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"""
"""
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""
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。
近 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())