信号 版本一

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
File diff suppressed because it is too large Load Diff
@@ -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<String, Value>) -> Vec<Signal>`,无 TaCache(核心层 K线已挂指标)。
**技术栈:** Rustedition 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/czscApache License 2.0),已简化适配。
use proc_macro::TokenStream;
/// 占位——任务 2 实现真实的 #[signal] 属性宏。
#[proc_macro_attribute]
pub fn signal(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
```
- [ ] **步骤 3chanlun 接线依赖**
修改 `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 不会报错)。
- [ ] **步骤 5Commit**
```bash
cd /home/moscow/chanlun.rs
git add chanlun-signal-macros chanlun/Cargo.toml
git commit -m "feat(signal-registry): 脚手架 — proc-macro crate + inventory 依赖"
```
---
## 任务 1registry.rs —— 描述符类型 + 归并 + 查询 API
**文件:**
- 创建:`chanlun/src/signal/registry.rs`
- 修改:`chanlun/src/signal/mod.rs`
- [ ] **步骤 1mod.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]` 注册的信号函数,运行时按名查表。
//!
//! 第三方代码声明:注册机制参考 czschttps://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<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
}
#[cfg(test)]
mod tests {
use super::*;
/// 探针信号函数(最小签名实现,仅供测试归并/查表)。
fn __probe(_obs: &, _p: &HashMap<String, Value>) -> Vec<Signal> {
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<String, Value>) -> Vec<Signal> {
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::<SignalDescriptor>.into_iter().copied()` 因 inventory 0.3 API 细节编译报错,改为 `inventory::iter::<SignalDescriptor>().copied()` 或 `inventory::iter::<SignalDescriptor> {}`(参考 `/home/moscow/czsc/crates/czsc-signals/src/registry.rs:136` 的 `inventory::iter::<...>.into_iter().copied().collect()` 写法)。
- [ ] **步骤 4Commit**
```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/czscApache License 2.0),已简化适配
//! (无 category / TaCache,签名固定为 fn(&观察者, &HashMap<String, Value>) -> Vec<Signal>)。
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::<Meta, Token![,]>::parse_terminated;
let metas = match parser.parse(attr) {
Ok(m) => m,
Err(e) => return e.to_compile_error().into(),
};
let mut name: Option<String> = None;
let mut template: Option<String> = 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`
预期:编译通过。
- [ ] **步骤 3Commit**
```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<String, Value>) -> Vec<Signal> {
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。
- [ ] **步骤 4Commit**
```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)→ 任务 2compile_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 实际写法)。
@@ -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` propertyRust 提供)
- [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 用例更新
@@ -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(信号 + 行情)
### 任务 A1SignalEngine 增加 `更新_完整()` 方法
**文件:** `chanlun/src/signal/engine.rs`
- [ ] **步骤 1:添加返回类型**
`SignalEngine``更新_含分数()` 之后添加新结构体:
```rust
/// 完整更新结果:信号字典 + 基础周期行情数据。
#[derive(Debug, Clone)]
pub struct {
/// 信号 key → value 映射
pub signals: HashMap<String, String>,
/// 基础周期最后一根 K 线的 OHLCV 数据
pub market: Option<MarketData>,
}
#[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
```
预期:编译通过。
- [ ] **步骤 4Commit**
```bash
git add chanlun/src/signal/engine.rs
git commit -m "feat(signal): SignalEngine.更新_完整() — 信号 + 基础周期行情
Co-Authored-By: Claude <noreply@anthropic.com>"
```
---
### 任务 A2PyO3 绑定增强
**文件:** `chanlun-py/src/signal_engine_py.rs`
- [ ] **步骤 1:暴露 `更新_完整()`**
`SignalEnginePy``#[pymethods]` 块中添加:
```rust
/// 更新信号并返回完整结果(信号 + 行情)。
/// 返回 dict: {"signals": {...}, "market": {...}}
fn _完整<'py>(&self, py: Python<'py>, analyzer: &Py) -> PyResult<Bound<'py, PyDict>> {
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<Py<PyAny>> {
```
- [ ] **步骤 3:添加 `freq 验证` 辅助函数**
`signal_engine_py.rs``SignalEnginePy::new()` 中添加 freq 验证(匹配 Python `信号计算器` setter 的行为):
```rust
// 在 new() 中,转换配置后:
// 验证所有 freq 已由调用方提供(不在构造时验证——没有分析器引用)
// 频率验证推迟到 更新() 调用时(与 Rust 核心行为一致)
```
不改变构造函数——保持最小侵入。频率验证由调用方负责(`SignalOrchestrator`)。
- [ ] **步骤 4:构建验证**
```bash
cd chanlun-py && cargo build
```
预期:编译通过。
- [ ] **步骤 5Commit**
```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 <noreply@anthropic.com>"
```
---
## 阶段 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
```
- [ ] **步骤 6Commit**
```bash
git add chanlun-py/chanlun/signal_orchestrator.py
git commit -m "feat(signal): SignalOrchestrator — Rust 优先 + Python 回退混合编排器
Co-Authored-By: Claude <noreply@anthropic.com>"
```
---
### 任务 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 测试通过,无回归。
- [ ] **步骤 3Commit**
```bash
git add strategies.py
git commit -m "refactor(strategies): 切换到 SignalOrchestrator 混合编排器
Co-Authored-By: Claude <noreply@anthropic.com>"
```
---
### 任务 B3:修复 main.py 中损坏的调用点
**文件:** `main.py:2220`
- [ ] **步骤 1:修复构造函数调用**
当前损坏的代码:
```python
计算器 = cet.信号计算器(观察者字典, 基础周期=周期组[0], 信号模块="chanlun.signals")
计算器.从信号序列设置配置([...]) # 方法不存在
```
修复为:
```python
计算器 = cet.SignalOrchestrator(分析器, 信号模块="chanlun.signals")
计算器.从信号列表提取配置([...])
```
> 注意:此处 `分析器` 变量需要在该作用域内可用。需要先检查 main.py 上下文。
- [ ] **步骤 2Commit**
```bash
git add main.py
git commit -m "fix(main): 修复损坏的 信号计算器 调用点 → SignalOrchestrator
Co-Authored-By: Claude <noreply@anthropic.com>"
```
---
## 阶段 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
```
预期:全部通过。
- [ ] **步骤 3Commit**
```bash
git add chanlun-py/tests/test_signal_orchestrator.py
git commit -m "test(signal): SignalOrchestrator 单元测试
Co-Authored-By: Claude <noreply@anthropic.com>"
```
---
### 任务 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` 使用别名导入——内部代码零改动
@@ -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<f64> {
self..read().线().and_then(|m| m.get(key).copied())
}
```
> **设计理由**:使用 `_cloned` 版本而非返回引用,避免 `RwLockReadGuard` 生命周期传染到调用方。MACD/RSI/KDJ/BOLL 结构体只含少量 f64 和 Option<f64>clone 开销可忽略。
### 步骤 2:缠论K线 便捷转发方法
`chanlun/src/kline/chan_kline.rs``impl 缠论K线` 块中添加转发方法。缠K 有 `pub 标的K线: RwLock<Arc<K线>>` 字段:
```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<f64> {
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<K线>> {
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<String, Value>` 中提取类型化参数。
use serde_json::Value;
use std::collections::HashMap;
/// 提取字符串参数,缺失或类型不对时返回默认值。
pub fn get_string(params: &HashMap<String, Value>, 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<String, Value>, key: &str, default: i64) -> i64 {
params.get(key)
.and_then(|v| v.as_i64())
.unwrap_or(default)
}
/// 提取 f64 参数。
pub fn get_f64(params: &HashMap<String, Value>, 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<String, Value>, 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
```
预期:编译通过。
### 步骤 6Commit
```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
```
### 步骤 3Commit
```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]` 注册的函数对应一个子模块。
//!
//! 第三方代码声明:信号函数模式参考 czschttps://github.com/waditu/czsc
//! Apache License 2.0),已适配为 Rust `fn(&观察者, &HashMap<String, Value>) -> Vec<Signal>`。
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<String, Value>,
) -> Vec<Signal> {
// 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<Arc<>> = 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
```
预期:编译通过。
### 步骤 4Commit
```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, &params);
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, &params);
assert_eq!(signals.len(), 1);
assert_eq!(signals[0].value(), "任意_任意_任意_0");
}
```
### 步骤 3:运行测试
```bash
cd chanlun && cargo test --test test_signal_youwukuncheng
```
预期:测试通过(或根据数据情况调整断言)。
### 步骤 4Commit
```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` 一致(过滤 "任意" 段)
@@ -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 代码,不涉及第三方许可证。