Multi-Strategy Portfolio Optimization Report
Multi-Strategy Portfolio Optimization Report
# --- SciPy
from [Link] import skew, kurtosis
from [Link] import minimize
# ============================================================
# Install helper (works in script + notebook)
# ============================================================
def _pip_install(pkg: str):
subprocess.check_call([[Link], "-m", "pip", "-q", "install", "--
upgrade", pkg])
# --- Plotly
try:
import plotly # noqa: F401
import plotly.graph_objects as go
import [Link] as pio
except Exception:
_pip_install("plotly")
import plotly.graph_objects as go
import [Link] as pio
# --- yfinance
try:
import yfinance as yf
except Exception:
_pip_install("yfinance")
import yfinance as yf
# Renderer (Colab/Jupyter)
try:
[Link] = "colab"
except Exception:
pass
# ============================================================
# CONFIG (EDITE AQUI)
# ============================================================
STRATEGIES = [
{
"name": "Strategy 1",
"path": Path("Path 1"),
"start_balance": None,
},
{
"name": "Strategy 2",
"path": Path("Path 2"),
"start_balance": None,
},
{
"name": "Strategy N",
"path": Path("Path N"),
"start_balance": None,
},
]
BENCHMARKS = [
{"name": "ETH Buy&Hold", "ticker": "ETH-USD"},
{"name": "BTC Buy&Hold", "ticker": "BTC-USD"},
]
RF_ANNUAL = 0.00
ANN_DAYS_MAIN = 365.25 # cripto
ANN_DAYS_ALT = 252.0
# PORTFOLIO
PORTFOLIO_ENABLE = True
PORTFOLIO_NAME = "Portfolio"
PORTFOLIO_LONG_ONLY = True
PORTFOLIO_MAX_WEIGHT = 0.40
PORTFOLIO_REG_EPS = 1e-10
# ============================================================
# PDF EXPORT CONFIG (NOVO)
# ============================================================
SAVE_PDF = True # True para gerar PDF
PDF_OUT_PATH = Path("./report_full.pdf")
PDF_INCLUDE_PER_SERIES = True
PDF_INCLUDE_SIMULATIONS = True
PDF_TABLE_MAX_ROWS = 35
# Tema (Aqua/Purple)
AQUA = "#20D3D8"
PURPLE = "#6C4BFF"
DARK = "#111827"
SAVE_HTML = False
HTML_OUT_DIR = Path("./report_interactive_html")
HTML_OUT_DIR.mkdir(parents=True, exist_ok=True)
# ============================================================
# HELPERS: cores
# ============================================================
def gradient_colors(n: int, c1: str = PURPLE, c2: str = AQUA) -> List[str]:
if n <= 1:
return [c1]
r1, g1, b1 = _hex_to_rgb(c1)
r2, g2, b2 = _hex_to_rgb(c2)
out = []
for i in range(n):
t = i / (n - 1)
r = int(round(r1 + (r2 - r1) * t))
g = int(round(g1 + (g2 - g1) * t))
b = int(round(b1 + (b2 - b1) * t))
[Link](_rgb_to_hex((r, g, b)))
return out
# ============================================================
# DATE / TIME HELPERS
# ============================================================
# ============================================================
# UTIL – robustez de colunas / leitura
# ============================================================
def ensure_series(x):
if isinstance(x, [Link]):
return x
if isinstance(x, [Link]):
if [Link][1] == 1:
return [Link][:, 0]
raise ValueError(f"Esperava DataFrame 1 coluna, recebi {[Link][1]}")
return [Link]([Link](x).ravel())
def ok(c):
s = low[c]
return all(p in s for p in include) and not any(p in s for p in exclude)
exits = [Link][df[type_col].astype(str).[Link]().[Link]("exit",
na=False)].copy()
[Link](columns={dt_col: "Date/Time"}, inplace=True)
exits["Date/Time"] = pd.to_datetime(exits["Date/Time"], errors="coerce")
if cum_usdt_col:
exits["CumPnlUSDT"] = pd.to_numeric(exits[cum_usdt_col], errors="coerce")
ref_cum = None
if ("CumPnlUSDT" in [Link]) and initial_capital:
ref_cum = exits["CumPnlUSDT"] / float(initial_capital)
if cum_pct_col:
exits["CumRet"] = normalize_percent(exits[cum_pct_col],
reference_decimal=ref_cum)
keep = ["Date/Time"]
if "CumRet" in [Link]:
[Link]("CumRet")
if "CumPnlUSDT" in [Link]:
[Link]("CumPnlUSDT")
exits = exits[keep].dropna(subset=["Date/Time"])
exits = exits.sort_values("Date/Time", ignore_index=True)
return exits
# ============================================================
# DRAWDOWN
# ============================================================
# ============================================================
# METRICS
# ============================================================
mu_ann = float([Link]() / T)
rf_log = float([Link](1.0 + rf_annual)) if rf_annual != 0 else 0.0
final_bal = float([Link][-1])
cagr = (final_bal / float(start_balance)) ** (1.0 / years) - 1.0
ret_d = eq.pct_change().dropna()
return {
"Start Balance": float(start_balance),
"Final Balance": final_bal,
"Net Profit": final_bal - float(start_balance),
"Net % Gain": final_bal / float(start_balance) - 1.0,
"CAGR": cagr,
"Skewness": skewness,
"Kurtosis (excess)": kurt_excess,
"Days": int(len(ret_d)),
}
# ============================================================
# HEATMAP MENSAL (compound)
# ============================================================
m = monthly.to_frame("ret")
m["Year"] = [Link]
m["Month"] = [Link]
heat = [Link](index="Year", columns="Month", values="ret").fillna(0.0)
fig = [Link](
data=[Link](
z=z,
x=[str(m) for m in [Link]],
y=[str(y) for y in [Link]],
colorscale=[[0.0, PURPLE],[0.5, DARK],[1.0, AQUA]],
zmid=0.0,
text=text,
texttemplate="%{text}",
hovertemplate="Year=%{y}<br>Month=%{x}<br>Return=%{z:.2f}%<extra></
extra>"
)
)
fig.update_layout(
title=title,
xaxis_title="Month",
yaxis_title="Year",
template="plotly_white",
height=420,
)
return fig
# ============================================================
# BENCHMARK FETCH (yfinance)
# ============================================================
# ============================================================
# SIMULAÇÕES
# ============================================================
# ============================================================
# BUILD STRATEGY FROM EXCEL
# ============================================================
xls = [Link](file_path)
initial_cap_file = read_initial_capital(xls)
start_balance = (
start_balance_override if start_balance_override is not None
else (initial_cap_file if initial_cap_file is not None else 100.0)
)
sheet = guess_trade_sheet(xls)
raw = pd.read_excel(xls, sheet_name=sheet)
exits = extract_exit_trades(raw, initial_cap_file)
equity_daily = equity_exit.resample("D").ffill()
return dict(
sheet=sheet,
start_balance=float(start_balance),
initial_cap_file=initial_cap_file,
exits=exits,
equity_exit=equity_exit,
equity_daily=equity_daily,
trade_ret=trade_ret_arr,
metrics=metrics,
start_date=pd.to_datetime(equity_daily.[Link]()).normalize(),
end_date=pd.to_datetime(equity_daily.[Link]()).normalize(),
)
# ============================================================
# NORMALIZAÇÃO + EXTENSÃO
# ============================================================
s = s.sort_index()
# força daily
try:
s = [Link]("D").ffill()
except Exception:
[Link] = pd.to_datetime([Link]).normalize()
s = [Link](level=0).last().asfreq("D").ffill()
end_date = pd.to_datetime(end_date).normalize()
start_date = pd.to_datetime([Link]()).normalize()
# ============================================================
# FIGURES: equity/drawdown/days-in-dd
# ============================================================
hovertemplate="%{x|%Y-%m-%d}<br>Equity=%{y:,.2f}<extra>"+name+"</extra>"
))
fig.update_layout(
title=title,
xaxis_title="Date",
yaxis_title="Equity",
template="plotly_white",
hovermode="x unified",
height=520,
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="left",
x=0.0),
)
return fig
# ============================================================
# CORR HEATMAP (strategies)
# ============================================================
fig = [Link]([Link](
z=z,
x=[Link](),
y=[Link](),
zmin=-1.0,
zmax=1.0,
zmid=0.0,
colorscale=[[0.0, PURPLE],[0.5, DARK],[1.0, AQUA]],
text=text,
texttemplate="%{text}",
hovertemplate="A=%{y}<br>B=%{x}<br>Corr=%{z:.3f}<extra></extra>"
))
fig.update_layout(
title=title,
template="plotly_white",
height=max(420, 90 + 28 * len(corr)),
xaxis_title="Strategy",
yaxis_title="Strategy",
)
return fig
# ============================================================
# OPTIMIZERS (Log-returns + Shrinkage + Risk Parity + Markowitz)
# ============================================================
def risk_parity_weights(
cov_ann: [Link],
long_only: bool = True,
max_weight: float = 1.0,
reg_eps: float = 1e-10,
n_random_starts: int = 8,
seed: int = 42,
):
"""
Risk Parity robusto:
- minimiza (RC - target)^2
- multi-start (equal, inverse-vol, random)
- bounds + sum(w)=1
"""
cov = [Link](cov_ann, dtype=float)
n = [Link][0]
cov = cov + [Link](n) * float(reg_eps)
target = [Link](n) / n
if long_only:
bounds = [(0.0, float(max_weight))] * n
w_eq = [Link](n) / n
else:
bounds = [(-float(max_weight), float(max_weight))] * n
w_eq = [Link](n) / n
cons = [{"type": "eq", "fun": lambda w: [Link](w) - 1.0}]
def obj(w):
w = [Link](w, dtype=float)
if long_only:
w = [Link](w, 0.0, float(max_weight))
else:
w = [Link](w, -float(max_weight), float(max_weight))
s = [Link]()
if not [Link](s) or abs(s) < 1e-18:
w = w_eq
else:
w = w / s
rng_local = [Link].default_rng(seed)
if long_only:
for _ in range(n_random_starts):
w0 = rng_local.random(n)
w0 = w0 / [Link]()
w0 = [Link](w0, 0.0, float(max_weight))
w0 = w0 / [Link]()
[Link](w0)
else:
for _ in range(n_random_starts):
w0 = rng_local.normal(0, 1, size=n)
w0 = [Link](w0, -float(max_weight), float(max_weight))
s = [Link]()
w0 = (w0 / s) if abs(s) > 1e-18 else w_eq
[Link](w0)
if long_only:
w = [Link](w, 0.0, float(max_weight))
else:
w = [Link](w, -float(max_weight), float(max_weight))
s = [Link]()
w = (w / s) if ([Link](s) and abs(s) > 1e-18) else w_eq
val = obj(w)
if (best_val is None) or (val < best_val):
best_val, best_w, best_res = val, w, res
def markowitz_max_sharpe(
returns_df_log: [Link],
rf_annual: float,
ann_days: float,
long_only: bool = True,
max_weight: float = 1.0,
reg_eps: float = 1e-10,
shrinkage: float = 0.15,
):
"""
Max Sharpe using LOG-RETURNS for estimation + shrinkage covariance.
returns_df_log: daily log-returns (columns=strategies), aligned, no NaN
"""
R = returns_df_log.copy().dropna(how="any")
n = [Link][1]
if n == 0:
raise ValueError("returns_df_log vazio.")
if n == 1:
w = [Link]([1.0], dtype=float)
return w, mu_ann, cov_ann, None
def neg_sharpe(w):
w = [Link](w, dtype=float)
pret = float([Link](w, mu_ann))
pvol = float([Link](w @ cov_ann @ w))
if not [Link](pvol) or pvol <= 0:
return 1e9
return - (pret - rf_log) / pvol
if long_only:
bounds = [(0.0, float(max_weight))] * n
x0 = [Link](n) / n
else:
bounds = [(-float(max_weight), float(max_weight))] * n
x0 = [Link](n) / n
w = [Link](res.x, dtype=float)
if long_only:
w = [Link](w, 0.0, float(max_weight))
s = [Link]()
w = w / s if s > 0 else [Link](n) / n
# ============================================================
# TOP-3 DRAWDOWNS
# ============================================================
episodes = []
in_dd = False
start_i = trough_i = None
trough_val = 0.0
for t in [Link]:
val = float([Link][t])
at_peak = float([Link][t] - [Link][t]) >= -tol
elif in_dd:
if val < trough_val:
trough_val, trough_i = val, t
if at_peak:
[Link]({"depth": trough_val, "start": start_i, "trough":
trough_i, "end": t})
in_dd = False
start_i = trough_i = None
trough_val = 0.0
if in_dd:
[Link]({"depth": trough_val, "start": start_i, "trough": trough_i,
"end": [Link][-1]})
out = []
for k, e in enumerate(episodes, 1):
dur = int((pd.to_datetime(e["end"]) - pd.to_datetime(e["start"])).days)
[Link]({
"rank": k,
"depth_pct": float(e["depth"] * 100.0),
"start": pd.to_datetime(e["start"]).date(),
"trough": pd.to_datetime(e["trough"]).date(),
"end": pd.to_datetime(e["end"]).date(),
"duration_days": dur
})
return out
def top3_timeline_all(df_top3dd: [Link]) -> Optional[[Link]]:
df = df_top3dd.copy().dropna(subset=["start", "end", "trough", "depth_pct"])
if [Link]:
return None
df["start"] = pd.to_datetime(df["start"])
df["end"] = pd.to_datetime(df["end"])
df["trough"] = pd.to_datetime(df["trough"])
df["rank"] = df["rank"].astype(int)
series_order = sorted(df["Series"].unique().tolist())
y_map = {s: i for i, s in enumerate(series_order)}
fig = [Link]()
shown_rank_legend = set()
for _, r in [Link]():
series = r["Series"]
y = y_map[series]
rank = int(r["rank"])
col = RANK_COLORS.get(rank, PURPLE)
fig.add_trace([Link](
x=[r["start"], r["end"]],
y=[y, y],
mode="lines",
line=dict(color=col, width=10),
name=f"Rank {rank}",
legendgroup=f"rank{rank}",
showlegend=showleg,
hovertemplate=(
f"<b>{series}</b><br>"
f"Rank={rank}<br>"
f"Depth={r['depth_pct']:.2f}%<br>"
f"Start=%{{x|%Y-%m-%d}}<br>"
f"End=%{{x|%Y-%m-%d}}<extra></extra>"
)
))
fig.add_trace([Link](
x=df["trough"],
y=df["Series"].map(y_map),
mode="markers",
marker=dict(size=10, symbol="x", color=DARK),
name="Trough",
hovertemplate="<b>%{text}</b><br>Trough=%{x|%Y-%m-%d}<extra></extra>",
text=df["Series"]
))
fig.update_layout(
title="Top-3 Drawdowns — episódios (todas as séries)",
template="plotly_white",
height=max(520, 120 + 35 * len(series_order)),
xaxis_title="Date",
yaxis=dict(
title="Series",
tickmode="array",
tickvals=list(y_map.values()),
ticktext=list(y_map.keys()),
autorange="reversed"
),
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="left",
x=0.0),
)
return fig
df["start"] = pd.to_datetime(df["start"])
df["end"] = pd.to_datetime(df["end"])
df["trough"] = pd.to_datetime(df["trough"])
df["rank"] = df["rank"].astype(int)
fig = [Link]()
shown_rank_legend = set()
for _, r in [Link]():
rank = int(r["rank"])
col = RANK_COLORS.get(rank, PURPLE)
fig.add_trace([Link](
x=[r["start"], r["end"]],
y=[rank, rank],
mode="lines",
line=dict(color=col, width=10),
name=f"Rank {rank}",
legendgroup=f"rank{rank}",
showlegend=showleg,
hovertemplate=(
f"<b>{series}</b><br>"
f"Rank={rank}<br>"
f"Depth={r['depth_pct']:.2f}%<br>"
f"Start=%{{x|%Y-%m-%d}}<br>"
f"End=%{{x|%Y-%m-%d}}<extra></extra>"
)
))
fig.add_trace([Link](
x=d["trough"],
y=d["rank"],
mode="markers",
marker=dict(size=10, symbol="x", color=DARK),
name="Trough",
hovertemplate="<b>Trough</b><br>%{x|%Y-%m-%d}<extra></extra>",
))
fig.update_layout(
title=f"Top-3 Drawdowns — {series}",
template="plotly_white",
height=420,
xaxis_title="Date",
yaxis=dict(title="Rank (1 = pior)", autorange="reversed",
tickmode="array", tickvals=[1, 2, 3]),
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="left",
x=0.0),
)
figs[series] = fig
return figs
# ============================================================
# PDF EXPORT HELPERS (Plotly -> PNG -> ReportLab PDF)
# ============================================================
# tamanho dinâmico
nrows, ncols = [Link]
fig_w = min(18, 3 + 1.1 * ncols)
fig_h = min(10, 1.5 + 0.35 * nrows)
tbl = [Link](
cellText=[Link],
colLabels=[Link](),
loc="center",
cellLoc="center",
)
tbl.auto_set_font_size(False)
tbl.set_fontsize(font_size)
[Link](1.0, 1.15)
if title:
ax.set_title(title, fontsize=12, pad=12)
buf = BytesIO()
plt.tight_layout()
[Link](buf, format="png", dpi=200, bbox_inches="tight")
[Link](fig)
[Link](0)
return buf
# Header
[Link]("Helvetica-Bold", 13)
[Link](margin, page_h - margin, header)
# Footer
if footer:
[Link]("Helvetica", 9)
[Link](margin, margin * 0.6, footer)
# Image placement
img = ImageReader(img_buf)
iw, ih = [Link]()
x = (page_w - w) / 2
y = (page_h - h) / 2 - 0.2 * cm
[Link](img, x, y, width=w, height=h, preserveAspectRatio=True,
mask='auto')
[Link]()
def export_full_report_pdf(
out_path: Path,
report_end: [Link],
strategies: List[str],
benchmarks: List[str],
df_metrics: [Link],
all_equity: Dict[str, [Link]],
colors_map: Dict[str, str],
corr: Optional[[Link]] = None,
portfolio_weights: Optional[object] = None, # ✅ aceita Series OU dict[str,
Series/DataFrame]
df_top3dd: Optional[[Link]] = None,
sim_targets: Optional[List[dict]] = None,
include_per_series: bool = True,
include_simulations: bool = True,
max_table_rows: int = 35,
extra_tables: Optional[List[Tuple[str, [Link]]]] = None, # ✅ NOVO
):
import importlib
import [Link] as pio
import [Link]._kaleido as _kaleido
[Link](_kaleido)
[Link](pio)
out_path = Path(out_path)
out_path.[Link](parents=True, exist_ok=True)
c = [Link](str(out_path), pagesize=landscape(A4))
page_w, page_h = landscape(A4)
[Link]("Helvetica", 11)
now = [Link]().strftime("%Y-%m-%d %H:%M:%S")
[Link](margin, page_h - 3.0 * cm, f"Generated at: {now}")
[Link](margin, page_h - 3.7 * cm, f"Report end date:
{pd.to_datetime(report_end).date()}")
y = page_h - 5.0 * cm
[Link]("Helvetica-Bold", 12)
[Link](margin, y, "Strategies:")
y -= 0.6 * cm
[Link]("Helvetica", 11)
for s in strategies:
[Link](margin + 0.5 * cm, y, f"• {s}")
y -= 0.55 * cm
if y < 2.0 * cm:
[Link]()
y = page_h - 2.0 * cm
[Link]("Helvetica-Bold", 12)
[Link](margin, y, "Benchmarks:")
y -= 0.6 * cm
[Link]("Helvetica", 11)
for b in benchmarks:
[Link](margin + 0.5 * cm, y, f"• {b}")
y -= 0.55 * cm
if y < 2.0 * cm:
[Link]()
y = page_h - 2.0 * cm
[Link]()
if isinstance(portfolio_weights, [Link]):
weights_dict = {"portfolio": portfolio_weights}
elif isinstance(portfolio_weights, dict):
weights_dict = portfolio_weights
if weights_dict:
for label, w in weights_dict.items():
if w is None:
continue
if isinstance(w, [Link]):
wdf = w.to_frame("weight")
elif isinstance(w, [Link]):
wdf = [Link]()
else:
continue
fig_top_all = top3_timeline_all(df_top3dd)
if fig_top_all is not None:
_add_image_page(c, _fig_to_png_bytes(fig_top_all, width=1600,
height=900, scale=2), header="Top-3 Drawdowns (Timeline)", footer=footer)
if len(ret_arr) == 0:
continue
fig_final = overlay_hist_fig(
mc_final, bs_final,
title=f"Final Equity (Overlay) — {nm}",
name_a="Monte Carlo (perturbed)", name_b="Bootstrap (resample)",
color_a=PURPLE, color_b=AQUA,
x_title="Final Equity"
)
_add_image_page(c, _fig_to_png_bytes(fig_final), header=f"Simulations —
Final Equity — {nm}", footer=footer)
fig_ddo = overlay_hist_fig(
mc_dd * 100.0, bs_dd * 100.0,
title=f"Max Drawdown (%) (Overlay) — {nm}",
name_a="Monte Carlo (perturbed)", name_b="Bootstrap (resample)",
color_a=PURPLE, color_b=AQUA,
x_title="Max DD (%)"
)
_add_image_page(c, _fig_to_png_bytes(fig_ddo), header=f"Simulations —
Max DD — {nm}", footer=footer)
# ---- EXTRA TABLES (overlap, weekly corr, spearman, etc.)
if extra_tables:
for title, df in extra_tables:
if df is None or (not isinstance(df, [Link])) or [Link]:
continue
for k, chunk in enumerate(_split_df(df, max_table_rows), 1):
img = _df_to_png_bytes(chunk, title=f"{title} (page {k})",
max_rows=max_table_rows, font_size=7)
_add_image_page(c, img, header=title, footer=footer)
[Link]()
# ============================================================
# MAIN
# ============================================================
# 1) Carregar estratégias
strategy_ctxs = []
for s in STRATEGIES:
ctx = build_strategy_from_excel(s["path"], [Link]("start_balance", None))
ctx["name"] = s["name"]
ctx["path"] = s["path"]
strategy_ctxs.append(ctx)
strategy_names = []
for c in strategy_ctxs:
name = c["name"]
eq = c["equity_daily"]
if NORMALIZE_ALL_TO_BASE:
eq = rebase_to(eq, BASE_VALUE)
eq = extend_daily_to(eq, REPORT_END_DATE)
all_equity[name] = eq
strategy_names.append(name)
series_meta[name] = {
"Type": "Strategy",
"Trades (EXIT)": c["metrics"].get("Trades (EXIT)", [Link]),
"Sharpe (event-time)": c["metrics"].get("Sharpe (event-time)", [Link]),
"Max Drawdown (event/exits)": c["metrics"].get("Max Drawdown
(event/exits)", [Link]),
}
for b in BENCHMARKS:
name = b["name"]
if name in benchmark_series:
eq = benchmark_series[name]
if NORMALIZE_ALL_TO_BASE:
eq = rebase_to(eq, BASE_VALUE)
eq = extend_daily_to(eq, REPORT_END_DATE)
all_equity[name] = eq
series_meta[name] = {
"Type": "Benchmark",
"Ticker": b["ticker"],
"Trades (EXIT)": [Link],
"Sharpe (event-time)": [Link],
"Max Drawdown (event/exits)": [Link],
}
# ============================================================
# PORTFOLIO (Strategies only)
# - estimation uses LOG-returns + shrinkage
# - baseline: Risk Parity
# - aggressive: Markowitz max Sharpe
# - equity built with arithmetic returns (rebalance daily)
# ============================================================
# ============================================================
# PORTFOLIOS (Strategies only) — build BOTH:
# 1) Risk Parity (weight risk)
# 2) Max Sharpe (sharpe puro) [Markowitz]
# ============================================================
ret_arith = eq_mat.pct_change().iloc[1:].dropna(how="any")
ret_log = [Link](eq_mat / eq_mat.shift(1)).iloc[1:].dropna(how="any")
corr = ret_log.corr()
print("\n🔗 Matriz de correlação (estratégias) — LOG-retornos diários (após
extensão até hoje):")
display(corr)
# -------------------------
# 1) RISK PARITY (weight risk)
# -------------------------
w_rp, res_rp = risk_parity_weights(
cov_ann,
long_only=PORTFOLIO_LONG_ONLY,
max_weight=PORTFOLIO_MAX_WEIGHT,
reg_eps=PORTFOLIO_REG_EPS,
n_random_starts=10,
seed=42
)
w_rp_s = [Link](w_rp, index=ret_log.columns,
name="weight_risk_parity").sort_values(ascending=False)
print("\n⚖️ Risk Parity — Pesos (weight risk) [cov shrinkage + cap]:")
display(w_rp_s.to_frame())
# -------------------------
# 2) MAX SHARPE (sharpe puro) — Markowitz
# -------------------------
w_mw, mu_ann, cov_ann_mw, res_mw = markowitz_max_sharpe(
ret_log,
rf_annual=RF_ANNUAL,
ann_days=ANN_DAYS_MAIN,
long_only=PORTFOLIO_LONG_ONLY,
max_weight=PORTFOLIO_MAX_WEIGHT,
reg_eps=PORTFOLIO_REG_EPS,
shrinkage=PORTFOLIO_SHRINKAGE
)
w_mw_s = [Link](w_mw, index=ret_log.columns,
name="weight_max_sharpe").sort_values(ascending=False)
summary_mw = [Link]([{
"Expected Return (ann, log)": pret_ann_mw,
"Expected Vol (ann, log)": pvol_ann_mw,
"Expected Sharpe (ann, log)": psharpe_mw,
"Shrinkage": PORTFOLIO_SHRINKAGE,
"Long-only": bool(PORTFOLIO_LONG_ONLY),
"Max weight": float(PORTFOLIO_MAX_WEIGHT),
"Days used": int(len(ret_log)),
"Portfolio Start": port_start.date(),
"Portfolio End": port_end.date(),
}])
print("\n📌 Resumo (Max Sharpe — estimado via mean/var de LOG-retornos):")
display(summary_mw)
# -------------------------
# Helper: construir equity a partir dos pesos (arithmetic daily rebalance)
# -------------------------
def _build_portfolio_eq(w: [Link], label: str):
w = [Link](w, dtype=float)
pr = ret_arith.dot(w) # Series index=ret_arith.index
# -------------------------
# Build BOTH portfolios
# -------------------------
name_rp, eq_rp, pr_rp = _build_portfolio_eq(w_rp, "risk_parity")
name_ms, eq_ms, pr_ms = _build_portfolio_eq(w_mw, "max_sharpe")
# guardar nomes
PORTFOLIO_SERIES_NAMES = [name_rp, name_ms]
portfolio_daily_returns["risk_parity"] = pr_rp
portfolio_daily_returns["max_sharpe"] = pr_ms
all_equity[name_ms] = eq_ms
series_meta[name_ms] = {"Type": "Portfolio", "Ticker": "", "Trades (EXIT)":
[Link], "Sharpe (event-time)": [Link], "Max Drawdown (event/exits)": [Link]}
# ============================================================
# (1) METRICS TABLE (inclui portfolio)
# ============================================================
metrics_rows = []
for name, eq in all_equity.items():
eq = ensure_series(eq).dropna()
if [Link]:
continue
start_bal = float([Link][0])
m = metrics_from_daily_equity(eq, start_bal, rf_annual=RF_ANNUAL)
df_metrics = [Link](metrics_rows)
cols_order = [
"Series", "Type", "Ticker", "Trades (EXIT)",
"Start Balance", "Final Balance", "Net Profit", "Net % Gain", "CAGR",
"Sharpe (365)", "Sortino (365)", "Volatility (365)",
"Sharpe (252)", "Sortino (252)", "Volatility (252)",
"Max Drawdown", "Max Drawdown %", "Max Drawdown (event/exits)",
"MAR (Calmar)",
"Skewness", "Kurtosis (excess)", "Days",
"Sharpe (event-time)"
]
df_metrics = df_metrics[[c for c in cols_order if c in df_metrics.columns]]
# ============================================================
# (2) EQUITY / DD / DAYS IN DD
# ============================================================
fig_eq_all = equity_figure(
all_equity,
colors_map,
title=f"Equity Curves (Overlay) — {('Rebased to '+str(BASE_VALUE)) if
NORMALIZE_ALL_TO_BASE else 'Raw'} — até {REPORT_END_DATE.date()}"
)
fig_eq_all.show()
if SAVE_HTML:
fig_eq_all.write_html(str(HTML_OUT_DIR / "equity_overlay.html"))
# ============================================================
# (3) HEATMAP MENSAL — por série (inclui portfolio)
# ============================================================
top3_rows = []
for name, eq in all_equity.items():
eps = top_n_drawdowns_from_equity(eq, n=3)
if not eps:
top3_rows.append({"Series": name, "rank": 1, "depth_pct": [Link], "start":
None, "trough": None, "end": None, "duration_days": None})
continue
for e in eps:
top3_rows.append({"Series": name, **e})
fig_top3_all = top3_timeline_all(df_top3dd)
if fig_top3_all is not None:
fig_top3_all.show()
if SAVE_HTML:
fig_top3_all.write_html(str(HTML_OUT_DIR /
"top3_drawdowns_timeline_all.html"))
figs_top3 = top3_timeline_per_series(df_top3dd)
for series, fig in figs_top3.items():
[Link]()
if SAVE_HTML:
safe = "".join(ch if [Link]() or ch in " _-" else "_" for ch in
series).strip().replace(" ", "_")
fig.write_html(str(HTML_OUT_DIR / f"top3_drawdowns_timeline_{safe}.html"))
# ============================================================
# (5) SIMULAÇÕES — estratégias + portfolio
# - estratégias: trade_ret (event returns)
# - portfolio: daily returns (porque não tem "trades")
# ============================================================
sim_targets = []
for c in strategy_ctxs:
sim_targets.append({
"name": c["name"],
"returns": [Link](c["trade_ret"], dtype=float),
"start": float(BASE_VALUE if NORMALIZE_ALL_TO_BASE else
c["start_balance"]),
"kind": "strategy-trade-returns"
})
if (PORTFOLIO_SERIES_NAME in all_equity):
port_ret_full =
all_equity[PORTFOLIO_SERIES_NAME].pct_change().fillna(0.0).values
sim_targets.append({
"name": PORTFOLIO_SERIES_NAME,
"returns": [Link](port_ret_full, dtype=float),
"start": float(BASE_VALUE),
"kind": "portfolio-daily-returns"
})
for tgt in sim_targets:
name = tgt["name"]
ret_arr = tgt["returns"]
start_for_sim = tgt["start"]
if len(ret_arr) == 0:
print(f"[WARN] Sem retornos para simulação: {name}")
continue
fig_overlay_final = overlay_hist_fig(
mc_final, bs_final,
title=f"Final Equity (Overlay) — {name}",
name_a="Monte Carlo (perturbed)", name_b="Bootstrap (resample)",
color_a=PURPLE, color_b=AQUA,
x_title="Final Equity"
)
fig_overlay_final.show()
fig_overlay_dd = overlay_hist_fig(
mc_dd * 100.0, bs_dd * 100.0,
title=f"Max Drawdown (%) (Overlay) — {name}",
name_a="Monte Carlo (perturbed)", name_b="Bootstrap (resample)",
color_a=PURPLE, color_b=AQUA,
x_title="Max DD (%)"
)
fig_overlay_dd.show()
# ============================================================
# EXTRA CORRELATION ANALYSES (fixed)
# ============================================================
np.fill_diagonal([Link], 1.0)
return corr, nobs
# 3) Semanal (composto)
R_week = (1 + R).resample("W").prod() - 1
print("\nSemanas no cálculo:", len(R_week))
corr_week = R_week.corr()
print("\nCorrelação semanal (retorno composto):")
display(corr_week)
# 4) Spearman
corr_spear = [Link](method="spearman")
print("\nSpearman diário:")
display(corr_spear)
else:
print("\n[WARN] returns_df não disponível (portfolio pode estar desabilitado ou
sem dados). Pulando correlações extras.")
if "w_rp_s" in globals():
extra_tables_pdf.append(("Weights — Risk Parity", w_rp_s.to_frame()))
if "w_mw_s" in globals():
extra_tables_pdf.append(("Weights — Max Sharpe", w_mw_s.to_frame()))
if "rc_rp_s" in globals():
extra_tables_pdf.append(("Risk Contributions — Risk Parity",
rc_rp_s.to_frame()))
if "summary_mw" in globals():
extra_tables_pdf.append(("Max Sharpe — Summary", summary_mw))
if PORTFOLIO_SERIES_NAME in all_equity:
pr = all_equity[PORTFOLIO_SERIES_NAME]
print(f"Range (portfolio): {[Link]().date()} → {[Link]().date()}")
if SAVE_HTML:
print("HTMLs salvos em:", HTML_OUT_DIR)
if PORTFOLIO_NAME in all_equity:
pr = all_equity[PORTFOLIO_NAME]
print(f"Range (portfolio): {[Link]().date()} → {[Link]().date()}")
if SAVE_HTML:
print("HTMLs salvos em:", HTML_OUT_DIR)
if SAVE_PDF:
export_full_report_pdf(
out_path=PDF_OUT_PATH,
report_end=REPORT_END_DATE,
strategies=strategy_names,
benchmarks=[b["name"] for b in BENCHMARKS if b["name"] in all_equity],
df_metrics=df_metrics,
all_equity=all_equity,
colors_map=colors_map,
corr=corr if "corr" in globals() else None,
portfolio_weights=portfolio_weights if "portfolio_weights" in globals()
else None,
df_top3dd=df_top3dd if "df_top3dd" in globals() else None,
sim_targets=sim_targets if "sim_targets" in globals() else None,
include_per_series=PDF_INCLUDE_PER_SERIES,
include_simulations=PDF_INCLUDE_SIMULATIONS,
max_table_rows=PDF_TABLE_MAX_ROWS,
extra_tables=extra_tables_pdf if "extra_tables_pdf" in globals() else None,
)
print("✅ PDF salvo em:", PDF_OUT_PATH)