diff --git a/chan.py b/chan.py index 7902482..22cf0a0 100644 --- a/chan.py +++ b/chan.py @@ -46,6 +46,7 @@ from typing import ( Union, Sequence, Callable, + Set, ) from pydantic import BaseModel, Field, model_validator, ValidationError, field_validator @@ -1816,7 +1817,7 @@ class K线(object): print(f"保存到DAT文件: {路径}") @classmethod - def 读取大端字节数组(cls, 字节组: bytes, 周期: int = 60, 标识: str = "Bar") -> "K线": + def 读取大端字节数组(cls, 字节组: bytes, 周期: int, 标识: str) -> "K线": """从大端字节序二进制数据反序列化K线(兼容.dat/.nb文件格式) :param 字节组: 二进制数据(48字节) @@ -1929,6 +1930,7 @@ class 缠论K线(object): self.原始起始序号: int = 原始起始序号 self.原始结束序号: int = 原始结束序号 self.标的K线: "K线" = 普K + self.买卖点信息: Set[str] = set() def __str__(self): return f"{self.标识}<{self.序号}, {self.分型}, {self.周期}, {self.方向}, {self.时间戳}, {self.高:g}, {self.低:g}>" @@ -1943,6 +1945,7 @@ class 缠论K线(object): :return: 新的缠论K线实例 """ K = 缠论K线(self.序号, self.时间戳, self.高, self.低, self.方向, self.标的K线, self.原始起始序号, self.原始结束序号, self.分型) + K.买卖点信息.update(self.买卖点信息) return K @property @@ -2518,6 +2521,7 @@ class 虚线(object): """ 文 = 虚线序列[0].文 武 = 虚线序列[-1].武 + assert 文.结构 != 武.结构 标识 = "线段" if 虚线序列[0].标识 == "笔" else f"线段<{虚线序列[0].标识}>" 段 = 虚线(0, 标识, 文, 武, 虚线序列[0].级别 + 1) 段.特征序列 = [None] * 3 @@ -2873,8 +2877,8 @@ class 虚线(object): def 统计MACD行为(cls, 普K序列: List[K线], 最大间隔: int = 8, 最少交叉数: int = 3) -> dict: """ :param 普K序列: K线序列 - :param 最大间隔: 最大间隔 - :param 最少交叉数: 最少交叉数 + :param 最大间隔: 最大间隔 8 + :param 最少交叉数: 最少交叉数 3 :return: 统计字典 """ # 1. 穿越零轴计数 @@ -2922,6 +2926,19 @@ class 虚线(object): "密集交叉区域": 密集区, # (起始交叉索引, 结束交叉索引, 交叉次数) } + @classmethod + def 获取_武(cls, 实线: 虚线) -> 分型: + """递归获取虚线的终点分型(笔直接返回武,线段递归到底层笔的武) + :param 实线: 虚线 + :return: 分型 + """ + if 实线.标识 == "笔": + return 实线.武 + tmp = 实线 + while tmp.标识 != "笔": + tmp = tmp.基础序列[-1] + return tmp.武 + class 笔(object): """笔 — 纯静态方法容器,提供笔划分算法的所有函数。 @@ -3278,7 +3295,7 @@ class 笔(object): return False @classmethod - def 获取所有停顿位置(cls, 筆: 虚线, 观察员: "观察者"): + def 获取所有停顿位置(cls, 筆: 虚线, 观察员: "观察者") -> List[虚线]: """获取笔内所有可能的停顿位置(用于背驰检测) :param 筆: 笔 @@ -3304,7 +3321,7 @@ class 笔(object): return 笔序列 @classmethod - def 是否背驰过(cls, 当前筆: 虚线, 观察员: "观察者"): + def 是否背驰过(cls, 当前筆: 虚线, 观察员: "观察者") -> List[缠论K线]: """判断笔内是否发生过MACD趋向背驰 :param 当前筆: 笔 @@ -4417,14 +4434,13 @@ class 线段(object): @classmethod @lru_cache(maxsize=128) - def 判断线段内部是否背驰(cls, 当前段: 虚线, 观察员: 观察者): + def 判断线段内部是否背驰(cls, 当前段: 虚线, 观察员: 观察者) -> bool: """判断线段内部是否发生背驰(基于内部中枢和MACD) :param 当前段: 线段 :param 观察员: 观察者 :return: bool """ - 虚, 实, 合 = 线段.获取内部中枢序列(当前段, 观察员.配置) 阳, 阴, _, _ = 线段.分割序列(当前段) if len(阴) > 0: """ @@ -4488,8 +4504,8 @@ class 线段(object): return 背驰 or 盘整背驰 @classmethod - def 段获取所有停顿位置(cls, 段: 虚线, 观察员: "观察者"): - """段获取所有停顿位置 + def 获取所有停顿位置(cls, 段: 虚线, 观察员: "观察者") -> List[虚线]: + """获取所有停顿位置 :param 段: 线段 :param 观察员: 观察者 @@ -4507,14 +4523,14 @@ class 线段(object): 当前停顿 = None for 筆 in 阳: - if len(笔序列) >= 2: + if len(笔序列) >= 3: 筆停顿 = 笔.获取所有停顿位置(筆, 观察员) 筆停顿.append(筆) for 停顿 in 筆停顿: 笔序列.append(停顿) 线段.分析(笔序列, 线段序列, 观察员.配置, 关系序列=[相对方向.向下, 相对方向.向上, 相对方向.顺, 相对方向.逆, 相对方向.同]) - if 线段序列 and 线段序列[-1].武 is not 当前停顿: - 新段 = 虚线.创建线段(线段序列[-1][:]) + if 线段序列 and 线段序列[-1].武 is not 当前停顿 and len(线段序列[-1].基础序列) % 2 == 1: + 新段 = 虚线.创建线段(线段序列[-1].基础序列[:]) 新段.序号 = self.序号 线段.刷新(新段, 观察员.配置) if 新段.方向 is self.方向: @@ -4528,19 +4544,19 @@ class 线段(object): return 结果 @classmethod - def 是否背驰过(cls, 当前段: 虚线, 观察员: "观察者"): + def 是否背驰过(cls, 当前段: 虚线, 观察员: "观察者") -> List[缠论K线]: """判断线段内是否发生过背驰(遍历所有停顿位置) :param 当前段: 线段 :param 观察员: 观察者 :return: 背驰点列表 """ - 停顿位置 = cls.段获取所有停顿位置(当前段, 观察员) + 停顿位置 = 线段.获取所有停顿位置(当前段, 观察员) 结果 = [] for 段 in 停顿位置: - 段.获取内部中枢序列(观察员.配置) - if cls.判断线段内部是否背驰(段, 观察员): + 线段.获取内部中枢序列(段, 观察员.配置) + if 线段.判断线段内部是否背驰(段, 观察员): 结果.append(段.武.中) return 结果 @@ -4666,7 +4682,7 @@ class 中枢(object): """获取用于保存的数据文本""" return f"{self.标识}, {self.序号}, {self.级别}, 文:({int(self.文.时间戳.timestamp())},{self.文.分型特征值:g}), 武:({int(self.武.时间戳.timestamp())},{self.武.分型特征值:g}), {self.第三买卖线}, {self.本级_第三买卖线}" - def 完整性(self, 虚实: str = "合"): + def 完整性(self, 虚实: str): """判断中枢是否完整(是否有第三买卖点或内部中枢离开) 详情见 教你炒股票 43:有关背驰的补习课(2007-04-06 15:31:28) @@ -4709,7 +4725,7 @@ class 中枢(object): 线段.扩展分析(self.基础序列, 扩展线段, 配置) 中枢.分析(扩展线段, 扩展中枢, False, f"{self.标识}_扩展中枢_") - def 校验合法性(self, 序列: Sequence[虚线], 中枢序列) -> bool: + def 校验合法性(self, 序列: Sequence[虚线], 中枢序列: List["中枢"]) -> bool: """校验当前中枢在给定序列中是否仍然合法 :param 序列: 基础虚线序列 @@ -4796,7 +4812,7 @@ class 中枢(object): :return: "中枢之中" / "中枢之上" / "中枢之下" """ 状态 = "中枢之中" - 尾部 = self.基础序列[-1].武 if self.基础序列[-1].标识 == "笔" else self.基础序列[-1].基础序列[-1].武 + 尾部 = 虚线.获取_武(self.基础序列[-1]) 关系 = 相对方向.分析(self.高, self.低, 尾部.中.高, 尾部.中.低) if 关系 is 相对方向.向上缺口: 状态 = "中枢之上" @@ -5184,6 +5200,15 @@ class 观察者: self.配置.分析线段 and 线段.分析(self.线段序列, self.线段_线段序列, self.配置) self.配置.分析线段中枢 and 中枢.分析(self.线段_线段序列, self.线段_中枢序列) + def 加载本地数据(self, 文件路径: str): + self.重置基础序列() + with open(文件路径, "rb") as f: + buffer = f.read() + size = struct.calcsize(">6d") + for i in range(len(buffer) // size): + k线 = K线.读取大端字节数组(buffer[i * size : i * size + size], self.周期, self.标识) + self.增加原始K线(k线) + @classmethod def 读取数据文件(cls, 文件路径: str, 配置=缠论配置()) -> Self: """ @@ -5199,7 +5224,7 @@ class 观察者: buffer = f.read() size = struct.calcsize(">6d") for i in range(len(buffer) // size): - k线 = K线.读取大端字节数组(buffer[i * size : i * size + size], int(周期)) + k线 = K线.读取大端字节数组(buffer[i * size : i * size + size], int(周期), 符号) 实例.增加原始K线(k线) return 实例 @@ -5492,7 +5517,7 @@ def 测试_周期合成(配置: 缠论配置, 配置组: Dict[int, 缠论配置] buffer = f.read() size = struct.calcsize(">6d") for i in range(len(buffer) // size): - k线 = K线.读取大端字节数组(buffer[i * size : i * size + size], 周期) + k线 = K线.读取大端字节数组(buffer[i * size : i * size + size], 周期, 符号) 多级别分析.投喂K线(k线) 消耗用时 = datetime.now() - 启动时间 print("测试_周期合成", 消耗用时, "普K数量", len(多级别分析._单体分析器[周期].普通K线序列)) diff --git a/chanlun-py/Cargo.toml b/chanlun-py/Cargo.toml index 140637e..d422173 100644 --- a/chanlun-py/Cargo.toml +++ b/chanlun-py/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "chanlun-py" -version = "26.5.11" +version = "26.5.46" edition = "2021" description = "缠论技术分析库 — Rust 高性能 Python 绑定" authors = ["YuYuKunKun"] @@ -12,7 +12,7 @@ crate-type = ["cdylib"] name = "chanlun" [dependencies] -chanlun = "26.5.1" # { path = "../chanlun" } +chanlun = "26.5.2" # { path = "../chanlun" } pyo3 = { version = "0.28", features = ["extension-module"] } serde_json = "1" chrono = "0.4" diff --git a/chanlun-py/pyproject.toml b/chanlun-py/pyproject.toml index de0a47c..1324522 100644 --- a/chanlun-py/pyproject.toml +++ b/chanlun-py/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "chanlun" -version = "2605.11" +version = "2605.46" description = "缠论技术分析库 — Rust 高性能实现" readme = { file = "README.md", content-type = "text/markdown" } license = { file = "LICENSE", content-type = "text/plain" } diff --git a/chanlun-py/src/algorithm_py.rs b/chanlun-py/src/algorithm_py.rs index eb7e71f..589b0f1 100644 --- a/chanlun-py/src/algorithm_py.rs +++ b/chanlun-py/src/algorithm_py.rs @@ -28,8 +28,8 @@ use std::rc::Rc; use crate::business_py::观察者Py; use crate::config_py::缠论配置Py; -use crate::kline_py::K线Py; -use crate::structure_py::{分型Py, 虚线Py}; +use crate::kline_py::{缠论K线Py, K线Py}; +use crate::structure_py::{分型Py, 线段特征Py, 虚线Py}; use crate::types_py::相对方向Py; // ========== 背驰分析 ========== @@ -61,7 +61,7 @@ impl 背驰分析Py { ) -> bool { let rc_list: Vec> = K线序列 .iter() - .map(|k| Rc::new(k.bind(py).borrow().inner.clone())) + .map(|k| k.bind(py).borrow().inner.clone()) .collect(); chanlun::algorithm::divergence::背驰分析::MACD背驰( &进入段.borrow().inner, @@ -105,7 +105,7 @@ impl 背驰分析Py { ) -> bool { let rc_list: Vec> = 普K序列 .iter() - .map(|k| Rc::new(k.bind(py).borrow().inner.clone())) + .map(|k| k.bind(py).borrow().inner.clone()) .collect(); chanlun::algorithm::divergence::背驰分析::全量背驰( &进入段.borrow().inner, @@ -124,7 +124,7 @@ impl 背驰分析Py { ) -> bool { let rc_list: Vec> = 普K序列 .iter() - .map(|k| Rc::new(k.bind(py).borrow().inner.clone())) + .map(|k| k.bind(py).borrow().inner.clone()) .collect(); chanlun::algorithm::divergence::背驰分析::任意背驰( &进入段.borrow().inner, @@ -144,7 +144,7 @@ impl 背驰分析Py { ) -> PyResult { let rc_list: Vec> = 普K序列 .iter() - .map(|k| Rc::new(k.bind(py).borrow().inner.clone())) + .map(|k| k.bind(py).borrow().inner.clone()) .collect(); let config = 配置.borrow().to_rust_config(py)?; Ok(chanlun::algorithm::divergence::背驰分析::配置背驰( @@ -165,7 +165,7 @@ impl 背驰分析Py { ) -> bool { let rc_list: Vec> = 普K序列 .iter() - .map(|k| Rc::new(k.bind(py).borrow().inner.clone())) + .map(|k| k.bind(py).borrow().inner.clone()) .collect(); chanlun::algorithm::divergence::背驰分析::任选背驰( &进入段.borrow().inner, @@ -186,7 +186,7 @@ impl 背驰分析Py { ) -> PyResult { let rc_list: Vec> = 普K序列 .iter() - .map(|k| Rc::new(k.bind(py).borrow().inner.clone())) + .map(|k| k.bind(py).borrow().inner.clone()) .collect(); let config = 配置.borrow().to_rust_config(py)?; Ok(chanlun::algorithm::divergence::背驰分析::背驰模式( @@ -251,7 +251,7 @@ impl 笔Py { .map(|k| Rc::clone(&k.bind(py).borrow().inner)) .collect(); chanlun::algorithm::bi::笔::次高(&ck_list, 取舍) - .map(|inner| crate::kline_py::缠论K线Py { inner }) + .map(|inner| crate::kline_py::缠论K线Py::from_rc(inner)) } #[classmethod] @@ -266,7 +266,7 @@ impl 笔Py { .map(|k| Rc::clone(&k.bind(py).borrow().inner)) .collect(); chanlun::algorithm::bi::笔::次低(&ck_list, 取舍) - .map(|inner| crate::kline_py::缠论K线Py { inner }) + .map(|inner| crate::kline_py::缠论K线Py::from_rc(inner)) } #[classmethod] @@ -281,7 +281,7 @@ impl 笔Py { .map(|k| Rc::clone(&k.bind(py).borrow().inner)) .collect(); chanlun::algorithm::bi::笔::实际高点(&ck_list, 取舍) - .map(|inner| crate::kline_py::缠论K线Py { inner }) + .map(|inner| crate::kline_py::缠论K线Py::from_rc(inner)) } #[classmethod] @@ -296,7 +296,7 @@ impl 笔Py { .map(|k| Rc::clone(&k.bind(py).borrow().inner)) .collect(); chanlun::algorithm::bi::笔::实际低点(&ck_list, 取舍) - .map(|inner| crate::kline_py::缠论K线Py { inner }) + .map(|inner| crate::kline_py::缠论K线Py::from_rc(inner)) } #[classmethod] @@ -361,16 +361,20 @@ impl 笔Py { } #[classmethod] + #[pyo3(signature = (当前分型, 分型序列, 笔序列, 缠K序列, 普K序列, 递归层次, 配置))] fn 分析( _cls: &Bound<'_, PyType>, - 当前分型: &Bound<'_, 分型Py>, + 当前分型: Option<&Bound<'_, 分型Py>>, 分型序列: Vec>, 笔序列: Vec>, 缠K序列: Vec>, 普K序列: Vec>, + 递归层次: i64, 配置: &Bound<'_, 缠论配置Py>, py: Python<'_>, ) -> PyResult { + let _ = 递归层次; // Python API 兼容参数,核心从0开始计数 + let 当前分型_rc = 当前分型.map(|f| Rc::clone(&f.borrow().inner)); let mut fr_seq: Vec> = 分型序列 .iter() .map(|f| Rc::clone(&f.bind(py).borrow().inner)) @@ -385,17 +389,20 @@ impl 笔Py { .collect(); let bar_list: Vec> = 普K序列 .iter() - .map(|k| Rc::new(k.bind(py).borrow().inner.clone())) + .map(|k| k.bind(py).borrow().inner.clone()) .collect(); let config = 配置.borrow().to_rust_config(py)?; - Ok(chanlun::algorithm::bi::笔::分析( - Rc::clone(&当前分型.borrow().inner), - &mut fr_seq, - &mut bi_seq, - &ck_list, - &bar_list, - &config, - )) + match 当前分型_rc { + Some(fr) => Ok(chanlun::algorithm::bi::笔::分析( + fr, + &mut fr_seq, + &mut bi_seq, + &ck_list, + &bar_list, + &config, + )), + None => Ok(递归层次), + } } #[classmethod] @@ -424,7 +431,7 @@ impl 笔Py { .collect(); let bar_list: Vec> = 普K序列 .iter() - .map(|k| Rc::new(k.bind(py).borrow().inner.clone())) + .map(|k| k.bind(py).borrow().inner.clone()) .collect(); let config = 配置.borrow().to_rust_config(py)?; Ok(chanlun::algorithm::bi::笔::分析递归( @@ -468,12 +475,12 @@ impl 笔Py { _cls: &Bound<'_, PyType>, 当前筆: &Bound<'_, 虚线Py>, 观察员: &Bound<'_, 观察者Py>, - ) -> Vec<分型Py> { + ) -> Vec<缠论K线Py> { let obs = 观察员.borrow(); let obs_ref = obs.obs(); chanlun::algorithm::bi::笔::是否背驰过(&当前筆.borrow().inner, &*obs_ref) .into_iter() - .map(|f| 分型Py { inner: f }) + .map(|ck| 缠论K线Py::from_rc(ck)) .collect() } } @@ -620,10 +627,26 @@ impl 线段Py { 序列: &Bound<'_, PyAny>, 行号: u32, ) -> PyResult<()> { - // 特征序列 contains Option>, skip for now + let seq: Vec>> = if 序列.is_none() { + vec![] + } else if let Ok(list) = 序列.downcast::() { + let mut result = Vec::with_capacity(list.len()); + for item in list.iter() { + if item.is_none() { + result.push(None); + } else { + let feat: PyRef<'_, 线段特征Py> = item.extract()?; + result.push(Some(Rc::clone(&feat.inner))); + } + } + result + } else { + return Err(pyo3::exceptions::PyTypeError::new_err( + "序列 必须是 list 或 None", + )); + }; let mut ref_mut = 段.borrow_mut(); - // Just pass empty vec for now — 线段特征Py not fully integrated - chanlun::algorithm::segment::线段::设置特征序列(&mut ref_mut.inner, vec![], 行号); + chanlun::algorithm::segment::线段::设置特征序列(&mut ref_mut.inner, seq, 行号); Ok(()) } @@ -658,9 +681,17 @@ impl 线段Py { Vec>, Option>, )> { - // 所属中枢 extraction is complex — pass None for now - let (a, b, c, d) = - chanlun::algorithm::segment::线段::分割序列(&段.borrow().inner, None); + let borrowed = 段.borrow(); + let (a, b, c, d) = if let Some(hub_bound) = 所属中枢 { + if let Ok(mut hub_ref) = hub_bound.extract::>() { + let inner_mut = Rc::make_mut(&mut hub_ref.inner); + chanlun::algorithm::segment::线段::分割序列(&borrowed.inner, Some(inner_mut)) + } else { + chanlun::algorithm::segment::线段::分割序列(&borrowed.inner, None) + } + } else { + chanlun::algorithm::segment::线段::分割序列(&borrowed.inner, None) + }; let wrap = |v: Vec>| -> PyResult>> { let mut result = Vec::new(); for x in v { @@ -945,20 +976,17 @@ impl 线段Py { } #[classmethod] - fn 段获取所有停顿位置( + fn 获取所有停顿位置( _cls: &Bound<'_, PyType>, 段: &Bound<'_, 虚线Py>, 观察员: &Bound<'_, 观察者Py>, ) -> Vec<虚线Py> { let obs = 观察员.borrow(); let obs_ref = obs.obs(); - chanlun::algorithm::segment::线段::段获取所有停顿位置( - &段.borrow().inner, - &*obs_ref, - ) - .into_iter() - .map(|d| 虚线Py { inner: Rc::new(d) }) - .collect() + chanlun::algorithm::segment::线段::获取所有停顿位置(&段.borrow().inner, &*obs_ref) + .into_iter() + .map(|d| 虚线Py { inner: Rc::new(d) }) + .collect() } #[classmethod] @@ -966,12 +994,12 @@ impl 线段Py { _cls: &Bound<'_, PyType>, 当前段: &Bound<'_, 虚线Py>, 观察员: &Bound<'_, 观察者Py>, - ) -> Vec<分型Py> { + ) -> Vec<缠论K线Py> { let obs = 观察员.borrow(); let obs_ref = obs.obs(); chanlun::algorithm::segment::线段::是否背驰过(&当前段.borrow().inner, &*obs_ref) .into_iter() - .map(|f| 分型Py { inner: f }) + .map(|ck| 缠论K线Py::from_rc(ck)) .collect() } } @@ -1153,7 +1181,14 @@ impl 中枢Py { self.inner.获取数据文本() } - fn 校验合法性(&mut self, 序列: Vec>, py: Python<'_>) -> bool { + #[pyo3(signature = (序列, 中枢序列 = None))] + fn 校验合法性( + &mut self, + 序列: Vec>, + 中枢序列: Option>>, + py: Python<'_>, + ) -> bool { + let _ = 中枢序列; // Python 版声明了此参数但未使用 let rc_list: Vec> = 序列 .iter() .map(|d| Rc::clone(&d.bind(py).borrow().inner)) @@ -1167,18 +1202,21 @@ impl 中枢Py { self.inner.完整性(虚实) } - fn 获取扩展中枢(&self, 扩展中枢: Vec>, py: Python<'_>) -> PyResult<()> { + fn 获取扩展中枢( + &self, + 扩展中枢: Vec>, + 配置: &Bound<'_, crate::config_py::缠论配置Py>, + py: Python<'_>, + ) -> PyResult<()> { let mut hub_seq: Vec> = 扩展中枢 .iter() .map(|h| Rc::clone(&h.bind(py).borrow().inner)) .collect(); - // Need config for this call — use default - let config = chanlun::config::缠论配置::default(); + let config = 配置.borrow().to_rust_config(配置.py())?; self.inner.获取扩展中枢(&mut hub_seq, &config); Ok(()) } - #[getter] fn 当前状态(&self) -> String { self.inner.当前状态().to_string() } @@ -1191,6 +1229,17 @@ impl 中枢Py { self.__str__() } + fn __eq__(&self, other: &Bound<'_, PyAny>) -> bool { + if let Ok(other) = other.extract::>() { + return Rc::as_ptr(&self.inner) == Rc::as_ptr(&other.inner); + } + false + } + + fn __hash__(&self) -> u64 { + Rc::as_ptr(&self.inner) as u64 + } + // ---- classmethods ---- #[classmethod] diff --git a/chanlun-py/src/business_py.rs b/chanlun-py/src/business_py.rs index 6c24cd7..1ab4806 100644 --- a/chanlun-py/src/business_py.rs +++ b/chanlun-py/src/business_py.rs @@ -63,7 +63,7 @@ impl 基础买卖点Py { Self { inner: chanlun::business::bsp::基础买卖点::new( 类型.borrow().inner, - Rc::new(当前K线.borrow().inner.clone()), + 当前K线.borrow().inner.clone(), Rc::clone(&买卖点分型.borrow().inner), 备注, 中枢破位值, @@ -76,6 +76,12 @@ impl 基础买卖点Py { self.inner.备注.clone() } + #[setter] + #[pyo3(name = "备注")] + fn 设置_备注(&mut self, value: String) { + self.inner.备注 = value; + } + #[getter] fn 类型(&self) -> 买卖点类型Py { 买卖点类型Py { @@ -92,29 +98,27 @@ impl 基础买卖点Py { #[getter] fn 买卖点K线(&self) -> 缠论K线Py { - 缠论K线Py { - inner: Rc::clone(&self.inner.买卖点K线), - } + 缠论K线Py::from_rc(Rc::clone(&self.inner.买卖点K线)) } #[getter] fn 当前K线(&self) -> K线Py { K线Py { - inner: (*self.inner.当前K线).clone(), + inner: self.inner.当前K线.clone(), } } #[getter] fn 失效K线(&self) -> Option { self.inner.失效K线.as_ref().map(|k| K线Py { - inner: (**k).clone(), + inner: Rc::clone(k), }) } #[getter] fn 终结K线(&self) -> Option { self.inner.终结K线.as_ref().map(|k| K线Py { - inner: (**k).clone(), + inner: Rc::clone(k), }) } @@ -130,10 +134,12 @@ impl 基础买卖点Py { .map(|f| crate::types_py::分型结构Py { inner: f }) } + #[getter] fn 偏移(&self) -> i64 { self.inner.偏移() } + #[getter] fn 失效偏移(&self) -> i64 { self.inner.失效偏移() } @@ -187,7 +193,7 @@ impl 买卖点Py { 基础买卖点Py { inner: chanlun::business::bsp::买卖点::一卖点( Rc::clone(&买卖点分型.borrow().inner), - Rc::new(当前K线.borrow().inner.clone()), + 当前K线.borrow().inner.clone(), 标识, 备注, 中枢破位值, @@ -207,7 +213,7 @@ impl 买卖点Py { 基础买卖点Py { inner: chanlun::business::bsp::买卖点::一买点( Rc::clone(&买卖点分型.borrow().inner), - Rc::new(当前K线.borrow().inner.clone()), + 当前K线.borrow().inner.clone(), 标识, 备注, 中枢破位值, @@ -227,7 +233,7 @@ impl 买卖点Py { 基础买卖点Py { inner: chanlun::business::bsp::买卖点::二卖点( Rc::clone(&买卖点分型.borrow().inner), - Rc::new(当前K线.borrow().inner.clone()), + 当前K线.borrow().inner.clone(), 标识, 备注, 中枢破位值, @@ -247,7 +253,7 @@ impl 买卖点Py { 基础买卖点Py { inner: chanlun::business::bsp::买卖点::二买点( Rc::clone(&买卖点分型.borrow().inner), - Rc::new(当前K线.borrow().inner.clone()), + 当前K线.borrow().inner.clone(), 标识, 备注, 中枢破位值, @@ -267,7 +273,7 @@ impl 买卖点Py { 基础买卖点Py { inner: chanlun::business::bsp::买卖点::三卖点( Rc::clone(&买卖点分型.borrow().inner), - Rc::new(当前K线.borrow().inner.clone()), + 当前K线.borrow().inner.clone(), 标识, 备注, 中枢破位值, @@ -287,7 +293,7 @@ impl 买卖点Py { 基础买卖点Py { inner: chanlun::business::bsp::买卖点::三买点( Rc::clone(&买卖点分型.borrow().inner), - Rc::new(当前K线.borrow().inner.clone()), + 当前K线.borrow().inner.clone(), 标识, 备注, 中枢破位值, @@ -370,33 +376,68 @@ impl 观察者Py { #[pymethods] impl 观察者Py { - /// __new__ 只分配空壳,构造逻辑在 __init__ + /// __new__ 从 *args/**kwargs 中提取 (符号, 周期, 配置),完整初始化观察者。 + /// 由于形参可变(子类参数各异),不依赖固定签名,而是从 args/kwargs 中按位置和名称智能提取。 #[new] #[pyo3(signature = (*args, **kwargs))] fn new( args: &Bound<'_, pyo3::types::PyTuple>, kwargs: Option<&Bound<'_, pyo3::types::PyDict>>, - ) -> Self { - let _ = (args, kwargs); - Self { inner: None } + ) -> PyResult { + let py = args.py(); + + // 提取 符号(位置 0 或关键字) + let 符号: String = if !args.is_empty() { + args.get_item(0)?.extract()? + } else { + match kwargs.and_then(|kw| kw.get_item("符号").ok().flatten()) { + Some(val) => val.extract()?, + None => return Err(pyo3::exceptions::PyTypeError::new_err("缺少参数: 符号")), + } + }; + + // 提取 周期(位置 1 或关键字) + let 周期: i64 = if args.len() >= 2 { + args.get_item(1)?.extract()? + } else { + match kwargs.and_then(|kw| kw.get_item("周期").ok().flatten()) { + Some(val) => val.extract()?, + None => return Err(pyo3::exceptions::PyTypeError::new_err("缺少参数: 周期")), + } + }; + + // 提取 配置(关键字优先,然后扫描剩余位置参数) + let mut config = None; + if let Some(val) = kwargs.and_then(|kw| kw.get_item("配置").ok().flatten()) { + let cfg: PyRef<'_, 缠论配置Py> = val.extract()?; + config = Some(cfg.to_rust_config(py)?); + } + if config.is_none() { + for i in 2..args.len() { + if let Ok(cfg) = args.get_item(i)?.extract::>() { + config = Some(cfg.to_rust_config(py)?); + break; + } + } + } + let config = config.unwrap_or_else(chanlun::config::缠论配置::default); + + Ok(Self { + inner: Some(chanlun::business::observer::观察者::new( + 符号, 周期, config, + )), + }) } - /// __init__ 执行真正的构造。子类可重写并调用 super().__init__(符号, 周期, 配置) - #[pyo3(signature = (符号, 周期, 配置 = None))] + /// __init__ 不重复构造 — __new__ 已完成初始化。 + /// 接收 *args/**kwargs 是为了兼容 Python 构造函数给 __init__ 传入的任意形参。 + #[pyo3(signature = (*args, **kwargs))] fn __init__( &mut self, - py: Python<'_>, - 符号: String, - 周期: i64, - 配置: Option<&Bound<'_, 缠论配置Py>>, + args: &Bound<'_, pyo3::types::PyTuple>, + kwargs: Option<&Bound<'_, pyo3::types::PyDict>>, ) -> PyResult<()> { - let config = match 配置 { - Some(cfg) => cfg.borrow().to_rust_config(py)?, - None => chanlun::config::缠论配置::default(), - }; - self.inner = Some(chanlun::business::observer::观察者::new( - 符号, 周期, config, - )); + let _ = (args, kwargs); Ok(()) } @@ -413,15 +454,15 @@ impl 观察者Py { #[getter] fn 当前K线(&self) -> Option { self.obs().当前K线().map(|k| K线Py { - inner: (**k).clone(), + inner: Rc::clone(k), }) } #[getter] fn 当前缠K(&self) -> Option<缠论K线Py> { - self.obs().当前缠K().map(|k| 缠论K线Py { - inner: Rc::clone(k), - }) + self.obs() + .当前缠K() + .map(|k| 缠论K线Py::from_rc(Rc::clone(k))) } #[getter] @@ -444,7 +485,35 @@ impl 观察者Py { } fn 增加原始K线(&mut self, 普K: &Bound<'_, K线Py>) { - self.obs_mut().增加原始K线(普K.borrow().inner.clone()); + self.obs_mut().增加原始K线((*普K.borrow().inner).clone()); + } + + /// 加载本地数据 — 从 .nb 文件加载K线数据(先重置,再通过 Python dispatch 逐根投喂, + /// 确保子类重写的 增加原始K线 被正确调用)。 + fn 加载本地数据(slf: &Bound<'_, Self>, 文件路径: &str) -> PyResult<()> { + let py = slf.py(); + + // 重置基础序列 + slf.borrow_mut().obs_mut().重置基础序列(); + + // 解析文件得到 K线 列表 + let bars = slf + .borrow() + .obs() + .解析本地数据(文件路径) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e))?; + + // 通过 Python dispatch 逐根投喂,确保子类重写生效 + for k线 in bars { + let k线_py = Py::new( + py, + K线Py { + inner: Rc::new(k线), + }, + )?; + slf.call_method1("增加原始K线", (k线_py,))?; + } + Ok(()) } fn 静态重新分析(&mut self) { @@ -456,19 +525,61 @@ impl 观察者Py { } #[classmethod] + #[pyo3(signature = (文件路径, 配置 = None))] fn 读取数据文件( - _cls: &Bound<'_, PyType>, + cls: &Bound<'_, PyType>, 文件路径: &str, 配置: Option<&Bound<'_, 缠论配置Py>>, py: Python<'_>, - ) -> PyResult { + ) -> PyResult> { let config = match 配置 { - Some(cfg) => Some(cfg.borrow().to_rust_config(py)?), - None => None, + Some(cfg) => cfg.borrow().to_rust_config(py)?, + None => chanlun::config::缠论配置::default(), }; - chanlun::business::observer::观察者::读取数据文件(文件路径, config) - .map(|inner| Self { inner: Some(inner) }) - .map_err(|e| pyo3::exceptions::PyValueError::new_err(e)) + + // 从文件名解析 符号/周期: "btcusd-300-1761327300-1776327900.nb" + let path = std::path::Path::new(文件路径); + let name = path + .file_stem() + .and_then(|n| n.to_str()) + .ok_or_else(|| pyo3::exceptions::PyValueError::new_err("invalid filename"))?; + let parts: Vec<&str> = name.split('-').collect(); + if parts.len() < 4 { + return Err(pyo3::exceptions::PyValueError::new_err(format!( + "invalid filename format: {}", + name + ))); + } + let 符号 = parts[0].to_string(); + let 周期: i64 = parts[1] + .parse() + .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("parse period: {}", e)))?; + + // 通过 cls 构造实例(支持子类化) + let cfg_py = 缠论配置Py::from_rust_config(&config)?; + let cfg_obj = Py::new(py, cfg_py)?; + let obj = cls.call1((符号.clone(), 周期, cfg_obj))?; + + // 读取文件并通过 Python 分发逐根投喂(支持子类重写 增加原始K线) + let data = std::fs::read(文件路径) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("read file: {}", e)))?; + let size: usize = 48; + for i in 0..data.len() / size { + let offset = i * size; + if let Some(k线) = + chanlun::kline::bar::K线::from_bytes(&data[offset..offset + size], 周期, &符号) + { + let k线_py = Py::new( + py, + K线Py { + inner: Rc::new(k线), + }, + )?; + obj.call_method1("增加原始K线", (k线_py,))?; + } + } + + Ok(obj.unbind()) } // ---- 序列 getters ---- @@ -478,7 +589,7 @@ impl 观察者Py { let list = pyo3::types::PyList::empty(py); for k in &self.obs().普通K线序列 { list.append(K线Py { - inner: (**k).clone(), + inner: Rc::clone(k), })?; } Ok(list.into()) @@ -488,9 +599,7 @@ impl 观察者Py { fn 缠论K线序列(&self, py: Python<'_>) -> PyResult> { let list = pyo3::types::PyList::empty(py); for k in &self.obs().缠论K线序列 { - list.append(缠论K线Py { - inner: Rc::clone(k), - })?; + list.append(缠论K线Py::from_rc(Rc::clone(k)))?; } Ok(list.into()) } @@ -670,10 +779,10 @@ impl K线合成器Py { 普K: &Bound<'_, K线Py>, py: Python<'_>, ) -> PyResult> { - let results = self.inner.投喂K线(普K.borrow().inner.clone()); + let results = self.inner.投喂K线((*普K.borrow().inner).clone()); Ok(results .into_iter() - .map(|(周期, k)| (周期, K线Py { inner: k })) + .map(|(周期, k)| (周期, K线Py { inner: Rc::new(k) })) .collect()) } @@ -701,14 +810,14 @@ impl K线合成器Py { let results = self.inner.投喂K线(k); results .into_iter() - .map(|(周期, k2)| (周期, K线Py { inner: k2 })) + .map(|(周期, k2)| (周期, K线Py { inner: Rc::new(k2) })) .collect() } fn 获取当前K线(&self, 周期: i64) -> Option { - self.inner - .获取当前K线(周期) - .map(|k| K线Py { inner: k.clone() }) + self.inner.获取当前K线(周期).map(|k| K线Py { + inner: Rc::new(k.clone()), + }) } #[getter] @@ -776,7 +885,7 @@ impl 立体分析器Py { } fn 投喂K线(&mut self, 普K: &Bound<'_, K线Py>) { - self.inner.投喂K线(普K.borrow().inner.clone()); + self.inner.投喂K线((*普K.borrow().inner).clone()); } fn 获取观察者(&self, 周期: i64) -> Option<观察者Py> { diff --git a/chanlun-py/src/config_py.rs b/chanlun-py/src/config_py.rs index c475575..6a96363 100644 --- a/chanlun-py/src/config_py.rs +++ b/chanlun-py/src/config_py.rs @@ -107,16 +107,18 @@ impl 缠论配置Py { if let Some(kwargs) = kwargs { for (key, value) in kwargs.iter() { let key: String = key.extract()?; - if fields.contains_key(&key) { - fields.insert(key, value.clone().unbind()); - } else { + if !fields.contains_key(&key) { return Err(pyo3::exceptions::PyAttributeError::new_err(format!( "缠论配置 没有字段: {key}" ))); } + fields.insert(key, value.clone().unbind()); } } + // 全部通过 serde_json 往返验证类型,统一处理字符串数字/布尔强制转换 + let config = dict_to_rust_config(&fields)?; + let fields = config_to_field_dict(&config)?; Ok(Self { fields }) } @@ -132,7 +134,16 @@ impl 缠论配置Py { fn __setattr__(&mut self, name: &str, value: &Bound<'_, PyAny>) -> PyResult<()> { if self.fields.contains_key(name) { self.fields.insert(name.to_string(), value.clone().unbind()); - Ok(()) + // 通过 serde 往返验证类型 + match dict_to_rust_config(&self.fields) { + Ok(config) => { + self.fields = config_to_field_dict(&config)?; + Ok(()) + } + Err(e) => Err(pyo3::exceptions::PyValueError::new_err(format!( + "配置转换失败: {e}" + ))), + } } else { Err(pyo3::exceptions::PyAttributeError::new_err(format!( "缠论配置 没有字段: {name}" @@ -198,10 +209,13 @@ impl 缠论配置Py { for (key, value) in data.iter() { let key: String = key.extract()?; - fields.insert(key, value.clone().unbind()); + if fields.contains_key(&key) { + fields.insert(key, value.clone().unbind()); + } } - dict_to_rust_config(&fields)?; + let config = dict_to_rust_config(&fields)?; + let fields = config_to_field_dict(&config)?; Ok(Self { fields }) } @@ -270,7 +284,8 @@ impl 缠论配置Py { fields.insert(key, value.clone().unbind()); } - dict_to_rust_config(&fields)?; + let config = dict_to_rust_config(&fields)?; + let fields = config_to_field_dict(&config)?; Ok(Self { fields }) } @@ -313,11 +328,84 @@ fn dict_to_rust_config( } let dumps = json_mod.getattr("dumps")?; let json_str: String = dumps.call1((dict,))?.extract()?; - serde_json::from_str(&json_str) + let mut value: serde_json::Value = serde_json::from_str(&json_str) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("配置转换失败: {e}")))?; + coerce_strings_to_numbers(&mut value); + serde_json::from_value(value) .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("配置转换失败: {e}"))) }) } +/// 递归遍历 JSON Value,将数字/布尔字符串转为对应类型。 +fn coerce_strings_to_numbers(value: &mut serde_json::Value) { + match value { + serde_json::Value::Object(map) => { + for (_, v) in map.iter_mut() { + coerce_strings_to_numbers(v); + } + } + serde_json::Value::Array(arr) => { + for v in arr.iter_mut() { + coerce_strings_to_numbers(v); + } + } + serde_json::Value::String(s) => { + // 先 clone 出独立副本,避免借用冲突 + let cloned = s.clone(); + if let Ok(n) = cloned.parse::() { + *value = serde_json::Value::Number(serde_json::Number::from(n)); + } else if let Ok(n) = cloned.parse::() { + if n.is_finite() { + if let Some(num) = serde_json::Number::from_f64(n) { + *value = serde_json::Value::Number(num); + } + } + } else if cloned.eq_ignore_ascii_case("true") { + *value = serde_json::Value::Bool(true); + } else if cloned.eq_ignore_ascii_case("false") { + *value = serde_json::Value::Bool(false); + } + // 非数字非布尔的原样保留,不做任何修改 + } + _ => {} + } +} + +/// 将 Python 字符串数字/布尔值强制转为对应类型,非字符串保持不变。 +/// 注:主代码路径现通过 serde 往返处理类型转换,此函数作为辅助保留。 +#[allow(dead_code)] +fn coerce_py_value(value: &Bound<'_, PyAny>) -> PyResult> { + let py = value.py(); + + let type_name: String = value.get_type().name()?.extract()?; + if type_name != "str" { + return Ok(value.clone().unbind()); + } + + let lower_obj = value.call_method0("lower")?; + let lower: String = lower_obj.extract()?; + + if lower == "true" || lower == "false" { + let b = lower == "true"; + let obj = pyo3::types::PyBool::new(py, b) + .to_owned() + .into_any() + .unbind(); + return Ok(obj); + } + + if let Ok(n) = lower.parse::() { + return Ok(n.into_pyobject(py)?.into_any().unbind()); + } + if let Ok(n) = lower.parse::() { + if n.is_finite() { + return Ok(n.into_pyobject(py)?.into_any().unbind()); + } + } + + Ok(value.clone().unbind()) +} + pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::<缠论配置Py>()?; Ok(()) diff --git a/chanlun-py/src/kline_py.rs b/chanlun-py/src/kline_py.rs index 82f755a..f759111 100644 --- a/chanlun-py/src/kline_py.rs +++ b/chanlun-py/src/kline_py.rs @@ -25,6 +25,7 @@ use pyo3::prelude::*; use pyo3::types::{PyBytes, PyType}; use std::collections::HashMap; +use std::rc::Rc; use crate::config_py::缠论配置Py; use crate::indicators_py::{平滑异同移动平均线Py, 相对强弱指数Py, 随机指标Py}; @@ -51,10 +52,9 @@ use crate::types_py::相对方向Py; /// 获取MACD(K线序列, 计算方式, 快线周期?, 慢线周期?, 信号周期?) -> list[平滑异同移动平均线] /// — 对整个K线序列批量计算 MACD /// 截取(序列, 起点K线, 终点K线) -> list — 按时间戳截取K线区间 -#[pyclass(name = "K线")] -#[derive(Clone)] +#[pyclass(name = "K线", unsendable)] pub struct K线Py { - pub(crate) inner: chanlun::kline::bar::K线, + pub(crate) inner: Rc, } #[pymethods] @@ -73,7 +73,7 @@ impl K线Py { 成交量: f64, ) -> Self { Self { - inner: chanlun::kline::bar::K线 { + inner: Rc::new(chanlun::kline::bar::K线 { 标识: 标识.to_string(), 序号, 周期, @@ -86,7 +86,7 @@ impl K线Py { macd: None, rsi: None, kdj: None, - }, + }), } } @@ -96,7 +96,7 @@ impl K线Py { } #[setter] fn set_标识(&mut self, v: String) { - self.inner.标识 = v; + Rc::make_mut(&mut self.inner).标识 = v; } #[getter] @@ -105,7 +105,7 @@ impl K线Py { } #[setter] fn set_序号(&mut self, v: i64) { - self.inner.序号 = v; + Rc::make_mut(&mut self.inner).序号 = v; } #[getter] @@ -114,7 +114,7 @@ impl K线Py { } #[setter] fn set_周期(&mut self, v: i64) { - self.inner.周期 = v; + Rc::make_mut(&mut self.inner).周期 = v; } #[getter] @@ -123,7 +123,7 @@ impl K线Py { } #[setter] fn set_时间戳(&mut self, v: i64) { - self.inner.时间戳 = v; + Rc::make_mut(&mut self.inner).时间戳 = v; } #[getter] @@ -132,7 +132,7 @@ impl K线Py { } #[setter] fn set_高(&mut self, v: f64) { - self.inner.高 = v; + Rc::make_mut(&mut self.inner).高 = v; } #[getter] @@ -141,7 +141,7 @@ impl K线Py { } #[setter] fn set_低(&mut self, v: f64) { - self.inner.低 = v; + Rc::make_mut(&mut self.inner).低 = v; } #[getter] @@ -150,7 +150,7 @@ impl K线Py { } #[setter] fn set_开盘价(&mut self, v: f64) { - self.inner.开盘价 = v; + Rc::make_mut(&mut self.inner).开盘价 = v; } #[getter] @@ -159,7 +159,7 @@ impl K线Py { } #[setter] fn set_收盘价(&mut self, v: f64) { - self.inner.收盘价 = v; + Rc::make_mut(&mut self.inner).收盘价 = v; } #[getter] @@ -168,7 +168,7 @@ impl K线Py { } #[setter] fn set_成交量(&mut self, v: f64) { - self.inner.成交量 = v; + Rc::make_mut(&mut self.inner).成交量 = v; } #[getter] @@ -179,7 +179,7 @@ impl K线Py { } #[getter] - fn MACD(&self) -> Option<平滑异同移动平均线Py> { + fn macd(&self) -> Option<平滑异同移动平均线Py> { self.inner .macd .as_ref() @@ -187,7 +187,7 @@ impl K线Py { } #[getter] - fn RSI(&self) -> Option<相对强弱指数Py> { + fn rsi(&self) -> Option<相对强弱指数Py> { self.inner .rsi .as_ref() @@ -195,7 +195,7 @@ impl K线Py { } #[getter] - fn KDJ(&self) -> Option<随机指标Py> { + fn kdj(&self) -> Option<随机指标Py> { self.inner .kdj .as_ref() @@ -214,6 +214,17 @@ impl K线Py { PyBytes::new(py, &self.inner.to_bytes()).into() } + fn __eq__(&self, other: &Bound<'_, PyAny>) -> bool { + if let Ok(other) = other.extract::>() { + return Rc::as_ptr(&self.inner) == Rc::as_ptr(&other.inner); + } + false + } + + fn __hash__(&self) -> u64 { + Rc::as_ptr(&self.inner) as u64 + } + #[classmethod] #[pyo3(signature = (标识, 时间戳, 开盘价, 最高价, 最低价, 收盘价, 成交量, 序号 = None, 周期 = None))] fn 创建普K( @@ -229,7 +240,7 @@ impl K线Py { 周期: Option, ) -> Self { Self { - inner: chanlun::kline::bar::K线::创建普K( + inner: Rc::new(chanlun::kline::bar::K线::创建普K( 标识, 时间戳, 开盘价, @@ -239,7 +250,7 @@ impl K线Py { 成交量, 序号.unwrap_or(0), 周期.unwrap_or(60), - ), + )), } } @@ -251,7 +262,7 @@ impl K线Py { py: Python<'_>, ) -> PyResult<()> { let refs: Vec<_> = K线序列.iter().map(|k| k.bind(py).borrow()).collect(); - let bars: Vec<&chanlun::kline::bar::K线> = refs.iter().map(|r| &r.inner).collect(); + let bars: Vec<&chanlun::kline::bar::K线> = refs.iter().map(|r| r.inner.as_ref()).collect(); chanlun::kline::bar::K线::保存到DAT文件(路径, &bars) .map_err(|e| pyo3::exceptions::PyIOError::new_err(e.to_string())) } @@ -263,8 +274,11 @@ impl K线Py { 周期: i64, 标识: &str, ) -> Option { - chanlun::kline::bar::K线::读取大端字节数组(字节组.as_bytes(), 周期, 标识) - .map(|inner| Self { inner }) + chanlun::kline::bar::K线::读取大端字节数组(字节组.as_bytes(), 周期, 标识).map(|inner| { + Self { + inner: Rc::new(inner), + } + }) } #[classmethod] @@ -276,7 +290,7 @@ impl K线Py { py: Python<'_>, ) -> HashMap { let refs: Vec<_> = k线序列.iter().map(|k| k.bind(py).borrow()).collect(); - let bars: Vec<&chanlun::kline::bar::K线> = refs.iter().map(|r| &r.inner).collect(); + let bars: Vec<&chanlun::kline::bar::K线> = refs.iter().map(|r| r.inner.as_ref()).collect(); chanlun::kline::bar::K线::获取MACD(&bars, &始.borrow().inner, &终.borrow().inner) } @@ -285,22 +299,33 @@ impl K线Py { 序列: Vec>, 始: &Bound<'_, Self>, 终: &Bound<'_, Self>, - ) -> Option>> { - let start_ptr = 始.as_ptr(); - let end_ptr = 终.as_ptr(); - let start_idx = 序列.iter().position(|k| k.as_ptr() == start_ptr)?; - let end_idx = 序列.iter().position(|k| k.as_ptr() == end_ptr)?; - if start_idx <= end_idx { - Some( - 序列 - .into_iter() - .skip(start_idx) - .take(end_idx - start_idx + 1) - .collect(), - ) - } else { - None + py: Python<'_>, + ) -> PyResult>> { + let start_ptr = Rc::as_ptr(&始.borrow().inner); + let end_ptr = Rc::as_ptr(&终.borrow().inner); + let start_ts = 始.borrow().inner.时间戳; + let end_ts = 终.borrow().inner.时间戳; + let start_idx = 序列 + .iter() + .position(|k| { + Rc::as_ptr(&k.borrow(py).inner) == start_ptr + || k.borrow(py).inner.时间戳 == start_ts + }) + .ok_or_else(|| pyo3::exceptions::PyValueError::new_err("始 不在序列中"))?; + let end_idx = 序列 + .iter() + .position(|k| { + Rc::as_ptr(&k.borrow(py).inner) == end_ptr || k.borrow(py).inner.时间戳 == end_ts + }) + .ok_or_else(|| pyo3::exceptions::PyValueError::new_err("终 不在序列中"))?; + if start_idx > end_idx { + return Err(pyo3::exceptions::PyValueError::new_err("始 排序在 终 之后")); } + Ok(序列 + .into_iter() + .skip(start_idx) + .take(end_idx - start_idx + 1) + .collect()) } } @@ -324,9 +349,27 @@ impl K线Py { /// — 分析分型形成结果 /// 截取(序列, 起点分型, 终点分型) -> list — 截取分型间的缠K子序列 #[pyclass(name = "缠论K线", unsendable)] -#[derive(Clone)] pub struct 缠论K线Py { pub(crate) inner: std::rc::Rc, + bsp_set: std::cell::RefCell>>, +} + +impl 缠论K线Py { + pub(crate) fn from_rc(inner: std::rc::Rc) -> Self { + Self { + inner, + bsp_set: std::cell::RefCell::new(None), + } + } +} + +impl Clone for 缠论K线Py { + fn clone(&self) -> Self { + Self { + inner: std::rc::Rc::clone(&self.inner), + bsp_set: std::cell::RefCell::new(None), + } + } } #[pymethods] @@ -398,7 +441,7 @@ impl 缠论K线Py { #[getter] fn 标的K线(&self) -> K线Py { K线Py { - inner: (*self.inner.标的K线).clone(), + inner: self.inner.标的K线.clone(), } } @@ -410,10 +453,32 @@ impl 缠论K线Py { self.__str__() } - fn 镜像(&self) -> Self { - Self { - inner: std::rc::Rc::new(self.inner.镜像()), + fn __eq__(&self, other: &Bound<'_, PyAny>) -> bool { + if let Ok(other) = other.extract::>() { + return Rc::as_ptr(&self.inner) == Rc::as_ptr(&other.inner); } + false + } + + fn __hash__(&self) -> u64 { + Rc::as_ptr(&self.inner) as u64 + } + + #[getter] + fn 镜像(&self, py: Python<'_>) -> Self { + let mut mirror = Self { + inner: std::rc::Rc::new(self.inner.镜像()), + bsp_set: std::cell::RefCell::new(None), + }; + if let Some(ref src_set) = *self.bsp_set.borrow() { + if let Ok(new_set) = pyo3::types::PySet::empty(py) { + for item in src_set.bind(py).iter() { + let _ = new_set.add(item); + } + mirror.bsp_set = std::cell::RefCell::new(Some(new_set.into())); + } + } + mirror } #[getter] @@ -431,6 +496,24 @@ impl 缠论K线Py { self.inner.与KDJ匹配() } + #[getter] + fn 买卖点信息(&self, py: Python<'_>) -> PyResult> { + if self.bsp_set.borrow().is_none() { + let set = pyo3::types::PySet::empty(py)?; + for s in self.inner.买卖点信息.borrow().iter() { + set.add(s.clone())?; + } + *self.bsp_set.borrow_mut() = Some(set.into()); + } + Ok(self + .bsp_set + .borrow() + .as_ref() + .unwrap() + .clone_ref(py) + .into_any()) + } + #[classmethod] fn 时间戳对齐( _cls: &Bound<'_, PyType>, @@ -466,12 +549,10 @@ impl 缠论K线Py { 方向.borrow().inner, 结构.map(|s| s.borrow().inner), 原始序号, - std::rc::Rc::new(普k.borrow().inner.clone()), + 普k.borrow().inner.clone(), prev_inner, ); - Self { - inner: std::rc::Rc::new(inner), - } + Self::from_rc(std::rc::Rc::new(inner)) } #[classmethod] @@ -493,7 +574,7 @@ impl 缠论K线Py { &当前普K.borrow().inner, &config, ); - Ok((result.map(|rc| Self { inner: rc }), mode)) + Ok((result.map(Self::from_rc), mode)) } #[classmethod] @@ -505,7 +586,7 @@ impl 缠论K线Py { 配置: &Bound<'_, 缠论配置Py>, py: Python<'_>, ) -> PyResult<(String, Option>)> { - let ck_inner = 当前K线.borrow().inner.clone(); + let ck_inner = (*当前K线.borrow().inner).clone(); let config = 配置.borrow().to_rust_config(py)?; let mut ck_seq: Vec<_> = 缠K序列 @@ -514,7 +595,7 @@ impl 缠论K线Py { .collect(); let mut bar_seq: Vec<_> = 普K序列 .iter() - .map(|k| std::rc::Rc::new(k.bind(py).borrow().inner.clone())) + .map(|k| k.bind(py).borrow().inner.clone()) .collect(); let (status, _fractal) = chanlun::kline::chan_kline::缠论K线::分析( @@ -532,22 +613,33 @@ impl 缠论K线Py { 序列: Vec>, 始: &Bound<'_, Self>, 终: &Bound<'_, Self>, - ) -> Option>> { - let start_ptr = 始.as_ptr(); - let end_ptr = 终.as_ptr(); - let start_idx = 序列.iter().position(|k| k.as_ptr() == start_ptr)?; - let end_idx = 序列.iter().position(|k| k.as_ptr() == end_ptr)?; - if start_idx <= end_idx { - Some( - 序列 - .into_iter() - .skip(start_idx) - .take(end_idx - start_idx + 1) - .collect(), - ) - } else { - None + py: Python<'_>, + ) -> PyResult>> { + let start_ptr = Rc::as_ptr(&始.borrow().inner); + let end_ptr = Rc::as_ptr(&终.borrow().inner); + let start_ts = 始.borrow().inner.时间戳; + let end_ts = 终.borrow().inner.时间戳; + let start_idx = 序列 + .iter() + .position(|k| { + Rc::as_ptr(&k.borrow(py).inner) == start_ptr + || k.borrow(py).inner.时间戳 == start_ts + }) + .ok_or_else(|| pyo3::exceptions::PyValueError::new_err("始 不在序列中"))?; + let end_idx = 序列 + .iter() + .position(|k| { + Rc::as_ptr(&k.borrow(py).inner) == end_ptr || k.borrow(py).inner.时间戳 == end_ts + }) + .ok_or_else(|| pyo3::exceptions::PyValueError::new_err("终 不在序列中"))?; + if start_idx > end_idx { + return Err(pyo3::exceptions::PyValueError::new_err("始 排序在 终 之后")); } + Ok(序列 + .into_iter() + .skip(start_idx) + .take(end_idx - start_idx + 1) + .collect()) } } diff --git a/chanlun-py/src/lib.rs b/chanlun-py/src/lib.rs index 415bf1d..883344e 100644 --- a/chanlun-py/src/lib.rs +++ b/chanlun-py/src/lib.rs @@ -51,3 +51,43 @@ fn _chanlun(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { business_py::register(m)?; Ok(()) } + +#[cfg(test)] +mod tests { + use crate::*; + use pyo3::prelude::*; + + #[test] + fn test_rc_pointer_across_getters() { + pyo3::prepare_freethreaded_python(); + Python::with_gil(|py| { + let module = PyModule::new(py, "test_module").unwrap(); + module.add_class::().unwrap(); + module.add_class::().unwrap(); + module.add_class::().unwrap(); + module.add_class::().unwrap(); + module.add_class::().unwrap(); + module.add_class::().unwrap(); + module.add_class::().unwrap(); + module.add_class::().unwrap(); + + let config = config_py::缠论配置Py::from_rust_config(&Default::default()).unwrap(); + let obs = business_py::观察者Py::new_impl("btcusd".into(), 300, config, py).unwrap(); + + // Feed one K line + let kline = kline_py::K线Py::new_impl( + "btcusd".into(), + 1000, + 100.0, + 105.0, + 99.0, + 103.0, + 1000.0, + 0, + 300, + ); + let kline_ref = kline.into_ref(py); + // ... this is too complex + }); + } +} diff --git a/chanlun-py/src/structure_py.rs b/chanlun-py/src/structure_py.rs index f7747b9..b7c9d3f 100644 --- a/chanlun-py/src/structure_py.rs +++ b/chanlun-py/src/structure_py.rs @@ -72,23 +72,23 @@ impl 分型Py { #[getter] fn 左(&self) -> Option<缠论K线Py> { - self.inner.左.as_ref().map(|k| 缠论K线Py { - inner: Rc::clone(k), - }) + self.inner + .左 + .as_ref() + .map(|k| 缠论K线Py::from_rc(Rc::clone(k))) } #[getter] fn 中(&self) -> 缠论K线Py { - 缠论K线Py { - inner: Rc::clone(&self.inner.中), - } + 缠论K线Py::from_rc(Rc::clone(&self.inner.中)) } #[getter] fn 右(&self) -> Option<缠论K线Py> { - self.inner.右.as_ref().map(|k| 缠论K线Py { - inner: Rc::clone(k), - }) + self.inner + .右 + .as_ref() + .map(|k| 缠论K线Py::from_rc(Rc::clone(k))) } #[getter] @@ -116,6 +116,17 @@ impl 分型Py { self.__str__() } + fn __eq__(&self, other: &Bound<'_, PyAny>) -> bool { + if let Ok(other) = other.extract::>() { + return Rc::as_ptr(&self.inner) == Rc::as_ptr(&other.inner); + } + false + } + + fn __hash__(&self) -> u64 { + Rc::as_ptr(&self.inner) as u64 + } + #[getter] fn 关系组(&self) -> Option<(相对方向Py, 相对方向Py, 相对方向Py)> { self.inner.关系组().map(|(a, b, c)| { @@ -208,8 +219,8 @@ impl 分型Py { /// 方法: /// 之前是(其他虚线) -> bool — 判断当前虚线是否紧接在另一虚线之前 /// 之后是(其他虚线) -> bool — 判断当前虚线是否紧接在另一虚线之后 -/// 获取普K序列(普K序列) -> list[K线] — 截取该虚线的原始K线范围 -/// 获取缠K序列(缠K序列) -> list[缠论K线] — 截取该虚线的缠K范围 +/// 获取普K序列(观察员) -> list[K线] — 截取该虚线的原始K线范围 +/// 获取缠K序列(观察员) -> list[缠论K线] — 截取该虚线的缠K范围 /// 获取数据文本() -> str /// /// 类方法(算法辅助): @@ -294,6 +305,25 @@ impl 虚线Py { self.inner._特征序列_显示 } + #[getter] + fn 特征序列(&self, py: Python<'_>) -> PyResult> { + let list = pyo3::types::PyList::empty(py); + for item in &self.inner.特征序列 { + match item { + Some(feat) => list.append(Py::new( + py, + 线段特征Py { + inner: Rc::clone(feat), + }, + )?)?, + None => { + list.append(py.None())?; + } + } + } + Ok(list.into()) + } + #[getter] fn 短路修正(&self) -> bool { self.inner.短路修正 @@ -301,9 +331,10 @@ impl 虚线Py { #[getter] fn 确认K线(&self) -> Option<缠论K线Py> { - self.inner.确认K线.as_ref().map(|k| 缠论K线Py { - inner: Rc::clone(k), - }) + self.inner + .确认K线 + .as_ref() + .map(|k| 缠论K线Py::from_rc(Rc::clone(k))) } #[getter] @@ -413,39 +444,43 @@ impl 虚线Py { self.inner.之后是(&之后.borrow().inner) } - fn 获取普K序列(&self, 普K序列: Vec>, py: Python<'_>) -> PyResult> { - let rc_list: Vec> = 普K序列 - .iter() - .map(|k| Rc::new(k.bind(py).borrow().inner.clone())) - .collect(); - let result = self.inner.获取普K序列(&rc_list); - let list = pyo3::types::PyList::empty(py); + fn 获取普K序列( + &self, + 观察员: &Bound<'_, crate::business_py::观察者Py>, + ) -> PyResult> { + let obs_ref = 观察员.borrow(); + let observer_inner = obs_ref.obs(); + let result = self.inner.获取普K序列(&observer_inner.普通K线序列); + let list = pyo3::types::PyList::empty(观察员.py()); for k in &result { list.append(K线Py { - inner: (**k).clone(), - })?; - } - Ok(list.into()) - } - - fn 获取缠K序列( - &self, 缠K序列: Vec>, py: Python<'_> - ) -> PyResult> { - let rc_list: Vec> = 缠K序列 - .iter() - .map(|k| Rc::clone(&k.bind(py).borrow().inner)) - .collect(); - let result = self.inner.获取缠K序列(&rc_list); - let list = pyo3::types::PyList::empty(py); - for k in &result { - list.append(缠论K线Py { inner: Rc::clone(k), })?; } Ok(list.into()) } - #[getter] + fn 获取缠K序列( + &self, + 观察员: &Bound<'_, crate::business_py::观察者Py>, + ) -> PyResult> { + let obs_ref = 观察员.borrow(); + let observer_inner = obs_ref.obs(); + let result = self.inner.获取缠K序列(&observer_inner.缠论K线序列); + let list = pyo3::types::PyList::empty(观察员.py()); + for k in &result { + list.append(缠论K线Py::from_rc(Rc::clone(k)))?; + } + Ok(list.into()) + } + + #[classmethod] + fn 获取_武(_cls: &Bound<'_, PyType>, 实线: &Bound<'_, Self>) -> 分型Py { + 分型Py { + inner: 实线.borrow().inner.获取_武(), + } + } + fn 获取数据文本(&self) -> String { self.inner.获取数据文本() } @@ -458,6 +493,17 @@ impl 虚线Py { self.__str__() } + fn __eq__(&self, other: &Bound<'_, PyAny>) -> bool { + if let Ok(other) = other.extract::>() { + return Rc::as_ptr(&self.inner) == Rc::as_ptr(&other.inner); + } + false + } + + fn __hash__(&self) -> u64 { + Rc::as_ptr(&self.inner) as u64 + } + // ---- 静态工厂方法 ---- #[classmethod] @@ -547,7 +593,7 @@ impl 虚线Py { ) -> f64 { let rc_list: Vec> = 普K序列 .iter() - .map(|k| Rc::new(k.bind(py).borrow().inner.clone())) + .map(|k| k.bind(py).borrow().inner.clone()) .collect(); chanlun::structure::dash_line::虚线::计算MACD柱子均值( &rc_list, @@ -564,7 +610,7 @@ impl 虚线Py { ) -> bool { let rc_list: Vec> = 普K序列 .iter() - .map(|k| Rc::new(k.bind(py).borrow().inner.clone())) + .map(|k| k.bind(py).borrow().inner.clone()) .collect(); chanlun::structure::dash_line::虚线::武之全量MACD均值( &rc_list, @@ -581,7 +627,7 @@ impl 虚线Py { ) -> bool { let rc_list: Vec> = 普K序列 .iter() - .map(|k| Rc::new(k.bind(py).borrow().inner.clone())) + .map(|k| k.bind(py).borrow().inner.clone()) .collect(); chanlun::structure::dash_line::虚线::武之MACD均值(&rc_list, &实线.borrow().inner) } @@ -595,7 +641,7 @@ impl 虚线Py { ) -> bool { let rc_list: Vec> = 普K序列 .iter() - .map(|k| Rc::new(k.bind(py).borrow().inner.clone())) + .map(|k| k.bind(py).borrow().inner.clone()) .collect(); chanlun::structure::dash_line::虚线::武之MACD极值(&rc_list, &实线.borrow().inner) } @@ -609,7 +655,7 @@ impl 虚线Py { ) -> Option { let rc_list: Vec> = 普K序列 .iter() - .map(|k| Rc::new(k.bind(py).borrow().inner.clone())) + .map(|k| k.bind(py).borrow().inner.clone()) .collect(); chanlun::structure::dash_line::虚线::计算MACD柱子均值_阴( &rc_list, @@ -626,7 +672,7 @@ impl 虚线Py { ) -> Option { let rc_list: Vec> = 普K序列 .iter() - .map(|k| Rc::new(k.bind(py).borrow().inner.clone())) + .map(|k| k.bind(py).borrow().inner.clone()) .collect(); chanlun::structure::dash_line::虚线::计算MACD柱子均值_阳( &rc_list, @@ -643,7 +689,7 @@ impl 虚线Py { ) -> bool { let rc_list: Vec> = 普K序列 .iter() - .map(|k| Rc::new(k.bind(py).borrow().inner.clone())) + .map(|k| k.bind(py).borrow().inner.clone()) .collect(); chanlun::structure::dash_line::虚线::武之MACD均值_阴(&rc_list, &实线.borrow().inner) } @@ -657,7 +703,7 @@ impl 虚线Py { ) -> bool { let rc_list: Vec> = 普K序列 .iter() - .map(|k| Rc::new(k.bind(py).borrow().inner.clone())) + .map(|k| k.bind(py).borrow().inner.clone()) .collect(); chanlun::structure::dash_line::虚线::武之MACD均值_阳(&rc_list, &实线.borrow().inner) } @@ -671,7 +717,7 @@ impl 虚线Py { ) -> [bool; 3] { let rc_list: Vec> = 普K序列 .iter() - .map(|k| Rc::new(k.bind(py).borrow().inner.clone())) + .map(|k| k.bind(py).borrow().inner.clone()) .collect(); chanlun::structure::dash_line::虚线::计算K线序列MACD趋向背驰( &rc_list, @@ -687,7 +733,7 @@ impl 虚线Py { ) -> Vec> { let rc_list: Vec> = k线序列 .iter() - .map(|k| Rc::new(k.bind(py).borrow().inner.clone())) + .map(|k| k.bind(py).borrow().inner.clone()) .collect(); chanlun::structure::dash_line::虚线::计算MACD柱子分段(&rc_list) } @@ -713,12 +759,37 @@ impl 虚线Py { 最大间隔: usize, 最少交叉数: usize, py: Python<'_>, - ) -> HashMap { + ) -> PyResult> { let rc_list: Vec> = 普K序列 .iter() - .map(|k| Rc::new(k.bind(py).borrow().inner.clone())) + .map(|k| k.bind(py).borrow().inner.clone()) .collect(); - chanlun::structure::dash_line::虚线::统计MACD行为(&rc_list, 最大间隔, 最少交叉数) + let result = + chanlun::structure::dash_line::虚线::统计MACD行为(&rc_list, 最大间隔, 最少交叉数); + let dict = pyo3::types::PyDict::new(py); + dict.set_item("DIF上穿0", result.DIF上穿0)?; + dict.set_item("DIF下穿0", result.DIF下穿0)?; + dict.set_item("DEA上穿0", result.DEA上穿0)?; + dict.set_item("DEA下穿0", result.DEA下穿0)?; + dict.set_item("金叉次数", result.金叉次数)?; + dict.set_item("死叉次数", result.死叉次数)?; + let 密集区: Vec> = result + .密集交叉区域 + .iter() + .map(|(a, b, c)| { + let tup = pyo3::types::PyTuple::new( + py, + [ + (*a).into_pyobject(py)?.into_any().unbind(), + (*b).into_pyobject(py)?.into_any().unbind(), + (*c).into_pyobject(py)?.into_any().unbind(), + ], + )?; + Ok(tup.into_any().unbind()) + }) + .collect::>>()?; + dict.set_item("密集交叉区域", 密集区)?; + Ok(dict.into()) } #[classmethod] @@ -821,6 +892,53 @@ impl 线段特征Py { self.__str__() } + fn __len__(&self) -> usize { + self.inner.元素.len() + } + + fn __getitem__(&self, index: isize, py: Python<'_>) -> PyResult> { + let len = self.inner.元素.len() as isize; + let idx = if index < 0 { index + len } else { index }; + if idx < 0 || idx >= len { + return Err(pyo3::exceptions::PyIndexError::new_err(format!( + "线段特征 index {index} out of range (len={len})" + ))); + } + let dash = &self.inner.元素[idx as usize]; + let obj: Py = Py::new( + py, + 虚线Py { + inner: Rc::clone(dash), + }, + )? + .into(); + Ok(obj) + } + + fn __iter__(&self, py: Python<'_>) -> PyResult> { + let list = pyo3::types::PyList::empty(py); + for d in &self.inner.元素 { + list.append(Py::new( + py, + 虚线Py { + inner: Rc::clone(d), + }, + )?)?; + } + list.call_method0("__iter__").map(|iter| iter.into()) + } + + fn __eq__(&self, other: &Bound<'_, PyAny>) -> bool { + if let Ok(other) = other.extract::>() { + return Rc::as_ptr(&self.inner) == Rc::as_ptr(&other.inner); + } + false + } + + fn __hash__(&self) -> u64 { + Rc::as_ptr(&self.inner) as u64 + } + // ---- instance methods ---- #[getter] @@ -1004,6 +1122,17 @@ impl 特征分型Py { fn __repr__(&self) -> String { self.__str__() } + + fn __eq__(&self, other: &Bound<'_, PyAny>) -> bool { + if let Ok(other) = other.extract::>() { + return Rc::as_ptr(&self.inner) == Rc::as_ptr(&other.inner); + } + false + } + + fn __hash__(&self) -> u64 { + Rc::as_ptr(&self.inner) as u64 + } } pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { diff --git a/chanlun-py/src/types_py.rs b/chanlun-py/src/types_py.rs index d7bf1a2..07b012d 100644 --- a/chanlun-py/src/types_py.rs +++ b/chanlun-py/src/types_py.rs @@ -135,27 +135,22 @@ impl 相对方向Py { } } - #[getter] fn 是否向上(&self) -> bool { self.inner.是否向上() } - #[getter] fn 是否向下(&self) -> bool { self.inner.是否向下() } - #[getter] fn 是否包含(&self) -> bool { self.inner.是否包含() } - #[getter] fn 是否缺口(&self) -> bool { self.inner.是否缺口() } - #[getter] fn 是否衔接(&self) -> bool { self.inner.是否衔接() } diff --git a/chanlun-py/test_integration.py b/chanlun-py/test_integration.py index 369df58..961db24 100644 --- a/chanlun-py/test_integration.py +++ b/chanlun-py/test_integration.py @@ -259,6 +259,417 @@ def test_override_reset(): 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(): + """重写关键序列 getter,super() 取基类值.""" + + class Sub(chanlun.观察者): + def __init__(self, 符号, 周期): + self._seq_accessed = set() + + @property + def 普通K线序列(self): + self._seq_accessed.add("普通K线序列") + return super().普通K线序列 + + @property + def 缠论K线序列(self): + self._seq_accessed.add("缠论K线序列") + return super().缠论K线序列 + + @property + def 笔序列(self): + self._seq_accessed.add("笔序列") + return super().笔序列 + + @property + def 线段序列(self): + self._seq_accessed.add("线段序列") + return super().线段序列 + + obs = Sub("btcusd", 300) + k = chanlun.K线.创建普K("t", 1761327300, 100.0, 105.0, 99.0, 103.0, 1000.0, 0, 300) + obs.增加原始K线(k) + + assert len(obs.普通K线序列) == 1 + assert "普通K线序列" in obs._seq_accessed + assert len(obs.缠论K线序列) == 1 + assert "缠论K线序列" in obs._seq_accessed + assert isinstance(obs.笔序列, list) + assert "笔序列" in obs._seq_accessed + assert isinstance(obs.线段序列, list) + assert "线段序列" in obs._seq_accessed + print(" ✓ test_override_sequence_getter") + + +def test_override_all_sequence_getters(): + """重写全部 15 个序列 getter,确认每个都可通过 super() 获取.""" + + all_seqs = [ + "普通K线序列", + "缠论K线序列", + "分型序列", + "笔序列", + "笔_中枢序列", + "线段序列", + "中枢序列", + "扩展线段序列", + "扩展中枢序列", + "扩展线段序列_线段", + "扩展中枢序列_线段", + "线段_线段序列", + "线段_中枢序列", + "扩展线段序列_扩展线段", + "扩展中枢序列_扩展线段", + ] + + # 动态构建子类,重写全部序列 getter + def _make_getter(name): + @property + def getter(self, _name=name): + self._all_seq_accessed.add(_name) + # 通过 MRO 找到父类的 property 并调用 + for cls in type(self).__mro__[1:]: + if hasattr(cls, _name) and isinstance(getattr(cls, _name, None), property): + return getattr(cls, _name).fget(self) + return [] + + return getter + + class Sub(chanlun.观察者): + def __init__(self, 符号, 周期): + self._all_seq_accessed = set() + + for seq_name in all_seqs: + setattr(Sub, seq_name, _make_getter(seq_name)) + + obs = Sub("btcusd", 300) + k = chanlun.K线.创建普K("t", 1761327300, 100.0, 105.0, 99.0, 103.0, 1000.0, 0, 300) + obs.增加原始K线(k) + + for seq_name in all_seqs: + seq = getattr(obs, seq_name) + assert seq_name in obs._all_seq_accessed, f"{seq_name} 未被拦截" + assert isinstance(seq, list), f"{seq_name} 不是 list,而是 {type(seq)}" + + print(" ✓ test_override_all_sequence_getters") + + +# --- 方法重写 --- + + +def test_override_加载本地数据(): + """重写 加载本地数据,super() 调基类.""" + + class Sub(chanlun.观察者): + def __init__(self, 符号, 周期): + self._loaded = False + self._load_count = 0 + + def 加载本地数据(self, 文件路径): + self._loaded = True + self._load_count += 1 + super().加载本地数据(文件路径) + + obs = Sub("btcusd", 300) + obs.加载本地数据(NB_PATH) + assert obs._loaded + assert obs._load_count == 1 + assert len(obs.普通K线序列) > 0 + print(" ✓ test_override_加载本地数据") + + +def test_override_静态重新分析(): + """重写 静态重新分析,super() 调基类.""" + + class Sub(chanlun.观察者): + def __init__(self, 符号, 周期): + self._reanalyzed = 0 + + def 静态重新分析(self): + self._reanalyzed += 1 + super().静态重新分析() + + obs = Sub("btcusd", 300) + obs.加载本地数据(NB_PATH) + bi_before = len(obs.笔序列) + obs.静态重新分析() + assert obs._reanalyzed == 1 + # 重新分析后笔序列仍然存在 + assert len(obs.笔序列) >= 0 + print(" ✓ test_override_静态重新分析") + + +def test_override_测试_保存数据(): + """重写 测试_保存数据,super() 调基类.""" + + class Sub(chanlun.观察者): + def __init__(self, 符号, 周期): + self._saved = False + self._save_root = None + + def 测试_保存数据(self, root=None): + self._saved = True + self._save_root = root + super().测试_保存数据(root) + + obs = Sub("btcusd", 300) + k = chanlun.K线.创建普K("t", 1761327300, 100.0, 105.0, 99.0, 103.0, 1000.0, 0, 300) + obs.增加原始K线(k) + with tempfile.TemporaryDirectory() as tmpdir: + obs.测试_保存数据(tmpdir) + assert obs._saved + assert obs._save_root == tmpdir + print(" ✓ test_override_测试_保存数据") + + +def test_override_读取数据文件(): + """重写 classmethod 读取数据文件.""" + + class Sub(chanlun.观察者): + @classmethod + def 读取数据文件(cls, 文件路径, 配置=None): + obs = super().读取数据文件(文件路径, 配置) + obs._custom_classmethod_flag = True + return obs + + obs = Sub.读取数据文件(NB_PATH) + assert isinstance(obs, Sub) + assert obs._custom_classmethod_flag + assert len(obs.普通K线序列) > 0 + print(" ✓ test_override_读取数据文件") + + +# --- 完全重写(不调 super) --- + + +def test_override_completely_no_super(): + """完全重写 增加原始K线,不调 super(),彻底接管.""" + + class Sub(chanlun.观察者): + def 增加原始K线(self, 普K): + self._custom = getattr(self, "_custom", []) + self._custom.append(普K.时间戳) + # 不调 super() + + obs = Sub("btcusd", 300) + k = chanlun.K线.创建普K("t", 1761327300, 100.0, 105.0, 99.0, 103.0, 1000.0, 0, 300) + obs.增加原始K线(k) + assert obs._custom == [1761327300] + assert len(obs.普通K线序列) == 0 # 基类逻辑没执行 + print(" ✓ test_override_completely_no_super") + + +# --- 同名继承(零重写) --- + + +def test_override_identical_subclass(): + """同名继承(零重写),行为与基类完全一致.""" + + class Sub(chanlun.观察者): + pass + + base = chanlun.观察者("btcusd", 300) + sub = Sub("btcusd", 300) + + bars = read_nb_bars(NB_PATH, max_bars=200) + for i, (ts, o, h, l, c, v) in enumerate(bars): + bk = chanlun.K线.创建普K(f"b_{i}", ts, o, h, l, c, v, i, 300) + sk = chanlun.K线.创建普K(f"s_{i}", ts, o, h, l, c, v, i, 300) + base.增加原始K线(bk) + sub.增加原始K线(sk) + + for attr in ["普通K线序列", "笔序列", "线段序列", "中枢序列"]: + base_len = len(getattr(base, attr)) + sub_len = len(getattr(sub, attr)) + assert base_len == sub_len, f"{attr}: base={base_len}, sub={sub_len}" + + print(" ✓ test_override_identical_subclass") + + +# --- 多层继承 --- + + +def test_override_three_level_mixed(): + """三层继承,每层重写不同方法,MRO 链完整.""" + + class L1(chanlun.观察者): + def __init__(self, 符号, 周期): + self._l1_feed = 0 + + def 增加原始K线(self, 普K): + self._l1_feed += 1 + super().增加原始K线(普K) + + class L2(L1): + def __init__(self, 符号, 周期): + super().__init__(符号, 周期) + self._l2_标识 = 0 + + @property + def 标识(self): + self._l2_标识 += 1 + return f"[L2] {super().标识}" + + class L3(L2): + def __init__(self, 符号, 周期): + super().__init__(符号, 周期) + self._l3_reset = 0 + + def 重置基础序列(self): + self._l3_reset += 1 + super().重置基础序列() + + obs = L3("btcusd", 300) + k = chanlun.K线.创建普K("t", 1761327300, 100.0, 105.0, 99.0, 103.0, 1000.0, 0, 300) + obs.增加原始K线(k) + + assert obs._l1_feed == 1, f"L1 feed: {obs._l1_feed}" + assert obs.标识 == "[L2] btcusd:300" + assert obs._l2_标识 == 1 + obs.重置基础序列() + assert obs._l3_reset == 1 + assert len(obs.普通K线序列) == 0 + print(" ✓ test_override_three_level_mixed") + + +# --- 跨方法调用 --- + + +def test_override_cross_method_dispatch(): + """子类方法间相互调用,确保 self 始终指向最外层实例.""" + + class Sub(chanlun.观察者): + def __init__(self, 符号, 周期): + self._feed_called = False + self._reset_called = False + + def 增加原始K线(self, 普K): + self._feed_called = True + super().增加原始K线(普K) + + def 重置基础序列(self): + self._reset_called = True + super().重置基础序列() + + def compound_operation(self, 普K): + self.增加原始K线(普K) + self.重置基础序列() + + obs = Sub("btcusd", 300) + k = chanlun.K线.创建普K("t", 1761327300, 100.0, 105.0, 99.0, 103.0, 1000.0, 0, 300) + obs.compound_operation(k) + + assert obs._feed_called + assert obs._reset_called + assert len(obs.普通K线序列) == 0 # feed 后又 reset + print(" ✓ test_override_cross_method_dispatch") + + def run_subclass_tests(): print("=== 观察者 子类化/重写 测试 ===") tests = [ @@ -272,6 +683,26 @@ def run_subclass_tests(): 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: @@ -330,7 +761,17 @@ def run_integration_test(): print(f" Output dir: {actual_out_dir}") print(f" Output files ({len(out_files)}): {out_files}") - # Compare with Python reference + # 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)) @@ -395,11 +836,332 @@ def run_integration_test(): 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"], help="运行哪组测试 (默认: all)") + parser.add_argument("test", nargs="?", default="all", choices=["all", "subclass", "integration", "jiequ", "rc"], help="运行哪组测试 (默认: all)") args = parser.parse_args() exit_code = 0 @@ -412,6 +1174,14 @@ def main(): 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✓ 所有测试通过") diff --git a/chanlun/Cargo.toml b/chanlun/Cargo.toml index 620eeca..882bbbe 100644 --- a/chanlun/Cargo.toml +++ b/chanlun/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "chanlun" -version = "26.5.1" +version = "26.5.2" edition = "2021" rust-version = "1.70" license = "MIT" diff --git a/chanlun/src/algorithm/bi.rs b/chanlun/src/algorithm/bi.rs index 120f6ab..fc56b75 100644 --- a/chanlun/src/algorithm/bi.rs +++ b/chanlun/src/algorithm/bi.rs @@ -928,7 +928,7 @@ impl 笔 { } /// 是否背驰过 — 判断笔是否在停顿位置出现过MACD趋向背驰 - pub fn 是否背驰过(当前筆: &虚线, 观察员: &观察者) -> Vec> { + pub fn 是否背驰过(当前筆: &虚线, 观察员: &观察者) -> Vec> { let 停顿位置 = Self::获取所有停顿位置(当前筆, 观察员); let mut 结果 = Vec::new(); @@ -940,7 +940,7 @@ impl 笔 { ); let 背驰信号 = 虚线::计算K线序列MACD趋向背驰(&k线范围, 筆.方向()); if 背驰信号.iter().all(|&x| x) { - 结果.push(Rc::clone(&筆.武)); + 结果.push(Rc::clone(&筆.武.中)); } } diff --git a/chanlun/src/algorithm/hub.rs b/chanlun/src/algorithm/hub.rs index dd0e13a..72d078e 100644 --- a/chanlun/src/algorithm/hub.rs +++ b/chanlun/src/algorithm/hub.rs @@ -288,12 +288,8 @@ impl 中枢 { /// 返回当前中枢最后一段所处的位置关系:中枢之中/中枢之上/中枢之下 pub fn 当前状态(&self) -> &str { let 最后 = self.基础序列.last().unwrap(); - let 尾部_中 = if 最后.标识 == "笔" { - &最后.武.中 - } else { - &最后.基础序列.last().unwrap().武.中 - }; - let 关系 = crate::types::相对方向::分析(self.高(), self.低(), 尾部_中.高, 尾部_中.低); + let 尾部 = 最后.获取_武(); + let 关系 = crate::types::相对方向::分析(self.高(), self.低(), 尾部.中.高, 尾部.中.低); if 关系 == crate::types::相对方向::向上缺口 { "中枢之上" } else if 关系 == crate::types::相对方向::向下缺口 { diff --git a/chanlun/src/algorithm/segment.rs b/chanlun/src/algorithm/segment.rs index cfdcf27..3fe145c 100644 --- a/chanlun/src/algorithm/segment.rs +++ b/chanlun/src/algorithm/segment.rs @@ -27,6 +27,7 @@ use crate::algorithm::hub::中枢; use crate::business::observer::观察者; use crate::config::缠论配置; use crate::kline::bar::K线; +use crate::kline::chan_kline::缠论K线; use crate::structure::dash_line::虚线; use crate::structure::fractal_obj::分型; use crate::structure::segment_feat::线段特征; @@ -56,7 +57,7 @@ impl 线段 { if !段.基础序列.is_empty() { if !分型::判断分型(&段.基础序列.last().unwrap().武, &筆.文, "中") { panic!( - "线段.添加虚线 不连续 {:?} {:?}", + "线段.添加虚线 不连续 {} {}", 段.基础序列.last().unwrap(), 筆 ); @@ -85,17 +86,14 @@ impl 线段 { ); } if 段.文.结构 == 武.结构 { - panic!("文武结构相同 {} {:?} {:?}", 行号, 段.文, 武); + panic!("文武结构相同 {} {} {}", 行号, 段.文, 武); } if let (Some(ref 左), Some(ref 右)) = (&武.左, &武.右) { if let Some(分析结构) = 分型结构::分析(左.as_ref(), 武.中.as_ref(), 右.as_ref(), false, false) { if 分析结构 != 武.结构 { - panic!( - "武斗[{}], 分型结构不一致 {:?} != {:?}", - 行号, 分析结构, 武.结构 - ); + panic!("武斗[{}], 分型结构不一致 {} != {}", 行号, 分析结构, 武.结构); } } } @@ -557,7 +555,7 @@ impl 线段 { if 特征后一笔.is_none() { eprintln!( - " 线段.刷新 特征后一笔 = None, {:?}, 有效特征: {}", + " 线段.刷新 特征后一笔 = None, {}, 有效特征: {}", 段2, 有效特征序列.len() ); @@ -679,7 +677,7 @@ impl 线段 { if let Some(前一个) = 线段序列.last() { if !前一个.之后是(seg) { panic!( - "线段.向序列中添加 不连续[{}] {:?} {:?}", + "线段.向序列中添加 不连续[{}] {} {}", 行号, 前一个.武, seg.文 ); } @@ -694,7 +692,7 @@ impl 线段 { { // The Python code asserts here; we warn eprintln!( - "线段._向序列中添加[{}], 之前线段.右 = None {:?}", + "线段._向序列中添加[{}], 之前线段.右 = None {}", 行号, 之前线段 ); } @@ -706,7 +704,7 @@ impl 线段 { && !之前线段.短路修正 { panic!( - "线段._向序列中添加[{}], 之前线段[-1] not in 待添加虚线! {:?}", + "线段._向序列中添加[{}], 之前线段[-1] not in 待添加虚线! {}", 行号, 之前线段 ); } @@ -735,7 +733,7 @@ impl 线段 { } if Rc::as_ptr(线段序列.last().unwrap()) != Rc::as_ptr(待弹出线段) { - panic!("线段._从序列中删除 弹出数据不在列表中 {:?}", 待弹出线段); + panic!("线段._从序列中删除 弹出数据不在列表中 {}", 待弹出线段); } if 待弹出线段.特征序列.len() >= 3 { @@ -889,7 +887,7 @@ impl 线段 { } // 执行修正 - eprintln!("[警告<{}, {}>]: 线段.修复贯穿伤 {:?}", 层级, 层级, 贯穿伤); + eprintln!("[警告<{}, {}>]: 线段.修复贯穿伤 {}", line!(), 层级, 贯穿伤); let 原始基础序列 = 当前线段.基础序列.clone(); Self::_弹出线段( @@ -1320,7 +1318,7 @@ impl 线段 { if let Some(前一个) = 线段序列.last() { if !前一个.之后是(seg) { panic!( - "线段.向序列中添加 不连续[{}] {:?} {:?}", + "线段.向序列中添加 不连续[{}] {} {}", 行号, 前一个.武, seg.文 ); } @@ -1347,7 +1345,7 @@ impl 线段 { seg.有效性 = false; Some(drop) } else { - panic!("线段._从序列中删除 弹出数据不在列表中 {:?}", 待弹出线段); + panic!("线段._从序列中删除 弹出数据不在列表中 {}", 待弹出线段); } } @@ -1630,8 +1628,8 @@ impl 线段 { 背驰 || 盘整背驰 } - /// 段获取所有停顿位置 — 在线段范围内找出所有停顿位置 - pub fn 段获取所有停顿位置(段: &虚线, 观察员: &观察者) -> Vec<虚线> { + /// 获取所有停顿位置 — 在线段范围内找出所有停顿位置 + pub fn 获取所有停顿位置(段: &虚线, 观察员: &观察者) -> Vec<虚线> { let mut 结果 = Vec::new(); if 段.模式 != "文武" || 段.标识 != "线段" { return 结果; @@ -1647,7 +1645,7 @@ impl 线段 { let mut 当前停顿: Option> = None; for 筆 in &阳 { - if 笔序列.len() >= 2 { + if 笔序列.len() >= 3 { let 筆停顿 = 笔::获取所有停顿位置(筆, 观察员); let mut 停顿列表: Vec> = 筆停顿.into_iter().map(|b| Rc::new(b)).collect(); 停顿列表.push(Rc::clone(筆)); @@ -1676,21 +1674,25 @@ impl 线段 { }; if !重复 { if let Some(最后线段) = 线段序列.last() { - let mut 新段 = 虚线::创建线段(&最后线段.基础序列); - 新段.序号 = 段.序号; - let mut 新段_rc = Rc::new(新段); - Self::刷新(&mut 新段_rc, &观察员.配置); - let 新段_inner = - Rc::try_unwrap(新段_rc).unwrap_or_else(|rc| (*rc).clone()); - if 新段_inner.方向() == 段.方向() { - 当前停顿 = Some(Rc::clone(&线段序列.last().unwrap().武)); - 结果.push(新段_inner); + if 最后线段.基础序列.len() % 2 == 1 { + let mut 新段 = 虚线::创建线段(&最后线段.基础序列); + 新段.序号 = 段.序号; + let mut 新段_rc = Rc::new(新段); + Self::刷新(&mut 新段_rc, &观察员.配置); + let 新段_inner = + Rc::try_unwrap(新段_rc).unwrap_or_else(|rc| (*rc).clone()); + if 新段_inner.方向() == 段.方向() { + 当前停顿 = Some(Rc::clone(&线段序列.last().unwrap().武)); + 结果.push(新段_inner); + } } } } if Rc::as_ptr(停顿) != Rc::as_ptr(筆) { - 笔序列.pop(); + if let Some(mut popped) = 笔序列.pop() { + Rc::make_mut(&mut popped).有效性 = false; + } } } } else { @@ -1701,15 +1703,15 @@ impl 线段 { } /// 是否背驰过 — 判断线段是否在停顿位置出现过背驰 - pub fn 是否背驰过(当前段: &虚线, 观察员: &观察者) -> Vec> { - let 停顿位置 = Self::段获取所有停顿位置(当前段, 观察员); + pub fn 是否背驰过(当前段: &虚线, 观察员: &观察者) -> Vec> { + let 停顿位置 = Self::获取所有停顿位置(当前段, 观察员); let mut 结果 = Vec::new(); for 段 in 停顿位置 { let mut 段_rc = Rc::new(段); Self::获取内部中枢序列(&mut 段_rc, &观察员.配置); if Self::判断线段内部是否背驰(&段_rc, 观察员) { - 结果.push(Rc::clone(&段_rc.武)); + 结果.push(Rc::clone(&段_rc.武.中)); } } diff --git a/chanlun/src/business/observer.rs b/chanlun/src/business/observer.rs index 46fa016..498b140 100644 --- a/chanlun/src/business/observer.rs +++ b/chanlun/src/business/observer.rs @@ -481,6 +481,31 @@ impl 观察者 { println!("全部数据拆分保存完成,目录:{}", 保存路径.display()); } + /// 解析本地数据文件 — 从 .nb 文件读取并解析所有 K线 + pub fn 解析本地数据(&self, 文件路径: &str) -> Result, String> { + let data = std::fs::read(文件路径).map_err(|e| format!("read file: {}", e))?; + let mut bars = Vec::new(); + let size = 48; + for i in 0..data.len() / size { + let offset = i * size; + if let Some(k线) = K线::from_bytes(&data[offset..offset + size], self.周期, &self.符号) + { + bars.push(k线); + } + } + Ok(bars) + } + + /// 加载本地数据 — 从 .nb 文件加载数据到当前观察者(先重置再投喂) + pub fn 加载本地数据(&mut self, 文件路径: &str) -> Result<(), String> { + self.重置基础序列(); + let bars = self.解析本地数据(文件路径)?; + for k线 in bars { + self.增加原始K线(k线); + } + Ok(()) + } + /// 读取数据文件 — 从 .nb 文件加载数据 pub fn 读取数据文件( 文件路径: &str, @@ -517,3 +542,96 @@ impl 观察者 { Ok(实例) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::缠论配置; + + #[test] + fn test_普k序列指针一致性() { + let config = 缠论配置::default(); + let obs = 观察者::读取数据文件( + "/home/moscow/chanlun.rs/btcusd-300-1777649100-1778398800.nb", + Some(config), + ) + .unwrap(); + let obs_ref = obs.borrow(); + + // 1. Check that each 笔's 获取普K序列 returns K lines whose Rc pointers + // match entries in 普通K线序列 + for (i, bi) in obs_ref.笔序列.iter().enumerate() { + let pu_seq = bi.获取普K序列(&obs_ref.普通K线序列); + if pu_seq.is_empty() { + println!("笔 {}: 获取普K序列 返回空 (fallback failed)", i); + println!(" 文.中.标的K线 原始起始序号: {}", bi.文.中.原始起始序号); + println!(" 武.中.标的K线 原始结束序号: {}", bi.武.中.原始结束序号); + println!(" 普通K线序列.len: {}", obs_ref.普通K线序列.len()); + } else { + // Check first element's pointer + let first_ptr = Rc::as_ptr(&pu_seq[0]); + let found = obs_ref + .普通K线序列 + .iter() + .any(|k| Rc::as_ptr(k) == first_ptr); + if !found { + println!("笔 {}: 获取普K序列[0] 的 Rc 指针不在 普通K线序列 中!", i); + // Check if 文.中.标的K线 pointer is in 普通K线序列 + let wen_ptr = Rc::as_ptr(&bi.文.中.标的K线); + let wen_found = obs_ref.普通K线序列.iter().any(|k| Rc::as_ptr(k) == wen_ptr); + println!(" 文.中.标的K线 在序列中: {}", wen_found); + } else { + println!("笔 {}: OK, 获取普K序列[0] 在序列中找到", i); + } + } + } + } + + #[test] + fn test_pyo3_flow_pointer_consistency() { + // Simulate what the PyO3 读取数据文件 classmethod does: + // 1. Parse K lines from file + // 2. Create "K线Py { inner: Rc::new(k线) }" for each + // 3. Call observer.增加原始K线(k线_value) + + let config = 缠论配置::default(); + let obs_ref = 观察者::new("btcusd".into(), 300, config); + + let file_path = "/home/moscow/chanlun.rs/btcusd-300-1777649100-1778398800.nb"; + let data = std::fs::read(file_path).unwrap(); + let size = 48; + + for i in 0..data.len() / size { + let offset = i * size; + if let Some(k线) = K线::from_bytes(&data[offset..offset + size], 300, "btcusd") { + // Simulate: K线Py { inner: Rc::new(k线) } + let _k线_py_inner = Rc::new(k线.clone()); + // In the actual PyO3 path, (*普K.borrow().inner).clone() extracts K线 value + // which then gets Rc::wrapped inside the observer + obs_ref.borrow_mut().增加原始K线(k线); + } + } + + let obs = obs_ref.borrow(); + println!("普通K线序列.len: {}", obs.普通K线序列.len()); + println!("笔序列.len: {}", obs.笔序列.len()); + + // Now check: does 获取普K序列 return K lines whose pointers match 普通K线序列? + for (i, bi) in obs.笔序列.iter().enumerate() { + let pu_seq = bi.获取普K序列(&obs.普通K线序列); + if pu_seq.is_empty() { + println!("笔 {}: 获取普K序列 返回空!", i); + } else { + let first_ptr = Rc::as_ptr(&pu_seq[0]); + let found = obs.普通K线序列.iter().any(|k| Rc::as_ptr(k) == first_ptr); + if !found { + println!("笔 {}: 获取普K序列[0] 指针不在序列中!", i); + } + // Only print first few + if i < 5 { + println!("笔 {}: OK, len={}", i, pu_seq.len()); + } + } + } + } +} diff --git a/chanlun/src/indicators/macd.rs b/chanlun/src/indicators/macd.rs index b10f4b9..26952f9 100644 --- a/chanlun/src/indicators/macd.rs +++ b/chanlun/src/indicators/macd.rs @@ -80,7 +80,7 @@ impl 平滑异同移动平均线 { let 慢线EMA = 初始收盘价; let DIF = 快线EMA - 慢线EMA; let DEA_EMA = DIF; - let MACD柱 = DIF - DEA_EMA; + let MACD柱 = 2.0 * (DIF - DEA_EMA); Self { 时间戳: 初始时间, diff --git a/chanlun/src/kline/chan_kline.rs b/chanlun/src/kline/chan_kline.rs index 8b323dc..1e55d85 100644 --- a/chanlun/src/kline/chan_kline.rs +++ b/chanlun/src/kline/chan_kline.rs @@ -47,7 +47,7 @@ pub struct 缠论K线 { pub 原始起始序号: i64, pub 原始结束序号: i64, pub 标的K线: Rc, - pub 买卖点信息: Option<()>, // 占位,后续替换为实际类型 + pub 买卖点信息: std::cell::RefCell>, } impl std::fmt::Display for 缠论K线 { @@ -84,7 +84,7 @@ impl 缠论K线 { 原始起始序号: self.原始起始序号, 原始结束序号: self.原始结束序号, 标的K线: Rc::clone(&self.标的K线), - 买卖点信息: None, + 买卖点信息: std::cell::RefCell::new(self.买卖点信息.borrow().clone()), } } @@ -217,7 +217,7 @@ impl 缠论K线 { 原始起始序号: 原始序号, 原始结束序号: 原始序号, 标的K线: 普k, - 买卖点信息: None, + 买卖点信息: std::cell::RefCell::new(Vec::new()), }; if let Some(之前) = 之前 { @@ -239,7 +239,7 @@ impl 缠论K线 { pub fn 兼并( 之前缠K: Option<&缠论K线>, 当前缠K: &mut 缠论K线, - 当前普K: &K线, + 当前普K: &Rc, 配置: &缠论配置, ) -> (Option>, Option) { let 关系 = 相对方向::分析(当前缠K.高, 当前缠K.低, 当前普K.高, 当前普K.低); @@ -258,7 +258,7 @@ impl 缠论K线 { 当前普K.方向(), 结构, 当前普K.序号, - Rc::new(当前普K.clone()), + Rc::clone(当前普K), Some(当前缠K), ); 新缠K.序号 = 当前缠K.序号 + 1; @@ -294,7 +294,7 @@ impl 缠论K线 { // 逆序包含时更新时间和标的K线 if 关系 != 相对方向::顺 { 当前缠K.时间戳 = 当前普K.时间戳; - 当前缠K.标的K线 = Rc::new(当前普K.clone()); + 当前缠K.标的K线 = Rc::clone(当前普K); } 当前缠K.高 = 取值函数(当前缠K.高, 当前普K.高); 当前缠K.低 = 取值函数(当前缠K.低, 当前普K.低); @@ -461,7 +461,7 @@ impl 缠论K线 { // ---- 阶段2: 缠K合并 ---- let 状态: String; - let 当前K线_ref: &K线 = &*普K序列.last().unwrap(); + let 当前K线_ref: &Rc = 普K序列.last().unwrap(); if !缠K序列.is_empty() { let len = 缠K序列.len(); diff --git a/chanlun/src/structure/dash_line.rs b/chanlun/src/structure/dash_line.rs index ac36ec3..00a0417 100644 --- a/chanlun/src/structure/dash_line.rs +++ b/chanlun/src/structure/dash_line.rs @@ -55,6 +55,18 @@ pub struct 虚线 { pub 短路修正: bool, } +/// MACD行为统计 — 统计MACD行为 方法的返回类型 +#[derive(Debug, Clone)] +pub struct MACD行为统计 { + pub DIF上穿0: i64, + pub DIF下穿0: i64, + pub DEA上穿0: i64, + pub DEA下穿0: i64, + pub 金叉次数: i64, + pub 死叉次数: i64, + pub 密集交叉区域: Vec<(usize, usize, usize)>, +} + impl 虚线 { pub fn new( 序号: i64, @@ -144,12 +156,27 @@ impl 虚线 { /// 获取该虚线范围内的普K序列 pub fn 获取普K序列(&self, 普K序列: &[Rc]) -> Vec> { - let 始 = self.文.中.原始起始序号 as usize; - let 终 = self.武.中.原始结束序号 as usize; - if 始 < 普K序列.len() && 终 < 普K序列.len() && 始 <= 终 { - 普K序列[始..=终].to_vec() - } else { - Vec::new() + // 使用指针查找(与 Python list.index 身份匹配行为一致), + // 而非序号切片——因为序号可能与实际位置不一致。 + let 始 = 普K序列 + .iter() + .position(|k| Rc::as_ptr(k) == Rc::as_ptr(&self.文.中.标的K线)); + let 终 = 普K序列 + .iter() + .position(|k| Rc::as_ptr(k) == Rc::as_ptr(&self.武.中.标的K线)); + match (始, 终) { + (Some(s), Some(e)) if s <= e => 普K序列[s..=e].to_vec(), + _ => { + // 指针查找失败时回退到序号方式 + println!("[警告]虚线.获取普K序列 <指针查找失败时回退到序号方式>"); + let 始 = self.文.中.原始起始序号 as usize; + let 终 = self.武.中.原始结束序号 as usize; + if 始 < 普K序列.len() && 终 < 普K序列.len() && 始 <= 终 { + 普K序列[始..=终].to_vec() + } else { + Vec::new() + } + } } } @@ -158,6 +185,18 @@ impl 虚线 { 缠论K线::截取(缠K序列, &self.文.中, &self.武.中).unwrap_or_default() } + /// 获取_武 — 递归获取虚线的终点分型(笔直接返回武,线段递归到底层笔的武) + pub fn 获取_武(&self) -> Rc<分型> { + if self.标识 == "笔" { + return Rc::clone(&self.武); + } + let mut current: &虚线 = self; + while current.标识 != "笔" { + current = &*current.基础序列.last().unwrap(); + } + Rc::clone(¤t.武) + } + /// 获取数据文本(用于保存/调试) pub fn 获取数据文本(&self) -> String { use crate::utils::format_f64_g; @@ -693,9 +732,7 @@ impl 虚线 { 普K序列: &[Rc], 最大间隔: usize, 最少交叉数: usize, - ) -> std::collections::HashMap { - use std::collections::HashMap; - + ) -> MACD行为统计 { let mut dif_up = 0; let mut dif_down = 0; let mut dea_up = 0; @@ -759,17 +796,17 @@ impl 虚线 { } } - let 密集区 = Self::密集区域按间隔(&交叉标记, 最大间隔, 最少交叉数); + let 密集交叉区域 = Self::密集区域按间隔(&交叉标记, 最大间隔, 最少交叉数); - let mut map = HashMap::new(); - map.insert("DIF上穿0".into(), dif_up.to_string()); - map.insert("DIF下穿0".into(), dif_down.to_string()); - map.insert("DEA上穿0".into(), dea_up.to_string()); - map.insert("DEA下穿0".into(), dea_down.to_string()); - map.insert("金叉次数".into(), golden.to_string()); - map.insert("死叉次数".into(), death.to_string()); - map.insert("密集交叉区域".into(), format!("{:?}", 密集区)); - map + MACD行为统计 { + DIF上穿0: dif_up, + DIF下穿0: dif_down, + DEA上穿0: dea_up, + DEA下穿0: dea_down, + 金叉次数: golden, + 死叉次数: death, + 密集交叉区域, + } } // ---- 买卖意义 ---- @@ -797,7 +834,7 @@ impl 虚线 { let 意义 = Self::缠K买卖点模式(&配置.买卖点_指标模式, &实线.武.中, 配置); let 结果 = false; - let 背驰过: Vec> = if 实线.标识 == "笔" { + let 背驰过: Vec> = if 实线.标识 == "笔" { crate::algorithm::bi::笔::是否背驰过(实线, 观察员) } else { crate::algorithm::segment::线段::是否背驰过(实线, 观察员) @@ -816,7 +853,11 @@ impl 虚线 { format!( "背驰过:{},极值:{},柱子分型匹配", 背驰过.len(), - Self::武之MACD极值(普K序列, 实线) + if Self::武之MACD极值(普K序列, 实线) { + "True" + } else { + "False" + } ), ); }