2026-05-25 12:41:14 +08:00
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
"""Integration test: feed .nb bars through PyO3 observer, compare with Python reference output."""
|
|
|
|
|
|
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import os
|
|
|
|
|
|
import struct
|
2026-05-26 10:59:28 +08:00
|
|
|
|
import tempfile
|
2026-05-25 12:41:14 +08:00
|
|
|
|
|
|
|
|
|
|
import chanlun
|
|
|
|
|
|
|
2026-05-26 10:59:28 +08:00
|
|
|
|
# 项目根目录(test_integration.py 位于 <root>/chanlun-py/ 下)
|
|
|
|
|
|
# 当脚本被复制到其他路径运行时,通过环境变量 CHANLUN_PROJECT_ROOT 指定
|
|
|
|
|
|
_PROJECT_ROOT = os.environ.get(
|
|
|
|
|
|
"CHANLUN_PROJECT_ROOT",
|
|
|
|
|
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
NB_PATH = os.path.join(_PROJECT_ROOT, "btcusd-300-1761327300-1776327900.nb")
|
|
|
|
|
|
_PY_REF_DIR = os.path.join(_PROJECT_ROOT, "Py_btcusd:300_1761327300_1776327900")
|
|
|
|
|
|
_RUST_REF_DIR = os.path.join(_PROJECT_ROOT, "chanlun", "Rust_btcusd:300_1761327300_1776327900")
|
|
|
|
|
|
|
2026-05-25 12:41:14 +08:00
|
|
|
|
|
|
|
|
|
|
def read_nb_bars(path, max_bars=None):
|
|
|
|
|
|
"""Read bars from .nb file (48 bytes each: 6 × f64 big-endian)."""
|
|
|
|
|
|
bars = []
|
|
|
|
|
|
with open(path, "rb") as f:
|
|
|
|
|
|
i = 0
|
|
|
|
|
|
while True:
|
|
|
|
|
|
data = f.read(48)
|
|
|
|
|
|
if not data:
|
|
|
|
|
|
break
|
|
|
|
|
|
ts, o, h, l, c, v = struct.unpack(">6d", data)
|
|
|
|
|
|
bars.append((int(ts), o, h, l, c, v))
|
|
|
|
|
|
i += 1
|
|
|
|
|
|
if max_bars and i >= max_bars:
|
|
|
|
|
|
break
|
|
|
|
|
|
return bars
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-26 10:59:28 +08:00
|
|
|
|
# ============================================================
|
|
|
|
|
|
# 观察者 子类化 / 方法重写 测试
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_subclass_basic():
|
|
|
|
|
|
"""子类可创建,isinstance 正确."""
|
|
|
|
|
|
|
|
|
|
|
|
class Sub(chanlun.观察者):
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
obs = Sub("btcusd", 300)
|
|
|
|
|
|
assert isinstance(obs, chanlun.观察者)
|
|
|
|
|
|
assert type(obs).__name__ == "Sub"
|
|
|
|
|
|
assert obs.标识 == "btcusd:300"
|
|
|
|
|
|
assert obs.周期 == 300
|
|
|
|
|
|
print(" ✓ test_subclass_basic")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_subclass_init_extra_attrs():
|
|
|
|
|
|
"""子类 __init__ 可添加自定义属性."""
|
|
|
|
|
|
|
|
|
|
|
|
class Sub(chanlun.观察者):
|
|
|
|
|
|
def __init__(self, 符号, 周期):
|
|
|
|
|
|
self.tag = "custom"
|
|
|
|
|
|
self.count = 0
|
|
|
|
|
|
|
|
|
|
|
|
obs = Sub("btcusd", 300)
|
|
|
|
|
|
assert obs.tag == "custom"
|
|
|
|
|
|
assert obs.count == 0
|
|
|
|
|
|
# 基类字段不受影响
|
|
|
|
|
|
assert obs.标识 == "btcusd:300"
|
|
|
|
|
|
print(" ✓ test_subclass_init_extra_attrs")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_subclass_new_filter_kwargs():
|
|
|
|
|
|
"""__new__ 过滤子类专属参数,只把父类需要的传给 super().__new__."""
|
|
|
|
|
|
|
|
|
|
|
|
class Sub(chanlun.观察者):
|
|
|
|
|
|
def __new__(cls, 符号, 周期, *, extra=None, **kwargs):
|
|
|
|
|
|
return super().__new__(cls, 符号, 周期)
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, 符号, 周期, *, extra=None, **kwargs):
|
|
|
|
|
|
self.extra = extra
|
|
|
|
|
|
|
|
|
|
|
|
obs = Sub("btcusd", 300, extra={"debug": True})
|
|
|
|
|
|
assert obs.extra == {"debug": True}
|
|
|
|
|
|
assert obs.标识 == "btcusd:300"
|
|
|
|
|
|
|
|
|
|
|
|
obs2 = Sub("ethusd", 60)
|
|
|
|
|
|
assert obs2.extra is None
|
|
|
|
|
|
print(" ✓ test_subclass_new_filter_kwargs")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_subclass_new_pass_config():
|
|
|
|
|
|
"""__new__ 透传 配置 参数到父类."""
|
|
|
|
|
|
cfg = chanlun.缠论配置()
|
|
|
|
|
|
|
|
|
|
|
|
class Sub(chanlun.观察者):
|
|
|
|
|
|
def __new__(cls, 符号, 周期, 配置=None, *, tag="", **kwargs):
|
|
|
|
|
|
return super().__new__(cls, 符号, 周期, 配置=配置)
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, 符号, 周期, 配置=None, *, tag="", **kwargs):
|
|
|
|
|
|
self.tag = tag
|
|
|
|
|
|
|
|
|
|
|
|
obs = Sub("btcusd", 300, cfg, tag="test-tag")
|
|
|
|
|
|
assert obs.标识 == "btcusd:300"
|
|
|
|
|
|
assert obs.tag == "test-tag"
|
|
|
|
|
|
print(" ✓ test_subclass_new_pass_config")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_override_method_super_call():
|
|
|
|
|
|
"""重写 增加原始K线,super() 调用父类,全线管线运行."""
|
|
|
|
|
|
bars = read_nb_bars(NB_PATH, max_bars=500)
|
|
|
|
|
|
|
|
|
|
|
|
# 基类对照组
|
|
|
|
|
|
base_obs = chanlun.观察者("btcusd", 300)
|
|
|
|
|
|
for i, (ts, o, h, l, c, v) in enumerate(bars):
|
|
|
|
|
|
k = chanlun.K线.创建普K(f"base_{i}", ts, o, h, l, c, v, i, 300)
|
|
|
|
|
|
base_obs.增加原始K线(k)
|
|
|
|
|
|
|
|
|
|
|
|
# 子类实验组
|
|
|
|
|
|
class Sub(chanlun.观察者):
|
|
|
|
|
|
def __init__(self, 符号, 周期):
|
|
|
|
|
|
self.intercept_count = 0
|
|
|
|
|
|
self.intercept_timestamps = []
|
|
|
|
|
|
|
|
|
|
|
|
def 增加原始K线(self, 普K):
|
|
|
|
|
|
self.intercept_count += 1
|
|
|
|
|
|
self.intercept_timestamps.append(普K.时间戳)
|
|
|
|
|
|
super().增加原始K线(普K)
|
|
|
|
|
|
|
|
|
|
|
|
sub_obs = Sub("btcusd", 300)
|
|
|
|
|
|
for i, (ts, o, h, l, c, v) in enumerate(bars):
|
|
|
|
|
|
k = chanlun.K线.创建普K(f"sub_{i}", ts, o, h, l, c, v, i, 300)
|
|
|
|
|
|
sub_obs.增加原始K线(k)
|
|
|
|
|
|
|
|
|
|
|
|
# 拦截次数
|
|
|
|
|
|
assert sub_obs.intercept_count == 500
|
|
|
|
|
|
assert len(sub_obs.intercept_timestamps) == 500
|
|
|
|
|
|
|
|
|
|
|
|
# 各层级序列与基类完全一致
|
|
|
|
|
|
sequences = [
|
|
|
|
|
|
"普通K线序列",
|
|
|
|
|
|
"缠论K线序列",
|
|
|
|
|
|
"分型序列",
|
|
|
|
|
|
"笔序列",
|
|
|
|
|
|
"线段序列",
|
|
|
|
|
|
"中枢序列",
|
|
|
|
|
|
]
|
|
|
|
|
|
for attr in sequences:
|
|
|
|
|
|
base_len = len(getattr(base_obs, attr))
|
|
|
|
|
|
sub_len = len(getattr(sub_obs, attr))
|
|
|
|
|
|
assert base_len == sub_len, f"{attr}: base={base_len}, sub={sub_len}"
|
|
|
|
|
|
|
|
|
|
|
|
# 笔时间戳精确对比
|
|
|
|
|
|
base_pens = base_obs.笔序列
|
|
|
|
|
|
sub_pens = sub_obs.笔序列
|
|
|
|
|
|
for j, (bp, sp) in enumerate(zip(base_pens, sub_pens)):
|
|
|
|
|
|
assert bp.文.中.时间戳 == sp.文.中.时间戳, f"笔[{j}] 时间戳不一致"
|
|
|
|
|
|
|
|
|
|
|
|
print(" ✓ test_override_method_super_call")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_override_getter():
|
|
|
|
|
|
"""重写 @property getter,super() 取基类值."""
|
|
|
|
|
|
|
|
|
|
|
|
class Sub(chanlun.观察者):
|
|
|
|
|
|
@property
|
|
|
|
|
|
def 标识(self):
|
|
|
|
|
|
return f"[MOCKED] {super().标识}"
|
|
|
|
|
|
|
|
|
|
|
|
obs = Sub("btcusd", 300)
|
|
|
|
|
|
assert obs.标识 == "[MOCKED] btcusd:300"
|
|
|
|
|
|
# 其他 getter 不受影响
|
|
|
|
|
|
assert obs.周期 == 300
|
|
|
|
|
|
print(" ✓ test_override_getter")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_override_str_repr():
|
|
|
|
|
|
"""重写 __str__ / __repr__."""
|
|
|
|
|
|
|
|
|
|
|
|
class Sub(chanlun.观察者):
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
|
return f"Custom({self.标识})"
|
|
|
|
|
|
|
|
|
|
|
|
def __repr__(self):
|
|
|
|
|
|
return self.__str__()
|
|
|
|
|
|
|
|
|
|
|
|
obs = Sub("btcusd", 300)
|
|
|
|
|
|
assert str(obs) == "Custom(btcusd:300)"
|
|
|
|
|
|
assert repr(obs) == "Custom(btcusd:300)"
|
|
|
|
|
|
print(" ✓ test_override_str_repr")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_multi_level_inheritance():
|
|
|
|
|
|
"""多层继承,MRO 调用链完整."""
|
|
|
|
|
|
|
|
|
|
|
|
class Level1(chanlun.观察者):
|
|
|
|
|
|
def 增加原始K线(self, 普K):
|
|
|
|
|
|
self.l1_log = getattr(self, "l1_log", [])
|
|
|
|
|
|
self.l1_log.append("L1")
|
|
|
|
|
|
super().增加原始K线(普K)
|
|
|
|
|
|
|
|
|
|
|
|
class Level2(Level1):
|
|
|
|
|
|
def 增加原始K线(self, 普K):
|
|
|
|
|
|
self.l2_log = getattr(self, "l2_log", [])
|
|
|
|
|
|
self.l2_log.append("L2")
|
|
|
|
|
|
super().增加原始K线(普K)
|
|
|
|
|
|
|
|
|
|
|
|
obs = Level2("btcusd", 300)
|
|
|
|
|
|
k = chanlun.K线.创建普K("test", 1761327300, 100.0, 105.0, 99.0, 103.0, 1000.0, 0, 300)
|
|
|
|
|
|
obs.增加原始K线(k)
|
|
|
|
|
|
|
|
|
|
|
|
assert obs.l2_log == ["L2"], f"L2 log: {obs.l2_log}"
|
|
|
|
|
|
assert obs.l1_log == ["L1"], f"L1 log: {obs.l1_log}"
|
|
|
|
|
|
assert len(obs.普通K线序列) == 1
|
|
|
|
|
|
print(" ✓ test_multi_level_inheritance")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_unoverridden_method_inherited():
|
|
|
|
|
|
"""未重写的方法从基类直接继承."""
|
|
|
|
|
|
|
|
|
|
|
|
class Sub(chanlun.观察者):
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
obs = Sub("btcusd", 120)
|
|
|
|
|
|
k = chanlun.K线.创建普K("test", 1761327900, 100.0, 105.0, 99.0, 103.0, 1000.0, 0, 300)
|
|
|
|
|
|
obs.增加原始K线(k)
|
|
|
|
|
|
|
|
|
|
|
|
assert obs.标识 == "btcusd:120"
|
|
|
|
|
|
assert obs.周期 == 120
|
|
|
|
|
|
assert len(obs.普通K线序列) == 1
|
|
|
|
|
|
assert len(obs.缠论K线序列) == 1
|
|
|
|
|
|
# 静态重新分析 也能正常继承
|
|
|
|
|
|
obs.静态重新分析()
|
|
|
|
|
|
print(" ✓ test_unoverridden_method_inherited")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_override_reset():
|
|
|
|
|
|
"""重写 重置基础序列,子类状态也重置."""
|
|
|
|
|
|
|
|
|
|
|
|
class Sub(chanlun.观察者):
|
|
|
|
|
|
def __init__(self, 符号, 周期):
|
|
|
|
|
|
self.my_log = []
|
|
|
|
|
|
|
|
|
|
|
|
def 重置基础序列(self):
|
|
|
|
|
|
self.my_log.clear()
|
|
|
|
|
|
super().重置基础序列()
|
|
|
|
|
|
|
|
|
|
|
|
obs = Sub("btcusd", 300)
|
|
|
|
|
|
k = chanlun.K线.创建普K("test", 1761327300, 100.0, 105.0, 99.0, 103.0, 1000.0, 0, 300)
|
|
|
|
|
|
obs.增加原始K线(k)
|
|
|
|
|
|
obs.my_log.append("test")
|
|
|
|
|
|
|
|
|
|
|
|
assert len(obs.普通K线序列) == 1
|
|
|
|
|
|
obs.重置基础序列()
|
|
|
|
|
|
assert len(obs.普通K线序列) == 0
|
|
|
|
|
|
assert obs.my_log == []
|
|
|
|
|
|
print(" ✓ test_override_reset")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-28 01:07:16 +08:00
|
|
|
|
# --- Property getter 逐一覆盖 ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_override_getter_符号():
|
|
|
|
|
|
"""重写 符号 property."""
|
|
|
|
|
|
|
|
|
|
|
|
class Sub(chanlun.观察者):
|
|
|
|
|
|
@property
|
|
|
|
|
|
def 符号(self):
|
|
|
|
|
|
return f"[WRAPPED] {super().符号}"
|
|
|
|
|
|
|
|
|
|
|
|
obs = Sub("btcusd", 300)
|
|
|
|
|
|
assert obs.符号 == "[WRAPPED] btcusd"
|
|
|
|
|
|
print(" ✓ test_override_getter_符号")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_override_getter_周期():
|
|
|
|
|
|
"""重写 周期 property."""
|
|
|
|
|
|
|
|
|
|
|
|
class Sub(chanlun.观察者):
|
|
|
|
|
|
@property
|
|
|
|
|
|
def 周期(self):
|
|
|
|
|
|
return super().周期 * 60
|
|
|
|
|
|
|
|
|
|
|
|
obs = Sub("btcusd", 5)
|
|
|
|
|
|
assert obs.周期 == 300
|
|
|
|
|
|
print(" ✓ test_override_getter_周期")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_override_getter_配置():
|
|
|
|
|
|
"""重写 配置 property."""
|
|
|
|
|
|
|
|
|
|
|
|
class Sub(chanlun.观察者):
|
|
|
|
|
|
@property
|
|
|
|
|
|
def 配置(self):
|
|
|
|
|
|
self._cfg_accessed = True
|
|
|
|
|
|
return super().配置
|
|
|
|
|
|
|
|
|
|
|
|
obs = Sub("btcusd", 300)
|
|
|
|
|
|
cfg = obs.配置
|
|
|
|
|
|
assert cfg is not None
|
|
|
|
|
|
assert obs._cfg_accessed
|
|
|
|
|
|
print(" ✓ test_override_getter_配置")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_override_getter_当前K线():
|
|
|
|
|
|
"""重写 当前K线 property."""
|
|
|
|
|
|
|
|
|
|
|
|
class Sub(chanlun.观察者):
|
|
|
|
|
|
def __init__(self, 符号, 周期):
|
|
|
|
|
|
self._cur_k_accessed = False
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def 当前K线(self):
|
|
|
|
|
|
self._cur_k_accessed = True
|
|
|
|
|
|
return super().当前K线
|
|
|
|
|
|
|
|
|
|
|
|
obs = Sub("btcusd", 300)
|
|
|
|
|
|
k = chanlun.K线.创建普K("t", 1761327300, 100.0, 105.0, 99.0, 103.0, 1000.0, 0, 300)
|
|
|
|
|
|
obs.增加原始K线(k)
|
|
|
|
|
|
cur = obs.当前K线
|
|
|
|
|
|
assert cur is not None
|
|
|
|
|
|
assert obs._cur_k_accessed
|
|
|
|
|
|
print(" ✓ test_override_getter_当前K线")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_override_getter_当前缠K():
|
|
|
|
|
|
"""重写 当前缠K property."""
|
|
|
|
|
|
|
|
|
|
|
|
class Sub(chanlun.观察者):
|
|
|
|
|
|
def __init__(self, 符号, 周期):
|
|
|
|
|
|
self._cur_ck_accessed = False
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def 当前缠K(self):
|
|
|
|
|
|
self._cur_ck_accessed = True
|
|
|
|
|
|
return super().当前缠K
|
|
|
|
|
|
|
|
|
|
|
|
obs = Sub("btcusd", 300)
|
|
|
|
|
|
k = chanlun.K线.创建普K("t", 1761327300, 100.0, 105.0, 99.0, 103.0, 1000.0, 0, 300)
|
|
|
|
|
|
obs.增加原始K线(k)
|
|
|
|
|
|
cur = obs.当前缠K
|
|
|
|
|
|
assert cur is not None
|
|
|
|
|
|
assert obs._cur_ck_accessed
|
|
|
|
|
|
print(" ✓ test_override_getter_当前缠K")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_override_getter_观察员():
|
|
|
|
|
|
"""重写 观察员 property."""
|
|
|
|
|
|
|
|
|
|
|
|
class Sub(chanlun.观察者):
|
|
|
|
|
|
@property
|
|
|
|
|
|
def 观察员(self):
|
|
|
|
|
|
self._obs_accessed = True
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
obs = Sub("btcusd", 300)
|
|
|
|
|
|
assert obs.观察员 is obs
|
|
|
|
|
|
assert obs._obs_accessed
|
|
|
|
|
|
print(" ✓ test_override_getter_观察员")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- 序列 getter 覆盖 ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_override_sequence_getter():
|
|
|
|
|
|
"""重写关键序列 getter,super() 取基类值."""
|
|
|
|
|
|
|
|
|
|
|
|
class Sub(chanlun.观察者):
|
|
|
|
|
|
def __init__(self, 符号, 周期):
|
|
|
|
|
|
self._seq_accessed = set()
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def 普通K线序列(self):
|
|
|
|
|
|
self._seq_accessed.add("普通K线序列")
|
|
|
|
|
|
return super().普通K线序列
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def 缠论K线序列(self):
|
|
|
|
|
|
self._seq_accessed.add("缠论K线序列")
|
|
|
|
|
|
return super().缠论K线序列
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def 笔序列(self):
|
|
|
|
|
|
self._seq_accessed.add("笔序列")
|
|
|
|
|
|
return super().笔序列
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def 线段序列(self):
|
|
|
|
|
|
self._seq_accessed.add("线段序列")
|
|
|
|
|
|
return super().线段序列
|
|
|
|
|
|
|
|
|
|
|
|
obs = Sub("btcusd", 300)
|
|
|
|
|
|
k = chanlun.K线.创建普K("t", 1761327300, 100.0, 105.0, 99.0, 103.0, 1000.0, 0, 300)
|
|
|
|
|
|
obs.增加原始K线(k)
|
|
|
|
|
|
|
|
|
|
|
|
assert len(obs.普通K线序列) == 1
|
|
|
|
|
|
assert "普通K线序列" in obs._seq_accessed
|
|
|
|
|
|
assert len(obs.缠论K线序列) == 1
|
|
|
|
|
|
assert "缠论K线序列" in obs._seq_accessed
|
|
|
|
|
|
assert isinstance(obs.笔序列, list)
|
|
|
|
|
|
assert "笔序列" in obs._seq_accessed
|
|
|
|
|
|
assert isinstance(obs.线段序列, list)
|
|
|
|
|
|
assert "线段序列" in obs._seq_accessed
|
|
|
|
|
|
print(" ✓ test_override_sequence_getter")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_override_all_sequence_getters():
|
|
|
|
|
|
"""重写全部 15 个序列 getter,确认每个都可通过 super() 获取."""
|
|
|
|
|
|
|
|
|
|
|
|
all_seqs = [
|
|
|
|
|
|
"普通K线序列",
|
|
|
|
|
|
"缠论K线序列",
|
|
|
|
|
|
"分型序列",
|
|
|
|
|
|
"笔序列",
|
|
|
|
|
|
"笔_中枢序列",
|
|
|
|
|
|
"线段序列",
|
|
|
|
|
|
"中枢序列",
|
|
|
|
|
|
"扩展线段序列",
|
|
|
|
|
|
"扩展中枢序列",
|
|
|
|
|
|
"扩展线段序列_线段",
|
|
|
|
|
|
"扩展中枢序列_线段",
|
|
|
|
|
|
"线段_线段序列",
|
|
|
|
|
|
"线段_中枢序列",
|
|
|
|
|
|
"扩展线段序列_扩展线段",
|
|
|
|
|
|
"扩展中枢序列_扩展线段",
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
# 动态构建子类,重写全部序列 getter
|
|
|
|
|
|
def _make_getter(name):
|
|
|
|
|
|
@property
|
|
|
|
|
|
def getter(self, _name=name):
|
|
|
|
|
|
self._all_seq_accessed.add(_name)
|
|
|
|
|
|
# 通过 MRO 找到父类的 property 并调用
|
|
|
|
|
|
for cls in type(self).__mro__[1:]:
|
|
|
|
|
|
if hasattr(cls, _name) and isinstance(getattr(cls, _name, None), property):
|
|
|
|
|
|
return getattr(cls, _name).fget(self)
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
return getter
|
|
|
|
|
|
|
|
|
|
|
|
class Sub(chanlun.观察者):
|
|
|
|
|
|
def __init__(self, 符号, 周期):
|
|
|
|
|
|
self._all_seq_accessed = set()
|
|
|
|
|
|
|
|
|
|
|
|
for seq_name in all_seqs:
|
|
|
|
|
|
setattr(Sub, seq_name, _make_getter(seq_name))
|
|
|
|
|
|
|
|
|
|
|
|
obs = Sub("btcusd", 300)
|
|
|
|
|
|
k = chanlun.K线.创建普K("t", 1761327300, 100.0, 105.0, 99.0, 103.0, 1000.0, 0, 300)
|
|
|
|
|
|
obs.增加原始K线(k)
|
|
|
|
|
|
|
|
|
|
|
|
for seq_name in all_seqs:
|
|
|
|
|
|
seq = getattr(obs, seq_name)
|
|
|
|
|
|
assert seq_name in obs._all_seq_accessed, f"{seq_name} 未被拦截"
|
|
|
|
|
|
assert isinstance(seq, list), f"{seq_name} 不是 list,而是 {type(seq)}"
|
|
|
|
|
|
|
|
|
|
|
|
print(" ✓ test_override_all_sequence_getters")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- 方法重写 ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_override_加载本地数据():
|
|
|
|
|
|
"""重写 加载本地数据,super() 调基类."""
|
|
|
|
|
|
|
|
|
|
|
|
class Sub(chanlun.观察者):
|
|
|
|
|
|
def __init__(self, 符号, 周期):
|
|
|
|
|
|
self._loaded = False
|
|
|
|
|
|
self._load_count = 0
|
|
|
|
|
|
|
|
|
|
|
|
def 加载本地数据(self, 文件路径):
|
|
|
|
|
|
self._loaded = True
|
|
|
|
|
|
self._load_count += 1
|
|
|
|
|
|
super().加载本地数据(文件路径)
|
|
|
|
|
|
|
|
|
|
|
|
obs = Sub("btcusd", 300)
|
|
|
|
|
|
obs.加载本地数据(NB_PATH)
|
|
|
|
|
|
assert obs._loaded
|
|
|
|
|
|
assert obs._load_count == 1
|
|
|
|
|
|
assert len(obs.普通K线序列) > 0
|
|
|
|
|
|
print(" ✓ test_override_加载本地数据")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_override_静态重新分析():
|
|
|
|
|
|
"""重写 静态重新分析,super() 调基类."""
|
|
|
|
|
|
|
|
|
|
|
|
class Sub(chanlun.观察者):
|
|
|
|
|
|
def __init__(self, 符号, 周期):
|
|
|
|
|
|
self._reanalyzed = 0
|
|
|
|
|
|
|
|
|
|
|
|
def 静态重新分析(self):
|
|
|
|
|
|
self._reanalyzed += 1
|
|
|
|
|
|
super().静态重新分析()
|
|
|
|
|
|
|
|
|
|
|
|
obs = Sub("btcusd", 300)
|
|
|
|
|
|
obs.加载本地数据(NB_PATH)
|
|
|
|
|
|
bi_before = len(obs.笔序列)
|
|
|
|
|
|
obs.静态重新分析()
|
|
|
|
|
|
assert obs._reanalyzed == 1
|
|
|
|
|
|
# 重新分析后笔序列仍然存在
|
|
|
|
|
|
assert len(obs.笔序列) >= 0
|
|
|
|
|
|
print(" ✓ test_override_静态重新分析")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_override_测试_保存数据():
|
|
|
|
|
|
"""重写 测试_保存数据,super() 调基类."""
|
|
|
|
|
|
|
|
|
|
|
|
class Sub(chanlun.观察者):
|
|
|
|
|
|
def __init__(self, 符号, 周期):
|
|
|
|
|
|
self._saved = False
|
|
|
|
|
|
self._save_root = None
|
|
|
|
|
|
|
|
|
|
|
|
def 测试_保存数据(self, root=None):
|
|
|
|
|
|
self._saved = True
|
|
|
|
|
|
self._save_root = root
|
|
|
|
|
|
super().测试_保存数据(root)
|
|
|
|
|
|
|
|
|
|
|
|
obs = Sub("btcusd", 300)
|
|
|
|
|
|
k = chanlun.K线.创建普K("t", 1761327300, 100.0, 105.0, 99.0, 103.0, 1000.0, 0, 300)
|
|
|
|
|
|
obs.增加原始K线(k)
|
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
|
|
|
|
obs.测试_保存数据(tmpdir)
|
|
|
|
|
|
assert obs._saved
|
|
|
|
|
|
assert obs._save_root == tmpdir
|
|
|
|
|
|
print(" ✓ test_override_测试_保存数据")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_override_读取数据文件():
|
|
|
|
|
|
"""重写 classmethod 读取数据文件."""
|
|
|
|
|
|
|
|
|
|
|
|
class Sub(chanlun.观察者):
|
|
|
|
|
|
@classmethod
|
|
|
|
|
|
def 读取数据文件(cls, 文件路径, 配置=None):
|
|
|
|
|
|
obs = super().读取数据文件(文件路径, 配置)
|
|
|
|
|
|
obs._custom_classmethod_flag = True
|
|
|
|
|
|
return obs
|
|
|
|
|
|
|
|
|
|
|
|
obs = Sub.读取数据文件(NB_PATH)
|
|
|
|
|
|
assert isinstance(obs, Sub)
|
|
|
|
|
|
assert obs._custom_classmethod_flag
|
|
|
|
|
|
assert len(obs.普通K线序列) > 0
|
|
|
|
|
|
print(" ✓ test_override_读取数据文件")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- 完全重写(不调 super) ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_override_completely_no_super():
|
|
|
|
|
|
"""完全重写 增加原始K线,不调 super(),彻底接管."""
|
|
|
|
|
|
|
|
|
|
|
|
class Sub(chanlun.观察者):
|
|
|
|
|
|
def 增加原始K线(self, 普K):
|
|
|
|
|
|
self._custom = getattr(self, "_custom", [])
|
|
|
|
|
|
self._custom.append(普K.时间戳)
|
|
|
|
|
|
# 不调 super()
|
|
|
|
|
|
|
|
|
|
|
|
obs = Sub("btcusd", 300)
|
|
|
|
|
|
k = chanlun.K线.创建普K("t", 1761327300, 100.0, 105.0, 99.0, 103.0, 1000.0, 0, 300)
|
|
|
|
|
|
obs.增加原始K线(k)
|
|
|
|
|
|
assert obs._custom == [1761327300]
|
|
|
|
|
|
assert len(obs.普通K线序列) == 0 # 基类逻辑没执行
|
|
|
|
|
|
print(" ✓ test_override_completely_no_super")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- 同名继承(零重写) ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_override_identical_subclass():
|
|
|
|
|
|
"""同名继承(零重写),行为与基类完全一致."""
|
|
|
|
|
|
|
|
|
|
|
|
class Sub(chanlun.观察者):
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
base = chanlun.观察者("btcusd", 300)
|
|
|
|
|
|
sub = Sub("btcusd", 300)
|
|
|
|
|
|
|
|
|
|
|
|
bars = read_nb_bars(NB_PATH, max_bars=200)
|
|
|
|
|
|
for i, (ts, o, h, l, c, v) in enumerate(bars):
|
|
|
|
|
|
bk = chanlun.K线.创建普K(f"b_{i}", ts, o, h, l, c, v, i, 300)
|
|
|
|
|
|
sk = chanlun.K线.创建普K(f"s_{i}", ts, o, h, l, c, v, i, 300)
|
|
|
|
|
|
base.增加原始K线(bk)
|
|
|
|
|
|
sub.增加原始K线(sk)
|
|
|
|
|
|
|
|
|
|
|
|
for attr in ["普通K线序列", "笔序列", "线段序列", "中枢序列"]:
|
|
|
|
|
|
base_len = len(getattr(base, attr))
|
|
|
|
|
|
sub_len = len(getattr(sub, attr))
|
|
|
|
|
|
assert base_len == sub_len, f"{attr}: base={base_len}, sub={sub_len}"
|
|
|
|
|
|
|
|
|
|
|
|
print(" ✓ test_override_identical_subclass")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- 多层继承 ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_override_three_level_mixed():
|
|
|
|
|
|
"""三层继承,每层重写不同方法,MRO 链完整."""
|
|
|
|
|
|
|
|
|
|
|
|
class L1(chanlun.观察者):
|
|
|
|
|
|
def __init__(self, 符号, 周期):
|
|
|
|
|
|
self._l1_feed = 0
|
|
|
|
|
|
|
|
|
|
|
|
def 增加原始K线(self, 普K):
|
|
|
|
|
|
self._l1_feed += 1
|
|
|
|
|
|
super().增加原始K线(普K)
|
|
|
|
|
|
|
|
|
|
|
|
class L2(L1):
|
|
|
|
|
|
def __init__(self, 符号, 周期):
|
|
|
|
|
|
super().__init__(符号, 周期)
|
|
|
|
|
|
self._l2_标识 = 0
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def 标识(self):
|
|
|
|
|
|
self._l2_标识 += 1
|
|
|
|
|
|
return f"[L2] {super().标识}"
|
|
|
|
|
|
|
|
|
|
|
|
class L3(L2):
|
|
|
|
|
|
def __init__(self, 符号, 周期):
|
|
|
|
|
|
super().__init__(符号, 周期)
|
|
|
|
|
|
self._l3_reset = 0
|
|
|
|
|
|
|
|
|
|
|
|
def 重置基础序列(self):
|
|
|
|
|
|
self._l3_reset += 1
|
|
|
|
|
|
super().重置基础序列()
|
|
|
|
|
|
|
|
|
|
|
|
obs = L3("btcusd", 300)
|
|
|
|
|
|
k = chanlun.K线.创建普K("t", 1761327300, 100.0, 105.0, 99.0, 103.0, 1000.0, 0, 300)
|
|
|
|
|
|
obs.增加原始K线(k)
|
|
|
|
|
|
|
|
|
|
|
|
assert obs._l1_feed == 1, f"L1 feed: {obs._l1_feed}"
|
|
|
|
|
|
assert obs.标识 == "[L2] btcusd:300"
|
|
|
|
|
|
assert obs._l2_标识 == 1
|
|
|
|
|
|
obs.重置基础序列()
|
|
|
|
|
|
assert obs._l3_reset == 1
|
|
|
|
|
|
assert len(obs.普通K线序列) == 0
|
|
|
|
|
|
print(" ✓ test_override_three_level_mixed")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --- 跨方法调用 ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_override_cross_method_dispatch():
|
|
|
|
|
|
"""子类方法间相互调用,确保 self 始终指向最外层实例."""
|
|
|
|
|
|
|
|
|
|
|
|
class Sub(chanlun.观察者):
|
|
|
|
|
|
def __init__(self, 符号, 周期):
|
|
|
|
|
|
self._feed_called = False
|
|
|
|
|
|
self._reset_called = False
|
|
|
|
|
|
|
|
|
|
|
|
def 增加原始K线(self, 普K):
|
|
|
|
|
|
self._feed_called = True
|
|
|
|
|
|
super().增加原始K线(普K)
|
|
|
|
|
|
|
|
|
|
|
|
def 重置基础序列(self):
|
|
|
|
|
|
self._reset_called = True
|
|
|
|
|
|
super().重置基础序列()
|
|
|
|
|
|
|
|
|
|
|
|
def compound_operation(self, 普K):
|
|
|
|
|
|
self.增加原始K线(普K)
|
|
|
|
|
|
self.重置基础序列()
|
|
|
|
|
|
|
|
|
|
|
|
obs = Sub("btcusd", 300)
|
|
|
|
|
|
k = chanlun.K线.创建普K("t", 1761327300, 100.0, 105.0, 99.0, 103.0, 1000.0, 0, 300)
|
|
|
|
|
|
obs.compound_operation(k)
|
|
|
|
|
|
|
|
|
|
|
|
assert obs._feed_called
|
|
|
|
|
|
assert obs._reset_called
|
|
|
|
|
|
assert len(obs.普通K线序列) == 0 # feed 后又 reset
|
|
|
|
|
|
print(" ✓ test_override_cross_method_dispatch")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-26 10:59:28 +08:00
|
|
|
|
def run_subclass_tests():
|
|
|
|
|
|
print("=== 观察者 子类化/重写 测试 ===")
|
|
|
|
|
|
tests = [
|
|
|
|
|
|
test_subclass_basic,
|
|
|
|
|
|
test_subclass_init_extra_attrs,
|
|
|
|
|
|
test_subclass_new_filter_kwargs,
|
|
|
|
|
|
test_subclass_new_pass_config,
|
|
|
|
|
|
test_override_method_super_call,
|
|
|
|
|
|
test_override_getter,
|
|
|
|
|
|
test_override_str_repr,
|
|
|
|
|
|
test_multi_level_inheritance,
|
|
|
|
|
|
test_unoverridden_method_inherited,
|
|
|
|
|
|
test_override_reset,
|
2026-05-28 01:07:16 +08:00
|
|
|
|
# 新增:property getter 逐一覆盖
|
|
|
|
|
|
test_override_getter_符号,
|
|
|
|
|
|
test_override_getter_周期,
|
|
|
|
|
|
test_override_getter_配置,
|
|
|
|
|
|
test_override_getter_当前K线,
|
|
|
|
|
|
test_override_getter_当前缠K,
|
|
|
|
|
|
test_override_getter_观察员,
|
|
|
|
|
|
# 新增:序列 getter 覆盖
|
|
|
|
|
|
test_override_sequence_getter,
|
|
|
|
|
|
test_override_all_sequence_getters,
|
|
|
|
|
|
# 新增:方法重写
|
|
|
|
|
|
test_override_加载本地数据,
|
|
|
|
|
|
test_override_静态重新分析,
|
|
|
|
|
|
test_override_测试_保存数据,
|
|
|
|
|
|
test_override_读取数据文件,
|
|
|
|
|
|
# 新增:完全重写 / 同名继承 / 多层 / 跨方法
|
|
|
|
|
|
test_override_completely_no_super,
|
|
|
|
|
|
test_override_identical_subclass,
|
|
|
|
|
|
test_override_three_level_mixed,
|
|
|
|
|
|
test_override_cross_method_dispatch,
|
2026-05-26 10:59:28 +08:00
|
|
|
|
]
|
|
|
|
|
|
for test in tests:
|
|
|
|
|
|
try:
|
|
|
|
|
|
test()
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f" ✗ {test.__name__} FAILED: {e}")
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
|
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
return 1
|
|
|
|
|
|
print(" ✓ 全部通过")
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
# 集成对比测试
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_integration_test():
|
|
|
|
|
|
"""全量集成测试:喂入 .nb 数据,与 Python 参考输出对比。"""
|
|
|
|
|
|
out_dir = os.path.join(tempfile.gettempdir(), "chanlun_py_test_output")
|
2026-05-25 12:41:14 +08:00
|
|
|
|
|
|
|
|
|
|
# Read all bars
|
|
|
|
|
|
print("Reading bars from .nb file...")
|
2026-05-26 10:59:28 +08:00
|
|
|
|
bars = read_nb_bars(NB_PATH)
|
2026-05-25 12:41:14 +08:00
|
|
|
|
print(f" Read {len(bars)} bars")
|
|
|
|
|
|
|
|
|
|
|
|
# Create observer (default config)
|
|
|
|
|
|
print("Creating observer...")
|
|
|
|
|
|
obs = chanlun.观察者("btcusd", 300)
|
|
|
|
|
|
print(f" Observer: {obs.标识}, period={obs.周期}")
|
|
|
|
|
|
|
|
|
|
|
|
# Feed bars
|
|
|
|
|
|
print("Feeding bars...")
|
|
|
|
|
|
for i, (ts, o, h, l, c, v) in enumerate(bars):
|
|
|
|
|
|
k = chanlun.K线.创建普K(f"btcusd_{i}", ts, o, h, l, c, v, i, 300)
|
|
|
|
|
|
obs.增加原始K线(k)
|
|
|
|
|
|
if i % 10000 == 0:
|
|
|
|
|
|
print(f" Fed {i}/{len(bars)} bars")
|
|
|
|
|
|
|
|
|
|
|
|
print(f" Done. {len(obs.普通K线序列)} normal K lines, {len(obs.缠论K线序列)} Chan K lines")
|
|
|
|
|
|
|
|
|
|
|
|
# Save output
|
|
|
|
|
|
print(f"Saving data to {out_dir}...")
|
|
|
|
|
|
os.makedirs(out_dir, exist_ok=True)
|
|
|
|
|
|
obs.测试_保存数据(out_dir)
|
|
|
|
|
|
|
|
|
|
|
|
# Find the actual output subdirectory created by 测试_保存数据
|
|
|
|
|
|
subdirs = [d for d in os.listdir(out_dir) if os.path.isdir(os.path.join(out_dir, d))]
|
|
|
|
|
|
if not subdirs:
|
|
|
|
|
|
print("ERROR: No output subdirectory found!")
|
|
|
|
|
|
return 1
|
|
|
|
|
|
actual_out_dir = os.path.join(out_dir, subdirs[0])
|
|
|
|
|
|
out_files = sorted(os.listdir(actual_out_dir))
|
|
|
|
|
|
print(f" Output dir: {actual_out_dir}")
|
|
|
|
|
|
print(f" Output files ({len(out_files)}): {out_files}")
|
|
|
|
|
|
|
2026-05-28 01:07:16 +08:00
|
|
|
|
# Compare with Python reference (skip if not available)
|
|
|
|
|
|
if not os.path.isdir(_PY_REF_DIR):
|
|
|
|
|
|
print(f"\nPython reference dir not found: {_PY_REF_DIR}")
|
|
|
|
|
|
print("Skipping Python comparison. Generate reference with chan.py first.")
|
|
|
|
|
|
print(f"Rust output is at: {actual_out_dir}")
|
|
|
|
|
|
print(f"Files: {out_files}")
|
|
|
|
|
|
# Self-consistency check: at least 14 output files expected
|
|
|
|
|
|
assert len(out_files) >= 14, f"Expected >= 14 output files, got {len(out_files)}"
|
|
|
|
|
|
print("Self-consistency check passed.")
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
2026-05-25 12:41:14 +08:00
|
|
|
|
print("\nComparing with Python reference...")
|
2026-05-26 10:59:28 +08:00
|
|
|
|
ref_files = sorted(os.listdir(_PY_REF_DIR))
|
2026-05-25 12:41:14 +08:00
|
|
|
|
|
|
|
|
|
|
match_count = 0
|
|
|
|
|
|
diff_count = 0
|
|
|
|
|
|
all_match = True
|
|
|
|
|
|
for fname in ref_files:
|
2026-05-26 10:59:28 +08:00
|
|
|
|
ref_path = os.path.join(_PY_REF_DIR, fname)
|
2026-05-25 12:41:14 +08:00
|
|
|
|
out_path = os.path.join(actual_out_dir, fname)
|
|
|
|
|
|
|
|
|
|
|
|
if not os.path.exists(out_path):
|
|
|
|
|
|
print(f" MISSING: {fname}")
|
|
|
|
|
|
all_match = False
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
with open(ref_path) as f:
|
|
|
|
|
|
ref_lines = f.readlines()
|
|
|
|
|
|
with open(out_path) as f:
|
|
|
|
|
|
out_lines = f.readlines()
|
|
|
|
|
|
|
|
|
|
|
|
if ref_lines == out_lines:
|
|
|
|
|
|
print(f" MATCH: {fname} ({len(ref_lines)} lines)")
|
|
|
|
|
|
match_count += 1
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f" DIFF: {fname} (ref={len(ref_lines)} lines, out={len(out_lines)} lines)")
|
|
|
|
|
|
for j, (rl, ol) in enumerate(zip(ref_lines, out_lines)):
|
|
|
|
|
|
if rl != ol:
|
|
|
|
|
|
print(f" Line {j}:")
|
|
|
|
|
|
print(f" REF: {rl.rstrip()}")
|
|
|
|
|
|
print(f" OUT: {ol.rstrip()}")
|
|
|
|
|
|
break
|
|
|
|
|
|
if len(ref_lines) != len(out_lines):
|
|
|
|
|
|
print(f" Line count differs")
|
|
|
|
|
|
diff_count += 1
|
|
|
|
|
|
all_match = False
|
|
|
|
|
|
|
|
|
|
|
|
# Also compare extra files against Rust reference
|
|
|
|
|
|
extra_files = set(out_files) - set(ref_files)
|
|
|
|
|
|
if extra_files:
|
|
|
|
|
|
print("\nComparing extra files with Rust reference...")
|
|
|
|
|
|
for fname in sorted(extra_files):
|
|
|
|
|
|
out_path = os.path.join(actual_out_dir, fname)
|
2026-05-26 10:59:28 +08:00
|
|
|
|
rust_ref_path = os.path.join(_RUST_REF_DIR, fname)
|
2026-05-25 12:41:14 +08:00
|
|
|
|
if os.path.exists(rust_ref_path):
|
|
|
|
|
|
with open(rust_ref_path) as f:
|
|
|
|
|
|
ref_lines = f.readlines()
|
|
|
|
|
|
with open(out_path) as f:
|
|
|
|
|
|
out_lines = f.readlines()
|
|
|
|
|
|
if ref_lines == out_lines:
|
|
|
|
|
|
print(f" MATCH: {fname} (vs Rust ref, {len(ref_lines)} lines)")
|
|
|
|
|
|
match_count += 1
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f" DIFF: {fname} (vs Rust ref)")
|
|
|
|
|
|
diff_count += 1
|
|
|
|
|
|
|
|
|
|
|
|
print(f"\nSummary: {match_count} match, {diff_count} differ")
|
|
|
|
|
|
if all_match:
|
|
|
|
|
|
print("All Python reference files match!")
|
|
|
|
|
|
return 0
|
|
|
|
|
|
else:
|
|
|
|
|
|
print("Some files differ (see above)")
|
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-28 01:07:16 +08:00
|
|
|
|
# ============================================================
|
|
|
|
|
|
# K线.截取 测试
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_kxian_jiequ_basic():
|
|
|
|
|
|
"""Python端创建K线,测试 K线.截取 基本功能."""
|
|
|
|
|
|
k1 = chanlun.K线.创建普K("k1", 1000000000, 100.0, 105.0, 98.0, 102.0, 1000.0, 0, 300)
|
|
|
|
|
|
k2 = chanlun.K线.创建普K("k2", 1000000060, 102.0, 108.0, 101.0, 106.0, 1200.0, 1, 300)
|
|
|
|
|
|
k3 = chanlun.K线.创建普K("k3", 1000000120, 106.0, 110.0, 104.0, 108.0, 900.0, 2, 300)
|
|
|
|
|
|
k4 = chanlun.K线.创建普K("k4", 1000000180, 108.0, 112.0, 107.0, 110.0, 1100.0, 3, 300)
|
|
|
|
|
|
ks = [k1, k2, k3, k4]
|
|
|
|
|
|
|
|
|
|
|
|
# 截取中间 (k2~k4),含头尾
|
|
|
|
|
|
r = chanlun.K线.截取(ks, k2, k4)
|
|
|
|
|
|
assert len(r) == 3
|
|
|
|
|
|
assert r[0].时间戳 == k2.时间戳
|
|
|
|
|
|
assert r[-1].时间戳 == k4.时间戳
|
|
|
|
|
|
|
|
|
|
|
|
# 截取全部
|
|
|
|
|
|
r = chanlun.K线.截取(ks, k1, k4)
|
|
|
|
|
|
assert len(r) == 4
|
|
|
|
|
|
|
|
|
|
|
|
# 截取单根
|
|
|
|
|
|
r = chanlun.K线.截取(ks, k3, k3)
|
|
|
|
|
|
assert len(r) == 1
|
|
|
|
|
|
assert r[0].时间戳 == k3.时间戳
|
|
|
|
|
|
|
|
|
|
|
|
print(" ✓ test_kxian_jiequ_basic")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_kxian_jiequ_error():
|
|
|
|
|
|
"""K线.截取 异常边界测试."""
|
|
|
|
|
|
k1 = chanlun.K线.创建普K("k1", 1000000000, 100.0, 105.0, 98.0, 102.0, 1000.0, 0, 300)
|
|
|
|
|
|
k2 = chanlun.K线.创建普K("k2", 1000000060, 102.0, 108.0, 101.0, 106.0, 1200.0, 1, 300)
|
|
|
|
|
|
k3 = chanlun.K线.创建普K("k3", 1000000120, 106.0, 110.0, 104.0, 108.0, 900.0, 2, 300)
|
|
|
|
|
|
ks = [k1, k2, k3]
|
|
|
|
|
|
|
|
|
|
|
|
# 空序列 → ValueError
|
|
|
|
|
|
try:
|
|
|
|
|
|
chanlun.K线.截取([], k1, k1)
|
|
|
|
|
|
raise AssertionError("空序列应抛 ValueError")
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
# K线不在序列中 → ValueError
|
|
|
|
|
|
kx = chanlun.K线.创建普K("kx", 9999999999, 1.0, 1.0, 1.0, 1.0, 1.0, 99, 300)
|
|
|
|
|
|
try:
|
|
|
|
|
|
chanlun.K线.截取(ks, kx, k3)
|
|
|
|
|
|
raise AssertionError("不在序列中应抛 ValueError")
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
# 始在终之后 → ValueError
|
|
|
|
|
|
try:
|
|
|
|
|
|
chanlun.K线.截取(ks, k3, k1)
|
|
|
|
|
|
raise AssertionError("始在终之后应抛 ValueError")
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
print(" ✓ test_kxian_jiequ_error")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_kxian_jiequ_from_observer():
|
|
|
|
|
|
"""通过观察者加载数据,从笔中获取普K序列再做截取."""
|
|
|
|
|
|
cfg = chanlun.缠论配置()
|
|
|
|
|
|
obs = chanlun.观察者.读取数据文件(NB_PATH, cfg)
|
|
|
|
|
|
|
|
|
|
|
|
assert len(obs.笔序列) > 0, "笔序列为空,数据可能不够"
|
|
|
|
|
|
bi = obs.笔序列[0]
|
|
|
|
|
|
|
|
|
|
|
|
# 从笔获取普K子序列
|
|
|
|
|
|
pk_seq = bi.获取普K序列(obs)
|
|
|
|
|
|
assert len(pk_seq) > 0
|
|
|
|
|
|
|
|
|
|
|
|
# 用标的K线在全序列上截取,验证与笔首尾匹配
|
|
|
|
|
|
r = chanlun.K线.截取(obs.普通K线序列, bi.文.中.标的K线, bi.武.中.标的K线)
|
|
|
|
|
|
assert len(r) > 0
|
|
|
|
|
|
assert r[0].时间戳 == bi.文.中.标的K线.时间戳
|
|
|
|
|
|
assert r[-1].时间戳 == bi.武.中.标的K线.时间戳
|
|
|
|
|
|
|
|
|
|
|
|
# 截取前半段
|
|
|
|
|
|
mid = len(pk_seq) // 2
|
|
|
|
|
|
if mid > 0:
|
|
|
|
|
|
r = chanlun.K线.截取(pk_seq, pk_seq[0], pk_seq[mid])
|
|
|
|
|
|
assert len(r) == mid + 1
|
|
|
|
|
|
assert r[0].时间戳 == pk_seq[0].时间戳
|
|
|
|
|
|
assert r[-1].时间戳 == pk_seq[mid].时间戳
|
|
|
|
|
|
|
|
|
|
|
|
print(f" ✓ test_kxian_jiequ_from_observer (笔0: {len(pk_seq)}根普K)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_kxian_jiequ_multi_bi():
|
|
|
|
|
|
"""遍历多根笔,各自截取笔内嵌K并验证首尾."""
|
|
|
|
|
|
cfg = chanlun.缠论配置()
|
|
|
|
|
|
obs = chanlun.观察者.读取数据文件(NB_PATH, cfg)
|
|
|
|
|
|
|
|
|
|
|
|
n_checked = 0
|
|
|
|
|
|
for i, bi in enumerate(obs.笔序列[:10]):
|
|
|
|
|
|
pk_seq = bi.获取普K序列(obs)
|
|
|
|
|
|
if len(pk_seq) < 2:
|
|
|
|
|
|
continue
|
|
|
|
|
|
r = chanlun.K线.截取(pk_seq, pk_seq[0], pk_seq[-1])
|
|
|
|
|
|
assert len(r) == len(pk_seq), f"笔[{i}] 截取长度不一致: {len(r)} vs {len(pk_seq)}"
|
|
|
|
|
|
n_checked += 1
|
|
|
|
|
|
|
|
|
|
|
|
assert n_checked > 0
|
|
|
|
|
|
print(f" ✓ test_kxian_jiequ_multi_bi (检查 {n_checked} 根笔)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_jiequ_tests():
|
|
|
|
|
|
print("=== K线.截取 测试 ===")
|
|
|
|
|
|
tests = [
|
|
|
|
|
|
test_kxian_jiequ_basic,
|
|
|
|
|
|
test_kxian_jiequ_error,
|
|
|
|
|
|
test_kxian_jiequ_from_observer,
|
|
|
|
|
|
test_kxian_jiequ_multi_bi,
|
|
|
|
|
|
]
|
|
|
|
|
|
for test in tests:
|
|
|
|
|
|
try:
|
|
|
|
|
|
test()
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f" ✗ {test.__name__} FAILED: {e}")
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
|
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
return 1
|
|
|
|
|
|
print(" ✓ 全部通过")
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
# Rc 指针身份 / list.index 测试
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _load_observer(max_bars=None):
|
|
|
|
|
|
"""辅助函数:从 .nb 文件加载数据并创建观察者."""
|
|
|
|
|
|
cfg = chanlun.缠论配置()
|
|
|
|
|
|
obs = chanlun.观察者("btcusd", 300, cfg)
|
|
|
|
|
|
bars = read_nb_bars(NB_PATH, max_bars=max_bars)
|
|
|
|
|
|
for i, (ts, o, h, l, c, v) in enumerate(bars):
|
|
|
|
|
|
k = chanlun.K线.创建普K(f"k{i}", ts, o, h, l, c, v, i, 300)
|
|
|
|
|
|
obs.增加原始K线(k)
|
|
|
|
|
|
return obs
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_rc_same_seq_index():
|
|
|
|
|
|
"""同一序列内 list.index 基于 Rc 指针身份正常工作."""
|
|
|
|
|
|
obs = _load_observer()
|
|
|
|
|
|
|
|
|
|
|
|
# 分型序列
|
|
|
|
|
|
fx_list = obs.分型序列
|
|
|
|
|
|
if len(fx_list) >= 2:
|
|
|
|
|
|
assert fx_list.index(fx_list[0]) == 0
|
|
|
|
|
|
assert fx_list.index(fx_list[1]) == 1
|
|
|
|
|
|
|
|
|
|
|
|
# 缠论K线序列
|
|
|
|
|
|
ck_list = obs.缠论K线序列
|
|
|
|
|
|
if len(ck_list) >= 2:
|
|
|
|
|
|
assert ck_list.index(ck_list[0]) == 0
|
|
|
|
|
|
assert ck_list.index(ck_list[-1]) == len(ck_list) - 1
|
|
|
|
|
|
|
|
|
|
|
|
# 笔序列
|
|
|
|
|
|
bi_list = obs.笔序列
|
|
|
|
|
|
if len(bi_list) >= 2:
|
|
|
|
|
|
assert bi_list.index(bi_list[0]) == 0
|
|
|
|
|
|
assert bi_list.index(bi_list[-1]) == len(bi_list) - 1
|
|
|
|
|
|
|
|
|
|
|
|
# 线段中的笔序列
|
|
|
|
|
|
if len(obs.线段序列) > 0:
|
|
|
|
|
|
for 段 in obs.线段序列:
|
|
|
|
|
|
笔列表 = 段.笔序列
|
|
|
|
|
|
if len(笔列表) >= 2:
|
|
|
|
|
|
assert 笔列表.index(笔列表[0]) == 0
|
|
|
|
|
|
assert 笔列表.index(笔列表[-1]) == len(笔列表) - 1
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
# 中枢序列
|
|
|
|
|
|
zs_list = obs.中枢序列
|
|
|
|
|
|
if len(zs_list) >= 2:
|
|
|
|
|
|
assert zs_list.index(zs_list[0]) == 0
|
|
|
|
|
|
assert zs_list.index(zs_list[-1]) == len(zs_list) - 1
|
|
|
|
|
|
|
|
|
|
|
|
print(" ✓ test_rc_same_seq_index")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_rc_cross_seq_index():
|
|
|
|
|
|
"""跨序列 list.index:段.笔序列 中查找 中枢.基础序列 元素."""
|
|
|
|
|
|
obs = _load_observer()
|
|
|
|
|
|
|
|
|
|
|
|
n_checked = 0
|
|
|
|
|
|
for 段 in obs.线段序列:
|
|
|
|
|
|
zs_list = 段.合_中枢序列
|
|
|
|
|
|
if len(zs_list) == 0:
|
|
|
|
|
|
continue
|
|
|
|
|
|
zs = zs_list[-1]
|
|
|
|
|
|
笔列表 = 段.笔序列
|
|
|
|
|
|
for j, elem in enumerate(zs.基础序列):
|
|
|
|
|
|
idx = 笔列表.index(elem)
|
|
|
|
|
|
assert idx >= 0
|
|
|
|
|
|
assert 笔列表[idx] == elem
|
|
|
|
|
|
n_checked += 1
|
|
|
|
|
|
break # 只测第一个有中枢的段
|
|
|
|
|
|
|
|
|
|
|
|
assert n_checked >= 3, f"应至少检查3个中枢元素,实际 {n_checked}"
|
|
|
|
|
|
print(f" ✓ test_rc_cross_seq_index (检查 {n_checked} 个元素)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_rc_zhaofenxing_cross_ref():
|
|
|
|
|
|
"""分型序列与笔之间的跨序列引用."""
|
|
|
|
|
|
obs = _load_observer()
|
|
|
|
|
|
|
|
|
|
|
|
fx_list = obs.分型序列
|
|
|
|
|
|
if len(fx_list) >= 2 and len(obs.笔序列) > 0:
|
|
|
|
|
|
# 笔.文 (起始分型) 应能在分型序列中找到
|
|
|
|
|
|
bi = obs.笔序列[-1]
|
|
|
|
|
|
try:
|
|
|
|
|
|
idx = fx_list.index(bi.文)
|
|
|
|
|
|
assert idx >= 0
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
# 可能因为分型过滤导致不在序列中,但不应 panic
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
print(" ✓ test_rc_zhaofenxing_cross_ref")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_rc_user_pattern():
|
|
|
|
|
|
"""复现用户报告的原始代码模式:在中枢基础序列第一个笔之前找同向笔."""
|
|
|
|
|
|
obs = _load_observer()
|
|
|
|
|
|
|
|
|
|
|
|
n_segments_with_zs = 0
|
|
|
|
|
|
for 段 in obs.线段序列:
|
|
|
|
|
|
zs_list = 段.合_中枢序列
|
|
|
|
|
|
if len(zs_list) == 0:
|
|
|
|
|
|
continue
|
|
|
|
|
|
n_segments_with_zs += 1
|
|
|
|
|
|
zs = zs_list[-1]
|
|
|
|
|
|
笔列表 = 段.笔序列
|
|
|
|
|
|
|
|
|
|
|
|
# 用户原始代码模式
|
|
|
|
|
|
try:
|
|
|
|
|
|
idx = 笔列表.index(zs.基础序列[0])
|
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
|
raise AssertionError(f"段[{段.序号}] 笔列表中找不到中枢基础序列[0]: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
# 向前查找同向笔
|
|
|
|
|
|
found = False
|
|
|
|
|
|
for bi in reversed(笔列表[:idx]):
|
|
|
|
|
|
if bi.方向 == zs.基础序列[0].方向:
|
|
|
|
|
|
found = True
|
|
|
|
|
|
break
|
|
|
|
|
|
# found 可以为 True 或 False(取决于是否有前向笔)
|
|
|
|
|
|
|
|
|
|
|
|
assert n_segments_with_zs > 0, "应至少有一个段包含中枢"
|
|
|
|
|
|
print(f" ✓ test_rc_user_pattern ({n_segments_with_zs} 个有中枢的段)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_tongji_macd_behavior_types():
|
|
|
|
|
|
"""统计MACD行为返回值类型正确: int和list(tuple),非str."""
|
|
|
|
|
|
obs = _load_observer(max_bars=5000)
|
|
|
|
|
|
if len(obs.笔序列) == 0:
|
|
|
|
|
|
print(" - test_tongji_macd_behavior_types SKIP (无笔)")
|
|
|
|
|
|
return
|
|
|
|
|
|
笔 = obs.笔序列[-1]
|
|
|
|
|
|
普K序列 = 笔.获取普K序列(obs)
|
|
|
|
|
|
if len(普K序列) < 2:
|
|
|
|
|
|
print(" - test_tongji_macd_behavior_types SKIP (K线不够)")
|
|
|
|
|
|
return
|
|
|
|
|
|
result = chanlun.虚线.统计MACD行为(普K序列, 8, 3)
|
|
|
|
|
|
for key in ["DIF上穿0", "DIF下穿0", "DEA上穿0", "DEA下穿0", "金叉次数", "死叉次数"]:
|
|
|
|
|
|
assert isinstance(result[key], int), f"{key} 应为 int, 实际 {type(result[key])}"
|
|
|
|
|
|
assert isinstance(result["密集交叉区域"], list), f"密集交叉区域 应为 list"
|
|
|
|
|
|
for item in result["密集交叉区域"]:
|
|
|
|
|
|
assert isinstance(item, tuple), f"密集交叉区域元素 应为 tuple"
|
|
|
|
|
|
assert len(item) == 3
|
|
|
|
|
|
assert all(isinstance(v, int) for v in item)
|
|
|
|
|
|
# 验证比较操作可用
|
|
|
|
|
|
_ = result["DEA上穿0"] > 0 and result["DEA下穿0"] > 0
|
|
|
|
|
|
print(" ✓ test_tongji_macd_behavior_types")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_xiangduifangxiang_methods():
|
|
|
|
|
|
"""相对方向.是否向上/是否向下 等必须是方法(需要括号调用),不能是 property."""
|
|
|
|
|
|
fx = chanlun.相对方向.分析(2.0, 0.5, 1.0, 0.8)
|
|
|
|
|
|
# 验证是可调用的方法
|
|
|
|
|
|
assert callable(fx.是否向上), "是否向上 必须是方法"
|
|
|
|
|
|
assert callable(fx.是否向下), "是否向下 必须是方法"
|
|
|
|
|
|
assert callable(fx.是否包含), "是否包含 必须是方法"
|
|
|
|
|
|
assert callable(fx.是否缺口), "是否缺口 必须是方法"
|
|
|
|
|
|
assert callable(fx.是否衔接), "是否衔接 必须是方法"
|
|
|
|
|
|
# 验证返回值类型
|
|
|
|
|
|
assert isinstance(fx.是否向上(), bool)
|
|
|
|
|
|
assert isinstance(fx.是否向下(), bool)
|
|
|
|
|
|
|
|
|
|
|
|
print(" ✓ test_xiangduifangxiang_methods")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run_rc_identity_tests():
|
|
|
|
|
|
print("=== Rc 身份 / list.index 测试 ===")
|
|
|
|
|
|
tests = [
|
|
|
|
|
|
test_tongji_macd_behavior_types,
|
|
|
|
|
|
test_xiangduifangxiang_methods,
|
|
|
|
|
|
test_rc_same_seq_index,
|
|
|
|
|
|
test_rc_cross_seq_index,
|
|
|
|
|
|
test_rc_zhaofenxing_cross_ref,
|
|
|
|
|
|
test_rc_user_pattern,
|
|
|
|
|
|
]
|
|
|
|
|
|
for test in tests:
|
|
|
|
|
|
try:
|
|
|
|
|
|
test()
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f" ✗ {test.__name__} FAILED: {e}")
|
|
|
|
|
|
import traceback
|
|
|
|
|
|
|
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
|
return 1
|
|
|
|
|
|
print(" ✓ 全部通过")
|
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-26 10:59:28 +08:00
|
|
|
|
def main():
|
|
|
|
|
|
import argparse
|
|
|
|
|
|
|
|
|
|
|
|
parser = argparse.ArgumentParser(description="chanlun PyO3 集成测试")
|
2026-05-28 01:07:16 +08:00
|
|
|
|
parser.add_argument("test", nargs="?", default="all", choices=["all", "subclass", "integration", "jiequ", "rc"], help="运行哪组测试 (默认: all)")
|
2026-05-26 10:59:28 +08:00
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
|
|
|
|
exit_code = 0
|
|
|
|
|
|
|
|
|
|
|
|
if args.test in ("all", "subclass"):
|
|
|
|
|
|
if run_subclass_tests() != 0:
|
|
|
|
|
|
exit_code = 1
|
|
|
|
|
|
|
|
|
|
|
|
if args.test in ("all", "integration"):
|
|
|
|
|
|
if run_integration_test() != 0:
|
|
|
|
|
|
exit_code = 1
|
|
|
|
|
|
|
2026-05-28 01:07:16 +08:00
|
|
|
|
if args.test in ("all", "jiequ"):
|
|
|
|
|
|
if run_jiequ_tests() != 0:
|
|
|
|
|
|
exit_code = 1
|
|
|
|
|
|
|
|
|
|
|
|
if args.test in ("all", "rc"):
|
|
|
|
|
|
if run_rc_identity_tests() != 0:
|
|
|
|
|
|
exit_code = 1
|
|
|
|
|
|
|
2026-05-26 10:59:28 +08:00
|
|
|
|
if exit_code == 0:
|
|
|
|
|
|
print("\n✓ 所有测试通过")
|
|
|
|
|
|
|
|
|
|
|
|
sys.exit(exit_code)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-25 12:41:14 +08:00
|
|
|
|
if __name__ == "__main__":
|
2026-05-26 10:59:28 +08:00
|
|
|
|
main()
|