From f1fc18bc86c35359bf62f4feb10f33bf68176981 Mon Sep 17 00:00:00 2001 From: claude_dev Date: Sat, 11 Jul 2026 15:03:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(backtest):=20=E8=A1=A5=E5=85=A8=E7=AC=AC5?= =?UTF-8?q?=E5=9B=BEVolatilityChart=E2=80=94metrics=E5=8A=A0rolling?= =?UTF-8?q?=E6=B3=A2=E5=8A=A8=E7=8E=87=E6=97=B6=E5=BA=8F+risk-series?= =?UTF-8?q?=E7=AB=AF=E7=82=B9+=E5=89=8D=E7=AB=AF=E6=8E=A5=E5=85=A5(?= =?UTF-8?q?=E5=87=91=E9=BD=90=E8=81=9A=E5=AE=BD5=E5=9B=BE=E5=85=A8?= =?UTF-8?q?=E5=A5=97)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/backtest.ts | 2 ++ frontend/src/views/backtest/Result.vue | 8 ++++++++ sanguo_api/routes.py | 6 +++++- sanguo_backtest/metrics.py | 11 +++++++++++ 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/frontend/src/api/backtest.ts b/frontend/src/api/backtest.ts index 892bd1b..1a7d899 100644 --- a/frontend/src/api/backtest.ts +++ b/frontend/src/api/backtest.ts @@ -156,6 +156,8 @@ export interface RiskSeriesData { alpha: number[] beta: number[] drawdown: number[] + strategy_vol?: number[] + benchmark_vol?: number[] } export async function getRelativeMetrics(taskId: string): Promise { diff --git a/frontend/src/views/backtest/Result.vue b/frontend/src/views/backtest/Result.vue index 2e886c2..d967163 100644 --- a/frontend/src/views/backtest/Result.vue +++ b/frontend/src/views/backtest/Result.vue @@ -13,6 +13,7 @@ import BenchmarkCurve from '@/components/backtest/BenchmarkCurve.vue' import AlphaChart from '@/components/backtest/AlphaChart.vue' import BetaChart from '@/components/backtest/BetaChart.vue' import DrawdownChart from '@/components/backtest/DrawdownChart.vue' +import VolatilityChart from '@/components/backtest/VolatilityChart.vue' const route = useRoute() const taskId = String(route.params.id) @@ -194,6 +195,13 @@ onMounted(async () => { :drawdown="filteredRiskSeries.drawdown" /> + + + diff --git a/sanguo_api/routes.py b/sanguo_api/routes.py index 3ac3d91..fa51c01 100644 --- a/sanguo_api/routes.py +++ b/sanguo_api/routes.py @@ -387,12 +387,16 @@ def risk_series(task_id: str): alpha = series.get("alpha", {}) beta = series.get("beta", {}) drawdown = series.get("drawdown", {}) + vol_s = series.get("volatility_strategy", {}) + vol_b = series.get("volatility_benchmark", {}) return { "dates": alpha.get("dates", []), "alpha": alpha.get("values", []), "beta": beta.get("values", []), - "drawdown": drawdown.get("values", []) + "drawdown": drawdown.get("values", []), + "strategy_vol": vol_s.get("values", []), + "benchmark_vol": vol_b.get("values", []) } diff --git a/sanguo_backtest/metrics.py b/sanguo_backtest/metrics.py index 2afd7b9..3825fec 100644 --- a/sanguo_backtest/metrics.py +++ b/sanguo_backtest/metrics.py @@ -64,11 +64,22 @@ def compute_metrics( cummax = equity.cummax() drawdown = (equity - cummax) / cummax + # Rolling annualized volatility (quarterly window) for the volatility chart + _vol_window = min(63, len(s)) + if _vol_window >= 2: + vol_strategy = s.rolling(_vol_window, min_periods=2).std() * np.sqrt(period) + vol_benchmark = b.rolling(_vol_window, min_periods=2).std() * np.sqrt(period) + else: + vol_strategy = pd.Series([np.nan] * len(s), index=s.index) + vol_benchmark = pd.Series([np.nan] * len(s), index=s.index) + series = { "equity_curve": equity, "benchmark_curve": bench_curve, "alpha": roll_alpha, "beta": roll_beta, "drawdown": drawdown, + "volatility_strategy": vol_strategy, + "volatility_benchmark": vol_benchmark, } return MetricsResult(scalars=scalars, series=series)