142 lines
5.9 KiB
Python
142 lines
5.9 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
||
|
|
"""
|
||
|
|
第五批模块单测 (实机运行, 零外部依赖)
|
||
|
|
======================================
|
||
|
|
运行: 在 tradingSystem 仓库根目录执行 python scripts/test_batch5_units.py
|
||
|
|
覆盖: signal_rules 两条信号流的解析 (含两条流置信度尺度不同这个坑) 与消化口径。
|
||
|
|
"""
|
||
|
|
import json
|
||
|
|
import os
|
||
|
|
import sys
|
||
|
|
import traceback
|
||
|
|
|
||
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
|
|
|
||
|
|
from app.core import signal_rules as sr # noqa: E402
|
||
|
|
|
||
|
|
RESULTS = []
|
||
|
|
|
||
|
|
|
||
|
|
def case(name):
|
||
|
|
def deco(fn):
|
||
|
|
RESULTS.append((name, fn))
|
||
|
|
return fn
|
||
|
|
return deco
|
||
|
|
|
||
|
|
|
||
|
|
PRM = {"sell_conf_min": 0.75, "auto_exit_conf": 0.85, "trim_ratio": 1 / 3}
|
||
|
|
POS = {"ts_code": "600000.SH", "total_qty": 6000, "avail_qty": 6000, "price": 10.0}
|
||
|
|
|
||
|
|
|
||
|
|
# ================================================================ 解析
|
||
|
|
@case("解析·盘中流 (db2): 扁平字段 + component_scores 内嵌 JSON, 置信度本就是 0~1")
|
||
|
|
def _():
|
||
|
|
s = sr.parse_intraday({"ts_code": "600000.SH", "action": "buy", "confidence": "0.83",
|
||
|
|
"component_scores": json.dumps({"minute_qrs": 2.4}),
|
||
|
|
"suggested_price": "10.25"}, msg_id="1-1")
|
||
|
|
assert s["source"] == sr.SRC_INTRADAY and s["ts_code"] == "600000.SH"
|
||
|
|
assert s["action"] == "BUY" and abs(s["confidence"] - 0.83) < 1e-9
|
||
|
|
assert abs(s["minute_qrs"] - 2.4) < 1e-9 and s["suggested_price"] == 10.25
|
||
|
|
# component_scores 给成 dict 或坏 JSON 都不能炸
|
||
|
|
assert sr.parse_intraday({"component_scores": {"minute_qrs": 3}})["minute_qrs"] == 3.0
|
||
|
|
assert sr.parse_intraday({"component_scores": "{坏"})["minute_qrs"] == 0.0
|
||
|
|
|
||
|
|
|
||
|
|
@case("解析·风控流 (db3): 外层套 data JSON, 置信度是 0~100 要归一")
|
||
|
|
def _():
|
||
|
|
inner = {"ts_code": "600000.SH", "action": "SELL", "confidence": 88,
|
||
|
|
"dominant_signal": "破位", "llm_reason": "跌破关键支撑且量能背离"}
|
||
|
|
s = sr.parse_risk_sell({"data": json.dumps(inner, ensure_ascii=False)}, msg_id="2-1")
|
||
|
|
assert s["source"] == sr.SRC_RISK_SELL and s["action"] == "SELL"
|
||
|
|
assert abs(s["confidence"] - 0.88) < 1e-9, s # 88 → 0.88, 两条流尺度不同
|
||
|
|
assert s["dominant_signal"] == "破位" and "支撑" in s["reason"]
|
||
|
|
# 已经是 0~1 的也不会被再除一次
|
||
|
|
assert abs(sr.parse_risk_sell({"data": json.dumps({"ts_code": "x", "action": "SELL",
|
||
|
|
"confidence": 0.9})})["confidence"]
|
||
|
|
- 0.9) < 1e-9
|
||
|
|
# 坏 JSON → 明确标记, 不抛异常
|
||
|
|
bad = sr.parse_risk_sell({"data": "{不是JSON"})
|
||
|
|
assert bad["ts_code"] == "" and "解析失败" in bad["parse_error"]
|
||
|
|
|
||
|
|
|
||
|
|
# ================================================================ 消化
|
||
|
|
@case("消化·高置信风控卖出 → 直接清仓 (减持不设确认门槛)")
|
||
|
|
def _():
|
||
|
|
s = sr.parse_risk_sell({"data": json.dumps({"ts_code": "600000.SH", "action": "SELL",
|
||
|
|
"confidence": 90, "llm_reason": "逻辑走坏"})})
|
||
|
|
d = sr.digest(s, POS, PRM)
|
||
|
|
assert d["action"] == sr.ACT_EXIT and d["qty"] == 6000, d
|
||
|
|
assert "清仓" in d["reason"] and "逻辑走坏" in d["reason"], d
|
||
|
|
assert d["hard_numbers"]["confidence"] == 0.9
|
||
|
|
|
||
|
|
|
||
|
|
@case("消化·中等置信 → 落提议待确认 (按比例减)")
|
||
|
|
def _():
|
||
|
|
s = sr.parse_risk_sell({"data": json.dumps({"ts_code": "600000.SH", "action": "SELL",
|
||
|
|
"confidence": 80})})
|
||
|
|
d = sr.digest(s, POS, PRM)
|
||
|
|
assert d["action"] == sr.ACT_PROPOSE and d["qty"] == 2000, d # 6000 的 1/3, 整百
|
||
|
|
assert "待确认" in d["reason"]
|
||
|
|
|
||
|
|
|
||
|
|
@case("消化·低置信 / 未持有 / 缺代码 一律不动")
|
||
|
|
def _():
|
||
|
|
low = sr.parse_risk_sell({"data": json.dumps({"ts_code": "600000.SH", "action": "SELL",
|
||
|
|
"confidence": 60})})
|
||
|
|
assert sr.digest(low, POS, PRM)["action"] == sr.ACT_IGNORE
|
||
|
|
hi = sr.parse_risk_sell({"data": json.dumps({"ts_code": "600000.SH", "action": "SELL",
|
||
|
|
"confidence": 95})})
|
||
|
|
assert sr.digest(hi, {"total_qty": 0}, PRM)["action"] == sr.ACT_IGNORE
|
||
|
|
assert sr.digest(hi, None, PRM)["action"] == sr.ACT_IGNORE
|
||
|
|
assert sr.digest({"ts_code": "", "action": "SELL", "confidence": 0.95},
|
||
|
|
POS, PRM)["action"] == sr.ACT_IGNORE
|
||
|
|
|
||
|
|
|
||
|
|
@case("消化·BUY/HOLD 信号只留痕不买 (买什么买多少由 PMS 自己决定)")
|
||
|
|
def _():
|
||
|
|
b = sr.parse_intraday({"ts_code": "600000.SH", "action": "BUY", "confidence": "0.95"})
|
||
|
|
d = sr.digest(b, POS, PRM)
|
||
|
|
assert d["action"] == sr.ACT_RECORD and d["qty"] == 0, d
|
||
|
|
assert "不据此买入" in d["reason"], d
|
||
|
|
h = sr.parse_intraday({"ts_code": "600000.SH", "action": "HOLD", "confidence": "0.99"})
|
||
|
|
assert sr.digest(h, POS, PRM)["action"] == sr.ACT_RECORD
|
||
|
|
|
||
|
|
|
||
|
|
@case("消化·盘中流里的 SELL 也照样消化 (两条流同一套口径)")
|
||
|
|
def _():
|
||
|
|
s = sr.parse_intraday({"ts_code": "600000.SH", "action": "SELL", "confidence": "0.92"})
|
||
|
|
d = sr.digest(s, POS, PRM)
|
||
|
|
assert d["action"] == sr.ACT_EXIT and d["qty"] == 6000, d
|
||
|
|
|
||
|
|
|
||
|
|
@case("去重键·同票同源同动作当日只算一次")
|
||
|
|
def _():
|
||
|
|
s = {"source": sr.SRC_RISK_SELL, "ts_code": "600000.SH", "action": "SELL"}
|
||
|
|
k1 = sr.dedup_key(s, 20260728)
|
||
|
|
assert k1 == sr.dedup_key(dict(s), 20260728)
|
||
|
|
assert k1 != sr.dedup_key(s, 20260729)
|
||
|
|
assert k1 != sr.dedup_key({**s, "source": sr.SRC_INTRADAY}, 20260728)
|
||
|
|
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------- runner
|
||
|
|
def main():
|
||
|
|
passed, failed = 0, 0
|
||
|
|
for name, fn in RESULTS:
|
||
|
|
try:
|
||
|
|
fn()
|
||
|
|
print(f" PASS {name}")
|
||
|
|
passed += 1
|
||
|
|
except Exception:
|
||
|
|
print(f" FAIL {name}")
|
||
|
|
traceback.print_exc()
|
||
|
|
failed += 1
|
||
|
|
print("-" * 60)
|
||
|
|
if failed:
|
||
|
|
print(f"FAILED: {failed} / {passed + failed}")
|
||
|
|
sys.exit(1)
|
||
|
|
print(f"ALL PASS ({passed} cases)")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|