信号 版本一

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` 一致(过滤 "任意" 段)