Files
chanlun.rs/chanlun-py/tests/test_all.py
T
2026-06-12 14:41:50 +08:00

2850 lines
105 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
"""chanlun PyO3 综合测试 — 所有测试类统一入口。
使用了 pyo3_test_helpers 中的泛化 Mixin:
- RcIdentityMixin: Rc/Arc 指针身份一致性
- PyO3SubclassMixin: PyO3 子类化兼容性
- ApiConsistencyMixin: chan/chanlun API 描述符类型一致性
- assert_type_shape: 返回值类型形状验证
运行方式:
python -m pytest tests/test_all.py -v
python tests/test_all.py # 全部测试
python tests/test_all.py Test观察者子类化 # 指定测试类
python tests/test_all.py Test观察者子类化.test_subclass_basic # 单个测试
"""
import unittest
import sys
import os
import struct
import tempfile
import math
from datetime import datetime
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "tests"))
import chanlun
import chanlun.chan # noqa # 用于 API 一致性测试
from helpers import ApiConsistencyMixin, RcIdentityMixin, PyO3SubclassMixin, assert_type_shape
# ---- 路径 ----
_PROJECT_ROOT = os.environ.get(
"CHANLUN_PROJECT_ROOT",
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
)
NB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "btcusd-300-1777649100-1778398800.nb")
_PY_REF_DIR = os.path.join(_PROJECT_ROOT, "Py_btcusd:300_1777649100_1778398800")
_RUST_REF_DIR = os.path.join(_PROJECT_ROOT, "chanlun", "Rust_btcusd:300_1777649100_1778398800")
# ---- 辅助函数 ----
def read_nb_bars(path, max_bars=None):
"""从 .nb 文件读取 K 线数据 (48 字节: 6 × f64 大端)."""
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 _has_nb():
"""检查 .nb 数据文件是否存在."""
return os.path.isfile(NB_PATH)
def create_observer(symbol="btcusd", period=14400, n_bars=500):
"""创建观察者并喂入模拟K线数据."""
cfg = chanlun.缠论配置()
obs = chanlun.观察者(symbol, period, cfg)
for i in range(n_bars):
trend = i * 3
wave = math.sin(i * 0.05) * 2000
mid = 68000.0 + trend + wave
high = mid + abs(math.cos(i * 0.3)) * 400 + 100
low = mid - abs(math.sin(i * 0.5)) * 400 - 100
k = chanlun.K线(
标识=symbol,
周期=period,
时间戳=1771675200 + i * period,
开盘价=mid - 50,
=high,
=low,
收盘价=mid + 50,
成交量=abs(math.sin(i)) * 1000,
)
obs.增加原始K线(k)
return obs
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
# ============================================================
# 观察者 子类化 / 方法重写 测试
# ============================================================
class Test观察者子类化(PyO3SubclassMixin, unittest.TestCase):
"""观察者 子类化/方法重写 测试 — 使用 PyO3SubclassMixin 泛化基类."""
base_class = chanlun.观察者
constructor_args = ("btcusd", 300)
constructor_kwargs = {}
sequence_getter_names = [
"普通K线序列",
"缠论K线序列",
"分型序列",
"笔序列",
"笔_中枢序列",
"线段序列",
"中枢序列",
"扩展线段序列",
"扩展中枢序列",
"扩展线段序列_线段",
"扩展中枢序列_线段",
"线段_线段序列",
"线段_中枢序列",
"扩展线段序列_扩展线段",
"扩展中枢序列_扩展线段",
]
feed_method_name = "增加原始K线"
# ---- Mixin hooks: 使用预加载数据,避免每个测试重复读取 .nb ----
@staticmethod
def make_data_item():
return chanlun.K线.创建普K("test", 1761327300, 100.0, 105.0, 99.0, 103.0, 1000.0, 0, 300)
@classmethod
def make_target_with_data(cls):
obs = chanlun.观察者("btcusd", 300)
for i, (ts, o, h, l, c, v) in enumerate(cls._bars):
k = chanlun.K线.创建普K(f"base_{i}", ts, o, h, l, c, v, i, 300)
obs.增加原始K线(k)
return obs
@classmethod
def make_sub_with_data(cls):
class Sub(chanlun.观察者):
pass
obs = Sub("btcusd", 300)
for i, (ts, o, h, l, c, v) in enumerate(cls._bars):
k = chanlun.K线.创建普K(f"sub_{i}", ts, o, h, l, c, v, i, 300)
obs.增加原始K线(k)
return obs
@classmethod
def setUpClass(cls):
"""预加载 .nb 数据和预暖观察者,所有测试共享."""
if not _has_nb():
return # setUPClass 中不能 skip,各测试自行检查
cls._bars = read_nb_bars(NB_PATH, max_bars=500)
cls._base_obs = chanlun.观察者("btcusd", 300)
for i, (ts, o, h, l, c, v) in enumerate(cls._bars):
k = chanlun.K线.创建普K(f"b_{i}", ts, o, h, l, c, v, i, 300)
cls._base_obs.增加原始K线(k)
def _require_bars(self):
if not hasattr(type(self), "_bars"):
self.skipTest("需要 .nb 数据文件")
# ---- 以下为 chanlun 专项测试 ----
def test_subclass_new_pass_config(self):
"""__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")
self.assertEqual(obs.标识, "btcusd:300")
self.assertEqual(obs.tag, "test-tag")
def test_override_method_super_call(self):
"""重写 增加原始K线,super() 调用父类,全线管线运行."""
self._require_bars()
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(self._bars):
k = chanlun.K线.创建普K(f"s_{i}", ts, o, h, l, c, v, i, 300)
sub_obs.增加原始K线(k)
self.assertEqual(sub_obs.intercept_count, len(self._bars))
self.assertEqual(len(sub_obs.intercept_timestamps), len(self._bars))
for attr in ["普通K线序列", "缠论K线序列", "分型序列", "笔序列", "线段序列", "中枢序列"]:
base_len = len(getattr(self._base_obs, attr))
sub_len = len(getattr(sub_obs, attr))
self.assertEqual(base_len, sub_len, f"{attr}: base={base_len}, sub={sub_len}")
for j, (bp, sp) in enumerate(zip(self._base_obs.笔序列, sub_obs.笔序列)):
self.assertEqual(bp...时间戳, sp...时间戳, f"笔[{j}] 时间戳不一致")
def test_override_getter(self):
"""重写 @property gettersuper() 取基类值."""
class Sub(chanlun.观察者):
@property
def 标识(self):
return f"[MOCKED] {super().标识}"
obs = Sub("btcusd", 300)
self.assertEqual(obs.标识, "[MOCKED] btcusd:300")
self.assertEqual(obs.周期, 300)
def test_override_str_repr(self):
"""重写 __str__ / __repr__."""
class Sub(chanlun.观察者):
def __str__(self):
return f"Custom({self.标识})"
def __repr__(self):
return self.__str__()
obs = Sub("btcusd", 300)
self.assertEqual(str(obs), "Custom(btcusd:300)")
self.assertEqual(repr(obs), "Custom(btcusd:300)")
def test_multi_level_inheritance(self):
"""多层继承,MRO 调用链完整."""
k = self.make_data_item()
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)
obs.增加原始K线(k)
self.assertEqual(obs.l2_log, ["L2"])
self.assertEqual(obs.l1_log, ["L1"])
self.assertEqual(len(obs.普通K线序列), 1)
def test_unoverridden_method_inherited(self):
"""未重写的方法从基类直接继承."""
k = self.make_data_item()
class Sub(chanlun.观察者):
pass
obs = Sub("btcusd", 120)
obs.增加原始K线(k)
self.assertEqual(obs.标识, "btcusd:120")
self.assertEqual(obs.周期, 120)
self.assertEqual(len(obs.普通K线序列), 1)
self.assertEqual(len(obs.缠论K线序列), 1)
obs.静态重新分析()
def test_override_reset(self):
"""重写 重置基础序列,子类状态也重置."""
k = self.make_data_item()
class Sub(chanlun.观察者):
def __init__(self, 符号, 周期):
self.my_log = []
def 重置基础序列(self):
self.my_log.clear()
super().重置基础序列()
obs = Sub("btcusd", 300)
obs.增加原始K线(k)
obs.my_log.append("test")
self.assertEqual(len(obs.普通K线序列), 1)
obs.重置基础序列()
self.assertEqual(len(obs.普通K线序列), 0)
self.assertEqual(obs.my_log, [])
# --- Property getter 逐一覆盖 ---
def test_override_getter_符号(self):
class Sub(chanlun.观察者):
@property
def 符号(self):
return f"[WRAPPED] {super().符号}"
obs = Sub("btcusd", 300)
self.assertEqual(obs.符号, "[WRAPPED] btcusd")
def test_override_getter_周期(self):
class Sub(chanlun.观察者):
@property
def 周期(self):
return super().周期 * 60
obs = Sub("btcusd", 5)
self.assertEqual(obs.周期, 300)
def test_override_getter_配置(self):
class Sub(chanlun.观察者):
@property
def 配置(self):
self._cfg_accessed = True
return super().配置
obs = Sub("btcusd", 300)
cfg = obs.配置
self.assertIsNotNone(cfg)
self.assertTrue(obs._cfg_accessed)
def test_override_getter_当前K线(self):
k = self.make_data_item()
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)
obs.增加原始K线(k)
cur = obs.当前K线
self.assertIsNotNone(cur)
self.assertTrue(obs._cur_k_accessed)
def test_override_getter_当前缠K(self):
k = self.make_data_item()
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)
obs.增加原始K线(k)
cur = obs.当前缠K
self.assertIsNotNone(cur)
self.assertTrue(obs._cur_ck_accessed)
def test_override_getter_观察员(self):
class Sub(chanlun.观察者):
@property
def 观察员(self):
self._obs_accessed = True
return self
obs = Sub("btcusd", 300)
self.assertIs(obs.观察员, obs)
self.assertTrue(obs._obs_accessed)
def test_override_sequence_getter(self):
k = self.make_data_item()
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)
obs.增加原始K线(k)
self.assertEqual(len(obs.普通K线序列), 1)
self.assertIn("普通K线序列", obs._seq_accessed)
self.assertEqual(len(obs.缠论K线序列), 1)
self.assertIn("缠论K线序列", obs._seq_accessed)
self.assertIsInstance(obs.笔序列, list)
self.assertIn("笔序列", obs._seq_accessed)
self.assertIsInstance(obs.线段序列, list)
self.assertIn("线段序列", obs._seq_accessed)
def test_override_all_sequence_getters(self):
k = self.make_data_item()
all_seqs = [
"普通K线序列",
"缠论K线序列",
"分型序列",
"笔序列",
"笔_中枢序列",
"线段序列",
"中枢序列",
"扩展线段序列",
"扩展中枢序列",
"扩展线段序列_线段",
"扩展中枢序列_线段",
"线段_线段序列",
"线段_中枢序列",
"扩展线段序列_扩展线段",
"扩展中枢序列_扩展线段",
]
def _make_getter(name):
@property
def getter(self, _name=name):
self._all_seq_accessed.add(_name)
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)
obs.增加原始K线(k)
for seq_name in all_seqs:
seq = getattr(obs, seq_name)
self.assertIn(seq_name, obs._all_seq_accessed, f"{seq_name} 未被拦截")
self.assertIsInstance(seq, list, f"{seq_name} 不是 list,而是 {type(seq)}")
# --- 方法重写(使用预加载数据) ---
def test_override_加载保存读取重分析(self):
"""串行验证 加载本地数据 / 保存数据 / 读取数据文件 / 静态重新分析 四个重写."""
self._require_bars()
class Sub(chanlun.观察者):
def __init__(self, 符号, 周期, 配置=None):
self._loaded = False
self._load_count = 0
self._reanalyzed = 0
self._saved = False
self._save_root = None
self._reload = 0
super().__init__(符号, 周期, 配置)
def 测试_保存数据(self, root=None):
self._saved = True
self._save_root = root
super().测试_保存数据(root)
def 静态重新分析(self):
self._reanalyzed += 1
super().静态重新分析()
def 加载本地数据(self, 文件路径):
self._loaded = True
self._load_count += 1
super().加载本地数据(文件路径)
def 重置基础序列(self):
self._reload += 1
super().重置基础序列()
@classmethod
def 读取数据文件(cls, 文件路径, 配置=None):
obs = cls("", 0) # 创建子类实例,父类方法会覆盖符号/周期
chanlun.观察者.读取数据文件(文件路径, 配置, 观察员=obs)
obs._custom_classmethod_flag = True
return obs
# 4. 读取数据文件(classmethod 重写
obs = Sub.读取数据文件(NB_PATH)
self.assertIsInstance(obs, Sub)
self.assertTrue(obs._custom_classmethod_flag)
self.assertGreater(len(obs.普通K线序列), 0)
# 1. 加载本地数据
obs.重置基础序列()
obs.加载本地数据(NB_PATH)
self.assertTrue(obs._loaded)
self.assertEqual(obs._load_count, 2)
self.assertGreater(len(obs.普通K线序列), 0)
# 2. 静态重新分析(复用已加载数据的 obs)
obs.重置基础序列()
obs.加载本地数据(NB_PATH)
obs.静态重新分析()
self.assertEqual(obs._reanalyzed, 1)
self.assertGreaterEqual(len(obs.笔序列), 0)
# 3. 保存数据
obs.重置基础序列()
obs.增加原始K线(self.make_data_item())
with tempfile.TemporaryDirectory() as tmpdir:
obs.测试_保存数据(tmpdir)
self.assertTrue(obs._saved)
self.assertEqual(obs._save_root, tmpdir)
# 5. 重置次数
self.assertEqual(obs._reload, 6)
def test_override_completely_no_super(self):
k = self.make_data_item()
class Sub(chanlun.观察者):
def 增加原始K线(self, 普K):
self._custom = getattr(self, "_custom", [])
self._custom.append(普K.时间戳)
obs = Sub("btcusd", 300)
obs.增加原始K线(k)
self.assertEqual(obs._custom, [1761327300])
self.assertEqual(len(obs.普通K线序列), 0)
def test_override_identical_subclass(self):
self._require_bars()
class Sub(chanlun.观察者):
pass
sub = Sub("btcusd", 300)
for i, (ts, o, h, l, c, v) in enumerate(self._bars):
sk = chanlun.K线.创建普K(f"s_{i}", ts, o, h, l, c, v, i, 300)
sub.增加原始K线(sk)
for attr in ["普通K线序列", "笔序列", "线段序列", "中枢序列"]:
base_len = len(getattr(self._base_obs, attr))
sub_len = len(getattr(sub, attr))
self.assertEqual(base_len, sub_len, f"{attr}: base={base_len}, sub={sub_len}")
def test_override_three_level_mixed(self):
k = self.make_data_item()
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)
obs.增加原始K线(k)
self.assertEqual(obs._l1_feed, 1)
self.assertEqual(obs.标识, "[L2] btcusd:300")
self.assertEqual(obs._l2_标识, 1)
obs.重置基础序列()
self.assertEqual(obs._l3_reset, 1)
self.assertEqual(len(obs.普通K线序列), 0)
def test_override_cross_method_dispatch(self):
k = self.make_data_item()
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)
obs.compound_operation(k)
self.assertTrue(obs._feed_called)
self.assertTrue(obs._reset_called)
self.assertEqual(len(obs.普通K线序列), 0)
# ============================================================
# K线.截取 测试
# ============================================================
class TestK线截取(unittest.TestCase):
"""K线.截取 测试."""
def test_jiequ_basic(self):
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]
r = chanlun.K线.截取(ks, k2, k4)
self.assertEqual(len(r), 3)
self.assertEqual(r[0].时间戳, k2.时间戳)
self.assertEqual(r[-1].时间戳, k4.时间戳)
r = chanlun.K线.截取(ks, k1, k4)
self.assertEqual(len(r), 4)
r = chanlun.K线.截取(ks, k3, k3)
self.assertEqual(len(r), 1)
self.assertEqual(r[0].时间戳, k3.时间戳)
def test_jiequ_error(self):
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]
with self.assertRaises(ValueError):
chanlun.K线.截取([], k1, k1)
kx = chanlun.K线.创建普K("kx", 9999999999, 1.0, 1.0, 1.0, 1.0, 1.0, 99, 300)
with self.assertRaises(ValueError):
chanlun.K线.截取(ks, kx, k3)
with self.assertRaises(ValueError):
chanlun.K线.截取(ks, k3, k1)
@unittest.skipUnless(_has_nb(), "需要 .nb 数据文件")
def test_jiequ_from_observer(self):
cfg = chanlun.缠论配置()
obs = chanlun.观察者.读取数据文件(NB_PATH, cfg)
self.assertGreater(len(obs.笔序列), 0, "笔序列为空,数据可能不够")
bi = obs.笔序列[0]
pk_seq = bi.获取普K序列(obs)
self.assertGreater(len(pk_seq), 0)
r = chanlun.K线.截取(obs.普通K线序列, bi...标的K线, bi...标的K线)
self.assertGreater(len(r), 0)
self.assertEqual(r[0].时间戳, bi...标的K线.时间戳)
self.assertEqual(r[-1].时间戳, bi...标的K线.时间戳)
mid = len(pk_seq) // 2
if mid > 0:
r = chanlun.K线.截取(pk_seq, pk_seq[0], pk_seq[mid])
self.assertEqual(len(r), mid + 1)
self.assertEqual(r[0].时间戳, pk_seq[0].时间戳)
self.assertEqual(r[-1].时间戳, pk_seq[mid].时间戳)
@unittest.skipUnless(_has_nb(), "需要 .nb 数据文件")
def test_jiequ_multi_bi(self):
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])
self.assertEqual(len(r), len(pk_seq), f"笔[{i}] 截取长度不一致: {len(r)} vs {len(pk_seq)}")
n_checked += 1
self.assertGreater(n_checked, 0)
# ============================================================
# Rc 指针身份 / list.index 测试
# ============================================================
class TestRc身份列表索引(unittest.TestCase):
"""Rc 指针身份 / list.index 测试."""
def setUp(self):
if not _has_nb():
self.skipTest("需要 .nb 数据文件")
self.obs = _load_observer()
def test_same_seq_index(self):
obs = self.obs
fx_list = obs.分型序列
if len(fx_list) >= 2:
self.assertEqual(fx_list.index(fx_list[0]), 0)
self.assertEqual(fx_list.index(fx_list[1]), 1)
ck_list = obs.缠论K线序列
if len(ck_list) >= 2:
self.assertEqual(ck_list.index(ck_list[0]), 0)
self.assertEqual(ck_list.index(ck_list[-1]), len(ck_list) - 1)
bi_list = obs.笔序列
if len(bi_list) >= 2:
self.assertEqual(bi_list.index(bi_list[0]), 0)
self.assertEqual(bi_list.index(bi_list[-1]), len(bi_list) - 1)
if len(obs.线段序列) > 0:
for in obs.线段序列:
笔列表 = .笔序列
if len(笔列表) >= 2:
self.assertEqual(笔列表.index(笔列表[0]), 0)
self.assertEqual(笔列表.index(笔列表[-1]), len(笔列表) - 1)
break
zs_list = obs.中枢序列
if len(zs_list) >= 2:
self.assertEqual(zs_list.index(zs_list[0]), 0)
self.assertEqual(zs_list.index(zs_list[-1]), len(zs_list) - 1)
def test_cross_seq_index(self):
obs = self.obs
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)
self.assertGreaterEqual(idx, 0)
self.assertEqual(笔列表[idx], elem)
n_checked += 1
break
self.assertGreaterEqual(n_checked, 3, f"应至少检查3个中枢元素,实际 {n_checked}")
def test_zhaofenxing_cross_ref(self):
obs = self.obs
fx_list = obs.分型序列
if len(fx_list) >= 2 and len(obs.笔序列) > 0:
bi = obs.笔序列[-1]
try:
idx = fx_list.index(bi.)
self.assertGreaterEqual(idx, 0)
except ValueError:
pass
def test_user_pattern(self):
obs = self.obs
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]
笔列表 = .笔序列
idx = 笔列表.index(zs.基础序列[0])
found = False
for bi in reversed(笔列表[:idx]):
if bi.方向 == zs.基础序列[0].方向:
found = True
break
self.assertGreater(n_segments_with_zs, 0, "应至少有一个段包含中枢")
def test_tongji_macd_behavior_types(self):
obs = _load_observer(max_bars=5000)
if len(obs.笔序列) == 0:
self.skipTest("无笔")
= obs.笔序列[-1]
普K序列 = .获取普K序列(obs)
if len(普K序列) < 2:
self.skipTest("K线不够")
result = chanlun.虚线.统计MACD行为(普K序列, 8, 3)
# 使用泛化工具验证类型形状
assert_type_shape(
result,
{
"DIF上穿0": int,
"DIF下穿0": int,
"DEA上穿0": int,
"DEA下穿0": int,
"金叉次数": int,
"死叉次数": int,
"密集交叉区域": [(int, int, int)],
},
)
def test_xiangduifangxiang_methods(self):
fx = chanlun.相对方向.分析(2.0, 0.5, 1.0, 0.8)
assert_type_shape(fx.是否向上, callable)
assert_type_shape(fx.是否向下, callable)
assert_type_shape(fx.是否包含, callable)
assert_type_shape(fx.是否缺口, callable)
assert_type_shape(fx.是否衔接, callable)
self.assertIsInstance(fx.是否向上(), bool)
self.assertIsInstance(fx.是否向下(), bool)
# ============================================================
# Rc 身份测试 — 使用 RcIdentityMixin
# ============================================================
class _Base身份:
"""身份测试基类:通过 setUpClass 预缓存 target 供 Mixin 使用."""
@classmethod
def setUpClass(cls):
cls._cached_target = cls.target_factory()
class Test缠K身份(_Base身份, RcIdentityMixin, unittest.TestCase):
"""缠论K线: 从序列、分型、笔端点、中枢等不同路径访问."""
@staticmethod
def target_factory():
return create_observer()
sequence_getters = {
"缠论K线序列": lambda t: t.缠论K线序列,
}
stable_getters = {
"分型[0].中": lambda t: t.分型序列[0]. if t.分型序列 else None,
}
# 专项测试保留
def test_分型中K(self):
t = self._get_target()
seq = t.缠论K线序列
分序 = t.分型序列
for fx in 分序[:10]:
= fx.
for ck in seq:
if ck.时间戳 == .时间戳:
self.assertIs(ck, , f"分型.中 (ts={.时间戳}) 与序列中元素不匹配")
break
def test_笔端点钟K(self):
t = self._get_target()
seq = t.缠论K线序列
for bi in t.笔序列:
for nm, getter in [("文", lambda b=bi: b.), ("武", lambda b=bi: b.)]:
ep = getter()
if ep is None:
continue
= ep.
for ck in seq:
if ck.时间戳 == .时间戳:
self.assertIs(ck, , f"笔.{nm}.中 (ts={.时间戳}) 与序列中元素不匹配")
break
class Test分型身份(_Base身份, RcIdentityMixin, unittest.TestCase):
"""分型: 从分型序列、笔/线段端点等不同路径访问."""
@staticmethod
def target_factory():
return create_observer()
sequence_getters = {
"分型序列": lambda t: t.分型序列,
}
stable_getters = {
"笔[0].文": lambda t: t.笔序列[0]. if t.笔序列 else None,
"笔[0].武": lambda t: t.笔序列[0]. if t.笔序列 else None,
}
def test_笔端点与序列(self):
t = self._get_target()
分序 = t.分型序列
for bi in t.笔序列:
for nm in ["文", "武"]:
ep = getattr(bi, nm)
if ep is None:
continue
matched = False
for fx in 分序:
if fx.时间戳 == ep.时间戳 and fx.结构 == ep.结构:
self.assertIs(fx, ep, f"笔.{nm} (ts={ep.时间戳}) 与分型序列中元素不匹配")
matched = True
break
self.assertTrue(matched, f"笔.{nm} (ts={ep.时间戳}) 在分型序列中未找到")
def test_段端点与序列(self):
t = self._get_target()
分序 = t.分型序列
for duan in t.线段序列:
for nm in ["文", "武"]:
ep = getattr(duan, nm)
if ep is None:
continue
matched = False
for fx in 分序:
if fx.时间戳 == ep.时间戳 and fx.结构 == ep.结构:
self.assertIs(fx, ep, f"段.{nm} (ts={ep.时间戳}) 与分型序列中元素不匹配")
matched = True
break
self.assertTrue(matched, f"段.{nm} (ts={ep.时间戳}) 在分型序列中未找到")
class Test虚线身份(_Base身份, RcIdentityMixin, unittest.TestCase):
"""虚线(笔/线段): 从笔序列、线段序列、中枢内部序列等不同路径访问."""
@staticmethod
def target_factory():
return create_observer()
sequence_getters = {
"笔序列": lambda t: t.笔序列,
"线段序列": lambda t: t.线段序列,
}
def test_多个扩展序列(self):
t = self._get_target()
s1 = t.扩展线段序列
s2 = t.扩展线段序列_线段
for d1 in s1:
for d2 in s2:
if d1.序号 == d2.序号:
self.assertIs(d1, d2, f"扩展线段序列[{d1.序号}] 跨序列身份不一致")
break
class TestK线身份(_Base身份, RcIdentityMixin, unittest.TestCase):
"""原始K线: 从序列、买卖点、缠K标的等不同路径访问."""
@staticmethod
def target_factory():
return create_observer()
sequence_getters = {
"普通K线序列": lambda t: t.普通K线序列,
}
class Test中枢身份(_Base身份, RcIdentityMixin, unittest.TestCase):
"""中枢: 从中枢序列、分型关联、笔中枢/线段中枢等不同路径访问."""
@staticmethod
def target_factory():
return create_observer(period=3600, n_bars=800)
sequence_getters = {
"中枢序列": lambda t: t.中枢序列,
}
stable_getters = {
"笔中枢[0].文": lambda t: t.笔_中枢序列[0]. if t.笔_中枢序列 else None,
"段中枢[0].文": lambda t: t.线段_中枢序列[0]. if t.线段_中枢序列 else None,
}
class Test整体身份(_Base身份, unittest.TestCase):
"""跨类型综合身份测试."""
@classmethod
def setUpClass(cls):
cls.obs = create_observer(period=3600, n_bars=800)
def test_买卖点分型(self):
分序 = self.obs.分型序列
笔序 = self.obs.笔序列
self.assertGreaterEqual(len(分序), 0)
self.assertGreaterEqual(len(笔序), 0)
def test_全链路一致性(self):
obs = create_observer()
seq = obs.缠论K线序列
for bi in obs.笔序列:
for nm, getter in [("文", lambda b=bi: b.), ("武", lambda b=bi: b.)]:
ep = getter()
if ep is None:
continue
= ep.
for ck in seq:
if ck.时间戳 == .时间戳:
self.assertIs(ck, )
break
= ep.
if is not None:
for ck in seq:
if ck.时间戳 == .时间戳:
self.assertIs(ck, )
break
= ep.
if is not None:
for ck in seq:
if ck.时间戳 == .时间戳:
self.assertIs(ck, )
break
# ============================================================
# 跨线程身份测试 — 验证全局缓存(非 thread_local)的跨线程一致性
# ============================================================
class Test跨线程身份(unittest.TestCase):
"""跨线程 RC 身份一致性:全局缓存应在不同线程间共享同一 Python 对象."""
@classmethod
def setUpClass(cls):
cls.obs = create_observer(period=3600, n_bars=800)
def _run_in_thread(self, fn):
"""在子线程中执行 fn,通过 queue 收集结果和异常."""
import threading
result = []
err = []
def wrapper():
try:
result.append(fn())
except Exception as e:
err.append(e)
t = threading.Thread(target=wrapper)
t.start()
t.join()
if err:
raise err[0]
return result[0]
# ---- 序列级别 ----
def test_缠K序列跨线程重复获取_is一致(self):
"""缠论K线序列:从子线程重复获取,元素 is 一致."""
obs = self.obs
def check():
s1 = obs.缠论K线序列
s2 = obs.缠论K线序列
return [(s1[i] is s2[i], len(s1), len(s2)) for i in range(min(len(s1), len(s2), 20))]
results = self._run_in_thread(check)
for i, (ok, l1, l2) in enumerate(results):
self.assertTrue(ok, f"缠K序列[{i}] 跨线程 is 不一致")
def test_分型序列跨线程重复获取_is一致(self):
"""分型序列:从子线程重复获取,元素 is 一致."""
obs = self.obs
def check():
s1 = obs.分型序列
s2 = obs.分型序列
return [(s1[i] is s2[i], len(s1)) for i in range(min(len(s1), len(s2), 20))]
results = self._run_in_thread(check)
for i, (ok, _) in enumerate(results):
self.assertTrue(ok, f"分型序列[{i}] 跨线程 is 不一致")
def test_笔序列跨线程重复获取_is一致(self):
"""笔序列:从子线程重复获取,元素 is 一致."""
obs = self.obs
def check():
s1 = obs.笔序列
s2 = obs.笔序列
return [(s1[i] is s2[i], len(s1)) for i in range(min(len(s1), len(s2), 20))]
results = self._run_in_thread(check)
for i, (ok, _) in enumerate(results):
self.assertTrue(ok, f"笔序列[{i}] 跨线程 is 不一致")
def test_线段序列跨线程重复获取_is一致(self):
"""线段序列:从子线程重复获取,元素 is 一致."""
obs = self.obs
def check():
s1 = obs.线段序列
s2 = obs.线段序列
return [(s1[i] is s2[i], len(s1)) for i in range(min(len(s1), len(s2), 20))]
results = self._run_in_thread(check)
for i, (ok, _) in enumerate(results):
self.assertTrue(ok, f"线段序列[{i}] 跨线程 is 不一致")
def test_中枢序列跨线程重复获取_is一致(self):
"""中枢序列:从子线程重复获取,元素 is 一致."""
obs = self.obs
def check():
s1 = obs.中枢序列
s2 = obs.中枢序列
return [(s1[i] is s2[i], len(s1)) for i in range(min(len(s1), len(s2), 20))]
results = self._run_in_thread(check)
for i, (ok, _) in enumerate(results):
self.assertTrue(ok, f"中枢序列[{i}] 跨线程 is 不一致")
def test_普K序列跨线程重复获取_is一致(self):
"""普通K线序列:从子线程重复获取,元素 is 一致."""
obs = self.obs
def check():
s1 = obs.普通K线序列
s2 = obs.普通K线序列
return [(s1[i] is s2[i], len(s1)) for i in range(min(len(s1), len(s2), 20))]
results = self._run_in_thread(check)
for i, (ok, _) in enumerate(results):
self.assertTrue(ok, f"普K序列[{i}] 跨线程 is 不一致")
# ---- 跨路径 ----
def test_跨线程分型中K线_is一致(self):
"""子线程中 分型.中 is 缠论K线序列[同时间戳]."""
obs = self.obs
def check():
results = []
seq = obs.缠论K线序列
for fx in obs.分型序列[:10]:
= fx.
found = False
for ck in seq:
if ck.时间戳 == .时间戳:
results.append((ck is , ck.时间戳))
found = True
break
if not found:
results.append((None, .时间戳))
return results
results = self._run_in_thread(check)
for ok, ts in results:
self.assertIsNotNone(ok, f"分型.中 ts={ts} 在缠K序列中未找到")
self.assertTrue(ok, f"跨线程 分型.中 ts={ts} is 不一致")
def test_跨线程笔端点钟K_is一致(self):
"""子线程中 笔.文中 is 缠论K线序列[同时间戳]."""
obs = self.obs
def check():
results = []
seq = obs.缠论K线序列
for bi in obs.笔序列[:10]:
for nm, ep in [("文", bi.), ("武", bi.)]:
if ep is None:
continue
= ep.
for ck in seq:
if ck.时间戳 == .时间戳:
results.append((ck is , nm, ck.时间戳))
break
return results
results = self._run_in_thread(check)
for ok, nm, ts in results:
self.assertTrue(ok, f"跨线程 笔.{nm}.中 ts={ts} is 不一致")
def test_跨线程中枢元件_is一致(self):
"""子线程中 中枢.元件 中的虚线对象 is 线段序列[同索引]."""
obs = self.obs
def check():
results = []
for zs in obs.中枢序列[:5]:
for elem in zs.元件[:3]:
results.append(elem is elem) # 自我 is
results.append(elem is not None)
return results
results = self._run_in_thread(check)
for ok in results:
self.assertTrue(ok)
# ---- list.index 基于 is ----
def test_跨线程list_index基于身份(self):
"""子线程中 list.index(elem) 正常工作(依赖 __eq__ 基于 is."""
obs = self.obs
def check():
results = []
for name, getter in [
("缠论K线序列", lambda o: o.缠论K线序列),
("分型序列", lambda o: o.分型序列),
("笔序列", lambda o: o.笔序列),
("线段序列", lambda o: o.线段序列),
("中枢序列", lambda o: o.中枢序列),
]:
seq = getter(obs)
if len(seq) >= 2:
r0 = seq.index(seq[0]) == 0
r1 = seq.index(seq[-1]) == len(seq) - 1
results.append((name, r0, r1, len(seq)))
return results
results = self._run_in_thread(check)
for name, r0, r1, length in results:
self.assertTrue(r0, f"跨线程 {name}(len={length}): index(seq[0]) != 0")
self.assertTrue(r1, f"跨线程 {name}(len={length}): index(seq[-1]) != {length - 1}")
# ---- 主线程-子线程之间 ----
def test_主线程与子线程对象_is一致(self):
"""主线程获取的对象与子线程获取的对象 is 相同."""
import threading
obs = self.obs
# 主线程先获取
seq_main = obs.缠论K线序列
fx_main = obs.分型序列
bi_main = obs.笔序列
err = []
def check():
try:
seq_thread = obs.缠论K线序列
for i in range(min(len(seq_main), 10)):
if seq_main[i] is not seq_thread[i]:
raise AssertionError(f"缠K[{i}] 主线程与子线程 is 不一致")
fx_thread = obs.分型序列
for i in range(min(len(fx_main), 10)):
if fx_main[i] is not fx_thread[i]:
raise AssertionError(f"分型[{i}] 主线程与子线程 is 不一致")
bi_thread = obs.笔序列
for i in range(min(len(bi_main), 10)):
if bi_main[i] is not bi_thread[i]:
raise AssertionError(f"笔[{i}] 主线程与子线程 is 不一致")
except Exception as e:
err.append(e)
t = threading.Thread(target=check)
t.start()
t.join()
if err:
raise err[0]
def test_跨线程getter稳定性(self):
"""子线程中同一 getter 多次调用返回同一对象(如 分型.结构 等)."""
obs = self.obs
def check():
results = []
if obs.分型序列:
fx = obs.分型序列[0]
for name, getter in [
("分型.结构", lambda f: f.结构),
("分型.左", lambda f: f.),
("分型.中", lambda f: f.),
("分型.右", lambda f: f.),
]:
v1 = getter(fx)
v2 = getter(fx)
results.append((name, v1 is v2 if v1 is not None else True))
return results
results = self._run_in_thread(check)
for name, ok in results:
self.assertTrue(ok, f"跨线程 {name}: 两次调用 is 不一致")
def test_多线程并发访问_is一致(self):
"""两个子线程同时访问,各自拿到的对象与主线程 is 一致."""
import threading
obs = self.obs
errors = []
seq_main = obs.笔序列
def worker(thread_id):
try:
seq = obs.笔序列
for i in range(min(len(seq), 10)):
if seq[i] is not seq_main[i]:
errors.append(f"线程{thread_id} 笔[{i}] is 不一致")
if seq[i] is not seq[i]:
errors.append(f"线程{thread_id} 笔[{i}] 自我 is 失败")
except Exception as e:
errors.append(f"线程{thread_id}: {e}")
t1 = threading.Thread(target=worker, args=(1,))
t2 = threading.Thread(target=worker, args=(2,))
t1.start()
t2.start()
t1.join()
t2.join()
self.assertEqual(len(errors), 0, "\n".join(errors))
# ============================================================
# 买卖意义 双端一致性测试
# ============================================================
class Test买卖意义双端对比(unittest.TestCase):
"""运行时对比 Rust 绑定层 与 chan.py 的 虚线.买卖意义() 结果."""
@classmethod
def setUpClass(cls):
if not _has_nb():
raise unittest.SkipTest("需要 .nb 数据文件")
cls.bars = read_nb_bars(NB_PATH)
def _build_observers(self, n_bars=2000):
"""构建双端观察者并喂入相同数据."""
import chanlun
from chanlun import chan
cfg_rs = chanlun.缠论配置()
obs_rs = chanlun.观察者("btcusd", 300, cfg_rs)
cfg_py = chan.缠论配置()
obs_py = chan.观察者("btcusd", 300, cfg_py)
for i, (ts, o, h, l, c, v) in enumerate(self.bars[:n_bars]):
k_rs = chanlun.K线.创建普K(f"k{i}", ts, o, h, l, c, v, i, 300)
k_py = chan.K线.创建普K(f"k{i}", ts, o, h, l, c, v, i, 300)
obs_rs.增加原始K线(k_rs)
obs_py.增加原始K线(k_py)
return obs_rs, obs_py
def test_笔买卖意义双端一致(self):
"""笔序列的 买卖意义() 双端结果完全一致."""
import chanlun
from chanlun import chan
obs_rs, obs_py = self._build_observers()
n = min(len(obs_rs.笔序列), len(obs_py.笔序列))
self.assertGreater(n, 0, "笔序列为空")
mismatches = []
for i in range(n):
r = chanlun.虚线.买卖意义(obs_rs.笔序列[i], obs_rs)
p = chan.虚线.买卖意义(obs_py.笔序列[i], obs_py)
if r != p:
mismatches.append((i, r, p, obs_rs.笔序列[i].获取数据文本(), obs_py.笔序列[i].获取数据文本()))
self.assertEqual(len(mismatches), 0, f"笔买卖意义 不一致 ({len(mismatches)}/{n}):\n" + "\n".join(f" [{i}] R={r} P={p}\n R文本={rt}\n P文本={pt}" for i, r, p, rt, pt in mismatches[:3]))
def test_线段买卖意义双端一致(self):
"""线段序列的 买卖意义() 双端结果完全一致."""
import chanlun
from chanlun import chan
obs_rs, obs_py = self._build_observers()
n = min(len(obs_rs.线段序列), len(obs_py.线段序列))
self.assertGreater(n, 0, "线段序列为空")
mismatches = []
for i in range(n):
r = chanlun.虚线.买卖意义(obs_rs.线段序列[i], obs_rs)
p = chan.虚线.买卖意义(obs_py.线段序列[i], obs_py)
if r != p:
mismatches.append((i, r, p, obs_rs.线段序列[i].获取数据文本(), obs_py.线段序列[i].获取数据文本()))
self.assertEqual(len(mismatches), 0, f"线段买卖意义 不一致 ({len(mismatches)}/{n}):\n" + "\n".join(f" [{i}] R={r} P={p}\n R文本={rt}\n P文本={pt}" for i, r, p, rt, pt in mismatches[:3]))
def test_笔序列长度一致(self):
"""双端笔序列数量一致."""
obs_rs, obs_py = self._build_observers()
self.assertGreater(len(obs_rs.笔序列), 0)
self.assertEqual(len(obs_rs.笔序列), len(obs_py.笔序列), f"笔数量: Rust={len(obs_rs.笔序列)} Py={len(obs_py.笔序列)}")
def test_线段序列长度一致(self):
"""双端线段序列数量一致."""
obs_rs, obs_py = self._build_observers()
self.assertGreater(len(obs_rs.线段序列), 0)
self.assertEqual(len(obs_rs.线段序列), len(obs_py.线段序列), f"线段数量: Rust={len(obs_rs.线段序列)} Py={len(obs_py.线段序列)}")
def test_中枢序列长度一致(self):
"""双端中枢序列数量一致."""
obs_rs, obs_py = self._build_observers()
self.assertGreater(len(obs_rs.中枢序列), 0)
self.assertEqual(len(obs_rs.中枢序列), len(obs_py.中枢序列), f"中枢数量: Rust={len(obs_rs.中枢序列)} Py={len(obs_py.中枢序列)}")
# ---- MACD趋向背驰 ----
def test_笔MACD趋向背驰双端一致(self):
"""笔的 K线序列 MACD趋向背驰 双端结果完全一致."""
import chanlun
from chanlun import chan
obs_rs, obs_py = self._build_observers()
mismatches = []
for bi_rs, bi_py in zip(obs_rs.笔序列, obs_py.笔序列):
k_seq_rs = chanlun.K线.截取(
obs_rs.普通K线序列,
bi_rs...标的K线,
bi_rs...标的K线,
)
k_seq_py = chan.K线.截取(
obs_py.普通K线序列,
bi_py...标的K线,
bi_py...标的K线,
)
r = chanlun.虚线.计算K线序列MACD趋向背驰(k_seq_rs, bi_rs.方向)
p = chan.虚线.计算K线序列MACD趋向背驰(k_seq_py, bi_py.方向)
if r != list(p):
mismatches.append((bi_rs..时间戳, bi_rs..时间戳, list(r), list(p)))
self.assertEqual(len(mismatches), 0, f"笔MACD趋向背驰 不一致 ({len(mismatches)}):\n" + "\n".join(f" [{ts_w},{ts_wu}] R={r} P={p}" for ts_w, ts_wu, r, p in mismatches[:5]))
def test_线段MACD趋向背驰双端一致(self):
"""线段的 K线序列 MACD趋向背驰 双端结果完全一致."""
import chanlun
from chanlun import chan
obs_rs, obs_py = self._build_observers()
mismatches = []
for seg_rs, seg_py in zip(obs_rs.线段序列, obs_py.线段序列):
k_seq_rs = chanlun.K线.截取(
obs_rs.普通K线序列,
seg_rs...标的K线,
seg_rs...标的K线,
)
k_seq_py = chan.K线.截取(
obs_py.普通K线序列,
seg_py...标的K线,
seg_py...标的K线,
)
r = chanlun.虚线.计算K线序列MACD趋向背驰(k_seq_rs, seg_rs.方向)
p = chan.虚线.计算K线序列MACD趋向背驰(k_seq_py, seg_py.方向)
if r != list(p):
mismatches.append((seg_rs..时间戳, seg_rs..时间戳, list(r), list(p)))
self.assertEqual(len(mismatches), 0, f"线段MACD趋向背驰 不一致 ({len(mismatches)}):\n" + "\n".join(f" [{ts_w},{ts_wu}] R={r} P={p}" for ts_w, ts_wu, r, p in mismatches[:5]))
# ---- 统计MACD行为 ----
def test_笔统计MACD行为双端一致(self):
"""笔的 统计MACD行为() 双端结果完全一致."""
import chanlun
from chanlun import chan
obs_rs, obs_py = self._build_observers()
mismatches = []
for bi_rs, bi_py in zip(obs_rs.笔序列, obs_py.笔序列):
k_seq_rs = chanlun.K线.截取(
obs_rs.普通K线序列,
bi_rs...标的K线,
bi_rs...标的K线,
)
k_seq_py = chan.K线.截取(
obs_py.普通K线序列,
bi_py...标的K线,
bi_py...标的K线,
)
r = chanlun.虚线.统计MACD行为(k_seq_rs)
p = chan.虚线.统计MACD行为(k_seq_py)
if r != p:
mismatches.append((bi_rs..时间戳, bi_rs..时间戳, r, p))
self.assertEqual(len(mismatches), 0, f"笔统计MACD行为 不一致 ({len(mismatches)}):\n" + "\n".join(f" [{ts_w},{ts_wu}] R={r} P={p}" for ts_w, ts_wu, r, p in mismatches[:5]))
def test_线段统计MACD行为双端一致(self):
"""线段的 统计MACD行为() 双端结果完全一致."""
import chanlun
from chanlun import chan
obs_rs, obs_py = self._build_observers()
mismatches = []
for seg_rs, seg_py in zip(obs_rs.线段序列, obs_py.线段序列):
k_seq_rs = chanlun.K线.截取(
obs_rs.普通K线序列,
seg_rs...标的K线,
seg_rs...标的K线,
)
k_seq_py = chan.K线.截取(
obs_py.普通K线序列,
seg_py...标的K线,
seg_py...标的K线,
)
r = chanlun.虚线.统计MACD行为(k_seq_rs)
p = chan.虚线.统计MACD行为(k_seq_py)
if r != p:
mismatches.append((seg_rs..时间戳, seg_rs..时间戳, r, p))
self.assertEqual(len(mismatches), 0, f"线段统计MACD行为 不一致 ({len(mismatches)}):\n" + "\n".join(f" [{ts_w},{ts_wu}] R={r} P={p}" for ts_w, ts_wu, r, p in mismatches[:5]))
# ---- 获取所有停顿位置 ----
def test_笔获取所有停顿位置双端一致(self):
"""笔的 获取所有停顿位置() 双端结果一致(通过虚线相等 逐项比对)."""
import chanlun
from chanlun import chan
obs_rs, obs_py = self._build_observers()
mismatches = []
for bi_rs, bi_py in zip(obs_rs.笔序列, obs_py.笔序列):
r = chanlun..获取所有停顿位置(bi_rs, obs_rs)
p = chan..获取所有停顿位置(bi_py, obs_py)
if len(r) != len(p):
mismatches.append((f"len R={len(r)} P={len(p)}", [x.获取数据文本() for x in r], [x.获取数据文本() for x in p]))
else:
for a, b in zip(r, p):
eq, msg = chanlun.虚线相等(a, b)
if not eq:
mismatches.append((msg, [x.获取数据文本() for x in r], [x.获取数据文本() for x in p]))
break
self.assertEqual(len(mismatches), 0, f"笔获取所有停顿位置 不一致 ({len(mismatches)}):\n" + "\n".join(f" {tag}\n R={rl}\n P={pl}" for tag, rl, pl in mismatches[:3]))
def test_线段获取所有停顿位置双端一致(self):
"""线段的 获取所有停顿位置() 双端结果一致(通过虚线相等 逐项比对)."""
import chanlun
from chanlun import chan
obs_rs, obs_py = self._build_observers()
mismatches = []
for seg_rs, seg_py in zip(obs_rs.线段序列, obs_py.线段序列):
r = chanlun.线段.获取所有停顿位置(seg_rs, obs_rs)
p = chan.线段.获取所有停顿位置(seg_py, obs_py)
if len(r) != len(p):
mismatches.append((f"len R={len(r)} P={len(p)}", [x.获取数据文本() for x in r], [x.获取数据文本() for x in p]))
else:
for a, b in zip(r, p):
eq, msg = chanlun.虚线相等(a, b)
if not eq:
mismatches.append((msg, [x.获取数据文本() for x in r], [x.获取数据文本() for x in p]))
break
self.assertEqual(len(mismatches), 0, f"线段获取所有停顿位置 不一致 ({len(mismatches)}):\n" + "\n".join(f" {tag}\n R={rl}\n P={pl}" for tag, rl, pl in mismatches[:3]))
# ---- 判断线段内部是否背驰 ----
def test_判断线段内部是否背驰双端一致(self):
"""线段的 判断线段内部是否背驰() 双端结果完全一致."""
import chanlun
from chanlun import chan
obs_rs, obs_py = self._build_observers()
n = min(len(obs_rs.线段序列), len(obs_py.线段序列))
self.assertGreater(n, 0, "线段序列为空")
mismatches = []
for i in range(n):
r = chanlun.线段.判断线段内部是否背驰(obs_rs.线段序列[i], obs_rs)
p = chan.线段.判断线段内部是否背驰(obs_py.线段序列[i], obs_py)
if r != p:
mismatches.append((i, r, p, obs_rs.线段序列[i].获取数据文本(), obs_py.线段序列[i].获取数据文本()))
self.assertEqual(len(mismatches), 0, f"判断线段内部是否背驰 不一致 ({len(mismatches)}/{n}):\n" + "\n".join(f" [{i}] R={r} P={p}\n R文本={rt}\n P文本={pt}" for i, r, p, rt, pt in mismatches[:3]))
# ---- 是否背驰过 ----
# ---- 获取内部中枢序列 ----
def test_线段获取内部中枢序列双端一致(self):
"""线段的 获取内部中枢序列() 双端结果完全一致(通过 中枢相等 逐项比对)."""
import chanlun
from chanlun import chan
obs_rs, obs_py = self._build_observers()
n = min(len(obs_rs.线段序列), len(obs_py.线段序列))
self.assertGreater(n, 0, "线段序列为空")
mismatches = []
for i in range(n):
seg_rs = obs_rs.线段序列[i]
seg_py = obs_py.线段序列[i]
r = chanlun.线段.获取内部中枢序列(seg_rs, obs_rs.配置)
p = chan.线段.获取内部中枢序列(seg_py, obs_py.配置)
if len(r) != len(p):
mismatches.append((i, f"tuple len R={len(r)} P={len(p)}"))
else:
for k, (hr, hp) in enumerate(zip(r, p)):
if len(hr) != len(hp):
mismatches.append((i, f"{['实', '虚', '合'][k]} len R={len(hr)} P={len(hp)}"))
break
for j, (ha, hb) in enumerate(zip(hr, hp)):
eq, msg = chanlun.中枢相等(ha, hb)
if not eq:
mismatches.append((i, f"{['实', '虚', '合'][k]}[{j}]: {msg}"))
break
self.assertEqual(len(mismatches), 0, f"线段获取内部中枢序列 不一致 ({len(mismatches)}/{n}):\n" + "\n".join(f" Seg[{i}]: {detail}" for i, detail in mismatches[:5]))
# ---- 是否背驰过 ----
def test_线段是否背驰过双端一致(self):
"""线段的 是否背驰过() 双端结果一致(通过 缠论K线相等 逐项比对)."""
import chanlun
from chanlun import chan
obs_rs, obs_py = self._build_observers()
n = min(len(obs_rs.线段序列), len(obs_py.线段序列))
self.assertGreater(n, 0, "线段序列为空")
mismatches = []
for i in range(n):
r = chanlun.线段.是否背驰过(obs_rs.线段序列[i], obs_rs)
p = chan.线段.是否背驰过(obs_py.线段序列[i], obs_py)
if len(r) != len(p):
mismatches.append((i, f"len R={len(r)} P={len(p)}", obs_rs.线段序列[i].获取数据文本()))
else:
for a, b in zip(r, p):
eq, msg = chanlun.缠论K线相等(a, b)
if not eq:
mismatches.append((i, msg, obs_rs.线段序列[i].获取数据文本()))
break
self.assertEqual(len(mismatches), 0, f"线段是否背驰过 不一致 ({len(mismatches)}/{n}):\n" + "\n".join(f" [{i}] {detail}\n 段={txt[:120]}" for i, detail, txt in mismatches[:3]))
# ============================================================
# 指标挂载测试
# ============================================================
class Test指标挂载(unittest.TestCase):
"""指标计算与动态挂载回填测试."""
@staticmethod
def _make_k(i: int, ts_base: int = 1771675200, period: int = 300):
"""创建一根模拟K线."""
return chanlun.K线.创建普K(
"btcusd",
ts_base + i * period,
50000.0 + i,
51000.0 + i,
49000.0 + i,
50500.0 + i,
100.0 + i,
i,
period,
)
def test_基本指标计算(self):
"""每根K线都应有默认指标值."""
cfg = chanlun.缠论配置()
obs = chanlun.观察者("btcusd", 300, cfg)
for i in range(50):
obs.增加原始K线(self._make_k(i))
for k in obs.普通K线序列:
self.assertIn("macd", k.指标)
self.assertIn("rsi", k.指标)
self.assertIn("kdj", k.指标)
def test_动态MACD参数回填(self):
"""中途修改 obs.配置 添加 MACD 变体后,历史K线应被回填."""
cfg = chanlun.缠论配置()
obs = chanlun.观察者("btcusd", 300, cfg)
for i in range(100):
if i == 50:
obs.配置.MACD_参数列表 = [
("macd", 12, 26, 9),
("macd_10_20_7", 10, 20, 7),
]
obs.增加原始K线(self._make_k(i))
for k in obs.普通K线序列:
self.assertIn("macd_10_20_7", k.指标)
def test_多指标同时回填(self):
"""同时修改 MACD + RSI + KDJ 参数,验证全部回填."""
cfg = chanlun.缠论配置()
obs = chanlun.观察者("btcusd", 300, cfg)
for i in range(80):
if i == 40:
obs.配置.MACD_参数列表 = [("macd", 12, 26, 9), ("macd_fast", 5, 13, 5)]
obs.配置.RSI_周期列表 = [("rsi", 14), ("rsi_7", 7)]
obs.配置.KDJ_参数列表 = [("kdj", 9, 3, 3), ("kdj_5", 5, 2, 2)]
obs.增加原始K线(self._make_k(i))
for k in obs.普通K线序列:
self.assertIn("macd_fast", k.指标)
self.assertIn("rsi_7", k.指标)
self.assertIn("kdj_5", k.指标)
def test_回填后增量计算一致(self):
"""回填后的指标值应与从头计算一致."""
cfg_full = chanlun.缠论配置()
cfg_full.MACD_参数列表 = [("macd", 12, 26, 9), ("macd_extra", 8, 16, 6)]
obs_full = chanlun.观察者("btcusd", 300, cfg_full)
cfg_late = chanlun.缠论配置()
obs_late = chanlun.观察者("btcusd", 300, cfg_late)
for i in range(100):
if i == 50:
obs_late.配置.MACD_参数列表 = [("macd", 12, 26, 9), ("macd_extra", 8, 16, 6)]
obs_full.增加原始K线(self._make_k(i))
obs_late.增加原始K线(self._make_k(i))
seq_full = obs_full.普通K线序列
seq_late = obs_late.普通K线序列
self.assertEqual(len(seq_full), len(seq_late))
for i in range(len(seq_full)):
macd_full = seq_full[i].指标["macd_extra"]
macd_late = seq_late[i].指标["macd_extra"]
self.assertEqual(macd_full.DIF, macd_late.DIF)
self.assertEqual(macd_full.DEA, macd_late.DEA)
def test_同时间戳更新后指标重算(self):
"""同时间戳K线更新后,指标应基于新值重新计算."""
cfg = chanlun.缠论配置()
obs = chanlun.观察者("btcusd", 300, cfg)
ts = 1771675200
k1 = chanlun.K线.创建普K("btcusd", ts, 50000, 51000, 49000, 50500, 100, 0, 300)
obs.增加原始K线(k1)
k2 = chanlun.K线.创建普K("btcusd", ts + 300, 50500, 52000, 50000, 51500, 200, 1, 300)
obs.增加原始K线(k2)
macd_before = obs.普通K线序列[-1].指标["macd"].DIF
# 同时间戳,不同收盘价
k2_upd = chanlun.K线.创建普K("btcusd", ts + 300, 50500, 53000, 49000, 52500, 300, 1, 300)
obs.增加原始K线(k2_upd)
macd_after = obs.普通K线序列[-1].指标["macd"].DIF
self.assertNotEqual(macd_before, macd_after)
# ============================================================
# 线段分析层次 = 0 时不崩溃
# ============================================================
class Test线段分析层次为零(unittest.TestCase):
"""验证 线段分析层次=0 时,各处理方法不会越界崩溃."""
@classmethod
def setUpClass(cls):
if not _has_nb():
raise unittest.SkipTest("需要 .nb 数据文件")
def test_处理数据_不崩溃(self):
"""投喂K线时 线段分析层次=0 → 跳过所有线段/扩展线段/混合扩展线段分析,不应崩溃."""
import chanlun
from chanlun import chan
# — Rust 侧 —
cfg_rs = chanlun.缠论配置()
obs_rs = chanlun.观察者("btcusd", 300, cfg_rs)
obs_rs.线段分析层次 = 0
obs_rs.重置基础序列()
for ts, o, h, l, c, v in read_nb_bars(NB_PATH)[:500]:
obs_rs.投喂原始数据(ts, o, h, l, c, v)
self.assertGreater(len(obs_rs.缠论K线序列), 0, "缠K序列应有数据")
self.assertGreater(len(obs_rs.分型序列), 0, "分型序列应有数据")
self.assertEqual(len(obs_rs.线段序列组), 0, "线段序列组应为空")
self.assertTrue(all(len(s) == 0 for s in obs_rs.混合扩展线段序列组), "混合扩展线段序列组所有条目应为空")
# — Python 侧 (chan.py) —
cfg_py = chan.缠论配置()
obs_py = chan.观察者("btcusd", 300, cfg_py)
obs_py.线段分析层次 = 0
obs_py.重置基础序列()
for ts, o, h, l, c, v in read_nb_bars(NB_PATH)[:500]:
obs_py.投喂原始数据(ts, o, h, l, c, v)
self.assertGreater(len(obs_py.缠论K线序列), 0, "chan.py 缠K序列应有数据")
self.assertGreater(len(obs_py.分型序列), 0, "chan.py 分型序列应有数据")
self.assertEqual(len(obs_py.线段序列组), 0, "chan.py 线段序列组应为空")
def test_静态重新分析_不崩溃(self):
"""静态重新分析时 线段分析层次=0 → 跳过所有线段分析,不应崩溃."""
import chanlun
from chanlun import chan
# — Rust 侧 —
cfg_rs = chanlun.缠论配置()
obs_rs = chanlun.观察者("btcusd", 300, cfg_rs)
for ts, o, h, l, c, v in read_nb_bars(NB_PATH)[:300]:
obs_rs.投喂原始数据(ts, o, h, l, c, v)
self.assertGreater(len(obs_rs.线段序列组), 0, "正常初始化后应有线段")
obs_rs.线段分析层次 = 0
obs_rs.静态重新分析()
self.assertEqual(len(obs_rs.线段序列组), 0, "静态重新分析后线段序列组应为空")
self.assertGreater(len(obs_rs.分型序列), 0, "静态重新分析后分型序列应有数据")
# — Python 侧 (chan.py) —
cfg_py = chan.缠论配置()
obs_py = chan.观察者("btcusd", 300, cfg_py)
for ts, o, h, l, c, v in read_nb_bars(NB_PATH)[:300]:
obs_py.投喂原始数据(ts, o, h, l, c, v)
self.assertGreater(len(obs_py.线段序列组), 0, "chan.py 正常初始化后应有线段")
obs_py.线段分析层次 = 0
obs_py.静态重新分析()
self.assertEqual(len(obs_py.线段序列组), 0, "chan.py 静态重新分析后线段序列组应为空")
self.assertGreater(len(obs_py.分型序列), 0, "chan.py 静态重新分析后分型序列应有数据")
# ============================================================
# 集成对比测试
# ============================================================
class Test集成对比(unittest.TestCase):
"""全量集成测试:喂入 .nb 数据,与 Python 参考输出对比."""
@classmethod
def setUpClass(cls):
if not _has_nb():
raise unittest.SkipTest("需要 .nb 数据文件")
def test_full_integration(self):
out_dir = os.path.join(tempfile.gettempdir(), "chanlun_py_test_output")
bars = read_nb_bars(NB_PATH)
self.assertGreater(len(bars), 0, "未读取到任何 K 线")
obs = chanlun.观察者("btcusd", 300)
self.assertEqual(obs.标识, "btcusd:300")
self.assertEqual(obs.周期, 300)
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)
self.assertGreater(len(obs.普通K线序列), 0)
self.assertGreater(len(obs.缠论K线序列), 0)
os.makedirs(out_dir, exist_ok=True)
obs.测试_保存数据(out_dir)
subdirs = [d for d in os.listdir(out_dir) if os.path.isdir(os.path.join(out_dir, d))]
self.assertGreater(len(subdirs), 0, "No output subdirectory found!")
actual_out_dir = os.path.join(out_dir, subdirs[0])
out_files = sorted(os.listdir(actual_out_dir))
self.assertGreaterEqual(len(out_files), 14, f"Expected >= 14 output files, got {len(out_files)}")
if not os.path.isdir(_PY_REF_DIR):
self.skipTest(f"Python reference dir not found: {_PY_REF_DIR}")
ref_files = sorted(os.listdir(_PY_REF_DIR))
match_count = 0
diff_count = 0
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):
self.fail(f"MISSING: {fname}")
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:
match_count += 1
else:
diff_count += 1
for j, (rl, ol) in enumerate(zip(ref_lines, out_lines)):
if rl != ol:
print(f" DIFF {fname} line {j}:")
print(f" REF: {rl.rstrip()}")
print(f" OUT: {ol.rstrip()}")
break
if len(ref_lines) != len(out_lines):
print(f" DIFF {fname}: line count {len(ref_lines)} vs {len(out_lines)}")
self.assertEqual(diff_count, 0, f"{diff_count} files differ, {match_count} match")
print(f" All {match_count} Python reference files match!")
# ============================================================
# API 一致性测试 — chan.py vs chanlun
# ============================================================
class TestApi一致性(ApiConsistencyMixin, unittest.TestCase):
"""chan.py (Python参考) 与 chanlun (Rust/PyO3) 的 API 描述符类型一致性.
验证: 同名类的同名成员在两边有相同的描述符类型:
property / classmethod / staticmethod / regular_method
"""
reference_module = chanlun.chan
target_module = chanlun
# pydantic 模型方法 + Python list/str 继承方法 → 噪音过滤
noise_filters = [
# pydantic v1/v2
"construct",
"copy",
"dict",
"json",
"schema",
"schema_json",
"validate",
"parse_file",
"parse_obj",
"parse_raw",
"from_orm",
"update_forward_refs",
"model_",
"bool_parse_fallback_default",
# Python str (Enum 继承)
"capitalize",
"casefold",
"center",
"count",
"encode",
"endswith",
"expandtabs",
"find",
"format",
"format_map",
"index",
"isalnum",
"isalpha",
"isascii",
"isdecimal",
"isdigit",
"isidentifier",
"islower",
"isnumeric",
"isprintable",
"isspace",
"istitle",
"isupper",
"join",
"ljust",
"lower",
"lstrip",
"maketrans",
"partition",
"removeprefix",
"removesuffix",
"replace",
"rfind",
"rindex",
"rjust",
"rpartition",
"rsplit",
"rstrip",
"split",
"splitlines",
"startswith",
"strip",
"swapcase",
"title",
"translate",
"upper",
"zfill",
# Python list
"append",
"clear",
"extend",
"insert",
"pop",
"remove",
"reverse",
"sort",
]
# 已知 target 中缺失的成员(有意未移植或用户自行实现)
known_missing_in_target = {
"K线合成器": {"设置事件回调"},
"观察者": {"识别买卖点"}, # 用户子类化时重写
"缠论配置": {"兼容旧版本配置"}, # pydantic v1 兼容
}
# ============================================================
# main
# ============================================================
class Test导出函数双端等效(unittest.TestCase):
"""验证: 序列修改类导出函数与 chan.py 行为一致 (就地修改 + 返回值等效)"""
_TEST_COUNT = 300 # 投喂 K 线数
@classmethod
def setUpClass(cls):
if not _has_nb():
raise unittest.SkipTest("需要 .nb 数据文件")
cls.bars = read_nb_bars(NB_PATH)
# ---- 辅助 ----
def _make_observers(self):
import chanlun
from chanlun import chan
cfg_rs = chanlun.缠论配置()
cfg_rs.计算指标 = True
cfg_py = chan.缠论配置()
cfg_py.计算指标 = True
obs_rs = chanlun.观察者("btcusd", 300, cfg_rs)
obs_py = chan.观察者("btcusd", 300, cfg_py)
for i, (ts, o, h, l, c, v) in enumerate(self.bars[: self._TEST_COUNT]):
obs_rs.投喂原始数据(ts, o, h, l, c, v)
obs_py.投喂原始数据(ts, o, h, l, c, v)
return obs_rs, obs_py
# ================================================================
# 缠论K线.分析
# ================================================================
def test_缠论K线分析_等效(self):
"""缠论K线.分析 双端行为一致 (返回值 + 序列长度)."""
import chanlun
from chanlun import chan
obs_rs, obs_py = self._make_observers()
ck_rs: list = []
bar_rs: list = []
ck_py: list = []
bar_py: list = []
mismatches = []
for k_rs, k_py in zip(obs_rs.普通K线序列[100:200], obs_py.普通K线序列[100:200]):
st_rs, fx_rs = chanlun.缠论K线.分析(k_rs, ck_rs, bar_rs, obs_rs.配置)
st_py, fx_py = chan.缠论K线.分析(k_py, ck_py, bar_py, obs_py.配置)
if st_rs != st_py:
mismatches.append(f"状态: R={st_rs} P={st_py}")
if (fx_rs is None) != (fx_py is None):
mismatches.append(f"分型None: R={fx_rs is None} P={fx_py is None}")
self.assertEqual(len(mismatches), 0, f"缠论K线.分析 不一致 ({len(mismatches)}):\n" + "\n".join(mismatches[:5]))
self.assertEqual(len(ck_rs), len(ck_py), f"缠K序列长度: R={len(ck_rs)} P={len(ck_py)}")
self.assertEqual(len(bar_rs), len(bar_py), f"普K序列长度: R={len(bar_rs)} P={len(bar_py)}")
# ================================================================
# 笔.分析
# ================================================================
def test_笔分析_等效(self):
"""笔.分析 双端行为一致 (返回值 + 序列修改)."""
import chanlun
from chanlun import chan
obs_rs, obs_py = self._make_observers()
ck_rs = list(obs_rs.缠论K线序列)
ck_py = list(obs_py.缠论K线序列)
bar_rs = list(obs_rs.普通K线序列)
bar_py = list(obs_py.普通K线序列)
fr_rs: list = []
bi_rs: list = []
fr_py: list = []
bi_py: list = []
mismatches = []
for fx_rs, fx_py in zip(obs_rs.分型序列[2:], obs_py.分型序列[2:]):
d_rs = chanlun..分析(fx_rs, fr_rs, bi_rs, ck_rs, bar_rs, 0, obs_rs.配置)
d_py = chan..分析(fx_py, fr_py, bi_py, ck_py, bar_py, 0, obs_py.配置)
if d_rs != d_py:
mismatches.append(f"递归层次: R={d_rs} P={d_py}")
self.assertEqual(len(mismatches), 0, f"笔.分析 不一致 ({len(mismatches)}):\n" + "\n".join(mismatches[:3]))
self.assertEqual(len(fr_rs), len(fr_py), f"分型序列长度: R={len(fr_rs)} P={len(fr_py)}")
self.assertGreater(len(bi_rs), 0, "笔序列为空 (Rust)")
self.assertEqual(len(bi_rs), len(bi_py), f"笔序列长度: R={len(bi_rs)} P={len(bi_py)}")
# ================================================================
# 线段.分析
# ================================================================
def test_线段分析_等效(self):
"""线段.分析 双端行为一致 (序列修改)."""
import chanlun
from chanlun import chan
obs_rs, obs_py = self._make_observers()
seg_rs = list(obs_rs.线段序列)
seg_py = list(obs_py.线段序列)
# 先用 chanlun.线段.分析 重新分析
bi_list_rs = list(obs_rs.笔序列)
bi_list_py = list(obs_py.笔序列)
# 重置线段序列
seg_rs.clear()
seg_py.clear()
chanlun.线段.分析(bi_list_rs, seg_rs, obs_rs.配置)
chan.线段.分析(bi_list_py, seg_py, obs_py.配置)
self.assertGreater(len(seg_rs), 0, "线段序列为空 (Rust)")
self.assertEqual(len(seg_rs), len(seg_py), f"线段序列长度: R={len(seg_rs)} P={len(seg_py)}")
# ================================================================
# 线段.扩展分析
# ================================================================
def test_线段扩展分析_等效(self):
"""线段.扩展分析 双端行为一致."""
import chanlun
from chanlun import chan
obs_rs, obs_py = self._make_observers()
ext_rs = list(obs_rs.扩展线段序列)
ext_py = list(obs_py.扩展线段序列)
ext_rs.clear()
ext_py.clear()
bi_list_rs = list(obs_rs.笔序列)
bi_list_py = list(obs_py.笔序列)
chanlun.线段.扩展分析(bi_list_rs, ext_rs, obs_rs.配置)
chan.线段.扩展分析(bi_list_py, ext_py, obs_py.配置)
self.assertGreater(len(ext_rs), 0, "扩展线段序列为空 (Rust)")
self.assertEqual(len(ext_rs), len(ext_py), f"扩展线段: R={len(ext_rs)} P={len(ext_py)}")
# ================================================================
# 中枢.分析
# ================================================================
def test_中枢分析_等效(self):
"""中枢.分析 双端行为一致."""
import chanlun
from chanlun import chan
obs_rs, obs_py = self._make_observers()
hub_rs: list = []
hub_py: list = []
bi_list_rs = list(obs_rs.笔序列)
bi_list_py = list(obs_py.笔序列)
chanlun.中枢.分析(bi_list_rs, hub_rs, True, "", 0)
chan.中枢.分析(bi_list_py, hub_py, True, "", 0)
self.assertGreater(len(hub_rs), 0, "笔中枢序列为空 (Rust)")
self.assertEqual(len(hub_rs), len(hub_py), f"笔中枢: R={len(hub_rs)} P={len(hub_py)}")
# ================================================================
# 中枢.获取扩展中枢
# ================================================================
def test_获取扩展中枢_等效(self):
"""中枢.获取扩展中枢 双端行为一致."""
import chanlun
from chanlun import chan
obs_rs, obs_py = self._make_observers()
ext_hub_rs = list(obs_rs.扩展中枢序列)
ext_hub_py = list(obs_py.扩展中枢序列)
ext_hub_rs.clear()
ext_hub_py.clear()
# init with 笔中枢
bi_list_rs = list(obs_rs.笔序列)
bi_list_py = list(obs_py.笔序列)
chanlun.中枢.分析(bi_list_rs, ext_hub_rs, True, "", 0)
chan.中枢.分析(bi_list_py, ext_hub_py, True, "", 0)
# 若基础序列≥9,调用获取扩展中枢
for hub_rs, hub_py in zip(ext_hub_rs, ext_hub_py):
sub_rs: list = []
sub_py: list = []
hub_rs.获取扩展中枢(sub_rs, obs_rs.配置)
hub_py.获取扩展中枢(sub_py, obs_py.配置)
if len(sub_rs) != len(sub_py):
self.fail(f"获取扩展中枢 长度不一致: R={len(sub_rs)} P={len(sub_py)}")
# all passed (or vacuously true if no hubs with >=9 segments)
class TestK线合成器(unittest.TestCase):
"""K线合成器 模块测试."""
def test_构造(self):
"""K线合成器 初始状态."""
import chanlun
s = chanlun.K线合成器("btcusd", [60, 300])
self.assertEqual(s.标识, "btcusd")
self.assertEqual(s.周期组, [60, 300])
self.assertIsNone(s.获取当前K线(60))
self.assertIsNone(s.获取当前K线(300))
def test_投喂单周期(self):
"""投喂单周期K线."""
import chanlun
s = chanlun.K线合成器("btcusd", [300])
bar = chanlun.K线.创建普K("btcusd", 300, 100, 110, 90, 105, 1000, 0, 60)
s.投喂K线(bar)
cur = s.获取当前K线(300)
self.assertIsNotNone(cur)
self.assertEqual(cur.周期, 300)
self.assertAlmostEqual(cur., 110)
def test_投喂多周期(self):
"""投喂生成多周期K线."""
import chanlun
s = chanlun.K线合成器("btcusd", [60, 300])
bar = chanlun.K线.创建普K("btcusd", 60, 100, 110, 90, 105, 1000, 0, 60)
s.投喂K线(bar)
self.assertIsNotNone(s.获取当前K线(60))
self.assertIsNotNone(s.获取当前K线(300))
def test_便捷投喂(self):
"""便捷投喂方法."""
import chanlun
s = chanlun.K线合成器("btcusd", [300])
s.投喂(1218124800, 100, 110, 90, 105, 1000)
cur = s.获取当前K线(300)
self.assertIsNotNone(cur)
class TestK线合成器双端一致(unittest.TestCase):
"""K线合成器 Rust vs chan.py 运行时行为一致 — 每步对比."""
_TEST_COUNT = 200
@classmethod
def setUpClass(cls):
if not _has_nb():
raise unittest.SkipTest("需要 .nb 数据文件")
cls.bars = read_nb_bars(NB_PATH, cls._TEST_COUNT)
def _make_synthesizers(self):
import chanlun
from chanlun import chan
return chanlun.K线合成器("btcusd", [60, 300]), chan.K线合成器("btcusd", [60, 300])
def test_合成K线逐笔OHLC一致(self):
"""每投喂一根K线后,大周期当前K线OHLC双端一致."""
import chanlun
from chanlun import chan
s_rs, s_py = self._make_synthesizers()
mismatches = []
for i, (ts, o, h, l, c, v) in enumerate(self.bars):
bar_rs = chanlun.K线.创建普K("btcusd", ts, o, h, l, c, v, 0, 60)
bar_py = chan.K线.创建普K("btcusd", ts, o, h, l, c, v, 0, 60)
s_rs.投喂K线(bar_rs)
s_py.投喂K线(bar_py)
cur_rs = s_rs.获取当前K线(300)
cur_py = s_py.获取当前K线(300)
if cur_rs is None and cur_py is None:
continue
if (cur_rs is None) != (cur_py is None):
mismatches.append(f"#{i} ts={ts}: R={cur_rs} P={cur_py}")
continue
if abs(cur_rs. - cur_py.) > 1e-6 or abs(cur_rs. - cur_py.) > 1e-6 or abs(cur_rs.开盘价 - cur_py.开盘价) > 1e-6 or abs(cur_rs.收盘价 - cur_py.收盘价) > 1e-6:
mismatches.append(f"#{i} ts={ts}: R(o={cur_rs.开盘价} h={cur_rs.} l={cur_rs.} c={cur_rs.收盘价}) P(o={cur_py.开盘价} h={cur_py.} l={cur_py.} c={cur_py.收盘价})")
self.assertEqual(len(mismatches), 0, f"合成K线不一致 ({len(mismatches)}处):\n" + "\n".join(mismatches[:10]))
def test_合成K线逐笔时间戳一致(self):
"""每投喂一根K线后,大周期当前K线时间戳双端一致."""
import chanlun
from chanlun import chan
s_rs, s_py = self._make_synthesizers()
mismatches = []
for i, (ts, o, h, l, c, v) in enumerate(self.bars):
bar_rs = chanlun.K线.创建普K("btcusd", ts, o, h, l, c, v, 0, 60)
bar_py = chan.K线.创建普K("btcusd", ts, o, h, l, c, v, 0, 60)
s_rs.投喂K线(bar_rs)
s_py.投喂K线(bar_py)
cur_rs = s_rs.获取当前K线(300)
cur_py = s_py.获取当前K线(300)
if cur_rs is None and cur_py is None:
continue
if (cur_rs is None) != (cur_py is None):
mismatches.append(f"#{i} ts={ts}: R={cur_rs} P={cur_py}")
continue
if int(cur_rs.时间戳) != int(cur_py.时间戳):
mismatches.append(f"#{i}: R={int(cur_rs.时间戳)} P={int(cur_py.时间戳)}")
self.assertEqual(len(mismatches), 0, f"时间戳不一致 ({len(mismatches)}处):\n" + "\n".join(mismatches[:10]))
class Test立体分析器(unittest.TestCase):
"""立体分析器 模块测试."""
@classmethod
def setUpClass(cls):
if not _has_nb():
raise unittest.SkipTest("需要 .nb 数据文件")
cls.bars = read_nb_bars(NB_PATH) # [:300]
def test_构造(self):
"""立体分析器 构造."""
import chanlun
cfg = chanlun.缠论配置()
ma = chanlun.立体分析器("btcusd", [60, 300], cfg)
self.assertEqual(ma.周期组, [60, 300])
def test_单体分析器(self):
"""单体分析器字典包含各周期观察者."""
import chanlun
cfg = chanlun.缠论配置()
ma = chanlun.立体分析器("btcusd", [60, 300], cfg)
d = ma._单体分析器
self.assertIn(60, d)
self.assertIn(300, d)
self.assertEqual(d[60].周期, 60)
self.assertEqual(d[300].周期, 300)
def test_投喂K线生成各级别数据(self):
"""投喂K线后各周期有分析数据."""
import chanlun
cfg = chanlun.缠论配置()
ma = chanlun.立体分析器("btcusd", [300, 300 * 5], cfg)
for ts, o, h, l, c, v in self.bars:
bar = chanlun.K线.创建普K("btcusd", ts, o, h, l, c, v, 0, 300)
ma.投喂K线(bar)
obs_300 = ma._单体分析器[300]
self.assertGreater(len(obs_300.缠论K线序列), 0, "300周期无缠K")
self.assertGreater(len(obs_300.普通K线序列), 0, "300周期无普K")
class Test立体分析器双端一致(unittest.TestCase):
"""立体分析器 Rust vs chan.py 运行时行为一致 — 每步对比 + 数据内容对比."""
_TEST_COUNT = 500
@classmethod
def setUpClass(cls):
if not _has_nb():
raise unittest.SkipTest("需要 .nb 数据文件")
cls.bars = read_nb_bars(NB_PATH, cls._TEST_COUNT)
def _make_analyzers(self):
import chanlun
from chanlun import chan
cfg_rs = chanlun.缠论配置()
cfg_py = chan.缠论配置()
return (chanlun.立体分析器("btcusd", [300, 300 * 5], cfg_rs), chan.立体分析器("btcusd", [300, 300 * 5], cfg_py))
def test_立体分析逐笔笔序列增长一致(self):
"""每投喂K线后,各周期笔序列长度双端一致."""
import chanlun
from chanlun import chan
ma_rs, ma_py = self._make_analyzers()
mismatches = []
for i, (ts, o, h, l, c, v) in enumerate(self.bars):
bar_rs = chanlun.K线.创建普K("btcusd", ts, o, h, l, c, v, 0, 300)
bar_py = chan.K线.创建普K("btcusd", ts, o, h, l, c, v, 0, 300)
ma_rs.投喂K线(bar_rs)
ma_py.投喂K线(bar_py)
for period in [300, 300 * 5]:
obs_rs = ma_rs._单体分析器[period]
obs_py = ma_py._单体分析器[period]
if len(obs_rs.笔序列) != len(obs_py.笔序列):
mismatches.append(f"#{i} ts={ts} 周期{period}: R笔={len(obs_rs.笔序列)} P笔={len(obs_py.笔序列)}")
if len(obs_rs.分型序列) != len(obs_py.分型序列):
mismatches.append(f"#{i} ts={ts} 周期{period}: R分型={len(obs_rs.分型序列)} P分型={len(obs_py.分型序列)}")
eq, msg = chan.观察者相等(obs_py, obs_rs)
self.assertTrue(eq, msg)
self.assertEqual(len(mismatches), 0, f"立体分析不一致 ({len(mismatches)}处):\n" + "\n".join(mismatches[:10]))
def test_立体分析逐笔缠K序列增长一致(self):
"""每投喂K线后,各周期缠论K线序列长度双端一致."""
import chanlun
from chanlun import chan
ma_rs, ma_py = self._make_analyzers()
mismatches = []
for i, (ts, o, h, l, c, v) in enumerate(self.bars):
bar_rs = chanlun.K线.创建普K("btcusd", ts, o, h, l, c, v, 0, 300)
bar_py = chan.K线.创建普K("btcusd", ts, o, h, l, c, v, 0, 300)
ma_rs.投喂K线(bar_rs)
ma_py.投喂K线(bar_py)
for period in [300, 300 * 5]:
obs_rs = ma_rs._单体分析器[period]
obs_py = ma_py._单体分析器[period]
if len(obs_rs.缠论K线序列) != len(obs_py.缠论K线序列):
mismatches.append(f"#{i} ts={ts} 周期{period}: R缠K={len(obs_rs.缠论K线序列)} P缠K={len(obs_py.缠论K线序列)}")
self.assertEqual(len(mismatches), 0, f"缠K序列不一致 ({len(mismatches)}处):\n" + "\n".join(mismatches[:10]))
def test_立体分析逐笔线段序列增长一致(self):
"""每投喂K线后,显示周期线段序列长度双端一致."""
import chanlun
from chanlun import chan
ma_rs, ma_py = self._make_analyzers()
mismatches = []
for i, (ts, o, h, l, c, v) in enumerate(self.bars):
bar_rs = chanlun.K线.创建普K("btcusd", ts, o, h, l, c, v, 0, 300)
bar_py = chan.K线.创建普K("btcusd", ts, o, h, l, c, v, 0, 300)
ma_rs.投喂K线(bar_rs)
ma_py.投喂K线(bar_py)
obs_rs = ma_rs._单体分析器[300 * 5]
obs_py = ma_py._单体分析器[300 * 5]
if len(obs_rs.线段序列) != len(obs_py.线段序列):
mismatches.append(f"#{i} ts={ts}: R线段={len(obs_rs.线段序列)} P线段={len(obs_py.线段序列)}")
self.assertEqual(len(mismatches), 0, f"线段序列不一致 ({len(mismatches)}处):\n" + "\n".join(mismatches[:10]))
def test_立体分析器相等(self):
"""立体分析后 chan.立体分析器相等 全量数据对比一致."""
import chanlun
from chanlun import chan
ma_rs, ma_py = self._make_analyzers()
for ts, o, h, l, c, v in self.bars:
bar_rs = chanlun.K线.创建普K("btcusd", ts, o, h, l, c, v, 0, 300)
bar_py = chan.K线.创建普K("btcusd", ts, o, h, l, c, v, 0, 300)
ma_rs.投喂K线(bar_rs)
ma_py.投喂K线(bar_py)
eq, msg = chan.立体分析器相等(ma_rs, ma_py)
self.assertTrue(eq, msg)
def test_立体分析观察者相等(self):
"""立体分析后主周期 chan.观察者相等 全量数据对比一致."""
import chanlun
from chanlun import chan
ma_rs, ma_py = self._make_analyzers()
for ts, o, h, l, c, v in self.bars:
bar_rs = chanlun.K线.创建普K("btcusd", ts, o, h, l, c, v, 0, 300)
bar_py = chan.K线.创建普K("btcusd", ts, o, h, l, c, v, 0, 300)
ma_rs.投喂K线(bar_rs)
ma_py.投喂K线(bar_py)
eq, msg = chan.观察者相等(ma_rs._单体分析器[300 * 5], ma_py._单体分析器[300 * 5])
self.assertTrue(eq, msg)
class Test缠论配置双端一致(unittest.TestCase):
"""缠论配置 to_dict / from_dict / model_copy 双端输出一致."""
def _make_configs(self):
import chanlun
from chanlun import chan
cfg_rs = chanlun.缠论配置()
cfg_py = chan.缠论配置()
return cfg_rs, cfg_py
def test_to_dict_keys_一致(self):
"""to_dict 字段名集合双端一致."""
import chanlun
from chanlun import chan
cfg_rs, cfg_py = self._make_configs()
d_rs = cfg_rs.to_dict()
d_py = cfg_py.to_dict()
self.assertEqual(set(d_rs.keys()), set(d_py.keys()), f"to_dict 字段不一致: R extra={set(d_rs.keys()) - set(d_py.keys())} P extra={set(d_py.keys()) - set(d_rs.keys())}")
def test_to_dict_values_一致(self):
"""to_dict 值双端一致."""
import chanlun
from chanlun import chan
cfg_rs, cfg_py = self._make_configs()
d_rs = cfg_rs.to_dict()
d_py = cfg_py.to_dict()
mismatches = []
for k in d_rs:
v_rs = d_rs[k]
v_py = d_py.get(k)
if v_rs is None and v_py is None:
continue
if v_rs != v_py:
mismatches.append(f" {k}: R={v_rs!r} P={v_py!r}")
self.assertEqual(len(mismatches), 0, f"to_dict 值不一致 ({len(mismatches)}处):\n" + "\n".join(mismatches[:10]))
def test_to_json_content_一致(self):
"""to_json 内容一致(JSON 解析后对比)."""
import chanlun
from chanlun import chan
import json
cfg_rs, cfg_py = self._make_configs()
j_rs = json.loads(cfg_rs.to_json())
j_py = json.loads(cfg_py.to_json())
self.assertEqual(j_rs, j_py, f"to_json 内容不一致")
def test_from_dict_roundtrip_一致(self):
"""from_dict → to_dict 往返双端一致."""
import chanlun
from chanlun import chan
cfg_rs, cfg_py = self._make_configs()
d = cfg_rs.to_dict()
cfg2_rs = chanlun.缠论配置.from_dict(d)
cfg2_py = chan.缠论配置.from_dict(d)
d2_rs = cfg2_rs.to_dict()
d2_py = cfg2_py.to_dict()
for k in d2_rs:
self.assertEqual(d2_rs[k], d2_py.get(k), f"from_dict 往返不一致: {k}")
def test_from_json_roundtrip_一致(self):
"""from_json → to_dict 往返双端一致."""
import chanlun
from chanlun import chan
cfg_rs, cfg_py = self._make_configs()
j = cfg_rs.to_json()
cfg2_rs = chanlun.缠论配置.from_json(j)
cfg2_py = chan.缠论配置.from_json(j)
# 验证标识和关键字段一致
self.assertEqual(cfg2_rs.标识, cfg2_py.标识)
self.assertEqual(cfg2_rs.笔内元素数量, cfg2_py.笔内元素数量)
self.assertEqual(cfg2_rs.买卖点偏移, cfg2_py.买卖点偏移)
self.assertEqual(cfg2_rs.指标计算方式, cfg2_py.指标计算方式)
def test_custom_values_from_dict_一致(self):
"""自定义字段 from_dict 双端一致."""
import chanlun
from chanlun import chan
data = {
"标识": "custom_test",
"缠K合并替换": True,
"笔内元素数量": 8,
"笔弱化": True,
"计算指标": False,
"指标计算方式": "高低均值",
"平滑异同移动平均线_快线周期": 12,
"买卖点偏移": 3,
"买卖点激进识别": True,
}
cfg_rs = chanlun.缠论配置.from_dict(data)
cfg_py = chan.缠论配置.from_dict(data)
d_rs = cfg_rs.to_dict()
d_py = cfg_py.to_dict()
for k in data:
self.assertEqual(d_rs.get(k), d_py.get(k), f"自定义字段 {k}: R={d_rs.get(k)} P={d_py.get(k)}")
def test_model_copy_一致(self):
"""model_copy 双端输出一致."""
import chanlun
from chanlun import chan
cfg_rs, cfg_py = self._make_configs()
update = {"标识": "copied", "推送K线": False, "笔内元素数量": 10}
copy_rs = cfg_rs.model_copy(update)
copy_py = cfg_py.model_copy(update)
self.assertEqual(copy_rs.标识, copy_py.标识)
self.assertEqual(copy_rs.笔内元素数量, copy_py.笔内元素数量)
self.assertFalse(copy_rs.推送K线)
self.assertFalse(copy_py.推送K线)
# 未更新字段保持原值一致
self.assertEqual(copy_rs.买卖点偏移, copy_py.买卖点偏移)
def test_from_dict_过滤未知字段_一致(self):
"""from_dict 过滤未知字段(兼容旧版本配置)双端一致."""
import chanlun
from chanlun import chan
data = {"标识": "test", "笔内元素数量": 7, "废弃字段_已删除": 999, "另一个旧字段": "xxx"}
cfg_rs = chanlun.缠论配置.from_dict(data)
cfg_py = chan.缠论配置.from_dict(data)
self.assertEqual(cfg_rs.标识, cfg_py.标识)
self.assertEqual(cfg_rs.笔内元素数量, cfg_py.笔内元素数量)
# 未知字段应被忽略,不影响构造
d_rs = cfg_rs.to_dict()
self.assertNotIn("废弃字段_已删除", d_rs)
def test_不推送_一致(self):
"""不推送 静态方法双端一致."""
import chanlun
from chanlun import chan
cfg_rs = chanlun.缠论配置.不推送()
cfg_py = chan.缠论配置.不推送()
self.assertFalse(cfg_rs.推送K线)
self.assertFalse(cfg_py.推送K线)
self.assertFalse(cfg_rs.图表展示)
self.assertFalse(cfg_py.图表展示)
self.assertEqual(cfg_rs.笔内元素数量, cfg_py.笔内元素数量)
def test_对比_默认一致(self):
"""默认配置 self 对比应无差异."""
import chanlun
from chanlun import chan
cfg_rs_a, _ = self._make_configs()
cfg_rs_b = chanlun.缠论配置()
diff_rs = cfg_rs_a.对比(cfg_rs_b)
self.assertIsInstance(diff_rs, dict)
self.assertEqual(len(diff_rs), 0, "默认一致配置不应有差异")
# Python side
cfg_py_a = chan.缠论配置()
cfg_py_b = chan.缠论配置()
diff_py = cfg_py_a.对比(cfg_py_b)
self.assertEqual(len(diff_py), 0)
def test_对比_有差异字段一致(self):
"""修改字段后 对比 双端输出一致."""
import chanlun
from chanlun import chan
cfg_rs_a, cfg_py_a = self._make_configs()
# 构造有差异的配置
update = {"标识": "changed", "笔内元素数量": 99, "推送K线": False}
cfg_rs_b = cfg_rs_a.model_copy(update)
cfg_py_b = cfg_py_a.model_copy(update)
diff_rs = cfg_rs_a.对比(cfg_rs_b)
diff_py = cfg_py_a.对比(cfg_py_b)
self.assertEqual(set(diff_rs.keys()), set(diff_py.keys()), f"对比字段不一致: R={set(diff_rs.keys())} P={set(diff_py.keys())}")
for k in diff_rs:
self.assertEqual(diff_rs[k], diff_py[k], f"对比[{k}] 值不一致: R={diff_rs[k]!r} P={diff_py[k]!r}")
def test_对比_往返一致(self):
"""to_dict → from_dict → 对比 应无差异."""
import chanlun
from chanlun import chan
cfg_rs, _ = self._make_configs()
d = cfg_rs.to_dict()
cfg2_rs = chanlun.缠论配置.from_dict(d)
diff = cfg_rs.对比(cfg2_rs)
self.assertEqual(len(diff), 0, f"Rust往返后对比不应有差异: {diff}")
# Python side
cfg_py = chan.缠论配置()
d_py = cfg_py.to_dict()
cfg2_py = chan.缠论配置.from_dict(d_py)
diff_py = cfg_py.对比(cfg2_py)
self.assertEqual(len(diff_py), 0)
def test_对比_与chan输出一致(self):
"""对比 输出与 chan.对比 逐项一致."""
import chanlun
from chanlun import chan
cfg_rs, cfg_py = self._make_configs()
update = {"标识": "test_x", "缠K合并替换": True, "笔内元素数量": 7, "计算指标": False, "买卖点激进识别": True, "线段_修正": True}
alt_rs = cfg_rs.model_copy(update)
alt_py = cfg_py.model_copy(update)
# Rust binding: cfg_rs.对比(alt_rs)
diff_rs = cfg_rs.对比(alt_rs)
# chan.py: cfg_py.对比(alt_py)
diff_py = cfg_py.对比(alt_py)
self.assertEqual(diff_rs, diff_py, f"对比输出不一致:\n R={diff_rs}\n P={diff_py}")
def test_对比_only_model_fields(self):
"""对比 仅比较 model_fields 字段."""
import chanlun
from chanlun import chan
cfg_rs, cfg_py = self._make_configs()
update = {"标识": "only_test"}
alt_rs = cfg_rs.model_copy(update)
alt_py = cfg_py.model_copy(update)
diff_rs = cfg_rs.对比(alt_rs)
diff_py = cfg_py.对比(alt_py)
self.assertEqual(len(diff_rs), 1)
self.assertEqual(len(diff_py), 1)
self.assertIn("标识", diff_rs)
self.assertIn("标识", diff_py)
self.assertEqual(diff_rs["标识"], "only_test")
self.assertEqual(diff_py["标识"], "only_test")
def test_对比_不推送_一致(self):
"""不推送 配置与默认配置 对比 双端一致."""
import chanlun
from chanlun import chan
cfg_rs, cfg_py = self._make_configs()
muted_rs = chanlun.缠论配置.不推送()
muted_py = chan.缠论配置.不推送()
diff_rs = cfg_rs.对比(muted_rs)
diff_py = cfg_py.对比(muted_py)
self.assertEqual(set(diff_rs.keys()), set(diff_py.keys()))
# 不推送应关闭所有推送/图表字段
for k in diff_rs:
self.assertFalse(diff_rs[k], f"不推送差异字段 {k} 应为 False")
self.assertFalse(diff_py[k], f"不推送差异字段 {k} 应为 False")
class Test生成K线双端一致(unittest.TestCase):
"""根据当前K线生成新K线 Rust vs chan.py 输出一致."""
def test_生成K线_居中各方向双端一致(self):
"""居中模式下各方向生成K线双端OHLC一致(居中=确定性输出)"""
import chanlun
from chanlun import chan
bar_rs = chanlun.K线.创建普K("btcusd", 1000000000, 50000, 50200, 49800, 50100, 100, 0, 300)
bar_py = chan.K线.创建普K("btcusd", chan.转化为时间戳(1000000000), 50000, 50200, 49800, 50100, 100, 0, 300)
directions = {
"向上": 0,
"向下": 1,
"向上缺口": 2,
"向下缺口": 3,
"衔接向上": 4,
"衔接向下": 5,
}
py_dirs = {
"向上": chan.相对方向.向上,
"向下": chan.相对方向.向下,
"向上缺口": chan.相对方向.向上缺口,
"向下缺口": chan.相对方向.向下缺口,
"衔接向上": chan.相对方向.衔接向上,
"衔接向下": chan.相对方向.衔接向下,
}
for name in directions:
new_rs = bar_rs.根据当前K线生成新K线(directions[name], 居中=True)
new_py = bar_py.根据当前K线生成新K线(py_dirs[name], 居中=True)
# 居中模式下,高和低是确定性的(偏移=高低差*0.5)
# 开盘价/收盘价/成交量含随机,不比较
tol = 1.0 + abs(new_py.) * 1e-6
self.assertAlmostEqual(new_rs., new_py., delta=tol, msg=f"{name}: 最高价不一致 (R={new_rs.}, P={new_py.})")
self.assertAlmostEqual(new_rs., new_py., delta=tol, msg=f"{name}: 最低价不一致 (R={new_rs.}, P={new_py.})")
# 时间戳和序号
self.assertEqual(new_rs.序号, new_py.序号)
self.assertEqual(int(new_rs.时间戳), int(chan.转化为时间戳_数字(new_py.时间戳)))
def test_生成K线_居中外推验证(self):
"""居中向上生成:新K线的高/低应整体高于原K线."""
import chanlun
bar = chanlun.K线.创建普K("btcusd", 1000000000, 50000, 50200, 49800, 50100, 100, 0, 300)
new = bar.根据当前K线生成新K线(0, 居中=True)
self.assertGreater(new., bar., "向上:新高应高于原高")
self.assertGreater(new., bar., "向上:新低应高于原低")
new_down = bar.根据当前K线生成新K线(1, 居中=True)
self.assertLess(new_down., bar., "向下:新高应低于原高")
self.assertLess(new_down., bar., "向下:新低应低于原低")
def test_生成K线_衔接验证(self):
"""衔接向上:新K线的低 = 原K线的高(无缝衔接)."""
import chanlun
bar = chanlun.K线.创建普K("btcusd", 1000000000, 50000, 50200, 49800, 50100, 100, 0, 300)
new = bar.根据当前K线生成新K线(4, 居中=True)
self.assertAlmostEqual(new., bar., delta=1e-6, msg="衔接向上:新低应=原高")
new_down = bar.根据当前K线生成新K线(5, 居中=True)
self.assertAlmostEqual(new_down., bar., delta=1e-6, msg="衔接向下:新高应=原低")
if __name__ == "__main__":
unittest.main()