def max_drawdown(ret):
"""Drawdown máximo de una serie mensual de rendimientos SIMPLES."""
wealth = (1 + ret).cumprod()
return (wealth / wealth.cummax() - 1).min()
def hf_stats(ret, rf=rf_s, mkt=Rs["SPY"]):
"""Estadísticas de una serie mensual de rendimientos SIMPLES."""
rf, mkt = rf.loc[ret.index], mkt.loc[ret.index]
ex = ret - rf # prima simple
return pd.Series({
"Ann. return": ann_ret(ret), # rendimiento anual compuesto
"Ann. vol": ret.std() * np.sqrt(12),
"Sharpe": ex.mean() / ex.std() * np.sqrt(12),
"Beta": np.cov(ret, mkt)[0, 1] / mkt.var(),
"Corr": ret.corr(mkt),
"Skew": stats.skew(ret),
"Ex. kurt": stats.kurtosis(ret),
"AC(1)": ret.autocorr(1),
"Max DD": max_drawdown(ret)})
# --- Style analysis (Parte I) ---------------------------------------------
X_style = Rs[STYLES].copy()
X_style["CASH"] = rf_s
def style_analysis(fund, X=X_style):
y, Xv = fund.values * 100, X.values * 100
K = Xv.shape[1]
te = lambda w: np.var(y - Xv @ w)
res = optimize.minimize(te, np.repeat(1/K, K), method="SLSQP", bounds=[(0, 1)] * K,
constraints=[{"type": "eq", "fun": lambda w: w.sum() - 1}],
options={"ftol": 1e-12, "maxiter": 1000})
w = pd.Series(res.x, index=X.columns)
e = fund - X @ w
return w, 1 - e.var() / fund.var(), e
def hf_style(fund, styles=STYLES, rets=Rs, rf=rf_s):
d = rets[[fund] + styles].sub(rf, axis=0)
return sm.OLS(d[fund], sm.add_constant(d[styles])).fit()
# --- Futuros del IPC y coberturas (Parte II) ---------------------------------
q_ipc = 0.025
fut_ipc = Rs["IPC"] - (i_mx - q_ipc) / 12
def hedge_report(df, market=fut_ipc):
out = {}
for c in df:
x = df[c]
out[c] = {"Annual return": ann_ret(x), "Annual vol": x.std() * np.sqrt(12),
"Beta (IPC)": np.cov(x, market)[0, 1] / market.var(),
"Corr (IPC)": x.corr(market)}
return pd.DataFrame(out).T
def pa_report(pa, bench):
act = pa - bench
return pd.DataFrame({
"Annual return": [ann_ret(bench), ann_ret(pa)],
"Annual vol": [bench.std()*np.sqrt(12), pa.std()*np.sqrt(12)],
"Tracking error": [0, act.std()*np.sqrt(12)],
"Info ratio": [np.nan, act.mean()/act.std()*np.sqrt(12)]},
index=["Benchmark", "Portable alpha"])
# --- Carry trade (Parte III) ------------------------------------------------
S = prices["USDMXN"]
carry = pd.DataFrame(index=r.index)
carry["Interest differential"] = (i_mx - i_us) / 12
carry["Currency return"] = (S.shift(1) / S).loc[r.index] - 1
carry["Carry trade"] = (1 + i_mx/12) * (S.shift(1) / S).loc[r.index] - (1 + i_us/12)
# --- Treynor-Black y Black-Litterman (Parte V) -------------------------------
MKT_PREMIUM = 0.055
sigma_M = mkt_ex.std() * np.sqrt(12)
def index_model(f):
m = sm.OLS(Rs[f] - rf_s, sm.add_constant(mkt_ex)).fit()
return pd.Series({"alpha": m.params.iloc[0]*12, "alpha SE": m.bse.iloc[0]*12,
"beta": m.params.iloc[1], "resid var": m.resid.var()*12})
def treynor_black(alpha, beta, resid_var, mkt_prem=MKT_PREMIUM, sig_m=sigma_M):
w = (alpha / resid_var) / (alpha / resid_var).sum()
a_A, b_A, s2e_A = w @ alpha, w @ beta, (w**2) @ resid_var
wA0 = (a_A / s2e_A) / (mkt_prem / sig_m**2)
wA = wA0 / (1 + (1 - b_A) * wA0)
S_M, IR_A = mkt_prem / sig_m, a_A / np.sqrt(s2e_A)
return w, pd.Series({"alpha_A": a_A, "beta_A": b_A, "sigma(e_A)": np.sqrt(s2e_A),
"IR_A": IR_A, "w_A (initial)": wA0, "w_A* (adjusted)": wA,
"Sharpe market": S_M, "Sharpe optimal": np.sqrt(S_M**2 + IR_A**2)})
fx = Rs["USDMXN"]
to_mxn = lambda x: (1 + x) * (1 + fx) - 1
hf_sleeve = Rs[["QMNIX", "QSPIX", "AQMIX", "MERFX"]].mean(axis=1)
fo = pd.DataFrame({"MX_EQ": to_mxn(Rs["EWW"]), "US_EQ": to_mxn(Rs["SPY"]),
"INTL_EQ": to_mxn(Rs["EFA"]), "UST": to_mxn(Rs["IEF"]),
"GOLD": to_mxn(Rs["GLD"]), "HF": to_mxn(hf_sleeve)})
fo_ex = fo.sub(i_mx / 12, axis=0)
ASSETS_FO = list(fo.columns)
Sigma = fo_ex.cov() * 12
w_eq = pd.Series({"MX_EQ": 0.25, "US_EQ": 0.30, "INTL_EQ": 0.10,
"UST": 0.20, "GOLD": 0.05, "HF": 0.10})
DELTA = 0.04 / (w_eq @ Sigma @ w_eq)
Pi = DELTA * Sigma @ w_eq
TAU = 0.05
def black_litterman(Sigma, Pi, P, Q, tau=TAU, conf=1.0):
Sm, Pm = Sigma.values, P.values
Omega = np.diag(np.diag(Pm @ (tau * Sm) @ Pm.T)) / conf
A = np.linalg.inv(tau * Sm) + Pm.T @ np.linalg.inv(Omega) @ Pm
b = np.linalg.inv(tau * Sm) @ Pi.values + Pm.T @ np.linalg.inv(Omega) @ Q.values
mu = np.linalg.solve(A, b)
return (pd.Series(mu, index=Sigma.index),
pd.DataFrame(Sm + np.linalg.inv(A), index=Sigma.index, columns=Sigma.index))
def constrained_mv(mu, S, delta=DELTA, caps={"HF": 0.15}):
n = len(mu)
obj = lambda w: -(w @ mu.values - 0.5 * delta * w @ S.values @ w)
res = optimize.minimize(obj, np.repeat(1/n, n), method="SLSQP",
bounds=[(0, caps.get(a, 1)) for a in mu.index],
constraints=[{"type": "ineq", "fun": lambda w: 1 - w.sum()}])
return pd.Series(res.x, index=mu.index)