Files
tick-stock-panel/backend/tests/test_monitor_hit_message.py
T
shy3130 918c7ade7e refactor(core): 策略与信号全面去内置化, 转为用户自定义定义
- 策略: 引擎只加载 data/strategies/{custom,ai,composite} 与研究模板目录,
  原 25 个内置策略一次性迁移为用户自定义文件(ID 不变, 引用零迁移);
  研究模板移至 app/strategy/research, 打包 spec 同步; 新装环境策略库为空
- 信号: 管线不再硬编码任何信号表达式; 涨跌停判定价成为运行时列
  limit_up_price/limit_down_price(生效价∓0.005 容差), 20 个信号转为
  data/user_data/custom_signals 定义(显式 column 沿用 signal_* 原列名);
  依赖解析/注入顺序/当日路径 prev-join 全部定义驱动; 语义统一为历史路径
  口径(顺带修正当日路径 BOLL 比较符与翘板/炸板空值两处既有偏差)
- 前端: 信号库单列表(全部可编辑), SignalPicker/监控选项/信号标签全走
  定义接口; 回测页新增免责声明(模拟性质/数据源准确性不作保证/不构成
  投资建议)
- 测试: 25 个策略夹具与 20 个信号种子入库 tests/fixtures, 受影响测试
  全部自包含化(不依赖运行目录用户数据); 77 万行 MA 死叉新旧表达式对拍
  逐行一致; 回测实跑 45 笔卖点信号出场验证矩阵链路
- 同批包含工作区既有进度: 策略结果 ext 列同日投影修复(含测试)、数据源插件、fuyao/auth/api_gateway/data_integrity 调整、README 信号库
  描述与 strategy.md 导入路径补充

验证: 后端 2592 passed(空数据目录 CI 模拟 2588 passed, 4 例为模拟环境
伪影复验排除); 前端 vitest 125 passed + tsc 0 错 + vite build 通过
2026-10-10 21:10:44 +08:00

125 lines
4.7 KiB
Python

"""告警 message 以实际命中信号为主 + 自定义信号中文名解析。
修复前: signal/price/market 类告警的 message 拼的是规则全量条件 (8 个 "或" 连接),
看不出实际触发的是哪条; 自定义信号 (csg_/csgi_) 显示原始列名而非用户命名。
修复后: 有命中信号时 message 以「命中 信号A、信号B」开头; 无 truth 命中
(纯比较条件) 时回退原条件摘要; 自定义信号解析为用户命名。
"""
import json
import tempfile
from pathlib import Path
import polars as pl
from app.strategy.monitor import MonitorRuleEngine
from tests.signal_seeds import materialize_data_root
# 信号列已定义驱动: 标签 (signal_* → 中文名) 从种子定义目录解析,
# 不依赖运行目录的用户数据。
_SEED_ROOT = materialize_data_root(Path(tempfile.mkdtemp(prefix="tsp_sig_seeds_")))
def _engine() -> MonitorRuleEngine:
eng = MonitorRuleEngine()
eng.set_data_dir(_SEED_ROOT)
return eng
def _rule(rid="r1", conditions=None, **over):
rule = {
"id": rid, "name": rid, "type": "signal", "asset_type": "stock",
"scope": "symbols", "symbols": ["000001.SH"], "logic": "or",
"conditions": conditions or [], "cooldown_seconds": 0, "enabled": True,
}
rule.update(over)
return rule
def _df(**cols):
base = {"symbol": ["000001.SH"], "close": [3000.0], "change_pct": [0.01]}
base.update(cols)
return pl.DataFrame(base)
def _fire(eng, rule, df):
eng.set_rules([rule])
events = eng.evaluate(df, asset_type="stock", reset_strategy_results=False)
assert len(events) == 1
return events[0]["message"]
def test_message_leads_with_hit_signals():
"""OR 规则部分命中: message 只含实际命中的信号, 不再列全量条件。"""
eng = _engine()
rule = _rule(conditions=[
{"field": "signal_ma_dead_5_20", "op": "truth"},
{"field": "signal_macd_dead", "op": "truth"},
{"field": "signal_boll_breakout_upper", "op": "truth"},
])
df = _df(signal_ma_dead_5_20=[True], signal_macd_dead=[True],
signal_boll_breakout_upper=[False])
msg = _fire(eng, rule, df)
assert msg.startswith("命中 MA5下穿MA20、MACD死叉")
assert "突破布林上轨" not in msg
assert " 或 " not in msg
assert "现价 3000.0" in msg
def test_message_falls_back_to_conditions_when_no_truth_hit():
"""纯比较条件规则 (hit_sigs 为空): 保持原条件摘要格式, 行为不变。"""
eng = _engine()
rule = _rule(type="price",
conditions=[{"field": "close", "op": ">=", "value": 2500}])
msg = _fire(eng, rule, _df())
assert "收盘价>=2500" in msg
assert "现价 3000.0" in msg
def test_message_resolves_custom_signal_cn_name(tmp_path):
"""csg_ 自定义信号在 message 中显示用户命名, 而非原始列名。"""
d = tmp_path / "user_data" / "custom_signals"
d.mkdir(parents=True)
(d / "ma_dead_5_10.json").write_text(json.dumps({
"id": "ma_dead_5_10", "name": "5日线下穿10日线", "kind": "exit",
"conditions": [{"left": "ma5", "op": "<=", "right": "field:ma10"}],
}, ensure_ascii=False), encoding="utf-8")
eng = _engine()
eng.set_data_dir(tmp_path)
rule = _rule(conditions=[{"field": "csg_ma_dead_5_10", "op": "truth"}])
msg = _fire(eng, rule, _df(csg_ma_dead_5_10=[True]))
assert "5日线下穿10日线" in msg
assert "csg_ma_dead_5_10" not in msg
def test_message_and_logic_includes_comparison_conditions():
"""AND 规则 truth+比较混合: 比较条件全部满足, 一并补进 message (信息完整)。"""
eng = _engine()
rule = _rule(logic="and", conditions=[
{"field": "signal_macd_dead", "op": "truth"},
{"field": "close", "op": ">=", "value": 2500},
])
msg = _fire(eng, rule, _df(signal_macd_dead=[True]))
assert msg.startswith("命中 MACD死叉 且 收盘价>=2500")
assert "现价 3000.0" in msg
def test_message_or_logic_omits_comparison_conditions():
"""OR 规则 truth+比较混合: 无法判定比较条件是否为真, 不补进 message。"""
eng = _engine()
rule = _rule(logic="or", conditions=[
{"field": "signal_macd_dead", "op": "truth"},
{"field": "close", "op": ">=", "value": 2500},
])
msg = _fire(eng, rule, _df(signal_macd_dead=[True]))
assert msg.startswith("命中 MACD死叉 · ")
assert "收盘价" not in msg
def test_message_custom_signal_fallback_without_data_dir():
"""未注入 data_dir 时 csg_ 名称解析优雅回退为原始列名 (不报错)。"""
eng = _engine()
rule = _rule(conditions=[{"field": "csg_unknown_sig", "op": "truth"}])
msg = _fire(eng, rule, _df(csg_unknown_sig=[True]))
assert "csg_unknown_sig" in msg