3 Commits

Author SHA1 Message Date
YuWuKunCheng 16ed3de8f5 信号 版本一 2026-06-27 18:17:57 +08:00
YuWuKunCheng 8405d478bc 首次提交 2026-06-09 20:09:55 +08:00
YuWuKunCheng 15a1d43b1d Nothing 2026-06-09 15:13:49 +08:00
100 changed files with 24635 additions and 4646 deletions
+123 -49
View File
@@ -16,16 +16,19 @@ on:
env:
CARGO_TERM_COLOR: always
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# ============================================================
# 1. 发布 chanlun 核心库至 crates.io
# 1. 校验 & 发布
# 解析 chanlun-py 依赖的版本号 → 检查 crates.io 是否可用 →
# 不可用时检查本地 chanlun 版本是否匹配 → 匹配则自动发布 →
# 等待索引同步
# ============================================================
publish-crates:
check-version:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
exists: ${{ steps.check.outputs.exists }}
version: ${{ steps.parse.outputs.version }}
steps:
- uses: actions/checkout@v4
@@ -34,72 +37,108 @@ jobs:
with:
components: rustfmt, clippy
- name: 缓存依赖
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('chanlun/Cargo.lock') }}
- name: 解析 chanlun-py 依赖的 chanlun 版本
id: parse
working-directory: chanlun-py
run: |
VER=$(grep -oP 'chanlun\s*=\s*"=?\s*\K[0-9]+\.[0-9]+\.[0-9]+(?=")' Cargo.toml | head -1)
if [ -z "$VER" ]; then
echo "::error::无法从 chanlun-py/Cargo.toml 解析 chanlun 版本号"
echo "请确保 Cargo.toml 中包含: chanlun = \"=X.Y.Z\""
exit 1
fi
echo "version=$VER" >> $GITHUB_OUTPUT
echo "依赖的 chanlun 版本: $VER"
- name: 提取版本
id: version
- name: 解析本地 chanlun 核心库版本
id: local-ver
working-directory: chanlun
run: |
VER=$(cargo metadata --format-version 1 --no-deps 2>/dev/null \
| jq -r '.packages[] | select(.name == "chanlun") | .version')
VER=$(grep -oP '^version\s*=\s*"\K[0-9]+\.[0-9]+\.[0-9]+(?=")' Cargo.toml | head -1)
echo "version=$VER" >> $GITHUB_OUTPUT
echo "当前版本: $VER"
echo "本地 chanlun 版本: $VER"
- name: 检查版本是否已存在
- name: 检查 crates.io 并决定是否发布
id: check
run: |
VER="${{ steps.version.outputs.version }}"
EXISTS=$(curl -sS "https://crates.io/api/v1/crates/chanlun" \
| jq -r --arg v "$VER" '.versions[]?.num // empty | select(. == $v)')
if [ -n "$EXISTS" ]; then
echo "版本 $VER 已存在于 crates.io,跳过发布"
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "版本 $VER 未发布,继续"
echo "exists=false" >> $GITHUB_OUTPUT
DEP_VER="${{ steps.parse.outputs.version }}"
LOCAL_VER="${{ steps.local-ver.outputs.version }}"
HTTP_CODE=$(curl -sS -o /dev/null -w "%{http_code}" \
-H "User-Agent: chanlun-rs/ci" \
"https://crates.io/api/v1/crates/chanlun/$DEP_VER")
if [ "$HTTP_CODE" = "200" ]; then
echo "chanlun $DEP_VER 在 crates.io 已可用,无需发布"
echo "need-publish=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "chanlun $DEP_VER 在 crates.io 不存在 (HTTP $HTTP_CODE)"
if [ "$DEP_VER" != "$LOCAL_VER" ]; then
echo "::error::本地 chanlun 版本 ($LOCAL_VER) 与依赖版本 ($DEP_VER) 不匹配"
echo ""
echo "请先发布 chanlun 核心库至 crates.io:"
echo " cd chanlun && cargo publish"
echo ""
echo "或修改 chanlun-py/Cargo.toml 中的版本号为已发布版本"
exit 1
fi
echo "本地版本 $LOCAL_VER 与依赖一致,将自动发布 chanlun 至 crates.io"
echo "need-publish=true" >> $GITHUB_OUTPUT
- name: 格式检查
if: steps.check.outputs.exists == 'false'
if: steps.check.outputs.need-publish == 'true'
working-directory: chanlun
run: cargo fmt --check
- name: Lint 检查
if: steps.check.outputs.exists == 'false'
if: steps.check.outputs.need-publish == 'true'
working-directory: chanlun
run: cargo clippy
- name: 运行测试
if: steps.check.outputs.exists == 'false'
if: steps.check.outputs.need-publish == 'true'
working-directory: chanlun
run: cargo test
- name: 验证打包
if: steps.check.outputs.exists == 'false'
if: steps.check.outputs.need-publish == 'true'
working-directory: chanlun
run: cargo publish --dry-run --allow-dirty
- name: 登录 crates.io
if: steps.check.outputs.exists == 'false'
run: cargo login ${{ secrets.CARGO_TOKEN }}
- name: 登录 crates.io 并发布
if: steps.check.outputs.need-publish == 'true'
run: |
cargo login ${{ secrets.CARGO_TOKEN }}
cd chanlun && cargo publish --allow-dirty
- name: 发布 chanlun 至 crates.io
if: steps.check.outputs.exists == 'false'
working-directory: chanlun
run: cargo publish --allow-dirty
- name: 等待 crates.io 索引同步
if: steps.check.outputs.need-publish == 'true'
run: |
DEP_VER="${{ steps.parse.outputs.version }}"
echo "等待 crates.io 索引同步 (最多 2 分钟)..."
for i in $(seq 1 12); do
HTTP_CODE=$(curl -sS -o /dev/null -w "%{http_code}" \
-H "User-Agent: chanlun-rs/ci" \
"https://crates.io/api/v1/crates/chanlun/$DEP_VER")
if [ "$HTTP_CODE" = "200" ]; then
echo "chanlun $DEP_VER 已在 crates.io 可用 (尝试 $i/12)"
exit 0
fi
echo " 等待中... ($i/12)"
sleep 10
done
echo "::error::等待超时:chanlun $DEP_VER 在 crates.io 仍不可用"
exit 1
# ============================================================
# 2. 构建 wheel — Linux x86_64 (manylinux)
# ============================================================
linux-x86_64:
needs: [publish-crates]
needs: [check-version]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -112,9 +151,19 @@ jobs:
- name: 安装 Rust 工具链
uses: dtolnay/rust-toolchain@stable
- name: 更新 cargo 索引(确保新版本可见
run: cargo update
- name: 更新 cargo 索引(含重试
working-directory: chanlun-py
run: |
for i in $(seq 1 6); do
if cargo update 2>&1; then
echo "cargo update 成功"
exit 0
fi
echo "cargo update 失败,重试... ($i/6)"
sleep 10
done
echo "::error::cargo update 失败"
exit 1
- name: 构建 wheel (manylinux)
uses: PyO3/maturin-action@v1
@@ -134,7 +183,7 @@ jobs:
# 3. 构建 wheel — macOS (x86_64 + arm64)
# ============================================================
macos:
needs: [publish-crates]
needs: [check-version]
runs-on: macos-latest
strategy:
matrix:
@@ -150,9 +199,19 @@ jobs:
with:
python-version: '3.12'
- name: 更新 cargo 索引
run: cargo update
- name: 更新 cargo 索引(含重试)
working-directory: chanlun-py
run: |
for i in $(seq 1 6); do
if cargo update 2>&1; then
echo "cargo update 成功"
exit 0
fi
echo "cargo update 失败,重试... ($i/6)"
sleep 10
done
echo "::error::cargo update 失败"
exit 1
- name: 构建 wheel
uses: PyO3/maturin-action@v1
@@ -171,7 +230,7 @@ jobs:
# 4. 构建 wheel — Windows x86_64
# ============================================================
windows:
needs: [publish-crates]
needs: [check-version]
runs-on: windows-latest
strategy:
matrix:
@@ -187,9 +246,21 @@ jobs:
with:
python-version: '3.12'
- name: 更新 cargo 索引
run: cargo update
- name: 更新 cargo 索引(含重试)
working-directory: chanlun-py
shell: pwsh
run: |
for ($i = 1; $i -le 6; $i++) {
cargo update
if ($LASTEXITCODE -eq 0) {
Write-Host "cargo update 成功"
exit 0
}
Write-Host "cargo update 失败,重试... ($i/6)"
Start-Sleep -Seconds 10
}
Write-Host "::error::cargo update 失败"
exit 1
- name: 构建 wheel
uses: PyO3/maturin-action@v1
@@ -208,7 +279,7 @@ jobs:
# 5. 源码分发包 (sdist)
# ============================================================
sdist:
needs: [publish-crates]
needs: [check-version]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -218,6 +289,9 @@ jobs:
with:
python-version: '3.12'
- name: 安装 Rust 工具链
uses: dtolnay/rust-toolchain@stable
- name: 构建 sdist
uses: PyO3/maturin-action@v1
with:
@@ -235,7 +309,7 @@ jobs:
# 6. 发布至 PyPI
# ============================================================
publish:
needs: [publish-crates, linux-x86_64, macos, windows, sdist]
needs: [linux-x86_64, macos, windows, sdist]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v') || github.event.inputs.publish-to-pypi == 'true'
permissions:
+13
View File
@@ -0,0 +1,13 @@
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.
+19
View File
@@ -0,0 +1,19 @@
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.
+19
View File
@@ -0,0 +1,19 @@
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.
+32
View File
@@ -0,0 +1,32 @@
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.
+174 -5
View File
@@ -19,7 +19,7 @@ import chanlun
# 创建配置(全部默认值)
config = chanlun.缠论配置()
# 读取 K 线数据文件(文件名需遵循 `符号-周期-起始时间戳-结束时间戳.nb` 格式,如 `btcusd-300-1631772074-1632222374.nb`
# 读取 K 线数据文件(文件名需遵循 `符号-周期-起始时间戳-结束时间戳.nb` 格式)
obs = chanlun.观察者.读取数据文件("path/to/btcusd-300-1631772074-1632222374.nb", config)
# 查看各层级序列
@@ -33,6 +33,171 @@ 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)
@@ -41,30 +206,34 @@ analyzer = chanlun.立体分析器("BTCUSD", [60, 60*5, 60*5*6], config)
pip install maturin
# 开发模式(直接安装到当前 venv)
maturin develop
cd chanlun-py && maturin develop
# 或构建 wheel
maturin build --release
cd chanlun-py && 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线合成器`, `立体分析器` | 分析框架 |
| 业务 | `缠论配置`, `观察者`, `K线合成器`, `立体分析器`, `买卖点` | 分析框架 |
| 信号 | `Signal`, `Factor`, `Event`, `Position`, `信号引擎` | 信号匹配+计算引擎 |
| 注册表 | `call_signal`, `list_signals`, `get_signal_template`, `register_signal`, `unregister_signal` | 信号发现+动态注册 |
## 兼容性
+1675 -306
View File
File diff suppressed because it is too large Load Diff
+975
View File
@@ -0,0 +1,975 @@
# 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` |
+7 -6
View File
@@ -1,6 +1,6 @@
[package]
name = "chanlun-py"
version = "26.6.47"
version = "26.6.125"
edition = "2024"
description = "缠论技术分析库 — Rust 高性能 Python 绑定"
authors = ["YuYuKunKun"]
@@ -12,11 +12,12 @@ crate-type = ["cdylib"]
name = "chanlun"
[dependencies]
chanlun = "26.6.3" # { path = "../chanlun" }
lru = "0.18"
chanlun = { path = "../chanlun" }
parking_lot = "0.12"
tracing-subscriber = { version = "0.3", features = ["env-filter", "ansi", "std", "registry"] }
tracing-core = "0.1"
dashmap = "6"
tracing = "0.1"
pyo3 = { version = "0.28", features = ["experimental-inspect"] }
serde_json = "1"
chrono = "0.4"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "ansi", "std", "registry"] }
tracing-core = "0.1"
+81
View File
@@ -40,6 +40,10 @@ analyzer = chanlun.立体分析器("BTCUSD", [60, 60*5, 60*5*6], config)
```bash
pip install maturin
# 推荐:一键清理缓存 + 构建 + 安装
./clean_install.sh
# 或手动:
# 开发模式(直接安装到当前 venv)
maturin develop
@@ -48,6 +52,12 @@ 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
@@ -72,6 +82,77 @@ pip install target/wheels/chanlun-*.whl
- 类名 / 方法名 / 字段名与 `chan.py` 保持一致
- 支持 `.nb` 二进制文件格式(大端字节序)
## 性能配置
### 缓存模式
Python 对象缓存有两种模式,通过环境变量 `CHANLUN_CACHE_MODE` 或函数调用切换:
```python
from chanlun._chanlun import set_cache_mode, get_cache_mode
# 默认:thread_local,每线程独立缓存,零锁,多线程场景最佳
print(get_cache_mode()) # "thread_local"
# 全局:dashmap 分片哈希表,跨线程 Python `is` 身份一致
set_cache_mode("global") # 必须在创建任何观察者之前调用
```
```bash
# 环境变量方式
CHANLUN_CACHE_MODE=global python main.py # 全局缓存
python main.py # 默认:线程局部缓存
```
| 模式 | 性能 | Python `is` 跨线程 | 适用场景 |
|------|------|---------------------|----------|
| `thread_local`(默认) | 零锁,最快 | 否 | 批量回测、多线程独立分析 |
| `global` | dashmap 分片锁 | 是 | 测试验证、跨线程对象共享 |
### 日志模式
日志输出有三种模式,通过环境变量 `CHANLUN_LOG_MODE` 或函数调用切换:
```python
from chanlun._chanlun import set_log_mode, set_log_level, get_log_mode
# 默认:off,不输出,零开销
print(get_log_mode()) # "off"
# 简单模式:直接 eprintln/println
set_log_mode("simple")
set_log_level("debug") # 必需:设置日志级别启用输出
# Tracing 模式:带时间戳和文件位置格式化输出
set_log_mode("tracing")
set_log_level("debug")
```
```bash
# 环境变量方式
CHANLUN_LOG_MODE=simple python main.py # 简单输出
CHANLUN_LOG_MODE=tracing python main.py # 格式化输出
python main.py # 默认:静默
```
| 模式 | 输出方式 | 性能 | 格式 |
|------|---------|------|------|
| `off`(默认) | 无 | 零开销 | — |
| `simple` | `eprintln!` / `println!` | 极轻 | 纯文本 |
| `tracing` | tracing-subscriber | 稍重 | `2026-06-12 01:57:59.942 WARN file.rs:line` |
### 观察者直传(避免 Python list 转换)
背驰分析新增 `_OBS` 后缀方法,直接接受观察者引用,跳过 `list[K线]``Vec<Arc<K线>>` 转换:
```python
# 旧方式:构建 Python 列表
result = 背驰分析.MACD背驰(进入段, 离开段, obs.普通K线序列, "")
# 新方式:直接传观察者
result = 背驰分析.MACD背驰_OBS(进入段, 离开段, obs, "")
```
## 许可
本项目主体采用 MIT 许可。包含以下第三方开源代码:czsc(Apache 2.0)、parseMIT)、termcolorMIT)。
+53 -4
View File
@@ -5,11 +5,18 @@ from typing import Any, ClassVar, Optional, List, Dict, Tuple, Union
from datetime import datetime
# ========== Module-level functions ==========
def get_rs_log_level() -> str: ...
def set_rs_log_level(level: str) -> None: ...
def get_log_level() -> str: ...
def set_log_level(level: str) -> None: ...
def get_log_mode() -> str: ...
def set_log_mode(mode: str) -> None: ...
def get_cache_mode() -> str: ...
def set_cache_mode(mode: str) -> None: ...
def get_分型模式() -> bool: ...
def set_分型模式(value: bool) -> None: ...
def get_扩展线段模式() -> bool: ...
def set_扩展线段模式(value: bool) -> None: ...
def 转化为时间戳(ts: Any) -> int: ...
def 转化为时间戳_数字(ts: Any) -> int: ...
def K线相等(A: K线, B: K线, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
@@ -75,6 +82,8 @@ class 相对方向:
def 翻转(self) -> 相对方向: ...
@classmethod
def 分析(cls, 前高: float, 前低: float, 后高: float, 后低: float) -> 相对方向: ...
@classmethod
def 从序列中机选(cls, 数量: int, 可选方向: List[相对方向], 可重复: bool = True) -> List[相对方向]: ...
def __str__(self) -> str: ...
def __repr__(self) -> str: ...
def __hash__(self) -> int: ...
@@ -326,6 +335,7 @@ class K线:
def 截取(序列: List[K线], : K线, : K线) -> List[K线]: ...
def __str__(self) -> str: ...
def __repr__(self) -> str: ...
def 根据当前K线生成新K线(self, 方向: 相对方向, 居中: bool = False) -> K线: ...
def __bytes__(self) -> bytes: ...
def __eq__(self, other: Any) -> bool: ...
def __hash__(self) -> int: ...
@@ -438,9 +448,9 @@ class 虚线:
@property
def 模式(self) -> str: ...
@property
def 特征序列_显示(self) -> bool: ...
@特征序列_显示.setter
def 特征序列_显示(self, value: bool) -> None: ...
def _特征序列_显示(self) -> bool: ...
@_特征序列_显示.setter
def _特征序列_显示(self, value: bool) -> None: ...
@property
def 特征序列(self) -> List[Optional[线段特征]]: ...
@property
@@ -526,6 +536,8 @@ class 虚线:
class 线段特征:
@property
def 序号(self) -> int: ...
@序号.setter
def 序号(self, value: int) -> None: ...
@property
def 标识(self) -> str: ...
@标识.setter
@@ -570,6 +582,18 @@ class 背驰分析:
def 任选背驰(cls, 进入段: 虚线, 离开段: 虚线, 普K序列: List[K线]) -> bool: ...
@classmethod
def 背驰模式(cls, 进入段: 虚线, 离开段: 虚线, 普K序列: List[K线], 配置: 缠论配置, 模式: str) -> bool: ...
@classmethod
def MACD背驰_OBS(cls, 进入段: 虚线, 离开段: 虚线, 观察员: 观察者, 方式: str = "") -> bool: ...
@classmethod
def 全量背驰_OBS(cls, 进入段: 虚线, 离开段: 虚线, 观察员: 观察者) -> bool: ...
@classmethod
def 任意背驰_OBS(cls, 进入段: 虚线, 离开段: 虚线, 观察员: 观察者) -> bool: ...
@classmethod
def 配置背驰_OBS(cls, 进入段: 虚线, 离开段: 虚线, 观察员: 观察者, 配置: 缠论配置) -> bool: ...
@classmethod
def 任选背驰_OBS(cls, 进入段: 虚线, 离开段: 虚线, 观察员: 观察者) -> bool: ...
@classmethod
def 背驰模式_OBS(cls, 进入段: 虚线, 离开段: 虚线, 观察员: 观察者, 配置: 缠论配置, 模式: str) -> bool: ...
class :
@classmethod
@@ -793,6 +817,30 @@ class 观察者:
def 扩展线段序列_扩展线段(self) -> List[虚线]: ...
@property
def 扩展中枢序列_扩展线段(self) -> List[中枢]: ...
@property
def 线段分析层次(self) -> int: ...
@线段分析层次.setter
def 线段分析层次(self, value: int) -> None: ...
@property
def 扩展线段分析层次(self) -> int: ...
@扩展线段分析层次.setter
def 扩展线段分析层次(self, value: int) -> None: ...
@property
def 混合扩展线段分析层次(self) -> int: ...
@混合扩展线段分析层次.setter
def 混合扩展线段分析层次(self, value: int) -> None: ...
@property
def 线段序列组(self) -> List[List[虚线]]: ...
@property
def 中枢序列组(self) -> List[List[中枢]]: ...
@property
def 扩展线段序列组(self) -> List[List[虚线]]: ...
@property
def 扩展中枢序列组(self) -> List[List[中枢]]: ...
@property
def 混合扩展线段序列组(self) -> List[List[虚线]]: ...
@property
def 混合扩展中枢序列组(self) -> List[List[中枢]]: ...
def 重置基础序列(self) -> None: ...
def 增加原始K线(self, 普K: K线) -> None: ...
def 投喂原始数据(self, 时间戳: int, : float, : float, : float, : float, : float) -> None: ...
@@ -838,6 +886,7 @@ 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: ...
+53 -3
View File
@@ -32,6 +32,8 @@ __all__ = [
"布林带",
"get_分型模式",
"set_分型模式",
"get_扩展线段模式",
"set_扩展线段模式",
"get_log_level",
"set_log_level",
"get_rs_log_level",
@@ -43,12 +45,60 @@ __all__ = [
"线段特征相等",
"中枢相等",
"虚线相等",
"chan",
]
from ._chanlun import *
from ._chanlun import set_log_level as _rs_set_log_level, get_log_level as _rs_get_log_level
from . import chan
from .chan import set_log_level, 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----
def set_rs_log_level(level: str):
+164
View File
@@ -9,8 +9,16 @@ def get_rs_log_level() -> str: ...
def set_rs_log_level(level: str) -> None: ...
def get_log_level() -> str: ...
def set_log_level(level: str) -> None: ...
def get_log_mode() -> str: ...
def set_log_mode(mode: str) -> None: ...
def get_cache_mode() -> str: ...
def set_cache_mode(mode: str) -> None: ...
def get_分型模式() -> bool: ...
def set_分型模式(value: bool) -> None: ...
def get_扩展线段模式() -> bool: ...
def set_扩展线段模式(value: bool) -> None: ...
def 转化为时间戳(ts: Any) -> int: ...
def 转化为时间戳_数字(ts: Any) -> int: ...
def K线相等(A: K线, B: K线, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
def 缠论K线相等(A: 缠论K线, B: 缠论K线, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
def 分型相等(A: 分型, B: 分型, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
@@ -74,6 +82,8 @@ class 相对方向:
def 翻转(self) -> 相对方向: ...
@classmethod
def 分析(cls, 前高: float, 前低: float, 后高: float, 后低: float) -> 相对方向: ...
@classmethod
def 从序列中机选(cls, 数量: int, 可选方向: List[相对方向], 可重复: bool = True) -> List[相对方向]: ...
def __str__(self) -> str: ...
def __repr__(self) -> str: ...
def __hash__(self) -> int: ...
@@ -325,6 +335,7 @@ class K线:
def 截取(序列: List[K线], : K线, : K线) -> List[K线]: ...
def __str__(self) -> str: ...
def __repr__(self) -> str: ...
def 根据当前K线生成新K线(self, 方向: 相对方向, 居中: bool = False) -> K线: ...
def __bytes__(self) -> bytes: ...
def __eq__(self, other: Any) -> bool: ...
def __hash__(self) -> int: ...
@@ -571,6 +582,18 @@ class 背驰分析:
def 任选背驰(cls, 进入段: 虚线, 离开段: 虚线, 普K序列: List[K线]) -> bool: ...
@classmethod
def 背驰模式(cls, 进入段: 虚线, 离开段: 虚线, 普K序列: List[K线], 配置: 缠论配置, 模式: str) -> bool: ...
@classmethod
def MACD背驰_OBS(cls, 进入段: 虚线, 离开段: 虚线, 观察员: 观察者, 方式: str = "") -> bool: ...
@classmethod
def 全量背驰_OBS(cls, 进入段: 虚线, 离开段: 虚线, 观察员: 观察者) -> bool: ...
@classmethod
def 任意背驰_OBS(cls, 进入段: 虚线, 离开段: 虚线, 观察员: 观察者) -> bool: ...
@classmethod
def 配置背驰_OBS(cls, 进入段: 虚线, 离开段: 虚线, 观察员: 观察者, 配置: 缠论配置) -> bool: ...
@classmethod
def 任选背驰_OBS(cls, 进入段: 虚线, 离开段: 虚线, 观察员: 观察者) -> bool: ...
@classmethod
def 背驰模式_OBS(cls, 进入段: 虚线, 离开段: 虚线, 观察员: 观察者, 配置: 缠论配置, 模式: str) -> bool: ...
class :
@classmethod
@@ -846,6 +869,146 @@ 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 缠论配置:
@@ -863,6 +1026,7 @@ 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: ...
+1675 -306
View File
File diff suppressed because it is too large Load Diff
+575
View File
@@ -0,0 +1,575 @@
# ==============================================================================
# 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
+113
View File
@@ -0,0 +1,113 @@
# 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: ...
+321
View File
@@ -0,0 +1,321 @@
"""信号编排器 — 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
+14
View File
@@ -0,0 +1,14 @@
"""缠论技术分析库 — 信号函数模块
每个信号函数接收 观察者 对象 + 关键字参数,返回 OrderedDict。
信号 key 格式:k1_k2_k3value 格式: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 *
+35
View File
@@ -0,0 +1,35 @@
"""信号函数模板 — 新建信号函数时以此为蓝本"""
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) ##
+417
View File
@@ -0,0 +1,417 @@
"""缠论技术分析库 — 信号函数示例合集"""
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)
+296
View File
@@ -0,0 +1,296 @@
# 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))
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "chanlun"
version = "2606.47"
version = "2606.125"
description = "缠论技术分析库 — Rust 高性能实现"
readme = { file = "README.md", content-type = "text/markdown" }
license = { file = "LICENSE", content-type = "text/plain" }
+270 -90
View File
@@ -26,33 +26,20 @@ use crate::kline_py::chan_kline_to_py;
use crate::structure_py::{dashed_to_py, fractal_to_py};
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyList, PyType};
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::RwLock;
use std::sync::atomic::Ordering;
// 使用全局 static 而非 thread_local!,保证跨线程对象标识一致性
static HUB_IDENTITY: std::sync::LazyLock<RwLock<HashMap<usize, Py<Py>>>> =
std::sync::LazyLock::new(|| RwLock::new(HashMap::new()));
// 缓存通过 crate::cache 模块管理(支持 thread_local / global 运行时切换)
pub(crate) fn hub_to_py(
py: Python<'_>, inner: Arc<chanlun::algorithm::hub::>
) -> Py<Py> {
let key = Arc::as_ptr(&inner) as usize;
if let Some(cached) = HUB_IDENTITY
.read()
.unwrap()
.get(&key)
.map(|p| p.clone_ref(py))
{
if let Some(cached) = crate::cache::hub_get(py, key) {
return cached;
}
HUB_IDENTITY
.write()
.unwrap()
.retain(|_, v| v.get_refcnt(py) > 1);
let obj = Py::new(py, Py { inner }).unwrap();
HUB_IDENTITY.write().unwrap().insert(key, obj.clone_ref(py));
crate::cache::hub_insert(py, key, &obj);
obj
}
@@ -90,16 +77,21 @@ impl 背驰分析Py {
: &str,
py: Python<'_>,
) -> bool {
let _inner = Arc::clone(&.borrow().inner);
let _inner = Arc::clone(&.borrow().inner);
let = .to_string();
let rc_list: Vec<Arc<chanlun::kline::bar::K线>> = K线序列
.iter()
.map(|k| k.bind(py).borrow().inner.clone())
.collect();
chanlun::algorithm::divergence::::MACD背驰(
&.borrow().inner,
&.borrow().inner,
&rc_list,
,
)
py.detach(move || {
chanlun::algorithm::divergence::::MACD背驰(
&_inner,
&_inner,
&rc_list,
&,
)
})
}
#[classmethod]
@@ -137,15 +129,19 @@ impl 背驰分析Py {
K序列: Vec<Py<K线Py>>,
py: Python<'_>,
) -> bool {
let _inner = Arc::clone(&.borrow().inner);
let _inner = Arc::clone(&.borrow().inner);
let rc_list: Vec<Arc<chanlun::kline::bar::K线>> = K序列
.iter()
.map(|k| k.bind(py).borrow().inner.clone())
.collect();
chanlun::algorithm::divergence::::(
&.borrow().inner,
&.borrow().inner,
&rc_list,
)
py.detach(move || {
chanlun::algorithm::divergence::::(
&_inner,
&_inner,
&rc_list,
)
})
}
#[classmethod]
@@ -157,15 +153,19 @@ impl 背驰分析Py {
K序列: Vec<Py<K线Py>>,
py: Python<'_>,
) -> bool {
let _inner = Arc::clone(&.borrow().inner);
let _inner = Arc::clone(&.borrow().inner);
let rc_list: Vec<Arc<chanlun::kline::bar::K线>> = K序列
.iter()
.map(|k| k.bind(py).borrow().inner.clone())
.collect();
chanlun::algorithm::divergence::::(
&.borrow().inner,
&.borrow().inner,
&rc_list,
)
py.detach(move || {
chanlun::algorithm::divergence::::(
&_inner,
&_inner,
&rc_list,
)
})
}
#[classmethod]
@@ -178,17 +178,21 @@ impl 背驰分析Py {
: &Bound<'_, Py>,
py: Python<'_>,
) -> PyResult<bool> {
let _inner = Arc::clone(&.borrow().inner);
let _inner = Arc::clone(&.borrow().inner);
let rc_list: Vec<Arc<chanlun::kline::bar::K线>> = K序列
.iter()
.map(|k| k.bind(py).borrow().inner.clone())
.collect();
let config = .borrow().to_rust_config(py)?;
Ok(chanlun::algorithm::divergence::::(
&.borrow().inner,
&.borrow().inner,
&rc_list,
&config,
))
Ok(py.detach(move || {
chanlun::algorithm::divergence::::(
&_inner,
&_inner,
&rc_list,
&config,
)
}))
}
#[classmethod]
@@ -200,15 +204,19 @@ impl 背驰分析Py {
K序列: Vec<Py<K线Py>>,
py: Python<'_>,
) -> bool {
let _inner = Arc::clone(&.borrow().inner);
let _inner = Arc::clone(&.borrow().inner);
let rc_list: Vec<Arc<chanlun::kline::bar::K线>> = K序列
.iter()
.map(|k| k.bind(py).borrow().inner.clone())
.collect();
chanlun::algorithm::divergence::::(
&.borrow().inner,
&.borrow().inner,
&rc_list,
)
py.detach(move || {
chanlun::algorithm::divergence::::(
&_inner,
&_inner,
&rc_list,
)
})
}
#[classmethod]
@@ -222,18 +230,169 @@ impl 背驰分析Py {
: &str,
py: Python<'_>,
) -> PyResult<bool> {
let _inner = Arc::clone(&.borrow().inner);
let _inner = Arc::clone(&.borrow().inner);
let rc_list: Vec<Arc<chanlun::kline::bar::K线>> = K序列
.iter()
.map(|k| k.bind(py).borrow().inner.clone())
.collect();
let config = .borrow().to_rust_config(py)?;
Ok(chanlun::algorithm::divergence::::(
&.borrow().inner,
&.borrow().inner,
&rc_list,
&config,
,
))
let = .to_string();
Ok(py.detach(move || {
chanlun::algorithm::divergence::::(
&_inner,
&_inner,
&rc_list,
&config,
&,
)
}))
}
// ---- 观察者直传(跳过 Python list→Vec 转换,直接借用观察者内部 &[Arc<K线>] ----
#[classmethod]
#[pyo3(name = "MACD背驰_OBS", signature = (进入段, 离开段, 观察员, 方式 = ""))]
fn MACD背驰_obs(
_cls: &Bound<'_, PyType>,
: &Bound<'_, 线Py>,
: &Bound<'_, 线Py>,
: &Bound<'_, Py>,
: &str,
py: Python<'_>,
) -> bool {
let _inner = Arc::clone(&.borrow().inner);
let _inner = Arc::clone(&.borrow().inner);
let = .to_string();
let obs_arc = .borrow().inner.clone().expect("观察者未初始化");
py.detach(move || {
let guard = obs_arc.read();
chanlun::algorithm::divergence::::MACD背驰(
&_inner,
&_inner,
&guard.K线序列,
&,
)
})
}
#[classmethod]
#[pyo3(name = "全量背驰_OBS", signature = (进入段, 离开段, 观察员))]
fn _obs(
_cls: &Bound<'_, PyType>,
: &Bound<'_, 线Py>,
: &Bound<'_, 线Py>,
: &Bound<'_, Py>,
py: Python<'_>,
) -> bool {
let _inner = Arc::clone(&.borrow().inner);
let _inner = Arc::clone(&.borrow().inner);
let obs_arc = .borrow().inner.clone().expect("观察者未初始化");
py.detach(move || {
let guard = obs_arc.read();
chanlun::algorithm::divergence::::(
&_inner,
&_inner,
&guard.K线序列,
)
})
}
#[classmethod]
#[pyo3(name = "任意背驰_OBS", signature = (进入段, 离开段, 观察员))]
fn _obs(
_cls: &Bound<'_, PyType>,
: &Bound<'_, 线Py>,
: &Bound<'_, 线Py>,
: &Bound<'_, Py>,
py: Python<'_>,
) -> bool {
let _inner = Arc::clone(&.borrow().inner);
let _inner = Arc::clone(&.borrow().inner);
let obs_arc = .borrow().inner.clone().expect("观察者未初始化");
py.detach(move || {
let guard = obs_arc.read();
chanlun::algorithm::divergence::::(
&_inner,
&_inner,
&guard.K线序列,
)
})
}
#[classmethod]
#[pyo3(name = "配置背驰_OBS", signature = (进入段, 离开段, 观察员, 配置))]
fn _obs(
_cls: &Bound<'_, PyType>,
: &Bound<'_, 线Py>,
: &Bound<'_, 线Py>,
: &Bound<'_, Py>,
: &Bound<'_, Py>,
py: Python<'_>,
) -> PyResult<bool> {
let _inner = Arc::clone(&.borrow().inner);
let _inner = Arc::clone(&.borrow().inner);
let config = .borrow().to_rust_config(py)?;
let obs_arc = .borrow().inner.clone().expect("观察者未初始化");
Ok(py.detach(move || {
let guard = obs_arc.read();
chanlun::algorithm::divergence::::(
&_inner,
&_inner,
&guard.K线序列,
&config,
)
}))
}
#[classmethod]
#[pyo3(name = "任选背驰_OBS", signature = (进入段, 离开段, 观察员))]
fn _obs(
_cls: &Bound<'_, PyType>,
: &Bound<'_, 线Py>,
: &Bound<'_, 线Py>,
: &Bound<'_, Py>,
py: Python<'_>,
) -> bool {
let _inner = Arc::clone(&.borrow().inner);
let _inner = Arc::clone(&.borrow().inner);
let obs_arc = .borrow().inner.clone().expect("观察者未初始化");
py.detach(move || {
let guard = obs_arc.read();
chanlun::algorithm::divergence::::(
&_inner,
&_inner,
&guard.K线序列,
)
})
}
#[classmethod]
#[pyo3(name = "背驰模式_OBS", signature = (进入段, 离开段, 观察员, 配置, 模式))]
fn _obs(
_cls: &Bound<'_, PyType>,
: &Bound<'_, 线Py>,
: &Bound<'_, 线Py>,
: &Bound<'_, Py>,
: &Bound<'_, Py>,
: &str,
py: Python<'_>,
) -> PyResult<bool> {
let _inner = Arc::clone(&.borrow().inner);
let _inner = Arc::clone(&.borrow().inner);
let config = .borrow().to_rust_config(py)?;
let = .to_string();
let obs_arc = .borrow().inner.clone().expect("观察者未初始化");
Ok(py.detach(move || {
let guard = obs_arc.read();
chanlun::algorithm::divergence::::(
&_inner,
&_inner,
&guard.K线序列,
&config,
&,
)
}))
}
}
@@ -345,7 +504,7 @@ impl 笔Py {
.map(|k| k.bind(py).borrow().inner.clone())
.collect();
let config = .borrow().to_rust_config(py)?;
let depth = match _rc {
let depth = py.detach(|| match _rc {
Some(fr) => chanlun::algorithm::bi::::(
fr,
&mut fr_seq,
@@ -356,17 +515,21 @@ impl 笔Py {
&config,
),
None => ,
};
});
// 写回 Python 列表
// 写回 Python 列表 (bulk extend)
let fr_items: Vec<Py<PyAny>> = fr_seq
.iter()
.map(|f| fractal_to_py(py, Arc::clone(f)).into_any())
.collect();
.call_method0("clear")?;
for f in fr_seq {
.call_method1("append", (fractal_to_py(py, f),))?;
}
.call_method1("extend", (PyList::new(py, &fr_items)?,))?;
let bi_items: Vec<Py<PyAny>> = bi_seq
.iter()
.map(|d| dashed_to_py(py, Arc::clone(d)).into_any())
.collect();
.call_method0("clear")?;
for d in bi_seq {
.call_method1("append", (dashed_to_py(py, d),))?;
}
.call_method1("extend", (PyList::new(py, &bi_items)?,))?;
Ok(depth)
}
@@ -487,7 +650,10 @@ impl 线段Py {
Some(&hub_ref.inner),
)
} else {
chanlun::algorithm::segment::线::(&borrowed.inner, None)
return Err(pyo3::exceptions::PyTypeError::new_err(format!(
"线段.分割序列 的 所属中枢 参数必须是 chanlun._chanlun.中枢 (Rust PyO3 对象),收到了 {}。请使用 chanlun.chan.线段.分割序列 (Python 版) 处理 Python 中枢对象。",
hub_bound.get_type().name()?,
)));
}
} else {
chanlun::algorithm::segment::线::(&borrowed.inner, None)
@@ -552,19 +718,23 @@ impl 线段Py {
let rel_list: Vec<chanlun::types::> =
.map(|v| v.into_iter().map(|d| d.inner).collect())
.unwrap_or(default_rel);
chanlun::algorithm::segment::线::(
&bi_list,
&mut seg_seq,
&config,
,
&rel_list,
);
py.detach(|| {
chanlun::algorithm::segment::线::(
&bi_list,
&mut seg_seq,
&config,
,
&rel_list,
);
});
// 写回 Python 列表
// 写回 Python 列表 (bulk extend)
let items: Vec<Py<PyAny>> = seg_seq
.iter()
.map(|d| dashed_to_py(py, Arc::clone(d)).into_any())
.collect();
线.call_method0("clear")?;
for d in seg_seq {
线.call_method1("append", (dashed_to_py(py, d),))?;
}
线.call_method1("extend", (PyList::new(py, &items)?,))?;
Ok(())
}
@@ -590,13 +760,17 @@ impl 线段Py {
}
let config = .borrow().to_rust_config(py)?;
chanlun::algorithm::segment::线::(&dash_list, &mut seg_seq, &config);
py.detach(|| {
chanlun::algorithm::segment::线::(&dash_list, &mut seg_seq, &config);
});
// 写回 Python 列表
// 写回 Python 列表 (bulk extend)
let items: Vec<Py<PyAny>> = seg_seq
.iter()
.map(|d| dashed_to_py(py, Arc::clone(d)).into_any())
.collect();
线.call_method0("clear")?;
for d in seg_seq {
线.call_method1("append", (dashed_to_py(py, d),))?;
}
线.call_method1("extend", (PyList::new(py, &items)?,))?;
Ok(())
}
@@ -714,7 +888,7 @@ impl 中枢Py {
#[getter]
fn (&self) -> String {
self.inner..read().unwrap().clone()
self.inner..read().clone()
}
#[getter]
@@ -725,7 +899,7 @@ impl 中枢Py {
#[getter]
fn (&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let list = pyo3::types::PyList::empty(py);
for d in self.inner..read().unwrap().iter() {
for d in self.inner..read().iter() {
list.append(dashed_to_py(py, Arc::clone(d)))?;
}
Ok(list.into())
@@ -736,7 +910,6 @@ impl 中枢Py {
self.inner
.线
.read()
.unwrap()
.as_ref()
.map(|d| dashed_to_py(py, Arc::clone(d)))
}
@@ -746,7 +919,6 @@ impl 中枢Py {
self.inner
._第三买卖线
.read()
.unwrap()
.as_ref()
.map(|d| dashed_to_py(py, Arc::clone(d)))
}
@@ -845,13 +1017,17 @@ impl 中枢Py {
hub_seq.push(Arc::clone(&h.inner));
}
let config = .borrow().to_rust_config(.py())?;
self.inner.(&mut hub_seq, &config);
py.detach(|| {
self.inner.(&mut hub_seq, &config);
});
// 写回 Python 列表
// 写回 Python 列表 (bulk extend)
let items: Vec<Py<PyAny>> = hub_seq
.iter()
.map(|h| hub_to_py(py, Arc::clone(h)).into_any())
.collect();
.call_method0("clear")?;
for h in hub_seq {
.call_method1("append", (hub_to_py(py, h),))?;
}
.call_method1("extend", (PyList::new(py, &items)?,))?;
Ok(())
}
@@ -958,13 +1134,17 @@ impl 中枢Py {
hub_seq.push(Arc::clone(&h.inner));
}
chanlun::algorithm::hub::::(&rc_list, &mut hub_seq, , , );
py.detach(|| {
chanlun::algorithm::hub::::(&rc_list, &mut hub_seq, , , );
});
// 写回 Python 列表
// 写回 Python 列表 (bulk extend)
let items: Vec<Py<PyAny>> = hub_seq
.iter()
.map(|h| hub_to_py(py, Arc::clone(h)).into_any())
.collect();
.call_method0("clear")?;
for h in hub_seq {
.call_method1("append", (hub_to_py(py, h),))?;
}
.call_method1("extend", (PyList::new(py, &items)?,))?;
Ok(())
}
+74 -25
View File
@@ -22,15 +22,16 @@
* SOFTWARE.
*/
use parking_lot::RwLock;
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyType};
use std::sync::RwLock;
use crate::algorithm_py::hub_to_py;
use crate::kline_py::bar_to_py;
use crate::structure_py::{dashed_to_py, fractal_to_py, Py};
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use crate::config_py::Py;
use crate::kline_py::{K线Py, K线Py};
@@ -693,28 +694,27 @@ impl 买卖点Py {
#[pyclass(name = "观察者", module = "chanlun._chanlun", subclass)]
pub struct Py {
pub(crate) inner: Option<Arc<RwLock<chanlun::business::observer::>>>,
: std::sync::Mutex<Option<Py<Py>>>,
: parking_lot::Mutex<Option<Py<Py>>>,
: AtomicU64,
}
impl Py {
pub(crate) fn obs(
&self,
) -> std::sync::RwLockReadGuard<'_, chanlun::business::observer::> {
) -> parking_lot::RwLockReadGuard<'_, chanlun::business::observer::> {
self.inner
.as_ref()
.expect("观察者 尚未初始化,请通过 __init__(符号, 周期, 配置) 构造")
.read()
.unwrap_or_else(|e| e.into_inner())
}
pub(crate) fn obs_mut(
&self,
) -> std::sync::RwLockWriteGuard<'_, chanlun::business::observer::> {
) -> parking_lot::RwLockWriteGuard<'_, chanlun::business::observer::> {
self.inner
.as_ref()
.expect("观察者 尚未初始化,请通过 __init__(符号, 周期, 配置) 构造")
.write()
.unwrap_or_else(|e| e.into_inner())
}
}
@@ -770,7 +770,8 @@ impl 观察者Py {
inner: Some(chanlun::business::observer::::new(
, , config,
)),
: std::sync::Mutex::new(None),
: parking_lot::Mutex::new(None),
: AtomicU64::new(0),
})
}
@@ -824,7 +825,7 @@ impl 观察者Py {
#[getter]
fn (&self, py: Python<'_>) -> PyResult<Py<Py>> {
let mut cache = self..lock().unwrap();
let mut cache = self..lock();
if let Some(ref cached) = *cache {
Ok(cached.clone_ref(py))
} else {
@@ -838,11 +839,8 @@ impl 观察者Py {
#[setter]
fn set_配置(&self, value: &Bound<'_, Py>) -> PyResult<()> {
let config = value.borrow().to_rust_config(value.py())?;
*self..lock() = Some(value.clone().unbind());
self.obs_mut(). = config;
self.
.lock()
.unwrap()
.replace(value.clone().unbind());
Ok(())
}
@@ -861,25 +859,53 @@ impl 观察者Py {
/// 核心入口 — 投喂一根原始K线,增量更新所有层级(内部实现)
#[pyo3(name = "_增加原始K线")]
fn K线_impl(&mut self, K: &Bound<'_, K线Py>) -> PyResult<()> {
self.obs_mut().K线((*K.borrow().inner).clone());
Ok(())
fn K线_impl(slf: &Bound<'_, Self>, K: &Bound<'_, K线Py>) -> PyResult<()> {
let kline = (*K.borrow().inner).clone();
let obs_arc = slf
.borrow()
.inner
.clone()
.expect("观察者 尚未初始化,请通过 __init__(符号, 周期, 配置) 构造");
let symbol = obs_arc.read()..clone();
let result = slf.py().detach(move || {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
obs_arc.write().K线(kline);
}))
});
match result {
Ok(()) => Ok(()),
Err(e) => {
let msg = e
.downcast_ref::<&str>()
.map(|s| s.to_string())
.or_else(|| e.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "未知算法错误".into());
Err(pyo3::exceptions::PyRuntimeError::new_err(format!(
"[{symbol}] 算法异常: {msg}"
)))
}
}
}
/// 核心入口 — 投喂一根原始K线,增量更新所有层级(公开分发器,支持子类重写)
fn K线(slf: &Bound<'_, Self>, K: &Bound<'_, K线Py>) -> PyResult<()> {
// 同步缓存的 Python 配置到 Rust 观察者(支持 obs.配置 直接修改)
// 版本对比,仅在配置变更时同步(支持 obs.配置.field = value 直接修改)
{
let me = slf.borrow();
if let Some(ref cached) = *me..lock().unwrap() {
if let Some(ref cached) = *me..lock() {
let py = slf.py();
if let Ok(config) = cached.bind(py).borrow().to_rust_config(py) {
me.obs_mut(). = config;
let cfg_ref = cached.bind(py).borrow();
let current_version = cfg_ref..load(Ordering::Relaxed);
if current_version != me..load(Ordering::Relaxed) {
if let Ok(config) = cfg_ref.to_rust_config(py) {
me.obs_mut(). = config;
}
me..store(current_version, Ordering::Relaxed);
}
}
}
slf.call_method1("_增加原始K线", (K,))?;
Ok(())
// 直接调用 Rust 实现,跳过 Python dispatch
Self::K线_impl(slf, K)
}
/// 投喂原始数据 — 便捷入口,直接从 OHLCV 创建 K线 并通过 Python 分发 增加原始K线,
@@ -931,9 +957,31 @@ impl 观察者Py {
/// 静态重新分析(内部实现)
#[pyo3(name = "_静态重新分析")]
fn _impl(&mut self) -> PyResult<()> {
self.obs_mut().();
Ok(())
fn _impl(slf: &Bound<'_, Self>) -> PyResult<()> {
let obs_arc = slf
.borrow()
.inner
.clone()
.expect("观察者 尚未初始化,请通过 __init__(符号, 周期, 配置) 构造");
let symbol = obs_arc.read()..clone();
let result = slf.py().detach(move || {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
obs_arc.write().();
}))
});
match result {
Ok(()) => Ok(()),
Err(e) => {
let msg = e
.downcast_ref::<&str>()
.map(|s| s.to_string())
.or_else(|| e.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "未知算法错误".into());
Err(pyo3::exceptions::PyRuntimeError::new_err(format!(
"[{symbol}] 静态重新分析异常: {msg}"
)))
}
}
}
/// 静态重新分析(公开分发器,支持子类重写)
@@ -1413,7 +1461,8 @@ impl 立体分析器Py {
for (, obs_rc) in &self.inner. {
let obs_py = Py {
inner: Some(obs_rc.clone()),
: std::sync::Mutex::new(None),
: parking_lot::Mutex::new(None),
: AtomicU64::new(0),
};
dict.set_item(, obs_py)?;
}
+223
View File
@@ -0,0 +1,223 @@
/*
* 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;
/// 缓存模式:线程局部(默认,零锁)或全局(dashmap,跨线程共享)
use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::OnceLock;
pub enum CacheMode {
ThreadLocal,
Global,
}
static CACHE_MODE: OnceLock<CacheMode> = OnceLock::new();
pub fn get_mode() -> &'static CacheMode {
CACHE_MODE.get_or_init(|| match std::env::var("CHANLUN_CACHE_MODE").as_deref() {
Ok("global") => CacheMode::Global,
_ => CacheMode::ThreadLocal,
})
}
pub fn peek_mode() -> Option<&'static CacheMode> {
CACHE_MODE.get()
}
pub fn set_mode(mode: CacheMode) -> Result<(), String> {
CACHE_MODE
.set(mode)
.map_err(|_| "缓存模式已初始化,请在创建任何观察者之前调用 set_cache_mode".into())
}
// ========== BAR_IDENTITY ==========
thread_local! {
static BAR_LOCAL: RefCell<HashMap<usize, Py<super::kline_py::K线Py>>> = RefCell::new(HashMap::new());
}
static BAR_GLOBAL: std::sync::LazyLock<DashMap<usize, Py<super::kline_py::K线Py>>> =
std::sync::LazyLock::new(DashMap::new);
pub fn bar_get(py: Python<'_>, key: usize) -> Option<Py<super::kline_py::K线Py>> {
match get_mode() {
CacheMode::ThreadLocal => BAR_LOCAL.with(|m| m.borrow().get(&key).map(|p| p.clone_ref(py))),
CacheMode::Global => BAR_GLOBAL.get(&key).map(|p| p.clone_ref(py)),
}
}
pub fn bar_insert(py: Python<'_>, key: usize, obj: &Py<super::kline_py::K线Py>) {
match get_mode() {
CacheMode::ThreadLocal => BAR_LOCAL.with(|m| {
let mut m = m.borrow_mut();
m.retain(|_, v| v.get_refcnt(py) > 1);
m.insert(key, obj.clone_ref(py));
}),
CacheMode::Global => {
BAR_GLOBAL.retain(|_, v| v.get_refcnt(py) > 1);
BAR_GLOBAL.insert(key, obj.clone_ref(py));
}
}
}
// ========== KLINE_IDENTITY ==========
thread_local! {
static KLINE_LOCAL: RefCell<HashMap<usize, Py<super::kline_py::K线Py>>> = RefCell::new(HashMap::new());
}
static KLINE_GLOBAL: std::sync::LazyLock<DashMap<usize, Py<super::kline_py::K线Py>>> =
std::sync::LazyLock::new(DashMap::new);
pub fn kline_get(py: Python<'_>, key: usize) -> Option<Py<super::kline_py::K线Py>> {
match get_mode() {
CacheMode::ThreadLocal => {
KLINE_LOCAL.with(|m| m.borrow().get(&key).map(|p| p.clone_ref(py)))
}
CacheMode::Global => KLINE_GLOBAL.get(&key).map(|p| p.clone_ref(py)),
}
}
pub fn kline_insert(py: Python<'_>, key: usize, obj: &Py<super::kline_py::K线Py>) {
match get_mode() {
CacheMode::ThreadLocal => KLINE_LOCAL.with(|m| {
let mut m = m.borrow_mut();
m.retain(|_, v| v.get_refcnt(py) > 1);
m.insert(key, obj.clone_ref(py));
}),
CacheMode::Global => {
KLINE_GLOBAL.retain(|_, v| v.get_refcnt(py) > 1);
KLINE_GLOBAL.insert(key, obj.clone_ref(py));
}
}
}
// ========== FRACTAL_IDENTITY ==========
use crate::structure_py::Py;
thread_local! {
static FRACTAL_LOCAL: RefCell<HashMap<usize, Py<Py>>> = RefCell::new(HashMap::new());
}
static FRACTAL_GLOBAL: std::sync::LazyLock<DashMap<usize, Py<Py>>> =
std::sync::LazyLock::new(DashMap::new);
pub fn fractal_get(py: Python<'_>, key: usize) -> Option<Py<Py>> {
match get_mode() {
CacheMode::ThreadLocal => {
FRACTAL_LOCAL.with(|m| m.borrow().get(&key).map(|p| p.clone_ref(py)))
}
CacheMode::Global => FRACTAL_GLOBAL.get(&key).map(|p| p.clone_ref(py)),
}
}
pub fn fractal_insert(py: Python<'_>, key: usize, obj: &Py<Py>) {
match get_mode() {
CacheMode::ThreadLocal => FRACTAL_LOCAL.with(|m| {
let mut m = m.borrow_mut();
m.retain(|_, v| v.get_refcnt(py) > 1);
m.insert(key, obj.clone_ref(py));
}),
CacheMode::Global => {
FRACTAL_GLOBAL.retain(|_, v| v.get_refcnt(py) > 1);
FRACTAL_GLOBAL.insert(key, obj.clone_ref(py));
}
}
}
// ========== DASHED_IDENTITY ==========
use crate::structure_py::线Py;
thread_local! {
static DASHED_LOCAL: RefCell<HashMap<usize, Py<线Py>>> = RefCell::new(HashMap::new());
}
static DASHED_GLOBAL: std::sync::LazyLock<DashMap<usize, Py<线Py>>> =
std::sync::LazyLock::new(DashMap::new);
pub fn dashed_get(py: Python<'_>, key: usize) -> Option<Py<线Py>> {
match get_mode() {
CacheMode::ThreadLocal => {
DASHED_LOCAL.with(|m| m.borrow().get(&key).map(|p| p.clone_ref(py)))
}
CacheMode::Global => DASHED_GLOBAL.get(&key).map(|p| p.clone_ref(py)),
}
}
pub fn dashed_insert(py: Python<'_>, key: usize, obj: &Py<线Py>) {
match get_mode() {
CacheMode::ThreadLocal => DASHED_LOCAL.with(|m| {
let mut m = m.borrow_mut();
m.retain(|_, v| v.get_refcnt(py) > 1);
m.insert(key, obj.clone_ref(py));
}),
CacheMode::Global => {
DASHED_GLOBAL.retain(|_, v| v.get_refcnt(py) > 1);
DASHED_GLOBAL.insert(key, obj.clone_ref(py));
}
}
}
// ========== HUB_IDENTITY ==========
use crate::algorithm_py::Py;
thread_local! {
static HUB_LOCAL: RefCell<HashMap<usize, Py<Py>>> = RefCell::new(HashMap::new());
}
static HUB_GLOBAL: std::sync::LazyLock<DashMap<usize, Py<Py>>> =
std::sync::LazyLock::new(DashMap::new);
pub fn hub_get(py: Python<'_>, key: usize) -> Option<Py<Py>> {
match get_mode() {
CacheMode::ThreadLocal => HUB_LOCAL.with(|m| m.borrow().get(&key).map(|p| p.clone_ref(py))),
CacheMode::Global => HUB_GLOBAL.get(&key).map(|p| p.clone_ref(py)),
}
}
pub fn hub_insert(py: Python<'_>, key: usize, obj: &Py<Py>) {
match get_mode() {
CacheMode::ThreadLocal => HUB_LOCAL.with(|m| {
let mut m = m.borrow_mut();
m.retain(|_, v| v.get_refcnt(py) > 1);
m.insert(key, obj.clone_ref(py));
}),
CacheMode::Global => {
HUB_GLOBAL.retain(|_, v| v.get_refcnt(py) > 1);
HUB_GLOBAL.insert(key, obj.clone_ref(py));
}
}
}
// ========== BSP_CACHE ==========
thread_local! {
static BSP_LOCAL: RefCell<HashMap<usize, Py<PySet>>> = RefCell::new(HashMap::new());
}
static BSP_GLOBAL: std::sync::LazyLock<DashMap<usize, Py<PySet>>> =
std::sync::LazyLock::new(DashMap::new);
pub fn bsp_get(py: Python<'_>, key: usize) -> Option<Py<PySet>> {
match get_mode() {
CacheMode::ThreadLocal => BSP_LOCAL.with(|m| m.borrow().get(&key).map(|p| p.clone_ref(py))),
CacheMode::Global => BSP_GLOBAL.get(&key).map(|p| p.clone_ref(py)),
}
}
pub fn bsp_insert(py: Python<'_>, key: usize, obj: Py<PySet>) {
match get_mode() {
CacheMode::ThreadLocal => BSP_LOCAL.with(|m| {
m.borrow_mut().insert(key, obj);
}),
CacheMode::Global => {
BSP_GLOBAL.insert(key, obj);
}
}
}
+92 -7
View File
@@ -22,10 +22,11 @@
* SOFTWARE.
*/
use chanlun::warn;
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyType};
use std::collections::HashMap;
use tracing::warn;
use std::sync::atomic::{AtomicU64, Ordering};
/// 缠论配置 — 控制所有分析阶段行为的参数集(共 60+ 字段,均有默认值)。
///
@@ -95,6 +96,8 @@ use tracing::warn;
#[pyclass(name = "缠论配置", module = "chanlun._chanlun")]
pub struct Py {
fields: HashMap<String, Py<PyAny>>,
: parking_lot::Mutex<Option<chanlun::config::>>,
pub(crate) : AtomicU64,
}
#[pymethods]
@@ -120,7 +123,11 @@ impl 缠论配置Py {
// 全部通过 serde_json 往返验证类型,统一处理字符串数字/布尔强制转换
let config = dict_to_rust_config(&fields)?;
let fields = config_to_field_dict(&config)?;
Ok(Self { fields })
Ok(Self {
fields,
: parking_lot::Mutex::new(Some(config)),
: AtomicU64::new(1),
})
}
fn __getattr__(&self, name: &str, py: Python<'_>) -> PyResult<Py<PyAny>> {
@@ -135,10 +142,13 @@ impl 缠论配置Py {
fn __setattr__(&mut self, name: &str, value: &Bound<'_, PyAny>) -> PyResult<()> {
if self.fields.contains_key(name) {
self.fields.insert(name.to_string(), value.clone().unbind());
*self..lock() = None;
self..fetch_add(1, Ordering::Relaxed);
// 通过 serde 往返验证类型
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!(
@@ -217,7 +227,11 @@ impl 缠论配置Py {
let config = dict_to_rust_config(&fields)?;
let fields = config_to_field_dict(&config)?;
Ok(Self { fields })
Ok(Self {
fields,
: parking_lot::Mutex::new(Some(config)),
: AtomicU64::new(1),
})
}
#[classmethod]
@@ -231,7 +245,16 @@ impl 缠论配置Py {
fn (_cls: &Bound<'_, PyType>) -> PyResult<Self> {
let config = chanlun::config::::default().();
let fields = config_to_field_dict(&config)?;
Ok(Self { fields })
Ok(Self {
fields,
: parking_lot::Mutex::new(Some(config)),
: AtomicU64::new(1),
})
}
/// 判断指定标签是否应展示。None = 全部展示,空列表 = 全部隐藏。
fn (&self, : &str) -> bool {
self..lock().as_ref().map_or(true, |c| c.())
}
#[classmethod]
@@ -286,6 +309,55 @@ 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 {
@@ -302,18 +374,31 @@ impl 缠论配置Py {
let config = dict_to_rust_config(&fields)?;
let fields = config_to_field_dict(&config)?;
Ok(Self { fields })
Ok(Self {
fields,
: parking_lot::Mutex::new(Some(config)),
: AtomicU64::new(1),
})
}
pub(crate) fn to_rust_config(
&self,
_py: Python<'_>,
) -> PyResult<chanlun::config::> {
dict_to_rust_config(&self.fields)
if let Some(ref cached) = *self..lock() {
return Ok(cached.clone());
}
let config = dict_to_rust_config(&self.fields)?;
*self..lock() = Some(config.clone());
Ok(config)
}
pub(crate) fn from_rust_config(config: &chanlun::config::) -> PyResult<Self> {
config_to_field_dict(config).map(|fields| Self { fields })
config_to_field_dict(config).map(|fields| Self {
fields,
: parking_lot::Mutex::new(Some(config.clone())),
: AtomicU64::new(1),
})
}
}
+291 -373
View File
@@ -22,33 +22,10 @@
* SOFTWARE.
*/
use std::num::NonZeroUsize;
use crate::business_py::Py;
use crate::business_py::Py;
use std::sync::Mutex;
use lru::LruCache;
use pyo3::prelude::*;
/// 缓存辅助宏:在调用点创建静态 LruCache,先查后存
macro_rules! with_cache {
($cache:ident, $size:literal, $key_expr:expr, $compute:expr) => {{
use std::sync::LazyLock;
static $cache: LazyLock<Mutex<LruCache<(usize, usize, i64), (bool, String)>>> =
LazyLock::new(|| Mutex::new(LruCache::new(NonZeroUsize::new($size).unwrap())));
let key = $key_expr;
if let Some(cached) = $cache.lock().unwrap().get(&key) {
return Ok(cached.clone());
}
let result: PyResult<(bool, String)> = $compute;
if let Ok(ref r) = result {
$cache.lock().unwrap().put(key, r.clone());
}
result
}};
}
/// 从 Python 值中提取时间戳(兼容 i64 和 datetime 两种类型)
fn (val: &Bound<'_, PyAny>) -> PyResult<i64> {
if let Ok(ts) = val.extract::<i64>() {
@@ -129,71 +106,60 @@ fn K线相等(
B: &Bound<'_, PyAny>,
: f64,
) -> PyResult<(bool, String)> {
with_cache!(
C_KLINE,
128,
(
A.as_ptr() as usize,
B.as_ptr() as usize,
.to_bits() as i64
),
{
// 快速路径
if let (Ok(a), Ok(b)) = (
A.cast::<crate::kline_py::K线Py>(),
B.cast::<crate::kline_py::K线Py>(),
) {
return Ok(a.borrow().inner.(&b.borrow().inner, ));
}
// 回退路径
let = "K线校验";
let = [
"标识",
"序号",
"周期",
"时间戳",
"",
"",
"开盘价",
"收盘价",
"成交量",
];
for & in & {
let (a有, b有) = (A.hasattr()?, B.hasattr()?);
if a有 && !b有 {
return Ok((false, format!("{标签}: [{字段}] A存在属性 B缺失属性")));
}
if !a有 && b有 {
return Ok((false, format!("{标签}: [{字段}] B存在属性 A缺失属性")));
}
if !a有 && !b有 {
continue;
}
let valA = A.getattr()?;
let valB = B.getattr()?;
if let Some(r) = (&valA, &valB, ) {
let (ok, m) = r?;
if !ok {
return Ok((false, format!("{标签}: [{字段}]{}", m)));
}
} else if == "时间戳" {
let a = (&valA).unwrap_or(0);
let b = (&valB).unwrap_or(0);
if a != b {
return Ok((false, format!("{标签}: [{字段}] 数值不等 A={a},B={b}")));
}
} else {
let eq: bool = valA.eq(&valB)?;
if !eq {
let ra = valA.repr()?.extract::<String>().unwrap_or_default();
let rb = valB.repr()?.extract::<String>().unwrap_or_default();
return Ok((false, format!("{标签}: [{字段}] 数值不等 A={ra},B={rb}")));
}
}
}
Ok((true, format!("{标签}: 全部字段一致")))
// 快速路径
if let (Ok(a), Ok(b)) = (
A.cast::<crate::kline_py::K线Py>(),
B.cast::<crate::kline_py::K线Py>(),
) {
return Ok(a.borrow().inner.(&b.borrow().inner, ));
}
// 回退路径
let = "K线校验";
let = [
"标识",
"序号",
"周期",
"时间戳",
"",
"",
"开盘价",
"收盘价",
"成交量",
];
for & in & {
let (a有, b有) = (A.hasattr()?, B.hasattr()?);
if a有 && !b有 {
return Ok((false, format!("{标签}: [{字段}] A存在属性 B缺失属性")));
}
)
if !a有 && b有 {
return Ok((false, format!("{标签}: [{字段}] B存在属性 A缺失属性")));
}
if !a有 && !b有 {
continue;
}
let valA = A.getattr()?;
let valB = B.getattr()?;
if let Some(r) = (&valA, &valB, ) {
let (ok, m) = r?;
if !ok {
return Ok((false, format!("{标签}: [{字段}]{}", m)));
}
} else if == "时间戳" {
let a = (&valA).unwrap_or(0);
let b = (&valB).unwrap_or(0);
if a != b {
return Ok((false, format!("{标签}: [{字段}] 数值不等 A={a},B={b}")));
}
} else {
let eq: bool = valA.eq(&valB)?;
if !eq {
let ra = valA.repr()?.extract::<String>().unwrap_or_default();
let rb = valB.repr()?.extract::<String>().unwrap_or_default();
return Ok((false, format!("{标签}: [{字段}] 数值不等 A={ra},B={rb}")));
}
}
}
Ok((true, format!("{标签}: 全部字段一致")))
}
// ========== 缠论K线相等 ==========
@@ -205,102 +171,91 @@ fn 缠论K线相等(
B: &Bound<'_, PyAny>,
: f64,
) -> PyResult<(bool, String)> {
with_cache!(
C_CHAN_K,
4096,
(
A.as_ptr() as usize,
B.as_ptr() as usize,
.to_bits() as i64
),
{
if let (Ok(a), Ok(b)) = (
A.cast::<crate::kline_py::K线Py>(),
B.cast::<crate::kline_py::K线Py>(),
) {
return Ok(a.borrow().inner.(&b.borrow().inner, ));
if let (Ok(a), Ok(b)) = (
A.cast::<crate::kline_py::K线Py>(),
B.cast::<crate::kline_py::K线Py>(),
) {
return Ok(a.borrow().inner.(&b.borrow().inner, ));
}
let = "缠论K线校验";
let = [
"序号",
"时间戳",
"",
"",
"方向",
"分型",
"周期",
"标识",
"分型特征值",
"原始起始序号",
"原始结束序号",
"标的K线",
"买卖点信息",
];
for & in & {
let (a有, b有) = (A.hasattr()?, B.hasattr()?);
if a有 && !b有 {
return Ok((false, format!("{标签}: [{字段}] A存在 B缺失属性")));
}
if !a有 && b有 {
return Ok((false, format!("{标签}: [{字段}] B存在 A缺失属性")));
}
if !a有 && !b有 {
continue;
}
let valA = A.getattr()?;
let valB = B.getattr()?;
if let Some(r) = (&valA, &valB, ) {
let (ok, m) = r?;
if !ok {
return Ok((false, format!("{标签}: [{字段}]{m}")));
}
let = "缠论K线校验";
let = [
"序号",
"时间戳",
"",
"",
"方向",
"分型",
"周期",
"标识",
"分型特征值",
"原始起始序号",
"原始结束序号",
"标的K线",
"买卖点信息",
];
for & in & {
let (a有, b有) = (A.hasattr()?, B.hasattr()?);
if a有 && !b有 {
return Ok((false, format!("{标签}: [{字段}] A存在 B缺失属性")));
}
if !a有 && b有 {
return Ok((false, format!("{标签}: [{字段}] B存在 A缺失属性")));
}
if !a有 && !b有 {
} else if == "标的K线" {
if let Some(r) = (&valA, &valB, , ) {
if !r.0 {
return Ok((false, r.1));
} else {
continue;
}
let valA = A.getattr()?;
let valB = B.getattr()?;
if let Some(r) = (&valA, &valB, ) {
let (ok, m) = r?;
if !ok {
return Ok((false, format!("{标签}: [{字段}]{m}")));
}
} else if == "标的K线" {
if let Some(r) = (&valA, &valB, , ) {
if !r.0 {
return Ok((false, r.1));
} else {
continue;
}
}
let (eq, msg) = K线相等(&valA, &valB, )?;
if !eq {
return Ok((false, format!("{标签}: 标的K线子项异常 >> {msg}")));
}
} else if == "时间戳" {
let a = (&valA).unwrap_or(0);
let b = (&valB).unwrap_or(0);
if a != b {
return Ok((false, format!("{标签}: [{字段}] 数值不等 A={a},B={b}")));
}
} else if == "方向" || == "分型" {
let sa = valA.str()?.extract::<String>().unwrap_or_default();
let sb = valB.str()?.extract::<String>().unwrap_or_default();
if sa != sb {
return Ok((false, format!("{标签}: [{字段}] 数值不等 A={sa},B={sb}")));
}
} else if == "买卖点信息" {
let py = A.py();
let set_a = py.import("builtins")?.getattr("set")?.call1((&valA,))?;
let set_b = py.import("builtins")?.getattr("set")?.call1((&valB,))?;
let eq: bool = set_a.eq(set_b)?;
if !eq {
let ra = valA.repr()?.extract::<String>().unwrap_or_default();
let rb = valB.repr()?.extract::<String>().unwrap_or_default();
return Ok((false, format!("{标签}: [{字段}] 数值不等 A={ra},B={rb}")));
}
} else {
let eq: bool = valA.eq(&valB)?;
if !eq {
let ra = valA.repr()?.extract::<String>().unwrap_or_default();
let rb = valB.repr()?.extract::<String>().unwrap_or_default();
return Ok((false, format!("{标签}: [{字段}] 数值不等 A={ra},B={rb}")));
}
}
}
Ok((true, format!("{标签}: 全部字段嵌套校验一致")))
let (eq, msg) = K线相等(&valA, &valB, )?;
if !eq {
return Ok((false, format!("{标签}: 标的K线子项异常 >> {msg}")));
}
} else if == "时间戳" {
let a = (&valA).unwrap_or(0);
let b = (&valB).unwrap_or(0);
if a != b {
return Ok((false, format!("{标签}: [{字段}] 数值不等 A={a},B={b}")));
}
} else if == "方向" || == "分型" {
let sa = valA.str()?.extract::<String>().unwrap_or_default();
let sb = valB.str()?.extract::<String>().unwrap_or_default();
if sa != sb {
return Ok((false, format!("{标签}: [{字段}] 数值不等 A={sa},B={sb}")));
}
} else if == "买卖点信息" {
let py = A.py();
let set_a = py.import("builtins")?.getattr("set")?.call1((&valA,))?;
let set_b = py.import("builtins")?.getattr("set")?.call1((&valB,))?;
let eq: bool = set_a.eq(set_b)?;
if !eq {
let ra = valA.repr()?.extract::<String>().unwrap_or_default();
let rb = valB.repr()?.extract::<String>().unwrap_or_default();
return Ok((false, format!("{标签}: [{字段}] 数值不等 A={ra},B={rb}")));
}
} else {
let eq: bool = valA.eq(&valB)?;
if !eq {
let ra = valA.repr()?.extract::<String>().unwrap_or_default();
let rb = valB.repr()?.extract::<String>().unwrap_or_default();
return Ok((false, format!("{标签}: [{字段}] 数值不等 A={ra},B={rb}")));
}
}
)
}
Ok((true, format!("{标签}: 全部字段嵌套校验一致")))
}
// ========== 分型相等 ==========
@@ -312,88 +267,77 @@ fn 分型相等(
B: &Bound<'_, PyAny>,
: f64,
) -> PyResult<(bool, String)> {
with_cache!(
C_FRACTAL,
4096,
(
A.as_ptr() as usize,
B.as_ptr() as usize,
.to_bits() as i64
),
{
if let (Ok(a), Ok(b)) = (
A.cast::<crate::structure_py::Py>(),
B.cast::<crate::structure_py::Py>(),
) {
return Ok(a.borrow().inner.(&b.borrow().inner, ));
if let (Ok(a), Ok(b)) = (
A.cast::<crate::structure_py::Py>(),
B.cast::<crate::structure_py::Py>(),
) {
return Ok(a.borrow().inner.(&b.borrow().inner, ));
}
let = "分型校验";
// Python 分型内部用 _结构/_时间戳/_分型特征值 作为 slot 名,Rust 用 结构/时间戳/分型特征值 作为 getter
for & in &["", "", ""] {
let valA = A.getattr()?;
let valB = B.getattr()?;
if let Some(r) = (&valA, &valB, , ) {
if !r.0 {
return Ok((false, r.1));
} else {
continue;
}
let = "分型校验";
// Python 分型内部用 _结构/_时间戳/_分型特征值 作为 slot 名,Rust 用 结构/时间戳/分型特征值 作为 getter
for & in &["", "", ""] {
let valA = A.getattr()?;
let valB = B.getattr()?;
if let Some(r) = (&valA, &valB, , ) {
if !r.0 {
return Ok((false, r.1));
} else {
continue;
}
}
let (eq, msg) = K线相等(&valA, &valB, )?;
if !eq {
return Ok((false, format!("{标签}: [{字段}]缠论K线子项异常 >> {msg}")));
}
}
for &(, ) in &[
("_结构", "结构"),
("_时间戳", "时间戳"),
("_分型特征值", "分型特征值"),
] {
// 先尝试 Python 侧的下划线名,再尝试 Rust 侧的无下划线名
let valA = (A, &[, ])?;
let valB = (B, &[, ])?;
let (a有, b有) = (valA.is_some(), valB.is_some());
if a有 && !b有 {
return Ok((false, format!("{标签}: [{字段}] A存在属性 B缺失属性")));
}
if !a有 && b有 {
return Ok((false, format!("{标签}: [{字段}] B存在属性 A缺失属性")));
}
if !a有 && !b有 {
continue;
}
let valA = valA.unwrap();
let valB = valB.unwrap();
if let Some(r) = (&valA, &valB, ) {
let (ok, m) = r?;
if !ok {
return Ok((false, format!("{标签}: [{字段}]{m}")));
}
} else if == "_时间戳" {
let a = (&valA).unwrap_or(0);
let b = (&valB).unwrap_or(0);
if a != b {
return Ok((false, format!("{标签}: [{字段}] 数值不等 A={a},B={b}")));
}
} else if == "_结构" {
let sa = valA.str()?.extract::<String>().unwrap_or_default();
let sb = valB.str()?.extract::<String>().unwrap_or_default();
if sa != sb {
return Ok((false, format!("{标签}: [{字段}] 数值不等 A={sa},B={sb}")));
}
} else {
let eq: bool = valA.eq(&valB)?;
if !eq {
let ra = valA.repr()?.extract::<String>().unwrap_or_default();
let rb = valB.repr()?.extract::<String>().unwrap_or_default();
return Ok((false, format!("{标签}: [{字段}] 数值不等 A={ra},B={rb}")));
}
}
}
Ok((true, format!("{标签}: 自有字段+三根缠论K线全部校验一致")))
}
)
let (eq, msg) = K线相等(&valA, &valB, )?;
if !eq {
return Ok((false, format!("{标签}: [{字段}]缠论K线子项异常 >> {msg}")));
}
}
for &(, ) in &[
("_结构", "结构"),
("_时间戳", "时间戳"),
("_分型特征值", "分型特征值"),
] {
// 先尝试 Python 侧的下划线名,再尝试 Rust 侧的无下划线名
let valA = (A, &[, ])?;
let valB = (B, &[, ])?;
let (a有, b有) = (valA.is_some(), valB.is_some());
if a有 && !b有 {
return Ok((false, format!("{标签}: [{字段}] A存在属性 B缺失属性")));
}
if !a有 && b有 {
return Ok((false, format!("{标签}: [{字段}] B存在属性 A缺失属性")));
}
if !a有 && !b有 {
continue;
}
let valA = valA.unwrap();
let valB = valB.unwrap();
if let Some(r) = (&valA, &valB, ) {
let (ok, m) = r?;
if !ok {
return Ok((false, format!("{标签}: [{字段}]{m}")));
}
} else if == "_时间戳" {
let a = (&valA).unwrap_or(0);
let b = (&valB).unwrap_or(0);
if a != b {
return Ok((false, format!("{标签}: [{字段}] 数值不等 A={a},B={b}")));
}
} else if == "_结构" {
let sa = valA.str()?.extract::<String>().unwrap_or_default();
let sb = valB.str()?.extract::<String>().unwrap_or_default();
if sa != sb {
return Ok((false, format!("{标签}: [{字段}] 数值不等 A={sa},B={sb}")));
}
} else {
let eq: bool = valA.eq(&valB)?;
if !eq {
let ra = valA.repr()?.extract::<String>().unwrap_or_default();
let rb = valB.repr()?.extract::<String>().unwrap_or_default();
return Ok((false, format!("{标签}: [{字段}] 数值不等 A={ra},B={rb}")));
}
}
}
Ok((true, format!("{标签}: 自有字段+三根缠论K线全部校验一致")))
}
// ========== 缺口相等 ==========
@@ -405,54 +349,42 @@ fn 缺口相等(
B: &Bound<'_, PyAny>,
: f64,
) -> PyResult<(bool, String)> {
with_cache!(
C_GAP,
4096,
(
A.as_ptr() as usize,
B.as_ptr() as usize,
.to_bits() as i64
),
{
if let (Ok(a), Ok(b)) = (
A.cast::<crate::types_py::Py>(),
B.cast::<crate::types_py::Py>(),
) {
return Ok(a.borrow().inner.(&b.borrow().inner, ));
}
let = "缺口校验";
for & in &["", ""] {
let (a有, b有) = (A.hasattr()?, B.hasattr()?);
if a有 && !b有 {
return Ok((false, format!("{标签}: [{字段}] A存在 B缺失属性")));
}
if !a有 && b有 {
return Ok((false, format!("{标签}: [{字段}] B存在 A缺失属性")));
}
if !a有 && !b有 {
continue;
}
let valA = A.getattr()?;
let valB = B.getattr()?;
if let Some(r) = (&valA, &valB, ) {
let (ok, m) = r?;
if !ok {
return Ok((false, format!("{标签}: [{字段}]{m}")));
}
} else {
let eq: bool = valA.eq(&valB)?;
if !eq {
let ra = valA.repr()?.extract::<String>().unwrap_or_default();
let rb = valB.repr()?.extract::<String>().unwrap_or_default();
return Ok((false, format!("{标签}: [{字段}] 数值不等 A={ra},B={rb}")));
}
}
}
Ok((true, format!("{标签}: 上下沿价格校验完全一致")))
if let (Ok(a), Ok(b)) = (
A.cast::<crate::types_py::Py>(),
B.cast::<crate::types_py::Py>(),
) {
return Ok(a.borrow().inner.(&b.borrow().inner, ));
}
let = "缺口校验";
for & in &["", ""] {
let (a有, b有) = (A.hasattr()?, B.hasattr()?);
if a有 && !b有 {
return Ok((false, format!("{标签}: [{字段}] A存在 B缺失属性")));
}
)
if !a有 && b有 {
return Ok((false, format!("{标签}: [{字段}] B存在 A缺失属性")));
}
if !a有 && !b有 {
continue;
}
let valA = A.getattr()?;
let valB = B.getattr()?;
if let Some(r) = (&valA, &valB, ) {
let (ok, m) = r?;
if !ok {
return Ok((false, format!("{标签}: [{字段}]{m}")));
}
} else {
let eq: bool = valA.eq(&valB)?;
if !eq {
let ra = valA.repr()?.extract::<String>().unwrap_or_default();
let rb = valB.repr()?.extract::<String>().unwrap_or_default();
return Ok((false, format!("{标签}: [{字段}] 数值不等 A={ra},B={rb}")));
}
}
}
Ok((true, format!("{标签}: 上下沿价格校验完全一致")))
}
// ========== 线段特征相等 ==========
#[pyfunction]
@@ -462,74 +394,60 @@ fn 线段特征相等(
B: &Bound<'_, PyAny>,
: f64,
) -> PyResult<(bool, String)> {
with_cache!(
C_SEG_FEAT,
4096,
(
A.as_ptr() as usize,
B.as_ptr() as usize,
.to_bits() as i64
),
{
if let (Ok(a), Ok(b)) = (
A.cast::<crate::structure_py::线Py>(),
B.cast::<crate::structure_py::线Py>(),
) {
return Ok(a.borrow().inner.(&b.borrow().inner, ));
}
let = "线段特征校验";
for & in &["序号", "标识", "线段方向", "基础序列"] {
let (a有, b有) = (A.hasattr()?, B.hasattr()?);
if a有 && !b有 {
return Ok((false, format!("{标签}: [{字段}] A存在 B缺失属性")));
}
if !a有 && b有 {
return Ok((false, format!("{标签}: [{字段}] B存在 A缺失属性")));
}
if !a有 && !b有 {
continue;
}
let valA = A.getattr()?;
let valB = B.getattr()?;
if == "基础序列" {
let len_a: usize = valA.len()?;
let len_b: usize = valB.len()?;
if len_a != len_b {
return Ok((
false,
format!("{标签}: [基础序列] 列表长度不一致 A={len_a},B={len_b}"),
));
}
for idx in 0..len_a {
let itemA = valA.get_item(idx)?;
let itemB = valB.get_item(idx)?;
let (eq, msg) = 线(&itemA, &itemB, )?;
if !eq {
return Ok((
false,
format!("{标签}: 基础序列[{idx}]子虚线异常 >> {msg}"),
));
}
}
} else if == "线段方向" {
let sa = valA.str()?.extract::<String>().unwrap_or_default();
let sb = valB.str()?.extract::<String>().unwrap_or_default();
if sa != sb {
return Ok((false, format!("{标签}: [{字段}] 数值不等 A={sa},B={sb}")));
}
} else {
let eq: bool = valA.eq(&valB)?;
if !eq {
let ra = valA.repr()?.extract::<String>().unwrap_or_default();
let rb = valB.repr()?.extract::<String>().unwrap_or_default();
return Ok((false, format!("{标签}: [{字段}] 数值不等 A={ra},B={rb}")));
}
}
}
Ok((true, format!("{标签}: 字段与内部虚线序列全部一致")))
if let (Ok(a), Ok(b)) = (
A.cast::<crate::structure_py::线Py>(),
B.cast::<crate::structure_py::线Py>(),
) {
return Ok(a.borrow().inner.(&b.borrow().inner, ));
}
let = "线段特征校验";
for & in &["序号", "标识", "线段方向", "基础序列"] {
let (a有, b有) = (A.hasattr()?, B.hasattr()?);
if a有 && !b有 {
return Ok((false, format!("{标签}: [{字段}] A存在 B缺失属性")));
}
)
if !a有 && b有 {
return Ok((false, format!("{标签}: [{字段}] B存在 A缺失属性")));
}
if !a有 && !b有 {
continue;
}
let valA = A.getattr()?;
let valB = B.getattr()?;
if == "基础序列" {
let len_a: usize = valA.len()?;
let len_b: usize = valB.len()?;
if len_a != len_b {
return Ok((
false,
format!("{标签}: [基础序列] 列表长度不一致 A={len_a},B={len_b}"),
));
}
for idx in 0..len_a {
let itemA = valA.get_item(idx)?;
let itemB = valB.get_item(idx)?;
let (eq, msg) = 线(&itemA, &itemB, )?;
if !eq {
return Ok((false, format!("{标签}: 基础序列[{idx}]子虚线异常 >> {msg}")));
}
}
} else if == "线段方向" {
let sa = valA.str()?.extract::<String>().unwrap_or_default();
let sb = valB.str()?.extract::<String>().unwrap_or_default();
if sa != sb {
return Ok((false, format!("{标签}: [{字段}] 数值不等 A={sa},B={sb}")));
}
} else {
let eq: bool = valA.eq(&valB)?;
if !eq {
let ra = valA.repr()?.extract::<String>().unwrap_or_default();
let rb = valB.repr()?.extract::<String>().unwrap_or_default();
return Ok((false, format!("{标签}: [{字段}] 数值不等 A={ra},B={rb}")));
}
}
}
Ok((true, format!("{标签}: 字段与内部虚线序列全部一致")))
}
// ========== 中枢相等 ==========
@@ -790,8 +708,8 @@ fn 观察者相等(
.inner
.clone()
.ok_or_else(|| pyo3::exceptions::PyValueError::new_err("观察者B 内部为空"))?;
let obs_a = arc_a.read().unwrap();
let obs_b = arc_b.read().unwrap();
let obs_a = arc_a.read();
let obs_b = arc_b.read();
Ok(obs_a.(&obs_b, ))
}
+17 -21
View File
@@ -290,7 +290,7 @@ impl 相对强弱指数Py {
}
#[getter]
fn RSI历史队列(&self) -> Vec<f64> {
self.inner.RSI历史队列.clone()
self.inner.RSI历史队列.iter().copied().collect()
}
fn __str__(&self) -> String {
@@ -470,11 +470,11 @@ impl 随机指标Py {
}
#[getter]
fn (&self) -> Vec<f64> {
self.inner..clone()
self.inner..iter().copied().collect()
}
#[getter]
fn (&self) -> Vec<f64> {
self.inner..clone()
self.inner..iter().copied().collect()
}
#[getter]
fn RSV(&self) -> Option<f64> {
@@ -796,16 +796,12 @@ impl 指标容器Py {
}
fn __getitem__(&self, : &str, py: Python<'_>) -> PyResult<Py<PyAny>> {
if self.() {
match self.inner.() {
Some(v) => _to_py(v, py),
None => Ok(py.None()),
}
} else {
Err(pyo3::exceptions::PyKeyError::new_err(format!(
match self.inner.() {
Some(v) => _to_py(v, py),
None => Err(pyo3::exceptions::PyKeyError::new_err(format!(
"指标 '{}' 不存在",
)))
))),
}
}
@@ -822,16 +818,12 @@ impl 指标容器Py {
}
return Ok(dict.into());
}
if self.() {
match self.inner.() {
Some(v) => _to_py(v, py),
None => Ok(py.None()),
}
} else {
Err(pyo3::exceptions::PyAttributeError::new_err(format!(
match self.inner.() {
Some(v) => _to_py(v, py),
None => Err(pyo3::exceptions::PyAttributeError::new_err(format!(
"指标 '{}' 不存在于 指标容器 中",
)))
))),
}
}
@@ -917,7 +909,12 @@ impl 均线工具Py {
return Ok(sum / (n.max(1)) as f64);
}
let prev_key = format!("SMA_{}", period);
let prev_key = {
let mut s = String::with_capacity(8);
use std::fmt::Write;
write!(&mut s, "SMA_{}", period).unwrap();
s
};
// 尝试从前一根K线的均线缓存中读取
let prev_cached = K序列[n - 2]
.bind(py)
@@ -925,7 +922,6 @@ impl 均线工具Py {
.inner
.
.read()
.unwrap()
.线()
.and_then(|m| m.get(&prev_key))
.copied();
+62 -57
View File
@@ -22,16 +22,16 @@
* SOFTWARE.
*/
use parking_lot::RwLock;
use pyo3::prelude::*;
use pyo3::types::{PyBytes, PyDict, PyList, PyType};
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::RwLock;
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;
@@ -149,7 +149,6 @@ impl K线Py {
self.inner
.
.read()
.unwrap()
.macd_cloned()
.map(|m| 线Py { inner: m })
}
@@ -159,7 +158,6 @@ impl K线Py {
self.inner
.
.read()
.unwrap()
.rsi_cloned()
.map(|r| Py { inner: r })
}
@@ -169,16 +167,29 @@ impl K线Py {
self.inner
.
.read()
.unwrap()
.kdj_cloned()
.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 {
Py {
inner: self.inner..read().unwrap().clone(),
inner: self.inner..read().clone(),
}
}
@@ -339,6 +350,39 @@ impl K线Py {
.take(end_idx - start_idx + 1)
.collect())
}
/// 根据当前K线和方向生成下一根K线(用于随机回测)
#[pyo3(signature = (方向, 居中 = false))]
fn K线生成新K线(
&self, : &Bound<'_, PyAny>, : bool
) -> PyResult<Self> {
let dir: chanlun::types:: = if let Ok(d) = .extract::<PyRef<'_, Py>>()
{
d.inner
} else if let Ok(i) = .extract::<i64>() {
match i {
0 => chanlun::types::::,
1 => chanlun::types::::,
2 => chanlun::types::::,
3 => chanlun::types::::,
4 => chanlun::types::::,
5 => chanlun::types::::,
_ => {
return Err(pyo3::exceptions::PyValueError::new_err(format!(
"无效方向: {i}"
)));
}
}
} else {
return Err(pyo3::exceptions::PyTypeError::new_err(
"方向 必须是 相对方向 或 int (0-5)",
));
};
let new_bar = self.inner.K线生成新K线(dir, );
Ok(Self {
inner: Arc::new(new_bar),
})
}
}
// ========== 缠论K线 ==========
@@ -371,57 +415,29 @@ impl 缠论K线Py {
}
}
/// 对象标识缓存:Arc 地址 → 规范 Python 对象
/// 确保同一底层 Arc 指针在 Python 侧始终映射到同一 PyObject
/// 使用全局 static 而非 thread_local!,保证跨线程对象标识和买卖点信息一致性
static BAR_IDENTITY: std::sync::LazyLock<RwLock<HashMap<usize, Py<K线Py>>>> =
std::sync::LazyLock::new(|| RwLock::new(HashMap::new()));
static KLINE_IDENTITY: std::sync::LazyLock<RwLock<HashMap<usize, Py<K线Py>>>> =
std::sync::LazyLock::new(|| RwLock::new(HashMap::new()));
/// 买卖点信息缓存 — 按 Arc 指针全局共享,确保所有 wrapper 看到同一 PySet
static BSP_CACHE: std::sync::LazyLock<RwLock<HashMap<usize, Py<pyo3::types::PySet>>>> =
std::sync::LazyLock::new(|| RwLock::new(HashMap::new()));
/// 将 Rc<K线> 转为 Py<K线Py>,确保同一 Rc 地址总是返回同一 Python 对象
pub(crate) fn bar_to_py(
py: Python<'_>,
inner: std::sync::Arc<chanlun::kline::bar::K线>,
) -> Py<K线Py> {
let key = Arc::as_ptr(&inner) as usize;
if let Some(cached) = BAR_IDENTITY
.read()
.unwrap()
.get(&key)
.map(|p| p.clone_ref(py))
{
if let Some(cached) = crate::cache::bar_get(py, key) {
return cached;
}
let obj = Py::new(py, K线Py { inner }).unwrap();
BAR_IDENTITY.write().unwrap().insert(key, obj.clone_ref(py));
crate::cache::bar_insert(py, key, &obj);
obj
}
/// 将 Rc<缠论K线> 转为 Py<缠论K线Py>,确保同一 Rc 地址总是返回同一 Python 对象
pub(crate) fn chan_kline_to_py(
py: Python<'_>,
inner: std::sync::Arc<chanlun::kline::chan_kline::K线>,
) -> Py<K线Py> {
let key = Arc::as_ptr(&inner) as usize;
if let Some(cached) = KLINE_IDENTITY
.read()
.unwrap()
.get(&key)
.map(|p| p.clone_ref(py))
{
if let Some(cached) = crate::cache::kline_get(py, key) {
return cached;
}
let obj = Py::new(py, K线Py::from_rc(inner)).unwrap();
KLINE_IDENTITY
.write()
.unwrap()
.insert(key, obj.clone_ref(py));
crate::cache::kline_insert(py, key, &obj);
obj
}
@@ -462,7 +478,7 @@ impl 缠论K线Py {
#[getter]
fn (&self, py: Python<'_>) -> Py<Py> {
crate::types_py::(py, *self.inner..read().unwrap())
crate::types_py::(py, *self.inner..read())
}
#[getter]
@@ -470,7 +486,6 @@ impl 缠论K线Py {
self.inner
.
.read()
.unwrap()
.map(|f| crate::types_py::(py, f))
}
@@ -501,7 +516,7 @@ impl 缠论K线Py {
#[getter]
fn K线(&self, py: Python<'_>) -> Py<K线Py> {
bar_to_py(py, self.inner.K线.read().unwrap().clone())
bar_to_py(py, self.inner.K线.read().clone())
}
/// pandas 兼容 — 返回所有字段构成的字典
@@ -556,11 +571,7 @@ impl 缠论K线Py {
// 复制买卖点信息到镜像
let src_key = Arc::as_ptr(&self.inner) as usize;
let dst_key = Arc::as_ptr(&mirror.inner) as usize;
let cached_src = BSP_CACHE
.read()
.unwrap()
.get(&src_key)
.map(|p| p.clone_ref(py));
let cached_src = crate::cache::bsp_get(py, src_key);
if let Some(cached_src) = cached_src
&& let Ok(new_set) = pyo3::types::PySet::empty(py)
{
@@ -568,7 +579,7 @@ impl 缠论K线Py {
let _ = new_set.add(item);
}
let py_set: Py<pyo3::types::PySet> = new_set.into();
BSP_CACHE.write().unwrap().insert(dst_key, py_set);
crate::cache::bsp_insert(py, dst_key, py_set);
}
mirror
}
@@ -595,25 +606,19 @@ impl 缠论K线Py {
fn (&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let key = Arc::as_ptr(&self.inner) as usize;
// 检查全局缓存
let cached = BSP_CACHE.read().unwrap().get(&key).map(|p| p.clone_ref(py));
let cached = crate::cache::bsp_get(py, key);
if let Some(set) = cached {
return Ok(set.into_any());
}
// 创建新的 PySet,从 Rust HashSet 同步已有内容
let set = pyo3::types::PySet::empty(py)?;
let bsp_info = self.inner..read().unwrap();
let bsp_info = self.inner..read();
for item in bsp_info.iter() {
set.add(item.as_str())?;
}
drop(bsp_info);
BSP_CACHE.write().unwrap().insert(key, set.into());
Ok(BSP_CACHE
.read()
.unwrap()
.get(&key)
.unwrap()
.clone_ref(py)
.into_any())
crate::cache::bsp_insert(py, key, set.into());
Ok(crate::cache::bsp_get(py, key).unwrap().into_any())
}
#[classmethod]
+93 -15
View File
@@ -102,10 +102,13 @@ fn init_tracing() {
mod algorithm_py;
mod business_py;
pub(crate) mod cache;
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;
@@ -121,15 +124,25 @@ fn set_分型模式(value: bool) {
chanlun::structure::fractal_obj::.store(value, Ordering::Relaxed);
}
/// 扩展线段模式 — 控制虚线高低取值方式,默认 False
#[pyfunction]
fn get_扩展线段模式() -> bool {
chanlun::structure::dash_line::线.load(Ordering::Relaxed)
}
/// 设置 扩展线段模式
#[pyfunction]
fn set_扩展线段模式(value: bool) {
chanlun::structure::dash_line::线.store(value, Ordering::Relaxed);
}
/// 获取当前日志级别 ("trace" / "debug" / "info" / "warn" / "error" / "off")
#[pyfunction]
fn get_log_level() -> &'static str {
(LOG_LEVEL.load(Ordering::Relaxed))
}
/// 设置日志级别 (不区分大小写: "trace" / "debug" / "info" / "warn" / "error" / "off")
///
/// 设为 "off" 可完全关闭日志输出。
/// 设置日志级别 — 自动启用日志,同步更新 tracing subscriber
#[pyfunction]
fn set_log_level(level: &str) -> PyResult<()> {
let = (level).ok_or_else(|| {
@@ -138,32 +151,95 @@ fn set_log_level(level: &str) -> PyResult<()> {
level
))
})?;
let guard =
.get()
.ok_or_else(|| pyo3::exceptions::PyRuntimeError::new_err("日志系统尚未初始化"))?;
let handle = guard.lock().unwrap();
let = ();
let filter = tracing_subscriber::EnvFilter::new();
handle
.reload(filter)
.map_err(|_| pyo3::exceptions::PyRuntimeError::new_err("切换日志级别失败"))?;
LOG_LEVEL.store(, Ordering::Relaxed);
chanlun::log::.store( < 5, Ordering::Relaxed);
// 同步更新 tracing subscriber
if let Some(guard) = .get() {
let handle = guard.lock().unwrap();
let = ();
let filter = tracing_subscriber::EnvFilter::new();
let _ = handle.reload(filter);
}
Ok(())
}
/// 获取日志输出模式 ("off", "simple", "tracing")
#[pyfunction]
fn get_log_mode() -> &'static str {
match chanlun::log::get_log_mode() {
0 => "off",
1 => "simple",
2 => "tracing",
_ => "unknown",
}
}
/// 设置日志输出模式(必须在任何日志输出之前调用)
/// - "off": 不输出
/// - "simple": 直接 eprintln/println(默认)
/// - "tracing": 带时间戳和格式化的 tracing subscriber
#[pyfunction]
fn set_log_mode(mode: &str) -> PyResult<()> {
let m = match mode.to_lowercase().as_str() {
"off" | "0" => 0u8,
"simple" | "on" | "1" => 1u8,
"tracing" | "2" => 2u8,
_ => {
return Err(pyo3::exceptions::PyValueError::new_err(
"无效日志模式,有效值: 'off', 'simple', 'tracing'",
));
}
};
if m == 2 {
init_tracing();
}
chanlun::log::set_log_mode(m);
Ok(())
}
/// 获取缓存模式 ("thread_local" 或 "global")
#[pyfunction]
fn get_cache_mode() -> &'static str {
match crate::cache::peek_mode().unwrap_or(&crate::cache::CacheMode::ThreadLocal) {
crate::cache::CacheMode::ThreadLocal => "thread_local",
crate::cache::CacheMode::Global => "global",
}
}
/// 设置缓存模式(必须在创建任何观察者之前调用)
#[pyfunction]
fn set_cache_mode(mode: &str) -> PyResult<()> {
let m = match mode.to_lowercase().as_str() {
"thread_local" | "local" => crate::cache::CacheMode::ThreadLocal,
"global" => crate::cache::CacheMode::Global,
_ => {
return Err(pyo3::exceptions::PyValueError::new_err(
"无效缓存模式,有效值: 'thread_local', 'global'",
));
}
};
crate::cache::set_mode(m).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e))
}
/// 缠论技术分析库 — Rust 高性能实现
#[pymodule]
/// 缠论技术分析库 — Rust 高性能实现
fn _chanlun(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
init_tracing();
chanlun::log::init_from_env();
m.add_function(wrap_pyfunction!(get_分型模式, m)?)?;
m.add_function(wrap_pyfunction!(set_分型模式, m)?)?;
m.add_function(wrap_pyfunction!(get_扩展线段模式, m)?)?;
m.add_function(wrap_pyfunction!(set_扩展线段模式, m)?)?;
m.add_function(wrap_pyfunction!(get_log_level, m)?)?;
m.add_function(wrap_pyfunction!(set_log_level, m)?)?;
m.add_function(wrap_pyfunction!(get_log_mode, m)?)?;
m.add_function(wrap_pyfunction!(set_log_mode, m)?)?;
m.add_function(wrap_pyfunction!(get_cache_mode, m)?)?;
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: 技术指标
@@ -176,6 +252,8 @@ 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(())
+281
View File
@@ -0,0 +1,281 @@
/*
* 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/czscApache 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, &params_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(())
}
+795
View File
@@ -0,0 +1,795 @@
/*
* 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_score7段),收到 {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 datetimeUTC)。
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` 时附带 statepairs, 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(())
}
+18 -75
View File
@@ -24,9 +24,7 @@
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyType};
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::RwLock;
use std::sync::atomic::Ordering;
use crate::algorithm_py::hub_to_py;
@@ -35,37 +33,18 @@ use crate::kline_py::{K线Py, bar_to_py, 缠论K线Py};
// ---- 身份缓存 (弱引用:通过 refcnt 检测存活,仅缓存持有则视为过期) ----
// 使用全局 static 而非 thread_local!,保证跨线程对象标识一致性
static FRACTAL_IDENTITY: std::sync::LazyLock<RwLock<HashMap<usize, Py<Py>>>> =
std::sync::LazyLock::new(|| RwLock::new(HashMap::new()));
static DASHED_IDENTITY: std::sync::LazyLock<RwLock<HashMap<usize, Py<线Py>>>> =
std::sync::LazyLock::new(|| RwLock::new(HashMap::new()));
static SEGFEAT_IDENTITY: std::sync::LazyLock<RwLock<HashMap<usize, Py<线Py>>>> =
std::sync::LazyLock::new(|| RwLock::new(HashMap::new()));
// 缓存通过 crate::cache 模块管理(支持 thread_local / global 运行时切换)
pub(crate) fn fractal_to_py(
py: Python<'_>,
inner: Arc<chanlun::structure::fractal_obj::>,
) -> Py<Py> {
let key = Arc::as_ptr(&inner) as usize;
if let Some(cached) = FRACTAL_IDENTITY
.read()
.unwrap()
.get(&key)
.map(|p| p.clone_ref(py))
{
if let Some(cached) = crate::cache::fractal_get(py, key) {
return cached;
}
// 清理 refcnt==1 的过期条目(仅缓存持有,Python 侧已无引用)
FRACTAL_IDENTITY
.write()
.unwrap()
.retain(|_, v| v.get_refcnt(py) > 1);
let obj = Py::new(py, Py { inner }).unwrap();
FRACTAL_IDENTITY
.write()
.unwrap()
.insert(key, obj.clone_ref(py));
crate::cache::fractal_insert(py, key, &obj);
obj
}
@@ -74,23 +53,11 @@ pub(crate) fn dashed_to_py(
inner: Arc<chanlun::structure::dash_line::线>,
) -> Py<线Py> {
let key = Arc::as_ptr(&inner) as usize;
if let Some(cached) = DASHED_IDENTITY
.read()
.unwrap()
.get(&key)
.map(|p| p.clone_ref(py))
{
if let Some(cached) = crate::cache::dashed_get(py, key) {
return cached;
}
DASHED_IDENTITY
.write()
.unwrap()
.retain(|_, v| v.get_refcnt(py) > 1);
let obj = Py::new(py, 线Py { inner }).unwrap();
DASHED_IDENTITY
.write()
.unwrap()
.insert(key, obj.clone_ref(py));
crate::cache::dashed_insert(py, key, &obj);
obj
}
@@ -98,25 +65,7 @@ pub(crate) fn segfeat_to_py(
py: Python<'_>,
inner: Arc<chanlun::structure::segment_feat::线>,
) -> Py<线Py> {
let key = Arc::as_ptr(&inner) as usize;
if let Some(cached) = SEGFEAT_IDENTITY
.read()
.unwrap()
.get(&key)
.map(|p| p.clone_ref(py))
{
return cached;
}
SEGFEAT_IDENTITY
.write()
.unwrap()
.retain(|_, v| v.get_refcnt(py) > 1);
let obj = Py::new(py, 线Py { inner }).unwrap();
SEGFEAT_IDENTITY
.write()
.unwrap()
.insert(key, obj.clone_ref(py));
obj
Py::new(py, 线Py { inner }).unwrap()
}
use crate::types_py::{Py, Py, Py};
@@ -393,7 +342,7 @@ impl 虚线Py {
#[getter]
fn (&self) -> String {
self.inner..read().unwrap().clone()
self.inner..read().clone()
}
#[getter]
@@ -413,7 +362,7 @@ impl 虚线Py {
#[getter]
fn (&self, py: Python<'_>) -> Py<Py> {
fractal_to_py(py, Arc::clone(&*self.inner..read().unwrap()))
fractal_to_py(py, Arc::clone(&*self.inner..read()))
}
#[getter]
@@ -423,7 +372,7 @@ impl 虚线Py {
#[getter]
fn (&self) -> String {
self.inner..read().unwrap().clone()
self.inner..read().clone()
}
#[getter(_特征序列_显示)]
@@ -439,7 +388,7 @@ impl 虚线Py {
#[getter]
fn (&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let list = pyo3::types::PyList::empty(py);
for item in self.inner..read().unwrap().iter() {
for item in self.inner..read().iter() {
match item {
Some(feat) => list.append(segfeat_to_py(py, Arc::clone(feat)))?,
None => {
@@ -460,18 +409,13 @@ impl 虚线Py {
self.inner
.K线
.read()
.unwrap()
.as_ref()
.map(|k| crate::kline_py::chan_kline_to_py(py, Arc::clone(k)))
}
#[getter]
fn (&self) -> Option<Py> {
self.inner
.
.read()
.unwrap()
.map(|q| Py { inner: q })
self.inner..read().map(|q| Py { inner: q })
}
#[getter]
@@ -479,7 +423,6 @@ impl 虚线Py {
self.inner
.
.read()
.unwrap()
.as_ref()
.map(|d| dashed_to_py(py, Arc::clone(d)))
}
@@ -489,7 +432,7 @@ impl 虚线Py {
#[getter]
fn (&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let list = pyo3::types::PyList::empty(py);
for d in self.inner..read().unwrap().iter() {
for d in self.inner..read().iter() {
list.append(dashed_to_py(py, Arc::clone(d)))?;
}
Ok(list.into())
@@ -498,7 +441,7 @@ impl 虚线Py {
#[getter]
fn _中枢序列(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let list = pyo3::types::PyList::empty(py);
for h in self.inner._中枢序列.read().unwrap().iter() {
for h in self.inner._中枢序列.read().iter() {
list.append(hub_to_py(py, Arc::clone(h)))?;
}
Ok(list.into())
@@ -507,7 +450,7 @@ impl 虚线Py {
#[getter]
fn _中枢序列(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let list = pyo3::types::PyList::empty(py);
for h in self.inner._中枢序列.read().unwrap().iter() {
for h in self.inner._中枢序列.read().iter() {
list.append(hub_to_py(py, Arc::clone(h)))?;
}
Ok(list.into())
@@ -516,7 +459,7 @@ impl 虚线Py {
#[getter]
fn _中枢序列(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let list = pyo3::types::PyList::empty(py);
for h in self.inner._中枢序列.read().unwrap().iter() {
for h in self.inner._中枢序列.read().iter() {
list.append(hub_to_py(py, Arc::clone(h)))?;
}
Ok(list.into())
@@ -528,7 +471,7 @@ impl 虚线Py {
/// 笔序列
fn (&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let list = pyo3::types::PyList::empty(py);
for d in self.inner..read().unwrap().iter() {
for d in self.inner..read().iter() {
list.append(dashed_to_py(py, Arc::clone(d)))?;
}
Ok(list.into())
@@ -1005,12 +948,12 @@ impl 线段特征Py {
#[getter]
fn (&self) -> String {
self.inner..read().unwrap().clone()
self.inner..read().clone()
}
#[setter]
fn set_标识(&self, value: String) {
*self.inner..write().unwrap() = value;
*self.inner..write() = value;
}
#[getter]
+19 -3
View File
@@ -22,8 +22,8 @@
* SOFTWARE.
*/
use parking_lot::Mutex;
use std::collections::HashMap;
use std::sync::Mutex;
use pyo3::basic::CompareOp;
use pyo3::prelude::*;
@@ -37,7 +37,7 @@ pub fn 获取分型结构单例(
py: Python<'_>,
inner: chanlun::types::,
) -> Py<Py> {
let mut guard = _单例缓存.lock().unwrap();
let mut guard = _单例缓存.lock();
if let Some(ref map) = *guard {
return map[&(inner as u8)].clone_ref(py);
}
@@ -67,7 +67,7 @@ pub fn 获取相对方向单例(
py: Python<'_>,
inner: chanlun::types::,
) -> Py<Py> {
let mut guard = _单例缓存.lock().unwrap();
let mut guard = _单例缓存.lock();
if let Some(ref map) = *guard {
return map[&(inner as u8)].clone_ref(py);
}
@@ -312,6 +312,22 @@ impl 相对方向Py {
chanlun::types::::(, , , ),
)
}
/// 从可选方向序列中随机选取指定数量
#[classmethod]
#[pyo3(signature = (数量, 可选方向, 可重复 = true))]
fn (
_cls: &Bound<'_, PyType>,
: usize,
: Vec<Py<Self>>,
: bool,
py: Python<'_>,
) -> Vec<Py<Self>> {
let dirs: Vec<chanlun::types::> =
.iter().map(|d| d.borrow(py).inner).collect();
let result = chanlun::types::::(, &dirs, );
result.iter().map(|d| (py, *d)).collect()
}
}
// ========== 分型结构 ==========
+86 -13
View File
@@ -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), ("rsi_7", 7)]
obs.配置.KDJ_参数列表 = [("kdj", 9, 3, 3), ("kdj_5", 5, 2, 2)]
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.增加原始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,13 +2542,21 @@ class Test缠论配置双端一致(unittest.TestCase):
d_rs = cfg_rs.to_dict()
d_py = cfg_py.to_dict()
# Rust (serde_json) 产 list-of-listPython 产 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 v_rs != v_py:
if _normalize(v_rs) != _normalize(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]))
@@ -2625,15 +2633,13 @@ class Test缠论配置双端一致(unittest.TestCase):
from chanlun import chan
cfg_rs, cfg_py = self._make_configs()
update = {"标识": "copied", "推送K线": False, "笔内元素数量": 10}
update = {"标识": "copied", "买卖点偏移": 5, "笔内元素数量": 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.买卖点偏移)
@@ -2660,8 +2666,6 @@ 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.笔内元素数量)
@@ -2776,5 +2780,74 @@ class Test缠论配置双端一致(unittest.TestCase):
self.assertFalse(diff_py[k], f"不推送差异字段 {k} 应为 False")
class Test生成K线双端一致(unittest.TestCase):
"""根据当前K线生成新K线 Rust vs chan.py 输出一致."""
def test_生成K线_居中各方向双端一致(self):
"""居中模式下各方向生成K线双端OHLC一致(居中=确定性输出)"""
import chanlun
from chanlun import chan
bar_rs = chanlun.K线.创建普K("btcusd", 1000000000, 50000, 50200, 49800, 50100, 100, 0, 300)
bar_py = chan.K线.创建普K("btcusd", chan.转化为时间戳(1000000000), 50000, 50200, 49800, 50100, 100, 0, 300)
directions = {
"向上": 0,
"向下": 1,
"向上缺口": 2,
"向下缺口": 3,
"衔接向上": 4,
"衔接向下": 5,
}
py_dirs = {
"向上": chan.相对方向.向上,
"向下": chan.相对方向.向下,
"向上缺口": chan.相对方向.向上缺口,
"向下缺口": chan.相对方向.向下缺口,
"衔接向上": chan.相对方向.衔接向上,
"衔接向下": chan.相对方向.衔接向下,
}
for name in directions:
new_rs = bar_rs.根据当前K线生成新K线(directions[name], 居中=True)
new_py = bar_py.根据当前K线生成新K线(py_dirs[name], 居中=True)
# 居中模式下,高和低是确定性的(偏移=高低差*0.5)
# 开盘价/收盘价/成交量含随机,不比较
tol = 1.0 + abs(new_py.) * 1e-6
self.assertAlmostEqual(new_rs., new_py., delta=tol, msg=f"{name}: 最高价不一致 (R={new_rs.}, P={new_py.})")
self.assertAlmostEqual(new_rs., new_py., delta=tol, msg=f"{name}: 最低价不一致 (R={new_rs.}, P={new_py.})")
# 时间戳和序号
self.assertEqual(new_rs.序号, new_py.序号)
self.assertEqual(int(new_rs.时间戳), int(chan.转化为时间戳_数字(new_py.时间戳)))
def test_生成K线_居中外推验证(self):
"""居中向上生成:新K线的高/低应整体高于原K线."""
import chanlun
bar = chanlun.K线.创建普K("btcusd", 1000000000, 50000, 50200, 49800, 50100, 100, 0, 300)
new = bar.根据当前K线生成新K线(0, 居中=True)
self.assertGreater(new., bar., "向上:新高应高于原高")
self.assertGreater(new., bar., "向上:新低应高于原低")
new_down = bar.根据当前K线生成新K线(1, 居中=True)
self.assertLess(new_down., bar., "向下:新高应低于原高")
self.assertLess(new_down., bar., "向下:新低应低于原低")
def test_生成K线_衔接验证(self):
"""衔接向上:新K线的低 = 原K线的高(无缝衔接)."""
import chanlun
bar = chanlun.K线.创建普K("btcusd", 1000000000, 50000, 50200, 49800, 50100, 100, 0, 300)
new = bar.根据当前K线生成新K线(4, 居中=True)
self.assertAlmostEqual(new., bar., delta=1e-6, msg="衔接向上:新低应=原高")
new_down = bar.根据当前K线生成新K线(5, 居中=True)
self.assertAlmostEqual(new_down., bar., delta=1e-6, msg="衔接向下:新高应=原低")
if __name__ == "__main__":
unittest.main()
+301
View File
@@ -0,0 +1,301 @@
"""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
+211
View File
@@ -0,0 +1,211 @@
"""信号原语 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() 内部用法
# ---- PositionRust 基类 + 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
View File
@@ -0,0 +1 @@
/target/
+47
View File
@@ -0,0 +1,47 @@
# 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"
+14
View File
@@ -0,0 +1,14 @@
[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"
+100
View File
@@ -0,0 +1,100 @@
//! chanlun 信号注册 proc-macro。
//!
//! 第三方代码声明:`#[signal]` 注册机制参考 czsc 项目
//! https://github.com/waditu/czscApache License 2.0),已简化适配
//! (无 category / TaCache,签名固定为 fn(&观察者, &HashMap<String, Value>) -> Vec<Signal>)。
use proc_macro::TokenStream;
use quote::quote;
use syn::parse::Parser;
use syn::punctuated::Punctuated;
use syn::{Expr, ExprLit, ItemFn, Lit, Meta, Token};
/// `#[signal(name = "foo_V230101", template = "{freq}_D1_foo")]`
///
/// 校验:函数名含 `_V<数字>``name` 与函数名一致;`name`/`template` 非空。
/// 生成:一个 `static` SignalDescriptor + `inventory::submit!`。
///
/// 路径:默认 `crate::signal::registry::`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()
}
+6 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "chanlun"
version = "26.6.3"
version = "26.6.4"
edition = "2024"
license = "MIT"
description = "基于缠论(缠中说禅)理论的量化技术分析核心库,支持流式数据处理和多周期联立分析。"
@@ -17,6 +17,9 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
byteorder = "1"
chrono = { version = "0.4", features = ["serde"] }
cached = "1"
parking_lot = "0.12"
tracing = "0.1"
tracing-subscriber = "0.3"
fastrand = "2"
sha2 = "0.10"
inventory = "0.3"
chanlun-signal-macros = { path = "../chanlun-signal-macros" }
+861 -676
View File
File diff suppressed because it is too large Load Diff
+39 -42
View File
@@ -29,9 +29,9 @@ use crate::kline::chan_kline::缠论K线;
use crate::structure::dash_line::线;
use crate::structure::fractal_obj::;
use crate::types::{, };
use crate::{error, warn};
use std::sync::Arc;
use std::sync::atomic::Ordering;
use tracing::{error, warn};
/// 笔 — 从分型生成笔的算法集合(静态方法命名空间)
pub struct ;
@@ -54,8 +54,7 @@ impl 笔 {
if let (Some(), Some()) = (&, &) {
let = 1
+ (.K线.read().unwrap(). - .K线.read().unwrap().)
.unsigned_abs() as usize;
+ (.K线.read(). - .K线.read().).unsigned_abs() as usize;
if >= . as usize {
return . as usize;
}
@@ -76,8 +75,8 @@ impl 笔 {
&& let (Some(_k), Some(_k)) = (&, &)
{
let = 1
+ (_k.K线.read().unwrap(). - _k.K线.read().unwrap().)
.unsigned_abs() as usize;
+ (_k.K线.read(). - _k.K线.read().).unsigned_abs()
as usize;
// 向上笔
if .().()
&& _k..get() < .()
@@ -219,7 +218,7 @@ impl 笔 {
/// 判断笔的相对关系是否合理
pub fn _相对关系(: &线, : &) -> bool {
let = &.;
let = ..read().unwrap();
let = ..read();
let = if . {
let _rc = Arc::clone(&.);
@@ -261,8 +260,8 @@ impl 笔 {
...get(),
);
if .K线包含整笔 {
let = ..K线.read().unwrap();
let = ..K线.read().unwrap();
let = ..K线.read();
let = ..K线.read();
if crate::types::::(., ., ., .)
.()
{
@@ -280,10 +279,7 @@ impl 笔 {
/// 以文会友 — 根据起点分型找笔
pub fn (: &[Arc<线>], : &Arc<>) -> Option<Arc<线>> {
.iter()
.find(|b| Arc::as_ptr(&b.) == Arc::as_ptr())
.cloned()
.iter().find(|b| Arc::ptr_eq(&b., )).cloned()
}
/// 以武会友 — 根据终点分型找笔
@@ -291,7 +287,7 @@ impl 笔 {
.iter()
.rev()
.find(|b| Arc::as_ptr(&*b..read().unwrap()) == Arc::as_ptr())
.find(|b| Arc::ptr_eq(&*b..read(), ))
.cloned()
}
@@ -305,8 +301,7 @@ impl 笔 {
for b in .iter().rev() {
// Python: 筆.文.中.序号 - 偏移 <= 缠K.序号 <= 筆.武.中.序号
if b....load(Ordering::Relaxed) - <= K..load(Ordering::Relaxed)
&& K..load(Ordering::Relaxed)
<= b..read().unwrap()...load(Ordering::Relaxed)
&& K..load(Ordering::Relaxed) <= b..read()...load(Ordering::Relaxed)
&& b... == K.
&& b... == K.
{
@@ -323,7 +318,7 @@ impl 笔 {
let = .pop();
if let (Some(), Some()) = (.pop(), ) {
assert!(
Arc::as_ptr(&..read().unwrap()) == Arc::as_ptr(&),
Arc::ptr_eq(&..read(), &),
"最后一笔终点错误{}",
);
@@ -388,7 +383,7 @@ impl 笔 {
// Python line 2343-2348: 笔弱化模式
if . && !.is_empty() {
let = .last().unwrap();
let K数 = ..read().unwrap()...load(Ordering::Relaxed)
let K数 = ..read()...load(Ordering::Relaxed)
- ....load(Ordering::Relaxed)
+ 1;
if K数 == 3 {
@@ -434,7 +429,7 @@ impl 笔 {
// Python line 2359-2367: 文官调整
if let Some(ref _k) =
&& Arc::as_ptr(_k) != Arc::as_ptr(&.)
&& !Arc::ptr_eq(_k, &.)
&& let Some() =
::K序列中获取分型(K序列, _k)
{
@@ -476,7 +471,7 @@ impl 笔 {
if Self::_相对关系(&, )
&& let Some(ref _k) =
&& Arc::as_ptr(_k) == Arc::as_ptr(&.)
&& Arc::ptr_eq(_k, &.)
{
// 直接添加(对照 Python _添加新笔:直接 append
Self::_添加新笔(, , , , line!());
@@ -490,7 +485,7 @@ impl 笔 {
_ => Self::_次高(&, .),
};
if let Some(ref _k) =
&& Arc::as_ptr(_k) == Arc::as_ptr(&.)
&& Arc::ptr_eq(_k, &.)
&& Self::_相对关系(&, )
{
Self::_添加新笔(, , , , line!());
@@ -557,13 +552,12 @@ impl 笔 {
if !.is_empty()
&& Arc::as_ptr(.last().unwrap())
== Arc::as_ptr(&_rc)
&& let Some(_idx) = K序列
.iter()
.position(|k| Arc::as_ptr(k) == Arc::as_ptr(_k))
&& let Some(_idx) =
K序列.iter().position(|k| Arc::ptr_eq(k, _k))
{
for ck in &K序列[_idx..] {
if (*ck..read().unwrap() == Some(::)
|| *ck..read().unwrap() == Some(::))
if (*ck..read() == Some(::)
|| *ck..read() == Some(::))
&& let Some() =
::K序列中获取分型(K序列, ck)
{
@@ -577,6 +571,15 @@ impl 笔 {
+ 1,
,
);
if !.is_empty()
&& Arc::as_ptr(.last().unwrap())
== Arc::as_ptr(&_rc)
{
warn!(
"笔.分析 事后修复错过的笔:{}, 当前分型: {}",
_rc,
);
}
}
}
}
@@ -649,14 +652,10 @@ impl 笔 {
.
.store(..load(Ordering::Relaxed) + 1, Ordering::Relaxed);
if ..read().unwrap()..is_none() || ..read().unwrap()..is_none()
{
if ..read()..is_none() || ..read()..is_none() {
..store(false, Ordering::Relaxed);
}
if matches!(
..read().unwrap().(),
:: | ::
) {
if matches!(..read().(), :: | ::) {
error!("_添加新笔[{}] 出现无效分型 {}", , );
}
}
@@ -676,7 +675,7 @@ impl 笔 {
Self::_实际低点(&, .),
)
&& Arc::ptr_eq(&.., &)
&& Arc::ptr_eq(&..read().unwrap()., &)
&& Arc::ptr_eq(&..read()., &)
{
return true;
}
@@ -686,7 +685,7 @@ impl 笔 {
Self::_实际高点(&, .),
)
&& Arc::ptr_eq(&.., &)
&& Arc::ptr_eq(&..read().unwrap()., &)
&& Arc::ptr_eq(&..read()., &)
{
return true;
}
@@ -696,9 +695,9 @@ impl 笔 {
/// 获取所有停顿位置 — 在笔范围内找出所有能成笔的分型组合
pub fn (: &线, : &) -> Vec<线> {
let mut = Vec::new();
let = Arc::clone(&.);
let = .K序列(&.K线序列);
let mut = Vec::with_capacity(.len() / 2);
let = Arc::clone(&.);
if .len() < 5 {
return ;
@@ -707,10 +706,8 @@ impl 笔 {
for i in 3...len() - 1 {
let k = &[i];
let =
*k..read().unwrap() == Some(::) && .() == ::;
let =
*k..read().unwrap() == Some(::) && .() == ::;
let = *k..read() == Some(::) && .() == ::;
let = *k..read() == Some(::) && .() == ::;
if || {
let = Arc::clone(&[i - 1]);
let = Arc::clone(k);
@@ -737,12 +734,12 @@ impl 笔 {
for in & {
let k线范围 = K线::rc(
&.K线序列,
&...K线.read().unwrap().clone(),
&..read().unwrap()..K线.read().unwrap().clone(),
&...K线.read().clone(),
&..read()..K线.read().clone(),
);
let = 线::K线序列MACD趋向背驰(&k线范围, .());
if .iter().all(|&x| x) {
.push(Arc::clone(&..read().unwrap().));
.push(Arc::clone(&..read().));
}
}
+15 -19
View File
@@ -39,13 +39,13 @@ impl 背驰分析 {
) -> bool {
let MACD = Self::_获取MACD面积(
K线序列,
&...K线.read().unwrap(),
&..read().unwrap()..K线.read().unwrap(),
&...K线.read(),
&..read()..K线.read(),
);
let MACD = Self::_获取MACD面积(
K线序列,
&...K线.read().unwrap(),
&..read().unwrap()..K线.read().unwrap(),
&...K线.read(),
&..read()..K线.read(),
);
// 计算面积(绝对值求和)
@@ -69,18 +69,18 @@ impl 背驰分析 {
/// 斜率背驰 — 价格斜率背驰
pub fn (: &线, : &线) -> bool {
let dx = (..read().unwrap().() - ..()) as f64;
let dx = (..read().() - ..()) as f64;
if dx == 0.0 {
return false;
}
let dy = ..read().unwrap(). - ..;
let dy = ..read(). - ..;
let = dy / dx;
let dx = (..read().unwrap().() - ..()) as f64;
let dx = (..read().() - ..()) as f64;
if dx == 0.0 {
return false;
}
let dy = ..read().unwrap(). - ..;
let dy = ..read(). - ..;
let = dy / dx;
if .() == :: {
@@ -92,12 +92,12 @@ impl 背驰分析 {
/// 测度背驰 — 价格时间测度背驰
pub fn (: &线, : &线) -> bool {
let dx = (..read().unwrap().() - ..()) as f64;
let dy = ..read().unwrap(). - ..;
let dx = (..read().() - ..()) as f64;
let dy = ..read(). - ..;
let = (dx * dx + dy * dy).sqrt();
let dx = (..read().unwrap().() - ..()) as f64;
let dy = ..read().unwrap(). - ..;
let dx = (..read().() - ..()) as f64;
let dy = ..read(). - ..;
let = (dx * dx + dy * dy).sqrt();
if .() == :: {
@@ -187,12 +187,8 @@ impl 背驰分析 {
// ---- 内部辅助 ----
fn _获取MACD面积(K线序列: &[Arc<K线>], : &Arc<K线>, : &Arc<K线>) -> MACD面积 {
let _idx = K线序列
.iter()
.position(|k| Arc::as_ptr(k) == Arc::as_ptr());
let _idx = K线序列
.iter()
.position(|k| Arc::as_ptr(k) == Arc::as_ptr());
let _idx = K线序列.iter().position(|k| Arc::ptr_eq(k, ));
let _idx = K线序列.iter().position(|k| Arc::ptr_eq(k, ));
let mut = 0.0f64;
let mut = 0.0f64;
@@ -200,7 +196,7 @@ impl 背驰分析 {
if let (Some(), Some()) = (_idx, _idx) {
let (, ) = if <= { (, ) } else { (, ) };
for k in &K线序列[..=] {
if let Some(macd) = k..read().unwrap().macd() {
if let Some(macd) = k..read().macd() {
let hist = macd.MACD柱;
if hist >= 0.0 {
+= hist;
+160 -176
View File
@@ -25,8 +25,9 @@
use crate::structure::dash_line::线;
use crate::structure::fractal_obj::;
use crate::types::;
use parking_lot::RwLock;
use std::sync::Arc;
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::{Arc, RwLock};
/// 中枢 — 三段虚线重叠区间构成的价格中枢
///
@@ -59,11 +60,11 @@ impl Clone for 中枢 {
fn clone(&self) -> Self {
Self {
: AtomicI64::new(self..load(Ordering::Relaxed)),
: RwLock::new(self..read().unwrap().clone()),
: RwLock::new(self..read().clone()),
: AtomicI64::new(self..load(Ordering::Relaxed)),
: RwLock::new(self..read().unwrap().clone()),
线: RwLock::new(self.线.read().unwrap().clone()),
_第三买卖线: RwLock::new(self._第三买卖线.read().unwrap().clone()),
: RwLock::new(self..read().clone()),
线: RwLock::new(self.线.read().clone()),
_第三买卖线: RwLock::new(self._第三买卖线.read().clone()),
}
}
}
@@ -83,9 +84,9 @@ impl 中枢 {
/// 向基础序列尾部添加虚线(中枢延伸),并清除第三买卖线
pub fn _添加虚线(&self, 线: Arc<线>) {
self..write().unwrap().push(线);
*self._第三买卖线.write().unwrap() = None;
*self.线.write().unwrap() = None;
self..write().push(线);
*self._第三买卖线.write() = None;
*self.线.write() = None;
}
/// 返回图表标题字符串,格式为 "文.标识:文.周期:中枢标识:序号"
@@ -94,24 +95,25 @@ impl 中枢 {
"{}:{}:{}:{}",
self.()..,
self.()..,
self..read().unwrap(),
self..read(),
self..load(Ordering::Relaxed)
)
}
/// 返回基础序列的最后一根虚线(当前离开段)
pub fn (&self) -> Arc<线> {
Arc::clone(&self..read().unwrap()[self..read().unwrap().len() - 1])
let guard = self..read();
Arc::clone(&guard[guard.len() - 1])
}
/// 返回中枢方向(与基础序列第一段方向相反)
pub fn (&self) -> {
self..read().unwrap()[0].().()
self..read()[0].().()
}
/// 中枢上沿 = min(前三段的高)
pub fn (&self) -> f64 {
self..read().unwrap()[..3]
self..read()[..3]
.iter()
.map(|x| x.())
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
@@ -120,7 +122,7 @@ impl 中枢 {
/// 中枢下沿 = max(前三段的低)
pub fn (&self) -> f64 {
self..read().unwrap()[..3]
self..read()[..3]
.iter()
.map(|x| x.())
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
@@ -131,7 +133,6 @@ impl 中枢 {
pub fn (&self) -> f64 {
self.
.read()
.unwrap()
.iter()
.map(|x| x.())
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
@@ -142,7 +143,6 @@ impl 中枢 {
pub fn (&self) -> f64 {
self.
.read()
.unwrap()
.iter()
.map(|x| x.())
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
@@ -151,46 +151,47 @@ impl 中枢 {
/// 返回基础序列第一段的起点分型
pub fn (&self) -> Arc<> {
Arc::clone(&self..read().unwrap()[0].)
Arc::clone(&self..read()[0].)
}
/// 返回基础序列最后一段的终点分型
pub fn (&self) -> Arc<> {
Arc::clone(
&*self..read().unwrap()[self..read().unwrap().len() - 1]
.
.read()
.unwrap(),
)
let guard = self..read();
Arc::clone(&*guard[guard.len() - 1]..read())
}
/// 设置第三类买卖点对应的虚线
pub fn 线(&self, 线: Option<Arc<线>>) {
*self.线.write().unwrap() = 线;
*self.线.write() = 线;
}
/// 获取序列 — 基础序列 + 第三买卖线(若有)
pub fn (&self) -> Vec<Arc<线>> {
let mut : Vec<Arc<线>> = self..read().unwrap().clone();
if let Some(ref ) = *self.线.read().unwrap() {
let mut : Vec<Arc<线>> = self..read().clone();
if let Some(ref ) = *self.线.read() {
.push(Arc::clone());
}
}
/// 获取基础序列最后一个元素
pub fn (&self) -> Option<Arc<线>> {
self..read().last().cloned()
}
/// 返回序列化数据文本,用于调试和存储
pub fn (&self) -> String {
let 线_str = match &*self.线.read().unwrap() {
let 线_str = match &*self.线.read() {
Some(x) => format!("{}", x),
None => "None".to_string(),
};
let _第三买卖线_str = match &*self._第三买卖线.read().unwrap() {
let _第三买卖线_str = match &*self._第三买卖线.read() {
Some(x) => format!("{}", x),
None => "None".to_string(),
};
format!(
"{}, {}, {}, 文:({},{}), 武:({},{}), {}, {}",
self..read().unwrap(),
self..read(),
self..load(Ordering::Relaxed),
self..load(Ordering::Relaxed),
self.().(),
@@ -204,67 +205,63 @@ impl 中枢 {
/// 校验中枢合法性
pub fn _校验合法性(&self, : &[Arc<线>]) -> bool {
let mut = self..read().unwrap().clone();
let guard = self..read();
let mut = guard.clone();
let mut : Vec<Arc<线>> = Vec::new();
for in self..read().unwrap().iter() {
if !.iter().any(|x| Arc::as_ptr(x) == Arc::as_ptr()) {
let = [0]..load(Ordering::Relaxed);
for in guard.iter() {
let idx = (..load(Ordering::Relaxed) - ) as usize;
if idx >= .len() || !Arc::ptr_eq(&[idx], ) {
.push(Arc::clone());
}
}
if !.is_empty() {
let = &[0];
if let Some(pos) = self
.
.read()
.unwrap()
.iter()
.position(|x| Arc::as_ptr(x) == Arc::as_ptr())
{
= self..read().unwrap()[..pos].to_vec();
}
// Python: 序号 = 线段._索引(self.基础序列, 无效)
let pos = crate::algorithm::segment::线::_索引(&guard, );
= guard[..pos].to_vec();
}
drop(guard);
if .len() < 3 {
self.线(None);
*self._第三买卖线.write().unwrap() = None;
*self._第三买卖线.write() = None;
return false;
}
*self..write().unwrap() = ;
*self..write() = ;
let = self.();
let = self.();
= Vec::new();
for in self..read().unwrap().iter() {
for in self..read().iter() {
if crate::types::::(, , .(), .()).()
{
break;
}
.push(Arc::clone());
}
*self..write().unwrap() = ;
*self..write() = ;
if self..read().unwrap().len() < 3 {
return false;
}
for i in 1..self..read().unwrap().len() {
let = &self..read().unwrap()[i - 1];
let = &self..read().unwrap()[i];
if !.() {
let = {
let guard = self..read();
if guard.len() < 3 {
return false;
}
}
if !crate::types::::(
self..read().unwrap()[0].(),
self..read().unwrap()[0].(),
self..read().unwrap()[2].(),
self..read().unwrap()[2].(),
)
.()
{
for i in 1..guard.len() {
if !guard[i - 1].(&guard[i]) {
return false;
}
}
crate::types::::(
guard[0].(),
guard[0].(),
guard[2].(),
guard[2].(),
)
.()
};
if ! {
let = self.();
let = self.();
if > {
@@ -272,10 +269,12 @@ impl 中枢 {
}
}
let 线_opt = self.线.read().unwrap().clone();
let 线_opt = self.线.read().clone();
if let Some(ref 线) = 线_opt {
if .iter().any(|x| Arc::as_ptr(x) == Arc::as_ptr(线)) {
if !self..read().unwrap().last().unwrap().(线) {
let = [0]..load(Ordering::Relaxed);
let idx = (线..load(Ordering::Relaxed) - ) as usize;
if idx < .len() && Arc::ptr_eq(&[idx], 线) {
if !self..read().last().unwrap().(线) {
self.线(None);
} else if !crate::types::::(
self.(),
@@ -298,25 +297,44 @@ impl 中枢 {
/// 完整性 — 详见教你炒股票43:有关背驰的补习课
/// 不完整时下一个中枢大概率会与当前中枢发生扩展
pub fn (&self, : &str) -> bool {
if *self..read().unwrap()[0]..read().unwrap() == "" {
return self.线.read().unwrap().is_some();
if *self..read()[0]..read() == "" {
return self.线.read().is_some();
}
let _ref = self..read().unwrap();
// if self.本级_第三买卖线: return True # 暂未启用
let = self.();
if == "中枢之中" {
return false;
}
let _ref = self..read();
let = _ref.last().unwrap();
let _vec = if == "" {
._中枢序列.read().unwrap()
._中枢序列.read()
} else {
._中枢序列.read().unwrap()
._中枢序列.read()
};
if _vec.is_empty() {
return false;
}
let = self.();
let = self.();
for in _vec.iter() {
if crate::types::::(
self.(),
self.(),
.(),
.(),
)
.()
let = .();
let = .();
if == "中枢之下" {
if <= {
continue;
}
} else {
// 中枢之上
if >= {
continue;
}
}
if crate::types::::(, , , ).()
{
return true;
}
@@ -330,24 +348,19 @@ impl 中枢 {
: &mut Vec<Arc<>>,
: &crate::config::,
) {
if self..read().unwrap().len() >= 9 {
if self..read().len() >= 9 {
let mut 线: Vec<Arc<线>> = Vec::new();
let _ref = self..read().unwrap();
let _ref = self..read();
crate::algorithm::segment::线::(&_ref, &mut 线, );
::(
&线,
,
false,
&format!("{}_扩展中枢_", self..read().unwrap()),
0,
);
let = format!("{}_扩展中枢_", self..read());
::(&线, , false, &, 0);
}
}
/// 当前状态 — 详见教你炒股票49:利润率最大的操作模式
/// 返回当前中枢最后一段所处的位置关系:中枢之中/中枢之上/中枢之下
pub fn (&self) -> &str {
let _ref = self..read().unwrap();
let _ref = self..read();
let = Arc::clone(_ref.last().unwrap());
let = ._武();
let = crate::types::::(
@@ -390,7 +403,7 @@ impl 中枢 {
assert!(Self::(&, &, &), "中枢.创建 基础检查失败");
Self::new(
0,
format!("{}中枢<{}>", , ..read().unwrap()),
format!("{}中枢<{}>", , ..read()),
,
vec![, , ],
)
@@ -422,18 +435,8 @@ impl 中枢 {
.
.store(..load(Ordering::Relaxed) + 1, Ordering::Relaxed);
let _last_序号 =
.()
.last()
.unwrap()
.
.load(Ordering::Relaxed);
let new_last_序号 =
.()
.last()
.unwrap()
.
.load(Ordering::Relaxed);
let _last_序号 = .().unwrap()..load(Ordering::Relaxed);
let new_last_序号 = .().unwrap()..load(Ordering::Relaxed);
if _last_序号 > new_last_序号 {
panic!(
"向中枢序列尾部添加 序号错误 前last={} > new_last={}",
@@ -478,11 +481,8 @@ impl 中枢 {
let = &线[i + 1];
if Self::(, , ) {
// Python: 序号 = 虚线序列.index(左)
let = 线
.iter()
.position(|x| Arc::as_ptr(x) == Arc::as_ptr())
.expect("中枢.分析: 左元素不在虚线序列中");
// Python: 序号 = 线段._索引(虚线序列, 左)
let : usize = crate::algorithm::segment::线::_索引(线, );
if && (..load(Ordering::Relaxed) == 0 || == 0) {
continue;
}
@@ -525,22 +525,16 @@ impl 中枢 {
return;
}
// 找到当前中枢最后一个元素在虚线序列中的位置
// Python: 序号 = 线段._索引(虚线序列, 当前中枢.基础序列[-1]) + 1
let = {
let cur = &[_idx];
let = &cur..read().unwrap()[cur..read().unwrap().len() - 1];
match 线
.iter()
.position(|x| Arc::as_ptr(x) == Arc::as_ptr())
{
Some(idx) => idx + 1,
None => return,
}
let guard = cur..read();
crate::algorithm::segment::线::_索引(线, &guard[guard.len() - 1]) + 1
};
let mut = [_idx].();
let mut = [_idx].();
let mut : Vec<Arc<线>> = Vec::new();
let mut = Vec::with_capacity(3);
for 线_ref in &线[..] {
let 线 = Arc::clone(线_ref);
@@ -553,12 +547,7 @@ impl 中枢 {
// Python: if 当前中枢.基础序列[-1].之后是(当前虚线):
let needs_三买 = {
let cur = &[_idx];
cur.
.read()
.unwrap()
.last()
.unwrap()
.(&线)
cur..read().last().unwrap().(&线)
};
if needs_三买 {
[_idx].线(Some(线.clone()));
@@ -570,17 +559,11 @@ impl 中枢 {
[_idx]
.
.read()
.unwrap()
.last()
.unwrap()
.(&线),
"中枢延伸: 不连续 {}, {}",
[_idx]
.
.read()
.unwrap()
.last()
.unwrap(),
[_idx]..read().last().unwrap(),
线
);
[_idx]._添加虚线(线);
@@ -594,7 +577,6 @@ impl 中枢 {
let = [_idx]
.
.read()
.unwrap()
.last()
.unwrap()
.()
@@ -628,13 +610,13 @@ impl 中枢 {
),
);
}
if *self..read().unwrap() != *other..read().unwrap() {
if *self..read() != *other..read() {
return (
false,
format!(
"中枢: [标识] 不等 A={},B={}",
self..read().unwrap(),
other..read().unwrap()
self..read(),
other..read()
),
);
}
@@ -649,8 +631,8 @@ impl 中枢 {
);
}
// 基础序列
let a_seq = self..read().unwrap();
let b_seq = other..read().unwrap();
let a_seq = self..read();
let b_seq = other..read();
if a_seq.len() != b_seq.len() {
return (
false,
@@ -692,16 +674,16 @@ impl 中枢 {
};
(
"第三买卖线",
&self.线.read().unwrap(),
&other.线.read().unwrap(),
&self.线.read(),
&other.线.read(),
,
)
.map_err(|e| (false, e))
.ok();
(
"本级_第三买卖线",
&self._第三买卖线.read().unwrap(),
&other._第三买卖线.read().unwrap(),
&self._第三买卖线.read(),
&other._第三买卖线.read(),
,
)
.map_err(|e| (false, e))
@@ -712,21 +694,26 @@ impl 中枢 {
impl std::fmt::Display for {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let _str = self
.
.read()
.unwrap()
.iter()
.map(|d| format!("{}", d))
.collect::<Vec<_>>()
.join(", ");
let guard = self..read();
let len = guard.len();
let _str = if let Some((first, rest)) = guard.split_first() {
let mut s = format!("{}", first);
for d in rest {
use std::fmt::Write;
write!(&mut s, ", {}", d).unwrap();
}
s
} else {
String::new()
};
drop(guard);
write!(
f,
"{}({}, {}, 元素数量: {}, [{}], {} ===>>> {})",
self..read().unwrap(),
self..read(),
crate::utils::format_f64_g(self.()),
crate::utils::format_f64_g(self.()),
self..read().unwrap().len(),
len,
_str,
self.(),
self.(),
@@ -851,11 +838,11 @@ mod tests {
);
assert_eq!(..load(Ordering::Relaxed), 1);
assert_eq!(*..read().unwrap(), "测试中枢");
assert_eq!(*..read(), "测试中枢");
assert_eq!(..load(Ordering::Relaxed), 1);
assert_eq!(..read().unwrap().len(), 3);
assert!(.线.read().unwrap().is_none());
assert!(._第三买卖线.read().unwrap().is_none());
assert_eq!(..read().len(), 3);
assert!(.线.read().is_none());
assert!(._第三买卖线.read().is_none());
}
#[test]
@@ -877,16 +864,16 @@ mod tests {
// RefCell 第三买卖线读写
.线(Some(Arc::clone(&1)));
assert!(.线.read().unwrap().is_some());
assert!(.线.read().is_some());
assert_eq!(
Arc::as_ptr(.线.read().unwrap().as_ref().unwrap()),
Arc::as_ptr(.线.read().as_ref().unwrap()),
Arc::as_ptr(&1)
);
// 本级_第三买卖线
assert!(._第三买卖线.read().unwrap().is_none());
*._第三买卖线.write().unwrap() = Some(Arc::clone(&3));
assert!(._第三买卖线.read().unwrap().is_some());
assert!(._第三买卖线.read().is_none());
*._第三买卖线.write() = Some(Arc::clone(&3));
assert!(._第三买卖线.read().is_some());
}
// ============================================================
@@ -906,14 +893,11 @@ mod tests {
1,
vec![Arc::clone(&1), Arc::clone(&2), Arc::clone(&3)],
);
assert_eq!(..read().unwrap().len(), 3);
assert_eq!(..read().len(), 3);
._添加虚线(Arc::clone(&4));
assert_eq!(..read().unwrap().len(), 4);
assert_eq!(
Arc::as_ptr(&..read().unwrap()[3]),
Arc::as_ptr(&4)
);
assert_eq!(..read().len(), 4);
assert_eq!(Arc::as_ptr(&..read()[3]), Arc::as_ptr(&4));
}
#[test]
@@ -930,14 +914,14 @@ mod tests {
vec![Arc::clone(&1), Arc::clone(&2), Arc::clone(&3)],
);
.线(Some(Arc::clone(&1)));
*._第三买卖线.write().unwrap() = Some(Arc::clone(&2));
assert!(.线.read().unwrap().is_some());
assert!(._第三买卖线.read().unwrap().is_some());
*._第三买卖线.write() = Some(Arc::clone(&2));
assert!(.线.read().is_some());
assert!(._第三买卖线.read().is_some());
._添加虚线(Arc::clone(&4));
// 添加虚线后第三买卖线被清除
assert!(.线.read().unwrap().is_none());
assert!(._第三买卖线.read().unwrap().is_none());
assert!(.线.read().is_none());
assert!(._第三买卖线.read().is_none());
}
// ============================================================
@@ -963,15 +947,15 @@ mod tests {
// 基础序列中的 Rc 指针应一致
for i in 0..3 {
assert_eq!(
Arc::as_ptr(&..read().unwrap()[i]),
Arc::as_ptr(&..read().unwrap()[i])
Arc::as_ptr(&..read()[i]),
Arc::as_ptr(&..read()[i])
);
}
// 第三买卖线 Rc 指针应一致
assert_eq!(
Arc::as_ptr(.线.read().unwrap().as_ref().unwrap()),
Arc::as_ptr(.线.read().unwrap().as_ref().unwrap())
Arc::as_ptr(.线.read().as_ref().unwrap()),
Arc::as_ptr(.线.read().as_ref().unwrap())
);
}
@@ -1030,12 +1014,12 @@ mod tests {
// 通过 rc1 添加虚线
1._线(Arc::clone(&4));
assert_eq!(2..read().unwrap().len(), 4);
assert_eq!(2..read().len(), 4);
// 验证共享的 Arc<虚线> 指针一致
assert_eq!(
Arc::as_ptr(&1..read().unwrap()[3]),
Arc::as_ptr(&2..read().unwrap()[3])
Arc::as_ptr(&1..read()[3]),
Arc::as_ptr(&2..read()[3])
);
}
}
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -347,7 +347,7 @@ impl 买卖点 {
let = .();
// 当前K线 — 从缠K获取其标的K线
let K线 = Arc::clone(&*K.K线.read().unwrap());
let K线 = Arc::clone(&*K.K线.read());
// 当前缠K序号 — 与买卖点K线(分型.中.序号)同尺度,用于偏移计算
let K序号 = K..load(Ordering::Relaxed);
+16 -20
View File
@@ -26,10 +26,10 @@ use crate::business::observer::观察者;
use crate::business::synthesizer::K线合成器;
use crate::config::;
use crate::kline::bar::K线;
use crate::{error, warn};
use parking_lot::RwLock;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::RwLock;
use tracing::{error, info};
/// 立体分析器 — 多周期协调器
pub struct {
@@ -61,8 +61,7 @@ impl 立体分析器 {
.get(&)
.cloned()
.unwrap_or_else(|| .clone());
.K线 = false;
.线 = false;
. = Some(vec![]);
. = .clone();
let = ::new(.clone(), , );
@@ -72,10 +71,8 @@ impl 立体分析器 {
// 显示周期特殊配置
{
let = .get(&).expect("显示周期观察者不存在");
let mut guard = .write().unwrap();
guard..K线 = true;
guard.. = true;
guard..线 = true;
let mut guard = .write();
guard.. = None; // None = 全部展示
guard.. = true;
guard.();
}
@@ -84,14 +81,14 @@ impl 立体分析器 {
{
let K序列 =
.get(&)
.map(|o| o.read().unwrap().K线序列.clone())
.map(|o| o.read().K线序列.clone())
.unwrap_or_default();
for & in & {
if !=
&& let Some() = .get(&)
{
.write().unwrap().K序列 = K序列.clone();
.write().K序列 = K序列.clone();
}
}
}
@@ -119,7 +116,7 @@ impl 立体分析器 {
/// __K线回调 — 对应 Python 立体分析器.__K线回调
fn __K线回调(&self, _信号类型: String, _标识: String, : i64, K线: K线) {
if let Some() = self..get(&) {
let mut obs = .write().unwrap();
let mut obs = .write();
obs.K线(K线);
// 对应 Python: if 当前K线 := self._K线合成器.获取当前K线(周期)
// _完成K线刚清空当前K线,获取当前K线返回 None,所以这里不添加
@@ -133,7 +130,7 @@ impl 立体分析器 {
K线: K线,
) {
if let Some() = .get(&) {
.write().unwrap().K线(K线);
.write().K线(K线);
}
}
@@ -165,22 +162,22 @@ impl 立体分析器 {
let = self
.
.get(&self.)
.and_then(|o| o.read().unwrap().K线序列.first().map(|k| k.))
.and_then(|o| o.read().K线序列.first().map(|k| k.))
.unwrap_or(0);
let = self
.
.get(&self.)
.and_then(|o| o.read().unwrap().K线序列.last().map(|k| k.))
.and_then(|o| o.read().K线序列.last().map(|k| k.))
.unwrap_or(0);
let = self
.
.get(&self.)
.map(|o| o.read().unwrap()..clone())
.map(|o| o.read()..clone())
.unwrap_or_default();
let = self
.
.get(&self.)
.map(|o| o.read().unwrap().)
.map(|o| o.read().)
.unwrap_or_default();
let = format!("RustM_{}:{}_{}_{}", , , , );
@@ -195,12 +192,11 @@ impl 立体分析器 {
if let Some() = self..get() {
.read()
.unwrap()
._保存数据(Some(&.to_string_lossy()));
}
}
info!("多级别数据拆分保存完成,目录:{}", .display());
warn!("多级别数据拆分保存完成,目录:{}", .display());
}
/// 相等 — 各周期观察者全量比对,对应 Python `立体分析器相等`
@@ -213,11 +209,11 @@ impl 立体分析器 {
for in &self. {
let a_obs = match self..get() {
Some(o) => o.read().unwrap(),
Some(o) => o.read(),
None => return (false, format!("{标签}: 周期{周期} 观察者不存在 (A)")),
};
let b_obs = match other..get() {
Some(o) => o.read().unwrap(),
Some(o) => o.read(),
None => return (false, format!("{标签}: 周期{周期} 观察者不存在 (B)")),
};
let (eq, msg) = a_obs.(&b_obs, );
+205 -109
View File
@@ -26,15 +26,17 @@ 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::线;
use crate::structure::fractal_obj::;
use crate::types::;
use crate::utils::datetime;
use crate::{error, warn};
use parking_lot::RwLock;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::sync::{Arc, RwLock};
use tracing::{error, info};
/// 观察者 — 单周期分析器,持有所有层级序列,接收K线流式输入后逐层计算
pub struct {
@@ -198,6 +200,14 @@ 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();
@@ -298,17 +308,15 @@ impl 观察者 {
&[::, ::],
);
}
} else {
if self..线 {
let = self.线[i - 1].clone();
线::(
&,
&mut self.线[i],
&self.,
0,
&[::, ::],
);
}
} else if self..线 {
let (left, right) = self.线.split_at_mut(i);
线::(
&left[i - 1],
&mut right[0],
&self.,
0,
&[::, ::],
);
}
if self..线 {
::(&self.线[i], &mut self.[i], true, "", 0);
@@ -323,11 +331,9 @@ impl 观察者 {
if self..线 {
线::(&self., &mut self.线[i], &self.);
}
} else {
if self..线 {
let = self.线[i - 1].clone();
线::(&, &mut self.线[i], &self.);
}
} else if self..线 {
let (left, right) = self.线.split_at_mut(i);
线::(&left[i - 1], &mut right[0], &self.);
}
if self..线 {
::(
@@ -345,8 +351,11 @@ impl 观察者 {
if self..线 || self..线 {
for i in 0..self.线.min(self.线.len()) {
if self..线 {
let = self.线[i].clone();
线::(&, &mut self.线[i], &self.);
线::(
&self.线[i],
&mut self.线[i],
&self.,
);
}
if self..线 {
::(
@@ -430,17 +439,15 @@ impl 观察者 {
&[::, ::],
);
}
} else {
if self..线 {
let = self.线[i - 1].clone();
线::(
&,
&mut self.线[i],
&self.,
0,
&[::, ::],
);
}
} else if self..线 {
let (left, right) = self.线.split_at_mut(i);
线::(
&left[i - 1],
&mut right[0],
&self.,
0,
&[::, ::],
);
}
if self..线 {
::(&self.线[i], &mut self.[i], true, "", 0);
@@ -454,11 +461,9 @@ impl 观察者 {
if self..线 {
线::(&self., &mut self.线[i], &self.);
}
} else {
if self..线 {
let = self.线[i - 1].clone();
线::(&, &mut self.线[i], &self.);
}
} else if self..线 {
let (left, right) = self.线.split_at_mut(i);
线::(&left[i - 1], &mut right[0], &self.);
}
if self..线 {
::(
@@ -475,8 +480,11 @@ impl 观察者 {
if self..线 || self..线 {
for i in 0..self.线.min(self.线.len()) {
if self..线 {
let = self.线[i].clone();
线::(&, &mut self.线[i], &self.);
线::(
&self.线[i],
&mut self.线[i],
&self.,
);
}
if self..线 {
::(
@@ -517,7 +525,7 @@ impl 观察者 {
return Ok(());
}
let : Vec<String> = .iter().map(|d| d.()).collect();
let = format!("{}.txt", [0]..read().unwrap());
let = format!("{}.txt", [0]..read());
std::fs::write(.join(&), .join("\n") + "\n")?;
Ok(())
};
@@ -528,7 +536,7 @@ impl 观察者 {
return Ok(());
}
let : Vec<String> = .iter().map(|h| h.()).collect();
let = format!("{}.txt", [0]..read().unwrap());
let = format!("{}.txt", [0]..read());
std::fs::write(.join(&), .join("\n") + "\n")?;
Ok(())
};
@@ -563,7 +571,7 @@ impl 观察者 {
ck..load(Ordering::Relaxed),
ck..load(Ordering::Relaxed),
ck.,
*ck..read().unwrap(),
*ck..read(),
ck..get(),
ck..get(),
ck.,
@@ -597,7 +605,7 @@ impl 观察者 {
_数据文本.join("\n") + "\n",
);
info!("全部数据拆分保存完成,目录:{}", .display());
warn!("全部数据拆分保存完成,目录:{}", .display());
.display().to_string()
}
@@ -734,6 +742,7 @@ impl 观察者 {
mod tests {
use super::*;
use crate::config::;
use crate::info;
fn test_data_path() -> String {
let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
@@ -749,10 +758,9 @@ mod tests {
fn test_普k序列指针一致性() {
let obs = ::new("btcusd".into(), 300, Default::default());
obs.write()
.unwrap()
.(&test_data_path(), Default::default())
.unwrap();
let obs_ref = obs.read().unwrap();
let obs_ref = obs.read();
for (i, bi) in obs_ref..iter().enumerate() {
let pu_seq = bi.K序列(&obs_ref.K线序列);
@@ -761,12 +769,7 @@ mod tests {
info!(" 文.中.标的K线 原始起始序号: {}", bi...);
info!(
" 武.中.标的K线 原始结束序号: {}",
bi.
.read()
.unwrap()
.
.
.load(Ordering::Relaxed)
bi..read()...load(Ordering::Relaxed)
);
info!(" 普通K线序列.len: {}", obs_ref.K线序列.len());
} else {
@@ -777,7 +780,7 @@ mod tests {
.any(|k| Arc::as_ptr(k) == first_ptr);
if !found {
info!("笔 {}: 获取普K序列[0] 的 Rc 指针不在 普通K线序列 中!", i);
let wen_ptr = Arc::as_ptr(&*bi...K线.read().unwrap());
let wen_ptr = Arc::as_ptr(&*bi...K线.read());
let wen_found = obs_ref
.K线序列
.iter()
@@ -802,11 +805,11 @@ mod tests {
let offset = i * size;
if let Some(k线) = K线::from_bytes(&data[offset..offset + size], 300, "btcusd") {
let _k线_py_inner = Arc::new(k线.clone());
obs_ref.write().unwrap().K线(k线);
obs_ref.write().K线(k线);
}
}
let obs = obs_ref.read().unwrap();
let obs = obs_ref.read();
info!("普通K线序列.len: {}", obs.K线序列.len());
info!("笔序列.len: {}", obs..len());
@@ -835,10 +838,9 @@ mod tests {
fn test_分型到笔的文武Rc指针一致性() {
let obs = ::new("btcusd".into(), 300, Default::default());
obs.write()
.unwrap()
.(&test_data_path(), Default::default())
.unwrap();
let obs_ref = obs.read().unwrap();
let obs_ref = obs.read();
// 每个笔的文/武 分型 Rc 指针必须在 分型序列 中
for (i, bi) in obs_ref..iter().enumerate() {
@@ -848,13 +850,13 @@ mod tests {
info!("笔 {}: 文(时间戳={}) 不在分型序列中!", i, bi..());
}
let _ptr = Arc::as_ptr(&*bi..read().unwrap());
let _ptr = Arc::as_ptr(&*bi..read());
let _found = obs_ref..iter().any(|f| Arc::as_ptr(f) == _ptr);
if !_found {
info!(
"笔 {}: 武(时间戳={}) 不在分型序列中!",
i,
bi..read().unwrap().()
bi..read().()
);
}
}
@@ -868,14 +870,13 @@ mod tests {
fn test_笔到线段的基础序列Rc指针一致性() {
let obs = ::new("btcusd".into(), 300, Default::default());
obs.write()
.unwrap()
.(&test_data_path(), Default::default())
.unwrap();
let obs_ref = obs.read().unwrap();
let obs_ref = obs.read();
// 每个线段的基础序列中的笔 Rc 指针必须在 笔序列 中
for (i, seg) in obs_ref.线().iter().enumerate() {
for (j, bi_in_seg) in seg..read().unwrap().iter().enumerate() {
for (j, bi_in_seg) in seg..read().iter().enumerate() {
let bi_ptr = Arc::as_ptr(bi_in_seg);
let found = obs_ref..iter().any(|b| Arc::as_ptr(b) == bi_ptr);
if !found {
@@ -893,13 +894,12 @@ mod tests {
fn test_中枢基础序列与笔序列Rc指针一致() {
let obs = ::new("btcusd".into(), 300, Default::default());
obs.write()
.unwrap()
.(&test_data_path(), Default::default())
.unwrap();
let obs_ref = obs.read().unwrap();
let obs_ref = obs.read();
for (i, hub) in obs_ref._中枢序列.iter().enumerate() {
for (j, bi_in_hub) in hub..read().unwrap().iter().enumerate() {
for (j, bi_in_hub) in hub..read().iter().enumerate() {
let bi_ptr = Arc::as_ptr(bi_in_hub);
let found = obs_ref..iter().any(|b| Arc::as_ptr(b) == bi_ptr);
if !found {
@@ -909,7 +909,7 @@ mod tests {
}
for (i, hub) in obs_ref.().iter().enumerate() {
for (j, seg_in_hub) in hub..read().unwrap().iter().enumerate() {
for (j, seg_in_hub) in hub..read().iter().enumerate() {
let seg_ptr = Arc::as_ptr(seg_in_hub);
let found = obs_ref.线().iter().any(|s| Arc::as_ptr(s) == seg_ptr);
if !found {
@@ -934,10 +934,10 @@ mod tests {
for i in 0..data.len() / size {
let offset = i * size;
if let Some(k线) = K线::from_bytes(&data[offset..offset + size], 300, "btcusd") {
obs_ref.write().unwrap().K线(k线);
obs_ref.write().K线(k线);
}
}
let obs = obs_ref.read().unwrap();
let obs = obs_ref.read();
(
obs..len(),
obs.线().len(),
@@ -975,28 +975,28 @@ mod tests {
for i in 0..data.len() / size {
let offset = i * size;
if let Some(k线) = K线::from_bytes(&data[offset..offset + size], 300, "btcusd") {
obs_ref.write().unwrap().K线(k线);
obs_ref.write().K线(k线);
}
}
let = obs_ref.read().unwrap()..len();
let = obs_ref.read().unwrap().线().len();
let = obs_ref.read()..len();
let = obs_ref.read().线().len();
// 重置
obs_ref.write().unwrap().();
assert_eq!(obs_ref.read().unwrap()..len(), 0);
assert_eq!(obs_ref.read().unwrap().线().len(), 0);
obs_ref.write().();
assert_eq!(obs_ref.read()..len(), 0);
assert_eq!(obs_ref.read().线().len(), 0);
// 重新投喂
for i in 0..data.len() / size {
let offset = i * size;
if let Some(k线) = K线::from_bytes(&data[offset..offset + size], 300, "btcusd") {
obs_ref.write().unwrap().K线(k线);
obs_ref.write().K线(k线);
}
}
let = obs_ref.read().unwrap()..len();
let = obs_ref.read().unwrap().线().len();
let = obs_ref.read()..len();
let = obs_ref.read().线().len();
assert_eq!(, , "重置后重新投喂笔数不一致");
assert_eq!(, , "重置后重新投喂线段数不一致");
@@ -1011,31 +1011,30 @@ mod tests {
fn test_RefCell借用安全性_连续读取不panic() {
let obs = ::new("btcusd".into(), 300, Default::default());
obs.write()
.unwrap()
.(&test_data_path(), Default::default())
.unwrap();
let obs_ref = obs.read().unwrap();
let obs_ref = obs.read();
// 连续大量读取所有 RefCell 字段,不应 panic
for _ in 0..100 {
for bi in &obs_ref. {
let _标识 = bi..read().unwrap().clone();
let _wu = bi..read().unwrap().clone();
let _基础序列 = bi..read().unwrap().len();
let _特征序列 = bi..read().unwrap().len();
let _模式 = bi..read().unwrap().clone();
let _实中枢 = bi._中枢序列.read().unwrap().len();
let _虚中枢 = bi._中枢序列.read().unwrap().len();
let _合中枢 = bi._中枢序列.read().unwrap().len();
let _确认K = bi.K线.read().unwrap().is_some();
let _标识 = bi..read().clone();
let _wu = bi..read().clone();
let _基础序列 = bi..read().len();
let _特征序列 = bi..read().len();
let _模式 = bi..read().clone();
let _实中枢 = bi._中枢序列.read().len();
let _虚中枢 = bi._中枢序列.read().len();
let _合中枢 = bi._中枢序列.read().len();
let _确认K = bi.K线.read().is_some();
let _序号 = bi..load(Ordering::Relaxed);
let _有效性 = bi..load(Ordering::Relaxed);
let _短路 = bi..load(Ordering::Relaxed);
let _前一缺口 = *bi..read().unwrap();
let _前一缺口 = *bi..read();
}
for seg in obs_ref.线() {
let _ = seg..read().unwrap().clone();
let _ = seg..read().unwrap().len();
let _ = seg..read().clone();
let _ = seg..read().len();
}
}
// 到达这里 = 无 panic
@@ -1045,27 +1044,26 @@ mod tests {
fn test_RefCell借用安全性_交替读写不panic() {
let obs = ::new("btcusd".into(), 300, Default::default());
obs.write()
.unwrap()
.(&test_data_path(), Default::default())
.unwrap();
let obs_ref = obs.read().unwrap();
let obs_ref = obs.read();
// 交替读写 RefCell 字段 — 先读再写同字段,分离 borrow 作用域
if !obs_ref..is_empty() {
let bi = &obs_ref.[0];
// 读
let old_mode = bi..read().unwrap().clone();
let old_mode = bi..read().clone();
// Ref 已释放,可以写
*bi..write().unwrap() = "测试模式".into();
let new_mode = bi..read().unwrap().clone();
*bi..write() = "测试模式".into();
let new_mode = bi..read().clone();
assert_eq!(new_mode, "测试模式");
// 恢复
*bi..write().unwrap() = old_mode;
*bi..write() = old_mode;
// 读武
let old_wu = bi..read().unwrap().clone();
let old_wu = bi..read().clone();
// Ref 已释放,可以检查
assert!(Arc::as_ptr(&old_wu) == Arc::as_ptr(&old_wu));
assert!(Arc::ptr_eq(&old_wu, &old_wu));
}
}
@@ -1077,10 +1075,9 @@ mod tests {
fn test_缠K到分型的Rc指针一致性() {
let obs = ::new("btcusd".into(), 300, Default::default());
obs.write()
.unwrap()
.(&test_data_path(), Default::default())
.unwrap();
let obs_ref = obs.read().unwrap();
let obs_ref = obs.read();
// 每个分型的左/中/右 缠K 指针必须在 缠论K线序列 中
for (i, f) in obs_ref..iter().enumerate() {
@@ -1196,7 +1193,7 @@ mod tests {
99
});
assert_eq!(handle.join().unwrap(), 99);
assert_eq!(dash..read().unwrap().as_str(), "");
assert_eq!(dash..read().as_str(), "");
}
/// 测试:Arc<中枢> 可跨线程传递
@@ -1222,11 +1219,11 @@ mod tests {
let obs3 = Arc::clone(&obs);
let h1 = std::thread::spawn(move || {
let guard = obs2.read().unwrap();
let guard = obs2.read();
guard..clone()
});
let h2 = std::thread::spawn(move || {
let guard = obs3.read().unwrap();
let guard = obs3.read();
guard.
});
@@ -1274,7 +1271,7 @@ mod tests {
let obs = ::new("ethusd".into(), 7200, Default::default());
let handle = std::thread::spawn(move || {
let guard = obs.read().unwrap();
let guard = obs.read();
(guard..clone(), guard.)
});
@@ -1288,7 +1285,7 @@ mod tests {
let mut config = ::default();
config. = test_data_path();
let obs = ::new("btcusd".into(), 300, config);
let mut obs_w = obs.write().unwrap();
let mut obs_w = obs.write();
obs_w.线 = 0;
obs_w.();
drop(obs_w);
@@ -1299,11 +1296,11 @@ mod tests {
for i in 0..(data.len() / size).min(500) {
let offset = i * size;
if let Some(k线) = K线::from_bytes(&data[offset..offset + size], 300, "btcusd") {
obs.write().unwrap().K线(k线);
obs.write().K线(k线);
}
}
let obs_r = obs.read().unwrap();
let obs_r = obs.read();
assert!(obs_r.K线序列.len() > 0, "缠K序列应有数据");
assert!(obs_r..len() > 0, "分型序列应有数据");
assert!(obs_r.线.is_empty(), "线段序列组应为空");
@@ -1329,17 +1326,16 @@ mod tests {
// 先正常投喂数据
obs.write()
.unwrap()
.(&test_data_path(), Default::default())
.unwrap();
// 设为0后执行静态重新分析,不应 panic
let mut obs_w = obs.write().unwrap();
let mut obs_w = obs.write();
obs_w.线 = 0;
obs_w.();
drop(obs_w);
let obs_r = obs.read().unwrap();
let obs_r = obs.read();
assert!(obs_r..len() > 0, "静态重新分析后分型序列应有数据");
assert!(obs_r.线.is_empty(), "线段序列组应为空");
assert!(
@@ -1352,4 +1348,104 @@ mod tests {
obs_r..len()
);
}
/// 创建随机配置(与 main.py 随机配置 对齐)
fn () -> crate::config:: {
let mut cfg = crate::config::::default().();
cfg.K合并替换 = fastrand::bool();
cfg. = fastrand::i64(3..=9);
cfg. = fastrand::bool();
cfg. = fastrand::bool();
cfg.K线包含整笔 = fastrand::bool();
cfg. = fastrand::bool();
cfg. = fastrand::bool();
cfg._原始数量 = fastrand::i64(3..=9);
cfg.线_非缺口下穿刺 = fastrand::bool();
cfg.线_特征序列忽视老阴老阳 = fastrand::bool();
cfg.线_修正 = fastrand::bool();
cfg.线_缺口后紧急修正 = fastrand::bool();
cfg.线_当下分析 = fastrand::bool();
cfg. = fastrand::bool();
cfg.MACD柱强相关 = fastrand::bool();
cfg
}
/// 单线程工作函数(与 main.py 运行单个回测 对齐)
fn (线: usize, limit: usize) {
let = ();
let =
crate::business::observer::::new(format!("btcusd_{}", 线), 300, );
let = [
crate::types::::,
crate::types::::,
crate::types::::,
crate::types::::,
crate::types::::,
crate::types::::,
];
let mut K线 = crate::kline::bar::K线::K(
&format!("btcusd_{}", 线),
1218124800 + 线 as i64 * 300,
8888.55,
10000.00,
9000.22,
9527.33,
888.0,
0,
300,
);
.write().K线(K线.clone());
let = crate::types::::(limit, &, true);
for in & {
K线 = K线.K线生成新K线(*, false);
.write().K线(K线.clone());
}
}
#[test]
fn test_50线程压测_10000K线() {
let 线 = 50usize;
let 线K线数 = 10000usize;
let start = std::time::Instant::now();
let mut = Vec::with_capacity(线);
let = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
for i in 1..=线 {
let _ref = .clone();
.push(std::thread::spawn(move || {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
(i, 线K线数);
}));
if result.is_err() {
_ref.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
}));
}
for h in {
let _ = h.join();
}
let = start.elapsed();
let K线数 = 线 * (线K线数 + 1);
let = .load(std::sync::atomic::Ordering::Relaxed);
eprintln!(
"核心层 {}线程×{}K线 = {} 次 耗时 {:.2?} ({:.0} K/s) 异常线程: {}",
线,
线K线数,
K线数,
,
K线数 as f64 / .as_secs_f64(),
);
}
#[test]
fn test_单线程基准_10000K线() {
let start = std::time::Instant::now();
(0, 10000);
let = start.elapsed();
eprintln!(
"核心层 单线程 10000 K线 耗时 {:.2?} ({:.0} K/s)",
,
10001.0 / .as_secs_f64()
);
}
}
+2 -2
View File
@@ -23,8 +23,8 @@
*/
use crate::kline::bar::K线;
use crate::warn;
use std::collections::HashMap;
use tracing;
/// 事件回调类型 — fn(信号类型, 标识, 周期, 完成K线)
type = Box<dyn Fn(String, String, i64, K线) + Send + Sync>;
@@ -178,7 +178,7 @@ impl K线合成器 {
.map(|s| s.to_string())
.or_else(|| e.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "未知错误".into());
tracing::error!("K线合成器 事件回调 异常: {}", msg);
warn!("K线合成器 事件回调 异常: {}", msg);
}
}
}
+69 -209
View File
@@ -22,9 +22,9 @@
* SOFTWARE.
*/
use crate::warn;
use serde::{Deserialize, Deserializer, Serialize};
use std::collections::HashMap;
use tracing::warn;
/// 缠论配置 —— 控制所有分析阶段的行为
///
@@ -92,103 +92,34 @@ pub struct 缠论配置 {
// ---- 指标 ----
/// 是否计算技术指标
pub : bool,
/// 是否计算布林带
pub BOLL: bool,
/// 指标计算方式(开/高/低/收/高低均值/高低收均值/开高低收均值)
/// 指标计算方式(均线使用,MACD/RSI/KDJ/BOLL 在参数元组中指定)
#[serde(deserialize_with = "deserialize_指标计算方式")]
pub : String,
// ---- MACD ----
/// MACD 快线 EMA 周期
pub 线_快线周期: i64,
/// MACD 慢线 EMA 周期
pub 线_慢线周期: i64,
/// MACD 信号线周期
pub 线_信号周期: i64,
/// MACD 多参数列表: Vec<(key, 快线, 慢线, 信号)>
/// MACD 参数列表 (key, 计算方式, 快线, 慢线, 信号)
#[serde(default)]
pub MACD_参数列表: Vec<(String, i64, i64, i64)>,
pub MACD_参数列表: Vec<(String, String, i64, i64, i64)>,
// ---- RSI ----
/// RSI 计算周期
pub _周期: i64,
/// RSI SMA 平滑周期
pub _移动平均线周期: i64,
/// RSI 超买阈值
pub _超买阈值: f64,
/// RSI 超卖阈值
pub _超卖阈值: f64,
/// RSI 多周期列表: Vec<(key, 周期)>
/// RSI 参数列表 (key, 计算方式, 周期, MA周期, 超买, 超卖)
#[serde(default)]
pub RSI_周期列表: Vec<(String, i64)>,
pub RSI_周期列表: Vec<(String, String, i64, i64, f64, f64)>,
// ---- 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平滑)>
/// KDJ 参数列表 (key, 计算方式, RSV, K平滑, D平滑, 超买, 超卖)
#[serde(default)]
pub KDJ_参数列表: Vec<(String, i64, i64, i64)>,
pub KDJ_参数列表: Vec<(String, String, i64, i64, i64, f64, f64)>,
// ---- BOLL ----
/// 布林带周期
pub _周期: i64,
/// 布林带标准差倍数
pub _标准差倍数: f64,
/// BOLL 多参数列表: Vec<(key, 周期, 标准差倍数)>
/// BOLL 参数列表 (key, 计算方式, 周期, 标准差倍数)
#[serde(default)]
pub BOLL_参数列表: Vec<(String, i64, f64)>,
pub BOLL_参数列表: Vec<(String, String, i64, f64)>,
// ---- 均线 ----
/// 均线类型列表: ["SMA", "EMA", ...]
/// 均线参数列表 (key, 计算方式, 类型, 周期) — 如 ("SMA_5", "收", "SMA", 5)
#[serde(default)]
pub 线_类型列表: Vec<String>,
/// 均线周期列表: [5, 10, 20, ...]
#[serde(default)]
pub 线_周期列表: Vec<i64>,
pub 线: Vec<(String, String, String, i64)>,
// ---- 推送/显示 ----
/// 是否启用图表展示
pub : bool,
/// 是否推送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,
/// 图表展示标签: None=全部, [] = 不展示
pub : Option<Vec<String>>,
// ---- 买卖点 ----
/// 买卖点偏移量
@@ -310,44 +241,14 @@ impl Default for 缠论配置 {
线: true,
: String::new(),
: true,
BOLL: false,
: "".into(),
线_快线周期: 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(),
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(),
: true,
K线: true,
: true,
线: true,
: true,
_笔: true,
_线段: true,
_扩展线段: true,
_扩展线段_线段: true,
_线段_线段: true,
_中枢_笔: true,
_中枢_线段: true,
_中枢_扩展线段: true,
_中枢_扩展线段_线段: true,
_中枢_线段_线段: true,
_中枢_线段内部: true,
: None,
: 1,
: false,
MACD柱强相关: false,
@@ -366,46 +267,42 @@ impl Default for 缠论配置 {
}
impl {
/// 解析MACD参数列表 — 如果列表非空则使用列表,否则返回默认单组
pub fn _解析MACD参数列表(&self) -> Vec<(String, i64, i64, i64)> {
if !self.MACD_参数列表.is_empty() {
return self.MACD_参数列表.clone();
/// 展示标签判定 — None=全部, [] = 全关
pub fn (&self, : &str) -> bool {
match &self. {
None => true,
Some(tags) => tags.iter().any(|t| t == ),
}
vec![(
"macd".into(),
self.线_快线周期,
self.线_慢线周期,
self.线_信号周期,
)]
}
/// 解析RSI周期列表 — 如果列表非空则使用列表,否则返回默认单组
pub fn _解析RSI周期列表(&self) -> Vec<(String, i64)> {
if !self.RSI_周期列表.is_empty() {
return self.RSI_周期列表.clone();
/// 统一设置所有指标参数(对应 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;
}
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) = MACD {
self.MACD_参数列表 = 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();
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![("boll".into(), self._周期, self._标准差倍数)]
}
/// 序列化为 JSON 字典(对应 Python to_dict,仅返回 model_fields 中的字段)
@@ -461,8 +358,11 @@ impl 缠论配置 {
/// 返回字段名列表(对应 Python model_fields().keys()
pub fn model_fields() -> &'static [&'static str] {
&[
// ---- 基础 ----
"标识",
// ---- 缠K ----
"缠K合并替换",
// ---- 笔 ----
"笔内元素数量",
"笔内相同终点取舍",
"笔内起始分型包含整笔",
@@ -471,57 +371,33 @@ impl 缠论配置 {
"笔次级成笔",
"笔弱化",
"笔弱化_原始数量",
// ---- 线段 ----
"线段_非缺口下穿刺",
"线段_特征序列忽视老阴老阳",
"线段_缺口后紧急修正",
"线段_修正",
"线段内部中枢图显",
"扩展线段_当下分析",
// ---- 分析开关 ----
"分析笔",
"分析线段",
"分析扩展线段",
"分析笔中枢",
"分析线段中枢",
// ---- 终止 ----
"手动终止",
// ---- 指标 ----
"计算指标",
"计算BOLL",
"指标计算方式",
"平滑异同移动平均线_快线周期",
"平滑异同移动平均线_慢线周期",
"平滑异同移动平均线_信号周期",
"相对强弱指数_周期",
"相对强弱指数_移动平均线周期",
"相对强弱指数_超买阈值",
"相对强弱指数_超卖阈值",
"随机指标_RSV周期",
"随机指标_K值平滑周期",
"随机指标_D值平滑周期",
"随机指标_超买阈值",
"随机指标_超卖阈值",
"布林带_周期",
"布林带_标准差倍数",
"MACD_参数列表",
"RSI_周期列表",
"KDJ_参数列表",
"BOLL_参数列表",
"均线_类型列表",
"均线_周期列表",
"均线参数列表",
// ---- 推送/显示 ----
"图表展示",
"推送K线",
"推送笔",
"推送线段",
"推送中枢",
"图表展示_笔",
"图表展示_线段",
"图表展示_扩展线段",
"图表展示_扩展线段_线段",
"图表展示_线段_线段",
"图表展示_中枢_笔",
"图表展示_中枢_线段",
"图表展示_中枢_扩展线段",
"图表展示_中枢_扩展线段_线段",
"图表展示_中枢_线段_线段",
"图表展示_中枢_线段内部",
"图表展示标签",
// ---- 买卖点 ----
"买卖点偏移",
"买卖点激进识别",
"买卖点与MACD柱强相关",
@@ -530,10 +406,12 @@ impl 缠论配置 {
"买卖点_指标匹配_MACD",
"买卖点_指标匹配_KDJ",
"买卖点_指标匹配_RSI",
// ---- 背驰 ----
"线段内部背驰_MACD",
"线段内部背驰_斜率",
"线段内部背驰_测度",
"线段内部背驰_模式",
// ---- 文件 ----
"加载文件路径",
]
}
@@ -576,21 +454,7 @@ impl 缠论配置 {
Self {
线: false,
: false,
K线: false,
: false,
线: false,
: false,
_笔: false,
_线段: false,
_扩展线段: false,
_扩展线段_线段: false,
_线段_线段: false,
_中枢_笔: false,
_中枢_线段: false,
_中枢_扩展线段: false,
_中枢_扩展线段_线段: false,
_中枢_线段_线段: false,
_中枢_线段内部: false,
: Some(vec![]),
..self.clone()
}
}
@@ -752,18 +616,15 @@ 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]
@@ -779,9 +640,8 @@ mod tests {
fn test_不推送() {
let config = ::default();
let muted = config.();
assert!(!muted.K线);
assert!(!muted.);
assert!(!muted.);
assert!(!muted.线);
assert_eq!(muted., 5);
}
@@ -795,7 +655,7 @@ mod tests {
#[test]
fn test_对比_有差异() {
let mut a = ::default();
let a = ::default();
let mut b = ::default();
b. = "changed".into();
b. = 99;
@@ -854,14 +714,14 @@ mod tests {
#[test]
fn test_对比_boolean_difference() {
let mut a = ::default();
let a = ::default();
let mut b = ::default();
b.K线 = false;
b. = false;
b. = false;
let diff = a.(&b);
assert_eq!(diff.len(), 2);
assert_eq!(diff["推送K线"], serde_json::json!(false));
assert_eq!(diff["分析笔"], serde_json::json!(false));
assert_eq!(diff["图表展示"], serde_json::json!(false));
}
+6 -5
View File
@@ -24,6 +24,7 @@
use crate::kline::bar::K线;
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
/// 布林带(BOLL)— 基于移动平均和标准差的波动率通道
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -43,7 +44,7 @@ pub struct 布林带 {
pub : f64,
/// 内部历史队列(不序列化)
#[serde(skip)]
_历史队列: Vec<f64>,
_历史队列: VecDeque<f64>,
/// 内部均值缓存(不序列化)
#[serde(skip)]
_均值: f64,
@@ -61,7 +62,7 @@ impl Default for 布林带 {
: 0.0,
: 0.0,
: 0.0,
_历史队列: Vec::new(),
_历史队列: VecDeque::new(),
_均值: 0.0,
_方差和: 0.0,
}
@@ -98,7 +99,7 @@ impl 布林带 {
: ,
: ,
: ,
_历史队列: vec![],
_历史队列: VecDeque::from([]),
_均值: ,
_方差和: 0.0,
}
@@ -110,9 +111,9 @@ impl 布林带 {
let = prev.;
let mut q = prev._历史队列.clone();
q.push();
q.push_back();
if q.len() > {
q.remove(0);
q.pop_front();
}
let (_均值, _方差和) = if q.len() < {
+443 -221
View File
@@ -32,44 +32,56 @@ use std::sync::Arc;
pub struct ;
impl {
/// 增量计算所有开启的指标,将结果写入 当前K线.指标
/// 增量计算所有开启的指标,将结果写入每一根 K 线。
///
/// `全序列` 包含当前K线(在末尾);prev 取自 全序列[..-1].last()
/// 通过 RwLock 内部可变性,以 `&K线` 共享引用写入指标值
pub fn (: &[Arc<K线>], : &) {
let n = .len();
let K线 = &[n - 1];
let = if n > 1 { &[..n - 1] } else { &[] };
if n == 0 {
return;
}
if !. && .线.is_empty() {
return;
}
// 作用域化 prev_guard:在 _回填新指标 之前释放,避免读锁与回填写锁冲突
let has_prev;
{
let prev_guard = if n > 1 {
Some([n - 2]..read().unwrap())
} 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, );
// 找到第一个 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
}
Self::_更新均线(K线, , );
has_prev = n > 1;
} // prev_guard dropped here
}
if has_prev {
// 回填:若有新增指标参数但首K线未被本轮计算覆盖,仍需填充历史K线
if n > 1 && start > 0 {
Self::_回填新指标(, );
}
}
fn _计算MACD组(K线: &K线, prev: Option<&>, : &) {
let = &.;
for (i, (key, , , )) in ._解析MACD参数列表().into_iter().enumerate()
{
let val = if let Some(prev_val) = prev.and_then(|p| p.(&key)) {
for (key, , , , ) in .MACD_参数列表.iter() {
let val = if let Some(prev_val) = prev.and_then(|p| p.(key)) {
if let ::MACD(prev_macd) = prev_val {
::MACD(线::(
prev_macd,
@@ -95,22 +107,19 @@ impl 指标计算器 {
,
),
K线.,
,
,
,
*,
*,
*,
))
};
K线..write().unwrap().(&key, val.clone());
if i == 0 {
K线..write().unwrap().("macd", val);
}
K线..write().(key, val.clone());
}
}
fn _计算RSI组(K线: &K线, prev: Option<&>, : &) {
let = &.;
for (i, (key, )) in ._解析RSI周期列表().into_iter().enumerate() {
let val = if let Some(prev_val) = prev.and_then(|p| p.(&key)) {
for (key, , , ma周期, , ) in .RSI_周期列表.iter()
{
let val = if let Some(prev_val) = prev.and_then(|p| p.(key)) {
if let ::RSI(prev_rsi) = prev_val {
::RSI(::(
prev_rsi,
@@ -136,23 +145,19 @@ impl 指标计算器 {
,
),
K线.,
,
._超买阈值,
._超卖阈值,
Some(._移动平均线周期),
*,
*,
*,
Some(*ma周期),
))
};
K线..write().unwrap().(&key, val.clone());
if i == 0 {
K线..write().unwrap().("rsi", val);
}
K线..write().(key, val.clone());
}
}
fn _计算KDJ组(K线: &K线, prev: Option<&>, : &) {
for (i, (key, rsv, k平滑, d平滑)) in ._解析KDJ参数列表().into_iter().enumerate()
{
let val = if let Some(prev_val) = prev.and_then(|p| p.(&key)) {
for (key, _fm, rsv, k平滑, d平滑, , ) in .KDJ_参数列表.iter() {
let val = if let Some(prev_val) = prev.and_then(|p| p.(key)) {
if let ::KDJ(prev_kdj) = prev_val {
::KDJ(::(
prev_kdj,
@@ -170,25 +175,20 @@ impl 指标计算器 {
K线.,
K线.,
K线.,
rsv,
k平滑,
d平滑,
._超买阈值,
._超卖阈值,
*rsv,
*k平滑,
*d平滑,
*,
*,
))
};
K线..write().unwrap().(&key, val.clone());
if i == 0 {
K线..write().unwrap().("kdj", val);
}
K线..write().(key, val.clone());
}
}
fn _计算BOLL组(K线: &K线, prev: Option<&>, : &) {
let = &.;
for (i, (key, , )) in ._解析BOLL参数列表().into_iter().enumerate()
{
let val = if let Some(prev_val) = prev.and_then(|p| p.(&key)) {
for (key, , , ) in .BOLL_参数列表.iter() {
let val = if let Some(prev_val) = prev.and_then(|p| p.(key)) {
if let ::BOLL(prev_boll) = prev_val {
::BOLL(::(
prev_boll,
@@ -214,22 +214,37 @@ impl 指标计算器 {
K线.,
,
),
as usize,
,
* as usize,
*,
))
};
K线..write().unwrap().(&key, val.clone());
if i == 0 {
K线..write().unwrap().("boll", val);
}
K线..write().(key, val.clone());
}
}
fn _更新均线(K线: &K线, : &[Arc<K线>], : &) {
if .线_类型列表.is_empty() || .线_周期列表.is_empty() {
if .线.is_empty() {
return;
}
let = &.;
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(), );
}
}
}
fn _增量SMA(
K线: &K线,
: &[Arc<K线>],
: &str,
period: i64,
prev_key: &str,
) -> f64 {
let = super::K线取值(
K线.,
K线.,
@@ -237,46 +252,20 @@ 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().unwrap().线_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;
// 现有序列 + 当前 = total_len
let total_len = existing_len + 1;
if total_len <= p {
let mut sum: f64 = [existing_len.saturating_sub(p.saturating_sub(1))..]
if existing_len < p {
let sum: f64 =
.iter()
.map(|k| super::K线取值(k., k., k., k., ))
.sum();
sum += ;
return sum / (total_len as f64).max(1.0);
.sum::<f64>()
+ ;
return sum / ((existing_len + 1) as f64).max(1.0);
}
// 尝试从前一根K线获取缓存的SMA
if let Some(prev) = .last().and_then(|k| {
let guard = k..read().unwrap();
guard.线().and_then(|m| m.get(prev_key)).copied()
}) {
if let Some(prev_sma) =
.last()
.and_then(|k| k..read().线().and_then(|m| m.get(prev_key)).copied())
{
let oldest = super::K线取值(
[existing_len - p].,
[existing_len - p].,
@@ -284,29 +273,33 @@ impl 指标计算器 {
[existing_len - p].,
,
);
return prev + ( - oldest) / period as f64;
return prev_sma + ( - oldest) / period as f64;
}
// 回退:完整计算
let mut sum: f64 = [existing_len.saturating_sub(p.saturating_sub(1))..]
let sum: f64 = [existing_len.saturating_sub(p.saturating_sub(1))..]
.iter()
.map(|k| super::K线取值(k., k., k., k., ))
.sum();
sum += ;
sum / (total_len as f64).min(p as f64)
.sum::<f64>()
+ ;
sum / ((existing_len + 1) as f64).min(p as f64)
}
/// 增量 EMA: 现有序列 (不含当前K线) + 当前价
fn _增量EMA(
K线: &K线,
: &[Arc<K线>],
: f64,
: &str,
period: i64,
_计算方式: &str,
prev_key: &str,
) -> f64 {
let = .last().and_then(|k| {
let guard = k..read().unwrap();
guard.线().and_then(|m| m.get(prev_key)).copied()
});
let = super::K线取值(
K线.,
K线.,
K线.,
K线.,
,
);
let =
.last()
.and_then(|k| k..read().线().and_then(|m| m.get(prev_key)).copied());
match {
None => ,
Some(prev) => {
@@ -318,159 +311,388 @@ impl 指标计算器 {
/// 运行中新增指标参数时,回填所有历史K线
fn _回填新指标(: &[Arc<K线>], : &) {
// 作用域化首尾读锁:在回填写循环之前释放,避免读锁与写锁冲突
let (MACD, RSI, KDJ, BOLL) = {
let K_guard = [0]..read().unwrap();
let K_guard = [.len() - 1]..read().unwrap();
let K_guard = [0]..read();
let K_guard = [.len() - 1]..read();
let MACD: Vec<_> =
._解析MACD参数列表()
.into_iter()
.filter(|(key, _, _, _)| K_guard.(key) && !K_guard.(key))
.MACD_参数列表
.iter()
.filter(|(key, ..)| K_guard.(key) && !K_guard.(key))
.cloned()
.collect();
let RSI: Vec<_> =
._解析RSI周期列表()
.into_iter()
.filter(|(key, _)| K_guard.(key) && !K_guard.(key))
.RSI_周期列表
.iter()
.filter(|(key, ..)| K_guard.(key) && !K_guard.(key))
.cloned()
.collect();
let KDJ: Vec<_> =
._解析KDJ参数列表()
.into_iter()
.filter(|(key, _, _, _)| K_guard.(key) && !K_guard.(key))
.KDJ_参数列表
.iter()
.filter(|(key, ..)| K_guard.(key) && !K_guard.(key))
.cloned()
.collect();
let BOLL: Vec<_> =
._解析BOLL参数列表()
.into_iter()
.filter(|(key, _, _)| K_guard.(key) && !K_guard.(key))
.BOLL_参数列表
.iter()
.filter(|(key, ..)| K_guard.(key) && !K_guard.(key))
.cloned()
.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 {
Some([i - 1]..read().unwrap())
Some([i - 1]..read())
} else {
None
};
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线(
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线(
k线,
,
*,
*,
*,
))
)),
};
k线..write().unwrap().(key, val);
k线..write().(key, val);
}
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线(
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线(
k线,
,
*,
._超买阈值,
._超卖阈值,
Some(._移动平均线周期),
))
*,
*,
Some(*ma周期),
)),
};
k线..write().unwrap().(key, val);
k线..write().(key, val);
}
for (key, rsv, k平滑, d平滑) in &KDJ {
let val = if let Some(ref prev) = prev_guard {
if let Some(::KDJ(prev_kdj)) = prev.(key) {
for (key, _fm, rsv, k平滑, d平滑, , ) in &KDJ {
let val = match prev_guard.as_ref().and_then(|p| p.(key)) {
Some(::KDJ(prev_kdj)) => {
::KDJ(::_K线(prev_kdj, k线))
} else {
::KDJ(::_K线(
k线,
*rsv,
*k平滑,
*d平滑,
._超买阈值,
._超卖阈值,
))
}
} else {
::KDJ(::_K线(
k线,
*rsv,
*k平滑,
*d平滑,
._超买阈值,
._超卖阈值,
))
_ => ::KDJ(::_K线(
k线, *rsv, *k平滑, *d平滑, *, *,
)),
};
k线..write().unwrap().(key, val);
k线..write().(key, val);
}
for (key, , ) in &BOLL {
let val = if let Some(ref prev) = prev_guard {
if let Some(::BOLL(prev_boll)) = prev.(key) {
for (key, , , ) in &BOLL {
let val = match prev_guard.as_ref().and_then(|p| p.(key)) {
Some(::BOLL(prev_boll)) => {
::BOLL(::_K线(prev_boll, k线, ))
} else {
::BOLL(::_K线(
k线,
,
* as usize,
*,
))
}
} else {
::BOLL(::_K线(
_ => ::BOLL(::_K线(
k线,
,
* as usize,
*,
))
)),
};
k线..write().unwrap().(key, val);
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=0EMA=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.,
);
}
}
+1 -3
View File
@@ -70,9 +70,7 @@ impl 指标容器 {
/// 预注册指标(不覆盖已有值)
pub fn (&mut self, : &str, : Option<>) {
if !self._数据.contains_key() {
self._数据.insert(.to_string(), );
}
self._数据.entry(.to_string()).or_insert();
}
/// 按名称获取指标值
+13 -12
View File
@@ -24,6 +24,7 @@
use crate::kline::bar::K线;
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
/// 随机指标 (KDJ)
///
@@ -58,9 +59,9 @@ pub struct 随机指标 {
/// J 值 (3K - 2D)
pub J: Option<f64>,
/// 历史最高价队列(滑动窗口)
pub : Vec<f64>,
pub : VecDeque<f64>,
/// 历史最低价队列(滑动窗口)
pub : Vec<f64>,
pub : VecDeque<f64>,
/// 前一个 RSV(用于平滑递推)
pub RSV: Option<f64>,
/// 前一个 K(用于平滑递推)
@@ -85,8 +86,8 @@ impl Default for 随机指标 {
K: None,
D: None,
J: None,
: Vec::new(),
: Vec::new(),
: VecDeque::new(),
: VecDeque::new(),
RSV: None,
K: None,
D: None,
@@ -124,8 +125,8 @@ impl 随机指标 {
K: None,
D: None,
J: None,
: vec![],
: vec![],
: VecDeque::from([]),
: VecDeque::from([]),
RSV: None,
K: None,
D: None,
@@ -181,16 +182,16 @@ impl 随机指标 {
// 更新历史最高价队列
let mut = KDJ..clone();
.push();
.push_back();
if .len() > N as usize {
.remove(0);
.pop_front();
}
// 更新历史最低价队列
let mut = KDJ..clone();
.push();
.push_back();
if .len() > N as usize {
.remove(0);
.pop_front();
}
// RSV
@@ -260,8 +261,8 @@ mod tests {
#[test]
fn test_first_calc() {
let kdj = ::(110.0, 90.0, 100.0, 1000, 9, 3, 3, 80.0, 20.0);
assert_eq!(kdj., vec![110.0]);
assert_eq!(kdj., vec![90.0]);
assert_eq!(kdj., VecDeque::from([110.0]));
assert_eq!(kdj., VecDeque::from([90.0]));
assert_eq!(kdj.K, None);
}
+20 -10
View File
@@ -24,6 +24,7 @@
use crate::kline::bar::K线;
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
/// 相对强弱指数 (RSI)
///
@@ -58,7 +59,9 @@ pub struct 相对强弱指数 {
/// RSI SMA 值
pub RSI_SMA: Option<f64>,
/// RSI 历史队列(用于滚动计算)
pub RSI历史队列: Vec<f64>,
pub RSI历史队列: VecDeque<f64>,
/// RSI 历史队列运行和(O(1) SMA
pub RSI和: f64,
}
impl Default for {
@@ -77,7 +80,8 @@ impl Default for 相对强弱指数 {
: 0.0,
: 0.0,
RSI_SMA: None,
RSI历史队列: Vec::new(),
RSI历史队列: VecDeque::new(),
RSI和: 0.0,
}
}
}
@@ -106,7 +110,8 @@ impl 相对强弱指数 {
: 0.0,
: 1.0 / as f64,
RSI_SMA: None,
RSI历史队列: Vec::new(),
RSI历史队列: VecDeque::new(),
RSI和: 0.0,
}
}
@@ -167,21 +172,25 @@ impl 相对强弱指数 {
};
// RSI_SMA
let (RSI_SMA, RSI历史队列) = match RSI_SMA周期 {
let (RSI_SMA, RSI历史队列, RSI和) = match RSI_SMA周期 {
Some(sma周期) if sma周期 > 0 => {
let mut = RSI.RSI历史队列.clone();
.push(RSI);
if .len() > sma周期 as usize {
.remove(0);
let mut sum = RSI.RSI和;
.push_back(RSI);
sum += RSI;
if .len() > sma周期 as usize
&& let Some(old) = .pop_front()
{
sum -= old;
}
let sma = if .is_empty() {
None
} else {
Some(.iter().sum::<f64>() / .len() as f64)
Some(sum / .len() as f64)
};
(sma, )
(sma, , sum)
}
_ => (None, Vec::new()),
_ => (None, VecDeque::new(), 0.0),
};
Self {
@@ -199,6 +208,7 @@ impl 相对强弱指数 {
,
RSI_SMA,
RSI历史队列,
RSI和,
}
}
}
+217 -5
View File
@@ -23,16 +23,19 @@
*/
use crate::indicators::;
use crate::indicators::{, 线, , };
use crate::info;
use crate::types::;
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::io::Write;
use std::sync::{Arc, RwLock};
use std::sync::Arc;
mod rwlock_container_serde {
use parking_lot::RwLock;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::sync::RwLock;
/// Serde 序列化辅助(RwLock<指标容器> → 序列化器)
pub fn serialize<S>(
@@ -42,7 +45,7 @@ mod rwlock_container_serde {
where
S: Serializer,
{
val.read().unwrap().serialize(ser)
val.read().serialize(ser)
}
/// Serde 反序列化辅助(反序列化器 → RwLock<指标容器>
@@ -115,7 +118,7 @@ impl Clone for K线 {
: self.,
: self.,
: self.,
: RwLock::new(self..read().unwrap().clone()),
: RwLock::new(self..read().clone()),
}
}
}
@@ -223,6 +226,7 @@ impl K线 {
/// 保存K线序列到 DAT 文件
pub fn DAT文件(: &str, K线序列: &[&Self]) -> std::io::Result<()> {
info!("保存到DAT文件: {}", );
let mut f = std::fs::File::create()?;
for k in K线序列 {
f.write_all(&k.to_bytes())?;
@@ -245,7 +249,7 @@ impl K线 {
let mut = 0.0f64;
let mut = 0.0f64;
for k in {
if let Some(macd) = k..read().unwrap().macd() {
if let Some(macd) = k..read().macd() {
let hist = macd.MACD柱;
if hist >= 0.0 {
+= hist;
@@ -314,6 +318,73 @@ impl K线 {
(true, "K线: 全部字段一致".into())
}
/// 根据当前K线和方向生成下一根K线(与 chan.py 对齐)
pub fn K线生成新K线(&self, : , : bool) -> Self {
let = self. - self.;
let = if {
* 0.5
} else {
let lo = ( * 0.1279) as i64;
let hi = ( * 0.883) as i64;
if hi > lo {
fastrand::i64(lo..=hi) as f64
} else {
lo as f64
}
};
let = if {
* 1.5
} else {
let lo = ( * 1.1279) as i64;
let hi = ( * 1.883) as i64;
if hi > lo {
fastrand::i64(lo..=hi) as f64
} else {
lo as f64
}
};
let (, ) = match {
:: => (self. + , self. + ),
:: => (self. - , self. - ),
:: => (self. + , self. + ),
:: => (self. - , self. - ),
:: => {
let off = ;
(self. + off, self.)
}
:: => {
let off = ;
(self., self. - off)
}
_ => (self., self.),
};
let = [self., self., self., self.]
.iter()
.map(|v| {
let s = format!("{v}");
s.split('.').nth(1).map(|d| d.len()).unwrap_or(0)
})
.max()
.unwrap_or(2);
let round = |v: f64| -> f64 {
let scale = 10_f64.powi( as i32);
(v * scale).round() / scale
};
let = round( + ( - ) * fastrand::f64());
let = round( + ( - ) * fastrand::f64());
Self::K(
&self.,
self. + self.,
,
round(),
round(),
,
998.0 * fastrand::f64(),
self. + 1,
self.,
)
}
/// 截取Arc<K线>序列中从始到终的片段
pub fn rc(: &[Arc<Self>], : &Arc<Self>, : &Arc<Self>) -> Vec<Arc<Self>> {
let _ptr = Arc::as_ptr();
@@ -325,6 +396,33 @@ 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线 {
@@ -385,4 +483,118 @@ mod tests {
assert_eq!(result.get(""), Some(&0.0));
assert_eq!(result.get(""), Some(&0.0));
}
// ---- 根据当前K线生成新K线 ----
#[test]
fn test_生成K线_居中向上() {
let bar = K线::K(
"test", 1000, 50000.0, 50200.0, 49800.0, 50100.0, 100.0, 0, 300,
);
let new = bar.K线生成新K线(::, true);
// 居中: 偏移 = (50200-49800)*0.5 = 200
assert!((new. - 50400.0).abs() < 1.0); // 50200 + 200
assert!((new. - 50000.0).abs() < 1.0); // 49800 + 200
assert_eq!(new., 1);
assert_eq!(new., 1300);
}
#[test]
fn test_生成K线_居中向下() {
let bar = K线::K(
"test", 1000, 50000.0, 50200.0, 49800.0, 50100.0, 100.0, 0, 300,
);
let new = bar.K线生成新K线(::, true);
assert!((new. - 50000.0).abs() < 1.0); // 50200 - 200
assert!((new. - 49600.0).abs() < 1.0); // 49800 - 200
}
#[test]
fn test_生成K线_居中向上缺口() {
let bar = K线::K(
"test", 1000, 50000.0, 50200.0, 49800.0, 50100.0, 100.0, 0, 300,
);
let new = bar.K线生成新K线(::, true);
// 居中缺口: 偏移 = 400*1.5 = 600
assert!((new. - 50800.0).abs() < 1.0); // 50200 + 600
assert!((new. - 50400.0).abs() < 1.0); // 49800 + 600
}
#[test]
fn test_生成K线_衔接向上() {
let bar = K线::K(
"test", 1000, 50000.0, 50200.0, 49800.0, 50100.0, 100.0, 0, 300,
);
let new = bar.K线生成新K线(::, true);
let = 50200.0 - 49800.0;
assert!((new. - (50200.0 + )).abs() < 1.0);
assert!((new. - 50200.0).abs() < 1.0); // 衔接向上: 低 = 原高
}
#[test]
fn test_生成K线_衔接向下() {
let bar = K线::K(
"test", 1000, 50000.0, 50200.0, 49800.0, 50100.0, 100.0, 0, 300,
);
let new = bar.K线生成新K线(::, true);
assert!((new. - 49800.0).abs() < 1.0); // 衔接向下: 高 = 原低
}
#[test]
fn test_生成K线_非居中随机范围() {
let bar = K线::K(
"test", 1000, 50000.0, 50200.0, 49800.0, 50100.0, 100.0, 0, 300,
);
// 非居中:偏移在 [高低差*0.1279, 高低差*0.883] 范围内随机
for _ in 0..20 {
let new = bar.K线生成新K线(::, false);
assert!(new. > bar., "向上:新高应高于原高");
assert!(new. > bar., "向上:新低应高于原低");
let = new. - bar.;
let = bar. - bar.;
let lo = * 0.1279;
let hi = * 0.883;
assert!(
>= lo && <= hi + 1.0,
"偏移 {偏移} 应在 [{lo}, {hi}] 范围内"
);
}
}
// ---- 从序列中机选 ----
#[test]
fn test_从序列中机选_可重复() {
let dirs = vec![::, ::, ::];
let result = ::(5, &dirs, true);
assert_eq!(result.len(), 5);
for d in &result {
assert!(dirs.contains(d));
}
}
#[test]
fn test_从序列中机选_不可重复() {
let dirs = vec![::, ::, ::];
let result = ::(3, &dirs, false);
assert_eq!(result.len(), 3);
for (i, d) in result.iter().enumerate() {
for prev in result[..i].iter() {
assert_ne!(prev, d, "重复方向: {:?}", d);
}
}
}
#[test]
#[should_panic(expected = "数量超过可选方向数")]
fn test_从序列中机选_数量超限() {
let dirs = vec![::, ::];
::(3, &dirs, false);
}
#[test]
fn test_从序列中机选_空序列() {
let result = ::(0, &[], true);
assert!(result.is_empty());
}
}
+70 -42
View File
@@ -29,9 +29,10 @@ use crate::structure::fractal_obj::分型;
use crate::types::SyncF64;
use crate::types::;
use crate::types::;
use parking_lot::RwLock;
use std::collections::HashSet;
use std::sync::Arc;
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::{Arc, RwLock};
/// 缠论K线 — 经包含处理过后的K线
///
@@ -74,15 +75,15 @@ impl Clone for 缠论K线 {
: AtomicI64::new(self..load(Ordering::Relaxed)),
: SyncF64::new(self..get()),
: SyncF64::new(self..get()),
: RwLock::new(*self..read().unwrap()),
: RwLock::new(*self..read().unwrap()),
: RwLock::new(*self..read()),
: RwLock::new(*self..read()),
: self.,
: self..clone(),
: SyncF64::new(self..get()),
: self.,
: AtomicI64::new(self..load(Ordering::Relaxed)),
K线: RwLock::new(Arc::clone(&self.K线.read().unwrap())),
: RwLock::new(self..read().unwrap().clone()),
K线: RwLock::new(Arc::clone(&self.K线.read())),
: RwLock::new(self..read().clone()),
}
}
}
@@ -97,10 +98,9 @@ impl std::fmt::Display for 缠论K线 {
self..load(Ordering::Relaxed),
self.
.read()
.unwrap()
.map_or("None".to_string(), |fx| fx.to_string()),
self.,
*self..read().unwrap(),
*self..read(),
self..load(Ordering::Relaxed),
format_f64_g(self..get()),
format_f64_g(self..get())
@@ -116,23 +116,23 @@ impl 缠论K线 {
: AtomicI64::new(self..load(Ordering::Relaxed)),
: SyncF64::new(self..get()),
: SyncF64::new(self..get()),
: RwLock::new(*self..read().unwrap()),
: RwLock::new(*self..read().unwrap()),
: RwLock::new(*self..read()),
: RwLock::new(*self..read()),
: self.,
: self..clone(),
: SyncF64::new(self..get()),
: self.,
: AtomicI64::new(self..load(Ordering::Relaxed)),
K线: RwLock::new(Arc::clone(&self.K线.read().unwrap())),
: RwLock::new(self..read().unwrap().clone()),
K线: RwLock::new(Arc::clone(&self.K线.read())),
: RwLock::new(self..read().clone()),
}
}
/// 与MACD柱子匹配 — 底分型时MACD柱应<0, 顶分型时>0
pub fn MACD柱子匹配(&self) -> bool {
let = self.K线.read().unwrap();
let = ..read().unwrap();
match *self..read().unwrap() {
let = self.K线.read();
let = ..read();
match *self..read() {
Some(::) | Some(::) => {
if let Some(macd) = .macd() {
macd.MACD柱 < 0.0
@@ -153,9 +153,9 @@ impl 缠论K线 {
/// 与RSI匹配 — 底分型时RSI应低于SMA, 顶分型时高于SMA
pub fn RSI匹配(&self) -> bool {
let = self.K线.read().unwrap();
let = ..read().unwrap();
match *self..read().unwrap() {
let = self.K线.read();
let = ..read();
match *self..read() {
Some(::) | Some(::) => {
if let Some(rsi) = .rsi() {
match (rsi.RSI, rsi.RSI_SMA) {
@@ -182,9 +182,9 @@ impl 缠论K线 {
/// 与KDJ匹配 — 底分型时K应低于D(死叉后), 顶分型时K应高于D(金叉后)
pub fn KDJ匹配(&self) -> bool {
let = self.K线.read().unwrap();
let = ..read().unwrap();
match *self..read().unwrap() {
let = self.K线.read();
let = ..read();
match *self..read() {
Some(::) | Some(::) => {
if let Some(kdj) = .kdj() {
match (kdj.K, kdj.D) {
@@ -356,12 +356,12 @@ impl 缠论K线 {
// 逆序包含时更新时间和标的K线
if != :: {
K..store(K., Ordering::Relaxed);
*K.K线.write().unwrap() = Arc::clone(K);
*K.K线.write() = Arc::clone(K);
}
K..set((K..get(), K.));
K..set((K..get(), K.));
K..store(K., Ordering::Relaxed);
*K..write().unwrap() = K.();
*K..write() = K.();
if let Some() = K {
K
@@ -466,29 +466,29 @@ impl 缠论K线 {
let = ::(&*, &*, &*, false, false);
// 对齐 Python:无条件设置 中.分型、中.分型特征值、右.分型特征值、右.分型
*K序列[idx - 2]..write().unwrap() = ;
*K序列[idx - 2]..write() = ;
if let Some() = {
match {
:: => {
K序列[idx - 2]..set(K序列[idx - 2]..get());
K序列[idx - 1]..set(K序列[idx - 1]..get());
*K序列[idx - 1]..write().unwrap() = Some(::);
*K序列[idx - 1]..write() = Some(::);
}
:: => {
K序列[idx - 2]..set(K序列[idx - 2]..get());
K序列[idx - 1]..set(K序列[idx - 1]..get());
*K序列[idx - 1]..write().unwrap() = Some(::);
*K序列[idx - 1]..write() = Some(::);
}
:: => {
K序列[idx - 2]..set(K序列[idx - 2]..get());
K序列[idx - 1]..set(K序列[idx - 1]..get());
*K序列[idx - 1]..write().unwrap() = Some(::);
*K序列[idx - 1]..write() = Some(::);
}
:: => {
K序列[idx - 2]..set(K序列[idx - 2]..get());
K序列[idx - 1]..set(K序列[idx - 1]..get());
*K序列[idx - 1]..write().unwrap() = Some(::);
*K序列[idx - 1]..write() = Some(::);
}
:: => {}
}
@@ -573,23 +573,23 @@ impl 缠论K线 {
),
);
}
if *self..read().unwrap() != *other..read().unwrap() {
if *self..read() != *other..read() {
return (
false,
format!(
"缠论K线: [方向] 不等 A={},B={}",
self..read().unwrap(),
other..read().unwrap()
self..read(),
other..read()
),
);
}
if *self..read().unwrap() != *other..read().unwrap() {
if *self..read() != *other..read() {
return (
false,
format!(
"缠论K线: [分型] 不等 A={:?},B={:?}",
self..read().unwrap(),
other..read().unwrap()
self..read(),
other..read()
),
);
}
@@ -636,17 +636,13 @@ impl 缠论K线 {
);
}
// 标的K线 递归
let (eq, msg) = self
.K线
.read()
.unwrap()
.(&other.K线.read().unwrap(), );
let (eq, msg) = self.K线.read().(&other.K线.read(), );
if !eq {
return (false, format!("缠论K线: 标的K线子项异常 >> {msg}"));
}
// 买卖点信息
let a_guard = self..read().unwrap();
let b_guard = other..read().unwrap();
let a_guard = self..read();
let b_guard = other..read();
let a_set: std::collections::HashSet<&String> = a_guard.iter().collect();
let b_set: std::collections::HashSet<&String> = b_guard.iter().collect();
if a_set != b_set {
@@ -654,13 +650,45 @@ impl 缠论K线 {
false,
format!(
"缠论K线: [买卖点信息] 集合不等 A={:?},B={:?}",
self..read().unwrap(),
other..read().unwrap()
self..read(),
other..read()
),
);
}
(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线 {
+2
View File
@@ -30,6 +30,8 @@ pub mod business;
pub mod config;
pub mod indicators;
pub mod kline;
pub mod log;
pub mod signal;
pub mod structure;
pub mod types;
pub mod utils;
+92
View File
@@ -0,0 +1,92 @@
/*
* 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)
pub static LOG_MODE: AtomicU8 = AtomicU8::new(0);
/// 向后兼容:set_log_level 设置此标志
pub static : AtomicBool = AtomicBool::new(false);
pub fn init_from_env() {
if let Ok(val) = std::env::var("CHANLUN_LOG_MODE") {
match val.to_lowercase().as_str() {
"simple" | "on" | "debug" | "1" => {
LOG_MODE.store(1, Ordering::Relaxed);
.store(true, Ordering::Relaxed);
}
"tracing" | "2" => {
LOG_MODE.store(2, Ordering::Relaxed);
.store(true, Ordering::Relaxed);
}
_ => {}
}
}
}
pub fn set_log_mode(mode: u8) {
LOG_MODE.store(mode.min(2), Ordering::Relaxed);
.store(mode > 0, Ordering::Relaxed);
}
pub fn get_log_mode() -> u8 {
LOG_MODE.load(Ordering::Relaxed)
}
#[macro_export]
macro_rules! warn {
($($arg:tt)*) => {
if $crate::log::.load(std::sync::atomic::Ordering::Relaxed) {
match $crate::log::LOG_MODE.load(std::sync::atomic::Ordering::Relaxed) {
2 => tracing::warn!($($arg)*),
_ => eprintln!($($arg)*),
}
}
};
}
#[macro_export]
macro_rules! error {
($($arg:tt)*) => {
if $crate::log::.load(std::sync::atomic::Ordering::Relaxed) {
match $crate::log::LOG_MODE.load(std::sync::atomic::Ordering::Relaxed) {
2 => tracing::error!($($arg)*),
_ => eprintln!($($arg)*),
}
}
};
}
#[macro_export]
macro_rules! info {
($($arg:tt)*) => {
if $crate::log::.load(std::sync::atomic::Ordering::Relaxed) {
match $crate::log::LOG_MODE.load(std::sync::atomic::Ordering::Relaxed) {
2 => tracing::info!($($arg)*),
_ => println!($($arg)*),
}
}
};
}
+2 -3
View File
@@ -80,10 +80,9 @@ fn 测试_读取数据(文件路径: &str) {
let = ::new("".into(), 0, ::default());
.write()
.unwrap()
.(, )
.expect("读取数据文件失败");
let = .read().unwrap();
let = .read();
let = .elapsed();
println!(
"测试_读取数据 耗时 {:.2?} 普K数量 {}",
@@ -160,7 +159,7 @@ fn 测试_周期合成(文件路径: &str) {
// Display stats per period
for &p in &[, * 5, * 5 * 6] {
if let Some() = .(p) {
let = .read().unwrap();
let = .read();
println!(
"周期<{}>: 缠K={}, 分型={}, 笔={}, 线段={}, 中枢={}",
p,
+378
View File
@@ -0,0 +1,378 @@
/*
* 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/czscApache 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, &params)
.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());
}
}
+227
View File
@@ -0,0 +1,227 @@
/*
* 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());
}
}
+175
View File
@@ -0,0 +1,175 @@
/*
* 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());
}
}
+98
View File
@@ -0,0 +1,98 @@
/*
* 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
}
+436
View File
@@ -0,0 +1,436 @@
/*
* 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`。
//!
//! 第三方代码声明:信号函数模式参考 czschttps://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)]
}
}
+31
View File
@@ -0,0 +1,31 @@
/*
* 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]` 注册的函数对应一个子模块。
//!
//! 第三方代码声明:信号函数模式参考 czschttps://github.com/waditu/czsc
//! Apache License 2.0),已适配为 Rust `fn(&观察者, &HashMap<String, Value>) -> Vec<Signal>`。
pub mod demo;
// pub mod youwukuncheng;
+74
View File
@@ -0,0 +1,74 @@
/*
* 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])
}
+64
View File
@@ -0,0 +1,64 @@
/*
* 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(), "持币");
}
}
+52
View File
@@ -0,0 +1,52 @@
/*
* 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
+282
View File
@@ -0,0 +1,282 @@
/*
* 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.
*/
//! 信号注册表 —— 编译期收集 + 运行时动态注册。
//!
//! 第三方代码声明:注册机制参考 czschttps://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()));
}
}
+63
View File
@@ -0,0 +1,63 @@
/*
* 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());
}
+188
View File
@@ -0,0 +1,188 @@
/*
* 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);
}
}
File diff suppressed because it is too large Load Diff
+21 -25
View File
@@ -25,10 +25,10 @@
use crate::kline::chan_kline::K线;
use crate::types::;
use crate::types::;
use crate::warn;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use tracing::warn;
/// 分型模式 — True 时使用构造时缓存值(默认),False 时从 中 缠K 实时读取
pub static : AtomicBool = AtomicBool::new(true);
@@ -75,7 +75,7 @@ impl 分型 {
..load(Ordering::Relaxed),
);
}
let = ..read().unwrap().unwrap_or(::);
let = ..read().unwrap_or(::);
let = ..load(Ordering::Relaxed);
let = ..get();
Self {
@@ -102,7 +102,7 @@ impl 分型 {
if .load(Ordering::Relaxed) {
self.
} else {
self...read().unwrap().unwrap_or(::) // FIXME 错误
self...read().unwrap_or(::) // FIXME 错误
}
}
@@ -157,19 +157,17 @@ impl 分型 {
if let (Some(), Some()) = (&self., &self.) {
if self.() == :: {
if .K线.read().unwrap(). > .K线.read().unwrap(). {
if .K线.read(). > .K线.read(). {
return "";
} else if .K线.read().unwrap(). > self..K线.read().unwrap().
{
} else if .K线.read(). > self..K线.read(). {
return "";
} else {
return "";
}
} else if self.() == :: {
if .K线.read().unwrap(). < .K线.read().unwrap(). {
if .K线.read(). < .K线.read(). {
return "";
} else if .K线.read().unwrap(). < self..K线.read().unwrap().
{
} else if .K线.read(). < self..K线.read(). {
return "";
} else {
return "";
@@ -183,12 +181,12 @@ impl 分型 {
pub fn MACD柱子分型匹配(&self) -> bool {
if let (Some(), Some()) = (&self., &self.) {
if self.() == :: {
let _k = .K线.read().unwrap();
let _k = self..K线.read().unwrap();
let _k = .K线.read().unwrap();
let _m = _k..read().unwrap();
let _m = _k..read().unwrap();
let _m = _k..read().unwrap();
let _k = .K线.read();
let _k = self..K线.read();
let _k = .K线.read();
let _m = _k..read();
let _m = _k..read();
let _m = _k..read();
if let (Some(macd), Some(macd), Some(macd)) =
(_m.macd(), _m.macd(), _m.macd())
{
@@ -196,12 +194,12 @@ impl 分型 {
}
}
if self.() == :: {
let _k = .K线.read().unwrap();
let _k = self..K线.read().unwrap();
let _k = .K线.read().unwrap();
let _m = _k..read().unwrap();
let _m = _k..read().unwrap();
let _m = _k..read().unwrap();
let _k = .K线.read();
let _k = self..K线.read();
let _k = .K线.read();
let _m = _k..read();
let _m = _k..read();
let _m = _k..read();
if let (Some(macd), Some(macd), Some(macd)) =
(_m.macd(), _m.macd(), _m.macd())
{
@@ -214,7 +212,7 @@ impl 分型 {
/// 判断两个分型是否匹配
pub fn (: &Arc<>, : &Arc<>, _模式: &str) -> bool {
Arc::as_ptr() == Arc::as_ptr()
Arc::ptr_eq(, )
}
/// 从缠K序列中获取以指定缠K为中元素的分型
@@ -222,9 +220,7 @@ impl 分型 {
K线序列: &[Arc<K线>],
: &Arc<K线>,
) -> Option<Self> {
let idx = K线序列
.iter()
.position(|k| Arc::as_ptr(k) == Arc::as_ptr())?;
let idx = K线序列.iter().position(|k| Arc::ptr_eq(k, ))?;
let = if idx > 0 {
Some(Arc::clone(&K线序列[idx - 1]))
} else {
+35 -50
View File
@@ -26,9 +26,10 @@ use crate::structure::dash_line::虚线;
use crate::structure::feat_fractal::;
use crate::structure::fractal_obj::;
use crate::types::{, };
use parking_lot::RwLock;
use std::sync::Arc;
use std::sync::atomic::AtomicI64;
use std::sync::atomic::Ordering;
use std::sync::{Arc, RwLock};
/// 线段特征 — 特征序列元素,内部是虚线的集合。
///
@@ -62,7 +63,7 @@ impl Clone for 线段特征 {
fn clone(&self) -> Self {
Self {
: AtomicI64::new(self..load(Ordering::Relaxed)),
: RwLock::new(self..read().unwrap().clone()),
: RwLock::new(self..read().clone()),
线: self.线,
: self..clone(),
}
@@ -82,7 +83,7 @@ impl 线段特征 {
/// 图表标题 — 返回标识字符串
pub fn (&self) -> String {
self..read().unwrap().clone()
self..read().clone()
}
/// 文 — 取特征序列元素中分型特征值最大/最小的文分型
@@ -118,47 +119,31 @@ impl 线段特征 {
/// 武 — 取特征序列元素中分型特征值最大/最小的武分型
/// tiebreaker: later时间戳 wins when特征值 equal (matches Python)
pub fn (&self) -> Arc<> {
if self.线.() {
self.
.iter()
.max_by(|a, b| {
a.
.read()
.unwrap()
.
.partial_cmp(&b..read().unwrap().)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| {
a.
.read()
.unwrap()
.()
.cmp(&b..read().unwrap().())
})
})
.map(|x| x..read().unwrap().clone())
.unwrap_or_else(|| self.[0]..read().unwrap().clone())
let best = if self.线.() {
self..iter().max_by(|a, b| {
let a_武 = a..read();
let b_武 = b..read();
a_武
.
.partial_cmp(&b_武.)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a_武.().cmp(&b_武.()))
})
} else {
self.
.iter()
.max_by(|a, b| {
b.
.read()
.unwrap()
.
.partial_cmp(&a..read().unwrap().)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| {
a.
.read()
.unwrap()
.()
.cmp(&b..read().unwrap().())
})
})
.map(|x| x..read().unwrap().clone())
.unwrap_or_else(|| self.[0]..read().unwrap().clone())
}
self..iter().max_by(|a, b| {
let a_武 = a..read();
let b_武 = b..read();
b_武
.
.partial_cmp(&a_武.)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a_武.().cmp(&b_武.()))
})
};
best.map_or_else(
|| self.[0]..read().clone(),
|x| x..read().clone(),
)
}
/// 高 — 文和武中分型特征值的较大者
@@ -197,7 +182,7 @@ impl 线段特征 {
if let Some(pos) = self
.
.iter()
.position(|x| Arc::as_ptr(x) == Arc::as_ptr(线))
.position(|x| Arc::ptr_eq(x, 线))
{
self..remove(pos);
Ok(())
@@ -256,7 +241,7 @@ impl 线段特征 {
.unwrap();
let fake = 线::(
Arc::clone(&线.),
线..read().unwrap().clone(),
线..read().clone(),
false,
);
.pop();
@@ -330,13 +315,13 @@ impl 线段特征 {
),
);
}
if *self..read().unwrap() != *other..read().unwrap() {
if *self..read() != *other..read() {
return (
false,
format!(
"线段特征: [标识] 不等 A={},B={}",
self..read().unwrap(),
other..read().unwrap()
self..read(),
other..read()
),
);
}
@@ -381,12 +366,12 @@ impl crate::types::fractal::有高低 for 线段特征 {
impl std::fmt::Display for 线 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self..is_empty() {
write!(f, "{}<{}, 空>", self..read().unwrap(), self.线)
write!(f, "{}<{}, 空>", self..read(), self.线)
} else {
write!(
f,
"{}<{}, {}, {}, {}>",
self..read().unwrap(),
self..read(),
self.线,
self.(),
self.(),
+28
View File
@@ -122,6 +122,34 @@ impl 相对方向 {
, , ,
);
}
/// 从可选方向序列中随机选取指定数量(与 chan.py 对齐)
pub fn (
: usize,
: &[],
: bool,
) -> Vec<> {
if == 0 || .is_empty() {
return Vec::new();
}
if ! && > .len() {
panic!("数量超过可选方向数");
}
let mut result = Vec::with_capacity();
if {
for _ in 0.. {
let idx = fastrand::usize(...len());
result.push([idx]);
}
} else {
let mut indices: Vec<usize> = (0...len()).collect();
fastrand::shuffle(&mut indices);
for &idx in indices.iter().take() {
result.push([idx]);
}
}
result
}
}
impl std::fmt::Display for {
+1 -1
View File
@@ -22,8 +22,8 @@
* SOFTWARE.
*/
use crate::warn;
use serde::{Deserialize, Serialize};
use tracing::warn;
/// 分型结构 —— 三根K线构成的结构形态
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+27
View File
@@ -0,0 +1,27 @@
#!/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
@@ -0,0 +1,467 @@
# 子项目1 信号注册框架 实现计划
> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
**目标:** 用 `#[signal]` proc-macro + `inventory` 编译期注册表替代 Python 的 `import_by_name` 动态导入和 `SignalsParser` docstring 解析,提供「信号名 → 函数指针」O(1) 查表。
**架构:** 新建独立 proc-macro crate `chanlun-signal-macros``#[signal(name, template)]` 属性宏,emit `crate::signal::registry::` 路径);核心 crate `chanlun` 新增 `signal/registry.rs`(描述符类型 + `inventory` 归并 + 查询 API),并依赖宏 crate + `inventory`。信号函数签名 `fn(&观察者, &HashMap<String, Value>) -> Vec<Signal>`,无 TaCache(核心层 K线已挂指标)。
**技术栈:** Rustedition 2024 / 宏 crate 2021)、`syn` 2 + `quote` + `proc-macro2``inventory` 0.3、`serde_json`
**设计文档:** `docs/superpowers/specs/2026-06-22-signal-registry-framework-design.md`
---
## 文件结构
| 文件 | 职责 |
|---|---|
| `chanlun-signal-macros/Cargo.toml` | proc-macro crate 清单(`proc-macro = true` + syn/quote/proc-macro2 |
| `chanlun-signal-macros/src/lib.rs` | `#[signal(name, template)]` 属性宏 |
| `chanlun/Cargo.toml` | 新增 `inventory` + path 依赖 `chanlun-signal-macros` |
| `chanlun/src/signal/registry.rs` | `SignalFn`/`SignalDescriptor`/`SignalMeta`/`归并`/`SIGNAL_REGISTRY`/查询 API + 探针单测 |
| `chanlun/src/signal/mod.rs` | 增 `pub mod registry;` |
| `chanlun/tests/test_signal_registry.rs` | 端到端集成测试:`#[signal]` 贴探针函数 → 注册表命中(在 chanlun crate 内,因宏 emit `crate::` 路径) |
**测试归属说明**`#[signal]` 宏 emit `crate::signal::registry::SignalDescriptor`,仅在 `chanlun` crate 内解析得了,故**宏的端到端测试放 `chanlun/tests/`,不放宏 crate**(放宏 crate 会循环依赖 chanlun)。宏 crate 自身只验证「能编译」。
---
## 任务 0:脚手架——proc-macro crate + 依赖接线
**文件:**
- 创建:`chanlun-signal-macros/Cargo.toml``chanlun-signal-macros/src/lib.rs`
- 修改:`chanlun/Cargo.toml`
- [ ] **步骤 1:创建宏 crate 清单**
创建 `chanlun-signal-macros/Cargo.toml`
```toml
[package]
name = "chanlun-signal-macros"
version = "0.1.0"
edition = "2021"
license = "MIT"
description = "chanlun 信号注册 proc-macro#[signal]"
[lib]
proc-macro = true
[dependencies]
syn = { version = "2", features = ["full"] }
quote = "1"
proc-macro2 = "1"
```
- [ ] **步骤 2:创建宏 crate 占位实现**
创建 `chanlun-signal-macros/src/lib.rs`(占位,任务 2 填充真实逻辑):
```rust
//! chanlun 信号注册 proc-macro。
//!
//! 第三方代码声明:`#[signal]` 注册机制参考 czsc 项目
//! https://github.com/waditu/czscApache License 2.0),已简化适配。
use proc_macro::TokenStream;
/// 占位——任务 2 实现真实的 #[signal] 属性宏。
#[proc_macro_attribute]
pub fn signal(_attr: TokenStream, item: TokenStream) -> TokenStream {
item
}
```
- [ ] **步骤 3chanlun 接线依赖**
修改 `chanlun/Cargo.toml``[dependencies]`,追加两行(放在 `sha2 = "0.10"` 之后):
```toml
inventory = "0.3"
chanlun-signal-macros = { path = "../chanlun-signal-macros" }
```
- [ ] **步骤 4:验证两个 crate 都能构建**
运行:`cd /home/moscow/chanlun.rs/chanlun-signal-macros && cargo build`
预期:编译通过(占位宏)。
运行:`cd /home/moscow/chanlun.rs/chanlun && cargo build`
预期:编译通过(新增依赖,尚未使用,unused-dep 不会报错)。
- [ ] **步骤 5Commit**
```bash
cd /home/moscow/chanlun.rs
git add chanlun-signal-macros chanlun/Cargo.toml
git commit -m "feat(signal-registry): 脚手架 — proc-macro crate + inventory 依赖"
```
---
## 任务 1registry.rs —— 描述符类型 + 归并 + 查询 API
**文件:**
- 创建:`chanlun/src/signal/registry.rs`
- 修改:`chanlun/src/signal/mod.rs`
- [ ] **步骤 1mod.rs 注册子模块**
修改 `chanlun/src/signal/mod.rs`,在 `pub mod signal;`(第 13 行)之后加一行:
```rust
pub mod registry;
```
- [ ] **步骤 2:编写 registry.rs(含 cargo 单测)**
创建 `chanlun/src/signal/registry.rs`(一字不差):
```rust
//! 信号注册表 —— 编译期收集 `#[signal]` 注册的信号函数,运行时按名查表。
//!
//! 第三方代码声明:注册机制参考 czschttps://github.com/waditu/czsc
//! Apache License 2.0),已简化适配(无 category / TaCache)。
use crate::business::observer::观察者;
use crate::signal::Signal;
use serde_json::Value;
use std::collections::HashMap;
use std::sync::LazyLock;
/// 信号函数签名 —— 读观察者状态(含 K线已挂指标)+ 参数 → 信号列表。无 TaCache。
pub type SignalFn = fn(&观察者, &HashMap<String, Value>) -> Vec<Signal>;
/// 信号描述符(编译期元数据,由 `#[signal]` 宏生成、`inventory` 收集)。
#[derive(Clone, Copy)]
pub struct SignalDescriptor {
/// 信号函数名,如 "youwukuncheng_中枢第三买卖点_V230602"
pub name: &'static str,
/// 参数模板,如 "{freq}_D1MO{max_overlap}_中枢第三买卖点V230602"
pub template: &'static str,
/// 函数指针
pub func: SignalFn,
}
inventory::collect!(SignalDescriptor);
/// 运行时信号元信息。
pub struct SignalMeta {
pub func: SignalFn,
pub template: &'static str,
}
/// 归并描述符为注册表;重名返回 Err(纯函数,便于单测)。
fn 归并(
descs: impl Iterator<Item = SignalDescriptor>,
) -> Result<HashMap<&'static str, SignalMeta>, String> {
let mut m: HashMap<&'static str, SignalMeta> = HashMap::new();
for d in descs {
if m
.insert(d.name, SignalMeta { func: d.func, template: d.template })
.is_some()
{
return Err(format!("信号重名:{}", d.name));
}
}
Ok(m)
}
/// 全局注册表视图(由 inventory 归并;重名 panicfail-fast)。
pub static SIGNAL_REGISTRY: LazyLock<HashMap<&'static str, SignalMeta>> = LazyLock::new(|| {
归并(inventory::iter::<SignalDescriptor>.into_iter().copied())
.unwrap_or_else(|e| panic!("{e}"))
});
/// 按名查信号元信息。
pub fn get_signal(name: &str) -> Option<&'static SignalMeta> {
SIGNAL_REGISTRY.get(name)
}
/// 按名查参数模板。
pub fn get_template(name: &str) -> Option<&'static str> {
SIGNAL_REGISTRY.get(name).map(|m| m.template)
}
/// 列出所有已注册信号名(排序)。
pub fn list_signal_names() -> Vec<&'static str> {
let mut v: Vec<_> = SIGNAL_REGISTRY.keys().copied().collect();
v.sort();
v
}
#[cfg(test)]
mod tests {
use super::*;
/// 探针信号函数(最小签名实现,仅供测试归并/查表)。
fn __probe(_obs: &观察者, _p: &HashMap<String, Value>) -> Vec<Signal> {
Vec::new()
}
fn 描述符(name: &'static str) -> SignalDescriptor {
SignalDescriptor { name, template: "{freq}_D1_probe", func: __probe }
}
#[test]
fn test_归并_正常() {
let m = 归并([描述符("a_V000001"), 描述符("b_V000001")].into_iter()).unwrap();
assert_eq!(m.len(), 2);
assert!(m.contains_key("a_V000001"));
assert_eq!(m["a_V000001"].template, "{freq}_D1_probe");
}
#[test]
fn test_归并_重名_返回Err() {
let r = 归并([描述符("dup_V000001"), 描述符("dup_V000001")].into_iter());
assert!(r.is_err());
assert!(r.unwrap_err().contains("信号重名"));
}
}
/// 测试用:通过 inventory 提交一个探针描述符,验证全局注册表能收到。
#[cfg(test)]
fn __probe_for_inventory(_obs: &观察者, _p: &HashMap<String, Value>) -> Vec<Signal> {
Vec::new()
}
#[cfg(test)]
inventory::submit! {
SignalDescriptor {
name: "__probe_inventory_V000000",
template: "{freq}_D1_probe_inventory",
func: __probe_for_inventory as SignalFn,
}
}
#[cfg(test)]
mod inventory_tests {
use super::*;
#[test]
fn test_全局注册表收到inventory探针() {
assert!(get_signal("__probe_inventory_V000000").is_some());
assert_eq!(
get_template("__probe_inventory_V000000"),
Some("{freq}_D1_probe_inventory")
);
assert!(list_signal_names().contains(&"__probe_inventory_V000000"));
}
}
```
- [ ] **步骤 3:运行测试**
运行:`cd /home/moscow/chanlun.rs/chanlun && cargo test signal::registry`
预期:3 个测试全 PASS`test_归并_正常``test_归并_重名_返回Err``test_全局注册表收到inventory探针`)。
> 注:若 `inventory::iter::<SignalDescriptor>.into_iter().copied()` 因 inventory 0.3 API 细节编译报错,改为 `inventory::iter::<SignalDescriptor>().copied()``inventory::iter::<SignalDescriptor> {}`(参考 `/home/moscow/czsc/crates/czsc-signals/src/registry.rs:136``inventory::iter::<...>.into_iter().copied().collect()` 写法)。
- [ ] **步骤 4Commit**
```bash
cd /home/moscow/chanlun.rs
git add chanlun/src/signal/registry.rs chanlun/src/signal/mod.rs
git commit -m "feat(signal-registry): registry.rs — 描述符/归并/查询 API + 探针测试"
```
---
## 任务 2`#[signal]` 属性宏
**文件:**
- 修改:`chanlun-signal-macros/src/lib.rs`
- [ ] **步骤 1:实现 #[signal] 宏**
`chanlun-signal-macros/src/lib.rs` 全部内容替换为(一字不差):
```rust
//! chanlun 信号注册 proc-macro。
//!
//! 第三方代码声明:`#[signal]` 注册机制参考 czsc 项目
//! https://github.com/waditu/czscApache License 2.0),已简化适配
//! (无 category / TaCache,签名固定为 fn(&观察者, &HashMap<String, Value>) -> Vec<Signal>)。
use proc_macro::TokenStream;
use quote::quote;
use syn::parse::Parser;
use syn::punctuated::Punctuated;
use syn::{Expr, ExprLit, ItemFn, Lit, Meta, Token};
/// `#[signal(name = "foo_V230101", template = "{freq}_D1_foo")]`
///
/// 校验:函数名含 `_V<数字>``name` 与函数名一致;`name`/`template` 非空。
/// 生成:一个 `static` SignalDescriptor + `inventory::submit!`,路径用 `crate::signal::registry::`
#[proc_macro_attribute]
pub fn signal(attr: TokenStream, item: TokenStream) -> TokenStream {
let parser = Punctuated::<Meta, Token![,]>::parse_terminated;
let metas = match parser.parse(attr) {
Ok(m) => m,
Err(e) => return e.to_compile_error().into(),
};
let mut name: Option<String> = None;
let mut template: Option<String> = None;
for m in metas {
if let Meta::NameValue(nv) = m
&& let Some(ident) = nv.path.get_ident()
&& let Expr::Lit(ExprLit { lit: Lit::Str(v), .. }) = nv.value
{
match ident.to_string().as_str() {
"name" => name = Some(v.value()),
"template" => template = Some(v.value()),
_ => {}
}
}
}
let f: ItemFn = match syn::parse(item) {
Ok(v) => v,
Err(e) => return e.to_compile_error().into(),
};
let name = name.unwrap_or_default();
let template = template.unwrap_or_default();
let fn_ident = &f.sig.ident;
let fn_name = fn_ident.to_string();
let mut errors = Vec::new();
if name.is_empty() || template.is_empty() {
errors.push(quote! { compile_error!("#[signal] name/template 不能为空"); });
}
if name != fn_name {
errors.push(quote! { compile_error!("#[signal] name 必须与函数名一致"); });
}
// 函数名须含 _V<数字>
let 有版本 = fn_name
.rsplit_once("_V")
.map(|(_, v)| !v.is_empty() && v.chars().all(|c| c.is_ascii_digit()))
.unwrap_or(false);
if !有版本 {
errors.push(quote! { compile_error!("#[signal] 函数名必须含 _V<版本号>,如 foo_V230101"); });
}
if !errors.is_empty() {
let errs = errors.into_iter();
return quote! { #(#errs)* }.into();
}
let descriptor_ident = syn::Ident::new(
&format!("__SIG_DESC_{}", fn_name).to_uppercase(),
fn_ident.span(),
);
let expanded = quote! {
#f
#[allow(non_upper_case_globals)]
static #descriptor_ident: crate::signal::registry::SignalDescriptor =
crate::signal::registry::SignalDescriptor {
name: #name,
template: #template,
func: #fn_ident as crate::signal::registry::SignalFn,
};
inventory::submit! { #descriptor_ident }
};
expanded.into()
}
```
- [ ] **步骤 2:验证宏 crate 编译**
运行:`cd /home/moscow/chanlun.rs/chanlun-signal-macros && cargo build`
预期:编译通过。
- [ ] **步骤 3Commit**
```bash
cd /home/moscow/chanlun.rs
git add chanlun-signal-macros/src/lib.rs
git commit -m "feat(signal-registry): #[signal] 属性宏 — 校验+生成描述符+提交"
```
---
## 任务 3:端到端集成测试(chanlun 内用 #[signal]
**文件:**
- 创建:`chanlun/tests/test_signal_registry.rs`
- [ ] **步骤 1:编写集成测试**
创建 `chanlun/tests/test_signal_registry.rs`(一字不差)。它在 chanlun crate 内用 `#[signal]` 贴一个探针函数,验证宏 + 注册表端到端:
```rust
//! 端到端:#[signal] 宏 + inventory 注册表协同。
//! 放在 chanlun crate 内,因 #[signal] emit 的是 `crate::signal::registry::` 路径。
use std::collections::HashMap;
use chanlun::business::observer::观察者;
use chanlun::signal::registry::{get_signal, get_template, list_signal_names};
use chanlun::signal::Signal;
use chanlun_signal_macros::signal;
use serde_json::Value;
/// 探针信号函数:贴 #[signal] 后应被自动注册。
#[signal(
name = "test_probe_signal_V230101",
template = "{freq}_D1MO{max_overlap}_test_probe_signalV230101"
)]
fn test_probe_signal_V230101(_obs: &观察者, _params: &HashMap<String, Value>) -> Vec<Signal> {
Vec::new()
}
#[test]
fn test_signal_宏自动注册到全局表() {
// get_signal 命中
assert!(
get_signal("test_probe_signal_V230101").is_some(),
"#[signal] 应把探针函数注册进 SIGNAL_REGISTRY"
);
// 模板正确
assert_eq!(
get_template("test_probe_signal_V230101"),
Some("{freq}_D1MO{max_overlap}_test_probe_signalV230101")
);
// 列表含它
assert!(list_signal_names().contains(&"test_probe_signal_V230101"));
}
#[test]
fn test_未注册信号返回None() {
assert!(get_signal("不存在的信号_V999999").is_none());
}
```
- [ ] **步骤 2:运行集成测试**
运行:`cd /home/moscow/chanlun.rs/chanlun && cargo test --test test_signal_registry`
预期:2 个测试全 PASS。
> 注:本测试与 registry.rs 的 `#[cfg(test)]` inventory 探针不冲突——集成测试是独立编译单元,`__probe_inventory_V000000` 仅在 lib 单测时提交,集成测试时只有 `test_probe_signal_V230101`
- [ ] **步骤 3:跑全量 signal 测试确认无回归**
运行:`cd /home/moscow/chanlun.rs/chanlun && cargo test signal`
预期:原 23 个原语单测 + registry 3 个 + 集成 2 个,全 PASS。
- [ ] **步骤 4Commit**
```bash
cd /home/moscow/chanlun.rs
git add chanlun/tests/test_signal_registry.rs
git commit -m "test(signal-registry): 端到端——#[signal] 宏自动注册 + 查表"
```
---
## 自检结论
- **规格覆盖**:设计 §4 crate 结构 → 任务 0;§5 描述符/注册表/查询 API → 任务 1;§6 `#[signal]` 宏 → 任务 2;§7 测试(归并重名/inventory 探针/宏端到端)→ 任务 1(单测)+ 任务 3(集成);§9 错误处理(编译期 compile_error、启动期重名 panic、运行期 None)→ 任务 2compile_error+ 任务 1(归并 Err→panic / get_signal None)。全覆盖。
- **类型一致**`SignalFn`/`SignalDescriptor`/`SignalMeta`/`归并`/`get_signal`/`get_template`/`list_signal_names` 在 registry.rs 定义,任务 2 宏 emit `crate::signal::registry::{SignalDescriptor, SignalFn}`、任务 3 集成测试 import `chanlun::signal::registry::{get_signal, get_template, list_signal_names}`,命名贯穿一致。
- **占位符**:任务 0 步骤 2 的占位宏是**有意的脚手架**(任务 2 替换为真实实现),非计划缺陷;其余步骤均含完整可编译代码。
- **风险提示**:任务 1 步骤 3 标注了 `inventory::iter` API 细节的 fallback(参考 czsc registry.rs 实际写法)。
@@ -0,0 +1,79 @@
# 子项目 4 Position.update 状态机迁移到 Rust 实现计划
> 目标:将 Position.update 状态机(~135 行 Python)从 Python 子类迁移到 Rust 核心。
**设计文档:** `docs/superpowers/specs/2026-06-23-position-update-state-machine-design.md`
---
## 任务 0:扩展 Rust 核心 Position
**文件:** `chanlun/src/signal/position.rs`
- [x] 新增类型:`操作记录``持仓记录``开平配对``最近事件`
- [x] Position 结构体新增 7 个状态字段(pos, pos_changed, operates, holds, last_event, last_lo_dt, last_so_dt, end_dt
- [x] `新建()` 构造函数适配(状态字段初始化为默认值)
- [x] 实现 `push_operate()` 内部辅助方法
- [x] 实现 `update(&mut self, dt, price, bid, signals) -> Result<(), 缺键错误>` — 核心状态机
- [x] 实现 `pairs() -> Vec<开平配对>` — 开平配对计算
- [x] 实现 `dump_config()` / `load_config()` — 序列化辅助
- [x] 内部辅助函数:`同一交易日``间隔检查``允许操作`
- [x] Rust 单元测试(28 用例)
## 任务 1:更新 PyO3 绑定
**文件:** `chanlun-py/src/signal_py.rs`
- [x] 新增 helper`核心op转pyop()``时间戳转datetime()`
- [x] 新增状态 getter`pos`, `pos_changed`, `operates`, `holds`, `pairs`
- [x] 实现 `update(PyDict)` — 提取 dt/close/bid + 转换 信号字典 + 调用核心
- [x] dt 类型兼容:支持 datetime / int / float
- [x] dump(with_data) — 支持附带 pairs/holds
- [x] load() 静态方法
- [x] 新增 `取事件列表` 辅助函数
- [x] 更新 `__repr__` 包含 pos
## 任务 2:更新 Python 子类
**文件:** `chanlun-py/chanlun/chan_external.py`
- [x] `__init__` 简化为 `pass`(状态由 Rust 初始化)
- [x] 删除 `update()`Rust 提供)
- [x] 删除 `pairs` propertyRust 提供)
- [x] `dump()` 委托给 Rust `super().dump(with_data=...)`
- [x] `load()` 使用 `cls(...)` 构造(保持子类类型)
- [x] 保留 `get_signals_config()`
## 任务 3:测试
**文件:**
- `chanlun/src/signal/position.rs` — Rust 单元测试(28 用例)
- `chanlun-py/tests/test_position_update.py` — Python 集成测试(24 用例)
- `chanlun-py/tests/test_signal_primitives.py` — 已有测试更新(4 position 用例)
- [x] 基础开多/开空/平多/平空
- [x] 间隔限制
- [x] 止损(多头/空头)
- [x] 超时
- [x] 时间倒退容错
- [x] 空事件列表容错
- [x] 无匹配事件容错
- [x] 缺键错误
- [x] T0 模式
- [x] pairs 盈亏计算(多头/空头)
- [x] pairs 持仓天数
- [x] dump/load with/without data
- [x] dt 类型兼容(datetime / int / float
## 任务 4:文档
- [x] 创建设计文档 `docs/superpowers/specs/2026-06-23-position-update-state-machine-design.md`
- [x] 创建实现计划 `docs/superpowers/plans/2026-06-23-position-update-state-machine.md`
- [x] 更新 `CLAUDE.md` 子项目表
## 自检结论
- **规格覆盖**:设计 §3 新增类型 → 任务 0;§4 update 算法 → 任务 0;§5 文件结构 → 任务 0-3
- **类型一致**`update()` 参数使用已有 `信号字典` 类型;`Operate` 枚举已有 Rust 版
- **向后兼容**Python 子类保留;update/pairs/operates/holds API 不变;dt 支持三种输入格式
- **测试覆盖**Rust 28 用例 + Python 24 用例 + 已有 4 用例更新
@@ -0,0 +1,678 @@
# 信号计算器 Rust 迁移 — 设计决策 + 实现计划
> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
**目标:** 将 `信号计算器`(Python 信号编排器)替换为混合架构:Rust `SignalEngine` 为主,Python fallback 为辅,逐步完成最终迁移。
**架构:** 增强 Rust `SignalEngine` 使其返回完整的 `信号字典`(信号 + OHLCV 行情);创建 `SignalOrchestrator` 支持 Rust 注册表优先 + Python `import_by_name` 回退;`SignalsParser` 暂留 Python。
**技术栈:** Rust edition 2024、PyO3 0.28、`serde_json::Value``parking_lot::RwLock``inventory`
**设计文档:** `docs/superpowers/specs/2026-06-23-signal-calculator-migration-design.md`
---
## 0. 决策分析
### 现状
| 组件 | 语言 | 职责 |
|------|------|------|
| `SignalEngine` | ✅ Rust | 按名查找已注册信号函数 → 执行 → 合并结果 |
| `信号计算器` | Python | 同上 + OHLCV 行情提取 + `SignalsParser` 集成 |
| `SignalsParser` | Python | 解析信号函数文档字符串 → 生成配置字典 |
| `get_signals_config` | Python | 将信号字符串列表 → 配置字典列表(用 `SignalsParser` |
两个计算引擎**并行存在**,完全独立。`strategies.py` 使用 Python `信号计算器`。Rust `SignalEngine` 没有被任何生产代码使用。
### 关键差异
| 能力 | Python `信号计算器` | Rust `SignalEngine` |
|------|---------------------|---------------------|
| 信号函数解析 | 运行时 `import_by_name()` | 编译时 `#[signal]` + `inventory` |
| OHLCV 行情 | 提取到 `self.行情` | ❌ 不处理 |
| 观察者访问 | 预提取 `{freq: Observer}` 字典 | 每次调用时通过 `&立体分析器` 查找 |
| 错误处理 | 每个信号函数的 `except Exception` | `tracing::warn!`,继续 |
| freq 验证 | 检查是否在分析器周期组中 | ❌ 不验证 |
| 信号字符串→配置 | `从信号列表提取配置()` | ❌ 不存在(Python `SignalsParser` 处理) |
### 建议:混合迁移(3 阶段)
**阶段 A:增强 Rust SignalEngine。** 添加 OHLCV 行情提取 + freq 验证 + Python `call_signal` 集成。
**阶段 B:创建混合编排器 `SignalOrchestrator`。** 替代 Python `信号计算器`Rust 注册表优先,Python `import_by_name` 回退。
**阶段 C:废弃 Python 并行路径。** 所有信号函数移植到 Rust 后,移除 `import_by_name` 回退和 `SignalsParser`
| 阶段 | 交付物 | 向后兼容 |
|------|--------|----------|
| A | `SignalEngine::更新_完整()``{signals, market_data}` | ✅ 不影响现有路径 |
| B | `SignalOrchestrator`Rust 优先 + Python fallback | ✅ `strategies.py` 切换到新类 |
| C | 移除 Python `信号计算器``SignalsParser` | ⚠️ 需所有信号函数先移植到 Rust |
---
## 文件结构
```
chanlun/src/signal/engine.rs ← 增强:更新_完整() 返回 {signals, market}
chanlun-py/src/signal_engine_py.rs ← 增强:SignalEnginePy 暴露 更新_完整()
chanlun-py/chanlun/signal_orchestrator.py ← 新建:混合编排器
chanlun-py/chanlun/chan_external.py ← 废弃:信号计算器(最终移除)
strategies.py ← 切换:使用 SignalOrchestrator
main.py ← 修复:损坏的 信号计算器 调用点
chanlun-py/tests/test_signal_orchestrator.py ← 新建:编排器测试
```
---
## 阶段 A:增强 Rust SignalEngine(信号 + 行情)
### 任务 A1SignalEngine 增加 `更新_完整()` 方法
**文件:** `chanlun/src/signal/engine.rs`
- [ ] **步骤 1:添加返回类型**
`SignalEngine``更新_含分数()` 之后添加新结构体:
```rust
/// 完整更新结果:信号字典 + 基础周期行情数据。
#[derive(Debug, Clone)]
pub struct 完整更新结果 {
/// 信号 key → value 映射
pub signals: HashMap<String, String>,
/// 基础周期最后一根 K 线的 OHLCV 数据
pub market: Option<MarketData>,
}
#[derive(Debug, Clone)]
pub struct MarketData {
pub symbol: String,
pub dt: i64, // Unix 秒
pub id: i64,
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub vol: f64,
}
```
- [ ] **步骤 2:实现 `更新_完整()`**
```rust
/// 运行信号计算并附带基础周期行情。
/// `base_freq` 为分析器的第一个周期(最小周期)。
pub fn 更新_完整(&self, analyzer: &立体分析器) -> 完整更新结果 {
let signals = self.更新(analyzer);
let base_freq = analyzer.周期组.first().copied().unwrap_or(0);
let market = analyzer._单体分析器.get(&base_freq).and_then(|obs| {
let obs = obs.read();
obs.普通K线序列.last().map(|k| {
MarketData {
symbol: obs.符号.clone(),
dt: k.时间戳,
id: k.序号.load(std::sync::atomic::Ordering::Relaxed),
open: k.开盘价,
high: k.最高价,
low: k.最低价,
close: k.收盘价,
vol: k.成交量,
}
})
});
完整更新结果 { signals, market }
}
```
- [ ] **步骤 3:构建验证**
```bash
cd chanlun && cargo build
```
预期:编译通过。
- [ ] **步骤 4Commit**
```bash
git add chanlun/src/signal/engine.rs
git commit -m "feat(signal): SignalEngine.更新_完整() — 信号 + 基础周期行情
Co-Authored-By: Claude <noreply@anthropic.com>"
```
---
### 任务 A2PyO3 绑定增强
**文件:** `chanlun-py/src/signal_engine_py.rs`
- [ ] **步骤 1:暴露 `更新_完整()`**
`SignalEnginePy``#[pymethods]` 块中添加:
```rust
/// 更新信号并返回完整结果(信号 + 行情)。
/// 返回 dict: {"signals": {...}, "market": {...}}
fn 更新_完整<'py>(&self, py: Python<'py>, analyzer: &立体分析器Py) -> PyResult<Bound<'py, PyDict>> {
let result = self.inner.更新_完整(&analyzer.inner);
let d = PyDict::new(py);
// signals
let signals_dict = PyDict::new(py);
for (k, v) in &result.signals {
signals_dict.set_item(k, v)?;
}
d.set_item("signals", signals_dict)?;
// market
if let Some(m) = &result.market {
let md = PyDict::new(py);
md.set_item("symbol", &m.symbol)?;
// Convert i64 to Python datetime
let dt = 时间戳转datetime(py, m.dt)?;
md.set_item("dt", dt)?;
md.set_item("id", m.id)?;
md.set_item("open", m.open)?;
md.set_item("high", m.high)?;
md.set_item("low", m.low)?;
md.set_item("close", m.close)?;
md.set_item("vol", m.vol)?;
d.set_item("market", md)?;
} else {
d.set_item("market", py.None())?;
}
Ok(d)
}
```
> 注意:`时间戳转datetime` 已在 `signal_py.rs` 中定义。需要将其改为 `pub(crate)` 可见性,或在 `signal_engine_py.rs` 中重复定义。
- [ ] **步骤 2:将 `时间戳转datetime` 改为 `pub(crate)`**
`signal_py.rs` 中:
```rust
// 将 fn 改为 pub(crate)
pub(crate) fn 时间戳转datetime(py: Python<'_>, ts: i64) -> PyResult<Py<PyAny>> {
```
- [ ] **步骤 3:添加 `freq 验证` 辅助函数**
`signal_engine_py.rs``SignalEnginePy::new()` 中添加 freq 验证(匹配 Python `信号计算器` setter 的行为):
```rust
// 在 new() 中,转换配置后:
// 验证所有 freq 已由调用方提供(不在构造时验证——没有分析器引用)
// 频率验证推迟到 更新() 调用时(与 Rust 核心行为一致)
```
不改变构造函数——保持最小侵入。频率验证由调用方负责(`SignalOrchestrator`)。
- [ ] **步骤 4:构建验证**
```bash
cd chanlun-py && cargo build
```
预期:编译通过。
- [ ] **步骤 5Commit**
```bash
git add chanlun-py/src/signal_engine_py.rs chanlun-py/src/signal_py.rs
git commit -m "feat(signal-py): SignalEnginePy.更新_完整() + 时间戳转datetime 公开
Co-Authored-By: Claude <noreply@anthropic.com>"
```
---
## 阶段 B:混合编排器 SignalOrchestrator
### 任务 B1:创建 `signal_orchestrator.py`
**文件:** 创建 `chanlun-py/chanlun/signal_orchestrator.py`
这是核心新文件。编排器:
1. 构造时接受 `立体分析器` + 信号配置 + 信号模块
2. 对每个配置,先尝试 Rust `call_signal()` 查找(通过 `list_signals()`
3. 如果信号名在 Rust 注册表中:使用 `SignalEngine` 批量执行
4. 如果不在:使用 Python `import_by_name` 回退
5. 合并所有结果,附加 OHLCV 行情
- [ ] **步骤 1:创建文件框架**
```python
"""信号编排器 — Rust 优先 + Python 回退的混合信号计算。
替代 chan_external.信号计算器,逐步迁移到全 Rust 路径。
使用方式::
分析器 = 立体分析器("btcusd", [300, 900, 3600], 配置)
编排器 = SignalOrchestrator(分析器, 信号配置=[...], 信号模块="chanlun.signals")
for k in k线列表:
分析器.投喂K线(k)
编排器.更新()
print(编排器.信号字典)
"""
import sys
from collections import OrderedDict
from typing import Any, Callable, Dict, List, Optional
from loguru import logger
from chanlun.chan import 观察者, 立体分析器
from chanlun._chanlun import (
SignalEngine as _RustSignalEngine,
call_signal as _rust_call_signal,
list_signals as _rust_list_signals,
)
class SignalOrchestrator:
"""混合信号编排器:Rust 注册表优先,Python import_by_name 回退。"""
def __init__(
self,
分析器: 立体分析器,
信号配置: Optional[List[Dict]] = None,
信号模块: str = "chanlun.signals",
):
self._分析器 = 分析器
self._观察者字典 = {p: 分析器._单体分析器[p] for p in 分析器.周期组}
self._基础周期 = 分析器.周期组[0]
self._信号模块 = 信号模块
# 初始化 Rust 引擎(用于已注册的 Rust 信号)
self._rust_engine = _RustSignalEngine(信号配置=信号配置 or [])
self._rust_engine.自动挂载指标(分析器)
# 分类配置:Rust 注册 vs Python 回退
self._rust_configs: List[Dict] = []
self._python_configs: List[Dict] = []
self._python_func_cache: Dict[str, Callable] = {}
# 结果容器
self.信号: Dict[str, str] = {}
self.行情: Dict[str, Any] = {}
# 初始设置
self.信号配置 = 信号配置 or []
# ... 其余方法见下面步骤
```
- [ ] **步骤 2:实现配置分类**
```python
@property
def 信号配置(self) -> List[Dict]:
return self._信号配置
@信号配置.setter
def 信号配置(self, value: List[Dict]):
可用周期 = set(self._分析器.周期组)
rust_names = set(_rust_list_signals())
self._rust_configs = []
self._python_configs = []
for c in self._去重配置(value):
freq = c.get("freq")
if freq is not None:
周期秒 = int(freq)
if 周期秒 not in 可用周期:
raise ValueError(
f"信号配置 freq={freq}({周期秒}s) 不在分析器周期组 {sorted(可用周期)} 中"
)
name = c.get("name", "")
if name in rust_names:
self._rust_configs.append(c)
else:
self._python_configs.append(c)
self._信号配置 = value
self._预加载Python信号函数()
```
- [ ] **步骤 3:实现更新循环**
```python
def 更新(self):
"""执行所有信号计算。Rust 优先(批量),Python 回退(逐个)。"""
self.信号.clear()
self.行情.clear()
# 1. Rust 批量执行
if self._rust_configs:
result = self._rust_engine.更新_完整(self._分析器)
if result.get("signals"):
for k, v in result["signals"].items():
if v != "任意_任意_任意_0":
self.信号[k] = v
if result.get("market"):
self.行情.update(result["market"])
# 2. Python 回退(逐个执行)
for config in self._python_configs:
try:
result = self._执行Python信号函数(config)
if result:
for k, v in result.items():
if v != "任意_任意_任意_0":
self.信号[k] = v
except Exception:
logger.exception(f"Python 信号函数执行失败: {config.get('name')}")
# 3. 补充基础周期行情(如果 Rust 引擎未提供)
if not self.行情:
self._提取行情()
```
- [ ] **步骤 4:实现 Python 信号函数执行(移植自 chan_external.py**
```python
def _执行Python信号函数(self, config: Dict) -> Optional[OrderedDict]:
"""执行单个 Python 信号函数(移植自 信号计算器._执行信号函数)。"""
import traceback
param = dict(config)
sig_name = param.pop("name")
sig_func = self._python_func_cache.get(sig_name) or self._解析信号函数(sig_name)
if sig_func is None:
logger.warning(f"信号函数未找到: {sig_name}")
return None
freq = param.pop("freq", None)
if freq is not None:
周期秒 = int(freq)
obs = self._观察者字典.get(周期秒)
if obs is None:
logger.warning(f"未找到周期 {freq} 的观察者")
return None
try:
return sig_func(obs, **param)
except Exception:
logger.exception(f"信号函数执行异常: {sig_name}")
return None
else:
try:
return sig_func(self, **param)
except Exception:
logger.exception(f"信号函数执行异常: {sig_name}")
return None
```
- [ ] **步骤 5:移植辅助方法**
```python
def _去重配置(self, configs: List[Dict]) -> List[Dict]:
seen = set()
unique = []
for c in configs:
key = (c.get("name"), frozenset(
(k, str(v)) for k, v in c.items() if k != "name"
))
if key not in seen:
seen.add(key)
unique.append(c)
return unique
def _预加载Python信号函数(self):
for config in self._python_configs:
name = config.get("name", "")
if name and name not in self._python_func_cache:
self._python_func_cache[name] = None # placeholder
for name in list(self._python_func_cache.keys()):
try:
self._python_func_cache[name] = self._解析信号函数(name)
except Exception:
logger.warning(f"预加载信号函数失败: {name}")
@staticmethod
def _解析信号函数(name: str) -> Optional[Callable]:
"""动态导入信号函数(移植自 信号计算器._解析信号函数)。"""
import os
if "." not in name:
return __import__(name)
module_name, func_name = name.rsplit(".", 1)
# 检查 __main__ 缓存
main_mod = sys.modules.get("__main__")
if main_mod is not None and hasattr(main_mod, func_name):
return getattr(main_mod, func_name)
module = __import__(module_name, fromlist=[func_name])
return getattr(module, func_name)
def _提取行情(self):
"""从基础周期观察者提取 OHLCV 行情(Python 回退路径)。"""
obs = self._观察者字典.get(self._基础周期)
if obs is None:
return
klines = obs.普通K线序列
if not klines:
return
k = klines[-1]
self.行情 = {
"symbol": obs.符号,
"dt": k.时间戳, # 需要从 i64 转 datetime
"id": k.序号,
"open": k.开盘价,
"high": k.最高价,
"low": k.最低价,
"close": k.收盘价,
"vol": k.成交量,
}
@property
def 信号字典(self) -> dict:
"""合并信号 + 行情(与 Position.update() 兼容)。"""
return {**self.信号, **self.行情}
def 获取周期观察者(self, freq: str) -> Optional[观察者]:
"""按频率获取观察者。"""
return self._观察者字典.get(int(freq))
def 从信号列表提取配置(self, 信号序列: List[str]):
"""从信号字符串列表解析配置(委托给 SignalsParser)。"""
from chanlun.chan_external import get_signals_config
from chanlun.chan_external import SignalsParser
if not 信号序列:
return
sp = SignalsParser(signals_module=self._信号模块)
conf = sp.parse(信号序列)
self.信号配置 = conf
```
- [ ] **步骤 6Commit**
```bash
git add chanlun-py/chanlun/signal_orchestrator.py
git commit -m "feat(signal): SignalOrchestrator — Rust 优先 + Python 回退混合编排器
Co-Authored-By: Claude <noreply@anthropic.com>"
```
---
### 任务 B2:切换到 strategies.py
**文件:** `strategies.py`
- [ ] **步骤 1:更新导入**
将第 28 行的导入从:
```python
from chanlun.chan_external import 信号计算器 as _信号计算器, get_signals_config
```
改为:
```python
from chanlun.chan_external import get_signals_config
from chanlun.signal_orchestrator import SignalOrchestrator as _信号计算器
```
> 使用别名 `_信号计算器` 保持类名不变——策略内部代码零改动。
- [ ] **步骤 2:运行策略验证测试**
```bash
python test_策略验证.py
```
预期:所有 V1-V7 测试通过,无回归。
- [ ] **步骤 3Commit**
```bash
git add strategies.py
git commit -m "refactor(strategies): 切换到 SignalOrchestrator 混合编排器
Co-Authored-By: Claude <noreply@anthropic.com>"
```
---
### 任务 B3:修复 main.py 中损坏的调用点
**文件:** `main.py:2220`
- [ ] **步骤 1:修复构造函数调用**
当前损坏的代码:
```python
计算器 = cet.信号计算器(观察者字典, 基础周期=周期组[0], 信号模块="chanlun.signals")
计算器.从信号序列设置配置([...]) # 方法不存在
```
修复为:
```python
计算器 = cet.SignalOrchestrator(分析器, 信号模块="chanlun.signals")
计算器.从信号列表提取配置([...])
```
> 注意:此处 `分析器` 变量需要在该作用域内可用。需要先检查 main.py 上下文。
- [ ] **步骤 2Commit**
```bash
git add main.py
git commit -m "fix(main): 修复损坏的 信号计算器 调用点 → SignalOrchestrator
Co-Authored-By: Claude <noreply@anthropic.com>"
```
---
## 阶段 C:测试
### 任务 C1:编排器单元测试
**文件:** 创建 `chanlun-py/tests/test_signal_orchestrator.py`
- [ ] **步骤 1:编写框架测试**
```python
"""SignalOrchestrator 集成测试 — 混合 Rust + Python 信号执行。"""
import pytest
from datetime import datetime, timezone
from chanlun.signal_orchestrator import SignalOrchestrator
def test_构造_空配置():
"""空配置构造不崩溃。"""
from chanlun import 立体分析器, 缠论配置
analyzer = 立体分析器("test", [300, 900], 缠论配置())
orch = SignalOrchestrator(analyzer)
assert orch.信号字典 == {}
assert orch._rust_configs == []
assert orch._python_configs == []
def test_Rust信号已注册():
"""youwukuncheng 信号名在 Rust 注册表中(应分类到 rust_configs)。"""
from chanlun import 立体分析器, 缠论配置
analyzer = 立体分析器("test", [86400], 缠论配置())
config = [{
"name": "youwukuncheng_中枢第三买卖点_V230602",
"freq": 86400,
"max_overlap": 3,
"本级完整性": "实",
"同级完整性": "合",
}]
orch = SignalOrchestrator(analyzer, 信号配置=config)
assert len(orch._rust_configs) == 1
assert len(orch._python_configs) == 0
def test_Python信号回退():
"""未知信号名分类到 python_configs。"""
from chanlun import 立体分析器, 缠论配置
analyzer = 立体分析器("test", [300], 缠论配置())
config = [{
"name": "chanlun.signals.demo.tas_ma_base_V230313",
"freq": 300,
"ma_type": "SMA",
"timeperiod": 5,
}]
orch = SignalOrchestrator(analyzer, 信号配置=config)
assert len(orch._rust_configs) == 0
assert len(orch._python_configs) == 1
def test_freq验证_不在周期组():
"""freq 不在分析器周期组中时抛出 ValueError。"""
from chanlun import 立体分析器, 缠论配置
analyzer = 立体分析器("test", [300], 缠论配置())
with pytest.raises(ValueError, match="不在分析器周期组"):
SignalOrchestrator(analyzer, 信号配置=[{
"name": "some_signal",
"freq": 99999,
}])
```
- [ ] **步骤 2:运行测试**
```bash
python -m pytest chanlun-py/tests/test_signal_orchestrator.py -v
```
预期:全部通过。
- [ ] **步骤 3Commit**
```bash
git add chanlun-py/tests/test_signal_orchestrator.py
git commit -m "test(signal): SignalOrchestrator 单元测试
Co-Authored-By: Claude <noreply@anthropic.com>"
```
---
### 任务 C2:端到端回归测试
- [ ] **步骤 1:运行所有 tests**
```bash
cd chanlun && cargo test
cd chanlun-py && cargo test
python -m pytest chanlun-py/tests/ -v
python test_策略验证.py
```
- [ ] **步骤 2:验证零回归**
预期:所有已有测试通过。新编排器测试通过。
---
## 自检结论
- **规格覆盖**:阶段 A 覆盖 SignalEngine 增强 → 完整信号字典;阶段 B 覆盖混合编排器 → 替代 Python `信号计算器`;阶段 C 覆盖测试 → 零回归
- **类型一致**`完整更新结果``MarketData` 字段与 Python `self.行情` 键名一致
- **风险提示**
1. `main.py:2220` 调用点需要确认其所在函数的上下文(分析器变量是否在作用域内)
2. `SignalOrchestrator``_提取行情()``k.时间戳` 是 i64,需用 `datetime.fromtimestamp` 转换
3. Python 信号函数需要 `chanlun.signals` 可导入——需确认安装包含 signals 子包
- **向后兼容**`strategies.py` 使用别名导入——内部代码零改动
@@ -0,0 +1,503 @@
# 子项目2 信号函数 API + 移植 youwukuncheng 实现计划
> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
**目标:** 建立 Rust 信号函数编写规范(便捷 API + 参数提取 + 确保指标),移植第一个真实信号 `youwukuncheng_中枢第三买卖点_V230602`,并通过集成测试与 Python 版对比验证。
**架构:** 便捷方法直接加到 `K线`/`缠论K线`/`观察者` 上(不引入额外 trait);信号函数放 `chanlun/src/signal/functions/`;参数提取独立为 `signal/params.rs`
**技术栈:** Rust edition 2024、`serde_json::Value``parking_lot::RwLock``inventory`
**设计文档:** `docs/superpowers/specs/2026-06-23-signal-fn-api-and-port-design.md`
---
## 文件结构
| 文件 | 职责 |
|---|---|
| `chanlun/src/kline/bar.rs` | 给 `K线` 加便捷指标访问方法 (`macd()`, `rsi()`, `kdj()`, `boll()`, `ma()`) |
| `chanlun/src/kline/chan_kline.rs` | 给 `缠论K线` 加转发便捷方法 |
| `chanlun/src/business/observer.rs` | 加 `普K偏移()``缠K偏移()``最后缠K序列()``确保指标已计算()` |
| `chanlun/src/signal/params.rs` | **新建** — 参数提取辅助函数 (`get_string`, `get_int`, `get_f64`) |
| `chanlun/src/signal/mod.rs` | 增 `pub mod params;` + `pub mod functions;` |
| `chanlun/src/signal/functions/mod.rs` | **新建**`pub mod youwukuncheng;` |
| `chanlun/src/signal/functions/youwukuncheng.rs` | **新建** — 移植的中枢第三买卖点信号 |
| `chanlun/tests/test_signal_youwukuncheng.rs` | **新建** — 集成测试(Rust vs Python 对比) |
---
## 任务 0:便捷 API — K线指标访问 + 观察者方法 + 参数提取
**文件:**
- 修改:`chanlun/src/kline/bar.rs`
- 修改:`chanlun/src/kline/chan_kline.rs`
- 修改:`chanlun/src/business/observer.rs`
- 创建:`chanlun/src/signal/params.rs`
- 修改:`chanlun/src/signal/mod.rs`
### 步骤 1:K线 便捷指标访问方法
`chanlun/src/kline/bar.rs``impl K线` 块中添加以下方法。
`K线` 已有 `pub 指标: RwLock<指标容器>` 字段,以及 `pub 收盘价: f64` 等 OHLC 字段。新增方法封装 `self.指标.read()` 的 boilerplate
```rust
/// 便捷读取 MACD 指标。若未计算则返回 None。
pub fn macd(&self) -> Option<&平滑异同移动平均线> {
// 注意:返回的引用受 RwLockReadGuard 生命周期约束
// 需要 unsafe 或者改用 cloned 版本
// 实际采用:提供返回 Option<平滑异同移动平均线> 的 cloned 版本
// 同时提供一个需要传入 guard 的零拷贝版本
}
// 实际实现方案:提供 _cloned 便捷方法(开销可忽略,MACD 仅几个 f64)
pub fn macd(&self) -> Option<平滑异同移动平均线> {
self.指标.read().macd_cloned()
}
pub fn rsi(&self) -> Option<相对强弱指数> {
self.指标.read().rsi_cloned()
}
pub fn kdj(&self) -> Option<随机指标> {
self.指标.read().kdj_cloned()
}
pub fn boll(&self) -> Option<布林带> {
self.指标.read().boll_cloned()
}
pub fn ma(&self, key: &str) -> Option<f64> {
self.指标.read().均线().and_then(|m| m.get(key).copied())
}
```
> **设计理由**:使用 `_cloned` 版本而非返回引用,避免 `RwLockReadGuard` 生命周期传染到调用方。MACD/RSI/KDJ/BOLL 结构体只含少量 f64 和 Option<f64>clone 开销可忽略。
### 步骤 2:缠论K线 便捷转发方法
`chanlun/src/kline/chan_kline.rs``impl 缠论K线` 块中添加转发方法。缠K 有 `pub 标的K线: RwLock<Arc<K线>>` 字段:
```rust
/// 便捷读取 MACD(委托给标的K线)
pub fn macd(&self) -> Option<平滑异同移动平均线> {
self.标的K线.read().macd()
}
pub fn rsi(&self) -> Option<相对强弱指数> {
self.标的K线.read().rsi()
}
pub fn kdj(&self) -> Option<随机指标> {
self.标的K线.read().kdj()
}
pub fn boll(&self) -> Option<布林带> {
self.标的K线.read().boll()
}
pub fn ma(&self, key: &str) -> Option<f64> {
self.标的K线.read().ma(key)
}
/// 读取收盘价(委托给标的K线)
pub fn 收盘价(&self) -> f64 {
self.标的K线.read().收盘价
}
```
### 步骤 3:观察者便捷访问方法
`chanlun/src/business/observer.rs``impl 观察者` 块中添加:
```rust
/// 按偏移取普K,di=1 为最后一根,di=2 为倒数第二根
pub fn 普K偏移(&self, di: usize) -> Option<&Arc<K线>> {
if di == 0 || di > self.普通K线序列.len() { return None; }
Some(&self.普通K线序列[self.普通K线序列.len() - di])
}
/// 按偏移取缠K,di=1 为最后一根
pub fn 缠K偏移(&self, di: usize) -> Option<&Arc<缠论K线>> {
if di == 0 || di > self.缠论K线序列.len() { return None; }
Some(&self.缠论K线序列[self.缠论K线序列.len() - di])
}
/// 最后 N 根缠K(返回切片引用)
pub fn 最后缠K序列(&self, n: usize) -> &[Arc<缠论K线>] {
let len = self.缠论K线序列.len();
if n >= len { &self.缠论K线序列[..] }
else { &self.缠论K线序列[len - n..] }
}
```
### 步骤 4:参数提取模块
创建 `chanlun/src/signal/params.rs`
```rust
//! 信号函数参数提取辅助 — 从 `HashMap<String, Value>` 中提取类型化参数。
use serde_json::Value;
use std::collections::HashMap;
/// 提取字符串参数,缺失或类型不对时返回默认值。
pub fn get_string(params: &HashMap<String, Value>, key: &str, default: &str) -> String {
params.get(key)
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| default.to_string())
}
/// 提取 i64 参数。
pub fn get_int(params: &HashMap<String, Value>, key: &str, default: i64) -> i64 {
params.get(key)
.and_then(|v| v.as_i64())
.unwrap_or(default)
}
/// 提取 f64 参数。
pub fn get_f64(params: &HashMap<String, Value>, key: &str, default: f64) -> f64 {
params.get(key)
.and_then(|v| v.as_f64())
.unwrap_or(default)
}
/// 提取字符串引用(零拷贝),缺失时返回默认值。
pub fn get_str<'a>(params: &'a HashMap<String, Value>, key: &str, default: &'a str) -> &'a str {
params.get(key)
.and_then(|v| v.as_str())
.unwrap_or(default)
}
```
修改 `chanlun/src/signal/mod.rs`,在 `pub mod registry;` 后追加:
```rust
pub mod params;
pub mod functions;
```
### 步骤 5:构建验证
```bash
cd chanlun && cargo build
```
预期:编译通过。
### 步骤 6Commit
```bash
git add chanlun/src/kline/bar.rs chanlun/src/kline/chan_kline.rs \
chanlun/src/business/observer.rs chanlun/src/signal/params.rs \
chanlun/src/signal/mod.rs
git commit -m "feat(signal): 便捷API — K线指标访问 + 观察者偏移 + 参数提取"
```
---
## 任务 1:确保指标 API
**文件:**
- 修改:`chanlun/src/business/observer.rs`
### 步骤 1:添加 `确保指标已计算` 方法
`观察者``impl` 块中添加(需要 `use crate::indicators::calculator::指标计算器;`):
```rust
/// 确保所有 K 线上的指标已计算(幂等)。
/// 在信号函数入口调用,保证后续 macd()/rsi() 等访问不返回 None。
pub fn 确保指标已计算(&self) {
if self.配置.计算指标 && !self.普通K线序列.is_empty() {
指标计算器::计算并挂载(&self.普通K线序列, &self.配置);
}
}
```
### 步骤 2:构建验证
```bash
cd chanlun && cargo build
```
### 步骤 3Commit
```bash
git add chanlun/src/business/observer.rs
git commit -m "feat(signal): 观察者.确保指标已计算() — 信号函数入口幂等调用"
```
---
## 任务 2:移植 youwukuncheng 信号函数
**文件:**
- 创建:`chanlun/src/signal/functions/mod.rs`
- 创建:`chanlun/src/signal/functions/youwukuncheng.rs`
### 步骤 1:创建 functions 模块入口
创建 `chanlun/src/signal/functions/mod.rs`
```rust
//! 信号函数实现 — 每个 `#[signal]` 注册的函数对应一个子模块。
//!
//! 第三方代码声明:信号函数模式参考 czschttps://github.com/waditu/czsc
//! Apache License 2.0),已适配为 Rust `fn(&观察者, &HashMap<String, Value>) -> Vec<Signal>`
pub mod youwukuncheng;
```
### 步骤 2:编写 youwukuncheng.rs
创建 `chanlun/src/signal/functions/youwukuncheng.rs`。核心结构:
```rust
use std::collections::HashMap;
use serde_json::Value;
use chanlun_signal_macros::signal;
use crate::business::observer::观察者;
use crate::signal::params;
use crate::signal::Signal;
/// 中枢第三买卖点信号 — 返回所有匹配的第三类买卖点信号。
///
/// 参数模板:"{freq}_D1MO{max_overlap}_中枢第三买卖点V230602"
///
/// 返回三种信号(k3 = 特征 + "V230602"):
/// - 中枢段DEA穿越2V230602(同级检查)
/// - DEA穿越0轴V230602(本级检查,无须分型)
/// - 首次穿越0轴V230602(本级检查 + 分型确认)
#[signal(
name = "youwukuncheng_中枢第三买卖点_V230602",
template = "{freq}_D1MO{max_overlap}_中枢第三买卖点V230602"
)]
pub fn youwukuncheng_中枢第三买卖点_V230602(
obs: &观察者,
params: &HashMap<String, Value>,
) -> Vec<Signal> {
// 1. 确保指标已计算
obs.确保指标已计算();
// 2. 提取参数
let max_overlap = params::get_int(params, "max_overlap", 3);
let freq = params::get_string(params, "freq", "日线");
let 本级完整性 = params::get_string(params, "本级完整性", "实");
let 同级完整性 = params::get_string(params, "同级完整性", "合");
let k1 = freq;
let k2 = format!("D1MO{max_overlap}");
let k3 = "中枢第三买卖点V230602";
// 3. 前置检查
let 当前缠K = match obs.当前缠K() {
Some(k) => k,
None => return vec![Signal::new_empty(&k1, &k2, k3)],
};
// 使用线段中枢序列(对应 Python 的 观察员.中枢序列)
let 中枢序列 = obs.线段中枢序列();
if 中枢序列.is_empty() {
return vec![Signal::new_empty(&k1, &k2, k3)];
}
let 当前中枢 = &中枢序列[中枢序列.len() - 1];
// 检查是否基于线段
if 当前中枢.基础序列.read()[0].标识.read().as_str() != "线段" {
return vec![Signal::new_empty(&k1, &k2, k3)];
}
// 检查中枢状态
if 当前中枢.当前状态() == "中枢之中" {
return vec![Signal::new_empty(&k1, &k2, k3)];
}
// 检查本级第三买卖线
let 本级线 = match 当前中枢.本级_第三买卖线.read().as_ref() {
Some(line) => Arc::clone(line),
None => return vec![Signal::new_empty(&k1, &k2, k3)],
};
let mut result = Vec::new();
let mut 买卖点分型: Option<Arc<分型>> = None;
let 状态 = 当前中枢.当前状态();
// 4. 本级检查
if 当前中枢.完整性(&本级完整性) {
// ... DEA穿越0轴 + 首次穿越0轴 逻辑
// (详见完整实现)
}
// 5. 同级检查
// ... 中枢段DEA穿越2 逻辑
// (详见完整实现)
if result.is_empty() {
vec![Signal::new_empty(&k1, &k2, k3)]
} else {
result
}
}
```
> **注意**:上述为骨架代码。完整实现需按 Python 版 1:1 翻译,包括:
> - `之后缠K序列` 切片(`缠论K线序列[index..]`
> - DIF/DEA 零轴穿越检测循环
> - 分型确认 + `分型::从缠K序列中获取分型`
> - `线段::分割序列` + `虚线::统计MACD行为`
> - 偏移计算与 score = max(0, 100 - 偏移 * 5)
需要额外依赖 `Signal` 的空构造器。在 `signal/signal.rs` 中添加:
```rust
impl Signal {
/// 创建一个"空"信号(v1=v2=v3="任意"score=0),对应 Python `create_single_signal(k1=k1, k2=k2, k3=k3)`
pub fn new_empty(k1: &str, k2: &str, k3: &str) -> Self {
Self {
signal: format!("{}_{}_{}_任意_任意_任意_0", k1, k2, k3),
score: 0,
k1: k1.to_string(),
k2: k2.to_string(),
k3: k3.to_string(),
v1: "任意".to_string(),
v2: "任意".to_string(),
v3: "任意".to_string(),
}
}
/// 创建带分类值的信号
pub fn new(k1: &str, k2: &str, k3: &str, v1: &str, v2: &str, v3: &str, score: i32) -> Self {
Self {
signal: format!("{}_{}_{}_{}_{}_{}_{}", k1, k2, k3, v1, v2, v3, score),
score,
k1: k1.to_string(),
k2: k2.to_string(),
k3: k3.to_string(),
v1: v1.to_string(),
v2: v2.to_string(),
v3: v3.to_string(),
}
}
}
```
### 步骤 3:构建验证
```bash
cd chanlun && cargo build
```
预期:编译通过。
### 步骤 4Commit
```bash
git add chanlun/src/signal/functions/ chanlun/src/signal/signal.rs
git commit -m "feat(signal): 移植 youwukuncheng_中枢第三买卖点_V230602 到 Rust"
```
---
## 任务 3:集成测试 — Rust vs Python 对比
**文件:**
- 创建:`chanlun/tests/test_signal_youwukuncheng.rs`
### 步骤 1:创建 Python 参考脚本
`chanlun-py/tests/` 下创建 `gen_youwukuncheng_golden.py`,跑 Python 版信号函数并输出 JSON
```python
"""生成 youwukuncheng 信号预期输出(golden file"""
import json, sys
sys.path.insert(0, '.')
from chanlun.chan import 观察者, 缠论配置, K线
from chanlun.signals.youwukuncheng import youwukuncheng_中枢第三买卖点_V230602
# 加载 .nb 文件
obs = 观察者("btcusd", 86400, 缠论配置.默认())
obs.读取数据文件("chanlun-py/tests/btcusd-86400-xxx.nb", 缠论配置.默认())
# 调用信号函数
params = {"freq": "日线", "max_overlap": 3, "本级完整性": "实", "同级完整性": "合"}
result = youwukuncheng_中枢第三买卖点_V230602(obs, **params)
# 输出为 JSON
output = {k: v for k, v in result.items()}
print(json.dumps(output, ensure_ascii=False, indent=2))
```
### 步骤 2:编写 Rust 集成测试
创建 `chanlun/tests/test_signal_youwukuncheng.rs`
```rust
use std::collections::HashMap;
use chanlun::business::observer::观察者;
use chanlun::config::缠论配置;
use chanlun::signal::functions::youwukuncheng::youwukuncheng_中枢第三买卖点_V230602;
use serde_json::Value;
#[test]
fn test_youwukuncheng_产生信号() {
let obs = 观察者::new("btcusd".into(), 86400, 缠论配置::default());
obs.write().读取数据文件("tests/btcusd-86400-xxx.nb", 缠论配置::default().不推送())
.expect("读取数据文件失败");
let obs = obs.read();
let mut params = HashMap::new();
params.insert("freq".to_string(), Value::String("日线".to_string()));
params.insert("max_overlap".to_string(), Value::Number(3.into()));
params.insert("本级完整性".to_string(), Value::String("实".to_string()));
params.insert("同级完整性".to_string(), Value::String("合".to_string()));
let signals = youwukuncheng_中枢第三买卖点_V230602(&obs, &params);
println!("产生 {} 个信号:", signals.len());
for s in &signals {
println!(" key={} value={} score={}", s.key(), s.value(), s.score);
}
// 至少有一个非空信号(取决于数据)
let non_empty: Vec<_> = signals.iter()
.filter(|s| s.value() != "任意_任意_任意_0")
.collect();
println!("非空信号数: {}", non_empty.len());
// 验证所有信号的 k3 后缀
for s in &signals {
assert!(s.k3.ends_with("V230602"), "k3 必须以 V230602 结尾: {}", s.k3);
}
}
#[test]
fn test_youwukuncheng_无中枢返回空信号() {
let obs = 观察者::new("empty".into(), 300, 缠论配置::default());
let obs = obs.read();
let params = HashMap::new();
let signals = youwukuncheng_中枢第三买卖点_V230602(&obs, &params);
assert_eq!(signals.len(), 1);
assert_eq!(signals[0].value(), "任意_任意_任意_0");
}
```
### 步骤 3:运行测试
```bash
cd chanlun && cargo test --test test_signal_youwukuncheng
```
预期:测试通过(或根据数据情况调整断言)。
### 步骤 4Commit
```bash
git add chanlun/tests/test_signal_youwukuncheng.rs
git commit -m "test(signal): youwukuncheng 集成测试 — 信号产出 + 空中枢边界"
```
---
## 自检结论
- **规格覆盖**:设计 §5 便捷 API → 任务 0;§7 确保指标 → 任务 1;§6 youwukuncheng → 任务 2;§8 测试 → 任务 3。全覆盖。
- **类型一致**`SignalFn` 签名不变。`#[signal]` 注册用子项目 1 的宏。`Signal::new_empty`/`Signal::new` 为新增构造器。
- **风险提示**
1. `K线::macd()` 返回 cloned 值而非引用——已在设计 §5.1 说明理由(避免 RwLockReadGuard 生命周期传染)
2. 集成测试依赖具体 `.nb` 测试数据——需确认文件存在且包含中枢结构
3. `Signal::new_empty` 的 key 格式需与 Python `create_single_signal` 一致(过滤 "任意" 段)
@@ -0,0 +1,239 @@
# 信号原语层移植到 Rust 核心层 — 设计文档
- 日期:2026-06-22
- 范围:原语层(Operate / Signal / Factor / Event / Position 配置与匹配部分)
- 参考:czsc`/home/moscow/czsc`)的 Rust workspace 分层
## 1. 目标与背景
当前信号匹配框架(`Signal` / `Factor` / `Event` / `Position` / `Operate`)以纯 Python 实现于 `chanlun-py/chanlun/chan_external.py`(已合并进根目录 `chan.py`)。这套框架抄录自 czscApache 2.0)。
把这层**纯结构 + 匹配逻辑**移植到 Rust 核心层(`chanlun/src/signal/`),目的:
- **消除跨模块枚举/类型不一致问题**:信号原语只跟字符串和信号字典打交道,不持有 Rust 分析对象,天然规避「同值枚举跨模块 `is` 不相等」「动态导入找不到模块」这类坑。
- **统一原语来源**:Rust 端策略/回测可直接用同一套 `Signal`/`Event`,无需经过 Python。
- **性能**:匹配逻辑是热路径(每根 K 线、每个 Position 都跑),Rust 实现去掉 Python 解释开销。
- **为后续分层铺路**:原语层稳定后,未来可按 czsc 的路线增量推进注册表、信号串解析、交易引擎。
## 2. 范围
### 纳入(Rust + PyO3
- `Operate` 枚举
- `Signal``key()` / `value()` / `is_match()`
- `Factor``is_match()` / `unique_signals()` / `dump()` / `load()`
- `Event``is_match()` / `unique_signals()` / `dump()` / `load()`
- `Position` 基类:配置字段 + 校验 + `unique_signals` + `__repr__` + config 部分的 `dump`/`load`
### 不纳入(保持 Python
- `Position.update()` 状态机(持仓推进、止损、超时、`pairs`、操作决策)
- `信号计算器`(信号计算引擎、配置管理、`_自动挂载指标`
- `SignalsParser`docstring 解析)
- `import_by_name`(动态导入)
- 全部信号函数(`chanlun.signals.*`
## 3. czsc 参考映射
czsc 把信号体系拆成分层 crate。本次只对应其最底层「信号原语」:
| czsc | 本次对应 |
|---|---|
| `czsc-core/objects/{signal,event,position,operate}.rs` | `chanlun/src/signal/{signal,factor,event,position,operate}.rs` |
| `czsc-core``#[cfg(feature="python")]` 内联 PyO3 包装 | `chanlun-py/src/signal_py.rs`(本项目沿用独立绑定 crate 的既有约定,不内联) |
czsc 的 `inventory` 编译期注册表、`#[signal]` 宏、`sig_parse``engine_v2` 交易引擎、`signals_dispatcher` **本次均不涉及**(属后续分层)。
## 4. 架构与模块布局
```
chanlun/src/signal/
├── mod.rs # pub mod 声明 + re-export
├── operate.rs # Operate 枚举(HL/HS/HO/LO/LE/SO/SE
├── signal.rs # Signal
├── factor.rs # Factor
├── event.rs # Event
└── position.rs # Position 基类(config + matching,不含 update
```
- `chanlun/src/lib.rs` 增加 `pub mod signal;`
- PyO3 绑定新增 `chanlun-py/src/signal_py.rs`,在 `lib.rs` 注册顺序:types → **signal** → config → indicators → kline → structure → algorithm → business → equality。
### 依赖边界
信号原语层**零依赖** `business` / `algorithm` / `structure` 层。它只操作:
- `String`(信号各字段)
- 信号字典:匹配时通过 PyO3 接收 `&Bound<PyDict>`,逐键取值判类型
这是它能独立 `cargo test`、规避跨模块类型问题的根本原因。
## 5. 逐组件设计
### 5.1 Operate
```rust
#[pyclass(eq, eq_int)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Operate { HL, HS, HO, LO, LE, SO, SE }
```
- 值映射中文:`HL="持多" HS="持空" HO="持币" LO="开多" LE="平多" SO="开空" SE="平空"`,通过 `value()` 方法 / `__str__` 暴露。
- Python 端 `cet.Operate.LO` 直接用该枚举。
### 5.2 Signal
```rust
#[pyclass(module = "chanlun._chanlun")]
pub struct Signal {
signal: String,
score: i32,
k1: String, k2: String, k3: String,
v1: String, v2: String, v3: String,
}
```
> 注:仅 `Position` 需要 `#[pyclass(subclass)]`Python 子类补 `update()`)。`Signal`/`Factor`/`Event` 不被子类化,用普通 `#[pyclass]`
- 构造签名:`Signal(signal="", score=0, k1="任意", k2="任意", k3="任意", v1="任意", v2="任意", v3="任意")`
- `signal` 非空 → 按 `_` 拆 7 段(非 7 段 raise);为空 → 由各字段拼。
- `signal` 非字符串 → `TypeError`(对齐 Python `__post_init__`)。
- `score` 越界 [0,100] → `ValueError`
- `key` property:拼接 k1/k2/k3 中非「任意」的部分,`_` 连接。
- `value` property`v1_v2_v3_score`
- `is_match(s) -> bool`:见 §6。
- `__repr__``Signal('<signal>')`
### 5.3 Factor
```rust
#[pyclass(module = "chanlun._chanlun")]
pub struct Factor {
signals_all: Vec<Signal>,
signals_any: Vec<Signal>,
signals_not: Vec<Signal>,
name: String,
}
```
- 构造:`Factor(signals_all, signals_any=[], signals_not=[], name="")``signals_all` 空 → `ValueError`
- 构造时计算 `name`:见 §6 ③(确定性哈希)。
- `unique_signals` property:所有 signals 的 `signal` 字符串去重列表。
- `is_match``signals_not` 任一命中 → False`signals_all` 必须全中;`signals_any` 非空时至少一中。
- `dump() -> dict``load(raw) classmethod`
### 5.4 Event
```rust
#[pyclass(module = "chanlun._chanlun")]
pub struct Event {
operate: Operate,
factors: Vec<Factor>,
signals_all: Vec<Signal>,
signals_any: Vec<Signal>,
signals_not: Vec<Signal>,
name: String,
sha256: String,
}
```
- 构造:`Event(operate, factors, signals_all=[], signals_any=[], signals_not=[], name="")``factors` 空 → `ValueError`
- `name`:有传名 → `<name>#<hash>`,否则 `<operate中文值>#<hash>`;同时存 `sha256` 字段。
- `unique_signals``is_match(s) -> (bool, Option<String>)`(命中返回 `(True, factor_name)`)、`dump``load`
- `get_signals_config` **不在 Rust 实现**(依赖 Python 的 `SignalsParser`),保留在调用方 Python。
### 5.5 Position 基类
```rust
#[pyclass(subclass, module = "chanlun._chanlun")]
pub struct Position {
symbol: String,
opens: Vec<Event>,
exits: Vec<Event>,
events: Vec<Event>, // opens + exits
name: String,
interval: i64,
timeout: i64,
stop_loss: i64,
T0: bool,
}
```
- 构造:`Position(symbol, opens, exits=[], interval=0, timeout=1000, stop_loss=1000, T0=False, name)`
- `name` 缺失 → `ValueError`(对齐 Python `assert name`)。
- 每个 event 的 `operate` ∈ {LO,LE,SO,SE},否则 raise。
- `unique_signals` property、`__repr__`、config 部分的 `dump`/`load`
- **状态字段、`update()``pairs``with_data` 版 dump、`get_signals_config` 全部留 Python 子类。**
## 6. 三个兼容性关键点
### ① `Signal.is_match` 缺键时 raise `ValueError`
Python 现状:键不在信号字典 → `raise ValueError``strategies.py``try: pos.update(...) except ValueError: pass` 兜底。
**决策**Rust `is_match` 缺键 → `PyValueError`,**不静默返回 False**。这是行为契约。
### ② 信号字典值可能非字符串
`信号计算器.信号字典` 合并了 OHLCV 行情(值为 datetime/float)。Python 有 `isinstance(v, str)` 守卫:非 str → `logger.warning` + 返回 False。
**决策**`is_match` 接收 `&Bound<PyDict>`。取到 key 对应值后:
- 值不存在 → `PyValueError`(关键点 ①)。
- 值非字符串 → 返回 False(对齐 Python 守卫)。**不打 warning**:匹配是每根 K 线的热路径,省去日志噪音;非 str 值来自 OHLCV 行情注入,是预期情况而非异常。
- 值是字符串 → 按 `_` 拆 4 段(`v1_v2_v3_score`)做匹配。
### ③ Factor/Event 的 sha256 命名
Python`hashlib.sha256(str(dump_dict_minus_name).encode()).hexdigest().upper()[:4]`,依赖 Python `str(dict)` 的逐字节格式。
**决策**:用 Rust 确定性哈希——对 `signals_all`/`signals_any`/`signals_not`Factor)或加上 factors 的 dumpEvent)拼成稳定字符串后算 sha256,取大写前 4。
- 自洽:同输入恒等同名,`dump`/`load` 来回一致。
- **取舍(已知不兼容)**:生成的 hash 与 Python 旧版不同。依赖旧 `name` 的持久化仓位(保存的 .json)不再 roundtrip。本项目 Position 基本每次运行新建,可接受。
## 7. Drop-in 兼容策略
- `chan_external.py` 顶部:`from chanlun._chanlun import Signal, Factor, Event, Operate, Position as _PositionBase`,删除原 Python 类定义。
- `Position` 改为子类:
```python
class Position(_PositionBase):
def __init__(self, symbol, opens, exits=[], interval=0, timeout=1000,
stop_loss=1000, T0=False, name=None):
super().__init__(symbol, opens, exits, interval, timeout, stop_loss, T0, name)
# Python 侧状态
self.pos_changed = False
self.operates = []
self.holds = []
self.pos = 0
self.last_event = {...}
self.last_lo_dt = None
self.last_so_dt = None
self.end_dt = None
# update() / pairs / get_signals_config / with_data dump 保留
```
- `main.py` / `strategies.py``cet.Signal(...)``cet.Factor(...)``cet.Event(...)``cet.Position(...)``cet.Operate.LO` **无需改动**——构造签名与方法名一致。
- 根目录 `chan.py` 的对应类同样替换为 import Rust 版本(保持与包版本一致)。
## 8. 测试策略
1. **Rust 单测**`cargo test``chanlun/src/signal/``#[cfg(test)]`):
- Signal7 段解析、非 7 段 raise、score 越界 raise、key 过滤「任意」、value 拼接。
- Factor/Event`signals_all/any/not` 真值表全覆盖、空 signals_all/factors raise、确定性哈希同输入同名。
- Positionname 缺失 raise、非法 operate raise、unique_signals 去重。
2. **跨语言一致性**pytest,复用 `tests/helpers/api_consistency.py`):
- 构造相同 Signal/Factor/Event/Position,断言 `is_match``unique_signals``dump` 结构与移植前**逐字段一致**(name hash 除外)。
- `is_match` 缺键 raise `ValueError`、值非 str 返回 False 两条边界。
3. **回归**:跑 `测试_信号识别` + sync 回测,确认信号匹配与开关仓行为不变。
## 9. 已知取舍
- **name hash 不兼容旧 Python 版本**(§6 ③):依赖旧 name 的持久化仓位会对不上。可接受,因 Position 多为运行时新建。
- **`get_signals_config` 留 Python**:它依赖 `SignalsParser` 动态解析,本次不移植;Rust `Event`/`Position` 不提供该方法,由 Python 调用方补。
- **`Position.update` 留 Python**:状态机本次不移植,Position 被一分为二(Rust 基类配置 + Python 子类状态)。
## 10. 许可证
新增 Rust 文件沿用项目 MIT 头。信号原语逻辑摘录/参考自 czsc(Apache 2.0),在 `signal/mod.rs` 顶部加第三方代码声明(与根 `chan.py` 已有声明一致)。
@@ -0,0 +1,181 @@
# 子项目 1:信号注册框架 — 设计文档
- 日期:2026-06-22
- 所属:「全 Rust 信号计算迁移」第 1 个子项目(共 4 个)
- 参考:czsc`/home/moscow/czsc`)的 `czsc-signal-macros` + `czsc-signals/{registry,types}.rs`
- 前置:原语层已完成(`chanlun/src/signal/` 的 Signal/Factor/Event/Position/Operate
## 1. 背景与目标
「全 Rust 信号计算迁移」把信号函数、注册/解析、计算引擎、持仓状态机全部移到 Rust。拆为 4 个子项目(依赖序 1→2→3→4):
1. **信号注册框架**(本文档)
2. 信号函数 API 暴露 + 移植 youwukuncheng
3. 信号计算引擎 + PyO3 分发器
4. Position.update 状态机
本子项目交付**编译期信号注册机制**:一个 `#[signal]` 属性宏 + `inventory` 注册表 + 描述符类型 + 一个探针信号验证机制。
**它消灭什么**Python 的 `import_by_name`(动态导入,曾导致「找不到模块」「跨模块枚举 `is` 不等」)和 `SignalsParser` 的 docstring 正则解析(曾导致「多 pattern sig_pats_map」「get_function_name v[0]」「sys 未导入」等脆弱 bug)。注册变成编译期完成、查表 O(1)。
## 2. 范围
### 纳入
- 新 proc-macro crate `chanlun-signal-macros``#[signal(name, template)]` 属性宏
- `chanlun/src/signal/registry.rs``SignalDescriptor` / `SignalFn` / `SignalMeta` / `SIGNAL_REGISTRY` + 只读查询 API
- `chanlun/Cargo.toml` 新增 `inventory` 依赖 + path 依赖 `chanlun-signal-macros`
- 一个探针信号 + 测试(验证注册→查表→重名检测)
### 不纳入(后续子项目)
- 真实信号函数移植(子项目 2
- 「确保指标按需增量计算」API(子项目 2,移植 youwukuncheng 读 MACD 时落地)
- 信号计算引擎 + `call_signal` PyO3 分发器(子项目 3
- Position.update 状态机(子项目 4
## 3. 关键设计决策
| 决策 | 选择 | 理由 |
|---|---|---|
| SignalFn 是否带 TaCache | **否** | 核心层 K线已挂载指标(`指标计算器::计算并挂载`),信号函数直接读 `标的K线.指标.macd(..)`,无需 czsc 式 TaCache |
| 注册表位置 | **chanlun 核心 crate** `signal/` 模块 | 信号函数直接读 observer(同 crate)、指标在 K线上,无需独立 signals crate |
| params 类型 | `HashMap<String, serde_json::Value>` | 灵活,对应 Python dict 来源(PyO3 层自然转换) |
| 描述符是否含 indicators/category 字段 | **否,保持最小 `{name, template, func}`** | 指标由「信号内识别 + 管线增量算」处理,不在描述符声明;本项目信号皆 observer 级,无需 category |
## 4. Crate 结构
```
chanlun-signal-macros/ ← 新建 proc-macro crateRust 强制独立)
├── Cargo.toml ← [lib] proc-macro = truedeps: syn, quote, proc-macro2
└── src/lib.rs ← #[signal] 属性宏
chanlun/ ← 现有核心 crate
├── Cargo.toml ← 新增 inventory="0.3" + path 依赖 chanlun-signal-macros
└── src/signal/
├── mod.rs ← pub mod registry;
└── registry.rs ← 描述符类型 + 注册表 + 探针信号(cfg(test)
```
`chanlun` 通过 path 依赖 `chanlun-signal-macros`(无需引入 workspaceCargo path 依赖即可。如愿统一可后续加 `[workspace]`)。
## 5. 描述符类型与签名(`chanlun/src/signal/registry.rs`
```rust
use crate::business::observer::观察者;
use crate::signal::Signal;
use serde_json::Value;
use std::collections::HashMap;
use std::sync::LazyLock;
/// 信号函数签名 — 读观察者状态(含 K线已挂指标)+ 参数 → 信号列表。无 TaCache。
pub type SignalFn = fn(&观察者, &HashMap<String, Value>) -> Vec<Signal>;
/// 信号描述符(编译期元数据,由 `#[signal]` 宏生成、`inventory` 收集)。
#[derive(Clone, Copy)]
pub struct SignalDescriptor {
/// 信号函数名,如 "youwukuncheng_中枢第三买卖点_V230602"
pub name: &'static str,
/// 参数模板,如 "{freq}_D1MO{max_overlap}_中枢第三买卖点V230602"
pub template: &'static str,
/// 函数指针
pub func: SignalFn,
}
inventory::collect!(SignalDescriptor);
/// 运行时信号元信息。
pub struct SignalMeta {
pub func: SignalFn,
pub template: &'static str,
}
/// 归并描述符为注册表;重名返回 Err(纯函数,便于单测)。
fn 归并(
descs: impl Iterator<Item = SignalDescriptor>,
) -> Result<HashMap<&'static str, SignalMeta>, String> {
let mut m: HashMap<&'static str, SignalMeta> = HashMap::new();
for d in descs {
if m.insert(d.name, SignalMeta { func: d.func, template: d.template }).is_some() {
return Err(format!("信号重名:{}", d.name));
}
}
Ok(m)
}
/// 全局注册表视图(由 inventory 归并;重名 panicfail-fast)。
pub static SIGNAL_REGISTRY: LazyLock<HashMap<&'static str, SignalMeta>> = LazyLock::new(|| {
归并(inventory::iter::<SignalDescriptor>.into_iter().copied())
.unwrap_or_else(|e| panic!("{e}"))
});
/// 按名查信号元信息。
pub fn get_signal(name: &str) -> Option<&'static SignalMeta> {
SIGNAL_REGISTRY.get(name)
}
/// 按名查参数模板。
pub fn get_template(name: &str) -> Option<&'static str> {
SIGNAL_REGISTRY.get(name).map(|m| m.template)
}
/// 列出所有已注册信号名(排序)。
pub fn list_signal_names() -> Vec<&'static str> {
let mut v: Vec<_> = SIGNAL_REGISTRY.keys().copied().collect();
v.sort();
v
}
```
## 6. `#[signal]` 宏(`chanlun-signal-macros/src/lib.rs`
属性宏贴在信号函数上,做三件事:
1. **校验**:函数名必须含 `_V<数字版本>``name` 属性须与函数名一致;`name`/`template` 非空。不符 → `compile_error!`
2. **保留原函数**不变。
3. **生成** 一个 `static` 描述符 + `inventory::submit!` 提交:
宏输入 `#[signal(name = "foo_V230101", template = "{freq}_D1_foo")]` 贴在 `fn foo_V230101(...)` 上,展开为(概念示意):
```rust
fn foo_V230101(观: &观察者, p: &HashMap<String, Value>) -> Vec<Signal> { /* 原体 */ }
inventory::submit! {
crate::signal::registry::SignalDescriptor {
name: "foo_V230101",
template: "{freq}_D1_foo",
func: foo_V230101 as crate::signal::registry::SignalFn,
}
}
```
**路径约定**:宏 emit `crate::signal::registry::...`,即假定信号函数住在 `chanlun` crate 内(本迁移的既定结构)。
## 7. 测试
1. **宏 crate**`chanlun-signal-macros/tests/test_signal_macro.rs`):普通集成测试——定义一个符合签名的探针函数并贴 `#[signal(name="probe_macro_V000000", template="{freq}_D1_probe")]`,断言它能编译且 `inventory::iter` 能收到对应描述符(name/template 正确)。编译失败用例(name 与函数名不一致、缺版本号)作为**可选** trybuild compile-fail 测试,非必须。
2. **核心注册表**`registry.rs``#[cfg(test)]`):
- 用 `inventory::submit!` 提交一个探针 `SignalDescriptor`name `__probe_V000000`);
- `get_signal("__probe_V000000")` 命中、`get_template` 返回模板、`list_signal_names()` 含它;
- 重名场景:把归并逻辑抽成一个可独立调用的纯函数 `fn 归并(descs: impl Iterator<Item=SignalDescriptor>) -> Result<HashMap<..>, String>`,单测对重复 name 返回 Err`SIGNAL_REGISTRY` 的 LazyLock 内部调用它并对 Err `panic!`),避免污染全局 inventory。
## 8. 数据流
```
编译期: #[signal] 宏 → SignalDescriptor 常量 → inventory::submit!
启动时: SIGNAL_REGISTRY (LazyLock) ← inventory::iter 归并(重名 panic
运行时: get_signal(name) -> &SignalMeta { func, template } O(1) 查表)
后续子项目 3 的计算引擎用 func 调用、用 template 反向生成信号 key
```
## 9. 错误处理
- **编译期**:宏校验失败 → `compile_error!`(带清晰中文消息)。
- **启动期**:重名信号 → `panic!("信号重名:{name}")`fail-fast,对应 czsc 的 normalize 重名检测)。
- **运行期**`get_signal` 未命中返回 `None`(调用方——子项目 3——决定如何处理,对应旧「未找到解析函数」告警)。
## 10. 已知取舍与后续
- **无运行时可扩展性**:信号在编译期注册,新增信号需重编译(`maturin build`)。这是「全 Rust」方案的既定取舍,用户已确认。
- **指标按需机制不在本子项目**:信号函数读指标 + 管线增量计算的「确保指标」API 在子项目 2 落地。
- **categorykline/trader)暂不引入**:若子项目 4 的 Position.update 引入 trader 级信号,届时再扩描述符。
## 11. 许可证
新增 Rust 文件沿用项目 MIT 头。注册/宏机制参考 czscApache 2.0),在 `registry.rs` 与 macro crate 顶部加第三方代码声明。
@@ -0,0 +1,105 @@
# 信号计算器完全移植评估
- 日期:2026-06-23
- 前置:混合迁移(SignalEngine + SignalOrchestrator)已完成
## 1. 当前差距
### 1.1 未移植的 Python 信号函数(7/8)
| 函数 | 文件 | 行数 | 复杂度 | 移植工时 |
|------|------|------|--------|----------|
| `bar_zdt_V230331` | demo.py | 38 | 极低 | ~1h |
| `macd_金叉` | demo.py | 56 | 低 | ~1-2h |
| `tas_macd_direct_V221106` | demo.py | 55 | 低 | ~1-2h |
| `tas_ma_base_V230313` | demo.py | 57 | 低-中 | ~2-3h |
| `cxt_停顿分型_V230106` | demo.py | 49 | 低-中 | ~3-5h |
| `cxt_bi_end_V230222` | demo.py | 74 | 中 | ~4-8h |
| `模板_V日期` | _template.py | 28 | 模板 | 不需要 |
**总计:约 12-21 小时**
> 已移植的只有 `youwukuncheng_中枢第三买卖点_V230602`1/8)。
### 1.2 可移除的 Python 组件
| 组件 | 文件 | 替换方案 |
|------|------|----------|
| `SignalsParser` 类 | chan_external.py:102-293 | 不再需要——配置由 Rust 注册表直接生成 |
| `get_signals_config()` | chan_external.py:296-310 | `list_signals()` + 直接构造配置 |
| `从信号列表提取配置()` | chan_external.py:522-530 | `list_signals()` + `get_signal_template()` |
| `create_single_signal()` | chan_external.py:312-319 | 不再需要(Rust 信号函数使用 `Signal::new_empty` |
| `chanlun.signals` 包 | signals/*.py | 所有函数已移植到 Rust |
| `chanlun.parse` | parse.py | 仅被 `SignalsParser` 使用 |
| `chan.py` 中的副本 | chan.py:7394+ | 内部副本,可单独处理 |
## 2. 关键依赖链
```
strategies.py
└→ get_signals_config(position.unique_signals, signals_module)
└→ SignalsParser(signals_module).parse(signal_strings)
└→ 遍历 chanlun.signals 模块的所有函数
└→ 读取文档字符串 → 正则提取参数模板
└→ parse 库反向格式化 → 配置字典
```
完全移植后,这个链简化为:
```
strategies.py
└→ 直接构造 config = [{name, freq, params}] 从 Rust list_signals()
```
## 3. 建议:分两阶段执行
### 阶段 1:移植剩余信号函数(~12-21h)
按复杂度递增顺序:
| 子任务 | 内容 |
|--------|------|
| 1.1 | 移植 `bar_zdt_V230331``chanlun/src/signal/functions/demo.rs` |
| 1.2 | 移植 `macd_金叉``demo.rs` |
| 1.3 | 移植 `tas_macd_direct_V221106``demo.rs` |
| 1.4 | 移植 `tas_ma_base_V230313``demo.rs`(需要均线计算辅助) |
| 1.5 | 移植 `cxt_停顿分型_V230106``demo.rs` |
| 1.6 | 移植 `cxt_bi_end_V230222``demo.rs` |
每个子任务:
- 编写 Rust 函数 + `#[signal]` 注册
- 编写 Rust 单元测试
- 编写 Python 对比测试(Rust vs Python 输出)
### 阶段 2:移除 Python 回退路径(~4-6h
| 子任务 | 内容 |
|--------|------|
| 2.1 | 简化 `SignalOrchestrator` → 仅使用 `SignalEngine` |
| 2.2 | 移除 `SignalsParser``get_signals_config``从信号列表提取配置` |
| 2.3 | 移除 `chanlun.signals` 包(demo.py/youwukuncheng.py/_template.py |
| 2.4 | 移除 `chanlun.parse`vendored parse 库) |
| 2.5 | 更新 `strategies.py` 使用直接配置构造 |
| 2.6 | 更新测试文件 |
## 4. 收益
| 收益 | 说明 |
|------|------|
| 代码量减少 | 移除 ~1,200 行 PythonSignalsParser + signals 包 + parse.py + chan.py 副本) |
| 统一执行路径 | 不再有 Rust/Python 双路径,消除维护成本 |
| 编译时安全 | 所有信号函数编译时注册,不会运行时 `import_by_name` 失败 |
| 性能提升 | 批量 Rust 执行 vs 逐个 Python 调用 |
| 依赖精简 | 移除 vendored `parse` 库和 `chanlun.signals` 包 |
## 5. 风险
| 风险 | 缓解 |
|------|------|
| `cxt_bi_end_V230222` 依赖笔/分型序列指针比较 | Rust 已有 `分型`/`笔` 结构,使用 `Arc` 指针 |
| `cxt_停顿分型_V230106` 依赖 `与MACD柱子分型匹配` | 需要确认 Rust 侧是否有该方法或等效逻辑 |
| `tas_ma_base_V230313` 依赖均线按需计算 | Rust 已有 `指标计算器::计算并挂载``k.ma(key)` |
| `strategies.py` 默认信号配置为空时依赖 `get_signals_config` | 切换到 `list_signals()` + 直接构造 |
## 6. 结论
**完全移植可行,建议执行。** 总工作量约 16-27 小时。7 个未移植信号函数按复杂度递增顺序逐个移植(阶段 1),然后移除 Python 回退路径(阶段 2)。完成后信号框架为纯 Rust 核心 + Python 薄绑定,不再有 Python 动态导入路径。
@@ -0,0 +1,58 @@
# 子项目 4Position.update 状态机迁移到 Rust — 设计文档
- 日期:2026-06-23
- 所属:「全 Rust 信号计算迁移」第 4 个子项目(共 4 个)
- 前置:子项目 1-3 已完成(注册表、信号函数、计算引擎)
## 1. 背景与目标
子项目 1-3 交付了完整的信号计算链路:`#[signal]` 注册表 → 信号函数 → 计算引擎。Position.update 状态机是信号框架中最后一个仍留在 Python 中的核心逻辑(~135 行),将其迁移到 Rust 后,信号框架的纯 Rust 核心部分全部就位。
子项目 4 交付:
1. Position 状态字段(pos, operates, holds, last_event...)→ Rust 核心
2. update() 状态机算法 → Rust 核心(与 Python 版 1:1 对应)
3. pairs() 开平配对计算 → Rust 核心
4. PyO3 绑定:update(), 状态 getter, dump/load 带状态
## 2. 设计决策
| 决策 | 选择 | 理由 |
|------|------|------|
| 状态字段位置 | 直接加在 Position 结构体 | backtrader 在 GIL 下单线程访问;不需要额外锁 |
| update 签名(Rust | `fn update(&mut self, dt: i64, price: f64, bid: i64, signals: &信号字典)` | 核心不依赖 Python 类型;OHLCV 由 PyO3 层提取 |
| PyDict → 信号字典 | 排除 OHLCV 键后调用 字典转核心 | 复用已有转换逻辑 |
| dt 类型兼容 | 支持 datetime/i64/f64 → 统一转为 i64 Unix 秒 | 兼容三种常见输入格式 |
| 时间戳 → Python datetime | `datetime.datetime.fromtimestamp(ts, UTC)` | 保持 operates/holds 元素类型与旧版一致 |
| Python 向后兼容 | 保留 Python 子类,__init__ 简化为空;update/pairs/dump 由 Rust 提供 | 不破坏 strategies.py 等下游代码 |
| Operate 枚举映射 | `核心Operate → OperatePy` 一对一转换函数 | 类型安全,无运行时开销 |
## 3. 新增 Rust 类型
```rust
pub struct 操作记录 { symbol, dt, bid, price, op: Operate, op_desc, pos }
pub struct 持仓记录 { dt, pos, price }
pub struct 开平配对 { 标的代码, 策略标记, 交易方向, 开仓时间, 平仓时间, 开仓价格, 平仓价格, 持仓K线数, 事件序列, 持仓天数, 盈亏比例 }
pub struct 最近事件 { dt, bid, price, op, op_desc }
```
Position 新增 7 个状态字段:`pos, pos_changed, operates, holds, last_event, last_lo_dt, last_so_dt, end_dt`
## 4. update() 状态机
与 Python `Position.update(s)` 1:1 对应:
1. 时间校验:`dt <= end_dt` → 日志警告,跳过
2. 事件匹配:遍历 events,调用 `event.is_match(signals)`
3. 开仓处理:LO → 间隔检查 → 开多/平空;SO → 间隔检查 → 开空/平多
4. 多头出场:LE 信号 / 止损(price/last_price - 1 < -stop_loss/10000/ 超时(bid - last_bid > timeout
5. 空头出场:SE 信号 / 止损(方向反转)/ 超时
6. 记录持仓快照 holds
## 5. 文件结构
```
chanlun/src/signal/position.rs ← 操作记录/持仓记录/开平配对/最近事件 类型 + 状态字段 + update/pairs
chanlun-py/src/signal_py.rs ← PositionPy: update(PyDict), 状态 getter, dump(with_data), load, 时间戳转datetime
chanlun-py/chanlun/chan_external.py ← Python Position 子类简化(__init__ → pass
chanlun-py/tests/test_position_update.py ← 集成测试(24 用例)
```
@@ -0,0 +1,142 @@
# `信号计算器` Rust 迁移 — 设计文档
- 日期:2026-06-23
- 所属:全 Rust 信号计算迁移 — 子项目 1-4 完成后的延续
- 前置:子项目 1-4 全部完成(注册表 + 信号函数 + 引擎 + Position 状态机)
## 1. 背景
「全 Rust 信号计算迁移」4 个子项目完成后,信号框架的 Rust 核心已就位:
- `#[signal]` 注册表 → 编译时信号函数发现
- `SignalEngine` → 按名查找 + 批量执行
- `Position.update()` → 状态机
**`信号计算器`(Python 信号编排器)仍然在使用 Python 动态导入**(`import_by_name`)来发现和执行信号函数。它与 Rust `SignalEngine` **并行存在**,形成两条独立的执行路径。
## 2. 设计目标
1. **统一信号执行路径**Rust `SignalEngine` 作为主路径,Python 动态导入作为回退
2. **保持向后兼容**`strategies.py` 无需改动内部逻辑
3. **渐进式迁移**:新增 Rust 信号函数自动通过引擎执行,无需修改编排器代码
4. **最终目标**:所有信号函数移植到 Rust 后,Python 回退路径可移除
## 3. 关键设计决策
| 决策 | 选择 | 理由 |
|------|------|------|
| 编排器架构 | 新建 `SignalOrchestrator` 类,不修改 `信号计算器` | 零风险切换;旧类保留用于对比验证 |
| 信号函数分类 | 构造时按 `list_signals()` 将配置分为 Rust/Python 两组 | 避免每次 `更新()` 都查注册表 |
| Rust 路径 | 使用 `SignalEngine.更新_完整()`(批量) | 性能优于逐个 `call_signal()` |
| Python 路径 | 保留 `import_by_name` + `_解析信号函数` | 非侵入式;已有信号函数无需任何修改 |
| OHLCV 行情 | Rust 引擎直接返回基础周期行情 | 消除 Python 侧的独立行情提取步骤 |
| freq 验证 | 在编排器 setter 中验证 | 与旧 `信号计算器` 行为一致 |
## 4. 架构图
```
┌─────────────────────────────────────────────────┐
│ SignalOrchestrator │
│ │
│ 信号配置 ──→ 分类(list_signals() 查表) │
│ │ │
│ ┌────────┴────────┐ │
│ │ Rust 已注册 │ Python 未注册 │
│ │ SignalEngine │ import_by_name │
│ │ .更新_完整() │ ._执行Python信号函数() │
│ └────────┬────────┘ │
│ │ │
│ 合并结果 → self.信号 + self.行情 │
│ │
│ self.信号字典 → Position.update() │
└─────────────────────────────────────────────────┘
```
## 5. SignalEngine 增强
### 5.1 新增 `更新_完整()` 方法
```rust
pub struct 完整更新结果 {
pub signals: HashMap<String, String>,
pub market: Option<MarketData>,
}
pub struct MarketData {
pub symbol: String,
pub dt: i64, // Unix 秒
pub id: i64,
pub open: f64, pub high: f64, pub low: f64,
pub close: f64, pub vol: f64,
}
```
`更新_完整(&self, analyzer: &立体分析器) -> 完整更新结果`:
1. 调用 `self.更新(analyzer)` 获取信号
2. 从 `analyzer.周期组[0]` 获取基础周期观察者
3. 提取最后一根普K的 OHLCV 数据
4. 返回组合结果
## 6. SignalOrchestrator 设计
### 6.1 类签名
```python
class SignalOrchestrator:
def __init__(
self,
分析器: 立体分析器,
信号配置: Optional[List[Dict]] = None,
信号模块: str = "chanlun.signals",
):
```
### 6.2 方法
| 方法 | 来源 | 说明 |
|------|------|------|
| `更新()` | 新写 | 先 Rust 批量,再 Python 逐个 |
| `信号配置` (property) | 移植 | setter 中添加 Rust/Python 分类 |
| `信号字典` (property) | 移植 | `{**self.信号, **self.行情}` |
| `获取周期观察者(freq)` | 移植 | 委托给 `_观察者字典` |
| `从信号列表提取配置(信号序列)` | 移植 | 委托给 `SignalsParser` |
| `_去重配置(configs)` | 移植 | 与旧版一致 |
| `_预加载Python信号函数()` | 移植 | 缓存 Python 函数引用 |
| `_解析信号函数(name)` | 移植 | `import_by_name` 逻辑 |
| `_执行Python信号函数(config)` | 移植 | Python 函数调用 |
| `_提取行情()` | 移植 | 仅 Python-only 回退路径使用 |
## 7. 迁移路径
### 阶段 A:增强 SyncSignalEngine1-2 commits
- `更新_完整()` + PyO3 绑定
- 不改变现有行为
### 阶段 B:引入 SignalOrchestrator2-3 commits
- 新文件 `signal_orchestrator.py`
- `strategies.py` 切换到新类(别名导入)
- 修复 `main.py` 损坏的调用点
### 阶段 C:废弃 Python 并行路径(未来)
- 所有信号函数移植到 Rust 后
- 移除 `信号计算器``SignalsParser``import_by_name`
- 移除 `signals/` 目录中的 Python 信号函数
## 8. 向后兼容
| 组件 | 兼容策略 |
|------|---------|
| `strategies.py` | 别名导入 `SignalOrchestrator as _信号计算器`——零代码改动 |
| `main.py` | 修复损坏的调用点(原本就 broken) |
| `test_策略验证.py` | 零改动——`信号计算器` 类名不变 |
| Python 信号函数 | 零改动——`import_by_name` 路径不变 |
| `Position.update()` | 零改动——Rust 状态机不变 |
## 9. 风险
| 风险 | 缓解 |
|------|------|
| `_提取行情()``k.时间戳` 是 i64Rust K线),不是 Python datetime | 已由 Rust `PositionPy::时间戳转datetime` 处理 |
| `SignalEngine.更新_完整()` 的基础周期可能与 `_基础周期` 不一致 | 统一从 `分析器.周期组[0]` 获取 |
| Python 信号函数的 `**kwargs``freq` 是字符串(来自 SignalsParser | `_执行Python信号函数``int(freq)` 转换 |
| `list_signals()` 返回的是 Rust 注册名,不含模块路径 | 按短名匹配(`youwukuncheng_中枢第三买卖点_V230602` 不含 `chanlun.signals.` 前缀) |
@@ -0,0 +1,200 @@
# 子项目 2:信号函数 API + 移植第一个真实信号 — 设计文档
- 日期:2026-06-23
- 所属:「全 Rust 信号计算迁移」第 2 个子项目(共 4 个)
- 前置:子项目 1 已完成(`#[signal]` 宏 + `inventory` 注册表)
- 参考:`chanlun-py/chanlun/signals/youwukuncheng.py`、czsc
## 1. 背景与目标
子项目 1 交付了编译期信号注册机制(`#[signal]` + `inventory` + `SIGNAL_REGISTRY`),探针信号已验证注册→查表链路。现在是时候移植第一个真实信号函数,并在过程中建立 Rust 信号函数的**编写规范**和**辅助 API**。
子项目 2 交付:
1. **信号函数便捷 API** — 扩展 trait,让 Rust 信号函数代码读起来接近 Python 版本
2. **确保指标按需增量计算** — 信号函数可确保所需指标已计算
3. **移植 youwukuncheng_中枢第三买卖点_V230602** — 第一个真实信号(3 种信号变体)
4. **集成测试** — Rust vs Python 输出对比
## 2. 范围
### 纳入
- `chanlun/src/signal/functions/` 模块(信号函数目录)
- `chanlun/src/signal/functions/youwukuncheng.rs` — 移植的中枢第三买卖点信号
- 便捷扩展 trait`IndicatorAccess`K线指标读取)、`ObserverAccess`(观察者便捷访问)
- 参数提取辅助函数(`params_ext.rs`
- 确保指标 API`观察者::确保指标已计算(&self)`
- 集成测试:喂入 `.nb` 数据,Rust 信号输出 vs Python 信号输出
- `#[signal]` 注册 youwukuncheng
### 不纳入(后续子项目)
- 信号计算引擎 + `call_signal` PyO3 分发器(子项目 3
- Position.update 状态机(子项目 4
- 其他信号函数(demo.py 中的 macd_金叉、cxt_bi_end 等)
- Python 侧可直接调用的 PyO3 信号函数分发器
## 3. 关键设计决策
| 决策 | 选择 | 理由 |
|---|---|---|
| 便捷 API 形式 | **直接给 K线 / 观察者 加方法** | 简洁,不需要 import 额外 trait。已有前例(观察者.当前缠K()) |
| 指标访问封装 | **方法返回 Option,隐藏 RwLock** | 信号函数不应关心锁细节;`kline.macd()` 返回 `Option<&MACD>` |
| 确保指标机制 | **观察者.确保指标已计算() 重跑计算器** | 简单,复用现有 `指标计算器::计算并挂载`。后续子项目 3 由计算引擎在调用前统一 ensure |
| 参数提取 | **独立 `params` 子模块,纯函数** | `HashMap<String, Value>` 的字符串/数字提取到处都需要,集中处理 |
| 信号函数位置 | `chanlun/src/signal/functions/` | 与 registry 同 crate`#[signal]` emit 的 `crate::` 路径可直接解析 |
| 测试策略 | **Rust 集成测试 + Python 对比** | 加载 .nb → 跑 Rust 信号 → 序列化输出;Python 侧同样跑 → diff |
## 4. 文件结构
```
chanlun/src/signal/
├── mod.rs ← pub mod functions; pub mod params;
├── functions/
│ ├── mod.rs ← pub mod youwukuncheng;
│ └── youwukuncheng.rs ← #[signal] fn youwukuncheng_中枢第三买卖点_V230602
├── params.rs ← 参数提取辅助函数
├── ... (已有: signal, factor, event, position, operate, registry)
chanlun/src/kline/
├── bar.rs ← 给 K线 加便捷指标访问方法
chanlun/src/business/
├── observer.rs ← 给 观察者 加便捷方法 + 确保指标
chanlun/tests/
├── test_signal_youwukuncheng.rs ← 集成测试(Rust vs Python 对比)
```
## 5. 便捷 API 设计
### 5.1 K线 便捷指标访问(`bar.rs` 新增方法)
将现有的 `k线.指标.read().macd()` 封装为直接的 `k线.macd()`
```rust
impl K线 {
/// 读取 MACD 指标(已计算则返回引用,否则 None)
pub fn macd(&self) -> Option<&平滑异同移动平均线> { ... }
pub fn rsi(&self) -> Option<&相对强弱指数> { ... }
pub fn kdj(&self) -> Option<&随机指标> { ... }
pub fn boll(&self) -> Option<&布林带> { ... }
/// 读取均线值,如 ma("SMA_5") → Option<f64>
pub fn ma(&self, key: &str) -> Option<f64> { ... }
}
```
同样给 `缠论K线` 加转发方法(委托给 `self.标的K线`)。
### 5.2 观察者便捷访问(`observer.rs` 新增方法)
```rust
impl 观察者 {
/// 按偏移取普K(di=1 为最后一根)
pub fn 普K偏移(&self, di: usize) -> Option<&Arc<K线>> { ... }
/// 按偏移取缠K
pub fn 缠K偏移(&self, di: usize) -> Option<&Arc<缠论K线>> { ... }
/// 最后 N 根缠K
pub fn 最后缠K序列(&self, n: usize) -> &[Arc<缠论K线>] { ... }
/// 线段级中枢序列(= 中枢序列组[1])
pub fn 线段中枢序列(&self) -> &Vec<Arc<中枢>> { ... }
/// 确保所有 K 线上的指标已计算(调用 指标计算器::计算并挂载)
pub fn 确保指标已计算(&self) { ... }
}
```
### 5.3 参数提取(`signal/params.rs`
```rust
/// 从 params HashMap 提取字符串参数
pub fn get_string(params: &HashMap<String, Value>, key: &str, default: &str) -> String;
/// 从 params HashMap 提取整数参数
pub fn get_int(params: &HashMap<String, Value>, key: &str, default: i64) -> i64;
/// 从 params HashMap 提取浮点参数
pub fn get_f64(params: &HashMap<String, Value>, key: &str, default: f64) -> f64;
```
这些是纯辅助函数,不做任何复杂逻辑。
## 6. youwukuncheng 移植要点
### 6.1 信号逻辑
Python 版 143 行 → Rust 预计 ~200 行(含类型标注和 RwLock 读取)。
三种产出信号(k3 后缀均为 `V230602`):
| k3 | 触发条件 | v1 | v2 | score |
|---|---|---|---|---|
| `中枢段DEA穿越2V230602` | 同级第三买卖线段内 DEA 穿越 0 轴 | 中枢段DEA穿越2 | 三买/三卖 | max(0, 100-偏移×5) |
| `DEA穿越0轴V230602` | 本级第三买卖线处 DEA 在 0 轴同侧 | DEA穿越0轴 | 三买/三卖 | max(0, 100-偏移×5) |
| `首次穿越0轴V230602` | DIF 首次反穿 0 轴 + 分型确认 | 首次穿越0轴 | 三买/三卖 | max(0, 100-偏移×5) |
### 6.2 关键 Rust 对应
| Python | Rust |
|---|---|
| `观察员.当前缠K` | `obs.当前缠K()` |
| `观察员.中枢序列` | `obs.中枢序列()` (笔中枢) 或 `obs.线段中枢序列()` (线段中枢) |
| `当前中枢.基础序列[0].标识` | `当前中枢.基础序列.read()[0].标识.read().as_str()` |
| `当前中枢.当前状态()` | `当前中枢.当前状态()` |
| `当前中枢.本级_第三买卖线` | `当前中枢.本级_第三买卖线.read().as_ref()` |
| `当前中枢.完整性("实")` | `当前中枢.完整性("实")` |
| `k.标的K线.macd.DEA` | `k.标的K线.read().macd().map(\|m\| m.DEA)` |
| `k.分型 is 分型结构.底` | `*k.分型.read() == Some(分型结构::底)` |
| `分型.从缠K序列中获取分型(序列, k)` | `分型::从缠K序列中获取分型(序列, k)` |
| `虚线.统计MACD行为(普K序列, 8, 3)` | `虚线::统计MACD行为(&普K序列, 8, 3)` |
| `段.获取普K序列(观察员.观察员)` | `段.获取普K序列(&obs.普通K线序列)` |
### 6.3 注意事项
1. **lock 顺序**:读取 `基础序列``武``标的K线``指标` 时注意 RwLock 不可重入。同一作用域内避免同时持有多个写锁。本函数只有读操作,安全。
2. **AtomicI64**`序号``.load(Ordering::Relaxed)` 读取
3. **Option 链**Python 的 `x.y.z` 在 Rust 中是 `x.y.read().z`,需要处理 `Option`
4. **空信号返回**Python 返回 `create_single_signal(k1, k2, k3)`v1=v2=v3="任意");Rust 返回 `vec![Signal::new_empty(k1, k2, k3)]`
## 7. 确保指标 API
```rust
impl 观察者 {
/// 确保所有 K线上的指标已计算。
/// 如果 配置.计算指标 为 true 且序列非空,则调用 指标计算器::计算并挂载。
pub fn 确保指标已计算(&self) {
if self.配置.计算指标 && !self.普通K线序列.is_empty() {
指标计算器::计算并挂载(&self.普通K线序列, &self.配置);
}
}
}
```
信号函数在入口调用一次 `obs.确保指标已计算()`(幂等——计算器检测已计算的值会跳过)。
注:后续子项目 3 的信号计算引擎会在调用任何信号前统一 ensure,信号函数内部的 ensure 调用届时可移除。
## 8. 测试设计
### 8.1 集成测试(`chanlun/tests/test_signal_youwukuncheng.rs`
1. 加载测试 `.nb` 文件(选择已有中枢结构的 btcusd 数据)
2. 创建观察者,喂入 K 线,触发分析
3. 调用 `youwukuncheng_中枢第三买卖点_V230602(&obs, &params)`
4. 验证返回的 `Vec<Signal>` 非空,信号 key/value 格式正确
5. 与 Python 版输出对比(golden 方式:运行 Python 脚本生成预期输出文件,Rust 测试读取对比)
### 8.2 测试数据
使用已有测试 `.nb` 文件(如 `btcusd-86400-...`,日线数据有丰富的中枢结构)。
## 9. 错误处理
- 信号函数内部所有 `Option` 缺值 → 返回空信号(与 Python 行为一致)
- `确保指标已计算` 失败 → 静默跳过(指标不存在时信号函数内部 `macd().is_none()` 自然会返回空)
- `#[signal]` 注册失败(重名)→ 子项目 1 已处理(编译期 panic)
## 10. 已知取舍
- **便捷方法只加常用读路径**`macd()/rsi()/kdj()/boll()/ma()` + 偏移访问。复杂查询(如遍历所有 K 线做自定义分析)直接用底层 API。
- **确保指标基于现有管线**:不做 czsc 式的 TaCache(已决策,见子项目 1 §3)。SignalFn 签名保持 `&观察者` 单参数。
- **信号函数在 lib 内**:不暴露为独立的 `chanlun-signals` crate。与子项目 1 决策一致——信号函数同 crate,可直接访问 observer 内部。
## 11. 许可证
新增文件沿用项目 MIT 头。youwukuncheng 移植自项目自有 Python 代码,不涉及第三方许可证。
+1
View File
@@ -0,0 +1 @@
/target/
+594
View File
@@ -0,0 +1,594 @@
# 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"
+14
View File
@@ -0,0 +1,14 @@
[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"
+202
View File
@@ -0,0 +1,202 @@
//! 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 注册
// ═══════════════════════════════════════════════════════════
/// 插件信号 AMACD 零上强势(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;
/// 插件信号 B1MACD 金叉(与宿主内 `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
}
+552 -649
View File
File diff suppressed because it is too large Load Diff
+232 -2
View File
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
import json
import backtrader as bt
from datetime import datetime, timedelta
from typing import Optional, List
@@ -7,9 +8,12 @@ import math
import queue
import threading
import numpy as np
import chanlun.chan_external as cet
__all__ = ["订单类型", "市场类型", "自适应市场仓位", "交易信号", "批次", "订单执行器", "高级策略基类_", "高级策略基类", "自定义实时数据源", "回测"]
__all__ = ["订单类型", "市场类型", "自适应市场仓位", "交易信号", "批次", "订单执行器", "高级策略基类_", "高级策略基类", "自定义实时数据源", "回测", "信号驱动策略", "NB数据源", "随机数据"]
# ---------- 订单类型枚举 ----------
@@ -52,6 +56,12 @@ 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]
@@ -735,7 +745,11 @@ class 自定义实时数据源(bt.feed.DataBase):
print(f"[{datetime.now()}] 自定义数据源: 数据格式错误,跳过: {e}")
return True
self.lines.datetime[0] = bt.date2num(datetime.utcfromtimestamp(int(dt)))
# 兼容 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.open[0] = o
self.lines.high[0] = h
self.lines.low[0] = l
@@ -1051,6 +1065,222 @@ 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