Files
chanlun.rs/chanlun-py/test_integration.py
T
2026-05-26 11:03:05 +08:00

423 lines
13 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")
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,
]
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
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
def main():
import argparse
parser = argparse.ArgumentParser(description="chanlun PyO3 集成测试")
parser.add_argument("test", nargs="?", default="all", choices=["all", "subclass", "integration"], 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 exit_code == 0:
print("\n✓ 所有测试通过")
sys.exit(exit_code)
if __name__ == "__main__":
main()