Files
chanlun.rs/docs/superpowers/specs/2026-06-22-signal-registry-framework-design.md
2026-06-27 18:17:57 +08:00

182 lines
8.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 子项目 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 顶部加第三方代码声明。