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)