feat(backtest): 买卖点理由(用数据说话)+ 因子曲线 + 曲线新页面放大
用户要求:「所有买卖点详细说明买卖理由,用数据说话」「回测图上增加因子相关曲线
(买卖依据是股息率,就加股息率曲线)」「所有曲线能弹出新页面放大」。
一、买卖理由(后端产出结构化数据,前端只展示)
- 新增 `quant/trade_reasons.py`:封闭词表 + 文案构造器,组合引擎与单策略引擎共用,
避免两个引擎对同一件事写出两种说法。理由里带**引擎当时的真实数字**:
综合分名次/候选数/综合分/各因子原始值/持有交易日/预算与最低佣金/涨停比值等。
- 买入:按名次建仓、顺延成交、涨停未买、停牌未买、现金不足、不足最低佣金;
卖出:跌出 TopN(含第几名掉出)、被股票池过滤(与「跌出 TopN」分开写)、
超 Tmax 强制了结、Tmin 保护暂留、停牌/跌停顺延。
- `ActionRecord.reason` 覆盖**成交与未成交**全部买卖点(原 `reject_reason` 保留不动,
老归档仍可读);`Trade.entry_reason / exit_reason` 跟着成交记录走。
- 名次来自调仓日完整排名(新增 `_ranked_by_day`),拿不到名次时如实写「未给出名次」,
绝不编造一个名次填进去。
- 未成交明细不再只写执行层原因:把「为什么选中它、当时各因子多少」一并给出。
二、因子曲线
- `FactorCurve`:每个策略因子一条曲线,值为**当日持仓按市值加权平均的原始值**
(不做 z-score、不按方向取反,空仓日不落点、不插值、不用 0 填充),并带
label/direction/unit 供界面说明口径;`FactorDef/FactorTemplate` 新增 `unit`
(股息率 %、量比/接近新高 倍数、动量等 小数),11 个内置因子实例已逐一核对。
- 归档体积预算照旧按整包计量,无需改迁移。
三、界面
- 结果页新增「买卖说明」区块:全部买卖点 + 理由 + 数字标签,支持方向/成交状态/关键字
筛选与日期排序;成交明细表加「为什么买 / 为什么卖」两列;新增「因子曲线」区块,
每条曲线标出组合成交日,直接对照「买卖发生在什么水平」。
- 「新页面放大」:每条曲线(净值/回撤/因子/个股/月度)都能开 `/charts/{归档id}?s=...`
整页看大图;放大页是 Server Component,数据从归档直出,URL 可分享且与归档一致。
未归档的结果如实说明「未归档,无法放大」,不给坏链接。
- 数字格式与后端 `f"{v:.4f}"` 同规则(四舍六入五成双):修掉 0.03125 在理由原文里
显示 0.0312、旁边标签显示 0.0313 的不一致(17 组边界值与 Python 逐一比对一致)。
- `/factors/compose` 结果区改用同一个 `BacktestResultView`,两处口径不会再漂移。
验证:
- 新增 `tests/test_trade_reasons.py` 8 条(买入数字、跌出 TopN 名次、不在候选池、
Tmax、Tmin 暂留、涨停未成交、因子曲线加权值、空仓不落点);后端 510 条全过,ruff clean。
- 真实数据端到端:`/api/combos/run` 6 个月高股息组合(EXP-8EA2819B)13 个买卖点
100% 带理由与数字,因子曲线 dividend_yield 117 点、单位 %;
`scripts/verify_backtest_page_contract.py`(4 年、301 个买卖点、140 笔成交)扩展断言
理由词表/名次/因子值/曲线单调性后通过。
- 浏览器实测:归档详情页与放大页 `/charts/...?s=factor:dividend_yield` 等 5 种曲线
全部 200 渲染,截图确认表格与曲线数值正确。
This commit is contained in:
@@ -117,6 +117,7 @@ class FactorDef:
|
||||
param_specs: tuple[ParamSpec, ...] = () # 可编辑参数与约束(供目录/界面)
|
||||
source: str = "builtin" # builtin(代码注册表)| custom(目录里创建的参数化实例)
|
||||
label: str = "" # 中文显示名(含参数),如「动量(窗口 90,越高越好)」
|
||||
unit: str | None = None # 因子值的量纲(% / 倍数 / 小数),供图表坐标轴与说明用
|
||||
|
||||
@property
|
||||
def display(self) -> str:
|
||||
@@ -141,6 +142,11 @@ class FactorTemplate:
|
||||
requires: tuple[str, ...] = ("close",)
|
||||
frequency: str = "daily"
|
||||
direction_default: str = DIRECTION_HIGHER
|
||||
# 因子值的量纲(由算法口径决定,不是可调参数):
|
||||
# "%" = 数值本身就是百分数(股息率 5.2 读作 5.2%)
|
||||
# "倍数" = 比值(量比 1.2 表示 1.2 倍)
|
||||
# "小数" = 无单位比例,0.15 表示 15%(图上按小数显示,不做 ×100 换算)
|
||||
unit: str | None = None
|
||||
lookback_of: Callable[[Mapping[str, Any]], int] | None = None
|
||||
check: Callable[[Mapping[str, Any]], str | None] | None = None # 跨参数约束
|
||||
instances: tuple[tuple[str, Mapping[str, Any]], ...] = () # ((历史名, 参数), ...)
|
||||
@@ -366,6 +372,7 @@ def build_factor_def(
|
||||
param_specs=template.specs(),
|
||||
source=source,
|
||||
label=label_of(checked),
|
||||
unit=template.unit,
|
||||
)
|
||||
|
||||
|
||||
@@ -496,6 +503,7 @@ register_template(
|
||||
description="过去 {window} 个交易日收益率",
|
||||
formula="close / close.shift({window}) - 1",
|
||||
brief="动量:强者延续,适合趋势延续环境;窗口越短越敏感、越长越稳。",
|
||||
unit="小数",
|
||||
fn=lambda fields, params: _rolling_return(fields["close"], params[P_WINDOW]),
|
||||
param_specs=(_window_spec(),),
|
||||
lookback_of=lambda params: params[P_WINDOW],
|
||||
@@ -514,6 +522,7 @@ register_template(
|
||||
description="过去 {window} 个交易日收益率波动率",
|
||||
formula="std(pct_change, {window})",
|
||||
brief="低波动防御:近段波动小的股票抗跌,弱市/熊市阶段相对占优(方向越低越好)。",
|
||||
unit="小数",
|
||||
fn=lambda fields, params: _rolling_vol(fields["close"], params[P_WINDOW]),
|
||||
param_specs=(_window_spec(),),
|
||||
direction_default=DIRECTION_LOWER,
|
||||
@@ -532,6 +541,7 @@ register_template(
|
||||
description="收盘价相对 {window} 日最高价的接近程度",
|
||||
formula="close / rolling_max(high, {window})",
|
||||
brief="贴近 n 日高点(接近新高):趋势确认型强势股,常与动量互补;需配合市场热度判断。",
|
||||
unit="倍数",
|
||||
fn=lambda fields, params: fields["close"] / fields["high"].rolling(params[P_WINDOW]).max(),
|
||||
param_specs=(_window_spec(),),
|
||||
requires=("close", "high"),
|
||||
@@ -559,6 +569,7 @@ register_template(
|
||||
description="量比:{fast} 日均量 / {slow} 日均量",
|
||||
formula="mean(volume, {fast}) / mean(volume, {slow})",
|
||||
brief="量比放大提示资金关注(短线活跃型);高换手也伴随更高波动,注意与波动因子搭配。",
|
||||
unit="倍数",
|
||||
fn=lambda fields, params: (
|
||||
fields["volume"].rolling(params[P_FAST]).mean()
|
||||
/ fields["volume"].rolling(params[P_SLOW]).mean()
|
||||
@@ -598,6 +609,7 @@ register_template(
|
||||
description="{window} 日均线乖离率",
|
||||
formula="(close - ma(close, {window})) / ma(close, {window})",
|
||||
brief="均线乖离:上行趋势中正乖离偏强;乖离过大易回落,需警惕过热。",
|
||||
unit="小数",
|
||||
fn=lambda fields, params: (
|
||||
(fields["close"] - fields["close"].rolling(params[P_WINDOW]).mean())
|
||||
/ fields["close"].rolling(params[P_WINDOW]).mean()
|
||||
@@ -615,6 +627,7 @@ register_template(
|
||||
description="短期反转:过去 {window} 日收益率取负",
|
||||
formula="-1 * (close / close.shift({window}) - 1)",
|
||||
brief="短期反转:前期跌幅大的超跌反弹机会,适合震荡/修复行情。",
|
||||
unit="小数",
|
||||
fn=lambda fields, params: -1.0 * _rolling_return(fields["close"], params[P_WINDOW]),
|
||||
param_specs=(_window_spec(),),
|
||||
lookback_of=lambda params: params[P_WINDOW],
|
||||
@@ -641,6 +654,7 @@ register_template(
|
||||
"建议配合 dv_ratio 上限过滤与盈利质量条件使用。"
|
||||
),
|
||||
fn=lambda fields, params: fields["dv_ratio"],
|
||||
unit="%",
|
||||
requires=("dv_ratio",),
|
||||
lookback_of=lambda params: 0, # 时点截面值,无滚动窗口
|
||||
instances=(("dividend_yield", {P_DIRECTION: DIRECTION_HIGHER}),),
|
||||
@@ -654,9 +668,10 @@ register_template(
|
||||
description="股息率 TTM(近 12 个月滚动现金分红 / 总市值 × 100,%)",
|
||||
formula="dv_ttm(Tushare daily_basic,逐日时点值)",
|
||||
brief="同股息率,但口径为 TTM;与 dv_ratio 多数日期取值一致,可作交叉验证。",
|
||||
unit="%",
|
||||
fn=lambda fields, params: fields["dv_ttm"],
|
||||
requires=("dv_ttm",),
|
||||
lookback_of=lambda params: 0,
|
||||
instances=(("dividend_yield_ttm", {P_DIRECTION: DIRECTION_HIGHER}),),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user