Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1477cde86b | |||
| da5222d960 |
@@ -1,13 +0,0 @@
|
||||
Copyright [2025] [zengbin93]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,19 +0,0 @@
|
||||
Copyright (c) 2012-2019 Richard Jones <richard@python.org>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,19 +0,0 @@
|
||||
Copyright (c) 2008-2011 Volvox Development Team
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -1,32 +0,0 @@
|
||||
chanlun — 缠论技术分析库
|
||||
===========================
|
||||
|
||||
Copyright (c) 2026 YuYuKunKun
|
||||
|
||||
This product includes software developed by third-party open source projects:
|
||||
|
||||
|
||||
----------------------------------------------------------------------
|
||||
1. czsc
|
||||
Repository: <https://github.com/waditu/czsc>
|
||||
License: Apache License 2.0
|
||||
Copyright (c) 2025 zengbin93
|
||||
Used in: chanlun-py/chanlun/chan_external.py(部分代码片段)
|
||||
|
||||
----------------------------------------------------------------------
|
||||
2. parse
|
||||
Repository: <https://github.com/r1chardj0n3s/parse>
|
||||
License: MIT License
|
||||
Copyright (c) 2012-2019 Richard Jones <richard@python.org>
|
||||
Used in: chanlun-py/chanlun/parse.py
|
||||
|
||||
----------------------------------------------------------------------
|
||||
3. termcolor
|
||||
Repository: <https://github.com/termcolor/termcolor>
|
||||
License: MIT License
|
||||
Copyright (c) 2008-2011 Volvox Development Team
|
||||
Used in: chanlun-py/chanlun/termcolor.py
|
||||
|
||||
|
||||
----------------------------------------------------------------------
|
||||
Full licenses are available in the LICENSES/ directory.
|
||||
@@ -19,7 +19,7 @@ import chanlun
|
||||
# 创建配置(全部默认值)
|
||||
config = chanlun.缠论配置()
|
||||
|
||||
# 读取 K 线数据文件(文件名需遵循 `符号-周期-起始时间戳-结束时间戳.nb` 格式)
|
||||
# 读取 K 线数据文件(文件名需遵循 `符号-周期-起始时间戳-结束时间戳.nb` 格式,如 `btcusd-300-1631772074-1632222374.nb`)
|
||||
obs = chanlun.观察者.读取数据文件("path/to/btcusd-300-1631772074-1632222374.nb", config)
|
||||
|
||||
# 查看各层级序列
|
||||
@@ -33,171 +33,6 @@ analyzer = chanlun.立体分析器("BTCUSD", [60, 60*5, 60*5*6], config)
|
||||
# 逐根投喂 K 线...
|
||||
```
|
||||
|
||||
## 信号计算 (Rust 核心)
|
||||
|
||||
信号框架(Signal/Factor/Event/Position/Operate)已全部迁移到 Rust 核心,通过 PyO3 暴露给 Python。
|
||||
|
||||
### 调用信号函数
|
||||
|
||||
```python
|
||||
from chanlun._chanlun import 信号引擎, call_signal, list_signals, get_signal_template
|
||||
|
||||
# 准备数据
|
||||
analyzer = chanlun.立体分析器("btcusd", [300, 900, 3600], chanlun.缠论配置())
|
||||
for k in klines:
|
||||
analyzer.投喂K线(k)
|
||||
|
||||
# ── 方式 1: 信号引擎(批量) ──
|
||||
engine = 信号引擎(信号配置=[
|
||||
{"name": "bar_zdt_V230331", "freq": "300"},
|
||||
{"name": "macd_金叉_V260601", "freq": "300", "fast": "13", "slow": "31"},
|
||||
])
|
||||
engine.自动挂载指标(analyzer)
|
||||
result = engine.更新(analyzer) # → {key: value}
|
||||
full = engine.更新_完整(analyzer) # → {"signals": {...}, "market": {...}}
|
||||
|
||||
# ── 方式 2: call_signal(单函数) ──
|
||||
obs = analyzer._单体分析器[300]
|
||||
signals = call_signal("macd_金叉_V260601", obs, {"freq": "5分钟", "di": "1"})
|
||||
for s in signals:
|
||||
print(s.key, s.value)
|
||||
|
||||
# ── 方式 3: SignalOrchestrator(高级编排,含行情) ──
|
||||
from chanlun.signal_orchestrator import SignalOrchestrator
|
||||
orch = SignalOrchestrator(analyzer, 信号配置=[...])
|
||||
orch.更新()
|
||||
orch.信号字典 # → {信号..., "symbol": "btcusd", "close": 50050, ...}
|
||||
|
||||
# ── 方式 4: 注册表探索 ──
|
||||
list_signals() # → ["bar_zdt_V230331", ...] (7个)
|
||||
get_signal_template("bar_zdt_V230331") # → "{freq}_D{di}_涨跌停V230331"
|
||||
```
|
||||
|
||||
### 已注册信号函数(8个)
|
||||
|
||||
| 信号名 | 模板 | 说明 |
|
||||
|--------|------|------|
|
||||
| `bar_zdt_V230331` | `{freq}_D{di}_涨跌停V230331` | 涨跌停检测 |
|
||||
| `macd_金叉_V260601` | `{freq}_D{di}#MACD#{fast}#{slow}#{signal}_MACD交叉V260601` | MACD 金叉/死叉 |
|
||||
| `tas_macd_direct_V221106` | `{freq}_D{di}#MACD#{fast}#{slow}#{signal}_MACD方向V221106` | MACD DIF 方向 |
|
||||
| `tas_ma_base_V230313` | `{freq}_D{di}#{ma_type}#{timeperiod}MO{max_overlap}_BS辅助V230313` | MA 均线多空 |
|
||||
| `cxt_停顿分型_V230106` | `{freq}_D{di}停顿分型_BE辅助V230106` | 停顿分型检测 |
|
||||
| `cxt_bi_end_V230222` | `{freq}_D1MO{max_overlap}_BE辅助V230222` | 笔结束辅助 |
|
||||
| `youwukuncheng_中枢第三买卖点_V230602` | `{freq}_D1MO{max_overlap}_中枢第三买卖点V230602` | 中枢第三买卖点 |
|
||||
|
||||
### Python 信号函数混合调用
|
||||
|
||||
编排器支持 Rust + Python 信号混合执行:
|
||||
|
||||
```python
|
||||
orch = SignalOrchestrator(analyzer, 信号配置=[
|
||||
{"name": "bar_zdt_V230331", "freq": 300}, # → Rust 路径
|
||||
{"name": "chanlun.signals.demo.tas_ma_base_V230313", ...}, # → Python 回退
|
||||
])
|
||||
orch.更新() # 自动分类,Rust 批量 + Python 逐个
|
||||
```
|
||||
|
||||
## 编写信号函数
|
||||
|
||||
### Rust 信号函数(推荐)
|
||||
|
||||
```rust
|
||||
// chanlun/src/signal/functions/my_signals.rs
|
||||
use chanlun_signal_macros::signal;
|
||||
use chanlun::business::observer::观察者;
|
||||
use chanlun::signal::Signal;
|
||||
use std::collections::HashMap;
|
||||
use serde_json::Value;
|
||||
|
||||
#[signal(
|
||||
name = "my_signal_V000001",
|
||||
template = "{freq}_D{di}_模板V000001"
|
||||
)]
|
||||
pub fn my_signal_V000001(obs: &观察者, params: &HashMap<String, Value>) -> Vec<Signal> {
|
||||
obs.确保指标已计算();
|
||||
|
||||
let di = params.get("di").and_then(|v| v.as_i64()).unwrap_or(1) as usize;
|
||||
let freq = params.get("freq").and_then(|v| v.as_str()).unwrap_or("日线");
|
||||
|
||||
let k1 = freq.to_string();
|
||||
let k2 = format!("D{di}");
|
||||
let k3 = "模板V000001";
|
||||
|
||||
let klines = &obs.普通K线序列;
|
||||
if klines.len() < di + 1 {
|
||||
return vec![Signal::new_empty(&k1, &k2, k3)];
|
||||
}
|
||||
|
||||
let k线 = &klines[klines.len() - di];
|
||||
if k线.收盘价 > k线.开盘价 {
|
||||
vec![Signal::new(&k1, &k2, k3, "阳线", "任意", "任意", 0)]
|
||||
} else {
|
||||
vec![Signal::new_empty(&k1, &k2, k3)]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
然后在 `chanlun/src/signal/functions/mod.rs` 中添加 `pub mod my_signals;`,重新编译即可自动注册。
|
||||
|
||||
### 动态加载 .so 插件
|
||||
|
||||
信号函数可以编译为独立 `.so` 动态库,运行时加载。支持两种注册方式。
|
||||
|
||||
**方式 A:手动 C-ABI 注册**
|
||||
|
||||
```rust
|
||||
// 独立 crate (cdylib)
|
||||
fn my_plugin_signal(obs: &观察者, params: &HashMap<String, Value>) -> Vec<Signal> { ... }
|
||||
|
||||
unsafe extern "C" {
|
||||
fn chanlun_register_signal(name: *const c_char, template: *const c_char, func: SignalFn) -> i32;
|
||||
fn chanlun_unregister_signal(name: *const c_char) -> i32;
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn init_plugin() -> i32 {
|
||||
chanlun_register_signal(
|
||||
c"my_plugin_signal_V000001".as_ptr(),
|
||||
c"{freq}_D{di}_模板V000001".as_ptr(),
|
||||
my_plugin_signal,
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**方式 B:`#[signal]` 宏 + inventory 批量提交**
|
||||
|
||||
```rust
|
||||
use chanlun_signal_macros::signal;
|
||||
|
||||
#[signal(name = "my_plugin_signal_V000001", template = "...", crate_path = "::chanlun")]
|
||||
fn my_plugin_signal_V000001(obs: &观察者, params: &HashMap<String, Value>) -> Vec<Signal> { ... }
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn init_plugin() -> i32 {
|
||||
for desc in inventory::iter::<SignalDescriptor> {
|
||||
chanlun_register_signal(name_ptr, template_ptr, desc.func);
|
||||
}
|
||||
0
|
||||
}
|
||||
```
|
||||
|
||||
Python 加载:
|
||||
|
||||
```python
|
||||
import ctypes, os, sys
|
||||
sys.setdlopenflags(os.RTLD_LAZY | os.RTLD_GLOBAL)
|
||||
import chanlun._chanlun # 先加载宿主
|
||||
|
||||
plugin = ctypes.CDLL("./libmy_plugin.so")
|
||||
plugin.init_plugin()
|
||||
|
||||
# 插件信号现在可通过 call_signal / 信号引擎 调用
|
||||
from chanlun._chanlun import call_signal
|
||||
call_signal("my_plugin_signal_V000001", obs, params)
|
||||
```
|
||||
|
||||
完整示例见 [examples/plugin-demo/](./examples/plugin-demo/)。
|
||||
|
||||
## 从源码构建
|
||||
|
||||
前置依赖: [Rust](https://www.rust-lang.org) + [maturin](https://www.maturin.rs)
|
||||
@@ -206,34 +41,30 @@ call_signal("my_plugin_signal_V000001", obs, params)
|
||||
pip install maturin
|
||||
|
||||
# 开发模式(直接安装到当前 venv)
|
||||
cd chanlun-py && maturin develop
|
||||
maturin develop
|
||||
|
||||
# 或构建 wheel
|
||||
cd chanlun-py && maturin build --release
|
||||
maturin build --release
|
||||
pip install target/wheels/chanlun-*.whl
|
||||
```
|
||||
|
||||
也可使用项目内的 `build.sh`:
|
||||
|
||||
```bash
|
||||
cd chanlun-py
|
||||
./build.sh develop # 开发安装
|
||||
./build.sh wheel # 构建 wheel
|
||||
./build.sh test # 运行集成测试
|
||||
```
|
||||
|
||||
## 导出类
|
||||
|
||||
| 类别 | 类名 | 说明 |
|
||||
|------|------|------|
|
||||
| 枚举 | `买卖点类型`, `相对方向`, `分型结构`, `Operate` | 缠论基础枚举 |
|
||||
| 枚举 | `买卖点类型`, `相对方向`, `分型结构` | 缠论基础枚举 |
|
||||
| 数据 | `缺口`, `K线`, `缠论K线` | K 线数据结构 |
|
||||
| 结构 | `分型`, `虚线`, `线段特征`, `特征分型` | 分析层级结构 |
|
||||
| 指标 | `平滑异同移动平均线`, `相对强弱指数`, `随机指标` | MACD/RSI/KDJ |
|
||||
| 算法 | `笔`, `线段`, `中枢`, `背驰分析` | 识别算法 |
|
||||
| 业务 | `缠论配置`, `观察者`, `K线合成器`, `立体分析器`, `买卖点` | 分析框架 |
|
||||
| 信号 | `Signal`, `Factor`, `Event`, `Position`, `信号引擎` | 信号匹配+计算引擎 |
|
||||
| 注册表 | `call_signal`, `list_signals`, `get_signal_template`, `register_signal`, `unregister_signal` | 信号发现+动态注册 |
|
||||
| 业务 | `缠论配置`, `基础买卖点`, `买卖点`, `观察者`, `K线合成器`, `立体分析器` | 分析框架 |
|
||||
|
||||
## 兼容性
|
||||
|
||||
|
||||
@@ -1,975 +0,0 @@
|
||||
# chan.py — 缠论技术分析库 AI 技能
|
||||
|
||||
> 约 6900 行 Python,从原始 K 线到买卖点的全链路流式缠论分析。所有标识符使用中文。
|
||||
|
||||
---
|
||||
|
||||
## Q1: 如何从 .nb 文件加载数据并完成分析?
|
||||
|
||||
```python
|
||||
from chanlun import chan
|
||||
|
||||
config = chan.缠论配置(标识="btcusd")
|
||||
obs = chan.观察者.读取数据文件("btcusd-300-xxx.nb", 配置=config)
|
||||
|
||||
# 加载即完成全部分析,直接读取结果
|
||||
obs.笔序列 # List[虚线],标识="笔"
|
||||
obs.线段序列 # List[虚线],标识="线段"
|
||||
obs.中枢序列 # List[中枢],标识="线段中枢"
|
||||
obs.笔_中枢序列 # List[中枢],标识="笔中枢"
|
||||
obs.分型序列 # List[分型]
|
||||
obs.缠论K线序列 # List[缠论K线]
|
||||
|
||||
# 高级层级
|
||||
obs.扩展线段序列 # 标识="扩展线段"
|
||||
obs.扩展中枢序列 # 标识="扩展线段中枢"
|
||||
obs.线段_线段序列 # 线段作为笔再划分
|
||||
obs.扩展线段序列_扩展线段 # 扩展线段作为笔再划分
|
||||
```
|
||||
|
||||
`.nb` 文件格式:每根 K 线 48 字节大端 `struct.pack(">6d")`,依次为 时间戳/开/高/低/收/量(均为 float64)。
|
||||
|
||||
---
|
||||
|
||||
## Q2: 如何流式实时分析?
|
||||
|
||||
```python
|
||||
obs = chan.观察者(符号="btcusd", 周期=300, 配置=config)
|
||||
|
||||
# 方式一:投喂原始数据
|
||||
obs.投喂原始数据(时间戳=datetime.now(), 开=50000, 高=50100, 低=49900, 收=50050, 量=100)
|
||||
|
||||
# 方式二:投喂预构建 K 线对象
|
||||
obs.增加原始K线(k线对象)
|
||||
|
||||
# 每次投喂都增量更新全链路:缠K → 分型 → 笔 → 线段 → 中枢
|
||||
# 结果即刻可用
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Q3: 如何做多周期立体分析?
|
||||
|
||||
```python
|
||||
ma = chan.立体分析器(
|
||||
符号="btcusd",
|
||||
周期组=[300, 1800], # [输入周期, 显示周期]
|
||||
配置=config,
|
||||
配置组={1800: chan.缠论配置(笔内元素数量=7)}, # 可选:按周期覆盖配置
|
||||
)
|
||||
|
||||
# 只投喂最小周期 K 线,合成器自动合成大周期
|
||||
for kline in klines_300:
|
||||
ma.投喂K线(kline)
|
||||
|
||||
# 获取各周期结果
|
||||
obs_300 = ma._单体分析器[300]
|
||||
obs_1800 = ma._单体分析器[1800]
|
||||
```
|
||||
|
||||
内部机制:
|
||||
- 内置 `K线合成器` 将小周期 K 线合成为大周期
|
||||
- 合成器事件回调 → 大周期 K 线完成 → 触发对应 `观察者.__处理数据`
|
||||
- 非显示周期的 `基础缠K序列` 引用显示周期的 `缠论K线序列`
|
||||
|
||||
---
|
||||
|
||||
## Q4: 如何单独使用 K 线合成器?
|
||||
|
||||
```python
|
||||
def 回调(信号类型, 标识, 周期, 完成K线):
|
||||
"""信号类型: "K线完成" """
|
||||
print(f"{标识} {周期}s 完成一根K线: {完成K线.收盘价}")
|
||||
|
||||
synth = chan.K线合成器(标识="btcusd", 周期组=[300, 900, 1800], 事件回调=回调)
|
||||
|
||||
# 投喂最小周期数据
|
||||
synth.投喂(时间戳, 开, 高, 低, 收, 量)
|
||||
# 或
|
||||
synth.投喂K线(普K)
|
||||
|
||||
# 查询当前合成中的 K 线
|
||||
k = synth.获取当前K线(周期=900)
|
||||
|
||||
# 支持后续设置回调
|
||||
synth.设置事件回调(新回调函数)
|
||||
```
|
||||
|
||||
合成器按 `(时间戳 // 周期) * 周期` 对齐时间戳到周期边界。
|
||||
|
||||
---
|
||||
|
||||
## Q5: 缠论配置有哪些关键参数?
|
||||
|
||||
```python
|
||||
config = chan.缠论配置(
|
||||
标识="btcusd",
|
||||
|
||||
# ---- 分析开关(按需关闭以跳过某层级)----
|
||||
分析笔=True, # 是否分析笔
|
||||
分析线段=True, # 是否分析线段
|
||||
分析扩展线段=True, # 是否分析扩展线段
|
||||
分析笔中枢=True, # 是否分析笔中枢
|
||||
分析线段中枢=True, # 是否分析线段中枢
|
||||
计算指标=True, # 是否计算技术指标
|
||||
|
||||
# ---- 笔参数(影响笔划分粒度)----
|
||||
笔内元素数量=5, # 笔内最少缠K数(含端点)
|
||||
笔内相同终点取舍=False, # True=取最后, False=取第一个
|
||||
笔内起始分型包含整笔=False,
|
||||
笔弱化=False, # 笔弱化开关
|
||||
笔弱化_原始数量=3, # 弱化参考原始K线数量
|
||||
笔次级成笔=False, # 次级成笔
|
||||
|
||||
# ---- 线段参数 ----
|
||||
线段_非缺口下穿刺=False, # 非缺口状态的贯穿伤回退
|
||||
线段_特征序列忽视老阴老阳=False, # True=忽视缺口全以无缺口对待
|
||||
线段_缺口后紧急修正=True, # 缺口后紧急修正
|
||||
线段_修正=False, # 短路修正(不建议使用)
|
||||
线段内部中枢图显=True, # 显示线段内部中枢
|
||||
扩展线段_当下分析=False, # True=以当下分析, False=以事后分析
|
||||
|
||||
# ---- 指标参数 ----
|
||||
指标计算方式="收", # "开"/"高"/"低"/"收"/"高低均值"/"高低收均值"/"开高低收均值"
|
||||
平滑异同移动平均线_快线周期=13,
|
||||
平滑异同移动平均线_慢线周期=31,
|
||||
平滑异同移动平均线_信号周期=11,
|
||||
相对强弱指数_周期=13,
|
||||
相对强弱指数_超买阈值=75,
|
||||
相对强弱指数_超卖阈值=25,
|
||||
随机指标_RSV周期=13,
|
||||
随机指标_K值平滑周期=5,
|
||||
随机指标_D值平滑周期=5,
|
||||
随机指标_超买阈值=80,
|
||||
随机指标_超卖阈值=20,
|
||||
计算BOLL=False, # 是否计算布林带
|
||||
布林带_周期=20,
|
||||
布林带_标准差倍数=2.0,
|
||||
|
||||
# ---- 多指标参数列表 ----
|
||||
MACD_参数列表=[], # [(key, 快线, 慢线, 信号), ...]
|
||||
RSI_周期列表=[], # [(key, 周期), ...]
|
||||
KDJ_参数列表=[], # [(key, RSV周期, K平滑, D平滑), ...]
|
||||
BOLL_参数列表=[], # [(key, 周期, 标准差倍数), ...]
|
||||
均线_类型列表=[], # SMA/EMA 类型列表
|
||||
均线_周期列表=[], # 对应周期列表
|
||||
|
||||
# ---- 买卖点 ----
|
||||
买卖点偏移=1, # 最大偏移量
|
||||
买卖点激进识别=False, # 激进模式不检查分型完整性
|
||||
买卖点与MACD柱强相关=False, # True=卖点需正值 买点需负值
|
||||
买卖点错过误差值=0.01, # 距离买卖点值的容差
|
||||
买卖点_指标模式="配置", # "任意"/"配置"/"全量"/"相对"
|
||||
买卖点_指标匹配_MACD=True,
|
||||
买卖点_指标匹配_KDJ=True,
|
||||
买卖点_指标匹配_RSI=True,
|
||||
|
||||
# ---- 线段内部背驰 ----
|
||||
线段内部背驰_MACD=True,
|
||||
线段内部背驰_斜率=True,
|
||||
线段内部背驰_测度=True,
|
||||
线段内部背驰_模式="相对", # "任意"/"配置"/"全量"/"相对"
|
||||
|
||||
# ---- 推送/图显 ----
|
||||
推送K线=True, 推送笔=True, 推送线段=True, 推送中枢=True,
|
||||
# ...另有 图表展示, 图表展示_笔, _线段, _中枢_* 等 20+ 图显字段
|
||||
)
|
||||
```
|
||||
|
||||
全部 74 个字段通过 `config.model_fields()` 获取。
|
||||
|
||||
---
|
||||
|
||||
## Q6: 如何创建和对比配置?
|
||||
|
||||
```python
|
||||
# 构造
|
||||
c1 = chan.缠论配置(笔内元素数量=5, 线段_缺口后紧急修正=True)
|
||||
c2 = chan.缠论配置(笔内元素数量=7, 线段_缺口后紧急修正=False)
|
||||
|
||||
# 差异对比 → {字段名: 新值}
|
||||
c1.对比(c2) # {"笔内元素数量": 7}
|
||||
|
||||
# 深拷贝 + 部分更新
|
||||
c3 = c1.model_copy(update={"笔内元素数量": 8}, deep=True)
|
||||
|
||||
# 创建不推送变体(所有推送标志 = False)
|
||||
c_no_push = c1.不推送()
|
||||
|
||||
# 序列化
|
||||
c1.to_dict() # dict
|
||||
c1.to_json() # JSON 字符串
|
||||
c1.保存配置("path.json")
|
||||
|
||||
# 反序列化
|
||||
chan.缠论配置.from_dict(data)
|
||||
chan.缠论配置.from_json(json_str)
|
||||
chan.缠论配置.加载配置("path.json")
|
||||
|
||||
# 按序号重组(兼容旧版扁平键名:"1_笔模式" → {1: {笔模式: ...}})
|
||||
chan.缠论配置.按序号重组字典(默认配置, 原始字典)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Q7: 虚线的标识有哪些?如何区分笔和线段?
|
||||
|
||||
`虚线` 是笔和线段的通用数据结构:
|
||||
|
||||
```python
|
||||
虚线(序号, 标识, 文, 武, 级别, 有效性=True)
|
||||
# 文=起点分型, 武=终点分型
|
||||
|
||||
# 标识类型
|
||||
"笔" # 来自 笔.分析
|
||||
"线段" # 来自 线段.分析
|
||||
"扩展线段" # 来自 线段.扩展分析
|
||||
"线段_线段" # 线段再做一次线段分析
|
||||
"扩展线段_扩展线段" # 扩展线段再做一次线段分析
|
||||
```
|
||||
|
||||
关键属性和方法:
|
||||
```python
|
||||
seg.方向 # 相对方向.向上 / 相对方向.向下
|
||||
seg.高 / seg.低 # max(文,武).高 / min(文,武).低
|
||||
seg.文 / seg.武 # 起点分型 / 终点分型
|
||||
seg.笔序列 # 内部笔序列(仅线段有)
|
||||
seg.之前是(prev) # prev.武 == seg.文 ?
|
||||
seg.之后是(next) # seg.武 == next.文 ?
|
||||
seg.获取普K序列(观察员) # 虚线覆盖的原始K线区间
|
||||
seg.获取缠K序列(观察员) # 虚线覆盖的缠论K线区间
|
||||
```
|
||||
|
||||
笔和线段类本身只包含 `@staticmethod` 算法方法,不实例化:
|
||||
```python
|
||||
笔.分析(当前分型, 分型序列, 笔序列, 缠K序列, 普K序列, 层级, 配置)
|
||||
线段.分析(笔序列, 线段序列, 配置, 层级=0)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Q8: 如何判断背驰?
|
||||
|
||||
```python
|
||||
# 需要离开段和进入段(都是虚线对象)
|
||||
进入段 = obs.线段序列[-2] # 倒数第二段
|
||||
离开段 = obs.线段序列[-1] # 最后一段
|
||||
|
||||
# 单项背驰
|
||||
chan.背驰分析.MACD背驰(进入段, 离开段, obs.普通K线序列, 方式="总")
|
||||
chan.背驰分析.斜率背驰(进入段, 离开段)
|
||||
chan.背驰分析.测度背驰(进入段, 离开段)
|
||||
|
||||
# 组合背驰
|
||||
chan.背驰分析.全量背驰(进入段, 离开段, obs.普通K线序列) # 三项全满足
|
||||
chan.背驰分析.任意背驰(进入段, 离开段, obs.普通K线序列) # 至少一项
|
||||
chan.背驰分析.任选背驰(进入段, 离开段, obs.普通K线序列) # 至少两项
|
||||
|
||||
# 按配置组合
|
||||
chan.背驰分析.配置背驰(进入段, 离开段, obs.普通K线序列, config)
|
||||
chan.背驰分析.背驰模式(进入段, 离开段, obs.普通K线序列, config, "全量")
|
||||
|
||||
# 判断线段内部是否背驰
|
||||
chan.线段.判断线段内部是否背驰(离开段, obs) # 分析段内中枢背驰
|
||||
```
|
||||
|
||||
MACD 背驰也可通过虚线的类方法直接判断:
|
||||
```python
|
||||
chan.虚线.武之全量MACD均值(obs.普通K线序列, 离开段) # 武端MACD < 均值?
|
||||
chan.虚线.武之MACD均值(obs.普通K线序列, 离开段) # 按方向比较
|
||||
chan.虚线.武之MACD极值(obs.普通K线序列, 离开段) # 武端是否为极值?
|
||||
chan.虚线.买卖意义(离开段, obs) # 盘整背驰 → (bool, 描述)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Q9: 中枢如何操作?
|
||||
|
||||
```python
|
||||
# 中枢 = 三段连续虚线重叠区间
|
||||
hub = chan.中枢.创建(左段, 中段, 右段, 级别=0, 标识="线段中枢")
|
||||
|
||||
# 属性
|
||||
hub.方向 # 基础序列[0].方向.翻转()
|
||||
hub.高 / hub.低 # 中枢区间: min(前三段.高) / max(前三段.低)
|
||||
hub.高高 / hub.低低 # 全部延伸段的最高/最低
|
||||
hub.离开段 # 基础序列[-1]
|
||||
hub.第三买卖线 # 第三类买卖点参考线
|
||||
hub.当前状态() # "中枢之中" / "中枢之上" / "中枢之下"
|
||||
|
||||
# 延伸与扩展
|
||||
hub._添加虚线(新段) # 中枢延伸
|
||||
hub.获取扩展中枢(扩展中枢列表, config) # 9段以上获得扩展中枢
|
||||
|
||||
# 类方法
|
||||
chan.中枢.基础检查(左, 中, 右) # 三段是否首尾相连
|
||||
chan.中枢.从序列中获取中枢(虚线序列, 起始方向, 标识)
|
||||
chan.中枢.分析(虚线序列, 中枢序列) # 自动识别中枢
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Q10: 如何判断中枢的第三类买卖点?
|
||||
|
||||
第三类买卖点出自中枢"离开-回抽不破":
|
||||
|
||||
```python
|
||||
# 分割序列获取第三买卖线
|
||||
前, 后, 第三买卖线, 贯穿伤 = chan.线段.分割序列(离开段, 所属中枢=hub)
|
||||
# 第三买卖线 != None → 存在第三类买卖点
|
||||
|
||||
# 或通过中枢完整性验证
|
||||
hub.完整性(虚实="实") # 验证第三买卖点是否有效
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Q11: 买卖点如何生成?
|
||||
|
||||
`观察者.识别买卖点()` 当前为空实现(`pass`)。买卖点类型已定义,可手动创建:
|
||||
|
||||
```python
|
||||
# 买卖点类型枚举 (18种)
|
||||
chan.买卖点类型.一买 / chan.买卖点类型.一卖
|
||||
chan.买卖点类型.二买 / chan.买卖点类型.二卖
|
||||
chan.买卖点类型.三买 / chan.买卖点类型.三卖
|
||||
chan.买卖点类型.类一买 / chan.买卖点类型.类一卖
|
||||
chan.买卖点类型.类二买 / chan.买卖点类型.类二卖
|
||||
chan.买卖点类型.T1B买 / chan.买卖点类型.T1B卖
|
||||
chan.买卖点类型.T2B买 / chan.买卖点类型.T2B卖
|
||||
chan.买卖点类型.T3B买 / chan.买卖点类型.T3B卖
|
||||
|
||||
# 工厂方法
|
||||
chan.买卖点.一买点(买卖点分型, 当前K线, 标识, 备注, 中枢破位值)
|
||||
chan.买卖点.一卖点(买卖点分型, 当前K线, 标识, 备注, 中枢破位值)
|
||||
# ... 二买点 / 二卖点 / 三买点 / 三卖点 / ...
|
||||
|
||||
# 属性
|
||||
bsp.类型 # 买卖点类型枚举
|
||||
bsp.当前K线 # 关联K线
|
||||
bsp.买卖点分型 # 关联分型
|
||||
bsp.破位值 # 中枢破位值
|
||||
bsp.有效性 # 是否有效
|
||||
bsp.偏移 / bsp.失效偏移
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Q12: 技术指标如何计算和访问?
|
||||
|
||||
指标通过 `指标计算器` 自动挂载到每根 K 线的 `指标` 容器:
|
||||
|
||||
```python
|
||||
# 自动挂载(观察者内部调用)
|
||||
指标计算器.计算并挂载(当前K线, 全序列, config)
|
||||
|
||||
# 访问指标
|
||||
k线.指标.macd # 平滑异同移动平均线 或 None
|
||||
k线.指标.rsi # 相对强弱指数 或 None
|
||||
k线.指标.kdj # 随机指标 或 None
|
||||
k线.指标.boll # 布林带 或 None
|
||||
|
||||
# MACD 字段
|
||||
macd.DIF / macd.DEA / macd.MACD柱 / macd.快线EMA / macd.慢线EMA
|
||||
|
||||
# 手动计算(不依赖观察者)
|
||||
macd1 = 平滑异同移动平均线.首次计算(收盘价, 时间戳, 快线周期=12, 慢线周期=26, 信号周期=9)
|
||||
macd2 = 平滑异同移动平均线.增量计算(macd1, 新收盘价, 新时间戳)
|
||||
# 或从 K 线直接计算
|
||||
macd = 平滑异同移动平均线.首次计算_K线(k线, 计算方式="收盘价")
|
||||
macd = 平滑异同移动平均线.增量计算_K线(前MACD, k线, 计算方式="收盘价")
|
||||
|
||||
# RSI / KDJ / BOLL 同理
|
||||
rsi = 相对强弱指数.首次计算(收盘价, 时间戳, 周期=14)
|
||||
rsi = 相对强弱指数.增量计算(前RSI, 新收盘价, 新时间戳)
|
||||
|
||||
kdj = 随机指标.首次计算(最高价, 最低价, 收盘价, 时间戳, N=9, M1=3, M2=3)
|
||||
kdj = 随机指标.增量计算(前KDJ, 最高价, 最低价, 收盘价, 时间戳)
|
||||
|
||||
boll = 布林带.首次计算(k线, 计算方式="收盘价", 周期=20, 标准差倍数=2.0)
|
||||
boll = 布林带.增量计算(前BOLL, k线, 计算方式="收盘价")
|
||||
```
|
||||
|
||||
配置变更后需回填指标:
|
||||
```python
|
||||
指标计算器._回填新指标(全序列, config) # 重算所有K线的指标
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Q13: 如何验证双端(Rust/Python)分析结果一致?
|
||||
|
||||
```python
|
||||
from chanlun import chan as chan_rs # Rust 绑定
|
||||
from chanlun import chan as chan_py # Python 参考
|
||||
|
||||
# 逐类型相等性检查(返回 (bool, 原因))
|
||||
chan.K线相等(k_rs, k_py, 浮点容差=1e-9)
|
||||
chan.缠论K线相等(ck_rs, ck_py, 浮点容差=1e-9)
|
||||
chan.分型相等(fx_rs, fx_py, 浮点容差=1e-9)
|
||||
chan.虚线相等(dl_rs, dl_py, 浮点容差=1e-9)
|
||||
chan.中枢相等(hub_rs, hub_py, 浮点容差=1e-9)
|
||||
chan.观察者相等(obs_rs, obs_py, 浮点容差=1e-9)
|
||||
chan.立体分析器相等(ma_rs, ma_py, 浮点容差=1e-9)
|
||||
```
|
||||
|
||||
所有相等函数返回 `(True, "")` 或 `(False, "差异描述")`。
|
||||
|
||||
---
|
||||
|
||||
## Q14: K 线如何创建和序列化?
|
||||
|
||||
```python
|
||||
# 创建
|
||||
k = chan.K线.创建普K(
|
||||
标识="btcusd", 时间戳=datetime.now(),
|
||||
开盘价=50000, 最高价=50100, 最低价=49900, 收盘价=50050,
|
||||
成交量=100, 序号=0, 周期=300,
|
||||
)
|
||||
|
||||
# 属性
|
||||
k.方向 # 相对方向(开盘价 vs 收盘价)
|
||||
k.指标 # 指标容器
|
||||
|
||||
# 二进制序列化 (48字节大端,兼容 Rust)
|
||||
data = bytes(k) # __bytes__ → struct.pack(">6d")
|
||||
|
||||
# 从二进制解析
|
||||
k2 = chan.K线.读取大端字节数组(data, 周期=300, 标识="btcusd")
|
||||
|
||||
# 批量保存/加载
|
||||
chan.K线.保存到DAT文件("output.dat", k线列表)
|
||||
|
||||
# 区间截取
|
||||
subset = chan.K线.截取(序列, 始=k起始, 终=k结束)
|
||||
|
||||
# 区间MACD面积
|
||||
chan.K线.获取MACD(序列, 始, 终) # {"MACD正面积": ..., "MACD负面积": ...}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Q15: 缠论K线包含处理逻辑是什么?
|
||||
|
||||
```python
|
||||
# 缠论K线.分析 的返回值
|
||||
状态, 分型 = chan.缠论K线.分析(当前K线, 缠K序列, 普K序列, config)
|
||||
|
||||
# 状态值含义
|
||||
"缠K完成" # 形成独立的缠论K线,可能伴随顶/底分型
|
||||
"包含处理中" # 当前K线被包含处理,未形成独立缠K
|
||||
"等" # 等待更多K线(如方向未确定)
|
||||
```
|
||||
|
||||
缠K 方向由包含处理决定,有向上和向下两种状态。包含处理规则:
|
||||
- 同向:取 高者之高、低者之低(向上)/ 高者之低、低者之高(向下)
|
||||
- 异向:先确定方向,再按方向处理
|
||||
|
||||
---
|
||||
|
||||
## Q16: 分型如何识别?
|
||||
|
||||
```python
|
||||
# 分型由左中右三根缠K构成
|
||||
fx = chan.分型(左=缠K_左, 中=缠K_中, 右=缠K_右)
|
||||
fx.结构 # 分型结构.顶分型 / 分型结构.底分型 / 分型结构.三连向上 / 分型结构.三连向下
|
||||
|
||||
# 分型结构分析
|
||||
chan.分型结构.分析(左, 中, 右, 可以逆序包含=False, 忽视顺序包含=False)
|
||||
# 返回: 三连向上 / 三连向下 / 顶分型 / 底分型 / 向右扩散 / None
|
||||
|
||||
# 分型辅助方法
|
||||
chan.分型.判断分型(左分型, 右分型, 模式="中")
|
||||
chan.分型.从缠K序列中获取分型(缠K序列, 中间缠K)
|
||||
chan.分型.向序列中添加(分型序列, 新分型) # 自动处理冲突
|
||||
|
||||
# 分型属性
|
||||
fx.分型特征值 # 中.高 + 中.低 * sign(顶=-1, 底=+1)
|
||||
fx.关系组 # (左→中, 中→右, 左→右) 相对方向元组
|
||||
fx.强度 # 高低差比例
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Q17: 如何获取虚线范围内的所有停顿位置?
|
||||
|
||||
```python
|
||||
# 获取线段中所有笔的端点停顿位置
|
||||
停顿 = chan.线段.获取所有停顿位置(段, 观察员)
|
||||
# 返回笔级虚线列表,可用于判断中枢区间
|
||||
|
||||
# 四象归类
|
||||
象 = chan.线段.四象(段)
|
||||
# "老阳" / "小阳" / "少阴" / "老阴"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Q18: 数据如何保存和恢复?
|
||||
|
||||
```python
|
||||
# 观察者保存到 .nb 文件(48字节/根格式)
|
||||
path = obs.测试_保存数据(root="/tmp") # 返回文件路径
|
||||
|
||||
# 从 .nb 文件恢复
|
||||
obs.加载本地数据(path)
|
||||
|
||||
# 立体分析器保存
|
||||
ma.测试_保存数据(root="/tmp")
|
||||
|
||||
# 静态重新分析(从已有K线全量重建所有序列)
|
||||
obs.静态重新分析()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Q19: 相对方向的判断逻辑?
|
||||
|
||||
```python
|
||||
chan.相对方向.分析(前高, 前低, 后高, 后低)
|
||||
# 返回: 向上 / 向下 / 包含 / 向上缺口 / 向下缺口 / 衔接
|
||||
|
||||
# 方向翻转
|
||||
方向.翻转() # 向上 ←→ 向下
|
||||
|
||||
# 判断方法
|
||||
方向.是否向上() / 方向.是否向下()
|
||||
方向.是否包含() / 方向.是否缺口() / 方向.是否衔接()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Q20: 常见错误排查
|
||||
|
||||
| 现象 | 可能原因 |
|
||||
|------|----------|
|
||||
| 笔序列为空 | `配置.分析笔=False` 或笔内缠K不足(需 ≥ 笔内元素数量) |
|
||||
| 线段序列为空 | `配置.分析线段=False` 或笔序列长度不足 |
|
||||
| 中枢序列为空 | 虚线序列长度不足 3 段,或三段不重叠 |
|
||||
| 指标为 None | 指标未挂载,需先调用 `指标计算器.计算并挂载` |
|
||||
| 配置变更不生效 | 需要 `指标计算器._回填新指标(全序列, 配置)` 和 `obs.静态重新分析()` |
|
||||
| 买卖点序列为空 | `识别买卖点()` 是空实现,需自行调用 `买卖点.生成买卖点` |
|
||||
| 多周期结果不一致 | 检查 `配置组` 是否覆盖了对应周期的配置 |
|
||||
| 浮点比较不通过 | 双端一致性测试使用 `浮点容差=1e-9` |
|
||||
|
||||
---
|
||||
|
||||
## Q21: 三个线段序列组有什么不同?分别用于什么场景?
|
||||
|
||||
`观察者` 内部有三条并行的多级序列树,每条树有 3 层(由 `线段分析层次=3` 控制):
|
||||
|
||||
```python
|
||||
# 1. 线段序列组 — 标准线段递归
|
||||
obs.线段序列组[0] # 标识="线段" 笔→线段(标准)
|
||||
obs.线段序列组[1] # 标识="线段<线段>" 线段→线段(段作为笔再划段)
|
||||
obs.线段序列组[2] # 标识="线段<线段<线段>" 再递归一层
|
||||
|
||||
# 2. 扩展线段序列组 — 扩展分析递归
|
||||
obs.扩展线段序列组[0] # 标识="扩展线段" 笔→扩展线段
|
||||
obs.扩展线段序列组[1] # 标识="扩展线段<扩展线段>" 扩展段→段
|
||||
obs.扩展线段序列组[2] # 标识="扩展线段<扩展线段<扩展线段>>" 再递归
|
||||
|
||||
# 3. 混合扩展线段序列组 — 混合递归(扩展线段分析标准线段)
|
||||
obs.混合扩展线段序列组[0] # 标识="扩展线段<线段>" 线段→扩展线段
|
||||
obs.混合扩展线段序列组[1] # 标识="扩展线段<线段<线段>>" 线段<线段>→扩展线段
|
||||
obs.混合扩展线段序列组[2] # 标识="扩展线段<线段<线段<线段>>>" 再递归
|
||||
```
|
||||
|
||||
**生成流程**(`静态重新分析` 中可见):
|
||||
1. 笔序列 → `线段.分析()` → 线段序列组[0]
|
||||
2. 线段序列组[0] → `线段.分析()` → 线段序列组[1](线段作为笔再划分)
|
||||
3. 线段序列组[1] → `线段.分析()` → 线段序列组[2]
|
||||
4. 笔序列 → `线段.扩展分析()` → 扩展线段序列组[0]
|
||||
5. 扩展线段序列组[0] → `线段.扩展分析()` → 扩展线段序列组[1]
|
||||
6. 扩展线段序列组[1] → `线段.扩展分析()` → 扩展线段序列组[2]
|
||||
7. 线段序列组[i] → `线段.扩展分析()` → 混合扩展线段序列组[i]
|
||||
|
||||
**实际验证**(2500 根 BTCUSD 300s K 线):
|
||||
```
|
||||
线段序列组: 26段, 3段<线段>, 无更高级
|
||||
扩展线段序列组: 48段, 14段<扩展>, 4段<扩展<扩展>>
|
||||
混合扩展线段组: 8段<线段>, 1段<线段<线段>>
|
||||
```
|
||||
|
||||
**关键差异**:
|
||||
- `线段.分析` 要求笔/线段之间**方向交替**(上下上下)
|
||||
- `线段.扩展分析` 允许**同向连续**(将同向虚线合并处理),因此产生更多扩展段
|
||||
|
||||
等效属性访问(@property):
|
||||
```python
|
||||
obs.线段序列 = obs.线段序列组[0]
|
||||
obs.线段_线段序列 = obs.线段序列组[1]
|
||||
obs.扩展线段序列 = obs.扩展线段序列组[0]
|
||||
obs.扩展线段序列_扩展线段 = obs.扩展线段序列组[1]
|
||||
obs.混合扩展线段序列 = obs.混合扩展线段序列组[0]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Q22: 特征序列是什么?老阴/老阳/小阳/少阴如何区分?
|
||||
|
||||
**特征序列**是线段划分算法的核心概念。当从笔划分线段时,每根笔的方向与线段方向之间有一个关系:
|
||||
- **线段方向向上** → 特征序列取每笔的"向下"特征(特征方向=向下)
|
||||
- **线段方向向下** → 特征序列取每笔的"向上"特征(特征方向=向上)
|
||||
|
||||
特征序列用于处理**缺口**和**包含**:多笔同特征方向的要素需要先做包含处理,再通过特征序列的分型来判断线段是否终结。
|
||||
|
||||
**线段特征**(`线段特征` 类)是对应笔的抽象:
|
||||
```python
|
||||
线段特征(标识, 基础序列, 线段方向)
|
||||
# 持有一组同向虚线(笔),是线段划分的中间结构
|
||||
# 特征.方向 = 线段方向的翻转
|
||||
# 特征.高/特征.低 → 按线段方向取极值
|
||||
```
|
||||
|
||||
**四象**描述线段与缺口的关系:
|
||||
```python
|
||||
线段.四象(段)
|
||||
# "老阳": 向下线段 + 存在前一缺口(向下线段后有向上缺口)
|
||||
# "老阴": 向上线段 + 存在前一缺口(向上线段后有向下缺口)
|
||||
# "小阳": 向上线段 + 无缺口(普通向上)
|
||||
# "少阴": 向下线段 + 无缺口(普通向下)
|
||||
```
|
||||
|
||||
**老阴老阳在特征序列中的作用**:
|
||||
当 `线段_特征序列忽视老阴老阳=False`(默认)时:
|
||||
- 老阳/老阴的特征序列**不参与**包含处理——缺口状态下的特征序列元素直接跳过包含合并
|
||||
- 相当于"缺口破坏了特征序列的连续性"
|
||||
|
||||
当 `线段_特征序列忽视老阴老阳=True` 时:
|
||||
- **忽略缺口**,所有特征序列元素都按无缺口处理(严格包含)
|
||||
- 实际效果:线段和中枢数量都**增加**(更多特征序列参与包含处理,产生更多分型终结)
|
||||
|
||||
**验证数据**(1500 根 K 线):
|
||||
```
|
||||
忽视老阴老阳=False: 线段数=374, 中枢数=48
|
||||
忽视老阴老阳=True: 线段数=390, 中枢数=56 (+16段, +8中枢)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Q23: 笔弱化、笔次级成笔到底改变了什么?
|
||||
|
||||
### 笔弱化 (`笔弱化=True`)
|
||||
|
||||
当一笔只有 3 个内部缠K(`武.中.序号 - 文.中.序号 + 1 == 3`)且下一分型直接穿透它时:
|
||||
- 向上笔 + 下一底分型低于该笔的低点 → 弹出这笔 → 重分析
|
||||
- 向下笔 + 下一顶分型高于该笔的高点 → 弹出这笔 → 重分析
|
||||
|
||||
效果是**移除过短的阻挡笔**,让分型能连接更合理的笔。实测效果:
|
||||
|
||||
```
|
||||
1500根K线:
|
||||
笔弱化=False: 笔数=2796, 线段数=374
|
||||
笔弱化=True: 笔数=3630, 线段数=500
|
||||
|
||||
5000根K线:
|
||||
笔弱化=False: 笔数=9358, 线段数=1261
|
||||
笔弱化=True: 笔数=11878, 线段数=1678
|
||||
```
|
||||
|
||||
笔弱化开启后笔数和线段数都**显著增加**(~30%),因为移除阻挡笔后产生了更多有效笔和段。
|
||||
|
||||
### 笔弱化_原始数量 (`笔弱化_原始数量=3`)
|
||||
|
||||
控制笔弱化判断时参考的原始K线数量。不同的值对结果影响:
|
||||
|
||||
```
|
||||
笔弱化_原始数量=3: 笔数=3556
|
||||
笔弱化_原始数量=5: 笔数=3556 (无变化)
|
||||
笔弱化_原始数量=7: 笔数=3556 (无变化)
|
||||
```
|
||||
|
||||
当前实现中此参数变化不明显,因为笔弱化主要受"原始分型数量为3"这个硬编码条件驱动。
|
||||
|
||||
### 笔次级成笔 (`笔次级成笔=False`)
|
||||
|
||||
控制是否在笔的分析中启用次级递归成笔。默认关闭。实测效果:
|
||||
|
||||
```
|
||||
1500根K线:
|
||||
笔次级成笔=False: 笔数=2796, 线段数=374
|
||||
笔次级成笔=True: 笔数=3228, 线段数=468 (+432笔, +94段)
|
||||
```
|
||||
|
||||
开启后笔数和线段数都有明显增加,因为允许在笔内部递归划分子笔,产生更多有效分型。
|
||||
|
||||
---
|
||||
|
||||
## Q24: 线段修正算法 _修正 / _缺口突破 / _非缺口下穿刺 做什么?
|
||||
|
||||
### _修正 (短路修正, 配置.线段_修正)
|
||||
|
||||
当线段基础序列 ≥ 9 且后半段满足特定条件时,将一段拆分为两段:
|
||||
1. 分割序列 → 前段+后段
|
||||
2. 后段 ≥ 6 个元素且偶数
|
||||
3. 后段倒数第3和第1元素方向 = 线段方向
|
||||
4. 满足条件 → 拆分为 新段1(后段[:-3]) + 新段2(后段[-3:])
|
||||
5. 标记 `短路修正=True`,清空老阴老阳段的缺口
|
||||
|
||||
### _缺口突破 (缺省启用,无配置开关)
|
||||
|
||||
处理有缺口的线段序列:
|
||||
- 当前线段特征序列右元素 ≠ None
|
||||
- 特征序列方向与线段方向匹配
|
||||
- 连续特征序列长度 ≥ 5 且奇数
|
||||
- 弹出当前段 → 根据特征分型终结点切分为两段
|
||||
|
||||
### _非缺口下穿刺 (配置.线段_非缺口下穿刺)
|
||||
|
||||
处理"贯穿伤":当一段线段的基础序列被另一段完全穿透时修复。只有在小阳/少阴(非缺口)状态下才触发。
|
||||
|
||||
---
|
||||
|
||||
## Q25: 买卖点的实际集成路径是什么?为什么识别买卖点()是空的?
|
||||
|
||||
`观察者.识别买卖点()` 是 `pass` 占位——具体买卖点识别逻辑**原本计划实现但未在 chan.py 中完成**。
|
||||
|
||||
但这不意味着买卖点不可用——可以通过以下路径手动生成:
|
||||
|
||||
```python
|
||||
# 路径1:手动创建(已有工厂方法)
|
||||
bsp = chan.买卖点.一卖点(
|
||||
买卖点分型=obs.分型序列[-1], # 或中枢离开段终点的分型
|
||||
当前K线=obs.普通K线序列[-1],
|
||||
标识="btcusd-300",
|
||||
备注="第一类卖点",
|
||||
中枢破位值=hub.低, # 中枢下沿
|
||||
)
|
||||
|
||||
# 路径2:通过生成买卖点路由
|
||||
chan.买卖点.生成买卖点(
|
||||
特征="一卖", # 特征字符串路由到工厂
|
||||
序号="1",
|
||||
级别="线段",
|
||||
买卖点分型=fx,
|
||||
当前缠K=ck,
|
||||
)
|
||||
|
||||
# 路径3:中枢完整性验证 → 判断第三买卖点
|
||||
hub.完整性(虚实="实") # 检查是否有第三买卖点条件
|
||||
```
|
||||
|
||||
买卖点类型的完整列表(18种):
|
||||
```
|
||||
一买/一卖 第一类(中枢背驰转折)
|
||||
二买/二卖 第二类(回抽不进中枢)
|
||||
三买/三卖 第三类(离开后回抽不破中枢)
|
||||
T1买/T1卖 第一类扩展
|
||||
T1P买/T1P卖 盘整型第一类
|
||||
T2买/T2卖 第二类扩展
|
||||
T2S买/T2S卖 强势第二类
|
||||
T3A买/T3A卖 第三类A型
|
||||
T3B买/T3B卖 第三类B型
|
||||
```
|
||||
|
||||
Rust 绑定层(`chanlun-py`)中的 `识别买卖点` 已实现完整逻辑,使用 `生成买卖点` 路由。
|
||||
|
||||
---
|
||||
|
||||
## 内部算法机制
|
||||
|
||||
以下方法不是公开 API,但理解它们的逻辑才能理解线段为什么这样划分、中枢为什么这样识别。
|
||||
|
||||
### 线段.分割序列 — 用终点切开基础序列
|
||||
|
||||
```
|
||||
输入: 段 (虚线), 所属中枢 (可选)
|
||||
输出: (前序列, 后序列, 第三买卖线列表, 贯穿伤)
|
||||
```
|
||||
|
||||
**切割规则**:遍历段.基础序列,找到第一笔满足 `笔.文 is 段.武` 的位置——从此笔开始全部归入"后",之前的归入"前"。
|
||||
|
||||
**为什么**:线段的终点分型(武)同时是下一笔的起点分型(文)。段.武 为起点的笔暂挂在当前段的基础序列里,但它已属于"下一段"范畴。分割序列区分"已确认"和"暂挂"。
|
||||
|
||||
**9 处调用各自取什么**:
|
||||
|
||||
| 调用方 | 取值 | 用途 |
|
||||
|--------|------|------|
|
||||
| `获取内部中枢序列` | 前, 后 | 分别在前后找中枢 |
|
||||
| `_缺口突破` | `[0]` 前 | 只在确认部分判断缺口 |
|
||||
| `_缺口后紧急修正` | `[1]` 后 | 检查暂挂部分是否需要修正 |
|
||||
| `_修正` | 前, 后 | 后≥6且方向一致 → 拆段 |
|
||||
| `分析`(递归)| `[1]` 后 | 用暂挂部分做下一轮划分 |
|
||||
| `判断线段内部是否背驰` | 前=阳, 后=阴 | 进入段 vs 离开段 |
|
||||
| `获取所有停顿位置` | 前, 后 | 前后端点都算停顿 |
|
||||
| `获取数据文本` | 全部 | 格式化输出 |
|
||||
|
||||
**贯穿伤**:后[0].武 穿透了 段.文——向上段的"后"终点比"文"还低、或向下段的"后"终点比"文"还高。意味着这段根本没真正转折。
|
||||
|
||||
### 线段.获取内部中枢序列 — 阴阳合三套中枢
|
||||
|
||||
```
|
||||
输入: 段, 配置
|
||||
输出: (虚中枢列表, 实中枢列表, 合中枢列表) # 注释: 阴 阳 合
|
||||
```
|
||||
|
||||
先用 `分割序列` 把线段基础序列切成"前"(确认部分)和"后"(暂挂部分),然后分别在三块区域跑中枢识别:
|
||||
|
||||
| 返回值 | 别称 | 识别范围 | 含义 |
|
||||
|--------|------|----------|------|
|
||||
| `实中枢序列` | 阳 | 分割序列的"前" | 段已确认部分的中枢——段主体运行阶段形成的 |
|
||||
| `虚中枢序列` | 阴 | 分割序列的"后" | 段暂挂部分的中枢——离开段所在的 |
|
||||
| `合中枢序列` | 合 | 整个基础序列(前+后) | 全景视角——但跨了段边界,含下一段笔 |
|
||||
|
||||
**核心用途**:给 `判断线段内部是否背驰` 提供判定依据。
|
||||
|
||||
背驰判断的三种情况(对照 `判断线段内部是否背驰` 源码):
|
||||
|
||||
1. **有阴**(暂挂部分非空)→ 段还未确认终点,不判断背驰(直接返回 False)
|
||||
2. **有实中枢** → 检查最后一笔是否在末个阳中枢里:
|
||||
- 在:取中枢起点前一笔作进入段,最后一笔作离开段 → 按配置模式判断背驰
|
||||
- 不在但末中枢有第三买卖线:取倒数第三笔和倒数第一笔比较 → 判断盘整背驰
|
||||
3. **无中枢** → 只有 3 笔时判断盘整背驰
|
||||
|
||||
**阴阳为什么分开**:同一个段内,前半(阳)是已完成的走势,后半(阴)是终点出现后的暂挂部分。背驰判断时,取阳中枢的起点前一笔作"进入段"、阳的最后一笔作"离开段",比较两者是否发生趋势衰减——这是段内部是否背驰的判定基础。
|
||||
|
||||
实测验证(2000 根 K 线):
|
||||
```
|
||||
段6 [向上] 10笔: 实中枢1个[114596-114189] 虚中枢1个[115382-114523] 合中枢1个[114596-114189]
|
||||
段7 [向下] 10笔: 实中枢1个[115382-114523] 虚中枢0个 合中枢2个[115382-114523][113297-112218]
|
||||
段8 [向上] 8笔: 实中枢0个 虚中枢1个[111409-111008] 合中枢2个[113641-112754][111409-111008]
|
||||
```
|
||||
|
||||
注意段7:虚中枢为空但合中枢有2个——因为跨"前/后"边界的笔凑出了合中枢里的第二个。但后部的笔属于下一段,这个中枢不应该算在此段头上。**判断单段是否趋势,应该用实中枢,不能用合中枢**:实中枢只看"前"(本段自己的笔),不会混入下一段的结构。
|
||||
|
||||
实测对比(2000根K线):
|
||||
```
|
||||
段7: 实中枢=1(盘整) vs 合中枢=2(趋势) ← 合中枢误判
|
||||
段8: 实中枢=0(盘整) vs 合中枢=2(趋势) ← 合中枢误判
|
||||
```
|
||||
两例都被合中枢误标为趋势,实际是盘整。
|
||||
|
||||
---
|
||||
|
||||
## Q26: 如何判断线段内部是趋势还是盘整?
|
||||
|
||||
```python
|
||||
虚, 实, 合 = chan.线段.获取内部中枢序列(段, 观察员.配置)
|
||||
|
||||
if len(实) >= 2:
|
||||
if 段.方向.是否向上():
|
||||
return "上涨趋势"
|
||||
else:
|
||||
return "下跌趋势"
|
||||
else:
|
||||
return "盘整"
|
||||
```
|
||||
|
||||
趋势必须有方向——段方向是向上 + 实中枢≥2 = 上涨趋势,段方向向下 + 实中枢≥2 = 下跌趋势。
|
||||
|
||||
**为什么是实中枢,不是合中枢?**
|
||||
|
||||
`分割序列` 把基础序列切成"前"(本段)和"后"(下一段起始)。合中枢在"前+后"上跑,会混入下一段的笔。段7 前7笔只有1个实中枢,但跨边界的笔在合中枢凑出第2个——这个多出来的中枢不属于此段。
|
||||
|
||||
**辅助信号**:
|
||||
|
||||
| 信号 | 上涨趋势 | 下跌趋势 | 盘整 |
|
||||
|------|---------|---------|------|
|
||||
| 段.方向 | 向上 | 向下 | 皆可 |
|
||||
| 实中枢数 | ≥2 | ≥2 | ≤1 |
|
||||
| `买卖意义` | 可能 False | 可能 False | 可能 True(盘整背驰) |
|
||||
| `判断线段内部是否背驰` | 可能 True | 可能 True | True=盘整衰竭 |
|
||||
|
||||
### 线段._刷新特征序列 — 线段级别包含处理
|
||||
|
||||
```
|
||||
输入: 段, 配置
|
||||
```
|
||||
|
||||
1. 取段.基础序列,若存在 `前一结束位置` 则从此位置-1处截取
|
||||
2. 调用 `线段特征.静态分析(基础序列, 段.方向, 四象, 忽视老阴老阳)` — 将笔序列做包含处理得到特征序列
|
||||
3. 从特征序列提取分型序列
|
||||
4. 若首分型方向与段.方向相同(特征分型终结条件)→ 截取基础序列到特征分型终点 + 调用 `_设置特征序列`
|
||||
|
||||
**特征序列方向 = 线段方向的翻转**。向上线段取向下的特征(调整段),向下线段取向上的特征(反弹段)。
|
||||
|
||||
### 线段特征.静态分析 — 笔的包含处理
|
||||
|
||||
将多笔同向特征元素做包含处理(与缠K的包含处理原理相同,但在笔级别操作):
|
||||
- 同向特征:取极值合并
|
||||
- 反向特征:独立成新元素
|
||||
- 老阴/老阳(有缺口)且不忽视时:跳过包含处理
|
||||
|
||||
输出一段 `List[线段特征]`,每个特征持有一组被合并的笔。
|
||||
|
||||
### 线段._武斗 — 线段终结判断
|
||||
|
||||
```
|
||||
输入: 段, 特征, 行号
|
||||
```
|
||||
|
||||
当特征序列右元素 ≠ None 时,判断线段是否被终结:
|
||||
1. 特征序列方向应与线段方向相同(特征分型终结)
|
||||
2. 检查特征分型是否有效(顶底交替,特征序列包含处理完毕)
|
||||
3. 满足条件 → 用 `_武终` 终结线段,重置基础序列,开启新段
|
||||
|
||||
### 线段._添加线段 — 含缺口处理
|
||||
|
||||
```
|
||||
输入: 线段序列, 待添加线段, 配置, 行号
|
||||
```
|
||||
|
||||
在追加线段前,检查前一段的缺口状态:
|
||||
- 若前一段 `短路修正=True` → 缺口 = None
|
||||
- 否则 → 调用 `获取缺口(前一段)`
|
||||
- 将缺口存入新段的 `前一缺口`
|
||||
|
||||
缺口影响四象判断和后续的特征序列包含处理。
|
||||
|
||||
### 笔._相对关系 — 判断笔是否反向
|
||||
|
||||
```
|
||||
输入: 筆, 配置
|
||||
```
|
||||
|
||||
验证笔的方向是否"真正"匹配分型方向。如果 `笔内起始分型包含整笔=True`,会用分型的左中右三根缠K构造缺口区间,判断区间方向与笔方向是否一致。防止"分型形态在但实际价格不匹配"的假笔。
|
||||
|
||||
### 笔弱化触发条件
|
||||
|
||||
```
|
||||
if 配置.笔弱化 and 笔序列:
|
||||
前一笔.武.中.序号 - 前一笔.文.中.序号 + 1 == 3: # 只有3根内部缠K
|
||||
if (向上笔 and 前一笔.低 > 当前分型.分型特征值 and 当前分型是底) or
|
||||
(向下笔 and 前一笔.高 < 当前分型.分型特征值 and 当前分型是顶):
|
||||
弹出旧笔 → 递归重分析
|
||||
```
|
||||
|
||||
只有**最短的笔(3缠K)**且被下一分型**完全穿透**时才会触发弱化。这就是为什么笔弱化能增加 30% 的笔——它移除了那些"看起来像笔但实际上阻挡了更合理划分"的过短笔。
|
||||
|
||||
### 中枢._校验合法性 — 确保中枢不包含已删除的元素
|
||||
|
||||
遍历中枢.基础序列,检查每个元素是否仍存在于源序列中。如果某段已被线段修正删除,中枢需要修剪。这保证了中枢始终反映当前有效的线段结构。
|
||||
|
||||
### 中枢.当前状态 — 判断价格相对中枢的位置
|
||||
|
||||
```python
|
||||
if 中枢.高 >= 尾部.分型特征值 >= 中枢.低 → "中枢之中"
|
||||
elif 中枢.高 < 尾部.分型特征值 → "中枢之上"
|
||||
elif 中枢.低 > 尾部.分型特征值 → "中枢之下"
|
||||
```
|
||||
|
||||
第三买卖点判断依赖此状态——只有离开中枢(之上/之下)后才可能产生第三类买卖点。
|
||||
|
||||
---
|
||||
|
||||
## 类型速查
|
||||
|
||||
| 类型 | 职责 | 关键方法 |
|
||||
|------|------|----------|
|
||||
| `K线` | OHLCV 数据 + 指标容器 | `创建普K`, `读取大端字节数组`, `截取` |
|
||||
| `缠论K线` | 包含处理后的K线 | `分析`, `创建缠K`, `与MACD柱子匹配` |
|
||||
| `分型` | 顶/底分型 | `从缠K序列中获取分型`, `向序列中添加` |
|
||||
| `分型结构` | 分型形态枚举 | `分析` |
|
||||
| `虚线` | 笔/线段数据结构 | `创建笔`, `创建线段`, `买卖意义`, `武之MACD极值` |
|
||||
| `笔` | 笔算法 (@staticmethod) | `分析` |
|
||||
| `线段` | 线段算法 (@staticmethod) | `分析`, `扩展分析`, `判断线段内部是否背驰` |
|
||||
| `中枢` | 三段重叠区间 | `创建`, `分析`, `获取扩展中枢`, `当前状态` |
|
||||
| `买卖点` | 买卖点工厂 | `一买点`~`T3B卖点` 18 个工厂方法 |
|
||||
| `背驰分析` | 背驰判断 (@staticmethod) | `MACD背驰`, `斜率背驰`, `测度背驰`, `全量背驰` |
|
||||
| `观察者` | 单周期分析器 | `投喂原始数据`, `读取数据文件`, `静态重新分析` |
|
||||
| `立体分析器` | 多周期分析器 | `投喂K线`, `_单体分析器` |
|
||||
| `K线合成器` | 周期合成 | `投喂`, `投喂K线`, `获取当前K线` |
|
||||
| `缠论配置` | 全局参数 | `to_dict`, `from_dict`, `model_copy`, `对比`, `不推送` |
|
||||
| `指标计算器` | 指标挂载 | `计算并挂载`, `_回填新指标` |
|
||||
| `均线工具` | SMA/EMA 辅助 | `增量SMA`, `增量EMA` |
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "chanlun-py"
|
||||
version = "26.6.125"
|
||||
version = "26.6.73"
|
||||
edition = "2024"
|
||||
description = "缠论技术分析库 — Rust 高性能 Python 绑定"
|
||||
authors = ["YuYuKunKun"]
|
||||
@@ -12,7 +12,7 @@ crate-type = ["cdylib"]
|
||||
name = "chanlun"
|
||||
|
||||
[dependencies]
|
||||
chanlun = { path = "../chanlun" }
|
||||
chanlun = "26.6.4" # { path = "../chanlun" }
|
||||
parking_lot = "0.12"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "ansi", "std", "registry"] }
|
||||
tracing-core = "0.1"
|
||||
|
||||
@@ -40,10 +40,6 @@ analyzer = chanlun.立体分析器("BTCUSD", [60, 60*5, 60*5*6], config)
|
||||
```bash
|
||||
pip install maturin
|
||||
|
||||
# 推荐:一键清理缓存 + 构建 + 安装
|
||||
./clean_install.sh
|
||||
|
||||
# 或手动:
|
||||
# 开发模式(直接安装到当前 venv)
|
||||
maturin develop
|
||||
|
||||
@@ -52,12 +48,6 @@ maturin build --release
|
||||
pip install target/wheels/chanlun-*.whl
|
||||
```
|
||||
|
||||
> **注意**:若修改了 `chan.py`,安装前需清除 `__pycache__`,否则旧 `.pyc` 会被打包进 wheel 导致修改不生效:
|
||||
> ```bash
|
||||
> find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null
|
||||
> find . -type f -name "*.pyc" -delete 2>/dev/null
|
||||
> ```
|
||||
|
||||
也可使用项目内的 `build.sh`:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -886,7 +886,6 @@ class 缠论配置:
|
||||
def from_json(cls, json_str: str) -> 缠论配置: ...
|
||||
@classmethod
|
||||
def 不推送(cls) -> 缠论配置: ...
|
||||
def 展示标签(self, 标签: str) -> bool: ...
|
||||
@classmethod
|
||||
def 按序号重组字典(cls, 默认配置: Any, 原始字典: Dict[str, Any]) -> Dict[str, Any]: ...
|
||||
def __str__(self) -> str: ...
|
||||
|
||||
@@ -45,60 +45,12 @@ __all__ = [
|
||||
"线段特征相等",
|
||||
"中枢相等",
|
||||
"虚线相等",
|
||||
"chan",
|
||||
]
|
||||
from ._chanlun import *
|
||||
from ._chanlun import set_log_level as _rs_set_log_level, get_log_level as _rs_get_log_level
|
||||
|
||||
import sys as _sys
|
||||
from loguru import logger as _logger
|
||||
|
||||
# ---- Python 侧日志(loguru)----
|
||||
|
||||
_级别映射 = {
|
||||
"trace": "TRACE",
|
||||
"debug": "DEBUG",
|
||||
"info": "INFO",
|
||||
"warn": "WARNING",
|
||||
"error": "ERROR",
|
||||
"off": "OFF",
|
||||
}
|
||||
_有效级别 = frozenset(_级别映射.keys())
|
||||
_当前日志级别 = "info"
|
||||
|
||||
|
||||
def set_log_level(level: str):
|
||||
"""设置 Python 侧日志级别 (loguru)。
|
||||
|
||||
:param level: 日志级别,不区分大小写 (trace / debug / info / warn / error / off)
|
||||
"""
|
||||
global _当前日志级别
|
||||
_level = level.lower()
|
||||
if _level not in _有效级别:
|
||||
raise ValueError(f"无效日志级别 '{level}',有效值: {', '.join(sorted(_有效级别))}")
|
||||
|
||||
_当前日志级别 = _level
|
||||
_loguru_level = _级别映射[_level]
|
||||
|
||||
try:
|
||||
_logger.remove(0)
|
||||
except ValueError:
|
||||
pass
|
||||
if _loguru_level != "OFF":
|
||||
_logger.add(_sys.stderr, level=_loguru_level)
|
||||
|
||||
|
||||
def get_log_level() -> str:
|
||||
"""获取 Python 侧当前日志级别。
|
||||
|
||||
:return: 日志级别字符串 (trace / debug / info / warn / error / off)
|
||||
"""
|
||||
return _当前日志级别
|
||||
|
||||
|
||||
set_log_level("error")
|
||||
|
||||
|
||||
# ---- Rust 侧日志(tracing)----
|
||||
from . import chan
|
||||
from .chan import set_log_level, get_log_level
|
||||
|
||||
|
||||
def set_rs_log_level(level: str):
|
||||
|
||||
@@ -869,146 +869,6 @@ class 立体分析器:
|
||||
def 投喂K线(self, 普K: K线) -> None: ...
|
||||
def 测试_保存数据(self, root: Optional[str] = None) -> None: ...
|
||||
|
||||
# ========== Signal framework (信号匹配框架) ==========
|
||||
|
||||
class Operate:
|
||||
HL: ClassVar[Operate]
|
||||
HS: ClassVar[Operate]
|
||||
HO: ClassVar[Operate]
|
||||
LO: ClassVar[Operate]
|
||||
LE: ClassVar[Operate]
|
||||
SO: ClassVar[Operate]
|
||||
SE: ClassVar[Operate]
|
||||
@property
|
||||
def value(self) -> str: ...
|
||||
@classmethod
|
||||
def from_value(cls, value: str) -> Operate: ...
|
||||
def __str__(self) -> str: ...
|
||||
def __repr__(self) -> str: ...
|
||||
def __eq__(self, other: Any) -> bool: ...
|
||||
def __hash__(self) -> int: ...
|
||||
|
||||
class Signal:
|
||||
def __init__(self, signal: str = "", score: int = 0, k1: str = "任意", k2: str = "任意", k3: str = "任意", v1: str = "任意", v2: str = "任意", v3: str = "任意") -> None: ...
|
||||
@property
|
||||
def signal(self) -> str: ...
|
||||
@property
|
||||
def score(self) -> int: ...
|
||||
@property
|
||||
def k1(self) -> str: ...
|
||||
@property
|
||||
def k2(self) -> str: ...
|
||||
@property
|
||||
def k3(self) -> str: ...
|
||||
@property
|
||||
def v1(self) -> str: ...
|
||||
@property
|
||||
def v2(self) -> str: ...
|
||||
@property
|
||||
def v3(self) -> str: ...
|
||||
@property
|
||||
def key(self) -> str: ...
|
||||
@property
|
||||
def value(self) -> str: ...
|
||||
def is_match(self, s: Dict[str, Any]) -> bool: ...
|
||||
def __repr__(self) -> str: ...
|
||||
|
||||
class Factor:
|
||||
def __init__(self, signals_all: List[Signal], signals_any: List[Signal] = ..., signals_not: List[Signal] = ..., name: str = "") -> None: ...
|
||||
@property
|
||||
def name(self) -> str: ...
|
||||
@property
|
||||
def signals_all(self) -> List[Signal]: ...
|
||||
@property
|
||||
def signals_any(self) -> List[Signal]: ...
|
||||
@property
|
||||
def signals_not(self) -> List[Signal]: ...
|
||||
@property
|
||||
def unique_signals(self) -> List[str]: ...
|
||||
def is_match(self, s: Dict[str, Any]) -> bool: ...
|
||||
def dump(self) -> Dict[str, Any]: ...
|
||||
@staticmethod
|
||||
def load(raw: Dict[str, Any]) -> Factor: ...
|
||||
def __repr__(self) -> str: ...
|
||||
|
||||
class Event:
|
||||
def __init__(self, operate: Operate, factors: List[Factor], signals_all: List[Signal] = ..., signals_any: List[Signal] = ..., signals_not: List[Signal] = ..., name: str = "") -> None: ...
|
||||
@property
|
||||
def operate(self) -> Operate: ...
|
||||
@property
|
||||
def factors(self) -> List[Factor]: ...
|
||||
@property
|
||||
def signals_all(self) -> List[Signal]: ...
|
||||
@property
|
||||
def signals_any(self) -> List[Signal]: ...
|
||||
@property
|
||||
def signals_not(self) -> List[Signal]: ...
|
||||
@property
|
||||
def name(self) -> str: ...
|
||||
@property
|
||||
def unique_signals(self) -> List[str]: ...
|
||||
def is_match(self, s: Dict[str, Any]) -> Tuple[bool, Optional[str]]: ...
|
||||
def dump(self) -> Dict[str, Any]: ...
|
||||
@staticmethod
|
||||
def load(raw: Dict[str, Any]) -> Event: ...
|
||||
def __repr__(self) -> str: ...
|
||||
|
||||
class Position:
|
||||
def __init__(self, symbol: str, opens: List[Event], exits: List[Event] = ..., interval: int = 0, timeout: int = 1000, stop_loss: int = 1000, T0: bool = False, name: str = "") -> None: ...
|
||||
# 配置
|
||||
@property
|
||||
def symbol(self) -> str: ...
|
||||
@property
|
||||
def name(self) -> str: ...
|
||||
@property
|
||||
def opens(self) -> List[Event]: ...
|
||||
@property
|
||||
def exits(self) -> List[Event]: ...
|
||||
@property
|
||||
def events(self) -> List[Event]: ...
|
||||
@property
|
||||
def interval(self) -> int: ...
|
||||
@property
|
||||
def timeout(self) -> int: ...
|
||||
@property
|
||||
def stop_loss(self) -> int: ...
|
||||
@property
|
||||
def T0(self) -> bool: ...
|
||||
@property
|
||||
def unique_signals(self) -> List[str]: ...
|
||||
# 状态 (Rust 核心提供)
|
||||
@property
|
||||
def pos(self) -> int: ...
|
||||
@property
|
||||
def pos_changed(self) -> bool: ...
|
||||
@property
|
||||
def operates(self) -> List[Dict[str, Any]]: ...
|
||||
@property
|
||||
def holds(self) -> List[Dict[str, Any]]: ...
|
||||
@property
|
||||
def pairs(self) -> List[Dict[str, Any]]: ...
|
||||
# 方法
|
||||
def update(self, 信号字典: Dict[str, Any]) -> None: ...
|
||||
def dump(self, with_data: bool = False) -> Dict[str, Any]: ...
|
||||
@staticmethod
|
||||
def load(raw: Dict[str, Any]) -> Position: ...
|
||||
def __repr__(self) -> str: ...
|
||||
|
||||
class 信号引擎:
|
||||
def __init__(self, 信号配置: List[Dict[str, Any]] = ...) -> None: ...
|
||||
def 自动挂载指标(self, analyzer: 立体分析器) -> None: ...
|
||||
def 更新(self, analyzer: 立体分析器) -> Dict[str, str]: ...
|
||||
def 更新_完整(self, analyzer: 立体分析器) -> Dict[str, Any]: ...
|
||||
def __len__(self) -> int: ...
|
||||
def __repr__(self) -> str: ...
|
||||
|
||||
# ========== 信号注册表函数 ==========
|
||||
|
||||
def call_signal(name: str, obs: 观察者, params: Dict[str, Any]) -> List[Signal]: ...
|
||||
def list_signals() -> List[str]: ...
|
||||
def get_signal_template(name: str) -> Optional[str]: ...
|
||||
def unregister_signal(name: str) -> None: ...
|
||||
|
||||
# ========== 缠论配置 (fields via __getattr__/__setattr__) ==========
|
||||
|
||||
class 缠论配置:
|
||||
@@ -1026,7 +886,6 @@ class 缠论配置:
|
||||
def from_json(cls, json_str: str) -> 缠论配置: ...
|
||||
@classmethod
|
||||
def 不推送(cls) -> 缠论配置: ...
|
||||
def 展示标签(self, 标签: str) -> bool: ...
|
||||
@classmethod
|
||||
def 按序号重组字典(cls, 默认配置: Any, 原始字典: Dict[str, Any]) -> Dict[str, Any]: ...
|
||||
def __str__(self) -> str: ...
|
||||
|
||||
+236
-1391
File diff suppressed because it is too large
Load Diff
@@ -1,575 +0,0 @@
|
||||
# ==============================================================================
|
||||
# Copyright (c) YuYuKunKun / chanlun.rs
|
||||
#
|
||||
# 本项目整体基于 MIT 协议开源
|
||||
# 部分代码片段摘录自 Apache License 2.0 授权项目
|
||||
#
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2026 YuYuKunKun
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
#
|
||||
# ==============================================================================
|
||||
# 摘录代码相关声明
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# Source: https://github.com/waditu/czsc/blob/v0.9.69/czsc/objects.py#L450
|
||||
# Modified: 【YuYuKunKun & 2026-05-31】
|
||||
# ==============================================================================
|
||||
import hashlib
|
||||
import re
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
from loguru import logger
|
||||
|
||||
from chanlun import K线, 虚线, 中枢, 观察者, 立体分析器
|
||||
from chanlun.parse import parse
|
||||
|
||||
# 信号匹配原语已移植到 Rust 核心层(chanlun._chanlun)。
|
||||
# Operate/Signal/Factor/Event/Position 改为从 Rust 导入;Position 在下方扩展为子类补 update 状态机。
|
||||
from chanlun.signal_orchestrator import SignalOrchestrator
|
||||
|
||||
from chanlun._chanlun import (
|
||||
Signal,
|
||||
Factor,
|
||||
Event,
|
||||
Operate,
|
||||
Position as _PositionBase,
|
||||
)
|
||||
|
||||
|
||||
def import_by_name(name: str):
|
||||
"""通过字符串导入模块、类、函数
|
||||
|
||||
函数执行逻辑:
|
||||
|
||||
1. 检查 name 中是否包含点号('.')。如果没有,则直接使用内置的 import 函数来导入整个模块,并返回该模块对象。
|
||||
2. 如果 name 包含点号,先处理一个相对路径。将 name 拆分为两部分:module_name 和 function_name。
|
||||
使用 Python 内置的 rsplit 方法从右边开始分割,只取一次,这样可以确保我们将最后的一个点号前的部分作为 module_name,点号后面的部分作为 function_name。
|
||||
3. 使用import函数导入指定的 module_name。
|
||||
这里传入三个参数:globals() 和 locals() 分别代表当前全局和局部命名空间;
|
||||
[function_name] 是一个列表,用于指定要导入的子模块或属性名。
|
||||
这样做是为了避免一次性导入整个模块的所有内容,提高效率。
|
||||
4. 使用 vars 函数获取模块的字典表示形式(即模块内所有的变量和函数),取出 function_name 对应的值,然后返回这个值。
|
||||
|
||||
:param name: 模块名,如:'czsc.objects.Factor'
|
||||
:return: 模块对象
|
||||
"""
|
||||
if "." not in name:
|
||||
return __import__(name)
|
||||
|
||||
# 从右边开始分割,分割成模块名和函数名
|
||||
module_name, function_name = name.rsplit(".", 1)
|
||||
module = __import__(module_name, globals(), locals(), [function_name])
|
||||
return vars(module)[function_name]
|
||||
|
||||
|
||||
class SignalsParser:
|
||||
"""解析一串信号,生成信号函数配置"""
|
||||
|
||||
def __init__(self, signals_module: str = "chanlun.signals"):
|
||||
"""
|
||||
|
||||
函数执行逻辑:
|
||||
|
||||
1. 将传入的 signals_module 参数赋给实例变量 self.signals_module,代表信号函数所在的模块,默认模块是czsc库的signals模块。
|
||||
2. 使用 import_by_name 函数导入了指定名称的模块 signals_module。
|
||||
3. 对于导入的模块中的每个属性名进行遍历:
|
||||
- 魔法函数和私有函数不进行处理。
|
||||
- 获取函数的注解信息,并通过正则表达式获取注解中的参数模板和信号列表。
|
||||
- 如果解析到了参数模板,则将其存储在 sig_pats_map 中,key是函数名称。
|
||||
- 如果解析到了信号列表,则将其存储在 sig_name_map 中,并且为每个信号创建了 Signal 对象并存储在列表中,key是函数名称。
|
||||
4. 最后将得到的 sig_name_map 和 sig_pats_map 存储在实例变量中,以便其他方法使用。
|
||||
|
||||
:param signals_module: 指定信号函数所在模块
|
||||
"""
|
||||
self.signals_module = signals_module
|
||||
sig_name_map = {}
|
||||
sig_pats_map = {}
|
||||
sig_trigger_map = {}
|
||||
|
||||
signals_module = import_by_name(signals_module)
|
||||
for name in dir(signals_module):
|
||||
if "_" not in name or name.startswith("__"):
|
||||
continue
|
||||
|
||||
try:
|
||||
doc = getattr(signals_module, name).__doc__
|
||||
# 解析信号函数参数
|
||||
pats = re.findall(r"参数模板:\"(.*)\"", doc)
|
||||
if pats:
|
||||
sig_pats_map[name] = pats[0]
|
||||
|
||||
# 解析信号列表
|
||||
sigs = re.findall(r"Signal\('(.*)'\)", doc)
|
||||
if sigs:
|
||||
sig_name_map[name] = [Signal(x) for x in sigs]
|
||||
|
||||
# 解析触发条件
|
||||
触发匹配 = re.findall(r"触发条件:(.*)", doc)
|
||||
if 触发匹配:
|
||||
sig_trigger_map[name] = [x.strip() for x in 触发匹配[0].split(",")]
|
||||
|
||||
except (OSError, ImportError, TypeError, ValueError, AttributeError) as e:
|
||||
logger.error(f"解析信号函数 {name} 出错:{e}")
|
||||
|
||||
# 为每个 k3 生成独立 pattern(支持单函数多 k3 信号,如 youwukuncheng 的 3 个 k3)。
|
||||
# base pattern 末段是 k3,按 sig_name_map 里各 Signal 的 k3 逐一替换。
|
||||
_multi_pats: Dict[str, List[str]] = {}
|
||||
for _name, _base in sig_pats_map.items():
|
||||
_sigs = sig_name_map.get(_name, [])
|
||||
if _sigs:
|
||||
_prefix = _base.rsplit("_", 1)[0] if "_" in _base else _base
|
||||
_pats: List[str] = []
|
||||
for _s in _sigs:
|
||||
_p = f"{_prefix}_{_s.k3}"
|
||||
if _p not in _pats:
|
||||
_pats.append(_p)
|
||||
_multi_pats[_name] = _pats
|
||||
else:
|
||||
_multi_pats[_name] = [_base]
|
||||
|
||||
self.sig_name_map = sig_name_map
|
||||
self.sig_pats_map = _multi_pats # name → List[pattern](每个 k3 一个)
|
||||
self.sig_trigger_map = sig_trigger_map
|
||||
|
||||
def parse_params(self, name, signal):
|
||||
"""获取信号函数参数
|
||||
|
||||
函数执行逻辑:
|
||||
|
||||
1. 首先根据传入的 name 和 signal 参数,通过 Signal(signal).key 获取一个键值。
|
||||
2. 然后从实例变量 sig_pats_map 中获取与指定名称对应的参数模板,并将其存储在 pats 中。
|
||||
3. 如果没有找到参数模板,则返回 None。
|
||||
4. 最后将信号函数的完整名称存储在参数字典中,并返回参数字典。
|
||||
|
||||
:param name: 信号函数名称, 如:cxt_bi_end_V230222
|
||||
:param signal: 需要解析的信号, 如:15分钟_D1K_量柱V221218_低量柱_6K_任意_0
|
||||
:return:
|
||||
"""
|
||||
key = Signal(signal).key
|
||||
pats_list = self.sig_pats_map.get(name, None)
|
||||
if not pats_list:
|
||||
return None
|
||||
|
||||
for pats in pats_list:
|
||||
try:
|
||||
parsed = parse(pats, key)
|
||||
except (ValueError, KeyError, TypeError, AttributeError):
|
||||
continue
|
||||
if parsed is None:
|
||||
continue
|
||||
params = parsed.named
|
||||
if "di" in params:
|
||||
params["di"] = int(params["di"])
|
||||
|
||||
params["name"] = f"{self.signals_module}.{name}"
|
||||
|
||||
# 附加上下文:触发条件与函数短名(供 信号计算器 优化用)
|
||||
触发条件 = self.sig_trigger_map.get(name)
|
||||
if 触发条件:
|
||||
params["触发条件"] = 触发条件
|
||||
params["_func_short_name"] = name
|
||||
|
||||
return params
|
||||
|
||||
logger.error(f"解析信号 {signal} - {name} 出错:无匹配模式 {pats_list}")
|
||||
return None
|
||||
|
||||
def get_function_name(self, signal: str):
|
||||
"""获取信号对应的信号函数名称
|
||||
|
||||
函数执行逻辑:
|
||||
|
||||
1. 创建一个 _signal 对象,通过传入的信号字符串进行初始化。
|
||||
2. 通过遍历 sig_name_map 中的项目,找出那些与 _signal.k3 相匹配的键,并将它们存储在 _k3_match 列表中。
|
||||
3. 如果只有一个匹配项,则返回该项;否则记录错误日志并返回 None。
|
||||
|
||||
:param signal: 信号,数据样例:15分钟_D1K_量柱V221218_低量柱_6K_任意_0
|
||||
:return: 信号函数名称
|
||||
"""
|
||||
sig_name_map = self.sig_name_map
|
||||
_signal = Signal(signal)
|
||||
_k3_match = list({k for k, v in sig_name_map.items() for s in v if s.k3 == _signal.k3})
|
||||
|
||||
# 多匹配时排除模板函数(以 "模板_" 开头)
|
||||
if len(_k3_match) > 1:
|
||||
non_template = [k for k in _k3_match if not k.startswith("模板_")]
|
||||
if len(non_template) == 1:
|
||||
return non_template[0]
|
||||
|
||||
if len(_k3_match) == 1:
|
||||
return _k3_match[0]
|
||||
else:
|
||||
logger.error(f"信号 {signal} 有多个匹配函数:{_k3_match},请手动解析信号")
|
||||
return None
|
||||
|
||||
def config_to_keys(self, config: List[Dict]):
|
||||
"""将信号函数配置转换为信号key列表
|
||||
|
||||
函数执行逻辑:
|
||||
|
||||
1. 首先创建了一个空列表 keys 用于存储信号key。
|
||||
2. 对于传入的 config 列表中的每个配置字典 conf 进行以下操作:
|
||||
- 获取信号函数的名称。
|
||||
- 如果该信号函数的名称在 self.sig_pats_map 中存在对应的模板,使用参数填充模板,并将结果添加到 keys 列表中。
|
||||
|
||||
:param config: 信号函数配置
|
||||
|
||||
config = [{'freq': '日线', 'max_overlap': '3', 'name': 'czsc.signals.cxt_bi_end_V230222'},
|
||||
{'freq1': '日线', 'freq2': '60分钟', 'name': 'czsc.signals.cxt_zhong_shu_gong_zhen_V221221'}]
|
||||
|
||||
:return: 信号key列表
|
||||
"""
|
||||
keys = []
|
||||
for conf in config:
|
||||
name = conf["name"].split(".")[-1]
|
||||
if name in self.sig_pats_map:
|
||||
for pats in self.sig_pats_map[name]:
|
||||
keys.append(pats.format(**conf))
|
||||
return keys
|
||||
|
||||
def parse(self, signal_seq: List[str]):
|
||||
"""解析信号序列
|
||||
|
||||
函数执行逻辑:
|
||||
|
||||
1. 接受一个signal_seq 参数。
|
||||
2. 定义一个空列表res ,用于存储解析结果。
|
||||
3. 遍历信号序列signal_seq 中的每一个信号:
|
||||
|
||||
- 调用get_function_name 方法,以信号为参数,获取该信号对应的函数名。
|
||||
- 进行函数名存在性判断,name 在sig_pats_map 中存在,
|
||||
调用parse_params 方法,以函数名和信号为参数,解析参数并返回结果。
|
||||
|
||||
:param signal_seq: 信号序列, 样例:
|
||||
['15分钟_D1K_量柱V221218_低量柱_6K_任意_0', '日线_D1K_量柱V221218_低量柱_6K_任意_0']
|
||||
:return: 信号函数配置
|
||||
"""
|
||||
res = []
|
||||
for signal in signal_seq:
|
||||
name = self.get_function_name(signal)
|
||||
if name in self.sig_pats_map:
|
||||
row = self.parse_params(name, signal)
|
||||
if row and row not in res:
|
||||
res.append(row)
|
||||
else:
|
||||
logger.warning(f"未找到解析函数:{name},请手动解析信号:{signal}")
|
||||
return res
|
||||
|
||||
|
||||
def get_signals_config(signals_seq: List[str], signals_module: str = "") -> List[Dict]:
|
||||
"""获取信号列表对应的信号函数配置
|
||||
|
||||
函数执行逻辑:
|
||||
|
||||
1. 首先创建了一个 SignalsParser 类的实例对象 sp,传入了参数 signals_module进行初始化,
|
||||
初始化工作主要是解析signals_module下的信号函数,生成了sig_pats_map信号参数模板字典和sig_name_map信号列表字典。
|
||||
2. 然后使用 sp 实例调用 parse 方法,该方法解析 signals_seq 中的信号,并返回信号函数的配置信息。
|
||||
|
||||
:param signals_seq: 信号列表
|
||||
:param signals_module: 信号函数所在模块
|
||||
:return: 信号函数配置
|
||||
"""
|
||||
sp = SignalsParser(signals_module=signals_module)
|
||||
conf = sp.parse(signals_seq)
|
||||
return conf
|
||||
|
||||
|
||||
def create_single_signal(**kwargs) -> OrderedDict:
|
||||
"""创建单个信号"""
|
||||
s = OrderedDict()
|
||||
k1, k2, k3 = kwargs.get("k1", "任意"), kwargs.get("k2", "任意"), kwargs.get("k3", "任意")
|
||||
v1, v2, v3 = kwargs.get("v1", "任意"), kwargs.get("v2", "任意"), kwargs.get("v3", "任意")
|
||||
v = Signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2, v3=v3, score=kwargs.get("score", 0))
|
||||
s[v.key] = v.value
|
||||
return s
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# Position — 持仓管理
|
||||
# ==============================================================================
|
||||
|
||||
|
||||
class Position(_PositionBase):
|
||||
"""持仓对象 — 配置 + 状态机均已迁移到 Rust 核心。
|
||||
|
||||
仓位表达:1 持有多头,-1 持有空头,0 空仓。
|
||||
|
||||
Rust 基类(chanlun._chanlun.Position)提供:
|
||||
- 配置字段 symbol/opens/exits/events/name/interval/timeout/stop_loss/T0(只读 getter)
|
||||
- 状态字段 pos/pos_changed/operates/holds/pairs(只读 getter)
|
||||
- update(信号字典) — 持仓状态机
|
||||
- dump(with_data) — 序列化(含可选状态)
|
||||
- load(raw) — 反序列化(静态方法)
|
||||
- unique_signals、__repr__
|
||||
本子类仅保留 get_signals_config(需 Python signals_module)。
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# 状态字段已由 Rust #[new] 初始化;无需 Python 侧初始化。
|
||||
# 不调用 super().__init__():PyO3 #[new] 已在 __new__ 阶段建好内部配置。
|
||||
pass
|
||||
|
||||
def get_signals_config(self, signals_module: str = "") -> List[Dict]:
|
||||
"""获取事件的信号配置"""
|
||||
return get_signals_config(self.unique_signals, signals_module)
|
||||
|
||||
def dump(self, with_data: bool = False) -> dict:
|
||||
"""序列化为 dict。Rust 基类 dump(with_data) 处理配置 + 可选状态。"""
|
||||
return super().dump(with_data=with_data)
|
||||
|
||||
@classmethod
|
||||
def load(cls, raw: dict) -> "Position":
|
||||
"""从 dict 反序列化为 Position(子类实例);opens/exits 用 Rust Event.load 还原。"""
|
||||
return cls(
|
||||
symbol=raw["symbol"],
|
||||
name=raw["name"],
|
||||
opens=[Event.load(x) for x in raw.get("opens", [])],
|
||||
exits=[Event.load(x) for x in raw.get("exits", [])],
|
||||
interval=raw["interval"],
|
||||
timeout=raw["timeout"],
|
||||
stop_loss=raw["stop_loss"],
|
||||
T0=raw["T0"],
|
||||
)
|
||||
|
||||
|
||||
class 信号计算器:
|
||||
"""多周期信号计算引擎 — 基于观察者字典。
|
||||
|
||||
不再依赖 立体分析器,直接接收 ``{周期秒: 观察者}`` 字典。
|
||||
|
||||
使用方式::
|
||||
|
||||
分析器 = 立体分析器("btcusd", [300, 900, 3600], 配置)
|
||||
观察者字典 = {p: 分析器._单体分析器[p] for p in 分析器.周期组}
|
||||
计算器 = 信号计算器(观察者字典, 基础周期=300, 信号配置=[...])
|
||||
|
||||
for k in k线列表:
|
||||
分析器.投喂K线(k)
|
||||
计算器.更新()
|
||||
print(计算器.信号字典)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
分析器: 立体分析器,
|
||||
信号配置: Optional[List[Dict]] = None,
|
||||
信号模块: str = "",
|
||||
):
|
||||
self._分析器 = 分析器
|
||||
self._观察者字典 = {p: 分析器._单体分析器[p] for p in 分析器.周期组}
|
||||
self._基础周期 = 分析器.周期组[0]
|
||||
self._信号模块 = 信号模块
|
||||
self._信号函数缓存: Dict[str, Callable] = {}
|
||||
self.信号: dict = {}
|
||||
self.行情: dict = {}
|
||||
self.信号配置 = 信号配置 or []
|
||||
self._自动挂载指标()
|
||||
|
||||
@property
|
||||
def 信号字典(self) -> dict: # 向后兼容:合并返回
|
||||
return {**self.信号, **self.行情}
|
||||
|
||||
@property
|
||||
def 信号配置(self) -> List[Dict]:
|
||||
return self._信号配置
|
||||
|
||||
@信号配置.setter
|
||||
def 信号配置(self, value: List[Dict]):
|
||||
可用周期 = set(self._分析器.周期组)
|
||||
for c in value:
|
||||
freq = c.get("freq")
|
||||
if freq is not None:
|
||||
周期秒 = int(freq)
|
||||
if 周期秒 not in 可用周期:
|
||||
raise ValueError(f"信号配置 freq={freq}({周期秒}s) 不在分析器周期组 {sorted(可用周期)} 中\n 信号: {c.get('name', '?')}")
|
||||
self._信号配置 = self._去重配置(value)
|
||||
self._预加载信号函数()
|
||||
|
||||
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)
|
||||
else:
|
||||
logger.warning(f"信号计算器: 重复信号配置已跳过 — {c.get('name', '?')} { {k: v for k, v in c.items() if k != 'name'} }")
|
||||
return unique
|
||||
|
||||
def _预加载信号函数(self):
|
||||
for config in self._信号配置:
|
||||
name = config.get("name")
|
||||
if name and name not in self._信号函数缓存:
|
||||
try:
|
||||
self._信号函数缓存[name] = self._解析信号函数(name)
|
||||
except (ImportError, ModuleNotFoundError, AttributeError, KeyError) as e:
|
||||
logger.warning(f"信号计算器: 无法导入 {name} ({e}),跳过")
|
||||
|
||||
@staticmethod
|
||||
def _解析信号函数(name: str):
|
||||
"""解析信号函数名,返回可调用对象。
|
||||
|
||||
当运行在 __main__ 上下文中且目标模块为 chan 时,优先使用 __main__
|
||||
命名空间中的函数,避免 import_by_name 触发 chan 模块的重复导入。
|
||||
"""
|
||||
if "." in name:
|
||||
module_name, func_name = name.rsplit(".", 1)
|
||||
main_mod = sys.modules.get("__main__")
|
||||
if main_mod is not None and hasattr(main_mod, func_name):
|
||||
# 验证 __main__ 确实是目标模块(通过文件名判断)
|
||||
main_file = getattr(main_mod, "__file__", "")
|
||||
expected_path = module_name.replace(".", os.sep) + ".py"
|
||||
if main_file.endswith(expected_path):
|
||||
return getattr(main_mod, func_name)
|
||||
|
||||
return import_by_name(name)
|
||||
|
||||
def _自动挂载指标(self):
|
||||
"""根据信号配置参数,在对应周期的观察者上自动补全缺失的指标。"""
|
||||
from collections import defaultdict
|
||||
|
||||
待补MACD: Dict[int, List[tuple]] = defaultdict(list)
|
||||
待补均线: Dict[int, List[tuple]] = defaultdict(list)
|
||||
|
||||
for config in self._信号配置:
|
||||
name = config.get("name", "")
|
||||
freq = config.get("freq")
|
||||
if not freq:
|
||||
continue
|
||||
周期秒 = int(freq)
|
||||
if 周期秒 not in self._观察者字典:
|
||||
continue
|
||||
|
||||
# MACD 类信号:从 config 解析 fast/slow/signal 参数
|
||||
if "macd" in name.lower() or "中枢" in name or "背驰" in name or "金叉" in name:
|
||||
fast = int(config.get("fast", config.get("快线周期", 13)))
|
||||
slow = int(config.get("slow", config.get("慢线周期", 31)))
|
||||
sig = int(config.get("signal", config.get("信号周期", 11)))
|
||||
key = f"macd_{fast}_{slow}_{sig}"
|
||||
if not any(t[0] == key for t in 待补MACD[周期秒]):
|
||||
待补MACD[周期秒].append((key, "收", fast, slow, sig))
|
||||
|
||||
# MA 类信号:从 config 解析 ma_type/timeperiod
|
||||
if "ma_" in name or "tas_ma" in name or "均线" in name:
|
||||
ma_type = config.get("ma_type", "SMA").upper()
|
||||
period = int(config.get("timeperiod", config.get("周期", 5)))
|
||||
key = f"{ma_type}_{period}"
|
||||
if not any(t[0] == key for t in 待补均线[周期秒]):
|
||||
待补均线[周期秒].append((key, "收", ma_type, period))
|
||||
|
||||
for 周期秒, macd_list in 待补MACD.items():
|
||||
cfg = self._观察者字典[周期秒].配置
|
||||
if not cfg.计算指标:
|
||||
cfg.计算指标 = True
|
||||
已有键 = {t[0] for t in cfg.MACD_参数列表}
|
||||
# 同时检查 (快线, 慢线, 信号) 参数避免只键名不同但参数相同的重复
|
||||
已有参数 = {(t[2], t[3], t[4]) for t in cfg.MACD_参数列表 if len(t) >= 5}
|
||||
新增 = [t for t in macd_list if t[0] not in 已有键 and (t[2], t[3], t[4]) not in 已有参数]
|
||||
if 新增:
|
||||
cfg.MACD_参数列表.extend(新增)
|
||||
if "macd" not in 已有键:
|
||||
cfg.MACD_参数列表.insert(0, ("macd", "收", 新增[0][2], 新增[0][3], 新增[0][4]))
|
||||
logger.warning(f"信号计算器: 周期{周期秒}s 自动补全 MACD — {[t[0] for t in 新增]}")
|
||||
|
||||
for 周期秒, ma_list in 待补均线.items():
|
||||
cfg = self._观察者字典[周期秒].配置
|
||||
if not cfg.计算指标:
|
||||
cfg.计算指标 = True
|
||||
已有 = {t[0] for t in cfg.均线参数列表}
|
||||
新增 = [t for t in ma_list if t[0] not in 已有]
|
||||
if 新增:
|
||||
cfg.均线参数列表.extend(新增)
|
||||
logger.warning(f"信号计算器: 周期{周期秒}s 自动补全 均线 — {[t[0] for t in 新增]}")
|
||||
|
||||
def 从信号列表提取配置(self, 信号序列: List[str]):
|
||||
"""从信号序列自动生成信号配置"""
|
||||
self.信号配置 = get_signals_config(list(set(信号序列)), self._信号模块)
|
||||
|
||||
def 更新(self):
|
||||
"""遍历信号配置,调用信号函数。结果写入 self.信号 和 self.行情。"""
|
||||
self.信号.clear()
|
||||
self.行情.clear()
|
||||
|
||||
for config in self._信号配置:
|
||||
try:
|
||||
result = self._执行信号函数(config)
|
||||
if result:
|
||||
for k, v in result.items():
|
||||
if v != "任意_任意_任意_0":
|
||||
self.信号[k] = v
|
||||
except (TypeError, ValueError, KeyError, AttributeError, IndexError) as e:
|
||||
logger.error(f"信号计算器: {config.get('name', '?')} 出错 — {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
# OHLCV 行情
|
||||
基础观察者 = self._观察者字典.get(self._基础周期)
|
||||
if 基础观察者 and 基础观察者.普通K线序列:
|
||||
最后K线 = 基础观察者.普通K线序列[-1]
|
||||
时间戳 = 最后K线.时间戳
|
||||
if isinstance(时间戳, (int, float)):
|
||||
时间戳 = datetime.fromtimestamp(int(时间戳))
|
||||
self.行情.update(
|
||||
symbol=基础观察者.符号,
|
||||
dt=时间戳,
|
||||
id=最后K线.序号,
|
||||
open=最后K线.开盘价,
|
||||
close=最后K线.收盘价,
|
||||
high=最后K线.高,
|
||||
low=最后K线.低,
|
||||
vol=最后K线.成交量,
|
||||
)
|
||||
|
||||
def _执行信号函数(self, config: Dict) -> Optional[OrderedDict]:
|
||||
param = dict(config)
|
||||
sig_name = param.pop("name")
|
||||
sig_func = self._信号函数缓存.get(sig_name) or self._解析信号函数(sig_name)
|
||||
|
||||
freq = param.get("freq")
|
||||
if freq is not None:
|
||||
周期秒 = int(freq)
|
||||
obs = self._观察者字典.get(周期秒)
|
||||
if obs is not None:
|
||||
return sig_func(obs, **param)
|
||||
else:
|
||||
raise KeyError(f"信号计算器: 未找到周期 {周期秒}s 的观察者,可用周期: {sorted(self._观察者字典.keys())}")
|
||||
else:
|
||||
return sig_func(self, **param)
|
||||
|
||||
def 获取周期观察者(self, freq: str) -> Optional[观察者]:
|
||||
return self._观察者字典.get(int(freq))
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,113 +0,0 @@
|
||||
# Copyright (c) 2012-2019 Richard Jones <richard@python.org>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import re
|
||||
from typing import Any, Callable, Generic, Literal, Protocol, TypeVar, overload
|
||||
|
||||
__all__ = ["parse", "search", "findall", "with_pattern"]
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_T_co = TypeVar("_T_co", covariant=True)
|
||||
|
||||
class _TypeConverter(Protocol[_T_co]):
|
||||
def __call__(self, string: str) -> _T_co: ...
|
||||
|
||||
_TTypeConverter = TypeVar("_TTypeConverter", bound="_TypeConverter[Any]")
|
||||
|
||||
def with_pattern(pattern: str, regex_group_count=None) -> Callable[[_TTypeConverter], _TTypeConverter]: ...
|
||||
|
||||
class Result:
|
||||
fixed: tuple[Any, ...]
|
||||
named: dict[str, Any]
|
||||
spans: dict[int | str, tuple[int, int]]
|
||||
|
||||
def __init__(self, fixed: tuple[Any, ...], named: dict[str, Any], spans: dict[int | str, tuple[int, int]]) -> None: ...
|
||||
def __getitem__(self, item) -> Any: ...
|
||||
def __contains__(self, name) -> bool: ...
|
||||
|
||||
class Match:
|
||||
parser: "Parser"
|
||||
match: re.Match # type: ignore[type-arg]
|
||||
|
||||
def __init__(self, parser: "Parser", match: re.Match) -> None: ... # type: ignore[type-arg]
|
||||
def evaluate_result(self) -> Result: ...
|
||||
|
||||
class ResultIterator(Generic[_T]):
|
||||
parser: "Parser"
|
||||
string: str
|
||||
pos: int
|
||||
endpos: int
|
||||
evaluate_result: bool
|
||||
def __next__(self) -> _T: ...
|
||||
next = __next__
|
||||
|
||||
def __init__(self, parser: "Parser", string: str, pos: int, endpos: int | None, evaluate_result: bool = True) -> None: ...
|
||||
def __iter__(self) -> "ResultIterator[_T]": ...
|
||||
|
||||
class TooManyFields(ValueError): ...
|
||||
class RepeatedNameError(ValueError): ...
|
||||
|
||||
class Parser:
|
||||
def __init__(self, format: str, extra_types: dict[str, _TypeConverter[Any]] | None = None, case_sensitive: bool = False) -> None: ...
|
||||
@property
|
||||
def named_fields(self) -> list[str]: ...
|
||||
@property
|
||||
def fixed_fields(self) -> list[int]: ...
|
||||
@property
|
||||
def format(self) -> str: ...
|
||||
@overload
|
||||
def parse(self, string: str, evaluate_result: Literal[True] = True) -> Result | None: ...
|
||||
@overload
|
||||
def parse(self, string: str, *, evaluate_result: Literal[False]) -> Match | None: ...
|
||||
@overload
|
||||
def parse(self, string: str, evaluate_result: Literal[False]) -> Match | None: ...
|
||||
@overload
|
||||
def search(self, string: str, pos: int = 0, endpos: int | None = None, evaluate_result: Literal[True] = True) -> Result | None: ...
|
||||
@overload
|
||||
def search(self, string: str, pos: int = 0, endpos: int | None = None, *, evaluate_result: Literal[False]) -> Match | None: ...
|
||||
@overload
|
||||
def search(self, string: str, pos: int, endpos: int | None, evaluate_result: Literal[False]) -> Match | None: ...
|
||||
@overload
|
||||
def findall(self, string: str, pos: int = 0, endpos=None, extra_types: dict[str, _TypeConverter[Any]] | None = None, evaluate_result: Literal[True] = True) -> ResultIterator[Result]: ...
|
||||
@overload
|
||||
def findall(self, string: str, pos: int = 0, endpos=None, extra_types: dict[str, _TypeConverter[Any]] | None = None, *, evaluate_result: Literal[False]) -> ResultIterator[Match]: ...
|
||||
@overload
|
||||
def findall(self, string: str, pos: int, endpos: int | None, extra_types, evaluate_result: Literal[False]) -> ResultIterator[Match]: ...
|
||||
def evaluate_result(self, m: re.Match) -> Result: ... # type: ignore[type-arg]
|
||||
|
||||
@overload
|
||||
def parse(format: str, string: str, extra_types: dict[str, _TypeConverter[Any]] | None = None, evaluate_result: Literal[True] = True, case_sensitive: bool = ...) -> Result | None: ...
|
||||
@overload
|
||||
def parse(format: str, string: str, extra_types: dict[str, _TypeConverter[Any]] | None = None, *, evaluate_result: Literal[False], case_sensitive: bool = ...) -> Match | None: ...
|
||||
@overload
|
||||
def parse(format: str, string: str, extra_types, evaluate_result: Literal[False], case_sensitive: bool = ...) -> Match | None: ...
|
||||
@overload
|
||||
def search(format: str, string: str, pos: int = 0, endpos: int | None = None, extra_types: dict[str, _TypeConverter[Any]] | None = None, evaluate_result: Literal[True] = True, case_sensitive: bool = False) -> Result | None: ...
|
||||
@overload
|
||||
def search(format: str, string: str, pos: int = 0, endpos: int | None = None, extra_types: dict[str, _TypeConverter[Any]] | None = None, *, evaluate_result: Literal[False], case_sensitive: bool = False) -> Match | None: ...
|
||||
@overload
|
||||
def search(format: str, string: str, pos: int, endpos: int | None, extra_types, evaluate_result: Literal[False], case_sensitive: bool = False) -> Match | None: ...
|
||||
@overload
|
||||
def findall(format: str, string: str, pos: int = 0, endpos=None, extra_types: dict[str, _TypeConverter[Any]] | None = None, evaluate_result: Literal[True] = True, case_sensitive: bool = False) -> ResultIterator[Result]: ...
|
||||
@overload
|
||||
def findall(format: str, string: str, pos: int = 0, endpos=None, extra_types: dict[str, _TypeConverter[Any]] | None = None, *, evaluate_result: Literal[False], case_sensitive: bool = False) -> ResultIterator[Match]: ...
|
||||
@overload
|
||||
def findall(format, string, pos, endpos, extra_types, evaluate_result: Literal[False], case_sensitive: bool = False) -> ResultIterator[Match]: ...
|
||||
def compile(format: str, extra_types: dict[str, _TypeConverter[Any]] | None = None, case_sensitive: bool = False) -> Parser: ...
|
||||
@@ -1,321 +0,0 @@
|
||||
"""信号编排器 — Rust 优先 + Python 回退的混合信号计算。
|
||||
|
||||
Rust 注册表优先(编译时 #[signal] 宏注册),Python import_by_name 回退。
|
||||
|
||||
使用方式::
|
||||
|
||||
分析器 = 立体分析器("btcusd", [300, 900, 3600], 配置)
|
||||
编排器 = SignalOrchestrator(分析器, 信号配置=[...])
|
||||
|
||||
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 import 观察者
|
||||
from chanlun._chanlun import (
|
||||
信号引擎 as _RustSignalEngine,
|
||||
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._信号模块 = 信号模块
|
||||
|
||||
# 分类存储
|
||||
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 []
|
||||
|
||||
# ── 信号配置 property ──
|
||||
|
||||
@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(可用周期)} 中\n 信号: {c.get('name', '?')}")
|
||||
|
||||
name = c.get("name", "")
|
||||
# 分类:含 '.' 的显式 Python 路径 → Python;短名查 Rust 注册表
|
||||
if "." in name:
|
||||
self._python_configs.append(c)
|
||||
elif name in rust_names:
|
||||
self._rust_configs.append(c)
|
||||
else:
|
||||
self._python_configs.append(c)
|
||||
|
||||
self._信号配置 = value
|
||||
self._预加载Python信号函数()
|
||||
|
||||
# ── 更新 ──
|
||||
|
||||
def 更新(self):
|
||||
"""执行所有信号:Rust 批量优先,Python 逐个回退。"""
|
||||
self.信号.clear()
|
||||
self.行情.clear()
|
||||
|
||||
# 0. 始终确保指标已计算(幂等),Rust/Python 信号都需要
|
||||
_RustSignalEngine(信号配置=[]).自动挂载指标(self._分析器)
|
||||
|
||||
# 1. Rust 批量执行
|
||||
if self._rust_configs:
|
||||
rust_cfgs = []
|
||||
for c in self._rust_configs:
|
||||
freq = int(c.get("freq", 0))
|
||||
rust_cfgs.append({"name": c["name"], "freq": str(freq)})
|
||||
|
||||
engine = _RustSignalEngine(信号配置=rust_cfgs)
|
||||
engine.自动挂载指标(self._分析器)
|
||||
|
||||
result = engine.更新_完整(self._分析器)
|
||||
if result.get("signals"):
|
||||
for k, v in result["signals"].items():
|
||||
if v != "任意_任意_任意_0":
|
||||
self.信号[k] = v
|
||||
if result.get("market"):
|
||||
self.行情 = dict(result["market"])
|
||||
|
||||
# 2. Python 回退(逐个 import_by_name 调用)
|
||||
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._提取行情()
|
||||
|
||||
# ── Python 信号函数执行 ──
|
||||
|
||||
def _执行Python信号函数(self, config: Dict) -> Optional[OrderedDict]:
|
||||
"""执行单个 Python 信号函数(import_by_name 动态导入)。"""
|
||||
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.get("freq", None)
|
||||
if freq is not None:
|
||||
周期秒 = int(freq)
|
||||
obs = self._观察者字典.get(周期秒)
|
||||
if obs is None:
|
||||
logger.warning(f"未找到周期 {freq} 的观察者")
|
||||
return None
|
||||
else:
|
||||
obs = self
|
||||
return sig_func(obs, **param)
|
||||
|
||||
# ── 辅助方法 ──
|
||||
|
||||
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
|
||||
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]:
|
||||
"""动态导入信号函数(与旧 信号计算器 逻辑一致)。"""
|
||||
if "." not in name:
|
||||
return __import__(name)
|
||||
|
||||
module_name, func_name = name.rsplit(".", 1)
|
||||
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, None)
|
||||
|
||||
def _提取行情(self):
|
||||
"""从基础周期观察者提取 OHLCV 行情。"""
|
||||
obs = self._观察者字典.get(self._基础周期)
|
||||
if obs is None:
|
||||
return
|
||||
klines = obs.普通K线序列
|
||||
if not klines:
|
||||
return
|
||||
k = klines[-1]
|
||||
self.行情 = {
|
||||
"symbol": obs.符号,
|
||||
"dt": k.时间戳,
|
||||
"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]):
|
||||
"""从信号字符串列表解析配置(Rust 模板 + Python SignalsParser 双路径)。"""
|
||||
from chanlun._chanlun import get_signal_template
|
||||
|
||||
if not 信号序列:
|
||||
return
|
||||
|
||||
rust_names = set(_rust_list_signals())
|
||||
configs = []
|
||||
seen = set()
|
||||
|
||||
for sig_key in 信号序列:
|
||||
matched = False
|
||||
# 1) 尝试 Rust 模板匹配
|
||||
for name in rust_names:
|
||||
template = get_signal_template(name)
|
||||
if template is None:
|
||||
continue
|
||||
from chanlun.parse import parse as _parse
|
||||
|
||||
parsed = _parse(template, sig_key)
|
||||
if parsed is not None:
|
||||
entry = {"name": name}
|
||||
entry.update(parsed.named)
|
||||
key = (name, frozenset((k, str(v)) for k, v in entry.items() if k != "name"))
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
configs.append(entry)
|
||||
matched = True
|
||||
break
|
||||
# 2) Python SignalsParser 回退
|
||||
if not matched:
|
||||
try:
|
||||
from chanlun.chan_external import SignalsParser
|
||||
|
||||
sp = SignalsParser(signals_module=self._信号模块)
|
||||
py_configs = sp.parse([sig_key])
|
||||
for c in py_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)
|
||||
configs.append(c)
|
||||
except Exception:
|
||||
logger.warning(f"无法解析信号 key: {sig_key}")
|
||||
|
||||
self.信号配置 = configs
|
||||
|
||||
|
||||
def get_signals_config(signal_keys: list, signals_module: str = "chanlun.signals") -> List[Dict]:
|
||||
"""从 Rust 注册表 + Python SignalsParser 生成信号配置(双路径)。
|
||||
|
||||
根据信号 key 字符串,优先用 Rust 注册表模板匹配,失败则回退到 Python SignalsParser。
|
||||
"""
|
||||
from chanlun._chanlun import list_signals, get_signal_template
|
||||
|
||||
rust_names = set(list_signals())
|
||||
configs = []
|
||||
seen = set()
|
||||
unmatched = []
|
||||
|
||||
for sig_key in signal_keys:
|
||||
matched = False
|
||||
# 1) Rust 模板
|
||||
for name in rust_names:
|
||||
template = get_signal_template(name)
|
||||
if template is None:
|
||||
continue
|
||||
from chanlun.parse import parse as _parse
|
||||
|
||||
parsed = _parse(template, sig_key)
|
||||
if parsed is not None:
|
||||
entry = {"name": name}
|
||||
entry.update(parsed.named)
|
||||
key = (name, frozenset((k, str(v)) for k, v in entry.items() if k != "name"))
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
configs.append(entry)
|
||||
matched = True
|
||||
break
|
||||
# 2) 回退到 Python
|
||||
if not matched:
|
||||
unmatched.append(sig_key)
|
||||
|
||||
if unmatched:
|
||||
try:
|
||||
from chanlun.chan_external import SignalsParser
|
||||
|
||||
sp = SignalsParser(signals_module=signals_module)
|
||||
py_configs = sp.parse(unmatched)
|
||||
for c in py_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)
|
||||
configs.append(c)
|
||||
except Exception:
|
||||
logger.warning(f"SignalsParser 无法解析: {unmatched}")
|
||||
|
||||
return configs
|
||||
@@ -1,14 +0,0 @@
|
||||
"""缠论技术分析库 — 信号函数模块
|
||||
|
||||
每个信号函数接收 观察者 对象 + 关键字参数,返回 OrderedDict。
|
||||
信号 key 格式:k1_k2_k3,value 格式:v1_v2_v3_score。
|
||||
"""
|
||||
|
||||
from chanlun.signals._template import 模板_V日期
|
||||
from chanlun.signals.demo import tas_ma_base_V230313
|
||||
from chanlun.signals.demo import tas_macd_direct_V221106
|
||||
from chanlun.signals.demo import macd_金叉
|
||||
from chanlun.signals.demo import cxt_bi_end_V230222
|
||||
from chanlun.signals.demo import cxt_停顿分型_V230106
|
||||
from chanlun.signals.demo import bar_zdt_V230331
|
||||
from chanlun.signals.youwukuncheng import *
|
||||
@@ -1,35 +0,0 @@
|
||||
"""信号函数模板 — 新建信号函数时以此为蓝本"""
|
||||
|
||||
from collections import OrderedDict
|
||||
from chanlun import 观察者
|
||||
from chanlun.chan_external import create_single_signal
|
||||
|
||||
|
||||
def 模板_V日期(观察员: 观察者, **kwargs) -> OrderedDict:
|
||||
"""##信号名称介绍##
|
||||
|
||||
触发条件:## 触发条件,注:当没有此条时则无条件执行 ##
|
||||
|
||||
参数模板:## 具体模板 如: "{freq}_D{di}#{ma_type}#{timeperiod}MO{max_overlap}_BS辅助V230313" ##
|
||||
|
||||
**信号逻辑:**
|
||||
|
||||
## 详细信号逻辑 ##
|
||||
|
||||
**信号列表:**
|
||||
|
||||
## 具体信号 如下:
|
||||
- Signal('15分钟_D1#SMA#5MO5_BS辅助V230313_看空_向下_任意_0')
|
||||
- Signal('15分钟_D1#SMA#5MO5_BS辅助V230313_看多_向下_任意_0')
|
||||
- Signal('15分钟_D1#SMA#5MO5_BS辅助V230313_看多_向上_任意_0')
|
||||
- Signal('15分钟_D1#SMA#5MO5_BS辅助V230313_看空_向上_任意_0')
|
||||
##
|
||||
|
||||
:param 观察员: 观察者对象
|
||||
:param kwargs: 其他参数
|
||||
- ## 具体参数介绍 ##
|
||||
:return: 信号识别结果
|
||||
"""
|
||||
## 具体代码过程 ##
|
||||
|
||||
return ## create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2) ##
|
||||
@@ -1,417 +0,0 @@
|
||||
"""缠论技术分析库 — 信号函数示例合集"""
|
||||
|
||||
from collections import OrderedDict
|
||||
from typing import List, Optional
|
||||
|
||||
from chanlun import 观察者, 分型结构, 虚线, 线段, 相对方向
|
||||
from chanlun.chan_external import create_single_signal
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 工具函数
|
||||
# ==============================================================================
|
||||
|
||||
|
||||
def _按需计算均线(普K序列: List, ma_type: str, timeperiod: int, offset: int = 0) -> Optional[float]:
|
||||
"""当 K线.指标.均线 中无预计算值时,从收盘价序列按需计算均线。
|
||||
|
||||
:param 普K序列: 普通K线序列
|
||||
:param ma_type: 均线类型(SMA/EMA)
|
||||
:param timeperiod: 均线周期
|
||||
:param offset: 从末尾倒数 offset 根K线(0=最后一根,di=倒数第di根)
|
||||
:return: 均线值,K线不足时返回 None
|
||||
"""
|
||||
n = len(普K序列)
|
||||
start = n - offset - timeperiod + 1
|
||||
end = n - offset + 1
|
||||
if start < 0:
|
||||
return None
|
||||
|
||||
closes = [普K序列[i].收盘价 for i in range(start, end)]
|
||||
|
||||
if ma_type == "SMA":
|
||||
return sum(closes) / len(closes)
|
||||
elif ma_type == "EMA":
|
||||
k = 2.0 / (timeperiod + 1)
|
||||
ema = closes[0]
|
||||
for price in closes[1:]:
|
||||
ema = price * k + ema * (1 - k)
|
||||
return ema
|
||||
return None
|
||||
|
||||
|
||||
def _获取或计算均线(普K序列: List, K线, ma_type: str, timeperiod: int, offset: int) -> Optional[float]:
|
||||
"""从K线指标容器获取均线,若缺失则按需计算。
|
||||
|
||||
:param 普K序列: 普通K线序列
|
||||
:param K线: 目标K线
|
||||
:param ma_type: 均线类型
|
||||
:param timeperiod: 均线周期
|
||||
:param offset: 从末尾倒数 offset 根K线
|
||||
:return: 均线值或 None
|
||||
"""
|
||||
ma_key = f"{ma_type}_{timeperiod}"
|
||||
try:
|
||||
if K线.指标 is not None:
|
||||
cached = K线.指标.均线.get(ma_key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
except Exception:
|
||||
pass
|
||||
return _按需计算均线(普K序列, ma_type, timeperiod, offset)
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# tas — 技术指标信号
|
||||
# ==============================================================================
|
||||
|
||||
|
||||
def tas_ma_base_V230313(c, **kwargs) -> OrderedDict:
|
||||
"""单均线多空和方向辅助开平仓信号
|
||||
|
||||
参数模板:"{freq}_D{di}#{ma_type}#{timeperiod}MO{max_overlap}_BS辅助V230313"
|
||||
|
||||
**信号逻辑:**
|
||||
|
||||
1. close > ma,多头(看多);反之,空头(看空)
|
||||
2. ma[-1] > ma[-2],向上;反之,向下
|
||||
3. 加入 max_overlap 参数控制相同信号最大重叠次数
|
||||
|
||||
**信号列表:**
|
||||
|
||||
- Signal('15分钟_D1#SMA#5MO5_BS辅助V230313_看空_向下_任意_0')
|
||||
- Signal('15分钟_D1#SMA#5MO5_BS辅助V230313_看多_向下_任意_0')
|
||||
- Signal('15分钟_D1#SMA#5MO5_BS辅助V230313_看多_向上_任意_0')
|
||||
- Signal('15分钟_D1#SMA#5MO5_BS辅助V230313_看空_向上_任意_0')
|
||||
|
||||
:param c: 观察者对象
|
||||
:param kwargs: 其他参数
|
||||
- ma_type: 均线类型(SMA/EMA)
|
||||
- timeperiod: 均线计算周期
|
||||
- di: 信号计算截止倒数第i根K线
|
||||
- max_overlap: 相同信号最大重叠次数
|
||||
:return: 信号识别结果
|
||||
"""
|
||||
ma_type = kwargs.get("ma_type", "SMA").upper()
|
||||
timeperiod = int(kwargs.get("timeperiod", 5))
|
||||
di = int(kwargs.get("di", 1))
|
||||
max_overlap = int(kwargs.get("max_overlap", 5))
|
||||
freq = kwargs.get("freq", "15分钟")
|
||||
|
||||
k1, k2, k3 = f"{freq}_D{di}#{ma_type}#{timeperiod}MO{max_overlap}_BS辅助V230313".split("_", 2)
|
||||
|
||||
普K序列 = c.普通K线序列
|
||||
if len(普K序列) < di + 1:
|
||||
return create_single_signal(k1=k1, k2=k2, k3=k3)
|
||||
|
||||
当前K线 = 普K序列[-di]
|
||||
当前均线 = _获取或计算均线(普K序列, 当前K线, ma_type, timeperiod, di)
|
||||
if 当前均线 is None:
|
||||
return create_single_signal(k1=k1, k2=k2, k3=k3)
|
||||
|
||||
当前价 = 当前K线.收盘价
|
||||
v1 = "看多" if 当前价 > 当前均线 else "看空"
|
||||
|
||||
# 均线方向:需要前一根K线的均线值
|
||||
if len(普K序列) >= di + 2:
|
||||
前均线 = _获取或计算均线(普K序列, 普K序列[-di - 1], ma_type, timeperiod, di + 1)
|
||||
if 前均线 is not None:
|
||||
v2 = "向上" if 当前均线 > 前均线 else "向下"
|
||||
else:
|
||||
v2 = "任意"
|
||||
else:
|
||||
v2 = "任意"
|
||||
|
||||
return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2)
|
||||
|
||||
|
||||
def tas_macd_direct_V221106(c, **kwargs) -> OrderedDict:
|
||||
"""MACD 方向信号 — DIF 在零轴上方为多头,下方为空头
|
||||
|
||||
参数模板:"{freq}_D{di}#MACD#{fast}#{slow}#{signal}_MACD方向V221106"
|
||||
|
||||
**信号逻辑:**
|
||||
|
||||
1. DIF > 0,多头;反之,空头
|
||||
2. DIF 值变化趋势(与前一根比较):向上/向下
|
||||
|
||||
**信号列表:**
|
||||
|
||||
- Signal('15分钟_D1#MACD#13#31#11_MACD方向V221106_看多_向上_任意_0')
|
||||
- Signal('15分钟_D1#MACD#13#31#11_MACD方向V221106_看多_向下_任意_0')
|
||||
- Signal('15分钟_D1#MACD#13#31#11_MACD方向V221106_看空_向上_任意_0')
|
||||
- Signal('15分钟_D1#MACD#13#31#11_MACD方向V221106_看空_向下_任意_0')
|
||||
|
||||
:param c: 观察者对象
|
||||
:param kwargs: 其他参数
|
||||
- fast: 快线周期(默认 13)
|
||||
- slow: 慢线周期(默认 31)
|
||||
- signal: 信号周期(默认 11)
|
||||
- di: 信号计算截止倒数第i根K线
|
||||
:return: 信号识别结果
|
||||
"""
|
||||
fast = int(kwargs.get("fast", 13))
|
||||
slow = int(kwargs.get("slow", 31))
|
||||
signal = int(kwargs.get("signal", 11))
|
||||
di = int(kwargs.get("di", 1))
|
||||
freq = kwargs.get("freq", "15分钟")
|
||||
|
||||
k1, k2, k3 = f"{freq}_D{di}#MACD#{fast}#{slow}#{signal}_MACD方向V221106".split("_", 2)
|
||||
|
||||
普K序列 = c.普通K线序列
|
||||
if len(普K序列) < di + 1:
|
||||
return create_single_signal(k1=k1, k2=k2, k3=k3)
|
||||
|
||||
当前K线 = 普K序列[-di]
|
||||
cur_macd = 当前K线.指标.macd if 当前K线.指标 else None
|
||||
if cur_macd is None or cur_macd.DIF is None:
|
||||
return create_single_signal(k1=k1, k2=k2, k3=k3)
|
||||
|
||||
v1 = "看多" if cur_macd.DIF > 0 else "看空"
|
||||
|
||||
if len(普K序列) >= di + 2:
|
||||
前K线 = 普K序列[-di - 1]
|
||||
prev_macd = 前K线.指标.macd if 前K线.指标 else None
|
||||
if prev_macd is not None and prev_macd.DIF is not None:
|
||||
v2 = "向上" if cur_macd.DIF > prev_macd.DIF else "向下"
|
||||
else:
|
||||
v2 = "任意"
|
||||
else:
|
||||
v2 = "任意"
|
||||
|
||||
return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2)
|
||||
|
||||
|
||||
def macd_金叉(观察员: 观察者, **kwargs) -> OrderedDict:
|
||||
"""MACD 金叉死叉信号 — DIF 与 DEA 的交叉判断
|
||||
|
||||
参数模板:"{freq}_D{di}#MACD#{fast}#{slow}#{signal}_MACD交叉V260601"
|
||||
|
||||
**信号逻辑:**
|
||||
|
||||
1. DIF 上穿 DEA(前一根 DIF <= DEA,当前 DIF > DEA)→ 金叉
|
||||
2. DIF 下穿 DEA(前一根 DIF >= DEA,当前 DIF < DEA)→ 死叉
|
||||
|
||||
**信号列表:**
|
||||
|
||||
- Signal('15分钟_D1#MACD#13#31#11_MACD交叉V260601_金叉_任意_任意_0')
|
||||
- Signal('15分钟_D1#MACD#13#31#11_MACD交叉V260601_死叉_任意_任意_0')
|
||||
|
||||
:param 观察员: 观察者对象
|
||||
:param kwargs: 其他参数
|
||||
- fast: 快线周期(默认 13)
|
||||
- slow: 慢线周期(默认 31)
|
||||
- signal: 信号周期(默认 11)
|
||||
- di: 信号计算截止倒数第i根K线
|
||||
:return: 信号识别结果
|
||||
"""
|
||||
fast = int(kwargs.get("fast", 13))
|
||||
slow = int(kwargs.get("slow", 31))
|
||||
signal = int(kwargs.get("signal", 11))
|
||||
di = int(kwargs.get("di", 1))
|
||||
freq = kwargs.get("freq", "15分钟")
|
||||
|
||||
k1, k2, k3 = f"{freq}_D{di}#MACD#{fast}#{slow}#{signal}_MACD交叉V260601".split("_", 2)
|
||||
|
||||
普K序列 = 观察员.普通K线序列
|
||||
if len(普K序列) < di + 2:
|
||||
return create_single_signal(k1=k1, k2=k2, k3=k3)
|
||||
|
||||
当前K线 = 普K序列[-di]
|
||||
前K线 = 普K序列[-di - 1]
|
||||
|
||||
cur_macd = 当前K线.指标.macd if 当前K线.指标 else None
|
||||
prev_macd = 前K线.指标.macd if 前K线.指标 else None
|
||||
|
||||
if cur_macd is None or prev_macd is None:
|
||||
return create_single_signal(k1=k1, k2=k2, k3=k3)
|
||||
if cur_macd.DIF is None or cur_macd.DEA is None:
|
||||
return create_single_signal(k1=k1, k2=k2, k3=k3)
|
||||
if prev_macd.DIF is None or prev_macd.DEA is None:
|
||||
return create_single_signal(k1=k1, k2=k2, k3=k3)
|
||||
|
||||
if prev_macd.DIF <= prev_macd.DEA and cur_macd.DIF > cur_macd.DEA:
|
||||
v1 = "金叉"
|
||||
elif prev_macd.DIF >= prev_macd.DEA and cur_macd.DIF < cur_macd.DEA:
|
||||
v1 = "死叉"
|
||||
else:
|
||||
v1 = "任意"
|
||||
|
||||
return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1)
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# cxt — 缠论形态信号
|
||||
# ==============================================================================
|
||||
|
||||
|
||||
def cxt_bi_end_V230222(c, **kwargs) -> OrderedDict:
|
||||
"""当前是最后笔的第几次新低底分型或新高顶分型,用于笔结束辅助
|
||||
|
||||
触发条件:新分型
|
||||
|
||||
参数模板:"{freq}_D1MO{max_overlap}_BE辅助V230222"
|
||||
|
||||
**信号逻辑:**
|
||||
|
||||
1. 取最后笔及未成笔的分型
|
||||
2. 当前如果是顶分型,则看当前顶分型是否新高,是第几个新高
|
||||
3. 当前如果是底分型,则看当前底分型是否新低,是第几个新低
|
||||
|
||||
**信号列表:**
|
||||
|
||||
- Signal('日线_D1MO3_BE辅助V230222_新低_第2次_任意_0')
|
||||
- Signal('日线_D1MO3_BE辅助V230222_新高_第2次_任意_0')
|
||||
- Signal('日线_D1MO3_BE辅助V230222_新低_第3次_任意_0')
|
||||
|
||||
:param c: 观察者对象
|
||||
:param kwargs:
|
||||
:return: 信号识别结果
|
||||
"""
|
||||
max_overlap = int(kwargs.get("max_overlap", 3))
|
||||
freq = kwargs.get("freq", "日线")
|
||||
k1, k2, k3 = f"{freq}_D1MO{max_overlap}_BE辅助V230222".split("_", 2)
|
||||
|
||||
分型序列 = c.分型序列
|
||||
笔序列 = c.笔序列
|
||||
|
||||
if len(分型序列) < 2 or len(笔序列) < 1:
|
||||
return create_single_signal(k1=k1, k2=k2, k3=k3)
|
||||
|
||||
最后笔 = 笔序列[-1]
|
||||
当前分型 = 分型序列[-1]
|
||||
|
||||
# 找到最后笔的武(终点分型)在分型序列中的位置
|
||||
try:
|
||||
笔终点索引 = next(i for i, f in enumerate(分型序列) if f.时间戳 == 最后笔.武.时间戳 and f.结构 == 最后笔.武.结构)
|
||||
except StopIteration:
|
||||
return create_single_signal(k1=k1, k2=k2, k3=k3)
|
||||
|
||||
# 取笔终点之后的分型(未成笔的分型)
|
||||
未成笔分型 = 分型序列[笔终点索引 + 1 :]
|
||||
if len(未成笔分型) < 1:
|
||||
return create_single_signal(k1=k1, k2=k2, k3=k3)
|
||||
|
||||
if 当前分型.结构.value == "顶":
|
||||
# 统计从笔终点到当前的顶分型新高次数
|
||||
笔终点顶高 = 最后笔.武.分型特征值
|
||||
计数 = 0
|
||||
for f in 未成笔分型:
|
||||
if f.结构.value == "顶" and f.分型特征值 > 笔终点顶高:
|
||||
计数 += 1
|
||||
笔终点顶高 = f.分型特征值
|
||||
if 计数 > 0 and 当前分型.分型特征值 >= 笔终点顶高:
|
||||
v1, v2 = "新高", f"第{计数}次"
|
||||
else:
|
||||
v1, v2 = "任意", "任意"
|
||||
elif 当前分型.结构.value == "底":
|
||||
笔终点底低 = 最后笔.武.分型特征值
|
||||
计数 = 0
|
||||
for f in 未成笔分型:
|
||||
if f.结构.value == "底" and f.分型特征值 < 笔终点底低:
|
||||
计数 += 1
|
||||
笔终点底低 = f.分型特征值
|
||||
if 计数 > 0 and 当前分型.分型特征值 <= 笔终点底低:
|
||||
v1, v2 = "新低", f"第{计数}次"
|
||||
else:
|
||||
v1, v2 = "任意", "任意"
|
||||
else:
|
||||
return create_single_signal(k1=k1, k2=k2, k3=k3)
|
||||
|
||||
return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2)
|
||||
|
||||
|
||||
def cxt_停顿分型_V230106(c, **kwargs) -> OrderedDict:
|
||||
"""停顿分型辅助信号 — 结合分型强度和MACD柱子匹配判断
|
||||
|
||||
触发条件:新分型
|
||||
|
||||
参数模板:"{freq}_D{di}停顿分型_BE辅助V230106"
|
||||
|
||||
**信号逻辑:**
|
||||
|
||||
判断当前分型是否为停顿分型,结合力度和形态给出信号。
|
||||
停顿分型 = 分型结构为顶/底 + 强度为强/中 + MACD柱子分型匹配。
|
||||
|
||||
**信号列表:**
|
||||
|
||||
- Signal('1分钟_D0停顿分型_BE辅助V230106_看空_强_任意_0')
|
||||
- Signal('1分钟_D0停顿分型_BE辅助V230106_看多_强_任意_0')
|
||||
- Signal('1分钟_D0停顿分型_BE辅助V230106_看空_中_任意_0')
|
||||
- Signal('1分钟_D0停顿分型_BE辅助V230106_看多_中_任意_0')
|
||||
|
||||
:param c: 观察者对象
|
||||
:param kwargs:
|
||||
:return: 信号识别结果
|
||||
"""
|
||||
di = int(kwargs.get("di", 0))
|
||||
freq = kwargs.get("freq", "1分钟")
|
||||
k1, k2, k3 = f"{freq}_D{di}停顿分型_BE辅助V230106".split("_", 2)
|
||||
|
||||
分型序列 = c.分型序列
|
||||
if len(分型序列) < di + 1:
|
||||
return create_single_signal(k1=k1, k2=k2, k3=k3)
|
||||
|
||||
当前分型 = 分型序列[-(di + 1)]
|
||||
|
||||
# 只对顶/底分型产出信号
|
||||
if 当前分型.结构.value not in ("顶", "底"):
|
||||
return create_single_signal(k1=k1, k2=k2, k3=k3)
|
||||
|
||||
v1 = "看空" if 当前分型.结构.value == "顶" else "看多"
|
||||
v2 = 当前分型.强度()
|
||||
|
||||
# 仅强/中分型 + MACD 柱子匹配时认为是有效的停顿分型
|
||||
if v2 in ("强", "中") and 当前分型.与MACD柱子分型匹配():
|
||||
pass # 保持 v1, v2
|
||||
elif v2 in ("强", "中"):
|
||||
pass # MACD不匹配也产出,但可能被下游过滤
|
||||
else:
|
||||
v1, v2 = "任意", "任意"
|
||||
|
||||
return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1, v2=v2)
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# bar — K线形态信号
|
||||
# ==============================================================================
|
||||
|
||||
|
||||
def bar_zdt_V230331(c, **kwargs) -> OrderedDict:
|
||||
"""计算倒数第di根K线的涨跌停信息
|
||||
|
||||
参数模板:"{freq}_D{di}_涨跌停V230331"
|
||||
|
||||
**信号逻辑:**
|
||||
|
||||
- close等于high且大于等于前一根K线的close,近似认为是涨停;反之,跌停。
|
||||
|
||||
**信号列表:**
|
||||
|
||||
- Signal('15分钟_D1_涨跌停V230331_涨停_任意_任意_0')
|
||||
- Signal('15分钟_D1_涨跌停V230331_跌停_任意_任意_0')
|
||||
|
||||
:param c: 基础周期的观察者对象
|
||||
:param kwargs:
|
||||
- di: 倒数第 di 根 K 线
|
||||
:return: 信号识别结果
|
||||
"""
|
||||
di = int(kwargs.get("di", 1))
|
||||
freq = kwargs.get("freq", "15分钟")
|
||||
k1, k2, k3 = f"{freq}_D{di}_涨跌停V230331".split("_", 2)
|
||||
|
||||
普K序列 = c.普通K线序列
|
||||
if len(普K序列) < di + 2:
|
||||
return create_single_signal(k1=k1, k2=k2, k3=k3)
|
||||
|
||||
当前K线 = 普K序列[-di]
|
||||
前K线 = 普K序列[-di - 1]
|
||||
|
||||
if 当前K线.收盘价 == 当前K线.高 and 当前K线.收盘价 >= 前K线.收盘价:
|
||||
v1 = "涨停"
|
||||
elif 当前K线.收盘价 == 当前K线.低 and 当前K线.收盘价 <= 前K线.收盘价:
|
||||
v1 = "跌停"
|
||||
else:
|
||||
v1 = "任意"
|
||||
|
||||
return create_single_signal(k1=k1, k2=k2, k3=k3, v1=v1)
|
||||
@@ -1,296 +0,0 @@
|
||||
# Copyright (c) 2008-2011 Volvox Development Team
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Konstantin Lepa <konstantin.lepa@gmail.com>
|
||||
|
||||
"""ANSI color formatting for output in terminal."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from functools import cache
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
__all__ = ["ATTRIBUTES", "COLORS", "HIGHLIGHTS", "RESET", "can_colorize", "colored", "cprint"]
|
||||
ATTRIBUTES: dict[str, int] = {
|
||||
"bold": 1,
|
||||
"dark": 2,
|
||||
"italic": 3,
|
||||
"underline": 4,
|
||||
"blink": 5,
|
||||
"reverse": 7,
|
||||
"concealed": 8,
|
||||
"strike": 9,
|
||||
}
|
||||
|
||||
HIGHLIGHTS: dict[str, int] = {
|
||||
"on_black": 40,
|
||||
"on_grey": 40, # Actually black but kept for backwards compatibility
|
||||
"on_red": 41,
|
||||
"on_green": 42,
|
||||
"on_yellow": 43,
|
||||
"on_blue": 44,
|
||||
"on_magenta": 45,
|
||||
"on_cyan": 46,
|
||||
"on_light_grey": 47,
|
||||
"on_dark_grey": 100,
|
||||
"on_light_red": 101,
|
||||
"on_light_green": 102,
|
||||
"on_light_yellow": 103,
|
||||
"on_light_blue": 104,
|
||||
"on_light_magenta": 105,
|
||||
"on_light_cyan": 106,
|
||||
"on_white": 107,
|
||||
}
|
||||
|
||||
COLORS: dict[str, int] = {
|
||||
"black": 30,
|
||||
"grey": 30, # Actually black but kept for backwards compatibility
|
||||
"red": 31,
|
||||
"green": 32,
|
||||
"yellow": 33,
|
||||
"blue": 34,
|
||||
"magenta": 35,
|
||||
"cyan": 36,
|
||||
"light_grey": 37,
|
||||
"dark_grey": 90,
|
||||
"light_red": 91,
|
||||
"light_green": 92,
|
||||
"light_yellow": 93,
|
||||
"light_blue": 94,
|
||||
"light_magenta": 95,
|
||||
"light_cyan": 96,
|
||||
"white": 97,
|
||||
}
|
||||
|
||||
|
||||
RESET = "\033[0m"
|
||||
|
||||
|
||||
@cache
|
||||
def can_colorize(*, no_color: bool | None = None, force_color: bool | None = None) -> bool:
|
||||
"""Check env vars and for tty/dumb terminal"""
|
||||
# First check overrides:
|
||||
# "User-level configuration files and per-instance command-line arguments should
|
||||
# override $NO_COLOR. A user should be able to export $NO_COLOR in their shell
|
||||
# configuration file as a default, but configure a specific program in its
|
||||
# configuration file to specifically enable color."
|
||||
# https://no-color.org
|
||||
if no_color is not None and no_color:
|
||||
return False
|
||||
if force_color is not None and force_color:
|
||||
return True
|
||||
|
||||
# Then check env vars:
|
||||
if os.environ.get("ANSI_COLORS_DISABLED"):
|
||||
return False
|
||||
if os.environ.get("NO_COLOR"):
|
||||
return False
|
||||
if os.environ.get("FORCE_COLOR"):
|
||||
return True
|
||||
|
||||
# Then check system:
|
||||
if os.environ.get("TERM") == "dumb":
|
||||
return False
|
||||
if not hasattr(sys.stdout, "fileno"):
|
||||
return False
|
||||
|
||||
try:
|
||||
return os.isatty(sys.stdout.fileno())
|
||||
except OSError:
|
||||
return sys.stdout.isatty()
|
||||
|
||||
|
||||
def _check_rgb(rgb: tuple[int, int, int]) -> None:
|
||||
if len(rgb) != 3 or not all(0 <= c <= 255 for c in rgb):
|
||||
msg = f"Expected a tuple of 3 ints in range 0-255, got {rgb!r}"
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
def colored(
|
||||
text: object,
|
||||
color: str | tuple[int, int, int] | None = None,
|
||||
on_color: str | tuple[int, int, int] | None = None,
|
||||
attrs: Iterable[str] | None = None,
|
||||
*,
|
||||
no_color: bool | None = None,
|
||||
force_color: bool | None = None,
|
||||
) -> str:
|
||||
"""Colorize text.
|
||||
|
||||
Available text colors:
|
||||
black, red, green, yellow, blue, magenta, cyan, white,
|
||||
light_grey, dark_grey, light_red, light_green, light_yellow, light_blue,
|
||||
light_magenta, light_cyan.
|
||||
|
||||
Available text highlights:
|
||||
on_black, on_red, on_green, on_yellow, on_blue, on_magenta, on_cyan, on_white,
|
||||
on_light_grey, on_dark_grey, on_light_red, on_light_green, on_light_yellow,
|
||||
on_light_blue, on_light_magenta, on_light_cyan.
|
||||
|
||||
Alternatively, both text colors (color) and highlights (on_color) may
|
||||
be specified via a tuple of 0-255 ints (R, G, B).
|
||||
|
||||
Available attributes:
|
||||
bold, dark, italic, underline, blink, reverse, concealed, strike.
|
||||
|
||||
Example:
|
||||
colored('Hello, World!', 'red', 'on_black', ['bold', 'blink'])
|
||||
colored('Hello, World!', 'green')
|
||||
colored('Hello, World!', (255, 0, 255)) # Purple
|
||||
"""
|
||||
result = str(text)
|
||||
if not can_colorize(no_color=no_color, force_color=force_color):
|
||||
return result
|
||||
|
||||
fmt_str = "\033[%dm%s"
|
||||
rgb_fore_fmt_str = "\033[38;2;%d;%d;%dm%s"
|
||||
rgb_back_fmt_str = "\033[48;2;%d;%d;%dm%s"
|
||||
if color is not None:
|
||||
if isinstance(color, str):
|
||||
result = fmt_str % (COLORS[color], result)
|
||||
elif isinstance(color, tuple):
|
||||
_check_rgb(color)
|
||||
result = rgb_fore_fmt_str % (color[0], color[1], color[2], result)
|
||||
|
||||
if on_color is not None:
|
||||
if isinstance(on_color, str):
|
||||
result = fmt_str % (HIGHLIGHTS[on_color], result)
|
||||
elif isinstance(on_color, tuple):
|
||||
_check_rgb(on_color)
|
||||
result = rgb_back_fmt_str % (on_color[0], on_color[1], on_color[2], result)
|
||||
|
||||
if attrs is not None:
|
||||
for attr in attrs:
|
||||
result = fmt_str % (ATTRIBUTES[attr], result)
|
||||
|
||||
result += RESET
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def cprint(
|
||||
text: object,
|
||||
color: str | tuple[int, int, int] | None = None,
|
||||
on_color: str | tuple[int, int, int] | None = None,
|
||||
attrs: Iterable[str] | None = None,
|
||||
*,
|
||||
no_color: bool | None = None,
|
||||
force_color: bool | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Print colorized text.
|
||||
|
||||
It accepts arguments of print function.
|
||||
"""
|
||||
|
||||
print(
|
||||
(
|
||||
colored(
|
||||
text,
|
||||
color,
|
||||
on_color,
|
||||
attrs,
|
||||
no_color=no_color,
|
||||
force_color=force_color,
|
||||
)
|
||||
),
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"Current terminal type: {os.getenv('TERM')}")
|
||||
print("Test basic colors:")
|
||||
cprint("Black color", "black")
|
||||
cprint("Red color", "red")
|
||||
cprint("Green color", "green")
|
||||
cprint("Yellow color", "yellow")
|
||||
cprint("Blue color", "blue")
|
||||
cprint("Magenta color", "magenta")
|
||||
cprint("Cyan color", "cyan")
|
||||
cprint("White color", "white")
|
||||
cprint("Light grey color", "light_grey")
|
||||
cprint("Dark grey color", "dark_grey")
|
||||
cprint("Light red color", "light_red")
|
||||
cprint("Light green color", "light_green")
|
||||
cprint("Light yellow color", "light_yellow")
|
||||
cprint("Light blue color", "light_blue")
|
||||
cprint("Light magenta color", "light_magenta")
|
||||
cprint("Light cyan color", "light_cyan")
|
||||
print("-" * 78)
|
||||
|
||||
print("Test highlights:")
|
||||
cprint("On black color", on_color="on_black")
|
||||
cprint("On red color", on_color="on_red")
|
||||
cprint("On green color", on_color="on_green")
|
||||
cprint("On yellow color", on_color="on_yellow")
|
||||
cprint("On blue color", on_color="on_blue")
|
||||
cprint("On magenta color", on_color="on_magenta")
|
||||
cprint("On cyan color", on_color="on_cyan")
|
||||
cprint("On white color", color="black", on_color="on_white")
|
||||
cprint("On light grey color", on_color="on_light_grey")
|
||||
cprint("On dark grey color", on_color="on_dark_grey")
|
||||
cprint("On light red color", on_color="on_light_red")
|
||||
cprint("On light green color", on_color="on_light_green")
|
||||
cprint("On light yellow color", on_color="on_light_yellow")
|
||||
cprint("On light blue color", on_color="on_light_blue")
|
||||
cprint("On light magenta color", on_color="on_light_magenta")
|
||||
cprint("On light cyan color", on_color="on_light_cyan")
|
||||
print("-" * 78)
|
||||
|
||||
print("Test attributes:")
|
||||
cprint("Bold black color", "black", attrs=["bold"])
|
||||
cprint("Dark red color", "red", attrs=["dark"])
|
||||
cprint("Italic blue color", "blue", attrs=["italic"])
|
||||
cprint("Underline green color", "green", attrs=["underline"])
|
||||
cprint("Blink yellow color", "yellow", attrs=["blink"])
|
||||
cprint("Reversed blue color", "blue", attrs=["reverse"])
|
||||
cprint("Concealed magenta color", "magenta", attrs=["concealed"])
|
||||
cprint("Strike red color", "red", attrs=["strike"])
|
||||
cprint("Bold underline reverse cyan color", "cyan", attrs=["bold", "underline", "reverse"])
|
||||
cprint("Dark blink concealed white color", "white", attrs=["dark", "blink", "concealed"])
|
||||
print("-" * 78)
|
||||
|
||||
print("Test mixing:")
|
||||
cprint("Underline red on black color", "red", "on_black", ["underline"])
|
||||
cprint("Reversed green on red color", "green", "on_red", ["reverse"])
|
||||
print("-" * 78)
|
||||
|
||||
print("Test RGB:")
|
||||
cprint("Pure red text (255, 0, 0)", (255, 0, 0))
|
||||
cprint("Default red for comparison", "red")
|
||||
cprint("Pure green text (0, 255, 0)", (0, 255, 0))
|
||||
cprint("Default green for comparison", "green")
|
||||
cprint("Pure blue text (0, 0, 255)", (0, 0, 255))
|
||||
cprint("Default blue for comparison", "blue")
|
||||
cprint("Pure yellow text (255, 255, 0)", (255, 255, 0))
|
||||
cprint("Default yellow for comparison", "yellow")
|
||||
cprint("Pure cyan text (0, 255, 255)", (0, 255, 255))
|
||||
cprint("Default cyan for comparison", "cyan")
|
||||
cprint("Pure magenta text (255, 0, 255)", (255, 0, 255))
|
||||
cprint("Default magenta for comparison", "magenta")
|
||||
cprint("Light pink (255, 182, 193)", (255, 182, 193))
|
||||
cprint("Light pink (255, 105, 180)", (255, 105, 180))
|
||||
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "chanlun"
|
||||
version = "2606.125"
|
||||
version = "2606.73"
|
||||
description = "缠论技术分析库 — Rust 高性能实现"
|
||||
readme = { file = "README.md", content-type = "text/markdown" }
|
||||
license = { file = "LICENSE", content-type = "text/plain" }
|
||||
|
||||
@@ -650,10 +650,7 @@ impl 线段Py {
|
||||
Some(&hub_ref.inner),
|
||||
)
|
||||
} else {
|
||||
return Err(pyo3::exceptions::PyTypeError::new_err(format!(
|
||||
"线段.分割序列 的 所属中枢 参数必须是 chanlun._chanlun.中枢 (Rust PyO3 对象),收到了 {}。请使用 chanlun.chan.线段.分割序列 (Python 版) 处理 Python 中枢对象。",
|
||||
hub_bound.get_type().name()?,
|
||||
)));
|
||||
chanlun::algorithm::segment::线段::分割序列(&borrowed.inner, None)
|
||||
}
|
||||
} else {
|
||||
chanlun::algorithm::segment::线段::分割序列(&borrowed.inner, None)
|
||||
|
||||
@@ -1,27 +1,3 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2026 YuYuKunKun
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
use dashmap::DashMap;
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::PySet;
|
||||
|
||||
@@ -252,11 +252,6 @@ impl 缠论配置Py {
|
||||
})
|
||||
}
|
||||
|
||||
/// 判断指定标签是否应展示。None = 全部展示,空列表 = 全部隐藏。
|
||||
fn 展示标签(&self, 标签: &str) -> bool {
|
||||
self.缓存.lock().as_ref().map_or(true, |c| c.展示标签(标签))
|
||||
}
|
||||
|
||||
#[classmethod]
|
||||
/// 将形如 "1_open", "1_close", "2_open", "name" 的字典重组为嵌套结构
|
||||
fn 按序号重组字典(
|
||||
@@ -309,55 +304,6 @@ impl 缠论配置Py {
|
||||
}
|
||||
Ok(dict.into())
|
||||
}
|
||||
|
||||
/// 统一设置所有指标参数(对应 Python 设置指标)。
|
||||
///
|
||||
/// 各参数为 None 时不修改对应字段。
|
||||
/// 调用后自动将 `计算指标` 设为 `true`。
|
||||
#[pyo3(signature = (*, 均线=None, MACD=None, RSI=None, KDJ=None, BOLL=None))]
|
||||
fn 设置指标(
|
||||
&mut self,
|
||||
py: Python<'_>,
|
||||
均线: Option<Bound<'_, PyAny>>,
|
||||
MACD: Option<Bound<'_, PyAny>>,
|
||||
RSI: Option<Bound<'_, PyAny>>,
|
||||
KDJ: Option<Bound<'_, PyAny>>,
|
||||
BOLL: Option<Bound<'_, PyAny>>,
|
||||
) -> PyResult<()> {
|
||||
self.fields.insert(
|
||||
"计算指标".into(),
|
||||
pyo3::types::PyBool::new(py, true).as_any().to_owned().unbind(),
|
||||
);
|
||||
if let Some(v) = 均线 {
|
||||
self.fields.insert("均线参数列表".into(), v.unbind());
|
||||
}
|
||||
if let Some(v) = MACD {
|
||||
self.fields.insert("MACD_参数列表".into(), v.unbind());
|
||||
}
|
||||
if let Some(v) = RSI {
|
||||
self.fields.insert("RSI_周期列表".into(), v.unbind());
|
||||
}
|
||||
if let Some(v) = KDJ {
|
||||
self.fields.insert("KDJ_参数列表".into(), v.unbind());
|
||||
}
|
||||
if let Some(v) = BOLL {
|
||||
self.fields.insert("BOLL_参数列表".into(), v.unbind());
|
||||
}
|
||||
|
||||
// 通过 serde 往返验证类型
|
||||
*self.缓存.lock() = None;
|
||||
self.版本.fetch_add(1, Ordering::Relaxed);
|
||||
match dict_to_rust_config(&self.fields) {
|
||||
Ok(config) => {
|
||||
self.fields = config_to_field_dict(&config)?;
|
||||
*self.缓存.lock() = Some(config);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(pyo3::exceptions::PyValueError::new_err(format!(
|
||||
"设置指标 转换失败: {e}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl 缠论配置Py {
|
||||
|
||||
@@ -31,7 +31,7 @@ use std::sync::atomic::Ordering;
|
||||
|
||||
use crate::config_py::缠论配置Py;
|
||||
use crate::indicators_py::{
|
||||
平滑异同移动平均线Py, 指标容器Py, 相对强弱指数Py, 随机指标Py, 布林带Py,
|
||||
平滑异同移动平均线Py, 指标容器Py, 相对强弱指数Py, 随机指标Py
|
||||
};
|
||||
use crate::structure_py::fractal_to_py;
|
||||
use crate::types_py::相对方向Py;
|
||||
@@ -171,20 +171,6 @@ impl K线Py {
|
||||
.map(|k| 随机指标Py { inner: k })
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn boll(&self) -> Option<布林带Py> {
|
||||
self.inner
|
||||
.指标
|
||||
.read()
|
||||
.boll_cloned()
|
||||
.map(|b| 布林带Py { inner: b })
|
||||
}
|
||||
|
||||
/// 读取均线值,如 `k.ma("SMA_5")` → `Optional[float]`
|
||||
fn ma(&self, key: &str) -> Option<f64> {
|
||||
self.inner.ma(key)
|
||||
}
|
||||
|
||||
/// 指标容器 — 包含所有已注册指标(MACD/RSI/KDJ/BOLL/均线/单值)
|
||||
#[getter]
|
||||
fn 指标(&self) -> 指标容器Py {
|
||||
|
||||
@@ -107,8 +107,6 @@ mod config_py;
|
||||
mod equality_py;
|
||||
mod indicators_py;
|
||||
mod kline_py;
|
||||
mod signal_engine_py;
|
||||
mod signal_py;
|
||||
mod structure_py;
|
||||
mod types_py;
|
||||
|
||||
@@ -238,8 +236,6 @@ fn _chanlun(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(wrap_pyfunction!(set_cache_mode, m)?)?;
|
||||
// 阶段 1: 枚举和基础类型
|
||||
types_py::register(m)?;
|
||||
// 阶段 1.5: 信号原语
|
||||
signal_py::register(m)?;
|
||||
// 阶段 2: 配置
|
||||
config_py::register(m)?;
|
||||
// 阶段 3: 技术指标
|
||||
@@ -252,8 +248,6 @@ fn _chanlun(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
algorithm_py::register(m)?;
|
||||
// 阶段 7: 业务
|
||||
business_py::register(m)?;
|
||||
// 阶段 7.5: 信号引擎
|
||||
signal_engine_py::register(m)?;
|
||||
// 阶段 8: 相等校验函数
|
||||
equality_py::register(m)?;
|
||||
Ok(())
|
||||
|
||||
@@ -1,281 +0,0 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2026 YuYuKunKun
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
//! PyO3 绑定:将信号引擎和 call_signal 暴露给 Python。
|
||||
//!
|
||||
//! 第三方代码声明:引擎架构参考 czsc 的 `信号计算器`
|
||||
//!(https://github.com/waditu/czsc,Apache License 2.0)。
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use chanlun::signal::engine::{self, SignalConfig, SignalEngine as 核心SignalEngine};
|
||||
|
||||
use pyo3::exceptions::{PyKeyError, PyValueError};
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::PyDict;
|
||||
|
||||
use crate::business_py::{立体分析器Py, 观察者Py};
|
||||
use crate::signal_py::{SignalPy, 时间戳转datetime};
|
||||
|
||||
// ======== 工具函数 ========
|
||||
|
||||
/// 将 PyAny 转换为 `serde_json::Value`。
|
||||
/// 尝试顺序:i64 → f64 → String → bool → 兜底转为 String。
|
||||
fn py_any_to_json_value(obj: &Bound<'_, PyAny>) -> PyResult<serde_json::Value> {
|
||||
// i64
|
||||
if let Ok(i) = obj.extract::<i64>() {
|
||||
return Ok(serde_json::Value::Number(i.into()));
|
||||
}
|
||||
// f64
|
||||
if let Ok(f) = obj.extract::<f64>() {
|
||||
if let Some(n) = serde_json::Number::from_f64(f) {
|
||||
return Ok(serde_json::Value::Number(n));
|
||||
}
|
||||
return Ok(serde_json::Value::String(f.to_string()));
|
||||
}
|
||||
// String
|
||||
if let Ok(s) = obj.extract::<String>() {
|
||||
return Ok(serde_json::Value::String(s));
|
||||
}
|
||||
// bool
|
||||
if let Ok(b) = obj.extract::<bool>() {
|
||||
return Ok(serde_json::Value::Bool(b));
|
||||
}
|
||||
// fallback: Python repr as string
|
||||
Ok(serde_json::Value::String(obj.to_string()))
|
||||
}
|
||||
|
||||
/// 将 `PyDict` 转换为 `HashMap<String, serde_json::Value>`。
|
||||
pub(crate) fn py_dict_to_params(
|
||||
dict: &Bound<'_, PyDict>,
|
||||
) -> PyResult<HashMap<String, serde_json::Value>> {
|
||||
let mut params = HashMap::new();
|
||||
for (k, v) in dict.iter() {
|
||||
let key: String = k.extract()?;
|
||||
let value = py_any_to_json_value(&v)?;
|
||||
params.insert(key, value);
|
||||
}
|
||||
Ok(params)
|
||||
}
|
||||
|
||||
// ======== 自由函数 ========
|
||||
|
||||
/// 通过 Rust 注册表按名调用单个信号函数。
|
||||
///
|
||||
/// Args:
|
||||
/// name: 注册的信号名,如 ``"youwukuncheng_中枢第三买卖点_V230602"``
|
||||
/// obs: 观察者Py 实例
|
||||
/// params: 信号参数字典(不含 name)
|
||||
///
|
||||
/// Returns:
|
||||
/// SignalPy 对象列表
|
||||
///
|
||||
/// Raises:
|
||||
/// PyValueError: 信号名未注册
|
||||
#[pyfunction]
|
||||
pub fn call_signal(
|
||||
name: &str,
|
||||
obs: &观察者Py,
|
||||
params: &Bound<'_, PyDict>,
|
||||
) -> PyResult<Vec<SignalPy>> {
|
||||
let obs_ref = obs.obs();
|
||||
let params_map = py_dict_to_params(params)?;
|
||||
|
||||
let inner =
|
||||
engine::call_signal(name, &obs_ref, ¶ms_map).map_err(|e| PyValueError::new_err(e))?;
|
||||
|
||||
Ok(inner.into_iter().map(|s| SignalPy { inner: s }).collect())
|
||||
}
|
||||
|
||||
/// 列出所有已注册的信号名(编译时 + 动态)。
|
||||
#[pyfunction]
|
||||
pub fn list_signals() -> Vec<String> {
|
||||
chanlun::signal::registry::list_signal_names()
|
||||
}
|
||||
|
||||
/// 按名获取信号参数模板(编译时 + 动态)。
|
||||
#[pyfunction]
|
||||
pub fn get_signal_template(name: &str) -> Option<String> {
|
||||
chanlun::signal::registry::get_template(name)
|
||||
}
|
||||
|
||||
// ======== 动态注册 API ========
|
||||
|
||||
/// 从动态注册表中移除信号。
|
||||
#[pyfunction]
|
||||
fn unregister_signal(name: &str) -> PyResult<()> {
|
||||
chanlun::signal::registry::unregister_signal(name).map_err(|e| PyValueError::new_err(e))
|
||||
}
|
||||
|
||||
// ======== 信号引擎 pyclass ========
|
||||
|
||||
/// Rust 信号计算引擎的 Python 绑定。
|
||||
///
|
||||
/// 用法::
|
||||
///
|
||||
/// from chanlun._chanlun import 信号引擎
|
||||
///
|
||||
/// 引擎 = 信号引擎([
|
||||
/// {"name": "youwukuncheng_中枢第三买卖点_V230602",
|
||||
/// "freq": 86400, "max_overlap": 3,
|
||||
/// "本级完整性": "实", "同级完整性": "合"},
|
||||
/// ])
|
||||
/// 引擎.自动挂载指标(分析器)
|
||||
/// 结果 = 引擎.更新(分析器) # dict[str, str]
|
||||
#[pyclass(name = "信号引擎", module = "chanlun._chanlun")]
|
||||
pub struct SignalEnginePy {
|
||||
inner: 核心SignalEngine,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl SignalEnginePy {
|
||||
/// 创建信号引擎。
|
||||
///
|
||||
/// Args:
|
||||
/// 信号配置: 信号配置字典列表,每项必须含 ``"name"`` 和 ``"freq"``。
|
||||
#[new]
|
||||
#[pyo3(signature = (信号配置=None))]
|
||||
fn new(信号配置: Option<Vec<Bound<'_, PyDict>>>) -> PyResult<Self> {
|
||||
let configs = match 信号配置 {
|
||||
Some(list) => {
|
||||
let mut configs = Vec::with_capacity(list.len());
|
||||
for d in &list {
|
||||
let name: String = d
|
||||
.get_item("name")?
|
||||
.ok_or_else(|| PyValueError::new_err("信号配置缺少 'name'"))?
|
||||
.extract()?;
|
||||
|
||||
let freq_raw = d
|
||||
.get_item("freq")?
|
||||
.ok_or_else(|| PyKeyError::new_err("信号配置缺少 'freq'"))?;
|
||||
|
||||
// freq 可以是 int 或 str
|
||||
let freq: i64 = if let Ok(i) = freq_raw.extract::<i64>() {
|
||||
i
|
||||
} else if let Ok(s) = freq_raw.extract::<String>() {
|
||||
s.parse::<i64>().map_err(|_| {
|
||||
PyValueError::new_err(format!("freq 无法解析为整数: {s}"))
|
||||
})?
|
||||
} else {
|
||||
return Err(PyValueError::new_err(format!(
|
||||
"freq 类型无效: {}",
|
||||
freq_raw.get_type().name()?
|
||||
)));
|
||||
};
|
||||
|
||||
// 构建 params(排除 "name",保留 "freq" 为字符串格式)
|
||||
let mut params = HashMap::new();
|
||||
for (k, v) in d.iter() {
|
||||
let key: String = k.extract()?;
|
||||
if key == "name" {
|
||||
continue;
|
||||
}
|
||||
if key == "freq" {
|
||||
// 统一为字符串,便于 Rust 信号函数通过 params::get_string 读取
|
||||
params.insert(key, serde_json::Value::String(freq.to_string()));
|
||||
continue;
|
||||
}
|
||||
let value = py_any_to_json_value(&v)?;
|
||||
params.insert(key, value);
|
||||
}
|
||||
|
||||
configs.push(SignalConfig {
|
||||
signal_name: name,
|
||||
freq,
|
||||
params,
|
||||
});
|
||||
}
|
||||
configs
|
||||
}
|
||||
None => Vec::new(),
|
||||
};
|
||||
Ok(Self {
|
||||
inner: 核心SignalEngine::new(configs),
|
||||
})
|
||||
}
|
||||
|
||||
/// 扫描所有配置中的 MACD / 均线关键字,为各周期 observer 的配置添加缺失的指标参数。
|
||||
fn 自动挂载指标(&self, analyzer: &立体分析器Py) {
|
||||
self.inner.自动挂载指标(&analyzer.inner);
|
||||
}
|
||||
|
||||
/// 遍历所有配置,执行信号函数,收集非空结果。
|
||||
///
|
||||
/// Returns:
|
||||
/// ``dict[str, str]`` — 信号 key → 信号 value(已过滤 "任意_任意_任意_0")
|
||||
fn 更新(&self, analyzer: &立体分析器Py) -> HashMap<String, String> {
|
||||
self.inner.更新(&analyzer.inner)
|
||||
}
|
||||
|
||||
/// 更新信号并返回完整结果(信号 + 行情)。
|
||||
/// 返回 dict: ``{"signals": {...}, "market": {...}}``,若无基础周期 K 线则 market 为 None。
|
||||
fn 更新_完整<'py>(
|
||||
&self, py: Python<'py>, analyzer: &立体分析器Py
|
||||
) -> PyResult<Py<PyAny>> {
|
||||
let result = self.inner.更新_完整(&analyzer.inner);
|
||||
let d = PyDict::new(py);
|
||||
|
||||
let signals_dict = PyDict::new(py);
|
||||
for (k, v) in &result.signals {
|
||||
signals_dict.set_item(k, v)?;
|
||||
}
|
||||
d.set_item("signals", signals_dict)?;
|
||||
|
||||
if let Some(m) = &result.market {
|
||||
let md = PyDict::new(py);
|
||||
md.set_item("symbol", &m.symbol)?;
|
||||
md.set_item("dt", 时间戳转datetime(py, m.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.into())
|
||||
}
|
||||
|
||||
/// 返回配置数量
|
||||
fn __len__(&self) -> usize {
|
||||
self.inner.len()
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!("信号引擎(configs={})", self.inner.len())
|
||||
}
|
||||
}
|
||||
|
||||
/// 注册模块。
|
||||
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<SignalEnginePy>()?;
|
||||
m.add_function(wrap_pyfunction!(call_signal, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(list_signals, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(get_signal_template, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(unregister_signal, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,795 +0,0 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2026 YuYuKunKun
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
use pyo3::exceptions::PyValueError;
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::PyDict;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use chanlun::signal::event::Event as 核心Event;
|
||||
use chanlun::signal::factor::Factor as 核心Factor;
|
||||
use chanlun::signal::operate::Operate as 核心Operate;
|
||||
use chanlun::signal::position::Position as 核心Position;
|
||||
use chanlun::signal::signal::Signal as 核心Signal;
|
||||
use chanlun::signal::{信号字典, 匹配值};
|
||||
|
||||
/// Operate 枚举绑定。
|
||||
#[pyclass(name = "Operate", module = "chanlun._chanlun", eq, eq_int)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum OperatePy {
|
||||
HL,
|
||||
HS,
|
||||
HO,
|
||||
LO,
|
||||
LE,
|
||||
SO,
|
||||
SE,
|
||||
}
|
||||
|
||||
impl OperatePy {
|
||||
pub(crate) fn 转核心(self) -> 核心Operate {
|
||||
match self {
|
||||
OperatePy::HL => 核心Operate::持多,
|
||||
OperatePy::HS => 核心Operate::持空,
|
||||
OperatePy::HO => 核心Operate::持币,
|
||||
OperatePy::LO => 核心Operate::开多,
|
||||
OperatePy::LE => 核心Operate::平多,
|
||||
OperatePy::SO => 核心Operate::开空,
|
||||
OperatePy::SE => 核心Operate::平空,
|
||||
}
|
||||
}
|
||||
pub(crate) fn 从核心(o: 核心Operate) -> Self {
|
||||
match o {
|
||||
核心Operate::持多 => OperatePy::HL,
|
||||
核心Operate::持空 => OperatePy::HS,
|
||||
核心Operate::持币 => OperatePy::HO,
|
||||
核心Operate::开多 => OperatePy::LO,
|
||||
核心Operate::平多 => OperatePy::LE,
|
||||
核心Operate::开空 => OperatePy::SO,
|
||||
核心Operate::平空 => OperatePy::SE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl OperatePy {
|
||||
#[getter]
|
||||
fn value(&self) -> &'static str {
|
||||
self.转核心().value()
|
||||
}
|
||||
fn __str__(&self) -> &'static str {
|
||||
self.转核心().value()
|
||||
}
|
||||
fn __repr__(&self) -> String {
|
||||
format!("Operate.{:?}", self)
|
||||
}
|
||||
/// 从中文值还原 Operate(供 Event.load 反序列化用,对应旧 Python `Operate("开多")`)。
|
||||
#[staticmethod]
|
||||
fn from_value(value: &str) -> PyResult<OperatePy> {
|
||||
match value {
|
||||
"持多" => Ok(OperatePy::HL),
|
||||
"持空" => Ok(OperatePy::HS),
|
||||
"持币" => Ok(OperatePy::HO),
|
||||
"开多" => Ok(OperatePy::LO),
|
||||
"平多" => Ok(OperatePy::LE),
|
||||
"开空" => Ok(OperatePy::SO),
|
||||
"平空" => Ok(OperatePy::SE),
|
||||
_ => Err(PyValueError::new_err(format!("未知 Operate 值: {value}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 把 PyDict 转成核心层信号字典:str 值 → 字符串,其余 → 非字符串。
|
||||
pub(crate) fn 字典转核心(s: &Bound<'_, PyDict>) -> PyResult<信号字典> {
|
||||
let mut out: 信号字典 = HashMap::new();
|
||||
for (k, v) in s.iter() {
|
||||
let key: String = k.extract()?;
|
||||
let 值 = match v.extract::<String>() {
|
||||
Ok(文本) if !文本.is_empty() => 匹配值::字符串(文本),
|
||||
_ => 匹配值::非字符串,
|
||||
};
|
||||
out.insert(key, 值);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// 反序列化辅助:从 dict 取字符串字段,缺省返回空串。
|
||||
fn 取字符串(raw: &Bound<'_, PyDict>, key: &str) -> PyResult<String> {
|
||||
match raw.get_item(key)? {
|
||||
Some(v) => v.extract(),
|
||||
None => Ok(String::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 从七段字符串解析 Signal(格式: k1_k2_k3_v1_v2_v3_score)。
|
||||
fn parse_signal_str(s: &str) -> PyResult<核心Signal> {
|
||||
let parts: Vec<&str> = s.split('_').collect();
|
||||
if parts.len() != 7 {
|
||||
return Err(PyValueError::new_err(format!(
|
||||
"Signal 格式无效:应为 k1_k2_k3_v1_v2_v3_score(7段),收到 {s}"
|
||||
)));
|
||||
}
|
||||
let score: i32 = parts[6]
|
||||
.parse()
|
||||
.map_err(|_| PyValueError::new_err(format!("无法解析 score: {}", parts[6])))?;
|
||||
Ok(核心Signal::new(
|
||||
parts[0], parts[1], parts[2], parts[3], parts[4], parts[5], score,
|
||||
))
|
||||
}
|
||||
|
||||
/// 反序列化辅助:从 dict 取信号串列表,逐个解析为核心 Signal。
|
||||
fn 取信号列表(raw: &Bound<'_, PyDict>, key: &str) -> PyResult<Vec<核心Signal>> {
|
||||
let mut out = Vec::new();
|
||||
if let Some(item) = raw.get_item(key)? {
|
||||
let strs: Vec<String> = item.extract()?;
|
||||
for s in strs {
|
||||
out.push(parse_signal_str(&s)?);
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// 反序列化辅助:从 dict 取事件列表,逐个调用 Event.load。
|
||||
fn 取事件列表(raw: &Bound<'_, PyDict>, key: &str) -> PyResult<Vec<核心Event>> {
|
||||
let mut out = Vec::new();
|
||||
if let Some(item) = raw.get_item(key)? {
|
||||
let dicts: Vec<Bound<'_, PyDict>> = item.extract()?;
|
||||
for d in &dicts {
|
||||
out.push(EventPy::load(d)?.inner);
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Signal 绑定。
|
||||
#[pyclass(name = "Signal", module = "chanlun._chanlun")]
|
||||
#[derive(Clone)]
|
||||
pub struct SignalPy {
|
||||
pub(crate) inner: 核心Signal,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl SignalPy {
|
||||
#[new]
|
||||
#[pyo3(signature = (signal=String::new(), score=0, k1="任意".to_string(), k2="任意".to_string(), k3="任意".to_string(), v1="任意".to_string(), v2="任意".to_string(), v3="任意".to_string()))]
|
||||
fn new(
|
||||
signal: String,
|
||||
score: i32,
|
||||
k1: String,
|
||||
k2: String,
|
||||
k3: String,
|
||||
v1: String,
|
||||
v2: String,
|
||||
v3: String,
|
||||
) -> PyResult<Self> {
|
||||
let inner = if signal.is_empty() {
|
||||
核心Signal::new(&k1, &k2, &k3, &v1, &v2, &v3, score)
|
||||
} else {
|
||||
parse_signal_str(&signal)?
|
||||
};
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn signal(&self) -> String {
|
||||
self.inner.signal.clone()
|
||||
}
|
||||
#[getter]
|
||||
fn score(&self) -> i32 {
|
||||
self.inner.score
|
||||
}
|
||||
#[getter]
|
||||
fn k1(&self) -> String {
|
||||
self.inner.k1.clone()
|
||||
}
|
||||
#[getter]
|
||||
fn k2(&self) -> String {
|
||||
self.inner.k2.clone()
|
||||
}
|
||||
#[getter]
|
||||
fn k3(&self) -> String {
|
||||
self.inner.k3.clone()
|
||||
}
|
||||
#[getter]
|
||||
fn v1(&self) -> String {
|
||||
self.inner.v1.clone()
|
||||
}
|
||||
#[getter]
|
||||
fn v2(&self) -> String {
|
||||
self.inner.v2.clone()
|
||||
}
|
||||
#[getter]
|
||||
fn v3(&self) -> String {
|
||||
self.inner.v3.clone()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn key(&self) -> String {
|
||||
self.inner.key()
|
||||
}
|
||||
#[getter]
|
||||
fn value(&self) -> String {
|
||||
self.inner.value()
|
||||
}
|
||||
|
||||
fn is_match(&self, s: &Bound<'_, PyDict>) -> PyResult<bool> {
|
||||
let 字典 = 字典转核心(s)?;
|
||||
self.inner
|
||||
.is_match(&字典)
|
||||
.map_err(|e| PyValueError::new_err(format!("{} 不在信号列表中", e.0)))
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!("Signal('{}')", self.inner.signal)
|
||||
}
|
||||
}
|
||||
|
||||
/// Factor 绑定。signals_all 全满足 + signals_any 任一满足 + signals_not 全不满足。
|
||||
#[pyclass(name = "Factor", module = "chanlun._chanlun")]
|
||||
#[derive(Clone)]
|
||||
pub struct FactorPy {
|
||||
pub(crate) inner: 核心Factor,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl FactorPy {
|
||||
#[new]
|
||||
#[pyo3(signature = (signals_all, signals_any=Vec::new(), signals_not=Vec::new(), name=String::new()))]
|
||||
fn new(
|
||||
signals_all: Vec<SignalPy>,
|
||||
signals_any: Vec<SignalPy>,
|
||||
signals_not: Vec<SignalPy>,
|
||||
name: String,
|
||||
) -> PyResult<Self> {
|
||||
let 取 = |v: Vec<SignalPy>| v.into_iter().map(|s| s.inner).collect::<Vec<_>>();
|
||||
let inner = 核心Factor::新建(取(signals_all), 取(signals_any), 取(signals_not), name)
|
||||
.map_err(PyValueError::new_err)?;
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn name(&self) -> String {
|
||||
self.inner.name.clone()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn signals_all(&self) -> Vec<SignalPy> {
|
||||
self.inner
|
||||
.signals_all
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|inner| SignalPy { inner })
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn signals_any(&self) -> Vec<SignalPy> {
|
||||
self.inner
|
||||
.signals_any
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|inner| SignalPy { inner })
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn signals_not(&self) -> Vec<SignalPy> {
|
||||
self.inner
|
||||
.signals_not
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|inner| SignalPy { inner })
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn unique_signals(&self) -> Vec<String> {
|
||||
self.inner.unique_signals()
|
||||
}
|
||||
|
||||
fn is_match(&self, s: &Bound<'_, PyDict>) -> PyResult<bool> {
|
||||
let 字典 = 字典转核心(s)?;
|
||||
self.inner
|
||||
.is_match(&字典)
|
||||
.map_err(|e| PyValueError::new_err(format!("{} 不在信号列表中", e.0)))
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!("Factor('{}')", self.inner.name)
|
||||
}
|
||||
|
||||
/// 序列化为 dict:{name, signals_all, signals_any, signals_not}(signals 存为信号串)。
|
||||
fn dump<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
|
||||
let d = PyDict::new(py);
|
||||
let 串 = |v: &[核心Signal]| v.iter().map(|s| s.signal.clone()).collect::<Vec<_>>();
|
||||
d.set_item("name", &self.inner.name)?;
|
||||
d.set_item("signals_all", 串(&self.inner.signals_all))?;
|
||||
d.set_item("signals_any", 串(&self.inner.signals_any))?;
|
||||
d.set_item("signals_not", 串(&self.inner.signals_not))?;
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
/// 从 dict 反序列化(对应旧 Python Factor.load)。
|
||||
#[staticmethod]
|
||||
fn load(raw: &Bound<'_, PyDict>) -> PyResult<FactorPy> {
|
||||
let inner = 核心Factor::新建(
|
||||
取信号列表(raw, "signals_all")?,
|
||||
取信号列表(raw, "signals_any")?,
|
||||
取信号列表(raw, "signals_not")?,
|
||||
取字符串(raw, "name")?,
|
||||
)
|
||||
.map_err(PyValueError::new_err)?;
|
||||
Ok(FactorPy { inner })
|
||||
}
|
||||
}
|
||||
|
||||
/// Event 绑定。operate + 因子列表(任一因子满足则事件为真)。
|
||||
#[pyclass(name = "Event", module = "chanlun._chanlun")]
|
||||
#[derive(Clone)]
|
||||
pub struct EventPy {
|
||||
pub(crate) inner: 核心Event,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl EventPy {
|
||||
#[new]
|
||||
#[pyo3(signature = (operate, factors, signals_all=Vec::new(), signals_any=Vec::new(), signals_not=Vec::new(), name=String::new()))]
|
||||
fn new(
|
||||
operate: OperatePy,
|
||||
factors: Vec<FactorPy>,
|
||||
signals_all: Vec<SignalPy>,
|
||||
signals_any: Vec<SignalPy>,
|
||||
signals_not: Vec<SignalPy>,
|
||||
name: String,
|
||||
) -> PyResult<Self> {
|
||||
let 取s = |v: Vec<SignalPy>| v.into_iter().map(|s| s.inner).collect::<Vec<_>>();
|
||||
let 取f = |v: Vec<FactorPy>| v.into_iter().map(|f| f.inner).collect::<Vec<_>>();
|
||||
let inner = 核心Event::新建(
|
||||
operate.转核心(),
|
||||
取f(factors),
|
||||
取s(signals_all),
|
||||
取s(signals_any),
|
||||
取s(signals_not),
|
||||
name,
|
||||
)
|
||||
.map_err(PyValueError::new_err)?;
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn name(&self) -> String {
|
||||
self.inner.name.clone()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn sha256(&self) -> String {
|
||||
self.inner.sha256.clone()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn operate(&self) -> OperatePy {
|
||||
OperatePy::从核心(self.inner.operate)
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn factors(&self) -> Vec<FactorPy> {
|
||||
self.inner
|
||||
.factors
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|inner| FactorPy { inner })
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn unique_signals(&self) -> Vec<String> {
|
||||
self.inner.unique_signals()
|
||||
}
|
||||
|
||||
fn is_match(&self, s: &Bound<'_, PyDict>) -> PyResult<(bool, Option<String>)> {
|
||||
let 字典 = 字典转核心(s)?;
|
||||
self.inner
|
||||
.is_match(&字典)
|
||||
.map_err(|e| PyValueError::new_err(format!("{} 不在信号列表中", e.0)))
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!("Event('{}')", self.inner.name)
|
||||
}
|
||||
|
||||
/// 序列化为 dict:{name, operate, signals_all/any/not, factors}。
|
||||
fn dump<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
|
||||
let d = PyDict::new(py);
|
||||
let 串 = |v: &[核心Signal]| v.iter().map(|s| s.signal.clone()).collect::<Vec<_>>();
|
||||
d.set_item("name", &self.inner.name)?;
|
||||
d.set_item("operate", self.inner.operate.value())?;
|
||||
d.set_item("signals_all", 串(&self.inner.signals_all))?;
|
||||
d.set_item("signals_any", 串(&self.inner.signals_any))?;
|
||||
d.set_item("signals_not", 串(&self.inner.signals_not))?;
|
||||
let factors: Vec<Bound<'py, PyDict>> = self
|
||||
.inner
|
||||
.factors
|
||||
.iter()
|
||||
.map(|f| FactorPy { inner: f.clone() }.dump(py))
|
||||
.collect::<PyResult<_>>()?;
|
||||
d.set_item("factors", factors)?;
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
/// 从 dict 反序列化(对应旧 Python Event.load)。
|
||||
#[staticmethod]
|
||||
fn load(raw: &Bound<'_, PyDict>) -> PyResult<EventPy> {
|
||||
let operate = OperatePy::from_value(&取字符串(raw, "operate")?)?.转核心();
|
||||
let mut factors = Vec::new();
|
||||
if let Some(item) = raw.get_item("factors")? {
|
||||
let dicts: Vec<Bound<'_, PyDict>> = item.extract()?;
|
||||
for fd in dicts {
|
||||
factors.push(FactorPy::load(&fd)?.inner);
|
||||
}
|
||||
}
|
||||
let inner = 核心Event::新建(
|
||||
operate,
|
||||
factors,
|
||||
取信号列表(raw, "signals_all")?,
|
||||
取信号列表(raw, "signals_any")?,
|
||||
取信号列表(raw, "signals_not")?,
|
||||
取字符串(raw, "name")?,
|
||||
)
|
||||
.map_err(PyValueError::new_err)?;
|
||||
Ok(EventPy { inner })
|
||||
}
|
||||
}
|
||||
|
||||
/// Position 绑定(可子类化)。Python 子类应实现 update() 状态机。
|
||||
#[pyclass(name = "Position", module = "chanlun._chanlun", subclass)]
|
||||
#[derive(Clone)]
|
||||
pub struct PositionPy {
|
||||
pub(crate) inner: 核心Position,
|
||||
}
|
||||
|
||||
/// 核心 Operate → PyO3 OperatePy 枚举变体映射。
|
||||
fn 核心op转pyop(op: 核心Operate) -> OperatePy {
|
||||
match op {
|
||||
核心Operate::持多 => OperatePy::HL,
|
||||
核心Operate::持空 => OperatePy::HS,
|
||||
核心Operate::持币 => OperatePy::HO,
|
||||
核心Operate::开多 => OperatePy::LO,
|
||||
核心Operate::平多 => OperatePy::LE,
|
||||
核心Operate::开空 => OperatePy::SO,
|
||||
核心Operate::平空 => OperatePy::SE,
|
||||
}
|
||||
}
|
||||
|
||||
/// 将 i64 Unix 时间戳转为 Python datetime(UTC)。
|
||||
pub(crate) fn 时间戳转datetime(py: Python<'_>, ts: i64) -> PyResult<Py<PyAny>> {
|
||||
let datetime_mod = py.import("datetime")?;
|
||||
let tz = datetime_mod.getattr("timezone")?.getattr("utc")?;
|
||||
let dt = datetime_mod
|
||||
.getattr("datetime")?
|
||||
.call_method1("fromtimestamp", (ts as f64, tz))?;
|
||||
Ok(dt.into())
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PositionPy {
|
||||
#[new]
|
||||
#[pyo3(signature = (symbol, opens, exits=Vec::new(), interval=0, timeout=1000, stop_loss=1000, T0=false, name=String::new()))]
|
||||
fn new(
|
||||
symbol: String,
|
||||
opens: Vec<EventPy>,
|
||||
exits: Vec<EventPy>,
|
||||
interval: i64,
|
||||
timeout: i64,
|
||||
stop_loss: i64,
|
||||
T0: bool,
|
||||
name: String,
|
||||
) -> PyResult<Self> {
|
||||
let 取 = |v: Vec<EventPy>| v.into_iter().map(|e| e.inner).collect::<Vec<_>>();
|
||||
let inner = 核心Position::新建(
|
||||
symbol,
|
||||
取(opens),
|
||||
取(exits),
|
||||
interval,
|
||||
timeout,
|
||||
stop_loss,
|
||||
T0,
|
||||
name,
|
||||
)
|
||||
.map_err(PyValueError::new_err)?;
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
// --- 配置 getter(不变)---
|
||||
#[getter]
|
||||
fn symbol(&self) -> String {
|
||||
self.inner.symbol.clone()
|
||||
}
|
||||
#[getter]
|
||||
fn name(&self) -> String {
|
||||
self.inner.name.clone()
|
||||
}
|
||||
#[getter]
|
||||
fn opens(&self) -> Vec<EventPy> {
|
||||
self.inner
|
||||
.opens
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|inner| EventPy { inner })
|
||||
.collect()
|
||||
}
|
||||
#[getter]
|
||||
fn exits(&self) -> Vec<EventPy> {
|
||||
self.inner
|
||||
.exits
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|inner| EventPy { inner })
|
||||
.collect()
|
||||
}
|
||||
#[getter]
|
||||
fn events(&self) -> Vec<EventPy> {
|
||||
self.inner
|
||||
.events
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|inner| EventPy { inner })
|
||||
.collect()
|
||||
}
|
||||
#[getter]
|
||||
fn interval(&self) -> i64 {
|
||||
self.inner.interval
|
||||
}
|
||||
#[getter]
|
||||
fn timeout(&self) -> i64 {
|
||||
self.inner.timeout
|
||||
}
|
||||
#[getter]
|
||||
fn stop_loss(&self) -> i64 {
|
||||
self.inner.stop_loss
|
||||
}
|
||||
#[getter]
|
||||
fn T0(&self) -> bool {
|
||||
self.inner.T0
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn unique_signals(&self) -> Vec<String> {
|
||||
self.inner.unique_signals()
|
||||
}
|
||||
|
||||
// --- 状态 getter(新增)---
|
||||
#[getter]
|
||||
fn pos(&self) -> i32 {
|
||||
self.inner.pos
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn pos_changed(&self) -> bool {
|
||||
self.inner.pos_changed
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn operates<'py>(&self, py: Python<'py>) -> PyResult<Vec<Bound<'py, PyDict>>> {
|
||||
self.inner
|
||||
.operates
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let d = PyDict::new(py);
|
||||
d.set_item("symbol", &r.symbol)?;
|
||||
d.set_item("dt", 时间戳转datetime(py, r.dt)?)?;
|
||||
d.set_item("bid", r.bid)?;
|
||||
d.set_item("price", r.price)?;
|
||||
d.set_item("op", 核心op转pyop(r.op))?;
|
||||
d.set_item("op_desc", &r.op_desc)?;
|
||||
d.set_item("pos", r.pos)?;
|
||||
Ok(d)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn holds<'py>(&self, py: Python<'py>) -> PyResult<Vec<Bound<'py, PyDict>>> {
|
||||
self.inner
|
||||
.holds
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let d = PyDict::new(py);
|
||||
d.set_item("dt", 时间戳转datetime(py, r.dt)?)?;
|
||||
d.set_item("pos", r.pos)?;
|
||||
d.set_item("price", r.price)?;
|
||||
Ok(d)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn pairs<'py>(&self, py: Python<'py>) -> PyResult<Vec<Bound<'py, PyDict>>> {
|
||||
self.inner
|
||||
.pairs()
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let d = PyDict::new(py);
|
||||
d.set_item("标的代码", &r.标的代码)?;
|
||||
d.set_item("策略标记", &r.策略标记)?;
|
||||
d.set_item("交易方向", &r.交易方向)?;
|
||||
d.set_item("开仓时间", 时间戳转datetime(py, r.开仓时间)?)?;
|
||||
d.set_item("平仓时间", 时间戳转datetime(py, r.平仓时间)?)?;
|
||||
d.set_item("开仓价格", r.开仓价格)?;
|
||||
d.set_item("平仓价格", r.平仓价格)?;
|
||||
d.set_item("持仓K线数", r.持仓K线数)?;
|
||||
d.set_item("事件序列", &r.事件序列)?;
|
||||
d.set_item("持仓天数", r.持仓天数)?;
|
||||
d.set_item("盈亏比例", r.盈亏比例)?;
|
||||
Ok(d)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 更新持仓状态。接收一个信号字典(含 OHLCV 字段 + 信号键)。
|
||||
///
|
||||
/// 信号字典必须包含:``dt``(datetime 或 Unix 时间戳), ``close``(收盘价)。
|
||||
/// 可选:``id`` 或 ``bid``(K线序号)。
|
||||
#[pyo3(signature = (信号字典))]
|
||||
fn update(&mut self, 信号字典: &Bound<'_, PyDict>) -> PyResult<()> {
|
||||
// 1. 提取 dt(支持 datetime 对象和 int/float Unix 时间戳)
|
||||
let dt: i64 = match 信号字典.get_item("dt")? {
|
||||
Some(v) => {
|
||||
// 尝试 i64
|
||||
if let Ok(ts) = v.extract::<i64>() {
|
||||
ts
|
||||
// 尝试 f64
|
||||
} else if let Ok(ts) = v.extract::<f64>() {
|
||||
ts as i64
|
||||
// 尝试 datetime.timestamp()
|
||||
} else if let Ok(ts) = v.call_method0("timestamp") {
|
||||
(ts.extract::<f64>()?) as i64
|
||||
} else {
|
||||
return Err(PyValueError::new_err(
|
||||
"无法从信号字典中提取 dt 字段(需要 datetime 或 Unix 时间戳)",
|
||||
));
|
||||
}
|
||||
}
|
||||
None => return Err(PyValueError::new_err("信号字典缺少 dt 字段")),
|
||||
};
|
||||
|
||||
// 2. 提取 price
|
||||
let price: f64 = 信号字典
|
||||
.get_item("close")?
|
||||
.and_then(|v| v.extract::<f64>().ok())
|
||||
.ok_or_else(|| PyValueError::new_err("信号字典缺少 close 字段"))?;
|
||||
|
||||
// 3. 提取 bid(可选)
|
||||
let bid: i64 = 信号字典
|
||||
.get_item("id")?
|
||||
.or_else(|| 信号字典.get_item("bid").ok().flatten())
|
||||
.and_then(|v| v.extract::<i64>().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
// 4. 转换为信号字典(排除 OHLCV 键)
|
||||
let ohkcv_keys: std::collections::HashSet<&str> = [
|
||||
"symbol", "dt", "open", "high", "low", "close", "vol", "id", "bid",
|
||||
]
|
||||
.iter()
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
let mut signals: 信号字典 = HashMap::new();
|
||||
for (k, v) in 信号字典.iter() {
|
||||
let key: String = k.extract()?;
|
||||
if ohkcv_keys.contains(key.as_str()) {
|
||||
continue;
|
||||
}
|
||||
let 值 = match v.extract::<String>() {
|
||||
Ok(文本) if !文本.is_empty() => 匹配值::字符串(文本),
|
||||
_ => 匹配值::非字符串,
|
||||
};
|
||||
signals.insert(key, 值);
|
||||
}
|
||||
|
||||
// 5. 调用核心状态机
|
||||
self.inner
|
||||
.update(dt, price, bid, &signals)
|
||||
.map_err(|e| PyValueError::new_err(format!("{} 不在信号列表中", e.0)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!(
|
||||
"Position(name={}, symbol={}, timeout={}, stop_loss={}BP, T0={}, interval={}s, pos={})",
|
||||
self.inner.name,
|
||||
self.inner.symbol,
|
||||
self.inner.timeout,
|
||||
self.inner.stop_loss,
|
||||
self.inner.T0,
|
||||
self.inner.interval,
|
||||
self.inner.pos
|
||||
)
|
||||
}
|
||||
|
||||
/// 序列化为 dict。
|
||||
/// `with_data=True` 时附带 state(pairs, holds);`with_data=False` 时仅配置。
|
||||
#[pyo3(signature = (with_data=false))]
|
||||
fn dump<'py>(&self, py: Python<'py>, with_data: bool) -> PyResult<Bound<'py, PyDict>> {
|
||||
let d = PyDict::new(py);
|
||||
d.set_item("symbol", &self.inner.symbol)?;
|
||||
d.set_item("name", &self.inner.name)?;
|
||||
let 事件dump = |evts: &[核心Event]| -> PyResult<Vec<Bound<'py, PyDict>>> {
|
||||
evts.iter()
|
||||
.map(|e| EventPy { inner: e.clone() }.dump(py))
|
||||
.collect()
|
||||
};
|
||||
d.set_item("opens", 事件dump(&self.inner.opens)?)?;
|
||||
d.set_item("exits", 事件dump(&self.inner.exits)?)?;
|
||||
d.set_item("interval", self.inner.interval)?;
|
||||
d.set_item("timeout", self.inner.timeout)?;
|
||||
d.set_item("stop_loss", self.inner.stop_loss)?;
|
||||
d.set_item("T0", self.inner.T0)?;
|
||||
if with_data {
|
||||
d.set_item("pairs", self.pairs(py)?)?;
|
||||
d.set_item("holds", self.holds(py)?)?;
|
||||
}
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
/// 从 dict 反序列化(仅配置,状态字段初始化为默认值)。
|
||||
#[staticmethod]
|
||||
fn load(raw: &Bound<'_, PyDict>) -> PyResult<PositionPy> {
|
||||
let symbol = 取字符串(raw, "symbol")?;
|
||||
let name = 取字符串(raw, "name")?;
|
||||
let interval: i64 = raw
|
||||
.get_item("interval")?
|
||||
.and_then(|v| v.extract().ok())
|
||||
.unwrap_or(0);
|
||||
let timeout: i64 = raw
|
||||
.get_item("timeout")?
|
||||
.and_then(|v| v.extract().ok())
|
||||
.unwrap_or(1000);
|
||||
let stop_loss: i64 = raw
|
||||
.get_item("stop_loss")?
|
||||
.and_then(|v| v.extract().ok())
|
||||
.unwrap_or(1000);
|
||||
let T0: bool = raw
|
||||
.get_item("T0")?
|
||||
.and_then(|v| v.extract().ok())
|
||||
.unwrap_or(false);
|
||||
|
||||
let opens = 取事件列表(raw, "opens")?;
|
||||
let exits = 取事件列表(raw, "exits")?;
|
||||
|
||||
let inner =
|
||||
核心Position::新建(symbol, opens, exits, interval, timeout, stop_loss, T0, name)
|
||||
.map_err(PyValueError::new_err)?;
|
||||
Ok(Self { inner })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<OperatePy>()?;
|
||||
m.add_class::<SignalPy>()?;
|
||||
m.add_class::<FactorPy>()?;
|
||||
m.add_class::<EventPy>()?;
|
||||
m.add_class::<PositionPy>()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1693,8 +1693,8 @@ class Test指标挂载(unittest.TestCase):
|
||||
for i in range(100):
|
||||
if i == 50:
|
||||
obs.配置.MACD_参数列表 = [
|
||||
("macd", "收", 12, 26, 9),
|
||||
("macd_10_20_7", "收", 10, 20, 7),
|
||||
("macd", 12, 26, 9),
|
||||
("macd_10_20_7", 10, 20, 7),
|
||||
]
|
||||
obs.增加原始K线(self._make_k(i))
|
||||
|
||||
@@ -1708,9 +1708,9 @@ class Test指标挂载(unittest.TestCase):
|
||||
|
||||
for i in range(80):
|
||||
if i == 40:
|
||||
obs.配置.MACD_参数列表 = [("macd", "收", 12, 26, 9), ("macd_fast", "收", 5, 13, 5)]
|
||||
obs.配置.RSI_周期列表 = [("rsi", "收", 14, 13, 75.0, 25.0), ("rsi_7", "收", 7, 6, 75.0, 25.0)]
|
||||
obs.配置.KDJ_参数列表 = [("kdj", "收", 9, 3, 3, 80.0, 20.0), ("kdj_5", "收", 5, 2, 2, 80.0, 20.0)]
|
||||
obs.配置.MACD_参数列表 = [("macd", 12, 26, 9), ("macd_fast", 5, 13, 5)]
|
||||
obs.配置.RSI_周期列表 = [("rsi", 14), ("rsi_7", 7)]
|
||||
obs.配置.KDJ_参数列表 = [("kdj", 9, 3, 3), ("kdj_5", 5, 2, 2)]
|
||||
obs.增加原始K线(self._make_k(i))
|
||||
|
||||
for k in obs.普通K线序列:
|
||||
@@ -1721,7 +1721,7 @@ class Test指标挂载(unittest.TestCase):
|
||||
def test_回填后增量计算一致(self):
|
||||
"""回填后的指标值应与从头计算一致."""
|
||||
cfg_full = chanlun.缠论配置()
|
||||
cfg_full.MACD_参数列表 = [("macd", "收", 12, 26, 9), ("macd_extra", "收", 8, 16, 6)]
|
||||
cfg_full.MACD_参数列表 = [("macd", 12, 26, 9), ("macd_extra", 8, 16, 6)]
|
||||
obs_full = chanlun.观察者("btcusd", 300, cfg_full)
|
||||
|
||||
cfg_late = chanlun.缠论配置()
|
||||
@@ -1729,7 +1729,7 @@ class Test指标挂载(unittest.TestCase):
|
||||
|
||||
for i in range(100):
|
||||
if i == 50:
|
||||
obs_late.配置.MACD_参数列表 = [("macd", "收", 12, 26, 9), ("macd_extra", "收", 8, 16, 6)]
|
||||
obs_late.配置.MACD_参数列表 = [("macd", 12, 26, 9), ("macd_extra", 8, 16, 6)]
|
||||
obs_full.增加原始K线(self._make_k(i))
|
||||
obs_late.增加原始K线(self._make_k(i))
|
||||
|
||||
@@ -2542,21 +2542,13 @@ class Test缠论配置双端一致(unittest.TestCase):
|
||||
d_rs = cfg_rs.to_dict()
|
||||
d_py = cfg_py.to_dict()
|
||||
|
||||
# Rust (serde_json) 产 list-of-list,Python 产 list-of-tuple,统一为 list 比较
|
||||
def _normalize(v):
|
||||
if isinstance(v, list):
|
||||
return [_normalize(x) for x in v]
|
||||
if isinstance(v, tuple):
|
||||
return [_normalize(x) for x in v]
|
||||
return v
|
||||
|
||||
mismatches = []
|
||||
for k in d_rs:
|
||||
v_rs = d_rs[k]
|
||||
v_py = d_py.get(k)
|
||||
if v_rs is None and v_py is None:
|
||||
continue
|
||||
if _normalize(v_rs) != _normalize(v_py):
|
||||
if v_rs != v_py:
|
||||
mismatches.append(f" {k}: R={v_rs!r} P={v_py!r}")
|
||||
self.assertEqual(len(mismatches), 0, f"to_dict 值不一致 ({len(mismatches)}处):\n" + "\n".join(mismatches[:10]))
|
||||
|
||||
@@ -2633,13 +2625,15 @@ class Test缠论配置双端一致(unittest.TestCase):
|
||||
from chanlun import chan
|
||||
|
||||
cfg_rs, cfg_py = self._make_configs()
|
||||
update = {"标识": "copied", "买卖点偏移": 5, "笔内元素数量": 10}
|
||||
update = {"标识": "copied", "推送K线": False, "笔内元素数量": 10}
|
||||
|
||||
copy_rs = cfg_rs.model_copy(update)
|
||||
copy_py = cfg_py.model_copy(update)
|
||||
|
||||
self.assertEqual(copy_rs.标识, copy_py.标识)
|
||||
self.assertEqual(copy_rs.笔内元素数量, copy_py.笔内元素数量)
|
||||
self.assertFalse(copy_rs.推送K线)
|
||||
self.assertFalse(copy_py.推送K线)
|
||||
# 未更新字段保持原值一致
|
||||
self.assertEqual(copy_rs.买卖点偏移, copy_py.买卖点偏移)
|
||||
|
||||
@@ -2666,6 +2660,8 @@ class Test缠论配置双端一致(unittest.TestCase):
|
||||
cfg_rs = chanlun.缠论配置.不推送()
|
||||
cfg_py = chan.缠论配置.不推送()
|
||||
|
||||
self.assertFalse(cfg_rs.推送K线)
|
||||
self.assertFalse(cfg_py.推送K线)
|
||||
self.assertFalse(cfg_rs.图表展示)
|
||||
self.assertFalse(cfg_py.图表展示)
|
||||
self.assertEqual(cfg_rs.笔内元素数量, cfg_py.笔内元素数量)
|
||||
|
||||
@@ -1,301 +0,0 @@
|
||||
"""Position.update() 集成测试 — 验证 Rust 核心状态机与 Python 行为一致。
|
||||
|
||||
测试覆盖:
|
||||
- 基础开多/开空/平多/平空
|
||||
- 间隔限制
|
||||
- 止损/超时
|
||||
- pairs 盈亏计算
|
||||
- 时间倒退容错
|
||||
- 空信号字典容错
|
||||
- dump/load 含状态
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from datetime import datetime, timezone
|
||||
from chanlun._chanlun import Position, Event, Factor, Signal, Operate
|
||||
|
||||
|
||||
# ---- 辅助函数 ----
|
||||
|
||||
|
||||
def 开多事件(k3="中枢", v2="三买"):
|
||||
s = Signal(k1="14400", k2="D1MO3", k3=k3, v2=v2)
|
||||
return Event(Operate.LO, [Factor(signals_all=[s])])
|
||||
|
||||
|
||||
def 平多事件(k3="中枢", v2="三卖"):
|
||||
s = Signal(k1="14400", k2="D1MO3", k3=k3, v2=v2)
|
||||
return Event(Operate.LE, [Factor(signals_all=[s])])
|
||||
|
||||
|
||||
def 开空事件(k3="中枢", v2="三卖"):
|
||||
s = Signal(k1="14400", k2="D1MO3", k3=k3, v2=v2)
|
||||
return Event(Operate.SO, [Factor(signals_all=[s])])
|
||||
|
||||
|
||||
def 平空事件(k3="中枢", v2="三买"):
|
||||
s = Signal(k1="14400", k2="D1MO3", k3=k3, v2=v2)
|
||||
return Event(Operate.SE, [Factor(signals_all=[s])])
|
||||
|
||||
|
||||
def 信号字典(symbol="btc", dt=None, close=50000.0, bid=1, **kwargs):
|
||||
"""构造信号字典(含 OHLCV + 信号键)。"""
|
||||
if dt is None:
|
||||
dt = datetime.now(timezone.utc)
|
||||
d = {"symbol": symbol, "dt": dt, "close": close, "id": bid}
|
||||
d.update(kwargs)
|
||||
return d
|
||||
|
||||
|
||||
# ---- 构造 ----
|
||||
|
||||
|
||||
def test_构造状态初始化为默认值():
|
||||
p = Position(symbol="btc", opens=[开多事件()], name="测试")
|
||||
assert p.pos == 0
|
||||
assert p.pos_changed is False
|
||||
assert p.operates == []
|
||||
assert p.holds == []
|
||||
|
||||
|
||||
# ---- update: 开仓 ----
|
||||
|
||||
|
||||
def test_update_开多():
|
||||
p = Position(symbol="btc", opens=[开多事件()], name="测试")
|
||||
p.update(信号字典(**{"14400_D1MO3_中枢": "任意_三买_任意_0"}))
|
||||
assert p.pos == 1
|
||||
assert p.pos_changed is True
|
||||
assert len(p.operates) == 1
|
||||
assert p.operates[0]["op"] == Operate.LO
|
||||
assert len(p.holds) == 1
|
||||
assert p.holds[0]["pos"] == 1
|
||||
|
||||
|
||||
def test_update_开空():
|
||||
p = Position(symbol="btc", opens=[开空事件()], name="测试")
|
||||
p.update(信号字典(**{"14400_D1MO3_中枢": "任意_三卖_任意_0"}))
|
||||
assert p.pos == -1
|
||||
assert p.operates[0]["op"] == Operate.SO
|
||||
|
||||
|
||||
def test_update_开多_已持仓_不重复开仓():
|
||||
p = Position(symbol="btc", opens=[开多事件()], name="测试")
|
||||
p.update(信号字典(dt=datetime(2020, 1, 1, tzinfo=timezone.utc), bid=1, **{"14400_D1MO3_中枢": "任意_三买_任意_0"}))
|
||||
assert p.pos == 1
|
||||
assert len(p.operates) == 1
|
||||
# 第二次相同信号,已多头,不再开仓
|
||||
p.update(信号字典(dt=datetime(2020, 1, 1, 1, tzinfo=timezone.utc), bid=2, **{"14400_D1MO3_中枢": "任意_三买_任意_0"}))
|
||||
assert p.pos == 1
|
||||
assert len(p.operates) == 1 # 无新操作
|
||||
|
||||
|
||||
# ---- update: 平仓 ----
|
||||
|
||||
|
||||
def test_update_开多后平多():
|
||||
p = Position(symbol="btc", opens=[开多事件()], exits=[平多事件()], name="测试")
|
||||
# Step 1: LO
|
||||
p.update(信号字典(dt=datetime(2020, 1, 1, tzinfo=timezone.utc), bid=1, **{"14400_D1MO3_中枢": "任意_三买_任意_0"}))
|
||||
assert p.pos == 1
|
||||
# Step 2: LE (next day to allow exit when T0=False)
|
||||
p.update(信号字典(dt=datetime(2020, 1, 2, tzinfo=timezone.utc), bid=2, **{"14400_D1MO3_中枢": "任意_三卖_任意_0"}))
|
||||
assert p.pos == 0, f"Expected pos=0, got {p.pos}"
|
||||
assert p.operates[-1]["op"] == Operate.LE
|
||||
|
||||
|
||||
def test_update_开空后平空():
|
||||
p = Position(symbol="btc", opens=[开空事件()], exits=[平空事件()], name="测试")
|
||||
p.update(信号字典(dt=datetime(2020, 1, 1, tzinfo=timezone.utc), bid=1, **{"14400_D1MO3_中枢": "任意_三卖_任意_0"}))
|
||||
assert p.pos == -1
|
||||
p.update(信号字典(dt=datetime(2020, 1, 2, tzinfo=timezone.utc), bid=2, **{"14400_D1MO3_中枢": "任意_三买_任意_0"}))
|
||||
assert p.pos == 0
|
||||
assert p.operates[-1]["op"] == Operate.SE
|
||||
|
||||
|
||||
# ---- update: 止损 ----
|
||||
|
||||
|
||||
def test_update_多头止损():
|
||||
p = Position(symbol="btc", opens=[开多事件()], name="测试", stop_loss=500)
|
||||
p.update(信号字典(dt=datetime(2020, 1, 1, tzinfo=timezone.utc), bid=1, close=50000.0, **{"14400_D1MO3_中枢": "任意_三买_任意_0"}))
|
||||
assert p.pos == 1
|
||||
# Price drops to 47000: (47000/50000 - 1) = -0.06 = -600 BP < -500 BP stop_loss
|
||||
p.update(信号字典(dt=datetime(2020, 1, 2, tzinfo=timezone.utc), bid=2, close=47000.0, **{"14400_D1MO3_中枢": "任意_无_任意_0"}))
|
||||
assert p.pos == 0, "Should be stopped out"
|
||||
assert "止损" in p.operates[-1]["op_desc"]
|
||||
|
||||
|
||||
def test_update_空头止损():
|
||||
p = Position(symbol="btc", opens=[开空事件()], name="测试", stop_loss=500)
|
||||
p.update(信号字典(dt=datetime(2020, 1, 1, tzinfo=timezone.utc), bid=1, close=50000.0, **{"14400_D1MO3_中枢": "任意_三卖_任意_0"}))
|
||||
assert p.pos == -1
|
||||
# Price rises to 53000: (1 - 53000/50000) = -0.06 = -600 BP < -500 BP stop_loss
|
||||
p.update(信号字典(dt=datetime(2020, 1, 2, tzinfo=timezone.utc), bid=2, close=53000.0, **{"14400_D1MO3_中枢": "任意_无_任意_0"}))
|
||||
assert p.pos == 0, "Should be stopped out"
|
||||
assert "止损" in p.operates[-1]["op_desc"]
|
||||
|
||||
|
||||
# ---- update: 超时 ----
|
||||
|
||||
|
||||
def test_update_多头超时():
|
||||
p = Position(symbol="btc", opens=[开多事件()], name="测试", timeout=5)
|
||||
p.update(信号字典(dt=datetime(2020, 1, 1, tzinfo=timezone.utc), bid=1, close=50000.0, **{"14400_D1MO3_中枢": "任意_三买_任意_0"}))
|
||||
assert p.pos == 1
|
||||
# bid diff=9 > timeout=5
|
||||
p.update(信号字典(dt=datetime(2020, 1, 2, tzinfo=timezone.utc), bid=10, close=50000.0, **{"14400_D1MO3_中枢": "任意_无_任意_0"}))
|
||||
assert p.pos == 0, "Should be timed out"
|
||||
assert "超时" in p.operates[-1]["op_desc"]
|
||||
|
||||
|
||||
# ---- update: 间隔限制 ----
|
||||
|
||||
|
||||
def test_update_间隔限制():
|
||||
p = Position(symbol="btc", opens=[开多事件()], name="测试", interval=3600)
|
||||
# Create fresh position, open, test interval protection
|
||||
p2 = Position(symbol="btc", opens=[开多事件()], name="测试", interval=3600)
|
||||
p2.update(信号字典(dt=datetime(2020, 1, 1, tzinfo=timezone.utc), bid=1, close=50000, **{"14400_D1MO3_中枢": "任意_三买_任意_0"}))
|
||||
assert len(p2.operates) == 1
|
||||
# Within interval, same day (T0=False) — no new open
|
||||
p2.update(信号字典(dt=datetime(2020, 1, 1, 1, tzinfo=timezone.utc), bid=2, close=50000, **{"14400_D1MO3_中枢": "任意_三买_任意_0"}))
|
||||
assert len(p2.operates) == 1 # No new operate (already long, interval not elapsed)
|
||||
|
||||
|
||||
# ---- update: 边界条件 ----
|
||||
|
||||
|
||||
def test_update_时间倒退_跳过():
|
||||
p = Position(symbol="btc", opens=[开多事件()], name="测试")
|
||||
dt1 = datetime(2020, 1, 2, tzinfo=timezone.utc)
|
||||
dt2 = datetime(2020, 1, 1, tzinfo=timezone.utc) # earlier
|
||||
p.update(信号字典(dt=dt1, **{"14400_D1MO3_中枢": "任意_三买_任意_0"}))
|
||||
operates_before = len(p.operates)
|
||||
p.update(信号字典(dt=dt2, **{"14400_D1MO3_中枢": "任意_三买_任意_0"}))
|
||||
assert len(p.operates) == operates_before # skipped
|
||||
|
||||
|
||||
def test_update_空事件列表():
|
||||
p = Position(symbol="btc", opens=[], name="空")
|
||||
p.update(信号字典(dt=datetime(2020, 1, 1, tzinfo=timezone.utc), **{"14400_D1MO3_中枢": "任意_三买_任意_0"}))
|
||||
assert p.pos == 0
|
||||
assert len(p.holds) == 1
|
||||
|
||||
|
||||
def test_update_无匹配事件_仅追加holds():
|
||||
p = Position(symbol="btc", opens=[开多事件()], name="测试")
|
||||
p.update(信号字典(dt=datetime(2020, 1, 1, tzinfo=timezone.utc), **{"14400_D1MO3_中枢": "任意_无_任意_0"}))
|
||||
assert p.pos == 0
|
||||
assert p.operates == []
|
||||
assert len(p.holds) == 1
|
||||
|
||||
|
||||
def test_update_缺键错误():
|
||||
"""信号字典缺少事件所需 key 时抛 ValueError。"""
|
||||
p = Position(symbol="btc", opens=[开多事件()], name="测试")
|
||||
with pytest.raises(ValueError, match="不在信号列表中"):
|
||||
# 空信号字典缺少 "14400_D1MO3_中枢" 键
|
||||
p.update(信号字典(dt=datetime(2020, 1, 1, tzinfo=timezone.utc)))
|
||||
|
||||
|
||||
def test_update_T0模式_同一天可操作():
|
||||
p = Position(symbol="btc", opens=[开多事件()], exits=[平多事件()], name="测试", T0=True)
|
||||
dt = datetime(2020, 1, 1, 0, 0, tzinfo=timezone.utc)
|
||||
p.update(信号字典(dt=dt, bid=1, **{"14400_D1MO3_中枢": "任意_三买_任意_0"}))
|
||||
assert p.pos == 1
|
||||
# Same day, T0=True → 允许平仓
|
||||
p.update(信号字典(dt=dt.replace(hour=1), bid=2, **{"14400_D1MO3_中枢": "任意_三卖_任意_0"}))
|
||||
assert p.pos == 0
|
||||
|
||||
|
||||
# ---- pairs ----
|
||||
|
||||
|
||||
def test_pairs_空():
|
||||
p = Position(symbol="btc", opens=[开多事件()], name="测试")
|
||||
assert p.pairs == []
|
||||
|
||||
|
||||
def test_pairs_单笔开平_多头盈利():
|
||||
p = Position(symbol="btc", opens=[开多事件()], exits=[平多事件()], name="测试")
|
||||
p.update(信号字典(dt=datetime(2020, 1, 1, tzinfo=timezone.utc), bid=1, close=50000.0, **{"14400_D1MO3_中枢": "任意_三买_任意_0"}))
|
||||
p.update(信号字典(dt=datetime(2020, 1, 2, tzinfo=timezone.utc), bid=2, close=51000.0, **{"14400_D1MO3_中枢": "任意_三卖_任意_0"}))
|
||||
pairs = p.pairs
|
||||
assert len(pairs) == 1
|
||||
assert pairs[0]["交易方向"] == "多头"
|
||||
assert pairs[0]["开仓价格"] == 50000.0
|
||||
assert pairs[0]["平仓价格"] == 51000.0
|
||||
# (51000/50000 - 1) * 10000 = 200 BP
|
||||
assert pairs[0]["盈亏比例"] == pytest.approx(200.0, abs=0.1)
|
||||
|
||||
|
||||
def test_pairs_单笔开平_空头盈利():
|
||||
p = Position(symbol="btc", opens=[开空事件()], exits=[平空事件()], name="测试")
|
||||
p.update(信号字典(dt=datetime(2020, 1, 1, tzinfo=timezone.utc), bid=1, close=50000.0, **{"14400_D1MO3_中枢": "任意_三卖_任意_0"}))
|
||||
p.update(信号字典(dt=datetime(2020, 1, 2, tzinfo=timezone.utc), bid=2, close=48000.0, **{"14400_D1MO3_中枢": "任意_三买_任意_0"}))
|
||||
pairs = p.pairs
|
||||
assert len(pairs) == 1
|
||||
assert pairs[0]["交易方向"] == "空头"
|
||||
assert pairs[0]["开仓价格"] == 50000.0
|
||||
assert pairs[0]["平仓价格"] == 48000.0
|
||||
# (1 - 48000/50000) * 10000 = 400 BP
|
||||
assert pairs[0]["盈亏比例"] == pytest.approx(400.0, abs=0.1)
|
||||
|
||||
|
||||
def test_pairs_持仓天数():
|
||||
p = Position(symbol="btc", opens=[开多事件()], exits=[平多事件()], name="测试")
|
||||
p.update(信号字典(dt=datetime(2020, 1, 1, tzinfo=timezone.utc), bid=1, close=50000.0, **{"14400_D1MO3_中枢": "任意_三买_任意_0"}))
|
||||
# 3 days later
|
||||
p.update(信号字典(dt=datetime(2020, 1, 4, tzinfo=timezone.utc), bid=2, close=51000.0, **{"14400_D1MO3_中枢": "任意_三卖_任意_0"}))
|
||||
assert p.pairs[0]["持仓天数"] == pytest.approx(3.0, abs=0.1)
|
||||
|
||||
|
||||
# ---- dump/load ----
|
||||
|
||||
|
||||
def test_dump_with_data():
|
||||
p = Position(symbol="btc", opens=[开多事件()], name="测试")
|
||||
p.update(信号字典(dt=datetime(2020, 1, 1, tzinfo=timezone.utc), bid=1, close=50000.0, **{"14400_D1MO3_中枢": "任意_三买_任意_0"}))
|
||||
raw = p.dump(with_data=True)
|
||||
assert "pairs" in raw
|
||||
assert "holds" in raw
|
||||
assert raw["symbol"] == "btc"
|
||||
assert len(raw["holds"]) == 1
|
||||
|
||||
|
||||
def test_dump_without_data():
|
||||
p = Position(symbol="btc", opens=[开多事件()], name="测试")
|
||||
raw = p.dump(with_data=False)
|
||||
assert "symbol" in raw
|
||||
assert "pairs" not in raw
|
||||
|
||||
|
||||
def test_load_roundtrip():
|
||||
from chanlun.chan_external import Position as PyPos
|
||||
|
||||
p = PyPos(symbol="btc", opens=[开多事件()], name="测试", timeout=500)
|
||||
p2 = PyPos.load(p.dump())
|
||||
assert p2.symbol == p.symbol
|
||||
assert p2.name == p.name
|
||||
assert p2.timeout == 500
|
||||
assert p2.pos == 0 # 新构造,状态初始
|
||||
|
||||
|
||||
# ---- 信号字典 dt 类型兼容 ----
|
||||
|
||||
|
||||
def test_update_dt_支持int时间戳():
|
||||
"""验证 update() 支持 int Unix 时间戳(除 datetime 外)。"""
|
||||
p = Position(symbol="btc", opens=[开多事件()], name="测试")
|
||||
ts = int(datetime(2020, 1, 1, tzinfo=timezone.utc).timestamp())
|
||||
p.update(信号字典(dt=ts, **{"14400_D1MO3_中枢": "任意_三买_任意_0"}))
|
||||
assert p.pos == 1
|
||||
|
||||
|
||||
def test_update_dt_支持float时间戳():
|
||||
"""验证 update() 支持 float Unix 时间戳。"""
|
||||
p = Position(symbol="btc", opens=[开多事件()], name="测试")
|
||||
ts = datetime(2020, 1, 1, tzinfo=timezone.utc).timestamp()
|
||||
p.update(信号字典(dt=ts, **{"14400_D1MO3_中枢": "任意_三买_任意_0"}))
|
||||
assert p.pos == 1
|
||||
@@ -1,211 +0,0 @@
|
||||
"""信号原语 Rust 移植后的跨语言一致性与边界行为测试。
|
||||
|
||||
验证 chanlun._chanlun 的 Signal/Factor/Event/Operate/Position 与移植前 Python 版本
|
||||
行为一致(name hash 除外,已改为 Rust 确定性哈希)。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from chanlun._chanlun import Signal, Factor, Event, Operate, Position
|
||||
|
||||
|
||||
# ---- Signal ----
|
||||
|
||||
|
||||
def test_signal_parse_and_props():
|
||||
s = Signal("14400_D1MO3_中枢_中枢段DEA穿越2_三买_偏移0_100")
|
||||
assert s.k1 == "14400" and s.k3 == "中枢" and s.v2 == "三买" and s.score == 100
|
||||
assert s.key == "14400_D1MO3_中枢"
|
||||
assert s.value == "中枢段DEA穿越2_三买_偏移0_100"
|
||||
assert repr(s) == "Signal('14400_D1MO3_中枢_中枢段DEA穿越2_三买_偏移0_100')"
|
||||
|
||||
|
||||
def test_signal_from_fields_default_任意():
|
||||
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
|
||||
# v1/v3 缺省为 任意 → key 过滤后保留全部 k;value 含 任意
|
||||
assert s.key == "14400_D1MO3_中枢"
|
||||
assert s.value == "任意_三买_任意_0"
|
||||
|
||||
|
||||
def test_signal_score_out_of_range():
|
||||
with pytest.raises(ValueError):
|
||||
Signal(k1="a", k2="b", k3="c", score=101)
|
||||
|
||||
|
||||
def test_signal_is_match_missing_key_raises():
|
||||
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
|
||||
with pytest.raises(ValueError):
|
||||
s.is_match({})
|
||||
|
||||
|
||||
def test_signal_is_match_non_str_value_false():
|
||||
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
|
||||
assert s.is_match({"14400_D1MO3_中枢": 123}) is False
|
||||
|
||||
|
||||
def test_signal_is_match_hit():
|
||||
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
|
||||
assert s.is_match({"14400_D1MO3_中枢": "x_三买_y_100"}) is True
|
||||
|
||||
|
||||
def test_signal_is_match_v2_mismatch():
|
||||
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
|
||||
assert s.is_match({"14400_D1MO3_中枢": "x_三卖_y_100"}) is False
|
||||
|
||||
|
||||
# ---- Factor ----
|
||||
|
||||
|
||||
def test_factor_empty_all_raises():
|
||||
with pytest.raises(ValueError):
|
||||
Factor(signals_all=[])
|
||||
|
||||
|
||||
def test_factor_name_deterministic():
|
||||
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
|
||||
f1 = Factor(signals_all=[s])
|
||||
f2 = Factor(signals_all=[Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")])
|
||||
assert f1.name == f2.name
|
||||
assert f1.name.startswith("#") and len(f1.name) == 5 # #XXXX
|
||||
|
||||
|
||||
def test_factor_not_short_circuit():
|
||||
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
|
||||
f = Factor(signals_all=[s], signals_not=[s])
|
||||
assert f.is_match({"14400_D1MO3_中枢": "x_三买_y_100"}) is False
|
||||
|
||||
|
||||
def test_factor_unique_signals_is_property():
|
||||
"""unique_signals 必须是 property(匹配 Python @property),不带括号访问。"""
|
||||
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
|
||||
f = Factor(signals_all=[s])
|
||||
assert f.unique_signals == [s.signal] # 属性访问,非方法调用
|
||||
|
||||
|
||||
# ---- Event ----
|
||||
|
||||
|
||||
def test_event_empty_factors_raises():
|
||||
with pytest.raises(ValueError):
|
||||
Event(Operate.LO, [])
|
||||
|
||||
|
||||
def test_event_name_uses_operate():
|
||||
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
|
||||
e = Event(Operate.LO, [Factor(signals_all=[s])])
|
||||
assert e.name.startswith("开多#")
|
||||
|
||||
|
||||
def test_event_match_returns_factor_name():
|
||||
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
|
||||
e = Event(Operate.LO, [Factor(signals_all=[s])])
|
||||
ok, name = e.is_match({"14400_D1MO3_中枢": "x_三买_y_100"})
|
||||
assert ok and name
|
||||
|
||||
|
||||
def test_event_multi_factor_or():
|
||||
"""多 Factor OR:两 key 都在场,第一个不匹配、第二个匹配 → 返回第二个因子名。"""
|
||||
base = "14400"
|
||||
f1 = Factor(signals_all=[Signal(k1=base, k2="D1MO3", k3="中枢A", v2="三买")])
|
||||
f2 = Factor(signals_all=[Signal(k1=base, k2="D1MO3", k3="中枢B", v2="三买")])
|
||||
e = Event(Operate.LO, [f1, f2])
|
||||
d = {"14400_D1MO3_中枢A": "x_三卖_y_100", "14400_D1MO3_中枢B": "x_三买_y_100"}
|
||||
ok, name = e.is_match(d)
|
||||
assert ok and name == f2.name
|
||||
|
||||
|
||||
# ---- Operate ----
|
||||
|
||||
|
||||
def test_operate_value_and_eq():
|
||||
assert Operate.LO.value == "开多"
|
||||
assert Operate.LE.value == "平多"
|
||||
assert Operate.LO == Operate.LO
|
||||
assert Operate.LO in [Operate.LO, Operate.SO] # update() 内部用法
|
||||
|
||||
|
||||
# ---- Position(Rust 基类 + Python 子类)----
|
||||
|
||||
|
||||
def test_position_requires_name():
|
||||
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
|
||||
e = Event(Operate.LO, [Factor(signals_all=[s])])
|
||||
with pytest.raises((ValueError, TypeError)):
|
||||
Position(symbol="btc", opens=[e])
|
||||
|
||||
|
||||
def test_position_config_getters():
|
||||
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
|
||||
e = Event(Operate.LO, [Factor(signals_all=[s])])
|
||||
p = Position(symbol="btc", opens=[e], name="中枢", timeout=500, stop_loss=200, T0=True)
|
||||
assert p.symbol == "btc" and p.name == "中枢"
|
||||
assert p.timeout == 500 and p.stop_loss == 200 and p.T0 is True
|
||||
assert len(p.events) == 1
|
||||
assert p.unique_signals == [s.signal]
|
||||
|
||||
|
||||
def test_position_subclassable_with_state():
|
||||
"""验证 Rust 基类可被 Python 子类化,状态字段由 Rust 初始化。
|
||||
|
||||
pos/pos_changed/operates/holds 等状态字段由 Rust 基类提供(只读 getter),
|
||||
初始值在构造时由 Rust #[new] 自动初始化。
|
||||
"""
|
||||
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
|
||||
e = Event(Operate.LO, [Factor(signals_all=[s])])
|
||||
p = Position(symbol="btc", opens=[e], name="中枢")
|
||||
assert p.name == "中枢" # Rust 基类 getter
|
||||
assert p.pos == 0 # Rust 初始化为 0 (空仓)
|
||||
assert p.pos_changed is False
|
||||
assert p.operates == []
|
||||
assert p.holds == []
|
||||
|
||||
|
||||
# ---- 序列化 dump/load ----
|
||||
|
||||
|
||||
def test_operate_from_value():
|
||||
assert Operate.from_value("开多") == Operate.LO
|
||||
assert Operate.from_value("平空") == Operate.SE
|
||||
with pytest.raises(ValueError):
|
||||
Operate.from_value("不存在")
|
||||
|
||||
|
||||
def test_factor_dump_load_roundtrip():
|
||||
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
|
||||
f = Factor(signals_all=[s], name="测试")
|
||||
d = f.dump()
|
||||
assert d["name"] == f.name
|
||||
assert d["signals_all"] == [s.signal]
|
||||
assert d["signals_any"] == [] and d["signals_not"] == []
|
||||
f2 = Factor.load(d)
|
||||
assert f2.name == f.name # 确定性哈希 → 同输入同名
|
||||
assert f2.unique_signals == f.unique_signals
|
||||
|
||||
|
||||
def test_event_dump_load_roundtrip():
|
||||
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
|
||||
e = Event(Operate.LO, [Factor(signals_all=[s])])
|
||||
d = e.dump()
|
||||
assert d["operate"] == "开多"
|
||||
assert len(d["factors"]) == 1
|
||||
e2 = Event.load(d)
|
||||
assert e2.name == e.name
|
||||
assert e2.operate == Operate.LO
|
||||
|
||||
|
||||
def test_position_dump_load_roundtrip():
|
||||
"""Position 序列化:Rust 基类 dump 配置 + Python 子类 with_data/load 返回子类实例。"""
|
||||
from chanlun.chan_external import Position as PositionExt, Signal as S, Factor as F, Event as E, Operate as O
|
||||
|
||||
e = E(O.LO, [F(signals_all=[S(k1="14400", k2="D1MO3", k3="中枢", v2="三买")])])
|
||||
p = PositionExt(symbol="btc", opens=[e], name="中枢", timeout=500, T0=True)
|
||||
raw = p.dump()
|
||||
assert raw["symbol"] == "btc" and raw["T0"] is True and raw["timeout"] == 500
|
||||
assert len(raw["opens"]) == 1
|
||||
# with_data 附加 pairs/holds
|
||||
raw2 = p.dump(with_data=True)
|
||||
assert "pairs" in raw2 and "holds" in raw2
|
||||
# load 返回子类实例(含状态字段)
|
||||
p2 = PositionExt.load(raw)
|
||||
assert type(p2) is PositionExt
|
||||
assert p2.symbol == "btc" and p2.name == "中枢" and p2.timeout == 500
|
||||
assert p2.pos == 0 # 子类状态已初始化
|
||||
@@ -1 +0,0 @@
|
||||
/target/
|
||||
Generated
-47
@@ -1,47 +0,0 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "chanlun-signal-macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
@@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "chanlun-signal-macros"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
description = "chanlun 信号注册 proc-macro(#[signal])"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = { version = "2", features = ["full"] }
|
||||
quote = "1"
|
||||
proc-macro2 = "1"
|
||||
@@ -1,100 +0,0 @@
|
||||
//! 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::`(chanlun crate 内部使用)。
|
||||
/// 外部 crate 使用需指定 `crate_path = "::chanlun"`。
|
||||
#[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;
|
||||
let mut crate_path: 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()),
|
||||
"crate_path" => crate_path = 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 path = crate_path.unwrap_or_else(|| "crate".to_string());
|
||||
let _registry_path: syn::Path = syn::parse_str(&format!("{path}::signal::registry")).unwrap();
|
||||
let signal_fn: syn::Type = syn::parse_str(&format!("{path}::signal::registry::SignalFn")).unwrap();
|
||||
let signal_desc: syn::Type = syn::parse_str(&format!("{path}::signal::registry::SignalDescriptor")).unwrap();
|
||||
|
||||
let expanded = quote! {
|
||||
#f
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
static #descriptor_ident: #signal_desc =
|
||||
#signal_desc {
|
||||
name: #name,
|
||||
template: #template,
|
||||
func: #fn_ident as #signal_fn,
|
||||
};
|
||||
|
||||
inventory::submit! { #descriptor_ident }
|
||||
};
|
||||
expanded.into()
|
||||
}
|
||||
@@ -20,6 +20,3 @@ chrono = { version = "0.4", features = ["serde"] }
|
||||
parking_lot = "0.12"
|
||||
tracing = "0.1"
|
||||
fastrand = "2"
|
||||
sha2 = "0.10"
|
||||
inventory = "0.3"
|
||||
chanlun-signal-macros = { path = "../chanlun-signal-macros" }
|
||||
|
||||
+679
-864
File diff suppressed because it is too large
Load Diff
@@ -301,13 +301,6 @@ impl 中枢 {
|
||||
return self.第三买卖线.read().is_some();
|
||||
}
|
||||
|
||||
// if self.本级_第三买卖线: return True # 暂未启用
|
||||
|
||||
let 中枢状态 = self.当前状态();
|
||||
if 中枢状态 == "中枢之中" {
|
||||
return false;
|
||||
}
|
||||
|
||||
let 基础序列_ref = self.基础序列.read();
|
||||
let 最后段 = 基础序列_ref.last().unwrap();
|
||||
let 内部中枢_vec = if 虚实 == "合" {
|
||||
@@ -315,26 +308,14 @@ impl 中枢 {
|
||||
} else {
|
||||
最后段.实_中枢序列.read()
|
||||
};
|
||||
if 内部中枢_vec.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let 高 = self.高();
|
||||
let 低 = self.低();
|
||||
for 内部中枢 in 内部中枢_vec.iter() {
|
||||
let 内部中枢高 = 内部中枢.高();
|
||||
let 内部中枢低 = 内部中枢.低();
|
||||
if 中枢状态 == "中枢之下" {
|
||||
if 低 <= 内部中枢高 {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// 中枢之上
|
||||
if 高 >= 内部中枢低 {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if crate::types::相对方向::分析(高, 低, 内部中枢高, 内部中枢低).是否缺口()
|
||||
if crate::types::相对方向::分析(
|
||||
self.高(),
|
||||
self.低(),
|
||||
内部中枢.高(),
|
||||
内部中枢.低(),
|
||||
)
|
||||
.是否缺口()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1783,6 +1783,7 @@ mod tests {
|
||||
use crate::kline::chan_kline::缠论K线;
|
||||
use crate::structure::fractal_obj::分型;
|
||||
use crate::types::分型结构;
|
||||
use crate::{error, info, warn};
|
||||
|
||||
fn 辅助_创建普K(时间戳: i64, 高: f64, 低: f64) -> Arc<K线> {
|
||||
Arc::new(K线 {
|
||||
|
||||
@@ -61,7 +61,8 @@ impl 立体分析器 {
|
||||
.get(&周期)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| 默认配置.clone());
|
||||
当前配置.图表展示标签 = Some(vec![]);
|
||||
当前配置.推送K线 = false;
|
||||
当前配置.推送线段 = false;
|
||||
当前配置.标识 = 符号.clone();
|
||||
|
||||
let 观察员 = 观察者::new(符号.clone(), 周期, 当前配置);
|
||||
@@ -72,7 +73,9 @@ impl 立体分析器 {
|
||||
{
|
||||
let 显示观察员 = 单体分析器.get(&显示周期).expect("显示周期观察者不存在");
|
||||
let mut guard = 显示观察员.write();
|
||||
guard.配置.图表展示标签 = None; // None = 全部展示
|
||||
guard.配置.推送K线 = true;
|
||||
guard.配置.推送笔 = true;
|
||||
guard.配置.推送线段 = true;
|
||||
guard.配置.图表展示 = true;
|
||||
guard.重置基础序列();
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ use crate::algorithm::bi::笔;
|
||||
use crate::algorithm::hub::中枢;
|
||||
use crate::algorithm::segment::线段;
|
||||
use crate::config::缠论配置;
|
||||
use crate::indicators::calculator::指标计算器;
|
||||
use crate::kline::bar::K线;
|
||||
use crate::kline::chan_kline::缠论K线;
|
||||
use crate::structure::dash_line::虚线;
|
||||
@@ -200,14 +199,6 @@ impl 观察者 {
|
||||
&self.混合扩展中枢序列组[0]
|
||||
}
|
||||
|
||||
/// 确保所有 K 线上的指标已计算(幂等)。
|
||||
/// 在信号函数入口调用,保证后续 macd()/rsi() 等访问不返回 None。
|
||||
pub fn 确保指标已计算(&self) {
|
||||
if self.配置.计算指标 && !self.普通K线序列.is_empty() {
|
||||
指标计算器::计算并挂载(&self.普通K线序列, &self.配置);
|
||||
}
|
||||
}
|
||||
|
||||
/// 重置基础序列
|
||||
pub fn 重置基础序列(&mut self) {
|
||||
self.普通K线序列.clear();
|
||||
@@ -742,7 +733,7 @@ impl 观察者 {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::缠论配置;
|
||||
use crate::info;
|
||||
use crate::{error, info};
|
||||
|
||||
fn test_data_path() -> String {
|
||||
let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
|
||||
+206
-66
@@ -92,34 +92,103 @@ pub struct 缠论配置 {
|
||||
// ---- 指标 ----
|
||||
/// 是否计算技术指标
|
||||
pub 计算指标: bool,
|
||||
/// 指标计算方式(均线使用,MACD/RSI/KDJ/BOLL 在参数元组中指定)
|
||||
/// 是否计算布林带
|
||||
pub 计算BOLL: bool,
|
||||
/// 指标计算方式(开/高/低/收/高低均值/高低收均值/开高低收均值)
|
||||
#[serde(deserialize_with = "deserialize_指标计算方式")]
|
||||
pub 指标计算方式: String,
|
||||
|
||||
/// MACD 参数列表 (key, 计算方式, 快线, 慢线, 信号)
|
||||
// ---- MACD ----
|
||||
/// MACD 快线 EMA 周期
|
||||
pub 平滑异同移动平均线_快线周期: i64,
|
||||
/// MACD 慢线 EMA 周期
|
||||
pub 平滑异同移动平均线_慢线周期: i64,
|
||||
/// MACD 信号线周期
|
||||
pub 平滑异同移动平均线_信号周期: i64,
|
||||
/// MACD 多参数列表: Vec<(key, 快线, 慢线, 信号)>
|
||||
#[serde(default)]
|
||||
pub MACD_参数列表: Vec<(String, String, i64, i64, i64)>,
|
||||
pub MACD_参数列表: Vec<(String, i64, i64, i64)>,
|
||||
|
||||
/// RSI 参数列表 (key, 计算方式, 周期, MA周期, 超买, 超卖)
|
||||
// ---- RSI ----
|
||||
/// RSI 计算周期
|
||||
pub 相对强弱指数_周期: i64,
|
||||
/// RSI SMA 平滑周期
|
||||
pub 相对强弱指数_移动平均线周期: i64,
|
||||
/// RSI 超买阈值
|
||||
pub 相对强弱指数_超买阈值: f64,
|
||||
/// RSI 超卖阈值
|
||||
pub 相对强弱指数_超卖阈值: f64,
|
||||
/// RSI 多周期列表: Vec<(key, 周期)>
|
||||
#[serde(default)]
|
||||
pub RSI_周期列表: Vec<(String, String, i64, i64, f64, f64)>,
|
||||
pub RSI_周期列表: Vec<(String, i64)>,
|
||||
|
||||
/// KDJ 参数列表 (key, 计算方式, RSV, K平滑, D平滑, 超买, 超卖)
|
||||
// ---- KDJ ----
|
||||
/// KDJ RSV 周期
|
||||
pub 随机指标_RSV周期: i64,
|
||||
/// KDJ K 值平滑周期
|
||||
pub 随机指标_K值平滑周期: i64,
|
||||
/// KDJ D 值平滑周期
|
||||
pub 随机指标_D值平滑周期: i64,
|
||||
/// KDJ 超买阈值
|
||||
pub 随机指标_超买阈值: f64,
|
||||
/// KDJ 超卖阈值
|
||||
pub 随机指标_超卖阈值: f64,
|
||||
/// KDJ 多参数列表: Vec<(key, RSV周期, K平滑, D平滑)>
|
||||
#[serde(default)]
|
||||
pub KDJ_参数列表: Vec<(String, String, i64, i64, i64, f64, f64)>,
|
||||
pub KDJ_参数列表: Vec<(String, i64, i64, i64)>,
|
||||
|
||||
/// BOLL 参数列表 (key, 计算方式, 周期, 标准差倍数)
|
||||
// ---- BOLL ----
|
||||
/// 布林带周期
|
||||
pub 布林带_周期: i64,
|
||||
/// 布林带标准差倍数
|
||||
pub 布林带_标准差倍数: f64,
|
||||
/// BOLL 多参数列表: Vec<(key, 周期, 标准差倍数)>
|
||||
#[serde(default)]
|
||||
pub BOLL_参数列表: Vec<(String, String, i64, f64)>,
|
||||
pub BOLL_参数列表: Vec<(String, i64, f64)>,
|
||||
|
||||
/// 均线参数列表 (key, 计算方式, 类型, 周期) — 如 ("SMA_5", "收", "SMA", 5)
|
||||
// ---- 均线 ----
|
||||
/// 均线类型列表: ["SMA", "EMA", ...]
|
||||
#[serde(default)]
|
||||
pub 均线参数列表: Vec<(String, String, String, i64)>,
|
||||
pub 均线_类型列表: Vec<String>,
|
||||
/// 均线周期列表: [5, 10, 20, ...]
|
||||
#[serde(default)]
|
||||
pub 均线_周期列表: Vec<i64>,
|
||||
|
||||
// ---- 推送/显示 ----
|
||||
/// 是否启用图表展示
|
||||
pub 图表展示: bool,
|
||||
/// 图表展示标签: None=全部, [] = 不展示
|
||||
pub 图表展示标签: Option<Vec<String>>,
|
||||
/// 是否推送K线
|
||||
pub 推送K线: bool,
|
||||
/// 是否推送笔
|
||||
pub 推送笔: bool,
|
||||
/// 是否推送线段
|
||||
pub 推送线段: bool,
|
||||
/// 是否推送中枢
|
||||
pub 推送中枢: bool,
|
||||
|
||||
// ---- 图表展示细分 ----
|
||||
/// 图表展示笔
|
||||
pub 图表展示_笔: bool,
|
||||
/// 图表展示线段
|
||||
pub 图表展示_线段: bool,
|
||||
/// 图表展示扩展线段
|
||||
pub 图表展示_扩展线段: bool,
|
||||
/// 图表展示扩展线段(线段级)
|
||||
pub 图表展示_扩展线段_线段: bool,
|
||||
/// 图表展示线段之线段
|
||||
pub 图表展示_线段_线段: bool,
|
||||
/// 图表展示笔中枢
|
||||
pub 图表展示_中枢_笔: bool,
|
||||
/// 图表展示线段中枢
|
||||
pub 图表展示_中枢_线段: bool,
|
||||
/// 图表展示扩展中枢
|
||||
pub 图表展示_中枢_扩展线段: bool,
|
||||
/// 图表展示扩展中枢(线段级)
|
||||
pub 图表展示_中枢_扩展线段_线段: bool,
|
||||
/// 图表展示线段之中枢
|
||||
pub 图表展示_中枢_线段_线段: bool,
|
||||
/// 图表展示线段内部中枢
|
||||
pub 图表展示_中枢_线段内部: bool,
|
||||
|
||||
// ---- 买卖点 ----
|
||||
/// 买卖点偏移量
|
||||
@@ -241,14 +310,44 @@ impl Default for 缠论配置 {
|
||||
分析线段中枢: true,
|
||||
手动终止: String::new(),
|
||||
计算指标: true,
|
||||
计算BOLL: false,
|
||||
指标计算方式: "收".into(),
|
||||
MACD_参数列表: vec![("macd".into(), "收".into(), 13, 31, 11)],
|
||||
RSI_周期列表: vec![("rsi".into(), "收".into(), 14, 13, 75.0, 25.0)],
|
||||
KDJ_参数列表: vec![("kdj".into(), "收".into(), 13, 5, 5, 80.0, 20.0)],
|
||||
BOLL_参数列表: vec![("boll".into(), "收".into(), 20, 2.0)],
|
||||
均线参数列表: Vec::new(),
|
||||
平滑异同移动平均线_快线周期: 13,
|
||||
平滑异同移动平均线_慢线周期: 31,
|
||||
平滑异同移动平均线_信号周期: 11,
|
||||
相对强弱指数_周期: 13,
|
||||
相对强弱指数_移动平均线周期: 13,
|
||||
相对强弱指数_超买阈值: 75.0,
|
||||
相对强弱指数_超卖阈值: 25.0,
|
||||
随机指标_RSV周期: 13,
|
||||
随机指标_K值平滑周期: 5,
|
||||
随机指标_D值平滑周期: 5,
|
||||
随机指标_超买阈值: 80.0,
|
||||
随机指标_超卖阈值: 20.0,
|
||||
MACD_参数列表: Vec::new(),
|
||||
RSI_周期列表: Vec::new(),
|
||||
KDJ_参数列表: Vec::new(),
|
||||
布林带_周期: 20,
|
||||
布林带_标准差倍数: 2.0,
|
||||
BOLL_参数列表: Vec::new(),
|
||||
均线_类型列表: Vec::new(),
|
||||
均线_周期列表: Vec::new(),
|
||||
图表展示: true,
|
||||
图表展示标签: None,
|
||||
推送K线: true,
|
||||
推送笔: true,
|
||||
推送线段: true,
|
||||
推送中枢: true,
|
||||
图表展示_笔: true,
|
||||
图表展示_线段: true,
|
||||
图表展示_扩展线段: true,
|
||||
图表展示_扩展线段_线段: true,
|
||||
图表展示_线段_线段: true,
|
||||
图表展示_中枢_笔: true,
|
||||
图表展示_中枢_线段: true,
|
||||
图表展示_中枢_扩展线段: true,
|
||||
图表展示_中枢_扩展线段_线段: true,
|
||||
图表展示_中枢_线段_线段: true,
|
||||
图表展示_中枢_线段内部: true,
|
||||
买卖点偏移: 1,
|
||||
买卖点激进识别: false,
|
||||
买卖点与MACD柱强相关: false,
|
||||
@@ -267,42 +366,46 @@ impl Default for 缠论配置 {
|
||||
}
|
||||
|
||||
impl 缠论配置 {
|
||||
/// 展示标签判定 — None=全部, [] = 全关
|
||||
pub fn 展示标签(&self, 标签: &str) -> bool {
|
||||
match &self.图表展示标签 {
|
||||
None => true,
|
||||
Some(tags) => tags.iter().any(|t| t == 标签),
|
||||
/// 解析MACD参数列表 — 如果列表非空则使用列表,否则返回默认单组
|
||||
pub fn _解析MACD参数列表(&self) -> Vec<(String, i64, i64, i64)> {
|
||||
if !self.MACD_参数列表.is_empty() {
|
||||
return self.MACD_参数列表.clone();
|
||||
}
|
||||
vec![(
|
||||
"macd".into(),
|
||||
self.平滑异同移动平均线_快线周期,
|
||||
self.平滑异同移动平均线_慢线周期,
|
||||
self.平滑异同移动平均线_信号周期,
|
||||
)]
|
||||
}
|
||||
|
||||
/// 统一设置所有指标参数(对应 Python 设置指标)。
|
||||
///
|
||||
/// 各参数为 None 时不修改对应字段;非 None 时替换对应参数列表。
|
||||
/// 调用后自动将 `计算指标` 设为 `true`。
|
||||
pub fn 设置指标(
|
||||
&mut self,
|
||||
均线: Option<Vec<(String, String, String, i64)>>,
|
||||
MACD: Option<Vec<(String, String, i64, i64, i64)>>,
|
||||
RSI: Option<Vec<(String, String, i64, i64, f64, f64)>>,
|
||||
KDJ: Option<Vec<(String, String, i64, i64, i64, f64, f64)>>,
|
||||
BOLL: Option<Vec<(String, String, i64, f64)>>,
|
||||
) {
|
||||
self.计算指标 = true;
|
||||
if let Some(v) = 均线 {
|
||||
self.均线参数列表 = v;
|
||||
/// 解析RSI周期列表 — 如果列表非空则使用列表,否则返回默认单组
|
||||
pub fn _解析RSI周期列表(&self) -> Vec<(String, i64)> {
|
||||
if !self.RSI_周期列表.is_empty() {
|
||||
return self.RSI_周期列表.clone();
|
||||
}
|
||||
if let Some(v) = MACD {
|
||||
self.MACD_参数列表 = v;
|
||||
vec![("rsi".into(), self.相对强弱指数_周期)]
|
||||
}
|
||||
|
||||
/// 解析KDJ参数列表 — 如果列表非空则使用列表,否则返回默认单组
|
||||
pub fn _解析KDJ参数列表(&self) -> Vec<(String, i64, i64, i64)> {
|
||||
if !self.KDJ_参数列表.is_empty() {
|
||||
return self.KDJ_参数列表.clone();
|
||||
}
|
||||
if let Some(v) = RSI {
|
||||
self.RSI_周期列表 = v;
|
||||
}
|
||||
if let Some(v) = KDJ {
|
||||
self.KDJ_参数列表 = v;
|
||||
}
|
||||
if let Some(v) = BOLL {
|
||||
self.BOLL_参数列表 = v;
|
||||
vec![(
|
||||
"kdj".into(),
|
||||
self.随机指标_RSV周期,
|
||||
self.随机指标_K值平滑周期,
|
||||
self.随机指标_D值平滑周期,
|
||||
)]
|
||||
}
|
||||
|
||||
/// 解析BOLL参数列表 — 如果列表非空则使用列表,否则返回默认单组
|
||||
pub fn _解析BOLL参数列表(&self) -> Vec<(String, i64, f64)> {
|
||||
if !self.BOLL_参数列表.is_empty() {
|
||||
return self.BOLL_参数列表.clone();
|
||||
}
|
||||
vec![("boll".into(), self.布林带_周期, self.布林带_标准差倍数)]
|
||||
}
|
||||
|
||||
/// 序列化为 JSON 字典(对应 Python to_dict,仅返回 model_fields 中的字段)
|
||||
@@ -358,11 +461,8 @@ impl 缠论配置 {
|
||||
/// 返回字段名列表(对应 Python model_fields().keys())
|
||||
pub fn model_fields() -> &'static [&'static str] {
|
||||
&[
|
||||
// ---- 基础 ----
|
||||
"标识",
|
||||
// ---- 缠K ----
|
||||
"缠K合并替换",
|
||||
// ---- 笔 ----
|
||||
"笔内元素数量",
|
||||
"笔内相同终点取舍",
|
||||
"笔内起始分型包含整笔",
|
||||
@@ -371,33 +471,57 @@ impl 缠论配置 {
|
||||
"笔次级成笔",
|
||||
"笔弱化",
|
||||
"笔弱化_原始数量",
|
||||
// ---- 线段 ----
|
||||
"线段_非缺口下穿刺",
|
||||
"线段_特征序列忽视老阴老阳",
|
||||
"线段_缺口后紧急修正",
|
||||
"线段_修正",
|
||||
"线段内部中枢图显",
|
||||
"扩展线段_当下分析",
|
||||
// ---- 分析开关 ----
|
||||
"分析笔",
|
||||
"分析线段",
|
||||
"分析扩展线段",
|
||||
"分析笔中枢",
|
||||
"分析线段中枢",
|
||||
// ---- 终止 ----
|
||||
"手动终止",
|
||||
// ---- 指标 ----
|
||||
"计算指标",
|
||||
"计算BOLL",
|
||||
"指标计算方式",
|
||||
"平滑异同移动平均线_快线周期",
|
||||
"平滑异同移动平均线_慢线周期",
|
||||
"平滑异同移动平均线_信号周期",
|
||||
"相对强弱指数_周期",
|
||||
"相对强弱指数_移动平均线周期",
|
||||
"相对强弱指数_超买阈值",
|
||||
"相对强弱指数_超卖阈值",
|
||||
"随机指标_RSV周期",
|
||||
"随机指标_K值平滑周期",
|
||||
"随机指标_D值平滑周期",
|
||||
"随机指标_超买阈值",
|
||||
"随机指标_超卖阈值",
|
||||
"布林带_周期",
|
||||
"布林带_标准差倍数",
|
||||
"MACD_参数列表",
|
||||
"RSI_周期列表",
|
||||
"KDJ_参数列表",
|
||||
"BOLL_参数列表",
|
||||
"均线参数列表",
|
||||
// ---- 推送/显示 ----
|
||||
"均线_类型列表",
|
||||
"均线_周期列表",
|
||||
"图表展示",
|
||||
"图表展示标签",
|
||||
// ---- 买卖点 ----
|
||||
"推送K线",
|
||||
"推送笔",
|
||||
"推送线段",
|
||||
"推送中枢",
|
||||
"图表展示_笔",
|
||||
"图表展示_线段",
|
||||
"图表展示_扩展线段",
|
||||
"图表展示_扩展线段_线段",
|
||||
"图表展示_线段_线段",
|
||||
"图表展示_中枢_笔",
|
||||
"图表展示_中枢_线段",
|
||||
"图表展示_中枢_扩展线段",
|
||||
"图表展示_中枢_扩展线段_线段",
|
||||
"图表展示_中枢_线段_线段",
|
||||
"图表展示_中枢_线段内部",
|
||||
"买卖点偏移",
|
||||
"买卖点激进识别",
|
||||
"买卖点与MACD柱强相关",
|
||||
@@ -406,12 +530,10 @@ impl 缠论配置 {
|
||||
"买卖点_指标匹配_MACD",
|
||||
"买卖点_指标匹配_KDJ",
|
||||
"买卖点_指标匹配_RSI",
|
||||
// ---- 背驰 ----
|
||||
"线段内部背驰_MACD",
|
||||
"线段内部背驰_斜率",
|
||||
"线段内部背驰_测度",
|
||||
"线段内部背驰_模式",
|
||||
// ---- 文件 ----
|
||||
"加载文件路径",
|
||||
]
|
||||
}
|
||||
@@ -454,7 +576,21 @@ impl 缠论配置 {
|
||||
Self {
|
||||
线段内部中枢图显: false,
|
||||
图表展示: false,
|
||||
图表展示标签: Some(vec![]),
|
||||
推送K线: false,
|
||||
推送笔: false,
|
||||
推送线段: false,
|
||||
推送中枢: false,
|
||||
图表展示_笔: false,
|
||||
图表展示_线段: false,
|
||||
图表展示_扩展线段: false,
|
||||
图表展示_扩展线段_线段: false,
|
||||
图表展示_线段_线段: false,
|
||||
图表展示_中枢_笔: false,
|
||||
图表展示_中枢_线段: false,
|
||||
图表展示_中枢_扩展线段: false,
|
||||
图表展示_中枢_扩展线段_线段: false,
|
||||
图表展示_中枢_线段_线段: false,
|
||||
图表展示_中枢_线段内部: false,
|
||||
..self.clone()
|
||||
}
|
||||
}
|
||||
@@ -616,15 +752,18 @@ mod tests {
|
||||
fn test_model_copy() {
|
||||
let mut update = std::collections::HashMap::new();
|
||||
update.insert("标识".into(), serde_json::json!("custom"));
|
||||
update.insert("推送K线".into(), serde_json::json!(false));
|
||||
update.insert("笔内元素数量".into(), serde_json::json!(10));
|
||||
|
||||
let config = 缠论配置::default();
|
||||
let copied = config.model_copy(&update);
|
||||
|
||||
assert_eq!(copied.标识, "custom");
|
||||
assert!(!copied.推送K线);
|
||||
assert_eq!(copied.笔内元素数量, 10);
|
||||
// 未指定字段保持不变
|
||||
assert_eq!(copied.买卖点偏移, 1);
|
||||
assert!(copied.推送笔);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -640,8 +779,9 @@ mod tests {
|
||||
fn test_不推送() {
|
||||
let config = 缠论配置::default();
|
||||
let muted = config.不推送();
|
||||
assert!(!muted.推送K线);
|
||||
assert!(!muted.推送笔);
|
||||
assert!(!muted.图表展示);
|
||||
assert!(!muted.线段内部中枢图显);
|
||||
assert_eq!(muted.笔内元素数量, 5);
|
||||
}
|
||||
|
||||
@@ -716,12 +856,12 @@ mod tests {
|
||||
fn test_对比_boolean_difference() {
|
||||
let a = 缠论配置::default();
|
||||
let mut b = 缠论配置::default();
|
||||
b.分析笔 = false;
|
||||
b.推送K线 = false;
|
||||
b.图表展示 = false;
|
||||
|
||||
let diff = a.对比(&b);
|
||||
assert_eq!(diff.len(), 2);
|
||||
assert_eq!(diff["分析笔"], serde_json::json!(false));
|
||||
assert_eq!(diff["推送K线"], serde_json::json!(false));
|
||||
assert_eq!(diff["图表展示"], serde_json::json!(false));
|
||||
}
|
||||
|
||||
|
||||
@@ -32,56 +32,44 @@ use std::sync::Arc;
|
||||
pub struct 指标计算器;
|
||||
|
||||
impl 指标计算器 {
|
||||
/// 增量计算所有开启的指标,将结果写入每一根 K 线。
|
||||
/// 增量计算所有开启的指标,将结果写入 当前K线.指标
|
||||
///
|
||||
/// `全序列` 包含当前K线(在末尾);prev 取自 全序列[..-1].last()
|
||||
/// 通过 RwLock 内部可变性,以 `&K线` 共享引用写入指标值
|
||||
pub fn 计算并挂载(全序列: &[Arc<K线>], 配置: &缠论配置) {
|
||||
let n = 全序列.len();
|
||||
if n == 0 {
|
||||
return;
|
||||
}
|
||||
if !配置.计算指标 && 配置.均线参数列表.is_empty() {
|
||||
return;
|
||||
}
|
||||
let 当前K线 = &全序列[n - 1];
|
||||
let 现有序列 = if n > 1 { &全序列[..n - 1] } else { &[] };
|
||||
|
||||
// 找到第一个 MACD 缺失的 K 线索引,若全部已有则只处理最后一根
|
||||
let start = 全序列
|
||||
.iter()
|
||||
.position(|k| k.macd().is_none())
|
||||
.unwrap_or(n - 1);
|
||||
|
||||
for i in start..n {
|
||||
let 当前K线 = &全序列[i];
|
||||
let 现有序列 = &全序列[..i];
|
||||
|
||||
// 确保 prev guard 在写入当前K线前释放
|
||||
{
|
||||
let prev = if i > 0 {
|
||||
Some(全序列[i - 1].指标.read())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let prev_deref = prev.as_deref();
|
||||
|
||||
if 配置.计算指标 {
|
||||
Self::_计算MACD组(当前K线, prev_deref, 配置);
|
||||
Self::_计算RSI组(当前K线, prev_deref, 配置);
|
||||
Self::_计算KDJ组(当前K线, prev_deref, 配置);
|
||||
Self::_计算BOLL组(当前K线, prev_deref, 配置);
|
||||
}
|
||||
Self::_更新均线(当前K线, 现有序列, 配置);
|
||||
// prev guard dropped here
|
||||
// 作用域化 prev_guard:在 _回填新指标 之前释放,避免读锁与回填写锁冲突
|
||||
let has_prev;
|
||||
{
|
||||
let prev_guard = if n > 1 {
|
||||
Some(全序列[n - 2].指标.read())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let prev = prev_guard.as_deref();
|
||||
if 配置.计算指标 {
|
||||
Self::_计算MACD组(当前K线, prev, 配置);
|
||||
Self::_计算RSI组(当前K线, prev, 配置);
|
||||
Self::_计算KDJ组(当前K线, prev, 配置);
|
||||
Self::_计算BOLL组(当前K线, prev, 配置);
|
||||
}
|
||||
}
|
||||
Self::_更新均线(当前K线, 现有序列, 配置);
|
||||
has_prev = n > 1;
|
||||
} // prev_guard dropped here
|
||||
|
||||
// 回填:若有新增指标参数但首K线未被本轮计算覆盖,仍需填充历史K线
|
||||
if n > 1 && start > 0 {
|
||||
if has_prev {
|
||||
Self::_回填新指标(全序列, 配置);
|
||||
}
|
||||
}
|
||||
|
||||
fn _计算MACD组(当前K线: &K线, prev: Option<&指标容器>, 配置: &缠论配置) {
|
||||
for (key, 计算方式, 快, 慢, 信号) in 配置.MACD_参数列表.iter() {
|
||||
let val = if let Some(prev_val) = prev.and_then(|p| p.获取(key)) {
|
||||
let 计算方式 = &配置.指标计算方式;
|
||||
for (i, (key, 快, 慢, 信号)) in 配置._解析MACD参数列表().into_iter().enumerate()
|
||||
{
|
||||
let val = if let Some(prev_val) = prev.and_then(|p| p.获取(&key)) {
|
||||
if let 指标值::MACD(prev_macd) = prev_val {
|
||||
指标值::MACD(平滑异同移动平均线::增量计算(
|
||||
prev_macd,
|
||||
@@ -107,19 +95,22 @@ impl 指标计算器 {
|
||||
计算方式,
|
||||
),
|
||||
当前K线.时间戳,
|
||||
*快,
|
||||
*慢,
|
||||
*信号,
|
||||
快,
|
||||
慢,
|
||||
信号,
|
||||
))
|
||||
};
|
||||
当前K线.指标.write().设置(key, val.clone());
|
||||
当前K线.指标.write().设置(&key, val.clone());
|
||||
if i == 0 {
|
||||
当前K线.指标.write().设置("macd", val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn _计算RSI组(当前K线: &K线, prev: Option<&指标容器>, 配置: &缠论配置) {
|
||||
for (key, 计算方式, 周期, ma周期, 超买, 超卖) in 配置.RSI_周期列表.iter()
|
||||
{
|
||||
let val = if let Some(prev_val) = prev.and_then(|p| p.获取(key)) {
|
||||
let 计算方式 = &配置.指标计算方式;
|
||||
for (i, (key, 周期)) in 配置._解析RSI周期列表().into_iter().enumerate() {
|
||||
let val = if let Some(prev_val) = prev.and_then(|p| p.获取(&key)) {
|
||||
if let 指标值::RSI(prev_rsi) = prev_val {
|
||||
指标值::RSI(相对强弱指数::增量计算(
|
||||
prev_rsi,
|
||||
@@ -145,19 +136,23 @@ impl 指标计算器 {
|
||||
计算方式,
|
||||
),
|
||||
当前K线.时间戳,
|
||||
*周期,
|
||||
*超买,
|
||||
*超卖,
|
||||
Some(*ma周期),
|
||||
周期,
|
||||
配置.相对强弱指数_超买阈值,
|
||||
配置.相对强弱指数_超卖阈值,
|
||||
Some(配置.相对强弱指数_移动平均线周期),
|
||||
))
|
||||
};
|
||||
当前K线.指标.write().设置(key, val.clone());
|
||||
当前K线.指标.write().设置(&key, val.clone());
|
||||
if i == 0 {
|
||||
当前K线.指标.write().设置("rsi", val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn _计算KDJ组(当前K线: &K线, prev: Option<&指标容器>, 配置: &缠论配置) {
|
||||
for (key, _fm, rsv, k平滑, d平滑, 超买, 超卖) in 配置.KDJ_参数列表.iter() {
|
||||
let val = if let Some(prev_val) = prev.and_then(|p| p.获取(key)) {
|
||||
for (i, (key, rsv, k平滑, d平滑)) in 配置._解析KDJ参数列表().into_iter().enumerate()
|
||||
{
|
||||
let val = if let Some(prev_val) = prev.and_then(|p| p.获取(&key)) {
|
||||
if let 指标值::KDJ(prev_kdj) = prev_val {
|
||||
指标值::KDJ(随机指标::增量计算(
|
||||
prev_kdj,
|
||||
@@ -175,20 +170,25 @@ impl 指标计算器 {
|
||||
当前K线.低,
|
||||
当前K线.收盘价,
|
||||
当前K线.时间戳,
|
||||
*rsv,
|
||||
*k平滑,
|
||||
*d平滑,
|
||||
*超买,
|
||||
*超卖,
|
||||
rsv,
|
||||
k平滑,
|
||||
d平滑,
|
||||
配置.随机指标_超买阈值,
|
||||
配置.随机指标_超卖阈值,
|
||||
))
|
||||
};
|
||||
当前K线.指标.write().设置(key, val.clone());
|
||||
当前K线.指标.write().设置(&key, val.clone());
|
||||
if i == 0 {
|
||||
当前K线.指标.write().设置("kdj", val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn _计算BOLL组(当前K线: &K线, prev: Option<&指标容器>, 配置: &缠论配置) {
|
||||
for (key, 计算方式, 周期, 标准差倍数) in 配置.BOLL_参数列表.iter() {
|
||||
let val = if let Some(prev_val) = prev.and_then(|p| p.获取(key)) {
|
||||
let 计算方式 = &配置.指标计算方式;
|
||||
for (i, (key, 周期, 标准差倍数)) in 配置._解析BOLL参数列表().into_iter().enumerate()
|
||||
{
|
||||
let val = if let Some(prev_val) = prev.and_then(|p| p.获取(&key)) {
|
||||
if let 指标值::BOLL(prev_boll) = prev_val {
|
||||
指标值::BOLL(布林带::增量计算(
|
||||
prev_boll,
|
||||
@@ -214,37 +214,22 @@ impl 指标计算器 {
|
||||
当前K线.收盘价,
|
||||
计算方式,
|
||||
),
|
||||
*周期 as usize,
|
||||
*标准差倍数,
|
||||
周期 as usize,
|
||||
标准差倍数,
|
||||
))
|
||||
};
|
||||
当前K线.指标.write().设置(key, val.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn _更新均线(当前K线: &K线, 现有序列: &[Arc<K线>], 配置: &缠论配置) {
|
||||
if 配置.均线参数列表.is_empty() {
|
||||
return;
|
||||
}
|
||||
for (key, 计算方式, ma_type, period) in &配置.均线参数列表 {
|
||||
let 值 = match ma_type.as_str() {
|
||||
"SMA" => Self::_增量SMA(当前K线, 现有序列, 计算方式, *period, key),
|
||||
"EMA" => Self::_增量EMA(当前K线, 现有序列, 计算方式, *period, key),
|
||||
_ => continue,
|
||||
};
|
||||
if let Some(均线_map) = 当前K线.指标.write().均线_mut() {
|
||||
均线_map.insert(key.clone(), 值);
|
||||
当前K线.指标.write().设置(&key, val.clone());
|
||||
if i == 0 {
|
||||
当前K线.指标.write().设置("boll", val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn _增量SMA(
|
||||
当前K线: &K线,
|
||||
现有序列: &[Arc<K线>],
|
||||
计算方式: &str,
|
||||
period: i64,
|
||||
prev_key: &str,
|
||||
) -> f64 {
|
||||
fn _更新均线(当前K线: &K线, 现有序列: &[Arc<K线>], 配置: &缠论配置) {
|
||||
if 配置.均线_类型列表.is_empty() || 配置.均线_周期列表.is_empty() {
|
||||
return;
|
||||
}
|
||||
let 计算方式 = &配置.指标计算方式;
|
||||
let 当前价 = super::K线取值(
|
||||
当前K线.开盘价,
|
||||
当前K线.高,
|
||||
@@ -252,20 +237,46 @@ impl 指标计算器 {
|
||||
当前K线.收盘价,
|
||||
计算方式,
|
||||
);
|
||||
for ma_type in &配置.均线_类型列表 {
|
||||
for period in &配置.均线_周期列表 {
|
||||
let key = format!("{}_{}", ma_type, period);
|
||||
let 值 = match ma_type.as_str() {
|
||||
"SMA" => Self::_增量SMA(现有序列, 当前价, *period, 计算方式, &key),
|
||||
"EMA" => Self::_增量EMA(现有序列, 当前价, *period, 计算方式, &key),
|
||||
_ => continue,
|
||||
};
|
||||
if let Some(均线_map) = 当前K线.指标.write().均线_mut() {
|
||||
均线_map.insert(key, 值);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 增量 SMA: 现有序列 (不含当前K线) + 当前价
|
||||
fn _增量SMA(
|
||||
现有序列: &[Arc<K线>],
|
||||
当前价: f64,
|
||||
period: i64,
|
||||
计算方式: &str,
|
||||
prev_key: &str,
|
||||
) -> f64 {
|
||||
let existing_len = 现有序列.len();
|
||||
let p = period as usize;
|
||||
if existing_len < p {
|
||||
let sum: f64 = 现有序列
|
||||
// 现有序列 + 当前 = total_len
|
||||
let total_len = existing_len + 1;
|
||||
if total_len <= p {
|
||||
let mut sum: f64 = 现有序列[existing_len.saturating_sub(p.saturating_sub(1))..]
|
||||
.iter()
|
||||
.map(|k| super::K线取值(k.开盘价, k.高, k.低, k.收盘价, 计算方式))
|
||||
.sum::<f64>()
|
||||
+ 当前价;
|
||||
return sum / ((existing_len + 1) as f64).max(1.0);
|
||||
.sum();
|
||||
sum += 当前价;
|
||||
return sum / (total_len as f64).max(1.0);
|
||||
}
|
||||
if let Some(prev_sma) = 现有序列
|
||||
.last()
|
||||
.and_then(|k| k.指标.read().均线().and_then(|m| m.get(prev_key)).copied())
|
||||
{
|
||||
// 尝试从前一根K线获取缓存的SMA
|
||||
if let Some(prev) = 现有序列.last().and_then(|k| {
|
||||
let guard = k.指标.read();
|
||||
guard.均线().and_then(|m| m.get(prev_key)).copied()
|
||||
}) {
|
||||
let oldest = super::K线取值(
|
||||
现有序列[existing_len - p].开盘价,
|
||||
现有序列[existing_len - p].高,
|
||||
@@ -273,33 +284,29 @@ impl 指标计算器 {
|
||||
现有序列[existing_len - p].收盘价,
|
||||
计算方式,
|
||||
);
|
||||
return prev_sma + (当前价 - oldest) / period as f64;
|
||||
return prev + (当前价 - oldest) / period as f64;
|
||||
}
|
||||
let sum: f64 = 现有序列[existing_len.saturating_sub(p.saturating_sub(1))..]
|
||||
// 回退:完整计算
|
||||
let mut sum: f64 = 现有序列[existing_len.saturating_sub(p.saturating_sub(1))..]
|
||||
.iter()
|
||||
.map(|k| super::K线取值(k.开盘价, k.高, k.低, k.收盘价, 计算方式))
|
||||
.sum::<f64>()
|
||||
+ 当前价;
|
||||
sum / ((existing_len + 1) as f64).min(p as f64)
|
||||
.sum();
|
||||
sum += 当前价;
|
||||
sum / (total_len as f64).min(p as f64)
|
||||
}
|
||||
|
||||
/// 增量 EMA: 现有序列 (不含当前K线) + 当前价
|
||||
fn _增量EMA(
|
||||
当前K线: &K线,
|
||||
现有序列: &[Arc<K线>],
|
||||
计算方式: &str,
|
||||
当前价: f64,
|
||||
period: i64,
|
||||
_计算方式: &str,
|
||||
prev_key: &str,
|
||||
) -> f64 {
|
||||
let 当前价 = super::K线取值(
|
||||
当前K线.开盘价,
|
||||
当前K线.高,
|
||||
当前K线.低,
|
||||
当前K线.收盘价,
|
||||
计算方式,
|
||||
);
|
||||
let 前值 = 现有序列
|
||||
.last()
|
||||
.and_then(|k| k.指标.read().均线().and_then(|m| m.get(prev_key)).copied());
|
||||
let 前值 = 现有序列.last().and_then(|k| {
|
||||
let guard = k.指标.read();
|
||||
guard.均线().and_then(|m| m.get(prev_key)).copied()
|
||||
});
|
||||
match 前值 {
|
||||
None => 当前价,
|
||||
Some(prev) => {
|
||||
@@ -311,40 +318,42 @@ impl 指标计算器 {
|
||||
|
||||
/// 运行中新增指标参数时,回填所有历史K线
|
||||
fn _回填新指标(全序列: &[Arc<K线>], 配置: &缠论配置) {
|
||||
// 作用域化首尾读锁:在回填写循环之前释放,避免读锁与写锁冲突
|
||||
let (新MACD, 新RSI, 新KDJ, 新BOLL) = {
|
||||
let 首K_guard = 全序列[0].指标.read();
|
||||
let 尾K_guard = 全序列[全序列.len() - 1].指标.read();
|
||||
|
||||
let 新MACD: Vec<_> = 配置
|
||||
.MACD_参数列表
|
||||
.iter()
|
||||
.filter(|(key, ..)| 尾K_guard.包含(key) && !首K_guard.包含(key))
|
||||
.cloned()
|
||||
._解析MACD参数列表()
|
||||
.into_iter()
|
||||
.filter(|(key, _, _, _)| 尾K_guard.包含(key) && !首K_guard.包含(key))
|
||||
.collect();
|
||||
let 新RSI: Vec<_> = 配置
|
||||
.RSI_周期列表
|
||||
.iter()
|
||||
.filter(|(key, ..)| 尾K_guard.包含(key) && !首K_guard.包含(key))
|
||||
.cloned()
|
||||
._解析RSI周期列表()
|
||||
.into_iter()
|
||||
.filter(|(key, _)| 尾K_guard.包含(key) && !首K_guard.包含(key))
|
||||
.collect();
|
||||
let 新KDJ: Vec<_> = 配置
|
||||
.KDJ_参数列表
|
||||
.iter()
|
||||
.filter(|(key, ..)| 尾K_guard.包含(key) && !首K_guard.包含(key))
|
||||
.cloned()
|
||||
._解析KDJ参数列表()
|
||||
.into_iter()
|
||||
.filter(|(key, _, _, _)| 尾K_guard.包含(key) && !首K_guard.包含(key))
|
||||
.collect();
|
||||
let 新BOLL: Vec<_> = 配置
|
||||
.BOLL_参数列表
|
||||
.iter()
|
||||
.filter(|(key, ..)| 尾K_guard.包含(key) && !首K_guard.包含(key))
|
||||
.cloned()
|
||||
._解析BOLL参数列表()
|
||||
.into_iter()
|
||||
.filter(|(key, _, _)| 尾K_guard.包含(key) && !首K_guard.包含(key))
|
||||
.collect();
|
||||
|
||||
(新MACD, 新RSI, 新KDJ, 新BOLL)
|
||||
};
|
||||
}; // 首K_guard, 尾K_guard dropped here
|
||||
|
||||
if 新MACD.is_empty() && 新RSI.is_empty() && 新KDJ.is_empty() && 新BOLL.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let 计算方式 = &配置.指标计算方式;
|
||||
|
||||
// 从第一根K线开始逐根回填,每次只持有一根prev读锁
|
||||
for i in 0..全序列.len() {
|
||||
let k线 = &全序列[i];
|
||||
let prev_guard = if i > 0 {
|
||||
@@ -353,346 +362,115 @@ impl 指标计算器 {
|
||||
None
|
||||
};
|
||||
|
||||
for (key, 计算方式, 快, 慢, 信号) in &新MACD {
|
||||
let val = match prev_guard.as_ref().and_then(|p| p.获取(key)) {
|
||||
Some(指标值::MACD(prev_macd)) => 指标值::MACD(
|
||||
平滑异同移动平均线::增量计算_K线(prev_macd, k线, 计算方式),
|
||||
),
|
||||
_ => 指标值::MACD(平滑异同移动平均线::首次计算_K线(
|
||||
for (key, 快, 慢, 信号) in &新MACD {
|
||||
let val = if let Some(ref prev) = prev_guard {
|
||||
if let Some(指标值::MACD(prev_macd)) = prev.获取(key) {
|
||||
指标值::MACD(平滑异同移动平均线::增量计算_K线(
|
||||
prev_macd,
|
||||
k线,
|
||||
计算方式,
|
||||
))
|
||||
} else {
|
||||
指标值::MACD(平滑异同移动平均线::首次计算_K线(
|
||||
k线,
|
||||
计算方式,
|
||||
*快,
|
||||
*慢,
|
||||
*信号,
|
||||
))
|
||||
}
|
||||
} else {
|
||||
指标值::MACD(平滑异同移动平均线::首次计算_K线(
|
||||
k线,
|
||||
计算方式,
|
||||
*快,
|
||||
*慢,
|
||||
*信号,
|
||||
)),
|
||||
))
|
||||
};
|
||||
k线.指标.write().设置(key, val);
|
||||
}
|
||||
|
||||
for (key, 计算方式, 周期, ma周期, 超买, 超卖) in &新RSI {
|
||||
let val = match prev_guard.as_ref().and_then(|p| p.获取(key)) {
|
||||
Some(指标值::RSI(prev_rsi)) => 指标值::RSI(
|
||||
相对强弱指数::增量计算_K线(prev_rsi, k线, 计算方式),
|
||||
),
|
||||
_ => 指标值::RSI(相对强弱指数::首次计算_K线(
|
||||
for (key, 周期) in &新RSI {
|
||||
let val = if let Some(ref prev) = prev_guard {
|
||||
if let Some(指标值::RSI(prev_rsi)) = prev.获取(key) {
|
||||
指标值::RSI(相对强弱指数::增量计算_K线(
|
||||
prev_rsi,
|
||||
k线,
|
||||
计算方式,
|
||||
))
|
||||
} else {
|
||||
指标值::RSI(相对强弱指数::首次计算_K线(
|
||||
k线,
|
||||
计算方式,
|
||||
*周期,
|
||||
配置.相对强弱指数_超买阈值,
|
||||
配置.相对强弱指数_超卖阈值,
|
||||
Some(配置.相对强弱指数_移动平均线周期),
|
||||
))
|
||||
}
|
||||
} else {
|
||||
指标值::RSI(相对强弱指数::首次计算_K线(
|
||||
k线,
|
||||
计算方式,
|
||||
*周期,
|
||||
*超买,
|
||||
*超卖,
|
||||
Some(*ma周期),
|
||||
)),
|
||||
配置.相对强弱指数_超买阈值,
|
||||
配置.相对强弱指数_超卖阈值,
|
||||
Some(配置.相对强弱指数_移动平均线周期),
|
||||
))
|
||||
};
|
||||
k线.指标.write().设置(key, val);
|
||||
}
|
||||
|
||||
for (key, _fm, rsv, k平滑, d平滑, 超买, 超卖) in &新KDJ {
|
||||
let val = match prev_guard.as_ref().and_then(|p| p.获取(key)) {
|
||||
Some(指标值::KDJ(prev_kdj)) => {
|
||||
for (key, rsv, k平滑, d平滑) in &新KDJ {
|
||||
let val = if let Some(ref prev) = prev_guard {
|
||||
if let Some(指标值::KDJ(prev_kdj)) = prev.获取(key) {
|
||||
指标值::KDJ(随机指标::增量计算_K线(prev_kdj, k线))
|
||||
} else {
|
||||
指标值::KDJ(随机指标::首次计算_K线(
|
||||
k线,
|
||||
*rsv,
|
||||
*k平滑,
|
||||
*d平滑,
|
||||
配置.随机指标_超买阈值,
|
||||
配置.随机指标_超卖阈值,
|
||||
))
|
||||
}
|
||||
_ => 指标值::KDJ(随机指标::首次计算_K线(
|
||||
k线, *rsv, *k平滑, *d平滑, *超买, *超卖,
|
||||
)),
|
||||
} else {
|
||||
指标值::KDJ(随机指标::首次计算_K线(
|
||||
k线,
|
||||
*rsv,
|
||||
*k平滑,
|
||||
*d平滑,
|
||||
配置.随机指标_超买阈值,
|
||||
配置.随机指标_超卖阈值,
|
||||
))
|
||||
};
|
||||
k线.指标.write().设置(key, val);
|
||||
}
|
||||
|
||||
for (key, 计算方式, 周期, 标准差倍数) in &新BOLL {
|
||||
let val = match prev_guard.as_ref().and_then(|p| p.获取(key)) {
|
||||
Some(指标值::BOLL(prev_boll)) => {
|
||||
for (key, 周期, 标准差倍数) in &新BOLL {
|
||||
let val = if let Some(ref prev) = prev_guard {
|
||||
if let Some(指标值::BOLL(prev_boll)) = prev.获取(key) {
|
||||
指标值::BOLL(布林带::增量计算_K线(prev_boll, k线, 计算方式))
|
||||
} else {
|
||||
指标值::BOLL(布林带::首次计算_K线(
|
||||
k线,
|
||||
计算方式,
|
||||
*周期 as usize,
|
||||
*标准差倍数,
|
||||
))
|
||||
}
|
||||
_ => 指标值::BOLL(布林带::首次计算_K线(
|
||||
} else {
|
||||
指标值::BOLL(布林带::首次计算_K线(
|
||||
k线,
|
||||
计算方式,
|
||||
*周期 as usize,
|
||||
*标准差倍数,
|
||||
)),
|
||||
))
|
||||
};
|
||||
k线.指标.write().设置(key, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::缠论配置;
|
||||
use crate::kline::bar::K线;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// 辅助:创建一根模拟 K 线
|
||||
fn 模拟K线(时间戳: i64, 开: f64, 高: f64, 低: f64, 收: f64, 量: f64) -> Arc<K线> {
|
||||
Arc::new(K线::创建普K("TEST", 时间戳, 开, 高, 低, 收, 量, 0, 300))
|
||||
}
|
||||
|
||||
/// 辅助:生成连续上涨的 K 线序列(每根涨 ~1%)
|
||||
fn 生成上涨序列(n: usize, 起始时间: i64, 起始价: f64) -> Vec<Arc<K线>> {
|
||||
let mut seq = Vec::with_capacity(n);
|
||||
let mut price = 起始价;
|
||||
for i in 0..n {
|
||||
let 开 = price;
|
||||
let 收 = price * 1.005; // 上涨 0.5%
|
||||
let 高 = 收 * 1.002;
|
||||
let 低 = 开 * 0.998;
|
||||
let 量 = 1000.0 + i as f64 * 10.0;
|
||||
seq.push(模拟K线(起始时间 + i as i64 * 300, 开, 高, 低, 收, 量));
|
||||
price = 收;
|
||||
}
|
||||
seq
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_单根K线_首次计算_挂载成功() {
|
||||
let k线 = 模拟K线(1000, 100.0, 102.0, 98.0, 101.0, 500.0);
|
||||
let seq = vec![k线.clone()];
|
||||
let 配置 = 缠论配置::default();
|
||||
|
||||
指标计算器::计算并挂载(&seq, &配置);
|
||||
|
||||
// 单根 K 线首次计算:MACD DIF=0(EMA=SMA 初始近似),柱=0
|
||||
let m = k线.macd().expect("MACD 应已挂载");
|
||||
assert_eq!(m.DIF, Some(0.0), "首根K线 DIF 应为 0");
|
||||
assert_eq!(m.MACD柱, 0.0, "首根K线 MACD柱 应为 0");
|
||||
|
||||
// RSI 首次计算后 RSI 为 None(需至少一个增量步才有值)
|
||||
// 但指标容器应已注册 RSI 槽位,boll_cloned() 返回的是字段默认值
|
||||
assert!(k线.rsi().is_some(), "RSI 结构体应已创建(即使 RSI 字段为 None)");
|
||||
assert!(k线.kdj().is_some(), "KDJ 结构体应已创建(即使 K/D 字段为 None)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_多根K线_增量计算_指标值递推() {
|
||||
let seq = 生成上涨序列(5, 1000, 100.0);
|
||||
let 配置 = 缠论配置::default();
|
||||
|
||||
// 逐根计算(模拟流式投喂)
|
||||
for i in 0..seq.len() {
|
||||
指标计算器::计算并挂载(&seq[..=i], &配置);
|
||||
}
|
||||
|
||||
// 第 5 根 K 线的 MACD DIF 应 > 0(持续上涨)
|
||||
let last = &seq[seq.len() - 1];
|
||||
let m = last.macd().expect("最后一根K线 MACD 应已挂载");
|
||||
assert!(m.DIF.unwrap() > 0.0, "上涨序列 DIF 应为正");
|
||||
|
||||
// 所有 K 线均应有 MACD/RSI/KDJ
|
||||
for (i, k) in seq.iter().enumerate() {
|
||||
assert!(k.macd().is_some(), "K线[{i}] MACD 缺失");
|
||||
assert!(k.rsi().is_some(), "K线[{i}] RSI 缺失");
|
||||
assert!(k.kdj().is_some(), "K线[{i}] KDJ 缺失");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_指标未计算时_返回None() {
|
||||
let k线 = 模拟K线(1000, 100.0, 102.0, 98.0, 101.0, 500.0);
|
||||
// 未调用 计算并挂载 — 指标应为 None
|
||||
assert!(k线.macd().is_none(), "未计算时 MACD 应为 None");
|
||||
assert!(k线.rsi().is_none(), "未计算时 RSI 应为 None");
|
||||
assert!(k线.kdj().is_none(), "未计算时 KDJ 应为 None");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_回填新指标_新增参数后历史K线也挂载() {
|
||||
let seq = 生成上涨序列(3, 1000, 100.0);
|
||||
let 配置 = 缠论配置::default();
|
||||
|
||||
// 第一轮:只计算默认 macd 组
|
||||
指标计算器::计算并挂载(&seq[..=2], &配置);
|
||||
assert!(seq[2].macd().is_some());
|
||||
|
||||
// 第二轮:新增一组 MACD 参数,模拟用户后期追加指标
|
||||
let mut 配置2 = 配置.clone();
|
||||
配置2.MACD_参数列表.push(("extra_macd".into(), "收".into(), 5, 10, 3));
|
||||
指标计算器::计算并挂载(&seq[..=2], &配置2);
|
||||
|
||||
// 最后一根K线应同时有默认和 extra MACD
|
||||
let last = &seq[2];
|
||||
let guard = last.指标.read();
|
||||
assert!(guard.包含("macd"), "应有默认 macd");
|
||||
assert!(guard.包含("extra_macd"), "应有新指标 extra_macd");
|
||||
|
||||
// 回填:第一根 K 线也应被回填 extra_macd
|
||||
assert!(seq[0].指标.read().包含("extra_macd"), "回填后首根K线应有 extra_macd");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_多指标组_RSI_KDJ_BOLL_同时挂载() {
|
||||
let seq = 生成上涨序列(2, 1000, 100.0);
|
||||
let 配置 = 缠论配置::default();
|
||||
|
||||
指标计算器::计算并挂载(&seq[..=1], &配置);
|
||||
|
||||
let last = &seq[1];
|
||||
assert!(last.macd().is_some(), "MACD 应已挂载");
|
||||
assert!(last.rsi().is_some(), "RSI 应已挂载");
|
||||
assert!(last.kdj().is_some(), "KDJ 应已挂载");
|
||||
assert!(last.boll().is_some(), "BOLL 应已挂载");
|
||||
|
||||
// 验证 RSI 值的范围
|
||||
let r = last.rsi().unwrap();
|
||||
if let Some(rsi_val) = r.RSI {
|
||||
assert!((0.0..=100.0).contains(&rsi_val), "RSI 应在 0~100 之间, 实际={rsi_val}");
|
||||
}
|
||||
|
||||
// 验证 KDJ 值范围
|
||||
let k = last.kdj().unwrap();
|
||||
if let Some(k_val) = k.K {
|
||||
assert!((0.0..=100.0).contains(&k_val), "KDJ.K 应在 0~100 之间, 实际={k_val}");
|
||||
}
|
||||
|
||||
// BOLL 上轨 >= 中轨 >= 下轨
|
||||
let b = last.boll().unwrap();
|
||||
assert!(b.上轨 >= b.中轨, "BOLL 上轨({})应 >= 中轨({})", b.上轨, b.中轨);
|
||||
assert!(b.中轨 >= b.下轨, "BOLL 中轨({})应 >= 下轨({})", b.中轨, b.下轨);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_均线挂载() {
|
||||
let seq = 生成上涨序列(5, 1000, 100.0);
|
||||
let mut 配置 = 缠论配置::default();
|
||||
配置.均线参数列表 = vec![
|
||||
("SMA_3".into(), "收".into(), "SMA".into(), 3),
|
||||
];
|
||||
|
||||
指标计算器::计算并挂载(&seq[..=4], &配置);
|
||||
|
||||
let last = &seq[4];
|
||||
let ma_val = last.ma("SMA_3").expect("SMA_3 应已挂载");
|
||||
assert!(ma_val > 0.0, "SMA_3 应为正值");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_观察者集成_确保指标已计算() {
|
||||
use crate::business::observer::观察者;
|
||||
|
||||
let 观察员 = 观察者::new("TEST".into(), 300, 缠论配置::default());
|
||||
|
||||
// 逐根投喂
|
||||
for i in 0..5 {
|
||||
let price = 100.0 * (1.0 + i as f64 * 0.01);
|
||||
观察员.write().投喂原始数据(
|
||||
1000 + i as i64 * 300, price, price * 1.02, price * 0.98, price * 1.01, 1000.0,
|
||||
);
|
||||
}
|
||||
|
||||
// 确保指标已计算
|
||||
观察员.read().确保指标已计算();
|
||||
|
||||
let obs = 观察员.read();
|
||||
let klines = &obs.普通K线序列;
|
||||
assert!(!klines.is_empty(), "应有K线");
|
||||
|
||||
// 最后一根K线应有指标
|
||||
let last = &klines[klines.len() - 1];
|
||||
assert!(last.macd().is_some(), "观察者集成: MACD 应已挂载");
|
||||
assert!(last.rsi().is_some(), "观察者集成: RSI 应已挂载");
|
||||
assert!(last.kdj().is_some(), "观察者集成: KDJ 应已挂载");
|
||||
}
|
||||
|
||||
/// 50 根 K 线后,各指标应有稳定、合理的数值(非初始默认值)。
|
||||
#[test]
|
||||
fn test_50根K线_指标值稳定合理() {
|
||||
// 模拟 50 根有涨有跌的 K 线
|
||||
let mut seq = Vec::with_capacity(50);
|
||||
let mut price = 100.0;
|
||||
let mut rng: u64 = 42;
|
||||
for i in 0..50 {
|
||||
// 简单 LCG 随机 ±2% 波动
|
||||
rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
||||
let change = ((rng as f64 / u64::MAX as f64) - 0.5) * 0.04; // -2% ~ +2%
|
||||
let 收 = price * (1.0 + change);
|
||||
let 开 = price;
|
||||
let 高 = 开.max(收) * (1.0 + (rng % 100) as f64 / 10000.0);
|
||||
let 低 = 开.min(收) * (1.0 - (rng % 100) as f64 / 10000.0);
|
||||
let 量 = 500.0 + (rng % 500) as f64;
|
||||
seq.push(模拟K线(1000 + i as i64 * 300, 开, 高, 低, 收, 量));
|
||||
price = 收;
|
||||
}
|
||||
|
||||
let 配置 = 缠论配置::default();
|
||||
|
||||
// 逐根增量计算(模拟流式管线)
|
||||
for i in 0..seq.len() {
|
||||
指标计算器::计算并挂载(&seq[..=i], &配置);
|
||||
}
|
||||
|
||||
// ── 验证每根 K 线都有指标 ──
|
||||
for (i, k) in seq.iter().enumerate() {
|
||||
assert!(k.macd().is_some(), "K线[{i}] MACD 缺失");
|
||||
assert!(k.rsi().is_some(), "K线[{i}] RSI 缺失");
|
||||
assert!(k.kdj().is_some(), "K线[{i}] KDJ 缺失");
|
||||
assert!(k.boll().is_some(), "K线[{i}] BOLL 缺失");
|
||||
}
|
||||
|
||||
// ── 第 50 根 K 线(最后一根)的详细校验 ──
|
||||
let last = &seq[49];
|
||||
|
||||
// MACD
|
||||
let m = last.macd().unwrap();
|
||||
assert!(m.DIF.is_some(), "50根后 DIF 应有值");
|
||||
assert!(m.DEA.is_some(), "50根后 DEA 应有值");
|
||||
let dif = m.DIF.unwrap();
|
||||
let dea = m.DEA.unwrap();
|
||||
// DIF 和 DEA 不应同时为 0(50 根有波动数据 EMA 应已收敛)
|
||||
assert!(
|
||||
dif.abs() > 1e-9 || dea.abs() > 1e-9,
|
||||
"50根有波动数据 DIF/DEA 应非零, DIF={dif}, DEA={dea}"
|
||||
);
|
||||
// MACD 柱 = 2*(DIF-DEA),数量级合理
|
||||
let bar = m.MACD柱;
|
||||
assert!(bar.is_finite(), "MACD柱 应为有限值");
|
||||
assert!(bar.abs() < 100.0, "MACD柱 不应过大, 实际={bar}");
|
||||
|
||||
// RSI
|
||||
let r = last.rsi().unwrap();
|
||||
let rsi_val = r.RSI.expect("50根后 RSI 应有值");
|
||||
assert!((0.0..=100.0).contains(&rsi_val), "RSI 应在 0~100, 实际={rsi_val}");
|
||||
// 50 根随机数据 RSI 不应卡在极端值
|
||||
assert!(rsi_val > 0.1 && rsi_val < 99.9, "RSI 不应在极端值, 实际={rsi_val}");
|
||||
|
||||
// KDJ
|
||||
let kdj = last.kdj().unwrap();
|
||||
let k_val = kdj.K.expect("50根后 KDJ.K 应有值");
|
||||
let d_val = kdj.D.expect("50根后 KDJ.D 应有值");
|
||||
let j_val = kdj.J.expect("50根后 KDJ.J 应有值");
|
||||
assert!((0.0..=100.0).contains(&k_val), "KDJ.K 应在 0~100, 实际={k_val}");
|
||||
assert!((0.0..=100.0).contains(&d_val), "KDJ.D 应在 0~100, 实际={d_val}");
|
||||
// J = 3K - 2D,可能略超 [0,100]
|
||||
assert!(j_val.is_finite(), "KDJ.J 应为有限值");
|
||||
|
||||
// BOLL
|
||||
let b = last.boll().unwrap();
|
||||
assert!(b.上轨 > b.中轨 || b.中轨 > b.下轨,
|
||||
"50根波动数据 BOLL 带宽应 > 0, 上={:.4} 中={:.4} 下={:.4}",
|
||||
b.上轨, b.中轨, b.下轨);
|
||||
|
||||
// ── 中间节点验证:第 25 根 K 线所有指标也应有值 ──
|
||||
let mid = &seq[24];
|
||||
let m25 = mid.macd().unwrap();
|
||||
assert!(m25.DIF.is_some(), "第25根 DIF 应有值");
|
||||
assert!(mid.rsi().unwrap().RSI.is_some(), "第25根 RSI 应有值");
|
||||
assert!(mid.kdj().unwrap().K.is_some(), "第25根 KDJ.K 应有值");
|
||||
|
||||
// ── 印出第 50 根用于人工审查 ──
|
||||
println!(
|
||||
"=== 第 50 根 K线 指标状态 ===",
|
||||
);
|
||||
println!(
|
||||
" MACD: DIF={dif:.6} DEA={dea:.6} BAR={bar:.6}",
|
||||
);
|
||||
println!(
|
||||
" RSI: RSI={rsi_val:.4}",
|
||||
);
|
||||
println!(
|
||||
" KDJ: K={k_val:.4} D={d_val:.4} J={j_val:.4}",
|
||||
);
|
||||
println!(
|
||||
" BOLL: 上={:.4} 中={:.4} 下={:.4}",
|
||||
b.上轨, b.中轨, b.下轨,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
*/
|
||||
|
||||
use crate::indicators::指标容器;
|
||||
use crate::indicators::{布林带, 平滑异同移动平均线, 相对强弱指数, 随机指标};
|
||||
use crate::info;
|
||||
use crate::types::相对方向;
|
||||
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
||||
@@ -396,33 +395,6 @@ impl K线 {
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// ── 便捷指标访问(封装 RwLock<指标容器> boilerplate)──
|
||||
|
||||
/// 读取 MACD 指标(已计算则返回克隆,否则 None)
|
||||
pub fn macd(&self) -> Option<平滑异同移动平均线> {
|
||||
self.指标.read().macd_cloned()
|
||||
}
|
||||
|
||||
/// 读取 RSI 指标
|
||||
pub fn rsi(&self) -> Option<相对强弱指数> {
|
||||
self.指标.read().rsi_cloned()
|
||||
}
|
||||
|
||||
/// 读取 KDJ 指标
|
||||
pub fn kdj(&self) -> Option<随机指标> {
|
||||
self.指标.read().kdj_cloned()
|
||||
}
|
||||
|
||||
/// 读取 BOLL 指标
|
||||
pub fn boll(&self) -> Option<布林带> {
|
||||
self.指标.read().boll_cloned()
|
||||
}
|
||||
|
||||
/// 读取均线值,如 `ma("SMA_5")` → `Option<f64>`
|
||||
pub fn ma(&self, key: &str) -> Option<f64> {
|
||||
self.指标.read().均线().and_then(|m| m.get(key).copied())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for K线 {
|
||||
@@ -447,6 +419,7 @@ impl std::fmt::Display for K线 {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{error, info, warn};
|
||||
|
||||
#[test]
|
||||
fn test_方向() {
|
||||
|
||||
@@ -657,38 +657,6 @@ impl 缠论K线 {
|
||||
}
|
||||
(true, "缠论K线: 全部字段一致".into())
|
||||
}
|
||||
|
||||
// ── 便捷指标访问(委托给标的K线)──
|
||||
|
||||
/// 读取 MACD 指标
|
||||
pub fn macd(&self) -> Option<crate::indicators::平滑异同移动平均线> {
|
||||
self.标的K线.read().macd()
|
||||
}
|
||||
|
||||
/// 读取 RSI 指标
|
||||
pub fn rsi(&self) -> Option<crate::indicators::相对强弱指数> {
|
||||
self.标的K线.read().rsi()
|
||||
}
|
||||
|
||||
/// 读取 KDJ 指标
|
||||
pub fn kdj(&self) -> Option<crate::indicators::随机指标> {
|
||||
self.标的K线.read().kdj()
|
||||
}
|
||||
|
||||
/// 读取 BOLL 指标
|
||||
pub fn boll(&self) -> Option<crate::indicators::布林带> {
|
||||
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().收盘价
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::types::fractal::有高低 for 缠论K线 {
|
||||
|
||||
@@ -31,7 +31,6 @@ pub mod config;
|
||||
pub mod indicators;
|
||||
pub mod kline;
|
||||
pub mod log;
|
||||
pub mod signal;
|
||||
pub mod structure;
|
||||
pub mod types;
|
||||
pub mod utils;
|
||||
|
||||
@@ -1,27 +1,3 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2026 YuYuKunKun
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
|
||||
|
||||
/// 日志模式: 0=Off, 1=Simple (eprintln), 2=Tracing (tracing subscriber)
|
||||
|
||||
@@ -1,378 +0,0 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2026 YuYuKunKun
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
//! 信号计算引擎 — 通过 `SIGNAL_REGISTRY` 按名查找信号函数并执行。
|
||||
//!
|
||||
//! 第三方代码声明:引擎架构参考 czsc 的 `信号计算器`
|
||||
//!(https://github.com/waditu/czsc,Apache License 2.0),已适配为 Rust。
|
||||
//!
|
||||
//! # 示例
|
||||
//!
|
||||
//! ```ignore
|
||||
//! use chanlun::signal::engine::{SignalEngine, SignalConfig, call_signal};
|
||||
//!
|
||||
//! let engine = SignalEngine::new(vec![SignalConfig {
|
||||
//! signal_name: "youwukuncheng_中枢第三买卖点_V230602".into(),
|
||||
//! freq: 86400,
|
||||
//! params: params_map,
|
||||
//! }]);
|
||||
//! engine.自动挂载指标(&analyzer);
|
||||
//! let results = engine.更新(&analyzer);
|
||||
//! ```
|
||||
|
||||
use crate::business::multi_frame::立体分析器;
|
||||
use crate::business::observer::观察者;
|
||||
use crate::signal::Signal;
|
||||
use crate::signal::registry;
|
||||
use serde_json::Value;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
/// 单一信号配置项 — 对应 Python 信号配置列表中的一条。
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SignalConfig {
|
||||
/// 注册表中的信号名,如 `"youwukuncheng_中枢第三买卖点_V230602"`
|
||||
pub signal_name: String,
|
||||
/// 本配置作用的周期(秒)
|
||||
pub freq: i64,
|
||||
/// 信号参数(含 `freq`,统一为字符串以便 Rust 信号函数读取)
|
||||
pub params: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
/// 完整更新结果:信号字典 + 基础周期行情数据。
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct 完整更新结果 {
|
||||
/// 信号 key → value 映射
|
||||
pub signals: HashMap<String, String>,
|
||||
/// 基础周期最后一根 K 线的 OHLCV 数据(若无 K 线则为 None)
|
||||
pub market: Option<MarketData>,
|
||||
}
|
||||
|
||||
/// 基础周期行情数据 — 对应 Python `信号计算器.行情`。
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MarketData {
|
||||
pub symbol: String,
|
||||
pub dt: i64, // Unix 秒(K线时间戳)
|
||||
pub id: i64, // K线序号
|
||||
pub open: f64,
|
||||
pub high: f64,
|
||||
pub low: f64,
|
||||
pub close: f64,
|
||||
pub vol: f64,
|
||||
}
|
||||
|
||||
/// 信号计算引擎 — 持有配置列表,按 `&立体分析器` 执行。
|
||||
///
|
||||
/// 引擎不持有分析器引用——每次调用时传入,避免借用冲突。
|
||||
pub struct SignalEngine {
|
||||
configs: Vec<SignalConfig>,
|
||||
}
|
||||
|
||||
impl SignalEngine {
|
||||
/// 创建引擎。配置中的信号名延迟到 `更新()` 时校验。
|
||||
pub fn new(configs: Vec<SignalConfig>) -> Self {
|
||||
Self { configs }
|
||||
}
|
||||
|
||||
/// 返回当前配置数量
|
||||
pub fn len(&self) -> usize {
|
||||
self.configs.len()
|
||||
}
|
||||
|
||||
/// 配置是否为空
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.configs.is_empty()
|
||||
}
|
||||
|
||||
/// 扫描信号名中的 MACD / 均线关键字,向各周期 observer 的配置中
|
||||
/// 追加缺失的指标参数,然后调用 `确保指标已计算()`(幂等)。
|
||||
///
|
||||
/// 与 Python `_自动挂载指标()` 逻辑一致。
|
||||
pub fn 自动挂载指标(&self, analyzer: &立体分析器) {
|
||||
// 第一遍:按周期收集需要的参数
|
||||
let mut macd_by_freq: HashMap<i64, Vec<(String, i64, i64, i64)>> = HashMap::new();
|
||||
let mut ma_by_freq: HashMap<i64, Vec<(String, String, i64)>> = HashMap::new();
|
||||
|
||||
for cfg in &self.configs {
|
||||
let name_lower = cfg.signal_name.to_lowercase();
|
||||
|
||||
// MACD 信号检测
|
||||
if name_lower.contains("macd")
|
||||
|| name_lower.contains("中枢")
|
||||
|| name_lower.contains("背驰")
|
||||
|| name_lower.contains("金叉")
|
||||
{
|
||||
let fast = cfg
|
||||
.params
|
||||
.get("fast")
|
||||
.and_then(|v| v.as_i64())
|
||||
.or_else(|| cfg.params.get("快线周期").and_then(|v| v.as_i64()))
|
||||
.unwrap_or(13);
|
||||
let slow = cfg
|
||||
.params
|
||||
.get("slow")
|
||||
.and_then(|v| v.as_i64())
|
||||
.or_else(|| cfg.params.get("慢线周期").and_then(|v| v.as_i64()))
|
||||
.unwrap_or(31);
|
||||
let signal = cfg
|
||||
.params
|
||||
.get("signal")
|
||||
.and_then(|v| v.as_i64())
|
||||
.or_else(|| cfg.params.get("信号周期").and_then(|v| v.as_i64()))
|
||||
.unwrap_or(11);
|
||||
|
||||
let key = format!("macd_{fast}_{slow}_{signal}");
|
||||
macd_by_freq
|
||||
.entry(cfg.freq)
|
||||
.or_default()
|
||||
.push((key, fast, slow, signal));
|
||||
}
|
||||
|
||||
// 均线信号检测
|
||||
if name_lower.contains("ma_")
|
||||
|| name_lower.contains("tas_ma")
|
||||
|| name_lower.contains("均线")
|
||||
{
|
||||
let ma_type = cfg
|
||||
.params
|
||||
.get("ma_type")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("SMA")
|
||||
.to_uppercase();
|
||||
let period = cfg
|
||||
.params
|
||||
.get("timeperiod")
|
||||
.and_then(|v| v.as_i64())
|
||||
.or_else(|| cfg.params.get("周期").and_then(|v| v.as_i64()))
|
||||
.unwrap_or(5);
|
||||
|
||||
let key = format!("{ma_type}_{period}");
|
||||
ma_by_freq
|
||||
.entry(cfg.freq)
|
||||
.or_default()
|
||||
.push((key, ma_type, period));
|
||||
}
|
||||
}
|
||||
|
||||
// 第二遍:写入 observer 配置(先收集已有 key,再 drop 后写入)
|
||||
for (freq, entries) in &macd_by_freq {
|
||||
if let Some(obs_arc) = analyzer.获取观察者(*freq) {
|
||||
let needs_push: Vec<(String, String, i64, i64, i64)> = {
|
||||
let obs = obs_arc.read();
|
||||
let existing: HashSet<String> =
|
||||
obs.配置.MACD_参数列表.iter().map(|t| t.0.clone()).collect();
|
||||
entries
|
||||
.iter()
|
||||
.filter(|(key, _, _, _)| !existing.contains(key))
|
||||
.map(|(key, fast, slow, signal)| {
|
||||
(key.clone(), "收".to_string(), *fast, *slow, *signal)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
if !needs_push.is_empty() {
|
||||
let mut obs = obs_arc.write();
|
||||
for tuple in needs_push {
|
||||
obs.配置.MACD_参数列表.push(tuple);
|
||||
}
|
||||
obs.配置.计算指标 = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (freq, entries) in &ma_by_freq {
|
||||
if let Some(obs_arc) = analyzer.获取观察者(*freq) {
|
||||
let needs_push: Vec<(String, String, String, i64)> = {
|
||||
let obs = obs_arc.read();
|
||||
let existing: HashSet<String> =
|
||||
obs.配置.均线参数列表.iter().map(|t| t.0.clone()).collect();
|
||||
entries
|
||||
.iter()
|
||||
.filter(|(key, _, _)| !existing.contains(key))
|
||||
.map(|(key, ma_type, period)| {
|
||||
(key.clone(), "收".to_string(), ma_type.clone(), *period)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
if !needs_push.is_empty() {
|
||||
let mut obs = obs_arc.write();
|
||||
for tuple in needs_push {
|
||||
obs.配置.均线参数列表.push(tuple);
|
||||
}
|
||||
obs.配置.计算指标 = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 第三遍:确保所有周期观察者的指标已计算(幂等)
|
||||
for freq in &analyzer.周期组 {
|
||||
if let Some(obs_arc) = analyzer.获取观察者(*freq) {
|
||||
obs_arc.read().确保指标已计算();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 遍历所有配置,执行信号函数,收集非空结果。
|
||||
///
|
||||
/// 返回 `{信号key: 信号value}` 字典(已过滤 `"任意_任意_任意_0"`)。
|
||||
/// 缺失的 observer 或未注册信号名会通过 tracing::warn! 记录并跳过。
|
||||
pub fn 更新(&self, analyzer: &立体分析器) -> HashMap<String, String> {
|
||||
let mut results: HashMap<String, String> = HashMap::new();
|
||||
|
||||
for cfg in &self.configs {
|
||||
let obs_arc = match analyzer.获取观察者(cfg.freq) {
|
||||
Some(o) => o,
|
||||
None => {
|
||||
tracing::warn!("信号引擎: 未找到周期 {} 的观察者", cfg.freq);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let meta = match registry::get_signal(&cfg.signal_name) {
|
||||
Some(m) => m,
|
||||
None => {
|
||||
tracing::warn!("信号引擎: 信号未注册: {}", cfg.signal_name);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let signals = {
|
||||
let obs_guard = obs_arc.read();
|
||||
(meta.func)(&obs_guard, &cfg.params)
|
||||
};
|
||||
|
||||
for sig in signals {
|
||||
if sig.value() != "任意_任意_任意_0" {
|
||||
results.insert(sig.key(), sig.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
/// 运行信号计算并附带基础周期行情。
|
||||
///
|
||||
/// `base_freq` 使用分析器的第一个周期(最小周期)。
|
||||
/// 返回的 `完整更新结果` 可直接组合为 Python `信号字典` 格式。
|
||||
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_guard = obs.read();
|
||||
obs_guard.普通K线序列.last().map(|k| MarketData {
|
||||
symbol: obs_guard.符号.clone(),
|
||||
dt: k.时间戳,
|
||||
id: k.序号,
|
||||
open: k.开盘价,
|
||||
high: k.高,
|
||||
low: k.低,
|
||||
close: k.收盘价,
|
||||
vol: k.成交量,
|
||||
})
|
||||
});
|
||||
|
||||
完整更新结果 { signals, market }
|
||||
}
|
||||
}
|
||||
|
||||
/// 按名查找并调用单个信号函数。
|
||||
///
|
||||
/// 适用于已有 `&观察者` 的场景(测试、单周期分析),无需构造完整的 `SignalEngine`。
|
||||
pub fn call_signal(
|
||||
name: &str,
|
||||
obs: &观察者,
|
||||
params: &HashMap<String, Value>,
|
||||
) -> Result<Vec<Signal>, String> {
|
||||
let meta = registry::get_signal(name).ok_or_else(|| format!("信号未注册: {name}"))?;
|
||||
Ok((meta.func)(obs, params))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::缠论配置;
|
||||
|
||||
/// 通过 call_signal 调用 youwukuncheng 信号,验证产出格式。
|
||||
#[test]
|
||||
fn test_call_signal_youwukuncheng() {
|
||||
let nb_path = concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../templates/btcusd-86400-1608854400-1781568000.nb"
|
||||
);
|
||||
|
||||
let 观察员 = 观察者::new("btcusd".into(), 86400, 缠论配置::default());
|
||||
观察员
|
||||
.write()
|
||||
.读取数据文件(nb_path, 缠论配置::default().不推送())
|
||||
.expect("读取数据文件失败");
|
||||
|
||||
let obs = 观察员.read();
|
||||
// 信号函数内部会调用 确保指标已计算,但为稳妥先调用一次
|
||||
obs.确保指标已计算();
|
||||
|
||||
let mut params: HashMap<String, Value> = HashMap::new();
|
||||
params.insert("freq".into(), Value::String("日线".into()));
|
||||
params.insert(
|
||||
"max_overlap".into(),
|
||||
Value::Number(serde_json::Number::from(3)),
|
||||
);
|
||||
params.insert("本级完整性".into(), Value::String("实".into()));
|
||||
params.insert("同级完整性".into(), Value::String("合".into()));
|
||||
|
||||
let signals = call_signal("youwukuncheng_中枢第三买卖点_V230602", &obs, ¶ms)
|
||||
.expect("call_signal 应成功");
|
||||
|
||||
assert!(!signals.is_empty(), "至少应返回一个信号(可能是空)");
|
||||
for s in &signals {
|
||||
assert!(s.k3.ends_with("V230602"), "k3 应以 V230602 结尾: {}", s.k3);
|
||||
assert!((0..=100).contains(&s.score), "score 超范围: {}", s.score);
|
||||
}
|
||||
|
||||
// 验证非空信号
|
||||
let non_empty: Vec<_> = signals
|
||||
.iter()
|
||||
.filter(|s| s.value() != "任意_任意_任意_0")
|
||||
.collect();
|
||||
println!(
|
||||
"call_signal: {} signals, {} non-empty",
|
||||
signals.len(),
|
||||
non_empty.len()
|
||||
);
|
||||
for s in &non_empty {
|
||||
println!(" k3={} v1={} v2={} score={}", s.k3, s.v1, s.v2, s.score);
|
||||
}
|
||||
}
|
||||
|
||||
/// 空配置返回空结果
|
||||
#[test]
|
||||
fn test_engine_空配置_返回空() {
|
||||
use crate::business::multi_frame::立体分析器;
|
||||
|
||||
// 立体分析器 至少需要 2 个周期(周期组[0]=输入周期,周期组[1]=显示周期)
|
||||
let analyzer = 立体分析器::new("test".into(), vec![300, 900], None, None);
|
||||
let engine = SignalEngine::new(vec![]);
|
||||
let results = engine.更新(&analyzer);
|
||||
assert!(results.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2026 YuYuKunKun
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
//! 事件 — operate + 因子列表(任一因子满足则事件为真)。
|
||||
|
||||
use crate::signal::factor::Factor;
|
||||
use crate::signal::operate::Operate;
|
||||
use crate::signal::signal::Signal;
|
||||
use crate::signal::{sha256前4, 信号字典, 缺键错误};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Event {
|
||||
pub operate: Operate,
|
||||
pub factors: Vec<Factor>,
|
||||
pub signals_all: Vec<Signal>,
|
||||
pub signals_any: Vec<Signal>,
|
||||
pub signals_not: Vec<Signal>,
|
||||
pub name: String,
|
||||
pub sha256: String,
|
||||
}
|
||||
|
||||
impl Event {
|
||||
/// 构造。factors 为空 → Err。name 自动补哈希。
|
||||
pub fn 新建(
|
||||
operate: Operate,
|
||||
factors: Vec<Factor>,
|
||||
signals_all: Vec<Signal>,
|
||||
signals_any: Vec<Signal>,
|
||||
signals_not: Vec<Signal>,
|
||||
name: String,
|
||||
) -> Result<Self, String> {
|
||||
if factors.is_empty() {
|
||||
return Err("factors 不能为空".to_string());
|
||||
}
|
||||
let hash = Self::计算哈希(&factors, &signals_all, &signals_any, &signals_not);
|
||||
let name = if name.is_empty() {
|
||||
format!("{}#{hash}", operate.value())
|
||||
} else {
|
||||
format!("{}#{hash}", name.split('#').next().unwrap_or(""))
|
||||
};
|
||||
Ok(Self {
|
||||
operate,
|
||||
factors,
|
||||
signals_all,
|
||||
signals_any,
|
||||
signals_not,
|
||||
name,
|
||||
sha256: hash,
|
||||
})
|
||||
}
|
||||
|
||||
fn 计算哈希(factors: &[Factor], all: &[Signal], any: &[Signal], not: &[Signal]) -> String {
|
||||
let 取串 = |v: &[Signal]| {
|
||||
v.iter()
|
||||
.map(|s| s.signal.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
};
|
||||
let 因子串 = factors
|
||||
.iter()
|
||||
.map(|f| f.name.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join(";");
|
||||
let 规范 = format!(
|
||||
"factors=[{}]|all=[{}]|any=[{}]|not=[{}]",
|
||||
因子串,
|
||||
取串(all),
|
||||
取串(any),
|
||||
取串(not)
|
||||
);
|
||||
sha256前4(&规范)
|
||||
}
|
||||
|
||||
pub fn unique_signals(&self) -> Vec<String> {
|
||||
let mut 集合 = std::collections::BTreeSet::new();
|
||||
for s in self
|
||||
.signals_all
|
||||
.iter()
|
||||
.chain(&self.signals_any)
|
||||
.chain(&self.signals_not)
|
||||
{
|
||||
集合.insert(s.signal.clone());
|
||||
}
|
||||
for f in &self.factors {
|
||||
for s in f.unique_signals() {
|
||||
集合.insert(s);
|
||||
}
|
||||
}
|
||||
集合.into_iter().collect()
|
||||
}
|
||||
|
||||
/// 事件匹配。命中返回 (true, 因子名),否则 (false, None)。
|
||||
pub fn is_match(&self, 字典: &信号字典) -> Result<(bool, Option<String>), 缺键错误> {
|
||||
for s in &self.signals_not {
|
||||
if s.is_match(字典)? {
|
||||
return Ok((false, None));
|
||||
}
|
||||
}
|
||||
for s in &self.signals_all {
|
||||
if !s.is_match(字典)? {
|
||||
return Ok((false, None));
|
||||
}
|
||||
}
|
||||
if !self.signals_any.is_empty() {
|
||||
let mut 任一命中 = false;
|
||||
for s in &self.signals_any {
|
||||
if s.is_match(字典)? {
|
||||
任一命中 = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !任一命中 {
|
||||
return Ok((false, None));
|
||||
}
|
||||
}
|
||||
for f in &self.factors {
|
||||
if f.is_match(字典)? {
|
||||
return Ok((true, Some(f.name.clone())));
|
||||
}
|
||||
}
|
||||
Ok((false, None))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::signal::匹配值;
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn 信号(k3: &str, v2: &str) -> Signal {
|
||||
Signal::new("14400", "D1MO3", k3, "任意", v2, "任意", 0)
|
||||
}
|
||||
fn 因子(k3: &str, v2: &str) -> Factor {
|
||||
Factor::新建(vec![信号(k3, v2)], vec![], vec![], "".into()).unwrap()
|
||||
}
|
||||
fn 字典(k3: &str, v2: &str) -> HashMap<String, 匹配值> {
|
||||
let mut m = HashMap::new();
|
||||
m.insert(
|
||||
format!("14400_D1MO3_{k3}"),
|
||||
匹配值::字符串(format!("x_{v2}_y_100")),
|
||||
);
|
||||
m
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_factors_为空_报错() {
|
||||
assert!(Event::新建(Operate::开多, vec![], vec![], vec![], vec![], "".into()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_name_默认用operate值() {
|
||||
let e = Event::新建(
|
||||
Operate::开多,
|
||||
vec![因子("中枢", "三买")],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
"".into(),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(e.name.starts_with("开多#"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_任一因子命中() {
|
||||
let e = Event::新建(
|
||||
Operate::开多,
|
||||
vec![因子("中枢A", "三买"), 因子("中枢B", "三买")],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
"".into(),
|
||||
)
|
||||
.unwrap();
|
||||
// 两个因子的 key 都在字典:中枢A 在场但 v2=三卖 不匹配,中枢B v2=三买 匹配
|
||||
let mut d = HashMap::new();
|
||||
d.insert(
|
||||
"14400_D1MO3_中枢A".to_string(),
|
||||
匹配值::字符串("x_三卖_y_100".to_string()),
|
||||
);
|
||||
d.insert(
|
||||
"14400_D1MO3_中枢B".to_string(),
|
||||
匹配值::字符串("x_三买_y_100".to_string()),
|
||||
);
|
||||
let (命中, 名) = e.is_match(&d).unwrap();
|
||||
assert!(命中);
|
||||
assert!(名.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_无因子命中_false() {
|
||||
let e = Event::新建(
|
||||
Operate::开多,
|
||||
vec![因子("中枢", "三买")],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
"".into(),
|
||||
)
|
||||
.unwrap();
|
||||
let (命中, 名) = e.is_match(&字典("中枢", "三卖")).unwrap();
|
||||
assert!(!命中);
|
||||
assert!(名.is_none());
|
||||
}
|
||||
}
|
||||
@@ -1,175 +0,0 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2026 YuYuKunKun
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
//! 因子 — signals_all 全满足 + signals_any 任一满足 + signals_not 全不满足。
|
||||
|
||||
use crate::signal::signal::Signal;
|
||||
use crate::signal::{sha256前4, 信号字典, 缺键错误};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Factor {
|
||||
pub signals_all: Vec<Signal>,
|
||||
pub signals_any: Vec<Signal>,
|
||||
pub signals_not: Vec<Signal>,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Factor {
|
||||
/// 构造。signals_all 为空 → Err。name 自动补确定性哈希后缀。
|
||||
pub fn 新建(
|
||||
signals_all: Vec<Signal>,
|
||||
signals_any: Vec<Signal>,
|
||||
signals_not: Vec<Signal>,
|
||||
name: String,
|
||||
) -> Result<Self, String> {
|
||||
if signals_all.is_empty() {
|
||||
return Err("signals_all 不能为空".to_string());
|
||||
}
|
||||
let hash = Self::计算哈希(&signals_all, &signals_any, &signals_not);
|
||||
let 前缀 = name.split('#').next().unwrap_or("").to_string();
|
||||
let name = format!("{前缀}#{hash}");
|
||||
Ok(Self {
|
||||
signals_all,
|
||||
signals_any,
|
||||
signals_not,
|
||||
name,
|
||||
})
|
||||
}
|
||||
|
||||
/// 确定性哈希 — 拼接三组 signals 串后算 sha256 前4。
|
||||
fn 计算哈希(all: &[Signal], any: &[Signal], not: &[Signal]) -> String {
|
||||
let 取串 = |v: &[Signal]| {
|
||||
v.iter()
|
||||
.map(|s| s.signal.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
};
|
||||
let 规范 = format!(
|
||||
"all=[{}]|any=[{}]|not=[{}]",
|
||||
取串(all),
|
||||
取串(any),
|
||||
取串(not)
|
||||
);
|
||||
sha256前4(&规范)
|
||||
}
|
||||
|
||||
pub fn unique_signals(&self) -> Vec<String> {
|
||||
let mut 集合 = std::collections::BTreeSet::new();
|
||||
for s in self
|
||||
.signals_all
|
||||
.iter()
|
||||
.chain(&self.signals_any)
|
||||
.chain(&self.signals_not)
|
||||
{
|
||||
集合.insert(s.signal.clone());
|
||||
}
|
||||
集合.into_iter().collect()
|
||||
}
|
||||
|
||||
/// 因子匹配。任一信号缺键 → Err 向上传播。
|
||||
pub fn is_match(&self, 字典: &信号字典) -> Result<bool, 缺键错误> {
|
||||
for s in &self.signals_not {
|
||||
if s.is_match(字典)? {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
for s in &self.signals_all {
|
||||
if !s.is_match(字典)? {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
if self.signals_any.is_empty() {
|
||||
return Ok(true);
|
||||
}
|
||||
for s in &self.signals_any {
|
||||
if s.is_match(字典)? {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::signal::匹配值;
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn 信号(k3: &str, v2: &str) -> Signal {
|
||||
Signal::new("14400", "D1MO3", k3, "任意", v2, "任意", 0)
|
||||
}
|
||||
fn 字典(k3: &str, v2: &str) -> HashMap<String, 匹配值> {
|
||||
let mut m = HashMap::new();
|
||||
m.insert(
|
||||
format!("14400_D1MO3_{k3}"),
|
||||
匹配值::字符串(format!("x_{v2}_y_100")),
|
||||
);
|
||||
m
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_signals_all_为空_报错() {
|
||||
assert!(Factor::新建(vec![], vec![], vec![], "".into()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_name_含哈希后缀() {
|
||||
let f = Factor::新建(vec![信号("中枢", "三买")], vec![], vec![], "测试".into()).unwrap();
|
||||
assert!(f.name.starts_with("测试#"));
|
||||
assert_eq!(f.name.len(), "测试#".len() + 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_name_确定性() {
|
||||
let f1 = Factor::新建(vec![信号("中枢", "三买")], vec![], vec![], "".into()).unwrap();
|
||||
let f2 = Factor::新建(vec![信号("中枢", "三买")], vec![], vec![], "".into()).unwrap();
|
||||
assert_eq!(f1.name, f2.name);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_命中() {
|
||||
let f = Factor::新建(vec![信号("中枢", "三买")], vec![], vec![], "".into()).unwrap();
|
||||
assert_eq!(f.is_match(&字典("中枢", "三买")).unwrap(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_not_命中则false() {
|
||||
let f = Factor::新建(
|
||||
vec![信号("中枢", "三买")],
|
||||
vec![],
|
||||
vec![信号("中枢", "三买")],
|
||||
"".into(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(f.is_match(&字典("中枢", "三买")).unwrap(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_缺键传播错误() {
|
||||
let f = Factor::新建(vec![信号("中枢", "三买")], vec![], vec![], "".into()).unwrap();
|
||||
let m: HashMap<String, 匹配值> = HashMap::new();
|
||||
assert!(f.is_match(&m).is_err());
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2026 YuYuKunKun
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
//! C-ABI 导出 — 供动态加载的 .so 插件调用。
|
||||
//!
|
||||
//! 插件编译为 cdylib (`.so`),由 Python `ctypes.CDLL` 或 Rust `libloading` 加载。
|
||||
//! 加载后插件调用 `chanlun_register_signal` 向宿主进程的 `DYNAMIC_REGISTRY` 注册信号。
|
||||
//!
|
||||
//! # 插件约定
|
||||
//!
|
||||
//! 1. 插件 .so 的构造函数中调用 `chanlun_register_signal(name, template, func)`
|
||||
//! 2. `func` 是 `SignalFn` 类型的函数指针(`fn(&观察者, &HashMap<String, Value>) -> Vec<Signal>`)
|
||||
//! 3. 插件和宿主必须用相同 Rust 编译器版本编译
|
||||
|
||||
use std::ffi::CStr;
|
||||
use std::os::raw::c_char;
|
||||
|
||||
use crate::signal::registry::{self, SignalFn};
|
||||
|
||||
/// 宿主导出:供外部动态库调用的注册入口。
|
||||
///
|
||||
/// - `name`: 信号名(C 字符串)
|
||||
/// - `template`: 参数模板(C 字符串)
|
||||
/// - `func`: 函数指针(Rust 调用约定,插件与宿主须同编译器版本)
|
||||
///
|
||||
/// 返回 0 成功,非 0 失败。
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// `name` 和 `template` 必须是非空的合法 UTF-8 C 字符串指针。
|
||||
/// `func` 必须是合法的 `SignalFn` 函数指针(Rust 调用约定)。
|
||||
#[unsafe(no_mangle)]
|
||||
#[allow(improper_ctypes_definitions)]
|
||||
pub unsafe extern "C" fn chanlun_register_signal(
|
||||
name: *const c_char,
|
||||
template: *const c_char,
|
||||
func: SignalFn,
|
||||
) -> i32 {
|
||||
if name.is_null() || template.is_null() {
|
||||
return 1;
|
||||
}
|
||||
let name_str = unsafe { CStr::from_ptr(name) }.to_string_lossy();
|
||||
let template_str = unsafe { CStr::from_ptr(template) }.to_string_lossy();
|
||||
match registry::register_signal(&name_str, &template_str, func) {
|
||||
Ok(()) => 0,
|
||||
Err(_) => 2,
|
||||
}
|
||||
}
|
||||
|
||||
/// 宿主导出:从动态注册表移除信号。
|
||||
///
|
||||
/// 返回 0 成功,非 0 失败。
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// `name` 必须是非空的合法 UTF-8 C 字符串指针。
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn chanlun_unregister_signal(name: *const c_char) -> i32 {
|
||||
if name.is_null() {
|
||||
return 1;
|
||||
}
|
||||
let name_str = unsafe { CStr::from_ptr(name) }.to_string_lossy();
|
||||
match registry::unregister_signal(&name_str) {
|
||||
Ok(()) => 0,
|
||||
Err(_) => 2,
|
||||
}
|
||||
}
|
||||
|
||||
/// 查询已注册信号总数(编译时 + 动态)。
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// 此函数不接受任何指针参数,调用始终安全。
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn chanlun_list_signal_count() -> i32 {
|
||||
registry::list_signal_names().len() as i32
|
||||
}
|
||||
@@ -1,436 +0,0 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2026 YuYuKunKun
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
//! 示例信号函数 — 移植自 `chanlun-py/chanlun/signals/demo.py`。
|
||||
//!
|
||||
//! 第三方代码声明:信号函数模式参考 czsc(https://github.com/waditu/czsc,
|
||||
//! Apache License 2.0),已适配为 Rust。
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use chanlun_signal_macros::signal;
|
||||
|
||||
use crate::business::observer::观察者;
|
||||
use crate::kline::bar::K线;
|
||||
use crate::signal::Signal;
|
||||
use crate::signal::params;
|
||||
|
||||
// =============================================================================
|
||||
// bar — K线形态信号
|
||||
// =============================================================================
|
||||
|
||||
/// 涨跌停检测信号。
|
||||
///
|
||||
/// `close == high && close >= prev_close` → 涨停
|
||||
/// `close == low && close <= prev_close` → 跌停
|
||||
#[signal(name = "bar_zdt_V230331", template = "{freq}_D{di}_涨跌停V230331")]
|
||||
pub fn bar_zdt_V230331(obs: &观察者, params: &HashMap<String, Value>) -> Vec<Signal> {
|
||||
let di = params::get_int(params, "di", 1) as usize;
|
||||
let freq = params::get_string(params, "freq", "15分钟");
|
||||
let k1 = freq;
|
||||
let k2 = format!("D{di}");
|
||||
let k3 = "涨跌停V230331";
|
||||
|
||||
let 普K序列 = &obs.普通K线序列;
|
||||
if 普K序列.len() < di + 2 {
|
||||
return vec![Signal::new_empty(&k1, &k2, k3)];
|
||||
}
|
||||
|
||||
let 当前K线 = &普K序列[普K序列.len() - di];
|
||||
let 前K线 = &普K序列[普K序列.len() - di - 1];
|
||||
|
||||
let v1 = if 当前K线.收盘价 == 当前K线.高 && 当前K线.收盘价 >= 前K线.收盘价
|
||||
{
|
||||
"涨停"
|
||||
} else if 当前K线.收盘价 == 当前K线.低 && 当前K线.收盘价 <= 前K线.收盘价 {
|
||||
"跌停"
|
||||
} else {
|
||||
"任意"
|
||||
};
|
||||
|
||||
if v1 == "任意" {
|
||||
vec![Signal::new_empty(&k1, &k2, k3)]
|
||||
} else {
|
||||
vec![Signal::new(&k1, &k2, k3, v1, "任意", "任意", 0)]
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// tas — 技术指标信号
|
||||
// =============================================================================
|
||||
|
||||
/// MACD 金叉死叉信号 — DIF 与 DEA 的交叉判断。
|
||||
///
|
||||
/// DIF 上穿 DEA → 金叉;DIF 下穿 DEA → 死叉。
|
||||
#[signal(
|
||||
name = "macd_金叉_V260601",
|
||||
template = "{freq}_D{di}#MACD#{fast}#{slow}#{signal}_MACD交叉V260601"
|
||||
)]
|
||||
pub fn macd_金叉_V260601(obs: &观察者, params: &HashMap<String, Value>) -> Vec<Signal> {
|
||||
let fast = params::get_int(params, "fast", 13);
|
||||
let slow = params::get_int(params, "slow", 31);
|
||||
let signal_p = params::get_int(params, "signal", 11);
|
||||
let di = params::get_int(params, "di", 1) as usize;
|
||||
let freq = params::get_string(params, "freq", "15分钟");
|
||||
|
||||
let k1 = freq;
|
||||
let k2 = format!("D{di}#MACD#{fast}#{slow}#{signal_p}");
|
||||
let k3 = "MACD交叉V260601";
|
||||
|
||||
obs.确保指标已计算();
|
||||
|
||||
let 普K序列 = &obs.普通K线序列;
|
||||
if 普K序列.len() < di + 2 {
|
||||
return vec![Signal::new_empty(&k1, &k2, k3)];
|
||||
}
|
||||
|
||||
let 当前K线 = &普K序列[普K序列.len() - di];
|
||||
let 前K线 = &普K序列[普K序列.len() - di - 1];
|
||||
|
||||
let cur_dif = match 当前K线.macd().as_ref().and_then(|m| m.DIF) {
|
||||
Some(v) => v,
|
||||
None => return vec![Signal::new_empty(&k1, &k2, k3)],
|
||||
};
|
||||
let cur_dea = match 当前K线.macd().as_ref().and_then(|m| m.DEA) {
|
||||
Some(v) => v,
|
||||
None => return vec![Signal::new_empty(&k1, &k2, k3)],
|
||||
};
|
||||
let prev_dif = match 前K线.macd().as_ref().and_then(|m| m.DIF) {
|
||||
Some(v) => v,
|
||||
None => return vec![Signal::new_empty(&k1, &k2, k3)],
|
||||
};
|
||||
let prev_dea = match 前K线.macd().as_ref().and_then(|m| m.DEA) {
|
||||
Some(v) => v,
|
||||
None => return vec![Signal::new_empty(&k1, &k2, k3)],
|
||||
};
|
||||
|
||||
let v1 = if prev_dif <= prev_dea && cur_dif > cur_dea {
|
||||
"金叉"
|
||||
} else if prev_dif >= prev_dea && cur_dif < cur_dea {
|
||||
"死叉"
|
||||
} else {
|
||||
"任意"
|
||||
};
|
||||
|
||||
if v1 == "任意" {
|
||||
vec![Signal::new_empty(&k1, &k2, k3)]
|
||||
} else {
|
||||
vec![Signal::new(&k1, &k2, k3, v1, "任意", "任意", 0)]
|
||||
}
|
||||
}
|
||||
|
||||
/// MACD 方向信号 — DIF 在零轴上方为多头,下方为空头。
|
||||
#[signal(
|
||||
name = "tas_macd_direct_V221106",
|
||||
template = "{freq}_D{di}#MACD#{fast}#{slow}#{signal}_MACD方向V221106"
|
||||
)]
|
||||
pub fn tas_macd_direct_V221106(obs: &观察者, params: &HashMap<String, Value>) -> Vec<Signal> {
|
||||
let fast = params::get_int(params, "fast", 13);
|
||||
let slow = params::get_int(params, "slow", 31);
|
||||
let signal_p = params::get_int(params, "signal", 11);
|
||||
let di = params::get_int(params, "di", 1) as usize;
|
||||
let freq = params::get_string(params, "freq", "15分钟");
|
||||
|
||||
let k1 = freq;
|
||||
let k2 = format!("D{di}#MACD#{fast}#{slow}#{signal_p}");
|
||||
let k3 = "MACD方向V221106";
|
||||
|
||||
obs.确保指标已计算();
|
||||
|
||||
let 普K序列 = &obs.普通K线序列;
|
||||
if 普K序列.len() < di + 1 {
|
||||
return vec![Signal::new_empty(&k1, &k2, k3)];
|
||||
}
|
||||
|
||||
let 当前K线 = &普K序列[普K序列.len() - di];
|
||||
let cur_dif = match 当前K线.macd().as_ref().and_then(|m| m.DIF) {
|
||||
Some(v) => v,
|
||||
None => return vec![Signal::new_empty(&k1, &k2, k3)],
|
||||
};
|
||||
|
||||
let v1 = if cur_dif > 0.0 { "看多" } else { "看空" };
|
||||
|
||||
let v2 = if 普K序列.len() >= di + 2 {
|
||||
let 前K线 = &普K序列[普K序列.len() - di - 1];
|
||||
match 前K线.macd().as_ref().and_then(|m| m.DIF) {
|
||||
Some(prev_dif) => {
|
||||
if cur_dif > prev_dif {
|
||||
"向上"
|
||||
} else {
|
||||
"向下"
|
||||
}
|
||||
}
|
||||
None => "任意",
|
||||
}
|
||||
} else {
|
||||
"任意"
|
||||
};
|
||||
|
||||
vec![Signal::new(&k1, &k2, k3, v1, v2, "任意", 0)]
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 内部辅助 — 均线按需计算
|
||||
// =============================================================================
|
||||
|
||||
/// 按需计算均线值(SMA / EMA)。
|
||||
fn 计算均线(
|
||||
普K序列: &[std::sync::Arc<K线>],
|
||||
ma_type: &str,
|
||||
timeperiod: usize,
|
||||
offset: usize,
|
||||
) -> Option<f64> {
|
||||
let n = 普K序列.len();
|
||||
let start = n.checked_sub(offset + timeperiod)?;
|
||||
let end = n.checked_sub(offset)?;
|
||||
if start >= end {
|
||||
return None;
|
||||
}
|
||||
let closes: Vec<f64> = 普K序列[start..end].iter().map(|k| k.收盘价).collect();
|
||||
if closes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
match ma_type {
|
||||
"SMA" | "sma" => Some(closes.iter().sum::<f64>() / closes.len() as f64),
|
||||
"EMA" | "ema" => {
|
||||
let k = 2.0 / (timeperiod as f64 + 1.0);
|
||||
let mut ema = closes[0];
|
||||
for &price in &closes[1..] {
|
||||
ema = price * k + ema * (1.0 - k);
|
||||
}
|
||||
Some(ema)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 从均线缓存或按需计算获取均线值。
|
||||
fn 获取均线(
|
||||
普K序列: &[std::sync::Arc<K线>],
|
||||
k线: &K线,
|
||||
ma_type: &str,
|
||||
timeperiod: usize,
|
||||
offset: usize,
|
||||
) -> Option<f64> {
|
||||
let ma_key = format!("{}_{}", ma_type.to_uppercase(), timeperiod);
|
||||
if let Some(ma_map) = k线.ma(&ma_key) {
|
||||
return Some(ma_map);
|
||||
}
|
||||
计算均线(普K序列, ma_type, timeperiod, offset)
|
||||
}
|
||||
|
||||
/// 单均线多空和方向信号。
|
||||
#[signal(
|
||||
name = "tas_ma_base_V230313",
|
||||
template = "{freq}_D{di}#{ma_type}#{timeperiod}MO{max_overlap}_BS辅助V230313"
|
||||
)]
|
||||
pub fn tas_ma_base_V230313(obs: &观察者, params: &HashMap<String, Value>) -> Vec<Signal> {
|
||||
let ma_type = params::get_string(params, "ma_type", "SMA").to_uppercase();
|
||||
let timeperiod = params::get_int(params, "timeperiod", 5) as usize;
|
||||
let di = params::get_int(params, "di", 1) as usize;
|
||||
let max_overlap = params::get_int(params, "max_overlap", 5);
|
||||
let freq = params::get_string(params, "freq", "15分钟");
|
||||
|
||||
let k1 = freq;
|
||||
let k2 = format!("D{di}#{ma_type}#{timeperiod}MO{max_overlap}");
|
||||
let k3 = "BS辅助V230313";
|
||||
|
||||
let 普K序列 = &obs.普通K线序列;
|
||||
if 普K序列.len() < di + 1 {
|
||||
return vec![Signal::new_empty(&k1, &k2, k3)];
|
||||
}
|
||||
|
||||
let 当前K线 = &普K序列[普K序列.len() - di];
|
||||
let 当前均线 = match 获取均线(普K序列, 当前K线, &ma_type, timeperiod, di) {
|
||||
Some(v) => v,
|
||||
None => return vec![Signal::new_empty(&k1, &k2, k3)],
|
||||
};
|
||||
|
||||
let v1 = if 当前K线.收盘价 > 当前均线 {
|
||||
"看多"
|
||||
} else {
|
||||
"看空"
|
||||
};
|
||||
|
||||
let v2 = if 普K序列.len() >= di + 2 {
|
||||
let 前K线 = &普K序列[普K序列.len() - di - 1];
|
||||
match 获取均线(普K序列, 前K线, &ma_type, timeperiod, di + 1) {
|
||||
Some(前均线) => {
|
||||
if 当前均线 > 前均线 {
|
||||
"向上"
|
||||
} else {
|
||||
"向下"
|
||||
}
|
||||
}
|
||||
None => "任意",
|
||||
}
|
||||
} else {
|
||||
"任意"
|
||||
};
|
||||
|
||||
vec![Signal::new(&k1, &k2, k3, v1, v2, "任意", 0)]
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// cxt — 缠论形态信号
|
||||
// =============================================================================
|
||||
|
||||
/// 停顿分型辅助信号 — 结合分型强度和 MACD 柱子匹配判断。
|
||||
#[signal(
|
||||
name = "cxt_停顿分型_V230106",
|
||||
template = "{freq}_D{di}停顿分型_BE辅助V230106"
|
||||
)]
|
||||
pub fn cxt_停顿分型_V230106(obs: &观察者, params: &HashMap<String, Value>) -> Vec<Signal> {
|
||||
let di = params::get_int(params, "di", 0) as usize;
|
||||
let freq = params::get_string(params, "freq", "1分钟");
|
||||
|
||||
let k1 = freq;
|
||||
let k2 = format!("D{di}停顿分型");
|
||||
let k3 = "BE辅助V230106";
|
||||
|
||||
let 分型序列 = &obs.分型序列;
|
||||
if 分型序列.len() < di + 1 {
|
||||
return vec![Signal::new_empty(&k1, &k2, k3)];
|
||||
}
|
||||
|
||||
let 当前分型 = &分型序列[分型序列.len() - (di + 1)];
|
||||
|
||||
// 只对顶/底分型产出信号
|
||||
let 结构值 = 当前分型.结构.to_string();
|
||||
if 结构值 != "顶" && 结构值 != "底" {
|
||||
return vec![Signal::new_empty(&k1, &k2, k3)];
|
||||
}
|
||||
|
||||
let v1 = if 结构值 == "顶" {
|
||||
"看空"
|
||||
} else {
|
||||
"看多"
|
||||
};
|
||||
let v2 = 当前分型.强度();
|
||||
|
||||
// 仅强/中分型产出有效信号
|
||||
if v2 != "强" && v2 != "中" {
|
||||
return vec![Signal::new_empty(&k1, &k2, k3)];
|
||||
}
|
||||
|
||||
vec![Signal::new(&k1, &k2, k3, v1, v2, "任意", 0)]
|
||||
}
|
||||
|
||||
/// 笔结束辅助信号 — 统计最后笔之后的新高/新低分型次数。
|
||||
#[signal(
|
||||
name = "cxt_bi_end_V230222",
|
||||
template = "{freq}_D1MO{max_overlap}_BE辅助V230222"
|
||||
)]
|
||||
pub fn cxt_bi_end_V230222(obs: &观察者, params: &HashMap<String, Value>) -> Vec<Signal> {
|
||||
let max_overlap = params::get_int(params, "max_overlap", 3);
|
||||
let freq = params::get_string(params, "freq", "日线");
|
||||
|
||||
let k1 = freq;
|
||||
let k2 = format!("D1MO{max_overlap}");
|
||||
let k3 = "BE辅助V230222";
|
||||
|
||||
let 分型序列 = &obs.分型序列;
|
||||
let 笔序列 = &obs.笔序列;
|
||||
|
||||
if 分型序列.len() < 2 || 笔序列.is_empty() {
|
||||
return vec![Signal::new_empty(&k1, &k2, k3)];
|
||||
}
|
||||
|
||||
let 最后笔 = &笔序列[笔序列.len() - 1];
|
||||
let 当前分型 = &分型序列[分型序列.len() - 1];
|
||||
|
||||
// 找到最后笔的武(终点分型)在分型序列中的位置
|
||||
let 笔武分型: std::sync::Arc<crate::structure::fractal_obj::分型> =
|
||||
{ 最后笔.武.read().clone() };
|
||||
|
||||
let 笔终点时间戳 = 笔武分型.时间戳;
|
||||
let 笔终点结构 = 笔武分型.结构;
|
||||
|
||||
let 笔终点索引 = match 分型序列
|
||||
.iter()
|
||||
.position(|f| f.时间戳 == 笔终点时间戳 && f.结构 == 笔终点结构)
|
||||
{
|
||||
Some(idx) => idx,
|
||||
None => return vec![Signal::new_empty(&k1, &k2, k3)],
|
||||
};
|
||||
|
||||
// 取笔终点之后的分型
|
||||
if 笔终点索引 + 1 >= 分型序列.len() {
|
||||
return vec![Signal::new_empty(&k1, &k2, k3)];
|
||||
}
|
||||
let 未成笔分型 = &分型序列[笔终点索引 + 1..];
|
||||
|
||||
let 当前结构 = 当前分型.结构;
|
||||
let 当前分型特征值 = 当前分型.分型特征值;
|
||||
|
||||
if 当前结构.to_string() == "顶" {
|
||||
let mut 笔终点顶高 = 笔武分型.分型特征值;
|
||||
let mut 计数 = 0i32;
|
||||
for f in 未成笔分型 {
|
||||
if f.结构 == 当前结构 && f.分型特征值 > 笔终点顶高 {
|
||||
计数 += 1;
|
||||
笔终点顶高 = f.分型特征值;
|
||||
}
|
||||
}
|
||||
if 计数 > 0 && 当前分型特征值 >= 笔终点顶高 {
|
||||
vec![Signal::new(
|
||||
&k1,
|
||||
&k2,
|
||||
k3,
|
||||
"新高",
|
||||
&format!("第{计数}次"),
|
||||
"任意",
|
||||
0,
|
||||
)]
|
||||
} else {
|
||||
vec![Signal::new_empty(&k1, &k2, k3)]
|
||||
}
|
||||
} else if 当前结构.to_string() == "底" {
|
||||
let mut 笔终点底低 = 笔武分型.分型特征值;
|
||||
let mut 计数 = 0i32;
|
||||
for f in 未成笔分型 {
|
||||
if f.结构 == 当前结构 && f.分型特征值 < 笔终点底低 {
|
||||
计数 += 1;
|
||||
笔终点底低 = f.分型特征值;
|
||||
}
|
||||
}
|
||||
if 计数 > 0 && 当前分型特征值 <= 笔终点底低 {
|
||||
vec![Signal::new(
|
||||
&k1,
|
||||
&k2,
|
||||
k3,
|
||||
"新低",
|
||||
&format!("第{计数}次"),
|
||||
"任意",
|
||||
0,
|
||||
)]
|
||||
} else {
|
||||
vec![Signal::new_empty(&k1, &k2, k3)]
|
||||
}
|
||||
} else {
|
||||
vec![Signal::new_empty(&k1, &k2, k3)]
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2026 YuYuKunKun
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
//! 信号函数实现 — 每个 `#[signal]` 注册的函数对应一个子模块。
|
||||
//!
|
||||
//! 第三方代码声明:信号函数模式参考 czsc(https://github.com/waditu/czsc,
|
||||
//! Apache License 2.0),已适配为 Rust `fn(&观察者, &HashMap<String, Value>) -> Vec<Signal>`。
|
||||
|
||||
pub mod demo;
|
||||
// pub mod youwukuncheng;
|
||||
@@ -1,74 +0,0 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2026 YuYuKunKun
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
//! 信号匹配原语层。
|
||||
//!
|
||||
//! 第三方代码声明:本模块的 Signal/Factor/Event/Position/Operate 匹配框架
|
||||
//! 摘录自 czsc 项目(https://github.com/waditu/czsc),Apache License 2.0 授权,
|
||||
//! 已做中文命名适配与 Rust 重写。
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub mod engine;
|
||||
pub mod event;
|
||||
pub mod factor;
|
||||
pub mod ffi;
|
||||
pub mod functions;
|
||||
pub mod operate;
|
||||
pub mod params;
|
||||
pub mod position;
|
||||
pub mod registry;
|
||||
#[cfg(test)]
|
||||
mod registry_macro_test;
|
||||
#[allow(clippy::module_inception)]
|
||||
pub mod signal;
|
||||
|
||||
pub use event::Event;
|
||||
pub use factor::Factor;
|
||||
pub use operate::Operate;
|
||||
pub use position::Position;
|
||||
pub use signal::Signal;
|
||||
|
||||
/// 信号字典中某个 key 对应的值。区分「字符串」与「非字符串」,
|
||||
/// 以在纯 Rust 内表达 Python `is_match` 的三态:缺键 / 非 str / str。
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum 匹配值 {
|
||||
字符串(String),
|
||||
非字符串,
|
||||
}
|
||||
|
||||
/// 信号字典类型别名。
|
||||
pub type 信号字典 = HashMap<String, 匹配值>;
|
||||
|
||||
/// 缺键错误 — `is_match` 在信号字典中找不到 key 时返回。
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct 缺键错误(pub String);
|
||||
|
||||
/// 对任意字节串算 sha256,取大写十六进制前 4 位(= 前 2 字节)。
|
||||
/// 对应 Python `hashlib.sha256(...).hexdigest().upper()[:4]`。
|
||||
pub(crate) fn sha256前4(输入: &str) -> String {
|
||||
use sha2::{Digest, Sha256};
|
||||
let 摘要 = Sha256::digest(输入.as_bytes());
|
||||
format!("{:02X}{:02X}", 摘要[0], 摘要[1])
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2026 YuYuKunKun
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
//! 缠论买卖操作类型。
|
||||
|
||||
/// 持仓/操作类型。值对应中文,与 Python `chan_external.Operate` 一致。
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Operate {
|
||||
持多, // HL
|
||||
持空, // HS
|
||||
持币, // HO
|
||||
开多, // LO
|
||||
平多, // LE
|
||||
开空, // SO
|
||||
平空, // SE
|
||||
}
|
||||
|
||||
impl Operate {
|
||||
/// 中文值,对应 Python Enum 的 `.value`。
|
||||
pub fn value(&self) -> &'static str {
|
||||
match self {
|
||||
Operate::持多 => "持多",
|
||||
Operate::持空 => "持空",
|
||||
Operate::持币 => "持币",
|
||||
Operate::开多 => "开多",
|
||||
Operate::平多 => "平多",
|
||||
Operate::开空 => "开空",
|
||||
Operate::平空 => "平空",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_operate_value() {
|
||||
assert_eq!(Operate::开多.value(), "开多");
|
||||
assert_eq!(Operate::平空.value(), "平空");
|
||||
assert_eq!(Operate::持币.value(), "持币");
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2026 YuYuKunKun
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
//! 信号函数参数提取辅助 — 从 `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)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,282 +0,0 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2026 YuYuKunKun
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
//! 信号注册表 —— 编译期收集 + 运行时动态注册。
|
||||
//!
|
||||
//! 第三方代码声明:注册机制参考 czsc(https://github.com/waditu/czsc,
|
||||
//! Apache License 2.0),已简化适配(无 category / TaCache)。
|
||||
//!
|
||||
//! # 双注册表架构
|
||||
//!
|
||||
//! - `SIGNAL_REGISTRY`: 编译时,`#[signal]` 宏 + `inventory`,不可变。
|
||||
//! - `DYNAMIC_REGISTRY`: 运行时,`register_signal()` / `unregister_signal()`,`RwLock`。
|
||||
//!
|
||||
//! 查找时先查编译时,再查动态。同名时编译时优先(动态注册被遮蔽)。
|
||||
|
||||
use crate::business::observer::观察者;
|
||||
use crate::signal::Signal;
|
||||
use parking_lot::RwLock;
|
||||
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 {
|
||||
pub name: &'static str,
|
||||
pub template: &'static str,
|
||||
pub func: SignalFn,
|
||||
}
|
||||
|
||||
inventory::collect!(SignalDescriptor);
|
||||
|
||||
/// 运行时信号元信息(编译时和动态共用)。
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SignalMeta {
|
||||
pub func: SignalFn,
|
||||
pub template: String,
|
||||
}
|
||||
|
||||
/// 归并描述符为注册表;重名返回 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.to_string(),
|
||||
},
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
return Err(format!("信号重名:{}", d.name));
|
||||
}
|
||||
}
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
/// 编译时注册表(`#[signal]` 宏,inventory 收集,不可变)。
|
||||
pub static SIGNAL_REGISTRY: LazyLock<HashMap<&'static str, SignalMeta>> = LazyLock::new(|| {
|
||||
归并(inventory::iter::<SignalDescriptor>.into_iter().copied()).unwrap_or_else(|e| panic!("{e}"))
|
||||
});
|
||||
|
||||
/// 动态注册表(运行时注册,RwLock)。
|
||||
pub static DYNAMIC_REGISTRY: LazyLock<RwLock<HashMap<String, SignalMeta>>> =
|
||||
LazyLock::new(|| RwLock::new(HashMap::new()));
|
||||
|
||||
// ============================================================================
|
||||
// 查询 API
|
||||
// ============================================================================
|
||||
|
||||
/// 按名查信号元信息。先查编译时,再查动态。
|
||||
pub fn get_signal(name: &str) -> Option<SignalMeta> {
|
||||
if let Some(m) = SIGNAL_REGISTRY.get(name) {
|
||||
return Some(m.clone());
|
||||
}
|
||||
DYNAMIC_REGISTRY.read().get(name).cloned()
|
||||
}
|
||||
|
||||
/// 按名查参数模板。
|
||||
pub fn get_template(name: &str) -> Option<String> {
|
||||
get_signal(name).map(|m| m.template)
|
||||
}
|
||||
|
||||
/// 列出所有已注册信号名(编译时 + 动态,排序)。
|
||||
pub fn list_signal_names() -> Vec<String> {
|
||||
let mut v: Vec<String> = SIGNAL_REGISTRY.keys().map(|k| k.to_string()).collect();
|
||||
for k in DYNAMIC_REGISTRY.read().keys() {
|
||||
if !SIGNAL_REGISTRY.contains_key(k.as_str()) {
|
||||
v.push(k.clone());
|
||||
}
|
||||
}
|
||||
v.sort();
|
||||
v
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 动态注册 API
|
||||
// ============================================================================
|
||||
|
||||
/// 运行时动态注册信号函数。
|
||||
///
|
||||
/// - `name`: 信号名(必须全局唯一)
|
||||
/// - `template`: 参数模板,如 `"{freq}_D{di}_涨跌停V230331"`
|
||||
/// - `func`: 信号函数指针
|
||||
///
|
||||
/// 返回 `Err` 如果同名信号已存在于编译时或动态注册表中。
|
||||
pub fn register_signal(name: &str, template: &str, func: SignalFn) -> Result<(), String> {
|
||||
if SIGNAL_REGISTRY.contains_key(name) {
|
||||
return Err(format!("信号 '{name}' 已在编译时注册表中,无法覆盖"));
|
||||
}
|
||||
let mut dyn_reg = DYNAMIC_REGISTRY.write();
|
||||
if dyn_reg.contains_key(name) {
|
||||
return Err(format!("信号 '{name}' 已在动态注册表中"));
|
||||
}
|
||||
dyn_reg.insert(
|
||||
name.to_string(),
|
||||
SignalMeta {
|
||||
func,
|
||||
template: template.to_string(),
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 从动态注册表中移除信号。返回 `Err` 如果信号不存在或属于编译时注册表。
|
||||
pub fn unregister_signal(name: &str) -> Result<(), String> {
|
||||
if SIGNAL_REGISTRY.contains_key(name) {
|
||||
return Err(format!("信号 '{name}' 属于编译时注册表,无法动态移除"));
|
||||
}
|
||||
let mut dyn_reg = DYNAMIC_REGISTRY.write();
|
||||
if dyn_reg.remove(name).is_none() {
|
||||
return Err(format!("信号 '{name}' 不在动态注册表中"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[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("信号重名"));
|
||||
}
|
||||
|
||||
// ── 动态注册测试 ──
|
||||
|
||||
#[test]
|
||||
fn test_动态注册成功() {
|
||||
assert!(register_signal("__dyn_test_V000001", "{freq}_D1_test", __probe).is_ok());
|
||||
let meta = get_signal("__dyn_test_V000001").unwrap();
|
||||
assert_eq!(meta.template, "{freq}_D1_test");
|
||||
// 清理
|
||||
unregister_signal("__dyn_test_V000001").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_动态重名_报错() {
|
||||
register_signal("__dyn_dup_V000001", "{freq}_D1_a", __probe).unwrap();
|
||||
let r = register_signal("__dyn_dup_V000001", "{freq}_D1_b", __probe);
|
||||
assert!(r.is_err());
|
||||
assert!(r.unwrap_err().contains("已在动态注册表中"));
|
||||
unregister_signal("__dyn_dup_V000001").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_动态覆盖编译时_报错() {
|
||||
// 编译时已注册的信号不允许动态覆盖
|
||||
let r = register_signal("__probe_inventory_V000000", "{freq}_test", __probe);
|
||||
assert!(r.is_err());
|
||||
assert!(r.unwrap_err().contains("已"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_注销_成功() {
|
||||
register_signal("__dyn_rm_V000001", "{freq}_D1_rm", __probe).unwrap();
|
||||
assert!(get_signal("__dyn_rm_V000001").is_some());
|
||||
unregister_signal("__dyn_rm_V000001").unwrap();
|
||||
assert!(get_signal("__dyn_rm_V000001").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_注销编译时_报错() {
|
||||
let r = unregister_signal("__probe_inventory_V000000");
|
||||
assert!(r.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_包含动态信号() {
|
||||
register_signal("__list_dyn_V000001", "{freq}_test", __probe).unwrap();
|
||||
let names = list_signal_names();
|
||||
assert!(names.contains(&"__list_dyn_V000001".to_string()));
|
||||
// 编译时信号也在
|
||||
assert!(names.contains(&"__probe_inventory_V000000".to_string()));
|
||||
unregister_signal("__list_dyn_V000001").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_动态优先_编译时不遮蔽() {
|
||||
// 编译时信号正常返回
|
||||
let meta = get_signal("__probe_inventory_V000000").unwrap();
|
||||
assert_eq!(meta.template, "{freq}_D1_probe_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".to_string())
|
||||
);
|
||||
assert!(list_signal_names().contains(&"__probe_inventory_V000000".to_string()));
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2026 YuYuKunKun
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
//! 端到端:#[signal] 宏 + inventory 注册表协同。
|
||||
//! 放在 chanlun lib 内(非 tests/ 外部集成测试),因 #[signal] emit 的是
|
||||
//! `crate::signal::registry::` 路径,只有在 chanlun crate 内才能解析。
|
||||
#![cfg(test)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::business::observer::观察者;
|
||||
use crate::signal::Signal;
|
||||
use crate::signal::registry::{get_signal, get_template, list_signal_names};
|
||||
use chanlun_signal_macros::signal;
|
||||
use serde_json::Value;
|
||||
|
||||
/// 探针信号函数:贴 #[signal] 后应被自动注册进 SIGNAL_REGISTRY。
|
||||
#[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_宏自动注册到全局表() {
|
||||
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".to_string())
|
||||
);
|
||||
assert!(list_signal_names().contains(&"test_probe_signal_V230101".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_未注册信号返回None() {
|
||||
assert!(get_signal("不存在的信号_V999999").is_none());
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2026 YuYuKunKun
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
//! 信号原语 — k1_k2_k3_v1_v2_v3_score 七段字符串。
|
||||
|
||||
use crate::signal::{信号字典, 匹配值, 缺键错误};
|
||||
|
||||
pub(crate) const 任意: &str = "任意";
|
||||
|
||||
/// 单个信号。字段与 Python `chan_external.Signal` 一致。
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Signal {
|
||||
pub signal: String,
|
||||
pub score: i32,
|
||||
pub k1: String,
|
||||
pub k2: String,
|
||||
pub k3: String,
|
||||
pub v1: String,
|
||||
pub v2: String,
|
||||
pub v3: String,
|
||||
}
|
||||
|
||||
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 {
|
||||
let signal = format!("{k1}_{k2}_{k3}_任意_任意_任意_0");
|
||||
Self {
|
||||
signal,
|
||||
score: 0,
|
||||
k1: k1.to_string(),
|
||||
k2: k2.to_string(),
|
||||
k3: k3.to_string(),
|
||||
v1: "任意".to_string(),
|
||||
v2: "任意".to_string(),
|
||||
v3: "任意".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建带分类值的信号(便捷构造器,score 自动钳制到 0..100)。
|
||||
pub fn new(k1: &str, k2: &str, k3: &str, v1: &str, v2: &str, v3: &str, score: i32) -> Self {
|
||||
let score = score.clamp(0, 100);
|
||||
let signal = format!("{k1}_{k2}_{k3}_{v1}_{v2}_{v3}_{score}");
|
||||
Self {
|
||||
signal,
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
/// key — k1/k2/k3 中非「任意」部分用 _ 连接。
|
||||
pub fn key(&self) -> String {
|
||||
[&self.k1, &self.k2, &self.k3]
|
||||
.iter()
|
||||
.filter(|k| k.as_str() != 任意)
|
||||
.map(|k| k.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join("_")
|
||||
}
|
||||
|
||||
/// value — v1_v2_v3_score。
|
||||
pub fn value(&self) -> String {
|
||||
format!("{}_{}_{}_{}", self.v1, self.v2, self.v3, self.score)
|
||||
}
|
||||
|
||||
/// 纯值匹配 — 给定信号字典里取到的 value 串(v1_v2_v3_score),判断是否匹配。
|
||||
pub fn is_match_value(&self, 值: &str) -> bool {
|
||||
let parts: Vec<&str> = 值.split('_').collect();
|
||||
if parts.len() != 4 {
|
||||
return false;
|
||||
}
|
||||
let (v1, v2, v3, score_str) = (parts[0], parts[1], parts[2], parts[3]);
|
||||
let score: i32 = score_str.parse().unwrap_or(0);
|
||||
score >= self.score
|
||||
&& (v1 == self.v1 || self.v1 == 任意)
|
||||
&& (v2 == self.v2 || self.v2 == 任意)
|
||||
&& (v3 == self.v3 || self.v3 == 任意)
|
||||
}
|
||||
|
||||
/// 在信号字典中匹配。缺键 → Err(对应 Python raise ValueError),
|
||||
/// 非字符串值 → Ok(false),字符串值 → 走 is_match_value。
|
||||
pub fn is_match(&self, 字典: &信号字典) -> Result<bool, 缺键错误> {
|
||||
let key = self.key();
|
||||
match 字典.get(&key) {
|
||||
None => Err(缺键错误(key)),
|
||||
Some(匹配值::非字符串) => Ok(false),
|
||||
Some(匹配值::字符串(v)) => Ok(self.is_match_value(v)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[test]
|
||||
fn test_score_越界_钳制() {
|
||||
// new() 自动将越界 score 钳制到 0~100
|
||||
let s = Signal::new("a", "b", "c", "d", "e", "f", 101);
|
||||
assert_eq!(s.score, 100);
|
||||
let s = Signal::new("a", "b", "c", "d", "e", "f", -1);
|
||||
assert_eq!(s.score, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_key_过滤任意() {
|
||||
let s = Signal::new("14400", "D1MO3", "中枢", 任意, "三买", 任意, 0);
|
||||
assert_eq!(s.key(), "14400_D1MO3_中枢");
|
||||
|
||||
// k1=任意 时 key 只剩 k2_k3
|
||||
let s2 = Signal::new(任意, "D1MO3", "中枢", 任意, "三买", 任意, 0);
|
||||
assert_eq!(s2.key(), "D1MO3_中枢");
|
||||
|
||||
// 全「任意」时 key 为空串
|
||||
let s3 = Signal::new(任意, 任意, 任意, 任意, 任意, 任意, 0);
|
||||
assert_eq!(s3.key(), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_value() {
|
||||
let s = Signal::new("k1", "k2", "k3", "v1", "v2", "v3", 88);
|
||||
assert_eq!(s.value(), "v1_v2_v3_88");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_match_缺键_报错() {
|
||||
let s = Signal::new("14400", "D1MO3", "中枢", 任意, "三买", 任意, 0);
|
||||
let 字典: HashMap<String, 匹配值> = HashMap::new();
|
||||
assert!(s.is_match(&字典).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_match_非字符串_false() {
|
||||
let s = Signal::new("14400", "D1MO3", "中枢", 任意, "三买", 任意, 0);
|
||||
let mut 字典 = HashMap::new();
|
||||
字典.insert("14400_D1MO3_中枢".to_string(), 匹配值::非字符串);
|
||||
assert_eq!(s.is_match(&字典).unwrap(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_match_命中() {
|
||||
let s = Signal::new("14400", "D1MO3", "中枢", 任意, "三买", 任意, 0);
|
||||
let mut 字典 = HashMap::new();
|
||||
字典.insert(
|
||||
"14400_D1MO3_中枢".to_string(),
|
||||
匹配值::字符串("中枢段DEA穿越2_三买_偏移0_100".into()),
|
||||
);
|
||||
assert_eq!(s.is_match(&字典).unwrap(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_match_v2不符_未命中() {
|
||||
let s = Signal::new("14400", "D1MO3", "中枢", 任意, "三买", 任意, 0);
|
||||
let mut 字典 = HashMap::new();
|
||||
字典.insert(
|
||||
"14400_D1MO3_中枢".to_string(),
|
||||
匹配值::字符串("中枢段DEA穿越2_三卖_偏移0_100".into()),
|
||||
);
|
||||
assert_eq!(s.is_match(&字典).unwrap(), false);
|
||||
}
|
||||
}
|
||||
@@ -1596,6 +1596,7 @@ mod tests {
|
||||
use crate::kline::bar::K线;
|
||||
use crate::kline::chan_kline::缠论K线;
|
||||
use crate::types::分型结构;
|
||||
use crate::{error, info, warn};
|
||||
|
||||
/// 辅助:创建一根最小化的原始K线
|
||||
fn 辅助_创建K线(时间戳: i64, 高: f64, 低: f64, 开: f64, 收: f64) -> K线 {
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo "=== 1/4 清除 Python 缓存 ==="
|
||||
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null
|
||||
find . -type f -name "*.pyc" -delete 2>/dev/null
|
||||
echo " Python 缓存已清除"
|
||||
|
||||
echo "=== 2/4 清除 Cargo 编译缓存 ==="
|
||||
rm -rf chanlun/target chanlun-py/target
|
||||
echo " target/ 已清除"
|
||||
|
||||
echo "=== 3/4 构建 Release ==="
|
||||
cd chanlun-py
|
||||
maturin build --release
|
||||
echo " 构建完成"
|
||||
|
||||
echo "=== 4/4 安装 ==="
|
||||
pip install --break-system-packages --force-reinstall --no-deps \
|
||||
target/wheels/chanlun-*.whl
|
||||
echo " 安装完成"
|
||||
|
||||
echo
|
||||
echo "✓ 清理 + 构建 + 安装完毕"
|
||||
echo " pip show chanlun | grep Version"
|
||||
pip show chanlun 2>/dev/null | grep Version
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,467 +0,0 @@
|
||||
# 子项目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 实际写法)。
|
||||
@@ -1,79 +0,0 @@
|
||||
# 子项目 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 用例更新
|
||||
@@ -1,678 +0,0 @@
|
||||
# 信号计算器 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` 使用别名导入——内部代码零改动
|
||||
@@ -1,503 +0,0 @@
|
||||
# 子项目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` 一致(过滤 "任意" 段)
|
||||
@@ -1,239 +0,0 @@
|
||||
# 信号原语层移植到 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` 已有声明一致)。
|
||||
@@ -1,181 +0,0 @@
|
||||
# 子项目 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 顶部加第三方代码声明。
|
||||
@@ -1,105 +0,0 @@
|
||||
# 信号计算器完全移植评估
|
||||
|
||||
- 日期: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 动态导入路径。
|
||||
@@ -1,58 +0,0 @@
|
||||
# 子项目 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 用例)
|
||||
```
|
||||
@@ -1,142 +0,0 @@
|
||||
# `信号计算器` 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.` 前缀) |
|
||||
@@ -1,200 +0,0 @@
|
||||
# 子项目 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 代码,不涉及第三方许可证。
|
||||
@@ -1 +0,0 @@
|
||||
/target/
|
||||
Generated
-594
@@ -1,594 +0,0 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "chanlun"
|
||||
version = "26.6.4"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"chanlun-signal-macros",
|
||||
"chrono",
|
||||
"fastrand",
|
||||
"inventory",
|
||||
"parking_lot",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chanlun-signal-macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inventory"
|
||||
version = "0.3.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||
dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "plugin-demo"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chanlun",
|
||||
"chanlun-signal-macros",
|
||||
"inventory",
|
||||
"parking_lot",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.150"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.125"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.125"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.125"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.125"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
@@ -1,14 +0,0 @@
|
||||
[package]
|
||||
name = "plugin-demo"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
chanlun = { path = "../../chanlun" }
|
||||
chanlun-signal-macros = { path = "../../chanlun-signal-macros" }
|
||||
inventory = "0.3"
|
||||
serde_json = "1"
|
||||
parking_lot = "0.12"
|
||||
@@ -1,202 +0,0 @@
|
||||
//! chanlun 信号插件示例 — 两种动态注册方式。
|
||||
//!
|
||||
//! 编译: `cargo build` → `target/debug/libplugin_demo.so`
|
||||
//! 加载: Python → `ctypes.CDLL(...)` → `init_plugin_manual()` / `init_plugin_macro()`
|
||||
//!
|
||||
//! # 方式 A: 手动 C-ABI 注册
|
||||
//! - 实现 `fn(SignalFn)` 信号函数
|
||||
//! - `init` 中调用 `chanlun_register_signal(name, template, func_ptr)`
|
||||
//!
|
||||
//! # 方式 B: #[signal] 宏 + inventory 批量提交
|
||||
//! - 用 `#[signal]` 宏写信号函数(与宿主内写法完全一致)
|
||||
//! - `init` 中遍历 `inventory::iter::<SignalDescriptor>` 批量调用 `chanlun_register_signal`
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::os::raw::c_char;
|
||||
|
||||
use chanlun::business::observer::观察者;
|
||||
use chanlun::signal::registry::SignalFn;
|
||||
use chanlun::signal::Signal;
|
||||
use serde_json::Value;
|
||||
|
||||
// ── C-ABI: 宿主导出的符号(由 dlopen 的动态链接器解析)──
|
||||
|
||||
unsafe extern "C" {
|
||||
fn chanlun_register_signal(
|
||||
name: *const c_char,
|
||||
template: *const c_char,
|
||||
func: SignalFn,
|
||||
) -> i32;
|
||||
fn chanlun_unregister_signal(name: *const c_char) -> i32;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 方式 A: 手动 C-ABI 注册
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
/// 插件信号 A:MACD 零上强势(DIF > 0 && DIF > DEA)。
|
||||
///
|
||||
/// 手动注册 — 不依赖 `#[signal]` 宏,不依赖 `chanlun-signal-macros`。
|
||||
fn 插件MACD强势_V999999(obs: &观察者, params: &HashMap<String, Value>) -> Vec<Signal> {
|
||||
obs.确保指标已计算();
|
||||
let freq = params.get("freq").and_then(|v| v.as_str()).unwrap_or("日线");
|
||||
let di = params.get("di").and_then(|v| v.as_i64()).unwrap_or(1) as usize;
|
||||
|
||||
let k1 = freq.to_string();
|
||||
let k2 = format!("D{di}");
|
||||
let k3 = "插件MACD强势V999999";
|
||||
|
||||
let klines = &obs.普通K线序列;
|
||||
if klines.len() < di + 1 {
|
||||
return vec![Signal::new_empty(&k1, &k2, k3)];
|
||||
}
|
||||
let k线 = &klines[klines.len() - di];
|
||||
let macd = match k线.macd() {
|
||||
Some(m) => m,
|
||||
None => return vec![Signal::new_empty(&k1, &k2, k3)],
|
||||
};
|
||||
let dif = match macd.DIF { Some(v) => v, None => return vec![Signal::new_empty(&k1, &k2, k3)] };
|
||||
let dea = match macd.DEA { Some(v) => v, None => return vec![Signal::new_empty(&k1, &k2, k3)] };
|
||||
|
||||
if dif > 0.0 && dif > dea {
|
||||
let score = ((dif - dea).abs() * 1000.0) as i32;
|
||||
vec![Signal::new(&k1, &k2, k3, "多头强势", "MACD零上", "DIF>DEA", score)]
|
||||
} else if dif < 0.0 && dif < dea {
|
||||
vec![Signal::new(&k1, &k2, k3, "空头强势", "MACD零下", "DIF<DEA", 0)]
|
||||
} else {
|
||||
vec![Signal::new_empty(&k1, &k2, k3)]
|
||||
}
|
||||
}
|
||||
|
||||
/// 方式 A 入口:Python 调用 `plugin.init_plugin_manual()`。
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn init_plugin_manual() -> i32 {
|
||||
let name = c"插件MACD强势_V999999";
|
||||
let template = c"{freq}_D{di}_插件MACD强势V999999";
|
||||
let func: SignalFn = 插件MACD强势_V999999;
|
||||
|
||||
let ret = chanlun_register_signal(name.as_ptr(), template.as_ptr(), func);
|
||||
eprintln!("[plugin/manual] 注册: {name:?} → 返回码 {ret}");
|
||||
ret
|
||||
}
|
||||
|
||||
/// 方式 A 清理。
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn deinit_plugin_manual() -> i32 {
|
||||
chanlun_unregister_signal(c"插件MACD强势_V999999".as_ptr())
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// 方式 B: #[signal] 宏 + inventory::iter 批量提交
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
// 依赖: Cargo.toml 中添加 chanlun-signal-macros
|
||||
use chanlun::signal::registry::SignalDescriptor;
|
||||
use chanlun_signal_macros::signal;
|
||||
|
||||
/// 插件信号 B1:MACD 金叉(与宿主内 `macd_金叉_V260601` 等价)。
|
||||
#[signal(
|
||||
name = "插件MACD金叉_V999999",
|
||||
template = "{freq}_D{di}#MACD#{fast}#{slow}#{signal}_插件MACD金叉V999999",
|
||||
crate_path = "::chanlun"
|
||||
)]
|
||||
fn 插件MACD金叉_V999999(obs: &观察者, params: &HashMap<String, Value>) -> Vec<Signal> {
|
||||
obs.确保指标已计算();
|
||||
let di = params.get("di").and_then(|v| v.as_i64()).unwrap_or(1) as usize;
|
||||
let freq = params.get("freq").and_then(|v| v.as_str()).unwrap_or("日线");
|
||||
|
||||
let k1 = freq.to_string();
|
||||
let k2 = "D1#MACD#13#31#11".to_string();
|
||||
let k3 = "插件MACD金叉V999999";
|
||||
|
||||
let klines = &obs.普通K线序列;
|
||||
if klines.len() < di + 2 {
|
||||
return vec![Signal::new_empty(&k1, &k2, k3)];
|
||||
}
|
||||
let cur = klines[klines.len() - di].macd();
|
||||
let prev = klines[klines.len() - di - 1].macd();
|
||||
let (cur, prev) = match (cur, prev) {
|
||||
(Some(c), Some(p)) => (c, p),
|
||||
_ => return vec![Signal::new_empty(&k1, &k2, k3)],
|
||||
};
|
||||
let cd = match cur.DIF { Some(v) => v, None => return vec![Signal::new_empty(&k1, &k2, k3)] };
|
||||
let ce = match cur.DEA { Some(v) => v, None => return vec![Signal::new_empty(&k1, &k2, k3)] };
|
||||
let pd = match prev.DIF { Some(v) => v, None => return vec![Signal::new_empty(&k1, &k2, k3)] };
|
||||
let pe = match prev.DEA { Some(v) => v, None => return vec![Signal::new_empty(&k1, &k2, k3)] };
|
||||
|
||||
if pd <= pe && cd > ce {
|
||||
vec![Signal::new(&k1, &k2, k3, "金叉", "插件", "[signal]宏", 80)]
|
||||
} else if pd >= pe && cd < ce {
|
||||
vec![Signal::new(&k1, &k2, k3, "死叉", "插件", "[signal]宏", 0)]
|
||||
} else {
|
||||
vec![Signal::new_empty(&k1, &k2, k3)]
|
||||
}
|
||||
}
|
||||
|
||||
/// 插件信号 B2:涨跌停(与宿主内 `bar_zdt_V230331` 等价)。
|
||||
#[signal(
|
||||
name = "插件涨跌停_V999999",
|
||||
template = "{freq}_D{di}_插件涨跌停V999999",
|
||||
crate_path = "::chanlun"
|
||||
)]
|
||||
fn 插件涨跌停_V999999(obs: &观察者, params: &HashMap<String, Value>) -> Vec<Signal> {
|
||||
let di = params.get("di").and_then(|v| v.as_i64()).unwrap_or(1) as usize;
|
||||
let freq = params.get("freq").and_then(|v| v.as_str()).unwrap_or("15分钟");
|
||||
|
||||
let k1 = freq.to_string();
|
||||
let k2 = format!("D{di}");
|
||||
let k3 = "插件涨跌停V999999";
|
||||
|
||||
let klines = &obs.普通K线序列;
|
||||
if klines.len() < di + 2 {
|
||||
return vec![Signal::new_empty(&k1, &k2, k3)];
|
||||
}
|
||||
let 当前 = &klines[klines.len() - di];
|
||||
let 前 = &klines[klines.len() - di - 1];
|
||||
|
||||
let v1 = if 当前.收盘价 == 当前.高 && 当前.收盘价 >= 前.收盘价 {
|
||||
"涨停"
|
||||
} else if 当前.收盘价 == 当前.低 && 当前.收盘价 <= 前.收盘价 {
|
||||
"跌停"
|
||||
} else {
|
||||
"任意"
|
||||
};
|
||||
|
||||
if v1 == "任意" {
|
||||
vec![Signal::new_empty(&k1, &k2, k3)]
|
||||
} else {
|
||||
vec![Signal::new(&k1, &k2, k3, v1, "插件", "[signal]宏", 0)]
|
||||
}
|
||||
}
|
||||
|
||||
/// 方式 B 入口:Python 调用 `plugin.init_plugin_macro()`。
|
||||
///
|
||||
/// 遍历 `inventory::iter::<SignalDescriptor>`,将 `#[signal]` 宏注册的
|
||||
/// 所有信号一次性提交到宿主 `DYNAMIC_REGISTRY`。
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn init_plugin_macro() -> i32 {
|
||||
let mut count = 0i32;
|
||||
for desc in inventory::iter::<SignalDescriptor> {
|
||||
let Ok(name_c) = std::ffi::CString::new(desc.name) else { continue };
|
||||
let Ok(tpl_c) = std::ffi::CString::new(desc.template) else { continue };
|
||||
let ret = chanlun_register_signal(name_c.as_ptr(), tpl_c.as_ptr(), desc.func);
|
||||
if ret == 0 {
|
||||
count += 1;
|
||||
eprintln!("[plugin/macro] ✅ {} → {}", desc.name, desc.template);
|
||||
} else {
|
||||
eprintln!("[plugin/macro] ❌ {} (err {ret})", desc.name);
|
||||
}
|
||||
}
|
||||
eprintln!("[plugin/macro] 批量注册完成: {count} 个信号");
|
||||
count
|
||||
}
|
||||
|
||||
/// 方式 B 清理:遍历 inventory 逐个调用 `chanlun_unregister_signal`。
|
||||
#[unsafe(no_mangle)]
|
||||
pub unsafe extern "C" fn deinit_plugin_macro() -> i32 {
|
||||
for desc in inventory::iter::<SignalDescriptor> {
|
||||
let Ok(name_c) = std::ffi::CString::new(desc.name) else { continue };
|
||||
chanlun_unregister_signal(name_c.as_ptr());
|
||||
}
|
||||
0
|
||||
}
|
||||
+2
-232
@@ -1,5 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
import backtrader as bt
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, List
|
||||
@@ -8,12 +7,9 @@ import math
|
||||
|
||||
import queue
|
||||
import threading
|
||||
import numpy as np
|
||||
|
||||
import chanlun.chan_external as cet
|
||||
|
||||
|
||||
__all__ = ["订单类型", "市场类型", "自适应市场仓位", "交易信号", "批次", "订单执行器", "高级策略基类_", "高级策略基类", "自定义实时数据源", "回测", "信号驱动策略", "NB数据源", "随机数据"]
|
||||
__all__ = ["订单类型", "市场类型", "自适应市场仓位", "交易信号", "批次", "订单执行器", "高级策略基类_", "高级策略基类", "自定义实时数据源", "回测"]
|
||||
|
||||
|
||||
# ---------- 订单类型枚举 ----------
|
||||
@@ -56,12 +52,6 @@ class 自适应市场仓位(bt.Sizer):
|
||||
("允许部分开仓", False),
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
已设置 = sum(1 for x in [self.p.固定金额, self.p.风险百分比, self.p.固定数量] if x is not None)
|
||||
if 已设置 != 1:
|
||||
raise ValueError(f"自适应市场仓位: 必须且只能设置 固定金额/风险百分比/固定数量 之一,当前设置了{已设置}个")
|
||||
|
||||
def _getsizing(self, 佣金信息, 可用现金, 数据, 是否买入):
|
||||
"""返回最终下单数量(股/币/手)"""
|
||||
价格 = 数据.close[0]
|
||||
@@ -745,11 +735,7 @@ class 自定义实时数据源(bt.feed.DataBase):
|
||||
print(f"[{datetime.now()}] 自定义数据源: 数据格式错误,跳过: {e}")
|
||||
return True
|
||||
|
||||
# 兼容 int (Unix timestamp) 和 datetime 两种时间戳格式
|
||||
if isinstance(dt, datetime):
|
||||
self.lines.datetime[0] = bt.date2num(dt)
|
||||
else:
|
||||
self.lines.datetime[0] = bt.date2num(datetime.utcfromtimestamp(int(dt)))
|
||||
self.lines.datetime[0] = bt.date2num(datetime.utcfromtimestamp(int(dt)))
|
||||
self.lines.open[0] = o
|
||||
self.lines.high[0] = h
|
||||
self.lines.low[0] = l
|
||||
@@ -1065,222 +1051,6 @@ class 回测(高级策略基类):
|
||||
print(f"[{dt.strftime('%Y-%m-%d %H:%M')}] {self.p.观察员.__class__.__name__}: {self.p.符号} | {文本}")
|
||||
|
||||
|
||||
# ==================== NB 数据源 — .nb 文件读取 ====================
|
||||
|
||||
|
||||
class NB数据源(bt.feeds.DataBase):
|
||||
"""从 .nb 文件读取K线数据作为 Backtrader 数据源
|
||||
|
||||
.nb 格式: 48字节大端序 — time:8, open:8, high:8, low:8, close:8, volume:8 (f64)
|
||||
"""
|
||||
|
||||
def __init__(self, 文件路径: str, 最大条数: int = None):
|
||||
super().__init__()
|
||||
import struct
|
||||
|
||||
with open(文件路径, "rb") as f:
|
||||
self._buffer = f.read()
|
||||
self._记录总数 = len(self._buffer) // 48
|
||||
if 最大条数:
|
||||
self._记录总数 = min(self._记录总数, 最大条数)
|
||||
self._索引 = 0
|
||||
|
||||
def _load(self):
|
||||
import struct
|
||||
|
||||
if self._索引 >= self._记录总数:
|
||||
return False
|
||||
offset = self._索引 * 48
|
||||
ts, o, h, l, c, v = struct.unpack(">6d", self._buffer[offset : offset + 48])
|
||||
self.lines.datetime[0] = bt.date2num(datetime.fromtimestamp(int(ts)))
|
||||
self.lines.open[0] = o
|
||||
self.lines.high[0] = h
|
||||
self.lines.low[0] = l
|
||||
self.lines.close[0] = c
|
||||
self.lines.volume[0] = v
|
||||
self._索引 += 1
|
||||
return True
|
||||
|
||||
|
||||
# ==================== 信号驱动策略 — 立体分析器 + 信号计算器 + Backtrader ====================
|
||||
|
||||
|
||||
class 信号驱动策略(高级策略基类):
|
||||
"""基于 立体分析器 / 信号计算器 的多周期信号驱动策略。
|
||||
|
||||
Backtrader 负责仓位管理和订单执行,信号计算器只负责产出信号字典。
|
||||
策略在 ``next()`` 中直接读取信号字典判断买卖。
|
||||
|
||||
数据流::
|
||||
|
||||
Backtrader bar → 立体分析器.投喂K线()
|
||||
→ 各周期缠论分析 → 信号计算器.更新() → 信号字典
|
||||
→ 策略读取 v2 判多空 → Backtrader 订单
|
||||
|
||||
使用方式::
|
||||
|
||||
cerebro.addstrategy(信号驱动策略, 符号="btcusd",
|
||||
多头信号=["三买"], 空头信号=["三卖"])
|
||||
"""
|
||||
|
||||
params = (
|
||||
("符号", "btcusd"),
|
||||
("基础周期", 300),
|
||||
("高级周期组", ()),
|
||||
("信号模块", "chanlun.signals"),
|
||||
("持仓", None),
|
||||
("信号配置", None),
|
||||
("投喂预热数", 2),
|
||||
("指标计算", True),
|
||||
)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.投喂计数 = 0
|
||||
self.已处理信号 = set()
|
||||
|
||||
self.持仓列表 = self.p.持仓 if self.p.持仓 is not None else []
|
||||
self._上次操作数 = {p.name: 0 for p in self.持仓列表}
|
||||
|
||||
from chanlun.signal_orchestrator import (
|
||||
SignalOrchestrator as _信号计算器,
|
||||
get_signals_config,
|
||||
)
|
||||
|
||||
信号配置 = self.p.信号配置
|
||||
if 信号配置 is None and self.持仓列表:
|
||||
所有信号 = set()
|
||||
for p in self.持仓列表:
|
||||
所有信号.update(p.unique_signals)
|
||||
信号配置 = get_signals_config(list(所有信号))
|
||||
|
||||
self.缠论配置 = self._构建缠论配置()
|
||||
周期组 = [self.p.基础周期]
|
||||
if self.p.高级周期组:
|
||||
周期组.extend(self.p.高级周期组)
|
||||
if len(周期组) < 2:
|
||||
周期组.append(self.p.基础周期 * 5)
|
||||
from chanlun import 立体分析器 as _立体分析器
|
||||
|
||||
self.分析器 = _立体分析器(self.p.符号, 周期组, self.缠论配置)
|
||||
self.计算器 = _信号计算器(
|
||||
分析器=self.分析器,
|
||||
信号配置=信号配置 or [],
|
||||
信号模块=self.p.信号模块,
|
||||
)
|
||||
|
||||
def _构建缠论配置(self):
|
||||
from chanlun import 缠论配置 as _缠论配置
|
||||
|
||||
配置 = _缠论配置()
|
||||
if self.p.指标计算:
|
||||
配置.设置指标(
|
||||
均线=[("SMA_5", "收", "SMA", 5), ("SMA_10", "收", "SMA", 10), ("SMA_20", "收", "SMA", 20)],
|
||||
MACD=[("macd", "收", 13, 31, 11)],
|
||||
)
|
||||
配置.图表展示 = False
|
||||
配置.图表展示标签 = []
|
||||
return 配置
|
||||
|
||||
def _从数据源创建K线(self) -> "K线":
|
||||
dt = bt.num2date(self.data.datetime[0])
|
||||
from chanlun import K线
|
||||
|
||||
return K线.创建普K(
|
||||
self.p.符号,
|
||||
int(dt.timestamp()),
|
||||
float(self.data.open[0]),
|
||||
float(self.data.high[0]),
|
||||
float(self.data.low[0]),
|
||||
float(self.data.close[0]),
|
||||
float(self.data.volume[0]),
|
||||
0,
|
||||
self.p.基础周期,
|
||||
)
|
||||
|
||||
def _检测新操作(self) -> list:
|
||||
新操作 = []
|
||||
for pos in self.持仓列表:
|
||||
curr = len(pos.operates)
|
||||
prev = self._上次操作数.get(pos.name, 0)
|
||||
if curr > prev:
|
||||
新操作.extend(pos.operates[prev:])
|
||||
self._上次操作数[pos.name] = curr
|
||||
return 新操作
|
||||
|
||||
def next(self):
|
||||
dt = self.datas[0].datetime.datetime(0)
|
||||
bar = len(self.data)
|
||||
|
||||
# ── 1. 投喂 ──
|
||||
k线 = self._从数据源创建K线()
|
||||
self.分析器.投喂K线(k线)
|
||||
self.投喂计数 += 1
|
||||
|
||||
if self.投喂计数 < self.p.投喂预热数:
|
||||
# K线合成器缓冲中,尚无完整高级K线
|
||||
return
|
||||
if bar < 50:
|
||||
return # 缠论分析预热
|
||||
|
||||
# ── 2. 信号计算 ──
|
||||
try:
|
||||
self.计算器.更新()
|
||||
except Exception:
|
||||
import traceback
|
||||
|
||||
self.日志(f"❌ 计算器异常:\n{traceback.format_exc()}")
|
||||
return
|
||||
|
||||
if self.计算器.信号:
|
||||
k, v = next(iter(self.计算器.信号.items()))
|
||||
self.日志(f"📡 信号={k}→{v} 持仓={self.position.size:+d}")
|
||||
|
||||
# ── 3. 去重 ──
|
||||
try:
|
||||
信号ID = json.dumps(self.计算器.信号, sort_keys=True, default=str)
|
||||
except Exception:
|
||||
信号ID = str(self.计算器.信号)
|
||||
if 信号ID in self.已处理信号:
|
||||
return
|
||||
self.已处理信号.add(信号ID)
|
||||
|
||||
# ── 4. 止损 ──
|
||||
if self.position:
|
||||
self.更新止损订单(self.position.size > 0, self.data.close[0])
|
||||
|
||||
# ── 5. 仓位匹配 ──
|
||||
for pos in self.持仓列表:
|
||||
try:
|
||||
prev_ops = len(pos.operates)
|
||||
pos.update(self.计算器.信号字典)
|
||||
if len(pos.operates) > prev_ops:
|
||||
self.日志(f" ✓ {pos.name} 匹配 → {pos.operates[-1]['op']}")
|
||||
except ValueError:
|
||||
pass # 信号键尚不存在
|
||||
|
||||
# ── 6. 执行操作 ──
|
||||
for op_record in self._检测新操作():
|
||||
op = op_record["op"]
|
||||
price = op_record.get("price", self.data.close[0])
|
||||
self.日志(f" ▶ {op} @{price:.0f} {op_record.get('op_desc', '')}")
|
||||
|
||||
if op == cet.Operate.LE and self.position.size > 0:
|
||||
self.平仓(self.data)
|
||||
elif op == cet.Operate.SE and self.position.size < 0:
|
||||
self.平仓(self.data)
|
||||
elif op == cet.Operate.LO and not self.position and self.p.允许做多:
|
||||
self.开仓(self.data, 是否做多=True)
|
||||
elif op == cet.Operate.SO and not self.position and self.p.允许做空:
|
||||
self.开仓(self.data, 是否做多=False)
|
||||
else:
|
||||
self.日志(f" ✗ {op} 跳过 (持仓={self.position.size:+d} 允许做多={self.p.允许做多} 允许做空={self.p.允许做空})")
|
||||
|
||||
def 日志(self, 文本: str):
|
||||
dt = self.datas[0].datetime.datetime(0)
|
||||
print(f"[{dt.strftime('%m-%d %H:%M')}] {文本}")
|
||||
|
||||
|
||||
# ==================== 回测运行入口 ====================
|
||||
if __name__ == "__main__":
|
||||
import numpy as np
|
||||
|
||||
Reference in New Issue
Block a user