From 5476ba0f33f9ebebf9ca17f998cec0ccfa2457f4 Mon Sep 17 00:00:00 2001 From: GitHub Date: Fri, 4 Sep 2026 22:42:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8F=82=E6=95=B0=E5=AF=BB=E4=BC=98?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=B9=B0=E5=85=A5=E6=8C=81=E6=9C=89=E5=9F=BA?= =?UTF-8?q?=E5=87=86=E5=AF=B9=E6=AF=94=20=E2=80=94=20=E4=B8=A4=E4=B8=AA?= =?UTF-8?q?=E5=AF=BB=E4=BC=98=E6=8E=A5=E5=8F=A3=E8=BF=94=E5=9B=9E=E5=90=8C?= =?UTF-8?q?=E5=8F=A3=E5=BE=84=20buy=5Fhold=EF=BC=88run=5Fbuy=5Fhold=5Fbenc?= =?UTF-8?q?hmark=EF=BC=89=EF=BC=8C=E5=85=A8=E5=B1=80=E6=9C=80=E4=BD=B3/?= =?UTF-8?q?=E6=9C=80=E4=BC=98=E7=BB=93=E6=9E=9C=E6=80=BB=E6=94=B6=E7=9B=8A?= =?UTF-8?q?=E6=97=81=E5=B1=95=E7=A4=BA=20[=E4=B9=B0=E5=85=A5=E6=8C=81?= =?UTF-8?q?=E6=9C=89=EF=BC=9Axx.xx%]=EF=BC=8C=E6=B6=A8=E7=BA=A2=E8=B7=8C?= =?UTF-8?q?=E7=BB=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/easy_tdx/web/backtest_schemas.py | 3 +++ src/easy_tdx/web/routers/backtest.py | 22 +++++++++++++++++++++- web-ui/src/types.ts | 14 ++++++++++++++ web-ui/src/views/OptimizeView.vue | 20 ++++++++++++++++++++ 4 files changed, 58 insertions(+), 1 deletion(-) 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;