信号 版本一

This commit is contained in:
YuWuKunCheng
2026-06-27 18:17:57 +08:00
parent 8405d478bc
commit 16ed3de8f5
91 changed files with 20966 additions and 7712 deletions
@@ -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`)。这套框架抄录自 czscApache 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<PyDict>`,逐键取值判类型
这是它能独立 `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('<signal>')`
### 5.3 Factor
```rust
#[pyclass(module = "chanlun._chanlun")]
pub struct Factor {
signals_all: Vec<Signal>,
signals_any: Vec<Signal>,
signals_not: Vec<Signal>,
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<Factor>,
signals_all: Vec<Signal>,
signals_any: Vec<Signal>,
signals_not: Vec<Signal>,
name: String,
sha256: String,
}
```
- 构造:`Event(operate, factors, signals_all=[], signals_any=[], signals_not=[], name="")``factors` 空 → `ValueError`
- `name`:有传名 → `<name>#<hash>`,否则 `<operate中文值>#<hash>`;同时存 `sha256` 字段。
- `unique_signals``is_match(s) -> (bool, Option<String>)`(命中返回 `(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<Event>,
exits: Vec<Event>,
events: Vec<Event>, // 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<PyDict>`。取到 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 的 dumpEvent)拼成稳定字符串后算 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)]`):
- Signal7 段解析、非 7 段 raise、score 越界 raise、key 过滤「任意」、value 拼接。
- Factor/Event`signals_all/any/not` 真值表全覆盖、空 signals_all/factors raise、确定性哈希同输入同名。
- Positionname 缺失 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` 已有声明一致)。
@@ -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<String, serde_json::Value>` | 灵活,对应 Python dict 来源(PyO3 层自然转换) |
| 描述符是否含 indicators/category 字段 | **否,保持最小 `{name, template, func}`** | 指标由「信号内识别 + 管线增量算」处理,不在描述符声明;本项目信号皆 observer 级,无需 category |
## 4. Crate 结构
```
chanlun-signal-macros/ ← 新建 proc-macro crateRust 强制独立)
├── Cargo.toml ← [lib] proc-macro = truedeps: 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`(无需引入 workspaceCargo 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<String, Value>) -> Vec<Signal>;
/// 信号描述符(编译期元数据,由 `#[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<Item = SignalDescriptor>,
) -> Result<HashMap<&'static str, SignalMeta>, 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 归并;重名 panicfail-fast)。
pub static SIGNAL_REGISTRY: LazyLock<HashMap<&'static str, SignalMeta>> = LazyLock::new(|| {
(inventory::iter::<SignalDescriptor>.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<String, Value>) -> Vec<Signal> { /* 原体 */ }
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<Item=SignalDescriptor>) -> Result<HashMap<..>, 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 落地。
- **categorykline/trader)暂不引入**:若子项目 4 的 Position.update 引入 trader 级信号,届时再扩描述符。
## 11. 许可证
新增 Rust 文件沿用项目 MIT 头。注册/宏机制参考 czscApache 2.0),在 `registry.rs` 与 macro crate 顶部加第三方代码声明。
@@ -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 行 PythonSignalsParser + 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 动态导入路径。
@@ -0,0 +1,58 @@
# 子项目 4Position.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 用例)
```
@@ -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<String, String>,
pub market: Option<MarketData>,
}
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:增强 SyncSignalEngine1-2 commits
- `更新_完整()` + PyO3 绑定
- 不改变现有行为
### 阶段 B:引入 SignalOrchestrator2-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.时间戳` 是 i64Rust K线),不是 Python datetime | 已由 Rust `PositionPy::时间戳转datetime` 处理 |
| `SignalEngine.更新_完整()` 的基础周期可能与 `_基础周期` 不一致 | 统一从 `分析器.周期组[0]` 获取 |
| Python 信号函数的 `**kwargs``freq` 是字符串(来自 SignalsParser | `_执行Python信号函数``int(freq)` 转换 |
| `list_signals()` 返回的是 Rust 注册名,不含模块路径 | 按短名匹配(`youwukuncheng_中枢第三买卖点_V230602` 不含 `chanlun.signals.` 前缀) |
@@ -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<String, Value>` 的字符串/数字提取到处都需要,集中处理 |
| 信号函数位置 | `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<f64>
pub fn ma(&self, key: &str) -> Option<f64> { ... }
}
```
同样给 `缠论K线` 加转发方法(委托给 `self.标的K线`)。
### 5.2 观察者便捷访问(`observer.rs` 新增方法)
```rust
impl {
/// 按偏移取普K(di=1 为最后一根)
pub fn K偏移(&self, di: usize) -> Option<&Arc<K线>> { ... }
/// 按偏移取缠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<Arc<>> { ... }
/// 确保所有 K 线上的指标已计算(调用 指标计算器::计算并挂载)
pub fn (&self) { ... }
}
```
### 5.3 参数提取(`signal/params.rs`
```rust
/// 从 params HashMap 提取字符串参数
pub fn get_string(params: &HashMap<String, Value>, key: &str, default: &str) -> String;
/// 从 params HashMap 提取整数参数
pub fn get_int(params: &HashMap<String, Value>, key: &str, default: i64) -> i64;
/// 从 params HashMap 提取浮点参数
pub fn get_f64(params: &HashMap<String, Value>, 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, &params)`
4. 验证返回的 `Vec<Signal>` 非空,信号 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 代码,不涉及第三方许可证。