Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 16ed3de8f5 | |||
| 8405d478bc | |||
| 15a1d43b1d | |||
| a8df8cd187 | |||
| 9c7686d104 | |||
| 34c42ecd68 | |||
| 8a2afb9ed0 | |||
| 11d897ebaa | |||
| 4bf9461009 | |||
| c83cddbcc4 | |||
| 0a7e9dc896 | |||
| dd40f95742 | |||
| e33b1f0744 |
@@ -0,0 +1,182 @@
|
||||
---
|
||||
name: Bug 报告
|
||||
about: 创建一个 Bug 报告帮助我们改进
|
||||
title: "[Bug] "
|
||||
labels: bug
|
||||
assignees: []
|
||||
---
|
||||
|
||||
## Bug 描述
|
||||
|
||||
请简洁清晰地描述这个 Bug。
|
||||
|
||||
## 复现步骤
|
||||
|
||||
请提供最简化的复现步骤(代码片段或操作序列):
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
|
||||
## 预期行为
|
||||
|
||||
请描述您期望发生的结果。
|
||||
|
||||
## 实际行为
|
||||
|
||||
请描述实际发生的结果。包括完整的错误消息、panic 信息或日志输出。
|
||||
|
||||
## 环境
|
||||
|
||||
| 项目 | 版本 |
|
||||
|------|------|
|
||||
| Rust 版本 | `rustc --version` 输出 |
|
||||
| Crate 版本 | `chanlun` 版本号 |
|
||||
| 操作系统 | Linux / macOS / Windows |
|
||||
| 数据文件(.nb) | 文件路径或来源 |
|
||||
|
||||
## 复现代码
|
||||
|
||||
请粘贴可复现问题的最小 Rust 代码或 Python 代码。
|
||||
|
||||
**Rust:**
|
||||
|
||||
```rust
|
||||
use chanlun::config::缠论配置;
|
||||
use chanlun::kline::bar::K线;
|
||||
use chanlun::business::observer::观察者;
|
||||
|
||||
// 复现代码
|
||||
```
|
||||
|
||||
**Python (绑定层):**
|
||||
|
||||
```python
|
||||
import chanlun
|
||||
|
||||
# 复现代码
|
||||
```
|
||||
|
||||
## 缠论配置
|
||||
|
||||
请提供您使用的完整配置(JSON 或代码形式)。这对于复现问题至关重要。
|
||||
|
||||
<details>
|
||||
<summary>展开查看配置 JSON</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"标识": "btcusd",
|
||||
"缠K合并替换": false,
|
||||
"笔内元素数量": 5,
|
||||
"笔内相同终点取舍": false,
|
||||
"笔内起始分型包含整笔": false,
|
||||
"笔内起始分型包含整笔_包括右": false,
|
||||
"笔内原始K线包含整笔": false,
|
||||
"笔次级成笔": false,
|
||||
"笔弱化": false,
|
||||
"笔弱化_原始数量": 3,
|
||||
"线段_非缺口下穿刺": false,
|
||||
"线段_特征序列忽视老阴老阳": false,
|
||||
"线段_缺口后紧急修正": true,
|
||||
"线段_修正": false,
|
||||
"线段内部中枢图显": true,
|
||||
"扩展线段_当下分析": false,
|
||||
"分析笔": true,
|
||||
"分析线段": true,
|
||||
"分析扩展线段": true,
|
||||
"分析笔中枢": true,
|
||||
"分析线段中枢": true,
|
||||
"手动终止": "",
|
||||
"计算指标": true,
|
||||
"计算BOLL": false,
|
||||
"指标计算方式": "收",
|
||||
"平滑异同移动平均线_快线周期": 13,
|
||||
"平滑异同移动平均线_慢线周期": 31,
|
||||
"平滑异同移动平均线_信号周期": 11,
|
||||
"MACD_参数列表": [],
|
||||
"相对强弱指数_周期": 13,
|
||||
"相对强弱指数_移动平均线周期": 13,
|
||||
"相对强弱指数_超买阈值": 75.0,
|
||||
"相对强弱指数_超卖阈值": 25.0,
|
||||
"RSI_周期列表": [],
|
||||
"随机指标_RSV周期": 13,
|
||||
"随机指标_K值平滑周期": 5,
|
||||
"随机指标_D值平滑周期": 5,
|
||||
"随机指标_超买阈值": 80.0,
|
||||
"随机指标_超卖阈值": 20.0,
|
||||
"KDJ_参数列表": [],
|
||||
"布林带_周期": 20,
|
||||
"布林带_标准差倍数": 2.0,
|
||||
"BOLL_参数列表": [],
|
||||
"均线_类型列表": [],
|
||||
"均线_周期列表": [],
|
||||
"图表展示": true,
|
||||
"推送K线": true,
|
||||
"推送笔": true,
|
||||
"推送线段": true,
|
||||
"推送中枢": true,
|
||||
"图表展示_笔": true,
|
||||
"图表展示_线段": true,
|
||||
"图表展示_扩展线段": true,
|
||||
"图表展示_扩展线段_线段": true,
|
||||
"图表展示_线段_线段": true,
|
||||
"图表展示_中枢_笔": true,
|
||||
"图表展示_中枢_线段": true,
|
||||
"图表展示_中枢_扩展线段": true,
|
||||
"图表展示_中枢_扩展线段_线段": true,
|
||||
"图表展示_中枢_线段_线段": true,
|
||||
"图表展示_中枢_线段内部": true,
|
||||
"买卖点偏移": 1,
|
||||
"买卖点激进识别": false,
|
||||
"买卖点与MACD柱强相关": false,
|
||||
"买卖点错过误差值": 0.01,
|
||||
"买卖点_指标模式": "配置",
|
||||
"买卖点_指标匹配_MACD": true,
|
||||
"买卖点_指标匹配_KDJ": true,
|
||||
"买卖点_指标匹配_RSI": true,
|
||||
"买卖点_背离率": "Infinity",
|
||||
"买卖点_T2_回调阈值": 1.0,
|
||||
"买卖点_T2S_最大层级": 3,
|
||||
"买卖点_峰值条件": false,
|
||||
"买卖点_计算方式": "峰",
|
||||
"买卖点_计算线段BSP1": true,
|
||||
"买卖点_处理BSP2": true,
|
||||
"买卖点_计算线段BSP3": true,
|
||||
"买卖点_依赖T1": true,
|
||||
"买卖点_中枢来源": "合",
|
||||
"买卖点_调试输出": false,
|
||||
"线段内部背驰_MACD": true,
|
||||
"线段内部背驰_斜率": true,
|
||||
"线段内部背驰_测度": true,
|
||||
"线段内部背驰_模式": "相对",
|
||||
"加载文件路径": ""
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
> **请修改上述 JSON 为您实际使用的配置值**,或直接粘贴通过 `config.to_json()` 输出的 JSON。
|
||||
|
||||
## 上下文
|
||||
|
||||
- [ ] 此 Bug 在 `chan.py` (Python 参考实现) 中也存在吗?
|
||||
- Python 版行为:
|
||||
- [ ] 此 Bug 是否与特定数据文件相关?
|
||||
- 数据文件名/时间范围:
|
||||
|
||||
## 日志 / Panic 输出
|
||||
|
||||
<details>
|
||||
<summary>展开查看详细输出</summary>
|
||||
|
||||
```
|
||||
在此粘贴日志或 panic 输出
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 补充信息
|
||||
|
||||
任何其他有助于理解此 Bug 的上下文、截图或补充说明。
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: 自定义问题
|
||||
about: 其他问题(问题咨询、文档改进、重构建议等)
|
||||
title: "[Question] "
|
||||
labels: question
|
||||
assignees: []
|
||||
---
|
||||
|
||||
## 问题概述
|
||||
|
||||
请描述您的需求。
|
||||
|
||||
## 问题类型
|
||||
|
||||
- [ ] 问题咨询 — 对 API 或算法的使用存在疑问
|
||||
- [ ] 文档 — 文档错误、缺失或改进建议
|
||||
- [ ] 重构 — 代码结构或设计调整建议
|
||||
- [ ] 兼容性 — Python 绑定层与 `chan.py` 的行为差异
|
||||
- [ ] 性能 — 运行效率或内存占用问题
|
||||
- [ ] 其他
|
||||
|
||||
## 涉及范围
|
||||
|
||||
> 可选择一项或多项。
|
||||
|
||||
| 层次 | 模块 |
|
||||
|------|------|
|
||||
| 核心层 | `types` / `kline` / `indicators` / `algorithm` / `structure` / `business` / `config` |
|
||||
| 绑定层 | `chanlun-py` (`src/business_py.rs` / `src/config_py.rs` / `src/structure_py.rs`) |
|
||||
| 测试 | `chanlun/src/*/tests` / `chanlun-py/tests/test_all.py` |
|
||||
| 文档 | `chanlun/README.md` / `CLAUDE.md` / 其他 |
|
||||
| 其他 | |
|
||||
|
||||
## 补充信息
|
||||
|
||||
任何有助于更好理解或解决此问题的信息。
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
name: 功能请求
|
||||
about: 提出一个新的功能或增强建议
|
||||
title: "[Feature] "
|
||||
labels: enhancement
|
||||
assignees: []
|
||||
---
|
||||
|
||||
## 动机
|
||||
|
||||
请描述这个功能要解决什么问题,或者满足什么使用场景。
|
||||
|
||||
## 提案
|
||||
|
||||
请描述您期望的功能或 API。
|
||||
|
||||
**Rust 核心层:**
|
||||
|
||||
```rust
|
||||
// 期望的 API 或行为
|
||||
```
|
||||
|
||||
**Python 绑定层 (如适用):**
|
||||
|
||||
```python
|
||||
# 期望的 API 或行为
|
||||
```
|
||||
|
||||
## 替代方案
|
||||
|
||||
是否有其他替代方案或现有机制可以满足需求?如果有,请描述。
|
||||
|
||||
## 与 chan.py 的关系
|
||||
|
||||
- [ ] `chan.py` (Python 参考实现) 中已有此功能
|
||||
- 相关代码位置: `chan.py` 行号或方法名
|
||||
- [ ] 这是绑定层 (`chanlun-py`) 的功能需求
|
||||
- [ ] 这是核心层 (`chanlun`) 的算法需求
|
||||
- [ ] 这是全新的功能提案
|
||||
|
||||
## 影响范围
|
||||
|
||||
> 请勾选可能受影响的模块。
|
||||
|
||||
- [ ] 类型定义 (`types/`)
|
||||
- [ ] K线层 (`kline/`)
|
||||
- [ ] 技术指标 (`indicators/`)
|
||||
- [ ] 笔划分 (`algorithm/bi`)
|
||||
- [ ] 线段划分 (`algorithm/segment`)
|
||||
- [ ] 中枢识别 (`algorithm/hub`)
|
||||
- [ ] 背驰检测 (`algorithm/divergence`)
|
||||
- [ ] 结构体 (`structure/`)
|
||||
- [ ] 观察者 (`business/observer`)
|
||||
- [ ] 买卖点 (`business/bsp`)
|
||||
- [ ] K线合成器 (`business/synthesizer`)
|
||||
- [ ] 立体分析器 (`business/multi_frame`)
|
||||
- [ ] 配置 (`config`)
|
||||
- [ ] Python 绑定 (`chanlun-py`)
|
||||
|
||||
## 补充信息
|
||||
|
||||
任何参考链接、图表、伪代码或其他有助于说明该功能的内容。
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
name: Pull Request
|
||||
about: 提交代码变更
|
||||
title: ""
|
||||
labels: []
|
||||
assignees: []
|
||||
---
|
||||
|
||||
## 描述
|
||||
|
||||
请简洁清晰地描述这个 PR 做了什么。
|
||||
|
||||
## 变更类型
|
||||
|
||||
- [ ] Bug 修复
|
||||
- [ ] 新功能
|
||||
- [ ] 重构 / 代码清理
|
||||
- [ ] 文档更新
|
||||
- [ ] 测试
|
||||
- [ ] 其他
|
||||
|
||||
## 变更范围
|
||||
|
||||
> 勾选涉及的文件或模块。
|
||||
|
||||
**核心层 (`chanlun/`):**
|
||||
|
||||
- [ ] `types/` — 基础类型
|
||||
- [ ] `kline/` — K线层
|
||||
- [ ] `indicators/` — 技术指标
|
||||
- [ ] `algorithm/bi` — 笔划分
|
||||
- [ ] `algorithm/segment` — 线段划分
|
||||
- [ ] `algorithm/hub` — 中枢识别
|
||||
- [ ] `algorithm/divergence` — 背驰检测
|
||||
- [ ] `structure/` — 结构体
|
||||
- [ ] `business/observer` — 观察者
|
||||
- [ ] `business/bsp` — 买卖点
|
||||
- [ ] `business/synthesizer` — K线合成器
|
||||
- [ ] `business/multi_frame` — 立体分析器
|
||||
- [ ] `config` — 配置
|
||||
|
||||
**绑定层 (`chanlun-py/`):**
|
||||
|
||||
- [ ] `src/lib.rs` — 模块注册
|
||||
- [ ] `src/business_py.rs` — 业务绑定
|
||||
- [ ] `src/config_py.rs` — 配置绑定
|
||||
- [ ] `src/structure_py.rs` — 结构体绑定
|
||||
|
||||
**其他:**
|
||||
|
||||
- [ ] 测试 (`chanlun/src/*/tests` 或 `chanlun-py/tests/`)
|
||||
- [ ] 文档 (`README.md` / `CLAUDE.md` / `.github/`)
|
||||
|
||||
## 测试
|
||||
|
||||
- [ ] 核心层测试通过 (`cargo test`)
|
||||
- [ ] 绑定层测试通过 (`python3 -m pytest chanlun-py/tests/test_all.py -v`)
|
||||
- [ ] `cargo clippy` 零警告
|
||||
- [ ] 与 `chan.py` 输出一致 (双端对比)
|
||||
- [ ] 新增了相关测试
|
||||
- [ ] 无新增测试(请说明原因):
|
||||
|
||||
## 破坏性变更
|
||||
|
||||
- [ ] 是(请在下文描述迁移步骤)
|
||||
- [ ] 否
|
||||
|
||||
## 补充信息
|
||||
|
||||
任何有助于审查者理解此 PR 的截图、日志或对比数据。
|
||||
+126
-49
@@ -1,5 +1,8 @@
|
||||
name: 构建发布
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
@@ -13,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
|
||||
|
||||
@@ -31,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
|
||||
@@ -109,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
|
||||
@@ -131,7 +183,7 @@ jobs:
|
||||
# 3. 构建 wheel — macOS (x86_64 + arm64)
|
||||
# ============================================================
|
||||
macos:
|
||||
needs: [publish-crates]
|
||||
needs: [check-version]
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -147,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
|
||||
@@ -168,7 +230,7 @@ jobs:
|
||||
# 4. 构建 wheel — Windows x86_64
|
||||
# ============================================================
|
||||
windows:
|
||||
needs: [publish-crates]
|
||||
needs: [check-version]
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -184,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
|
||||
@@ -205,7 +279,7 @@ jobs:
|
||||
# 5. 源码分发包 (sdist)
|
||||
# ============================================================
|
||||
sdist:
|
||||
needs: [publish-crates]
|
||||
needs: [check-version]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -215,6 +289,9 @@ jobs:
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: 安装 Rust 工具链
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: 构建 sdist
|
||||
uses: PyO3/maturin-action@v1
|
||||
with:
|
||||
@@ -232,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:
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
|
||||
# 贡献者公约
|
||||
|
||||
## 我们的承诺
|
||||
|
||||
我们承诺使我们的社区对所有人保持友好、安全且公平。
|
||||
|
||||
我们承诺营造一个尊重并促进每位个体尊严、权利与贡献的环境,无论其种族、民族、种姓、肤色、年龄、体貌特征、神经多样性、残疾状况、生理性别或社会性别、性别认同或表达、性取向、语言、哲学或宗教信仰、国籍或社会出身、社会经济地位、教育程度或其他身份属性。所有真诚参与并遵守本公约者,均享有同等的参与权利。
|
||||
|
||||
|
||||
## 受鼓励的行为
|
||||
|
||||
虽然各自的社会规范可能有差异,但我们都努力达到这个社区对积极行为的期待。我们也了解,因为文化、背景或母语的不同,别人对我们的言行的解读可能不同于我们的初衷。
|
||||
|
||||
考虑到以上这些,我们承诺以审慎的态度彼此相待,并以践行以下共同价值为行为准则:
|
||||
|
||||
1. 尊重我们**社区的宗旨**、各项活动及集会方式
|
||||
2. 以**善意与诚实**的态度与他人互动
|
||||
3. 尊重**不同的观点与经历**
|
||||
4. 对自己的言行及贡献**负责**
|
||||
5. 以得体的方式给予并接受**建设性意见**
|
||||
6. 承诺在造成伤害时进行**弥补**
|
||||
7. 采取其他有益于**社区福祉**的行为
|
||||
|
||||
|
||||
## 受限制的行为
|
||||
|
||||
我们同意在社区内限制以下行为。出现这些行为、威胁实施这些行为、或宣传这些行为,均视为违反本行为准则。
|
||||
|
||||
1. **骚扰**:在明确表达界限后仍侵犯这些界限,或在被清楚要求停止后,仍进行不必要的个人关注。
|
||||
2. **人身攻击**:针对社区成员或群体发表侮辱、贬低或带有蔑视性的言论。
|
||||
3. **刻板印象或歧视**:基于无法改变的身份或特征,来评判他人的性格或行为。
|
||||
4. **性化**:做出在社区场景或宗旨下普遍认为不恰当的亲密举动。
|
||||
5. **侵犯保密性**:未经允许分享或利用他人的个人或隐私信息。
|
||||
6. **危害行为**:对任何人或群体实施、煽动或威胁施加暴力及其他伤害。
|
||||
7. 其他威胁**社区福祉**的行为。
|
||||
|
||||
|
||||
### 其他限制行为
|
||||
|
||||
1. **虚假身份**:出于任何原因冒充他人,或假扮他人以规避监管措施。
|
||||
2. **未正确标明来源**:未正确标明所贡献内容的来源。
|
||||
3. **宣传材料**:以不符合社区规范的方式分享营销或其他商业内容。
|
||||
4. **不当传播**:未能以负责任的方式呈现包含、链接或描述任何其他受限制行为的内容。
|
||||
|
||||
|
||||
## 通报问题
|
||||
|
||||
即使社区成员之间尽其所能地合作,也仍然可能发生矛盾。并不是所有冲突都涉及违反行为准则,本准则旨在强化受鼓励的行为与规范,它们有助于预防冲突并将伤害降到最低。
|
||||
|
||||
当事件发生时,及时报告非常重要。要报告可能的违规行为,**youwukuncheng@163.com**。
|
||||
|
||||
社区管理员会严肃对待违规报告,并尽力及时回应。将对所有违反行为准则的报告展开调查,方式包括查阅消息、日志及录音,或访谈证人及其他相关参与人。社区管理员在优先保障安全与保密性的前提下,会尽可能保持调查与执行过程的透明度。为践行这些价值观,执行措施会在涉事各方的私下环境中进行,但如经各方同意,将事件通报全体社区也可以作为解决方案的一部分。
|
||||
|
||||
|
||||
## 处理与弥补伤害
|
||||
|
||||
**[注意:下文所列的处理办法与补救措施,是基于行为准则执行过程中的最佳做法而提出的建议。如果你们的社区已经有既定的执行流程,请确保修改本段内容,以描述你们自己的政策。]**
|
||||
|
||||
若社区管理员经调查确认存在违反此行为准则的行为,将参照以下"分级处理"机制,根据事件对相关人员及社区整体造成的影响程度,确定最适宜的伤害弥补方案。根据违规严重程度,可跳过较低级别的处理措施。
|
||||
|
||||
1) 警告
|
||||
1) **事件**:单次或连续违规行为
|
||||
2) **后果**:社区管理员将发出书面私信警告
|
||||
3) **弥补**:弥补的方式如书面私下致歉、坦承自己的责任,或主动确认清楚今后该如何做才符合期待。
|
||||
2) 暂时限制活动
|
||||
1) **事件**:重复造成先前已被警告的违规,或首次发生略为严重的违规行为。
|
||||
2) **后果**:发出私下的书面警告,并设定一个有时间限制的冷静期,旨在强调事态的严重性,并让相关社区成员有时间消化与处理该事件。冷静期可能是限制在特定的交流渠道,或限制与特定社区成员的互动。
|
||||
3) **弥补**:修复的方式可能包括道歉、利用冷静期反思自身行为及其影响,以及充分意识到在冷静期结束后如何重新进入社区空间。
|
||||
3) 暂时停权
|
||||
1) **事件**:出现社区管理员已多次警告后仍然重复违规的模式,或一次严重违规行为。
|
||||
2) **后果**:发出私下的书面警告,并附上恢复权限所需满足的条件。通常,临时停权旨在给予被停权者时间,反思其行为以及考虑可能的改正措施。
|
||||
3) **弥补**:弥补的条件包括尊重停权的意旨、达成恢复权限的指定条件,以及充分认识到在停权解除后如何重新融入社区。
|
||||
4) 永久封禁
|
||||
1) **事件**:多次违反行为准则,且其他分级处理措施均未能解决问题,或发生严重到社区管理员认定无法在该成员继续存在的情况下保障社区安全的违规行为。
|
||||
2) **后果**:撤销其对所有社区空间、工具及交流渠道的访问权限。一般而言,永久封禁应极少使用,必须有充分且有力的理由,且仅在其他弥补手段未能改变其行为时才作为最后手段实施。
|
||||
3) **弥补**:此类严重情形下,不存在可行的弥补途径。
|
||||
|
||||
本分级处理措施旨在作为指导方针,并不限制社区管理者在符合社区最大利益的前提下,运用其自主裁量权与判断力。
|
||||
|
||||
|
||||
## 施行范围
|
||||
|
||||
本行为准则适用于社区所有空间,同时也适用于个人在公共场合或其他场合正式代表社区的情况。代表社区的行为包括但不限于:使用官方电子邮件地址;通过官方社交媒体账号发布内容;作为指定代表出席线上或线下活动。
|
||||
|
||||
|
||||
## 贡献归属
|
||||
|
||||
本行为准则改编自 贡献者公约 3.0 版,该公约永久可在此查阅:[https://www.contributor-covenant.org/version/3/0/](https://www.contributor-covenant.org/version/3/0/)。
|
||||
|
||||
贡献者公约 由 Organization for Ethical Source 负责维护,并以 CC BY-SA 4.0 许可协议发布。查看该许可协议请访问:[https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/)。
|
||||
|
||||
关于 贡献者公约 的常见问题解答,请参阅:[https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq)。各语言版本译文请见:[https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations)。更多执行与社区指南资源请见:[https://www.contributor-covenant.org/resources](https://www.contributor-covenant.org/resources)。本分级措施的灵感来源于 [Mozilla 行为准则团队](https://github.com/mozilla/inclusion) 的工作。
|
||||
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
# 贡献指南
|
||||
|
||||
感谢你对 `chanlun` 项目的关注!
|
||||
|
||||
本项目将 Python 版缠论技术分析库 (`chan.py`) 完整移植为 Rust,同时通过 PyO3 绑定层保持 Python API 兼容。以下指南旨在帮助平滑贡献流程。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- [角色与分工](#角色与分工)
|
||||
- [开发环境](#开发环境)
|
||||
- [项目结构](#项目结构)
|
||||
- [开发流程](#开发流程)
|
||||
- [代码规范](#代码规范)
|
||||
- [测试指南](#测试指南)
|
||||
- [提交信息](#提交信息)
|
||||
- [双端对齐](#双端对齐)
|
||||
|
||||
---
|
||||
|
||||
## 角色与分工
|
||||
|
||||
| 角色 | 范围 | 联系 |
|
||||
|------|------|------|
|
||||
| 维护者 | 架构决策、代码审查、发布 | @YuWuKunCheng |
|
||||
| 贡献者 | 提交 PR、报告 Bug、改进文档 | 任何人 |
|
||||
|
||||
---
|
||||
|
||||
## 开发环境
|
||||
|
||||
### 必需工具
|
||||
|
||||
| 工具 | 最低版本 | 用途 |
|
||||
|------|---------|------|
|
||||
| Rust | 1.85+ | 核心层编译 |
|
||||
| Python | 3.10+ | 绑定层测试、对比验证 |
|
||||
| maturin | 1.x | PyO3 绑定开发与安装 |
|
||||
|
||||
### 初始化
|
||||
|
||||
```bash
|
||||
# 克隆仓库
|
||||
git clone https://github.com/YuYuKunKun/chanlun.rs.git
|
||||
cd chanlun.rs
|
||||
|
||||
# 核心层
|
||||
cd chanlun
|
||||
cargo build
|
||||
cargo test
|
||||
|
||||
# 绑定层
|
||||
cd ../chanlun-py
|
||||
maturin develop
|
||||
python3 -m pytest tests/test_all.py -v
|
||||
|
||||
# 确保 clippy 零警告
|
||||
cd ../chanlun
|
||||
cargo clippy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
chanlun.rs/
|
||||
├── chan.py # Python 参考实现 (~4200 行)
|
||||
├── chanlun/ # Rust 核心层
|
||||
│ ├── Cargo.toml
|
||||
│ └── src/
|
||||
│ ├── lib.rs # 模块注册
|
||||
│ ├── config.rs # 缠论配置 (62 字段, serde)
|
||||
│ ├── types/ # 基础类型
|
||||
│ ├── kline/ # K线层
|
||||
│ ├── indicators/ # 技术指标
|
||||
│ ├── algorithm/ # 核心算法 (笔/线段/中枢/背驰)
|
||||
│ ├── structure/ # 结构体 (虚线/分型/特征)
|
||||
│ ├── business/ # 业务层 (观察者/合成器/立体分析)
|
||||
│ └── utils/ # 工具
|
||||
├── chanlun-py/ # PyO3 Python 绑定
|
||||
│ ├── Cargo.toml
|
||||
│ ├── src/
|
||||
│ │ ├── lib.rs # 模块注册与导出
|
||||
│ │ ├── business_py.rs # 业务层 Python 封装
|
||||
│ │ ├── config_py.rs # 配置 Python 封装
|
||||
│ │ └── structure_py.rs # 结构体 Python 封装
|
||||
│ ├── chanlun/ # Python 存根模块
|
||||
│ │ └── __init__.py
|
||||
│ └── tests/
|
||||
│ └── test_all.py # 完整测试套件
|
||||
├── CLAUDE.md # AI 辅助开发指令
|
||||
├── .github/ # GitHub 模板
|
||||
│ ├── pull_request_template.md
|
||||
│ └── ISSUE_TEMPLATE/
|
||||
│ ├── bug_report.md
|
||||
│ ├── feature_request.md
|
||||
│ └── custom.md
|
||||
├── README.md
|
||||
├── SECURITY.md
|
||||
└── CODE_OF_CONDUCT.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 开发流程
|
||||
|
||||
### 从 Issue 开始
|
||||
|
||||
1. 查找或创建相关 Issue
|
||||
2. 在 Issue 中讨论方案,达成共识后再开始编码
|
||||
3. 避免在没有 Issue 的情况下提交大型 PR
|
||||
|
||||
### 分支策略
|
||||
|
||||
```bash
|
||||
# 从 develop 分支创建功能分支
|
||||
git checkout develop
|
||||
git pull origin develop
|
||||
git checkout -b feature/your-feature-name
|
||||
|
||||
# 或从 develop 分支创建修复分支
|
||||
git checkout -b fix/your-bug-fix
|
||||
```
|
||||
|
||||
### 提交 PR
|
||||
|
||||
1. 确保所有测试通过
|
||||
2. 确保 `cargo clippy` 零警告
|
||||
3. 推送到你的分支并发起 PR 到 `develop`
|
||||
4. 填写 PR 模板中的所有内容
|
||||
5. 等待审查并响应反馈
|
||||
|
||||
---
|
||||
|
||||
## 代码规范
|
||||
|
||||
### 中文标识符
|
||||
|
||||
所有类型名、方法名、字段名必须使用中文,与 `chan.py` 保持 1:1 对应:
|
||||
|
||||
```rust
|
||||
// ✓ 正确
|
||||
pub struct 缠论K线 { pub 高: SyncF64, pub 低: SyncF64 }
|
||||
pub fn 方向(&self) -> 相对方向 { ... }
|
||||
|
||||
// ✗ 错误 — 不允许英文
|
||||
pub struct ChanKline { pub high: f64 }
|
||||
```
|
||||
|
||||
### 许可证头部
|
||||
|
||||
每个 `.rs` 文件必须以 MIT 许可证头部开始:
|
||||
|
||||
```rust
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2026 YuYuKunKun
|
||||
* ...
|
||||
*/
|
||||
```
|
||||
|
||||
### 代码风格
|
||||
|
||||
- 使用 `cargo fmt` 自动格式化
|
||||
- 遵循 `cargo clippy` 建议(零警告)
|
||||
- 仅写必要注释 — 解释"为什么"而非"做什么"
|
||||
- 不对仅使用一次的代码做抽象
|
||||
- 不添加方案之外的特性和错误处理
|
||||
|
||||
### Rust 相关约定
|
||||
|
||||
- `#![allow(non_snake_case)]` 和 `#![allow(non_camel_case_types)]` 已在 `lib.rs` 中声明
|
||||
- 内部可变性优先用 `AtomicI64`/`AtomicBool`/`SyncF64`,复杂字段用 `RwLock`
|
||||
- `Arc<分型>` 通过 `Arc::as_ptr` 比较身份(而非值比较)
|
||||
- 全局缓存使用 `LazyLock<Mutex<>>`,不使用 `thread_local!`
|
||||
- 读写锁作用域化,防止死锁
|
||||
|
||||
---
|
||||
|
||||
## 测试指南
|
||||
|
||||
### 核心层测试
|
||||
|
||||
```bash
|
||||
cd chanlun
|
||||
cargo test # 运行所有测试
|
||||
cargo test -- <name> # 运行匹配名称的测试
|
||||
```
|
||||
|
||||
测试应覆盖:
|
||||
- 类型构造/字段读写/Clone 后指针一致性
|
||||
- 算法函数的边界情况(空序列、单元素、极端价格)
|
||||
- 流式增量结果与静态重新分析的一致性
|
||||
- `Send + Sync` 编译期断言
|
||||
- 跨线程读写不 panic
|
||||
|
||||
### 绑定层测试
|
||||
|
||||
```bash
|
||||
cd chanlun-py
|
||||
maturin develop
|
||||
python3 -m pytest tests/test_all.py -v
|
||||
```
|
||||
|
||||
测试应覆盖:
|
||||
- Python API 与 `chan.py` 的接口兼容性
|
||||
- 跨线程 `is` 身份一致性
|
||||
- 双端(Rust 绑定 vs `chan.py`)关键算法输出对比
|
||||
|
||||
### 双端对比
|
||||
|
||||
当我们修改算法层代码时,必须验证 Rust 输出与 Python 版一致:
|
||||
|
||||
```python
|
||||
# 典型双端对比模式
|
||||
from chanlun import 观察者 as 观察者Rust
|
||||
from chanlun.chan import 观察者 as 观察者Py
|
||||
|
||||
# 加载同样的数据
|
||||
obs_rust = 观察者Rust("btcusd", 300, config)
|
||||
obs_py = 观察者Py("btcusd", 300, config)
|
||||
|
||||
# 对比结果
|
||||
assert len(obs_rust.笔序列) == len(obs_py.笔序列)
|
||||
assert len(obs_rust.线段序列) == len(obs_py.线段序列)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 提交信息
|
||||
|
||||
使用简洁的中文,格式为:
|
||||
|
||||
```
|
||||
<类型>: <简要描述>
|
||||
|
||||
<详细说明(可选)>
|
||||
|
||||
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||||
```
|
||||
|
||||
类型示例:
|
||||
- `fix:` — Bug 修复
|
||||
- `feat:` — 新功能
|
||||
- `refactor:` — 重构(行为不变)
|
||||
- `test:` — 添加或修改测试
|
||||
- `docs:` — 文档更新
|
||||
- `chore:` — 构建/工具
|
||||
|
||||
所有提交必须以 `Co-Authored-By:` 行结尾,这是本项目对 AI 辅助开发的惯例。
|
||||
|
||||
---
|
||||
|
||||
## 双端对齐
|
||||
|
||||
本项目最核心的质量要求是 Rust 实现与 `chan.py` 行为完全一致。对齐时遵循:
|
||||
|
||||
1. **以 `chan.py` 为准** — Python 实现是 golden source
|
||||
2. **增量对齐** — 优先修复数量差异(笔数、线段数),再深入字段级对齐
|
||||
3. **算法差异分类**:
|
||||
- 核心公式错误:如 MACD 面积计算 `阳+阴` vs `阳+|阴|`
|
||||
- 边界条件遗漏:如 `计算MACD柱子分段` 末尾段未追加
|
||||
- 指针身份 vs 值索引:`position(|k| Arc::as_ptr(k) == ...)` vs `position(|k| k.序号 == ...)`
|
||||
4. **使用测试驱动** — 先写双端对比测试,确认差异存在,再改 Rust 代码对齐
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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` | 信号发现+动态注册 |
|
||||
|
||||
## 兼容性
|
||||
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
# 安全策略
|
||||
|
||||
## 适用范围
|
||||
|
||||
本安全策略适用于 `chanlun` Rust 核心库、`chanlun-py` Python 绑定层,以及本仓库中的相关工具和脚本。
|
||||
|
||||
## 免责声明
|
||||
|
||||
本项目是一个**技术分析库**,用于价格走势的结构化分析。它不存储用户资金、不处理身份认证、不直接发起交易,但反馈任何潜在安全漏洞(如代码层面的崩溃、死锁、不安全内存操作等)仍非常重要。
|
||||
|
||||
## 支持的版本
|
||||
|
||||
| 版本 | 支持状态 |
|
||||
|------|---------|
|
||||
| `26.x` | 积极支持 |
|
||||
| `< 26.0` | 不再支持 |
|
||||
|
||||
## 报告漏洞
|
||||
|
||||
如果您发现了安全漏洞,请**不要**通过公开的 Issue 报告。请通过以下方式私密报告:
|
||||
|
||||
- **邮箱**: youwukuncheng@163.com
|
||||
- **主题**: `[SECURITY] — <简要描述>`
|
||||
|
||||
请在报告中包含:
|
||||
|
||||
1. **漏洞描述** — 清晰描述漏洞的性质
|
||||
2. **复现步骤** — 最小可复现的代码片段或操作序列
|
||||
3. **受影响版本** — 您正在使用的 `chanlun` 版本号
|
||||
4. **潜在影响** — 可能产生的后果(崩溃、数据泄露、死锁等)
|
||||
5. **建议修复** — 如果您有修复建议
|
||||
|
||||
## 处理流程
|
||||
|
||||
收到报告后,我们承诺:
|
||||
|
||||
1. **确认收到** — 3 个工作日内确认收到报告
|
||||
2. **初步评估** — 7 个工作日内完成漏洞严重性评估并通知报告者
|
||||
3. **修复时间线** — 根据严重程度:
|
||||
- 严重(可导致崩溃/死锁/未定义行为): 14 天内发布修复
|
||||
- 中等: 30 天内发布修复
|
||||
- 低风险: 在下一个常规版本中包含修复
|
||||
4. **公开披露** — 修复发布后,在 Release Notes 中致谢报告者(需经同意)
|
||||
|
||||
## 关注领域
|
||||
|
||||
以下类型的漏洞尤其值得关注:
|
||||
|
||||
- `unsafe` 代码块中的内存安全问题
|
||||
- `RwLock` 死锁(读锁中获取写锁)
|
||||
- `LazyLock<Mutex<>>` 全局缓存的锁竞争
|
||||
- `panic!` 导致的未预期崩溃
|
||||
- `Arc` 循环引用造成的内存泄漏
|
||||
- 大端字节序列化 (`to_bytes`/`from_bytes`) 的缓冲区越界
|
||||
- PyO3 FFI 边界的类型转换安全
|
||||
- `AtomicI64` / `SyncF64` 的 `Ordering::Relaxed` 使用是否合理
|
||||
|
||||
## 偏好语言
|
||||
|
||||
请使用简体中文或英文撰写报告。
|
||||
@@ -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` |
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "chanlun-py"
|
||||
version = "26.6.17"
|
||||
version = "26.6.125"
|
||||
edition = "2024"
|
||||
description = "缠论技术分析库 — Rust 高性能 Python 绑定"
|
||||
authors = ["YuYuKunKun"]
|
||||
@@ -12,10 +12,12 @@ crate-type = ["cdylib"]
|
||||
name = "chanlun"
|
||||
|
||||
[dependencies]
|
||||
chanlun = "26.6.1" # { path = "../chanlun" }
|
||||
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"
|
||||
|
||||
@@ -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)、parse(MIT)、termcolor(MIT)。
|
||||
|
||||
+68
-8
@@ -5,13 +5,29 @@ 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]: ...
|
||||
def 缠论K线相等(A: 缠论K线, B: 缠论K线, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
|
||||
def 分型相等(A: 分型, B: 分型, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
|
||||
def 缺口相等(A: 缺口, B: 缺口, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
|
||||
def 线段特征相等(A: 线段特征, B: 线段特征, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
|
||||
def 中枢相等(A: 中枢, B: 中枢, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
|
||||
def 虚线相等(A: 虚线, B: 虚线, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
|
||||
def 观察者相等(A: 观察者, B: 观察者, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
|
||||
def 立体分析器相等(A: 立体分析器, B: 立体分析器, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
|
||||
|
||||
# ========== Enum-like types ==========
|
||||
|
||||
@@ -66,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: ...
|
||||
@@ -317,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: ...
|
||||
@@ -429,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
|
||||
@@ -517,6 +536,8 @@ class 虚线:
|
||||
class 线段特征:
|
||||
@property
|
||||
def 序号(self) -> int: ...
|
||||
@序号.setter
|
||||
def 序号(self, value: int) -> None: ...
|
||||
@property
|
||||
def 标识(self) -> str: ...
|
||||
@标识.setter
|
||||
@@ -561,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
|
||||
@@ -784,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: ...
|
||||
@@ -799,16 +856,17 @@ class K线合成器:
|
||||
def 标识(self) -> str: ...
|
||||
@property
|
||||
def 周期组(self) -> List[int]: ...
|
||||
def 投喂K线(self, 普K: K线) -> List[Tuple[int, K线]]: ...
|
||||
def 投喂(self, 时间戳: int, 开: float, 高: float, 低: float, 收: float, 量: float) -> List[Tuple[int, K线]]: ...
|
||||
def 投喂K线(self, 普K: K线) -> None: ...
|
||||
def 投喂(self, 时间戳: int, 开: float, 高: float, 低: float, 收: float, 量: float) -> None: ...
|
||||
def 获取当前K线(self, 周期: int) -> Optional[K线]: ...
|
||||
|
||||
class 立体分析器:
|
||||
def __init__(self, 符号: str, 周期组: List[int], 配置: Optional[缠论配置] = None, 配置组: Optional[Dict[int, 缠论配置]] = None) -> None: ...
|
||||
@property
|
||||
def 周期组(self) -> List[int]: ...
|
||||
@property
|
||||
def _单体分析器(self) -> Dict[int, 观察者]: ...
|
||||
def 投喂K线(self, 普K: K线) -> None: ...
|
||||
def 获取观察者(self, 周期: int) -> Optional[观察者]: ...
|
||||
def 测试_保存数据(self, root: Optional[str] = None) -> None: ...
|
||||
|
||||
# ========== 缠论配置 (fields via __getattr__/__setattr__) ==========
|
||||
@@ -818,7 +876,8 @@ class 缠论配置:
|
||||
def to_dict(self) -> Dict[str, Any]: ...
|
||||
def to_json(self) -> str: ...
|
||||
def 保存配置(self, path: str = "缠论配置.json") -> None: ...
|
||||
def 对比(self, other: 缠论配置) -> Dict[str, Tuple[Any, Any]]: ...
|
||||
def 对比(self, other: 缠论配置) -> Dict[str, Any]: ...
|
||||
def model_copy(self, update: Optional[Dict[str, Any]] = None) -> 缠论配置: ...
|
||||
@classmethod
|
||||
def 加载配置(cls, path: str = "缠论配置.json") -> 缠论配置: ...
|
||||
@classmethod
|
||||
@@ -827,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: ...
|
||||
|
||||
@@ -14,7 +14,6 @@ __all__ = [
|
||||
"指标容器",
|
||||
"指标计算器",
|
||||
"均线工具",
|
||||
"测试_读取数据",
|
||||
"相对强弱指数",
|
||||
"相对方向",
|
||||
"立体分析器",
|
||||
@@ -33,17 +32,73 @@ __all__ = [
|
||||
"布林带",
|
||||
"get_分型模式",
|
||||
"set_分型模式",
|
||||
"get_扩展线段模式",
|
||||
"set_扩展线段模式",
|
||||
"get_log_level",
|
||||
"set_log_level",
|
||||
"get_rs_log_level",
|
||||
"set_rs_log_level",
|
||||
"chan",
|
||||
"K线相等",
|
||||
"缠论K线相等",
|
||||
"分型相等",
|
||||
"缺口相等",
|
||||
"线段特征相等",
|
||||
"中枢相等",
|
||||
"虚线相等",
|
||||
]
|
||||
|
||||
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):
|
||||
|
||||
@@ -5,13 +5,29 @@ 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]: ...
|
||||
def 缠论K线相等(A: 缠论K线, B: 缠论K线, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
|
||||
def 分型相等(A: 分型, B: 分型, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
|
||||
def 缺口相等(A: 缺口, B: 缺口, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
|
||||
def 线段特征相等(A: 线段特征, B: 线段特征, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
|
||||
def 中枢相等(A: 中枢, B: 中枢, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
|
||||
def 虚线相等(A: 虚线, B: 虚线, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
|
||||
def 观察者相等(A: 观察者, B: 观察者, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
|
||||
def 立体分析器相等(A: 立体分析器, B: 立体分析器, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
|
||||
|
||||
# ========== Enum-like types ==========
|
||||
|
||||
@@ -66,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: ...
|
||||
@@ -317,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: ...
|
||||
@@ -429,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
|
||||
@@ -517,6 +536,8 @@ class 虚线:
|
||||
class 线段特征:
|
||||
@property
|
||||
def 序号(self) -> int: ...
|
||||
@序号.setter
|
||||
def 序号(self, value: int) -> None: ...
|
||||
@property
|
||||
def 标识(self) -> str: ...
|
||||
@标识.setter
|
||||
@@ -561,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
|
||||
@@ -784,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: ...
|
||||
@@ -799,18 +856,159 @@ class K线合成器:
|
||||
def 标识(self) -> str: ...
|
||||
@property
|
||||
def 周期组(self) -> List[int]: ...
|
||||
def 投喂K线(self, 普K: K线) -> List[Tuple[int, K线]]: ...
|
||||
def 投喂(self, 时间戳: int, 开: float, 高: float, 低: float, 收: float, 量: float) -> List[Tuple[int, K线]]: ...
|
||||
def 投喂K线(self, 普K: K线) -> None: ...
|
||||
def 投喂(self, 时间戳: int, 开: float, 高: float, 低: float, 收: float, 量: float) -> None: ...
|
||||
def 获取当前K线(self, 周期: int) -> Optional[K线]: ...
|
||||
|
||||
class 立体分析器:
|
||||
def __init__(self, 符号: str, 周期组: List[int], 配置: Optional[缠论配置] = None, 配置组: Optional[Dict[int, 缠论配置]] = None) -> None: ...
|
||||
@property
|
||||
def 周期组(self) -> List[int]: ...
|
||||
@property
|
||||
def _单体分析器(self) -> Dict[int, 观察者]: ...
|
||||
def 投喂K线(self, 普K: K线) -> None: ...
|
||||
def 获取观察者(self, 周期: int) -> Optional[观察者]: ...
|
||||
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 缠论配置:
|
||||
@@ -818,7 +1016,8 @@ class 缠论配置:
|
||||
def to_dict(self) -> Dict[str, Any]: ...
|
||||
def to_json(self) -> str: ...
|
||||
def 保存配置(self, path: str = "缠论配置.json") -> None: ...
|
||||
def 对比(self, other: 缠论配置) -> Dict[str, Tuple[Any, Any]]: ...
|
||||
def 对比(self, other: 缠论配置) -> Dict[str, Any]: ...
|
||||
def model_copy(self, update: Optional[Dict[str, Any]] = None) -> 缠论配置: ...
|
||||
@classmethod
|
||||
def 加载配置(cls, path: str = "缠论配置.json") -> 缠论配置: ...
|
||||
@classmethod
|
||||
@@ -827,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: ...
|
||||
|
||||
+2345
-430
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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: ...
|
||||
@@ -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
|
||||
@@ -0,0 +1,14 @@
|
||||
"""缠论技术分析库 — 信号函数模块
|
||||
|
||||
每个信号函数接收 观察者 对象 + 关键字参数,返回 OrderedDict。
|
||||
信号 key 格式:k1_k2_k3,value 格式:v1_v2_v3_score。
|
||||
"""
|
||||
|
||||
from chanlun.signals._template import 模板_V日期
|
||||
from chanlun.signals.demo import tas_ma_base_V230313
|
||||
from chanlun.signals.demo import tas_macd_direct_V221106
|
||||
from chanlun.signals.demo import macd_金叉
|
||||
from chanlun.signals.demo import cxt_bi_end_V230222
|
||||
from chanlun.signals.demo import cxt_停顿分型_V230106
|
||||
from chanlun.signals.demo import bar_zdt_V230331
|
||||
from chanlun.signals.youwukuncheng import *
|
||||
@@ -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) ##
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "chanlun"
|
||||
version = "2606.17"
|
||||
version = "2606.125"
|
||||
description = "缠论技术分析库 — Rust 高性能实现"
|
||||
readme = { file = "README.md", content-type = "text/markdown" }
|
||||
license = { file = "LICENSE", content-type = "text/plain" }
|
||||
@@ -28,6 +28,7 @@ classifiers = [
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"termcolor>=3.0",
|
||||
"parse>=1.2",
|
||||
"loguru>=0.6",
|
||||
"backtrader==1.9.78.123",
|
||||
]
|
||||
|
||||
+337
-99
@@ -25,34 +25,21 @@
|
||||
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, PyType};
|
||||
use std::collections::HashMap;
|
||||
use pyo3::types::{PyDict, PyList, PyType};
|
||||
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,
|
||||
&模式,
|
||||
)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,11 +467,13 @@ impl 笔Py {
|
||||
#[classmethod]
|
||||
#[pyo3(signature = (当前分型, 分型序列, 笔序列, 缠K序列, 普K序列, 递归层次, 配置))]
|
||||
/// 笔划分核心递归算法
|
||||
/// 分型序列/笔序列 原地修改(与 chan.py 行为一致)
|
||||
/// :return: 递归层次
|
||||
fn 分析(
|
||||
_cls: &Bound<'_, PyType>,
|
||||
当前分型: Option<&Bound<'_, 分型Py>>,
|
||||
分型序列: Vec<Py<分型Py>>,
|
||||
笔序列: Vec<Py<虚线Py>>,
|
||||
分型序列: &Bound<'_, PyList>,
|
||||
笔序列: &Bound<'_, PyList>,
|
||||
缠K序列: Vec<Py<crate::kline_py::缠论K线Py>>,
|
||||
普K序列: Vec<Py<K线Py>>,
|
||||
递归层次: i64,
|
||||
@@ -321,14 +482,19 @@ impl 笔Py {
|
||||
) -> PyResult<i64> {
|
||||
let _ = 递归层次; // Python API 兼容参数,核心从0开始计数
|
||||
let 当前分型_rc = 当前分型.map(|f| Arc::clone(&f.borrow().inner));
|
||||
let mut fr_seq: Vec<Arc<chanlun::structure::fractal_obj::分型>> = 分型序列
|
||||
.iter()
|
||||
.map(|f| Arc::clone(&f.bind(py).borrow().inner))
|
||||
.collect();
|
||||
let mut bi_seq: Vec<Arc<chanlun::structure::dash_line::虚线>> = 笔序列
|
||||
.iter()
|
||||
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
|
||||
.collect();
|
||||
|
||||
// 从 Python 列表提取
|
||||
let mut fr_seq = Vec::with_capacity(分型序列.len());
|
||||
for item in 分型序列.iter() {
|
||||
let f: PyRef<'_, 分型Py> = item.extract()?;
|
||||
fr_seq.push(Arc::clone(&f.inner));
|
||||
}
|
||||
let mut bi_seq = Vec::with_capacity(笔序列.len());
|
||||
for item in 笔序列.iter() {
|
||||
let d: PyRef<'_, 虚线Py> = item.extract()?;
|
||||
bi_seq.push(Arc::clone(&d.inner));
|
||||
}
|
||||
|
||||
let ck_list: Vec<Arc<chanlun::kline::chan_kline::缠论K线>> = 缠K序列
|
||||
.iter()
|
||||
.map(|k| Arc::clone(&k.bind(py).borrow().inner))
|
||||
@@ -338,8 +504,8 @@ impl 笔Py {
|
||||
.map(|k| k.bind(py).borrow().inner.clone())
|
||||
.collect();
|
||||
let config = 配置.borrow().to_rust_config(py)?;
|
||||
match 当前分型_rc {
|
||||
Some(fr) => Ok(chanlun::algorithm::bi::笔::分析(
|
||||
let depth = py.detach(|| match 当前分型_rc {
|
||||
Some(fr) => chanlun::algorithm::bi::笔::分析(
|
||||
fr,
|
||||
&mut fr_seq,
|
||||
&mut bi_seq,
|
||||
@@ -347,9 +513,25 @@ impl 笔Py {
|
||||
&bar_list,
|
||||
递归层次,
|
||||
&config,
|
||||
)),
|
||||
None => Ok(递归层次),
|
||||
}
|
||||
),
|
||||
None => 递归层次,
|
||||
});
|
||||
|
||||
// 写回 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")?;
|
||||
分型序列.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")?;
|
||||
笔序列.call_method1("extend", (PyList::new(py, &bi_items)?,))?;
|
||||
|
||||
Ok(depth)
|
||||
}
|
||||
|
||||
#[classmethod]
|
||||
@@ -468,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)
|
||||
@@ -502,11 +687,13 @@ impl 线段Py {
|
||||
}
|
||||
|
||||
#[classmethod]
|
||||
#[pyo3(signature = (笔序列, 线段序列, 配置, 层级 = 0, 关系序列 = None))]
|
||||
/// 线段划分核心递归算法
|
||||
/// 线段序列 原地修改(与 chan.py 行为一致)
|
||||
fn 分析(
|
||||
_cls: &Bound<'_, PyType>,
|
||||
笔序列: Vec<Py<虚线Py>>,
|
||||
线段序列: Vec<Py<虚线Py>>,
|
||||
线段序列: &Bound<'_, PyList>,
|
||||
配置: &Bound<'_, 缠论配置Py>,
|
||||
层级: i64,
|
||||
关系序列: Option<Vec<相对方向Py>>,
|
||||
@@ -516,10 +703,13 @@ impl 线段Py {
|
||||
.iter()
|
||||
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
|
||||
.collect();
|
||||
let mut seg_seq: Vec<Arc<chanlun::structure::dash_line::虚线>> = 线段序列
|
||||
.iter()
|
||||
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
|
||||
.collect();
|
||||
|
||||
let mut seg_seq = Vec::with_capacity(线段序列.len());
|
||||
for item in 线段序列.iter() {
|
||||
let d: PyRef<'_, 虚线Py> = item.extract()?;
|
||||
seg_seq.push(Arc::clone(&d.inner));
|
||||
}
|
||||
|
||||
let config = 配置.borrow().to_rust_config(py)?;
|
||||
let default_rel = vec![
|
||||
chanlun::types::相对方向::向上,
|
||||
@@ -528,22 +718,33 @@ 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 列表 (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")?;
|
||||
线段序列.call_method1("extend", (PyList::new(py, &items)?,))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[classmethod]
|
||||
/// 即同级别分析
|
||||
/// 线段序列 原地修改(与 chan.py 行为一致)
|
||||
fn 扩展分析(
|
||||
_cls: &Bound<'_, PyType>,
|
||||
虚线序列: Vec<Py<虚线Py>>,
|
||||
线段序列: Vec<Py<虚线Py>>,
|
||||
线段序列: &Bound<'_, PyList>,
|
||||
配置: &Bound<'_, 缠论配置Py>,
|
||||
py: Python<'_>,
|
||||
) -> PyResult<()> {
|
||||
@@ -551,12 +752,25 @@ impl 线段Py {
|
||||
.iter()
|
||||
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
|
||||
.collect();
|
||||
let mut seg_seq: Vec<Arc<chanlun::structure::dash_line::虚线>> = 线段序列
|
||||
.iter()
|
||||
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
|
||||
.collect();
|
||||
|
||||
let mut seg_seq = Vec::with_capacity(线段序列.len());
|
||||
for item in 线段序列.iter() {
|
||||
let d: PyRef<'_, 虚线Py> = item.extract()?;
|
||||
seg_seq.push(Arc::clone(&d.inner));
|
||||
}
|
||||
|
||||
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 列表 (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")?;
|
||||
线段序列.call_method1("extend", (PyList::new(py, &items)?,))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -674,7 +888,7 @@ impl 中枢Py {
|
||||
|
||||
#[getter]
|
||||
fn 标识(&self) -> String {
|
||||
self.inner.标识.read().unwrap().clone()
|
||||
self.inner.标识.read().clone()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
@@ -685,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())
|
||||
@@ -696,7 +910,6 @@ impl 中枢Py {
|
||||
self.inner
|
||||
.第三买卖线
|
||||
.read()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.map(|d| dashed_to_py(py, Arc::clone(d)))
|
||||
}
|
||||
@@ -706,7 +919,6 @@ impl 中枢Py {
|
||||
self.inner
|
||||
.本级_第三买卖线
|
||||
.read()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.map(|d| dashed_to_py(py, Arc::clone(d)))
|
||||
}
|
||||
@@ -792,18 +1004,30 @@ impl 中枢Py {
|
||||
}
|
||||
|
||||
/// 当基础序列>=9时,从中枢中提取扩展线段中枢
|
||||
/// 扩展中枢 原地修改(与 chan.py 行为一致)
|
||||
fn 获取扩展中枢(
|
||||
&self,
|
||||
扩展中枢: Vec<Py<Self>>,
|
||||
扩展中枢: &Bound<'_, PyList>,
|
||||
配置: &Bound<'_, crate::config_py::缠论配置Py>,
|
||||
py: Python<'_>,
|
||||
) -> PyResult<()> {
|
||||
let mut hub_seq: Vec<Arc<chanlun::algorithm::hub::中枢>> = 扩展中枢
|
||||
.iter()
|
||||
.map(|h| Arc::clone(&h.bind(py).borrow().inner))
|
||||
.collect();
|
||||
let mut hub_seq = Vec::with_capacity(扩展中枢.len());
|
||||
for item in 扩展中枢.iter() {
|
||||
let h: PyRef<'_, 中枢Py> = item.extract()?;
|
||||
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 列表 (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")?;
|
||||
扩展中枢.call_method1("extend", (PyList::new(py, &items)?,))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -889,10 +1113,11 @@ impl 中枢Py {
|
||||
#[classmethod]
|
||||
#[pyo3(signature = (虚线序列, 中枢序列, 跳过首部 = true, 标识 = "", 层级 = 0))]
|
||||
/// 中枢识别核心递归算法
|
||||
/// 中枢序列 原地修改(与 chan.py 行为一致)
|
||||
fn 分析(
|
||||
_cls: &Bound<'_, PyType>,
|
||||
虚线序列: Vec<Py<虚线Py>>,
|
||||
中枢序列: Vec<Py<Self>>,
|
||||
中枢序列: &Bound<'_, PyList>,
|
||||
跳过首部: bool,
|
||||
标识: &str,
|
||||
层级: i64,
|
||||
@@ -902,11 +1127,24 @@ impl 中枢Py {
|
||||
.iter()
|
||||
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
|
||||
.collect();
|
||||
let mut hub_seq: Vec<Arc<chanlun::algorithm::hub::中枢>> = 中枢序列
|
||||
|
||||
let mut hub_seq = Vec::with_capacity(中枢序列.len());
|
||||
for item in 中枢序列.iter() {
|
||||
let h: PyRef<'_, 中枢Py> = item.extract()?;
|
||||
hub_seq.push(Arc::clone(&h.inner));
|
||||
}
|
||||
|
||||
py.detach(|| {
|
||||
chanlun::algorithm::hub::中枢::分析(&rc_list, &mut hub_seq, 跳过首部, 标识, 层级);
|
||||
});
|
||||
|
||||
// 写回 Python 列表 (bulk extend)
|
||||
let items: Vec<Py<PyAny>> = hub_seq
|
||||
.iter()
|
||||
.map(|h| Arc::clone(&h.bind(py).borrow().inner))
|
||||
.map(|h| hub_to_py(py, Arc::clone(h)).into_any())
|
||||
.collect();
|
||||
chanlun::algorithm::hub::中枢::分析(&rc_list, &mut hub_seq, 跳过首部, 标识, 层级);
|
||||
中枢序列.call_method0("clear")?;
|
||||
中枢序列.call_method1("extend", (PyList::new(py, &items)?,))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
+267
-87
@@ -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};
|
||||
@@ -675,6 +676,13 @@ impl 买卖点Py {
|
||||
/// 线段_线段序列 / 线段_中枢序列
|
||||
/// 扩展线段序列_扩展线段 / 扩展中枢序列_扩展线段
|
||||
///
|
||||
/// 分析层次(可读写):
|
||||
/// 线段分析层次: int (默认 3) / 扩展线段分析层次: int (默认 3) / 混合扩展线段分析层次: int (默认 3)
|
||||
///
|
||||
/// 序列组(只读,返回 list[list[...]]):
|
||||
/// 线段序列组 / 中枢序列组 / 扩展线段序列组 / 扩展中枢序列组
|
||||
/// 混合扩展线段序列组 / 混合扩展中枢序列组
|
||||
///
|
||||
/// 核心方法:
|
||||
/// 增加原始K线(普K) — 喂入一根普通K线,触发全层级增量计算
|
||||
/// 重置基础序列() — 清空所有计算状态和序列
|
||||
@@ -686,27 +694,27 @@ impl 买卖点Py {
|
||||
#[pyclass(name = "观察者", module = "chanlun._chanlun", subclass)]
|
||||
pub struct 观察者Py {
|
||||
pub(crate) inner: Option<Arc<RwLock<chanlun::business::observer::观察者>>>,
|
||||
配置缓存: 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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -762,6 +770,8 @@ impl 观察者Py {
|
||||
inner: Some(chanlun::business::observer::观察者::new(
|
||||
符号, 周期, config,
|
||||
)),
|
||||
配置缓存: parking_lot::Mutex::new(None),
|
||||
最后配置版本: AtomicU64::new(0),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -814,8 +824,24 @@ impl 观察者Py {
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn 配置(&self) -> PyResult<缠论配置Py> {
|
||||
缠论配置Py::from_rust_config(&self.obs().配置)
|
||||
fn 配置(&self, py: Python<'_>) -> PyResult<Py<缠论配置Py>> {
|
||||
let mut cache = self.配置缓存.lock();
|
||||
if let Some(ref cached) = *cache {
|
||||
Ok(cached.clone_ref(py))
|
||||
} else {
|
||||
let cfg_py = 缠论配置Py::from_rust_config(&self.obs().配置)?;
|
||||
let obj = Py::new(py, cfg_py)?;
|
||||
*cache = Some(obj.clone_ref(py));
|
||||
Ok(obj)
|
||||
}
|
||||
}
|
||||
|
||||
#[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;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 清空所有分析序列,重置为初始状态(内部实现)
|
||||
@@ -833,15 +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<()> {
|
||||
slf.call_method1("_增加原始K线", (普K,))?;
|
||||
Ok(())
|
||||
// 版本对比,仅在配置变更时同步(支持 obs.配置.field = value 直接修改)
|
||||
{
|
||||
let me = slf.borrow();
|
||||
if let Some(ref cached) = *me.配置缓存.lock() {
|
||||
let py = slf.py();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 直接调用 Rust 实现,跳过 Python dispatch
|
||||
Self::增加原始K线_impl(slf, 普K)
|
||||
}
|
||||
|
||||
/// 投喂原始数据 — 便捷入口,直接从 OHLCV 创建 K线 并通过 Python 分发 增加原始K线,
|
||||
@@ -893,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}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 静态重新分析(公开分发器,支持子类重写)
|
||||
@@ -918,16 +1004,16 @@ impl 观察者Py {
|
||||
}
|
||||
|
||||
#[classmethod]
|
||||
#[pyo3(signature = (观察员, 文件路径, 配置 = None))]
|
||||
/// :param 观察员: 观察者实例
|
||||
#[pyo3(signature = (文件路径, 配置 = None, 观察员 = None))]
|
||||
/// :param 文件路径: 数据文件路径 格式如: btcusd-300-1631772074-1632222374.nb
|
||||
/// :param 配置: 缠论配置
|
||||
/// :param 观察员: 可选,已有观察者实例;不传则自动创建
|
||||
/// :return: 观察者实例
|
||||
fn 读取数据文件(
|
||||
_cls: &Bound<'_, PyType>,
|
||||
观察员: &Bound<'_, Self>,
|
||||
文件路径: &str,
|
||||
配置: Option<&Bound<'_, 缠论配置Py>>,
|
||||
观察员: Option<&Bound<'_, Self>>,
|
||||
py: Python<'_>,
|
||||
) -> PyResult<Py<PyAny>> {
|
||||
let config = match 配置 {
|
||||
@@ -953,19 +1039,37 @@ impl 观察者Py {
|
||||
.parse()
|
||||
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("parse period: {}", e)))?;
|
||||
|
||||
// 设置观察员属性
|
||||
{
|
||||
let slf_ref = 观察员.borrow_mut();
|
||||
let mut obs_mut = slf_ref.obs_mut();
|
||||
obs_mut.符号 = 符号;
|
||||
obs_mut.周期 = 周期;
|
||||
obs_mut.配置 = config;
|
||||
}
|
||||
let obs_ref = match 观察员 {
|
||||
Some(obs) => {
|
||||
// 更新已有观察员属性
|
||||
{
|
||||
let slf_ref = obs.borrow_mut();
|
||||
let mut obs_mut = slf_ref.obs_mut();
|
||||
obs_mut.符号 = 符号;
|
||||
obs_mut.周期 = 周期;
|
||||
obs_mut.配置 = config;
|
||||
}
|
||||
obs.clone()
|
||||
}
|
||||
None => {
|
||||
// 创建新观察者:调用 cls(符号, 周期),配置后续通过 obs_mut 设置
|
||||
let obj = _cls.call1((符号.as_str(), 周期))?;
|
||||
let obs: &Bound<'_, Self> = obj.cast().map_err(|_| {
|
||||
pyo3::exceptions::PyTypeError::new_err("failed to create 观察者")
|
||||
})?;
|
||||
{
|
||||
let slf_ref = obs.borrow_mut();
|
||||
let mut obs_mut = slf_ref.obs_mut();
|
||||
obs_mut.配置 = config;
|
||||
}
|
||||
obs.clone()
|
||||
}
|
||||
};
|
||||
|
||||
// 调用加载本地数据
|
||||
观察员.call_method1("加载本地数据", (文件路径,))?;
|
||||
// 调用加载本地数据(通过 Python dispatch,支持子类重写)
|
||||
obs_ref.call_method1("加载本地数据", (文件路径,))?;
|
||||
|
||||
Ok(观察员.clone().unbind().into())
|
||||
Ok(obs_ref.unbind().into())
|
||||
}
|
||||
|
||||
// ---- 序列 getters ----
|
||||
@@ -1040,7 +1144,7 @@ impl 观察者Py {
|
||||
#[getter]
|
||||
fn 线段序列(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
let list = pyo3::types::PyList::empty(py);
|
||||
for d in &self.obs().线段序列 {
|
||||
for d in self.obs().线段序列() {
|
||||
list.append(dashed_to_py(py, Arc::clone(d)))?;
|
||||
}
|
||||
Ok(list.into())
|
||||
@@ -1049,7 +1153,7 @@ impl 观察者Py {
|
||||
#[getter]
|
||||
fn 中枢序列(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
let list = pyo3::types::PyList::empty(py);
|
||||
for h in &self.obs().中枢序列 {
|
||||
for h in self.obs().中枢序列() {
|
||||
list.append(hub_to_py(py, Arc::clone(h)))?;
|
||||
}
|
||||
Ok(list.into())
|
||||
@@ -1058,7 +1162,7 @@ impl 观察者Py {
|
||||
#[getter]
|
||||
fn 扩展线段序列(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
let list = pyo3::types::PyList::empty(py);
|
||||
for d in &self.obs().扩展线段序列 {
|
||||
for d in self.obs().扩展线段序列() {
|
||||
list.append(dashed_to_py(py, Arc::clone(d)))?;
|
||||
}
|
||||
Ok(list.into())
|
||||
@@ -1067,7 +1171,7 @@ impl 观察者Py {
|
||||
#[getter]
|
||||
fn 扩展中枢序列(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
let list = pyo3::types::PyList::empty(py);
|
||||
for h in &self.obs().扩展中枢序列 {
|
||||
for h in self.obs().扩展中枢序列() {
|
||||
list.append(hub_to_py(py, Arc::clone(h)))?;
|
||||
}
|
||||
Ok(list.into())
|
||||
@@ -1076,7 +1180,7 @@ impl 观察者Py {
|
||||
#[getter]
|
||||
fn 扩展线段序列_线段(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
let list = pyo3::types::PyList::empty(py);
|
||||
for d in &self.obs().扩展线段序列_线段 {
|
||||
for d in self.obs().扩展线段序列_线段() {
|
||||
list.append(dashed_to_py(py, Arc::clone(d)))?;
|
||||
}
|
||||
Ok(list.into())
|
||||
@@ -1085,7 +1189,7 @@ impl 观察者Py {
|
||||
#[getter]
|
||||
fn 扩展中枢序列_线段(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
let list = pyo3::types::PyList::empty(py);
|
||||
for h in &self.obs().扩展中枢序列_线段 {
|
||||
for h in self.obs().扩展中枢序列_线段() {
|
||||
list.append(hub_to_py(py, Arc::clone(h)))?;
|
||||
}
|
||||
Ok(list.into())
|
||||
@@ -1094,7 +1198,7 @@ impl 观察者Py {
|
||||
#[getter]
|
||||
fn 线段_线段序列(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
let list = pyo3::types::PyList::empty(py);
|
||||
for d in &self.obs().线段_线段序列 {
|
||||
for d in self.obs().线段_线段序列() {
|
||||
list.append(dashed_to_py(py, Arc::clone(d)))?;
|
||||
}
|
||||
Ok(list.into())
|
||||
@@ -1103,7 +1207,7 @@ impl 观察者Py {
|
||||
#[getter]
|
||||
fn 线段_中枢序列(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
let list = pyo3::types::PyList::empty(py);
|
||||
for h in &self.obs().线段_中枢序列 {
|
||||
for h in self.obs().线段_中枢序列() {
|
||||
list.append(hub_to_py(py, Arc::clone(h)))?;
|
||||
}
|
||||
Ok(list.into())
|
||||
@@ -1112,7 +1216,7 @@ impl 观察者Py {
|
||||
#[getter]
|
||||
fn 扩展线段序列_扩展线段(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
let list = pyo3::types::PyList::empty(py);
|
||||
for d in &self.obs().扩展线段序列_扩展线段 {
|
||||
for d in self.obs().扩展线段序列_扩展线段() {
|
||||
list.append(dashed_to_py(py, Arc::clone(d)))?;
|
||||
}
|
||||
Ok(list.into())
|
||||
@@ -1121,11 +1225,112 @@ impl 观察者Py {
|
||||
#[getter]
|
||||
fn 扩展中枢序列_扩展线段(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
let list = pyo3::types::PyList::empty(py);
|
||||
for h in &self.obs().扩展中枢序列_扩展线段 {
|
||||
for h in self.obs().扩展中枢序列_扩展线段() {
|
||||
list.append(hub_to_py(py, Arc::clone(h)))?;
|
||||
}
|
||||
Ok(list.into())
|
||||
}
|
||||
|
||||
// ---- 分析层次 getter/setter ----
|
||||
|
||||
#[getter]
|
||||
fn 线段分析层次(&self) -> usize {
|
||||
self.obs().线段分析层次
|
||||
}
|
||||
|
||||
#[setter]
|
||||
#[pyo3(name = "线段分析层次")]
|
||||
fn 设置_线段分析层次(&mut self, value: usize) {
|
||||
self.obs_mut().线段分析层次 = value;
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn 扩展线段分析层次(&self) -> usize {
|
||||
self.obs().扩展线段分析层次
|
||||
}
|
||||
|
||||
#[setter]
|
||||
#[pyo3(name = "扩展线段分析层次")]
|
||||
fn 设置_扩展线段分析层次(&mut self, value: usize) {
|
||||
self.obs_mut().扩展线段分析层次 = value;
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn 混合扩展线段分析层次(&self) -> usize {
|
||||
self.obs().混合扩展线段分析层次
|
||||
}
|
||||
|
||||
#[setter]
|
||||
#[pyo3(name = "混合扩展线段分析层次")]
|
||||
fn 设置_混合扩展线段分析层次(&mut self, value: usize) {
|
||||
self.obs_mut().混合扩展线段分析层次 = value;
|
||||
}
|
||||
|
||||
// ---- 序列组 getters (返回 list[list[...]]) ----
|
||||
|
||||
#[getter]
|
||||
fn 线段序列组(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
self._虚线序列组_to_py(&self.obs().线段序列组, py)
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn 中枢序列组(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
self._中枢序列组_to_py(&self.obs().中枢序列组, py)
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn 扩展线段序列组(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
self._虚线序列组_to_py(&self.obs().扩展线段序列组, py)
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn 扩展中枢序列组(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
self._中枢序列组_to_py(&self.obs().扩展中枢序列组, py)
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn 混合扩展线段序列组(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
self._虚线序列组_to_py(&self.obs().混合扩展线段序列组, py)
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn 混合扩展中枢序列组(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
self._中枢序列组_to_py(&self.obs().混合扩展中枢序列组, py)
|
||||
}
|
||||
}
|
||||
|
||||
impl 观察者Py {
|
||||
fn _虚线序列组_to_py(
|
||||
&self,
|
||||
组: &Vec<Vec<Arc<chanlun::structure::dash_line::虚线>>>,
|
||||
py: Python<'_>,
|
||||
) -> PyResult<Py<PyAny>> {
|
||||
let outer = pyo3::types::PyList::empty(py);
|
||||
for inner_vec in 组 {
|
||||
let inner_list = pyo3::types::PyList::empty(py);
|
||||
for d in inner_vec {
|
||||
inner_list.append(dashed_to_py(py, Arc::clone(d)))?;
|
||||
}
|
||||
outer.append(inner_list)?;
|
||||
}
|
||||
Ok(outer.into())
|
||||
}
|
||||
|
||||
fn _中枢序列组_to_py(
|
||||
&self,
|
||||
组: &Vec<Vec<Arc<chanlun::algorithm::hub::中枢>>>,
|
||||
py: Python<'_>,
|
||||
) -> PyResult<Py<PyAny>> {
|
||||
let outer = pyo3::types::PyList::empty(py);
|
||||
for inner_vec in 组 {
|
||||
let inner_list = pyo3::types::PyList::empty(py);
|
||||
for h in inner_vec {
|
||||
inner_list.append(hub_to_py(py, Arc::clone(h)))?;
|
||||
}
|
||||
outer.append(inner_list)?;
|
||||
}
|
||||
Ok(outer.into())
|
||||
}
|
||||
}
|
||||
|
||||
// ========== K线合成器 ==========
|
||||
@@ -1136,9 +1341,8 @@ impl 观察者Py {
|
||||
/// K线合成器(标识, 周期组) — 周期组为升序排列的整数列表(如 [60, 300, 900])
|
||||
///
|
||||
/// 方法:
|
||||
/// 投喂K线(普K) -> list[(周期, K线)] — 喂入普通K线,返回合成后的大周期K线
|
||||
/// 投喂(时间戳, 开盘价, 最高价, 最低价, 收盘价, 成交量) -> list[(周期, K线)]
|
||||
/// — 快捷入口,免去构造K线对象
|
||||
/// 投喂K线(普K) — 喂入普通K线,完成事件通过回调分发
|
||||
/// 投喂(时间戳, 开盘价, 最高价, 最低价, 收盘价, 成交量) — 快捷入口
|
||||
/// 获取当前K线(周期) -> K线|None — 获取指定周期的当前合成结果
|
||||
#[pyclass(name = "K线合成器", module = "chanlun._chanlun")]
|
||||
pub struct K线合成器Py {
|
||||
@@ -1150,51 +1354,18 @@ impl K线合成器Py {
|
||||
#[new]
|
||||
fn new(标识: String, 周期组: Vec<i64>) -> Self {
|
||||
Self {
|
||||
inner: chanlun::business::synthesizer::K线合成器::new(标识, 周期组),
|
||||
inner: chanlun::business::synthesizer::K线合成器::new(标识, 周期组, None),
|
||||
}
|
||||
}
|
||||
|
||||
/// 统一入口 — 投喂最小周期K线,自动合成大周期并分发给各周期观察者
|
||||
fn 投喂K线(
|
||||
&mut self,
|
||||
普K: &Bound<'_, K线Py>,
|
||||
py: Python<'_>,
|
||||
) -> PyResult<Vec<(i64, Py<K线Py>)>> {
|
||||
let results = self.inner.投喂K线((*普K.borrow().inner).clone());
|
||||
Ok(results
|
||||
.into_iter()
|
||||
.map(|(周期, k)| (周期, bar_to_py(py, Arc::new(k))))
|
||||
.collect())
|
||||
/// 投喂K线 — 输入最小周期K线,合成为所有目标周期,完成事件通过回调分发
|
||||
fn 投喂K线(&mut self, 普K: &Bound<'_, K线Py>) {
|
||||
self.inner.投喂K线((*普K.borrow().inner).clone());
|
||||
}
|
||||
|
||||
/// 投喂原始tick数据
|
||||
fn 投喂(
|
||||
&mut self,
|
||||
时间戳: i64,
|
||||
开: f64,
|
||||
高: f64,
|
||||
低: f64,
|
||||
收: f64,
|
||||
量: f64,
|
||||
py: Python<'_>,
|
||||
) -> Vec<(i64, Py<K线Py>)> {
|
||||
let min_cycle = self.inner.周期组.iter().copied().min().unwrap_or(1);
|
||||
let k = chanlun::kline::bar::K线::创建普K(
|
||||
&self.inner.标识,
|
||||
时间戳,
|
||||
开,
|
||||
高,
|
||||
低,
|
||||
收,
|
||||
量,
|
||||
0,
|
||||
min_cycle,
|
||||
);
|
||||
let results = self.inner.投喂K线(k);
|
||||
results
|
||||
.into_iter()
|
||||
.map(|(周期, k2)| (周期, bar_to_py(py, Arc::new(k2))))
|
||||
.collect()
|
||||
fn 投喂(&mut self, 时间戳: i64, 开: f64, 高: f64, 低: f64, 收: f64, 量: f64) {
|
||||
self.inner.投喂(时间戳, 开, 高, 低, 收, 量);
|
||||
}
|
||||
|
||||
/// 获取指定周期当前正在合成的K线
|
||||
@@ -1273,12 +1444,6 @@ impl 立体分析器Py {
|
||||
self.inner.投喂K线((*普K.borrow().inner).clone());
|
||||
}
|
||||
|
||||
fn 获取观察者(&self, 周期: i64) -> Option<观察者Py> {
|
||||
self.inner
|
||||
.获取观察者(周期)
|
||||
.map(|rc| 观察者Py { inner: Some(rc) })
|
||||
}
|
||||
|
||||
/// 拆分各序列数据,单独存文件,文件名为对应变量名
|
||||
fn 测试_保存数据(&self, root: Option<&str>) {
|
||||
self.inner.测试_保存数据(root);
|
||||
@@ -1288,6 +1453,21 @@ impl 立体分析器Py {
|
||||
fn 周期组(&self) -> Vec<i64> {
|
||||
self.inner.周期组.clone()
|
||||
}
|
||||
|
||||
/// _单体分析器 — 对应 Python 立体分析器._单体分析器: dict[周期, 观察者]
|
||||
#[getter(_单体分析器)]
|
||||
fn get_单体分析器(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
let dict = pyo3::types::PyDict::new(py);
|
||||
for (周期, obs_rc) in &self.inner.单体分析器 {
|
||||
let obs_py = 观察者Py {
|
||||
inner: Some(obs_rc.clone()),
|
||||
配置缓存: parking_lot::Mutex::new(None),
|
||||
最后配置版本: AtomicU64::new(0),
|
||||
};
|
||||
dict.set_item(周期, obs_py)?;
|
||||
}
|
||||
Ok(dict.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+126
-24
@@ -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!(
|
||||
@@ -169,8 +179,11 @@ impl 缠论配置Py {
|
||||
/// 将配置导出为 Python 字典。
|
||||
fn to_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
|
||||
let dict = PyDict::new(py);
|
||||
let valid = chanlun::config::缠论配置::model_fields();
|
||||
for (k, v) in &self.fields {
|
||||
dict.set_item(k, v.clone_ref(py))?;
|
||||
if valid.contains(&k.as_str()) {
|
||||
dict.set_item(k, v.clone_ref(py))?;
|
||||
}
|
||||
}
|
||||
Ok(dict.into())
|
||||
}
|
||||
@@ -214,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]
|
||||
@@ -228,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]
|
||||
@@ -252,26 +278,85 @@ impl 缠论配置Py {
|
||||
Ok(result.into())
|
||||
}
|
||||
|
||||
/// 比较当前配置与另一个配置的差异
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn 对比(
|
||||
&self,
|
||||
py: Python<'_>,
|
||||
other: &Bound<'_, 缠论配置Py>,
|
||||
) -> PyResult<HashMap<String, (Py<PyAny>, Py<PyAny>)>> {
|
||||
/// 创建当前配置的拷贝并可选择更新字段(对应 Python model_copy(update={...}, deep=True))
|
||||
#[pyo3(signature = (update = None))]
|
||||
fn model_copy(&self, py: Python<'_>, update: Option<&Bound<'_, PyDict>>) -> PyResult<Self> {
|
||||
let current = self.to_dict(py)?;
|
||||
if let Some(updates) = update {
|
||||
for (key, value) in updates.iter() {
|
||||
current.bind(py).set_item(key, value)?;
|
||||
}
|
||||
}
|
||||
Self::from_dict(&py.get_type::<Self>(), current.bind(py))
|
||||
}
|
||||
|
||||
/// 比较当前配置与另一个配置的差异(对应 Python 对比 → dict[字段名, 新值])
|
||||
fn 对比(&self, py: Python<'_>, other: &Bound<'_, 缠论配置Py>) -> PyResult<Py<PyAny>> {
|
||||
let other_ref = other.borrow();
|
||||
let mut diff = HashMap::new();
|
||||
for (key, val) in &self.fields {
|
||||
if let Some(other_val) = other_ref.fields.get(key) {
|
||||
let a = val.clone_ref(py);
|
||||
let dict = PyDict::new(py);
|
||||
let valid = chanlun::config::缠论配置::model_fields();
|
||||
for key in valid {
|
||||
if let (Some(self_val), Some(other_val)) =
|
||||
(self.fields.get(*key), other_ref.fields.get(*key))
|
||||
{
|
||||
let a = self_val.clone_ref(py);
|
||||
let b = other_val.clone_ref(py);
|
||||
let eq = a.bind(py).eq(b.bind(py))?;
|
||||
if !eq {
|
||||
diff.insert(key.clone(), (val.clone_ref(py), other_val.clone_ref(py)));
|
||||
dict.set_item(*key, b)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(diff)
|
||||
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}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,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),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,9 +491,9 @@ fn validate_field(
|
||||
) -> Result<(), String> {
|
||||
use serde_json::Value;
|
||||
|
||||
// 输入为 null → 跳过(保留默认)
|
||||
// 输入为 null → 保留(对应 Optional/Infinity 字段)
|
||||
if input.is_null() {
|
||||
return Err("值为 null".into());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 字符串字段:检查有效值白名单
|
||||
@@ -414,6 +512,8 @@ fn validate_field(
|
||||
(Value::Bool(_), Value::Bool(_)) => return Ok(()),
|
||||
(Value::Number(_), Value::Number(_)) => return Ok(()),
|
||||
(Value::String(_), Value::String(_)) => return Ok(()),
|
||||
(Value::Array(_), Value::Array(_)) => return Ok(()),
|
||||
(Value::Object(_), Value::Object(_)) => return Ok(()),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -430,7 +530,9 @@ fn validate_field(
|
||||
Value::Bool(_) => "布尔",
|
||||
Value::Number(_) => "数值",
|
||||
Value::String(_) => "字符串",
|
||||
_ => "其他",
|
||||
Value::Array(_) => "数组",
|
||||
Value::Object(_) => "字典",
|
||||
Value::Null => "null",
|
||||
};
|
||||
Err(format!("类型不匹配(需要 {expected},收到 {type_name})"))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,744 @@
|
||||
/*
|
||||
* 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 crate::business_py::立体分析器Py;
|
||||
use crate::business_py::观察者Py;
|
||||
use pyo3::prelude::*;
|
||||
|
||||
/// 从 Python 值中提取时间戳(兼容 i64 和 datetime 两种类型)
|
||||
fn 提取时间戳(val: &Bound<'_, PyAny>) -> PyResult<i64> {
|
||||
if let Ok(ts) = val.extract::<i64>() {
|
||||
return Ok(ts);
|
||||
}
|
||||
let ts_f: f64 = val.call_method0("timestamp")?.extract()?;
|
||||
Ok(ts_f as i64)
|
||||
}
|
||||
|
||||
/// 从对象获取属性,依次尝试多个候选名
|
||||
fn 获取属性任意<'a>(
|
||||
obj: &'a Bound<'_, PyAny>,
|
||||
候选名: &[&str],
|
||||
) -> PyResult<Option<Bound<'a, PyAny>>> {
|
||||
for name in 候选名 {
|
||||
if obj.hasattr(name)? {
|
||||
return Ok(Some(obj.getattr(name)?));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// 比较两个 Python 值是否为 float(容差比较)
|
||||
fn 尝试浮点比较(
|
||||
valA: &Bound<'_, PyAny>,
|
||||
valB: &Bound<'_, PyAny>,
|
||||
容差: f64,
|
||||
) -> Option<PyResult<(bool, String)>> {
|
||||
if let (Ok(a), Ok(b)) = (valA.extract::<f64>(), valB.extract::<f64>()) {
|
||||
if (a - b).abs() > 容差 {
|
||||
return Some(Ok((
|
||||
false,
|
||||
format!("浮点超限 容差={:.2e} A={:.10},B={:.10}", 容差, a, b),
|
||||
)));
|
||||
}
|
||||
return Some(Ok((true, String::new())));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// 尝试从对象获取 `标识` 字段,失败返回空字符串
|
||||
fn 尝试获取标识(obj: &Bound<'_, PyAny>) -> String {
|
||||
if let Ok(val) = obj.getattr("标识")
|
||||
&& let Ok(py_str) = val.str()
|
||||
{
|
||||
return py_str.extract::<String>().unwrap_or_default();
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
|
||||
/// None 检查辅助:双方为 None 返回 true,单方为 None 返回 false+消息
|
||||
fn 检查空值一致(
|
||||
valA: &Bound<'_, PyAny>,
|
||||
valB: &Bound<'_, PyAny>,
|
||||
字段: &str,
|
||||
标签: &str,
|
||||
) -> Option<(bool, String)> {
|
||||
let a_none = valA.is_none();
|
||||
let b_none = valB.is_none();
|
||||
if a_none && b_none {
|
||||
return Some((true, String::new()));
|
||||
}
|
||||
if a_none || b_none {
|
||||
return Some((
|
||||
false,
|
||||
format!("{标签}: [{字段}] 空值不一致 A=None={a_none},B=None={b_none}"),
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// ========== K线相等 ==========
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (A, B, 浮点容差 = 1e-9))]
|
||||
fn K线相等(
|
||||
A: &Bound<'_, PyAny>,
|
||||
B: &Bound<'_, PyAny>,
|
||||
浮点容差: f64,
|
||||
) -> PyResult<(bool, String)> {
|
||||
// 快速路径
|
||||
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线相等 ==========
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (A, B, 浮点容差 = 1e-9))]
|
||||
fn 缠论K线相等(
|
||||
A: &Bound<'_, PyAny>,
|
||||
B: &Bound<'_, PyAny>,
|
||||
浮点容差: f64,
|
||||
) -> PyResult<(bool, String)> {
|
||||
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}")));
|
||||
}
|
||||
} 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!("{标签}: 全部字段嵌套校验一致")))
|
||||
}
|
||||
|
||||
// ========== 分型相等 ==========
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (A, B, 浮点容差 = 1e-9))]
|
||||
fn 分型相等(
|
||||
A: &Bound<'_, PyAny>,
|
||||
B: &Bound<'_, PyAny>,
|
||||
浮点容差: f64,
|
||||
) -> PyResult<(bool, String)> {
|
||||
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 (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线全部校验一致")))
|
||||
}
|
||||
|
||||
// ========== 缺口相等 ==========
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (A, B, 浮点容差 = 1e-9))]
|
||||
fn 缺口相等(
|
||||
A: &Bound<'_, PyAny>,
|
||||
B: &Bound<'_, PyAny>,
|
||||
浮点容差: f64,
|
||||
) -> PyResult<(bool, String)> {
|
||||
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]
|
||||
#[pyo3(signature = (A, B, 浮点容差 = 1e-9))]
|
||||
fn 线段特征相等(
|
||||
A: &Bound<'_, PyAny>,
|
||||
B: &Bound<'_, PyAny>,
|
||||
浮点容差: f64,
|
||||
) -> PyResult<(bool, String)> {
|
||||
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!("{标签}: 字段与内部虚线序列全部一致")))
|
||||
}
|
||||
|
||||
// ========== 中枢相等 ==========
|
||||
|
||||
#[pyfunction]
|
||||
#[pyo3(signature = (A, B, 浮点容差 = 1e-9))]
|
||||
fn 中枢相等(
|
||||
A: &Bound<'_, PyAny>,
|
||||
B: &Bound<'_, PyAny>,
|
||||
浮点容差: f64,
|
||||
) -> PyResult<(bool, String)> {
|
||||
if let (Ok(a), Ok(b)) = (
|
||||
A.cast::<crate::algorithm_py::中枢Py>(),
|
||||
B.cast::<crate::algorithm_py::中枢Py>(),
|
||||
) {
|
||||
return Ok(a.borrow().inner.相等(&b.borrow().inner, 浮点容差));
|
||||
}
|
||||
let a标识 = 尝试获取标识(A);
|
||||
let b标识 = 尝试获取标识(B);
|
||||
let 标签 = format!("中枢校验[A标识={a标识},B标识={b标识}]");
|
||||
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 字段 == "第三买卖线" || 字段 == "本级_第三买卖线" {
|
||||
if let Some(r) = 检查空值一致(&valA, &valB, 字段, &标签) {
|
||||
if !r.0 {
|
||||
return Ok((false, r.1));
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let (eq, msg) = 虚线相等(&valA, &valB, 浮点容差)?;
|
||||
if !eq {
|
||||
return Ok((false, format!("{标签}: [{字段}]子虚线异常 >> {msg}")));
|
||||
}
|
||||
} 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]
|
||||
#[pyo3(signature = (A, B, 浮点容差 = 1e-9))]
|
||||
fn 虚线相等(
|
||||
A: &Bound<'_, PyAny>,
|
||||
B: &Bound<'_, PyAny>,
|
||||
浮点容差: f64,
|
||||
) -> PyResult<(bool, String)> {
|
||||
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 a标识 = 尝试获取标识(A);
|
||||
let b标识 = 尝试获取标识(B);
|
||||
let 标签 = format!("虚线校验[A标识={a标识},B标识={b标识}]");
|
||||
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 字段 == "文" || 字段 == "武" {
|
||||
if let Some(r) = 检查空值一致(&valA, &valB, 字段, &标签) {
|
||||
if !r.0 {
|
||||
return Ok((false, r.1));
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let (eq, msg) = 分型相等(&valA, &valB, 浮点容差)?;
|
||||
if !eq {
|
||||
return Ok((false, format!("{标签}: [{字段}]子分型异常 >> {msg}")));
|
||||
}
|
||||
}
|
||||
// 前一缺口
|
||||
else if 字段 == "前一缺口" {
|
||||
if let Some(r) = 检查空值一致(&valA, &valB, 字段, &标签) {
|
||||
if !r.0 {
|
||||
return Ok((false, r.1));
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let (eq, msg) = 缺口相等(&valA, &valB, 浮点容差)?;
|
||||
if !eq {
|
||||
return Ok((false, format!("{标签}: [前一缺口]子缺口异常 >> {msg}")));
|
||||
}
|
||||
}
|
||||
// 前一结束位置
|
||||
else if 字段 == "前一结束位置" {
|
||||
if let Some(r) = 检查空值一致(&valA, &valB, 字段, &标签) {
|
||||
if !r.0 {
|
||||
return Ok((false, r.1));
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let (eq, msg) = 虚线相等(&valA, &valB, 浮点容差)?;
|
||||
if !eq {
|
||||
return Ok((false, format!("{标签}: [前一结束位置]异常 >> {msg}")));
|
||||
}
|
||||
}
|
||||
// 确认K线
|
||||
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线]子缠论K线异常 >> {msg}")));
|
||||
}
|
||||
}
|
||||
// 各类列表
|
||||
else 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)?;
|
||||
if let Some(r) =
|
||||
检查空值一致(&itemA, &itemB, &format!("{字段}[{idx}]"), &标签)
|
||||
{
|
||||
if !r.0 {
|
||||
return Ok((false, r.1));
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let (eq, msg) = if 字段 == "基础序列" {
|
||||
虚线相等(&itemA, &itemB, 浮点容差)?
|
||||
} else if 字段.contains("中枢") {
|
||||
中枢相等(&itemA, &itemB, 浮点容差)?
|
||||
} else {
|
||||
线段特征相等(&itemA, &itemB, 浮点容差)?
|
||||
};
|
||||
if !eq {
|
||||
return Ok((false, format!("{标签}: [{字段}][{idx}]子项异常 >> {msg}")));
|
||||
}
|
||||
}
|
||||
}
|
||||
// 普通字段
|
||||
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]
|
||||
fn 观察者相等(
|
||||
a: &Bound<'_, 观察者Py>,
|
||||
b: &Bound<'_, 观察者Py>,
|
||||
浮点容差: Option<f64>,
|
||||
) -> PyResult<(bool, String)> {
|
||||
let 容差 = 浮点容差.unwrap_or(1e-9);
|
||||
let arc_a = a
|
||||
.borrow()
|
||||
.inner
|
||||
.clone()
|
||||
.ok_or_else(|| pyo3::exceptions::PyValueError::new_err("观察者A 内部为空"))?;
|
||||
let arc_b = b
|
||||
.borrow()
|
||||
.inner
|
||||
.clone()
|
||||
.ok_or_else(|| pyo3::exceptions::PyValueError::new_err("观察者B 内部为空"))?;
|
||||
let obs_a = arc_a.read();
|
||||
let obs_b = arc_b.read();
|
||||
Ok(obs_a.相等(&obs_b, 容差))
|
||||
}
|
||||
|
||||
// ========== 立体分析器相等 ==========
|
||||
|
||||
#[pyfunction]
|
||||
fn 立体分析器相等(
|
||||
a: &Bound<'_, 立体分析器Py>,
|
||||
b: &Bound<'_, 立体分析器Py>,
|
||||
浮点容差: Option<f64>,
|
||||
) -> PyResult<(bool, String)> {
|
||||
let 容差 = 浮点容差.unwrap_or(1e-9);
|
||||
let result = {
|
||||
let ref_a = a.borrow();
|
||||
let ref_b = b.borrow();
|
||||
ref_a.inner.相等(&ref_b.inner, 容差)
|
||||
};
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(wrap_pyfunction!(K线相等, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(缠论K线相等, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(分型相等, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(缺口相等, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(线段特征相等, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(中枢相等, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(虚线相等, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(观察者相等, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(立体分析器相等, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -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();
|
||||
@@ -985,26 +981,17 @@ impl 指标计算器Py {
|
||||
/// 增量计算所有开启的指标,将结果写入 当前K线.指标
|
||||
#[staticmethod]
|
||||
fn 计算并挂载(
|
||||
当前K线: &Bound<'_, crate::kline_py::K线Py>,
|
||||
_当前K线: &Bound<'_, crate::kline_py::K线Py>,
|
||||
全序列: Vec<Py<crate::kline_py::K线Py>>,
|
||||
配置: &Bound<'_, crate::config_py::缠论配置Py>,
|
||||
py: Python<'_>,
|
||||
) -> PyResult<()> {
|
||||
let config = 配置.borrow().to_rust_config(py)?;
|
||||
// 全序列包含 当前K线 在末尾;Rust 计算并挂载 的 现有序列 不含当前K线
|
||||
let 现有序列: Vec<Arc<chanlun::kline::bar::K线>> = if 全序列.len() > 1 {
|
||||
全序列[..全序列.len() - 1]
|
||||
.iter()
|
||||
.map(|k| k.bind(py).borrow().inner.clone())
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
chanlun::indicators::指标计算器::计算并挂载(
|
||||
&当前K线.borrow().inner,
|
||||
&现有序列,
|
||||
&config,
|
||||
);
|
||||
let 全序列_rust: Vec<Arc<chanlun::kline::bar::K线>> = 全序列
|
||||
.iter()
|
||||
.map(|k| k.bind(py).borrow().inner.clone())
|
||||
.collect();
|
||||
chanlun::indicators::指标计算器::计算并挂载(&全序列_rust, &config);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+88
-68
@@ -22,16 +22,16 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
use parking_lot::RwLock;
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::{PyBytes, PyDict, PyType};
|
||||
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]
|
||||
@@ -663,25 +668,30 @@ impl 缠论K线Py {
|
||||
|
||||
#[classmethod]
|
||||
/// 分析K线,执行指标计算+包含处理+分型判定
|
||||
/// 缠K序列/普K序列 原地修改(与 chan.py 行为一致)
|
||||
/// :return: (状态, 分型|None)
|
||||
fn 分析(
|
||||
_cls: &Bound<'_, PyType>,
|
||||
当前K线: &Bound<'_, K线Py>,
|
||||
缠K序列: Vec<Py<Self>>,
|
||||
普K序列: Vec<Py<K线Py>>,
|
||||
缠K序列: &Bound<'_, PyList>,
|
||||
普K序列: &Bound<'_, PyList>,
|
||||
配置: &Bound<'_, 缠论配置Py>,
|
||||
py: Python<'_>,
|
||||
) -> PyResult<(String, Option<Py<PyAny>>)> {
|
||||
let ck_inner = (*当前K线.borrow().inner).clone();
|
||||
let config = 配置.borrow().to_rust_config(py)?;
|
||||
|
||||
let mut ck_seq: Vec<_> = 缠K序列
|
||||
.iter()
|
||||
.map(|k| std::sync::Arc::clone(&k.bind(py).borrow().inner))
|
||||
.collect();
|
||||
let mut bar_seq: Vec<_> = 普K序列
|
||||
.iter()
|
||||
.map(|k| k.bind(py).borrow().inner.clone())
|
||||
.collect();
|
||||
// 从 Python 列表提取
|
||||
let mut ck_seq = Vec::with_capacity(缠K序列.len());
|
||||
for item in 缠K序列.iter() {
|
||||
let ck: PyRef<'_, Self> = item.extract()?;
|
||||
ck_seq.push(std::sync::Arc::clone(&ck.inner));
|
||||
}
|
||||
let mut bar_seq = Vec::with_capacity(普K序列.len());
|
||||
for item in 普K序列.iter() {
|
||||
let bar: PyRef<'_, K线Py> = item.extract()?;
|
||||
bar_seq.push(bar.inner.clone());
|
||||
}
|
||||
|
||||
let (status, fractal) = chanlun::kline::chan_kline::缠论K线::分析(
|
||||
ck_inner,
|
||||
@@ -690,6 +700,16 @@ impl 缠论K线Py {
|
||||
&config,
|
||||
);
|
||||
|
||||
// 写回 Python 列表(clear + extend)
|
||||
缠K序列.call_method0("clear")?;
|
||||
for k in ck_seq {
|
||||
缠K序列.call_method1("append", (chan_kline_to_py(py, k),))?;
|
||||
}
|
||||
普K序列.call_method0("clear")?;
|
||||
for k in bar_seq {
|
||||
普K序列.call_method1("append", (bar_to_py(py, k),))?;
|
||||
}
|
||||
|
||||
Ok((status, fractal.map(|f| fractal_to_py(py, f).into_any())))
|
||||
}
|
||||
|
||||
|
||||
+96
-15
@@ -102,9 +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;
|
||||
|
||||
@@ -120,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(|| {
|
||||
@@ -137,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: 技术指标
|
||||
@@ -175,6 +252,10 @@ 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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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/czsc,Apache License 2.0)。
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use chanlun::signal::engine::{self, SignalConfig, SignalEngine as 核心SignalEngine};
|
||||
|
||||
use pyo3::exceptions::{PyKeyError, PyValueError};
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::PyDict;
|
||||
|
||||
use crate::business_py::{立体分析器Py, 观察者Py};
|
||||
use crate::signal_py::{SignalPy, 时间戳转datetime};
|
||||
|
||||
// ======== 工具函数 ========
|
||||
|
||||
/// 将 PyAny 转换为 `serde_json::Value`。
|
||||
/// 尝试顺序:i64 → f64 → String → bool → 兜底转为 String。
|
||||
fn py_any_to_json_value(obj: &Bound<'_, PyAny>) -> PyResult<serde_json::Value> {
|
||||
// i64
|
||||
if let Ok(i) = obj.extract::<i64>() {
|
||||
return Ok(serde_json::Value::Number(i.into()));
|
||||
}
|
||||
// f64
|
||||
if let Ok(f) = obj.extract::<f64>() {
|
||||
if let Some(n) = serde_json::Number::from_f64(f) {
|
||||
return Ok(serde_json::Value::Number(n));
|
||||
}
|
||||
return Ok(serde_json::Value::String(f.to_string()));
|
||||
}
|
||||
// String
|
||||
if let Ok(s) = obj.extract::<String>() {
|
||||
return Ok(serde_json::Value::String(s));
|
||||
}
|
||||
// bool
|
||||
if let Ok(b) = obj.extract::<bool>() {
|
||||
return Ok(serde_json::Value::Bool(b));
|
||||
}
|
||||
// fallback: Python repr as string
|
||||
Ok(serde_json::Value::String(obj.to_string()))
|
||||
}
|
||||
|
||||
/// 将 `PyDict` 转换为 `HashMap<String, serde_json::Value>`。
|
||||
pub(crate) fn py_dict_to_params(
|
||||
dict: &Bound<'_, PyDict>,
|
||||
) -> PyResult<HashMap<String, serde_json::Value>> {
|
||||
let mut params = HashMap::new();
|
||||
for (k, v) in dict.iter() {
|
||||
let key: String = k.extract()?;
|
||||
let value = py_any_to_json_value(&v)?;
|
||||
params.insert(key, value);
|
||||
}
|
||||
Ok(params)
|
||||
}
|
||||
|
||||
// ======== 自由函数 ========
|
||||
|
||||
/// 通过 Rust 注册表按名调用单个信号函数。
|
||||
///
|
||||
/// Args:
|
||||
/// name: 注册的信号名,如 ``"youwukuncheng_中枢第三买卖点_V230602"``
|
||||
/// obs: 观察者Py 实例
|
||||
/// params: 信号参数字典(不含 name)
|
||||
///
|
||||
/// Returns:
|
||||
/// SignalPy 对象列表
|
||||
///
|
||||
/// Raises:
|
||||
/// PyValueError: 信号名未注册
|
||||
#[pyfunction]
|
||||
pub fn call_signal(
|
||||
name: &str,
|
||||
obs: &观察者Py,
|
||||
params: &Bound<'_, PyDict>,
|
||||
) -> PyResult<Vec<SignalPy>> {
|
||||
let obs_ref = obs.obs();
|
||||
let params_map = py_dict_to_params(params)?;
|
||||
|
||||
let inner =
|
||||
engine::call_signal(name, &obs_ref, ¶ms_map).map_err(|e| PyValueError::new_err(e))?;
|
||||
|
||||
Ok(inner.into_iter().map(|s| SignalPy { inner: s }).collect())
|
||||
}
|
||||
|
||||
/// 列出所有已注册的信号名(编译时 + 动态)。
|
||||
#[pyfunction]
|
||||
pub fn list_signals() -> Vec<String> {
|
||||
chanlun::signal::registry::list_signal_names()
|
||||
}
|
||||
|
||||
/// 按名获取信号参数模板(编译时 + 动态)。
|
||||
#[pyfunction]
|
||||
pub fn get_signal_template(name: &str) -> Option<String> {
|
||||
chanlun::signal::registry::get_template(name)
|
||||
}
|
||||
|
||||
// ======== 动态注册 API ========
|
||||
|
||||
/// 从动态注册表中移除信号。
|
||||
#[pyfunction]
|
||||
fn unregister_signal(name: &str) -> PyResult<()> {
|
||||
chanlun::signal::registry::unregister_signal(name).map_err(|e| PyValueError::new_err(e))
|
||||
}
|
||||
|
||||
// ======== 信号引擎 pyclass ========
|
||||
|
||||
/// Rust 信号计算引擎的 Python 绑定。
|
||||
///
|
||||
/// 用法::
|
||||
///
|
||||
/// from chanlun._chanlun import 信号引擎
|
||||
///
|
||||
/// 引擎 = 信号引擎([
|
||||
/// {"name": "youwukuncheng_中枢第三买卖点_V230602",
|
||||
/// "freq": 86400, "max_overlap": 3,
|
||||
/// "本级完整性": "实", "同级完整性": "合"},
|
||||
/// ])
|
||||
/// 引擎.自动挂载指标(分析器)
|
||||
/// 结果 = 引擎.更新(分析器) # dict[str, str]
|
||||
#[pyclass(name = "信号引擎", module = "chanlun._chanlun")]
|
||||
pub struct SignalEnginePy {
|
||||
inner: 核心SignalEngine,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl SignalEnginePy {
|
||||
/// 创建信号引擎。
|
||||
///
|
||||
/// Args:
|
||||
/// 信号配置: 信号配置字典列表,每项必须含 ``"name"`` 和 ``"freq"``。
|
||||
#[new]
|
||||
#[pyo3(signature = (信号配置=None))]
|
||||
fn new(信号配置: Option<Vec<Bound<'_, PyDict>>>) -> PyResult<Self> {
|
||||
let configs = match 信号配置 {
|
||||
Some(list) => {
|
||||
let mut configs = Vec::with_capacity(list.len());
|
||||
for d in &list {
|
||||
let name: String = d
|
||||
.get_item("name")?
|
||||
.ok_or_else(|| PyValueError::new_err("信号配置缺少 'name'"))?
|
||||
.extract()?;
|
||||
|
||||
let freq_raw = d
|
||||
.get_item("freq")?
|
||||
.ok_or_else(|| PyKeyError::new_err("信号配置缺少 'freq'"))?;
|
||||
|
||||
// freq 可以是 int 或 str
|
||||
let freq: i64 = if let Ok(i) = freq_raw.extract::<i64>() {
|
||||
i
|
||||
} else if let Ok(s) = freq_raw.extract::<String>() {
|
||||
s.parse::<i64>().map_err(|_| {
|
||||
PyValueError::new_err(format!("freq 无法解析为整数: {s}"))
|
||||
})?
|
||||
} else {
|
||||
return Err(PyValueError::new_err(format!(
|
||||
"freq 类型无效: {}",
|
||||
freq_raw.get_type().name()?
|
||||
)));
|
||||
};
|
||||
|
||||
// 构建 params(排除 "name",保留 "freq" 为字符串格式)
|
||||
let mut params = HashMap::new();
|
||||
for (k, v) in d.iter() {
|
||||
let key: String = k.extract()?;
|
||||
if key == "name" {
|
||||
continue;
|
||||
}
|
||||
if key == "freq" {
|
||||
// 统一为字符串,便于 Rust 信号函数通过 params::get_string 读取
|
||||
params.insert(key, serde_json::Value::String(freq.to_string()));
|
||||
continue;
|
||||
}
|
||||
let value = py_any_to_json_value(&v)?;
|
||||
params.insert(key, value);
|
||||
}
|
||||
|
||||
configs.push(SignalConfig {
|
||||
signal_name: name,
|
||||
freq,
|
||||
params,
|
||||
});
|
||||
}
|
||||
configs
|
||||
}
|
||||
None => Vec::new(),
|
||||
};
|
||||
Ok(Self {
|
||||
inner: 核心SignalEngine::new(configs),
|
||||
})
|
||||
}
|
||||
|
||||
/// 扫描所有配置中的 MACD / 均线关键字,为各周期 observer 的配置添加缺失的指标参数。
|
||||
fn 自动挂载指标(&self, analyzer: &立体分析器Py) {
|
||||
self.inner.自动挂载指标(&analyzer.inner);
|
||||
}
|
||||
|
||||
/// 遍历所有配置,执行信号函数,收集非空结果。
|
||||
///
|
||||
/// Returns:
|
||||
/// ``dict[str, str]`` — 信号 key → 信号 value(已过滤 "任意_任意_任意_0")
|
||||
fn 更新(&self, analyzer: &立体分析器Py) -> HashMap<String, String> {
|
||||
self.inner.更新(&analyzer.inner)
|
||||
}
|
||||
|
||||
/// 更新信号并返回完整结果(信号 + 行情)。
|
||||
/// 返回 dict: ``{"signals": {...}, "market": {...}}``,若无基础周期 K 线则 market 为 None。
|
||||
fn 更新_完整<'py>(
|
||||
&self, py: Python<'py>, analyzer: &立体分析器Py
|
||||
) -> PyResult<Py<PyAny>> {
|
||||
let result = self.inner.更新_完整(&analyzer.inner);
|
||||
let d = PyDict::new(py);
|
||||
|
||||
let signals_dict = PyDict::new(py);
|
||||
for (k, v) in &result.signals {
|
||||
signals_dict.set_item(k, v)?;
|
||||
}
|
||||
d.set_item("signals", signals_dict)?;
|
||||
|
||||
if let Some(m) = &result.market {
|
||||
let md = PyDict::new(py);
|
||||
md.set_item("symbol", &m.symbol)?;
|
||||
md.set_item("dt", 时间戳转datetime(py, m.dt)?)?;
|
||||
md.set_item("id", m.id)?;
|
||||
md.set_item("open", m.open)?;
|
||||
md.set_item("high", m.high)?;
|
||||
md.set_item("low", m.low)?;
|
||||
md.set_item("close", m.close)?;
|
||||
md.set_item("vol", m.vol)?;
|
||||
d.set_item("market", md)?;
|
||||
} else {
|
||||
d.set_item("market", py.None())?;
|
||||
}
|
||||
|
||||
Ok(d.into())
|
||||
}
|
||||
|
||||
/// 返回配置数量
|
||||
fn __len__(&self) -> usize {
|
||||
self.inner.len()
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!("信号引擎(configs={})", self.inner.len())
|
||||
}
|
||||
}
|
||||
|
||||
/// 注册模块。
|
||||
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<SignalEnginePy>()?;
|
||||
m.add_function(wrap_pyfunction!(call_signal, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(list_signals, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(get_signal_template, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(unregister_signal, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -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_score(7段),收到 {s}"
|
||||
)));
|
||||
}
|
||||
let score: i32 = parts[6]
|
||||
.parse()
|
||||
.map_err(|_| PyValueError::new_err(format!("无法解析 score: {}", parts[6])))?;
|
||||
Ok(核心Signal::new(
|
||||
parts[0], parts[1], parts[2], parts[3], parts[4], parts[5], score,
|
||||
))
|
||||
}
|
||||
|
||||
/// 反序列化辅助:从 dict 取信号串列表,逐个解析为核心 Signal。
|
||||
fn 取信号列表(raw: &Bound<'_, PyDict>, key: &str) -> PyResult<Vec<核心Signal>> {
|
||||
let mut out = Vec::new();
|
||||
if let Some(item) = raw.get_item(key)? {
|
||||
let strs: Vec<String> = item.extract()?;
|
||||
for s in strs {
|
||||
out.push(parse_signal_str(&s)?);
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// 反序列化辅助:从 dict 取事件列表,逐个调用 Event.load。
|
||||
fn 取事件列表(raw: &Bound<'_, PyDict>, key: &str) -> PyResult<Vec<核心Event>> {
|
||||
let mut out = Vec::new();
|
||||
if let Some(item) = raw.get_item(key)? {
|
||||
let dicts: Vec<Bound<'_, PyDict>> = item.extract()?;
|
||||
for d in &dicts {
|
||||
out.push(EventPy::load(d)?.inner);
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Signal 绑定。
|
||||
#[pyclass(name = "Signal", module = "chanlun._chanlun")]
|
||||
#[derive(Clone)]
|
||||
pub struct SignalPy {
|
||||
pub(crate) inner: 核心Signal,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl SignalPy {
|
||||
#[new]
|
||||
#[pyo3(signature = (signal=String::new(), score=0, k1="任意".to_string(), k2="任意".to_string(), k3="任意".to_string(), v1="任意".to_string(), v2="任意".to_string(), v3="任意".to_string()))]
|
||||
fn new(
|
||||
signal: String,
|
||||
score: i32,
|
||||
k1: String,
|
||||
k2: String,
|
||||
k3: String,
|
||||
v1: String,
|
||||
v2: String,
|
||||
v3: String,
|
||||
) -> PyResult<Self> {
|
||||
let inner = if signal.is_empty() {
|
||||
核心Signal::new(&k1, &k2, &k3, &v1, &v2, &v3, score)
|
||||
} else {
|
||||
parse_signal_str(&signal)?
|
||||
};
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn signal(&self) -> String {
|
||||
self.inner.signal.clone()
|
||||
}
|
||||
#[getter]
|
||||
fn score(&self) -> i32 {
|
||||
self.inner.score
|
||||
}
|
||||
#[getter]
|
||||
fn k1(&self) -> String {
|
||||
self.inner.k1.clone()
|
||||
}
|
||||
#[getter]
|
||||
fn k2(&self) -> String {
|
||||
self.inner.k2.clone()
|
||||
}
|
||||
#[getter]
|
||||
fn k3(&self) -> String {
|
||||
self.inner.k3.clone()
|
||||
}
|
||||
#[getter]
|
||||
fn v1(&self) -> String {
|
||||
self.inner.v1.clone()
|
||||
}
|
||||
#[getter]
|
||||
fn v2(&self) -> String {
|
||||
self.inner.v2.clone()
|
||||
}
|
||||
#[getter]
|
||||
fn v3(&self) -> String {
|
||||
self.inner.v3.clone()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn key(&self) -> String {
|
||||
self.inner.key()
|
||||
}
|
||||
#[getter]
|
||||
fn value(&self) -> String {
|
||||
self.inner.value()
|
||||
}
|
||||
|
||||
fn is_match(&self, s: &Bound<'_, PyDict>) -> PyResult<bool> {
|
||||
let 字典 = 字典转核心(s)?;
|
||||
self.inner
|
||||
.is_match(&字典)
|
||||
.map_err(|e| PyValueError::new_err(format!("{} 不在信号列表中", e.0)))
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!("Signal('{}')", self.inner.signal)
|
||||
}
|
||||
}
|
||||
|
||||
/// Factor 绑定。signals_all 全满足 + signals_any 任一满足 + signals_not 全不满足。
|
||||
#[pyclass(name = "Factor", module = "chanlun._chanlun")]
|
||||
#[derive(Clone)]
|
||||
pub struct FactorPy {
|
||||
pub(crate) inner: 核心Factor,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl FactorPy {
|
||||
#[new]
|
||||
#[pyo3(signature = (signals_all, signals_any=Vec::new(), signals_not=Vec::new(), name=String::new()))]
|
||||
fn new(
|
||||
signals_all: Vec<SignalPy>,
|
||||
signals_any: Vec<SignalPy>,
|
||||
signals_not: Vec<SignalPy>,
|
||||
name: String,
|
||||
) -> PyResult<Self> {
|
||||
let 取 = |v: Vec<SignalPy>| v.into_iter().map(|s| s.inner).collect::<Vec<_>>();
|
||||
let inner = 核心Factor::新建(取(signals_all), 取(signals_any), 取(signals_not), name)
|
||||
.map_err(PyValueError::new_err)?;
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn name(&self) -> String {
|
||||
self.inner.name.clone()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn signals_all(&self) -> Vec<SignalPy> {
|
||||
self.inner
|
||||
.signals_all
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|inner| SignalPy { inner })
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn signals_any(&self) -> Vec<SignalPy> {
|
||||
self.inner
|
||||
.signals_any
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|inner| SignalPy { inner })
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn signals_not(&self) -> Vec<SignalPy> {
|
||||
self.inner
|
||||
.signals_not
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|inner| SignalPy { inner })
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn unique_signals(&self) -> Vec<String> {
|
||||
self.inner.unique_signals()
|
||||
}
|
||||
|
||||
fn is_match(&self, s: &Bound<'_, PyDict>) -> PyResult<bool> {
|
||||
let 字典 = 字典转核心(s)?;
|
||||
self.inner
|
||||
.is_match(&字典)
|
||||
.map_err(|e| PyValueError::new_err(format!("{} 不在信号列表中", e.0)))
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!("Factor('{}')", self.inner.name)
|
||||
}
|
||||
|
||||
/// 序列化为 dict:{name, signals_all, signals_any, signals_not}(signals 存为信号串)。
|
||||
fn dump<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
|
||||
let d = PyDict::new(py);
|
||||
let 串 = |v: &[核心Signal]| v.iter().map(|s| s.signal.clone()).collect::<Vec<_>>();
|
||||
d.set_item("name", &self.inner.name)?;
|
||||
d.set_item("signals_all", 串(&self.inner.signals_all))?;
|
||||
d.set_item("signals_any", 串(&self.inner.signals_any))?;
|
||||
d.set_item("signals_not", 串(&self.inner.signals_not))?;
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
/// 从 dict 反序列化(对应旧 Python Factor.load)。
|
||||
#[staticmethod]
|
||||
fn load(raw: &Bound<'_, PyDict>) -> PyResult<FactorPy> {
|
||||
let inner = 核心Factor::新建(
|
||||
取信号列表(raw, "signals_all")?,
|
||||
取信号列表(raw, "signals_any")?,
|
||||
取信号列表(raw, "signals_not")?,
|
||||
取字符串(raw, "name")?,
|
||||
)
|
||||
.map_err(PyValueError::new_err)?;
|
||||
Ok(FactorPy { inner })
|
||||
}
|
||||
}
|
||||
|
||||
/// Event 绑定。operate + 因子列表(任一因子满足则事件为真)。
|
||||
#[pyclass(name = "Event", module = "chanlun._chanlun")]
|
||||
#[derive(Clone)]
|
||||
pub struct EventPy {
|
||||
pub(crate) inner: 核心Event,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl EventPy {
|
||||
#[new]
|
||||
#[pyo3(signature = (operate, factors, signals_all=Vec::new(), signals_any=Vec::new(), signals_not=Vec::new(), name=String::new()))]
|
||||
fn new(
|
||||
operate: OperatePy,
|
||||
factors: Vec<FactorPy>,
|
||||
signals_all: Vec<SignalPy>,
|
||||
signals_any: Vec<SignalPy>,
|
||||
signals_not: Vec<SignalPy>,
|
||||
name: String,
|
||||
) -> PyResult<Self> {
|
||||
let 取s = |v: Vec<SignalPy>| v.into_iter().map(|s| s.inner).collect::<Vec<_>>();
|
||||
let 取f = |v: Vec<FactorPy>| v.into_iter().map(|f| f.inner).collect::<Vec<_>>();
|
||||
let inner = 核心Event::新建(
|
||||
operate.转核心(),
|
||||
取f(factors),
|
||||
取s(signals_all),
|
||||
取s(signals_any),
|
||||
取s(signals_not),
|
||||
name,
|
||||
)
|
||||
.map_err(PyValueError::new_err)?;
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn name(&self) -> String {
|
||||
self.inner.name.clone()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn sha256(&self) -> String {
|
||||
self.inner.sha256.clone()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn operate(&self) -> OperatePy {
|
||||
OperatePy::从核心(self.inner.operate)
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn factors(&self) -> Vec<FactorPy> {
|
||||
self.inner
|
||||
.factors
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|inner| FactorPy { inner })
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn unique_signals(&self) -> Vec<String> {
|
||||
self.inner.unique_signals()
|
||||
}
|
||||
|
||||
fn is_match(&self, s: &Bound<'_, PyDict>) -> PyResult<(bool, Option<String>)> {
|
||||
let 字典 = 字典转核心(s)?;
|
||||
self.inner
|
||||
.is_match(&字典)
|
||||
.map_err(|e| PyValueError::new_err(format!("{} 不在信号列表中", e.0)))
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!("Event('{}')", self.inner.name)
|
||||
}
|
||||
|
||||
/// 序列化为 dict:{name, operate, signals_all/any/not, factors}。
|
||||
fn dump<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
|
||||
let d = PyDict::new(py);
|
||||
let 串 = |v: &[核心Signal]| v.iter().map(|s| s.signal.clone()).collect::<Vec<_>>();
|
||||
d.set_item("name", &self.inner.name)?;
|
||||
d.set_item("operate", self.inner.operate.value())?;
|
||||
d.set_item("signals_all", 串(&self.inner.signals_all))?;
|
||||
d.set_item("signals_any", 串(&self.inner.signals_any))?;
|
||||
d.set_item("signals_not", 串(&self.inner.signals_not))?;
|
||||
let factors: Vec<Bound<'py, PyDict>> = self
|
||||
.inner
|
||||
.factors
|
||||
.iter()
|
||||
.map(|f| FactorPy { inner: f.clone() }.dump(py))
|
||||
.collect::<PyResult<_>>()?;
|
||||
d.set_item("factors", factors)?;
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
/// 从 dict 反序列化(对应旧 Python Event.load)。
|
||||
#[staticmethod]
|
||||
fn load(raw: &Bound<'_, PyDict>) -> PyResult<EventPy> {
|
||||
let operate = OperatePy::from_value(&取字符串(raw, "operate")?)?.转核心();
|
||||
let mut factors = Vec::new();
|
||||
if let Some(item) = raw.get_item("factors")? {
|
||||
let dicts: Vec<Bound<'_, PyDict>> = item.extract()?;
|
||||
for fd in dicts {
|
||||
factors.push(FactorPy::load(&fd)?.inner);
|
||||
}
|
||||
}
|
||||
let inner = 核心Event::新建(
|
||||
operate,
|
||||
factors,
|
||||
取信号列表(raw, "signals_all")?,
|
||||
取信号列表(raw, "signals_any")?,
|
||||
取信号列表(raw, "signals_not")?,
|
||||
取字符串(raw, "name")?,
|
||||
)
|
||||
.map_err(PyValueError::new_err)?;
|
||||
Ok(EventPy { inner })
|
||||
}
|
||||
}
|
||||
|
||||
/// Position 绑定(可子类化)。Python 子类应实现 update() 状态机。
|
||||
#[pyclass(name = "Position", module = "chanlun._chanlun", subclass)]
|
||||
#[derive(Clone)]
|
||||
pub struct PositionPy {
|
||||
pub(crate) inner: 核心Position,
|
||||
}
|
||||
|
||||
/// 核心 Operate → PyO3 OperatePy 枚举变体映射。
|
||||
fn 核心op转pyop(op: 核心Operate) -> OperatePy {
|
||||
match op {
|
||||
核心Operate::持多 => OperatePy::HL,
|
||||
核心Operate::持空 => OperatePy::HS,
|
||||
核心Operate::持币 => OperatePy::HO,
|
||||
核心Operate::开多 => OperatePy::LO,
|
||||
核心Operate::平多 => OperatePy::LE,
|
||||
核心Operate::开空 => OperatePy::SO,
|
||||
核心Operate::平空 => OperatePy::SE,
|
||||
}
|
||||
}
|
||||
|
||||
/// 将 i64 Unix 时间戳转为 Python datetime(UTC)。
|
||||
pub(crate) fn 时间戳转datetime(py: Python<'_>, ts: i64) -> PyResult<Py<PyAny>> {
|
||||
let datetime_mod = py.import("datetime")?;
|
||||
let tz = datetime_mod.getattr("timezone")?.getattr("utc")?;
|
||||
let dt = datetime_mod
|
||||
.getattr("datetime")?
|
||||
.call_method1("fromtimestamp", (ts as f64, tz))?;
|
||||
Ok(dt.into())
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PositionPy {
|
||||
#[new]
|
||||
#[pyo3(signature = (symbol, opens, exits=Vec::new(), interval=0, timeout=1000, stop_loss=1000, T0=false, name=String::new()))]
|
||||
fn new(
|
||||
symbol: String,
|
||||
opens: Vec<EventPy>,
|
||||
exits: Vec<EventPy>,
|
||||
interval: i64,
|
||||
timeout: i64,
|
||||
stop_loss: i64,
|
||||
T0: bool,
|
||||
name: String,
|
||||
) -> PyResult<Self> {
|
||||
let 取 = |v: Vec<EventPy>| v.into_iter().map(|e| e.inner).collect::<Vec<_>>();
|
||||
let inner = 核心Position::新建(
|
||||
symbol,
|
||||
取(opens),
|
||||
取(exits),
|
||||
interval,
|
||||
timeout,
|
||||
stop_loss,
|
||||
T0,
|
||||
name,
|
||||
)
|
||||
.map_err(PyValueError::new_err)?;
|
||||
Ok(Self { inner })
|
||||
}
|
||||
|
||||
// --- 配置 getter(不变)---
|
||||
#[getter]
|
||||
fn symbol(&self) -> String {
|
||||
self.inner.symbol.clone()
|
||||
}
|
||||
#[getter]
|
||||
fn name(&self) -> String {
|
||||
self.inner.name.clone()
|
||||
}
|
||||
#[getter]
|
||||
fn opens(&self) -> Vec<EventPy> {
|
||||
self.inner
|
||||
.opens
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|inner| EventPy { inner })
|
||||
.collect()
|
||||
}
|
||||
#[getter]
|
||||
fn exits(&self) -> Vec<EventPy> {
|
||||
self.inner
|
||||
.exits
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|inner| EventPy { inner })
|
||||
.collect()
|
||||
}
|
||||
#[getter]
|
||||
fn events(&self) -> Vec<EventPy> {
|
||||
self.inner
|
||||
.events
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|inner| EventPy { inner })
|
||||
.collect()
|
||||
}
|
||||
#[getter]
|
||||
fn interval(&self) -> i64 {
|
||||
self.inner.interval
|
||||
}
|
||||
#[getter]
|
||||
fn timeout(&self) -> i64 {
|
||||
self.inner.timeout
|
||||
}
|
||||
#[getter]
|
||||
fn stop_loss(&self) -> i64 {
|
||||
self.inner.stop_loss
|
||||
}
|
||||
#[getter]
|
||||
fn T0(&self) -> bool {
|
||||
self.inner.T0
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn unique_signals(&self) -> Vec<String> {
|
||||
self.inner.unique_signals()
|
||||
}
|
||||
|
||||
// --- 状态 getter(新增)---
|
||||
#[getter]
|
||||
fn pos(&self) -> i32 {
|
||||
self.inner.pos
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn pos_changed(&self) -> bool {
|
||||
self.inner.pos_changed
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn operates<'py>(&self, py: Python<'py>) -> PyResult<Vec<Bound<'py, PyDict>>> {
|
||||
self.inner
|
||||
.operates
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let d = PyDict::new(py);
|
||||
d.set_item("symbol", &r.symbol)?;
|
||||
d.set_item("dt", 时间戳转datetime(py, r.dt)?)?;
|
||||
d.set_item("bid", r.bid)?;
|
||||
d.set_item("price", r.price)?;
|
||||
d.set_item("op", 核心op转pyop(r.op))?;
|
||||
d.set_item("op_desc", &r.op_desc)?;
|
||||
d.set_item("pos", r.pos)?;
|
||||
Ok(d)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn holds<'py>(&self, py: Python<'py>) -> PyResult<Vec<Bound<'py, PyDict>>> {
|
||||
self.inner
|
||||
.holds
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let d = PyDict::new(py);
|
||||
d.set_item("dt", 时间戳转datetime(py, r.dt)?)?;
|
||||
d.set_item("pos", r.pos)?;
|
||||
d.set_item("price", r.price)?;
|
||||
Ok(d)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn pairs<'py>(&self, py: Python<'py>) -> PyResult<Vec<Bound<'py, PyDict>>> {
|
||||
self.inner
|
||||
.pairs()
|
||||
.iter()
|
||||
.map(|r| {
|
||||
let d = PyDict::new(py);
|
||||
d.set_item("标的代码", &r.标的代码)?;
|
||||
d.set_item("策略标记", &r.策略标记)?;
|
||||
d.set_item("交易方向", &r.交易方向)?;
|
||||
d.set_item("开仓时间", 时间戳转datetime(py, r.开仓时间)?)?;
|
||||
d.set_item("平仓时间", 时间戳转datetime(py, r.平仓时间)?)?;
|
||||
d.set_item("开仓价格", r.开仓价格)?;
|
||||
d.set_item("平仓价格", r.平仓价格)?;
|
||||
d.set_item("持仓K线数", r.持仓K线数)?;
|
||||
d.set_item("事件序列", &r.事件序列)?;
|
||||
d.set_item("持仓天数", r.持仓天数)?;
|
||||
d.set_item("盈亏比例", r.盈亏比例)?;
|
||||
Ok(d)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 更新持仓状态。接收一个信号字典(含 OHLCV 字段 + 信号键)。
|
||||
///
|
||||
/// 信号字典必须包含:``dt``(datetime 或 Unix 时间戳), ``close``(收盘价)。
|
||||
/// 可选:``id`` 或 ``bid``(K线序号)。
|
||||
#[pyo3(signature = (信号字典))]
|
||||
fn update(&mut self, 信号字典: &Bound<'_, PyDict>) -> PyResult<()> {
|
||||
// 1. 提取 dt(支持 datetime 对象和 int/float Unix 时间戳)
|
||||
let dt: i64 = match 信号字典.get_item("dt")? {
|
||||
Some(v) => {
|
||||
// 尝试 i64
|
||||
if let Ok(ts) = v.extract::<i64>() {
|
||||
ts
|
||||
// 尝试 f64
|
||||
} else if let Ok(ts) = v.extract::<f64>() {
|
||||
ts as i64
|
||||
// 尝试 datetime.timestamp()
|
||||
} else if let Ok(ts) = v.call_method0("timestamp") {
|
||||
(ts.extract::<f64>()?) as i64
|
||||
} else {
|
||||
return Err(PyValueError::new_err(
|
||||
"无法从信号字典中提取 dt 字段(需要 datetime 或 Unix 时间戳)",
|
||||
));
|
||||
}
|
||||
}
|
||||
None => return Err(PyValueError::new_err("信号字典缺少 dt 字段")),
|
||||
};
|
||||
|
||||
// 2. 提取 price
|
||||
let price: f64 = 信号字典
|
||||
.get_item("close")?
|
||||
.and_then(|v| v.extract::<f64>().ok())
|
||||
.ok_or_else(|| PyValueError::new_err("信号字典缺少 close 字段"))?;
|
||||
|
||||
// 3. 提取 bid(可选)
|
||||
let bid: i64 = 信号字典
|
||||
.get_item("id")?
|
||||
.or_else(|| 信号字典.get_item("bid").ok().flatten())
|
||||
.and_then(|v| v.extract::<i64>().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
// 4. 转换为信号字典(排除 OHLCV 键)
|
||||
let ohkcv_keys: std::collections::HashSet<&str> = [
|
||||
"symbol", "dt", "open", "high", "low", "close", "vol", "id", "bid",
|
||||
]
|
||||
.iter()
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
let mut signals: 信号字典 = HashMap::new();
|
||||
for (k, v) in 信号字典.iter() {
|
||||
let key: String = k.extract()?;
|
||||
if ohkcv_keys.contains(key.as_str()) {
|
||||
continue;
|
||||
}
|
||||
let 值 = match v.extract::<String>() {
|
||||
Ok(文本) if !文本.is_empty() => 匹配值::字符串(文本),
|
||||
_ => 匹配值::非字符串,
|
||||
};
|
||||
signals.insert(key, 值);
|
||||
}
|
||||
|
||||
// 5. 调用核心状态机
|
||||
self.inner
|
||||
.update(dt, price, bid, &signals)
|
||||
.map_err(|e| PyValueError::new_err(format!("{} 不在信号列表中", e.0)))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
format!(
|
||||
"Position(name={}, symbol={}, timeout={}, stop_loss={}BP, T0={}, interval={}s, pos={})",
|
||||
self.inner.name,
|
||||
self.inner.symbol,
|
||||
self.inner.timeout,
|
||||
self.inner.stop_loss,
|
||||
self.inner.T0,
|
||||
self.inner.interval,
|
||||
self.inner.pos
|
||||
)
|
||||
}
|
||||
|
||||
/// 序列化为 dict。
|
||||
/// `with_data=True` 时附带 state(pairs, holds);`with_data=False` 时仅配置。
|
||||
#[pyo3(signature = (with_data=false))]
|
||||
fn dump<'py>(&self, py: Python<'py>, with_data: bool) -> PyResult<Bound<'py, PyDict>> {
|
||||
let d = PyDict::new(py);
|
||||
d.set_item("symbol", &self.inner.symbol)?;
|
||||
d.set_item("name", &self.inner.name)?;
|
||||
let 事件dump = |evts: &[核心Event]| -> PyResult<Vec<Bound<'py, PyDict>>> {
|
||||
evts.iter()
|
||||
.map(|e| EventPy { inner: e.clone() }.dump(py))
|
||||
.collect()
|
||||
};
|
||||
d.set_item("opens", 事件dump(&self.inner.opens)?)?;
|
||||
d.set_item("exits", 事件dump(&self.inner.exits)?)?;
|
||||
d.set_item("interval", self.inner.interval)?;
|
||||
d.set_item("timeout", self.inner.timeout)?;
|
||||
d.set_item("stop_loss", self.inner.stop_loss)?;
|
||||
d.set_item("T0", self.inner.T0)?;
|
||||
if with_data {
|
||||
d.set_item("pairs", self.pairs(py)?)?;
|
||||
d.set_item("holds", self.holds(py)?)?;
|
||||
}
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
/// 从 dict 反序列化(仅配置,状态字段初始化为默认值)。
|
||||
#[staticmethod]
|
||||
fn load(raw: &Bound<'_, PyDict>) -> PyResult<PositionPy> {
|
||||
let symbol = 取字符串(raw, "symbol")?;
|
||||
let name = 取字符串(raw, "name")?;
|
||||
let interval: i64 = raw
|
||||
.get_item("interval")?
|
||||
.and_then(|v| v.extract().ok())
|
||||
.unwrap_or(0);
|
||||
let timeout: i64 = raw
|
||||
.get_item("timeout")?
|
||||
.and_then(|v| v.extract().ok())
|
||||
.unwrap_or(1000);
|
||||
let stop_loss: i64 = raw
|
||||
.get_item("stop_loss")?
|
||||
.and_then(|v| v.extract().ok())
|
||||
.unwrap_or(1000);
|
||||
let T0: bool = raw
|
||||
.get_item("T0")?
|
||||
.and_then(|v| v.extract().ok())
|
||||
.unwrap_or(false);
|
||||
|
||||
let opens = 取事件列表(raw, "opens")?;
|
||||
let exits = 取事件列表(raw, "exits")?;
|
||||
|
||||
let inner =
|
||||
核心Position::新建(symbol, opens, exits, interval, timeout, stop_loss, T0, name)
|
||||
.map_err(PyValueError::new_err)?;
|
||||
Ok(Self { inner })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<OperatePy>()?;
|
||||
m.add_class::<SignalPy>()?;
|
||||
m.add_class::<FactorPy>()?;
|
||||
m.add_class::<EventPy>()?;
|
||||
m.add_class::<PositionPy>()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -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())
|
||||
@@ -994,17 +937,23 @@ pub struct 线段特征Py {
|
||||
impl 线段特征Py {
|
||||
#[getter]
|
||||
fn 序号(&self) -> i64 {
|
||||
self.inner.序号
|
||||
self.inner.序号.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
#[setter]
|
||||
#[pyo3(name = "序号")]
|
||||
fn set_序号(&self, value: i64) {
|
||||
self.inner.序号.store(value, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
#[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]
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 分型结构 ==========
|
||||
|
||||
Binary file not shown.
+1635
-15
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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() 内部用法
|
||||
|
||||
|
||||
# ---- Position(Rust 基类 + Python 子类)----
|
||||
|
||||
|
||||
def test_position_requires_name():
|
||||
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
|
||||
e = Event(Operate.LO, [Factor(signals_all=[s])])
|
||||
with pytest.raises((ValueError, TypeError)):
|
||||
Position(symbol="btc", opens=[e])
|
||||
|
||||
|
||||
def test_position_config_getters():
|
||||
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
|
||||
e = Event(Operate.LO, [Factor(signals_all=[s])])
|
||||
p = Position(symbol="btc", opens=[e], name="中枢", timeout=500, stop_loss=200, T0=True)
|
||||
assert p.symbol == "btc" and p.name == "中枢"
|
||||
assert p.timeout == 500 and p.stop_loss == 200 and p.T0 is True
|
||||
assert len(p.events) == 1
|
||||
assert p.unique_signals == [s.signal]
|
||||
|
||||
|
||||
def test_position_subclassable_with_state():
|
||||
"""验证 Rust 基类可被 Python 子类化,状态字段由 Rust 初始化。
|
||||
|
||||
pos/pos_changed/operates/holds 等状态字段由 Rust 基类提供(只读 getter),
|
||||
初始值在构造时由 Rust #[new] 自动初始化。
|
||||
"""
|
||||
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
|
||||
e = Event(Operate.LO, [Factor(signals_all=[s])])
|
||||
p = Position(symbol="btc", opens=[e], name="中枢")
|
||||
assert p.name == "中枢" # Rust 基类 getter
|
||||
assert p.pos == 0 # Rust 初始化为 0 (空仓)
|
||||
assert p.pos_changed is False
|
||||
assert p.operates == []
|
||||
assert p.holds == []
|
||||
|
||||
|
||||
# ---- 序列化 dump/load ----
|
||||
|
||||
|
||||
def test_operate_from_value():
|
||||
assert Operate.from_value("开多") == Operate.LO
|
||||
assert Operate.from_value("平空") == Operate.SE
|
||||
with pytest.raises(ValueError):
|
||||
Operate.from_value("不存在")
|
||||
|
||||
|
||||
def test_factor_dump_load_roundtrip():
|
||||
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
|
||||
f = Factor(signals_all=[s], name="测试")
|
||||
d = f.dump()
|
||||
assert d["name"] == f.name
|
||||
assert d["signals_all"] == [s.signal]
|
||||
assert d["signals_any"] == [] and d["signals_not"] == []
|
||||
f2 = Factor.load(d)
|
||||
assert f2.name == f.name # 确定性哈希 → 同输入同名
|
||||
assert f2.unique_signals == f.unique_signals
|
||||
|
||||
|
||||
def test_event_dump_load_roundtrip():
|
||||
s = Signal(k1="14400", k2="D1MO3", k3="中枢", v2="三买")
|
||||
e = Event(Operate.LO, [Factor(signals_all=[s])])
|
||||
d = e.dump()
|
||||
assert d["operate"] == "开多"
|
||||
assert len(d["factors"]) == 1
|
||||
e2 = Event.load(d)
|
||||
assert e2.name == e.name
|
||||
assert e2.operate == Operate.LO
|
||||
|
||||
|
||||
def test_position_dump_load_roundtrip():
|
||||
"""Position 序列化:Rust 基类 dump 配置 + Python 子类 with_data/load 返回子类实例。"""
|
||||
from chanlun.chan_external import Position as PositionExt, Signal as S, Factor as F, Event as E, Operate as O
|
||||
|
||||
e = E(O.LO, [F(signals_all=[S(k1="14400", k2="D1MO3", k3="中枢", v2="三买")])])
|
||||
p = PositionExt(symbol="btc", opens=[e], name="中枢", timeout=500, T0=True)
|
||||
raw = p.dump()
|
||||
assert raw["symbol"] == "btc" and raw["T0"] is True and raw["timeout"] == 500
|
||||
assert len(raw["opens"]) == 1
|
||||
# with_data 附加 pairs/holds
|
||||
raw2 = p.dump(with_data=True)
|
||||
assert "pairs" in raw2 and "holds" in raw2
|
||||
# load 返回子类实例(含状态字段)
|
||||
p2 = PositionExt.load(raw)
|
||||
assert type(p2) is PositionExt
|
||||
assert p2.symbol == "btc" and p2.name == "中枢" and p2.timeout == 500
|
||||
assert p2.pos == 0 # 子类状态已初始化
|
||||
@@ -0,0 +1 @@
|
||||
/target/
|
||||
Generated
+47
@@ -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"
|
||||
@@ -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"
|
||||
@@ -0,0 +1,100 @@
|
||||
//! chanlun 信号注册 proc-macro。
|
||||
//!
|
||||
//! 第三方代码声明:`#[signal]` 注册机制参考 czsc 项目
|
||||
//! (https://github.com/waditu/czsc,Apache License 2.0),已简化适配
|
||||
//! (无 category / TaCache,签名固定为 fn(&观察者, &HashMap<String, Value>) -> Vec<Signal>)。
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::parse::Parser;
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::{Expr, ExprLit, ItemFn, Lit, Meta, Token};
|
||||
|
||||
/// `#[signal(name = "foo_V230101", template = "{freq}_D1_foo")]`
|
||||
///
|
||||
/// 校验:函数名含 `_V<数字>`;`name` 与函数名一致;`name`/`template` 非空。
|
||||
/// 生成:一个 `static` SignalDescriptor + `inventory::submit!`。
|
||||
///
|
||||
/// 路径:默认 `crate::signal::registry::`(chanlun crate 内部使用)。
|
||||
/// 外部 crate 使用需指定 `crate_path = "::chanlun"`。
|
||||
#[proc_macro_attribute]
|
||||
pub fn signal(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let parser = Punctuated::<Meta, Token![,]>::parse_terminated;
|
||||
let metas = match parser.parse(attr) {
|
||||
Ok(m) => m,
|
||||
Err(e) => return e.to_compile_error().into(),
|
||||
};
|
||||
|
||||
let mut name: Option<String> = None;
|
||||
let mut template: Option<String> = None;
|
||||
let mut crate_path: Option<String> = None;
|
||||
for m in metas {
|
||||
if let Meta::NameValue(nv) = m
|
||||
&& let Some(ident) = nv.path.get_ident()
|
||||
&& let Expr::Lit(ExprLit { lit: Lit::Str(v), .. }) = nv.value
|
||||
{
|
||||
match ident.to_string().as_str() {
|
||||
"name" => name = Some(v.value()),
|
||||
"template" => template = Some(v.value()),
|
||||
"crate_path" => crate_path = Some(v.value()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let f: ItemFn = match syn::parse(item) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return e.to_compile_error().into(),
|
||||
};
|
||||
|
||||
let name = name.unwrap_or_default();
|
||||
let template = template.unwrap_or_default();
|
||||
let fn_ident = &f.sig.ident;
|
||||
let fn_name = fn_ident.to_string();
|
||||
|
||||
let mut errors = Vec::new();
|
||||
if name.is_empty() || template.is_empty() {
|
||||
errors.push(quote! { compile_error!("#[signal] name/template 不能为空"); });
|
||||
}
|
||||
if name != fn_name {
|
||||
errors.push(quote! { compile_error!("#[signal] name 必须与函数名一致"); });
|
||||
}
|
||||
// 函数名须含 _V<数字>
|
||||
let 有版本 = fn_name
|
||||
.rsplit_once("_V")
|
||||
.map(|(_, v)| !v.is_empty() && v.chars().all(|c| c.is_ascii_digit()))
|
||||
.unwrap_or(false);
|
||||
if !有版本 {
|
||||
errors.push(quote! { compile_error!("#[signal] 函数名必须含 _V<版本号>,如 foo_V230101"); });
|
||||
}
|
||||
|
||||
if !errors.is_empty() {
|
||||
let errs = errors.into_iter();
|
||||
return quote! { #(#errs)* }.into();
|
||||
}
|
||||
|
||||
let descriptor_ident = syn::Ident::new(
|
||||
&format!("__SIG_DESC_{}", fn_name).to_uppercase(),
|
||||
fn_ident.span(),
|
||||
);
|
||||
|
||||
let path = crate_path.unwrap_or_else(|| "crate".to_string());
|
||||
let _registry_path: syn::Path = syn::parse_str(&format!("{path}::signal::registry")).unwrap();
|
||||
let signal_fn: syn::Type = syn::parse_str(&format!("{path}::signal::registry::SignalFn")).unwrap();
|
||||
let signal_desc: syn::Type = syn::parse_str(&format!("{path}::signal::registry::SignalDescriptor")).unwrap();
|
||||
|
||||
let expanded = quote! {
|
||||
#f
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
static #descriptor_ident: #signal_desc =
|
||||
#signal_desc {
|
||||
name: #name,
|
||||
template: #template,
|
||||
func: #fn_ident as #signal_fn,
|
||||
};
|
||||
|
||||
inventory::submit! { #descriptor_ident }
|
||||
};
|
||||
expanded.into()
|
||||
}
|
||||
+6
-3
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "chanlun"
|
||||
version = "26.6.1"
|
||||
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" }
|
||||
|
||||
+1034
-317
File diff suppressed because it is too large
Load Diff
+43
-44
@@ -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, 当前分型
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -645,16 +648,14 @@ impl 笔 {
|
||||
}
|
||||
|
||||
if let Some(前一笔) = 笔序列.last() {
|
||||
let seg = Arc::make_mut(&mut 新笔);
|
||||
seg.序号
|
||||
let 新筆 = Arc::make_mut(&mut 新笔);
|
||||
新筆
|
||||
.序号
|
||||
.store(前一笔.序号.load(Ordering::Relaxed) + 1, Ordering::Relaxed);
|
||||
if seg.武.read().unwrap().左.is_none() || seg.武.read().unwrap().右.is_none() {
|
||||
seg.有效性.store(false, Ordering::Relaxed);
|
||||
if 新筆.武.read().左.is_none() || 新筆.武.read().右.is_none() {
|
||||
新筆.有效性.store(false, Ordering::Relaxed);
|
||||
}
|
||||
if matches!(
|
||||
前一笔.武.read().unwrap().结构(),
|
||||
分型结构::上 | 分型结构::下
|
||||
) {
|
||||
if matches!(前一笔.武.read().结构(), 分型结构::上 | 分型结构::下) {
|
||||
error!("_添加新笔[{}] 出现无效分型 {}", 行号, 前一笔);
|
||||
}
|
||||
}
|
||||
@@ -674,7 +675,7 @@ impl 笔 {
|
||||
Self::_实际低点(&基础序列, 配置.笔内相同终点取舍),
|
||||
)
|
||||
&& Arc::ptr_eq(&筆.文.中, &实际高)
|
||||
&& Arc::ptr_eq(&筆.武.read().unwrap().中, &实际低)
|
||||
&& Arc::ptr_eq(&筆.武.read().中, &实际低)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -684,7 +685,7 @@ impl 笔 {
|
||||
Self::_实际高点(&基础序列, 配置.笔内相同终点取舍),
|
||||
)
|
||||
&& Arc::ptr_eq(&筆.文.中, &实际低)
|
||||
&& Arc::ptr_eq(&筆.武.read().unwrap().中, &实际高)
|
||||
&& Arc::ptr_eq(&筆.武.read().中, &实际高)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -694,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 笔序列;
|
||||
@@ -705,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);
|
||||
@@ -735,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().中));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -222,6 +218,6 @@ struct MACD面积 {
|
||||
|
||||
impl MACD面积 {
|
||||
fn 总(&self) -> f64 {
|
||||
self.阳 + self.阴
|
||||
self.阳 + self.阴.abs()
|
||||
}
|
||||
}
|
||||
|
||||
+246
-169
@@ -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::相对方向::分析(
|
||||
@@ -387,10 +400,10 @@ impl 中枢 {
|
||||
pub fn 创建(
|
||||
左: Arc<虚线>, 中: Arc<虚线>, 右: Arc<虚线>, 级别: i64, 标识: &str
|
||||
) -> Self {
|
||||
debug_assert!(Self::基础检查(&左, &中, &右), "中枢.创建 基础检查失败");
|
||||
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()));
|
||||
@@ -566,21 +555,15 @@ impl 中枢 {
|
||||
} else {
|
||||
if 候选序列.is_empty() {
|
||||
// 仍在范围内:延伸中枢
|
||||
debug_assert!(
|
||||
assert!(
|
||||
中枢序列[当前中枢_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()
|
||||
.方向()
|
||||
@@ -615,25 +597,123 @@ impl 中枢 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 结构化相等校验 — 递归校验基础序列虚线和第三买卖线,返回 (是否相等, 差异描述)
|
||||
pub fn 相等(&self, other: &Self, 浮点容差: f64) -> (bool, String) {
|
||||
if self.序号.load(Ordering::Relaxed) != other.序号.load(Ordering::Relaxed) {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"中枢: [序号] 不等 A={},B={}",
|
||||
self.序号.load(Ordering::Relaxed),
|
||||
other.序号.load(Ordering::Relaxed)
|
||||
),
|
||||
);
|
||||
}
|
||||
if *self.标识.read() != *other.标识.read() {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"中枢: [标识] 不等 A={},B={}",
|
||||
self.标识.read(),
|
||||
other.标识.read()
|
||||
),
|
||||
);
|
||||
}
|
||||
if self.级别.load(Ordering::Relaxed) != other.级别.load(Ordering::Relaxed) {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"中枢: [级别] 不等 A={},B={}",
|
||||
self.级别.load(Ordering::Relaxed),
|
||||
other.级别.load(Ordering::Relaxed)
|
||||
),
|
||||
);
|
||||
}
|
||||
// 基础序列
|
||||
let a_seq = self.基础序列.read();
|
||||
let b_seq = other.基础序列.read();
|
||||
if a_seq.len() != b_seq.len() {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"中枢: [基础序列] 长度不一致 A={},B={}",
|
||||
a_seq.len(),
|
||||
b_seq.len()
|
||||
),
|
||||
);
|
||||
}
|
||||
for (idx, (a, b)) in a_seq.iter().zip(b_seq.iter()).enumerate() {
|
||||
let (eq, msg) = a.相等(b, 浮点容差);
|
||||
if !eq {
|
||||
return (false, format!("中枢: 基础序列[{idx}]虚线异常 >> {msg}"));
|
||||
}
|
||||
}
|
||||
// 第三买卖线
|
||||
let 检查单个 = |名: &str,
|
||||
a: &Option<Arc<虚线>>,
|
||||
b: &Option<Arc<虚线>>,
|
||||
容差: f64|
|
||||
-> Result<(), String> {
|
||||
match (a, b) {
|
||||
(None, None) => Ok(()),
|
||||
(Some(x), Some(y)) => {
|
||||
let (eq, msg) = x.相等(y, 容差);
|
||||
if eq {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("中枢: [{名}]子虚线异常 >> {msg}"))
|
||||
}
|
||||
}
|
||||
_ => Err(format!(
|
||||
"中枢: [{名}]空值不一致 A={},B={}",
|
||||
a.is_some(),
|
||||
b.is_some()
|
||||
)),
|
||||
}
|
||||
};
|
||||
检查单个(
|
||||
"第三买卖线",
|
||||
&self.第三买卖线.read(),
|
||||
&other.第三买卖线.read(),
|
||||
浮点容差,
|
||||
)
|
||||
.map_err(|e| (false, e))
|
||||
.ok();
|
||||
检查单个(
|
||||
"本级_第三买卖线",
|
||||
&self.本级_第三买卖线.read(),
|
||||
&other.本级_第三买卖线.read(),
|
||||
浮点容差,
|
||||
)
|
||||
.map_err(|e| (false, e))
|
||||
.ok();
|
||||
(true, "中枢: 全部字段一致".into())
|
||||
}
|
||||
}
|
||||
|
||||
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.武(),
|
||||
@@ -758,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]
|
||||
@@ -784,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());
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -813,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]
|
||||
@@ -837,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());
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -870,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())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -937,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])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+590
-746
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
|
||||
|
||||
@@ -26,24 +26,21 @@ 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};
|
||||
|
||||
/// 立体分析器 — 多周期协调器
|
||||
///
|
||||
/// 包含一个K线合成器和每周期一个观察者。
|
||||
/// 输入最小周期K线,合成大周期后分发到对应观察者。
|
||||
pub struct 立体分析器 {
|
||||
pub 周期组: Vec<i64>,
|
||||
输入周期: i64,
|
||||
K线合成器: K线合成器,
|
||||
单体分析器: HashMap<i64, Arc<RwLock<观察者>>>,
|
||||
pub K线合成器: K线合成器,
|
||||
pub 单体分析器: HashMap<i64, Arc<RwLock<观察者>>>,
|
||||
}
|
||||
|
||||
impl 立体分析器 {
|
||||
/// 创建立体分析器,自动创建K线合成器 + 每周期一个观察者
|
||||
/// 创建立体分析器 — 对应 Python 立体分析器.__init__
|
||||
pub fn new(
|
||||
符号: String,
|
||||
周期组: Vec<i64>,
|
||||
@@ -58,16 +55,13 @@ impl 立体分析器 {
|
||||
let 默认配置 = 配置.unwrap_or_default();
|
||||
let 配置组 = 配置组.unwrap_or_default();
|
||||
|
||||
let K线合成器 = K线合成器::new(符号.clone(), 周期组.clone());
|
||||
|
||||
let mut 单体分析器 = HashMap::new();
|
||||
let mut 单体分析器: HashMap<i64, Arc<RwLock<观察者>>> = HashMap::new();
|
||||
for &周期 in &周期组 {
|
||||
let mut 当前配置 = 配置组
|
||||
.get(&周期)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| 默认配置.clone());
|
||||
当前配置.推送K线 = false;
|
||||
当前配置.推送线段 = false;
|
||||
当前配置.图表展示标签 = Some(vec![]);
|
||||
当前配置.标识 = 符号.clone();
|
||||
|
||||
let 观察员 = 观察者::new(符号.clone(), 周期, 当前配置);
|
||||
@@ -77,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.重置基础序列();
|
||||
}
|
||||
@@ -89,18 +81,30 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 对应 Python: K线合成器(符号, 周期组, self.__K线回调)
|
||||
let 单体分析器_回调 = 单体分析器.clone();
|
||||
let K线合成器 = K线合成器::new(
|
||||
符号.clone(),
|
||||
周期组.clone(),
|
||||
Some(Box::new(
|
||||
move |_信号类型: String, _标识: String, 周期: i64, 完成K线: K线| {
|
||||
立体分析器::__K线回调_调度(&单体分析器_回调, 周期, 完成K线);
|
||||
},
|
||||
)),
|
||||
);
|
||||
|
||||
Self {
|
||||
周期组,
|
||||
输入周期,
|
||||
@@ -109,8 +113,28 @@ impl 立体分析器 {
|
||||
}
|
||||
}
|
||||
|
||||
/// 投喂K线 — 统一入口,接收最小周期K线
|
||||
/// 匹配 Python __K线回调:合成器完成K线时喂给观察者
|
||||
/// __K线回调 — 对应 Python 立体分析器.__K线回调
|
||||
fn __K线回调(&self, _信号类型: String, _标识: String, 周期: i64, 完成K线: K线) {
|
||||
if let Some(观察员) = self.单体分析器.get(&周期) {
|
||||
let mut obs = 观察员.write();
|
||||
obs.增加原始K线(完成K线);
|
||||
// 对应 Python: if 当前K线 := self._K线合成器.获取当前K线(周期)
|
||||
// _完成K线刚清空当前K线,获取当前K线返回 None,所以这里不添加
|
||||
}
|
||||
}
|
||||
|
||||
/// 静态调度版本 — 用于回调闭包
|
||||
fn __K线回调_调度(
|
||||
单体分析器: &HashMap<i64, Arc<RwLock<观察者>>>,
|
||||
周期: i64,
|
||||
完成K线: K线,
|
||||
) {
|
||||
if let Some(观察员) = 单体分析器.get(&周期) {
|
||||
观察员.write().增加原始K线(完成K线);
|
||||
}
|
||||
}
|
||||
|
||||
/// 投喂K线 — 对应 Python 立体分析器.投喂K线
|
||||
pub fn 投喂K线(&mut self, 普K: K线) {
|
||||
if 普K.周期 != self.输入周期 {
|
||||
panic!(
|
||||
@@ -118,19 +142,7 @@ impl 立体分析器 {
|
||||
普K.周期, self.输入周期
|
||||
);
|
||||
}
|
||||
|
||||
// Feed to synthesizer, get completion events
|
||||
let 完成事件 = self.K线合成器.投喂K线(普K);
|
||||
|
||||
// Dispatch on completion events (matching Python's __K线回调)
|
||||
for (周期, 完成K线) in 完成事件 {
|
||||
if let Some(观察员) = self.单体分析器.get(&周期) {
|
||||
观察员.write().unwrap().增加原始K线(完成K线);
|
||||
if let Some(当前K线) = self.K线合成器.获取当前K线(周期) {
|
||||
观察员.write().unwrap().增加原始K线(当前K线.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
self.K线合成器.投喂K线(普K);
|
||||
}
|
||||
|
||||
/// 获取指定周期的观察者
|
||||
@@ -138,8 +150,7 @@ impl 立体分析器 {
|
||||
self.单体分析器.get(&周期).cloned()
|
||||
}
|
||||
|
||||
/// 测试_保存数据 — 多级别数据拆分保存
|
||||
/// 创建父目录 PyM_{标识}_{起始时间}_{结束时间},各周期观察者保存到子目录
|
||||
/// 测试_保存数据 — 对应 Python 立体分析器.测试_保存数据
|
||||
pub fn 测试_保存数据(&self, root: Option<&str>) {
|
||||
let 根目录 = match root {
|
||||
Some(r) => std::path::PathBuf::from(r),
|
||||
@@ -151,23 +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_{}:{}_{}_{}", 标识, 周期, 起始时间, 结束时间);
|
||||
@@ -182,11 +192,36 @@ impl 立体分析器 {
|
||||
if let Some(观察员) = self.单体分析器.get(周期) {
|
||||
观察员
|
||||
.read()
|
||||
.unwrap()
|
||||
.测试_保存数据(Some(&保存路径.to_string_lossy()));
|
||||
}
|
||||
}
|
||||
|
||||
info!("多级别数据拆分保存完成,目录:{}", 保存路径.display());
|
||||
warn!("多级别数据拆分保存完成,目录:{}", 保存路径.display());
|
||||
}
|
||||
|
||||
/// 相等 — 各周期观察者全量比对,对应 Python `立体分析器相等`
|
||||
pub fn 相等(&self, other: &Self, 浮点容差: f64) -> (bool, String) {
|
||||
let 标签 = format!("立体分析器校验[A={:?},B={:?}]", self.周期组, other.周期组);
|
||||
|
||||
if self.周期组 != other.周期组 {
|
||||
return (false, format!("{标签}: 周期组不一致"));
|
||||
}
|
||||
|
||||
for 周期 in &self.周期组 {
|
||||
let a_obs = match self.单体分析器.get(周期) {
|
||||
Some(o) => o.read(),
|
||||
None => return (false, format!("{标签}: 周期{周期} 观察者不存在 (A)")),
|
||||
};
|
||||
let b_obs = match other.单体分析器.get(周期) {
|
||||
Some(o) => o.read(),
|
||||
None => return (false, format!("{标签}: 周期{周期} 观察者不存在 (B)")),
|
||||
};
|
||||
let (eq, msg) = a_obs.相等(&b_obs, 浮点容差);
|
||||
if !eq {
|
||||
return (false, format!("{标签}: 周期{周期} >> {msg}"));
|
||||
}
|
||||
}
|
||||
|
||||
(true, format!("{标签}:所有周期观察者全量校验全部一致"))
|
||||
}
|
||||
}
|
||||
|
||||
+678
-310
File diff suppressed because it is too large
Load Diff
@@ -23,19 +23,29 @@
|
||||
*/
|
||||
|
||||
use crate::kline::bar::K线;
|
||||
use crate::warn;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// 事件回调类型 — fn(信号类型, 标识, 周期, 完成K线)
|
||||
type 合成器事件回调 = Box<dyn Fn(String, String, i64, K线) + Send + Sync>;
|
||||
|
||||
/// K线合成器 — 将小周期K线合成为大周期K线
|
||||
pub struct K线合成器 {
|
||||
pub 标识: String,
|
||||
pub 周期组: Vec<i64>,
|
||||
pub 当前K线: HashMap<i64, Option<K线>>,
|
||||
pub 合成K线列表: HashMap<i64, Vec<K线>>,
|
||||
/// 事件回调 — K线完成时触发,对应 Python K线合成器.事件回调
|
||||
/// 签名: fn(信号类型: str, 标识: str, 周期: i64, 完成K线: K线)
|
||||
/// 在 _完成K线 清空当前K线后、新K线创建前触发
|
||||
事件回调: Option<合成器事件回调>,
|
||||
}
|
||||
|
||||
impl K线合成器 {
|
||||
/// 创建K线合成器,按周期升序排列,初始化当前K线和合成K线列表
|
||||
pub fn new(标识: String, 周期组: Vec<i64>) -> Self {
|
||||
/// 创建K线合成器 — 对应 Python K线合成器.__init__(标识, 周期组, 事件回调=None)
|
||||
pub fn new(
|
||||
标识: String, 周期组: Vec<i64>, 事件回调: Option<合成器事件回调>
|
||||
) -> Self {
|
||||
let mut 周期组 = 周期组;
|
||||
周期组.sort();
|
||||
|
||||
@@ -51,37 +61,30 @@ impl K线合成器 {
|
||||
周期组,
|
||||
当前K线,
|
||||
合成K线列表,
|
||||
事件回调,
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置事件回调 — 对应 Python `设置事件回调`
|
||||
pub fn 设置事件回调(&mut self, 回调: 合成器事件回调) {
|
||||
self.事件回调 = Some(回调);
|
||||
}
|
||||
|
||||
/// 投喂 — 便捷入口,直接从 OHLCV 创建 K线 并投喂
|
||||
pub fn 投喂(
|
||||
&mut self,
|
||||
时间戳: i64,
|
||||
开: f64,
|
||||
高: f64,
|
||||
低: f64,
|
||||
收: f64,
|
||||
量: f64,
|
||||
) -> Vec<(i64, K线)> {
|
||||
pub fn 投喂(&mut self, 时间戳: i64, 开: f64, 高: f64, 低: f64, 收: f64, 量: f64) {
|
||||
let 普K = K线::创建普K(&self.标识, 时间戳, 开, 高, 低, 收, 量, 0, 0);
|
||||
self.投喂K线(普K)
|
||||
self.投喂K线(普K);
|
||||
}
|
||||
|
||||
/// 投喂K线 — 输入最小周期K线,合成为所有目标周期
|
||||
/// 返回本次投喂完成了哪些周期的K线(周期 → 完成K线)
|
||||
pub fn 投喂K线(&mut self, 普K: K线) -> Vec<(i64, K线)> {
|
||||
let mut 完成记录 = Vec::new();
|
||||
pub fn 投喂K线(&mut self, 普K: K线) {
|
||||
let 周期组 = self.周期组.clone();
|
||||
for 周期 in 周期组 {
|
||||
if let Some(完成K线) = self._处理单个周期(周期, &普K) {
|
||||
完成记录.push((周期, 完成K线));
|
||||
}
|
||||
self._处理单个周期(周期, &普K);
|
||||
}
|
||||
完成记录
|
||||
}
|
||||
|
||||
fn _处理单个周期(&mut self, 周期: i64, 普K: &K线) -> Option<K线> {
|
||||
fn _处理单个周期(&mut self, 周期: i64, 普K: &K线) {
|
||||
let 目标时间戳 = self._对齐时间戳(普K.时间戳, 周期);
|
||||
let 相同时间 = self.当前K线[&周期]
|
||||
.as_ref()
|
||||
@@ -91,19 +94,17 @@ impl K线合成器 {
|
||||
if self.当前K线[&周期].is_none() {
|
||||
let 新K线 = self._创建新K线(周期, 目标时间戳, 普K);
|
||||
self.当前K线.insert(周期, Some(新K线));
|
||||
None
|
||||
} else if 相同时间 {
|
||||
let ent = self.当前K线.get_mut(&周期).unwrap();
|
||||
Self::_更新K线(ent.as_mut().unwrap(), 普K);
|
||||
None
|
||||
} else {
|
||||
let 完成K线 = self._完成K线(周期);
|
||||
self._完成K线(周期);
|
||||
let 新K线 = self._创建新K线(周期, 目标时间戳, 普K);
|
||||
self.当前K线.insert(周期, Some(新K线));
|
||||
完成K线
|
||||
}
|
||||
}
|
||||
|
||||
/// 对齐时间戳到周期边界 — 对应 Python `_对齐时间戳`
|
||||
fn _对齐时间戳(&self, 时间戳: i64, 周期: i64) -> i64 {
|
||||
if 周期 == 0 {
|
||||
panic!("_对齐时间戳: 周期不能为0");
|
||||
@@ -111,6 +112,7 @@ impl K线合成器 {
|
||||
(时间戳 / 周期) * 周期
|
||||
}
|
||||
|
||||
/// 创建新K线 — 对应 Python `_创建新K线`
|
||||
fn _创建新K线(&self, 周期: i64, 时间戳: i64, 普K: &K线) -> K线 {
|
||||
let 序号 = self
|
||||
.合成K线列表
|
||||
@@ -132,6 +134,7 @@ impl K线合成器 {
|
||||
)
|
||||
}
|
||||
|
||||
/// 更新K线 — 对应 Python `_更新K线`
|
||||
fn _更新K线(当前K线: &mut K线, 新数据: &K线) {
|
||||
当前K线.高 = 当前K线.高.max(新数据.高);
|
||||
当前K线.低 = 当前K线.低.min(新数据.低);
|
||||
@@ -139,9 +142,14 @@ impl K线合成器 {
|
||||
当前K线.成交量 += 新数据.成交量;
|
||||
}
|
||||
|
||||
fn _完成K线(&mut self, 周期: i64) -> Option<K线> {
|
||||
/// 完成K线 — 对应 Python `_完成K线`
|
||||
/// 清空当前K线后,触发事件回调(此时获取当前K线返回 None)
|
||||
fn _完成K线(&mut self, 周期: i64) {
|
||||
let ent = self.当前K线.get_mut(&周期).unwrap();
|
||||
let mut k线 = ent.take()?;
|
||||
let mut k线 = match ent.take() {
|
||||
Some(k) => k,
|
||||
None => return,
|
||||
};
|
||||
k线.序号 = self
|
||||
.合成K线列表
|
||||
.get(&周期)
|
||||
@@ -151,11 +159,142 @@ impl K线合成器 {
|
||||
|
||||
let 完成K线 = k线.clone();
|
||||
self.合成K线列表.get_mut(&周期).unwrap().push(k线);
|
||||
Some(完成K线)
|
||||
|
||||
// 对应 Python _完成K线:清空当前K线后、新K线创建前触发回调
|
||||
self._产生完成K线信号(周期, 完成K线);
|
||||
}
|
||||
|
||||
/// 获取指定周期当前正在合成的K线
|
||||
/// 产生完成K线信号 — 对应 Python `_产生完成K线信号`
|
||||
/// 异常安全:若回调 panic,捕获并记录错误,不中断管线
|
||||
fn _产生完成K线信号(&self, 周期: i64, 完成K线: K线) {
|
||||
if let Some(ref cb) = self.事件回调 {
|
||||
let 标识 = self.标识.clone();
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
cb("K线完成".into(), 标识, 周期, 完成K线);
|
||||
}));
|
||||
if let Err(e) = result {
|
||||
let msg = e
|
||||
.downcast_ref::<&str>()
|
||||
.map(|s| s.to_string())
|
||||
.or_else(|| e.downcast_ref::<String>().cloned())
|
||||
.unwrap_or_else(|| "未知错误".into());
|
||||
warn!("K线合成器 事件回调 异常: {}", msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取指定周期当前正在合成的K线 — 对应 Python `获取当前K线`
|
||||
pub fn 获取当前K线(&self, 周期: i64) -> Option<&K线> {
|
||||
self.当前K线.get(&周期).and_then(|k| k.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_创建合成器_初始状态正确() {
|
||||
let synth = K线合成器::new("btcusd".into(), vec![60, 300], None);
|
||||
assert_eq!(synth.标识, "btcusd");
|
||||
assert_eq!(synth.周期组, vec![60, 300]);
|
||||
assert!(synth.事件回调.is_none());
|
||||
assert!(synth.当前K线[&60].is_none());
|
||||
assert!(synth.当前K线[&300].is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_设置事件回调() {
|
||||
let mut synth = K线合成器::new("btcusd".into(), vec![60], None);
|
||||
assert!(synth.事件回调.is_none());
|
||||
synth.设置事件回调(Box::new(|_, _, _, _| {}));
|
||||
assert!(synth.事件回调.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_对齐时间戳() {
|
||||
let synth = K线合成器::new("t".into(), vec![300], None);
|
||||
assert_eq!(synth._对齐时间戳(1218124800, 300), 1218124800);
|
||||
assert_eq!(synth._对齐时间戳(1218124801, 300), 1218124800);
|
||||
assert_eq!(synth._对齐时间戳(1218125099, 300), 1218124800);
|
||||
assert_eq!(synth._对齐时间戳(1218125100, 300), 1218125100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_创建新K线_序号递进() {
|
||||
let mut synth = K线合成器::new("btcusd".into(), vec![300], None);
|
||||
{
|
||||
let first = K线::创建普K("btcusd", 0, 100.0, 110.0, 90.0, 105.0, 1000.0, 0, 300);
|
||||
synth.合成K线列表.get_mut(&300).unwrap().push(first);
|
||||
}
|
||||
let new_bar = K线::创建普K("btcusd", 100, 200.0, 210.0, 190.0, 205.0, 500.0, 0, 60);
|
||||
let created = synth._创建新K线(300, 300, &new_bar);
|
||||
assert_eq!(created.序号, 1);
|
||||
assert_eq!(created.时间戳, 300);
|
||||
assert_eq!(created.开盘价, 200.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_更新K线_高低更新() {
|
||||
let mut current = K线::创建普K("t", 0, 100.0, 110.0, 90.0, 105.0, 100.0, 0, 300);
|
||||
let new_data = K线::创建普K("t", 0, 102.0, 115.0, 85.0, 108.0, 50.0, 0, 60);
|
||||
K线合成器::_更新K线(&mut current, &new_data);
|
||||
assert_eq!(current.高, 115.0);
|
||||
assert_eq!(current.低, 85.0);
|
||||
assert_eq!(current.收盘价, 108.0);
|
||||
assert_eq!(current.成交量, 150.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_完成K线_返回完成K并将当前置空() {
|
||||
let mut synth = K线合成器::new("btcusd".into(), vec![300], None);
|
||||
let bar = K线::创建普K("btcusd", 300, 100.0, 110.0, 90.0, 105.0, 1000.0, 0, 300);
|
||||
synth.当前K线.insert(300, Some(bar));
|
||||
synth._完成K线(300);
|
||||
assert!(synth.当前K线[&300].is_none());
|
||||
assert_eq!(synth.合成K线列表[&300].len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_完成K线_事件回调触发() {
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
let callback_fired = Arc::new(AtomicBool::new(false));
|
||||
let cb_flag = Arc::clone(&callback_fired);
|
||||
|
||||
let mut synth = K线合成器::new(
|
||||
"btcusd".into(),
|
||||
vec![300],
|
||||
Some(Box::new(move |信号类型, 标识, 周期, _完成K线| {
|
||||
assert_eq!(信号类型, "K线完成");
|
||||
assert_eq!(标识, "btcusd");
|
||||
assert_eq!(周期, 300);
|
||||
cb_flag.store(true, Ordering::SeqCst);
|
||||
})),
|
||||
);
|
||||
|
||||
let bar1 = K线::创建普K("btcusd", 0, 100.0, 110.0, 90.0, 105.0, 1000.0, 0, 300);
|
||||
synth.当前K线.insert(300, Some(bar1));
|
||||
let bar2 = K线::创建普K("btcusd", 400, 200.0, 210.0, 190.0, 205.0, 500.0, 0, 60);
|
||||
synth.投喂K线(bar2);
|
||||
assert!(callback_fired.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_投喂K线_多周期合成() {
|
||||
let mut synth = K线合成器::new("btcusd".into(), vec![60, 300], None);
|
||||
synth.投喂K线(K线::创建普K(
|
||||
"btcusd", 60, 100.0, 110.0, 90.0, 105.0, 100.0, 0, 60,
|
||||
));
|
||||
assert!(synth.获取当前K线(60).is_some());
|
||||
assert!(synth.获取当前K线(300).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_投喂_便捷方法() {
|
||||
let mut synth = K线合成器::new("btcusd".into(), vec![300], None);
|
||||
synth.投喂(1218124800, 100.0, 110.0, 90.0, 105.0, 1000.0);
|
||||
assert!(synth.获取当前K线(300).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
+334
-217
@@ -22,12 +22,9 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
use crate::warn;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use tracing::warn;
|
||||
|
||||
fn is_infinite_f64(v: &f64) -> bool {
|
||||
v.is_infinite()
|
||||
}
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// 缠论配置 —— 控制所有分析阶段的行为
|
||||
///
|
||||
@@ -95,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>>,
|
||||
|
||||
// ---- 买卖点 ----
|
||||
/// 买卖点偏移量
|
||||
@@ -211,29 +139,6 @@ pub struct 缠论配置 {
|
||||
pub 买卖点_指标匹配_KDJ: bool,
|
||||
/// 买卖点指标匹配 RSI
|
||||
pub 买卖点_指标匹配_RSI: bool,
|
||||
/// 买卖点背离率阈值(Infinity 表示不使用)
|
||||
#[serde(skip_serializing_if = "is_infinite_f64")]
|
||||
pub 买卖点_背离率: f64,
|
||||
/// 买卖点 T2 回调阈值
|
||||
pub 买卖点_T2_回调阈值: f64,
|
||||
/// 买卖点 T2S 最大层级
|
||||
pub 买卖点_T2S_最大层级: i64,
|
||||
/// 买卖点峰值条件
|
||||
pub 买卖点_峰值条件: bool,
|
||||
/// 买卖点计算方式(峰/谷等)
|
||||
pub 买卖点_计算方式: String,
|
||||
/// 是否计算线段BSP1
|
||||
pub 买卖点_计算线段BSP1: bool,
|
||||
/// 是否处理BSP2
|
||||
pub 买卖点_处理BSP2: bool,
|
||||
/// 是否计算线段BSP3
|
||||
pub 买卖点_计算线段BSP3: bool,
|
||||
/// 是否依赖T1买卖点
|
||||
pub 买卖点_依赖T1: bool,
|
||||
/// 买卖点中枢来源(实/虚/合)
|
||||
pub 买卖点_中枢来源: String,
|
||||
/// 买卖点调试输出
|
||||
pub 买卖点_调试输出: bool,
|
||||
|
||||
// ---- 背驰 ----
|
||||
/// 线段内部背驰使用 MACD
|
||||
@@ -336,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,
|
||||
@@ -382,17 +257,6 @@ impl Default for 缠论配置 {
|
||||
买卖点_指标匹配_MACD: true,
|
||||
买卖点_指标匹配_KDJ: true,
|
||||
买卖点_指标匹配_RSI: true,
|
||||
买卖点_背离率: f64::INFINITY,
|
||||
买卖点_T2_回调阈值: 1.0,
|
||||
买卖点_T2S_最大层级: 3,
|
||||
买卖点_峰值条件: false,
|
||||
买卖点_计算方式: "峰".into(),
|
||||
买卖点_计算线段BSP1: true,
|
||||
买卖点_处理BSP2: true,
|
||||
买卖点_计算线段BSP3: true,
|
||||
买卖点_依赖T1: true,
|
||||
买卖点_中枢来源: "合".into(),
|
||||
买卖点_调试输出: false,
|
||||
线段内部背驰_MACD: true,
|
||||
线段内部背驰_斜率: true,
|
||||
线段内部背驰_测度: true,
|
||||
@@ -403,46 +267,164 @@ 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;
|
||||
}
|
||||
if let Some(v) = MACD {
|
||||
self.MACD_参数列表 = v;
|
||||
}
|
||||
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![("rsi".into(), self.相对强弱指数_周期)]
|
||||
}
|
||||
|
||||
/// 解析KDJ参数列表 — 如果列表非空则使用列表,否则返回默认单组
|
||||
pub fn _解析KDJ参数列表(&self) -> Vec<(String, i64, i64, i64)> {
|
||||
if !self.KDJ_参数列表.is_empty() {
|
||||
return self.KDJ_参数列表.clone();
|
||||
/// 序列化为 JSON 字典(对应 Python to_dict,仅返回 model_fields 中的字段)
|
||||
pub fn to_dict(&self) -> serde_json::Value {
|
||||
let full = serde_json::to_value(self).unwrap_or_default();
|
||||
let valid = Self::model_fields();
|
||||
if let serde_json::Value::Object(map) = full {
|
||||
let filtered: serde_json::Map<_, _> = map
|
||||
.into_iter()
|
||||
.filter(|(k, _)| valid.contains(&k.as_str()))
|
||||
.collect();
|
||||
serde_json::Value::Object(filtered)
|
||||
} else {
|
||||
full
|
||||
}
|
||||
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();
|
||||
/// 从 JSON 字典反序列化(对应 Python from_dict / 兼容旧版本配置)
|
||||
pub fn from_dict(value: &serde_json::Value) -> Result<Self, serde_json::Error> {
|
||||
if let serde_json::Value::Object(map) = value {
|
||||
let valid_fields = Self::model_fields();
|
||||
let cleaned: serde_json::Map<_, _> = map
|
||||
.iter()
|
||||
.filter(|(k, _)| valid_fields.contains(&k.as_str()))
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect();
|
||||
serde_json::from_value(serde_json::Value::Object(cleaned))
|
||||
} else {
|
||||
serde_json::from_value(value.clone())
|
||||
}
|
||||
vec![("boll".into(), self.布林带_周期, self.布林带_标准差倍数)]
|
||||
}
|
||||
|
||||
/// 验证并修正字段值(对应 Python _validate_all_fields)
|
||||
pub fn _validate_all_fields(&mut self) {
|
||||
const 允许: &[&str] = &[
|
||||
"开",
|
||||
"高",
|
||||
"低",
|
||||
"收",
|
||||
"高低均值",
|
||||
"高低收均值",
|
||||
"开高低收均值",
|
||||
];
|
||||
if !允许.contains(&self.指标计算方式.as_str()) {
|
||||
warn!(
|
||||
"[指标计算方式] = {} 值不在允许范围内,使用默认值:收",
|
||||
self.指标计算方式
|
||||
);
|
||||
self.指标计算方式 = "收".into();
|
||||
}
|
||||
}
|
||||
|
||||
/// 返回字段名列表(对应 Python model_fields().keys())
|
||||
pub fn model_fields() -> &'static [&'static str] {
|
||||
&[
|
||||
// ---- 基础 ----
|
||||
"标识",
|
||||
// ---- 缠K ----
|
||||
"缠K合并替换",
|
||||
// ---- 笔 ----
|
||||
"笔内元素数量",
|
||||
"笔内相同终点取舍",
|
||||
"笔内起始分型包含整笔",
|
||||
"笔内起始分型包含整笔_包括右",
|
||||
"笔内原始K线包含整笔",
|
||||
"笔次级成笔",
|
||||
"笔弱化",
|
||||
"笔弱化_原始数量",
|
||||
// ---- 线段 ----
|
||||
"线段_非缺口下穿刺",
|
||||
"线段_特征序列忽视老阴老阳",
|
||||
"线段_缺口后紧急修正",
|
||||
"线段_修正",
|
||||
"线段内部中枢图显",
|
||||
"扩展线段_当下分析",
|
||||
// ---- 分析开关 ----
|
||||
"分析笔",
|
||||
"分析线段",
|
||||
"分析扩展线段",
|
||||
"分析笔中枢",
|
||||
"分析线段中枢",
|
||||
// ---- 终止 ----
|
||||
"手动终止",
|
||||
// ---- 指标 ----
|
||||
"计算指标",
|
||||
"指标计算方式",
|
||||
"MACD_参数列表",
|
||||
"RSI_周期列表",
|
||||
"KDJ_参数列表",
|
||||
"BOLL_参数列表",
|
||||
"均线参数列表",
|
||||
// ---- 推送/显示 ----
|
||||
"图表展示",
|
||||
"图表展示标签",
|
||||
// ---- 买卖点 ----
|
||||
"买卖点偏移",
|
||||
"买卖点激进识别",
|
||||
"买卖点与MACD柱强相关",
|
||||
"买卖点错过误差值",
|
||||
"买卖点_指标模式",
|
||||
"买卖点_指标匹配_MACD",
|
||||
"买卖点_指标匹配_KDJ",
|
||||
"买卖点_指标匹配_RSI",
|
||||
// ---- 背驰 ----
|
||||
"线段内部背驰_MACD",
|
||||
"线段内部背驰_斜率",
|
||||
"线段内部背驰_测度",
|
||||
"线段内部背驰_模式",
|
||||
// ---- 文件 ----
|
||||
"加载文件路径",
|
||||
]
|
||||
}
|
||||
|
||||
/// 深拷贝并更新指定字段(对应 Python model_copy(update={...}, deep=True))
|
||||
pub fn model_copy(&self, update: &HashMap<String, serde_json::Value>) -> Self {
|
||||
let mut value = serde_json::to_value(self).unwrap_or_default();
|
||||
if let serde_json::Value::Object(ref mut map) = value {
|
||||
for (k, v) in update {
|
||||
map.insert(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
serde_json::from_value(value).unwrap_or_else(|_| self.clone())
|
||||
}
|
||||
|
||||
/// 序列化为 JSON 字符串
|
||||
@@ -467,25 +449,12 @@ impl 缠论配置 {
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// 返回一个关闭所有推送/显示的新配置
|
||||
/// 返回一个关闭所有推送/显示的新配置(对应 Python 不推送)
|
||||
pub fn 不推送(&self) -> Self {
|
||||
Self {
|
||||
线段内部中枢图显: false,
|
||||
图表展示: false,
|
||||
推送K线: false,
|
||||
推送笔: false,
|
||||
推送线段: false,
|
||||
推送中枢: false,
|
||||
图表展示_笔: false,
|
||||
图表展示_线段: false,
|
||||
图表展示_扩展线段: false,
|
||||
图表展示_扩展线段_线段: false,
|
||||
图表展示_线段_线段: false,
|
||||
图表展示_中枢_笔: false,
|
||||
图表展示_中枢_线段: false,
|
||||
图表展示_中枢_扩展线段: false,
|
||||
图表展示_中枢_扩展线段_线段: false,
|
||||
图表展示_中枢_线段_线段: false,
|
||||
图表展示_中枢_线段内部: false,
|
||||
图表展示标签: Some(vec![]),
|
||||
..self.clone()
|
||||
}
|
||||
}
|
||||
@@ -524,19 +493,21 @@ impl 缠论配置 {
|
||||
result
|
||||
}
|
||||
|
||||
/// 对比两个配置,返回差异字段
|
||||
pub fn 对比(&self, other: &Self) -> Vec<String> {
|
||||
let mut diffs = Vec::new();
|
||||
let self_json = serde_json::to_value(self).unwrap();
|
||||
let other_json = serde_json::to_value(other).unwrap();
|
||||
/// 对比两个配置,返回差异字段及新值(对应 Python 对比 → dict[字段名, 新值])
|
||||
pub fn 对比(&self, other: &Self) -> HashMap<String, serde_json::Value> {
|
||||
let mut diffs = HashMap::new();
|
||||
let self_dict = self.to_dict();
|
||||
let other_dict = other.to_dict();
|
||||
if let (serde_json::Value::Object(self_map), serde_json::Value::Object(other_map)) =
|
||||
(&self_json, &other_json)
|
||||
(&self_dict, &other_dict)
|
||||
{
|
||||
for (key, self_val) in self_map {
|
||||
if let Some(other_val) = other_map.get(key)
|
||||
&& self_val != other_val
|
||||
for key in Self::model_fields() {
|
||||
let self_val = self_map.get(*key);
|
||||
let other_val = other_map.get(*key);
|
||||
if self_val != other_val
|
||||
&& let Some(v) = other_val
|
||||
{
|
||||
diffs.push(key.clone());
|
||||
diffs.insert(key.to_string(), v.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -562,7 +533,6 @@ mod tests {
|
||||
let config = 缠论配置::default();
|
||||
assert_eq!(config.标识, "bar");
|
||||
assert_eq!(config.笔内元素数量, 5);
|
||||
assert!(config.买卖点_背离率.is_infinite());
|
||||
assert_eq!(config.指标计算方式, "收");
|
||||
}
|
||||
|
||||
@@ -609,14 +579,161 @@ mod tests {
|
||||
assert_eq!(config.线段内部背驰_模式, "全量");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_dict_roundtrip() {
|
||||
let config = 缠论配置::default();
|
||||
let dict = config.to_dict();
|
||||
let restored = 缠论配置::from_dict(&dict).unwrap();
|
||||
assert_eq!(config.to_json(), restored.to_json());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_dict_filters_unknown_fields() {
|
||||
// 兼容旧版本配置 — unknown fields are silently dropped
|
||||
let json = serde_json::json!({
|
||||
"标识": "test",
|
||||
"不存在的字段": 42,
|
||||
"另一个废弃字段": "xxx",
|
||||
"笔内元素数量": 8,
|
||||
});
|
||||
let config = 缠论配置::from_dict(&json).unwrap();
|
||||
assert_eq!(config.标识, "test");
|
||||
assert_eq!(config.笔内元素数量, 8);
|
||||
// 未指定字段使用默认值
|
||||
assert_eq!(config.买卖点偏移, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_model_fields_contains_all() {
|
||||
let fields = 缠论配置::model_fields();
|
||||
assert!(fields.contains(&"标识"));
|
||||
assert!(fields.contains(&"笔内元素数量"));
|
||||
assert!(fields.contains(&"买卖点偏移"));
|
||||
assert!(fields.contains(&"线段内部背驰_MACD"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_model_copy() {
|
||||
let mut update = std::collections::HashMap::new();
|
||||
update.insert("标识".into(), serde_json::json!("custom"));
|
||||
update.insert("笔内元素数量".into(), serde_json::json!(10));
|
||||
|
||||
let config = 缠论配置::default();
|
||||
let copied = config.model_copy(&update);
|
||||
|
||||
assert_eq!(copied.标识, "custom");
|
||||
assert_eq!(copied.笔内元素数量, 10);
|
||||
// 未指定字段保持不变
|
||||
assert_eq!(copied.买卖点偏移, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_dict_to_json_consistency() {
|
||||
let config = 缠论配置::default();
|
||||
let dict = config.to_dict();
|
||||
// to_dict → from_dict → to_json should equal original to_json
|
||||
let restored = 缠论配置::from_dict(&dict).unwrap();
|
||||
assert_eq!(config.to_json(), restored.to_json());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_不推送() {
|
||||
let config = 缠论配置::default();
|
||||
let muted = config.不推送();
|
||||
assert!(!muted.推送K线);
|
||||
assert!(!muted.推送笔);
|
||||
assert!(!muted.图表展示);
|
||||
// 其他字段不变
|
||||
assert!(!muted.线段内部中枢图显);
|
||||
assert_eq!(muted.笔内元素数量, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_对比_无差异() {
|
||||
let a = 缠论配置::default();
|
||||
let b = 缠论配置::default();
|
||||
let diff = a.对比(&b);
|
||||
assert!(diff.is_empty(), "identical configs should have empty diff");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_对比_有差异() {
|
||||
let a = 缠论配置::default();
|
||||
let mut b = 缠论配置::default();
|
||||
b.标识 = "changed".into();
|
||||
b.笔内元素数量 = 99;
|
||||
|
||||
let diff = a.对比(&b);
|
||||
assert_eq!(diff.len(), 2);
|
||||
assert_eq!(diff.get("标识").unwrap().as_str().unwrap(), "changed");
|
||||
assert_eq!(diff.get("笔内元素数量").unwrap().as_i64().unwrap(), 99);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_对比_仅比较model_fields() {
|
||||
// 仅比较 model_fields 中的字段(Python 一致行为)
|
||||
let a = 缠论配置::default();
|
||||
let b = 缠论配置::default();
|
||||
let diff = a.对比(&b);
|
||||
// 验证不包含废弃字段(如已删除的 "买卖点_背离率" 等)
|
||||
assert!(!diff.contains_key("买卖点_背离率"));
|
||||
assert!(diff.is_empty(), "default configs should have no diff");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_dict_excludes_non_model_fields() {
|
||||
let config = 缠论配置::default();
|
||||
let dict = config.to_dict();
|
||||
let valid = 缠论配置::model_fields();
|
||||
if let serde_json::Value::Object(map) = &dict {
|
||||
for key in map.keys() {
|
||||
assert!(
|
||||
valid.contains(&key.as_str()),
|
||||
"{key} should not be in to_dict output"
|
||||
);
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
valid.len(),
|
||||
dict.as_object().map(|m| m.len()).unwrap_or(0),
|
||||
"to_dict should have exactly model_fields count"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_model_copy_then_对比() {
|
||||
let config = 缠论配置::default();
|
||||
let mut update = HashMap::new();
|
||||
update.insert("标识".into(), serde_json::json!("copied"));
|
||||
update.insert("笔内元素数量".into(), serde_json::json!(10));
|
||||
|
||||
let copied = config.model_copy(&update);
|
||||
let diff = config.对比(&copied);
|
||||
|
||||
assert_eq!(diff.len(), 2);
|
||||
assert_eq!(diff["标识"].as_str().unwrap(), "copied");
|
||||
assert_eq!(diff["笔内元素数量"].as_i64().unwrap(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_对比_boolean_difference() {
|
||||
let a = 缠论配置::default();
|
||||
let mut b = 缠论配置::default();
|
||||
b.分析笔 = false;
|
||||
b.图表展示 = false;
|
||||
|
||||
let diff = a.对比(&b);
|
||||
assert_eq!(diff.len(), 2);
|
||||
assert_eq!(diff["分析笔"], serde_json::json!(false));
|
||||
assert_eq!(diff["图表展示"], serde_json::json!(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_dict_from_dict_对比_roundtrip() {
|
||||
let config = 缠论配置::default();
|
||||
let dict = config.to_dict();
|
||||
let restored = 缠论配置::from_dict(&dict).unwrap();
|
||||
let diff = config.对比(&restored);
|
||||
assert!(
|
||||
diff.is_empty(),
|
||||
"to_dict→from_dict roundtrip should produce no diff"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() < 周期 {
|
||||
|
||||
@@ -32,27 +32,56 @@ use std::sync::Arc;
|
||||
pub struct 指标计算器;
|
||||
|
||||
impl 指标计算器 {
|
||||
/// 增量计算所有开启的指标,将结果写入 当前K线.指标
|
||||
/// 增量计算所有开启的指标,将结果写入每一根 K 线。
|
||||
///
|
||||
/// `现有序列` 不包含当前K线;prev 取自 现有序列.last()
|
||||
/// 通过 RwLock 内部可变性,以 `&K线` 共享引用写入指标值
|
||||
pub fn 计算并挂载(当前K线: &K线, 现有序列: &[Arc<K线>], 配置: &缠论配置) {
|
||||
let prev_guard = 现有序列.last().map(|k| k.指标.read().unwrap());
|
||||
let prev = prev_guard.as_deref();
|
||||
if 配置.计算指标 {
|
||||
Self::_计算MACD组(当前K线, prev, 配置);
|
||||
Self::_计算RSI组(当前K线, prev, 配置);
|
||||
Self::_计算KDJ组(当前K线, prev, 配置);
|
||||
Self::_计算BOLL组(当前K线, prev, 配置);
|
||||
pub fn 计算并挂载(全序列: &[Arc<K线>], 配置: &缠论配置) {
|
||||
let n = 全序列.len();
|
||||
if n == 0 {
|
||||
return;
|
||||
}
|
||||
if !配置.计算指标 && 配置.均线参数列表.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 找到第一个 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
|
||||
}
|
||||
}
|
||||
|
||||
// 回填:若有新增指标参数但首K线未被本轮计算覆盖,仍需填充历史K线
|
||||
if n > 1 && start > 0 {
|
||||
Self::_回填新指标(全序列, 配置);
|
||||
}
|
||||
Self::_更新均线(当前K线, 现有序列, 配置);
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -78,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,
|
||||
@@ -119,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,
|
||||
@@ -153,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,
|
||||
@@ -197,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线.高,
|
||||
@@ -220,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].高,
|
||||
@@ -267,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) => {
|
||||
@@ -298,4 +308,391 @@ impl 指标计算器 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 运行中新增指标参数时,回填所有历史K线
|
||||
fn _回填新指标(全序列: &[Arc<K线>], 配置: &缠论配置) {
|
||||
let (新MACD, 新RSI, 新KDJ, 新BOLL) = {
|
||||
let 首K_guard = 全序列[0].指标.read();
|
||||
let 尾K_guard = 全序列[全序列.len() - 1].指标.read();
|
||||
let 新MACD: Vec<_> = 配置
|
||||
.MACD_参数列表
|
||||
.iter()
|
||||
.filter(|(key, ..)| 尾K_guard.包含(key) && !首K_guard.包含(key))
|
||||
.cloned()
|
||||
.collect();
|
||||
let 新RSI: Vec<_> = 配置
|
||||
.RSI_周期列表
|
||||
.iter()
|
||||
.filter(|(key, ..)| 尾K_guard.包含(key) && !首K_guard.包含(key))
|
||||
.cloned()
|
||||
.collect();
|
||||
let 新KDJ: Vec<_> = 配置
|
||||
.KDJ_参数列表
|
||||
.iter()
|
||||
.filter(|(key, ..)| 尾K_guard.包含(key) && !首K_guard.包含(key))
|
||||
.cloned()
|
||||
.collect();
|
||||
let 新BOLL: Vec<_> = 配置
|
||||
.BOLL_参数列表
|
||||
.iter()
|
||||
.filter(|(key, ..)| 尾K_guard.包含(key) && !首K_guard.包含(key))
|
||||
.cloned()
|
||||
.collect();
|
||||
(新MACD, 新RSI, 新KDJ, 新BOLL)
|
||||
};
|
||||
|
||||
if 新MACD.is_empty() && 新RSI.is_empty() && 新KDJ.is_empty() && 新BOLL.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
for i in 0..全序列.len() {
|
||||
let k线 = &全序列[i];
|
||||
let prev_guard = if i > 0 {
|
||||
Some(全序列[i - 1].指标.read())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
for (key, 计算方式, 快, 慢, 信号) in &新MACD {
|
||||
let val = match prev_guard.as_ref().and_then(|p| p.获取(key)) {
|
||||
Some(指标值::MACD(prev_macd)) => 指标值::MACD(
|
||||
平滑异同移动平均线::增量计算_K线(prev_macd, k线, 计算方式),
|
||||
),
|
||||
_ => 指标值::MACD(平滑异同移动平均线::首次计算_K线(
|
||||
k线,
|
||||
计算方式,
|
||||
*快,
|
||||
*慢,
|
||||
*信号,
|
||||
)),
|
||||
};
|
||||
k线.指标.write().设置(key, val);
|
||||
}
|
||||
|
||||
for (key, 计算方式, 周期, ma周期, 超买, 超卖) in &新RSI {
|
||||
let val = match prev_guard.as_ref().and_then(|p| p.获取(key)) {
|
||||
Some(指标值::RSI(prev_rsi)) => 指标值::RSI(
|
||||
相对强弱指数::增量计算_K线(prev_rsi, k线, 计算方式),
|
||||
),
|
||||
_ => 指标值::RSI(相对强弱指数::首次计算_K线(
|
||||
k线,
|
||||
计算方式,
|
||||
*周期,
|
||||
*超买,
|
||||
*超卖,
|
||||
Some(*ma周期),
|
||||
)),
|
||||
};
|
||||
k线.指标.write().设置(key, val);
|
||||
}
|
||||
|
||||
for (key, _fm, rsv, k平滑, d平滑, 超买, 超卖) in &新KDJ {
|
||||
let val = match prev_guard.as_ref().and_then(|p| p.获取(key)) {
|
||||
Some(指标值::KDJ(prev_kdj)) => {
|
||||
指标值::KDJ(随机指标::增量计算_K线(prev_kdj, k线))
|
||||
}
|
||||
_ => 指标值::KDJ(随机指标::首次计算_K线(
|
||||
k线, *rsv, *k平滑, *d平滑, *超买, *超卖,
|
||||
)),
|
||||
};
|
||||
k线.指标.write().设置(key, val);
|
||||
}
|
||||
|
||||
for (key, 计算方式, 周期, 标准差倍数) in &新BOLL {
|
||||
let val = match prev_guard.as_ref().and_then(|p| p.获取(key)) {
|
||||
Some(指标值::BOLL(prev_boll)) => {
|
||||
指标值::BOLL(布林带::增量计算_K线(prev_boll, k线, 计算方式))
|
||||
}
|
||||
_ => 指标值::BOLL(布林带::首次计算_K线(
|
||||
k线,
|
||||
计算方式,
|
||||
*周期 as usize,
|
||||
*标准差倍数,
|
||||
)),
|
||||
};
|
||||
k线.指标.write().设置(key, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::缠论配置;
|
||||
use crate::kline::bar::K线;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// 辅助:创建一根模拟 K 线
|
||||
fn 模拟K线(时间戳: i64, 开: f64, 高: f64, 低: f64, 收: f64, 量: f64) -> Arc<K线> {
|
||||
Arc::new(K线::创建普K("TEST", 时间戳, 开, 高, 低, 收, 量, 0, 300))
|
||||
}
|
||||
|
||||
/// 辅助:生成连续上涨的 K 线序列(每根涨 ~1%)
|
||||
fn 生成上涨序列(n: usize, 起始时间: i64, 起始价: f64) -> Vec<Arc<K线>> {
|
||||
let mut seq = Vec::with_capacity(n);
|
||||
let mut price = 起始价;
|
||||
for i in 0..n {
|
||||
let 开 = price;
|
||||
let 收 = price * 1.005; // 上涨 0.5%
|
||||
let 高 = 收 * 1.002;
|
||||
let 低 = 开 * 0.998;
|
||||
let 量 = 1000.0 + i as f64 * 10.0;
|
||||
seq.push(模拟K线(起始时间 + i as i64 * 300, 开, 高, 低, 收, 量));
|
||||
price = 收;
|
||||
}
|
||||
seq
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_单根K线_首次计算_挂载成功() {
|
||||
let k线 = 模拟K线(1000, 100.0, 102.0, 98.0, 101.0, 500.0);
|
||||
let seq = vec![k线.clone()];
|
||||
let 配置 = 缠论配置::default();
|
||||
|
||||
指标计算器::计算并挂载(&seq, &配置);
|
||||
|
||||
// 单根 K 线首次计算:MACD DIF=0(EMA=SMA 初始近似),柱=0
|
||||
let m = k线.macd().expect("MACD 应已挂载");
|
||||
assert_eq!(m.DIF, Some(0.0), "首根K线 DIF 应为 0");
|
||||
assert_eq!(m.MACD柱, 0.0, "首根K线 MACD柱 应为 0");
|
||||
|
||||
// RSI 首次计算后 RSI 为 None(需至少一个增量步才有值)
|
||||
// 但指标容器应已注册 RSI 槽位,boll_cloned() 返回的是字段默认值
|
||||
assert!(k线.rsi().is_some(), "RSI 结构体应已创建(即使 RSI 字段为 None)");
|
||||
assert!(k线.kdj().is_some(), "KDJ 结构体应已创建(即使 K/D 字段为 None)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_多根K线_增量计算_指标值递推() {
|
||||
let seq = 生成上涨序列(5, 1000, 100.0);
|
||||
let 配置 = 缠论配置::default();
|
||||
|
||||
// 逐根计算(模拟流式投喂)
|
||||
for i in 0..seq.len() {
|
||||
指标计算器::计算并挂载(&seq[..=i], &配置);
|
||||
}
|
||||
|
||||
// 第 5 根 K 线的 MACD DIF 应 > 0(持续上涨)
|
||||
let last = &seq[seq.len() - 1];
|
||||
let m = last.macd().expect("最后一根K线 MACD 应已挂载");
|
||||
assert!(m.DIF.unwrap() > 0.0, "上涨序列 DIF 应为正");
|
||||
|
||||
// 所有 K 线均应有 MACD/RSI/KDJ
|
||||
for (i, k) in seq.iter().enumerate() {
|
||||
assert!(k.macd().is_some(), "K线[{i}] MACD 缺失");
|
||||
assert!(k.rsi().is_some(), "K线[{i}] RSI 缺失");
|
||||
assert!(k.kdj().is_some(), "K线[{i}] KDJ 缺失");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_指标未计算时_返回None() {
|
||||
let k线 = 模拟K线(1000, 100.0, 102.0, 98.0, 101.0, 500.0);
|
||||
// 未调用 计算并挂载 — 指标应为 None
|
||||
assert!(k线.macd().is_none(), "未计算时 MACD 应为 None");
|
||||
assert!(k线.rsi().is_none(), "未计算时 RSI 应为 None");
|
||||
assert!(k线.kdj().is_none(), "未计算时 KDJ 应为 None");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_回填新指标_新增参数后历史K线也挂载() {
|
||||
let seq = 生成上涨序列(3, 1000, 100.0);
|
||||
let 配置 = 缠论配置::default();
|
||||
|
||||
// 第一轮:只计算默认 macd 组
|
||||
指标计算器::计算并挂载(&seq[..=2], &配置);
|
||||
assert!(seq[2].macd().is_some());
|
||||
|
||||
// 第二轮:新增一组 MACD 参数,模拟用户后期追加指标
|
||||
let mut 配置2 = 配置.clone();
|
||||
配置2.MACD_参数列表.push(("extra_macd".into(), "收".into(), 5, 10, 3));
|
||||
指标计算器::计算并挂载(&seq[..=2], &配置2);
|
||||
|
||||
// 最后一根K线应同时有默认和 extra MACD
|
||||
let last = &seq[2];
|
||||
let guard = last.指标.read();
|
||||
assert!(guard.包含("macd"), "应有默认 macd");
|
||||
assert!(guard.包含("extra_macd"), "应有新指标 extra_macd");
|
||||
|
||||
// 回填:第一根 K 线也应被回填 extra_macd
|
||||
assert!(seq[0].指标.read().包含("extra_macd"), "回填后首根K线应有 extra_macd");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_多指标组_RSI_KDJ_BOLL_同时挂载() {
|
||||
let seq = 生成上涨序列(2, 1000, 100.0);
|
||||
let 配置 = 缠论配置::default();
|
||||
|
||||
指标计算器::计算并挂载(&seq[..=1], &配置);
|
||||
|
||||
let last = &seq[1];
|
||||
assert!(last.macd().is_some(), "MACD 应已挂载");
|
||||
assert!(last.rsi().is_some(), "RSI 应已挂载");
|
||||
assert!(last.kdj().is_some(), "KDJ 应已挂载");
|
||||
assert!(last.boll().is_some(), "BOLL 应已挂载");
|
||||
|
||||
// 验证 RSI 值的范围
|
||||
let r = last.rsi().unwrap();
|
||||
if let Some(rsi_val) = r.RSI {
|
||||
assert!((0.0..=100.0).contains(&rsi_val), "RSI 应在 0~100 之间, 实际={rsi_val}");
|
||||
}
|
||||
|
||||
// 验证 KDJ 值范围
|
||||
let k = last.kdj().unwrap();
|
||||
if let Some(k_val) = k.K {
|
||||
assert!((0.0..=100.0).contains(&k_val), "KDJ.K 应在 0~100 之间, 实际={k_val}");
|
||||
}
|
||||
|
||||
// BOLL 上轨 >= 中轨 >= 下轨
|
||||
let b = last.boll().unwrap();
|
||||
assert!(b.上轨 >= b.中轨, "BOLL 上轨({})应 >= 中轨({})", b.上轨, b.中轨);
|
||||
assert!(b.中轨 >= b.下轨, "BOLL 中轨({})应 >= 下轨({})", b.中轨, b.下轨);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_均线挂载() {
|
||||
let seq = 生成上涨序列(5, 1000, 100.0);
|
||||
let mut 配置 = 缠论配置::default();
|
||||
配置.均线参数列表 = vec![
|
||||
("SMA_3".into(), "收".into(), "SMA".into(), 3),
|
||||
];
|
||||
|
||||
指标计算器::计算并挂载(&seq[..=4], &配置);
|
||||
|
||||
let last = &seq[4];
|
||||
let ma_val = last.ma("SMA_3").expect("SMA_3 应已挂载");
|
||||
assert!(ma_val > 0.0, "SMA_3 应为正值");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_观察者集成_确保指标已计算() {
|
||||
use crate::business::observer::观察者;
|
||||
|
||||
let 观察员 = 观察者::new("TEST".into(), 300, 缠论配置::default());
|
||||
|
||||
// 逐根投喂
|
||||
for i in 0..5 {
|
||||
let price = 100.0 * (1.0 + i as f64 * 0.01);
|
||||
观察员.write().投喂原始数据(
|
||||
1000 + i as i64 * 300, price, price * 1.02, price * 0.98, price * 1.01, 1000.0,
|
||||
);
|
||||
}
|
||||
|
||||
// 确保指标已计算
|
||||
观察员.read().确保指标已计算();
|
||||
|
||||
let obs = 观察员.read();
|
||||
let klines = &obs.普通K线序列;
|
||||
assert!(!klines.is_empty(), "应有K线");
|
||||
|
||||
// 最后一根K线应有指标
|
||||
let last = &klines[klines.len() - 1];
|
||||
assert!(last.macd().is_some(), "观察者集成: MACD 应已挂载");
|
||||
assert!(last.rsi().is_some(), "观察者集成: RSI 应已挂载");
|
||||
assert!(last.kdj().is_some(), "观察者集成: KDJ 应已挂载");
|
||||
}
|
||||
|
||||
/// 50 根 K 线后,各指标应有稳定、合理的数值(非初始默认值)。
|
||||
#[test]
|
||||
fn test_50根K线_指标值稳定合理() {
|
||||
// 模拟 50 根有涨有跌的 K 线
|
||||
let mut seq = Vec::with_capacity(50);
|
||||
let mut price = 100.0;
|
||||
let mut rng: u64 = 42;
|
||||
for i in 0..50 {
|
||||
// 简单 LCG 随机 ±2% 波动
|
||||
rng = rng.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
|
||||
let change = ((rng as f64 / u64::MAX as f64) - 0.5) * 0.04; // -2% ~ +2%
|
||||
let 收 = price * (1.0 + change);
|
||||
let 开 = price;
|
||||
let 高 = 开.max(收) * (1.0 + (rng % 100) as f64 / 10000.0);
|
||||
let 低 = 开.min(收) * (1.0 - (rng % 100) as f64 / 10000.0);
|
||||
let 量 = 500.0 + (rng % 500) as f64;
|
||||
seq.push(模拟K线(1000 + i as i64 * 300, 开, 高, 低, 收, 量));
|
||||
price = 收;
|
||||
}
|
||||
|
||||
let 配置 = 缠论配置::default();
|
||||
|
||||
// 逐根增量计算(模拟流式管线)
|
||||
for i in 0..seq.len() {
|
||||
指标计算器::计算并挂载(&seq[..=i], &配置);
|
||||
}
|
||||
|
||||
// ── 验证每根 K 线都有指标 ──
|
||||
for (i, k) in seq.iter().enumerate() {
|
||||
assert!(k.macd().is_some(), "K线[{i}] MACD 缺失");
|
||||
assert!(k.rsi().is_some(), "K线[{i}] RSI 缺失");
|
||||
assert!(k.kdj().is_some(), "K线[{i}] KDJ 缺失");
|
||||
assert!(k.boll().is_some(), "K线[{i}] BOLL 缺失");
|
||||
}
|
||||
|
||||
// ── 第 50 根 K 线(最后一根)的详细校验 ──
|
||||
let last = &seq[49];
|
||||
|
||||
// MACD
|
||||
let m = last.macd().unwrap();
|
||||
assert!(m.DIF.is_some(), "50根后 DIF 应有值");
|
||||
assert!(m.DEA.is_some(), "50根后 DEA 应有值");
|
||||
let dif = m.DIF.unwrap();
|
||||
let dea = m.DEA.unwrap();
|
||||
// DIF 和 DEA 不应同时为 0(50 根有波动数据 EMA 应已收敛)
|
||||
assert!(
|
||||
dif.abs() > 1e-9 || dea.abs() > 1e-9,
|
||||
"50根有波动数据 DIF/DEA 应非零, DIF={dif}, DEA={dea}"
|
||||
);
|
||||
// MACD 柱 = 2*(DIF-DEA),数量级合理
|
||||
let bar = m.MACD柱;
|
||||
assert!(bar.is_finite(), "MACD柱 应为有限值");
|
||||
assert!(bar.abs() < 100.0, "MACD柱 不应过大, 实际={bar}");
|
||||
|
||||
// RSI
|
||||
let r = last.rsi().unwrap();
|
||||
let rsi_val = r.RSI.expect("50根后 RSI 应有值");
|
||||
assert!((0.0..=100.0).contains(&rsi_val), "RSI 应在 0~100, 实际={rsi_val}");
|
||||
// 50 根随机数据 RSI 不应卡在极端值
|
||||
assert!(rsi_val > 0.1 && rsi_val < 99.9, "RSI 不应在极端值, 实际={rsi_val}");
|
||||
|
||||
// KDJ
|
||||
let kdj = last.kdj().unwrap();
|
||||
let k_val = kdj.K.expect("50根后 KDJ.K 应有值");
|
||||
let d_val = kdj.D.expect("50根后 KDJ.D 应有值");
|
||||
let j_val = kdj.J.expect("50根后 KDJ.J 应有值");
|
||||
assert!((0.0..=100.0).contains(&k_val), "KDJ.K 应在 0~100, 实际={k_val}");
|
||||
assert!((0.0..=100.0).contains(&d_val), "KDJ.D 应在 0~100, 实际={d_val}");
|
||||
// J = 3K - 2D,可能略超 [0,100]
|
||||
assert!(j_val.is_finite(), "KDJ.J 应为有限值");
|
||||
|
||||
// BOLL
|
||||
let b = last.boll().unwrap();
|
||||
assert!(b.上轨 > b.中轨 || b.中轨 > b.下轨,
|
||||
"50根波动数据 BOLL 带宽应 > 0, 上={:.4} 中={:.4} 下={:.4}",
|
||||
b.上轨, b.中轨, b.下轨);
|
||||
|
||||
// ── 中间节点验证:第 25 根 K 线所有指标也应有值 ──
|
||||
let mid = &seq[24];
|
||||
let m25 = mid.macd().unwrap();
|
||||
assert!(m25.DIF.is_some(), "第25根 DIF 应有值");
|
||||
assert!(mid.rsi().unwrap().RSI.is_some(), "第25根 RSI 应有值");
|
||||
assert!(mid.kdj().unwrap().K.is_some(), "第25根 KDJ.K 应有值");
|
||||
|
||||
// ── 印出第 50 根用于人工审查 ──
|
||||
println!(
|
||||
"=== 第 50 根 K线 指标状态 ===",
|
||||
);
|
||||
println!(
|
||||
" MACD: DIF={dif:.6} DEA={dea:.6} BAR={bar:.6}",
|
||||
);
|
||||
println!(
|
||||
" RSI: RSI={rsi_val:.4}",
|
||||
);
|
||||
println!(
|
||||
" KDJ: K={k_val:.4} D={d_val:.4} J={j_val:.4}",
|
||||
);
|
||||
println!(
|
||||
" BOLL: 上={:.4} 中={:.4} 下={:.4}",
|
||||
b.上轨, b.中轨, b.下轨,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(默认值);
|
||||
}
|
||||
|
||||
/// 按名称获取指标值
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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和,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+261
-5
@@ -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;
|
||||
@@ -270,6 +274,117 @@ impl K线 {
|
||||
Some(&序列[始_idx..=终_idx])
|
||||
}
|
||||
|
||||
/// 结构化相等校验 — 比对各字段,浮点字段使用容差比较,返回 (是否相等, 差异描述)
|
||||
pub fn 相等(&self, other: &Self, 浮点容差: f64) -> (bool, String) {
|
||||
if self.标识 != other.标识 {
|
||||
return (
|
||||
false,
|
||||
format!("K线: [标识] 不等 A={},B={}", self.标识, other.标识),
|
||||
);
|
||||
}
|
||||
if self.序号 != other.序号 {
|
||||
return (
|
||||
false,
|
||||
format!("K线: [序号] 不等 A={},B={}", self.序号, other.序号),
|
||||
);
|
||||
}
|
||||
if self.周期 != other.周期 {
|
||||
return (
|
||||
false,
|
||||
format!("K线: [周期] 不等 A={},B={}", self.周期, other.周期),
|
||||
);
|
||||
}
|
||||
if self.时间戳 != other.时间戳 {
|
||||
return (
|
||||
false,
|
||||
format!("K线: [时间戳] 不等 A={},B={}", self.时间戳, other.时间戳),
|
||||
);
|
||||
}
|
||||
let 浮点字段 = [
|
||||
("高", self.高, other.高),
|
||||
("低", self.低, other.低),
|
||||
("开盘价", self.开盘价, other.开盘价),
|
||||
("收盘价", self.收盘价, other.收盘价),
|
||||
("成交量", self.成交量, other.成交量),
|
||||
];
|
||||
for (名, a, b) in &浮点字段 {
|
||||
if (a - b).abs() > 浮点容差 {
|
||||
return (
|
||||
false,
|
||||
format!("K线: [{名}] 浮点超限 容差={浮点容差:.2e} A={a:.10},B={b:.10}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
(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(始);
|
||||
@@ -281,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线 {
|
||||
@@ -341,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());
|
||||
}
|
||||
}
|
||||
|
||||
+187
-29
@@ -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
|
||||
@@ -408,8 +408,7 @@ impl 缠论K线 {
|
||||
}
|
||||
// 计算指标: 对齐 Python,仅当 计算指标 开启时执行
|
||||
if 配置.计算指标 {
|
||||
let n = 普K序列.len();
|
||||
指标计算器::计算并挂载(&普K序列[n - 1], &普K序列[..n - 1], 配置);
|
||||
指标计算器::计算并挂载(普K序列, 配置);
|
||||
}
|
||||
|
||||
// ---- 阶段2: 缠K合并 ----
|
||||
@@ -467,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(分型结构::底);
|
||||
}
|
||||
分型结构::散 => {}
|
||||
}
|
||||
@@ -531,6 +530,165 @@ impl 缠论K线 {
|
||||
let 终_idx = 序列.iter().position(|k| std::ptr::eq(Arc::as_ptr(k), 终))?;
|
||||
Some(序列[始_idx..=终_idx].to_vec())
|
||||
}
|
||||
|
||||
/// 结构化相等校验 — 比对所有字段,浮点容差,递归校验标的K线,返回 (是否相等, 差异描述)
|
||||
pub fn 相等(&self, other: &Self, 浮点容差: f64) -> (bool, String) {
|
||||
if self.序号.load(Ordering::Relaxed) != other.序号.load(Ordering::Relaxed) {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"缠论K线: [序号] 不等 A={},B={}",
|
||||
self.序号.load(Ordering::Relaxed),
|
||||
other.序号.load(Ordering::Relaxed)
|
||||
),
|
||||
);
|
||||
}
|
||||
if self.时间戳.load(Ordering::Relaxed) != other.时间戳.load(Ordering::Relaxed) {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"缠论K线: [时间戳] 不等 A={},B={}",
|
||||
self.时间戳.load(Ordering::Relaxed),
|
||||
other.时间戳.load(Ordering::Relaxed)
|
||||
),
|
||||
);
|
||||
}
|
||||
if (self.高.get() - other.高.get()).abs() > 浮点容差 {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"缠论K线: [高] 浮点超限 容差={浮点容差:.2e} A={:.10},B={:.10}",
|
||||
self.高.get(),
|
||||
other.高.get()
|
||||
),
|
||||
);
|
||||
}
|
||||
if (self.低.get() - other.低.get()).abs() > 浮点容差 {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"缠论K线: [低] 浮点超限 容差={浮点容差:.2e} A={:.10},B={:.10}",
|
||||
self.低.get(),
|
||||
other.低.get()
|
||||
),
|
||||
);
|
||||
}
|
||||
if *self.方向.read() != *other.方向.read() {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"缠论K线: [方向] 不等 A={},B={}",
|
||||
self.方向.read(),
|
||||
other.方向.read()
|
||||
),
|
||||
);
|
||||
}
|
||||
if *self.分型.read() != *other.分型.read() {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"缠论K线: [分型] 不等 A={:?},B={:?}",
|
||||
self.分型.read(),
|
||||
other.分型.read()
|
||||
),
|
||||
);
|
||||
}
|
||||
if self.周期 != other.周期 {
|
||||
return (
|
||||
false,
|
||||
format!("缠论K线: [周期] 不等 A={},B={}", self.周期, other.周期),
|
||||
);
|
||||
}
|
||||
if self.标识 != other.标识 {
|
||||
return (
|
||||
false,
|
||||
format!("缠论K线: [标识] 不等 A={},B={}", self.标识, other.标识),
|
||||
);
|
||||
}
|
||||
if (self.分型特征值.get() - other.分型特征值.get()).abs() > 浮点容差 {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"缠论K线: [分型特征值] 浮点超限 A={:.10},B={:.10}",
|
||||
self.分型特征值.get(),
|
||||
other.分型特征值.get()
|
||||
),
|
||||
);
|
||||
}
|
||||
if self.原始起始序号 != other.原始起始序号 {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"缠论K线: [原始起始序号] 不等 A={},B={}",
|
||||
self.原始起始序号, other.原始起始序号
|
||||
),
|
||||
);
|
||||
}
|
||||
if self.原始结束序号.load(Ordering::Relaxed) != other.原始结束序号.load(Ordering::Relaxed)
|
||||
{
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"缠论K线: [原始结束序号] 不等 A={},B={}",
|
||||
self.原始结束序号.load(Ordering::Relaxed),
|
||||
other.原始结束序号.load(Ordering::Relaxed)
|
||||
),
|
||||
);
|
||||
}
|
||||
// 标的K线 递归
|
||||
let (eq, msg) = self.标的K线.read().相等(&other.标的K线.read(), 浮点容差);
|
||||
if !eq {
|
||||
return (false, format!("缠论K线: 标的K线子项异常 >> {msg}"));
|
||||
}
|
||||
// 买卖点信息
|
||||
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 {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"缠论K线: [买卖点信息] 集合不等 A={:?},B={:?}",
|
||||
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线 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)*),
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
+9
-10
@@ -80,10 +80,9 @@ fn 测试_读取数据(文件路径: &str) {
|
||||
let 观察员 = 观察者::new("".into(), 0, 缠论配置::default());
|
||||
观察员
|
||||
.write()
|
||||
.unwrap()
|
||||
.读取数据文件(文件路径, 配置)
|
||||
.expect("读取数据文件失败");
|
||||
let 观察员 = 观察员.read().unwrap();
|
||||
let 观察员 = 观察员.read();
|
||||
let 消耗用时 = 启动时间.elapsed();
|
||||
println!(
|
||||
"测试_读取数据 耗时 {:.2?} 普K数量 {}",
|
||||
@@ -96,13 +95,13 @@ fn 测试_读取数据(文件路径: &str) {
|
||||
println!("分型数量: {}", 观察员.分型序列.len());
|
||||
println!("笔数量: {}", 观察员.笔序列.len());
|
||||
println!("笔中枢数量: {}", 观察员.笔_中枢序列.len());
|
||||
println!("线段数量: {}", 观察员.线段序列.len());
|
||||
println!("中枢数量: {}", 观察员.中枢序列.len());
|
||||
println!("扩展线段数量: {}", 观察员.扩展线段序列.len());
|
||||
println!("线段_线段序列数量: {}", 观察员.线段_线段序列.len());
|
||||
println!("线段数量: {}", 观察员.线段序列().len());
|
||||
println!("中枢数量: {}", 观察员.中枢序列().len());
|
||||
println!("扩展线段数量: {}", 观察员.扩展线段序列().len());
|
||||
println!("线段_线段序列数量: {}", 观察员.线段_线段序列().len());
|
||||
println!(
|
||||
"扩展线段_扩展线段数量: {}",
|
||||
观察员.扩展线段序列_扩展线段.len()
|
||||
观察员.扩展线段序列_扩展线段().len()
|
||||
);
|
||||
|
||||
println!("\n===== 保存分析数据 =====\n");
|
||||
@@ -160,15 +159,15 @@ fn 测试_周期合成(文件路径: &str) {
|
||||
// Display stats per period
|
||||
for &p in &[周期, 周期 * 5, 周期 * 5 * 6] {
|
||||
if let Some(观察员) = 多级别分析.获取观察者(p) {
|
||||
let 观察员 = 观察员.read().unwrap();
|
||||
let 观察员 = 观察员.read();
|
||||
println!(
|
||||
"周期<{}>: 缠K={}, 分型={}, 笔={}, 线段={}, 中枢={}",
|
||||
p,
|
||||
观察员.缠论K线序列.len(),
|
||||
观察员.分型序列.len(),
|
||||
观察员.笔序列.len(),
|
||||
观察员.线段序列.len(),
|
||||
观察员.中枢序列.len(),
|
||||
观察员.线段序列().len(),
|
||||
观察员.中枢序列().len(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/czsc,Apache License 2.0),已适配为 Rust。
|
||||
//!
|
||||
//! # 示例
|
||||
//!
|
||||
//! ```ignore
|
||||
//! use chanlun::signal::engine::{SignalEngine, SignalConfig, call_signal};
|
||||
//!
|
||||
//! let engine = SignalEngine::new(vec![SignalConfig {
|
||||
//! signal_name: "youwukuncheng_中枢第三买卖点_V230602".into(),
|
||||
//! freq: 86400,
|
||||
//! params: params_map,
|
||||
//! }]);
|
||||
//! engine.自动挂载指标(&analyzer);
|
||||
//! let results = engine.更新(&analyzer);
|
||||
//! ```
|
||||
|
||||
use crate::business::multi_frame::立体分析器;
|
||||
use crate::business::observer::观察者;
|
||||
use crate::signal::Signal;
|
||||
use crate::signal::registry;
|
||||
use serde_json::Value;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
/// 单一信号配置项 — 对应 Python 信号配置列表中的一条。
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SignalConfig {
|
||||
/// 注册表中的信号名,如 `"youwukuncheng_中枢第三买卖点_V230602"`
|
||||
pub signal_name: String,
|
||||
/// 本配置作用的周期(秒)
|
||||
pub freq: i64,
|
||||
/// 信号参数(含 `freq`,统一为字符串以便 Rust 信号函数读取)
|
||||
pub params: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
/// 完整更新结果:信号字典 + 基础周期行情数据。
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct 完整更新结果 {
|
||||
/// 信号 key → value 映射
|
||||
pub signals: HashMap<String, String>,
|
||||
/// 基础周期最后一根 K 线的 OHLCV 数据(若无 K 线则为 None)
|
||||
pub market: Option<MarketData>,
|
||||
}
|
||||
|
||||
/// 基础周期行情数据 — 对应 Python `信号计算器.行情`。
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MarketData {
|
||||
pub symbol: String,
|
||||
pub dt: i64, // Unix 秒(K线时间戳)
|
||||
pub id: i64, // K线序号
|
||||
pub open: f64,
|
||||
pub high: f64,
|
||||
pub low: f64,
|
||||
pub close: f64,
|
||||
pub vol: f64,
|
||||
}
|
||||
|
||||
/// 信号计算引擎 — 持有配置列表,按 `&立体分析器` 执行。
|
||||
///
|
||||
/// 引擎不持有分析器引用——每次调用时传入,避免借用冲突。
|
||||
pub struct SignalEngine {
|
||||
configs: Vec<SignalConfig>,
|
||||
}
|
||||
|
||||
impl SignalEngine {
|
||||
/// 创建引擎。配置中的信号名延迟到 `更新()` 时校验。
|
||||
pub fn new(configs: Vec<SignalConfig>) -> Self {
|
||||
Self { configs }
|
||||
}
|
||||
|
||||
/// 返回当前配置数量
|
||||
pub fn len(&self) -> usize {
|
||||
self.configs.len()
|
||||
}
|
||||
|
||||
/// 配置是否为空
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.configs.is_empty()
|
||||
}
|
||||
|
||||
/// 扫描信号名中的 MACD / 均线关键字,向各周期 observer 的配置中
|
||||
/// 追加缺失的指标参数,然后调用 `确保指标已计算()`(幂等)。
|
||||
///
|
||||
/// 与 Python `_自动挂载指标()` 逻辑一致。
|
||||
pub fn 自动挂载指标(&self, analyzer: &立体分析器) {
|
||||
// 第一遍:按周期收集需要的参数
|
||||
let mut macd_by_freq: HashMap<i64, Vec<(String, i64, i64, i64)>> = HashMap::new();
|
||||
let mut ma_by_freq: HashMap<i64, Vec<(String, String, i64)>> = HashMap::new();
|
||||
|
||||
for cfg in &self.configs {
|
||||
let name_lower = cfg.signal_name.to_lowercase();
|
||||
|
||||
// MACD 信号检测
|
||||
if name_lower.contains("macd")
|
||||
|| name_lower.contains("中枢")
|
||||
|| name_lower.contains("背驰")
|
||||
|| name_lower.contains("金叉")
|
||||
{
|
||||
let fast = cfg
|
||||
.params
|
||||
.get("fast")
|
||||
.and_then(|v| v.as_i64())
|
||||
.or_else(|| cfg.params.get("快线周期").and_then(|v| v.as_i64()))
|
||||
.unwrap_or(13);
|
||||
let slow = cfg
|
||||
.params
|
||||
.get("slow")
|
||||
.and_then(|v| v.as_i64())
|
||||
.or_else(|| cfg.params.get("慢线周期").and_then(|v| v.as_i64()))
|
||||
.unwrap_or(31);
|
||||
let signal = cfg
|
||||
.params
|
||||
.get("signal")
|
||||
.and_then(|v| v.as_i64())
|
||||
.or_else(|| cfg.params.get("信号周期").and_then(|v| v.as_i64()))
|
||||
.unwrap_or(11);
|
||||
|
||||
let key = format!("macd_{fast}_{slow}_{signal}");
|
||||
macd_by_freq
|
||||
.entry(cfg.freq)
|
||||
.or_default()
|
||||
.push((key, fast, slow, signal));
|
||||
}
|
||||
|
||||
// 均线信号检测
|
||||
if name_lower.contains("ma_")
|
||||
|| name_lower.contains("tas_ma")
|
||||
|| name_lower.contains("均线")
|
||||
{
|
||||
let ma_type = cfg
|
||||
.params
|
||||
.get("ma_type")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("SMA")
|
||||
.to_uppercase();
|
||||
let period = cfg
|
||||
.params
|
||||
.get("timeperiod")
|
||||
.and_then(|v| v.as_i64())
|
||||
.or_else(|| cfg.params.get("周期").and_then(|v| v.as_i64()))
|
||||
.unwrap_or(5);
|
||||
|
||||
let key = format!("{ma_type}_{period}");
|
||||
ma_by_freq
|
||||
.entry(cfg.freq)
|
||||
.or_default()
|
||||
.push((key, ma_type, period));
|
||||
}
|
||||
}
|
||||
|
||||
// 第二遍:写入 observer 配置(先收集已有 key,再 drop 后写入)
|
||||
for (freq, entries) in &macd_by_freq {
|
||||
if let Some(obs_arc) = analyzer.获取观察者(*freq) {
|
||||
let needs_push: Vec<(String, String, i64, i64, i64)> = {
|
||||
let obs = obs_arc.read();
|
||||
let existing: HashSet<String> =
|
||||
obs.配置.MACD_参数列表.iter().map(|t| t.0.clone()).collect();
|
||||
entries
|
||||
.iter()
|
||||
.filter(|(key, _, _, _)| !existing.contains(key))
|
||||
.map(|(key, fast, slow, signal)| {
|
||||
(key.clone(), "收".to_string(), *fast, *slow, *signal)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
if !needs_push.is_empty() {
|
||||
let mut obs = obs_arc.write();
|
||||
for tuple in needs_push {
|
||||
obs.配置.MACD_参数列表.push(tuple);
|
||||
}
|
||||
obs.配置.计算指标 = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (freq, entries) in &ma_by_freq {
|
||||
if let Some(obs_arc) = analyzer.获取观察者(*freq) {
|
||||
let needs_push: Vec<(String, String, String, i64)> = {
|
||||
let obs = obs_arc.read();
|
||||
let existing: HashSet<String> =
|
||||
obs.配置.均线参数列表.iter().map(|t| t.0.clone()).collect();
|
||||
entries
|
||||
.iter()
|
||||
.filter(|(key, _, _)| !existing.contains(key))
|
||||
.map(|(key, ma_type, period)| {
|
||||
(key.clone(), "收".to_string(), ma_type.clone(), *period)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
if !needs_push.is_empty() {
|
||||
let mut obs = obs_arc.write();
|
||||
for tuple in needs_push {
|
||||
obs.配置.均线参数列表.push(tuple);
|
||||
}
|
||||
obs.配置.计算指标 = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 第三遍:确保所有周期观察者的指标已计算(幂等)
|
||||
for freq in &analyzer.周期组 {
|
||||
if let Some(obs_arc) = analyzer.获取观察者(*freq) {
|
||||
obs_arc.read().确保指标已计算();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 遍历所有配置,执行信号函数,收集非空结果。
|
||||
///
|
||||
/// 返回 `{信号key: 信号value}` 字典(已过滤 `"任意_任意_任意_0"`)。
|
||||
/// 缺失的 observer 或未注册信号名会通过 tracing::warn! 记录并跳过。
|
||||
pub fn 更新(&self, analyzer: &立体分析器) -> HashMap<String, String> {
|
||||
let mut results: HashMap<String, String> = HashMap::new();
|
||||
|
||||
for cfg in &self.configs {
|
||||
let obs_arc = match analyzer.获取观察者(cfg.freq) {
|
||||
Some(o) => o,
|
||||
None => {
|
||||
tracing::warn!("信号引擎: 未找到周期 {} 的观察者", cfg.freq);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let meta = match registry::get_signal(&cfg.signal_name) {
|
||||
Some(m) => m,
|
||||
None => {
|
||||
tracing::warn!("信号引擎: 信号未注册: {}", cfg.signal_name);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let signals = {
|
||||
let obs_guard = obs_arc.read();
|
||||
(meta.func)(&obs_guard, &cfg.params)
|
||||
};
|
||||
|
||||
for sig in signals {
|
||||
if sig.value() != "任意_任意_任意_0" {
|
||||
results.insert(sig.key(), sig.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
/// 运行信号计算并附带基础周期行情。
|
||||
///
|
||||
/// `base_freq` 使用分析器的第一个周期(最小周期)。
|
||||
/// 返回的 `完整更新结果` 可直接组合为 Python `信号字典` 格式。
|
||||
pub fn 更新_完整(&self, analyzer: &立体分析器) -> 完整更新结果 {
|
||||
let signals = self.更新(analyzer);
|
||||
|
||||
let base_freq = analyzer.周期组.first().copied().unwrap_or(0);
|
||||
let market = analyzer.单体分析器.get(&base_freq).and_then(|obs| {
|
||||
let obs_guard = obs.read();
|
||||
obs_guard.普通K线序列.last().map(|k| MarketData {
|
||||
symbol: obs_guard.符号.clone(),
|
||||
dt: k.时间戳,
|
||||
id: k.序号,
|
||||
open: k.开盘价,
|
||||
high: k.高,
|
||||
low: k.低,
|
||||
close: k.收盘价,
|
||||
vol: k.成交量,
|
||||
})
|
||||
});
|
||||
|
||||
完整更新结果 { signals, market }
|
||||
}
|
||||
}
|
||||
|
||||
/// 按名查找并调用单个信号函数。
|
||||
///
|
||||
/// 适用于已有 `&观察者` 的场景(测试、单周期分析),无需构造完整的 `SignalEngine`。
|
||||
pub fn call_signal(
|
||||
name: &str,
|
||||
obs: &观察者,
|
||||
params: &HashMap<String, Value>,
|
||||
) -> Result<Vec<Signal>, String> {
|
||||
let meta = registry::get_signal(name).ok_or_else(|| format!("信号未注册: {name}"))?;
|
||||
Ok((meta.func)(obs, params))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::缠论配置;
|
||||
|
||||
/// 通过 call_signal 调用 youwukuncheng 信号,验证产出格式。
|
||||
#[test]
|
||||
fn test_call_signal_youwukuncheng() {
|
||||
let nb_path = concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../templates/btcusd-86400-1608854400-1781568000.nb"
|
||||
);
|
||||
|
||||
let 观察员 = 观察者::new("btcusd".into(), 86400, 缠论配置::default());
|
||||
观察员
|
||||
.write()
|
||||
.读取数据文件(nb_path, 缠论配置::default().不推送())
|
||||
.expect("读取数据文件失败");
|
||||
|
||||
let obs = 观察员.read();
|
||||
// 信号函数内部会调用 确保指标已计算,但为稳妥先调用一次
|
||||
obs.确保指标已计算();
|
||||
|
||||
let mut params: HashMap<String, Value> = HashMap::new();
|
||||
params.insert("freq".into(), Value::String("日线".into()));
|
||||
params.insert(
|
||||
"max_overlap".into(),
|
||||
Value::Number(serde_json::Number::from(3)),
|
||||
);
|
||||
params.insert("本级完整性".into(), Value::String("实".into()));
|
||||
params.insert("同级完整性".into(), Value::String("合".into()));
|
||||
|
||||
let signals = call_signal("youwukuncheng_中枢第三买卖点_V230602", &obs, ¶ms)
|
||||
.expect("call_signal 应成功");
|
||||
|
||||
assert!(!signals.is_empty(), "至少应返回一个信号(可能是空)");
|
||||
for s in &signals {
|
||||
assert!(s.k3.ends_with("V230602"), "k3 应以 V230602 结尾: {}", s.k3);
|
||||
assert!((0..=100).contains(&s.score), "score 超范围: {}", s.score);
|
||||
}
|
||||
|
||||
// 验证非空信号
|
||||
let non_empty: Vec<_> = signals
|
||||
.iter()
|
||||
.filter(|s| s.value() != "任意_任意_任意_0")
|
||||
.collect();
|
||||
println!(
|
||||
"call_signal: {} signals, {} non-empty",
|
||||
signals.len(),
|
||||
non_empty.len()
|
||||
);
|
||||
for s in &non_empty {
|
||||
println!(" k3={} v1={} v2={} score={}", s.k3, s.v1, s.v2, s.score);
|
||||
}
|
||||
}
|
||||
|
||||
/// 空配置返回空结果
|
||||
#[test]
|
||||
fn test_engine_空配置_返回空() {
|
||||
use crate::business::multi_frame::立体分析器;
|
||||
|
||||
// 立体分析器 至少需要 2 个周期(周期组[0]=输入周期,周期组[1]=显示周期)
|
||||
let analyzer = 立体分析器::new("test".into(), vec![300, 900], None, None);
|
||||
let engine = SignalEngine::new(vec![]);
|
||||
let results = engine.更新(&analyzer);
|
||||
assert!(results.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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`。
|
||||
//!
|
||||
//! 第三方代码声明:信号函数模式参考 czsc(https://github.com/waditu/czsc,
|
||||
//! Apache License 2.0),已适配为 Rust。
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use chanlun_signal_macros::signal;
|
||||
|
||||
use crate::business::observer::观察者;
|
||||
use crate::kline::bar::K线;
|
||||
use crate::signal::Signal;
|
||||
use crate::signal::params;
|
||||
|
||||
// =============================================================================
|
||||
// bar — K线形态信号
|
||||
// =============================================================================
|
||||
|
||||
/// 涨跌停检测信号。
|
||||
///
|
||||
/// `close == high && close >= prev_close` → 涨停
|
||||
/// `close == low && close <= prev_close` → 跌停
|
||||
#[signal(name = "bar_zdt_V230331", template = "{freq}_D{di}_涨跌停V230331")]
|
||||
pub fn bar_zdt_V230331(obs: &观察者, params: &HashMap<String, Value>) -> Vec<Signal> {
|
||||
let di = params::get_int(params, "di", 1) as usize;
|
||||
let freq = params::get_string(params, "freq", "15分钟");
|
||||
let k1 = freq;
|
||||
let k2 = format!("D{di}");
|
||||
let k3 = "涨跌停V230331";
|
||||
|
||||
let 普K序列 = &obs.普通K线序列;
|
||||
if 普K序列.len() < di + 2 {
|
||||
return vec![Signal::new_empty(&k1, &k2, k3)];
|
||||
}
|
||||
|
||||
let 当前K线 = &普K序列[普K序列.len() - di];
|
||||
let 前K线 = &普K序列[普K序列.len() - di - 1];
|
||||
|
||||
let v1 = if 当前K线.收盘价 == 当前K线.高 && 当前K线.收盘价 >= 前K线.收盘价
|
||||
{
|
||||
"涨停"
|
||||
} else if 当前K线.收盘价 == 当前K线.低 && 当前K线.收盘价 <= 前K线.收盘价 {
|
||||
"跌停"
|
||||
} else {
|
||||
"任意"
|
||||
};
|
||||
|
||||
if v1 == "任意" {
|
||||
vec![Signal::new_empty(&k1, &k2, k3)]
|
||||
} else {
|
||||
vec![Signal::new(&k1, &k2, k3, v1, "任意", "任意", 0)]
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// tas — 技术指标信号
|
||||
// =============================================================================
|
||||
|
||||
/// MACD 金叉死叉信号 — DIF 与 DEA 的交叉判断。
|
||||
///
|
||||
/// DIF 上穿 DEA → 金叉;DIF 下穿 DEA → 死叉。
|
||||
#[signal(
|
||||
name = "macd_金叉_V260601",
|
||||
template = "{freq}_D{di}#MACD#{fast}#{slow}#{signal}_MACD交叉V260601"
|
||||
)]
|
||||
pub fn macd_金叉_V260601(obs: &观察者, params: &HashMap<String, Value>) -> Vec<Signal> {
|
||||
let fast = params::get_int(params, "fast", 13);
|
||||
let slow = params::get_int(params, "slow", 31);
|
||||
let signal_p = params::get_int(params, "signal", 11);
|
||||
let di = params::get_int(params, "di", 1) as usize;
|
||||
let freq = params::get_string(params, "freq", "15分钟");
|
||||
|
||||
let k1 = freq;
|
||||
let k2 = format!("D{di}#MACD#{fast}#{slow}#{signal_p}");
|
||||
let k3 = "MACD交叉V260601";
|
||||
|
||||
obs.确保指标已计算();
|
||||
|
||||
let 普K序列 = &obs.普通K线序列;
|
||||
if 普K序列.len() < di + 2 {
|
||||
return vec![Signal::new_empty(&k1, &k2, k3)];
|
||||
}
|
||||
|
||||
let 当前K线 = &普K序列[普K序列.len() - di];
|
||||
let 前K线 = &普K序列[普K序列.len() - di - 1];
|
||||
|
||||
let cur_dif = match 当前K线.macd().as_ref().and_then(|m| m.DIF) {
|
||||
Some(v) => v,
|
||||
None => return vec![Signal::new_empty(&k1, &k2, k3)],
|
||||
};
|
||||
let cur_dea = match 当前K线.macd().as_ref().and_then(|m| m.DEA) {
|
||||
Some(v) => v,
|
||||
None => return vec![Signal::new_empty(&k1, &k2, k3)],
|
||||
};
|
||||
let prev_dif = match 前K线.macd().as_ref().and_then(|m| m.DIF) {
|
||||
Some(v) => v,
|
||||
None => return vec![Signal::new_empty(&k1, &k2, k3)],
|
||||
};
|
||||
let prev_dea = match 前K线.macd().as_ref().and_then(|m| m.DEA) {
|
||||
Some(v) => v,
|
||||
None => return vec![Signal::new_empty(&k1, &k2, k3)],
|
||||
};
|
||||
|
||||
let v1 = if prev_dif <= prev_dea && cur_dif > cur_dea {
|
||||
"金叉"
|
||||
} else if prev_dif >= prev_dea && cur_dif < cur_dea {
|
||||
"死叉"
|
||||
} else {
|
||||
"任意"
|
||||
};
|
||||
|
||||
if v1 == "任意" {
|
||||
vec![Signal::new_empty(&k1, &k2, k3)]
|
||||
} else {
|
||||
vec![Signal::new(&k1, &k2, k3, v1, "任意", "任意", 0)]
|
||||
}
|
||||
}
|
||||
|
||||
/// MACD 方向信号 — DIF 在零轴上方为多头,下方为空头。
|
||||
#[signal(
|
||||
name = "tas_macd_direct_V221106",
|
||||
template = "{freq}_D{di}#MACD#{fast}#{slow}#{signal}_MACD方向V221106"
|
||||
)]
|
||||
pub fn tas_macd_direct_V221106(obs: &观察者, params: &HashMap<String, Value>) -> Vec<Signal> {
|
||||
let fast = params::get_int(params, "fast", 13);
|
||||
let slow = params::get_int(params, "slow", 31);
|
||||
let signal_p = params::get_int(params, "signal", 11);
|
||||
let di = params::get_int(params, "di", 1) as usize;
|
||||
let freq = params::get_string(params, "freq", "15分钟");
|
||||
|
||||
let k1 = freq;
|
||||
let k2 = format!("D{di}#MACD#{fast}#{slow}#{signal_p}");
|
||||
let k3 = "MACD方向V221106";
|
||||
|
||||
obs.确保指标已计算();
|
||||
|
||||
let 普K序列 = &obs.普通K线序列;
|
||||
if 普K序列.len() < di + 1 {
|
||||
return vec![Signal::new_empty(&k1, &k2, k3)];
|
||||
}
|
||||
|
||||
let 当前K线 = &普K序列[普K序列.len() - di];
|
||||
let cur_dif = match 当前K线.macd().as_ref().and_then(|m| m.DIF) {
|
||||
Some(v) => v,
|
||||
None => return vec![Signal::new_empty(&k1, &k2, k3)],
|
||||
};
|
||||
|
||||
let v1 = if cur_dif > 0.0 { "看多" } else { "看空" };
|
||||
|
||||
let v2 = if 普K序列.len() >= di + 2 {
|
||||
let 前K线 = &普K序列[普K序列.len() - di - 1];
|
||||
match 前K线.macd().as_ref().and_then(|m| m.DIF) {
|
||||
Some(prev_dif) => {
|
||||
if cur_dif > prev_dif {
|
||||
"向上"
|
||||
} else {
|
||||
"向下"
|
||||
}
|
||||
}
|
||||
None => "任意",
|
||||
}
|
||||
} else {
|
||||
"任意"
|
||||
};
|
||||
|
||||
vec![Signal::new(&k1, &k2, k3, v1, v2, "任意", 0)]
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 内部辅助 — 均线按需计算
|
||||
// =============================================================================
|
||||
|
||||
/// 按需计算均线值(SMA / EMA)。
|
||||
fn 计算均线(
|
||||
普K序列: &[std::sync::Arc<K线>],
|
||||
ma_type: &str,
|
||||
timeperiod: usize,
|
||||
offset: usize,
|
||||
) -> Option<f64> {
|
||||
let n = 普K序列.len();
|
||||
let start = n.checked_sub(offset + timeperiod)?;
|
||||
let end = n.checked_sub(offset)?;
|
||||
if start >= end {
|
||||
return None;
|
||||
}
|
||||
let closes: Vec<f64> = 普K序列[start..end].iter().map(|k| k.收盘价).collect();
|
||||
if closes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
match ma_type {
|
||||
"SMA" | "sma" => Some(closes.iter().sum::<f64>() / closes.len() as f64),
|
||||
"EMA" | "ema" => {
|
||||
let k = 2.0 / (timeperiod as f64 + 1.0);
|
||||
let mut ema = closes[0];
|
||||
for &price in &closes[1..] {
|
||||
ema = price * k + ema * (1.0 - k);
|
||||
}
|
||||
Some(ema)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 从均线缓存或按需计算获取均线值。
|
||||
fn 获取均线(
|
||||
普K序列: &[std::sync::Arc<K线>],
|
||||
k线: &K线,
|
||||
ma_type: &str,
|
||||
timeperiod: usize,
|
||||
offset: usize,
|
||||
) -> Option<f64> {
|
||||
let ma_key = format!("{}_{}", ma_type.to_uppercase(), timeperiod);
|
||||
if let Some(ma_map) = k线.ma(&ma_key) {
|
||||
return Some(ma_map);
|
||||
}
|
||||
计算均线(普K序列, ma_type, timeperiod, offset)
|
||||
}
|
||||
|
||||
/// 单均线多空和方向信号。
|
||||
#[signal(
|
||||
name = "tas_ma_base_V230313",
|
||||
template = "{freq}_D{di}#{ma_type}#{timeperiod}MO{max_overlap}_BS辅助V230313"
|
||||
)]
|
||||
pub fn tas_ma_base_V230313(obs: &观察者, params: &HashMap<String, Value>) -> Vec<Signal> {
|
||||
let ma_type = params::get_string(params, "ma_type", "SMA").to_uppercase();
|
||||
let timeperiod = params::get_int(params, "timeperiod", 5) as usize;
|
||||
let di = params::get_int(params, "di", 1) as usize;
|
||||
let max_overlap = params::get_int(params, "max_overlap", 5);
|
||||
let freq = params::get_string(params, "freq", "15分钟");
|
||||
|
||||
let k1 = freq;
|
||||
let k2 = format!("D{di}#{ma_type}#{timeperiod}MO{max_overlap}");
|
||||
let k3 = "BS辅助V230313";
|
||||
|
||||
let 普K序列 = &obs.普通K线序列;
|
||||
if 普K序列.len() < di + 1 {
|
||||
return vec![Signal::new_empty(&k1, &k2, k3)];
|
||||
}
|
||||
|
||||
let 当前K线 = &普K序列[普K序列.len() - di];
|
||||
let 当前均线 = match 获取均线(普K序列, 当前K线, &ma_type, timeperiod, di) {
|
||||
Some(v) => v,
|
||||
None => return vec![Signal::new_empty(&k1, &k2, k3)],
|
||||
};
|
||||
|
||||
let v1 = if 当前K线.收盘价 > 当前均线 {
|
||||
"看多"
|
||||
} else {
|
||||
"看空"
|
||||
};
|
||||
|
||||
let v2 = if 普K序列.len() >= di + 2 {
|
||||
let 前K线 = &普K序列[普K序列.len() - di - 1];
|
||||
match 获取均线(普K序列, 前K线, &ma_type, timeperiod, di + 1) {
|
||||
Some(前均线) => {
|
||||
if 当前均线 > 前均线 {
|
||||
"向上"
|
||||
} else {
|
||||
"向下"
|
||||
}
|
||||
}
|
||||
None => "任意",
|
||||
}
|
||||
} else {
|
||||
"任意"
|
||||
};
|
||||
|
||||
vec![Signal::new(&k1, &k2, k3, v1, v2, "任意", 0)]
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// cxt — 缠论形态信号
|
||||
// =============================================================================
|
||||
|
||||
/// 停顿分型辅助信号 — 结合分型强度和 MACD 柱子匹配判断。
|
||||
#[signal(
|
||||
name = "cxt_停顿分型_V230106",
|
||||
template = "{freq}_D{di}停顿分型_BE辅助V230106"
|
||||
)]
|
||||
pub fn cxt_停顿分型_V230106(obs: &观察者, params: &HashMap<String, Value>) -> Vec<Signal> {
|
||||
let di = params::get_int(params, "di", 0) as usize;
|
||||
let freq = params::get_string(params, "freq", "1分钟");
|
||||
|
||||
let k1 = freq;
|
||||
let k2 = format!("D{di}停顿分型");
|
||||
let k3 = "BE辅助V230106";
|
||||
|
||||
let 分型序列 = &obs.分型序列;
|
||||
if 分型序列.len() < di + 1 {
|
||||
return vec![Signal::new_empty(&k1, &k2, k3)];
|
||||
}
|
||||
|
||||
let 当前分型 = &分型序列[分型序列.len() - (di + 1)];
|
||||
|
||||
// 只对顶/底分型产出信号
|
||||
let 结构值 = 当前分型.结构.to_string();
|
||||
if 结构值 != "顶" && 结构值 != "底" {
|
||||
return vec![Signal::new_empty(&k1, &k2, k3)];
|
||||
}
|
||||
|
||||
let v1 = if 结构值 == "顶" {
|
||||
"看空"
|
||||
} else {
|
||||
"看多"
|
||||
};
|
||||
let v2 = 当前分型.强度();
|
||||
|
||||
// 仅强/中分型产出有效信号
|
||||
if v2 != "强" && v2 != "中" {
|
||||
return vec![Signal::new_empty(&k1, &k2, k3)];
|
||||
}
|
||||
|
||||
vec![Signal::new(&k1, &k2, k3, v1, v2, "任意", 0)]
|
||||
}
|
||||
|
||||
/// 笔结束辅助信号 — 统计最后笔之后的新高/新低分型次数。
|
||||
#[signal(
|
||||
name = "cxt_bi_end_V230222",
|
||||
template = "{freq}_D1MO{max_overlap}_BE辅助V230222"
|
||||
)]
|
||||
pub fn cxt_bi_end_V230222(obs: &观察者, params: &HashMap<String, Value>) -> Vec<Signal> {
|
||||
let max_overlap = params::get_int(params, "max_overlap", 3);
|
||||
let freq = params::get_string(params, "freq", "日线");
|
||||
|
||||
let k1 = freq;
|
||||
let k2 = format!("D1MO{max_overlap}");
|
||||
let k3 = "BE辅助V230222";
|
||||
|
||||
let 分型序列 = &obs.分型序列;
|
||||
let 笔序列 = &obs.笔序列;
|
||||
|
||||
if 分型序列.len() < 2 || 笔序列.is_empty() {
|
||||
return vec![Signal::new_empty(&k1, &k2, k3)];
|
||||
}
|
||||
|
||||
let 最后笔 = &笔序列[笔序列.len() - 1];
|
||||
let 当前分型 = &分型序列[分型序列.len() - 1];
|
||||
|
||||
// 找到最后笔的武(终点分型)在分型序列中的位置
|
||||
let 笔武分型: std::sync::Arc<crate::structure::fractal_obj::分型> =
|
||||
{ 最后笔.武.read().clone() };
|
||||
|
||||
let 笔终点时间戳 = 笔武分型.时间戳;
|
||||
let 笔终点结构 = 笔武分型.结构;
|
||||
|
||||
let 笔终点索引 = match 分型序列
|
||||
.iter()
|
||||
.position(|f| f.时间戳 == 笔终点时间戳 && f.结构 == 笔终点结构)
|
||||
{
|
||||
Some(idx) => idx,
|
||||
None => return vec![Signal::new_empty(&k1, &k2, k3)],
|
||||
};
|
||||
|
||||
// 取笔终点之后的分型
|
||||
if 笔终点索引 + 1 >= 分型序列.len() {
|
||||
return vec![Signal::new_empty(&k1, &k2, k3)];
|
||||
}
|
||||
let 未成笔分型 = &分型序列[笔终点索引 + 1..];
|
||||
|
||||
let 当前结构 = 当前分型.结构;
|
||||
let 当前分型特征值 = 当前分型.分型特征值;
|
||||
|
||||
if 当前结构.to_string() == "顶" {
|
||||
let mut 笔终点顶高 = 笔武分型.分型特征值;
|
||||
let mut 计数 = 0i32;
|
||||
for f in 未成笔分型 {
|
||||
if f.结构 == 当前结构 && f.分型特征值 > 笔终点顶高 {
|
||||
计数 += 1;
|
||||
笔终点顶高 = f.分型特征值;
|
||||
}
|
||||
}
|
||||
if 计数 > 0 && 当前分型特征值 >= 笔终点顶高 {
|
||||
vec![Signal::new(
|
||||
&k1,
|
||||
&k2,
|
||||
k3,
|
||||
"新高",
|
||||
&format!("第{计数}次"),
|
||||
"任意",
|
||||
0,
|
||||
)]
|
||||
} else {
|
||||
vec![Signal::new_empty(&k1, &k2, k3)]
|
||||
}
|
||||
} else if 当前结构.to_string() == "底" {
|
||||
let mut 笔终点底低 = 笔武分型.分型特征值;
|
||||
let mut 计数 = 0i32;
|
||||
for f in 未成笔分型 {
|
||||
if f.结构 == 当前结构 && f.分型特征值 < 笔终点底低 {
|
||||
计数 += 1;
|
||||
笔终点底低 = f.分型特征值;
|
||||
}
|
||||
}
|
||||
if 计数 > 0 && 当前分型特征值 <= 笔终点底低 {
|
||||
vec![Signal::new(
|
||||
&k1,
|
||||
&k2,
|
||||
k3,
|
||||
"新低",
|
||||
&format!("第{计数}次"),
|
||||
"任意",
|
||||
0,
|
||||
)]
|
||||
} else {
|
||||
vec![Signal::new_empty(&k1, &k2, k3)]
|
||||
}
|
||||
} else {
|
||||
vec![Signal::new_empty(&k1, &k2, k3)]
|
||||
}
|
||||
}
|
||||
@@ -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]` 注册的函数对应一个子模块。
|
||||
//!
|
||||
//! 第三方代码声明:信号函数模式参考 czsc(https://github.com/waditu/czsc,
|
||||
//! Apache License 2.0),已适配为 Rust `fn(&观察者, &HashMap<String, Value>) -> Vec<Signal>`。
|
||||
|
||||
pub mod demo;
|
||||
// pub mod youwukuncheng;
|
||||
@@ -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])
|
||||
}
|
||||
@@ -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(), "持币");
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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.
|
||||
*/
|
||||
|
||||
//! 信号注册表 —— 编译期收集 + 运行时动态注册。
|
||||
//!
|
||||
//! 第三方代码声明:注册机制参考 czsc(https://github.com/waditu/czsc,
|
||||
//! Apache License 2.0),已简化适配(无 category / TaCache)。
|
||||
//!
|
||||
//! # 双注册表架构
|
||||
//!
|
||||
//! - `SIGNAL_REGISTRY`: 编译时,`#[signal]` 宏 + `inventory`,不可变。
|
||||
//! - `DYNAMIC_REGISTRY`: 运行时,`register_signal()` / `unregister_signal()`,`RwLock`。
|
||||
//!
|
||||
//! 查找时先查编译时,再查动态。同名时编译时优先(动态注册被遮蔽)。
|
||||
|
||||
use crate::business::observer::观察者;
|
||||
use crate::signal::Signal;
|
||||
use parking_lot::RwLock;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
/// 信号函数签名 —— 读观察者状态(含 K线已挂指标)+ 参数 → 信号列表。无 TaCache。
|
||||
pub type SignalFn = fn(&观察者, &HashMap<String, Value>) -> Vec<Signal>;
|
||||
|
||||
/// 信号描述符(编译期元数据,由 `#[signal]` 宏生成、`inventory` 收集)。
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct SignalDescriptor {
|
||||
pub name: &'static str,
|
||||
pub template: &'static str,
|
||||
pub func: SignalFn,
|
||||
}
|
||||
|
||||
inventory::collect!(SignalDescriptor);
|
||||
|
||||
/// 运行时信号元信息(编译时和动态共用)。
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SignalMeta {
|
||||
pub func: SignalFn,
|
||||
pub template: String,
|
||||
}
|
||||
|
||||
/// 归并描述符为注册表;重名返回 Err。
|
||||
fn 归并(
|
||||
descs: impl Iterator<Item = SignalDescriptor>,
|
||||
) -> Result<HashMap<&'static str, SignalMeta>, String> {
|
||||
let mut m: HashMap<&'static str, SignalMeta> = HashMap::new();
|
||||
for d in descs {
|
||||
if m.insert(
|
||||
d.name,
|
||||
SignalMeta {
|
||||
func: d.func,
|
||||
template: d.template.to_string(),
|
||||
},
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
return Err(format!("信号重名:{}", d.name));
|
||||
}
|
||||
}
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
/// 编译时注册表(`#[signal]` 宏,inventory 收集,不可变)。
|
||||
pub static SIGNAL_REGISTRY: LazyLock<HashMap<&'static str, SignalMeta>> = LazyLock::new(|| {
|
||||
归并(inventory::iter::<SignalDescriptor>.into_iter().copied()).unwrap_or_else(|e| panic!("{e}"))
|
||||
});
|
||||
|
||||
/// 动态注册表(运行时注册,RwLock)。
|
||||
pub static DYNAMIC_REGISTRY: LazyLock<RwLock<HashMap<String, SignalMeta>>> =
|
||||
LazyLock::new(|| RwLock::new(HashMap::new()));
|
||||
|
||||
// ============================================================================
|
||||
// 查询 API
|
||||
// ============================================================================
|
||||
|
||||
/// 按名查信号元信息。先查编译时,再查动态。
|
||||
pub fn get_signal(name: &str) -> Option<SignalMeta> {
|
||||
if let Some(m) = SIGNAL_REGISTRY.get(name) {
|
||||
return Some(m.clone());
|
||||
}
|
||||
DYNAMIC_REGISTRY.read().get(name).cloned()
|
||||
}
|
||||
|
||||
/// 按名查参数模板。
|
||||
pub fn get_template(name: &str) -> Option<String> {
|
||||
get_signal(name).map(|m| m.template)
|
||||
}
|
||||
|
||||
/// 列出所有已注册信号名(编译时 + 动态,排序)。
|
||||
pub fn list_signal_names() -> Vec<String> {
|
||||
let mut v: Vec<String> = SIGNAL_REGISTRY.keys().map(|k| k.to_string()).collect();
|
||||
for k in DYNAMIC_REGISTRY.read().keys() {
|
||||
if !SIGNAL_REGISTRY.contains_key(k.as_str()) {
|
||||
v.push(k.clone());
|
||||
}
|
||||
}
|
||||
v.sort();
|
||||
v
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 动态注册 API
|
||||
// ============================================================================
|
||||
|
||||
/// 运行时动态注册信号函数。
|
||||
///
|
||||
/// - `name`: 信号名(必须全局唯一)
|
||||
/// - `template`: 参数模板,如 `"{freq}_D{di}_涨跌停V230331"`
|
||||
/// - `func`: 信号函数指针
|
||||
///
|
||||
/// 返回 `Err` 如果同名信号已存在于编译时或动态注册表中。
|
||||
pub fn register_signal(name: &str, template: &str, func: SignalFn) -> Result<(), String> {
|
||||
if SIGNAL_REGISTRY.contains_key(name) {
|
||||
return Err(format!("信号 '{name}' 已在编译时注册表中,无法覆盖"));
|
||||
}
|
||||
let mut dyn_reg = DYNAMIC_REGISTRY.write();
|
||||
if dyn_reg.contains_key(name) {
|
||||
return Err(format!("信号 '{name}' 已在动态注册表中"));
|
||||
}
|
||||
dyn_reg.insert(
|
||||
name.to_string(),
|
||||
SignalMeta {
|
||||
func,
|
||||
template: template.to_string(),
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 从动态注册表中移除信号。返回 `Err` 如果信号不存在或属于编译时注册表。
|
||||
pub fn unregister_signal(name: &str) -> Result<(), String> {
|
||||
if SIGNAL_REGISTRY.contains_key(name) {
|
||||
return Err(format!("信号 '{name}' 属于编译时注册表,无法动态移除"));
|
||||
}
|
||||
let mut dyn_reg = DYNAMIC_REGISTRY.write();
|
||||
if dyn_reg.remove(name).is_none() {
|
||||
return Err(format!("信号 '{name}' 不在动态注册表中"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn __probe(_obs: &观察者, _p: &HashMap<String, Value>) -> Vec<Signal> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn 描述符(name: &'static str) -> SignalDescriptor {
|
||||
SignalDescriptor {
|
||||
name,
|
||||
template: "{freq}_D1_probe",
|
||||
func: __probe,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_归并_正常() {
|
||||
let m = 归并([描述符("a_V000001"), 描述符("b_V000001")].into_iter()).unwrap();
|
||||
assert_eq!(m.len(), 2);
|
||||
assert!(m.contains_key("a_V000001"));
|
||||
assert_eq!(m["a_V000001"].template, "{freq}_D1_probe");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_归并_重名_返回Err() {
|
||||
let r = 归并([描述符("dup_V000001"), 描述符("dup_V000001")].into_iter());
|
||||
assert!(r.is_err());
|
||||
assert!(r.unwrap_err().contains("信号重名"));
|
||||
}
|
||||
|
||||
// ── 动态注册测试 ──
|
||||
|
||||
#[test]
|
||||
fn test_动态注册成功() {
|
||||
assert!(register_signal("__dyn_test_V000001", "{freq}_D1_test", __probe).is_ok());
|
||||
let meta = get_signal("__dyn_test_V000001").unwrap();
|
||||
assert_eq!(meta.template, "{freq}_D1_test");
|
||||
// 清理
|
||||
unregister_signal("__dyn_test_V000001").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_动态重名_报错() {
|
||||
register_signal("__dyn_dup_V000001", "{freq}_D1_a", __probe).unwrap();
|
||||
let r = register_signal("__dyn_dup_V000001", "{freq}_D1_b", __probe);
|
||||
assert!(r.is_err());
|
||||
assert!(r.unwrap_err().contains("已在动态注册表中"));
|
||||
unregister_signal("__dyn_dup_V000001").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_动态覆盖编译时_报错() {
|
||||
// 编译时已注册的信号不允许动态覆盖
|
||||
let r = register_signal("__probe_inventory_V000000", "{freq}_test", __probe);
|
||||
assert!(r.is_err());
|
||||
assert!(r.unwrap_err().contains("已"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_注销_成功() {
|
||||
register_signal("__dyn_rm_V000001", "{freq}_D1_rm", __probe).unwrap();
|
||||
assert!(get_signal("__dyn_rm_V000001").is_some());
|
||||
unregister_signal("__dyn_rm_V000001").unwrap();
|
||||
assert!(get_signal("__dyn_rm_V000001").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_注销编译时_报错() {
|
||||
let r = unregister_signal("__probe_inventory_V000000");
|
||||
assert!(r.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_包含动态信号() {
|
||||
register_signal("__list_dyn_V000001", "{freq}_test", __probe).unwrap();
|
||||
let names = list_signal_names();
|
||||
assert!(names.contains(&"__list_dyn_V000001".to_string()));
|
||||
// 编译时信号也在
|
||||
assert!(names.contains(&"__probe_inventory_V000000".to_string()));
|
||||
unregister_signal("__list_dyn_V000001").unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_动态优先_编译时不遮蔽() {
|
||||
// 编译时信号正常返回
|
||||
let meta = get_signal("__probe_inventory_V000000").unwrap();
|
||||
assert_eq!(meta.template, "{freq}_D1_probe_inventory");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn __probe_for_inventory(_obs: &观察者, _p: &HashMap<String, Value>) -> Vec<Signal> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
inventory::submit! {
|
||||
SignalDescriptor {
|
||||
name: "__probe_inventory_V000000",
|
||||
template: "{freq}_D1_probe_inventory",
|
||||
func: __probe_for_inventory as SignalFn,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod inventory_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_全局注册表收到inventory探针() {
|
||||
assert!(get_signal("__probe_inventory_V000000").is_some());
|
||||
assert_eq!(
|
||||
get_template("__probe_inventory_V000000"),
|
||||
Some("{freq}_D1_probe_inventory".to_string())
|
||||
);
|
||||
assert!(list_signal_names().contains(&"__probe_inventory_V000000".to_string()));
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+654
-261
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
@@ -66,7 +66,7 @@ impl 分型 {
|
||||
左: Option<Arc<缠论K线>>, 中: Arc<缠论K线>, 右: Option<Arc<缠论K线>>
|
||||
) -> Self {
|
||||
if let (Some(左), Some(右)) = (&左, &右) {
|
||||
debug_assert!(
|
||||
assert!(
|
||||
左.时间戳.load(Ordering::Relaxed) < 中.时间戳.load(Ordering::Relaxed)
|
||||
&& 中.时间戳.load(Ordering::Relaxed) < 右.时间戳.load(Ordering::Relaxed),
|
||||
"分型时间戳断言失败: 左={}, 中={}, 右={}",
|
||||
@@ -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 {
|
||||
@@ -256,6 +252,68 @@ impl 分型 {
|
||||
}
|
||||
分型序列.push(当前分型);
|
||||
}
|
||||
|
||||
/// 结构化相等校验 — 递归校验左/中/右缠伦K线 + 自有缓存字段,返回 (是否相等, 差异描述)
|
||||
pub fn 相等(&self, other: &Self, 浮点容差: f64) -> (bool, String) {
|
||||
match (&self.左, &other.左) {
|
||||
(None, None) => {}
|
||||
(Some(a), Some(b)) => {
|
||||
let (eq, msg) = a.相等(b, 浮点容差);
|
||||
if !eq {
|
||||
return (false, format!("分型: [左]缠论K线异常 >> {msg}"));
|
||||
}
|
||||
}
|
||||
(a, b) => {
|
||||
return (
|
||||
false,
|
||||
format!("分型: [左]空值不一致 A={},B={}", a.is_some(), b.is_some()),
|
||||
);
|
||||
}
|
||||
}
|
||||
{
|
||||
let (eq, msg) = self.中.相等(&other.中, 浮点容差);
|
||||
if !eq {
|
||||
return (false, format!("分型: [中]缠论K线异常 >> {msg}"));
|
||||
}
|
||||
}
|
||||
match (&self.右, &other.右) {
|
||||
(None, None) => {}
|
||||
(Some(a), Some(b)) => {
|
||||
let (eq, msg) = a.相等(b, 浮点容差);
|
||||
if !eq {
|
||||
return (false, format!("分型: [右]缠论K线异常 >> {msg}"));
|
||||
}
|
||||
}
|
||||
(a, b) => {
|
||||
return (
|
||||
false,
|
||||
format!("分型: [右]空值不一致 A={},B={}", a.is_some(), b.is_some()),
|
||||
);
|
||||
}
|
||||
}
|
||||
if self.结构 != other.结构 {
|
||||
return (
|
||||
false,
|
||||
format!("分型: [结构] 不等 A={},B={}", self.结构, other.结构),
|
||||
);
|
||||
}
|
||||
if self.时间戳 != other.时间戳 {
|
||||
return (
|
||||
false,
|
||||
format!("分型: [时间戳] 不等 A={},B={}", self.时间戳, other.时间戳),
|
||||
);
|
||||
}
|
||||
if (self.分型特征值 - other.分型特征值).abs() > 浮点容差 {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"分型: [分型特征值] 浮点超限 A={:.10},B={:.10}",
|
||||
self.分型特征值, other.分型特征值
|
||||
),
|
||||
);
|
||||
}
|
||||
(true, "分型: 全部字段一致".into())
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::types::fractal::有高低 for 分型 {
|
||||
|
||||
@@ -26,8 +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};
|
||||
|
||||
/// 线段特征 — 特征序列元素,内部是虚线的集合。
|
||||
///
|
||||
@@ -48,7 +50,7 @@ use std::sync::{Arc, RwLock};
|
||||
#[derive(Debug)]
|
||||
pub struct 线段特征 {
|
||||
/// 特征序列元素编号
|
||||
pub 序号: i64,
|
||||
pub 序号: AtomicI64,
|
||||
/// 标识字符串(如 "特征<虚线>")
|
||||
pub 标识: RwLock<String>,
|
||||
/// 所属线段的方向
|
||||
@@ -60,8 +62,8 @@ pub struct 线段特征 {
|
||||
impl Clone for 线段特征 {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
序号: self.序号,
|
||||
标识: RwLock::new(self.标识.read().unwrap().clone()),
|
||||
序号: AtomicI64::new(self.序号.load(Ordering::Relaxed)),
|
||||
标识: RwLock::new(self.标识.read().clone()),
|
||||
线段方向: self.线段方向,
|
||||
基础序列: self.基础序列.clone(),
|
||||
}
|
||||
@@ -72,7 +74,7 @@ impl 线段特征 {
|
||||
/// 新建线段特征(给定标识、基础序列和线段方向)
|
||||
pub fn new(标识: String, 基础序列: Vec<Arc<虚线>>, 线段方向: 相对方向) -> Self {
|
||||
Self {
|
||||
序号: 0,
|
||||
序号: AtomicI64::new(0),
|
||||
标识: RwLock::new(标识),
|
||||
线段方向,
|
||||
基础序列,
|
||||
@@ -81,7 +83,7 @@ impl 线段特征 {
|
||||
|
||||
/// 图表标题 — 返回标识字符串
|
||||
pub fn 图表标题(&self) -> String {
|
||||
self.标识.read().unwrap().clone()
|
||||
self.标识.read().clone()
|
||||
}
|
||||
|
||||
/// 文 — 取特征序列元素中分型特征值最大/最小的文分型
|
||||
@@ -117,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(),
|
||||
)
|
||||
}
|
||||
|
||||
/// 高 — 文和武中分型特征值的较大者
|
||||
@@ -196,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(())
|
||||
@@ -255,7 +241,7 @@ impl 线段特征 {
|
||||
.unwrap();
|
||||
let fake = 虚线::创建笔(
|
||||
Arc::clone(&小号虚线.文),
|
||||
大号虚线.武.read().unwrap().clone(),
|
||||
大号虚线.武.read().clone(),
|
||||
false,
|
||||
);
|
||||
结果.pop();
|
||||
@@ -316,6 +302,56 @@ impl 线段特征 {
|
||||
}
|
||||
结果
|
||||
}
|
||||
|
||||
/// 结构化相等校验 — 逐项递归校验基础序列中的虚线,返回 (是否相等, 差异描述)
|
||||
pub fn 相等(&self, other: &Self, 浮点容差: f64) -> (bool, String) {
|
||||
if self.序号.load(Ordering::Relaxed) != other.序号.load(Ordering::Relaxed) {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"线段特征: [序号] 不等 A={},B={}",
|
||||
self.序号.load(Ordering::Relaxed),
|
||||
other.序号.load(Ordering::Relaxed)
|
||||
),
|
||||
);
|
||||
}
|
||||
if *self.标识.read() != *other.标识.read() {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"线段特征: [标识] 不等 A={},B={}",
|
||||
self.标识.read(),
|
||||
other.标识.read()
|
||||
),
|
||||
);
|
||||
}
|
||||
if self.线段方向 != other.线段方向 {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"线段特征: [线段方向] 不等 A={},B={}",
|
||||
self.线段方向, other.线段方向
|
||||
),
|
||||
);
|
||||
}
|
||||
if self.基础序列.len() != other.基础序列.len() {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"线段特征: [基础序列] 长度不一致 A={},B={}",
|
||||
self.基础序列.len(),
|
||||
other.基础序列.len()
|
||||
),
|
||||
);
|
||||
}
|
||||
for (idx, (a, b)) in self.基础序列.iter().zip(other.基础序列.iter()).enumerate() {
|
||||
let (eq, msg) = a.相等(b, 浮点容差);
|
||||
if !eq {
|
||||
return (false, format!("线段特征: 基础序列[{idx}]虚线异常 >> {msg}"));
|
||||
}
|
||||
}
|
||||
(true, "线段特征: 全部字段一致".into())
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::types::fractal::有高低 for 线段特征 {
|
||||
@@ -330,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.武(),
|
||||
|
||||
@@ -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 相对方向 {
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
use crate::warn;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::warn;
|
||||
|
||||
/// 分型结构 —— 三根K线构成的结构形态
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
||||
@@ -64,6 +64,29 @@ impl 缺口 {
|
||||
|
||||
Some(Self::new(上界.max(下界), 上界.min(下界)))
|
||||
}
|
||||
|
||||
/// 结构化相等校验 — 浮点容差比较高/低,返回 (是否相等, 差异描述)
|
||||
pub fn 相等(&self, other: &Self, 浮点容差: f64) -> (bool, String) {
|
||||
if (self.高 - other.高).abs() > 浮点容差 {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"缺口: [高] 浮点超限 容差={浮点容差:.2e} A={:.10},B={:.10}",
|
||||
self.高, other.高
|
||||
),
|
||||
);
|
||||
}
|
||||
if (self.低 - other.低).abs() > 浮点容差 {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"缺口: [低] 浮点超限 容差={浮点容差:.2e} A={:.10},B={:.10}",
|
||||
self.低, other.低
|
||||
),
|
||||
);
|
||||
}
|
||||
(true, "缺口: 高低价格一致".into())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for 缺口 {
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env python3
|
||||
"""检测 Rust 源码文件头部是否有 MIT 协议,若无则自动注入。
|
||||
|
||||
用法:
|
||||
python3 check_license.py # 检测 chanlun/ 和 chanlun-py/ 下所有 .rs
|
||||
python3 check_license.py --check-only # 仅检测,不修改
|
||||
python3 check_license.py --fix # 检测并修复
|
||||
python3 check_license.py path/to/dir # 指定目录
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def find_repo_root() -> Path:
|
||||
"""从脚本位置向上查找仓库根目录(含 LICENSE 文件的目录)。"""
|
||||
current = Path(__file__).resolve().parent
|
||||
while current != current.parent:
|
||||
if (current / "LICENSE").exists():
|
||||
return current
|
||||
current = current.parent
|
||||
# Fallback: 脚本所在目录的父目录
|
||||
return Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def build_license_header(license_path: Path) -> str:
|
||||
"""读取 LICENSE 文件并格式化为 Rust 块注释头。"""
|
||||
lines = license_path.read_text(encoding="utf-8").rstrip("\n").split("\n")
|
||||
header_lines = ["/*"]
|
||||
for line in lines:
|
||||
if line.strip():
|
||||
header_lines.append(f" * {line}")
|
||||
else:
|
||||
header_lines.append(" *")
|
||||
header_lines.append(" */")
|
||||
header_lines.append("") # 末尾空行分隔
|
||||
return "\n".join(header_lines) + "\n"
|
||||
|
||||
|
||||
def has_license_header(file_path: Path) -> bool:
|
||||
"""检测文件头部是否已包含块注释风格的 MIT License。"""
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
head = f.read(512)
|
||||
except (OSError, UnicodeDecodeError):
|
||||
return True
|
||||
return head.lstrip().startswith("/*") and "MIT License" in head
|
||||
|
||||
|
||||
def strip_old_license_header(text: str) -> str:
|
||||
"""去除文件中已有的 // 风格 license header(重新注入前调用)。"""
|
||||
stripped = text.lstrip("\n")
|
||||
if stripped.startswith("// MIT License"):
|
||||
# 找到 // 注释块结束位置(第一个非 // 非空行)
|
||||
lines = stripped.split("\n")
|
||||
end_idx = 0
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith("//") or line.strip() == "":
|
||||
end_idx = i + 1
|
||||
else:
|
||||
break
|
||||
return "\n".join(lines[end_idx:])
|
||||
return text
|
||||
|
||||
|
||||
def inject_license(file_path: Path, header: str) -> bool:
|
||||
"""将 license header 注入文件头部。返回 True 表示已修改。"""
|
||||
original = file_path.read_text(encoding="utf-8")
|
||||
# 已有块注释风格则跳过
|
||||
if original.lstrip().startswith("/*") and "MIT License" in original[:512]:
|
||||
return False
|
||||
# 去除旧的 // 风格 header(如果存在)
|
||||
cleaned = strip_old_license_header(original)
|
||||
file_path.write_text(header + cleaned, encoding="utf-8")
|
||||
return True
|
||||
|
||||
|
||||
def collect_rs_files(roots: list[Path]) -> list[Path]:
|
||||
"""递归收集所有 .rs 文件,排除 target/ 等构建产物目录。"""
|
||||
exclude_dirs = {"target", ".git", "__pycache__", "dist", "build", ".venv", "venv"}
|
||||
files = []
|
||||
for root in roots:
|
||||
if not root.is_dir():
|
||||
continue
|
||||
for path in root.rglob("*.rs"):
|
||||
if any(excl in path.parts for excl in exclude_dirs):
|
||||
continue
|
||||
files.append(path)
|
||||
return sorted(files)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="检测 Rust 源码 MIT 协议头")
|
||||
parser.add_argument(
|
||||
"paths",
|
||||
nargs="*",
|
||||
help="要检测的目录(默认: chanlun 和 chanlun-py 源码目录)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--check-only",
|
||||
action="store_true",
|
||||
help="仅检测,不修改文件",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fix",
|
||||
action="store_true",
|
||||
help="检测并自动注入缺失的协议头(默认行为)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = find_repo_root()
|
||||
license_path = repo_root / "LICENSE"
|
||||
|
||||
if not license_path.exists():
|
||||
print(f"错误: 未找到 LICENSE 文件 ({license_path})", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
header = build_license_header(license_path)
|
||||
|
||||
# 确定扫描目录
|
||||
if args.paths:
|
||||
roots = [Path(p).resolve() for p in args.paths]
|
||||
else:
|
||||
roots = [
|
||||
repo_root / "chanlun" / "src",
|
||||
repo_root / "chanlun-py" / "src",
|
||||
repo_root / "chanlun" / "tests",
|
||||
repo_root / "chanlun-py" / "tests",
|
||||
]
|
||||
roots = [r for r in roots if r.is_dir()]
|
||||
|
||||
if not roots:
|
||||
print("错误: 未找到任何源码目录", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
rs_files = collect_rs_files(roots)
|
||||
|
||||
if not rs_files:
|
||||
print("未找到 .rs 文件")
|
||||
return 0
|
||||
|
||||
missing = []
|
||||
injected = []
|
||||
|
||||
for f in rs_files:
|
||||
if has_license_header(f):
|
||||
continue
|
||||
missing.append(f)
|
||||
if not args.check_only:
|
||||
if inject_license(f, header):
|
||||
injected.append(f)
|
||||
|
||||
if args.check_only:
|
||||
if missing:
|
||||
print(f"缺失 MIT 协议头: {len(missing)} 个文件")
|
||||
for f in missing:
|
||||
print(f" {f}")
|
||||
return 1
|
||||
else:
|
||||
print(f"全部 {len(rs_files)} 个 .rs 文件均已包含 MIT 协议头")
|
||||
return 0
|
||||
else:
|
||||
if injected:
|
||||
print(f"已注入 MIT 协议头: {len(injected)} 个文件")
|
||||
for f in injected:
|
||||
print(f" {f}")
|
||||
if missing:
|
||||
already = len(missing) - len(injected)
|
||||
if already > 0:
|
||||
print(f"已有协议头: {already} 个文件(无需修改)")
|
||||
total = len(rs_files) - len(missing)
|
||||
print(f"总计: {len(rs_files)} 个 .rs 文件, {total} 个已含协议头")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Executable
+27
@@ -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线已挂指标)。
|
||||
|
||||
**技术栈:** Rust(edition 2024 / 宏 crate 2021)、`syn` 2 + `quote` + `proc-macro2`、`inventory` 0.3、`serde_json`。
|
||||
|
||||
**设计文档:** `docs/superpowers/specs/2026-06-22-signal-registry-framework-design.md`
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
| 文件 | 职责 |
|
||||
|---|---|
|
||||
| `chanlun-signal-macros/Cargo.toml` | proc-macro crate 清单(`proc-macro = true` + syn/quote/proc-macro2) |
|
||||
| `chanlun-signal-macros/src/lib.rs` | `#[signal(name, template)]` 属性宏 |
|
||||
| `chanlun/Cargo.toml` | 新增 `inventory` + path 依赖 `chanlun-signal-macros` |
|
||||
| `chanlun/src/signal/registry.rs` | `SignalFn`/`SignalDescriptor`/`SignalMeta`/`归并`/`SIGNAL_REGISTRY`/查询 API + 探针单测 |
|
||||
| `chanlun/src/signal/mod.rs` | 增 `pub mod registry;` |
|
||||
| `chanlun/tests/test_signal_registry.rs` | 端到端集成测试:`#[signal]` 贴探针函数 → 注册表命中(在 chanlun crate 内,因宏 emit `crate::` 路径) |
|
||||
|
||||
**测试归属说明**:`#[signal]` 宏 emit `crate::signal::registry::SignalDescriptor`,仅在 `chanlun` crate 内解析得了,故**宏的端到端测试放 `chanlun/tests/`,不放宏 crate**(放宏 crate 会循环依赖 chanlun)。宏 crate 自身只验证「能编译」。
|
||||
|
||||
---
|
||||
|
||||
## 任务 0:脚手架——proc-macro crate + 依赖接线
|
||||
|
||||
**文件:**
|
||||
- 创建:`chanlun-signal-macros/Cargo.toml`、`chanlun-signal-macros/src/lib.rs`
|
||||
- 修改:`chanlun/Cargo.toml`
|
||||
|
||||
- [ ] **步骤 1:创建宏 crate 清单**
|
||||
|
||||
创建 `chanlun-signal-macros/Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "chanlun-signal-macros"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
description = "chanlun 信号注册 proc-macro(#[signal])"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = { version = "2", features = ["full"] }
|
||||
quote = "1"
|
||||
proc-macro2 = "1"
|
||||
```
|
||||
|
||||
- [ ] **步骤 2:创建宏 crate 占位实现**
|
||||
|
||||
创建 `chanlun-signal-macros/src/lib.rs`(占位,任务 2 填充真实逻辑):
|
||||
|
||||
```rust
|
||||
//! chanlun 信号注册 proc-macro。
|
||||
//!
|
||||
//! 第三方代码声明:`#[signal]` 注册机制参考 czsc 项目
|
||||
//! (https://github.com/waditu/czsc,Apache License 2.0),已简化适配。
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
|
||||
/// 占位——任务 2 实现真实的 #[signal] 属性宏。
|
||||
#[proc_macro_attribute]
|
||||
pub fn signal(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
item
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **步骤 3:chanlun 接线依赖**
|
||||
|
||||
修改 `chanlun/Cargo.toml` 的 `[dependencies]`,追加两行(放在 `sha2 = "0.10"` 之后):
|
||||
|
||||
```toml
|
||||
inventory = "0.3"
|
||||
chanlun-signal-macros = { path = "../chanlun-signal-macros" }
|
||||
```
|
||||
|
||||
- [ ] **步骤 4:验证两个 crate 都能构建**
|
||||
|
||||
运行:`cd /home/moscow/chanlun.rs/chanlun-signal-macros && cargo build`
|
||||
预期:编译通过(占位宏)。
|
||||
|
||||
运行:`cd /home/moscow/chanlun.rs/chanlun && cargo build`
|
||||
预期:编译通过(新增依赖,尚未使用,unused-dep 不会报错)。
|
||||
|
||||
- [ ] **步骤 5:Commit**
|
||||
|
||||
```bash
|
||||
cd /home/moscow/chanlun.rs
|
||||
git add chanlun-signal-macros chanlun/Cargo.toml
|
||||
git commit -m "feat(signal-registry): 脚手架 — proc-macro crate + inventory 依赖"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 任务 1:registry.rs —— 描述符类型 + 归并 + 查询 API
|
||||
|
||||
**文件:**
|
||||
- 创建:`chanlun/src/signal/registry.rs`
|
||||
- 修改:`chanlun/src/signal/mod.rs`
|
||||
|
||||
- [ ] **步骤 1:mod.rs 注册子模块**
|
||||
|
||||
修改 `chanlun/src/signal/mod.rs`,在 `pub mod signal;`(第 13 行)之后加一行:
|
||||
|
||||
```rust
|
||||
pub mod registry;
|
||||
```
|
||||
|
||||
- [ ] **步骤 2:编写 registry.rs(含 cargo 单测)**
|
||||
|
||||
创建 `chanlun/src/signal/registry.rs`(一字不差):
|
||||
|
||||
```rust
|
||||
//! 信号注册表 —— 编译期收集 `#[signal]` 注册的信号函数,运行时按名查表。
|
||||
//!
|
||||
//! 第三方代码声明:注册机制参考 czsc(https://github.com/waditu/czsc,
|
||||
//! Apache License 2.0),已简化适配(无 category / TaCache)。
|
||||
|
||||
use crate::business::observer::观察者;
|
||||
use crate::signal::Signal;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
/// 信号函数签名 —— 读观察者状态(含 K线已挂指标)+ 参数 → 信号列表。无 TaCache。
|
||||
pub type SignalFn = fn(&观察者, &HashMap<String, Value>) -> Vec<Signal>;
|
||||
|
||||
/// 信号描述符(编译期元数据,由 `#[signal]` 宏生成、`inventory` 收集)。
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct SignalDescriptor {
|
||||
/// 信号函数名,如 "youwukuncheng_中枢第三买卖点_V230602"
|
||||
pub name: &'static str,
|
||||
/// 参数模板,如 "{freq}_D1MO{max_overlap}_中枢第三买卖点V230602"
|
||||
pub template: &'static str,
|
||||
/// 函数指针
|
||||
pub func: SignalFn,
|
||||
}
|
||||
|
||||
inventory::collect!(SignalDescriptor);
|
||||
|
||||
/// 运行时信号元信息。
|
||||
pub struct SignalMeta {
|
||||
pub func: SignalFn,
|
||||
pub template: &'static str,
|
||||
}
|
||||
|
||||
/// 归并描述符为注册表;重名返回 Err(纯函数,便于单测)。
|
||||
fn 归并(
|
||||
descs: impl Iterator<Item = SignalDescriptor>,
|
||||
) -> Result<HashMap<&'static str, SignalMeta>, String> {
|
||||
let mut m: HashMap<&'static str, SignalMeta> = HashMap::new();
|
||||
for d in descs {
|
||||
if m
|
||||
.insert(d.name, SignalMeta { func: d.func, template: d.template })
|
||||
.is_some()
|
||||
{
|
||||
return Err(format!("信号重名:{}", d.name));
|
||||
}
|
||||
}
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
/// 全局注册表视图(由 inventory 归并;重名 panic,fail-fast)。
|
||||
pub static SIGNAL_REGISTRY: LazyLock<HashMap<&'static str, SignalMeta>> = LazyLock::new(|| {
|
||||
归并(inventory::iter::<SignalDescriptor>.into_iter().copied())
|
||||
.unwrap_or_else(|e| panic!("{e}"))
|
||||
});
|
||||
|
||||
/// 按名查信号元信息。
|
||||
pub fn get_signal(name: &str) -> Option<&'static SignalMeta> {
|
||||
SIGNAL_REGISTRY.get(name)
|
||||
}
|
||||
|
||||
/// 按名查参数模板。
|
||||
pub fn get_template(name: &str) -> Option<&'static str> {
|
||||
SIGNAL_REGISTRY.get(name).map(|m| m.template)
|
||||
}
|
||||
|
||||
/// 列出所有已注册信号名(排序)。
|
||||
pub fn list_signal_names() -> Vec<&'static str> {
|
||||
let mut v: Vec<_> = SIGNAL_REGISTRY.keys().copied().collect();
|
||||
v.sort();
|
||||
v
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// 探针信号函数(最小签名实现,仅供测试归并/查表)。
|
||||
fn __probe(_obs: &观察者, _p: &HashMap<String, Value>) -> Vec<Signal> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn 描述符(name: &'static str) -> SignalDescriptor {
|
||||
SignalDescriptor { name, template: "{freq}_D1_probe", func: __probe }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_归并_正常() {
|
||||
let m = 归并([描述符("a_V000001"), 描述符("b_V000001")].into_iter()).unwrap();
|
||||
assert_eq!(m.len(), 2);
|
||||
assert!(m.contains_key("a_V000001"));
|
||||
assert_eq!(m["a_V000001"].template, "{freq}_D1_probe");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_归并_重名_返回Err() {
|
||||
let r = 归并([描述符("dup_V000001"), 描述符("dup_V000001")].into_iter());
|
||||
assert!(r.is_err());
|
||||
assert!(r.unwrap_err().contains("信号重名"));
|
||||
}
|
||||
}
|
||||
|
||||
/// 测试用:通过 inventory 提交一个探针描述符,验证全局注册表能收到。
|
||||
#[cfg(test)]
|
||||
fn __probe_for_inventory(_obs: &观察者, _p: &HashMap<String, Value>) -> Vec<Signal> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
inventory::submit! {
|
||||
SignalDescriptor {
|
||||
name: "__probe_inventory_V000000",
|
||||
template: "{freq}_D1_probe_inventory",
|
||||
func: __probe_for_inventory as SignalFn,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod inventory_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_全局注册表收到inventory探针() {
|
||||
assert!(get_signal("__probe_inventory_V000000").is_some());
|
||||
assert_eq!(
|
||||
get_template("__probe_inventory_V000000"),
|
||||
Some("{freq}_D1_probe_inventory")
|
||||
);
|
||||
assert!(list_signal_names().contains(&"__probe_inventory_V000000"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **步骤 3:运行测试**
|
||||
|
||||
运行:`cd /home/moscow/chanlun.rs/chanlun && cargo test signal::registry`
|
||||
预期:3 个测试全 PASS(`test_归并_正常`、`test_归并_重名_返回Err`、`test_全局注册表收到inventory探针`)。
|
||||
|
||||
> 注:若 `inventory::iter::<SignalDescriptor>.into_iter().copied()` 因 inventory 0.3 API 细节编译报错,改为 `inventory::iter::<SignalDescriptor>().copied()` 或 `inventory::iter::<SignalDescriptor> {}`(参考 `/home/moscow/czsc/crates/czsc-signals/src/registry.rs:136` 的 `inventory::iter::<...>.into_iter().copied().collect()` 写法)。
|
||||
|
||||
- [ ] **步骤 4:Commit**
|
||||
|
||||
```bash
|
||||
cd /home/moscow/chanlun.rs
|
||||
git add chanlun/src/signal/registry.rs chanlun/src/signal/mod.rs
|
||||
git commit -m "feat(signal-registry): registry.rs — 描述符/归并/查询 API + 探针测试"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 任务 2:`#[signal]` 属性宏
|
||||
|
||||
**文件:**
|
||||
- 修改:`chanlun-signal-macros/src/lib.rs`
|
||||
|
||||
- [ ] **步骤 1:实现 #[signal] 宏**
|
||||
|
||||
把 `chanlun-signal-macros/src/lib.rs` 全部内容替换为(一字不差):
|
||||
|
||||
```rust
|
||||
//! chanlun 信号注册 proc-macro。
|
||||
//!
|
||||
//! 第三方代码声明:`#[signal]` 注册机制参考 czsc 项目
|
||||
//! (https://github.com/waditu/czsc,Apache License 2.0),已简化适配
|
||||
//! (无 category / TaCache,签名固定为 fn(&观察者, &HashMap<String, Value>) -> Vec<Signal>)。
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::parse::Parser;
|
||||
use syn::punctuated::Punctuated;
|
||||
use syn::{Expr, ExprLit, ItemFn, Lit, Meta, Token};
|
||||
|
||||
/// `#[signal(name = "foo_V230101", template = "{freq}_D1_foo")]`
|
||||
///
|
||||
/// 校验:函数名含 `_V<数字>`;`name` 与函数名一致;`name`/`template` 非空。
|
||||
/// 生成:一个 `static` SignalDescriptor + `inventory::submit!`,路径用 `crate::signal::registry::`。
|
||||
#[proc_macro_attribute]
|
||||
pub fn signal(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let parser = Punctuated::<Meta, Token![,]>::parse_terminated;
|
||||
let metas = match parser.parse(attr) {
|
||||
Ok(m) => m,
|
||||
Err(e) => return e.to_compile_error().into(),
|
||||
};
|
||||
|
||||
let mut name: Option<String> = None;
|
||||
let mut template: Option<String> = None;
|
||||
for m in metas {
|
||||
if let Meta::NameValue(nv) = m
|
||||
&& let Some(ident) = nv.path.get_ident()
|
||||
&& let Expr::Lit(ExprLit { lit: Lit::Str(v), .. }) = nv.value
|
||||
{
|
||||
match ident.to_string().as_str() {
|
||||
"name" => name = Some(v.value()),
|
||||
"template" => template = Some(v.value()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let f: ItemFn = match syn::parse(item) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return e.to_compile_error().into(),
|
||||
};
|
||||
|
||||
let name = name.unwrap_or_default();
|
||||
let template = template.unwrap_or_default();
|
||||
let fn_ident = &f.sig.ident;
|
||||
let fn_name = fn_ident.to_string();
|
||||
|
||||
let mut errors = Vec::new();
|
||||
if name.is_empty() || template.is_empty() {
|
||||
errors.push(quote! { compile_error!("#[signal] name/template 不能为空"); });
|
||||
}
|
||||
if name != fn_name {
|
||||
errors.push(quote! { compile_error!("#[signal] name 必须与函数名一致"); });
|
||||
}
|
||||
// 函数名须含 _V<数字>
|
||||
let 有版本 = fn_name
|
||||
.rsplit_once("_V")
|
||||
.map(|(_, v)| !v.is_empty() && v.chars().all(|c| c.is_ascii_digit()))
|
||||
.unwrap_or(false);
|
||||
if !有版本 {
|
||||
errors.push(quote! { compile_error!("#[signal] 函数名必须含 _V<版本号>,如 foo_V230101"); });
|
||||
}
|
||||
|
||||
if !errors.is_empty() {
|
||||
let errs = errors.into_iter();
|
||||
return quote! { #(#errs)* }.into();
|
||||
}
|
||||
|
||||
let descriptor_ident = syn::Ident::new(
|
||||
&format!("__SIG_DESC_{}", fn_name).to_uppercase(),
|
||||
fn_ident.span(),
|
||||
);
|
||||
|
||||
let expanded = quote! {
|
||||
#f
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
static #descriptor_ident: crate::signal::registry::SignalDescriptor =
|
||||
crate::signal::registry::SignalDescriptor {
|
||||
name: #name,
|
||||
template: #template,
|
||||
func: #fn_ident as crate::signal::registry::SignalFn,
|
||||
};
|
||||
|
||||
inventory::submit! { #descriptor_ident }
|
||||
};
|
||||
expanded.into()
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **步骤 2:验证宏 crate 编译**
|
||||
|
||||
运行:`cd /home/moscow/chanlun.rs/chanlun-signal-macros && cargo build`
|
||||
预期:编译通过。
|
||||
|
||||
- [ ] **步骤 3:Commit**
|
||||
|
||||
```bash
|
||||
cd /home/moscow/chanlun.rs
|
||||
git add chanlun-signal-macros/src/lib.rs
|
||||
git commit -m "feat(signal-registry): #[signal] 属性宏 — 校验+生成描述符+提交"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 任务 3:端到端集成测试(chanlun 内用 #[signal])
|
||||
|
||||
**文件:**
|
||||
- 创建:`chanlun/tests/test_signal_registry.rs`
|
||||
|
||||
- [ ] **步骤 1:编写集成测试**
|
||||
|
||||
创建 `chanlun/tests/test_signal_registry.rs`(一字不差)。它在 chanlun crate 内用 `#[signal]` 贴一个探针函数,验证宏 + 注册表端到端:
|
||||
|
||||
```rust
|
||||
//! 端到端:#[signal] 宏 + inventory 注册表协同。
|
||||
//! 放在 chanlun crate 内,因 #[signal] emit 的是 `crate::signal::registry::` 路径。
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use chanlun::business::observer::观察者;
|
||||
use chanlun::signal::registry::{get_signal, get_template, list_signal_names};
|
||||
use chanlun::signal::Signal;
|
||||
use chanlun_signal_macros::signal;
|
||||
use serde_json::Value;
|
||||
|
||||
/// 探针信号函数:贴 #[signal] 后应被自动注册。
|
||||
#[signal(
|
||||
name = "test_probe_signal_V230101",
|
||||
template = "{freq}_D1MO{max_overlap}_test_probe_signalV230101"
|
||||
)]
|
||||
fn test_probe_signal_V230101(_obs: &观察者, _params: &HashMap<String, Value>) -> Vec<Signal> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_signal_宏自动注册到全局表() {
|
||||
// get_signal 命中
|
||||
assert!(
|
||||
get_signal("test_probe_signal_V230101").is_some(),
|
||||
"#[signal] 应把探针函数注册进 SIGNAL_REGISTRY"
|
||||
);
|
||||
// 模板正确
|
||||
assert_eq!(
|
||||
get_template("test_probe_signal_V230101"),
|
||||
Some("{freq}_D1MO{max_overlap}_test_probe_signalV230101")
|
||||
);
|
||||
// 列表含它
|
||||
assert!(list_signal_names().contains(&"test_probe_signal_V230101"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_未注册信号返回None() {
|
||||
assert!(get_signal("不存在的信号_V999999").is_none());
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **步骤 2:运行集成测试**
|
||||
|
||||
运行:`cd /home/moscow/chanlun.rs/chanlun && cargo test --test test_signal_registry`
|
||||
预期:2 个测试全 PASS。
|
||||
|
||||
> 注:本测试与 registry.rs 的 `#[cfg(test)]` inventory 探针不冲突——集成测试是独立编译单元,`__probe_inventory_V000000` 仅在 lib 单测时提交,集成测试时只有 `test_probe_signal_V230101`。
|
||||
|
||||
- [ ] **步骤 3:跑全量 signal 测试确认无回归**
|
||||
|
||||
运行:`cd /home/moscow/chanlun.rs/chanlun && cargo test signal`
|
||||
预期:原 23 个原语单测 + registry 3 个 + 集成 2 个,全 PASS。
|
||||
|
||||
- [ ] **步骤 4:Commit**
|
||||
|
||||
```bash
|
||||
cd /home/moscow/chanlun.rs
|
||||
git add chanlun/tests/test_signal_registry.rs
|
||||
git commit -m "test(signal-registry): 端到端——#[signal] 宏自动注册 + 查表"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 自检结论
|
||||
|
||||
- **规格覆盖**:设计 §4 crate 结构 → 任务 0;§5 描述符/注册表/查询 API → 任务 1;§6 `#[signal]` 宏 → 任务 2;§7 测试(归并重名/inventory 探针/宏端到端)→ 任务 1(单测)+ 任务 3(集成);§9 错误处理(编译期 compile_error、启动期重名 panic、运行期 None)→ 任务 2(compile_error)+ 任务 1(归并 Err→panic / get_signal None)。全覆盖。
|
||||
- **类型一致**:`SignalFn`/`SignalDescriptor`/`SignalMeta`/`归并`/`get_signal`/`get_template`/`list_signal_names` 在 registry.rs 定义,任务 2 宏 emit `crate::signal::registry::{SignalDescriptor, SignalFn}`、任务 3 集成测试 import `chanlun::signal::registry::{get_signal, get_template, list_signal_names}`,命名贯穿一致。
|
||||
- **占位符**:任务 0 步骤 2 的占位宏是**有意的脚手架**(任务 2 替换为真实实现),非计划缺陷;其余步骤均含完整可编译代码。
|
||||
- **风险提示**:任务 1 步骤 3 标注了 `inventory::iter` API 细节的 fallback(参考 czsc registry.rs 实际写法)。
|
||||
@@ -0,0 +1,79 @@
|
||||
# 子项目 4 Position.update 状态机迁移到 Rust 实现计划
|
||||
|
||||
> 目标:将 Position.update 状态机(~135 行 Python)从 Python 子类迁移到 Rust 核心。
|
||||
|
||||
**设计文档:** `docs/superpowers/specs/2026-06-23-position-update-state-machine-design.md`
|
||||
|
||||
---
|
||||
|
||||
## 任务 0:扩展 Rust 核心 Position
|
||||
|
||||
**文件:** `chanlun/src/signal/position.rs`
|
||||
|
||||
- [x] 新增类型:`操作记录`、`持仓记录`、`开平配对`、`最近事件`
|
||||
- [x] Position 结构体新增 7 个状态字段(pos, pos_changed, operates, holds, last_event, last_lo_dt, last_so_dt, end_dt)
|
||||
- [x] `新建()` 构造函数适配(状态字段初始化为默认值)
|
||||
- [x] 实现 `push_operate()` 内部辅助方法
|
||||
- [x] 实现 `update(&mut self, dt, price, bid, signals) -> Result<(), 缺键错误>` — 核心状态机
|
||||
- [x] 实现 `pairs() -> Vec<开平配对>` — 开平配对计算
|
||||
- [x] 实现 `dump_config()` / `load_config()` — 序列化辅助
|
||||
- [x] 内部辅助函数:`同一交易日`、`间隔检查`、`允许操作`
|
||||
- [x] Rust 单元测试(28 用例)
|
||||
|
||||
## 任务 1:更新 PyO3 绑定
|
||||
|
||||
**文件:** `chanlun-py/src/signal_py.rs`
|
||||
|
||||
- [x] 新增 helper:`核心op转pyop()`、`时间戳转datetime()`
|
||||
- [x] 新增状态 getter:`pos`, `pos_changed`, `operates`, `holds`, `pairs`
|
||||
- [x] 实现 `update(PyDict)` — 提取 dt/close/bid + 转换 信号字典 + 调用核心
|
||||
- [x] dt 类型兼容:支持 datetime / int / float
|
||||
- [x] dump(with_data) — 支持附带 pairs/holds
|
||||
- [x] load() 静态方法
|
||||
- [x] 新增 `取事件列表` 辅助函数
|
||||
- [x] 更新 `__repr__` 包含 pos
|
||||
|
||||
## 任务 2:更新 Python 子类
|
||||
|
||||
**文件:** `chanlun-py/chanlun/chan_external.py`
|
||||
|
||||
- [x] `__init__` 简化为 `pass`(状态由 Rust 初始化)
|
||||
- [x] 删除 `update()`(Rust 提供)
|
||||
- [x] 删除 `pairs` property(Rust 提供)
|
||||
- [x] `dump()` 委托给 Rust `super().dump(with_data=...)`
|
||||
- [x] `load()` 使用 `cls(...)` 构造(保持子类类型)
|
||||
- [x] 保留 `get_signals_config()`
|
||||
|
||||
## 任务 3:测试
|
||||
|
||||
**文件:**
|
||||
- `chanlun/src/signal/position.rs` — Rust 单元测试(28 用例)
|
||||
- `chanlun-py/tests/test_position_update.py` — Python 集成测试(24 用例)
|
||||
- `chanlun-py/tests/test_signal_primitives.py` — 已有测试更新(4 position 用例)
|
||||
|
||||
- [x] 基础开多/开空/平多/平空
|
||||
- [x] 间隔限制
|
||||
- [x] 止损(多头/空头)
|
||||
- [x] 超时
|
||||
- [x] 时间倒退容错
|
||||
- [x] 空事件列表容错
|
||||
- [x] 无匹配事件容错
|
||||
- [x] 缺键错误
|
||||
- [x] T0 模式
|
||||
- [x] pairs 盈亏计算(多头/空头)
|
||||
- [x] pairs 持仓天数
|
||||
- [x] dump/load with/without data
|
||||
- [x] dt 类型兼容(datetime / int / float)
|
||||
|
||||
## 任务 4:文档
|
||||
|
||||
- [x] 创建设计文档 `docs/superpowers/specs/2026-06-23-position-update-state-machine-design.md`
|
||||
- [x] 创建实现计划 `docs/superpowers/plans/2026-06-23-position-update-state-machine.md`
|
||||
- [x] 更新 `CLAUDE.md` 子项目表
|
||||
|
||||
## 自检结论
|
||||
|
||||
- **规格覆盖**:设计 §3 新增类型 → 任务 0;§4 update 算法 → 任务 0;§5 文件结构 → 任务 0-3
|
||||
- **类型一致**:`update()` 参数使用已有 `信号字典` 类型;`Operate` 枚举已有 Rust 版
|
||||
- **向后兼容**:Python 子类保留;update/pairs/operates/holds API 不变;dt 支持三种输入格式
|
||||
- **测试覆盖**:Rust 28 用例 + Python 24 用例 + 已有 4 用例更新
|
||||
@@ -0,0 +1,678 @@
|
||||
# 信号计算器 Rust 迁移 — 设计决策 + 实现计划
|
||||
|
||||
> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
|
||||
|
||||
**目标:** 将 `信号计算器`(Python 信号编排器)替换为混合架构:Rust `SignalEngine` 为主,Python fallback 为辅,逐步完成最终迁移。
|
||||
|
||||
**架构:** 增强 Rust `SignalEngine` 使其返回完整的 `信号字典`(信号 + OHLCV 行情);创建 `SignalOrchestrator` 支持 Rust 注册表优先 + Python `import_by_name` 回退;`SignalsParser` 暂留 Python。
|
||||
|
||||
**技术栈:** Rust edition 2024、PyO3 0.28、`serde_json::Value`、`parking_lot::RwLock`、`inventory`。
|
||||
|
||||
**设计文档:** `docs/superpowers/specs/2026-06-23-signal-calculator-migration-design.md`
|
||||
|
||||
---
|
||||
|
||||
## 0. 决策分析
|
||||
|
||||
### 现状
|
||||
|
||||
| 组件 | 语言 | 职责 |
|
||||
|------|------|------|
|
||||
| `SignalEngine` | ✅ Rust | 按名查找已注册信号函数 → 执行 → 合并结果 |
|
||||
| `信号计算器` | Python | 同上 + OHLCV 行情提取 + `SignalsParser` 集成 |
|
||||
| `SignalsParser` | Python | 解析信号函数文档字符串 → 生成配置字典 |
|
||||
| `get_signals_config` | Python | 将信号字符串列表 → 配置字典列表(用 `SignalsParser`) |
|
||||
|
||||
两个计算引擎**并行存在**,完全独立。`strategies.py` 使用 Python `信号计算器`。Rust `SignalEngine` 没有被任何生产代码使用。
|
||||
|
||||
### 关键差异
|
||||
|
||||
| 能力 | Python `信号计算器` | Rust `SignalEngine` |
|
||||
|------|---------------------|---------------------|
|
||||
| 信号函数解析 | 运行时 `import_by_name()` | 编译时 `#[signal]` + `inventory` |
|
||||
| OHLCV 行情 | 提取到 `self.行情` | ❌ 不处理 |
|
||||
| 观察者访问 | 预提取 `{freq: Observer}` 字典 | 每次调用时通过 `&立体分析器` 查找 |
|
||||
| 错误处理 | 每个信号函数的 `except Exception` | `tracing::warn!`,继续 |
|
||||
| freq 验证 | 检查是否在分析器周期组中 | ❌ 不验证 |
|
||||
| 信号字符串→配置 | `从信号列表提取配置()` | ❌ 不存在(Python `SignalsParser` 处理) |
|
||||
|
||||
### 建议:混合迁移(3 阶段)
|
||||
|
||||
**阶段 A:增强 Rust SignalEngine。** 添加 OHLCV 行情提取 + freq 验证 + Python `call_signal` 集成。
|
||||
|
||||
**阶段 B:创建混合编排器 `SignalOrchestrator`。** 替代 Python `信号计算器`;Rust 注册表优先,Python `import_by_name` 回退。
|
||||
|
||||
**阶段 C:废弃 Python 并行路径。** 所有信号函数移植到 Rust 后,移除 `import_by_name` 回退和 `SignalsParser`。
|
||||
|
||||
| 阶段 | 交付物 | 向后兼容 |
|
||||
|------|--------|----------|
|
||||
| A | `SignalEngine::更新_完整()` → `{signals, market_data}` | ✅ 不影响现有路径 |
|
||||
| B | `SignalOrchestrator`(Rust 优先 + Python fallback) | ✅ `strategies.py` 切换到新类 |
|
||||
| C | 移除 Python `信号计算器` 和 `SignalsParser` | ⚠️ 需所有信号函数先移植到 Rust |
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
chanlun/src/signal/engine.rs ← 增强:更新_完整() 返回 {signals, market}
|
||||
chanlun-py/src/signal_engine_py.rs ← 增强:SignalEnginePy 暴露 更新_完整()
|
||||
chanlun-py/chanlun/signal_orchestrator.py ← 新建:混合编排器
|
||||
chanlun-py/chanlun/chan_external.py ← 废弃:信号计算器(最终移除)
|
||||
strategies.py ← 切换:使用 SignalOrchestrator
|
||||
main.py ← 修复:损坏的 信号计算器 调用点
|
||||
chanlun-py/tests/test_signal_orchestrator.py ← 新建:编排器测试
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 阶段 A:增强 Rust SignalEngine(信号 + 行情)
|
||||
|
||||
### 任务 A1:SignalEngine 增加 `更新_完整()` 方法
|
||||
|
||||
**文件:** `chanlun/src/signal/engine.rs`
|
||||
|
||||
- [ ] **步骤 1:添加返回类型**
|
||||
|
||||
在 `SignalEngine` 的 `更新_含分数()` 之后添加新结构体:
|
||||
|
||||
```rust
|
||||
/// 完整更新结果:信号字典 + 基础周期行情数据。
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct 完整更新结果 {
|
||||
/// 信号 key → value 映射
|
||||
pub signals: HashMap<String, String>,
|
||||
/// 基础周期最后一根 K 线的 OHLCV 数据
|
||||
pub market: Option<MarketData>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MarketData {
|
||||
pub symbol: String,
|
||||
pub dt: i64, // Unix 秒
|
||||
pub id: i64,
|
||||
pub open: f64,
|
||||
pub high: f64,
|
||||
pub low: f64,
|
||||
pub close: f64,
|
||||
pub vol: f64,
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **步骤 2:实现 `更新_完整()`**
|
||||
|
||||
```rust
|
||||
/// 运行信号计算并附带基础周期行情。
|
||||
/// `base_freq` 为分析器的第一个周期(最小周期)。
|
||||
pub fn 更新_完整(&self, analyzer: &立体分析器) -> 完整更新结果 {
|
||||
let signals = self.更新(analyzer);
|
||||
let base_freq = analyzer.周期组.first().copied().unwrap_or(0);
|
||||
let market = analyzer._单体分析器.get(&base_freq).and_then(|obs| {
|
||||
let obs = obs.read();
|
||||
obs.普通K线序列.last().map(|k| {
|
||||
MarketData {
|
||||
symbol: obs.符号.clone(),
|
||||
dt: k.时间戳,
|
||||
id: k.序号.load(std::sync::atomic::Ordering::Relaxed),
|
||||
open: k.开盘价,
|
||||
high: k.最高价,
|
||||
low: k.最低价,
|
||||
close: k.收盘价,
|
||||
vol: k.成交量,
|
||||
}
|
||||
})
|
||||
});
|
||||
完整更新结果 { signals, market }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **步骤 3:构建验证**
|
||||
|
||||
```bash
|
||||
cd chanlun && cargo build
|
||||
```
|
||||
预期:编译通过。
|
||||
|
||||
- [ ] **步骤 4:Commit**
|
||||
|
||||
```bash
|
||||
git add chanlun/src/signal/engine.rs
|
||||
git commit -m "feat(signal): SignalEngine.更新_完整() — 信号 + 基础周期行情
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 任务 A2:PyO3 绑定增强
|
||||
|
||||
**文件:** `chanlun-py/src/signal_engine_py.rs`
|
||||
|
||||
- [ ] **步骤 1:暴露 `更新_完整()`**
|
||||
|
||||
在 `SignalEnginePy` 的 `#[pymethods]` 块中添加:
|
||||
|
||||
```rust
|
||||
/// 更新信号并返回完整结果(信号 + 行情)。
|
||||
/// 返回 dict: {"signals": {...}, "market": {...}}
|
||||
fn 更新_完整<'py>(&self, py: Python<'py>, analyzer: &立体分析器Py) -> PyResult<Bound<'py, PyDict>> {
|
||||
let result = self.inner.更新_完整(&analyzer.inner);
|
||||
let d = PyDict::new(py);
|
||||
// signals
|
||||
let signals_dict = PyDict::new(py);
|
||||
for (k, v) in &result.signals {
|
||||
signals_dict.set_item(k, v)?;
|
||||
}
|
||||
d.set_item("signals", signals_dict)?;
|
||||
// market
|
||||
if let Some(m) = &result.market {
|
||||
let md = PyDict::new(py);
|
||||
md.set_item("symbol", &m.symbol)?;
|
||||
// Convert i64 to Python datetime
|
||||
let dt = 时间戳转datetime(py, m.dt)?;
|
||||
md.set_item("dt", dt)?;
|
||||
md.set_item("id", m.id)?;
|
||||
md.set_item("open", m.open)?;
|
||||
md.set_item("high", m.high)?;
|
||||
md.set_item("low", m.low)?;
|
||||
md.set_item("close", m.close)?;
|
||||
md.set_item("vol", m.vol)?;
|
||||
d.set_item("market", md)?;
|
||||
} else {
|
||||
d.set_item("market", py.None())?;
|
||||
}
|
||||
Ok(d)
|
||||
}
|
||||
```
|
||||
|
||||
> 注意:`时间戳转datetime` 已在 `signal_py.rs` 中定义。需要将其改为 `pub(crate)` 可见性,或在 `signal_engine_py.rs` 中重复定义。
|
||||
|
||||
- [ ] **步骤 2:将 `时间戳转datetime` 改为 `pub(crate)`**
|
||||
|
||||
在 `signal_py.rs` 中:
|
||||
```rust
|
||||
// 将 fn 改为 pub(crate)
|
||||
pub(crate) fn 时间戳转datetime(py: Python<'_>, ts: i64) -> PyResult<Py<PyAny>> {
|
||||
```
|
||||
|
||||
- [ ] **步骤 3:添加 `freq 验证` 辅助函数**
|
||||
|
||||
在 `signal_engine_py.rs` 的 `SignalEnginePy::new()` 中添加 freq 验证(匹配 Python `信号计算器` setter 的行为):
|
||||
|
||||
```rust
|
||||
// 在 new() 中,转换配置后:
|
||||
// 验证所有 freq 已由调用方提供(不在构造时验证——没有分析器引用)
|
||||
// 频率验证推迟到 更新() 调用时(与 Rust 核心行为一致)
|
||||
```
|
||||
|
||||
不改变构造函数——保持最小侵入。频率验证由调用方负责(`SignalOrchestrator`)。
|
||||
|
||||
- [ ] **步骤 4:构建验证**
|
||||
|
||||
```bash
|
||||
cd chanlun-py && cargo build
|
||||
```
|
||||
预期:编译通过。
|
||||
|
||||
- [ ] **步骤 5:Commit**
|
||||
|
||||
```bash
|
||||
git add chanlun-py/src/signal_engine_py.rs chanlun-py/src/signal_py.rs
|
||||
git commit -m "feat(signal-py): SignalEnginePy.更新_完整() + 时间戳转datetime 公开
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 阶段 B:混合编排器 SignalOrchestrator
|
||||
|
||||
### 任务 B1:创建 `signal_orchestrator.py`
|
||||
|
||||
**文件:** 创建 `chanlun-py/chanlun/signal_orchestrator.py`
|
||||
|
||||
这是核心新文件。编排器:
|
||||
1. 构造时接受 `立体分析器` + 信号配置 + 信号模块
|
||||
2. 对每个配置,先尝试 Rust `call_signal()` 查找(通过 `list_signals()`)
|
||||
3. 如果信号名在 Rust 注册表中:使用 `SignalEngine` 批量执行
|
||||
4. 如果不在:使用 Python `import_by_name` 回退
|
||||
5. 合并所有结果,附加 OHLCV 行情
|
||||
|
||||
- [ ] **步骤 1:创建文件框架**
|
||||
|
||||
```python
|
||||
"""信号编排器 — Rust 优先 + Python 回退的混合信号计算。
|
||||
|
||||
替代 chan_external.信号计算器,逐步迁移到全 Rust 路径。
|
||||
|
||||
使用方式::
|
||||
|
||||
分析器 = 立体分析器("btcusd", [300, 900, 3600], 配置)
|
||||
编排器 = SignalOrchestrator(分析器, 信号配置=[...], 信号模块="chanlun.signals")
|
||||
|
||||
for k in k线列表:
|
||||
分析器.投喂K线(k)
|
||||
编排器.更新()
|
||||
print(编排器.信号字典)
|
||||
"""
|
||||
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from chanlun.chan import 观察者, 立体分析器
|
||||
from chanlun._chanlun import (
|
||||
SignalEngine as _RustSignalEngine,
|
||||
call_signal as _rust_call_signal,
|
||||
list_signals as _rust_list_signals,
|
||||
)
|
||||
|
||||
|
||||
class SignalOrchestrator:
|
||||
"""混合信号编排器:Rust 注册表优先,Python import_by_name 回退。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
分析器: 立体分析器,
|
||||
信号配置: Optional[List[Dict]] = None,
|
||||
信号模块: str = "chanlun.signals",
|
||||
):
|
||||
self._分析器 = 分析器
|
||||
self._观察者字典 = {p: 分析器._单体分析器[p] for p in 分析器.周期组}
|
||||
self._基础周期 = 分析器.周期组[0]
|
||||
self._信号模块 = 信号模块
|
||||
|
||||
# 初始化 Rust 引擎(用于已注册的 Rust 信号)
|
||||
self._rust_engine = _RustSignalEngine(信号配置=信号配置 or [])
|
||||
self._rust_engine.自动挂载指标(分析器)
|
||||
|
||||
# 分类配置:Rust 注册 vs Python 回退
|
||||
self._rust_configs: List[Dict] = []
|
||||
self._python_configs: List[Dict] = []
|
||||
self._python_func_cache: Dict[str, Callable] = {}
|
||||
|
||||
# 结果容器
|
||||
self.信号: Dict[str, str] = {}
|
||||
self.行情: Dict[str, Any] = {}
|
||||
|
||||
# 初始设置
|
||||
self.信号配置 = 信号配置 or []
|
||||
|
||||
# ... 其余方法见下面步骤
|
||||
```
|
||||
|
||||
- [ ] **步骤 2:实现配置分类**
|
||||
|
||||
```python
|
||||
@property
|
||||
def 信号配置(self) -> List[Dict]:
|
||||
return self._信号配置
|
||||
|
||||
@信号配置.setter
|
||||
def 信号配置(self, value: List[Dict]):
|
||||
可用周期 = set(self._分析器.周期组)
|
||||
rust_names = set(_rust_list_signals())
|
||||
|
||||
self._rust_configs = []
|
||||
self._python_configs = []
|
||||
|
||||
for c in self._去重配置(value):
|
||||
freq = c.get("freq")
|
||||
if freq is not None:
|
||||
周期秒 = int(freq)
|
||||
if 周期秒 not in 可用周期:
|
||||
raise ValueError(
|
||||
f"信号配置 freq={freq}({周期秒}s) 不在分析器周期组 {sorted(可用周期)} 中"
|
||||
)
|
||||
|
||||
name = c.get("name", "")
|
||||
if name in rust_names:
|
||||
self._rust_configs.append(c)
|
||||
else:
|
||||
self._python_configs.append(c)
|
||||
|
||||
self._信号配置 = value
|
||||
self._预加载Python信号函数()
|
||||
```
|
||||
|
||||
- [ ] **步骤 3:实现更新循环**
|
||||
|
||||
```python
|
||||
def 更新(self):
|
||||
"""执行所有信号计算。Rust 优先(批量),Python 回退(逐个)。"""
|
||||
self.信号.clear()
|
||||
self.行情.clear()
|
||||
|
||||
# 1. Rust 批量执行
|
||||
if self._rust_configs:
|
||||
result = self._rust_engine.更新_完整(self._分析器)
|
||||
if result.get("signals"):
|
||||
for k, v in result["signals"].items():
|
||||
if v != "任意_任意_任意_0":
|
||||
self.信号[k] = v
|
||||
if result.get("market"):
|
||||
self.行情.update(result["market"])
|
||||
|
||||
# 2. Python 回退(逐个执行)
|
||||
for config in self._python_configs:
|
||||
try:
|
||||
result = self._执行Python信号函数(config)
|
||||
if result:
|
||||
for k, v in result.items():
|
||||
if v != "任意_任意_任意_0":
|
||||
self.信号[k] = v
|
||||
except Exception:
|
||||
logger.exception(f"Python 信号函数执行失败: {config.get('name')}")
|
||||
|
||||
# 3. 补充基础周期行情(如果 Rust 引擎未提供)
|
||||
if not self.行情:
|
||||
self._提取行情()
|
||||
```
|
||||
|
||||
- [ ] **步骤 4:实现 Python 信号函数执行(移植自 chan_external.py)**
|
||||
|
||||
```python
|
||||
def _执行Python信号函数(self, config: Dict) -> Optional[OrderedDict]:
|
||||
"""执行单个 Python 信号函数(移植自 信号计算器._执行信号函数)。"""
|
||||
import traceback
|
||||
param = dict(config)
|
||||
sig_name = param.pop("name")
|
||||
sig_func = self._python_func_cache.get(sig_name) or self._解析信号函数(sig_name)
|
||||
if sig_func is None:
|
||||
logger.warning(f"信号函数未找到: {sig_name}")
|
||||
return None
|
||||
|
||||
freq = param.pop("freq", None)
|
||||
if freq is not None:
|
||||
周期秒 = int(freq)
|
||||
obs = self._观察者字典.get(周期秒)
|
||||
if obs is None:
|
||||
logger.warning(f"未找到周期 {freq} 的观察者")
|
||||
return None
|
||||
try:
|
||||
return sig_func(obs, **param)
|
||||
except Exception:
|
||||
logger.exception(f"信号函数执行异常: {sig_name}")
|
||||
return None
|
||||
else:
|
||||
try:
|
||||
return sig_func(self, **param)
|
||||
except Exception:
|
||||
logger.exception(f"信号函数执行异常: {sig_name}")
|
||||
return None
|
||||
```
|
||||
|
||||
- [ ] **步骤 5:移植辅助方法**
|
||||
|
||||
```python
|
||||
def _去重配置(self, configs: List[Dict]) -> List[Dict]:
|
||||
seen = set()
|
||||
unique = []
|
||||
for c in configs:
|
||||
key = (c.get("name"), frozenset(
|
||||
(k, str(v)) for k, v in c.items() if k != "name"
|
||||
))
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
unique.append(c)
|
||||
return unique
|
||||
|
||||
def _预加载Python信号函数(self):
|
||||
for config in self._python_configs:
|
||||
name = config.get("name", "")
|
||||
if name and name not in self._python_func_cache:
|
||||
self._python_func_cache[name] = None # placeholder
|
||||
for name in list(self._python_func_cache.keys()):
|
||||
try:
|
||||
self._python_func_cache[name] = self._解析信号函数(name)
|
||||
except Exception:
|
||||
logger.warning(f"预加载信号函数失败: {name}")
|
||||
|
||||
@staticmethod
|
||||
def _解析信号函数(name: str) -> Optional[Callable]:
|
||||
"""动态导入信号函数(移植自 信号计算器._解析信号函数)。"""
|
||||
import os
|
||||
if "." not in name:
|
||||
return __import__(name)
|
||||
|
||||
module_name, func_name = name.rsplit(".", 1)
|
||||
# 检查 __main__ 缓存
|
||||
main_mod = sys.modules.get("__main__")
|
||||
if main_mod is not None and hasattr(main_mod, func_name):
|
||||
return getattr(main_mod, func_name)
|
||||
|
||||
module = __import__(module_name, fromlist=[func_name])
|
||||
return getattr(module, func_name)
|
||||
|
||||
def _提取行情(self):
|
||||
"""从基础周期观察者提取 OHLCV 行情(Python 回退路径)。"""
|
||||
obs = self._观察者字典.get(self._基础周期)
|
||||
if obs is None:
|
||||
return
|
||||
klines = obs.普通K线序列
|
||||
if not klines:
|
||||
return
|
||||
k = klines[-1]
|
||||
self.行情 = {
|
||||
"symbol": obs.符号,
|
||||
"dt": k.时间戳, # 需要从 i64 转 datetime
|
||||
"id": k.序号,
|
||||
"open": k.开盘价,
|
||||
"high": k.最高价,
|
||||
"low": k.最低价,
|
||||
"close": k.收盘价,
|
||||
"vol": k.成交量,
|
||||
}
|
||||
|
||||
@property
|
||||
def 信号字典(self) -> dict:
|
||||
"""合并信号 + 行情(与 Position.update() 兼容)。"""
|
||||
return {**self.信号, **self.行情}
|
||||
|
||||
def 获取周期观察者(self, freq: str) -> Optional[观察者]:
|
||||
"""按频率获取观察者。"""
|
||||
return self._观察者字典.get(int(freq))
|
||||
|
||||
def 从信号列表提取配置(self, 信号序列: List[str]):
|
||||
"""从信号字符串列表解析配置(委托给 SignalsParser)。"""
|
||||
from chanlun.chan_external import get_signals_config
|
||||
from chanlun.chan_external import SignalsParser
|
||||
|
||||
if not 信号序列:
|
||||
return
|
||||
sp = SignalsParser(signals_module=self._信号模块)
|
||||
conf = sp.parse(信号序列)
|
||||
self.信号配置 = conf
|
||||
```
|
||||
|
||||
- [ ] **步骤 6:Commit**
|
||||
|
||||
```bash
|
||||
git add chanlun-py/chanlun/signal_orchestrator.py
|
||||
git commit -m "feat(signal): SignalOrchestrator — Rust 优先 + Python 回退混合编排器
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 任务 B2:切换到 strategies.py
|
||||
|
||||
**文件:** `strategies.py`
|
||||
|
||||
- [ ] **步骤 1:更新导入**
|
||||
|
||||
将第 28 行的导入从:
|
||||
```python
|
||||
from chanlun.chan_external import 信号计算器 as _信号计算器, get_signals_config
|
||||
```
|
||||
改为:
|
||||
```python
|
||||
from chanlun.chan_external import get_signals_config
|
||||
from chanlun.signal_orchestrator import SignalOrchestrator as _信号计算器
|
||||
```
|
||||
|
||||
> 使用别名 `_信号计算器` 保持类名不变——策略内部代码零改动。
|
||||
|
||||
- [ ] **步骤 2:运行策略验证测试**
|
||||
|
||||
```bash
|
||||
python test_策略验证.py
|
||||
```
|
||||
预期:所有 V1-V7 测试通过,无回归。
|
||||
|
||||
- [ ] **步骤 3:Commit**
|
||||
|
||||
```bash
|
||||
git add strategies.py
|
||||
git commit -m "refactor(strategies): 切换到 SignalOrchestrator 混合编排器
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 任务 B3:修复 main.py 中损坏的调用点
|
||||
|
||||
**文件:** `main.py:2220`
|
||||
|
||||
- [ ] **步骤 1:修复构造函数调用**
|
||||
|
||||
当前损坏的代码:
|
||||
```python
|
||||
计算器 = cet.信号计算器(观察者字典, 基础周期=周期组[0], 信号模块="chanlun.signals")
|
||||
计算器.从信号序列设置配置([...]) # 方法不存在
|
||||
```
|
||||
|
||||
修复为:
|
||||
```python
|
||||
计算器 = cet.SignalOrchestrator(分析器, 信号模块="chanlun.signals")
|
||||
计算器.从信号列表提取配置([...])
|
||||
```
|
||||
|
||||
> 注意:此处 `分析器` 变量需要在该作用域内可用。需要先检查 main.py 上下文。
|
||||
|
||||
- [ ] **步骤 2:Commit**
|
||||
|
||||
```bash
|
||||
git add main.py
|
||||
git commit -m "fix(main): 修复损坏的 信号计算器 调用点 → SignalOrchestrator
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 阶段 C:测试
|
||||
|
||||
### 任务 C1:编排器单元测试
|
||||
|
||||
**文件:** 创建 `chanlun-py/tests/test_signal_orchestrator.py`
|
||||
|
||||
- [ ] **步骤 1:编写框架测试**
|
||||
|
||||
```python
|
||||
"""SignalOrchestrator 集成测试 — 混合 Rust + Python 信号执行。"""
|
||||
import pytest
|
||||
from datetime import datetime, timezone
|
||||
from chanlun.signal_orchestrator import SignalOrchestrator
|
||||
|
||||
|
||||
def test_构造_空配置():
|
||||
"""空配置构造不崩溃。"""
|
||||
from chanlun import 立体分析器, 缠论配置
|
||||
analyzer = 立体分析器("test", [300, 900], 缠论配置())
|
||||
orch = SignalOrchestrator(analyzer)
|
||||
assert orch.信号字典 == {}
|
||||
assert orch._rust_configs == []
|
||||
assert orch._python_configs == []
|
||||
|
||||
|
||||
def test_Rust信号已注册():
|
||||
"""youwukuncheng 信号名在 Rust 注册表中(应分类到 rust_configs)。"""
|
||||
from chanlun import 立体分析器, 缠论配置
|
||||
analyzer = 立体分析器("test", [86400], 缠论配置())
|
||||
config = [{
|
||||
"name": "youwukuncheng_中枢第三买卖点_V230602",
|
||||
"freq": 86400,
|
||||
"max_overlap": 3,
|
||||
"本级完整性": "实",
|
||||
"同级完整性": "合",
|
||||
}]
|
||||
orch = SignalOrchestrator(analyzer, 信号配置=config)
|
||||
assert len(orch._rust_configs) == 1
|
||||
assert len(orch._python_configs) == 0
|
||||
|
||||
|
||||
def test_Python信号回退():
|
||||
"""未知信号名分类到 python_configs。"""
|
||||
from chanlun import 立体分析器, 缠论配置
|
||||
analyzer = 立体分析器("test", [300], 缠论配置())
|
||||
config = [{
|
||||
"name": "chanlun.signals.demo.tas_ma_base_V230313",
|
||||
"freq": 300,
|
||||
"ma_type": "SMA",
|
||||
"timeperiod": 5,
|
||||
}]
|
||||
orch = SignalOrchestrator(analyzer, 信号配置=config)
|
||||
assert len(orch._rust_configs) == 0
|
||||
assert len(orch._python_configs) == 1
|
||||
|
||||
|
||||
def test_freq验证_不在周期组():
|
||||
"""freq 不在分析器周期组中时抛出 ValueError。"""
|
||||
from chanlun import 立体分析器, 缠论配置
|
||||
analyzer = 立体分析器("test", [300], 缠论配置())
|
||||
with pytest.raises(ValueError, match="不在分析器周期组"):
|
||||
SignalOrchestrator(analyzer, 信号配置=[{
|
||||
"name": "some_signal",
|
||||
"freq": 99999,
|
||||
}])
|
||||
```
|
||||
|
||||
- [ ] **步骤 2:运行测试**
|
||||
|
||||
```bash
|
||||
python -m pytest chanlun-py/tests/test_signal_orchestrator.py -v
|
||||
```
|
||||
预期:全部通过。
|
||||
|
||||
- [ ] **步骤 3:Commit**
|
||||
|
||||
```bash
|
||||
git add chanlun-py/tests/test_signal_orchestrator.py
|
||||
git commit -m "test(signal): SignalOrchestrator 单元测试
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 任务 C2:端到端回归测试
|
||||
|
||||
- [ ] **步骤 1:运行所有 tests**
|
||||
|
||||
```bash
|
||||
cd chanlun && cargo test
|
||||
cd chanlun-py && cargo test
|
||||
python -m pytest chanlun-py/tests/ -v
|
||||
python test_策略验证.py
|
||||
```
|
||||
|
||||
- [ ] **步骤 2:验证零回归**
|
||||
|
||||
预期:所有已有测试通过。新编排器测试通过。
|
||||
|
||||
---
|
||||
|
||||
## 自检结论
|
||||
|
||||
- **规格覆盖**:阶段 A 覆盖 SignalEngine 增强 → 完整信号字典;阶段 B 覆盖混合编排器 → 替代 Python `信号计算器`;阶段 C 覆盖测试 → 零回归
|
||||
- **类型一致**:`完整更新结果` 的 `MarketData` 字段与 Python `self.行情` 键名一致
|
||||
- **风险提示**:
|
||||
1. `main.py:2220` 调用点需要确认其所在函数的上下文(分析器变量是否在作用域内)
|
||||
2. `SignalOrchestrator` 的 `_提取行情()` 中 `k.时间戳` 是 i64,需用 `datetime.fromtimestamp` 转换
|
||||
3. Python 信号函数需要 `chanlun.signals` 可导入——需确认安装包含 signals 子包
|
||||
- **向后兼容**:`strategies.py` 使用别名导入——内部代码零改动
|
||||
@@ -0,0 +1,503 @@
|
||||
# 子项目2 信号函数 API + 移植 youwukuncheng 实现计划
|
||||
|
||||
> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
|
||||
|
||||
**目标:** 建立 Rust 信号函数编写规范(便捷 API + 参数提取 + 确保指标),移植第一个真实信号 `youwukuncheng_中枢第三买卖点_V230602`,并通过集成测试与 Python 版对比验证。
|
||||
|
||||
**架构:** 便捷方法直接加到 `K线`/`缠论K线`/`观察者` 上(不引入额外 trait);信号函数放 `chanlun/src/signal/functions/`;参数提取独立为 `signal/params.rs`。
|
||||
|
||||
**技术栈:** Rust edition 2024、`serde_json::Value`、`parking_lot::RwLock`、`inventory`。
|
||||
|
||||
**设计文档:** `docs/superpowers/specs/2026-06-23-signal-fn-api-and-port-design.md`
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
| 文件 | 职责 |
|
||||
|---|---|
|
||||
| `chanlun/src/kline/bar.rs` | 给 `K线` 加便捷指标访问方法 (`macd()`, `rsi()`, `kdj()`, `boll()`, `ma()`) |
|
||||
| `chanlun/src/kline/chan_kline.rs` | 给 `缠论K线` 加转发便捷方法 |
|
||||
| `chanlun/src/business/observer.rs` | 加 `普K偏移()`、`缠K偏移()`、`最后缠K序列()`、`确保指标已计算()` |
|
||||
| `chanlun/src/signal/params.rs` | **新建** — 参数提取辅助函数 (`get_string`, `get_int`, `get_f64`) |
|
||||
| `chanlun/src/signal/mod.rs` | 增 `pub mod params;` + `pub mod functions;` |
|
||||
| `chanlun/src/signal/functions/mod.rs` | **新建** — `pub mod youwukuncheng;` |
|
||||
| `chanlun/src/signal/functions/youwukuncheng.rs` | **新建** — 移植的中枢第三买卖点信号 |
|
||||
| `chanlun/tests/test_signal_youwukuncheng.rs` | **新建** — 集成测试(Rust vs Python 对比) |
|
||||
|
||||
---
|
||||
|
||||
## 任务 0:便捷 API — K线指标访问 + 观察者方法 + 参数提取
|
||||
|
||||
**文件:**
|
||||
- 修改:`chanlun/src/kline/bar.rs`
|
||||
- 修改:`chanlun/src/kline/chan_kline.rs`
|
||||
- 修改:`chanlun/src/business/observer.rs`
|
||||
- 创建:`chanlun/src/signal/params.rs`
|
||||
- 修改:`chanlun/src/signal/mod.rs`
|
||||
|
||||
### 步骤 1:K线 便捷指标访问方法
|
||||
|
||||
在 `chanlun/src/kline/bar.rs` 的 `impl K线` 块中添加以下方法。
|
||||
|
||||
`K线` 已有 `pub 指标: RwLock<指标容器>` 字段,以及 `pub 收盘价: f64` 等 OHLC 字段。新增方法封装 `self.指标.read()` 的 boilerplate:
|
||||
|
||||
```rust
|
||||
/// 便捷读取 MACD 指标。若未计算则返回 None。
|
||||
pub fn macd(&self) -> Option<&平滑异同移动平均线> {
|
||||
// 注意:返回的引用受 RwLockReadGuard 生命周期约束
|
||||
// 需要 unsafe 或者改用 cloned 版本
|
||||
// 实际采用:提供返回 Option<平滑异同移动平均线> 的 cloned 版本
|
||||
// 同时提供一个需要传入 guard 的零拷贝版本
|
||||
}
|
||||
|
||||
// 实际实现方案:提供 _cloned 便捷方法(开销可忽略,MACD 仅几个 f64)
|
||||
pub fn macd(&self) -> Option<平滑异同移动平均线> {
|
||||
self.指标.read().macd_cloned()
|
||||
}
|
||||
pub fn rsi(&self) -> Option<相对强弱指数> {
|
||||
self.指标.read().rsi_cloned()
|
||||
}
|
||||
pub fn kdj(&self) -> Option<随机指标> {
|
||||
self.指标.read().kdj_cloned()
|
||||
}
|
||||
pub fn boll(&self) -> Option<布林带> {
|
||||
self.指标.read().boll_cloned()
|
||||
}
|
||||
pub fn ma(&self, key: &str) -> Option<f64> {
|
||||
self.指标.read().均线().and_then(|m| m.get(key).copied())
|
||||
}
|
||||
```
|
||||
|
||||
> **设计理由**:使用 `_cloned` 版本而非返回引用,避免 `RwLockReadGuard` 生命周期传染到调用方。MACD/RSI/KDJ/BOLL 结构体只含少量 f64 和 Option<f64>,clone 开销可忽略。
|
||||
|
||||
### 步骤 2:缠论K线 便捷转发方法
|
||||
|
||||
在 `chanlun/src/kline/chan_kline.rs` 的 `impl 缠论K线` 块中添加转发方法。缠K 有 `pub 标的K线: RwLock<Arc<K线>>` 字段:
|
||||
|
||||
```rust
|
||||
/// 便捷读取 MACD(委托给标的K线)
|
||||
pub fn macd(&self) -> Option<平滑异同移动平均线> {
|
||||
self.标的K线.read().macd()
|
||||
}
|
||||
pub fn rsi(&self) -> Option<相对强弱指数> {
|
||||
self.标的K线.read().rsi()
|
||||
}
|
||||
pub fn kdj(&self) -> Option<随机指标> {
|
||||
self.标的K线.read().kdj()
|
||||
}
|
||||
pub fn boll(&self) -> Option<布林带> {
|
||||
self.标的K线.read().boll()
|
||||
}
|
||||
pub fn ma(&self, key: &str) -> Option<f64> {
|
||||
self.标的K线.read().ma(key)
|
||||
}
|
||||
/// 读取收盘价(委托给标的K线)
|
||||
pub fn 收盘价(&self) -> f64 {
|
||||
self.标的K线.read().收盘价
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 3:观察者便捷访问方法
|
||||
|
||||
在 `chanlun/src/business/observer.rs` 的 `impl 观察者` 块中添加:
|
||||
|
||||
```rust
|
||||
/// 按偏移取普K,di=1 为最后一根,di=2 为倒数第二根
|
||||
pub fn 普K偏移(&self, di: usize) -> Option<&Arc<K线>> {
|
||||
if di == 0 || di > self.普通K线序列.len() { return None; }
|
||||
Some(&self.普通K线序列[self.普通K线序列.len() - di])
|
||||
}
|
||||
|
||||
/// 按偏移取缠K,di=1 为最后一根
|
||||
pub fn 缠K偏移(&self, di: usize) -> Option<&Arc<缠论K线>> {
|
||||
if di == 0 || di > self.缠论K线序列.len() { return None; }
|
||||
Some(&self.缠论K线序列[self.缠论K线序列.len() - di])
|
||||
}
|
||||
|
||||
/// 最后 N 根缠K(返回切片引用)
|
||||
pub fn 最后缠K序列(&self, n: usize) -> &[Arc<缠论K线>] {
|
||||
let len = self.缠论K线序列.len();
|
||||
if n >= len { &self.缠论K线序列[..] }
|
||||
else { &self.缠论K线序列[len - n..] }
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 4:参数提取模块
|
||||
|
||||
创建 `chanlun/src/signal/params.rs`:
|
||||
|
||||
```rust
|
||||
//! 信号函数参数提取辅助 — 从 `HashMap<String, Value>` 中提取类型化参数。
|
||||
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// 提取字符串参数,缺失或类型不对时返回默认值。
|
||||
pub fn get_string(params: &HashMap<String, Value>, key: &str, default: &str) -> String {
|
||||
params.get(key)
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| default.to_string())
|
||||
}
|
||||
|
||||
/// 提取 i64 参数。
|
||||
pub fn get_int(params: &HashMap<String, Value>, key: &str, default: i64) -> i64 {
|
||||
params.get(key)
|
||||
.and_then(|v| v.as_i64())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
/// 提取 f64 参数。
|
||||
pub fn get_f64(params: &HashMap<String, Value>, key: &str, default: f64) -> f64 {
|
||||
params.get(key)
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
/// 提取字符串引用(零拷贝),缺失时返回默认值。
|
||||
pub fn get_str<'a>(params: &'a HashMap<String, Value>, key: &str, default: &'a str) -> &'a str {
|
||||
params.get(key)
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
```
|
||||
|
||||
修改 `chanlun/src/signal/mod.rs`,在 `pub mod registry;` 后追加:
|
||||
```rust
|
||||
pub mod params;
|
||||
pub mod functions;
|
||||
```
|
||||
|
||||
### 步骤 5:构建验证
|
||||
|
||||
```bash
|
||||
cd chanlun && cargo build
|
||||
```
|
||||
预期:编译通过。
|
||||
|
||||
### 步骤 6:Commit
|
||||
|
||||
```bash
|
||||
git add chanlun/src/kline/bar.rs chanlun/src/kline/chan_kline.rs \
|
||||
chanlun/src/business/observer.rs chanlun/src/signal/params.rs \
|
||||
chanlun/src/signal/mod.rs
|
||||
git commit -m "feat(signal): 便捷API — K线指标访问 + 观察者偏移 + 参数提取"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 任务 1:确保指标 API
|
||||
|
||||
**文件:**
|
||||
- 修改:`chanlun/src/business/observer.rs`
|
||||
|
||||
### 步骤 1:添加 `确保指标已计算` 方法
|
||||
|
||||
在 `观察者` 的 `impl` 块中添加(需要 `use crate::indicators::calculator::指标计算器;`):
|
||||
|
||||
```rust
|
||||
/// 确保所有 K 线上的指标已计算(幂等)。
|
||||
/// 在信号函数入口调用,保证后续 macd()/rsi() 等访问不返回 None。
|
||||
pub fn 确保指标已计算(&self) {
|
||||
if self.配置.计算指标 && !self.普通K线序列.is_empty() {
|
||||
指标计算器::计算并挂载(&self.普通K线序列, &self.配置);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 2:构建验证
|
||||
|
||||
```bash
|
||||
cd chanlun && cargo build
|
||||
```
|
||||
|
||||
### 步骤 3:Commit
|
||||
|
||||
```bash
|
||||
git add chanlun/src/business/observer.rs
|
||||
git commit -m "feat(signal): 观察者.确保指标已计算() — 信号函数入口幂等调用"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 任务 2:移植 youwukuncheng 信号函数
|
||||
|
||||
**文件:**
|
||||
- 创建:`chanlun/src/signal/functions/mod.rs`
|
||||
- 创建:`chanlun/src/signal/functions/youwukuncheng.rs`
|
||||
|
||||
### 步骤 1:创建 functions 模块入口
|
||||
|
||||
创建 `chanlun/src/signal/functions/mod.rs`:
|
||||
|
||||
```rust
|
||||
//! 信号函数实现 — 每个 `#[signal]` 注册的函数对应一个子模块。
|
||||
//!
|
||||
//! 第三方代码声明:信号函数模式参考 czsc(https://github.com/waditu/czsc,
|
||||
//! Apache License 2.0),已适配为 Rust `fn(&观察者, &HashMap<String, Value>) -> Vec<Signal>`。
|
||||
|
||||
pub mod youwukuncheng;
|
||||
```
|
||||
|
||||
### 步骤 2:编写 youwukuncheng.rs
|
||||
|
||||
创建 `chanlun/src/signal/functions/youwukuncheng.rs`。核心结构:
|
||||
|
||||
```rust
|
||||
use std::collections::HashMap;
|
||||
use serde_json::Value;
|
||||
use chanlun_signal_macros::signal;
|
||||
|
||||
use crate::business::observer::观察者;
|
||||
use crate::signal::params;
|
||||
use crate::signal::Signal;
|
||||
|
||||
/// 中枢第三买卖点信号 — 返回所有匹配的第三类买卖点信号。
|
||||
///
|
||||
/// 参数模板:"{freq}_D1MO{max_overlap}_中枢第三买卖点V230602"
|
||||
///
|
||||
/// 返回三种信号(k3 = 特征 + "V230602"):
|
||||
/// - 中枢段DEA穿越2V230602(同级检查)
|
||||
/// - DEA穿越0轴V230602(本级检查,无须分型)
|
||||
/// - 首次穿越0轴V230602(本级检查 + 分型确认)
|
||||
#[signal(
|
||||
name = "youwukuncheng_中枢第三买卖点_V230602",
|
||||
template = "{freq}_D1MO{max_overlap}_中枢第三买卖点V230602"
|
||||
)]
|
||||
pub fn youwukuncheng_中枢第三买卖点_V230602(
|
||||
obs: &观察者,
|
||||
params: &HashMap<String, Value>,
|
||||
) -> Vec<Signal> {
|
||||
// 1. 确保指标已计算
|
||||
obs.确保指标已计算();
|
||||
|
||||
// 2. 提取参数
|
||||
let max_overlap = params::get_int(params, "max_overlap", 3);
|
||||
let freq = params::get_string(params, "freq", "日线");
|
||||
let 本级完整性 = params::get_string(params, "本级完整性", "实");
|
||||
let 同级完整性 = params::get_string(params, "同级完整性", "合");
|
||||
|
||||
let k1 = freq;
|
||||
let k2 = format!("D1MO{max_overlap}");
|
||||
let k3 = "中枢第三买卖点V230602";
|
||||
|
||||
// 3. 前置检查
|
||||
let 当前缠K = match obs.当前缠K() {
|
||||
Some(k) => k,
|
||||
None => return vec![Signal::new_empty(&k1, &k2, k3)],
|
||||
};
|
||||
|
||||
// 使用线段中枢序列(对应 Python 的 观察员.中枢序列)
|
||||
let 中枢序列 = obs.线段中枢序列();
|
||||
if 中枢序列.is_empty() {
|
||||
return vec![Signal::new_empty(&k1, &k2, k3)];
|
||||
}
|
||||
|
||||
let 当前中枢 = &中枢序列[中枢序列.len() - 1];
|
||||
|
||||
// 检查是否基于线段
|
||||
if 当前中枢.基础序列.read()[0].标识.read().as_str() != "线段" {
|
||||
return vec![Signal::new_empty(&k1, &k2, k3)];
|
||||
}
|
||||
|
||||
// 检查中枢状态
|
||||
if 当前中枢.当前状态() == "中枢之中" {
|
||||
return vec![Signal::new_empty(&k1, &k2, k3)];
|
||||
}
|
||||
|
||||
// 检查本级第三买卖线
|
||||
let 本级线 = match 当前中枢.本级_第三买卖线.read().as_ref() {
|
||||
Some(line) => Arc::clone(line),
|
||||
None => return vec![Signal::new_empty(&k1, &k2, k3)],
|
||||
};
|
||||
|
||||
let mut result = Vec::new();
|
||||
let mut 买卖点分型: Option<Arc<分型>> = None;
|
||||
let 状态 = 当前中枢.当前状态();
|
||||
|
||||
// 4. 本级检查
|
||||
if 当前中枢.完整性(&本级完整性) {
|
||||
// ... DEA穿越0轴 + 首次穿越0轴 逻辑
|
||||
// (详见完整实现)
|
||||
}
|
||||
|
||||
// 5. 同级检查
|
||||
// ... 中枢段DEA穿越2 逻辑
|
||||
// (详见完整实现)
|
||||
|
||||
if result.is_empty() {
|
||||
vec![Signal::new_empty(&k1, &k2, k3)]
|
||||
} else {
|
||||
result
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **注意**:上述为骨架代码。完整实现需按 Python 版 1:1 翻译,包括:
|
||||
> - `之后缠K序列` 切片(`缠论K线序列[index..]`)
|
||||
> - DIF/DEA 零轴穿越检测循环
|
||||
> - 分型确认 + `分型::从缠K序列中获取分型`
|
||||
> - `线段::分割序列` + `虚线::统计MACD行为`
|
||||
> - 偏移计算与 score = max(0, 100 - 偏移 * 5)
|
||||
|
||||
需要额外依赖 `Signal` 的空构造器。在 `signal/signal.rs` 中添加:
|
||||
|
||||
```rust
|
||||
impl Signal {
|
||||
/// 创建一个"空"信号(v1=v2=v3="任意",score=0),对应 Python `create_single_signal(k1=k1, k2=k2, k3=k3)`
|
||||
pub fn new_empty(k1: &str, k2: &str, k3: &str) -> Self {
|
||||
Self {
|
||||
signal: format!("{}_{}_{}_任意_任意_任意_0", k1, k2, k3),
|
||||
score: 0,
|
||||
k1: k1.to_string(),
|
||||
k2: k2.to_string(),
|
||||
k3: k3.to_string(),
|
||||
v1: "任意".to_string(),
|
||||
v2: "任意".to_string(),
|
||||
v3: "任意".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建带分类值的信号
|
||||
pub fn new(k1: &str, k2: &str, k3: &str, v1: &str, v2: &str, v3: &str, score: i32) -> Self {
|
||||
Self {
|
||||
signal: format!("{}_{}_{}_{}_{}_{}_{}", k1, k2, k3, v1, v2, v3, score),
|
||||
score,
|
||||
k1: k1.to_string(),
|
||||
k2: k2.to_string(),
|
||||
k3: k3.to_string(),
|
||||
v1: v1.to_string(),
|
||||
v2: v2.to_string(),
|
||||
v3: v3.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 3:构建验证
|
||||
|
||||
```bash
|
||||
cd chanlun && cargo build
|
||||
```
|
||||
预期:编译通过。
|
||||
|
||||
### 步骤 4:Commit
|
||||
|
||||
```bash
|
||||
git add chanlun/src/signal/functions/ chanlun/src/signal/signal.rs
|
||||
git commit -m "feat(signal): 移植 youwukuncheng_中枢第三买卖点_V230602 到 Rust"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 任务 3:集成测试 — Rust vs Python 对比
|
||||
|
||||
**文件:**
|
||||
- 创建:`chanlun/tests/test_signal_youwukuncheng.rs`
|
||||
|
||||
### 步骤 1:创建 Python 参考脚本
|
||||
|
||||
在 `chanlun-py/tests/` 下创建 `gen_youwukuncheng_golden.py`,跑 Python 版信号函数并输出 JSON:
|
||||
|
||||
```python
|
||||
"""生成 youwukuncheng 信号预期输出(golden file)"""
|
||||
import json, sys
|
||||
sys.path.insert(0, '.')
|
||||
from chanlun.chan import 观察者, 缠论配置, K线
|
||||
from chanlun.signals.youwukuncheng import youwukuncheng_中枢第三买卖点_V230602
|
||||
|
||||
# 加载 .nb 文件
|
||||
obs = 观察者("btcusd", 86400, 缠论配置.默认())
|
||||
obs.读取数据文件("chanlun-py/tests/btcusd-86400-xxx.nb", 缠论配置.默认())
|
||||
|
||||
# 调用信号函数
|
||||
params = {"freq": "日线", "max_overlap": 3, "本级完整性": "实", "同级完整性": "合"}
|
||||
result = youwukuncheng_中枢第三买卖点_V230602(obs, **params)
|
||||
|
||||
# 输出为 JSON
|
||||
output = {k: v for k, v in result.items()}
|
||||
print(json.dumps(output, ensure_ascii=False, indent=2))
|
||||
```
|
||||
|
||||
### 步骤 2:编写 Rust 集成测试
|
||||
|
||||
创建 `chanlun/tests/test_signal_youwukuncheng.rs`:
|
||||
|
||||
```rust
|
||||
use std::collections::HashMap;
|
||||
use chanlun::business::observer::观察者;
|
||||
use chanlun::config::缠论配置;
|
||||
use chanlun::signal::functions::youwukuncheng::youwukuncheng_中枢第三买卖点_V230602;
|
||||
use serde_json::Value;
|
||||
|
||||
#[test]
|
||||
fn test_youwukuncheng_产生信号() {
|
||||
let obs = 观察者::new("btcusd".into(), 86400, 缠论配置::default());
|
||||
obs.write().读取数据文件("tests/btcusd-86400-xxx.nb", 缠论配置::default().不推送())
|
||||
.expect("读取数据文件失败");
|
||||
|
||||
let obs = obs.read();
|
||||
|
||||
let mut params = HashMap::new();
|
||||
params.insert("freq".to_string(), Value::String("日线".to_string()));
|
||||
params.insert("max_overlap".to_string(), Value::Number(3.into()));
|
||||
params.insert("本级完整性".to_string(), Value::String("实".to_string()));
|
||||
params.insert("同级完整性".to_string(), Value::String("合".to_string()));
|
||||
|
||||
let signals = youwukuncheng_中枢第三买卖点_V230602(&obs, ¶ms);
|
||||
|
||||
println!("产生 {} 个信号:", signals.len());
|
||||
for s in &signals {
|
||||
println!(" key={} value={} score={}", s.key(), s.value(), s.score);
|
||||
}
|
||||
|
||||
// 至少有一个非空信号(取决于数据)
|
||||
let non_empty: Vec<_> = signals.iter()
|
||||
.filter(|s| s.value() != "任意_任意_任意_0")
|
||||
.collect();
|
||||
println!("非空信号数: {}", non_empty.len());
|
||||
|
||||
// 验证所有信号的 k3 后缀
|
||||
for s in &signals {
|
||||
assert!(s.k3.ends_with("V230602"), "k3 必须以 V230602 结尾: {}", s.k3);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_youwukuncheng_无中枢返回空信号() {
|
||||
let obs = 观察者::new("empty".into(), 300, 缠论配置::default());
|
||||
let obs = obs.read();
|
||||
|
||||
let params = HashMap::new();
|
||||
let signals = youwukuncheng_中枢第三买卖点_V230602(&obs, ¶ms);
|
||||
|
||||
assert_eq!(signals.len(), 1);
|
||||
assert_eq!(signals[0].value(), "任意_任意_任意_0");
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 3:运行测试
|
||||
|
||||
```bash
|
||||
cd chanlun && cargo test --test test_signal_youwukuncheng
|
||||
```
|
||||
预期:测试通过(或根据数据情况调整断言)。
|
||||
|
||||
### 步骤 4:Commit
|
||||
|
||||
```bash
|
||||
git add chanlun/tests/test_signal_youwukuncheng.rs
|
||||
git commit -m "test(signal): youwukuncheng 集成测试 — 信号产出 + 空中枢边界"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 自检结论
|
||||
|
||||
- **规格覆盖**:设计 §5 便捷 API → 任务 0;§7 确保指标 → 任务 1;§6 youwukuncheng → 任务 2;§8 测试 → 任务 3。全覆盖。
|
||||
- **类型一致**:`SignalFn` 签名不变。`#[signal]` 注册用子项目 1 的宏。`Signal::new_empty`/`Signal::new` 为新增构造器。
|
||||
- **风险提示**:
|
||||
1. `K线::macd()` 返回 cloned 值而非引用——已在设计 §5.1 说明理由(避免 RwLockReadGuard 生命周期传染)
|
||||
2. 集成测试依赖具体 `.nb` 测试数据——需确认文件存在且包含中枢结构
|
||||
3. `Signal::new_empty` 的 key 格式需与 Python `create_single_signal` 一致(过滤 "任意" 段)
|
||||
@@ -0,0 +1,239 @@
|
||||
# 信号原语层移植到 Rust 核心层 — 设计文档
|
||||
|
||||
- 日期:2026-06-22
|
||||
- 范围:原语层(Operate / Signal / Factor / Event / Position 配置与匹配部分)
|
||||
- 参考:czsc(`/home/moscow/czsc`)的 Rust workspace 分层
|
||||
|
||||
## 1. 目标与背景
|
||||
|
||||
当前信号匹配框架(`Signal` / `Factor` / `Event` / `Position` / `Operate`)以纯 Python 实现于 `chanlun-py/chanlun/chan_external.py`(已合并进根目录 `chan.py`)。这套框架抄录自 czsc(Apache 2.0)。
|
||||
|
||||
把这层**纯结构 + 匹配逻辑**移植到 Rust 核心层(`chanlun/src/signal/`),目的:
|
||||
|
||||
- **消除跨模块枚举/类型不一致问题**:信号原语只跟字符串和信号字典打交道,不持有 Rust 分析对象,天然规避「同值枚举跨模块 `is` 不相等」「动态导入找不到模块」这类坑。
|
||||
- **统一原语来源**:Rust 端策略/回测可直接用同一套 `Signal`/`Event`,无需经过 Python。
|
||||
- **性能**:匹配逻辑是热路径(每根 K 线、每个 Position 都跑),Rust 实现去掉 Python 解释开销。
|
||||
- **为后续分层铺路**:原语层稳定后,未来可按 czsc 的路线增量推进注册表、信号串解析、交易引擎。
|
||||
|
||||
## 2. 范围
|
||||
|
||||
### 纳入(Rust + PyO3)
|
||||
|
||||
- `Operate` 枚举
|
||||
- `Signal`:`key()` / `value()` / `is_match()`
|
||||
- `Factor`:`is_match()` / `unique_signals()` / `dump()` / `load()`
|
||||
- `Event`:`is_match()` / `unique_signals()` / `dump()` / `load()`
|
||||
- `Position` 基类:配置字段 + 校验 + `unique_signals` + `__repr__` + config 部分的 `dump`/`load`
|
||||
|
||||
### 不纳入(保持 Python)
|
||||
|
||||
- `Position.update()` 状态机(持仓推进、止损、超时、`pairs`、操作决策)
|
||||
- `信号计算器`(信号计算引擎、配置管理、`_自动挂载指标`)
|
||||
- `SignalsParser`(docstring 解析)
|
||||
- `import_by_name`(动态导入)
|
||||
- 全部信号函数(`chanlun.signals.*`)
|
||||
|
||||
## 3. czsc 参考映射
|
||||
|
||||
czsc 把信号体系拆成分层 crate。本次只对应其最底层「信号原语」:
|
||||
|
||||
| czsc | 本次对应 |
|
||||
|---|---|
|
||||
| `czsc-core/objects/{signal,event,position,operate}.rs` | `chanlun/src/signal/{signal,factor,event,position,operate}.rs` |
|
||||
| `czsc-core` 内 `#[cfg(feature="python")]` 内联 PyO3 包装 | `chanlun-py/src/signal_py.rs`(本项目沿用独立绑定 crate 的既有约定,不内联) |
|
||||
|
||||
czsc 的 `inventory` 编译期注册表、`#[signal]` 宏、`sig_parse`、`engine_v2` 交易引擎、`signals_dispatcher` **本次均不涉及**(属后续分层)。
|
||||
|
||||
## 4. 架构与模块布局
|
||||
|
||||
```
|
||||
chanlun/src/signal/
|
||||
├── mod.rs # pub mod 声明 + re-export
|
||||
├── operate.rs # Operate 枚举(HL/HS/HO/LO/LE/SO/SE)
|
||||
├── signal.rs # Signal
|
||||
├── factor.rs # Factor
|
||||
├── event.rs # Event
|
||||
└── position.rs # Position 基类(config + matching,不含 update)
|
||||
```
|
||||
|
||||
- `chanlun/src/lib.rs` 增加 `pub mod signal;`。
|
||||
- PyO3 绑定新增 `chanlun-py/src/signal_py.rs`,在 `lib.rs` 注册顺序:types → **signal** → config → indicators → kline → structure → algorithm → business → equality。
|
||||
|
||||
### 依赖边界
|
||||
|
||||
信号原语层**零依赖** `business` / `algorithm` / `structure` 层。它只操作:
|
||||
|
||||
- `String`(信号各字段)
|
||||
- 信号字典:匹配时通过 PyO3 接收 `&Bound<PyDict>`,逐键取值判类型
|
||||
|
||||
这是它能独立 `cargo test`、规避跨模块类型问题的根本原因。
|
||||
|
||||
## 5. 逐组件设计
|
||||
|
||||
### 5.1 Operate
|
||||
|
||||
```rust
|
||||
#[pyclass(eq, eq_int)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Operate { HL, HS, HO, LO, LE, SO, SE }
|
||||
```
|
||||
|
||||
- 值映射中文:`HL="持多" HS="持空" HO="持币" LO="开多" LE="平多" SO="开空" SE="平空"`,通过 `value()` 方法 / `__str__` 暴露。
|
||||
- Python 端 `cet.Operate.LO` 直接用该枚举。
|
||||
|
||||
### 5.2 Signal
|
||||
|
||||
```rust
|
||||
#[pyclass(module = "chanlun._chanlun")]
|
||||
pub struct Signal {
|
||||
signal: String,
|
||||
score: i32,
|
||||
k1: String, k2: String, k3: String,
|
||||
v1: String, v2: String, v3: String,
|
||||
}
|
||||
```
|
||||
|
||||
> 注:仅 `Position` 需要 `#[pyclass(subclass)]`(Python 子类补 `update()`)。`Signal`/`Factor`/`Event` 不被子类化,用普通 `#[pyclass]`。
|
||||
|
||||
- 构造签名:`Signal(signal="", score=0, k1="任意", k2="任意", k3="任意", v1="任意", v2="任意", v3="任意")`。
|
||||
- `signal` 非空 → 按 `_` 拆 7 段(非 7 段 raise);为空 → 由各字段拼。
|
||||
- `signal` 非字符串 → `TypeError`(对齐 Python `__post_init__`)。
|
||||
- `score` 越界 [0,100] → `ValueError`。
|
||||
- `key` property:拼接 k1/k2/k3 中非「任意」的部分,`_` 连接。
|
||||
- `value` property:`v1_v2_v3_score`。
|
||||
- `is_match(s) -> bool`:见 §6。
|
||||
- `__repr__` → `Signal('<signal>')`。
|
||||
|
||||
### 5.3 Factor
|
||||
|
||||
```rust
|
||||
#[pyclass(module = "chanlun._chanlun")]
|
||||
pub struct Factor {
|
||||
signals_all: Vec<Signal>,
|
||||
signals_any: Vec<Signal>,
|
||||
signals_not: Vec<Signal>,
|
||||
name: String,
|
||||
}
|
||||
```
|
||||
|
||||
- 构造:`Factor(signals_all, signals_any=[], signals_not=[], name="")`。`signals_all` 空 → `ValueError`。
|
||||
- 构造时计算 `name`:见 §6 ③(确定性哈希)。
|
||||
- `unique_signals` property:所有 signals 的 `signal` 字符串去重列表。
|
||||
- `is_match`:`signals_not` 任一命中 → False;`signals_all` 必须全中;`signals_any` 非空时至少一中。
|
||||
- `dump() -> dict`、`load(raw) classmethod`。
|
||||
|
||||
### 5.4 Event
|
||||
|
||||
```rust
|
||||
#[pyclass(module = "chanlun._chanlun")]
|
||||
pub struct Event {
|
||||
operate: Operate,
|
||||
factors: Vec<Factor>,
|
||||
signals_all: Vec<Signal>,
|
||||
signals_any: Vec<Signal>,
|
||||
signals_not: Vec<Signal>,
|
||||
name: String,
|
||||
sha256: String,
|
||||
}
|
||||
```
|
||||
|
||||
- 构造:`Event(operate, factors, signals_all=[], signals_any=[], signals_not=[], name="")`。`factors` 空 → `ValueError`。
|
||||
- `name`:有传名 → `<name>#<hash>`,否则 `<operate中文值>#<hash>`;同时存 `sha256` 字段。
|
||||
- `unique_signals`、`is_match(s) -> (bool, Option<String>)`(命中返回 `(True, factor_name)`)、`dump`、`load`。
|
||||
- `get_signals_config` **不在 Rust 实现**(依赖 Python 的 `SignalsParser`),保留在调用方 Python。
|
||||
|
||||
### 5.5 Position 基类
|
||||
|
||||
```rust
|
||||
#[pyclass(subclass, module = "chanlun._chanlun")]
|
||||
pub struct Position {
|
||||
symbol: String,
|
||||
opens: Vec<Event>,
|
||||
exits: Vec<Event>,
|
||||
events: Vec<Event>, // opens + exits
|
||||
name: String,
|
||||
interval: i64,
|
||||
timeout: i64,
|
||||
stop_loss: i64,
|
||||
T0: bool,
|
||||
}
|
||||
```
|
||||
|
||||
- 构造:`Position(symbol, opens, exits=[], interval=0, timeout=1000, stop_loss=1000, T0=False, name)`。
|
||||
- `name` 缺失 → `ValueError`(对齐 Python `assert name`)。
|
||||
- 每个 event 的 `operate` ∈ {LO,LE,SO,SE},否则 raise。
|
||||
- `unique_signals` property、`__repr__`、config 部分的 `dump`/`load`。
|
||||
- **状态字段、`update()`、`pairs`、`with_data` 版 dump、`get_signals_config` 全部留 Python 子类。**
|
||||
|
||||
## 6. 三个兼容性关键点
|
||||
|
||||
### ① `Signal.is_match` 缺键时 raise `ValueError`
|
||||
|
||||
Python 现状:键不在信号字典 → `raise ValueError`。`strategies.py` 靠 `try: pos.update(...) except ValueError: pass` 兜底。
|
||||
|
||||
**决策**:Rust `is_match` 缺键 → `PyValueError`,**不静默返回 False**。这是行为契约。
|
||||
|
||||
### ② 信号字典值可能非字符串
|
||||
|
||||
`信号计算器.信号字典` 合并了 OHLCV 行情(值为 datetime/float)。Python 有 `isinstance(v, str)` 守卫:非 str → `logger.warning` + 返回 False。
|
||||
|
||||
**决策**:`is_match` 接收 `&Bound<PyDict>`。取到 key 对应值后:
|
||||
|
||||
- 值不存在 → `PyValueError`(关键点 ①)。
|
||||
- 值非字符串 → 返回 False(对齐 Python 守卫)。**不打 warning**:匹配是每根 K 线的热路径,省去日志噪音;非 str 值来自 OHLCV 行情注入,是预期情况而非异常。
|
||||
- 值是字符串 → 按 `_` 拆 4 段(`v1_v2_v3_score`)做匹配。
|
||||
|
||||
### ③ Factor/Event 的 sha256 命名
|
||||
|
||||
Python:`hashlib.sha256(str(dump_dict_minus_name).encode()).hexdigest().upper()[:4]`,依赖 Python `str(dict)` 的逐字节格式。
|
||||
|
||||
**决策**:用 Rust 确定性哈希——对 `signals_all`/`signals_any`/`signals_not`(Factor)或加上 factors 的 dump(Event)拼成稳定字符串后算 sha256,取大写前 4。
|
||||
|
||||
- 自洽:同输入恒等同名,`dump`/`load` 来回一致。
|
||||
- **取舍(已知不兼容)**:生成的 hash 与 Python 旧版不同。依赖旧 `name` 的持久化仓位(保存的 .json)不再 roundtrip。本项目 Position 基本每次运行新建,可接受。
|
||||
|
||||
## 7. Drop-in 兼容策略
|
||||
|
||||
- `chan_external.py` 顶部:`from chanlun._chanlun import Signal, Factor, Event, Operate, Position as _PositionBase`,删除原 Python 类定义。
|
||||
- `Position` 改为子类:
|
||||
|
||||
```python
|
||||
class Position(_PositionBase):
|
||||
def __init__(self, symbol, opens, exits=[], interval=0, timeout=1000,
|
||||
stop_loss=1000, T0=False, name=None):
|
||||
super().__init__(symbol, opens, exits, interval, timeout, stop_loss, T0, name)
|
||||
# Python 侧状态
|
||||
self.pos_changed = False
|
||||
self.operates = []
|
||||
self.holds = []
|
||||
self.pos = 0
|
||||
self.last_event = {...}
|
||||
self.last_lo_dt = None
|
||||
self.last_so_dt = None
|
||||
self.end_dt = None
|
||||
# update() / pairs / get_signals_config / with_data dump 保留
|
||||
```
|
||||
|
||||
- `main.py` / `strategies.py` 中 `cet.Signal(...)`、`cet.Factor(...)`、`cet.Event(...)`、`cet.Position(...)`、`cet.Operate.LO` **无需改动**——构造签名与方法名一致。
|
||||
- 根目录 `chan.py` 的对应类同样替换为 import Rust 版本(保持与包版本一致)。
|
||||
|
||||
## 8. 测试策略
|
||||
|
||||
1. **Rust 单测**(`cargo test`,`chanlun/src/signal/` 内 `#[cfg(test)]`):
|
||||
- Signal:7 段解析、非 7 段 raise、score 越界 raise、key 过滤「任意」、value 拼接。
|
||||
- Factor/Event:`signals_all/any/not` 真值表全覆盖、空 signals_all/factors raise、确定性哈希同输入同名。
|
||||
- Position:name 缺失 raise、非法 operate raise、unique_signals 去重。
|
||||
2. **跨语言一致性**(pytest,复用 `tests/helpers/api_consistency.py`):
|
||||
- 构造相同 Signal/Factor/Event/Position,断言 `is_match`、`unique_signals`、`dump` 结构与移植前**逐字段一致**(name hash 除外)。
|
||||
- `is_match` 缺键 raise `ValueError`、值非 str 返回 False 两条边界。
|
||||
3. **回归**:跑 `测试_信号识别` + sync 回测,确认信号匹配与开关仓行为不变。
|
||||
|
||||
## 9. 已知取舍
|
||||
|
||||
- **name hash 不兼容旧 Python 版本**(§6 ③):依赖旧 name 的持久化仓位会对不上。可接受,因 Position 多为运行时新建。
|
||||
- **`get_signals_config` 留 Python**:它依赖 `SignalsParser` 动态解析,本次不移植;Rust `Event`/`Position` 不提供该方法,由 Python 调用方补。
|
||||
- **`Position.update` 留 Python**:状态机本次不移植,Position 被一分为二(Rust 基类配置 + Python 子类状态)。
|
||||
|
||||
## 10. 许可证
|
||||
|
||||
新增 Rust 文件沿用项目 MIT 头。信号原语逻辑摘录/参考自 czsc(Apache 2.0),在 `signal/mod.rs` 顶部加第三方代码声明(与根 `chan.py` 已有声明一致)。
|
||||
@@ -0,0 +1,181 @@
|
||||
# 子项目 1:信号注册框架 — 设计文档
|
||||
|
||||
- 日期:2026-06-22
|
||||
- 所属:「全 Rust 信号计算迁移」第 1 个子项目(共 4 个)
|
||||
- 参考:czsc(`/home/moscow/czsc`)的 `czsc-signal-macros` + `czsc-signals/{registry,types}.rs`
|
||||
- 前置:原语层已完成(`chanlun/src/signal/` 的 Signal/Factor/Event/Position/Operate)
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
「全 Rust 信号计算迁移」把信号函数、注册/解析、计算引擎、持仓状态机全部移到 Rust。拆为 4 个子项目(依赖序 1→2→3→4):
|
||||
|
||||
1. **信号注册框架**(本文档)
|
||||
2. 信号函数 API 暴露 + 移植 youwukuncheng
|
||||
3. 信号计算引擎 + PyO3 分发器
|
||||
4. Position.update 状态机
|
||||
|
||||
本子项目交付**编译期信号注册机制**:一个 `#[signal]` 属性宏 + `inventory` 注册表 + 描述符类型 + 一个探针信号验证机制。
|
||||
|
||||
**它消灭什么**:Python 的 `import_by_name`(动态导入,曾导致「找不到模块」「跨模块枚举 `is` 不等」)和 `SignalsParser` 的 docstring 正则解析(曾导致「多 pattern sig_pats_map」「get_function_name v[0]」「sys 未导入」等脆弱 bug)。注册变成编译期完成、查表 O(1)。
|
||||
|
||||
## 2. 范围
|
||||
|
||||
### 纳入
|
||||
- 新 proc-macro crate `chanlun-signal-macros`:`#[signal(name, template)]` 属性宏
|
||||
- `chanlun/src/signal/registry.rs`:`SignalDescriptor` / `SignalFn` / `SignalMeta` / `SIGNAL_REGISTRY` + 只读查询 API
|
||||
- `chanlun/Cargo.toml` 新增 `inventory` 依赖 + path 依赖 `chanlun-signal-macros`
|
||||
- 一个探针信号 + 测试(验证注册→查表→重名检测)
|
||||
|
||||
### 不纳入(后续子项目)
|
||||
- 真实信号函数移植(子项目 2)
|
||||
- 「确保指标按需增量计算」API(子项目 2,移植 youwukuncheng 读 MACD 时落地)
|
||||
- 信号计算引擎 + `call_signal` PyO3 分发器(子项目 3)
|
||||
- Position.update 状态机(子项目 4)
|
||||
|
||||
## 3. 关键设计决策
|
||||
|
||||
| 决策 | 选择 | 理由 |
|
||||
|---|---|---|
|
||||
| SignalFn 是否带 TaCache | **否** | 核心层 K线已挂载指标(`指标计算器::计算并挂载`),信号函数直接读 `标的K线.指标.macd(..)`,无需 czsc 式 TaCache |
|
||||
| 注册表位置 | **chanlun 核心 crate** `signal/` 模块 | 信号函数直接读 observer(同 crate)、指标在 K线上,无需独立 signals crate |
|
||||
| params 类型 | `HashMap<String, serde_json::Value>` | 灵活,对应 Python dict 来源(PyO3 层自然转换) |
|
||||
| 描述符是否含 indicators/category 字段 | **否,保持最小 `{name, template, func}`** | 指标由「信号内识别 + 管线增量算」处理,不在描述符声明;本项目信号皆 observer 级,无需 category |
|
||||
|
||||
## 4. Crate 结构
|
||||
|
||||
```
|
||||
chanlun-signal-macros/ ← 新建 proc-macro crate(Rust 强制独立)
|
||||
├── Cargo.toml ← [lib] proc-macro = true;deps: syn, quote, proc-macro2
|
||||
└── src/lib.rs ← #[signal] 属性宏
|
||||
|
||||
chanlun/ ← 现有核心 crate
|
||||
├── Cargo.toml ← 新增 inventory="0.3" + path 依赖 chanlun-signal-macros
|
||||
└── src/signal/
|
||||
├── mod.rs ← pub mod registry;
|
||||
└── registry.rs ← 描述符类型 + 注册表 + 探针信号(cfg(test))
|
||||
```
|
||||
|
||||
`chanlun` 通过 path 依赖 `chanlun-signal-macros`(无需引入 workspace;Cargo path 依赖即可。如愿统一可后续加 `[workspace]`)。
|
||||
|
||||
## 5. 描述符类型与签名(`chanlun/src/signal/registry.rs`)
|
||||
|
||||
```rust
|
||||
use crate::business::observer::观察者;
|
||||
use crate::signal::Signal;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
/// 信号函数签名 — 读观察者状态(含 K线已挂指标)+ 参数 → 信号列表。无 TaCache。
|
||||
pub type SignalFn = fn(&观察者, &HashMap<String, Value>) -> Vec<Signal>;
|
||||
|
||||
/// 信号描述符(编译期元数据,由 `#[signal]` 宏生成、`inventory` 收集)。
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct SignalDescriptor {
|
||||
/// 信号函数名,如 "youwukuncheng_中枢第三买卖点_V230602"
|
||||
pub name: &'static str,
|
||||
/// 参数模板,如 "{freq}_D1MO{max_overlap}_中枢第三买卖点V230602"
|
||||
pub template: &'static str,
|
||||
/// 函数指针
|
||||
pub func: SignalFn,
|
||||
}
|
||||
|
||||
inventory::collect!(SignalDescriptor);
|
||||
|
||||
/// 运行时信号元信息。
|
||||
pub struct SignalMeta {
|
||||
pub func: SignalFn,
|
||||
pub template: &'static str,
|
||||
}
|
||||
|
||||
/// 归并描述符为注册表;重名返回 Err(纯函数,便于单测)。
|
||||
fn 归并(
|
||||
descs: impl Iterator<Item = SignalDescriptor>,
|
||||
) -> Result<HashMap<&'static str, SignalMeta>, String> {
|
||||
let mut m: HashMap<&'static str, SignalMeta> = HashMap::new();
|
||||
for d in descs {
|
||||
if m.insert(d.name, SignalMeta { func: d.func, template: d.template }).is_some() {
|
||||
return Err(format!("信号重名:{}", d.name));
|
||||
}
|
||||
}
|
||||
Ok(m)
|
||||
}
|
||||
|
||||
/// 全局注册表视图(由 inventory 归并;重名 panic,fail-fast)。
|
||||
pub static SIGNAL_REGISTRY: LazyLock<HashMap<&'static str, SignalMeta>> = LazyLock::new(|| {
|
||||
归并(inventory::iter::<SignalDescriptor>.into_iter().copied())
|
||||
.unwrap_or_else(|e| panic!("{e}"))
|
||||
});
|
||||
|
||||
/// 按名查信号元信息。
|
||||
pub fn get_signal(name: &str) -> Option<&'static SignalMeta> {
|
||||
SIGNAL_REGISTRY.get(name)
|
||||
}
|
||||
|
||||
/// 按名查参数模板。
|
||||
pub fn get_template(name: &str) -> Option<&'static str> {
|
||||
SIGNAL_REGISTRY.get(name).map(|m| m.template)
|
||||
}
|
||||
|
||||
/// 列出所有已注册信号名(排序)。
|
||||
pub fn list_signal_names() -> Vec<&'static str> {
|
||||
let mut v: Vec<_> = SIGNAL_REGISTRY.keys().copied().collect();
|
||||
v.sort();
|
||||
v
|
||||
}
|
||||
```
|
||||
|
||||
## 6. `#[signal]` 宏(`chanlun-signal-macros/src/lib.rs`)
|
||||
|
||||
属性宏贴在信号函数上,做三件事:
|
||||
|
||||
1. **校验**:函数名必须含 `_V<数字版本>`;`name` 属性须与函数名一致;`name`/`template` 非空。不符 → `compile_error!`。
|
||||
2. **保留原函数**不变。
|
||||
3. **生成** 一个 `static` 描述符 + `inventory::submit!` 提交:
|
||||
|
||||
宏输入 `#[signal(name = "foo_V230101", template = "{freq}_D1_foo")]` 贴在 `fn foo_V230101(...)` 上,展开为(概念示意):
|
||||
```rust
|
||||
fn foo_V230101(观: &观察者, p: &HashMap<String, Value>) -> Vec<Signal> { /* 原体 */ }
|
||||
inventory::submit! {
|
||||
crate::signal::registry::SignalDescriptor {
|
||||
name: "foo_V230101",
|
||||
template: "{freq}_D1_foo",
|
||||
func: foo_V230101 as crate::signal::registry::SignalFn,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**路径约定**:宏 emit `crate::signal::registry::...`,即假定信号函数住在 `chanlun` crate 内(本迁移的既定结构)。
|
||||
|
||||
## 7. 测试
|
||||
|
||||
1. **宏 crate**(`chanlun-signal-macros/tests/test_signal_macro.rs`):普通集成测试——定义一个符合签名的探针函数并贴 `#[signal(name="probe_macro_V000000", template="{freq}_D1_probe")]`,断言它能编译且 `inventory::iter` 能收到对应描述符(name/template 正确)。编译失败用例(name 与函数名不一致、缺版本号)作为**可选** trybuild compile-fail 测试,非必须。
|
||||
2. **核心注册表**(`registry.rs` 内 `#[cfg(test)]`):
|
||||
- 用 `inventory::submit!` 提交一个探针 `SignalDescriptor`(name `__probe_V000000`);
|
||||
- `get_signal("__probe_V000000")` 命中、`get_template` 返回模板、`list_signal_names()` 含它;
|
||||
- 重名场景:把归并逻辑抽成一个可独立调用的纯函数 `fn 归并(descs: impl Iterator<Item=SignalDescriptor>) -> Result<HashMap<..>, String>`,单测对重复 name 返回 Err(`SIGNAL_REGISTRY` 的 LazyLock 内部调用它并对 Err `panic!`),避免污染全局 inventory。
|
||||
|
||||
## 8. 数据流
|
||||
|
||||
```
|
||||
编译期: #[signal] 宏 → SignalDescriptor 常量 → inventory::submit!
|
||||
启动时: SIGNAL_REGISTRY (LazyLock) ← inventory::iter 归并(重名 panic)
|
||||
运行时: get_signal(name) -> &SignalMeta { func, template } (O(1) 查表)
|
||||
后续子项目 3 的计算引擎用 func 调用、用 template 反向生成信号 key
|
||||
```
|
||||
|
||||
## 9. 错误处理
|
||||
|
||||
- **编译期**:宏校验失败 → `compile_error!`(带清晰中文消息)。
|
||||
- **启动期**:重名信号 → `panic!("信号重名:{name}")`(fail-fast,对应 czsc 的 normalize 重名检测)。
|
||||
- **运行期**:`get_signal` 未命中返回 `None`(调用方——子项目 3——决定如何处理,对应旧「未找到解析函数」告警)。
|
||||
|
||||
## 10. 已知取舍与后续
|
||||
|
||||
- **无运行时可扩展性**:信号在编译期注册,新增信号需重编译(`maturin build`)。这是「全 Rust」方案的既定取舍,用户已确认。
|
||||
- **指标按需机制不在本子项目**:信号函数读指标 + 管线增量计算的「确保指标」API 在子项目 2 落地。
|
||||
- **category(kline/trader)暂不引入**:若子项目 4 的 Position.update 引入 trader 级信号,届时再扩描述符。
|
||||
|
||||
## 11. 许可证
|
||||
|
||||
新增 Rust 文件沿用项目 MIT 头。注册/宏机制参考 czsc(Apache 2.0),在 `registry.rs` 与 macro crate 顶部加第三方代码声明。
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user