from __future__ import annotations import importlib.util import sys from dataclasses import dataclass from html import escape from pathlib import Path import pandas as pd ROUNDTRIP_COST_ON_MARGIN = 0.0021 COST_SCENARIOS = ( ("maker_maker", 0.0012), ("maker_taker", 0.0021), ("taker_taker", 0.0030), ) REPORT_FILE = Path("ultrashort-recent-report.html") REPORT_DIR = Path("reports/ultrashort") HORIZONS = ( ("3y", pd.DateOffset(years=3)), ("1y", pd.DateOffset(years=1)), ("6m", pd.DateOffset(months=6)), ("3m", pd.DateOffset(months=3)), ) @dataclass(frozen=True) class StrategySpec: label: str symbol: str bar: str candidate: object is_pair: bool = False def load_explore_module(): path = Path(__file__).resolve().with_name("explore_ultrashort.py") spec = importlib.util.spec_from_file_location("explore_ultrashort", path) if spec is None or spec.loader is None: raise RuntimeError("cannot load explore_ultrashort.py") module = importlib.util.module_from_spec(spec) sys.modules[spec.name] = module spec.loader.exec_module(module) return module def load_cached_history(explore, symbol: str, bar: str, requested_bars: int): candles, _ = explore.load_cached_candles(explore.CANDLE_CACHE_DIR, symbol, bar) if not candles: raise RuntimeError(f"missing cached candles: {symbol} {bar}") return candles[-requested_bars:] if len(candles) > requested_bars else candles def pct(value: float) -> str: return f"{value * 100:.2f}%" def money(value: float) -> str: return f"{value:,.0f}" def normalize_equity(frame: pd.DataFrame, cutoff: pd.Timestamp) -> pd.DataFrame: recent = frame[frame["ts"] >= cutoff][["ts", "equity"]].copy() if recent.empty: recent = frame[["ts", "equity"]].copy() recent["equity"] = recent["equity"] / float(recent["equity"].iloc[0]) * 10_000.0 return recent def monthly_from_equity(frame: pd.DataFrame) -> pd.DataFrame: month_end = frame.set_index("ts")["equity"].resample("ME").last().ffill() month_start = month_end.shift(1) if len(month_end): month_start.iloc[0] = float(frame["equity"].iloc[0]) monthly = pd.DataFrame( { "period": month_end.index.tz_localize(None).to_period("M").astype(str), "return": month_end.to_numpy() / month_start.to_numpy() - 1.0, "end_equity": month_end.to_numpy(), } ) return monthly def combine_equities(frames: list[pd.DataFrame]) -> pd.DataFrame: combined = pd.concat( [ frame.set_index("ts")["equity"].rename(str(index)) for index, frame in enumerate(frames) ], axis=1, sort=True, ).sort_index().ffill().dropna() return pd.DataFrame({"ts": combined.index, "equity": combined.sum(axis=1).to_numpy()}) def daily_return_frame(equities: dict[str, pd.DataFrame]) -> pd.DataFrame: combined = pd.concat( [ frame.set_index("ts")["equity"].rename(name) for name, frame in equities.items() ], axis=1, sort=True, ).sort_index().ffill().dropna() return combined.pct_change().dropna() def apply_drawdown_overlay(frame: pd.DataFrame, threshold: float, reduced_exposure: float) -> pd.DataFrame: raw = frame.sort_values("ts").reset_index(drop=True) values = [float(raw["equity"].iloc[0])] raw_peak = float(raw["equity"].iloc[0]) for index in range(1, len(raw)): previous_raw_equity = float(raw["equity"].iloc[index - 1]) raw_peak = max(raw_peak, previous_raw_equity) previous_drawdown = raw_peak / previous_raw_equity - 1.0 if previous_raw_equity else 0.0 exposure = reduced_exposure if previous_drawdown >= threshold else 1.0 raw_return = float(raw["equity"].iloc[index]) / previous_raw_equity - 1.0 values.append(values[-1] * (1.0 + raw_return * exposure)) return pd.DataFrame({"ts": raw["ts"], "equity": values}) def yearly_from_equity(label: str, frame: pd.DataFrame) -> list[dict[str, object]]: year_end = frame.set_index("ts")["equity"].resample("YE").last().ffill() year_start = year_end.shift(1) if len(year_end): year_start.iloc[0] = float(frame["equity"].iloc[0]) return [ { "name": label, "year": str(index.year), "return": pct(float(end / start - 1.0)), "end_equity": money(float(end)), } for index, start, end in zip(year_end.index, year_start.to_numpy(), year_end.to_numpy()) ] def horizon_rows(explore, label: str, kind: str, frame: pd.DataFrame) -> list[dict[str, object]]: last_ts = int(frame["ts"].iloc[-1].timestamp() * 1000) horizons = explore.recent_horizon_metrics_from_equity(frame, last_ts, HORIZONS) return [ { kind: label, "horizon": row["horizon"], "return": pct(float(row["net_total_return"])), "annualized": pct(float(row["net_annualized_return"])), "max_dd": pct(float(row["net_max_drawdown"])), "calmar": f"{float(row['net_calmar']):.2f}", } for row in horizons.to_dict("records") ] def make_svg(curves: list[dict[str, object]]) -> str: width = 1200 height = 460 left = 58 top = 28 right = 24 bottom = 44 plot_width = width - left - right plot_height = height - top - bottom all_points = [point for curve in curves for point in curve["points"]] min_ts = min(ts for ts, _ in all_points) max_ts = max(ts for ts, _ in all_points) min_value = min(value for _, value in all_points) max_value = max(value for _, value in all_points) if max_value == min_value: max_value += 1.0 def x(ts: float) -> float: return left + (ts - min_ts) / (max_ts - min_ts) * plot_width def y(value: float) -> float: return top + (max_value - value) / (max_value - min_value) * plot_height grid = [] for i in range(5): yy = top + i * plot_height / 4 value = max_value - i * (max_value - min_value) / 4 grid.append(f'') grid.append(f'{money(value)}') paths = [] for curve in curves: points = " ".join(f"{x(ts):.1f},{y(value):.1f}" for ts, value in curve["points"]) paths.append(f'') legend = [] legend_x = left legend_y = height - 18 for curve in curves: legend.append(f'') legend.append(f'{escape(str(curve["label"]))}') legend_x += 170 return f""" {''.join(grid)} {''.join(paths)} {''.join(legend)} """ def render_table(frame: pd.DataFrame, columns: list[str]) -> str: header = "".join(f"{escape(column)}" for column in columns) rows = [] for row in frame.to_dict("records"): cells = "".join(f"{escape(str(row[column]))}" for column in columns) rows.append(f"{cells}") return f"{header}{''.join(rows)}
" def main() -> int: explore = load_explore_module() specs = [ StrategySpec("BTC RSI2 Guarded 15m", "BTC-USDT-SWAP", "15m", explore.build_rsi2_long_guarded_candidate(240, 2.0, 55.0, 0.008, 48)), StrategySpec("BTC RSI2 TWAP2 15m", "BTC-USDT-SWAP", "15m", explore.build_rsi2_long_guarded_twap_candidate(240, 2.0, 55.0, 0.008, 48, 2)), StrategySpec("BTC RSI2 TWAP3 15m", "BTC-USDT-SWAP", "15m", explore.build_rsi2_long_guarded_twap_candidate(240, 2.0, 55.0, 0.008, 48, 3)), StrategySpec("BTC RSI2 Price TWAP shallow 15m", "BTC-USDT-SWAP", "15m", explore.build_rsi2_long_guarded_price_twap_candidate(240, 2.0, 55.0, 0.008, 48, (0.0005, 0.0015, 0.003), 2)), StrategySpec("BTC RSI2 Price TWAP mid 15m", "BTC-USDT-SWAP", "15m", explore.build_rsi2_long_guarded_price_twap_candidate(240, 2.0, 55.0, 0.008, 48, (0.001, 0.003, 0.005), 2)), StrategySpec("BTC RSI2 Price TWAP deep 15m", "BTC-USDT-SWAP", "15m", explore.build_rsi2_long_guarded_price_twap_candidate(240, 2.0, 55.0, 0.008, 48, (0.002, 0.005, 0.008), 3)), StrategySpec("BTC RSI2 Price TWAP 2slice 15m", "BTC-USDT-SWAP", "15m", explore.build_rsi2_long_guarded_price_twap_candidate(240, 2.0, 55.0, 0.008, 48, (0.001, 0.004), 2)), StrategySpec("BTC RSI2 Price TWAP mid fb2 15m", "BTC-USDT-SWAP", "15m", explore.build_rsi2_long_guarded_price_twap_candidate(240, 2.0, 55.0, 0.008, 48, (0.001, 0.003, 0.005), 2, 0.0002)), StrategySpec("BTC RSI2 Price TWAP mid fb5 15m", "BTC-USDT-SWAP", "15m", explore.build_rsi2_long_guarded_price_twap_candidate(240, 2.0, 55.0, 0.008, 48, (0.001, 0.003, 0.005), 2, 0.0005)), StrategySpec("BTC RSI2 Price TWAP deep fb2 15m", "BTC-USDT-SWAP", "15m", explore.build_rsi2_long_guarded_price_twap_candidate(240, 2.0, 55.0, 0.008, 48, (0.002, 0.005, 0.008), 3, 0.0002)), StrategySpec("BTC RSI2 Price TWAP deep fb5 15m", "BTC-USDT-SWAP", "15m", explore.build_rsi2_long_guarded_price_twap_candidate(240, 2.0, 55.0, 0.008, 48, (0.002, 0.005, 0.008), 3, 0.0005)), StrategySpec("BTC Trend RSI-BB 15m", "BTC-USDT-SWAP", "15m", explore.build_trend_rsi_bb_long_candidate(240, 20, 2.5, 5.0, 55.0, 0.008)), StrategySpec("ETH RSI2 15m", "ETH-USDT-SWAP", "15m", explore.build_rsi2_side_candidate(50, 3.0, 97.0, 55.0, "long")), StrategySpec("ETH RSI2 Price TWAP mid 15m", "ETH-USDT-SWAP", "15m", explore.build_rsi2_long_guarded_price_twap_candidate(50, 3.0, 55.0, 0.008, 48, (0.001, 0.003, 0.005), 2)), StrategySpec("ETH RSI2 Price TWAP deep 15m", "ETH-USDT-SWAP", "15m", explore.build_rsi2_long_guarded_price_twap_candidate(50, 3.0, 55.0, 0.008, 48, (0.002, 0.005, 0.008), 3)), StrategySpec("ETH RSI2 Price TWAP mid fb2 15m", "ETH-USDT-SWAP", "15m", explore.build_rsi2_long_guarded_price_twap_candidate(50, 3.0, 55.0, 0.008, 48, (0.001, 0.003, 0.005), 2, 0.0002)), StrategySpec("ETH Robust Price TWAP 15m", "ETH-USDT-SWAP", "15m", explore.build_rsi2_long_guarded_price_twap_candidate(60, 3.0, 50.0, 0.012, 48, (0.003, 0.006, 0.009), 4, 0.0)), StrategySpec("ETH Trend RSI-BB 15m", "ETH-USDT-SWAP", "15m", explore.build_trend_rsi_bb_long_candidate(480, 20, 2.0, 5.0, 45.0, 0.005)), StrategySpec("ETH/BTC RSI Filter 15m", "ETH-USDT-SWAP", "15m", explore.build_eth_btc_rsi_filter_candidate(50, 3.0, 55.0, 480, 240, 0.0), True), StrategySpec("BTC Lead ETH Lag 15m", "ETH-USDT-SWAP", "15m", explore.build_btc_lead_eth_lag_candidate(16, 0.024, 0.006, 32, 0.008, 0.018), True), StrategySpec("BTC Lead ETH Lag 5m", "ETH-USDT-SWAP", "5m", explore.build_btc_lead_eth_lag_candidate(16, 0.018, 0.006, 32, 0.006, 0.018), True), StrategySpec("BTC Lead ETH Lag 3m", "ETH-USDT-SWAP", "3m", explore.build_btc_lead_eth_lag_candidate(8, 0.012, 0.006, 32, 0.006, 0.012), True), ] colors = ["#2563eb", "#16a34a", "#0f766e", "#65a30d", "#84cc16", "#ca8a04", "#f97316", "#a3e635", "#bef264", "#facc15", "#fde047", "#dc2626", "#059669", "#22c55e", "#10b981", "#14b8a6", "#7c3aed", "#ea580c", "#0891b2", "#be123c", "#4b5563", "#9333ea"] result_rows: list[dict[str, object]] = [] monthly_rows: list[dict[str, object]] = [] monthly_raw_rows: list[dict[str, object]] = [] curves: list[dict[str, object]] = [] recent_equities: dict[str, pd.DataFrame] = {} yearly_rows: list[dict[str, object]] = [] strategy_horizon_rows: list[dict[str, object]] = [] cost_scenario_rows: list[dict[str, object]] = [] for index, spec in enumerate(specs): requested_bars = explore.history_bars_for_years(spec.bar, 10.0) candles = load_cached_history(explore, spec.symbol, spec.bar, requested_bars) if spec.is_pair: btc = load_cached_history(explore, "BTC-USDT-SWAP", spec.bar, requested_bars) candles, btc = explore.align_pair_candles(candles, btc) result = spec.candidate.run(eth_candles=candles, btc_candles=btc, leverage=explore.LEVERAGE, warmup_bars=spec.candidate.warmup_bars) else: result = spec.candidate.run(candles=candles, leverage=explore.LEVERAGE, warmup_bars=spec.candidate.warmup_bars) for cost_name, cost_value in COST_SCENARIOS: scenario_equity = explore.cost_adjusted_trade_equity_frame(result, cost_value) scenario_metrics = explore.annualized_metrics_from_equity(scenario_equity, int(scenario_equity["ts"].iloc[0].timestamp() * 1000), candles[-1].ts) cost_scenario_rows.append( { "strategy": spec.label, "cost": cost_name, "return": pct(scenario_metrics["net_total_return"]), "annualized": pct(scenario_metrics["net_annualized_return"]), "max_dd": pct(scenario_metrics["net_max_drawdown"]), "calmar": f"{scenario_metrics['net_calmar']:.2f}", } ) equity = explore.cost_adjusted_trade_equity_frame(result, ROUNDTRIP_COST_ON_MARGIN) strategy_horizon_rows.extend(horizon_rows(explore, spec.label, "strategy", equity)) cutoff = pd.to_datetime(candles[-1].ts, unit="ms", utc=True) - pd.DateOffset(years=3) recent_equity = normalize_equity(equity, cutoff) metrics = explore.annualized_metrics_from_equity(recent_equity, int(recent_equity["ts"].iloc[0].timestamp() * 1000), candles[-1].ts) result_rows.append( { "strategy": spec.label, "bar": spec.bar, "trades": result.trade_count, "3y_return": pct(metrics["net_total_return"]), "3y_annualized": pct(metrics["net_annualized_return"]), "3y_max_dd": pct(metrics["net_max_drawdown"]), "3y_calmar": f"{metrics['net_calmar']:.2f}", } ) monthly = monthly_from_equity(equity) monthly = monthly[monthly["period"].str.startswith(("2025-", "2026-"))].copy() monthly.insert(0, "strategy", spec.label) for row in monthly.to_dict("records"): monthly_raw_rows.append( { "strategy": row["strategy"], "period": row["period"], "return": float(row["return"]), "end_equity": float(row["end_equity"]), } ) monthly_rows.append( { "strategy": row["strategy"], "period": row["period"], "return": pct(float(row["return"])), "end_equity": money(float(row["end_equity"])), } ) daily = recent_equity.set_index("ts")["equity"].resample("1D").last().ffill().reset_index() recent_equities[spec.label] = daily yearly_rows.extend(yearly_from_equity(spec.label, daily)) curves.append( { "label": spec.label, "color": colors[index], "points": [(float(row.ts.timestamp()), float(row.equity)) for row in daily.itertuples(index=False)], } ) print(f"done {spec.label}") portfolio_specs = [ ( "Balanced 4", [ "BTC RSI2 Guarded 15m", "BTC Trend RSI-BB 15m", "ETH/BTC RSI Filter 15m", "BTC Lead ETH Lag 5m", ], ), ( "Aggressive 5", [ "BTC RSI2 Guarded 15m", "ETH RSI2 15m", "ETH/BTC RSI Filter 15m", "BTC Lead ETH Lag 15m", "BTC Lead ETH Lag 5m", ], ), ( "Lead Lag Basket", [ "BTC Lead ETH Lag 15m", "BTC Lead ETH Lag 5m", "BTC Lead ETH Lag 3m", ], ), ( "ETH Focused 2", [ "ETH/BTC RSI Filter 15m", "BTC Lead ETH Lag 5m", ], ), ] portfolio_rows: list[dict[str, object]] = [] portfolio_monthly_rows: list[dict[str, object]] = [] portfolio_monthly_raw_rows: list[dict[str, object]] = [] portfolio_curves: list[dict[str, object]] = [] portfolio_equities: dict[str, pd.DataFrame] = {} portfolio_horizon_rows: list[dict[str, object]] = [] portfolio_colors = ["#111827", "#b45309", "#0f766e", "#7c3aed"] overlay_rows: list[dict[str, object]] = [] overlay_horizon_rows: list[dict[str, object]] = [] overlay_curves: list[dict[str, object]] = [] for index, (portfolio_name, names) in enumerate(portfolio_specs): portfolio_equity = combine_equities([recent_equities[name] for name in names]) metrics = explore.annualized_metrics_from_equity( portfolio_equity, int(portfolio_equity["ts"].iloc[0].timestamp() * 1000), int(portfolio_equity["ts"].iloc[-1].timestamp() * 1000), ) portfolio_rows.append( { "portfolio": portfolio_name, "legs": len(names), "3y_return": pct(metrics["net_total_return"]), "3y_annualized": pct(metrics["net_annualized_return"]), "3y_max_dd": pct(metrics["net_max_drawdown"]), "3y_calmar": f"{metrics['net_calmar']:.2f}", } ) portfolio_horizon_rows.extend(horizon_rows(explore, portfolio_name, "portfolio", portfolio_equity)) monthly = monthly_from_equity(portfolio_equity) monthly = monthly[monthly["period"].str.startswith(("2025-", "2026-"))].copy() monthly.insert(0, "portfolio", portfolio_name) for row in monthly.to_dict("records"): portfolio_monthly_raw_rows.append( { "portfolio": row["portfolio"], "period": row["period"], "return": float(row["return"]), "end_equity": float(row["end_equity"]), } ) portfolio_monthly_rows.append( { "portfolio": row["portfolio"], "period": row["period"], "return": pct(float(row["return"])), "end_equity": money(float(row["end_equity"])), } ) portfolio_equities[portfolio_name] = portfolio_equity yearly_rows.extend(yearly_from_equity(portfolio_name, portfolio_equity)) portfolio_curves.append( { "label": portfolio_name, "color": portfolio_colors[index], "points": [(float(row.ts.timestamp()), float(row.equity)) for row in portfolio_equity.itertuples(index=False)], } ) if portfolio_name in {"Balanced 4", "Aggressive 5"}: for threshold in (0.03, 0.05, 0.07): for reduced_exposure in (0.0, 0.5): overlay_name = f"{portfolio_name} DD>{threshold:.0%} x{reduced_exposure:.1f}" overlay_equity = apply_drawdown_overlay(portfolio_equity, threshold, reduced_exposure) overlay_metrics = explore.annualized_metrics_from_equity( overlay_equity, int(overlay_equity["ts"].iloc[0].timestamp() * 1000), int(overlay_equity["ts"].iloc[-1].timestamp() * 1000), ) overlay_rows.append( { "overlay": overlay_name, "base": portfolio_name, "threshold": pct(threshold), "reduced_exposure": f"{reduced_exposure:.1f}", "sort_calmar": overlay_metrics["net_calmar"], "sort_annualized": overlay_metrics["net_annualized_return"], "3y_return": pct(overlay_metrics["net_total_return"]), "3y_annualized": pct(overlay_metrics["net_annualized_return"]), "3y_max_dd": pct(overlay_metrics["net_max_drawdown"]), "3y_calmar": f"{overlay_metrics['net_calmar']:.2f}", } ) overlay_horizon_rows.extend(horizon_rows(explore, overlay_name, "overlay", overlay_equity)) if portfolio_name == "Balanced 4" and threshold == 0.05: overlay_curves.append( { "label": overlay_name, "color": "#2563eb" if reduced_exposure == 0.5 else "#dc2626", "points": [(float(row.ts.timestamp()), float(row.equity)) for row in overlay_equity.itertuples(index=False)], } ) summary = pd.DataFrame(result_rows) monthly_frame = pd.DataFrame(monthly_rows) monthly_raw = pd.DataFrame(monthly_raw_rows) portfolio_summary = pd.DataFrame(portfolio_rows) portfolio_monthly = pd.DataFrame(portfolio_monthly_rows) strategy_horizon = pd.DataFrame(strategy_horizon_rows) cost_scenarios = pd.DataFrame(cost_scenario_rows) portfolio_horizon = pd.DataFrame(portfolio_horizon_rows) overlay_summary = ( pd.DataFrame(overlay_rows) .sort_values(["sort_calmar", "sort_annualized"], ascending=False) .drop(columns=["sort_calmar", "sort_annualized"]) ) overlay_horizon = pd.DataFrame(overlay_horizon_rows) portfolio_monthly_raw = pd.DataFrame(portfolio_monthly_raw_rows) correlations = daily_return_frame({**recent_equities, **portfolio_equities}).corr() correlation_rows = correlations.reset_index().rename(columns={"index": "name"}) worst_strategy_months = monthly_raw.sort_values("return").head(20).copy() worst_strategy_months["return"] = worst_strategy_months["return"].map(pct) worst_strategy_months["end_equity"] = worst_strategy_months["end_equity"].map(money) worst_portfolio_months = portfolio_monthly_raw.sort_values("return").head(12).copy() worst_portfolio_months["return"] = worst_portfolio_months["return"].map(pct) worst_portfolio_months["end_equity"] = worst_portfolio_months["end_equity"].map(money) yearly_frame = pd.DataFrame(yearly_rows) REPORT_DIR.mkdir(parents=True, exist_ok=True) monthly_frame.to_csv(REPORT_DIR / "ultrashort-recent-monthly.csv", index=False) summary.to_csv(REPORT_DIR / "ultrashort-recent-summary.csv", index=False) portfolio_summary.to_csv(REPORT_DIR / "ultrashort-portfolio-summary.csv", index=False) portfolio_monthly.to_csv(REPORT_DIR / "ultrashort-portfolio-monthly.csv", index=False) strategy_horizon.to_csv(REPORT_DIR / "ultrashort-strategy-horizons.csv", index=False) cost_scenarios.to_csv(REPORT_DIR / "ultrashort-cost-scenarios.csv", index=False) portfolio_horizon.to_csv(REPORT_DIR / "ultrashort-portfolio-horizons.csv", index=False) overlay_summary.to_csv(REPORT_DIR / "ultrashort-overlay-summary.csv", index=False) overlay_horizon.to_csv(REPORT_DIR / "ultrashort-overlay-horizons.csv", index=False) correlation_rows.to_csv(REPORT_DIR / "ultrashort-correlation.csv", index=False) worst_strategy_months.to_csv(REPORT_DIR / "ultrashort-worst-strategy-months.csv", index=False) worst_portfolio_months.to_csv(REPORT_DIR / "ultrashort-worst-portfolio-months.csv", index=False) yearly_frame.to_csv(REPORT_DIR / "ultrashort-yearly.csv", index=False) html = f""" Ultra Short Recent Strategy Report

近 3 年超短线策略报告

数据截至 {escape(str(pd.Timestamp.now("UTC").strftime("%Y-%m-%d %H:%M UTC")))},主结果成本按 maker+taker,每次完整交易扣除保证金 0.21%。曲线以近 3 年起点统一归一为 10,000。
{make_svg(curves)}

近 3 年汇总

{render_table(summary, ["strategy", "bar", "trades", "3y_return", "3y_annualized", "3y_max_dd", "3y_calmar"])}

单策略分周期表现

{render_table(strategy_horizon, ["strategy", "horizon", "return", "annualized", "max_dd", "calmar"])}

手续费三档敏感性

{render_table(cost_scenarios, ["strategy", "cost", "return", "annualized", "max_dd", "calmar"])}

组合曲线

{make_svg(portfolio_curves)}

组合近 3 年汇总

{render_table(portfolio_summary, ["portfolio", "legs", "3y_return", "3y_annualized", "3y_max_dd", "3y_calmar"])}

组合分周期表现

{render_table(portfolio_horizon, ["portfolio", "horizon", "return", "annualized", "max_dd", "calmar"])}

回撤降仓 Overlay

{make_svg([portfolio_curves[0], *overlay_curves])}
{render_table(overlay_summary, ["overlay", "base", "threshold", "reduced_exposure", "3y_return", "3y_annualized", "3y_max_dd", "3y_calmar"])}

Overlay 分周期表现

{render_table(overlay_horizon, ["overlay", "horizon", "return", "annualized", "max_dd", "calmar"])}

组合 2025-2026 月度收益

{render_table(portfolio_monthly, ["portfolio", "period", "return", "end_equity"])}

年度收益

{render_table(yearly_frame, ["name", "year", "return", "end_equity"])}

最差月份

{render_table(worst_portfolio_months, ["portfolio", "period", "return", "end_equity"])}
{render_table(worst_strategy_months, ["strategy", "period", "return", "end_equity"])}

相关性矩阵

{correlation_rows.round(3).to_html(index=False, escape=True)}

2025-2026 月度收益

{render_table(monthly_frame, ["strategy", "period", "return", "end_equity"])}

组合按近 3 年起点等资金分配,不做月度再平衡。月度收益按成本调整后的闭合交易权益计算;若月内无平仓交易,收益可能显示为 0。BB squeeze risk 低回撤候选保留观察:现有可复用逻辑只在探索脚本中,接入主报告需要搬运完整 runner,本轮不接入。

""" output_file = REPORT_DIR / REPORT_FILE output_file.write_text(html, encoding="utf-8") print(output_file) return 0 if __name__ == "__main__": raise SystemExit(main())