From 1e6025a968604ac718b80f815bdc0cc20e8e944c Mon Sep 17 00:00:00 2001 From: YuWuKunCheng Date: Fri, 29 May 2026 23:51:58 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20=E5=8E=9F=E5=A7=8Bchan?= =?UTF-8?q?=E5=88=B0=E6=A8=A1=E5=9D=97=20=E4=BF=AE=E5=A4=8D=20=E7=9B=B8?= =?UTF-8?q?=E5=AF=B9=E6=96=B9=E5=90=91=E7=9A=84=E4=B8=80=E8=87=B4=E6=80=A7?= =?UTF-8?q?=20=E6=B7=BB=E5=8A=A0=20=E8=A7=82=E5=AF=9F=E8=80=85.=E6=8A=95?= =?UTF-8?q?=E5=96=82=E5=8E=9F=E5=A7=8B=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chan.py | 49 +++++++----- chanlun-py/Cargo.toml | 6 +- chanlun-py/chanlun/__init__.py | 2 + chanlun-py/chanlun/chan.py | 46 ++++++----- chanlun-py/pyproject.toml | 2 +- chanlun-py/src/algorithm_py.rs | 8 +- chanlun-py/src/business_py.rs | 78 +++++++++---------- chanlun-py/src/kline_py.rs | 16 ++-- chanlun-py/src/lib.rs | 59 +++++++------- chanlun-py/src/structure_py.rs | 33 ++++---- chanlun-py/src/types_py.rs | 49 ++++++++++-- chanlun/src/business/observer.rs | 129 +++++++++++++++++-------------- chanlun/src/kline/bar.rs | 15 ++++ chanlun/src/main.rs | 60 +++++++------- chanlun/src/types/sync_f64.rs | 24 ++++++ 15 files changed, 333 insertions(+), 243 deletions(-) diff --git a/chan.py b/chan.py index c4bc32f..2cef756 100644 --- a/chan.py +++ b/chan.py @@ -5078,6 +5078,17 @@ class 观察者: self.扩展线段序列_扩展线段: List[虚线] = [] self.扩展中枢序列_扩展线段: List[中枢] = [] + def 投喂原始数据(self, 时间戳: datetime, 开: float, 高: float, 低: float, 收: float, 量: float): + """便捷入口,直接从 OHLCV 创建 K线 并投喂 + :param 时间戳: 时间戳 + :param 开: 开盘价 + :param 高: 最高价 + :param 低: 最低价 + :param 收: 收盘价 + :param 量: 成交量 + """ + self.增加原始K线(K线.创建普K(self.标识, 时间戳, 开, 高, 低, 收, 量, 0, self.周期)) + @final def 增加原始K线(self, 普K: K线): """核心入口 — 投喂一根原始K线,增量更新所有层级 @@ -5224,33 +5235,32 @@ class 观察者: self.配置.分析线段中枢 and 中枢.分析(self.线段_线段序列, self.线段_中枢序列) def 加载本地数据(self, 文件路径: str): + """重置基础序列后加载数据文件 + :param 文件路径: 数据文件路径 格式如: btcusd-300-1631772074-1632222374.nb + """ 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线) + 时间戳, 开盘价, 最高价, 最低价, 收盘价, 成交量 = struct.unpack(">6d", buffer[i * size: i * size + size]) + self.投喂原始数据(转化为时间戳(int(时间戳)), 开盘价, 最高价, 最低价, 收盘价, 成交量) @classmethod - def 读取数据文件(cls, 文件路径: str, 配置=缠论配置()) -> Self: - """ + def 读取数据文件(cls,观察员:"观察者", 文件路径: str, 配置=缠论配置()) -> Self: + """ 加载数据文件 + :param 观察员: 观察者 :param 文件路径: 数据文件路径 格式如: btcusd-300-1631772074-1632222374.nb :param 配置: 缠论配置 :return: 观察者实例 """ name = Path(文件路径).name.split(".")[0] 符号, 周期, 起始时间戳, 结束时间戳 = name.split("-") - 实例 = cls(符号=符号, 周期=int(周期), 配置=配置) - - 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], int(周期), 符号) - 实例.增加原始K线(k线) - - return 实例 + 观察员.符号 = 符号 + 观察员.周期 = int(周期) + 观察员.配置 = 配置 + 观察员.加载本地数据(文件路径) + return 观察员 class K线合成器: @@ -5503,16 +5513,16 @@ class 立体分析器: print(f"多级别数据拆分保存完成,目录:{保存路径.resolve()}") -def 测试_读取数据(配置: 缠论配置): +def 测试_读取数据(观察员: 观察者, 配置: 缠论配置)-> Callable[[], "观察者"]: """测试_读取数据 - + :param 观察员: 观察者 :param 配置: 缠论配置 :return: 测试函数 """ def 魔法(): 启动时间 = datetime.now() - 观察员 = 观察者.读取数据文件(配置.加载文件路径, 配置) + 观察者.读取数据文件(观察员, 配置.加载文件路径, 配置) 消耗用时 = datetime.now() - 启动时间 print("测试_读取数据 耗时", 消耗用时, "普K数量", len(观察员.普通K线序列)) return 观察员 @@ -5552,5 +5562,6 @@ def 测试_周期合成(配置: 缠论配置, 配置组: Dict[int, 缠论配置] if __name__ == "__main__": 当前配置 = 缠论配置.不推送() 当前配置.加载文件路径 = str(Path(__file__).parent / "btcusd-300-1761327300-1776327900.nb") - 测试_读取数据(当前配置)().测试_保存数据() - 测试_周期合成(当前配置)().测试_保存数据() + 观察员 = 观察者("", 0, 当前配置) + 测试_读取数据(观察员,当前配置)()# .测试_保存数据() + # 测试_周期合成(当前配置)().测试_保存数据() diff --git a/chanlun-py/Cargo.toml b/chanlun-py/Cargo.toml index d0346bf..cad5d29 100644 --- a/chanlun-py/Cargo.toml +++ b/chanlun-py/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "chanlun-py" -version = "26.5.94" +version = "26.5.101" edition = "2021" description = "缠论技术分析库 — Rust 高性能 Python 绑定" authors = ["YuYuKunKun"] @@ -12,7 +12,7 @@ crate-type = ["cdylib"] name = "chanlun" [dependencies] -chanlun = "26.5.4" # { path = "../chanlun" } -pyo3 = { version = "0.28", features = ["extension-module", "experimental-inspect"] } +chanlun = { path = "../chanlun" } +pyo3 = { version = "0.28", features = ["experimental-inspect"] } serde_json = "1" chrono = "0.4" diff --git a/chanlun-py/chanlun/__init__.py b/chanlun-py/chanlun/__init__.py index 6d17b33..b943aac 100644 --- a/chanlun-py/chanlun/__init__.py +++ b/chanlun-py/chanlun/__init__.py @@ -27,6 +27,8 @@ __all__ = [ "转化为时间戳", "转化为时间戳_数字", "随机指标", + "chan", ] from ._chanlun import * +from . import chan diff --git a/chanlun-py/chanlun/chan.py b/chanlun-py/chanlun/chan.py index 3e838d2..b75e6ec 100644 --- a/chanlun-py/chanlun/chan.py +++ b/chanlun-py/chanlun/chan.py @@ -5078,6 +5078,17 @@ class 观察者: self.扩展线段序列_扩展线段: List[虚线] = [] self.扩展中枢序列_扩展线段: List[中枢] = [] + def 投喂原始数据(self, 时间戳: datetime, 开: float, 高: float, 低: float, 收: float, 量: float): + """便捷入口,直接从 OHLCV 创建 K线 并投喂 + :param 时间戳: 时间戳 + :param 开: 开盘价 + :param 高: 最高价 + :param 低: 最低价 + :param 收: 收盘价 + :param 量: 成交量 + """ + self.增加原始K线(K线.创建普K(self.标识, 时间戳, 开, 高, 低, 收, 量, 0, self.周期)) + @final def 增加原始K线(self, 普K: K线): """核心入口 — 投喂一根原始K线,增量更新所有层级 @@ -5224,33 +5235,32 @@ class 观察者: self.配置.分析线段中枢 and 中枢.分析(self.线段_线段序列, self.线段_中枢序列) def 加载本地数据(self, 文件路径: str): + """重置基础序列后加载数据文件 + :param 文件路径: 数据文件路径 格式如: btcusd-300-1631772074-1632222374.nb + """ 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线) + 时间戳, 开盘价, 最高价, 最低价, 收盘价, 成交量 = struct.unpack(">6d", buffer[i * size: i * size + size]) + self.投喂原始数据(转化为时间戳(int(时间戳)), 开盘价, 最高价, 最低价, 收盘价, 成交量) @classmethod - def 读取数据文件(cls, 文件路径: str, 配置=缠论配置()) -> Self: - """ + def 读取数据文件(cls,观察员:"观察者", 文件路径: str, 配置=缠论配置()) -> Self: + """ 加载数据文件 + :param 观察员: 观察者 :param 文件路径: 数据文件路径 格式如: btcusd-300-1631772074-1632222374.nb :param 配置: 缠论配置 :return: 观察者实例 """ name = Path(文件路径).name.split(".")[0] 符号, 周期, 起始时间戳, 结束时间戳 = name.split("-") - 实例 = cls(符号=符号, 周期=int(周期), 配置=配置) - - 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], int(周期), 符号) - 实例.增加原始K线(k线) - - return 实例 + 观察员.符号 = 符号 + 观察员.周期 = int(周期) + 观察员.配置 = 配置 + 观察员.加载本地数据(文件路径) + return 观察员 class K线合成器: @@ -5503,16 +5513,16 @@ class 立体分析器: print(f"多级别数据拆分保存完成,目录:{保存路径.resolve()}") -def 测试_读取数据(配置: 缠论配置): +def 测试_读取数据(观察员: 观察者, 配置: 缠论配置)-> Callable[()]: """测试_读取数据 - + :param 观察员: 观察者 :param 配置: 缠论配置 :return: 测试函数 """ def 魔法(): 启动时间 = datetime.now() - 观察员 = 观察者.读取数据文件(配置.加载文件路径, 配置) + 观察者.读取数据文件(观察员, 配置.加载文件路径, 配置) 消耗用时 = datetime.now() - 启动时间 print("测试_读取数据 耗时", 消耗用时, "普K数量", len(观察员.普通K线序列)) return 观察员 @@ -5553,5 +5563,5 @@ if __name__ == "__main__": 当前配置 = 缠论配置.不推送() 当前配置.加载文件路径 = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "tests", "btcusd-300-1761327300-1776327900.nb") with tempfile.TemporaryDirectory() as tmpdir: - 测试_读取数据(当前配置)().测试_保存数据(tmpdir) + 测试_读取数据(观察者("", 0, 当前配置), 当前配置)().测试_保存数据(tmpdir) 测试_周期合成(当前配置)().测试_保存数据(tmpdir) diff --git a/chanlun-py/pyproject.toml b/chanlun-py/pyproject.toml index 095153d..3fae520 100644 --- a/chanlun-py/pyproject.toml +++ b/chanlun-py/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "chanlun" -version = "2605.94" +version = "2605.101" 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 2b383a8..bcaba57 100644 --- a/chanlun-py/src/algorithm_py.rs +++ b/chanlun-py/src/algorithm_py.rs @@ -1214,10 +1214,8 @@ impl 中枢Py { #[getter] /// :return: 中枢方向(首条虚线的方向翻转) - fn 方向(&self) -> 相对方向Py { - 相对方向Py { - inner: self.inner.方向(), - } + fn 方向(&self, py: Python<'_>) -> Py<相对方向Py> { + crate::types_py::获取相对方向单例(py, self.inner.方向()) } #[getter] @@ -1328,7 +1326,7 @@ impl 中枢Py { dict.set_item("标识", self.标识())?; dict.set_item("级别", self.级别())?; dict.set_item("图表标题", self.图表标题())?; - dict.set_item("方向", self.方向())?; + dict.set_item("方向", self.方向(py))?; dict.set_item("高", self.高())?; dict.set_item("低", self.低())?; dict.set_item("高高", self.高高())?; diff --git a/chanlun-py/src/business_py.rs b/chanlun-py/src/business_py.rs index 010dd66..1ae7907 100644 --- a/chanlun-py/src/business_py.rs +++ b/chanlun-py/src/business_py.rs @@ -560,30 +560,30 @@ impl 观察者Py { self.obs_mut().增加原始K线((*普K.borrow().inner).clone()); } + /// 投喂原始数据 — 便捷入口,直接从 OHLCV 创建 K线 并投喂 + fn 投喂原始数据( + &mut self, 时间戳: i64, 开: f64, 高: f64, 低: f64, 收: f64, 量: f64 + ) { + self.obs_mut().投喂原始数据(时间戳, 开, 高, 低, 收, 量); + } + /// 加载本地数据 — 从 .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: Arc::new(k线), - }, - )?; - slf.call_method1("增加原始K线", (k线_py,))?; + // 读取文件,通过 Python dispatch 逐根投喂(支持子类重写 增加原始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((时间戳, 开, 高, 低, 收, 量)) = + chanlun::kline::bar::K线::解析原始数据(&data[offset..offset + size]) + { + slf.call_method1("投喂原始数据", (时间戳, 开, 高, 低, 收, 量))?; + } } Ok(()) } @@ -600,10 +600,14 @@ impl 观察者Py { } #[classmethod] - #[pyo3(signature = (文件路径, 配置 = None))] + #[pyo3(signature = (观察员, 文件路径, 配置 = None))] + /// :param 观察员: 观察者实例 /// :param 文件路径: 数据文件路径 格式如: btcusd-300-1631772074-1632222374.nb + /// :param 配置: 缠论配置 + /// :return: 观察者实例 fn 读取数据文件( - cls: &Bound<'_, PyType>, + _cls: &Bound<'_, PyType>, + 观察员: &Bound<'_, Self>, 文件路径: &str, 配置: Option<&Bound<'_, 缠论配置Py>>, py: Python<'_>, @@ -631,31 +635,19 @@ impl 观察者Py { .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: Arc::new(k线), - }, - )?; - obj.call_method1("增加原始K线", (k线_py,))?; - } + // 设置观察员属性 + { + let slf_ref = 观察员.borrow_mut(); + let mut obs_mut = slf_ref.obs_mut(); + obs_mut.符号 = 符号; + obs_mut.周期 = 周期; + obs_mut.配置 = config; } - Ok(obj.unbind()) + // 调用加载本地数据 + 观察员.call_method1("加载本地数据", (文件路径,))?; + + Ok(观察员.clone().unbind().into()) } // ---- 序列 getters ---- diff --git a/chanlun-py/src/kline_py.rs b/chanlun-py/src/kline_py.rs index 7ef4734..6849404 100644 --- a/chanlun-py/src/kline_py.rs +++ b/chanlun-py/src/kline_py.rs @@ -139,10 +139,8 @@ impl K线Py { #[getter] /// :return: 相对方向.向上(开盘<收盘)或 相对方向.向下(开盘>收盘) - fn 方向(&self) -> 相对方向Py { - 相对方向Py { - inner: self.inner.方向(), - } + fn 方向(&self, py: Python<'_>) -> Py<相对方向Py> { + crate::types_py::获取相对方向单例(py, self.inner.方向()) } #[getter] @@ -182,7 +180,7 @@ impl K线Py { dict.set_item("开盘价", self.开盘价())?; dict.set_item("收盘价", self.收盘价())?; dict.set_item("成交量", self.成交量())?; - dict.set_item("方向", self.方向())?; + dict.set_item("方向", self.方向(py))?; if let Some(v) = self.macd() { dict.set_item("macd", v)?; } @@ -443,10 +441,8 @@ impl 缠论K线Py { } #[getter] - fn 方向(&self) -> 相对方向Py { - 相对方向Py { - inner: *self.inner.方向.read().unwrap(), - } + fn 方向(&self, py: Python<'_>) -> Py<相对方向Py> { + crate::types_py::获取相对方向单例(py, *self.inner.方向.read().unwrap()) } #[getter] @@ -496,7 +492,7 @@ impl 缠论K线Py { dict.set_item("时间戳", self.时间戳())?; dict.set_item("高", self.高())?; dict.set_item("低", self.低())?; - dict.set_item("方向", self.方向())?; + dict.set_item("方向", self.方向(py))?; dict.set_item("周期", self.周期())?; dict.set_item("标识", self.标识())?; dict.set_item("分型特征值", self.分型特征值())?; diff --git a/chanlun-py/src/lib.rs b/chanlun-py/src/lib.rs index e050027..89174ef 100644 --- a/chanlun-py/src/lib.rs +++ b/chanlun-py/src/lib.rs @@ -71,39 +71,40 @@ fn _chanlun(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { #[cfg(test)] mod tests { use crate::*; - use pyo3::prelude::*; #[test] - fn test_rc_pointer_across_getters() { - pyo3::prepare_freethreaded_python(); - Python::with_gil(|py| { + fn test_分型模式_get_set() { + // 手动初始化 Python 解释器(cargo test 环境下 auto-initialize 不一定生效) + unsafe { + if pyo3::ffi::Py_IsInitialized() == 0 { + pyo3::ffi::Py_Initialize(); + } + } + pyo3::Python::try_attach(|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(); + module + .add_function(wrap_pyfunction!(get_分型模式, &module).unwrap()) + .unwrap(); + module + .add_function(wrap_pyfunction!(set_分型模式, &module).unwrap()) + .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(); + // 默认 true + let getter = module.getattr("get_分型模式").unwrap(); + let result: bool = getter.call0().unwrap().extract().unwrap(); + assert!(result, "分型模式 默认应为 True"); - // 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 - }); + // 设置为 false + let setter = module.getattr("set_分型模式").unwrap(); + setter.call1((false,)).unwrap(); + let result: bool = getter.call0().unwrap().extract().unwrap(); + assert!(!result, "分型模式 应为 False"); + + // 恢复 true + setter.call1((true,)).unwrap(); + let result: bool = getter.call0().unwrap().extract().unwrap(); + assert!(result, "分型模式 应为 True"); + }) + .expect("Python 解释器初始化后 attach 仍失败"); } } diff --git a/chanlun-py/src/structure_py.rs b/chanlun-py/src/structure_py.rs index 1680af6..de96b52 100644 --- a/chanlun-py/src/structure_py.rs +++ b/chanlun-py/src/structure_py.rs @@ -233,12 +233,15 @@ impl 分型Py { #[getter] /// 左、中、右三对相对方向关系 - fn 关系组(&self) -> Option<(相对方向Py, 相对方向Py, 相对方向Py)> { + fn 关系组( + &self, + py: Python<'_>, + ) -> Option<(Py<相对方向Py>, Py<相对方向Py>, Py<相对方向Py>)> { self.inner.关系组().map(|(a, b, c)| { ( - 相对方向Py { inner: a }, - 相对方向Py { inner: b }, - 相对方向Py { inner: c }, + crate::types_py::获取相对方向单例(py, a), + crate::types_py::获取相对方向单例(py, b), + crate::types_py::获取相对方向单例(py, c), ) }) } @@ -271,7 +274,7 @@ impl 分型Py { if let Some(v) = self.右(py) { dict.set_item("右", v)?; } - if let Some(v) = self.关系组() { + if let Some(v) = self.关系组(py) { dict.set_item("关系组", v)?; } Ok(dict.into()) @@ -545,10 +548,8 @@ impl 虚线Py { #[getter] /// :return: 运行方向 - fn 方向(&self) -> 相对方向Py { - 相对方向Py { - inner: self.inner.方向(), - } + fn 方向(&self, py: Python<'_>) -> Py<相对方向Py> { + crate::types_py::获取相对方向单例(py, self.inner.方向()) } #[getter] @@ -1036,10 +1037,8 @@ impl 线段特征Py { } #[getter] - fn 线段方向(&self) -> 相对方向Py { - 相对方向Py { - inner: self.inner.线段方向, - } + fn 线段方向(&self, py: Python<'_>) -> Py<相对方向Py> { + crate::types_py::获取相对方向单例(py, self.inner.线段方向) } #[getter] @@ -1101,7 +1100,7 @@ impl 线段特征Py { let dict = PyDict::new(py); dict.set_item("序号", self.序号())?; dict.set_item("标识", self.标识())?; - dict.set_item("线段方向", self.线段方向())?; + dict.set_item("线段方向", self.线段方向(py))?; dict.set_item("图表标题", self.图表标题())?; Ok(dict.into()) } @@ -1128,10 +1127,8 @@ impl 线段特征Py { #[getter] /// :return: 特征序列方向(线段方向的翻转) - fn 方向(&self) -> 相对方向Py { - 相对方向Py { - inner: self.inner.方向(), - } + fn 方向(&self, py: Python<'_>) -> Py<相对方向Py> { + crate::types_py::获取相对方向单例(py, self.inner.方向()) } #[getter] diff --git a/chanlun-py/src/types_py.rs b/chanlun-py/src/types_py.rs index 6c58436..550c6aa 100644 --- a/chanlun-py/src/types_py.rs +++ b/chanlun-py/src/types_py.rs @@ -61,6 +61,40 @@ pub fn 获取分型结构单例( result } +static 相对方向_单例缓存: Mutex>>> = Mutex::new(None); + +pub fn 获取相对方向单例( + py: Python<'_>, + inner: chanlun::types::相对方向, +) -> Py<相对方向Py> { + let mut guard = 相对方向_单例缓存.lock().unwrap(); + if let Some(ref map) = *guard { + return map[&(inner as u8)].clone_ref(py); + } + + // 首次访问时从类属性加载单例 + let module = py.import("chanlun._chanlun").unwrap(); + let class = module.getattr("相对方向").unwrap(); + let mut map = HashMap::new(); + for (name, variant) in &[ + ("向上", chanlun::types::相对方向::向上), + ("向下", chanlun::types::相对方向::向下), + ("向上缺口", chanlun::types::相对方向::向上缺口), + ("向下缺口", chanlun::types::相对方向::向下缺口), + ("衔接向上", chanlun::types::相对方向::衔接向上), + ("衔接向下", chanlun::types::相对方向::衔接向下), + ("顺", chanlun::types::相对方向::顺), + ("逆", chanlun::types::相对方向::逆), + ("同", chanlun::types::相对方向::同), + ] { + let instance: Py<相对方向Py> = class.getattr(*name).unwrap().extract().unwrap(); + map.insert(*variant as u8, instance); + } + let result = map[&(inner as u8)].clone_ref(py); + *guard = Some(map); + result +} + // ========== 买卖点类型 ========== /// 买卖点类型 — 缠论的三类买卖点及扩展类型。 @@ -189,10 +223,8 @@ impl 相对方向Py { } /// 返回方向的对立面(向上↔向下, 缺口↔反向缺口, 衔接↔反向衔接)。 - fn 翻转(&self) -> Self { - Self { - inner: self.inner.翻转(), - } + fn 翻转(&self, py: Python<'_>) -> Py { + 获取相对方向单例(py, self.inner.翻转()) } /// 判断是否为向上方向(向上/向上缺口/衔接向上) @@ -245,10 +277,11 @@ impl 相对方向Py { #[classmethod] fn 分析( _cls: &Bound<'_, PyType>, 前高: f64, 前低: f64, 后高: f64, 后低: f64 - ) -> Self { - Self { - inner: chanlun::types::相对方向::分析(前高, 前低, 后高, 后低), - } + ) -> Py { + 获取相对方向单例( + _cls.py(), + chanlun::types::相对方向::分析(前高, 前低, 后高, 后低), + ) } } diff --git a/chanlun/src/business/observer.rs b/chanlun/src/business/observer.rs index 3a5b0f5..ed818a9 100644 --- a/chanlun/src/business/observer.rs +++ b/chanlun/src/business/observer.rs @@ -155,6 +155,14 @@ impl 观察者 { self.__处理数据(普K); } + /// 投喂原始数据 — 便捷入口,直接从 OHLCV 创建 K线 并投喂 + pub fn 投喂原始数据( + &mut self, 时间戳: i64, 开: f64, 高: f64, 低: f64, 收: f64, 量: f64 + ) { + let 普K = K线::创建普K(&self.符号, 时间戳, 开, 高, 低, 收, 量, 0, self.周期); + self.增加原始K线(普K); + } + /// 核心数据处理管道 fn __处理数据(&mut self, 普K: K线) { // Step 1: 缠论K线分析 (普K is consumed by 分析 as &mut) @@ -479,39 +487,26 @@ 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线); + let data = std::fs::read(文件路径).map_err(|e| format!("read file: {}", e))?; + let size = 48; + for i in 0..data.len() / size { + let offset = i * size; + if let Some((时间戳, 开, 高, 低, 收, 量)) = + K线::解析原始数据(&data[offset..offset + size]) + { + self.投喂原始数据(时间戳, 开, 高, 低, 收, 量); + } } Ok(()) } - /// 读取数据文件 — 从 .nb 文件加载数据 + /// 读取数据文件 — 更新当前观察者并加载 .nb 文件 pub fn 读取数据文件( - 文件路径: &str, - 配置: Option<缠论配置>, - ) -> Result>, String> { - let 配置 = 配置.unwrap_or_default(); - - // Parse filename: btcusd-300-1631772074-1632222374.nb + &mut self, 文件路径: &str, 配置: 缠论配置 + ) -> Result<(), String> { let path = std::path::Path::new(文件路径); let name = path .file_stem() @@ -521,23 +516,12 @@ impl 观察者 { if parts.len() < 4 { return Err(format!("invalid filename format: {}", name)); } - let 符号 = parts[0].to_string(); - let 周期: i64 = parts[1] + self.符号 = parts[0].to_string(); + self.周期 = parts[1] .parse() .map_err(|e| format!("parse period: {}", e))?; - - let 实例 = Self::new(符号, 周期, 配置); - - let data = std::fs::read(文件路径).map_err(|e| format!("read file: {}", e))?; - let size = 48; // 6 × 8 bytes (big-endian double) - for i in 0..data.len() / size { - let offset = i * size; - if let Some(k线) = K线::from_bytes(&data[offset..offset + size], 周期, "nb") { - 实例.write().unwrap().增加原始K线(k线); - } - } - - Ok(实例) + self.配置 = 配置; + self.加载本地数据(文件路径) } } @@ -546,12 +530,23 @@ mod tests { use super::*; use crate::config::缠论配置; - const TEST_DATA_PATH: &str = "/home/moscow/chanlun.rs/btcusd-300-1777649100-1778398800.nb"; + fn test_data_path() -> String { + let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); + manifest + .parent() + .unwrap() + .join("btcusd-300-1777649100-1778398800.nb") + .to_string_lossy() + .to_string() + } #[test] fn test_普k序列指针一致性() { - let config = 缠论配置::default(); - let obs = 观察者::读取数据文件(TEST_DATA_PATH, Some(config)).unwrap(); + let obs = 观察者::new("btcusd".into(), 300, Default::default()); + obs.write() + .unwrap() + .读取数据文件(&test_data_path(), Default::default()) + .unwrap(); let obs_ref = obs.read().unwrap(); for (i, bi) in obs_ref.笔序列.iter().enumerate() { @@ -595,7 +590,7 @@ mod tests { let config = 缠论配置::default(); let obs_ref = 观察者::new("btcusd".into(), 300, config); - let data = std::fs::read(TEST_DATA_PATH).unwrap(); + let data = std::fs::read(&test_data_path()).unwrap(); let size = 48; for i in 0..data.len() / size { @@ -633,8 +628,11 @@ mod tests { #[test] fn test_分型到笔的文武Rc指针一致性() { - let config = 缠论配置::default(); - let obs = 观察者::读取数据文件(TEST_DATA_PATH, Some(config)).unwrap(); + let obs = 观察者::new("btcusd".into(), 300, Default::default()); + obs.write() + .unwrap() + .读取数据文件(&test_data_path(), Default::default()) + .unwrap(); let obs_ref = obs.read().unwrap(); // 每个笔的文/武 分型 Rc 指针必须在 分型序列 中 @@ -663,8 +661,11 @@ mod tests { #[test] fn test_笔到线段的基础序列Rc指针一致性() { - let config = 缠论配置::default(); - let obs = 观察者::读取数据文件(TEST_DATA_PATH, Some(config)).unwrap(); + let obs = 观察者::new("btcusd".into(), 300, Default::default()); + obs.write() + .unwrap() + .读取数据文件(&test_data_path(), Default::default()) + .unwrap(); let obs_ref = obs.read().unwrap(); // 每个线段的基础序列中的笔 Rc 指针必须在 笔序列 中 @@ -685,8 +686,11 @@ mod tests { #[test] fn test_中枢基础序列与笔序列Rc指针一致() { - let config = 缠论配置::default(); - let obs = 观察者::读取数据文件(TEST_DATA_PATH, Some(config)).unwrap(); + let obs = 观察者::new("btcusd".into(), 300, Default::default()); + obs.write() + .unwrap() + .读取数据文件(&test_data_path(), Default::default()) + .unwrap(); let obs_ref = obs.read().unwrap(); for (i, hub) in obs_ref.笔_中枢序列.iter().enumerate() { @@ -716,7 +720,7 @@ mod tests { #[test] fn test_重复计算后结果一致() { - let data = std::fs::read(TEST_DATA_PATH).unwrap(); + let data = std::fs::read(&test_data_path()).unwrap(); let size = 48; let 计算 = || { @@ -760,7 +764,7 @@ mod tests { let config = 缠论配置::default(); let obs_ref = 观察者::new("btcusd".into(), 300, config); - let data = std::fs::read(TEST_DATA_PATH).unwrap(); + let data = std::fs::read(&test_data_path()).unwrap(); let size = 48; for i in 0..data.len() / size { @@ -800,8 +804,11 @@ mod tests { #[test] fn test_RefCell借用安全性_连续读取不panic() { - let config = 缠论配置::default(); - let obs = 观察者::读取数据文件(TEST_DATA_PATH, Some(config)).unwrap(); + let obs = 观察者::new("btcusd".into(), 300, Default::default()); + obs.write() + .unwrap() + .读取数据文件(&test_data_path(), Default::default()) + .unwrap(); let obs_ref = obs.read().unwrap(); // 连续大量读取所有 RefCell 字段,不应 panic @@ -831,8 +838,11 @@ mod tests { #[test] fn test_RefCell借用安全性_交替读写不panic() { - let config = 缠论配置::default(); - let obs = 观察者::读取数据文件(TEST_DATA_PATH, Some(config)).unwrap(); + let obs = 观察者::new("btcusd".into(), 300, Default::default()); + obs.write() + .unwrap() + .读取数据文件(&test_data_path(), Default::default()) + .unwrap(); let obs_ref = obs.read().unwrap(); // 交替读写 RefCell 字段 — 先读再写同字段,分离 borrow 作用域 @@ -860,8 +870,11 @@ mod tests { #[test] fn test_缠K到分型的Rc指针一致性() { - let config = 缠论配置::default(); - let obs = 观察者::读取数据文件(TEST_DATA_PATH, Some(config)).unwrap(); + let obs = 观察者::new("btcusd".into(), 300, Default::default()); + obs.write() + .unwrap() + .读取数据文件(&test_data_path(), Default::default()) + .unwrap(); let obs_ref = obs.read().unwrap(); // 每个分型的左/中/右 缠K 指针必须在 缠论K线序列 中 diff --git a/chanlun/src/kline/bar.rs b/chanlun/src/kline/bar.rs index 851ffe4..19896aa 100644 --- a/chanlun/src/kline/bar.rs +++ b/chanlun/src/kline/bar.rs @@ -125,6 +125,21 @@ impl K线 { Self::from_bytes(字节组, 周期, 标识) } + /// 解析原始数据 — 只提取时间戳+OHLCV,不构造 K线 + pub fn 解析原始数据(字节组: &[u8]) -> Option<(i64, f64, f64, f64, f64, f64)> { + if 字节组.len() < 48 { + return None; + } + let mut reader = &字节组[..48]; + let 时间戳 = reader.read_f64::().ok()? as i64; + let 开 = reader.read_f64::().ok()?; + let 高 = reader.read_f64::().ok()?; + let 低 = reader.read_f64::().ok()?; + let 收 = reader.read_f64::().ok()?; + let 量 = reader.read_f64::().ok()?; + Some((时间戳, 开, 高, 低, 收, 量)) + } + /// 创建普通K线 pub fn 创建普K( 标识: &str, diff --git a/chanlun/src/main.rs b/chanlun/src/main.rs index b38af6b..1e208e7 100644 --- a/chanlun/src/main.rs +++ b/chanlun/src/main.rs @@ -77,38 +77,36 @@ fn 测试_读取数据(文件路径: &str) { let 启动时间 = Instant::now(); let 配置 = 缠论配置::default().不推送(); - match 观察者::读取数据文件(文件路径, Some(配置)) { - Ok(观察员) => { - let 观察员 = 观察员.read().unwrap(); - let 消耗用时 = 启动时间.elapsed(); - println!( - "测试_读取数据 耗时 {:.2?} 普K数量 {}", - 消耗用时, - 观察员.普通K线序列.len() - ); - println!("符号: {}", 观察员.符号); - println!("周期: {}", 观察员.周期); - println!("缠K数量: {}", 观察员.缠论K线序列.len()); - println!("分型数量: {}", 观察员.分型序列.len()); - println!("笔数量: {}", 观察员.笔序列.len()); - println!("笔中枢数量: {}", 观察员.笔_中枢序列.len()); - println!("线段数量: {}", 观察员.线段序列.len()); - println!("中枢数量: {}", 观察员.中枢序列.len()); - println!("扩展线段数量: {}", 观察员.扩展线段序列.len()); - println!("线段_线段序列数量: {}", 观察员.线段_线段序列.len()); - println!( - "扩展线段_扩展线段数量: {}", - 观察员.扩展线段序列_扩展线段.len() - ); + let 观察员 = 观察者::new("".into(), 0, 缠论配置::default()); + 观察员 + .write() + .unwrap() + .读取数据文件(文件路径, 配置) + .expect("读取数据文件失败"); + let 观察员 = 观察员.read().unwrap(); + let 消耗用时 = 启动时间.elapsed(); + println!( + "测试_读取数据 耗时 {:.2?} 普K数量 {}", + 消耗用时, + 观察员.普通K线序列.len() + ); + println!("符号: {}", 观察员.符号); + println!("周期: {}", 观察员.周期); + println!("缠K数量: {}", 观察员.缠论K线序列.len()); + println!("分型数量: {}", 观察员.分型序列.len()); + println!("笔数量: {}", 观察员.笔序列.len()); + println!("笔中枢数量: {}", 观察员.笔_中枢序列.len()); + println!("线段数量: {}", 观察员.线段序列.len()); + println!("中枢数量: {}", 观察员.中枢序列.len()); + println!("扩展线段数量: {}", 观察员.扩展线段序列.len()); + println!("线段_线段序列数量: {}", 观察员.线段_线段序列.len()); + println!( + "扩展线段_扩展线段数量: {}", + 观察员.扩展线段序列_扩展线段.len() + ); - println!("\n===== 保存分析数据 =====\n"); - 观察员.测试_保存数据(None); - } - Err(e) => { - eprintln!("读取失败: {}", e); - std::process::exit(1); - } - } + println!("\n===== 保存分析数据 =====\n"); + 观察员.测试_保存数据(None); } /// 测试_周期合成 — 多周期合成分析 diff --git a/chanlun/src/types/sync_f64.rs b/chanlun/src/types/sync_f64.rs index 7adfe8b..65878fd 100644 --- a/chanlun/src/types/sync_f64.rs +++ b/chanlun/src/types/sync_f64.rs @@ -1,3 +1,27 @@ +/* + * MIT License + * + * Copyright (c) 2026 YuYuKunKun + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + use std::sync::atomic::{AtomicU64, Ordering}; /// f64 原子类型 — 基于 AtomicU64 + 位转换,API 与 `Cell` 一致。