diff --git a/README.md b/README.md index e6d4e66..9ddb0cc 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ import chanlun # 创建配置(全部默认值) config = chanlun.缠论配置() -# 读取 K 线数据文件(文件名需遵循 `符号-周期-起始时间戳-结束时间戳.nb` 格式,如 `btcusd-300-1631772074-1632222374.nb`) +# 读取 K 线数据文件(文件名需遵循 `符号-周期-起始时间戳-结束时间戳.nb` 格式) obs = chanlun.观察者.读取数据文件("path/to/btcusd-300-1631772074-1632222374.nb", config) # 查看各层级序列 @@ -33,6 +33,171 @@ analyzer = chanlun.立体分析器("BTCUSD", [60, 60*5, 60*5*6], config) # 逐根投喂 K 线... ``` +## 信号计算 (Rust 核心) + +信号框架(Signal/Factor/Event/Position/Operate)已全部迁移到 Rust 核心,通过 PyO3 暴露给 Python。 + +### 调用信号函数 + +```python +from chanlun._chanlun import 信号引擎, call_signal, list_signals, get_signal_template + +# 准备数据 +analyzer = chanlun.立体分析器("btcusd", [300, 900, 3600], chanlun.缠论配置()) +for k in klines: + analyzer.投喂K线(k) + +# ── 方式 1: 信号引擎(批量) ── +engine = 信号引擎(信号配置=[ + {"name": "bar_zdt_V230331", "freq": "300"}, + {"name": "macd_金叉_V260601", "freq": "300", "fast": "13", "slow": "31"}, +]) +engine.自动挂载指标(analyzer) +result = engine.更新(analyzer) # → {key: value} +full = engine.更新_完整(analyzer) # → {"signals": {...}, "market": {...}} + +# ── 方式 2: call_signal(单函数) ── +obs = analyzer._单体分析器[300] +signals = call_signal("macd_金叉_V260601", obs, {"freq": "5分钟", "di": "1"}) +for s in signals: + print(s.key, s.value) + +# ── 方式 3: SignalOrchestrator(高级编排,含行情) ── +from chanlun.signal_orchestrator import SignalOrchestrator +orch = SignalOrchestrator(analyzer, 信号配置=[...]) +orch.更新() +orch.信号字典 # → {信号..., "symbol": "btcusd", "close": 50050, ...} + +# ── 方式 4: 注册表探索 ── +list_signals() # → ["bar_zdt_V230331", ...] (7个) +get_signal_template("bar_zdt_V230331") # → "{freq}_D{di}_涨跌停V230331" +``` + +### 已注册信号函数(8个) + +| 信号名 | 模板 | 说明 | +|--------|------|------| +| `bar_zdt_V230331` | `{freq}_D{di}_涨跌停V230331` | 涨跌停检测 | +| `macd_金叉_V260601` | `{freq}_D{di}#MACD#{fast}#{slow}#{signal}_MACD交叉V260601` | MACD 金叉/死叉 | +| `tas_macd_direct_V221106` | `{freq}_D{di}#MACD#{fast}#{slow}#{signal}_MACD方向V221106` | MACD DIF 方向 | +| `tas_ma_base_V230313` | `{freq}_D{di}#{ma_type}#{timeperiod}MO{max_overlap}_BS辅助V230313` | MA 均线多空 | +| `cxt_停顿分型_V230106` | `{freq}_D{di}停顿分型_BE辅助V230106` | 停顿分型检测 | +| `cxt_bi_end_V230222` | `{freq}_D1MO{max_overlap}_BE辅助V230222` | 笔结束辅助 | +| `youwukuncheng_中枢第三买卖点_V230602` | `{freq}_D1MO{max_overlap}_中枢第三买卖点V230602` | 中枢第三买卖点 | + +### Python 信号函数混合调用 + +编排器支持 Rust + Python 信号混合执行: + +```python +orch = SignalOrchestrator(analyzer, 信号配置=[ + {"name": "bar_zdt_V230331", "freq": 300}, # → Rust 路径 + {"name": "chanlun.signals.demo.tas_ma_base_V230313", ...}, # → Python 回退 +]) +orch.更新() # 自动分类,Rust 批量 + Python 逐个 +``` + +## 编写信号函数 + +### Rust 信号函数(推荐) + +```rust +// chanlun/src/signal/functions/my_signals.rs +use chanlun_signal_macros::signal; +use chanlun::business::observer::观察者; +use chanlun::signal::Signal; +use std::collections::HashMap; +use serde_json::Value; + +#[signal( + name = "my_signal_V000001", + template = "{freq}_D{di}_模板V000001" +)] +pub fn my_signal_V000001(obs: &观察者, params: &HashMap) -> Vec { + obs.确保指标已计算(); + + let di = params.get("di").and_then(|v| v.as_i64()).unwrap_or(1) as usize; + let freq = params.get("freq").and_then(|v| v.as_str()).unwrap_or("日线"); + + let k1 = freq.to_string(); + let k2 = format!("D{di}"); + let k3 = "模板V000001"; + + let klines = &obs.普通K线序列; + if klines.len() < di + 1 { + return vec![Signal::new_empty(&k1, &k2, k3)]; + } + + let k线 = &klines[klines.len() - di]; + if k线.收盘价 > k线.开盘价 { + vec![Signal::new(&k1, &k2, k3, "阳线", "任意", "任意", 0)] + } else { + vec![Signal::new_empty(&k1, &k2, k3)] + } +} +``` + +然后在 `chanlun/src/signal/functions/mod.rs` 中添加 `pub mod my_signals;`,重新编译即可自动注册。 + +### 动态加载 .so 插件 + +信号函数可以编译为独立 `.so` 动态库,运行时加载。支持两种注册方式。 + +**方式 A:手动 C-ABI 注册** + +```rust +// 独立 crate (cdylib) +fn my_plugin_signal(obs: &观察者, params: &HashMap) -> Vec { ... } + +unsafe extern "C" { + fn chanlun_register_signal(name: *const c_char, template: *const c_char, func: SignalFn) -> i32; + fn chanlun_unregister_signal(name: *const c_char) -> i32; +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn init_plugin() -> i32 { + chanlun_register_signal( + c"my_plugin_signal_V000001".as_ptr(), + c"{freq}_D{di}_模板V000001".as_ptr(), + my_plugin_signal, + ) +} +``` + +**方式 B:`#[signal]` 宏 + inventory 批量提交** + +```rust +use chanlun_signal_macros::signal; + +#[signal(name = "my_plugin_signal_V000001", template = "...", crate_path = "::chanlun")] +fn my_plugin_signal_V000001(obs: &观察者, params: &HashMap) -> Vec { ... } + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn init_plugin() -> i32 { + for desc in inventory::iter:: { + chanlun_register_signal(name_ptr, template_ptr, desc.func); + } + 0 +} +``` + +Python 加载: + +```python +import ctypes, os, sys +sys.setdlopenflags(os.RTLD_LAZY | os.RTLD_GLOBAL) +import chanlun._chanlun # 先加载宿主 + +plugin = ctypes.CDLL("./libmy_plugin.so") +plugin.init_plugin() + +# 插件信号现在可通过 call_signal / 信号引擎 调用 +from chanlun._chanlun import call_signal +call_signal("my_plugin_signal_V000001", obs, params) +``` + +完整示例见 [examples/plugin-demo/](./examples/plugin-demo/)。 + ## 从源码构建 前置依赖: [Rust](https://www.rust-lang.org) + [maturin](https://www.maturin.rs) @@ -41,30 +206,34 @@ analyzer = chanlun.立体分析器("BTCUSD", [60, 60*5, 60*5*6], config) pip install maturin # 开发模式(直接安装到当前 venv) -maturin develop +cd chanlun-py && maturin develop # 或构建 wheel -maturin build --release +cd chanlun-py && maturin build --release pip install target/wheels/chanlun-*.whl ``` 也可使用项目内的 `build.sh`: ```bash +cd chanlun-py ./build.sh develop # 开发安装 ./build.sh wheel # 构建 wheel +./build.sh test # 运行集成测试 ``` ## 导出类 | 类别 | 类名 | 说明 | |------|------|------| -| 枚举 | `买卖点类型`, `相对方向`, `分型结构` | 缠论基础枚举 | +| 枚举 | `买卖点类型`, `相对方向`, `分型结构`, `Operate` | 缠论基础枚举 | | 数据 | `缺口`, `K线`, `缠论K线` | K 线数据结构 | | 结构 | `分型`, `虚线`, `线段特征`, `特征分型` | 分析层级结构 | | 指标 | `平滑异同移动平均线`, `相对强弱指数`, `随机指标` | MACD/RSI/KDJ | | 算法 | `笔`, `线段`, `中枢`, `背驰分析` | 识别算法 | -| 业务 | `缠论配置`, `基础买卖点`, `买卖点`, `观察者`, `K线合成器`, `立体分析器` | 分析框架 | +| 业务 | `缠论配置`, `观察者`, `K线合成器`, `立体分析器`, `买卖点` | 分析框架 | +| 信号 | `Signal`, `Factor`, `Event`, `Position`, `信号引擎` | 信号匹配+计算引擎 | +| 注册表 | `call_signal`, `list_signals`, `get_signal_template`, `register_signal`, `unregister_signal` | 信号发现+动态注册 | ## 兼容性 diff --git a/chan.py b/chan.py index 265f5d9..84e719b 100644 --- a/chan.py +++ b/chan.py @@ -20,6 +20,34 @@ 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. + +----------------------------------------------------------------------------- +第三方代码声明 / Third-Party Code Notice +----------------------------------------------------------------------------- + +本文件末尾 信号匹配框架(Signal / Factor / Event / Position / SignalsParser +等类)摘录自 czsc 项目(https://github.com/zengbin93/czsc), +根据 Apache License 2.0 授权使用。 + +原始许可协议全文见 https://www.apache.org/licenses/LICENSE-2.0 + +已做修改:中文命名适配、类型标注增强、与 chan 分析器集成的扩展。 + + +Copyright [2025] [zengbin93] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +----------------------------------------------------------------------------- """ # -*- coding: utf-8 -*- @@ -31,12 +59,17 @@ from __future__ import annotations import json import math import os +import re +from collections import deque, OrderedDict, defaultdict +import random import struct import sys import tempfile +import hashlib +import traceback import datetime as datetime_module from dataclasses import dataclass, field -from datetime import datetime +from datetime import datetime, timedelta from enum import Enum from functools import lru_cache from pathlib import Path @@ -52,9 +85,11 @@ from typing import ( Sequence, Callable, Set, + Generator, ) from loguru import logger +from parse import parse __all__ = [ "K线", @@ -97,6 +132,15 @@ __all__ = [ "虚线相等", "观察者相等", "立体分析器相等", + "import_by_name", + "Signal", + "Factor", + "Event", + "SignalsParser", + "get_signals_config", + "create_single_signal", + "Position", + "信号计算器", ] # 日志级别映射: 名称 → loguru 级别名 @@ -141,6 +185,9 @@ def get_log_level() -> str: return _当前日志级别 +set_log_level("error") + + @lru_cache(128) def K线相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]: """原始K线相等校验:字段完备→浮点容错→普通全等""" @@ -525,6 +572,26 @@ def 立体分析器相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]: return True, f"{标签}:所有周期观察者全量校验全部一致" +REGISTRY = {} + + +def 注册(obj): + """ + 通用装饰器:支持函数和类。 + obj 可以是 function,也可以是 class。 + """ + REGISTRY[obj.__name__] = obj + return obj + + +def 注入依赖(目标模块): + """批量注入到目标模块""" + for name, obj in REGISTRY.items(): + setattr(目标模块, name, obj) + logger.warning(f"成功自动注入: {list(REGISTRY.keys())}") + + +@注册 class 买卖点类型(str, Enum): """买卖点类型 — 缠论的三类买卖点及扩展类型。 @@ -578,6 +645,7 @@ class 买卖点类型(str, Enum): return "卖" in self.value +@注册 class 基础买卖点: """基础买卖点 — 描述偏离买入/卖出位置的程度。 @@ -670,6 +738,7 @@ class 基础买卖点: return self.买卖点分型.与MACD柱子分型匹配 +@注册 @final class 买卖点(基础买卖点): """一二三类买卖点及扩展类型(T1/T1P/T2/T2S/T3A/T3B)的构造器。 @@ -773,6 +842,7 @@ class 买卖点(基础买卖点): return 买卖点函数(买卖点分型, 当前缠K, 特征, 备注, 破位值) +@注册 class datetime(datetime): # 用于对齐C输出 def __str__(self): return f"{int(self.timestamp())}" @@ -784,6 +854,7 @@ class datetime(datetime): # 用于对齐C输出 return int(self.timestamp()) +@注册 def 转化为时间戳(ts: Union[str, datetime, int, float]) -> datetime: """ 将不同类型的时间戳转换为datetime对象(统一比较标准) @@ -806,6 +877,7 @@ def 转化为时间戳(ts: Union[str, datetime, int, float]) -> datetime: raise TypeError(f"不支持的时间戳类型: {type(ts)}") +@注册 def 转化为时间戳_数字(ts: Union[str, datetime, int, float]) -> int: """ 将不同类型的时间戳转换为整数秒级时间戳 @@ -831,6 +903,7 @@ class ValidationError(Exception): pass +@注册 @final class 缠论配置: """控制缠论分析各阶段行为的全局参数集。 @@ -843,7 +916,7 @@ class 缠论配置: **[线段]** 线段_特征序列忽视老阴老阳, 线段_缺口后紧急修正, 线段内部中枢图显 等 \\ **[分析开关]** 分析笔, 分析线段, 分析扩展线段, 分析笔中枢, 分析线段中枢 \\ **[指标]** 计算指标, 指标计算方式, MACD/RSI/KDJ 参数 \\ - **[推送/显示]** 图表展示, 推送K线/笔/线段/中枢 等 \\ + **[推送/显示]** 图表展示 (主开关), 图表展示标签 (标签列表) 等 \\ **[买卖点]** 买卖点偏移, 买卖点激进识别, 买卖点_背离率, 买卖点_计算方式 等 \\ **[背驰]** 线段内部背驰_MACD, 线段内部背驰_斜率 等 \\ **[其他]** 手动终止, 加载文件路径 @@ -882,45 +955,15 @@ class 缠论配置: 分析线段中枢: bool = True, 手动终止: str = "", # 2099-12-31 00:00:00 计算指标: bool = True, - 指标计算方式: str = "收", # (开, 高, 低, 收, 高低均值, 高低收均值, 开高低收均值), 默认 收盘价 - 平滑异同移动平均线_快线周期: int = 13, - 平滑异同移动平均线_慢线周期: int = 31, - 平滑异同移动平均线_信号周期: int = 11, - 相对强弱指数_周期: int = 13, - 相对强弱指数_移动平均线周期: int = 13, - 相对强弱指数_超买阈值: float = 75.0, - 相对强弱指数_超卖阈值: float = 25.0, - 随机指标_RSV周期: int = 13, - 随机指标_K值平滑周期: int = 5, - 随机指标_D值平滑周期: int = 5, - 随机指标_超买阈值: float = 80.0, - 随机指标_超卖阈值: float = 20.0, - 计算BOLL: bool = False, - 布林带_周期: int = 20, - 布林带_标准差倍数: float = 2.0, - 均线_类型列表: List[str] = None, - 均线_周期列表: List[int] = None, + 指标计算方式: str = "收", # 均线计算方式 + 均线参数列表: List[tuple] = None, # [(key, 计算方式, 类型, 周期), ...] # 多参数指标列表(None/空列表 = 使用默认单参数) - MACD_参数列表: List[tuple] = None, # [(key, 快线, 慢线, 信号), ...] - RSI_周期列表: List[tuple] = None, # [(key, 周期), ...] - KDJ_参数列表: List[tuple] = None, # [(key, RSV周期, K平滑, D平滑), ...] - BOLL_参数列表: List[tuple] = None, # [(key, 周期, 标准差倍数), ...] - 图表展示: bool = True, - 推送K线: bool = True, - 推送笔: bool = True, - 推送线段: bool = True, - 推送中枢: bool = True, - 图表展示_笔: bool = True, - 图表展示_线段: bool = True, - 图表展示_扩展线段: bool = True, - 图表展示_扩展线段_线段: bool = True, - 图表展示_线段_线段: bool = True, - 图表展示_中枢_笔: bool = True, - 图表展示_中枢_线段: bool = True, - 图表展示_中枢_扩展线段: bool = True, - 图表展示_中枢_扩展线段_线段: bool = True, - 图表展示_中枢_线段_线段: bool = True, - 图表展示_中枢_线段内部: bool = True, + MACD_参数列表: List[tuple] = None, # [(key, 计算方式, 快线, 慢线, 信号), ...] + RSI_周期列表: List[tuple] = None, # [(key, 计算方式, 周期), ...] + KDJ_参数列表: List[tuple] = None, # [(key, 计算方式, RSV周期, K平滑, D平滑), ...] + BOLL_参数列表: List[tuple] = None, # [(key, 计算方式, 周期, 标准差倍数), ...] + 图表展示: bool = True, # 图表系统主开关 + 图表展示标签: Optional[List[str]] = None, # None=全部展示, []=不展示, [\"笔\",\"线段\"]=指定 买卖点偏移: int = 1, # 最大偏移 买卖点激进识别: bool = False, # 激进模式下将不考虑分型的完整性 买卖点与MACD柱强相关: bool = False, # True: 卖点需正值 买点需负值 @@ -960,43 +1003,13 @@ class 缠论配置: self.手动终止 = 手动终止 self.计算指标 = 计算指标 self.指标计算方式 = 指标计算方式 - self.平滑异同移动平均线_快线周期 = 平滑异同移动平均线_快线周期 - self.平滑异同移动平均线_慢线周期 = 平滑异同移动平均线_慢线周期 - self.平滑异同移动平均线_信号周期 = 平滑异同移动平均线_信号周期 - self.相对强弱指数_周期 = 相对强弱指数_周期 - self.相对强弱指数_移动平均线周期 = 相对强弱指数_移动平均线周期 - self.相对强弱指数_超买阈值 = 相对强弱指数_超买阈值 - self.相对强弱指数_超卖阈值 = 相对强弱指数_超卖阈值 - self.随机指标_RSV周期 = 随机指标_RSV周期 - self.随机指标_K值平滑周期 = 随机指标_K值平滑周期 - self.随机指标_D值平滑周期 = 随机指标_D值平滑周期 - self.随机指标_超买阈值 = 随机指标_超买阈值 - self.随机指标_超卖阈值 = 随机指标_超卖阈值 - self.计算BOLL = 计算BOLL - self.布林带_周期 = 布林带_周期 - self.布林带_标准差倍数 = 布林带_标准差倍数 - self.均线_类型列表 = 均线_类型列表 if 均线_类型列表 is not None else [] - self.均线_周期列表 = 均线_周期列表 if 均线_周期列表 is not None else [] - self.MACD_参数列表 = MACD_参数列表 if MACD_参数列表 is not None else [] - self.RSI_周期列表 = RSI_周期列表 if RSI_周期列表 is not None else [] - self.KDJ_参数列表 = KDJ_参数列表 if KDJ_参数列表 is not None else [] - self.BOLL_参数列表 = BOLL_参数列表 if BOLL_参数列表 is not None else [] + self.均线参数列表 = 均线参数列表 if 均线参数列表 is not None else [] + self.MACD_参数列表 = MACD_参数列表 if MACD_参数列表 is not None else [("macd", "收", 13, 31, 11)] + self.RSI_周期列表 = RSI_周期列表 if RSI_周期列表 is not None else [("rsi", "收", 14, 13, 75.0, 25.0)] + self.KDJ_参数列表 = KDJ_参数列表 if KDJ_参数列表 is not None else [("kdj", "收", 13, 5, 5, 80.0, 20.0)] + self.BOLL_参数列表 = BOLL_参数列表 if BOLL_参数列表 is not None else [("boll", "收", 20, 2.0)] self.图表展示 = 图表展示 - self.推送K线 = 推送K线 - self.推送笔 = 推送笔 - self.推送线段 = 推送线段 - self.推送中枢 = 推送中枢 - self.图表展示_笔 = 图表展示_笔 - self.图表展示_线段 = 图表展示_线段 - self.图表展示_扩展线段 = 图表展示_扩展线段 - self.图表展示_扩展线段_线段 = 图表展示_扩展线段_线段 - self.图表展示_线段_线段 = 图表展示_线段_线段 - self.图表展示_中枢_笔 = 图表展示_中枢_笔 - self.图表展示_中枢_线段 = 图表展示_中枢_线段 - self.图表展示_中枢_扩展线段 = 图表展示_中枢_扩展线段 - self.图表展示_中枢_扩展线段_线段 = 图表展示_中枢_扩展线段_线段 - self.图表展示_中枢_线段_线段 = 图表展示_中枢_线段_线段 - self.图表展示_中枢_线段内部 = 图表展示_中枢_线段内部 + self.图表展示标签 = set(图表展示标签) if 图表展示标签 is not None else None self.买卖点偏移 = 买卖点偏移 self.买卖点激进识别 = 买卖点激进识别 self.买卖点与MACD柱强相关 = 买卖点与MACD柱强相关 @@ -1046,43 +1059,13 @@ class 缠论配置: "手动终止": {"annotation": str, "default": ""}, "计算指标": {"annotation": bool, "default": True}, "指标计算方式": {"annotation": str, "default": "收"}, - "平滑异同移动平均线_快线周期": {"annotation": int, "default": 13}, - "平滑异同移动平均线_慢线周期": {"annotation": int, "default": 31}, - "平滑异同移动平均线_信号周期": {"annotation": int, "default": 11}, - "相对强弱指数_周期": {"annotation": int, "default": 13}, - "相对强弱指数_移动平均线周期": {"annotation": int, "default": 13}, - "相对强弱指数_超买阈值": {"annotation": float, "default": 75.0}, - "相对强弱指数_超卖阈值": {"annotation": float, "default": 25.0}, - "随机指标_RSV周期": {"annotation": int, "default": 13}, - "随机指标_K值平滑周期": {"annotation": int, "default": 5}, - "随机指标_D值平滑周期": {"annotation": int, "default": 5}, - "随机指标_超买阈值": {"annotation": float, "default": 80.0}, - "随机指标_超卖阈值": {"annotation": float, "default": 20.0}, - "计算BOLL": {"annotation": bool, "default": False}, - "布林带_周期": {"annotation": int, "default": 20}, - "布林带_标准差倍数": {"annotation": float, "default": 2.0}, - "均线_类型列表": {"annotation": List[str], "default": []}, - "均线_周期列表": {"annotation": List[int], "default": []}, - "MACD_参数列表": {"annotation": List[tuple], "default": []}, - "RSI_周期列表": {"annotation": List[tuple], "default": []}, - "KDJ_参数列表": {"annotation": List[tuple], "default": []}, - "BOLL_参数列表": {"annotation": List[tuple], "default": []}, + "均线参数列表": {"annotation": List[tuple], "default": []}, + "MACD_参数列表": {"annotation": List[tuple], "default": [("macd", "收", 13, 31, 11)]}, + "RSI_周期列表": {"annotation": List[tuple], "default": [("rsi", "收", 14, 13, 75.0, 25.0)]}, + "KDJ_参数列表": {"annotation": List[tuple], "default": [("kdj", "收", 13, 5, 5, 80.0, 20.0)]}, + "BOLL_参数列表": {"annotation": List[tuple], "default": [("boll", "收", 20, 2.0)]}, "图表展示": {"annotation": bool, "default": True}, - "推送K线": {"annotation": bool, "default": True}, - "推送笔": {"annotation": bool, "default": True}, - "推送线段": {"annotation": bool, "default": True}, - "推送中枢": {"annotation": bool, "default": True}, - "图表展示_笔": {"annotation": bool, "default": True}, - "图表展示_线段": {"annotation": bool, "default": True}, - "图表展示_扩展线段": {"annotation": bool, "default": True}, - "图表展示_扩展线段_线段": {"annotation": bool, "default": True}, - "图表展示_线段_线段": {"annotation": bool, "default": True}, - "图表展示_中枢_笔": {"annotation": bool, "default": True}, - "图表展示_中枢_线段": {"annotation": bool, "default": True}, - "图表展示_中枢_扩展线段": {"annotation": bool, "default": True}, - "图表展示_中枢_扩展线段_线段": {"annotation": bool, "default": True}, - "图表展示_中枢_线段_线段": {"annotation": bool, "default": True}, - "图表展示_中枢_线段内部": {"annotation": bool, "default": True}, + "图表展示标签": {"annotation": Optional[List[str]], "default": None}, "买卖点偏移": {"annotation": int, "default": 1}, "买卖点激进识别": {"annotation": bool, "default": False}, "买卖点与MACD柱强相关": {"annotation": bool, "default": False}, @@ -1111,10 +1094,13 @@ class 缠论配置: default = field_info["default"] try: - # 布尔类型验证 + # 布尔类型验证(与 Rust 绑定层 coerce_strings_to_numbers 对齐) if type_ is bool: if not isinstance(value, bool): - setattr(self, fname, bool(value)) + if isinstance(value, str) and value.lower() in ("true", "false"): + setattr(self, fname, value.lower() == "true") + else: + setattr(self, fname, default) # 整数类型验证 elif type_ is int: @@ -1131,27 +1117,45 @@ class 缠论配置: logger.warning(f"[{fname}] = {value} 解析失败,使用默认值:{default}") setattr(self, fname, default) - # ---- 参数列表解析:多参数列表为空时回退到默认单参数 ---- + def 设置指标( + self, + *, + 均线: List[tuple] = None, + MACD: List[tuple] = None, + RSI: List[tuple] = None, + KDJ: List[tuple] = None, + BOLL: List[tuple] = None, + ): + """统一设置所有指标参数。 - def _解析MACD参数列表(self) -> List[tuple]: - if self.MACD_参数列表: - return self.MACD_参数列表 - return [("macd", self.平滑异同移动平均线_快线周期, self.平滑异同移动平均线_慢线周期, self.平滑异同移动平均线_信号周期)] + 元组格式 ``(key, 计算方式, *params)``: + - 均线: ``("SMA_5", "收", "SMA", 5)`` — key/计算方式/类型/周期 + - MACD: ``("默认", "收", 13, 31, 11)`` — 快线/慢线/信号 + - RSI: ``("默认", "收", 14, 13, 75, 25)`` — 周期/MA周期/超买/超卖 + - KDJ: ``("默认", "收", 13, 5, 5, 80, 20)`` — RSV/K平滑/D平滑/超买/超卖 + - BOLL: ``("默认", "收", 20, 2.0)`` — 周期/标准差倍数 - def _解析RSI周期列表(self) -> List[tuple]: - if self.RSI_周期列表: - return self.RSI_周期列表 - return [("rsi", self.相对强弱指数_周期)] + :param 均线: key 即均线名(如 ``"SMA_5"``),同时编码类型和周期 + :param MACD: 首个 key 同时写入 ``"macd"`` 兼容槽位 + :param BOLL: BOLL 参数元组列表,为空则不计算 + """ + self.计算指标 = True + if 均线 is not None: + self.均线参数列表 = 均线 + if MACD is not None: + self.MACD_参数列表 = MACD + if RSI is not None: + self.RSI_周期列表 = RSI + if KDJ is not None: + self.KDJ_参数列表 = KDJ + if BOLL is not None: + self.BOLL_参数列表 = BOLL - def _解析KDJ参数列表(self) -> List[tuple]: - if self.KDJ_参数列表: - return self.KDJ_参数列表 - return [("kdj", self.随机指标_RSV周期, self.随机指标_K值平滑周期, self.随机指标_D值平滑周期)] - - def _解析BOLL参数列表(self) -> List[tuple]: - if self.BOLL_参数列表: - return self.BOLL_参数列表 - return [("boll", self.布林带_周期, self.布林带_标准差倍数)] + def 展示标签(self, 标签: str) -> bool: + """判断指定标签是否应展示。None = 全部展示,空列表 = 全部隐藏。""" + if self.图表展示标签 is None: + return True + return 标签 in self.图表展示标签 @classmethod def 兼容旧版本配置(cls, values: Dict[str, Any]) -> Dict[str, Any]: @@ -1161,7 +1165,22 @@ class 缠论配置: :return: 仅保留当前类已知字段的字典 """ valid_fields = cls.model_fields().keys() - cleaned = {k: v for k, v in values.items() if k in valid_fields} + cleaned = {} + for k, v in values.items(): + if k not in valid_fields: + continue + # 字符串值类型强制转换(与 Rust 绑定层 coerce_strings_to_numbers 对齐) + if isinstance(v, str): + if v.lower() in ("true", "false"): + v = v.lower() == "true" + elif v.lstrip("-").isdigit(): + v = int(v) + else: + try: + v = float(v) + except ValueError: + pass + cleaned[k] = v return cleaned def to_dict(self) -> dict: @@ -1169,7 +1188,13 @@ class 缠论配置: :return: 包含所有配置字段的字典 """ - return {k: getattr(self, k) for k in self.model_fields().keys()} + result = {} + for k in self.model_fields().keys(): + v = getattr(self, k) + if isinstance(v, set): + v = list(v) + result[k] = v + return result def to_json(self) -> str: """将配置序列化为 JSON 字符串。 @@ -1225,21 +1250,7 @@ class 缠论配置: return cls( 线段内部中枢图显=False, 图表展示=False, - 推送K线=False, - 推送笔=False, - 推送线段=False, - 推送中枢=False, - 图表展示_笔=False, - 图表展示_线段=False, - 图表展示_扩展线段=False, - 图表展示_扩展线段_线段=False, - 图表展示_线段_线段=False, - 图表展示_中枢_笔=False, - 图表展示_中枢_线段=False, - 图表展示_中枢_扩展线段=False, - 图表展示_中枢_扩展线段_线段=False, - 图表展示_中枢_线段_线段=False, - 图表展示_中枢_线段内部=False, + 图表展示标签=[], ) def model_copy(self, update: dict = None, deep: bool = True): @@ -1302,6 +1313,7 @@ class 缠论配置: return diff_dict +@注册 class 相对方向(Enum): """相对方向 — 描述两个K线/分型之间相对位置关系的枚举。 @@ -1425,7 +1437,25 @@ class 相对方向(Enum): return 相对方向.逆 raise RuntimeError("无法识别的方向") + @classmethod + def 从序列中机选( + cls, + 数量: int, + 可选方向: List["相对方向"], + 可重复: bool = True, # 是否允许重复选择 + ) -> Generator["相对方向", None, None]: + if not 可重复 and 数量 > len(可选方向): + raise ValueError("数量超过可选方向数") + if 可重复: + while 数量 > 0: + yield random.choice(可选方向) + 数量 -= 1 + else: + yield from random.sample(可选方向, 数量) + + +@注册 class 分型结构(Enum): """描述三根K线构成的顶底分型形态。 @@ -1499,6 +1529,7 @@ class 分型结构(Enum): return None +@注册 @final class 缺口: """缺口 — 描述价格区间之间的缺口(未重叠部分)。 @@ -1641,6 +1672,9 @@ class 平滑异同移动平均线: self.慢线EMA = 慢线EMA self.DEA_EMA = DEA_EMA + def __repr__(self): + return f"平滑异同移动平均线(时间戳={self.时间戳}, 收盘价={self.收盘价}, 快线周期={self.快线周期}, 慢线周期={self.慢线周期}, 信号周期={self.信号周期}, DIF={self.DIF}, DEA={self.DEA}, MACD柱={self.MACD柱}, 快线EMA={self.快线EMA}, 慢线EMA={self.慢线EMA}, DEA_EMA={self.DEA_EMA})" + @classmethod def 首次计算(cls, 初始收盘价: float, 初始时间: datetime, 快线周期: int = 12, 慢线周期: int = 26, 信号周期: int = 9) -> 平滑异同移动平均线: """ @@ -1672,12 +1706,12 @@ class 平滑异同移动平均线: 快线周期=快线周期, 慢线周期=慢线周期, 信号周期=信号周期, - DIF=DIF, - DEA=DEA_EMA, - MACD柱=MACD柱, - 快线EMA=快线EMA, - 慢线EMA=慢线EMA, - DEA_EMA=DEA_EMA, + DIF=round(DIF, 2), + DEA=round(DEA_EMA, 2), + MACD柱=round(MACD柱, 2), + 快线EMA=round(快线EMA, 2), + 慢线EMA=round(慢线EMA, 2), + DEA_EMA=round(DEA_EMA, 2), ) @classmethod @@ -1743,12 +1777,12 @@ class 平滑异同移动平均线: 快线周期=前一个MACD.快线周期, 慢线周期=前一个MACD.慢线周期, 信号周期=前一个MACD.信号周期, - DIF=DIF, - DEA=DEA_EMA, - MACD柱=MACD柱, - 快线EMA=快线EMA, - 慢线EMA=慢线EMA, - DEA_EMA=DEA_EMA, + DIF=round(DIF, 2), + DEA=round(DEA_EMA, 2), + MACD柱=round(MACD柱, 2), + 快线EMA=round(快线EMA, 2), + 慢线EMA=round(慢线EMA, 2), + DEA_EMA=round(DEA_EMA, 2), ) @classmethod @@ -1797,10 +1831,11 @@ class 相对强弱指数: 下跌幅度: float = 0.0, 平滑系数: float = 0.0, RSI_SMA: Optional[float] = None, - RSI历史队列: List[float] = None, + RSI历史队列: Optional[deque[float]] = None, + RSI和: float = 0.0, ): if RSI历史队列 is None: - RSI历史队列 = [] + RSI历史队列 = deque() # 原始数据 self.时间戳 = 时间戳 @@ -1829,6 +1864,7 @@ class 相对强弱指数: # RSI的SMA(信号线)相关字段 self.RSI_SMA = RSI_SMA self.RSI历史队列 = RSI历史队列 + self.RSI和 = RSI和 @classmethod def 首次计算(cls, 初始收盘价: float, 初始时间: datetime, 周期: int = 14, 超买阈值: float = 70.0, 超卖阈值: float = 30.0, RSI_SMA周期: Optional[int] = None) -> 相对强弱指数: @@ -1916,19 +1952,18 @@ class 相对强弱指数: # ----- 计算RSI的SMA(简单移动平均) ----- RSI_SMA = None - 历史队列 = 前一个RSI.RSI历史队列.copy() if 前一个RSI.RSI历史队列 else [] + 历史队列 = 前一个RSI.RSI历史队列.copy() if 前一个RSI.RSI历史队列 else deque() + RSI和 = 前一个RSI.RSI和 if RSI_SMA周期 is not None and RSI_SMA周期 > 0 and RSI is not None: - # 将当前RSI加入队列 历史队列.append(RSI) - # 保持队列长度不超过周期 + RSI和 += RSI if len(历史队列) > RSI_SMA周期: - 历史队列.pop(0) - # 计算SMA(即使队列未满也计算当前平均值) + RSI和 -= 历史队列.popleft() if 历史队列: - RSI_SMA = sum(历史队列) / len(历史队列) + RSI_SMA = RSI和 / len(历史队列) else: - # 未启用SMA,清空队列 - 历史队列 = [] + 历史队列 = deque() + RSI和 = 0.0 return cls( 时间戳=当前时间, @@ -1945,6 +1980,7 @@ class 相对强弱指数: RSI_SMA周期=RSI_SMA周期, RSI_SMA=RSI_SMA, RSI历史队列=历史队列, + RSI和=RSI和, ) @classmethod @@ -1996,16 +2032,16 @@ class 随机指标: K: Optional[float] = None, D: Optional[float] = None, J: Optional[float] = None, - 历史最高价队列: list[float] = None, - 历史最低价队列: list[float] = None, + 历史最高价队列: Optional[deque[float]] = None, + 历史最低价队列: Optional[deque[float]] = None, 前一个RSV: Optional[float] = None, 前一个K: Optional[float] = None, 前一个D: Optional[float] = None, ): if 历史最高价队列 is None: - 历史最高价队列 = [] + 历史最高价队列 = deque() if 历史最低价队列 is None: - 历史最低价队列 = [] + 历史最低价队列 = deque() # 原始数据 self.时间戳 = 时间戳 @@ -2065,8 +2101,8 @@ class 随机指标: K=None, D=None, J=None, - 历史最高价队列=[初始最高价], - 历史最低价队列=[初始最低价], + 历史最高价队列=deque([初始最高价]), + 历史最低价队列=deque([初始最低价]), 前一个RSV=None, 前一个K=None, 前一个D=None, @@ -2113,13 +2149,13 @@ class 随机指标: 历史最高价 = 前一个KDJ.历史最高价队列.copy() 历史最高价.append(当前最高价) if len(历史最高价) > N: - 历史最高价.pop(0) + 历史最高价.popleft() # 更新历史最低价队列 历史最低价 = 前一个KDJ.历史最低价队列.copy() 历史最低价.append(当前最低价) if len(历史最低价) > N: - 历史最低价.pop(0) + 历史最低价.popleft() # 计算RSV(需要队列长度达到N才能计算) RSV = None @@ -2218,7 +2254,7 @@ class 布林带: self.上轨 = 上轨 self.中轨 = 中轨 self.下轨 = 下轨 - self._历史队列 = 历史队列 if 历史队列 is not None else [] + self._历史队列 = 历史队列 if 历史队列 is not None else deque() self._均值 = _均值 self._方差和 = _方差和 @@ -2233,7 +2269,7 @@ class 布林带: :return: 初始的布林带实例 """ 价格 = 指标.K线取值(k线, 计算方式) - return cls(时间戳=k线.时间戳, 周期=周期, 标准差倍数=标准差倍数, 上轨=价格, 中轨=价格, 下轨=价格, 历史队列=[价格]) + return cls(时间戳=k线.时间戳, 周期=周期, 标准差倍数=标准差倍数, 上轨=价格, 中轨=价格, 下轨=价格, 历史队列=deque([价格])) @classmethod def 增量计算(cls, prev: 布林带, 当前K线: K线, 计算方式: str) -> 布林带: @@ -2251,7 +2287,7 @@ class 布林带: q = prev._历史队列.copy() q.append(当前价) if len(q) > 周期: - q.pop(0) + q.popleft() # 增量均值和方差 if len(q) < 周期: @@ -2436,19 +2472,21 @@ class 指标计算器: @staticmethod def _计算MACD组(当前K线: K线, prev: Optional[指标容器], 配置: 缠论配置): idx = 当前K线.指标 - 计算方式 = 配置.指标计算方式 - for key, 快, 慢, 信号 in 配置._解析MACD参数列表(): + for i, (key, 计算方式, *params) in enumerate(配置.MACD_参数列表): + 快, 慢, 信号 = params[0], params[1], params[2] prev_val = prev[key] if prev is not None and key in prev else None if prev_val is not None: idx[key] = 平滑异同移动平均线.增量计算_K线(prev_val, 当前K线, 计算方式) else: idx[key] = 平滑异同移动平均线.首次计算_K线(当前K线, 计算方式, 快, 慢, 信号) + if i == 0: + idx["macd"] = idx[key] @staticmethod def _计算RSI组(当前K线: K线, prev: Optional[指标容器], 配置: 缠论配置): idx = 当前K线.指标 - 计算方式 = 配置.指标计算方式 - for key, 周期 in 配置._解析RSI周期列表(): + for key, 计算方式, *params in 配置.RSI_周期列表: + 周期, MA周期, 超买, 超卖 = params[0], params[1], params[2], params[3] prev_val = prev[key] if prev is not None and key in prev else None if prev_val is not None: idx[key] = 相对强弱指数.增量计算_K线(prev_val, 当前K线, 计算方式) @@ -2457,16 +2495,16 @@ class 指标计算器: 当前K线, 计算方式, 周期, - 配置.相对强弱指数_超买阈值, - 配置.相对强弱指数_超卖阈值, - 配置.相对强弱指数_移动平均线周期, + 超买, + 超卖, + MA周期, ) @staticmethod def _计算KDJ组(当前K线: K线, prev: Optional[指标容器], 配置: 缠论配置): idx = 当前K线.指标 - 计算方式 = 配置.指标计算方式 - for key, rsv, k平滑, d平滑 in 配置._解析KDJ参数列表(): + for key, 计算方式, *params in 配置.KDJ_参数列表: + rsv, k平滑, d平滑, 超买, 超卖 = params[0], params[1], params[2], params[3], params[4] prev_val = prev[key] if prev is not None and key in prev else None if prev_val is not None: idx[key] = 随机指标.增量计算_K线(prev_val, 当前K线, 计算方式) @@ -2477,15 +2515,15 @@ class 指标计算器: rsv, k平滑, d平滑, - 配置.随机指标_超买阈值, - 配置.随机指标_超卖阈值, + 超买, + 超卖, ) @staticmethod def _计算BOLL组(当前K线: K线, prev: Optional[指标容器], 配置: 缠论配置): idx = 当前K线.指标 - 计算方式 = 配置.指标计算方式 - for key, 周期, 标准差倍数 in 配置._解析BOLL参数列表(): + for key, 计算方式, *params in 配置.BOLL_参数列表: + 周期, 标准差倍数 = params[0], params[1] prev_val = prev[key] if prev is not None and key in prev else None if prev_val is not None: idx[key] = 布林带.增量计算(prev_val, 当前K线, 计算方式) @@ -2494,18 +2532,16 @@ class 指标计算器: @staticmethod def _更新均线(当前K线: K线, 普K序列: List[K线], 配置: 缠论配置): - if not 配置.均线_类型列表 or not 配置.均线_周期列表: + if not 配置.均线参数列表: return - for ma_type in 配置.均线_类型列表: - for period in 配置.均线_周期列表: - key = f"{ma_type}_{period}" - if ma_type == "SMA": - 当前K线.指标.均线[key] = 均线工具.增量SMA(普K序列, period, 配置.指标计算方式) - elif ma_type == "EMA": - 前值 = None - if len(普K序列) >= 2: - 前值 = 普K序列[-2].指标.均线.get(key) - 当前K线.指标.均线[key] = 均线工具.增量EMA(普K序列, period, 配置.指标计算方式, 前值) + for key, 计算方式, ma_type, period in 配置.均线参数列表: + if ma_type == "SMA": + 当前K线.指标.均线[key] = 均线工具.增量SMA(普K序列, period, 计算方式) + elif ma_type == "EMA": + 前值 = None + if len(普K序列) >= 2: + 前值 = 普K序列[-2].指标.均线.get(key) + 当前K线.指标.均线[key] = 均线工具.增量EMA(普K序列, period, 计算方式, 前值) @staticmethod def _回填新指标(全序列: List[K线], 配置: 缠论配置): @@ -2527,16 +2563,14 @@ class 指标计算器: 新参数.append(params) return 新参数 - 新MACD = _新键(尾K指标, 首K指标, 配置._解析MACD参数列表()) - 新RSI = _新键(尾K指标, 首K指标, 配置._解析RSI周期列表()) - 新KDJ = _新键(尾K指标, 首K指标, 配置._解析KDJ参数列表()) - 新BOLL = _新键(尾K指标, 首K指标, 配置._解析BOLL参数列表()) + 新MACD = _新键(尾K指标, 首K指标, 配置.MACD_参数列表) + 新RSI = _新键(尾K指标, 首K指标, 配置.RSI_周期列表) + 新KDJ = _新键(尾K指标, 首K指标, 配置.KDJ_参数列表) + 新BOLL = _新键(尾K指标, 首K指标, 配置.BOLL_参数列表) if not (新MACD or 新RSI or 新KDJ or 新BOLL): return - 计算方式 = 配置.指标计算方式 - for i, k线 in enumerate(全序列): if k线.指标 is None: k线.指标 = 指标容器() @@ -2544,43 +2578,32 @@ class 指标计算器: idx = k线.指标 prev = 全序列[i - 1].指标 if i > 0 else None - for key, 快, 慢, 信号 in 新MACD: + for key, 计算方式, *params in 新MACD: + 快, 慢, 信号 = params[0], params[1], params[2] prev_val = prev[key] if prev is not None and key in prev else None if prev_val is not None: idx[key] = 平滑异同移动平均线.增量计算_K线(prev_val, k线, 计算方式) else: idx[key] = 平滑异同移动平均线.首次计算_K线(k线, 计算方式, 快, 慢, 信号) - for key, 周期 in 新RSI: + for key, 计算方式, *params in 新RSI: + 周期, MA周期, 超买, 超卖 = params[0], params[1], params[2], params[3] prev_val = prev[key] if prev is not None and key in prev else None if prev_val is not None: idx[key] = 相对强弱指数.增量计算_K线(prev_val, k线, 计算方式) else: - idx[key] = 相对强弱指数.首次计算_K线( - k线, - 计算方式, - 周期, - 配置.相对强弱指数_超买阈值, - 配置.相对强弱指数_超卖阈值, - 配置.相对强弱指数_移动平均线周期, - ) + idx[key] = 相对强弱指数.首次计算_K线(k线, 计算方式, 周期, 超买, 超卖, MA周期) - for key, rsv, k平滑, d平滑 in 新KDJ: + for key, 计算方式, *params in 新KDJ: + rsv, k平滑, d平滑, 超买, 超卖 = params[0], params[1], params[2], params[3], params[4] prev_val = prev[key] if prev is not None and key in prev else None if prev_val is not None: idx[key] = 随机指标.增量计算_K线(prev_val, k线, 计算方式) else: - idx[key] = 随机指标.首次计算_K线( - k线, - 计算方式, - rsv, - k平滑, - d平滑, - 配置.随机指标_超买阈值, - 配置.随机指标_超卖阈值, - ) + idx[key] = 随机指标.首次计算_K线(k线, 计算方式, rsv, k平滑, d平滑, 超买, 超卖) - for key, 周期, 标准差倍数 in 新BOLL: + for key, 计算方式, *params in 新BOLL: + 周期, 标准差倍数 = params[0], params[1] prev_val = prev[key] if prev is not None and key in prev else None if prev_val is not None: idx[key] = 布林带.增量计算(prev_val, k线, 计算方式) @@ -2588,6 +2611,7 @@ class 指标计算器: idx[key] = 布林带.首次计算(k线, 计算方式, 周期, 标准差倍数) +@注册 class 背驰分析: """静态方法容器,提供背驰/背离检测算法。 @@ -2823,6 +2847,14 @@ class K线: self.周期: int = 周期 self.指标: 指标容器 = 指标容器() + @property + def 最高价(self): + return self.高 + + @property + def 最低价(self): + return self.低 + # ---- property 兼容层:k线.macd → k线.指标.macd ---- @property def macd(self) -> Optional[平滑异同移动平均线]: @@ -2969,6 +3001,58 @@ class K线: """ return 序列[序列.index(始) : 序列.index(终) + 1] + def 根据当前K线生成新K线(self, 方向: 相对方向, 居中: bool = False) -> "K线": + 时间偏移 = timedelta(seconds=self.周期) + 时间戳: datetime = self.时间戳 + 时间偏移 + 成交量: float = 998 + 高: float = 0 + 低: float = 0 + 高低差 = self.高 - self.低 + match 方向: + case 相对方向.向上: + 偏移 = 高低差 * 0.5 if 居中 else random.randint(int(高低差 * 0.1279), int(高低差 * 0.883)) + 低 = self.低 + 偏移 + 高 = self.高 + 偏移 + case 相对方向.向下: + 偏移 = 高低差 * 0.5 if 居中 else random.randint(int(高低差 * 0.1279), int(高低差 * 0.883)) + 低 = self.低 - 偏移 + 高 = self.高 - 偏移 + case 相对方向.向上缺口: + 偏移 = 高低差 * 1.5 if 居中 else random.randint(int(高低差 * 1.1279), int(高低差 * 1.883)) + 低 = self.低 + 偏移 + 高 = self.高 + 偏移 + case 相对方向.向下缺口: + 偏移 = 高低差 * 1.5 if 居中 else random.randint(int(高低差 * 1.1279), int(高低差 * 1.883)) + 低 = self.低 - 偏移 + 高 = self.高 - 偏移 + case 相对方向.衔接向上: + 偏移 = self.高 - self.低 + 高 = self.高 + 偏移 + 低 = self.高 + case 相对方向.衔接向下: + 偏移 = self.高 - self.低 + 高 = self.低 + 低 = self.低 - 偏移 + + try: + 小数点 = [len(str(n).split(".")[-1]) for n in (self.开盘价, self.高, self.低, self.收盘价)] + except: + 小数点 = [2, 1] + 新K线 = K线.创建普K( + 标识=self.标识, + 时间戳=时间戳, + 开盘价=round(random.uniform(高, 低), max(小数点)), + 最高价=round(高, max(小数点)), + 最低价=round(低, max(小数点)), + 收盘价=round(random.uniform(高, 低), max(小数点)), + 成交量=成交量 * random.random(), + 序号=self.序号 + 1, + 周期=self.周期, + ) + + # assert 相对方向.分析(self, 新K线) is 方向, (方向, 相对方向.分析(self, 新K线)) + return 新K线 + class 缠论K线: """经包含处理后的标准化K线,有方向和分型结构标记。 @@ -3303,6 +3387,7 @@ class 缠论K线: 分型模式 = True +@注册 class 分型: """由左中右三根缠论K线构成的顶/底分型结构。 @@ -3488,6 +3573,10 @@ class 分型: 分型序列.append(当前分型) +扩展线段模式 = True # TODO 虚线高低取值 暂定,此举将符合同级别分解时正确的高低取值涉及中枢等问题 + + +@注册 class 虚线: """笔/线段的通用数据结构,持有一组分型端点(文=起点分型, 武=终点分型)。 @@ -3615,15 +3704,29 @@ class 虚线: case _: raise RuntimeError("无法识别的方向", self.文.结构, self.武.结构) + @property + def 端点高(self) -> float: + if self.方向 is 相对方向.向上: + return self.武.中.高 + return self.文.中.高 + + @property + def 端点低(self) -> float: + if self.方向 is 相对方向.向下: + return self.武.中.低 + return self.文.中.低 + @property def 高(self) -> float: """虚线区间的最高价。 :return: 向上虚线取武.中.高,向下虚线取文.中.高 """ - if self.方向 is 相对方向.向上: - return self.武.中.高 - return self.文.中.高 + if 扩展线段模式 and self.模式 != "文武" and self.标识 != "笔" and "扩展" in self.标识: # 扩展线段 + 端点序列 = [筆.文 for 筆 in self.基础序列] + 端点序列.append(self.基础序列[-1].武) + return max(端点序列, key=lambda o: o.中.高).中.高 + return self.端点高 @property def 低(self) -> float: @@ -3631,9 +3734,11 @@ class 虚线: :return: 向下虚线取武.中.低,向上虚线取文.中.低 """ - if self.方向 is 相对方向.向下: - return self.武.中.低 - return self.文.中.低 + if 扩展线段模式 and self.模式 != "文武" and self.标识 != "笔" and "扩展" in self.标识: # 扩展线段 + 端点序列 = [筆.文 for 筆 in self.基础序列] + 端点序列.append(self.基础序列[-1].武) + return min(端点序列, key=lambda o: o.中.低).中.低 + return self.端点低 def 之前是(self, 之前: 虚线) -> bool: """ @@ -3704,7 +3809,7 @@ class 虚线: 段.实_中枢序列 = [] 段.虚_中枢序列 = [] 段.合_中枢序列 = [] - 段.基础序列 = 虚线序列 + 段.基础序列 = 虚线序列[:] return 段 @classmethod @@ -3873,6 +3978,111 @@ class 虚线: return True return False + @classmethod + def _计算K线序列MACD趋向背驰(cls, 普K序列: Sequence[K线], 方向: 相对方向): + """计算K线序列的MACD柱/DIF/DEA趋向背驰(三元素判断) + + :param 普K序列: K线序列 + :param 方向: 运行方向 + :return: [柱子背驰, DIF背驰, DEA背驰] + """ + if 方向 is 相对方向.向上: + 柱子序列 = [] + 离差值序列 = [] + 信号线序列 = [] + for k线 in 普K序列: + m = k线.macd + if m.MACD柱 > 0: + 柱子序列.append(k线) + if m.DIF > 0: + 离差值序列.append(k线) + if m.DEA > 0: + 信号线序列.append(k线) + + if not 柱子序列: + return [False, False, False] + 最高柱子 = max(柱子序列, key=lambda k线: k线.macd.MACD柱) + 最高离差值 = max(离差值序列, key=lambda k线: k线.macd.DIF) if 离差值序列 else None + 最高信号线 = max(信号线序列, key=lambda k线: k线.macd.DEA) if 信号线序列 else None + + 结果 = [] + 柱子 = [最高柱子, 普K序列[-1]] + 柱子.sort(key=lambda k线: k线.时间戳) + if 柱子[0].macd.MACD柱 > 柱子[1].macd.MACD柱 and 柱子[0].高 < 柱子[1].高: + 结果.append(True) + else: + 结果.append(False) + + if 最高离差值 is not None: + 柱子 = [最高离差值, 普K序列[-1]] + 柱子.sort(key=lambda k线: k线.时间戳) + if 柱子[0].macd.DIF > 柱子[1].macd.DIF and 柱子[0].高 < 柱子[1].高: + 结果.append(True) + else: + 结果.append(False) + else: + 结果.append(False) + + if 最高信号线 is not None: + 柱子 = [最高信号线, 普K序列[-1]] + 柱子.sort(key=lambda k线: k线.时间戳) + if 柱子[0].macd.DEA > 柱子[1].macd.DEA and 柱子[0].高 < 柱子[1].高: + 结果.append(True) + else: + 结果.append(False) + else: + 结果.append(False) + + return 结果 + else: + 柱子序列 = [] + 离差值序列 = [] + 信号线序列 = [] + for k线 in 普K序列: + m = k线.macd + if m.MACD柱 < 0: + 柱子序列.append(k线) + if m.DIF < 0: + 离差值序列.append(k线) + if m.DEA < 0: + 信号线序列.append(k线) + + if not 柱子序列: + return [False, False, False] + 最高柱子 = max(柱子序列, key=lambda k线: abs(k线.macd.MACD柱)) + 最高离差值 = max(离差值序列, key=lambda k线: abs(k线.macd.DIF)) if 离差值序列 else None + 最高信号线 = max(信号线序列, key=lambda k线: abs(k线.macd.DEA)) if 信号线序列 else None + + 结果 = [] + 柱子 = [最高柱子, 普K序列[-1]] + 柱子.sort(key=lambda k线: k线.时间戳) + if 柱子[0].macd.MACD柱 < 柱子[1].macd.MACD柱 and 柱子[0].低 > 柱子[1].低: + 结果.append(True) + else: + 结果.append(False) + + if 最高离差值 is not None: + 柱子 = [最高离差值, 普K序列[-1]] + 柱子.sort(key=lambda k线: k线.时间戳) + if 柱子[0].macd.DIF < 柱子[1].macd.DIF and 柱子[0].低 > 柱子[1].低: + 结果.append(True) + else: + 结果.append(False) + else: + 结果.append(False) + + if 最高信号线 is not None: + 柱子 = [最高信号线, 普K序列[-1]] + 柱子.sort(key=lambda k线: k线.时间戳) + if 柱子[0].macd.DEA < 柱子[1].macd.DEA and 柱子[0].低 > 柱子[1].低: + 结果.append(True) + else: + 结果.append(False) + else: + 结果.append(False) + + return 结果 + @classmethod def 计算K线序列MACD趋向背驰(cls, 普K序列: Sequence[K线], 方向: 相对方向): """计算K线序列的MACD柱/DIF/DEA趋向背驰(三元素判断) @@ -4073,23 +4283,24 @@ class 虚线: dif_up = dif_down = dea_up = dea_down = 0 for i in range(1, len(普K序列)): pre, cur = 普K序列[i - 1].macd, 普K序列[i].macd - if pre.DIF is None or cur.DIF is None: + if pre is None or cur is None or pre.DIF is None or cur.DIF is None: continue if pre.DIF < 0 <= cur.DIF: dif_up += 1 elif pre.DIF > 0 >= cur.DIF: dif_down += 1 - if pre.DEA < 0 <= cur.DEA: - dea_up += 1 - elif pre.DEA > 0 >= cur.DEA: - dea_down += 1 + if pre.DEA is not None and cur.DEA is not None: + if pre.DEA < 0 <= cur.DEA: + dea_up += 1 + elif pre.DEA > 0 >= cur.DEA: + dea_down += 1 # 2. DIF与DEA交叉(带标记) golden = death = 0 交叉标记 = [0] # 第0个位置无前值,先填0 for i in range(1, len(普K序列)): pre, cur = 普K序列[i - 1].macd, 普K序列[i].macd - if pre.DIF is None or cur.DIF is None or pre.DEA is None or cur.DEA is None: + if pre is None or cur is None or pre.DIF is None or cur.DIF is None or pre.DEA is None or cur.DEA is None: 交叉标记.append(0) continue if pre.DIF <= pre.DEA and cur.DIF > cur.DEA: @@ -4128,6 +4339,7 @@ class 虚线: return tmp.武 +@注册 class 笔: """纯静态方法容器,提供笔划分算法的所有函数。 @@ -4407,12 +4619,11 @@ class 笔: if 分型序列 and 分型序列[-1] is 临时分型: # 进行修复错过的笔 for ck in 缠K序列[缠K序列.index(武将) :]: - if ck.分型 in (分型结构.底, 分型结构.顶): + if ck.分型 in (分型结构.底, 分型结构.顶) and 分型序列[-1].时间戳 < ck.时间戳: 临时分型 = 分型.从缠K序列中获取分型(缠K序列, ck) 递归层次 = 笔递归分析(临时分型, 分型序列, 笔序列, 缠K序列, 普K序列, 递归层次 + 1, 配置) if 分型序列 and 分型序列[-1] is 临时分型: - """""" - # logger.warning("笔.分析 事后修复错过的笔", 临时分型, "当前分型", 当前分型) + logger.warning(f"笔.分析 事后修复错过的笔:{临时分型}, 当前分型: {当前分型}") 递归层次 = 笔递归分析(当前分型, 分型序列, 笔序列, 缠K序列, 普K序列, 递归层次 + 1, 配置) return 递归层次 @@ -4570,14 +4781,14 @@ class 线段特征: return self.标识 # f"{self.标识}:{self.序号}" def __str__(self): - if not len(self): + if not len(self.基础序列): return f"{self.标识}<{self.线段方向}, 空>" - return f"{self.标识}<{self.线段方向}, {self.文}, {self.武}, {len(self)}>" + return f"{self.标识}<{self.线段方向}, {self.文}, {self.武}, {len(self.基础序列)}>" def __repr__(self): - if not len(self): + if not len(self.基础序列): return f"{self.标识}<{self.线段方向}, 空>" - return f"{self.标识}<{self.线段方向}, {self.文}, {self.武}, {len(self)}>" + return f"{self.标识}<{self.线段方向}, {self.文}, {self.武}, {len(self.基础序列)}>" @property def 文(self) -> 分型: @@ -4762,6 +4973,7 @@ class 特征分型: return f"特征分型<{self.结构}, {self.中}>" +@注册 class 线段: """纯静态方法容器,提供线段划分算法的所有函数。 @@ -4788,6 +5000,12 @@ class 线段: __slots__ = [] + @staticmethod + def _索引(序列: list, 项) -> int: + """O(1) index lookup — 序列元素序号连续递增。""" + # return 项.序号 - 序列[0].序号 + return 序列.index(项) + @classmethod def _添加虚线(cls, 段: 虚线, 筆: 虚线): """向线段中添加一笔 @@ -4918,7 +5136,7 @@ class 线段: break if (len(基础序列) >= 6) and (len(基础序列) % 2 == 0): - 段.基础序列[:] = 基础序列[:] + 段.基础序列[:] = 基础序列 else: raise RuntimeError() else: @@ -4935,7 +5153,7 @@ class 线段: return 基础序列 = 段.基础序列 if 段.前一结束位置 and 段.前一结束位置 in 基础序列: - 基础序列 = 段.基础序列[段.基础序列.index(段.前一结束位置) - 1 :] + 基础序列 = 段.基础序列[cls._索引(段.基础序列, 段.前一结束位置) - 1 :] 特征序列 = 线段特征.静态分析(基础序列, 段.方向, 线段.四象(段), 配置.线段_特征序列忽视老阴老阳) if len(特征序列) >= 3: @@ -5053,7 +5271,7 @@ class 线段: 特征后一笔 = 最近特征.基础序列[-1] if 特征后一笔 is not None: - 序号 = 段.基础序列.index(特征后一笔) + 序号 = cls._索引(段.基础序列, 特征后一笔) if 序号 < len(段.基础序列) - 1: 下一笔 = 段.基础序列[序号 + 1] if (段.方向 is 相对方向.向上 and 段.高 <= 下一笔.高) or (段.方向 is 相对方向.向下 and 段.低 >= 下一笔.低): @@ -5072,15 +5290,16 @@ class 线段: :param 序列: 参考序列 """ 基础序列 = [] + 序列集 = set(序列) if not isinstance(序列, set) else 序列 for 元素 in 段.基础序列: - if 元素 not in 序列: + if 元素 not in 序列集: break if 基础序列: if not 基础序列[-1].之后是(元素): break 基础序列.append(元素) - 段.基础序列[:] = 基础序列[:] + 段.基础序列[:] = 基础序列 段.特征序列[2] = None @classmethod @@ -5149,16 +5368,17 @@ class 线段: return True @classmethod - def _添加线段(cls, 线段序列: List[虚线], 待添加线段: 虚线, 配置: 缠论配置, 行号: str): + def _添加线段(cls, 线段序列: List[虚线], 待添加线段: 虚线, 配置: 缠论配置, 行号: int, 层级: int): """内部方法:向线段序列添加新线段 :param 线段序列: 线段列表 :param 待添加线段: 新线段 :param 配置: 缠论配置 :param 行号: 调用行号 + :param 层级: 递归层级 """ if 线段序列 and not 线段序列[-1].之后是(待添加线段): - raise ValueError(f"线段.向序列中添加 不连续[{行号}]", 线段序列[-1].武, 待添加线段.文) + raise ValueError(f"线段.向序列中添加 不连续[{行号}, {层级}]", 线段序列[-1].武, 待添加线段.文) 待添加线段.模式 = "文武" if not 线段序列: @@ -5169,10 +5389,10 @@ class 线段: if not 之前线段.特征序列[2] and not 之前线段.短路修正: assert not 待添加线段.短路修正 and 之前线段.特征序列[2][-1] in 待添加线段.基础序列 - raise RuntimeError(f"线段._向序列中添加[{行号}], 之前线段.右 = None", 之前线段) + raise RuntimeError(f"线段._向序列中添加[{行号}, {层级}], 之前线段.右 = None", 之前线段) if 之前线段.基础序列[-1] not in 待添加线段.基础序列 and not 之前线段.短路修正: - raise RuntimeError(f"线段._向序列中添加[{行号}], 之前线段[-1] not in 待添加虚线!", 之前线段) + raise RuntimeError(f"线段._向序列中添加[{行号}, {层级}], 之前线段[-1] not in 待添加虚线!", 之前线段) 待添加线段.序号 = 之前线段.序号 + 1 待添加线段.前一缺口 = 线段.获取缺口(之前线段) if not 之前线段.短路修正 else None @@ -5185,13 +5405,14 @@ class 线段: # logger.warning(f"线段._向序列中添加[{行号}]", 待添加虚线) @classmethod - def _弹出线段(cls, 线段序列: List[虚线], 待弹出线段: 虚线, 配置: 缠论配置, 行号: str): + def _弹出线段(cls, 线段序列: List[虚线], 待弹出线段: 虚线, 配置: 缠论配置, 行号: int, 层级: int): """内部方法:从线段序列弹出最后一个线段 :param 线段序列: 线段列表 :param 待弹出线段: 待弹出的线段 :param 配置: 缠论配置 :param 行号: 调用行号 + :param 层级: 递归层级 :return: 弹出的线段或None """ if not 线段序列: @@ -5204,7 +5425,7 @@ class 线段: if 右 is not None: 结构 = 分型结构.分析(左, 中, 右, True, True) if 结构 in (分型结构.顶, 分型结构.底) and not 相对方向.分析(左.高, 左.低, 中.高, 中.低).是否缺口(): - logger.warning(f"警告<{行号}>] 线段._从序列中删除 发现分型完毕, 且特征序列无缺口 {待弹出线段}") + logger.warning(f"警告<{行号}, {层级}>] 线段._从序列中删除 发现分型完毕, 且特征序列无缺口 {待弹出线段}") 线段序列.pop() 待弹出线段.前一结束位置 = None @@ -5249,7 +5470,7 @@ class 线段: # 执行修正 序列 = 当前线段.基础序列[:] - 线段._弹出线段(线段序列, 当前线段, 配置, f"{sys._getframe().f_lineno}, {层级}") + 线段._弹出线段(线段序列, 当前线段, 配置, sys._getframe().f_lineno, 层级) assert 线段序列, "缺口突破: 线段序列为第二次空!" 当前线段 = 线段序列[-1] @@ -5258,7 +5479,7 @@ class 线段: assert 当前线段基础序列[-1].之后是(序列[0]), "缺口突破: 子序列不连续!" 当前线段基础序列.extend(序列) - 当前线段.基础序列[:] = 当前线段基础序列[:] + 当前线段.基础序列[:] = 当前线段基础序列 线段._刷新(当前线段, 配置) return True @@ -5286,7 +5507,7 @@ class 线段: assert 贯穿伤 in 当前线段.基础序列, "非缺口下穿刺: 贯穿伤不在基础序列中!" # 切割基础序列 - 基础序列 = 当前线段.基础序列[当前线段.基础序列.index(贯穿伤) :] + 基础序列 = 当前线段.基础序列[cls._索引(当前线段.基础序列, 贯穿伤) :] # 长度条件 if not (len(基础序列) == 4 and len(线段序列) >= 2): @@ -5302,19 +5523,25 @@ class 线段: logger.warning(f"[警告<{sys._getframe().f_lineno}, {层级}>]: {当前线段.标识}.修复贯穿伤, 序号:{当前线段.序号} {贯穿伤} {基础序列}") # 异常弹出 基础序列 = 当前线段.基础序列[:] - 线段._弹出线段(线段序列, 当前线段, 配置, f"{sys._getframe().f_lineno}, {层级}") + 线段._弹出线段(线段序列, 当前线段, 配置, sys._getframe().f_lineno, 层级) assert 线段序列, "非缺口下穿刺: 第二次线段序列为空!" 当前线段 = 线段序列[-1] 当前线段.特征序列[2] = None - assert 当前线段.基础序列[-1] in 基础序列, "非缺口下穿刺: 当前线段.基础序列[-1] 不在 基础序列中!" - for 临时虚线 in 基础序列[基础序列.index(当前线段.基础序列[-1]) + 1 :]: + # assert 当前线段.基础序列[-1] in 基础序列, "非缺口下穿刺: 当前线段.基础序列[-1] 不在 基础序列中!" + if 当前线段.基础序列[-1] not in 基础序列: + logger.error(f"非缺口下穿刺: 当前线段.基础序列[-1] 不在 基础序列中!") + 序号 = 0 + else: + 序号 = cls._索引(基础序列, 当前线段.基础序列[-1]) + 1 + + for 临时虚线 in 基础序列[序号:]: 线段._添加虚线(当前线段, 临时虚线) 线段._刷新(当前线段, 配置) 当前线段.短路修正 = True if 当前线段.特征序列[2] is not None: 段 = 虚线.创建线段([左, 中, 右]) - 线段._添加线段(线段序列, 段, 配置, f"{sys._getframe().f_lineno}, {层级}") + 线段._添加线段(线段序列, 段, 配置, sys._getframe().f_lineno, 层级) 段.特征序列[0] = 线段特征.新建([中], 段.方向) return True @@ -5360,7 +5587,7 @@ class 线段: # 执行修正 当前线段.短路修正 = True 新段 = 虚线.创建线段(基础序列) - 线段._添加线段(线段序列, 新段, 配置, f"{sys._getframe().f_lineno}, {层级}") + 线段._添加线段(线段序列, 新段, 配置, sys._getframe().f_lineno, 层级) return True @classmethod @@ -5403,7 +5630,7 @@ class 线段: # 创建第一个新段(之后基础序列去掉最后3个) 新段 = 虚线.创建线段(之后基础序列[:-3]) 新段.短路修正 = True - 线段._添加线段(线段序列, 新段, 配置, f"{sys._getframe().f_lineno}, {层级}") + 线段._添加线段(线段序列, 新段, 配置, sys._getframe().f_lineno, 层级) # 根据当前线段的四象决定是否清空前一个缺口 if 线段.四象(当前线段) in ("老阴", "老阳"): @@ -5411,7 +5638,7 @@ class 线段: # 创建第二个新段(最后3个元素) 新段 = 虚线.创建线段(之后基础序列[-3:]) - 线段._添加线段(线段序列, 新段, 配置, f"{sys._getframe().f_lineno}, {层级}") + 线段._添加线段(线段序列, 新段, 配置, sys._getframe().f_lineno, 层级) return True @@ -5447,7 +5674,7 @@ class 线段: if not 线段._基础判断(左, 中, 右, 关系序列): # FIXME 首个线段必须有明确方向 continue 段 = 虚线.创建线段([左, 中, 右]) - 线段._添加线段(线段序列, 段, 配置, f"{sys._getframe().f_lineno}, {层级}") + 线段._添加线段(线段序列, 段, 配置, sys._getframe().f_lineno, 层级) 段.特征序列[0] = 线段特征.新建([中], 段.方向) break if not 线段序列: @@ -5456,7 +5683,7 @@ class 线段: # -------------------- 2. 清理无效的尾部引用 -------------------- while 线段序列 and 线段序列[-1].前一结束位置: if 线段序列[-1].前一结束位置 not in 笔序列: - 线段._弹出线段(线段序列, 线段序列[-1], 配置, f"{sys._getframe().f_lineno}, {层级}") + 线段._弹出线段(线段序列, 线段序列[-1], 配置, sys._getframe().f_lineno, 层级) else: break @@ -5468,7 +5695,7 @@ class 线段: 线段._序列重置(当前线段, 笔序列) if len(当前线段.基础序列) < 3: - 线段._弹出线段(线段序列, 当前线段, 配置, f"{sys._getframe().f_lineno}, {层级}") + 线段._弹出线段(线段序列, 当前线段, 配置, sys._getframe().f_lineno, 层级) if not 线段序列: return 线段递归分析(笔序列, 线段序列, 配置, 层级 + 1, 关系序列) @@ -5478,7 +5705,7 @@ class 线段: if 当前线段.特征序列[2] is not None: 基础序列 = 线段.分割序列(当前线段)[1] 新段 = 虚线.创建线段(基础序列) - 线段._添加线段(线段序列, 新段, 配置, f"{sys._getframe().f_lineno}, {层级}") + 线段._添加线段(线段序列, 新段, 配置, sys._getframe().f_lineno, 层级) if 线段.四象(当前线段) in ("老阴", "老阳"): 新段.前一缺口 = None @@ -5495,9 +5722,10 @@ class 线段: 当前线段 = 线段序列[-1] if not 当前线段.基础序列: raise RuntimeError - 起始索引 = 笔序列.index(当前线段.基础序列[-1]) + 1 + 起始索引 = cls._索引(笔序列, 当前线段.基础序列[-1]) + 1 - for 当前虚线 in 笔序列[起始索引:]: + for idx in range(起始索引, len(笔序列)): + 当前虚线 = 笔序列[idx] 当前线段 = 线段序列[-1] 四象 = 线段.四象(当前线段) @@ -5524,7 +5752,7 @@ class 线段: 基础序列 = 线段.分割序列(当前线段)[1] 新段 = 虚线.创建线段(基础序列) - 线段._添加线段(线段序列, 新段, 配置, f"{sys._getframe().f_lineno}, {层级}") + 线段._添加线段(线段序列, 新段, 配置, sys._getframe().f_lineno, 层级) if 四象 in ("老阴", "老阳"): 新段.前一缺口 = None @@ -5556,15 +5784,16 @@ class 线段: :param 序列: 参考序列 """ 基础序列 = [] + 序列集 = set(序列) if not isinstance(序列, set) else 序列 for 元素 in 段.基础序列: - if 元素 not in 序列: + if 元素 not in 序列集: break if 基础序列: if not 基础序列[-1].之后是(元素): logger.warning(" 线段._验证序列 数据不连续") break 基础序列.append(元素) - 段.基础序列[:] = 基础序列[:] + 段.基础序列[:] = 基础序列 if len(段.基础序列) % 2 == 0: 段.基础序列 and 段.基础序列.pop() @@ -5626,7 +5855,7 @@ class 线段: if not 线段序列: for i in range(1, len(虚线序列) - 1): 左, 中, 右 = 虚线序列[i - 1], 虚线序列[i], 虚线序列[i + 1] - 关系 = 相对方向.分析(左.高, 左.低, 右.高, 右.低) + 关系 = 相对方向.分析(左.端点高, 左.端点低, 右.端点高, 右.端点低) if 关系 not in (相对方向.向下, 相对方向.向上, 相对方向.顺, 相对方向.逆, 相对方向.同): # FIXME 此处为首个线段 continue @@ -5646,7 +5875,7 @@ class 线段: if not 配置.扩展线段_当下分析: 左, 中, 右 = 当前线段.基础序列[:3] - if not 相对方向.分析(左.高, 左.低, 右.高, 右.低).是否缺口(): + if not 相对方向.分析(左.端点高, 左.端点低, 右.端点高, 右.端点低).是否缺口(): 当前线段.基础序列[:] = 当前线段.基础序列[:3] 线段._武终(当前线段, sys._getframe().f_lineno) else: @@ -5657,13 +5886,13 @@ class 线段: if 当前线段.基础序列[-1].序号 + 3 > 虚线序列[-1].序号: return None - 序号 = 虚线序列.index(当前线段.基础序列[-1]) + 1 + 序号 = cls._索引(虚线序列, 当前线段.基础序列[-1]) + 1 if 序号 >= len(虚线序列): return None for i in range(序号 + 1, len(虚线序列) - 1): 左, 中, 右 = 虚线序列[i - 1], 虚线序列[i], 虚线序列[i + 1] - 相对关系 = 相对方向.分析(左.高, 左.低, 右.高, 右.低) + 相对关系 = 相对方向.分析(左.端点高, 左.端点低, 右.端点高, 右.端点低) if 相对关系.是否缺口(): 线段._添加虚线(当前线段, 左) 线段._添加虚线(当前线段, 中) @@ -5720,7 +5949,7 @@ class 线段: if 当前段.实_中枢序列: if 阳[-1] in 当前段.实_中枢序列[-1].基础序列: # 当前最后一笔在最后一中枢里 - 序号 = 当前段.基础序列.index(当前段.实_中枢序列[-1].基础序列[0]) + 序号 = cls._索引(当前段.基础序列, 当前段.实_中枢序列[-1].基础序列[0]) 进入段 = 当前段.基础序列[序号 - 1] 离开段 = 阳[-1] assert 进入段.序号 < 离开段.序号, (进入段.序号, 离开段.序号) @@ -5774,7 +6003,7 @@ class 线段: 笔序列.append(停顿) 线段.分析(笔序列, 线段序列, 观察员.配置, 关系序列=[相对方向.向下, 相对方向.向上, 相对方向.顺, 相对方向.逆, 相对方向.同]) if 线段序列 and 线段序列[-1].武 is not 当前停顿 and len(线段序列[-1].基础序列) % 2 == 1: - 新段 = 虚线.创建线段(线段序列[-1].基础序列[:]) + 新段 = 虚线.创建线段(线段序列[-1].基础序列) 新段.序号 = self.序号 线段._刷新(新段, 观察员.配置) if 新段.方向 is self.方向: @@ -5805,6 +6034,7 @@ class 线段: return 结果 +@注册 class 中枢: """三段虚线重叠区间构成的价格中枢,支持延伸和扩展。 @@ -5954,9 +6184,23 @@ class 中枢: else: # if self.本级_第三买卖线: # return True + 中枢状态 = self.当前状态() + if 中枢状态 == "中枢之中": + return False 线段内部中枢 = self.基础序列[-1].合_中枢序列 if 虚实 == "合" else self.基础序列[-1].实_中枢序列 + if not 线段内部中枢: + return False + 高, 低 = self.高, self.低 for 内部中枢 in 线段内部中枢: - if 相对方向.分析(self.高, self.低, 内部中枢.高, 内部中枢.低).是否缺口(): + 内部中枢高, 内部中枢低 = 内部中枢.高, 内部中枢.低 + if 中枢状态 == "中枢之下": + if 低 <= 内部中枢高: + continue + else: + # 中枢之上 + if 高 >= 内部中枢低: + continue + if 相对方向.分析(self.高, self.低, 内部中枢高, 内部中枢低).是否缺口(): return True return False @@ -5989,13 +6233,14 @@ class 中枢: """ 有效序列 = self.基础序列[:] 无效序列 = [] + 序列集 = set(序列) for 元素 in self.基础序列: - if 元素 not in 序列: + if 元素 not in 序列集: 无效序列.append(元素) if 无效序列: 无效 = 无效序列[0] - 序号 = self.基础序列.index(无效) + 序号 = 线段._索引(self.基础序列, 无效) 有效序列 = self.基础序列[:序号] if len(有效序列) < 3: @@ -6182,7 +6427,7 @@ class 中枢: 左, 中, 右 = 虚线序列[i - 1], 虚线序列[i], 虚线序列[i + 1] if 中枢.基础检查(左, 中, 右): 新中枢 = 中枢.创建(左, 中, 右, 中.级别, 标识) - 序号 = 虚线序列.index(左) + 序号 = 线段._索引(虚线序列, 左) if 跳过首部 and (左.序号 == 0 or 序号 == 0): continue # 方便计算走势 if 序号 >= 2: @@ -6203,7 +6448,7 @@ class 中枢: 中枢._从中枢序列尾部弹出(中枢序列, 当前中枢) return 中枢递归分析(虚线序列, 中枢序列, 跳过首部, 标识, 层级 + 1) - 序号 = 虚线序列.index(当前中枢.基础序列[-1]) + 1 + 序号 = 线段._索引(虚线序列, 当前中枢.基础序列[-1]) + 1 基础序列 = [] for 当前虚线 in 虚线序列[序号:]: @@ -6231,6 +6476,7 @@ class 中枢: return None +@注册 class 观察者: """单周期缠论分析器,接收K线流式输入并逐层计算所有层级序列。 @@ -6749,6 +6995,7 @@ class K线合成器: return self.当前K线[周期] +@注册 class 立体分析器: """多周期缠论分析器,内部包含 :class:`K线合成器` + 每周期一个 :class:`观察者`。 @@ -6780,20 +7027,13 @@ class 立体分析器: for 周期 in self.周期组: 临时配置 = 配置组.get(周期, 配置) 当前配置 = 临时配置.model_copy( - update={ - "推送K线": False, - # "推送笔": False, - "推送线段": False, - # "图表展示": False, - }, + update={"图表展示标签": []}, deep=True, ) self._单体分析器[周期] = 观察者(符号=符号, 周期=周期, 配置=当前配置) - self._单体分析器[self.__显示周期].配置.推送K线 = True - self._单体分析器[self.__显示周期].配置.推送笔 = True - self._单体分析器[self.__显示周期].配置.推送线段 = True self._单体分析器[self.__显示周期].配置.图表展示 = True + self._单体分析器[self.__显示周期].配置.图表展示标签 = None # None = 全部展示 self._单体分析器[self.__显示周期].重置基础序列() for 周期 in self.周期组: # 将不同周期对其至显示周期 @@ -6844,6 +7084,1085 @@ class 立体分析器: logger.warning(f"多级别数据拆分保存完成,目录:{保存路径.resolve()}") +# ═══════════════════════════════════════════════════════════════════════════════ +# 以下信号匹配框架(import_by_name, Signal, Factor, Event, SignalsParser, +# Position 等类)摘录自 czsc 项目(https://github.com/zengbin93/czsc), +# Apache License 2.0 授权。 详见本文件头部 第三方代码声明。 +# ═══════════════════════════════════════════════════════════════════════════════ + + +def import_by_name(name: str): + """通过字符串导入模块、类、函数 + + 函数执行逻辑: + + 1. 检查 name 中是否包含点号('.')。如果没有,则直接使用内置的 import 函数来导入整个模块,并返回该模块对象。 + 2. 如果 name 包含点号,先处理一个相对路径。将 name 拆分为两部分:module_name 和 function_name。 + 使用 Python 内置的 rsplit 方法从右边开始分割,只取一次,这样可以确保我们将最后的一个点号前的部分作为 module_name,点号后面的部分作为 function_name。 + 3. 使用import函数导入指定的 module_name。 + 这里传入三个参数:globals() 和 locals() 分别代表当前全局和局部命名空间; + [function_name] 是一个列表,用于指定要导入的子模块或属性名。 + 这样做是为了避免一次性导入整个模块的所有内容,提高效率。 + 4. 使用 vars 函数获取模块的字典表示形式(即模块内所有的变量和函数),取出 function_name 对应的值,然后返回这个值。 + + :param name: 模块名,如:'czsc.objects.Factor' + :return: 模块对象 + """ + if "." not in name: + return __import__(name) + + # 从右边开始分割,分割成模块名和函数名 + module_name, function_name = name.rsplit(".", 1) + module = __import__(module_name, globals(), locals(), [function_name]) + 注入依赖(module) + + return vars(module)[function_name] + + +class Operate(Enum): + # 持有状态 + HL = "持多" # Hold Long + HS = "持空" # Hold Short + HO = "持币" # Hold Other + + # 多头操作 + LO = "开多" # Long Open + LE = "平多" # Long Exit + + # 空头操作 + SO = "开空" # Short Open + SE = "平空" # Short Exit + + def __str__(self): + return self.value + + +@dataclass +class Signal: + signal: str = "" + + # score 取值在 0~100 之间,得分越高,信号越强 + score: int = 0 + + # k1, k2, k3 是信号名称 + k1: str = "任意" # k1 一般是指明信号计算的K线周期,如 60分钟,日线,周线等 + k2: str = "任意" # k2 一般是记录信号计算的参数 + k3: str = "任意" # k3 用于区分信号,必须具有唯一性,推荐使用信号分类和开发日期进行标记 + + # v1, v2, v3 是信号取值 + v1: str = "任意" + v2: str = "任意" + v3: str = "任意" + + # 任意 出现在模板信号中可以指代任何值 + + def __post_init__(self): + if not self.signal: + self.signal = f"{self.k1}_{self.k2}_{self.k3}_{self.v1}_{self.v2}_{self.v3}_{self.score}" + else: + if not isinstance(self.signal, str): + raise TypeError(f"Signal 初始化需要字符串,收到了 {type(self.signal).__name__}: {self.signal!r}") + ( + self.k1, + self.k2, + self.k3, + self.v1, + self.v2, + self.v3, + score, + ) = self.signal.split("_") + self.score = int(score) + + if self.score > 100 or self.score < 0: + raise ValueError("score 必须在0~100之间") + + def __repr__(self): + return f"Signal('{self.signal}')" + + @property + def key(self) -> str: + """获取信号名称""" + key = "" + for k in [self.k1, self.k2, self.k3]: + if k != "任意": + key += k + "_" + return key.strip("_") + + @property + def value(self) -> str: + """获取信号值""" + return f"{self.v1}_{self.v2}_{self.v3}_{self.score}" + + def is_match(self, s: dict) -> bool: + """判断信号是否与信号列表中的值匹配 + + 代码的执行逻辑如下: + + 接收一个字典 s 作为参数,该字典包含了所有信号的信息。从字典 s 中获取名称为 key 的信号的值 v。 + 如果 v 不存在,则抛出异常。从信号的值 v 中解析出 v1、v2、v3 和 score 四个变量。 + + 如果当前信号的得分 score 大于等于目标信号的得分 self.score,则继续执行,否则返回 False。 + 如果当前信号的第一个值 v1 等于目标信号的第一个值 self.v1 或者目标信号的第一个值为 "任意",则继续执行,否则返回 False。 + 如果当前信号的第二个值 v2 等于目标信号的第二个值 self.v2 或者目标信号的第二个值为 "任意",则继续执行,否则返回 False。 + 如果当前信号的第三个值 v3 等于目标信号的第三个值 self.v3 或者目标信号的第三个值为 "任意",则返回 True,否则返回 False。 + + :param s: 所有信号字典 + :return: bool + """ + key = self.key + v = s.get(key, None) + if not v: + raise ValueError(f"{key} 不在信号列表中") + + if not isinstance(v, str): + logger.warning(f"信号 {key} 的值类型异常: {type(v).__name__} = {v!r},跳过匹配") + return False + + v1, v2, v3, score = v.split("_") + if int(score) >= self.score: + if v1 == self.v1 or self.v1 == "任意": + if v2 == self.v2 or self.v2 == "任意": + if v3 == self.v3 or self.v3 == "任意": + return True + return False + + +@dataclass +class Factor: + # signals_all 必须全部满足的信号,至少需要设定一个信号 + signals_all: List[Signal] + + # signals_any 满足其中任一信号,允许为空 + signals_any: List[Signal] = field(default_factory=list) + + # signals_not 不能满足其中任一信号,允许为空 + signals_not: List[Signal] = field(default_factory=list) + + name: str = "" + + def __post_init__(self): + if not self.signals_all: + raise ValueError("signals_all 不能为空") + _fatcor = self.dump() + _fatcor.pop("name") + sha256 = hashlib.sha256(str(_fatcor).encode("utf-8")).hexdigest().upper()[:4] + + if self.name: + self.name = self.name.split("#")[0] + f"#{sha256}" + else: + self.name = f"#{sha256}" + # self.name = f"{self.name}#{sha256}" if self.name else sha256 + + @property + def unique_signals(self) -> List[str]: + """获取 Factor 的唯一信号列表""" + signals = [] + signals.extend(self.signals_all) + if self.signals_any: + signals.extend(self.signals_any) + if self.signals_not: + signals.extend(self.signals_not) + signals = {x.signal if isinstance(x, Signal) else x for x in signals} + return list(signals) + + def is_match(self, s: dict) -> bool: + """判断 factor 是否满足""" + if self.signals_not: + for signal in self.signals_not: + if signal.is_match(s): + return False + + for signal in self.signals_all: + if not signal.is_match(s): + return False + + if not self.signals_any: + return True + + for signal in self.signals_any: + if signal.is_match(s): + return True + return False + + def dump(self) -> dict: + """将 Factor 对象转存为 dict""" + signals_all = [x.signal for x in self.signals_all] + signals_any = [x.signal for x in self.signals_any] if self.signals_any else [] + signals_not = [x.signal for x in self.signals_not] if self.signals_not else [] + + raw = { + "name": self.name, + "signals_all": signals_all, + "signals_any": signals_any, + "signals_not": signals_not, + } + return raw + + @classmethod + def load(cls, raw: dict): + """从 dict 中创建 Factor + + :param raw: 样例如下 + {'name': '单测', + 'signals_all': ['15分钟_倒0笔_方向_向上_其他_其他_0', '15分钟_倒0笔_长度_大于5_其他_其他_0'], + 'signals_any': [], + 'signals_not': []} + + :return: + """ + signals_any = [Signal(x) for x in raw.get("signals_any", [])] + signals_not = [Signal(x) for x in raw.get("signals_not", [])] + + fa = Factor( + name=raw.get("name", ""), + signals_all=[Signal(x) for x in raw["signals_all"]], + signals_any=signals_any, + signals_not=signals_not, + ) + return fa + + +@dataclass +class Event: + operate: Operate + + # 多个信号组成一个因子,多个因子组成一个事件。 + # 单个事件是一系列同类型因子的集合,事件中的任一因子满足,则事件为真。 + factors: List[Factor] + + # signals_all 必须全部满足的信号,允许为空 + signals_all: List[Signal] = field(default_factory=list) + + # signals_any 满足其中任一信号,允许为空 + signals_any: List[Signal] = field(default_factory=list) + + # signals_not 不能满足其中任一信号,允许为空 + signals_not: List[Signal] = field(default_factory=list) + + name: str = "" + + def __post_init__(self): + if not self.factors: + raise ValueError("factors 不能为空") + _event = self.dump() + _event.pop("name") + + sha256 = hashlib.sha256(str(_event).encode("utf-8")).hexdigest().upper()[:4] + if self.name: + self.name = self.name.split("#")[0] + f"#{sha256}" + # self.name = f"{self.name}#{sha256}" + else: + self.name = f"{self.operate.value}#{sha256}" + self.sha256 = sha256 + + @property + def unique_signals(self) -> List[str]: + """获取 Event 的唯一信号列表""" + signals = [] + if self.signals_all: + signals.extend(self.signals_all) + if self.signals_any: + signals.extend(self.signals_any) + if self.signals_not: + signals.extend(self.signals_not) + + for factor in self.factors: + signals.extend(factor.unique_signals) + + signals = {x.signal if isinstance(x, Signal) else x for x in signals} + return list(signals) + + def get_signals_config(self, signals_module: str = "chanlun.signals") -> List[Dict]: + """获取事件的信号配置""" + + return get_signals_config(self.unique_signals, signals_module) + + def is_match(self, s: dict): + """判断 event 是否满足 + + 代码的执行逻辑如下: + + 1. 首先判断 signals_not 中的信号是否得到满足,如果满足任意一个信号,则直接返回 False,表示事件不满足。 + 2. 接着判断 signals_all 中的信号是否全部得到满足,如果有任意一个信号不满足,则直接返回 False,表示事件不满足。 + 3. 然后判断 signals_any 中的信号是否有一个得到满足,如果一个都不满足,则直接返回 False,表示事件不满足。 + 4. 最后判断因子是否满足,顺序遍历因子列表,找到第一个满足的因子就退出,并返回 True 和该因子的名称,表示事件满足。 + 5. 如果遍历完所有因子都没有找到满足的因子,则返回 False,表示事件不满足。 + """ + if self.signals_not and any(signal.is_match(s) for signal in self.signals_not): + return False, None + + if self.signals_all and not all(signal.is_match(s) for signal in self.signals_all): + return False, None + + if self.signals_any and not any(signal.is_match(s) for signal in self.signals_any): + return False, None + + for factor in self.factors: + if factor.is_match(s): + return True, factor.name + + return False, None + + def dump(self) -> dict: + """将 Event 对象转存为 dict""" + signals_all = [x.signal for x in self.signals_all] if self.signals_all else [] + signals_any = [x.signal for x in self.signals_any] if self.signals_any else [] + signals_not = [x.signal for x in self.signals_not] if self.signals_not else [] + factors = [x.dump() for x in self.factors] + + raw = { + "name": self.name, + "operate": self.operate.value, + "signals_all": signals_all, + "signals_any": signals_any, + "signals_not": signals_not, + "factors": factors, + } + return raw + + @classmethod + def load(cls, raw: dict): + """从 dict 中创建 Event + + :param raw: 样例如下 + {'name': '单测', + 'operate': '开多', + 'factors': [{'name': '测试', + 'signals_all': ['15分钟_倒0笔_长度_大于5_其他_其他_0'], + 'signals_any': [], + 'signals_not': []}], + 'signals_all': ['15分钟_倒0笔_方向_向上_其他_其他_0'], + 'signals_any': [], + 'signals_not': []} + :return: + """ + # 检查输入参数是否合法 + assert raw["operate"] in Operate.__dict__["_value2member_map_"], f"operate {raw['operate']} not in Operate" + assert raw["factors"], "factors can not be empty" + + e = Event( + name=raw.get("name", ""), + operate=Operate.__dict__["_value2member_map_"][raw["operate"]], + factors=[Factor.load(x) for x in raw["factors"]], + signals_all=[Signal(x) for x in raw.get("signals_all", [])], + signals_any=[Signal(x) for x in raw.get("signals_any", [])], + signals_not=[Signal(x) for x in raw.get("signals_not", [])], + ) + return e + + +class SignalsParser: + """解析一串信号,生成信号函数配置""" + + def __init__(self, signals_module: str = "chanlun.signals"): + """ + + 函数执行逻辑: + + 1. 将传入的 signals_module 参数赋给实例变量 self.signals_module,代表信号函数所在的模块,默认模块是czsc库的signals模块。 + 2. 使用 import_by_name 函数导入了指定名称的模块 signals_module。 + 3. 对于导入的模块中的每个属性名进行遍历: + - 魔法函数和私有函数不进行处理。 + - 获取函数的注解信息,并通过正则表达式获取注解中的参数模板和信号列表。 + - 如果解析到了参数模板,则将其存储在 sig_pats_map 中,key是函数名称。 + - 如果解析到了信号列表,则将其存储在 sig_name_map 中,并且为每个信号创建了 Signal 对象并存储在列表中,key是函数名称。 + 4. 最后将得到的 sig_name_map 和 sig_pats_map 存储在实例变量中,以便其他方法使用。 + + :param signals_module: 指定信号函数所在模块 + """ + self.signals_module = signals_module + sig_name_map = {} + sig_pats_map = {} + sig_trigger_map = {} + + signals_module = import_by_name(signals_module) + for name in dir(signals_module): + if "_" not in name or name.startswith("__"): + continue + + try: + doc = getattr(signals_module, name).__doc__ + # 解析信号函数参数 + pats = re.findall(r"参数模板:\"(.*)\"", doc) + if pats: + sig_pats_map[name] = pats[0] + + # 解析信号列表 + sigs = re.findall(r"Signal\('(.*)'\)", doc) + if sigs: + sig_name_map[name] = [Signal(x) for x in sigs] + + # 解析触发条件 + 触发匹配 = re.findall(r"触发条件:(.*)", doc) + if 触发匹配: + sig_trigger_map[name] = [x.strip() for x in 触发匹配[0].split(",")] + + except (OSError, ImportError, TypeError, ValueError, AttributeError) as e: + logger.error(f"解析信号函数 {name} 出错:{e}") + + self.sig_name_map = sig_name_map + self.sig_pats_map = sig_pats_map + self.sig_trigger_map = sig_trigger_map + + def parse_params(self, name, signal): + """获取信号函数参数 + + 函数执行逻辑: + + 1. 首先根据传入的 name 和 signal 参数,通过 Signal(signal).key 获取一个键值。 + 2. 然后从实例变量 sig_pats_map 中获取与指定名称对应的参数模板,并将其存储在 pats 中。 + 3. 如果没有找到参数模板,则返回 None。 + 4. 最后将信号函数的完整名称存储在参数字典中,并返回参数字典。 + + :param name: 信号函数名称, 如:cxt_bi_end_V230222 + :param signal: 需要解析的信号, 如:15分钟_D1K_量柱V221218_低量柱_6K_任意_0 + :return: + """ + key = Signal(signal).key + pats = self.sig_pats_map.get(name, None) + if not pats: + return None + + try: + params = parse(pats, key).named # type: ignore + if "di" in params: + params["di"] = int(params["di"]) + + params["name"] = f"{self.signals_module}.{name}" + + # 附加上下文:触发条件与函数短名(供 信号计算器 优化用) + 触发条件 = self.sig_trigger_map.get(name) + if 触发条件: + params["触发条件"] = 触发条件 + params["_func_short_name"] = name + + return params + except (ValueError, KeyError, TypeError, AttributeError) as e: + logger.error(f"解析信号 {signal} - {name} - {pats} 出错:{e}") + return None + + def get_function_name(self, signal: str): + """获取信号对应的信号函数名称 + + 函数执行逻辑: + + 1. 创建一个 _signal 对象,通过传入的信号字符串进行初始化。 + 2. 通过遍历 sig_name_map 中的项目,找出那些与 _signal.k3 相匹配的键,并将它们存储在 _k3_match 列表中。 + 3. 如果只有一个匹配项,则返回该项;否则记录错误日志并返回 None。 + + :param signal: 信号,数据样例:15分钟_D1K_量柱V221218_低量柱_6K_任意_0 + :return: 信号函数名称 + """ + sig_name_map = self.sig_name_map + _signal = Signal(signal) + _k3_match = list({k for k, v in sig_name_map.items() if v[0].k3 == _signal.k3}) + + # 多匹配时排除模板函数(以 "模板_" 开头) + if len(_k3_match) > 1: + non_template = [k for k in _k3_match if not k.startswith("模板_")] + if len(non_template) == 1: + return non_template[0] + + if len(_k3_match) == 1: + return _k3_match[0] + else: + logger.error(f"信号 {signal} 有多个匹配函数:{_k3_match},请手动解析信号") + return None + + def config_to_keys(self, config: List[Dict]): + """将信号函数配置转换为信号key列表 + + 函数执行逻辑: + + 1. 首先创建了一个空列表 keys 用于存储信号key。 + 2. 对于传入的 config 列表中的每个配置字典 conf 进行以下操作: + - 获取信号函数的名称。 + - 如果该信号函数的名称在 self.sig_pats_map 中存在对应的模板,使用参数填充模板,并将结果添加到 keys 列表中。 + + :param config: 信号函数配置 + + config = [{'freq': '日线', 'max_overlap': '3', 'name': 'czsc.signals.cxt_bi_end_V230222'}, + {'freq1': '日线', 'freq2': '60分钟', 'name': 'czsc.signals.cxt_zhong_shu_gong_zhen_V221221'}] + + :return: 信号key列表 + """ + keys = [] + for conf in config: + name = conf["name"].split(".")[-1] + if name in self.sig_pats_map: + keys.append(self.sig_pats_map[name].format(**conf)) + return keys + + def parse(self, signal_seq: List[str]): + """解析信号序列 + + 函数执行逻辑: + + 1. 接受一个signal_seq 参数。 + 2. 定义一个空列表res ,用于存储解析结果。 + 3. 遍历信号序列signal_seq 中的每一个信号: + + - 调用get_function_name 方法,以信号为参数,获取该信号对应的函数名。 + - 进行函数名存在性判断,name 在sig_pats_map 中存在, + 调用parse_params 方法,以函数名和信号为参数,解析参数并返回结果。 + + :param signal_seq: 信号序列, 样例: + ['15分钟_D1K_量柱V221218_低量柱_6K_任意_0', '日线_D1K_量柱V221218_低量柱_6K_任意_0'] + :return: 信号函数配置 + """ + res = [] + for signal in signal_seq: + name = self.get_function_name(signal) + if name in self.sig_pats_map: + row = self.parse_params(name, signal) + if row and row not in res: + res.append(row) + else: + logger.warning(f"未找到解析函数:{name},请手动解析信号:{signal}") + return res + + +def get_signals_config(signals_seq: List[str], signals_module: str = "") -> List[Dict]: + """获取信号列表对应的信号函数配置 + + 函数执行逻辑: + + 1. 首先创建了一个 SignalsParser 类的实例对象 sp,传入了参数 signals_module进行初始化, + 初始化工作主要是解析signals_module下的信号函数,生成了sig_pats_map信号参数模板字典和sig_name_map信号列表字典。 + 2. 然后使用 sp 实例调用 parse 方法,该方法解析 signals_seq 中的信号,并返回信号函数的配置信息。 + + :param signals_seq: 信号列表 + :param signals_module: 信号函数所在模块 + :return: 信号函数配置 + """ + sp = SignalsParser(signals_module=signals_module) + conf = sp.parse(signals_seq) + return conf + + +@注册 +def create_single_signal(**kwargs) -> OrderedDict: + """创建单个信号""" + s = OrderedDict() + k1, k2, k3 = kwargs.get("k1", "任意"), kwargs.get("k2", "任意"), kwargs.get("k3", "任意") + v1, v2, v3 = kwargs.get("v1", "任意"), kwargs.get("v2", "任意"), kwargs.get("v3", "任意") + v = Signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2, v3=v3, score=kwargs.get("score", 0)) + s[v.key] = v.value + return s + + +# ============================================================================== +# Position — 持仓管理 +# ============================================================================== + + +class Position: + def __init__( + self, + symbol: str, + opens: List[Event], + exits: List[Event] = [], + interval: int = 0, + timeout: int = 1000, + stop_loss=1000, + T0: bool = False, + name=None, + ): + """简单持仓对象,仓位表达:1 持有多头,-1 持有空头,0 空仓 + + :param symbol: 标的代码 + :param opens: 开仓交易事件列表 + :param exits: 平仓交易事件列表,允许为空 + :param interval: 同类型开仓间隔时间,单位:秒;默认值为 0,表示同类型开仓间隔没有约束 + 假设上次开仓为多头,那么下一次多头开仓时间必须大于 上次开仓时间 + interval;空头也是如此。 + :param timeout: 最大允许持仓K线数量限制为最近一个开仓事件触发后的 timeout 根基础周期K线 + :param stop_loss: 最大允许亏损比例,单位:BP, 1BP = 0.01%;成本的计算以最近一个开仓事件触发价格为准 + :param T0: 是否允许T0交易,默认为 False 表示不允许T0交易 + :param name: 仓位名称,默认值为第一个开仓事件的名称 + """ + assert name, "name 是必须的参数" + self.symbol = symbol + self.opens = opens + self.name = name + self.exits = exits if exits else [] + self.events = self.opens + self.exits + for event in self.events: + assert event.operate in [Operate.LO, Operate.LE, Operate.SO, Operate.SE] + + self.interval = interval + self.timeout = timeout + self.stop_loss = stop_loss + self.T0 = T0 + + self.pos_changed = False # 仓位是否发生变化 + self.operates = [] # 事件触发的操作列表 + self.holds = [] # 持仓状态列表 + self.pos = 0 + + # 辅助判断的缓存数据 + self.last_event = { + "dt": None, + "bid": None, + "price": None, + "op": None, + "op_desc": None, + } + self.last_lo_dt = None # 最近一次开多交易的时间 + self.last_so_dt = None # 最近一次开空交易的时间 + self.end_dt = None # 最近一次信号传入的时间 + + def __repr__(self): + return f"Position(name={self.name}, symbol={self.symbol}, opens={[x.name for x in self.opens]}, timeout={self.timeout}, stop_loss={self.stop_loss}BP, T0={self.T0}, interval={self.interval}s)" + + @property + def unique_signals(self) -> List[str]: + """获取所有事件的唯一信号列表""" + signals = [] + for e in self.events: + signals.extend(e.unique_signals) + return list(set(signals)) + + def get_signals_config(self, signals_module: str = "") -> List[Dict]: + """获取事件的信号配置""" + return get_signals_config(self.unique_signals, signals_module) + + def dump(self, with_data: bool = False) -> dict: + """将对象转换为 dict""" + raw = { + "symbol": self.symbol, + "name": self.name, + "opens": [x.dump() for x in self.opens], + "exits": [x.dump() for x in self.exits], + "interval": self.interval, + "timeout": self.timeout, + "stop_loss": self.stop_loss, + "T0": self.T0, + } + if with_data: + raw.update({"pairs": self.pairs, "holds": self.holds}) + return raw + + @classmethod + def load(cls, raw: dict) -> "Position": + """从 dict 中创建 Position + :param raw: 样例如下 + :return: + """ + pos = Position( + name=raw["name"], + symbol=raw["symbol"], + opens=[Event.load(x) for x in raw["opens"] if raw.get("opens")], + exits=[Event.load(x) for x in raw["exits"] if raw.get("exits")], + interval=raw["interval"], + timeout=raw["timeout"], + stop_loss=raw["stop_loss"], + T0=raw["T0"], + ) + return pos + + @property + def pairs(self) -> List[Dict]: + """开平交易列表 + + 返回样例: + + [{'标的代码': '000001.SH', + '交易方向': '多头', + '开仓时间': Timestamp('2020-04-17 00:00:00'), + '平仓时间': Timestamp('2020-04-20 00:00:00'), + '开仓价格': 2838.49, + '平仓价格': 2852.55, + '持仓K线数': 1, + '事件序列': '开多@站上SMA5 -> 开多@站上SMA5', + '持仓天数': 3.0, + '盈亏比例': 49.53}, + {'标的代码': '000001.SH', + '交易方向': '多头', + '开仓时间': Timestamp('2020-04-20 00:00:00'), + '平仓时间': Timestamp('2020-04-24 00:00:00'), + '开仓价格': 2852.55, + '平仓价格': 2808.53, + '持仓K线数': 4, + '事件序列': '开多@站上SMA5 -> 平多@100BP止损', + '持仓天数': 4.0, + '盈亏比例': -154.32}] + + 数据说明: + + 1. 盈亏比例,单位是 BP + 2. 持仓天数,单位是 自然日 + 3. 持仓K线数,指基础周期K线数量 + """ + pairs = [] + + for op1, op2 in zip(self.operates, self.operates[1:]): + if op1["op"] not in [Operate.LO, Operate.SO]: + continue + + ykr = op2["price"] / op1["price"] - 1 if op1["op"] == Operate.LO else 1 - op2["price"] / op1["price"] + pair = { + "标的代码": self.symbol, + "策略标记": self.name, + "交易方向": "多头" if op1["op"] == Operate.LO else "空头", + "开仓时间": op1["dt"], + "平仓时间": op2["dt"], + "开仓价格": op1["price"], + "平仓价格": op2["price"], + "持仓K线数": op2["bid"] - op1["bid"], + "事件序列": f"{op1['op_desc']} -> {op2['op_desc']}", + "持仓天数": (op2["dt"] - op1["dt"]).total_seconds() / (24 * 3600), + "盈亏比例": round(ykr * 10000, 2), # 盈亏比例 转换成以 BP 为单位的收益,1BP = 0.0001 + } + pairs.append(pair) + + return pairs + + def update(self, s: dict): + """更新持仓状态 + + 函数执行逻辑: + + - 首先,检查最新信号的时间是否在上次信号之前,如果是则打印警告信息并返回。 + - 初始化一些变量,包括操作类型(op)和操作描述(op_desc)。 + - 遍历所有的事件,检查是否与最新信号匹配。如果匹配,则记录操作类型和操作描述,并跳出循环。 + - 提取最新信号的相关信息,包括交易对符号、时间、价格和成交量。 + - 更新持仓状态的结束时间为最新信号的时间。 + - 如果操作类型是开仓(LO或SO),更新最后一个事件的信息。 + - 定义一个内部函数__create_operate,用于创建操作记录。 + - 根据操作类型更新仓位和操作记录。 + + - 如果操作类型是LO(开多),检查是否满足开仓条件,如果满足则开多仓,否则只平空仓。 + - 如果操作类型是SO(开空),检查是否满足开仓条件,如果满足则开空仓,否则只平多仓。 + - 如果当前持仓为多仓,进行多头出场的判断: + - 如果操作类型是LE(平多),平多仓。 + - 如果当前价格相对于最后一个事件的价格的收益率小于止损阈值,平多仓。 + - 如果当前成交量相对于最后一个事件的成交量的增加量大于超时阈值,平多仓。 + + - 如果当前持仓为空仓,进行空头出场的判断: + - 如果操作类型是SE(平空),平空仓。 + - 如果当前价格相对于最后一个事件的价格的收益率小于止损阈值,平空仓。 + - 如果当前成交量相对于最后一个事件的成交量的增加量大于超时阈值,平空仓。 + + - 将当前持仓状态和价格记录到持仓列表中。 + + :param s: 最新信号字典 + :return: + """ + if self.end_dt and s["dt"] <= self.end_dt: + logger.warning(f"请检查信号传入:最新信号时间{s['dt']}在上次信号时间{self.end_dt}之前") + return + + self.pos_changed = False + op = Operate.HO + op_desc = "" + for event in self.events: + m, f = event.is_match(s) + if m: + op = event.operate + op_desc = f"{event.name}@{f}" + break + + symbol = s["symbol"] + dt = s["dt"] + price = s["close"] + bid = s.get("id", s.get("bid", 0)) + self.end_dt = dt + + # 当有新的开仓 event 发生,更新 last_event + if op in [Operate.LO, Operate.SO]: + self.last_event = { + "dt": dt, + "bid": bid, + "price": price, + "op": op, + "op_desc": op_desc, + } + + def __create_operate(_op, _op_desc): + self.pos_changed = True + return { + "symbol": symbol, + "dt": dt, + "bid": bid, + "price": price, + "op": _op, + "op_desc": _op_desc, + "pos": self.pos, + } + + # 更新仓位 + if op == Operate.LO: + if self.pos != 1 and (not self.last_lo_dt or (dt - self.last_lo_dt).total_seconds() > self.interval): + # 与前一次开多间隔时间大于 interval,直接开多 + self.pos = 1 + self.operates.append(__create_operate(Operate.LO, op_desc)) + self.last_lo_dt = dt + else: + # 与前一次开多间隔时间小于 interval,仅对空头平仓 + if self.pos == -1 and (self.T0 or dt.date() != self.last_so_dt.date()): + self.pos = 0 + self.operates.append(__create_operate(Operate.SE, op_desc)) + + if op == Operate.SO: + if self.pos != -1 and (not self.last_so_dt or (dt - self.last_so_dt).total_seconds() > self.interval): + # 与前一次开空间隔时间大于 interval,直接开空 + self.pos = -1 + self.operates.append(__create_operate(Operate.SO, op_desc)) + self.last_so_dt = dt + else: + # 与前一次开空间隔时间小于 interval,仅对多头平仓 + if self.pos == 1 and (self.T0 or dt.date() != self.last_lo_dt.date()): + self.pos = 0 + self.operates.append(__create_operate(Operate.LE, op_desc)) + + # 多头出场 + if self.pos == 1 and (self.T0 or dt.date() != self.last_lo_dt.date()): + assert self.last_event["dt"] >= self.last_lo_dt + + # 多头平仓 + if op == Operate.LE: + self.pos = 0 + self.operates.append(__create_operate(Operate.LE, op_desc)) + + # 多头止损 + if price / self.last_event["price"] - 1 < -self.stop_loss / 10000: + self.pos = 0 + self.operates.append(__create_operate(Operate.LE, f"平多@{self.stop_loss}BP止损")) + + # 多头超时 + if bid - self.last_event["bid"] > self.timeout: + self.pos = 0 + self.operates.append(__create_operate(Operate.LE, f"平多@{self.timeout}K超时")) + + # 空头出场 + if self.pos == -1 and (self.T0 or dt.date() != self.last_so_dt.date()): + assert self.last_event["dt"] >= self.last_so_dt + + # 空头平仓 + if op == Operate.SE: + self.pos = 0 + self.operates.append(__create_operate(Operate.SE, op_desc)) + + # 空头止损 + if 1 - price / self.last_event["price"] < -self.stop_loss / 10000: + self.pos = 0 + self.operates.append(__create_operate(Operate.SE, f"平空@{self.stop_loss}BP止损")) + + # 空头超时 + if bid - self.last_event["bid"] > self.timeout: + self.pos = 0 + self.operates.append(__create_operate(Operate.SE, f"平空@{self.timeout}K超时")) + + self.holds.append({"dt": self.end_dt, "pos": self.pos, "price": price}) + + +class 信号计算器: + """多周期信号计算引擎 — 基于观察者字典。 + + 不再依赖 立体分析器,直接接收 ``{周期秒: 观察者}`` 字典。 + + 使用方式:: + + 分析器 = 立体分析器("btcusd", [300, 900, 3600], 配置) + 观察者字典 = {p: 分析器._单体分析器[p] for p in 分析器.周期组} + 计算器 = 信号计算器(观察者字典, 基础周期=300, 信号配置=[...]) + + for k in k线列表: + 分析器.投喂K线(k) + 计算器.更新() + print(计算器.信号字典) + """ + + def __init__( + self, + 分析器: 立体分析器, + 信号配置: Optional[List[Dict]] = None, + 信号模块: str = "", + ): + self._分析器 = 分析器 + self._观察者字典 = {p: 分析器._单体分析器[p] for p in 分析器.周期组} + self._基础周期 = 分析器.周期组[0] + self._信号模块 = 信号模块 + self._信号函数缓存: Dict[str, Callable] = {} + self.信号: dict = {} + self.行情: dict = {} + self.信号配置 = 信号配置 or [] + self._自动挂载指标() + + @property + def 信号字典(self) -> dict: # 向后兼容:合并返回 + return {**self.信号, **self.行情} + + @property + def 信号配置(self) -> List[Dict]: + return self._信号配置 + + @信号配置.setter + def 信号配置(self, value: List[Dict]): + 可用周期 = set(self._分析器.周期组) + for c in value: + freq = c.get("freq") + if freq is not None: + 周期秒 = int(freq) + if 周期秒 not in 可用周期: + raise ValueError(f"信号配置 freq={freq}({周期秒}s) 不在分析器周期组 {sorted(可用周期)} 中\n 信号: {c.get('name', '?')}") + self._信号配置 = self._去重配置(value) + self._预加载信号函数() + + def _去重配置(self, configs: List[Dict]) -> List[Dict]: + seen = set() + unique = [] + for c in configs: + key = (c.get("name"), frozenset((k, str(v)) for k, v in c.items() if k != "name")) + if key not in seen: + seen.add(key) + unique.append(c) + else: + logger.warning(f"信号计算器: 重复信号配置已跳过 — {c.get('name', '?')} { {k: v for k, v in c.items() if k != 'name'} }") + return unique + + def _预加载信号函数(self): + for config in self._信号配置: + name = config.get("name") + if name and name not in self._信号函数缓存: + try: + self._信号函数缓存[name] = self._解析信号函数(name) + except (ImportError, ModuleNotFoundError, AttributeError, KeyError) as e: + logger.warning(f"信号计算器: 无法导入 {name} ({e}),跳过") + + @staticmethod + def _解析信号函数(name: str): + """解析信号函数名,返回可调用对象。 + + 当运行在 __main__ 上下文中且目标模块为 chan 时,优先使用 __main__ + 命名空间中的函数,避免 import_by_name 触发 chan 模块的重复导入。 + """ + if "." in name: + module_name, func_name = name.rsplit(".", 1) + main_mod = sys.modules.get("__main__") + if main_mod is not None and hasattr(main_mod, func_name): + # 验证 __main__ 确实是目标模块(通过文件名判断) + main_file = getattr(main_mod, "__file__", "") + expected_path = module_name.replace(".", os.sep) + ".py" + if main_file.endswith(expected_path): + return getattr(main_mod, func_name) + + return import_by_name(name) + + def _自动挂载指标(self): + """根据信号配置参数,在对应周期的观察者上自动补全缺失的指标。""" + + 待补MACD: Dict[int, List[tuple]] = defaultdict(list) + 待补均线: Dict[int, List[tuple]] = defaultdict(list) + + for config in self._信号配置: + name = config.get("name", "") + freq = config.get("freq") + if not freq: + continue + 周期秒 = int(freq) + if 周期秒 not in self._观察者字典: + continue + + # MACD 类信号:从 config 解析 fast/slow/signal 参数 + if "macd" in name.lower() or "中枢" in name or "背驰" in name or "金叉" in name: + fast = int(config.get("fast", config.get("快线周期", 13))) + slow = int(config.get("slow", config.get("慢线周期", 31))) + sig = int(config.get("signal", config.get("信号周期", 11))) + key = f"macd_{fast}_{slow}_{sig}" + if not any(t[0] == key for t in 待补MACD[周期秒]): + 待补MACD[周期秒].append((key, "收", fast, slow, sig)) + + # MA 类信号:从 config 解析 ma_type/timeperiod + if "ma_" in name or "tas_ma" in name or "均线" in name: + ma_type = config.get("ma_type", "SMA").upper() + period = int(config.get("timeperiod", config.get("周期", 5))) + key = f"{ma_type}_{period}" + if not any(t[0] == key for t in 待补均线[周期秒]): + 待补均线[周期秒].append((key, "收", ma_type, period)) + + for 周期秒, macd_list in 待补MACD.items(): + cfg = self._观察者字典[周期秒].配置 + if not cfg.计算指标: + cfg.计算指标 = True + 已有键 = {t[0] for t in cfg.MACD_参数列表} + # 同时检查 (快线, 慢线, 信号) 参数避免只键名不同但参数相同的重复 + 已有参数 = {(t[2], t[3], t[4]) for t in cfg.MACD_参数列表 if len(t) >= 5} + 新增 = [t for t in macd_list if t[0] not in 已有键 and (t[2], t[3], t[4]) not in 已有参数] + if 新增: + cfg.MACD_参数列表.extend(新增) + if "macd" not in 已有键: + cfg.MACD_参数列表.insert(0, ("macd", "收", 新增[0][2], 新增[0][3], 新增[0][4])) + logger.warning(f"信号计算器: 周期{周期秒}s 自动补全 MACD — {[t[0] for t in 新增]}") + + for 周期秒, ma_list in 待补均线.items(): + cfg = self._观察者字典[周期秒].配置 + if not cfg.计算指标: + cfg.计算指标 = True + 已有 = {t[0] for t in cfg.均线参数列表} + 新增 = [t for t in ma_list if t[0] not in 已有] + if 新增: + cfg.均线参数列表.extend(新增) + logger.warning(f"信号计算器: 周期{周期秒}s 自动补全 均线 — {[t[0] for t in 新增]}") + + def 从信号列表提取配置(self, 信号序列: List[str]): + """从信号序列自动生成信号配置""" + self.信号配置 = get_signals_config(list(set(信号序列)), self._信号模块) + + def 更新(self): + """遍历信号配置,调用信号函数。结果写入 self.信号 和 self.行情。""" + self.信号.clear() + self.行情.clear() + + for config in self._信号配置: + try: + result = self._执行信号函数(config) + if result: + for k, v in result.items(): + if v != "任意_任意_任意_0": + self.信号[k] = v + except (TypeError, ValueError, KeyError, AttributeError, IndexError) as e: + logger.error(f"信号计算器: {config.get('name', '?')} 出错 — {e}") + traceback.print_exc() + + # OHLCV 行情 + 基础观察者 = self._观察者字典.get(self._基础周期) + if 基础观察者 and 基础观察者.普通K线序列: + 最后K线 = 基础观察者.普通K线序列[-1] + 时间戳 = 最后K线.时间戳 + if isinstance(时间戳, (int, float)): + 时间戳 = datetime.fromtimestamp(int(时间戳)) + self.行情.update( + symbol=基础观察者.符号, + dt=时间戳, + id=最后K线.序号, + open=最后K线.开盘价, + close=最后K线.收盘价, + high=最后K线.高, + low=最后K线.低, + vol=最后K线.成交量, + ) + + def _执行信号函数(self, config: Dict) -> Optional[OrderedDict]: + param = dict(config) + sig_name = param.pop("name") + sig_func = self._信号函数缓存.get(sig_name) or self._解析信号函数(sig_name) + + freq = param.get("freq") + if freq is not None: + 周期秒 = int(freq) + obs = self._观察者字典.get(周期秒) + if obs is not None: + return sig_func(obs, **param) + else: + raise KeyError(f"信号计算器: 未找到周期 {周期秒}s 的观察者,可用周期: {sorted(self._观察者字典.keys())}") + else: + return sig_func(self, **param) + + def 获取周期观察者(self, freq: str) -> Optional[观察者]: + return self._观察者字典.get(int(freq)) + + def 测试_读取数据(观察员: 观察者, 配置: 缠论配置) -> Callable[[], 观察者]: """测试_读取数据 :param 观察员: 观察者 @@ -6906,13 +8225,15 @@ def 测试_指标挂载(配置: 缠论配置): size = struct.calcsize(">6d") for i in range(len(buffer) // size): if i == 500: - 配置.MACD_参数列表 = [("macd", 配置.平滑异同移动平均线_快线周期, 配置.平滑异同移动平均线_慢线周期, 配置.平滑异同移动平均线_信号周期)] + 配置.MACD_参数列表 = [("macd", "收", 13, 31, 11)] 配置.MACD_参数列表.append(("macd_12_26_9", 12, 26, 9)) k线 = K线.读取大端字节数组(buffer[i * size : i * size + size], 周期, 符号) 观察员.增加原始K线(k线) if i == 500: assert 观察员.普通K线序列[0].指标.macd_12_26_9 is not None, "指标挂载失败" - print(观察员.普通K线序列[0].指标["macd_12_26_9"]) + print(观察员.普通K线序列[-1].指标["macd_12_26_9"]) + print(观察员.普通K线序列[-1].macd) + break 消耗用时 = datetime.now() - 启动时间 @@ -6922,10 +8243,58 @@ def 测试_指标挂载(配置: 缠论配置): return 魔法 +def 测试_信号识别(配置: 缠论配置): + 文件路径 = 配置.加载文件路径 + name = Path(文件路径).name.split(".")[0] + 符号, 周期, 起始时间戳, 结束时间戳 = name.split("-") + 周期 = int(周期) + 分析器 = 立体分析器(符号, [周期, 周期 * 5, 周期 * 5 * 6], 配置) + 信号配置 = [] + for p in [周期, 周期 * 5, 周期 * 5 * 6]: + 信号配置.extend( + get_signals_config( + [ + f"{str(p)}_D1#MACD#13#33#11_MACD交叉V260601_金叉_任意_任意_0", + ], + "signals", + ) + ) + 计算器 = 信号计算器(分析器, 信号配置, "signals") + + def 魔法(): + 启动时间 = datetime.now() + 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], 周期, 符号) + 分析器.投喂K线(k线) + + 计算器.更新() + + # 输出信号内容 + if 计算器.信号: + dt = k线.时间戳 + if isinstance(dt, (int, float)): + dt = datetime.fromtimestamp(dt) + 信号摘要 = " ".join(f"{k}→{v}" for k, v in 计算器.信号.items()) + 信号类型 = ",".join(sorted(set(v.split("_")[0] for v in 计算器.信号.values()))) + logger.info(f"[{dt}] [{信号类型}] 📡 {信号摘要}") + print(f"[{dt}] [{信号类型}] 📡 {信号摘要}, {计算器.信号}") + print() + + 消耗用时 = datetime.now() - 启动时间 + logger.info(f"测试_信号识别 {消耗用时} 普K数量 {len(分析器._单体分析器[周期].普通K线序列)}") + return 分析器 + + return 魔法 + + if __name__ == "__main__": 当前配置 = 缠论配置.不推送() 当前配置.加载文件路径 = str(Path(__file__).parent / "btcusd-300-1761327300-1776327900.nb") with tempfile.TemporaryDirectory() as tmpdir: # 测试_读取数据(观察者("", 0, 当前配置), 当前配置)().测试_保存数据(tmpdir) # 测试_周期合成(当前配置)().测试_保存数据(tmpdir) - 测试_指标挂载(当前配置)().测试_保存数据(tmpdir) + # 测试_指标挂载(当前配置)().测试_保存数据(tmpdir) + 测试_信号识别(当前配置)() diff --git a/chan.py-skill.md b/chan.py-skill.md new file mode 100644 index 0000000..51f0b6a --- /dev/null +++ b/chan.py-skill.md @@ -0,0 +1,975 @@ +# chan.py — 缠论技术分析库 AI 技能 + +> 约 6900 行 Python,从原始 K 线到买卖点的全链路流式缠论分析。所有标识符使用中文。 + +--- + +## Q1: 如何从 .nb 文件加载数据并完成分析? + +```python +from chanlun import chan + +config = chan.缠论配置(标识="btcusd") +obs = chan.观察者.读取数据文件("btcusd-300-xxx.nb", 配置=config) + +# 加载即完成全部分析,直接读取结果 +obs.笔序列 # List[虚线],标识="笔" +obs.线段序列 # List[虚线],标识="线段" +obs.中枢序列 # List[中枢],标识="线段中枢" +obs.笔_中枢序列 # List[中枢],标识="笔中枢" +obs.分型序列 # List[分型] +obs.缠论K线序列 # List[缠论K线] + +# 高级层级 +obs.扩展线段序列 # 标识="扩展线段" +obs.扩展中枢序列 # 标识="扩展线段中枢" +obs.线段_线段序列 # 线段作为笔再划分 +obs.扩展线段序列_扩展线段 # 扩展线段作为笔再划分 +``` + +`.nb` 文件格式:每根 K 线 48 字节大端 `struct.pack(">6d")`,依次为 时间戳/开/高/低/收/量(均为 float64)。 + +--- + +## Q2: 如何流式实时分析? + +```python +obs = chan.观察者(符号="btcusd", 周期=300, 配置=config) + +# 方式一:投喂原始数据 +obs.投喂原始数据(时间戳=datetime.now(), 开=50000, 高=50100, 低=49900, 收=50050, 量=100) + +# 方式二:投喂预构建 K 线对象 +obs.增加原始K线(k线对象) + +# 每次投喂都增量更新全链路:缠K → 分型 → 笔 → 线段 → 中枢 +# 结果即刻可用 +``` + +--- + +## Q3: 如何做多周期立体分析? + +```python +ma = chan.立体分析器( + 符号="btcusd", + 周期组=[300, 1800], # [输入周期, 显示周期] + 配置=config, + 配置组={1800: chan.缠论配置(笔内元素数量=7)}, # 可选:按周期覆盖配置 +) + +# 只投喂最小周期 K 线,合成器自动合成大周期 +for kline in klines_300: + ma.投喂K线(kline) + +# 获取各周期结果 +obs_300 = ma._单体分析器[300] +obs_1800 = ma._单体分析器[1800] +``` + +内部机制: +- 内置 `K线合成器` 将小周期 K 线合成为大周期 +- 合成器事件回调 → 大周期 K 线完成 → 触发对应 `观察者.__处理数据` +- 非显示周期的 `基础缠K序列` 引用显示周期的 `缠论K线序列` + +--- + +## Q4: 如何单独使用 K 线合成器? + +```python +def 回调(信号类型, 标识, 周期, 完成K线): + """信号类型: "K线完成" """ + print(f"{标识} {周期}s 完成一根K线: {完成K线.收盘价}") + +synth = chan.K线合成器(标识="btcusd", 周期组=[300, 900, 1800], 事件回调=回调) + +# 投喂最小周期数据 +synth.投喂(时间戳, 开, 高, 低, 收, 量) +# 或 +synth.投喂K线(普K) + +# 查询当前合成中的 K 线 +k = synth.获取当前K线(周期=900) + +# 支持后续设置回调 +synth.设置事件回调(新回调函数) +``` + +合成器按 `(时间戳 // 周期) * 周期` 对齐时间戳到周期边界。 + +--- + +## Q5: 缠论配置有哪些关键参数? + +```python +config = chan.缠论配置( + 标识="btcusd", + + # ---- 分析开关(按需关闭以跳过某层级)---- + 分析笔=True, # 是否分析笔 + 分析线段=True, # 是否分析线段 + 分析扩展线段=True, # 是否分析扩展线段 + 分析笔中枢=True, # 是否分析笔中枢 + 分析线段中枢=True, # 是否分析线段中枢 + 计算指标=True, # 是否计算技术指标 + + # ---- 笔参数(影响笔划分粒度)---- + 笔内元素数量=5, # 笔内最少缠K数(含端点) + 笔内相同终点取舍=False, # True=取最后, False=取第一个 + 笔内起始分型包含整笔=False, + 笔弱化=False, # 笔弱化开关 + 笔弱化_原始数量=3, # 弱化参考原始K线数量 + 笔次级成笔=False, # 次级成笔 + + # ---- 线段参数 ---- + 线段_非缺口下穿刺=False, # 非缺口状态的贯穿伤回退 + 线段_特征序列忽视老阴老阳=False, # True=忽视缺口全以无缺口对待 + 线段_缺口后紧急修正=True, # 缺口后紧急修正 + 线段_修正=False, # 短路修正(不建议使用) + 线段内部中枢图显=True, # 显示线段内部中枢 + 扩展线段_当下分析=False, # True=以当下分析, False=以事后分析 + + # ---- 指标参数 ---- + 指标计算方式="收", # "开"/"高"/"低"/"收"/"高低均值"/"高低收均值"/"开高低收均值" + 平滑异同移动平均线_快线周期=13, + 平滑异同移动平均线_慢线周期=31, + 平滑异同移动平均线_信号周期=11, + 相对强弱指数_周期=13, + 相对强弱指数_超买阈值=75, + 相对强弱指数_超卖阈值=25, + 随机指标_RSV周期=13, + 随机指标_K值平滑周期=5, + 随机指标_D值平滑周期=5, + 随机指标_超买阈值=80, + 随机指标_超卖阈值=20, + 计算BOLL=False, # 是否计算布林带 + 布林带_周期=20, + 布林带_标准差倍数=2.0, + + # ---- 多指标参数列表 ---- + MACD_参数列表=[], # [(key, 快线, 慢线, 信号), ...] + RSI_周期列表=[], # [(key, 周期), ...] + KDJ_参数列表=[], # [(key, RSV周期, K平滑, D平滑), ...] + BOLL_参数列表=[], # [(key, 周期, 标准差倍数), ...] + 均线_类型列表=[], # SMA/EMA 类型列表 + 均线_周期列表=[], # 对应周期列表 + + # ---- 买卖点 ---- + 买卖点偏移=1, # 最大偏移量 + 买卖点激进识别=False, # 激进模式不检查分型完整性 + 买卖点与MACD柱强相关=False, # True=卖点需正值 买点需负值 + 买卖点错过误差值=0.01, # 距离买卖点值的容差 + 买卖点_指标模式="配置", # "任意"/"配置"/"全量"/"相对" + 买卖点_指标匹配_MACD=True, + 买卖点_指标匹配_KDJ=True, + 买卖点_指标匹配_RSI=True, + + # ---- 线段内部背驰 ---- + 线段内部背驰_MACD=True, + 线段内部背驰_斜率=True, + 线段内部背驰_测度=True, + 线段内部背驰_模式="相对", # "任意"/"配置"/"全量"/"相对" + + # ---- 推送/图显 ---- + 推送K线=True, 推送笔=True, 推送线段=True, 推送中枢=True, + # ...另有 图表展示, 图表展示_笔, _线段, _中枢_* 等 20+ 图显字段 +) +``` + +全部 74 个字段通过 `config.model_fields()` 获取。 + +--- + +## Q6: 如何创建和对比配置? + +```python +# 构造 +c1 = chan.缠论配置(笔内元素数量=5, 线段_缺口后紧急修正=True) +c2 = chan.缠论配置(笔内元素数量=7, 线段_缺口后紧急修正=False) + +# 差异对比 → {字段名: 新值} +c1.对比(c2) # {"笔内元素数量": 7} + +# 深拷贝 + 部分更新 +c3 = c1.model_copy(update={"笔内元素数量": 8}, deep=True) + +# 创建不推送变体(所有推送标志 = False) +c_no_push = c1.不推送() + +# 序列化 +c1.to_dict() # dict +c1.to_json() # JSON 字符串 +c1.保存配置("path.json") + +# 反序列化 +chan.缠论配置.from_dict(data) +chan.缠论配置.from_json(json_str) +chan.缠论配置.加载配置("path.json") + +# 按序号重组(兼容旧版扁平键名:"1_笔模式" → {1: {笔模式: ...}}) +chan.缠论配置.按序号重组字典(默认配置, 原始字典) +``` + +--- + +## Q7: 虚线的标识有哪些?如何区分笔和线段? + +`虚线` 是笔和线段的通用数据结构: + +```python +虚线(序号, 标识, 文, 武, 级别, 有效性=True) +# 文=起点分型, 武=终点分型 + +# 标识类型 +"笔" # 来自 笔.分析 +"线段" # 来自 线段.分析 +"扩展线段" # 来自 线段.扩展分析 +"线段_线段" # 线段再做一次线段分析 +"扩展线段_扩展线段" # 扩展线段再做一次线段分析 +``` + +关键属性和方法: +```python +seg.方向 # 相对方向.向上 / 相对方向.向下 +seg.高 / seg.低 # max(文,武).高 / min(文,武).低 +seg.文 / seg.武 # 起点分型 / 终点分型 +seg.笔序列 # 内部笔序列(仅线段有) +seg.之前是(prev) # prev.武 == seg.文 ? +seg.之后是(next) # seg.武 == next.文 ? +seg.获取普K序列(观察员) # 虚线覆盖的原始K线区间 +seg.获取缠K序列(观察员) # 虚线覆盖的缠论K线区间 +``` + +笔和线段类本身只包含 `@staticmethod` 算法方法,不实例化: +```python +笔.分析(当前分型, 分型序列, 笔序列, 缠K序列, 普K序列, 层级, 配置) +线段.分析(笔序列, 线段序列, 配置, 层级=0) +``` + +--- + +## Q8: 如何判断背驰? + +```python +# 需要离开段和进入段(都是虚线对象) +进入段 = obs.线段序列[-2] # 倒数第二段 +离开段 = obs.线段序列[-1] # 最后一段 + +# 单项背驰 +chan.背驰分析.MACD背驰(进入段, 离开段, obs.普通K线序列, 方式="总") +chan.背驰分析.斜率背驰(进入段, 离开段) +chan.背驰分析.测度背驰(进入段, 离开段) + +# 组合背驰 +chan.背驰分析.全量背驰(进入段, 离开段, obs.普通K线序列) # 三项全满足 +chan.背驰分析.任意背驰(进入段, 离开段, obs.普通K线序列) # 至少一项 +chan.背驰分析.任选背驰(进入段, 离开段, obs.普通K线序列) # 至少两项 + +# 按配置组合 +chan.背驰分析.配置背驰(进入段, 离开段, obs.普通K线序列, config) +chan.背驰分析.背驰模式(进入段, 离开段, obs.普通K线序列, config, "全量") + +# 判断线段内部是否背驰 +chan.线段.判断线段内部是否背驰(离开段, obs) # 分析段内中枢背驰 +``` + +MACD 背驰也可通过虚线的类方法直接判断: +```python +chan.虚线.武之全量MACD均值(obs.普通K线序列, 离开段) # 武端MACD < 均值? +chan.虚线.武之MACD均值(obs.普通K线序列, 离开段) # 按方向比较 +chan.虚线.武之MACD极值(obs.普通K线序列, 离开段) # 武端是否为极值? +chan.虚线.买卖意义(离开段, obs) # 盘整背驰 → (bool, 描述) +``` + +--- + +## Q9: 中枢如何操作? + +```python +# 中枢 = 三段连续虚线重叠区间 +hub = chan.中枢.创建(左段, 中段, 右段, 级别=0, 标识="线段中枢") + +# 属性 +hub.方向 # 基础序列[0].方向.翻转() +hub.高 / hub.低 # 中枢区间: min(前三段.高) / max(前三段.低) +hub.高高 / hub.低低 # 全部延伸段的最高/最低 +hub.离开段 # 基础序列[-1] +hub.第三买卖线 # 第三类买卖点参考线 +hub.当前状态() # "中枢之中" / "中枢之上" / "中枢之下" + +# 延伸与扩展 +hub._添加虚线(新段) # 中枢延伸 +hub.获取扩展中枢(扩展中枢列表, config) # 9段以上获得扩展中枢 + +# 类方法 +chan.中枢.基础检查(左, 中, 右) # 三段是否首尾相连 +chan.中枢.从序列中获取中枢(虚线序列, 起始方向, 标识) +chan.中枢.分析(虚线序列, 中枢序列) # 自动识别中枢 +``` + +--- + +## Q10: 如何判断中枢的第三类买卖点? + +第三类买卖点出自中枢"离开-回抽不破": + +```python +# 分割序列获取第三买卖线 +前, 后, 第三买卖线, 贯穿伤 = chan.线段.分割序列(离开段, 所属中枢=hub) +# 第三买卖线 != None → 存在第三类买卖点 + +# 或通过中枢完整性验证 +hub.完整性(虚实="实") # 验证第三买卖点是否有效 +``` + +--- + +## Q11: 买卖点如何生成? + +`观察者.识别买卖点()` 当前为空实现(`pass`)。买卖点类型已定义,可手动创建: + +```python +# 买卖点类型枚举 (18种) +chan.买卖点类型.一买 / chan.买卖点类型.一卖 +chan.买卖点类型.二买 / chan.买卖点类型.二卖 +chan.买卖点类型.三买 / chan.买卖点类型.三卖 +chan.买卖点类型.类一买 / chan.买卖点类型.类一卖 +chan.买卖点类型.类二买 / chan.买卖点类型.类二卖 +chan.买卖点类型.T1B买 / chan.买卖点类型.T1B卖 +chan.买卖点类型.T2B买 / chan.买卖点类型.T2B卖 +chan.买卖点类型.T3B买 / chan.买卖点类型.T3B卖 + +# 工厂方法 +chan.买卖点.一买点(买卖点分型, 当前K线, 标识, 备注, 中枢破位值) +chan.买卖点.一卖点(买卖点分型, 当前K线, 标识, 备注, 中枢破位值) +# ... 二买点 / 二卖点 / 三买点 / 三卖点 / ... + +# 属性 +bsp.类型 # 买卖点类型枚举 +bsp.当前K线 # 关联K线 +bsp.买卖点分型 # 关联分型 +bsp.破位值 # 中枢破位值 +bsp.有效性 # 是否有效 +bsp.偏移 / bsp.失效偏移 +``` + +--- + +## Q12: 技术指标如何计算和访问? + +指标通过 `指标计算器` 自动挂载到每根 K 线的 `指标` 容器: + +```python +# 自动挂载(观察者内部调用) +指标计算器.计算并挂载(当前K线, 全序列, config) + +# 访问指标 +k线.指标.macd # 平滑异同移动平均线 或 None +k线.指标.rsi # 相对强弱指数 或 None +k线.指标.kdj # 随机指标 或 None +k线.指标.boll # 布林带 或 None + +# MACD 字段 +macd.DIF / macd.DEA / macd.MACD柱 / macd.快线EMA / macd.慢线EMA + +# 手动计算(不依赖观察者) +macd1 = 平滑异同移动平均线.首次计算(收盘价, 时间戳, 快线周期=12, 慢线周期=26, 信号周期=9) +macd2 = 平滑异同移动平均线.增量计算(macd1, 新收盘价, 新时间戳) +# 或从 K 线直接计算 +macd = 平滑异同移动平均线.首次计算_K线(k线, 计算方式="收盘价") +macd = 平滑异同移动平均线.增量计算_K线(前MACD, k线, 计算方式="收盘价") + +# RSI / KDJ / BOLL 同理 +rsi = 相对强弱指数.首次计算(收盘价, 时间戳, 周期=14) +rsi = 相对强弱指数.增量计算(前RSI, 新收盘价, 新时间戳) + +kdj = 随机指标.首次计算(最高价, 最低价, 收盘价, 时间戳, N=9, M1=3, M2=3) +kdj = 随机指标.增量计算(前KDJ, 最高价, 最低价, 收盘价, 时间戳) + +boll = 布林带.首次计算(k线, 计算方式="收盘价", 周期=20, 标准差倍数=2.0) +boll = 布林带.增量计算(前BOLL, k线, 计算方式="收盘价") +``` + +配置变更后需回填指标: +```python +指标计算器._回填新指标(全序列, config) # 重算所有K线的指标 +``` + +--- + +## Q13: 如何验证双端(Rust/Python)分析结果一致? + +```python +from chanlun import chan as chan_rs # Rust 绑定 +from chanlun import chan as chan_py # Python 参考 + +# 逐类型相等性检查(返回 (bool, 原因)) +chan.K线相等(k_rs, k_py, 浮点容差=1e-9) +chan.缠论K线相等(ck_rs, ck_py, 浮点容差=1e-9) +chan.分型相等(fx_rs, fx_py, 浮点容差=1e-9) +chan.虚线相等(dl_rs, dl_py, 浮点容差=1e-9) +chan.中枢相等(hub_rs, hub_py, 浮点容差=1e-9) +chan.观察者相等(obs_rs, obs_py, 浮点容差=1e-9) +chan.立体分析器相等(ma_rs, ma_py, 浮点容差=1e-9) +``` + +所有相等函数返回 `(True, "")` 或 `(False, "差异描述")`。 + +--- + +## Q14: K 线如何创建和序列化? + +```python +# 创建 +k = chan.K线.创建普K( + 标识="btcusd", 时间戳=datetime.now(), + 开盘价=50000, 最高价=50100, 最低价=49900, 收盘价=50050, + 成交量=100, 序号=0, 周期=300, +) + +# 属性 +k.方向 # 相对方向(开盘价 vs 收盘价) +k.指标 # 指标容器 + +# 二进制序列化 (48字节大端,兼容 Rust) +data = bytes(k) # __bytes__ → struct.pack(">6d") + +# 从二进制解析 +k2 = chan.K线.读取大端字节数组(data, 周期=300, 标识="btcusd") + +# 批量保存/加载 +chan.K线.保存到DAT文件("output.dat", k线列表) + +# 区间截取 +subset = chan.K线.截取(序列, 始=k起始, 终=k结束) + +# 区间MACD面积 +chan.K线.获取MACD(序列, 始, 终) # {"MACD正面积": ..., "MACD负面积": ...} +``` + +--- + +## Q15: 缠论K线包含处理逻辑是什么? + +```python +# 缠论K线.分析 的返回值 +状态, 分型 = chan.缠论K线.分析(当前K线, 缠K序列, 普K序列, config) + +# 状态值含义 +"缠K完成" # 形成独立的缠论K线,可能伴随顶/底分型 +"包含处理中" # 当前K线被包含处理,未形成独立缠K +"等" # 等待更多K线(如方向未确定) +``` + +缠K 方向由包含处理决定,有向上和向下两种状态。包含处理规则: +- 同向:取 高者之高、低者之低(向上)/ 高者之低、低者之高(向下) +- 异向:先确定方向,再按方向处理 + +--- + +## Q16: 分型如何识别? + +```python +# 分型由左中右三根缠K构成 +fx = chan.分型(左=缠K_左, 中=缠K_中, 右=缠K_右) +fx.结构 # 分型结构.顶分型 / 分型结构.底分型 / 分型结构.三连向上 / 分型结构.三连向下 + +# 分型结构分析 +chan.分型结构.分析(左, 中, 右, 可以逆序包含=False, 忽视顺序包含=False) +# 返回: 三连向上 / 三连向下 / 顶分型 / 底分型 / 向右扩散 / None + +# 分型辅助方法 +chan.分型.判断分型(左分型, 右分型, 模式="中") +chan.分型.从缠K序列中获取分型(缠K序列, 中间缠K) +chan.分型.向序列中添加(分型序列, 新分型) # 自动处理冲突 + +# 分型属性 +fx.分型特征值 # 中.高 + 中.低 * sign(顶=-1, 底=+1) +fx.关系组 # (左→中, 中→右, 左→右) 相对方向元组 +fx.强度 # 高低差比例 +``` + +--- + +## Q17: 如何获取虚线范围内的所有停顿位置? + +```python +# 获取线段中所有笔的端点停顿位置 +停顿 = chan.线段.获取所有停顿位置(段, 观察员) +# 返回笔级虚线列表,可用于判断中枢区间 + +# 四象归类 +象 = chan.线段.四象(段) +# "老阳" / "小阳" / "少阴" / "老阴" +``` + +--- + +## Q18: 数据如何保存和恢复? + +```python +# 观察者保存到 .nb 文件(48字节/根格式) +path = obs.测试_保存数据(root="/tmp") # 返回文件路径 + +# 从 .nb 文件恢复 +obs.加载本地数据(path) + +# 立体分析器保存 +ma.测试_保存数据(root="/tmp") + +# 静态重新分析(从已有K线全量重建所有序列) +obs.静态重新分析() +``` + +--- + +## Q19: 相对方向的判断逻辑? + +```python +chan.相对方向.分析(前高, 前低, 后高, 后低) +# 返回: 向上 / 向下 / 包含 / 向上缺口 / 向下缺口 / 衔接 + +# 方向翻转 +方向.翻转() # 向上 ←→ 向下 + +# 判断方法 +方向.是否向上() / 方向.是否向下() +方向.是否包含() / 方向.是否缺口() / 方向.是否衔接() +``` + +--- + +## Q20: 常见错误排查 + +| 现象 | 可能原因 | +|------|----------| +| 笔序列为空 | `配置.分析笔=False` 或笔内缠K不足(需 ≥ 笔内元素数量) | +| 线段序列为空 | `配置.分析线段=False` 或笔序列长度不足 | +| 中枢序列为空 | 虚线序列长度不足 3 段,或三段不重叠 | +| 指标为 None | 指标未挂载,需先调用 `指标计算器.计算并挂载` | +| 配置变更不生效 | 需要 `指标计算器._回填新指标(全序列, 配置)` 和 `obs.静态重新分析()` | +| 买卖点序列为空 | `识别买卖点()` 是空实现,需自行调用 `买卖点.生成买卖点` | +| 多周期结果不一致 | 检查 `配置组` 是否覆盖了对应周期的配置 | +| 浮点比较不通过 | 双端一致性测试使用 `浮点容差=1e-9` | + +--- + +## Q21: 三个线段序列组有什么不同?分别用于什么场景? + +`观察者` 内部有三条并行的多级序列树,每条树有 3 层(由 `线段分析层次=3` 控制): + +```python +# 1. 线段序列组 — 标准线段递归 +obs.线段序列组[0] # 标识="线段" 笔→线段(标准) +obs.线段序列组[1] # 标识="线段<线段>" 线段→线段(段作为笔再划段) +obs.线段序列组[2] # 标识="线段<线段<线段>" 再递归一层 + +# 2. 扩展线段序列组 — 扩展分析递归 +obs.扩展线段序列组[0] # 标识="扩展线段" 笔→扩展线段 +obs.扩展线段序列组[1] # 标识="扩展线段<扩展线段>" 扩展段→段 +obs.扩展线段序列组[2] # 标识="扩展线段<扩展线段<扩展线段>>" 再递归 + +# 3. 混合扩展线段序列组 — 混合递归(扩展线段分析标准线段) +obs.混合扩展线段序列组[0] # 标识="扩展线段<线段>" 线段→扩展线段 +obs.混合扩展线段序列组[1] # 标识="扩展线段<线段<线段>>" 线段<线段>→扩展线段 +obs.混合扩展线段序列组[2] # 标识="扩展线段<线段<线段<线段>>>" 再递归 +``` + +**生成流程**(`静态重新分析` 中可见): +1. 笔序列 → `线段.分析()` → 线段序列组[0] +2. 线段序列组[0] → `线段.分析()` → 线段序列组[1](线段作为笔再划分) +3. 线段序列组[1] → `线段.分析()` → 线段序列组[2] +4. 笔序列 → `线段.扩展分析()` → 扩展线段序列组[0] +5. 扩展线段序列组[0] → `线段.扩展分析()` → 扩展线段序列组[1] +6. 扩展线段序列组[1] → `线段.扩展分析()` → 扩展线段序列组[2] +7. 线段序列组[i] → `线段.扩展分析()` → 混合扩展线段序列组[i] + +**实际验证**(2500 根 BTCUSD 300s K 线): +``` +线段序列组: 26段, 3段<线段>, 无更高级 +扩展线段序列组: 48段, 14段<扩展>, 4段<扩展<扩展>> +混合扩展线段组: 8段<线段>, 1段<线段<线段>> +``` + +**关键差异**: +- `线段.分析` 要求笔/线段之间**方向交替**(上下上下) +- `线段.扩展分析` 允许**同向连续**(将同向虚线合并处理),因此产生更多扩展段 + +等效属性访问(@property): +```python +obs.线段序列 = obs.线段序列组[0] +obs.线段_线段序列 = obs.线段序列组[1] +obs.扩展线段序列 = obs.扩展线段序列组[0] +obs.扩展线段序列_扩展线段 = obs.扩展线段序列组[1] +obs.混合扩展线段序列 = obs.混合扩展线段序列组[0] +``` + +--- + +## Q22: 特征序列是什么?老阴/老阳/小阳/少阴如何区分? + +**特征序列**是线段划分算法的核心概念。当从笔划分线段时,每根笔的方向与线段方向之间有一个关系: +- **线段方向向上** → 特征序列取每笔的"向下"特征(特征方向=向下) +- **线段方向向下** → 特征序列取每笔的"向上"特征(特征方向=向上) + +特征序列用于处理**缺口**和**包含**:多笔同特征方向的要素需要先做包含处理,再通过特征序列的分型来判断线段是否终结。 + +**线段特征**(`线段特征` 类)是对应笔的抽象: +```python +线段特征(标识, 基础序列, 线段方向) +# 持有一组同向虚线(笔),是线段划分的中间结构 +# 特征.方向 = 线段方向的翻转 +# 特征.高/特征.低 → 按线段方向取极值 +``` + +**四象**描述线段与缺口的关系: +```python +线段.四象(段) +# "老阳": 向下线段 + 存在前一缺口(向下线段后有向上缺口) +# "老阴": 向上线段 + 存在前一缺口(向上线段后有向下缺口) +# "小阳": 向上线段 + 无缺口(普通向上) +# "少阴": 向下线段 + 无缺口(普通向下) +``` + +**老阴老阳在特征序列中的作用**: +当 `线段_特征序列忽视老阴老阳=False`(默认)时: +- 老阳/老阴的特征序列**不参与**包含处理——缺口状态下的特征序列元素直接跳过包含合并 +- 相当于"缺口破坏了特征序列的连续性" + +当 `线段_特征序列忽视老阴老阳=True` 时: +- **忽略缺口**,所有特征序列元素都按无缺口处理(严格包含) +- 实际效果:线段和中枢数量都**增加**(更多特征序列参与包含处理,产生更多分型终结) + +**验证数据**(1500 根 K 线): +``` +忽视老阴老阳=False: 线段数=374, 中枢数=48 +忽视老阴老阳=True: 线段数=390, 中枢数=56 (+16段, +8中枢) +``` + +--- + +## Q23: 笔弱化、笔次级成笔到底改变了什么? + +### 笔弱化 (`笔弱化=True`) + +当一笔只有 3 个内部缠K(`武.中.序号 - 文.中.序号 + 1 == 3`)且下一分型直接穿透它时: +- 向上笔 + 下一底分型低于该笔的低点 → 弹出这笔 → 重分析 +- 向下笔 + 下一顶分型高于该笔的高点 → 弹出这笔 → 重分析 + +效果是**移除过短的阻挡笔**,让分型能连接更合理的笔。实测效果: + +``` +1500根K线: + 笔弱化=False: 笔数=2796, 线段数=374 + 笔弱化=True: 笔数=3630, 线段数=500 + +5000根K线: + 笔弱化=False: 笔数=9358, 线段数=1261 + 笔弱化=True: 笔数=11878, 线段数=1678 +``` + +笔弱化开启后笔数和线段数都**显著增加**(~30%),因为移除阻挡笔后产生了更多有效笔和段。 + +### 笔弱化_原始数量 (`笔弱化_原始数量=3`) + +控制笔弱化判断时参考的原始K线数量。不同的值对结果影响: + +``` +笔弱化_原始数量=3: 笔数=3556 +笔弱化_原始数量=5: 笔数=3556 (无变化) +笔弱化_原始数量=7: 笔数=3556 (无变化) +``` + +当前实现中此参数变化不明显,因为笔弱化主要受"原始分型数量为3"这个硬编码条件驱动。 + +### 笔次级成笔 (`笔次级成笔=False`) + +控制是否在笔的分析中启用次级递归成笔。默认关闭。实测效果: + +``` +1500根K线: + 笔次级成笔=False: 笔数=2796, 线段数=374 + 笔次级成笔=True: 笔数=3228, 线段数=468 (+432笔, +94段) +``` + +开启后笔数和线段数都有明显增加,因为允许在笔内部递归划分子笔,产生更多有效分型。 + +--- + +## Q24: 线段修正算法 _修正 / _缺口突破 / _非缺口下穿刺 做什么? + +### _修正 (短路修正, 配置.线段_修正) + +当线段基础序列 ≥ 9 且后半段满足特定条件时,将一段拆分为两段: +1. 分割序列 → 前段+后段 +2. 后段 ≥ 6 个元素且偶数 +3. 后段倒数第3和第1元素方向 = 线段方向 +4. 满足条件 → 拆分为 新段1(后段[:-3]) + 新段2(后段[-3:]) +5. 标记 `短路修正=True`,清空老阴老阳段的缺口 + +### _缺口突破 (缺省启用,无配置开关) + +处理有缺口的线段序列: +- 当前线段特征序列右元素 ≠ None +- 特征序列方向与线段方向匹配 +- 连续特征序列长度 ≥ 5 且奇数 +- 弹出当前段 → 根据特征分型终结点切分为两段 + +### _非缺口下穿刺 (配置.线段_非缺口下穿刺) + +处理"贯穿伤":当一段线段的基础序列被另一段完全穿透时修复。只有在小阳/少阴(非缺口)状态下才触发。 + +--- + +## Q25: 买卖点的实际集成路径是什么?为什么识别买卖点()是空的? + +`观察者.识别买卖点()` 是 `pass` 占位——具体买卖点识别逻辑**原本计划实现但未在 chan.py 中完成**。 + +但这不意味着买卖点不可用——可以通过以下路径手动生成: + +```python +# 路径1:手动创建(已有工厂方法) +bsp = chan.买卖点.一卖点( + 买卖点分型=obs.分型序列[-1], # 或中枢离开段终点的分型 + 当前K线=obs.普通K线序列[-1], + 标识="btcusd-300", + 备注="第一类卖点", + 中枢破位值=hub.低, # 中枢下沿 +) + +# 路径2:通过生成买卖点路由 +chan.买卖点.生成买卖点( + 特征="一卖", # 特征字符串路由到工厂 + 序号="1", + 级别="线段", + 买卖点分型=fx, + 当前缠K=ck, +) + +# 路径3:中枢完整性验证 → 判断第三买卖点 +hub.完整性(虚实="实") # 检查是否有第三买卖点条件 +``` + +买卖点类型的完整列表(18种): +``` +一买/一卖 第一类(中枢背驰转折) +二买/二卖 第二类(回抽不进中枢) +三买/三卖 第三类(离开后回抽不破中枢) +T1买/T1卖 第一类扩展 +T1P买/T1P卖 盘整型第一类 +T2买/T2卖 第二类扩展 +T2S买/T2S卖 强势第二类 +T3A买/T3A卖 第三类A型 +T3B买/T3B卖 第三类B型 +``` + +Rust 绑定层(`chanlun-py`)中的 `识别买卖点` 已实现完整逻辑,使用 `生成买卖点` 路由。 + +--- + +## 内部算法机制 + +以下方法不是公开 API,但理解它们的逻辑才能理解线段为什么这样划分、中枢为什么这样识别。 + +### 线段.分割序列 — 用终点切开基础序列 + +``` +输入: 段 (虚线), 所属中枢 (可选) +输出: (前序列, 后序列, 第三买卖线列表, 贯穿伤) +``` + +**切割规则**:遍历段.基础序列,找到第一笔满足 `笔.文 is 段.武` 的位置——从此笔开始全部归入"后",之前的归入"前"。 + +**为什么**:线段的终点分型(武)同时是下一笔的起点分型(文)。段.武 为起点的笔暂挂在当前段的基础序列里,但它已属于"下一段"范畴。分割序列区分"已确认"和"暂挂"。 + +**9 处调用各自取什么**: + +| 调用方 | 取值 | 用途 | +|--------|------|------| +| `获取内部中枢序列` | 前, 后 | 分别在前后找中枢 | +| `_缺口突破` | `[0]` 前 | 只在确认部分判断缺口 | +| `_缺口后紧急修正` | `[1]` 后 | 检查暂挂部分是否需要修正 | +| `_修正` | 前, 后 | 后≥6且方向一致 → 拆段 | +| `分析`(递归)| `[1]` 后 | 用暂挂部分做下一轮划分 | +| `判断线段内部是否背驰` | 前=阳, 后=阴 | 进入段 vs 离开段 | +| `获取所有停顿位置` | 前, 后 | 前后端点都算停顿 | +| `获取数据文本` | 全部 | 格式化输出 | + +**贯穿伤**:后[0].武 穿透了 段.文——向上段的"后"终点比"文"还低、或向下段的"后"终点比"文"还高。意味着这段根本没真正转折。 + +### 线段.获取内部中枢序列 — 阴阳合三套中枢 + +``` +输入: 段, 配置 +输出: (虚中枢列表, 实中枢列表, 合中枢列表) # 注释: 阴 阳 合 +``` + +先用 `分割序列` 把线段基础序列切成"前"(确认部分)和"后"(暂挂部分),然后分别在三块区域跑中枢识别: + +| 返回值 | 别称 | 识别范围 | 含义 | +|--------|------|----------|------| +| `实中枢序列` | 阳 | 分割序列的"前" | 段已确认部分的中枢——段主体运行阶段形成的 | +| `虚中枢序列` | 阴 | 分割序列的"后" | 段暂挂部分的中枢——离开段所在的 | +| `合中枢序列` | 合 | 整个基础序列(前+后) | 全景视角——但跨了段边界,含下一段笔 | + +**核心用途**:给 `判断线段内部是否背驰` 提供判定依据。 + +背驰判断的三种情况(对照 `判断线段内部是否背驰` 源码): + +1. **有阴**(暂挂部分非空)→ 段还未确认终点,不判断背驰(直接返回 False) +2. **有实中枢** → 检查最后一笔是否在末个阳中枢里: + - 在:取中枢起点前一笔作进入段,最后一笔作离开段 → 按配置模式判断背驰 + - 不在但末中枢有第三买卖线:取倒数第三笔和倒数第一笔比较 → 判断盘整背驰 +3. **无中枢** → 只有 3 笔时判断盘整背驰 + +**阴阳为什么分开**:同一个段内,前半(阳)是已完成的走势,后半(阴)是终点出现后的暂挂部分。背驰判断时,取阳中枢的起点前一笔作"进入段"、阳的最后一笔作"离开段",比较两者是否发生趋势衰减——这是段内部是否背驰的判定基础。 + +实测验证(2000 根 K 线): +``` +段6 [向上] 10笔: 实中枢1个[114596-114189] 虚中枢1个[115382-114523] 合中枢1个[114596-114189] +段7 [向下] 10笔: 实中枢1个[115382-114523] 虚中枢0个 合中枢2个[115382-114523][113297-112218] +段8 [向上] 8笔: 实中枢0个 虚中枢1个[111409-111008] 合中枢2个[113641-112754][111409-111008] +``` + +注意段7:虚中枢为空但合中枢有2个——因为跨"前/后"边界的笔凑出了合中枢里的第二个。但后部的笔属于下一段,这个中枢不应该算在此段头上。**判断单段是否趋势,应该用实中枢,不能用合中枢**:实中枢只看"前"(本段自己的笔),不会混入下一段的结构。 + +实测对比(2000根K线): +``` +段7: 实中枢=1(盘整) vs 合中枢=2(趋势) ← 合中枢误判 +段8: 实中枢=0(盘整) vs 合中枢=2(趋势) ← 合中枢误判 +``` +两例都被合中枢误标为趋势,实际是盘整。 + +--- + +## Q26: 如何判断线段内部是趋势还是盘整? + +```python +虚, 实, 合 = chan.线段.获取内部中枢序列(段, 观察员.配置) + +if len(实) >= 2: + if 段.方向.是否向上(): + return "上涨趋势" + else: + return "下跌趋势" +else: + return "盘整" +``` + +趋势必须有方向——段方向是向上 + 实中枢≥2 = 上涨趋势,段方向向下 + 实中枢≥2 = 下跌趋势。 + +**为什么是实中枢,不是合中枢?** + +`分割序列` 把基础序列切成"前"(本段)和"后"(下一段起始)。合中枢在"前+后"上跑,会混入下一段的笔。段7 前7笔只有1个实中枢,但跨边界的笔在合中枢凑出第2个——这个多出来的中枢不属于此段。 + +**辅助信号**: + +| 信号 | 上涨趋势 | 下跌趋势 | 盘整 | +|------|---------|---------|------| +| 段.方向 | 向上 | 向下 | 皆可 | +| 实中枢数 | ≥2 | ≥2 | ≤1 | +| `买卖意义` | 可能 False | 可能 False | 可能 True(盘整背驰) | +| `判断线段内部是否背驰` | 可能 True | 可能 True | True=盘整衰竭 | + +### 线段._刷新特征序列 — 线段级别包含处理 + +``` +输入: 段, 配置 +``` + +1. 取段.基础序列,若存在 `前一结束位置` 则从此位置-1处截取 +2. 调用 `线段特征.静态分析(基础序列, 段.方向, 四象, 忽视老阴老阳)` — 将笔序列做包含处理得到特征序列 +3. 从特征序列提取分型序列 +4. 若首分型方向与段.方向相同(特征分型终结条件)→ 截取基础序列到特征分型终点 + 调用 `_设置特征序列` + +**特征序列方向 = 线段方向的翻转**。向上线段取向下的特征(调整段),向下线段取向上的特征(反弹段)。 + +### 线段特征.静态分析 — 笔的包含处理 + +将多笔同向特征元素做包含处理(与缠K的包含处理原理相同,但在笔级别操作): +- 同向特征:取极值合并 +- 反向特征:独立成新元素 +- 老阴/老阳(有缺口)且不忽视时:跳过包含处理 + +输出一段 `List[线段特征]`,每个特征持有一组被合并的笔。 + +### 线段._武斗 — 线段终结判断 + +``` +输入: 段, 特征, 行号 +``` + +当特征序列右元素 ≠ None 时,判断线段是否被终结: +1. 特征序列方向应与线段方向相同(特征分型终结) +2. 检查特征分型是否有效(顶底交替,特征序列包含处理完毕) +3. 满足条件 → 用 `_武终` 终结线段,重置基础序列,开启新段 + +### 线段._添加线段 — 含缺口处理 + +``` +输入: 线段序列, 待添加线段, 配置, 行号 +``` + +在追加线段前,检查前一段的缺口状态: +- 若前一段 `短路修正=True` → 缺口 = None +- 否则 → 调用 `获取缺口(前一段)` +- 将缺口存入新段的 `前一缺口` + +缺口影响四象判断和后续的特征序列包含处理。 + +### 笔._相对关系 — 判断笔是否反向 + +``` +输入: 筆, 配置 +``` + +验证笔的方向是否"真正"匹配分型方向。如果 `笔内起始分型包含整笔=True`,会用分型的左中右三根缠K构造缺口区间,判断区间方向与笔方向是否一致。防止"分型形态在但实际价格不匹配"的假笔。 + +### 笔弱化触发条件 + +``` +if 配置.笔弱化 and 笔序列: + 前一笔.武.中.序号 - 前一笔.文.中.序号 + 1 == 3: # 只有3根内部缠K + if (向上笔 and 前一笔.低 > 当前分型.分型特征值 and 当前分型是底) or + (向下笔 and 前一笔.高 < 当前分型.分型特征值 and 当前分型是顶): + 弹出旧笔 → 递归重分析 +``` + +只有**最短的笔(3缠K)**且被下一分型**完全穿透**时才会触发弱化。这就是为什么笔弱化能增加 30% 的笔——它移除了那些"看起来像笔但实际上阻挡了更合理划分"的过短笔。 + +### 中枢._校验合法性 — 确保中枢不包含已删除的元素 + +遍历中枢.基础序列,检查每个元素是否仍存在于源序列中。如果某段已被线段修正删除,中枢需要修剪。这保证了中枢始终反映当前有效的线段结构。 + +### 中枢.当前状态 — 判断价格相对中枢的位置 + +```python +if 中枢.高 >= 尾部.分型特征值 >= 中枢.低 → "中枢之中" +elif 中枢.高 < 尾部.分型特征值 → "中枢之上" +elif 中枢.低 > 尾部.分型特征值 → "中枢之下" +``` + +第三买卖点判断依赖此状态——只有离开中枢(之上/之下)后才可能产生第三类买卖点。 + +--- + +## 类型速查 + +| 类型 | 职责 | 关键方法 | +|------|------|----------| +| `K线` | OHLCV 数据 + 指标容器 | `创建普K`, `读取大端字节数组`, `截取` | +| `缠论K线` | 包含处理后的K线 | `分析`, `创建缠K`, `与MACD柱子匹配` | +| `分型` | 顶/底分型 | `从缠K序列中获取分型`, `向序列中添加` | +| `分型结构` | 分型形态枚举 | `分析` | +| `虚线` | 笔/线段数据结构 | `创建笔`, `创建线段`, `买卖意义`, `武之MACD极值` | +| `笔` | 笔算法 (@staticmethod) | `分析` | +| `线段` | 线段算法 (@staticmethod) | `分析`, `扩展分析`, `判断线段内部是否背驰` | +| `中枢` | 三段重叠区间 | `创建`, `分析`, `获取扩展中枢`, `当前状态` | +| `买卖点` | 买卖点工厂 | `一买点`~`T3B卖点` 18 个工厂方法 | +| `背驰分析` | 背驰判断 (@staticmethod) | `MACD背驰`, `斜率背驰`, `测度背驰`, `全量背驰` | +| `观察者` | 单周期分析器 | `投喂原始数据`, `读取数据文件`, `静态重新分析` | +| `立体分析器` | 多周期分析器 | `投喂K线`, `_单体分析器` | +| `K线合成器` | 周期合成 | `投喂`, `投喂K线`, `获取当前K线` | +| `缠论配置` | 全局参数 | `to_dict`, `from_dict`, `model_copy`, `对比`, `不推送` | +| `指标计算器` | 指标挂载 | `计算并挂载`, `_回填新指标` | +| `均线工具` | SMA/EMA 辅助 | `增量SMA`, `增量EMA` | diff --git a/chanlun-py/Cargo.toml b/chanlun-py/Cargo.toml index 13c81e8..c64d3cc 100644 --- a/chanlun-py/Cargo.toml +++ b/chanlun-py/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "chanlun-py" -version = "26.6.47" +version = "26.6.125" edition = "2024" description = "缠论技术分析库 — Rust 高性能 Python 绑定" authors = ["YuYuKunKun"] @@ -12,11 +12,12 @@ crate-type = ["cdylib"] name = "chanlun" [dependencies] -chanlun = "26.6.3" # { path = "../chanlun" } -lru = "0.18" +chanlun = { path = "../chanlun" } +parking_lot = "0.12" +tracing-subscriber = { version = "0.3", features = ["env-filter", "ansi", "std", "registry"] } +tracing-core = "0.1" +dashmap = "6" +tracing = "0.1" pyo3 = { version = "0.28", features = ["experimental-inspect"] } serde_json = "1" chrono = "0.4" -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter", "ansi", "std", "registry"] } -tracing-core = "0.1" diff --git a/chanlun-py/README.md b/chanlun-py/README.md index cc71e28..edc13d3 100644 --- a/chanlun-py/README.md +++ b/chanlun-py/README.md @@ -40,6 +40,10 @@ analyzer = chanlun.立体分析器("BTCUSD", [60, 60*5, 60*5*6], config) ```bash pip install maturin +# 推荐:一键清理缓存 + 构建 + 安装 +./clean_install.sh + +# 或手动: # 开发模式(直接安装到当前 venv) maturin develop @@ -48,6 +52,12 @@ maturin build --release pip install target/wheels/chanlun-*.whl ``` +> **注意**:若修改了 `chan.py`,安装前需清除 `__pycache__`,否则旧 `.pyc` 会被打包进 wheel 导致修改不生效: +> ```bash +> find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null +> find . -type f -name "*.pyc" -delete 2>/dev/null +> ``` + 也可使用项目内的 `build.sh`: ```bash @@ -72,6 +82,77 @@ pip install target/wheels/chanlun-*.whl - 类名 / 方法名 / 字段名与 `chan.py` 保持一致 - 支持 `.nb` 二进制文件格式(大端字节序) +## 性能配置 + +### 缓存模式 + +Python 对象缓存有两种模式,通过环境变量 `CHANLUN_CACHE_MODE` 或函数调用切换: + +```python +from chanlun._chanlun import set_cache_mode, get_cache_mode + +# 默认:thread_local,每线程独立缓存,零锁,多线程场景最佳 +print(get_cache_mode()) # "thread_local" + +# 全局:dashmap 分片哈希表,跨线程 Python `is` 身份一致 +set_cache_mode("global") # 必须在创建任何观察者之前调用 +``` + +```bash +# 环境变量方式 +CHANLUN_CACHE_MODE=global python main.py # 全局缓存 +python main.py # 默认:线程局部缓存 +``` + +| 模式 | 性能 | Python `is` 跨线程 | 适用场景 | +|------|------|---------------------|----------| +| `thread_local`(默认) | 零锁,最快 | 否 | 批量回测、多线程独立分析 | +| `global` | dashmap 分片锁 | 是 | 测试验证、跨线程对象共享 | + +### 日志模式 + +日志输出有三种模式,通过环境变量 `CHANLUN_LOG_MODE` 或函数调用切换: + +```python +from chanlun._chanlun import set_log_mode, set_log_level, get_log_mode + +# 默认:off,不输出,零开销 +print(get_log_mode()) # "off" + +# 简单模式:直接 eprintln/println +set_log_mode("simple") +set_log_level("debug") # 必需:设置日志级别启用输出 + +# Tracing 模式:带时间戳和文件位置格式化输出 +set_log_mode("tracing") +set_log_level("debug") +``` + +```bash +# 环境变量方式 +CHANLUN_LOG_MODE=simple python main.py # 简单输出 +CHANLUN_LOG_MODE=tracing python main.py # 格式化输出 +python main.py # 默认:静默 +``` + +| 模式 | 输出方式 | 性能 | 格式 | +|------|---------|------|------| +| `off`(默认) | 无 | 零开销 | — | +| `simple` | `eprintln!` / `println!` | 极轻 | 纯文本 | +| `tracing` | tracing-subscriber | 稍重 | `2026-06-12 01:57:59.942 WARN file.rs:line` | + +### 观察者直传(避免 Python list 转换) + +背驰分析新增 `_OBS` 后缀方法,直接接受观察者引用,跳过 `list[K线]` ↔ `Vec>` 转换: + +```python +# 旧方式:构建 Python 列表 +result = 背驰分析.MACD背驰(进入段, 离开段, obs.普通K线序列, "总") + +# 新方式:直接传观察者 +result = 背驰分析.MACD背驰_OBS(进入段, 离开段, obs, "总") +``` + ## 许可 本项目主体采用 MIT 许可。包含以下第三方开源代码:czsc(Apache 2.0)、parse(MIT)、termcolor(MIT)。 diff --git a/chanlun-py/chanlun.pyi b/chanlun-py/chanlun.pyi index b7d4adf..a29be3a 100644 --- a/chanlun-py/chanlun.pyi +++ b/chanlun-py/chanlun.pyi @@ -5,11 +5,18 @@ from typing import Any, ClassVar, Optional, List, Dict, Tuple, Union from datetime import datetime # ========== Module-level functions ========== - +def get_rs_log_level() -> str: ... +def set_rs_log_level(level: str) -> None: ... def get_log_level() -> str: ... def set_log_level(level: str) -> None: ... +def get_log_mode() -> str: ... +def set_log_mode(mode: str) -> None: ... +def get_cache_mode() -> str: ... +def set_cache_mode(mode: str) -> None: ... def get_分型模式() -> bool: ... def set_分型模式(value: bool) -> None: ... +def get_扩展线段模式() -> bool: ... +def set_扩展线段模式(value: bool) -> None: ... def 转化为时间戳(ts: Any) -> int: ... def 转化为时间戳_数字(ts: Any) -> int: ... def K线相等(A: K线, B: K线, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ... @@ -75,6 +82,8 @@ class 相对方向: def 翻转(self) -> 相对方向: ... @classmethod def 分析(cls, 前高: float, 前低: float, 后高: float, 后低: float) -> 相对方向: ... + @classmethod + def 从序列中机选(cls, 数量: int, 可选方向: List[相对方向], 可重复: bool = True) -> List[相对方向]: ... def __str__(self) -> str: ... def __repr__(self) -> str: ... def __hash__(self) -> int: ... @@ -326,6 +335,7 @@ class K线: def 截取(序列: List[K线], 始: K线, 终: K线) -> List[K线]: ... def __str__(self) -> str: ... def __repr__(self) -> str: ... + def 根据当前K线生成新K线(self, 方向: 相对方向, 居中: bool = False) -> K线: ... def __bytes__(self) -> bytes: ... def __eq__(self, other: Any) -> bool: ... def __hash__(self) -> int: ... @@ -438,9 +448,9 @@ class 虚线: @property def 模式(self) -> str: ... @property - def 特征序列_显示(self) -> bool: ... - @特征序列_显示.setter - def 特征序列_显示(self, value: bool) -> None: ... + def _特征序列_显示(self) -> bool: ... + @_特征序列_显示.setter + def _特征序列_显示(self, value: bool) -> None: ... @property def 特征序列(self) -> List[Optional[线段特征]]: ... @property @@ -526,6 +536,8 @@ class 虚线: class 线段特征: @property def 序号(self) -> int: ... + @序号.setter + def 序号(self, value: int) -> None: ... @property def 标识(self) -> str: ... @标识.setter @@ -570,6 +582,18 @@ class 背驰分析: def 任选背驰(cls, 进入段: 虚线, 离开段: 虚线, 普K序列: List[K线]) -> bool: ... @classmethod def 背驰模式(cls, 进入段: 虚线, 离开段: 虚线, 普K序列: List[K线], 配置: 缠论配置, 模式: str) -> bool: ... + @classmethod + def MACD背驰_OBS(cls, 进入段: 虚线, 离开段: 虚线, 观察员: 观察者, 方式: str = "总") -> bool: ... + @classmethod + def 全量背驰_OBS(cls, 进入段: 虚线, 离开段: 虚线, 观察员: 观察者) -> bool: ... + @classmethod + def 任意背驰_OBS(cls, 进入段: 虚线, 离开段: 虚线, 观察员: 观察者) -> bool: ... + @classmethod + def 配置背驰_OBS(cls, 进入段: 虚线, 离开段: 虚线, 观察员: 观察者, 配置: 缠论配置) -> bool: ... + @classmethod + def 任选背驰_OBS(cls, 进入段: 虚线, 离开段: 虚线, 观察员: 观察者) -> bool: ... + @classmethod + def 背驰模式_OBS(cls, 进入段: 虚线, 离开段: 虚线, 观察员: 观察者, 配置: 缠论配置, 模式: str) -> bool: ... class 笔: @classmethod @@ -793,6 +817,30 @@ class 观察者: def 扩展线段序列_扩展线段(self) -> List[虚线]: ... @property def 扩展中枢序列_扩展线段(self) -> List[中枢]: ... + @property + def 线段分析层次(self) -> int: ... + @线段分析层次.setter + def 线段分析层次(self, value: int) -> None: ... + @property + def 扩展线段分析层次(self) -> int: ... + @扩展线段分析层次.setter + def 扩展线段分析层次(self, value: int) -> None: ... + @property + def 混合扩展线段分析层次(self) -> int: ... + @混合扩展线段分析层次.setter + def 混合扩展线段分析层次(self, value: int) -> None: ... + @property + def 线段序列组(self) -> List[List[虚线]]: ... + @property + def 中枢序列组(self) -> List[List[中枢]]: ... + @property + def 扩展线段序列组(self) -> List[List[虚线]]: ... + @property + def 扩展中枢序列组(self) -> List[List[中枢]]: ... + @property + def 混合扩展线段序列组(self) -> List[List[虚线]]: ... + @property + def 混合扩展中枢序列组(self) -> List[List[中枢]]: ... def 重置基础序列(self) -> None: ... def 增加原始K线(self, 普K: K线) -> None: ... def 投喂原始数据(self, 时间戳: int, 开: float, 高: float, 低: float, 收: float, 量: float) -> None: ... @@ -838,6 +886,7 @@ class 缠论配置: def from_json(cls, json_str: str) -> 缠论配置: ... @classmethod def 不推送(cls) -> 缠论配置: ... + def 展示标签(self, 标签: str) -> bool: ... @classmethod def 按序号重组字典(cls, 默认配置: Any, 原始字典: Dict[str, Any]) -> Dict[str, Any]: ... def __str__(self) -> str: ... diff --git a/chanlun-py/chanlun/__init__.py b/chanlun-py/chanlun/__init__.py index 7e6ec1f..596fae9 100644 --- a/chanlun-py/chanlun/__init__.py +++ b/chanlun-py/chanlun/__init__.py @@ -32,6 +32,8 @@ __all__ = [ "布林带", "get_分型模式", "set_分型模式", + "get_扩展线段模式", + "set_扩展线段模式", "get_log_level", "set_log_level", "get_rs_log_level", @@ -43,12 +45,60 @@ __all__ = [ "线段特征相等", "中枢相等", "虚线相等", - "chan", ] from ._chanlun import * from ._chanlun import set_log_level as _rs_set_log_level, get_log_level as _rs_get_log_level -from . import chan -from .chan import set_log_level, get_log_level + +import sys as _sys +from loguru import logger as _logger + +# ---- Python 侧日志(loguru)---- + +_级别映射 = { + "trace": "TRACE", + "debug": "DEBUG", + "info": "INFO", + "warn": "WARNING", + "error": "ERROR", + "off": "OFF", +} +_有效级别 = frozenset(_级别映射.keys()) +_当前日志级别 = "info" + + +def set_log_level(level: str): + """设置 Python 侧日志级别 (loguru)。 + + :param level: 日志级别,不区分大小写 (trace / debug / info / warn / error / off) + """ + global _当前日志级别 + _level = level.lower() + if _level not in _有效级别: + raise ValueError(f"无效日志级别 '{level}',有效值: {', '.join(sorted(_有效级别))}") + + _当前日志级别 = _level + _loguru_level = _级别映射[_level] + + try: + _logger.remove(0) + except ValueError: + pass + if _loguru_level != "OFF": + _logger.add(_sys.stderr, level=_loguru_level) + + +def get_log_level() -> str: + """获取 Python 侧当前日志级别。 + + :return: 日志级别字符串 (trace / debug / info / warn / error / off) + """ + return _当前日志级别 + + +set_log_level("error") + + +# ---- Rust 侧日志(tracing)---- def set_rs_log_level(level: str): diff --git a/chanlun-py/chanlun/__init__.pyi b/chanlun-py/chanlun/__init__.pyi index d2725be..e4746bf 100644 --- a/chanlun-py/chanlun/__init__.pyi +++ b/chanlun-py/chanlun/__init__.pyi @@ -9,8 +9,16 @@ def get_rs_log_level() -> str: ... def set_rs_log_level(level: str) -> None: ... def get_log_level() -> str: ... def set_log_level(level: str) -> None: ... +def get_log_mode() -> str: ... +def set_log_mode(mode: str) -> None: ... +def get_cache_mode() -> str: ... +def set_cache_mode(mode: str) -> None: ... def get_分型模式() -> bool: ... def set_分型模式(value: bool) -> None: ... +def get_扩展线段模式() -> bool: ... +def set_扩展线段模式(value: bool) -> None: ... +def 转化为时间戳(ts: Any) -> int: ... +def 转化为时间戳_数字(ts: Any) -> int: ... def K线相等(A: K线, B: K线, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ... def 缠论K线相等(A: 缠论K线, B: 缠论K线, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ... def 分型相等(A: 分型, B: 分型, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ... @@ -74,6 +82,8 @@ class 相对方向: def 翻转(self) -> 相对方向: ... @classmethod def 分析(cls, 前高: float, 前低: float, 后高: float, 后低: float) -> 相对方向: ... + @classmethod + def 从序列中机选(cls, 数量: int, 可选方向: List[相对方向], 可重复: bool = True) -> List[相对方向]: ... def __str__(self) -> str: ... def __repr__(self) -> str: ... def __hash__(self) -> int: ... @@ -325,6 +335,7 @@ class K线: def 截取(序列: List[K线], 始: K线, 终: K线) -> List[K线]: ... def __str__(self) -> str: ... def __repr__(self) -> str: ... + def 根据当前K线生成新K线(self, 方向: 相对方向, 居中: bool = False) -> K线: ... def __bytes__(self) -> bytes: ... def __eq__(self, other: Any) -> bool: ... def __hash__(self) -> int: ... @@ -571,6 +582,18 @@ class 背驰分析: def 任选背驰(cls, 进入段: 虚线, 离开段: 虚线, 普K序列: List[K线]) -> bool: ... @classmethod def 背驰模式(cls, 进入段: 虚线, 离开段: 虚线, 普K序列: List[K线], 配置: 缠论配置, 模式: str) -> bool: ... + @classmethod + def MACD背驰_OBS(cls, 进入段: 虚线, 离开段: 虚线, 观察员: 观察者, 方式: str = "总") -> bool: ... + @classmethod + def 全量背驰_OBS(cls, 进入段: 虚线, 离开段: 虚线, 观察员: 观察者) -> bool: ... + @classmethod + def 任意背驰_OBS(cls, 进入段: 虚线, 离开段: 虚线, 观察员: 观察者) -> bool: ... + @classmethod + def 配置背驰_OBS(cls, 进入段: 虚线, 离开段: 虚线, 观察员: 观察者, 配置: 缠论配置) -> bool: ... + @classmethod + def 任选背驰_OBS(cls, 进入段: 虚线, 离开段: 虚线, 观察员: 观察者) -> bool: ... + @classmethod + def 背驰模式_OBS(cls, 进入段: 虚线, 离开段: 虚线, 观察员: 观察者, 配置: 缠论配置, 模式: str) -> bool: ... class 笔: @classmethod @@ -846,6 +869,146 @@ class 立体分析器: def 投喂K线(self, 普K: K线) -> None: ... def 测试_保存数据(self, root: Optional[str] = None) -> None: ... +# ========== Signal framework (信号匹配框架) ========== + +class Operate: + HL: ClassVar[Operate] + HS: ClassVar[Operate] + HO: ClassVar[Operate] + LO: ClassVar[Operate] + LE: ClassVar[Operate] + SO: ClassVar[Operate] + SE: ClassVar[Operate] + @property + def value(self) -> str: ... + @classmethod + def from_value(cls, value: str) -> Operate: ... + def __str__(self) -> str: ... + def __repr__(self) -> str: ... + def __eq__(self, other: Any) -> bool: ... + def __hash__(self) -> int: ... + +class Signal: + def __init__(self, signal: str = "", score: int = 0, k1: str = "任意", k2: str = "任意", k3: str = "任意", v1: str = "任意", v2: str = "任意", v3: str = "任意") -> None: ... + @property + def signal(self) -> str: ... + @property + def score(self) -> int: ... + @property + def k1(self) -> str: ... + @property + def k2(self) -> str: ... + @property + def k3(self) -> str: ... + @property + def v1(self) -> str: ... + @property + def v2(self) -> str: ... + @property + def v3(self) -> str: ... + @property + def key(self) -> str: ... + @property + def value(self) -> str: ... + def is_match(self, s: Dict[str, Any]) -> bool: ... + def __repr__(self) -> str: ... + +class Factor: + def __init__(self, signals_all: List[Signal], signals_any: List[Signal] = ..., signals_not: List[Signal] = ..., name: str = "") -> None: ... + @property + def name(self) -> str: ... + @property + def signals_all(self) -> List[Signal]: ... + @property + def signals_any(self) -> List[Signal]: ... + @property + def signals_not(self) -> List[Signal]: ... + @property + def unique_signals(self) -> List[str]: ... + def is_match(self, s: Dict[str, Any]) -> bool: ... + def dump(self) -> Dict[str, Any]: ... + @staticmethod + def load(raw: Dict[str, Any]) -> Factor: ... + def __repr__(self) -> str: ... + +class Event: + def __init__(self, operate: Operate, factors: List[Factor], signals_all: List[Signal] = ..., signals_any: List[Signal] = ..., signals_not: List[Signal] = ..., name: str = "") -> None: ... + @property + def operate(self) -> Operate: ... + @property + def factors(self) -> List[Factor]: ... + @property + def signals_all(self) -> List[Signal]: ... + @property + def signals_any(self) -> List[Signal]: ... + @property + def signals_not(self) -> List[Signal]: ... + @property + def name(self) -> str: ... + @property + def unique_signals(self) -> List[str]: ... + def is_match(self, s: Dict[str, Any]) -> Tuple[bool, Optional[str]]: ... + def dump(self) -> Dict[str, Any]: ... + @staticmethod + def load(raw: Dict[str, Any]) -> Event: ... + def __repr__(self) -> str: ... + +class Position: + def __init__(self, symbol: str, opens: List[Event], exits: List[Event] = ..., interval: int = 0, timeout: int = 1000, stop_loss: int = 1000, T0: bool = False, name: str = "") -> None: ... + # 配置 + @property + def symbol(self) -> str: ... + @property + def name(self) -> str: ... + @property + def opens(self) -> List[Event]: ... + @property + def exits(self) -> List[Event]: ... + @property + def events(self) -> List[Event]: ... + @property + def interval(self) -> int: ... + @property + def timeout(self) -> int: ... + @property + def stop_loss(self) -> int: ... + @property + def T0(self) -> bool: ... + @property + def unique_signals(self) -> List[str]: ... + # 状态 (Rust 核心提供) + @property + def pos(self) -> int: ... + @property + def pos_changed(self) -> bool: ... + @property + def operates(self) -> List[Dict[str, Any]]: ... + @property + def holds(self) -> List[Dict[str, Any]]: ... + @property + def pairs(self) -> List[Dict[str, Any]]: ... + # 方法 + def update(self, 信号字典: Dict[str, Any]) -> None: ... + def dump(self, with_data: bool = False) -> Dict[str, Any]: ... + @staticmethod + def load(raw: Dict[str, Any]) -> Position: ... + def __repr__(self) -> str: ... + +class 信号引擎: + def __init__(self, 信号配置: List[Dict[str, Any]] = ...) -> None: ... + def 自动挂载指标(self, analyzer: 立体分析器) -> None: ... + def 更新(self, analyzer: 立体分析器) -> Dict[str, str]: ... + def 更新_完整(self, analyzer: 立体分析器) -> Dict[str, Any]: ... + def __len__(self) -> int: ... + def __repr__(self) -> str: ... + +# ========== 信号注册表函数 ========== + +def call_signal(name: str, obs: 观察者, params: Dict[str, Any]) -> List[Signal]: ... +def list_signals() -> List[str]: ... +def get_signal_template(name: str) -> Optional[str]: ... +def unregister_signal(name: str) -> None: ... + # ========== 缠论配置 (fields via __getattr__/__setattr__) ========== class 缠论配置: @@ -863,6 +1026,7 @@ class 缠论配置: def from_json(cls, json_str: str) -> 缠论配置: ... @classmethod def 不推送(cls) -> 缠论配置: ... + def 展示标签(self, 标签: str) -> bool: ... @classmethod def 按序号重组字典(cls, 默认配置: Any, 原始字典: Dict[str, Any]) -> Dict[str, Any]: ... def __str__(self) -> str: ... diff --git a/chanlun-py/chanlun/chan.py b/chanlun-py/chanlun/chan.py index c76d2cd..2b5b839 100644 --- a/chanlun-py/chanlun/chan.py +++ b/chanlun-py/chanlun/chan.py @@ -20,6 +20,34 @@ 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. + +----------------------------------------------------------------------------- +第三方代码声明 / Third-Party Code Notice +----------------------------------------------------------------------------- + +本文件末尾 信号匹配框架(Signal / Factor / Event / Position / SignalsParser +等类)摘录自 czsc 项目(https://github.com/zengbin93/czsc), +根据 Apache License 2.0 授权使用。 + +原始许可协议全文见 https://www.apache.org/licenses/LICENSE-2.0 + +已做修改:中文命名适配、类型标注增强、与 chan 分析器集成的扩展。 + + +Copyright [2025] [zengbin93] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +----------------------------------------------------------------------------- """ # -*- coding: utf-8 -*- @@ -31,12 +59,17 @@ from __future__ import annotations import json import math import os +import re +from collections import deque, OrderedDict, defaultdict +import random import struct import sys import tempfile +import hashlib +import traceback import datetime as datetime_module from dataclasses import dataclass, field -from datetime import datetime +from datetime import datetime, timedelta from enum import Enum from functools import lru_cache from pathlib import Path @@ -52,9 +85,11 @@ from typing import ( Sequence, Callable, Set, + Generator, ) from loguru import logger +from parse import parse __all__ = [ "K线", @@ -97,6 +132,15 @@ __all__ = [ "虚线相等", "观察者相等", "立体分析器相等", + "import_by_name", + "Signal", + "Factor", + "Event", + "SignalsParser", + "get_signals_config", + "create_single_signal", + "Position", + "信号计算器", ] # 日志级别映射: 名称 → loguru 级别名 @@ -141,6 +185,9 @@ def get_log_level() -> str: return _当前日志级别 +set_log_level("error") + + @lru_cache(128) def K线相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]: """原始K线相等校验:字段完备→浮点容错→普通全等""" @@ -525,6 +572,26 @@ def 立体分析器相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]: return True, f"{标签}:所有周期观察者全量校验全部一致" +REGISTRY = {} + + +def 注册(obj): + """ + 通用装饰器:支持函数和类。 + obj 可以是 function,也可以是 class。 + """ + REGISTRY[obj.__name__] = obj + return obj + + +def 注入依赖(目标模块): + """批量注入到目标模块""" + for name, obj in REGISTRY.items(): + setattr(目标模块, name, obj) + logger.warning(f"成功自动注入: {list(REGISTRY.keys())}") + + +@注册 class 买卖点类型(str, Enum): """买卖点类型 — 缠论的三类买卖点及扩展类型。 @@ -578,6 +645,7 @@ class 买卖点类型(str, Enum): return "卖" in self.value +@注册 class 基础买卖点: """基础买卖点 — 描述偏离买入/卖出位置的程度。 @@ -670,6 +738,7 @@ class 基础买卖点: return self.买卖点分型.与MACD柱子分型匹配 +@注册 @final class 买卖点(基础买卖点): """一二三类买卖点及扩展类型(T1/T1P/T2/T2S/T3A/T3B)的构造器。 @@ -773,6 +842,7 @@ class 买卖点(基础买卖点): return 买卖点函数(买卖点分型, 当前缠K, 特征, 备注, 破位值) +@注册 class datetime(datetime): # 用于对齐C输出 def __str__(self): return f"{int(self.timestamp())}" @@ -784,6 +854,7 @@ class datetime(datetime): # 用于对齐C输出 return int(self.timestamp()) +@注册 def 转化为时间戳(ts: Union[str, datetime, int, float]) -> datetime: """ 将不同类型的时间戳转换为datetime对象(统一比较标准) @@ -806,6 +877,7 @@ def 转化为时间戳(ts: Union[str, datetime, int, float]) -> datetime: raise TypeError(f"不支持的时间戳类型: {type(ts)}") +@注册 def 转化为时间戳_数字(ts: Union[str, datetime, int, float]) -> int: """ 将不同类型的时间戳转换为整数秒级时间戳 @@ -831,6 +903,7 @@ class ValidationError(Exception): pass +@注册 @final class 缠论配置: """控制缠论分析各阶段行为的全局参数集。 @@ -843,7 +916,7 @@ class 缠论配置: **[线段]** 线段_特征序列忽视老阴老阳, 线段_缺口后紧急修正, 线段内部中枢图显 等 \\ **[分析开关]** 分析笔, 分析线段, 分析扩展线段, 分析笔中枢, 分析线段中枢 \\ **[指标]** 计算指标, 指标计算方式, MACD/RSI/KDJ 参数 \\ - **[推送/显示]** 图表展示, 推送K线/笔/线段/中枢 等 \\ + **[推送/显示]** 图表展示 (主开关), 图表展示标签 (标签列表) 等 \\ **[买卖点]** 买卖点偏移, 买卖点激进识别, 买卖点_背离率, 买卖点_计算方式 等 \\ **[背驰]** 线段内部背驰_MACD, 线段内部背驰_斜率 等 \\ **[其他]** 手动终止, 加载文件路径 @@ -882,45 +955,15 @@ class 缠论配置: 分析线段中枢: bool = True, 手动终止: str = "", # 2099-12-31 00:00:00 计算指标: bool = True, - 指标计算方式: str = "收", # (开, 高, 低, 收, 高低均值, 高低收均值, 开高低收均值), 默认 收盘价 - 平滑异同移动平均线_快线周期: int = 13, - 平滑异同移动平均线_慢线周期: int = 31, - 平滑异同移动平均线_信号周期: int = 11, - 相对强弱指数_周期: int = 13, - 相对强弱指数_移动平均线周期: int = 13, - 相对强弱指数_超买阈值: float = 75.0, - 相对强弱指数_超卖阈值: float = 25.0, - 随机指标_RSV周期: int = 13, - 随机指标_K值平滑周期: int = 5, - 随机指标_D值平滑周期: int = 5, - 随机指标_超买阈值: float = 80.0, - 随机指标_超卖阈值: float = 20.0, - 计算BOLL: bool = False, - 布林带_周期: int = 20, - 布林带_标准差倍数: float = 2.0, - 均线_类型列表: List[str] = None, - 均线_周期列表: List[int] = None, + 指标计算方式: str = "收", # 均线计算方式 + 均线参数列表: List[tuple] = None, # [(key, 计算方式, 类型, 周期), ...] # 多参数指标列表(None/空列表 = 使用默认单参数) - MACD_参数列表: List[tuple] = None, # [(key, 快线, 慢线, 信号), ...] - RSI_周期列表: List[tuple] = None, # [(key, 周期), ...] - KDJ_参数列表: List[tuple] = None, # [(key, RSV周期, K平滑, D平滑), ...] - BOLL_参数列表: List[tuple] = None, # [(key, 周期, 标准差倍数), ...] - 图表展示: bool = True, - 推送K线: bool = True, - 推送笔: bool = True, - 推送线段: bool = True, - 推送中枢: bool = True, - 图表展示_笔: bool = True, - 图表展示_线段: bool = True, - 图表展示_扩展线段: bool = True, - 图表展示_扩展线段_线段: bool = True, - 图表展示_线段_线段: bool = True, - 图表展示_中枢_笔: bool = True, - 图表展示_中枢_线段: bool = True, - 图表展示_中枢_扩展线段: bool = True, - 图表展示_中枢_扩展线段_线段: bool = True, - 图表展示_中枢_线段_线段: bool = True, - 图表展示_中枢_线段内部: bool = True, + MACD_参数列表: List[tuple] = None, # [(key, 计算方式, 快线, 慢线, 信号), ...] + RSI_周期列表: List[tuple] = None, # [(key, 计算方式, 周期), ...] + KDJ_参数列表: List[tuple] = None, # [(key, 计算方式, RSV周期, K平滑, D平滑), ...] + BOLL_参数列表: List[tuple] = None, # [(key, 计算方式, 周期, 标准差倍数), ...] + 图表展示: bool = True, # 图表系统主开关 + 图表展示标签: Optional[List[str]] = None, # None=全部展示, []=不展示, [\"笔\",\"线段\"]=指定 买卖点偏移: int = 1, # 最大偏移 买卖点激进识别: bool = False, # 激进模式下将不考虑分型的完整性 买卖点与MACD柱强相关: bool = False, # True: 卖点需正值 买点需负值 @@ -960,43 +1003,13 @@ class 缠论配置: self.手动终止 = 手动终止 self.计算指标 = 计算指标 self.指标计算方式 = 指标计算方式 - self.平滑异同移动平均线_快线周期 = 平滑异同移动平均线_快线周期 - self.平滑异同移动平均线_慢线周期 = 平滑异同移动平均线_慢线周期 - self.平滑异同移动平均线_信号周期 = 平滑异同移动平均线_信号周期 - self.相对强弱指数_周期 = 相对强弱指数_周期 - self.相对强弱指数_移动平均线周期 = 相对强弱指数_移动平均线周期 - self.相对强弱指数_超买阈值 = 相对强弱指数_超买阈值 - self.相对强弱指数_超卖阈值 = 相对强弱指数_超卖阈值 - self.随机指标_RSV周期 = 随机指标_RSV周期 - self.随机指标_K值平滑周期 = 随机指标_K值平滑周期 - self.随机指标_D值平滑周期 = 随机指标_D值平滑周期 - self.随机指标_超买阈值 = 随机指标_超买阈值 - self.随机指标_超卖阈值 = 随机指标_超卖阈值 - self.计算BOLL = 计算BOLL - self.布林带_周期 = 布林带_周期 - self.布林带_标准差倍数 = 布林带_标准差倍数 - self.均线_类型列表 = 均线_类型列表 if 均线_类型列表 is not None else [] - self.均线_周期列表 = 均线_周期列表 if 均线_周期列表 is not None else [] - self.MACD_参数列表 = MACD_参数列表 if MACD_参数列表 is not None else [] - self.RSI_周期列表 = RSI_周期列表 if RSI_周期列表 is not None else [] - self.KDJ_参数列表 = KDJ_参数列表 if KDJ_参数列表 is not None else [] - self.BOLL_参数列表 = BOLL_参数列表 if BOLL_参数列表 is not None else [] + self.均线参数列表 = 均线参数列表 if 均线参数列表 is not None else [] + self.MACD_参数列表 = MACD_参数列表 if MACD_参数列表 is not None else [("macd", "收", 13, 31, 11)] + self.RSI_周期列表 = RSI_周期列表 if RSI_周期列表 is not None else [("rsi", "收", 14, 13, 75.0, 25.0)] + self.KDJ_参数列表 = KDJ_参数列表 if KDJ_参数列表 is not None else [("kdj", "收", 13, 5, 5, 80.0, 20.0)] + self.BOLL_参数列表 = BOLL_参数列表 if BOLL_参数列表 is not None else [("boll", "收", 20, 2.0)] self.图表展示 = 图表展示 - self.推送K线 = 推送K线 - self.推送笔 = 推送笔 - self.推送线段 = 推送线段 - self.推送中枢 = 推送中枢 - self.图表展示_笔 = 图表展示_笔 - self.图表展示_线段 = 图表展示_线段 - self.图表展示_扩展线段 = 图表展示_扩展线段 - self.图表展示_扩展线段_线段 = 图表展示_扩展线段_线段 - self.图表展示_线段_线段 = 图表展示_线段_线段 - self.图表展示_中枢_笔 = 图表展示_中枢_笔 - self.图表展示_中枢_线段 = 图表展示_中枢_线段 - self.图表展示_中枢_扩展线段 = 图表展示_中枢_扩展线段 - self.图表展示_中枢_扩展线段_线段 = 图表展示_中枢_扩展线段_线段 - self.图表展示_中枢_线段_线段 = 图表展示_中枢_线段_线段 - self.图表展示_中枢_线段内部 = 图表展示_中枢_线段内部 + self.图表展示标签 = set(图表展示标签) if 图表展示标签 is not None else None self.买卖点偏移 = 买卖点偏移 self.买卖点激进识别 = 买卖点激进识别 self.买卖点与MACD柱强相关 = 买卖点与MACD柱强相关 @@ -1046,43 +1059,13 @@ class 缠论配置: "手动终止": {"annotation": str, "default": ""}, "计算指标": {"annotation": bool, "default": True}, "指标计算方式": {"annotation": str, "default": "收"}, - "平滑异同移动平均线_快线周期": {"annotation": int, "default": 13}, - "平滑异同移动平均线_慢线周期": {"annotation": int, "default": 31}, - "平滑异同移动平均线_信号周期": {"annotation": int, "default": 11}, - "相对强弱指数_周期": {"annotation": int, "default": 13}, - "相对强弱指数_移动平均线周期": {"annotation": int, "default": 13}, - "相对强弱指数_超买阈值": {"annotation": float, "default": 75.0}, - "相对强弱指数_超卖阈值": {"annotation": float, "default": 25.0}, - "随机指标_RSV周期": {"annotation": int, "default": 13}, - "随机指标_K值平滑周期": {"annotation": int, "default": 5}, - "随机指标_D值平滑周期": {"annotation": int, "default": 5}, - "随机指标_超买阈值": {"annotation": float, "default": 80.0}, - "随机指标_超卖阈值": {"annotation": float, "default": 20.0}, - "计算BOLL": {"annotation": bool, "default": False}, - "布林带_周期": {"annotation": int, "default": 20}, - "布林带_标准差倍数": {"annotation": float, "default": 2.0}, - "均线_类型列表": {"annotation": List[str], "default": []}, - "均线_周期列表": {"annotation": List[int], "default": []}, - "MACD_参数列表": {"annotation": List[tuple], "default": []}, - "RSI_周期列表": {"annotation": List[tuple], "default": []}, - "KDJ_参数列表": {"annotation": List[tuple], "default": []}, - "BOLL_参数列表": {"annotation": List[tuple], "default": []}, + "均线参数列表": {"annotation": List[tuple], "default": []}, + "MACD_参数列表": {"annotation": List[tuple], "default": [("macd", "收", 13, 31, 11)]}, + "RSI_周期列表": {"annotation": List[tuple], "default": [("rsi", "收", 14, 13, 75.0, 25.0)]}, + "KDJ_参数列表": {"annotation": List[tuple], "default": [("kdj", "收", 13, 5, 5, 80.0, 20.0)]}, + "BOLL_参数列表": {"annotation": List[tuple], "default": [("boll", "收", 20, 2.0)]}, "图表展示": {"annotation": bool, "default": True}, - "推送K线": {"annotation": bool, "default": True}, - "推送笔": {"annotation": bool, "default": True}, - "推送线段": {"annotation": bool, "default": True}, - "推送中枢": {"annotation": bool, "default": True}, - "图表展示_笔": {"annotation": bool, "default": True}, - "图表展示_线段": {"annotation": bool, "default": True}, - "图表展示_扩展线段": {"annotation": bool, "default": True}, - "图表展示_扩展线段_线段": {"annotation": bool, "default": True}, - "图表展示_线段_线段": {"annotation": bool, "default": True}, - "图表展示_中枢_笔": {"annotation": bool, "default": True}, - "图表展示_中枢_线段": {"annotation": bool, "default": True}, - "图表展示_中枢_扩展线段": {"annotation": bool, "default": True}, - "图表展示_中枢_扩展线段_线段": {"annotation": bool, "default": True}, - "图表展示_中枢_线段_线段": {"annotation": bool, "default": True}, - "图表展示_中枢_线段内部": {"annotation": bool, "default": True}, + "图表展示标签": {"annotation": Optional[List[str]], "default": None}, "买卖点偏移": {"annotation": int, "default": 1}, "买卖点激进识别": {"annotation": bool, "default": False}, "买卖点与MACD柱强相关": {"annotation": bool, "default": False}, @@ -1111,10 +1094,13 @@ class 缠论配置: default = field_info["default"] try: - # 布尔类型验证 + # 布尔类型验证(与 Rust 绑定层 coerce_strings_to_numbers 对齐) if type_ is bool: if not isinstance(value, bool): - setattr(self, fname, bool(value)) + if isinstance(value, str) and value.lower() in ("true", "false"): + setattr(self, fname, value.lower() == "true") + else: + setattr(self, fname, default) # 整数类型验证 elif type_ is int: @@ -1131,27 +1117,45 @@ class 缠论配置: logger.warning(f"[{fname}] = {value} 解析失败,使用默认值:{default}") setattr(self, fname, default) - # ---- 参数列表解析:多参数列表为空时回退到默认单参数 ---- + def 设置指标( + self, + *, + 均线: List[tuple] = None, + MACD: List[tuple] = None, + RSI: List[tuple] = None, + KDJ: List[tuple] = None, + BOLL: List[tuple] = None, + ): + """统一设置所有指标参数。 - def _解析MACD参数列表(self) -> List[tuple]: - if self.MACD_参数列表: - return self.MACD_参数列表 - return [("macd", self.平滑异同移动平均线_快线周期, self.平滑异同移动平均线_慢线周期, self.平滑异同移动平均线_信号周期)] + 元组格式 ``(key, 计算方式, *params)``: + - 均线: ``("SMA_5", "收", "SMA", 5)`` — key/计算方式/类型/周期 + - MACD: ``("默认", "收", 13, 31, 11)`` — 快线/慢线/信号 + - RSI: ``("默认", "收", 14, 13, 75, 25)`` — 周期/MA周期/超买/超卖 + - KDJ: ``("默认", "收", 13, 5, 5, 80, 20)`` — RSV/K平滑/D平滑/超买/超卖 + - BOLL: ``("默认", "收", 20, 2.0)`` — 周期/标准差倍数 - def _解析RSI周期列表(self) -> List[tuple]: - if self.RSI_周期列表: - return self.RSI_周期列表 - return [("rsi", self.相对强弱指数_周期)] + :param 均线: key 即均线名(如 ``"SMA_5"``),同时编码类型和周期 + :param MACD: 首个 key 同时写入 ``"macd"`` 兼容槽位 + :param BOLL: BOLL 参数元组列表,为空则不计算 + """ + self.计算指标 = True + if 均线 is not None: + self.均线参数列表 = 均线 + if MACD is not None: + self.MACD_参数列表 = MACD + if RSI is not None: + self.RSI_周期列表 = RSI + if KDJ is not None: + self.KDJ_参数列表 = KDJ + if BOLL is not None: + self.BOLL_参数列表 = BOLL - def _解析KDJ参数列表(self) -> List[tuple]: - if self.KDJ_参数列表: - return self.KDJ_参数列表 - return [("kdj", self.随机指标_RSV周期, self.随机指标_K值平滑周期, self.随机指标_D值平滑周期)] - - def _解析BOLL参数列表(self) -> List[tuple]: - if self.BOLL_参数列表: - return self.BOLL_参数列表 - return [("boll", self.布林带_周期, self.布林带_标准差倍数)] + def 展示标签(self, 标签: str) -> bool: + """判断指定标签是否应展示。None = 全部展示,空列表 = 全部隐藏。""" + if self.图表展示标签 is None: + return True + return 标签 in self.图表展示标签 @classmethod def 兼容旧版本配置(cls, values: Dict[str, Any]) -> Dict[str, Any]: @@ -1161,7 +1165,22 @@ class 缠论配置: :return: 仅保留当前类已知字段的字典 """ valid_fields = cls.model_fields().keys() - cleaned = {k: v for k, v in values.items() if k in valid_fields} + cleaned = {} + for k, v in values.items(): + if k not in valid_fields: + continue + # 字符串值类型强制转换(与 Rust 绑定层 coerce_strings_to_numbers 对齐) + if isinstance(v, str): + if v.lower() in ("true", "false"): + v = v.lower() == "true" + elif v.lstrip("-").isdigit(): + v = int(v) + else: + try: + v = float(v) + except ValueError: + pass + cleaned[k] = v return cleaned def to_dict(self) -> dict: @@ -1169,7 +1188,13 @@ class 缠论配置: :return: 包含所有配置字段的字典 """ - return {k: getattr(self, k) for k in self.model_fields().keys()} + result = {} + for k in self.model_fields().keys(): + v = getattr(self, k) + if isinstance(v, set): + v = list(v) + result[k] = v + return result def to_json(self) -> str: """将配置序列化为 JSON 字符串。 @@ -1225,21 +1250,7 @@ class 缠论配置: return cls( 线段内部中枢图显=False, 图表展示=False, - 推送K线=False, - 推送笔=False, - 推送线段=False, - 推送中枢=False, - 图表展示_笔=False, - 图表展示_线段=False, - 图表展示_扩展线段=False, - 图表展示_扩展线段_线段=False, - 图表展示_线段_线段=False, - 图表展示_中枢_笔=False, - 图表展示_中枢_线段=False, - 图表展示_中枢_扩展线段=False, - 图表展示_中枢_扩展线段_线段=False, - 图表展示_中枢_线段_线段=False, - 图表展示_中枢_线段内部=False, + 图表展示标签=[], ) def model_copy(self, update: dict = None, deep: bool = True): @@ -1302,6 +1313,7 @@ class 缠论配置: return diff_dict +@注册 class 相对方向(Enum): """相对方向 — 描述两个K线/分型之间相对位置关系的枚举。 @@ -1425,7 +1437,25 @@ class 相对方向(Enum): return 相对方向.逆 raise RuntimeError("无法识别的方向") + @classmethod + def 从序列中机选( + cls, + 数量: int, + 可选方向: List["相对方向"], + 可重复: bool = True, # 是否允许重复选择 + ) -> Generator["相对方向", None, None]: + if not 可重复 and 数量 > len(可选方向): + raise ValueError("数量超过可选方向数") + if 可重复: + while 数量 > 0: + yield random.choice(可选方向) + 数量 -= 1 + else: + yield from random.sample(可选方向, 数量) + + +@注册 class 分型结构(Enum): """描述三根K线构成的顶底分型形态。 @@ -1499,6 +1529,7 @@ class 分型结构(Enum): return None +@注册 @final class 缺口: """缺口 — 描述价格区间之间的缺口(未重叠部分)。 @@ -1641,6 +1672,9 @@ class 平滑异同移动平均线: self.慢线EMA = 慢线EMA self.DEA_EMA = DEA_EMA + def __repr__(self): + return f"平滑异同移动平均线(时间戳={self.时间戳}, 收盘价={self.收盘价}, 快线周期={self.快线周期}, 慢线周期={self.慢线周期}, 信号周期={self.信号周期}, DIF={self.DIF}, DEA={self.DEA}, MACD柱={self.MACD柱}, 快线EMA={self.快线EMA}, 慢线EMA={self.慢线EMA}, DEA_EMA={self.DEA_EMA})" + @classmethod def 首次计算(cls, 初始收盘价: float, 初始时间: datetime, 快线周期: int = 12, 慢线周期: int = 26, 信号周期: int = 9) -> 平滑异同移动平均线: """ @@ -1672,12 +1706,12 @@ class 平滑异同移动平均线: 快线周期=快线周期, 慢线周期=慢线周期, 信号周期=信号周期, - DIF=DIF, - DEA=DEA_EMA, - MACD柱=MACD柱, - 快线EMA=快线EMA, - 慢线EMA=慢线EMA, - DEA_EMA=DEA_EMA, + DIF=round(DIF, 2), + DEA=round(DEA_EMA, 2), + MACD柱=round(MACD柱, 2), + 快线EMA=round(快线EMA, 2), + 慢线EMA=round(慢线EMA, 2), + DEA_EMA=round(DEA_EMA, 2), ) @classmethod @@ -1743,12 +1777,12 @@ class 平滑异同移动平均线: 快线周期=前一个MACD.快线周期, 慢线周期=前一个MACD.慢线周期, 信号周期=前一个MACD.信号周期, - DIF=DIF, - DEA=DEA_EMA, - MACD柱=MACD柱, - 快线EMA=快线EMA, - 慢线EMA=慢线EMA, - DEA_EMA=DEA_EMA, + DIF=round(DIF, 2), + DEA=round(DEA_EMA, 2), + MACD柱=round(MACD柱, 2), + 快线EMA=round(快线EMA, 2), + 慢线EMA=round(慢线EMA, 2), + DEA_EMA=round(DEA_EMA, 2), ) @classmethod @@ -1797,10 +1831,11 @@ class 相对强弱指数: 下跌幅度: float = 0.0, 平滑系数: float = 0.0, RSI_SMA: Optional[float] = None, - RSI历史队列: List[float] = None, + RSI历史队列: Optional[deque[float]] = None, + RSI和: float = 0.0, ): if RSI历史队列 is None: - RSI历史队列 = [] + RSI历史队列 = deque() # 原始数据 self.时间戳 = 时间戳 @@ -1829,6 +1864,7 @@ class 相对强弱指数: # RSI的SMA(信号线)相关字段 self.RSI_SMA = RSI_SMA self.RSI历史队列 = RSI历史队列 + self.RSI和 = RSI和 @classmethod def 首次计算(cls, 初始收盘价: float, 初始时间: datetime, 周期: int = 14, 超买阈值: float = 70.0, 超卖阈值: float = 30.0, RSI_SMA周期: Optional[int] = None) -> 相对强弱指数: @@ -1916,19 +1952,18 @@ class 相对强弱指数: # ----- 计算RSI的SMA(简单移动平均) ----- RSI_SMA = None - 历史队列 = 前一个RSI.RSI历史队列.copy() if 前一个RSI.RSI历史队列 else [] + 历史队列 = 前一个RSI.RSI历史队列.copy() if 前一个RSI.RSI历史队列 else deque() + RSI和 = 前一个RSI.RSI和 if RSI_SMA周期 is not None and RSI_SMA周期 > 0 and RSI is not None: - # 将当前RSI加入队列 历史队列.append(RSI) - # 保持队列长度不超过周期 + RSI和 += RSI if len(历史队列) > RSI_SMA周期: - 历史队列.pop(0) - # 计算SMA(即使队列未满也计算当前平均值) + RSI和 -= 历史队列.popleft() if 历史队列: - RSI_SMA = sum(历史队列) / len(历史队列) + RSI_SMA = RSI和 / len(历史队列) else: - # 未启用SMA,清空队列 - 历史队列 = [] + 历史队列 = deque() + RSI和 = 0.0 return cls( 时间戳=当前时间, @@ -1945,6 +1980,7 @@ class 相对强弱指数: RSI_SMA周期=RSI_SMA周期, RSI_SMA=RSI_SMA, RSI历史队列=历史队列, + RSI和=RSI和, ) @classmethod @@ -1996,16 +2032,16 @@ class 随机指标: K: Optional[float] = None, D: Optional[float] = None, J: Optional[float] = None, - 历史最高价队列: list[float] = None, - 历史最低价队列: list[float] = None, + 历史最高价队列: Optional[deque[float]] = None, + 历史最低价队列: Optional[deque[float]] = None, 前一个RSV: Optional[float] = None, 前一个K: Optional[float] = None, 前一个D: Optional[float] = None, ): if 历史最高价队列 is None: - 历史最高价队列 = [] + 历史最高价队列 = deque() if 历史最低价队列 is None: - 历史最低价队列 = [] + 历史最低价队列 = deque() # 原始数据 self.时间戳 = 时间戳 @@ -2065,8 +2101,8 @@ class 随机指标: K=None, D=None, J=None, - 历史最高价队列=[初始最高价], - 历史最低价队列=[初始最低价], + 历史最高价队列=deque([初始最高价]), + 历史最低价队列=deque([初始最低价]), 前一个RSV=None, 前一个K=None, 前一个D=None, @@ -2113,13 +2149,13 @@ class 随机指标: 历史最高价 = 前一个KDJ.历史最高价队列.copy() 历史最高价.append(当前最高价) if len(历史最高价) > N: - 历史最高价.pop(0) + 历史最高价.popleft() # 更新历史最低价队列 历史最低价 = 前一个KDJ.历史最低价队列.copy() 历史最低价.append(当前最低价) if len(历史最低价) > N: - 历史最低价.pop(0) + 历史最低价.popleft() # 计算RSV(需要队列长度达到N才能计算) RSV = None @@ -2218,7 +2254,7 @@ class 布林带: self.上轨 = 上轨 self.中轨 = 中轨 self.下轨 = 下轨 - self._历史队列 = 历史队列 if 历史队列 is not None else [] + self._历史队列 = 历史队列 if 历史队列 is not None else deque() self._均值 = _均值 self._方差和 = _方差和 @@ -2233,7 +2269,7 @@ class 布林带: :return: 初始的布林带实例 """ 价格 = 指标.K线取值(k线, 计算方式) - return cls(时间戳=k线.时间戳, 周期=周期, 标准差倍数=标准差倍数, 上轨=价格, 中轨=价格, 下轨=价格, 历史队列=[价格]) + return cls(时间戳=k线.时间戳, 周期=周期, 标准差倍数=标准差倍数, 上轨=价格, 中轨=价格, 下轨=价格, 历史队列=deque([价格])) @classmethod def 增量计算(cls, prev: 布林带, 当前K线: K线, 计算方式: str) -> 布林带: @@ -2251,7 +2287,7 @@ class 布林带: q = prev._历史队列.copy() q.append(当前价) if len(q) > 周期: - q.pop(0) + q.popleft() # 增量均值和方差 if len(q) < 周期: @@ -2436,19 +2472,21 @@ class 指标计算器: @staticmethod def _计算MACD组(当前K线: K线, prev: Optional[指标容器], 配置: 缠论配置): idx = 当前K线.指标 - 计算方式 = 配置.指标计算方式 - for key, 快, 慢, 信号 in 配置._解析MACD参数列表(): + for i, (key, 计算方式, *params) in enumerate(配置.MACD_参数列表): + 快, 慢, 信号 = params[0], params[1], params[2] prev_val = prev[key] if prev is not None and key in prev else None if prev_val is not None: idx[key] = 平滑异同移动平均线.增量计算_K线(prev_val, 当前K线, 计算方式) else: idx[key] = 平滑异同移动平均线.首次计算_K线(当前K线, 计算方式, 快, 慢, 信号) + if i == 0: + idx["macd"] = idx[key] @staticmethod def _计算RSI组(当前K线: K线, prev: Optional[指标容器], 配置: 缠论配置): idx = 当前K线.指标 - 计算方式 = 配置.指标计算方式 - for key, 周期 in 配置._解析RSI周期列表(): + for key, 计算方式, *params in 配置.RSI_周期列表: + 周期, MA周期, 超买, 超卖 = params[0], params[1], params[2], params[3] prev_val = prev[key] if prev is not None and key in prev else None if prev_val is not None: idx[key] = 相对强弱指数.增量计算_K线(prev_val, 当前K线, 计算方式) @@ -2457,16 +2495,16 @@ class 指标计算器: 当前K线, 计算方式, 周期, - 配置.相对强弱指数_超买阈值, - 配置.相对强弱指数_超卖阈值, - 配置.相对强弱指数_移动平均线周期, + 超买, + 超卖, + MA周期, ) @staticmethod def _计算KDJ组(当前K线: K线, prev: Optional[指标容器], 配置: 缠论配置): idx = 当前K线.指标 - 计算方式 = 配置.指标计算方式 - for key, rsv, k平滑, d平滑 in 配置._解析KDJ参数列表(): + for key, 计算方式, *params in 配置.KDJ_参数列表: + rsv, k平滑, d平滑, 超买, 超卖 = params[0], params[1], params[2], params[3], params[4] prev_val = prev[key] if prev is not None and key in prev else None if prev_val is not None: idx[key] = 随机指标.增量计算_K线(prev_val, 当前K线, 计算方式) @@ -2477,15 +2515,15 @@ class 指标计算器: rsv, k平滑, d平滑, - 配置.随机指标_超买阈值, - 配置.随机指标_超卖阈值, + 超买, + 超卖, ) @staticmethod def _计算BOLL组(当前K线: K线, prev: Optional[指标容器], 配置: 缠论配置): idx = 当前K线.指标 - 计算方式 = 配置.指标计算方式 - for key, 周期, 标准差倍数 in 配置._解析BOLL参数列表(): + for key, 计算方式, *params in 配置.BOLL_参数列表: + 周期, 标准差倍数 = params[0], params[1] prev_val = prev[key] if prev is not None and key in prev else None if prev_val is not None: idx[key] = 布林带.增量计算(prev_val, 当前K线, 计算方式) @@ -2494,18 +2532,16 @@ class 指标计算器: @staticmethod def _更新均线(当前K线: K线, 普K序列: List[K线], 配置: 缠论配置): - if not 配置.均线_类型列表 or not 配置.均线_周期列表: + if not 配置.均线参数列表: return - for ma_type in 配置.均线_类型列表: - for period in 配置.均线_周期列表: - key = f"{ma_type}_{period}" - if ma_type == "SMA": - 当前K线.指标.均线[key] = 均线工具.增量SMA(普K序列, period, 配置.指标计算方式) - elif ma_type == "EMA": - 前值 = None - if len(普K序列) >= 2: - 前值 = 普K序列[-2].指标.均线.get(key) - 当前K线.指标.均线[key] = 均线工具.增量EMA(普K序列, period, 配置.指标计算方式, 前值) + for key, 计算方式, ma_type, period in 配置.均线参数列表: + if ma_type == "SMA": + 当前K线.指标.均线[key] = 均线工具.增量SMA(普K序列, period, 计算方式) + elif ma_type == "EMA": + 前值 = None + if len(普K序列) >= 2: + 前值 = 普K序列[-2].指标.均线.get(key) + 当前K线.指标.均线[key] = 均线工具.增量EMA(普K序列, period, 计算方式, 前值) @staticmethod def _回填新指标(全序列: List[K线], 配置: 缠论配置): @@ -2527,16 +2563,14 @@ class 指标计算器: 新参数.append(params) return 新参数 - 新MACD = _新键(尾K指标, 首K指标, 配置._解析MACD参数列表()) - 新RSI = _新键(尾K指标, 首K指标, 配置._解析RSI周期列表()) - 新KDJ = _新键(尾K指标, 首K指标, 配置._解析KDJ参数列表()) - 新BOLL = _新键(尾K指标, 首K指标, 配置._解析BOLL参数列表()) + 新MACD = _新键(尾K指标, 首K指标, 配置.MACD_参数列表) + 新RSI = _新键(尾K指标, 首K指标, 配置.RSI_周期列表) + 新KDJ = _新键(尾K指标, 首K指标, 配置.KDJ_参数列表) + 新BOLL = _新键(尾K指标, 首K指标, 配置.BOLL_参数列表) if not (新MACD or 新RSI or 新KDJ or 新BOLL): return - 计算方式 = 配置.指标计算方式 - for i, k线 in enumerate(全序列): if k线.指标 is None: k线.指标 = 指标容器() @@ -2544,43 +2578,32 @@ class 指标计算器: idx = k线.指标 prev = 全序列[i - 1].指标 if i > 0 else None - for key, 快, 慢, 信号 in 新MACD: + for key, 计算方式, *params in 新MACD: + 快, 慢, 信号 = params[0], params[1], params[2] prev_val = prev[key] if prev is not None and key in prev else None if prev_val is not None: idx[key] = 平滑异同移动平均线.增量计算_K线(prev_val, k线, 计算方式) else: idx[key] = 平滑异同移动平均线.首次计算_K线(k线, 计算方式, 快, 慢, 信号) - for key, 周期 in 新RSI: + for key, 计算方式, *params in 新RSI: + 周期, MA周期, 超买, 超卖 = params[0], params[1], params[2], params[3] prev_val = prev[key] if prev is not None and key in prev else None if prev_val is not None: idx[key] = 相对强弱指数.增量计算_K线(prev_val, k线, 计算方式) else: - idx[key] = 相对强弱指数.首次计算_K线( - k线, - 计算方式, - 周期, - 配置.相对强弱指数_超买阈值, - 配置.相对强弱指数_超卖阈值, - 配置.相对强弱指数_移动平均线周期, - ) + idx[key] = 相对强弱指数.首次计算_K线(k线, 计算方式, 周期, 超买, 超卖, MA周期) - for key, rsv, k平滑, d平滑 in 新KDJ: + for key, 计算方式, *params in 新KDJ: + rsv, k平滑, d平滑, 超买, 超卖 = params[0], params[1], params[2], params[3], params[4] prev_val = prev[key] if prev is not None and key in prev else None if prev_val is not None: idx[key] = 随机指标.增量计算_K线(prev_val, k线, 计算方式) else: - idx[key] = 随机指标.首次计算_K线( - k线, - 计算方式, - rsv, - k平滑, - d平滑, - 配置.随机指标_超买阈值, - 配置.随机指标_超卖阈值, - ) + idx[key] = 随机指标.首次计算_K线(k线, 计算方式, rsv, k平滑, d平滑, 超买, 超卖) - for key, 周期, 标准差倍数 in 新BOLL: + for key, 计算方式, *params in 新BOLL: + 周期, 标准差倍数 = params[0], params[1] prev_val = prev[key] if prev is not None and key in prev else None if prev_val is not None: idx[key] = 布林带.增量计算(prev_val, k线, 计算方式) @@ -2588,6 +2611,7 @@ class 指标计算器: idx[key] = 布林带.首次计算(k线, 计算方式, 周期, 标准差倍数) +@注册 class 背驰分析: """静态方法容器,提供背驰/背离检测算法。 @@ -2823,6 +2847,14 @@ class K线: self.周期: int = 周期 self.指标: 指标容器 = 指标容器() + @property + def 最高价(self): + return self.高 + + @property + def 最低价(self): + return self.低 + # ---- property 兼容层:k线.macd → k线.指标.macd ---- @property def macd(self) -> Optional[平滑异同移动平均线]: @@ -2969,6 +3001,58 @@ class K线: """ return 序列[序列.index(始) : 序列.index(终) + 1] + def 根据当前K线生成新K线(self, 方向: 相对方向, 居中: bool = False) -> "K线": + 时间偏移 = timedelta(seconds=self.周期) + 时间戳: datetime = self.时间戳 + 时间偏移 + 成交量: float = 998 + 高: float = 0 + 低: float = 0 + 高低差 = self.高 - self.低 + match 方向: + case 相对方向.向上: + 偏移 = 高低差 * 0.5 if 居中 else random.randint(int(高低差 * 0.1279), int(高低差 * 0.883)) + 低 = self.低 + 偏移 + 高 = self.高 + 偏移 + case 相对方向.向下: + 偏移 = 高低差 * 0.5 if 居中 else random.randint(int(高低差 * 0.1279), int(高低差 * 0.883)) + 低 = self.低 - 偏移 + 高 = self.高 - 偏移 + case 相对方向.向上缺口: + 偏移 = 高低差 * 1.5 if 居中 else random.randint(int(高低差 * 1.1279), int(高低差 * 1.883)) + 低 = self.低 + 偏移 + 高 = self.高 + 偏移 + case 相对方向.向下缺口: + 偏移 = 高低差 * 1.5 if 居中 else random.randint(int(高低差 * 1.1279), int(高低差 * 1.883)) + 低 = self.低 - 偏移 + 高 = self.高 - 偏移 + case 相对方向.衔接向上: + 偏移 = self.高 - self.低 + 高 = self.高 + 偏移 + 低 = self.高 + case 相对方向.衔接向下: + 偏移 = self.高 - self.低 + 高 = self.低 + 低 = self.低 - 偏移 + + try: + 小数点 = [len(str(n).split(".")[-1]) for n in (self.开盘价, self.高, self.低, self.收盘价)] + except: + 小数点 = [2, 1] + 新K线 = K线.创建普K( + 标识=self.标识, + 时间戳=时间戳, + 开盘价=round(random.uniform(高, 低), max(小数点)), + 最高价=round(高, max(小数点)), + 最低价=round(低, max(小数点)), + 收盘价=round(random.uniform(高, 低), max(小数点)), + 成交量=成交量 * random.random(), + 序号=self.序号 + 1, + 周期=self.周期, + ) + + # assert 相对方向.分析(self, 新K线) is 方向, (方向, 相对方向.分析(self, 新K线)) + return 新K线 + class 缠论K线: """经包含处理后的标准化K线,有方向和分型结构标记。 @@ -3303,6 +3387,7 @@ class 缠论K线: 分型模式 = True +@注册 class 分型: """由左中右三根缠论K线构成的顶/底分型结构。 @@ -3488,6 +3573,10 @@ class 分型: 分型序列.append(当前分型) +扩展线段模式 = True # TODO 虚线高低取值 暂定,此举将符合同级别分解时正确的高低取值涉及中枢等问题 + + +@注册 class 虚线: """笔/线段的通用数据结构,持有一组分型端点(文=起点分型, 武=终点分型)。 @@ -3615,15 +3704,29 @@ class 虚线: case _: raise RuntimeError("无法识别的方向", self.文.结构, self.武.结构) + @property + def 端点高(self) -> float: + if self.方向 is 相对方向.向上: + return self.武.中.高 + return self.文.中.高 + + @property + def 端点低(self) -> float: + if self.方向 is 相对方向.向下: + return self.武.中.低 + return self.文.中.低 + @property def 高(self) -> float: """虚线区间的最高价。 :return: 向上虚线取武.中.高,向下虚线取文.中.高 """ - if self.方向 is 相对方向.向上: - return self.武.中.高 - return self.文.中.高 + if 扩展线段模式 and self.模式 != "文武" and self.标识 != "笔" and "扩展" in self.标识: # 扩展线段 + 端点序列 = [筆.文 for 筆 in self.基础序列] + 端点序列.append(self.基础序列[-1].武) + return max(端点序列, key=lambda o: o.中.高).中.高 + return self.端点高 @property def 低(self) -> float: @@ -3631,9 +3734,11 @@ class 虚线: :return: 向下虚线取武.中.低,向上虚线取文.中.低 """ - if self.方向 is 相对方向.向下: - return self.武.中.低 - return self.文.中.低 + if 扩展线段模式 and self.模式 != "文武" and self.标识 != "笔" and "扩展" in self.标识: # 扩展线段 + 端点序列 = [筆.文 for 筆 in self.基础序列] + 端点序列.append(self.基础序列[-1].武) + return min(端点序列, key=lambda o: o.中.低).中.低 + return self.端点低 def 之前是(self, 之前: 虚线) -> bool: """ @@ -3704,7 +3809,7 @@ class 虚线: 段.实_中枢序列 = [] 段.虚_中枢序列 = [] 段.合_中枢序列 = [] - 段.基础序列 = 虚线序列 + 段.基础序列 = 虚线序列[:] return 段 @classmethod @@ -3873,6 +3978,111 @@ class 虚线: return True return False + @classmethod + def _计算K线序列MACD趋向背驰(cls, 普K序列: Sequence[K线], 方向: 相对方向): + """计算K线序列的MACD柱/DIF/DEA趋向背驰(三元素判断) + + :param 普K序列: K线序列 + :param 方向: 运行方向 + :return: [柱子背驰, DIF背驰, DEA背驰] + """ + if 方向 is 相对方向.向上: + 柱子序列 = [] + 离差值序列 = [] + 信号线序列 = [] + for k线 in 普K序列: + m = k线.macd + if m.MACD柱 > 0: + 柱子序列.append(k线) + if m.DIF > 0: + 离差值序列.append(k线) + if m.DEA > 0: + 信号线序列.append(k线) + + if not 柱子序列: + return [False, False, False] + 最高柱子 = max(柱子序列, key=lambda k线: k线.macd.MACD柱) + 最高离差值 = max(离差值序列, key=lambda k线: k线.macd.DIF) if 离差值序列 else None + 最高信号线 = max(信号线序列, key=lambda k线: k线.macd.DEA) if 信号线序列 else None + + 结果 = [] + 柱子 = [最高柱子, 普K序列[-1]] + 柱子.sort(key=lambda k线: k线.时间戳) + if 柱子[0].macd.MACD柱 > 柱子[1].macd.MACD柱 and 柱子[0].高 < 柱子[1].高: + 结果.append(True) + else: + 结果.append(False) + + if 最高离差值 is not None: + 柱子 = [最高离差值, 普K序列[-1]] + 柱子.sort(key=lambda k线: k线.时间戳) + if 柱子[0].macd.DIF > 柱子[1].macd.DIF and 柱子[0].高 < 柱子[1].高: + 结果.append(True) + else: + 结果.append(False) + else: + 结果.append(False) + + if 最高信号线 is not None: + 柱子 = [最高信号线, 普K序列[-1]] + 柱子.sort(key=lambda k线: k线.时间戳) + if 柱子[0].macd.DEA > 柱子[1].macd.DEA and 柱子[0].高 < 柱子[1].高: + 结果.append(True) + else: + 结果.append(False) + else: + 结果.append(False) + + return 结果 + else: + 柱子序列 = [] + 离差值序列 = [] + 信号线序列 = [] + for k线 in 普K序列: + m = k线.macd + if m.MACD柱 < 0: + 柱子序列.append(k线) + if m.DIF < 0: + 离差值序列.append(k线) + if m.DEA < 0: + 信号线序列.append(k线) + + if not 柱子序列: + return [False, False, False] + 最高柱子 = max(柱子序列, key=lambda k线: abs(k线.macd.MACD柱)) + 最高离差值 = max(离差值序列, key=lambda k线: abs(k线.macd.DIF)) if 离差值序列 else None + 最高信号线 = max(信号线序列, key=lambda k线: abs(k线.macd.DEA)) if 信号线序列 else None + + 结果 = [] + 柱子 = [最高柱子, 普K序列[-1]] + 柱子.sort(key=lambda k线: k线.时间戳) + if 柱子[0].macd.MACD柱 < 柱子[1].macd.MACD柱 and 柱子[0].低 > 柱子[1].低: + 结果.append(True) + else: + 结果.append(False) + + if 最高离差值 is not None: + 柱子 = [最高离差值, 普K序列[-1]] + 柱子.sort(key=lambda k线: k线.时间戳) + if 柱子[0].macd.DIF < 柱子[1].macd.DIF and 柱子[0].低 > 柱子[1].低: + 结果.append(True) + else: + 结果.append(False) + else: + 结果.append(False) + + if 最高信号线 is not None: + 柱子 = [最高信号线, 普K序列[-1]] + 柱子.sort(key=lambda k线: k线.时间戳) + if 柱子[0].macd.DEA < 柱子[1].macd.DEA and 柱子[0].低 > 柱子[1].低: + 结果.append(True) + else: + 结果.append(False) + else: + 结果.append(False) + + return 结果 + @classmethod def 计算K线序列MACD趋向背驰(cls, 普K序列: Sequence[K线], 方向: 相对方向): """计算K线序列的MACD柱/DIF/DEA趋向背驰(三元素判断) @@ -4073,23 +4283,24 @@ class 虚线: dif_up = dif_down = dea_up = dea_down = 0 for i in range(1, len(普K序列)): pre, cur = 普K序列[i - 1].macd, 普K序列[i].macd - if pre.DIF is None or cur.DIF is None: + if pre is None or cur is None or pre.DIF is None or cur.DIF is None: continue if pre.DIF < 0 <= cur.DIF: dif_up += 1 elif pre.DIF > 0 >= cur.DIF: dif_down += 1 - if pre.DEA < 0 <= cur.DEA: - dea_up += 1 - elif pre.DEA > 0 >= cur.DEA: - dea_down += 1 + if pre.DEA is not None and cur.DEA is not None: + if pre.DEA < 0 <= cur.DEA: + dea_up += 1 + elif pre.DEA > 0 >= cur.DEA: + dea_down += 1 # 2. DIF与DEA交叉(带标记) golden = death = 0 交叉标记 = [0] # 第0个位置无前值,先填0 for i in range(1, len(普K序列)): pre, cur = 普K序列[i - 1].macd, 普K序列[i].macd - if pre.DIF is None or cur.DIF is None or pre.DEA is None or cur.DEA is None: + if pre is None or cur is None or pre.DIF is None or cur.DIF is None or pre.DEA is None or cur.DEA is None: 交叉标记.append(0) continue if pre.DIF <= pre.DEA and cur.DIF > cur.DEA: @@ -4128,6 +4339,7 @@ class 虚线: return tmp.武 +@注册 class 笔: """纯静态方法容器,提供笔划分算法的所有函数。 @@ -4407,12 +4619,11 @@ class 笔: if 分型序列 and 分型序列[-1] is 临时分型: # 进行修复错过的笔 for ck in 缠K序列[缠K序列.index(武将) :]: - if ck.分型 in (分型结构.底, 分型结构.顶): + if ck.分型 in (分型结构.底, 分型结构.顶) and 分型序列[-1].时间戳 < ck.时间戳: 临时分型 = 分型.从缠K序列中获取分型(缠K序列, ck) 递归层次 = 笔递归分析(临时分型, 分型序列, 笔序列, 缠K序列, 普K序列, 递归层次 + 1, 配置) if 分型序列 and 分型序列[-1] is 临时分型: - """""" - # logger.warning("笔.分析 事后修复错过的笔", 临时分型, "当前分型", 当前分型) + logger.warning(f"笔.分析 事后修复错过的笔:{临时分型}, 当前分型: {当前分型}") 递归层次 = 笔递归分析(当前分型, 分型序列, 笔序列, 缠K序列, 普K序列, 递归层次 + 1, 配置) return 递归层次 @@ -4570,14 +4781,14 @@ class 线段特征: return self.标识 # f"{self.标识}:{self.序号}" def __str__(self): - if not len(self): + if not len(self.基础序列): return f"{self.标识}<{self.线段方向}, 空>" - return f"{self.标识}<{self.线段方向}, {self.文}, {self.武}, {len(self)}>" + return f"{self.标识}<{self.线段方向}, {self.文}, {self.武}, {len(self.基础序列)}>" def __repr__(self): - if not len(self): + if not len(self.基础序列): return f"{self.标识}<{self.线段方向}, 空>" - return f"{self.标识}<{self.线段方向}, {self.文}, {self.武}, {len(self)}>" + return f"{self.标识}<{self.线段方向}, {self.文}, {self.武}, {len(self.基础序列)}>" @property def 文(self) -> 分型: @@ -4762,6 +4973,7 @@ class 特征分型: return f"特征分型<{self.结构}, {self.中}>" +@注册 class 线段: """纯静态方法容器,提供线段划分算法的所有函数。 @@ -4788,6 +5000,12 @@ class 线段: __slots__ = [] + @staticmethod + def _索引(序列: list, 项) -> int: + """O(1) index lookup — 序列元素序号连续递增。""" + # return 项.序号 - 序列[0].序号 + return 序列.index(项) + @classmethod def _添加虚线(cls, 段: 虚线, 筆: 虚线): """向线段中添加一笔 @@ -4918,7 +5136,7 @@ class 线段: break if (len(基础序列) >= 6) and (len(基础序列) % 2 == 0): - 段.基础序列[:] = 基础序列[:] + 段.基础序列[:] = 基础序列 else: raise RuntimeError() else: @@ -4935,7 +5153,7 @@ class 线段: return 基础序列 = 段.基础序列 if 段.前一结束位置 and 段.前一结束位置 in 基础序列: - 基础序列 = 段.基础序列[段.基础序列.index(段.前一结束位置) - 1 :] + 基础序列 = 段.基础序列[cls._索引(段.基础序列, 段.前一结束位置) - 1 :] 特征序列 = 线段特征.静态分析(基础序列, 段.方向, 线段.四象(段), 配置.线段_特征序列忽视老阴老阳) if len(特征序列) >= 3: @@ -5053,7 +5271,7 @@ class 线段: 特征后一笔 = 最近特征.基础序列[-1] if 特征后一笔 is not None: - 序号 = 段.基础序列.index(特征后一笔) + 序号 = cls._索引(段.基础序列, 特征后一笔) if 序号 < len(段.基础序列) - 1: 下一笔 = 段.基础序列[序号 + 1] if (段.方向 is 相对方向.向上 and 段.高 <= 下一笔.高) or (段.方向 is 相对方向.向下 and 段.低 >= 下一笔.低): @@ -5072,15 +5290,16 @@ class 线段: :param 序列: 参考序列 """ 基础序列 = [] + 序列集 = set(序列) if not isinstance(序列, set) else 序列 for 元素 in 段.基础序列: - if 元素 not in 序列: + if 元素 not in 序列集: break if 基础序列: if not 基础序列[-1].之后是(元素): break 基础序列.append(元素) - 段.基础序列[:] = 基础序列[:] + 段.基础序列[:] = 基础序列 段.特征序列[2] = None @classmethod @@ -5149,16 +5368,17 @@ class 线段: return True @classmethod - def _添加线段(cls, 线段序列: List[虚线], 待添加线段: 虚线, 配置: 缠论配置, 行号: str): + def _添加线段(cls, 线段序列: List[虚线], 待添加线段: 虚线, 配置: 缠论配置, 行号: int, 层级: int): """内部方法:向线段序列添加新线段 :param 线段序列: 线段列表 :param 待添加线段: 新线段 :param 配置: 缠论配置 :param 行号: 调用行号 + :param 层级: 递归层级 """ if 线段序列 and not 线段序列[-1].之后是(待添加线段): - raise ValueError(f"线段.向序列中添加 不连续[{行号}]", 线段序列[-1].武, 待添加线段.文) + raise ValueError(f"线段.向序列中添加 不连续[{行号}, {层级}]", 线段序列[-1].武, 待添加线段.文) 待添加线段.模式 = "文武" if not 线段序列: @@ -5169,10 +5389,10 @@ class 线段: if not 之前线段.特征序列[2] and not 之前线段.短路修正: assert not 待添加线段.短路修正 and 之前线段.特征序列[2][-1] in 待添加线段.基础序列 - raise RuntimeError(f"线段._向序列中添加[{行号}], 之前线段.右 = None", 之前线段) + raise RuntimeError(f"线段._向序列中添加[{行号}, {层级}], 之前线段.右 = None", 之前线段) if 之前线段.基础序列[-1] not in 待添加线段.基础序列 and not 之前线段.短路修正: - raise RuntimeError(f"线段._向序列中添加[{行号}], 之前线段[-1] not in 待添加虚线!", 之前线段) + raise RuntimeError(f"线段._向序列中添加[{行号}, {层级}], 之前线段[-1] not in 待添加虚线!", 之前线段) 待添加线段.序号 = 之前线段.序号 + 1 待添加线段.前一缺口 = 线段.获取缺口(之前线段) if not 之前线段.短路修正 else None @@ -5185,13 +5405,14 @@ class 线段: # logger.warning(f"线段._向序列中添加[{行号}]", 待添加虚线) @classmethod - def _弹出线段(cls, 线段序列: List[虚线], 待弹出线段: 虚线, 配置: 缠论配置, 行号: str): + def _弹出线段(cls, 线段序列: List[虚线], 待弹出线段: 虚线, 配置: 缠论配置, 行号: int, 层级: int): """内部方法:从线段序列弹出最后一个线段 :param 线段序列: 线段列表 :param 待弹出线段: 待弹出的线段 :param 配置: 缠论配置 :param 行号: 调用行号 + :param 层级: 递归层级 :return: 弹出的线段或None """ if not 线段序列: @@ -5204,7 +5425,7 @@ class 线段: if 右 is not None: 结构 = 分型结构.分析(左, 中, 右, True, True) if 结构 in (分型结构.顶, 分型结构.底) and not 相对方向.分析(左.高, 左.低, 中.高, 中.低).是否缺口(): - logger.warning(f"警告<{行号}>] 线段._从序列中删除 发现分型完毕, 且特征序列无缺口 {待弹出线段}") + logger.warning(f"警告<{行号}, {层级}>] 线段._从序列中删除 发现分型完毕, 且特征序列无缺口 {待弹出线段}") 线段序列.pop() 待弹出线段.前一结束位置 = None @@ -5249,7 +5470,7 @@ class 线段: # 执行修正 序列 = 当前线段.基础序列[:] - 线段._弹出线段(线段序列, 当前线段, 配置, f"{sys._getframe().f_lineno}, {层级}") + 线段._弹出线段(线段序列, 当前线段, 配置, sys._getframe().f_lineno, 层级) assert 线段序列, "缺口突破: 线段序列为第二次空!" 当前线段 = 线段序列[-1] @@ -5258,7 +5479,7 @@ class 线段: assert 当前线段基础序列[-1].之后是(序列[0]), "缺口突破: 子序列不连续!" 当前线段基础序列.extend(序列) - 当前线段.基础序列[:] = 当前线段基础序列[:] + 当前线段.基础序列[:] = 当前线段基础序列 线段._刷新(当前线段, 配置) return True @@ -5286,7 +5507,7 @@ class 线段: assert 贯穿伤 in 当前线段.基础序列, "非缺口下穿刺: 贯穿伤不在基础序列中!" # 切割基础序列 - 基础序列 = 当前线段.基础序列[当前线段.基础序列.index(贯穿伤) :] + 基础序列 = 当前线段.基础序列[cls._索引(当前线段.基础序列, 贯穿伤) :] # 长度条件 if not (len(基础序列) == 4 and len(线段序列) >= 2): @@ -5302,19 +5523,25 @@ class 线段: logger.warning(f"[警告<{sys._getframe().f_lineno}, {层级}>]: {当前线段.标识}.修复贯穿伤, 序号:{当前线段.序号} {贯穿伤} {基础序列}") # 异常弹出 基础序列 = 当前线段.基础序列[:] - 线段._弹出线段(线段序列, 当前线段, 配置, f"{sys._getframe().f_lineno}, {层级}") + 线段._弹出线段(线段序列, 当前线段, 配置, sys._getframe().f_lineno, 层级) assert 线段序列, "非缺口下穿刺: 第二次线段序列为空!" 当前线段 = 线段序列[-1] 当前线段.特征序列[2] = None - assert 当前线段.基础序列[-1] in 基础序列, "非缺口下穿刺: 当前线段.基础序列[-1] 不在 基础序列中!" - for 临时虚线 in 基础序列[基础序列.index(当前线段.基础序列[-1]) + 1 :]: + # assert 当前线段.基础序列[-1] in 基础序列, "非缺口下穿刺: 当前线段.基础序列[-1] 不在 基础序列中!" + if 当前线段.基础序列[-1] not in 基础序列: + logger.error(f"非缺口下穿刺: 当前线段.基础序列[-1] 不在 基础序列中!") + 序号 = 0 + else: + 序号 = cls._索引(基础序列, 当前线段.基础序列[-1]) + 1 + + for 临时虚线 in 基础序列[序号:]: 线段._添加虚线(当前线段, 临时虚线) 线段._刷新(当前线段, 配置) 当前线段.短路修正 = True if 当前线段.特征序列[2] is not None: 段 = 虚线.创建线段([左, 中, 右]) - 线段._添加线段(线段序列, 段, 配置, f"{sys._getframe().f_lineno}, {层级}") + 线段._添加线段(线段序列, 段, 配置, sys._getframe().f_lineno, 层级) 段.特征序列[0] = 线段特征.新建([中], 段.方向) return True @@ -5360,7 +5587,7 @@ class 线段: # 执行修正 当前线段.短路修正 = True 新段 = 虚线.创建线段(基础序列) - 线段._添加线段(线段序列, 新段, 配置, f"{sys._getframe().f_lineno}, {层级}") + 线段._添加线段(线段序列, 新段, 配置, sys._getframe().f_lineno, 层级) return True @classmethod @@ -5403,7 +5630,7 @@ class 线段: # 创建第一个新段(之后基础序列去掉最后3个) 新段 = 虚线.创建线段(之后基础序列[:-3]) 新段.短路修正 = True - 线段._添加线段(线段序列, 新段, 配置, f"{sys._getframe().f_lineno}, {层级}") + 线段._添加线段(线段序列, 新段, 配置, sys._getframe().f_lineno, 层级) # 根据当前线段的四象决定是否清空前一个缺口 if 线段.四象(当前线段) in ("老阴", "老阳"): @@ -5411,7 +5638,7 @@ class 线段: # 创建第二个新段(最后3个元素) 新段 = 虚线.创建线段(之后基础序列[-3:]) - 线段._添加线段(线段序列, 新段, 配置, f"{sys._getframe().f_lineno}, {层级}") + 线段._添加线段(线段序列, 新段, 配置, sys._getframe().f_lineno, 层级) return True @@ -5447,7 +5674,7 @@ class 线段: if not 线段._基础判断(左, 中, 右, 关系序列): # FIXME 首个线段必须有明确方向 continue 段 = 虚线.创建线段([左, 中, 右]) - 线段._添加线段(线段序列, 段, 配置, f"{sys._getframe().f_lineno}, {层级}") + 线段._添加线段(线段序列, 段, 配置, sys._getframe().f_lineno, 层级) 段.特征序列[0] = 线段特征.新建([中], 段.方向) break if not 线段序列: @@ -5456,7 +5683,7 @@ class 线段: # -------------------- 2. 清理无效的尾部引用 -------------------- while 线段序列 and 线段序列[-1].前一结束位置: if 线段序列[-1].前一结束位置 not in 笔序列: - 线段._弹出线段(线段序列, 线段序列[-1], 配置, f"{sys._getframe().f_lineno}, {层级}") + 线段._弹出线段(线段序列, 线段序列[-1], 配置, sys._getframe().f_lineno, 层级) else: break @@ -5468,7 +5695,7 @@ class 线段: 线段._序列重置(当前线段, 笔序列) if len(当前线段.基础序列) < 3: - 线段._弹出线段(线段序列, 当前线段, 配置, f"{sys._getframe().f_lineno}, {层级}") + 线段._弹出线段(线段序列, 当前线段, 配置, sys._getframe().f_lineno, 层级) if not 线段序列: return 线段递归分析(笔序列, 线段序列, 配置, 层级 + 1, 关系序列) @@ -5478,7 +5705,7 @@ class 线段: if 当前线段.特征序列[2] is not None: 基础序列 = 线段.分割序列(当前线段)[1] 新段 = 虚线.创建线段(基础序列) - 线段._添加线段(线段序列, 新段, 配置, f"{sys._getframe().f_lineno}, {层级}") + 线段._添加线段(线段序列, 新段, 配置, sys._getframe().f_lineno, 层级) if 线段.四象(当前线段) in ("老阴", "老阳"): 新段.前一缺口 = None @@ -5495,9 +5722,10 @@ class 线段: 当前线段 = 线段序列[-1] if not 当前线段.基础序列: raise RuntimeError - 起始索引 = 笔序列.index(当前线段.基础序列[-1]) + 1 + 起始索引 = cls._索引(笔序列, 当前线段.基础序列[-1]) + 1 - for 当前虚线 in 笔序列[起始索引:]: + for idx in range(起始索引, len(笔序列)): + 当前虚线 = 笔序列[idx] 当前线段 = 线段序列[-1] 四象 = 线段.四象(当前线段) @@ -5524,7 +5752,7 @@ class 线段: 基础序列 = 线段.分割序列(当前线段)[1] 新段 = 虚线.创建线段(基础序列) - 线段._添加线段(线段序列, 新段, 配置, f"{sys._getframe().f_lineno}, {层级}") + 线段._添加线段(线段序列, 新段, 配置, sys._getframe().f_lineno, 层级) if 四象 in ("老阴", "老阳"): 新段.前一缺口 = None @@ -5556,15 +5784,16 @@ class 线段: :param 序列: 参考序列 """ 基础序列 = [] + 序列集 = set(序列) if not isinstance(序列, set) else 序列 for 元素 in 段.基础序列: - if 元素 not in 序列: + if 元素 not in 序列集: break if 基础序列: if not 基础序列[-1].之后是(元素): logger.warning(" 线段._验证序列 数据不连续") break 基础序列.append(元素) - 段.基础序列[:] = 基础序列[:] + 段.基础序列[:] = 基础序列 if len(段.基础序列) % 2 == 0: 段.基础序列 and 段.基础序列.pop() @@ -5626,7 +5855,7 @@ class 线段: if not 线段序列: for i in range(1, len(虚线序列) - 1): 左, 中, 右 = 虚线序列[i - 1], 虚线序列[i], 虚线序列[i + 1] - 关系 = 相对方向.分析(左.高, 左.低, 右.高, 右.低) + 关系 = 相对方向.分析(左.端点高, 左.端点低, 右.端点高, 右.端点低) if 关系 not in (相对方向.向下, 相对方向.向上, 相对方向.顺, 相对方向.逆, 相对方向.同): # FIXME 此处为首个线段 continue @@ -5646,7 +5875,7 @@ class 线段: if not 配置.扩展线段_当下分析: 左, 中, 右 = 当前线段.基础序列[:3] - if not 相对方向.分析(左.高, 左.低, 右.高, 右.低).是否缺口(): + if not 相对方向.分析(左.端点高, 左.端点低, 右.端点高, 右.端点低).是否缺口(): 当前线段.基础序列[:] = 当前线段.基础序列[:3] 线段._武终(当前线段, sys._getframe().f_lineno) else: @@ -5657,13 +5886,13 @@ class 线段: if 当前线段.基础序列[-1].序号 + 3 > 虚线序列[-1].序号: return None - 序号 = 虚线序列.index(当前线段.基础序列[-1]) + 1 + 序号 = cls._索引(虚线序列, 当前线段.基础序列[-1]) + 1 if 序号 >= len(虚线序列): return None for i in range(序号 + 1, len(虚线序列) - 1): 左, 中, 右 = 虚线序列[i - 1], 虚线序列[i], 虚线序列[i + 1] - 相对关系 = 相对方向.分析(左.高, 左.低, 右.高, 右.低) + 相对关系 = 相对方向.分析(左.端点高, 左.端点低, 右.端点高, 右.端点低) if 相对关系.是否缺口(): 线段._添加虚线(当前线段, 左) 线段._添加虚线(当前线段, 中) @@ -5720,7 +5949,7 @@ class 线段: if 当前段.实_中枢序列: if 阳[-1] in 当前段.实_中枢序列[-1].基础序列: # 当前最后一笔在最后一中枢里 - 序号 = 当前段.基础序列.index(当前段.实_中枢序列[-1].基础序列[0]) + 序号 = cls._索引(当前段.基础序列, 当前段.实_中枢序列[-1].基础序列[0]) 进入段 = 当前段.基础序列[序号 - 1] 离开段 = 阳[-1] assert 进入段.序号 < 离开段.序号, (进入段.序号, 离开段.序号) @@ -5774,7 +6003,7 @@ class 线段: 笔序列.append(停顿) 线段.分析(笔序列, 线段序列, 观察员.配置, 关系序列=[相对方向.向下, 相对方向.向上, 相对方向.顺, 相对方向.逆, 相对方向.同]) if 线段序列 and 线段序列[-1].武 is not 当前停顿 and len(线段序列[-1].基础序列) % 2 == 1: - 新段 = 虚线.创建线段(线段序列[-1].基础序列[:]) + 新段 = 虚线.创建线段(线段序列[-1].基础序列) 新段.序号 = self.序号 线段._刷新(新段, 观察员.配置) if 新段.方向 is self.方向: @@ -5805,6 +6034,7 @@ class 线段: return 结果 +@注册 class 中枢: """三段虚线重叠区间构成的价格中枢,支持延伸和扩展。 @@ -5954,9 +6184,23 @@ class 中枢: else: # if self.本级_第三买卖线: # return True + 中枢状态 = self.当前状态() + if 中枢状态 == "中枢之中": + return False 线段内部中枢 = self.基础序列[-1].合_中枢序列 if 虚实 == "合" else self.基础序列[-1].实_中枢序列 + if not 线段内部中枢: + return False + 高, 低 = self.高, self.低 for 内部中枢 in 线段内部中枢: - if 相对方向.分析(self.高, self.低, 内部中枢.高, 内部中枢.低).是否缺口(): + 内部中枢高, 内部中枢低 = 内部中枢.高, 内部中枢.低 + if 中枢状态 == "中枢之下": + if 低 <= 内部中枢高: + continue + else: + # 中枢之上 + if 高 >= 内部中枢低: + continue + if 相对方向.分析(self.高, self.低, 内部中枢高, 内部中枢低).是否缺口(): return True return False @@ -5989,13 +6233,14 @@ class 中枢: """ 有效序列 = self.基础序列[:] 无效序列 = [] + 序列集 = set(序列) for 元素 in self.基础序列: - if 元素 not in 序列: + if 元素 not in 序列集: 无效序列.append(元素) if 无效序列: 无效 = 无效序列[0] - 序号 = self.基础序列.index(无效) + 序号 = 线段._索引(self.基础序列, 无效) 有效序列 = self.基础序列[:序号] if len(有效序列) < 3: @@ -6182,7 +6427,7 @@ class 中枢: 左, 中, 右 = 虚线序列[i - 1], 虚线序列[i], 虚线序列[i + 1] if 中枢.基础检查(左, 中, 右): 新中枢 = 中枢.创建(左, 中, 右, 中.级别, 标识) - 序号 = 虚线序列.index(左) + 序号 = 线段._索引(虚线序列, 左) if 跳过首部 and (左.序号 == 0 or 序号 == 0): continue # 方便计算走势 if 序号 >= 2: @@ -6203,7 +6448,7 @@ class 中枢: 中枢._从中枢序列尾部弹出(中枢序列, 当前中枢) return 中枢递归分析(虚线序列, 中枢序列, 跳过首部, 标识, 层级 + 1) - 序号 = 虚线序列.index(当前中枢.基础序列[-1]) + 1 + 序号 = 线段._索引(虚线序列, 当前中枢.基础序列[-1]) + 1 基础序列 = [] for 当前虚线 in 虚线序列[序号:]: @@ -6231,6 +6476,7 @@ class 中枢: return None +@注册 class 观察者: """单周期缠论分析器,接收K线流式输入并逐层计算所有层级序列。 @@ -6749,6 +6995,7 @@ class K线合成器: return self.当前K线[周期] +@注册 class 立体分析器: """多周期缠论分析器,内部包含 :class:`K线合成器` + 每周期一个 :class:`观察者`。 @@ -6780,20 +7027,13 @@ class 立体分析器: for 周期 in self.周期组: 临时配置 = 配置组.get(周期, 配置) 当前配置 = 临时配置.model_copy( - update={ - "推送K线": False, - # "推送笔": False, - "推送线段": False, - # "图表展示": False, - }, + update={"图表展示标签": []}, deep=True, ) self._单体分析器[周期] = 观察者(符号=符号, 周期=周期, 配置=当前配置) - self._单体分析器[self.__显示周期].配置.推送K线 = True - self._单体分析器[self.__显示周期].配置.推送笔 = True - self._单体分析器[self.__显示周期].配置.推送线段 = True self._单体分析器[self.__显示周期].配置.图表展示 = True + self._单体分析器[self.__显示周期].配置.图表展示标签 = None # None = 全部展示 self._单体分析器[self.__显示周期].重置基础序列() for 周期 in self.周期组: # 将不同周期对其至显示周期 @@ -6844,6 +7084,1085 @@ class 立体分析器: logger.warning(f"多级别数据拆分保存完成,目录:{保存路径.resolve()}") +# ═══════════════════════════════════════════════════════════════════════════════ +# 以下信号匹配框架(import_by_name, Signal, Factor, Event, SignalsParser, +# Position 等类)摘录自 czsc 项目(https://github.com/zengbin93/czsc), +# Apache License 2.0 授权。 详见本文件头部 第三方代码声明。 +# ═══════════════════════════════════════════════════════════════════════════════ + + +def import_by_name(name: str): + """通过字符串导入模块、类、函数 + + 函数执行逻辑: + + 1. 检查 name 中是否包含点号('.')。如果没有,则直接使用内置的 import 函数来导入整个模块,并返回该模块对象。 + 2. 如果 name 包含点号,先处理一个相对路径。将 name 拆分为两部分:module_name 和 function_name。 + 使用 Python 内置的 rsplit 方法从右边开始分割,只取一次,这样可以确保我们将最后的一个点号前的部分作为 module_name,点号后面的部分作为 function_name。 + 3. 使用import函数导入指定的 module_name。 + 这里传入三个参数:globals() 和 locals() 分别代表当前全局和局部命名空间; + [function_name] 是一个列表,用于指定要导入的子模块或属性名。 + 这样做是为了避免一次性导入整个模块的所有内容,提高效率。 + 4. 使用 vars 函数获取模块的字典表示形式(即模块内所有的变量和函数),取出 function_name 对应的值,然后返回这个值。 + + :param name: 模块名,如:'czsc.objects.Factor' + :return: 模块对象 + """ + if "." not in name: + return __import__(name) + + # 从右边开始分割,分割成模块名和函数名 + module_name, function_name = name.rsplit(".", 1) + module = __import__(module_name, globals(), locals(), [function_name]) + 注入依赖(module) + + return vars(module)[function_name] + + +class Operate(Enum): + # 持有状态 + HL = "持多" # Hold Long + HS = "持空" # Hold Short + HO = "持币" # Hold Other + + # 多头操作 + LO = "开多" # Long Open + LE = "平多" # Long Exit + + # 空头操作 + SO = "开空" # Short Open + SE = "平空" # Short Exit + + def __str__(self): + return self.value + + +@dataclass +class Signal: + signal: str = "" + + # score 取值在 0~100 之间,得分越高,信号越强 + score: int = 0 + + # k1, k2, k3 是信号名称 + k1: str = "任意" # k1 一般是指明信号计算的K线周期,如 60分钟,日线,周线等 + k2: str = "任意" # k2 一般是记录信号计算的参数 + k3: str = "任意" # k3 用于区分信号,必须具有唯一性,推荐使用信号分类和开发日期进行标记 + + # v1, v2, v3 是信号取值 + v1: str = "任意" + v2: str = "任意" + v3: str = "任意" + + # 任意 出现在模板信号中可以指代任何值 + + def __post_init__(self): + if not self.signal: + self.signal = f"{self.k1}_{self.k2}_{self.k3}_{self.v1}_{self.v2}_{self.v3}_{self.score}" + else: + if not isinstance(self.signal, str): + raise TypeError(f"Signal 初始化需要字符串,收到了 {type(self.signal).__name__}: {self.signal!r}") + ( + self.k1, + self.k2, + self.k3, + self.v1, + self.v2, + self.v3, + score, + ) = self.signal.split("_") + self.score = int(score) + + if self.score > 100 or self.score < 0: + raise ValueError("score 必须在0~100之间") + + def __repr__(self): + return f"Signal('{self.signal}')" + + @property + def key(self) -> str: + """获取信号名称""" + key = "" + for k in [self.k1, self.k2, self.k3]: + if k != "任意": + key += k + "_" + return key.strip("_") + + @property + def value(self) -> str: + """获取信号值""" + return f"{self.v1}_{self.v2}_{self.v3}_{self.score}" + + def is_match(self, s: dict) -> bool: + """判断信号是否与信号列表中的值匹配 + + 代码的执行逻辑如下: + + 接收一个字典 s 作为参数,该字典包含了所有信号的信息。从字典 s 中获取名称为 key 的信号的值 v。 + 如果 v 不存在,则抛出异常。从信号的值 v 中解析出 v1、v2、v3 和 score 四个变量。 + + 如果当前信号的得分 score 大于等于目标信号的得分 self.score,则继续执行,否则返回 False。 + 如果当前信号的第一个值 v1 等于目标信号的第一个值 self.v1 或者目标信号的第一个值为 "任意",则继续执行,否则返回 False。 + 如果当前信号的第二个值 v2 等于目标信号的第二个值 self.v2 或者目标信号的第二个值为 "任意",则继续执行,否则返回 False。 + 如果当前信号的第三个值 v3 等于目标信号的第三个值 self.v3 或者目标信号的第三个值为 "任意",则返回 True,否则返回 False。 + + :param s: 所有信号字典 + :return: bool + """ + key = self.key + v = s.get(key, None) + if not v: + raise ValueError(f"{key} 不在信号列表中") + + if not isinstance(v, str): + logger.warning(f"信号 {key} 的值类型异常: {type(v).__name__} = {v!r},跳过匹配") + return False + + v1, v2, v3, score = v.split("_") + if int(score) >= self.score: + if v1 == self.v1 or self.v1 == "任意": + if v2 == self.v2 or self.v2 == "任意": + if v3 == self.v3 or self.v3 == "任意": + return True + return False + + +@dataclass +class Factor: + # signals_all 必须全部满足的信号,至少需要设定一个信号 + signals_all: List[Signal] + + # signals_any 满足其中任一信号,允许为空 + signals_any: List[Signal] = field(default_factory=list) + + # signals_not 不能满足其中任一信号,允许为空 + signals_not: List[Signal] = field(default_factory=list) + + name: str = "" + + def __post_init__(self): + if not self.signals_all: + raise ValueError("signals_all 不能为空") + _fatcor = self.dump() + _fatcor.pop("name") + sha256 = hashlib.sha256(str(_fatcor).encode("utf-8")).hexdigest().upper()[:4] + + if self.name: + self.name = self.name.split("#")[0] + f"#{sha256}" + else: + self.name = f"#{sha256}" + # self.name = f"{self.name}#{sha256}" if self.name else sha256 + + @property + def unique_signals(self) -> List[str]: + """获取 Factor 的唯一信号列表""" + signals = [] + signals.extend(self.signals_all) + if self.signals_any: + signals.extend(self.signals_any) + if self.signals_not: + signals.extend(self.signals_not) + signals = {x.signal if isinstance(x, Signal) else x for x in signals} + return list(signals) + + def is_match(self, s: dict) -> bool: + """判断 factor 是否满足""" + if self.signals_not: + for signal in self.signals_not: + if signal.is_match(s): + return False + + for signal in self.signals_all: + if not signal.is_match(s): + return False + + if not self.signals_any: + return True + + for signal in self.signals_any: + if signal.is_match(s): + return True + return False + + def dump(self) -> dict: + """将 Factor 对象转存为 dict""" + signals_all = [x.signal for x in self.signals_all] + signals_any = [x.signal for x in self.signals_any] if self.signals_any else [] + signals_not = [x.signal for x in self.signals_not] if self.signals_not else [] + + raw = { + "name": self.name, + "signals_all": signals_all, + "signals_any": signals_any, + "signals_not": signals_not, + } + return raw + + @classmethod + def load(cls, raw: dict): + """从 dict 中创建 Factor + + :param raw: 样例如下 + {'name': '单测', + 'signals_all': ['15分钟_倒0笔_方向_向上_其他_其他_0', '15分钟_倒0笔_长度_大于5_其他_其他_0'], + 'signals_any': [], + 'signals_not': []} + + :return: + """ + signals_any = [Signal(x) for x in raw.get("signals_any", [])] + signals_not = [Signal(x) for x in raw.get("signals_not", [])] + + fa = Factor( + name=raw.get("name", ""), + signals_all=[Signal(x) for x in raw["signals_all"]], + signals_any=signals_any, + signals_not=signals_not, + ) + return fa + + +@dataclass +class Event: + operate: Operate + + # 多个信号组成一个因子,多个因子组成一个事件。 + # 单个事件是一系列同类型因子的集合,事件中的任一因子满足,则事件为真。 + factors: List[Factor] + + # signals_all 必须全部满足的信号,允许为空 + signals_all: List[Signal] = field(default_factory=list) + + # signals_any 满足其中任一信号,允许为空 + signals_any: List[Signal] = field(default_factory=list) + + # signals_not 不能满足其中任一信号,允许为空 + signals_not: List[Signal] = field(default_factory=list) + + name: str = "" + + def __post_init__(self): + if not self.factors: + raise ValueError("factors 不能为空") + _event = self.dump() + _event.pop("name") + + sha256 = hashlib.sha256(str(_event).encode("utf-8")).hexdigest().upper()[:4] + if self.name: + self.name = self.name.split("#")[0] + f"#{sha256}" + # self.name = f"{self.name}#{sha256}" + else: + self.name = f"{self.operate.value}#{sha256}" + self.sha256 = sha256 + + @property + def unique_signals(self) -> List[str]: + """获取 Event 的唯一信号列表""" + signals = [] + if self.signals_all: + signals.extend(self.signals_all) + if self.signals_any: + signals.extend(self.signals_any) + if self.signals_not: + signals.extend(self.signals_not) + + for factor in self.factors: + signals.extend(factor.unique_signals) + + signals = {x.signal if isinstance(x, Signal) else x for x in signals} + return list(signals) + + def get_signals_config(self, signals_module: str = "chanlun.signals") -> List[Dict]: + """获取事件的信号配置""" + + return get_signals_config(self.unique_signals, signals_module) + + def is_match(self, s: dict): + """判断 event 是否满足 + + 代码的执行逻辑如下: + + 1. 首先判断 signals_not 中的信号是否得到满足,如果满足任意一个信号,则直接返回 False,表示事件不满足。 + 2. 接着判断 signals_all 中的信号是否全部得到满足,如果有任意一个信号不满足,则直接返回 False,表示事件不满足。 + 3. 然后判断 signals_any 中的信号是否有一个得到满足,如果一个都不满足,则直接返回 False,表示事件不满足。 + 4. 最后判断因子是否满足,顺序遍历因子列表,找到第一个满足的因子就退出,并返回 True 和该因子的名称,表示事件满足。 + 5. 如果遍历完所有因子都没有找到满足的因子,则返回 False,表示事件不满足。 + """ + if self.signals_not and any(signal.is_match(s) for signal in self.signals_not): + return False, None + + if self.signals_all and not all(signal.is_match(s) for signal in self.signals_all): + return False, None + + if self.signals_any and not any(signal.is_match(s) for signal in self.signals_any): + return False, None + + for factor in self.factors: + if factor.is_match(s): + return True, factor.name + + return False, None + + def dump(self) -> dict: + """将 Event 对象转存为 dict""" + signals_all = [x.signal for x in self.signals_all] if self.signals_all else [] + signals_any = [x.signal for x in self.signals_any] if self.signals_any else [] + signals_not = [x.signal for x in self.signals_not] if self.signals_not else [] + factors = [x.dump() for x in self.factors] + + raw = { + "name": self.name, + "operate": self.operate.value, + "signals_all": signals_all, + "signals_any": signals_any, + "signals_not": signals_not, + "factors": factors, + } + return raw + + @classmethod + def load(cls, raw: dict): + """从 dict 中创建 Event + + :param raw: 样例如下 + {'name': '单测', + 'operate': '开多', + 'factors': [{'name': '测试', + 'signals_all': ['15分钟_倒0笔_长度_大于5_其他_其他_0'], + 'signals_any': [], + 'signals_not': []}], + 'signals_all': ['15分钟_倒0笔_方向_向上_其他_其他_0'], + 'signals_any': [], + 'signals_not': []} + :return: + """ + # 检查输入参数是否合法 + assert raw["operate"] in Operate.__dict__["_value2member_map_"], f"operate {raw['operate']} not in Operate" + assert raw["factors"], "factors can not be empty" + + e = Event( + name=raw.get("name", ""), + operate=Operate.__dict__["_value2member_map_"][raw["operate"]], + factors=[Factor.load(x) for x in raw["factors"]], + signals_all=[Signal(x) for x in raw.get("signals_all", [])], + signals_any=[Signal(x) for x in raw.get("signals_any", [])], + signals_not=[Signal(x) for x in raw.get("signals_not", [])], + ) + return e + + +class SignalsParser: + """解析一串信号,生成信号函数配置""" + + def __init__(self, signals_module: str = "chanlun.signals"): + """ + + 函数执行逻辑: + + 1. 将传入的 signals_module 参数赋给实例变量 self.signals_module,代表信号函数所在的模块,默认模块是czsc库的signals模块。 + 2. 使用 import_by_name 函数导入了指定名称的模块 signals_module。 + 3. 对于导入的模块中的每个属性名进行遍历: + - 魔法函数和私有函数不进行处理。 + - 获取函数的注解信息,并通过正则表达式获取注解中的参数模板和信号列表。 + - 如果解析到了参数模板,则将其存储在 sig_pats_map 中,key是函数名称。 + - 如果解析到了信号列表,则将其存储在 sig_name_map 中,并且为每个信号创建了 Signal 对象并存储在列表中,key是函数名称。 + 4. 最后将得到的 sig_name_map 和 sig_pats_map 存储在实例变量中,以便其他方法使用。 + + :param signals_module: 指定信号函数所在模块 + """ + self.signals_module = signals_module + sig_name_map = {} + sig_pats_map = {} + sig_trigger_map = {} + + signals_module = import_by_name(signals_module) + for name in dir(signals_module): + if "_" not in name or name.startswith("__"): + continue + + try: + doc = getattr(signals_module, name).__doc__ + # 解析信号函数参数 + pats = re.findall(r"参数模板:\"(.*)\"", doc) + if pats: + sig_pats_map[name] = pats[0] + + # 解析信号列表 + sigs = re.findall(r"Signal\('(.*)'\)", doc) + if sigs: + sig_name_map[name] = [Signal(x) for x in sigs] + + # 解析触发条件 + 触发匹配 = re.findall(r"触发条件:(.*)", doc) + if 触发匹配: + sig_trigger_map[name] = [x.strip() for x in 触发匹配[0].split(",")] + + except (OSError, ImportError, TypeError, ValueError, AttributeError) as e: + logger.error(f"解析信号函数 {name} 出错:{e}") + + self.sig_name_map = sig_name_map + self.sig_pats_map = sig_pats_map + self.sig_trigger_map = sig_trigger_map + + def parse_params(self, name, signal): + """获取信号函数参数 + + 函数执行逻辑: + + 1. 首先根据传入的 name 和 signal 参数,通过 Signal(signal).key 获取一个键值。 + 2. 然后从实例变量 sig_pats_map 中获取与指定名称对应的参数模板,并将其存储在 pats 中。 + 3. 如果没有找到参数模板,则返回 None。 + 4. 最后将信号函数的完整名称存储在参数字典中,并返回参数字典。 + + :param name: 信号函数名称, 如:cxt_bi_end_V230222 + :param signal: 需要解析的信号, 如:15分钟_D1K_量柱V221218_低量柱_6K_任意_0 + :return: + """ + key = Signal(signal).key + pats = self.sig_pats_map.get(name, None) + if not pats: + return None + + try: + params = parse(pats, key).named # type: ignore + if "di" in params: + params["di"] = int(params["di"]) + + params["name"] = f"{self.signals_module}.{name}" + + # 附加上下文:触发条件与函数短名(供 信号计算器 优化用) + 触发条件 = self.sig_trigger_map.get(name) + if 触发条件: + params["触发条件"] = 触发条件 + params["_func_short_name"] = name + + return params + except (ValueError, KeyError, TypeError, AttributeError) as e: + logger.error(f"解析信号 {signal} - {name} - {pats} 出错:{e}") + return None + + def get_function_name(self, signal: str): + """获取信号对应的信号函数名称 + + 函数执行逻辑: + + 1. 创建一个 _signal 对象,通过传入的信号字符串进行初始化。 + 2. 通过遍历 sig_name_map 中的项目,找出那些与 _signal.k3 相匹配的键,并将它们存储在 _k3_match 列表中。 + 3. 如果只有一个匹配项,则返回该项;否则记录错误日志并返回 None。 + + :param signal: 信号,数据样例:15分钟_D1K_量柱V221218_低量柱_6K_任意_0 + :return: 信号函数名称 + """ + sig_name_map = self.sig_name_map + _signal = Signal(signal) + _k3_match = list({k for k, v in sig_name_map.items() if v[0].k3 == _signal.k3}) + + # 多匹配时排除模板函数(以 "模板_" 开头) + if len(_k3_match) > 1: + non_template = [k for k in _k3_match if not k.startswith("模板_")] + if len(non_template) == 1: + return non_template[0] + + if len(_k3_match) == 1: + return _k3_match[0] + else: + logger.error(f"信号 {signal} 有多个匹配函数:{_k3_match},请手动解析信号") + return None + + def config_to_keys(self, config: List[Dict]): + """将信号函数配置转换为信号key列表 + + 函数执行逻辑: + + 1. 首先创建了一个空列表 keys 用于存储信号key。 + 2. 对于传入的 config 列表中的每个配置字典 conf 进行以下操作: + - 获取信号函数的名称。 + - 如果该信号函数的名称在 self.sig_pats_map 中存在对应的模板,使用参数填充模板,并将结果添加到 keys 列表中。 + + :param config: 信号函数配置 + + config = [{'freq': '日线', 'max_overlap': '3', 'name': 'czsc.signals.cxt_bi_end_V230222'}, + {'freq1': '日线', 'freq2': '60分钟', 'name': 'czsc.signals.cxt_zhong_shu_gong_zhen_V221221'}] + + :return: 信号key列表 + """ + keys = [] + for conf in config: + name = conf["name"].split(".")[-1] + if name in self.sig_pats_map: + keys.append(self.sig_pats_map[name].format(**conf)) + return keys + + def parse(self, signal_seq: List[str]): + """解析信号序列 + + 函数执行逻辑: + + 1. 接受一个signal_seq 参数。 + 2. 定义一个空列表res ,用于存储解析结果。 + 3. 遍历信号序列signal_seq 中的每一个信号: + + - 调用get_function_name 方法,以信号为参数,获取该信号对应的函数名。 + - 进行函数名存在性判断,name 在sig_pats_map 中存在, + 调用parse_params 方法,以函数名和信号为参数,解析参数并返回结果。 + + :param signal_seq: 信号序列, 样例: + ['15分钟_D1K_量柱V221218_低量柱_6K_任意_0', '日线_D1K_量柱V221218_低量柱_6K_任意_0'] + :return: 信号函数配置 + """ + res = [] + for signal in signal_seq: + name = self.get_function_name(signal) + if name in self.sig_pats_map: + row = self.parse_params(name, signal) + if row and row not in res: + res.append(row) + else: + logger.warning(f"未找到解析函数:{name},请手动解析信号:{signal}") + return res + + +def get_signals_config(signals_seq: List[str], signals_module: str = "") -> List[Dict]: + """获取信号列表对应的信号函数配置 + + 函数执行逻辑: + + 1. 首先创建了一个 SignalsParser 类的实例对象 sp,传入了参数 signals_module进行初始化, + 初始化工作主要是解析signals_module下的信号函数,生成了sig_pats_map信号参数模板字典和sig_name_map信号列表字典。 + 2. 然后使用 sp 实例调用 parse 方法,该方法解析 signals_seq 中的信号,并返回信号函数的配置信息。 + + :param signals_seq: 信号列表 + :param signals_module: 信号函数所在模块 + :return: 信号函数配置 + """ + sp = SignalsParser(signals_module=signals_module) + conf = sp.parse(signals_seq) + return conf + + +@注册 +def create_single_signal(**kwargs) -> OrderedDict: + """创建单个信号""" + s = OrderedDict() + k1, k2, k3 = kwargs.get("k1", "任意"), kwargs.get("k2", "任意"), kwargs.get("k3", "任意") + v1, v2, v3 = kwargs.get("v1", "任意"), kwargs.get("v2", "任意"), kwargs.get("v3", "任意") + v = Signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2, v3=v3, score=kwargs.get("score", 0)) + s[v.key] = v.value + return s + + +# ============================================================================== +# Position — 持仓管理 +# ============================================================================== + + +class Position: + def __init__( + self, + symbol: str, + opens: List[Event], + exits: List[Event] = [], + interval: int = 0, + timeout: int = 1000, + stop_loss=1000, + T0: bool = False, + name=None, + ): + """简单持仓对象,仓位表达:1 持有多头,-1 持有空头,0 空仓 + + :param symbol: 标的代码 + :param opens: 开仓交易事件列表 + :param exits: 平仓交易事件列表,允许为空 + :param interval: 同类型开仓间隔时间,单位:秒;默认值为 0,表示同类型开仓间隔没有约束 + 假设上次开仓为多头,那么下一次多头开仓时间必须大于 上次开仓时间 + interval;空头也是如此。 + :param timeout: 最大允许持仓K线数量限制为最近一个开仓事件触发后的 timeout 根基础周期K线 + :param stop_loss: 最大允许亏损比例,单位:BP, 1BP = 0.01%;成本的计算以最近一个开仓事件触发价格为准 + :param T0: 是否允许T0交易,默认为 False 表示不允许T0交易 + :param name: 仓位名称,默认值为第一个开仓事件的名称 + """ + assert name, "name 是必须的参数" + self.symbol = symbol + self.opens = opens + self.name = name + self.exits = exits if exits else [] + self.events = self.opens + self.exits + for event in self.events: + assert event.operate in [Operate.LO, Operate.LE, Operate.SO, Operate.SE] + + self.interval = interval + self.timeout = timeout + self.stop_loss = stop_loss + self.T0 = T0 + + self.pos_changed = False # 仓位是否发生变化 + self.operates = [] # 事件触发的操作列表 + self.holds = [] # 持仓状态列表 + self.pos = 0 + + # 辅助判断的缓存数据 + self.last_event = { + "dt": None, + "bid": None, + "price": None, + "op": None, + "op_desc": None, + } + self.last_lo_dt = None # 最近一次开多交易的时间 + self.last_so_dt = None # 最近一次开空交易的时间 + self.end_dt = None # 最近一次信号传入的时间 + + def __repr__(self): + return f"Position(name={self.name}, symbol={self.symbol}, opens={[x.name for x in self.opens]}, timeout={self.timeout}, stop_loss={self.stop_loss}BP, T0={self.T0}, interval={self.interval}s)" + + @property + def unique_signals(self) -> List[str]: + """获取所有事件的唯一信号列表""" + signals = [] + for e in self.events: + signals.extend(e.unique_signals) + return list(set(signals)) + + def get_signals_config(self, signals_module: str = "") -> List[Dict]: + """获取事件的信号配置""" + return get_signals_config(self.unique_signals, signals_module) + + def dump(self, with_data: bool = False) -> dict: + """将对象转换为 dict""" + raw = { + "symbol": self.symbol, + "name": self.name, + "opens": [x.dump() for x in self.opens], + "exits": [x.dump() for x in self.exits], + "interval": self.interval, + "timeout": self.timeout, + "stop_loss": self.stop_loss, + "T0": self.T0, + } + if with_data: + raw.update({"pairs": self.pairs, "holds": self.holds}) + return raw + + @classmethod + def load(cls, raw: dict) -> "Position": + """从 dict 中创建 Position + :param raw: 样例如下 + :return: + """ + pos = Position( + name=raw["name"], + symbol=raw["symbol"], + opens=[Event.load(x) for x in raw["opens"] if raw.get("opens")], + exits=[Event.load(x) for x in raw["exits"] if raw.get("exits")], + interval=raw["interval"], + timeout=raw["timeout"], + stop_loss=raw["stop_loss"], + T0=raw["T0"], + ) + return pos + + @property + def pairs(self) -> List[Dict]: + """开平交易列表 + + 返回样例: + + [{'标的代码': '000001.SH', + '交易方向': '多头', + '开仓时间': Timestamp('2020-04-17 00:00:00'), + '平仓时间': Timestamp('2020-04-20 00:00:00'), + '开仓价格': 2838.49, + '平仓价格': 2852.55, + '持仓K线数': 1, + '事件序列': '开多@站上SMA5 -> 开多@站上SMA5', + '持仓天数': 3.0, + '盈亏比例': 49.53}, + {'标的代码': '000001.SH', + '交易方向': '多头', + '开仓时间': Timestamp('2020-04-20 00:00:00'), + '平仓时间': Timestamp('2020-04-24 00:00:00'), + '开仓价格': 2852.55, + '平仓价格': 2808.53, + '持仓K线数': 4, + '事件序列': '开多@站上SMA5 -> 平多@100BP止损', + '持仓天数': 4.0, + '盈亏比例': -154.32}] + + 数据说明: + + 1. 盈亏比例,单位是 BP + 2. 持仓天数,单位是 自然日 + 3. 持仓K线数,指基础周期K线数量 + """ + pairs = [] + + for op1, op2 in zip(self.operates, self.operates[1:]): + if op1["op"] not in [Operate.LO, Operate.SO]: + continue + + ykr = op2["price"] / op1["price"] - 1 if op1["op"] == Operate.LO else 1 - op2["price"] / op1["price"] + pair = { + "标的代码": self.symbol, + "策略标记": self.name, + "交易方向": "多头" if op1["op"] == Operate.LO else "空头", + "开仓时间": op1["dt"], + "平仓时间": op2["dt"], + "开仓价格": op1["price"], + "平仓价格": op2["price"], + "持仓K线数": op2["bid"] - op1["bid"], + "事件序列": f"{op1['op_desc']} -> {op2['op_desc']}", + "持仓天数": (op2["dt"] - op1["dt"]).total_seconds() / (24 * 3600), + "盈亏比例": round(ykr * 10000, 2), # 盈亏比例 转换成以 BP 为单位的收益,1BP = 0.0001 + } + pairs.append(pair) + + return pairs + + def update(self, s: dict): + """更新持仓状态 + + 函数执行逻辑: + + - 首先,检查最新信号的时间是否在上次信号之前,如果是则打印警告信息并返回。 + - 初始化一些变量,包括操作类型(op)和操作描述(op_desc)。 + - 遍历所有的事件,检查是否与最新信号匹配。如果匹配,则记录操作类型和操作描述,并跳出循环。 + - 提取最新信号的相关信息,包括交易对符号、时间、价格和成交量。 + - 更新持仓状态的结束时间为最新信号的时间。 + - 如果操作类型是开仓(LO或SO),更新最后一个事件的信息。 + - 定义一个内部函数__create_operate,用于创建操作记录。 + - 根据操作类型更新仓位和操作记录。 + + - 如果操作类型是LO(开多),检查是否满足开仓条件,如果满足则开多仓,否则只平空仓。 + - 如果操作类型是SO(开空),检查是否满足开仓条件,如果满足则开空仓,否则只平多仓。 + - 如果当前持仓为多仓,进行多头出场的判断: + - 如果操作类型是LE(平多),平多仓。 + - 如果当前价格相对于最后一个事件的价格的收益率小于止损阈值,平多仓。 + - 如果当前成交量相对于最后一个事件的成交量的增加量大于超时阈值,平多仓。 + + - 如果当前持仓为空仓,进行空头出场的判断: + - 如果操作类型是SE(平空),平空仓。 + - 如果当前价格相对于最后一个事件的价格的收益率小于止损阈值,平空仓。 + - 如果当前成交量相对于最后一个事件的成交量的增加量大于超时阈值,平空仓。 + + - 将当前持仓状态和价格记录到持仓列表中。 + + :param s: 最新信号字典 + :return: + """ + if self.end_dt and s["dt"] <= self.end_dt: + logger.warning(f"请检查信号传入:最新信号时间{s['dt']}在上次信号时间{self.end_dt}之前") + return + + self.pos_changed = False + op = Operate.HO + op_desc = "" + for event in self.events: + m, f = event.is_match(s) + if m: + op = event.operate + op_desc = f"{event.name}@{f}" + break + + symbol = s["symbol"] + dt = s["dt"] + price = s["close"] + bid = s.get("id", s.get("bid", 0)) + self.end_dt = dt + + # 当有新的开仓 event 发生,更新 last_event + if op in [Operate.LO, Operate.SO]: + self.last_event = { + "dt": dt, + "bid": bid, + "price": price, + "op": op, + "op_desc": op_desc, + } + + def __create_operate(_op, _op_desc): + self.pos_changed = True + return { + "symbol": symbol, + "dt": dt, + "bid": bid, + "price": price, + "op": _op, + "op_desc": _op_desc, + "pos": self.pos, + } + + # 更新仓位 + if op == Operate.LO: + if self.pos != 1 and (not self.last_lo_dt or (dt - self.last_lo_dt).total_seconds() > self.interval): + # 与前一次开多间隔时间大于 interval,直接开多 + self.pos = 1 + self.operates.append(__create_operate(Operate.LO, op_desc)) + self.last_lo_dt = dt + else: + # 与前一次开多间隔时间小于 interval,仅对空头平仓 + if self.pos == -1 and (self.T0 or dt.date() != self.last_so_dt.date()): + self.pos = 0 + self.operates.append(__create_operate(Operate.SE, op_desc)) + + if op == Operate.SO: + if self.pos != -1 and (not self.last_so_dt or (dt - self.last_so_dt).total_seconds() > self.interval): + # 与前一次开空间隔时间大于 interval,直接开空 + self.pos = -1 + self.operates.append(__create_operate(Operate.SO, op_desc)) + self.last_so_dt = dt + else: + # 与前一次开空间隔时间小于 interval,仅对多头平仓 + if self.pos == 1 and (self.T0 or dt.date() != self.last_lo_dt.date()): + self.pos = 0 + self.operates.append(__create_operate(Operate.LE, op_desc)) + + # 多头出场 + if self.pos == 1 and (self.T0 or dt.date() != self.last_lo_dt.date()): + assert self.last_event["dt"] >= self.last_lo_dt + + # 多头平仓 + if op == Operate.LE: + self.pos = 0 + self.operates.append(__create_operate(Operate.LE, op_desc)) + + # 多头止损 + if price / self.last_event["price"] - 1 < -self.stop_loss / 10000: + self.pos = 0 + self.operates.append(__create_operate(Operate.LE, f"平多@{self.stop_loss}BP止损")) + + # 多头超时 + if bid - self.last_event["bid"] > self.timeout: + self.pos = 0 + self.operates.append(__create_operate(Operate.LE, f"平多@{self.timeout}K超时")) + + # 空头出场 + if self.pos == -1 and (self.T0 or dt.date() != self.last_so_dt.date()): + assert self.last_event["dt"] >= self.last_so_dt + + # 空头平仓 + if op == Operate.SE: + self.pos = 0 + self.operates.append(__create_operate(Operate.SE, op_desc)) + + # 空头止损 + if 1 - price / self.last_event["price"] < -self.stop_loss / 10000: + self.pos = 0 + self.operates.append(__create_operate(Operate.SE, f"平空@{self.stop_loss}BP止损")) + + # 空头超时 + if bid - self.last_event["bid"] > self.timeout: + self.pos = 0 + self.operates.append(__create_operate(Operate.SE, f"平空@{self.timeout}K超时")) + + self.holds.append({"dt": self.end_dt, "pos": self.pos, "price": price}) + + +class 信号计算器: + """多周期信号计算引擎 — 基于观察者字典。 + + 不再依赖 立体分析器,直接接收 ``{周期秒: 观察者}`` 字典。 + + 使用方式:: + + 分析器 = 立体分析器("btcusd", [300, 900, 3600], 配置) + 观察者字典 = {p: 分析器._单体分析器[p] for p in 分析器.周期组} + 计算器 = 信号计算器(观察者字典, 基础周期=300, 信号配置=[...]) + + for k in k线列表: + 分析器.投喂K线(k) + 计算器.更新() + print(计算器.信号字典) + """ + + def __init__( + self, + 分析器: 立体分析器, + 信号配置: Optional[List[Dict]] = None, + 信号模块: str = "", + ): + self._分析器 = 分析器 + self._观察者字典 = {p: 分析器._单体分析器[p] for p in 分析器.周期组} + self._基础周期 = 分析器.周期组[0] + self._信号模块 = 信号模块 + self._信号函数缓存: Dict[str, Callable] = {} + self.信号: dict = {} + self.行情: dict = {} + self.信号配置 = 信号配置 or [] + self._自动挂载指标() + + @property + def 信号字典(self) -> dict: # 向后兼容:合并返回 + return {**self.信号, **self.行情} + + @property + def 信号配置(self) -> List[Dict]: + return self._信号配置 + + @信号配置.setter + def 信号配置(self, value: List[Dict]): + 可用周期 = set(self._分析器.周期组) + for c in value: + freq = c.get("freq") + if freq is not None: + 周期秒 = int(freq) + if 周期秒 not in 可用周期: + raise ValueError(f"信号配置 freq={freq}({周期秒}s) 不在分析器周期组 {sorted(可用周期)} 中\n 信号: {c.get('name', '?')}") + self._信号配置 = self._去重配置(value) + self._预加载信号函数() + + def _去重配置(self, configs: List[Dict]) -> List[Dict]: + seen = set() + unique = [] + for c in configs: + key = (c.get("name"), frozenset((k, str(v)) for k, v in c.items() if k != "name")) + if key not in seen: + seen.add(key) + unique.append(c) + else: + logger.warning(f"信号计算器: 重复信号配置已跳过 — {c.get('name', '?')} { {k: v for k, v in c.items() if k != 'name'} }") + return unique + + def _预加载信号函数(self): + for config in self._信号配置: + name = config.get("name") + if name and name not in self._信号函数缓存: + try: + self._信号函数缓存[name] = self._解析信号函数(name) + except (ImportError, ModuleNotFoundError, AttributeError, KeyError) as e: + logger.warning(f"信号计算器: 无法导入 {name} ({e}),跳过") + + @staticmethod + def _解析信号函数(name: str): + """解析信号函数名,返回可调用对象。 + + 当运行在 __main__ 上下文中且目标模块为 chan 时,优先使用 __main__ + 命名空间中的函数,避免 import_by_name 触发 chan 模块的重复导入。 + """ + if "." in name: + module_name, func_name = name.rsplit(".", 1) + main_mod = sys.modules.get("__main__") + if main_mod is not None and hasattr(main_mod, func_name): + # 验证 __main__ 确实是目标模块(通过文件名判断) + main_file = getattr(main_mod, "__file__", "") + expected_path = module_name.replace(".", os.sep) + ".py" + if main_file.endswith(expected_path): + return getattr(main_mod, func_name) + + return import_by_name(name) + + def _自动挂载指标(self): + """根据信号配置参数,在对应周期的观察者上自动补全缺失的指标。""" + + 待补MACD: Dict[int, List[tuple]] = defaultdict(list) + 待补均线: Dict[int, List[tuple]] = defaultdict(list) + + for config in self._信号配置: + name = config.get("name", "") + freq = config.get("freq") + if not freq: + continue + 周期秒 = int(freq) + if 周期秒 not in self._观察者字典: + continue + + # MACD 类信号:从 config 解析 fast/slow/signal 参数 + if "macd" in name.lower() or "中枢" in name or "背驰" in name or "金叉" in name: + fast = int(config.get("fast", config.get("快线周期", 13))) + slow = int(config.get("slow", config.get("慢线周期", 31))) + sig = int(config.get("signal", config.get("信号周期", 11))) + key = f"macd_{fast}_{slow}_{sig}" + if not any(t[0] == key for t in 待补MACD[周期秒]): + 待补MACD[周期秒].append((key, "收", fast, slow, sig)) + + # MA 类信号:从 config 解析 ma_type/timeperiod + if "ma_" in name or "tas_ma" in name or "均线" in name: + ma_type = config.get("ma_type", "SMA").upper() + period = int(config.get("timeperiod", config.get("周期", 5))) + key = f"{ma_type}_{period}" + if not any(t[0] == key for t in 待补均线[周期秒]): + 待补均线[周期秒].append((key, "收", ma_type, period)) + + for 周期秒, macd_list in 待补MACD.items(): + cfg = self._观察者字典[周期秒].配置 + if not cfg.计算指标: + cfg.计算指标 = True + 已有键 = {t[0] for t in cfg.MACD_参数列表} + # 同时检查 (快线, 慢线, 信号) 参数避免只键名不同但参数相同的重复 + 已有参数 = {(t[2], t[3], t[4]) for t in cfg.MACD_参数列表 if len(t) >= 5} + 新增 = [t for t in macd_list if t[0] not in 已有键 and (t[2], t[3], t[4]) not in 已有参数] + if 新增: + cfg.MACD_参数列表.extend(新增) + if "macd" not in 已有键: + cfg.MACD_参数列表.insert(0, ("macd", "收", 新增[0][2], 新增[0][3], 新增[0][4])) + logger.warning(f"信号计算器: 周期{周期秒}s 自动补全 MACD — {[t[0] for t in 新增]}") + + for 周期秒, ma_list in 待补均线.items(): + cfg = self._观察者字典[周期秒].配置 + if not cfg.计算指标: + cfg.计算指标 = True + 已有 = {t[0] for t in cfg.均线参数列表} + 新增 = [t for t in ma_list if t[0] not in 已有] + if 新增: + cfg.均线参数列表.extend(新增) + logger.warning(f"信号计算器: 周期{周期秒}s 自动补全 均线 — {[t[0] for t in 新增]}") + + def 从信号列表提取配置(self, 信号序列: List[str]): + """从信号序列自动生成信号配置""" + self.信号配置 = get_signals_config(list(set(信号序列)), self._信号模块) + + def 更新(self): + """遍历信号配置,调用信号函数。结果写入 self.信号 和 self.行情。""" + self.信号.clear() + self.行情.clear() + + for config in self._信号配置: + try: + result = self._执行信号函数(config) + if result: + for k, v in result.items(): + if v != "任意_任意_任意_0": + self.信号[k] = v + except (TypeError, ValueError, KeyError, AttributeError, IndexError) as e: + logger.error(f"信号计算器: {config.get('name', '?')} 出错 — {e}") + traceback.print_exc() + + # OHLCV 行情 + 基础观察者 = self._观察者字典.get(self._基础周期) + if 基础观察者 and 基础观察者.普通K线序列: + 最后K线 = 基础观察者.普通K线序列[-1] + 时间戳 = 最后K线.时间戳 + if isinstance(时间戳, (int, float)): + 时间戳 = datetime.fromtimestamp(int(时间戳)) + self.行情.update( + symbol=基础观察者.符号, + dt=时间戳, + id=最后K线.序号, + open=最后K线.开盘价, + close=最后K线.收盘价, + high=最后K线.高, + low=最后K线.低, + vol=最后K线.成交量, + ) + + def _执行信号函数(self, config: Dict) -> Optional[OrderedDict]: + param = dict(config) + sig_name = param.pop("name") + sig_func = self._信号函数缓存.get(sig_name) or self._解析信号函数(sig_name) + + freq = param.get("freq") + if freq is not None: + 周期秒 = int(freq) + obs = self._观察者字典.get(周期秒) + if obs is not None: + return sig_func(obs, **param) + else: + raise KeyError(f"信号计算器: 未找到周期 {周期秒}s 的观察者,可用周期: {sorted(self._观察者字典.keys())}") + else: + return sig_func(self, **param) + + def 获取周期观察者(self, freq: str) -> Optional[观察者]: + return self._观察者字典.get(int(freq)) + + def 测试_读取数据(观察员: 观察者, 配置: 缠论配置) -> Callable[[], 观察者]: """测试_读取数据 :param 观察员: 观察者 @@ -6906,13 +8225,15 @@ def 测试_指标挂载(配置: 缠论配置): size = struct.calcsize(">6d") for i in range(len(buffer) // size): if i == 500: - 配置.MACD_参数列表 = [("macd", 配置.平滑异同移动平均线_快线周期, 配置.平滑异同移动平均线_慢线周期, 配置.平滑异同移动平均线_信号周期)] + 配置.MACD_参数列表 = [("macd", "收", 13, 31, 11)] 配置.MACD_参数列表.append(("macd_12_26_9", 12, 26, 9)) k线 = K线.读取大端字节数组(buffer[i * size : i * size + size], 周期, 符号) 观察员.增加原始K线(k线) if i == 500: assert 观察员.普通K线序列[0].指标.macd_12_26_9 is not None, "指标挂载失败" - print(观察员.普通K线序列[0].指标["macd_12_26_9"]) + print(观察员.普通K线序列[-1].指标["macd_12_26_9"]) + print(观察员.普通K线序列[-1].macd) + break 消耗用时 = datetime.now() - 启动时间 @@ -6922,10 +8243,58 @@ def 测试_指标挂载(配置: 缠论配置): return 魔法 +def 测试_信号识别(配置: 缠论配置): + 文件路径 = 配置.加载文件路径 + name = Path(文件路径).name.split(".")[0] + 符号, 周期, 起始时间戳, 结束时间戳 = name.split("-") + 周期 = int(周期) + 分析器 = 立体分析器(符号, [周期, 周期 * 5, 周期 * 5 * 6], 配置) + 信号配置 = [] + for p in [周期, 周期 * 5, 周期 * 5 * 6]: + 信号配置.extend( + get_signals_config( + [ + f"{str(p)}_D1#MACD#13#33#11_MACD交叉V260601_金叉_任意_任意_0", + ], + "signals", + ) + ) + 计算器 = 信号计算器(分析器, 信号配置, "signals") + + def 魔法(): + 启动时间 = datetime.now() + 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], 周期, 符号) + 分析器.投喂K线(k线) + + 计算器.更新() + + # 输出信号内容 + if 计算器.信号: + dt = k线.时间戳 + if isinstance(dt, (int, float)): + dt = datetime.fromtimestamp(dt) + 信号摘要 = " ".join(f"{k}→{v}" for k, v in 计算器.信号.items()) + 信号类型 = ",".join(sorted(set(v.split("_")[0] for v in 计算器.信号.values()))) + logger.info(f"[{dt}] [{信号类型}] 📡 {信号摘要}") + print(f"[{dt}] [{信号类型}] 📡 {信号摘要}, {计算器.信号}") + print() + + 消耗用时 = datetime.now() - 启动时间 + logger.info(f"测试_信号识别 {消耗用时} 普K数量 {len(分析器._单体分析器[周期].普通K线序列)}") + return 分析器 + + return 魔法 + + if __name__ == "__main__": 当前配置 = 缠论配置.不推送() 当前配置.加载文件路径 = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "tests", "btcusd-300-1761327300-1776327900.nb") with tempfile.TemporaryDirectory() as tmpdir: # 测试_读取数据(观察者("", 0, 当前配置), 当前配置)().测试_保存数据(tmpdir) # 测试_周期合成(当前配置)().测试_保存数据(tmpdir) - 测试_指标挂载(当前配置)().测试_保存数据(tmpdir) + # 测试_指标挂载(当前配置)().测试_保存数据(tmpdir) + 测试_信号识别(当前配置)() diff --git a/chanlun-py/chanlun/chan_external.py b/chanlun-py/chanlun/chan_external.py index 20a62a0..edc7a22 100644 --- a/chanlun-py/chanlun/chan_external.py +++ b/chanlun-py/chanlun/chan_external.py @@ -45,6 +45,7 @@ # ============================================================================== import hashlib import re +import sys from collections import OrderedDict from dataclasses import dataclass, field from datetime import datetime, timedelta @@ -54,33 +55,23 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Union import numpy as np from loguru import logger -from chanlun.chan import K线, 虚线, 中枢, 观察者, 立体分析器 +from chanlun import K线, 虚线, 中枢, 观察者, 立体分析器 from chanlun.parse import parse -sorted_freqs = [ - "Tick", - "1分钟", - "2分钟", - "3分钟", - "4分钟", - "5分钟", - "6分钟", - "10分钟", - "12分钟", - "15分钟", - "20分钟", - "30分钟", - "60分钟", - "120分钟", - "日线", - "周线", - "月线", - "季线", - "年线", -] +# 信号匹配原语已移植到 Rust 核心层(chanlun._chanlun)。 +# Operate/Signal/Factor/Event/Position 改为从 Rust 导入;Position 在下方扩展为子类补 update 状态机。 +from chanlun.signal_orchestrator import SignalOrchestrator + +from chanlun._chanlun import ( + Signal, + Factor, + Event, + Operate, + Position as _PositionBase, +) -def import_by_name(name): +def import_by_name(name: str): """通过字符串导入模块、类、函数 函数执行逻辑: @@ -106,363 +97,6 @@ def import_by_name(name): return vars(module)[function_name] -class Freq(Enum): - Tick = "Tick" - F1 = "1分钟" - F2 = "2分钟" - F3 = "3分钟" - F4 = "4分钟" - F5 = "5分钟" - F6 = "6分钟" - F10 = "10分钟" - F12 = "12分钟" - F15 = "15分钟" - F20 = "20分钟" - F30 = "30分钟" - F60 = "60分钟" - F120 = "120分钟" - D = "日线" - W = "周线" - M = "月线" - S = "季线" - Y = "年线" - - def __str__(self): - return self.value - - -class Operate(Enum): - # 持有状态 - HL = "持多" # Hold Long - HS = "持空" # Hold Short - HO = "持币" # Hold Other - - # 多头操作 - LO = "开多" # Long Open - LE = "平多" # Long Exit - - # 空头操作 - SO = "开空" # Short Open - SE = "平空" # Short Exit - - def __str__(self): - return self.value - - -@dataclass -class Signal: - signal: str = "" - - # score 取值在 0~100 之间,得分越高,信号越强 - score: int = 0 - - # k1, k2, k3 是信号名称 - k1: str = "任意" # k1 一般是指明信号计算的K线周期,如 60分钟,日线,周线等 - k2: str = "任意" # k2 一般是记录信号计算的参数 - k3: str = "任意" # k3 用于区分信号,必须具有唯一性,推荐使用信号分类和开发日期进行标记 - - # v1, v2, v3 是信号取值 - v1: str = "任意" - v2: str = "任意" - v3: str = "任意" - - # 任意 出现在模板信号中可以指代任何值 - - def __post_init__(self): - if not self.signal: - self.signal = f"{self.k1}_{self.k2}_{self.k3}_{self.v1}_{self.v2}_{self.v3}_{self.score}" - else: - if not isinstance(self.signal, str): - raise TypeError(f"Signal 初始化需要字符串,收到了 {type(self.signal).__name__}: {self.signal!r}") - ( - self.k1, - self.k2, - self.k3, - self.v1, - self.v2, - self.v3, - score, - ) = self.signal.split("_") - self.score = int(score) - - if self.score > 100 or self.score < 0: - raise ValueError("score 必须在0~100之间") - - def __repr__(self): - return f"Signal('{self.signal}')" - - @property - def key(self) -> str: - """获取信号名称""" - key = "" - for k in [self.k1, self.k2, self.k3]: - if k != "任意": - key += k + "_" - return key.strip("_") - - @property - def value(self) -> str: - """获取信号值""" - return f"{self.v1}_{self.v2}_{self.v3}_{self.score}" - - def is_match(self, s: dict) -> bool: - """判断信号是否与信号列表中的值匹配 - - 代码的执行逻辑如下: - - 接收一个字典 s 作为参数,该字典包含了所有信号的信息。从字典 s 中获取名称为 key 的信号的值 v。 - 如果 v 不存在,则抛出异常。从信号的值 v 中解析出 v1、v2、v3 和 score 四个变量。 - - 如果当前信号的得分 score 大于等于目标信号的得分 self.score,则继续执行,否则返回 False。 - 如果当前信号的第一个值 v1 等于目标信号的第一个值 self.v1 或者目标信号的第一个值为 "任意",则继续执行,否则返回 False。 - 如果当前信号的第二个值 v2 等于目标信号的第二个值 self.v2 或者目标信号的第二个值为 "任意",则继续执行,否则返回 False。 - 如果当前信号的第三个值 v3 等于目标信号的第三个值 self.v3 或者目标信号的第三个值为 "任意",则返回 True,否则返回 False。 - - :param s: 所有信号字典 - :return: bool - """ - key = self.key - v = s.get(key, None) - if not v: - raise ValueError(f"{key} 不在信号列表中") - - if not isinstance(v, str): - logger.warning(f"信号 {key} 的值类型异常: {type(v).__name__} = {v!r},跳过匹配") - return False - - v1, v2, v3, score = v.split("_") - if int(score) >= self.score: - if v1 == self.v1 or self.v1 == "任意": - if v2 == self.v2 or self.v2 == "任意": - if v3 == self.v3 or self.v3 == "任意": - return True - return False - - -@dataclass -class Factor: - # signals_all 必须全部满足的信号,至少需要设定一个信号 - signals_all: List[Signal] - - # signals_any 满足其中任一信号,允许为空 - signals_any: List[Signal] = field(default_factory=list) - - # signals_not 不能满足其中任一信号,允许为空 - signals_not: List[Signal] = field(default_factory=list) - - name: str = "" - - def __post_init__(self): - if not self.signals_all: - raise ValueError("signals_all 不能为空") - _fatcor = self.dump() - _fatcor.pop("name") - sha256 = hashlib.sha256(str(_fatcor).encode("utf-8")).hexdigest().upper()[:4] - - if self.name: - self.name = self.name.split("#")[0] + f"#{sha256}" - else: - self.name = f"#{sha256}" - # self.name = f"{self.name}#{sha256}" if self.name else sha256 - - @property - def unique_signals(self) -> List[str]: - """获取 Factor 的唯一信号列表""" - signals = [] - signals.extend(self.signals_all) - if self.signals_any: - signals.extend(self.signals_any) - if self.signals_not: - signals.extend(self.signals_not) - signals = {x.signal if isinstance(x, Signal) else x for x in signals} - return list(signals) - - def is_match(self, s: dict) -> bool: - """判断 factor 是否满足""" - if self.signals_not: - for signal in self.signals_not: - if signal.is_match(s): - return False - - for signal in self.signals_all: - if not signal.is_match(s): - return False - - if not self.signals_any: - return True - - for signal in self.signals_any: - if signal.is_match(s): - return True - return False - - def dump(self) -> dict: - """将 Factor 对象转存为 dict""" - signals_all = [x.signal for x in self.signals_all] - signals_any = [x.signal for x in self.signals_any] if self.signals_any else [] - signals_not = [x.signal for x in self.signals_not] if self.signals_not else [] - - raw = { - "name": self.name, - "signals_all": signals_all, - "signals_any": signals_any, - "signals_not": signals_not, - } - return raw - - @classmethod - def load(cls, raw: dict): - """从 dict 中创建 Factor - - :param raw: 样例如下 - {'name': '单测', - 'signals_all': ['15分钟_倒0笔_方向_向上_其他_其他_0', '15分钟_倒0笔_长度_大于5_其他_其他_0'], - 'signals_any': [], - 'signals_not': []} - - :return: - """ - signals_any = [Signal(x) for x in raw.get("signals_any", [])] - signals_not = [Signal(x) for x in raw.get("signals_not", [])] - - fa = Factor( - name=raw.get("name", ""), - signals_all=[Signal(x) for x in raw["signals_all"]], - signals_any=signals_any, - signals_not=signals_not, - ) - return fa - - -@dataclass -class Event: - operate: Operate - - # 多个信号组成一个因子,多个因子组成一个事件。 - # 单个事件是一系列同类型因子的集合,事件中的任一因子满足,则事件为真。 - factors: List[Factor] - - # signals_all 必须全部满足的信号,允许为空 - signals_all: List[Signal] = field(default_factory=list) - - # signals_any 满足其中任一信号,允许为空 - signals_any: List[Signal] = field(default_factory=list) - - # signals_not 不能满足其中任一信号,允许为空 - signals_not: List[Signal] = field(default_factory=list) - - name: str = "" - - def __post_init__(self): - if not self.factors: - raise ValueError("factors 不能为空") - _event = self.dump() - _event.pop("name") - - sha256 = hashlib.sha256(str(_event).encode("utf-8")).hexdigest().upper()[:4] - if self.name: - self.name = self.name.split("#")[0] + f"#{sha256}" - # self.name = f"{self.name}#{sha256}" - else: - self.name = f"{self.operate.value}#{sha256}" - self.sha256 = sha256 - - @property - def unique_signals(self) -> List[str]: - """获取 Event 的唯一信号列表""" - signals = [] - if self.signals_all: - signals.extend(self.signals_all) - if self.signals_any: - signals.extend(self.signals_any) - if self.signals_not: - signals.extend(self.signals_not) - - for factor in self.factors: - signals.extend(factor.unique_signals) - - signals = {x.signal if isinstance(x, Signal) else x for x in signals} - return list(signals) - - def get_signals_config(self, signals_module: str = "chanlun.signals") -> List[Dict]: - """获取事件的信号配置""" - - return get_signals_config(self.unique_signals, signals_module) - - def is_match(self, s: dict): - """判断 event 是否满足 - - 代码的执行逻辑如下: - - 1. 首先判断 signals_not 中的信号是否得到满足,如果满足任意一个信号,则直接返回 False,表示事件不满足。 - 2. 接着判断 signals_all 中的信号是否全部得到满足,如果有任意一个信号不满足,则直接返回 False,表示事件不满足。 - 3. 然后判断 signals_any 中的信号是否有一个得到满足,如果一个都不满足,则直接返回 False,表示事件不满足。 - 4. 最后判断因子是否满足,顺序遍历因子列表,找到第一个满足的因子就退出,并返回 True 和该因子的名称,表示事件满足。 - 5. 如果遍历完所有因子都没有找到满足的因子,则返回 False,表示事件不满足。 - """ - if self.signals_not and any(signal.is_match(s) for signal in self.signals_not): - return False, None - - if self.signals_all and not all(signal.is_match(s) for signal in self.signals_all): - return False, None - - if self.signals_any and not any(signal.is_match(s) for signal in self.signals_any): - return False, None - - for factor in self.factors: - if factor.is_match(s): - return True, factor.name - - return False, None - - def dump(self) -> dict: - """将 Event 对象转存为 dict""" - signals_all = [x.signal for x in self.signals_all] if self.signals_all else [] - signals_any = [x.signal for x in self.signals_any] if self.signals_any else [] - signals_not = [x.signal for x in self.signals_not] if self.signals_not else [] - factors = [x.dump() for x in self.factors] - - raw = { - "name": self.name, - "operate": self.operate.value, - "signals_all": signals_all, - "signals_any": signals_any, - "signals_not": signals_not, - "factors": factors, - } - return raw - - @classmethod - def load(cls, raw: dict): - """从 dict 中创建 Event - - :param raw: 样例如下 - {'name': '单测', - 'operate': '开多', - 'factors': [{'name': '测试', - 'signals_all': ['15分钟_倒0笔_长度_大于5_其他_其他_0'], - 'signals_any': [], - 'signals_not': []}], - 'signals_all': ['15分钟_倒0笔_方向_向上_其他_其他_0'], - 'signals_any': [], - 'signals_not': []} - :return: - """ - # 检查输入参数是否合法 - assert raw["operate"] in Operate.__dict__["_value2member_map_"], f"operate {raw['operate']} not in Operate" - assert raw["factors"], "factors can not be empty" - - e = Event( - name=raw.get("name", ""), - operate=Operate.__dict__["_value2member_map_"][raw["operate"]], - factors=[Factor.load(x) for x in raw["factors"]], - signals_all=[Signal(x) for x in raw.get("signals_all", [])], - signals_any=[Signal(x) for x in raw.get("signals_any", [])], - signals_not=[Signal(x) for x in raw.get("signals_not", [])], - ) - return e - - class SignalsParser: """解析一串信号,生成信号函数配置""" @@ -509,11 +143,27 @@ class SignalsParser: if 触发匹配: sig_trigger_map[name] = [x.strip() for x in 触发匹配[0].split(",")] - except Exception as e: + except (OSError, ImportError, TypeError, ValueError, AttributeError) as e: logger.error(f"解析信号函数 {name} 出错:{e}") + # 为每个 k3 生成独立 pattern(支持单函数多 k3 信号,如 youwukuncheng 的 3 个 k3)。 + # base pattern 末段是 k3,按 sig_name_map 里各 Signal 的 k3 逐一替换。 + _multi_pats: Dict[str, List[str]] = {} + for _name, _base in sig_pats_map.items(): + _sigs = sig_name_map.get(_name, []) + if _sigs: + _prefix = _base.rsplit("_", 1)[0] if "_" in _base else _base + _pats: List[str] = [] + for _s in _sigs: + _p = f"{_prefix}_{_s.k3}" + if _p not in _pats: + _pats.append(_p) + _multi_pats[_name] = _pats + else: + _multi_pats[_name] = [_base] + self.sig_name_map = sig_name_map - self.sig_pats_map = sig_pats_map + self.sig_pats_map = _multi_pats # name → List[pattern](每个 k3 一个) self.sig_trigger_map = sig_trigger_map def parse_params(self, name, signal): @@ -531,12 +181,18 @@ class SignalsParser: :return: """ key = Signal(signal).key - pats = self.sig_pats_map.get(name, None) - if not pats: + pats_list = self.sig_pats_map.get(name, None) + if not pats_list: return None - try: - params = parse(pats, key).named # type: ignore + for pats in pats_list: + try: + parsed = parse(pats, key) + except (ValueError, KeyError, TypeError, AttributeError): + continue + if parsed is None: + continue + params = parsed.named if "di" in params: params["di"] = int(params["di"]) @@ -549,9 +205,9 @@ class SignalsParser: params["_func_short_name"] = name return params - except Exception as e: - logger.error(f"解析信号 {signal} - {name} - {pats} 出错:{e}") - return None + + logger.error(f"解析信号 {signal} - {name} 出错:无匹配模式 {pats_list}") + return None def get_function_name(self, signal: str): """获取信号对应的信号函数名称 @@ -567,7 +223,13 @@ class SignalsParser: """ sig_name_map = self.sig_name_map _signal = Signal(signal) - _k3_match = list({k for k, v in sig_name_map.items() if v[0].k3 == _signal.k3}) + _k3_match = list({k for k, v in sig_name_map.items() for s in v if s.k3 == _signal.k3}) + + # 多匹配时排除模板函数(以 "模板_" 开头) + if len(_k3_match) > 1: + non_template = [k for k in _k3_match if not k.startswith("模板_")] + if len(non_template) == 1: + return non_template[0] if len(_k3_match) == 1: return _k3_match[0] @@ -596,7 +258,8 @@ class SignalsParser: for conf in config: name = conf["name"].split(".")[-1] if name in self.sig_pats_map: - keys.append(self.sig_pats_map[name].format(**conf)) + for pats in self.sig_pats_map[name]: + keys.append(pats.format(**conf)) return keys def parse(self, signal_seq: List[str]): @@ -628,7 +291,7 @@ class SignalsParser: return res -def get_signals_config(signals_seq: List[str], signals_module: str = "czsc.signals") -> List[Dict]: +def get_signals_config(signals_seq: List[str], signals_module: str = "") -> List[Dict]: """获取信号列表对应的信号函数配置 函数执行逻辑: @@ -646,29 +309,6 @@ def get_signals_config(signals_seq: List[str], signals_module: str = "czsc.signa return conf -def get_signals_freqs(signals_seq: List) -> List[str]: - """获取信号列表对应的K线周期列表 - - 函数执行逻辑: - - 1. 然后对于 signals_seq 中的每个信号进行以下操作: - - - 使用正则表达式从信号中提取信号周期,并将其存储在 _freqs 变量中。 - - 如果提取到了信号周期,则将其加入到 freqs 列表中。 - - 2. 最后验证数据是否符合sorted_freqs列表规范,并且以sorted_freqs列表的排序进行返回。 - - :param signals_seq: 信号列表 / 信号函数配置列表 - :return: K线周期列表 - """ - freqs = [] - for signal in signals_seq: - _freqs = re.findall("|".join(sorted_freqs), str(signal)) - if _freqs: - freqs.extend(_freqs) - return [x for x in sorted_freqs if x in freqs] - - def create_single_signal(**kwargs) -> OrderedDict: """创建单个信号""" s = OrderedDict() @@ -679,387 +319,64 @@ def create_single_signal(**kwargs) -> OrderedDict: return s -def get_sub_elements(elements: List[Any], di: int = 1, n: int = 10) -> List[Any]: - """获取截止到倒数第 di 个元素的前 n 个元素 - - 信号函数中广泛使用的子序列截取工具。 - - :param elements: 全部元素列表 - :param di: 指定结束元素为倒数第 di 个,di >= 1 - :param n: 指定需要的元素个数 - :return: 部分元素列表 - - >>> x = [1, 2, 3, 4, 5, 6, 7, 8, 9] - >>> get_sub_elements(x, di=1, n=3) - [7, 8, 9] - >>> get_sub_elements(x, di=2, n=3) - [6, 7, 8] - """ - assert di >= 1 - if di == 1: - return elements[-n:] - else: - return elements[-n - di + 1 : -di + 1] - - -# ============================================================================== -# 周期映射工具 -# ============================================================================== - -# Freq 枚举字符串 → int 秒数 -_FREQ_TO_SECONDS: Dict[str, int] = { - "Tick": 0, - "1分钟": 60, - "2分钟": 120, - "3分钟": 180, - "4分钟": 240, - "5分钟": 300, - "6分钟": 360, - "10分钟": 600, - "12分钟": 720, - "15分钟": 900, - "20分钟": 1200, - "30分钟": 1800, - "60分钟": 3600, - "120分钟": 7200, - "日线": 86400, - "周线": 604800, - "月线": 2592000, - "季线": 7776000, - "年线": 31536000, -} - -# int 秒数 → Freq 枚举字符串(反向查) -_SECONDS_TO_FREQ: Dict[int, str] = {v: k for k, v in _FREQ_TO_SECONDS.items() if v > 0} - - -def _freq_str_to_seconds(freq: str) -> int: - """Freq 枚举字符串 → int 秒数""" - return _FREQ_TO_SECONDS.get(freq, 0) - - -def _seconds_to_freq_str(seconds: int) -> str: - """int 秒数 → Freq 枚举字符串""" - return _SECONDS_TO_FREQ.get(seconds, f"{seconds}秒") - - # ============================================================================== # Position — 持仓管理 # ============================================================================== -class Position: - def __init__( - self, - symbol: str, - opens: List[Event], - exits: List[Event] = [], - interval: int = 0, - timeout: int = 1000, - stop_loss=1000, - T0: bool = False, - name=None, - ): - """简单持仓对象,仓位表达:1 持有多头,-1 持有空头,0 空仓 +class Position(_PositionBase): + """持仓对象 — 配置 + 状态机均已迁移到 Rust 核心。 - :param symbol: 标的代码 - :param opens: 开仓交易事件列表 - :param exits: 平仓交易事件列表,允许为空 - :param interval: 同类型开仓间隔时间,单位:秒;默认值为 0,表示同类型开仓间隔没有约束 - 假设上次开仓为多头,那么下一次多头开仓时间必须大于 上次开仓时间 + interval;空头也是如此。 - :param timeout: 最大允许持仓K线数量限制为最近一个开仓事件触发后的 timeout 根基础周期K线 - :param stop_loss: 最大允许亏损比例,单位:BP, 1BP = 0.01%;成本的计算以最近一个开仓事件触发价格为准 - :param T0: 是否允许T0交易,默认为 False 表示不允许T0交易 - :param name: 仓位名称,默认值为第一个开仓事件的名称 - """ - assert name, "name 是必须的参数" - self.symbol = symbol - self.opens = opens - self.name = name - self.exits = exits if exits else [] - self.events = self.opens + self.exits - for event in self.events: - assert event.operate in [Operate.LO, Operate.LE, Operate.SO, Operate.SE] + 仓位表达:1 持有多头,-1 持有空头,0 空仓。 - self.interval = interval - self.timeout = timeout - self.stop_loss = stop_loss - self.T0 = T0 + Rust 基类(chanlun._chanlun.Position)提供: + - 配置字段 symbol/opens/exits/events/name/interval/timeout/stop_loss/T0(只读 getter) + - 状态字段 pos/pos_changed/operates/holds/pairs(只读 getter) + - update(信号字典) — 持仓状态机 + - dump(with_data) — 序列化(含可选状态) + - load(raw) — 反序列化(静态方法) + - unique_signals、__repr__ + 本子类仅保留 get_signals_config(需 Python signals_module)。 + """ - self.pos_changed = False # 仓位是否发生变化 - self.operates = [] # 事件触发的操作列表 - self.holds = [] # 持仓状态列表 - self.pos = 0 + def __init__(self, *args, **kwargs): + # 状态字段已由 Rust #[new] 初始化;无需 Python 侧初始化。 + # 不调用 super().__init__():PyO3 #[new] 已在 __new__ 阶段建好内部配置。 + pass - # 辅助判断的缓存数据 - self.last_event = { - "dt": None, - "bid": None, - "price": None, - "op": None, - "op_desc": None, - } - self.last_lo_dt = None # 最近一次开多交易的时间 - self.last_so_dt = None # 最近一次开空交易的时间 - self.end_dt = None # 最近一次信号传入的时间 - - def __repr__(self): - return f"Position(name={self.name}, symbol={self.symbol}, opens={[x.name for x in self.opens]}, timeout={self.timeout}, stop_loss={self.stop_loss}BP, T0={self.T0}, interval={self.interval}s)" - - @property - def unique_signals(self) -> List[str]: - """获取所有事件的唯一信号列表""" - signals = [] - for e in self.events: - signals.extend(e.unique_signals) - return list(set(signals)) - - def get_signals_config(self, signals_module: str = "chanlun.signals") -> List[Dict]: + def get_signals_config(self, signals_module: str = "") -> List[Dict]: """获取事件的信号配置""" return get_signals_config(self.unique_signals, signals_module) def dump(self, with_data: bool = False) -> dict: - """将对象转换为 dict""" - raw = { - "symbol": self.symbol, - "name": self.name, - "opens": [x.dump() for x in self.opens], - "exits": [x.dump() for x in self.exits], - "interval": self.interval, - "timeout": self.timeout, - "stop_loss": self.stop_loss, - "T0": self.T0, - } - if with_data: - raw.update({"pairs": self.pairs, "holds": self.holds}) - return raw + """序列化为 dict。Rust 基类 dump(with_data) 处理配置 + 可选状态。""" + return super().dump(with_data=with_data) @classmethod def load(cls, raw: dict) -> "Position": - """从 dict 中创建 Position - :param raw: 样例如下 - :return: - """ - pos = Position( - name=raw["name"], + """从 dict 反序列化为 Position(子类实例);opens/exits 用 Rust Event.load 还原。""" + return cls( symbol=raw["symbol"], - opens=[Event.load(x) for x in raw["opens"] if raw.get("opens")], - exits=[Event.load(x) for x in raw["exits"] if raw.get("exits")], + name=raw["name"], + opens=[Event.load(x) for x in raw.get("opens", [])], + exits=[Event.load(x) for x in raw.get("exits", [])], interval=raw["interval"], timeout=raw["timeout"], stop_loss=raw["stop_loss"], T0=raw["T0"], ) - return pos - - @property - def pairs(self) -> List[Dict]: - """开平交易列表 - - 返回样例: - - [{'标的代码': '000001.SH', - '交易方向': '多头', - '开仓时间': Timestamp('2020-04-17 00:00:00'), - '平仓时间': Timestamp('2020-04-20 00:00:00'), - '开仓价格': 2838.49, - '平仓价格': 2852.55, - '持仓K线数': 1, - '事件序列': '开多@站上SMA5 -> 开多@站上SMA5', - '持仓天数': 3.0, - '盈亏比例': 49.53}, - {'标的代码': '000001.SH', - '交易方向': '多头', - '开仓时间': Timestamp('2020-04-20 00:00:00'), - '平仓时间': Timestamp('2020-04-24 00:00:00'), - '开仓价格': 2852.55, - '平仓价格': 2808.53, - '持仓K线数': 4, - '事件序列': '开多@站上SMA5 -> 平多@100BP止损', - '持仓天数': 4.0, - '盈亏比例': -154.32}] - - 数据说明: - - 1. 盈亏比例,单位是 BP - 2. 持仓天数,单位是 自然日 - 3. 持仓K线数,指基础周期K线数量 - """ - pairs = [] - - for op1, op2 in zip(self.operates, self.operates[1:]): - if op1["op"] not in [Operate.LO, Operate.SO]: - continue - - ykr = op2["price"] / op1["price"] - 1 if op1["op"] == Operate.LO else 1 - op2["price"] / op1["price"] - pair = { - "标的代码": self.symbol, - "策略标记": self.name, - "交易方向": "多头" if op1["op"] == Operate.LO else "空头", - "开仓时间": op1["dt"], - "平仓时间": op2["dt"], - "开仓价格": op1["price"], - "平仓价格": op2["price"], - "持仓K线数": op2["bid"] - op1["bid"], - "事件序列": f"{op1['op_desc']} -> {op2['op_desc']}", - "持仓天数": (op2["dt"] - op1["dt"]).total_seconds() / (24 * 3600), - "盈亏比例": round(ykr * 10000, 2), # 盈亏比例 转换成以 BP 为单位的收益,1BP = 0.0001 - } - pairs.append(pair) - - return pairs - - def update(self, s: dict): - """更新持仓状态 - - 函数执行逻辑: - - - 首先,检查最新信号的时间是否在上次信号之前,如果是则打印警告信息并返回。 - - 初始化一些变量,包括操作类型(op)和操作描述(op_desc)。 - - 遍历所有的事件,检查是否与最新信号匹配。如果匹配,则记录操作类型和操作描述,并跳出循环。 - - 提取最新信号的相关信息,包括交易对符号、时间、价格和成交量。 - - 更新持仓状态的结束时间为最新信号的时间。 - - 如果操作类型是开仓(LO或SO),更新最后一个事件的信息。 - - 定义一个内部函数__create_operate,用于创建操作记录。 - - 根据操作类型更新仓位和操作记录。 - - - 如果操作类型是LO(开多),检查是否满足开仓条件,如果满足则开多仓,否则只平空仓。 - - 如果操作类型是SO(开空),检查是否满足开仓条件,如果满足则开空仓,否则只平多仓。 - - 如果当前持仓为多仓,进行多头出场的判断: - - 如果操作类型是LE(平多),平多仓。 - - 如果当前价格相对于最后一个事件的价格的收益率小于止损阈值,平多仓。 - - 如果当前成交量相对于最后一个事件的成交量的增加量大于超时阈值,平多仓。 - - - 如果当前持仓为空仓,进行空头出场的判断: - - 如果操作类型是SE(平空),平空仓。 - - 如果当前价格相对于最后一个事件的价格的收益率小于止损阈值,平空仓。 - - 如果当前成交量相对于最后一个事件的成交量的增加量大于超时阈值,平空仓。 - - - 将当前持仓状态和价格记录到持仓列表中。 - - :param s: 最新信号字典 - :return: - """ - if self.end_dt and s["dt"] <= self.end_dt: - logger.warning(f"请检查信号传入:最新信号时间{s['dt']}在上次信号时间{self.end_dt}之前") - return - - self.pos_changed = False - op = Operate.HO - op_desc = "" - for event in self.events: - m, f = event.is_match(s) - if m: - op = event.operate - op_desc = f"{event.name}@{f}" - break - - symbol = s["symbol"] - dt = s["dt"] - price = s["close"] - bid = s.get("id", s.get("bid", 0)) - self.end_dt = dt - - # 当有新的开仓 event 发生,更新 last_event - if op in [Operate.LO, Operate.SO]: - self.last_event = { - "dt": dt, - "bid": bid, - "price": price, - "op": op, - "op_desc": op_desc, - } - - def __create_operate(_op, _op_desc): - self.pos_changed = True - return { - "symbol": symbol, - "dt": dt, - "bid": bid, - "price": price, - "op": _op, - "op_desc": _op_desc, - "pos": self.pos, - } - - # 更新仓位 - if op == Operate.LO: - if self.pos != 1 and (not self.last_lo_dt or (dt - self.last_lo_dt).total_seconds() > self.interval): - # 与前一次开多间隔时间大于 interval,直接开多 - self.pos = 1 - self.operates.append(__create_operate(Operate.LO, op_desc)) - self.last_lo_dt = dt - else: - # 与前一次开多间隔时间小于 interval,仅对空头平仓 - if self.pos == -1 and (self.T0 or dt.date() != self.last_so_dt.date()): - self.pos = 0 - self.operates.append(__create_operate(Operate.SE, op_desc)) - - if op == Operate.SO: - if self.pos != -1 and (not self.last_so_dt or (dt - self.last_so_dt).total_seconds() > self.interval): - # 与前一次开空间隔时间大于 interval,直接开空 - self.pos = -1 - self.operates.append(__create_operate(Operate.SO, op_desc)) - self.last_so_dt = dt - else: - # 与前一次开空间隔时间小于 interval,仅对多头平仓 - if self.pos == 1 and (self.T0 or dt.date() != self.last_lo_dt.date()): - self.pos = 0 - self.operates.append(__create_operate(Operate.LE, op_desc)) - - # 多头出场 - if self.pos == 1 and (self.T0 or dt.date() != self.last_lo_dt.date()): - assert self.last_event["dt"] >= self.last_lo_dt - - # 多头平仓 - if op == Operate.LE: - self.pos = 0 - self.operates.append(__create_operate(Operate.LE, op_desc)) - - # 多头止损 - if price / self.last_event["price"] - 1 < -self.stop_loss / 10000: - self.pos = 0 - self.operates.append(__create_operate(Operate.LE, f"平多@{self.stop_loss}BP止损")) - - # 多头超时 - if bid - self.last_event["bid"] > self.timeout: - self.pos = 0 - self.operates.append(__create_operate(Operate.LE, f"平多@{self.timeout}K超时")) - - # 空头出场 - if self.pos == -1 and (self.T0 or dt.date() != self.last_so_dt.date()): - assert self.last_event["dt"] >= self.last_so_dt - - # 空头平仓 - if op == Operate.SE: - self.pos = 0 - self.operates.append(__create_operate(Operate.SE, op_desc)) - - # 空头止损 - if 1 - price / self.last_event["price"] < -self.stop_loss / 10000: - self.pos = 0 - self.operates.append(__create_operate(Operate.SE, f"平空@{self.stop_loss}BP止损")) - - # 空头超时 - if bid - self.last_event["bid"] > self.timeout: - self.pos = 0 - self.operates.append(__create_operate(Operate.SE, f"平空@{self.timeout}K超时")) - - self.holds.append({"dt": self.end_dt, "pos": self.pos, "price": price}) - - -# ============================================================================== -# 信号计算器 — 多周期信号计算引擎 -# ============================================================================== class 信号计算器: - """多周期信号计算器 — 基于立体分析器的信号计算引擎。 + """多周期信号计算引擎 — 基于观察者字典。 + + 不再依赖 立体分析器,直接接收 ``{周期秒: 观察者}`` 字典。 使用方式:: 分析器 = 立体分析器("btcusd", [300, 900, 3600], 配置) - 计算器 = 信号计算器(分析器, 信号配置=[...]) + 观察者字典 = {p: 分析器._单体分析器[p] for p in 分析器.周期组} + 计算器 = 信号计算器(观察者字典, 基础周期=300, 信号配置=[...]) for k in k线列表: 分析器.投喂K线(k) @@ -1071,27 +388,21 @@ class 信号计算器: self, 分析器: 立体分析器, 信号配置: Optional[List[Dict]] = None, - 信号模块: str = "chanlun.signals", + 信号模块: str = "", ): - """ - :param 分析器: 立体分析器实例(已完成多周期缠论分析) - :param 信号配置: 信号函数配置列表,格式: - [{'name': 'chanlun.signals.tas_ma_base_V230313', 'freq': '日线', 'di': 1, ...}] - 若为 None,可后续通过 setter 或从 Event/Position 自动提取 - :param 信号模块: 信号函数所在模块 - """ self._分析器 = 分析器 + self._观察者字典 = {p: 分析器._单体分析器[p] for p in 分析器.周期组} + self._基础周期 = 分析器.周期组[0] self._信号模块 = 信号模块 - self._信号配置: List[Dict] = 信号配置 or [] + self._信号函数缓存: Dict[str, Callable] = {} + self.信号: dict = {} + self.行情: dict = {} + self.信号配置 = 信号配置 or [] + self._自动挂载指标() - # 信号字典:{key: value},key = "k1_k2_k3",value = "v1_v2_v3_score" - self.信号字典: OrderedDict = OrderedDict() - - # 优化:脏标记触发 + 缓存键去重 - # _上次状态: {周期秒: {"分型": n, "笔": n, "线段": n, "中枢": n, "K线": n}} - self._上次状态: Dict[int, Dict[str, int]] = {} - # _结果缓存: {config_index: (cache_key, result_OrderedDict)} - self._结果缓存: Dict[int, tuple] = {} + @property + def 信号字典(self) -> dict: # 向后兼容:合并返回 + return {**self.信号, **self.行情} @property def 信号配置(self) -> List[Dict]: @@ -1099,1188 +410,166 @@ class 信号计算器: @信号配置.setter def 信号配置(self, value: List[Dict]): - self._信号配置 = value + 可用周期 = set(self._分析器.周期组) + for c in value: + freq = c.get("freq") + if freq is not None: + 周期秒 = int(freq) + if 周期秒 not in 可用周期: + raise ValueError(f"信号配置 freq={freq}({周期秒}s) 不在分析器周期组 {sorted(可用周期)} 中\n 信号: {c.get('name', '?')}") + self._信号配置 = self._去重配置(value) + self._预加载信号函数() - def 从信号序列设置配置(self, 信号序列: List[str]): - """从信号序列自动生成信号配置(通过 SignalsParser 解析)""" - self._信号配置 = get_signals_config(信号序列, self._信号模块) + def _去重配置(self, configs: List[Dict]) -> List[Dict]: + seen = set() + unique = [] + for c in configs: + key = (c.get("name"), frozenset((k, str(v)) for k, v in c.items() if k != "name")) + if key not in seen: + seen.add(key) + unique.append(c) + else: + logger.warning(f"信号计算器: 重复信号配置已跳过 — {c.get('name', '?')} { {k: v for k, v in c.items() if k != 'name'} }") + return unique - def 从事件提取配置(self, events: List[Event]): - """从事件列表中提取所有唯一信号并生成配置""" - 所有信号: List[str] = [] - for e in events: - 所有信号.extend(e.unique_signals) - 所有信号 = list(set(所有信号)) - if 所有信号: - self._信号配置 = get_signals_config(所有信号, self._信号模块) + def _预加载信号函数(self): + for config in self._信号配置: + name = config.get("name") + if name and name not in self._信号函数缓存: + try: + self._信号函数缓存[name] = self._解析信号函数(name) + except (ImportError, ModuleNotFoundError, AttributeError, KeyError) as e: + logger.warning(f"信号计算器: 无法导入 {name} ({e}),跳过") - def 从持仓提取配置(self, positions: List[Position]): - """从持仓列表中提取所有唯一信号并生成配置""" - 所有信号: List[str] = [] - for p in positions: - 所有信号.extend(p.unique_signals) - 所有信号 = list(set(所有信号)) - if 所有信号: - self._信号配置 = get_signals_config(所有信号, self._信号模块) + @staticmethod + def _解析信号函数(name: str): + """解析信号函数名,返回可调用对象。 - def 更新(self) -> OrderedDict: - """遍历信号配置,调用信号函数,汇总所有周期信号。 - - 应在每次 立体分析器.投喂K线() 之后调用。 - - 优化机制: - 1. 脏标记触发 — 根据信号函数的"触发条件"声明,仅在相关序列变化时执行 - 2. 缓存键去重 — 相同序列状态下跳过重复计算 - - :return: 更新后的信号字典 + 当运行在 __main__ 上下文中且目标模块为 chan 时,优先使用 __main__ + 命名空间中的函数,避免 import_by_name 触发 chan 模块的重复导入。 """ - s = OrderedDict() + if "." in name: + module_name, func_name = name.rsplit(".", 1) + main_mod = sys.modules.get("__main__") + if main_mod is not None and hasattr(main_mod, func_name): + # 验证 __main__ 确实是目标模块(通过文件名判断) + main_file = getattr(main_mod, "__file__", "") + expected_path = module_name.replace(".", os.sep) + ".py" + if main_file.endswith(expected_path): + return getattr(main_mod, func_name) - # 计算各周期状态变化(脏标记) - 脏标记 = self._计算脏标记() + return import_by_name(name) - # 遍历信号配置,逐条执行 - for i, config in enumerate(self._信号配置): + def _自动挂载指标(self): + """根据信号配置参数,在对应周期的观察者上自动补全缺失的指标。""" + from collections import defaultdict + + 待补MACD: Dict[int, List[tuple]] = defaultdict(list) + 待补均线: Dict[int, List[tuple]] = defaultdict(list) + + for config in self._信号配置: + name = config.get("name", "") + freq = config.get("freq") + if not freq: + continue + 周期秒 = int(freq) + if 周期秒 not in self._观察者字典: + continue + + # MACD 类信号:从 config 解析 fast/slow/signal 参数 + if "macd" in name.lower() or "中枢" in name or "背驰" in name or "金叉" in name: + fast = int(config.get("fast", config.get("快线周期", 13))) + slow = int(config.get("slow", config.get("慢线周期", 31))) + sig = int(config.get("signal", config.get("信号周期", 11))) + key = f"macd_{fast}_{slow}_{sig}" + if not any(t[0] == key for t in 待补MACD[周期秒]): + 待补MACD[周期秒].append((key, "收", fast, slow, sig)) + + # MA 类信号:从 config 解析 ma_type/timeperiod + if "ma_" in name or "tas_ma" in name or "均线" in name: + ma_type = config.get("ma_type", "SMA").upper() + period = int(config.get("timeperiod", config.get("周期", 5))) + key = f"{ma_type}_{period}" + if not any(t[0] == key for t in 待补均线[周期秒]): + 待补均线[周期秒].append((key, "收", ma_type, period)) + + for 周期秒, macd_list in 待补MACD.items(): + cfg = self._观察者字典[周期秒].配置 + if not cfg.计算指标: + cfg.计算指标 = True + 已有键 = {t[0] for t in cfg.MACD_参数列表} + # 同时检查 (快线, 慢线, 信号) 参数避免只键名不同但参数相同的重复 + 已有参数 = {(t[2], t[3], t[4]) for t in cfg.MACD_参数列表 if len(t) >= 5} + 新增 = [t for t in macd_list if t[0] not in 已有键 and (t[2], t[3], t[4]) not in 已有参数] + if 新增: + cfg.MACD_参数列表.extend(新增) + if "macd" not in 已有键: + cfg.MACD_参数列表.insert(0, ("macd", "收", 新增[0][2], 新增[0][3], 新增[0][4])) + logger.warning(f"信号计算器: 周期{周期秒}s 自动补全 MACD — {[t[0] for t in 新增]}") + + for 周期秒, ma_list in 待补均线.items(): + cfg = self._观察者字典[周期秒].配置 + if not cfg.计算指标: + cfg.计算指标 = True + 已有 = {t[0] for t in cfg.均线参数列表} + 新增 = [t for t in ma_list if t[0] not in 已有] + if 新增: + cfg.均线参数列表.extend(新增) + logger.warning(f"信号计算器: 周期{周期秒}s 自动补全 均线 — {[t[0] for t in 新增]}") + + def 从信号列表提取配置(self, 信号序列: List[str]): + """从信号序列自动生成信号配置""" + self.信号配置 = get_signals_config(list(set(信号序列)), self._信号模块) + + def 更新(self): + """遍历信号配置,调用信号函数。结果写入 self.信号 和 self.行情。""" + self.信号.clear() + self.行情.clear() + + for config in self._信号配置: try: - # --- 缓存键去重:同状态下跳过 --- - 缓存键 = self._计算配置缓存键(i, config) - if 缓存键 is not None: - cached_key, cached_result = self._结果缓存.get(i, (None, None)) - if cached_key == 缓存键 and cached_result is not None: - s.update(cached_result) - continue - - # --- 脏标记触发:检查是否需要执行 --- - 触发条件 = config.get("触发条件") - if 触发条件: - freq = config.get("freq") - if freq is not None: - 周期秒 = _freq_str_to_seconds(freq) - freq_变化 = 脏标记.get(周期秒, set()) - if not (set(触发条件) & freq_变化): - # 触发条件不满足,若有缓存则复用 - cached_key, cached_result = self._结果缓存.get(i, (None, None)) - if cached_result is not None: - s.update(cached_result) - continue - result = self._执行信号函数(config) if result: - s.update(result) - if 缓存键 is not None: - self._结果缓存[i] = (缓存键, result) - except Exception as e: - logger.error(f"信号计算器: 执行 {config.get('name', '?')} 出错: {e}") + for k, v in result.items(): + if v != "任意_任意_任意_0": + self.信号[k] = v + except (TypeError, ValueError, KeyError, AttributeError, IndexError) as e: + logger.error(f"信号计算器: {config.get('name', '?')} 出错 — {e}") + traceback.print_exc() - # 注入基础周期K线的 OHLCV - 基础周期 = self._分析器.周期组[0] - 基础观察者 = self._获取周期观察者(基础周期) - if 基础观察者.普通K线序列: + # OHLCV 行情 + 基础观察者 = self._观察者字典.get(self._基础周期) + if 基础观察者 and 基础观察者.普通K线序列: 最后K线 = 基础观察者.普通K线序列[-1] - s.update( - { - "symbol": 基础观察者.符号, - "dt": 最后K线.时间戳, - "id": 最后K线.序号, - "open": 最后K线.开盘价, - "close": 最后K线.收盘价, - "high": 最后K线.高, - "low": 最后K线.低, - "vol": 最后K线.成交量, - } + 时间戳 = 最后K线.时间戳 + if isinstance(时间戳, (int, float)): + 时间戳 = datetime.fromtimestamp(int(时间戳)) + self.行情.update( + symbol=基础观察者.符号, + dt=时间戳, + id=最后K线.序号, + open=最后K线.开盘价, + close=最后K线.收盘价, + high=最后K线.高, + low=最后K线.低, + vol=最后K线.成交量, ) - self.信号字典 = s - return s - - def _获取周期观察者(self, 周期秒: int): - """统一获取观察者 — 兼容 Python 和 Rust 立体分析器""" - if hasattr(self._分析器, "_单体分析器"): - return self._分析器._单体分析器.get(周期秒) - if hasattr(self._分析器, "获取观察者"): - return self._分析器.获取观察者(周期秒) - return None - - def _遍历观察者(self): - """统一遍历所有周期的观察者 — 兼容 Python 和 Rust 立体分析器""" - if hasattr(self._分析器, "_单体分析器"): - yield from self._分析器._单体分析器.items() - elif hasattr(self._分析器, "获取观察者"): - for 周期秒 in self._分析器.周期组: - obs = self._分析器.获取观察者(周期秒) - if obs is not None: - yield 周期秒, obs - - def _计算脏标记(self) -> Dict[int, set]: - """比较各周期序列长度,返回变化类型集合。 - - :return: {周期秒: {"新分型", "新笔", "新线段", "新中枢", "新K线"}} - """ - 脏 = {} - for 周期秒, obs in self._遍历观察者(): - 变化 = set() - 上次 = self._上次状态.get(周期秒, {}) - - 当前分型数 = len(obs.分型序列) - if 当前分型数 != 上次.get("分型", 0): - 变化.add("新分型") - - 当前笔数 = len(obs.笔序列) - if 当前笔数 != 上次.get("笔", 0): - 变化.add("新笔") - - 当前线段数 = len(obs.线段序列) - if 当前线段数 != 上次.get("线段", 0): - 变化.add("新线段") - - 当前中枢数 = len(obs.中枢序列) - if 当前中枢数 != 上次.get("中枢", 0): - 变化.add("新中枢") - - 当前K线数 = len(obs.普通K线序列) - if 当前K线数 != 上次.get("K线", 0): - 变化.add("新K线") - - self._上次状态[周期秒] = { - "分型": 当前分型数, - "笔": 当前笔数, - "线段": 当前线段数, - "中枢": 当前中枢数, - "K线": 当前K线数, - } - 脏[周期秒] = 变化 - - return 脏 - - def _计算配置缓存键(self, config_index: int, config: Dict): - """基于配置对应周期的序列长度生成缓存键。 - - 无 freq 的配置返回 None(不缓存)。 - """ - freq = config.get("freq") - if freq is None: - return None - 周期秒 = _freq_str_to_seconds(freq) - obs = self._获取周期观察者(周期秒) - if obs is None: - return None - return ( - len(obs.分型序列), - len(obs.笔序列), - len(obs.线段序列), - len(obs.中枢序列), - len(obs.普通K线序列), - ) - def _执行信号函数(self, config: Dict) -> Optional[OrderedDict]: - """执行单条信号配置。 - - :param config: 信号配置,如: - {'name': 'chanlun.signals.tas_ma_base_V230313', - 'freq': '日线', 'di': 1, 'ma_type': 'SMA', 'timeperiod': 5} - :return: 信号 OrderedDict 或 None - """ param = dict(config) sig_name = param.pop("name") - sig_func = import_by_name(sig_name) if isinstance(sig_name, str) else sig_name + sig_func = self._信号函数缓存.get(sig_name) or self._解析信号函数(sig_name) - freq = param.get("freq", None) + freq = param.get("freq") if freq is not None: - 周期秒 = _freq_str_to_seconds(freq) - obs = self._获取周期观察者(周期秒) + 周期秒 = int(freq) + obs = self._观察者字典.get(周期秒) if obs is not None: return sig_func(obs, **param) else: - logger.debug(f"周期 '{freq}' ({周期秒}秒) 不在分析器的周期组 {self._分析器.周期组} 中,跳过") - return None + raise KeyError(f"信号计算器: 未找到周期 {周期秒}s 的观察者,可用周期: {sorted(self._观察者字典.keys())}") else: - # 无 freq 参数,传入自身(信号计算器),用于非周期绑定信号 return sig_func(self, **param) def 获取周期观察者(self, freq: str) -> Optional[观察者]: - """通过 Freq 枚举字符串获取对应周期的观察者""" - 周期秒 = _freq_str_to_seconds(freq) - return self._获取周期观察者(周期秒) - - -# ============================================================================== -# 信号交易员 — 信号驱动的多策略交易引擎 -# ============================================================================== - - -class 信号交易员(信号计算器): - """多周期信号驱动的交易决策引擎,继承信号计算器并管理多个持仓策略。 - - 使用方式:: - - 分析器 = 立体分析器("btcusd", [300, 900, 3600], 配置) - - 多头策略 = Position(symbol="btcusd", name="趋势多头", - opens=[开多事件], exits=[平多事件]) - 交易员 = 信号交易员(分析器, 持仓策略=[多头策略]) - - for k in k线列表: - 分析器.投喂K线(k) - 交易员.更新() - if 交易员.仓位变化: - print(f"仓位: {交易员.集成仓位()}") - """ - - def __init__( - self, - 分析器: 立体分析器, - 持仓策略: Optional[List[Position]] = None, - 集成方式: Union[str, Callable] = "mean", - 信号配置: Optional[List[Dict]] = None, - 信号模块: str = "chanlun.signals", - ): - """ - :param 分析器: 立体分析器实例 - :param 持仓策略: Position 列表(每个策略独立判断) - :param 集成方式: 多策略仓位集成方式 - - "mean": 平均仓位 np.mean([1, 1, -1]) = 0.33 - - "vote": 投票表决 sign(sum([1, 1, -1])) = 1 - - "max": 取最大值 max([1, 1, -1]) = 1 - - Callable: 自定义回调,输入 {name: pos},输出 float - :param 信号配置: 信号函数配置列表(若为 None 则从 持仓策略 自动提取) - :param 信号模块: 信号函数所在模块 - """ - self.持仓策略: List[Position] = 持仓策略 or [] - - # 预存信号模块,在 super().__init__ 之前设置,供 _从持仓提取配置 使用 - self._信号模块 = 信号模块 - - # 若未提供信号配置,从持仓策略自动提取 - if 信号配置 is None and self.持仓策略: - 信号配置 = self._从持仓提取配置() - elif 信号配置 is None: - 信号配置 = [] - - super().__init__(分析器, 信号配置=信号配置, 信号模块=信号模块) - self.__集成方式 = 集成方式 - - def _从持仓提取配置(self) -> List[Dict]: - """从所有持仓策略中提取唯一信号并生成配置""" - 所有信号: List[str] = [] - for p in self.持仓策略: - 所有信号.extend(p.unique_signals) - 所有信号 = list(set(所有信号)) - if not 所有信号: - return [] - return get_signals_config(所有信号, self._信号模块) - - def 更新(self) -> OrderedDict: - """投喂K线后的完整更新:信号计算 + 交易决策 - - :return: 更新后的信号字典 - """ - super().更新() # 信号计算器.更新 → self.信号字典 - - for pos in self.持仓策略: - pos.update(self.信号字典) - - return self.信号字典 - - @property - def 仓位变化(self) -> bool: - """任一持仓策略是否发生仓位变化""" - if not self.持仓策略: - return False - return any(p.pos_changed for p in self.持仓策略) - - def 集成仓位(self, method: Optional[Union[str, Callable]] = None) -> float: - """多策略仓位集成 - - :param method: 集成方法,覆盖构造时指定的默认方法 - :return: 集成仓位 float - """ - if not self.持仓策略: - return 0.0 - - method = method or self.__集成方式 - if isinstance(method, str): - method = method.lower() - pos_seq = [p.pos for p in self.持仓策略] - - if method == "mean": - return float(np.mean(pos_seq)) - elif method == "vote": - return float(np.sign(sum(pos_seq))) - elif method == "max": - return float(max(pos_seq)) - else: - raise ValueError(f"不支持的集成方式: {method}") - else: - return float(method({p.name: p.pos for p in self.持仓策略})) - - def 获取策略(self, name: str) -> Optional[Position]: - """获取指定名称的持仓策略""" - for pos in self.持仓策略: - if pos.name == name: - return pos - return None - - -if __name__ == "__main__": - # ================================================================ - # 测试 import_by_name - # ================================================================ - print("=" * 60) - print("测试 import_by_name") - print("=" * 60) - - def test_import_by_name(): - """测试动态导入""" - # 导入内置模块 - mod = import_by_name("os") - assert hasattr(mod, "path"), "应能导入 os 模块" - print(" ✓ 导入顶层模块 os") - - # 导入包内的子模块 - mod = import_by_name("logging.handlers") - assert mod.__name__ == "logging.handlers", "应能导入子模块" - print(" ✓ 导入子模块 logging.handlers") - - # 顶层规则: 不含 '.' 就用 __import__ 导入 - mod = import_by_name("json") - assert mod is not None - print(" ✓ 导入 json (无点号路径)") - - print(" ✅ import_by_name 全部通过") - - test_import_by_name() - - # ================================================================ - # 测试 Operate 枚举 - # ================================================================ - print() - print("=" * 60) - print("测试 Operate 枚举") - print("=" * 60) - - def test_operate(): - assert str(Operate.LO) == "开多", f"LO 应为 '开多',实际: {Operate.LO}" - assert str(Operate.LE) == "平多", f"LE 应为 '平多'" - assert str(Operate.SO) == "开空", f"SO 应为 '开空'" - assert str(Operate.SE) == "平空", f"SE 应为 '平空'" - assert str(Operate.HL) == "持多", f"HL 应为 '持多'" - assert str(Operate.HS) == "持空", f"HS 应为 '持空'" - assert str(Operate.HO) == "持币", f"HO 应为 '持币'" - print(" ✓ 7 个枚举值字符串全部正确") - - # Operate 应为 7 个成员 - members = list(Operate) - assert len(members) == 7, f"应有 7 个成员,实际 {len(members)}" - print(" ✓ 枚举成员数 = 7") - - print(" ✅ Operate 全部通过") - - test_operate() - - # ================================================================ - # 测试 Signal 模型 - # ================================================================ - print() - print("=" * 60) - print("测试 Signal 模型") - print("=" * 60) - - def test_signal(): - # ---- 创建方式1: 传入完整 signal 字符串 ---- - s1 = Signal(signal="60分钟_倒0笔_方向_向上_其他_其他_80") - assert s1.k1 == "60分钟", f"k1 应为 '60分钟',实际: {s1.k1}" - assert s1.k2 == "倒0笔", f"k2 应为 '倒0笔',实际: {s1.k2}" - assert s1.k3 == "方向", f"k3 应为 '方向',实际: {s1.k3}" - assert s1.v1 == "向上", f"v1 应为 '向上',实际: {s1.v1}" - assert s1.v2 == "其他", f"v2 应为 '其他',实际: {s1.v2}" - assert s1.v3 == "其他", f"v3 应为 '其他',实际: {s1.v3}" - assert s1.score == 80, f"score 应为 80,实际: {s1.score}" - print(" ✓ 从 signal 字符串解析 7 个字段") - - # ---- 创建方式2: 传入各字段,自动生成 signal ---- - s2 = Signal(k1="日线", k2="底分型", k3="突破V250101", v1="向上", v2="强", v3="", score=60) - assert s2.signal == "日线_底分型_突破V250101_向上_强__60", f"signal 应自动生成,实际: {s2.signal}" - print(" ✓ 从字段自动生成 signal 字符串") - - # ---- key 属性 ---- - assert s1.key == "60分钟_倒0笔_方向", f"key 应为 '60分钟_倒0笔_方向',实际: {s1.key}" - print(" ✓ key 属性 (去掉'任意'的 k1_k2_k3)") - - # key 中跳过"任意" - s_any = Signal(k1="任意", k2="任意", k3="唯一标记") - assert s_any.key == "唯一标记", f"全任意 key 应为 k3 本身,实际: {s_any.key}" - print(" ✓ key 属性 跳过 '任意' 字段") - - # ---- value 属性 ---- - assert s1.value == "向上_其他_其他_80", f"value 应为 '向上_其他_其他_80',实际: {s1.value}" - print(" ✓ value 属性 (v1_v2_v3_score)") - - # ---- is_match 判断 ---- - signals_dict = { - "60分钟_倒0笔_方向": "向上_其他_其他_80", - "日线_底分型_突破V250101": "向上_强__90", - } - - # 完全匹配 - assert s1.is_match(signals_dict), "同值应匹配" - print(" ✓ is_match 完全匹配") - - # score 更高(>= 即可) - s_low_score = Signal(signal="60分钟_倒0笔_方向_向上_其他_其他_50") - assert s_low_score.is_match(signals_dict), "低 score 的信号应匹配高 score 的目标 (>=)" - # 反过来不行 - s_high_score = Signal(signal="60分钟_倒0笔_方向_向上_其他_其他_90") - assert not s_high_score.is_match({"60分钟_倒0笔_方向": "向上_其他_其他_80"}), "高 score 的信号不应匹配低 score 的目标" - print(" ✓ is_match score 阈值正确 (>=)") - - # "任意" 通配 — 只对 v1/v2/v3 生效(key 必须精确匹配) - s_wild_v1 = Signal(signal="60分钟_倒0笔_方向_任意_其他_其他_60") - assert s_wild_v1.is_match(signals_dict), "'任意' v1 应匹配任何值" - s_wild_v2 = Signal(signal="60分钟_倒0笔_方向_向上_任意_其他_60") - assert s_wild_v2.is_match(signals_dict), "'任意' v2 应匹配任何值" - s_wild_v3 = Signal(signal="60分钟_倒0笔_方向_向上_其他_任意_60") - assert s_wild_v3.is_match(signals_dict), "'任意' v3 应匹配任何值" - print(" ✓ is_match '任意' 通配符 (v1/v2/v3)") - - # key 中 "任意" 字段被跳过,不参与 key 比较 - s_any_k = Signal(signal="60分钟_任意_方向_向上_其他_其他_60") - assert s_any_k.key == "60分钟_方向" - print(" ✓ key 计算跳过 '任意' k2") - - # key 不存在应抛出 ValueError - try: - s_bad = Signal(signal="不存在_键_信号_其他_其他_其他_50") - s_bad.is_match(signals_dict) - assert False, "不存在的 key 应抛出 ValueError" - except ValueError as e: - assert "不在信号列表中" in str(e) - print(" ✓ is_match 不存在的 key 抛出 ValueError") - - # @dataclass 版: __post_init__ 在字符串解析后校验 score,两种创建方式都生效 - try: - Signal(signal="15分钟_方向_向上_其他_其他_其他_150") - assert False, "信号字符串 score=150 应抛出 ValueError" - except ValueError as e: - assert "0~100" in str(e) - print(" ✓ signal 字符串 score 范围校验 (dataclass __post_init__)") - - try: - Signal(k1="日线", k2="底", k3="V1", score=101) - assert False, "score=101 应抛出 ValueError" - except ValueError: - print(" ✓ score 范围校验 0~100 (字段创建)") - - try: - Signal(k1="日线", k2="底", k3="V1", score=-1) - assert False, "score=-1 应抛出 ValueError" - except ValueError: - print(" ✓ score 范围校验 0~100 (负值)") - - # ---- __repr__ ---- - assert repr(s1) == "Signal('60分钟_倒0笔_方向_向上_其他_其他_80')", f"repr 应为 Signal('...'),实际: {repr(s1)}" - print(" ✓ __repr__") - - print(" ✅ Signal 全部通过") - - test_signal() - - # ================================================================ - # 测试 Factor 模型 - # ================================================================ - print() - print("=" * 60) - print("测试 Factor 模型") - print("=" * 60) - - def test_factor(): - # ---- 基础创建 ---- - f1 = Factor( - signals_all=[ - Signal(signal="15分钟_倒0笔_方向_向上_其他_其他_0"), - Signal(signal="15分钟_倒0笔_长度_大于5_其他_其他_0"), - ], - name="测试因子", - ) - assert f1.name.startswith("测试因子#"), f"name 应包含 hash,实际: {f1.name}" - assert len(f1.name.split("#")[1]) == 4, f"hash 应为 4 位,实际: {f1.name.split('#')[1]}" - print(" ✓ Factor 创建并自动生成 4 位 hash") - - # ---- 同名不重复加 hash ---- - f_with_hash = Factor( - signals_all=[Signal(signal="15分钟_方向_向上_其他_其他_其他_0")], - name="已有因子#ABCD", - ) - assert "#" in f_with_hash.name and f_with_hash.name.split("#")[0] == "已有因子" - # 重新计算的 hash 会覆盖旧 hash - assert f_with_hash.name.split("#")[1] != "ABCD" or f_with_hash.name.split("#")[1] == "ABCD" - print(" ✓ name 中旧 hash 被覆盖") - - # ---- name 为空时自动生成 ---- - f_no_name = Factor( - signals_all=[Signal(signal="15分钟_方向_向上_其他_其他_其他_0")], - ) - assert f_no_name.name.startswith("#"), f"无名 Factor name 应以 # 开头,实际: {f_no_name.name}" - print(" ✓ name 为空时自动生成 '#XXXX'") - - # ---- signals_all 为空应报错 ---- - try: - Factor(signals_all=[], name="空因子") - assert False, "空 signals_all 应抛出 ValueError" - except ValueError as e: - assert "不能为空" in str(e) - print(" ✓ signals_all 为空时抛出 ValueError") - - # ---- unique_signals ---- - f_uniq = Factor( - signals_all=[ - Signal(signal="15分钟_方向_向上_其他_其他_其他_60"), - Signal(signal="15分钟_方向_向上_其他_其他_其他_60"), # 重复,应去重 - ], - signals_any=[Signal(signal="日线_底分型_其他_其他_其他_其他_0")], - signals_not=[Signal(signal="周线_其他_其他_其他_其他_其他_0")], - ) - uniq = f_uniq.unique_signals - assert len(uniq) == 3, f"去重后应为 3 个唯一 signal,实际: {len(uniq)}" - print(" ✓ unique_signals 去重 (重复 signal 只保留一个)") - - # ---- is_match ---- - sig_dict = { - "15分钟_倒0笔_方向": "向上_其他_其他_80", - "15分钟_倒0笔_长度": "大于5_其他_其他_80", - } - f_match = Factor( - signals_all=[ - Signal(signal="15分钟_倒0笔_方向_向上_其他_其他_50"), - Signal(signal="15分钟_倒0笔_长度_大于5_其他_其他_50"), - ], - ) - assert f_match.is_match(sig_dict), "signals_all 全部满足应匹配" - print(" ✓ is_match signals_all 全部满足") - - # signals_all 不满足 - f_no_match = Factor( - signals_all=[ - Signal(signal="15分钟_倒0笔_方向_向下_其他_其他_50"), - ], - ) - assert not f_no_match.is_match(sig_dict), "signals_all 不满足应返回 False" - - # signals_any — 其中有一个不满足(键不存在会抛异常),需确保 key 都在 dict 中 - f_any = Factor( - signals_all=[Signal(signal="15分钟_倒0笔_方向_向上_其他_其他_50")], - signals_any=[ - Signal(signal="15分钟_倒0笔_方向_向下_其他_其他_50"), # v1=向下 不匹配 - Signal(signal="15分钟_倒0笔_长度_大于5_其他_其他_50"), # 这个匹配 - ], - ) - assert f_any.is_match(sig_dict), "signals_all 满足 + signals_any 任一满足(第二个)" - print(" ✓ is_match signals_any 任一满足") - - # signals_any 全部不满足 → Factor 不匹配(设置了 any 就必须至少一个满足) - f_no_any = Factor( - signals_all=[Signal(signal="15分钟_倒0笔_方向_向上_其他_其他_50")], - signals_any=[ - Signal(signal="15分钟_倒0笔_方向_向下_其他_其他_50"), - ], - ) - assert not f_no_any.is_match(sig_dict), "signals_any 设了就必须至少一个满足" - print(" ✓ is_match signals_any 全部不满足时 Factor 不匹配") - - # signals_not - f_not = Factor( - signals_all=[Signal(signal="15分钟_倒0笔_方向_向上_其他_其他_50")], - signals_not=[Signal(signal="15分钟_倒0笔_长度_大于5_其他_其他_50")], - ) - assert not f_not.is_match(sig_dict), "signals_not 满足时应返回 False" - print(" ✓ is_match signals_not 排除") - - # ---- dump / load 往返 ---- - f_dump = Factor( - signals_all=[ - Signal(signal="15分钟_倒0笔_方向_向上_其他_其他_50"), - Signal(signal="日线_底分型_突破V250101_向上_其他_其他_90"), - ], - signals_any=[Signal(signal="60分钟_其他_其他_其他_其他_其他_0")], - signals_not=[], - name="往返测试", - ) - raw = f_dump.dump() - assert raw["name"] == f_dump.name - assert len(raw["signals_all"]) == 2 - assert len(raw["signals_any"]) == 1 - assert raw["signals_not"] == [] - - f_loaded = Factor.load(raw) - assert f_loaded.name == f_dump.name, f"load 后 name 应一致,实际: {f_loaded.name} vs {f_dump.name}" - assert len(f_loaded.signals_all) == 2 - assert len(f_loaded.signals_any) == 1 - assert len(f_loaded.signals_not) == 0 - print(" ✓ Factor dump / load 往返一致 (@dataclass)") - - print(" ✅ Factor 全部通过") - - test_factor() - - # ================================================================ - # 测试 Event 模型 - # ================================================================ - print() - print("=" * 60) - print("测试 Event 模型") - print("=" * 60) - - def test_event(): - # ---- 基础创建 ---- - e1 = Event( - operate=Operate.LO, - factors=[ - Factor( - signals_all=[ - Signal(signal="15分钟_方向_向上_其他_其他_其他_50"), - ], - name="因子1", - ), - Factor( - signals_all=[ - Signal(signal="日线_底分型_突破_向上_其他_其他_80"), - ], - name="因子2", - ), - ], - name="测试事件", - ) - assert e1.name.startswith("测试事件#"), f"name 应包含 hash,实际: {e1.name}" - assert len(e1.sha256) == 4, "sha256 应为 4 位" - print(" ✓ Event 创建并自动生成 hash") - - # ---- factors 为空应报错 ---- - try: - Event(operate=Operate.LO, factors=[], name="空事件") - assert False, "空 factors 应抛出 ValueError" - except ValueError as e: - assert "不能为空" in str(e) - print(" ✓ factors 为空时抛出 ValueError") - - # ---- name 自动使用 operate ---- - e_auto = Event( - operate=Operate.SO, - factors=[ - Factor( - signals_all=[Signal(signal="15分钟_方向_向下_其他_其他_其他_0")], - ), - ], - ) - assert e_auto.name.startswith("开空#"), f"无名 Event 应以 operate 开头,实际: {e_auto.name}" - print(" ✓ name 为空时自动使用 operate.value 作为前缀") - - # ---- unique_signals 包含 event 级别 + 所有 factor 的 signals ---- - e_uniq = Event( - operate=Operate.LO, - factors=[ - Factor( - signals_all=[Signal(signal="A_k1_k2_up_其他_其他_60")], - signals_any=[Signal(signal="B_k1_k2_up_其他_其他_60")], - ), - ], - signals_all=[Signal(signal="C_k1_k2_up_其他_其他_0")], - signals_any=[Signal(signal="D_k1_k2_up_其他_其他_0")], - signals_not=[Signal(signal="E_k1_k2_up_其他_其他_0")], - ) - uniq = e_uniq.unique_signals - assert len(uniq) == 5, f"unique_signals 应为 5 (A+B+C+D+E),实际: {len(uniq)}" - print(" ✓ unique_signals 汇总 event + factor 信号并去重") - - # ---- is_match ---- - sig_dict = { - "15分钟_方向_向上": "向上_其他_其他_80", - "日线_底分_突破": "突破_向上_其他_90", - "排除_信号_排除": "排除_排除_排除_50", - } - - # 全部满足 - e_match = Event( - operate=Operate.LO, - factors=[ - Factor( - signals_all=[Signal(signal="15分钟_方向_向上_向上_其他_其他_50")], - ), - ], - ) - is_match, factor_name = e_match.is_match(sig_dict) - assert is_match, "Event 应匹配" - print(f" ✓ is_match 返回 (True, factor_name): ({is_match}, {factor_name})") - - # factor 不满足 (同 key,v1 不匹配) - e_no_factor = Event( - operate=Operate.LO, - factors=[ - Factor( - signals_all=[Signal(signal="15分钟_方向_向上_向下_其他_其他_50")], - ), - ], - ) - is_match, factor_name = e_no_factor.is_match(sig_dict) - assert not is_match, "factor v1 不匹配时 Event 应不匹配" - print(" ✓ is_match factor 不满足返回 (False, None)") - - # signals_not 排除 - e_not = Event( - operate=Operate.LO, - factors=[ - Factor( - signals_all=[Signal(signal="15分钟_方向_向上_向上_其他_其他_50")], - ), - ], - signals_not=[Signal(signal="排除_信号_排除_排除_排除_排除_0")], - ) - is_match, _ = e_not.is_match(sig_dict) - assert not is_match, "signals_not 满足时应返回 False" - print(" ✓ is_match signals_not 排除") - - # signals_all (event级) 不满足 — key 不存在抛异常 - e_all = Event( - operate=Operate.LO, - factors=[ - Factor( - signals_all=[Signal(signal="15分钟_方向_向上_向上_其他_其他_50")], - ), - ], - signals_all=[Signal(signal="不存在_键_信号_其他_其他_其他_0")], - ) - try: - is_match, _ = e_all.is_match(sig_dict) - assert False, "signals_all 的键不存在应抛异常" - except ValueError: - print(" ✓ is_match event 级 signals_all 键不存在时抛异常") - - # signals_any (event级) — key 不存在抛异常 - e_any_fail = Event( - operate=Operate.LO, - factors=[ - Factor( - signals_all=[Signal(signal="15分钟_方向_向上_向上_其他_其他_50")], - ), - ], - signals_any=[Signal(signal="不存在_键_信号_其他_其他_其他_0")], - ) - try: - is_match, _ = e_any_fail.is_match(sig_dict) - assert False, "signals_any 的键不存在应抛异常" - except ValueError: - print(" ✓ is_match event 级 signals_any 键不存在时抛异常") - - # ---- dump ---- - e_dump = Event( - operate=Operate.LE, - factors=[ - Factor( - signals_all=[ - Signal(signal="60分钟_方向_向上_其他_其他_其他_0"), - Signal(signal="日线_底分_突破_向上_向上_其他_80"), - ], - signals_any=[Signal(signal="15分钟_其他_其他_其他_其他_其他_0")], - name="往返因子", - ), - ], - signals_all=[Signal(signal="周线_趋势_向上_其他_其他_其他_60")], - signals_any=[], - signals_not=[Signal(signal="月线_背离_向下_其他_其他_其他_30")], - name="往返测试", - ) - raw = e_dump.dump() - assert raw["operate"] == "平多", f"operate 应为 '平多',实际: {raw['operate']}" - assert len(raw["factors"]) == 1 - assert len(raw["signals_all"]) == 1 - assert len(raw["signals_not"]) == 1 - print(" ✓ Event dump 结构正确") - - e_loaded = Event.load(raw) - assert e_loaded.name == e_dump.name - assert e_loaded.operate == Operate.LE - assert len(e_loaded.factors) == 1 - assert len(e_loaded.signals_all) == 1 - print(" ✓ Event dump / load 往返一致 (@dataclass)") - - # load 时 operate 无效应报错 - raw_bad = dict(raw) - raw_bad["operate"] = "不存在的操作" - try: - Event.load(raw_bad) - assert False, "无效 operate 应抛异常" - except AssertionError: - print(" ✓ load 时无效 operate 抛出 AssertionError") - - print(" ✅ Event 全部通过") - - test_event() - - # ================================================================ - # 测试 get_signals_freqs - # ================================================================ - print() - print("=" * 60) - print("测试 get_signals_freqs") - print("=" * 60) - - def test_get_signals_freqs(): - # 从信号字符串提取周期 - signals = [ - "15分钟_D1K_量柱V221218_低量柱_6K_任意_0", - "日线_D1K_量柱V221218_低量柱_6K_任意_0", - "60分钟_方向_向上_其他_其他_其他_80", - ] - freqs = get_signals_freqs(signals) - assert "15分钟" in freqs, "应包含 15分钟" - assert "60分钟" in freqs, "应包含 60分钟" - assert "日线" in freqs, "应包含 日线" - # 按 sorted_freqs 排序 - assert freqs == ["15分钟", "60分钟", "日线"], f"freqs 应按 sorted_freqs 排序,实际: {freqs}" - print(f" ✓ 提取周期: {freqs}") - - # 不匹配任何周期的信号 - empty_freqs = get_signals_freqs(["无关文本_无周期_其他_其他_其他_0"]) - assert empty_freqs == [], f"无周期时应返回空列表,实际: {empty_freqs}" - print(" ✓ 无周期信号返回空列表") - - # 从信号函数配置字典中提取 - config = [ - {"freq": "日线", "max_overlap": "3", "name": "czsc.signals.cxt_bi_end_V230222"}, - {"freq1": "日线", "freq2": "60分钟", "name": "czsc.signals.cxt_zhong_shu_gong_zhen_V221221"}, - ] - config_freqs = get_signals_freqs(config) - assert "60分钟" in config_freqs - assert "日线" in config_freqs - assert config_freqs == ["60分钟", "日线"], f"应从配置字典中提取周期,实际: {config_freqs}" - print(f" ✓ 从配置字典提取周期: {config_freqs}") - - # Tick 在 sorted_freqs 开头 - tick_signals = ["Tick_数据源_原始K_其他_其他_其他_0"] - tick_freqs = get_signals_freqs(tick_signals) - assert "Tick" in tick_freqs, f"应提取 Tick 周期,实际: {tick_freqs}" - print(" ✓ 提取 Tick 周期") - - print(" ✅ get_signals_freqs 全部通过") - - test_get_signals_freqs() - - # ================================================================ - # 测试 边界 & 特殊情况 - # ================================================================ - print() - print("=" * 60) - print("测试边界 & 特殊情况") - print("=" * 60) - - def test_edge_cases(): - # Signal 空 signal 字符串 + 字段 - s_empty = Signal(k1="测试", k2="空值", k3="V1", score=0) - # Pydantic 默认值 "任意" 替代了空字符串 - assert s_empty.signal == "测试_空值_V1_任意_任意_任意_0", f"空字段默认 '任意',实际: {s_empty.signal}" - print(" ✓ Signal 空 v1/v2/v3 字段 (默认 '任意')") - - # Signal 最小 score 0 - s_min = Signal(signal="test_k1_k2_up_其他_其他_0") - assert s_min.score == 0 - print(" ✓ Signal score=0 (边界)") - - # Signal 最大 score 100 - s_max = Signal(signal="test_k1_k2_up_其他_其他_100") - assert s_max.score == 100 - print(" ✓ Signal score=100 (边界)") - - # Factor 空 signals_any 和 signals_not - f_min = Factor( - signals_all=[Signal(signal="test_k1_k2_up_其他_其他_0")], - ) - assert f_min.signals_any == [] - assert f_min.signals_not == [] - assert f_min.name.startswith("#") - print(" ✓ Factor 最小构造 (仅 signals_all)") - - # Factor.load 无 signals_any/signals_not - f_from_raw = Factor.load( - { - "name": "最小因子", - "signals_all": ["test_k1_k2_up_其他_其他_0"], - } - ) - assert len(f_from_raw.signals_all) == 1 - assert f_from_raw.signals_any == [] - assert f_from_raw.signals_not == [] - print(" ✓ Factor.load 缺省 signals_any/signals_not (@dataclass)") - - # Event signals_any 和 signals_not 可为空 - e_min = Event( - operate=Operate.HO, - factors=[ - Factor( - signals_all=[Signal(signal="test_k1_k2_up_其他_其他_0")], - ), - ], - ) - assert e_min.signals_all == [] - assert e_min.signals_any == [] - assert e_min.signals_not == [] - print(" ✓ Event 最小构造 (仅 operate + factors)") - - # 同一 Signal 内容不同 score 视为不同信号 - s_a = Signal(signal="test_k1_k2_up_其他_其他_10") - s_b = Signal(signal="test_k1_k2_up_其他_其他_90") - assert s_a.signal != s_b.signal - print(" ✓ 不同 score 产生不同 signal 字符串") - - print(" ✅ 边界测试全部通过") - - test_edge_cases() - - def test_signal(): - s = Signal(k1="1分钟", k3="倒1形态", v1="类一买", v2="七笔", v3="基础型", score=3) - assert str(s) == "Signal('1分钟_任意_倒1形态_类一买_七笔_基础型_3')" - assert s.key == "1分钟_倒1形态" - s1 = Signal(signal="1分钟_任意_倒1形态_类一买_七笔_基础型_3") - assert s == s1 - assert s.is_match({"1分钟_倒1形态": "类一买_七笔_基础型_3"}) - assert not s.is_match({"1分钟_倒1形态": "类一买_七笔_特例一_3"}) - assert not s.is_match({"1分钟_倒1形态": "类一买_九笔_基础型_3"}) - - s = Signal(k1="1分钟", k2="倒1形态", k3="类一买", score=3) - assert str(s) == "Signal('1分钟_倒1形态_类一买_任意_任意_任意_3')" - assert s.key == "1分钟_倒1形态_类一买" - - try: - s = Signal(k1="1分钟", k2="倒1形态", k3="类一买", score=101) - except ValueError as e: - assert str(e) == "score 必须在0~100之间" - - test_signal() - - def test_factor(): - freq = Freq.F15 - s = OrderedDict() - default_signals = [ - Signal(k1=str(freq.value), k2="倒0笔", k3="方向", v1="向上", v2="其他", v3="其他"), - Signal(k1=str(freq.value), k2="倒0笔", k3="长度", v1="大于5", v2="其他", v3="其他"), - Signal(k1=str(freq.value), k2="倒0笔", k3="三K形态", v1="顶分型", v2="其他", v3="其他"), - Signal(k1=str(freq.value), k2="倒1笔", k3="表里关系", v1="其他", v2="其他", v3="其他"), - Signal(k1=str(freq.value), k2="倒1笔", k3="RSQ状态", v1="小于0.2", v2="其他", v3="其他"), - ] - for signal in default_signals: - s[signal.key] = signal.value - - factor = Factor( - name="单测", - signals_all=[ - Signal(k1=str(freq.value), k2="倒0笔", k3="方向", v1="向上", v2="其他", v3="其他"), - Signal(k1=str(freq.value), k2="倒0笔", k3="长度", v1="大于5", v2="其他", v3="其他"), - ], - ) - assert factor.is_match(s) - - factor_raw = factor.dump() - new_factor = Factor.load(factor_raw) - assert new_factor.is_match(s) - - factor = Factor( - name="单测", - signals_all=[ - Signal(k1=str(freq.value), k2="倒0笔", k3="方向", v1="向上", v2="其他", v3="其他"), - Signal(k1=str(freq.value), k2="倒0笔", k3="长度", v1="大于5", v2="其他", v3="其他"), - ], - signals_any=[Signal(k1=str(freq.value), k2="倒1笔", k3="RSQ状态", v1="小于0.2", v2="其他", v3="其他")], - ) - assert factor.is_match(s) - - factor = Factor( - name="单测", - signals_all=[ - Signal(k1=str(freq.value), k2="倒0笔", k3="方向", v1="向上", v2="其他", v3="其他"), - Signal(k1=str(freq.value), k2="倒0笔", k3="长度", v1="大于5", v2="其他", v3="其他"), - ], - signals_any=[Signal(k1=str(freq.value), k2="倒1笔", k3="RSQ状态", v1="小于0.8", v2="其他", v3="其他")], - ) - assert not factor.is_match(s) - - factor = Factor( - name="单测", - signals_all=[ - Signal(k1=str(freq.value), k2="倒0笔", k3="方向", v1="向上", v2="其他", v3="其他"), - Signal(k1=str(freq.value), k2="倒0笔", k3="长度", v1="大于5", v2="其他", v3="其他"), - ], - signals_any=[Signal(k1=str(freq.value), k2="倒1笔", k3="RSQ状态", v1="小于0.2", v2="其他", v3="其他")], - signals_not=[ - Signal(k1=str(freq.value), k2="倒0笔", k3="三K形态", v1="顶分型", v2="其他", v3="其他"), - ], - ) - assert not factor.is_match(s) - - test_factor() - - def test_event(): - freq = Freq.F15 - s = OrderedDict() - default_signals = [ - Signal(k1=str(freq.value), k2="倒0笔", k3="方向", v1="向上", v2="其他", v3="其他"), - Signal(k1=str(freq.value), k2="倒0笔", k3="长度", v1="大于5", v2="其他", v3="其他"), - Signal(k1=str(freq.value), k2="倒0笔", k3="三K形态", v1="顶分型", v2="其他", v3="其他"), - Signal(k1=str(freq.value), k2="倒1笔", k3="表里关系", v1="其他", v2="其他", v3="其他"), - Signal(k1=str(freq.value), k2="倒1笔", k3="RSQ状态", v1="小于0.2", v2="其他", v3="其他"), - ] - for signal in default_signals: - s[signal.key] = signal.value - - event = Event( - name="单测", - operate=Operate.LO, - factors=[ - Factor( - name="测试", - signals_all=[Signal(k1=str(freq.value), k2="倒0笔", k3="长度", v1="大于5", v2="其他", v3="其他")], - ) - ], - signals_all=[ - Signal(k1=str(freq.value), k2="倒0笔", k3="方向", v1="向上", v2="其他", v3="其他"), - ], - ) - m, f = event.is_match(s) - assert m and f - - raw = event.dump() - new_event = Event.load(raw) - m, f = new_event.is_match(s) - assert m and f - - raw1 = { - "name": "单测", - "operate": "开多", - "signals_all": ["15分钟_倒0笔_方向_向上_其他_其他_0"], - "factors": [{"name": "测试", "signals_all": ["15分钟_倒0笔_长度_大于5_其他_其他_0"]}], - } - new_event = Event.load(raw1) - m, f = new_event.is_match(s) - assert m and f - - raw1 = { - "operate": "开多", - "signals_all": ["15分钟_倒0笔_方向_向上_其他_其他_0"], - "factors": [{"name": "测试", "signals_all": ["15分钟_倒0笔_长度_大于5_其他_其他_0"]}], - } - new_event = Event.load(raw1) - m, f = new_event.is_match(s) - assert m and f - - event = Event( - name="单测", - operate=Operate.LO, - factors=[ - Factor(name="测试", signals_all=[Signal("15分钟_倒0笔_长度_大于5_其他_其他_0")]), - ], - signals_any=[Signal("15分钟_倒0笔_方向_向上_其他_其他_0"), Signal("15分钟_倒0笔_长度_大于100_其他_其他_0")], - ) - m, f = event.is_match(s) - assert m and f - - event = Event( - name="单测", - operate=Operate.LO, - factors=[ - Factor( - name="测试", - signals_all=[Signal(k1=str(freq.value), k2="倒0笔", k3="长度", v1="大于5", v2="其他", v3="其他")], - ) - ], - signals_not=[ - Signal(k1=str(freq.value), k2="倒0笔", k3="方向", v1="向上", v2="其他", v3="其他"), - ], - ) - m, f = event.is_match(s) - assert not m and not f - - event = Event( - name="单测", - operate=Operate.LO, - factors=[ - Factor( - name="测试", - signals_all=[ - Signal(k1=str(freq.value), k2="倒0笔", k3="方向", v1="向上", v2="其他", v3="其他"), - Signal(k1=str(freq.value), k2="倒0笔", k3="长度", v1="大于5", v2="其他", v3="其他"), - ], - ) - ], - ) - m, f = event.is_match(s) - assert m and f - - event = Event( - name="单测", - operate=Operate.LO, - factors=[ - Factor( - name="测试", - signals_all=[ - Signal("15分钟_倒0笔_方向_向上_其他_其他_0"), - Signal("15分钟_倒0笔_长度_任意_其他_其他_0"), - ], - ) - ], - ) - m, f = event.is_match(s) - assert m and f - - event = Event( - name="单测", - operate=Operate.LO, - factors=[ - Factor( - name="测试", - signals_all=[ - Signal("15分钟_倒0笔_方向_向上_其他_其他_20"), - Signal("15分钟_倒0笔_长度_任意_其他_其他_0"), - ], - ) - ], - ) - m, f = event.is_match(s) - assert not m and not f - - event = Event( - name="单测", - operate=Operate.LO, - factors=[ - Factor( - name="测试", - signals_all=[ - Signal("15分钟_倒0笔_方向_向下_其他_其他_0"), - Signal("15分钟_倒0笔_长度_任意_其他_其他_0"), - ], - ) - ], - ) - m, f = event.is_match(s) - assert not m and not f - - event = Event.load( - { - "name": "开多", - "operate": "开多", - "signals_all": ["1分钟_D1_涨跌停V230331_任意_任意_任意_0", "1分钟_D0停顿分型_BE辅助V230106_看空_强_任意_0"], - "signals_any": [], - "signals_not": [], - "factors": [ - { - "name": "SMA#40多头", - "signals_all": ["5分钟_D1#SMA#40MO10_BS辅助V230313_看多_任意_任意_0"], - "signals_any": [], - "signals_not": [], - } - ], - } - ) - assert len(event.get_signals_config()) == 3 - - test_event() - - print() - print("=" * 60) - print("🎉 全部测试通过!") - print("=" * 60) + return self._观察者字典.get(int(freq)) diff --git a/chanlun-py/chanlun/signal_orchestrator.py b/chanlun-py/chanlun/signal_orchestrator.py new file mode 100644 index 0000000..d37362c --- /dev/null +++ b/chanlun-py/chanlun/signal_orchestrator.py @@ -0,0 +1,321 @@ +"""信号编排器 — Rust 优先 + Python 回退的混合信号计算。 + +Rust 注册表优先(编译时 #[signal] 宏注册),Python import_by_name 回退。 + +使用方式:: + + 分析器 = 立体分析器("btcusd", [300, 900, 3600], 配置) + 编排器 = SignalOrchestrator(分析器, 信号配置=[...]) + + for k in k线列表: + 分析器.投喂K线(k) + 编排器.更新() + print(编排器.信号字典) +""" + +import sys +from collections import OrderedDict +from typing import Any, Callable, Dict, List, Optional + +from loguru import logger + +from chanlun import 观察者 +from chanlun._chanlun import ( + 信号引擎 as _RustSignalEngine, + list_signals as _rust_list_signals, +) + + +class SignalOrchestrator: + """混合信号编排器:Rust 注册表优先,Python import_by_name 回退。""" + + def __init__( + self, + 分析器, + 信号配置: Optional[List[Dict]] = None, + 信号模块: str = "chanlun.signals", + ): + self._分析器 = 分析器 + self._观察者字典 = {p: 分析器._单体分析器[p] for p in 分析器.周期组} + self._基础周期 = 分析器.周期组[0] + self._信号模块 = 信号模块 + + # 分类存储 + self._rust_configs: List[Dict] = [] + self._python_configs: List[Dict] = [] + self._python_func_cache: Dict[str, Callable] = {} + + # 结果容器 + self.信号: Dict[str, str] = {} + self.行情: Dict[str, Any] = {} + + # 初始设置 + self.信号配置 = 信号配置 or [] + + # ── 信号配置 property ── + + @property + def 信号配置(self) -> List[Dict]: + return self._信号配置 + + @信号配置.setter + def 信号配置(self, value: List[Dict]): + 可用周期 = set(self._分析器.周期组) + rust_names = set(_rust_list_signals()) + + self._rust_configs = [] + self._python_configs = [] + + for c in self._去重配置(value): + freq = c.get("freq") + if freq is not None: + 周期秒 = int(freq) + if 周期秒 not in 可用周期: + raise ValueError(f"信号配置 freq={freq}({周期秒}s) 不在分析器周期组 {sorted(可用周期)} 中\n 信号: {c.get('name', '?')}") + + name = c.get("name", "") + # 分类:含 '.' 的显式 Python 路径 → Python;短名查 Rust 注册表 + if "." in name: + self._python_configs.append(c) + elif name in rust_names: + self._rust_configs.append(c) + else: + self._python_configs.append(c) + + self._信号配置 = value + self._预加载Python信号函数() + + # ── 更新 ── + + def 更新(self): + """执行所有信号:Rust 批量优先,Python 逐个回退。""" + self.信号.clear() + self.行情.clear() + + # 0. 始终确保指标已计算(幂等),Rust/Python 信号都需要 + _RustSignalEngine(信号配置=[]).自动挂载指标(self._分析器) + + # 1. Rust 批量执行 + if self._rust_configs: + rust_cfgs = [] + for c in self._rust_configs: + freq = int(c.get("freq", 0)) + rust_cfgs.append({"name": c["name"], "freq": str(freq)}) + + engine = _RustSignalEngine(信号配置=rust_cfgs) + engine.自动挂载指标(self._分析器) + + result = engine.更新_完整(self._分析器) + if result.get("signals"): + for k, v in result["signals"].items(): + if v != "任意_任意_任意_0": + self.信号[k] = v + if result.get("market"): + self.行情 = dict(result["market"]) + + # 2. Python 回退(逐个 import_by_name 调用) + for config in self._python_configs: + try: + result = self._执行Python信号函数(config) + if result: + for k, v in result.items(): + if v != "任意_任意_任意_0": + self.信号[k] = v + except Exception: + logger.exception(f"Python 信号函数执行失败: {config.get('name')}") + + # 3. 补充行情(若 Rust 引擎未提供) + if not self.行情: + self._提取行情() + + # ── Python 信号函数执行 ── + + def _执行Python信号函数(self, config: Dict) -> Optional[OrderedDict]: + """执行单个 Python 信号函数(import_by_name 动态导入)。""" + param = dict(config) + sig_name = param.pop("name") + sig_func = self._python_func_cache.get(sig_name) or self._解析信号函数(sig_name) + if sig_func is None: + logger.warning(f"信号函数未找到: {sig_name}") + return None + + freq = param.get("freq", None) + if freq is not None: + 周期秒 = int(freq) + obs = self._观察者字典.get(周期秒) + if obs is None: + logger.warning(f"未找到周期 {freq} 的观察者") + return None + else: + obs = self + return sig_func(obs, **param) + + # ── 辅助方法 ── + + def _去重配置(self, configs: List[Dict]) -> List[Dict]: + seen = set() + unique = [] + for c in configs: + key = ( + c.get("name"), + frozenset((k, str(v)) for k, v in c.items() if k != "name"), + ) + if key not in seen: + seen.add(key) + unique.append(c) + return unique + + def _预加载Python信号函数(self): + for config in self._python_configs: + name = config.get("name", "") + if name and name not in self._python_func_cache: + self._python_func_cache[name] = None + for name in list(self._python_func_cache.keys()): + try: + self._python_func_cache[name] = self._解析信号函数(name) + except Exception: + logger.warning(f"预加载信号函数失败: {name}") + + @staticmethod + def _解析信号函数(name: str) -> Optional[Callable]: + """动态导入信号函数(与旧 信号计算器 逻辑一致)。""" + if "." not in name: + return __import__(name) + + module_name, func_name = name.rsplit(".", 1) + main_mod = sys.modules.get("__main__") + if main_mod is not None and hasattr(main_mod, func_name): + return getattr(main_mod, func_name) + + module = __import__(module_name, fromlist=[func_name]) + return getattr(module, func_name, None) + + def _提取行情(self): + """从基础周期观察者提取 OHLCV 行情。""" + obs = self._观察者字典.get(self._基础周期) + if obs is None: + return + klines = obs.普通K线序列 + if not klines: + return + k = klines[-1] + self.行情 = { + "symbol": obs.符号, + "dt": k.时间戳, + "id": k.序号, + "open": k.开盘价, + "high": k.高, + "low": k.低, + "close": k.收盘价, + "vol": k.成交量, + } + + # ── 公共属性 ── + + @property + def 信号字典(self) -> dict: + """合并信号 + 行情(与 Position.update() 兼容)。""" + return {**self.信号, **self.行情} + + def 获取周期观察者(self, freq: str) -> Optional[观察者]: + """按频率字符串获取观察者。""" + return self._观察者字典.get(int(freq)) + + def 从信号列表提取配置(self, 信号序列: List[str]): + """从信号字符串列表解析配置(Rust 模板 + Python SignalsParser 双路径)。""" + from chanlun._chanlun import get_signal_template + + if not 信号序列: + return + + rust_names = set(_rust_list_signals()) + configs = [] + seen = set() + + for sig_key in 信号序列: + matched = False + # 1) 尝试 Rust 模板匹配 + for name in rust_names: + template = get_signal_template(name) + if template is None: + continue + from chanlun.parse import parse as _parse + + parsed = _parse(template, sig_key) + if parsed is not None: + entry = {"name": name} + entry.update(parsed.named) + key = (name, frozenset((k, str(v)) for k, v in entry.items() if k != "name")) + if key not in seen: + seen.add(key) + configs.append(entry) + matched = True + break + # 2) Python SignalsParser 回退 + if not matched: + try: + from chanlun.chan_external import SignalsParser + + sp = SignalsParser(signals_module=self._信号模块) + py_configs = sp.parse([sig_key]) + for c in py_configs: + key = (c.get("name"), frozenset((k, str(v)) for k, v in c.items() if k != "name")) + if key not in seen: + seen.add(key) + configs.append(c) + except Exception: + logger.warning(f"无法解析信号 key: {sig_key}") + + self.信号配置 = configs + + +def get_signals_config(signal_keys: list, signals_module: str = "chanlun.signals") -> List[Dict]: + """从 Rust 注册表 + Python SignalsParser 生成信号配置(双路径)。 + + 根据信号 key 字符串,优先用 Rust 注册表模板匹配,失败则回退到 Python SignalsParser。 + """ + from chanlun._chanlun import list_signals, get_signal_template + + rust_names = set(list_signals()) + configs = [] + seen = set() + unmatched = [] + + for sig_key in signal_keys: + matched = False + # 1) Rust 模板 + for name in rust_names: + template = get_signal_template(name) + if template is None: + continue + from chanlun.parse import parse as _parse + + parsed = _parse(template, sig_key) + if parsed is not None: + entry = {"name": name} + entry.update(parsed.named) + key = (name, frozenset((k, str(v)) for k, v in entry.items() if k != "name")) + if key not in seen: + seen.add(key) + configs.append(entry) + matched = True + break + # 2) 回退到 Python + if not matched: + unmatched.append(sig_key) + + if unmatched: + try: + from chanlun.chan_external import SignalsParser + + sp = SignalsParser(signals_module=signals_module) + py_configs = sp.parse(unmatched) + for c in py_configs: + key = (c.get("name"), frozenset((k, str(v)) for k, v in c.items() if k != "name")) + if key not in seen: + seen.add(key) + configs.append(c) + except Exception: + logger.warning(f"SignalsParser 无法解析: {unmatched}") + + return configs diff --git a/chanlun-py/chanlun/signals/__init__.py b/chanlun-py/chanlun/signals/__init__.py index 2f1b0a5..14edf23 100644 --- a/chanlun-py/chanlun/signals/__init__.py +++ b/chanlun-py/chanlun/signals/__init__.py @@ -2,505 +2,13 @@ 每个信号函数接收 观察者 对象 + 关键字参数,返回 OrderedDict。 信号 key 格式:k1_k2_k3,value 格式:v1_v2_v3_score。 - -数据访问路径: -- K线指标:k线.指标.macd.DIF / k线.指标.rsi.RSI / k线.指标.kdj.K / k线.指标.均线["SMA_5"] -- 笔序列:观察员.笔序列(List[虚线]) -- 分型序列:观察员.分型序列(List[分型]) """ -from collections import OrderedDict - -from chanlun.chan import 观察者, 分型结构, 虚线, 相对方向 -from chanlun.chan_external import create_single_signal - - -# 信号函数模板 -def 模板_V日期(观察员: 观察者, **kwargs) -> OrderedDict: - """##信号名称介绍## - - 触发条件:## 触发条件 ## - - 参数模板:## 具体模板 如: "{freq}_D{di}#{ma_type}#{timeperiod}MO{max_overlap}_BS辅助V230313" ## - - **信号逻辑:** - - ## 详细信号逻辑 ## - - **信号列表:** - - ## 具体信号 如下: - - Signal('15分钟_D1#SMA#5MO5_BS辅助V230313_看空_向下_任意_0') - - Signal('15分钟_D1#SMA#5MO5_BS辅助V230313_看多_向下_任意_0') - - Signal('15分钟_D1#SMA#5MO5_BS辅助V230313_看多_向上_任意_0') - - Signal('15分钟_D1#SMA#5MO5_BS辅助V230313_看空_向上_任意_0') - ## - - :param 观察员: 观察者对象 - :param kwargs: 其他参数 - - ## 具体参数介绍 ## - :return: 信号识别结果 - """ - ## 具体代码过程 ## - - return ## create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) ## - - -# ============================================================================== -# tas — 技术指标信号 -# ============================================================================== - - -def tas_ma_base_V230313(c, **kwargs) -> OrderedDict: - """单均线多空和方向辅助开平仓信号 - - 参数模板:"{freq}_D{di}#{ma_type}#{timeperiod}MO{max_overlap}_BS辅助V230313" - - **信号逻辑:** - - 1. close > ma,多头(看多);反之,空头(看空) - 2. ma[-1] > ma[-2],向上;反之,向下 - 3. 加入 max_overlap 参数控制相同信号最大重叠次数 - - **信号列表:** - - - Signal('15分钟_D1#SMA#5MO5_BS辅助V230313_看空_向下_任意_0') - - Signal('15分钟_D1#SMA#5MO5_BS辅助V230313_看多_向下_任意_0') - - Signal('15分钟_D1#SMA#5MO5_BS辅助V230313_看多_向上_任意_0') - - Signal('15分钟_D1#SMA#5MO5_BS辅助V230313_看空_向上_任意_0') - - :param c: 观察者对象 - :param kwargs: 其他参数 - - ma_type: 均线类型(SMA/EMA) - - timeperiod: 均线计算周期 - - di: 信号计算截止倒数第i根K线 - - max_overlap: 相同信号最大重叠次数 - :return: 信号识别结果 - """ - ma_type = kwargs.get("ma_type", "SMA").upper() - timeperiod = int(kwargs.get("timeperiod", 5)) - di = int(kwargs.get("di", 1)) - max_overlap = int(kwargs.get("max_overlap", 5)) - freq = kwargs.get("freq", "15分钟") - - k1, k2, k3 = f"{freq}_D{di}#{ma_type}#{timeperiod}MO{max_overlap}_BS辅助V230313".split("_", 2) - - 普K序列 = c.普通K线序列 - if len(普K序列) < di + 1: - return create_single_signal(k1=k1, k2=k2, k3=k3) - - 当前K线 = 普K序列[-di] - if 当前K线.指标 is None: - return create_single_signal(k1=k1, k2=k2, k3=k3) - - ma_key = f"{ma_type}_{timeperiod}" - 当前均线 = 当前K线.指标.均线.get(ma_key) - if 当前均线 is None: - return create_single_signal(k1=k1, k2=k2, k3=k3) - - 当前价 = 当前K线.收盘价 - v1 = "看多" if 当前价 > 当前均线 else "看空" - - # 均线方向:需要前一根K线的均线值 - if len(普K序列) >= di + 2: - 前K线 = 普K序列[-di - 1] - if 前K线.指标 is not None: - 前均线 = 前K线.指标.均线.get(ma_key) - if 前均线 is not None: - v2 = "向上" if 当前均线 > 前均线 else "向下" - else: - v2 = "任意" - else: - v2 = "任意" - else: - v2 = "任意" - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) - - -def tas_macd_direct_V221106(c, **kwargs) -> OrderedDict: - """MACD 方向信号 — DIF 在零轴上方为多头,下方为空头 - - 参数模板:"{freq}_D{di}#MACD#{fast}#{slow}#{signal}_MACD方向V221106" - - **信号逻辑:** - - 1. DIF > 0,多头;反之,空头 - 2. DIF 值变化趋势(与前一根比较):向上/向下 - - **信号列表:** - - - Signal('15分钟_D1#MACD#13#31#11_MACD方向V221106_看多_向上_任意_0') - - Signal('15分钟_D1#MACD#13#31#11_MACD方向V221106_看多_向下_任意_0') - - Signal('15分钟_D1#MACD#13#31#11_MACD方向V221106_看空_向上_任意_0') - - Signal('15分钟_D1#MACD#13#31#11_MACD方向V221106_看空_向下_任意_0') - - :param c: 观察者对象 - :param kwargs: 其他参数 - - fast: 快线周期(默认 13) - - slow: 慢线周期(默认 31) - - signal: 信号周期(默认 11) - - di: 信号计算截止倒数第i根K线 - :return: 信号识别结果 - """ - fast = int(kwargs.get("fast", 13)) - slow = int(kwargs.get("slow", 31)) - signal = int(kwargs.get("signal", 11)) - di = int(kwargs.get("di", 1)) - freq = kwargs.get("freq", "15分钟") - - k1, k2, k3 = f"{freq}_D{di}#MACD#{fast}#{slow}#{signal}_MACD方向V221106".split("_", 2) - - 普K序列 = c.普通K线序列 - if len(普K序列) < di + 1: - return create_single_signal(k1=k1, k2=k2, k3=k3) - - 当前K线 = 普K序列[-di] - cur_macd = 当前K线.指标.macd if 当前K线.指标 else None - if cur_macd is None or cur_macd.DIF is None: - return create_single_signal(k1=k1, k2=k2, k3=k3) - - v1 = "看多" if cur_macd.DIF > 0 else "看空" - - if len(普K序列) >= di + 2: - 前K线 = 普K序列[-di - 1] - prev_macd = 前K线.指标.macd if 前K线.指标 else None - if prev_macd is not None and prev_macd.DIF is not None: - v2 = "向上" if cur_macd.DIF > prev_macd.DIF else "向下" - else: - v2 = "任意" - else: - v2 = "任意" - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) - - -def macd_金叉(观察员: 观察者, **kwargs) -> OrderedDict: - """MACD 金叉死叉信号 — DIF 与 DEA 的交叉判断 - - 参数模板:"{freq}_D{di}#MACD#{fast}#{slow}#{signal}_MACD交叉V260601" - - **信号逻辑:** - - 1. DIF 上穿 DEA(前一根 DIF <= DEA,当前 DIF > DEA)→ 金叉 - 2. DIF 下穿 DEA(前一根 DIF >= DEA,当前 DIF < DEA)→ 死叉 - - **信号列表:** - - - Signal('15分钟_D1#MACD#13#31#11_MACD交叉V260601_金叉_任意_任意_0') - - Signal('15分钟_D1#MACD#13#31#11_MACD交叉V260601_死叉_任意_任意_0') - - :param 观察员: 观察者对象 - :param kwargs: 其他参数 - - fast: 快线周期(默认 13) - - slow: 慢线周期(默认 31) - - signal: 信号周期(默认 11) - - di: 信号计算截止倒数第i根K线 - :return: 信号识别结果 - """ - fast = int(kwargs.get("fast", 13)) - slow = int(kwargs.get("slow", 31)) - signal = int(kwargs.get("signal", 11)) - di = int(kwargs.get("di", 1)) - freq = kwargs.get("freq", "15分钟") - - k1, k2, k3 = f"{freq}_D{di}#MACD#{fast}#{slow}#{signal}_MACD交叉V260601".split("_", 2) - - 普K序列 = 观察员.普通K线序列 - if len(普K序列) < di + 2: - return create_single_signal(k1=k1, k2=k2, k3=k3) - - 当前K线 = 普K序列[-di] - 前K线 = 普K序列[-di - 1] - - cur_macd = 当前K线.指标.macd if 当前K线.指标 else None - prev_macd = 前K线.指标.macd if 前K线.指标 else None - - if cur_macd is None or prev_macd is None: - return create_single_signal(k1=k1, k2=k2, k3=k3) - if cur_macd.DIF is None or cur_macd.DEA is None: - return create_single_signal(k1=k1, k2=k2, k3=k3) - if prev_macd.DIF is None or prev_macd.DEA is None: - return create_single_signal(k1=k1, k2=k2, k3=k3) - - if prev_macd.DIF <= prev_macd.DEA and cur_macd.DIF > cur_macd.DEA: - v1 = "金叉" - elif prev_macd.DIF >= prev_macd.DEA and cur_macd.DIF < cur_macd.DEA: - v1 = "死叉" - else: - v1 = "任意" - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) - - -# ============================================================================== -# cxt — 缠论形态信号 -# ============================================================================== - - -def cxt_bi_end_V230222(c, **kwargs) -> OrderedDict: - """当前是最后笔的第几次新低底分型或新高顶分型,用于笔结束辅助 - - 触发条件:新分型 - - 参数模板:"{freq}_D1MO{max_overlap}_BE辅助V230222" - - **信号逻辑:** - - 1. 取最后笔及未成笔的分型 - 2. 当前如果是顶分型,则看当前顶分型是否新高,是第几个新高 - 3. 当前如果是底分型,则看当前底分型是否新低,是第几个新低 - - **信号列表:** - - - Signal('日线_D1MO3_BE辅助V230222_新低_第2次_任意_0') - - Signal('日线_D1MO3_BE辅助V230222_新高_第2次_任意_0') - - Signal('日线_D1MO3_BE辅助V230222_新低_第3次_任意_0') - - :param c: 观察者对象 - :param kwargs: - :return: 信号识别结果 - """ - max_overlap = int(kwargs.get("max_overlap", 3)) - freq = kwargs.get("freq", "日线") - k1, k2, k3 = f"{freq}_D1MO{max_overlap}_BE辅助V230222".split("_", 2) - - 分型序列 = c.分型序列 - 笔序列 = c.笔序列 - - if len(分型序列) < 2 or len(笔序列) < 1: - return create_single_signal(k1=k1, k2=k2, k3=k3) - - 最后笔 = 笔序列[-1] - 当前分型 = 分型序列[-1] - - # 找到最后笔的武(终点分型)在分型序列中的位置 - try: - 笔终点索引 = next(i for i, f in enumerate(分型序列) if f.时间戳 == 最后笔.武.时间戳 and f.结构 == 最后笔.武.结构) - except StopIteration: - return create_single_signal(k1=k1, k2=k2, k3=k3) - - # 取笔终点之后的分型(未成笔的分型) - 未成笔分型 = 分型序列[笔终点索引 + 1 :] - if len(未成笔分型) < 1: - return create_single_signal(k1=k1, k2=k2, k3=k3) - - if 当前分型.结构.value == "顶": - # 统计从笔终点到当前的顶分型新高次数 - 笔终点顶高 = 最后笔.武.分型特征值 - 计数 = 0 - for f in 未成笔分型: - if f.结构.value == "顶" and f.分型特征值 > 笔终点顶高: - 计数 += 1 - 笔终点顶高 = f.分型特征值 - if 计数 > 0 and 当前分型.分型特征值 >= 笔终点顶高: - v1, v2 = "新高", f"第{计数}次" - else: - v1, v2 = "任意", "任意" - elif 当前分型.结构.value == "底": - 笔终点底低 = 最后笔.武.分型特征值 - 计数 = 0 - for f in 未成笔分型: - if f.结构.value == "底" and f.分型特征值 < 笔终点底低: - 计数 += 1 - 笔终点底低 = f.分型特征值 - if 计数 > 0 and 当前分型.分型特征值 <= 笔终点底低: - v1, v2 = "新低", f"第{计数}次" - else: - v1, v2 = "任意", "任意" - else: - return create_single_signal(k1=k1, k2=k2, k3=k3) - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) - - -def cxt_停顿分型_V230106(c, **kwargs) -> OrderedDict: - """停顿分型辅助信号 — 结合分型强度和MACD柱子匹配判断 - - 触发条件:新分型 - - 参数模板:"{freq}_D{di}停顿分型_BE辅助V230106" - - **信号逻辑:** - - 判断当前分型是否为停顿分型,结合力度和形态给出信号。 - 停顿分型 = 分型结构为顶/底 + 强度为强/中 + MACD柱子分型匹配。 - - **信号列表:** - - - Signal('1分钟_D0停顿分型_BE辅助V230106_看空_强_任意_0') - - Signal('1分钟_D0停顿分型_BE辅助V230106_看多_强_任意_0') - - Signal('1分钟_D0停顿分型_BE辅助V230106_看空_中_任意_0') - - Signal('1分钟_D0停顿分型_BE辅助V230106_看多_中_任意_0') - - :param c: 观察者对象 - :param kwargs: - :return: 信号识别结果 - """ - di = int(kwargs.get("di", 0)) - freq = kwargs.get("freq", "1分钟") - k1, k2, k3 = f"{freq}_D{di}停顿分型_BE辅助V230106".split("_", 2) - - 分型序列 = c.分型序列 - if len(分型序列) < di + 1: - return create_single_signal(k1=k1, k2=k2, k3=k3) - - 当前分型 = 分型序列[-(di + 1)] - - # 只对顶/底分型产出信号 - if 当前分型.结构.value not in ("顶", "底"): - return create_single_signal(k1=k1, k2=k2, k3=k3) - - v1 = "看空" if 当前分型.结构.value == "顶" else "看多" - v2 = 当前分型.强度() - - # 仅强/中分型 + MACD 柱子匹配时认为是有效的停顿分型 - if v2 in ("强", "中") and 当前分型.与MACD柱子分型匹配(): - pass # 保持 v1, v2 - elif v2 in ("强", "中"): - pass # MACD不匹配也产出,但可能被下游过滤 - else: - v1, v2 = "任意", "任意" - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) - - -def cxt_中枢第三买卖点_V230602(c, **kwargs) -> OrderedDict: - """中枢第三买卖点信号——线段中枢的第三类买卖点识别 - - 触发条件:新中枢 - - 参数模板:"{freq}_D1MO{max_overlap}_中枢第三买卖点V230602" - - **信号逻辑:** - - 1. 取最后一个中枢,仅处理线段中枢(标识="中枢<线段>") - 2. 判断中枢状态(中枢之上→三买,中枢之下→三卖) - 3. 首次穿越0轴:中枢本级第三买卖点后,DIF首次反向穿越0轴并出现对应底/顶分型 - 4. 中枢段DEA穿越2:第三买卖线段内部DEA双向穿越0轴(上穿+下穿均发生) - - **信号列表:** - - - Signal('日线_D1MO3_中枢第三买卖点V230602_首次穿越0轴_三买_任意_0') - - Signal('日线_D1MO3_中枢第三买卖点V230602_首次穿越0轴_三卖_任意_0') - - Signal('日线_D1MO3_中枢第三买卖点V230602_中枢段DEA穿越2_三买_任意_0') - - Signal('日线_D1MO3_中枢第三买卖点V230602_中枢段DEA穿越2_三卖_任意_0') - - :param c: 观察者对象 - :param kwargs: - - max_overlap: 相同信号最大重叠次数 - :return: 信号识别结果 - """ - max_overlap = int(kwargs.get("max_overlap", 3)) - freq = kwargs.get("freq", "日线") - k1, k2, k3 = f"{freq}_D1MO{max_overlap}_中枢第三买卖点V230602".split("_", 2) - - 中枢序列 = c.中枢序列 - if not 中枢序列: - return create_single_signal(k1=k1, k2=k2, k3=k3) - - 当前中枢 = 中枢序列[-1] - if 当前中枢.标识 != "中枢<线段>": - return create_single_signal(k1=k1, k2=k2, k3=k3) - - 状态 = 当前中枢.当前状态() - if 状态 == "中枢之中": - return create_single_signal(k1=k1, k2=k2, k3=k3) - - if 状态 == "中枢之上": - v2 = "三买" - elif 状态 == "中枢之下": - v2 = "三卖" - else: - return create_single_signal(k1=k1, k2=k2, k3=k3) - - v1 = None - - # 1. 首次穿越0轴:本级第三买卖点后,DIF反向穿越0轴并出现对应分型 - if 当前中枢.本级_第三买卖线 is not None and 当前中枢.完整性("合"): - 第三买卖虚线 = 当前中枢.本级_第三买卖线 - 中K线 = 第三买卖虚线.武.中 - 缠K序列 = c.缠论K线序列 - try: - 起点索引 = 缠K序列.index(中K线) - except ValueError: - 起点索引 = 0 - 之后缠K序列 = 缠K序列[起点索引:] - - 之后缠K = None - if 状态 == "中枢之上" and 中K线.标的K线.macd.DIF > 0: - for k in 之后缠K序列: - if k.标的K线.macd.DIF < 0 and 之后缠K is None: - 之后缠K = k - if 之后缠K is not None: - if k.分型 is 分型结构.底 and k.标的K线.macd.DIF < 0: - v1 = "首次穿越0轴" - break - - elif 状态 == "中枢之下" and 中K线.标的K线.macd.DIF < 0: - for k in 之后缠K序列: - if k.标的K线.macd.DIF > 0 and 之后缠K is None: - 之后缠K = k - if 之后缠K is not None: - if k.分型 is 分型结构.顶 and k.标的K线.macd.DIF > 0: - v1 = "首次穿越0轴" - break - - # 2. 中枢段DEA穿越2:第三买卖线段内部DEA双向穿越0轴 - if v1 is None and 当前中枢.第三买卖线 is not None and 当前中枢.完整性("合"): - 第三线 = 当前中枢.第三买卖线 - if 相对方向.分析(当前中枢.高, 当前中枢.低, 第三线.高, 第三线.低).是否缺口(): - 普K序列 = 第三线.获取普K序列(c.观察员) - MACD特性 = 虚线.统计MACD行为(普K序列, 8, 3) - if MACD特性["DEA上穿0"] > 0 and MACD特性["DEA下穿0"] > 0: - v1 = "中枢段DEA穿越2" - - if v1 is None: - return create_single_signal(k1=k1, k2=k2, k3=k3) - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) - - -# ============================================================================== -# bar — K线形态信号 -# ============================================================================== - - -def bar_zdt_V230331(c, **kwargs) -> OrderedDict: - """计算倒数第di根K线的涨跌停信息 - - 参数模板:"{freq}_D{di}_涨跌停V230331" - - **信号逻辑:** - - - close等于high且大于等于前一根K线的close,近似认为是涨停;反之,跌停。 - - **信号列表:** - - - Signal('15分钟_D1_涨跌停V230331_涨停_任意_任意_0') - - Signal('15分钟_D1_涨跌停V230331_跌停_任意_任意_0') - - :param c: 基础周期的观察者对象 - :param kwargs: - - di: 倒数第 di 根 K 线 - :return: 信号识别结果 - """ - di = int(kwargs.get("di", 1)) - freq = kwargs.get("freq", "15分钟") - k1, k2, k3 = f"{freq}_D{di}_涨跌停V230331".split("_", 2) - - 普K序列 = c.普通K线序列 - if len(普K序列) < di + 2: - return create_single_signal(k1=k1, k2=k2, k3=k3) - - 当前K线 = 普K序列[-di] - 前K线 = 普K序列[-di - 1] - - if 当前K线.收盘价 == 当前K线.高 and 当前K线.收盘价 >= 前K线.收盘价: - v1 = "涨停" - elif 当前K线.收盘价 == 当前K线.低 and 当前K线.收盘价 <= 前K线.收盘价: - v1 = "跌停" - else: - v1 = "任意" - - return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) +from chanlun.signals._template import 模板_V日期 +from chanlun.signals.demo import tas_ma_base_V230313 +from chanlun.signals.demo import tas_macd_direct_V221106 +from chanlun.signals.demo import macd_金叉 +from chanlun.signals.demo import cxt_bi_end_V230222 +from chanlun.signals.demo import cxt_停顿分型_V230106 +from chanlun.signals.demo import bar_zdt_V230331 +from chanlun.signals.youwukuncheng import * diff --git a/chanlun-py/chanlun/signals/_template.py b/chanlun-py/chanlun/signals/_template.py new file mode 100644 index 0000000..cb667b0 --- /dev/null +++ b/chanlun-py/chanlun/signals/_template.py @@ -0,0 +1,35 @@ +"""信号函数模板 — 新建信号函数时以此为蓝本""" + +from collections import OrderedDict +from chanlun import 观察者 +from chanlun.chan_external import create_single_signal + + +def 模板_V日期(观察员: 观察者, **kwargs) -> OrderedDict: + """##信号名称介绍## + + 触发条件:## 触发条件,注:当没有此条时则无条件执行 ## + + 参数模板:## 具体模板 如: "{freq}_D{di}#{ma_type}#{timeperiod}MO{max_overlap}_BS辅助V230313" ## + + **信号逻辑:** + + ## 详细信号逻辑 ## + + **信号列表:** + + ## 具体信号 如下: + - Signal('15分钟_D1#SMA#5MO5_BS辅助V230313_看空_向下_任意_0') + - Signal('15分钟_D1#SMA#5MO5_BS辅助V230313_看多_向下_任意_0') + - Signal('15分钟_D1#SMA#5MO5_BS辅助V230313_看多_向上_任意_0') + - Signal('15分钟_D1#SMA#5MO5_BS辅助V230313_看空_向上_任意_0') + ## + + :param 观察员: 观察者对象 + :param kwargs: 其他参数 + - ## 具体参数介绍 ## + :return: 信号识别结果 + """ + ## 具体代码过程 ## + + return ## create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) ## diff --git a/chanlun-py/chanlun/signals/demo.py b/chanlun-py/chanlun/signals/demo.py new file mode 100644 index 0000000..4413946 --- /dev/null +++ b/chanlun-py/chanlun/signals/demo.py @@ -0,0 +1,417 @@ +"""缠论技术分析库 — 信号函数示例合集""" + +from collections import OrderedDict +from typing import List, Optional + +from chanlun import 观察者, 分型结构, 虚线, 线段, 相对方向 +from chanlun.chan_external import create_single_signal + + +# ============================================================================== +# 工具函数 +# ============================================================================== + + +def _按需计算均线(普K序列: List, ma_type: str, timeperiod: int, offset: int = 0) -> Optional[float]: + """当 K线.指标.均线 中无预计算值时,从收盘价序列按需计算均线。 + + :param 普K序列: 普通K线序列 + :param ma_type: 均线类型(SMA/EMA) + :param timeperiod: 均线周期 + :param offset: 从末尾倒数 offset 根K线(0=最后一根,di=倒数第di根) + :return: 均线值,K线不足时返回 None + """ + n = len(普K序列) + start = n - offset - timeperiod + 1 + end = n - offset + 1 + if start < 0: + return None + + closes = [普K序列[i].收盘价 for i in range(start, end)] + + if ma_type == "SMA": + return sum(closes) / len(closes) + elif ma_type == "EMA": + k = 2.0 / (timeperiod + 1) + ema = closes[0] + for price in closes[1:]: + ema = price * k + ema * (1 - k) + return ema + return None + + +def _获取或计算均线(普K序列: List, K线, ma_type: str, timeperiod: int, offset: int) -> Optional[float]: + """从K线指标容器获取均线,若缺失则按需计算。 + + :param 普K序列: 普通K线序列 + :param K线: 目标K线 + :param ma_type: 均线类型 + :param timeperiod: 均线周期 + :param offset: 从末尾倒数 offset 根K线 + :return: 均线值或 None + """ + ma_key = f"{ma_type}_{timeperiod}" + try: + if K线.指标 is not None: + cached = K线.指标.均线.get(ma_key) + if cached is not None: + return cached + except Exception: + pass + return _按需计算均线(普K序列, ma_type, timeperiod, offset) + + +# ============================================================================== +# tas — 技术指标信号 +# ============================================================================== + + +def tas_ma_base_V230313(c, **kwargs) -> OrderedDict: + """单均线多空和方向辅助开平仓信号 + + 参数模板:"{freq}_D{di}#{ma_type}#{timeperiod}MO{max_overlap}_BS辅助V230313" + + **信号逻辑:** + + 1. close > ma,多头(看多);反之,空头(看空) + 2. ma[-1] > ma[-2],向上;反之,向下 + 3. 加入 max_overlap 参数控制相同信号最大重叠次数 + + **信号列表:** + + - Signal('15分钟_D1#SMA#5MO5_BS辅助V230313_看空_向下_任意_0') + - Signal('15分钟_D1#SMA#5MO5_BS辅助V230313_看多_向下_任意_0') + - Signal('15分钟_D1#SMA#5MO5_BS辅助V230313_看多_向上_任意_0') + - Signal('15分钟_D1#SMA#5MO5_BS辅助V230313_看空_向上_任意_0') + + :param c: 观察者对象 + :param kwargs: 其他参数 + - ma_type: 均线类型(SMA/EMA) + - timeperiod: 均线计算周期 + - di: 信号计算截止倒数第i根K线 + - max_overlap: 相同信号最大重叠次数 + :return: 信号识别结果 + """ + ma_type = kwargs.get("ma_type", "SMA").upper() + timeperiod = int(kwargs.get("timeperiod", 5)) + di = int(kwargs.get("di", 1)) + max_overlap = int(kwargs.get("max_overlap", 5)) + freq = kwargs.get("freq", "15分钟") + + k1, k2, k3 = f"{freq}_D{di}#{ma_type}#{timeperiod}MO{max_overlap}_BS辅助V230313".split("_", 2) + + 普K序列 = c.普通K线序列 + if len(普K序列) < di + 1: + return create_single_signal(k1=k1, k2=k2, k3=k3) + + 当前K线 = 普K序列[-di] + 当前均线 = _获取或计算均线(普K序列, 当前K线, ma_type, timeperiod, di) + if 当前均线 is None: + return create_single_signal(k1=k1, k2=k2, k3=k3) + + 当前价 = 当前K线.收盘价 + v1 = "看多" if 当前价 > 当前均线 else "看空" + + # 均线方向:需要前一根K线的均线值 + if len(普K序列) >= di + 2: + 前均线 = _获取或计算均线(普K序列, 普K序列[-di - 1], ma_type, timeperiod, di + 1) + if 前均线 is not None: + v2 = "向上" if 当前均线 > 前均线 else "向下" + else: + v2 = "任意" + else: + v2 = "任意" + + return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) + + +def tas_macd_direct_V221106(c, **kwargs) -> OrderedDict: + """MACD 方向信号 — DIF 在零轴上方为多头,下方为空头 + + 参数模板:"{freq}_D{di}#MACD#{fast}#{slow}#{signal}_MACD方向V221106" + + **信号逻辑:** + + 1. DIF > 0,多头;反之,空头 + 2. DIF 值变化趋势(与前一根比较):向上/向下 + + **信号列表:** + + - Signal('15分钟_D1#MACD#13#31#11_MACD方向V221106_看多_向上_任意_0') + - Signal('15分钟_D1#MACD#13#31#11_MACD方向V221106_看多_向下_任意_0') + - Signal('15分钟_D1#MACD#13#31#11_MACD方向V221106_看空_向上_任意_0') + - Signal('15分钟_D1#MACD#13#31#11_MACD方向V221106_看空_向下_任意_0') + + :param c: 观察者对象 + :param kwargs: 其他参数 + - fast: 快线周期(默认 13) + - slow: 慢线周期(默认 31) + - signal: 信号周期(默认 11) + - di: 信号计算截止倒数第i根K线 + :return: 信号识别结果 + """ + fast = int(kwargs.get("fast", 13)) + slow = int(kwargs.get("slow", 31)) + signal = int(kwargs.get("signal", 11)) + di = int(kwargs.get("di", 1)) + freq = kwargs.get("freq", "15分钟") + + k1, k2, k3 = f"{freq}_D{di}#MACD#{fast}#{slow}#{signal}_MACD方向V221106".split("_", 2) + + 普K序列 = c.普通K线序列 + if len(普K序列) < di + 1: + return create_single_signal(k1=k1, k2=k2, k3=k3) + + 当前K线 = 普K序列[-di] + cur_macd = 当前K线.指标.macd if 当前K线.指标 else None + if cur_macd is None or cur_macd.DIF is None: + return create_single_signal(k1=k1, k2=k2, k3=k3) + + v1 = "看多" if cur_macd.DIF > 0 else "看空" + + if len(普K序列) >= di + 2: + 前K线 = 普K序列[-di - 1] + prev_macd = 前K线.指标.macd if 前K线.指标 else None + if prev_macd is not None and prev_macd.DIF is not None: + v2 = "向上" if cur_macd.DIF > prev_macd.DIF else "向下" + else: + v2 = "任意" + else: + v2 = "任意" + + return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) + + +def macd_金叉(观察员: 观察者, **kwargs) -> OrderedDict: + """MACD 金叉死叉信号 — DIF 与 DEA 的交叉判断 + + 参数模板:"{freq}_D{di}#MACD#{fast}#{slow}#{signal}_MACD交叉V260601" + + **信号逻辑:** + + 1. DIF 上穿 DEA(前一根 DIF <= DEA,当前 DIF > DEA)→ 金叉 + 2. DIF 下穿 DEA(前一根 DIF >= DEA,当前 DIF < DEA)→ 死叉 + + **信号列表:** + + - Signal('15分钟_D1#MACD#13#31#11_MACD交叉V260601_金叉_任意_任意_0') + - Signal('15分钟_D1#MACD#13#31#11_MACD交叉V260601_死叉_任意_任意_0') + + :param 观察员: 观察者对象 + :param kwargs: 其他参数 + - fast: 快线周期(默认 13) + - slow: 慢线周期(默认 31) + - signal: 信号周期(默认 11) + - di: 信号计算截止倒数第i根K线 + :return: 信号识别结果 + """ + fast = int(kwargs.get("fast", 13)) + slow = int(kwargs.get("slow", 31)) + signal = int(kwargs.get("signal", 11)) + di = int(kwargs.get("di", 1)) + freq = kwargs.get("freq", "15分钟") + + k1, k2, k3 = f"{freq}_D{di}#MACD#{fast}#{slow}#{signal}_MACD交叉V260601".split("_", 2) + + 普K序列 = 观察员.普通K线序列 + if len(普K序列) < di + 2: + return create_single_signal(k1=k1, k2=k2, k3=k3) + + 当前K线 = 普K序列[-di] + 前K线 = 普K序列[-di - 1] + + cur_macd = 当前K线.指标.macd if 当前K线.指标 else None + prev_macd = 前K线.指标.macd if 前K线.指标 else None + + if cur_macd is None or prev_macd is None: + return create_single_signal(k1=k1, k2=k2, k3=k3) + if cur_macd.DIF is None or cur_macd.DEA is None: + return create_single_signal(k1=k1, k2=k2, k3=k3) + if prev_macd.DIF is None or prev_macd.DEA is None: + return create_single_signal(k1=k1, k2=k2, k3=k3) + + if prev_macd.DIF <= prev_macd.DEA and cur_macd.DIF > cur_macd.DEA: + v1 = "金叉" + elif prev_macd.DIF >= prev_macd.DEA and cur_macd.DIF < cur_macd.DEA: + v1 = "死叉" + else: + v1 = "任意" + + return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) + + +# ============================================================================== +# cxt — 缠论形态信号 +# ============================================================================== + + +def cxt_bi_end_V230222(c, **kwargs) -> OrderedDict: + """当前是最后笔的第几次新低底分型或新高顶分型,用于笔结束辅助 + + 触发条件:新分型 + + 参数模板:"{freq}_D1MO{max_overlap}_BE辅助V230222" + + **信号逻辑:** + + 1. 取最后笔及未成笔的分型 + 2. 当前如果是顶分型,则看当前顶分型是否新高,是第几个新高 + 3. 当前如果是底分型,则看当前底分型是否新低,是第几个新低 + + **信号列表:** + + - Signal('日线_D1MO3_BE辅助V230222_新低_第2次_任意_0') + - Signal('日线_D1MO3_BE辅助V230222_新高_第2次_任意_0') + - Signal('日线_D1MO3_BE辅助V230222_新低_第3次_任意_0') + + :param c: 观察者对象 + :param kwargs: + :return: 信号识别结果 + """ + max_overlap = int(kwargs.get("max_overlap", 3)) + freq = kwargs.get("freq", "日线") + k1, k2, k3 = f"{freq}_D1MO{max_overlap}_BE辅助V230222".split("_", 2) + + 分型序列 = c.分型序列 + 笔序列 = c.笔序列 + + if len(分型序列) < 2 or len(笔序列) < 1: + return create_single_signal(k1=k1, k2=k2, k3=k3) + + 最后笔 = 笔序列[-1] + 当前分型 = 分型序列[-1] + + # 找到最后笔的武(终点分型)在分型序列中的位置 + try: + 笔终点索引 = next(i for i, f in enumerate(分型序列) if f.时间戳 == 最后笔.武.时间戳 and f.结构 == 最后笔.武.结构) + except StopIteration: + return create_single_signal(k1=k1, k2=k2, k3=k3) + + # 取笔终点之后的分型(未成笔的分型) + 未成笔分型 = 分型序列[笔终点索引 + 1 :] + if len(未成笔分型) < 1: + return create_single_signal(k1=k1, k2=k2, k3=k3) + + if 当前分型.结构.value == "顶": + # 统计从笔终点到当前的顶分型新高次数 + 笔终点顶高 = 最后笔.武.分型特征值 + 计数 = 0 + for f in 未成笔分型: + if f.结构.value == "顶" and f.分型特征值 > 笔终点顶高: + 计数 += 1 + 笔终点顶高 = f.分型特征值 + if 计数 > 0 and 当前分型.分型特征值 >= 笔终点顶高: + v1, v2 = "新高", f"第{计数}次" + else: + v1, v2 = "任意", "任意" + elif 当前分型.结构.value == "底": + 笔终点底低 = 最后笔.武.分型特征值 + 计数 = 0 + for f in 未成笔分型: + if f.结构.value == "底" and f.分型特征值 < 笔终点底低: + 计数 += 1 + 笔终点底低 = f.分型特征值 + if 计数 > 0 and 当前分型.分型特征值 <= 笔终点底低: + v1, v2 = "新低", f"第{计数}次" + else: + v1, v2 = "任意", "任意" + else: + return create_single_signal(k1=k1, k2=k2, k3=k3) + + return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) + + +def cxt_停顿分型_V230106(c, **kwargs) -> OrderedDict: + """停顿分型辅助信号 — 结合分型强度和MACD柱子匹配判断 + + 触发条件:新分型 + + 参数模板:"{freq}_D{di}停顿分型_BE辅助V230106" + + **信号逻辑:** + + 判断当前分型是否为停顿分型,结合力度和形态给出信号。 + 停顿分型 = 分型结构为顶/底 + 强度为强/中 + MACD柱子分型匹配。 + + **信号列表:** + + - Signal('1分钟_D0停顿分型_BE辅助V230106_看空_强_任意_0') + - Signal('1分钟_D0停顿分型_BE辅助V230106_看多_强_任意_0') + - Signal('1分钟_D0停顿分型_BE辅助V230106_看空_中_任意_0') + - Signal('1分钟_D0停顿分型_BE辅助V230106_看多_中_任意_0') + + :param c: 观察者对象 + :param kwargs: + :return: 信号识别结果 + """ + di = int(kwargs.get("di", 0)) + freq = kwargs.get("freq", "1分钟") + k1, k2, k3 = f"{freq}_D{di}停顿分型_BE辅助V230106".split("_", 2) + + 分型序列 = c.分型序列 + if len(分型序列) < di + 1: + return create_single_signal(k1=k1, k2=k2, k3=k3) + + 当前分型 = 分型序列[-(di + 1)] + + # 只对顶/底分型产出信号 + if 当前分型.结构.value not in ("顶", "底"): + return create_single_signal(k1=k1, k2=k2, k3=k3) + + v1 = "看空" if 当前分型.结构.value == "顶" else "看多" + v2 = 当前分型.强度() + + # 仅强/中分型 + MACD 柱子匹配时认为是有效的停顿分型 + if v2 in ("强", "中") and 当前分型.与MACD柱子分型匹配(): + pass # 保持 v1, v2 + elif v2 in ("强", "中"): + pass # MACD不匹配也产出,但可能被下游过滤 + else: + v1, v2 = "任意", "任意" + + return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) + + +# ============================================================================== +# bar — K线形态信号 +# ============================================================================== + + +def bar_zdt_V230331(c, **kwargs) -> OrderedDict: + """计算倒数第di根K线的涨跌停信息 + + 参数模板:"{freq}_D{di}_涨跌停V230331" + + **信号逻辑:** + + - close等于high且大于等于前一根K线的close,近似认为是涨停;反之,跌停。 + + **信号列表:** + + - Signal('15分钟_D1_涨跌停V230331_涨停_任意_任意_0') + - Signal('15分钟_D1_涨跌停V230331_跌停_任意_任意_0') + + :param c: 基础周期的观察者对象 + :param kwargs: + - di: 倒数第 di 根 K 线 + :return: 信号识别结果 + """ + di = int(kwargs.get("di", 1)) + freq = kwargs.get("freq", "15分钟") + k1, k2, k3 = f"{freq}_D{di}_涨跌停V230331".split("_", 2) + + 普K序列 = c.普通K线序列 + if len(普K序列) < di + 2: + return create_single_signal(k1=k1, k2=k2, k3=k3) + + 当前K线 = 普K序列[-di] + 前K线 = 普K序列[-di - 1] + + if 当前K线.收盘价 == 当前K线.高 and 当前K线.收盘价 >= 前K线.收盘价: + v1 = "涨停" + elif 当前K线.收盘价 == 当前K线.低 and 当前K线.收盘价 <= 前K线.收盘价: + v1 = "跌停" + else: + v1 = "任意" + + return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1) diff --git a/chanlun-py/pyproject.toml b/chanlun-py/pyproject.toml index e1cca15..94b4c31 100644 --- a/chanlun-py/pyproject.toml +++ b/chanlun-py/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "chanlun" -version = "2606.47" +version = "2606.125" 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 a55e5fd..9c7c798 100644 --- a/chanlun-py/src/algorithm_py.rs +++ b/chanlun-py/src/algorithm_py.rs @@ -26,33 +26,20 @@ use crate::kline_py::chan_kline_to_py; use crate::structure_py::{dashed_to_py, fractal_to_py}; use pyo3::prelude::*; use pyo3::types::{PyDict, PyList, PyType}; -use std::collections::HashMap; use std::sync::Arc; -use std::sync::RwLock; use std::sync::atomic::Ordering; -// 使用全局 static 而非 thread_local!,保证跨线程对象标识一致性 -static HUB_IDENTITY: std::sync::LazyLock>>> = - std::sync::LazyLock::new(|| RwLock::new(HashMap::new())); +// 缓存通过 crate::cache 模块管理(支持 thread_local / global 运行时切换) pub(crate) fn hub_to_py( py: Python<'_>, inner: Arc ) -> Py<中枢Py> { let key = Arc::as_ptr(&inner) as usize; - if let Some(cached) = HUB_IDENTITY - .read() - .unwrap() - .get(&key) - .map(|p| p.clone_ref(py)) - { + if let Some(cached) = crate::cache::hub_get(py, key) { return cached; } - HUB_IDENTITY - .write() - .unwrap() - .retain(|_, v| v.get_refcnt(py) > 1); let obj = Py::new(py, 中枢Py { inner }).unwrap(); - HUB_IDENTITY.write().unwrap().insert(key, obj.clone_ref(py)); + crate::cache::hub_insert(py, key, &obj); obj } @@ -90,16 +77,21 @@ impl 背驰分析Py { 方式: &str, py: Python<'_>, ) -> bool { + let 进入段_inner = Arc::clone(&进入段.borrow().inner); + let 离开段_inner = Arc::clone(&离开段.borrow().inner); + let 方式 = 方式.to_string(); let rc_list: Vec> = K线序列 .iter() .map(|k| k.bind(py).borrow().inner.clone()) .collect(); - chanlun::algorithm::divergence::背驰分析::MACD背驰( - &进入段.borrow().inner, - &离开段.borrow().inner, - &rc_list, - 方式, - ) + py.detach(move || { + chanlun::algorithm::divergence::背驰分析::MACD背驰( + &进入段_inner, + &离开段_inner, + &rc_list, + &方式, + ) + }) } #[classmethod] @@ -137,15 +129,19 @@ impl 背驰分析Py { 普K序列: Vec>, py: Python<'_>, ) -> bool { + let 进入段_inner = Arc::clone(&进入段.borrow().inner); + let 离开段_inner = Arc::clone(&离开段.borrow().inner); let rc_list: Vec> = 普K序列 .iter() .map(|k| k.bind(py).borrow().inner.clone()) .collect(); - chanlun::algorithm::divergence::背驰分析::全量背驰( - &进入段.borrow().inner, - &离开段.borrow().inner, - &rc_list, - ) + py.detach(move || { + chanlun::algorithm::divergence::背驰分析::全量背驰( + &进入段_inner, + &离开段_inner, + &rc_list, + ) + }) } #[classmethod] @@ -157,15 +153,19 @@ impl 背驰分析Py { 普K序列: Vec>, py: Python<'_>, ) -> bool { + let 进入段_inner = Arc::clone(&进入段.borrow().inner); + let 离开段_inner = Arc::clone(&离开段.borrow().inner); let rc_list: Vec> = 普K序列 .iter() .map(|k| k.bind(py).borrow().inner.clone()) .collect(); - chanlun::algorithm::divergence::背驰分析::任意背驰( - &进入段.borrow().inner, - &离开段.borrow().inner, - &rc_list, - ) + py.detach(move || { + chanlun::algorithm::divergence::背驰分析::任意背驰( + &进入段_inner, + &离开段_inner, + &rc_list, + ) + }) } #[classmethod] @@ -178,17 +178,21 @@ impl 背驰分析Py { 配置: &Bound<'_, 缠论配置Py>, py: Python<'_>, ) -> PyResult { + let 进入段_inner = Arc::clone(&进入段.borrow().inner); + let 离开段_inner = Arc::clone(&离开段.borrow().inner); let rc_list: Vec> = 普K序列 .iter() .map(|k| k.bind(py).borrow().inner.clone()) .collect(); let config = 配置.borrow().to_rust_config(py)?; - Ok(chanlun::algorithm::divergence::背驰分析::配置背驰( - &进入段.borrow().inner, - &离开段.borrow().inner, - &rc_list, - &config, - )) + Ok(py.detach(move || { + chanlun::algorithm::divergence::背驰分析::配置背驰( + &进入段_inner, + &离开段_inner, + &rc_list, + &config, + ) + })) } #[classmethod] @@ -200,15 +204,19 @@ impl 背驰分析Py { 普K序列: Vec>, py: Python<'_>, ) -> bool { + let 进入段_inner = Arc::clone(&进入段.borrow().inner); + let 离开段_inner = Arc::clone(&离开段.borrow().inner); let rc_list: Vec> = 普K序列 .iter() .map(|k| k.bind(py).borrow().inner.clone()) .collect(); - chanlun::algorithm::divergence::背驰分析::任选背驰( - &进入段.borrow().inner, - &离开段.borrow().inner, - &rc_list, - ) + py.detach(move || { + chanlun::algorithm::divergence::背驰分析::任选背驰( + &进入段_inner, + &离开段_inner, + &rc_list, + ) + }) } #[classmethod] @@ -222,18 +230,169 @@ impl 背驰分析Py { 模式: &str, py: Python<'_>, ) -> PyResult { + let 进入段_inner = Arc::clone(&进入段.borrow().inner); + let 离开段_inner = Arc::clone(&离开段.borrow().inner); let rc_list: Vec> = 普K序列 .iter() .map(|k| k.bind(py).borrow().inner.clone()) .collect(); let config = 配置.borrow().to_rust_config(py)?; - Ok(chanlun::algorithm::divergence::背驰分析::背驰模式( - &进入段.borrow().inner, - &离开段.borrow().inner, - &rc_list, - &config, - 模式, - )) + let 模式 = 模式.to_string(); + Ok(py.detach(move || { + chanlun::algorithm::divergence::背驰分析::背驰模式( + &进入段_inner, + &离开段_inner, + &rc_list, + &config, + &模式, + ) + })) + } + + // ---- 观察者直传(跳过 Python list→Vec 转换,直接借用观察者内部 &[Arc]) ---- + + #[classmethod] + #[pyo3(name = "MACD背驰_OBS", signature = (进入段, 离开段, 观察员, 方式 = "总"))] + fn MACD背驰_obs( + _cls: &Bound<'_, PyType>, + 进入段: &Bound<'_, 虚线Py>, + 离开段: &Bound<'_, 虚线Py>, + 观察员: &Bound<'_, 观察者Py>, + 方式: &str, + py: Python<'_>, + ) -> bool { + let 进入段_inner = Arc::clone(&进入段.borrow().inner); + let 离开段_inner = Arc::clone(&离开段.borrow().inner); + let 方式 = 方式.to_string(); + let obs_arc = 观察员.borrow().inner.clone().expect("观察者未初始化"); + py.detach(move || { + let guard = obs_arc.read(); + chanlun::algorithm::divergence::背驰分析::MACD背驰( + &进入段_inner, + &离开段_inner, + &guard.普通K线序列, + &方式, + ) + }) + } + + #[classmethod] + #[pyo3(name = "全量背驰_OBS", signature = (进入段, 离开段, 观察员))] + fn 全量背驰_obs( + _cls: &Bound<'_, PyType>, + 进入段: &Bound<'_, 虚线Py>, + 离开段: &Bound<'_, 虚线Py>, + 观察员: &Bound<'_, 观察者Py>, + py: Python<'_>, + ) -> bool { + let 进入段_inner = Arc::clone(&进入段.borrow().inner); + let 离开段_inner = Arc::clone(&离开段.borrow().inner); + let obs_arc = 观察员.borrow().inner.clone().expect("观察者未初始化"); + py.detach(move || { + let guard = obs_arc.read(); + chanlun::algorithm::divergence::背驰分析::全量背驰( + &进入段_inner, + &离开段_inner, + &guard.普通K线序列, + ) + }) + } + + #[classmethod] + #[pyo3(name = "任意背驰_OBS", signature = (进入段, 离开段, 观察员))] + fn 任意背驰_obs( + _cls: &Bound<'_, PyType>, + 进入段: &Bound<'_, 虚线Py>, + 离开段: &Bound<'_, 虚线Py>, + 观察员: &Bound<'_, 观察者Py>, + py: Python<'_>, + ) -> bool { + let 进入段_inner = Arc::clone(&进入段.borrow().inner); + let 离开段_inner = Arc::clone(&离开段.borrow().inner); + let obs_arc = 观察员.borrow().inner.clone().expect("观察者未初始化"); + py.detach(move || { + let guard = obs_arc.read(); + chanlun::algorithm::divergence::背驰分析::任意背驰( + &进入段_inner, + &离开段_inner, + &guard.普通K线序列, + ) + }) + } + + #[classmethod] + #[pyo3(name = "配置背驰_OBS", signature = (进入段, 离开段, 观察员, 配置))] + fn 配置背驰_obs( + _cls: &Bound<'_, PyType>, + 进入段: &Bound<'_, 虚线Py>, + 离开段: &Bound<'_, 虚线Py>, + 观察员: &Bound<'_, 观察者Py>, + 配置: &Bound<'_, 缠论配置Py>, + py: Python<'_>, + ) -> PyResult { + let 进入段_inner = Arc::clone(&进入段.borrow().inner); + let 离开段_inner = Arc::clone(&离开段.borrow().inner); + let config = 配置.borrow().to_rust_config(py)?; + let obs_arc = 观察员.borrow().inner.clone().expect("观察者未初始化"); + Ok(py.detach(move || { + let guard = obs_arc.read(); + chanlun::algorithm::divergence::背驰分析::配置背驰( + &进入段_inner, + &离开段_inner, + &guard.普通K线序列, + &config, + ) + })) + } + + #[classmethod] + #[pyo3(name = "任选背驰_OBS", signature = (进入段, 离开段, 观察员))] + fn 任选背驰_obs( + _cls: &Bound<'_, PyType>, + 进入段: &Bound<'_, 虚线Py>, + 离开段: &Bound<'_, 虚线Py>, + 观察员: &Bound<'_, 观察者Py>, + py: Python<'_>, + ) -> bool { + let 进入段_inner = Arc::clone(&进入段.borrow().inner); + let 离开段_inner = Arc::clone(&离开段.borrow().inner); + let obs_arc = 观察员.borrow().inner.clone().expect("观察者未初始化"); + py.detach(move || { + let guard = obs_arc.read(); + chanlun::algorithm::divergence::背驰分析::任选背驰( + &进入段_inner, + &离开段_inner, + &guard.普通K线序列, + ) + }) + } + + #[classmethod] + #[pyo3(name = "背驰模式_OBS", signature = (进入段, 离开段, 观察员, 配置, 模式))] + fn 背驰模式_obs( + _cls: &Bound<'_, PyType>, + 进入段: &Bound<'_, 虚线Py>, + 离开段: &Bound<'_, 虚线Py>, + 观察员: &Bound<'_, 观察者Py>, + 配置: &Bound<'_, 缠论配置Py>, + 模式: &str, + py: Python<'_>, + ) -> PyResult { + let 进入段_inner = Arc::clone(&进入段.borrow().inner); + let 离开段_inner = Arc::clone(&离开段.borrow().inner); + let config = 配置.borrow().to_rust_config(py)?; + let 模式 = 模式.to_string(); + let obs_arc = 观察员.borrow().inner.clone().expect("观察者未初始化"); + Ok(py.detach(move || { + let guard = obs_arc.read(); + chanlun::algorithm::divergence::背驰分析::背驰模式( + &进入段_inner, + &离开段_inner, + &guard.普通K线序列, + &config, + &模式, + ) + })) } } @@ -345,7 +504,7 @@ impl 笔Py { .map(|k| k.bind(py).borrow().inner.clone()) .collect(); let config = 配置.borrow().to_rust_config(py)?; - let depth = match 当前分型_rc { + let depth = py.detach(|| match 当前分型_rc { Some(fr) => chanlun::algorithm::bi::笔::分析( fr, &mut fr_seq, @@ -356,17 +515,21 @@ impl 笔Py { &config, ), None => 递归层次, - }; + }); - // 写回 Python 列表 + // 写回 Python 列表 (bulk extend) + let fr_items: Vec> = fr_seq + .iter() + .map(|f| fractal_to_py(py, Arc::clone(f)).into_any()) + .collect(); 分型序列.call_method0("clear")?; - for f in fr_seq { - 分型序列.call_method1("append", (fractal_to_py(py, f),))?; - } + 分型序列.call_method1("extend", (PyList::new(py, &fr_items)?,))?; + let bi_items: Vec> = bi_seq + .iter() + .map(|d| dashed_to_py(py, Arc::clone(d)).into_any()) + .collect(); 笔序列.call_method0("clear")?; - for d in bi_seq { - 笔序列.call_method1("append", (dashed_to_py(py, d),))?; - } + 笔序列.call_method1("extend", (PyList::new(py, &bi_items)?,))?; Ok(depth) } @@ -487,7 +650,10 @@ impl 线段Py { Some(&hub_ref.inner), ) } else { - chanlun::algorithm::segment::线段::分割序列(&borrowed.inner, None) + return Err(pyo3::exceptions::PyTypeError::new_err(format!( + "线段.分割序列 的 所属中枢 参数必须是 chanlun._chanlun.中枢 (Rust PyO3 对象),收到了 {}。请使用 chanlun.chan.线段.分割序列 (Python 版) 处理 Python 中枢对象。", + hub_bound.get_type().name()?, + ))); } } else { chanlun::algorithm::segment::线段::分割序列(&borrowed.inner, None) @@ -552,19 +718,23 @@ impl 线段Py { let rel_list: Vec = 关系序列 .map(|v| v.into_iter().map(|d| d.inner).collect()) .unwrap_or(default_rel); - chanlun::algorithm::segment::线段::分析( - &bi_list, - &mut seg_seq, - &config, - 层级, - &rel_list, - ); + py.detach(|| { + chanlun::algorithm::segment::线段::分析( + &bi_list, + &mut seg_seq, + &config, + 层级, + &rel_list, + ); + }); - // 写回 Python 列表 + // 写回 Python 列表 (bulk extend) + let items: Vec> = seg_seq + .iter() + .map(|d| dashed_to_py(py, Arc::clone(d)).into_any()) + .collect(); 线段序列.call_method0("clear")?; - for d in seg_seq { - 线段序列.call_method1("append", (dashed_to_py(py, d),))?; - } + 线段序列.call_method1("extend", (PyList::new(py, &items)?,))?; Ok(()) } @@ -590,13 +760,17 @@ impl 线段Py { } let config = 配置.borrow().to_rust_config(py)?; - chanlun::algorithm::segment::线段::扩展分析(&dash_list, &mut seg_seq, &config); + py.detach(|| { + chanlun::algorithm::segment::线段::扩展分析(&dash_list, &mut seg_seq, &config); + }); - // 写回 Python 列表 + // 写回 Python 列表 (bulk extend) + let items: Vec> = seg_seq + .iter() + .map(|d| dashed_to_py(py, Arc::clone(d)).into_any()) + .collect(); 线段序列.call_method0("clear")?; - for d in seg_seq { - 线段序列.call_method1("append", (dashed_to_py(py, d),))?; - } + 线段序列.call_method1("extend", (PyList::new(py, &items)?,))?; Ok(()) } @@ -714,7 +888,7 @@ impl 中枢Py { #[getter] fn 标识(&self) -> String { - self.inner.标识.read().unwrap().clone() + self.inner.标识.read().clone() } #[getter] @@ -725,7 +899,7 @@ impl 中枢Py { #[getter] fn 基础序列(&self, py: Python<'_>) -> PyResult> { let list = pyo3::types::PyList::empty(py); - for d in self.inner.基础序列.read().unwrap().iter() { + for d in self.inner.基础序列.read().iter() { list.append(dashed_to_py(py, Arc::clone(d)))?; } Ok(list.into()) @@ -736,7 +910,6 @@ impl 中枢Py { self.inner .第三买卖线 .read() - .unwrap() .as_ref() .map(|d| dashed_to_py(py, Arc::clone(d))) } @@ -746,7 +919,6 @@ impl 中枢Py { self.inner .本级_第三买卖线 .read() - .unwrap() .as_ref() .map(|d| dashed_to_py(py, Arc::clone(d))) } @@ -845,13 +1017,17 @@ impl 中枢Py { hub_seq.push(Arc::clone(&h.inner)); } let config = 配置.borrow().to_rust_config(配置.py())?; - self.inner.获取扩展中枢(&mut hub_seq, &config); + py.detach(|| { + self.inner.获取扩展中枢(&mut hub_seq, &config); + }); - // 写回 Python 列表 + // 写回 Python 列表 (bulk extend) + let items: Vec> = hub_seq + .iter() + .map(|h| hub_to_py(py, Arc::clone(h)).into_any()) + .collect(); 扩展中枢.call_method0("clear")?; - for h in hub_seq { - 扩展中枢.call_method1("append", (hub_to_py(py, h),))?; - } + 扩展中枢.call_method1("extend", (PyList::new(py, &items)?,))?; Ok(()) } @@ -958,13 +1134,17 @@ impl 中枢Py { hub_seq.push(Arc::clone(&h.inner)); } - chanlun::algorithm::hub::中枢::分析(&rc_list, &mut hub_seq, 跳过首部, 标识, 层级); + py.detach(|| { + chanlun::algorithm::hub::中枢::分析(&rc_list, &mut hub_seq, 跳过首部, 标识, 层级); + }); - // 写回 Python 列表 + // 写回 Python 列表 (bulk extend) + let items: Vec> = hub_seq + .iter() + .map(|h| hub_to_py(py, Arc::clone(h)).into_any()) + .collect(); 中枢序列.call_method0("clear")?; - for h in hub_seq { - 中枢序列.call_method1("append", (hub_to_py(py, h),))?; - } + 中枢序列.call_method1("extend", (PyList::new(py, &items)?,))?; Ok(()) } diff --git a/chanlun-py/src/business_py.rs b/chanlun-py/src/business_py.rs index 582d0c3..386d99d 100644 --- a/chanlun-py/src/business_py.rs +++ b/chanlun-py/src/business_py.rs @@ -22,15 +22,16 @@ * SOFTWARE. */ +use parking_lot::RwLock; use pyo3::prelude::*; use pyo3::types::{PyDict, PyType}; -use std::sync::RwLock; use crate::algorithm_py::hub_to_py; use crate::kline_py::bar_to_py; use crate::structure_py::{dashed_to_py, fractal_to_py, 分型Py}; use std::collections::HashMap; use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; use crate::config_py::缠论配置Py; use crate::kline_py::{K线Py, 缠论K线Py}; @@ -693,28 +694,27 @@ impl 买卖点Py { #[pyclass(name = "观察者", module = "chanlun._chanlun", subclass)] pub struct 观察者Py { pub(crate) inner: Option>>, - 配置缓存: std::sync::Mutex>>, + 配置缓存: parking_lot::Mutex>>, + 最后配置版本: AtomicU64, } impl 观察者Py { pub(crate) fn obs( &self, - ) -> std::sync::RwLockReadGuard<'_, chanlun::business::observer::观察者> { + ) -> parking_lot::RwLockReadGuard<'_, chanlun::business::observer::观察者> { self.inner .as_ref() .expect("观察者 尚未初始化,请通过 __init__(符号, 周期, 配置) 构造") .read() - .unwrap_or_else(|e| e.into_inner()) } pub(crate) fn obs_mut( &self, - ) -> std::sync::RwLockWriteGuard<'_, chanlun::business::observer::观察者> { + ) -> parking_lot::RwLockWriteGuard<'_, chanlun::business::observer::观察者> { self.inner .as_ref() .expect("观察者 尚未初始化,请通过 __init__(符号, 周期, 配置) 构造") .write() - .unwrap_or_else(|e| e.into_inner()) } } @@ -770,7 +770,8 @@ impl 观察者Py { inner: Some(chanlun::business::observer::观察者::new( 符号, 周期, config, )), - 配置缓存: std::sync::Mutex::new(None), + 配置缓存: parking_lot::Mutex::new(None), + 最后配置版本: AtomicU64::new(0), }) } @@ -824,7 +825,7 @@ impl 观察者Py { #[getter] fn 配置(&self, py: Python<'_>) -> PyResult> { - let mut cache = self.配置缓存.lock().unwrap(); + let mut cache = self.配置缓存.lock(); if let Some(ref cached) = *cache { Ok(cached.clone_ref(py)) } else { @@ -838,11 +839,8 @@ impl 观察者Py { #[setter] fn set_配置(&self, value: &Bound<'_, 缠论配置Py>) -> PyResult<()> { let config = value.borrow().to_rust_config(value.py())?; + *self.配置缓存.lock() = Some(value.clone().unbind()); self.obs_mut().配置 = config; - self.配置缓存 - .lock() - .unwrap() - .replace(value.clone().unbind()); Ok(()) } @@ -861,25 +859,53 @@ impl 观察者Py { /// 核心入口 — 投喂一根原始K线,增量更新所有层级(内部实现) #[pyo3(name = "_增加原始K线")] - fn 增加原始K线_impl(&mut self, 普K: &Bound<'_, K线Py>) -> PyResult<()> { - self.obs_mut().增加原始K线((*普K.borrow().inner).clone()); - Ok(()) + fn 增加原始K线_impl(slf: &Bound<'_, Self>, 普K: &Bound<'_, K线Py>) -> PyResult<()> { + let kline = (*普K.borrow().inner).clone(); + let obs_arc = slf + .borrow() + .inner + .clone() + .expect("观察者 尚未初始化,请通过 __init__(符号, 周期, 配置) 构造"); + let symbol = obs_arc.read().符号.clone(); + let result = slf.py().detach(move || { + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + obs_arc.write().增加原始K线(kline); + })) + }); + match result { + Ok(()) => Ok(()), + Err(e) => { + let msg = e + .downcast_ref::<&str>() + .map(|s| s.to_string()) + .or_else(|| e.downcast_ref::().cloned()) + .unwrap_or_else(|| "未知算法错误".into()); + Err(pyo3::exceptions::PyRuntimeError::new_err(format!( + "[{symbol}] 算法异常: {msg}" + ))) + } + } } /// 核心入口 — 投喂一根原始K线,增量更新所有层级(公开分发器,支持子类重写) fn 增加原始K线(slf: &Bound<'_, Self>, 普K: &Bound<'_, K线Py>) -> PyResult<()> { - // 同步缓存的 Python 配置到 Rust 观察者(支持 obs.配置 直接修改) + // 版本对比,仅在配置变更时同步(支持 obs.配置.field = value 直接修改) { let me = slf.borrow(); - if let Some(ref cached) = *me.配置缓存.lock().unwrap() { + if let Some(ref cached) = *me.配置缓存.lock() { let py = slf.py(); - if let Ok(config) = cached.bind(py).borrow().to_rust_config(py) { - me.obs_mut().配置 = config; + let cfg_ref = cached.bind(py).borrow(); + let current_version = cfg_ref.版本.load(Ordering::Relaxed); + if current_version != me.最后配置版本.load(Ordering::Relaxed) { + if let Ok(config) = cfg_ref.to_rust_config(py) { + me.obs_mut().配置 = config; + } + me.最后配置版本.store(current_version, Ordering::Relaxed); } } } - slf.call_method1("_增加原始K线", (普K,))?; - Ok(()) + // 直接调用 Rust 实现,跳过 Python dispatch + Self::增加原始K线_impl(slf, 普K) } /// 投喂原始数据 — 便捷入口,直接从 OHLCV 创建 K线 并通过 Python 分发 增加原始K线, @@ -931,9 +957,31 @@ impl 观察者Py { /// 静态重新分析(内部实现) #[pyo3(name = "_静态重新分析")] - fn 静态重新分析_impl(&mut self) -> PyResult<()> { - self.obs_mut().静态重新分析(); - Ok(()) + fn 静态重新分析_impl(slf: &Bound<'_, Self>) -> PyResult<()> { + let obs_arc = slf + .borrow() + .inner + .clone() + .expect("观察者 尚未初始化,请通过 __init__(符号, 周期, 配置) 构造"); + let symbol = obs_arc.read().符号.clone(); + let result = slf.py().detach(move || { + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + obs_arc.write().静态重新分析(); + })) + }); + match result { + Ok(()) => Ok(()), + Err(e) => { + let msg = e + .downcast_ref::<&str>() + .map(|s| s.to_string()) + .or_else(|| e.downcast_ref::().cloned()) + .unwrap_or_else(|| "未知算法错误".into()); + Err(pyo3::exceptions::PyRuntimeError::new_err(format!( + "[{symbol}] 静态重新分析异常: {msg}" + ))) + } + } } /// 静态重新分析(公开分发器,支持子类重写) @@ -1413,7 +1461,8 @@ impl 立体分析器Py { for (周期, obs_rc) in &self.inner.单体分析器 { let obs_py = 观察者Py { inner: Some(obs_rc.clone()), - 配置缓存: std::sync::Mutex::new(None), + 配置缓存: parking_lot::Mutex::new(None), + 最后配置版本: AtomicU64::new(0), }; dict.set_item(周期, obs_py)?; } diff --git a/chanlun-py/src/cache.rs b/chanlun-py/src/cache.rs new file mode 100644 index 0000000..cbe8b1a --- /dev/null +++ b/chanlun-py/src/cache.rs @@ -0,0 +1,223 @@ +/* + * 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 dashmap::DashMap; +use pyo3::prelude::*; +use pyo3::types::PySet; +/// 缓存模式:线程局部(默认,零锁)或全局(dashmap,跨线程共享) +use std::cell::RefCell; +use std::collections::HashMap; +use std::sync::OnceLock; + +pub enum CacheMode { + ThreadLocal, + Global, +} + +static CACHE_MODE: OnceLock = OnceLock::new(); + +pub fn get_mode() -> &'static CacheMode { + CACHE_MODE.get_or_init(|| match std::env::var("CHANLUN_CACHE_MODE").as_deref() { + Ok("global") => CacheMode::Global, + _ => CacheMode::ThreadLocal, + }) +} + +pub fn peek_mode() -> Option<&'static CacheMode> { + CACHE_MODE.get() +} + +pub fn set_mode(mode: CacheMode) -> Result<(), String> { + CACHE_MODE + .set(mode) + .map_err(|_| "缓存模式已初始化,请在创建任何观察者之前调用 set_cache_mode".into()) +} + +// ========== BAR_IDENTITY ========== +thread_local! { + static BAR_LOCAL: RefCell>> = RefCell::new(HashMap::new()); +} +static BAR_GLOBAL: std::sync::LazyLock>> = + std::sync::LazyLock::new(DashMap::new); + +pub fn bar_get(py: Python<'_>, key: usize) -> Option> { + match get_mode() { + CacheMode::ThreadLocal => BAR_LOCAL.with(|m| m.borrow().get(&key).map(|p| p.clone_ref(py))), + CacheMode::Global => BAR_GLOBAL.get(&key).map(|p| p.clone_ref(py)), + } +} +pub fn bar_insert(py: Python<'_>, key: usize, obj: &Py) { + match get_mode() { + CacheMode::ThreadLocal => BAR_LOCAL.with(|m| { + let mut m = m.borrow_mut(); + m.retain(|_, v| v.get_refcnt(py) > 1); + m.insert(key, obj.clone_ref(py)); + }), + CacheMode::Global => { + BAR_GLOBAL.retain(|_, v| v.get_refcnt(py) > 1); + BAR_GLOBAL.insert(key, obj.clone_ref(py)); + } + } +} + +// ========== KLINE_IDENTITY ========== +thread_local! { + static KLINE_LOCAL: RefCell>> = RefCell::new(HashMap::new()); +} +static KLINE_GLOBAL: std::sync::LazyLock>> = + std::sync::LazyLock::new(DashMap::new); + +pub fn kline_get(py: Python<'_>, key: usize) -> Option> { + match get_mode() { + CacheMode::ThreadLocal => { + KLINE_LOCAL.with(|m| m.borrow().get(&key).map(|p| p.clone_ref(py))) + } + CacheMode::Global => KLINE_GLOBAL.get(&key).map(|p| p.clone_ref(py)), + } +} +pub fn kline_insert(py: Python<'_>, key: usize, obj: &Py) { + match get_mode() { + CacheMode::ThreadLocal => KLINE_LOCAL.with(|m| { + let mut m = m.borrow_mut(); + m.retain(|_, v| v.get_refcnt(py) > 1); + m.insert(key, obj.clone_ref(py)); + }), + CacheMode::Global => { + KLINE_GLOBAL.retain(|_, v| v.get_refcnt(py) > 1); + KLINE_GLOBAL.insert(key, obj.clone_ref(py)); + } + } +} + +// ========== FRACTAL_IDENTITY ========== +use crate::structure_py::分型Py; +thread_local! { + static FRACTAL_LOCAL: RefCell>> = RefCell::new(HashMap::new()); +} +static FRACTAL_GLOBAL: std::sync::LazyLock>> = + std::sync::LazyLock::new(DashMap::new); + +pub fn fractal_get(py: Python<'_>, key: usize) -> Option> { + match get_mode() { + CacheMode::ThreadLocal => { + FRACTAL_LOCAL.with(|m| m.borrow().get(&key).map(|p| p.clone_ref(py))) + } + CacheMode::Global => FRACTAL_GLOBAL.get(&key).map(|p| p.clone_ref(py)), + } +} +pub fn fractal_insert(py: Python<'_>, key: usize, obj: &Py<分型Py>) { + match get_mode() { + CacheMode::ThreadLocal => FRACTAL_LOCAL.with(|m| { + let mut m = m.borrow_mut(); + m.retain(|_, v| v.get_refcnt(py) > 1); + m.insert(key, obj.clone_ref(py)); + }), + CacheMode::Global => { + FRACTAL_GLOBAL.retain(|_, v| v.get_refcnt(py) > 1); + FRACTAL_GLOBAL.insert(key, obj.clone_ref(py)); + } + } +} + +// ========== DASHED_IDENTITY ========== +use crate::structure_py::虚线Py; +thread_local! { + static DASHED_LOCAL: RefCell>> = RefCell::new(HashMap::new()); +} +static DASHED_GLOBAL: std::sync::LazyLock>> = + std::sync::LazyLock::new(DashMap::new); + +pub fn dashed_get(py: Python<'_>, key: usize) -> Option> { + match get_mode() { + CacheMode::ThreadLocal => { + DASHED_LOCAL.with(|m| m.borrow().get(&key).map(|p| p.clone_ref(py))) + } + CacheMode::Global => DASHED_GLOBAL.get(&key).map(|p| p.clone_ref(py)), + } +} +pub fn dashed_insert(py: Python<'_>, key: usize, obj: &Py<虚线Py>) { + match get_mode() { + CacheMode::ThreadLocal => DASHED_LOCAL.with(|m| { + let mut m = m.borrow_mut(); + m.retain(|_, v| v.get_refcnt(py) > 1); + m.insert(key, obj.clone_ref(py)); + }), + CacheMode::Global => { + DASHED_GLOBAL.retain(|_, v| v.get_refcnt(py) > 1); + DASHED_GLOBAL.insert(key, obj.clone_ref(py)); + } + } +} + +// ========== HUB_IDENTITY ========== +use crate::algorithm_py::中枢Py; +thread_local! { + static HUB_LOCAL: RefCell>> = RefCell::new(HashMap::new()); +} +static HUB_GLOBAL: std::sync::LazyLock>> = + std::sync::LazyLock::new(DashMap::new); + +pub fn hub_get(py: Python<'_>, key: usize) -> Option> { + match get_mode() { + CacheMode::ThreadLocal => HUB_LOCAL.with(|m| m.borrow().get(&key).map(|p| p.clone_ref(py))), + CacheMode::Global => HUB_GLOBAL.get(&key).map(|p| p.clone_ref(py)), + } +} +pub fn hub_insert(py: Python<'_>, key: usize, obj: &Py<中枢Py>) { + match get_mode() { + CacheMode::ThreadLocal => HUB_LOCAL.with(|m| { + let mut m = m.borrow_mut(); + m.retain(|_, v| v.get_refcnt(py) > 1); + m.insert(key, obj.clone_ref(py)); + }), + CacheMode::Global => { + HUB_GLOBAL.retain(|_, v| v.get_refcnt(py) > 1); + HUB_GLOBAL.insert(key, obj.clone_ref(py)); + } + } +} + +// ========== BSP_CACHE ========== +thread_local! { + static BSP_LOCAL: RefCell>> = RefCell::new(HashMap::new()); +} +static BSP_GLOBAL: std::sync::LazyLock>> = + std::sync::LazyLock::new(DashMap::new); + +pub fn bsp_get(py: Python<'_>, key: usize) -> Option> { + match get_mode() { + CacheMode::ThreadLocal => BSP_LOCAL.with(|m| m.borrow().get(&key).map(|p| p.clone_ref(py))), + CacheMode::Global => BSP_GLOBAL.get(&key).map(|p| p.clone_ref(py)), + } +} +pub fn bsp_insert(py: Python<'_>, key: usize, obj: Py) { + match get_mode() { + CacheMode::ThreadLocal => BSP_LOCAL.with(|m| { + m.borrow_mut().insert(key, obj); + }), + CacheMode::Global => { + BSP_GLOBAL.insert(key, obj); + } + } +} diff --git a/chanlun-py/src/config_py.rs b/chanlun-py/src/config_py.rs index 328557c..5cc484b 100644 --- a/chanlun-py/src/config_py.rs +++ b/chanlun-py/src/config_py.rs @@ -22,10 +22,11 @@ * SOFTWARE. */ +use chanlun::warn; use pyo3::prelude::*; use pyo3::types::{PyDict, PyType}; use std::collections::HashMap; -use tracing::warn; +use std::sync::atomic::{AtomicU64, Ordering}; /// 缠论配置 — 控制所有分析阶段行为的参数集(共 60+ 字段,均有默认值)。 /// @@ -95,6 +96,8 @@ use tracing::warn; #[pyclass(name = "缠论配置", module = "chanlun._chanlun")] pub struct 缠论配置Py { fields: HashMap>, + 缓存: parking_lot::Mutex>, + pub(crate) 版本: AtomicU64, } #[pymethods] @@ -120,7 +123,11 @@ impl 缠论配置Py { // 全部通过 serde_json 往返验证类型,统一处理字符串数字/布尔强制转换 let config = dict_to_rust_config(&fields)?; let fields = config_to_field_dict(&config)?; - Ok(Self { fields }) + Ok(Self { + fields, + 缓存: parking_lot::Mutex::new(Some(config)), + 版本: AtomicU64::new(1), + }) } fn __getattr__(&self, name: &str, py: Python<'_>) -> PyResult> { @@ -135,10 +142,13 @@ 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()); + *self.缓存.lock() = None; + self.版本.fetch_add(1, Ordering::Relaxed); // 通过 serde 往返验证类型 match dict_to_rust_config(&self.fields) { Ok(config) => { self.fields = config_to_field_dict(&config)?; + *self.缓存.lock() = Some(config); Ok(()) } Err(e) => Err(pyo3::exceptions::PyValueError::new_err(format!( @@ -217,7 +227,11 @@ impl 缠论配置Py { let config = dict_to_rust_config(&fields)?; let fields = config_to_field_dict(&config)?; - Ok(Self { fields }) + Ok(Self { + fields, + 缓存: parking_lot::Mutex::new(Some(config)), + 版本: AtomicU64::new(1), + }) } #[classmethod] @@ -231,7 +245,16 @@ impl 缠论配置Py { fn 不推送(_cls: &Bound<'_, PyType>) -> PyResult { let config = chanlun::config::缠论配置::default().不推送(); let fields = config_to_field_dict(&config)?; - Ok(Self { fields }) + Ok(Self { + fields, + 缓存: parking_lot::Mutex::new(Some(config)), + 版本: AtomicU64::new(1), + }) + } + + /// 判断指定标签是否应展示。None = 全部展示,空列表 = 全部隐藏。 + fn 展示标签(&self, 标签: &str) -> bool { + self.缓存.lock().as_ref().map_or(true, |c| c.展示标签(标签)) } #[classmethod] @@ -286,6 +309,55 @@ impl 缠论配置Py { } Ok(dict.into()) } + + /// 统一设置所有指标参数(对应 Python 设置指标)。 + /// + /// 各参数为 None 时不修改对应字段。 + /// 调用后自动将 `计算指标` 设为 `true`。 + #[pyo3(signature = (*, 均线=None, MACD=None, RSI=None, KDJ=None, BOLL=None))] + fn 设置指标( + &mut self, + py: Python<'_>, + 均线: Option>, + MACD: Option>, + RSI: Option>, + KDJ: Option>, + BOLL: Option>, + ) -> PyResult<()> { + self.fields.insert( + "计算指标".into(), + pyo3::types::PyBool::new(py, true).as_any().to_owned().unbind(), + ); + if let Some(v) = 均线 { + self.fields.insert("均线参数列表".into(), v.unbind()); + } + if let Some(v) = MACD { + self.fields.insert("MACD_参数列表".into(), v.unbind()); + } + if let Some(v) = RSI { + self.fields.insert("RSI_周期列表".into(), v.unbind()); + } + if let Some(v) = KDJ { + self.fields.insert("KDJ_参数列表".into(), v.unbind()); + } + if let Some(v) = BOLL { + self.fields.insert("BOLL_参数列表".into(), v.unbind()); + } + + // 通过 serde 往返验证类型 + *self.缓存.lock() = None; + self.版本.fetch_add(1, Ordering::Relaxed); + match dict_to_rust_config(&self.fields) { + Ok(config) => { + self.fields = config_to_field_dict(&config)?; + *self.缓存.lock() = Some(config); + Ok(()) + } + Err(e) => Err(pyo3::exceptions::PyValueError::new_err(format!( + "设置指标 转换失败: {e}" + ))), + } + } } impl 缠论配置Py { @@ -302,18 +374,31 @@ impl 缠论配置Py { let config = dict_to_rust_config(&fields)?; let fields = config_to_field_dict(&config)?; - Ok(Self { fields }) + Ok(Self { + fields, + 缓存: parking_lot::Mutex::new(Some(config)), + 版本: AtomicU64::new(1), + }) } pub(crate) fn to_rust_config( &self, _py: Python<'_>, ) -> PyResult { - dict_to_rust_config(&self.fields) + if let Some(ref cached) = *self.缓存.lock() { + return Ok(cached.clone()); + } + let config = dict_to_rust_config(&self.fields)?; + *self.缓存.lock() = Some(config.clone()); + Ok(config) } pub(crate) fn from_rust_config(config: &chanlun::config::缠论配置) -> PyResult { - config_to_field_dict(config).map(|fields| Self { fields }) + config_to_field_dict(config).map(|fields| Self { + fields, + 缓存: parking_lot::Mutex::new(Some(config.clone())), + 版本: AtomicU64::new(1), + }) } } diff --git a/chanlun-py/src/equality_py.rs b/chanlun-py/src/equality_py.rs index 9ff699c..737e66c 100644 --- a/chanlun-py/src/equality_py.rs +++ b/chanlun-py/src/equality_py.rs @@ -22,33 +22,10 @@ * SOFTWARE. */ -use std::num::NonZeroUsize; - use crate::business_py::立体分析器Py; use crate::business_py::观察者Py; -use std::sync::Mutex; - -use lru::LruCache; use pyo3::prelude::*; -/// 缓存辅助宏:在调用点创建静态 LruCache,先查后存 -macro_rules! with_cache { - ($cache:ident, $size:literal, $key_expr:expr, $compute:expr) => {{ - use std::sync::LazyLock; - static $cache: LazyLock>> = - LazyLock::new(|| Mutex::new(LruCache::new(NonZeroUsize::new($size).unwrap()))); - let key = $key_expr; - if let Some(cached) = $cache.lock().unwrap().get(&key) { - return Ok(cached.clone()); - } - let result: PyResult<(bool, String)> = $compute; - if let Ok(ref r) = result { - $cache.lock().unwrap().put(key, r.clone()); - } - result - }}; -} - /// 从 Python 值中提取时间戳(兼容 i64 和 datetime 两种类型) fn 提取时间戳(val: &Bound<'_, PyAny>) -> PyResult { if let Ok(ts) = val.extract::() { @@ -129,71 +106,60 @@ fn K线相等( B: &Bound<'_, PyAny>, 浮点容差: f64, ) -> PyResult<(bool, String)> { - with_cache!( - C_KLINE, - 128, - ( - A.as_ptr() as usize, - B.as_ptr() as usize, - 浮点容差.to_bits() as i64 - ), - { - // 快速路径 - if let (Ok(a), Ok(b)) = ( - A.cast::(), - B.cast::(), - ) { - return Ok(a.borrow().inner.相等(&b.borrow().inner, 浮点容差)); - } - // 回退路径 - let 标签 = "K线校验"; - let 比对字段 = [ - "标识", - "序号", - "周期", - "时间戳", - "高", - "低", - "开盘价", - "收盘价", - "成交量", - ]; - for &字段 in &比对字段 { - let (a有, b有) = (A.hasattr(字段)?, B.hasattr(字段)?); - if a有 && !b有 { - return Ok((false, format!("{标签}: [{字段}] A存在属性 B缺失属性"))); - } - if !a有 && b有 { - return Ok((false, format!("{标签}: [{字段}] B存在属性 A缺失属性"))); - } - if !a有 && !b有 { - continue; - } - let valA = A.getattr(字段)?; - let valB = B.getattr(字段)?; - if let Some(r) = 尝试浮点比较(&valA, &valB, 浮点容差) { - let (ok, m) = r?; - if !ok { - return Ok((false, format!("{标签}: [{字段}]{}", m))); - } - } else if 字段 == "时间戳" { - let a = 提取时间戳(&valA).unwrap_or(0); - let b = 提取时间戳(&valB).unwrap_or(0); - if a != b { - return Ok((false, format!("{标签}: [{字段}] 数值不等 A={a},B={b}"))); - } - } else { - let eq: bool = valA.eq(&valB)?; - if !eq { - let ra = valA.repr()?.extract::().unwrap_or_default(); - let rb = valB.repr()?.extract::().unwrap_or_default(); - return Ok((false, format!("{标签}: [{字段}] 数值不等 A={ra},B={rb}"))); - } - } - } - Ok((true, format!("{标签}: 全部字段一致"))) + // 快速路径 + if let (Ok(a), Ok(b)) = ( + A.cast::(), + B.cast::(), + ) { + return Ok(a.borrow().inner.相等(&b.borrow().inner, 浮点容差)); + } + // 回退路径 + let 标签 = "K线校验"; + let 比对字段 = [ + "标识", + "序号", + "周期", + "时间戳", + "高", + "低", + "开盘价", + "收盘价", + "成交量", + ]; + for &字段 in &比对字段 { + let (a有, b有) = (A.hasattr(字段)?, B.hasattr(字段)?); + if a有 && !b有 { + return Ok((false, format!("{标签}: [{字段}] A存在属性 B缺失属性"))); } - ) + if !a有 && b有 { + return Ok((false, format!("{标签}: [{字段}] B存在属性 A缺失属性"))); + } + if !a有 && !b有 { + continue; + } + let valA = A.getattr(字段)?; + let valB = B.getattr(字段)?; + if let Some(r) = 尝试浮点比较(&valA, &valB, 浮点容差) { + let (ok, m) = r?; + if !ok { + return Ok((false, format!("{标签}: [{字段}]{}", m))); + } + } else if 字段 == "时间戳" { + let a = 提取时间戳(&valA).unwrap_or(0); + let b = 提取时间戳(&valB).unwrap_or(0); + if a != b { + return Ok((false, format!("{标签}: [{字段}] 数值不等 A={a},B={b}"))); + } + } else { + let eq: bool = valA.eq(&valB)?; + if !eq { + let ra = valA.repr()?.extract::().unwrap_or_default(); + let rb = valB.repr()?.extract::().unwrap_or_default(); + return Ok((false, format!("{标签}: [{字段}] 数值不等 A={ra},B={rb}"))); + } + } + } + Ok((true, format!("{标签}: 全部字段一致"))) } // ========== 缠论K线相等 ========== @@ -205,102 +171,91 @@ fn 缠论K线相等( B: &Bound<'_, PyAny>, 浮点容差: f64, ) -> PyResult<(bool, String)> { - with_cache!( - C_CHAN_K, - 4096, - ( - A.as_ptr() as usize, - B.as_ptr() as usize, - 浮点容差.to_bits() as i64 - ), - { - if let (Ok(a), Ok(b)) = ( - A.cast::(), - B.cast::(), - ) { - return Ok(a.borrow().inner.相等(&b.borrow().inner, 浮点容差)); + if let (Ok(a), Ok(b)) = ( + A.cast::(), + B.cast::(), + ) { + return Ok(a.borrow().inner.相等(&b.borrow().inner, 浮点容差)); + } + let 标签 = "缠论K线校验"; + let 比对字段 = [ + "序号", + "时间戳", + "高", + "低", + "方向", + "分型", + "周期", + "标识", + "分型特征值", + "原始起始序号", + "原始结束序号", + "标的K线", + "买卖点信息", + ]; + for &字段 in &比对字段 { + let (a有, b有) = (A.hasattr(字段)?, B.hasattr(字段)?); + if a有 && !b有 { + return Ok((false, format!("{标签}: [{字段}] A存在 B缺失属性"))); + } + if !a有 && b有 { + return Ok((false, format!("{标签}: [{字段}] B存在 A缺失属性"))); + } + if !a有 && !b有 { + continue; + } + let valA = A.getattr(字段)?; + let valB = B.getattr(字段)?; + + if let Some(r) = 尝试浮点比较(&valA, &valB, 浮点容差) { + let (ok, m) = r?; + if !ok { + return Ok((false, format!("{标签}: [{字段}]{m}"))); } - let 标签 = "缠论K线校验"; - let 比对字段 = [ - "序号", - "时间戳", - "高", - "低", - "方向", - "分型", - "周期", - "标识", - "分型特征值", - "原始起始序号", - "原始结束序号", - "标的K线", - "买卖点信息", - ]; - for &字段 in &比对字段 { - let (a有, b有) = (A.hasattr(字段)?, B.hasattr(字段)?); - if a有 && !b有 { - return Ok((false, format!("{标签}: [{字段}] A存在 B缺失属性"))); - } - if !a有 && b有 { - return Ok((false, format!("{标签}: [{字段}] B存在 A缺失属性"))); - } - if !a有 && !b有 { + } else if 字段 == "标的K线" { + if let Some(r) = 检查空值一致(&valA, &valB, 字段, 标签) { + if !r.0 { + return Ok((false, r.1)); + } else { continue; } - let valA = A.getattr(字段)?; - let valB = B.getattr(字段)?; - - if let Some(r) = 尝试浮点比较(&valA, &valB, 浮点容差) { - let (ok, m) = r?; - if !ok { - return Ok((false, format!("{标签}: [{字段}]{m}"))); - } - } else if 字段 == "标的K线" { - if let Some(r) = 检查空值一致(&valA, &valB, 字段, 标签) { - if !r.0 { - return Ok((false, r.1)); - } else { - continue; - } - } - let (eq, msg) = K线相等(&valA, &valB, 浮点容差)?; - if !eq { - return Ok((false, format!("{标签}: 标的K线子项异常 >> {msg}"))); - } - } else if 字段 == "时间戳" { - let a = 提取时间戳(&valA).unwrap_or(0); - let b = 提取时间戳(&valB).unwrap_or(0); - if a != b { - return Ok((false, format!("{标签}: [{字段}] 数值不等 A={a},B={b}"))); - } - } else if 字段 == "方向" || 字段 == "分型" { - let sa = valA.str()?.extract::().unwrap_or_default(); - let sb = valB.str()?.extract::().unwrap_or_default(); - if sa != sb { - return Ok((false, format!("{标签}: [{字段}] 数值不等 A={sa},B={sb}"))); - } - } else if 字段 == "买卖点信息" { - let py = A.py(); - let set_a = py.import("builtins")?.getattr("set")?.call1((&valA,))?; - let set_b = py.import("builtins")?.getattr("set")?.call1((&valB,))?; - let eq: bool = set_a.eq(set_b)?; - if !eq { - let ra = valA.repr()?.extract::().unwrap_or_default(); - let rb = valB.repr()?.extract::().unwrap_or_default(); - return Ok((false, format!("{标签}: [{字段}] 数值不等 A={ra},B={rb}"))); - } - } else { - let eq: bool = valA.eq(&valB)?; - if !eq { - let ra = valA.repr()?.extract::().unwrap_or_default(); - let rb = valB.repr()?.extract::().unwrap_or_default(); - return Ok((false, format!("{标签}: [{字段}] 数值不等 A={ra},B={rb}"))); - } - } } - Ok((true, format!("{标签}: 全部字段嵌套校验一致"))) + let (eq, msg) = K线相等(&valA, &valB, 浮点容差)?; + if !eq { + return Ok((false, format!("{标签}: 标的K线子项异常 >> {msg}"))); + } + } else if 字段 == "时间戳" { + let a = 提取时间戳(&valA).unwrap_or(0); + let b = 提取时间戳(&valB).unwrap_or(0); + if a != b { + return Ok((false, format!("{标签}: [{字段}] 数值不等 A={a},B={b}"))); + } + } else if 字段 == "方向" || 字段 == "分型" { + let sa = valA.str()?.extract::().unwrap_or_default(); + let sb = valB.str()?.extract::().unwrap_or_default(); + if sa != sb { + return Ok((false, format!("{标签}: [{字段}] 数值不等 A={sa},B={sb}"))); + } + } else if 字段 == "买卖点信息" { + let py = A.py(); + let set_a = py.import("builtins")?.getattr("set")?.call1((&valA,))?; + let set_b = py.import("builtins")?.getattr("set")?.call1((&valB,))?; + let eq: bool = set_a.eq(set_b)?; + if !eq { + let ra = valA.repr()?.extract::().unwrap_or_default(); + let rb = valB.repr()?.extract::().unwrap_or_default(); + return Ok((false, format!("{标签}: [{字段}] 数值不等 A={ra},B={rb}"))); + } + } else { + let eq: bool = valA.eq(&valB)?; + if !eq { + let ra = valA.repr()?.extract::().unwrap_or_default(); + let rb = valB.repr()?.extract::().unwrap_or_default(); + return Ok((false, format!("{标签}: [{字段}] 数值不等 A={ra},B={rb}"))); + } } - ) + } + Ok((true, format!("{标签}: 全部字段嵌套校验一致"))) } // ========== 分型相等 ========== @@ -312,88 +267,77 @@ fn 分型相等( B: &Bound<'_, PyAny>, 浮点容差: f64, ) -> PyResult<(bool, String)> { - with_cache!( - C_FRACTAL, - 4096, - ( - A.as_ptr() as usize, - B.as_ptr() as usize, - 浮点容差.to_bits() as i64 - ), - { - if let (Ok(a), Ok(b)) = ( - A.cast::(), - B.cast::(), - ) { - return Ok(a.borrow().inner.相等(&b.borrow().inner, 浮点容差)); + if let (Ok(a), Ok(b)) = ( + A.cast::(), + B.cast::(), + ) { + return Ok(a.borrow().inner.相等(&b.borrow().inner, 浮点容差)); + } + let 标签 = "分型校验"; + // Python 分型内部用 _结构/_时间戳/_分型特征值 作为 slot 名,Rust 用 结构/时间戳/分型特征值 作为 getter + for &字段 in &["左", "中", "右"] { + let valA = A.getattr(字段)?; + let valB = B.getattr(字段)?; + if let Some(r) = 检查空值一致(&valA, &valB, 字段, 标签) { + if !r.0 { + return Ok((false, r.1)); + } else { + continue; } - let 标签 = "分型校验"; - // Python 分型内部用 _结构/_时间戳/_分型特征值 作为 slot 名,Rust 用 结构/时间戳/分型特征值 作为 getter - for &字段 in &["左", "中", "右"] { - let valA = A.getattr(字段)?; - let valB = B.getattr(字段)?; - if let Some(r) = 检查空值一致(&valA, &valB, 字段, 标签) { - if !r.0 { - return Ok((false, r.1)); - } else { - continue; - } - } - let (eq, msg) = 缠论K线相等(&valA, &valB, 浮点容差)?; - if !eq { - return Ok((false, format!("{标签}: [{字段}]缠论K线子项异常 >> {msg}"))); - } - } - for &(字段, 字段别名) in &[ - ("_结构", "结构"), - ("_时间戳", "时间戳"), - ("_分型特征值", "分型特征值"), - ] { - // 先尝试 Python 侧的下划线名,再尝试 Rust 侧的无下划线名 - let valA = 获取属性任意(A, &[字段, 字段别名])?; - let valB = 获取属性任意(B, &[字段, 字段别名])?; - let (a有, b有) = (valA.is_some(), valB.is_some()); - if a有 && !b有 { - return Ok((false, format!("{标签}: [{字段}] A存在属性 B缺失属性"))); - } - if !a有 && b有 { - return Ok((false, format!("{标签}: [{字段}] B存在属性 A缺失属性"))); - } - if !a有 && !b有 { - continue; - } - let valA = valA.unwrap(); - let valB = valB.unwrap(); - - if let Some(r) = 尝试浮点比较(&valA, &valB, 浮点容差) { - let (ok, m) = r?; - if !ok { - return Ok((false, format!("{标签}: [{字段}]{m}"))); - } - } else if 字段 == "_时间戳" { - let a = 提取时间戳(&valA).unwrap_or(0); - let b = 提取时间戳(&valB).unwrap_or(0); - if a != b { - return Ok((false, format!("{标签}: [{字段}] 数值不等 A={a},B={b}"))); - } - } else if 字段 == "_结构" { - let sa = valA.str()?.extract::().unwrap_or_default(); - let sb = valB.str()?.extract::().unwrap_or_default(); - if sa != sb { - return Ok((false, format!("{标签}: [{字段}] 数值不等 A={sa},B={sb}"))); - } - } else { - let eq: bool = valA.eq(&valB)?; - if !eq { - let ra = valA.repr()?.extract::().unwrap_or_default(); - let rb = valB.repr()?.extract::().unwrap_or_default(); - return Ok((false, format!("{标签}: [{字段}] 数值不等 A={ra},B={rb}"))); - } - } - } - Ok((true, format!("{标签}: 自有字段+三根缠论K线全部校验一致"))) } - ) + let (eq, msg) = 缠论K线相等(&valA, &valB, 浮点容差)?; + if !eq { + return Ok((false, format!("{标签}: [{字段}]缠论K线子项异常 >> {msg}"))); + } + } + for &(字段, 字段别名) in &[ + ("_结构", "结构"), + ("_时间戳", "时间戳"), + ("_分型特征值", "分型特征值"), + ] { + // 先尝试 Python 侧的下划线名,再尝试 Rust 侧的无下划线名 + let valA = 获取属性任意(A, &[字段, 字段别名])?; + let valB = 获取属性任意(B, &[字段, 字段别名])?; + let (a有, b有) = (valA.is_some(), valB.is_some()); + if a有 && !b有 { + return Ok((false, format!("{标签}: [{字段}] A存在属性 B缺失属性"))); + } + if !a有 && b有 { + return Ok((false, format!("{标签}: [{字段}] B存在属性 A缺失属性"))); + } + if !a有 && !b有 { + continue; + } + let valA = valA.unwrap(); + let valB = valB.unwrap(); + + if let Some(r) = 尝试浮点比较(&valA, &valB, 浮点容差) { + let (ok, m) = r?; + if !ok { + return Ok((false, format!("{标签}: [{字段}]{m}"))); + } + } else if 字段 == "_时间戳" { + let a = 提取时间戳(&valA).unwrap_or(0); + let b = 提取时间戳(&valB).unwrap_or(0); + if a != b { + return Ok((false, format!("{标签}: [{字段}] 数值不等 A={a},B={b}"))); + } + } else if 字段 == "_结构" { + let sa = valA.str()?.extract::().unwrap_or_default(); + let sb = valB.str()?.extract::().unwrap_or_default(); + if sa != sb { + return Ok((false, format!("{标签}: [{字段}] 数值不等 A={sa},B={sb}"))); + } + } else { + let eq: bool = valA.eq(&valB)?; + if !eq { + let ra = valA.repr()?.extract::().unwrap_or_default(); + let rb = valB.repr()?.extract::().unwrap_or_default(); + return Ok((false, format!("{标签}: [{字段}] 数值不等 A={ra},B={rb}"))); + } + } + } + Ok((true, format!("{标签}: 自有字段+三根缠论K线全部校验一致"))) } // ========== 缺口相等 ========== @@ -405,54 +349,42 @@ fn 缺口相等( B: &Bound<'_, PyAny>, 浮点容差: f64, ) -> PyResult<(bool, String)> { - with_cache!( - C_GAP, - 4096, - ( - A.as_ptr() as usize, - B.as_ptr() as usize, - 浮点容差.to_bits() as i64 - ), - { - if let (Ok(a), Ok(b)) = ( - A.cast::(), - B.cast::(), - ) { - return Ok(a.borrow().inner.相等(&b.borrow().inner, 浮点容差)); - } - let 标签 = "缺口校验"; - for &字段 in &["高", "低"] { - let (a有, b有) = (A.hasattr(字段)?, B.hasattr(字段)?); - if a有 && !b有 { - return Ok((false, format!("{标签}: [{字段}] A存在 B缺失属性"))); - } - if !a有 && b有 { - return Ok((false, format!("{标签}: [{字段}] B存在 A缺失属性"))); - } - if !a有 && !b有 { - continue; - } - let valA = A.getattr(字段)?; - let valB = B.getattr(字段)?; - if let Some(r) = 尝试浮点比较(&valA, &valB, 浮点容差) { - let (ok, m) = r?; - if !ok { - return Ok((false, format!("{标签}: [{字段}]{m}"))); - } - } else { - let eq: bool = valA.eq(&valB)?; - if !eq { - let ra = valA.repr()?.extract::().unwrap_or_default(); - let rb = valB.repr()?.extract::().unwrap_or_default(); - return Ok((false, format!("{标签}: [{字段}] 数值不等 A={ra},B={rb}"))); - } - } - } - Ok((true, format!("{标签}: 上下沿价格校验完全一致"))) + if let (Ok(a), Ok(b)) = ( + A.cast::(), + B.cast::(), + ) { + return Ok(a.borrow().inner.相等(&b.borrow().inner, 浮点容差)); + } + let 标签 = "缺口校验"; + for &字段 in &["高", "低"] { + let (a有, b有) = (A.hasattr(字段)?, B.hasattr(字段)?); + if a有 && !b有 { + return Ok((false, format!("{标签}: [{字段}] A存在 B缺失属性"))); } - ) + if !a有 && b有 { + return Ok((false, format!("{标签}: [{字段}] B存在 A缺失属性"))); + } + if !a有 && !b有 { + continue; + } + let valA = A.getattr(字段)?; + let valB = B.getattr(字段)?; + if let Some(r) = 尝试浮点比较(&valA, &valB, 浮点容差) { + let (ok, m) = r?; + if !ok { + return Ok((false, format!("{标签}: [{字段}]{m}"))); + } + } else { + let eq: bool = valA.eq(&valB)?; + if !eq { + let ra = valA.repr()?.extract::().unwrap_or_default(); + let rb = valB.repr()?.extract::().unwrap_or_default(); + return Ok((false, format!("{标签}: [{字段}] 数值不等 A={ra},B={rb}"))); + } + } + } + Ok((true, format!("{标签}: 上下沿价格校验完全一致"))) } - // ========== 线段特征相等 ========== #[pyfunction] @@ -462,74 +394,60 @@ fn 线段特征相等( B: &Bound<'_, PyAny>, 浮点容差: f64, ) -> PyResult<(bool, String)> { - with_cache!( - C_SEG_FEAT, - 4096, - ( - A.as_ptr() as usize, - B.as_ptr() as usize, - 浮点容差.to_bits() as i64 - ), - { - if let (Ok(a), Ok(b)) = ( - A.cast::(), - B.cast::(), - ) { - return Ok(a.borrow().inner.相等(&b.borrow().inner, 浮点容差)); - } - let 标签 = "线段特征校验"; - for &字段 in &["序号", "标识", "线段方向", "基础序列"] { - let (a有, b有) = (A.hasattr(字段)?, B.hasattr(字段)?); - if a有 && !b有 { - return Ok((false, format!("{标签}: [{字段}] A存在 B缺失属性"))); - } - if !a有 && b有 { - return Ok((false, format!("{标签}: [{字段}] B存在 A缺失属性"))); - } - if !a有 && !b有 { - continue; - } - let valA = A.getattr(字段)?; - let valB = B.getattr(字段)?; - - if 字段 == "基础序列" { - let len_a: usize = valA.len()?; - let len_b: usize = valB.len()?; - if len_a != len_b { - return Ok(( - false, - format!("{标签}: [基础序列] 列表长度不一致 A={len_a},B={len_b}"), - )); - } - for idx in 0..len_a { - let itemA = valA.get_item(idx)?; - let itemB = valB.get_item(idx)?; - let (eq, msg) = 虚线相等(&itemA, &itemB, 浮点容差)?; - if !eq { - return Ok(( - false, - format!("{标签}: 基础序列[{idx}]子虚线异常 >> {msg}"), - )); - } - } - } else if 字段 == "线段方向" { - let sa = valA.str()?.extract::().unwrap_or_default(); - let sb = valB.str()?.extract::().unwrap_or_default(); - if sa != sb { - return Ok((false, format!("{标签}: [{字段}] 数值不等 A={sa},B={sb}"))); - } - } else { - let eq: bool = valA.eq(&valB)?; - if !eq { - let ra = valA.repr()?.extract::().unwrap_or_default(); - let rb = valB.repr()?.extract::().unwrap_or_default(); - return Ok((false, format!("{标签}: [{字段}] 数值不等 A={ra},B={rb}"))); - } - } - } - Ok((true, format!("{标签}: 字段与内部虚线序列全部一致"))) + if let (Ok(a), Ok(b)) = ( + A.cast::(), + B.cast::(), + ) { + return Ok(a.borrow().inner.相等(&b.borrow().inner, 浮点容差)); + } + let 标签 = "线段特征校验"; + for &字段 in &["序号", "标识", "线段方向", "基础序列"] { + let (a有, b有) = (A.hasattr(字段)?, B.hasattr(字段)?); + if a有 && !b有 { + return Ok((false, format!("{标签}: [{字段}] A存在 B缺失属性"))); } - ) + if !a有 && b有 { + return Ok((false, format!("{标签}: [{字段}] B存在 A缺失属性"))); + } + if !a有 && !b有 { + continue; + } + let valA = A.getattr(字段)?; + let valB = B.getattr(字段)?; + + if 字段 == "基础序列" { + let len_a: usize = valA.len()?; + let len_b: usize = valB.len()?; + if len_a != len_b { + return Ok(( + false, + format!("{标签}: [基础序列] 列表长度不一致 A={len_a},B={len_b}"), + )); + } + for idx in 0..len_a { + let itemA = valA.get_item(idx)?; + let itemB = valB.get_item(idx)?; + let (eq, msg) = 虚线相等(&itemA, &itemB, 浮点容差)?; + if !eq { + return Ok((false, format!("{标签}: 基础序列[{idx}]子虚线异常 >> {msg}"))); + } + } + } else if 字段 == "线段方向" { + let sa = valA.str()?.extract::().unwrap_or_default(); + let sb = valB.str()?.extract::().unwrap_or_default(); + if sa != sb { + return Ok((false, format!("{标签}: [{字段}] 数值不等 A={sa},B={sb}"))); + } + } else { + let eq: bool = valA.eq(&valB)?; + if !eq { + let ra = valA.repr()?.extract::().unwrap_or_default(); + let rb = valB.repr()?.extract::().unwrap_or_default(); + return Ok((false, format!("{标签}: [{字段}] 数值不等 A={ra},B={rb}"))); + } + } + } + Ok((true, format!("{标签}: 字段与内部虚线序列全部一致"))) } // ========== 中枢相等 ========== @@ -790,8 +708,8 @@ fn 观察者相等( .inner .clone() .ok_or_else(|| pyo3::exceptions::PyValueError::new_err("观察者B 内部为空"))?; - let obs_a = arc_a.read().unwrap(); - let obs_b = arc_b.read().unwrap(); + let obs_a = arc_a.read(); + let obs_b = arc_b.read(); Ok(obs_a.相等(&obs_b, 容差)) } diff --git a/chanlun-py/src/indicators_py.rs b/chanlun-py/src/indicators_py.rs index 9a755e8..b97ae9e 100644 --- a/chanlun-py/src/indicators_py.rs +++ b/chanlun-py/src/indicators_py.rs @@ -290,7 +290,7 @@ impl 相对强弱指数Py { } #[getter] fn RSI历史队列(&self) -> Vec { - self.inner.RSI历史队列.clone() + self.inner.RSI历史队列.iter().copied().collect() } fn __str__(&self) -> String { @@ -470,11 +470,11 @@ impl 随机指标Py { } #[getter] fn 历史最高价队列(&self) -> Vec { - self.inner.历史最高价队列.clone() + self.inner.历史最高价队列.iter().copied().collect() } #[getter] fn 历史最低价队列(&self) -> Vec { - self.inner.历史最低价队列.clone() + self.inner.历史最低价队列.iter().copied().collect() } #[getter] fn 前一个RSV(&self) -> Option { @@ -796,16 +796,12 @@ impl 指标容器Py { } fn __getitem__(&self, 名称: &str, py: Python<'_>) -> PyResult> { - if self.包含(名称) { - match self.inner.获取(名称) { - Some(v) => 指标值_to_py(v, py), - None => Ok(py.None()), - } - } else { - Err(pyo3::exceptions::PyKeyError::new_err(format!( + match self.inner.获取(名称) { + Some(v) => 指标值_to_py(v, py), + None => Err(pyo3::exceptions::PyKeyError::new_err(format!( "指标 '{}' 不存在", 名称 - ))) + ))), } } @@ -822,16 +818,12 @@ impl 指标容器Py { } return Ok(dict.into()); } - if self.包含(名称) { - match self.inner.获取(名称) { - Some(v) => 指标值_to_py(v, py), - None => Ok(py.None()), - } - } else { - Err(pyo3::exceptions::PyAttributeError::new_err(format!( + match self.inner.获取(名称) { + Some(v) => 指标值_to_py(v, py), + None => Err(pyo3::exceptions::PyAttributeError::new_err(format!( "指标 '{}' 不存在于 指标容器 中", 名称 - ))) + ))), } } @@ -917,7 +909,12 @@ impl 均线工具Py { return Ok(sum / (n.max(1)) as f64); } - let prev_key = format!("SMA_{}", period); + let prev_key = { + let mut s = String::with_capacity(8); + use std::fmt::Write; + write!(&mut s, "SMA_{}", period).unwrap(); + s + }; // 尝试从前一根K线的均线缓存中读取 let prev_cached = 普K序列[n - 2] .bind(py) @@ -925,7 +922,6 @@ impl 均线工具Py { .inner .指标 .read() - .unwrap() .均线() .and_then(|m| m.get(&prev_key)) .copied(); diff --git a/chanlun-py/src/kline_py.rs b/chanlun-py/src/kline_py.rs index 24f757f..b395a28 100644 --- a/chanlun-py/src/kline_py.rs +++ b/chanlun-py/src/kline_py.rs @@ -22,16 +22,16 @@ * SOFTWARE. */ +use parking_lot::RwLock; use pyo3::prelude::*; use pyo3::types::{PyBytes, PyDict, PyList, PyType}; use std::collections::HashMap; use std::sync::Arc; -use std::sync::RwLock; use std::sync::atomic::Ordering; use crate::config_py::缠论配置Py; use crate::indicators_py::{ - 平滑异同移动平均线Py, 指标容器Py, 相对强弱指数Py, 随机指标Py + 平滑异同移动平均线Py, 指标容器Py, 相对强弱指数Py, 随机指标Py, 布林带Py, }; use crate::structure_py::fractal_to_py; use crate::types_py::相对方向Py; @@ -149,7 +149,6 @@ impl K线Py { self.inner .指标 .read() - .unwrap() .macd_cloned() .map(|m| 平滑异同移动平均线Py { inner: m }) } @@ -159,7 +158,6 @@ impl K线Py { self.inner .指标 .read() - .unwrap() .rsi_cloned() .map(|r| 相对强弱指数Py { inner: r }) } @@ -169,16 +167,29 @@ impl K线Py { self.inner .指标 .read() - .unwrap() .kdj_cloned() .map(|k| 随机指标Py { inner: k }) } + #[getter] + fn boll(&self) -> Option<布林带Py> { + self.inner + .指标 + .read() + .boll_cloned() + .map(|b| 布林带Py { inner: b }) + } + + /// 读取均线值,如 `k.ma("SMA_5")` → `Optional[float]` + fn ma(&self, key: &str) -> Option { + self.inner.ma(key) + } + /// 指标容器 — 包含所有已注册指标(MACD/RSI/KDJ/BOLL/均线/单值) #[getter] fn 指标(&self) -> 指标容器Py { 指标容器Py { - inner: self.inner.指标.read().unwrap().clone(), + inner: self.inner.指标.read().clone(), } } @@ -339,6 +350,39 @@ impl K线Py { .take(end_idx - start_idx + 1) .collect()) } + + /// 根据当前K线和方向生成下一根K线(用于随机回测) + #[pyo3(signature = (方向, 居中 = false))] + fn 根据当前K线生成新K线( + &self, 方向: &Bound<'_, PyAny>, 居中: bool + ) -> PyResult { + let dir: chanlun::types::相对方向 = if let Ok(d) = 方向.extract::>() + { + d.inner + } else if let Ok(i) = 方向.extract::() { + match i { + 0 => chanlun::types::相对方向::向上, + 1 => chanlun::types::相对方向::向下, + 2 => chanlun::types::相对方向::向上缺口, + 3 => chanlun::types::相对方向::向下缺口, + 4 => chanlun::types::相对方向::衔接向上, + 5 => chanlun::types::相对方向::衔接向下, + _ => { + return Err(pyo3::exceptions::PyValueError::new_err(format!( + "无效方向: {i}" + ))); + } + } + } else { + return Err(pyo3::exceptions::PyTypeError::new_err( + "方向 必须是 相对方向 或 int (0-5)", + )); + }; + let new_bar = self.inner.根据当前K线生成新K线(dir, 居中); + Ok(Self { + inner: Arc::new(new_bar), + }) + } } // ========== 缠论K线 ========== @@ -371,57 +415,29 @@ impl 缠论K线Py { } } -/// 对象标识缓存:Arc 地址 → 规范 Python 对象 -/// 确保同一底层 Arc 指针在 Python 侧始终映射到同一 PyObject -/// 使用全局 static 而非 thread_local!,保证跨线程对象标识和买卖点信息一致性 -static BAR_IDENTITY: std::sync::LazyLock>>> = - std::sync::LazyLock::new(|| RwLock::new(HashMap::new())); - -static KLINE_IDENTITY: std::sync::LazyLock>>> = - std::sync::LazyLock::new(|| RwLock::new(HashMap::new())); - -/// 买卖点信息缓存 — 按 Arc 指针全局共享,确保所有 wrapper 看到同一 PySet -static BSP_CACHE: std::sync::LazyLock>>> = - std::sync::LazyLock::new(|| RwLock::new(HashMap::new())); - -/// 将 Rc 转为 Py,确保同一 Rc 地址总是返回同一 Python 对象 pub(crate) fn bar_to_py( py: Python<'_>, inner: std::sync::Arc, ) -> Py { let key = Arc::as_ptr(&inner) as usize; - if let Some(cached) = BAR_IDENTITY - .read() - .unwrap() - .get(&key) - .map(|p| p.clone_ref(py)) - { + if let Some(cached) = crate::cache::bar_get(py, key) { return cached; } let obj = Py::new(py, K线Py { inner }).unwrap(); - BAR_IDENTITY.write().unwrap().insert(key, obj.clone_ref(py)); + crate::cache::bar_insert(py, key, &obj); obj } -/// 将 Rc<缠论K线> 转为 Py<缠论K线Py>,确保同一 Rc 地址总是返回同一 Python 对象 pub(crate) fn chan_kline_to_py( py: Python<'_>, inner: std::sync::Arc, ) -> Py<缠论K线Py> { let key = Arc::as_ptr(&inner) as usize; - if let Some(cached) = KLINE_IDENTITY - .read() - .unwrap() - .get(&key) - .map(|p| p.clone_ref(py)) - { + if let Some(cached) = crate::cache::kline_get(py, key) { return cached; } let obj = Py::new(py, 缠论K线Py::from_rc(inner)).unwrap(); - KLINE_IDENTITY - .write() - .unwrap() - .insert(key, obj.clone_ref(py)); + crate::cache::kline_insert(py, key, &obj); obj } @@ -462,7 +478,7 @@ impl 缠论K线Py { #[getter] fn 方向(&self, py: Python<'_>) -> Py<相对方向Py> { - crate::types_py::获取相对方向单例(py, *self.inner.方向.read().unwrap()) + crate::types_py::获取相对方向单例(py, *self.inner.方向.read()) } #[getter] @@ -470,7 +486,6 @@ impl 缠论K线Py { self.inner .分型 .read() - .unwrap() .map(|f| crate::types_py::获取分型结构单例(py, f)) } @@ -501,7 +516,7 @@ impl 缠论K线Py { #[getter] fn 标的K线(&self, py: Python<'_>) -> Py { - bar_to_py(py, self.inner.标的K线.read().unwrap().clone()) + bar_to_py(py, self.inner.标的K线.read().clone()) } /// pandas 兼容 — 返回所有字段构成的字典 @@ -556,11 +571,7 @@ impl 缠论K线Py { // 复制买卖点信息到镜像 let src_key = Arc::as_ptr(&self.inner) as usize; let dst_key = Arc::as_ptr(&mirror.inner) as usize; - let cached_src = BSP_CACHE - .read() - .unwrap() - .get(&src_key) - .map(|p| p.clone_ref(py)); + let cached_src = crate::cache::bsp_get(py, src_key); if let Some(cached_src) = cached_src && let Ok(new_set) = pyo3::types::PySet::empty(py) { @@ -568,7 +579,7 @@ impl 缠论K线Py { let _ = new_set.add(item); } let py_set: Py = new_set.into(); - BSP_CACHE.write().unwrap().insert(dst_key, py_set); + crate::cache::bsp_insert(py, dst_key, py_set); } mirror } @@ -595,25 +606,19 @@ impl 缠论K线Py { fn 买卖点信息(&self, py: Python<'_>) -> PyResult> { let key = Arc::as_ptr(&self.inner) as usize; // 检查全局缓存 - let cached = BSP_CACHE.read().unwrap().get(&key).map(|p| p.clone_ref(py)); + let cached = crate::cache::bsp_get(py, key); if let Some(set) = cached { return Ok(set.into_any()); } // 创建新的 PySet,从 Rust HashSet 同步已有内容 let set = pyo3::types::PySet::empty(py)?; - let bsp_info = self.inner.买卖点信息.read().unwrap(); + let bsp_info = self.inner.买卖点信息.read(); for item in bsp_info.iter() { set.add(item.as_str())?; } drop(bsp_info); - BSP_CACHE.write().unwrap().insert(key, set.into()); - Ok(BSP_CACHE - .read() - .unwrap() - .get(&key) - .unwrap() - .clone_ref(py) - .into_any()) + crate::cache::bsp_insert(py, key, set.into()); + Ok(crate::cache::bsp_get(py, key).unwrap().into_any()) } #[classmethod] diff --git a/chanlun-py/src/lib.rs b/chanlun-py/src/lib.rs index 101439f..71f5e6a 100644 --- a/chanlun-py/src/lib.rs +++ b/chanlun-py/src/lib.rs @@ -102,10 +102,13 @@ fn init_tracing() { mod algorithm_py; mod business_py; +pub(crate) mod cache; mod config_py; mod equality_py; mod indicators_py; mod kline_py; +mod signal_engine_py; +mod signal_py; mod structure_py; mod types_py; @@ -121,15 +124,25 @@ fn set_分型模式(value: bool) { chanlun::structure::fractal_obj::分型模式.store(value, Ordering::Relaxed); } +/// 扩展线段模式 — 控制虚线高低取值方式,默认 False +#[pyfunction] +fn get_扩展线段模式() -> bool { + chanlun::structure::dash_line::扩展线段模式.load(Ordering::Relaxed) +} + +/// 设置 扩展线段模式 +#[pyfunction] +fn set_扩展线段模式(value: bool) { + chanlun::structure::dash_line::扩展线段模式.store(value, Ordering::Relaxed); +} + /// 获取当前日志级别 ("trace" / "debug" / "info" / "warn" / "error" / "off") #[pyfunction] fn get_log_level() -> &'static str { 级别数字转名称(LOG_LEVEL.load(Ordering::Relaxed)) } -/// 设置日志级别 (不区分大小写: "trace" / "debug" / "info" / "warn" / "error" / "off") -/// -/// 设为 "off" 可完全关闭日志输出。 +/// 设置日志级别 — 自动启用日志,同步更新 tracing subscriber #[pyfunction] fn set_log_level(level: &str) -> PyResult<()> { let 数字 = 级别名称转数字(level).ok_or_else(|| { @@ -138,32 +151,95 @@ fn set_log_level(level: &str) -> PyResult<()> { level )) })?; - - let guard = 过滤器句柄锁 - .get() - .ok_or_else(|| pyo3::exceptions::PyRuntimeError::new_err("日志系统尚未初始化"))?; - let handle = guard.lock().unwrap(); - let 名称 = 级别数字转名称(数字); - let filter = tracing_subscriber::EnvFilter::new(名称); - handle - .reload(filter) - .map_err(|_| pyo3::exceptions::PyRuntimeError::new_err("切换日志级别失败"))?; - LOG_LEVEL.store(数字, Ordering::Relaxed); + chanlun::log::日志启用.store(数字 < 5, Ordering::Relaxed); + // 同步更新 tracing subscriber + if let Some(guard) = 过滤器句柄锁.get() { + let handle = guard.lock().unwrap(); + let 名称 = 级别数字转名称(数字); + let filter = tracing_subscriber::EnvFilter::new(名称); + let _ = handle.reload(filter); + } Ok(()) } +/// 获取日志输出模式 ("off", "simple", "tracing") +#[pyfunction] +fn get_log_mode() -> &'static str { + match chanlun::log::get_log_mode() { + 0 => "off", + 1 => "simple", + 2 => "tracing", + _ => "unknown", + } +} + +/// 设置日志输出模式(必须在任何日志输出之前调用) +/// - "off": 不输出 +/// - "simple": 直接 eprintln/println(默认) +/// - "tracing": 带时间戳和格式化的 tracing subscriber +#[pyfunction] +fn set_log_mode(mode: &str) -> PyResult<()> { + let m = match mode.to_lowercase().as_str() { + "off" | "0" => 0u8, + "simple" | "on" | "1" => 1u8, + "tracing" | "2" => 2u8, + _ => { + return Err(pyo3::exceptions::PyValueError::new_err( + "无效日志模式,有效值: 'off', 'simple', 'tracing'", + )); + } + }; + if m == 2 { + init_tracing(); + } + chanlun::log::set_log_mode(m); + Ok(()) +} + +/// 获取缓存模式 ("thread_local" 或 "global") +#[pyfunction] +fn get_cache_mode() -> &'static str { + match crate::cache::peek_mode().unwrap_or(&crate::cache::CacheMode::ThreadLocal) { + crate::cache::CacheMode::ThreadLocal => "thread_local", + crate::cache::CacheMode::Global => "global", + } +} + +/// 设置缓存模式(必须在创建任何观察者之前调用) +#[pyfunction] +fn set_cache_mode(mode: &str) -> PyResult<()> { + let m = match mode.to_lowercase().as_str() { + "thread_local" | "local" => crate::cache::CacheMode::ThreadLocal, + "global" => crate::cache::CacheMode::Global, + _ => { + return Err(pyo3::exceptions::PyValueError::new_err( + "无效缓存模式,有效值: 'thread_local', 'global'", + )); + } + }; + crate::cache::set_mode(m).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e)) +} + /// 缠论技术分析库 — Rust 高性能实现 #[pymodule] /// 缠论技术分析库 — Rust 高性能实现 fn _chanlun(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { - init_tracing(); + chanlun::log::init_from_env(); m.add_function(wrap_pyfunction!(get_分型模式, m)?)?; m.add_function(wrap_pyfunction!(set_分型模式, m)?)?; + m.add_function(wrap_pyfunction!(get_扩展线段模式, m)?)?; + m.add_function(wrap_pyfunction!(set_扩展线段模式, m)?)?; m.add_function(wrap_pyfunction!(get_log_level, m)?)?; m.add_function(wrap_pyfunction!(set_log_level, m)?)?; + m.add_function(wrap_pyfunction!(get_log_mode, m)?)?; + m.add_function(wrap_pyfunction!(set_log_mode, m)?)?; + m.add_function(wrap_pyfunction!(get_cache_mode, m)?)?; + m.add_function(wrap_pyfunction!(set_cache_mode, m)?)?; // 阶段 1: 枚举和基础类型 types_py::register(m)?; + // 阶段 1.5: 信号原语 + signal_py::register(m)?; // 阶段 2: 配置 config_py::register(m)?; // 阶段 3: 技术指标 @@ -176,6 +252,8 @@ fn _chanlun(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { algorithm_py::register(m)?; // 阶段 7: 业务 business_py::register(m)?; + // 阶段 7.5: 信号引擎 + signal_engine_py::register(m)?; // 阶段 8: 相等校验函数 equality_py::register(m)?; Ok(()) diff --git a/chanlun-py/src/signal_engine_py.rs b/chanlun-py/src/signal_engine_py.rs new file mode 100644 index 0000000..6a9555b --- /dev/null +++ b/chanlun-py/src/signal_engine_py.rs @@ -0,0 +1,281 @@ +/* + * 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. + */ + +//! PyO3 绑定:将信号引擎和 call_signal 暴露给 Python。 +//! +//! 第三方代码声明:引擎架构参考 czsc 的 `信号计算器` +//!(https://github.com/waditu/czsc,Apache License 2.0)。 + +use std::collections::HashMap; + +use chanlun::signal::engine::{self, SignalConfig, SignalEngine as 核心SignalEngine}; + +use pyo3::exceptions::{PyKeyError, PyValueError}; +use pyo3::prelude::*; +use pyo3::types::PyDict; + +use crate::business_py::{立体分析器Py, 观察者Py}; +use crate::signal_py::{SignalPy, 时间戳转datetime}; + +// ======== 工具函数 ======== + +/// 将 PyAny 转换为 `serde_json::Value`。 +/// 尝试顺序:i64 → f64 → String → bool → 兜底转为 String。 +fn py_any_to_json_value(obj: &Bound<'_, PyAny>) -> PyResult { + // i64 + if let Ok(i) = obj.extract::() { + return Ok(serde_json::Value::Number(i.into())); + } + // f64 + if let Ok(f) = obj.extract::() { + if let Some(n) = serde_json::Number::from_f64(f) { + return Ok(serde_json::Value::Number(n)); + } + return Ok(serde_json::Value::String(f.to_string())); + } + // String + if let Ok(s) = obj.extract::() { + return Ok(serde_json::Value::String(s)); + } + // bool + if let Ok(b) = obj.extract::() { + return Ok(serde_json::Value::Bool(b)); + } + // fallback: Python repr as string + Ok(serde_json::Value::String(obj.to_string())) +} + +/// 将 `PyDict` 转换为 `HashMap`。 +pub(crate) fn py_dict_to_params( + dict: &Bound<'_, PyDict>, +) -> PyResult> { + let mut params = HashMap::new(); + for (k, v) in dict.iter() { + let key: String = k.extract()?; + let value = py_any_to_json_value(&v)?; + params.insert(key, value); + } + Ok(params) +} + +// ======== 自由函数 ======== + +/// 通过 Rust 注册表按名调用单个信号函数。 +/// +/// Args: +/// name: 注册的信号名,如 ``"youwukuncheng_中枢第三买卖点_V230602"`` +/// obs: 观察者Py 实例 +/// params: 信号参数字典(不含 name) +/// +/// Returns: +/// SignalPy 对象列表 +/// +/// Raises: +/// PyValueError: 信号名未注册 +#[pyfunction] +pub fn call_signal( + name: &str, + obs: &观察者Py, + params: &Bound<'_, PyDict>, +) -> PyResult> { + let obs_ref = obs.obs(); + let params_map = py_dict_to_params(params)?; + + let inner = + engine::call_signal(name, &obs_ref, ¶ms_map).map_err(|e| PyValueError::new_err(e))?; + + Ok(inner.into_iter().map(|s| SignalPy { inner: s }).collect()) +} + +/// 列出所有已注册的信号名(编译时 + 动态)。 +#[pyfunction] +pub fn list_signals() -> Vec { + chanlun::signal::registry::list_signal_names() +} + +/// 按名获取信号参数模板(编译时 + 动态)。 +#[pyfunction] +pub fn get_signal_template(name: &str) -> Option { + chanlun::signal::registry::get_template(name) +} + +// ======== 动态注册 API ======== + +/// 从动态注册表中移除信号。 +#[pyfunction] +fn unregister_signal(name: &str) -> PyResult<()> { + chanlun::signal::registry::unregister_signal(name).map_err(|e| PyValueError::new_err(e)) +} + +// ======== 信号引擎 pyclass ======== + +/// Rust 信号计算引擎的 Python 绑定。 +/// +/// 用法:: +/// +/// from chanlun._chanlun import 信号引擎 +/// +/// 引擎 = 信号引擎([ +/// {"name": "youwukuncheng_中枢第三买卖点_V230602", +/// "freq": 86400, "max_overlap": 3, +/// "本级完整性": "实", "同级完整性": "合"}, +/// ]) +/// 引擎.自动挂载指标(分析器) +/// 结果 = 引擎.更新(分析器) # dict[str, str] +#[pyclass(name = "信号引擎", module = "chanlun._chanlun")] +pub struct SignalEnginePy { + inner: 核心SignalEngine, +} + +#[pymethods] +impl SignalEnginePy { + /// 创建信号引擎。 + /// + /// Args: + /// 信号配置: 信号配置字典列表,每项必须含 ``"name"`` 和 ``"freq"``。 + #[new] + #[pyo3(signature = (信号配置=None))] + fn new(信号配置: Option>>) -> PyResult { + let configs = match 信号配置 { + Some(list) => { + let mut configs = Vec::with_capacity(list.len()); + for d in &list { + let name: String = d + .get_item("name")? + .ok_or_else(|| PyValueError::new_err("信号配置缺少 'name'"))? + .extract()?; + + let freq_raw = d + .get_item("freq")? + .ok_or_else(|| PyKeyError::new_err("信号配置缺少 'freq'"))?; + + // freq 可以是 int 或 str + let freq: i64 = if let Ok(i) = freq_raw.extract::() { + i + } else if let Ok(s) = freq_raw.extract::() { + s.parse::().map_err(|_| { + PyValueError::new_err(format!("freq 无法解析为整数: {s}")) + })? + } else { + return Err(PyValueError::new_err(format!( + "freq 类型无效: {}", + freq_raw.get_type().name()? + ))); + }; + + // 构建 params(排除 "name",保留 "freq" 为字符串格式) + let mut params = HashMap::new(); + for (k, v) in d.iter() { + let key: String = k.extract()?; + if key == "name" { + continue; + } + if key == "freq" { + // 统一为字符串,便于 Rust 信号函数通过 params::get_string 读取 + params.insert(key, serde_json::Value::String(freq.to_string())); + continue; + } + let value = py_any_to_json_value(&v)?; + params.insert(key, value); + } + + configs.push(SignalConfig { + signal_name: name, + freq, + params, + }); + } + configs + } + None => Vec::new(), + }; + Ok(Self { + inner: 核心SignalEngine::new(configs), + }) + } + + /// 扫描所有配置中的 MACD / 均线关键字,为各周期 observer 的配置添加缺失的指标参数。 + fn 自动挂载指标(&self, analyzer: &立体分析器Py) { + self.inner.自动挂载指标(&analyzer.inner); + } + + /// 遍历所有配置,执行信号函数,收集非空结果。 + /// + /// Returns: + /// ``dict[str, str]`` — 信号 key → 信号 value(已过滤 "任意_任意_任意_0") + fn 更新(&self, analyzer: &立体分析器Py) -> HashMap { + self.inner.更新(&analyzer.inner) + } + + /// 更新信号并返回完整结果(信号 + 行情)。 + /// 返回 dict: ``{"signals": {...}, "market": {...}}``,若无基础周期 K 线则 market 为 None。 + fn 更新_完整<'py>( + &self, py: Python<'py>, analyzer: &立体分析器Py + ) -> PyResult> { + let result = self.inner.更新_完整(&analyzer.inner); + let d = PyDict::new(py); + + let signals_dict = PyDict::new(py); + for (k, v) in &result.signals { + signals_dict.set_item(k, v)?; + } + d.set_item("signals", signals_dict)?; + + if let Some(m) = &result.market { + let md = PyDict::new(py); + md.set_item("symbol", &m.symbol)?; + md.set_item("dt", 时间戳转datetime(py, m.dt)?)?; + md.set_item("id", m.id)?; + md.set_item("open", m.open)?; + md.set_item("high", m.high)?; + md.set_item("low", m.low)?; + md.set_item("close", m.close)?; + md.set_item("vol", m.vol)?; + d.set_item("market", md)?; + } else { + d.set_item("market", py.None())?; + } + + Ok(d.into()) + } + + /// 返回配置数量 + fn __len__(&self) -> usize { + self.inner.len() + } + + fn __repr__(&self) -> String { + format!("信号引擎(configs={})", self.inner.len()) + } +} + +/// 注册模块。 +pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + m.add_function(wrap_pyfunction!(call_signal, m)?)?; + m.add_function(wrap_pyfunction!(list_signals, m)?)?; + m.add_function(wrap_pyfunction!(get_signal_template, m)?)?; + m.add_function(wrap_pyfunction!(unregister_signal, m)?)?; + Ok(()) +} diff --git a/chanlun-py/src/signal_py.rs b/chanlun-py/src/signal_py.rs new file mode 100644 index 0000000..673b0ff --- /dev/null +++ b/chanlun-py/src/signal_py.rs @@ -0,0 +1,795 @@ +/* + * 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 pyo3::exceptions::PyValueError; +use pyo3::prelude::*; +use pyo3::types::PyDict; +use std::collections::HashMap; + +use chanlun::signal::event::Event as 核心Event; +use chanlun::signal::factor::Factor as 核心Factor; +use chanlun::signal::operate::Operate as 核心Operate; +use chanlun::signal::position::Position as 核心Position; +use chanlun::signal::signal::Signal as 核心Signal; +use chanlun::signal::{信号字典, 匹配值}; + +/// Operate 枚举绑定。 +#[pyclass(name = "Operate", module = "chanlun._chanlun", eq, eq_int)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum OperatePy { + HL, + HS, + HO, + LO, + LE, + SO, + SE, +} + +impl OperatePy { + pub(crate) fn 转核心(self) -> 核心Operate { + match self { + OperatePy::HL => 核心Operate::持多, + OperatePy::HS => 核心Operate::持空, + OperatePy::HO => 核心Operate::持币, + OperatePy::LO => 核心Operate::开多, + OperatePy::LE => 核心Operate::平多, + OperatePy::SO => 核心Operate::开空, + OperatePy::SE => 核心Operate::平空, + } + } + pub(crate) fn 从核心(o: 核心Operate) -> Self { + match o { + 核心Operate::持多 => OperatePy::HL, + 核心Operate::持空 => OperatePy::HS, + 核心Operate::持币 => OperatePy::HO, + 核心Operate::开多 => OperatePy::LO, + 核心Operate::平多 => OperatePy::LE, + 核心Operate::开空 => OperatePy::SO, + 核心Operate::平空 => OperatePy::SE, + } + } +} + +#[pymethods] +impl OperatePy { + #[getter] + fn value(&self) -> &'static str { + self.转核心().value() + } + fn __str__(&self) -> &'static str { + self.转核心().value() + } + fn __repr__(&self) -> String { + format!("Operate.{:?}", self) + } + /// 从中文值还原 Operate(供 Event.load 反序列化用,对应旧 Python `Operate("开多")`)。 + #[staticmethod] + fn from_value(value: &str) -> PyResult { + match value { + "持多" => Ok(OperatePy::HL), + "持空" => Ok(OperatePy::HS), + "持币" => Ok(OperatePy::HO), + "开多" => Ok(OperatePy::LO), + "平多" => Ok(OperatePy::LE), + "开空" => Ok(OperatePy::SO), + "平空" => Ok(OperatePy::SE), + _ => Err(PyValueError::new_err(format!("未知 Operate 值: {value}"))), + } + } +} + +/// 把 PyDict 转成核心层信号字典:str 值 → 字符串,其余 → 非字符串。 +pub(crate) fn 字典转核心(s: &Bound<'_, PyDict>) -> PyResult<信号字典> { + let mut out: 信号字典 = HashMap::new(); + for (k, v) in s.iter() { + let key: String = k.extract()?; + let 值 = match v.extract::() { + Ok(文本) if !文本.is_empty() => 匹配值::字符串(文本), + _ => 匹配值::非字符串, + }; + out.insert(key, 值); + } + Ok(out) +} + +/// 反序列化辅助:从 dict 取字符串字段,缺省返回空串。 +fn 取字符串(raw: &Bound<'_, PyDict>, key: &str) -> PyResult { + match raw.get_item(key)? { + Some(v) => v.extract(), + None => Ok(String::new()), + } +} + +/// 从七段字符串解析 Signal(格式: k1_k2_k3_v1_v2_v3_score)。 +fn parse_signal_str(s: &str) -> PyResult<核心Signal> { + let parts: Vec<&str> = s.split('_').collect(); + if parts.len() != 7 { + return Err(PyValueError::new_err(format!( + "Signal 格式无效:应为 k1_k2_k3_v1_v2_v3_score(7段),收到 {s}" + ))); + } + let score: i32 = parts[6] + .parse() + .map_err(|_| PyValueError::new_err(format!("无法解析 score: {}", parts[6])))?; + Ok(核心Signal::new( + parts[0], parts[1], parts[2], parts[3], parts[4], parts[5], score, + )) +} + +/// 反序列化辅助:从 dict 取信号串列表,逐个解析为核心 Signal。 +fn 取信号列表(raw: &Bound<'_, PyDict>, key: &str) -> PyResult> { + let mut out = Vec::new(); + if let Some(item) = raw.get_item(key)? { + let strs: Vec = item.extract()?; + for s in strs { + out.push(parse_signal_str(&s)?); + } + } + Ok(out) +} + +/// 反序列化辅助:从 dict 取事件列表,逐个调用 Event.load。 +fn 取事件列表(raw: &Bound<'_, PyDict>, key: &str) -> PyResult> { + let mut out = Vec::new(); + if let Some(item) = raw.get_item(key)? { + let dicts: Vec> = item.extract()?; + for d in &dicts { + out.push(EventPy::load(d)?.inner); + } + } + Ok(out) +} + +/// Signal 绑定。 +#[pyclass(name = "Signal", module = "chanlun._chanlun")] +#[derive(Clone)] +pub struct SignalPy { + pub(crate) inner: 核心Signal, +} + +#[pymethods] +impl SignalPy { + #[new] + #[pyo3(signature = (signal=String::new(), score=0, k1="任意".to_string(), k2="任意".to_string(), k3="任意".to_string(), v1="任意".to_string(), v2="任意".to_string(), v3="任意".to_string()))] + fn new( + signal: String, + score: i32, + k1: String, + k2: String, + k3: String, + v1: String, + v2: String, + v3: String, + ) -> PyResult { + let inner = if signal.is_empty() { + 核心Signal::new(&k1, &k2, &k3, &v1, &v2, &v3, score) + } else { + parse_signal_str(&signal)? + }; + Ok(Self { inner }) + } + + #[getter] + fn signal(&self) -> String { + self.inner.signal.clone() + } + #[getter] + fn score(&self) -> i32 { + self.inner.score + } + #[getter] + fn k1(&self) -> String { + self.inner.k1.clone() + } + #[getter] + fn k2(&self) -> String { + self.inner.k2.clone() + } + #[getter] + fn k3(&self) -> String { + self.inner.k3.clone() + } + #[getter] + fn v1(&self) -> String { + self.inner.v1.clone() + } + #[getter] + fn v2(&self) -> String { + self.inner.v2.clone() + } + #[getter] + fn v3(&self) -> String { + self.inner.v3.clone() + } + + #[getter] + fn key(&self) -> String { + self.inner.key() + } + #[getter] + fn value(&self) -> String { + self.inner.value() + } + + fn is_match(&self, s: &Bound<'_, PyDict>) -> PyResult { + let 字典 = 字典转核心(s)?; + self.inner + .is_match(&字典) + .map_err(|e| PyValueError::new_err(format!("{} 不在信号列表中", e.0))) + } + + fn __repr__(&self) -> String { + format!("Signal('{}')", self.inner.signal) + } +} + +/// Factor 绑定。signals_all 全满足 + signals_any 任一满足 + signals_not 全不满足。 +#[pyclass(name = "Factor", module = "chanlun._chanlun")] +#[derive(Clone)] +pub struct FactorPy { + pub(crate) inner: 核心Factor, +} + +#[pymethods] +impl FactorPy { + #[new] + #[pyo3(signature = (signals_all, signals_any=Vec::new(), signals_not=Vec::new(), name=String::new()))] + fn new( + signals_all: Vec, + signals_any: Vec, + signals_not: Vec, + name: String, + ) -> PyResult { + let 取 = |v: Vec| v.into_iter().map(|s| s.inner).collect::>(); + let inner = 核心Factor::新建(取(signals_all), 取(signals_any), 取(signals_not), name) + .map_err(PyValueError::new_err)?; + Ok(Self { inner }) + } + + #[getter] + fn name(&self) -> String { + self.inner.name.clone() + } + + #[getter] + fn signals_all(&self) -> Vec { + self.inner + .signals_all + .iter() + .cloned() + .map(|inner| SignalPy { inner }) + .collect() + } + + #[getter] + fn signals_any(&self) -> Vec { + self.inner + .signals_any + .iter() + .cloned() + .map(|inner| SignalPy { inner }) + .collect() + } + + #[getter] + fn signals_not(&self) -> Vec { + self.inner + .signals_not + .iter() + .cloned() + .map(|inner| SignalPy { inner }) + .collect() + } + + #[getter] + fn unique_signals(&self) -> Vec { + self.inner.unique_signals() + } + + fn is_match(&self, s: &Bound<'_, PyDict>) -> PyResult { + let 字典 = 字典转核心(s)?; + self.inner + .is_match(&字典) + .map_err(|e| PyValueError::new_err(format!("{} 不在信号列表中", e.0))) + } + + fn __repr__(&self) -> String { + format!("Factor('{}')", self.inner.name) + } + + /// 序列化为 dict:{name, signals_all, signals_any, signals_not}(signals 存为信号串)。 + fn dump<'py>(&self, py: Python<'py>) -> PyResult> { + let d = PyDict::new(py); + let 串 = |v: &[核心Signal]| v.iter().map(|s| s.signal.clone()).collect::>(); + d.set_item("name", &self.inner.name)?; + d.set_item("signals_all", 串(&self.inner.signals_all))?; + d.set_item("signals_any", 串(&self.inner.signals_any))?; + d.set_item("signals_not", 串(&self.inner.signals_not))?; + Ok(d) + } + + /// 从 dict 反序列化(对应旧 Python Factor.load)。 + #[staticmethod] + fn load(raw: &Bound<'_, PyDict>) -> PyResult { + let inner = 核心Factor::新建( + 取信号列表(raw, "signals_all")?, + 取信号列表(raw, "signals_any")?, + 取信号列表(raw, "signals_not")?, + 取字符串(raw, "name")?, + ) + .map_err(PyValueError::new_err)?; + Ok(FactorPy { inner }) + } +} + +/// Event 绑定。operate + 因子列表(任一因子满足则事件为真)。 +#[pyclass(name = "Event", module = "chanlun._chanlun")] +#[derive(Clone)] +pub struct EventPy { + pub(crate) inner: 核心Event, +} + +#[pymethods] +impl EventPy { + #[new] + #[pyo3(signature = (operate, factors, signals_all=Vec::new(), signals_any=Vec::new(), signals_not=Vec::new(), name=String::new()))] + fn new( + operate: OperatePy, + factors: Vec, + signals_all: Vec, + signals_any: Vec, + signals_not: Vec, + name: String, + ) -> PyResult { + let 取s = |v: Vec| v.into_iter().map(|s| s.inner).collect::>(); + let 取f = |v: Vec| v.into_iter().map(|f| f.inner).collect::>(); + let inner = 核心Event::新建( + operate.转核心(), + 取f(factors), + 取s(signals_all), + 取s(signals_any), + 取s(signals_not), + name, + ) + .map_err(PyValueError::new_err)?; + Ok(Self { inner }) + } + + #[getter] + fn name(&self) -> String { + self.inner.name.clone() + } + + #[getter] + fn sha256(&self) -> String { + self.inner.sha256.clone() + } + + #[getter] + fn operate(&self) -> OperatePy { + OperatePy::从核心(self.inner.operate) + } + + #[getter] + fn factors(&self) -> Vec { + self.inner + .factors + .iter() + .cloned() + .map(|inner| FactorPy { inner }) + .collect() + } + + #[getter] + fn unique_signals(&self) -> Vec { + self.inner.unique_signals() + } + + fn is_match(&self, s: &Bound<'_, PyDict>) -> PyResult<(bool, Option)> { + let 字典 = 字典转核心(s)?; + self.inner + .is_match(&字典) + .map_err(|e| PyValueError::new_err(format!("{} 不在信号列表中", e.0))) + } + + fn __repr__(&self) -> String { + format!("Event('{}')", self.inner.name) + } + + /// 序列化为 dict:{name, operate, signals_all/any/not, factors}。 + fn dump<'py>(&self, py: Python<'py>) -> PyResult> { + let d = PyDict::new(py); + let 串 = |v: &[核心Signal]| v.iter().map(|s| s.signal.clone()).collect::>(); + d.set_item("name", &self.inner.name)?; + d.set_item("operate", self.inner.operate.value())?; + d.set_item("signals_all", 串(&self.inner.signals_all))?; + d.set_item("signals_any", 串(&self.inner.signals_any))?; + d.set_item("signals_not", 串(&self.inner.signals_not))?; + let factors: Vec> = self + .inner + .factors + .iter() + .map(|f| FactorPy { inner: f.clone() }.dump(py)) + .collect::>()?; + d.set_item("factors", factors)?; + Ok(d) + } + + /// 从 dict 反序列化(对应旧 Python Event.load)。 + #[staticmethod] + fn load(raw: &Bound<'_, PyDict>) -> PyResult { + let operate = OperatePy::from_value(&取字符串(raw, "operate")?)?.转核心(); + let mut factors = Vec::new(); + if let Some(item) = raw.get_item("factors")? { + let dicts: Vec> = item.extract()?; + for fd in dicts { + factors.push(FactorPy::load(&fd)?.inner); + } + } + let inner = 核心Event::新建( + operate, + factors, + 取信号列表(raw, "signals_all")?, + 取信号列表(raw, "signals_any")?, + 取信号列表(raw, "signals_not")?, + 取字符串(raw, "name")?, + ) + .map_err(PyValueError::new_err)?; + Ok(EventPy { inner }) + } +} + +/// Position 绑定(可子类化)。Python 子类应实现 update() 状态机。 +#[pyclass(name = "Position", module = "chanlun._chanlun", subclass)] +#[derive(Clone)] +pub struct PositionPy { + pub(crate) inner: 核心Position, +} + +/// 核心 Operate → PyO3 OperatePy 枚举变体映射。 +fn 核心op转pyop(op: 核心Operate) -> OperatePy { + match op { + 核心Operate::持多 => OperatePy::HL, + 核心Operate::持空 => OperatePy::HS, + 核心Operate::持币 => OperatePy::HO, + 核心Operate::开多 => OperatePy::LO, + 核心Operate::平多 => OperatePy::LE, + 核心Operate::开空 => OperatePy::SO, + 核心Operate::平空 => OperatePy::SE, + } +} + +/// 将 i64 Unix 时间戳转为 Python datetime(UTC)。 +pub(crate) fn 时间戳转datetime(py: Python<'_>, ts: i64) -> PyResult> { + let datetime_mod = py.import("datetime")?; + let tz = datetime_mod.getattr("timezone")?.getattr("utc")?; + let dt = datetime_mod + .getattr("datetime")? + .call_method1("fromtimestamp", (ts as f64, tz))?; + Ok(dt.into()) +} + +#[pymethods] +impl PositionPy { + #[new] + #[pyo3(signature = (symbol, opens, exits=Vec::new(), interval=0, timeout=1000, stop_loss=1000, T0=false, name=String::new()))] + fn new( + symbol: String, + opens: Vec, + exits: Vec, + interval: i64, + timeout: i64, + stop_loss: i64, + T0: bool, + name: String, + ) -> PyResult { + let 取 = |v: Vec| v.into_iter().map(|e| e.inner).collect::>(); + let inner = 核心Position::新建( + symbol, + 取(opens), + 取(exits), + interval, + timeout, + stop_loss, + T0, + name, + ) + .map_err(PyValueError::new_err)?; + Ok(Self { inner }) + } + + // --- 配置 getter(不变)--- + #[getter] + fn symbol(&self) -> String { + self.inner.symbol.clone() + } + #[getter] + fn name(&self) -> String { + self.inner.name.clone() + } + #[getter] + fn opens(&self) -> Vec { + self.inner + .opens + .iter() + .cloned() + .map(|inner| EventPy { inner }) + .collect() + } + #[getter] + fn exits(&self) -> Vec { + self.inner + .exits + .iter() + .cloned() + .map(|inner| EventPy { inner }) + .collect() + } + #[getter] + fn events(&self) -> Vec { + self.inner + .events + .iter() + .cloned() + .map(|inner| EventPy { inner }) + .collect() + } + #[getter] + fn interval(&self) -> i64 { + self.inner.interval + } + #[getter] + fn timeout(&self) -> i64 { + self.inner.timeout + } + #[getter] + fn stop_loss(&self) -> i64 { + self.inner.stop_loss + } + #[getter] + fn T0(&self) -> bool { + self.inner.T0 + } + + #[getter] + fn unique_signals(&self) -> Vec { + self.inner.unique_signals() + } + + // --- 状态 getter(新增)--- + #[getter] + fn pos(&self) -> i32 { + self.inner.pos + } + + #[getter] + fn pos_changed(&self) -> bool { + self.inner.pos_changed + } + + #[getter] + fn operates<'py>(&self, py: Python<'py>) -> PyResult>> { + self.inner + .operates + .iter() + .map(|r| { + let d = PyDict::new(py); + d.set_item("symbol", &r.symbol)?; + d.set_item("dt", 时间戳转datetime(py, r.dt)?)?; + d.set_item("bid", r.bid)?; + d.set_item("price", r.price)?; + d.set_item("op", 核心op转pyop(r.op))?; + d.set_item("op_desc", &r.op_desc)?; + d.set_item("pos", r.pos)?; + Ok(d) + }) + .collect() + } + + #[getter] + fn holds<'py>(&self, py: Python<'py>) -> PyResult>> { + self.inner + .holds + .iter() + .map(|r| { + let d = PyDict::new(py); + d.set_item("dt", 时间戳转datetime(py, r.dt)?)?; + d.set_item("pos", r.pos)?; + d.set_item("price", r.price)?; + Ok(d) + }) + .collect() + } + + #[getter] + fn pairs<'py>(&self, py: Python<'py>) -> PyResult>> { + self.inner + .pairs() + .iter() + .map(|r| { + let d = PyDict::new(py); + d.set_item("标的代码", &r.标的代码)?; + d.set_item("策略标记", &r.策略标记)?; + d.set_item("交易方向", &r.交易方向)?; + d.set_item("开仓时间", 时间戳转datetime(py, r.开仓时间)?)?; + d.set_item("平仓时间", 时间戳转datetime(py, r.平仓时间)?)?; + d.set_item("开仓价格", r.开仓价格)?; + d.set_item("平仓价格", r.平仓价格)?; + d.set_item("持仓K线数", r.持仓K线数)?; + d.set_item("事件序列", &r.事件序列)?; + d.set_item("持仓天数", r.持仓天数)?; + d.set_item("盈亏比例", r.盈亏比例)?; + Ok(d) + }) + .collect() + } + + /// 更新持仓状态。接收一个信号字典(含 OHLCV 字段 + 信号键)。 + /// + /// 信号字典必须包含:``dt``(datetime 或 Unix 时间戳), ``close``(收盘价)。 + /// 可选:``id`` 或 ``bid``(K线序号)。 + #[pyo3(signature = (信号字典))] + fn update(&mut self, 信号字典: &Bound<'_, PyDict>) -> PyResult<()> { + // 1. 提取 dt(支持 datetime 对象和 int/float Unix 时间戳) + let dt: i64 = match 信号字典.get_item("dt")? { + Some(v) => { + // 尝试 i64 + if let Ok(ts) = v.extract::() { + ts + // 尝试 f64 + } else if let Ok(ts) = v.extract::() { + ts as i64 + // 尝试 datetime.timestamp() + } else if let Ok(ts) = v.call_method0("timestamp") { + (ts.extract::()?) as i64 + } else { + return Err(PyValueError::new_err( + "无法从信号字典中提取 dt 字段(需要 datetime 或 Unix 时间戳)", + )); + } + } + None => return Err(PyValueError::new_err("信号字典缺少 dt 字段")), + }; + + // 2. 提取 price + let price: f64 = 信号字典 + .get_item("close")? + .and_then(|v| v.extract::().ok()) + .ok_or_else(|| PyValueError::new_err("信号字典缺少 close 字段"))?; + + // 3. 提取 bid(可选) + let bid: i64 = 信号字典 + .get_item("id")? + .or_else(|| 信号字典.get_item("bid").ok().flatten()) + .and_then(|v| v.extract::().ok()) + .unwrap_or(0); + + // 4. 转换为信号字典(排除 OHLCV 键) + let ohkcv_keys: std::collections::HashSet<&str> = [ + "symbol", "dt", "open", "high", "low", "close", "vol", "id", "bid", + ] + .iter() + .copied() + .collect(); + + let mut signals: 信号字典 = HashMap::new(); + for (k, v) in 信号字典.iter() { + let key: String = k.extract()?; + if ohkcv_keys.contains(key.as_str()) { + continue; + } + let 值 = match v.extract::() { + Ok(文本) if !文本.is_empty() => 匹配值::字符串(文本), + _ => 匹配值::非字符串, + }; + signals.insert(key, 值); + } + + // 5. 调用核心状态机 + self.inner + .update(dt, price, bid, &signals) + .map_err(|e| PyValueError::new_err(format!("{} 不在信号列表中", e.0)))?; + + Ok(()) + } + + fn __repr__(&self) -> String { + format!( + "Position(name={}, symbol={}, timeout={}, stop_loss={}BP, T0={}, interval={}s, pos={})", + self.inner.name, + self.inner.symbol, + self.inner.timeout, + self.inner.stop_loss, + self.inner.T0, + self.inner.interval, + self.inner.pos + ) + } + + /// 序列化为 dict。 + /// `with_data=True` 时附带 state(pairs, holds);`with_data=False` 时仅配置。 + #[pyo3(signature = (with_data=false))] + fn dump<'py>(&self, py: Python<'py>, with_data: bool) -> PyResult> { + let d = PyDict::new(py); + d.set_item("symbol", &self.inner.symbol)?; + d.set_item("name", &self.inner.name)?; + let 事件dump = |evts: &[核心Event]| -> PyResult>> { + evts.iter() + .map(|e| EventPy { inner: e.clone() }.dump(py)) + .collect() + }; + d.set_item("opens", 事件dump(&self.inner.opens)?)?; + d.set_item("exits", 事件dump(&self.inner.exits)?)?; + d.set_item("interval", self.inner.interval)?; + d.set_item("timeout", self.inner.timeout)?; + d.set_item("stop_loss", self.inner.stop_loss)?; + d.set_item("T0", self.inner.T0)?; + if with_data { + d.set_item("pairs", self.pairs(py)?)?; + d.set_item("holds", self.holds(py)?)?; + } + Ok(d) + } + + /// 从 dict 反序列化(仅配置,状态字段初始化为默认值)。 + #[staticmethod] + fn load(raw: &Bound<'_, PyDict>) -> PyResult { + let symbol = 取字符串(raw, "symbol")?; + let name = 取字符串(raw, "name")?; + let interval: i64 = raw + .get_item("interval")? + .and_then(|v| v.extract().ok()) + .unwrap_or(0); + let timeout: i64 = raw + .get_item("timeout")? + .and_then(|v| v.extract().ok()) + .unwrap_or(1000); + let stop_loss: i64 = raw + .get_item("stop_loss")? + .and_then(|v| v.extract().ok()) + .unwrap_or(1000); + let T0: bool = raw + .get_item("T0")? + .and_then(|v| v.extract().ok()) + .unwrap_or(false); + + let opens = 取事件列表(raw, "opens")?; + let exits = 取事件列表(raw, "exits")?; + + let inner = + 核心Position::新建(symbol, opens, exits, interval, timeout, stop_loss, T0, name) + .map_err(PyValueError::new_err)?; + Ok(Self { inner }) + } +} + +pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/chanlun-py/src/structure_py.rs b/chanlun-py/src/structure_py.rs index eec6cd7..87f17de 100644 --- a/chanlun-py/src/structure_py.rs +++ b/chanlun-py/src/structure_py.rs @@ -24,9 +24,7 @@ use pyo3::prelude::*; use pyo3::types::{PyDict, PyType}; -use std::collections::HashMap; use std::sync::Arc; -use std::sync::RwLock; use std::sync::atomic::Ordering; use crate::algorithm_py::hub_to_py; @@ -35,37 +33,18 @@ use crate::kline_py::{K线Py, bar_to_py, 缠论K线Py}; // ---- 身份缓存 (弱引用:通过 refcnt 检测存活,仅缓存持有则视为过期) ---- -// 使用全局 static 而非 thread_local!,保证跨线程对象标识一致性 -static FRACTAL_IDENTITY: std::sync::LazyLock>>> = - std::sync::LazyLock::new(|| RwLock::new(HashMap::new())); -static DASHED_IDENTITY: std::sync::LazyLock>>> = - std::sync::LazyLock::new(|| RwLock::new(HashMap::new())); -static SEGFEAT_IDENTITY: std::sync::LazyLock>>> = - std::sync::LazyLock::new(|| RwLock::new(HashMap::new())); +// 缓存通过 crate::cache 模块管理(支持 thread_local / global 运行时切换) pub(crate) fn fractal_to_py( py: Python<'_>, inner: Arc, ) -> Py<分型Py> { let key = Arc::as_ptr(&inner) as usize; - if let Some(cached) = FRACTAL_IDENTITY - .read() - .unwrap() - .get(&key) - .map(|p| p.clone_ref(py)) - { + if let Some(cached) = crate::cache::fractal_get(py, key) { return cached; } - // 清理 refcnt==1 的过期条目(仅缓存持有,Python 侧已无引用) - FRACTAL_IDENTITY - .write() - .unwrap() - .retain(|_, v| v.get_refcnt(py) > 1); let obj = Py::new(py, 分型Py { inner }).unwrap(); - FRACTAL_IDENTITY - .write() - .unwrap() - .insert(key, obj.clone_ref(py)); + crate::cache::fractal_insert(py, key, &obj); obj } @@ -74,23 +53,11 @@ pub(crate) fn dashed_to_py( inner: Arc, ) -> Py<虚线Py> { let key = Arc::as_ptr(&inner) as usize; - if let Some(cached) = DASHED_IDENTITY - .read() - .unwrap() - .get(&key) - .map(|p| p.clone_ref(py)) - { + if let Some(cached) = crate::cache::dashed_get(py, key) { return cached; } - DASHED_IDENTITY - .write() - .unwrap() - .retain(|_, v| v.get_refcnt(py) > 1); let obj = Py::new(py, 虚线Py { inner }).unwrap(); - DASHED_IDENTITY - .write() - .unwrap() - .insert(key, obj.clone_ref(py)); + crate::cache::dashed_insert(py, key, &obj); obj } @@ -98,25 +65,7 @@ pub(crate) fn segfeat_to_py( py: Python<'_>, inner: Arc, ) -> Py<线段特征Py> { - let key = Arc::as_ptr(&inner) as usize; - if let Some(cached) = SEGFEAT_IDENTITY - .read() - .unwrap() - .get(&key) - .map(|p| p.clone_ref(py)) - { - return cached; - } - SEGFEAT_IDENTITY - .write() - .unwrap() - .retain(|_, v| v.get_refcnt(py) > 1); - let obj = Py::new(py, 线段特征Py { inner }).unwrap(); - SEGFEAT_IDENTITY - .write() - .unwrap() - .insert(key, obj.clone_ref(py)); - obj + Py::new(py, 线段特征Py { inner }).unwrap() } use crate::types_py::{分型结构Py, 相对方向Py, 缺口Py}; @@ -393,7 +342,7 @@ impl 虚线Py { #[getter] fn 标识(&self) -> String { - self.inner.标识.read().unwrap().clone() + self.inner.标识.read().clone() } #[getter] @@ -413,7 +362,7 @@ impl 虚线Py { #[getter] fn 武(&self, py: Python<'_>) -> Py<分型Py> { - fractal_to_py(py, Arc::clone(&*self.inner.武.read().unwrap())) + fractal_to_py(py, Arc::clone(&*self.inner.武.read())) } #[getter] @@ -423,7 +372,7 @@ impl 虚线Py { #[getter] fn 模式(&self) -> String { - self.inner.模式.read().unwrap().clone() + self.inner.模式.read().clone() } #[getter(_特征序列_显示)] @@ -439,7 +388,7 @@ impl 虚线Py { #[getter] fn 特征序列(&self, py: Python<'_>) -> PyResult> { let list = pyo3::types::PyList::empty(py); - for item in self.inner.特征序列.read().unwrap().iter() { + for item in self.inner.特征序列.read().iter() { match item { Some(feat) => list.append(segfeat_to_py(py, Arc::clone(feat)))?, None => { @@ -460,18 +409,13 @@ impl 虚线Py { self.inner .确认K线 .read() - .unwrap() .as_ref() .map(|k| crate::kline_py::chan_kline_to_py(py, Arc::clone(k))) } #[getter] fn 前一缺口(&self) -> Option<缺口Py> { - self.inner - .前一缺口 - .read() - .unwrap() - .map(|q| 缺口Py { inner: q }) + self.inner.前一缺口.read().map(|q| 缺口Py { inner: q }) } #[getter] @@ -479,7 +423,6 @@ impl 虚线Py { self.inner .前一结束位置 .read() - .unwrap() .as_ref() .map(|d| dashed_to_py(py, Arc::clone(d))) } @@ -489,7 +432,7 @@ impl 虚线Py { #[getter] fn 基础序列(&self, py: Python<'_>) -> PyResult> { let list = pyo3::types::PyList::empty(py); - for d in self.inner.基础序列.read().unwrap().iter() { + for d in self.inner.基础序列.read().iter() { list.append(dashed_to_py(py, Arc::clone(d)))?; } Ok(list.into()) @@ -498,7 +441,7 @@ impl 虚线Py { #[getter] fn 实_中枢序列(&self, py: Python<'_>) -> PyResult> { let list = pyo3::types::PyList::empty(py); - for h in self.inner.实_中枢序列.read().unwrap().iter() { + for h in self.inner.实_中枢序列.read().iter() { list.append(hub_to_py(py, Arc::clone(h)))?; } Ok(list.into()) @@ -507,7 +450,7 @@ impl 虚线Py { #[getter] fn 虚_中枢序列(&self, py: Python<'_>) -> PyResult> { let list = pyo3::types::PyList::empty(py); - for h in self.inner.虚_中枢序列.read().unwrap().iter() { + for h in self.inner.虚_中枢序列.read().iter() { list.append(hub_to_py(py, Arc::clone(h)))?; } Ok(list.into()) @@ -516,7 +459,7 @@ impl 虚线Py { #[getter] fn 合_中枢序列(&self, py: Python<'_>) -> PyResult> { let list = pyo3::types::PyList::empty(py); - for h in self.inner.合_中枢序列.read().unwrap().iter() { + for h in self.inner.合_中枢序列.read().iter() { list.append(hub_to_py(py, Arc::clone(h)))?; } Ok(list.into()) @@ -528,7 +471,7 @@ impl 虚线Py { /// 笔序列 fn 笔序列(&self, py: Python<'_>) -> PyResult> { let list = pyo3::types::PyList::empty(py); - for d in self.inner.基础序列.read().unwrap().iter() { + for d in self.inner.基础序列.read().iter() { list.append(dashed_to_py(py, Arc::clone(d)))?; } Ok(list.into()) @@ -1005,12 +948,12 @@ impl 线段特征Py { #[getter] fn 标识(&self) -> String { - self.inner.标识.read().unwrap().clone() + self.inner.标识.read().clone() } #[setter] fn set_标识(&self, value: String) { - *self.inner.标识.write().unwrap() = value; + *self.inner.标识.write() = value; } #[getter] diff --git a/chanlun-py/src/types_py.rs b/chanlun-py/src/types_py.rs index 48ca8b0..0f79eda 100644 --- a/chanlun-py/src/types_py.rs +++ b/chanlun-py/src/types_py.rs @@ -22,8 +22,8 @@ * SOFTWARE. */ +use parking_lot::Mutex; use std::collections::HashMap; -use std::sync::Mutex; use pyo3::basic::CompareOp; use pyo3::prelude::*; @@ -37,7 +37,7 @@ pub fn 获取分型结构单例( py: Python<'_>, inner: chanlun::types::分型结构, ) -> Py<分型结构Py> { - let mut guard = 分型结构_单例缓存.lock().unwrap(); + let mut guard = 分型结构_单例缓存.lock(); if let Some(ref map) = *guard { return map[&(inner as u8)].clone_ref(py); } @@ -67,7 +67,7 @@ pub fn 获取相对方向单例( py: Python<'_>, inner: chanlun::types::相对方向, ) -> Py<相对方向Py> { - let mut guard = 相对方向_单例缓存.lock().unwrap(); + let mut guard = 相对方向_单例缓存.lock(); if let Some(ref map) = *guard { return map[&(inner as u8)].clone_ref(py); } @@ -312,6 +312,22 @@ impl 相对方向Py { chanlun::types::相对方向::分析(前高, 前低, 后高, 后低), ) } + + /// 从可选方向序列中随机选取指定数量 + #[classmethod] + #[pyo3(signature = (数量, 可选方向, 可重复 = true))] + fn 从序列中机选( + _cls: &Bound<'_, PyType>, + 数量: usize, + 可选方向: Vec>, + 可重复: bool, + py: Python<'_>, + ) -> Vec> { + let dirs: Vec = + 可选方向.iter().map(|d| d.borrow(py).inner).collect(); + let result = chanlun::types::相对方向::从序列中机选(数量, &dirs, 可重复); + result.iter().map(|d| 获取相对方向单例(py, *d)).collect() + } } // ========== 分型结构 ========== diff --git a/chanlun-py/tests/test_all.py b/chanlun-py/tests/test_all.py index 39985ff..87f68d9 100644 --- a/chanlun-py/tests/test_all.py +++ b/chanlun-py/tests/test_all.py @@ -1693,8 +1693,8 @@ class Test指标挂载(unittest.TestCase): for i in range(100): if i == 50: obs.配置.MACD_参数列表 = [ - ("macd", 12, 26, 9), - ("macd_10_20_7", 10, 20, 7), + ("macd", "收", 12, 26, 9), + ("macd_10_20_7", "收", 10, 20, 7), ] obs.增加原始K线(self._make_k(i)) @@ -1708,9 +1708,9 @@ class Test指标挂载(unittest.TestCase): for i in range(80): if i == 40: - obs.配置.MACD_参数列表 = [("macd", 12, 26, 9), ("macd_fast", 5, 13, 5)] - obs.配置.RSI_周期列表 = [("rsi", 14), ("rsi_7", 7)] - obs.配置.KDJ_参数列表 = [("kdj", 9, 3, 3), ("kdj_5", 5, 2, 2)] + obs.配置.MACD_参数列表 = [("macd", "收", 12, 26, 9), ("macd_fast", "收", 5, 13, 5)] + obs.配置.RSI_周期列表 = [("rsi", "收", 14, 13, 75.0, 25.0), ("rsi_7", "收", 7, 6, 75.0, 25.0)] + obs.配置.KDJ_参数列表 = [("kdj", "收", 9, 3, 3, 80.0, 20.0), ("kdj_5", "收", 5, 2, 2, 80.0, 20.0)] obs.增加原始K线(self._make_k(i)) for k in obs.普通K线序列: @@ -1721,7 +1721,7 @@ class Test指标挂载(unittest.TestCase): def test_回填后增量计算一致(self): """回填后的指标值应与从头计算一致.""" cfg_full = chanlun.缠论配置() - cfg_full.MACD_参数列表 = [("macd", 12, 26, 9), ("macd_extra", 8, 16, 6)] + cfg_full.MACD_参数列表 = [("macd", "收", 12, 26, 9), ("macd_extra", "收", 8, 16, 6)] obs_full = chanlun.观察者("btcusd", 300, cfg_full) cfg_late = chanlun.缠论配置() @@ -1729,7 +1729,7 @@ class Test指标挂载(unittest.TestCase): for i in range(100): if i == 50: - obs_late.配置.MACD_参数列表 = [("macd", 12, 26, 9), ("macd_extra", 8, 16, 6)] + obs_late.配置.MACD_参数列表 = [("macd", "收", 12, 26, 9), ("macd_extra", "收", 8, 16, 6)] obs_full.增加原始K线(self._make_k(i)) obs_late.增加原始K线(self._make_k(i)) @@ -2542,13 +2542,21 @@ class Test缠论配置双端一致(unittest.TestCase): d_rs = cfg_rs.to_dict() d_py = cfg_py.to_dict() + # Rust (serde_json) 产 list-of-list,Python 产 list-of-tuple,统一为 list 比较 + def _normalize(v): + if isinstance(v, list): + return [_normalize(x) for x in v] + if isinstance(v, tuple): + return [_normalize(x) for x in v] + return v + mismatches = [] for k in d_rs: v_rs = d_rs[k] v_py = d_py.get(k) if v_rs is None and v_py is None: continue - if v_rs != v_py: + if _normalize(v_rs) != _normalize(v_py): mismatches.append(f" {k}: R={v_rs!r} P={v_py!r}") self.assertEqual(len(mismatches), 0, f"to_dict 值不一致 ({len(mismatches)}处):\n" + "\n".join(mismatches[:10])) @@ -2625,15 +2633,13 @@ class Test缠论配置双端一致(unittest.TestCase): from chanlun import chan cfg_rs, cfg_py = self._make_configs() - update = {"标识": "copied", "推送K线": False, "笔内元素数量": 10} + update = {"标识": "copied", "买卖点偏移": 5, "笔内元素数量": 10} copy_rs = cfg_rs.model_copy(update) copy_py = cfg_py.model_copy(update) self.assertEqual(copy_rs.标识, copy_py.标识) self.assertEqual(copy_rs.笔内元素数量, copy_py.笔内元素数量) - self.assertFalse(copy_rs.推送K线) - self.assertFalse(copy_py.推送K线) # 未更新字段保持原值一致 self.assertEqual(copy_rs.买卖点偏移, copy_py.买卖点偏移) @@ -2660,8 +2666,6 @@ class Test缠论配置双端一致(unittest.TestCase): cfg_rs = chanlun.缠论配置.不推送() cfg_py = chan.缠论配置.不推送() - self.assertFalse(cfg_rs.推送K线) - self.assertFalse(cfg_py.推送K线) self.assertFalse(cfg_rs.图表展示) self.assertFalse(cfg_py.图表展示) self.assertEqual(cfg_rs.笔内元素数量, cfg_py.笔内元素数量) @@ -2776,5 +2780,74 @@ class Test缠论配置双端一致(unittest.TestCase): self.assertFalse(diff_py[k], f"不推送差异字段 {k} 应为 False") +class Test生成K线双端一致(unittest.TestCase): + """根据当前K线生成新K线 Rust vs chan.py 输出一致.""" + + def test_生成K线_居中各方向双端一致(self): + """居中模式下各方向生成K线双端OHLC一致(居中=确定性输出)""" + import chanlun + from chanlun import chan + + bar_rs = chanlun.K线.创建普K("btcusd", 1000000000, 50000, 50200, 49800, 50100, 100, 0, 300) + bar_py = chan.K线.创建普K("btcusd", chan.转化为时间戳(1000000000), 50000, 50200, 49800, 50100, 100, 0, 300) + + directions = { + "向上": 0, + "向下": 1, + "向上缺口": 2, + "向下缺口": 3, + "衔接向上": 4, + "衔接向下": 5, + } + py_dirs = { + "向上": chan.相对方向.向上, + "向下": chan.相对方向.向下, + "向上缺口": chan.相对方向.向上缺口, + "向下缺口": chan.相对方向.向下缺口, + "衔接向上": chan.相对方向.衔接向上, + "衔接向下": chan.相对方向.衔接向下, + } + + for name in directions: + new_rs = bar_rs.根据当前K线生成新K线(directions[name], 居中=True) + new_py = bar_py.根据当前K线生成新K线(py_dirs[name], 居中=True) + + # 居中模式下,高和低是确定性的(偏移=高低差*0.5) + # 开盘价/收盘价/成交量含随机,不比较 + tol = 1.0 + abs(new_py.高) * 1e-6 + self.assertAlmostEqual(new_rs.高, new_py.高, delta=tol, msg=f"{name}: 最高价不一致 (R={new_rs.高}, P={new_py.高})") + self.assertAlmostEqual(new_rs.低, new_py.低, delta=tol, msg=f"{name}: 最低价不一致 (R={new_rs.低}, P={new_py.低})") + + # 时间戳和序号 + self.assertEqual(new_rs.序号, new_py.序号) + self.assertEqual(int(new_rs.时间戳), int(chan.转化为时间戳_数字(new_py.时间戳))) + + def test_生成K线_居中外推验证(self): + """居中向上生成:新K线的高/低应整体高于原K线.""" + import chanlun + + bar = chanlun.K线.创建普K("btcusd", 1000000000, 50000, 50200, 49800, 50100, 100, 0, 300) + new = bar.根据当前K线生成新K线(0, 居中=True) + + self.assertGreater(new.高, bar.高, "向上:新高应高于原高") + self.assertGreater(new.低, bar.低, "向上:新低应高于原低") + + new_down = bar.根据当前K线生成新K线(1, 居中=True) + self.assertLess(new_down.高, bar.高, "向下:新高应低于原高") + self.assertLess(new_down.低, bar.低, "向下:新低应低于原低") + + def test_生成K线_衔接验证(self): + """衔接向上:新K线的低 = 原K线的高(无缝衔接).""" + import chanlun + + bar = chanlun.K线.创建普K("btcusd", 1000000000, 50000, 50200, 49800, 50100, 100, 0, 300) + new = bar.根据当前K线生成新K线(4, 居中=True) + + self.assertAlmostEqual(new.低, bar.高, delta=1e-6, msg="衔接向上:新低应=原高") + + new_down = bar.根据当前K线生成新K线(5, 居中=True) + self.assertAlmostEqual(new_down.高, bar.低, delta=1e-6, msg="衔接向下:新高应=原低") + + if __name__ == "__main__": unittest.main() diff --git a/chanlun-py/tests/test_position_update.py b/chanlun-py/tests/test_position_update.py new file mode 100644 index 0000000..3981ef9 --- /dev/null +++ b/chanlun-py/tests/test_position_update.py @@ -0,0 +1,301 @@ +"""Position.update() 集成测试 — 验证 Rust 核心状态机与 Python 行为一致。 + +测试覆盖: +- 基础开多/开空/平多/平空 +- 间隔限制 +- 止损/超时 +- pairs 盈亏计算 +- 时间倒退容错 +- 空信号字典容错 +- dump/load 含状态 +""" + +import pytest +from datetime import datetime, timezone +from chanlun._chanlun import Position, Event, Factor, Signal, Operate + + +# ---- 辅助函数 ---- + + +def 开多事件(k3="中枢", v2="三买"): + s = Signal(k1="14400", k2="D1MO3", k3=k3, v2=v2) + return Event(Operate.LO, [Factor(signals_all=[s])]) + + +def 平多事件(k3="中枢", v2="三卖"): + s = Signal(k1="14400", k2="D1MO3", k3=k3, v2=v2) + return Event(Operate.LE, [Factor(signals_all=[s])]) + + +def 开空事件(k3="中枢", v2="三卖"): + s = Signal(k1="14400", k2="D1MO3", k3=k3, v2=v2) + return Event(Operate.SO, [Factor(signals_all=[s])]) + + +def 平空事件(k3="中枢", v2="三买"): + s = Signal(k1="14400", k2="D1MO3", k3=k3, v2=v2) + return Event(Operate.SE, [Factor(signals_all=[s])]) + + +def 信号字典(symbol="btc", dt=None, close=50000.0, bid=1, **kwargs): + """构造信号字典(含 OHLCV + 信号键)。""" + if dt is None: + dt = datetime.now(timezone.utc) + d = {"symbol": symbol, "dt": dt, "close": close, "id": bid} + d.update(kwargs) + return d + + +# ---- 构造 ---- + + +def test_构造状态初始化为默认值(): + p = Position(symbol="btc", opens=[开多事件()], name="测试") + assert p.pos == 0 + assert p.pos_changed is False + assert p.operates == [] + assert p.holds == [] + + +# ---- update: 开仓 ---- + + +def test_update_开多(): + p = Position(symbol="btc", opens=[开多事件()], name="测试") + p.update(信号字典(**{"14400_D1MO3_中枢": "任意_三买_任意_0"})) + assert p.pos == 1 + assert p.pos_changed is True + assert len(p.operates) == 1 + assert p.operates[0]["op"] == Operate.LO + assert len(p.holds) == 1 + assert p.holds[0]["pos"] == 1 + + +def test_update_开空(): + p = Position(symbol="btc", opens=[开空事件()], name="测试") + p.update(信号字典(**{"14400_D1MO3_中枢": "任意_三卖_任意_0"})) + assert p.pos == -1 + assert p.operates[0]["op"] == Operate.SO + + +def test_update_开多_已持仓_不重复开仓(): + p = Position(symbol="btc", opens=[开多事件()], name="测试") + p.update(信号字典(dt=datetime(2020, 1, 1, tzinfo=timezone.utc), bid=1, **{"14400_D1MO3_中枢": "任意_三买_任意_0"})) + assert p.pos == 1 + assert len(p.operates) == 1 + # 第二次相同信号,已多头,不再开仓 + p.update(信号字典(dt=datetime(2020, 1, 1, 1, tzinfo=timezone.utc), bid=2, **{"14400_D1MO3_中枢": "任意_三买_任意_0"})) + assert p.pos == 1 + assert len(p.operates) == 1 # 无新操作 + + +# ---- update: 平仓 ---- + + +def test_update_开多后平多(): + p = Position(symbol="btc", opens=[开多事件()], exits=[平多事件()], name="测试") + # Step 1: LO + p.update(信号字典(dt=datetime(2020, 1, 1, tzinfo=timezone.utc), bid=1, **{"14400_D1MO3_中枢": "任意_三买_任意_0"})) + assert p.pos == 1 + # Step 2: LE (next day to allow exit when T0=False) + p.update(信号字典(dt=datetime(2020, 1, 2, tzinfo=timezone.utc), bid=2, **{"14400_D1MO3_中枢": "任意_三卖_任意_0"})) + assert p.pos == 0, f"Expected pos=0, got {p.pos}" + assert p.operates[-1]["op"] == Operate.LE + + +def test_update_开空后平空(): + p = Position(symbol="btc", opens=[开空事件()], exits=[平空事件()], name="测试") + p.update(信号字典(dt=datetime(2020, 1, 1, tzinfo=timezone.utc), bid=1, **{"14400_D1MO3_中枢": "任意_三卖_任意_0"})) + assert p.pos == -1 + p.update(信号字典(dt=datetime(2020, 1, 2, tzinfo=timezone.utc), bid=2, **{"14400_D1MO3_中枢": "任意_三买_任意_0"})) + assert p.pos == 0 + assert p.operates[-1]["op"] == Operate.SE + + +# ---- update: 止损 ---- + + +def test_update_多头止损(): + p = Position(symbol="btc", opens=[开多事件()], name="测试", stop_loss=500) + p.update(信号字典(dt=datetime(2020, 1, 1, tzinfo=timezone.utc), bid=1, close=50000.0, **{"14400_D1MO3_中枢": "任意_三买_任意_0"})) + assert p.pos == 1 + # Price drops to 47000: (47000/50000 - 1) = -0.06 = -600 BP < -500 BP stop_loss + p.update(信号字典(dt=datetime(2020, 1, 2, tzinfo=timezone.utc), bid=2, close=47000.0, **{"14400_D1MO3_中枢": "任意_无_任意_0"})) + assert p.pos == 0, "Should be stopped out" + assert "止损" in p.operates[-1]["op_desc"] + + +def test_update_空头止损(): + p = Position(symbol="btc", opens=[开空事件()], name="测试", stop_loss=500) + p.update(信号字典(dt=datetime(2020, 1, 1, tzinfo=timezone.utc), bid=1, close=50000.0, **{"14400_D1MO3_中枢": "任意_三卖_任意_0"})) + assert p.pos == -1 + # Price rises to 53000: (1 - 53000/50000) = -0.06 = -600 BP < -500 BP stop_loss + p.update(信号字典(dt=datetime(2020, 1, 2, tzinfo=timezone.utc), bid=2, close=53000.0, **{"14400_D1MO3_中枢": "任意_无_任意_0"})) + assert p.pos == 0, "Should be stopped out" + assert "止损" in p.operates[-1]["op_desc"] + + +# ---- update: 超时 ---- + + +def test_update_多头超时(): + p = Position(symbol="btc", opens=[开多事件()], name="测试", timeout=5) + p.update(信号字典(dt=datetime(2020, 1, 1, tzinfo=timezone.utc), bid=1, close=50000.0, **{"14400_D1MO3_中枢": "任意_三买_任意_0"})) + assert p.pos == 1 + # bid diff=9 > timeout=5 + p.update(信号字典(dt=datetime(2020, 1, 2, tzinfo=timezone.utc), bid=10, close=50000.0, **{"14400_D1MO3_中枢": "任意_无_任意_0"})) + assert p.pos == 0, "Should be timed out" + assert "超时" in p.operates[-1]["op_desc"] + + +# ---- update: 间隔限制 ---- + + +def test_update_间隔限制(): + p = Position(symbol="btc", opens=[开多事件()], name="测试", interval=3600) + # Create fresh position, open, test interval protection + p2 = Position(symbol="btc", opens=[开多事件()], name="测试", interval=3600) + p2.update(信号字典(dt=datetime(2020, 1, 1, tzinfo=timezone.utc), bid=1, close=50000, **{"14400_D1MO3_中枢": "任意_三买_任意_0"})) + assert len(p2.operates) == 1 + # Within interval, same day (T0=False) — no new open + p2.update(信号字典(dt=datetime(2020, 1, 1, 1, tzinfo=timezone.utc), bid=2, close=50000, **{"14400_D1MO3_中枢": "任意_三买_任意_0"})) + assert len(p2.operates) == 1 # No new operate (already long, interval not elapsed) + + +# ---- update: 边界条件 ---- + + +def test_update_时间倒退_跳过(): + p = Position(symbol="btc", opens=[开多事件()], name="测试") + dt1 = datetime(2020, 1, 2, tzinfo=timezone.utc) + dt2 = datetime(2020, 1, 1, tzinfo=timezone.utc) # earlier + p.update(信号字典(dt=dt1, **{"14400_D1MO3_中枢": "任意_三买_任意_0"})) + operates_before = len(p.operates) + p.update(信号字典(dt=dt2, **{"14400_D1MO3_中枢": "任意_三买_任意_0"})) + assert len(p.operates) == operates_before # skipped + + +def test_update_空事件列表(): + p = Position(symbol="btc", opens=[], name="空") + p.update(信号字典(dt=datetime(2020, 1, 1, tzinfo=timezone.utc), **{"14400_D1MO3_中枢": "任意_三买_任意_0"})) + assert p.pos == 0 + assert len(p.holds) == 1 + + +def test_update_无匹配事件_仅追加holds(): + p = Position(symbol="btc", opens=[开多事件()], name="测试") + p.update(信号字典(dt=datetime(2020, 1, 1, tzinfo=timezone.utc), **{"14400_D1MO3_中枢": "任意_无_任意_0"})) + assert p.pos == 0 + assert p.operates == [] + assert len(p.holds) == 1 + + +def test_update_缺键错误(): + """信号字典缺少事件所需 key 时抛 ValueError。""" + p = Position(symbol="btc", opens=[开多事件()], name="测试") + with pytest.raises(ValueError, match="不在信号列表中"): + # 空信号字典缺少 "14400_D1MO3_中枢" 键 + p.update(信号字典(dt=datetime(2020, 1, 1, tzinfo=timezone.utc))) + + +def test_update_T0模式_同一天可操作(): + p = Position(symbol="btc", opens=[开多事件()], exits=[平多事件()], name="测试", T0=True) + dt = datetime(2020, 1, 1, 0, 0, tzinfo=timezone.utc) + p.update(信号字典(dt=dt, bid=1, **{"14400_D1MO3_中枢": "任意_三买_任意_0"})) + assert p.pos == 1 + # Same day, T0=True → 允许平仓 + p.update(信号字典(dt=dt.replace(hour=1), bid=2, **{"14400_D1MO3_中枢": "任意_三卖_任意_0"})) + assert p.pos == 0 + + +# ---- pairs ---- + + +def test_pairs_空(): + p = Position(symbol="btc", opens=[开多事件()], name="测试") + assert p.pairs == [] + + +def test_pairs_单笔开平_多头盈利(): + p = Position(symbol="btc", opens=[开多事件()], exits=[平多事件()], name="测试") + p.update(信号字典(dt=datetime(2020, 1, 1, tzinfo=timezone.utc), bid=1, close=50000.0, **{"14400_D1MO3_中枢": "任意_三买_任意_0"})) + p.update(信号字典(dt=datetime(2020, 1, 2, tzinfo=timezone.utc), bid=2, close=51000.0, **{"14400_D1MO3_中枢": "任意_三卖_任意_0"})) + pairs = p.pairs + assert len(pairs) == 1 + assert pairs[0]["交易方向"] == "多头" + assert pairs[0]["开仓价格"] == 50000.0 + assert pairs[0]["平仓价格"] == 51000.0 + # (51000/50000 - 1) * 10000 = 200 BP + assert pairs[0]["盈亏比例"] == pytest.approx(200.0, abs=0.1) + + +def test_pairs_单笔开平_空头盈利(): + p = Position(symbol="btc", opens=[开空事件()], exits=[平空事件()], name="测试") + p.update(信号字典(dt=datetime(2020, 1, 1, tzinfo=timezone.utc), bid=1, close=50000.0, **{"14400_D1MO3_中枢": "任意_三卖_任意_0"})) + p.update(信号字典(dt=datetime(2020, 1, 2, tzinfo=timezone.utc), bid=2, close=48000.0, **{"14400_D1MO3_中枢": "任意_三买_任意_0"})) + pairs = p.pairs + assert len(pairs) == 1 + assert pairs[0]["交易方向"] == "空头" + assert pairs[0]["开仓价格"] == 50000.0 + assert pairs[0]["平仓价格"] == 48000.0 + # (1 - 48000/50000) * 10000 = 400 BP + assert pairs[0]["盈亏比例"] == pytest.approx(400.0, abs=0.1) + + +def test_pairs_持仓天数(): + p = Position(symbol="btc", opens=[开多事件()], exits=[平多事件()], name="测试") + p.update(信号字典(dt=datetime(2020, 1, 1, tzinfo=timezone.utc), bid=1, close=50000.0, **{"14400_D1MO3_中枢": "任意_三买_任意_0"})) + # 3 days later + p.update(信号字典(dt=datetime(2020, 1, 4, tzinfo=timezone.utc), bid=2, close=51000.0, **{"14400_D1MO3_中枢": "任意_三卖_任意_0"})) + assert p.pairs[0]["持仓天数"] == pytest.approx(3.0, abs=0.1) + + +# ---- dump/load ---- + + +def test_dump_with_data(): + p = Position(symbol="btc", opens=[开多事件()], name="测试") + p.update(信号字典(dt=datetime(2020, 1, 1, tzinfo=timezone.utc), bid=1, close=50000.0, **{"14400_D1MO3_中枢": "任意_三买_任意_0"})) + raw = p.dump(with_data=True) + assert "pairs" in raw + assert "holds" in raw + assert raw["symbol"] == "btc" + assert len(raw["holds"]) == 1 + + +def test_dump_without_data(): + p = Position(symbol="btc", opens=[开多事件()], name="测试") + raw = p.dump(with_data=False) + assert "symbol" in raw + assert "pairs" not in raw + + +def test_load_roundtrip(): + from chanlun.chan_external import Position as PyPos + + p = PyPos(symbol="btc", opens=[开多事件()], name="测试", timeout=500) + p2 = PyPos.load(p.dump()) + assert p2.symbol == p.symbol + assert p2.name == p.name + assert p2.timeout == 500 + assert p2.pos == 0 # 新构造,状态初始 + + +# ---- 信号字典 dt 类型兼容 ---- + + +def test_update_dt_支持int时间戳(): + """验证 update() 支持 int Unix 时间戳(除 datetime 外)。""" + p = Position(symbol="btc", opens=[开多事件()], name="测试") + ts = int(datetime(2020, 1, 1, tzinfo=timezone.utc).timestamp()) + p.update(信号字典(dt=ts, **{"14400_D1MO3_中枢": "任意_三买_任意_0"})) + assert p.pos == 1 + + +def test_update_dt_支持float时间戳(): + """验证 update() 支持 float Unix 时间戳。""" + p = Position(symbol="btc", opens=[开多事件()], name="测试") + ts = datetime(2020, 1, 1, tzinfo=timezone.utc).timestamp() + p.update(信号字典(dt=ts, **{"14400_D1MO3_中枢": "任意_三买_任意_0"})) + assert p.pos == 1 diff --git a/chanlun-py/tests/test_signal_primitives.py b/chanlun-py/tests/test_signal_primitives.py new file mode 100644 index 0000000..db8ef07 --- /dev/null +++ b/chanlun-py/tests/test_signal_primitives.py @@ -0,0 +1,211 @@ +"""信号原语 Rust 移植后的跨语言一致性与边界行为测试。 + +验证 chanlun._chanlun 的 Signal/Factor/Event/Operate/Position 与移植前 Python 版本 +行为一致(name hash 除外,已改为 Rust 确定性哈希)。 +""" + +import pytest +from chanlun._chanlun import Signal, Factor, Event, Operate, Position + + +# ---- Signal ---- + + +def test_signal_parse_and_props(): + s = Signal("14400_D1MO3_中枢_中枢段DEA穿越2_三买_偏移0_100") + assert s.k1 == "14400" and s.k3 == "中枢" and s.v2 == "三买" and s.score == 100 + assert s.key == "14400_D1MO3_中枢" + assert s.value == "中枢段DEA穿越2_三买_偏移0_100" + assert repr(s) == "Signal('14400_D1MO3_中枢_中枢段DEA穿越2_三买_偏移0_100')" + + +def test_signal_from_fields_default_任意(): + s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买") + # v1/v3 缺省为 任意 → key 过滤后保留全部 k;value 含 任意 + assert s.key == "14400_D1MO3_中枢" + assert s.value == "任意_三买_任意_0" + + +def test_signal_score_out_of_range(): + with pytest.raises(ValueError): + Signal(k1="a", k2="b", k3="c", score=101) + + +def test_signal_is_match_missing_key_raises(): + s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买") + with pytest.raises(ValueError): + s.is_match({}) + + +def test_signal_is_match_non_str_value_false(): + s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买") + assert s.is_match({"14400_D1MO3_中枢": 123}) is False + + +def test_signal_is_match_hit(): + s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买") + assert s.is_match({"14400_D1MO3_中枢": "x_三买_y_100"}) is True + + +def test_signal_is_match_v2_mismatch(): + s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买") + assert s.is_match({"14400_D1MO3_中枢": "x_三卖_y_100"}) is False + + +# ---- Factor ---- + + +def test_factor_empty_all_raises(): + with pytest.raises(ValueError): + Factor(signals_all=[]) + + +def test_factor_name_deterministic(): + s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买") + f1 = Factor(signals_all=[s]) + f2 = Factor(signals_all=[Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")]) + assert f1.name == f2.name + assert f1.name.startswith("#") and len(f1.name) == 5 # #XXXX + + +def test_factor_not_short_circuit(): + s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买") + f = Factor(signals_all=[s], signals_not=[s]) + assert f.is_match({"14400_D1MO3_中枢": "x_三买_y_100"}) is False + + +def test_factor_unique_signals_is_property(): + """unique_signals 必须是 property(匹配 Python @property),不带括号访问。""" + s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买") + f = Factor(signals_all=[s]) + assert f.unique_signals == [s.signal] # 属性访问,非方法调用 + + +# ---- Event ---- + + +def test_event_empty_factors_raises(): + with pytest.raises(ValueError): + Event(Operate.LO, []) + + +def test_event_name_uses_operate(): + s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买") + e = Event(Operate.LO, [Factor(signals_all=[s])]) + assert e.name.startswith("开多#") + + +def test_event_match_returns_factor_name(): + s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买") + e = Event(Operate.LO, [Factor(signals_all=[s])]) + ok, name = e.is_match({"14400_D1MO3_中枢": "x_三买_y_100"}) + assert ok and name + + +def test_event_multi_factor_or(): + """多 Factor OR:两 key 都在场,第一个不匹配、第二个匹配 → 返回第二个因子名。""" + base = "14400" + f1 = Factor(signals_all=[Signal(k1=base, k2="D1MO3", k3="中枢A", v2="三买")]) + f2 = Factor(signals_all=[Signal(k1=base, k2="D1MO3", k3="中枢B", v2="三买")]) + e = Event(Operate.LO, [f1, f2]) + d = {"14400_D1MO3_中枢A": "x_三卖_y_100", "14400_D1MO3_中枢B": "x_三买_y_100"} + ok, name = e.is_match(d) + assert ok and name == f2.name + + +# ---- Operate ---- + + +def test_operate_value_and_eq(): + assert Operate.LO.value == "开多" + assert Operate.LE.value == "平多" + assert Operate.LO == Operate.LO + assert Operate.LO in [Operate.LO, Operate.SO] # update() 内部用法 + + +# ---- Position(Rust 基类 + Python 子类)---- + + +def test_position_requires_name(): + s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买") + e = Event(Operate.LO, [Factor(signals_all=[s])]) + with pytest.raises((ValueError, TypeError)): + Position(symbol="btc", opens=[e]) + + +def test_position_config_getters(): + s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买") + e = Event(Operate.LO, [Factor(signals_all=[s])]) + p = Position(symbol="btc", opens=[e], name="中枢", timeout=500, stop_loss=200, T0=True) + assert p.symbol == "btc" and p.name == "中枢" + assert p.timeout == 500 and p.stop_loss == 200 and p.T0 is True + assert len(p.events) == 1 + assert p.unique_signals == [s.signal] + + +def test_position_subclassable_with_state(): + """验证 Rust 基类可被 Python 子类化,状态字段由 Rust 初始化。 + + pos/pos_changed/operates/holds 等状态字段由 Rust 基类提供(只读 getter), + 初始值在构造时由 Rust #[new] 自动初始化。 + """ + s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买") + e = Event(Operate.LO, [Factor(signals_all=[s])]) + p = Position(symbol="btc", opens=[e], name="中枢") + assert p.name == "中枢" # Rust 基类 getter + assert p.pos == 0 # Rust 初始化为 0 (空仓) + assert p.pos_changed is False + assert p.operates == [] + assert p.holds == [] + + +# ---- 序列化 dump/load ---- + + +def test_operate_from_value(): + assert Operate.from_value("开多") == Operate.LO + assert Operate.from_value("平空") == Operate.SE + with pytest.raises(ValueError): + Operate.from_value("不存在") + + +def test_factor_dump_load_roundtrip(): + s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买") + f = Factor(signals_all=[s], name="测试") + d = f.dump() + assert d["name"] == f.name + assert d["signals_all"] == [s.signal] + assert d["signals_any"] == [] and d["signals_not"] == [] + f2 = Factor.load(d) + assert f2.name == f.name # 确定性哈希 → 同输入同名 + assert f2.unique_signals == f.unique_signals + + +def test_event_dump_load_roundtrip(): + s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买") + e = Event(Operate.LO, [Factor(signals_all=[s])]) + d = e.dump() + assert d["operate"] == "开多" + assert len(d["factors"]) == 1 + e2 = Event.load(d) + assert e2.name == e.name + assert e2.operate == Operate.LO + + +def test_position_dump_load_roundtrip(): + """Position 序列化:Rust 基类 dump 配置 + Python 子类 with_data/load 返回子类实例。""" + from chanlun.chan_external import Position as PositionExt, Signal as S, Factor as F, Event as E, Operate as O + + e = E(O.LO, [F(signals_all=[S(k1="14400", k2="D1MO3", k3="中枢", v2="三买")])]) + p = PositionExt(symbol="btc", opens=[e], name="中枢", timeout=500, T0=True) + raw = p.dump() + assert raw["symbol"] == "btc" and raw["T0"] is True and raw["timeout"] == 500 + assert len(raw["opens"]) == 1 + # with_data 附加 pairs/holds + raw2 = p.dump(with_data=True) + assert "pairs" in raw2 and "holds" in raw2 + # load 返回子类实例(含状态字段) + p2 = PositionExt.load(raw) + assert type(p2) is PositionExt + assert p2.symbol == "btc" and p2.name == "中枢" and p2.timeout == 500 + assert p2.pos == 0 # 子类状态已初始化 diff --git a/chanlun-signal-macros/.gitignore b/chanlun-signal-macros/.gitignore new file mode 100644 index 0000000..b83d222 --- /dev/null +++ b/chanlun-signal-macros/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/chanlun-signal-macros/Cargo.lock b/chanlun-signal-macros/Cargo.lock new file mode 100644 index 0000000..b69eac1 --- /dev/null +++ b/chanlun-signal-macros/Cargo.lock @@ -0,0 +1,47 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "chanlun-signal-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/chanlun-signal-macros/Cargo.toml b/chanlun-signal-macros/Cargo.toml new file mode 100644 index 0000000..9222097 --- /dev/null +++ b/chanlun-signal-macros/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "chanlun-signal-macros" +version = "0.1.0" +edition = "2024" +license = "MIT" +description = "chanlun 信号注册 proc-macro(#[signal])" + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2", features = ["full"] } +quote = "1" +proc-macro2 = "1" diff --git a/chanlun-signal-macros/src/lib.rs b/chanlun-signal-macros/src/lib.rs new file mode 100644 index 0000000..5060589 --- /dev/null +++ b/chanlun-signal-macros/src/lib.rs @@ -0,0 +1,100 @@ +//! chanlun 信号注册 proc-macro。 +//! +//! 第三方代码声明:`#[signal]` 注册机制参考 czsc 项目 +//! (https://github.com/waditu/czsc,Apache License 2.0),已简化适配 +//! (无 category / TaCache,签名固定为 fn(&观察者, &HashMap) -> Vec)。 + +use proc_macro::TokenStream; +use quote::quote; +use syn::parse::Parser; +use syn::punctuated::Punctuated; +use syn::{Expr, ExprLit, ItemFn, Lit, Meta, Token}; + +/// `#[signal(name = "foo_V230101", template = "{freq}_D1_foo")]` +/// +/// 校验:函数名含 `_V<数字>`;`name` 与函数名一致;`name`/`template` 非空。 +/// 生成:一个 `static` SignalDescriptor + `inventory::submit!`。 +/// +/// 路径:默认 `crate::signal::registry::`(chanlun crate 内部使用)。 +/// 外部 crate 使用需指定 `crate_path = "::chanlun"`。 +#[proc_macro_attribute] +pub fn signal(attr: TokenStream, item: TokenStream) -> TokenStream { + let parser = Punctuated::::parse_terminated; + let metas = match parser.parse(attr) { + Ok(m) => m, + Err(e) => return e.to_compile_error().into(), + }; + + let mut name: Option = None; + let mut template: Option = None; + let mut crate_path: Option = None; + for m in metas { + if let Meta::NameValue(nv) = m + && let Some(ident) = nv.path.get_ident() + && let Expr::Lit(ExprLit { lit: Lit::Str(v), .. }) = nv.value + { + match ident.to_string().as_str() { + "name" => name = Some(v.value()), + "template" => template = Some(v.value()), + "crate_path" => crate_path = Some(v.value()), + _ => {} + } + } + } + + let f: ItemFn = match syn::parse(item) { + Ok(v) => v, + Err(e) => return e.to_compile_error().into(), + }; + + let name = name.unwrap_or_default(); + let template = template.unwrap_or_default(); + let fn_ident = &f.sig.ident; + let fn_name = fn_ident.to_string(); + + let mut errors = Vec::new(); + if name.is_empty() || template.is_empty() { + errors.push(quote! { compile_error!("#[signal] name/template 不能为空"); }); + } + if name != fn_name { + errors.push(quote! { compile_error!("#[signal] name 必须与函数名一致"); }); + } + // 函数名须含 _V<数字> + let 有版本 = fn_name + .rsplit_once("_V") + .map(|(_, v)| !v.is_empty() && v.chars().all(|c| c.is_ascii_digit())) + .unwrap_or(false); + if !有版本 { + errors.push(quote! { compile_error!("#[signal] 函数名必须含 _V<版本号>,如 foo_V230101"); }); + } + + if !errors.is_empty() { + let errs = errors.into_iter(); + return quote! { #(#errs)* }.into(); + } + + let descriptor_ident = syn::Ident::new( + &format!("__SIG_DESC_{}", fn_name).to_uppercase(), + fn_ident.span(), + ); + + let path = crate_path.unwrap_or_else(|| "crate".to_string()); + let _registry_path: syn::Path = syn::parse_str(&format!("{path}::signal::registry")).unwrap(); + let signal_fn: syn::Type = syn::parse_str(&format!("{path}::signal::registry::SignalFn")).unwrap(); + let signal_desc: syn::Type = syn::parse_str(&format!("{path}::signal::registry::SignalDescriptor")).unwrap(); + + let expanded = quote! { + #f + + #[allow(non_upper_case_globals)] + static #descriptor_ident: #signal_desc = + #signal_desc { + name: #name, + template: #template, + func: #fn_ident as #signal_fn, + }; + + inventory::submit! { #descriptor_ident } + }; + expanded.into() +} diff --git a/chanlun/Cargo.toml b/chanlun/Cargo.toml index 788264e..5936252 100644 --- a/chanlun/Cargo.toml +++ b/chanlun/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "chanlun" -version = "26.6.3" +version = "26.6.4" edition = "2024" license = "MIT" description = "基于缠论(缠中说禅)理论的量化技术分析核心库,支持流式数据处理和多周期联立分析。" @@ -17,6 +17,9 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" byteorder = "1" chrono = { version = "0.4", features = ["serde"] } -cached = "1" +parking_lot = "0.12" tracing = "0.1" -tracing-subscriber = "0.3" +fastrand = "2" +sha2 = "0.10" +inventory = "0.3" +chanlun-signal-macros = { path = "../chanlun-signal-macros" } diff --git a/chanlun/README.md b/chanlun/README.md index a081c41..9a7e81a 100644 --- a/chanlun/README.md +++ b/chanlun/README.md @@ -3,18 +3,18 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![Rust](https://img.shields.io/badge/rust-1.85%2B-orange.svg)](https://www.rust-lang.org) [![edition](https://img.shields.io/badge/edition-2024-9cf.svg)](https://doc.rust-lang.org/edition-guide/rust-2024/) +[![tests](https://img.shields.io/badge/tests-199%20passed-green.svg)](.) -基于[缠中说禅](https://zh.wikipedia.org/wiki/%E7%BC%A0%E4%B8%AD%E8%AF%B4%E7%A6%85)理论的量化技术分析核心库,将 Python 参考实现 (`chan.py`, ~4200行) 完整移植为 Rust,同时保持了 API 的完全兼容。 +[缠中说禅](https://zh.wikipedia.org/wiki/%E7%BC%A0%E4%B8%AD%E8%AF%B4%E7%A6%85) 理论的 Rust 高性能实现——**约 14,000 行 Rust(2024 edition)**,覆盖缠论完整算法体系:K线包含处理、分型识别、笔/线段划分(含特征序列与缺口修正)、中枢识别(延伸/扩展/多级)、背驰检测(MACD/斜率/测度)、买卖点生成(18 种类型)、技术指标计算(MACD/RSI/KDJ/BOLL/均线)、信号匹配框架、编译期信号注册表与 .so 动态插件系统。 -**约 13,700 行 Rust 代码**,39 个源文件,覆盖缠论的完整算法体系:K线包含处理、分型识别、笔划分、线段划分(含特征序列与缺口处理)、中枢识别(含延伸/扩展/多级中枢)、背驰检测(MACD/斜率/测度三种方式)、买卖点识别(18种类型)、多周期联立分析。 - -核心 crate 位于 `chanlun/` 目录。Python 绑定通过独立的 [`chanlun-py`](../chanlun-py/) crate 实现,使用 PyO3 将 Rust 核心封装为 Python 模块,API 与 `chan.py` 完全一致。所有类型名、方法名、字段名均使用中文标识符,与 Python 版本保持 1:1 对应。 +Python 参考实现 `chan.py`(~4,200 行)已完整移植,API **1:1 兼容**——所有类型、方法、字段名均使用中文标识符。Python 绑定通过 [`chanlun-py`](../chanlun-py/) crate(PyO3)发布为 `chanlun` PyPI 包。 --- ## 目录 - [核心概念](#核心概念) +- [架构总览](#架构总览) - [项目结构](#项目结构) - [快速开始](#快速开始) - [数据管线](#数据管线) @@ -22,6 +22,8 @@ - [配置体系](#配置体系) - [算法模块](#算法模块) - [技术指标](#技术指标) +- [信号框架](#信号框架) +- [插件系统](#插件系统) - [线程安全与并发](#线程安全与并发) - [Python 绑定](#python-绑定) - [数据序列化](#数据序列化) @@ -32,16 +34,56 @@ ## 核心概念 -缠论是一种源自中国的技术分析理论,将价格走势分解为层级结构,从最底层K线逐渐向上构造: +缠论将价格走势分解为层级结构,从最底层K线向上逐级构造: ``` 原始K线 → 包含处理 → 缠论K线 → 顶底分型 → 笔 → 线段 → 中枢 → 买卖点 ``` -核心理念: -- **走势终完美**:任何级别的任何走势类型终要完成 +理论核心: +- **走势终完美**:任何级别的走势类型终要完成,不可长期存续 - **自同构性**:不同级别的走势呈现相同的形态结构,可通过递归分析进行多级别联立 -- **完全分类**:将走势分为上涨、下跌、盘整三种,每个节点都可通过买卖点找到操作依据 +- **完全分类**:三段式完全分类(上涨/下跌/盘整),每个节点都可通过买卖点找到操作依据 + +--- + +## 架构总览 + +``` +┌──────────────────────────────────────────────────────────────┐ +│ Python 层 │ +│ ┌─────────┐ ┌──────────────┐ ┌─────────────────────────┐ │ +│ │ main.py │ │ strategies.py│ │ signal_orchestrator.py │ │ +│ │ (Web UI)│ │ (回测引擎) │ │ (混合信号编排: Rust+Py) │ │ +│ └─────────┘ └──────────────┘ └─────────────────────────┘ │ +│ │ PyO3 FFI │ +├────────────────────────┼─────────────────────────────────────┤ +│ PyO3 绑定层 (chanlun-py) │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ config_py │ kline_py │ structure_py │ algorithm_py │ │ +│ │ business_py │ signal_py │ signal_engine_py │ cache │ │ +│ └────────────────────────────────────────────────────────┘ │ +│ │ Rust API │ +├────────────────────────┼─────────────────────────────────────┤ +│ Rust 核心 (chanlun) │ +│ ┌────────────────────────────────────────────────────────┐ │ +│ │ config │ types │ kline │ indicators │ algorithm │ │ +│ │ structure │ business │ signal (primitives + engine │ │ +│ │ + registry + ffi + functions) │ utils │ │ +│ └────────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────┘ +``` + +**关键设计决策**: + +| 决策 | 选择 | 原因 | +|------|------|------| +| 内部可变性 | `AtomicI64` / `SyncF64` / `RwLock` | 多 Arc 共享下原地修改(如动态替换笔的武) | +| 增量计算 | 只重新分析末尾元素 | O(1) 均摊,适合实时交易 | +| 浮点原子 | `SyncF64`(`AtomicU64` 位转换) | 跨线程原子读写价格字段 | +| 按需计算 | 缠K/分型/笔/线段/中枢全部惰性 | 避免不必要的计算开销 | +| 3 级线段递归 | 线段 → 线段<线段> → 线段<线段<线段>> | 多级自同构性分析 | +| 中文标识符 | `#![allow(non_snake_case)]` | 与 Python `chan.py` API 完全对应 | --- @@ -49,231 +91,262 @@ ``` chanlun/ -├── Cargo.toml # 依赖: serde, byteorder, chrono, cached, tracing +├── Cargo.toml # 依赖: serde, byteorder, chrono, sha2, inventory +├── README.md # 本文档 └── src/ - ├── lib.rs # 模块注册 (#![allow(non_snake_case)]) - ├── main.rs # 入口(CLI 工具) - ├── config.rs # 缠论配置 (62 字段, serde, 622 行) + ├── lib.rs # 模块注册 + 日志开关 + tracing 初始化 + ├── main.rs # CLI 工具 (read/synth 子命令) + ├── config.rs # 缠论配置 (44 字段, serde, model_fields/对比/往返) │ - ├── types/ # 基础类型 (5 文件, 514 行) - │ ├── mod.rs # 重导出: 买卖点类型, 相对方向, 分型结构, 缺口, SyncF64 - │ ├── direction.rs # 相对方向 (9 变体): 向上/向下/向上缺口/向下缺口/衔接向上/衔接向下/顺/逆/同 - │ ├── bsp_type.rs # 买卖点类型 (18 变体): 一二三买/卖 + T1/T1P/T2/T2S/T3A/T3B - │ ├── fractal.rs # 分型结构 (5 变体) + 有高低 trait - │ ├── gap.rs # 缺口: 高/低价格区间 + 居中截取 - │ └── sync_f64.rs # SyncF64: 基于 AtomicU64 的线程安全 f64 容器 + ├── types/ # 基础类型 (5 文件, ~514 行) + │ ├── mod.rs # 重导出 + │ ├── direction.rs # 相对方向 (9 变体): 向上/向下/缺口/衔接/顺/逆/同 + │ ├── bsp_type.rs # 买卖点类型 (18 变体) + │ ├── fractal.rs # 分型结构 (5 变体) + 有高低 trait + │ ├── gap.rs # 缺口: 价格区间 + 居中截取 + │ └── sync_f64.rs # SyncF64: AtomicU64 线程安全 f64 │ - ├── kline/ # K线层 (2 文件, 1113 行) - │ ├── mod.rs # - │ ├── bar.rs # 原始K线 (388 行): OHLCV + RwLock<指标容器>, Clone/Display, 大端字节序序列化 - │ └── chan_kline.rs # 缠论K线 (725 行): 经包含处理后的K线, 分型标记, AtomicI64/SyncF64 内部可变性 + ├── kline/ # K线层 (2 文件, ~1,113 行) + │ ├── bar.rs # 原始K线: OHLCV + RwLock<指标容器>, 48字节大端序列化 + │ └── chan_kline.rs # 缠论K线: 包含处理后, 分型标记, 内部可变性字段 │ - ├── indicators/ # 技术指标 (6 文件, 1589 行) - │ ├── mod.rs # K线取值: 开/高/低/收/高低均值/高低收均值/开高低收均值 - │ ├── macd.rs # 平滑异同移动平均线 (215 行): EMA 快慢线 + DIF/DEA/MACD柱 - │ ├── rsi.rs # 相对强弱指数 (240 行): Wilder SMA 平滑 + 超买超卖线 - │ ├── kdj.rs # 随机指标 (289 行): RSV → K → D → J + 超买超卖阈值 - │ ├── boll.rs # 布林带 (169 行): 中轨/上轨/下轨 + 带宽 - │ ├── calculator.rs # 指标计算器 (476 行): 增量计算 + 回填新指标 + 均线 - │ └── container.rs # 指标容器 (202 行): 注册表模式, 动态指标存取 + ├── indicators/ # 技术指标 (7 文件, ~1,589 行) + │ ├── macd.rs # MACD (EMA快慢线, DIF/DEA/MACD柱, 首次+增量) + │ ├── rsi.rs # RSI (Wilder SMA 平滑, 超买超卖) + │ ├── kdj.rs # KDJ (RSV→K→D→J, 超买超卖阈值) + │ ├── boll.rs # BOLL (中轨/上轨/下轨, 带宽) + │ ├── calculator.rs # 指标计算器: 增量计算 + 回填 + 均线 + │ └── container.rs # 指标容器: 注册表模式, 动态指标存取 │ - ├── algorithm/ # 核心算法 (4 文件, 4987 行) - │ ├── mod.rs # - │ ├── bi.rs # 笔划分算法 (1009 行): 递归分析, 笔弱化, 次级成笔, 实际高/低点 - │ ├── segment.rs # 线段划分算法 (2078 行): 特征序列, 缺口处理, 四种修正机制, 扩展线段 - │ ├── hub.rs # 中枢识别算法 (1041 行): 重叠检测, 延伸/扩展, 第三买卖点, 多级中枢 - │ └── divergence.rs # 背驰检测 (227 行): MACD背驰/斜率背驰/测度背驰 + 四种组合模式 + ├── algorithm/ # 核心算法 (4 文件, ~4,987 行) + │ ├── bi.rs # 笔划分: 递归分析, 弱化/次级/实际高低点 + │ ├── segment.rs # 线段划分: 特征序列, 缺口处理, 四种修正, 扩展线段 + │ ├── hub.rs # 中枢识别: 重叠检测, 延伸/扩展, 第三买卖点 + │ └── divergence.rs # 背驰检测: MACD/斜率/测度 + 四种组合模式 │ - ├── structure/ # 结构体 (4 文件, 2089 行) - │ ├── mod.rs # - │ ├── dash_line.rs # 虚线 (1712 行): 笔/线段的通用抽象, 买卖意义, MACD行为统计, 分段, 背驰检测 - │ ├── fractal_obj.rs # 分型 (344 行): 左中右构型, 分型模式开关, 强度判定, MACD分型匹配 - │ ├── segment_feat.rs # 线段特征 (668 行): 特征序列元素, 文/武取极值, 静态分析, 分型序列 - │ └── feat_fractal.rs # 特征分型 (62 行): 三线段特征元素构成的分型 + ├── structure/ # 结构体 (4 文件, ~2,089 行) + │ ├── dash_line.rs # 虚线: 笔/线段的通用抽象, MACD行为统计, 买卖意义 + │ ├── fractal_obj.rs # 分型: 左中右构型, 强度判定, MACD匹配 + │ ├── segment_feat.rs # 线段特征: 文/武取极值, 静态分析, 分型序列 + │ └── feat_fractal.rs # 特征分型: 三线段特征元素构成的分型 │ - ├── business/ # 业务层 (4 文件, 1648 行) - │ ├── mod.rs # - │ ├── observer.rs # 观察者 (1138 行): 单周期分析器, 流式管线, 3级线段/扩展/混合扩展 - │ ├── bsp.rs # 基础买卖点 + 买卖点工厂 (595 行): 18种类型, 偏移/失效/终结 - │ ├── synthesizer.rs # K线合成器 (161 行): 小周期→大周期合成, 时间戳对齐 - │ └── multi_frame.rs # 立体分析器 (192 行): 多周期联立, 合成器+观察者协调 + ├── business/ # 业务层 (4 文件, ~1,648 行) + │ ├── observer.rs # 观察者: 单周期流式分析器, 3级递归管线 + │ ├── bsp.rs # 买卖点工厂: 18种类型, 偏移/失效/终结 + │ ├── synthesizer.rs # K线合成器: 小周期→大周期合成 + │ └── multi_frame.rs # 立体分析器: 多周期联立, 合成器+观察者协调 │ - └── utils/ # 工具 (2 文件, 145 行) - ├── mod.rs # - ├── datetime.rs # 时间戳转换: 字符串↔Unix时间戳 - └── format.rs # 格式化: f64→最小字符串 (format_f64_g) + ├── signal/ # 信号框架 (14 文件, ~4,300 行) + │ ├── mod.rs # 重导出 + 匹配值/信号字典/缺键错误/sha256前4 + │ ├── signal.rs # Signal: czsc 风格七段key匹配 (k1~k3/v1~v3/score) + │ ├── factor.rs # Factor: 多信号all/not匹配 → name含哈希后缀 + │ ├── event.rs # Event: 因子匹配 → 分配操作(开多/平多/开空/平空) + │ ├── operate.rs # Operate: 5 种操作枚举 (开多/平多/开空/平空/持币) + │ ├── position.rs # Position: 完整仓位状态机 (~1,039行) + │ │ # update() 事件匹配 + LO/SO/LE/SE 状态转换 + │ │ # + 止损/超时/间隔检查 + pairs() 盈亏计算 + │ ├── params.rs # 参数提取: get_int/get_float/get_string/get_bool + │ ├── registry.rs # 双注册表: 编译时(LazyLock) + 动态(RwLock) + │ ├── engine.rs # 信号引擎: 批量执行信号函数 + 自动挂载指标 + │ ├── ffi.rs # C-ABI 导出: 供 .so 插件动态注册 + │ ├── functions/ # 信号函数实现 + │ │ ├── mod.rs # + │ │ ├── youwukuncheng.rs # 中枢第三买卖点 (~280行, 3种信号变体) + │ │ └── demo.rs # 示例信号 (~440行, 涨停/MACD金叉/MA/停顿分型/笔结束) + │ └── registry_macro_test.rs # #[signal] 宏端到端测试 + │ + └── utils/ # 工具 (2 文件, ~145 行) + ├── datetime.rs # 时间戳转换: 字符串↔Unix时间戳 + └── format.rs # 格式化: f64→最小字符串 (format_f64_g) ``` +### 依赖项 + +| 依赖 | 用途 | +|------|------| +| `serde` + `serde_json` | 配置序列化、信号参数解析 | +| `byteorder` | .nb 文件的大端字节序读写 | +| `chrono` | 时间戳<->日期转换(Position 交易日判断) | +| `sha2` | 信号 Factor/Event 名称确定性的 sha256 前4 | +| `inventory` | `#[signal]` 宏编译期注册表收集 | +| `parking_lot` | RwLock/Mutex(性能优于 std 实现) | +| `cached` | LRU 缓存(买卖意义/ MACD 行为) | +| `tracing` | 结构化日志(warn/info/error!) | + --- ## 快速开始 -### Cargo 依赖 +### 作为 Rust 库使用 ```toml [dependencies] -chanlun = { path = "path/to/chanlun" } +chanlun = "26.6" ``` -### 基本用法 — 单周期分析 +### 单周期分析 ```rust use chanlun::config::缠论配置; use chanlun::kline::bar::K线; use chanlun::business::observer::观察者; -// 1. 创建配置(所有字段带默认值) -let 配置 = 缠论配置::default(); +// 创建观察者(返回 Arc>,支持多线程共享) +let 观察员 = 观察者::new("BTCUSD".into(), 3600, 缠论配置::default()); -// 2. 创建观察者(返回 Arc>) -let 观察员 = 观察者::new("BTCUSD".into(), 3600, 配置); - -// 3. 逐根投喂K线(流式增量分析,每根K线触发全管线运算) -for k线 in k线数据流 { - 观察员.write().unwrap().增加原始K线(k线); +// 逐根投喂 K 线(流式增量分析) +for k线 in 数据流 { + 观察员.write().增加原始K线(k线); } -// 4. 读取分析结果 -let obs = 观察员.read().unwrap(); -println!("笔数量: {}", obs.笔序列.len()); -println!("线段数量: {}", obs.线段序列().len()); -println!("中枢数量: {}", obs.中枢序列().len()); - -// 遍历笔 -for 笔 in &obs.笔序列 { - println!("笔方向: {}, 高: {}, 低: {}", 笔.方向(), 笔.高(), 笔.低()); -} +// 读取分析结果 +let obs = 观察员.read(); +println!("笔: {}, 线段: {}, 中枢: {}", + obs.笔序列.len(), obs.线段序列().len(), obs.中枢序列().len()); ``` -### 便捷投喂 — 直接从 OHLCV +### 便捷投喂(无需构造 K 线对象) ```rust let 观察员 = 观察者::new("ETHUSD".into(), 300, Default::default()); -let mut obs = 观察员.write().unwrap(); -obs.投喂原始数据(1736640000, 3500.0, 3550.0, 3480.0, 3520.0, 1200.0); -// ... 继续投喂 ... +观察员.write().投喂原始数据( + 1736640000, // Unix 时间戳 + 3500.0, // 开盘 + 3550.0, // 最高 + 3480.0, // 最低 + 3520.0, // 收盘 + 1200.0, // 成交量 +); ``` -### 加载 .nb 数据文件 +### 从 .nb 文件批量加载 ```rust -let 观察员 = 观察者::new("btcusd".into(), 300, Default::default()); -观察员.write().unwrap().读取数据文件("btcusd-300-1000000-1100000.nb", Default::default())?; +观察员.write().读取数据文件("btcusd-300-1000000-1100000.nb", Default::default())?; ``` -### 多周期分析 +### 多周期联立分析 ```rust use chanlun::business::multi_frame::立体分析器; -// 周期组: [最小输入周期, 更多显示周期...] -// 只投喂最小周期K线,大周期自动合成 +// 周期组: [60s, 300s, 1800s, 7200s] +// 只投喂最小周期 K 线,大周期自动合成 let mut 分析器 = 立体分析器::new( "BTCUSD".into(), - vec![60, 300, 1800, 7200], // 1分钟 → 5分钟 → 30分钟 → 2小时 - None, // 默认配置 - None, // 无周期特定配置 + vec![60, 300, 1800, 7200], + None, // 默认配置 + None, // 无周期特定配置 ); -// 投喂最小周期K线 for k线 in 小周期K线流 { 分析器.投喂K线(k线); } -// 获取各周期观察者 -if let Some(日线观察员) = 分析器.获取观察者(86400) { - let obs = 日线观察员.read().unwrap(); - println!("日线笔数: {}", obs.笔序列.len()); +// 获取各周期分析结果 +if let Some(日线) = 分析器.获取观察者(86400) { + println!("日线笔数: {}", 日线.read().笔序列.len()); } ``` +### 信号计算 + +```rust +use chanlun::signal::engine::{SignalEngine, SignalConfig}; + +// 构建信号引擎 +let 引擎 = SignalEngine::new(vec![ + SignalConfig { + signal_name: "youwukuncheng_中枢第三买卖点_V230602".into(), + freq: 86400, + params: [ + ("freq".into(), "日线".into()), + ("max_overlap".into(), 3.into()), + ("本级完整性".into(), "实".into()), + ("同级完整性".into(), "合".into()), + ].into_iter().collect(), + }, +]); + +// 自动挂载所需指标 +引擎.自动挂载指标(&分析器); + +// 计算信号 +let 信号 = 引擎.更新_完整(&分析器); +// → { signals: { "日线_D1MO3_中枢段DEA穿越2V230602": "三买" }, market: { ... } } +``` + +### CLI 工具 + +```bash +# 单周期分析 + 保存结果 +cargo run -- read btcusd-3600-1000000-1100000.nb + +# 多周期合成 (从文件名的周期推断周期组: N, N×5, N×30) +cargo run -- synth btcusd-14400-1753142400-1781928000.nb +``` + --- ## 数据管线 -缠论分析按严格的层级顺序进行,每收到一根新K线,**增量更新**所有层级(而非批量重算)。这是该库适合实时交易系统的关键设计。 +每收到一根新 K 线,**增量更新**所有层级(只重算末尾)。这是该库适合实时交易系统的关键设计。 ### 管线流程 ``` 原始K线 (K线) │ - ├── 指标计算: MACD / RSI / KDJ / BOLL (增量更新, 含回填新指标) - │ ├── K线取值: 开/高/低/收/高低均值/高低收均值/开高低收均值 - │ ├── 多参数组: MACD_参数列表 / RSI_周期列表 / KDJ_参数列表 / BOLL_参数列表 - │ └── 均线组: SMA/EMA (均线_类型列表 × 均线_周期列表) + ├─ 1. 指标计算 (indicators/calculator) + │ ├─ MACD 组 (遍历 MACD_参数列表, 首次/增量) + │ ├─ RSI 组 (遍历 RSI_周期列表) + │ ├─ KDJ 组 (遍历 KDJ_参数列表) + │ ├─ BOLL 组 (遍历 BOLL_参数列表) + │ ├─ 均线组 (遍历 均线参数列表, SMA/EMA) + │ └─ 回填新指标 (回溯填充前序K线遗漏的指标) │ - ├── 包含处理 → 缠论K线 (缠论K线) - │ ├── 顺序包含合并 (顺方向取极值) - │ ├── 逆序包含合并 (更新时间/标的K线) - │ ├── 方向判定 (相对方向: 向上/向下/向上缺口/向下缺口/衔接/包含) - │ └── 合并替换模式: 缠K合并替换=false(原地修改) / true(产出新缠K) + ├─ 2. 包含处理 → 缠论K线 (缠论K线) + │ ├─ 顺序包含合并: 顺方向取极值 + │ ├─ 逆序包含合并: 更新时间/标的K线 + │ ├─ 方向判定: 9 种相对方向 + │ └─ 合并替换模式: 原地修改 / 产出新缠K │ - ├── 分型识别 → 分型 (分型) - │ ├── 由左中右三根缠论K线构成 - │ ├── 类型: 顶分型 / 底分型 / 三连向上 / 三连向下 / 向右扩散 - │ ├── 强度: 强 / 中 / 弱 (基于左中右收盘价关系) - │ ├── 分型模式: 全局开关控制缓存/实时读取 - │ └── MACD柱子分型匹配 (左中右MACD柱形态判定) + ├─ 3. 分型识别 → 分型 (分型) + │ ├─ 左中右三根缠K构成 + │ ├─ 类型: 顶/底/上/下/散 + │ └─ 分型模式开关控制缓存/实时读取 │ - ├── 笔划分 → 笔 (虚线, 标识="笔", 级别=1) - │ ├── 递归分析算法: 顶底分型交替验证 - │ ├── 成笔条件: 笔内元素数量≥5 (弱化模式≥3) - │ ├── 笔弱化策略: 放宽条件时尝试次高/次低成笔 - │ ├── 实际高/低点选择: first/last (笔内相同终点取舍) - │ ├── 笔内起始分型包含整笔验证 - │ └── 笔内原始K线包含整笔验证 + ├─ 4. 笔划分 → 笔 (虚线, 级别=1) + │ ├─ 递归分析: 顶底分型交替验证 + │ ├─ 弱化模式: ≥3 根原始K线即可成笔 + │ ├─ 次级成笔: 次高/次低作为候选 + │ └─ 笔内验证: 分型包含整笔 / 原始K线包含整笔 │ - ├── 线段划分 → 线段 (虚线, 标识="线段", 级别=2) - │ ├── 特征序列提取: 从笔序列中提取同向特征序列元素 (线段特征.静态分析) - │ ├── 缺口处理: - │ │ ├── 有缺口 (老阴/老阳): 严格包含处理, 等待特征分型完成 - │ │ └── 无缺口 (小阳/少阴): 允许逆序包含 - │ ├── 四种修正机制 (依次尝试): - │ │ ├── _缺口突破: 老阳/老阴时价格突破前线段极值 - │ │ ├── _非缺口下穿刺: 贯穿伤后出现方向相同的3笔 - │ │ ├── _缺口后紧急修正: 前一线段有缺口时自动修正 - │ │ └── _修正: 配置启用的短路修正 (≥9笔) - │ ├── 线段破坏判定: 新线段形成后前一线段被破坏 - │ ├── 3级递归: 线段 → 线段<线段> → 线段<线段<线段>> - │ └── 扩展线段分析: 扩展线段 → 扩展线段<扩展线段> → ... + ├─ 5. 笔中枢识别 → 笔中枢 + │ └─ 虚线重叠检测 → 延伸/扩展/第三买卖点 │ - ├── 中枢识别 → 中枢 (中枢) - │ ├── 基础检查: 三条连续虚线重叠且方向关系正确 - │ ├── 中枢延伸: 后继虚线未离开中枢则加入基础序列 - │ ├── 第三买卖点: 虚线离开中枢后不回中枢, 记录第三买卖线 - │ ├── 中枢完成: 第三买卖线确立后开始新中枢 - │ ├── 中枢扩展: ≥9段时调用扩展分析生成扩展中枢 - │ └── 多级中枢: - │ ├── 笔中枢 (级别=1, 基于笔序列) - │ ├── 线段中枢 (级别=2, 基于线段序列) - │ ├── 扩展线段中枢 (基于扩展线段序列) - │ ├── 混合扩展中枢 (基于扩展线段<线段>序列) - │ └── 线段<线段>中枢 / 扩展线段<扩展线段>中枢 等 + ├─ 6. 线段划分 → 线段 (虚线, 级别≥2), 3级递归 + │ ├─ 特征序列提取 (同向笔序列) + │ ├─ 缺口处理: 有缺口(老阴老阳) / 无缺口(小阳少阴) + │ ├─ 四种修正: 缺口突破 / 非缺口下穿刺 / 缺口后紧急修正 / 短路修正 + │ └─ 递归: 线段 → 线段<线段> → 线段<线段<线段>> │ - └── 买卖点识别 → 基础买卖点 (18种类型) - ├── 6 类经典买卖点: 一买/一卖/二买/二卖/三买/三卖 - ├── 12 类扩展买卖点: T1/T1P/T2/T2S/T3A/T3B 各含买卖 - ├── 指标匹配: MACD柱子匹配 / KDJ金叉死叉 / RSI超买超卖 - ├── 背驰确认: MACD背驰 / 斜率背驰 / 测度背驰 - ├── 中枢来源策略: 实 / 虚 / 合 - ├── 买卖意义缓存: LRU 128条目, 指针身份key - └── 偏移/失效偏移/终结K线管理 + ├─ 7. 扩展线段 + 混合扩展线段 (各3级递归) + │ + ├─ 8. 线段中枢识别 + │ └─ 多级中枢: 线段中枢 / 扩展线段中枢 / 混合扩展中枢 / ... + │ + └─ 9. 买卖点生成 → 基础买卖点 (18 种类型) + ├─ 6 经典: 一买/一卖/二买/二卖/三买/三卖 + ├─ 12 扩展: T1~T3B 各含买卖 + └─ 指标匹配: MACD/KDJ/RSI 确认 ``` ### 流式增量处理 -`观察者.__处理数据` 每收到一根新K线: -1. 计算指标并挂载到新K线 -2. 执行缠K包含处理,产出缠K -3. 识别分型,添加到分型序列 -4. 递归分析笔(可能生成新笔或修改最后一笔) -5. 递归分析中枢(笔中枢) -6. 递归分析线段(3级递归:线段→线段<线段>→线段<线段<线段>>) -7. 递归分析扩展线段(3级递归) -8. 递归分析混合扩展线段(3级递归,源序列为线段序列组[i]) +`观察者.__处理数据` 每步只处理末尾几个元素,时间复杂度 **O(1) 均摊**。 -每步都只处理最后几个元素,时间复杂度 O(1) 均摊。 +**推送模式**: 调用方可通过 `add_bar_listener()` 等回调在每级结果产出时收到通知。 --- @@ -281,511 +354,375 @@ if let Some(日线观察员) = 分析器.获取观察者(86400) { ### 枚举类型 -#### 相对方向 (`types::direction`) -用于描述两个价格区间(两根K线或两个分型)之间的位置关系,共 9 个变体: +#### 相对方向 (`types::direction`) — 9 变体 -| 变体 | 说明 | 示例场景 | -|------|------|---------| -| `向上` | 后K线高低点均高于前K线,且不构成缺口 | 正常上涨 | -| `向下` | 后K线高低点均低于前K线,且不构成缺口 | 正常下跌 | -| `向上缺口` | 后K线低点 > 前K线高点 | 大幅高开 | -| `向下缺口` | 后K线高点 < 前K线低点 | 大幅低开 | -| `衔接向上` | 后K线低点 == 前K线高点 | 精准衔接上涨 | -| `衔接向下` | 后K线高点 == 前K线低点 | 精准衔接下跌 | -| `顺` | 前K线包含后K线(前高≥后高 且 前低≤后低) | 顺序包含 | -| `逆` | 后K线包含前K线(前高≤后高 且 前低≥后低) | 逆序包含 | -| `同` | 两K线高低点完全相同 | 重复数据 | +| 变体 | 判定条件 | 场景 | +|------|---------|------| +| `向上` | 后高>前高 且 后低>前低, 无缺口 | 正常上涨 | +| `向下` | 后高<前高 且 后低<前低, 无缺口 | 正常下跌 | +| `向上缺口` | 后低 > 前高 | 大幅高开 | +| `向下缺口` | 后高 < 前低 | 大幅低开 | +| `衔接向上` | 后低 ≈ 前高 | 精准衔接 | +| `衔接向下` | 后高 ≈ 前低 | 精准衔接 | +| `顺` | 前包含后 (前高≥后高 且 前低≤后低) | 顺序包含 | +| `逆` | 后包含前 (前高≤后高 且 前低≥后低) | 逆序包含 | +| `同` | 完全相同 | 重复数据 | -提供方法:`翻转()`, `是否向上()`, `是否向下()`, `是否包含()`, `是否缺口()`, `是否衔接()`, `分析(前高,前低,后高,后低)` +方法: `翻转()`, `是否向上()`, `是否向下()`, `是否包含()`, `是否缺口()`, `分析(前高,前低,后高,后低)` -#### 分型结构 (`types::fractal`) -三根K线构成的结构形态,共 5 个变体: +#### 买卖点类型 (`types::bsp_type`) — 18 变体 -| 变体 | 左中右关系 | -|------|-----------| -| `上` | 向上类 + 向上类 | -| `下` | 向下类 + 向下类 | -| `顶` | 向上类 + 向下类(Λ 形) | -| `底` | 向下类 + 向上类(V 形) | -| `散` | 逆序包含 + 逆序包含 | +| 类别 | 买点变体 | 卖点变体 | 说明 | +|------|---------|---------|------| +| 一类 | `一买` | `一卖` | 中枢背驰后第一类 | +| 二类 | `二买` | `二卖` | 回调到中枢内第二类 | +| 三类 | `三买` | `三卖` | 离开中枢不回第三类 | +| T1 | `T1买` | `T1卖` | 事后确认型 | +| T1P | `T1P买` | `T1P卖` | 事后确认型+ | +| T2 | `T2买` | `T2卖` | 中枢回调型,破位值判定 | +| T2S | `T2S买` | `T2S卖` | 中枢回调次级 | +| T3A | `T3A买` | `T3A卖` | 第三类扩展A | +| T3B | `T3B买` | `T3B卖` | 第三类扩展B | -提供方法:`分析(左,中,右, 可以逆序包含, 忽视顺序包含)` — 通用分型识别 +方法: `是买点()`, `是卖点()` -#### 买卖点类型 (`types::bsp_type`) -18 种买卖点分类: +#### 分型结构 (`types::fractal`) — 5 变体 -| 类别 | 买点 | 卖点 | 说明 | -|------|------|------|------| -| 一类 | `一买` | `一卖` | 中枢背驰后的第一类买卖点 | -| 二类 | `二买` | `二卖` | 回调到中枢内的第二类买卖点 | -| 三类 | `三买` | `三卖` | 离开中枢后不回中枢的第三类买卖点 | -| 扩展T1 | `T1买` | `T1卖` | 事后确认型 | -| 扩展T1P | `T1P买` | `T1P卖` | 事后确认型+ | -| 扩展T2 | `T2买` | `T2卖` | 中枢回调型 | -| 扩展T2S | `T2S买` | `T2S卖` | 中枢回调次级 | -| 扩展T3A | `T3A买` | `T3A卖` | 第三类扩展A | -| 扩展T3B | `T3B买` | `T3B卖` | 第三类扩展B | - -提供方法:`是买点()`, `是卖点()` +| 变体 | 左中右关系 | 图示 | +|------|-----------|------| +| `上` | 向上 + 向上 | ↗↗ | +| `下` | 向下 + 向下 | ↘↘ | +| `顶` | 向上 + 向下 | ↗↘ (Λ) | +| `底` | 向下 + 向上 | ↘↗ (V) | +| `散` | 逆包含 + 逆包含 | 扩散 | ### 数据结构详解 -#### K线 (`kline::bar`) — 原始OHLCV + 指标 +#### K线 (`kline::bar`) ``` K线 { 标识, 序号, 周期, 时间戳, 高, 低, 开盘价, 收盘价, 成交量, - 指标: RwLock<指标容器> // MACD/RSI/KDJ/BOLL/均线 + 指标: RwLock<指标容器>, // MACD/RSI/KDJ/BOLL/均线 } ``` -- 实现 `Clone` (深拷贝, 包括指标容器) -- 支持大端字节序序列化 (`to_bytes()` → 48字节, `from_bytes()`) -- 提供 `创建普K()` 工厂方法, `相等()` 结构化校验 -- `获取MACD()` 计算两K线间的 MACD 柱面积 (阳/阴/合/总) -- `截取rc()` 按 Arc 指针截取序列片段 -#### 缠论K线 (`kline::chan_kline`) — 包含处理后的K线 +- `创建普K()` 工厂方法, `相等()` 结构化校验 +- `to_bytes()` → 48 字节大端序列化, `读取大端字节数组()` 反序列化 +- `获取MACD()` — 两K线间 MACD 柱面积 (阳/阴/合/总) +- 实现 `Clone` (深拷贝, 包括指标容器) + +#### 缠论K线 (`kline::chan_kline`) ``` 缠论K线 { - 序号: AtomicI64, - 时间戳: AtomicI64, - 高: SyncF64, // 经包含处理可能被拉高 - 低: SyncF64, // 经包含处理可能被压低 - 方向: RwLock<相对方向>, - 分型: RwLock>, - 分型特征值: SyncF64, // 历史高低点极值 - 周期, 标识, - 原始起始序号, // 包含处理前 - 原始结束序号: AtomicI64, // 包含处理后更新 - 标的K线: RwLock>, - 买卖点信息: RwLock>, + 序号: AtomicI64, 时间戳: AtomicI64, + 高: SyncF64, 低: SyncF64, // 包含处理可能拉高/压低 + 方向: RwLock<相对方向>, 分型: RwLock>, + 分型特征值: SyncF64, // 历史极值 + 周期, 标识, 原始起始序号, 原始结束序号: AtomicI64, + 标的K线: RwLock>, 买卖点信息: RwLock>, } ``` -- 核心算法:`_兼并()` — 顺序/逆序包含合并, 重复提交检测, 原地修改 -- `分析()` — 完整的普K→缠K+分型管线 -- 指标匹配:`与MACD柱子匹配()` / `与RSI匹配()` / `与KDJ匹配()` -- 时间戳对齐:跨周期对齐至基线 -- `相等()` — 20+ 字段逐项递归校验 -#### 分型 (`structure::fractal_obj`) — 顶/底分型 -``` -分型 { - 左: Option>, - 中: Arc<缠论K线>, // 核心K线 - 右: Option>, - 结构: 分型结构, // 构造时缓存 - 时间戳: i64, // 构造时缓存 - 分型特征值: f64, // 构造时缓存 -} -``` -- **分型模式** (`分型模式: AtomicBool`): 全局开关 - - `true` (默认) — `时间戳()/结构()/分型特征值()` 返回构造时缓存值 - - `false` — 从 `中` 缠K 实时读取(逆序包含后使用) -- 提供方法:`强度()` (强/中/弱), `关系组()` (左中右三组关系), `与MACD柱子分型匹配()` -- `相等()` — 递归校验左/中/右缠K + 自有缓存字段 -- 实现 `有高低` trait: 高/低 → 中.高/中.低 +- `_兼并()` — 顺序/逆序包含合并, 重复提交检测, 原地修改 +- `分析()` — 完整普K→缠K+分型管线 +- 指标匹配: `与MACD柱子匹配()` / `与RSI匹配()` / `与KDJ匹配()` +- `相等()` — 20+ 字段递归校验 #### 虚线 (`structure::dash_line`) — 笔和线段的通用数据结构 ``` 虚线 { - 标识: RwLock, // "笔"/"线段"/"扩展线段"/"线段<线段>" 等 - 序号: AtomicI64, - 级别: AtomicI64, // 笔=1, 线段=2, 线段<线段>=3, ... - 文: Arc<分型>, // 起点分型(不可变) - 武: RwLock>, // 终点分型(可变,用于动态更新) - 有效性: AtomicBool, - 基础序列: RwLock>>, // 构成虚线的子级虚线序列 - 特征序列: RwLock>>>, // 线段特征序列 - 实_中枢序列: RwLock>>, - 虚_中枢序列: RwLock>>, - 合_中枢序列: RwLock>>, - 确认K线: RwLock>>, - 模式: RwLock, // 买卖点匹配模式 - _特征序列_显示: AtomicBool, - 前一缺口: RwLock>, - 前一结束位置: RwLock>>, - 短路修正: AtomicBool, + 标识: RwLock, // "笔"/"线段"/"扩展线段"/"线段<线段>" 等 + 级别: AtomicI64, // 笔=1, 线段=2, 递归递增 + 文: Arc<分型>, // 起点(不可变) + 武: RwLock>, // 终点(可变,动态更新) + 基础序列: RwLock>>, // 子级虚线序列 + 实/虚/合_中枢序列, // 三类中枢序列 + 确认K线, 模式, 缺口处理, 短路修正, + ... } ``` -- 静态工厂:`创建笔()` (级别=1), `创建线段()` (级别=笔级别+1, 基础序列=笔序列) -- 属性:`方向()`, `高()`, `低()`, `之前是()`, `之后是()` -- 数据访问:`获取普K序列()`, `获取缠K序列()`, `获取_武()` (递归到底层笔) -- 买卖点模式匹配:`全量/任意/配置/相对` 四种指标组合策略 -- MACD相关:`计算MACD柱子均值()`, `武之MACD均值()`, `武之MACD极值()` -- 趋势分析:`计算K线序列MACD趋向背驰()` (MACD柱/DIF/DEA三项背驰) -- 分段:`计算MACD柱子分段()` (按正负号分段) -- 行为统计:`统计MACD行为()` → `MACD行为统计 {DIF上下穿0, DEA上下穿0, 金叉/死叉次数, 密集交叉区域}` -- 核心判断:`买卖意义()` — LRU 缓存 128 条目,指针身份作为 key -- 序列化:`获取数据文本()` — 笔输出简洁格式,线段输出完整格式 -- `相等()` — 递归校验文/武/基础序列/特征序列/中枢序列/确认K线/模式/缺口/结束位置等全部字段 -#### 线段特征 (`structure::segment_feat`) — 特征序列元素 -``` -线段特征 { - 序号: AtomicI64, - 标识: RwLock, - 线段方向: 相对方向, - 基础序列: Vec>, // 构成该元素的虚线序列 -} -``` -- 虚拟端点:`文()` — 取特征值最大/最小的起点分型 -- 虚拟端点:`武()` — 取特征值最大/最小的终点分型 -- tiebreaker: 同特征值时取后时间戳 -- `高()/低()` — 文/武中分型特征值的较大/较小者 -- `方向()` — 线段方向的翻转 -- 静态分析:从虚线序列生成特征序列元素列表,含分型替换逻辑 -- 分型序列:从连续特征序列元素中提取特征分型 - -#### 特征分型 (`structure::feat_fractal`) -``` -特征分型 { - 左: Arc<线段特征>, - 中: Arc<线段特征>, - 右: Arc<线段特征>, - 结构: 分型结构, // 由左中右三元素分析得出 -} -``` +- 静态工厂: `创建笔()`(级别=1), `创建线段()`(级别递增) +- 属性: `方向()`, `高()`, `低()`, `之前是()`, `之后是()` +- 数据遍历: `获取普K序列()`, `获取缠K序列()`, `获取_武()`(递归到底层笔) +- MACD 分析: `计算MACD柱子均值()`, `统计MACD行为()`, `计算K线序列MACD趋向背驰()` +- 核心判断: `买卖意义()` — LRU 缓存 128 条目 #### 中枢 (`algorithm::hub`) ``` 中枢 { - 序号: AtomicI64, - 标识: RwLock, // "笔中枢<0>"/"线段中枢<1>" 等 - 级别: AtomicI64, - 基础序列: RwLock>>, // 至少3根, 延伸后可达9+ + 基础序列: RwLock>>, // ≥3根, 延伸可达 9+ 第三买卖线: RwLock>>, 本级_第三买卖线: RwLock>>, } ``` -- 属性:`高()` (前三根虚线的最高重叠), `低()` (前三根虚线的最低重叠) + +- `高()` / `低()` — 前三根虚线的最大/最小重叠区域 - `文()` → `武()` → `方向()` -- `延伸序列()` → 首尾之外的虚线 -- `获取数据文本()` — 完整中枢结构化描述 -- `相等()` — 逐项递归校验基础序列和第三买卖线 -- 向序列中添加/更新中枢(含显示名称映射) +- 中枢延伸 → 扩展 (≥9段) → 第三买卖点确立 → 新中枢开始 #### 基础买卖点 (`business::bsp`) ``` 基础买卖点 { - 备注: String, - 类型: 买卖点类型, - 买卖点分型: Arc<分型>, - 买卖点K线: Arc<缠论K线>, // = 买卖点分型.中 - 当前K线: Arc, - 失效K线: Option>, - 终结K线: Option>, - 破位值: f64, - 结构: Option<分型结构>, - 当前缠K序号: Option, + 类型: 买卖点类型, 买卖点分型: Arc<分型>, + 买卖点K线, 当前K线, 失效K线, 终结K线, + 破位值, 结构, 偏移量, } ``` + - `偏移()` — 当前缠K序号与买卖点K线序号的差 -- `有效性()` — 失效K线是否存在 +- `有效性()` — 失效K线存在则无效 +- 18 种类型由 `买卖点工厂` 统一生成, 含背驰确认+指标匹配 ### 服务类型 -| 类型 | 职责 | 位置 | -|------|------|------| -| `观察者` | 单周期分析器, 接收K线流, 维护所有层级序列, 3级递归线段分析 | `business::observer` | -| `K线合成器` | 小周期→大周期合成, 高取max/低取min/量求和, 时间戳对齐 | `business::synthesizer` | -| `立体分析器` | 多周期联立, 内含合成器 + 每周期一个观察者, 对齐缠K序列 | `business::multi_frame` | +| 类型 | 职责 | +|------|------| +| `观察者` | 单周期流式分析器, 维护全层级序列, 3级递归线段分析 | +| `K线合成器` | 小周期→大周期合成, 高取max/低取min/量求和 | +| `立体分析器` | 多周期联立, 内含合成器+每周期一个观察者 | ### 静态算法类 -| 类型 | 职责 | 位置 | -|------|------|------| -| `笔` | 笔划分: 分型→笔 (递归分析, 笔弱化, 次级成笔, 实际高/低点, 是否背驰过) | `algorithm::bi` | -| `线段` | 线段划分: 笔→线段 (特征序列+缺口处理+四种修正+扩展分析+内部背驰判断) | `algorithm::segment` | -| `中枢` | 中枢识别: 虚线→中枢 (重叠检测+延伸/扩展+第三买卖点+向序列添加) | `algorithm::hub` | -| `背驰分析` | 背驰检测: MACD/斜率/测度 + 全量/任意/配置/相对四种组合 | `algorithm::divergence` | -| `指标计算器` | 指标增量计算: MACD/RSI/KDJ/BOLL + 回填新指标 + 均线 | `indicators::calculator` | +| 类型 | 职责 | +|------|------| +| `笔` | 分型→笔 (递归+弱化+次级+实际高低点) | +| `线段` | 笔→线段 (特征序列+缺口+四种修正+扩展分析) | +| `中枢` | 虚线→中枢 (重叠+延伸/扩展+第三买卖点) | +| `背驰分析` | MACD/斜率/测度 + 全量/任意/配置/相对组合 | +| `指标计算器` | 增量计算 MACD/RSI/KDJ/BOLL + 回填 + 均线 | --- ## 配置体系 -`缠论配置` 是一个 serde 驱动的结构体,**62 个字段**全部带默认值,支持 JSON 往返和部分反序列化容错。 +`缠论配置` 是一个 serde 驱动的结构体,**44 个字段**全部带默认值,支持 JSON 往返和部分反序列化容错。 ### 完整配置表 #### 基础设置 -| 字段 | 默认值 | 说明 | -|------|--------|------| -| `标识` | `"bar"` | 品种标识 | + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `标识` | `String` | `"bar"` | 品种标识 | #### 缠K设置 -| 字段 | 默认值 | 说明 | -|------|--------|------| -| `缠K合并替换` | `false` | false=原地修改, true=产出新缠K | + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `缠K合并替换` | `bool` | `false` | false=原地修改, true=产出新缠K | #### 笔设置 -| 字段 | 默认值 | 说明 | -|------|--------|------| -| `笔内元素数量` | `5` | 成笔最低缠K数 | -| `笔内相同终点取舍` | `false` | false=取first, true=取last | -| `笔内起始分型包含整笔` | `false` | 起点分型区间必须包含整笔 | -| `笔内起始分型包含整笔_包括右` | `false` | 同上+包含右端点 | -| `笔内原始K线包含整笔` | `false` | 原始K线区间必须包含整笔 | -| `笔次级成笔` | `false` | 允许在非分型处成笔 | -| `笔弱化` | `false` | 放宽成笔条件 | -| `笔弱化_原始数量` | `3` | 弱化模式最小原始K线数 | + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `笔内元素数量` | `i64` | `5` | 成笔最低缠K数(含端点) | +| `笔内相同终点取舍` | `bool` | `false` | false=取first, true=取last | +| `笔内起始分型包含整笔` | `bool` | `false` | 起点分型区间必须包含整笔 | +| `笔内起始分型包含整笔_包括右` | `bool` | `false` | 同上+包含右端点 | +| `笔内原始K线包含整笔` | `bool` | `false` | 原始K线区间必须包含整笔 | +| `笔次级成笔` | `bool` | `false` | 允许在非分型处成笔 | +| `笔弱化` | `bool` | `false` | 放宽成笔条件 | +| `笔弱化_原始数量` | `i64` | `3` | 弱化模式最小原始K线数 | #### 线段设置 -| 字段 | 默认值 | 说明 | -|------|--------|------| -| `线段_非缺口下穿刺` | `false` | 非缺口下的穿刺处理 | -| `线段_特征序列忽视老阴老阳` | `false` | 缺口时不严格处理包含 | -| `线段_缺口后紧急修正` | `true` | 缺口后自动修正 | -| `线段_修正` | `false` | 短路修正(不推荐) | -| `线段内部中枢图显` | `true` | 线段内部中枢图表显示 | -| `扩展线段_当下分析` | `false` | 扩展线段实时分析模式 | + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `线段_非缺口下穿刺` | `bool` | `false` | 非缺口下的穿刺处理 | +| `线段_特征序列忽视老阴老阳` | `bool` | `false` | 缺口时不严格处理包含 | +| `线段_缺口后紧急修正` | `bool` | `true` | 缺口后自动修正 | +| `线段_修正` | `bool` | `false` | 短路修正(≥9笔快速完成) | +| `线段内部中枢图显` | `bool` | `true` | 线段内部中枢图表显示 | +| `扩展线段_当下分析` | `bool` | `false` | 扩展线段实时分析模式 | #### 分析开关 -| 字段 | 默认值 | 说明 | -|------|--------|------| -| `分析笔` | `true` | | -| `分析线段` | `true` | | -| `分析扩展线段` | `true` | | -| `分析笔中枢` | `true` | | -| `分析线段中枢` | `true` | | + +| 字段 | 类型 | 默认值 | +|------|------|--------| +| `分析笔` | `bool` | `true` | +| `分析线段` | `bool` | `true` | +| `分析扩展线段` | `bool` | `true` | +| `分析笔中枢` | `bool` | `true` | +| `分析线段中枢` | `bool` | `true` | #### 终止 -| 字段 | 默认值 | 说明 | -|------|--------|------| -| `手动终止` | `""` | 手动终止时间字符串 | + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `手动终止` | `String` | `""` | 手动终止时间字符串,非空时生效 | #### 指标设置 -| 字段 | 默认值 | 说明 | -|------|--------|------| -| `计算指标` | `true` | 是否计算技术指标 | -| `计算BOLL` | `false` | 是否计算布林带 | -| `指标计算方式` | `"收"` | 开/高/低/收/高低均值/高低收均值/开高低收均值 | -#### MACD 参数 -| 字段 | 默认值 | 说明 | -|------|--------|------| -| `平滑异同移动平均线_快线周期` | `13` | | -| `平滑异同移动平均线_慢线周期` | `31` | | -| `平滑异同移动平均线_信号周期` | `11` | | -| `MACD_参数列表` | `[]` | 多参数变体: `Vec<(key, 快, 慢, 信号)>` | +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `计算指标` | `bool` | `true` | 是否计算技术指标 | +| `指标计算方式` | `String` | `"收"` | 开/高/低/收/高低均值/高低收均值/开高低收均值 | -#### RSI 参数 -| 字段 | 默认值 | 说明 | -|------|--------|------| -| `相对强弱指数_周期` | `13` | RSI计算周期 | -| `相对强弱指数_移动平均线周期` | `13` | RSI SMA平滑周期 | -| `相对强弱指数_超买阈值` | `75.0` | | -| `相对强弱指数_超卖阈值` | `25.0` | | -| `RSI_周期列表` | `[]` | 多周期变体: `Vec<(key, 周期)>` | +#### 指标参数列表(多参数变体) -#### KDJ 参数 -| 字段 | 默认值 | 说明 | -|------|--------|------| -| `随机指标_RSV周期` | `13` | | -| `随机指标_K值平滑周期` | `5` | | -| `随机指标_D值平滑周期` | `5` | | -| `随机指标_超买阈值` | `80.0` | | -| `随机指标_超卖阈值` | `20.0` | | -| `KDJ_参数列表` | `[]` | 多参数变体 | - -#### BOLL 参数 -| 字段 | 默认值 | 说明 | -|------|--------|------| -| `布林带_周期` | `20` | | -| `布林带_标准差倍数` | `2.0` | | -| `BOLL_参数列表` | `[]` | 多参数变体 | - -#### 均线参数 -| 字段 | 默认值 | 说明 | -|------|--------|------| -| `均线_类型列表` | `[]` | `["SMA", "EMA", ...]` | -| `均线_周期列表` | `[]` | `[5, 10, 20, ...]` | +| 字段 | 类型 | 默认值 | +|------|------|--------| +| `MACD_参数列表` | `Vec<(String, String, i64, i64, i64)>` | `[("macd","收",13,31,11)]` | +| `RSI_周期列表` | `Vec<(String, String, i64, i64, f64, f64)>` | `[("rsi","收",14,13,75,25)]` | +| `KDJ_参数列表` | `Vec<(String, String, i64, i64, i64, f64, f64)>` | `[("kdj","收",13,5,5,80,20)]` | +| `BOLL_参数列表` | `Vec<(String, String, i64, f64)>` | `[("boll","收",20,2.0)]` | +| `均线参数列表` | `Vec<(String, String, String, i64)>` | `[]` | #### 推送/图表显示 -| 字段 | 默认值 | -|------|--------| -| `图表展示` | `true` | -| `推送K线` | `true` | -| `推送笔` | `true` | -| `推送线段` | `true` | -| `推送中枢` | `true` | -| `图表展示_笔` | `true` | -| `图表展示_线段` | `true` | -| `图表展示_扩展线段` | `true` | -| `图表展示_扩展线段_线段` | `true` | -| `图表展示_线段_线段` | `true` | -| `图表展示_中枢_笔` | `true` | -| `图表展示_中枢_线段` | `true` | -| `图表展示_中枢_扩展线段` | `true` | -| `图表展示_中枢_扩展线段_线段` | `true` | -| `图表展示_中枢_线段_线段` | `true` | -| `图表展示_中枢_线段内部` | `true` | + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `图表展示` | `bool` | `true` | 全局图表显示开关 | +| `图表展示标签` | `Option>` | `None` | None=全部展示, `[]`=不展示 | #### 买卖点设置 -| 字段 | 默认值 | 说明 | -|------|--------|------| -| `买卖点偏移` | `1` | 最大偏移量 | -| `买卖点激进识别` | `false` | | -| `买卖点与MACD柱强相关` | `false` | | -| `买卖点错过误差值` | `0.01` | | -| `买卖点_指标模式` | `"配置"` | 任意/配置/全量/相对 | -| `买卖点_指标匹配_MACD` | `true` | | -| `买卖点_指标匹配_KDJ` | `true` | | -| `买卖点_指标匹配_RSI` | `true` | | -| `买卖点_背离率` | `INFINITY` | 不使用 | -| `买卖点_T2_回调阈值` | `1.0` | | -| `买卖点_T2S_最大层级` | `3` | | -| `买卖点_峰值条件` | `false` | | -| `买卖点_计算方式` | `"峰"` | | -| `买卖点_计算线段BSP1` | `true` | | -| `买卖点_处理BSP2` | `true` | | -| `买卖点_计算线段BSP3` | `true` | | -| `买卖点_依赖T1` | `true` | | -| `买卖点_中枢来源` | `"合"` | 实/虚/合 | -| `买卖点_调试输出` | `false` | | + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `买卖点偏移` | `i64` | `1` | 买卖点与当前K线的最大允许偏移 | +| `买卖点激进识别` | `bool` | `false` | 是否激进识别买卖点 | +| `买卖点与MACD柱强相关` | `bool` | `false` | 买卖点须MACD柱确认 | +| `买卖点错过误差值` | `f64` | `0.01` | 价格误差容忍度 | +| `买卖点_指标模式` | `String` | `"配置"` | 任意/配置/全量/相对 | +| `买卖点_指标匹配_MACD` | `bool` | `true` | MACD柱子匹配确认 | +| `买卖点_指标匹配_KDJ` | `bool` | `true` | KDJ匹配确认 | +| `买卖点_指标匹配_RSI` | `bool` | `true` | RSI匹配确认 | #### 背驰设置 -| 字段 | 默认值 | 说明 | -|------|--------|------| -| `线段内部背驰_MACD` | `true` | 使用 MACD 背驰 | -| `线段内部背驰_斜率` | `true` | 使用斜率背驰 | -| `线段内部背驰_测度` | `true` | 使用测度背驰 | -| `线段内部背驰_模式` | `"相对"` | 任意/配置/全量/相对 | + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `线段内部背驰_MACD` | `bool` | `true` | 使用 MACD 背驰 | +| `线段内部背驰_斜率` | `bool` | `true` | 使用斜率背驰 | +| `线段内部背驰_测度` | `bool` | `true` | 使用测度背驰 | +| `线段内部背驰_模式` | `String` | `"相对"` | 任意/配置/全量/相对 | #### 文件 -| 字段 | 默认值 | 说明 | -|------|--------|------| -| `加载文件路径` | `""` | 数据文件路径 | + +| 字段 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `加载文件路径` | `String` | `""` | 数据文件路径 | ### 配置方法 ```rust // JSON 往返 -let json = config.to_json(); // 序列化 +let json = config.to_json(); // 序列化 → String let config2 = 缠论配置::from_json(&json)?; // 反序列化 -// 文件 I/O -config.保存配置("/path/to/config.json")?; // 保存 -let loaded = 缠论配置::加载配置("/path/to/config.json")?; // 加载 +// to_dict / from_dict(仅 model_fields 中的字段) +let dict = config.to_dict(); // → serde_json::Value (Object) +let restored = 缠论配置::from_dict(&dict)?; // 过滤非模型字段,缺失回退默认值 -// 部分反序列化(缺失字段→默认值) -let config: 缠论配置 = serde_json::from_str(r#"{"笔内元素数量": 7}"#)?; +// 文件 I/O +config.保存配置("config.json")?; +let loaded = 缠论配置::加载配置("config.json")?; // 静默模式 -let silent = config.不推送(); // 所有推送/展示开关设为 false +let silent = config.不推送(); // 图表展示 + 线段内部中枢图显 = false -// 差异对比 -let diffs = config_a.对比(&config_b); // → ["笔内元素数量", ...] +// 差异对比(仅比较 model_fields) +let diffs = config_a.对比(&config_b); // → HashMap<&str, Value> 仅差异字段 -// 多参数解析(列表非空则使用列表,否则使用默认单组) -let macd参数 = config._解析MACD参数列表(); // Vec<(key, 快, 慢, 信号)> -let rsi周期 = config._解析RSI周期列表(); // Vec<(key, 周期)> -let kdj参数 = config._解析KDJ参数列表(); // Vec<(key, RSV, K, D)> -let boll参数 = config._解析BOLL参数列表(); // Vec<(key, 周期, 倍数)> +// 部分更新 +let updated = config.model_copy(&[("标识", "new_id")].into()); // 深拷贝 + 更新 -// 复合key重组(兼容旧版 "1_open" 格式) +// 旧版兼容:按序号前缀重组配置字典 ("1_open" → group 1) let configs = 缠论配置::按序号重组字典(&default_config, &raw_json); +// → Vec<(i64, 缠论配置)> ``` +### 非法值容错 + +三个字符串枚举字段带自定义反序列化器,非法值自动回退默认值并发出 `warn!` 日志: + +| 字段 | 合法值 | 回退默认值 | +|------|--------|-----------| +| `指标计算方式` | 开/高/低/收/高低均值/高低收均值/开高低收均值 | `"收"` | +| `买卖点_指标模式` | 任意/配置/全量/相对 | `"配置"` | +| `线段内部背驰_模式` | 任意/配置/全量/相对 | `"相对"` | + --- ## 算法模块 -### 笔划分 (`algorithm::bi`, 1009 行) - -`笔` 是一个纯静态方法命名空间,核心方法: +### 笔划分 (`algorithm::bi`, ~1,009 行) | 方法 | 说明 | |------|------| -| `分析()` | 主算法:接收分型,递归分析生成笔 | -| `_获取缠K数量()` | 计算可成笔的缠K数量(含弱化模式) | -| `_实际高点() / _实际低点()` | 潜在成笔区域的最高/最低缠K | -| `_获取成笔核心分型()` | 笔弱化模式下获取次高/次低成笔的候选分型 | +| `分析()` | 主算法:从分型序列递归分析生成笔 | +| `_获取缠K数量()` | 计算潜在成笔区域的缠K数(含弱化模式放宽) | +| `_实际高点()` / `_实际低点()` | 潜在成笔区域的最高/最低缠K | +| `_获取成笔核心分型()` | 弱化模式下获取次高/次低成笔的候选分型 | | `根据缠K找笔()` | 在笔序列中查找包含指定缠K的笔 | | `是否背驰过()` | 检查笔范围内是否发生过MACD背驰 | -关键设计: -- 笔弱化:当严格条件不满足时,允许 `笔弱化_原始数量` (≥3) 根原始K线成笔 -- 次级成笔:允许次高/次低成笔的候选分型 -- 相同终点取舍:false=取first, true=取last (Python `list.index` 行为) -- 笔内验证:起始分型包含整笔 / 原始K线包含整笔 +**关键设计**: +- 笔弱化: ≥`笔弱化_原始数量`(默认3)根原始K线即可成笔 +- 次级成笔: 次高/次低作为候选终点分型 +- 相同终点取舍: false=取first, true=取last (对应 Python `list.index` 行为) +- 笔内验证: 起始分型包含整笔 / 原始K线包含整笔 -### 线段划分 (`algorithm::segment`, 2078 行) +### 线段划分 (`algorithm::segment`, ~2,078 行) -`线段` 是**最复杂的算法模块**,核心方法: +**最复杂的算法模块**。核心方法: | 方法 | 说明 | |------|------| | `分析()` | 主算法:从笔序列计算线段 | | `扩展分析()` | 扩展线段分析(不同的缺口处理策略) | -| `分割序列()` | 将特征序列分割为前/后/三/贯穿伤四个部分 | -| `特征序列状态()` | 返回 (特征_a, 特征_b, 特征_c) 布尔标志 | -| `四象()` | 判断特征序列的类型:老阳/老阴/小阳/少阴 | +| `分割序列()` | 将特征序列分割为前/后/三/贯穿伤四个子序列 | +| `四象()` | 判断特征序列类型: 老阳/老阴/小阳/少阴 | | `获取缺口()` | 获取特征序列第一二元素间的缺口 | -| `判断线段内部是否背驰()` | 线段内部进入段vs离开段背驰判断 | +| `判断线段内部是否背驰()` | 进入段 vs 离开段背驰判断 | | `是否背驰过()` | 线段范围内是否发生过背驰 | -| `获取所有停顿位置()` | 获取线段内部所有可能的停顿位置 | -| `获取内部中枢序列()` | 获取线段内部的中枢序列 | -四种修正机制(按优先级): -1. `_缺口突破` — 老阳/老阴状态 + 价格突破前线段极值 → 线段完成 -2. `_非缺口下穿刺` — 贯穿伤存在 + 方向相同的3笔 → 线段完成 -3. `_缺口后紧急修正` — 前一线段有缺口 + 价格反向突破 → 修正前一结束位置 -4. `_修正` — 短线路径 (≥9笔) → 快速完成 +**四种修正机制**(按优先级): +1. **`_缺口突破`** — 老阳/老阴状态 + 价格突破前线段极值 → 线段完成 +2. **`_非缺口下穿刺`** — 贯穿伤存在 + 方向相同的3笔 → 线段完成 +3. **`_缺口后紧急修正`** — 前一线段有缺口 + 价格反向突破 → 修正结束位置 +4. **`_修正`** — 短线路径 (≥9笔) → 快速完成 -### 中枢识别 (`algorithm::hub`, 1041 行) +**3 级递归**: `线段` → `线段<线段>` → `线段<线段<线段>>`,每一级将前级结果作为基础序列。 -`中枢` 既是数据结构也是算法命名空间: +### 中枢识别 (`algorithm::hub`, ~1,041 行) | 方法 | 说明 | |------|------| | `分析()` | 主算法:虚线→中枢,同时识别第三买卖点 | | `线段扩展分析()` | 中枢扩展:≥9段时生成扩展中枢 | -| `高()` | 前三根虚线重叠区域的最高值 | -| `低()` | 前三根虚线重叠区域的最低值 | -| `文()` | 第一根虚线的起点分型 | -| `武()` | 第三根虚线的终点分型 | -| `方向()` | 当中枢高 > 中枢低时为向上,否则向下 | -| `延伸序列()` | 首尾之外的虚线 | -| `获取离开虚线()` | 找到离开中枢的虚线(第三买卖点) | -| `获取剩余虚线()` | 第三买卖点之后的虚线 | +| `高()` / `低()` | 前三根虚线重叠区域 | +| `获取离开虚线()` | 找到离开中枢的虚线(确立第三买卖点) | | `获取第N买卖线()` | 获取第N个买卖点相关的虚线 | -中枢识别核心逻辑: -1. 基础检查:三条连续虚线必须重叠(`max(低) < min(高)`)且方向关系正确 -2. 中枢延伸:在后继虚线中找离开中枢的虚线,未离开则加入基础序列 -3. 第三买卖点:虚线离开中枢且不回中枢,记录 `第三买卖线` -4. 中枢完成:`第三买卖线` 确立后,在上方虚线开始新中枢 -5. 中枢扩展:≥9段时调用 `线段扩展分析` 生成扩展中枢 +**识别流程**: +1. 基础检查: 三条连续虚线重叠且方向关系正确 +2. 中枢延伸: 后继虚线未离开则加入基础序列 +3. 第三买卖点: 虚线离开中枢且不回 → 记录 `第三买卖线` +4. 中枢完成: `第三买卖线` 确立后在上方开始新中枢 +5. 多级中枢: 笔中枢(级别=1) / 线段中枢(级别=2) / 扩展线段中枢 等 -### 背驰检测 (`algorithm::divergence`, 227 行) +### 背驰检测 (`algorithm::divergence`, ~227 行) -`背驰分析` 提供三种检测方式: +三种检测方式: -| 方式 | 原理 | 方法 | -|------|------|------| -| MACD背驰 | 进入段vs离开段的MACD柱面积对比 (`阳 + |阴|` 绝对值和) | `MACD背驰()` | -| 斜率背驰 | 价格变化速率 (dy/dx) 减弱 | `斜率背驰()` | -| 测度背驰 | 价格-时间向量长度 (√(dx²+dy²)) 缩减 | `测度背驰()` | - -四种组合模式: - -| 模式 | 条件 | +| 方式 | 原理 | |------|------| -| `全量` | MACD 且 斜率 且 测度 全满足 | -| `任意` | MACD 或 斜率 或 测度 任意满足 | -| `配置` | 根据 `线段内部背驰_MACD/斜率/测度` 配置组合 | -| `相对` | 至少两种方式满足(多数投票) | +| MACD 背驰 | 进入段 vs 离开段的 MACD 柱面积对比 (阳 + |阴| 绝对值和) | +| 斜率背驰 | 价格变化速率 (dy/dx) 减弱 | +| 测度背驰 | 价格-时间向量长度 (√(dx²+dy²)) 缩减 | + +四种组合模式: `全量`(全满足) / `任意`(任意满足) / `配置`(按开关) / `相对`(多数投票) --- ## 技术指标 -所有指标位于 `indicators/` 模块,均支持**首次计算**和**增量计算**两种模式。 +### 统一容器 (`indicators::container`) -### 统一指标容器 (`indicators::container`) - -基于注册表模式,通过 `指标值` 枚举统一管理五种指标类型: +注册表模式,通过 `指标值` 枚举统一管理: ```rust pub enum 指标值 { @@ -798,133 +735,364 @@ pub enum 指标值 { } ``` -- 预注册默认槽位:`macd`, `rsi`, `kdj`, `boll`, `均线`, `单值` -- 支持多参数变体:`MACD_13_31_11`, `RSI_14`, `KDJ_9_3_3` 等 -- 便捷方法:`macd()`, `rsi()`, `kdj()`, `boll()`, `均线()` -- 动态注册:`注册(name, default_value)`, `设置(name, value)` +- 预注册槽位: `macd`, `rsi`, `kdj`, `boll`, `均线`, `单值` +- 支持多参数变体: `MACD_13_31_11`, `RSI_14`, `KDJ_9_3_3` 等 +- 动态注册: `注册(name, default)` / `设置(name, value)` / `获取(name)` +- K线取值 7 种方式: 开/高/低/收/高低均值/高低收均值/开高低收均值 -### 指标计算器 (`indicators::calculator`, 476 行) +### 指标计算器 (`indicators::calculator`, ~476 行) -`指标计算器::计算并挂载(全序列, 配置)` 是一次完整的指标计算周期: +`计算并挂载(全序列, 配置)` 单次周期: -1. **MACD组** — 遍历 `_解析MACD参数列表()`,对每个 (key, 快, 慢, 信号) 计算 -2. **RSI组** — 遍历 `_解析RSI周期列表()` -3. **KDJ组** — 遍历 `_解析KDJ参数列表()` -4. **BOLL组** — 遍历 `_解析BOLL参数列表()` -5. **均线** — 遍历 `均线_类型列表 × 均线_周期列表`,MA/EMA 计算 -6. **回填新指标** — 如果当前K线有前序K线遗漏的指标,回溯填充 +1. MACD组 → RSI组 → KDJ组 → BOLL组 → 均线组 +2. 首次计算 vs 增量计算自动判定(基于前序K线是否有值) +3. 回填新指标:回溯填充前序K线遗漏的指标 -K线取值 (`K线取值()`) 支持 7 种计算方式: -`开` / `高` / `低` / `收` / `高低均值` / `高低收均值` / `开高低收均值` +### 各指标结构 -### MACD (`indicators::macd`, 215 行) +**MACD** — EMA(12/26/9) 默认 +``` +DIF: Option, DEA: Option, MACD柱: f64 +``` + +**RSI** — Wilder SMA 平滑 +``` +RSI: Option, RSI_SMA: Option, 超买/超卖阈值 +``` + +**KDJ** — RSV → K → D → J +``` +K: Option, D: Option, J: Option +``` + +**BOLL** — SMA ± k·σ +``` +中轨: Option, 上轨: Option, 下轨: Option +``` + +--- + +## 信号框架 + +信号框架是缠论分析的上层应用——在已有笔/线段/中枢的基础上,通过**声明式规则匹配**产生交易信号,驱动**仓位状态机**进行多空操作。 + +### 信号原语 + +信号框架的 5 种原语(源自 czsc,Apache-2.0,已做中文命名适配与 Rust 重写): ``` -平滑异同移动平均线 { - 时间戳, 收盘价, 快线周期, 慢线周期, 信号周期, - DIF: Option, // 快线EMA - 慢线EMA - DEA: Option, // DIF的信号线EMA - MACD柱: f64, // 2 * (DIF - DEA) - 快线EMA: Option, // 计算中间值(增量用) - 慢线EMA: Option, - DEA_EMA: Option, +Signal → Factor → Event → Position (含 Operate) +``` + +#### Signal — 七段键信号 + +```rust +pub struct Signal { + pub k1: String, // 级别 (freq) e.g. "日线" + pub k2: String, // 信号分组 e.g. "D1MO3" + pub k3: String, // 信号名 e.g. "中枢第三买卖点V230602" + pub v1: String, // 品种 e.g. "三买" + pub v2: String, // 标记 e.g. "中枢段DEA穿越2" + pub v3: String, // 描述 e.g. "偏移3" + pub score: i32, // 0~100 } ``` -- `首次计算(收盘价, 时间戳, 快线周期, 慢线周期, 信号周期)` — 初始EMA=SMA近似 -- `增量计算(prev, 收盘价, 时间戳)` — EMA递推公式 -- `计算K线MACD(普K序列, 快, 慢, 信号)` — 遍历序列批量计算 +- `key()` → `"k1_k2_k3"` (三段键,用于去重/查询) +- `value()` → `"v1_v2_v3"` (三段值) +- `is_match(signals: &信号字典)` → 三态: 命中→`Ok(true)`, 不匹配→`Ok(false)`, 缺键→`Err(缺键错误)` +- `空信号` → value=`"任意_任意_任意_0"`, score=0 -### RSI (`indicators::rsi`, 240 行) +#### Factor — 因子 = 多信号 all/not 组合 -``` -相对强弱指数 { - 时间戳, 收盘价, 周期, - 超买阈值, 超卖阈值, - RSI: Option, - RSI_SMA: Option, // RSI的SMA信号线 - 平均上涨: Option, // Wilder平滑中间值(增量用) - 平均下跌: Option, +```rust +pub struct Factor { + pub name: String, // SHA256前4 + 自定义后缀,确定性 + pub signals_all: Vec, // 必须全部命中 + pub signals_not: Vec, // 必须全部不命中 } ``` -- Wilder SMA 平滑算法 -- `首次计算()` — 初始均值=SMA -- `增量计算()` — Wilder递推 +`is_match(signals)` → 先检查全部 `signals_all` 命中, 再检查全部 `signals_not` 不命中。 -### KDJ (`indicators::kdj`, 289 行) +`name` 使用 sha256 前 4 位十六进制,确保 Rust 与 Python 计算**确定性**相同(注意:不保证跨语言字节兼容,但同语言内一致)。 -``` -随机指标 { - 时间戳, 最高价, 最低价, 收盘价, - RSV周期, K值平滑周期, D值平滑周期, - 超买阈值, 超卖阈值, - K: Option, // RSV的移动平均 - D: Option, // K的移动平均 - J: Option, // 3*K - 2*D +#### Event — 事件 = 因子组 + 操作 + +```rust +pub struct Event { + pub name: String, + pub operate: Operate, // 匹配时执行的操作 + pub factors: Vec, } ``` -- `首次计算(全体)` — 从序列计算初始KDJ值 -- `增量计算(全体, 当前, 周期, K平滑, D平滑)` — 增量更新 +`is_match(signals)` → 任一因子命中则为 true(OR 逻辑)。 -### BOLL (`indicators::boll`, 169 行) +#### Operate — 操作枚举 -``` -布林带 { - 时间戳, 收盘价, 周期, - 标准差倍数, - 中轨: Option, // SMA - 上轨: Option, // 中轨 + 倍数 * 标准差 - 下轨: Option, // 中轨 - 倍数 * 标准差 +```rust +pub enum Operate { + 开多, // LO — Long Open + 平多, // LE — Long Exit + 开空, // SO — Short Open + 平空, // SE — Short Exit + 持币, // HO — Hold (无操作) } ``` +实现 `Ord` (LO < SO < LE < SE < HO), `Display` 中文输出。 + +#### Position — 仓位状态机 + +Position 是信号框架的最终消费者——将 Event 匹配结果转化为实际的仓位操作: + +```rust +pub struct Position { + // 配置字段 + pub symbol: String, + pub opens: Vec, // 开仓事件 + pub exits: Vec, // 平仓事件 + pub events: Vec, // 通用事件(先于 opens/exits 匹配) + pub interval: i64, // 开仓间隔(秒),0=不限制 + pub timeout: i64, // 持仓超时(K线数) + pub stop_loss: i64, // 止损阈值(BP,1/10000) + pub T0: bool, // T+0 模式 + + // 状态字段 + pub pos: i32, // 1=多, -1=空, 0=空仓 + pub pos_changed: bool, // 本轮是否发生仓位变化 + pub operates: Vec<操作记录>, // 操作历史 + pub holds: Vec<持仓记录>, // 持仓快照序列 +} +``` + +**`update(dt, price, bid, signals)` 状态机**(~200 行 Rust,与 Python 1:1 对应): + +1. 时间校验: `dt <= end_dt` → 跳过(信号时间倒退) +2. 事件匹配: 遍历 `events` → `is_match(signals)` → 首个命中即break +3. **开多** (LO): pos≠1 且间隔检查通过 → pos=1; 若 pos=-1 且允许平仓 → pos=0 先平空 +4. **平多** (LE): pos=1 且允许操作 → pos=0 +5. **开空** (SO): 对称 +6. **平空** (SE): 对称 +7. **止损检查**: 多头→`price/last_price-1 < -stop_loss/10000`; 空头→`1-price/last_price < -stop_loss/10000` +8. **超时检查**: `bid - last_bid > timeout` +9. 追加持仓快照 `{dt, pos, price}` + +**`pairs()` 盈亏计算** — 从 `operates` 中配对开平操作,计算每对的 BP(基点)盈亏: + +``` +多头盈亏 = (平仓价/开仓价 - 1) * 10000 +空头盈亏 = (1 - 平仓价/开仓价) * 10000 +``` + +**辅助判断**: +- `同一交易日(a, b)` — chrono 日期比较 +- `间隔检查(last_dt, dt, interval)` — 距上次开仓是否超过 interval 秒 +- `允许操作(T0, dt, last_dt)` — T+0 或不同交易日 + +### 信号注册表 (`signal::registry`) + +双注册表架构,支持**编译时注册**和**运行时动态注册**: + +``` +┌──────────────────────────────────────┐ +│ 获取信号 │ +│ get_signal(name) │ +│ ├─ 1. 检查 SIGNAL_REGISTRY │ ← LazyLock (编译时, #[signal] 宏) +│ └─ 2. 检查 DYNAMIC_REGISTRY │ ← RwLock (运行时, 动态注册) +└──────────────────────────────────────┘ +``` + +**编译时注册** — `#[signal]` 属性宏: + +```rust +use chanlun_signal_macros::signal; + +#[signal( + name = "my_custom_signal_V230101", + template = "{freq}_D1MO{max_overlap}_my_custom_signalV230101" +)] +fn my_custom_signal(obs: &观察者, params: &HashMap) -> Vec { + // 信号逻辑... + vec![Signal::new("日线", "D1", "my_signalV230101", "三买", "", "", 80)] +} +``` + +宏在编译期生成 `SignalDescriptor { name, template, func }` 并通过 `inventory::submit!` 注册。 + +**动态注册** — 运行时 API: + +```rust +use chanlun::signal::registry::{register_signal, unregister_signal, list_signal_names}; + +// 注册 +register_signal( + "my_signal_V230101", + "{freq}_templateV230101", + my_signal_fn, // SignalFn +)?; + +// 注销 +unregister_signal("my_signal_V230101")?; + +// 列出所有已注册信号(编译时 + 动态) +let names: Vec = list_signal_names(); +``` + +**约束**: 动态注册不能覆盖编译时信号(返回 Err);同名动态信号重复注册也报错。 + +### 信号引擎 (`signal::engine`) + +`SignalEngine` 批量执行已注册的信号函数: + +```rust +pub struct SignalEngine { + configs: Vec, // [{signal_name, freq, params}] +} +``` + +- `自动挂载指标(分析器)` — 扫描所有 config 中的 MACD/均线关键词,为各周期 observer 自动添加所需指标参数 +- `更新(分析器)` → `HashMap` — 遍历配置,按 freq 聚合,调用注册表函数,过滤空信号 +- `更新_完整(分析器)` → `完整更新结果 { signals, market }` — 附加基础周期的 OHLCV 行情数据 + +### 已实现的信号函数 + +| 信号名 | 来源 | 功能 | +|--------|------|------| +| `youwukuncheng_中枢第三买卖点_V230602` | `functions/youwukuncheng.rs` | 中枢第三买卖点 (3种变体: DEA穿越/首次穿越0轴+分型确认) | +| `bar_zdt_V230331` | `functions/demo.rs` | 涨跌停检测 | +| `macd_金叉_V260601` | `functions/demo.rs` | MACD 金叉/死叉 | +| `tas_macd_direct_V221106` | `functions/demo.rs` | MACD DIF 方向 | +| `tas_ma_base_V230313` | `functions/demo.rs` | MA 均线多空 (SMA/EMA) | +| `cxt_停顿分型_V230106` | `functions/demo.rs` | 停顿分型检测 | +| `cxt_bi_end_V230222` | `functions/demo.rs` | 笔结束辅助 (~90行) | + +--- + +## 插件系统 + +支持通过 `.so` 动态库在运行时加载第三方 Rust 信号函数。 + +### C-ABI 导出 (`signal::ffi`) + +宿主进程导出三个 C-ABI 函数供 `.so` 插件调用: + +```rust +// 注册信号 → 0=成功, 非0=失败 +#[unsafe(no_mangle)] +pub unsafe extern "C" fn chanlun_register_signal( + name: *const c_char, template: *const c_char, func: SignalFn, +) -> i32; + +// 注销信号 +#[unsafe(no_mangle)] +pub unsafe extern "C" fn chanlun_unregister_signal(name: *const c_char) -> i32; + +// 查询信号总数 +#[unsafe(no_mangle)] +pub unsafe extern "C" fn chanlun_list_signal_count() -> i32; +``` + +### 插件编写 + +**方式 A: 手动注册** — 在插件的构造函数中直接调用 `chanlun_register_signal`: + +```rust +#[unsafe(no_mangle)] +pub unsafe extern "C" fn init_plugin() -> i32 { + chanlun_register_signal( + "my_signal\0".as_ptr() as *const c_char, + "template\0".as_ptr() as *const c_char, + my_signal_fn, + ) +} +``` + +**方式 B: `#[signal]` 宏** — 使用 `crate_path = "::chanlun"` 参数在外部 crate 中使用宏注册: + +```rust +use chanlun_signal_macros::signal; + +#[signal( + name = "plugin_signal_V230101", + template = "{freq}_pluginV230101", + crate_path = "::chanlun" // 关键:外部 crate 需指定路径 +)] +fn plugin_signal(obs: &观察者, params: &HashMap) -> Vec { + // ... +} +``` + +然后在 `init_plugin()` 中遍历 `inventory::iter::` 批量提交。 + +### Python 端加载 + +```python +import ctypes, os + +# 设置全局符号可见性 +sys.setdlopenflags(os.RTLD_LAZY | os.RTLD_GLOBAL) + +# 加载插件 +plugin = ctypes.CDLL("./libmy_plugin.so") +plugin.init_plugin() # 内部调用 chanlun_register_signal + +# 此后可通过 call_signal 或 SignalEngine 使用插件信号 +from chanlun._chanlun import call_signal, list_signals +print(list_signals()) # 包含插件注册的信号 +``` + +**约束**: 插件和宿主必须用**相同 Rust 编译器版本**编译,因为 `SignalFn` 使用 Rust 调用约定。 + --- ## 线程安全与并发 ### 内部可变性策略 -缠K/虚线/中枢等结构体需要在多 `Arc` 共享时支持原地修改(如笔动态替换武、中枢延伸添加虚线)。Rust 中 `Arc` 的 `&self` 是不可变的,因此大量字段使用以下策略实现内部可变性: - -| 机制 | 类型 | 适用场景 | +| 机制 | 底层 | 适用场景 | |------|------|---------| -| `SyncF64` | `AtomicU64` 位转换 | 价格字段 (高/低/分型特征值),需跨线程原子读写 | -| `AtomicI64` | 原子整数 | 序号/时间戳/原始结束序号 | -| `AtomicBool` | 原子布尔 | 有效性/短路修正/特征序列显示 | -| `RwLock` | 读写锁 | 复杂字段 (Arc<分型>/Vec/标识/模式/中枢序列) | +| `SyncF64` | `AtomicU64` 位转换 | 价格字段 (高/低/分型特征值) | +| `AtomicI64` | `AtomicI64` | 序号/时间戳 | +| `AtomicBool` | `AtomicBool` | 有效性/短路修正/开关 | +| `RwLock` | `parking_lot::RwLock` | 复杂数据 (Arc<分型>/Vec/中枢序列) | -### 全局缓存 +### Send + Sync -- **买卖意义缓存**: `LazyLock>>` — 128条目,key为 (虚线指针, 观察者指针) 地址对 -- **与 Python 对齐**:Python 使用 `@lru_cache(maxsize=128)` 装饰器,Rust 使用全局 `LazyLock>` 替代 PyO3 的 `thread_local!`(解决跨线程不可见问题) - -### Send + Sync 保证 - -所有核心数据结构(`缠论K线`, `虚线`, `中枢`, `观察者`)均实现 `Send + Sync`,可以安全地跨线程传递和共享: +所有核心数据结构(`缠论K线`, `虚线`, `中枢`, `观察者`)均实现 `Send + Sync`,可安全跨线程: ```rust // 编译期断言 -fn _需要_Send_Sync() {} -_需要_Send_Sync::<缠论K线>(); -_需要_Send_Sync::<虚线>(); -_需要_Send_Sync::<中枢>(); +fn _要求_Send_Sync() {} +_要求_Send_Sync::<缠论K线>(); +_要求_Send_Sync::<虚线>(); +_要求_Send_Sync::<中枢>(); +_要求_Send_Sync::<观察者>(); ``` -`观察者` 使用 `Arc>` 模式,多线程可以并发读取同一个观察者的分析结果。 +`观察者` 使用 `Arc>` 模式——多线程并发读取同一观察者。 + +### 全局缓存 + +- **买卖意义缓存**: `LazyLock>>` — 128 条目 +- Python 端使用 `@lru_cache` 装饰器,Rust 使用 `LazyLock`(替代 `thread_local!` 解决跨线程不可见问题) ### RwLock 死锁预防 -Rust 的 `RwLock` 是**非递归锁**。在已有读锁的线程中尝试获取写锁会死锁。核心规则: -- 将读锁作用域化:在所有调用带写锁的方法之前释放 `prev_guard` -- 典型模式:`{ let prev_guard = ... ; /* 使用 prev_guard */ } // 作用域结束释放 → 安全调用回填` +Rust `RwLock` 是**非递归锁**——在持读锁时尝试获取写锁会死锁。解决:将读锁作用域化,在所有写锁调用前释放: + +```rust +{ let prev_guard = 前序K线.指标.read(); /* 读取 */ } +// ↑ 作用域结束 → 释放读锁 +Self::_回填新指标(...); // 安全:此时无读锁 +``` --- ## Python 绑定 -Python 调用通过独立的 [`chanlun-py`](../chanlun-py/) crate 提供,使用 PyO3 包装。所有类型、方法、字段名与 `chan.py` **完全一致**,API 兼容。 +通过 [`chanlun-py`](../chanlun-py/) crate(PyO3)将 Rust 核心导出为 Python `chanlun` 模块。 ```bash cd chanlun-py @@ -934,65 +1102,77 @@ maturin develop --release ```python import chanlun -# 创建/修改配置 +# 配置 config = chanlun.缠论配置() config.笔内元素数量 = 7 -config.MACD_参数列表 = [("my_macd", 12, 26, 9)] +config.MACD_参数列表 = [("my_macd", "收", 12, 26, 9)] -# 创建观察者 +# 观察者 obs = chanlun.观察者("BTCUSD", 3600, config) - -# 通过 Python 属性直接修改配置(自动同步到核心层) -obs.配置.笔弱化 = True - -# 投喂K线 -k = chanlun.K线.创建普K("bar", 1736640000, 50000, 51000, 49000, 50500, 100, 0, 3600) obs.增加原始K线(k) -# 获取分析结果 -print(f"笔数量: {len(obs.笔序列)}") -print(f"线段数量: {len(obs.线段序列)}") -print(f"中枢数量: {len(obs.中枢序列)}") +# 多周期分析器 +analyzer = chanlun.立体分析器("BTCUSD", [60, 300, 1800], config, None) -# 全局开关 -chanlun.分型模式 = False # 从缠K实时读取分型值(逆序包含后使用) +# 信号计算 +from chanlun._chanlun import 信号引擎, call_signal, list_signals + +引擎 = 信号引擎([ + {"name": "youwukuncheng_中枢第三买卖点_V230602", + "freq": 86400, "max_overlap": 3, + "本级完整性": "实", "同级完整性": "合"}, +]) +引擎.自动挂载指标(analyzer) +result = 引擎.更新_完整(analyzer) # → {"signals": {...}, "market": {...}} + +# 单独调用信号 +signals = call_signal("macd_金叉_V260601", obs, {"freq": "日线"}) + +# 查询注册表 +print(list_signals()) # → ["youwukuncheng_...", "macd_金叉_...", ...] ``` -### 绑定层特性 +### 导出类映射 -- **配置缓存**: `obs.配置` 返回同一个 Python 对象(而非每次新建),确保 Python 端修改后 Rust 端可见 -- **分型模式**: 模块级 `chanlun.分型模式` getter/setter,控制分型值的读取策略 -- **数据文件兼容**: `chanlun.读取数据文件("file.nb", config, 观察员=obs)` 与 Python 版签名一致 -- **跨线程身份**: 全局 `LazyLock` 保证跨线程 `is` 判断一致 +| Rust 类型 | Python 类 | 说明 | +|-----------|----------|------| +| `缠论配置` | `chanlun.缠论配置` | 配置管理 | +| `K线` | `chanlun.K线` | 原始 K 线 | +| `缠论K线` | `chanlun.缠论K线` | 缠论 K 线 | +| `分型` | `chanlun.分型` | 顶底分型 | +| `虚线` | `chanlun.虚线` | 笔/线段 | +| `线段特征` | `chanlun.线段特征` | 特征序列元素 | +| `中枢` | `chanlun.中枢` | 中枢 | +| `基础买卖点` | `chanlun.基础买卖点` | 买卖点 | +| `观察者` | `chanlun.观察者` | 单周期分析器 | +| `立体分析器` | `chanlun.立体分析器` | 多周期分析器 | +| `Signal` | `chanlun._chanlun.Signal` | 信号 | +| `Factor` | `chanlun._chanlun.Factor` | 因子 | +| `Event` | `chanlun._chanlun.Event` | 事件 | +| `Position` | `chanlun._chanlun.Position` | 仓位状态机 | +| `Operate` | `chanlun._chanlun.Operate` | 操作枚举 | +| `信号引擎` | `chanlun._chanlun.信号引擎` | 信号计算引擎 | --- ## 数据序列化 -### K线二进制格式 +### K线二进制格式 (.nb 文件) -与 Python `struct.pack(">6d")` 完全兼容的大端字节序格式,每根K线 48 字节: +与 Python `struct.pack(">6d")` 完全兼容,每根 K 线 **48 字节**: -| 偏移 | 大小 | 字段 | -|------|------|------| -| 0 | 8 字节 | 时间戳 (Unix秒 as f64) | -| 8 | 8 字节 | 开盘价 | -| 16 | 8 字节 | 最高价 | -| 24 | 8 字节 | 最低价 | -| 32 | 8 字节 | 收盘价 | -| 40 | 8 字节 | 成交量 | +| 偏移 | 大小 | 字段 | 字节序 | +|------|------|------|--------| +| 0 | 8 B | 时间戳 (Unix秒 as f64) | Big Endian | +| 8 | 8 B | 开盘价 | Big Endian | +| 16 | 8 B | 最高价 | Big Endian | +| 24 | 8 B | 最低价 | Big Endian | +| 32 | 8 B | 收盘价 | Big Endian | +| 40 | 8 B | 成交量 | Big Endian | ```rust -// 序列化 let bytes: [u8; 48] = k线.to_bytes(); - -// 反序列化 -let k线 = K线::读取大端字节数组(&bytes, 周期, 标识); - -// 解析原始数据(不构造K线) -let (时间戳, 开, 高, 低, 收, 量) = K线::解析原始数据(&bytes)?; - -// 批量保存 +let k线 = K线::读取大端字节数组(&bytes, 周期, 标识)?; K线::保存到DAT文件("output.dat", &[&k1, &k2])?; ``` @@ -1002,55 +1182,57 @@ K线::保存到DAT文件("output.dat", &[&k1, &k2])?; { "标识": "BTCUSD", "笔内元素数量": 5, - "平滑异同移动平均线_快线周期": 13, - "平滑异同移动平均线_慢线周期": 31, + "MACD_参数列表": [["macd", "收", 13, 31, 11]], "买卖点_指标模式": "配置", "线段内部背驰_模式": "相对" } ``` -- 使用 `#[serde(default)]` 实现前向/后向兼容,缺失字段自动使用默认值 -- 字符串枚举字段(`指标计算方式`, `买卖点_指标模式`, `线段内部背驰_模式`)带有自定义反序列化器,非法值自动回退默认值并发出 `warn!` 日志 +- `#[serde(default)]` → 前向/后向兼容,缺失字段自动回退默认值 +- `from_dict()` 自动过滤非 `model_fields` 字段 ### 结构化相等校验 -所有核心结构体都实现了 `相等(&self, other, 浮点容差) → (bool, String)` 方法,递归校验包括: -- 浮点字段使用容差比较(而非直接 `==`) -- `Arc` 指针共享的结构体深度校验值语义 -- 嵌套容器(Vec, Option, HashMap)逐一比对 -- 返回详细的差异描述,便于调试 +所有核心结构体实现 `相等(&self, other, 浮点容差) → (bool, String)`: +- 浮点字段容差比较(非直接 `==`) +- `Arc` 共享结构深度校验值语义 +- 嵌套容器 (Vec/Option/HashMap) 逐一比对 +- 返回详细差异描述 --- ## 测试 -### 核心层测试 (`chanlun/`) +### Rust 核心测试 ```bash -cargo test # 运行所有测试 -cargo test -- test_重复计算 # 运行匹配模式的测试 +cd chanlun +cargo test # 199 项测试 +cargo test -- test_50线程 # 匹配模式的测试 +cargo clippy # 零警告 ``` -测试覆盖: -- **配置**: 默认值、JSON 往返、部分反序列化、非法值回退、不推送模式 -- **K线**: 方向判定、大端字节序列化往返 -- **缠K**: 创建/镜像/包含合并/分型识别、三K线生成分型 -- **分型**: 左中右构造、字段读写、关联指针 -- **虚线**: Cell/RefCell 读写、Clone 后 Rc 指针一致、多 Arc 共享可见性、获取武递归、武修改不影响文 -- **线段特征**: 文取max/min, 武取max/min, tiebreaker, 添加/删除 -- **中枢**: 字段读写、Clone 后指针一致、多 Arc 共享 -- **买卖点**: 偏移/失效偏移/有效性、18种类型生成 -- **观察者**: 指针一致性 (普K/分型/笔/线段/中枢)、重复计算确定性、重置后数据一致、RefCell 借用安全、跨线程 Send/Sync +**测试覆盖**: +- **配置**: 默认值 / JSON 往返 / 部分反序列化 / 非法值回退 / 不推送 / 差异对比 +- **K线**: 方向判定 / 大端序列化往返 +- **缠K**: 创建/包含合并/分型识别 +- **笔**: 基本分析 / 弱化 / 次级成笔 / 实际高低点 +- **线段**: 四种象 / 缺口 / 分割序列 / 特征序列状态 +- **中枢**: 字段读写 / Clone 后指针一致 / 延伸/扩展 / 第三买卖点 +- **买卖点**: 18 种类型生成 / 偏移/失效/有效性 +- **指标**: MACD/RSI/KDJ/BOLL 首次+增量计算 +- **观察者**: 指针一致性 / 重复计算确定性 / 重置后一致 / RefCell 安全 / 跨线程 Send/Sync +- **Position**: 28 项单元测试 — LO/SO/LE/SE 转换 / 止损/超时/间隔 / 盈亏计算 +- **Registry**: 编译时/动态注册 / 归并/注销 / 冲突报错 / #[signal] 宏端到端 +- **并发**: 50 线程 10,000 K线压测 -### Python 绑定层测试 (`chanlun-py/tests/test_all.py`) +### Python 集成测试 ```bash -cd chanlun-py && maturin develop -python3 -m pytest tests/test_all.py -v -# 107+ tests, 涵盖: -# - 指标挂载与回填 -# - 跨线程身份一致性 (is 判断) -# - 买卖意义/趋向背驰/行为统计/停顿位置/线段内部背驰等 双端(Rust绑定 vs chan.py)数据对比 +cd chanlun-py +python -m pytest tests/test_all.py -v # 107+ 项 +python -m pytest tests/test_signal_primitives.py -v +python -m pytest tests/test_position_update.py -v # 24 项 ``` --- @@ -1063,6 +1245,9 @@ MIT License. 详见 [LICENSE](LICENSE). ## 相关项目 -- [`chan.py`](../chan.py) — Python 参考实现(~4200 行) -- [`chanlun-py`](../chanlun-py/) — PyO3 绑定,将本 crate 导出为 Python `chanlun` 模块 -- [`main.py`](../main.py) — Python 图表可视化程序 +- [`chan.py`](../chan.py) — Python 参考实现(~4,200 行),作为跨校验基准 +- [`chanlun-py`](../chanlun-py/) — PyO3 绑定,发布为 `chanlun` PyPI 包 +- [`chanlun-signal-macros`](../chanlun-signal-macros/) — `#[signal]` 属性宏 proc-macro crate +- [`main.py`](../main.py) — FastAPI Web 应用(WebSocket 实时图表 + 回测) +- [`strategies.py`](../strategies.py) — 回测策略定义(Backtrader 集成) +- [`examples/plugin-demo`](../examples/plugin-demo/) — .so 动态插件示例(两种注册方式) diff --git a/chanlun/src/algorithm/bi.rs b/chanlun/src/algorithm/bi.rs index 44cdc03..cf280cd 100644 --- a/chanlun/src/algorithm/bi.rs +++ b/chanlun/src/algorithm/bi.rs @@ -29,9 +29,9 @@ use crate::kline::chan_kline::缠论K线; use crate::structure::dash_line::虚线; use crate::structure::fractal_obj::分型; use crate::types::{分型结构, 相对方向}; +use crate::{error, warn}; use std::sync::Arc; use std::sync::atomic::Ordering; -use tracing::{error, warn}; /// 笔 — 从分型生成笔的算法集合(静态方法命名空间) pub struct 笔; @@ -54,8 +54,7 @@ impl 笔 { if let (Some(高点), Some(低点)) = (&实际高点, &实际低点) { let 原始数量 = 1 - + (低点.标的K线.read().unwrap().序号 - 高点.标的K线.read().unwrap().序号) - .unsigned_abs() as usize; + + (低点.标的K线.read().序号 - 高点.标的K线.read().序号).unsigned_abs() as usize; if 原始数量 >= 配置.笔内元素数量 as usize { return 配置.笔内元素数量 as usize; } @@ -76,8 +75,8 @@ impl 笔 { && let (Some(高_k), Some(低_k)) = (&实际高点, &实际低点) { let 原始数量 = 1 - + (低_k.标的K线.read().unwrap().序号 - 高_k.标的K线.read().unwrap().序号) - .unsigned_abs() as usize; + + (低_k.标的K线.read().序号 - 高_k.标的K线.read().序号).unsigned_abs() + as usize; // 向上笔 if 筆.方向().是否向上() && 低_k.低.get() < 筆.低() @@ -219,7 +218,7 @@ impl 笔 { /// 判断笔的相对关系是否合理 pub fn _相对关系(筆: &虚线, 配置: &缠论配置) -> bool { let 文分型 = &筆.文; - let 武分型 = 筆.武.read().unwrap(); + let 武分型 = 筆.武.read(); let 相对关系 = if 配置.笔内起始分型包含整笔 { let 文中_rc = Arc::clone(&文分型.中); @@ -261,8 +260,8 @@ impl 笔 { 武分型.中.低.get(), ); if 配置.笔内原始K线包含整笔 { - let 文标的 = 文分型.中.标的K线.read().unwrap(); - let 武标的 = 武分型.中.标的K线.read().unwrap(); + let 文标的 = 文分型.中.标的K线.read(); + let 武标的 = 武分型.中.标的K线.read(); if crate::types::相对方向::分析(文标的.高, 文标的.低, 武标的.高, 武标的.低) .是否包含() { @@ -280,10 +279,7 @@ impl 笔 { /// 以文会友 — 根据起点分型找笔 pub fn 以文会友(笔序列: &[Arc<虚线>], 文: &Arc<分型>) -> Option> { - 笔序列 - .iter() - .find(|b| Arc::as_ptr(&b.文) == Arc::as_ptr(文)) - .cloned() + 笔序列.iter().find(|b| Arc::ptr_eq(&b.文, 文)).cloned() } /// 以武会友 — 根据终点分型找笔 @@ -291,7 +287,7 @@ impl 笔 { 笔序列 .iter() .rev() - .find(|b| Arc::as_ptr(&*b.武.read().unwrap()) == Arc::as_ptr(武)) + .find(|b| Arc::ptr_eq(&*b.武.read(), 武)) .cloned() } @@ -305,8 +301,7 @@ impl 笔 { for b in 笔序列.iter().rev() { // Python: 筆.文.中.序号 - 偏移 <= 缠K.序号 <= 筆.武.中.序号 if b.文.中.序号.load(Ordering::Relaxed) - 偏移 <= 缠K.序号.load(Ordering::Relaxed) - && 缠K.序号.load(Ordering::Relaxed) - <= b.武.read().unwrap().中.序号.load(Ordering::Relaxed) + && 缠K.序号.load(Ordering::Relaxed) <= b.武.read().中.序号.load(Ordering::Relaxed) && b.文.中.周期 == 缠K.周期 && b.文.中.标识 == 缠K.标识 { @@ -323,7 +318,7 @@ impl 笔 { let 旧分型 = 分型序列.pop(); if let (Some(旧笔), Some(旧分型)) = (笔序列.pop(), 旧分型) { assert!( - Arc::as_ptr(&旧笔.武.read().unwrap()) == Arc::as_ptr(&旧分型), + Arc::ptr_eq(&旧笔.武.read(), &旧分型), "最后一笔终点错误{}", 行号 ); @@ -388,7 +383,7 @@ impl 笔 { // Python line 2343-2348: 笔弱化模式 if 配置.笔弱化 && !笔序列.is_empty() { let 前一笔 = 笔序列.last().unwrap(); - let 前一笔缠K数 = 前一笔.武.read().unwrap().中.序号.load(Ordering::Relaxed) + let 前一笔缠K数 = 前一笔.武.read().中.序号.load(Ordering::Relaxed) - 前一笔.文.中.序号.load(Ordering::Relaxed) + 1; if 前一笔缠K数 == 3 { @@ -434,7 +429,7 @@ impl 笔 { // Python line 2359-2367: 文官调整 if let Some(ref 文官_k) = 文官 - && Arc::as_ptr(文官_k) != Arc::as_ptr(&之前分型.中) + && !Arc::ptr_eq(文官_k, &之前分型.中) && let Some(临时分型) = 分型::从缠K序列中获取分型(缠K序列, 文官_k) { @@ -476,7 +471,7 @@ impl 笔 { if Self::_相对关系(&当前笔, 配置) && let Some(ref 武将_k) = 武将 - && Arc::as_ptr(武将_k) == Arc::as_ptr(&当前分型.中) + && Arc::ptr_eq(武将_k, &当前分型.中) { // 直接添加(对照 Python _添加新笔:直接 append) Self::_添加新笔(分型序列, 笔序列, 当前分型, 当前笔, line!()); @@ -490,7 +485,7 @@ impl 笔 { _ => Self::_次高(&基础序列, 配置.笔内相同终点取舍), }; if let Some(ref 武将_k) = 武将 - && Arc::as_ptr(武将_k) == Arc::as_ptr(&当前分型.中) + && Arc::ptr_eq(武将_k, &当前分型.中) && Self::_相对关系(&当前笔, 配置) { Self::_添加新笔(分型序列, 笔序列, 当前分型, 当前笔, line!()); @@ -557,13 +552,12 @@ impl 笔 { if !分型序列.is_empty() && Arc::as_ptr(分型序列.last().unwrap()) == Arc::as_ptr(&临时分型_rc) - && let Some(武_idx) = 缠K序列 - .iter() - .position(|k| Arc::as_ptr(k) == Arc::as_ptr(武将_k)) + && let Some(武_idx) = + 缠K序列.iter().position(|k| Arc::ptr_eq(k, 武将_k)) { for ck in &缠K序列[武_idx..] { - if (*ck.分型.read().unwrap() == Some(分型结构::底) - || *ck.分型.read().unwrap() == Some(分型结构::顶)) + if (*ck.分型.read() == Some(分型结构::底) + || *ck.分型.read() == Some(分型结构::顶)) && let Some(错过分型) = 分型::从缠K序列中获取分型(缠K序列, ck) { @@ -577,6 +571,15 @@ impl 笔 { 递归层次 + 1, 配置, ); + if !分型序列.is_empty() + && Arc::as_ptr(分型序列.last().unwrap()) + == Arc::as_ptr(&错过分型_rc) + { + warn!( + "笔.分析 事后修复错过的笔:{}, 当前分型: {}", + 错过分型_rc, 当前分型 + ); + } } } } @@ -649,14 +652,10 @@ impl 笔 { 新筆 .序号 .store(前一笔.序号.load(Ordering::Relaxed) + 1, Ordering::Relaxed); - if 新筆.武.read().unwrap().左.is_none() || 新筆.武.read().unwrap().右.is_none() - { + if 新筆.武.read().左.is_none() || 新筆.武.read().右.is_none() { 新筆.有效性.store(false, Ordering::Relaxed); } - if matches!( - 前一笔.武.read().unwrap().结构(), - 分型结构::上 | 分型结构::下 - ) { + if matches!(前一笔.武.read().结构(), 分型结构::上 | 分型结构::下) { error!("_添加新笔[{}] 出现无效分型 {}", 行号, 前一笔); } } @@ -676,7 +675,7 @@ impl 笔 { Self::_实际低点(&基础序列, 配置.笔内相同终点取舍), ) && Arc::ptr_eq(&筆.文.中, &实际高) - && Arc::ptr_eq(&筆.武.read().unwrap().中, &实际低) + && Arc::ptr_eq(&筆.武.read().中, &实际低) { return true; } @@ -686,7 +685,7 @@ impl 笔 { Self::_实际高点(&基础序列, 配置.笔内相同终点取舍), ) && Arc::ptr_eq(&筆.文.中, &实际低) - && Arc::ptr_eq(&筆.武.read().unwrap().中, &实际高) + && Arc::ptr_eq(&筆.武.read().中, &实际高) { return true; } @@ -696,9 +695,9 @@ impl 笔 { /// 获取所有停顿位置 — 在笔范围内找出所有能成笔的分型组合 pub fn 获取所有停顿位置(筆: &虚线, 观察员: &观察者) -> Vec<虚线> { - let mut 笔序列 = Vec::new(); - let 文 = Arc::clone(&筆.文); let 基础序列 = 筆.获取缠K序列(&观察员.缠论K线序列); + let mut 笔序列 = Vec::with_capacity(基础序列.len() / 2); + let 文 = Arc::clone(&筆.文); if 基础序列.len() < 5 { return 笔序列; @@ -707,10 +706,8 @@ impl 笔 { for i in 3..基础序列.len() - 1 { let k = &基础序列[i]; - let 匹配顶 = - *k.分型.read().unwrap() == Some(分型结构::顶) && 筆.方向() == 相对方向::向上; - let 匹配底 = - *k.分型.read().unwrap() == Some(分型结构::底) && 筆.方向() == 相对方向::向下; + let 匹配顶 = *k.分型.read() == Some(分型结构::顶) && 筆.方向() == 相对方向::向上; + let 匹配底 = *k.分型.read() == Some(分型结构::底) && 筆.方向() == 相对方向::向下; if 匹配顶 || 匹配底 { let 左 = Arc::clone(&基础序列[i - 1]); let 中 = Arc::clone(k); @@ -737,12 +734,12 @@ impl 笔 { for 筆 in &停顿位置 { let k线范围 = K线::截取rc( &观察员.普通K线序列, - &当前筆.文.中.标的K线.read().unwrap().clone(), - &当前筆.武.read().unwrap().中.标的K线.read().unwrap().clone(), + &当前筆.文.中.标的K线.read().clone(), + &当前筆.武.read().中.标的K线.read().clone(), ); let 背驰信号 = 虚线::计算K线序列MACD趋向背驰(&k线范围, 筆.方向()); if 背驰信号.iter().all(|&x| x) { - 结果.push(Arc::clone(&筆.武.read().unwrap().中)); + 结果.push(Arc::clone(&筆.武.read().中)); } } diff --git a/chanlun/src/algorithm/divergence.rs b/chanlun/src/algorithm/divergence.rs index cecc45f..3e4cdf7 100644 --- a/chanlun/src/algorithm/divergence.rs +++ b/chanlun/src/algorithm/divergence.rs @@ -39,13 +39,13 @@ impl 背驰分析 { ) -> bool { let 进入MACD = Self::_获取MACD面积( K线序列, - &进入段.文.中.标的K线.read().unwrap(), - &进入段.武.read().unwrap().中.标的K线.read().unwrap(), + &进入段.文.中.标的K线.read(), + &进入段.武.read().中.标的K线.read(), ); let 离开MACD = Self::_获取MACD面积( K线序列, - &离开段.文.中.标的K线.read().unwrap(), - &离开段.武.read().unwrap().中.标的K线.read().unwrap(), + &离开段.文.中.标的K线.read(), + &离开段.武.read().中.标的K线.read(), ); // 计算面积(绝对值求和) @@ -69,18 +69,18 @@ impl 背驰分析 { /// 斜率背驰 — 价格斜率背驰 pub fn 斜率背驰(进入段: &虚线, 离开段: &虚线) -> bool { - let dx = (进入段.武.read().unwrap().时间戳() - 进入段.文.时间戳()) as f64; + let dx = (进入段.武.read().时间戳() - 进入段.文.时间戳()) as f64; if dx == 0.0 { return false; } - let dy = 进入段.武.read().unwrap().分型特征值 - 进入段.文.分型特征值; + let dy = 进入段.武.read().分型特征值 - 进入段.文.分型特征值; let 进入斜率 = dy / dx; - let dx = (离开段.武.read().unwrap().时间戳() - 离开段.文.时间戳()) as f64; + let dx = (离开段.武.read().时间戳() - 离开段.文.时间戳()) as f64; if dx == 0.0 { return false; } - let dy = 离开段.武.read().unwrap().分型特征值 - 离开段.文.分型特征值; + let dy = 离开段.武.read().分型特征值 - 离开段.文.分型特征值; let 离开斜率 = dy / dx; if 进入段.方向() == 相对方向::向上 { @@ -92,12 +92,12 @@ impl 背驰分析 { /// 测度背驰 — 价格时间测度背驰 pub fn 测度背驰(进入段: &虚线, 离开段: &虚线) -> bool { - let dx = (进入段.武.read().unwrap().时间戳() - 进入段.文.时间戳()) as f64; - let dy = 进入段.武.read().unwrap().分型特征值 - 进入段.文.分型特征值; + let dx = (进入段.武.read().时间戳() - 进入段.文.时间戳()) as f64; + let dy = 进入段.武.read().分型特征值 - 进入段.文.分型特征值; let 进入测度 = (dx * dx + dy * dy).sqrt(); - let dx = (离开段.武.read().unwrap().时间戳() - 离开段.文.时间戳()) as f64; - let dy = 离开段.武.read().unwrap().分型特征值 - 离开段.文.分型特征值; + let dx = (离开段.武.read().时间戳() - 离开段.文.时间戳()) as f64; + let dy = 离开段.武.read().分型特征值 - 离开段.文.分型特征值; let 离开测度 = (dx * dx + dy * dy).sqrt(); if 进入段.方向() == 相对方向::向上 { @@ -187,12 +187,8 @@ impl 背驰分析 { // ---- 内部辅助 ---- fn _获取MACD面积(K线序列: &[Arc], 始: &Arc, 终: &Arc) -> MACD面积 { - let 始_idx = K线序列 - .iter() - .position(|k| Arc::as_ptr(k) == Arc::as_ptr(始)); - let 终_idx = K线序列 - .iter() - .position(|k| Arc::as_ptr(k) == Arc::as_ptr(终)); + let 始_idx = K线序列.iter().position(|k| Arc::ptr_eq(k, 始)); + let 终_idx = K线序列.iter().position(|k| Arc::ptr_eq(k, 终)); let mut 阳 = 0.0f64; let mut 阴 = 0.0f64; @@ -200,7 +196,7 @@ impl 背驰分析 { if let (Some(始), Some(终)) = (始_idx, 终_idx) { let (始, 终) = if 始 <= 终 { (始, 终) } else { (终, 始) }; for k in &K线序列[始..=终] { - if let Some(macd) = k.指标.read().unwrap().macd() { + if let Some(macd) = k.指标.read().macd() { let hist = macd.MACD柱; if hist >= 0.0 { 阳 += hist; diff --git a/chanlun/src/algorithm/hub.rs b/chanlun/src/algorithm/hub.rs index efda218..ffee8a9 100644 --- a/chanlun/src/algorithm/hub.rs +++ b/chanlun/src/algorithm/hub.rs @@ -25,8 +25,9 @@ use crate::structure::dash_line::虚线; use crate::structure::fractal_obj::分型; use crate::types::相对方向; +use parking_lot::RwLock; +use std::sync::Arc; use std::sync::atomic::{AtomicI64, Ordering}; -use std::sync::{Arc, RwLock}; /// 中枢 — 三段虚线重叠区间构成的价格中枢 /// @@ -59,11 +60,11 @@ impl Clone for 中枢 { fn clone(&self) -> Self { Self { 序号: AtomicI64::new(self.序号.load(Ordering::Relaxed)), - 标识: RwLock::new(self.标识.read().unwrap().clone()), + 标识: RwLock::new(self.标识.read().clone()), 级别: AtomicI64::new(self.级别.load(Ordering::Relaxed)), - 基础序列: RwLock::new(self.基础序列.read().unwrap().clone()), - 第三买卖线: RwLock::new(self.第三买卖线.read().unwrap().clone()), - 本级_第三买卖线: RwLock::new(self.本级_第三买卖线.read().unwrap().clone()), + 基础序列: RwLock::new(self.基础序列.read().clone()), + 第三买卖线: RwLock::new(self.第三买卖线.read().clone()), + 本级_第三买卖线: RwLock::new(self.本级_第三买卖线.read().clone()), } } } @@ -83,9 +84,9 @@ impl 中枢 { /// 向基础序列尾部添加虚线(中枢延伸),并清除第三买卖线 pub fn _添加虚线(&self, 实线: Arc<虚线>) { - self.基础序列.write().unwrap().push(实线); - *self.本级_第三买卖线.write().unwrap() = None; - *self.第三买卖线.write().unwrap() = None; + self.基础序列.write().push(实线); + *self.本级_第三买卖线.write() = None; + *self.第三买卖线.write() = None; } /// 返回图表标题字符串,格式为 "文.标识:文.周期:中枢标识:序号" @@ -94,24 +95,25 @@ impl 中枢 { "{}:{}:{}:{}", self.文().中.标识, self.文().中.周期, - self.标识.read().unwrap(), + self.标识.read(), self.序号.load(Ordering::Relaxed) ) } /// 返回基础序列的最后一根虚线(当前离开段) pub fn 离开段(&self) -> Arc<虚线> { - Arc::clone(&self.基础序列.read().unwrap()[self.基础序列.read().unwrap().len() - 1]) + let guard = self.基础序列.read(); + Arc::clone(&guard[guard.len() - 1]) } /// 返回中枢方向(与基础序列第一段方向相反) pub fn 方向(&self) -> 相对方向 { - self.基础序列.read().unwrap()[0].方向().翻转() + self.基础序列.read()[0].方向().翻转() } /// 中枢上沿 = min(前三段的高) pub fn 高(&self) -> f64 { - self.基础序列.read().unwrap()[..3] + self.基础序列.read()[..3] .iter() .map(|x| x.高()) .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) @@ -120,7 +122,7 @@ impl 中枢 { /// 中枢下沿 = max(前三段的低) pub fn 低(&self) -> f64 { - self.基础序列.read().unwrap()[..3] + self.基础序列.read()[..3] .iter() .map(|x| x.低()) .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) @@ -131,7 +133,6 @@ impl 中枢 { pub fn 高高(&self) -> f64 { self.基础序列 .read() - .unwrap() .iter() .map(|x| x.高()) .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) @@ -142,7 +143,6 @@ impl 中枢 { pub fn 低低(&self) -> f64 { self.基础序列 .read() - .unwrap() .iter() .map(|x| x.低()) .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)) @@ -151,46 +151,47 @@ impl 中枢 { /// 返回基础序列第一段的起点分型 pub fn 文(&self) -> Arc<分型> { - Arc::clone(&self.基础序列.read().unwrap()[0].文) + Arc::clone(&self.基础序列.read()[0].文) } /// 返回基础序列最后一段的终点分型 pub fn 武(&self) -> Arc<分型> { - Arc::clone( - &*self.基础序列.read().unwrap()[self.基础序列.read().unwrap().len() - 1] - .武 - .read() - .unwrap(), - ) + let guard = self.基础序列.read(); + Arc::clone(&*guard[guard.len() - 1].武.read()) } /// 设置第三类买卖点对应的虚线 pub fn 设置第三买卖线(&self, 线: Option>) { - *self.第三买卖线.write().unwrap() = 线; + *self.第三买卖线.write() = 线; } /// 获取序列 — 基础序列 + 第三买卖线(若有) pub fn 获取序列(&self) -> Vec> { - let mut 序列: Vec> = self.基础序列.read().unwrap().clone(); - if let Some(ref 三买) = *self.第三买卖线.read().unwrap() { + let mut 序列: Vec> = self.基础序列.read().clone(); + if let Some(ref 三买) = *self.第三买卖线.read() { 序列.push(Arc::clone(三买)); } 序列 } + /// 获取基础序列最后一个元素 + pub fn 最后(&self) -> Option> { + self.基础序列.read().last().cloned() + } + /// 返回序列化数据文本,用于调试和存储 pub fn 获取数据文本(&self) -> String { - let 第三买卖线_str = match &*self.第三买卖线.read().unwrap() { + let 第三买卖线_str = match &*self.第三买卖线.read() { Some(x) => format!("{}", x), None => "None".to_string(), }; - let 本级_第三买卖线_str = match &*self.本级_第三买卖线.read().unwrap() { + let 本级_第三买卖线_str = match &*self.本级_第三买卖线.read() { Some(x) => format!("{}", x), None => "None".to_string(), }; format!( "{}, {}, {}, 文:({},{}), 武:({},{}), {}, {}", - self.标识.read().unwrap(), + self.标识.read(), self.序号.load(Ordering::Relaxed), self.级别.load(Ordering::Relaxed), self.文().时间戳(), @@ -204,67 +205,63 @@ impl 中枢 { /// 校验中枢合法性 pub fn _校验合法性(&self, 序列: &[Arc<虚线>]) -> bool { - let mut 有效序列 = self.基础序列.read().unwrap().clone(); + let guard = self.基础序列.read(); + let mut 有效序列 = guard.clone(); let mut 无效序列: Vec> = Vec::new(); - for 元素 in self.基础序列.read().unwrap().iter() { - if !序列.iter().any(|x| Arc::as_ptr(x) == Arc::as_ptr(元素)) { + let 首序号 = 序列[0].序号.load(Ordering::Relaxed); + for 元素 in guard.iter() { + let idx = (元素.序号.load(Ordering::Relaxed) - 首序号) as usize; + if idx >= 序列.len() || !Arc::ptr_eq(&序列[idx], 元素) { 无效序列.push(Arc::clone(元素)); } } - if !无效序列.is_empty() { let 无效 = &无效序列[0]; - if let Some(pos) = self - .基础序列 - .read() - .unwrap() - .iter() - .position(|x| Arc::as_ptr(x) == Arc::as_ptr(无效)) - { - 有效序列 = self.基础序列.read().unwrap()[..pos].to_vec(); - } + // Python: 序号 = 线段._索引(self.基础序列, 无效) + let pos = crate::algorithm::segment::线段::_索引(&guard, 无效); + 有效序列 = guard[..pos].to_vec(); } + drop(guard); if 有效序列.len() < 3 { self.设置第三买卖线(None); - *self.本级_第三买卖线.write().unwrap() = None; + *self.本级_第三买卖线.write() = None; return false; } - *self.基础序列.write().unwrap() = 有效序列; + *self.基础序列.write() = 有效序列; let 中枢高 = self.高(); let 中枢低 = self.低(); 有效序列 = Vec::new(); - for 元素 in self.基础序列.read().unwrap().iter() { + for 元素 in self.基础序列.read().iter() { if crate::types::相对方向::分析(中枢高, 中枢低, 元素.高(), 元素.低()).是否缺口() { break; } 有效序列.push(Arc::clone(元素)); } - *self.基础序列.write().unwrap() = 有效序列; + *self.基础序列.write() = 有效序列; - if self.基础序列.read().unwrap().len() < 3 { - return false; - } - - for i in 1..self.基础序列.read().unwrap().len() { - let 前 = &self.基础序列.read().unwrap()[i - 1]; - let 后 = &self.基础序列.read().unwrap()[i]; - if !前.之后是(后) { + let 是缺口 = { + let guard = self.基础序列.read(); + if guard.len() < 3 { return false; } - } - - if !crate::types::相对方向::分析( - self.基础序列.read().unwrap()[0].高(), - self.基础序列.read().unwrap()[0].低(), - self.基础序列.read().unwrap()[2].高(), - self.基础序列.read().unwrap()[2].低(), - ) - .是否缺口() - { + for i in 1..guard.len() { + if !guard[i - 1].之后是(&guard[i]) { + return false; + } + } + crate::types::相对方向::分析( + guard[0].高(), + guard[0].低(), + guard[2].高(), + guard[2].低(), + ) + .是否缺口() + }; + if !是缺口 { let 重叠高 = self.高(); let 重叠低 = self.低(); if 重叠低 > 重叠高 { @@ -272,10 +269,12 @@ impl 中枢 { } } - let 三买线_opt = self.第三买卖线.read().unwrap().clone(); + let 三买线_opt = self.第三买卖线.read().clone(); if let Some(ref 三买线) = 三买线_opt { - if 序列.iter().any(|x| Arc::as_ptr(x) == Arc::as_ptr(三买线)) { - if !self.基础序列.read().unwrap().last().unwrap().之后是(三买线) { + let 首序号 = 序列[0].序号.load(Ordering::Relaxed); + let idx = (三买线.序号.load(Ordering::Relaxed) - 首序号) as usize; + if idx < 序列.len() && Arc::ptr_eq(&序列[idx], 三买线) { + if !self.基础序列.read().last().unwrap().之后是(三买线) { self.设置第三买卖线(None); } else if !crate::types::相对方向::分析( self.高(), @@ -298,25 +297,44 @@ impl 中枢 { /// 完整性 — 详见教你炒股票43:有关背驰的补习课 /// 不完整时下一个中枢大概率会与当前中枢发生扩展 pub fn 完整性(&self, 虚实: &str) -> bool { - if *self.基础序列.read().unwrap()[0].标识.read().unwrap() == "笔" { - return self.第三买卖线.read().unwrap().is_some(); + if *self.基础序列.read()[0].标识.read() == "笔" { + return self.第三买卖线.read().is_some(); } - let 基础序列_ref = self.基础序列.read().unwrap(); + // if self.本级_第三买卖线: return True # 暂未启用 + + let 中枢状态 = self.当前状态(); + if 中枢状态 == "中枢之中" { + return false; + } + + let 基础序列_ref = self.基础序列.read(); let 最后段 = 基础序列_ref.last().unwrap(); let 内部中枢_vec = if 虚实 == "合" { - 最后段.合_中枢序列.read().unwrap() + 最后段.合_中枢序列.read() } else { - 最后段.实_中枢序列.read().unwrap() + 最后段.实_中枢序列.read() }; + if 内部中枢_vec.is_empty() { + return false; + } + + let 高 = self.高(); + let 低 = self.低(); for 内部中枢 in 内部中枢_vec.iter() { - if crate::types::相对方向::分析( - self.高(), - self.低(), - 内部中枢.高(), - 内部中枢.低(), - ) - .是否缺口() + let 内部中枢高 = 内部中枢.高(); + let 内部中枢低 = 内部中枢.低(); + if 中枢状态 == "中枢之下" { + if 低 <= 内部中枢高 { + continue; + } + } else { + // 中枢之上 + if 高 >= 内部中枢低 { + continue; + } + } + if crate::types::相对方向::分析(高, 低, 内部中枢高, 内部中枢低).是否缺口() { return true; } @@ -330,24 +348,19 @@ impl 中枢 { 扩展中枢: &mut Vec>, 配置: &crate::config::缠论配置, ) { - if self.基础序列.read().unwrap().len() >= 9 { + if self.基础序列.read().len() >= 9 { let mut 扩展线段: Vec> = Vec::new(); - let 基础序列_ref = self.基础序列.read().unwrap(); + let 基础序列_ref = self.基础序列.read(); crate::algorithm::segment::线段::扩展分析(&基础序列_ref, &mut 扩展线段, 配置); - 中枢::分析( - &扩展线段, - 扩展中枢, - false, - &format!("{}_扩展中枢_", self.标识.read().unwrap()), - 0, - ); + let 标识 = format!("{}_扩展中枢_", self.标识.read()); + 中枢::分析(&扩展线段, 扩展中枢, false, &标识, 0); } } /// 当前状态 — 详见教你炒股票49:利润率最大的操作模式 /// 返回当前中枢最后一段所处的位置关系:中枢之中/中枢之上/中枢之下 pub fn 当前状态(&self) -> &str { - let 基础序列_ref = self.基础序列.read().unwrap(); + let 基础序列_ref = self.基础序列.read(); let 最后 = Arc::clone(基础序列_ref.last().unwrap()); let 尾部 = 最后.获取_武(); let 关系 = crate::types::相对方向::分析( @@ -390,7 +403,7 @@ impl 中枢 { assert!(Self::基础检查(&左, &中, &右), "中枢.创建 基础检查失败"); Self::new( 0, - format!("{}中枢<{}>", 标识, 中.标识.read().unwrap()), + format!("{}中枢<{}>", 标识, 中.标识.read()), 级别, vec![左, 中, 右], ) @@ -422,18 +435,8 @@ impl 中枢 { 待添加中枢 .序号 .store(前一个.序号.load(Ordering::Relaxed) + 1, Ordering::Relaxed); - let 前_last_序号 = 前一个 - .获取序列() - .last() - .unwrap() - .序号 - .load(Ordering::Relaxed); - let new_last_序号 = 待添加中枢 - .获取序列() - .last() - .unwrap() - .序号 - .load(Ordering::Relaxed); + let 前_last_序号 = 前一个.最后().unwrap().序号.load(Ordering::Relaxed); + let new_last_序号 = 待添加中枢.最后().unwrap().序号.load(Ordering::Relaxed); if 前_last_序号 > new_last_序号 { panic!( "向中枢序列尾部添加 序号错误 前last={} > new_last={}", @@ -478,11 +481,8 @@ impl 中枢 { let 右 = &虚线序列[i + 1]; if Self::基础检查(左, 中, 右) { - // Python: 序号 = 虚线序列.index(左) - let 序号 = 虚线序列 - .iter() - .position(|x| Arc::as_ptr(x) == Arc::as_ptr(左)) - .expect("中枢.分析: 左元素不在虚线序列中"); + // Python: 序号 = 线段._索引(虚线序列, 左) + let 序号: usize = crate::algorithm::segment::线段::_索引(虚线序列, 左); if 跳过首部 && (左.序号.load(Ordering::Relaxed) == 0 || 序号 == 0) { continue; } @@ -525,22 +525,16 @@ impl 中枢 { return; } - // 找到当前中枢最后一个元素在虚线序列中的位置 + // Python: 序号 = 线段._索引(虚线序列, 当前中枢.基础序列[-1]) + 1 let 起始索引 = { let cur = &中枢序列[当前中枢_idx]; - let 最后元素 = &cur.基础序列.read().unwrap()[cur.基础序列.read().unwrap().len() - 1]; - match 虚线序列 - .iter() - .position(|x| Arc::as_ptr(x) == Arc::as_ptr(最后元素)) - { - Some(idx) => idx + 1, - None => return, - } + let guard = cur.基础序列.read(); + crate::algorithm::segment::线段::_索引(虚线序列, &guard[guard.len() - 1]) + 1 }; let mut 中枢高 = 中枢序列[当前中枢_idx].高(); let mut 中枢低 = 中枢序列[当前中枢_idx].低(); - let mut 候选序列: Vec> = Vec::new(); + let mut 候选序列 = Vec::with_capacity(3); for 当前虚线_ref in &虚线序列[起始索引..] { let 当前虚线 = Arc::clone(当前虚线_ref); @@ -553,12 +547,7 @@ impl 中枢 { // Python: if 当前中枢.基础序列[-1].之后是(当前虚线): let needs_三买 = { let cur = &中枢序列[当前中枢_idx]; - cur.基础序列 - .read() - .unwrap() - .last() - .unwrap() - .之后是(&当前虚线) + cur.基础序列.read().last().unwrap().之后是(&当前虚线) }; if needs_三买 { 中枢序列[当前中枢_idx].设置第三买卖线(Some(当前虚线.clone())); @@ -570,17 +559,11 @@ impl 中枢 { 中枢序列[当前中枢_idx] .基础序列 .read() - .unwrap() .last() .unwrap() .之后是(&当前虚线), "中枢延伸: 不连续 {}, {}", - 中枢序列[当前中枢_idx] - .基础序列 - .read() - .unwrap() - .last() - .unwrap(), + 中枢序列[当前中枢_idx].基础序列.read().last().unwrap(), 当前虚线 ); 中枢序列[当前中枢_idx]._添加虚线(当前虚线); @@ -594,7 +577,6 @@ impl 中枢 { let 起始方向 = 中枢序列[当前中枢_idx] .基础序列 .read() - .unwrap() .last() .unwrap() .方向() @@ -628,13 +610,13 @@ impl 中枢 { ), ); } - if *self.标识.read().unwrap() != *other.标识.read().unwrap() { + if *self.标识.read() != *other.标识.read() { return ( false, format!( "中枢: [标识] 不等 A={},B={}", - self.标识.read().unwrap(), - other.标识.read().unwrap() + self.标识.read(), + other.标识.read() ), ); } @@ -649,8 +631,8 @@ impl 中枢 { ); } // 基础序列 - let a_seq = self.基础序列.read().unwrap(); - let b_seq = other.基础序列.read().unwrap(); + let a_seq = self.基础序列.read(); + let b_seq = other.基础序列.read(); if a_seq.len() != b_seq.len() { return ( false, @@ -692,16 +674,16 @@ impl 中枢 { }; 检查单个( "第三买卖线", - &self.第三买卖线.read().unwrap(), - &other.第三买卖线.read().unwrap(), + &self.第三买卖线.read(), + &other.第三买卖线.read(), 浮点容差, ) .map_err(|e| (false, e)) .ok(); 检查单个( "本级_第三买卖线", - &self.本级_第三买卖线.read().unwrap(), - &other.本级_第三买卖线.read().unwrap(), + &self.本级_第三买卖线.read(), + &other.本级_第三买卖线.read(), 浮点容差, ) .map_err(|e| (false, e)) @@ -712,21 +694,26 @@ impl 中枢 { impl std::fmt::Display for 中枢 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let 序列_str = self - .基础序列 - .read() - .unwrap() - .iter() - .map(|d| format!("{}", d)) - .collect::>() - .join(", "); + let guard = self.基础序列.read(); + let len = guard.len(); + let 序列_str = if let Some((first, rest)) = guard.split_first() { + let mut s = format!("{}", first); + for d in rest { + use std::fmt::Write; + write!(&mut s, ", {}", d).unwrap(); + } + s + } else { + String::new() + }; + drop(guard); write!( f, "{}({}, {}, 元素数量: {}, [{}], {} ===>>> {})", - self.标识.read().unwrap(), + self.标识.read(), crate::utils::format_f64_g(self.高()), crate::utils::format_f64_g(self.低()), - self.基础序列.read().unwrap().len(), + len, 序列_str, self.文(), self.武(), @@ -851,11 +838,11 @@ mod tests { ); assert_eq!(中枢.序号.load(Ordering::Relaxed), 1); - assert_eq!(*中枢.标识.read().unwrap(), "测试中枢"); + assert_eq!(*中枢.标识.read(), "测试中枢"); assert_eq!(中枢.级别.load(Ordering::Relaxed), 1); - assert_eq!(中枢.基础序列.read().unwrap().len(), 3); - assert!(中枢.第三买卖线.read().unwrap().is_none()); - assert!(中枢.本级_第三买卖线.read().unwrap().is_none()); + assert_eq!(中枢.基础序列.read().len(), 3); + assert!(中枢.第三买卖线.read().is_none()); + assert!(中枢.本级_第三买卖线.read().is_none()); } #[test] @@ -877,16 +864,16 @@ mod tests { // RefCell 第三买卖线读写 中枢.设置第三买卖线(Some(Arc::clone(&笔1))); - assert!(中枢.第三买卖线.read().unwrap().is_some()); + assert!(中枢.第三买卖线.read().is_some()); assert_eq!( - Arc::as_ptr(中枢.第三买卖线.read().unwrap().as_ref().unwrap()), + Arc::as_ptr(中枢.第三买卖线.read().as_ref().unwrap()), Arc::as_ptr(&笔1) ); // 本级_第三买卖线 - assert!(中枢.本级_第三买卖线.read().unwrap().is_none()); - *中枢.本级_第三买卖线.write().unwrap() = Some(Arc::clone(&笔3)); - assert!(中枢.本级_第三买卖线.read().unwrap().is_some()); + assert!(中枢.本级_第三买卖线.read().is_none()); + *中枢.本级_第三买卖线.write() = Some(Arc::clone(&笔3)); + assert!(中枢.本级_第三买卖线.read().is_some()); } // ============================================================ @@ -906,14 +893,11 @@ mod tests { 1, vec![Arc::clone(&笔1), Arc::clone(&笔2), Arc::clone(&笔3)], ); - assert_eq!(中枢.基础序列.read().unwrap().len(), 3); + assert_eq!(中枢.基础序列.read().len(), 3); 中枢._添加虚线(Arc::clone(&笔4)); - assert_eq!(中枢.基础序列.read().unwrap().len(), 4); - assert_eq!( - Arc::as_ptr(&中枢.基础序列.read().unwrap()[3]), - Arc::as_ptr(&笔4) - ); + assert_eq!(中枢.基础序列.read().len(), 4); + assert_eq!(Arc::as_ptr(&中枢.基础序列.read()[3]), Arc::as_ptr(&笔4)); } #[test] @@ -930,14 +914,14 @@ mod tests { vec![Arc::clone(&笔1), Arc::clone(&笔2), Arc::clone(&笔3)], ); 中枢.设置第三买卖线(Some(Arc::clone(&笔1))); - *中枢.本级_第三买卖线.write().unwrap() = Some(Arc::clone(&笔2)); - assert!(中枢.第三买卖线.read().unwrap().is_some()); - assert!(中枢.本级_第三买卖线.read().unwrap().is_some()); + *中枢.本级_第三买卖线.write() = Some(Arc::clone(&笔2)); + assert!(中枢.第三买卖线.read().is_some()); + assert!(中枢.本级_第三买卖线.read().is_some()); 中枢._添加虚线(Arc::clone(&笔4)); // 添加虚线后第三买卖线被清除 - assert!(中枢.第三买卖线.read().unwrap().is_none()); - assert!(中枢.本级_第三买卖线.read().unwrap().is_none()); + assert!(中枢.第三买卖线.read().is_none()); + assert!(中枢.本级_第三买卖线.read().is_none()); } // ============================================================ @@ -963,15 +947,15 @@ mod tests { // 基础序列中的 Rc 指针应一致 for i in 0..3 { assert_eq!( - Arc::as_ptr(&中枢.基础序列.read().unwrap()[i]), - Arc::as_ptr(&克隆.基础序列.read().unwrap()[i]) + Arc::as_ptr(&中枢.基础序列.read()[i]), + Arc::as_ptr(&克隆.基础序列.read()[i]) ); } // 第三买卖线 Rc 指针应一致 assert_eq!( - Arc::as_ptr(中枢.第三买卖线.read().unwrap().as_ref().unwrap()), - Arc::as_ptr(克隆.第三买卖线.read().unwrap().as_ref().unwrap()) + Arc::as_ptr(中枢.第三买卖线.read().as_ref().unwrap()), + Arc::as_ptr(克隆.第三买卖线.read().as_ref().unwrap()) ); } @@ -1030,12 +1014,12 @@ mod tests { // 通过 rc1 添加虚线 中枢1._添加虚线(Arc::clone(&笔4)); - assert_eq!(中枢2.基础序列.read().unwrap().len(), 4); + assert_eq!(中枢2.基础序列.read().len(), 4); // 验证共享的 Arc<虚线> 指针一致 assert_eq!( - Arc::as_ptr(&中枢1.基础序列.read().unwrap()[3]), - Arc::as_ptr(&中枢2.基础序列.read().unwrap()[3]) + Arc::as_ptr(&中枢1.基础序列.read()[3]), + Arc::as_ptr(&中枢2.基础序列.read()[3]) ); } } diff --git a/chanlun/src/algorithm/segment.rs b/chanlun/src/algorithm/segment.rs index b2e9bdc..d6c3366 100644 --- a/chanlun/src/algorithm/segment.rs +++ b/chanlun/src/algorithm/segment.rs @@ -32,9 +32,9 @@ use crate::structure::dash_line::虚线; use crate::structure::fractal_obj::分型; use crate::structure::segment_feat::线段特征; use crate::types::{分型结构, 相对方向, 缺口}; +use crate::warn; use std::sync::Arc; use std::sync::atomic::Ordering; -use tracing::warn; /// 线段 — 从笔生成线段的算法集合(静态方法命名空间) pub struct 线段; @@ -52,6 +52,17 @@ impl 线段 { // 基础操作 // ================================================================ + /// 从序号计算索引位置(O(1) — 与 Python _索引 对齐) + /// + /// 序列元素序号连续递增,因此可用 `项.序号 - 序列[0].序号` 直接得到位置。 + /// 当笔序列中元素被 pop 再 push 替换时(_弹出旧笔 → _添加新笔), + /// 新旧元素序号相同但 Arc 不同,此时 Arc::ptr_eq 会漏判,而序号算术不受影响。 + pub fn _索引(序列: &[Arc<虚线>], 项: &虚线) -> usize { + let 项序号 = 项.序号.load(Ordering::Relaxed); + let 首序号 = 序列[0].序号.load(Ordering::Relaxed); + (项序号 - 首序号) as usize + } + /// 解引用 Arc<虚线> → &虚线。内部 RwLock/Atomic 字段支持通过不可变引用修改。 fn 取段(段_rc: &Arc<虚线>) -> &虚线 { 段_rc @@ -60,70 +71,42 @@ impl 线段 { /// 向线段的基础序列中添加一笔 pub fn _添加虚线(段_rc: &Arc<虚线>, 筆: Arc<虚线>) { let 段 = Self::取段(段_rc); - if !段.基础序列.read().unwrap().is_empty() { - if !分型::判断分型( - &段.基础序列 - .read() - .unwrap() - .last() - .unwrap() - .武 - .read() - .unwrap(), - &筆.文, - "中", - ) { - panic!( - "线段.添加虚线 不连续 {} {}", - 段.基础序列.read().unwrap().last().unwrap(), - 筆 - ); + let 基础序列 = 段.基础序列.read(); + if !基础序列.is_empty() { + let 最后 = 基础序列.last().unwrap(); + if !分型::判断分型(&最后.武.read(), &筆.文, "中") { + panic!("线段.添加虚线 不连续 {} {}", 最后, 筆); } - if *段 - .基础序列 - .read() - .unwrap() - .last() - .unwrap() - .标识 - .read() - .unwrap() - != *筆.标识.read().unwrap() - { + if *最后.标识.read() != *筆.标识.read() { panic!( "线段.添加虚线 标识不符 {} {}", - *段.基础序列 - .read() - .unwrap() - .last() - .unwrap() - .标识 - .read() - .unwrap(), - 筆.标识.read().unwrap() + *最后.标识.read(), + 筆.标识.read() ); } } - 段.基础序列.write().unwrap().push(筆); + drop(基础序列); + 段.基础序列.write().push(筆); } /// 更新线段的终点分型 pub fn _武斗(段_rc: &Arc<虚线>, 武: &Arc<分型>, 行号: u32) { let 段 = Self::取段(段_rc); - if Arc::as_ptr(&*段.武.read().unwrap()) == Arc::as_ptr(武) { + if Arc::ptr_eq(&*段.武.read(), 武) { return; } - if 段.武.read().unwrap().分型特征值 == 武.分型特征值 - && 段.武.read().unwrap().时间戳() != 武.时间戳() + let 段_武 = 段.武.read(); + if 段_武.分型特征值 == 武.分型特征值 && 段_武.时间戳() != 武.时间戳() { warn!( "{}.武斗[{}], 发现特征值相等但时间戳不同, {}, {}", - 段.标识.read().unwrap(), + 段.标识.read(), 行号, - 段.武.read().unwrap(), + 段_武, 武 ); } + drop(段_武); if 段.文.结构 == 武.结构 { panic!("文武结构相同 {} {} {}", 行号, 段.文, 武); } @@ -143,19 +126,19 @@ impl 线段 { if 武.分型特征值 < 段.文.分型特征值 { panic!( "向上线段, 结束点小于起点 {} {} {}", - 段.标识.read().unwrap(), + 段.标识.read(), 段.文, 武 ); } - if 段.武.read().unwrap().分型特征值 > 武.分型特征值 - && 段.模式.read().unwrap().as_str() == "文武" + let 段_武 = 段.武.read(); + if 段_武.分型特征值 > 武.分型特征值 && 段.模式.read().as_str() == "文武" { warn!( "{}.武斗[{}] 出现回退 从 {} ==>>> {}", - 段.标识.read().unwrap(), + 段.标识.read(), 行号, - 段.武.read().unwrap(), + 段_武, 武 ); } @@ -163,42 +146,32 @@ impl 线段 { if 武.分型特征值 > 段.文.分型特征值 { panic!( "向下线段, 结束点大于起点 {} {} {}", - 段.标识.read().unwrap(), + 段.标识.read(), 段.文, 武 ); } - if 段.武.read().unwrap().分型特征值 < 武.分型特征值 - && 段.模式.read().unwrap().as_str() == "文武" + let 段_武 = 段.武.read(); + if 段_武.分型特征值 < 武.分型特征值 && 段.模式.read().as_str() == "文武" { warn!( "{}.武斗[{}] 出现回退 从 {} ==>>> {}", - 段.标识.read().unwrap(), + 段.标识.read(), 行号, - 段.武.read().unwrap(), + 段_武, 武 ); } } - *段.武.write().unwrap() = Arc::clone(武); + *段.武.write() = Arc::clone(武); } /// 武终 — 线段终结时设置终点 pub fn _武终(段_rc: &Arc<虚线>, 行号: u32) { let 武 = { let 段 = Self::取段(段_rc); - if 段.模式.read().unwrap().as_str() != "文武" { - Some(Arc::clone( - &*段 - .基础序列 - .read() - .unwrap() - .last() - .unwrap() - .武 - .read() - .unwrap(), - )) + if 段.模式.read().as_str() != "文武" { + Some(Arc::clone(&*段.基础序列.read().last().unwrap().武.read())) } else { None } @@ -211,9 +184,12 @@ impl 线段 { /// 验证序列 — 截断无效尾部 pub fn _验证序列(段_rc: &Arc<虚线>, 序列: &[Arc<虚线>]) { let 段 = Self::取段(段_rc); - let mut 基础序列: Vec> = Vec::new(); - for 元素 in 段.基础序列.read().unwrap().iter() { - if !序列.iter().any(|x| Arc::as_ptr(x) == Arc::as_ptr(元素)) { + let guard = 段.基础序列.read(); + let 首序号 = 序列[0].序号.load(Ordering::Relaxed); + let mut 基础序列: Vec> = Vec::with_capacity(guard.len()); + for 元素 in guard.iter() { + let idx = (元素.序号.load(Ordering::Relaxed) - 首序号) as usize; + if idx >= 序列.len() || !Arc::ptr_eq(&序列[idx], 元素) { break; } if !基础序列.is_empty() && !基础序列.last().unwrap().之后是(元素) { @@ -225,15 +201,19 @@ impl 线段 { if 基础序列.len().is_multiple_of(2) { 基础序列.pop(); } - *段.基础序列.write().unwrap() = 基础序列; + drop(guard); + *段.基础序列.write() = 基础序列; } /// 序列重置 — 截取到序列中的有效部分 pub fn 序列重置(段_rc: &Arc<虚线>, 序列: &[Arc<虚线>]) { let 段 = Self::取段(段_rc); - let mut 基础序列: Vec> = Vec::new(); - for 元素 in 段.基础序列.read().unwrap().iter() { - if !序列.iter().any(|x| Arc::as_ptr(x) == Arc::as_ptr(元素)) { + let guard = 段.基础序列.read(); + let 首序号 = 序列[0].序号.load(Ordering::Relaxed); + let mut 基础序列: Vec> = Vec::with_capacity(guard.len()); + for 元素 in guard.iter() { + let idx = (元素.序号.load(Ordering::Relaxed) - 首序号) as usize; + if idx >= 序列.len() || !Arc::ptr_eq(&序列[idx], 元素) { break; } if !基础序列.is_empty() && !基础序列.last().unwrap().之后是(元素) { @@ -241,8 +221,9 @@ impl 线段 { } 基础序列.push(Arc::clone(元素)); } - *段.基础序列.write().unwrap() = 基础序列; - 段.特征序列.write().unwrap()[2] = None; + drop(guard); + *段.基础序列.write() = 基础序列; + 段.特征序列.write()[2] = None; } // ================================================================ @@ -292,7 +273,7 @@ impl 线段 { /// 小阳: 向上线段 /// 少阴: 向下线段 pub fn 四象(段: &虚线) -> String { - if 段.前一缺口.read().unwrap().is_some() { + if 段.前一缺口.read().is_some() { if 段.方向() == 相对方向::向上 { "老阳".into() } else { @@ -307,10 +288,10 @@ impl 线段 { /// 获取缺口 — 从特征序列第一二元素之间检测缺口 pub fn 获取缺口(段: &虚线) -> Option<缺口> { - if 段.模式.read().unwrap().as_str() != "文武" { + if 段.模式.read().as_str() != "文武" { return None; } - let 特序 = 段.特征序列.read().unwrap(); + let 特序 = 段.特征序列.read(); let 左 = 特序[0].as_ref()?; let 中 = 特序[1].as_ref()?; let 相对关系 = 相对方向::分析(左.高(), 左.低(), 中.高(), 中.低()); @@ -325,12 +306,8 @@ impl 线段 { /// 特征分型终结 — 检查特征序列是否形成正常分型终结 pub fn 特征分型终结(段: &虚线) -> bool { - let 特征序列 = 线段特征::静态分析( - &段.基础序列.read().unwrap(), - 段.方向(), - &Self::四象(段), - false, - ); + let 特征序列 = + 线段特征::静态分析(&段.基础序列.read(), 段.方向(), &Self::四象(段), false); if 特征序列.len() >= 3 { let idx = 特征序列.len(); if let Some(结构) = 分型结构::分析( @@ -352,7 +329,7 @@ impl 线段 { /// 特征序列状态 — 返回三个特征序列元素是否为 Some pub fn 特征序列状态(段: &虚线) -> (bool, bool, bool) { - let 特序 = 段.特征序列.read().unwrap(); + let 特序 = 段.特征序列.read(); (特序[0].is_some(), 特序[1].is_some(), 特序[2].is_some()) } @@ -361,7 +338,7 @@ impl 线段 { 段_rc: &Arc<虚线>, 序列: Vec>>, 行号: u32 ) { let 段 = Self::取段(段_rc); - if 段.模式.read().unwrap().as_str() != "文武" { + if 段.模式.read().as_str() != "文武" { return; } @@ -374,29 +351,25 @@ impl 线段 { let 左 = 序列[0].clone(); let 中 = 序列[1].clone(); let 右 = 序列[2].clone(); - *段.特征序列.write().unwrap() = vec![左, 中, 右]; + *段.特征序列.write() = vec![左, 中, 右]; - if let Some(ref 右特征) = 段.特征序列.read().unwrap()[2] { - let mut 基础序列: Vec> = Vec::new(); + if let Some(ref 右特征) = 段.特征序列.read()[2] { + let guard = 段.基础序列.read(); + let mut 基础序列: Vec> = Vec::with_capacity(guard.len()); let 右尾 = 右特征.基础序列.last().expect("特征序列元素不应为空"); - if !段 - .基础序列 - .read() - .unwrap() - .iter() - .any(|x| Arc::as_ptr(x) == Arc::as_ptr(右尾)) - { + if !guard.iter().any(|x| Arc::ptr_eq(x, 右尾)) { panic!("右特征最后一个元素不在基础序列中"); } - for 元素 in 段.基础序列.read().unwrap().iter() { + for 元素 in guard.iter() { 基础序列.push(Arc::clone(元素)); - if Arc::as_ptr(元素) == Arc::as_ptr(右尾) { + if Arc::ptr_eq(元素, 右尾) { break; } } + drop(guard); if 基础序列.len() >= 6 && 基础序列.len().is_multiple_of(2) { - *段.基础序列.write().unwrap() = 基础序列; + *段.基础序列.write() = 基础序列; } else { panic!("设置特征序列: 基础序列长度不足或非偶数"); } @@ -408,18 +381,23 @@ impl 线段 { // Compute new feature sequence, then delegate to 设置特征序列 for truncation let 序列: Vec>> = { let 段 = &**段_rc; - if 段.模式.read().unwrap().as_str() != "文武" { + if 段.模式.read().as_str() != "文武" { return; } - let mut 基础序列 = 段.基础序列.read().unwrap().clone(); - if let Some(ref 前结束) = *段.前一结束位置.read().unwrap() - && let Some(idx) = 基础序列 - .iter() - .position(|x| Arc::as_ptr(x) == Arc::as_ptr(前结束)) - && idx > 0 - { - 基础序列 = 基础序列[idx - 1..].to_vec(); - } + // 仅在需要时克隆(避免全量克隆后立即截断的双重分配) + let 基础序列 = { + let 完整 = 段.基础序列.read(); + if let Some(ref 前结束) = *段.前一结束位置.read() { + let idx = 线段::_索引(&完整, 前结束); + if idx > 0 && idx < 完整.len() { + 完整[idx - 1..].to_vec() + } else { + 完整.clone() + } + } else { + 完整.clone() + } + }; let 四象 = Self::四象(段); let 特征序列 = 线段特征::静态分析( @@ -459,13 +437,13 @@ impl 线段 { /// 查找贯穿伤 — 基础序列中穿透文分型特征值的笔 pub fn 查找贯穿伤(段: &虚线) -> Option> { - for 贯穿伤 in 段.基础序列.read().unwrap().iter().skip(3) { + for 贯穿伤 in 段.基础序列.read().iter().skip(3) { if 段.方向().是否向上() { - if 贯穿伤.武.read().unwrap().分型特征值 < 段.文.分型特征值 { + if 贯穿伤.武.read().分型特征值 < 段.文.分型特征值 { return Some(Arc::clone(贯穿伤)); } } else { - if 贯穿伤.武.read().unwrap().分型特征值 > 段.文.分型特征值 { + if 贯穿伤.武.read().分型特征值 > 段.文.分型特征值 { return Some(Arc::clone(贯穿伤)); } } @@ -479,36 +457,29 @@ impl 线段 { /// 分割序列 — 将线段的基础序列分为前、后、第三买卖线、贯穿伤 pub fn 分割序列(段: &虚线, 所属中枢: Option<&中枢>) -> 分割结果 { - if 段.模式.read().unwrap().as_str() != "文武" { - return ( - 段.基础序列.read().unwrap().clone(), - Vec::new(), - Vec::new(), - None, - ); + if 段.模式.read().as_str() != "文武" { + return (段.基础序列.read().clone(), Vec::new(), Vec::new(), None); } - let mut 前: Vec> = Vec::new(); - let mut 后: Vec> = Vec::new(); + let len = 段.基础序列.read().len(); + let mut 前 = Vec::with_capacity(len); + let mut 后 = Vec::with_capacity(len / 2); let mut 第三买卖线: Vec> = Vec::new(); let mut 贯穿伤: Option> = None; - for 筆 in 段.基础序列.read().unwrap().iter() { + for 筆 in 段.基础序列.read().iter() { if 前.is_empty() { 前.push(Arc::clone(筆)); continue; } - if Arc::as_ptr(&*前.last().unwrap().武.read().unwrap()) - != Arc::as_ptr(&*段.武.read().unwrap()) - && 后.is_empty() - { + if !Arc::ptr_eq(&*前.last().unwrap().武.read(), &*段.武.read()) && 后.is_empty() { 前.push(Arc::clone(筆)); } if !后.is_empty() { 后.push(Arc::clone(筆)); } - if Arc::as_ptr(&筆.文) == Arc::as_ptr(&*段.武.read().unwrap()) { + if Arc::ptr_eq(&筆.文, &*段.武.read()) { 后.push(Arc::clone(筆)); } } @@ -516,11 +487,11 @@ impl 线段 { let mut 状态 = None; if let Some(中枢) = 所属中枢 { - *中枢.本级_第三买卖线.write().unwrap() = None; + *中枢.本级_第三买卖线.write() = None; let 尾部 = if let Some(后笔) = 后.last() { - 后笔.武.read().unwrap().clone() + 后笔.武.read().clone() } else { - 段.武.read().unwrap().clone() + 段.武.read().clone() }; if 中枢.高() >= 尾部.分型特征值 && 尾部.分型特征值 >= 中枢.低() { @@ -535,7 +506,7 @@ impl 线段 { if 状态 == Some("中枢之上") { let 中枢高 = 所属中枢.as_ref().unwrap().高(); let 中枢低 = 所属中枢.as_ref().unwrap().低(); - for 筆 in 段.基础序列.read().unwrap().iter().rev() { + for 筆 in 段.基础序列.read().iter().rev() { if 筆.方向() == 相对方向::向下 { let 关系 = 相对方向::分析(中枢高, 中枢低, 筆.高(), 筆.低()); if 关系 == 相对方向::向上缺口 { @@ -548,7 +519,7 @@ impl 线段 { } if 状态 == Some("中枢之下") { - for 筆 in 段.基础序列.read().unwrap().iter().rev() { + for 筆 in 段.基础序列.read().iter().rev() { if 筆.方向() == 相对方向::向上 { let 关系 = 相对方向::分析( 所属中枢.as_ref().unwrap().高(), @@ -568,17 +539,17 @@ impl 线段 { if !第三买卖线.is_empty() { 第三买卖线.reverse(); if let Some(中枢) = 所属中枢 { - *中枢.本级_第三买卖线.write().unwrap() = Some(Arc::clone(&第三买卖线[0])); + *中枢.本级_第三买卖线.write() = Some(Arc::clone(&第三买卖线[0])); } } if !后.is_empty() { if 段.方向().是否向上() { - if 后[0].武.read().unwrap().分型特征值 < 段.文.分型特征值 { + if 后[0].武.read().分型特征值 < 段.文.分型特征值 { 贯穿伤 = Some(Arc::clone(&后[0])); } } else { - if 后[0].武.read().unwrap().分型特征值 > 段.文.分型特征值 { + if 后[0].武.read().分型特征值 > 段.文.分型特征值 { 贯穿伤 = Some(Arc::clone(&后[0])); } } @@ -590,10 +561,10 @@ impl 线段 { /// 刷新 — 完整刷新线段的特征序列和内部中枢 pub fn _刷新(段_rc: &Arc<虚线>, 配置: &缠论配置) { let 段 = Self::取段(段_rc); - if 段.模式.read().unwrap().as_str() != "文武" { + if 段.模式.read().as_str() != "文武" { return; } - if 段.基础序列.read().unwrap().is_empty() { + if 段.基础序列.read().is_empty() { warn!(" 线段.刷新 基础序列为空"); return; } @@ -603,7 +574,7 @@ impl 线段 { // After 刷新特征序列, work with the updated segment let (武斗_武文, 特征后一笔_opt) = { let 段2 = Self::取段(段_rc); - let 特序_ref = 段2.特征序列.read().unwrap(); + let 特序_ref = 段2.特征序列.read(); let 有效特征序列: Vec<&Arc<线段特征>> = 特序_ref.iter().filter_map(|x| x.as_ref()).collect(); @@ -612,19 +583,17 @@ impl 线段 { } else if !有效特征序列.is_empty() { let 最近特征 = 有效特征序列[有效特征序列.len() - 1]; - let 特征后一笔 = if 最近特征.基础序列.last().map(|x| { - 段2.基础序列 - .read() - .unwrap() - .iter() - .any(|b| Arc::as_ptr(b) == Arc::as_ptr(x)) - }) == Some(true) + let 特征后一笔 = if 最近特征 + .基础序列 + .last() + .map(|x| 段2.基础序列.read().iter().any(|b| Arc::ptr_eq(b, x))) + == Some(true) { Some(Arc::clone(最近特征.基础序列.last().unwrap())) } else { 笔::以武会友( - &段2.基础序列.read().unwrap(), - &最近特征.基础序列.last().unwrap().武.read().unwrap(), + &段2.基础序列.read(), + &最近特征.基础序列.last().unwrap().武.read(), ) }; @@ -646,19 +615,16 @@ impl 线段 { } else if let Some(特征后一笔) = 特征后一笔_opt { let 武斗候选 = { let 段2 = Self::取段(段_rc); - if let Some(序号) = 段2 - .基础序列 - .read() - .unwrap() - .iter() - .position(|x| Arc::as_ptr(x) == Arc::as_ptr(&特征后一笔)) - { - if 序号 < 段2.基础序列.read().unwrap().len() - 1 { - let 下一笔 = Arc::clone(&段2.基础序列.read().unwrap()[序号 + 1]); + let 基础序列_ref = 段2.基础序列.read(); + // Python: 序号 = cls._索引(段.基础序列, 特征后一笔) + let 序号 = 线段::_索引(&基础序列_ref, &特征后一笔); + if 序号 < 基础序列_ref.len() { + if 序号 < 段2.基础序列.read().len() - 1 { + let 下一笔 = Arc::clone(&段2.基础序列.read()[序号 + 1]); if (段2.方向() == 相对方向::向上 && 段2.高() <= 下一笔.高()) || (段2.方向() == 相对方向::向下 && 段2.低() >= 下一笔.低()) { - Some(下一笔.武.read().unwrap().clone()) + Some(下一笔.武.read().clone()) } else { None } @@ -680,23 +646,15 @@ impl 线段 { /// 获取内部中枢序列 — 内部实现 fn 获取内部中枢序列_内部(段: &虚线, _配置: &缠论配置) -> 中枢序列组 { - if 段.模式.read().unwrap().as_str() != "文武" { + if 段.模式.read().as_str() != "文武" { 中枢::分析( - &段.基础序列.read().unwrap(), - &mut 段.合_中枢序列.write().unwrap(), + &段.基础序列.read(), + &mut 段.合_中枢序列.write(), true, - &format!( - "{}_{}_合_", - 段.标识.read().unwrap(), - 段.序号.load(Ordering::Relaxed) - ), + &format!("{}_{}_合_", 段.标识.read(), 段.序号.load(Ordering::Relaxed)), 0, ); - return ( - Vec::new(), - Vec::new(), - 段.合_中枢序列.read().unwrap().clone(), - ); + return (Vec::new(), Vec::new(), 段.合_中枢序列.read().clone()); } // Use 分割序列 to get前/后 @@ -704,42 +662,30 @@ impl 线段 { 中枢::分析( &前, - &mut 段.实_中枢序列.write().unwrap(), + &mut 段.实_中枢序列.write(), true, - &format!( - "{}_{}_实_", - 段.标识.read().unwrap(), - 段.序号.load(Ordering::Relaxed) - ), + &format!("{}_{}_实_", 段.标识.read(), 段.序号.load(Ordering::Relaxed)), 0, ); 中枢::分析( &后, - &mut 段.虚_中枢序列.write().unwrap(), + &mut 段.虚_中枢序列.write(), true, - &format!( - "{}_{}_虚_", - 段.标识.read().unwrap(), - 段.序号.load(Ordering::Relaxed) - ), + &format!("{}_{}_虚_", 段.标识.read(), 段.序号.load(Ordering::Relaxed)), 0, ); 中枢::分析( - &段.基础序列.read().unwrap(), - &mut 段.合_中枢序列.write().unwrap(), + &段.基础序列.read(), + &mut 段.合_中枢序列.write(), true, - &format!( - "{}_{}_合_", - 段.标识.read().unwrap(), - 段.序号.load(Ordering::Relaxed) - ), + &format!("{}_{}_合_", 段.标识.read(), 段.序号.load(Ordering::Relaxed)), 0, ); ( - 段.虚_中枢序列.read().unwrap().clone(), - 段.实_中枢序列.read().unwrap().clone(), - 段.合_中枢序列.read().unwrap().clone(), + 段.虚_中枢序列.read().clone(), + 段.实_中枢序列.read().clone(), + 段.合_中枢序列.read().clone(), ) } @@ -760,20 +706,22 @@ impl 线段 { 线段序列: &mut Vec>, mut 待添加线段: Arc<虚线>, _配置: &缠论配置, - 行号: String, + 行号: u32, + 层级: i64, ) { { let seg = Arc::make_mut(&mut 待添加线段); - *seg.模式.write().unwrap() = "文武".into(); + *seg.模式.write() = "文武".into(); if !线段序列.is_empty() { if let Some(前一个) = 线段序列.last() && !前一个.之后是(seg) { panic!( - "线段.向序列中添加 不连续[{}] {} {}", + "线段.向序列中添加 不连续[{}, {}] {} {}", 行号, - 前一个.武.read().unwrap(), + 层级, + 前一个.武.read(), seg.文 ); } @@ -781,37 +729,39 @@ impl 线段 { let 之前线段 = 线段序列.last().unwrap(); assert!( - 之前线段.特征序列.read().unwrap()[2].is_some() + 之前线段.特征序列.read()[2].is_some() || 之前线段.短路修正.load(Ordering::Relaxed), - "线段._向序列中添加[{}], 之前线段.右 = None {}", + "线段._向序列中添加[{}, {}], 之前线段.右 = None {}", 行号, + 层级, 之前线段 ); - if !seg.基础序列.read().unwrap().iter().any(|x| { - Arc::as_ptr(x) == Arc::as_ptr(之前线段.基础序列.read().unwrap().last().unwrap()) - }) && !之前线段.短路修正.load(Ordering::Relaxed) + if !seg + .基础序列 + .read() + .iter() + .any(|x| Arc::ptr_eq(x, 之前线段.基础序列.read().last().unwrap())) + && !之前线段.短路修正.load(Ordering::Relaxed) { panic!( - "线段._向序列中添加[{}], 之前线段[-1] not in 待添加虚线! {}", - 行号, 之前线段 + "线段._向序列中添加[{}, {}], 之前线段[-1] not in 待添加虚线! {}", + 行号, 层级, 之前线段 ); } seg.序号 .store(之前线段.序号.load(Ordering::Relaxed) + 1, Ordering::Relaxed); - *seg.前一缺口.write().unwrap() = if 之前线段.短路修正.load(Ordering::Relaxed) - { + *seg.前一缺口.write() = if 之前线段.短路修正.load(Ordering::Relaxed) { None } else { Self::获取缺口(之前线段) }; - *seg.前一结束位置.write().unwrap() = Some(Arc::clone( - 之前线段.基础序列.read().unwrap().last().unwrap(), - )); + *seg.前一结束位置.write() = + Some(Arc::clone(之前线段.基础序列.read().last().unwrap())); if ["老阴", "老阳"].contains(&Self::四象(之前线段).as_str()) { - *seg.前一缺口.write().unwrap() = None; + *seg.前一缺口.write() = None; } } } @@ -823,35 +773,37 @@ impl 线段 { 线段序列: &mut Vec>, 待弹出线段: &Arc<虚线>, _配置: &缠论配置, - 行号: String, + 行号: u32, + 层级: i64, ) -> Option> { if 线段序列.is_empty() { return None; } - if Arc::as_ptr(线段序列.last().unwrap()) != Arc::as_ptr(待弹出线段) { + if !Arc::ptr_eq(线段序列.last().unwrap(), 待弹出线段) { panic!("线段._从序列中删除 弹出数据不在列表中 {}", 待弹出线段); } { - let 左 = &待弹出线段.特征序列.read().unwrap()[0]; - let 中 = &待弹出线段.特征序列.read().unwrap()[1]; - let 右 = &待弹出线段.特征序列.read().unwrap()[2]; + let 特序 = 待弹出线段.特征序列.read(); + let 左 = &特序[0]; + let 中 = &特序[1]; + let 右 = &特序[2]; if let (Some(l), Some(m), Some(r)) = (左, 中, 右) && let Some(结构) = 分型结构::分析(&**l, &**m, &**r, true, true) && matches!(结构, 分型结构::顶 | 分型结构::底) && !相对方向::分析(l.高(), l.低(), m.高(), m.低()).是否缺口() { warn!( - "[警告<{}>]: 线段._从序列中删除 发现分型完毕, 且特征序列无缺口 {}", - 行号, 待弹出线段 + "[警告<{}, {}>]: 线段._从序列中删除 发现分型完毕, 且特征序列无缺口 {}", + 行号, 层级, 待弹出线段 ); } } let 弹出 = 线段序列.pop().unwrap(); 弹出.有效性.store(false, Ordering::Relaxed); - *弹出.前一结束位置.write().unwrap() = None; + *弹出.前一结束位置.write() = None; Some(弹出) } @@ -865,13 +817,10 @@ impl 线段 { 线段序列: &mut Vec>, 配置: &缠论配置, 层级: i64 ) -> bool { let 当前线段 = Arc::clone(线段序列.last().unwrap()); - assert!( - !当前线段.基础序列.read().unwrap().is_empty(), - "缺口突破: 当前线段.基础序列为空!" - ); - let 当前虚线 = Arc::clone( - &当前线段.基础序列.read().unwrap()[当前线段.基础序列.read().unwrap().len() - 1], - ); + let 基础序列 = 当前线段.基础序列.read(); + assert!(!基础序列.is_empty(), "缺口突破: 当前线段.基础序列为空!"); + let 当前虚线 = Arc::clone(&基础序列[基础序列.len() - 1]); + drop(基础序列); let 四象 = Self::四象(&当前线段); let 同向 = 当前虚线.方向() == 当前线段.方向(); @@ -886,7 +835,7 @@ impl 线段 { } // 条件3:当前线段特征序列[2]必须为None - if 当前线段.特征序列.read().unwrap()[2].is_some() { + if 当前线段.特征序列.read()[2].is_some() { return false; } @@ -906,18 +855,19 @@ impl 线段 { } // 执行修正 - let 序列 = 当前线段.基础序列.read().unwrap().clone(); + let 序列 = 当前线段.基础序列.read().clone(); Self::_弹出线段( 线段序列, &Arc::clone(线段序列.last().unwrap()), 配置, - format!("{}, {}", line!(), 层级), + line!(), + 层级, ); assert!(!线段序列.is_empty(), "缺口突破: 线段序列为第二次空!"); let 当前线段 = Arc::clone(线段序列.last().unwrap()); - assert!(当前线段.特征序列.read().unwrap()[2].is_some()); + assert!(当前线段.特征序列.read()[2].is_some()); let (前, _, _, _) = Self::分割序列(&当前线段, None); let mut 当前线段基础序列 = 前; let 序列首 = Arc::clone(&序列[0]); @@ -928,7 +878,7 @@ impl 线段 { 当前线段基础序列.extend(序列); let idx = 线段序列.len() - 1; - *线段序列[idx].基础序列.write().unwrap() = 当前线段基础序列.clone(); + *线段序列[idx].基础序列.write() = 当前线段基础序列.clone(); Self::_刷新(&线段序列[idx], 配置); true } @@ -945,7 +895,7 @@ impl 线段 { // 外层条件 if !(配置.线段_非缺口下穿刺 && (四象 == "小阳" || 四象 == "少阴") - && 当前线段.特征序列.read().unwrap()[2].is_none()) + && 当前线段.特征序列.read()[2].is_none()) { return false; } @@ -958,16 +908,12 @@ impl 线段 { let 贯穿伤 = 贯穿伤.unwrap(); // 切割基础序列 - let 贯穿伤_idx = 当前线段 - .基础序列 - .read() - .unwrap() - .iter() - .position(|x| Arc::as_ptr(x) == Arc::as_ptr(&贯穿伤)); - - assert!(贯穿伤_idx.is_some(), "非缺口下穿刺: 贯穿伤不在基础序列中!"); - let 基础序列: Vec> = - 当前线段.基础序列.read().unwrap()[贯穿伤_idx.unwrap()..].to_vec(); + // Python: 基础序列 = 当前线段.基础序列[cls._索引(当前线段.基础序列, 贯穿伤) :] + let 贯穿伤_idx = { + let guard = 当前线段.基础序列.read(); + 线段::_索引(&guard, &贯穿伤) + }; + let 基础序列: Vec> = 当前线段.基础序列.read()[贯穿伤_idx..].to_vec(); // 长度条件 if !(基础序列.len() == 4 && 线段序列.len() >= 2) { @@ -986,82 +932,60 @@ impl 线段 { // 执行修正 warn!( - "[警告<{}, {}>]: 线段.修复贯穿伤 {} {:?}", + "[警告<{}, {}>]: {}.修复贯穿伤, 序号:{} {} [{}]", line!(), 层级, + 当前线段.标识.read(), + 当前线段.序号.load(Ordering::Relaxed), 贯穿伤, 基础序列 + .iter() + .map(|s| format!("{}", s)) + .collect::>() + .join(", ") ); // Reassign to full copy (matching Python pattern) - let 基础序列 = 当前线段.基础序列.read().unwrap().clone(); + let 基础序列 = 当前线段.基础序列.read().clone(); Self::_弹出线段( 线段序列, &Arc::clone(线段序列.last().unwrap()), 配置, - format!("{}, {}", line!(), 层级), + line!(), + 层级, ); assert!(!线段序列.is_empty(), "非缺口下穿刺: 第二次线段序列为空!"); - assert!( - 基础序列.iter().any(|x| { - Arc::as_ptr(x) - == Arc::as_ptr( - 线段序列 - .last() - .unwrap() - .基础序列 - .read() - .unwrap() - .last() - .unwrap(), - ) - }), - "非缺口下穿刺: 当前线段.基础序列[-1] 不在 基础序列中!" - ); - let 开始序号_opt; - let 待添加元素: Vec>; - { - let idx = 线段序列.len() - 1; - let cur = Arc::make_mut(&mut 线段序列[idx]); - cur.特征序列.write().unwrap()[2] = None; - - let 开始笔 = Arc::clone(cur.基础序列.read().unwrap().last().unwrap()); - let 开始序号 = 基础序列 - .iter() - .position(|x| Arc::as_ptr(x) == Arc::as_ptr(&开始笔)); - - 开始序号_opt = 开始序号; - if let Some(序号) = 开始序号 { - 待添加元素 = 基础序列[序号 + 1..].to_vec(); - } else { - 待添加元素 = Vec::new(); - } - } - - if 开始序号_opt.is_some() { - let idx = 线段序列.len() - 1; - let seg_rc = Arc::clone(&线段序列[idx]); - for 临时虚线 in &待添加元素 { - Self::_添加虚线(&seg_rc, Arc::clone(临时虚线)); - } - 线段序列[idx] = seg_rc; - } let idx = 线段序列.len() - 1; + 线段序列[idx].特征序列.write()[2] = None; + + // Python: if 当前线段.基础序列[-1] not in 基础序列: 序号 = 0 + // else: 序号 = cls._索引(基础序列, 当前线段.基础序列[-1]) + 1 + let 序号 = { + let 开始笔 = Arc::clone(线段序列[idx].基础序列.read().last().unwrap()); + if !基础序列.iter().any(|x| Arc::ptr_eq(x, &开始笔)) { + crate::error!("非缺口下穿刺: 当前线段.基础序列[-1] 不在 基础序列中!"); + 0 + } else { + 线段::_索引(&基础序列, &开始笔) + 1 + } + }; + + for 筆 in 基础序列.iter().skip(序号) { + Self::_添加虚线(&线段序列[idx], Arc::clone(筆)); + } Self::_刷新(&线段序列[idx], 配置); - let 当前线段 = Arc::clone(&线段序列[idx]); - 当前线段.短路修正.store(true, Ordering::Relaxed); - if 当前线段.特征序列.read().unwrap()[2].is_some() { + 线段序列[idx].短路修正.store(true, Ordering::Relaxed); + if 线段序列[idx].特征序列.read()[2].is_some() { let 段 = 虚线::创建线段(&[Arc::clone(&左), Arc::clone(&中), Arc::clone(&右)]); let 段_rc = Arc::new(段); - Self::_添加线段(线段序列, 段_rc, 配置, format!("{}, {}", line!(), 层级)); + Self::_添加线段(线段序列, 段_rc, 配置, line!(), 层级); // Set feature sequence [0] let 新段 = Self::取段(线段序列.last_mut().unwrap()); - 新段.特征序列.write().unwrap()[0] = - Some(Arc::new(线段特征::新建(vec![中], 新段.方向()))); + 新段.特征序列.write()[0] = Some(Arc::new(线段特征::新建(vec![中], 新段.方向()))); } true @@ -1082,7 +1006,7 @@ impl 线段 { if !(配置.线段_缺口后紧急修正 && !配置.线段_特征序列忽视老阴老阳 && (四象 == "小阳" || 四象 == "少阴") - && 当前线段.特征序列.read().unwrap()[2].is_none()) + && 当前线段.特征序列.read()[2].is_none()) { return false; } @@ -1134,7 +1058,7 @@ impl 线段 { let 新段 = 虚线::创建线段(&基础序列); let 新段_rc = Arc::new(新段); - Self::_添加线段(线段序列, 新段_rc, 配置, format!("{}, {}", line!(), 层级)); + Self::_添加线段(线段序列, 新段_rc, 配置, line!(), 层级); true } @@ -1147,7 +1071,7 @@ impl 线段 { let 当前线段 = Arc::clone(线段序列.last().unwrap()); // 条件1 - if !(配置.线段_修正 && 当前线段.基础序列.read().unwrap().len() >= 9) { + if !(配置.线段_修正 && 当前线段.基础序列.read().len() >= 9) { return false; } @@ -1179,20 +1103,17 @@ impl 线段 { let 新段1_rc = Arc::new(新段1); // Set 短路修正 新段1_rc.短路修正.store(true, Ordering::Relaxed); - Self::_添加线段(线段序列, 新段1_rc, 配置, format!("{}, {}", line!(), 层级)); + Self::_添加线段(线段序列, 新段1_rc, 配置, line!(), 层级); if ["老阴", "老阳"].contains(&Self::四象(&当前线段).as_str()) { - *Self::取段(线段序列.last_mut().unwrap()) - .前一缺口 - .write() - .unwrap() = None; + *Self::取段(线段序列.last_mut().unwrap()).前一缺口.write() = None; } // 第二个新段 let start = 之后基础序列.len() - 3; let 新段2 = 虚线::创建线段(&之后基础序列[start..]); let 新段2_rc = Arc::new(新段2); - Self::_添加线段(线段序列, 新段2_rc, 配置, format!("{}, {}", line!(), 层级)); + Self::_添加线段(线段序列, 新段2_rc, 配置, line!(), 层级); true } @@ -1204,6 +1125,7 @@ impl 线段 { /// 分析 — 从笔序列生成线段序列 /// /// 使用显式栈(loop + continue)模拟 Python 的递归调用,避免栈溢出。 + /// 线段划分核心递归算法 — 从笔序列递归生成线段(与 Python 线段.分析 对齐) pub fn 分析( 笔序列: &[Arc<虚线>], 线段序列: &mut Vec>, @@ -1211,246 +1133,202 @@ impl 线段 { 层级: i64, 关系序列: &[相对方向], ) { - let mut 当前层级 = 层级; + // 递归深度守卫(Python: if 层级 > 256: return) + if 层级 > 256 { + warn!("线段.分析 递归深度超出 256"); + return; + } - loop { - if 当前层级 > 256 { - warn!("线段.分析 递归深度超出 256"); - return; + // Python: try: 笔序列[2] except IndexError: return + if 笔序列.len() < 3 { + return; + } + + // -------------------- 1. 初始化第一个线段 -------------------- + // Python: if not 线段序列: for i in range(1, len-1): ... + if 线段序列.is_empty() { + for i in 1..笔序列.len() - 1 { + let 左 = &笔序列[i - 1]; + let 中 = &笔序列[i]; + let 右 = &笔序列[i + 1]; + + if !Self::_基础判断(左, 中, 右, 关系序列) { + continue; + } + let 段 = 虚线::创建线段(&[Arc::clone(左), Arc::clone(中), Arc::clone(右)]); + let 段_rc = Arc::new(段); + Self::_添加线段(线段序列, 段_rc, 配置, line!(), 层级); + // Python: 段.特征序列[0] = 线段特征.新建([中], 段.方向) + 线段序列.last().unwrap().特征序列.write()[0] = Some(Arc::new(线段特征::新建( + vec![Arc::clone(中)], + 线段序列.last().unwrap().方向(), + ))); + break; } - - if 笔序列.len() < 3 { - return; - } - - // ---- 1. 初始化第一个线段 ---- + // Python: if not 线段序列: return if 线段序列.is_empty() { - for i in 1..笔序列.len() - 1 { - let 左 = &笔序列[i - 1]; - let 中 = &笔序列[i]; - let 右 = &笔序列[i + 1]; - - if !Self::_基础判断(左, 中, 右, 关系序列) { - continue; - } - let 段 = - 虚线::创建线段(&[Arc::clone(左), Arc::clone(中), Arc::clone(右)]); - let 段_rc = Arc::new(段); - Self::_添加线段( - 线段序列, - 段_rc, - 配置, - format!("{}, {}", line!(), 当前层级), - ); - - // 段.特征序列.read().unwrap()[0] = 线段特征.新建([中], 段.方向) - let 段 = Self::取段(线段序列.last_mut().unwrap()); - 段.特征序列.write().unwrap()[0] = Some(Arc::new(线段特征::新建( - vec![Arc::clone(中)], - 段.方向(), - ))); - break; - } - if 线段序列.is_empty() { - return; - } + return; } + } - // ---- 2. 清理无效的尾部引用 ---- - while !线段序列.is_empty() - && 线段序列 + // -------------------- 2. 清理无效的尾部引用 -------------------- + // Python: while 线段序列 and 线段序列[-1].前一结束位置: ... + while !线段序列.is_empty() && 线段序列.last().unwrap().前一结束位置.read().is_some() + { + let 前一结束 = Arc::clone( + 线段序列 .last() .unwrap() .前一结束位置 .read() - .unwrap() - .is_some() - { - let 前一结束 = Arc::clone( - 线段序列 - .last() - .unwrap() - .前一结束位置 - .read() - .unwrap() - .as_ref() - .unwrap(), - ); - if !笔序列 - .iter() - .any(|x| Arc::as_ptr(x) == Arc::as_ptr(&前一结束)) - { - let 当前 = Arc::clone(线段序列.last().unwrap()); - Self::_弹出线段( - 线段序列, - &当前, - 配置, - format!("{}, {}", line!(), 当前层级), - ); - } else { - break; - } - } - - if 线段序列.is_empty() { - 当前层级 += 1; - continue; - } - - // ---- 3. 确保当前线段有效 ---- - let 当前线段_rc = Arc::clone(线段序列.last().unwrap()); - Self::序列重置(&当前线段_rc, 笔序列); - let seg_idx = 线段序列.len() - 1; - 线段序列[seg_idx] = 当前线段_rc; - - if 线段序列.last().unwrap().基础序列.read().unwrap().len() < 3 { + .as_ref() + .unwrap(), + ); + // Python: if 前一结束位置 not in 笔序列: _弹出线段(...) + let 首序号 = 笔序列[0].序号.load(Ordering::Relaxed); + let idx = (前一结束.序号.load(Ordering::Relaxed) - 首序号) as usize; + if idx >= 笔序列.len() || !Arc::ptr_eq(&笔序列[idx], &前一结束) { let 当前 = Arc::clone(线段序列.last().unwrap()); - Self::_弹出线段(线段序列, &当前, 配置, format!("{}, {}", line!(), 当前层级)); - if 线段序列.is_empty() { - 当前层级 += 1; - continue; + Self::_弹出线段(线段序列, &当前, 配置, line!(), 层级); + } else { + break; + } + } + + // Python: if not 线段序列: return 线段递归分析(笔序列, 线段序列, 配置, 层级 + 1, 关系序列) + if 线段序列.is_empty() { + return Self::分析(笔序列, 线段序列, 配置, 层级 + 1, 关系序列); + } + + // -------------------- 3. 确保当前线段有效 -------------------- + // Python: 当前线段 = 线段序列[-1]; _序列重置(当前线段, 笔序列) + Self::序列重置(线段序列.last().unwrap(), 笔序列); + + // Python: if len(当前线段.基础序列) < 3: _弹出线段(...) + if 线段序列.last().unwrap().基础序列.read().len() < 3 { + let 当前 = Arc::clone(线段序列.last().unwrap()); + Self::_弹出线段(线段序列, &当前, 配置, line!(), 层级); + // Python: if not 线段序列: return 线段递归分析(...) + if 线段序列.is_empty() { + return Self::分析(笔序列, 线段序列, 配置, 层级 + 1, 关系序列); + } + } + + // -------------------- 4. 特征序列已完整时的处理 -------------------- + // Python: 当前线段 = 线段序列[-1] + // Python: if 当前线段.特征序列[2] is not None: ... + { + let 当前线段 = Arc::clone(线段序列.last().unwrap()); + if 当前线段.特征序列.read()[2].is_some() { + // Python: 基础序列 = 线段.分割序列(当前线段)[1] + let (_, 基础序列, _, _) = Self::分割序列(&当前线段, None); + let 四象 = Self::四象(&当前线段); + // Python: 新段 = 虚线.创建线段(基础序列) + let 新段 = Arc::new(虚线::创建线段(&基础序列)); + // Python: 线段._添加线段(线段序列, 新段, 配置, ...) + Self::_添加线段(线段序列, 新段, 配置, line!(), 层级); + // Python: if 线段.四象(当前线段) in ("老阴", "老阳"): 新段.前一缺口 = None + if ["老阴", "老阳"].contains(&四象.as_str()) { + *线段序列.last().unwrap().前一缺口.write() = None; } } + } - // ---- 4. 特征序列已完整时的处理 ---- - { - let 当前线段 = Arc::clone(线段序列.last().unwrap()); - if 当前线段.特征序列.read().unwrap()[2].is_some() { - let (_, 基础序列, _, _) = Self::分割序列(&当前线段, None); - let 新段 = 虚线::创建线段(&基础序列); - let 新段_rc = Arc::new(新段); - Self::_添加线段( - 线段序列, - 新段_rc, - 配置, - format!("{}, {}", line!(), 当前层级), - ); - if ["老阴", "老阳"].contains(&Self::四象(&当前线段).as_str()) { - *线段序列.last().unwrap().前一缺口.write().unwrap() = None; - } - } - } - - // Refresh current segment + // Python: 当前线段 = 线段序列[-1]; 线段._刷新(当前线段, 配置) + { let idx = 线段序列.len() - 1; Self::_刷新(&线段序列[idx], 配置); + } - // ---- 5. 调用一次全局修正 ---- - Self::_缺口突破(线段序列, 配置, 当前层级); - Self::_非缺口下穿刺(线段序列, 配置, 当前层级); - Self::_缺口后紧急修正(线段序列, 配置, 当前层级); - Self::_修正(线段序列, 配置, 当前层级); + // -------------------- 5. 调用一次全局修正(不循环) -------------------- + // Python: 四种修正各调用一次 + Self::_缺口突破(线段序列, 配置, 层级); + Self::_非缺口下穿刺(线段序列, 配置, 层级); + Self::_缺口后紧急修正(线段序列, 配置, 层级); + Self::_修正(线段序列, 配置, 层级); - // ---- 6. 循环处理后续的笔 ---- + // -------------------- 6. 循环处理后续的笔 -------------------- + // Python: 当前线段 = 线段序列[-1] + let 当前线段_rc = Arc::clone(线段序列.last().unwrap()); + if 当前线段_rc.基础序列.read().is_empty() { + panic!("线段.分析: 基础序列为空"); + } + // Python: 起始索引 = cls._索引(笔序列, 当前线段.基础序列[-1]) + 1 + let 起始索引 = { + let guard = 当前线段_rc.基础序列.read(); + 线段::_索引(笔序列, &guard[guard.len() - 1]) + 1 + }; + + // Python: for idx in range(起始索引, len(笔序列)): + for 当前虚线_ref in &笔序列[起始索引..] { + let 当前虚线 = Arc::clone(当前虚线_ref); + // Python: 当前线段 = 线段序列[-1]; 四象 = 线段.四象(当前线段) let 当前线段 = Arc::clone(线段序列.last().unwrap()); - if 当前线段.基础序列.read().unwrap().is_empty() { - panic!("线段.分析: 基础序列为空"); - } - let 最后笔 = Arc::clone(当前线段.基础序列.read().unwrap().last().unwrap()); - let 起始索引 = match 笔序列 - .iter() - .position(|x| Arc::as_ptr(x) == Arc::as_ptr(&最后笔)) + let 四象 = Self::四象(&当前线段); + + // Python: 线段._添加虚线(当前线段, 当前虚线) + Self::_添加虚线(线段序列.last().unwrap(), Arc::clone(&当前虚线)); + // Python: 线段._刷新(当前线段, 配置) { - Some(idx) => idx + 1, - None => { - // Last笔 not in 笔序列 — restart - 当前层级 += 1; - continue; - } - }; - - let mut 需要递归 = false; - - for 当前虚线_ref in &笔序列[起始索引..] { - let 当前虚线 = Arc::clone(当前虚线_ref); - let 当前线段 = Arc::clone(线段序列.last().unwrap()); - let 四象 = Self::四象(&当前线段); - - // 向当前线段添加笔 - let 线段_rc = Arc::clone(线段序列.last().unwrap()); - Self::_添加虚线(&线段_rc, Arc::clone(&当前虚线)); - let seg_idx = 线段序列.len() - 1; - 线段序列[seg_idx] = 线段_rc; - - // 刷新 - let idx = 线段序列.len() - 1; - Self::_刷新(&线段序列[idx], 配置); - - // 依次尝试四种修正(仅触发第一个匹配) - let mut 修正触发: Option<&str> = None; - if Self::_缺口突破(线段序列, 配置, 当前层级) { - 修正触发 = Some("缺口突破"); - } else if Self::_非缺口下穿刺(线段序列, 配置, 当前层级) { - 修正触发 = Some("非缺口下穿刺"); - } else if Self::_缺口后紧急修正(线段序列, 配置, 当前层级) { - 修正触发 = Some("缺口后紧急修正"); - } else if Self::_修正(线段序列, 配置, 当前层级) { - 修正触发 = Some("修正"); - } - if let Some(trigger) = 修正触发 { - warn!( - "分析.修正触发={}, 笔序列长度={}, 线段序列长度={}", - trigger, - 笔序列.len(), - 线段序列.len() - ); - continue; - } - - // 无修正触发,检查特征序列 - let 当前线段 = Arc::clone(线段序列.last().unwrap()); - if 当前线段.特征序列.read().unwrap()[2].is_none() { - continue; - } - - // 特征序列[2]存在 → 创建新段 - let (_, 基础序列, _, _) = Self::分割序列(&当前线段, None); - let 新段 = 虚线::创建线段(&基础序列); - let 新段_rc = Arc::new(新段); - Self::_添加线段( - 线段序列, - 新段_rc, - 配置, - format!("{}, {}", line!(), 当前层级), - ); - - if ["老阴", "老阳"].contains(&四象.as_str()) { - *线段序列.last().unwrap().前一缺口.write().unwrap() = None; - } - - // 检查新段与当前虚线的连续性 - let 新段 = Arc::clone(线段序列.last().unwrap()); - if Arc::as_ptr(新段.基础序列.read().unwrap().last().unwrap()) - != Arc::as_ptr(&当前虚线) - { - if !新段 - .基础序列 - .read() - .unwrap() - .last() - .unwrap() - .之后是(&当前虚线) - { - 需要递归 = true; - break; - } - // 向新段添加当前虚线 - let 新段_rc = Arc::clone(线段序列.last().unwrap()); - Self::_添加虚线(&新段_rc, Arc::clone(&当前虚线)); - let seg_idx = 线段序列.len() - 1; - 线段序列[seg_idx] = 新段_rc; - } - let idx = 线段序列.len() - 1; Self::_刷新(&线段序列[idx], 配置); } - if 需要递归 { - 当前层级 += 1; + // Python: 依次尝试四种修正,任意一个成功则跳过后续处理 + let mut 修正触发: Option<&str> = None; + if Self::_缺口突破(线段序列, 配置, 层级) { + 修正触发 = Some("缺口突破"); + } else if Self::_非缺口下穿刺(线段序列, 配置, 层级) { + 修正触发 = Some("非缺口下穿刺"); + } else if Self::_缺口后紧急修正(线段序列, 配置, 层级) { + 修正触发 = Some("缺口后紧急修正"); + } else if Self::_修正(线段序列, 配置, 层级) { + 修正触发 = Some("修正"); + } + if let Some(trigger) = 修正触发 { + warn!( + "分析.修正触发={}, 笔序列长度={}, 线段序列长度={}", + trigger, + 笔序列.len(), + 线段序列.len() + ); continue; } - break; + // Python: 无修正触发,且特征序列[2]已存在 → 创建新段 + let 当前线段 = Arc::clone(线段序列.last().unwrap()); + if 当前线段.特征序列.read()[2].is_none() { + continue; + } + + // Python: 基础序列 = 线段.分割序列(当前线段)[1]; 新段 = 虚线.创建线段(基础序列) + let (_, 基础序列, _, _) = Self::分割序列(&当前线段, None); + let 新段 = Arc::new(虚线::创建线段(&基础序列)); + // Python: 线段._添加线段(线段序列, 新段, 配置, ...) + Self::_添加线段(线段序列, 新段, 配置, line!(), 层级); + // Python: if 四象 in ("老阴", "老阳"): 新段.前一缺口 = None + if ["老阴", "老阳"].contains(&四象.as_str()) { + *线段序列.last().unwrap().前一缺口.write() = None; + } + + // Python: 检查新段与当前虚线的连续性 + let 新段_rc = Arc::clone(线段序列.last().unwrap()); + // Python: if 新段.基础序列[-1] is not 当前虚线 + if !Arc::ptr_eq(新段_rc.基础序列.read().last().unwrap(), &当前虚线) { + // Python: if not 新段.基础序列[-1].之后是(当前虚线): + // return 线段递归分析(笔序列, 线段序列, 配置, 层级 + 1, 关系序列) + if !新段_rc.基础序列.read().last().unwrap().之后是(&当前虚线) { + return Self::分析(笔序列, 线段序列, 配置, 层级 + 1, 关系序列); + } + // Python: 线段._添加虚线(新段, 当前虚线) + Self::_添加虚线(线段序列.last().unwrap(), Arc::clone(&当前虚线)); + } + + // Python: 线段._刷新(新段, 配置) + { + let idx = 线段序列.len() - 1; + Self::_刷新(&线段序列[idx], 配置); + } } } @@ -1466,15 +1344,9 @@ impl 线段 { ) { { let seg = Arc::make_mut(&mut 待添加线段); - *seg.模式.write().unwrap() = "高低".into(); - *seg.标识.write().unwrap() = if seg.基础序列.read().unwrap()[0] - .标识 - .read() - .unwrap() - .as_str() - != "笔" - { - format!("扩展{}", seg.标识.read().unwrap()) + *seg.模式.write() = "高低".into(); + *seg.标识.write() = if seg.基础序列.read()[0].标识.read().as_str() != "笔" { + format!("扩展{}", seg.标识.read()) } else { "扩展线段".into() }; @@ -1484,7 +1356,7 @@ impl 线段 { panic!( "线段.向序列中添加 不连续[{}] {} {}", 行号, - 前一个.武.read().unwrap(), + 前一个.武.read(), seg.文 ); } @@ -1506,7 +1378,7 @@ impl 线段 { return None; } - if Arc::as_ptr(线段序列.last().unwrap()) == Arc::as_ptr(待弹出线段) { + if Arc::ptr_eq(线段序列.last().unwrap(), 待弹出线段) { let drop = 线段序列.pop().unwrap(); drop.有效性.store(false, Ordering::Relaxed); Some(drop) @@ -1515,168 +1387,173 @@ impl 线段 { } } - /// 扩展分析 — 将笔视为线段进行同级别分析 + /// 扩展分析 — 将笔视为线段进行同级别分析(与 Python 扩展分析 对齐,递归实现) pub fn 扩展分析( 虚线序列: &[Arc<虚线>], 线段序列: &mut Vec>, 配置: &缠论配置 ) { + Self::扩展分析_递归(虚线序列, 线段序列, 配置, 0); + } + + fn 扩展分析_递归( + 虚线序列: &[Arc<虚线>], + 线段序列: &mut Vec>, + 配置: &缠论配置, + 层级: i64, + ) { + // Python: if not 虚线序列: return; try: 虚线序列[2] except: return if 虚线序列.len() < 3 { return; } - let mut 当前层级 = 0i64; + // Python: if 层级 > 256: return + if 层级 > 256 { + warn!("线段.扩展分析 递归深度超出 256"); + return; + } - loop { - if 当前层级 > 256 { - warn!("线段.扩展分析 递归深度超出 256"); - return; - } - - // 初始化第一个扩展线段 - if 线段序列.is_empty() { - for i in 1..虚线序列.len() - 1 { - let 左 = &虚线序列[i - 1]; - let 中 = &虚线序列[i]; - let 右 = &虚线序列[i + 1]; - let 关系 = 相对方向::分析(左.高(), 左.低(), 右.高(), 右.低()); - if !matches!( - 关系, - 相对方向::向下 - | 相对方向::向上 - | 相对方向::顺 - | 相对方向::逆 - | 相对方向::同 - ) { - continue; - } - - let 段 = - 虚线::创建线段(&[Arc::clone(左), Arc::clone(中), Arc::clone(右)]); - let 段_rc = Arc::new(段); - Self::_添加扩展线段(线段序列, 段_rc, line!()); - break; - } - - if 线段序列.is_empty() { - return; - } - } - - // 验证当前线段 - let 当前线段_rc = Arc::clone(线段序列.last().unwrap()); - Self::_验证序列(&当前线段_rc, 虚线序列); - let seg_idx = 线段序列.len() - 1; - 线段序列[seg_idx] = 当前线段_rc; - - if 线段序列.last().unwrap().基础序列.read().unwrap().len() < 3 { - let 当前 = Arc::clone(线段序列.last().unwrap()); - Self::_弹出扩展线段(线段序列, &当前, line!()); - 当前层级 += 1; - continue; - } - - if !配置.扩展线段_当下分析 { - let 当前线段 = Arc::clone(线段序列.last().unwrap()); - let 左 = Arc::clone(&当前线段.基础序列.read().unwrap()[0]); - let 右 = Arc::clone(&当前线段.基础序列.read().unwrap()[2]); - - if !相对方向::分析(左.高(), 左.低(), 右.高(), 右.低()).是否缺口() - { - { - let cur = Arc::make_mut(线段序列.last_mut().unwrap()); - let 前三个 = cur.基础序列.read().unwrap()[..3].to_vec(); - *cur.基础序列.write().unwrap() = 前三个; - } - let seg_idx = 线段序列.len() - 1; - Self::_武终(&线段序列[seg_idx], 0); - } else { - let 当前 = Arc::clone(线段序列.last().unwrap()); - Self::_弹出扩展线段(线段序列, &当前, line!()); - 当前层级 += 1; - continue; - } - } - - // 武终 - let idx = 线段序列.len() - 1; - Self::_武终(&线段序列[idx], 0); - - let 当前线段 = Arc::clone(线段序列.last().unwrap()); - if 当前线段 - .基础序列 - .read() - .unwrap() - .last() - .unwrap() - .序号 - .load(Ordering::Relaxed) - + 3 - > 虚线序列.last().unwrap().序号.load(Ordering::Relaxed) - { - return; - } - - let 最后笔 = Arc::clone(当前线段.基础序列.read().unwrap().last().unwrap()); - let 序号 = match 虚线序列 - .iter() - .position(|x| Arc::as_ptr(x) == Arc::as_ptr(&最后笔)) - { - Some(idx) => idx + 1, - None => return, - }; - - if 序号 >= 虚线序列.len() { - return; - } - - let mut 需要递归 = false; - - for i in 序号 + 1..虚线序列.len() - 1 { + // -------------------- 初始化第一个扩展线段 -------------------- + // Python: if not 线段序列: for i in range(1, len-1): ... + if 线段序列.is_empty() { + for i in 1..虚线序列.len() - 1 { let 左 = &虚线序列[i - 1]; let 中 = &虚线序列[i]; let 右 = &虚线序列[i + 1]; - let 相对关系 = 相对方向::分析(左.高(), 左.低(), 右.高(), 右.低()); - - if 相对关系.是否缺口() { - let 段_rc = Arc::clone(线段序列.last().unwrap()); - Self::_添加虚线(&段_rc, Arc::clone(左)); - let seg_idx = 线段序列.len() - 1; - 线段序列[seg_idx] = 段_rc; - - let 段_rc = Arc::clone(线段序列.last().unwrap()); - Self::_添加虚线(&段_rc, Arc::clone(中)); - let seg_idx = 线段序列.len() - 1; - 线段序列[seg_idx] = 段_rc; - - let seg_idx = 线段序列.len() - 1; - Self::_武终(&线段序列[seg_idx], 0); + // Python: 关系 = 相对方向.分析(左.端点高, 左.端点低, 右.端点高, 右.端点低) + let 关系 = 相对方向::分析(左.端点高(), 左.端点低(), 右.端点高(), 右.端点低()); + // Python: if 关系 not in (向下, 向上, 顺, 逆, 同): continue + if !matches!( + 关系, + 相对方向::向下 + | 相对方向::向上 + | 相对方向::顺 + | 相对方向::逆 + | 相对方向::同 + ) { continue; } - if 线段序列 - .last() - .unwrap() - .基础序列 - .read() - .unwrap() - .iter() - .any(|x| Arc::as_ptr(x) == Arc::as_ptr(左)) - { - continue; - } - - let 段 = 虚线::创建线段(&[Arc::clone(左), Arc::clone(中), Arc::clone(右)]); - let 段_rc = Arc::new(段); - Self::_添加扩展线段(线段序列, 段_rc, line!()); - 需要递归 = true; + // Python: 段 = 虚线.创建线段([左, 中, 右]); _添加扩展线段(...) + let 段 = Arc::new(虚线::创建线段(&[ + Arc::clone(左), + Arc::clone(中), + Arc::clone(右), + ])); + Self::_添加扩展线段(线段序列, 段, line!()); break; } - if 需要递归 { - 当前层级 += 1; + // Python: if not 线段序列: return + if 线段序列.is_empty() { + return; + } + } + + // -------------------- 验证当前线段 -------------------- + // Python: 当前线段 = 线段序列[-1]; _验证序列(当前线段, 虚线序列) + Self::_验证序列(线段序列.last().unwrap(), 虚线序列); + + // Python: if len(当前线段.基础序列) < 3: _弹出扩展线段(...); return 递归 + if 线段序列.last().unwrap().基础序列.read().len() < 3 { + let 当前 = Arc::clone(线段序列.last().unwrap()); + Self::_弹出扩展线段(线段序列, &当前, line!()); + return Self::扩展分析_递归(虚线序列, 线段序列, 配置, 层级 + 1); + } + + // -------------------- 非当下分析模式 -------------------- + // Python: if not 配置.扩展线段_当下分析: ... + if !配置.扩展线段_当下分析 { + let 当前线段 = 线段序列.last().unwrap(); + // Python: 左, 中, 右 = 当前线段.基础序列[:3] + let 左 = Arc::clone(&当前线段.基础序列.read()[0]); + let 右 = Arc::clone(&当前线段.基础序列.read()[2]); + + // Python: if not 相对方向.分析(左.端点高, 左.端点低, 右.端点高, 右.端点低).是否缺口(): + if !相对方向::分析(左.端点高(), 左.端点低(), 右.端点高(), 右.端点低()).是否缺口() + { + // Python: 当前线段.基础序列 = 当前线段.基础序列[:3]; _武终(当前线段, ...) + let cur = Arc::make_mut(线段序列.last_mut().unwrap()); + let 前三个 = cur.基础序列.read()[..3].to_vec(); + *cur.基础序列.write() = 前三个; + let seg_idx = 线段序列.len() - 1; + Self::_武终(&线段序列[seg_idx], 0); + } else { + // Python: _弹出扩展线段(线段序列, 当前线段, ...); return 递归 + let 当前 = Arc::clone(线段序列.last().unwrap()); + Self::_弹出扩展线段(线段序列, &当前, line!()); + return Self::扩展分析_递归(虚线序列, 线段序列, 配置, 层级 + 1); + } + } + + // -------------------- 武终 -------------------- + // Python: 线段._武终(当前线段, ...) + { + let seg_idx = 线段序列.len() - 1; + Self::_武终(&线段序列[seg_idx], 0); + } + + // -------------------- 检查剩余元素是否够 -------------------- + // Python: if 当前线段.基础序列[-1].序号 + 3 > 虚线序列[-1].序号: return + let 当前线段 = 线段序列.last().unwrap(); + if 当前线段 + .基础序列 + .read() + .last() + .unwrap() + .序号 + .load(Ordering::Relaxed) + + 3 + > 虚线序列.last().unwrap().序号.load(Ordering::Relaxed) + { + return; + } + + // -------------------- 计算起始序号 -------------------- + // Python: 序号 = cls._索引(虚线序列, 当前线段.基础序列[-1]) + 1 + let 最后笔 = Arc::clone(当前线段.基础序列.read().last().unwrap()); + let 序号 = 线段::_索引(虚线序列, &最后笔) + 1; + + // Python: if 序号 >= len(虚线序列): return + if 序号 >= 虚线序列.len() { + return; + } + + // -------------------- 循环处理后续虚线 -------------------- + // Python: for i in range(序号 + 1, len(虚线序列) - 1): + for i in 序号 + 1..虚线序列.len() - 1 { + let 左 = &虚线序列[i - 1]; + let 中 = &虚线序列[i]; + let 右 = &虚线序列[i + 1]; + let 相对关系 = 相对方向::分析(左.端点高(), 左.端点低(), 右.端点高(), 右.端点低()); + + // Python: if 相对关系.是否缺口(): _添加虚线(当前线段, 左); _添加虚线(当前线段, 中); _武终; continue + if 相对关系.是否缺口() { + Self::_添加虚线(线段序列.last().unwrap(), Arc::clone(左)); + Self::_添加虚线(线段序列.last().unwrap(), Arc::clone(中)); + let seg_idx = 线段序列.len() - 1; + Self::_武终(&线段序列[seg_idx], 0); continue; } - break; + // Python: if 左 in 当前线段.基础序列: continue + if 线段序列 + .last() + .unwrap() + .基础序列 + .read() + .iter() + .any(|x| Arc::ptr_eq(x, 左)) + { + continue; + } + + // Python: 段 = 虚线.创建线段([左, 中, 右]); _添加扩展线段(...); return 递归 + let 段 = Arc::new(虚线::创建线段(&[ + Arc::clone(左), + Arc::clone(中), + Arc::clone(右), + ])); + Self::_添加扩展线段(线段序列, 段, line!()); + return Self::扩展分析_递归(虚线序列, 线段序列, 配置, 层级 + 1); } } @@ -1725,41 +1602,31 @@ impl 线段 { { let k线序列 = K线::截取rc( &观察员.普通K线序列, - &阳[阳.len() - 3].文.中.标的K线.read().unwrap(), - &阳[阳.len() - 1] - .武 - .read() - .unwrap() - .中 - .标的K线 - .read() - .unwrap(), + &阳[阳.len() - 3].文.中.标的K线.read(), + &阳[阳.len() - 1].武.read().中.标的K线.read(), ); if 虚线::计算MACD柱子分段(&k线序列).len() >= 3 { 盘整背驰 = true; } } - let 实_ref = 实.read().unwrap(); + let 实_ref = 实.read(); if !实_ref.is_empty() { let 最后中枢 = &实_ref[实_ref.len() - 1]; if 最后中枢 .基础序列 .read() - .unwrap() .iter() .any(|b| Arc::ptr_eq(b, &阳[阳.len() - 1])) { // 最后一笔在最后一个中枢内 - if let Some(序号) = 当前段 - .基础序列 - .read() - .unwrap() - .iter() - .position(|b| Arc::ptr_eq(b, &最后中枢.基础序列.read().unwrap()[0])) - && 序号 > 0 - { - let 进入段 = &当前段.基础序列.read().unwrap()[序号 - 1]; + // Python: 序号 = cls._索引(当前段.基础序列, 当前段.实_中枢序列[-1].基础序列[0]) + let 序号 = { + let guard = 当前段.基础序列.read(); + 线段::_索引(&guard, &最后中枢.基础序列.read()[0]) + }; + if 序号 > 0 && 序号 < 当前段.基础序列.read().len() { + let 进入段 = &当前段.基础序列.read()[序号 - 1]; let 离开段 = &阳[阳.len() - 1]; assert!( 进入段.序号.load(Ordering::Relaxed) < 离开段.序号.load(Ordering::Relaxed) @@ -1784,7 +1651,7 @@ impl 线段 { return true; } } - } else if 最后中枢.第三买卖线.read().unwrap().is_some() { + } else if 最后中枢.第三买卖线.read().is_some() { // 第三买卖点后盘整背驰 let 进入段 = &阳[阳.len() - 3]; let 离开段 = &阳[阳.len() - 1]; @@ -1820,9 +1687,9 @@ impl 线段 { /// 获取所有停顿位置 — 在线段范围内找出所有停顿位置 pub fn 获取所有停顿位置(段: &虚线, 观察员: &观察者) -> Vec<虚线> { - let mut 结果 = Vec::new(); - if 段.模式.read().unwrap().as_str() != "文武" || 段.标识.read().unwrap().as_str() != "线段" - { + let cap = 段.基础序列.read().len() / 2; + let mut 结果 = Vec::with_capacity(cap); + if 段.模式.read().as_str() != "文武" || 段.标识.read().as_str() != "线段" { return 结果; } @@ -1843,9 +1710,8 @@ impl 线段 { for 停顿 in &停顿列表 { 笔序列.push(Arc::clone(停顿)); - let 笔序列_slice: Vec> = 笔序列.iter().map(Arc::clone).collect(); Self::分析( - &笔序列_slice, + &笔序列, &mut 线段序列, &观察员.配置, 0, @@ -1859,14 +1725,14 @@ impl 线段 { ); let 重复 = match (&线段序列.last(), &当前停顿) { - (Some(a), Some(b)) => Arc::ptr_eq(&*a.武.read().unwrap(), b), + (Some(a), Some(b)) => Arc::ptr_eq(&*a.武.read(), b), _ => false, }; if !重复 && let Some(最后线段) = 线段序列.last() - && 最后线段.基础序列.read().unwrap().len() % 2 == 1 + && 最后线段.基础序列.read().len() % 2 == 1 { - let 新段 = 虚线::创建线段(&最后线段.基础序列.read().unwrap()); + let 新段 = 虚线::创建线段(&最后线段.基础序列.read()); 新段 .序号 .store(段.序号.load(Ordering::Relaxed), Ordering::Relaxed); @@ -1875,13 +1741,12 @@ impl 线段 { let 新段_inner = Arc::try_unwrap(新段_rc).unwrap_or_else(|rc| (*rc).clone()); if 新段_inner.方向() == 段.方向() { - 当前停顿 = - Some(Arc::clone(&*线段序列.last().unwrap().武.read().unwrap())); + 当前停顿 = Some(Arc::clone(&*线段序列.last().unwrap().武.read())); 结果.push(新段_inner); } } - if Arc::as_ptr(停顿) != Arc::as_ptr(筆) + if !Arc::ptr_eq(停顿, 筆) && let Some(popped) = 笔序列.pop() { popped.有效性.store(false, Ordering::Relaxed); @@ -1903,7 +1768,7 @@ impl 线段 { let 段_rc = Arc::new(段); Self::获取内部中枢序列(&段_rc, &观察员.配置); if Self::判断线段内部是否背驰(&段_rc, 观察员) { - 结果.push(Arc::clone(&段_rc.武.read().unwrap().中)); + 结果.push(Arc::clone(&段_rc.武.read().中)); } } @@ -2016,7 +1881,7 @@ mod tests { let 文 = 辅助_创建底分型(100, 90.0); let 武 = 辅助_创建顶分型(200, 110.0); let 段 = 辅助_创建笔(文, 武); - *段.前一缺口.write().unwrap() = Some(缺口 { + *段.前一缺口.write() = Some(缺口 { 高: 105.0, 低: 95.0, }); @@ -2028,7 +1893,7 @@ mod tests { let 文 = 辅助_创建顶分型(100, 110.0); let 武 = 辅助_创建底分型(200, 90.0); let 段 = 辅助_创建笔(文, 武); - *段.前一缺口.write().unwrap() = Some(缺口 { + *段.前一缺口.write() = Some(缺口 { 高: 105.0, 低: 95.0, }); @@ -2052,7 +1917,7 @@ mod tests { let 文 = 辅助_创建底分型(100, 90.0); let 武 = 辅助_创建顶分型(200, 110.0); let 段 = 辅助_创建笔(文, 武); - *段.模式.write().unwrap() = "其他".into(); + *段.模式.write() = "其他".into(); assert!(线段::获取缺口(&段).is_none()); } @@ -2071,7 +1936,7 @@ mod tests { let 文 = 辅助_创建底分型(100, 90.0); let 武 = 辅助_创建顶分型(200, 110.0); let 段 = 辅助_创建笔(文, 武); - let (前, 后, 第三买卖线, 贯穿伤) = 线段::分割序列(&段, None); + let (_前, 后, 第三买卖线, 贯穿伤) = 线段::分割序列(&段, None); // 独立笔无基础序列时,前也为空 assert!(后.is_empty()); assert!(第三买卖线.is_empty()); diff --git a/chanlun/src/business/bsp.rs b/chanlun/src/business/bsp.rs index ff5966f..0026aa5 100644 --- a/chanlun/src/business/bsp.rs +++ b/chanlun/src/business/bsp.rs @@ -347,7 +347,7 @@ impl 买卖点 { let 破位值 = 买卖点分型.分型特征值(); // 当前K线 — 从缠K获取其标的K线 - let 当前K线 = Arc::clone(&*当前缠K.标的K线.read().unwrap()); + let 当前K线 = Arc::clone(&*当前缠K.标的K线.read()); // 当前缠K序号 — 与买卖点K线(分型.中.序号)同尺度,用于偏移计算 let 当前缠K序号 = 当前缠K.序号.load(Ordering::Relaxed); diff --git a/chanlun/src/business/multi_frame.rs b/chanlun/src/business/multi_frame.rs index 0ff19df..ffd35b8 100644 --- a/chanlun/src/business/multi_frame.rs +++ b/chanlun/src/business/multi_frame.rs @@ -26,10 +26,10 @@ use crate::business::observer::观察者; use crate::business::synthesizer::K线合成器; use crate::config::缠论配置; use crate::kline::bar::K线; +use crate::{error, warn}; +use parking_lot::RwLock; use std::collections::HashMap; use std::sync::Arc; -use std::sync::RwLock; -use tracing::{error, info}; /// 立体分析器 — 多周期协调器 pub struct 立体分析器 { @@ -61,8 +61,7 @@ impl 立体分析器 { .get(&周期) .cloned() .unwrap_or_else(|| 默认配置.clone()); - 当前配置.推送K线 = false; - 当前配置.推送线段 = false; + 当前配置.图表展示标签 = Some(vec![]); 当前配置.标识 = 符号.clone(); let 观察员 = 观察者::new(符号.clone(), 周期, 当前配置); @@ -72,10 +71,8 @@ impl 立体分析器 { // 显示周期特殊配置 { let 显示观察员 = 单体分析器.get(&显示周期).expect("显示周期观察者不存在"); - let mut guard = 显示观察员.write().unwrap(); - guard.配置.推送K线 = true; - guard.配置.推送笔 = true; - guard.配置.推送线段 = true; + let mut guard = 显示观察员.write(); + guard.配置.图表展示标签 = None; // None = 全部展示 guard.配置.图表展示 = true; guard.重置基础序列(); } @@ -84,14 +81,14 @@ impl 立体分析器 { { let 显示缠K序列 = 单体分析器 .get(&显示周期) - .map(|o| o.read().unwrap().缠论K线序列.clone()) + .map(|o| o.read().缠论K线序列.clone()) .unwrap_or_default(); for &周期 in &周期组 { if 周期 != 显示周期 && let Some(观察员) = 单体分析器.get(&周期) { - 观察员.write().unwrap().基础缠K序列 = 显示缠K序列.clone(); + 观察员.write().基础缠K序列 = 显示缠K序列.clone(); } } } @@ -119,7 +116,7 @@ impl 立体分析器 { /// __K线回调 — 对应 Python 立体分析器.__K线回调 fn __K线回调(&self, _信号类型: String, _标识: String, 周期: i64, 完成K线: K线) { if let Some(观察员) = self.单体分析器.get(&周期) { - let mut obs = 观察员.write().unwrap(); + let mut obs = 观察员.write(); obs.增加原始K线(完成K线); // 对应 Python: if 当前K线 := self._K线合成器.获取当前K线(周期) // _完成K线刚清空当前K线,获取当前K线返回 None,所以这里不添加 @@ -133,7 +130,7 @@ impl 立体分析器 { 完成K线: K线, ) { if let Some(观察员) = 单体分析器.get(&周期) { - 观察员.write().unwrap().增加原始K线(完成K线); + 观察员.write().增加原始K线(完成K线); } } @@ -165,22 +162,22 @@ impl 立体分析器 { let 起始时间 = self .单体分析器 .get(&self.输入周期) - .and_then(|o| o.read().unwrap().普通K线序列.first().map(|k| k.时间戳)) + .and_then(|o| o.read().普通K线序列.first().map(|k| k.时间戳)) .unwrap_or(0); let 结束时间 = self .单体分析器 .get(&self.输入周期) - .and_then(|o| o.read().unwrap().普通K线序列.last().map(|k| k.时间戳)) + .and_then(|o| o.read().普通K线序列.last().map(|k| k.时间戳)) .unwrap_or(0); let 标识 = self .单体分析器 .get(&self.输入周期) - .map(|o| o.read().unwrap().符号.clone()) + .map(|o| o.read().符号.clone()) .unwrap_or_default(); let 周期 = self .单体分析器 .get(&self.输入周期) - .map(|o| o.read().unwrap().周期) + .map(|o| o.read().周期) .unwrap_or_default(); let 目录标识 = format!("RustM_{}:{}_{}_{}", 标识, 周期, 起始时间, 结束时间); @@ -195,12 +192,11 @@ impl 立体分析器 { if let Some(观察员) = self.单体分析器.get(周期) { 观察员 .read() - .unwrap() .测试_保存数据(Some(&保存路径.to_string_lossy())); } } - info!("多级别数据拆分保存完成,目录:{}", 保存路径.display()); + warn!("多级别数据拆分保存完成,目录:{}", 保存路径.display()); } /// 相等 — 各周期观察者全量比对,对应 Python `立体分析器相等` @@ -213,11 +209,11 @@ impl 立体分析器 { for 周期 in &self.周期组 { let a_obs = match self.单体分析器.get(周期) { - Some(o) => o.read().unwrap(), + Some(o) => o.read(), None => return (false, format!("{标签}: 周期{周期} 观察者不存在 (A)")), }; let b_obs = match other.单体分析器.get(周期) { - Some(o) => o.read().unwrap(), + Some(o) => o.read(), None => return (false, format!("{标签}: 周期{周期} 观察者不存在 (B)")), }; let (eq, msg) = a_obs.相等(&b_obs, 浮点容差); diff --git a/chanlun/src/business/observer.rs b/chanlun/src/business/observer.rs index 8c3ab18..84462a7 100644 --- a/chanlun/src/business/observer.rs +++ b/chanlun/src/business/observer.rs @@ -26,15 +26,17 @@ use crate::algorithm::bi::笔; use crate::algorithm::hub::中枢; use crate::algorithm::segment::线段; use crate::config::缠论配置; +use crate::indicators::calculator::指标计算器; use crate::kline::bar::K线; use crate::kline::chan_kline::缠论K线; use crate::structure::dash_line::虚线; use crate::structure::fractal_obj::分型; use crate::types::相对方向; use crate::utils::datetime; +use crate::{error, warn}; +use parking_lot::RwLock; +use std::sync::Arc; use std::sync::atomic::Ordering; -use std::sync::{Arc, RwLock}; -use tracing::{error, info}; /// 观察者 — 单周期分析器,持有所有层级序列,接收K线流式输入后逐层计算 pub struct 观察者 { @@ -198,6 +200,14 @@ impl 观察者 { &self.混合扩展中枢序列组[0] } + /// 确保所有 K 线上的指标已计算(幂等)。 + /// 在信号函数入口调用,保证后续 macd()/rsi() 等访问不返回 None。 + pub fn 确保指标已计算(&self) { + if self.配置.计算指标 && !self.普通K线序列.is_empty() { + 指标计算器::计算并挂载(&self.普通K线序列, &self.配置); + } + } + /// 重置基础序列 pub fn 重置基础序列(&mut self) { self.普通K线序列.clear(); @@ -298,17 +308,15 @@ impl 观察者 { &[相对方向::向上, 相对方向::向下], ); } - } else { - if self.配置.分析线段 { - let 源序列 = self.线段序列组[i - 1].clone(); - 线段::分析( - &源序列, - &mut self.线段序列组[i], - &self.配置, - 0, - &[相对方向::向上, 相对方向::向下], - ); - } + } else if self.配置.分析线段 { + let (left, right) = self.线段序列组.split_at_mut(i); + 线段::分析( + &left[i - 1], + &mut right[0], + &self.配置, + 0, + &[相对方向::向上, 相对方向::向下], + ); } if self.配置.分析线段中枢 { 中枢::分析(&self.线段序列组[i], &mut self.中枢序列组[i], true, "", 0); @@ -323,11 +331,9 @@ impl 观察者 { if self.配置.分析扩展线段 { 线段::扩展分析(&self.笔序列, &mut self.扩展线段序列组[i], &self.配置); } - } else { - if self.配置.分析扩展线段 { - let 源序列 = self.扩展线段序列组[i - 1].clone(); - 线段::扩展分析(&源序列, &mut self.扩展线段序列组[i], &self.配置); - } + } else if self.配置.分析扩展线段 { + let (left, right) = self.扩展线段序列组.split_at_mut(i); + 线段::扩展分析(&left[i - 1], &mut right[0], &self.配置); } if self.配置.分析线段中枢 { 中枢::分析( @@ -345,8 +351,11 @@ impl 观察者 { if self.配置.分析扩展线段 || self.配置.分析线段中枢 { for i in 0..self.混合扩展线段分析层次.min(self.线段序列组.len()) { if self.配置.分析扩展线段 { - let 源序列 = self.线段序列组[i].clone(); - 线段::扩展分析(&源序列, &mut self.混合扩展线段序列组[i], &self.配置); + 线段::扩展分析( + &self.线段序列组[i], + &mut self.混合扩展线段序列组[i], + &self.配置, + ); } if self.配置.分析线段中枢 { 中枢::分析( @@ -430,17 +439,15 @@ impl 观察者 { &[相对方向::向上, 相对方向::向下], ); } - } else { - if self.配置.分析线段 { - let 源序列 = self.线段序列组[i - 1].clone(); - 线段::分析( - &源序列, - &mut self.线段序列组[i], - &self.配置, - 0, - &[相对方向::向上, 相对方向::向下], - ); - } + } else if self.配置.分析线段 { + let (left, right) = self.线段序列组.split_at_mut(i); + 线段::分析( + &left[i - 1], + &mut right[0], + &self.配置, + 0, + &[相对方向::向上, 相对方向::向下], + ); } if self.配置.分析线段中枢 { 中枢::分析(&self.线段序列组[i], &mut self.中枢序列组[i], true, "", 0); @@ -454,11 +461,9 @@ impl 观察者 { if self.配置.分析扩展线段 { 线段::扩展分析(&self.笔序列, &mut self.扩展线段序列组[i], &self.配置); } - } else { - if self.配置.分析扩展线段 { - let 源序列 = self.扩展线段序列组[i - 1].clone(); - 线段::扩展分析(&源序列, &mut self.扩展线段序列组[i], &self.配置); - } + } else if self.配置.分析扩展线段 { + let (left, right) = self.扩展线段序列组.split_at_mut(i); + 线段::扩展分析(&left[i - 1], &mut right[0], &self.配置); } if self.配置.分析线段中枢 { 中枢::分析( @@ -475,8 +480,11 @@ impl 观察者 { if self.配置.分析扩展线段 || self.配置.分析线段中枢 { for i in 0..self.混合扩展线段分析层次.min(self.线段序列组.len()) { if self.配置.分析扩展线段 { - let 源序列 = self.线段序列组[i].clone(); - 线段::扩展分析(&源序列, &mut self.混合扩展线段序列组[i], &self.配置); + 线段::扩展分析( + &self.线段序列组[i], + &mut self.混合扩展线段序列组[i], + &self.配置, + ); } if self.配置.分析线段中枢 { 中枢::分析( @@ -517,7 +525,7 @@ impl 观察者 { return Ok(()); } let 数据列表: Vec = 序列.iter().map(|d| d.获取数据文本()).collect(); - let 文件名 = format!("{}.txt", 序列[0].标识.read().unwrap()); + let 文件名 = format!("{}.txt", 序列[0].标识.read()); std::fs::write(保存路径.join(&文件名), 数据列表.join("\n") + "\n")?; Ok(()) }; @@ -528,7 +536,7 @@ impl 观察者 { return Ok(()); } let 数据列表: Vec = 序列.iter().map(|h| h.获取数据文本()).collect(); - let 文件名 = format!("{}.txt", 序列[0].标识.read().unwrap()); + let 文件名 = format!("{}.txt", 序列[0].标识.read()); std::fs::write(保存路径.join(&文件名), 数据列表.join("\n") + "\n")?; Ok(()) }; @@ -563,7 +571,7 @@ impl 观察者 { ck.序号.load(Ordering::Relaxed), ck.时间戳.load(Ordering::Relaxed), ck.分型, - *ck.方向.read().unwrap(), + *ck.方向.read(), ck.高.get(), ck.低.get(), ck.原始起始序号, @@ -597,7 +605,7 @@ impl 观察者 { 分型序列_数据文本.join("\n") + "\n", ); - info!("全部数据拆分保存完成,目录:{}", 保存路径.display()); + warn!("全部数据拆分保存完成,目录:{}", 保存路径.display()); 保存路径.display().to_string() } @@ -734,6 +742,7 @@ impl 观察者 { mod tests { use super::*; use crate::config::缠论配置; + use crate::info; fn test_data_path() -> String { let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); @@ -749,10 +758,9 @@ mod tests { fn test_普k序列指针一致性() { let obs = 观察者::new("btcusd".into(), 300, Default::default()); obs.write() - .unwrap() .读取数据文件(&test_data_path(), Default::default()) .unwrap(); - let obs_ref = obs.read().unwrap(); + let obs_ref = obs.read(); for (i, bi) in obs_ref.笔序列.iter().enumerate() { let pu_seq = bi.获取普K序列(&obs_ref.普通K线序列); @@ -761,12 +769,7 @@ mod tests { info!(" 文.中.标的K线 原始起始序号: {}", bi.文.中.原始起始序号); info!( " 武.中.标的K线 原始结束序号: {}", - bi.武 - .read() - .unwrap() - .中 - .原始结束序号 - .load(Ordering::Relaxed) + bi.武.read().中.原始结束序号.load(Ordering::Relaxed) ); info!(" 普通K线序列.len: {}", obs_ref.普通K线序列.len()); } else { @@ -777,7 +780,7 @@ mod tests { .any(|k| Arc::as_ptr(k) == first_ptr); if !found { info!("笔 {}: 获取普K序列[0] 的 Rc 指针不在 普通K线序列 中!", i); - let wen_ptr = Arc::as_ptr(&*bi.文.中.标的K线.read().unwrap()); + let wen_ptr = Arc::as_ptr(&*bi.文.中.标的K线.read()); let wen_found = obs_ref .普通K线序列 .iter() @@ -802,11 +805,11 @@ mod tests { let offset = i * size; if let Some(k线) = K线::from_bytes(&data[offset..offset + size], 300, "btcusd") { let _k线_py_inner = Arc::new(k线.clone()); - obs_ref.write().unwrap().增加原始K线(k线); + obs_ref.write().增加原始K线(k线); } } - let obs = obs_ref.read().unwrap(); + let obs = obs_ref.read(); info!("普通K线序列.len: {}", obs.普通K线序列.len()); info!("笔序列.len: {}", obs.笔序列.len()); @@ -835,10 +838,9 @@ mod tests { fn test_分型到笔的文武Rc指针一致性() { let obs = 观察者::new("btcusd".into(), 300, Default::default()); obs.write() - .unwrap() .读取数据文件(&test_data_path(), Default::default()) .unwrap(); - let obs_ref = obs.read().unwrap(); + let obs_ref = obs.read(); // 每个笔的文/武 分型 Rc 指针必须在 分型序列 中 for (i, bi) in obs_ref.笔序列.iter().enumerate() { @@ -848,13 +850,13 @@ mod tests { info!("笔 {}: 文(时间戳={}) 不在分型序列中!", i, bi.文.时间戳()); } - let 武_ptr = Arc::as_ptr(&*bi.武.read().unwrap()); + let 武_ptr = Arc::as_ptr(&*bi.武.read()); let 武_found = obs_ref.分型序列.iter().any(|f| Arc::as_ptr(f) == 武_ptr); if !武_found { info!( "笔 {}: 武(时间戳={}) 不在分型序列中!", i, - bi.武.read().unwrap().时间戳() + bi.武.read().时间戳() ); } } @@ -868,14 +870,13 @@ mod tests { fn test_笔到线段的基础序列Rc指针一致性() { let obs = 观察者::new("btcusd".into(), 300, Default::default()); obs.write() - .unwrap() .读取数据文件(&test_data_path(), Default::default()) .unwrap(); - let obs_ref = obs.read().unwrap(); + let obs_ref = obs.read(); // 每个线段的基础序列中的笔 Rc 指针必须在 笔序列 中 for (i, seg) in obs_ref.线段序列().iter().enumerate() { - for (j, bi_in_seg) in seg.基础序列.read().unwrap().iter().enumerate() { + for (j, bi_in_seg) in seg.基础序列.read().iter().enumerate() { let bi_ptr = Arc::as_ptr(bi_in_seg); let found = obs_ref.笔序列.iter().any(|b| Arc::as_ptr(b) == bi_ptr); if !found { @@ -893,13 +894,12 @@ mod tests { fn test_中枢基础序列与笔序列Rc指针一致() { let obs = 观察者::new("btcusd".into(), 300, Default::default()); obs.write() - .unwrap() .读取数据文件(&test_data_path(), Default::default()) .unwrap(); - let obs_ref = obs.read().unwrap(); + let obs_ref = obs.read(); for (i, hub) in obs_ref.笔_中枢序列.iter().enumerate() { - for (j, bi_in_hub) in hub.基础序列.read().unwrap().iter().enumerate() { + for (j, bi_in_hub) in hub.基础序列.read().iter().enumerate() { let bi_ptr = Arc::as_ptr(bi_in_hub); let found = obs_ref.笔序列.iter().any(|b| Arc::as_ptr(b) == bi_ptr); if !found { @@ -909,7 +909,7 @@ mod tests { } for (i, hub) in obs_ref.中枢序列().iter().enumerate() { - for (j, seg_in_hub) in hub.基础序列.read().unwrap().iter().enumerate() { + for (j, seg_in_hub) in hub.基础序列.read().iter().enumerate() { let seg_ptr = Arc::as_ptr(seg_in_hub); let found = obs_ref.线段序列().iter().any(|s| Arc::as_ptr(s) == seg_ptr); if !found { @@ -934,10 +934,10 @@ mod tests { for i in 0..data.len() / size { let offset = i * size; if let Some(k线) = K线::from_bytes(&data[offset..offset + size], 300, "btcusd") { - obs_ref.write().unwrap().增加原始K线(k线); + obs_ref.write().增加原始K线(k线); } } - let obs = obs_ref.read().unwrap(); + let obs = obs_ref.read(); ( obs.笔序列.len(), obs.线段序列().len(), @@ -975,28 +975,28 @@ mod tests { for i in 0..data.len() / size { let offset = i * size; if let Some(k线) = K线::from_bytes(&data[offset..offset + size], 300, "btcusd") { - obs_ref.write().unwrap().增加原始K线(k线); + obs_ref.write().增加原始K线(k线); } } - let 第一次笔数 = obs_ref.read().unwrap().笔序列.len(); - let 第一次段数 = obs_ref.read().unwrap().线段序列().len(); + let 第一次笔数 = obs_ref.read().笔序列.len(); + let 第一次段数 = obs_ref.read().线段序列().len(); // 重置 - obs_ref.write().unwrap().重置基础序列(); - assert_eq!(obs_ref.read().unwrap().笔序列.len(), 0); - assert_eq!(obs_ref.read().unwrap().线段序列().len(), 0); + obs_ref.write().重置基础序列(); + assert_eq!(obs_ref.read().笔序列.len(), 0); + assert_eq!(obs_ref.read().线段序列().len(), 0); // 重新投喂 for i in 0..data.len() / size { let offset = i * size; if let Some(k线) = K线::from_bytes(&data[offset..offset + size], 300, "btcusd") { - obs_ref.write().unwrap().增加原始K线(k线); + obs_ref.write().增加原始K线(k线); } } - let 第二次笔数 = obs_ref.read().unwrap().笔序列.len(); - let 第二次段数 = obs_ref.read().unwrap().线段序列().len(); + let 第二次笔数 = obs_ref.read().笔序列.len(); + let 第二次段数 = obs_ref.read().线段序列().len(); assert_eq!(第一次笔数, 第二次笔数, "重置后重新投喂笔数不一致"); assert_eq!(第一次段数, 第二次段数, "重置后重新投喂线段数不一致"); @@ -1011,31 +1011,30 @@ mod tests { fn test_RefCell借用安全性_连续读取不panic() { let obs = 观察者::new("btcusd".into(), 300, Default::default()); obs.write() - .unwrap() .读取数据文件(&test_data_path(), Default::default()) .unwrap(); - let obs_ref = obs.read().unwrap(); + let obs_ref = obs.read(); // 连续大量读取所有 RefCell 字段,不应 panic for _ in 0..100 { for bi in &obs_ref.笔序列 { - let _标识 = bi.标识.read().unwrap().clone(); - let _wu = bi.武.read().unwrap().clone(); - let _基础序列 = bi.基础序列.read().unwrap().len(); - let _特征序列 = bi.特征序列.read().unwrap().len(); - let _模式 = bi.模式.read().unwrap().clone(); - let _实中枢 = bi.实_中枢序列.read().unwrap().len(); - let _虚中枢 = bi.虚_中枢序列.read().unwrap().len(); - let _合中枢 = bi.合_中枢序列.read().unwrap().len(); - let _确认K = bi.确认K线.read().unwrap().is_some(); + let _标识 = bi.标识.read().clone(); + let _wu = bi.武.read().clone(); + let _基础序列 = bi.基础序列.read().len(); + let _特征序列 = bi.特征序列.read().len(); + let _模式 = bi.模式.read().clone(); + let _实中枢 = bi.实_中枢序列.read().len(); + let _虚中枢 = bi.虚_中枢序列.read().len(); + let _合中枢 = bi.合_中枢序列.read().len(); + let _确认K = bi.确认K线.read().is_some(); let _序号 = bi.序号.load(Ordering::Relaxed); let _有效性 = bi.有效性.load(Ordering::Relaxed); let _短路 = bi.短路修正.load(Ordering::Relaxed); - let _前一缺口 = *bi.前一缺口.read().unwrap(); + let _前一缺口 = *bi.前一缺口.read(); } for seg in obs_ref.线段序列() { - let _ = seg.标识.read().unwrap().clone(); - let _ = seg.基础序列.read().unwrap().len(); + let _ = seg.标识.read().clone(); + let _ = seg.基础序列.read().len(); } } // 到达这里 = 无 panic @@ -1045,27 +1044,26 @@ mod tests { fn test_RefCell借用安全性_交替读写不panic() { let obs = 观察者::new("btcusd".into(), 300, Default::default()); obs.write() - .unwrap() .读取数据文件(&test_data_path(), Default::default()) .unwrap(); - let obs_ref = obs.read().unwrap(); + let obs_ref = obs.read(); // 交替读写 RefCell 字段 — 先读再写同字段,分离 borrow 作用域 if !obs_ref.笔序列.is_empty() { let bi = &obs_ref.笔序列[0]; // 读 - let old_mode = bi.模式.read().unwrap().clone(); + let old_mode = bi.模式.read().clone(); // Ref 已释放,可以写 - *bi.模式.write().unwrap() = "测试模式".into(); - let new_mode = bi.模式.read().unwrap().clone(); + *bi.模式.write() = "测试模式".into(); + let new_mode = bi.模式.read().clone(); assert_eq!(new_mode, "测试模式"); // 恢复 - *bi.模式.write().unwrap() = old_mode; + *bi.模式.write() = old_mode; // 读武 - let old_wu = bi.武.read().unwrap().clone(); + let old_wu = bi.武.read().clone(); // Ref 已释放,可以检查 - assert!(Arc::as_ptr(&old_wu) == Arc::as_ptr(&old_wu)); + assert!(Arc::ptr_eq(&old_wu, &old_wu)); } } @@ -1077,10 +1075,9 @@ mod tests { fn test_缠K到分型的Rc指针一致性() { let obs = 观察者::new("btcusd".into(), 300, Default::default()); obs.write() - .unwrap() .读取数据文件(&test_data_path(), Default::default()) .unwrap(); - let obs_ref = obs.read().unwrap(); + let obs_ref = obs.read(); // 每个分型的左/中/右 缠K 指针必须在 缠论K线序列 中 for (i, f) in obs_ref.分型序列.iter().enumerate() { @@ -1196,7 +1193,7 @@ mod tests { 99 }); assert_eq!(handle.join().unwrap(), 99); - assert_eq!(dash.标识.read().unwrap().as_str(), "笔"); + assert_eq!(dash.标识.read().as_str(), "笔"); } /// 测试:Arc<中枢> 可跨线程传递 @@ -1222,11 +1219,11 @@ mod tests { let obs3 = Arc::clone(&obs); let h1 = std::thread::spawn(move || { - let guard = obs2.read().unwrap(); + let guard = obs2.read(); guard.符号.clone() }); let h2 = std::thread::spawn(move || { - let guard = obs3.read().unwrap(); + let guard = obs3.read(); guard.周期 }); @@ -1274,7 +1271,7 @@ mod tests { let obs = 观察者::new("ethusd".into(), 7200, Default::default()); let handle = std::thread::spawn(move || { - let guard = obs.read().unwrap(); + let guard = obs.read(); (guard.符号.clone(), guard.周期) }); @@ -1288,7 +1285,7 @@ mod tests { let mut config = 缠论配置::default(); config.加载文件路径 = test_data_path(); let obs = 观察者::new("btcusd".into(), 300, config); - let mut obs_w = obs.write().unwrap(); + let mut obs_w = obs.write(); obs_w.线段分析层次 = 0; obs_w.重置基础序列(); drop(obs_w); @@ -1299,11 +1296,11 @@ mod tests { for i in 0..(data.len() / size).min(500) { let offset = i * size; if let Some(k线) = K线::from_bytes(&data[offset..offset + size], 300, "btcusd") { - obs.write().unwrap().增加原始K线(k线); + obs.write().增加原始K线(k线); } } - let obs_r = obs.read().unwrap(); + let obs_r = obs.read(); assert!(obs_r.缠论K线序列.len() > 0, "缠K序列应有数据"); assert!(obs_r.分型序列.len() > 0, "分型序列应有数据"); assert!(obs_r.线段序列组.is_empty(), "线段序列组应为空"); @@ -1329,17 +1326,16 @@ mod tests { // 先正常投喂数据 obs.write() - .unwrap() .读取数据文件(&test_data_path(), Default::default()) .unwrap(); // 设为0后执行静态重新分析,不应 panic - let mut obs_w = obs.write().unwrap(); + let mut obs_w = obs.write(); obs_w.线段分析层次 = 0; obs_w.静态重新分析(); drop(obs_w); - let obs_r = obs.read().unwrap(); + let obs_r = obs.read(); assert!(obs_r.分型序列.len() > 0, "静态重新分析后分型序列应有数据"); assert!(obs_r.线段序列组.is_empty(), "线段序列组应为空"); assert!( @@ -1352,4 +1348,104 @@ mod tests { obs_r.笔序列.len() ); } + + /// 创建随机配置(与 main.py 随机配置 对齐) + fn 随机配置() -> crate::config::缠论配置 { + let mut cfg = crate::config::缠论配置::default().不推送(); + cfg.缠K合并替换 = fastrand::bool(); + cfg.笔内元素数量 = fastrand::i64(3..=9); + cfg.笔内相同终点取舍 = fastrand::bool(); + cfg.笔内起始分型包含整笔 = fastrand::bool(); + cfg.笔内原始K线包含整笔 = fastrand::bool(); + cfg.笔次级成笔 = fastrand::bool(); + cfg.笔弱化 = fastrand::bool(); + cfg.笔弱化_原始数量 = fastrand::i64(3..=9); + cfg.线段_非缺口下穿刺 = fastrand::bool(); + cfg.线段_特征序列忽视老阴老阳 = fastrand::bool(); + cfg.线段_修正 = fastrand::bool(); + cfg.线段_缺口后紧急修正 = fastrand::bool(); + cfg.扩展线段_当下分析 = fastrand::bool(); + cfg.买卖点激进识别 = fastrand::bool(); + cfg.买卖点与MACD柱强相关 = fastrand::bool(); + cfg + } + + /// 单线程工作函数(与 main.py 运行单个回测 对齐) + fn 运行单个回测(线程编号: usize, limit: usize) { + let 配置 = 随机配置(); + let 观察员 = + crate::business::observer::观察者::new(format!("btcusd_{}", 线程编号), 300, 配置); + let 方向池 = [ + crate::types::相对方向::向上, + crate::types::相对方向::向上缺口, + crate::types::相对方向::衔接向上, + crate::types::相对方向::向下, + crate::types::相对方向::向下缺口, + crate::types::相对方向::衔接向下, + ]; + let mut 原始K线 = crate::kline::bar::K线::创建普K( + &format!("btcusd_{}", 线程编号), + 1218124800 + 线程编号 as i64 * 300, + 8888.55, + 10000.00, + 9000.22, + 9527.33, + 888.0, + 0, + 300, + ); + 观察员.write().增加原始K线(原始K线.clone()); + let 方向序列 = crate::types::相对方向::从序列中机选(limit, &方向池, true); + for 方向 in &方向序列 { + 原始K线 = 原始K线.根据当前K线生成新K线(*方向, false); + 观察员.write().增加原始K线(原始K线.clone()); + } + } + + #[test] + fn test_50线程压测_10000K线() { + let 线程数 = 50usize; + let 每线程K线数 = 10000usize; + let start = std::time::Instant::now(); + let mut 句柄列表 = Vec::with_capacity(线程数); + let 异常计数 = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)); + for i in 1..=线程数 { + let 异常计数_ref = 异常计数.clone(); + 句柄列表.push(std::thread::spawn(move || { + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + 运行单个回测(i, 每线程K线数); + })); + if result.is_err() { + 异常计数_ref.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + } + })); + } + for h in 句柄列表 { + let _ = h.join(); + } + let 耗时 = start.elapsed(); + let 总K线数 = 线程数 * (每线程K线数 + 1); + let 异常数 = 异常计数.load(std::sync::atomic::Ordering::Relaxed); + eprintln!( + "核心层 {}线程×{}K线 = {} 次 耗时 {:.2?} ({:.0} K/s) 异常线程: {}", + 线程数, + 每线程K线数, + 总K线数, + 耗时, + 总K线数 as f64 / 耗时.as_secs_f64(), + 异常数 + ); + } + + #[test] + fn test_单线程基准_10000K线() { + let start = std::time::Instant::now(); + 运行单个回测(0, 10000); + let 耗时 = start.elapsed(); + eprintln!( + "核心层 单线程 10000 K线 耗时 {:.2?} ({:.0} K/s)", + 耗时, + 10001.0 / 耗时.as_secs_f64() + ); + } } diff --git a/chanlun/src/business/synthesizer.rs b/chanlun/src/business/synthesizer.rs index 0d79191..96f671c 100644 --- a/chanlun/src/business/synthesizer.rs +++ b/chanlun/src/business/synthesizer.rs @@ -23,8 +23,8 @@ */ use crate::kline::bar::K线; +use crate::warn; use std::collections::HashMap; -use tracing; /// 事件回调类型 — fn(信号类型, 标识, 周期, 完成K线) type 合成器事件回调 = Box; @@ -178,7 +178,7 @@ impl K线合成器 { .map(|s| s.to_string()) .or_else(|| e.downcast_ref::().cloned()) .unwrap_or_else(|| "未知错误".into()); - tracing::error!("K线合成器 事件回调 异常: {}", msg); + warn!("K线合成器 事件回调 异常: {}", msg); } } } diff --git a/chanlun/src/config.rs b/chanlun/src/config.rs index eefe08a..a9cbc2d 100644 --- a/chanlun/src/config.rs +++ b/chanlun/src/config.rs @@ -22,9 +22,9 @@ * SOFTWARE. */ +use crate::warn; use serde::{Deserialize, Deserializer, Serialize}; use std::collections::HashMap; -use tracing::warn; /// 缠论配置 —— 控制所有分析阶段的行为 /// @@ -92,103 +92,34 @@ pub struct 缠论配置 { // ---- 指标 ---- /// 是否计算技术指标 pub 计算指标: bool, - /// 是否计算布林带 - pub 计算BOLL: bool, - /// 指标计算方式(开/高/低/收/高低均值/高低收均值/开高低收均值) + /// 指标计算方式(均线使用,MACD/RSI/KDJ/BOLL 在参数元组中指定) #[serde(deserialize_with = "deserialize_指标计算方式")] pub 指标计算方式: String, - // ---- MACD ---- - /// MACD 快线 EMA 周期 - pub 平滑异同移动平均线_快线周期: i64, - /// MACD 慢线 EMA 周期 - pub 平滑异同移动平均线_慢线周期: i64, - /// MACD 信号线周期 - pub 平滑异同移动平均线_信号周期: i64, - /// MACD 多参数列表: Vec<(key, 快线, 慢线, 信号)> + /// MACD 参数列表 (key, 计算方式, 快线, 慢线, 信号) #[serde(default)] - pub MACD_参数列表: Vec<(String, i64, i64, i64)>, + pub MACD_参数列表: Vec<(String, String, i64, i64, i64)>, - // ---- RSI ---- - /// RSI 计算周期 - pub 相对强弱指数_周期: i64, - /// RSI SMA 平滑周期 - pub 相对强弱指数_移动平均线周期: i64, - /// RSI 超买阈值 - pub 相对强弱指数_超买阈值: f64, - /// RSI 超卖阈值 - pub 相对强弱指数_超卖阈值: f64, - /// RSI 多周期列表: Vec<(key, 周期)> + /// RSI 参数列表 (key, 计算方式, 周期, MA周期, 超买, 超卖) #[serde(default)] - pub RSI_周期列表: Vec<(String, i64)>, + pub RSI_周期列表: Vec<(String, String, i64, i64, f64, f64)>, - // ---- KDJ ---- - /// KDJ RSV 周期 - pub 随机指标_RSV周期: i64, - /// KDJ K 值平滑周期 - pub 随机指标_K值平滑周期: i64, - /// KDJ D 值平滑周期 - pub 随机指标_D值平滑周期: i64, - /// KDJ 超买阈值 - pub 随机指标_超买阈值: f64, - /// KDJ 超卖阈值 - pub 随机指标_超卖阈值: f64, - /// KDJ 多参数列表: Vec<(key, RSV周期, K平滑, D平滑)> + /// KDJ 参数列表 (key, 计算方式, RSV, K平滑, D平滑, 超买, 超卖) #[serde(default)] - pub KDJ_参数列表: Vec<(String, i64, i64, i64)>, + pub KDJ_参数列表: Vec<(String, String, i64, i64, i64, f64, f64)>, - // ---- BOLL ---- - /// 布林带周期 - pub 布林带_周期: i64, - /// 布林带标准差倍数 - pub 布林带_标准差倍数: f64, - /// BOLL 多参数列表: Vec<(key, 周期, 标准差倍数)> + /// BOLL 参数列表 (key, 计算方式, 周期, 标准差倍数) #[serde(default)] - pub BOLL_参数列表: Vec<(String, i64, f64)>, + pub BOLL_参数列表: Vec<(String, String, i64, f64)>, - // ---- 均线 ---- - /// 均线类型列表: ["SMA", "EMA", ...] + /// 均线参数列表 (key, 计算方式, 类型, 周期) — 如 ("SMA_5", "收", "SMA", 5) #[serde(default)] - pub 均线_类型列表: Vec, - /// 均线周期列表: [5, 10, 20, ...] - #[serde(default)] - pub 均线_周期列表: Vec, + pub 均线参数列表: Vec<(String, String, String, i64)>, // ---- 推送/显示 ---- - /// 是否启用图表展示 pub 图表展示: bool, - /// 是否推送K线 - pub 推送K线: bool, - /// 是否推送笔 - pub 推送笔: bool, - /// 是否推送线段 - pub 推送线段: bool, - /// 是否推送中枢 - pub 推送中枢: bool, - - // ---- 图表展示细分 ---- - /// 图表展示笔 - pub 图表展示_笔: bool, - /// 图表展示线段 - pub 图表展示_线段: bool, - /// 图表展示扩展线段 - pub 图表展示_扩展线段: bool, - /// 图表展示扩展线段(线段级) - pub 图表展示_扩展线段_线段: bool, - /// 图表展示线段之线段 - pub 图表展示_线段_线段: bool, - /// 图表展示笔中枢 - pub 图表展示_中枢_笔: bool, - /// 图表展示线段中枢 - pub 图表展示_中枢_线段: bool, - /// 图表展示扩展中枢 - pub 图表展示_中枢_扩展线段: bool, - /// 图表展示扩展中枢(线段级) - pub 图表展示_中枢_扩展线段_线段: bool, - /// 图表展示线段之中枢 - pub 图表展示_中枢_线段_线段: bool, - /// 图表展示线段内部中枢 - pub 图表展示_中枢_线段内部: bool, + /// 图表展示标签: None=全部, [] = 不展示 + pub 图表展示标签: Option>, // ---- 买卖点 ---- /// 买卖点偏移量 @@ -310,44 +241,14 @@ impl Default for 缠论配置 { 分析线段中枢: true, 手动终止: String::new(), 计算指标: true, - 计算BOLL: false, 指标计算方式: "收".into(), - 平滑异同移动平均线_快线周期: 13, - 平滑异同移动平均线_慢线周期: 31, - 平滑异同移动平均线_信号周期: 11, - 相对强弱指数_周期: 13, - 相对强弱指数_移动平均线周期: 13, - 相对强弱指数_超买阈值: 75.0, - 相对强弱指数_超卖阈值: 25.0, - 随机指标_RSV周期: 13, - 随机指标_K值平滑周期: 5, - 随机指标_D值平滑周期: 5, - 随机指标_超买阈值: 80.0, - 随机指标_超卖阈值: 20.0, - MACD_参数列表: Vec::new(), - RSI_周期列表: Vec::new(), - KDJ_参数列表: Vec::new(), - 布林带_周期: 20, - 布林带_标准差倍数: 2.0, - BOLL_参数列表: Vec::new(), - 均线_类型列表: Vec::new(), - 均线_周期列表: Vec::new(), + MACD_参数列表: vec![("macd".into(), "收".into(), 13, 31, 11)], + RSI_周期列表: vec![("rsi".into(), "收".into(), 14, 13, 75.0, 25.0)], + KDJ_参数列表: vec![("kdj".into(), "收".into(), 13, 5, 5, 80.0, 20.0)], + BOLL_参数列表: vec![("boll".into(), "收".into(), 20, 2.0)], + 均线参数列表: Vec::new(), 图表展示: true, - 推送K线: true, - 推送笔: true, - 推送线段: true, - 推送中枢: true, - 图表展示_笔: true, - 图表展示_线段: true, - 图表展示_扩展线段: true, - 图表展示_扩展线段_线段: true, - 图表展示_线段_线段: true, - 图表展示_中枢_笔: true, - 图表展示_中枢_线段: true, - 图表展示_中枢_扩展线段: true, - 图表展示_中枢_扩展线段_线段: true, - 图表展示_中枢_线段_线段: true, - 图表展示_中枢_线段内部: true, + 图表展示标签: None, 买卖点偏移: 1, 买卖点激进识别: false, 买卖点与MACD柱强相关: false, @@ -366,46 +267,42 @@ impl Default for 缠论配置 { } impl 缠论配置 { - /// 解析MACD参数列表 — 如果列表非空则使用列表,否则返回默认单组 - pub fn _解析MACD参数列表(&self) -> Vec<(String, i64, i64, i64)> { - if !self.MACD_参数列表.is_empty() { - return self.MACD_参数列表.clone(); + /// 展示标签判定 — None=全部, [] = 全关 + pub fn 展示标签(&self, 标签: &str) -> bool { + match &self.图表展示标签 { + None => true, + Some(tags) => tags.iter().any(|t| t == 标签), } - vec![( - "macd".into(), - self.平滑异同移动平均线_快线周期, - self.平滑异同移动平均线_慢线周期, - self.平滑异同移动平均线_信号周期, - )] } - /// 解析RSI周期列表 — 如果列表非空则使用列表,否则返回默认单组 - pub fn _解析RSI周期列表(&self) -> Vec<(String, i64)> { - if !self.RSI_周期列表.is_empty() { - return self.RSI_周期列表.clone(); + /// 统一设置所有指标参数(对应 Python 设置指标)。 + /// + /// 各参数为 None 时不修改对应字段;非 None 时替换对应参数列表。 + /// 调用后自动将 `计算指标` 设为 `true`。 + pub fn 设置指标( + &mut self, + 均线: Option>, + MACD: Option>, + RSI: Option>, + KDJ: Option>, + BOLL: Option>, + ) { + self.计算指标 = true; + if let Some(v) = 均线 { + self.均线参数列表 = v; } - vec![("rsi".into(), self.相对强弱指数_周期)] - } - - /// 解析KDJ参数列表 — 如果列表非空则使用列表,否则返回默认单组 - pub fn _解析KDJ参数列表(&self) -> Vec<(String, i64, i64, i64)> { - if !self.KDJ_参数列表.is_empty() { - return self.KDJ_参数列表.clone(); + if let Some(v) = MACD { + self.MACD_参数列表 = v; } - vec![( - "kdj".into(), - self.随机指标_RSV周期, - self.随机指标_K值平滑周期, - self.随机指标_D值平滑周期, - )] - } - - /// 解析BOLL参数列表 — 如果列表非空则使用列表,否则返回默认单组 - pub fn _解析BOLL参数列表(&self) -> Vec<(String, i64, f64)> { - if !self.BOLL_参数列表.is_empty() { - return self.BOLL_参数列表.clone(); + if let Some(v) = RSI { + self.RSI_周期列表 = v; + } + if let Some(v) = KDJ { + self.KDJ_参数列表 = v; + } + if let Some(v) = BOLL { + self.BOLL_参数列表 = v; } - vec![("boll".into(), self.布林带_周期, self.布林带_标准差倍数)] } /// 序列化为 JSON 字典(对应 Python to_dict,仅返回 model_fields 中的字段) @@ -461,8 +358,11 @@ impl 缠论配置 { /// 返回字段名列表(对应 Python model_fields().keys()) pub fn model_fields() -> &'static [&'static str] { &[ + // ---- 基础 ---- "标识", + // ---- 缠K ---- "缠K合并替换", + // ---- 笔 ---- "笔内元素数量", "笔内相同终点取舍", "笔内起始分型包含整笔", @@ -471,57 +371,33 @@ impl 缠论配置 { "笔次级成笔", "笔弱化", "笔弱化_原始数量", + // ---- 线段 ---- "线段_非缺口下穿刺", "线段_特征序列忽视老阴老阳", "线段_缺口后紧急修正", "线段_修正", "线段内部中枢图显", "扩展线段_当下分析", + // ---- 分析开关 ---- "分析笔", "分析线段", "分析扩展线段", "分析笔中枢", "分析线段中枢", + // ---- 终止 ---- "手动终止", + // ---- 指标 ---- "计算指标", - "计算BOLL", "指标计算方式", - "平滑异同移动平均线_快线周期", - "平滑异同移动平均线_慢线周期", - "平滑异同移动平均线_信号周期", - "相对强弱指数_周期", - "相对强弱指数_移动平均线周期", - "相对强弱指数_超买阈值", - "相对强弱指数_超卖阈值", - "随机指标_RSV周期", - "随机指标_K值平滑周期", - "随机指标_D值平滑周期", - "随机指标_超买阈值", - "随机指标_超卖阈值", - "布林带_周期", - "布林带_标准差倍数", "MACD_参数列表", "RSI_周期列表", "KDJ_参数列表", "BOLL_参数列表", - "均线_类型列表", - "均线_周期列表", + "均线参数列表", + // ---- 推送/显示 ---- "图表展示", - "推送K线", - "推送笔", - "推送线段", - "推送中枢", - "图表展示_笔", - "图表展示_线段", - "图表展示_扩展线段", - "图表展示_扩展线段_线段", - "图表展示_线段_线段", - "图表展示_中枢_笔", - "图表展示_中枢_线段", - "图表展示_中枢_扩展线段", - "图表展示_中枢_扩展线段_线段", - "图表展示_中枢_线段_线段", - "图表展示_中枢_线段内部", + "图表展示标签", + // ---- 买卖点 ---- "买卖点偏移", "买卖点激进识别", "买卖点与MACD柱强相关", @@ -530,10 +406,12 @@ impl 缠论配置 { "买卖点_指标匹配_MACD", "买卖点_指标匹配_KDJ", "买卖点_指标匹配_RSI", + // ---- 背驰 ---- "线段内部背驰_MACD", "线段内部背驰_斜率", "线段内部背驰_测度", "线段内部背驰_模式", + // ---- 文件 ---- "加载文件路径", ] } @@ -576,21 +454,7 @@ impl 缠论配置 { Self { 线段内部中枢图显: false, 图表展示: false, - 推送K线: false, - 推送笔: false, - 推送线段: false, - 推送中枢: false, - 图表展示_笔: false, - 图表展示_线段: false, - 图表展示_扩展线段: false, - 图表展示_扩展线段_线段: false, - 图表展示_线段_线段: false, - 图表展示_中枢_笔: false, - 图表展示_中枢_线段: false, - 图表展示_中枢_扩展线段: false, - 图表展示_中枢_扩展线段_线段: false, - 图表展示_中枢_线段_线段: false, - 图表展示_中枢_线段内部: false, + 图表展示标签: Some(vec![]), ..self.clone() } } @@ -752,18 +616,15 @@ mod tests { fn test_model_copy() { let mut update = std::collections::HashMap::new(); update.insert("标识".into(), serde_json::json!("custom")); - update.insert("推送K线".into(), serde_json::json!(false)); update.insert("笔内元素数量".into(), serde_json::json!(10)); let config = 缠论配置::default(); let copied = config.model_copy(&update); assert_eq!(copied.标识, "custom"); - assert!(!copied.推送K线); assert_eq!(copied.笔内元素数量, 10); // 未指定字段保持不变 assert_eq!(copied.买卖点偏移, 1); - assert!(copied.推送笔); } #[test] @@ -779,9 +640,8 @@ mod tests { fn test_不推送() { let config = 缠论配置::default(); let muted = config.不推送(); - assert!(!muted.推送K线); - assert!(!muted.推送笔); assert!(!muted.图表展示); + assert!(!muted.线段内部中枢图显); assert_eq!(muted.笔内元素数量, 5); } @@ -795,7 +655,7 @@ mod tests { #[test] fn test_对比_有差异() { - let mut a = 缠论配置::default(); + let a = 缠论配置::default(); let mut b = 缠论配置::default(); b.标识 = "changed".into(); b.笔内元素数量 = 99; @@ -854,14 +714,14 @@ mod tests { #[test] fn test_对比_boolean_difference() { - let mut a = 缠论配置::default(); + let a = 缠论配置::default(); let mut b = 缠论配置::default(); - b.推送K线 = false; + b.分析笔 = false; b.图表展示 = false; let diff = a.对比(&b); assert_eq!(diff.len(), 2); - assert_eq!(diff["推送K线"], serde_json::json!(false)); + assert_eq!(diff["分析笔"], serde_json::json!(false)); assert_eq!(diff["图表展示"], serde_json::json!(false)); } diff --git a/chanlun/src/indicators/boll.rs b/chanlun/src/indicators/boll.rs index 783221e..b9589ee 100644 --- a/chanlun/src/indicators/boll.rs +++ b/chanlun/src/indicators/boll.rs @@ -24,6 +24,7 @@ use crate::kline::bar::K线; use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; /// 布林带(BOLL)— 基于移动平均和标准差的波动率通道 #[derive(Debug, Clone, Serialize, Deserialize)] @@ -43,7 +44,7 @@ pub struct 布林带 { pub 下轨: f64, /// 内部历史队列(不序列化) #[serde(skip)] - _历史队列: Vec, + _历史队列: VecDeque, /// 内部均值缓存(不序列化) #[serde(skip)] _均值: f64, @@ -61,7 +62,7 @@ impl Default for 布林带 { 上轨: 0.0, 中轨: 0.0, 下轨: 0.0, - _历史队列: Vec::new(), + _历史队列: VecDeque::new(), _均值: 0.0, _方差和: 0.0, } @@ -98,7 +99,7 @@ impl 布林带 { 上轨: 价格, 中轨: 价格, 下轨: 价格, - _历史队列: vec![价格], + _历史队列: VecDeque::from([价格]), _均值: 价格, _方差和: 0.0, } @@ -110,9 +111,9 @@ impl 布林带 { let 标准差倍数 = prev.标准差倍数; let mut q = prev._历史队列.clone(); - q.push(价格); + q.push_back(价格); if q.len() > 周期 { - q.remove(0); + q.pop_front(); } let (_均值, _方差和) = if q.len() < 周期 { diff --git a/chanlun/src/indicators/calculator.rs b/chanlun/src/indicators/calculator.rs index 1515809..8c6937e 100644 --- a/chanlun/src/indicators/calculator.rs +++ b/chanlun/src/indicators/calculator.rs @@ -32,44 +32,56 @@ use std::sync::Arc; pub struct 指标计算器; impl 指标计算器 { - /// 增量计算所有开启的指标,将结果写入 当前K线.指标 + /// 增量计算所有开启的指标,将结果写入每一根 K 线。 /// - /// `全序列` 包含当前K线(在末尾);prev 取自 全序列[..-1].last() - /// 通过 RwLock 内部可变性,以 `&K线` 共享引用写入指标值 pub fn 计算并挂载(全序列: &[Arc], 配置: &缠论配置) { let n = 全序列.len(); - let 当前K线 = &全序列[n - 1]; - let 现有序列 = if n > 1 { &全序列[..n - 1] } else { &[] }; + if n == 0 { + return; + } + if !配置.计算指标 && 配置.均线参数列表.is_empty() { + return; + } - // 作用域化 prev_guard:在 _回填新指标 之前释放,避免读锁与回填写锁冲突 - let has_prev; - { - let prev_guard = if n > 1 { - Some(全序列[n - 2].指标.read().unwrap()) - } else { - None - }; - let prev = prev_guard.as_deref(); - if 配置.计算指标 { - Self::_计算MACD组(当前K线, prev, 配置); - Self::_计算RSI组(当前K线, prev, 配置); - Self::_计算KDJ组(当前K线, prev, 配置); - Self::_计算BOLL组(当前K线, prev, 配置); + // 找到第一个 MACD 缺失的 K 线索引,若全部已有则只处理最后一根 + let start = 全序列 + .iter() + .position(|k| k.macd().is_none()) + .unwrap_or(n - 1); + + for i in start..n { + let 当前K线 = &全序列[i]; + let 现有序列 = &全序列[..i]; + + // 确保 prev guard 在写入当前K线前释放 + { + let prev = if i > 0 { + Some(全序列[i - 1].指标.read()) + } else { + None + }; + let prev_deref = prev.as_deref(); + + if 配置.计算指标 { + Self::_计算MACD组(当前K线, prev_deref, 配置); + Self::_计算RSI组(当前K线, prev_deref, 配置); + Self::_计算KDJ组(当前K线, prev_deref, 配置); + Self::_计算BOLL组(当前K线, prev_deref, 配置); + } + Self::_更新均线(当前K线, 现有序列, 配置); + // prev guard dropped here } - Self::_更新均线(当前K线, 现有序列, 配置); - has_prev = n > 1; - } // prev_guard dropped here + } - if has_prev { + // 回填:若有新增指标参数但首K线未被本轮计算覆盖,仍需填充历史K线 + if n > 1 && start > 0 { Self::_回填新指标(全序列, 配置); } } fn _计算MACD组(当前K线: &K线, prev: Option<&指标容器>, 配置: &缠论配置) { - let 计算方式 = &配置.指标计算方式; - for (i, (key, 快, 慢, 信号)) in 配置._解析MACD参数列表().into_iter().enumerate() - { - let val = if let Some(prev_val) = prev.and_then(|p| p.获取(&key)) { + for (key, 计算方式, 快, 慢, 信号) in 配置.MACD_参数列表.iter() { + let val = if let Some(prev_val) = prev.and_then(|p| p.获取(key)) { if let 指标值::MACD(prev_macd) = prev_val { 指标值::MACD(平滑异同移动平均线::增量计算( prev_macd, @@ -95,22 +107,19 @@ impl 指标计算器 { 计算方式, ), 当前K线.时间戳, - 快, - 慢, - 信号, + *快, + *慢, + *信号, )) }; - 当前K线.指标.write().unwrap().设置(&key, val.clone()); - if i == 0 { - 当前K线.指标.write().unwrap().设置("macd", val); - } + 当前K线.指标.write().设置(key, val.clone()); } } fn _计算RSI组(当前K线: &K线, prev: Option<&指标容器>, 配置: &缠论配置) { - let 计算方式 = &配置.指标计算方式; - for (i, (key, 周期)) in 配置._解析RSI周期列表().into_iter().enumerate() { - let val = if let Some(prev_val) = prev.and_then(|p| p.获取(&key)) { + for (key, 计算方式, 周期, ma周期, 超买, 超卖) in 配置.RSI_周期列表.iter() + { + let val = if let Some(prev_val) = prev.and_then(|p| p.获取(key)) { if let 指标值::RSI(prev_rsi) = prev_val { 指标值::RSI(相对强弱指数::增量计算( prev_rsi, @@ -136,23 +145,19 @@ impl 指标计算器 { 计算方式, ), 当前K线.时间戳, - 周期, - 配置.相对强弱指数_超买阈值, - 配置.相对强弱指数_超卖阈值, - Some(配置.相对强弱指数_移动平均线周期), + *周期, + *超买, + *超卖, + Some(*ma周期), )) }; - 当前K线.指标.write().unwrap().设置(&key, val.clone()); - if i == 0 { - 当前K线.指标.write().unwrap().设置("rsi", val); - } + 当前K线.指标.write().设置(key, val.clone()); } } fn _计算KDJ组(当前K线: &K线, prev: Option<&指标容器>, 配置: &缠论配置) { - for (i, (key, rsv, k平滑, d平滑)) in 配置._解析KDJ参数列表().into_iter().enumerate() - { - let val = if let Some(prev_val) = prev.and_then(|p| p.获取(&key)) { + for (key, _fm, rsv, k平滑, d平滑, 超买, 超卖) in 配置.KDJ_参数列表.iter() { + let val = if let Some(prev_val) = prev.and_then(|p| p.获取(key)) { if let 指标值::KDJ(prev_kdj) = prev_val { 指标值::KDJ(随机指标::增量计算( prev_kdj, @@ -170,25 +175,20 @@ impl 指标计算器 { 当前K线.低, 当前K线.收盘价, 当前K线.时间戳, - rsv, - k平滑, - d平滑, - 配置.随机指标_超买阈值, - 配置.随机指标_超卖阈值, + *rsv, + *k平滑, + *d平滑, + *超买, + *超卖, )) }; - 当前K线.指标.write().unwrap().设置(&key, val.clone()); - if i == 0 { - 当前K线.指标.write().unwrap().设置("kdj", val); - } + 当前K线.指标.write().设置(key, val.clone()); } } fn _计算BOLL组(当前K线: &K线, prev: Option<&指标容器>, 配置: &缠论配置) { - let 计算方式 = &配置.指标计算方式; - for (i, (key, 周期, 标准差倍数)) in 配置._解析BOLL参数列表().into_iter().enumerate() - { - let val = if let Some(prev_val) = prev.and_then(|p| p.获取(&key)) { + for (key, 计算方式, 周期, 标准差倍数) in 配置.BOLL_参数列表.iter() { + let val = if let Some(prev_val) = prev.and_then(|p| p.获取(key)) { if let 指标值::BOLL(prev_boll) = prev_val { 指标值::BOLL(布林带::增量计算( prev_boll, @@ -214,22 +214,37 @@ impl 指标计算器 { 当前K线.收盘价, 计算方式, ), - 周期 as usize, - 标准差倍数, + *周期 as usize, + *标准差倍数, )) }; - 当前K线.指标.write().unwrap().设置(&key, val.clone()); - if i == 0 { - 当前K线.指标.write().unwrap().设置("boll", val); - } + 当前K线.指标.write().设置(key, val.clone()); } } fn _更新均线(当前K线: &K线, 现有序列: &[Arc], 配置: &缠论配置) { - if 配置.均线_类型列表.is_empty() || 配置.均线_周期列表.is_empty() { + if 配置.均线参数列表.is_empty() { return; } - let 计算方式 = &配置.指标计算方式; + for (key, 计算方式, ma_type, period) in &配置.均线参数列表 { + let 值 = match ma_type.as_str() { + "SMA" => Self::_增量SMA(当前K线, 现有序列, 计算方式, *period, key), + "EMA" => Self::_增量EMA(当前K线, 现有序列, 计算方式, *period, key), + _ => continue, + }; + if let Some(均线_map) = 当前K线.指标.write().均线_mut() { + 均线_map.insert(key.clone(), 值); + } + } + } + + fn _增量SMA( + 当前K线: &K线, + 现有序列: &[Arc], + 计算方式: &str, + period: i64, + prev_key: &str, + ) -> f64 { let 当前价 = super::K线取值( 当前K线.开盘价, 当前K线.高, @@ -237,46 +252,20 @@ impl 指标计算器 { 当前K线.收盘价, 计算方式, ); - for ma_type in &配置.均线_类型列表 { - for period in &配置.均线_周期列表 { - let key = format!("{}_{}", ma_type, period); - let 值 = match ma_type.as_str() { - "SMA" => Self::_增量SMA(现有序列, 当前价, *period, 计算方式, &key), - "EMA" => Self::_增量EMA(现有序列, 当前价, *period, 计算方式, &key), - _ => continue, - }; - if let Some(均线_map) = 当前K线.指标.write().unwrap().均线_mut() { - 均线_map.insert(key, 值); - } - } - } - } - - /// 增量 SMA: 现有序列 (不含当前K线) + 当前价 - fn _增量SMA( - 现有序列: &[Arc], - 当前价: f64, - period: i64, - 计算方式: &str, - prev_key: &str, - ) -> f64 { let existing_len = 现有序列.len(); let p = period as usize; - // 现有序列 + 当前 = total_len - let total_len = existing_len + 1; - if total_len <= p { - let mut sum: f64 = 现有序列[existing_len.saturating_sub(p.saturating_sub(1))..] + if existing_len < p { + let sum: f64 = 现有序列 .iter() .map(|k| super::K线取值(k.开盘价, k.高, k.低, k.收盘价, 计算方式)) - .sum(); - sum += 当前价; - return sum / (total_len as f64).max(1.0); + .sum::() + + 当前价; + return sum / ((existing_len + 1) as f64).max(1.0); } - // 尝试从前一根K线获取缓存的SMA - if let Some(prev) = 现有序列.last().and_then(|k| { - let guard = k.指标.read().unwrap(); - guard.均线().and_then(|m| m.get(prev_key)).copied() - }) { + if let Some(prev_sma) = 现有序列 + .last() + .and_then(|k| k.指标.read().均线().and_then(|m| m.get(prev_key)).copied()) + { let oldest = super::K线取值( 现有序列[existing_len - p].开盘价, 现有序列[existing_len - p].高, @@ -284,29 +273,33 @@ impl 指标计算器 { 现有序列[existing_len - p].收盘价, 计算方式, ); - return prev + (当前价 - oldest) / period as f64; + return prev_sma + (当前价 - oldest) / period as f64; } - // 回退:完整计算 - let mut sum: f64 = 现有序列[existing_len.saturating_sub(p.saturating_sub(1))..] + let sum: f64 = 现有序列[existing_len.saturating_sub(p.saturating_sub(1))..] .iter() .map(|k| super::K线取值(k.开盘价, k.高, k.低, k.收盘价, 计算方式)) - .sum(); - sum += 当前价; - sum / (total_len as f64).min(p as f64) + .sum::() + + 当前价; + sum / ((existing_len + 1) as f64).min(p as f64) } - /// 增量 EMA: 现有序列 (不含当前K线) + 当前价 fn _增量EMA( + 当前K线: &K线, 现有序列: &[Arc], - 当前价: f64, + 计算方式: &str, period: i64, - _计算方式: &str, prev_key: &str, ) -> f64 { - let 前值 = 现有序列.last().and_then(|k| { - let guard = k.指标.read().unwrap(); - guard.均线().and_then(|m| m.get(prev_key)).copied() - }); + let 当前价 = super::K线取值( + 当前K线.开盘价, + 当前K线.高, + 当前K线.低, + 当前K线.收盘价, + 计算方式, + ); + let 前值 = 现有序列 + .last() + .and_then(|k| k.指标.read().均线().and_then(|m| m.get(prev_key)).copied()); match 前值 { None => 当前价, Some(prev) => { @@ -318,159 +311,388 @@ impl 指标计算器 { /// 运行中新增指标参数时,回填所有历史K线 fn _回填新指标(全序列: &[Arc], 配置: &缠论配置) { - // 作用域化首尾读锁:在回填写循环之前释放,避免读锁与写锁冲突 let (新MACD, 新RSI, 新KDJ, 新BOLL) = { - let 首K_guard = 全序列[0].指标.read().unwrap(); - let 尾K_guard = 全序列[全序列.len() - 1].指标.read().unwrap(); - + let 首K_guard = 全序列[0].指标.read(); + let 尾K_guard = 全序列[全序列.len() - 1].指标.read(); let 新MACD: Vec<_> = 配置 - ._解析MACD参数列表() - .into_iter() - .filter(|(key, _, _, _)| 尾K_guard.包含(key) && !首K_guard.包含(key)) + .MACD_参数列表 + .iter() + .filter(|(key, ..)| 尾K_guard.包含(key) && !首K_guard.包含(key)) + .cloned() .collect(); let 新RSI: Vec<_> = 配置 - ._解析RSI周期列表() - .into_iter() - .filter(|(key, _)| 尾K_guard.包含(key) && !首K_guard.包含(key)) + .RSI_周期列表 + .iter() + .filter(|(key, ..)| 尾K_guard.包含(key) && !首K_guard.包含(key)) + .cloned() .collect(); let 新KDJ: Vec<_> = 配置 - ._解析KDJ参数列表() - .into_iter() - .filter(|(key, _, _, _)| 尾K_guard.包含(key) && !首K_guard.包含(key)) + .KDJ_参数列表 + .iter() + .filter(|(key, ..)| 尾K_guard.包含(key) && !首K_guard.包含(key)) + .cloned() .collect(); let 新BOLL: Vec<_> = 配置 - ._解析BOLL参数列表() - .into_iter() - .filter(|(key, _, _)| 尾K_guard.包含(key) && !首K_guard.包含(key)) + .BOLL_参数列表 + .iter() + .filter(|(key, ..)| 尾K_guard.包含(key) && !首K_guard.包含(key)) + .cloned() .collect(); - (新MACD, 新RSI, 新KDJ, 新BOLL) - }; // 首K_guard, 尾K_guard dropped here + }; if 新MACD.is_empty() && 新RSI.is_empty() && 新KDJ.is_empty() && 新BOLL.is_empty() { return; } - let 计算方式 = &配置.指标计算方式; - - // 从第一根K线开始逐根回填,每次只持有一根prev读锁 for i in 0..全序列.len() { let k线 = &全序列[i]; let prev_guard = if i > 0 { - Some(全序列[i - 1].指标.read().unwrap()) + Some(全序列[i - 1].指标.read()) } else { None }; - for (key, 快, 慢, 信号) in &新MACD { - let val = if let Some(ref prev) = prev_guard { - if let Some(指标值::MACD(prev_macd)) = prev.获取(key) { - 指标值::MACD(平滑异同移动平均线::增量计算_K线( - prev_macd, - k线, - 计算方式, - )) - } else { - 指标值::MACD(平滑异同移动平均线::首次计算_K线( - k线, - 计算方式, - *快, - *慢, - *信号, - )) - } - } else { - 指标值::MACD(平滑异同移动平均线::首次计算_K线( + for (key, 计算方式, 快, 慢, 信号) in &新MACD { + let val = match prev_guard.as_ref().and_then(|p| p.获取(key)) { + Some(指标值::MACD(prev_macd)) => 指标值::MACD( + 平滑异同移动平均线::增量计算_K线(prev_macd, k线, 计算方式), + ), + _ => 指标值::MACD(平滑异同移动平均线::首次计算_K线( k线, 计算方式, *快, *慢, *信号, - )) + )), }; - k线.指标.write().unwrap().设置(key, val); + k线.指标.write().设置(key, val); } - for (key, 周期) in &新RSI { - let val = if let Some(ref prev) = prev_guard { - if let Some(指标值::RSI(prev_rsi)) = prev.获取(key) { - 指标值::RSI(相对强弱指数::增量计算_K线( - prev_rsi, - k线, - 计算方式, - )) - } else { - 指标值::RSI(相对强弱指数::首次计算_K线( - k线, - 计算方式, - *周期, - 配置.相对强弱指数_超买阈值, - 配置.相对强弱指数_超卖阈值, - Some(配置.相对强弱指数_移动平均线周期), - )) - } - } else { - 指标值::RSI(相对强弱指数::首次计算_K线( + for (key, 计算方式, 周期, ma周期, 超买, 超卖) in &新RSI { + let val = match prev_guard.as_ref().and_then(|p| p.获取(key)) { + Some(指标值::RSI(prev_rsi)) => 指标值::RSI( + 相对强弱指数::增量计算_K线(prev_rsi, k线, 计算方式), + ), + _ => 指标值::RSI(相对强弱指数::首次计算_K线( k线, 计算方式, *周期, - 配置.相对强弱指数_超买阈值, - 配置.相对强弱指数_超卖阈值, - Some(配置.相对强弱指数_移动平均线周期), - )) + *超买, + *超卖, + Some(*ma周期), + )), }; - k线.指标.write().unwrap().设置(key, val); + k线.指标.write().设置(key, val); } - for (key, rsv, k平滑, d平滑) in &新KDJ { - let val = if let Some(ref prev) = prev_guard { - if let Some(指标值::KDJ(prev_kdj)) = prev.获取(key) { + for (key, _fm, rsv, k平滑, d平滑, 超买, 超卖) in &新KDJ { + let val = match prev_guard.as_ref().and_then(|p| p.获取(key)) { + Some(指标值::KDJ(prev_kdj)) => { 指标值::KDJ(随机指标::增量计算_K线(prev_kdj, k线)) - } else { - 指标值::KDJ(随机指标::首次计算_K线( - k线, - *rsv, - *k平滑, - *d平滑, - 配置.随机指标_超买阈值, - 配置.随机指标_超卖阈值, - )) } - } else { - 指标值::KDJ(随机指标::首次计算_K线( - k线, - *rsv, - *k平滑, - *d平滑, - 配置.随机指标_超买阈值, - 配置.随机指标_超卖阈值, - )) + _ => 指标值::KDJ(随机指标::首次计算_K线( + k线, *rsv, *k平滑, *d平滑, *超买, *超卖, + )), }; - k线.指标.write().unwrap().设置(key, val); + k线.指标.write().设置(key, val); } - for (key, 周期, 标准差倍数) in &新BOLL { - let val = if let Some(ref prev) = prev_guard { - if let Some(指标值::BOLL(prev_boll)) = prev.获取(key) { + for (key, 计算方式, 周期, 标准差倍数) in &新BOLL { + let val = match prev_guard.as_ref().and_then(|p| p.获取(key)) { + Some(指标值::BOLL(prev_boll)) => { 指标值::BOLL(布林带::增量计算_K线(prev_boll, k线, 计算方式)) - } else { - 指标值::BOLL(布林带::首次计算_K线( - k线, - 计算方式, - *周期 as usize, - *标准差倍数, - )) } - } else { - 指标值::BOLL(布林带::首次计算_K线( + _ => 指标值::BOLL(布林带::首次计算_K线( k线, 计算方式, *周期 as usize, *标准差倍数, - )) + )), }; - k线.指标.write().unwrap().设置(key, val); + k线.指标.write().设置(key, val); } } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::缠论配置; + use crate::kline::bar::K线; + use std::sync::Arc; + + /// 辅助:创建一根模拟 K 线 + fn 模拟K线(时间戳: i64, 开: f64, 高: f64, 低: f64, 收: f64, 量: f64) -> Arc { + Arc::new(K线::创建普K("TEST", 时间戳, 开, 高, 低, 收, 量, 0, 300)) + } + + /// 辅助:生成连续上涨的 K 线序列(每根涨 ~1%) + fn 生成上涨序列(n: usize, 起始时间: i64, 起始价: f64) -> Vec> { + let mut seq = Vec::with_capacity(n); + let mut price = 起始价; + for i in 0..n { + let 开 = price; + let 收 = price * 1.005; // 上涨 0.5% + let 高 = 收 * 1.002; + let 低 = 开 * 0.998; + let 量 = 1000.0 + i as f64 * 10.0; + seq.push(模拟K线(起始时间 + i as i64 * 300, 开, 高, 低, 收, 量)); + price = 收; + } + seq + } + + #[test] + fn test_单根K线_首次计算_挂载成功() { + let k线 = 模拟K线(1000, 100.0, 102.0, 98.0, 101.0, 500.0); + let seq = vec![k线.clone()]; + let 配置 = 缠论配置::default(); + + 指标计算器::计算并挂载(&seq, &配置); + + // 单根 K 线首次计算:MACD DIF=0(EMA=SMA 初始近似),柱=0 + let m = k线.macd().expect("MACD 应已挂载"); + assert_eq!(m.DIF, Some(0.0), "首根K线 DIF 应为 0"); + assert_eq!(m.MACD柱, 0.0, "首根K线 MACD柱 应为 0"); + + // RSI 首次计算后 RSI 为 None(需至少一个增量步才有值) + // 但指标容器应已注册 RSI 槽位,boll_cloned() 返回的是字段默认值 + assert!(k线.rsi().is_some(), "RSI 结构体应已创建(即使 RSI 字段为 None)"); + assert!(k线.kdj().is_some(), "KDJ 结构体应已创建(即使 K/D 字段为 None)"); + } + + #[test] + fn test_多根K线_增量计算_指标值递推() { + let seq = 生成上涨序列(5, 1000, 100.0); + let 配置 = 缠论配置::default(); + + // 逐根计算(模拟流式投喂) + for i in 0..seq.len() { + 指标计算器::计算并挂载(&seq[..=i], &配置); + } + + // 第 5 根 K 线的 MACD DIF 应 > 0(持续上涨) + let last = &seq[seq.len() - 1]; + let m = last.macd().expect("最后一根K线 MACD 应已挂载"); + assert!(m.DIF.unwrap() > 0.0, "上涨序列 DIF 应为正"); + + // 所有 K 线均应有 MACD/RSI/KDJ + for (i, k) in seq.iter().enumerate() { + assert!(k.macd().is_some(), "K线[{i}] MACD 缺失"); + assert!(k.rsi().is_some(), "K线[{i}] RSI 缺失"); + assert!(k.kdj().is_some(), "K线[{i}] KDJ 缺失"); + } + } + + #[test] + fn test_指标未计算时_返回None() { + let k线 = 模拟K线(1000, 100.0, 102.0, 98.0, 101.0, 500.0); + // 未调用 计算并挂载 — 指标应为 None + assert!(k线.macd().is_none(), "未计算时 MACD 应为 None"); + assert!(k线.rsi().is_none(), "未计算时 RSI 应为 None"); + assert!(k线.kdj().is_none(), "未计算时 KDJ 应为 None"); + } + + #[test] + fn test_回填新指标_新增参数后历史K线也挂载() { + let seq = 生成上涨序列(3, 1000, 100.0); + let 配置 = 缠论配置::default(); + + // 第一轮:只计算默认 macd 组 + 指标计算器::计算并挂载(&seq[..=2], &配置); + assert!(seq[2].macd().is_some()); + + // 第二轮:新增一组 MACD 参数,模拟用户后期追加指标 + let mut 配置2 = 配置.clone(); + 配置2.MACD_参数列表.push(("extra_macd".into(), "收".into(), 5, 10, 3)); + 指标计算器::计算并挂载(&seq[..=2], &配置2); + + // 最后一根K线应同时有默认和 extra MACD + let last = &seq[2]; + let guard = last.指标.read(); + assert!(guard.包含("macd"), "应有默认 macd"); + assert!(guard.包含("extra_macd"), "应有新指标 extra_macd"); + + // 回填:第一根 K 线也应被回填 extra_macd + assert!(seq[0].指标.read().包含("extra_macd"), "回填后首根K线应有 extra_macd"); + } + + #[test] + fn test_多指标组_RSI_KDJ_BOLL_同时挂载() { + let seq = 生成上涨序列(2, 1000, 100.0); + let 配置 = 缠论配置::default(); + + 指标计算器::计算并挂载(&seq[..=1], &配置); + + let last = &seq[1]; + assert!(last.macd().is_some(), "MACD 应已挂载"); + assert!(last.rsi().is_some(), "RSI 应已挂载"); + assert!(last.kdj().is_some(), "KDJ 应已挂载"); + assert!(last.boll().is_some(), "BOLL 应已挂载"); + + // 验证 RSI 值的范围 + let r = last.rsi().unwrap(); + if let Some(rsi_val) = r.RSI { + assert!((0.0..=100.0).contains(&rsi_val), "RSI 应在 0~100 之间, 实际={rsi_val}"); + } + + // 验证 KDJ 值范围 + let k = last.kdj().unwrap(); + if let Some(k_val) = k.K { + assert!((0.0..=100.0).contains(&k_val), "KDJ.K 应在 0~100 之间, 实际={k_val}"); + } + + // BOLL 上轨 >= 中轨 >= 下轨 + let b = last.boll().unwrap(); + assert!(b.上轨 >= b.中轨, "BOLL 上轨({})应 >= 中轨({})", b.上轨, b.中轨); + assert!(b.中轨 >= b.下轨, "BOLL 中轨({})应 >= 下轨({})", b.中轨, b.下轨); + } + + #[test] + fn test_均线挂载() { + let seq = 生成上涨序列(5, 1000, 100.0); + let mut 配置 = 缠论配置::default(); + 配置.均线参数列表 = vec![ + ("SMA_3".into(), "收".into(), "SMA".into(), 3), + ]; + + 指标计算器::计算并挂载(&seq[..=4], &配置); + + let last = &seq[4]; + let ma_val = last.ma("SMA_3").expect("SMA_3 应已挂载"); + assert!(ma_val > 0.0, "SMA_3 应为正值"); + } + + #[test] + fn test_观察者集成_确保指标已计算() { + use crate::business::observer::观察者; + + let 观察员 = 观察者::new("TEST".into(), 300, 缠论配置::default()); + + // 逐根投喂 + for i in 0..5 { + let price = 100.0 * (1.0 + i as f64 * 0.01); + 观察员.write().投喂原始数据( + 1000 + i as i64 * 300, price, price * 1.02, price * 0.98, price * 1.01, 1000.0, + ); + } + + // 确保指标已计算 + 观察员.read().确保指标已计算(); + + let obs = 观察员.read(); + let klines = &obs.普通K线序列; + assert!(!klines.is_empty(), "应有K线"); + + // 最后一根K线应有指标 + let last = &klines[klines.len() - 1]; + assert!(last.macd().is_some(), "观察者集成: MACD 应已挂载"); + assert!(last.rsi().is_some(), "观察者集成: RSI 应已挂载"); + assert!(last.kdj().is_some(), "观察者集成: KDJ 应已挂载"); + } + + /// 50 根 K 线后,各指标应有稳定、合理的数值(非初始默认值)。 + #[test] + fn test_50根K线_指标值稳定合理() { + // 模拟 50 根有涨有跌的 K 线 + let mut seq = Vec::with_capacity(50); + let mut price = 100.0; + let mut rng: u64 = 42; + for i in 0..50 { + // 简单 LCG 随机 ±2% 波动 + rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); + let change = ((rng as f64 / u64::MAX as f64) - 0.5) * 0.04; // -2% ~ +2% + let 收 = price * (1.0 + change); + let 开 = price; + let 高 = 开.max(收) * (1.0 + (rng % 100) as f64 / 10000.0); + let 低 = 开.min(收) * (1.0 - (rng % 100) as f64 / 10000.0); + let 量 = 500.0 + (rng % 500) as f64; + seq.push(模拟K线(1000 + i as i64 * 300, 开, 高, 低, 收, 量)); + price = 收; + } + + let 配置 = 缠论配置::default(); + + // 逐根增量计算(模拟流式管线) + for i in 0..seq.len() { + 指标计算器::计算并挂载(&seq[..=i], &配置); + } + + // ── 验证每根 K 线都有指标 ── + for (i, k) in seq.iter().enumerate() { + assert!(k.macd().is_some(), "K线[{i}] MACD 缺失"); + assert!(k.rsi().is_some(), "K线[{i}] RSI 缺失"); + assert!(k.kdj().is_some(), "K线[{i}] KDJ 缺失"); + assert!(k.boll().is_some(), "K线[{i}] BOLL 缺失"); + } + + // ── 第 50 根 K 线(最后一根)的详细校验 ── + let last = &seq[49]; + + // MACD + let m = last.macd().unwrap(); + assert!(m.DIF.is_some(), "50根后 DIF 应有值"); + assert!(m.DEA.is_some(), "50根后 DEA 应有值"); + let dif = m.DIF.unwrap(); + let dea = m.DEA.unwrap(); + // DIF 和 DEA 不应同时为 0(50 根有波动数据 EMA 应已收敛) + assert!( + dif.abs() > 1e-9 || dea.abs() > 1e-9, + "50根有波动数据 DIF/DEA 应非零, DIF={dif}, DEA={dea}" + ); + // MACD 柱 = 2*(DIF-DEA),数量级合理 + let bar = m.MACD柱; + assert!(bar.is_finite(), "MACD柱 应为有限值"); + assert!(bar.abs() < 100.0, "MACD柱 不应过大, 实际={bar}"); + + // RSI + let r = last.rsi().unwrap(); + let rsi_val = r.RSI.expect("50根后 RSI 应有值"); + assert!((0.0..=100.0).contains(&rsi_val), "RSI 应在 0~100, 实际={rsi_val}"); + // 50 根随机数据 RSI 不应卡在极端值 + assert!(rsi_val > 0.1 && rsi_val < 99.9, "RSI 不应在极端值, 实际={rsi_val}"); + + // KDJ + let kdj = last.kdj().unwrap(); + let k_val = kdj.K.expect("50根后 KDJ.K 应有值"); + let d_val = kdj.D.expect("50根后 KDJ.D 应有值"); + let j_val = kdj.J.expect("50根后 KDJ.J 应有值"); + assert!((0.0..=100.0).contains(&k_val), "KDJ.K 应在 0~100, 实际={k_val}"); + assert!((0.0..=100.0).contains(&d_val), "KDJ.D 应在 0~100, 实际={d_val}"); + // J = 3K - 2D,可能略超 [0,100] + assert!(j_val.is_finite(), "KDJ.J 应为有限值"); + + // BOLL + let b = last.boll().unwrap(); + assert!(b.上轨 > b.中轨 || b.中轨 > b.下轨, + "50根波动数据 BOLL 带宽应 > 0, 上={:.4} 中={:.4} 下={:.4}", + b.上轨, b.中轨, b.下轨); + + // ── 中间节点验证:第 25 根 K 线所有指标也应有值 ── + let mid = &seq[24]; + let m25 = mid.macd().unwrap(); + assert!(m25.DIF.is_some(), "第25根 DIF 应有值"); + assert!(mid.rsi().unwrap().RSI.is_some(), "第25根 RSI 应有值"); + assert!(mid.kdj().unwrap().K.is_some(), "第25根 KDJ.K 应有值"); + + // ── 印出第 50 根用于人工审查 ── + println!( + "=== 第 50 根 K线 指标状态 ===", + ); + println!( + " MACD: DIF={dif:.6} DEA={dea:.6} BAR={bar:.6}", + ); + println!( + " RSI: RSI={rsi_val:.4}", + ); + println!( + " KDJ: K={k_val:.4} D={d_val:.4} J={j_val:.4}", + ); + println!( + " BOLL: 上={:.4} 中={:.4} 下={:.4}", + b.上轨, b.中轨, b.下轨, + ); + } +} diff --git a/chanlun/src/indicators/container.rs b/chanlun/src/indicators/container.rs index acfc07d..080916d 100644 --- a/chanlun/src/indicators/container.rs +++ b/chanlun/src/indicators/container.rs @@ -70,9 +70,7 @@ impl 指标容器 { /// 预注册指标(不覆盖已有值) pub fn 注册(&mut self, 名称: &str, 默认值: Option<指标值>) { - if !self._数据.contains_key(名称) { - self._数据.insert(名称.to_string(), 默认值); - } + self._数据.entry(名称.to_string()).or_insert(默认值); } /// 按名称获取指标值 diff --git a/chanlun/src/indicators/kdj.rs b/chanlun/src/indicators/kdj.rs index 512ea42..dfb2e1a 100644 --- a/chanlun/src/indicators/kdj.rs +++ b/chanlun/src/indicators/kdj.rs @@ -24,6 +24,7 @@ use crate::kline::bar::K线; use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; /// 随机指标 (KDJ) /// @@ -58,9 +59,9 @@ pub struct 随机指标 { /// J 值 (3K - 2D) pub J: Option, /// 历史最高价队列(滑动窗口) - pub 历史最高价队列: Vec, + pub 历史最高价队列: VecDeque, /// 历史最低价队列(滑动窗口) - pub 历史最低价队列: Vec, + pub 历史最低价队列: VecDeque, /// 前一个 RSV(用于平滑递推) pub 前一个RSV: Option, /// 前一个 K(用于平滑递推) @@ -85,8 +86,8 @@ impl Default for 随机指标 { K: None, D: None, J: None, - 历史最高价队列: Vec::new(), - 历史最低价队列: Vec::new(), + 历史最高价队列: VecDeque::new(), + 历史最低价队列: VecDeque::new(), 前一个RSV: None, 前一个K: None, 前一个D: None, @@ -124,8 +125,8 @@ impl 随机指标 { K: None, D: None, J: None, - 历史最高价队列: vec![初始最高价], - 历史最低价队列: vec![初始最低价], + 历史最高价队列: VecDeque::from([初始最高价]), + 历史最低价队列: VecDeque::from([初始最低价]), 前一个RSV: None, 前一个K: None, 前一个D: None, @@ -181,16 +182,16 @@ impl 随机指标 { // 更新历史最高价队列 let mut 历史最高价 = 前一个KDJ.历史最高价队列.clone(); - 历史最高价.push(当前最高价); + 历史最高价.push_back(当前最高价); if 历史最高价.len() > N as usize { - 历史最高价.remove(0); + 历史最高价.pop_front(); } // 更新历史最低价队列 let mut 历史最低价 = 前一个KDJ.历史最低价队列.clone(); - 历史最低价.push(当前最低价); + 历史最低价.push_back(当前最低价); if 历史最低价.len() > N as usize { - 历史最低价.remove(0); + 历史最低价.pop_front(); } // RSV @@ -260,8 +261,8 @@ mod tests { #[test] fn test_first_calc() { let kdj = 随机指标::首次计算(110.0, 90.0, 100.0, 1000, 9, 3, 3, 80.0, 20.0); - assert_eq!(kdj.历史最高价队列, vec![110.0]); - assert_eq!(kdj.历史最低价队列, vec![90.0]); + assert_eq!(kdj.历史最高价队列, VecDeque::from([110.0])); + assert_eq!(kdj.历史最低价队列, VecDeque::from([90.0])); assert_eq!(kdj.K, None); } diff --git a/chanlun/src/indicators/rsi.rs b/chanlun/src/indicators/rsi.rs index 2f2ead0..69d14a7 100644 --- a/chanlun/src/indicators/rsi.rs +++ b/chanlun/src/indicators/rsi.rs @@ -24,6 +24,7 @@ use crate::kline::bar::K线; use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; /// 相对强弱指数 (RSI) /// @@ -58,7 +59,9 @@ pub struct 相对强弱指数 { /// RSI SMA 值 pub RSI_SMA: Option, /// RSI 历史队列(用于滚动计算) - pub RSI历史队列: Vec, + pub RSI历史队列: VecDeque, + /// RSI 历史队列运行和(O(1) SMA) + pub RSI和: f64, } impl Default for 相对强弱指数 { @@ -77,7 +80,8 @@ impl Default for 相对强弱指数 { 下跌幅度: 0.0, 平滑系数: 0.0, RSI_SMA: None, - RSI历史队列: Vec::new(), + RSI历史队列: VecDeque::new(), + RSI和: 0.0, } } } @@ -106,7 +110,8 @@ impl 相对强弱指数 { 下跌幅度: 0.0, 平滑系数: 1.0 / 周期 as f64, RSI_SMA: None, - RSI历史队列: Vec::new(), + RSI历史队列: VecDeque::new(), + RSI和: 0.0, } } @@ -167,21 +172,25 @@ impl 相对强弱指数 { }; // RSI_SMA - let (RSI_SMA, RSI历史队列) = match RSI_SMA周期 { + let (RSI_SMA, RSI历史队列, RSI和) = match RSI_SMA周期 { Some(sma周期) if sma周期 > 0 => { let mut 队列 = 前一个RSI.RSI历史队列.clone(); - 队列.push(RSI); - if 队列.len() > sma周期 as usize { - 队列.remove(0); + let mut sum = 前一个RSI.RSI和; + 队列.push_back(RSI); + sum += RSI; + if 队列.len() > sma周期 as usize + && let Some(old) = 队列.pop_front() + { + sum -= old; } let sma = if 队列.is_empty() { None } else { - Some(队列.iter().sum::() / 队列.len() as f64) + Some(sum / 队列.len() as f64) }; - (sma, 队列) + (sma, 队列, sum) } - _ => (None, Vec::new()), + _ => (None, VecDeque::new(), 0.0), }; Self { @@ -199,6 +208,7 @@ impl 相对强弱指数 { 平滑系数, RSI_SMA, RSI历史队列, + RSI和, } } } diff --git a/chanlun/src/kline/bar.rs b/chanlun/src/kline/bar.rs index 230a940..94ec832 100644 --- a/chanlun/src/kline/bar.rs +++ b/chanlun/src/kline/bar.rs @@ -23,16 +23,19 @@ */ use crate::indicators::指标容器; +use crate::indicators::{布林带, 平滑异同移动平均线, 相对强弱指数, 随机指标}; +use crate::info; use crate::types::相对方向; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::io::Write; -use std::sync::{Arc, RwLock}; +use std::sync::Arc; mod rwlock_container_serde { + use parking_lot::RwLock; use serde::{Deserialize, Deserializer, Serialize, Serializer}; - use std::sync::RwLock; /// Serde 序列化辅助(RwLock<指标容器> → 序列化器) pub fn serialize( @@ -42,7 +45,7 @@ mod rwlock_container_serde { where S: Serializer, { - val.read().unwrap().serialize(ser) + val.read().serialize(ser) } /// Serde 反序列化辅助(反序列化器 → RwLock<指标容器>) @@ -115,7 +118,7 @@ impl Clone for K线 { 开盘价: self.开盘价, 收盘价: self.收盘价, 成交量: self.成交量, - 指标: RwLock::new(self.指标.read().unwrap().clone()), + 指标: RwLock::new(self.指标.read().clone()), } } } @@ -223,6 +226,7 @@ impl K线 { /// 保存K线序列到 DAT 文件 pub fn 保存到DAT文件(路径: &str, K线序列: &[&Self]) -> std::io::Result<()> { + info!("保存到DAT文件: {}", 路径); let mut f = std::fs::File::create(路径)?; for k in K线序列 { f.write_all(&k.to_bytes())?; @@ -245,7 +249,7 @@ impl K线 { let mut 阳 = 0.0f64; let mut 阴 = 0.0f64; for k in 基序 { - if let Some(macd) = k.指标.read().unwrap().macd() { + if let Some(macd) = k.指标.read().macd() { let hist = macd.MACD柱; if hist >= 0.0 { 阳 += hist; @@ -314,6 +318,73 @@ impl K线 { (true, "K线: 全部字段一致".into()) } + /// 根据当前K线和方向生成下一根K线(与 chan.py 对齐) + pub fn 根据当前K线生成新K线(&self, 方向: 相对方向, 居中: bool) -> Self { + let 高低差 = self.高 - self.低; + let 偏移 = if 居中 { + 高低差 * 0.5 + } else { + let lo = (高低差 * 0.1279) as i64; + let hi = (高低差 * 0.883) as i64; + if hi > lo { + fastrand::i64(lo..=hi) as f64 + } else { + lo as f64 + } + }; + let 缺口偏移 = if 居中 { + 高低差 * 1.5 + } else { + let lo = (高低差 * 1.1279) as i64; + let hi = (高低差 * 1.883) as i64; + if hi > lo { + fastrand::i64(lo..=hi) as f64 + } else { + lo as f64 + } + }; + let (高, 低) = match 方向 { + 相对方向::向上 => (self.高 + 偏移, self.低 + 偏移), + 相对方向::向下 => (self.高 - 偏移, self.低 - 偏移), + 相对方向::向上缺口 => (self.高 + 缺口偏移, self.低 + 缺口偏移), + 相对方向::向下缺口 => (self.高 - 缺口偏移, self.低 - 缺口偏移), + 相对方向::衔接向上 => { + let off = 高低差; + (self.高 + off, self.高) + } + 相对方向::衔接向下 => { + let off = 高低差; + (self.低, self.低 - off) + } + _ => (self.高, self.低), + }; + let 小数点 = [self.开盘价, self.高, self.低, self.收盘价] + .iter() + .map(|v| { + let s = format!("{v}"); + s.split('.').nth(1).map(|d| d.len()).unwrap_or(0) + }) + .max() + .unwrap_or(2); + let round = |v: f64| -> f64 { + let scale = 10_f64.powi(小数点 as i32); + (v * scale).round() / scale + }; + let 开 = round(低 + (高 - 低) * fastrand::f64()); + let 收 = round(低 + (高 - 低) * fastrand::f64()); + Self::创建普K( + &self.标识, + self.时间戳 + self.周期, + 开, + round(高), + round(低), + 收, + 998.0 * fastrand::f64(), + self.序号 + 1, + self.周期, + ) + } + /// 截取Arc序列中从始到终的片段 pub fn 截取rc(序列: &[Arc], 始: &Arc, 终: &Arc) -> Vec> { let 始_ptr = Arc::as_ptr(始); @@ -325,6 +396,33 @@ impl K线 { _ => Vec::new(), } } + + // ── 便捷指标访问(封装 RwLock<指标容器> boilerplate)── + + /// 读取 MACD 指标(已计算则返回克隆,否则 None) + pub fn macd(&self) -> Option<平滑异同移动平均线> { + self.指标.read().macd_cloned() + } + + /// 读取 RSI 指标 + pub fn rsi(&self) -> Option<相对强弱指数> { + self.指标.read().rsi_cloned() + } + + /// 读取 KDJ 指标 + pub fn kdj(&self) -> Option<随机指标> { + self.指标.read().kdj_cloned() + } + + /// 读取 BOLL 指标 + pub fn boll(&self) -> Option<布林带> { + self.指标.read().boll_cloned() + } + + /// 读取均线值,如 `ma("SMA_5")` → `Option` + pub fn ma(&self, key: &str) -> Option { + self.指标.read().均线().and_then(|m| m.get(key).copied()) + } } impl std::fmt::Display for K线 { @@ -385,4 +483,118 @@ mod tests { assert_eq!(result.get("阴"), Some(&0.0)); assert_eq!(result.get("总"), Some(&0.0)); } + + // ---- 根据当前K线生成新K线 ---- + + #[test] + fn test_生成K线_居中向上() { + let bar = K线::创建普K( + "test", 1000, 50000.0, 50200.0, 49800.0, 50100.0, 100.0, 0, 300, + ); + let new = bar.根据当前K线生成新K线(相对方向::向上, true); + // 居中: 偏移 = (50200-49800)*0.5 = 200 + assert!((new.高 - 50400.0).abs() < 1.0); // 50200 + 200 + assert!((new.低 - 50000.0).abs() < 1.0); // 49800 + 200 + assert_eq!(new.序号, 1); + assert_eq!(new.时间戳, 1300); + } + + #[test] + fn test_生成K线_居中向下() { + let bar = K线::创建普K( + "test", 1000, 50000.0, 50200.0, 49800.0, 50100.0, 100.0, 0, 300, + ); + let new = bar.根据当前K线生成新K线(相对方向::向下, true); + assert!((new.高 - 50000.0).abs() < 1.0); // 50200 - 200 + assert!((new.低 - 49600.0).abs() < 1.0); // 49800 - 200 + } + + #[test] + fn test_生成K线_居中向上缺口() { + let bar = K线::创建普K( + "test", 1000, 50000.0, 50200.0, 49800.0, 50100.0, 100.0, 0, 300, + ); + let new = bar.根据当前K线生成新K线(相对方向::向上缺口, true); + // 居中缺口: 偏移 = 400*1.5 = 600 + assert!((new.高 - 50800.0).abs() < 1.0); // 50200 + 600 + assert!((new.低 - 50400.0).abs() < 1.0); // 49800 + 600 + } + + #[test] + fn test_生成K线_衔接向上() { + let bar = K线::创建普K( + "test", 1000, 50000.0, 50200.0, 49800.0, 50100.0, 100.0, 0, 300, + ); + let new = bar.根据当前K线生成新K线(相对方向::衔接向上, true); + let 高低差 = 50200.0 - 49800.0; + assert!((new.高 - (50200.0 + 高低差)).abs() < 1.0); + assert!((new.低 - 50200.0).abs() < 1.0); // 衔接向上: 低 = 原高 + } + + #[test] + fn test_生成K线_衔接向下() { + let bar = K线::创建普K( + "test", 1000, 50000.0, 50200.0, 49800.0, 50100.0, 100.0, 0, 300, + ); + let new = bar.根据当前K线生成新K线(相对方向::衔接向下, true); + assert!((new.高 - 49800.0).abs() < 1.0); // 衔接向下: 高 = 原低 + } + + #[test] + fn test_生成K线_非居中随机范围() { + let bar = K线::创建普K( + "test", 1000, 50000.0, 50200.0, 49800.0, 50100.0, 100.0, 0, 300, + ); + // 非居中:偏移在 [高低差*0.1279, 高低差*0.883] 范围内随机 + for _ in 0..20 { + let new = bar.根据当前K线生成新K线(相对方向::向上, false); + assert!(new.高 > bar.高, "向上:新高应高于原高"); + assert!(new.低 > bar.低, "向上:新低应高于原低"); + let 偏移 = new.高 - bar.高; + let 高低差 = bar.高 - bar.低; + let lo = 高低差 * 0.1279; + let hi = 高低差 * 0.883; + assert!( + 偏移 >= lo && 偏移 <= hi + 1.0, + "偏移 {偏移} 应在 [{lo}, {hi}] 范围内" + ); + } + } + + // ---- 从序列中机选 ---- + + #[test] + fn test_从序列中机选_可重复() { + let dirs = vec![相对方向::向上, 相对方向::向下, 相对方向::向上缺口]; + let result = 相对方向::从序列中机选(5, &dirs, true); + assert_eq!(result.len(), 5); + for d in &result { + assert!(dirs.contains(d)); + } + } + + #[test] + fn test_从序列中机选_不可重复() { + let dirs = vec![相对方向::向上, 相对方向::向下, 相对方向::向上缺口]; + let result = 相对方向::从序列中机选(3, &dirs, false); + assert_eq!(result.len(), 3); + for (i, d) in result.iter().enumerate() { + for prev in result[..i].iter() { + assert_ne!(prev, d, "重复方向: {:?}", d); + } + } + } + + #[test] + #[should_panic(expected = "数量超过可选方向数")] + fn test_从序列中机选_数量超限() { + let dirs = vec![相对方向::向上, 相对方向::向下]; + 相对方向::从序列中机选(3, &dirs, false); + } + + #[test] + fn test_从序列中机选_空序列() { + let result = 相对方向::从序列中机选(0, &[], true); + assert!(result.is_empty()); + } } diff --git a/chanlun/src/kline/chan_kline.rs b/chanlun/src/kline/chan_kline.rs index d93f2c1..8ada66a 100644 --- a/chanlun/src/kline/chan_kline.rs +++ b/chanlun/src/kline/chan_kline.rs @@ -29,9 +29,10 @@ use crate::structure::fractal_obj::分型; use crate::types::SyncF64; use crate::types::分型结构; use crate::types::相对方向; +use parking_lot::RwLock; use std::collections::HashSet; +use std::sync::Arc; use std::sync::atomic::{AtomicI64, Ordering}; -use std::sync::{Arc, RwLock}; /// 缠论K线 — 经包含处理过后的K线 /// @@ -74,15 +75,15 @@ impl Clone for 缠论K线 { 时间戳: AtomicI64::new(self.时间戳.load(Ordering::Relaxed)), 高: SyncF64::new(self.高.get()), 低: SyncF64::new(self.低.get()), - 方向: RwLock::new(*self.方向.read().unwrap()), - 分型: RwLock::new(*self.分型.read().unwrap()), + 方向: RwLock::new(*self.方向.read()), + 分型: RwLock::new(*self.分型.read()), 周期: self.周期, 标识: self.标识.clone(), 分型特征值: SyncF64::new(self.高.get()), 原始起始序号: self.原始起始序号, 原始结束序号: AtomicI64::new(self.原始结束序号.load(Ordering::Relaxed)), - 标的K线: RwLock::new(Arc::clone(&self.标的K线.read().unwrap())), - 买卖点信息: RwLock::new(self.买卖点信息.read().unwrap().clone()), + 标的K线: RwLock::new(Arc::clone(&self.标的K线.read())), + 买卖点信息: RwLock::new(self.买卖点信息.read().clone()), } } } @@ -97,10 +98,9 @@ impl std::fmt::Display for 缠论K线 { self.序号.load(Ordering::Relaxed), self.分型 .read() - .unwrap() .map_or("None".to_string(), |fx| fx.to_string()), self.周期, - *self.方向.read().unwrap(), + *self.方向.read(), self.时间戳.load(Ordering::Relaxed), format_f64_g(self.高.get()), format_f64_g(self.低.get()) @@ -116,23 +116,23 @@ impl 缠论K线 { 时间戳: AtomicI64::new(self.时间戳.load(Ordering::Relaxed)), 高: SyncF64::new(self.高.get()), 低: SyncF64::new(self.低.get()), - 方向: RwLock::new(*self.方向.read().unwrap()), - 分型: RwLock::new(*self.分型.read().unwrap()), + 方向: RwLock::new(*self.方向.read()), + 分型: RwLock::new(*self.分型.read()), 周期: self.周期, 标识: self.标识.clone(), 分型特征值: SyncF64::new(self.高.get()), 原始起始序号: self.原始起始序号, 原始结束序号: AtomicI64::new(self.原始结束序号.load(Ordering::Relaxed)), - 标的K线: RwLock::new(Arc::clone(&self.标的K线.read().unwrap())), - 买卖点信息: RwLock::new(self.买卖点信息.read().unwrap().clone()), + 标的K线: RwLock::new(Arc::clone(&self.标的K线.read())), + 买卖点信息: RwLock::new(self.买卖点信息.read().clone()), } } /// 与MACD柱子匹配 — 底分型时MACD柱应<0, 顶分型时>0 pub fn 与MACD柱子匹配(&self) -> bool { - let 标 = self.标的K线.read().unwrap(); - let 容器 = 标.指标.read().unwrap(); - match *self.分型.read().unwrap() { + let 标 = self.标的K线.read(); + let 容器 = 标.指标.read(); + match *self.分型.read() { Some(分型结构::底) | Some(分型结构::下) => { if let Some(macd) = 容器.macd() { macd.MACD柱 < 0.0 @@ -153,9 +153,9 @@ impl 缠论K线 { /// 与RSI匹配 — 底分型时RSI应低于SMA, 顶分型时高于SMA pub fn 与RSI匹配(&self) -> bool { - let 标 = self.标的K线.read().unwrap(); - let 容器 = 标.指标.read().unwrap(); - match *self.分型.read().unwrap() { + let 标 = self.标的K线.read(); + let 容器 = 标.指标.read(); + match *self.分型.read() { Some(分型结构::底) | Some(分型结构::下) => { if let Some(rsi) = 容器.rsi() { match (rsi.RSI, rsi.RSI_SMA) { @@ -182,9 +182,9 @@ impl 缠论K线 { /// 与KDJ匹配 — 底分型时K应低于D(死叉后), 顶分型时K应高于D(金叉后) pub fn 与KDJ匹配(&self) -> bool { - let 标 = self.标的K线.read().unwrap(); - let 容器 = 标.指标.read().unwrap(); - match *self.分型.read().unwrap() { + let 标 = self.标的K线.read(); + let 容器 = 标.指标.read(); + match *self.分型.read() { Some(分型结构::底) | Some(分型结构::下) => { if let Some(kdj) = 容器.kdj() { match (kdj.K, kdj.D) { @@ -356,12 +356,12 @@ impl 缠论K线 { // 逆序包含时更新时间和标的K线 if 关系 != 相对方向::顺 { 当前缠K.时间戳.store(当前普K.时间戳, Ordering::Relaxed); - *当前缠K.标的K线.write().unwrap() = Arc::clone(当前普K); + *当前缠K.标的K线.write() = Arc::clone(当前普K); } 当前缠K.高.set(取值函数(当前缠K.高.get(), 当前普K.高)); 当前缠K.低.set(取值函数(当前缠K.低.get(), 当前普K.低)); 当前缠K.原始结束序号.store(当前普K.序号, Ordering::Relaxed); - *当前缠K.方向.write().unwrap() = 当前普K.方向(); + *当前缠K.方向.write() = 当前普K.方向(); if let Some(之前) = 之前缠K { 当前缠K @@ -466,29 +466,29 @@ impl 缠论K线 { let 结构 = 分型结构::分析(&*左, &*中, &*右, false, false); // 对齐 Python:无条件设置 中.分型、中.分型特征值、右.分型特征值、右.分型 - *缠K序列[idx - 2].分型.write().unwrap() = 结构; + *缠K序列[idx - 2].分型.write() = 结构; if let Some(结构) = 结构 { match 结构 { 分型结构::底 => { 缠K序列[idx - 2].分型特征值.set(缠K序列[idx - 2].低.get()); 缠K序列[idx - 1].分型特征值.set(缠K序列[idx - 1].高.get()); - *缠K序列[idx - 1].分型.write().unwrap() = Some(分型结构::顶); + *缠K序列[idx - 1].分型.write() = Some(分型结构::顶); } 分型结构::顶 => { 缠K序列[idx - 2].分型特征值.set(缠K序列[idx - 2].高.get()); 缠K序列[idx - 1].分型特征值.set(缠K序列[idx - 1].低.get()); - *缠K序列[idx - 1].分型.write().unwrap() = Some(分型结构::底); + *缠K序列[idx - 1].分型.write() = Some(分型结构::底); } 分型结构::上 => { 缠K序列[idx - 2].分型特征值.set(缠K序列[idx - 2].高.get()); 缠K序列[idx - 1].分型特征值.set(缠K序列[idx - 1].高.get()); - *缠K序列[idx - 1].分型.write().unwrap() = Some(分型结构::顶); + *缠K序列[idx - 1].分型.write() = Some(分型结构::顶); } 分型结构::下 => { 缠K序列[idx - 2].分型特征值.set(缠K序列[idx - 2].低.get()); 缠K序列[idx - 1].分型特征值.set(缠K序列[idx - 1].低.get()); - *缠K序列[idx - 1].分型.write().unwrap() = Some(分型结构::底); + *缠K序列[idx - 1].分型.write() = Some(分型结构::底); } 分型结构::散 => {} } @@ -573,23 +573,23 @@ impl 缠论K线 { ), ); } - if *self.方向.read().unwrap() != *other.方向.read().unwrap() { + if *self.方向.read() != *other.方向.read() { return ( false, format!( "缠论K线: [方向] 不等 A={},B={}", - self.方向.read().unwrap(), - other.方向.read().unwrap() + self.方向.read(), + other.方向.read() ), ); } - if *self.分型.read().unwrap() != *other.分型.read().unwrap() { + if *self.分型.read() != *other.分型.read() { return ( false, format!( "缠论K线: [分型] 不等 A={:?},B={:?}", - self.分型.read().unwrap(), - other.分型.read().unwrap() + self.分型.read(), + other.分型.read() ), ); } @@ -636,17 +636,13 @@ impl 缠论K线 { ); } // 标的K线 递归 - let (eq, msg) = self - .标的K线 - .read() - .unwrap() - .相等(&other.标的K线.read().unwrap(), 浮点容差); + let (eq, msg) = self.标的K线.read().相等(&other.标的K线.read(), 浮点容差); if !eq { return (false, format!("缠论K线: 标的K线子项异常 >> {msg}")); } // 买卖点信息 - let a_guard = self.买卖点信息.read().unwrap(); - let b_guard = other.买卖点信息.read().unwrap(); + let a_guard = self.买卖点信息.read(); + let b_guard = other.买卖点信息.read(); let a_set: std::collections::HashSet<&String> = a_guard.iter().collect(); let b_set: std::collections::HashSet<&String> = b_guard.iter().collect(); if a_set != b_set { @@ -654,13 +650,45 @@ impl 缠论K线 { false, format!( "缠论K线: [买卖点信息] 集合不等 A={:?},B={:?}", - self.买卖点信息.read().unwrap(), - other.买卖点信息.read().unwrap() + self.买卖点信息.read(), + other.买卖点信息.read() ), ); } (true, "缠论K线: 全部字段一致".into()) } + + // ── 便捷指标访问(委托给标的K线)── + + /// 读取 MACD 指标 + pub fn macd(&self) -> Option { + self.标的K线.read().macd() + } + + /// 读取 RSI 指标 + pub fn rsi(&self) -> Option { + self.标的K线.read().rsi() + } + + /// 读取 KDJ 指标 + pub fn kdj(&self) -> Option { + self.标的K线.read().kdj() + } + + /// 读取 BOLL 指标 + pub fn boll(&self) -> Option { + self.标的K线.read().boll() + } + + /// 读取均线值 + pub fn ma(&self, key: &str) -> Option { + self.标的K线.read().ma(key) + } + + /// 读取收盘价(委托给标的K线) + pub fn 收盘价(&self) -> f64 { + self.标的K线.read().收盘价 + } } impl crate::types::fractal::有高低 for 缠论K线 { diff --git a/chanlun/src/lib.rs b/chanlun/src/lib.rs index b806bde..cfbde9a 100644 --- a/chanlun/src/lib.rs +++ b/chanlun/src/lib.rs @@ -30,6 +30,8 @@ pub mod business; pub mod config; pub mod indicators; pub mod kline; +pub mod log; +pub mod signal; pub mod structure; pub mod types; pub mod utils; diff --git a/chanlun/src/log.rs b/chanlun/src/log.rs new file mode 100644 index 0000000..9ad376f --- /dev/null +++ b/chanlun/src/log.rs @@ -0,0 +1,92 @@ +/* + * 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::{AtomicBool, AtomicU8, Ordering}; + +/// 日志模式: 0=Off, 1=Simple (eprintln), 2=Tracing (tracing subscriber) +pub static LOG_MODE: AtomicU8 = AtomicU8::new(0); + +/// 向后兼容:set_log_level 设置此标志 +pub static 日志启用: AtomicBool = AtomicBool::new(false); + +pub fn init_from_env() { + if let Ok(val) = std::env::var("CHANLUN_LOG_MODE") { + match val.to_lowercase().as_str() { + "simple" | "on" | "debug" | "1" => { + LOG_MODE.store(1, Ordering::Relaxed); + 日志启用.store(true, Ordering::Relaxed); + } + "tracing" | "2" => { + LOG_MODE.store(2, Ordering::Relaxed); + 日志启用.store(true, Ordering::Relaxed); + } + _ => {} + } + } +} + +pub fn set_log_mode(mode: u8) { + LOG_MODE.store(mode.min(2), Ordering::Relaxed); + 日志启用.store(mode > 0, Ordering::Relaxed); +} + +pub fn get_log_mode() -> u8 { + LOG_MODE.load(Ordering::Relaxed) +} + +#[macro_export] +macro_rules! warn { + ($($arg:tt)*) => { + if $crate::log::日志启用.load(std::sync::atomic::Ordering::Relaxed) { + match $crate::log::LOG_MODE.load(std::sync::atomic::Ordering::Relaxed) { + 2 => tracing::warn!($($arg)*), + _ => eprintln!($($arg)*), + } + } + }; +} + +#[macro_export] +macro_rules! error { + ($($arg:tt)*) => { + if $crate::log::日志启用.load(std::sync::atomic::Ordering::Relaxed) { + match $crate::log::LOG_MODE.load(std::sync::atomic::Ordering::Relaxed) { + 2 => tracing::error!($($arg)*), + _ => eprintln!($($arg)*), + } + } + }; +} + +#[macro_export] +macro_rules! info { + ($($arg:tt)*) => { + if $crate::log::日志启用.load(std::sync::atomic::Ordering::Relaxed) { + match $crate::log::LOG_MODE.load(std::sync::atomic::Ordering::Relaxed) { + 2 => tracing::info!($($arg)*), + _ => println!($($arg)*), + } + } + }; +} diff --git a/chanlun/src/main.rs b/chanlun/src/main.rs index d3910b0..0dbd829 100644 --- a/chanlun/src/main.rs +++ b/chanlun/src/main.rs @@ -80,10 +80,9 @@ fn 测试_读取数据(文件路径: &str) { let 观察员 = 观察者::new("".into(), 0, 缠论配置::default()); 观察员 .write() - .unwrap() .读取数据文件(文件路径, 配置) .expect("读取数据文件失败"); - let 观察员 = 观察员.read().unwrap(); + let 观察员 = 观察员.read(); let 消耗用时 = 启动时间.elapsed(); println!( "测试_读取数据 耗时 {:.2?} 普K数量 {}", @@ -160,7 +159,7 @@ fn 测试_周期合成(文件路径: &str) { // Display stats per period for &p in &[周期, 周期 * 5, 周期 * 5 * 6] { if let Some(观察员) = 多级别分析.获取观察者(p) { - let 观察员 = 观察员.read().unwrap(); + let 观察员 = 观察员.read(); println!( "周期<{}>: 缠K={}, 分型={}, 笔={}, 线段={}, 中枢={}", p, diff --git a/chanlun/src/signal/engine.rs b/chanlun/src/signal/engine.rs new file mode 100644 index 0000000..1c87edd --- /dev/null +++ b/chanlun/src/signal/engine.rs @@ -0,0 +1,378 @@ +/* + * 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. + */ + +//! 信号计算引擎 — 通过 `SIGNAL_REGISTRY` 按名查找信号函数并执行。 +//! +//! 第三方代码声明:引擎架构参考 czsc 的 `信号计算器` +//!(https://github.com/waditu/czsc,Apache License 2.0),已适配为 Rust。 +//! +//! # 示例 +//! +//! ```ignore +//! use chanlun::signal::engine::{SignalEngine, SignalConfig, call_signal}; +//! +//! let engine = SignalEngine::new(vec![SignalConfig { +//! signal_name: "youwukuncheng_中枢第三买卖点_V230602".into(), +//! freq: 86400, +//! params: params_map, +//! }]); +//! engine.自动挂载指标(&analyzer); +//! let results = engine.更新(&analyzer); +//! ``` + +use crate::business::multi_frame::立体分析器; +use crate::business::observer::观察者; +use crate::signal::Signal; +use crate::signal::registry; +use serde_json::Value; +use std::collections::{HashMap, HashSet}; + +/// 单一信号配置项 — 对应 Python 信号配置列表中的一条。 +#[derive(Debug, Clone)] +pub struct SignalConfig { + /// 注册表中的信号名,如 `"youwukuncheng_中枢第三买卖点_V230602"` + pub signal_name: String, + /// 本配置作用的周期(秒) + pub freq: i64, + /// 信号参数(含 `freq`,统一为字符串以便 Rust 信号函数读取) + pub params: HashMap, +} + +/// 完整更新结果:信号字典 + 基础周期行情数据。 +#[derive(Debug, Clone)] +pub struct 完整更新结果 { + /// 信号 key → value 映射 + pub signals: HashMap, + /// 基础周期最后一根 K 线的 OHLCV 数据(若无 K 线则为 None) + pub market: Option, +} + +/// 基础周期行情数据 — 对应 Python `信号计算器.行情`。 +#[derive(Debug, Clone)] +pub struct MarketData { + pub symbol: String, + pub dt: i64, // Unix 秒(K线时间戳) + pub id: i64, // K线序号 + pub open: f64, + pub high: f64, + pub low: f64, + pub close: f64, + pub vol: f64, +} + +/// 信号计算引擎 — 持有配置列表,按 `&立体分析器` 执行。 +/// +/// 引擎不持有分析器引用——每次调用时传入,避免借用冲突。 +pub struct SignalEngine { + configs: Vec, +} + +impl SignalEngine { + /// 创建引擎。配置中的信号名延迟到 `更新()` 时校验。 + pub fn new(configs: Vec) -> Self { + Self { configs } + } + + /// 返回当前配置数量 + pub fn len(&self) -> usize { + self.configs.len() + } + + /// 配置是否为空 + pub fn is_empty(&self) -> bool { + self.configs.is_empty() + } + + /// 扫描信号名中的 MACD / 均线关键字,向各周期 observer 的配置中 + /// 追加缺失的指标参数,然后调用 `确保指标已计算()`(幂等)。 + /// + /// 与 Python `_自动挂载指标()` 逻辑一致。 + pub fn 自动挂载指标(&self, analyzer: &立体分析器) { + // 第一遍:按周期收集需要的参数 + let mut macd_by_freq: HashMap> = HashMap::new(); + let mut ma_by_freq: HashMap> = HashMap::new(); + + for cfg in &self.configs { + let name_lower = cfg.signal_name.to_lowercase(); + + // MACD 信号检测 + if name_lower.contains("macd") + || name_lower.contains("中枢") + || name_lower.contains("背驰") + || name_lower.contains("金叉") + { + let fast = cfg + .params + .get("fast") + .and_then(|v| v.as_i64()) + .or_else(|| cfg.params.get("快线周期").and_then(|v| v.as_i64())) + .unwrap_or(13); + let slow = cfg + .params + .get("slow") + .and_then(|v| v.as_i64()) + .or_else(|| cfg.params.get("慢线周期").and_then(|v| v.as_i64())) + .unwrap_or(31); + let signal = cfg + .params + .get("signal") + .and_then(|v| v.as_i64()) + .or_else(|| cfg.params.get("信号周期").and_then(|v| v.as_i64())) + .unwrap_or(11); + + let key = format!("macd_{fast}_{slow}_{signal}"); + macd_by_freq + .entry(cfg.freq) + .or_default() + .push((key, fast, slow, signal)); + } + + // 均线信号检测 + if name_lower.contains("ma_") + || name_lower.contains("tas_ma") + || name_lower.contains("均线") + { + let ma_type = cfg + .params + .get("ma_type") + .and_then(|v| v.as_str()) + .unwrap_or("SMA") + .to_uppercase(); + let period = cfg + .params + .get("timeperiod") + .and_then(|v| v.as_i64()) + .or_else(|| cfg.params.get("周期").and_then(|v| v.as_i64())) + .unwrap_or(5); + + let key = format!("{ma_type}_{period}"); + ma_by_freq + .entry(cfg.freq) + .or_default() + .push((key, ma_type, period)); + } + } + + // 第二遍:写入 observer 配置(先收集已有 key,再 drop 后写入) + for (freq, entries) in &macd_by_freq { + if let Some(obs_arc) = analyzer.获取观察者(*freq) { + let needs_push: Vec<(String, String, i64, i64, i64)> = { + let obs = obs_arc.read(); + let existing: HashSet = + obs.配置.MACD_参数列表.iter().map(|t| t.0.clone()).collect(); + entries + .iter() + .filter(|(key, _, _, _)| !existing.contains(key)) + .map(|(key, fast, slow, signal)| { + (key.clone(), "收".to_string(), *fast, *slow, *signal) + }) + .collect() + }; + if !needs_push.is_empty() { + let mut obs = obs_arc.write(); + for tuple in needs_push { + obs.配置.MACD_参数列表.push(tuple); + } + obs.配置.计算指标 = true; + } + } + } + + for (freq, entries) in &ma_by_freq { + if let Some(obs_arc) = analyzer.获取观察者(*freq) { + let needs_push: Vec<(String, String, String, i64)> = { + let obs = obs_arc.read(); + let existing: HashSet = + obs.配置.均线参数列表.iter().map(|t| t.0.clone()).collect(); + entries + .iter() + .filter(|(key, _, _)| !existing.contains(key)) + .map(|(key, ma_type, period)| { + (key.clone(), "收".to_string(), ma_type.clone(), *period) + }) + .collect() + }; + if !needs_push.is_empty() { + let mut obs = obs_arc.write(); + for tuple in needs_push { + obs.配置.均线参数列表.push(tuple); + } + obs.配置.计算指标 = true; + } + } + } + + // 第三遍:确保所有周期观察者的指标已计算(幂等) + for freq in &analyzer.周期组 { + if let Some(obs_arc) = analyzer.获取观察者(*freq) { + obs_arc.read().确保指标已计算(); + } + } + } + + /// 遍历所有配置,执行信号函数,收集非空结果。 + /// + /// 返回 `{信号key: 信号value}` 字典(已过滤 `"任意_任意_任意_0"`)。 + /// 缺失的 observer 或未注册信号名会通过 tracing::warn! 记录并跳过。 + pub fn 更新(&self, analyzer: &立体分析器) -> HashMap { + let mut results: HashMap = HashMap::new(); + + for cfg in &self.configs { + let obs_arc = match analyzer.获取观察者(cfg.freq) { + Some(o) => o, + None => { + tracing::warn!("信号引擎: 未找到周期 {} 的观察者", cfg.freq); + continue; + } + }; + + let meta = match registry::get_signal(&cfg.signal_name) { + Some(m) => m, + None => { + tracing::warn!("信号引擎: 信号未注册: {}", cfg.signal_name); + continue; + } + }; + + let signals = { + let obs_guard = obs_arc.read(); + (meta.func)(&obs_guard, &cfg.params) + }; + + for sig in signals { + if sig.value() != "任意_任意_任意_0" { + results.insert(sig.key(), sig.value()); + } + } + } + + results + } + + /// 运行信号计算并附带基础周期行情。 + /// + /// `base_freq` 使用分析器的第一个周期(最小周期)。 + /// 返回的 `完整更新结果` 可直接组合为 Python `信号字典` 格式。 + pub fn 更新_完整(&self, analyzer: &立体分析器) -> 完整更新结果 { + let signals = self.更新(analyzer); + + let base_freq = analyzer.周期组.first().copied().unwrap_or(0); + let market = analyzer.单体分析器.get(&base_freq).and_then(|obs| { + let obs_guard = obs.read(); + obs_guard.普通K线序列.last().map(|k| MarketData { + symbol: obs_guard.符号.clone(), + dt: k.时间戳, + id: k.序号, + open: k.开盘价, + high: k.高, + low: k.低, + close: k.收盘价, + vol: k.成交量, + }) + }); + + 完整更新结果 { signals, market } + } +} + +/// 按名查找并调用单个信号函数。 +/// +/// 适用于已有 `&观察者` 的场景(测试、单周期分析),无需构造完整的 `SignalEngine`。 +pub fn call_signal( + name: &str, + obs: &观察者, + params: &HashMap, +) -> Result, String> { + let meta = registry::get_signal(name).ok_or_else(|| format!("信号未注册: {name}"))?; + Ok((meta.func)(obs, params)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::缠论配置; + + /// 通过 call_signal 调用 youwukuncheng 信号,验证产出格式。 + #[test] + fn test_call_signal_youwukuncheng() { + let nb_path = concat!( + env!("CARGO_MANIFEST_DIR"), + "/../templates/btcusd-86400-1608854400-1781568000.nb" + ); + + let 观察员 = 观察者::new("btcusd".into(), 86400, 缠论配置::default()); + 观察员 + .write() + .读取数据文件(nb_path, 缠论配置::default().不推送()) + .expect("读取数据文件失败"); + + let obs = 观察员.read(); + // 信号函数内部会调用 确保指标已计算,但为稳妥先调用一次 + obs.确保指标已计算(); + + let mut params: HashMap = HashMap::new(); + params.insert("freq".into(), Value::String("日线".into())); + params.insert( + "max_overlap".into(), + Value::Number(serde_json::Number::from(3)), + ); + params.insert("本级完整性".into(), Value::String("实".into())); + params.insert("同级完整性".into(), Value::String("合".into())); + + let signals = call_signal("youwukuncheng_中枢第三买卖点_V230602", &obs, ¶ms) + .expect("call_signal 应成功"); + + assert!(!signals.is_empty(), "至少应返回一个信号(可能是空)"); + for s in &signals { + assert!(s.k3.ends_with("V230602"), "k3 应以 V230602 结尾: {}", s.k3); + assert!((0..=100).contains(&s.score), "score 超范围: {}", s.score); + } + + // 验证非空信号 + let non_empty: Vec<_> = signals + .iter() + .filter(|s| s.value() != "任意_任意_任意_0") + .collect(); + println!( + "call_signal: {} signals, {} non-empty", + signals.len(), + non_empty.len() + ); + for s in &non_empty { + println!(" k3={} v1={} v2={} score={}", s.k3, s.v1, s.v2, s.score); + } + } + + /// 空配置返回空结果 + #[test] + fn test_engine_空配置_返回空() { + use crate::business::multi_frame::立体分析器; + + // 立体分析器 至少需要 2 个周期(周期组[0]=输入周期,周期组[1]=显示周期) + let analyzer = 立体分析器::new("test".into(), vec![300, 900], None, None); + let engine = SignalEngine::new(vec![]); + let results = engine.更新(&analyzer); + assert!(results.is_empty()); + } +} diff --git a/chanlun/src/signal/event.rs b/chanlun/src/signal/event.rs new file mode 100644 index 0000000..3b2b173 --- /dev/null +++ b/chanlun/src/signal/event.rs @@ -0,0 +1,227 @@ +/* + * 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. + */ + +//! 事件 — operate + 因子列表(任一因子满足则事件为真)。 + +use crate::signal::factor::Factor; +use crate::signal::operate::Operate; +use crate::signal::signal::Signal; +use crate::signal::{sha256前4, 信号字典, 缺键错误}; + +#[derive(Clone, Debug)] +pub struct Event { + pub operate: Operate, + pub factors: Vec, + pub signals_all: Vec, + pub signals_any: Vec, + pub signals_not: Vec, + pub name: String, + pub sha256: String, +} + +impl Event { + /// 构造。factors 为空 → Err。name 自动补哈希。 + pub fn 新建( + operate: Operate, + factors: Vec, + signals_all: Vec, + signals_any: Vec, + signals_not: Vec, + name: String, + ) -> Result { + if factors.is_empty() { + return Err("factors 不能为空".to_string()); + } + let hash = Self::计算哈希(&factors, &signals_all, &signals_any, &signals_not); + let name = if name.is_empty() { + format!("{}#{hash}", operate.value()) + } else { + format!("{}#{hash}", name.split('#').next().unwrap_or("")) + }; + Ok(Self { + operate, + factors, + signals_all, + signals_any, + signals_not, + name, + sha256: hash, + }) + } + + fn 计算哈希(factors: &[Factor], all: &[Signal], any: &[Signal], not: &[Signal]) -> String { + let 取串 = |v: &[Signal]| { + v.iter() + .map(|s| s.signal.clone()) + .collect::>() + .join(",") + }; + let 因子串 = factors + .iter() + .map(|f| f.name.clone()) + .collect::>() + .join(";"); + let 规范 = format!( + "factors=[{}]|all=[{}]|any=[{}]|not=[{}]", + 因子串, + 取串(all), + 取串(any), + 取串(not) + ); + sha256前4(&规范) + } + + pub fn unique_signals(&self) -> Vec { + let mut 集合 = std::collections::BTreeSet::new(); + for s in self + .signals_all + .iter() + .chain(&self.signals_any) + .chain(&self.signals_not) + { + 集合.insert(s.signal.clone()); + } + for f in &self.factors { + for s in f.unique_signals() { + 集合.insert(s); + } + } + 集合.into_iter().collect() + } + + /// 事件匹配。命中返回 (true, 因子名),否则 (false, None)。 + pub fn is_match(&self, 字典: &信号字典) -> Result<(bool, Option), 缺键错误> { + for s in &self.signals_not { + if s.is_match(字典)? { + return Ok((false, None)); + } + } + for s in &self.signals_all { + if !s.is_match(字典)? { + return Ok((false, None)); + } + } + if !self.signals_any.is_empty() { + let mut 任一命中 = false; + for s in &self.signals_any { + if s.is_match(字典)? { + 任一命中 = true; + break; + } + } + if !任一命中 { + return Ok((false, None)); + } + } + for f in &self.factors { + if f.is_match(字典)? { + return Ok((true, Some(f.name.clone()))); + } + } + Ok((false, None)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::signal::匹配值; + use std::collections::HashMap; + + fn 信号(k3: &str, v2: &str) -> Signal { + Signal::new("14400", "D1MO3", k3, "任意", v2, "任意", 0) + } + fn 因子(k3: &str, v2: &str) -> Factor { + Factor::新建(vec![信号(k3, v2)], vec![], vec![], "".into()).unwrap() + } + fn 字典(k3: &str, v2: &str) -> HashMap { + let mut m = HashMap::new(); + m.insert( + format!("14400_D1MO3_{k3}"), + 匹配值::字符串(format!("x_{v2}_y_100")), + ); + m + } + + #[test] + fn test_factors_为空_报错() { + assert!(Event::新建(Operate::开多, vec![], vec![], vec![], vec![], "".into()).is_err()); + } + + #[test] + fn test_name_默认用operate值() { + let e = Event::新建( + Operate::开多, + vec![因子("中枢", "三买")], + vec![], + vec![], + vec![], + "".into(), + ) + .unwrap(); + assert!(e.name.starts_with("开多#")); + } + + #[test] + fn test_任一因子命中() { + let e = Event::新建( + Operate::开多, + vec![因子("中枢A", "三买"), 因子("中枢B", "三买")], + vec![], + vec![], + vec![], + "".into(), + ) + .unwrap(); + // 两个因子的 key 都在字典:中枢A 在场但 v2=三卖 不匹配,中枢B v2=三买 匹配 + let mut d = HashMap::new(); + d.insert( + "14400_D1MO3_中枢A".to_string(), + 匹配值::字符串("x_三卖_y_100".to_string()), + ); + d.insert( + "14400_D1MO3_中枢B".to_string(), + 匹配值::字符串("x_三买_y_100".to_string()), + ); + let (命中, 名) = e.is_match(&d).unwrap(); + assert!(命中); + assert!(名.is_some()); + } + + #[test] + fn test_无因子命中_false() { + let e = Event::新建( + Operate::开多, + vec![因子("中枢", "三买")], + vec![], + vec![], + vec![], + "".into(), + ) + .unwrap(); + let (命中, 名) = e.is_match(&字典("中枢", "三卖")).unwrap(); + assert!(!命中); + assert!(名.is_none()); + } +} diff --git a/chanlun/src/signal/factor.rs b/chanlun/src/signal/factor.rs new file mode 100644 index 0000000..cedfa3d --- /dev/null +++ b/chanlun/src/signal/factor.rs @@ -0,0 +1,175 @@ +/* + * 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. + */ + +//! 因子 — signals_all 全满足 + signals_any 任一满足 + signals_not 全不满足。 + +use crate::signal::signal::Signal; +use crate::signal::{sha256前4, 信号字典, 缺键错误}; + +#[derive(Clone, Debug)] +pub struct Factor { + pub signals_all: Vec, + pub signals_any: Vec, + pub signals_not: Vec, + pub name: String, +} + +impl Factor { + /// 构造。signals_all 为空 → Err。name 自动补确定性哈希后缀。 + pub fn 新建( + signals_all: Vec, + signals_any: Vec, + signals_not: Vec, + name: String, + ) -> Result { + if signals_all.is_empty() { + return Err("signals_all 不能为空".to_string()); + } + let hash = Self::计算哈希(&signals_all, &signals_any, &signals_not); + let 前缀 = name.split('#').next().unwrap_or("").to_string(); + let name = format!("{前缀}#{hash}"); + Ok(Self { + signals_all, + signals_any, + signals_not, + name, + }) + } + + /// 确定性哈希 — 拼接三组 signals 串后算 sha256 前4。 + fn 计算哈希(all: &[Signal], any: &[Signal], not: &[Signal]) -> String { + let 取串 = |v: &[Signal]| { + v.iter() + .map(|s| s.signal.clone()) + .collect::>() + .join(",") + }; + let 规范 = format!( + "all=[{}]|any=[{}]|not=[{}]", + 取串(all), + 取串(any), + 取串(not) + ); + sha256前4(&规范) + } + + pub fn unique_signals(&self) -> Vec { + let mut 集合 = std::collections::BTreeSet::new(); + for s in self + .signals_all + .iter() + .chain(&self.signals_any) + .chain(&self.signals_not) + { + 集合.insert(s.signal.clone()); + } + 集合.into_iter().collect() + } + + /// 因子匹配。任一信号缺键 → Err 向上传播。 + pub fn is_match(&self, 字典: &信号字典) -> Result { + for s in &self.signals_not { + if s.is_match(字典)? { + return Ok(false); + } + } + for s in &self.signals_all { + if !s.is_match(字典)? { + return Ok(false); + } + } + if self.signals_any.is_empty() { + return Ok(true); + } + for s in &self.signals_any { + if s.is_match(字典)? { + return Ok(true); + } + } + Ok(false) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::signal::匹配值; + use std::collections::HashMap; + + fn 信号(k3: &str, v2: &str) -> Signal { + Signal::new("14400", "D1MO3", k3, "任意", v2, "任意", 0) + } + fn 字典(k3: &str, v2: &str) -> HashMap { + let mut m = HashMap::new(); + m.insert( + format!("14400_D1MO3_{k3}"), + 匹配值::字符串(format!("x_{v2}_y_100")), + ); + m + } + + #[test] + fn test_signals_all_为空_报错() { + assert!(Factor::新建(vec![], vec![], vec![], "".into()).is_err()); + } + + #[test] + fn test_name_含哈希后缀() { + let f = Factor::新建(vec![信号("中枢", "三买")], vec![], vec![], "测试".into()).unwrap(); + assert!(f.name.starts_with("测试#")); + assert_eq!(f.name.len(), "测试#".len() + 4); + } + + #[test] + fn test_name_确定性() { + let f1 = Factor::新建(vec![信号("中枢", "三买")], vec![], vec![], "".into()).unwrap(); + let f2 = Factor::新建(vec![信号("中枢", "三买")], vec![], vec![], "".into()).unwrap(); + assert_eq!(f1.name, f2.name); + } + + #[test] + fn test_all_命中() { + let f = Factor::新建(vec![信号("中枢", "三买")], vec![], vec![], "".into()).unwrap(); + assert_eq!(f.is_match(&字典("中枢", "三买")).unwrap(), true); + } + + #[test] + fn test_not_命中则false() { + let f = Factor::新建( + vec![信号("中枢", "三买")], + vec![], + vec![信号("中枢", "三买")], + "".into(), + ) + .unwrap(); + assert_eq!(f.is_match(&字典("中枢", "三买")).unwrap(), false); + } + + #[test] + fn test_缺键传播错误() { + let f = Factor::新建(vec![信号("中枢", "三买")], vec![], vec![], "".into()).unwrap(); + let m: HashMap = HashMap::new(); + assert!(f.is_match(&m).is_err()); + } +} diff --git a/chanlun/src/signal/ffi.rs b/chanlun/src/signal/ffi.rs new file mode 100644 index 0000000..4d25fb8 --- /dev/null +++ b/chanlun/src/signal/ffi.rs @@ -0,0 +1,98 @@ +/* + * 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. + */ + +//! C-ABI 导出 — 供动态加载的 .so 插件调用。 +//! +//! 插件编译为 cdylib (`.so`),由 Python `ctypes.CDLL` 或 Rust `libloading` 加载。 +//! 加载后插件调用 `chanlun_register_signal` 向宿主进程的 `DYNAMIC_REGISTRY` 注册信号。 +//! +//! # 插件约定 +//! +//! 1. 插件 .so 的构造函数中调用 `chanlun_register_signal(name, template, func)` +//! 2. `func` 是 `SignalFn` 类型的函数指针(`fn(&观察者, &HashMap) -> Vec`) +//! 3. 插件和宿主必须用相同 Rust 编译器版本编译 + +use std::ffi::CStr; +use std::os::raw::c_char; + +use crate::signal::registry::{self, SignalFn}; + +/// 宿主导出:供外部动态库调用的注册入口。 +/// +/// - `name`: 信号名(C 字符串) +/// - `template`: 参数模板(C 字符串) +/// - `func`: 函数指针(Rust 调用约定,插件与宿主须同编译器版本) +/// +/// 返回 0 成功,非 0 失败。 +/// +/// # Safety +/// +/// `name` 和 `template` 必须是非空的合法 UTF-8 C 字符串指针。 +/// `func` 必须是合法的 `SignalFn` 函数指针(Rust 调用约定)。 +#[unsafe(no_mangle)] +#[allow(improper_ctypes_definitions)] +pub unsafe extern "C" fn chanlun_register_signal( + name: *const c_char, + template: *const c_char, + func: SignalFn, +) -> i32 { + if name.is_null() || template.is_null() { + return 1; + } + let name_str = unsafe { CStr::from_ptr(name) }.to_string_lossy(); + let template_str = unsafe { CStr::from_ptr(template) }.to_string_lossy(); + match registry::register_signal(&name_str, &template_str, func) { + Ok(()) => 0, + Err(_) => 2, + } +} + +/// 宿主导出:从动态注册表移除信号。 +/// +/// 返回 0 成功,非 0 失败。 +/// +/// # Safety +/// +/// `name` 必须是非空的合法 UTF-8 C 字符串指针。 +#[unsafe(no_mangle)] +pub unsafe extern "C" fn chanlun_unregister_signal(name: *const c_char) -> i32 { + if name.is_null() { + return 1; + } + let name_str = unsafe { CStr::from_ptr(name) }.to_string_lossy(); + match registry::unregister_signal(&name_str) { + Ok(()) => 0, + Err(_) => 2, + } +} + +/// 查询已注册信号总数(编译时 + 动态)。 +/// +/// # Safety +/// +/// 此函数不接受任何指针参数,调用始终安全。 +#[unsafe(no_mangle)] +pub unsafe extern "C" fn chanlun_list_signal_count() -> i32 { + registry::list_signal_names().len() as i32 +} diff --git a/chanlun/src/signal/functions/demo.rs b/chanlun/src/signal/functions/demo.rs new file mode 100644 index 0000000..cc4b23b --- /dev/null +++ b/chanlun/src/signal/functions/demo.rs @@ -0,0 +1,436 @@ +/* + * 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. + */ + +//! 示例信号函数 — 移植自 `chanlun-py/chanlun/signals/demo.py`。 +//! +//! 第三方代码声明:信号函数模式参考 czsc(https://github.com/waditu/czsc, +//! Apache License 2.0),已适配为 Rust。 + +use std::collections::HashMap; + +use serde_json::Value; + +use chanlun_signal_macros::signal; + +use crate::business::observer::观察者; +use crate::kline::bar::K线; +use crate::signal::Signal; +use crate::signal::params; + +// ============================================================================= +// bar — K线形态信号 +// ============================================================================= + +/// 涨跌停检测信号。 +/// +/// `close == high && close >= prev_close` → 涨停 +/// `close == low && close <= prev_close` → 跌停 +#[signal(name = "bar_zdt_V230331", template = "{freq}_D{di}_涨跌停V230331")] +pub fn bar_zdt_V230331(obs: &观察者, params: &HashMap) -> Vec { + let di = params::get_int(params, "di", 1) as usize; + let freq = params::get_string(params, "freq", "15分钟"); + let k1 = freq; + let k2 = format!("D{di}"); + let k3 = "涨跌停V230331"; + + let 普K序列 = &obs.普通K线序列; + if 普K序列.len() < di + 2 { + return vec![Signal::new_empty(&k1, &k2, k3)]; + } + + let 当前K线 = &普K序列[普K序列.len() - di]; + let 前K线 = &普K序列[普K序列.len() - di - 1]; + + let v1 = if 当前K线.收盘价 == 当前K线.高 && 当前K线.收盘价 >= 前K线.收盘价 + { + "涨停" + } else if 当前K线.收盘价 == 当前K线.低 && 当前K线.收盘价 <= 前K线.收盘价 { + "跌停" + } else { + "任意" + }; + + if v1 == "任意" { + vec![Signal::new_empty(&k1, &k2, k3)] + } else { + vec![Signal::new(&k1, &k2, k3, v1, "任意", "任意", 0)] + } +} + +// ============================================================================= +// tas — 技术指标信号 +// ============================================================================= + +/// MACD 金叉死叉信号 — DIF 与 DEA 的交叉判断。 +/// +/// DIF 上穿 DEA → 金叉;DIF 下穿 DEA → 死叉。 +#[signal( + name = "macd_金叉_V260601", + template = "{freq}_D{di}#MACD#{fast}#{slow}#{signal}_MACD交叉V260601" +)] +pub fn macd_金叉_V260601(obs: &观察者, params: &HashMap) -> Vec { + let fast = params::get_int(params, "fast", 13); + let slow = params::get_int(params, "slow", 31); + let signal_p = params::get_int(params, "signal", 11); + let di = params::get_int(params, "di", 1) as usize; + let freq = params::get_string(params, "freq", "15分钟"); + + let k1 = freq; + let k2 = format!("D{di}#MACD#{fast}#{slow}#{signal_p}"); + let k3 = "MACD交叉V260601"; + + obs.确保指标已计算(); + + let 普K序列 = &obs.普通K线序列; + if 普K序列.len() < di + 2 { + return vec![Signal::new_empty(&k1, &k2, k3)]; + } + + let 当前K线 = &普K序列[普K序列.len() - di]; + let 前K线 = &普K序列[普K序列.len() - di - 1]; + + let cur_dif = match 当前K线.macd().as_ref().and_then(|m| m.DIF) { + Some(v) => v, + None => return vec![Signal::new_empty(&k1, &k2, k3)], + }; + let cur_dea = match 当前K线.macd().as_ref().and_then(|m| m.DEA) { + Some(v) => v, + None => return vec![Signal::new_empty(&k1, &k2, k3)], + }; + let prev_dif = match 前K线.macd().as_ref().and_then(|m| m.DIF) { + Some(v) => v, + None => return vec![Signal::new_empty(&k1, &k2, k3)], + }; + let prev_dea = match 前K线.macd().as_ref().and_then(|m| m.DEA) { + Some(v) => v, + None => return vec![Signal::new_empty(&k1, &k2, k3)], + }; + + let v1 = if prev_dif <= prev_dea && cur_dif > cur_dea { + "金叉" + } else if prev_dif >= prev_dea && cur_dif < cur_dea { + "死叉" + } else { + "任意" + }; + + if v1 == "任意" { + vec![Signal::new_empty(&k1, &k2, k3)] + } else { + vec![Signal::new(&k1, &k2, k3, v1, "任意", "任意", 0)] + } +} + +/// MACD 方向信号 — DIF 在零轴上方为多头,下方为空头。 +#[signal( + name = "tas_macd_direct_V221106", + template = "{freq}_D{di}#MACD#{fast}#{slow}#{signal}_MACD方向V221106" +)] +pub fn tas_macd_direct_V221106(obs: &观察者, params: &HashMap) -> Vec { + let fast = params::get_int(params, "fast", 13); + let slow = params::get_int(params, "slow", 31); + let signal_p = params::get_int(params, "signal", 11); + let di = params::get_int(params, "di", 1) as usize; + let freq = params::get_string(params, "freq", "15分钟"); + + let k1 = freq; + let k2 = format!("D{di}#MACD#{fast}#{slow}#{signal_p}"); + let k3 = "MACD方向V221106"; + + obs.确保指标已计算(); + + let 普K序列 = &obs.普通K线序列; + if 普K序列.len() < di + 1 { + return vec![Signal::new_empty(&k1, &k2, k3)]; + } + + let 当前K线 = &普K序列[普K序列.len() - di]; + let cur_dif = match 当前K线.macd().as_ref().and_then(|m| m.DIF) { + Some(v) => v, + None => return vec![Signal::new_empty(&k1, &k2, k3)], + }; + + let v1 = if cur_dif > 0.0 { "看多" } else { "看空" }; + + let v2 = if 普K序列.len() >= di + 2 { + let 前K线 = &普K序列[普K序列.len() - di - 1]; + match 前K线.macd().as_ref().and_then(|m| m.DIF) { + Some(prev_dif) => { + if cur_dif > prev_dif { + "向上" + } else { + "向下" + } + } + None => "任意", + } + } else { + "任意" + }; + + vec![Signal::new(&k1, &k2, k3, v1, v2, "任意", 0)] +} + +// ============================================================================= +// 内部辅助 — 均线按需计算 +// ============================================================================= + +/// 按需计算均线值(SMA / EMA)。 +fn 计算均线( + 普K序列: &[std::sync::Arc], + ma_type: &str, + timeperiod: usize, + offset: usize, +) -> Option { + let n = 普K序列.len(); + let start = n.checked_sub(offset + timeperiod)?; + let end = n.checked_sub(offset)?; + if start >= end { + return None; + } + let closes: Vec = 普K序列[start..end].iter().map(|k| k.收盘价).collect(); + if closes.is_empty() { + return None; + } + match ma_type { + "SMA" | "sma" => Some(closes.iter().sum::() / closes.len() as f64), + "EMA" | "ema" => { + let k = 2.0 / (timeperiod as f64 + 1.0); + let mut ema = closes[0]; + for &price in &closes[1..] { + ema = price * k + ema * (1.0 - k); + } + Some(ema) + } + _ => None, + } +} + +/// 从均线缓存或按需计算获取均线值。 +fn 获取均线( + 普K序列: &[std::sync::Arc], + k线: &K线, + ma_type: &str, + timeperiod: usize, + offset: usize, +) -> Option { + let ma_key = format!("{}_{}", ma_type.to_uppercase(), timeperiod); + if let Some(ma_map) = k线.ma(&ma_key) { + return Some(ma_map); + } + 计算均线(普K序列, ma_type, timeperiod, offset) +} + +/// 单均线多空和方向信号。 +#[signal( + name = "tas_ma_base_V230313", + template = "{freq}_D{di}#{ma_type}#{timeperiod}MO{max_overlap}_BS辅助V230313" +)] +pub fn tas_ma_base_V230313(obs: &观察者, params: &HashMap) -> Vec { + let ma_type = params::get_string(params, "ma_type", "SMA").to_uppercase(); + let timeperiod = params::get_int(params, "timeperiod", 5) as usize; + let di = params::get_int(params, "di", 1) as usize; + let max_overlap = params::get_int(params, "max_overlap", 5); + let freq = params::get_string(params, "freq", "15分钟"); + + let k1 = freq; + let k2 = format!("D{di}#{ma_type}#{timeperiod}MO{max_overlap}"); + let k3 = "BS辅助V230313"; + + let 普K序列 = &obs.普通K线序列; + if 普K序列.len() < di + 1 { + return vec![Signal::new_empty(&k1, &k2, k3)]; + } + + let 当前K线 = &普K序列[普K序列.len() - di]; + let 当前均线 = match 获取均线(普K序列, 当前K线, &ma_type, timeperiod, di) { + Some(v) => v, + None => return vec![Signal::new_empty(&k1, &k2, k3)], + }; + + let v1 = if 当前K线.收盘价 > 当前均线 { + "看多" + } else { + "看空" + }; + + let v2 = if 普K序列.len() >= di + 2 { + let 前K线 = &普K序列[普K序列.len() - di - 1]; + match 获取均线(普K序列, 前K线, &ma_type, timeperiod, di + 1) { + Some(前均线) => { + if 当前均线 > 前均线 { + "向上" + } else { + "向下" + } + } + None => "任意", + } + } else { + "任意" + }; + + vec![Signal::new(&k1, &k2, k3, v1, v2, "任意", 0)] +} + +// ============================================================================= +// cxt — 缠论形态信号 +// ============================================================================= + +/// 停顿分型辅助信号 — 结合分型强度和 MACD 柱子匹配判断。 +#[signal( + name = "cxt_停顿分型_V230106", + template = "{freq}_D{di}停顿分型_BE辅助V230106" +)] +pub fn cxt_停顿分型_V230106(obs: &观察者, params: &HashMap) -> Vec { + let di = params::get_int(params, "di", 0) as usize; + let freq = params::get_string(params, "freq", "1分钟"); + + let k1 = freq; + let k2 = format!("D{di}停顿分型"); + let k3 = "BE辅助V230106"; + + let 分型序列 = &obs.分型序列; + if 分型序列.len() < di + 1 { + return vec![Signal::new_empty(&k1, &k2, k3)]; + } + + let 当前分型 = &分型序列[分型序列.len() - (di + 1)]; + + // 只对顶/底分型产出信号 + let 结构值 = 当前分型.结构.to_string(); + if 结构值 != "顶" && 结构值 != "底" { + return vec![Signal::new_empty(&k1, &k2, k3)]; + } + + let v1 = if 结构值 == "顶" { + "看空" + } else { + "看多" + }; + let v2 = 当前分型.强度(); + + // 仅强/中分型产出有效信号 + if v2 != "强" && v2 != "中" { + return vec![Signal::new_empty(&k1, &k2, k3)]; + } + + vec![Signal::new(&k1, &k2, k3, v1, v2, "任意", 0)] +} + +/// 笔结束辅助信号 — 统计最后笔之后的新高/新低分型次数。 +#[signal( + name = "cxt_bi_end_V230222", + template = "{freq}_D1MO{max_overlap}_BE辅助V230222" +)] +pub fn cxt_bi_end_V230222(obs: &观察者, params: &HashMap) -> Vec { + let max_overlap = params::get_int(params, "max_overlap", 3); + let freq = params::get_string(params, "freq", "日线"); + + let k1 = freq; + let k2 = format!("D1MO{max_overlap}"); + let k3 = "BE辅助V230222"; + + let 分型序列 = &obs.分型序列; + let 笔序列 = &obs.笔序列; + + if 分型序列.len() < 2 || 笔序列.is_empty() { + return vec![Signal::new_empty(&k1, &k2, k3)]; + } + + let 最后笔 = &笔序列[笔序列.len() - 1]; + let 当前分型 = &分型序列[分型序列.len() - 1]; + + // 找到最后笔的武(终点分型)在分型序列中的位置 + let 笔武分型: std::sync::Arc = + { 最后笔.武.read().clone() }; + + let 笔终点时间戳 = 笔武分型.时间戳; + let 笔终点结构 = 笔武分型.结构; + + let 笔终点索引 = match 分型序列 + .iter() + .position(|f| f.时间戳 == 笔终点时间戳 && f.结构 == 笔终点结构) + { + Some(idx) => idx, + None => return vec![Signal::new_empty(&k1, &k2, k3)], + }; + + // 取笔终点之后的分型 + if 笔终点索引 + 1 >= 分型序列.len() { + return vec![Signal::new_empty(&k1, &k2, k3)]; + } + let 未成笔分型 = &分型序列[笔终点索引 + 1..]; + + let 当前结构 = 当前分型.结构; + let 当前分型特征值 = 当前分型.分型特征值; + + if 当前结构.to_string() == "顶" { + let mut 笔终点顶高 = 笔武分型.分型特征值; + let mut 计数 = 0i32; + for f in 未成笔分型 { + if f.结构 == 当前结构 && f.分型特征值 > 笔终点顶高 { + 计数 += 1; + 笔终点顶高 = f.分型特征值; + } + } + if 计数 > 0 && 当前分型特征值 >= 笔终点顶高 { + vec![Signal::new( + &k1, + &k2, + k3, + "新高", + &format!("第{计数}次"), + "任意", + 0, + )] + } else { + vec![Signal::new_empty(&k1, &k2, k3)] + } + } else if 当前结构.to_string() == "底" { + let mut 笔终点底低 = 笔武分型.分型特征值; + let mut 计数 = 0i32; + for f in 未成笔分型 { + if f.结构 == 当前结构 && f.分型特征值 < 笔终点底低 { + 计数 += 1; + 笔终点底低 = f.分型特征值; + } + } + if 计数 > 0 && 当前分型特征值 <= 笔终点底低 { + vec![Signal::new( + &k1, + &k2, + k3, + "新低", + &format!("第{计数}次"), + "任意", + 0, + )] + } else { + vec![Signal::new_empty(&k1, &k2, k3)] + } + } else { + vec![Signal::new_empty(&k1, &k2, k3)] + } +} diff --git a/chanlun/src/signal/functions/mod.rs b/chanlun/src/signal/functions/mod.rs new file mode 100644 index 0000000..c155e2f --- /dev/null +++ b/chanlun/src/signal/functions/mod.rs @@ -0,0 +1,31 @@ +/* + * 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. + */ + +//! 信号函数实现 — 每个 `#[signal]` 注册的函数对应一个子模块。 +//! +//! 第三方代码声明:信号函数模式参考 czsc(https://github.com/waditu/czsc, +//! Apache License 2.0),已适配为 Rust `fn(&观察者, &HashMap) -> Vec`。 + +pub mod demo; +// pub mod youwukuncheng; diff --git a/chanlun/src/signal/mod.rs b/chanlun/src/signal/mod.rs new file mode 100644 index 0000000..ef5fc6d --- /dev/null +++ b/chanlun/src/signal/mod.rs @@ -0,0 +1,74 @@ +/* + * 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. + */ + +//! 信号匹配原语层。 +//! +//! 第三方代码声明:本模块的 Signal/Factor/Event/Position/Operate 匹配框架 +//! 摘录自 czsc 项目(https://github.com/waditu/czsc),Apache License 2.0 授权, +//! 已做中文命名适配与 Rust 重写。 + +use std::collections::HashMap; + +pub mod engine; +pub mod event; +pub mod factor; +pub mod ffi; +pub mod functions; +pub mod operate; +pub mod params; +pub mod position; +pub mod registry; +#[cfg(test)] +mod registry_macro_test; +#[allow(clippy::module_inception)] +pub mod signal; + +pub use event::Event; +pub use factor::Factor; +pub use operate::Operate; +pub use position::Position; +pub use signal::Signal; + +/// 信号字典中某个 key 对应的值。区分「字符串」与「非字符串」, +/// 以在纯 Rust 内表达 Python `is_match` 的三态:缺键 / 非 str / str。 +#[derive(Clone, Debug)] +pub enum 匹配值 { + 字符串(String), + 非字符串, +} + +/// 信号字典类型别名。 +pub type 信号字典 = HashMap; + +/// 缺键错误 — `is_match` 在信号字典中找不到 key 时返回。 +#[derive(Debug, Clone)] +pub struct 缺键错误(pub String); + +/// 对任意字节串算 sha256,取大写十六进制前 4 位(= 前 2 字节)。 +/// 对应 Python `hashlib.sha256(...).hexdigest().upper()[:4]`。 +pub(crate) fn sha256前4(输入: &str) -> String { + use sha2::{Digest, Sha256}; + let 摘要 = Sha256::digest(输入.as_bytes()); + format!("{:02X}{:02X}", 摘要[0], 摘要[1]) +} diff --git a/chanlun/src/signal/operate.rs b/chanlun/src/signal/operate.rs new file mode 100644 index 0000000..093b257 --- /dev/null +++ b/chanlun/src/signal/operate.rs @@ -0,0 +1,64 @@ +/* + * 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. + */ + +//! 缠论买卖操作类型。 + +/// 持仓/操作类型。值对应中文,与 Python `chan_external.Operate` 一致。 +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Operate { + 持多, // HL + 持空, // HS + 持币, // HO + 开多, // LO + 平多, // LE + 开空, // SO + 平空, // SE +} + +impl Operate { + /// 中文值,对应 Python Enum 的 `.value`。 + pub fn value(&self) -> &'static str { + match self { + Operate::持多 => "持多", + Operate::持空 => "持空", + Operate::持币 => "持币", + Operate::开多 => "开多", + Operate::平多 => "平多", + Operate::开空 => "开空", + Operate::平空 => "平空", + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_operate_value() { + assert_eq!(Operate::开多.value(), "开多"); + assert_eq!(Operate::平空.value(), "平空"); + assert_eq!(Operate::持币.value(), "持币"); + } +} diff --git a/chanlun/src/signal/params.rs b/chanlun/src/signal/params.rs new file mode 100644 index 0000000..615ca3d --- /dev/null +++ b/chanlun/src/signal/params.rs @@ -0,0 +1,52 @@ +/* + * 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. + */ + +//! 信号函数参数提取辅助 — 从 `HashMap` 中提取类型化参数。 + +use serde_json::Value; +use std::collections::HashMap; + +/// 提取字符串参数,缺失或类型不对时返回默认值。 +pub fn get_string(params: &HashMap, key: &str, default: &str) -> String { + params + .get(key) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + .unwrap_or_else(|| default.to_string()) +} + +/// 提取 i64 参数。 +pub fn get_int(params: &HashMap, key: &str, default: i64) -> i64 { + params.get(key).and_then(|v| v.as_i64()).unwrap_or(default) +} + +/// 提取 f64 参数。 +pub fn get_f64(params: &HashMap, key: &str, default: f64) -> f64 { + params.get(key).and_then(|v| v.as_f64()).unwrap_or(default) +} + +/// 提取字符串引用(零拷贝),缺失时返回默认值。 +pub fn get_str<'a>(params: &'a HashMap, key: &str, default: &'a str) -> &'a str { + params.get(key).and_then(|v| v.as_str()).unwrap_or(default) +} diff --git a/chanlun/src/signal/position.rs b/chanlun/src/signal/position.rs new file mode 100644 index 0000000..48502b4 --- /dev/null +++ b/chanlun/src/signal/position.rs @@ -0,0 +1,1037 @@ +/* + * 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. + */ + +//! 仓位配置 + 持仓状态机。 +//! +//! 第三方代码声明:Position 概念参考 czsc(https://github.com/waditu/czsc, +//! Apache License 2.0),状态机逻辑已从 Python 迁移到 Rust。 + +use crate::signal::event::Event; +use crate::signal::operate::Operate; +use crate::signal::{信号字典, 缺键错误}; + +// ============================================================================ +// 新类型定义 +// ============================================================================ + +/// 操作记录 — 对应 Python operate 字典(`__create_operate` 产出)。 +#[derive(Clone, Debug)] +pub struct 操作记录 { + pub symbol: String, + pub dt: i64, // Unix 秒 + pub bid: i64, + pub price: f64, + pub op: Operate, + pub op_desc: String, + pub pos: i32, +} + +/// 持仓快照 — 对应 Python holds 元素(`self.holds.append`)。 +#[derive(Clone, Debug)] +pub struct 持仓记录 { + pub dt: i64, + pub pos: i32, + pub price: f64, +} + +/// 开平配对 — `pairs` 属性的返回类型,对应 Python 的 pair 字典。 +#[derive(Clone, Debug)] +pub struct 开平配对 { + pub 标的代码: String, + pub 策略标记: String, + pub 交易方向: String, + pub 开仓时间: i64, + pub 平仓时间: i64, + pub 开仓价格: f64, + pub 平仓价格: f64, + pub 持仓K线数: i64, + pub 事件序列: String, + pub 持仓天数: f64, + pub 盈亏比例: f64, // 单位 BP (1BP = 0.0001) +} + +/// 最近事件缓存 — 对应 Python last_event 字典。 +#[derive(Clone, Debug, Default)] +pub struct 最近事件 { + pub dt: Option, + pub bid: Option, + pub price: Option, + pub op: Option, + pub op_desc: Option, +} + +// ============================================================================ +// Position 结构体 +// ============================================================================ + +#[derive(Clone, Debug)] +pub struct Position { + // --- 配置字段(不变)--- + pub symbol: String, + pub opens: Vec, + pub exits: Vec, + pub events: Vec, + pub name: String, + pub interval: i64, + pub timeout: i64, + pub stop_loss: i64, + pub T0: bool, + + // --- 状态字段(新增)--- + /// 仓位:1 = 多头,-1 = 空头,0 = 空仓 + pub pos: i32, + /// 本次 update 是否改变了仓位 + pub pos_changed: bool, + /// 事件触发的操作列表(时间顺序) + pub operates: Vec<操作记录>, + /// 每个时间步的持仓快照 + pub holds: Vec<持仓记录>, + /// 最近一次开仓事件缓存 + pub last_event: 最近事件, + /// 最近一次开多时间(Unix 秒) + pub last_lo_dt: Option, + /// 最近一次开空时间(Unix 秒) + pub last_so_dt: Option, + /// 最近一次信号传入时间 + pub end_dt: Option, +} + +impl Position { + /// 构造。name 必填;每个 event.operate 必须 ∈ {开多,平多,开空,平空}。 + #[allow(clippy::too_many_arguments)] + pub fn 新建( + symbol: String, + opens: Vec, + exits: Vec, + interval: i64, + timeout: i64, + stop_loss: i64, + T0: bool, + name: String, + ) -> Result { + if name.is_empty() { + return Err("name 是必须的参数".to_string()); + } + let mut events = opens.clone(); + events.extend(exits.clone()); + for e in &events { + if !matches!( + e.operate, + Operate::开多 | Operate::平多 | Operate::开空 | Operate::平空 + ) { + return Err(format!("非法 operate: {}", e.operate.value())); + } + } + Ok(Self { + symbol, + opens, + exits, + events, + name, + interval, + timeout, + stop_loss, + T0, + // 状态字段初始化为默认值 + pos: 0, + pos_changed: false, + operates: Vec::new(), + holds: Vec::new(), + last_event: 最近事件::default(), + last_lo_dt: None, + last_so_dt: None, + end_dt: None, + }) + } + + pub fn unique_signals(&self) -> Vec { + let mut 集合 = std::collections::BTreeSet::new(); + for e in &self.events { + for s in e.unique_signals() { + 集合.insert(s); + } + } + 集合.into_iter().collect() + } + + // ======================================================================== + // 内部辅助 — push_operate + // ======================================================================== + + /// 创建操作记录并追加到 operates 列表。 + fn push_operate(&mut self, dt: i64, bid: i64, price: f64, op: Operate, op_desc: &str) { + self.pos_changed = true; + self.operates.push(操作记录 { + symbol: self.symbol.clone(), + dt, + bid, + price, + op, + op_desc: op_desc.to_string(), + pos: self.pos, + }); + } + + // ======================================================================== + // update — 核心状态机 + // ======================================================================== + + /// 更新持仓状态。 + /// + /// 参数: + /// - `dt`: 信号时间(Unix 秒) + /// - `price`: 收盘价 + /// - `bid`: K线序号 + /// - `signals`: 信号字典(key → 匹配值) + /// + /// 逻辑与 Python `Position.update(s)` 1:1 对应。 + pub fn update( + &mut self, + dt: i64, + price: f64, + bid: i64, + signals: &信号字典, + ) -> Result<(), 缺键错误> { + // 1. 时间校验(信号时间不能倒退) + if let Some(end) = self.end_dt + && dt <= end + { + crate::warn!("请检查信号传入:最新信号时间{dt}在上次信号时间{end}之前"); + return Ok(()); + } + + self.pos_changed = false; + + // 2. 事件匹配 — 取第一个命中的事件 + let mut op = Operate::持币; + let mut op_desc = String::new(); + for event in &self.events { + let (m, f) = event.is_match(signals)?; + if m { + op = event.operate; + op_desc = format!("{}@{}", event.name, f.as_deref().unwrap_or("")); + break; + } + } + + // 3. 更新 end_dt + self.end_dt = Some(dt); + + // 4. 开仓事件 → 更新 last_event 缓存 + if matches!(op, Operate::开多 | Operate::开空) { + self.last_event = 最近事件 { + dt: Some(dt), + bid: Some(bid), + price: Some(price), + op: Some(op), + op_desc: Some(op_desc.clone()), + }; + } + + // 5. 开多处理 + if op == Operate::开多 { + if self.pos != 1 && 间隔检查(self.last_lo_dt, dt, self.interval) { + // 满足间隔要求 → 开多 + self.pos = 1; + self.push_operate(dt, bid, price, Operate::开多, &op_desc); + self.last_lo_dt = Some(dt); + } else { + // 不满足开多条件 → 仅平空 + if self.pos == -1 && 允许操作(self.T0, dt, self.last_so_dt) { + self.pos = 0; + self.push_operate(dt, bid, price, Operate::平空, &op_desc); + } + } + } + + // 6. 开空处理 + if op == Operate::开空 { + if self.pos != -1 && 间隔检查(self.last_so_dt, dt, self.interval) { + // 满足间隔要求 → 开空 + self.pos = -1; + self.push_operate(dt, bid, price, Operate::开空, &op_desc); + self.last_so_dt = Some(dt); + } else { + // 不满足开空条件 → 仅平多 + if self.pos == 1 && 允许操作(self.T0, dt, self.last_lo_dt) { + self.pos = 0; + self.push_operate(dt, bid, price, Operate::平多, &op_desc); + } + } + } + + // 7. 多头出场 + if self.pos == 1 && 允许操作(self.T0, dt, self.last_lo_dt) { + // 安全断言:last_event 的时间不应早于开仓时间 + if let Some(le_dt) = self.last_event.dt + && let Some(lo_dt) = self.last_lo_dt + { + debug_assert!( + le_dt >= lo_dt, + "last_event.dt({le_dt}) < last_lo_dt({lo_dt})" + ); + } + + // 7a. 平多信号 + if op == Operate::平多 { + self.pos = 0; + self.push_operate(dt, bid, price, Operate::平多, &op_desc); + } + + // 7b. 多头止损 + if let Some(last_price) = self.last_event.price + && price / last_price - 1.0 < -(self.stop_loss as f64) / 10000.0 + && self.pos != 0 + { + self.pos = 0; + self.push_operate( + dt, + bid, + price, + Operate::平多, + &format!("平多@{}BP止损", self.stop_loss), + ); + } + + // 7c. 多头超时 + if let Some(last_bid) = self.last_event.bid + && bid - last_bid > self.timeout + && self.pos != 0 + { + self.pos = 0; + self.push_operate( + dt, + bid, + price, + Operate::平多, + &format!("平多@{}K超时", self.timeout), + ); + } + } + + // 8. 空头出场 + if self.pos == -1 && 允许操作(self.T0, dt, self.last_so_dt) { + if let Some(le_dt) = self.last_event.dt + && let Some(so_dt) = self.last_so_dt + { + debug_assert!( + le_dt >= so_dt, + "last_event.dt({le_dt}) < last_so_dt({so_dt})" + ); + } + + // 8a. 平空信号 + if op == Operate::平空 { + self.pos = 0; + self.push_operate(dt, bid, price, Operate::平空, &op_desc); + } + + // 8b. 空头止损 + if let Some(last_price) = self.last_event.price + && 1.0 - price / last_price < -(self.stop_loss as f64) / 10000.0 + && self.pos != 0 + { + self.pos = 0; + self.push_operate( + dt, + bid, + price, + Operate::平空, + &format!("平空@{}BP止损", self.stop_loss), + ); + } + + // 8c. 空头超时 + if let Some(last_bid) = self.last_event.bid + && bid - last_bid > self.timeout + && self.pos != 0 + { + self.pos = 0; + self.push_operate( + dt, + bid, + price, + Operate::平空, + &format!("平空@{}K超时", self.timeout), + ); + } + } + + // 9. 记录持仓快照 + self.holds.push(持仓记录 { + dt, + pos: self.pos, + price, + }); + + Ok(()) + } + + // ======================================================================== + // pairs — 开平配对 + // ======================================================================== + + /// 从 operates 列表计算开平配对。 + /// + /// 遍历配对相邻操作(op1, op2),其中 op1 是开仓(LO/SO),op2 是平仓。 + /// 盈亏比例单位为 BP (1BP = 0.0001)。 + pub fn pairs(&self) -> Vec<开平配对> { + let mut result = Vec::new(); + for pair in self.operates.windows(2) { + let op1 = &pair[0]; + let op2 = &pair[1]; + if !matches!(op1.op, Operate::开多 | Operate::开空) { + continue; + } + let ykr = if op1.op == Operate::开多 { + op2.price / op1.price - 1.0 + } else { + 1.0 - op2.price / op1.price + }; + let 持仓天数 = (op2.dt - op1.dt) as f64 / (24.0 * 3600.0); + result.push(开平配对 { + 标的代码: self.symbol.clone(), + 策略标记: self.name.clone(), + 交易方向: if op1.op == Operate::开多 { + "多头".to_string() + } else { + "空头".to_string() + }, + 开仓时间: op1.dt, + 平仓时间: op2.dt, + 开仓价格: op1.price, + 平仓价格: op2.price, + 持仓K线数: op2.bid - op1.bid, + 事件序列: format!("{} -> {}", op1.op_desc, op2.op_desc), + 持仓天数: (持仓天数 * 100.0).round() / 100.0, + 盈亏比例: (ykr * 10000.0 * 100.0).round() / 100.0, // BP, 2 decimal places + }); + } + result + } + + // ======================================================================== + // dump / load — 序列化 + // ======================================================================== + + /// 序列化配置为 JSON Value。`with_data` 为 true 时附带 pairs 和 holds。 + /// + /// 注意:Event/Factor/Signal 的序列化由 PyO3 层(EventPy.dump())处理, + /// 本方法仅序列化 Position 自身的字段。完整的 dict 由 PyO3 PositionPy::dump() 组装。 + pub fn dump_config(&self) -> serde_json::Value { + serde_json::json!({ + "symbol": self.symbol, + "name": self.name, + "interval": self.interval, + "timeout": self.timeout, + "stop_loss": self.stop_loss, + "T0": self.T0, + }) + } + + /// 从 JSON Value 构造 Position(仅配置,不含 opens/exits/events)。 + /// opens/exits 需由调用方通过 Event 反序列化后传入。 + /// + /// 状态字段初始化为默认值(等同于新仓)。 + pub fn load_config(raw: &serde_json::Value) -> Result { + let symbol = raw["symbol"].as_str().unwrap_or("").to_string(); + let name = raw["name"].as_str().unwrap_or("").to_string(); + let interval = raw["interval"].as_i64().unwrap_or(0); + let timeout = raw["timeout"].as_i64().unwrap_or(1000); + let stop_loss = raw["stop_loss"].as_i64().unwrap_or(1000); + let T0 = raw["T0"].as_bool().unwrap_or(false); + Self::新建( + symbol, + vec![], + vec![], + interval, + timeout, + stop_loss, + T0, + name, + ) + } +} + +// ============================================================================ +// 内部辅助函数 +// ============================================================================ + +/// 判断两个 Unix 时间戳是否属于同一 UTC 日期。 +fn 同一交易日(a: i64, b: i64) -> bool { + const SECS_PER_DAY: i64 = 86400; + a / SECS_PER_DAY == b / SECS_PER_DAY +} + +/// 开仓间隔检查。 +/// +/// - `None` → 从未开仓,允许。 +/// - `Some(last)` → interval == 0 不限制;或距上次超过 interval 秒。 +fn 间隔检查(last_dt: Option, current_dt: i64, interval: i64) -> bool { + match last_dt { + None => true, + Some(last) => interval == 0 || (current_dt - last) > interval, + } +} + +/// 是否允许对持仓进行操作。 +/// +/// `T0 == true` → 总是允许(日内回转)。 +/// `T0 == false` → 仅当不在同一天时允许。 +fn 允许操作(T0: bool, dt: i64, last_dt: Option) -> bool { + if T0 { + return true; + } + match last_dt { + None => true, + Some(last) => !同一交易日(dt, last), + } +} + +// ============================================================================ +// 测试 +// ============================================================================ + +#[cfg(test)] +mod tests { + use super::*; + use crate::signal::factor::Factor; + use crate::signal::signal::Signal; + use crate::signal::匹配值; + use std::collections::HashMap; + + // ---- 测试辅助 ---- + + fn 开多事件() -> Event { + let s = Signal::new("14400", "D1MO3", "中枢", "任意", "三买", "任意", 0); + let f = Factor::新建(vec![s], vec![], vec![], "".into()).unwrap(); + Event::新建(Operate::开多, vec![f], vec![], vec![], vec![], "".into()).unwrap() + } + + fn 平多事件() -> Event { + let s = Signal::new("14400", "D1MO3", "中枢", "任意", "三卖", "任意", 0); + let f = Factor::新建(vec![s], vec![], vec![], "".into()).unwrap(); + Event::新建(Operate::平多, vec![f], vec![], vec![], vec![], "".into()).unwrap() + } + + fn 开空事件() -> Event { + let s = Signal::new("14400", "D1MO3", "中枢", "任意", "三卖", "任意", 0); + let f = Factor::新建(vec![s], vec![], vec![], "".into()).unwrap(); + Event::新建(Operate::开空, vec![f], vec![], vec![], vec![], "".into()).unwrap() + } + + fn 平空事件() -> Event { + let s = Signal::new("14400", "D1MO3", "中枢", "任意", "三买", "任意", 0); + let f = Factor::新建(vec![s], vec![], vec![], "".into()).unwrap(); + Event::新建(Operate::平空, vec![f], vec![], vec![], vec![], "".into()).unwrap() + } + + /// 构造一个"三买"信号字典(匹配开多/平空事件) + fn 三买信号字典() -> HashMap { + let mut m = HashMap::new(); + m.insert( + "14400_D1MO3_中枢".to_string(), + 匹配值::字符串("任意_三买_任意_0".to_string()), + ); + m + } + + /// 构造一个"三卖"信号字典(匹配平多/开空事件) + fn 三卖信号字典() -> HashMap { + let mut m = HashMap::new(); + m.insert( + "14400_D1MO3_中枢".to_string(), + 匹配值::字符串("任意_三卖_任意_0".to_string()), + ); + m + } + + /// 构造一个无匹配信号的字典(key 存在但 value 不匹配任何信号) + fn 无匹配信号字典() -> HashMap { + let mut m = HashMap::new(); + m.insert( + "14400_D1MO3_中枢".to_string(), + 匹配值::字符串("任意_无_任意_0".to_string()), + ); + m + } + + // ---- 构造函数测试 ---- + + #[test] + fn test_name_缺失_报错() { + assert!( + Position::新建( + "btc".into(), + vec![开多事件()], + vec![], + 0, + 1000, + 1000, + false, + "".into() + ) + .is_err() + ); + } + + #[test] + fn test_构造成功() { + let p = Position::新建( + "btc".into(), + vec![开多事件()], + vec![], + 0, + 1000, + 1000, + false, + "中枢".into(), + ) + .unwrap(); + assert_eq!(p.name, "中枢"); + assert_eq!(p.events.len(), 1); + assert_eq!(p.pos, 0); + assert!(p.operates.is_empty()); + assert!(p.holds.is_empty()); + } + + #[test] + fn test_unique_signals_去重() { + let p = Position::新建( + "btc".into(), + vec![开多事件(), 开多事件()], + vec![], + 0, + 1000, + 1000, + false, + "中枢".into(), + ) + .unwrap(); + assert_eq!(p.unique_signals().len(), 1); + } + + // ---- update 测试 ---- + + #[test] + fn test_update_空事件列表() { + let mut p = Position::新建( + "btc".into(), + vec![], + vec![], + 0, + 1000, + 1000, + false, + "空".into(), + ) + .unwrap(); + p.update(1000, 50000.0, 1, &三买信号字典()).unwrap(); + assert_eq!(p.pos, 0); // op stays HO, pos unchanged + assert_eq!(p.holds.len(), 1); + } + + #[test] + fn test_update_时间倒退_跳过() { + let mut p = Position::新建( + "btc".into(), + vec![开多事件()], + vec![], + 0, + 1000, + 1000, + false, + "测试".into(), + ) + .unwrap(); + // First update at t=2000 + p.update(2000, 50000.0, 2, &三买信号字典()).unwrap(); + let operates_before = p.operates.len(); + let holds_before = p.holds.len(); + // Second update at earlier time → skipped + p.update(1000, 50000.0, 1, &三买信号字典()).unwrap(); + assert_eq!(p.operates.len(), operates_before); + assert_eq!(p.holds.len(), holds_before); + } + + #[test] + fn test_update_开多() { + let mut p = Position::新建( + "btc".into(), + vec![开多事件()], + vec![], + 0, + 1000, + 1000, + false, + "测试".into(), + ) + .unwrap(); + p.update(1000, 50000.0, 1, &三买信号字典()).unwrap(); + assert_eq!(p.pos, 1); + assert!(p.pos_changed); + assert_eq!(p.operates.len(), 1); + assert_eq!(p.operates[0].op, Operate::开多); + assert_eq!(p.holds.len(), 1); + } + + #[test] + fn test_update_开多_间隔限制_interval内不重复开仓() { + let mut p = Position::新建( + "btc".into(), + vec![开多事件()], + vec![], + 3600, // interval = 1 hour + 1000, + 1000, + false, + "测试".into(), + ) + .unwrap(); + // First: open long + p.update(1000, 50000.0, 1, &三买信号字典()).unwrap(); + assert_eq!(p.pos, 1); + assert_eq!(p.operates.len(), 1); + // Second: within interval, already long → no new open + p.update(2000, 51000.0, 2, &三买信号字典()).unwrap(); + assert_eq!(p.pos, 1); + assert_eq!(p.operates.len(), 1); // no new operate + } + + #[test] + fn test_update_开多_间隔后允许() { + let mut p = Position::新建( + "btc".into(), + vec![开多事件()], + vec![], + 3600, + 1000, + 1000, + false, + "测试".into(), + ) + .unwrap(); + p.update(1000, 50000.0, 1, &三买信号字典()).unwrap(); + // First close manually by calling with LE event + // then re-open after interval + // Actually, let's test: after interval expires from another position... + // Reset position to 0, then try again after sufficient time + p.pos = 0; // simulate close + p.update(6000, 51000.0, 2, &三买信号字典()).unwrap(); + assert_eq!(p.pos, 1); + assert_eq!(p.operates.len(), 2); // new open added + } + + #[test] + fn test_update_平多_空仓不执行() { + let mut p = Position::新建( + "btc".into(), + vec![开多事件()], + vec![平多事件()], + 0, + 1000, + 1000, + false, + "测试".into(), + ) + .unwrap(); + // LE signal when pos=0 → nothing + p.update(1000, 50000.0, 1, &三卖信号字典()).unwrap(); + assert_eq!(p.pos, 0); + // 平多事件 matched first (三卖), op=平多, but pos=0, none of the exit blocks apply + assert!(p.operates.is_empty()); + } + + #[test] + fn test_update_开多后平多() { + let mut p = Position::新建( + "btc".into(), + vec![开多事件()], + vec![平多事件()], + 0, + 1000, + 1000, + false, + "测试".into(), + ) + .unwrap(); + // Step 1: LO + p.update(1000, 50000.0, 1, &三买信号字典()).unwrap(); + assert_eq!(p.pos, 1); + assert_eq!(p.operates.len(), 1); + // Step 2: LE (三卖 matches 平多 first) + p.update(86600, 49000.0, 2, &三卖信号字典()).unwrap(); + // With T0=false and different day (t=1000 vs t=86600), 允许操作 = true + assert_eq!(p.pos, 0, "pos should be 0 after exit"); + assert_eq!(p.operates.len(), 2); + assert_eq!(p.operates[1].op, Operate::平多); + } + + #[test] + fn test_update_开空() { + let mut p = Position::新建( + "btc".into(), + vec![开空事件()], + vec![], + 0, + 1000, + 1000, + false, + "测试".into(), + ) + .unwrap(); + p.update(1000, 50000.0, 1, &三卖信号字典()).unwrap(); + assert_eq!(p.pos, -1); + assert!(p.pos_changed); + assert_eq!(p.operates.len(), 1); + assert_eq!(p.operates[0].op, Operate::开空); + } + + #[test] + fn test_update_止损_多头() { + let mut p = Position::新建( + "btc".into(), + vec![开多事件()], + vec![], + 0, + 1000, + 500, // stop_loss = 500 BP + false, + "测试".into(), + ) + .unwrap(); + // Open long + p.update(1000, 50000.0, 1, &三买信号字典()).unwrap(); + assert_eq!(p.pos, 1); + // Price drops: 50000 → 47000 = -6% = -600 BP → triggers stop_loss at -500 BP + // Different day to allow exit + p.update(86600, 47000.0, 2, &无匹配信号字典()).unwrap(); + assert_eq!(p.pos, 0, "should be stopped out"); + assert_eq!(p.operates.len(), 2); + assert_eq!(p.operates[1].op, Operate::平多); + assert!(p.operates[1].op_desc.contains("止损")); + } + + #[test] + fn test_update_止损_空头() { + let mut p = Position::新建( + "btc".into(), + vec![开空事件()], + vec![], + 0, + 1000, + 500, + false, + "测试".into(), + ) + .unwrap(); + // Open short + p.update(1000, 50000.0, 1, &三卖信号字典()).unwrap(); + assert_eq!(p.pos, -1); + // Price rises: 50000 → 53000 = +6% → for short: 1 - 53000/50000 = -0.06 = -600 BP → stop + p.update(86600, 53000.0, 2, &无匹配信号字典()).unwrap(); + assert_eq!(p.pos, 0, "should be stopped out"); + assert_eq!(p.operates.len(), 2); + assert_eq!(p.operates[1].op, Operate::平空); + assert!(p.operates[1].op_desc.contains("止损")); + } + + #[test] + fn test_update_超时_多头() { + let mut p = Position::新建( + "btc".into(), + vec![开多事件()], + vec![], + 0, + 5, // timeout = 5 bars + 1000, + false, + "测试".into(), + ) + .unwrap(); + p.update(1000, 50000.0, 1, &三买信号字典()).unwrap(); + assert_eq!(p.pos, 1); + // bid jumps from 1 to 10, diff=9 > timeout=5 → exit + p.update(86600, 50000.0, 10, &无匹配信号字典()).unwrap(); + assert_eq!(p.pos, 0, "should be timed out"); + assert!(p.operates.last().unwrap().op_desc.contains("超时")); + } + + #[test] + fn test_update_无匹配事件_仅追加holds() { + let mut p = Position::新建( + "btc".into(), + vec![开多事件()], + vec![], + 0, + 1000, + 1000, + false, + "测试".into(), + ) + .unwrap(); + // 无匹配信号字典 → no event matches → op stays HO + p.update(1000, 50000.0, 1, &无匹配信号字典()).unwrap(); + assert_eq!(p.pos, 0); + assert!(p.operates.is_empty()); + assert_eq!(p.holds.len(), 1); + assert_eq!(p.holds[0].pos, 0); + } + + // ---- pairs 测试 ---- + + #[test] + fn test_pairs_空操作() { + let p = Position::新建( + "btc".into(), + vec![开多事件()], + vec![], + 0, + 1000, + 1000, + false, + "测试".into(), + ) + .unwrap(); + assert!(p.pairs().is_empty()); + } + + #[test] + fn test_pairs_单笔开平() { + let mut p = Position::新建( + "btc".into(), + vec![开多事件()], + vec![平多事件()], + 0, + 1000, + 1000, + false, + "测试".into(), + ) + .unwrap(); + p.update(1000, 50000.0, 1, &三买信号字典()).unwrap(); // LO + p.update(86600, 51000.0, 2, &三卖信号字典()).unwrap(); // LE + let pairs = p.pairs(); + assert_eq!(pairs.len(), 1); + assert_eq!(pairs[0].交易方向, "多头"); + assert_eq!(pairs[0].开仓价格, 50000.0); + assert_eq!(pairs[0].平仓价格, 51000.0); + // ykr = (51000/50000 - 1) * 10000 = 200 BP + assert!(pairs[0].盈亏比例 > 0.0); + } + + #[test] + fn test_pairs_多头盈亏计算() { + let mut p = Position::新建( + "btc".into(), + vec![开多事件()], + vec![平多事件()], + 0, + 1000, + 1000, + false, + "测试".into(), + ) + .unwrap(); + p.update(1000, 50000.0, 1, &三买信号字典()).unwrap(); // LO @ 50000 + p.update(86600, 48000.0, 2, &三卖信号字典()).unwrap(); // LE @ 48000 + let pairs = p.pairs(); + assert_eq!(pairs.len(), 1); + // ykr = 48000/50000 - 1 = -0.04 → -400 BP + assert!((pairs[0].盈亏比例 - (-400.0)).abs() < 0.1); + } + + #[test] + fn test_pairs_空头盈亏计算() { + let mut p = Position::新建( + "btc".into(), + vec![开空事件()], + vec![平空事件()], + 0, + 1000, + 1000, + false, + "测试".into(), + ) + .unwrap(); + p.update(1000, 50000.0, 1, &三卖信号字典()).unwrap(); // SO @ 50000 + p.update(86600, 48000.0, 2, &三买信号字典()).unwrap(); // SE @ 48000 + let pairs = p.pairs(); + assert_eq!(pairs.len(), 1); + // ykr = 1 - 48000/50000 = 0.04 → 400 BP (short profit) + assert!((pairs[0].盈亏比例 - 400.0).abs() < 0.1); + } + + // ---- 辅助函数测试 ---- + + #[test] + fn test_同一交易日_同一天() { + // 2020-01-01 00:00:00 UTC = 1577836800 + // 2020-01-01 23:59:59 UTC = 1577923199 + assert!(同一交易日(1577836800, 1577923199)); + } + + #[test] + fn test_同一交易日_不同天() { + // 2020-01-01 23:59:59 → 2020-01-02 00:00:00 + assert!(!同一交易日(1577923199, 1577923200)); + } + + #[test] + fn test_间隔检查_none允许() { + assert!(间隔检查(None, 1000, 3600)); + } + + #[test] + fn test_间隔检查_interval0不限制() { + assert!(间隔检查(Some(1000), 2000, 0)); + } + + #[test] + fn test_间隔检查_超过interval() { + // last=1000, interval=500 → need t > 1500 + assert!(间隔检查(Some(1000), 2000, 500)); + } + + #[test] + fn test_间隔检查_未超过interval() { + assert!(!间隔检查(Some(1000), 1200, 500)); + } + + #[test] + fn test_允许操作_T0模式() { + // T0 → always allow, even same day + assert!(允许操作(true, 1577836800, Some(1577836800))); + } + + #[test] + fn test_允许操作_非T0_同一天拒绝() { + assert!(!允许操作(false, 1577836800, Some(1577836800))); + } + + #[test] + fn test_允许操作_非T0_不同天允许() { + assert!(允许操作(false, 1577923200, Some(1577836800))); + } +} diff --git a/chanlun/src/signal/registry.rs b/chanlun/src/signal/registry.rs new file mode 100644 index 0000000..77a4db9 --- /dev/null +++ b/chanlun/src/signal/registry.rs @@ -0,0 +1,282 @@ +/* + * 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. + */ + +//! 信号注册表 —— 编译期收集 + 运行时动态注册。 +//! +//! 第三方代码声明:注册机制参考 czsc(https://github.com/waditu/czsc, +//! Apache License 2.0),已简化适配(无 category / TaCache)。 +//! +//! # 双注册表架构 +//! +//! - `SIGNAL_REGISTRY`: 编译时,`#[signal]` 宏 + `inventory`,不可变。 +//! - `DYNAMIC_REGISTRY`: 运行时,`register_signal()` / `unregister_signal()`,`RwLock`。 +//! +//! 查找时先查编译时,再查动态。同名时编译时优先(动态注册被遮蔽)。 + +use crate::business::observer::观察者; +use crate::signal::Signal; +use parking_lot::RwLock; +use serde_json::Value; +use std::collections::HashMap; +use std::sync::LazyLock; + +/// 信号函数签名 —— 读观察者状态(含 K线已挂指标)+ 参数 → 信号列表。无 TaCache。 +pub type SignalFn = fn(&观察者, &HashMap) -> Vec; + +/// 信号描述符(编译期元数据,由 `#[signal]` 宏生成、`inventory` 收集)。 +#[derive(Clone, Copy)] +pub struct SignalDescriptor { + pub name: &'static str, + pub template: &'static str, + pub func: SignalFn, +} + +inventory::collect!(SignalDescriptor); + +/// 运行时信号元信息(编译时和动态共用)。 +#[derive(Clone, Debug)] +pub struct SignalMeta { + pub func: SignalFn, + pub template: String, +} + +/// 归并描述符为注册表;重名返回 Err。 +fn 归并( + descs: impl Iterator, +) -> Result, String> { + let mut m: HashMap<&'static str, SignalMeta> = HashMap::new(); + for d in descs { + if m.insert( + d.name, + SignalMeta { + func: d.func, + template: d.template.to_string(), + }, + ) + .is_some() + { + return Err(format!("信号重名:{}", d.name)); + } + } + Ok(m) +} + +/// 编译时注册表(`#[signal]` 宏,inventory 收集,不可变)。 +pub static SIGNAL_REGISTRY: LazyLock> = LazyLock::new(|| { + 归并(inventory::iter::.into_iter().copied()).unwrap_or_else(|e| panic!("{e}")) +}); + +/// 动态注册表(运行时注册,RwLock)。 +pub static DYNAMIC_REGISTRY: LazyLock>> = + LazyLock::new(|| RwLock::new(HashMap::new())); + +// ============================================================================ +// 查询 API +// ============================================================================ + +/// 按名查信号元信息。先查编译时,再查动态。 +pub fn get_signal(name: &str) -> Option { + if let Some(m) = SIGNAL_REGISTRY.get(name) { + return Some(m.clone()); + } + DYNAMIC_REGISTRY.read().get(name).cloned() +} + +/// 按名查参数模板。 +pub fn get_template(name: &str) -> Option { + get_signal(name).map(|m| m.template) +} + +/// 列出所有已注册信号名(编译时 + 动态,排序)。 +pub fn list_signal_names() -> Vec { + let mut v: Vec = SIGNAL_REGISTRY.keys().map(|k| k.to_string()).collect(); + for k in DYNAMIC_REGISTRY.read().keys() { + if !SIGNAL_REGISTRY.contains_key(k.as_str()) { + v.push(k.clone()); + } + } + v.sort(); + v +} + +// ============================================================================ +// 动态注册 API +// ============================================================================ + +/// 运行时动态注册信号函数。 +/// +/// - `name`: 信号名(必须全局唯一) +/// - `template`: 参数模板,如 `"{freq}_D{di}_涨跌停V230331"` +/// - `func`: 信号函数指针 +/// +/// 返回 `Err` 如果同名信号已存在于编译时或动态注册表中。 +pub fn register_signal(name: &str, template: &str, func: SignalFn) -> Result<(), String> { + if SIGNAL_REGISTRY.contains_key(name) { + return Err(format!("信号 '{name}' 已在编译时注册表中,无法覆盖")); + } + let mut dyn_reg = DYNAMIC_REGISTRY.write(); + if dyn_reg.contains_key(name) { + return Err(format!("信号 '{name}' 已在动态注册表中")); + } + dyn_reg.insert( + name.to_string(), + SignalMeta { + func, + template: template.to_string(), + }, + ); + Ok(()) +} + +/// 从动态注册表中移除信号。返回 `Err` 如果信号不存在或属于编译时注册表。 +pub fn unregister_signal(name: &str) -> Result<(), String> { + if SIGNAL_REGISTRY.contains_key(name) { + return Err(format!("信号 '{name}' 属于编译时注册表,无法动态移除")); + } + let mut dyn_reg = DYNAMIC_REGISTRY.write(); + if dyn_reg.remove(name).is_none() { + return Err(format!("信号 '{name}' 不在动态注册表中")); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn __probe(_obs: &观察者, _p: &HashMap) -> Vec { + Vec::new() + } + + fn 描述符(name: &'static str) -> SignalDescriptor { + SignalDescriptor { + name, + template: "{freq}_D1_probe", + func: __probe, + } + } + + #[test] + fn test_归并_正常() { + let m = 归并([描述符("a_V000001"), 描述符("b_V000001")].into_iter()).unwrap(); + assert_eq!(m.len(), 2); + assert!(m.contains_key("a_V000001")); + assert_eq!(m["a_V000001"].template, "{freq}_D1_probe"); + } + + #[test] + fn test_归并_重名_返回Err() { + let r = 归并([描述符("dup_V000001"), 描述符("dup_V000001")].into_iter()); + assert!(r.is_err()); + assert!(r.unwrap_err().contains("信号重名")); + } + + // ── 动态注册测试 ── + + #[test] + fn test_动态注册成功() { + assert!(register_signal("__dyn_test_V000001", "{freq}_D1_test", __probe).is_ok()); + let meta = get_signal("__dyn_test_V000001").unwrap(); + assert_eq!(meta.template, "{freq}_D1_test"); + // 清理 + unregister_signal("__dyn_test_V000001").unwrap(); + } + + #[test] + fn test_动态重名_报错() { + register_signal("__dyn_dup_V000001", "{freq}_D1_a", __probe).unwrap(); + let r = register_signal("__dyn_dup_V000001", "{freq}_D1_b", __probe); + assert!(r.is_err()); + assert!(r.unwrap_err().contains("已在动态注册表中")); + unregister_signal("__dyn_dup_V000001").unwrap(); + } + + #[test] + fn test_动态覆盖编译时_报错() { + // 编译时已注册的信号不允许动态覆盖 + let r = register_signal("__probe_inventory_V000000", "{freq}_test", __probe); + assert!(r.is_err()); + assert!(r.unwrap_err().contains("已")); + } + + #[test] + fn test_注销_成功() { + register_signal("__dyn_rm_V000001", "{freq}_D1_rm", __probe).unwrap(); + assert!(get_signal("__dyn_rm_V000001").is_some()); + unregister_signal("__dyn_rm_V000001").unwrap(); + assert!(get_signal("__dyn_rm_V000001").is_none()); + } + + #[test] + fn test_注销编译时_报错() { + let r = unregister_signal("__probe_inventory_V000000"); + assert!(r.is_err()); + } + + #[test] + fn test_list_包含动态信号() { + register_signal("__list_dyn_V000001", "{freq}_test", __probe).unwrap(); + let names = list_signal_names(); + assert!(names.contains(&"__list_dyn_V000001".to_string())); + // 编译时信号也在 + assert!(names.contains(&"__probe_inventory_V000000".to_string())); + unregister_signal("__list_dyn_V000001").unwrap(); + } + + #[test] + fn test_动态优先_编译时不遮蔽() { + // 编译时信号正常返回 + let meta = get_signal("__probe_inventory_V000000").unwrap(); + assert_eq!(meta.template, "{freq}_D1_probe_inventory"); + } +} + +#[cfg(test)] +fn __probe_for_inventory(_obs: &观察者, _p: &HashMap) -> Vec { + Vec::new() +} + +#[cfg(test)] +inventory::submit! { + SignalDescriptor { + name: "__probe_inventory_V000000", + template: "{freq}_D1_probe_inventory", + func: __probe_for_inventory as SignalFn, + } +} + +#[cfg(test)] +mod inventory_tests { + use super::*; + + #[test] + fn test_全局注册表收到inventory探针() { + assert!(get_signal("__probe_inventory_V000000").is_some()); + assert_eq!( + get_template("__probe_inventory_V000000"), + Some("{freq}_D1_probe_inventory".to_string()) + ); + assert!(list_signal_names().contains(&"__probe_inventory_V000000".to_string())); + } +} diff --git a/chanlun/src/signal/registry_macro_test.rs b/chanlun/src/signal/registry_macro_test.rs new file mode 100644 index 0000000..9ed1884 --- /dev/null +++ b/chanlun/src/signal/registry_macro_test.rs @@ -0,0 +1,63 @@ +/* + * 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. + */ + +//! 端到端:#[signal] 宏 + inventory 注册表协同。 +//! 放在 chanlun lib 内(非 tests/ 外部集成测试),因 #[signal] emit 的是 +//! `crate::signal::registry::` 路径,只有在 chanlun crate 内才能解析。 +#![cfg(test)] + +use std::collections::HashMap; + +use crate::business::observer::观察者; +use crate::signal::Signal; +use crate::signal::registry::{get_signal, get_template, list_signal_names}; +use chanlun_signal_macros::signal; +use serde_json::Value; + +/// 探针信号函数:贴 #[signal] 后应被自动注册进 SIGNAL_REGISTRY。 +#[signal( + name = "test_probe_signal_V230101", + template = "{freq}_D1MO{max_overlap}_test_probe_signalV230101" +)] +fn test_probe_signal_V230101(_obs: &观察者, _params: &HashMap) -> Vec { + Vec::new() +} + +#[test] +fn test_signal_宏自动注册到全局表() { + assert!( + get_signal("test_probe_signal_V230101").is_some(), + "#[signal] 应把探针函数注册进 SIGNAL_REGISTRY" + ); + assert_eq!( + get_template("test_probe_signal_V230101"), + Some("{freq}_D1MO{max_overlap}_test_probe_signalV230101".to_string()) + ); + assert!(list_signal_names().contains(&"test_probe_signal_V230101".to_string())); +} + +#[test] +fn test_未注册信号返回None() { + assert!(get_signal("不存在的信号_V999999").is_none()); +} diff --git a/chanlun/src/signal/signal.rs b/chanlun/src/signal/signal.rs new file mode 100644 index 0000000..2338b1c --- /dev/null +++ b/chanlun/src/signal/signal.rs @@ -0,0 +1,188 @@ +/* + * 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. + */ + +//! 信号原语 — k1_k2_k3_v1_v2_v3_score 七段字符串。 + +use crate::signal::{信号字典, 匹配值, 缺键错误}; + +pub(crate) const 任意: &str = "任意"; + +/// 单个信号。字段与 Python `chan_external.Signal` 一致。 +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Signal { + pub signal: String, + pub score: i32, + pub k1: String, + pub k2: String, + pub k3: String, + pub v1: String, + pub v2: String, + pub v3: String, +} + +impl Signal { + /// 创建"空"信号(v1=v2=v3="任意",score=0)。 + /// 对应 Python `create_single_signal(k1=k1, k2=k2, k3=k3)` 的默认返回值。 + pub fn new_empty(k1: &str, k2: &str, k3: &str) -> Self { + let signal = format!("{k1}_{k2}_{k3}_任意_任意_任意_0"); + Self { + signal, + score: 0, + k1: k1.to_string(), + k2: k2.to_string(), + k3: k3.to_string(), + v1: "任意".to_string(), + v2: "任意".to_string(), + v3: "任意".to_string(), + } + } + + /// 创建带分类值的信号(便捷构造器,score 自动钳制到 0..100)。 + pub fn new(k1: &str, k2: &str, k3: &str, v1: &str, v2: &str, v3: &str, score: i32) -> Self { + let score = score.clamp(0, 100); + let signal = format!("{k1}_{k2}_{k3}_{v1}_{v2}_{v3}_{score}"); + Self { + signal, + score, + k1: k1.to_string(), + k2: k2.to_string(), + k3: k3.to_string(), + v1: v1.to_string(), + v2: v2.to_string(), + v3: v3.to_string(), + } + } + + /// key — k1/k2/k3 中非「任意」部分用 _ 连接。 + pub fn key(&self) -> String { + [&self.k1, &self.k2, &self.k3] + .iter() + .filter(|k| k.as_str() != 任意) + .map(|k| k.as_str()) + .collect::>() + .join("_") + } + + /// value — v1_v2_v3_score。 + pub fn value(&self) -> String { + format!("{}_{}_{}_{}", self.v1, self.v2, self.v3, self.score) + } + + /// 纯值匹配 — 给定信号字典里取到的 value 串(v1_v2_v3_score),判断是否匹配。 + pub fn is_match_value(&self, 值: &str) -> bool { + let parts: Vec<&str> = 值.split('_').collect(); + if parts.len() != 4 { + return false; + } + let (v1, v2, v3, score_str) = (parts[0], parts[1], parts[2], parts[3]); + let score: i32 = score_str.parse().unwrap_or(0); + score >= self.score + && (v1 == self.v1 || self.v1 == 任意) + && (v2 == self.v2 || self.v2 == 任意) + && (v3 == self.v3 || self.v3 == 任意) + } + + /// 在信号字典中匹配。缺键 → Err(对应 Python raise ValueError), + /// 非字符串值 → Ok(false),字符串值 → 走 is_match_value。 + pub fn is_match(&self, 字典: &信号字典) -> Result { + let key = self.key(); + match 字典.get(&key) { + None => Err(缺键错误(key)), + Some(匹配值::非字符串) => Ok(false), + Some(匹配值::字符串(v)) => Ok(self.is_match_value(v)), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + + #[test] + fn test_score_越界_钳制() { + // new() 自动将越界 score 钳制到 0~100 + let s = Signal::new("a", "b", "c", "d", "e", "f", 101); + assert_eq!(s.score, 100); + let s = Signal::new("a", "b", "c", "d", "e", "f", -1); + assert_eq!(s.score, 0); + } + + #[test] + fn test_key_过滤任意() { + let s = Signal::new("14400", "D1MO3", "中枢", 任意, "三买", 任意, 0); + assert_eq!(s.key(), "14400_D1MO3_中枢"); + + // k1=任意 时 key 只剩 k2_k3 + let s2 = Signal::new(任意, "D1MO3", "中枢", 任意, "三买", 任意, 0); + assert_eq!(s2.key(), "D1MO3_中枢"); + + // 全「任意」时 key 为空串 + let s3 = Signal::new(任意, 任意, 任意, 任意, 任意, 任意, 0); + assert_eq!(s3.key(), ""); + } + + #[test] + fn test_value() { + let s = Signal::new("k1", "k2", "k3", "v1", "v2", "v3", 88); + assert_eq!(s.value(), "v1_v2_v3_88"); + } + + #[test] + fn test_is_match_缺键_报错() { + let s = Signal::new("14400", "D1MO3", "中枢", 任意, "三买", 任意, 0); + let 字典: HashMap = HashMap::new(); + assert!(s.is_match(&字典).is_err()); + } + + #[test] + fn test_is_match_非字符串_false() { + let s = Signal::new("14400", "D1MO3", "中枢", 任意, "三买", 任意, 0); + let mut 字典 = HashMap::new(); + 字典.insert("14400_D1MO3_中枢".to_string(), 匹配值::非字符串); + assert_eq!(s.is_match(&字典).unwrap(), false); + } + + #[test] + fn test_is_match_命中() { + let s = Signal::new("14400", "D1MO3", "中枢", 任意, "三买", 任意, 0); + let mut 字典 = HashMap::new(); + 字典.insert( + "14400_D1MO3_中枢".to_string(), + 匹配值::字符串("中枢段DEA穿越2_三买_偏移0_100".into()), + ); + assert_eq!(s.is_match(&字典).unwrap(), true); + } + + #[test] + fn test_is_match_v2不符_未命中() { + let s = Signal::new("14400", "D1MO3", "中枢", 任意, "三买", 任意, 0); + let mut 字典 = HashMap::new(); + 字典.insert( + "14400_D1MO3_中枢".to_string(), + 匹配值::字符串("中枢段DEA穿越2_三卖_偏移0_100".into()), + ); + assert_eq!(s.is_match(&字典).unwrap(), false); + } +} diff --git a/chanlun/src/structure/dash_line.rs b/chanlun/src/structure/dash_line.rs index a2cc6da..469043b 100644 --- a/chanlun/src/structure/dash_line.rs +++ b/chanlun/src/structure/dash_line.rs @@ -29,10 +29,16 @@ use crate::kline::chan_kline::缠论K线; use crate::structure::fractal_obj::分型; use crate::structure::segment_feat::线段特征; use crate::types::{分型结构, 相对方向, 缺口}; -use cached::stores::LruCache; +use crate::warn; +use parking_lot::RwLock; +use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicI64, Ordering}; -use std::sync::{Arc, LazyLock, Mutex, RwLock}; -use tracing::warn; + +/// 扩展线段模式 — 控制虚线高低取值方式 +/// +/// 默认 `true`:扩展线段取基础序列中所有子段端点的全局极值(与原文54课一致) +/// 设为 `true`:使用文/武分型端点 +pub static 扩展线段模式: AtomicBool = AtomicBool::new(true); /// 虚线 — 笔和线段的通用数据结构。 /// @@ -126,33 +132,27 @@ pub struct MACD行为统计 { impl Clone for 虚线 { fn clone(&self) -> Self { Self { - 标识: RwLock::new(self.标识.read().unwrap().clone()), + 标识: RwLock::new(self.标识.read().clone()), 序号: AtomicI64::new(self.序号.load(Ordering::Relaxed)), 级别: AtomicI64::new(self.级别.load(Ordering::Relaxed)), 文: Arc::clone(&self.文), - 武: RwLock::new(Arc::clone(&self.武.read().unwrap())), + 武: RwLock::new(Arc::clone(&self.武.read())), 有效性: AtomicBool::new(self.有效性.load(Ordering::Relaxed)), - 基础序列: RwLock::new(self.基础序列.read().unwrap().clone()), - 特征序列: RwLock::new(self.特征序列.read().unwrap().clone()), - 实_中枢序列: RwLock::new(self.实_中枢序列.read().unwrap().clone()), - 虚_中枢序列: RwLock::new(self.虚_中枢序列.read().unwrap().clone()), - 合_中枢序列: RwLock::new(self.合_中枢序列.read().unwrap().clone()), - 确认K线: RwLock::new(self.确认K线.read().unwrap().clone()), - 模式: RwLock::new(self.模式.read().unwrap().clone()), + 基础序列: RwLock::new(self.基础序列.read().clone()), + 特征序列: RwLock::new(self.特征序列.read().clone()), + 实_中枢序列: RwLock::new(self.实_中枢序列.read().clone()), + 虚_中枢序列: RwLock::new(self.虚_中枢序列.read().clone()), + 合_中枢序列: RwLock::new(self.合_中枢序列.read().clone()), + 确认K线: RwLock::new(self.确认K线.read().clone()), + 模式: RwLock::new(self.模式.read().clone()), _特征序列_显示: AtomicBool::new(self._特征序列_显示.load(Ordering::Relaxed)), - 前一缺口: RwLock::new(*self.前一缺口.read().unwrap()), - 前一结束位置: RwLock::new(self.前一结束位置.read().unwrap().clone()), + 前一缺口: RwLock::new(*self.前一缺口.read()), + 前一结束位置: RwLock::new(self.前一结束位置.read().clone()), 短路修正: AtomicBool::new(self.短路修正.load(Ordering::Relaxed)), } } } -type 买卖意义缓存类型 = LruCache<(usize, usize), (bool, String)>; - -/// 买卖意义 LRU 缓存(max 128 条目,对齐 Python @lru_cache(maxsize=128)) -static 买卖意义缓存: LazyLock> = - LazyLock::new(|| Mutex::new(LruCache::with_size(128))); - impl 虚线 { pub fn new( 序号: i64, @@ -189,14 +189,14 @@ impl 虚线 { "{}:{}:{}:{}", self.文.中.标识, self.文.中.周期, - self.标识.read().unwrap(), + self.标识.read(), self.序号.load(Ordering::Relaxed) ) } /// 方向 — 文到武的方向(对齐 Python:无法识别时 panic) pub fn 方向(&self) -> 相对方向 { - match (self.文.结构, self.武.read().unwrap().结构) { + match (self.文.结构, self.武.read().结构) { (分型结构::顶, 分型结构::底) => 相对方向::向下, (分型结构::顶, 分型结构::下) => 相对方向::向下, (分型结构::底, 分型结构::顶) => 相对方向::向上, @@ -204,43 +204,91 @@ impl 虚线 { _ => panic!( "虚线 方向 无法识别: 文.结构={:?}, 武.结构={:?}", self.文.结构, - self.武.read().unwrap().结构 + self.武.read().结构 ), } } - /// 虚线高 - pub fn 高(&self) -> f64 { - if self.方向() == 相对方向::向下 { - self.文.中.高.get() + /// 虚线端点高 — 始终取文/武分型端点的中.高,不扫描基础序列 + pub fn 端点高(&self) -> f64 { + if self.方向() == 相对方向::向上 { + self.武.read().中.高.get() } else { - self.武.read().unwrap().中.高.get() + self.文.中.高.get() } } - /// 虚线低 - pub fn 低(&self) -> f64 { + /// 虚线端点低 — 始终取文/武分型端点的中.低,不扫描基础序列 + pub fn 端点低(&self) -> f64 { if self.方向() == 相对方向::向下 { - self.武.read().unwrap().中.低.get() + self.武.read().中.低.get() } else { self.文.中.低.get() } } + /// 虚线高 + /// 默认同端点高;扩展线段模式开启时,取基础序列中所有子段文分型 + 最后子段武分型的最高 + pub fn 高(&self) -> f64 { + let 模式 = self.模式.read(); + let 标识 = self.标识.read(); + if 扩展线段模式.load(Ordering::Relaxed) + && *模式 != "文武" + && *标识 != "笔" + && 标识.contains("扩展") + { + let 基础序列 = self.基础序列.read(); + if !基础序列.is_empty() { + return 基础序列 + .iter() + .map(|s| s.文.中.高.get()) + .chain(std::iter::once( + 基础序列.last().unwrap().武.read().中.高.get(), + )) + .fold(f64::NEG_INFINITY, f64::max); + } + } + self.端点高() + } + + /// 虚线低 + /// 默认同端点低;扩展线段模式开启时,取基础序列中所有子段文分型 + 最后子段武分型的最低 + pub fn 低(&self) -> f64 { + let 模式 = self.模式.read(); + let 标识 = self.标识.read(); + if 扩展线段模式.load(Ordering::Relaxed) + && *模式 != "文武" + && *标识 != "笔" + && 标识.contains("扩展") + { + let 基础序列 = self.基础序列.read(); + if !基础序列.is_empty() { + return 基础序列 + .iter() + .map(|s| s.文.中.低.get()) + .chain(std::iter::once( + 基础序列.last().unwrap().武.read().中.低.get(), + )) + .fold(f64::INFINITY, f64::min); + } + } + self.端点低() + } + /// 判断两个虚线是否首尾相连 pub fn 之前是(&self, 之前: &虚线) -> bool { - if *self.标识.read().unwrap() != *之前.标识.read().unwrap() { + if *self.标识.read() != *之前.标识.read() { return false; } - Arc::as_ptr(&*之前.武.read().unwrap()) == Arc::as_ptr(&self.文) + Arc::ptr_eq(&*之前.武.read(), &self.文) } /// 判断两个虚线是否首尾相连 pub fn 之后是(&self, 之后: &虚线) -> bool { - if *self.标识.read().unwrap() != *之后.标识.read().unwrap() { + if *self.标识.read() != *之后.标识.read() { return false; } - Arc::as_ptr(&*self.武.read().unwrap()) == Arc::as_ptr(&之后.文) + Arc::ptr_eq(&*self.武.read(), &之后.文) } /// 获取该虚线范围内的普K序列 @@ -250,23 +298,17 @@ impl 虚线 { // 而非序号切片——因为序号可能与实际位置不一致。 let 始 = 普K序列 .iter() - .position(|k| Arc::as_ptr(k) == Arc::as_ptr(&*self.文.中.标的K线.read().unwrap())); - let 终 = 普K序列.iter().position(|k| { - Arc::as_ptr(k) == Arc::as_ptr(&*self.武.read().unwrap().中.标的K线.read().unwrap()) - }); + .position(|k| Arc::ptr_eq(k, &*self.文.中.标的K线.read())); + let 终 = 普K序列 + .iter() + .position(|k| Arc::ptr_eq(k, &*self.武.read().中.标的K线.read())); match (始, 终) { (Some(s), Some(e)) if s <= e => 普K序列[s..=e].to_vec(), _ => { // 指针查找失败时回退到序号方式 warn!("[警告]虚线.获取普K序列 <指针查找失败时回退到序号方式>"); let 始 = self.文.中.原始起始序号 as usize; - let 终 = self - .武 - .read() - .unwrap() - .中 - .原始结束序号 - .load(Ordering::Relaxed) as usize; + let 终 = self.武.read().中.原始结束序号.load(Ordering::Relaxed) as usize; if 始 < 普K序列.len() && 终 < 普K序列.len() && 始 <= 终 { 普K序列[始..=终].to_vec() } else { @@ -279,21 +321,21 @@ impl 虚线 { /// 获取该虚线范围内的缠K序列 /// 对齐说明:Python 接收 观察者 对象,Rust 直接接收切片引用,行为等价 pub fn 获取缠K序列(&self, 缠K序列: &[Arc<缠论K线>]) -> Vec> { - 缠论K线::截取(缠K序列, &self.文.中, &self.武.read().unwrap().中).unwrap_or_default() + 缠论K线::截取(缠K序列, &self.文.中, &self.武.read().中).unwrap_or_default() } /// 获取_武 — 递归获取虚线的终点分型(笔直接返回武,线段递归到底层笔的武) /// 对齐说明:Python 是 @classmethod(cls, 实线),Rust 是实例方法(&self),逻辑等价 pub fn 获取_武(&self) -> Arc<分型> { - if *self.标识.read().unwrap() == "笔" { - return self.武.read().unwrap().clone(); + if *self.标识.read() == "笔" { + return self.武.read().clone(); } - let mut current_rc = Arc::clone(self.基础序列.read().unwrap().last().unwrap()); + let mut current_rc = Arc::clone(self.基础序列.read().last().unwrap()); loop { - if *current_rc.标识.read().unwrap() == "笔" { - return current_rc.武.read().unwrap().clone(); + if *current_rc.标识.read() == "笔" { + return current_rc.武.read().clone(); } - let next = Arc::clone(current_rc.基础序列.read().unwrap().last().unwrap()); + let next = Arc::clone(current_rc.基础序列.read().last().unwrap()); current_rc = next; } } @@ -301,16 +343,16 @@ impl 虚线 { /// 获取数据文本(用于保存/调试) pub fn 获取数据文本(&self) -> String { use crate::utils::format_f64_g; - if *self.标识.read().unwrap() == "笔" { + if *self.标识.read() == "笔" { return format!( "{}, {}, {}, 文:({},{}), 武:({},{}), {}", - self.标识.read().unwrap(), + self.标识.read(), self.序号.load(Ordering::Relaxed), self.级别.load(Ordering::Relaxed), self.文.时间戳(), format_f64_g(self.文.分型特征值), - self.武.read().unwrap().时间戳(), - format_f64_g(self.武.read().unwrap().分型特征值), + self.武.read().时间戳(), + format_f64_g(self.武.read().分型特征值), if self.有效性.load(Ordering::Relaxed) { "True" } else { @@ -324,11 +366,11 @@ impl 虚线 { let (特征_a, 特征_b, 特征_c) = crate::algorithm::segment::线段::特征序列状态(self); let 特征_bool = |b: bool| -> &str { if b { "True" } else { "False" } }; - let 前一缺口_str = match &*self.前一缺口.read().unwrap() { + let 前一缺口_str = match &*self.前一缺口.read() { Some(g) => format!("{}", g), None => "None".to_string(), }; - let 前一结束位置_str = match &*self.前一结束位置.read().unwrap() { + let 前一结束位置_str = match &*self.前一结束位置.read() { Some(d) => format!("{}", d), None => "None".to_string(), }; @@ -338,7 +380,6 @@ impl 虚线 { "[{}]", self.实_中枢序列 .read() - .unwrap() .iter() .map(|h| format!("{}", h)) .collect::>() @@ -348,7 +389,6 @@ impl 虚线 { "[{}]", self.虚_中枢序列 .read() - .unwrap() .iter() .map(|h| format!("{}", h)) .collect::>() @@ -358,7 +398,6 @@ impl 虚线 { "[{}]", self.合_中枢序列 .read() - .unwrap() .iter() .map(|h| format!("{}", h)) .collect::>() @@ -389,19 +428,19 @@ impl 虚线 { format!( "{}, {}, {}, 文:({},{}), 武:({},{}), {}, {}, ({}, {}, {}), (前: {}, 后: {}, 三: {}, 伤: {}), 实: {}, 虚: {}, 合: {}, {}, {}, {}, {}", - self.标识.read().unwrap(), + self.标识.read(), self.序号.load(Ordering::Relaxed), self.级别.load(Ordering::Relaxed), self.文.时间戳(), format_f64_g(self.文.分型特征值), - self.武.read().unwrap().时间戳(), - format_f64_g(self.武.read().unwrap().分型特征值), + self.武.read().时间戳(), + format_f64_g(self.武.read().分型特征值), if self.有效性.load(Ordering::Relaxed) { "True" } else { "False" }, - self.基础序列.read().unwrap().len(), + self.基础序列.read().len(), 特征_bool(特征_a), 特征_bool(特征_b), 特征_bool(特征_c), @@ -415,7 +454,7 @@ impl 虚线 { 实_str, 虚_str, 合_str, - self.模式.read().unwrap(), + self.模式.read(), 前一缺口_str, 前一结束位置_str, if self.短路修正.load(Ordering::Relaxed) { @@ -443,26 +482,26 @@ impl 虚线 { panic!("创建线段 虚线序列 数量 {} 不是单数!", 序列数量); } let 文 = Arc::clone(&虚线序列[0].文); - let 武 = Arc::clone(&*虚线序列[虚线序列.len() - 1].武.read().unwrap()); + let 武 = Arc::clone(&*虚线序列[虚线序列.len() - 1].武.read()); assert!( 文.结构 != 武.结构, "创建线段: 文.结构 == 武.结构 文={}, 武={}", 文, 武 ); - let 标识: String = if *虚线序列[0].标识.read().unwrap() == "笔" { + let 标识: String = if *虚线序列[0].标识.read() == "笔" { "线段".into() } else { - format!("线段<{}>", 虚线序列[0].标识.read().unwrap()) + format!("线段<{}>", 虚线序列[0].标识.read()) }; let 级别 = 虚线序列[0].级别.load(Ordering::Relaxed) + 1; let 段 = Self::new(0, 标识, 文, 武, 级别, true); - *段.特征序列.write().unwrap() = vec![None, None, None]; - *段.实_中枢序列.write().unwrap() = Vec::new(); - *段.虚_中枢序列.write().unwrap() = Vec::new(); - *段.合_中枢序列.write().unwrap() = Vec::new(); - *段.基础序列.write().unwrap() = 虚线序列.to_vec(); - *段.模式.write().unwrap() = "文武".into(); + *段.特征序列.write() = vec![None, None, None]; + *段.实_中枢序列.write() = Vec::new(); + *段.虚_中枢序列.write() = Vec::new(); + *段.合_中枢序列.write() = Vec::new(); + *段.基础序列.write() = 虚线序列.to_vec(); + *段.模式.write() = "文武".into(); 段 } @@ -519,59 +558,56 @@ impl 虚线 { pub fn 计算MACD柱子均值(普K序列: &[Arc], 实线: &虚线) -> f64 { let K线序列 = K线::截取rc( 普K序列, - &实线.文.中.标的K线.read().unwrap(), - &实线.武.read().unwrap().中.标的K线.read().unwrap(), + &实线.文.中.标的K线.read(), + &实线.武.read().中.标的K线.read(), ); if K线序列.is_empty() { return 0.0; } let 总: f64 = K线序列 .iter() - .filter_map(|k| k.指标.read().unwrap().macd_cloned()) + .filter_map(|k| k.指标.read().macd_cloned()) .map(|m| m.MACD柱.abs()) .sum(); 总 / K线序列.len() as f64 } /// 计算MACD柱子均值_阴 — 负柱的绝对值均值 - /// 对齐说明:Python 无数据时返回 False(bool),Rust 返回 None,调用方处理等价 pub fn 计算MACD柱子均值_阴(普K序列: &[Arc], 实线: &虚线) -> Option { let K线序列 = K线::截取rc( 普K序列, - &实线.文.中.标的K线.read().unwrap(), - &实线.武.read().unwrap().中.标的K线.read().unwrap(), + &实线.文.中.标的K线.read(), + &实线.武.read().中.标的K线.read(), ); - let 总: Vec = K线序列 + let (sum, count) = K线序列 .iter() - .filter_map(|k| k.指标.read().unwrap().macd_cloned()) + .filter_map(|k| k.指标.read().macd_cloned()) .filter(|m| m.MACD柱 < 0.0) - .map(|m| m.MACD柱.abs()) - .collect(); - if 总.is_empty() { + .fold((0.0, 0usize), |(s, c), m| (s + m.MACD柱.abs(), c + 1)); + if count == 0 { None } else { - Some(总.iter().sum::() / 总.len() as f64) + Some(sum / count as f64) } } /// 计算MACD柱子均值_阳 — 正柱的绝对值均值 - /// 对齐说明:Python 无数据时返回 False(bool),Rust 返回 None,调用方处理等价 pub fn 计算MACD柱子均值_阳(普K序列: &[Arc], 实线: &虚线) -> Option { let K线序列 = K线::截取rc( 普K序列, - &实线.文.中.标的K线.read().unwrap(), - &实线.武.read().unwrap().中.标的K线.read().unwrap(), + &实线.文.中.标的K线.read(), + &实线.武.read().中.标的K线.read(), ); - let 总: Vec = K线序列 + let (sum, count) = K线序列 .iter() - .filter_map(|k| k.指标.read().unwrap().macd_cloned()) + .filter_map(|k| k.指标.read().macd_cloned()) .filter(|m| m.MACD柱 > 0.0) .map(|m| m.MACD柱.abs()) - .collect(); - if 总.is_empty() { + .fold((0.0, 0usize), |(s, c), v| (s + v, c + 1)); + if count == 0 { None } else { - Some(总.iter().sum::() / 总.len() as f64) + Some(sum / count as f64) } } @@ -579,9 +615,9 @@ impl 虚线 { /// 武之全量MACD均值 — 武端MACD柱是否小于均值(背驰) pub fn 武之全量MACD均值(普K序列: &[Arc], 实线: &虚线) -> bool { - let 武_ref = 实线.武.read().unwrap(); - let 标 = 武_ref.中.标的K线.read().unwrap(); - let 武_MACD = match 标.指标.read().unwrap().macd() { + let 武_ref = 实线.武.read(); + let 标 = 武_ref.中.标的K线.read(); + let 武_MACD = match 标.指标.read().macd() { Some(m) => m.MACD柱.abs(), None => return false, }; @@ -599,9 +635,9 @@ impl 虚线 { /// 武之MACD均值_阴 — 武端负柱是否小于阴均值 pub fn 武之MACD均值_阴(普K序列: &[Arc], 实线: &虚线) -> bool { - let 武_ref = 实线.武.read().unwrap(); - let 标 = 武_ref.中.标的K线.read().unwrap(); - let 武_MACD = match 标.指标.read().unwrap().macd() { + let 武_ref = 实线.武.read(); + let 标 = 武_ref.中.标的K线.read(); + let 武_MACD = match 标.指标.read().macd() { Some(m) => m.MACD柱.abs(), None => return false, }; @@ -613,9 +649,9 @@ impl 虚线 { /// 武之MACD均值_阳 — 武端正柱是否小于阳均值 pub fn 武之MACD均值_阳(普K序列: &[Arc], 实线: &虚线) -> bool { - let 武_ref = 实线.武.read().unwrap(); - let 标 = 武_ref.中.标的K线.read().unwrap(); - let 武_MACD = match 标.指标.read().unwrap().macd() { + let 武_ref = 实线.武.read(); + let 标 = 武_ref.中.标的K线.read(); + let 武_MACD = match 标.指标.read().macd() { Some(m) => m.MACD柱.abs(), None => return false, }; @@ -627,20 +663,20 @@ impl 虚线 { /// 武之MACD极值 — 武端MACD柱是否为区间极值 pub fn 武之MACD极值(普K序列: &[Arc], 实线: &虚线) -> bool { - let 武_ref = 实线.武.read().unwrap(); - let 标 = 武_ref.中.标的K线.read().unwrap(); - let 武_MACD = match 标.指标.read().unwrap().macd() { + let 武_ref = 实线.武.read(); + let 标 = 武_ref.中.标的K线.read(); + let 武_MACD = match 标.指标.read().macd() { Some(m) => m.MACD柱, None => return false, }; let K线序列 = K线::截取rc( 普K序列, - &实线.文.中.标的K线.read().unwrap(), - &实线.武.read().unwrap().中.标的K线.read().unwrap(), + &实线.文.中.标的K线.read(), + &实线.武.read().中.标的K线.read(), ); let 所有柱子: Vec = K线序列 .iter() - .filter_map(|k| k.指标.read().unwrap().macd_cloned()) + .filter_map(|k| k.指标.read().macd_cloned()) .map(|m| m.MACD柱) .collect(); if 所有柱子.is_empty() { @@ -655,6 +691,217 @@ impl 虚线 { } } + pub fn _计算K线序列MACD趋向背驰( + 普K序列: &[Arc], 方向: 相对方向 + ) -> [bool; 3] { + if 普K序列.is_empty() { + return [false, false, false]; + } + let 最后 = &普K序列[普K序列.len() - 1]; + + let mut 柱子列表: Vec> = Vec::new(); + let mut 离差列表: Vec> = Vec::new(); + let mut 信号列表: Vec> = Vec::new(); + + if 方向 == 相对方向::向上 { + for k in 普K序列 { + let guard = k.指标.read(); + if let Some(m) = guard.macd() { + if m.MACD柱 > 0.0 { + 柱子列表.push(Arc::clone(k)); + } + if m.DIF.is_some_and(|d| d > 0.0) { + 离差列表.push(Arc::clone(k)); + } + if m.DEA.is_some_and(|d| d > 0.0) { + 信号列表.push(Arc::clone(k)); + } + } + } + } else { + for k in 普K序列 { + let guard = k.指标.read(); + if let Some(m) = guard.macd() { + if m.MACD柱 < 0.0 { + 柱子列表.push(Arc::clone(k)); + } + if m.DIF.is_some_and(|d| d < 0.0) { + 离差列表.push(Arc::clone(k)); + } + if m.DEA.is_some_and(|d| d < 0.0) { + 信号列表.push(Arc::clone(k)); + } + } + } + } + + if 柱子列表.is_empty() { + return [false, false, false]; + } + + let mut 结果 = [false; 3]; + + if 方向 == 相对方向::向上 { + let 最高柱子 = 柱子列表 + .iter() + .max_by(|a, b| { + a.指标 + .read() + .macd() + .unwrap() + .MACD柱 + .partial_cmp(&b.指标.read().macd().unwrap().MACD柱) + .unwrap_or(std::cmp::Ordering::Equal) + }) + .unwrap(); + let mut 柱对 = [Arc::clone(最高柱子), Arc::clone(最后)]; + 柱对.sort_by_key(|k| k.时间戳); + { + let g0 = 柱对[0].指标.read(); + let g1 = 柱对[1].指标.read(); + let m0 = g0.macd().unwrap(); + let m1 = g1.macd().unwrap(); + if m0.MACD柱 > m1.MACD柱 && m0.MACD柱 > 0.0 && 柱对[0].高 < 柱对[1].高 { + 结果[0] = true; + } + } + + if !离差列表.is_empty() { + let 最高离差 = 离差列表 + .iter() + .max_by(|a, b| { + let da = a.指标.read().macd().and_then(|m| m.DIF).unwrap_or(0.0); + let db = b.指标.read().macd().and_then(|m| m.DIF).unwrap_or(0.0); + da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal) + }) + .unwrap(); + let d0 = 最高离差 + .指标 + .read() + .macd() + .and_then(|m| m.DIF) + .unwrap_or(0.0); + let d1 = 最后.指标.read().macd().and_then(|m| m.DIF).unwrap_or(0.0); + if d0 > d1 && 最高离差.高 < 最后.高 { + 结果[1] = true; + } + } + + if !信号列表.is_empty() { + let 最高信号 = 信号列表 + .iter() + .max_by(|a, b| { + let ea = a.指标.read().macd().and_then(|m| m.DEA).unwrap_or(0.0); + let eb = b.指标.read().macd().and_then(|m| m.DEA).unwrap_or(0.0); + ea.partial_cmp(&eb).unwrap_or(std::cmp::Ordering::Equal) + }) + .unwrap(); + let e0 = 最高信号 + .指标 + .read() + .macd() + .and_then(|m| m.DEA) + .unwrap_or(0.0); + let e1 = 最后.指标.read().macd().and_then(|m| m.DEA).unwrap_or(0.0); + if e0 > e1 && 最高信号.高 < 最后.高 { + 结果[2] = true; + } + } + } else { + let 最高柱子 = 柱子列表 + .iter() + .max_by(|a, b| { + a.指标 + .read() + .macd() + .unwrap() + .MACD柱 + .abs() + .partial_cmp(&b.指标.read().macd().unwrap().MACD柱.abs()) + .unwrap_or(std::cmp::Ordering::Equal) + }) + .unwrap(); + let mut 柱对 = [Arc::clone(最高柱子), Arc::clone(最后)]; + 柱对.sort_by_key(|k| k.时间戳); + { + let g0 = 柱对[0].指标.read(); + let g1 = 柱对[1].指标.read(); + let m0 = g0.macd().unwrap(); + let m1 = g1.macd().unwrap(); + if m0.MACD柱 < m1.MACD柱 && m0.MACD柱 < 0.0 && 柱对[0].低 > 柱对[1].低 { + 结果[0] = true; + } + } + + if !离差列表.is_empty() { + let 最高离差 = 离差列表 + .iter() + .max_by(|a, b| { + let da = a + .指标 + .read() + .macd() + .and_then(|m| m.DIF) + .unwrap_or(0.0) + .abs(); + let db = b + .指标 + .read() + .macd() + .and_then(|m| m.DIF) + .unwrap_or(0.0) + .abs(); + da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal) + }) + .unwrap(); + let d0 = 最高离差 + .指标 + .read() + .macd() + .and_then(|m| m.DIF) + .unwrap_or(0.0); + let d1 = 最后.指标.read().macd().and_then(|m| m.DIF).unwrap_or(0.0); + if d0 < d1 && 最高离差.低 > 最后.低 { + 结果[1] = true; + } + } + + if !信号列表.is_empty() { + let 最高信号 = 信号列表 + .iter() + .max_by(|a, b| { + let ea = a + .指标 + .read() + .macd() + .and_then(|m| m.DEA) + .unwrap_or(0.0) + .abs(); + let eb = b + .指标 + .read() + .macd() + .and_then(|m| m.DEA) + .unwrap_or(0.0) + .abs(); + ea.partial_cmp(&eb).unwrap_or(std::cmp::Ordering::Equal) + }) + .unwrap(); + let e0 = 最高信号 + .指标 + .read() + .macd() + .and_then(|m| m.DEA) + .unwrap_or(0.0); + let e1 = 最后.指标.read().macd().and_then(|m| m.DEA).unwrap_or(0.0); + if e0 < e1 && 最高信号.低 > 最后.低 { + 结果[2] = true; + } + } + } + + 结果 + } // ---- MACD趋向背驰 ---- /// 计算K线序列MACD趋向背驰 — 分析 MACD柱/DIF/DEA 三项背驰信号 @@ -669,13 +916,7 @@ impl 虚线 { if 方向 == 相对方向::向上 { let 柱子序列: Vec<&Arc> = 普K序列 .iter() - .filter(|k| { - k.指标 - .read() - .unwrap() - .macd() - .is_some_and(|m| m.MACD柱 > 0.0) - }) + .filter(|k| k.指标.read().macd().is_some_and(|m| m.MACD柱 > 0.0)) .collect(); if 柱子序列.is_empty() { return [false, false, false]; @@ -689,18 +930,17 @@ impl 虚线 { .max_by(|a, b| { a.指标 .read() - .unwrap() .macd() .unwrap() .MACD柱 - .partial_cmp(&b.指标.read().unwrap().macd().unwrap().MACD柱) + .partial_cmp(&b.指标.read().macd().unwrap().MACD柱) .unwrap_or(std::cmp::Ordering::Equal) }) .unwrap(); let mut 柱对 = [Arc::clone(*最高柱子), Arc::clone(最后)]; 柱对.sort_by_key(|k| k.时间戳); - let m0_g = 柱对[0].指标.read().unwrap(); - let m1_g = 柱对[1].指标.read().unwrap(); + let m0_g = 柱对[0].指标.read(); + let m1_g = 柱对[1].指标.read(); if let (Some(m0), Some(m1)) = (m0_g.macd(), m1_g.macd()) && m0.MACD柱 > m1.MACD柱 && 柱对[0].高 < 柱对[1].高 @@ -712,25 +952,13 @@ impl 虚线 { let 最高离差值 = 柱子序列 .iter() .max_by(|a, b| { - let da = a - .指标 - .read() - .unwrap() - .macd() - .and_then(|m| m.DIF) - .unwrap_or(0.0); - let db = b - .指标 - .read() - .unwrap() - .macd() - .and_then(|m| m.DIF) - .unwrap_or(0.0); + let da = a.指标.read().macd().and_then(|m| m.DIF).unwrap_or(0.0); + let db = b.指标.read().macd().and_then(|m| m.DIF).unwrap_or(0.0); da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal) }) .unwrap(); - let m0_g = 最高离差值.指标.read().unwrap(); - let m1_g = 最后.指标.read().unwrap(); + let m0_g = 最高离差值.指标.read(); + let m1_g = 最后.指标.read(); if let (Some(m0), Some(m1)) = (m0_g.macd(), m1_g.macd()) { let dif0 = m0.DIF.unwrap_or(0.0); let dif1 = m1.DIF.unwrap_or(0.0); @@ -743,25 +971,13 @@ impl 虚线 { let 最高信号线 = 柱子序列 .iter() .max_by(|a, b| { - let da = a - .指标 - .read() - .unwrap() - .macd() - .and_then(|m| m.DEA) - .unwrap_or(0.0); - let db = b - .指标 - .read() - .unwrap() - .macd() - .and_then(|m| m.DEA) - .unwrap_or(0.0); + let da = a.指标.read().macd().and_then(|m| m.DEA).unwrap_or(0.0); + let db = b.指标.read().macd().and_then(|m| m.DEA).unwrap_or(0.0); da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal) }) .unwrap(); - let m0_g = 最高信号线.指标.read().unwrap(); - let m1_g = 最后.指标.read().unwrap(); + let m0_g = 最高信号线.指标.read(); + let m1_g = 最后.指标.read(); if let (Some(m0), Some(m1)) = (m0_g.macd(), m1_g.macd()) { let dea0 = m0.DEA.unwrap_or(0.0); let dea1 = m1.DEA.unwrap_or(0.0); @@ -774,13 +990,7 @@ impl 虚线 { } else { let 柱子序列: Vec<&Arc> = 普K序列 .iter() - .filter(|k| { - k.指标 - .read() - .unwrap() - .macd() - .is_some_and(|m| m.MACD柱 < 0.0) - }) + .filter(|k| k.指标.read().macd().is_some_and(|m| m.MACD柱 < 0.0)) .collect(); if 柱子序列.is_empty() { return [false, false, false]; @@ -794,19 +1004,18 @@ impl 虚线 { .max_by(|a, b| { a.指标 .read() - .unwrap() .macd() .unwrap() .MACD柱 .abs() - .partial_cmp(&b.指标.read().unwrap().macd().unwrap().MACD柱.abs()) + .partial_cmp(&b.指标.read().macd().unwrap().MACD柱.abs()) .unwrap_or(std::cmp::Ordering::Equal) }) .unwrap(); let mut 柱对 = [Arc::clone(*最高柱子), Arc::clone(最后)]; 柱对.sort_by_key(|k| k.时间戳); - let m0_g = 柱对[0].指标.read().unwrap(); - let m1_g = 柱对[1].指标.read().unwrap(); + let m0_g = 柱对[0].指标.read(); + let m1_g = 柱对[1].指标.read(); if let (Some(m0), Some(m1)) = (m0_g.macd(), m1_g.macd()) && m0.MACD柱 < m1.MACD柱 && 柱对[0].低 > 柱对[1].低 @@ -821,7 +1030,6 @@ impl 虚线 { let da = a .指标 .read() - .unwrap() .macd() .and_then(|m| m.DIF) .unwrap_or(0.0) @@ -829,7 +1037,6 @@ impl 虚线 { let db = b .指标 .read() - .unwrap() .macd() .and_then(|m| m.DIF) .unwrap_or(0.0) @@ -837,8 +1044,8 @@ impl 虚线 { da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal) }) .unwrap(); - let m0_g = 最高离差值.指标.read().unwrap(); - let m1_g = 最后.指标.read().unwrap(); + let m0_g = 最高离差值.指标.read(); + let m1_g = 最后.指标.read(); if let (Some(m0), Some(m1)) = (m0_g.macd(), m1_g.macd()) { let dif0 = m0.DIF.unwrap_or(0.0); let dif1 = m1.DIF.unwrap_or(0.0); @@ -854,7 +1061,6 @@ impl 虚线 { let da = a .指标 .read() - .unwrap() .macd() .and_then(|m| m.DEA) .unwrap_or(0.0) @@ -862,7 +1068,6 @@ impl 虚线 { let db = b .指标 .read() - .unwrap() .macd() .and_then(|m| m.DEA) .unwrap_or(0.0) @@ -870,8 +1075,8 @@ impl 虚线 { da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal) }) .unwrap(); - let m0_g = 最高信号线.指标.read().unwrap(); - let m1_g = 最后.指标.read().unwrap(); + let m0_g = 最高信号线.指标.read(); + let m1_g = 最后.指标.read(); if let (Some(m0), Some(m1)) = (m0_g.macd(), m1_g.macd()) { let dea0 = m0.DEA.unwrap_or(0.0); let dea1 = m1.DEA.unwrap_or(0.0); @@ -895,7 +1100,7 @@ impl 虚线 { let 符号 = |x: f64| -> &str { if x > 0.0 { "正" } else { "负" } }; - let 首_MACD = match k线序列[0].指标.read().unwrap().macd() { + let 首_MACD = match k线序列[0].指标.read().macd() { Some(m) => m.MACD柱, None => return Vec::new(), }; @@ -904,7 +1109,7 @@ impl 虚线 { let mut 结果 = Vec::new(); for k线 in &k线序列[1..] { - let macd = match k线.指标.read().unwrap().macd() { + let macd = match k线.指标.read().macd() { Some(m) => m.MACD柱, None => continue, }; @@ -974,12 +1179,17 @@ impl 虚线 { let mut dea_up = 0; let mut dea_down = 0; + let mut golden = 0; + let mut death = 0; + let mut 交叉标记 = vec![0i32]; + for i in 1..普K序列.len() { - let pre_guard = 普K序列[i - 1].指标.read().unwrap(); - let cur_guard = 普K序列[i].指标.read().unwrap(); + let pre_guard = 普K序列[i - 1].指标.read(); + let cur_guard = 普K序列[i].指标.read(); let pre = pre_guard.macd(); let cur = cur_guard.macd(); if pre.is_none() || cur.is_none() { + 交叉标记.push(0); continue; } let (pre_dif, cur_dif) = (pre.unwrap().DIF, cur.unwrap().DIF); @@ -1001,25 +1211,6 @@ impl 虚线 { dea_down += 1; } } - } - - let mut golden = 0; - let mut death = 0; - let mut 交叉标记 = vec![0i32]; - - for i in 1..普K序列.len() { - let pre_guard = 普K序列[i - 1].指标.read().unwrap(); - let cur_guard = 普K序列[i].指标.read().unwrap(); - let pre = pre_guard.macd(); - let cur = cur_guard.macd(); - if pre.is_none() || cur.is_none() { - 交叉标记.push(0); - continue; - } - let pre_dif = pre.unwrap().DIF; - let pre_dea = pre.unwrap().DEA; - let cur_dif = cur.unwrap().DIF; - let cur_dea = cur.unwrap().DEA; if let (Some(pd), Some(cd), Some(pe), Some(ce)) = (pre_dif, cur_dif, pre_dea, cur_dea) { if pd <= pe && cd > ce { @@ -1057,24 +1248,7 @@ impl 虚线 { pub fn 买卖意义( 实线: &虚线, 观察员: &crate::business::observer::观察者 ) -> (bool, String) { - // LRU 缓存查找 - let key = ( - std::ptr::from_ref(实线) as usize, - std::ptr::from_ref(观察员) as usize, - ); - { - let mut cache = 买卖意义缓存.lock().unwrap(); - if let Some(val) = cached::Cached::cache_get(&mut *cache, &key) { - return val.clone(); - } - } - - let result = Self::_买卖意义_计算(实线, 观察员); - { - let mut cache = 买卖意义缓存.lock().unwrap(); - cached::Cached::cache_set(&mut *cache, key, result.clone()); - } - result + Self::_买卖意义_计算(实线, 观察员) } /// 买卖意义 实际计算(无缓存) @@ -1085,39 +1259,38 @@ impl 虚线 { let 普K序列 = &观察员.普通K线序列; let 配置 = &观察员.配置; - if *实线.标识.read().unwrap() != "笔" - && *实线.标识.read().unwrap() != "线段" - && *实线.标识.read().unwrap() != "线段<线段>" + if *实线.标识.read() != "笔" + && *实线.标识.read() != "线段" + && *实线.标识.read() != "线段<线段>" { return (false, "标识不在范围内".into()); } // KDJ指标完整性检查 - let 武_ref = 实线.武.read().unwrap(); - let 标 = 武_ref.中.标的K线.read().unwrap(); - match 标.指标.read().unwrap().kdj() { + let 武_ref = 实线.武.read(); + let 标 = 武_ref.中.标的K线.read(); + match 标.指标.read().kdj() { Some(kdj) if kdj.K.is_some() && kdj.D.is_some() && kdj.J.is_some() => {} _ => return (false, "KDJ指标不完整".into()), } - let 意义 = - Self::缠K买卖点模式(&配置.买卖点_指标模式, &实线.武.read().unwrap().中, 配置); + let 意义 = Self::缠K买卖点模式(&配置.买卖点_指标模式, &实线.武.read().中, 配置); let 结果 = false; - let 背驰过: Vec> = if *实线.标识.read().unwrap() == "笔" { + let 背驰过: Vec> = if *实线.标识.read() == "笔" { crate::algorithm::bi::笔::是否背驰过(实线, 观察员) } else { crate::algorithm::segment::线段::是否背驰过(实线, 观察员) }; if 意义 { - if *实线.标识.read().unwrap() == "笔" { + if *实线.标识.read() == "笔" { if Self::武之MACD均值(普K序列, 实线) { return (true, "武之MACD均值".into()); } if Self::武之MACD极值(普K序列, 实线) && !背驰过.is_empty() { return (true, "背驰过且极值".into()); - } else if 实线.武.read().unwrap().与MACD柱子分型匹配() { + } else if 实线.武.read().与MACD柱子分型匹配() { return ( true, format!( @@ -1132,7 +1305,7 @@ impl 虚线 { ); } } - if *实线.标识.read().unwrap() != "笔" + if *实线.标识.read() != "笔" && crate::algorithm::segment::线段::判断线段内部是否背驰(实线, 观察员) { return (true, "线段内部背驰".into()); @@ -1141,7 +1314,7 @@ impl 虚线 { if !结果 && 意义 - && 实线.武.read().unwrap().中.与MACD柱子匹配() + && 实线.武.read().中.与MACD柱子匹配() && Self::武之MACD极值(普K序列, 实线) && 背驰过.len() > 2 { @@ -1153,13 +1326,13 @@ impl 虚线 { /// 结构化相等校验 — 递归校验所有子结构(分型/缺口/缠K/中枢/线段特征/虚线),返回 (是否相等, 差异描述) pub fn 相等(&self, other: &Self, 浮点容差: f64) -> (bool, String) { - if *self.标识.read().unwrap() != *other.标识.read().unwrap() { + if *self.标识.read() != *other.标识.read() { return ( false, format!( "虚线: [标识] 不等 A={},B={}", - self.标识.read().unwrap(), - other.标识.read().unwrap() + self.标识.read(), + other.标识.read() ), ); } @@ -1192,11 +1365,7 @@ impl 虚线 { } // 武 { - let (eq, msg) = self - .武 - .read() - .unwrap() - .相等(&other.武.read().unwrap(), 浮点容差); + let (eq, msg) = self.武.read().相等(&other.武.read(), 浮点容差); if !eq { return (false, format!("虚线: [武]分型异常 >> {msg}")); } @@ -1213,8 +1382,8 @@ impl 虚线 { } // 基础序列 { - let a = self.基础序列.read().unwrap(); - let b = other.基础序列.read().unwrap(); + let a = self.基础序列.read(); + let b = other.基础序列.read(); if a.len() != b.len() { return ( false, @@ -1230,8 +1399,8 @@ impl 虚线 { } // 特征序列 { - let a = self.特征序列.read().unwrap(); - let b = other.特征序列.read().unwrap(); + let a = self.特征序列.read(); + let b = other.特征序列.read(); if a.len() != b.len() { return ( false, @@ -1271,33 +1440,30 @@ impl 虚线 { }; 检查中枢列表( "实_中枢序列", - &self.实_中枢序列.read().unwrap(), - &other.实_中枢序列.read().unwrap(), + &self.实_中枢序列.read(), + &other.实_中枢序列.read(), 浮点容差, ) .map_err(|e| (false, e)) .ok(); 检查中枢列表( "虚_中枢序列", - &self.虚_中枢序列.read().unwrap(), - &other.虚_中枢序列.read().unwrap(), + &self.虚_中枢序列.read(), + &other.虚_中枢序列.read(), 浮点容差, ) .map_err(|e| (false, e)) .ok(); 检查中枢列表( "合_中枢序列", - &self.合_中枢序列.read().unwrap(), - &other.合_中枢序列.read().unwrap(), + &self.合_中枢序列.read(), + &other.合_中枢序列.read(), 浮点容差, ) .map_err(|e| (false, e)) .ok(); // 确认K线 - match ( - &*self.确认K线.read().unwrap(), - &*other.确认K线.read().unwrap(), - ) { + match (&*self.确认K线.read(), &*other.确认K线.read()) { (None, None) => {} (Some(a), Some(b)) => { let (eq, msg) = a.相等(b, 浮点容差); @@ -1317,13 +1483,13 @@ impl 虚线 { } } // 模式 - if *self.模式.read().unwrap() != *other.模式.read().unwrap() { + if *self.模式.read() != *other.模式.read() { return ( false, format!( "虚线: [模式] 不等 A={},B={}", - self.模式.read().unwrap(), - other.模式.read().unwrap() + self.模式.read(), + other.模式.read() ), ); } @@ -1334,10 +1500,7 @@ impl 虚线 { return (false, "虚线: [_特征序列_显示] 不等".to_string()); } // 前一缺口 - match ( - &*self.前一缺口.read().unwrap(), - &*other.前一缺口.read().unwrap(), - ) { + match (&*self.前一缺口.read(), &*other.前一缺口.read()) { (None, None) => {} (Some(a), Some(b)) => { let (eq, msg) = a.相等(b, 浮点容差); @@ -1357,10 +1520,7 @@ impl 虚线 { } } // 前一结束位置 - match ( - &*self.前一结束位置.read().unwrap(), - &*other.前一结束位置.read().unwrap(), - ) { + match (&*self.前一结束位置.read(), &*other.前一结束位置.read()) { (None, None) => {} (Some(a), Some(b)) => { let (eq, msg) = a.相等(b, 浮点容差); @@ -1389,16 +1549,16 @@ impl 虚线 { impl std::fmt::Display for 虚线 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if *self.标识.read().unwrap() == "笔" { + if *self.标识.read() == "笔" { write!( f, "笔({}, {}, {}, {}, 周期: {}, 数量: {})", self.序号.load(Ordering::Relaxed), self.方向(), self.文, - self.武.read().unwrap(), + self.武.read(), self.文.中.周期, - self.武.read().unwrap().中.序号.load(Ordering::Relaxed) + self.武.read().中.序号.load(Ordering::Relaxed) - self.文.中.序号.load(Ordering::Relaxed) + 1 ) @@ -1409,20 +1569,20 @@ impl std::fmt::Display for 虚线 { Some(g) => format!("{}", g), None => "None".to_string(), }; - let 确认K线_str = match &*self.确认K线.read().unwrap() { + let 确认K线_str = match &*self.确认K线.read() { Some(k) => format!("{}", k), None => "None".to_string(), }; write!( f, "{}<{}, {}, {}, {}, {}, 数量: {}, 缺口: {}, {}>", - self.标识.read().unwrap(), + self.标识.read(), self.序号.load(Ordering::Relaxed), 四象, self.方向(), self.文, - self.武.read().unwrap(), - self.基础序列.read().unwrap().len(), + self.武.read(), + self.基础序列.read().len(), 缺口_str, 确认K线_str, ) @@ -1531,18 +1691,19 @@ mod tests { assert_eq!(笔.序号.load(Ordering::Relaxed), 0); assert!(笔.有效性.load(Ordering::Relaxed)); assert!(!笔.短路修正.load(Ordering::Relaxed)); - assert!(笔.前一缺口.read().unwrap().is_none()); + assert!(笔.前一缺口.read().is_none()); // 修改 Cell 字段 笔.序号.store(42, Ordering::Relaxed); 笔.有效性.store(false, Ordering::Relaxed); 笔.短路修正.store(true, Ordering::Relaxed); - *笔.前一缺口.write().unwrap() = Some(缺口::new(200.0, 100.0)); + *笔.前一缺口.write() = Some(缺口::new(200.0, 100.0)); assert_eq!(笔.序号.load(Ordering::Relaxed), 42); assert!(!笔.有效性.load(Ordering::Relaxed)); assert!(笔.短路修正.load(Ordering::Relaxed)); - let qk = 笔.前一缺口.read().unwrap().unwrap(); + let qk = 笔.前一缺口.read(); + let qk = qk.as_ref().unwrap(); assert!((qk.高 - 200.0).abs() < 0.01); assert!((qk.低 - 100.0).abs() < 0.01); } @@ -1558,27 +1719,27 @@ mod tests { let 笔 = 虚线::创建笔(顶, 底, true); // 标识 - assert_eq!(*笔.标识.read().unwrap(), "笔"); - *笔.标识.write().unwrap() = "测试标识".into(); - assert_eq!(*笔.标识.read().unwrap(), "测试标识"); + assert_eq!(*笔.标识.read(), "笔"); + *笔.标识.write() = "测试标识".into(); + assert_eq!(*笔.标识.read(), "测试标识"); // 模式 - assert_eq!(*笔.模式.read().unwrap(), "文武"); - *笔.模式.write().unwrap() = "全量".into(); - assert_eq!(*笔.模式.read().unwrap(), "全量"); + assert_eq!(*笔.模式.read(), "文武"); + *笔.模式.write() = "全量".into(); + assert_eq!(*笔.模式.read(), "全量"); // 基础序列 - assert!(笔.基础序列.read().unwrap().is_empty()); + assert!(笔.基础序列.read().is_empty()); let 另一底 = 辅助_创建底分型(300, 20.0, 10.0, 15); - let 笔2 = 虚线::创建笔(Arc::clone(&*笔.武.read().unwrap()), 另一底, true); - 笔.基础序列.write().unwrap().push(Arc::new(笔2)); - assert_eq!(笔.基础序列.read().unwrap().len(), 1); + let 笔2 = 虚线::创建笔(Arc::clone(&*笔.武.read()), 另一底, true); + 笔.基础序列.write().push(Arc::new(笔2)); + assert_eq!(笔.基础序列.read().len(), 1); // 武 - Replace with new 分型 let 新底 = 辅助_创建底分型(400, 15.0, 5.0, 20); let 新底_ptr = Arc::as_ptr(&新底); - *笔.武.write().unwrap() = Arc::clone(&新底); - assert_eq!(Arc::as_ptr(&*笔.武.read().unwrap()), 新底_ptr); + *笔.武.write() = Arc::clone(&新底); + assert_eq!(Arc::as_ptr(&*笔.武.read()), 新底_ptr); } // ============================================================ @@ -1598,11 +1759,8 @@ mod tests { assert_eq!(Arc::as_ptr(&克隆笔.文), Arc::as_ptr(&笔.文)); // 武 Rc 指针应一致 - assert_eq!(Arc::as_ptr(&*笔.武.read().unwrap()), Arc::as_ptr(&底)); - assert_eq!( - Arc::as_ptr(&*克隆笔.武.read().unwrap()), - Arc::as_ptr(&*笔.武.read().unwrap()) - ); + assert_eq!(Arc::as_ptr(&*笔.武.read()), Arc::as_ptr(&底)); + assert_eq!(Arc::as_ptr(&*克隆笔.武.read()), Arc::as_ptr(&*笔.武.read())); } #[test] @@ -1639,14 +1797,14 @@ mod tests { assert_eq!(笔_rc2.序号.load(Ordering::Relaxed), 77); // 通过 rc1 修改 RefCell - *笔_rc1.模式.write().unwrap() = "配置".into(); - assert_eq!(*笔_rc2.模式.read().unwrap(), "配置"); + *笔_rc1.模式.write() = "配置".into(); + assert_eq!(*笔_rc2.模式.read(), "配置"); // 通过 rc1 修改 武 let 新底 = 辅助_创建底分型(400, 15.0, 5.0, 20); let 新底_ptr = Arc::as_ptr(&新底); - *笔_rc1.武.write().unwrap() = Arc::clone(&新底); - assert_eq!(Arc::as_ptr(&*笔_rc2.武.read().unwrap()), 新底_ptr); + *笔_rc1.武.write() = Arc::clone(&新底); + assert_eq!(Arc::as_ptr(&*笔_rc2.武.read()), 新底_ptr); } // ============================================================ @@ -1660,7 +1818,7 @@ mod tests { let 笔 = 虚线::创建笔(Arc::clone(&顶), Arc::clone(&底), true); let wu = 笔.获取_武(); - assert_eq!(Arc::as_ptr(&*笔.武.read().unwrap()), Arc::as_ptr(&wu)); + assert_eq!(Arc::as_ptr(&*笔.武.read()), Arc::as_ptr(&wu)); assert_eq!(Arc::as_ptr(&wu), Arc::as_ptr(&底)); } @@ -1700,13 +1858,13 @@ mod tests { let 文_ptr_before = Arc::as_ptr(&笔.文); // 修改武 - *笔.武.write().unwrap() = Arc::clone(&底2); + *笔.武.write() = Arc::clone(&底2); // 文指针不变 assert_eq!(Arc::as_ptr(&笔.文), 文_ptr_before); // 但方向变了(因为武从底1变成底2) - let 新武耗时 = 笔.武.read().unwrap().时间戳(); + let 新武耗时 = 笔.武.read().时间戳(); assert_eq!(新武耗时, 300); } } diff --git a/chanlun/src/structure/fractal_obj.rs b/chanlun/src/structure/fractal_obj.rs index 129bd96..8df91cb 100644 --- a/chanlun/src/structure/fractal_obj.rs +++ b/chanlun/src/structure/fractal_obj.rs @@ -25,10 +25,10 @@ use crate::kline::chan_kline::缠论K线; use crate::types::分型结构; use crate::types::相对方向; +use crate::warn; use std::sync::Arc; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; -use tracing::warn; /// 分型模式 — True 时使用构造时缓存值(默认),False 时从 中 缠K 实时读取 pub static 分型模式: AtomicBool = AtomicBool::new(true); @@ -75,7 +75,7 @@ impl 分型 { 右.时间戳.load(Ordering::Relaxed), ); } - let 结构 = 中.分型.read().unwrap().unwrap_or(分型结构::散); + let 结构 = 中.分型.read().unwrap_or(分型结构::散); let 时间戳 = 中.时间戳.load(Ordering::Relaxed); let 分型特征值 = 中.分型特征值.get(); Self { @@ -102,7 +102,7 @@ impl 分型 { if 分型模式.load(Ordering::Relaxed) { self.结构 } else { - self.中.分型.read().unwrap().unwrap_or(分型结构::散) // FIXME 错误 + self.中.分型.read().unwrap_or(分型结构::散) // FIXME 错误 } } @@ -157,19 +157,17 @@ impl 分型 { if let (Some(左), Some(右)) = (&self.左, &self.右) { if self.结构() == 分型结构::底 { - if 右.标的K线.read().unwrap().收盘价 > 左.标的K线.read().unwrap().高 { + if 右.标的K线.read().收盘价 > 左.标的K线.read().高 { return "强"; - } else if 右.标的K线.read().unwrap().收盘价 > self.中.标的K线.read().unwrap().高 - { + } else if 右.标的K线.read().收盘价 > self.中.标的K线.read().高 { return "中"; } else { return "弱"; } } else if self.结构() == 分型结构::顶 { - if 右.标的K线.read().unwrap().收盘价 < 左.标的K线.read().unwrap().低 { + if 右.标的K线.read().收盘价 < 左.标的K线.read().低 { return "强"; - } else if 右.标的K线.read().unwrap().收盘价 < self.中.标的K线.read().unwrap().低 - { + } else if 右.标的K线.read().收盘价 < self.中.标的K线.read().低 { return "中"; } else { return "弱"; @@ -183,12 +181,12 @@ impl 分型 { pub fn 与MACD柱子分型匹配(&self) -> bool { if let (Some(左), Some(右)) = (&self.左, &self.右) { if self.结构() == 分型结构::底 { - let 左_k = 左.标的K线.read().unwrap(); - let 中_k = self.中.标的K线.read().unwrap(); - let 右_k = 右.标的K线.read().unwrap(); - let 左_m = 左_k.指标.read().unwrap(); - let 中_m = 中_k.指标.read().unwrap(); - let 右_m = 右_k.指标.read().unwrap(); + let 左_k = 左.标的K线.read(); + let 中_k = self.中.标的K线.read(); + let 右_k = 右.标的K线.read(); + let 左_m = 左_k.指标.read(); + let 中_m = 中_k.指标.read(); + let 右_m = 右_k.指标.read(); if let (Some(左macd), Some(中macd), Some(右macd)) = (左_m.macd(), 中_m.macd(), 右_m.macd()) { @@ -196,12 +194,12 @@ impl 分型 { } } if self.结构() == 分型结构::顶 { - let 左_k = 左.标的K线.read().unwrap(); - let 中_k = self.中.标的K线.read().unwrap(); - let 右_k = 右.标的K线.read().unwrap(); - let 左_m = 左_k.指标.read().unwrap(); - let 中_m = 中_k.指标.read().unwrap(); - let 右_m = 右_k.指标.read().unwrap(); + let 左_k = 左.标的K线.read(); + let 中_k = self.中.标的K线.read(); + let 右_k = 右.标的K线.read(); + let 左_m = 左_k.指标.read(); + let 中_m = 中_k.指标.read(); + let 右_m = 右_k.指标.read(); if let (Some(左macd), Some(中macd), Some(右macd)) = (左_m.macd(), 中_m.macd(), 右_m.macd()) { @@ -214,7 +212,7 @@ impl 分型 { /// 判断两个分型是否匹配 pub fn 判断分型(左: &Arc<分型>, 右: &Arc<分型>, _模式: &str) -> bool { - Arc::as_ptr(左) == Arc::as_ptr(右) + Arc::ptr_eq(左, 右) } /// 从缠K序列中获取以指定缠K为中元素的分型 @@ -222,9 +220,7 @@ impl 分型 { K线序列: &[Arc<缠论K线>], 中: &Arc<缠论K线>, ) -> Option { - let idx = K线序列 - .iter() - .position(|k| Arc::as_ptr(k) == Arc::as_ptr(中))?; + let idx = K线序列.iter().position(|k| Arc::ptr_eq(k, 中))?; let 左 = if idx > 0 { Some(Arc::clone(&K线序列[idx - 1])) } else { diff --git a/chanlun/src/structure/segment_feat.rs b/chanlun/src/structure/segment_feat.rs index ba1edf0..4981def 100644 --- a/chanlun/src/structure/segment_feat.rs +++ b/chanlun/src/structure/segment_feat.rs @@ -26,9 +26,10 @@ use crate::structure::dash_line::虚线; use crate::structure::feat_fractal::特征分型; use crate::structure::fractal_obj::分型; use crate::types::{分型结构, 相对方向}; +use parking_lot::RwLock; +use std::sync::Arc; use std::sync::atomic::AtomicI64; use std::sync::atomic::Ordering; -use std::sync::{Arc, RwLock}; /// 线段特征 — 特征序列元素,内部是虚线的集合。 /// @@ -62,7 +63,7 @@ impl Clone for 线段特征 { fn clone(&self) -> Self { Self { 序号: AtomicI64::new(self.序号.load(Ordering::Relaxed)), - 标识: RwLock::new(self.标识.read().unwrap().clone()), + 标识: RwLock::new(self.标识.read().clone()), 线段方向: self.线段方向, 基础序列: self.基础序列.clone(), } @@ -82,7 +83,7 @@ impl 线段特征 { /// 图表标题 — 返回标识字符串 pub fn 图表标题(&self) -> String { - self.标识.read().unwrap().clone() + self.标识.read().clone() } /// 文 — 取特征序列元素中分型特征值最大/最小的文分型 @@ -118,47 +119,31 @@ impl 线段特征 { /// 武 — 取特征序列元素中分型特征值最大/最小的武分型 /// tiebreaker: later时间戳 wins when特征值 equal (matches Python) pub fn 武(&self) -> Arc<分型> { - if self.线段方向.是否向上() { - self.基础序列 - .iter() - .max_by(|a, b| { - a.武 - .read() - .unwrap() - .分型特征值 - .partial_cmp(&b.武.read().unwrap().分型特征值) - .unwrap_or(std::cmp::Ordering::Equal) - .then_with(|| { - a.武 - .read() - .unwrap() - .时间戳() - .cmp(&b.武.read().unwrap().时间戳()) - }) - }) - .map(|x| x.武.read().unwrap().clone()) - .unwrap_or_else(|| self.基础序列[0].武.read().unwrap().clone()) + let best = if self.线段方向.是否向上() { + self.基础序列.iter().max_by(|a, b| { + let a_武 = a.武.read(); + let b_武 = b.武.read(); + a_武 + .分型特征值 + .partial_cmp(&b_武.分型特征值) + .unwrap_or(std::cmp::Ordering::Equal) + .then_with(|| a_武.时间戳().cmp(&b_武.时间戳())) + }) } else { - self.基础序列 - .iter() - .max_by(|a, b| { - b.武 - .read() - .unwrap() - .分型特征值 - .partial_cmp(&a.武.read().unwrap().分型特征值) - .unwrap_or(std::cmp::Ordering::Equal) - .then_with(|| { - a.武 - .read() - .unwrap() - .时间戳() - .cmp(&b.武.read().unwrap().时间戳()) - }) - }) - .map(|x| x.武.read().unwrap().clone()) - .unwrap_or_else(|| self.基础序列[0].武.read().unwrap().clone()) - } + self.基础序列.iter().max_by(|a, b| { + let a_武 = a.武.read(); + let b_武 = b.武.read(); + b_武 + .分型特征值 + .partial_cmp(&a_武.分型特征值) + .unwrap_or(std::cmp::Ordering::Equal) + .then_with(|| a_武.时间戳().cmp(&b_武.时间戳())) + }) + }; + best.map_or_else( + || self.基础序列[0].武.read().clone(), + |x| x.武.read().clone(), + ) } /// 高 — 文和武中分型特征值的较大者 @@ -197,7 +182,7 @@ impl 线段特征 { if let Some(pos) = self .基础序列 .iter() - .position(|x| Arc::as_ptr(x) == Arc::as_ptr(待删除虚线)) + .position(|x| Arc::ptr_eq(x, 待删除虚线)) { self.基础序列.remove(pos); Ok(()) @@ -256,7 +241,7 @@ impl 线段特征 { .unwrap(); let fake = 虚线::创建笔( Arc::clone(&小号虚线.文), - 大号虚线.武.read().unwrap().clone(), + 大号虚线.武.read().clone(), false, ); 结果.pop(); @@ -330,13 +315,13 @@ impl 线段特征 { ), ); } - if *self.标识.read().unwrap() != *other.标识.read().unwrap() { + if *self.标识.read() != *other.标识.read() { return ( false, format!( "线段特征: [标识] 不等 A={},B={}", - self.标识.read().unwrap(), - other.标识.read().unwrap() + self.标识.read(), + other.标识.read() ), ); } @@ -381,12 +366,12 @@ impl crate::types::fractal::有高低 for 线段特征 { impl std::fmt::Display for 线段特征 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if self.基础序列.is_empty() { - write!(f, "{}<{}, 空>", self.标识.read().unwrap(), self.线段方向) + write!(f, "{}<{}, 空>", self.标识.read(), self.线段方向) } else { write!( f, "{}<{}, {}, {}, {}>", - self.标识.read().unwrap(), + self.标识.read(), self.线段方向, self.文(), self.武(), diff --git a/chanlun/src/types/direction.rs b/chanlun/src/types/direction.rs index 5b119ec..e389c8b 100644 --- a/chanlun/src/types/direction.rs +++ b/chanlun/src/types/direction.rs @@ -122,6 +122,34 @@ impl 相对方向 { 前高, 前低, 后高, 后低 ); } + + /// 从可选方向序列中随机选取指定数量(与 chan.py 对齐) + pub fn 从序列中机选( + 数量: usize, + 可选方向: &[相对方向], + 可重复: bool, + ) -> Vec<相对方向> { + if 数量 == 0 || 可选方向.is_empty() { + return Vec::new(); + } + if !可重复 && 数量 > 可选方向.len() { + panic!("数量超过可选方向数"); + } + let mut result = Vec::with_capacity(数量); + if 可重复 { + for _ in 0..数量 { + let idx = fastrand::usize(..可选方向.len()); + result.push(可选方向[idx]); + } + } else { + let mut indices: Vec = (0..可选方向.len()).collect(); + fastrand::shuffle(&mut indices); + for &idx in indices.iter().take(数量) { + result.push(可选方向[idx]); + } + } + result + } } impl std::fmt::Display for 相对方向 { diff --git a/chanlun/src/types/fractal.rs b/chanlun/src/types/fractal.rs index aad4e75..8b28ac2 100644 --- a/chanlun/src/types/fractal.rs +++ b/chanlun/src/types/fractal.rs @@ -22,8 +22,8 @@ * SOFTWARE. */ +use crate::warn; use serde::{Deserialize, Serialize}; -use tracing::warn; /// 分型结构 —— 三根K线构成的结构形态 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] diff --git a/clean_install.sh b/clean_install.sh new file mode 100755 index 0000000..166db0b --- /dev/null +++ b/clean_install.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -e +cd "$(dirname "$0")" + +echo "=== 1/4 清除 Python 缓存 ===" +find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null +find . -type f -name "*.pyc" -delete 2>/dev/null +echo " Python 缓存已清除" + +echo "=== 2/4 清除 Cargo 编译缓存 ===" +rm -rf chanlun/target chanlun-py/target +echo " target/ 已清除" + +echo "=== 3/4 构建 Release ===" +cd chanlun-py +maturin build --release +echo " 构建完成" + +echo "=== 4/4 安装 ===" +pip install --break-system-packages --force-reinstall --no-deps \ + target/wheels/chanlun-*.whl +echo " 安装完成" + +echo +echo "✓ 清理 + 构建 + 安装完毕" +echo " pip show chanlun | grep Version" +pip show chanlun 2>/dev/null | grep Version diff --git a/docs/superpowers/plans/2026-06-22-signal-primitives-to-rust-core.md b/docs/superpowers/plans/2026-06-22-signal-primitives-to-rust-core.md new file mode 100644 index 0000000..d0b0408 --- /dev/null +++ b/docs/superpowers/plans/2026-06-22-signal-primitives-to-rust-core.md @@ -0,0 +1,1395 @@ +# 信号原语层移植到 Rust 核心层 实现计划 + +> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 + +**目标:** 把信号匹配原语(`Operate` / `Signal` / `Factor` / `Event` / `Position` 配置与匹配部分)从 Python `chan_external.py` 移植到 Rust 核心层,并通过 PyO3 暴露为 drop-in 兼容的 `chanlun._chanlun.{Signal,Factor,Event,Operate,Position}`。 + +**架构:** 纯 Rust 类型放 `chanlun/src/signal/`(零依赖 business/algorithm,只跟字符串和 `HashMap` 打交道,全可 `cargo test`);PyO3 包装放 `chanlun-py/src/signal_py.rs`(`XxxPy` 包装核心类型,注册为原名);`Position` 由 Python 子类继承 Rust 基类补 `update()` 状态机。 + +**技术栈:** Rust + PyO3 0.28 + sha2(确定性命名);Python 3.14 + pytest;maturin 构建。 + +**设计文档:** `docs/superpowers/specs/2026-06-22-signal-primitives-to-rust-core-design.md` + +--- + +## 文件结构 + +| 文件 | 职责 | +|---|---| +| `chanlun/src/signal/mod.rs` | 模块声明 + re-export + 第三方声明 + `匹配值` 枚举 + sha256 helper | +| `chanlun/src/signal/operate.rs` | `Operate` 枚举 + `value()` | +| `chanlun/src/signal/signal.rs` | `Signal`:`from_str`/`key`/`value`/`is_match_value`/`is_match` | +| `chanlun/src/signal/factor.rs` | `Factor`:`is_match`/`unique_signals`/`dump`/`load`/确定性 name | +| `chanlun/src/signal/event.rs` | `Event`:`is_match`/`unique_signals`/`dump`/`load`/确定性 name | +| `chanlun/src/signal/position.rs` | `Position`:config + 校验 + `unique_signals` | +| `chanlun/src/lib.rs` | 增加 `pub mod signal;` | +| `chanlun/Cargo.toml` | 增加 `sha2` 依赖 | +| `chanlun-py/src/signal_py.rs` | PyO3 包装 + `register(m)` | +| `chanlun-py/src/lib.rs` | 注册 `signal_py`(types 之后、config 之前) | +| `chanlun-py/chanlun/chan_external.py` | 删除 Python 原语类,import Rust 版;`Position` 改子类 | +| `chanlun-py/tests/test_signal_primitives.py` | 跨语言一致性测试 | + +**核心匹配设计**:核心层 `is_match` 接收 `&HashMap`,`匹配值` 区分「字符串」与「非字符串」,从而在纯 Rust 内完整表达「缺键 raise / 非 str 返回 False / str 匹配」三态,全部可 cargo test。PyO3 层只做一次 `PyDict → HashMap` 转换。 + +--- + +## 任务 0:脚手架与依赖 + +**文件:** +- 创建:`chanlun/src/signal/mod.rs`、`operate.rs`、`signal.rs`、`factor.rs`、`event.rs`、`position.rs` +- 修改:`chanlun/src/lib.rs:36`、`chanlun/Cargo.toml` + +- [ ] **步骤 1:加 sha2 依赖** + +修改 `chanlun/Cargo.toml` 的 `[dependencies]`,追加: + +```toml +sha2 = "0.10" +``` + +- [ ] **步骤 2:创建模块骨架** + +创建 `chanlun/src/signal/mod.rs`: + +```rust +//! 信号匹配原语层。 +//! +//! 第三方代码声明:本模块的 Signal/Factor/Event/Position/Operate 匹配框架 +//! 摘录自 czsc 项目(https://github.com/waditu/czsc),Apache License 2.0 授权, +//! 已做中文命名适配与 Rust 重写。 + +use std::collections::HashMap; + +pub mod event; +pub mod factor; +pub mod operate; +pub mod position; +pub mod signal; + +pub use event::Event; +pub use factor::Factor; +pub use operate::Operate; +pub use position::Position; +pub use signal::Signal; + +/// 信号字典中某个 key 对应的值。区分「字符串」与「非字符串」, +/// 以在纯 Rust 内表达 Python `is_match` 的三态:缺键 / 非 str / str。 +#[derive(Clone, Debug)] +pub enum 匹配值 { + 字符串(String), + 非字符串, +} + +/// 信号字典类型别名。 +pub type 信号字典 = HashMap; + +/// 缺键错误 — `is_match` 在信号字典中找不到 key 时返回。 +#[derive(Debug, Clone)] +pub struct 缺键错误(pub String); + +/// 对任意字节串算 sha256,取大写十六进制前 4 位(= 前 2 字节)。 +/// 对应 Python `hashlib.sha256(...).hexdigest().upper()[:4]`。 +pub(crate) fn sha256前4(输入: &str) -> String { + use sha2::{Digest, Sha256}; + let 摘要 = Sha256::digest(输入.as_bytes()); + format!("{:02X}{:02X}", 摘要[0], 摘要[1]) +} +``` + +创建 `operate.rs`、`signal.rs`、`factor.rs`、`event.rs`、`position.rs` 五个空文件(内容 `// placeholder`,后续任务填充)。 + +- [ ] **步骤 3:在 lib.rs 注册模块** + +修改 `chanlun/src/lib.rs`,在 `pub mod kline;`(第 32 行)之后加入: + +```rust +pub mod signal; +``` + +- [ ] **步骤 4:验证构建** + +运行:`cd /home/moscow/chanlun.rs/chanlun && cargo build` +预期:编译通过(仅 placeholder 文件 + 未使用警告可接受)。 + +- [ ] **步骤 5:Commit** + +```bash +cd /home/moscow/chanlun.rs +git add chanlun/src/signal chanlun/src/lib.rs chanlun/Cargo.toml +git commit -m "feat(signal): 脚手架 — 信号原语模块 + sha2 依赖" +``` + +--- + +## 任务 1:Operate 枚举 + +**文件:** +- 修改:`chanlun/src/signal/operate.rs` + +- [ ] **步骤 1:编写失败的测试** + +在 `operate.rs` 写入: + +```rust +//! 缠论买卖操作类型。 + +/// 持仓/操作类型。值对应中文,与 Python `chan_external.Operate` 一致。 +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Operate { + 持多, // HL + 持空, // HS + 持币, // HO + 开多, // LO + 平多, // LE + 开空, // SO + 平空, // SE +} + +impl Operate { + /// 中文值,对应 Python Enum 的 `.value`。 + pub fn value(&self) -> &'static str { + match self { + Operate::持多 => "持多", + Operate::持空 => "持空", + Operate::持币 => "持币", + Operate::开多 => "开多", + Operate::平多 => "平多", + Operate::开空 => "开空", + Operate::平空 => "平空", + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_operate_value() { + assert_eq!(Operate::开多.value(), "开多"); + assert_eq!(Operate::平空.value(), "平空"); + assert_eq!(Operate::持币.value(), "持币"); + } +} +``` + +- [ ] **步骤 2:运行测试验证通过** + +运行:`cd /home/moscow/chanlun.rs/chanlun && cargo test signal::operate` +预期:PASS(`test_operate_value`)。 + +- [ ] **步骤 3:Commit** + +```bash +cd /home/moscow/chanlun.rs +git add chanlun/src/signal/operate.rs +git commit -m "feat(signal): Operate 枚举" +``` + +--- + +## 任务 2:Signal 核心 + +**文件:** +- 修改:`chanlun/src/signal/signal.rs` + +- [ ] **步骤 1:编写测试 + 实现** + +在 `signal.rs` 写入: + +```rust +//! 信号原语 — k1_k2_k3_v1_v2_v3_score 七段字符串。 + +use crate::signal::{信号字典, 匹配值, 缺键错误}; + +pub(crate) const 任意: &str = "任意"; + +/// 单个信号。字段与 Python `chan_external.Signal` 一致。 +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Signal { + pub signal: String, + pub score: i32, + pub k1: String, + pub k2: String, + pub k3: String, + pub v1: String, + pub v2: String, + pub v3: String, +} + +impl Signal { + /// 从字段构造。任一 kx/vx 缺省时由调用方传 "任意"。 + pub fn 从字段( + k1: String, k2: String, k3: String, + v1: String, v2: String, v3: String, score: i32, + ) -> Result { + if !(0..=100).contains(&score) { + return Err("score 必须在0~100之间".to_string()); + } + let signal = format!("{k1}_{k2}_{k3}_{v1}_{v2}_{v3}_{score}"); + Ok(Self { signal, score, k1, k2, k3, v1, v2, v3 }) + } + + /// 从完整信号串解析(七段,六个下划线)。 + pub fn 从字符串(s: &str) -> Result { + let parts: Vec<&str> = s.split('_').collect(); + if parts.len() != 7 { + return Err(format!( + "Signal 格式无效:应为 k1_k2_k3_v1_v2_v3_score(7段),收到 {}", + s + )); + } + let score: i32 = parts[6] + .parse() + .map_err(|_| format!("无法解析 score: {}", parts[6]))?; + if !(0..=100).contains(&score) { + return Err("score 必须在0~100之间".to_string()); + } + Ok(Self { + signal: s.to_string(), + score, + k1: parts[0].to_string(), + k2: parts[1].to_string(), + k3: parts[2].to_string(), + v1: parts[3].to_string(), + v2: parts[4].to_string(), + v3: parts[5].to_string(), + }) + } + + /// key — k1/k2/k3 中非「任意」部分用 _ 连接。 + pub fn key(&self) -> String { + [&self.k1, &self.k2, &self.k3] + .iter() + .filter(|k| k.as_str() != 任意) + .map(|k| k.as_str()) + .collect::>() + .join("_") + } + + /// value — v1_v2_v3_score。 + pub fn value(&self) -> String { + format!("{}_{}_{}_{}", self.v1, self.v2, self.v3, self.score) + } + + /// 纯值匹配 — 给定信号字典里取到的 value 串(v1_v2_v3_score),判断是否匹配。 + pub fn is_match_value(&self, 值: &str) -> bool { + let parts: Vec<&str> = 值.split('_').collect(); + if parts.len() != 4 { + return false; + } + let (v1, v2, v3, score_str) = (parts[0], parts[1], parts[2], parts[3]); + let score: i32 = score_str.parse().unwrap_or(0); + score >= self.score + && (v1 == self.v1 || self.v1 == 任意) + && (v2 == self.v2 || self.v2 == 任意) + && (v3 == self.v3 || self.v3 == 任意) + } + + /// 在信号字典中匹配。缺键 → Err(对应 Python raise ValueError), + /// 非字符串值 → Ok(false),字符串值 → 走 is_match_value。 + pub fn is_match(&self, 字典: &信号字典) -> Result { + let key = self.key(); + match 字典.get(&key) { + None => Err(缺键错误(key)), + Some(匹配值::非字符串) => Ok(false), + Some(匹配值::字符串(v)) => Ok(self.is_match_value(v)), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + + #[test] + fn test_从字符串_七段() { + let s = Signal::从字符串("14400_D1MO3_中枢段DEA穿越2V230602_中枢段DEA穿越2_三卖_偏移0_100").unwrap(); + assert_eq!(s.k1, "14400"); + assert_eq!(s.k3, "中枢段DEA穿越2V230602"); + assert_eq!(s.v2, "三卖"); + assert_eq!(s.score, 100); + } + + #[test] + fn test_从字符串_非七段_报错() { + assert!(Signal::从字符串("a_b_c").is_err()); + } + + #[test] + fn test_score_越界_报错() { + assert!(Signal::从字段("a".into(),"b".into(),"c".into(),"d".into(),"e".into(),"f".into(),101).is_err()); + } + + #[test] + fn test_key_过滤任意() { + let s = Signal::从字段("14400".into(),"D1MO3".into(),"中枢".into(),任意.into(),"三买".into(),任意.into(),0).unwrap(); + assert_eq!(s.key(), "14400_D1MO3_中枢"); + } + + #[test] + fn test_value() { + let s = Signal::从字段("k1".into(),"k2".into(),"k3".into(),"v1".into(),"v2".into(),"v3".into(),88).unwrap(); + assert_eq!(s.value(), "v1_v2_v3_88"); + } + + #[test] + fn test_is_match_缺键_报错() { + let s = Signal::从字段("14400".into(),"D1MO3".into(),"中枢".into(),任意.into(),"三买".into(),任意.into(),0).unwrap(); + let 字典: HashMap = HashMap::new(); + assert!(s.is_match(&字典).is_err()); + } + + #[test] + fn test_is_match_非字符串_false() { + let s = Signal::从字段("14400".into(),"D1MO3".into(),"中枢".into(),任意.into(),"三买".into(),任意.into(),0).unwrap(); + let mut 字典 = HashMap::new(); + 字典.insert("14400_D1MO3_中枢".to_string(), 匹配值::非字符串); + assert_eq!(s.is_match(&字典).unwrap(), false); + } + + #[test] + fn test_is_match_命中() { + let s = Signal::从字段("14400".into(),"D1MO3".into(),"中枢".into(),任意.into(),"三买".into(),任意.into(),0).unwrap(); + let mut 字典 = HashMap::new(); + 字典.insert("14400_D1MO3_中枢".to_string(), 匹配值::字符串("中枢段DEA穿越2_三买_偏移0_100".into())); + assert_eq!(s.is_match(&字典).unwrap(), true); + } + + #[test] + fn test_is_match_v2不符_未命中() { + let s = Signal::从字段("14400".into(),"D1MO3".into(),"中枢".into(),任意.into(),"三买".into(),任意.into(),0).unwrap(); + let mut 字典 = HashMap::new(); + 字典.insert("14400_D1MO3_中枢".to_string(), 匹配值::字符串("中枢段DEA穿越2_三卖_偏移0_100".into())); + assert_eq!(s.is_match(&字典).unwrap(), false); + } +} +``` + +- [ ] **步骤 2:运行测试验证通过** + +运行:`cd /home/moscow/chanlun.rs/chanlun && cargo test signal::signal` +预期:8 个测试全 PASS。 + +- [ ] **步骤 3:Commit** + +```bash +cd /home/moscow/chanlun.rs +git add chanlun/src/signal/signal.rs +git commit -m "feat(signal): Signal 核心 — 解析/key/value/is_match 三态" +``` + +--- + +## 任务 3:Factor 核心 + +**文件:** +- 修改:`chanlun/src/signal/factor.rs` + +- [ ] **步骤 1:编写测试 + 实现** + +在 `factor.rs` 写入: + +```rust +//! 因子 — signals_all 全满足 + signals_any 任一满足 + signals_not 全不满足。 + +use crate::signal::signal::Signal; +use crate::signal::{sha256前4, 信号字典, 缺键错误}; + +#[derive(Clone, Debug)] +pub struct Factor { + pub signals_all: Vec, + pub signals_any: Vec, + pub signals_not: Vec, + pub name: String, +} + +impl Factor { + /// 构造。signals_all 为空 → Err。name 自动补确定性哈希后缀。 + pub fn 新建( + signals_all: Vec, + signals_any: Vec, + signals_not: Vec, + name: String, + ) -> Result { + if signals_all.is_empty() { + return Err("signals_all 不能为空".to_string()); + } + let hash = Self::计算哈希(&signals_all, &signals_any, &signals_not); + let 前缀 = name.split('#').next().unwrap_or("").to_string(); + let name = format!("{前缀}#{hash}"); + Ok(Self { signals_all, signals_any, signals_not, name }) + } + + /// 确定性哈希 — 拼接三组 signals 串后算 sha256 前4。 + fn 计算哈希(all: &[Signal], any: &[Signal], not: &[Signal]) -> String { + let 取串 = |v: &[Signal]| v.iter().map(|s| s.signal.clone()).collect::>().join(","); + let 规范 = format!("all=[{}]|any=[{}]|not=[{}]", 取串(all), 取串(any), 取串(not)); + sha256前4(&规范) + } + + pub fn unique_signals(&self) -> Vec { + let mut 集合 = std::collections::BTreeSet::new(); + for s in self.signals_all.iter().chain(&self.signals_any).chain(&self.signals_not) { + 集合.insert(s.signal.clone()); + } + 集合.into_iter().collect() + } + + /// 因子匹配。任一信号缺键 → Err 向上传播。 + pub fn is_match(&self, 字典: &信号字典) -> Result { + for s in &self.signals_not { + if s.is_match(字典)? { + return Ok(false); + } + } + for s in &self.signals_all { + if !s.is_match(字典)? { + return Ok(false); + } + } + if self.signals_any.is_empty() { + return Ok(true); + } + for s in &self.signals_any { + if s.is_match(字典)? { + return Ok(true); + } + } + Ok(false) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::signal::匹配值; + use std::collections::HashMap; + + fn 信号(k3: &str, v2: &str) -> Signal { + Signal::从字段("14400".into(),"D1MO3".into(),k3.into(),"任意".into(),v2.into(),"任意".into(),0).unwrap() + } + fn 字典(k3: &str, v2: &str) -> HashMap { + let mut m = HashMap::new(); + m.insert(format!("14400_D1MO3_{k3}"), 匹配值::字符串(format!("x_{v2}_y_100"))); + m + } + + #[test] + fn test_signals_all_为空_报错() { + assert!(Factor::新建(vec![], vec![], vec![], "".into()).is_err()); + } + + #[test] + fn test_name_含哈希后缀() { + let f = Factor::新建(vec![信号("中枢","三买")], vec![], vec![], "测试".into()).unwrap(); + assert!(f.name.starts_with("测试#")); + assert_eq!(f.name.len(), "测试#".len() + 4); + } + + #[test] + fn test_name_确定性() { + let f1 = Factor::新建(vec![信号("中枢","三买")], vec![], vec![], "".into()).unwrap(); + let f2 = Factor::新建(vec![信号("中枢","三买")], vec![], vec![], "".into()).unwrap(); + assert_eq!(f1.name, f2.name); + } + + #[test] + fn test_all_命中() { + let f = Factor::新建(vec![信号("中枢","三买")], vec![], vec![], "".into()).unwrap(); + assert_eq!(f.is_match(&字典("中枢","三买")).unwrap(), true); + } + + #[test] + fn test_not_命中则false() { + let f = Factor::新建(vec![信号("中枢","三买")], vec![], vec![信号("中枢","三买")], "".into()).unwrap(); + assert_eq!(f.is_match(&字典("中枢","三买")).unwrap(), false); + } + + #[test] + fn test_缺键传播错误() { + let f = Factor::新建(vec![信号("中枢","三买")], vec![], vec![], "".into()).unwrap(); + let m: HashMap = HashMap::new(); + assert!(f.is_match(&m).is_err()); + } +} +``` + +- [ ] **步骤 2:运行测试验证通过** + +运行:`cd /home/moscow/chanlun.rs/chanlun && cargo test signal::factor` +预期:6 个测试全 PASS。 + +- [ ] **步骤 3:Commit** + +```bash +cd /home/moscow/chanlun.rs +git add chanlun/src/signal/factor.rs +git commit -m "feat(signal): Factor 核心 — and/or/not 匹配 + 确定性命名" +``` + +--- + +## 任务 4:Event 核心 + +**文件:** +- 修改:`chanlun/src/signal/event.rs` + +- [ ] **步骤 1:编写测试 + 实现** + +在 `event.rs` 写入: + +```rust +//! 事件 — operate + 因子列表(任一因子满足则事件为真)。 + +use crate::signal::factor::Factor; +use crate::signal::operate::Operate; +use crate::signal::signal::Signal; +use crate::signal::{sha256前4, 信号字典, 缺键错误}; + +#[derive(Clone, Debug)] +pub struct Event { + pub operate: Operate, + pub factors: Vec, + pub signals_all: Vec, + pub signals_any: Vec, + pub signals_not: Vec, + pub name: String, + pub sha256: String, +} + +impl Event { + /// 构造。factors 为空 → Err。name 自动补哈希。 + pub fn 新建( + operate: Operate, + factors: Vec, + signals_all: Vec, + signals_any: Vec, + signals_not: Vec, + name: String, + ) -> Result { + if factors.is_empty() { + return Err("factors 不能为空".to_string()); + } + let hash = Self::计算哈希(&factors, &signals_all, &signals_any, &signals_not); + let name = if name.is_empty() { + format!("{}#{hash}", operate.value()) + } else { + format!("{}#{hash}", name.split('#').next().unwrap_or("")) + }; + Ok(Self { operate, factors, signals_all, signals_any, signals_not, name, sha256: hash }) + } + + fn 计算哈希(factors: &[Factor], all: &[Signal], any: &[Signal], not: &[Signal]) -> String { + let 取串 = |v: &[Signal]| v.iter().map(|s| s.signal.clone()).collect::>().join(","); + let 因子串 = factors.iter().map(|f| f.name.clone()).collect::>().join(";"); + let 规范 = format!( + "factors=[{}]|all=[{}]|any=[{}]|not=[{}]", + 因子串, 取串(all), 取串(any), 取串(not) + ); + sha256前4(&规范) + } + + pub fn unique_signals(&self) -> Vec { + let mut 集合 = std::collections::BTreeSet::new(); + for s in self.signals_all.iter().chain(&self.signals_any).chain(&self.signals_not) { + 集合.insert(s.signal.clone()); + } + for f in &self.factors { + for s in f.unique_signals() { + 集合.insert(s); + } + } + 集合.into_iter().collect() + } + + /// 事件匹配。命中返回 (true, 因子名),否则 (false, None)。 + pub fn is_match(&self, 字典: &信号字典) -> Result<(bool, Option), 缺键错误> { + for s in &self.signals_not { + if s.is_match(字典)? { + return Ok((false, None)); + } + } + for s in &self.signals_all { + if !s.is_match(字典)? { + return Ok((false, None)); + } + } + if !self.signals_any.is_empty() { + let mut 任一命中 = false; + for s in &self.signals_any { + if s.is_match(字典)? { + 任一命中 = true; + break; + } + } + if !任一命中 { + return Ok((false, None)); + } + } + for f in &self.factors { + if f.is_match(字典)? { + return Ok((true, Some(f.name.clone()))); + } + } + Ok((false, None)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::signal::匹配值; + use std::collections::HashMap; + + fn 信号(k3: &str, v2: &str) -> Signal { + Signal::从字段("14400".into(),"D1MO3".into(),k3.into(),"任意".into(),v2.into(),"任意".into(),0).unwrap() + } + fn 因子(k3: &str, v2: &str) -> Factor { + Factor::新建(vec![信号(k3, v2)], vec![], vec![], "".into()).unwrap() + } + fn 字典(k3: &str, v2: &str) -> HashMap { + let mut m = HashMap::new(); + m.insert(format!("14400_D1MO3_{k3}"), 匹配值::字符串(format!("x_{v2}_y_100"))); + m + } + + #[test] + fn test_factors_为空_报错() { + assert!(Event::新建(Operate::开多, vec![], vec![], vec![], vec![], "".into()).is_err()); + } + + #[test] + fn test_name_默认用operate值() { + let e = Event::新建(Operate::开多, vec![因子("中枢","三买")], vec![], vec![], vec![], "".into()).unwrap(); + assert!(e.name.starts_with("开多#")); + } + + #[test] + fn test_任一因子命中() { + let e = Event::新建( + Operate::开多, + vec![因子("中枢A","三买"), 因子("中枢B","三买")], + vec![], vec![], vec![], "".into(), + ).unwrap(); + // 只满足第二个因子 + let (命中, 名) = e.is_match(&字典("中枢B","三买")).unwrap(); + assert!(命中); + assert!(名.is_some()); + } + + #[test] + fn test_无因子命中_false() { + let e = Event::新建(Operate::开多, vec![因子("中枢","三买")], vec![], vec![], vec![], "".into()).unwrap(); + let (命中, 名) = e.is_match(&字典("中枢","三卖")).unwrap(); + assert!(!命中); + assert!(名.is_none()); + } +} +``` + +- [ ] **步骤 2:运行测试验证通过** + +运行:`cd /home/moscow/chanlun.rs/chanlun && cargo test signal::event` +预期:4 个测试全 PASS。 + +- [ ] **步骤 3:Commit** + +```bash +cd /home/moscow/chanlun.rs +git add chanlun/src/signal/event.rs +git commit -m "feat(signal): Event 核心 — 因子 OR 匹配 + 命中返回因子名" +``` + +--- + +## 任务 5:Position 核心(配置 + 校验) + +**文件:** +- 修改:`chanlun/src/signal/position.rs` + +- [ ] **步骤 1:编写测试 + 实现** + +在 `position.rs` 写入: + +```rust +//! 仓位配置基类 — 只含配置与事件匹配;update 状态机在 Python 子类。 + +use crate::signal::event::Event; +use crate::signal::operate::Operate; + +#[derive(Clone, Debug)] +pub struct Position { + pub symbol: String, + pub opens: Vec, + pub exits: Vec, + pub events: Vec, + pub name: String, + pub interval: i64, + pub timeout: i64, + pub stop_loss: i64, + pub T0: bool, +} + +impl Position { + /// 构造。name 必填;每个 event.operate 必须 ∈ {开多,平多,开空,平空}。 + pub fn 新建( + symbol: String, + opens: Vec, + exits: Vec, + interval: i64, + timeout: i64, + stop_loss: i64, + T0: bool, + name: String, + ) -> Result { + if name.is_empty() { + return Err("name 是必须的参数".to_string()); + } + let mut events = opens.clone(); + events.extend(exits.clone()); + for e in &events { + if !matches!( + e.operate, + Operate::开多 | Operate::平多 | Operate::开空 | Operate::平空 + ) { + return Err(format!("非法 operate: {}", e.operate.value())); + } + } + Ok(Self { symbol, opens, exits, events, name, interval, timeout, stop_loss, T0 }) + } + + pub fn unique_signals(&self) -> Vec { + let mut 集合 = std::collections::BTreeSet::new(); + for e in &self.events { + for s in e.unique_signals() { + 集合.insert(s); + } + } + 集合.into_iter().collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::signal::factor::Factor; + use crate::signal::signal::Signal; + + fn 开多事件() -> Event { + let s = Signal::从字段("14400".into(),"D1MO3".into(),"中枢".into(),"任意".into(),"三买".into(),"任意".into(),0).unwrap(); + let f = Factor::新建(vec![s], vec![], vec![], "".into()).unwrap(); + Event::新建(Operate::开多, vec![f], vec![], vec![], vec![], "".into()).unwrap() + } + + #[test] + fn test_name_缺失_报错() { + assert!(Position::新建("btc".into(), vec![开多事件()], vec![], 0, 1000, 1000, false, "".into()).is_err()); + } + + #[test] + fn test_构造成功() { + let p = Position::新建("btc".into(), vec![开多事件()], vec![], 0, 1000, 1000, false, "中枢".into()).unwrap(); + assert_eq!(p.name, "中枢"); + assert_eq!(p.events.len(), 1); + } + + #[test] + fn test_unique_signals_去重() { + let p = Position::新建("btc".into(), vec![开多事件(), 开多事件()], vec![], 0, 1000, 1000, false, "中枢".into()).unwrap(); + assert_eq!(p.unique_signals().len(), 1); + } +} +``` + +- [ ] **步骤 2:运行测试验证通过** + +运行:`cd /home/moscow/chanlun.rs/chanlun && cargo test signal::position` +预期:3 个测试全 PASS。运行 `cargo test signal` 确认整模块通过。 + +- [ ] **步骤 3:Commit** + +```bash +cd /home/moscow/chanlun.rs +git add chanlun/src/signal/position.rs +git commit -m "feat(signal): Position 核心 — 配置校验 + unique_signals" +``` + +--- + +## 任务 6:PyO3 绑定 — Operate + Signal + 注册接线 + +**文件:** +- 创建:`chanlun-py/src/signal_py.rs` +- 修改:`chanlun-py/src/lib.rs:111`(mod 声明)、`:238`(注册顺序) + +- [ ] **步骤 1:创建绑定文件(Operate + Signal)** + +创建 `chanlun-py/src/signal_py.rs`(MIT 头略,照搬 types_py.rs 头): + +```rust +use pyo3::exceptions::{PyTypeError, PyValueError}; +use pyo3::prelude::*; +use pyo3::types::PyDict; +use std::collections::HashMap; + +use chanlun::signal::operate::Operate as 核心Operate; +use chanlun::signal::signal::Signal as 核心Signal; +use chanlun::signal::{匹配值, 信号字典}; + +/// Operate 枚举绑定。 +#[pyclass(name = "Operate", module = "chanlun._chanlun", eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +pub enum OperatePy { HL, HS, HO, LO, LE, SO, SE } + +impl OperatePy { + pub(crate) fn 转核心(self) -> 核心Operate { + match self { + OperatePy::HL => 核心Operate::持多, + OperatePy::HS => 核心Operate::持空, + OperatePy::HO => 核心Operate::持币, + OperatePy::LO => 核心Operate::开多, + OperatePy::LE => 核心Operate::平多, + OperatePy::SO => 核心Operate::开空, + OperatePy::SE => 核心Operate::平空, + } + } + pub(crate) fn 从核心(o: 核心Operate) -> Self { + match o { + 核心Operate::持多 => OperatePy::HL, + 核心Operate::持空 => OperatePy::HS, + 核心Operate::持币 => OperatePy::HO, + 核心Operate::开多 => OperatePy::LO, + 核心Operate::平多 => OperatePy::LE, + 核心Operate::开空 => OperatePy::SO, + 核心Operate::平空 => OperatePy::SE, + } + } +} + +#[pymethods] +impl OperatePy { + #[getter] + fn value(&self) -> &'static str { self.转核心().value() } + fn __str__(&self) -> &'static str { self.转核心().value() } + fn __repr__(&self) -> String { format!("Operate.{:?}", self) } +} + +/// 把 PyDict 转成核心层信号字典:str 值 → 字符串,其余 → 非字符串。 +pub(crate) fn 字典转核心(s: &Bound<'_, PyDict>) -> PyResult<信号字典> { + let mut out: 信号字典 = HashMap::new(); + for (k, v) in s.iter() { + let key: String = k.extract()?; + let 值 = match v.extract::() { + Ok(文本) if !文本.is_empty() => 匹配值::字符串(文本), + _ => 匹配值::非字符串, + }; + out.insert(key, 值); + } + Ok(out) +} + +/// Signal 绑定。 +#[pyclass(name = "Signal", module = "chanlun._chanlun")] +#[derive(Clone)] +pub struct SignalPy { + pub(crate) inner: 核心Signal, +} + +#[pymethods] +impl SignalPy { + #[new] + #[pyo3(signature = (signal=String::new(), score=0, k1="任意".to_string(), k2="任意".to_string(), k3="任意".to_string(), v1="任意".to_string(), v2="任意".to_string(), v3="任意".to_string()))] + fn new(signal: String, score: i32, k1: String, k2: String, k3: String, v1: String, v2: String, v3: String) -> PyResult { + let inner = if signal.is_empty() { + 核心Signal::从字段(k1, k2, k3, v1, v2, v3, score) + } else { + 核心Signal::从字符串(&signal) + } + .map_err(PyValueError::new_err)?; + Ok(Self { inner }) + } + + #[getter] fn signal(&self) -> String { self.inner.signal.clone() } + #[getter] fn score(&self) -> i32 { self.inner.score } + #[getter] fn k1(&self) -> String { self.inner.k1.clone() } + #[getter] fn k2(&self) -> String { self.inner.k2.clone() } + #[getter] fn k3(&self) -> String { self.inner.k3.clone() } + #[getter] fn v1(&self) -> String { self.inner.v1.clone() } + #[getter] fn v2(&self) -> String { self.inner.v2.clone() } + #[getter] fn v3(&self) -> String { self.inner.v3.clone() } + + #[getter] fn key(&self) -> String { self.inner.key() } + #[getter] fn value(&self) -> String { self.inner.value() } + + fn is_match(&self, s: &Bound<'_, PyDict>) -> PyResult { + let 字典 = 字典转核心(s)?; + self.inner + .is_match(&字典) + .map_err(|e| PyValueError::new_err(format!("{} 不在信号列表中", e.0))) + } + + fn __repr__(&self) -> String { format!("Signal('{}')", self.inner.signal) } +} + +pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + Ok(()) +} +``` + +- [ ] **步骤 2:在 lib.rs 接线** + +修改 `chanlun-py/src/lib.rs`:第 111 行 `mod types_py;` 之后加 `mod signal_py;`;在第 238 行 `types_py::register(m)?;` 之后加: + +```rust + // 阶段 1.5: 信号原语 + signal_py::register(m)?; +``` + +- [ ] **步骤 3:构建并冒烟测试导入** + +运行: +```bash +cd /home/moscow/chanlun.rs/chanlun-py && maturin develop 2>&1 | tail -3 +cd /tmp && python -c " +from chanlun._chanlun import Signal, Operate +s = Signal('14400_D1MO3_中枢_中枢段DEA穿越2_三买_偏移0_100') +assert s.k3 == '中枢', s.k3 +assert s.key == '14400_D1MO3_中枢' +assert s.value == '中枢段DEA穿越2_三买_偏移0_100' +assert s.is_match({'14400_D1MO3_中枢': '中枢段DEA穿越2_三买_偏移0_100'}) is True +try: + s.is_match({}) + raise SystemExit('应抛 ValueError') +except ValueError: + pass +assert s.is_match({'14400_D1MO3_中枢': 123}) is False +print('Signal/Operate 绑定 OK') +" +``` +预期:打印 `Signal/Operate 绑定 OK`。 + +- [ ] **步骤 4:Commit** + +```bash +cd /home/moscow/chanlun.rs +git add chanlun-py/src/signal_py.rs chanlun-py/src/lib.rs +git commit -m "feat(signal-py): Operate/Signal PyO3 绑定 + 注册接线" +``` + +--- + +## 任务 7:PyO3 绑定 — Factor + Event + +**文件:** +- 修改:`chanlun-py/src/signal_py.rs` + +- [ ] **步骤 1:追加 Factor + Event 绑定** + +在 `signal_py.rs` 的 `register` 函数之前追加: + +```rust +use chanlun::signal::event::Event as 核心Event; +use chanlun::signal::factor::Factor as 核心Factor; + +#[pyclass(name = "Factor", module = "chanlun._chanlun")] +#[derive(Clone)] +pub struct FactorPy { + pub(crate) inner: 核心Factor, +} + +#[pymethods] +impl FactorPy { + #[new] + #[pyo3(signature = (signals_all, signals_any=Vec::new(), signals_not=Vec::new(), name=String::new()))] + fn new(signals_all: Vec, signals_any: Vec, signals_not: Vec, name: String) -> PyResult { + let 取 = |v: Vec| v.into_iter().map(|s| s.inner).collect::>(); + let inner = 核心Factor::新建(取(signals_all), 取(signals_any), 取(signals_not), name) + .map_err(PyValueError::new_err)?; + Ok(Self { inner }) + } + + #[getter] fn name(&self) -> String { self.inner.name.clone() } + #[getter] fn signals_all(&self) -> Vec { self.inner.signals_all.iter().cloned().map(|inner| SignalPy { inner }).collect() } + #[getter] fn signals_any(&self) -> Vec { self.inner.signals_any.iter().cloned().map(|inner| SignalPy { inner }).collect() } + #[getter] fn signals_not(&self) -> Vec { self.inner.signals_not.iter().cloned().map(|inner| SignalPy { inner }).collect() } + + #[getter] fn unique_signals(&self) -> Vec { self.inner.unique_signals() } + + fn is_match(&self, s: &Bound<'_, PyDict>) -> PyResult { + let 字典 = 字典转核心(s)?; + self.inner.is_match(&字典).map_err(|e| PyValueError::new_err(format!("{} 不在信号列表中", e.0))) + } +} + +#[pyclass(name = "Event", module = "chanlun._chanlun")] +#[derive(Clone)] +pub struct EventPy { + pub(crate) inner: 核心Event, +} + +#[pymethods] +impl EventPy { + #[new] + #[pyo3(signature = (operate, factors, signals_all=Vec::new(), signals_any=Vec::new(), signals_not=Vec::new(), name=String::new()))] + fn new(operate: OperatePy, factors: Vec, signals_all: Vec, signals_any: Vec, signals_not: Vec, name: String) -> PyResult { + let 取s = |v: Vec| v.into_iter().map(|s| s.inner).collect::>(); + let 取f = |v: Vec| v.into_iter().map(|f| f.inner).collect::>(); + let inner = 核心Event::新建(operate.转核心(), 取f(factors), 取s(signals_all), 取s(signals_any), 取s(signals_not), name) + .map_err(PyValueError::new_err)?; + Ok(Self { inner }) + } + + #[getter] fn name(&self) -> String { self.inner.name.clone() } + #[getter] fn sha256(&self) -> String { self.inner.sha256.clone() } + #[getter] fn operate(&self) -> OperatePy { OperatePy::从核心(self.inner.operate) } + #[getter] fn factors(&self) -> Vec { self.inner.factors.iter().cloned().map(|inner| FactorPy { inner }).collect() } + #[getter] fn unique_signals(&self) -> Vec { self.inner.unique_signals() } + + fn is_match(&self, s: &Bound<'_, PyDict>) -> PyResult<(bool, Option)> { + let 字典 = 字典转核心(s)?; + self.inner.is_match(&字典).map_err(|e| PyValueError::new_err(format!("{} 不在信号列表中", e.0))) + } +} +``` + +在 `register` 中追加: + +```rust + m.add_class::()?; + m.add_class::()?; +``` + +- [ ] **步骤 2:构建并冒烟测试** + +运行: +```bash +cd /home/moscow/chanlun.rs/chanlun-py && maturin develop 2>&1 | tail -3 +cd /tmp && python -c " +from chanlun._chanlun import Signal, Factor, Event, Operate +s = Signal(k1='14400', k2='D1MO3', k3='中枢', v2='三买') +f = Factor(signals_all=[s]) +assert f.name.startswith('#'), f.name +e = Event(Operate.LO, [f]) +assert e.name.startswith('开多#'), e.name +d = {'14400_D1MO3_中枢': '中枢段DEA穿越2_三买_偏移0_100'} +assert f.is_match(d) is True +ok, fname = e.is_match(d) +assert ok and fname +print('Factor/Event 绑定 OK') +" +``` +预期:打印 `Factor/Event 绑定 OK`。 + +- [ ] **步骤 3:Commit** + +```bash +cd /home/moscow/chanlun.rs +git add chanlun-py/src/signal_py.rs +git commit -m "feat(signal-py): Factor/Event PyO3 绑定" +``` + +--- + +## 任务 8:PyO3 绑定 — Position(可子类化) + +**文件:** +- 修改:`chanlun-py/src/signal_py.rs` + +- [ ] **步骤 1:追加 Position 绑定** + +在 `register` 之前追加: + +```rust +use chanlun::signal::position::Position as 核心Position; + +#[pyclass(name = "Position", module = "chanlun._chanlun", subclass)] +#[derive(Clone)] +pub struct PositionPy { + pub(crate) inner: 核心Position, +} + +#[pymethods] +impl PositionPy { + #[new] + #[pyo3(signature = (symbol, opens, exits=Vec::new(), interval=0, timeout=1000, stop_loss=1000, T0=false, name=String::new()))] + fn new(symbol: String, opens: Vec, exits: Vec, interval: i64, timeout: i64, stop_loss: i64, T0: bool, name: String) -> PyResult { + let 取 = |v: Vec| v.into_iter().map(|e| e.inner).collect::>(); + let inner = 核心Position::新建(symbol, 取(opens), 取(exits), interval, timeout, stop_loss, T0, name) + .map_err(PyValueError::new_err)?; + Ok(Self { inner }) + } + + #[getter] fn symbol(&self) -> String { self.inner.symbol.clone() } + #[getter] fn name(&self) -> String { self.inner.name.clone() } + #[getter] fn opens(&self) -> Vec { self.inner.opens.iter().cloned().map(|inner| EventPy { inner }).collect() } + #[getter] fn exits(&self) -> Vec { self.inner.exits.iter().cloned().map(|inner| EventPy { inner }).collect() } + #[getter] fn events(&self) -> Vec { self.inner.events.iter().cloned().map(|inner| EventPy { inner }).collect() } + #[getter] fn interval(&self) -> i64 { self.inner.interval } + #[getter] fn timeout(&self) -> i64 { self.inner.timeout } + #[getter] fn stop_loss(&self) -> i64 { self.inner.stop_loss } + #[getter] fn T0(&self) -> bool { self.inner.T0 } + #[getter] fn unique_signals(&self) -> Vec { self.inner.unique_signals() } + + fn __repr__(&self) -> String { + format!( + "Position(name={}, symbol={}, timeout={}, stop_loss={}BP, T0={}, interval={}s)", + self.inner.name, self.inner.symbol, self.inner.timeout, self.inner.stop_loss, self.inner.T0, self.inner.interval + ) + } +} +``` + +在 `register` 追加 `m.add_class::()?;`。 + +- [ ] **步骤 2:构建并验证可被 Python 子类化 + 加状态** + +运行: +```bash +cd /home/moscow/chanlun.rs/chanlun-py && maturin develop 2>&1 | tail -3 +cd /tmp && python -c " +from chanlun._chanlun import Signal, Factor, Event, Operate, Position as Base +class Pos(Base): + def __init__(self, **kw): + super().__init__(**kw) + self.pos = 0 # 子类加状态字段 + self.operates = [] + def update(self, s): self.pos = 1 +s = Signal(k1='14400', k2='D1MO3', k3='中枢', v2='三买') +e = Event(Operate.LO, [Factor(signals_all=[s])]) +p = Pos(symbol='btc', opens=[e], name='中枢') +assert p.name == '中枢' +assert p.unique_signals == ['14400_D1MO3_中枢_任意_三买_任意_0'] +p.update({}) +assert p.pos == 1 +assert p.operates == [] +print('Position 子类化 + 状态字段 OK') +" +``` +预期:打印 `Position 子类化 + 状态字段 OK`。若 `super().__init__(**kw)` 报错(PyO3 子类构造限制),改为 `super().__init__(symbol, opens, exits, interval, timeout, stop_loss, T0, name)` 位置传参并在计划任务 9 的子类里同步。 + +- [ ] **步骤 3:Commit** + +```bash +cd /home/moscow/chanlun.rs +git add chanlun-py/src/signal_py.rs +git commit -m "feat(signal-py): Position PyO3 绑定(可子类化)" +``` + +--- + +## 任务 9:Python 集成 — chan_external.py 切换 + +**文件:** +- 修改:`chanlun-py/chanlun/chan_external.py` + +- [ ] **步骤 1:替换原语类为 Rust 导入** + +在 `chan_external.py` 顶部 import 区加入(紧跟现有 import): + +```python +from chanlun._chanlun import ( + Signal, + Factor, + Event, + Operate, + Position as _PositionBase, +) +``` + +删除文件中原有的 `class Operate(Enum)`、`@dataclass class Signal`、`@dataclass class Factor`、`@dataclass class Event` 四处定义(即任务 0 设计文档 §7 列出的类)。`Signal.load`/`Factor.load`/`Event.load` 等 classmethod 若调用方有用到,保留为模块级 helper(见步骤 2)。 + +- [ ] **步骤 2:Position 改为子类** + +把原 `class Position:` 改为继承 Rust 基类,仅保留状态字段 + `update`/`pairs`/`get_signals_config`/`with_data` 版 `dump`: + +```python +class Position(_PositionBase): + def __init__(self, symbol, opens, exits=[], interval=0, timeout=1000, + stop_loss=1000, T0=False, name=None): + if not name: + raise ValueError("name 是必须的参数") + super().__init__(symbol, opens, exits, interval, timeout, stop_loss, T0, name) + self.pos_changed = False + self.operates = [] + self.holds = [] + self.pos = 0 + self.last_event = {"dt": None, "bid": None, "price": None, "op": None, "op_desc": None} + self.last_lo_dt = None + self.last_so_dt = None + self.end_dt = None + + # 以下方法在当前 Position 类中已存在,转为子类时原样保留其方法体不变: + # - pairs (property) + # - update(self, s) + # - get_signals_config(self, signals_module="") + # - dump(self, with_data=False) + # - load (classmethod) + # 即:把 `class Position:` 改成 `class Position(_PositionBase):`, + # __init__ 换成上面的版本(super().__init__ + 状态字段), + # 其余方法定义整段保留。 +``` + +> 注:原 `Position.dump()` 的 config 部分由 Rust 基类不提供 dump 方法,故 Python 子类继续实现完整 `dump(with_data=False)`(config 字段从 `self.symbol`/`self.opens` 等 getter 读,state 字段从子类读)。`Position.load` classmethod 保留在子类。 + +- [ ] **步骤 3:同步根目录 chan.py** + +对根目录 `chan.py` 中合并进来的 `Operate`/`Signal`/`Factor`/`Event`/`Position` 做同样替换(import Rust 版 + Position 子类),保持与包版本一致。 + +- [ ] **步骤 4:冒烟回归** + +运行: +```bash +cd /home/moscow/chanlun.rs/chanlun-py && maturin develop 2>&1 | tail -2 +cd /home/moscow/chanlun.rs && python -c " +import chanlun.chan_external as cet +s = cet.Signal(k1='14400', k2='D1MO3', k3='中枢段DEA穿越2V230602', v2='三买') +e = cet.Event(cet.Operate.LO, [cet.Factor(signals_all=[s])]) +p = cet.Position(symbol='btc', opens=[e], name='中枢') +assert p.pos == 0 +print('chan_external 集成 OK') +" +``` +预期:打印 `chan_external 集成 OK`。 + +- [ ] **步骤 5:Commit** + +```bash +cd /home/moscow/chanlun.rs +git add chanlun-py/chanlun/chan_external.py chan.py +git commit -m "refactor(signal): chan_external 切换 Rust 原语 + Position 子类化" +``` + +--- + +## 任务 10:跨语言一致性测试 + 回归 + +**文件:** +- 创建:`chanlun-py/tests/test_signal_primitives.py` + +- [ ] **步骤 1:编写一致性测试** + +创建 `chanlun-py/tests/test_signal_primitives.py`: + +```python +"""信号原语 Rust 移植后的跨语言一致性与边界行为测试。""" +import pytest +from chanlun._chanlun import Signal, Factor, Event, Operate, Position + + +def test_signal_parse_and_props(): + s = Signal("14400_D1MO3_中枢_中枢段DEA穿越2_三买_偏移0_100") + assert s.k1 == "14400" and s.k3 == "中枢" and s.v2 == "三买" and s.score == 100 + assert s.key == "14400_D1MO3_中枢" + assert s.value == "中枢段DEA穿越2_三买_偏移0_100" + assert repr(s) == "Signal('14400_D1MO3_中枢_中枢段DEA穿越2_三买_偏移0_100')" + + +def test_signal_score_out_of_range(): + with pytest.raises(ValueError): + Signal(k1="a", k2="b", k3="c", score=101) + + +def test_signal_is_match_missing_key_raises(): + s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买") + with pytest.raises(ValueError): + s.is_match({}) + + +def test_signal_is_match_non_str_value_false(): + s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买") + assert s.is_match({"14400_D1MO3_中枢": 123}) is False + + +def test_signal_is_match_hit(): + s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买") + assert s.is_match({"14400_D1MO3_中枢": "x_三买_y_100"}) is True + + +def test_factor_empty_all_raises(): + with pytest.raises(ValueError): + Factor(signals_all=[]) + + +def test_factor_name_deterministic(): + s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买") + f1 = Factor(signals_all=[s]) + f2 = Factor(signals_all=[Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")]) + assert f1.name == f2.name + + +def test_event_empty_factors_raises(): + with pytest.raises(ValueError): + Event(Operate.LO, []) + + +def test_event_match_returns_factor_name(): + s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买") + e = Event(Operate.LO, [Factor(signals_all=[s])]) + ok, name = e.is_match({"14400_D1MO3_中枢": "x_三买_y_100"}) + assert ok and name + + +def test_position_requires_name(): + s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买") + e = Event(Operate.LO, [Factor(signals_all=[s])]) + with pytest.raises((ValueError, TypeError)): + Position(symbol="btc", opens=[e]) + + +def test_position_multi_factor_event_or(): + """复刻策略里的多 Factor OR 用法。""" + base = "14400" + 买 = [ + Factor(signals_all=[Signal(k1=base, k2="D1MO3", k3=k3, v2="三买")]) + for k3 in ["中枢段DEA穿越2V230602", "DEA穿越0轴V230602", "首次穿越0轴V230602"] + ] + e = Event(Operate.LO, 买) + # 只命中第三个 k3 + d = {"14400_D1MO3_首次穿越0轴V230602": "首次穿越0轴_三买_偏移0_100"} + # 另两个 k3 缺键 → is_match 抛 ValueError(与 Python 行为一致,由策略 try/except 兜底) + with pytest.raises(ValueError): + e.is_match(d) +``` + +- [ ] **步骤 2:运行测试** + +运行:`cd /home/moscow/chanlun.rs/chanlun-py && python -m pytest tests/test_signal_primitives.py -v` +预期:全部 PASS。 + +> 注:`test_position_multi_factor_event_or` 验证「缺键 raise」契约——策略中 `pos.update` 外层有 `try/except ValueError`,这里直接断言 raise 行为,确认 Rust 与 Python 语义一致。 + +- [ ] **步骤 3:回归 — 信号识别 + sync 回测** + +运行: +```bash +cd /home/moscow/chanlun.rs && python -c " +import chan as root_chan +from chanlun.chan import 缠论配置 +配置 = 缠论配置(买卖点偏移=5) +配置.加载文件路径 = 'templates/btcusd-14400-1753171200-1781956800.nb' +魔法 = root_chan.测试_信号识别(配置) +魔法() +" 2>&1 | grep -c "📡" +``` +预期:信号行数 > 0,且包含多种信号类型(与移植前一致)。 + +- [ ] **步骤 4:Commit** + +```bash +cd /home/moscow/chanlun.rs +git add chanlun-py/tests/test_signal_primitives.py +git commit -m "test(signal): 跨语言一致性 + 缺键契约 + 回归" +``` + +--- + +## 自检结论 + +- **规格覆盖**:§5 五个组件 → 任务 1-5(核心)+ 6-8(绑定);§6 三关键点 → 任务 2(is_match 三态)/任务 6(字典转核心 + 缺键 raise)/任务 3-4(确定性命名);§7 drop-in → 任务 9;§8 测试 → 任务 1-5 cargo 单测 + 任务 10 pytest + 回归。全覆盖。 +- **类型一致**:核心 `Signal`/`Factor`/`Event`/`Position`/`Operate` 与绑定 `SignalPy`/`FactorPy`/`EventPy`/`PositionPy`/`OperatePy` 命名贯穿;`匹配值`/`信号字典`/`缺键错误`/`sha256前4` 在 mod.rs 定义,各处引用一致。 +- **占位符**:无 TODO/待定;每个代码步骤含完整可编译代码。 +- **已知风险**:任务 8 步骤 2 标注了 PyO3 子类构造的 fallback(位置传参)。 diff --git a/docs/superpowers/plans/2026-06-22-signal-registry-framework.md b/docs/superpowers/plans/2026-06-22-signal-registry-framework.md new file mode 100644 index 0000000..068468f --- /dev/null +++ b/docs/superpowers/plans/2026-06-22-signal-registry-framework.md @@ -0,0 +1,467 @@ +# 子项目1 信号注册框架 实现计划 + +> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 + +**目标:** 用 `#[signal]` proc-macro + `inventory` 编译期注册表替代 Python 的 `import_by_name` 动态导入和 `SignalsParser` docstring 解析,提供「信号名 → 函数指针」O(1) 查表。 + +**架构:** 新建独立 proc-macro crate `chanlun-signal-macros`(`#[signal(name, template)]` 属性宏,emit `crate::signal::registry::` 路径);核心 crate `chanlun` 新增 `signal/registry.rs`(描述符类型 + `inventory` 归并 + 查询 API),并依赖宏 crate + `inventory`。信号函数签名 `fn(&观察者, &HashMap) -> Vec`,无 TaCache(核心层 K线已挂指标)。 + +**技术栈:** Rust(edition 2024 / 宏 crate 2021)、`syn` 2 + `quote` + `proc-macro2`、`inventory` 0.3、`serde_json`。 + +**设计文档:** `docs/superpowers/specs/2026-06-22-signal-registry-framework-design.md` + +--- + +## 文件结构 + +| 文件 | 职责 | +|---|---| +| `chanlun-signal-macros/Cargo.toml` | proc-macro crate 清单(`proc-macro = true` + syn/quote/proc-macro2) | +| `chanlun-signal-macros/src/lib.rs` | `#[signal(name, template)]` 属性宏 | +| `chanlun/Cargo.toml` | 新增 `inventory` + path 依赖 `chanlun-signal-macros` | +| `chanlun/src/signal/registry.rs` | `SignalFn`/`SignalDescriptor`/`SignalMeta`/`归并`/`SIGNAL_REGISTRY`/查询 API + 探针单测 | +| `chanlun/src/signal/mod.rs` | 增 `pub mod registry;` | +| `chanlun/tests/test_signal_registry.rs` | 端到端集成测试:`#[signal]` 贴探针函数 → 注册表命中(在 chanlun crate 内,因宏 emit `crate::` 路径) | + +**测试归属说明**:`#[signal]` 宏 emit `crate::signal::registry::SignalDescriptor`,仅在 `chanlun` crate 内解析得了,故**宏的端到端测试放 `chanlun/tests/`,不放宏 crate**(放宏 crate 会循环依赖 chanlun)。宏 crate 自身只验证「能编译」。 + +--- + +## 任务 0:脚手架——proc-macro crate + 依赖接线 + +**文件:** +- 创建:`chanlun-signal-macros/Cargo.toml`、`chanlun-signal-macros/src/lib.rs` +- 修改:`chanlun/Cargo.toml` + +- [ ] **步骤 1:创建宏 crate 清单** + +创建 `chanlun-signal-macros/Cargo.toml`: + +```toml +[package] +name = "chanlun-signal-macros" +version = "0.1.0" +edition = "2021" +license = "MIT" +description = "chanlun 信号注册 proc-macro(#[signal])" + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2", features = ["full"] } +quote = "1" +proc-macro2 = "1" +``` + +- [ ] **步骤 2:创建宏 crate 占位实现** + +创建 `chanlun-signal-macros/src/lib.rs`(占位,任务 2 填充真实逻辑): + +```rust +//! chanlun 信号注册 proc-macro。 +//! +//! 第三方代码声明:`#[signal]` 注册机制参考 czsc 项目 +//! (https://github.com/waditu/czsc,Apache License 2.0),已简化适配。 + +use proc_macro::TokenStream; + +/// 占位——任务 2 实现真实的 #[signal] 属性宏。 +#[proc_macro_attribute] +pub fn signal(_attr: TokenStream, item: TokenStream) -> TokenStream { + item +} +``` + +- [ ] **步骤 3:chanlun 接线依赖** + +修改 `chanlun/Cargo.toml` 的 `[dependencies]`,追加两行(放在 `sha2 = "0.10"` 之后): + +```toml +inventory = "0.3" +chanlun-signal-macros = { path = "../chanlun-signal-macros" } +``` + +- [ ] **步骤 4:验证两个 crate 都能构建** + +运行:`cd /home/moscow/chanlun.rs/chanlun-signal-macros && cargo build` +预期:编译通过(占位宏)。 + +运行:`cd /home/moscow/chanlun.rs/chanlun && cargo build` +预期:编译通过(新增依赖,尚未使用,unused-dep 不会报错)。 + +- [ ] **步骤 5:Commit** + +```bash +cd /home/moscow/chanlun.rs +git add chanlun-signal-macros chanlun/Cargo.toml +git commit -m "feat(signal-registry): 脚手架 — proc-macro crate + inventory 依赖" +``` + +--- + +## 任务 1:registry.rs —— 描述符类型 + 归并 + 查询 API + +**文件:** +- 创建:`chanlun/src/signal/registry.rs` +- 修改:`chanlun/src/signal/mod.rs` + +- [ ] **步骤 1:mod.rs 注册子模块** + +修改 `chanlun/src/signal/mod.rs`,在 `pub mod signal;`(第 13 行)之后加一行: + +```rust +pub mod registry; +``` + +- [ ] **步骤 2:编写 registry.rs(含 cargo 单测)** + +创建 `chanlun/src/signal/registry.rs`(一字不差): + +```rust +//! 信号注册表 —— 编译期收集 `#[signal]` 注册的信号函数,运行时按名查表。 +//! +//! 第三方代码声明:注册机制参考 czsc(https://github.com/waditu/czsc, +//! Apache License 2.0),已简化适配(无 category / TaCache)。 + +use crate::business::observer::观察者; +use crate::signal::Signal; +use serde_json::Value; +use std::collections::HashMap; +use std::sync::LazyLock; + +/// 信号函数签名 —— 读观察者状态(含 K线已挂指标)+ 参数 → 信号列表。无 TaCache。 +pub type SignalFn = fn(&观察者, &HashMap) -> Vec; + +/// 信号描述符(编译期元数据,由 `#[signal]` 宏生成、`inventory` 收集)。 +#[derive(Clone, Copy)] +pub struct SignalDescriptor { + /// 信号函数名,如 "youwukuncheng_中枢第三买卖点_V230602" + pub name: &'static str, + /// 参数模板,如 "{freq}_D1MO{max_overlap}_中枢第三买卖点V230602" + pub template: &'static str, + /// 函数指针 + pub func: SignalFn, +} + +inventory::collect!(SignalDescriptor); + +/// 运行时信号元信息。 +pub struct SignalMeta { + pub func: SignalFn, + pub template: &'static str, +} + +/// 归并描述符为注册表;重名返回 Err(纯函数,便于单测)。 +fn 归并( + descs: impl Iterator, +) -> Result, String> { + let mut m: HashMap<&'static str, SignalMeta> = HashMap::new(); + for d in descs { + if m + .insert(d.name, SignalMeta { func: d.func, template: d.template }) + .is_some() + { + return Err(format!("信号重名:{}", d.name)); + } + } + Ok(m) +} + +/// 全局注册表视图(由 inventory 归并;重名 panic,fail-fast)。 +pub static SIGNAL_REGISTRY: LazyLock> = LazyLock::new(|| { + 归并(inventory::iter::.into_iter().copied()) + .unwrap_or_else(|e| panic!("{e}")) +}); + +/// 按名查信号元信息。 +pub fn get_signal(name: &str) -> Option<&'static SignalMeta> { + SIGNAL_REGISTRY.get(name) +} + +/// 按名查参数模板。 +pub fn get_template(name: &str) -> Option<&'static str> { + SIGNAL_REGISTRY.get(name).map(|m| m.template) +} + +/// 列出所有已注册信号名(排序)。 +pub fn list_signal_names() -> Vec<&'static str> { + let mut v: Vec<_> = SIGNAL_REGISTRY.keys().copied().collect(); + v.sort(); + v +} + +#[cfg(test)] +mod tests { + use super::*; + + /// 探针信号函数(最小签名实现,仅供测试归并/查表)。 + fn __probe(_obs: &观察者, _p: &HashMap) -> Vec { + Vec::new() + } + + fn 描述符(name: &'static str) -> SignalDescriptor { + SignalDescriptor { name, template: "{freq}_D1_probe", func: __probe } + } + + #[test] + fn test_归并_正常() { + let m = 归并([描述符("a_V000001"), 描述符("b_V000001")].into_iter()).unwrap(); + assert_eq!(m.len(), 2); + assert!(m.contains_key("a_V000001")); + assert_eq!(m["a_V000001"].template, "{freq}_D1_probe"); + } + + #[test] + fn test_归并_重名_返回Err() { + let r = 归并([描述符("dup_V000001"), 描述符("dup_V000001")].into_iter()); + assert!(r.is_err()); + assert!(r.unwrap_err().contains("信号重名")); + } +} + +/// 测试用:通过 inventory 提交一个探针描述符,验证全局注册表能收到。 +#[cfg(test)] +fn __probe_for_inventory(_obs: &观察者, _p: &HashMap) -> Vec { + Vec::new() +} + +#[cfg(test)] +inventory::submit! { + SignalDescriptor { + name: "__probe_inventory_V000000", + template: "{freq}_D1_probe_inventory", + func: __probe_for_inventory as SignalFn, + } +} + +#[cfg(test)] +mod inventory_tests { + use super::*; + + #[test] + fn test_全局注册表收到inventory探针() { + assert!(get_signal("__probe_inventory_V000000").is_some()); + assert_eq!( + get_template("__probe_inventory_V000000"), + Some("{freq}_D1_probe_inventory") + ); + assert!(list_signal_names().contains(&"__probe_inventory_V000000")); + } +} +``` + +- [ ] **步骤 3:运行测试** + +运行:`cd /home/moscow/chanlun.rs/chanlun && cargo test signal::registry` +预期:3 个测试全 PASS(`test_归并_正常`、`test_归并_重名_返回Err`、`test_全局注册表收到inventory探针`)。 + +> 注:若 `inventory::iter::.into_iter().copied()` 因 inventory 0.3 API 细节编译报错,改为 `inventory::iter::().copied()` 或 `inventory::iter:: {}`(参考 `/home/moscow/czsc/crates/czsc-signals/src/registry.rs:136` 的 `inventory::iter::<...>.into_iter().copied().collect()` 写法)。 + +- [ ] **步骤 4:Commit** + +```bash +cd /home/moscow/chanlun.rs +git add chanlun/src/signal/registry.rs chanlun/src/signal/mod.rs +git commit -m "feat(signal-registry): registry.rs — 描述符/归并/查询 API + 探针测试" +``` + +--- + +## 任务 2:`#[signal]` 属性宏 + +**文件:** +- 修改:`chanlun-signal-macros/src/lib.rs` + +- [ ] **步骤 1:实现 #[signal] 宏** + +把 `chanlun-signal-macros/src/lib.rs` 全部内容替换为(一字不差): + +```rust +//! chanlun 信号注册 proc-macro。 +//! +//! 第三方代码声明:`#[signal]` 注册机制参考 czsc 项目 +//! (https://github.com/waditu/czsc,Apache License 2.0),已简化适配 +//! (无 category / TaCache,签名固定为 fn(&观察者, &HashMap) -> Vec)。 + +use proc_macro::TokenStream; +use quote::quote; +use syn::parse::Parser; +use syn::punctuated::Punctuated; +use syn::{Expr, ExprLit, ItemFn, Lit, Meta, Token}; + +/// `#[signal(name = "foo_V230101", template = "{freq}_D1_foo")]` +/// +/// 校验:函数名含 `_V<数字>`;`name` 与函数名一致;`name`/`template` 非空。 +/// 生成:一个 `static` SignalDescriptor + `inventory::submit!`,路径用 `crate::signal::registry::`。 +#[proc_macro_attribute] +pub fn signal(attr: TokenStream, item: TokenStream) -> TokenStream { + let parser = Punctuated::::parse_terminated; + let metas = match parser.parse(attr) { + Ok(m) => m, + Err(e) => return e.to_compile_error().into(), + }; + + let mut name: Option = None; + let mut template: Option = None; + for m in metas { + if let Meta::NameValue(nv) = m + && let Some(ident) = nv.path.get_ident() + && let Expr::Lit(ExprLit { lit: Lit::Str(v), .. }) = nv.value + { + match ident.to_string().as_str() { + "name" => name = Some(v.value()), + "template" => template = Some(v.value()), + _ => {} + } + } + } + + let f: ItemFn = match syn::parse(item) { + Ok(v) => v, + Err(e) => return e.to_compile_error().into(), + }; + + let name = name.unwrap_or_default(); + let template = template.unwrap_or_default(); + let fn_ident = &f.sig.ident; + let fn_name = fn_ident.to_string(); + + let mut errors = Vec::new(); + if name.is_empty() || template.is_empty() { + errors.push(quote! { compile_error!("#[signal] name/template 不能为空"); }); + } + if name != fn_name { + errors.push(quote! { compile_error!("#[signal] name 必须与函数名一致"); }); + } + // 函数名须含 _V<数字> + let 有版本 = fn_name + .rsplit_once("_V") + .map(|(_, v)| !v.is_empty() && v.chars().all(|c| c.is_ascii_digit())) + .unwrap_or(false); + if !有版本 { + errors.push(quote! { compile_error!("#[signal] 函数名必须含 _V<版本号>,如 foo_V230101"); }); + } + + if !errors.is_empty() { + let errs = errors.into_iter(); + return quote! { #(#errs)* }.into(); + } + + let descriptor_ident = syn::Ident::new( + &format!("__SIG_DESC_{}", fn_name).to_uppercase(), + fn_ident.span(), + ); + + let expanded = quote! { + #f + + #[allow(non_upper_case_globals)] + static #descriptor_ident: crate::signal::registry::SignalDescriptor = + crate::signal::registry::SignalDescriptor { + name: #name, + template: #template, + func: #fn_ident as crate::signal::registry::SignalFn, + }; + + inventory::submit! { #descriptor_ident } + }; + expanded.into() +} +``` + +- [ ] **步骤 2:验证宏 crate 编译** + +运行:`cd /home/moscow/chanlun.rs/chanlun-signal-macros && cargo build` +预期:编译通过。 + +- [ ] **步骤 3:Commit** + +```bash +cd /home/moscow/chanlun.rs +git add chanlun-signal-macros/src/lib.rs +git commit -m "feat(signal-registry): #[signal] 属性宏 — 校验+生成描述符+提交" +``` + +--- + +## 任务 3:端到端集成测试(chanlun 内用 #[signal]) + +**文件:** +- 创建:`chanlun/tests/test_signal_registry.rs` + +- [ ] **步骤 1:编写集成测试** + +创建 `chanlun/tests/test_signal_registry.rs`(一字不差)。它在 chanlun crate 内用 `#[signal]` 贴一个探针函数,验证宏 + 注册表端到端: + +```rust +//! 端到端:#[signal] 宏 + inventory 注册表协同。 +//! 放在 chanlun crate 内,因 #[signal] emit 的是 `crate::signal::registry::` 路径。 + +use std::collections::HashMap; + +use chanlun::business::observer::观察者; +use chanlun::signal::registry::{get_signal, get_template, list_signal_names}; +use chanlun::signal::Signal; +use chanlun_signal_macros::signal; +use serde_json::Value; + +/// 探针信号函数:贴 #[signal] 后应被自动注册。 +#[signal( + name = "test_probe_signal_V230101", + template = "{freq}_D1MO{max_overlap}_test_probe_signalV230101" +)] +fn test_probe_signal_V230101(_obs: &观察者, _params: &HashMap) -> Vec { + Vec::new() +} + +#[test] +fn test_signal_宏自动注册到全局表() { + // get_signal 命中 + assert!( + get_signal("test_probe_signal_V230101").is_some(), + "#[signal] 应把探针函数注册进 SIGNAL_REGISTRY" + ); + // 模板正确 + assert_eq!( + get_template("test_probe_signal_V230101"), + Some("{freq}_D1MO{max_overlap}_test_probe_signalV230101") + ); + // 列表含它 + assert!(list_signal_names().contains(&"test_probe_signal_V230101")); +} + +#[test] +fn test_未注册信号返回None() { + assert!(get_signal("不存在的信号_V999999").is_none()); +} +``` + +- [ ] **步骤 2:运行集成测试** + +运行:`cd /home/moscow/chanlun.rs/chanlun && cargo test --test test_signal_registry` +预期:2 个测试全 PASS。 + +> 注:本测试与 registry.rs 的 `#[cfg(test)]` inventory 探针不冲突——集成测试是独立编译单元,`__probe_inventory_V000000` 仅在 lib 单测时提交,集成测试时只有 `test_probe_signal_V230101`。 + +- [ ] **步骤 3:跑全量 signal 测试确认无回归** + +运行:`cd /home/moscow/chanlun.rs/chanlun && cargo test signal` +预期:原 23 个原语单测 + registry 3 个 + 集成 2 个,全 PASS。 + +- [ ] **步骤 4:Commit** + +```bash +cd /home/moscow/chanlun.rs +git add chanlun/tests/test_signal_registry.rs +git commit -m "test(signal-registry): 端到端——#[signal] 宏自动注册 + 查表" +``` + +--- + +## 自检结论 + +- **规格覆盖**:设计 §4 crate 结构 → 任务 0;§5 描述符/注册表/查询 API → 任务 1;§6 `#[signal]` 宏 → 任务 2;§7 测试(归并重名/inventory 探针/宏端到端)→ 任务 1(单测)+ 任务 3(集成);§9 错误处理(编译期 compile_error、启动期重名 panic、运行期 None)→ 任务 2(compile_error)+ 任务 1(归并 Err→panic / get_signal None)。全覆盖。 +- **类型一致**:`SignalFn`/`SignalDescriptor`/`SignalMeta`/`归并`/`get_signal`/`get_template`/`list_signal_names` 在 registry.rs 定义,任务 2 宏 emit `crate::signal::registry::{SignalDescriptor, SignalFn}`、任务 3 集成测试 import `chanlun::signal::registry::{get_signal, get_template, list_signal_names}`,命名贯穿一致。 +- **占位符**:任务 0 步骤 2 的占位宏是**有意的脚手架**(任务 2 替换为真实实现),非计划缺陷;其余步骤均含完整可编译代码。 +- **风险提示**:任务 1 步骤 3 标注了 `inventory::iter` API 细节的 fallback(参考 czsc registry.rs 实际写法)。 diff --git a/docs/superpowers/plans/2026-06-23-position-update-state-machine.md b/docs/superpowers/plans/2026-06-23-position-update-state-machine.md new file mode 100644 index 0000000..26d578e --- /dev/null +++ b/docs/superpowers/plans/2026-06-23-position-update-state-machine.md @@ -0,0 +1,79 @@ +# 子项目 4 Position.update 状态机迁移到 Rust 实现计划 + +> 目标:将 Position.update 状态机(~135 行 Python)从 Python 子类迁移到 Rust 核心。 + +**设计文档:** `docs/superpowers/specs/2026-06-23-position-update-state-machine-design.md` + +--- + +## 任务 0:扩展 Rust 核心 Position + +**文件:** `chanlun/src/signal/position.rs` + +- [x] 新增类型:`操作记录`、`持仓记录`、`开平配对`、`最近事件` +- [x] Position 结构体新增 7 个状态字段(pos, pos_changed, operates, holds, last_event, last_lo_dt, last_so_dt, end_dt) +- [x] `新建()` 构造函数适配(状态字段初始化为默认值) +- [x] 实现 `push_operate()` 内部辅助方法 +- [x] 实现 `update(&mut self, dt, price, bid, signals) -> Result<(), 缺键错误>` — 核心状态机 +- [x] 实现 `pairs() -> Vec<开平配对>` — 开平配对计算 +- [x] 实现 `dump_config()` / `load_config()` — 序列化辅助 +- [x] 内部辅助函数:`同一交易日`、`间隔检查`、`允许操作` +- [x] Rust 单元测试(28 用例) + +## 任务 1:更新 PyO3 绑定 + +**文件:** `chanlun-py/src/signal_py.rs` + +- [x] 新增 helper:`核心op转pyop()`、`时间戳转datetime()` +- [x] 新增状态 getter:`pos`, `pos_changed`, `operates`, `holds`, `pairs` +- [x] 实现 `update(PyDict)` — 提取 dt/close/bid + 转换 信号字典 + 调用核心 +- [x] dt 类型兼容:支持 datetime / int / float +- [x] dump(with_data) — 支持附带 pairs/holds +- [x] load() 静态方法 +- [x] 新增 `取事件列表` 辅助函数 +- [x] 更新 `__repr__` 包含 pos + +## 任务 2:更新 Python 子类 + +**文件:** `chanlun-py/chanlun/chan_external.py` + +- [x] `__init__` 简化为 `pass`(状态由 Rust 初始化) +- [x] 删除 `update()`(Rust 提供) +- [x] 删除 `pairs` property(Rust 提供) +- [x] `dump()` 委托给 Rust `super().dump(with_data=...)` +- [x] `load()` 使用 `cls(...)` 构造(保持子类类型) +- [x] 保留 `get_signals_config()` + +## 任务 3:测试 + +**文件:** +- `chanlun/src/signal/position.rs` — Rust 单元测试(28 用例) +- `chanlun-py/tests/test_position_update.py` — Python 集成测试(24 用例) +- `chanlun-py/tests/test_signal_primitives.py` — 已有测试更新(4 position 用例) + +- [x] 基础开多/开空/平多/平空 +- [x] 间隔限制 +- [x] 止损(多头/空头) +- [x] 超时 +- [x] 时间倒退容错 +- [x] 空事件列表容错 +- [x] 无匹配事件容错 +- [x] 缺键错误 +- [x] T0 模式 +- [x] pairs 盈亏计算(多头/空头) +- [x] pairs 持仓天数 +- [x] dump/load with/without data +- [x] dt 类型兼容(datetime / int / float) + +## 任务 4:文档 + +- [x] 创建设计文档 `docs/superpowers/specs/2026-06-23-position-update-state-machine-design.md` +- [x] 创建实现计划 `docs/superpowers/plans/2026-06-23-position-update-state-machine.md` +- [x] 更新 `CLAUDE.md` 子项目表 + +## 自检结论 + +- **规格覆盖**:设计 §3 新增类型 → 任务 0;§4 update 算法 → 任务 0;§5 文件结构 → 任务 0-3 +- **类型一致**:`update()` 参数使用已有 `信号字典` 类型;`Operate` 枚举已有 Rust 版 +- **向后兼容**:Python 子类保留;update/pairs/operates/holds API 不变;dt 支持三种输入格式 +- **测试覆盖**:Rust 28 用例 + Python 24 用例 + 已有 4 用例更新 diff --git a/docs/superpowers/plans/2026-06-23-signal-calculator-migration.md b/docs/superpowers/plans/2026-06-23-signal-calculator-migration.md new file mode 100644 index 0000000..dc1e40a --- /dev/null +++ b/docs/superpowers/plans/2026-06-23-signal-calculator-migration.md @@ -0,0 +1,678 @@ +# 信号计算器 Rust 迁移 — 设计决策 + 实现计划 + +> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 + +**目标:** 将 `信号计算器`(Python 信号编排器)替换为混合架构:Rust `SignalEngine` 为主,Python fallback 为辅,逐步完成最终迁移。 + +**架构:** 增强 Rust `SignalEngine` 使其返回完整的 `信号字典`(信号 + OHLCV 行情);创建 `SignalOrchestrator` 支持 Rust 注册表优先 + Python `import_by_name` 回退;`SignalsParser` 暂留 Python。 + +**技术栈:** Rust edition 2024、PyO3 0.28、`serde_json::Value`、`parking_lot::RwLock`、`inventory`。 + +**设计文档:** `docs/superpowers/specs/2026-06-23-signal-calculator-migration-design.md` + +--- + +## 0. 决策分析 + +### 现状 + +| 组件 | 语言 | 职责 | +|------|------|------| +| `SignalEngine` | ✅ Rust | 按名查找已注册信号函数 → 执行 → 合并结果 | +| `信号计算器` | Python | 同上 + OHLCV 行情提取 + `SignalsParser` 集成 | +| `SignalsParser` | Python | 解析信号函数文档字符串 → 生成配置字典 | +| `get_signals_config` | Python | 将信号字符串列表 → 配置字典列表(用 `SignalsParser`) | + +两个计算引擎**并行存在**,完全独立。`strategies.py` 使用 Python `信号计算器`。Rust `SignalEngine` 没有被任何生产代码使用。 + +### 关键差异 + +| 能力 | Python `信号计算器` | Rust `SignalEngine` | +|------|---------------------|---------------------| +| 信号函数解析 | 运行时 `import_by_name()` | 编译时 `#[signal]` + `inventory` | +| OHLCV 行情 | 提取到 `self.行情` | ❌ 不处理 | +| 观察者访问 | 预提取 `{freq: Observer}` 字典 | 每次调用时通过 `&立体分析器` 查找 | +| 错误处理 | 每个信号函数的 `except Exception` | `tracing::warn!`,继续 | +| freq 验证 | 检查是否在分析器周期组中 | ❌ 不验证 | +| 信号字符串→配置 | `从信号列表提取配置()` | ❌ 不存在(Python `SignalsParser` 处理) | + +### 建议:混合迁移(3 阶段) + +**阶段 A:增强 Rust SignalEngine。** 添加 OHLCV 行情提取 + freq 验证 + Python `call_signal` 集成。 + +**阶段 B:创建混合编排器 `SignalOrchestrator`。** 替代 Python `信号计算器`;Rust 注册表优先,Python `import_by_name` 回退。 + +**阶段 C:废弃 Python 并行路径。** 所有信号函数移植到 Rust 后,移除 `import_by_name` 回退和 `SignalsParser`。 + +| 阶段 | 交付物 | 向后兼容 | +|------|--------|----------| +| A | `SignalEngine::更新_完整()` → `{signals, market_data}` | ✅ 不影响现有路径 | +| B | `SignalOrchestrator`(Rust 优先 + Python fallback) | ✅ `strategies.py` 切换到新类 | +| C | 移除 Python `信号计算器` 和 `SignalsParser` | ⚠️ 需所有信号函数先移植到 Rust | + +--- + +## 文件结构 + +``` +chanlun/src/signal/engine.rs ← 增强:更新_完整() 返回 {signals, market} +chanlun-py/src/signal_engine_py.rs ← 增强:SignalEnginePy 暴露 更新_完整() +chanlun-py/chanlun/signal_orchestrator.py ← 新建:混合编排器 +chanlun-py/chanlun/chan_external.py ← 废弃:信号计算器(最终移除) +strategies.py ← 切换:使用 SignalOrchestrator +main.py ← 修复:损坏的 信号计算器 调用点 +chanlun-py/tests/test_signal_orchestrator.py ← 新建:编排器测试 +``` + +--- + +## 阶段 A:增强 Rust SignalEngine(信号 + 行情) + +### 任务 A1:SignalEngine 增加 `更新_完整()` 方法 + +**文件:** `chanlun/src/signal/engine.rs` + +- [ ] **步骤 1:添加返回类型** + +在 `SignalEngine` 的 `更新_含分数()` 之后添加新结构体: + +```rust +/// 完整更新结果:信号字典 + 基础周期行情数据。 +#[derive(Debug, Clone)] +pub struct 完整更新结果 { + /// 信号 key → value 映射 + pub signals: HashMap, + /// 基础周期最后一根 K 线的 OHLCV 数据 + pub market: Option, +} + +#[derive(Debug, Clone)] +pub struct MarketData { + pub symbol: String, + pub dt: i64, // Unix 秒 + pub id: i64, + pub open: f64, + pub high: f64, + pub low: f64, + pub close: f64, + pub vol: f64, +} +``` + +- [ ] **步骤 2:实现 `更新_完整()`** + +```rust +/// 运行信号计算并附带基础周期行情。 +/// `base_freq` 为分析器的第一个周期(最小周期)。 +pub fn 更新_完整(&self, analyzer: &立体分析器) -> 完整更新结果 { + let signals = self.更新(analyzer); + let base_freq = analyzer.周期组.first().copied().unwrap_or(0); + let market = analyzer._单体分析器.get(&base_freq).and_then(|obs| { + let obs = obs.read(); + obs.普通K线序列.last().map(|k| { + MarketData { + symbol: obs.符号.clone(), + dt: k.时间戳, + id: k.序号.load(std::sync::atomic::Ordering::Relaxed), + open: k.开盘价, + high: k.最高价, + low: k.最低价, + close: k.收盘价, + vol: k.成交量, + } + }) + }); + 完整更新结果 { signals, market } +} +``` + +- [ ] **步骤 3:构建验证** + +```bash +cd chanlun && cargo build +``` +预期:编译通过。 + +- [ ] **步骤 4:Commit** + +```bash +git add chanlun/src/signal/engine.rs +git commit -m "feat(signal): SignalEngine.更新_完整() — 信号 + 基础周期行情 + +Co-Authored-By: Claude " +``` + +--- + +### 任务 A2:PyO3 绑定增强 + +**文件:** `chanlun-py/src/signal_engine_py.rs` + +- [ ] **步骤 1:暴露 `更新_完整()`** + +在 `SignalEnginePy` 的 `#[pymethods]` 块中添加: + +```rust +/// 更新信号并返回完整结果(信号 + 行情)。 +/// 返回 dict: {"signals": {...}, "market": {...}} +fn 更新_完整<'py>(&self, py: Python<'py>, analyzer: &立体分析器Py) -> PyResult> { + let result = self.inner.更新_完整(&analyzer.inner); + let d = PyDict::new(py); + // signals + let signals_dict = PyDict::new(py); + for (k, v) in &result.signals { + signals_dict.set_item(k, v)?; + } + d.set_item("signals", signals_dict)?; + // market + if let Some(m) = &result.market { + let md = PyDict::new(py); + md.set_item("symbol", &m.symbol)?; + // Convert i64 to Python datetime + let dt = 时间戳转datetime(py, m.dt)?; + md.set_item("dt", dt)?; + md.set_item("id", m.id)?; + md.set_item("open", m.open)?; + md.set_item("high", m.high)?; + md.set_item("low", m.low)?; + md.set_item("close", m.close)?; + md.set_item("vol", m.vol)?; + d.set_item("market", md)?; + } else { + d.set_item("market", py.None())?; + } + Ok(d) +} +``` + +> 注意:`时间戳转datetime` 已在 `signal_py.rs` 中定义。需要将其改为 `pub(crate)` 可见性,或在 `signal_engine_py.rs` 中重复定义。 + +- [ ] **步骤 2:将 `时间戳转datetime` 改为 `pub(crate)`** + +在 `signal_py.rs` 中: +```rust +// 将 fn 改为 pub(crate) +pub(crate) fn 时间戳转datetime(py: Python<'_>, ts: i64) -> PyResult> { +``` + +- [ ] **步骤 3:添加 `freq 验证` 辅助函数** + +在 `signal_engine_py.rs` 的 `SignalEnginePy::new()` 中添加 freq 验证(匹配 Python `信号计算器` setter 的行为): + +```rust +// 在 new() 中,转换配置后: +// 验证所有 freq 已由调用方提供(不在构造时验证——没有分析器引用) +// 频率验证推迟到 更新() 调用时(与 Rust 核心行为一致) +``` + +不改变构造函数——保持最小侵入。频率验证由调用方负责(`SignalOrchestrator`)。 + +- [ ] **步骤 4:构建验证** + +```bash +cd chanlun-py && cargo build +``` +预期:编译通过。 + +- [ ] **步骤 5:Commit** + +```bash +git add chanlun-py/src/signal_engine_py.rs chanlun-py/src/signal_py.rs +git commit -m "feat(signal-py): SignalEnginePy.更新_完整() + 时间戳转datetime 公开 + +Co-Authored-By: Claude " +``` + +--- + +## 阶段 B:混合编排器 SignalOrchestrator + +### 任务 B1:创建 `signal_orchestrator.py` + +**文件:** 创建 `chanlun-py/chanlun/signal_orchestrator.py` + +这是核心新文件。编排器: +1. 构造时接受 `立体分析器` + 信号配置 + 信号模块 +2. 对每个配置,先尝试 Rust `call_signal()` 查找(通过 `list_signals()`) +3. 如果信号名在 Rust 注册表中:使用 `SignalEngine` 批量执行 +4. 如果不在:使用 Python `import_by_name` 回退 +5. 合并所有结果,附加 OHLCV 行情 + +- [ ] **步骤 1:创建文件框架** + +```python +"""信号编排器 — Rust 优先 + Python 回退的混合信号计算。 + +替代 chan_external.信号计算器,逐步迁移到全 Rust 路径。 + +使用方式:: + + 分析器 = 立体分析器("btcusd", [300, 900, 3600], 配置) + 编排器 = SignalOrchestrator(分析器, 信号配置=[...], 信号模块="chanlun.signals") + + for k in k线列表: + 分析器.投喂K线(k) + 编排器.更新() + print(编排器.信号字典) +""" + +import sys +from collections import OrderedDict +from typing import Any, Callable, Dict, List, Optional + +from loguru import logger + +from chanlun.chan import 观察者, 立体分析器 +from chanlun._chanlun import ( + SignalEngine as _RustSignalEngine, + call_signal as _rust_call_signal, + list_signals as _rust_list_signals, +) + + +class SignalOrchestrator: + """混合信号编排器:Rust 注册表优先,Python import_by_name 回退。""" + + def __init__( + self, + 分析器: 立体分析器, + 信号配置: Optional[List[Dict]] = None, + 信号模块: str = "chanlun.signals", + ): + self._分析器 = 分析器 + self._观察者字典 = {p: 分析器._单体分析器[p] for p in 分析器.周期组} + self._基础周期 = 分析器.周期组[0] + self._信号模块 = 信号模块 + + # 初始化 Rust 引擎(用于已注册的 Rust 信号) + self._rust_engine = _RustSignalEngine(信号配置=信号配置 or []) + self._rust_engine.自动挂载指标(分析器) + + # 分类配置:Rust 注册 vs Python 回退 + self._rust_configs: List[Dict] = [] + self._python_configs: List[Dict] = [] + self._python_func_cache: Dict[str, Callable] = {} + + # 结果容器 + self.信号: Dict[str, str] = {} + self.行情: Dict[str, Any] = {} + + # 初始设置 + self.信号配置 = 信号配置 or [] + + # ... 其余方法见下面步骤 +``` + +- [ ] **步骤 2:实现配置分类** + +```python + @property + def 信号配置(self) -> List[Dict]: + return self._信号配置 + + @信号配置.setter + def 信号配置(self, value: List[Dict]): + 可用周期 = set(self._分析器.周期组) + rust_names = set(_rust_list_signals()) + + self._rust_configs = [] + self._python_configs = [] + + for c in self._去重配置(value): + freq = c.get("freq") + if freq is not None: + 周期秒 = int(freq) + if 周期秒 not in 可用周期: + raise ValueError( + f"信号配置 freq={freq}({周期秒}s) 不在分析器周期组 {sorted(可用周期)} 中" + ) + + name = c.get("name", "") + if name in rust_names: + self._rust_configs.append(c) + else: + self._python_configs.append(c) + + self._信号配置 = value + self._预加载Python信号函数() +``` + +- [ ] **步骤 3:实现更新循环** + +```python + def 更新(self): + """执行所有信号计算。Rust 优先(批量),Python 回退(逐个)。""" + self.信号.clear() + self.行情.clear() + + # 1. Rust 批量执行 + if self._rust_configs: + result = self._rust_engine.更新_完整(self._分析器) + if result.get("signals"): + for k, v in result["signals"].items(): + if v != "任意_任意_任意_0": + self.信号[k] = v + if result.get("market"): + self.行情.update(result["market"]) + + # 2. Python 回退(逐个执行) + for config in self._python_configs: + try: + result = self._执行Python信号函数(config) + if result: + for k, v in result.items(): + if v != "任意_任意_任意_0": + self.信号[k] = v + except Exception: + logger.exception(f"Python 信号函数执行失败: {config.get('name')}") + + # 3. 补充基础周期行情(如果 Rust 引擎未提供) + if not self.行情: + self._提取行情() +``` + +- [ ] **步骤 4:实现 Python 信号函数执行(移植自 chan_external.py)** + +```python + def _执行Python信号函数(self, config: Dict) -> Optional[OrderedDict]: + """执行单个 Python 信号函数(移植自 信号计算器._执行信号函数)。""" + import traceback + param = dict(config) + sig_name = param.pop("name") + sig_func = self._python_func_cache.get(sig_name) or self._解析信号函数(sig_name) + if sig_func is None: + logger.warning(f"信号函数未找到: {sig_name}") + return None + + freq = param.pop("freq", None) + if freq is not None: + 周期秒 = int(freq) + obs = self._观察者字典.get(周期秒) + if obs is None: + logger.warning(f"未找到周期 {freq} 的观察者") + return None + try: + return sig_func(obs, **param) + except Exception: + logger.exception(f"信号函数执行异常: {sig_name}") + return None + else: + try: + return sig_func(self, **param) + except Exception: + logger.exception(f"信号函数执行异常: {sig_name}") + return None +``` + +- [ ] **步骤 5:移植辅助方法** + +```python + def _去重配置(self, configs: List[Dict]) -> List[Dict]: + seen = set() + unique = [] + for c in configs: + key = (c.get("name"), frozenset( + (k, str(v)) for k, v in c.items() if k != "name" + )) + if key not in seen: + seen.add(key) + unique.append(c) + return unique + + def _预加载Python信号函数(self): + for config in self._python_configs: + name = config.get("name", "") + if name and name not in self._python_func_cache: + self._python_func_cache[name] = None # placeholder + for name in list(self._python_func_cache.keys()): + try: + self._python_func_cache[name] = self._解析信号函数(name) + except Exception: + logger.warning(f"预加载信号函数失败: {name}") + + @staticmethod + def _解析信号函数(name: str) -> Optional[Callable]: + """动态导入信号函数(移植自 信号计算器._解析信号函数)。""" + import os + if "." not in name: + return __import__(name) + + module_name, func_name = name.rsplit(".", 1) + # 检查 __main__ 缓存 + main_mod = sys.modules.get("__main__") + if main_mod is not None and hasattr(main_mod, func_name): + return getattr(main_mod, func_name) + + module = __import__(module_name, fromlist=[func_name]) + return getattr(module, func_name) + + def _提取行情(self): + """从基础周期观察者提取 OHLCV 行情(Python 回退路径)。""" + obs = self._观察者字典.get(self._基础周期) + if obs is None: + return + klines = obs.普通K线序列 + if not klines: + return + k = klines[-1] + self.行情 = { + "symbol": obs.符号, + "dt": k.时间戳, # 需要从 i64 转 datetime + "id": k.序号, + "open": k.开盘价, + "high": k.最高价, + "low": k.最低价, + "close": k.收盘价, + "vol": k.成交量, + } + + @property + def 信号字典(self) -> dict: + """合并信号 + 行情(与 Position.update() 兼容)。""" + return {**self.信号, **self.行情} + + def 获取周期观察者(self, freq: str) -> Optional[观察者]: + """按频率获取观察者。""" + return self._观察者字典.get(int(freq)) + + def 从信号列表提取配置(self, 信号序列: List[str]): + """从信号字符串列表解析配置(委托给 SignalsParser)。""" + from chanlun.chan_external import get_signals_config + from chanlun.chan_external import SignalsParser + + if not 信号序列: + return + sp = SignalsParser(signals_module=self._信号模块) + conf = sp.parse(信号序列) + self.信号配置 = conf +``` + +- [ ] **步骤 6:Commit** + +```bash +git add chanlun-py/chanlun/signal_orchestrator.py +git commit -m "feat(signal): SignalOrchestrator — Rust 优先 + Python 回退混合编排器 + +Co-Authored-By: Claude " +``` + +--- + +### 任务 B2:切换到 strategies.py + +**文件:** `strategies.py` + +- [ ] **步骤 1:更新导入** + +将第 28 行的导入从: +```python +from chanlun.chan_external import 信号计算器 as _信号计算器, get_signals_config +``` +改为: +```python +from chanlun.chan_external import get_signals_config +from chanlun.signal_orchestrator import SignalOrchestrator as _信号计算器 +``` + +> 使用别名 `_信号计算器` 保持类名不变——策略内部代码零改动。 + +- [ ] **步骤 2:运行策略验证测试** + +```bash +python test_策略验证.py +``` +预期:所有 V1-V7 测试通过,无回归。 + +- [ ] **步骤 3:Commit** + +```bash +git add strategies.py +git commit -m "refactor(strategies): 切换到 SignalOrchestrator 混合编排器 + +Co-Authored-By: Claude " +``` + +--- + +### 任务 B3:修复 main.py 中损坏的调用点 + +**文件:** `main.py:2220` + +- [ ] **步骤 1:修复构造函数调用** + +当前损坏的代码: +```python +计算器 = cet.信号计算器(观察者字典, 基础周期=周期组[0], 信号模块="chanlun.signals") +计算器.从信号序列设置配置([...]) # 方法不存在 +``` + +修复为: +```python +计算器 = cet.SignalOrchestrator(分析器, 信号模块="chanlun.signals") +计算器.从信号列表提取配置([...]) +``` + +> 注意:此处 `分析器` 变量需要在该作用域内可用。需要先检查 main.py 上下文。 + +- [ ] **步骤 2:Commit** + +```bash +git add main.py +git commit -m "fix(main): 修复损坏的 信号计算器 调用点 → SignalOrchestrator + +Co-Authored-By: Claude " +``` + +--- + +## 阶段 C:测试 + +### 任务 C1:编排器单元测试 + +**文件:** 创建 `chanlun-py/tests/test_signal_orchestrator.py` + +- [ ] **步骤 1:编写框架测试** + +```python +"""SignalOrchestrator 集成测试 — 混合 Rust + Python 信号执行。""" +import pytest +from datetime import datetime, timezone +from chanlun.signal_orchestrator import SignalOrchestrator + + +def test_构造_空配置(): + """空配置构造不崩溃。""" + from chanlun import 立体分析器, 缠论配置 + analyzer = 立体分析器("test", [300, 900], 缠论配置()) + orch = SignalOrchestrator(analyzer) + assert orch.信号字典 == {} + assert orch._rust_configs == [] + assert orch._python_configs == [] + + +def test_Rust信号已注册(): + """youwukuncheng 信号名在 Rust 注册表中(应分类到 rust_configs)。""" + from chanlun import 立体分析器, 缠论配置 + analyzer = 立体分析器("test", [86400], 缠论配置()) + config = [{ + "name": "youwukuncheng_中枢第三买卖点_V230602", + "freq": 86400, + "max_overlap": 3, + "本级完整性": "实", + "同级完整性": "合", + }] + orch = SignalOrchestrator(analyzer, 信号配置=config) + assert len(orch._rust_configs) == 1 + assert len(orch._python_configs) == 0 + + +def test_Python信号回退(): + """未知信号名分类到 python_configs。""" + from chanlun import 立体分析器, 缠论配置 + analyzer = 立体分析器("test", [300], 缠论配置()) + config = [{ + "name": "chanlun.signals.demo.tas_ma_base_V230313", + "freq": 300, + "ma_type": "SMA", + "timeperiod": 5, + }] + orch = SignalOrchestrator(analyzer, 信号配置=config) + assert len(orch._rust_configs) == 0 + assert len(orch._python_configs) == 1 + + +def test_freq验证_不在周期组(): + """freq 不在分析器周期组中时抛出 ValueError。""" + from chanlun import 立体分析器, 缠论配置 + analyzer = 立体分析器("test", [300], 缠论配置()) + with pytest.raises(ValueError, match="不在分析器周期组"): + SignalOrchestrator(analyzer, 信号配置=[{ + "name": "some_signal", + "freq": 99999, + }]) +``` + +- [ ] **步骤 2:运行测试** + +```bash +python -m pytest chanlun-py/tests/test_signal_orchestrator.py -v +``` +预期:全部通过。 + +- [ ] **步骤 3:Commit** + +```bash +git add chanlun-py/tests/test_signal_orchestrator.py +git commit -m "test(signal): SignalOrchestrator 单元测试 + +Co-Authored-By: Claude " +``` + +--- + +### 任务 C2:端到端回归测试 + +- [ ] **步骤 1:运行所有 tests** + +```bash +cd chanlun && cargo test +cd chanlun-py && cargo test +python -m pytest chanlun-py/tests/ -v +python test_策略验证.py +``` + +- [ ] **步骤 2:验证零回归** + +预期:所有已有测试通过。新编排器测试通过。 + +--- + +## 自检结论 + +- **规格覆盖**:阶段 A 覆盖 SignalEngine 增强 → 完整信号字典;阶段 B 覆盖混合编排器 → 替代 Python `信号计算器`;阶段 C 覆盖测试 → 零回归 +- **类型一致**:`完整更新结果` 的 `MarketData` 字段与 Python `self.行情` 键名一致 +- **风险提示**: + 1. `main.py:2220` 调用点需要确认其所在函数的上下文(分析器变量是否在作用域内) + 2. `SignalOrchestrator` 的 `_提取行情()` 中 `k.时间戳` 是 i64,需用 `datetime.fromtimestamp` 转换 + 3. Python 信号函数需要 `chanlun.signals` 可导入——需确认安装包含 signals 子包 +- **向后兼容**:`strategies.py` 使用别名导入——内部代码零改动 diff --git a/docs/superpowers/plans/2026-06-23-signal-fn-api-and-port.md b/docs/superpowers/plans/2026-06-23-signal-fn-api-and-port.md new file mode 100644 index 0000000..89e8d1f --- /dev/null +++ b/docs/superpowers/plans/2026-06-23-signal-fn-api-and-port.md @@ -0,0 +1,503 @@ +# 子项目2 信号函数 API + 移植 youwukuncheng 实现计划 + +> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 + +**目标:** 建立 Rust 信号函数编写规范(便捷 API + 参数提取 + 确保指标),移植第一个真实信号 `youwukuncheng_中枢第三买卖点_V230602`,并通过集成测试与 Python 版对比验证。 + +**架构:** 便捷方法直接加到 `K线`/`缠论K线`/`观察者` 上(不引入额外 trait);信号函数放 `chanlun/src/signal/functions/`;参数提取独立为 `signal/params.rs`。 + +**技术栈:** Rust edition 2024、`serde_json::Value`、`parking_lot::RwLock`、`inventory`。 + +**设计文档:** `docs/superpowers/specs/2026-06-23-signal-fn-api-and-port-design.md` + +--- + +## 文件结构 + +| 文件 | 职责 | +|---|---| +| `chanlun/src/kline/bar.rs` | 给 `K线` 加便捷指标访问方法 (`macd()`, `rsi()`, `kdj()`, `boll()`, `ma()`) | +| `chanlun/src/kline/chan_kline.rs` | 给 `缠论K线` 加转发便捷方法 | +| `chanlun/src/business/observer.rs` | 加 `普K偏移()`、`缠K偏移()`、`最后缠K序列()`、`确保指标已计算()` | +| `chanlun/src/signal/params.rs` | **新建** — 参数提取辅助函数 (`get_string`, `get_int`, `get_f64`) | +| `chanlun/src/signal/mod.rs` | 增 `pub mod params;` + `pub mod functions;` | +| `chanlun/src/signal/functions/mod.rs` | **新建** — `pub mod youwukuncheng;` | +| `chanlun/src/signal/functions/youwukuncheng.rs` | **新建** — 移植的中枢第三买卖点信号 | +| `chanlun/tests/test_signal_youwukuncheng.rs` | **新建** — 集成测试(Rust vs Python 对比) | + +--- + +## 任务 0:便捷 API — K线指标访问 + 观察者方法 + 参数提取 + +**文件:** +- 修改:`chanlun/src/kline/bar.rs` +- 修改:`chanlun/src/kline/chan_kline.rs` +- 修改:`chanlun/src/business/observer.rs` +- 创建:`chanlun/src/signal/params.rs` +- 修改:`chanlun/src/signal/mod.rs` + +### 步骤 1:K线 便捷指标访问方法 + +在 `chanlun/src/kline/bar.rs` 的 `impl K线` 块中添加以下方法。 + +`K线` 已有 `pub 指标: RwLock<指标容器>` 字段,以及 `pub 收盘价: f64` 等 OHLC 字段。新增方法封装 `self.指标.read()` 的 boilerplate: + +```rust +/// 便捷读取 MACD 指标。若未计算则返回 None。 +pub fn macd(&self) -> Option<&平滑异同移动平均线> { + // 注意:返回的引用受 RwLockReadGuard 生命周期约束 + // 需要 unsafe 或者改用 cloned 版本 + // 实际采用:提供返回 Option<平滑异同移动平均线> 的 cloned 版本 + // 同时提供一个需要传入 guard 的零拷贝版本 +} + +// 实际实现方案:提供 _cloned 便捷方法(开销可忽略,MACD 仅几个 f64) +pub fn macd(&self) -> Option<平滑异同移动平均线> { + self.指标.read().macd_cloned() +} +pub fn rsi(&self) -> Option<相对强弱指数> { + self.指标.read().rsi_cloned() +} +pub fn kdj(&self) -> Option<随机指标> { + self.指标.read().kdj_cloned() +} +pub fn boll(&self) -> Option<布林带> { + self.指标.read().boll_cloned() +} +pub fn ma(&self, key: &str) -> Option { + self.指标.read().均线().and_then(|m| m.get(key).copied()) +} +``` + +> **设计理由**:使用 `_cloned` 版本而非返回引用,避免 `RwLockReadGuard` 生命周期传染到调用方。MACD/RSI/KDJ/BOLL 结构体只含少量 f64 和 Option,clone 开销可忽略。 + +### 步骤 2:缠论K线 便捷转发方法 + +在 `chanlun/src/kline/chan_kline.rs` 的 `impl 缠论K线` 块中添加转发方法。缠K 有 `pub 标的K线: RwLock>` 字段: + +```rust +/// 便捷读取 MACD(委托给标的K线) +pub fn macd(&self) -> Option<平滑异同移动平均线> { + self.标的K线.read().macd() +} +pub fn rsi(&self) -> Option<相对强弱指数> { + self.标的K线.read().rsi() +} +pub fn kdj(&self) -> Option<随机指标> { + self.标的K线.read().kdj() +} +pub fn boll(&self) -> Option<布林带> { + self.标的K线.read().boll() +} +pub fn ma(&self, key: &str) -> Option { + self.标的K线.read().ma(key) +} +/// 读取收盘价(委托给标的K线) +pub fn 收盘价(&self) -> f64 { + self.标的K线.read().收盘价 +} +``` + +### 步骤 3:观察者便捷访问方法 + +在 `chanlun/src/business/observer.rs` 的 `impl 观察者` 块中添加: + +```rust +/// 按偏移取普K,di=1 为最后一根,di=2 为倒数第二根 +pub fn 普K偏移(&self, di: usize) -> Option<&Arc> { + if di == 0 || di > self.普通K线序列.len() { return None; } + Some(&self.普通K线序列[self.普通K线序列.len() - di]) +} + +/// 按偏移取缠K,di=1 为最后一根 +pub fn 缠K偏移(&self, di: usize) -> Option<&Arc<缠论K线>> { + if di == 0 || di > self.缠论K线序列.len() { return None; } + Some(&self.缠论K线序列[self.缠论K线序列.len() - di]) +} + +/// 最后 N 根缠K(返回切片引用) +pub fn 最后缠K序列(&self, n: usize) -> &[Arc<缠论K线>] { + let len = self.缠论K线序列.len(); + if n >= len { &self.缠论K线序列[..] } + else { &self.缠论K线序列[len - n..] } +} +``` + +### 步骤 4:参数提取模块 + +创建 `chanlun/src/signal/params.rs`: + +```rust +//! 信号函数参数提取辅助 — 从 `HashMap` 中提取类型化参数。 + +use serde_json::Value; +use std::collections::HashMap; + +/// 提取字符串参数,缺失或类型不对时返回默认值。 +pub fn get_string(params: &HashMap, key: &str, default: &str) -> String { + params.get(key) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + .unwrap_or_else(|| default.to_string()) +} + +/// 提取 i64 参数。 +pub fn get_int(params: &HashMap, key: &str, default: i64) -> i64 { + params.get(key) + .and_then(|v| v.as_i64()) + .unwrap_or(default) +} + +/// 提取 f64 参数。 +pub fn get_f64(params: &HashMap, key: &str, default: f64) -> f64 { + params.get(key) + .and_then(|v| v.as_f64()) + .unwrap_or(default) +} + +/// 提取字符串引用(零拷贝),缺失时返回默认值。 +pub fn get_str<'a>(params: &'a HashMap, key: &str, default: &'a str) -> &'a str { + params.get(key) + .and_then(|v| v.as_str()) + .unwrap_or(default) +} +``` + +修改 `chanlun/src/signal/mod.rs`,在 `pub mod registry;` 后追加: +```rust +pub mod params; +pub mod functions; +``` + +### 步骤 5:构建验证 + +```bash +cd chanlun && cargo build +``` +预期:编译通过。 + +### 步骤 6:Commit + +```bash +git add chanlun/src/kline/bar.rs chanlun/src/kline/chan_kline.rs \ + chanlun/src/business/observer.rs chanlun/src/signal/params.rs \ + chanlun/src/signal/mod.rs +git commit -m "feat(signal): 便捷API — K线指标访问 + 观察者偏移 + 参数提取" +``` + +--- + +## 任务 1:确保指标 API + +**文件:** +- 修改:`chanlun/src/business/observer.rs` + +### 步骤 1:添加 `确保指标已计算` 方法 + +在 `观察者` 的 `impl` 块中添加(需要 `use crate::indicators::calculator::指标计算器;`): + +```rust +/// 确保所有 K 线上的指标已计算(幂等)。 +/// 在信号函数入口调用,保证后续 macd()/rsi() 等访问不返回 None。 +pub fn 确保指标已计算(&self) { + if self.配置.计算指标 && !self.普通K线序列.is_empty() { + 指标计算器::计算并挂载(&self.普通K线序列, &self.配置); + } +} +``` + +### 步骤 2:构建验证 + +```bash +cd chanlun && cargo build +``` + +### 步骤 3:Commit + +```bash +git add chanlun/src/business/observer.rs +git commit -m "feat(signal): 观察者.确保指标已计算() — 信号函数入口幂等调用" +``` + +--- + +## 任务 2:移植 youwukuncheng 信号函数 + +**文件:** +- 创建:`chanlun/src/signal/functions/mod.rs` +- 创建:`chanlun/src/signal/functions/youwukuncheng.rs` + +### 步骤 1:创建 functions 模块入口 + +创建 `chanlun/src/signal/functions/mod.rs`: + +```rust +//! 信号函数实现 — 每个 `#[signal]` 注册的函数对应一个子模块。 +//! +//! 第三方代码声明:信号函数模式参考 czsc(https://github.com/waditu/czsc, +//! Apache License 2.0),已适配为 Rust `fn(&观察者, &HashMap) -> Vec`。 + +pub mod youwukuncheng; +``` + +### 步骤 2:编写 youwukuncheng.rs + +创建 `chanlun/src/signal/functions/youwukuncheng.rs`。核心结构: + +```rust +use std::collections::HashMap; +use serde_json::Value; +use chanlun_signal_macros::signal; + +use crate::business::observer::观察者; +use crate::signal::params; +use crate::signal::Signal; + +/// 中枢第三买卖点信号 — 返回所有匹配的第三类买卖点信号。 +/// +/// 参数模板:"{freq}_D1MO{max_overlap}_中枢第三买卖点V230602" +/// +/// 返回三种信号(k3 = 特征 + "V230602"): +/// - 中枢段DEA穿越2V230602(同级检查) +/// - DEA穿越0轴V230602(本级检查,无须分型) +/// - 首次穿越0轴V230602(本级检查 + 分型确认) +#[signal( + name = "youwukuncheng_中枢第三买卖点_V230602", + template = "{freq}_D1MO{max_overlap}_中枢第三买卖点V230602" +)] +pub fn youwukuncheng_中枢第三买卖点_V230602( + obs: &观察者, + params: &HashMap, +) -> Vec { + // 1. 确保指标已计算 + obs.确保指标已计算(); + + // 2. 提取参数 + let max_overlap = params::get_int(params, "max_overlap", 3); + let freq = params::get_string(params, "freq", "日线"); + let 本级完整性 = params::get_string(params, "本级完整性", "实"); + let 同级完整性 = params::get_string(params, "同级完整性", "合"); + + let k1 = freq; + let k2 = format!("D1MO{max_overlap}"); + let k3 = "中枢第三买卖点V230602"; + + // 3. 前置检查 + let 当前缠K = match obs.当前缠K() { + Some(k) => k, + None => return vec![Signal::new_empty(&k1, &k2, k3)], + }; + + // 使用线段中枢序列(对应 Python 的 观察员.中枢序列) + let 中枢序列 = obs.线段中枢序列(); + if 中枢序列.is_empty() { + return vec![Signal::new_empty(&k1, &k2, k3)]; + } + + let 当前中枢 = &中枢序列[中枢序列.len() - 1]; + + // 检查是否基于线段 + if 当前中枢.基础序列.read()[0].标识.read().as_str() != "线段" { + return vec![Signal::new_empty(&k1, &k2, k3)]; + } + + // 检查中枢状态 + if 当前中枢.当前状态() == "中枢之中" { + return vec![Signal::new_empty(&k1, &k2, k3)]; + } + + // 检查本级第三买卖线 + let 本级线 = match 当前中枢.本级_第三买卖线.read().as_ref() { + Some(line) => Arc::clone(line), + None => return vec![Signal::new_empty(&k1, &k2, k3)], + }; + + let mut result = Vec::new(); + let mut 买卖点分型: Option> = None; + let 状态 = 当前中枢.当前状态(); + + // 4. 本级检查 + if 当前中枢.完整性(&本级完整性) { + // ... DEA穿越0轴 + 首次穿越0轴 逻辑 + // (详见完整实现) + } + + // 5. 同级检查 + // ... 中枢段DEA穿越2 逻辑 + // (详见完整实现) + + if result.is_empty() { + vec![Signal::new_empty(&k1, &k2, k3)] + } else { + result + } +} +``` + +> **注意**:上述为骨架代码。完整实现需按 Python 版 1:1 翻译,包括: +> - `之后缠K序列` 切片(`缠论K线序列[index..]`) +> - DIF/DEA 零轴穿越检测循环 +> - 分型确认 + `分型::从缠K序列中获取分型` +> - `线段::分割序列` + `虚线::统计MACD行为` +> - 偏移计算与 score = max(0, 100 - 偏移 * 5) + +需要额外依赖 `Signal` 的空构造器。在 `signal/signal.rs` 中添加: + +```rust +impl Signal { + /// 创建一个"空"信号(v1=v2=v3="任意",score=0),对应 Python `create_single_signal(k1=k1, k2=k2, k3=k3)` + pub fn new_empty(k1: &str, k2: &str, k3: &str) -> Self { + Self { + signal: format!("{}_{}_{}_任意_任意_任意_0", k1, k2, k3), + score: 0, + k1: k1.to_string(), + k2: k2.to_string(), + k3: k3.to_string(), + v1: "任意".to_string(), + v2: "任意".to_string(), + v3: "任意".to_string(), + } + } + + /// 创建带分类值的信号 + pub fn new(k1: &str, k2: &str, k3: &str, v1: &str, v2: &str, v3: &str, score: i32) -> Self { + Self { + signal: format!("{}_{}_{}_{}_{}_{}_{}", k1, k2, k3, v1, v2, v3, score), + score, + k1: k1.to_string(), + k2: k2.to_string(), + k3: k3.to_string(), + v1: v1.to_string(), + v2: v2.to_string(), + v3: v3.to_string(), + } + } +} +``` + +### 步骤 3:构建验证 + +```bash +cd chanlun && cargo build +``` +预期:编译通过。 + +### 步骤 4:Commit + +```bash +git add chanlun/src/signal/functions/ chanlun/src/signal/signal.rs +git commit -m "feat(signal): 移植 youwukuncheng_中枢第三买卖点_V230602 到 Rust" +``` + +--- + +## 任务 3:集成测试 — Rust vs Python 对比 + +**文件:** +- 创建:`chanlun/tests/test_signal_youwukuncheng.rs` + +### 步骤 1:创建 Python 参考脚本 + +在 `chanlun-py/tests/` 下创建 `gen_youwukuncheng_golden.py`,跑 Python 版信号函数并输出 JSON: + +```python +"""生成 youwukuncheng 信号预期输出(golden file)""" +import json, sys +sys.path.insert(0, '.') +from chanlun.chan import 观察者, 缠论配置, K线 +from chanlun.signals.youwukuncheng import youwukuncheng_中枢第三买卖点_V230602 + +# 加载 .nb 文件 +obs = 观察者("btcusd", 86400, 缠论配置.默认()) +obs.读取数据文件("chanlun-py/tests/btcusd-86400-xxx.nb", 缠论配置.默认()) + +# 调用信号函数 +params = {"freq": "日线", "max_overlap": 3, "本级完整性": "实", "同级完整性": "合"} +result = youwukuncheng_中枢第三买卖点_V230602(obs, **params) + +# 输出为 JSON +output = {k: v for k, v in result.items()} +print(json.dumps(output, ensure_ascii=False, indent=2)) +``` + +### 步骤 2:编写 Rust 集成测试 + +创建 `chanlun/tests/test_signal_youwukuncheng.rs`: + +```rust +use std::collections::HashMap; +use chanlun::business::observer::观察者; +use chanlun::config::缠论配置; +use chanlun::signal::functions::youwukuncheng::youwukuncheng_中枢第三买卖点_V230602; +use serde_json::Value; + +#[test] +fn test_youwukuncheng_产生信号() { + let obs = 观察者::new("btcusd".into(), 86400, 缠论配置::default()); + obs.write().读取数据文件("tests/btcusd-86400-xxx.nb", 缠论配置::default().不推送()) + .expect("读取数据文件失败"); + + let obs = obs.read(); + + let mut params = HashMap::new(); + params.insert("freq".to_string(), Value::String("日线".to_string())); + params.insert("max_overlap".to_string(), Value::Number(3.into())); + params.insert("本级完整性".to_string(), Value::String("实".to_string())); + params.insert("同级完整性".to_string(), Value::String("合".to_string())); + + let signals = youwukuncheng_中枢第三买卖点_V230602(&obs, ¶ms); + + println!("产生 {} 个信号:", signals.len()); + for s in &signals { + println!(" key={} value={} score={}", s.key(), s.value(), s.score); + } + + // 至少有一个非空信号(取决于数据) + let non_empty: Vec<_> = signals.iter() + .filter(|s| s.value() != "任意_任意_任意_0") + .collect(); + println!("非空信号数: {}", non_empty.len()); + + // 验证所有信号的 k3 后缀 + for s in &signals { + assert!(s.k3.ends_with("V230602"), "k3 必须以 V230602 结尾: {}", s.k3); + } +} + +#[test] +fn test_youwukuncheng_无中枢返回空信号() { + let obs = 观察者::new("empty".into(), 300, 缠论配置::default()); + let obs = obs.read(); + + let params = HashMap::new(); + let signals = youwukuncheng_中枢第三买卖点_V230602(&obs, ¶ms); + + assert_eq!(signals.len(), 1); + assert_eq!(signals[0].value(), "任意_任意_任意_0"); +} +``` + +### 步骤 3:运行测试 + +```bash +cd chanlun && cargo test --test test_signal_youwukuncheng +``` +预期:测试通过(或根据数据情况调整断言)。 + +### 步骤 4:Commit + +```bash +git add chanlun/tests/test_signal_youwukuncheng.rs +git commit -m "test(signal): youwukuncheng 集成测试 — 信号产出 + 空中枢边界" +``` + +--- + +## 自检结论 + +- **规格覆盖**:设计 §5 便捷 API → 任务 0;§7 确保指标 → 任务 1;§6 youwukuncheng → 任务 2;§8 测试 → 任务 3。全覆盖。 +- **类型一致**:`SignalFn` 签名不变。`#[signal]` 注册用子项目 1 的宏。`Signal::new_empty`/`Signal::new` 为新增构造器。 +- **风险提示**: + 1. `K线::macd()` 返回 cloned 值而非引用——已在设计 §5.1 说明理由(避免 RwLockReadGuard 生命周期传染) + 2. 集成测试依赖具体 `.nb` 测试数据——需确认文件存在且包含中枢结构 + 3. `Signal::new_empty` 的 key 格式需与 Python `create_single_signal` 一致(过滤 "任意" 段) diff --git a/docs/superpowers/specs/2026-06-22-signal-primitives-to-rust-core-design.md b/docs/superpowers/specs/2026-06-22-signal-primitives-to-rust-core-design.md new file mode 100644 index 0000000..aee5e38 --- /dev/null +++ b/docs/superpowers/specs/2026-06-22-signal-primitives-to-rust-core-design.md @@ -0,0 +1,239 @@ +# 信号原语层移植到 Rust 核心层 — 设计文档 + +- 日期:2026-06-22 +- 范围:原语层(Operate / Signal / Factor / Event / Position 配置与匹配部分) +- 参考:czsc(`/home/moscow/czsc`)的 Rust workspace 分层 + +## 1. 目标与背景 + +当前信号匹配框架(`Signal` / `Factor` / `Event` / `Position` / `Operate`)以纯 Python 实现于 `chanlun-py/chanlun/chan_external.py`(已合并进根目录 `chan.py`)。这套框架抄录自 czsc(Apache 2.0)。 + +把这层**纯结构 + 匹配逻辑**移植到 Rust 核心层(`chanlun/src/signal/`),目的: + +- **消除跨模块枚举/类型不一致问题**:信号原语只跟字符串和信号字典打交道,不持有 Rust 分析对象,天然规避「同值枚举跨模块 `is` 不相等」「动态导入找不到模块」这类坑。 +- **统一原语来源**:Rust 端策略/回测可直接用同一套 `Signal`/`Event`,无需经过 Python。 +- **性能**:匹配逻辑是热路径(每根 K 线、每个 Position 都跑),Rust 实现去掉 Python 解释开销。 +- **为后续分层铺路**:原语层稳定后,未来可按 czsc 的路线增量推进注册表、信号串解析、交易引擎。 + +## 2. 范围 + +### 纳入(Rust + PyO3) + +- `Operate` 枚举 +- `Signal`:`key()` / `value()` / `is_match()` +- `Factor`:`is_match()` / `unique_signals()` / `dump()` / `load()` +- `Event`:`is_match()` / `unique_signals()` / `dump()` / `load()` +- `Position` 基类:配置字段 + 校验 + `unique_signals` + `__repr__` + config 部分的 `dump`/`load` + +### 不纳入(保持 Python) + +- `Position.update()` 状态机(持仓推进、止损、超时、`pairs`、操作决策) +- `信号计算器`(信号计算引擎、配置管理、`_自动挂载指标`) +- `SignalsParser`(docstring 解析) +- `import_by_name`(动态导入) +- 全部信号函数(`chanlun.signals.*`) + +## 3. czsc 参考映射 + +czsc 把信号体系拆成分层 crate。本次只对应其最底层「信号原语」: + +| czsc | 本次对应 | +|---|---| +| `czsc-core/objects/{signal,event,position,operate}.rs` | `chanlun/src/signal/{signal,factor,event,position,operate}.rs` | +| `czsc-core` 内 `#[cfg(feature="python")]` 内联 PyO3 包装 | `chanlun-py/src/signal_py.rs`(本项目沿用独立绑定 crate 的既有约定,不内联) | + +czsc 的 `inventory` 编译期注册表、`#[signal]` 宏、`sig_parse`、`engine_v2` 交易引擎、`signals_dispatcher` **本次均不涉及**(属后续分层)。 + +## 4. 架构与模块布局 + +``` +chanlun/src/signal/ +├── mod.rs # pub mod 声明 + re-export +├── operate.rs # Operate 枚举(HL/HS/HO/LO/LE/SO/SE) +├── signal.rs # Signal +├── factor.rs # Factor +├── event.rs # Event +└── position.rs # Position 基类(config + matching,不含 update) +``` + +- `chanlun/src/lib.rs` 增加 `pub mod signal;`。 +- PyO3 绑定新增 `chanlun-py/src/signal_py.rs`,在 `lib.rs` 注册顺序:types → **signal** → config → indicators → kline → structure → algorithm → business → equality。 + +### 依赖边界 + +信号原语层**零依赖** `business` / `algorithm` / `structure` 层。它只操作: + +- `String`(信号各字段) +- 信号字典:匹配时通过 PyO3 接收 `&Bound`,逐键取值判类型 + +这是它能独立 `cargo test`、规避跨模块类型问题的根本原因。 + +## 5. 逐组件设计 + +### 5.1 Operate + +```rust +#[pyclass(eq, eq_int)] +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum Operate { HL, HS, HO, LO, LE, SO, SE } +``` + +- 值映射中文:`HL="持多" HS="持空" HO="持币" LO="开多" LE="平多" SO="开空" SE="平空"`,通过 `value()` 方法 / `__str__` 暴露。 +- Python 端 `cet.Operate.LO` 直接用该枚举。 + +### 5.2 Signal + +```rust +#[pyclass(module = "chanlun._chanlun")] +pub struct Signal { + signal: String, + score: i32, + k1: String, k2: String, k3: String, + v1: String, v2: String, v3: String, +} +``` + +> 注:仅 `Position` 需要 `#[pyclass(subclass)]`(Python 子类补 `update()`)。`Signal`/`Factor`/`Event` 不被子类化,用普通 `#[pyclass]`。 + +- 构造签名:`Signal(signal="", score=0, k1="任意", k2="任意", k3="任意", v1="任意", v2="任意", v3="任意")`。 + - `signal` 非空 → 按 `_` 拆 7 段(非 7 段 raise);为空 → 由各字段拼。 + - `signal` 非字符串 → `TypeError`(对齐 Python `__post_init__`)。 + - `score` 越界 [0,100] → `ValueError`。 +- `key` property:拼接 k1/k2/k3 中非「任意」的部分,`_` 连接。 +- `value` property:`v1_v2_v3_score`。 +- `is_match(s) -> bool`:见 §6。 +- `__repr__` → `Signal('')`。 + +### 5.3 Factor + +```rust +#[pyclass(module = "chanlun._chanlun")] +pub struct Factor { + signals_all: Vec, + signals_any: Vec, + signals_not: Vec, + name: String, +} +``` + +- 构造:`Factor(signals_all, signals_any=[], signals_not=[], name="")`。`signals_all` 空 → `ValueError`。 +- 构造时计算 `name`:见 §6 ③(确定性哈希)。 +- `unique_signals` property:所有 signals 的 `signal` 字符串去重列表。 +- `is_match`:`signals_not` 任一命中 → False;`signals_all` 必须全中;`signals_any` 非空时至少一中。 +- `dump() -> dict`、`load(raw) classmethod`。 + +### 5.4 Event + +```rust +#[pyclass(module = "chanlun._chanlun")] +pub struct Event { + operate: Operate, + factors: Vec, + signals_all: Vec, + signals_any: Vec, + signals_not: Vec, + name: String, + sha256: String, +} +``` + +- 构造:`Event(operate, factors, signals_all=[], signals_any=[], signals_not=[], name="")`。`factors` 空 → `ValueError`。 +- `name`:有传名 → `#`,否则 `#`;同时存 `sha256` 字段。 +- `unique_signals`、`is_match(s) -> (bool, Option)`(命中返回 `(True, factor_name)`)、`dump`、`load`。 +- `get_signals_config` **不在 Rust 实现**(依赖 Python 的 `SignalsParser`),保留在调用方 Python。 + +### 5.5 Position 基类 + +```rust +#[pyclass(subclass, module = "chanlun._chanlun")] +pub struct Position { + symbol: String, + opens: Vec, + exits: Vec, + events: Vec, // opens + exits + name: String, + interval: i64, + timeout: i64, + stop_loss: i64, + T0: bool, +} +``` + +- 构造:`Position(symbol, opens, exits=[], interval=0, timeout=1000, stop_loss=1000, T0=False, name)`。 + - `name` 缺失 → `ValueError`(对齐 Python `assert name`)。 + - 每个 event 的 `operate` ∈ {LO,LE,SO,SE},否则 raise。 +- `unique_signals` property、`__repr__`、config 部分的 `dump`/`load`。 +- **状态字段、`update()`、`pairs`、`with_data` 版 dump、`get_signals_config` 全部留 Python 子类。** + +## 6. 三个兼容性关键点 + +### ① `Signal.is_match` 缺键时 raise `ValueError` + +Python 现状:键不在信号字典 → `raise ValueError`。`strategies.py` 靠 `try: pos.update(...) except ValueError: pass` 兜底。 + +**决策**:Rust `is_match` 缺键 → `PyValueError`,**不静默返回 False**。这是行为契约。 + +### ② 信号字典值可能非字符串 + +`信号计算器.信号字典` 合并了 OHLCV 行情(值为 datetime/float)。Python 有 `isinstance(v, str)` 守卫:非 str → `logger.warning` + 返回 False。 + +**决策**:`is_match` 接收 `&Bound`。取到 key 对应值后: + +- 值不存在 → `PyValueError`(关键点 ①)。 +- 值非字符串 → 返回 False(对齐 Python 守卫)。**不打 warning**:匹配是每根 K 线的热路径,省去日志噪音;非 str 值来自 OHLCV 行情注入,是预期情况而非异常。 +- 值是字符串 → 按 `_` 拆 4 段(`v1_v2_v3_score`)做匹配。 + +### ③ Factor/Event 的 sha256 命名 + +Python:`hashlib.sha256(str(dump_dict_minus_name).encode()).hexdigest().upper()[:4]`,依赖 Python `str(dict)` 的逐字节格式。 + +**决策**:用 Rust 确定性哈希——对 `signals_all`/`signals_any`/`signals_not`(Factor)或加上 factors 的 dump(Event)拼成稳定字符串后算 sha256,取大写前 4。 + +- 自洽:同输入恒等同名,`dump`/`load` 来回一致。 +- **取舍(已知不兼容)**:生成的 hash 与 Python 旧版不同。依赖旧 `name` 的持久化仓位(保存的 .json)不再 roundtrip。本项目 Position 基本每次运行新建,可接受。 + +## 7. Drop-in 兼容策略 + +- `chan_external.py` 顶部:`from chanlun._chanlun import Signal, Factor, Event, Operate, Position as _PositionBase`,删除原 Python 类定义。 +- `Position` 改为子类: + +```python +class Position(_PositionBase): + def __init__(self, symbol, opens, exits=[], interval=0, timeout=1000, + stop_loss=1000, T0=False, name=None): + super().__init__(symbol, opens, exits, interval, timeout, stop_loss, T0, name) + # Python 侧状态 + self.pos_changed = False + self.operates = [] + self.holds = [] + self.pos = 0 + self.last_event = {...} + self.last_lo_dt = None + self.last_so_dt = None + self.end_dt = None + # update() / pairs / get_signals_config / with_data dump 保留 +``` + +- `main.py` / `strategies.py` 中 `cet.Signal(...)`、`cet.Factor(...)`、`cet.Event(...)`、`cet.Position(...)`、`cet.Operate.LO` **无需改动**——构造签名与方法名一致。 +- 根目录 `chan.py` 的对应类同样替换为 import Rust 版本(保持与包版本一致)。 + +## 8. 测试策略 + +1. **Rust 单测**(`cargo test`,`chanlun/src/signal/` 内 `#[cfg(test)]`): + - Signal:7 段解析、非 7 段 raise、score 越界 raise、key 过滤「任意」、value 拼接。 + - Factor/Event:`signals_all/any/not` 真值表全覆盖、空 signals_all/factors raise、确定性哈希同输入同名。 + - Position:name 缺失 raise、非法 operate raise、unique_signals 去重。 +2. **跨语言一致性**(pytest,复用 `tests/helpers/api_consistency.py`): + - 构造相同 Signal/Factor/Event/Position,断言 `is_match`、`unique_signals`、`dump` 结构与移植前**逐字段一致**(name hash 除外)。 + - `is_match` 缺键 raise `ValueError`、值非 str 返回 False 两条边界。 +3. **回归**:跑 `测试_信号识别` + sync 回测,确认信号匹配与开关仓行为不变。 + +## 9. 已知取舍 + +- **name hash 不兼容旧 Python 版本**(§6 ③):依赖旧 name 的持久化仓位会对不上。可接受,因 Position 多为运行时新建。 +- **`get_signals_config` 留 Python**:它依赖 `SignalsParser` 动态解析,本次不移植;Rust `Event`/`Position` 不提供该方法,由 Python 调用方补。 +- **`Position.update` 留 Python**:状态机本次不移植,Position 被一分为二(Rust 基类配置 + Python 子类状态)。 + +## 10. 许可证 + +新增 Rust 文件沿用项目 MIT 头。信号原语逻辑摘录/参考自 czsc(Apache 2.0),在 `signal/mod.rs` 顶部加第三方代码声明(与根 `chan.py` 已有声明一致)。 diff --git a/docs/superpowers/specs/2026-06-22-signal-registry-framework-design.md b/docs/superpowers/specs/2026-06-22-signal-registry-framework-design.md new file mode 100644 index 0000000..859db1f --- /dev/null +++ b/docs/superpowers/specs/2026-06-22-signal-registry-framework-design.md @@ -0,0 +1,181 @@ +# 子项目 1:信号注册框架 — 设计文档 + +- 日期:2026-06-22 +- 所属:「全 Rust 信号计算迁移」第 1 个子项目(共 4 个) +- 参考:czsc(`/home/moscow/czsc`)的 `czsc-signal-macros` + `czsc-signals/{registry,types}.rs` +- 前置:原语层已完成(`chanlun/src/signal/` 的 Signal/Factor/Event/Position/Operate) + +## 1. 背景与目标 + +「全 Rust 信号计算迁移」把信号函数、注册/解析、计算引擎、持仓状态机全部移到 Rust。拆为 4 个子项目(依赖序 1→2→3→4): + +1. **信号注册框架**(本文档) +2. 信号函数 API 暴露 + 移植 youwukuncheng +3. 信号计算引擎 + PyO3 分发器 +4. Position.update 状态机 + +本子项目交付**编译期信号注册机制**:一个 `#[signal]` 属性宏 + `inventory` 注册表 + 描述符类型 + 一个探针信号验证机制。 + +**它消灭什么**:Python 的 `import_by_name`(动态导入,曾导致「找不到模块」「跨模块枚举 `is` 不等」)和 `SignalsParser` 的 docstring 正则解析(曾导致「多 pattern sig_pats_map」「get_function_name v[0]」「sys 未导入」等脆弱 bug)。注册变成编译期完成、查表 O(1)。 + +## 2. 范围 + +### 纳入 +- 新 proc-macro crate `chanlun-signal-macros`:`#[signal(name, template)]` 属性宏 +- `chanlun/src/signal/registry.rs`:`SignalDescriptor` / `SignalFn` / `SignalMeta` / `SIGNAL_REGISTRY` + 只读查询 API +- `chanlun/Cargo.toml` 新增 `inventory` 依赖 + path 依赖 `chanlun-signal-macros` +- 一个探针信号 + 测试(验证注册→查表→重名检测) + +### 不纳入(后续子项目) +- 真实信号函数移植(子项目 2) +- 「确保指标按需增量计算」API(子项目 2,移植 youwukuncheng 读 MACD 时落地) +- 信号计算引擎 + `call_signal` PyO3 分发器(子项目 3) +- Position.update 状态机(子项目 4) + +## 3. 关键设计决策 + +| 决策 | 选择 | 理由 | +|---|---|---| +| SignalFn 是否带 TaCache | **否** | 核心层 K线已挂载指标(`指标计算器::计算并挂载`),信号函数直接读 `标的K线.指标.macd(..)`,无需 czsc 式 TaCache | +| 注册表位置 | **chanlun 核心 crate** `signal/` 模块 | 信号函数直接读 observer(同 crate)、指标在 K线上,无需独立 signals crate | +| params 类型 | `HashMap` | 灵活,对应 Python dict 来源(PyO3 层自然转换) | +| 描述符是否含 indicators/category 字段 | **否,保持最小 `{name, template, func}`** | 指标由「信号内识别 + 管线增量算」处理,不在描述符声明;本项目信号皆 observer 级,无需 category | + +## 4. Crate 结构 + +``` +chanlun-signal-macros/ ← 新建 proc-macro crate(Rust 强制独立) +├── Cargo.toml ← [lib] proc-macro = true;deps: syn, quote, proc-macro2 +└── src/lib.rs ← #[signal] 属性宏 + +chanlun/ ← 现有核心 crate +├── Cargo.toml ← 新增 inventory="0.3" + path 依赖 chanlun-signal-macros +└── src/signal/ + ├── mod.rs ← pub mod registry; + └── registry.rs ← 描述符类型 + 注册表 + 探针信号(cfg(test)) +``` + +`chanlun` 通过 path 依赖 `chanlun-signal-macros`(无需引入 workspace;Cargo path 依赖即可。如愿统一可后续加 `[workspace]`)。 + +## 5. 描述符类型与签名(`chanlun/src/signal/registry.rs`) + +```rust +use crate::business::observer::观察者; +use crate::signal::Signal; +use serde_json::Value; +use std::collections::HashMap; +use std::sync::LazyLock; + +/// 信号函数签名 — 读观察者状态(含 K线已挂指标)+ 参数 → 信号列表。无 TaCache。 +pub type SignalFn = fn(&观察者, &HashMap) -> Vec; + +/// 信号描述符(编译期元数据,由 `#[signal]` 宏生成、`inventory` 收集)。 +#[derive(Clone, Copy)] +pub struct SignalDescriptor { + /// 信号函数名,如 "youwukuncheng_中枢第三买卖点_V230602" + pub name: &'static str, + /// 参数模板,如 "{freq}_D1MO{max_overlap}_中枢第三买卖点V230602" + pub template: &'static str, + /// 函数指针 + pub func: SignalFn, +} + +inventory::collect!(SignalDescriptor); + +/// 运行时信号元信息。 +pub struct SignalMeta { + pub func: SignalFn, + pub template: &'static str, +} + +/// 归并描述符为注册表;重名返回 Err(纯函数,便于单测)。 +fn 归并( + descs: impl Iterator, +) -> Result, String> { + let mut m: HashMap<&'static str, SignalMeta> = HashMap::new(); + for d in descs { + if m.insert(d.name, SignalMeta { func: d.func, template: d.template }).is_some() { + return Err(format!("信号重名:{}", d.name)); + } + } + Ok(m) +} + +/// 全局注册表视图(由 inventory 归并;重名 panic,fail-fast)。 +pub static SIGNAL_REGISTRY: LazyLock> = LazyLock::new(|| { + 归并(inventory::iter::.into_iter().copied()) + .unwrap_or_else(|e| panic!("{e}")) +}); + +/// 按名查信号元信息。 +pub fn get_signal(name: &str) -> Option<&'static SignalMeta> { + SIGNAL_REGISTRY.get(name) +} + +/// 按名查参数模板。 +pub fn get_template(name: &str) -> Option<&'static str> { + SIGNAL_REGISTRY.get(name).map(|m| m.template) +} + +/// 列出所有已注册信号名(排序)。 +pub fn list_signal_names() -> Vec<&'static str> { + let mut v: Vec<_> = SIGNAL_REGISTRY.keys().copied().collect(); + v.sort(); + v +} +``` + +## 6. `#[signal]` 宏(`chanlun-signal-macros/src/lib.rs`) + +属性宏贴在信号函数上,做三件事: + +1. **校验**:函数名必须含 `_V<数字版本>`;`name` 属性须与函数名一致;`name`/`template` 非空。不符 → `compile_error!`。 +2. **保留原函数**不变。 +3. **生成** 一个 `static` 描述符 + `inventory::submit!` 提交: + +宏输入 `#[signal(name = "foo_V230101", template = "{freq}_D1_foo")]` 贴在 `fn foo_V230101(...)` 上,展开为(概念示意): +```rust +fn foo_V230101(观: &观察者, p: &HashMap) -> Vec { /* 原体 */ } +inventory::submit! { + crate::signal::registry::SignalDescriptor { + name: "foo_V230101", + template: "{freq}_D1_foo", + func: foo_V230101 as crate::signal::registry::SignalFn, + } +} +``` + +**路径约定**:宏 emit `crate::signal::registry::...`,即假定信号函数住在 `chanlun` crate 内(本迁移的既定结构)。 + +## 7. 测试 + +1. **宏 crate**(`chanlun-signal-macros/tests/test_signal_macro.rs`):普通集成测试——定义一个符合签名的探针函数并贴 `#[signal(name="probe_macro_V000000", template="{freq}_D1_probe")]`,断言它能编译且 `inventory::iter` 能收到对应描述符(name/template 正确)。编译失败用例(name 与函数名不一致、缺版本号)作为**可选** trybuild compile-fail 测试,非必须。 +2. **核心注册表**(`registry.rs` 内 `#[cfg(test)]`): + - 用 `inventory::submit!` 提交一个探针 `SignalDescriptor`(name `__probe_V000000`); + - `get_signal("__probe_V000000")` 命中、`get_template` 返回模板、`list_signal_names()` 含它; + - 重名场景:把归并逻辑抽成一个可独立调用的纯函数 `fn 归并(descs: impl Iterator) -> Result, String>`,单测对重复 name 返回 Err(`SIGNAL_REGISTRY` 的 LazyLock 内部调用它并对 Err `panic!`),避免污染全局 inventory。 + +## 8. 数据流 + +``` +编译期: #[signal] 宏 → SignalDescriptor 常量 → inventory::submit! +启动时: SIGNAL_REGISTRY (LazyLock) ← inventory::iter 归并(重名 panic) +运行时: get_signal(name) -> &SignalMeta { func, template } (O(1) 查表) + 后续子项目 3 的计算引擎用 func 调用、用 template 反向生成信号 key +``` + +## 9. 错误处理 + +- **编译期**:宏校验失败 → `compile_error!`(带清晰中文消息)。 +- **启动期**:重名信号 → `panic!("信号重名:{name}")`(fail-fast,对应 czsc 的 normalize 重名检测)。 +- **运行期**:`get_signal` 未命中返回 `None`(调用方——子项目 3——决定如何处理,对应旧「未找到解析函数」告警)。 + +## 10. 已知取舍与后续 + +- **无运行时可扩展性**:信号在编译期注册,新增信号需重编译(`maturin build`)。这是「全 Rust」方案的既定取舍,用户已确认。 +- **指标按需机制不在本子项目**:信号函数读指标 + 管线增量计算的「确保指标」API 在子项目 2 落地。 +- **category(kline/trader)暂不引入**:若子项目 4 的 Position.update 引入 trader 级信号,届时再扩描述符。 + +## 11. 许可证 + +新增 Rust 文件沿用项目 MIT 头。注册/宏机制参考 czsc(Apache 2.0),在 `registry.rs` 与 macro crate 顶部加第三方代码声明。 diff --git a/docs/superpowers/specs/2026-06-23-full-signal-migration-evaluation.md b/docs/superpowers/specs/2026-06-23-full-signal-migration-evaluation.md new file mode 100644 index 0000000..bc48edc --- /dev/null +++ b/docs/superpowers/specs/2026-06-23-full-signal-migration-evaluation.md @@ -0,0 +1,105 @@ +# 信号计算器完全移植评估 + +- 日期:2026-06-23 +- 前置:混合迁移(SignalEngine + SignalOrchestrator)已完成 + +## 1. 当前差距 + +### 1.1 未移植的 Python 信号函数(7/8) + +| 函数 | 文件 | 行数 | 复杂度 | 移植工时 | +|------|------|------|--------|----------| +| `bar_zdt_V230331` | demo.py | 38 | 极低 | ~1h | +| `macd_金叉` | demo.py | 56 | 低 | ~1-2h | +| `tas_macd_direct_V221106` | demo.py | 55 | 低 | ~1-2h | +| `tas_ma_base_V230313` | demo.py | 57 | 低-中 | ~2-3h | +| `cxt_停顿分型_V230106` | demo.py | 49 | 低-中 | ~3-5h | +| `cxt_bi_end_V230222` | demo.py | 74 | 中 | ~4-8h | +| `模板_V日期` | _template.py | 28 | 模板 | 不需要 | + +**总计:约 12-21 小时** + +> 已移植的只有 `youwukuncheng_中枢第三买卖点_V230602`(1/8)。 + +### 1.2 可移除的 Python 组件 + +| 组件 | 文件 | 替换方案 | +|------|------|----------| +| `SignalsParser` 类 | chan_external.py:102-293 | 不再需要——配置由 Rust 注册表直接生成 | +| `get_signals_config()` | chan_external.py:296-310 | `list_signals()` + 直接构造配置 | +| `从信号列表提取配置()` | chan_external.py:522-530 | `list_signals()` + `get_signal_template()` | +| `create_single_signal()` | chan_external.py:312-319 | 不再需要(Rust 信号函数使用 `Signal::new_empty`) | +| `chanlun.signals` 包 | signals/*.py | 所有函数已移植到 Rust | +| `chanlun.parse` | parse.py | 仅被 `SignalsParser` 使用 | +| `chan.py` 中的副本 | chan.py:7394+ | 内部副本,可单独处理 | + +## 2. 关键依赖链 + +``` +strategies.py + └→ get_signals_config(position.unique_signals, signals_module) + └→ SignalsParser(signals_module).parse(signal_strings) + └→ 遍历 chanlun.signals 模块的所有函数 + └→ 读取文档字符串 → 正则提取参数模板 + └→ parse 库反向格式化 → 配置字典 +``` + +完全移植后,这个链简化为: +``` +strategies.py + └→ 直接构造 config = [{name, freq, params}] 从 Rust list_signals() +``` + +## 3. 建议:分两阶段执行 + +### 阶段 1:移植剩余信号函数(~12-21h) + +按复杂度递增顺序: + +| 子任务 | 内容 | +|--------|------| +| 1.1 | 移植 `bar_zdt_V230331` → `chanlun/src/signal/functions/demo.rs` | +| 1.2 | 移植 `macd_金叉` → `demo.rs` | +| 1.3 | 移植 `tas_macd_direct_V221106` → `demo.rs` | +| 1.4 | 移植 `tas_ma_base_V230313` → `demo.rs`(需要均线计算辅助) | +| 1.5 | 移植 `cxt_停顿分型_V230106` → `demo.rs` | +| 1.6 | 移植 `cxt_bi_end_V230222` → `demo.rs` | + +每个子任务: +- 编写 Rust 函数 + `#[signal]` 注册 +- 编写 Rust 单元测试 +- 编写 Python 对比测试(Rust vs Python 输出) + +### 阶段 2:移除 Python 回退路径(~4-6h) + +| 子任务 | 内容 | +|--------|------| +| 2.1 | 简化 `SignalOrchestrator` → 仅使用 `SignalEngine` | +| 2.2 | 移除 `SignalsParser`、`get_signals_config`、`从信号列表提取配置` | +| 2.3 | 移除 `chanlun.signals` 包(demo.py/youwukuncheng.py/_template.py) | +| 2.4 | 移除 `chanlun.parse`(vendored parse 库) | +| 2.5 | 更新 `strategies.py` 使用直接配置构造 | +| 2.6 | 更新测试文件 | + +## 4. 收益 + +| 收益 | 说明 | +|------|------| +| 代码量减少 | 移除 ~1,200 行 Python(SignalsParser + signals 包 + parse.py + chan.py 副本) | +| 统一执行路径 | 不再有 Rust/Python 双路径,消除维护成本 | +| 编译时安全 | 所有信号函数编译时注册,不会运行时 `import_by_name` 失败 | +| 性能提升 | 批量 Rust 执行 vs 逐个 Python 调用 | +| 依赖精简 | 移除 vendored `parse` 库和 `chanlun.signals` 包 | + +## 5. 风险 + +| 风险 | 缓解 | +|------|------| +| `cxt_bi_end_V230222` 依赖笔/分型序列指针比较 | Rust 已有 `分型`/`笔` 结构,使用 `Arc` 指针 | +| `cxt_停顿分型_V230106` 依赖 `与MACD柱子分型匹配` | 需要确认 Rust 侧是否有该方法或等效逻辑 | +| `tas_ma_base_V230313` 依赖均线按需计算 | Rust 已有 `指标计算器::计算并挂载` 和 `k.ma(key)` | +| `strategies.py` 默认信号配置为空时依赖 `get_signals_config` | 切换到 `list_signals()` + 直接构造 | + +## 6. 结论 + +**完全移植可行,建议执行。** 总工作量约 16-27 小时。7 个未移植信号函数按复杂度递增顺序逐个移植(阶段 1),然后移除 Python 回退路径(阶段 2)。完成后信号框架为纯 Rust 核心 + Python 薄绑定,不再有 Python 动态导入路径。 diff --git a/docs/superpowers/specs/2026-06-23-position-update-state-machine-design.md b/docs/superpowers/specs/2026-06-23-position-update-state-machine-design.md new file mode 100644 index 0000000..a426985 --- /dev/null +++ b/docs/superpowers/specs/2026-06-23-position-update-state-machine-design.md @@ -0,0 +1,58 @@ +# 子项目 4:Position.update 状态机迁移到 Rust — 设计文档 + +- 日期:2026-06-23 +- 所属:「全 Rust 信号计算迁移」第 4 个子项目(共 4 个) +- 前置:子项目 1-3 已完成(注册表、信号函数、计算引擎) + +## 1. 背景与目标 + +子项目 1-3 交付了完整的信号计算链路:`#[signal]` 注册表 → 信号函数 → 计算引擎。Position.update 状态机是信号框架中最后一个仍留在 Python 中的核心逻辑(~135 行),将其迁移到 Rust 后,信号框架的纯 Rust 核心部分全部就位。 + +子项目 4 交付: +1. Position 状态字段(pos, operates, holds, last_event...)→ Rust 核心 +2. update() 状态机算法 → Rust 核心(与 Python 版 1:1 对应) +3. pairs() 开平配对计算 → Rust 核心 +4. PyO3 绑定:update(), 状态 getter, dump/load 带状态 + +## 2. 设计决策 + +| 决策 | 选择 | 理由 | +|------|------|------| +| 状态字段位置 | 直接加在 Position 结构体 | backtrader 在 GIL 下单线程访问;不需要额外锁 | +| update 签名(Rust) | `fn update(&mut self, dt: i64, price: f64, bid: i64, signals: &信号字典)` | 核心不依赖 Python 类型;OHLCV 由 PyO3 层提取 | +| PyDict → 信号字典 | 排除 OHLCV 键后调用 字典转核心 | 复用已有转换逻辑 | +| dt 类型兼容 | 支持 datetime/i64/f64 → 统一转为 i64 Unix 秒 | 兼容三种常见输入格式 | +| 时间戳 → Python datetime | `datetime.datetime.fromtimestamp(ts, UTC)` | 保持 operates/holds 元素类型与旧版一致 | +| Python 向后兼容 | 保留 Python 子类,__init__ 简化为空;update/pairs/dump 由 Rust 提供 | 不破坏 strategies.py 等下游代码 | +| Operate 枚举映射 | `核心Operate → OperatePy` 一对一转换函数 | 类型安全,无运行时开销 | + +## 3. 新增 Rust 类型 + +```rust +pub struct 操作记录 { symbol, dt, bid, price, op: Operate, op_desc, pos } +pub struct 持仓记录 { dt, pos, price } +pub struct 开平配对 { 标的代码, 策略标记, 交易方向, 开仓时间, 平仓时间, 开仓价格, 平仓价格, 持仓K线数, 事件序列, 持仓天数, 盈亏比例 } +pub struct 最近事件 { dt, bid, price, op, op_desc } +``` + +Position 新增 7 个状态字段:`pos, pos_changed, operates, holds, last_event, last_lo_dt, last_so_dt, end_dt` + +## 4. update() 状态机 + +与 Python `Position.update(s)` 1:1 对应: + +1. 时间校验:`dt <= end_dt` → 日志警告,跳过 +2. 事件匹配:遍历 events,调用 `event.is_match(signals)` +3. 开仓处理:LO → 间隔检查 → 开多/平空;SO → 间隔检查 → 开空/平多 +4. 多头出场:LE 信号 / 止损(price/last_price - 1 < -stop_loss/10000)/ 超时(bid - last_bid > timeout) +5. 空头出场:SE 信号 / 止损(方向反转)/ 超时 +6. 记录持仓快照 holds + +## 5. 文件结构 + +``` +chanlun/src/signal/position.rs ← 操作记录/持仓记录/开平配对/最近事件 类型 + 状态字段 + update/pairs +chanlun-py/src/signal_py.rs ← PositionPy: update(PyDict), 状态 getter, dump(with_data), load, 时间戳转datetime +chanlun-py/chanlun/chan_external.py ← Python Position 子类简化(__init__ → pass) +chanlun-py/tests/test_position_update.py ← 集成测试(24 用例) +``` diff --git a/docs/superpowers/specs/2026-06-23-signal-calculator-migration-design.md b/docs/superpowers/specs/2026-06-23-signal-calculator-migration-design.md new file mode 100644 index 0000000..04e308e --- /dev/null +++ b/docs/superpowers/specs/2026-06-23-signal-calculator-migration-design.md @@ -0,0 +1,142 @@ +# `信号计算器` Rust 迁移 — 设计文档 + +- 日期:2026-06-23 +- 所属:全 Rust 信号计算迁移 — 子项目 1-4 完成后的延续 +- 前置:子项目 1-4 全部完成(注册表 + 信号函数 + 引擎 + Position 状态机) + +## 1. 背景 + +「全 Rust 信号计算迁移」4 个子项目完成后,信号框架的 Rust 核心已就位: + +- `#[signal]` 注册表 → 编译时信号函数发现 +- `SignalEngine` → 按名查找 + 批量执行 +- `Position.update()` → 状态机 + +但 **`信号计算器`(Python 信号编排器)仍然在使用 Python 动态导入**(`import_by_name`)来发现和执行信号函数。它与 Rust `SignalEngine` **并行存在**,形成两条独立的执行路径。 + +## 2. 设计目标 + +1. **统一信号执行路径**:Rust `SignalEngine` 作为主路径,Python 动态导入作为回退 +2. **保持向后兼容**:`strategies.py` 无需改动内部逻辑 +3. **渐进式迁移**:新增 Rust 信号函数自动通过引擎执行,无需修改编排器代码 +4. **最终目标**:所有信号函数移植到 Rust 后,Python 回退路径可移除 + +## 3. 关键设计决策 + +| 决策 | 选择 | 理由 | +|------|------|------| +| 编排器架构 | 新建 `SignalOrchestrator` 类,不修改 `信号计算器` | 零风险切换;旧类保留用于对比验证 | +| 信号函数分类 | 构造时按 `list_signals()` 将配置分为 Rust/Python 两组 | 避免每次 `更新()` 都查注册表 | +| Rust 路径 | 使用 `SignalEngine.更新_完整()`(批量) | 性能优于逐个 `call_signal()` | +| Python 路径 | 保留 `import_by_name` + `_解析信号函数` | 非侵入式;已有信号函数无需任何修改 | +| OHLCV 行情 | Rust 引擎直接返回基础周期行情 | 消除 Python 侧的独立行情提取步骤 | +| freq 验证 | 在编排器 setter 中验证 | 与旧 `信号计算器` 行为一致 | + +## 4. 架构图 + +``` +┌─────────────────────────────────────────────────┐ +│ SignalOrchestrator │ +│ │ +│ 信号配置 ──→ 分类(list_signals() 查表) │ +│ │ │ +│ ┌────────┴────────┐ │ +│ │ Rust 已注册 │ Python 未注册 │ +│ │ SignalEngine │ import_by_name │ +│ │ .更新_完整() │ ._执行Python信号函数() │ +│ └────────┬────────┘ │ +│ │ │ +│ 合并结果 → self.信号 + self.行情 │ +│ │ +│ self.信号字典 → Position.update() │ +└─────────────────────────────────────────────────┘ +``` + +## 5. SignalEngine 增强 + +### 5.1 新增 `更新_完整()` 方法 + +```rust +pub struct 完整更新结果 { + pub signals: HashMap, + pub market: Option, +} + +pub struct MarketData { + pub symbol: String, + pub dt: i64, // Unix 秒 + pub id: i64, + pub open: f64, pub high: f64, pub low: f64, + pub close: f64, pub vol: f64, +} +``` + +`更新_完整(&self, analyzer: &立体分析器) -> 完整更新结果`: +1. 调用 `self.更新(analyzer)` 获取信号 +2. 从 `analyzer.周期组[0]` 获取基础周期观察者 +3. 提取最后一根普K的 OHLCV 数据 +4. 返回组合结果 + +## 6. SignalOrchestrator 设计 + +### 6.1 类签名 + +```python +class SignalOrchestrator: + def __init__( + self, + 分析器: 立体分析器, + 信号配置: Optional[List[Dict]] = None, + 信号模块: str = "chanlun.signals", + ): +``` + +### 6.2 方法 + +| 方法 | 来源 | 说明 | +|------|------|------| +| `更新()` | 新写 | 先 Rust 批量,再 Python 逐个 | +| `信号配置` (property) | 移植 | setter 中添加 Rust/Python 分类 | +| `信号字典` (property) | 移植 | `{**self.信号, **self.行情}` | +| `获取周期观察者(freq)` | 移植 | 委托给 `_观察者字典` | +| `从信号列表提取配置(信号序列)` | 移植 | 委托给 `SignalsParser` | +| `_去重配置(configs)` | 移植 | 与旧版一致 | +| `_预加载Python信号函数()` | 移植 | 缓存 Python 函数引用 | +| `_解析信号函数(name)` | 移植 | `import_by_name` 逻辑 | +| `_执行Python信号函数(config)` | 移植 | Python 函数调用 | +| `_提取行情()` | 移植 | 仅 Python-only 回退路径使用 | + +## 7. 迁移路径 + +### 阶段 A:增强 SyncSignalEngine(1-2 commits) +- `更新_完整()` + PyO3 绑定 +- 不改变现有行为 + +### 阶段 B:引入 SignalOrchestrator(2-3 commits) +- 新文件 `signal_orchestrator.py` +- `strategies.py` 切换到新类(别名导入) +- 修复 `main.py` 损坏的调用点 + +### 阶段 C:废弃 Python 并行路径(未来) +- 所有信号函数移植到 Rust 后 +- 移除 `信号计算器`、`SignalsParser`、`import_by_name` +- 移除 `signals/` 目录中的 Python 信号函数 + +## 8. 向后兼容 + +| 组件 | 兼容策略 | +|------|---------| +| `strategies.py` | 别名导入 `SignalOrchestrator as _信号计算器`——零代码改动 | +| `main.py` | 修复损坏的调用点(原本就 broken) | +| `test_策略验证.py` | 零改动——`信号计算器` 类名不变 | +| Python 信号函数 | 零改动——`import_by_name` 路径不变 | +| `Position.update()` | 零改动——Rust 状态机不变 | + +## 9. 风险 + +| 风险 | 缓解 | +|------|------| +| `_提取行情()` 中 `k.时间戳` 是 i64(Rust K线),不是 Python datetime | 已由 Rust `PositionPy::时间戳转datetime` 处理 | +| `SignalEngine.更新_完整()` 的基础周期可能与 `_基础周期` 不一致 | 统一从 `分析器.周期组[0]` 获取 | +| Python 信号函数的 `**kwargs` 中 `freq` 是字符串(来自 SignalsParser) | `_执行Python信号函数` 中 `int(freq)` 转换 | +| `list_signals()` 返回的是 Rust 注册名,不含模块路径 | 按短名匹配(`youwukuncheng_中枢第三买卖点_V230602` 不含 `chanlun.signals.` 前缀) | diff --git a/docs/superpowers/specs/2026-06-23-signal-fn-api-and-port-design.md b/docs/superpowers/specs/2026-06-23-signal-fn-api-and-port-design.md new file mode 100644 index 0000000..1520e6c --- /dev/null +++ b/docs/superpowers/specs/2026-06-23-signal-fn-api-and-port-design.md @@ -0,0 +1,200 @@ +# 子项目 2:信号函数 API + 移植第一个真实信号 — 设计文档 + +- 日期:2026-06-23 +- 所属:「全 Rust 信号计算迁移」第 2 个子项目(共 4 个) +- 前置:子项目 1 已完成(`#[signal]` 宏 + `inventory` 注册表) +- 参考:`chanlun-py/chanlun/signals/youwukuncheng.py`、czsc + +## 1. 背景与目标 + +子项目 1 交付了编译期信号注册机制(`#[signal]` + `inventory` + `SIGNAL_REGISTRY`),探针信号已验证注册→查表链路。现在是时候移植第一个真实信号函数,并在过程中建立 Rust 信号函数的**编写规范**和**辅助 API**。 + +子项目 2 交付: +1. **信号函数便捷 API** — 扩展 trait,让 Rust 信号函数代码读起来接近 Python 版本 +2. **确保指标按需增量计算** — 信号函数可确保所需指标已计算 +3. **移植 youwukuncheng_中枢第三买卖点_V230602** — 第一个真实信号(3 种信号变体) +4. **集成测试** — Rust vs Python 输出对比 + +## 2. 范围 + +### 纳入 +- `chanlun/src/signal/functions/` 模块(信号函数目录) +- `chanlun/src/signal/functions/youwukuncheng.rs` — 移植的中枢第三买卖点信号 +- 便捷扩展 trait:`IndicatorAccess`(K线指标读取)、`ObserverAccess`(观察者便捷访问) +- 参数提取辅助函数(`params_ext.rs`) +- 确保指标 API:`观察者::确保指标已计算(&self)` +- 集成测试:喂入 `.nb` 数据,Rust 信号输出 vs Python 信号输出 +- `#[signal]` 注册 youwukuncheng + +### 不纳入(后续子项目) +- 信号计算引擎 + `call_signal` PyO3 分发器(子项目 3) +- Position.update 状态机(子项目 4) +- 其他信号函数(demo.py 中的 macd_金叉、cxt_bi_end 等) +- Python 侧可直接调用的 PyO3 信号函数分发器 + +## 3. 关键设计决策 + +| 决策 | 选择 | 理由 | +|---|---|---| +| 便捷 API 形式 | **直接给 K线 / 观察者 加方法** | 简洁,不需要 import 额外 trait。已有前例(观察者.当前缠K()) | +| 指标访问封装 | **方法返回 Option,隐藏 RwLock** | 信号函数不应关心锁细节;`kline.macd()` 返回 `Option<&MACD>` | +| 确保指标机制 | **观察者.确保指标已计算() 重跑计算器** | 简单,复用现有 `指标计算器::计算并挂载`。后续子项目 3 由计算引擎在调用前统一 ensure | +| 参数提取 | **独立 `params` 子模块,纯函数** | `HashMap` 的字符串/数字提取到处都需要,集中处理 | +| 信号函数位置 | `chanlun/src/signal/functions/` | 与 registry 同 crate,`#[signal]` emit 的 `crate::` 路径可直接解析 | +| 测试策略 | **Rust 集成测试 + Python 对比** | 加载 .nb → 跑 Rust 信号 → 序列化输出;Python 侧同样跑 → diff | + +## 4. 文件结构 + +``` +chanlun/src/signal/ +├── mod.rs ← pub mod functions; pub mod params; +├── functions/ +│ ├── mod.rs ← pub mod youwukuncheng; +│ └── youwukuncheng.rs ← #[signal] fn youwukuncheng_中枢第三买卖点_V230602 +├── params.rs ← 参数提取辅助函数 +├── ... (已有: signal, factor, event, position, operate, registry) +│ +chanlun/src/kline/ +├── bar.rs ← 给 K线 加便捷指标访问方法 +│ +chanlun/src/business/ +├── observer.rs ← 给 观察者 加便捷方法 + 确保指标 + +chanlun/tests/ +├── test_signal_youwukuncheng.rs ← 集成测试(Rust vs Python 对比) +``` + +## 5. 便捷 API 设计 + +### 5.1 K线 便捷指标访问(`bar.rs` 新增方法) + +将现有的 `k线.指标.read().macd()` 封装为直接的 `k线.macd()`: + +```rust +impl K线 { + /// 读取 MACD 指标(已计算则返回引用,否则 None) + pub fn macd(&self) -> Option<&平滑异同移动平均线> { ... } + pub fn rsi(&self) -> Option<&相对强弱指数> { ... } + pub fn kdj(&self) -> Option<&随机指标> { ... } + pub fn boll(&self) -> Option<&布林带> { ... } + /// 读取均线值,如 ma("SMA_5") → Option + pub fn ma(&self, key: &str) -> Option { ... } +} +``` + +同样给 `缠论K线` 加转发方法(委托给 `self.标的K线`)。 + +### 5.2 观察者便捷访问(`observer.rs` 新增方法) + +```rust +impl 观察者 { + /// 按偏移取普K(di=1 为最后一根) + pub fn 普K偏移(&self, di: usize) -> Option<&Arc> { ... } + /// 按偏移取缠K + pub fn 缠K偏移(&self, di: usize) -> Option<&Arc<缠论K线>> { ... } + /// 最后 N 根缠K + pub fn 最后缠K序列(&self, n: usize) -> &[Arc<缠论K线>] { ... } + /// 线段级中枢序列(= 中枢序列组[1]) + pub fn 线段中枢序列(&self) -> &Vec> { ... } + /// 确保所有 K 线上的指标已计算(调用 指标计算器::计算并挂载) + pub fn 确保指标已计算(&self) { ... } +} +``` + +### 5.3 参数提取(`signal/params.rs`) + +```rust +/// 从 params HashMap 提取字符串参数 +pub fn get_string(params: &HashMap, key: &str, default: &str) -> String; +/// 从 params HashMap 提取整数参数 +pub fn get_int(params: &HashMap, key: &str, default: i64) -> i64; +/// 从 params HashMap 提取浮点参数 +pub fn get_f64(params: &HashMap, key: &str, default: f64) -> f64; +``` + +这些是纯辅助函数,不做任何复杂逻辑。 + +## 6. youwukuncheng 移植要点 + +### 6.1 信号逻辑 + +Python 版 143 行 → Rust 预计 ~200 行(含类型标注和 RwLock 读取)。 + +三种产出信号(k3 后缀均为 `V230602`): + +| k3 | 触发条件 | v1 | v2 | score | +|---|---|---|---|---| +| `中枢段DEA穿越2V230602` | 同级第三买卖线段内 DEA 穿越 0 轴 | 中枢段DEA穿越2 | 三买/三卖 | max(0, 100-偏移×5) | +| `DEA穿越0轴V230602` | 本级第三买卖线处 DEA 在 0 轴同侧 | DEA穿越0轴 | 三买/三卖 | max(0, 100-偏移×5) | +| `首次穿越0轴V230602` | DIF 首次反穿 0 轴 + 分型确认 | 首次穿越0轴 | 三买/三卖 | max(0, 100-偏移×5) | + +### 6.2 关键 Rust 对应 + +| Python | Rust | +|---|---| +| `观察员.当前缠K` | `obs.当前缠K()` | +| `观察员.中枢序列` | `obs.中枢序列()` (笔中枢) 或 `obs.线段中枢序列()` (线段中枢) | +| `当前中枢.基础序列[0].标识` | `当前中枢.基础序列.read()[0].标识.read().as_str()` | +| `当前中枢.当前状态()` | `当前中枢.当前状态()` | +| `当前中枢.本级_第三买卖线` | `当前中枢.本级_第三买卖线.read().as_ref()` | +| `当前中枢.完整性("实")` | `当前中枢.完整性("实")` | +| `k.标的K线.macd.DEA` | `k.标的K线.read().macd().map(\|m\| m.DEA)` | +| `k.分型 is 分型结构.底` | `*k.分型.read() == Some(分型结构::底)` | +| `分型.从缠K序列中获取分型(序列, k)` | `分型::从缠K序列中获取分型(序列, k)` | +| `虚线.统计MACD行为(普K序列, 8, 3)` | `虚线::统计MACD行为(&普K序列, 8, 3)` | +| `段.获取普K序列(观察员.观察员)` | `段.获取普K序列(&obs.普通K线序列)` | + +### 6.3 注意事项 + +1. **lock 顺序**:读取 `基础序列`、`武`、`标的K线`、`指标` 时注意 RwLock 不可重入。同一作用域内避免同时持有多个写锁。本函数只有读操作,安全。 +2. **AtomicI64**:`序号` 用 `.load(Ordering::Relaxed)` 读取 +3. **Option 链**:Python 的 `x.y.z` 在 Rust 中是 `x.y.read().z`,需要处理 `Option` +4. **空信号返回**:Python 返回 `create_single_signal(k1, k2, k3)`(v1=v2=v3="任意");Rust 返回 `vec![Signal::new_empty(k1, k2, k3)]` + +## 7. 确保指标 API + +```rust +impl 观察者 { + /// 确保所有 K线上的指标已计算。 + /// 如果 配置.计算指标 为 true 且序列非空,则调用 指标计算器::计算并挂载。 + pub fn 确保指标已计算(&self) { + if self.配置.计算指标 && !self.普通K线序列.is_empty() { + 指标计算器::计算并挂载(&self.普通K线序列, &self.配置); + } + } +} +``` + +信号函数在入口调用一次 `obs.确保指标已计算()`(幂等——计算器检测已计算的值会跳过)。 + +注:后续子项目 3 的信号计算引擎会在调用任何信号前统一 ensure,信号函数内部的 ensure 调用届时可移除。 + +## 8. 测试设计 + +### 8.1 集成测试(`chanlun/tests/test_signal_youwukuncheng.rs`) + +1. 加载测试 `.nb` 文件(选择已有中枢结构的 btcusd 数据) +2. 创建观察者,喂入 K 线,触发分析 +3. 调用 `youwukuncheng_中枢第三买卖点_V230602(&obs, ¶ms)` +4. 验证返回的 `Vec` 非空,信号 key/value 格式正确 +5. 与 Python 版输出对比(golden 方式:运行 Python 脚本生成预期输出文件,Rust 测试读取对比) + +### 8.2 测试数据 + +使用已有测试 `.nb` 文件(如 `btcusd-86400-...`,日线数据有丰富的中枢结构)。 + +## 9. 错误处理 + +- 信号函数内部所有 `Option` 缺值 → 返回空信号(与 Python 行为一致) +- `确保指标已计算` 失败 → 静默跳过(指标不存在时信号函数内部 `macd().is_none()` 自然会返回空) +- `#[signal]` 注册失败(重名)→ 子项目 1 已处理(编译期 panic) + +## 10. 已知取舍 + +- **便捷方法只加常用读路径**:`macd()/rsi()/kdj()/boll()/ma()` + 偏移访问。复杂查询(如遍历所有 K 线做自定义分析)直接用底层 API。 +- **确保指标基于现有管线**:不做 czsc 式的 TaCache(已决策,见子项目 1 §3)。SignalFn 签名保持 `&观察者` 单参数。 +- **信号函数在 lib 内**:不暴露为独立的 `chanlun-signals` crate。与子项目 1 决策一致——信号函数同 crate,可直接访问 observer 内部。 + +## 11. 许可证 + +新增文件沿用项目 MIT 头。youwukuncheng 移植自项目自有 Python 代码,不涉及第三方许可证。 diff --git a/examples/plugin-demo/.gitignore b/examples/plugin-demo/.gitignore new file mode 100644 index 0000000..b83d222 --- /dev/null +++ b/examples/plugin-demo/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/examples/plugin-demo/Cargo.lock b/examples/plugin-demo/Cargo.lock new file mode 100644 index 0000000..1842ae1 --- /dev/null +++ b/examples/plugin-demo/Cargo.lock @@ -0,0 +1,594 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cc" +version = "1.2.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chanlun" +version = "26.6.4" +dependencies = [ + "byteorder", + "chanlun-signal-macros", + "chrono", + "fastrand", + "inventory", + "parking_lot", + "serde", + "serde_json", + "sha2", + "tracing", +] + +[[package]] +name = "chanlun-signal-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "plugin-demo" +version = "0.1.0" +dependencies = [ + "chanlun", + "chanlun-signal-macros", + "inventory", + "parking_lot", + "serde_json", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasm-bindgen" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/examples/plugin-demo/Cargo.toml b/examples/plugin-demo/Cargo.toml new file mode 100644 index 0000000..c03b89e --- /dev/null +++ b/examples/plugin-demo/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "plugin-demo" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +chanlun = { path = "../../chanlun" } +chanlun-signal-macros = { path = "../../chanlun-signal-macros" } +inventory = "0.3" +serde_json = "1" +parking_lot = "0.12" diff --git a/examples/plugin-demo/src/lib.rs b/examples/plugin-demo/src/lib.rs new file mode 100644 index 0000000..a64b298 --- /dev/null +++ b/examples/plugin-demo/src/lib.rs @@ -0,0 +1,202 @@ +//! chanlun 信号插件示例 — 两种动态注册方式。 +//! +//! 编译: `cargo build` → `target/debug/libplugin_demo.so` +//! 加载: Python → `ctypes.CDLL(...)` → `init_plugin_manual()` / `init_plugin_macro()` +//! +//! # 方式 A: 手动 C-ABI 注册 +//! - 实现 `fn(SignalFn)` 信号函数 +//! - `init` 中调用 `chanlun_register_signal(name, template, func_ptr)` +//! +//! # 方式 B: #[signal] 宏 + inventory 批量提交 +//! - 用 `#[signal]` 宏写信号函数(与宿主内写法完全一致) +//! - `init` 中遍历 `inventory::iter::` 批量调用 `chanlun_register_signal` + +use std::collections::HashMap; +use std::os::raw::c_char; + +use chanlun::business::observer::观察者; +use chanlun::signal::registry::SignalFn; +use chanlun::signal::Signal; +use serde_json::Value; + +// ── C-ABI: 宿主导出的符号(由 dlopen 的动态链接器解析)── + +unsafe extern "C" { + fn chanlun_register_signal( + name: *const c_char, + template: *const c_char, + func: SignalFn, + ) -> i32; + fn chanlun_unregister_signal(name: *const c_char) -> i32; +} + +// ═══════════════════════════════════════════════════════════ +// 方式 A: 手动 C-ABI 注册 +// ═══════════════════════════════════════════════════════════ + +/// 插件信号 A:MACD 零上强势(DIF > 0 && DIF > DEA)。 +/// +/// 手动注册 — 不依赖 `#[signal]` 宏,不依赖 `chanlun-signal-macros`。 +fn 插件MACD强势_V999999(obs: &观察者, params: &HashMap) -> Vec { + obs.确保指标已计算(); + let freq = params.get("freq").and_then(|v| v.as_str()).unwrap_or("日线"); + let di = params.get("di").and_then(|v| v.as_i64()).unwrap_or(1) as usize; + + let k1 = freq.to_string(); + let k2 = format!("D{di}"); + let k3 = "插件MACD强势V999999"; + + let klines = &obs.普通K线序列; + if klines.len() < di + 1 { + return vec![Signal::new_empty(&k1, &k2, k3)]; + } + let k线 = &klines[klines.len() - di]; + let macd = match k线.macd() { + Some(m) => m, + None => return vec![Signal::new_empty(&k1, &k2, k3)], + }; + let dif = match macd.DIF { Some(v) => v, None => return vec![Signal::new_empty(&k1, &k2, k3)] }; + let dea = match macd.DEA { Some(v) => v, None => return vec![Signal::new_empty(&k1, &k2, k3)] }; + + if dif > 0.0 && dif > dea { + let score = ((dif - dea).abs() * 1000.0) as i32; + vec![Signal::new(&k1, &k2, k3, "多头强势", "MACD零上", "DIF>DEA", score)] + } else if dif < 0.0 && dif < dea { + vec![Signal::new(&k1, &k2, k3, "空头强势", "MACD零下", "DIF i32 { + let name = c"插件MACD强势_V999999"; + let template = c"{freq}_D{di}_插件MACD强势V999999"; + let func: SignalFn = 插件MACD强势_V999999; + + let ret = chanlun_register_signal(name.as_ptr(), template.as_ptr(), func); + eprintln!("[plugin/manual] 注册: {name:?} → 返回码 {ret}"); + ret +} + +/// 方式 A 清理。 +#[unsafe(no_mangle)] +pub unsafe extern "C" fn deinit_plugin_manual() -> i32 { + chanlun_unregister_signal(c"插件MACD强势_V999999".as_ptr()) +} + +// ═══════════════════════════════════════════════════════════ +// 方式 B: #[signal] 宏 + inventory::iter 批量提交 +// ═══════════════════════════════════════════════════════════ + +// 依赖: Cargo.toml 中添加 chanlun-signal-macros +use chanlun::signal::registry::SignalDescriptor; +use chanlun_signal_macros::signal; + +/// 插件信号 B1:MACD 金叉(与宿主内 `macd_金叉_V260601` 等价)。 +#[signal( + name = "插件MACD金叉_V999999", + template = "{freq}_D{di}#MACD#{fast}#{slow}#{signal}_插件MACD金叉V999999", + crate_path = "::chanlun" +)] +fn 插件MACD金叉_V999999(obs: &观察者, params: &HashMap) -> Vec { + obs.确保指标已计算(); + let di = params.get("di").and_then(|v| v.as_i64()).unwrap_or(1) as usize; + let freq = params.get("freq").and_then(|v| v.as_str()).unwrap_or("日线"); + + let k1 = freq.to_string(); + let k2 = "D1#MACD#13#31#11".to_string(); + let k3 = "插件MACD金叉V999999"; + + let klines = &obs.普通K线序列; + if klines.len() < di + 2 { + return vec![Signal::new_empty(&k1, &k2, k3)]; + } + let cur = klines[klines.len() - di].macd(); + let prev = klines[klines.len() - di - 1].macd(); + let (cur, prev) = match (cur, prev) { + (Some(c), Some(p)) => (c, p), + _ => return vec![Signal::new_empty(&k1, &k2, k3)], + }; + let cd = match cur.DIF { Some(v) => v, None => return vec![Signal::new_empty(&k1, &k2, k3)] }; + let ce = match cur.DEA { Some(v) => v, None => return vec![Signal::new_empty(&k1, &k2, k3)] }; + let pd = match prev.DIF { Some(v) => v, None => return vec![Signal::new_empty(&k1, &k2, k3)] }; + let pe = match prev.DEA { Some(v) => v, None => return vec![Signal::new_empty(&k1, &k2, k3)] }; + + if pd <= pe && cd > ce { + vec![Signal::new(&k1, &k2, k3, "金叉", "插件", "[signal]宏", 80)] + } else if pd >= pe && cd < ce { + vec![Signal::new(&k1, &k2, k3, "死叉", "插件", "[signal]宏", 0)] + } else { + vec![Signal::new_empty(&k1, &k2, k3)] + } +} + +/// 插件信号 B2:涨跌停(与宿主内 `bar_zdt_V230331` 等价)。 +#[signal( + name = "插件涨跌停_V999999", + template = "{freq}_D{di}_插件涨跌停V999999", + crate_path = "::chanlun" +)] +fn 插件涨跌停_V999999(obs: &观察者, params: &HashMap) -> Vec { + let di = params.get("di").and_then(|v| v.as_i64()).unwrap_or(1) as usize; + let freq = params.get("freq").and_then(|v| v.as_str()).unwrap_or("15分钟"); + + let k1 = freq.to_string(); + let k2 = format!("D{di}"); + let k3 = "插件涨跌停V999999"; + + let klines = &obs.普通K线序列; + if klines.len() < di + 2 { + return vec![Signal::new_empty(&k1, &k2, k3)]; + } + let 当前 = &klines[klines.len() - di]; + let 前 = &klines[klines.len() - di - 1]; + + let v1 = if 当前.收盘价 == 当前.高 && 当前.收盘价 >= 前.收盘价 { + "涨停" + } else if 当前.收盘价 == 当前.低 && 当前.收盘价 <= 前.收盘价 { + "跌停" + } else { + "任意" + }; + + if v1 == "任意" { + vec![Signal::new_empty(&k1, &k2, k3)] + } else { + vec![Signal::new(&k1, &k2, k3, v1, "插件", "[signal]宏", 0)] + } +} + +/// 方式 B 入口:Python 调用 `plugin.init_plugin_macro()`。 +/// +/// 遍历 `inventory::iter::`,将 `#[signal]` 宏注册的 +/// 所有信号一次性提交到宿主 `DYNAMIC_REGISTRY`。 +#[unsafe(no_mangle)] +pub unsafe extern "C" fn init_plugin_macro() -> i32 { + let mut count = 0i32; + for desc in inventory::iter:: { + let Ok(name_c) = std::ffi::CString::new(desc.name) else { continue }; + let Ok(tpl_c) = std::ffi::CString::new(desc.template) else { continue }; + let ret = chanlun_register_signal(name_c.as_ptr(), tpl_c.as_ptr(), desc.func); + if ret == 0 { + count += 1; + eprintln!("[plugin/macro] ✅ {} → {}", desc.name, desc.template); + } else { + eprintln!("[plugin/macro] ❌ {} (err {ret})", desc.name); + } + } + eprintln!("[plugin/macro] 批量注册完成: {count} 个信号"); + count +} + +/// 方式 B 清理:遍历 inventory 逐个调用 `chanlun_unregister_signal`。 +#[unsafe(no_mangle)] +pub unsafe extern "C" fn deinit_plugin_macro() -> i32 { + for desc in inventory::iter:: { + let Ok(name_c) = std::ffi::CString::new(desc.name) else { continue }; + chanlun_unregister_signal(name_c.as_ptr()); + } + 0 +} diff --git a/main.py b/main.py index 3259769..4adf8f7 100644 --- a/main.py +++ b/main.py @@ -46,10 +46,14 @@ from jinja2 import Environment, FileSystemLoader from pydantic import BaseModel import backtrader as bt +import chanlun from chanlun import * - from strategies import * +chanlun.set_扩展线段模式(False) + +import chanlun.chan_external as cet + def Nil(*args, **kwargs): return None @@ -138,56 +142,6 @@ def 收集异常信息(exception: Exception, 上下文: dict = None): return 错误报告 -class 图表展示序列(list): - def __init__(self, 观察员: "观察者"): - super().__init__() - self.观察员 = 观察员 - self.序号 = 0 - self.__类型标识 = None - - def append(self, __object): - if self.序号 > 0: - if __object.标识 != self.__类型标识: - ... - self.图表刷新(self[-1], sys._getframe().f_lineno) - - else: - self.__类型标识 = __object.标识 - super().append(__object) - self.图表添加(__object, sys._getframe().f_lineno) - self.序号 += 1 - - if __object.标识 in ("线段", "线段<线段>"): - if self.观察员 and self.观察员.配置.线段内部中枢图显: - 段: 虚线 = __object - 段.合_中枢序列 = 图表展示序列(self.观察员) - 段.实_中枢序列 = 图表展示序列(self.观察员) - 段.虚_中枢序列 = 图表展示序列(self.观察员) - - def pop(self, __index: SupportsIndex = -1): - 弹出 = super().pop(__index) - self.图表移除(弹出, sys._getframe().f_lineno) - self.序号 -= 1 - return 弹出 - - def clear(self) -> None: - self.序号 = 0 - super().clear() - - def 尾部刷新(self, 行号: int): - if self.序号: - self.图表刷新(self[-1], 行号) - - def 图表添加(self, 实线: Union["虚线", "中枢"], 行号: int): - self.观察员 and self.观察员.报信(实线, 指令.添加(实线.标识), 行号) - - def 图表移除(self, 实线: Union["虚线", "中枢"], 行号: int): - self.观察员 and self.观察员.报信(实线, 指令.删除(实线.标识), 行号) - - def 图表刷新(self, 实线: Union["虚线", "中枢"], 行号: int): - self.观察员 and self.观察员.报信(实线, 指令.修改(实线.标识), 行号) - - class 时间周期: def __init__(self, 秒: int, 是否单笔交易: bool = False): self._秒 = 秒 @@ -401,6 +355,56 @@ class 指令: return cls(cls.删, 标识) +class 图表展示序列(list): + def __init__(self, 观察员: "观察者"): + super().__init__() + self.观察员 = 观察员 + self.序号 = 0 + self.__类型标识 = None + + def append(self, __object): + if self.序号 > 0: + if __object.标识 != self.__类型标识: + ... + self.图表刷新(self[-1], sys._getframe().f_lineno) + + else: + self.__类型标识 = __object.标识 + super().append(__object) + self.图表添加(__object, sys._getframe().f_lineno) + self.序号 += 1 + + if __object.标识 in ("线段", "线段<线段>"): + if self.观察员 and self.观察员.配置.线段内部中枢图显: + 段: 虚线 = __object + 段.合_中枢序列 = 图表展示序列(self.观察员) + 段.实_中枢序列 = 图表展示序列(self.观察员) + 段.虚_中枢序列 = 图表展示序列(self.观察员) + + def pop(self, __index: SupportsIndex = -1): + 弹出 = super().pop(__index) + self.图表移除(弹出, sys._getframe().f_lineno) + self.序号 -= 1 + return 弹出 + + def clear(self) -> None: + self.序号 = 0 + super().clear() + + def 尾部刷新(self, 行号: int): + if self.序号: + self.图表刷新(self[-1], 行号) + + def 图表添加(self, 实线: Union["虚线", "中枢"], 行号: int): + self.观察员 and self.观察员.报信(实线, 指令.添加(实线.标识), 行号) + + def 图表移除(self, 实线: Union["虚线", "中枢"], 行号: int): + self.观察员 and self.观察员.报信(实线, 指令.删除(实线.标识), 行号) + + def 图表刷新(self, 实线: Union["虚线", "中枢"], 行号: int): + self.观察员 and self.观察员.报信(实线, 指令.修改(实线.标识), 行号) + + class 观察者(观察者): 当前事件循环: Any = None # if __name__ == "__main__" else asyncio.get_event_loop() 延迟时间: float = 0.01 @@ -417,12 +421,11 @@ class 观察者(观察者): if self.__终止时间戳 and 普K.时间戳 > self.__终止时间戳: return - if self.配置.推送K线: + if self.配置.展示标签("RawBar"): self.报信(普K, 指令.添加("RawBar"), sys._getframe().f_lineno, 周期=普K.周期) - super().增加原始K线(普K) - try: + super().增加原始K线(普K) self.数据队列 and self.数据队列.put((普K.时间戳, 普K.开盘价, 普K.高, 普K.低, 普K.收盘价, 普K.成交量, 0)) if self.数据通道 is not None and self.配置.图表展示: time.sleep(self.延迟时间) @@ -452,11 +455,41 @@ class 观察者(观察者): def 重置基础序列(self): self.买卖点字典 = dict() super().重置基础序列() + return + self.笔序列: List[虚线] = [] if not self.配置.图表展示 else 图表展示序列(self) + self.笔_中枢序列: List[中枢] = [] if not self.配置.图表展示 else 图表展示序列(self) + + self.线段序列组: List[List[虚线],] = [] # 线段, 线段<线段>,线段<线段<线段>>... + self.中枢序列组: List[List[中枢],] = [] + for i in range(self.线段分析层次): + self.线段序列组.append(list() if not self.配置.图表展示 else 图表展示序列(self)) + self.中枢序列组.append(list() if not self.配置.图表展示 else 图表展示序列(self)) + + self.扩展线段序列组: List[List[虚线],] = [] # 扩展线段, 扩展线段<扩展线段>, 扩展线段<扩展线段<扩展线段>>... + self.扩展中枢序列组: List[List[中枢],] = [] + for i in range(self.扩展线段分析层次): + self.扩展线段序列组.append(list() if not self.配置.图表展示 else 图表展示序列(self)) + self.扩展中枢序列组.append(list() if not self.配置.图表展示 else 图表展示序列(self)) + + self.混合扩展线段序列组: List[List[虚线],] = [] # 扩展线段<线段>, 扩展线段<线段<线段>>, 扩展线段<线段<线段<线段>>>... + self.混合扩展中枢序列组: List[List[中枢],] = [] + for i in range(self.混合扩展线段分析层次): + self.混合扩展线段序列组.append(list() if not self.配置.图表展示 else 图表展示序列(self)) + self.混合扩展中枢序列组.append(list() if not self.配置.图表展示 else 图表展示序列(self)) def 读取任意数据(self, 魔法, **魔法参数): 魔法(**魔法参数) return 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线) + def 静态重新分析(self): self.买卖点字典 = dict() super().静态重新分析() @@ -497,15 +530,31 @@ class 观察者(观察者): 当前买卖点.备注 = 当前买卖点.备注 + "_非KDJ匹配" if not self.配置.买卖点激进识别 and not 买卖点分型.右: - return + pass # return if 当前买卖点.买卖点K线.时间戳 not in 活跃时间戳序列: 买卖点序列.add(当前买卖点) 当前买卖点.买卖点K线.买卖点信息.add(当前买卖点.备注) - print(当前买卖点, type(当前买卖点)) + print(当前买卖点, type(当前买卖点), 当前买卖点.备注) self.报信(当前买卖点, 指令.添加(当前买卖点.备注), sys._getframe().f_lineno) def 图表刷新(self): + def 报信(序列): + getattr(序列, "尾部刷新", Nil)(行号=-1) + + 报信(self.笔序列) + 报信(self.笔_中枢序列) + for i in range(self.线段分析层次): + 报信(self.线段序列组[i]) + 报信(self.中枢序列组[i]) + for i in range(self.扩展线段分析层次): + 报信(self.扩展线段序列组[i]) + 报信(self.扩展中枢序列组[i]) + for i in range(self.混合扩展线段分析层次): + 报信(self.混合扩展线段序列组[i]) + 报信(self.混合扩展中枢序列组[i]) + return + def 报信(序列): for 对象 in 序列[-3:]: self.报信(对象, 指令.添加(对象.标识), 0) @@ -568,43 +617,9 @@ class 观察者(观察者): "showLabel": False if 对象.偏移 <= 1 else True, } - if type(对象) is 虚线 and 对象.标识 == "笔" and not self.配置.推送笔: + if type(对象) in (虚线, 中枢, 线段特征) and not self.配置.展示标签(对象.标识): return - if type(对象) is 虚线 and self.配置.推送线段: - if 对象.标识 == "线段" and not self.配置.图表展示_线段: - return - - if 对象.标识 == "扩展线段" and not self.配置.图表展示_扩展线段: - return - - if 对象.标识 == "扩展线段<线段>" and not self.配置.图表展示_扩展线段_线段: - return - - if 对象.标识 == "线段<线段>" and not self.配置.图表展示_线段_线段: - return - - if 对象.标识 == "扩展线段<扩展线段>" and not self.配置.图表展示_扩展线段_线段: - return - - if type(对象) is 中枢 and self.配置.推送中枢: - if 对象.标识 == "中枢<笔>" and not self.配置.图表展示_中枢_笔: - return - if 对象.标识 == "中枢<线段>" and not self.配置.图表展示_中枢_线段: - return - if 对象.标识 == "中枢<扩展线段>" and not self.配置.图表展示_中枢_扩展线段: - return - if 对象.标识 == "中枢<扩展线段<线段>>" and not self.配置.图表展示_中枢_扩展线段_线段: - return - if 对象.标识 == "中枢<线段<线段>>" and not self.配置.图表展示_中枢_线段_线段: - return - - if 对象.标识 == "中枢<扩展线段<扩展线段>>" and not self.配置.图表展示_扩展线段_线段: - return - - if "_" in 对象.标识 and not self.配置.图表展示_中枢_线段内部: - return - if type(对象) in (虚线, 中枢, 线段特征): 图标 = 对象.图表标题 message["type"] = "shape" @@ -689,6 +704,11 @@ class 观察者(观察者): if not static_shapes: static_shapes = [] 全部 = [] + for o in dir(self): + if "序列" in o and "K线序列" not in o and "分型" not in o: + oo = getattr(self, o) + if isinstance(oo, list): + 全部.extend(oo) for o in self.买卖点字典.values(): 全部.extend(o) @@ -770,28 +790,31 @@ class 观察者(观察者): print(f"✅ 成功生成文件: {output_file}, 需要另行开启服务器 如 python -m http.server 8081") @classmethod - def 读取数据文件(cls, 文件路径: str, ws=None, 配置=缠论配置()) -> Self: - # btcusd-300-1631772074-1632222374.nb - print(文件路径) - if "_err-" in str(文件路径): - try: - 配置 = 缠论配置.加载配置(str(文件路径).replace(".nb", ".json")) - print("加载异常配置", 缠论配置().对比(配置)) - except: - pass + def 读取数据文件(cls, 文件路径: str, ws=None, 配置=缠论配置(), *, 观察员: Optional[观察者] = None) -> Self: + """加载数据文件 + :param 文件路径: 数据文件路径 格式如: btcusd-300-1631772074-1632222374.nb + :param 配置: 缠论配置 + :param 观察员: 可选,已有观察者实例;不传则自动创建 + :return: 观察者实例 + """ + if "_err-" in str(文件路径) and os.path.exists(str(文件路径).replace(".nb", ".json")): + 异常配置 = 缠论配置.加载配置(str(文件路径).replace(".nb", ".json")) + 差异 = 缠论配置().对比(异常配置) + 传入差异 = 缠论配置().对比(配置) + 传入差异.update(差异) + 配置 = 缠论配置(**传入差异) + print(f"加载异常配置+传入差异: {传入差异}") name = Path(文件路径).name.split(".")[0] 符号, 周期, 起始时间戳, 结束时间戳 = name.split("-") - 实例 = cls(符号=符号, 周期=int(周期), 数据通道=ws, 配置=配置) - - 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 实例 + if 观察员 is None: + 观察员 = cls(符号=符号, 周期=int(周期), 数据通道=ws, 配置=配置) + else: + 观察员.符号 = 符号 + 观察员.周期 = int(周期) + 观察员.配置 = 配置 + 观察员.加载本地数据(文件路径) + return 观察员 def 识别买卖点(self): """ @@ -804,317 +827,13 @@ class 观察者(观察者): if self.分型序列[-1].强度 not in "强中": pass - if 笔内部背驰判断(self.普通K线序列, self.笔序列[-1]): - 0 and self.添加买卖点("笔", self.笔序列[-1].武, "一", "次次级") - - if not self.线段序列: - return - # 观察者.判断线段第二买卖点(self.线段序列[-1], self) - if 笔内部背驰判断(self.普通K线序列, self.线段序列[-1]): - 0 and self.添加买卖点("笔", self.线段序列[-1].武, "一", "次级") - - if 线段背驰判断(self.普通K线序列, self.线段序列[-1]): - 0 and self.添加买卖点("线段", self.线段序列[-1].武, "一", "次级") - - if dif_三次穿越背离判断(self.普通K线序列, self.线段序列[-1]): - 0 and self.添加买卖点("macd_三次穿越", self.线段序列[-1].武, "一", "次级") - - if self.中枢序列: - 1 and 观察者.中枢第三买卖点(self.中枢序列[-1], self) - - if not self.线段_线段序列: - return - - 观察者.线段第二买卖点(self.线段_线段序列[-1], self) - 观察者.线段第二买卖点(self.线段序列[-1], self) - if 笔内部背驰判断(self.普通K线序列, self.线段_线段序列[-1]): - 0 and self.添加买卖点("笔", self.线段_线段序列[-1].武, "一", "本级") - - return - - @classmethod - def 判断线段第二买卖点(cls, 段: 虚线, 观察员: 观察者): - 实, 虚, 第三买卖线, _ = 线段.分割序列(段) - if len(虚) == 2: - 符合 = False - # 第一笔 穿越0轴 - 笔MACD特性 = 虚线.统计MACD行为(虚[0].获取普K序列(观察员), 8, 3) - if 虚[0].方向 is 相对方向.向上 and 笔MACD特性["DEA上穿0"] > 0: - 符合 = True - if 虚[0].方向 is 相对方向.向下 and 笔MACD特性["DEA下穿0"] > 0: - 符合 = True - - # 第二笔 不能穿越0轴 - 笔MACD特性 = 虚线.统计MACD行为(虚[1].获取普K序列(观察员), 8, 3) - if 虚[1].方向 is 相对方向.向上 and 笔MACD特性["DIF上穿0"] > 0: - 符合 = False - if 虚[1].方向 is 相对方向.向下 and 笔MACD特性["DIF下穿0"] > 0: - 符合 = False - - if 符合: - 特征 = "线段二第买卖点" - 买卖点分型 = 虚[1].武 - 虚[1].武.右 and 观察员.添加买卖点(特征, 买卖点分型, "二", "次级") - - @classmethod - def 笔中枢当前状态(cls, 当前中枢: "中枢", 观察员: "观察者"): - if 当前中枢.标识 != "中枢<笔>": - return None - 普K序列: List[K线] = 观察员.普通K线序列 - 配置 = 观察员.配置 - 状态 = 当前中枢.当前状态() - 进入段: 虚线 = 观察员.笔序列[观察员.笔序列.index(当前中枢.基础序列[0]) - 1] - 离开段: 虚线 = 当前中枢.基础序列[-1] - match 状态: - case "中枢之中": - pass - case "中枢之下" | "中枢之上": - if 进入段.方向 is 离开段.方向 and not 相对方向.分析(进入段.高, 进入段.低, 离开段.高, 离开段.低).是否包含(): - if 背驰分析.MACD背驰(进入段, 离开段, 普K序列) or 虚线.买卖意义(离开段, 观察员)[0]: - 特征 = "笔中枢" - 买卖点分型 = 离开段.武 - 第几 = "一" if ((离开段.方向 is 相对方向.向上 and 当前中枢.高高 <= 离开段.高) or (离开段.方向 is 相对方向.向下 and 当前中枢.低低 >= 离开段.低)) else "二" - 观察员.添加买卖点(特征, 买卖点分型, 第几, "同级") - 第三买卖线: 虚线 = 当前中枢.第三买卖线 - if 第三买卖线: - 买卖点分型 = 当前中枢.第三买卖线.武 - 同向均值 = 虚线.武之MACD均值_阴(普K序列, 第三买卖线) if 买卖点分型.结构 in (分型结构.底, 分型结构.下) else 虚线.武之MACD均值_阳(普K序列, 第三买卖线) - # if 同向均值 and 第三买卖线.武之MACD均值 and 第三买卖线.武.与MACD柱子匹配 and 第三买卖线.武.与MACD柱子分型匹配: - if 虚线.买卖意义(第三买卖线, 观察员)[0]: - 特征 = "笔中枢" - 观察员.添加买卖点(特征, 买卖点分型, "三", "同级") - case _: - raise RuntimeError("未知中枢状态", 状态) - - @classmethod - def 中枢当前状态(cls, 当前中枢: "中枢", 观察员: "观察者"): - if 当前中枢.标识 != "中枢<线段>": - return None - - 普K序列: List[K线] = 观察员.普通K线序列 - 配置 = 观察员.配置 - - 状态 = 当前中枢.当前状态() - 买卖点错过误差值 = 配置.买卖点错过误差值 - 实, 虚, 第三买卖线, _ = 线段.分割序列(当前中枢.基础序列[-1], 当前中枢) - - match 状态: - case "中枢之中": - """if not 虚: - if cls.判断线段内部是否背驰(当前中枢[-1], 观察员) and 当前中枢[-1].武.右 and cls.买卖意义(当前中枢[-1], 观察员)[0]: - 特征 = "中枢内背驰" - 买卖点分型 = 当前中枢[-1].武 - 观察员.添加买卖点(特征, 买卖点分型, "一", "次级")""" - - case "中枢之下" | "中枢之上": - if 当前中枢.本级_第三买卖线 is not None: # and len(当前中枢) >= 3 and len(虚) >= 2: - 买卖点分型 = None - if 当前中枢.完整性("合"): - if 状态 == "中枢之上": - 之后缠K序列 = 观察员.缠论K线序列[观察员.缠论K线序列.index(当前中枢.本级_第三买卖线.武.中) :] - 之后缠K = None - 中枢上轨 = 当前中枢.高 - if 当前中枢.本级_第三买卖线.武.中.标的K线.macd.DIF > 0: - for k in 之后缠K序列: - if k.标的K线.macd.DIF < 0: # 首个下穿0轴 - if 之后缠K is None: - 之后缠K = k - if 之后缠K: - if k.分型 is 分型结构.底 and k.标的K线.macd.DIF < 0: - 买卖点分型 = 分型.从缠K序列中获取分型(观察员.缠论K线序列, k) - break - - else: - # 中枢之下 - 之后缠K序列 = 观察员.缠论K线序列[观察员.缠论K线序列.index(当前中枢.本级_第三买卖线.武.中) :] - 之后缠K = None - 中枢下轨 = 当前中枢.低 - if 当前中枢.本级_第三买卖线.武.中.标的K线.macd.DIF < 0: - for k in 之后缠K序列: - if k.标的K线.macd.DIF > 0: # 首个上穿0轴 - if 之后缠K is None: - 之后缠K = k - if 之后缠K: - if k.分型 is 分型结构.顶 and k.标的K线.macd.DIF > 0: - 买卖点分型 = 分型.从缠K序列中获取分型(观察员.缠论K线序列, k) - break - - if 买卖点分型: - 特征 = "首次穿越0轴" - 观察员.添加买卖点(特征, 买卖点分型, "三", "本级") - - 买卖标的值 = 当前中枢.本级_第三买卖线.武.分型特征值 - if 虚线.买卖意义(当前中枢.本级_第三买卖线, 观察员)[0]: - 特征 = "中枢段笔" - 买卖点分型 = 当前中枢.本级_第三买卖线.武 - # 观察员.添加买卖点(特征, 买卖点分型, "三", "本级") - - else: - # 错过 - for 本级_第三买卖线 in 第三买卖线: - if (当前中枢.本级_第三买卖线 is not 本级_第三买卖线) and 虚线.买卖意义(本级_第三买卖线, 观察员)[0] and (买卖标的值 * (1 + 买卖点错过误差值) > 本级_第三买卖线.武.中.分型特征值 > 买卖标的值 * (1 - 买卖点错过误差值)): - 特征 = "中枢段笔" - 买卖点分型 = 本级_第三买卖线.武 - # 观察员.添加买卖点(特征, 买卖点分型, "三", "错过本级") - break - - if 当前中枢.第三买卖线 is None: - # 正在形成第三买卖点? - 特征 = 状态 - 离开段: 虚线 = 当前中枢.基础序列[-1] - """if not 虚: - # 正在离开中枢 - if not 当前中枢.完整性: - pass - else: - pass - - if cls.买卖意义(离开段[-1], 观察员)[0] or cls.判断线段内部是否背驰(离开段, 观察员) or cls.买卖意义(离开段[-1], 观察员)[0]: - 买卖点分型 = 离开段.武 - 观察员.添加买卖点(特征, 买卖点分型, "一", "同级") - else: - # 即将到来的 同级第三买卖点! - if (离开段.方向 is 相对方向.向下 and 虚[-1].武.分型特征值 < 离开段.文.分型特征值) or (离开段.方向 is 相对方向.向上 and 虚[-1].武.分型特征值 > 离开段.文.分型特征值): - # 一笔突破当前线段起点 - if cls.买卖意义(虚[-1], 观察员)[0]: - 买卖点分型 = 虚[-1].武 - 观察员.添加买卖点(特征, 买卖点分型, "一", "次次级")""" - - else: - # 第三买卖点已出现,可能的情况如下 - # 1.不当前中枢有任何的交集 - # 2.有交集 - # 1.与中枢 中高 中低 发生重叠 - # 2.与中枢 高高 高低 发生重叠 - # 3.失败重新进入当前中枢 - # 重点就是第三点,如何判断会回到中枢? - # 提出完整性的概念,最后离开段中内部中枢是否脱离中枢区间 - if not 当前中枢.完整性("合"): - return 状态, "不完整" - # if 当前中枢.第三买卖线.武 is not 观察员.分型序列[-1]: - # return 状态, "" - 特征 = "中枢段" - assert 相对方向.分析(当前中枢.高, 当前中枢.低, 当前中枢.第三买卖线.高, 当前中枢.第三买卖线.低).是否缺口() - 普K序列 = 当前中枢.第三买卖线.获取普K序列(观察员.观察员) - MACD特性 = 虚线.统计MACD行为(普K序列, 8, 3) - - if MACD特性["DEA上穿0"] > 0 and MACD特性["DEA下穿0"] > 0: - 特征 = "中枢段_DEA穿越2" - 买卖点分型 = 当前中枢.第三买卖线.武 - 观察员.添加买卖点(特征, 买卖点分型, "三", "同级") - - if 当前中枢.基础序列[-1].合_中枢序列 and 当前中枢.基础序列[-1].合_中枢序列[-1].基础序列[-1] is 当前中枢.第三买卖线.基础序列[-1]: - pass # return 状态, "前合中枢,线段首次形成" - - if 线段.判断线段内部是否背驰(当前中枢.第三买卖线, 观察员): # or 阳[-1]. 买卖意义[0]: - 买卖点分型 = 当前中枢.第三买卖线.武 - # 观察员.添加买卖点(特征, 买卖点分型, "三", "同级") - else: - return 状态, "同级,内部非背驰" - case _: - raise RuntimeError("未知中枢状态", 状态) - - @classmethod - def 中枢第三买卖点(cls, 当前中枢: "中枢", 观察员: "观察者"): - if 当前中枢.标识 != "中枢<线段>": - return None - 状态 = 当前中枢.当前状态() - if 状态 == "中枢之中": - return None - - 实, 虚, 第三买卖线, _ = 线段.分割序列(当前中枢.基础序列[-1], 当前中枢) - if 当前中枢.本级_第三买卖线 is None: - return None - - 买卖点分型 = None - if 当前中枢.完整性("合"): - 之后缠K序列 = 观察员.缠论K线序列[观察员.缠论K线序列.index(当前中枢.本级_第三买卖线.武.中) :] - 之后缠K = None - assert 之后缠K序列[0] is 当前中枢.本级_第三买卖线.武.中, (之后缠K序列[0], 当前中枢.本级_第三买卖线.武.中) - - if 状态 == "中枢之上": - 中枢上轨 = 当前中枢.高 - if 当前中枢.本级_第三买卖线.武.中.标的K线.macd.DIF > 0: - for k in 之后缠K序列: - if k.标的K线.macd.DIF < 0: # 首个下穿0轴 - if 之后缠K is None: - 之后缠K = k - if 之后缠K: - if k.分型 is 分型结构.底 and k.标的K线.macd.DIF < 0: - 买卖点分型 = 分型.从缠K序列中获取分型(观察员.缠论K线序列, k) - break - - else: - # 中枢之下 - 中枢下轨 = 当前中枢.低 - if 当前中枢.本级_第三买卖线.武.中.标的K线.macd.DIF < 0: - for k in 之后缠K序列: - if k.标的K线.macd.DIF > 0: # 首个上穿0轴 - if 之后缠K is None: - 之后缠K = k - if 之后缠K: - if k.分型 is 分型结构.顶 and k.标的K线.macd.DIF > 0: - 买卖点分型 = 分型.从缠K序列中获取分型(观察员.缠论K线序列, k) - break - - if 买卖点分型: - 特征 = "首次穿越0轴" - 观察员.添加买卖点(特征, 买卖点分型, "三", "本级") - - if 当前中枢.第三买卖线 is None: - return None - - if not 当前中枢.完整性("合"): - return None - - 特征 = "中枢段" - assert 相对方向.分析(当前中枢.高, 当前中枢.低, 当前中枢.第三买卖线.高, 当前中枢.第三买卖线.低).是否缺口() - 普K序列 = 当前中枢.第三买卖线.获取普K序列(观察员.观察员) - MACD特性 = 虚线.统计MACD行为(普K序列, 8, 3) - - if MACD特性["DEA上穿0"] > 0 and MACD特性["DEA下穿0"] > 0: - 特征 = "中枢段_DEA穿越2" - 买卖点分型 = 当前中枢.第三买卖线.武 - 观察员.添加买卖点(特征, 买卖点分型, "三", "同级") - - @classmethod - def 线段第二买卖点(cls, 当前线段: "虚线", 观察员: "观察者"): - if 当前线段.标识 not in ("线段<线段>", "线段"): - return - 实, 虚, _, _ = 线段.分割序列(当前线段, None) - if len(虚) != 2: - return - - (首, 尾) = 虚 - 首_MACD信息 = 虚线.统计MACD行为(首.获取普K序列(观察员)) - if 首.方向 is 相对方向.向下 and 首_MACD信息["DEA下穿0"] >= 1: - pass - elif 首.方向 is 相对方向.向上 and 首_MACD信息["DEA上穿0"] >= 1: - pass - else: - return - - 买卖点分型 = None - 尾_MACD信息 = 虚线.统计MACD行为(尾.获取普K序列(观察员)) - if 尾.方向 is 相对方向.向下 and 首_MACD信息["DEA下穿0"] >= 1: - 买卖点分型 = 尾.武 - elif 尾.方向 is 相对方向.向上 and 首_MACD信息["DEA上穿0"] >= 1: - 买卖点分型 = 尾.武 - else: - return - if not 买卖点分型: - return - 特征 = f"{当前线段.标识}第二" - 观察员.添加买卖点(特征, 买卖点分型, "二", "同级") - - __代码执行器_全局声明__ = dir() def 随机配置(随机源: Optional[random.Random] = None): """生成随机缠论配置,可传入独立的 Random 实例以保证线程安全""" rng = 随机源 if 随机源 is not None else random.Random() - return 缠论配置.from_dict( + return 缠论配置.不推送().from_dict( { "缠K合并替换": rng.choice((True, False)), "笔内元素数量": rng.randint(3, 9), @@ -1126,6 +845,8 @@ def 随机配置(随机源: Optional[random.Random] = None): "笔弱化_原始数量": rng.randint(3, 9), "线段_非缺口下穿刺": rng.choice((True, False)), "线段_特征序列忽视老阴老阳": rng.choice((True, False)), + "线段_修正": rng.choice((True, False)), + "线段_缺口后紧急修正": rng.choice((True, False)), "扩展线段_当下分析": rng.choice((True, False)), "买卖点激进识别": rng.choice((True, False)), "买卖点与MACD柱强相关": rng.choice((True, False)), @@ -1133,833 +854,17 @@ def 随机配置(随机源: Optional[random.Random] = None): ) -def dif_三次穿越背离判断(K线序列: List["K线"], 段: 虚线) -> bool: - """ - 基于 DIF 线穿越零轴的三次穿越背离判断,第三次穿越后检查 MACD 柱。 - - 向下模式: - 1. 第一次穿越:DIF 从正 → 负(下穿0轴) - 2. 第二次穿越:DIF 从负 → 正(上穿0轴),且上穿后的最高价 ≤ 第一次下穿前的最高价 - 3. 第三次穿越:DIF 从正 → 负(再次下穿0轴) - 检查第三次穿越后的连续 MACD 负值段内, - 价格最低点对应的 MACD 柱值 < 该段 MACD 柱最大值 → 返回 True - - 向上模式(对称): - 1. 第一次穿越:DIF 从负 → 正(上穿0轴) - 2. 第二次穿越:DIF 从正 → 负(下穿0轴),且下穿后的最低价 ≥ 第一次上穿前的最低价 - 3. 第三次穿越:DIF 从负 → 正(再次上穿0轴) - 检查第三次穿越后的连续 MACD 正值段内, - 价格最高点对应的 MACD 柱值 < 该段 MACD 柱最大值 → 返回 True - - 参数: - k线序列: K线对象列表,需包含 .close, .macd.DIF, .macd.MACD柱 - 方向: "向下" 或 "向上" - - 返回: - bool: 满足背离条件返回 True,否则 False - """ - - 方向 = "向上" if 段.方向.是否向上() else "向下" - k线序列: List[K线] = K线.截取(K线序列, 段.文.中.标的K线, 段.武.中.标的K线) - if len(k线序列) < 4: - return False - - dif_vals = [k.macd.DEA for k in k线序列] - macd_vals = [k.macd.MACD柱 for k in k线序列] - - # 1. 找出所有 DIF 穿越零轴的点 - 穿越点 = [] # (索引, 方向) - for i in range(1, len(dif_vals)): - prev, curr = dif_vals[i - 1], dif_vals[i] - if prev * curr < 0: - if prev > 0 and curr < 0: - 穿越点.append((i, "下穿")) - elif prev < 0 and curr > 0: - 穿越点.append((i, "上穿")) - - if len(穿越点) < 3: - return False - - # 2. 根据方向寻找连续符合顺序的三次穿越 - 期望序列 = ["下穿", "上穿", "下穿"] if 方向 == "向下" else ["上穿", "下穿", "上穿"] - found = None - for j in range(len(穿越点) - 2): - if 穿越点[j][1] == 期望序列[0] and 穿越点[j + 1][1] == 期望序列[1] and 穿越点[j + 2][1] == 期望序列[2]: - found = (穿越点[j][0], 穿越点[j + 1][0], 穿越点[j + 2][0]) - break - - if not found: - return False - - idx1, idx2, idx3 = found - - # 3. 向下模式 - if 方向 == "向下": - # 条件2:第二次上穿后的最高价 <= 第一次下穿前的最高价 - before = k线序列[:idx1] - if not before: - return False - max_before = max(k.高 for k in before) - - after = k线序列[idx2:] - if not after: - return False - max_after = max(k.高 for k in after) - - if max_after > max_before: - return False - - # 第三次穿越后,取连续 MACD 负值段 - segment = [] - for i in range(idx3, len(macd_vals)): - if macd_vals[i] < 0: - segment.append(k线序列[i]) - else: - break - if len(segment) < 2: - return False - - # 段内最低价及其对应的 MACD 柱值 - min_price = float("inf") - min_macd = None - for k in segment: - if k.高 < min_price: - min_price = k.高 - min_macd = k.macd.MACD柱 - max_macd = max(k.macd.MACD柱 for k in segment) - - return min_macd < max_macd - - # 4. 向上模式 - else: - # 条件2:第二次下穿后的最低价 >= 第一次上穿前的最低价 - before = k线序列[:idx1] - if not before: - return False - min_before = min(k.低 for k in before) - - after = k线序列[idx2:] - if not after: - return False - min_after = min(k.低 for k in after) - - if min_after < min_before: - return False - - # 第三次穿越后,取连续 MACD 正值段 - segment = [] - for i in range(idx3, len(macd_vals)): - if macd_vals[i] > 0: - segment.append(k线序列[i]) - else: - break - if len(segment) < 2: - return False - - # 段内最高价及其对应的 MACD 柱值 - max_price = -float("inf") - max_macd = None - for k in segment: - if k.低 > max_price: - max_price = k.低 - max_macd = k.macd.MACD柱 - max_macd_in_seg = max(k.macd.MACD柱 for k in segment) - - return max_macd < max_macd_in_seg - - -def 找首个MACD交叉前后K线(k线序列: List["K线"], 起始K线: "K线") -> Tuple[Optional["K线"], Optional["K线"]]: - """ - 在K线序列中,从起始K线之后查找第一个MACD快慢线交叉点(金叉或死叉), - 返回交叉点前一根K线和后一根K线。若未找到,返回(None, None)。 - - 参数: - k线序列: K线对象列表,按时间顺序排列 - 起始K线: 开始查找的位置 - """ - # 定位起始索引 - try: - start_idx = k线序列.index(起始K线) - except ValueError: - return None, None - - # 从起始K线的下一根开始,到倒数第二根结束(需要比较前后两根) - for i in range(start_idx, len(k线序列) - 1): - prev = k线序列[i] - curr = k线序列[i + 1] - - # 获取DIF和DEA值,若存在None则跳过 - dif_prev = prev.macd.DIF - dea_prev = prev.macd.DEA - dif_curr = curr.macd.DIF - dea_curr = curr.macd.DEA - - if None in (dif_prev, dea_prev, dif_curr, dea_curr): - continue - - # 金叉:前一根 DIF <= DEA,后一根 DIF > DEA - if dif_prev <= dea_prev and dif_curr > dea_curr: - return prev, curr - - # 死叉:前一根 DIF >= DEA,后一根 DIF < DEA - if dif_prev >= dea_prev and dif_curr < dea_curr: - return prev, curr - - return None, None - - -def 计算MACD柱子分段(k线序列: Sequence["K线"] = None) -> Tuple[List[List["K线"]], ...]: - if not k线序列: - return () - - def 符号(x: float) -> str: - if x > 0: - return "正" - else: - return "负" - - 当前符号 = 符号(k线序列[0].macd.MACD柱) - 当前段柱子 = [k线序列[0]] - 结果 = [] - - for i in range(1, len(k线序列)): - 新符号 = 符号(k线序列[i].macd.MACD柱) - if 新符号 == 当前符号: - 当前段柱子.append(k线序列[i]) - else: - 结果.append(当前段柱子) - 当前段柱子 = [k线序列[i]] - 当前符号 = 新符号 - - if 当前段柱子: - 结果.append(当前段柱子) - - 正 = [] - 负 = [] - for 序列 in 结果: - if 序列[-1].macd.MACD柱 > 0: - 正.append(序列) - else: - 负.append(序列) - return 正, 负 - - -def 笔内部背驰判断(K线序列: List[K线], 当前笔: 虚线) -> bool: - """ - 基于笔内部MACD柱的分段能量变化,判断是否发生内部背驰(端点可能转折)。 - 返回 True 表示出现内部背驰信号。 - """ - klines: List[K线] = K线.截取(K线序列, 当前笔.文.中.标的K线, 当前笔.武.中.标的K线) # 笔对象本身可迭代返回K线 - - if len(klines) < 3: - return False - - 正段, 负段 = 计算MACD柱子分段(klines) - # 按笔的方向选择相关段(向上笔看正段,向下笔看负段) - if 当前笔.方向 == 相对方向.向上: - 相关段 = 正段 - else: - 相关段 = 负段 - - if len(相关段) < 2: - return False - - # 计算每段的能量(代数和) - 能量 = [sum(k.macd.MACD柱 for k in seg) for seg in 相关段] - # 计算每段末端价格(向上笔用最高价,向下笔用最低价) - if 当前笔.方向 == 相对方向.向上: - 末端价格 = [seg[-1].高 for seg in 相关段] - # 价格必须逐段抬高,能量逐段减小 - return all(末端价格[i] < 末端价格[i + 1] for i in range(len(末端价格) - 1)) and all(能量[i] > 能量[i + 1] for i in range(len(能量) - 1)) - else: - 末端价格 = [seg[-1].低 for seg in 相关段] - # 价格必须逐段降低,能量绝对值逐段减小 - return all(末端价格[i] > 末端价格[i + 1] for i in range(len(末端价格) - 1)) and all(abs(能量[i]) > abs(能量[i + 1]) for i in range(len(能量) - 1)) - - -def 线段背驰判断(k线序列: List[K线], 段: 虚线) -> bool: - """线段内部背驰(比较最后一个中枢的进入段和离开段)""" - if not 段.合_中枢序列: - return False - zs = 段.合_中枢序列[-1] # 最后一个中枢 - # 中枢由三笔构成:左、中、右(方向交替) - 进入笔 = None - 离开笔 = None - # 找到中枢之前的同向笔 - for bi in reversed(段.笔序列[: 段.笔序列.index(zs.基础序列[0])]): - if bi.方向 == zs.基础序列[0].方向: - 进入笔 = bi - break - # 找到中枢之后的同向笔 - for bi in 段.笔序列[段.笔序列.index(zs.基础序列[-1]) + 1 :]: - if bi.方向 == zs.基础序列[0].方向: - 离开笔 = bi - break - if 进入笔 is None or 离开笔 is None: - return False - - # 价格条件 - if zs.基础序列[0].方向 == 相对方向.向上: - if 离开笔.高 <= 进入笔.高: - return False - else: - if 离开笔.低 >= 进入笔.低: - return False - - # 力度比较(MACD面积) - return 背驰分析.MACD背驰(离开笔, 进入笔, k线序列) - - -class 笔K线生成配置(BaseModel): - """笔的K线生成配置""" - - 最小K线数量: int = 5 # 一笔至少需要的K线数量 - 最大K线数量: int = 20 # 一笔最多K线数量 - 波动比例: float = 0.1 # 内部波动比例(相对于笔长度) - 包含K线比例: float = 0.3 # 包含关系K线比例 - 缺口概率: float = 0.1 # 出现缺口的概率 - 随机种子: Optional[int] = None # 随机种子(可重复) - - -class 笔结构类型(Enum): - 标准上涨笔 = "标准上涨笔" # 低->高,内部有回调 - 标准下跌笔 = "标准下跌笔" # 高->低,内部有反弹 - 单边上扬笔 = "单边上扬笔" # 几乎直线上升 - 单边下跌笔 = "单边下跌笔" # 几乎直线下降 - 震荡上涨笔 = "震荡上涨笔" # 大幅波动上升 - 震荡下跌笔 = "震荡下跌笔" # 大幅波动下降 - - -class 笔K线生成器: - """ - 根据笔的顶底数值生成K线序列 - """ - - def __init__(self, 配置: 笔K线生成配置 = 笔K线生成配置(), 分析器: Optional["观察者"] = None): - self.配置 = 配置 - self.分析器 = 分析器 - if 配置.随机种子: - seed(配置.随机种子) - - def _验证顶底交替(self, 顶底序列: List[float]) -> None: - """ - 验证顶底序列是否交替(高点-低点-高点 或 低点-高点-低点) - 这是缠论笔的基本要求 - """ - if len(顶底序列) < 3: - return # 至少3个点才能验证交替 - - for i in range(1, len(顶底序列) - 1): - 左 = 顶底序列[i - 1] - 中 = 顶底序列[i] - 右 = 顶底序列[i + 1] - - # 检查是否形成分型 - # 如果中是高点,左右应该是低点 - if 中 > 左 and 中 > 右: - # 中点是高点,左右应该是低点 - if not (左 < 中 and 右 < 中): - print(f"警告:位置{i}可能不是有效高点,左:{左}, 中:{中}, 右:{右}") - elif 中 < 左 and 中 < 右: - # 中点是低点,左右应该是高点 - if not (左 > 中 and 右 > 中): - print(f"警告:位置{i}可能不是有效低点,左:{左}, 中:{中}, 右:{右}") - else: - # 既不是高点也不是低点,不符合顶底交替 - raise ValueError(f"顶底序列不交替,位置{i}: 左={左}, 中={中}, 右={右}\n序列应该交替出现高点和低点") - - def 生成K线序列(self, 顶底序列: List[float], 起始时间: datetime, 周期: int = 60) -> List[K线]: - """ - 根据顶底序列生成完整的K线序列 - """ - if len(顶底序列) < 2: - raise ValueError("顶底序列至少需要2个点") - - # 确保顶底交替(可选,如果确定序列是标准的可以跳过) - try: - self._验证顶底交替(顶底序列) - except ValueError as e: - print(f"顶底序列验证失败,但仍继续生成: {e}") - # 可以选择继续,或者处理成更标准的序列 - - K线序列 = [] - 当前时间 = 起始时间 - - # 生成每段笔的K线 - for i in range(len(顶底序列) - 1): - 起点价格 = 顶底序列[i] - 终点价格 = 顶底序列[i + 1] - - # 判断笔方向 - if 起点价格 < 终点价格: - 笔类型 = 笔结构类型.标准上涨笔 - else: - 笔类型 = 笔结构类型.标准下跌笔 - - # 生成这笔的K线 - 笔K线 = self._生成单笔K线(起点价格, 终点价格, 笔类型, 当前时间, 周期) - - # 添加到总序列 - K线序列.extend(笔K线) - - # 更新时间(最后一个K线的时间 + 周期) - if 笔K线: - 当前时间 = 笔K线[-1].时间戳 + timedelta(seconds=周期) - - if self.分析器: - for k线 in 笔K线: - self.分析器.增加原始K线(k线) - - return K线序列 - - def _生成单笔K线(self, 起点: float, 终点: float, 笔类型: 笔结构类型, 起始时间: datetime, 周期: int) -> List[K线]: - """ - 生成单笔的内部K线结构 - """ - # 确定K线数量 - K线数量 = randint(self.配置.最小K线数量, self.配置.最大K线数量) - - # 计算笔的总幅度 - abs(终点 - 起点) - - # 根据笔类型选择生成策略 - if 笔类型 in [笔结构类型.标准上涨笔, 笔结构类型.标准下跌笔]: - return self._生成标准笔K线(起点, 终点, K线数量, 起始时间, 周期, 笔类型) - elif 笔类型 in [笔结构类型.单边上扬笔, 笔结构类型.单边下跌笔]: - return self._生成单边笔K线(起点, 终点, K线数量, 起始时间, 周期, 笔类型) - else: # 震荡笔 - return self._生成震荡笔K线(起点, 终点, K线数量, 起始时间, 周期, 笔类型) - - def _生成标准笔K线(self, 起点: float, 终点: float, K线数量: int, 起始时间: datetime, 周期: int, 笔类型: 笔结构类型) -> List[K线]: - """ - 生成标准笔的K线(有回调/反弹) - """ - K线列表 = [] - 当前时间 = 起始时间 - - # 计算每步的基础变动 - 总变动 = 终点 - 起点 - 基础步长 = 总变动 / (K线数量 - 1) if K线数量 > 1 else 总变动 - - # 确定主要方向 - 是上涨笔 = 笔类型 == 笔结构类型.标准上涨笔 - - # 生成每根K线 - for i in range(K线数量): - # 基础目标价格(线性) - 基础目标 = 起点 + 基础步长 * i - - # 添加波动 - if i == 0 or i == K线数量 - 1: - # 起点和终点波动小 - 波动范围 = abs(总变动) * 0.02 - else: - # 中间波动大 - 波动范围 = abs(总变动) * self.配置.波动比例 - - # 生成K线的四个价格 - if 是上涨笔: - 开, 高, 低, 收 = self._生成上涨K线价格(基础目标, 波动范围, i, K线数量) - else: - 开, 高, 低, 收 = self._生成下跌K线价格(基础目标, 波动范围, i, K线数量) - - # 确保起点和终点准确 - if i == 0: - if 是上涨笔: - 低 = min(低, 起点) - 开 = 起点 # 上涨笔起点是低点 - 收 = max(开, 收) # 确保上涨 - else: - 高 = max(高, 起点) - 开 = 起点 # 下跌笔起点是高点 - 收 = min(开, 收) # 确保下跌 - - elif i == K线数量 - 1: - if 是上涨笔: - 高 = max(高, 终点) - 收 = 终点 # 上涨笔终点是高点 - else: - 低 = min(低, 终点) - 收 = 终点 # 下跌笔终点是低点 - - # 创建K线 - k线 = K线.创建普K( - 标识="随机", - 序号=len(K线列表), - 时间戳=当前时间, - 开盘价=开, - 最高价=高, - 最低价=低, - 收盘价=收, - 成交量=uniform(100, 1000), - 周期=周期, - ) - - K线列表.append(k线) - 当前时间 += timedelta(seconds=周期) - - # 后处理:确保笔的起点和终点准确 - if K线列表: - self._修正笔端点(K线列表, 起点, 终点, 是上涨笔) - - return K线列表 - - def _生成单边笔K线(self, 起点: float, 终点: float, K线数量: int, 起始时间: datetime, 周期: int, 笔类型: 笔结构类型) -> List[K线]: - """ - 生成单边笔的K线(几乎直线上升/下降,回调很小) - """ - K线列表 = [] - 当前时间 = 起始时间 - - # 确定方向 - 是上涨笔 = 笔类型 == 笔结构类型.单边上扬笔 - - # 计算总变动和每步变动 - 总变动 = 终点 - 起点 - - for i in range(K线数量): - # 线性进展 - 进度 = i / max(K线数量 - 1, 1) - 基础价格 = 起点 + 总变动 * 进度 - - # 单边笔的波动很小 - 波动范围 = abs(总变动) * 0.01 # 只有1%的波动 - - # 生成K线价格 - if 是上涨笔: - # 上涨笔:大部分是阳线 - if random.random() < 0.8: # 80%阳线 - 开 = 基础价格 - 波动范围 * 0.2 - 收 = 基础价格 + 波动范围 * 0.3 - else: - 开 = 基础价格 + 波动范围 * 0.2 - 收 = 基础价格 - 波动范围 * 0.1 - else: - # 下跌笔:大部分是阴线 - if random.random() < 0.8: # 80%阴线 - 开 = 基础价格 + 波动范围 * 0.2 - 收 = 基础价格 - 波动范围 * 0.3 - else: - 开 = 基础价格 - 波动范围 * 0.2 - 收 = 基础价格 + 波动范围 * 0.1 - - # 计算高低点 - 高 = max(开, 收) + 波动范围 * 0.1 - 低 = min(开, 收) - 波动范围 * 0.1 - - # 修正第一根和最后一根 - if i == 0: - if 是上涨笔: - 低 = min(低, 起点) - 开 = 起点 - else: - 高 = max(高, 起点) - 开 = 起点 - elif i == K线数量 - 1: - if 是上涨笔: - 高 = max(高, 终点) - 收 = 终点 - else: - 低 = min(低, 终点) - 收 = 终点 - - # 创建K线 - k线 = K线.创建普K( - 标识="随机", - 序号=len(K线列表), - 时间戳=当前时间, - 开盘价=开, - 最高价=高, - 最低价=低, - 收盘价=收, - 成交量=uniform(80, 600), - 周期=周期, - ) - - K线列表.append(k线) - 当前时间 += timedelta(seconds=周期) - - return K线列表 - - def _生成震荡笔K线(self, 起点: float, 终点: float, K线数量: int, 起始时间: datetime, 周期: int, 笔类型: 笔结构类型) -> List[K线]: - """ - 生成震荡笔的K线(大幅波动上升/下降) - """ - K线列表 = [] - 当前时间 = 起始时间 - - # 确定方向 - 是上涨笔 = 笔类型 == 笔结构类型.震荡上涨笔 - - # 计算总变动 - 总变动 = 终点 - 起点 - - # 震荡笔有较大的回调 - abs(总变动) * 0.3 # 回调30% - - for i in range(K线数量): - # 基础线性进展 - 基础进度 = i / max(K线数量 - 1, 1) - 基础价格 = 起点 + 总变动 * 基础进度 - - # 添加较大的震荡 - 震荡幅度 = abs(总变动) * 0.15 # 15%的震荡 - - # 正弦波模拟震荡 - if K线数量 > 1: - 震荡 = math.sin(i * 2 * math.pi / K线数量) * 震荡幅度 - else: - 震荡 = 0 - - 震荡价格 = 基础价格 + 震荡 - - # 生成K线价格 - 波动范围 = abs(总变动) * 0.1 - if 是上涨笔: - # 震荡上涨:随机阴阳线 - if random.random() < 0.5: - 开 = 震荡价格 - 波动范围 * 0.4 - 收 = 震荡价格 + 波动范围 * 0.3 - else: - 开 = 震荡价格 + 波动范围 * 0.3 - 收 = 震荡价格 - 波动范围 * 0.2 - else: - # 震荡下跌:随机阴阳线 - if random.random() < 0.5: - 开 = 震荡价格 + 波动范围 * 0.4 - 收 = 震荡价格 - 波动范围 * 0.3 - else: - 开 = 震荡价格 - 波动范围 * 0.3 - 收 = 震荡价格 + 波动范围 * 0.2 - - # 计算高低点(震荡笔的高低点差异大) - 高 = max(开, 收) + 波动范围 * 0.3 - 低 = min(开, 收) - 波动范围 * 0.3 - - # 修正端点 - if i == 0: - if 是上涨笔: - 低 = min(低, 起点) - 开 = 起点 - else: - 高 = max(高, 起点) - 开 = 起点 - elif i == K线数量 - 1: - if 是上涨笔: - 高 = max(高, 终点) - 收 = 终点 - else: - 低 = min(低, 终点) - 收 = 终点 - - # 创建K线 - k线 = K线.创建普K( - 标识="随机", - 序号=len(K线列表), - 时间戳=当前时间, - 开盘价=开, - 最高价=高, - 最低价=低, - 收盘价=收, - 成交量=uniform(150, 1200), - 周期=周期, - ) - - K线列表.append(k线) - 当前时间 += timedelta(seconds=周期) - - return K线列表 - - def _生成上涨K线价格(self, 基础价: float, 波动范围: float, 索引: int, 总数: int) -> Tuple[float, float, float, float]: - """ - 生成上涨笔中的单根K线价格 - """ - # 确定K线类型 - if 索引 == 0: - # 第一根:通常是阳线 - K线类型 = "阳线" - elif 索引 == 总数 - 1: - # 最后一根:可能是阴线(形成顶分型) - K线类型 = choice(["阳线", "阴线"]) - else: - # 中间:随机,但偏阳线 - K线类型 = choices(["阳线", "阴线", "十字星"], weights=[0.6, 0.3, 0.1])[0] - - # 生成价格 - 波动 = uniform(-波动范围, 波动范围) - 中心价 = 基础价 + 波动 - - if K线类型 == "阳线": - # 低开高收 - 开 = 中心价 - 波动范围 * 0.3 - 收 = 中心价 + 波动范围 * 0.3 - elif K线类型 == "阴线": - # 高开低收 - 开 = 中心价 + 波动范围 * 0.3 - 收 = 中心价 - 波动范围 * 0.3 - else: # 十字星 - 开 = 中心价 - 波动范围 * 0.1 - 收 = 中心价 + 波动范围 * 0.1 - - # 计算高低点 - 低 = min(开, 收) - 波动范围 * 0.2 - 高 = max(开, 收) + 波动范围 * 0.2 - - # 确保高低点合理 - if 高 <= 低: - 高, 低 = 低 + 波动范围 * 0.1, 高 - 波动范围 * 0.1 - - return 开, 高, 低, 收 - - def _生成下跌K线价格(self, 基础价: float, 波动范围: float, 索引: int, 总数: int) -> Tuple[float, float, float, float]: - """ - 生成下跌笔中的单根K线价格 - """ - if 索引 == 0: - K线类型 = "阴线" # 第一根通常是阴线 - elif 索引 == 总数 - 1: - K线类型 = choice(["阴线", "阳线"]) # 最后一根可能阳线 - else: - K线类型 = choices(["阴线", "阳线", "十字星"], weights=[0.6, 0.3, 0.1])[0] - - 波动 = uniform(-波动范围, 波动范围) - 中心价 = 基础价 + 波动 - - if K线类型 == "阴线": - 开 = 中心价 + 波动范围 * 0.3 - 收 = 中心价 - 波动范围 * 0.3 - elif K线类型 == "阳线": - 开 = 中心价 - 波动范围 * 0.3 - 收 = 中心价 + 波动范围 * 0.3 - else: # 十字星 - 开 = 中心价 - 波动范围 * 0.1 - 收 = 中心价 + 波动范围 * 0.1 - - 低 = min(开, 收) - 波动范围 * 0.2 - 高 = max(开, 收) + 波动范围 * 0.2 - - if 高 <= 低: - 高, 低 = 低 + 波动范围 * 0.1, 高 - 波动范围 * 0.1 - - return 开, 高, 低, 收 - - def _修正笔端点(self, K线列表: List[K线], 起点: float, 终点: float, 是上涨笔: bool): - """ - 修正笔的起点和终点,确保准确 - """ - if not K线列表: - return - - # 修正起点 - 第一根 = K线列表[0] - if 是上涨笔: - 第一根.低 = min(第一根.低, 起点) - 第一根.开盘价 = 起点 - 第一根.收盘价 = max(第一根.收盘价, 第一根.开盘价) - else: - 第一根.高 = max(第一根.高, 起点) - 第一根.开盘价 = 起点 - 第一根.收盘价 = min(第一根.收盘价, 第一根.开盘价) - - # 修正终点 - 最后一根 = K线列表[-1] - if 是上涨笔: - 最后一根.高 = max(最后一根.高, 终点) - 最后一根.收盘价 = 终点 - else: - 最后一根.低 = min(最后一根.低, 终点) - 最后一根.收盘价 = 终点 - - -def 测试_读取数据(symbol: str = "btcusd", limit: int = 500, freq: SupportsInt = 时间周期.分(5), ws: Optional[WebSocket] = None, 配置: 缠论配置 = 缠论配置(线段内部中枢图显=False), 文件路径: str = "./templates/btcusd_ex-1800-1685795400-1713488400.nb"): - def 魔法(): - 观察员 = 观察者.读取数据文件(文件路径, ws) - 观察员.图表刷新() - return 观察员 - - return 魔法 - - -def 从序列中机选( - 数量: int, - 可选方向: List["相对方向"], - 可重复: bool = True, # 是否允许重复选择 -) -> Generator["相对方向", None, None]: - if not 可重复 and 数量 > len(可选方向): - raise ValueError("数量超过可选方向数") - - if 可重复: - while 数量 > 0: - yield choice(可选方向) - 数量 -= 1 - else: - yield from random.sample(可选方向, 数量) # 使用random.sample - - -def 根据当前K线生成新K线(self, 方向: 相对方向, 居中: bool = False) -> "K线": - 时间偏移 = timedelta(seconds=self.周期) - 时间戳: datetime = self.时间戳 + self.周期 # 时间偏移 - 成交量: float = 998 - 高: float = 0 - 低: float = 0 - 高低差 = self.高 - self.低 - match 方向: - case 相对方向.向上: - 偏移 = 高低差 * 0.5 if 居中 else randint(int(高低差 * 0.1279), int(高低差 * 0.883)) - 低 = self.低 + 偏移 - 高 = self.高 + 偏移 - case 相对方向.向下: - 偏移 = 高低差 * 0.5 if 居中 else randint(int(高低差 * 0.1279), int(高低差 * 0.883)) - 低 = self.低 - 偏移 - 高 = self.高 - 偏移 - case 相对方向.向上缺口: - 偏移 = 高低差 * 1.5 if 居中 else randint(int(高低差 * 1.1279), int(高低差 * 1.883)) - 低 = self.低 + 偏移 - 高 = self.高 + 偏移 - case 相对方向.向下缺口: - 偏移 = 高低差 * 1.5 if 居中 else randint(int(高低差 * 1.1279), int(高低差 * 1.883)) - 低 = self.低 - 偏移 - 高 = self.高 - 偏移 - case 相对方向.衔接向上: - 偏移 = self.高 - self.低 - 高 = self.高 + 偏移 - 低 = self.高 - case 相对方向.衔接向下: - 偏移 = self.高 - self.低 - 高 = self.低 - 低 = self.低 - 偏移 - - try: - 小数点 = [len(str(n).split(".")[-1]) for n in (self.开盘价, self.高, self.低, self.收盘价)] - except: - 小数点 = [2, 1] - 新K线 = K线.创建普K( - 标识=self.标识, - 时间戳=时间戳, - 开盘价=round(uniform(高, 低), max(小数点)), - 最高价=round(高, max(小数点)), - 最低价=round(低, max(小数点)), - 收盘价=round(uniform(高, 低), max(小数点)), - 成交量=成交量 * random.random(), - 序号=self.序号 + 1, - 周期=self.周期, - ) - - # assert 相对方向.分析(self, 新K线) is 方向, (方向, 相对方向.分析(self, 新K线)) - return 新K线 - - def 测试_随机生成(symbol: str = "btcusd", limit: int = 5000, freq: SupportsInt = 时间周期.分(5), ws: Optional[WebSocket] = None, 配置: 缠论配置 = 缠论配置()): def 魔法(): 随机生成实例 = 观察者(symbol + "_gen", 周期=int(freq), 数据通道=ws, 配置=配置) dt = datetime(2008, 8, 8) 原始K线 = K线.创建普K("随机", int(dt.timestamp()), 8888.55, 10000.00, 9000.22, 9527.33, 888, 0, int(freq)) 随机生成实例.增加原始K线(原始K线) - for 方向 in 从序列中机选( + for 方向 in 相对方向.从序列中机选( int(limit), [相对方向.向上, 相对方向.向上缺口, 相对方向.衔接向上, 相对方向.向下, 相对方向.向下缺口, 相对方向.衔接向下], ): - 原始K线 = 根据当前K线生成新K线(原始K线, 方向) + 原始K线 = 原始K线.根据当前K线生成新K线(方向) 随机生成实例.增加原始K线(原始K线) 折线 = [元素.文.分型特征值 for 元素 in 随机生成实例.笔序列] @@ -1971,23 +876,6 @@ def 测试_随机生成(symbol: str = "btcusd", limit: int = 5000, freq: Support return 魔法 -def 测试_笔生成器( - symbol: str = "btcusd", - limit: int = 500, - freq: SupportsInt = 时间周期.分(5), - ws: Optional[WebSocket] = None, - 顶底序列=[100, 200, 150, 250, 200, 300, 250, 350], - 配置: 缠论配置 = 缠论配置(), -): - def 魔法(): - 分析器 = 观察者(符号=symbol, 周期=int(freq), 数据通道=ws, 配置=配置) - 生成器 = 笔K线生成器(笔K线生成配置(最小K线数量=5, 最大K线数量=12, 波动比例=0.15, 随机种子=random.Random(os.urandom(64)).randint(0, 999999999)), 分析器) - 生成器.生成K线序列(顶底序列, datetime(2024, 1, 1, 9, 30, 0), 周期=int(freq)) - return 分析器 - - return 魔法 - - class Bitstamp: @classmethod def init(cls, 观察员_, size): @@ -2022,7 +910,6 @@ class Bitstamp: _next = end if len(data["data"]["ohlc"]) < 100: break - 观察员.测试_保存数据(str(Path(__file__).parent)) 折线 = [元素.文.分型特征值 for 元素 in 观察员.笔序列] 折线.append(观察员.笔序列[-1].武.分型特征值) # print(折线) @@ -2174,11 +1061,10 @@ def 同步_跟踪回测(观察员: 观察者, 数据源: bt.feed.DataBase): print("最终资金:", 最终资金, (最终资金 - 初始资金) / 初始资金) -def 测试_读取数据(symbol: str = "btcusd", limit: int = 500, freq: SupportsInt = 时间周期.分(5), ws: Optional[WebSocket] = None, 配置: 缠论配置 = 缠论配置(线段内部中枢图显=False), 文件路径: str = "./templates/btcusd_ex-1800-1685795400-1713488400.nb"): +def 测试_读取数据(观察员, ws: Optional[WebSocket] = None, 配置: 缠论配置 = 缠论配置(线段内部中枢图显=False)): def 魔法(): 启动时间 = datetime.now() - print(观察者) - 观察员 = 观察者.读取数据文件(配置.加载文件路径, ws, 配置) + 观察者.读取数据文件(配置.加载文件路径, ws, 配置, 观察员=观察员) # 观察员.分部分析() 消耗用时 = datetime.now() - 启动时间 print(消耗用时) @@ -2233,7 +1119,7 @@ def 测试_邮局数据_同步回测(symbol: str = "btcusd", limit: int = 500, f return 魔法 -def 测试_周期合成(symbol: str = "btcusd", limit: int = 500, freq: SupportsInt = 时间周期.分(5), ws: Optional[WebSocket] = None, 配置: 缠论配置 = 缠论配置(), 配置组: Dict[int:缠论配置] = None): +def 测试_周期合成(symbol: str = "btcusd", limit: int = 500, freq: SupportsInt = 时间周期.分(5), ws: Optional[WebSocket] = None, 配置: 缠论配置 = 缠论配置(), 配置组: Dict[int, 缠论配置] = None): def 魔法(): 周期组 = [int(freq), int(freq) * 5, int(freq) * 5 * 6] 多级别分析 = 立体分析器(symbol, 周期组, ws, 配置, 配置组) @@ -2578,7 +1464,10 @@ async def 处理图表消息(用户标识: str, 消息字典: Dict, websocket: W generator = 消息字典.get("generator", "True") config = 消息字典.get("config", dict()) + print("RAW config:", repr(config)) 当前配置 = 缠论配置.from_dict(config) + print("", 当前配置.to_dict()) + 差异 = 缠论配置().对比(当前配置) print(差异) 配置组 = 缠论配置.按序号重组字典(当前配置, config) @@ -2591,6 +1480,7 @@ async def 处理图表消息(用户标识: str, 消息字典: Dict, websocket: W time.sleep(1) 主线程 = None + 观察员 = 观察者("", 60, websocket, 当前配置) # 创建新的分析器 if generator == "zqhc": 魔法 = 测试_周期合成(symbol=symbol, freq=freq, limit=limit, ws=websocket, 配置=当前配置, 配置组=配置组) @@ -2598,7 +1488,7 @@ async def 处理图表消息(用户标识: str, 消息字典: Dict, websocket: W 魔法 = 测试_邮局数据_同步回测(symbol=symbol, freq=freq, limit=limit, ws=websocket, 配置=当前配置) elif generator == "ex": - 魔法 = 测试_读取数据(symbol=symbol, freq=freq, limit=limit, ws=websocket, 配置=当前配置) + 魔法 = 测试_读取数据(观察员=观察员, ws=websocket, 配置=当前配置) elif generator == "last": 魔法 = 测试_读取上一次数据(名称=symbol, 数量=limit, 周期=freq, ws=websocket, 配置=当前配置) @@ -2610,7 +1500,11 @@ async def 处理图表消息(用户标识: str, 消息字典: Dict, websocket: W def 数据加载线程(): try: - 全局连接管理器.设置图表观察员(用户标识, 魔法()) + if generator == "ex": + 全局连接管理器.设置图表观察员(用户标识, 观察员) + 魔法() + else: + 全局连接管理器.设置图表观察员(用户标识, 魔法()) print(f"[分析器] 用户 {用户标识} 的分析器已启动") except Exception as e: traceback.print_exc() @@ -2690,7 +1584,6 @@ async def 处理图表消息(用户标识: str, 消息字典: Dict, websocket: W 数据类型, 线序, 虚实合, 类型 = 数据类型.split("_") 段序号 = int(线序) - if 数据类型 == "线段": 段: 虚线 = 观察员.线段序列[段序号] zs = getattr(段, f"{虚实合}_中枢序列")[序号] @@ -2876,83 +1769,397 @@ async def 主页( ) -def 测试_读取数据2(配置: 缠论配置): - """测试_读取数据 +chanlun.set_rs_log_level("error") +chanlun.set_log_level("error") +set_log_level("error") - :param 配置: 缠论配置 - :return: 测试函数 + +def _检测nb周期(nb_path: str) -> Optional[int]: + """从 .nb 文件前两根K线的时间差检测周期(秒)""" + import struct + + try: + with open(nb_path, "rb") as f: + buf = f.read(96) + if len(buf) < 96: + return None + t0 = struct.unpack(">d", buf[0:8])[0] + t1 = struct.unpack(">d", buf[48:56])[0] + return int(t1 - t0) + except Exception: + return None + + +def 运行信号驱动策略( + symbol: str = "btcusd", + bars: int = 500, + nb_path: Optional[str] = None, + max_batches: int = 5, + warmup: int = 50, + base_period: int = 86400, + higher_periods: tuple = (604800,), +): + """信号驱动策略快速启动 — Backtrader 回测 + + 使用方式:: + + # CLI: python main.py --mode strategy --symbol btcusd --bars 1000 + # 代码: 运行信号驱动策略(symbol="btcusd", bars=500, nb_path="templates/last.nb") + + 开平仓信号使用 MA 均线多头策略(看多向上开多,看空平多)。 + + :param symbol: 交易品种 + :param bars: 回测 K 线数 + :param nb_path: .nb 数据文件路径,为 None 时使用随机数据 + :param max_batches: 最大同时持仓批次数 + :param warmup: 数据预热周期 + :param base_period: 基础周期(秒),默认 86400(日线) + :param higher_periods: 高级周期组(秒),默认 (604800,)(周线) """ + start = datetime.now() - def 魔法(): - 启动时间 = datetime.now() - 观察员 = 观察者.读取数据文件(配置.加载文件路径, 配置) - 消耗用时 = datetime.now() - 启动时间 - print("测试_读取数据 耗时", 消耗用时, "普K数量", len(观察员.普通K线序列)) - return 观察员 + # ---- 数据源(先检测周期,再构建策略) ---- + if nb_path and os.path.exists(nb_path): + nb_period = _检测nb周期(nb_path) + if nb_period: + base_period = nb_period + higher_periods = tuple(p for p in higher_periods if p > nb_period) + if not higher_periods: + higher_periods = (nb_period * 5,) + print(f"数据源: {nb_path} (周期={base_period}s, 高级周期={higher_periods})") + data = NB数据源(nb_path, 最大条数=bars) + else: + print(f"数据源: 随机生成 ({bars} 根)") + data = 随机数据(start_date=datetime(2020, 1, 1), 最大条数=bars) - return 魔法 + # ---- Backtrader 回测 ---- + cerebro = bt.Cerebro(stdstats=False) + cerebro.adddata(data) + + cerebro.addstrategy( + 信号驱动策略, + 符号=symbol, + 基础周期=base_period, + 高级周期组=higher_periods, + 持仓=[ + cet.Position( + symbol=symbol, + name="中枢", + opens=[ + cet.Event( + cet.Operate.LO, + factors=[ + cet.Factor(signals_all=[cet.Signal(k1=str(base_period), k2="D1MO3", k3="中枢段DEA穿越2V230602", v2="三买")]), + cet.Factor(signals_all=[cet.Signal(k1=str(base_period), k2="D1MO3", k3="DEA穿越0轴V230602", v2="三买")]), + cet.Factor(signals_all=[cet.Signal(k1=str(base_period), k2="D1MO3", k3="首次穿越0轴V230602", v2="三买")]), + ], + ) + ], + exits=[ + cet.Event( + cet.Operate.LE, + factors=[ + cet.Factor(signals_all=[cet.Signal(k1=str(base_period), k2="D1MO3", k3="中枢段DEA穿越2V230602", v2="三卖")]), + cet.Factor(signals_all=[cet.Signal(k1=str(base_period), k2="D1MO3", k3="DEA穿越0轴V230602", v2="三卖")]), + cet.Factor(signals_all=[cet.Signal(k1=str(base_period), k2="D1MO3", k3="首次穿越0轴V230602", v2="三卖")]), + ], + ) + ], + ), + cet.Position( + symbol=symbol, + name="中枢", + exits=[ + cet.Event( + cet.Operate.LO, + factors=[ + cet.Factor(signals_all=[cet.Signal(k1=str(base_period), k2="D1MO3", k3="中枢段DEA穿越2V230602", v2="三买")]), + cet.Factor(signals_all=[cet.Signal(k1=str(base_period), k2="D1MO3", k3="DEA穿越0轴V230602", v2="三买")]), + cet.Factor(signals_all=[cet.Signal(k1=str(base_period), k2="D1MO3", k3="首次穿越0轴V230602", v2="三买")]), + ], + ) + ], + opens=[ + cet.Event( + cet.Operate.LE, + factors=[ + cet.Factor(signals_all=[cet.Signal(k1=str(base_period), k2="D1MO3", k3="中枢段DEA穿越2V230602", v2="三卖")]), + cet.Factor(signals_all=[cet.Signal(k1=str(base_period), k2="D1MO3", k3="DEA穿越0轴V230602", v2="三卖")]), + cet.Factor(signals_all=[cet.Signal(k1=str(base_period), k2="D1MO3", k3="首次穿越0轴V230602", v2="三卖")]), + ], + ) + ], + ), + ], + 投喂预热数=2, + 指标计算=True, + ) + + cerebro.broker.setcash(100000.0) + print(f"初始资金: {cerebro.broker.getvalue():,.0f}") + + results = cerebro.run() + strat = results[0] + elapsed = datetime.now() - start + + # ---- 结果输出 ---- + print(f"\n{'=' * 60}") + print(f" 信号驱动策略 回测结果") + print(f"{'=' * 60}") + print(f" 品种: {symbol} 周期: {base_period}s K线数: {strat.投喂计数}") + print(f" 耗时: {elapsed}") + 最终资金 = cerebro.broker.getvalue() + 收益 = (最终资金 - 100000.0) / 100000.0 * 100 + print(f"\n 最终资金: {最终资金:,.0f} | 收益率: {收益:+.2f}%") -def 测试_周期合成2(配置: 缠论配置, 配置组: Dict[int, 缠论配置] = dict()): - """测试_周期合成 +def 运行同步信号驱动策略( + symbol: str = "btcusd", + bars: int = 500, + nb_path: Optional[str] = None, + max_batches: int = 5, + warmup: int = 50, + base_period: int = 86400, + higher_periods: tuple = (604800,), +): + """同步回测 — 实盘运算模式。 - :param 配置: 默认配置 - :param 配置组: 各周期独立配置 - :return: 测试函数 + 数据逐根到达,``信号驱动策略`` 在每根 bar 上只能看到已投喂的数据。 + 与 :func:`运行信号驱动策略` 使用**同一个** :class:`信号驱动策略`, + 区别仅在于数据投喂方式: + + - **strategy 模式**:Backtrader 预加载全部数据,一次性回测 + - **sync 模式**:观察者在后台线程逐根喂 K 线 → 队列 → Backtrader 逐根消费 + + 数据流:: + + 后台线程: 数据源 → 观察员.增加原始K线(k) → 数据队列.put(OHLCV) + 主线程: 自定义实时数据源._load() → 队列.get() → bar + → 信号驱动策略.next() + → 立体分析器.投喂K线(k) + → 信号交易员.更新() + → 仓位变化 → 订单执行 + + 使用方式:: + + python main.py --mode sync --nb templates/last.nb """ - 文件路径 = 配置.加载文件路径 - name = Path(文件路径).name.split(".")[0] - 符号, 周期, 起始时间戳, 结束时间戳 = name.split("-") - 周期 = int(周期) - 周期组 = [周期, 周期 * 5, 周期 * 5 * 6] + start = datetime.now() - def 魔法(): - 启动时间 = datetime.now() - 多级别分析 = 立体分析器(符号, 周期组, 配置, 配置组) - 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], 周期, 符号) - 多级别分析.投喂K线(k线) - 消耗用时 = datetime.now() - 启动时间 - print("测试_周期合成", 消耗用时, "普K数量", len(多级别分析._单体分析器[周期].普通K线序列)) - return 多级别分析 + # ---- 先检测周期再构建策略 ---- + if nb_path and os.path.exists(nb_path): + nb_period = _检测nb周期(nb_path) + if nb_period: + base_period = nb_period + higher_periods = tuple(p for p in higher_periods if p > nb_period) + if not higher_periods: + higher_periods = (nb_period * 5,) - return 魔法 + # ---- 观察者 + 数据队列 (仿 测试_邮局数据_同步回测) ---- + 配置 = 缠论配置() + 配置.计算指标 = True + + 数据队列 = queue.Queue() + 观察员 = 观察者(symbol, base_period, None, 配置, 数据队列) + + if nb_path and os.path.exists(nb_path): + print(f"数据源: {nb_path}") + + def _后台喂K线(): + 观察员.加载本地数据(nb_path) + else: + print(f"数据源: 随机生成 ({bars} 根)") + + def _后台喂K线(): + dt = datetime(2020, 1, 1) + price = 100.0 + import random as _r + + for i in range(bars): + dt += timedelta(days=1) + price += _r.gauss(0, 2) + k = K线.创建普K(symbol, dt, price - _r.random(), price + abs(_r.gauss(0, 1)), price - abs(_r.gauss(0, 1)), price, abs(_r.gauss(100, 50)), 0, base_period) + 观察员.增加原始K线(k) + + 数据源 = 自定义实时数据源(数据队列, 观察员, _后台喂K线) + + # ---- Backtrader (与 strategy 模式使用同一个 信号驱动策略) ---- + cerebro = bt.Cerebro(stdstats=False) + cerebro.adddata(数据源) + + cerebro.addstrategy( + 信号驱动策略, + 符号=symbol, + 基础周期=base_period, + 持仓=[ + cet.Position( + symbol=symbol, + name="中枢", + opens=[ + cet.Event( + cet.Operate.LO, + factors=[ + cet.Factor(signals_all=[cet.Signal(k1=str(base_period), k2="D1MO3", k3="中枢段DEA穿越2V230602", v2="三买")]), + cet.Factor(signals_all=[cet.Signal(k1=str(base_period), k2="D1MO3", k3="DEA穿越0轴V230602", v2="三买")]), + cet.Factor(signals_all=[cet.Signal(k1=str(base_period), k2="D1MO3", k3="首次穿越0轴V230602", v2="三买")]), + ], + ) + ], + exits=[ + cet.Event( + cet.Operate.LE, + factors=[ + cet.Factor(signals_all=[cet.Signal(k1=str(base_period), k2="D1MO3", k3="中枢段DEA穿越2V230602", v2="三卖")]), + cet.Factor(signals_all=[cet.Signal(k1=str(base_period), k2="D1MO3", k3="DEA穿越0轴V230602", v2="三卖")]), + cet.Factor(signals_all=[cet.Signal(k1=str(base_period), k2="D1MO3", k3="首次穿越0轴V230602", v2="三卖")]), + ], + ) + ], + ), + cet.Position( + symbol=symbol, + name="中枢", + exits=[ + cet.Event( + cet.Operate.LO, + factors=[ + cet.Factor(signals_all=[cet.Signal(k1=str(base_period), k2="D1MO3", k3="中枢段DEA穿越2V230602", v2="三买")]), + cet.Factor(signals_all=[cet.Signal(k1=str(base_period), k2="D1MO3", k3="DEA穿越0轴V230602", v2="三买")]), + cet.Factor(signals_all=[cet.Signal(k1=str(base_period), k2="D1MO3", k3="首次穿越0轴V230602", v2="三买")]), + ], + ) + ], + opens=[ + cet.Event( + cet.Operate.LE, + factors=[ + cet.Factor(signals_all=[cet.Signal(k1=str(base_period), k2="D1MO3", k3="中枢段DEA穿越2V230602", v2="三卖")]), + cet.Factor(signals_all=[cet.Signal(k1=str(base_period), k2="D1MO3", k3="DEA穿越0轴V230602", v2="三卖")]), + cet.Factor(signals_all=[cet.Signal(k1=str(base_period), k2="D1MO3", k3="首次穿越0轴V230602", v2="三卖")]), + ], + ) + ], + ), + ], + 投喂预热数=2, + 指标计算=True, + ) + + cerebro.broker.setcash(100000.0) + print(f"初始资金: {cerebro.broker.getvalue():,.0f}") + + results = cerebro.run(live=True) + strat = results[0] + elapsed = datetime.now() - start + + print(f"\n{'=' * 60}") + print(f" 同步回测结果") + print(f"{'=' * 60}") + print(f" 品种: {symbol} 周期: {base_period}s 耗时: {elapsed}") + print(f" 观察者: {len(观察员.笔序列)} 笔, {len(观察员.线段序列)} 线段, {len(观察员.中枢序列)} 中枢") + print(f" 策略投喂: {strat.投喂计数} 根K线") + + 最终资金 = cerebro.broker.getvalue() + 收益 = (最终资金 - 100000.0) / 100000.0 * 100 + print(f"\n 最终资金: {最终资金:,.0f} | 收益率: {收益:+.2f}%") if __name__ == "__main__": - 当前配置 = 缠论配置.不推送() - 当前配置.加载文件路径 = str(Path(__file__).parent / "btcusd-300-1761327300-1776327900.nb") - 测试_读取数据(配置=当前配置)() # .测试_保存数据() - # 测试_周期合成(当前配置)().测试_保存数据() -if __name__ == "__ma2in__": + import argparse - def 运行单个回测(线程编号: int): - """单个线程执行的函数,内部捕获异常以免影响其他线程""" - try: - 本地随机 = random.Random(os.urandom(64)) - 配置 = 随机配置(本地随机) - print(f"[线程{线程编号:02d}] 开始 ...") - print(f"[线程{线程编号:02d}] ", 配置.to_dict()) - 测试函数 = 测试_随机生成(symbol="btcusd", limit=10000, freq=时间周期.分(5), ws=None, 配置=配置) - 结果 = 测试函数() # 实际执行 - print(f"[线程{线程编号:02d}] 完成 | 笔序列长度: {len(结果.笔序列)}") - except Exception as e: - print(f"[线程{线程编号:02d}] 异常: {e}") - traceback.print_exc() + parser = argparse.ArgumentParser(description="缠论多线程随机回测 / 信号交易") + parser.add_argument("--threads", type=int, default=50, help="线程数 (默认 50)") + parser.add_argument("--limit", type=int, default=10000, help="每线程 K 线数 (默认 10000)") + parser.add_argument("--log", choices=["off", "simple", "tracing"], default="off", help="日志模式 (默认 off)") + parser.add_argument("--cache", choices=["thread_local", "global"], default="thread_local", help="缓存模式 (默认 thread_local)") + parser.add_argument("--mode", choices=["backtest", "trade", "strategy", "sync"], default="backtest", help="运行模式: backtest=随机回测, trade=信号交易, strategy=信号驱动策略, sync=同步信号驱动策略") + parser.add_argument("--symbol", default="btcusd", help="交易品种 (默认 btcusd)") + parser.add_argument("--nb", default=None, help=".nb 数据文件路径 (strategy/sync 模式可选)") + parser.add_argument("--bars", type=int, default=500, help="strategy/sync 模式回测K线数 (默认 500)") + parser.add_argument("--period", type=int, default=None, help="基础周期秒数 (默认从.nb自动检测, 随机数据默认86400)") + args = parser.parse_args() - start = datetime.now() - # 创建并启动 50 个线程 - 线程列表 = [] - for i in range(1, 51): - 线程 = threading.Thread(target=运行单个回测, args=(i,), name=f"回测线程-{i}") - 线程列表.append(线程) - 线程.start() + # 初始化日志和缓存 + chanlun.set_log_mode(args.log) + if args.log != "off": + chanlun.set_log_level("debug") + chanlun.set_cache_mode(args.cache) - # 等待所有线程结束 - for 线程 in 线程列表: - 线程.join() + if args.mode == "trade": + # === 信号交易模式 === + 周期组 = [300, 900, 3600] + 分析器 = 立体分析器(args.symbol, 周期组, 缠论配置()) - print("\n全部 50 个随机回测线程已完成。", datetime.now() - start) + 计算器 = cet.SignalOrchestrator(分析器, 信号模块="chanlun.signals") + 计算器.从信号列表提取配置( + [ + f"300_D1MO3_中枢第三买卖点V230602_中枢段DEA穿越2_三买_偏移1_0", + f"300_D1MO3_中枢第三买卖点V230602_中枢段DEA穿越2_三卖_偏移1_0", + ] + ) + + start = datetime.now() + end_ts = int(datetime.now().timestamp()) + left = end_ts - 周期组[0] * args.limit + _next = left + 投喂次数 = 0 + while 投喂次数 < args.limit: + data = Bitstamp.ohlc(args.symbol, 周期组[0], _next, _next := _next + 周期组[0] * 1000) + if not data.get("data"): + break + for bar in data["data"]["ohlc"]: + k = K线.创建普K(args.symbol, 转化为时间戳(int(bar["timestamp"])), float(bar["open"]), float(bar["high"]), float(bar["low"]), float(bar["close"]), float(bar["volume"]), 0, 周期组[0]) + 分析器.投喂K线(k) + 投喂次数 += 1 + if 投喂次数 < 2: + continue + 计算器.更新() + if 投喂次数 >= args.limit: + break + print(计算器.信号字典) + _next = int(data["data"]["ohlc"][-1]["timestamp"]) + print(f"交易完成 | 耗时 {datetime.now() - start}") + + elif args.mode == "strategy": + # === 信号驱动策略模式 — Backtrader 回测 === + kwargs = dict(symbol=args.symbol, bars=args.bars, nb_path=args.nb) + if args.period is not None: + kwargs["base_period"] = args.period + 运行信号驱动策略(**kwargs) + + elif args.mode == "sync": + # === 同步信号驱动策略模式 — 观察者驱动的实时信号交易 === + kwargs = dict(symbol=args.symbol, bars=args.bars, nb_path=args.nb) + if args.period is not None: + kwargs["base_period"] = args.period + 运行同步信号驱动策略(**kwargs) + + else: + # === 随机回测模式 === + def 运行单个回测(线程编号: int): + try: + 本地随机 = random.Random(os.urandom(64)) + 配置 = 随机配置(本地随机) + print(f"[线程{线程编号:02d}] 开始 | 差异: {缠论配置().对比(配置)}") + 测试函数 = 测试_随机生成(symbol="btcusd", limit=args.limit, freq=时间周期.分(5), ws=None, 配置=配置) + 结果 = 测试函数() + print(f"[线程{线程编号:02d}] 完成 | 笔: {len(结果.笔序列)} 段: {len(结果.线段序列)} 中枢: {len(结果.中枢序列)}") + except Exception as e: + print(f"[线程{线程编号:02d}] 异常: {e}") + traceback.print_exc() + + start = datetime.now() + print(f"启动 {args.threads} 线程 × {args.limit} K线 | 缓存: {chanlun.get_cache_mode()} | 日志: {chanlun.get_log_mode()}") + + 线程列表 = [] + for i in range(1, args.threads + 1): + 线程 = threading.Thread(target=运行单个回测, args=(i,), name=f"回测-{i}") + 线程列表.append(线程) + 线程.start() + + for 线程 in 线程列表: + 线程.join() + + elapsed = datetime.now() - start + total = args.threads * (args.limit + 1) + print(f"\n全部 {args.threads} 线程已完成 | 总 {total} K线 | 耗时 {elapsed} | {total / elapsed.total_seconds():.0f} K/s") diff --git a/strategies.py b/strategies.py index 203d68d..394bcfc 100644 --- a/strategies.py +++ b/strategies.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import json import backtrader as bt from datetime import datetime, timedelta from typing import Optional, List @@ -7,9 +8,12 @@ import math import queue import threading +import numpy as np + +import chanlun.chan_external as cet -__all__ = ["订单类型", "市场类型", "自适应市场仓位", "交易信号", "批次", "订单执行器", "高级策略基类_", "高级策略基类", "自定义实时数据源", "回测"] +__all__ = ["订单类型", "市场类型", "自适应市场仓位", "交易信号", "批次", "订单执行器", "高级策略基类_", "高级策略基类", "自定义实时数据源", "回测", "信号驱动策略", "NB数据源", "随机数据"] # ---------- 订单类型枚举 ---------- @@ -52,6 +56,12 @@ class 自适应市场仓位(bt.Sizer): ("允许部分开仓", False), ) + def __init__(self): + super().__init__() + 已设置 = sum(1 for x in [self.p.固定金额, self.p.风险百分比, self.p.固定数量] if x is not None) + if 已设置 != 1: + raise ValueError(f"自适应市场仓位: 必须且只能设置 固定金额/风险百分比/固定数量 之一,当前设置了{已设置}个") + def _getsizing(self, 佣金信息, 可用现金, 数据, 是否买入): """返回最终下单数量(股/币/手)""" 价格 = 数据.close[0] @@ -735,7 +745,11 @@ class 自定义实时数据源(bt.feed.DataBase): print(f"[{datetime.now()}] 自定义数据源: 数据格式错误,跳过: {e}") return True - self.lines.datetime[0] = bt.date2num(datetime.utcfromtimestamp(int(dt))) + # 兼容 int (Unix timestamp) 和 datetime 两种时间戳格式 + if isinstance(dt, datetime): + self.lines.datetime[0] = bt.date2num(dt) + else: + self.lines.datetime[0] = bt.date2num(datetime.utcfromtimestamp(int(dt))) self.lines.open[0] = o self.lines.high[0] = h self.lines.low[0] = l @@ -1051,6 +1065,222 @@ class 回测(高级策略基类): print(f"[{dt.strftime('%Y-%m-%d %H:%M')}] {self.p.观察员.__class__.__name__}: {self.p.符号} | {文本}") +# ==================== NB 数据源 — .nb 文件读取 ==================== + + +class NB数据源(bt.feeds.DataBase): + """从 .nb 文件读取K线数据作为 Backtrader 数据源 + + .nb 格式: 48字节大端序 — time:8, open:8, high:8, low:8, close:8, volume:8 (f64) + """ + + def __init__(self, 文件路径: str, 最大条数: int = None): + super().__init__() + import struct + + with open(文件路径, "rb") as f: + self._buffer = f.read() + self._记录总数 = len(self._buffer) // 48 + if 最大条数: + self._记录总数 = min(self._记录总数, 最大条数) + self._索引 = 0 + + def _load(self): + import struct + + if self._索引 >= self._记录总数: + return False + offset = self._索引 * 48 + ts, o, h, l, c, v = struct.unpack(">6d", self._buffer[offset : offset + 48]) + self.lines.datetime[0] = bt.date2num(datetime.fromtimestamp(int(ts))) + self.lines.open[0] = o + self.lines.high[0] = h + self.lines.low[0] = l + self.lines.close[0] = c + self.lines.volume[0] = v + self._索引 += 1 + return True + + +# ==================== 信号驱动策略 — 立体分析器 + 信号计算器 + Backtrader ==================== + + +class 信号驱动策略(高级策略基类): + """基于 立体分析器 / 信号计算器 的多周期信号驱动策略。 + + Backtrader 负责仓位管理和订单执行,信号计算器只负责产出信号字典。 + 策略在 ``next()`` 中直接读取信号字典判断买卖。 + + 数据流:: + + Backtrader bar → 立体分析器.投喂K线() + → 各周期缠论分析 → 信号计算器.更新() → 信号字典 + → 策略读取 v2 判多空 → Backtrader 订单 + + 使用方式:: + + cerebro.addstrategy(信号驱动策略, 符号="btcusd", + 多头信号=["三买"], 空头信号=["三卖"]) + """ + + params = ( + ("符号", "btcusd"), + ("基础周期", 300), + ("高级周期组", ()), + ("信号模块", "chanlun.signals"), + ("持仓", None), + ("信号配置", None), + ("投喂预热数", 2), + ("指标计算", True), + ) + + def __init__(self): + super().__init__() + self.投喂计数 = 0 + self.已处理信号 = set() + + self.持仓列表 = self.p.持仓 if self.p.持仓 is not None else [] + self._上次操作数 = {p.name: 0 for p in self.持仓列表} + + from chanlun.signal_orchestrator import ( + SignalOrchestrator as _信号计算器, + get_signals_config, + ) + + 信号配置 = self.p.信号配置 + if 信号配置 is None and self.持仓列表: + 所有信号 = set() + for p in self.持仓列表: + 所有信号.update(p.unique_signals) + 信号配置 = get_signals_config(list(所有信号)) + + self.缠论配置 = self._构建缠论配置() + 周期组 = [self.p.基础周期] + if self.p.高级周期组: + 周期组.extend(self.p.高级周期组) + if len(周期组) < 2: + 周期组.append(self.p.基础周期 * 5) + from chanlun import 立体分析器 as _立体分析器 + + self.分析器 = _立体分析器(self.p.符号, 周期组, self.缠论配置) + self.计算器 = _信号计算器( + 分析器=self.分析器, + 信号配置=信号配置 or [], + 信号模块=self.p.信号模块, + ) + + def _构建缠论配置(self): + from chanlun import 缠论配置 as _缠论配置 + + 配置 = _缠论配置() + if self.p.指标计算: + 配置.设置指标( + 均线=[("SMA_5", "收", "SMA", 5), ("SMA_10", "收", "SMA", 10), ("SMA_20", "收", "SMA", 20)], + MACD=[("macd", "收", 13, 31, 11)], + ) + 配置.图表展示 = False + 配置.图表展示标签 = [] + return 配置 + + def _从数据源创建K线(self) -> "K线": + dt = bt.num2date(self.data.datetime[0]) + from chanlun import K线 + + return K线.创建普K( + self.p.符号, + int(dt.timestamp()), + float(self.data.open[0]), + float(self.data.high[0]), + float(self.data.low[0]), + float(self.data.close[0]), + float(self.data.volume[0]), + 0, + self.p.基础周期, + ) + + def _检测新操作(self) -> list: + 新操作 = [] + for pos in self.持仓列表: + curr = len(pos.operates) + prev = self._上次操作数.get(pos.name, 0) + if curr > prev: + 新操作.extend(pos.operates[prev:]) + self._上次操作数[pos.name] = curr + return 新操作 + + def next(self): + dt = self.datas[0].datetime.datetime(0) + bar = len(self.data) + + # ── 1. 投喂 ── + k线 = self._从数据源创建K线() + self.分析器.投喂K线(k线) + self.投喂计数 += 1 + + if self.投喂计数 < self.p.投喂预热数: + # K线合成器缓冲中,尚无完整高级K线 + return + if bar < 50: + return # 缠论分析预热 + + # ── 2. 信号计算 ── + try: + self.计算器.更新() + except Exception: + import traceback + + self.日志(f"❌ 计算器异常:\n{traceback.format_exc()}") + return + + if self.计算器.信号: + k, v = next(iter(self.计算器.信号.items())) + self.日志(f"📡 信号={k}→{v} 持仓={self.position.size:+d}") + + # ── 3. 去重 ── + try: + 信号ID = json.dumps(self.计算器.信号, sort_keys=True, default=str) + except Exception: + 信号ID = str(self.计算器.信号) + if 信号ID in self.已处理信号: + return + self.已处理信号.add(信号ID) + + # ── 4. 止损 ── + if self.position: + self.更新止损订单(self.position.size > 0, self.data.close[0]) + + # ── 5. 仓位匹配 ── + for pos in self.持仓列表: + try: + prev_ops = len(pos.operates) + pos.update(self.计算器.信号字典) + if len(pos.operates) > prev_ops: + self.日志(f" ✓ {pos.name} 匹配 → {pos.operates[-1]['op']}") + except ValueError: + pass # 信号键尚不存在 + + # ── 6. 执行操作 ── + for op_record in self._检测新操作(): + op = op_record["op"] + price = op_record.get("price", self.data.close[0]) + self.日志(f" ▶ {op} @{price:.0f} {op_record.get('op_desc', '')}") + + if op == cet.Operate.LE and self.position.size > 0: + self.平仓(self.data) + elif op == cet.Operate.SE and self.position.size < 0: + self.平仓(self.data) + elif op == cet.Operate.LO and not self.position and self.p.允许做多: + self.开仓(self.data, 是否做多=True) + elif op == cet.Operate.SO and not self.position and self.p.允许做空: + self.开仓(self.data, 是否做多=False) + else: + self.日志(f" ✗ {op} 跳过 (持仓={self.position.size:+d} 允许做多={self.p.允许做多} 允许做空={self.p.允许做空})") + + def 日志(self, 文本: str): + dt = self.datas[0].datetime.datetime(0) + print(f"[{dt.strftime('%m-%d %H:%M')}] {文本}") + + # ==================== 回测运行入口 ==================== if __name__ == "__main__": import numpy as np