diff --git a/src/easy_tdx/web/backtest_schemas.py b/src/easy_tdx/web/backtest_schemas.py index 07533e3..0838d7e 100644 --- a/src/easy_tdx/web/backtest_schemas.py +++ b/src/easy_tdx/web/backtest_schemas.py @@ -360,6 +360,9 @@ class OptimizeAllResult(BaseModel): best: OptimizeAllRankEntry | None = None per_strategy: dict[str, OptimizeAllRankEntry] = {} # 策略名 → 最优点 total_grid_points: int = 0 # 所有策略网格点合计 + # 买入持有基准(同区间/同费率/同资金;含 total_return 等 6 项指标), + # 供前端在全局最佳旁直观对比「策略 vs 买入不动」。老版本结果可能缺省。 + buy_hold: dict[str, Any] | None = None # ── 已保存策略(策略库 / StrategyLibrary)─────────────────────────────────────── diff --git a/src/easy_tdx/web/routers/backtest.py b/src/easy_tdx/web/routers/backtest.py index 57c7aff..834098f 100644 --- a/src/easy_tdx/web/routers/backtest.py +++ b/src/easy_tdx/web/routers/backtest.py @@ -1184,6 +1184,7 @@ def _run_multi_strategy_evaluate( def _run_optimize(df: pd.DataFrame, req: OptimizeBacktestRequest) -> dict[str, Any]: """执行参数网格寻优并返回清洗后的结果字典(后台线程内调用)。""" + from easy_tdx.backtest.benchmark import run_buy_hold_benchmark from easy_tdx.backtest.optimizer import ParamGridOptimizer optimizer = ParamGridOptimizer( @@ -1197,7 +1198,17 @@ def _run_optimize(df: pd.DataFrame, req: OptimizeBacktestRequest) -> dict[str, A workers=req.workers, ) result = optimizer.run() - return result.to_dict() + out = result.to_dict() + # 买入持有基准(同区间/同费率/同资金,与一条龙评估同口径), + # 供前端在最优结果旁直观对比「策略 vs 买入不动」。 + out["buy_hold"] = run_buy_hold_benchmark( + df, + cash=req.cash, + commission=req.commission, + slippage=req.slippage, + execution=req.execution, + ) + return out def _optimize_one_strategy( @@ -1263,6 +1274,7 @@ def _run_optimize_all(df: pd.DataFrame, req: OptimizeAllBacktestRequest) -> dict ``workers`` 为 0 或 1 时串行。进程池在函数内 ``with`` 创建/销毁,对前端 轮询与 task_runner 透明。 """ + from easy_tdx.backtest.benchmark import run_buy_hold_benchmark from easy_tdx.backtest.strategies import get_registry from easy_tdx.backtest.strategies.presets import STRATEGY_PRESETS @@ -1338,6 +1350,14 @@ def _run_optimize_all(df: pd.DataFrame, req: OptimizeAllBacktestRequest) -> dict best=best, per_strategy=per_strategy, total_grid_points=total_grid, + # 买入持有基准(同区间/同费率/同资金),供前端在全局最佳旁直观对比 + buy_hold=run_buy_hold_benchmark( + df, + cash=req.cash, + commission=req.commission, + slippage=req.slippage, + execution=req.execution, + ), ) return result_obj.model_dump() diff --git a/web-ui/src/types.ts b/web-ui/src/types.ts index f8fbb76..e32c3b2 100644 --- a/web-ui/src/types.ts +++ b/web-ui/src/types.ts @@ -257,12 +257,24 @@ export interface OptimizeHeatmap { data: Array<[number, number, number | null]> } +/** 买入持有基准(同区间/同费率,首根买入持有到末根,与一条龙评估同口径) */ +export interface BuyHoldBenchmark { + total_return: number + annual_return: number + max_drawdown: number + sharpe: number + calmar: number + volatility: number +} + export interface OptimizeResult { strategy: string param_names: string[] results: GridPointResult[] best: GridPointResult | null heatmap: OptimizeHeatmap | null + /** 买入持有基准,供直观对比「策略 vs 买入不动」(老版本后端结果可能缺省) */ + buy_hold?: BuyHoldBenchmark | null } // ── 一键寻优所有策略(Phase 6) ────────────────────────────────────────────── @@ -299,6 +311,8 @@ export interface OptimizeAllResult { best: OptimizeAllRankEntry | null per_strategy: Record total_grid_points: number + /** 买入持有基准,供直观对比「策略 vs 买入不动」(老版本后端结果可能缺省) */ + buy_hold?: BuyHoldBenchmark | null } // ── 错误响应(后端 ApiErrorResponse) ───────────────────────────────────────── diff --git a/web-ui/src/views/OptimizeView.vue b/web-ui/src/views/OptimizeView.vue index 3826df2..cc5654a 100644 --- a/web-ui/src/views/OptimizeView.vue +++ b/web-ui/src/views/OptimizeView.vue @@ -196,6 +196,10 @@ function onViewAll(strategyName: string, params: Record function pct(v: number | null | undefined): string { return v !== null && v !== undefined && Number.isFinite(v) ? `${(v * 100).toFixed(2)}%` : '-' } +/** 买入持有的盈亏配色(A 股习惯:涨红跌绿;非数值不加色) */ +function bhClass(v: number | null | undefined): string { + return v !== null && v !== undefined && Number.isFinite(v) ? (v >= 0 ? 'pos' : 'neg') : '' +} function num(v: number | null | undefined, d = 2): string { return v !== null && v !== undefined && Number.isFinite(v) ? v.toFixed(d) : '-' } @@ -316,6 +320,11 @@ const rankingGrades = computed(() => {{ (store.optimizeResult.best.total_return! * 100).toFixed(2) }}% + + [买入持有:{{ + pct(store.optimizeResult.buy_hold.total_return) + }}] + 夏普 {{ store.optimizeResult.best.sharpe?.toFixed(2) }} · 回撤 {{ (store.optimizeResult.best.max_drawdown! * 100).toFixed(2) }}% @@ -351,6 +360,11 @@ const rankingGrades = computed(() => {{ (store.optimizeAllResult.best.total_return! * 100).toFixed(2) }}% + + [买入持有:{{ + pct(store.optimizeAllResult.buy_hold.total_return) + }}] + 夏普 {{ store.optimizeAllResult.best.sharpe?.toFixed(2) }} · 回撤 {{ (store.optimizeAllResult.best.max_drawdown! * 100).toFixed(2) }}% · 胜率 @@ -542,6 +556,12 @@ const rankingGrades = computed(() => font-weight: 700; font-family: var(--font-mono); } +/* 买入持有对比项:标签/括号弱化不抢焦点,百分比按盈亏走 .pos/.neg 红绿 */ +.best-bh { + color: var(--text-dim); + font-size: 13px; + font-family: var(--font-mono); +} .best-meta { color: var(--text-dim); font-size: 12px;