Files
chanlun.rs/chanlun-py/test_integration.py
T
2026-05-28 01:07:16 +08:00

1193 lines
38 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.
#!/usr/bin/env python3
"""Integration test: feed .nb bars through PyO3 observer, compare with Python reference output."""
import sys
import os
import struct
import tempfile
import chanlun
# 项目根目录(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")
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
# ============================================================
# 观察者 子类化 / 方法重写 测试
# ============================================================
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 gettersuper() 取基类值."""
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")
# --- 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():
"""重写关键序列 gettersuper() 取基类值."""
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")
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,
# 新增: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,
]
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")
# Read all bars
print("Reading bars from .nb file...")
bars = read_nb_bars(NB_PATH)
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}")
# 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
print("\nComparing with Python reference...")
ref_files = sorted(os.listdir(_PY_REF_DIR))
match_count = 0
diff_count = 0
all_match = True
for fname in ref_files:
ref_path = os.path.join(_PY_REF_DIR, fname)
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)
rust_ref_path = os.path.join(_RUST_REF_DIR, fname)
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
# ============================================================
# 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
def main():
import argparse
parser = argparse.ArgumentParser(description="chanlun PyO3 集成测试")
parser.add_argument("test", nargs="?", default="all", choices=["all", "subclass", "integration", "jiequ", "rc"], help="运行哪组测试 (默认: all)")
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
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
if exit_code == 0:
print("\n✓ 所有测试通过")
sys.exit(exit_code)
if __name__ == "__main__":
main()