现象
ETF 上跑任意策略回测,返回"在指定区间内未产生买入信号",0 笔交易——全程无任何报错。数据管道本身健康:同一窗口实测矩阵里有 50 次 MA5/MA20 金叉、7 个完整入场信号。
环境
- 版本:v0.2.1,TickFlow 免费档 + 自定义日K数据源
- ETF enriched 已正常落盘(矩阵 64 列指标现算正常)
- 复现资产:ETF(510300 等,代码前缀 51/15/56/58)
复现步骤
curl -X POST "http://localhost:3018/api/backtest/strategy/run" \
-H "Content-Type: application/json" \
-d '{"strategy_id":"ma_golden_cross","symbols":null,
"start":"2025-09-01","end":"2026-08-28",
"asset_type":"etf","holding_days":10,"max_positions":5,"mode":"position"}'
返回:"在指定区间内未产生买入信号",0 笔交易。但用同一份矩阵数据手工数金叉,一年窗口内有 50 次——信号是被过滤链静默清掉的,不是数据或策略问题。
根因
app/strategy/engine.py:36-48 的兜底过滤 DEFAULT_BASIC_FILTER 是纯股票口径:
DEFAULT_BASIC_FILTER: dict = {
"price_min": 3, # ETF 单价普遍 0.5~7 元, 整列误杀
"price_max": 300,
"market_cap_min": 10e8, # ETF 无股本概念
"amount_min": 0.2e8,
...
"boards": ["沪主板", "深主板", "创业板", "科创板", "北交所"],
}
其中 boards 走 app/backtest/matrix.py:4136 的 _symbol_in_boards,按股票代码前缀匹配:
def _symbol_in_boards(symbol: str, boards: list[str]) -> bool:
for board in boards:
if board == "沪主板" and symbol.startswith("60"):
return True
if board == "深主板" and symbol.startswith(("00", "001")):
...
ETF 代码(51/15/56/58 开头)不属于任何板块 → board_mask 全 False → basic 过滤掩码全 False → 所有 ETF 在入场前就被静默团灭。这比 #187 的报错更隐蔽:回测"正常完成",只是零信号,用户会误以为该策略在 ETF 上没有机会。
建议修法
在运行期(不只是依赖解析期)对 asset_type != "stock" 中和股票专属键(boards / price_min / price_max / market_cap_min / float_cap_*)。项目里已有先例:tests/test_matrix_etf_share_fields.py 针对市值界做了依赖解析前的中和,但运行期 basic_filter 用的是原始值(app/backtest/strategy.py 的回测/挖掘 run 路径、app/strategy/engine.py 的 screener _run_matrix_strategy 路径),没有同等处理。
验证
本地在以下三处调用点对非 stock 资产做了中和后,同一回测立刻恢复出信号(参数未变,一年窗口 7 笔交易、7 年窗口 98 笔):
app/backtest/strategy.py 回测 run 路径(约 line 847)
app/backtest/strategy.py 挖掘 run 路径(约 line 1014)
app/strategy/engine.py screener _run_matrix_strategy(约 line 1167)
关联
现象
ETF 上跑任意策略回测,返回"在指定区间内未产生买入信号",0 笔交易——全程无任何报错。数据管道本身健康:同一窗口实测矩阵里有 50 次 MA5/MA20 金叉、7 个完整入场信号。
环境
复现步骤
返回:"在指定区间内未产生买入信号",0 笔交易。但用同一份矩阵数据手工数金叉,一年窗口内有 50 次——信号是被过滤链静默清掉的,不是数据或策略问题。
根因
app/strategy/engine.py:36-48的兜底过滤DEFAULT_BASIC_FILTER是纯股票口径:其中
boards走app/backtest/matrix.py:4136的_symbol_in_boards,按股票代码前缀匹配:ETF 代码(51/15/56/58 开头)不属于任何板块 →
board_mask全 False → basic 过滤掩码全 False → 所有 ETF 在入场前就被静默团灭。这比 #187 的报错更隐蔽:回测"正常完成",只是零信号,用户会误以为该策略在 ETF 上没有机会。建议修法
在运行期(不只是依赖解析期)对
asset_type != "stock"中和股票专属键(boards/price_min/price_max/market_cap_min/float_cap_*)。项目里已有先例:tests/test_matrix_etf_share_fields.py针对市值界做了依赖解析前的中和,但运行期 basic_filter 用的是原始值(app/backtest/strategy.py的回测/挖掘 run 路径、app/strategy/engine.py的 screener_run_matrix_strategy路径),没有同等处理。验证
本地在以下三处调用点对非 stock 资产做了中和后,同一回测立刻恢复出信号(参数未变,一年窗口 7 笔交易、7 年窗口 98 笔):
app/backtest/strategy.py回测 run 路径(约 line 847)app/backtest/strategy.py挖掘 run 路径(约 line 1014)app/strategy/engine.pyscreener_run_matrix_strategy(约 line 1167)关联