Parcourir la source

feat: add portfolio drawdown overlay analysis

lxy il y a 1 mois
Parent
commit
14fffffc99

+ 49 - 0
reports/ultrashort/ultrashort-overlay-horizons.csv

@@ -0,0 +1,49 @@
+overlay,horizon,return,annualized,max_dd,calmar
+Balanced 4 DD>3% x0.0,3y,50.08%,14.59%,7.12%,2.05
+Balanced 4 DD>3% x0.0,1y,7.67%,7.67%,7.12%,1.08
+Balanced 4 DD>3% x0.0,6m,4.01%,8.20%,3.47%,2.36
+Balanced 4 DD>3% x0.0,3m,5.95%,26.44%,1.37%,19.31
+Balanced 4 DD>3% x0.5,3y,66.32%,18.61%,5.53%,3.36
+Balanced 4 DD>3% x0.5,1y,10.14%,10.14%,5.53%,1.83
+Balanced 4 DD>3% x0.5,6m,5.66%,11.67%,3.30%,3.53
+Balanced 4 DD>3% x0.5,3m,5.95%,26.44%,1.37%,19.31
+Balanced 4 DD>5% x0.0,3y,80.91%,22.00%,6.09%,3.62
+Balanced 4 DD>5% x0.0,1y,12.65%,12.65%,4.09%,3.09
+Balanced 4 DD>5% x0.0,6m,7.33%,15.25%,3.30%,4.61
+Balanced 4 DD>5% x0.0,3m,5.95%,26.44%,1.37%,19.31
+Balanced 4 DD>5% x0.5,3y,82.44%,22.35%,5.88%,3.80
+Balanced 4 DD>5% x0.5,1y,12.65%,12.65%,4.09%,3.09
+Balanced 4 DD>5% x0.5,6m,7.33%,15.25%,3.30%,4.61
+Balanced 4 DD>5% x0.5,3m,5.95%,26.44%,1.37%,19.31
+Balanced 4 DD>7% x0.0,3y,83.94%,22.69%,6.38%,3.56
+Balanced 4 DD>7% x0.0,1y,12.65%,12.65%,4.09%,3.09
+Balanced 4 DD>7% x0.0,6m,7.33%,15.25%,3.30%,4.61
+Balanced 4 DD>7% x0.0,3m,5.95%,26.44%,1.37%,19.31
+Balanced 4 DD>7% x0.5,3y,83.94%,22.69%,6.38%,3.56
+Balanced 4 DD>7% x0.5,1y,12.65%,12.65%,4.09%,3.09
+Balanced 4 DD>7% x0.5,6m,7.33%,15.25%,3.30%,4.61
+Balanced 4 DD>7% x0.5,3m,5.95%,26.44%,1.37%,19.31
+Aggressive 5 DD>3% x0.0,3y,49.42%,14.41%,7.92%,1.82
+Aggressive 5 DD>3% x0.0,1y,5.80%,5.80%,6.31%,0.92
+Aggressive 5 DD>3% x0.0,6m,-2.64%,-5.22%,4.79%,-1.09
+Aggressive 5 DD>3% x0.0,3m,0.85%,3.49%,1.56%,2.24
+Aggressive 5 DD>3% x0.5,3y,71.80%,19.89%,6.86%,2.90
+Aggressive 5 DD>3% x0.5,1y,7.81%,7.81%,6.39%,1.22
+Aggressive 5 DD>3% x0.5,6m,-0.79%,-1.58%,4.88%,-0.32
+Aggressive 5 DD>3% x0.5,3m,3.39%,14.48%,1.23%,11.79
+Aggressive 5 DD>5% x0.0,3y,61.74%,17.49%,8.78%,1.99
+Aggressive 5 DD>5% x0.0,1y,8.94%,8.94%,5.02%,1.78
+Aggressive 5 DD>5% x0.0,6m,0.26%,0.52%,3.49%,0.15
+Aggressive 5 DD>5% x0.0,3m,3.37%,14.37%,1.11%,12.89
+Aggressive 5 DD>5% x0.5,3y,78.56%,21.45%,7.30%,2.94
+Aggressive 5 DD>5% x0.5,1y,9.39%,9.39%,6.18%,1.52
+Aggressive 5 DD>5% x0.5,6m,0.67%,1.34%,4.67%,0.29
+Aggressive 5 DD>5% x0.5,3m,4.67%,20.33%,1.11%,18.23
+Aggressive 5 DD>7% x0.0,3y,68.67%,19.15%,9.03%,2.12
+Aggressive 5 DD>7% x0.0,1y,8.04%,8.04%,7.90%,1.02
+Aggressive 5 DD>7% x0.0,6m,-0.57%,-1.15%,6.41%,-0.18
+Aggressive 5 DD>7% x0.0,3m,5.98%,26.54%,1.11%,23.80
+Aggressive 5 DD>7% x0.5,3y,82.21%,22.28%,8.03%,2.77
+Aggressive 5 DD>7% x0.5,1y,8.92%,8.92%,7.57%,1.18
+Aggressive 5 DD>7% x0.5,6m,0.23%,0.47%,6.08%,0.08
+Aggressive 5 DD>7% x0.5,3m,5.98%,26.54%,1.11%,23.80

+ 13 - 0
reports/ultrashort/ultrashort-overlay-summary.csv

@@ -0,0 +1,13 @@
+overlay,base,threshold,reduced_exposure,3y_return,3y_annualized,3y_max_dd,3y_calmar
+Balanced 4 DD>5% x0.5,Balanced 4,5.00%,0.5,82.44%,22.35%,5.88%,3.80
+Balanced 4 DD>5% x0.0,Balanced 4,5.00%,0.0,80.91%,22.00%,6.09%,3.62
+Balanced 4 DD>7% x0.0,Balanced 4,7.00%,0.0,83.94%,22.69%,6.38%,3.56
+Balanced 4 DD>7% x0.5,Balanced 4,7.00%,0.5,83.94%,22.69%,6.38%,3.56
+Balanced 4 DD>3% x0.5,Balanced 4,3.00%,0.5,66.32%,18.61%,5.53%,3.36
+Aggressive 5 DD>5% x0.5,Aggressive 5,5.00%,0.5,78.56%,21.45%,7.30%,2.94
+Aggressive 5 DD>3% x0.5,Aggressive 5,3.00%,0.5,71.80%,19.89%,6.86%,2.90
+Aggressive 5 DD>7% x0.5,Aggressive 5,7.00%,0.5,82.21%,22.28%,8.03%,2.77
+Aggressive 5 DD>7% x0.0,Aggressive 5,7.00%,0.0,68.67%,19.15%,9.03%,2.12
+Balanced 4 DD>3% x0.0,Balanced 4,3.00%,0.0,50.08%,14.59%,7.12%,2.05
+Aggressive 5 DD>5% x0.0,Aggressive 5,5.00%,0.0,61.74%,17.49%,8.78%,1.99
+Aggressive 5 DD>3% x0.0,Aggressive 5,3.00%,0.0,49.42%,14.41%,7.92%,1.82

Fichier diff supprimé car celui-ci est trop grand
+ 11 - 1
reports/ultrashort/ultrashort-recent-report.html


+ 63 - 0
scripts/generate_ultrashort_report.py

@@ -101,6 +101,20 @@ def daily_return_frame(equities: dict[str, pd.DataFrame]) -> pd.DataFrame:
     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)
@@ -315,6 +329,9 @@ def main() -> int:
     portfolio_equities: dict[str, pd.DataFrame] = {}
     portfolio_horizon_rows: list[dict[str, object]] = []
     portfolio_colors = ["#111827", "#b45309", "#0f766e"]
+    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(
@@ -362,6 +379,39 @@ def main() -> int:
                 "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)
@@ -370,6 +420,12 @@ def main() -> int:
     portfolio_monthly = pd.DataFrame(portfolio_monthly_rows)
     strategy_horizon = pd.DataFrame(strategy_horizon_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"})
@@ -387,6 +443,8 @@ def main() -> int:
     portfolio_monthly.to_csv(REPORT_DIR / "ultrashort-portfolio-monthly.csv", index=False)
     strategy_horizon.to_csv(REPORT_DIR / "ultrashort-strategy-horizons.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)
@@ -426,6 +484,11 @@ def main() -> int:
   <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>

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff