Files
chanlun.rs/chanlun-py/tests/test_signal_primitives.py
T
2026-06-27 18:17:57 +08:00

212 lines
7.4 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.
"""信号原语 Rust 移植后的跨语言一致性与边界行为测试。
验证 chanlun._chanlun 的 Signal/Factor/Event/Operate/Position 与移植前 Python 版本
行为一致(name hash 除外,已改为 Rust 确定性哈希)。
"""
import pytest
from chanlun._chanlun import Signal, Factor, Event, Operate, Position
# ---- Signal ----
def test_signal_parse_and_props():
s = Signal("14400_D1MO3_中枢_中枢段DEA穿越2_三买_偏移0_100")
assert s.k1 == "14400" and s.k3 == "中枢" and s.v2 == "三买" and s.score == 100
assert s.key == "14400_D1MO3_中枢"
assert s.value == "中枢段DEA穿越2_三买_偏移0_100"
assert repr(s) == "Signal('14400_D1MO3_中枢_中枢段DEA穿越2_三买_偏移0_100')"
def test_signal_from_fields_default_任意():
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
# v1/v3 缺省为 任意 → key 过滤后保留全部 k;value 含 任意
assert s.key == "14400_D1MO3_中枢"
assert s.value == "任意_三买_任意_0"
def test_signal_score_out_of_range():
with pytest.raises(ValueError):
Signal(k1="a", k2="b", k3="c", score=101)
def test_signal_is_match_missing_key_raises():
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
with pytest.raises(ValueError):
s.is_match({})
def test_signal_is_match_non_str_value_false():
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
assert s.is_match({"14400_D1MO3_中枢": 123}) is False
def test_signal_is_match_hit():
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
assert s.is_match({"14400_D1MO3_中枢": "x_三买_y_100"}) is True
def test_signal_is_match_v2_mismatch():
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
assert s.is_match({"14400_D1MO3_中枢": "x_三卖_y_100"}) is False
# ---- Factor ----
def test_factor_empty_all_raises():
with pytest.raises(ValueError):
Factor(signals_all=[])
def test_factor_name_deterministic():
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
f1 = Factor(signals_all=[s])
f2 = Factor(signals_all=[Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")])
assert f1.name == f2.name
assert f1.name.startswith("#") and len(f1.name) == 5 # #XXXX
def test_factor_not_short_circuit():
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
f = Factor(signals_all=[s], signals_not=[s])
assert f.is_match({"14400_D1MO3_中枢": "x_三买_y_100"}) is False
def test_factor_unique_signals_is_property():
"""unique_signals 必须是 property(匹配 Python @property),不带括号访问。"""
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
f = Factor(signals_all=[s])
assert f.unique_signals == [s.signal] # 属性访问,非方法调用
# ---- Event ----
def test_event_empty_factors_raises():
with pytest.raises(ValueError):
Event(Operate.LO, [])
def test_event_name_uses_operate():
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
e = Event(Operate.LO, [Factor(signals_all=[s])])
assert e.name.startswith("开多#")
def test_event_match_returns_factor_name():
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
e = Event(Operate.LO, [Factor(signals_all=[s])])
ok, name = e.is_match({"14400_D1MO3_中枢": "x_三买_y_100"})
assert ok and name
def test_event_multi_factor_or():
"""多 Factor OR:两 key 都在场,第一个不匹配、第二个匹配 → 返回第二个因子名。"""
base = "14400"
f1 = Factor(signals_all=[Signal(k1=base, k2="D1MO3", k3="中枢A", v2="三买")])
f2 = Factor(signals_all=[Signal(k1=base, k2="D1MO3", k3="中枢B", v2="三买")])
e = Event(Operate.LO, [f1, f2])
d = {"14400_D1MO3_中枢A": "x_三卖_y_100", "14400_D1MO3_中枢B": "x_三买_y_100"}
ok, name = e.is_match(d)
assert ok and name == f2.name
# ---- Operate ----
def test_operate_value_and_eq():
assert Operate.LO.value == "开多"
assert Operate.LE.value == "平多"
assert Operate.LO == Operate.LO
assert Operate.LO in [Operate.LO, Operate.SO] # update() 内部用法
# ---- PositionRust 基类 + Python 子类)----
def test_position_requires_name():
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
e = Event(Operate.LO, [Factor(signals_all=[s])])
with pytest.raises((ValueError, TypeError)):
Position(symbol="btc", opens=[e])
def test_position_config_getters():
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
e = Event(Operate.LO, [Factor(signals_all=[s])])
p = Position(symbol="btc", opens=[e], name="中枢", timeout=500, stop_loss=200, T0=True)
assert p.symbol == "btc" and p.name == "中枢"
assert p.timeout == 500 and p.stop_loss == 200 and p.T0 is True
assert len(p.events) == 1
assert p.unique_signals == [s.signal]
def test_position_subclassable_with_state():
"""验证 Rust 基类可被 Python 子类化,状态字段由 Rust 初始化。
pos/pos_changed/operates/holds 等状态字段由 Rust 基类提供(只读 getter),
初始值在构造时由 Rust #[new] 自动初始化。
"""
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
e = Event(Operate.LO, [Factor(signals_all=[s])])
p = Position(symbol="btc", opens=[e], name="中枢")
assert p.name == "中枢" # Rust 基类 getter
assert p.pos == 0 # Rust 初始化为 0 (空仓)
assert p.pos_changed is False
assert p.operates == []
assert p.holds == []
# ---- 序列化 dump/load ----
def test_operate_from_value():
assert Operate.from_value("开多") == Operate.LO
assert Operate.from_value("平空") == Operate.SE
with pytest.raises(ValueError):
Operate.from_value("不存在")
def test_factor_dump_load_roundtrip():
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
f = Factor(signals_all=[s], name="测试")
d = f.dump()
assert d["name"] == f.name
assert d["signals_all"] == [s.signal]
assert d["signals_any"] == [] and d["signals_not"] == []
f2 = Factor.load(d)
assert f2.name == f.name # 确定性哈希 → 同输入同名
assert f2.unique_signals == f.unique_signals
def test_event_dump_load_roundtrip():
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
e = Event(Operate.LO, [Factor(signals_all=[s])])
d = e.dump()
assert d["operate"] == "开多"
assert len(d["factors"]) == 1
e2 = Event.load(d)
assert e2.name == e.name
assert e2.operate == Operate.LO
def test_position_dump_load_roundtrip():
"""Position 序列化:Rust 基类 dump 配置 + Python 子类 with_data/load 返回子类实例。"""
from chanlun.chan_external import Position as PositionExt, Signal as S, Factor as F, Event as E, Operate as O
e = E(O.LO, [F(signals_all=[S(k1="14400", k2="D1MO3", k3="中枢", v2="三买")])])
p = PositionExt(symbol="btc", opens=[e], name="中枢", timeout=500, T0=True)
raw = p.dump()
assert raw["symbol"] == "btc" and raw["T0"] is True and raw["timeout"] == 500
assert len(raw["opens"]) == 1
# with_data 附加 pairs/holds
raw2 = p.dump(with_data=True)
assert "pairs" in raw2 and "holds" in raw2
# load 返回子类实例(含状态字段)
p2 = PositionExt.load(raw)
assert type(p2) is PositionExt
assert p2.symbol == "btc" and p2.name == "中枢" and p2.timeout == 500
assert p2.pos == 0 # 子类状态已初始化