信号 版本一
This commit is contained in:
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线已挂指标)。
|
||||
|
||||
**技术栈:** Rust(edition 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/czsc,Apache License 2.0),已简化适配。
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
|
||||
/// 占位——任务 2 实现真实的 #[signal] 属性宏。
|
||||
#[proc_macro_attribute]
|
||||
pub fn signal(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
item
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **步骤 3:chanlun 接线依赖**
|
||||
|
||||
修改 `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 不会报错)。
|
||||
|
||||
- [ ] **步骤 5:Commit**
|
||||
|
||||
```bash
|
||||
cd /home/moscow/chanlun.rs
|
||||
git add chanlun-signal-macros chanlun/Cargo.toml
|
||||
git commit -m "feat(signal-registry): 脚手架 — proc-macro crate + inventory 依赖"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 任务 1:registry.rs —— 描述符类型 + 归并 + 查询 API
|
||||
|
||||
**文件:**
|
||||
- 创建:`chanlun/src/signal/registry.rs`
|
||||
- 修改:`chanlun/src/signal/mod.rs`
|
||||
|
||||
- [ ] **步骤 1:mod.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]` 注册的信号函数,运行时按名查表。
|
||||
//!
|
||||
//! 第三方代码声明:注册机制参考 czsc(https://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 归并;重名 panic,fail-fast)。
|
||||
pub static SIGNAL_REGISTRY: LazyLock<HashMap<&'static str, SignalMeta>> = LazyLock::new(|| {
|
||||
归并(inventory::iter::<SignalDescriptor>.into_iter().copied())
|
||||
.unwrap_or_else(|e| panic!("{e}"))
|
||||
});
|
||||
|
||||
/// 按名查信号元信息。
|
||||
pub fn get_signal(name: &str) -> Option<&'static SignalMeta> {
|
||||
SIGNAL_REGISTRY.get(name)
|
||||
}
|
||||
|
||||
/// 按名查参数模板。
|
||||
pub fn get_template(name: &str) -> Option<&'static str> {
|
||||
SIGNAL_REGISTRY.get(name).map(|m| m.template)
|
||||
}
|
||||
|
||||
/// 列出所有已注册信号名(排序)。
|
||||
pub fn list_signal_names() -> Vec<&'static str> {
|
||||
let mut v: Vec<_> = SIGNAL_REGISTRY.keys().copied().collect();
|
||||
v.sort();
|
||||
v
|
||||
}
|
||||
|
||||
#[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()` 写法)。
|
||||
|
||||
- [ ] **步骤 4:Commit**
|
||||
|
||||
```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/czsc,Apache 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`
|
||||
预期:编译通过。
|
||||
|
||||
- [ ] **步骤 3:Commit**
|
||||
|
||||
```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。
|
||||
|
||||
- [ ] **步骤 4:Commit**
|
||||
|
||||
```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)→ 任务 2(compile_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` property(Rust 提供)
|
||||
- [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(信号 + 行情)
|
||||
|
||||
### 任务 A1:SignalEngine 增加 `更新_完整()` 方法
|
||||
|
||||
**文件:** `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
|
||||
```
|
||||
预期:编译通过。
|
||||
|
||||
- [ ] **步骤 4:Commit**
|
||||
|
||||
```bash
|
||||
git add chanlun/src/signal/engine.rs
|
||||
git commit -m "feat(signal): SignalEngine.更新_完整() — 信号 + 基础周期行情
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 任务 A2:PyO3 绑定增强
|
||||
|
||||
**文件:** `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
|
||||
```
|
||||
预期:编译通过。
|
||||
|
||||
- [ ] **步骤 5:Commit**
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
- [ ] **步骤 6:Commit**
|
||||
|
||||
```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 测试通过,无回归。
|
||||
|
||||
- [ ] **步骤 3:Commit**
|
||||
|
||||
```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 上下文。
|
||||
|
||||
- [ ] **步骤 2:Commit**
|
||||
|
||||
```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
|
||||
```
|
||||
预期:全部通过。
|
||||
|
||||
- [ ] **步骤 3:Commit**
|
||||
|
||||
```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
|
||||
```
|
||||
预期:编译通过。
|
||||
|
||||
### 步骤 6:Commit
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### 步骤 3:Commit
|
||||
|
||||
```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]` 注册的函数对应一个子模块。
|
||||
//!
|
||||
//! 第三方代码声明:信号函数模式参考 czsc(https://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
|
||||
```
|
||||
预期:编译通过。
|
||||
|
||||
### 步骤 4:Commit
|
||||
|
||||
```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, ¶ms);
|
||||
|
||||
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, ¶ms);
|
||||
|
||||
assert_eq!(signals.len(), 1);
|
||||
assert_eq!(signals[0].value(), "任意_任意_任意_0");
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 3:运行测试
|
||||
|
||||
```bash
|
||||
cd chanlun && cargo test --test test_signal_youwukuncheng
|
||||
```
|
||||
预期:测试通过(或根据数据情况调整断言)。
|
||||
|
||||
### 步骤 4:Commit
|
||||
|
||||
```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`)。这套框架抄录自 czsc(Apache 2.0)。
|
||||
|
||||
把这层**纯结构 + 匹配逻辑**移植到 Rust 核心层(`chanlun/src/signal/`),目的:
|
||||
|
||||
- **消除跨模块枚举/类型不一致问题**:信号原语只跟字符串和信号字典打交道,不持有 Rust 分析对象,天然规避「同值枚举跨模块 `is` 不相等」「动态导入找不到模块」这类坑。
|
||||
- **统一原语来源**:Rust 端策略/回测可直接用同一套 `Signal`/`Event`,无需经过 Python。
|
||||
- **性能**:匹配逻辑是热路径(每根 K 线、每个 Position 都跑),Rust 实现去掉 Python 解释开销。
|
||||
- **为后续分层铺路**:原语层稳定后,未来可按 czsc 的路线增量推进注册表、信号串解析、交易引擎。
|
||||
|
||||
## 2. 范围
|
||||
|
||||
### 纳入(Rust + PyO3)
|
||||
|
||||
- `Operate` 枚举
|
||||
- `Signal`:`key()` / `value()` / `is_match()`
|
||||
- `Factor`:`is_match()` / `unique_signals()` / `dump()` / `load()`
|
||||
- `Event`:`is_match()` / `unique_signals()` / `dump()` / `load()`
|
||||
- `Position` 基类:配置字段 + 校验 + `unique_signals` + `__repr__` + config 部分的 `dump`/`load`
|
||||
|
||||
### 不纳入(保持 Python)
|
||||
|
||||
- `Position.update()` 状态机(持仓推进、止损、超时、`pairs`、操作决策)
|
||||
- `信号计算器`(信号计算引擎、配置管理、`_自动挂载指标`)
|
||||
- `SignalsParser`(docstring 解析)
|
||||
- `import_by_name`(动态导入)
|
||||
- 全部信号函数(`chanlun.signals.*`)
|
||||
|
||||
## 3. czsc 参考映射
|
||||
|
||||
czsc 把信号体系拆成分层 crate。本次只对应其最底层「信号原语」:
|
||||
|
||||
| czsc | 本次对应 |
|
||||
|---|---|
|
||||
| `czsc-core/objects/{signal,event,position,operate}.rs` | `chanlun/src/signal/{signal,factor,event,position,operate}.rs` |
|
||||
| `czsc-core` 内 `#[cfg(feature="python")]` 内联 PyO3 包装 | `chanlun-py/src/signal_py.rs`(本项目沿用独立绑定 crate 的既有约定,不内联) |
|
||||
|
||||
czsc 的 `inventory` 编译期注册表、`#[signal]` 宏、`sig_parse`、`engine_v2` 交易引擎、`signals_dispatcher` **本次均不涉及**(属后续分层)。
|
||||
|
||||
## 4. 架构与模块布局
|
||||
|
||||
```
|
||||
chanlun/src/signal/
|
||||
├── mod.rs # pub mod 声明 + re-export
|
||||
├── operate.rs # Operate 枚举(HL/HS/HO/LO/LE/SO/SE)
|
||||
├── signal.rs # Signal
|
||||
├── factor.rs # Factor
|
||||
├── event.rs # Event
|
||||
└── position.rs # Position 基类(config + matching,不含 update)
|
||||
```
|
||||
|
||||
- `chanlun/src/lib.rs` 增加 `pub mod signal;`。
|
||||
- PyO3 绑定新增 `chanlun-py/src/signal_py.rs`,在 `lib.rs` 注册顺序:types → **signal** → config → indicators → kline → structure → algorithm → business → equality。
|
||||
|
||||
### 依赖边界
|
||||
|
||||
信号原语层**零依赖** `business` / `algorithm` / `structure` 层。它只操作:
|
||||
|
||||
- `String`(信号各字段)
|
||||
- 信号字典:匹配时通过 PyO3 接收 `&Bound<PyDict>`,逐键取值判类型
|
||||
|
||||
这是它能独立 `cargo test`、规避跨模块类型问题的根本原因。
|
||||
|
||||
## 5. 逐组件设计
|
||||
|
||||
### 5.1 Operate
|
||||
|
||||
```rust
|
||||
#[pyclass(eq, eq_int)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Operate { HL, HS, HO, LO, LE, SO, SE }
|
||||
```
|
||||
|
||||
- 值映射中文:`HL="持多" HS="持空" HO="持币" LO="开多" LE="平多" SO="开空" SE="平空"`,通过 `value()` 方法 / `__str__` 暴露。
|
||||
- Python 端 `cet.Operate.LO` 直接用该枚举。
|
||||
|
||||
### 5.2 Signal
|
||||
|
||||
```rust
|
||||
#[pyclass(module = "chanlun._chanlun")]
|
||||
pub struct Signal {
|
||||
signal: String,
|
||||
score: i32,
|
||||
k1: String, k2: String, k3: String,
|
||||
v1: String, v2: String, v3: String,
|
||||
}
|
||||
```
|
||||
|
||||
> 注:仅 `Position` 需要 `#[pyclass(subclass)]`(Python 子类补 `update()`)。`Signal`/`Factor`/`Event` 不被子类化,用普通 `#[pyclass]`。
|
||||
|
||||
- 构造签名:`Signal(signal="", score=0, k1="任意", k2="任意", k3="任意", v1="任意", v2="任意", v3="任意")`。
|
||||
- `signal` 非空 → 按 `_` 拆 7 段(非 7 段 raise);为空 → 由各字段拼。
|
||||
- `signal` 非字符串 → `TypeError`(对齐 Python `__post_init__`)。
|
||||
- `score` 越界 [0,100] → `ValueError`。
|
||||
- `key` property:拼接 k1/k2/k3 中非「任意」的部分,`_` 连接。
|
||||
- `value` property:`v1_v2_v3_score`。
|
||||
- `is_match(s) -> bool`:见 §6。
|
||||
- `__repr__` → `Signal('<signal>')`。
|
||||
|
||||
### 5.3 Factor
|
||||
|
||||
```rust
|
||||
#[pyclass(module = "chanlun._chanlun")]
|
||||
pub struct Factor {
|
||||
signals_all: Vec<Signal>,
|
||||
signals_any: Vec<Signal>,
|
||||
signals_not: Vec<Signal>,
|
||||
name: String,
|
||||
}
|
||||
```
|
||||
|
||||
- 构造:`Factor(signals_all, signals_any=[], signals_not=[], name="")`。`signals_all` 空 → `ValueError`。
|
||||
- 构造时计算 `name`:见 §6 ③(确定性哈希)。
|
||||
- `unique_signals` property:所有 signals 的 `signal` 字符串去重列表。
|
||||
- `is_match`:`signals_not` 任一命中 → False;`signals_all` 必须全中;`signals_any` 非空时至少一中。
|
||||
- `dump() -> dict`、`load(raw) classmethod`。
|
||||
|
||||
### 5.4 Event
|
||||
|
||||
```rust
|
||||
#[pyclass(module = "chanlun._chanlun")]
|
||||
pub struct Event {
|
||||
operate: Operate,
|
||||
factors: Vec<Factor>,
|
||||
signals_all: Vec<Signal>,
|
||||
signals_any: Vec<Signal>,
|
||||
signals_not: Vec<Signal>,
|
||||
name: String,
|
||||
sha256: String,
|
||||
}
|
||||
```
|
||||
|
||||
- 构造:`Event(operate, factors, signals_all=[], signals_any=[], signals_not=[], name="")`。`factors` 空 → `ValueError`。
|
||||
- `name`:有传名 → `<name>#<hash>`,否则 `<operate中文值>#<hash>`;同时存 `sha256` 字段。
|
||||
- `unique_signals`、`is_match(s) -> (bool, Option<String>)`(命中返回 `(True, factor_name)`)、`dump`、`load`。
|
||||
- `get_signals_config` **不在 Rust 实现**(依赖 Python 的 `SignalsParser`),保留在调用方 Python。
|
||||
|
||||
### 5.5 Position 基类
|
||||
|
||||
```rust
|
||||
#[pyclass(subclass, module = "chanlun._chanlun")]
|
||||
pub struct Position {
|
||||
symbol: String,
|
||||
opens: Vec<Event>,
|
||||
exits: Vec<Event>,
|
||||
events: Vec<Event>, // opens + exits
|
||||
name: String,
|
||||
interval: i64,
|
||||
timeout: i64,
|
||||
stop_loss: i64,
|
||||
T0: bool,
|
||||
}
|
||||
```
|
||||
|
||||
- 构造:`Position(symbol, opens, exits=[], interval=0, timeout=1000, stop_loss=1000, T0=False, name)`。
|
||||
- `name` 缺失 → `ValueError`(对齐 Python `assert name`)。
|
||||
- 每个 event 的 `operate` ∈ {LO,LE,SO,SE},否则 raise。
|
||||
- `unique_signals` property、`__repr__`、config 部分的 `dump`/`load`。
|
||||
- **状态字段、`update()`、`pairs`、`with_data` 版 dump、`get_signals_config` 全部留 Python 子类。**
|
||||
|
||||
## 6. 三个兼容性关键点
|
||||
|
||||
### ① `Signal.is_match` 缺键时 raise `ValueError`
|
||||
|
||||
Python 现状:键不在信号字典 → `raise ValueError`。`strategies.py` 靠 `try: pos.update(...) except ValueError: pass` 兜底。
|
||||
|
||||
**决策**:Rust `is_match` 缺键 → `PyValueError`,**不静默返回 False**。这是行为契约。
|
||||
|
||||
### ② 信号字典值可能非字符串
|
||||
|
||||
`信号计算器.信号字典` 合并了 OHLCV 行情(值为 datetime/float)。Python 有 `isinstance(v, str)` 守卫:非 str → `logger.warning` + 返回 False。
|
||||
|
||||
**决策**:`is_match` 接收 `&Bound<PyDict>`。取到 key 对应值后:
|
||||
|
||||
- 值不存在 → `PyValueError`(关键点 ①)。
|
||||
- 值非字符串 → 返回 False(对齐 Python 守卫)。**不打 warning**:匹配是每根 K 线的热路径,省去日志噪音;非 str 值来自 OHLCV 行情注入,是预期情况而非异常。
|
||||
- 值是字符串 → 按 `_` 拆 4 段(`v1_v2_v3_score`)做匹配。
|
||||
|
||||
### ③ Factor/Event 的 sha256 命名
|
||||
|
||||
Python:`hashlib.sha256(str(dump_dict_minus_name).encode()).hexdigest().upper()[:4]`,依赖 Python `str(dict)` 的逐字节格式。
|
||||
|
||||
**决策**:用 Rust 确定性哈希——对 `signals_all`/`signals_any`/`signals_not`(Factor)或加上 factors 的 dump(Event)拼成稳定字符串后算 sha256,取大写前 4。
|
||||
|
||||
- 自洽:同输入恒等同名,`dump`/`load` 来回一致。
|
||||
- **取舍(已知不兼容)**:生成的 hash 与 Python 旧版不同。依赖旧 `name` 的持久化仓位(保存的 .json)不再 roundtrip。本项目 Position 基本每次运行新建,可接受。
|
||||
|
||||
## 7. Drop-in 兼容策略
|
||||
|
||||
- `chan_external.py` 顶部:`from chanlun._chanlun import Signal, Factor, Event, Operate, Position as _PositionBase`,删除原 Python 类定义。
|
||||
- `Position` 改为子类:
|
||||
|
||||
```python
|
||||
class Position(_PositionBase):
|
||||
def __init__(self, symbol, opens, exits=[], interval=0, timeout=1000,
|
||||
stop_loss=1000, T0=False, name=None):
|
||||
super().__init__(symbol, opens, exits, interval, timeout, stop_loss, T0, name)
|
||||
# Python 侧状态
|
||||
self.pos_changed = False
|
||||
self.operates = []
|
||||
self.holds = []
|
||||
self.pos = 0
|
||||
self.last_event = {...}
|
||||
self.last_lo_dt = None
|
||||
self.last_so_dt = None
|
||||
self.end_dt = None
|
||||
# update() / pairs / get_signals_config / with_data dump 保留
|
||||
```
|
||||
|
||||
- `main.py` / `strategies.py` 中 `cet.Signal(...)`、`cet.Factor(...)`、`cet.Event(...)`、`cet.Position(...)`、`cet.Operate.LO` **无需改动**——构造签名与方法名一致。
|
||||
- 根目录 `chan.py` 的对应类同样替换为 import Rust 版本(保持与包版本一致)。
|
||||
|
||||
## 8. 测试策略
|
||||
|
||||
1. **Rust 单测**(`cargo test`,`chanlun/src/signal/` 内 `#[cfg(test)]`):
|
||||
- Signal:7 段解析、非 7 段 raise、score 越界 raise、key 过滤「任意」、value 拼接。
|
||||
- Factor/Event:`signals_all/any/not` 真值表全覆盖、空 signals_all/factors raise、确定性哈希同输入同名。
|
||||
- Position:name 缺失 raise、非法 operate raise、unique_signals 去重。
|
||||
2. **跨语言一致性**(pytest,复用 `tests/helpers/api_consistency.py`):
|
||||
- 构造相同 Signal/Factor/Event/Position,断言 `is_match`、`unique_signals`、`dump` 结构与移植前**逐字段一致**(name hash 除外)。
|
||||
- `is_match` 缺键 raise `ValueError`、值非 str 返回 False 两条边界。
|
||||
3. **回归**:跑 `测试_信号识别` + sync 回测,确认信号匹配与开关仓行为不变。
|
||||
|
||||
## 9. 已知取舍
|
||||
|
||||
- **name hash 不兼容旧 Python 版本**(§6 ③):依赖旧 name 的持久化仓位会对不上。可接受,因 Position 多为运行时新建。
|
||||
- **`get_signals_config` 留 Python**:它依赖 `SignalsParser` 动态解析,本次不移植;Rust `Event`/`Position` 不提供该方法,由 Python 调用方补。
|
||||
- **`Position.update` 留 Python**:状态机本次不移植,Position 被一分为二(Rust 基类配置 + Python 子类状态)。
|
||||
|
||||
## 10. 许可证
|
||||
|
||||
新增 Rust 文件沿用项目 MIT 头。信号原语逻辑摘录/参考自 czsc(Apache 2.0),在 `signal/mod.rs` 顶部加第三方代码声明(与根 `chan.py` 已有声明一致)。
|
||||
@@ -0,0 +1,181 @@
|
||||
# 子项目 1:信号注册框架 — 设计文档
|
||||
|
||||
- 日期:2026-06-22
|
||||
- 所属:「全 Rust 信号计算迁移」第 1 个子项目(共 4 个)
|
||||
- 参考:czsc(`/home/moscow/czsc`)的 `czsc-signal-macros` + `czsc-signals/{registry,types}.rs`
|
||||
- 前置:原语层已完成(`chanlun/src/signal/` 的 Signal/Factor/Event/Position/Operate)
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
「全 Rust 信号计算迁移」把信号函数、注册/解析、计算引擎、持仓状态机全部移到 Rust。拆为 4 个子项目(依赖序 1→2→3→4):
|
||||
|
||||
1. **信号注册框架**(本文档)
|
||||
2. 信号函数 API 暴露 + 移植 youwukuncheng
|
||||
3. 信号计算引擎 + PyO3 分发器
|
||||
4. Position.update 状态机
|
||||
|
||||
本子项目交付**编译期信号注册机制**:一个 `#[signal]` 属性宏 + `inventory` 注册表 + 描述符类型 + 一个探针信号验证机制。
|
||||
|
||||
**它消灭什么**:Python 的 `import_by_name`(动态导入,曾导致「找不到模块」「跨模块枚举 `is` 不等」)和 `SignalsParser` 的 docstring 正则解析(曾导致「多 pattern sig_pats_map」「get_function_name v[0]」「sys 未导入」等脆弱 bug)。注册变成编译期完成、查表 O(1)。
|
||||
|
||||
## 2. 范围
|
||||
|
||||
### 纳入
|
||||
- 新 proc-macro crate `chanlun-signal-macros`:`#[signal(name, template)]` 属性宏
|
||||
- `chanlun/src/signal/registry.rs`:`SignalDescriptor` / `SignalFn` / `SignalMeta` / `SIGNAL_REGISTRY` + 只读查询 API
|
||||
- `chanlun/Cargo.toml` 新增 `inventory` 依赖 + path 依赖 `chanlun-signal-macros`
|
||||
- 一个探针信号 + 测试(验证注册→查表→重名检测)
|
||||
|
||||
### 不纳入(后续子项目)
|
||||
- 真实信号函数移植(子项目 2)
|
||||
- 「确保指标按需增量计算」API(子项目 2,移植 youwukuncheng 读 MACD 时落地)
|
||||
- 信号计算引擎 + `call_signal` PyO3 分发器(子项目 3)
|
||||
- Position.update 状态机(子项目 4)
|
||||
|
||||
## 3. 关键设计决策
|
||||
|
||||
| 决策 | 选择 | 理由 |
|
||||
|---|---|---|
|
||||
| SignalFn 是否带 TaCache | **否** | 核心层 K线已挂载指标(`指标计算器::计算并挂载`),信号函数直接读 `标的K线.指标.macd(..)`,无需 czsc 式 TaCache |
|
||||
| 注册表位置 | **chanlun 核心 crate** `signal/` 模块 | 信号函数直接读 observer(同 crate)、指标在 K线上,无需独立 signals crate |
|
||||
| params 类型 | `HashMap<String, serde_json::Value>` | 灵活,对应 Python dict 来源(PyO3 层自然转换) |
|
||||
| 描述符是否含 indicators/category 字段 | **否,保持最小 `{name, template, func}`** | 指标由「信号内识别 + 管线增量算」处理,不在描述符声明;本项目信号皆 observer 级,无需 category |
|
||||
|
||||
## 4. Crate 结构
|
||||
|
||||
```
|
||||
chanlun-signal-macros/ ← 新建 proc-macro crate(Rust 强制独立)
|
||||
├── Cargo.toml ← [lib] proc-macro = true;deps: syn, quote, proc-macro2
|
||||
└── src/lib.rs ← #[signal] 属性宏
|
||||
|
||||
chanlun/ ← 现有核心 crate
|
||||
├── Cargo.toml ← 新增 inventory="0.3" + path 依赖 chanlun-signal-macros
|
||||
└── src/signal/
|
||||
├── mod.rs ← pub mod registry;
|
||||
└── registry.rs ← 描述符类型 + 注册表 + 探针信号(cfg(test))
|
||||
```
|
||||
|
||||
`chanlun` 通过 path 依赖 `chanlun-signal-macros`(无需引入 workspace;Cargo path 依赖即可。如愿统一可后续加 `[workspace]`)。
|
||||
|
||||
## 5. 描述符类型与签名(`chanlun/src/signal/registry.rs`)
|
||||
|
||||
```rust
|
||||
use crate::business::observer::观察者;
|
||||
use crate::signal::Signal;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
/// 信号函数签名 — 读观察者状态(含 K线已挂指标)+ 参数 → 信号列表。无 TaCache。
|
||||
pub type SignalFn = fn(&观察者, &HashMap<String, Value>) -> Vec<Signal>;
|
||||
|
||||
/// 信号描述符(编译期元数据,由 `#[signal]` 宏生成、`inventory` 收集)。
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct SignalDescriptor {
|
||||
/// 信号函数名,如 "youwukuncheng_中枢第三买卖点_V230602"
|
||||
pub name: &'static str,
|
||||
/// 参数模板,如 "{freq}_D1MO{max_overlap}_中枢第三买卖点V230602"
|
||||
pub template: &'static str,
|
||||
/// 函数指针
|
||||
pub func: SignalFn,
|
||||
}
|
||||
|
||||
inventory::collect!(SignalDescriptor);
|
||||
|
||||
/// 运行时信号元信息。
|
||||
pub struct SignalMeta {
|
||||
pub func: SignalFn,
|
||||
pub template: &'static str,
|
||||
}
|
||||
|
||||
/// 归并描述符为注册表;重名返回 Err(纯函数,便于单测)。
|
||||
fn 归并(
|
||||
descs: impl Iterator<Item = SignalDescriptor>,
|
||||
) -> Result<HashMap<&'static str, SignalMeta>, String> {
|
||||
let mut m: HashMap<&'static str, SignalMeta> = HashMap::new();
|
||||
for d in descs {
|
||||
if m.insert(d.name, SignalMeta { func: d.func, template: d.template }).is_some() {
|
||||
return Err(format!("信号重名:{}", d.name));
|
||||
}
|
||||
}
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
/// 全局注册表视图(由 inventory 归并;重名 panic,fail-fast)。
|
||||
pub static SIGNAL_REGISTRY: LazyLock<HashMap<&'static str, SignalMeta>> = LazyLock::new(|| {
|
||||
归并(inventory::iter::<SignalDescriptor>.into_iter().copied())
|
||||
.unwrap_or_else(|e| panic!("{e}"))
|
||||
});
|
||||
|
||||
/// 按名查信号元信息。
|
||||
pub fn get_signal(name: &str) -> Option<&'static SignalMeta> {
|
||||
SIGNAL_REGISTRY.get(name)
|
||||
}
|
||||
|
||||
/// 按名查参数模板。
|
||||
pub fn get_template(name: &str) -> Option<&'static str> {
|
||||
SIGNAL_REGISTRY.get(name).map(|m| m.template)
|
||||
}
|
||||
|
||||
/// 列出所有已注册信号名(排序)。
|
||||
pub fn list_signal_names() -> Vec<&'static str> {
|
||||
let mut v: Vec<_> = SIGNAL_REGISTRY.keys().copied().collect();
|
||||
v.sort();
|
||||
v
|
||||
}
|
||||
```
|
||||
|
||||
## 6. `#[signal]` 宏(`chanlun-signal-macros/src/lib.rs`)
|
||||
|
||||
属性宏贴在信号函数上,做三件事:
|
||||
|
||||
1. **校验**:函数名必须含 `_V<数字版本>`;`name` 属性须与函数名一致;`name`/`template` 非空。不符 → `compile_error!`。
|
||||
2. **保留原函数**不变。
|
||||
3. **生成** 一个 `static` 描述符 + `inventory::submit!` 提交:
|
||||
|
||||
宏输入 `#[signal(name = "foo_V230101", template = "{freq}_D1_foo")]` 贴在 `fn foo_V230101(...)` 上,展开为(概念示意):
|
||||
```rust
|
||||
fn foo_V230101(观: &观察者, p: &HashMap<String, Value>) -> Vec<Signal> { /* 原体 */ }
|
||||
inventory::submit! {
|
||||
crate::signal::registry::SignalDescriptor {
|
||||
name: "foo_V230101",
|
||||
template: "{freq}_D1_foo",
|
||||
func: foo_V230101 as crate::signal::registry::SignalFn,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**路径约定**:宏 emit `crate::signal::registry::...`,即假定信号函数住在 `chanlun` crate 内(本迁移的既定结构)。
|
||||
|
||||
## 7. 测试
|
||||
|
||||
1. **宏 crate**(`chanlun-signal-macros/tests/test_signal_macro.rs`):普通集成测试——定义一个符合签名的探针函数并贴 `#[signal(name="probe_macro_V000000", template="{freq}_D1_probe")]`,断言它能编译且 `inventory::iter` 能收到对应描述符(name/template 正确)。编译失败用例(name 与函数名不一致、缺版本号)作为**可选** trybuild compile-fail 测试,非必须。
|
||||
2. **核心注册表**(`registry.rs` 内 `#[cfg(test)]`):
|
||||
- 用 `inventory::submit!` 提交一个探针 `SignalDescriptor`(name `__probe_V000000`);
|
||||
- `get_signal("__probe_V000000")` 命中、`get_template` 返回模板、`list_signal_names()` 含它;
|
||||
- 重名场景:把归并逻辑抽成一个可独立调用的纯函数 `fn 归并(descs: impl Iterator<Item=SignalDescriptor>) -> Result<HashMap<..>, String>`,单测对重复 name 返回 Err(`SIGNAL_REGISTRY` 的 LazyLock 内部调用它并对 Err `panic!`),避免污染全局 inventory。
|
||||
|
||||
## 8. 数据流
|
||||
|
||||
```
|
||||
编译期: #[signal] 宏 → SignalDescriptor 常量 → inventory::submit!
|
||||
启动时: SIGNAL_REGISTRY (LazyLock) ← inventory::iter 归并(重名 panic)
|
||||
运行时: get_signal(name) -> &SignalMeta { func, template } (O(1) 查表)
|
||||
后续子项目 3 的计算引擎用 func 调用、用 template 反向生成信号 key
|
||||
```
|
||||
|
||||
## 9. 错误处理
|
||||
|
||||
- **编译期**:宏校验失败 → `compile_error!`(带清晰中文消息)。
|
||||
- **启动期**:重名信号 → `panic!("信号重名:{name}")`(fail-fast,对应 czsc 的 normalize 重名检测)。
|
||||
- **运行期**:`get_signal` 未命中返回 `None`(调用方——子项目 3——决定如何处理,对应旧「未找到解析函数」告警)。
|
||||
|
||||
## 10. 已知取舍与后续
|
||||
|
||||
- **无运行时可扩展性**:信号在编译期注册,新增信号需重编译(`maturin build`)。这是「全 Rust」方案的既定取舍,用户已确认。
|
||||
- **指标按需机制不在本子项目**:信号函数读指标 + 管线增量计算的「确保指标」API 在子项目 2 落地。
|
||||
- **category(kline/trader)暂不引入**:若子项目 4 的 Position.update 引入 trader 级信号,届时再扩描述符。
|
||||
|
||||
## 11. 许可证
|
||||
|
||||
新增 Rust 文件沿用项目 MIT 头。注册/宏机制参考 czsc(Apache 2.0),在 `registry.rs` 与 macro crate 顶部加第三方代码声明。
|
||||
@@ -0,0 +1,105 @@
|
||||
# 信号计算器完全移植评估
|
||||
|
||||
- 日期:2026-06-23
|
||||
- 前置:混合迁移(SignalEngine + SignalOrchestrator)已完成
|
||||
|
||||
## 1. 当前差距
|
||||
|
||||
### 1.1 未移植的 Python 信号函数(7/8)
|
||||
|
||||
| 函数 | 文件 | 行数 | 复杂度 | 移植工时 |
|
||||
|------|------|------|--------|----------|
|
||||
| `bar_zdt_V230331` | demo.py | 38 | 极低 | ~1h |
|
||||
| `macd_金叉` | demo.py | 56 | 低 | ~1-2h |
|
||||
| `tas_macd_direct_V221106` | demo.py | 55 | 低 | ~1-2h |
|
||||
| `tas_ma_base_V230313` | demo.py | 57 | 低-中 | ~2-3h |
|
||||
| `cxt_停顿分型_V230106` | demo.py | 49 | 低-中 | ~3-5h |
|
||||
| `cxt_bi_end_V230222` | demo.py | 74 | 中 | ~4-8h |
|
||||
| `模板_V日期` | _template.py | 28 | 模板 | 不需要 |
|
||||
|
||||
**总计:约 12-21 小时**
|
||||
|
||||
> 已移植的只有 `youwukuncheng_中枢第三买卖点_V230602`(1/8)。
|
||||
|
||||
### 1.2 可移除的 Python 组件
|
||||
|
||||
| 组件 | 文件 | 替换方案 |
|
||||
|------|------|----------|
|
||||
| `SignalsParser` 类 | chan_external.py:102-293 | 不再需要——配置由 Rust 注册表直接生成 |
|
||||
| `get_signals_config()` | chan_external.py:296-310 | `list_signals()` + 直接构造配置 |
|
||||
| `从信号列表提取配置()` | chan_external.py:522-530 | `list_signals()` + `get_signal_template()` |
|
||||
| `create_single_signal()` | chan_external.py:312-319 | 不再需要(Rust 信号函数使用 `Signal::new_empty`) |
|
||||
| `chanlun.signals` 包 | signals/*.py | 所有函数已移植到 Rust |
|
||||
| `chanlun.parse` | parse.py | 仅被 `SignalsParser` 使用 |
|
||||
| `chan.py` 中的副本 | chan.py:7394+ | 内部副本,可单独处理 |
|
||||
|
||||
## 2. 关键依赖链
|
||||
|
||||
```
|
||||
strategies.py
|
||||
└→ get_signals_config(position.unique_signals, signals_module)
|
||||
└→ SignalsParser(signals_module).parse(signal_strings)
|
||||
└→ 遍历 chanlun.signals 模块的所有函数
|
||||
└→ 读取文档字符串 → 正则提取参数模板
|
||||
└→ parse 库反向格式化 → 配置字典
|
||||
```
|
||||
|
||||
完全移植后,这个链简化为:
|
||||
```
|
||||
strategies.py
|
||||
└→ 直接构造 config = [{name, freq, params}] 从 Rust list_signals()
|
||||
```
|
||||
|
||||
## 3. 建议:分两阶段执行
|
||||
|
||||
### 阶段 1:移植剩余信号函数(~12-21h)
|
||||
|
||||
按复杂度递增顺序:
|
||||
|
||||
| 子任务 | 内容 |
|
||||
|--------|------|
|
||||
| 1.1 | 移植 `bar_zdt_V230331` → `chanlun/src/signal/functions/demo.rs` |
|
||||
| 1.2 | 移植 `macd_金叉` → `demo.rs` |
|
||||
| 1.3 | 移植 `tas_macd_direct_V221106` → `demo.rs` |
|
||||
| 1.4 | 移植 `tas_ma_base_V230313` → `demo.rs`(需要均线计算辅助) |
|
||||
| 1.5 | 移植 `cxt_停顿分型_V230106` → `demo.rs` |
|
||||
| 1.6 | 移植 `cxt_bi_end_V230222` → `demo.rs` |
|
||||
|
||||
每个子任务:
|
||||
- 编写 Rust 函数 + `#[signal]` 注册
|
||||
- 编写 Rust 单元测试
|
||||
- 编写 Python 对比测试(Rust vs Python 输出)
|
||||
|
||||
### 阶段 2:移除 Python 回退路径(~4-6h)
|
||||
|
||||
| 子任务 | 内容 |
|
||||
|--------|------|
|
||||
| 2.1 | 简化 `SignalOrchestrator` → 仅使用 `SignalEngine` |
|
||||
| 2.2 | 移除 `SignalsParser`、`get_signals_config`、`从信号列表提取配置` |
|
||||
| 2.3 | 移除 `chanlun.signals` 包(demo.py/youwukuncheng.py/_template.py) |
|
||||
| 2.4 | 移除 `chanlun.parse`(vendored parse 库) |
|
||||
| 2.5 | 更新 `strategies.py` 使用直接配置构造 |
|
||||
| 2.6 | 更新测试文件 |
|
||||
|
||||
## 4. 收益
|
||||
|
||||
| 收益 | 说明 |
|
||||
|------|------|
|
||||
| 代码量减少 | 移除 ~1,200 行 Python(SignalsParser + signals 包 + parse.py + chan.py 副本) |
|
||||
| 统一执行路径 | 不再有 Rust/Python 双路径,消除维护成本 |
|
||||
| 编译时安全 | 所有信号函数编译时注册,不会运行时 `import_by_name` 失败 |
|
||||
| 性能提升 | 批量 Rust 执行 vs 逐个 Python 调用 |
|
||||
| 依赖精简 | 移除 vendored `parse` 库和 `chanlun.signals` 包 |
|
||||
|
||||
## 5. 风险
|
||||
|
||||
| 风险 | 缓解 |
|
||||
|------|------|
|
||||
| `cxt_bi_end_V230222` 依赖笔/分型序列指针比较 | Rust 已有 `分型`/`笔` 结构,使用 `Arc` 指针 |
|
||||
| `cxt_停顿分型_V230106` 依赖 `与MACD柱子分型匹配` | 需要确认 Rust 侧是否有该方法或等效逻辑 |
|
||||
| `tas_ma_base_V230313` 依赖均线按需计算 | Rust 已有 `指标计算器::计算并挂载` 和 `k.ma(key)` |
|
||||
| `strategies.py` 默认信号配置为空时依赖 `get_signals_config` | 切换到 `list_signals()` + 直接构造 |
|
||||
|
||||
## 6. 结论
|
||||
|
||||
**完全移植可行,建议执行。** 总工作量约 16-27 小时。7 个未移植信号函数按复杂度递增顺序逐个移植(阶段 1),然后移除 Python 回退路径(阶段 2)。完成后信号框架为纯 Rust 核心 + Python 薄绑定,不再有 Python 动态导入路径。
|
||||
@@ -0,0 +1,58 @@
|
||||
# 子项目 4:Position.update 状态机迁移到 Rust — 设计文档
|
||||
|
||||
- 日期:2026-06-23
|
||||
- 所属:「全 Rust 信号计算迁移」第 4 个子项目(共 4 个)
|
||||
- 前置:子项目 1-3 已完成(注册表、信号函数、计算引擎)
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
子项目 1-3 交付了完整的信号计算链路:`#[signal]` 注册表 → 信号函数 → 计算引擎。Position.update 状态机是信号框架中最后一个仍留在 Python 中的核心逻辑(~135 行),将其迁移到 Rust 后,信号框架的纯 Rust 核心部分全部就位。
|
||||
|
||||
子项目 4 交付:
|
||||
1. Position 状态字段(pos, operates, holds, last_event...)→ Rust 核心
|
||||
2. update() 状态机算法 → Rust 核心(与 Python 版 1:1 对应)
|
||||
3. pairs() 开平配对计算 → Rust 核心
|
||||
4. PyO3 绑定:update(), 状态 getter, dump/load 带状态
|
||||
|
||||
## 2. 设计决策
|
||||
|
||||
| 决策 | 选择 | 理由 |
|
||||
|------|------|------|
|
||||
| 状态字段位置 | 直接加在 Position 结构体 | backtrader 在 GIL 下单线程访问;不需要额外锁 |
|
||||
| update 签名(Rust) | `fn update(&mut self, dt: i64, price: f64, bid: i64, signals: &信号字典)` | 核心不依赖 Python 类型;OHLCV 由 PyO3 层提取 |
|
||||
| PyDict → 信号字典 | 排除 OHLCV 键后调用 字典转核心 | 复用已有转换逻辑 |
|
||||
| dt 类型兼容 | 支持 datetime/i64/f64 → 统一转为 i64 Unix 秒 | 兼容三种常见输入格式 |
|
||||
| 时间戳 → Python datetime | `datetime.datetime.fromtimestamp(ts, UTC)` | 保持 operates/holds 元素类型与旧版一致 |
|
||||
| Python 向后兼容 | 保留 Python 子类,__init__ 简化为空;update/pairs/dump 由 Rust 提供 | 不破坏 strategies.py 等下游代码 |
|
||||
| Operate 枚举映射 | `核心Operate → OperatePy` 一对一转换函数 | 类型安全,无运行时开销 |
|
||||
|
||||
## 3. 新增 Rust 类型
|
||||
|
||||
```rust
|
||||
pub struct 操作记录 { symbol, dt, bid, price, op: Operate, op_desc, pos }
|
||||
pub struct 持仓记录 { dt, pos, price }
|
||||
pub struct 开平配对 { 标的代码, 策略标记, 交易方向, 开仓时间, 平仓时间, 开仓价格, 平仓价格, 持仓K线数, 事件序列, 持仓天数, 盈亏比例 }
|
||||
pub struct 最近事件 { dt, bid, price, op, op_desc }
|
||||
```
|
||||
|
||||
Position 新增 7 个状态字段:`pos, pos_changed, operates, holds, last_event, last_lo_dt, last_so_dt, end_dt`
|
||||
|
||||
## 4. update() 状态机
|
||||
|
||||
与 Python `Position.update(s)` 1:1 对应:
|
||||
|
||||
1. 时间校验:`dt <= end_dt` → 日志警告,跳过
|
||||
2. 事件匹配:遍历 events,调用 `event.is_match(signals)`
|
||||
3. 开仓处理:LO → 间隔检查 → 开多/平空;SO → 间隔检查 → 开空/平多
|
||||
4. 多头出场:LE 信号 / 止损(price/last_price - 1 < -stop_loss/10000)/ 超时(bid - last_bid > timeout)
|
||||
5. 空头出场:SE 信号 / 止损(方向反转)/ 超时
|
||||
6. 记录持仓快照 holds
|
||||
|
||||
## 5. 文件结构
|
||||
|
||||
```
|
||||
chanlun/src/signal/position.rs ← 操作记录/持仓记录/开平配对/最近事件 类型 + 状态字段 + update/pairs
|
||||
chanlun-py/src/signal_py.rs ← PositionPy: update(PyDict), 状态 getter, dump(with_data), load, 时间戳转datetime
|
||||
chanlun-py/chanlun/chan_external.py ← Python Position 子类简化(__init__ → pass)
|
||||
chanlun-py/tests/test_position_update.py ← 集成测试(24 用例)
|
||||
```
|
||||
@@ -0,0 +1,142 @@
|
||||
# `信号计算器` Rust 迁移 — 设计文档
|
||||
|
||||
- 日期:2026-06-23
|
||||
- 所属:全 Rust 信号计算迁移 — 子项目 1-4 完成后的延续
|
||||
- 前置:子项目 1-4 全部完成(注册表 + 信号函数 + 引擎 + Position 状态机)
|
||||
|
||||
## 1. 背景
|
||||
|
||||
「全 Rust 信号计算迁移」4 个子项目完成后,信号框架的 Rust 核心已就位:
|
||||
|
||||
- `#[signal]` 注册表 → 编译时信号函数发现
|
||||
- `SignalEngine` → 按名查找 + 批量执行
|
||||
- `Position.update()` → 状态机
|
||||
|
||||
但 **`信号计算器`(Python 信号编排器)仍然在使用 Python 动态导入**(`import_by_name`)来发现和执行信号函数。它与 Rust `SignalEngine` **并行存在**,形成两条独立的执行路径。
|
||||
|
||||
## 2. 设计目标
|
||||
|
||||
1. **统一信号执行路径**:Rust `SignalEngine` 作为主路径,Python 动态导入作为回退
|
||||
2. **保持向后兼容**:`strategies.py` 无需改动内部逻辑
|
||||
3. **渐进式迁移**:新增 Rust 信号函数自动通过引擎执行,无需修改编排器代码
|
||||
4. **最终目标**:所有信号函数移植到 Rust 后,Python 回退路径可移除
|
||||
|
||||
## 3. 关键设计决策
|
||||
|
||||
| 决策 | 选择 | 理由 |
|
||||
|------|------|------|
|
||||
| 编排器架构 | 新建 `SignalOrchestrator` 类,不修改 `信号计算器` | 零风险切换;旧类保留用于对比验证 |
|
||||
| 信号函数分类 | 构造时按 `list_signals()` 将配置分为 Rust/Python 两组 | 避免每次 `更新()` 都查注册表 |
|
||||
| Rust 路径 | 使用 `SignalEngine.更新_完整()`(批量) | 性能优于逐个 `call_signal()` |
|
||||
| Python 路径 | 保留 `import_by_name` + `_解析信号函数` | 非侵入式;已有信号函数无需任何修改 |
|
||||
| OHLCV 行情 | Rust 引擎直接返回基础周期行情 | 消除 Python 侧的独立行情提取步骤 |
|
||||
| freq 验证 | 在编排器 setter 中验证 | 与旧 `信号计算器` 行为一致 |
|
||||
|
||||
## 4. 架构图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ SignalOrchestrator │
|
||||
│ │
|
||||
│ 信号配置 ──→ 分类(list_signals() 查表) │
|
||||
│ │ │
|
||||
│ ┌────────┴────────┐ │
|
||||
│ │ Rust 已注册 │ Python 未注册 │
|
||||
│ │ SignalEngine │ import_by_name │
|
||||
│ │ .更新_完整() │ ._执行Python信号函数() │
|
||||
│ └────────┬────────┘ │
|
||||
│ │ │
|
||||
│ 合并结果 → self.信号 + self.行情 │
|
||||
│ │
|
||||
│ self.信号字典 → Position.update() │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 5. SignalEngine 增强
|
||||
|
||||
### 5.1 新增 `更新_完整()` 方法
|
||||
|
||||
```rust
|
||||
pub struct 完整更新结果 {
|
||||
pub signals: HashMap<String, String>,
|
||||
pub market: Option<MarketData>,
|
||||
}
|
||||
|
||||
pub struct MarketData {
|
||||
pub symbol: String,
|
||||
pub dt: i64, // Unix 秒
|
||||
pub id: i64,
|
||||
pub open: f64, pub high: f64, pub low: f64,
|
||||
pub close: f64, pub vol: f64,
|
||||
}
|
||||
```
|
||||
|
||||
`更新_完整(&self, analyzer: &立体分析器) -> 完整更新结果`:
|
||||
1. 调用 `self.更新(analyzer)` 获取信号
|
||||
2. 从 `analyzer.周期组[0]` 获取基础周期观察者
|
||||
3. 提取最后一根普K的 OHLCV 数据
|
||||
4. 返回组合结果
|
||||
|
||||
## 6. SignalOrchestrator 设计
|
||||
|
||||
### 6.1 类签名
|
||||
|
||||
```python
|
||||
class SignalOrchestrator:
|
||||
def __init__(
|
||||
self,
|
||||
分析器: 立体分析器,
|
||||
信号配置: Optional[List[Dict]] = None,
|
||||
信号模块: str = "chanlun.signals",
|
||||
):
|
||||
```
|
||||
|
||||
### 6.2 方法
|
||||
|
||||
| 方法 | 来源 | 说明 |
|
||||
|------|------|------|
|
||||
| `更新()` | 新写 | 先 Rust 批量,再 Python 逐个 |
|
||||
| `信号配置` (property) | 移植 | setter 中添加 Rust/Python 分类 |
|
||||
| `信号字典` (property) | 移植 | `{**self.信号, **self.行情}` |
|
||||
| `获取周期观察者(freq)` | 移植 | 委托给 `_观察者字典` |
|
||||
| `从信号列表提取配置(信号序列)` | 移植 | 委托给 `SignalsParser` |
|
||||
| `_去重配置(configs)` | 移植 | 与旧版一致 |
|
||||
| `_预加载Python信号函数()` | 移植 | 缓存 Python 函数引用 |
|
||||
| `_解析信号函数(name)` | 移植 | `import_by_name` 逻辑 |
|
||||
| `_执行Python信号函数(config)` | 移植 | Python 函数调用 |
|
||||
| `_提取行情()` | 移植 | 仅 Python-only 回退路径使用 |
|
||||
|
||||
## 7. 迁移路径
|
||||
|
||||
### 阶段 A:增强 SyncSignalEngine(1-2 commits)
|
||||
- `更新_完整()` + PyO3 绑定
|
||||
- 不改变现有行为
|
||||
|
||||
### 阶段 B:引入 SignalOrchestrator(2-3 commits)
|
||||
- 新文件 `signal_orchestrator.py`
|
||||
- `strategies.py` 切换到新类(别名导入)
|
||||
- 修复 `main.py` 损坏的调用点
|
||||
|
||||
### 阶段 C:废弃 Python 并行路径(未来)
|
||||
- 所有信号函数移植到 Rust 后
|
||||
- 移除 `信号计算器`、`SignalsParser`、`import_by_name`
|
||||
- 移除 `signals/` 目录中的 Python 信号函数
|
||||
|
||||
## 8. 向后兼容
|
||||
|
||||
| 组件 | 兼容策略 |
|
||||
|------|---------|
|
||||
| `strategies.py` | 别名导入 `SignalOrchestrator as _信号计算器`——零代码改动 |
|
||||
| `main.py` | 修复损坏的调用点(原本就 broken) |
|
||||
| `test_策略验证.py` | 零改动——`信号计算器` 类名不变 |
|
||||
| Python 信号函数 | 零改动——`import_by_name` 路径不变 |
|
||||
| `Position.update()` | 零改动——Rust 状态机不变 |
|
||||
|
||||
## 9. 风险
|
||||
|
||||
| 风险 | 缓解 |
|
||||
|------|------|
|
||||
| `_提取行情()` 中 `k.时间戳` 是 i64(Rust K线),不是 Python datetime | 已由 Rust `PositionPy::时间戳转datetime` 处理 |
|
||||
| `SignalEngine.更新_完整()` 的基础周期可能与 `_基础周期` 不一致 | 统一从 `分析器.周期组[0]` 获取 |
|
||||
| Python 信号函数的 `**kwargs` 中 `freq` 是字符串(来自 SignalsParser) | `_执行Python信号函数` 中 `int(freq)` 转换 |
|
||||
| `list_signals()` 返回的是 Rust 注册名,不含模块路径 | 按短名匹配(`youwukuncheng_中枢第三买卖点_V230602` 不含 `chanlun.signals.` 前缀) |
|
||||
@@ -0,0 +1,200 @@
|
||||
# 子项目 2:信号函数 API + 移植第一个真实信号 — 设计文档
|
||||
|
||||
- 日期:2026-06-23
|
||||
- 所属:「全 Rust 信号计算迁移」第 2 个子项目(共 4 个)
|
||||
- 前置:子项目 1 已完成(`#[signal]` 宏 + `inventory` 注册表)
|
||||
- 参考:`chanlun-py/chanlun/signals/youwukuncheng.py`、czsc
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
子项目 1 交付了编译期信号注册机制(`#[signal]` + `inventory` + `SIGNAL_REGISTRY`),探针信号已验证注册→查表链路。现在是时候移植第一个真实信号函数,并在过程中建立 Rust 信号函数的**编写规范**和**辅助 API**。
|
||||
|
||||
子项目 2 交付:
|
||||
1. **信号函数便捷 API** — 扩展 trait,让 Rust 信号函数代码读起来接近 Python 版本
|
||||
2. **确保指标按需增量计算** — 信号函数可确保所需指标已计算
|
||||
3. **移植 youwukuncheng_中枢第三买卖点_V230602** — 第一个真实信号(3 种信号变体)
|
||||
4. **集成测试** — Rust vs Python 输出对比
|
||||
|
||||
## 2. 范围
|
||||
|
||||
### 纳入
|
||||
- `chanlun/src/signal/functions/` 模块(信号函数目录)
|
||||
- `chanlun/src/signal/functions/youwukuncheng.rs` — 移植的中枢第三买卖点信号
|
||||
- 便捷扩展 trait:`IndicatorAccess`(K线指标读取)、`ObserverAccess`(观察者便捷访问)
|
||||
- 参数提取辅助函数(`params_ext.rs`)
|
||||
- 确保指标 API:`观察者::确保指标已计算(&self)`
|
||||
- 集成测试:喂入 `.nb` 数据,Rust 信号输出 vs Python 信号输出
|
||||
- `#[signal]` 注册 youwukuncheng
|
||||
|
||||
### 不纳入(后续子项目)
|
||||
- 信号计算引擎 + `call_signal` PyO3 分发器(子项目 3)
|
||||
- Position.update 状态机(子项目 4)
|
||||
- 其他信号函数(demo.py 中的 macd_金叉、cxt_bi_end 等)
|
||||
- Python 侧可直接调用的 PyO3 信号函数分发器
|
||||
|
||||
## 3. 关键设计决策
|
||||
|
||||
| 决策 | 选择 | 理由 |
|
||||
|---|---|---|
|
||||
| 便捷 API 形式 | **直接给 K线 / 观察者 加方法** | 简洁,不需要 import 额外 trait。已有前例(观察者.当前缠K()) |
|
||||
| 指标访问封装 | **方法返回 Option,隐藏 RwLock** | 信号函数不应关心锁细节;`kline.macd()` 返回 `Option<&MACD>` |
|
||||
| 确保指标机制 | **观察者.确保指标已计算() 重跑计算器** | 简单,复用现有 `指标计算器::计算并挂载`。后续子项目 3 由计算引擎在调用前统一 ensure |
|
||||
| 参数提取 | **独立 `params` 子模块,纯函数** | `HashMap<String, Value>` 的字符串/数字提取到处都需要,集中处理 |
|
||||
| 信号函数位置 | `chanlun/src/signal/functions/` | 与 registry 同 crate,`#[signal]` emit 的 `crate::` 路径可直接解析 |
|
||||
| 测试策略 | **Rust 集成测试 + Python 对比** | 加载 .nb → 跑 Rust 信号 → 序列化输出;Python 侧同样跑 → diff |
|
||||
|
||||
## 4. 文件结构
|
||||
|
||||
```
|
||||
chanlun/src/signal/
|
||||
├── mod.rs ← pub mod functions; pub mod params;
|
||||
├── functions/
|
||||
│ ├── mod.rs ← pub mod youwukuncheng;
|
||||
│ └── youwukuncheng.rs ← #[signal] fn youwukuncheng_中枢第三买卖点_V230602
|
||||
├── params.rs ← 参数提取辅助函数
|
||||
├── ... (已有: signal, factor, event, position, operate, registry)
|
||||
│
|
||||
chanlun/src/kline/
|
||||
├── bar.rs ← 给 K线 加便捷指标访问方法
|
||||
│
|
||||
chanlun/src/business/
|
||||
├── observer.rs ← 给 观察者 加便捷方法 + 确保指标
|
||||
|
||||
chanlun/tests/
|
||||
├── test_signal_youwukuncheng.rs ← 集成测试(Rust vs Python 对比)
|
||||
```
|
||||
|
||||
## 5. 便捷 API 设计
|
||||
|
||||
### 5.1 K线 便捷指标访问(`bar.rs` 新增方法)
|
||||
|
||||
将现有的 `k线.指标.read().macd()` 封装为直接的 `k线.macd()`:
|
||||
|
||||
```rust
|
||||
impl K线 {
|
||||
/// 读取 MACD 指标(已计算则返回引用,否则 None)
|
||||
pub fn macd(&self) -> Option<&平滑异同移动平均线> { ... }
|
||||
pub fn rsi(&self) -> Option<&相对强弱指数> { ... }
|
||||
pub fn kdj(&self) -> Option<&随机指标> { ... }
|
||||
pub fn boll(&self) -> Option<&布林带> { ... }
|
||||
/// 读取均线值,如 ma("SMA_5") → Option<f64>
|
||||
pub fn ma(&self, key: &str) -> Option<f64> { ... }
|
||||
}
|
||||
```
|
||||
|
||||
同样给 `缠论K线` 加转发方法(委托给 `self.标的K线`)。
|
||||
|
||||
### 5.2 观察者便捷访问(`observer.rs` 新增方法)
|
||||
|
||||
```rust
|
||||
impl 观察者 {
|
||||
/// 按偏移取普K(di=1 为最后一根)
|
||||
pub fn 普K偏移(&self, di: usize) -> Option<&Arc<K线>> { ... }
|
||||
/// 按偏移取缠K
|
||||
pub fn 缠K偏移(&self, di: usize) -> Option<&Arc<缠论K线>> { ... }
|
||||
/// 最后 N 根缠K
|
||||
pub fn 最后缠K序列(&self, n: usize) -> &[Arc<缠论K线>] { ... }
|
||||
/// 线段级中枢序列(= 中枢序列组[1])
|
||||
pub fn 线段中枢序列(&self) -> &Vec<Arc<中枢>> { ... }
|
||||
/// 确保所有 K 线上的指标已计算(调用 指标计算器::计算并挂载)
|
||||
pub fn 确保指标已计算(&self) { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 参数提取(`signal/params.rs`)
|
||||
|
||||
```rust
|
||||
/// 从 params HashMap 提取字符串参数
|
||||
pub fn get_string(params: &HashMap<String, Value>, key: &str, default: &str) -> String;
|
||||
/// 从 params HashMap 提取整数参数
|
||||
pub fn get_int(params: &HashMap<String, Value>, key: &str, default: i64) -> i64;
|
||||
/// 从 params HashMap 提取浮点参数
|
||||
pub fn get_f64(params: &HashMap<String, Value>, key: &str, default: f64) -> f64;
|
||||
```
|
||||
|
||||
这些是纯辅助函数,不做任何复杂逻辑。
|
||||
|
||||
## 6. youwukuncheng 移植要点
|
||||
|
||||
### 6.1 信号逻辑
|
||||
|
||||
Python 版 143 行 → Rust 预计 ~200 行(含类型标注和 RwLock 读取)。
|
||||
|
||||
三种产出信号(k3 后缀均为 `V230602`):
|
||||
|
||||
| k3 | 触发条件 | v1 | v2 | score |
|
||||
|---|---|---|---|---|
|
||||
| `中枢段DEA穿越2V230602` | 同级第三买卖线段内 DEA 穿越 0 轴 | 中枢段DEA穿越2 | 三买/三卖 | max(0, 100-偏移×5) |
|
||||
| `DEA穿越0轴V230602` | 本级第三买卖线处 DEA 在 0 轴同侧 | DEA穿越0轴 | 三买/三卖 | max(0, 100-偏移×5) |
|
||||
| `首次穿越0轴V230602` | DIF 首次反穿 0 轴 + 分型确认 | 首次穿越0轴 | 三买/三卖 | max(0, 100-偏移×5) |
|
||||
|
||||
### 6.2 关键 Rust 对应
|
||||
|
||||
| Python | Rust |
|
||||
|---|---|
|
||||
| `观察员.当前缠K` | `obs.当前缠K()` |
|
||||
| `观察员.中枢序列` | `obs.中枢序列()` (笔中枢) 或 `obs.线段中枢序列()` (线段中枢) |
|
||||
| `当前中枢.基础序列[0].标识` | `当前中枢.基础序列.read()[0].标识.read().as_str()` |
|
||||
| `当前中枢.当前状态()` | `当前中枢.当前状态()` |
|
||||
| `当前中枢.本级_第三买卖线` | `当前中枢.本级_第三买卖线.read().as_ref()` |
|
||||
| `当前中枢.完整性("实")` | `当前中枢.完整性("实")` |
|
||||
| `k.标的K线.macd.DEA` | `k.标的K线.read().macd().map(\|m\| m.DEA)` |
|
||||
| `k.分型 is 分型结构.底` | `*k.分型.read() == Some(分型结构::底)` |
|
||||
| `分型.从缠K序列中获取分型(序列, k)` | `分型::从缠K序列中获取分型(序列, k)` |
|
||||
| `虚线.统计MACD行为(普K序列, 8, 3)` | `虚线::统计MACD行为(&普K序列, 8, 3)` |
|
||||
| `段.获取普K序列(观察员.观察员)` | `段.获取普K序列(&obs.普通K线序列)` |
|
||||
|
||||
### 6.3 注意事项
|
||||
|
||||
1. **lock 顺序**:读取 `基础序列`、`武`、`标的K线`、`指标` 时注意 RwLock 不可重入。同一作用域内避免同时持有多个写锁。本函数只有读操作,安全。
|
||||
2. **AtomicI64**:`序号` 用 `.load(Ordering::Relaxed)` 读取
|
||||
3. **Option 链**:Python 的 `x.y.z` 在 Rust 中是 `x.y.read().z`,需要处理 `Option`
|
||||
4. **空信号返回**:Python 返回 `create_single_signal(k1, k2, k3)`(v1=v2=v3="任意");Rust 返回 `vec![Signal::new_empty(k1, k2, k3)]`
|
||||
|
||||
## 7. 确保指标 API
|
||||
|
||||
```rust
|
||||
impl 观察者 {
|
||||
/// 确保所有 K线上的指标已计算。
|
||||
/// 如果 配置.计算指标 为 true 且序列非空,则调用 指标计算器::计算并挂载。
|
||||
pub fn 确保指标已计算(&self) {
|
||||
if self.配置.计算指标 && !self.普通K线序列.is_empty() {
|
||||
指标计算器::计算并挂载(&self.普通K线序列, &self.配置);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
信号函数在入口调用一次 `obs.确保指标已计算()`(幂等——计算器检测已计算的值会跳过)。
|
||||
|
||||
注:后续子项目 3 的信号计算引擎会在调用任何信号前统一 ensure,信号函数内部的 ensure 调用届时可移除。
|
||||
|
||||
## 8. 测试设计
|
||||
|
||||
### 8.1 集成测试(`chanlun/tests/test_signal_youwukuncheng.rs`)
|
||||
|
||||
1. 加载测试 `.nb` 文件(选择已有中枢结构的 btcusd 数据)
|
||||
2. 创建观察者,喂入 K 线,触发分析
|
||||
3. 调用 `youwukuncheng_中枢第三买卖点_V230602(&obs, ¶ms)`
|
||||
4. 验证返回的 `Vec<Signal>` 非空,信号 key/value 格式正确
|
||||
5. 与 Python 版输出对比(golden 方式:运行 Python 脚本生成预期输出文件,Rust 测试读取对比)
|
||||
|
||||
### 8.2 测试数据
|
||||
|
||||
使用已有测试 `.nb` 文件(如 `btcusd-86400-...`,日线数据有丰富的中枢结构)。
|
||||
|
||||
## 9. 错误处理
|
||||
|
||||
- 信号函数内部所有 `Option` 缺值 → 返回空信号(与 Python 行为一致)
|
||||
- `确保指标已计算` 失败 → 静默跳过(指标不存在时信号函数内部 `macd().is_none()` 自然会返回空)
|
||||
- `#[signal]` 注册失败(重名)→ 子项目 1 已处理(编译期 panic)
|
||||
|
||||
## 10. 已知取舍
|
||||
|
||||
- **便捷方法只加常用读路径**:`macd()/rsi()/kdj()/boll()/ma()` + 偏移访问。复杂查询(如遍历所有 K 线做自定义分析)直接用底层 API。
|
||||
- **确保指标基于现有管线**:不做 czsc 式的 TaCache(已决策,见子项目 1 §3)。SignalFn 签名保持 `&观察者` 单参数。
|
||||
- **信号函数在 lib 内**:不暴露为独立的 `chanlun-signals` crate。与子项目 1 决策一致——信号函数同 crate,可直接访问 observer 内部。
|
||||
|
||||
## 11. 许可证
|
||||
|
||||
新增文件沿用项目 MIT 头。youwukuncheng 移植自项目自有 Python 代码,不涉及第三方许可证。
|
||||
Reference in New Issue
Block a user