| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559 |
- 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'<line x1="{left}" y1="{yy:.1f}" x2="{width-right}" y2="{yy:.1f}" class="grid"/>')
- grid.append(f'<text x="8" y="{yy + 4:.1f}" class="axis">{money(value)}</text>')
- paths = []
- for curve in curves:
- points = " ".join(f"{x(ts):.1f},{y(value):.1f}" for ts, value in curve["points"])
- paths.append(f'<polyline points="{points}" fill="none" stroke="{curve["color"]}" stroke-width="2.4"/>')
- legend = []
- legend_x = left
- legend_y = height - 18
- for curve in curves:
- legend.append(f'<circle cx="{legend_x}" cy="{legend_y}" r="5" fill="{curve["color"]}"/>')
- legend.append(f'<text x="{legend_x + 10}" y="{legend_y + 4}" class="legend">{escape(str(curve["label"]))}</text>')
- legend_x += 170
- return f"""
- <svg viewBox="0 0 {width} {height}" role="img" aria-label="Recent equity curves">
- <style>
- .grid {{ stroke: #e5e7eb; stroke-width: 1; }}
- .axis {{ fill: #6b7280; font: 12px Inter, system-ui, sans-serif; }}
- .legend {{ fill: #111827; font: 12px Inter, system-ui, sans-serif; }}
- </style>
- <rect x="0" y="0" width="{width}" height="{height}" fill="#ffffff"/>
- {''.join(grid)}
- {''.join(paths)}
- {''.join(legend)}
- </svg>
- """
- def render_table(frame: pd.DataFrame, columns: list[str]) -> str:
- header = "".join(f"<th>{escape(column)}</th>" for column in columns)
- rows = []
- for row in frame.to_dict("records"):
- cells = "".join(f"<td>{escape(str(row[column]))}</td>" for column in columns)
- rows.append(f"<tr>{cells}</tr>")
- return f"<table><thead><tr>{header}</tr></thead><tbody>{''.join(rows)}</tbody></table>"
- 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"""<!doctype html>
- <html lang="zh-CN">
- <head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <title>Ultra Short Recent Strategy Report</title>
- <style>
- body {{ margin: 0; background: #f8fafc; color: #111827; font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }}
- main {{ max-width: 1280px; margin: 0 auto; padding: 28px 24px 48px; }}
- h1 {{ margin: 0 0 8px; font-size: 30px; }}
- h2 {{ margin: 28px 0 12px; font-size: 20px; }}
- .meta {{ color: #4b5563; margin-bottom: 18px; }}
- .panel {{ background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 16px; margin-bottom: 18px; overflow-x: auto; }}
- table {{ width: 100%; border-collapse: collapse; font-size: 13px; white-space: nowrap; }}
- th, td {{ text-align: right; padding: 8px 10px; border-bottom: 1px solid #e5e7eb; }}
- th:first-child, td:first-child {{ text-align: left; }}
- th {{ color: #374151; background: #f9fafb; font-weight: 700; }}
- .note {{ color: #6b7280; font-size: 13px; line-height: 1.6; }}
- </style>
- </head>
- <body>
- <main>
- <h1>近 3 年超短线策略报告</h1>
- <div class="meta">数据截至 {escape(str(pd.Timestamp.now("UTC").strftime("%Y-%m-%d %H:%M UTC")))},主结果成本按 maker+taker,每次完整交易扣除保证金 0.21%。曲线以近 3 年起点统一归一为 10,000。</div>
- <section class="panel">{make_svg(curves)}</section>
- <h2>近 3 年汇总</h2>
- <section class="panel">{render_table(summary, ["strategy", "bar", "trades", "3y_return", "3y_annualized", "3y_max_dd", "3y_calmar"])}</section>
- <h2>单策略分周期表现</h2>
- <section class="panel">{render_table(strategy_horizon, ["strategy", "horizon", "return", "annualized", "max_dd", "calmar"])}</section>
- <h2>手续费三档敏感性</h2>
- <section class="panel">{render_table(cost_scenarios, ["strategy", "cost", "return", "annualized", "max_dd", "calmar"])}</section>
- <h2>组合曲线</h2>
- <section class="panel">{make_svg(portfolio_curves)}</section>
- <h2>组合近 3 年汇总</h2>
- <section class="panel">{render_table(portfolio_summary, ["portfolio", "legs", "3y_return", "3y_annualized", "3y_max_dd", "3y_calmar"])}</section>
- <h2>组合分周期表现</h2>
- <section class="panel">{render_table(portfolio_horizon, ["portfolio", "horizon", "return", "annualized", "max_dd", "calmar"])}</section>
- <h2>回撤降仓 Overlay</h2>
- <section class="panel">{make_svg([portfolio_curves[0], *overlay_curves])}</section>
- <section class="panel">{render_table(overlay_summary, ["overlay", "base", "threshold", "reduced_exposure", "3y_return", "3y_annualized", "3y_max_dd", "3y_calmar"])}</section>
- <h2>Overlay 分周期表现</h2>
- <section class="panel">{render_table(overlay_horizon, ["overlay", "horizon", "return", "annualized", "max_dd", "calmar"])}</section>
- <h2>组合 2025-2026 月度收益</h2>
- <section class="panel">{render_table(portfolio_monthly, ["portfolio", "period", "return", "end_equity"])}</section>
- <h2>年度收益</h2>
- <section class="panel">{render_table(yearly_frame, ["name", "year", "return", "end_equity"])}</section>
- <h2>最差月份</h2>
- <section class="panel">{render_table(worst_portfolio_months, ["portfolio", "period", "return", "end_equity"])}</section>
- <section class="panel">{render_table(worst_strategy_months, ["strategy", "period", "return", "end_equity"])}</section>
- <h2>相关性矩阵</h2>
- <section class="panel">{correlation_rows.round(3).to_html(index=False, escape=True)}</section>
- <h2>2025-2026 月度收益</h2>
- <section class="panel">{render_table(monthly_frame, ["strategy", "period", "return", "end_equity"])}</section>
- <p class="note">组合按近 3 年起点等资金分配,不做月度再平衡。月度收益按成本调整后的闭合交易权益计算;若月内无平仓交易,收益可能显示为 0。BB squeeze risk 低回撤候选保留观察:现有可复用逻辑只在探索脚本中,接入主报告需要搬运完整 runner,本轮不接入。</p>
- </main>
- </body>
- </html>
- """
- 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())
|