mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 19:04:15 +08:00
- 因子平台: /factors 一级页(检验/因子库/编辑器/组合/挖掘), DSL 公式因子(25 算子点选、双语字段、我的因子模板、脏公式守卫), 版本与生命周期, 自动挖掘 L1 统计筛选 - 因子↔策略四条桥: 触发器 Zap 快建因子条件信号、因子一键生成排名策略、自定义信号 AI 提示词接入因子分组、策略回测因子归因(胜/败单入场信号日因子均值, 独立 tab, 双语因子名) - 回测: 统计卡新增盈亏比(≥1 红/<1 绿), 蒙卡回撤合并为中位/95% 双值卡(自适应字号), 高级设置基础过滤与策略编辑器参数对齐(5 组区间) - 信号库独立页 /signals(原设置 tab 迁出), 持仓提醒入导航; 挖掘并入因子页第 5 tab, /mining 旧链接重定向 - 研究线配套: 因子目录 61→77(评分/矩阵双内核), stats_v2(Newey-West/BH-FDR/DSR), enriched 管道与异动/报价服务配套调整 - 文档: README 导航与特性表、features.md 因子平台章节、操作说明书 9.2、factor-platform-plan 执行状态与 §5、二开文档桥接说明; 交流与支持节改版 - 版本 0.2.2 → 0.2.3; 后端全量 1625 passed(1 例环境性跳过), 前端 build 通过
81 lines
2.9 KiB
Python
81 lines
2.9 KiB
Python
"""回测因子归因 (v1) — _factor_attribution_summary 单元测试。
|
|
|
|
覆盖:
|
|
- 盈利/亏损单因子均值、样本数计算
|
|
- snapshot 日期列 date/str 两种 dtype 均可关联
|
|
- entry_signal_date 缺失时回退 entry_date
|
|
- 无可关联行 / 空成交 / 无因子列 → None (fail-open)
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import date
|
|
from types import SimpleNamespace
|
|
|
|
import polars as pl
|
|
|
|
from app.backtest.strategy import _factor_attribution_summary
|
|
|
|
|
|
def _trade(symbol: str, day: str, pnl: float):
|
|
return SimpleNamespace(
|
|
symbol=symbol,
|
|
entry_signal_date=day,
|
|
entry_date=day,
|
|
pnl_pct=pnl,
|
|
)
|
|
|
|
|
|
def _snapshot(dates_as: str = "str") -> pl.DataFrame:
|
|
frame = pl.DataFrame({
|
|
"symbol": ["000001", "000002", "000003", "000004"],
|
|
"date": ["2026-01-05"] * 4,
|
|
"momentum_20d": [0.10, -0.05, 0.20, 0.00],
|
|
"turnover_rate": [5.0, 8.0, 6.0, 7.0],
|
|
})
|
|
if dates_as == "date":
|
|
frame = frame.with_columns(pl.col("date").str.to_date())
|
|
return frame
|
|
|
|
|
|
def test_summary_win_lose_means():
|
|
trades = [
|
|
_trade("000001", "2026-01-05", 0.10), # 盈利: mom 0.10, to 5
|
|
_trade("000003", "2026-01-05", 0.05), # 盈利: mom 0.20, to 6
|
|
_trade("000002", "2026-01-05", -0.03), # 亏损: mom -0.05, to 8
|
|
_trade("000004", "2026-01-05", -0.08), # 亏损: mom 0.00, to 7
|
|
]
|
|
result = _factor_attribution_summary(_snapshot(), trades)
|
|
assert result is not None
|
|
assert result["n_win"] == 2 and result["n_lose"] == 2
|
|
by_factor = {f["factor"]: f for f in result["factors"]}
|
|
assert by_factor["momentum_20d"]["win_mean"] == round((0.10 + 0.20) / 2, 6)
|
|
assert by_factor["momentum_20d"]["lose_mean"] == round((-0.05 + 0.00) / 2, 6)
|
|
assert by_factor["turnover_rate"]["win_mean"] == 5.5
|
|
assert by_factor["turnover_rate"]["lose_n"] == 2
|
|
|
|
|
|
def test_summary_accepts_date_dtype_snapshot():
|
|
trades = [_trade("000001", "2026-01-05", 0.1), _trade("000002", "2026-01-05", -0.1)]
|
|
result = _factor_attribution_summary(_snapshot(dates_as="date"), trades)
|
|
assert result is not None
|
|
assert result["n_win"] == 1
|
|
|
|
|
|
def test_summary_falls_back_to_entry_date():
|
|
trade = SimpleNamespace(symbol="000001", entry_signal_date=None,
|
|
entry_date=date(2026, 1, 5), pnl_pct=0.2)
|
|
result = _factor_attribution_summary(_snapshot(), [trade])
|
|
assert result is not None
|
|
assert result["factors"][0]["win_n"] == 1
|
|
|
|
|
|
def test_summary_returns_none_when_no_overlap():
|
|
trades = [_trade("600000", "2026-02-10", 0.1)] # 不在快照里
|
|
assert _factor_attribution_summary(_snapshot(), trades) is None
|
|
|
|
|
|
def test_summary_returns_none_on_empty_inputs():
|
|
assert _factor_attribution_summary(_snapshot(), []) is None
|
|
no_factor = pl.DataFrame({"symbol": ["000001"], "date": ["2026-01-05"]})
|
|
assert _factor_attribution_summary(no_factor, [_trade("000001", "2026-01-05", 0.1)]) is None
|