信号 版本一
This commit is contained in:
@@ -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<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 的 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` 已有声明一致)。
|
||||
@@ -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 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<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 归并;重名 panic,fail-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 落地。
|
||||
- **category(kline/trader)暂不引入**:若子项目 4 的 Position.update 引入 trader 级信号,届时再扩描述符。
|
||||
|
||||
## 11. 许可证
|
||||
|
||||
新增 Rust 文件沿用项目 MIT 头。注册/宏机制参考 czsc(Apache 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 行 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 动态导入路径。
|
||||
@@ -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 用例)
|
||||
```
|
||||
@@ -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:增强 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.` 前缀) |
|
||||
@@ -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, ¶ms)`
|
||||
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 代码,不涉及第三方许可证。
|
||||
Reference in New Issue
Block a user