tradingSystem/scripts/test_batch5_units.py

155 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""
第五批模块单测 (实机运行, 零外部依赖)
======================================
运行: 在 tradingSystem 仓库根目录执行 python scripts/test_batch5_units.py
覆盖: signal_rules 两条信号流的解析 (含两条流置信度尺度不同这个坑) 与消化口径。
"""
import unit_test_guard # noqa: F401,单独运行也禁止联网
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"]
# 2026-08-28 口径修正: 风控流按文档**固定 0~100 制, 一律除以 100**, 不再做
# "大于 1 才除"的猜测 —— 原口径下 0~100 制里的 1 (即 1%) 会被当成 100% 直接清仓。
assert abs(sr.parse_risk_sell({"data": json.dumps({"ts_code": "x", "action": "SELL",
"confidence": 0.9})})["confidence"]
- 0.009) < 1e-9
# 危险边界: 1 是 1%, 绝不能被解释成 100%
assert abs(sr.parse_risk_sell({"data": json.dumps({"ts_code": "x", "action": "SELL",
"confidence": 1})})["confidence"]
- 0.01) < 1e-9
# 盘中流 (0~1 制) 的口径不变: 0.9 就是 90%
assert abs(sr.parse_intraday({"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 _():
# 2026-08-06: BUY 从 ACT_RECORD 改判 ACT_NOTE_BUY。**「不买」这条口径一个字没变** ——
# 变的只是留痕范围: RECORD 只给持仓票写账本, 而未持仓的票正是新建仓关心的那一批,
# 从前它们连一行痕迹都没有。NOTE_BUY 持没持仓都写。HOLD 等其余类型仍走 RECORD。
b = sr.parse_intraday({"ts_code": "600000.SH", "action": "BUY", "confidence": "0.95"})
d = sr.digest(b, POS, PRM)
assert d["action"] == sr.ACT_NOTE_BUY and d["qty"] == 0, d
assert "只留痕" in d["reason"] and "买不买由动作引擎" 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
assert "不据此买入" in sr.digest(h, POS, PRM)["reason"]
@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()