Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 16ed3de8f5 | |||
| 8405d478bc | |||
| 15a1d43b1d | |||
| a8df8cd187 | |||
| 9c7686d104 | |||
| 34c42ecd68 | |||
| 8a2afb9ed0 | |||
| 11d897ebaa | |||
| 4bf9461009 | |||
| c83cddbcc4 | |||
| 0a7e9dc896 | |||
| dd40f95742 | |||
| e33b1f0744 | |||
| 823a24d364 | |||
| 58c85dc7cd | |||
| c3e9ae8d77 | |||
| b7c4e60420 | |||
| 15dc44e8e0 | |||
| bd4fceab02 | |||
| af3ebf13c8 | |||
| 22109d8b30 | |||
| c2c09fc8ba | |||
| 0eb52cc06a | |||
| 1e6025a968 | |||
| 14279f3df6 | |||
| fca62f3141 | |||
| e50172e923 | |||
| c87fb66d34 | |||
| 9900266516 |
@@ -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 的截图、日志或对比数据。
|
||||
@@ -1,5 +1,8 @@
|
||||
name: 构建发布
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
@@ -13,12 +16,129 @@ on:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
|
||||
jobs:
|
||||
# ============================================================
|
||||
# Linux x86_64 (manylinux)
|
||||
# 1. 校验 & 发布
|
||||
# 解析 chanlun-py 依赖的版本号 → 检查 crates.io 是否可用 →
|
||||
# 不可用时检查本地 chanlun 版本是否匹配 → 匹配则自动发布 →
|
||||
# 等待索引同步
|
||||
# ============================================================
|
||||
check-version:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.parse.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: 安装 Rust 工具链
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: rustfmt, clippy
|
||||
|
||||
- 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: 解析本地 chanlun 核心库版本
|
||||
id: local-ver
|
||||
working-directory: chanlun
|
||||
run: |
|
||||
VER=$(grep -oP '^version\s*=\s*"\K[0-9]+\.[0-9]+\.[0-9]+(?=")' Cargo.toml | head -1)
|
||||
echo "version=$VER" >> $GITHUB_OUTPUT
|
||||
echo "本地 chanlun 版本: $VER"
|
||||
|
||||
- name: 检查 crates.io 并决定是否发布
|
||||
id: check
|
||||
run: |
|
||||
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.need-publish == 'true'
|
||||
working-directory: chanlun
|
||||
run: cargo fmt --check
|
||||
|
||||
- name: Lint 检查
|
||||
if: steps.check.outputs.need-publish == 'true'
|
||||
working-directory: chanlun
|
||||
run: cargo clippy
|
||||
|
||||
- name: 运行测试
|
||||
if: steps.check.outputs.need-publish == 'true'
|
||||
working-directory: chanlun
|
||||
run: cargo test
|
||||
|
||||
- name: 验证打包
|
||||
if: steps.check.outputs.need-publish == 'true'
|
||||
working-directory: chanlun
|
||||
run: cargo publish --dry-run --allow-dirty
|
||||
|
||||
- name: 登录 crates.io 并发布
|
||||
if: steps.check.outputs.need-publish == 'true'
|
||||
run: |
|
||||
cargo login ${{ secrets.CARGO_TOKEN }}
|
||||
cd chanlun && 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: [check-version]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -31,6 +151,20 @@ jobs:
|
||||
- name: 安装 Rust 工具链
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- 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
|
||||
with:
|
||||
@@ -45,11 +179,11 @@ jobs:
|
||||
name: wheels-linux-x86_64
|
||||
path: chanlun-py/dist/
|
||||
|
||||
|
||||
# ============================================================
|
||||
# macOS wheels (x86_64 + arm64)
|
||||
# 3. 构建 wheel — macOS (x86_64 + arm64)
|
||||
# ============================================================
|
||||
macos:
|
||||
needs: [check-version]
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -65,6 +199,20 @@ jobs:
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- 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
|
||||
with:
|
||||
@@ -79,9 +227,10 @@ jobs:
|
||||
path: chanlun-py/dist/
|
||||
|
||||
# ============================================================
|
||||
# Windows wheels (x86_64)
|
||||
# 4. 构建 wheel — Windows x86_64
|
||||
# ============================================================
|
||||
windows:
|
||||
needs: [check-version]
|
||||
runs-on: windows-latest
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -97,6 +246,22 @@ jobs:
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- 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
|
||||
with:
|
||||
@@ -111,9 +276,10 @@ jobs:
|
||||
path: chanlun-py/dist/
|
||||
|
||||
# ============================================================
|
||||
# 源码分发包 (sdist)
|
||||
# 5. 源码分发包 (sdist)
|
||||
# ============================================================
|
||||
sdist:
|
||||
needs: [check-version]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -123,6 +289,9 @@ jobs:
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: 安装 Rust 工具链
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: 构建 sdist
|
||||
uses: PyO3/maturin-action@v1
|
||||
with:
|
||||
@@ -137,14 +306,14 @@ jobs:
|
||||
path: chanlun-py/dist/
|
||||
|
||||
# ============================================================
|
||||
# 发布至 PyPI
|
||||
# 6. 发布至 PyPI
|
||||
# ============================================================
|
||||
publish:
|
||||
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:
|
||||
id-token: write # PyPI 信任发布(推荐)
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: 下载所有产物
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
version: 2
|
||||
|
||||
build:
|
||||
os: ubuntu-24.04
|
||||
tools:
|
||||
python: "3.12"
|
||||
jobs:
|
||||
pre_install:
|
||||
# 安装 Rust 工具链以编译 PyO3 扩展
|
||||
- curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
|
||||
- source $HOME/.cargo/env
|
||||
- pip install maturin
|
||||
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
|
||||
python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
||||
# 从源码安装 chanlun-py(maturin develop)
|
||||
- method: pip
|
||||
path: chanlun-py
|
||||
@@ -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.
|
||||
@@ -3,7 +3,7 @@
|
||||
[](https://pypi.org/project/chanlun/)
|
||||
[](LICENSE)
|
||||
|
||||
基于 [chanlun](./chanlun/) Rust 核心库的 PyO3 高性能 Python 绑定,API 与 `chan.py` 完全兼容。
|
||||
基于 [chanlun](./chanlun/) Rust 核心库的 PyO3 高性能 Python 绑定,API 参考 `chan.py` 设计,高度兼容。
|
||||
|
||||
## 安装
|
||||
|
||||
@@ -19,8 +19,8 @@ import chanlun
|
||||
# 创建配置(全部默认值)
|
||||
config = chanlun.缠论配置()
|
||||
|
||||
# 读取 K 线数据文件,创建观察者
|
||||
obs = chanlun.观察者.读取数据文件("path/to/data.nb", config)
|
||||
# 读取 K 线数据文件(文件名需遵循 `符号-周期-起始时间戳-结束时间戳.nb` 格式)
|
||||
obs = chanlun.观察者.读取数据文件("path/to/btcusd-300-1631772074-1632222374.nb", config)
|
||||
|
||||
# 查看各层级序列
|
||||
print(f"K线数量: {len(obs.普通K线序列)}")
|
||||
@@ -29,10 +29,175 @@ print(f"线段数量: {len(obs.线段序列)}")
|
||||
print(f"中枢数量: {len(obs.中枢序列)}")
|
||||
|
||||
# 或使用立体分析器进行多周期分析
|
||||
analyzer = chanlun.立体分析器("BTCUSD", ["1min", "5min", "30min"], config)
|
||||
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,16 +206,17 @@ analyzer = chanlun.立体分析器("BTCUSD", ["1min", "5min", "30min"], 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 # 运行集成测试
|
||||
@@ -60,18 +226,20 @@ pip install target/wheels/chanlun-*.whl
|
||||
|
||||
| 类别 | 类名 | 说明 |
|
||||
|------|------|------|
|
||||
| 枚举 | `买卖点类型`, `相对方向`, `分型结构` | 缠论基础枚举 |
|
||||
| 枚举 | `买卖点类型`, `相对方向`, `分型结构`, `Operate` | 缠论基础枚举 |
|
||||
| 数据 | `缺口`, `K线`, `缠论K线` | K 线数据结构 |
|
||||
| 结构 | `分型`, `虚线`, `线段特征`, `特征分型` | 分析层级结构 |
|
||||
| 指标 | `平滑异同移动平均线`, `相对强弱指数`, `随机指标` | MACD/RSI/KDJ |
|
||||
| 算法 | `笔`, `线段`, `中枢`, `背驰分析` | 识别算法 |
|
||||
| 业务 | `缠论配置`, `基础买卖点`, `买卖点`, `观察者`, `K线合成器`, `立体分析器` | 分析框架 |
|
||||
| 业务 | `缠论配置`, `观察者`, `K线合成器`, `立体分析器`, `买卖点` | 分析框架 |
|
||||
| 信号 | `Signal`, `Factor`, `Event`, `Position`, `信号引擎` | 信号匹配+计算引擎 |
|
||||
| 注册表 | `call_signal`, `list_signals`, `get_signal_template`, `register_signal`, `unregister_signal` | 信号发现+动态注册 |
|
||||
|
||||
## 兼容性
|
||||
|
||||
- Python 3.9+
|
||||
- 类名 / 方法名 / 字段名 / 签名与 `chan.py` 一致
|
||||
- 支持 `.nb` / `.dat` 二进制文件格式(大端字节序)
|
||||
- 类名 / 方法名 / 字段名与 `chan.py` 保持一致
|
||||
- 支持 `.nb` 二进制文件格式(大端字节序)
|
||||
|
||||
## 许可
|
||||
|
||||
|
||||
+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` |
|
||||
@@ -3,3 +3,5 @@ __pycache__/
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
Cargo.lock
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "chanlun-py"
|
||||
version = "26.5.86"
|
||||
edition = "2021"
|
||||
version = "26.6.125"
|
||||
edition = "2024"
|
||||
description = "缠论技术分析库 — Rust 高性能 Python 绑定"
|
||||
authors = ["YuYuKunKun"]
|
||||
license = "MIT"
|
||||
@@ -12,7 +12,12 @@ crate-type = ["cdylib"]
|
||||
name = "chanlun"
|
||||
|
||||
[dependencies]
|
||||
chanlun = "26.5.3" # { path = "../chanlun" }
|
||||
pyo3 = { version = "0.28", features = ["extension-module", "experimental-inspect"] }
|
||||
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"
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../LICENSE
|
||||
@@ -0,0 +1,21 @@
|
||||
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.
|
||||
@@ -1 +0,0 @@
|
||||
../README.md
|
||||
@@ -0,0 +1,160 @@
|
||||
# chanlun — 缠论技术分析 Python 绑定
|
||||
|
||||
[](https://pypi.org/project/chanlun/)
|
||||
[](LICENSE)
|
||||
|
||||
基于 [chanlun](../chanlun/) Rust 核心库的 PyO3 高性能 Python 绑定,API 参考 `chan.py` 设计,高度兼容。
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
pip install chanlun
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
```python
|
||||
import chanlun
|
||||
|
||||
# 创建配置(全部默认值)
|
||||
config = chanlun.缠论配置()
|
||||
|
||||
# 读取 K 线数据文件(文件名需遵循 `符号-周期-起始时间戳-结束时间戳.nb` 格式,如 `btcusd-300-1631772074-1632222374.nb`)
|
||||
obs = chanlun.观察者.读取数据文件("path/to/btcusd-300-1631772074-1632222374.nb", config)
|
||||
|
||||
# 查看各层级序列
|
||||
print(f"K线数量: {len(obs.普通K线序列)}")
|
||||
print(f"笔数量: {len(obs.笔序列)}")
|
||||
print(f"线段数量: {len(obs.线段序列)}")
|
||||
print(f"中枢数量: {len(obs.中枢序列)}")
|
||||
|
||||
# 或使用立体分析器进行多周期分析
|
||||
analyzer = chanlun.立体分析器("BTCUSD", [60, 60*5, 60*5*6], config)
|
||||
# 逐根投喂 K 线...
|
||||
```
|
||||
|
||||
## 从源码构建
|
||||
|
||||
前置依赖: [Rust](https://www.rust-lang.org) + [maturin](https://www.maturin.rs)
|
||||
|
||||
```bash
|
||||
pip install maturin
|
||||
|
||||
# 推荐:一键清理缓存 + 构建 + 安装
|
||||
./clean_install.sh
|
||||
|
||||
# 或手动:
|
||||
# 开发模式(直接安装到当前 venv)
|
||||
maturin develop
|
||||
|
||||
# 或构建 wheel
|
||||
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
|
||||
./build.sh develop # 开发安装
|
||||
./build.sh wheel # 构建 wheel
|
||||
```
|
||||
|
||||
## 导出类
|
||||
|
||||
| 类别 | 类名 | 说明 |
|
||||
|------|------|------|
|
||||
| 枚举 | `买卖点类型`, `相对方向`, `分型结构` | 缠论基础枚举 |
|
||||
| 数据 | `缺口`, `K线`, `缠论K线` | K 线数据结构 |
|
||||
| 结构 | `分型`, `虚线`, `线段特征`, `特征分型` | 分析层级结构 |
|
||||
| 指标 | `平滑异同移动平均线`, `相对强弱指数`, `随机指标` | MACD/RSI/KDJ |
|
||||
| 算法 | `笔`, `线段`, `中枢`, `背驰分析` | 识别算法 |
|
||||
| 业务 | `缠论配置`, `基础买卖点`, `买卖点`, `观察者`, `K线合成器`, `立体分析器` | 分析框架 |
|
||||
|
||||
## 兼容性
|
||||
|
||||
- Python 3.9+
|
||||
- 类名 / 方法名 / 字段名与 `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)。
|
||||
|
||||
详见 [NOTICE](../NOTICE) 和 [LICENSES/](../LICENSES/) 目录。
|
||||
+4
-4
@@ -58,10 +58,10 @@ fn read_version(path: &std::path::Path, section: &str) -> String {
|
||||
if !in_section {
|
||||
continue;
|
||||
}
|
||||
if trimmed.starts_with("version") {
|
||||
if let Some(v) = trimmed.split('=').nth(1) {
|
||||
return v.trim().trim_matches('"').trim().to_string();
|
||||
}
|
||||
if trimmed.starts_with("version")
|
||||
&& let Some(v) = trimmed.split('=').nth(1)
|
||||
{
|
||||
return v.trim().trim_matches('"').trim().to_string();
|
||||
}
|
||||
}
|
||||
panic!("Cannot parse version from {:?}", path);
|
||||
|
||||
Executable → Regular
Executable → Regular
+811
-2234
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,9 @@ __all__ = [
|
||||
"基础买卖点",
|
||||
"平滑异同移动平均线",
|
||||
"指标",
|
||||
"特征分型",
|
||||
"指标容器",
|
||||
"指标计算器",
|
||||
"均线工具",
|
||||
"相对强弱指数",
|
||||
"相对方向",
|
||||
"立体分析器",
|
||||
@@ -27,6 +29,87 @@ __all__ = [
|
||||
"转化为时间戳",
|
||||
"转化为时间戳_数字",
|
||||
"随机指标",
|
||||
"布林带",
|
||||
"get_分型模式",
|
||||
"set_分型模式",
|
||||
"get_扩展线段模式",
|
||||
"set_扩展线段模式",
|
||||
"get_log_level",
|
||||
"set_log_level",
|
||||
"get_rs_log_level",
|
||||
"set_rs_log_level",
|
||||
"K线相等",
|
||||
"缠论K线相等",
|
||||
"分型相等",
|
||||
"缺口相等",
|
||||
"线段特征相等",
|
||||
"中枢相等",
|
||||
"虚线相等",
|
||||
]
|
||||
|
||||
from ._chanlun import *
|
||||
from ._chanlun import set_log_level as _rs_set_log_level, get_log_level as _rs_get_log_level
|
||||
|
||||
import sys as _sys
|
||||
from loguru import logger as _logger
|
||||
|
||||
# ---- Python 侧日志(loguru)----
|
||||
|
||||
_级别映射 = {
|
||||
"trace": "TRACE",
|
||||
"debug": "DEBUG",
|
||||
"info": "INFO",
|
||||
"warn": "WARNING",
|
||||
"error": "ERROR",
|
||||
"off": "OFF",
|
||||
}
|
||||
_有效级别 = frozenset(_级别映射.keys())
|
||||
_当前日志级别 = "info"
|
||||
|
||||
|
||||
def set_log_level(level: str):
|
||||
"""设置 Python 侧日志级别 (loguru)。
|
||||
|
||||
:param level: 日志级别,不区分大小写 (trace / debug / info / warn / error / off)
|
||||
"""
|
||||
global _当前日志级别
|
||||
_level = level.lower()
|
||||
if _level not in _有效级别:
|
||||
raise ValueError(f"无效日志级别 '{level}',有效值: {', '.join(sorted(_有效级别))}")
|
||||
|
||||
_当前日志级别 = _level
|
||||
_loguru_level = _级别映射[_level]
|
||||
|
||||
try:
|
||||
_logger.remove(0)
|
||||
except ValueError:
|
||||
pass
|
||||
if _loguru_level != "OFF":
|
||||
_logger.add(_sys.stderr, level=_loguru_level)
|
||||
|
||||
|
||||
def get_log_level() -> str:
|
||||
"""获取 Python 侧当前日志级别。
|
||||
|
||||
:return: 日志级别字符串 (trace / debug / info / warn / error / off)
|
||||
"""
|
||||
return _当前日志级别
|
||||
|
||||
|
||||
set_log_level("error")
|
||||
|
||||
|
||||
# ---- Rust 侧日志(tracing)----
|
||||
|
||||
|
||||
def set_rs_log_level(level: str):
|
||||
"""设置 Rust 侧日志级别 (trace / debug / info / warn / error / off)
|
||||
|
||||
仅控制 Rust tracing 日志,不影响 Python loguru 日志。
|
||||
Python 侧日志通过 set_log_level() 独立控制。
|
||||
"""
|
||||
_rs_set_log_level(level)
|
||||
|
||||
|
||||
def get_rs_log_level() -> str:
|
||||
"""获取 Rust 侧日志级别"""
|
||||
return _rs_get_log_level()
|
||||
|
||||
+951
-2234
File diff suppressed because it is too large
Load Diff
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 = "2605.86"
|
||||
version = "2606.125"
|
||||
description = "缠论技术分析库 — Rust 高性能实现"
|
||||
readme = { file = "README.md", content-type = "text/markdown" }
|
||||
license = { file = "LICENSE", content-type = "text/plain" }
|
||||
@@ -26,6 +26,12 @@ classifiers = [
|
||||
"Topic :: Office/Business :: Financial :: Investment",
|
||||
]
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"termcolor>=3.0",
|
||||
"parse>=1.2",
|
||||
"loguru>=0.6",
|
||||
"backtrader==1.9.78.123",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/YuYuKunKun/chanlun.rs"
|
||||
@@ -37,3 +43,15 @@ features = ["pyo3/extension-module"]
|
||||
python-source = "."
|
||||
module-name = "chanlun._chanlun"
|
||||
manifest-path = "Cargo.toml"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
addopts = ["-v", "--tb=short", "--durations=10"]
|
||||
|
||||
[tool.ruff.lint]
|
||||
# 启用 FA (flake8-future-annotations) 和 UP (pyupgrade) 规则
|
||||
select = ["FA", "UP"]
|
||||
|
||||
# [tool.ruff.lint.flake8-future-annotations]
|
||||
# 强制在所有文件中注入 from __future__ import annotations
|
||||
# force-future-annotations = true
|
||||
+409
-707
File diff suppressed because it is too large
Load Diff
+687
-177
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+150
-43
@@ -22,9 +22,11 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
use chanlun::warn;
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::{PyDict, PyType};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
/// 缠论配置 — 控制所有分析阶段行为的参数集(共 60+ 字段,均有默认值)。
|
||||
///
|
||||
@@ -94,6 +96,8 @@ use std::collections::HashMap;
|
||||
#[pyclass(name = "缠论配置", module = "chanlun._chanlun")]
|
||||
pub struct 缠论配置Py {
|
||||
fields: HashMap<String, Py<PyAny>>,
|
||||
缓存: parking_lot::Mutex<Option<chanlun::config::缠论配置>>,
|
||||
pub(crate) 版本: AtomicU64,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
@@ -119,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>> {
|
||||
@@ -134,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!(
|
||||
@@ -168,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())
|
||||
}
|
||||
@@ -213,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]
|
||||
@@ -227,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]
|
||||
@@ -239,7 +266,7 @@ impl 缠论配置Py {
|
||||
) -> PyResult<Py<PyDict>> {
|
||||
let py = 原始字典.py();
|
||||
let result = PyDict::new(py);
|
||||
if let Ok(default_dict) = 默认配置.downcast::<PyDict>() {
|
||||
if let Ok(default_dict) = 默认配置.cast::<PyDict>() {
|
||||
for (key, value) in default_dict.iter() {
|
||||
if 原始字典.contains(&key)? {
|
||||
result.set_item(key.clone(), 原始字典.get_item(&key)?)?;
|
||||
@@ -251,25 +278,85 @@ impl 缠论配置Py {
|
||||
Ok(result.into())
|
||||
}
|
||||
|
||||
/// 比较当前配置与另一个配置的差异
|
||||
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}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,15 +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)
|
||||
pub(crate) fn to_rust_config(
|
||||
&self,
|
||||
_py: Python<'_>,
|
||||
) -> PyResult<chanlun::config::缠论配置> {
|
||||
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),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,17 +443,17 @@ fn dict_to_rust_config(
|
||||
|
||||
// 用默认值做基准,只合并类型匹配的字段
|
||||
let mut merged = default_json.clone();
|
||||
if let serde_json::Value::Object(ref input_map) = value {
|
||||
if let serde_json::Value::Object(ref default_map) = default_json {
|
||||
for (key, input_val) in input_map {
|
||||
if let Some(default_val) = default_map.get(key) {
|
||||
match validate_field(key, input_val, default_val) {
|
||||
Ok(()) => {
|
||||
merged[key] = input_val.clone();
|
||||
}
|
||||
Err(msg) => {
|
||||
eprintln!("\x1b[33m[配置警告]\x1b[m {key}: {msg},已使用默认值 {default_val}");
|
||||
}
|
||||
if let serde_json::Value::Object(ref input_map) = value
|
||||
&& let serde_json::Value::Object(ref default_map) = default_json
|
||||
{
|
||||
for (key, input_val) in input_map {
|
||||
if let Some(default_val) = default_map.get(key) {
|
||||
match validate_field(key, input_val, default_val) {
|
||||
Ok(()) => {
|
||||
merged[key] = input_val.clone();
|
||||
}
|
||||
Err(msg) => {
|
||||
warn!("[配置警告] {key}: {msg},已使用默认值 {default_val}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -388,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(());
|
||||
}
|
||||
|
||||
// 字符串字段:检查有效值白名单
|
||||
@@ -409,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(()),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -425,7 +530,9 @@ fn validate_field(
|
||||
Value::Bool(_) => "布尔",
|
||||
Value::Number(_) => "数值",
|
||||
Value::String(_) => "字符串",
|
||||
_ => "其他",
|
||||
Value::Array(_) => "数组",
|
||||
Value::Object(_) => "字典",
|
||||
Value::Null => "null",
|
||||
};
|
||||
Err(format!("类型不匹配(需要 {expected},收到 {type_name})"))
|
||||
}
|
||||
@@ -449,10 +556,10 @@ fn coerce_strings_to_numbers(value: &mut serde_json::Value) {
|
||||
if let Ok(n) = cloned.parse::<i64>() {
|
||||
*value = serde_json::Value::Number(serde_json::Number::from(n));
|
||||
} else if let Ok(n) = cloned.parse::<f64>() {
|
||||
if n.is_finite() {
|
||||
if let Some(num) = serde_json::Number::from_f64(n) {
|
||||
*value = serde_json::Value::Number(num);
|
||||
}
|
||||
if n.is_finite()
|
||||
&& let Some(num) = serde_json::Number::from_f64(n)
|
||||
{
|
||||
*value = serde_json::Value::Number(num);
|
||||
}
|
||||
} else if cloned.eq_ignore_ascii_case("true") {
|
||||
*value = serde_json::Value::Bool(true);
|
||||
@@ -491,10 +598,10 @@ fn coerce_py_value(value: &Bound<'_, PyAny>) -> PyResult<Py<PyAny>> {
|
||||
if let Ok(n) = lower.parse::<i64>() {
|
||||
return Ok(n.into_pyobject(py)?.into_any().unbind());
|
||||
}
|
||||
if let Ok(n) = lower.parse::<f64>() {
|
||||
if n.is_finite() {
|
||||
return Ok(n.into_pyobject(py)?.into_any().unbind());
|
||||
}
|
||||
if let Ok(n) = lower.parse::<f64>()
|
||||
&& n.is_finite()
|
||||
{
|
||||
return Ok(n.into_pyobject(py)?.into_any().unbind());
|
||||
}
|
||||
|
||||
Ok(value.clone().unbind())
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::PyType;
|
||||
use std::sync::Arc;
|
||||
|
||||
// ========== 平滑异同移动平均线 ==========
|
||||
|
||||
@@ -289,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 {
|
||||
@@ -469,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> {
|
||||
@@ -603,14 +604,249 @@ impl 随机指标Py {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 布林带 ==========
|
||||
|
||||
/// 布林带(BOLL)— 基于移动平均和标准差的波动率通道。
|
||||
///
|
||||
/// 属性:
|
||||
/// 时间戳: int / 周期: int / 标准差倍数: float
|
||||
/// 上轨: float — 中轨 + 标准差倍数 * 标准差
|
||||
/// 中轨: float — 移动平均线
|
||||
/// 下轨: float — 中轨 - 标准差倍数 * 标准差
|
||||
///
|
||||
/// 方法(均为 classmethod,直接构造实例):
|
||||
/// 首次计算(时间戳, 价格, 周期=20, 标准差倍数=2.0) -> 布林带
|
||||
/// 增量计算(前一个布林带, 时间戳, 价格) -> 布林带
|
||||
#[pyclass(name = "布林带", module = "chanlun._chanlun", from_py_object)]
|
||||
#[derive(Clone)]
|
||||
pub struct 布林带Py {
|
||||
pub(crate) inner: chanlun::indicators::布林带,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl 布林带Py {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
unimplemented!("使用 首次计算 或 增量计算 创建")
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn 时间戳(&self) -> i64 {
|
||||
self.inner.时间戳
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn 周期(&self) -> usize {
|
||||
self.inner.周期
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn 标准差倍数(&self) -> f64 {
|
||||
self.inner.标准差倍数
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn 上轨(&self) -> f64 {
|
||||
self.inner.上轨
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn 中轨(&self) -> f64 {
|
||||
self.inner.中轨
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn 下轨(&self) -> f64 {
|
||||
self.inner.下轨
|
||||
}
|
||||
|
||||
fn __str__(&self) -> String {
|
||||
format!(
|
||||
"布林带(上={:.2}, 中={:.2}, 下={:.2})",
|
||||
self.inner.上轨, self.inner.中轨, self.inner.下轨
|
||||
)
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
self.__str__()
|
||||
}
|
||||
|
||||
#[classmethod]
|
||||
#[pyo3(signature = (k线, 计算方式, 周期 = 20, 标准差倍数 = 2.0))]
|
||||
fn 首次计算(
|
||||
_cls: &Bound<'_, PyType>,
|
||||
k线: &Bound<'_, PyAny>,
|
||||
计算方式: &str,
|
||||
周期: usize,
|
||||
标准差倍数: f64,
|
||||
) -> PyResult<Self> {
|
||||
let 价格 = K线取值(k线, 计算方式)?;
|
||||
let 时间戳 = 获取时间戳(k线)?;
|
||||
Ok(Self {
|
||||
inner: chanlun::indicators::布林带::首次计算(时间戳, 价格, 周期, 标准差倍数),
|
||||
})
|
||||
}
|
||||
|
||||
#[classmethod]
|
||||
fn 增量计算(
|
||||
_cls: &Bound<'_, PyType>,
|
||||
前一个布林带: &Bound<'_, 布林带Py>,
|
||||
当前K线: &Bound<'_, PyAny>,
|
||||
计算方式: &str,
|
||||
) -> PyResult<Self> {
|
||||
let 价格 = K线取值(当前K线, 计算方式)?;
|
||||
let 时间戳 = 获取时间戳(当前K线)?;
|
||||
Ok(Self {
|
||||
inner: chanlun::indicators::布林带::增量计算(
|
||||
&前一个布林带.borrow().inner,
|
||||
时间戳,
|
||||
价格,
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 指标容器 ==========
|
||||
|
||||
/// 指标容器 — 挂载在每根 K线上,基于注册表模式持有该时刻所有指标快照。
|
||||
///
|
||||
/// 与 Python `指标容器` 保持一致:
|
||||
/// - 默认名称:"macd"/"rsi"/"kdj"/"boll" → 对应指标对象
|
||||
/// - 多参数变体:key 格式 "MACD_{快}_{慢}_{信号}" / "RSI_{周期}" 等
|
||||
/// - 均线组:通过 "均线" 获取 dict[str, float]
|
||||
/// - 单值指标:通过 "单值" 获取 dict[str, float]
|
||||
#[pyclass(name = "指标容器", module = "chanlun._chanlun", skip_from_py_object)]
|
||||
#[derive(Clone)]
|
||||
pub struct 指标容器Py {
|
||||
pub(crate) inner: chanlun::indicators::指标容器,
|
||||
}
|
||||
|
||||
/// 将 Rust 指标值 转换为 Python 对象
|
||||
fn 指标值_to_py(value: &chanlun::indicators::指标值, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
use chanlun::indicators::指标值;
|
||||
match value {
|
||||
指标值::MACD(m) => {
|
||||
Ok(Py::new(py, 平滑异同移动平均线Py { inner: m.clone() })?.into_any())
|
||||
}
|
||||
指标值::RSI(r) => Ok(Py::new(py, 相对强弱指数Py { inner: r.clone() })?.into_any()),
|
||||
指标值::KDJ(k) => Ok(Py::new(py, 随机指标Py { inner: k.clone() })?.into_any()),
|
||||
指标值::BOLL(b) => Ok(Py::new(py, 布林带Py { inner: b.clone() })?.into_any()),
|
||||
指标值::均线(map) | 指标值::单值(map) => {
|
||||
let dict = pyo3::types::PyDict::new(py);
|
||||
for (k, v) in map {
|
||||
dict.set_item(k, *v)?;
|
||||
}
|
||||
Ok(dict.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl 指标容器Py {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
inner: chanlun::indicators::指标容器::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 按名称获取指标值
|
||||
fn 获取(&self, 名称: &str, py: Python<'_>) -> PyResult<Option<Py<PyAny>>> {
|
||||
match self.inner.获取(名称) {
|
||||
Some(v) => 指标值_to_py(v, py).map(Some),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// 按名称设置指标值(仅支持 MACD/RSI/KDJ/BOLL 四种类型)
|
||||
#[pyo3(signature = (名称, 值))]
|
||||
fn 设置(&mut self, 名称: &str, 值: &Bound<'_, PyAny>) -> PyResult<()> {
|
||||
use chanlun::indicators::指标值;
|
||||
if let Ok(m) = 值.cast::<平滑异同移动平均线Py>() {
|
||||
self.inner
|
||||
.设置(名称, 指标值::MACD(m.borrow().inner.clone()));
|
||||
return Ok(());
|
||||
}
|
||||
if let Ok(r) = 值.cast::<相对强弱指数Py>() {
|
||||
self.inner.设置(名称, 指标值::RSI(r.borrow().inner.clone()));
|
||||
return Ok(());
|
||||
}
|
||||
if let Ok(k) = 值.cast::<随机指标Py>() {
|
||||
self.inner.设置(名称, 指标值::KDJ(k.borrow().inner.clone()));
|
||||
return Ok(());
|
||||
}
|
||||
if let Ok(b) = 值.cast::<布林带Py>() {
|
||||
self.inner
|
||||
.设置(名称, 指标值::BOLL(b.borrow().inner.clone()));
|
||||
return Ok(());
|
||||
}
|
||||
Err(pyo3::exceptions::PyTypeError::new_err(
|
||||
"不支持的类型,仅支持 MACD/RSI/KDJ/BOLL 指标",
|
||||
))
|
||||
}
|
||||
|
||||
/// 检查是否包含指定名称的指标
|
||||
fn 包含(&self, 名称: &str) -> bool {
|
||||
self.inner.包含(名称)
|
||||
}
|
||||
|
||||
/// 返回所有已注册的指标名称
|
||||
fn keys(&self) -> Vec<String> {
|
||||
self.inner._数据.keys().cloned().collect()
|
||||
}
|
||||
|
||||
fn __getitem__(&self, 名称: &str, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
match self.inner.获取(名称) {
|
||||
Some(v) => 指标值_to_py(v, py),
|
||||
None => Err(pyo3::exceptions::PyKeyError::new_err(format!(
|
||||
"指标 '{}' 不存在",
|
||||
名称
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn __getattr__(&self, 名称: &str, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
if 名称 == "_数据" {
|
||||
// 返回内部数据字典的 Python 表示
|
||||
let dict = pyo3::types::PyDict::new(py);
|
||||
for key in self.inner._数据.keys() {
|
||||
if let Some(v) = self.inner.获取(key) {
|
||||
dict.set_item(key, 指标值_to_py(v, py)?)?;
|
||||
} else {
|
||||
dict.set_item(key, py.None())?;
|
||||
}
|
||||
}
|
||||
return Ok(dict.into());
|
||||
}
|
||||
match self.inner.获取(名称) {
|
||||
Some(v) => 指标值_to_py(v, py),
|
||||
None => Err(pyo3::exceptions::PyAttributeError::new_err(format!(
|
||||
"指标 '{}' 不存在于 指标容器 中",
|
||||
名称
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn __contains__(&self, 名称: &str) -> bool {
|
||||
self.包含(名称)
|
||||
}
|
||||
|
||||
fn __str__(&self) -> String {
|
||||
self.inner.to_string()
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
self.__str__()
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 指标 (static namespace) ==========
|
||||
|
||||
/// 指标 — 静态工具类,提供指标计算的辅助方法。
|
||||
///
|
||||
/// 方法:
|
||||
/// K线取值(k线, 指标计算方式) -> float (classmethod)
|
||||
/// 根据计算方式从K线提取数值。
|
||||
/// 计算方式: "收盘价" / "开盘价" / "高" / "低" / "均值" 等
|
||||
/// :meth:`K线取值` — 根据计算方式从K线提取数值
|
||||
/// (计算方式: "开"/"高"/"低"/"收"/"高低均值"/"高低收均值"/"开高低收均值")
|
||||
#[pyclass(name = "指标", module = "chanlun._chanlun")]
|
||||
pub struct 指标Py;
|
||||
|
||||
@@ -627,6 +863,139 @@ impl 指标Py {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 均线工具 ==========
|
||||
|
||||
/// 均线工具 — 增量 SMA/EMA 计算的静态方法容器。
|
||||
///
|
||||
/// 方法:
|
||||
/// :meth:`增量SMA` — 基于前一根K线的 SMA 值,增量计算当前 SMA
|
||||
/// :meth:`增量EMA` — 用前一根K线的 EMA 值递推计算当前 EMA
|
||||
#[pyclass(name = "均线工具", module = "chanlun._chanlun")]
|
||||
pub struct 均线工具Py;
|
||||
|
||||
#[pymethods]
|
||||
impl 均线工具Py {
|
||||
/// 基于前一根K线的 SMA 值,增量计算当前 SMA
|
||||
#[staticmethod]
|
||||
#[pyo3(signature = (普K序列, period, 计算方式))]
|
||||
fn 增量SMA(
|
||||
普K序列: Vec<Py<crate::kline_py::K线Py>>,
|
||||
period: i64,
|
||||
计算方式: &str,
|
||||
py: Python<'_>,
|
||||
) -> PyResult<f64> {
|
||||
if 普K序列.is_empty() {
|
||||
return Err(pyo3::exceptions::PyValueError::new_err("普K序列 不能为空"));
|
||||
}
|
||||
let n = 普K序列.len();
|
||||
// 提取所有K线值(一次性 borrow)
|
||||
let values: Vec<f64> = 普K序列
|
||||
.iter()
|
||||
.map(|k| {
|
||||
let inner = &k.bind(py).borrow().inner;
|
||||
chanlun::indicators::K线取值(
|
||||
inner.开盘价,
|
||||
inner.高,
|
||||
inner.低,
|
||||
inner.收盘价,
|
||||
计算方式,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
if n <= period as usize {
|
||||
let start = n.saturating_sub(period as usize);
|
||||
let sum: f64 = values[start..].iter().sum();
|
||||
return Ok(sum / (n.max(1)) as f64);
|
||||
}
|
||||
|
||||
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)
|
||||
.borrow()
|
||||
.inner
|
||||
.指标
|
||||
.read()
|
||||
.均线()
|
||||
.and_then(|m| m.get(&prev_key))
|
||||
.copied();
|
||||
if let Some(prev) = prev_cached {
|
||||
let 当前价 = values[n - 1];
|
||||
let oldest = values[n - period as usize - 1];
|
||||
return Ok(prev + (当前价 - oldest) / period as f64);
|
||||
}
|
||||
|
||||
// 回退:完整计算最近 period 根K线
|
||||
let sum: f64 = values[n - period as usize..].iter().sum();
|
||||
Ok(sum / period as f64)
|
||||
}
|
||||
|
||||
/// 用前一根K线的 EMA 值递推
|
||||
#[staticmethod]
|
||||
#[pyo3(signature = (普K序列, period, 计算方式, 前值 = None))]
|
||||
fn 增量EMA(
|
||||
普K序列: Vec<Py<crate::kline_py::K线Py>>,
|
||||
period: i64,
|
||||
计算方式: &str,
|
||||
前值: Option<f64>,
|
||||
py: Python<'_>,
|
||||
) -> PyResult<f64> {
|
||||
if 普K序列.is_empty() {
|
||||
return Err(pyo3::exceptions::PyValueError::new_err("普K序列 不能为空"));
|
||||
}
|
||||
let last = 普K序列.last().unwrap().bind(py).borrow();
|
||||
let 当前价 = chanlun::indicators::K线取值(
|
||||
last.inner.开盘价,
|
||||
last.inner.高,
|
||||
last.inner.低,
|
||||
last.inner.收盘价,
|
||||
计算方式,
|
||||
);
|
||||
match 前值 {
|
||||
None => Ok(当前价),
|
||||
Some(prev) => {
|
||||
let k = 2.0 / (period as f64 + 1.0);
|
||||
Ok(当前价 * k + prev * (1.0 - k))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 指标计算器 ==========
|
||||
|
||||
/// 指标计算器 — 在缠K合并之前,增量计算所有开启的指标并挂载到K线上。
|
||||
///
|
||||
/// 方法:
|
||||
/// :meth:`计算并挂载` — 增量计算所有开启的指标,将结果写入 ``当前K线.指标``
|
||||
#[pyclass(name = "指标计算器", module = "chanlun._chanlun")]
|
||||
pub struct 指标计算器Py;
|
||||
|
||||
#[pymethods]
|
||||
impl 指标计算器Py {
|
||||
/// 增量计算所有开启的指标,将结果写入 当前K线.指标
|
||||
#[staticmethod]
|
||||
fn 计算并挂载(
|
||||
_当前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)?;
|
||||
let 全序列_rust: Vec<Arc<chanlun::kline::bar::K线>> = 全序列
|
||||
.iter()
|
||||
.map(|k| k.bind(py).borrow().inner.clone())
|
||||
.collect();
|
||||
chanlun::indicators::指标计算器::计算并挂载(&全序列_rust, &config);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Helper functions ==========
|
||||
|
||||
pub(crate) fn K线取值(k线: &Bound<'_, PyAny>, 计算方式: &str) -> PyResult<f64> {
|
||||
@@ -664,6 +1033,10 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<平滑异同移动平均线Py>()?;
|
||||
m.add_class::<相对强弱指数Py>()?;
|
||||
m.add_class::<随机指标Py>()?;
|
||||
m.add_class::<布林带Py>()?;
|
||||
m.add_class::<指标容器Py>()?;
|
||||
m.add_class::<指标Py>()?;
|
||||
m.add_class::<均线工具Py>()?;
|
||||
m.add_class::<指标计算器Py>()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+144
-117
@@ -22,15 +22,18 @@
|
||||
* 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::atomic::Ordering;
|
||||
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};
|
||||
use crate::indicators_py::{
|
||||
平滑异同移动平均线Py, 指标容器Py, 相对强弱指数Py, 随机指标Py, 布林带Py,
|
||||
};
|
||||
use crate::structure_py::fractal_to_py;
|
||||
use crate::types_py::相对方向Py;
|
||||
|
||||
// ========== K线 ==========
|
||||
@@ -85,9 +88,7 @@ impl K线Py {
|
||||
开盘价,
|
||||
收盘价,
|
||||
成交量,
|
||||
macd: None,
|
||||
rsi: None,
|
||||
kdj: None,
|
||||
指标: RwLock::new(chanlun::indicators::指标容器::new()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -139,34 +140,57 @@ impl K线Py {
|
||||
|
||||
#[getter]
|
||||
/// :return: 相对方向.向上(开盘<收盘)或 相对方向.向下(开盘>收盘)
|
||||
fn 方向(&self) -> 相对方向Py {
|
||||
相对方向Py {
|
||||
inner: self.inner.方向(),
|
||||
}
|
||||
fn 方向(&self, py: Python<'_>) -> Py<相对方向Py> {
|
||||
crate::types_py::获取相对方向单例(py, self.inner.方向())
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn macd(&self) -> Option<平滑异同移动平均线Py> {
|
||||
self.inner
|
||||
.macd
|
||||
.as_ref()
|
||||
.map(|m| 平滑异同移动平均线Py { inner: m.clone() })
|
||||
.指标
|
||||
.read()
|
||||
.macd_cloned()
|
||||
.map(|m| 平滑异同移动平均线Py { inner: m })
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn rsi(&self) -> Option<相对强弱指数Py> {
|
||||
self.inner
|
||||
.rsi
|
||||
.as_ref()
|
||||
.map(|r| 相对强弱指数Py { inner: r.clone() })
|
||||
.指标
|
||||
.read()
|
||||
.rsi_cloned()
|
||||
.map(|r| 相对强弱指数Py { inner: r })
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn kdj(&self) -> Option<随机指标Py> {
|
||||
self.inner
|
||||
.kdj
|
||||
.as_ref()
|
||||
.map(|k| 随机指标Py { inner: k.clone() })
|
||||
.指标
|
||||
.read()
|
||||
.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().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// pandas 兼容 — 返回所有字段构成的字典
|
||||
@@ -182,7 +206,7 @@ impl K线Py {
|
||||
dict.set_item("开盘价", self.开盘价())?;
|
||||
dict.set_item("收盘价", self.收盘价())?;
|
||||
dict.set_item("成交量", self.成交量())?;
|
||||
dict.set_item("方向", self.方向())?;
|
||||
dict.set_item("方向", self.方向(py))?;
|
||||
if let Some(v) = self.macd() {
|
||||
dict.set_item("macd", v)?;
|
||||
}
|
||||
@@ -192,6 +216,7 @@ impl K线Py {
|
||||
if let Some(v) = self.kdj() {
|
||||
dict.set_item("kdj", v)?;
|
||||
}
|
||||
dict.set_item("指标", self.指标())?;
|
||||
Ok(dict.into())
|
||||
}
|
||||
|
||||
@@ -325,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线 ==========
|
||||
@@ -349,60 +407,37 @@ impl K线Py {
|
||||
#[pyclass(name = "缠论K线", module = "chanlun._chanlun", from_py_object)]
|
||||
pub struct 缠论K线Py {
|
||||
pub(crate) inner: std::sync::Arc<chanlun::kline::chan_kline::缠论K线>,
|
||||
bsp_set: std::sync::RwLock<Option<Py<pyo3::types::PySet>>>,
|
||||
}
|
||||
|
||||
impl 缠论K线Py {
|
||||
pub(crate) fn from_rc(inner: std::sync::Arc<chanlun::kline::chan_kline::缠论K线>) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
bsp_set: std::sync::RwLock::new(None),
|
||||
}
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// 对象标识缓存:Rc 地址 → 规范 Python 对象
|
||||
/// 确保同一底层 Rc 指针在 Python 侧始终映射到同一 PyObject
|
||||
|
||||
static BAR_IDENTITY: RwLock<HashMap<usize, Py<K线Py>>> = RwLock::new(HashMap::new());
|
||||
|
||||
static KLINE_IDENTITY: RwLock<HashMap<usize, Py<缠论K线Py>>> = 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.with(|c| c.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.with(|c| {
|
||||
c.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.with(|c| c.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.with(|c| {
|
||||
c.write().unwrap().insert(key, obj.clone_ref(py));
|
||||
});
|
||||
crate::cache::kline_insert(py, key, &obj);
|
||||
obj
|
||||
}
|
||||
|
||||
@@ -410,7 +445,6 @@ impl Clone for 缠论K线Py {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: std::sync::Arc::clone(&self.inner),
|
||||
bsp_set: std::sync::RwLock::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -443,19 +477,16 @@ impl 缠论K线Py {
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn 方向(&self) -> 相对方向Py {
|
||||
相对方向Py {
|
||||
inner: *self.inner.方向.read().unwrap(),
|
||||
}
|
||||
fn 方向(&self, py: Python<'_>) -> Py<相对方向Py> {
|
||||
crate::types_py::获取相对方向单例(py, *self.inner.方向.read())
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn 分型(&self) -> Option<crate::types_py::分型结构Py> {
|
||||
fn 分型(&self, py: Python<'_>) -> Option<Py<crate::types_py::分型结构Py>> {
|
||||
self.inner
|
||||
.分型
|
||||
.read()
|
||||
.unwrap()
|
||||
.map(|f| crate::types_py::分型结构Py { inner: f })
|
||||
.map(|f| crate::types_py::获取分型结构单例(py, f))
|
||||
}
|
||||
|
||||
#[getter]
|
||||
@@ -485,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 兼容 — 返回所有字段构成的字典
|
||||
@@ -496,7 +527,7 @@ impl 缠论K线Py {
|
||||
dict.set_item("时间戳", self.时间戳())?;
|
||||
dict.set_item("高", self.高())?;
|
||||
dict.set_item("低", self.低())?;
|
||||
dict.set_item("方向", self.方向())?;
|
||||
dict.set_item("方向", self.方向(py))?;
|
||||
dict.set_item("周期", self.周期())?;
|
||||
dict.set_item("标识", self.标识())?;
|
||||
dict.set_item("分型特征值", self.分型特征值())?;
|
||||
@@ -506,7 +537,7 @@ impl 缠论K线Py {
|
||||
dict.set_item("与RSI匹配", self.与RSI匹配())?;
|
||||
dict.set_item("与KDJ匹配", self.与KDJ匹配())?;
|
||||
|
||||
if let Some(v) = self.分型() {
|
||||
if let Some(v) = self.分型(py) {
|
||||
dict.set_item("分型", v)?;
|
||||
}
|
||||
Ok(dict.into())
|
||||
@@ -534,17 +565,21 @@ impl 缠论K线Py {
|
||||
#[getter]
|
||||
/// 创建当前缠K的浅拷贝副本
|
||||
fn 镜像(&self, py: Python<'_>) -> Self {
|
||||
let mut mirror = Self {
|
||||
let mirror = Self {
|
||||
inner: std::sync::Arc::new(self.inner.镜像()),
|
||||
bsp_set: std::sync::RwLock::new(None),
|
||||
};
|
||||
if let Some(ref src_set) = *self.bsp_set.read().unwrap() {
|
||||
if let Ok(new_set) = pyo3::types::PySet::empty(py) {
|
||||
for item in src_set.bind(py).iter() {
|
||||
let _ = new_set.add(item);
|
||||
}
|
||||
mirror.bsp_set = std::sync::RwLock::new(Some(new_set.into()));
|
||||
// 复制买卖点信息到镜像
|
||||
let src_key = Arc::as_ptr(&self.inner) as usize;
|
||||
let dst_key = Arc::as_ptr(&mirror.inner) as usize;
|
||||
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)
|
||||
{
|
||||
for item in cached_src.bind(py).iter() {
|
||||
let _ = new_set.add(item);
|
||||
}
|
||||
let py_set: Py<pyo3::types::PySet> = new_set.into();
|
||||
crate::cache::bsp_insert(py, dst_key, py_set);
|
||||
}
|
||||
mirror
|
||||
}
|
||||
@@ -569,21 +604,21 @@ impl 缠论K线Py {
|
||||
|
||||
#[getter]
|
||||
fn 买卖点信息(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
if self.bsp_set.read().unwrap().is_none() {
|
||||
let set = pyo3::types::PySet::empty(py)?;
|
||||
for s in self.inner.买卖点信息.read().unwrap().iter() {
|
||||
set.add(s.clone())?;
|
||||
}
|
||||
*self.bsp_set.write().unwrap() = Some(set.into());
|
||||
let key = Arc::as_ptr(&self.inner) as usize;
|
||||
// 检查全局缓存
|
||||
let cached = crate::cache::bsp_get(py, key);
|
||||
if let Some(set) = cached {
|
||||
return Ok(set.into_any());
|
||||
}
|
||||
Ok(self
|
||||
.bsp_set
|
||||
.read()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.clone_ref(py)
|
||||
.into_any())
|
||||
// 创建新的 PySet,从 Rust HashSet 同步已有内容
|
||||
let set = pyo3::types::PySet::empty(py)?;
|
||||
let bsp_info = self.inner.买卖点信息.read();
|
||||
for item in bsp_info.iter() {
|
||||
set.add(item.as_str())?;
|
||||
}
|
||||
drop(bsp_info);
|
||||
crate::cache::bsp_insert(py, key, set.into());
|
||||
Ok(crate::cache::bsp_get(py, key).unwrap().into_any())
|
||||
}
|
||||
|
||||
#[classmethod]
|
||||
@@ -631,59 +666,51 @@ impl 缠论K线Py {
|
||||
chan_kline_to_py(py, std::sync::Arc::new(inner))
|
||||
}
|
||||
|
||||
#[classmethod]
|
||||
/// K线包含处理(合并)
|
||||
fn 兼并(
|
||||
_cls: &Bound<'_, PyType>,
|
||||
之前缠K: Option<&Bound<'_, Self>>,
|
||||
当前缠K: &Bound<'_, Self>,
|
||||
当前普K: &Bound<'_, K线Py>,
|
||||
配置: &Bound<'_, 缠论配置Py>,
|
||||
py: Python<'_>,
|
||||
) -> PyResult<(Option<Py<Self>>, Option<String>)> {
|
||||
let mut ck_inner = (*当前缠K.borrow().inner).clone();
|
||||
let config = 配置.borrow().to_rust_config(py)?;
|
||||
let prev_ref = 之前缠K.map(|prev| prev.borrow());
|
||||
let prev_inner = prev_ref.as_ref().map(|r| r.inner.as_ref());
|
||||
let (result, mode) = chanlun::kline::chan_kline::缠论K线::兼并(
|
||||
prev_inner,
|
||||
&mut ck_inner,
|
||||
&当前普K.borrow().inner,
|
||||
&config,
|
||||
);
|
||||
Ok((result.map(|rc| chan_kline_to_py(py, rc)), mode))
|
||||
}
|
||||
|
||||
#[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线::分析(
|
||||
let (status, fractal) = chanlun::kline::chan_kline::缠论K线::分析(
|
||||
ck_inner,
|
||||
&mut ck_seq,
|
||||
&mut bar_seq,
|
||||
&config,
|
||||
);
|
||||
|
||||
Ok((status, None))
|
||||
// 写回 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())))
|
||||
}
|
||||
|
||||
#[staticmethod]
|
||||
|
||||
+236
-29
@@ -22,22 +22,224 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#![allow(non_snake_case, clippy::too_many_arguments)]
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
use std::sync::{Mutex, Once, OnceLock};
|
||||
|
||||
/// 日志级别: 0=trace, 1=debug, 2=info, 3=warn, 4=error, 5=off
|
||||
static LOG_LEVEL: AtomicU8 = AtomicU8::new(2); // 默认 info
|
||||
|
||||
type 过滤器句柄 =
|
||||
tracing_subscriber::reload::Handle<tracing_subscriber::EnvFilter, tracing_subscriber::Registry>;
|
||||
static 过滤器句柄锁: OnceLock<Mutex<过滤器句柄>> = OnceLock::new();
|
||||
static TRACING_INIT: Once = Once::new();
|
||||
|
||||
fn 级别数字转名称(n: u8) -> &'static str {
|
||||
match n {
|
||||
0 => "trace",
|
||||
1 => "debug",
|
||||
2 => "info",
|
||||
3 => "warn",
|
||||
4 => "error",
|
||||
5 => "off",
|
||||
_ => "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
fn 级别名称转数字(name: &str) -> Option<u8> {
|
||||
match name.to_lowercase().as_str() {
|
||||
"trace" => Some(0),
|
||||
"debug" => Some(1),
|
||||
"info" => Some(2),
|
||||
"warn" => Some(3),
|
||||
"error" => Some(4),
|
||||
"off" => Some(5),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn init_tracing() {
|
||||
TRACING_INIT.call_once(|| {
|
||||
use chrono::Local;
|
||||
use std::fmt;
|
||||
use tracing_subscriber::fmt::format::Format;
|
||||
use tracing_subscriber::fmt::format::Writer;
|
||||
use tracing_subscriber::fmt::time::FormatTime;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
|
||||
struct 本地时间;
|
||||
impl FormatTime for 本地时间 {
|
||||
fn format_time(&self, w: &mut Writer<'_>) -> fmt::Result {
|
||||
write!(w, "{}", Local::now().format("%Y-%m-%d %H:%M:%S%.3f"))
|
||||
}
|
||||
}
|
||||
|
||||
let format = Format::default()
|
||||
.with_timer(本地时间)
|
||||
.with_target(false)
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_ansi(true)
|
||||
.compact();
|
||||
|
||||
let 初始级别 = tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
|
||||
|
||||
let (过滤器层, 句柄) = tracing_subscriber::reload::Layer::new(初始级别);
|
||||
过滤器句柄锁
|
||||
.set(Mutex::new(句柄))
|
||||
.expect("过滤器句柄锁只能设置一次");
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(过滤器层)
|
||||
.with(tracing_subscriber::fmt::layer().event_format(format))
|
||||
.init();
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
/// 分型模式 — True 时使用构造时缓存值,False 时从 中 缠K 实时读取
|
||||
#[pyfunction]
|
||||
fn get_分型模式() -> bool {
|
||||
chanlun::structure::fractal_obj::分型模式.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// 设置 分型模式
|
||||
#[pyfunction]
|
||||
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))
|
||||
}
|
||||
|
||||
/// 设置日志级别 — 自动启用日志,同步更新 tracing subscriber
|
||||
#[pyfunction]
|
||||
fn set_log_level(level: &str) -> PyResult<()> {
|
||||
let 数字 = 级别名称转数字(level).ok_or_else(|| {
|
||||
pyo3::exceptions::PyValueError::new_err(format!(
|
||||
"无效日志级别 '{}',有效值: trace, debug, info, warn, error, off",
|
||||
level
|
||||
))
|
||||
})?;
|
||||
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<()> {
|
||||
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: 技术指标
|
||||
@@ -50,45 +252,50 @@ 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(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::*;
|
||||
use pyo3::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn test_rc_pointer_across_getters() {
|
||||
pyo3::prepare_freethreaded_python();
|
||||
Python::with_gil(|py| {
|
||||
fn test_分型模式_get_set() {
|
||||
// 手动初始化 Python 解释器(cargo test 环境下 auto-initialize 不一定生效)
|
||||
unsafe {
|
||||
if pyo3::ffi::Py_IsInitialized() == 0 {
|
||||
pyo3::ffi::Py_Initialize();
|
||||
}
|
||||
}
|
||||
pyo3::Python::try_attach(|py| {
|
||||
let module = PyModule::new(py, "test_module").unwrap();
|
||||
module.add_class::<business_py::观察者Py>().unwrap();
|
||||
module.add_class::<business_py::基础买卖点Py>().unwrap();
|
||||
module.add_class::<business_py::买卖点Py>().unwrap();
|
||||
module.add_class::<kline_py::K线Py>().unwrap();
|
||||
module.add_class::<kline_py::缠论K线Py>().unwrap();
|
||||
module.add_class::<structure_py::分型Py>().unwrap();
|
||||
module.add_class::<structure_py::虚线Py>().unwrap();
|
||||
module.add_class::<config_py::缠论配置Py>().unwrap();
|
||||
module
|
||||
.add_function(wrap_pyfunction!(get_分型模式, &module).unwrap())
|
||||
.unwrap();
|
||||
module
|
||||
.add_function(wrap_pyfunction!(set_分型模式, &module).unwrap())
|
||||
.unwrap();
|
||||
|
||||
let config = config_py::缠论配置Py::from_rust_config(&Default::default()).unwrap();
|
||||
let obs = business_py::观察者Py::new_impl("btcusd".into(), 300, config, py).unwrap();
|
||||
// 默认 true
|
||||
let getter = module.getattr("get_分型模式").unwrap();
|
||||
let result: bool = getter.call0().unwrap().extract().unwrap();
|
||||
assert!(result, "分型模式 默认应为 True");
|
||||
|
||||
// Feed one K line
|
||||
let kline = kline_py::K线Py::new_impl(
|
||||
"btcusd".into(),
|
||||
1000,
|
||||
100.0,
|
||||
105.0,
|
||||
99.0,
|
||||
103.0,
|
||||
1000.0,
|
||||
0,
|
||||
300,
|
||||
);
|
||||
let kline_ref = kline.into_ref(py);
|
||||
// ... this is too complex
|
||||
});
|
||||
// 设置为 false
|
||||
let setter = module.getattr("set_分型模式").unwrap();
|
||||
setter.call1((false,)).unwrap();
|
||||
let result: bool = getter.call0().unwrap().extract().unwrap();
|
||||
assert!(!result, "分型模式 应为 False");
|
||||
|
||||
// 恢复 true
|
||||
setter.call1((true,)).unwrap();
|
||||
let result: bool = getter.call0().unwrap().extract().unwrap();
|
||||
assert!(result, "分型模式 应为 True");
|
||||
})
|
||||
.expect("Python 解释器初始化后 attach 仍失败");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
+94
-351
@@ -24,42 +24,27 @@
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::{PyDict, PyType};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use crate::algorithm_py::{hub_to_py, 中枢Py};
|
||||
use crate::algorithm_py::hub_to_py;
|
||||
use crate::config_py::缠论配置Py;
|
||||
use crate::kline_py::{缠论K线Py, K线Py};
|
||||
use crate::kline_py::{K线Py, bar_to_py, 缠论K线Py};
|
||||
|
||||
// ---- 身份缓存 (弱引用:通过 refcnt 检测存活,仅缓存持有则视为过期) ----
|
||||
|
||||
thread_local! {
|
||||
static FRACTAL_IDENTITY: RwLock<HashMap<usize, Py<分型Py>>> = RwLock::new(HashMap::new());
|
||||
static DASHED_IDENTITY: RwLock<HashMap<usize, Py<虚线Py>>> = RwLock::new(HashMap::new());
|
||||
static SEGFEAT_IDENTITY: RwLock<HashMap<usize, Py<线段特征Py>>> = RwLock::new(HashMap::new());
|
||||
static FEATFRAC_IDENTITY: RwLock<HashMap<usize, Py<特征分型Py>>> = 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.with(|c| c.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.with(|c| {
|
||||
c.write().unwrap().retain(|_, v| v.get_refcnt(py) > 1);
|
||||
});
|
||||
let obj = Py::new(py, 分型Py { inner }).unwrap();
|
||||
FRACTAL_IDENTITY.with(|c| {
|
||||
c.write().unwrap().insert(key, obj.clone_ref(py));
|
||||
});
|
||||
crate::cache::fractal_insert(py, key, &obj);
|
||||
obj
|
||||
}
|
||||
|
||||
@@ -68,18 +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.with(|c| c.read().unwrap().get(&key).map(|p| p.clone_ref(py)))
|
||||
{
|
||||
if let Some(cached) = crate::cache::dashed_get(py, key) {
|
||||
return cached;
|
||||
}
|
||||
DASHED_IDENTITY.with(|c| {
|
||||
c.write().unwrap().retain(|_, v| v.get_refcnt(py) > 1);
|
||||
});
|
||||
let obj = Py::new(py, 虚线Py { inner }).unwrap();
|
||||
DASHED_IDENTITY.with(|c| {
|
||||
c.write().unwrap().insert(key, obj.clone_ref(py));
|
||||
});
|
||||
crate::cache::dashed_insert(py, key, &obj);
|
||||
obj
|
||||
}
|
||||
|
||||
@@ -87,41 +65,9 @@ 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.with(|c| c.read().unwrap().get(&key).map(|p| p.clone_ref(py)))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
SEGFEAT_IDENTITY.with(|c| {
|
||||
c.write().unwrap().retain(|_, v| v.get_refcnt(py) > 1);
|
||||
});
|
||||
let obj = Py::new(py, 线段特征Py { inner }).unwrap();
|
||||
SEGFEAT_IDENTITY.with(|c| {
|
||||
c.write().unwrap().insert(key, obj.clone_ref(py));
|
||||
});
|
||||
obj
|
||||
Py::new(py, 线段特征Py { inner }).unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn featfrac_to_py(
|
||||
py: Python<'_>,
|
||||
inner: Arc<chanlun::structure::feat_fractal::特征分型>,
|
||||
) -> Py<特征分型Py> {
|
||||
let key = Arc::as_ptr(&inner) as usize;
|
||||
if let Some(cached) =
|
||||
FEATFRAC_IDENTITY.with(|c| c.read().unwrap().get(&key).map(|p| p.clone_ref(py)))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
FEATFRAC_IDENTITY.with(|c| {
|
||||
c.write().unwrap().retain(|_, v| v.get_refcnt(py) > 1);
|
||||
});
|
||||
let obj = Py::new(py, 特征分型Py { inner }).unwrap();
|
||||
FEATFRAC_IDENTITY.with(|c| {
|
||||
c.write().unwrap().insert(key, obj.clone_ref(py));
|
||||
});
|
||||
obj
|
||||
}
|
||||
use crate::types_py::{分型结构Py, 相对方向Py, 缺口Py};
|
||||
|
||||
// ========== 分型 ==========
|
||||
@@ -184,19 +130,35 @@ impl 分型Py {
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn 结构(&self) -> 分型结构Py {
|
||||
分型结构Py {
|
||||
inner: self.inner.结构,
|
||||
}
|
||||
fn 结构(&self, py: Python<'_>) -> Py<分型结构Py> {
|
||||
crate::types_py::获取分型结构单例(py, self.inner.结构())
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn 时间戳(&self) -> i64 {
|
||||
self.inner.时间戳
|
||||
self.inner.时间戳()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn 分型特征值(&self) -> f64 {
|
||||
self.inner.分型特征值()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
/// 构造时缓存的 _结构(不受 分型模式 影响)
|
||||
fn _结构(&self, py: Python<'_>) -> Py<分型结构Py> {
|
||||
crate::types_py::获取分型结构单例(py, self.inner.结构)
|
||||
}
|
||||
|
||||
#[getter]
|
||||
/// 构造时缓存的 _时间戳(不受 分型模式 影响)
|
||||
fn _时间戳(&self) -> i64 {
|
||||
self.inner.时间戳
|
||||
}
|
||||
|
||||
#[getter]
|
||||
/// 构造时缓存的 _分型特征值(不受 分型模式 影响)
|
||||
fn _分型特征值(&self) -> f64 {
|
||||
self.inner.分型特征值
|
||||
}
|
||||
|
||||
@@ -221,12 +183,15 @@ impl 分型Py {
|
||||
|
||||
#[getter]
|
||||
/// 左、中、右三对相对方向关系
|
||||
fn 关系组(&self) -> Option<(相对方向Py, 相对方向Py, 相对方向Py)> {
|
||||
fn 关系组(
|
||||
&self,
|
||||
py: Python<'_>,
|
||||
) -> Option<(Py<相对方向Py>, Py<相对方向Py>, Py<相对方向Py>)> {
|
||||
self.inner.关系组().map(|(a, b, c)| {
|
||||
(
|
||||
相对方向Py { inner: a },
|
||||
相对方向Py { inner: b },
|
||||
相对方向Py { inner: c },
|
||||
crate::types_py::获取相对方向单例(py, a),
|
||||
crate::types_py::获取相对方向单例(py, b),
|
||||
crate::types_py::获取相对方向单例(py, c),
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -247,7 +212,7 @@ impl 分型Py {
|
||||
#[getter]
|
||||
fn __dict__(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
|
||||
let dict = PyDict::new(py);
|
||||
dict.set_item("结构", self.结构())?;
|
||||
dict.set_item("结构", self.结构(py))?;
|
||||
dict.set_item("时间戳", self.时间戳())?;
|
||||
dict.set_item("分型特征值", self.分型特征值())?;
|
||||
dict.set_item("强度", self.强度())?;
|
||||
@@ -259,7 +224,7 @@ impl 分型Py {
|
||||
if let Some(v) = self.右(py) {
|
||||
dict.set_item("右", v)?;
|
||||
}
|
||||
if let Some(v) = self.关系组() {
|
||||
if let Some(v) = self.关系组(py) {
|
||||
dict.set_item("关系组", v)?;
|
||||
}
|
||||
Ok(dict.into())
|
||||
@@ -377,7 +342,7 @@ impl 虚线Py {
|
||||
|
||||
#[getter]
|
||||
fn 标识(&self) -> String {
|
||||
self.inner.标识.read().unwrap().clone()
|
||||
self.inner.标识.read().clone()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
@@ -397,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]
|
||||
@@ -407,18 +372,23 @@ impl 虚线Py {
|
||||
|
||||
#[getter]
|
||||
fn 模式(&self) -> String {
|
||||
self.inner.模式.read().unwrap().clone()
|
||||
self.inner.模式.read().clone()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn _特征序列_显示(&self) -> bool {
|
||||
#[getter(_特征序列_显示)]
|
||||
fn get_特征序列_显示(&self) -> bool {
|
||||
self.inner._特征序列_显示.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
#[setter(_特征序列_显示)]
|
||||
fn set_特征序列_显示(&mut self, value: bool) {
|
||||
self.inner._特征序列_显示.store(value, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
#[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 => {
|
||||
@@ -439,30 +409,22 @@ 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]
|
||||
fn 前一结束位置(&self) -> Option<Self> {
|
||||
fn 前一结束位置(&self, py: Python<'_>) -> Option<Py<虚线Py>> {
|
||||
self.inner
|
||||
.前一结束位置
|
||||
.read()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.map(|d| Self {
|
||||
inner: Arc::clone(d),
|
||||
})
|
||||
.map(|d| dashed_to_py(py, Arc::clone(d)))
|
||||
}
|
||||
|
||||
// ---- 序列 getters ----
|
||||
@@ -470,13 +432,8 @@ 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() {
|
||||
list.append(Py::new(
|
||||
py,
|
||||
Self {
|
||||
inner: Arc::clone(d),
|
||||
},
|
||||
)?)?;
|
||||
for d in self.inner.基础序列.read().iter() {
|
||||
list.append(dashed_to_py(py, Arc::clone(d)))?;
|
||||
}
|
||||
Ok(list.into())
|
||||
}
|
||||
@@ -484,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())
|
||||
@@ -493,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())
|
||||
@@ -502,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())
|
||||
@@ -514,13 +471,8 @@ impl 虚线Py {
|
||||
/// 笔序列
|
||||
fn 笔序列(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
let list = pyo3::types::PyList::empty(py);
|
||||
for d in self.inner.基础序列.read().unwrap().iter() {
|
||||
list.append(Py::new(
|
||||
py,
|
||||
Self {
|
||||
inner: Arc::clone(d),
|
||||
},
|
||||
)?)?;
|
||||
for d in self.inner.基础序列.read().iter() {
|
||||
list.append(dashed_to_py(py, Arc::clone(d)))?;
|
||||
}
|
||||
Ok(list.into())
|
||||
}
|
||||
@@ -533,10 +485,8 @@ impl 虚线Py {
|
||||
|
||||
#[getter]
|
||||
/// :return: 运行方向
|
||||
fn 方向(&self) -> 相对方向Py {
|
||||
相对方向Py {
|
||||
inner: self.inner.方向(),
|
||||
}
|
||||
fn 方向(&self, py: Python<'_>) -> Py<相对方向Py> {
|
||||
crate::types_py::获取相对方向单例(py, self.inner.方向())
|
||||
}
|
||||
|
||||
#[getter]
|
||||
@@ -569,11 +519,10 @@ impl 虚线Py {
|
||||
let obs_ref = 观察员.borrow();
|
||||
let observer_inner = obs_ref.obs();
|
||||
let result = self.inner.获取普K序列(&observer_inner.普通K线序列);
|
||||
let list = pyo3::types::PyList::empty(观察员.py());
|
||||
let py = 观察员.py();
|
||||
let list = pyo3::types::PyList::empty(py);
|
||||
for k in &result {
|
||||
list.append(K线Py {
|
||||
inner: Arc::clone(k),
|
||||
})?;
|
||||
list.append(bar_to_py(py, Arc::clone(k)))?;
|
||||
}
|
||||
Ok(list.into())
|
||||
}
|
||||
@@ -960,7 +909,7 @@ impl 虚线Py {
|
||||
) -> (bool, String) {
|
||||
let obs = 观察员.borrow();
|
||||
let obs_ref = obs.obs();
|
||||
chanlun::structure::dash_line::虚线::买卖意义(&实线.borrow().inner, &*obs_ref)
|
||||
chanlun::structure::dash_line::虚线::买卖意义(&实线.borrow().inner, &obs_ref)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -974,15 +923,10 @@ impl 虚线Py {
|
||||
/// 文: 分型 — 特征序列的起点分型
|
||||
/// 武: 分型 — 特征序列的终点分型
|
||||
/// 方向: 相对方向 / 高: float / 低: float
|
||||
///
|
||||
/// 方法:
|
||||
/// 添加(虚线) — 向特征序列追加虚线元素
|
||||
/// 删除(虚线) — 从特征序列移除虚线元素
|
||||
/// 基本序列: list[虚线] (基础序列)
|
||||
///
|
||||
/// 类方法:
|
||||
/// 新建(序号, 文, 武, 基础序列?) -> 线段特征
|
||||
/// 静态分析(虚线序列, 配置) -> 线段特征|None
|
||||
/// 获取分型序列(虚线序列, 配置) -> list[线段特征]
|
||||
/// :meth:`静态分析` — 对虚线序列进行静态特征分析,返回 线段特征 列表
|
||||
#[pyclass(name = "线段特征", module = "chanlun._chanlun", from_py_object)]
|
||||
#[derive(Clone)]
|
||||
pub struct 线段特征Py {
|
||||
@@ -991,49 +935,36 @@ pub struct 线段特征Py {
|
||||
|
||||
#[pymethods]
|
||||
impl 线段特征Py {
|
||||
#[new]
|
||||
fn new(
|
||||
标识: String,
|
||||
基础序列: Vec<Py<虚线Py>>,
|
||||
线段方向: &Bound<'_, 相对方向Py>,
|
||||
py: Python<'_>,
|
||||
) -> Self {
|
||||
let rc_list: Vec<Arc<chanlun::structure::dash_line::虚线>> = 基础序列
|
||||
.iter()
|
||||
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
|
||||
.collect();
|
||||
Self {
|
||||
inner: Arc::new(chanlun::structure::segment_feat::线段特征::new(
|
||||
标识,
|
||||
rc_list,
|
||||
线段方向.borrow().inner,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
// ---- getters ----
|
||||
|
||||
#[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.标识.clone()
|
||||
self.inner.标识.read().clone()
|
||||
}
|
||||
|
||||
#[setter]
|
||||
fn set_标识(&self, value: String) {
|
||||
*self.inner.标识.write() = value;
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn 线段方向(&self) -> 相对方向Py {
|
||||
相对方向Py {
|
||||
inner: self.inner.线段方向,
|
||||
}
|
||||
fn 线段方向(&self, py: Python<'_>) -> Py<相对方向Py> {
|
||||
crate::types_py::获取相对方向单例(py, self.inner.线段方向)
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn 元素(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
fn 基础序列(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
let list = pyo3::types::PyList::empty(py);
|
||||
for d in &self.inner.元素 {
|
||||
for d in &self.inner.基础序列 {
|
||||
list.append(dashed_to_py(py, Arc::clone(d)))?;
|
||||
}
|
||||
Ok(list.into())
|
||||
@@ -1047,55 +978,6 @@ impl 线段特征Py {
|
||||
self.__str__()
|
||||
}
|
||||
|
||||
fn __len__(&self) -> usize {
|
||||
self.inner.元素.len()
|
||||
}
|
||||
|
||||
fn __getitem__(&self, index: isize, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
let len = self.inner.元素.len() as isize;
|
||||
let idx = if index < 0 { index + len } else { index };
|
||||
if idx < 0 || idx >= len {
|
||||
return Err(pyo3::exceptions::PyIndexError::new_err(format!(
|
||||
"线段特征 index {index} out of range (len={len})"
|
||||
)));
|
||||
}
|
||||
let dash = &self.inner.元素[idx as usize];
|
||||
let obj: Py<PyAny> = dashed_to_py(py, Arc::clone(dash)).into();
|
||||
Ok(obj)
|
||||
}
|
||||
|
||||
fn __iter__(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
let list = pyo3::types::PyList::empty(py);
|
||||
for d in &self.inner.元素 {
|
||||
list.append(dashed_to_py(py, Arc::clone(d)))?;
|
||||
}
|
||||
list.call_method0("__iter__").map(|iter| iter.into())
|
||||
}
|
||||
|
||||
fn __eq__(&self, other: &Bound<'_, PyAny>) -> bool {
|
||||
if let Ok(other) = other.extract::<PyRef<'_, Self>>() {
|
||||
return Arc::as_ptr(&self.inner) == Arc::as_ptr(&other.inner);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn __hash__(&self) -> u64 {
|
||||
Arc::as_ptr(&self.inner) as u64
|
||||
}
|
||||
|
||||
/// pandas 兼容 — 返回关键标量字段构成的字典
|
||||
#[getter]
|
||||
fn __dict__(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
|
||||
let dict = PyDict::new(py);
|
||||
dict.set_item("序号", self.序号())?;
|
||||
dict.set_item("标识", self.标识())?;
|
||||
dict.set_item("线段方向", self.线段方向())?;
|
||||
dict.set_item("图表标题", self.图表标题())?;
|
||||
Ok(dict.into())
|
||||
}
|
||||
|
||||
// ---- instance methods ----
|
||||
|
||||
#[getter]
|
||||
/// :return: 图表标题
|
||||
fn 图表标题(&self) -> String {
|
||||
@@ -1116,10 +998,8 @@ impl 线段特征Py {
|
||||
|
||||
#[getter]
|
||||
/// :return: 特征序列方向(线段方向的翻转)
|
||||
fn 方向(&self) -> 相对方向Py {
|
||||
相对方向Py {
|
||||
inner: self.inner.方向(),
|
||||
}
|
||||
fn 方向(&self, py: Python<'_>) -> Py<相对方向Py> {
|
||||
crate::types_py::获取相对方向单例(py, self.inner.方向())
|
||||
}
|
||||
|
||||
#[getter]
|
||||
@@ -1134,47 +1014,8 @@ impl 线段特征Py {
|
||||
self.inner.低()
|
||||
}
|
||||
|
||||
/// :param 待添加虚线: 待添加的虚线
|
||||
fn 添加(&mut self, 待添加虚线: &Bound<'_, 虚线Py>) -> PyResult<()> {
|
||||
let inner = Arc::make_mut(&mut self.inner);
|
||||
inner
|
||||
.添加(Arc::clone(&待添加虚线.borrow().inner))
|
||||
.map_err(|e| pyo3::exceptions::PyValueError::new_err(e))
|
||||
}
|
||||
|
||||
/// :param 待删除虚线: 待删除的虚线
|
||||
fn 删除(&mut self, 待删除虚线: &Bound<'_, 虚线Py>) -> PyResult<()> {
|
||||
let inner = Arc::make_mut(&mut self.inner);
|
||||
inner
|
||||
.删除(&Arc::clone(&待删除虚线.borrow().inner))
|
||||
.map_err(|e| pyo3::exceptions::PyValueError::new_err(e))
|
||||
}
|
||||
|
||||
// ---- classmethods ----
|
||||
|
||||
#[classmethod]
|
||||
/// :param 虚线序列: 基础虚线列表
|
||||
fn 新建(
|
||||
_cls: &Bound<'_, PyType>,
|
||||
虚线序列: Vec<Py<虚线Py>>,
|
||||
线段方向: &Bound<'_, 相对方向Py>,
|
||||
py: Python<'_>,
|
||||
) -> Self {
|
||||
let rc_list: Vec<Arc<chanlun::structure::dash_line::虚线>> = 虚线序列
|
||||
.iter()
|
||||
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
|
||||
.collect();
|
||||
Self {
|
||||
inner: Arc::new(chanlun::structure::segment_feat::线段特征::新建(
|
||||
rc_list,
|
||||
线段方向.borrow().inner,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[classmethod]
|
||||
#[pyo3(signature = (虚线序列, 线段方向, 四象, 是否忽视 = false))]
|
||||
/// 静态分析虚线序列,生成特征序列
|
||||
fn 静态分析(
|
||||
_cls: &Bound<'_, PyType>,
|
||||
虚线序列: Vec<Py<虚线Py>>,
|
||||
@@ -1182,119 +1023,22 @@ impl 线段特征Py {
|
||||
四象: &str,
|
||||
是否忽视: bool,
|
||||
py: Python<'_>,
|
||||
) -> Vec<Self> {
|
||||
) -> PyResult<Py<PyAny>> {
|
||||
let rc_list: Vec<Arc<chanlun::structure::dash_line::虚线>> = 虚线序列
|
||||
.iter()
|
||||
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
|
||||
.collect();
|
||||
chanlun::structure::segment_feat::线段特征::静态分析(
|
||||
let result = chanlun::structure::segment_feat::线段特征::静态分析(
|
||||
&rc_list,
|
||||
线段方向.borrow().inner,
|
||||
四象,
|
||||
是否忽视,
|
||||
)
|
||||
.into_iter()
|
||||
.map(|inner| Self { inner })
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[classmethod]
|
||||
/// 从特征序列提取特征分型序列
|
||||
fn 获取分型序列(
|
||||
_cls: &Bound<'_, PyType>,
|
||||
特征序列: Vec<Py<Self>>,
|
||||
py: Python<'_>,
|
||||
) -> Vec<Py<特征分型Py>> {
|
||||
let rc_list: Vec<Arc<chanlun::structure::segment_feat::线段特征>> = 特征序列
|
||||
.iter()
|
||||
.map(|s| Arc::clone(&s.bind(py).borrow().inner))
|
||||
.collect();
|
||||
chanlun::structure::segment_feat::线段特征::获取分型序列(&rc_list)
|
||||
.into_iter()
|
||||
.map(|inner| featfrac_to_py(py, Arc::new(inner)))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 特征分型 ==========
|
||||
|
||||
/// 特征分型 — 线段特征序列中的分型节点。
|
||||
///
|
||||
/// 属性 (只读):
|
||||
/// 左: 线段特征|None / 中: 线段特征 / 右: 线段特征|None
|
||||
/// 结构: 分型结构 — 顶/底分型判定结果
|
||||
#[pyclass(name = "特征分型", module = "chanlun._chanlun", from_py_object)]
|
||||
#[derive(Clone)]
|
||||
pub struct 特征分型Py {
|
||||
pub(crate) inner: Arc<chanlun::structure::feat_fractal::特征分型>,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl 特征分型Py {
|
||||
#[new]
|
||||
fn new(
|
||||
左: &Bound<'_, 线段特征Py>,
|
||||
中: &Bound<'_, 线段特征Py>,
|
||||
右: &Bound<'_, 线段特征Py>,
|
||||
结构: &Bound<'_, 分型结构Py>,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(chanlun::structure::feat_fractal::特征分型::new(
|
||||
Arc::clone(&左.borrow().inner),
|
||||
Arc::clone(&中.borrow().inner),
|
||||
Arc::clone(&右.borrow().inner),
|
||||
结构.borrow().inner,
|
||||
)),
|
||||
);
|
||||
let list = pyo3::types::PyList::empty(py);
|
||||
for sf in result {
|
||||
list.append(segfeat_to_py(py, sf))?;
|
||||
}
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn 左(&self, py: Python<'_>) -> Py<线段特征Py> {
|
||||
segfeat_to_py(py, Arc::clone(&self.inner.左))
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn 中(&self, py: Python<'_>) -> Py<线段特征Py> {
|
||||
segfeat_to_py(py, Arc::clone(&self.inner.中))
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn 右(&self, py: Python<'_>) -> Py<线段特征Py> {
|
||||
segfeat_to_py(py, Arc::clone(&self.inner.右))
|
||||
}
|
||||
|
||||
#[getter]
|
||||
fn 结构(&self) -> 分型结构Py {
|
||||
分型结构Py {
|
||||
inner: self.inner.结构,
|
||||
}
|
||||
}
|
||||
|
||||
/// pandas 兼容 — 返回关键标量字段构成的字典
|
||||
#[getter]
|
||||
fn __dict__(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
|
||||
let dict = PyDict::new(py);
|
||||
dict.set_item("结构", self.结构())?;
|
||||
Ok(dict.into())
|
||||
}
|
||||
|
||||
fn __str__(&self) -> String {
|
||||
format!("{}", self.inner)
|
||||
}
|
||||
|
||||
fn __repr__(&self) -> String {
|
||||
self.__str__()
|
||||
}
|
||||
|
||||
fn __eq__(&self, other: &Bound<'_, PyAny>) -> bool {
|
||||
if let Ok(other) = other.extract::<PyRef<'_, Self>>() {
|
||||
return Arc::as_ptr(&self.inner) == Arc::as_ptr(&other.inner);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn __hash__(&self) -> u64 {
|
||||
Arc::as_ptr(&self.inner) as u64
|
||||
Ok(list.into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1302,6 +1046,5 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_class::<分型Py>()?;
|
||||
m.add_class::<虚线Py>()?;
|
||||
m.add_class::<线段特征Py>()?;
|
||||
m.add_class::<特征分型Py>()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+218
-93
@@ -22,9 +22,78 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use pyo3::basic::CompareOp;
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::{PyDict, PyType};
|
||||
use pyo3::types::{PyBool, PyDict, PyType};
|
||||
|
||||
// ========== 单例缓存 ==========
|
||||
|
||||
static 分型结构_单例缓存: Mutex<Option<HashMap<u8, Py<分型结构Py>>>> = Mutex::new(None);
|
||||
|
||||
pub fn 获取分型结构单例(
|
||||
py: Python<'_>,
|
||||
inner: chanlun::types::分型结构,
|
||||
) -> Py<分型结构Py> {
|
||||
let mut guard = 分型结构_单例缓存.lock();
|
||||
if let Some(ref map) = *guard {
|
||||
return map[&(inner as u8)].clone_ref(py);
|
||||
}
|
||||
|
||||
// 首次访问时从类属性加载单例
|
||||
let module = py.import("chanlun._chanlun").unwrap();
|
||||
let class = module.getattr("分型结构").unwrap();
|
||||
let mut map = HashMap::new();
|
||||
for (name, variant) in &[
|
||||
("上", chanlun::types::分型结构::上),
|
||||
("下", chanlun::types::分型结构::下),
|
||||
("顶", chanlun::types::分型结构::顶),
|
||||
("底", chanlun::types::分型结构::底),
|
||||
("散", chanlun::types::分型结构::散),
|
||||
] {
|
||||
let instance: Py<分型结构Py> = class.getattr(*name).unwrap().extract().unwrap();
|
||||
map.insert(*variant as u8, instance);
|
||||
}
|
||||
let result = map[&(inner as u8)].clone_ref(py);
|
||||
*guard = Some(map);
|
||||
result
|
||||
}
|
||||
|
||||
static 相对方向_单例缓存: Mutex<Option<HashMap<u8, Py<相对方向Py>>>> = Mutex::new(None);
|
||||
|
||||
pub fn 获取相对方向单例(
|
||||
py: Python<'_>,
|
||||
inner: chanlun::types::相对方向,
|
||||
) -> Py<相对方向Py> {
|
||||
let mut guard = 相对方向_单例缓存.lock();
|
||||
if let Some(ref map) = *guard {
|
||||
return map[&(inner as u8)].clone_ref(py);
|
||||
}
|
||||
|
||||
// 首次访问时从类属性加载单例
|
||||
let module = py.import("chanlun._chanlun").unwrap();
|
||||
let class = module.getattr("相对方向").unwrap();
|
||||
let mut map = HashMap::new();
|
||||
for (name, variant) in &[
|
||||
("向上", chanlun::types::相对方向::向上),
|
||||
("向下", chanlun::types::相对方向::向下),
|
||||
("向上缺口", chanlun::types::相对方向::向上缺口),
|
||||
("向下缺口", chanlun::types::相对方向::向下缺口),
|
||||
("衔接向上", chanlun::types::相对方向::衔接向上),
|
||||
("衔接向下", chanlun::types::相对方向::衔接向下),
|
||||
("顺", chanlun::types::相对方向::顺),
|
||||
("逆", chanlun::types::相对方向::逆),
|
||||
("同", chanlun::types::相对方向::同),
|
||||
] {
|
||||
let instance: Py<相对方向Py> = class.getattr(*name).unwrap().extract().unwrap();
|
||||
map.insert(*variant as u8, instance);
|
||||
}
|
||||
let result = map[&(inner as u8)].clone_ref(py);
|
||||
*guard = Some(map);
|
||||
result
|
||||
}
|
||||
|
||||
// ========== 买卖点类型 ==========
|
||||
|
||||
@@ -54,18 +123,22 @@ impl 买卖点类型Py {
|
||||
self.inner.to_string()
|
||||
}
|
||||
|
||||
fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult<bool> {
|
||||
fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult<Py<PyAny>> {
|
||||
let py = other.py();
|
||||
// 比较:先尝试字符串(Python Enum(str)),再同类型,再 name 属性
|
||||
let eq = if let Ok(s) = other.extract::<String>() {
|
||||
self.inner.to_string() == s
|
||||
} else if let Ok(other) = other.extract::<PyRef<'_, Self>>() {
|
||||
self.inner == other.inner
|
||||
} else if let Ok(py_name) = other.getattr("name").and_then(|n| n.extract::<String>()) {
|
||||
self.inner.to_string() == py_name
|
||||
} else {
|
||||
return Err(pyo3::exceptions::PyNotImplementedError::new_err(""));
|
||||
return Ok(py.NotImplemented());
|
||||
};
|
||||
match op {
|
||||
CompareOp::Eq => Ok(eq),
|
||||
CompareOp::Ne => Ok(!eq),
|
||||
_ => Err(pyo3::exceptions::PyNotImplementedError::new_err("")),
|
||||
CompareOp::Eq => Ok(PyBool::new(py, eq).as_any().to_owned().unbind()),
|
||||
CompareOp::Ne => Ok(PyBool::new(py, !eq).as_any().to_owned().unbind()),
|
||||
_ => Ok(py.NotImplemented()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,19 +207,44 @@ impl 相对方向Py {
|
||||
format!("{}", self.inner)
|
||||
}
|
||||
|
||||
fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult<bool> {
|
||||
let Ok(other) = other.extract::<PyRef<'_, Self>>() else {
|
||||
return Err(pyo3::exceptions::PyNotImplementedError::new_err(""));
|
||||
};
|
||||
let eq = self.inner == other.inner;
|
||||
match op {
|
||||
CompareOp::Eq => Ok(eq),
|
||||
CompareOp::Ne => Ok(!eq),
|
||||
CompareOp::Lt => Ok((self.inner as u8) < (other.inner as u8)),
|
||||
CompareOp::Le => Ok((self.inner as u8) <= (other.inner as u8)),
|
||||
CompareOp::Gt => Ok((self.inner as u8) > (other.inner as u8)),
|
||||
CompareOp::Ge => Ok((self.inner as u8) >= (other.inner as u8)),
|
||||
fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult<Py<PyAny>> {
|
||||
let py = other.py();
|
||||
// 同类型比较
|
||||
if let Ok(other) = other.extract::<PyRef<'_, Self>>() {
|
||||
let eq = self.inner == other.inner;
|
||||
return Ok(match op {
|
||||
CompareOp::Eq => PyBool::new(py, eq).as_any().to_owned().unbind(),
|
||||
CompareOp::Ne => PyBool::new(py, !eq).as_any().to_owned().unbind(),
|
||||
CompareOp::Lt => PyBool::new(py, (self.inner as u8) < (other.inner as u8))
|
||||
.as_any()
|
||||
.to_owned()
|
||||
.unbind(),
|
||||
CompareOp::Le => PyBool::new(py, (self.inner as u8) <= (other.inner as u8))
|
||||
.as_any()
|
||||
.to_owned()
|
||||
.unbind(),
|
||||
CompareOp::Gt => PyBool::new(py, (self.inner as u8) > (other.inner as u8))
|
||||
.as_any()
|
||||
.to_owned()
|
||||
.unbind(),
|
||||
CompareOp::Ge => PyBool::new(py, (self.inner as u8) >= (other.inner as u8))
|
||||
.as_any()
|
||||
.to_owned()
|
||||
.unbind(),
|
||||
});
|
||||
}
|
||||
// 跨模块比较:通过 name 属性匹配 Python Enum(如 chan.chan.相对方向)
|
||||
if let Ok(py_name) = other.getattr("name").and_then(|n| n.extract::<String>()) {
|
||||
let self_name = format!("{:?}", self.inner);
|
||||
let eq = self_name == py_name;
|
||||
return Ok(match op {
|
||||
CompareOp::Eq => PyBool::new(py, eq).as_any().to_owned().unbind(),
|
||||
CompareOp::Ne => PyBool::new(py, !eq).as_any().to_owned().unbind(),
|
||||
_ => py.NotImplemented(),
|
||||
});
|
||||
}
|
||||
// 回退:返回 Python NotImplemented
|
||||
Ok(py.NotImplemented())
|
||||
}
|
||||
|
||||
fn __hash__(&self) -> u64 {
|
||||
@@ -154,10 +252,8 @@ impl 相对方向Py {
|
||||
}
|
||||
|
||||
/// 返回方向的对立面(向上↔向下, 缺口↔反向缺口, 衔接↔反向衔接)。
|
||||
fn 翻转(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner.翻转(),
|
||||
}
|
||||
fn 翻转(&self, py: Python<'_>) -> Py<Self> {
|
||||
获取相对方向单例(py, self.inner.翻转())
|
||||
}
|
||||
|
||||
/// 判断是否为向上方向(向上/向上缺口/衔接向上)
|
||||
@@ -210,10 +306,27 @@ impl 相对方向Py {
|
||||
#[classmethod]
|
||||
fn 分析(
|
||||
_cls: &Bound<'_, PyType>, 前高: f64, 前低: f64, 后高: f64, 后低: f64
|
||||
) -> Self {
|
||||
Self {
|
||||
inner: chanlun::types::相对方向::分析(前高, 前低, 后高, 后低),
|
||||
}
|
||||
) -> Py<Self> {
|
||||
获取相对方向单例(
|
||||
_cls.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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,19 +360,43 @@ impl 分型结构Py {
|
||||
self.inner.to_string()
|
||||
}
|
||||
|
||||
fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult<bool> {
|
||||
let Ok(other) = other.extract::<PyRef<'_, Self>>() else {
|
||||
return Err(pyo3::exceptions::PyNotImplementedError::new_err(""));
|
||||
};
|
||||
let eq = self.inner == other.inner;
|
||||
match op {
|
||||
CompareOp::Eq => Ok(eq),
|
||||
CompareOp::Ne => Ok(!eq),
|
||||
CompareOp::Lt => Ok((self.inner as u8) < (other.inner as u8)),
|
||||
CompareOp::Le => Ok((self.inner as u8) <= (other.inner as u8)),
|
||||
CompareOp::Gt => Ok((self.inner as u8) > (other.inner as u8)),
|
||||
CompareOp::Ge => Ok((self.inner as u8) >= (other.inner as u8)),
|
||||
fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult<Py<PyAny>> {
|
||||
let py = other.py();
|
||||
// 同类型比较
|
||||
if let Ok(other) = other.extract::<PyRef<'_, Self>>() {
|
||||
let eq = self.inner == other.inner;
|
||||
return Ok(match op {
|
||||
CompareOp::Eq => PyBool::new(py, eq).as_any().to_owned().unbind(),
|
||||
CompareOp::Ne => PyBool::new(py, !eq).as_any().to_owned().unbind(),
|
||||
CompareOp::Lt => PyBool::new(py, (self.inner as u8) < (other.inner as u8))
|
||||
.as_any()
|
||||
.to_owned()
|
||||
.unbind(),
|
||||
CompareOp::Le => PyBool::new(py, (self.inner as u8) <= (other.inner as u8))
|
||||
.as_any()
|
||||
.to_owned()
|
||||
.unbind(),
|
||||
CompareOp::Gt => PyBool::new(py, (self.inner as u8) > (other.inner as u8))
|
||||
.as_any()
|
||||
.to_owned()
|
||||
.unbind(),
|
||||
CompareOp::Ge => PyBool::new(py, (self.inner as u8) >= (other.inner as u8))
|
||||
.as_any()
|
||||
.to_owned()
|
||||
.unbind(),
|
||||
});
|
||||
}
|
||||
// 跨模块比较:通过 name 属性匹配 Python Enum(如 chan.chan.分型结构)
|
||||
if let Ok(py_name) = other.getattr("name").and_then(|n| n.extract::<String>()) {
|
||||
let self_name = self.inner.to_string();
|
||||
let eq = self_name == py_name;
|
||||
return Ok(match op {
|
||||
CompareOp::Eq => PyBool::new(py, eq).as_any().to_owned().unbind(),
|
||||
CompareOp::Ne => PyBool::new(py, !eq).as_any().to_owned().unbind(),
|
||||
_ => py.NotImplemented(),
|
||||
});
|
||||
}
|
||||
Ok(py.NotImplemented())
|
||||
}
|
||||
|
||||
fn __hash__(&self) -> u64 {
|
||||
@@ -306,41 +443,17 @@ impl 分型结构Py {
|
||||
let (中高, 中低) = get_hl(中)?;
|
||||
let (右高, 右低) = get_hl(右)?;
|
||||
|
||||
let 左中关系 = chanlun::types::相对方向::分析(左高, 左低, 中高, 中低);
|
||||
let 中右关系 = chanlun::types::相对方向::分析(中高, 中低, 右高, 右低);
|
||||
|
||||
let 向上类 = |d: chanlun::types::相对方向| d.是否向上();
|
||||
let 向下类 = |d: chanlun::types::相对方向| d.是否向下();
|
||||
|
||||
let result = match (左中关系, 中右关系) {
|
||||
(d1, d2) if matches!(d1, chanlun::types::相对方向::顺) && !忽视顺序包含 => {
|
||||
panic!("顺序包含: {:?} {:?}", d1, d2);
|
||||
}
|
||||
(d1, d2) if matches!(d2, chanlun::types::相对方向::顺) && !忽视顺序包含 => {
|
||||
panic!("顺序包含: {:?} {:?}", d1, d2);
|
||||
}
|
||||
(a, b) if 向上类(a) && 向上类(b) => chanlun::types::分型结构::上,
|
||||
(a, b) if 向上类(a) && 向下类(b) => chanlun::types::分型结构::顶,
|
||||
(a, chanlun::types::相对方向::逆) if 向上类(a) && 可以逆序包含 => {
|
||||
chanlun::types::分型结构::上
|
||||
}
|
||||
(a, b) if 向下类(a) && 向上类(b) => chanlun::types::分型结构::底,
|
||||
(a, b) if 向下类(a) && 向下类(b) => chanlun::types::分型结构::下,
|
||||
(a, chanlun::types::相对方向::逆) if 向下类(a) && 可以逆序包含 => {
|
||||
chanlun::types::分型结构::下
|
||||
}
|
||||
(chanlun::types::相对方向::逆, a) if 向上类(a) && 可以逆序包含 => {
|
||||
chanlun::types::分型结构::底
|
||||
}
|
||||
(chanlun::types::相对方向::逆, a) if 向下类(a) && 可以逆序包含 => {
|
||||
chanlun::types::分型结构::顶
|
||||
}
|
||||
(chanlun::types::相对方向::逆, chanlun::types::相对方向::逆) if 可以逆序包含 => {
|
||||
chanlun::types::分型结构::散
|
||||
}
|
||||
_ => return Ok(None),
|
||||
};
|
||||
Ok(Some(Self { inner: result }))
|
||||
Ok(chanlun::types::分型结构::分析_内部(
|
||||
左高,
|
||||
左低,
|
||||
中高,
|
||||
中低,
|
||||
右高,
|
||||
右低,
|
||||
可以逆序包含,
|
||||
忽视顺序包含,
|
||||
)
|
||||
.map(|inner| Self { inner }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,23 +496,35 @@ impl 缺口Py {
|
||||
format!("{}", self.inner)
|
||||
}
|
||||
|
||||
fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult<bool> {
|
||||
fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult<Py<PyAny>> {
|
||||
let py = other.py();
|
||||
let Ok(other) = other.extract::<PyRef<'_, Self>>() else {
|
||||
return Err(pyo3::exceptions::PyNotImplementedError::new_err(""));
|
||||
return Ok(py.NotImplemented());
|
||||
};
|
||||
let to_bool = |b: bool| PyBool::new(py, b).as_any().to_owned().unbind();
|
||||
match op {
|
||||
CompareOp::Eq => Ok(self.inner.高 == other.inner.高 && self.inner.低 == other.inner.低),
|
||||
CompareOp::Ne => {
|
||||
Ok(!(self.inner.高 == other.inner.高 && self.inner.低 == other.inner.低))
|
||||
}
|
||||
CompareOp::Lt => Ok(self.inner.高 < other.inner.高
|
||||
|| (self.inner.高 == other.inner.高 && self.inner.低 < other.inner.低)),
|
||||
CompareOp::Le => Ok(self.inner.高 < other.inner.高
|
||||
|| (self.inner.高 == other.inner.高 && self.inner.低 <= other.inner.低)),
|
||||
CompareOp::Gt => Ok(self.inner.高 > other.inner.高
|
||||
|| (self.inner.高 == other.inner.高 && self.inner.低 > other.inner.低)),
|
||||
CompareOp::Ge => Ok(self.inner.高 > other.inner.高
|
||||
|| (self.inner.高 == other.inner.高 && self.inner.低 >= other.inner.低)),
|
||||
CompareOp::Eq => Ok(to_bool(
|
||||
self.inner.高 == other.inner.高 && self.inner.低 == other.inner.低,
|
||||
)),
|
||||
CompareOp::Ne => Ok(to_bool(
|
||||
!(self.inner.高 == other.inner.高 && self.inner.低 == other.inner.低),
|
||||
)),
|
||||
CompareOp::Lt => Ok(to_bool(
|
||||
self.inner.高 < other.inner.高
|
||||
|| (self.inner.高 == other.inner.高 && self.inner.低 < other.inner.低),
|
||||
)),
|
||||
CompareOp::Le => Ok(to_bool(
|
||||
self.inner.高 < other.inner.高
|
||||
|| (self.inner.高 == other.inner.高 && self.inner.低 <= other.inner.低),
|
||||
)),
|
||||
CompareOp::Gt => Ok(to_bool(
|
||||
self.inner.高 > other.inner.高
|
||||
|| (self.inner.高 == other.inner.高 && self.inner.低 > other.inner.低),
|
||||
)),
|
||||
CompareOp::Ge => Ok(to_bool(
|
||||
self.inner.高 > other.inner.高
|
||||
|| (self.inner.高 == other.inner.高 && self.inner.低 >= other.inner.低),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,7 +602,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
// 买卖点类型 class attributes (singleton instances)
|
||||
let py = m.py();
|
||||
let bsp_class = m.getattr("买卖点类型")?;
|
||||
let bsp_class = bsp_class.downcast_into::<PyType>()?;
|
||||
let bsp_class = bsp_class.cast_into::<PyType>()?;
|
||||
|
||||
let variants: &[(&str, chanlun::types::买卖点类型)] = &[
|
||||
("一买", chanlun::types::买卖点类型::一买),
|
||||
@@ -500,7 +625,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
("T3B卖", chanlun::types::买卖点类型::T3B卖),
|
||||
];
|
||||
|
||||
let mut bsp_members = PyDict::new(py);
|
||||
let bsp_members = PyDict::new(py);
|
||||
for (name, value) in variants {
|
||||
let instance = Py::new(py, 买卖点类型Py { inner: *value })?;
|
||||
bsp_class.setattr(*name, instance.clone_ref(py))?;
|
||||
@@ -509,7 +634,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
bsp_class.setattr("__members__", bsp_members)?;
|
||||
|
||||
// 相对方向 class attributes
|
||||
let dir_class = m.getattr("相对方向")?.downcast_into::<PyType>()?.clone();
|
||||
let dir_class = m.getattr("相对方向")?.cast_into::<PyType>()?.clone();
|
||||
let dir_variants: &[(&str, chanlun::types::相对方向)] = &[
|
||||
("向上", chanlun::types::相对方向::向上),
|
||||
("向下", chanlun::types::相对方向::向下),
|
||||
@@ -522,7 +647,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
("同", chanlun::types::相对方向::同),
|
||||
];
|
||||
|
||||
let mut dir_members = PyDict::new(py);
|
||||
let dir_members = PyDict::new(py);
|
||||
for (name, value) in dir_variants {
|
||||
let instance = Py::new(py, 相对方向Py { inner: *value })?;
|
||||
dir_class.setattr(*name, instance.clone_ref(py))?;
|
||||
@@ -531,7 +656,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
dir_class.setattr("__members__", dir_members)?;
|
||||
|
||||
// 分型结构 class attributes
|
||||
let frac_class = m.getattr("分型结构")?.downcast_into::<PyType>()?.clone();
|
||||
let frac_class = m.getattr("分型结构")?.cast_into::<PyType>()?.clone();
|
||||
let frac_variants: &[(&str, chanlun::types::分型结构)] = &[
|
||||
("上", chanlun::types::分型结构::上),
|
||||
("下", chanlun::types::分型结构::下),
|
||||
@@ -540,7 +665,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
("散", chanlun::types::分型结构::散),
|
||||
];
|
||||
|
||||
let mut frac_members = PyDict::new(py);
|
||||
let frac_members = PyDict::new(py);
|
||||
for (name, value) in frac_variants {
|
||||
let instance = Py::new(py, 分型结构Py { inner: *value })?;
|
||||
frac_class.setattr(*name, instance.clone_ref(py))?;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,25 @@
|
||||
"""pyo3_test_helpers — 可复用的 PyO3 测试工具包。
|
||||
|
||||
提供四个核心模块:
|
||||
|
||||
rc_identity — Rc/Arc 指针身份一致性测试 Mixin
|
||||
subclass — PyO3 #[pyclass(subclass)] 子类化兼容性测试 Mixin
|
||||
type_shape — 返回值类型形状验证工具
|
||||
api_consistency — 两个模块间 API 描述符类型一致性测试 Mixin
|
||||
|
||||
所有 Mixin 都是纯 Python,不依赖 pytest,与 unittest.TestCase 配合使用。
|
||||
下游项目复制此目录即可复用。
|
||||
"""
|
||||
|
||||
from .api_consistency import ApiConsistencyMixin
|
||||
from .rc_identity import RcIdentityMixin
|
||||
from .subclass import PyO3SubclassMixin
|
||||
from .type_shape import assert_type_shape, TypeShapeAssertions
|
||||
|
||||
__all__ = [
|
||||
"ApiConsistencyMixin",
|
||||
"RcIdentityMixin",
|
||||
"PyO3SubclassMixin",
|
||||
"assert_type_shape",
|
||||
"TypeShapeAssertions",
|
||||
]
|
||||
@@ -0,0 +1,196 @@
|
||||
"""API 一致性测试 Mixin。
|
||||
|
||||
验证两个模块中同名类的公开成员描述符类型一致。
|
||||
典型用途:对比 Python 参考实现 (chan.py) 与 Rust/PyO3 移植 (chanlun) 的 API 兼容性。
|
||||
|
||||
用法::
|
||||
|
||||
class TestApi一致性(ApiConsistencyMixin, unittest.TestCase):
|
||||
reference_module = mylib.ref # Python 参考实现
|
||||
target_module = mylib # Rust/PyO3 移植
|
||||
|
||||
# 可选: 已知差异(不会报错)
|
||||
known_missing_in_target = {
|
||||
"SomeClass": {"old_deprecated_method"},
|
||||
}
|
||||
known_descriptor_diffs = {
|
||||
# (class_name, member, ref_type, target_type)
|
||||
}
|
||||
|
||||
# 可选: 成员名过滤(匹配则跳过,支持前缀用 "prefix_" 表示)
|
||||
noise_filters = ["model_", "parse_", "from_orm"]
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
|
||||
def _classify_member(cls, attr_name):
|
||||
"""返回描述符类型: property / classmethod / staticmethod / regular_method / None(data)."""
|
||||
# 优先检查元类字典中的描述符
|
||||
for klass in type(cls).__mro__:
|
||||
if attr_name in klass.__dict__:
|
||||
raw = klass.__dict__[attr_name]
|
||||
if isinstance(raw, property):
|
||||
return "property"
|
||||
elif isinstance(raw, classmethod):
|
||||
return "classmethod"
|
||||
elif isinstance(raw, staticmethod):
|
||||
return "staticmethod"
|
||||
break
|
||||
try:
|
||||
attr = getattr(cls, attr_name)
|
||||
except Exception:
|
||||
return None
|
||||
if callable(attr):
|
||||
return "regular_method"
|
||||
return None
|
||||
|
||||
|
||||
def _is_noise(name, filters):
|
||||
for pat in filters:
|
||||
if pat == name:
|
||||
return True
|
||||
if pat.endswith("_") and name.startswith(pat):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _get_classes(mod):
|
||||
"""获取模块中所有公开的 type."""
|
||||
return {n: getattr(mod, n) for n in dir(mod) if not n.startswith("_") and isinstance(getattr(mod, n), type)}
|
||||
|
||||
|
||||
class ApiConsistencyMixin:
|
||||
"""API 一致性测试 Mixin。
|
||||
|
||||
子类必须定义:
|
||||
reference_module: 参考模块 (Python 实现)
|
||||
target_module: 目标模块 (Rust/PyO3 移植)
|
||||
|
||||
子类可选定义:
|
||||
known_missing_in_target: dict[str, set[str]] — 已知 target 中缺失的成员
|
||||
known_descriptor_diffs: set[tuple] — 已知描述符类型差异
|
||||
noise_filters: list[str] — 噪音成员名过滤
|
||||
"""
|
||||
|
||||
reference_module = None
|
||||
target_module = None
|
||||
known_missing_in_target: dict = {}
|
||||
known_descriptor_diffs: set = set()
|
||||
noise_filters: list = []
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if cls.reference_module is None or cls.target_module is None:
|
||||
raise unittest.SkipTest(f"{cls.__name__} 未定义 reference_module / target_module")
|
||||
|
||||
# ---- 描述符类型一致性 ----
|
||||
|
||||
def test_共有成员描述符类型一致(self):
|
||||
"""同名类的同名成员,描述符类型 (property/classmethod/staticmethod/regular) 一致."""
|
||||
ref_classes = _get_classes(self.reference_module)
|
||||
tgt_classes = _get_classes(self.target_module)
|
||||
shared = sorted(set(ref_classes) & set(tgt_classes))
|
||||
|
||||
failures = []
|
||||
for cls_name in shared:
|
||||
ref_cls = ref_classes[cls_name]
|
||||
tgt_cls = tgt_classes[cls_name]
|
||||
|
||||
ref_members = {}
|
||||
tgt_members = {}
|
||||
|
||||
for attr_name in sorted(dir(ref_cls)):
|
||||
if attr_name.startswith("_") or _is_noise(attr_name, self.noise_filters):
|
||||
continue
|
||||
cat = _classify_member(ref_cls, attr_name)
|
||||
if cat:
|
||||
ref_members[attr_name] = cat
|
||||
|
||||
for attr_name in sorted(dir(tgt_cls)):
|
||||
if attr_name.startswith("_") or _is_noise(attr_name, self.noise_filters):
|
||||
continue
|
||||
cat = _classify_member(tgt_cls, attr_name)
|
||||
if cat:
|
||||
tgt_members[attr_name] = cat
|
||||
|
||||
shared_members = sorted(set(ref_members) & set(tgt_members))
|
||||
for member in shared_members:
|
||||
ref_cat = ref_members[member]
|
||||
tgt_cat = tgt_members[member]
|
||||
if ref_cat != tgt_cat:
|
||||
diff_key = (cls_name, member, ref_cat, tgt_cat)
|
||||
if diff_key not in self.known_descriptor_diffs:
|
||||
failures.append(f"{cls_name}.{member}: ref={ref_cat}, tgt={tgt_cat}")
|
||||
|
||||
if failures:
|
||||
self.fail("描述符类型不一致:\n " + "\n ".join(failures))
|
||||
|
||||
# ---- 缺失成员检查 ----
|
||||
|
||||
def test_参考模块成员在目标模块中存在(self):
|
||||
"""chan 中的关键公开成员在 chanlun 中均有对应."""
|
||||
ref_classes = _get_classes(self.reference_module)
|
||||
tgt_classes = _get_classes(self.target_module)
|
||||
shared = sorted(set(ref_classes) & set(tgt_classes))
|
||||
|
||||
failures = []
|
||||
for cls_name in shared:
|
||||
if cls_name not in self.known_missing_in_target:
|
||||
continue
|
||||
ref_cls = ref_classes[cls_name]
|
||||
tgt_cls = tgt_classes[cls_name]
|
||||
|
||||
expected_missing = self.known_missing_in_target.get(cls_name, set())
|
||||
|
||||
ref_members = set()
|
||||
for attr_name in sorted(dir(ref_cls)):
|
||||
if attr_name.startswith("_") or _is_noise(attr_name, self.noise_filters):
|
||||
continue
|
||||
cat = _classify_member(ref_cls, attr_name)
|
||||
if cat and attr_name not in expected_missing:
|
||||
ref_members.add(attr_name)
|
||||
|
||||
tgt_members = set()
|
||||
for attr_name in sorted(dir(tgt_cls)):
|
||||
if attr_name.startswith("_") or _is_noise(attr_name, self.noise_filters):
|
||||
continue
|
||||
cat = _classify_member(tgt_cls, attr_name)
|
||||
if cat:
|
||||
tgt_members.add(attr_name)
|
||||
|
||||
missing = ref_members - tgt_members - expected_missing
|
||||
for member in sorted(missing):
|
||||
failures.append(f"{cls_name}.{member}: ref={_classify_member(ref_cls, member)}, tgt=未导出")
|
||||
|
||||
if failures:
|
||||
self.fail("参考模块中的成员在目标模块中缺失:\n " + "\n ".join(failures))
|
||||
|
||||
# ---- 方法可调用性 ----
|
||||
|
||||
def test_共有方法均可调用(self):
|
||||
"""所有共有 regular_method 在两边都是 callable."""
|
||||
ref_classes = _get_classes(self.reference_module)
|
||||
tgt_classes = _get_classes(self.target_module)
|
||||
shared = sorted(set(ref_classes) & set(tgt_classes))
|
||||
|
||||
failures = []
|
||||
for cls_name in shared:
|
||||
ref_cls = ref_classes[cls_name]
|
||||
tgt_cls = tgt_classes[cls_name]
|
||||
|
||||
for attr_name in sorted(dir(ref_cls)):
|
||||
if attr_name.startswith("_") or _is_noise(attr_name, self.noise_filters):
|
||||
continue
|
||||
ref_cat = _classify_member(ref_cls, attr_name)
|
||||
tgt_cat = _classify_member(tgt_cls, attr_name)
|
||||
if ref_cat == "regular_method" and tgt_cat == "regular_method":
|
||||
ref_obj = getattr(ref_cls, attr_name)
|
||||
tgt_obj = getattr(tgt_cls, attr_name)
|
||||
if not callable(ref_obj):
|
||||
failures.append(f"{cls_name}.{attr_name}: ref 不是 callable")
|
||||
if not callable(tgt_obj):
|
||||
failures.append(f"{cls_name}.{attr_name}: tgt 不是 callable")
|
||||
|
||||
if failures:
|
||||
self.fail("方法不可调用:\n " + "\n ".join(failures))
|
||||
@@ -0,0 +1,126 @@
|
||||
"""Rc/Arc 指针身份一致性测试 Mixin。
|
||||
|
||||
验证:同一个 Rust Rc<T>/Arc<T> 无论通过哪条路径到达 Python,
|
||||
始终返回相同的 PyObject(`a is b` 为 True)。
|
||||
|
||||
用法::
|
||||
|
||||
class TestMyLib(RcIdentityMixin, unittest.TestCase):
|
||||
# 必须: 创建被测对象实例(每个 test_ 调用一次)
|
||||
@staticmethod
|
||||
def target_factory():
|
||||
return make_fresh_instance()
|
||||
|
||||
# 必须: 序列 getter —— (名称, target → list)
|
||||
# Mixin 会验证: 同一 getter 调用两次,list[i] is list[j]
|
||||
sequence_getters = {
|
||||
"主序列": lambda t: t.items,
|
||||
"子序列": lambda t: t.children,
|
||||
}
|
||||
|
||||
# 可选: 跨路径身份断言 —— (名称, (target → obj_a, target → obj_b))
|
||||
# Mixin 会验证: obj_a is obj_b
|
||||
cross_path_assertions = [
|
||||
("序列[0] 与 首元素.父", lambda t: t.items[0], lambda t: t.items[0].parent),
|
||||
]
|
||||
|
||||
# 可选: getter 稳定性 —— (名称, target → obj)
|
||||
# Mixin 会验证: obj is obj (两次调用返回同一对象)
|
||||
stable_getters = {
|
||||
"首元素.属性": lambda t: t.items[0].attr,
|
||||
}
|
||||
|
||||
# 可选: 序列长度检查的最小值(默认不检查,设为 >0 开启)
|
||||
min_sequence_lengths = {
|
||||
"主序列": 3,
|
||||
"子序列": 2,
|
||||
}
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
|
||||
class RcIdentityMixin:
|
||||
"""Rc/Arc 指针身份一致性测试 Mixin。
|
||||
|
||||
子类必须定义:
|
||||
target_factory: Callable[[], Any]
|
||||
sequence_getters: dict[str, Callable[[Any], list]]
|
||||
|
||||
子类可选定义:
|
||||
cross_path_assertions: list[tuple[str, Callable, Callable]]
|
||||
stable_getters: dict[str, Callable]
|
||||
min_sequence_lengths: dict[str, int]
|
||||
"""
|
||||
|
||||
target_factory = None
|
||||
sequence_getters: dict = {}
|
||||
cross_path_assertions: list = []
|
||||
stable_getters: dict = {}
|
||||
min_sequence_lengths: dict = {}
|
||||
|
||||
def _get_target(self):
|
||||
"""惰性获取 target,首次调用后缓存在类上。避免 setUpClass MRO 冲突."""
|
||||
cls = type(self)
|
||||
# 每次测试重新创建——但这会太慢。用类级别缓存。
|
||||
# 子类应在 setUpClass 中调用 self._get_target() 或自己设置 cls._cached_target。
|
||||
if not hasattr(cls, "_cached_target"):
|
||||
if cls.target_factory is None:
|
||||
raise unittest.SkipTest(f"{cls.__name__} 未定义 target_factory")
|
||||
cls._cached_target = cls.target_factory()
|
||||
return cls._cached_target
|
||||
|
||||
# ---- 序列 getter 稳定性 ----
|
||||
|
||||
def test_序列重复获取身份一致(self):
|
||||
"""同一序列 getter 调用两次,对应位置元素 is 相同."""
|
||||
t = self._get_target()
|
||||
for name, getter in self.sequence_getters.items():
|
||||
seq1 = getter(t)
|
||||
seq2 = getter(t)
|
||||
self.assertEqual(len(seq1), len(seq2), f"{name}: 两次获取长度不同")
|
||||
check_n = min(len(seq1), 10)
|
||||
for i in range(check_n):
|
||||
self.assertIs(seq1[i], seq2[i], f"{name}[{i}] 身份不一致")
|
||||
|
||||
def test_序列最小长度(self):
|
||||
"""序列长度至少达到配置的最小值."""
|
||||
t = self._get_target()
|
||||
for name, getter in self.sequence_getters.items():
|
||||
if name in self.min_sequence_lengths:
|
||||
min_len = self.min_sequence_lengths[name]
|
||||
actual = len(getter(t))
|
||||
self.assertGreaterEqual(actual, min_len, f"{name} 长度 {actual} < {min_len}")
|
||||
|
||||
# ---- 跨路径身份 ----
|
||||
|
||||
def test_跨路径身份一致(self):
|
||||
"""不同访问路径到达的同一 Rust 对象在 Python 侧 is 相同."""
|
||||
t = self._get_target()
|
||||
for i, (label, path_a, path_b) in enumerate(self.cross_path_assertions):
|
||||
obj_a = path_a(t)
|
||||
obj_b = path_b(t)
|
||||
self.assertIsNotNone(obj_a, f"[{i}] {label}: path_a 返回 None")
|
||||
self.assertIsNotNone(obj_b, f"[{i}] {label}: path_b 返回 None")
|
||||
self.assertIs(obj_a, obj_b, f"[{i}] {label}: 身份不一致")
|
||||
|
||||
# ---- getter 稳定性 ----
|
||||
|
||||
def test_getter重复调用身份一致(self):
|
||||
"""同一 getter 调用两次返回同一 PyObject."""
|
||||
t = self._get_target()
|
||||
for name, getter in self.stable_getters.items():
|
||||
obj1 = getter(t)
|
||||
obj2 = getter(t)
|
||||
self.assertIs(obj1, obj2, f"{name}: 两次调用返回不同对象")
|
||||
|
||||
# ---- list.index 基于 is ----
|
||||
|
||||
def test_list_index_基于身份(self):
|
||||
"""list.index(elem) 正常工作(依赖 __eq__ 基于 is 比较)."""
|
||||
t = self._get_target()
|
||||
for name, getter in self.sequence_getters.items():
|
||||
seq = getter(t)
|
||||
if len(seq) >= 2:
|
||||
self.assertEqual(seq.index(seq[0]), 0, f"{name}: index(seq[0]) != 0")
|
||||
self.assertEqual(seq.index(seq[-1]), len(seq) - 1, f"{name}: index(seq[-1]) != {len(seq) - 1}")
|
||||
@@ -0,0 +1,247 @@
|
||||
"""PyO3 #[pyclass(subclass)] 子类化兼容性测试 Mixin。
|
||||
|
||||
验证: Python 端可以正常子类化 PyO3 导出的类,__new__/__init__ 协作、
|
||||
super() 委托、MRO 链、property/method 重写等全部正确。
|
||||
|
||||
用法::
|
||||
|
||||
class TestMyObserver(PyO3SubclassMixin, unittest.TestCase):
|
||||
base_class = mylib.Observer
|
||||
constructor_args = ("symbol", 300)
|
||||
constructor_kwargs = {}
|
||||
|
||||
# 可选: 用 kwargs 的构造
|
||||
constructor_with_config = ("symbol", 300, {"配置": mylib.Config()})
|
||||
|
||||
# 可选: 序列 getter 名称列表(重写测试会检查这些 getter 可被覆盖)
|
||||
sequence_getter_names = [
|
||||
"普通K线序列", "高级序列",
|
||||
]
|
||||
|
||||
# 可选: 需要 .nb 数据文件才能运行的测试会检查这个
|
||||
@staticmethod
|
||||
def has_data_file():
|
||||
return os.path.isfile("data.nb")
|
||||
|
||||
# 可选: 创建一个"喂了一根K线"的 target
|
||||
@staticmethod
|
||||
def make_target_with_data():
|
||||
obs = mylib.Observer("sym", 300)
|
||||
k = mylib.KLine(...)
|
||||
obs.feed(k)
|
||||
return obs
|
||||
|
||||
# 可选: 创建一个"喂了一根K线"的子类实例
|
||||
@staticmethod
|
||||
def make_sub_with_data():
|
||||
class Sub(mylib.Observer):
|
||||
pass
|
||||
obs = Sub("sym", 300)
|
||||
k = mylib.KLine(...)
|
||||
obs.feed(k)
|
||||
return obs
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
|
||||
class PyO3SubclassMixin:
|
||||
"""PyO3 子类化兼容性测试 Mixin。
|
||||
|
||||
子类必须定义:
|
||||
base_class: type
|
||||
constructor_args: tuple
|
||||
constructor_kwargs: dict
|
||||
|
||||
子类可选定义:
|
||||
sequence_getter_names: list[str]
|
||||
has_data_file: Callable[[], bool]
|
||||
make_target_with_data: Callable[[], Any]
|
||||
make_sub_with_data: Callable[[], Any]
|
||||
make_data_item: Callable[[], Any] # 创建一根可喂入的数据项
|
||||
feed_method_name: str # 喂数据的方法名,默认 "增加原始K线"
|
||||
property_getters: list[str] # 需要逐一下覆写的 property 名
|
||||
method_overrides: list[str] # 需要逐一重写的方法名
|
||||
"""
|
||||
|
||||
base_class: type = None
|
||||
constructor_args: tuple = ()
|
||||
constructor_kwargs: dict = {}
|
||||
sequence_getter_names: list = []
|
||||
|
||||
# 可选 hooks
|
||||
has_data_file = None
|
||||
make_target_with_data = None
|
||||
make_sub_with_data = None
|
||||
make_data_item = None
|
||||
feed_method_name = "增加原始K线"
|
||||
property_getters: list = []
|
||||
method_overrides: list = []
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if cls.base_class is None:
|
||||
raise unittest.SkipTest(f"{cls.__name__} 未定义 base_class")
|
||||
|
||||
# ---- 基础子类化 ----
|
||||
|
||||
def test_子类可实例化(self):
|
||||
"""子类可创建,isinstance 正确."""
|
||||
Base = self.base_class
|
||||
|
||||
class Sub(Base):
|
||||
pass
|
||||
|
||||
obs = Sub(*self.constructor_args, **self.constructor_kwargs)
|
||||
self.assertIsInstance(obs, Base)
|
||||
self.assertEqual(type(obs).__name__, "Sub")
|
||||
|
||||
def test_子类_init_可添加自定义属性(self):
|
||||
"""子类 __init__ 可添加自定义属性,基类字段不受影响."""
|
||||
Base = self.base_class
|
||||
args = self.constructor_args
|
||||
kwargs = self.constructor_kwargs
|
||||
|
||||
class Sub(Base):
|
||||
def __init__(self, *a, **kw):
|
||||
self.tag = "custom"
|
||||
self.count = 0
|
||||
|
||||
obs = Sub(*args, **kwargs)
|
||||
self.assertEqual(obs.tag, "custom")
|
||||
self.assertEqual(obs.count, 0)
|
||||
|
||||
def test_子类_new_过滤_kwargs(self):
|
||||
"""__new__ 过滤子类专属参数,只把父类需要的传给 super().__new__."""
|
||||
Base = self.base_class
|
||||
args = self.constructor_args
|
||||
|
||||
class Sub(Base):
|
||||
def __new__(cls, *a, extra=None, **kw):
|
||||
return super().__new__(cls, *a)
|
||||
|
||||
def __init__(self, *a, extra=None, **kw):
|
||||
self.extra = extra
|
||||
|
||||
obs = Sub(*args, extra={"debug": True})
|
||||
self.assertEqual(obs.extra, {"debug": True})
|
||||
|
||||
obs2 = Sub(*args)
|
||||
self.assertIsNone(obs2.extra)
|
||||
|
||||
# ---- 方法重写 ----
|
||||
|
||||
def test_方法重写_super调用(self):
|
||||
"""重写方法,super() 调用父类."""
|
||||
if self.make_target_with_data is None or self.make_sub_with_data is None:
|
||||
self.skipTest("未定义 make_target_with_data / make_sub_with_data")
|
||||
|
||||
base_obs = self.make_target_with_data()
|
||||
sub_obs = self.make_sub_with_data()
|
||||
|
||||
for attr in self.sequence_getter_names:
|
||||
base_len = len(getattr(base_obs, attr))
|
||||
sub_len = len(getattr(sub_obs, attr))
|
||||
self.assertEqual(base_len, sub_len, f"{attr}: base={base_len}, sub={sub_len}")
|
||||
|
||||
def test_方法完全重写不调super(self):
|
||||
"""完全重写方法不调 super(),基类逻辑不执行."""
|
||||
Base = self.base_class
|
||||
args = self.constructor_args
|
||||
kwargs = self.constructor_kwargs
|
||||
|
||||
class Sub(Base):
|
||||
def __init__(self, *a, **kw):
|
||||
self.log = []
|
||||
|
||||
obs = Sub(*args, **kwargs)
|
||||
self.assertEqual(obs.log, [])
|
||||
|
||||
# ---- property 重写 ----
|
||||
|
||||
def test_property_重写_super调用(self):
|
||||
"""重写 @property getter,super() 取基类值."""
|
||||
Base = self.base_class
|
||||
args = self.constructor_args
|
||||
kwargs = self.constructor_kwargs
|
||||
|
||||
class Sub(Base):
|
||||
pass
|
||||
|
||||
obs = Sub(*args, **kwargs)
|
||||
# 验证实例创建成功即可,具体 getter 覆盖由子类测试
|
||||
self.assertIsInstance(obs, Base)
|
||||
|
||||
def test_str_repr_重写(self):
|
||||
"""重写 __str__ / __repr__."""
|
||||
Base = self.base_class
|
||||
args = self.constructor_args
|
||||
kwargs = self.constructor_kwargs
|
||||
|
||||
class Sub(Base):
|
||||
def __str__(self):
|
||||
return f"Custom({id(self)})"
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
obs = Sub(*args, **kwargs)
|
||||
self.assertIn("Custom", str(obs))
|
||||
self.assertEqual(str(obs), repr(obs))
|
||||
|
||||
# ---- 多层继承 MRO ----
|
||||
|
||||
def test_多层继承_MRO链完整(self):
|
||||
"""多层继承,MRO 调用链完整."""
|
||||
if self.make_data_item is None:
|
||||
self.skipTest("未定义 make_data_item")
|
||||
|
||||
Base = self.base_class
|
||||
args = self.constructor_args
|
||||
kwargs = self.constructor_kwargs
|
||||
feed_name = self.feed_method_name
|
||||
|
||||
class L1(Base):
|
||||
def __init__(self, *a, **kw):
|
||||
self._l1_called = False
|
||||
|
||||
class L2(L1):
|
||||
def __init__(self, *a, **kw):
|
||||
super().__init__(*a, **kw)
|
||||
self._l2_called = True
|
||||
|
||||
obs = L2(*args, **kwargs)
|
||||
self.assertTrue(obs._l2_called)
|
||||
|
||||
def test_未重写方法直接继承(self):
|
||||
"""未重写的方法从基类直接继承."""
|
||||
if self.make_data_item is None:
|
||||
self.skipTest("未定义 make_data_item")
|
||||
|
||||
Base = self.base_class
|
||||
args = self.constructor_args
|
||||
kwargs = self.constructor_kwargs
|
||||
|
||||
class Sub(Base):
|
||||
pass
|
||||
|
||||
obs = Sub(*args, **kwargs)
|
||||
self.assertIsInstance(obs, Base)
|
||||
|
||||
# ---- 重写后实例行为与基类一致 ----
|
||||
|
||||
def test_同名继承行为一致(self):
|
||||
"""同名继承(零重写),行为与基类完全一致."""
|
||||
if self.make_target_with_data is None:
|
||||
self.skipTest("未定义 make_target_with_data")
|
||||
|
||||
Base = self.base_class
|
||||
args = self.constructor_args
|
||||
kwargs = self.constructor_kwargs
|
||||
|
||||
class Sub(Base):
|
||||
pass
|
||||
|
||||
base = Base(*args, **kwargs)
|
||||
sub = Sub(*args, **kwargs)
|
||||
self.assertIsInstance(sub, Base)
|
||||
@@ -0,0 +1,88 @@
|
||||
"""PyO3 返回值的 Python 类型形状验证工具。
|
||||
|
||||
验证 PyO3 导出的函数/方法返回值类型正确:
|
||||
- int 不是 str/float
|
||||
- list 元素是 tuple 不是 list
|
||||
- 方法是 callable 不是 property
|
||||
- 返回值结构(嵌套类型)符合预期
|
||||
|
||||
用法::
|
||||
|
||||
from helpers.type_shape import assert_type_shape
|
||||
|
||||
result = mylib.compute(some_input)
|
||||
assert_type_shape(result, {
|
||||
"count": int,
|
||||
"ratio": float,
|
||||
"label": str,
|
||||
"items": [(int, str, bool)], # list of 3-tuples
|
||||
"nested": {"key": int},
|
||||
})
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
|
||||
def assert_type_shape(obj, schema, path=""):
|
||||
"""验证 obj 的类型形状与 schema 一致。
|
||||
|
||||
schema 支持:
|
||||
- type: obj 必须是该类型实例
|
||||
- [inner]: obj 必须是 list,每个元素验证 inner
|
||||
- (t1, t2, ...): obj 必须是 tuple,每字段验证对应类型
|
||||
- {key: inner}: obj 必须是 dict,递归验证
|
||||
- callable: obj 必须是 callable(函数/方法)
|
||||
"""
|
||||
if isinstance(schema, type):
|
||||
_check_type(obj, schema, path)
|
||||
elif isinstance(schema, list):
|
||||
_check_list(obj, schema, path)
|
||||
elif isinstance(schema, tuple):
|
||||
_check_tuple(obj, schema, path)
|
||||
elif isinstance(schema, dict):
|
||||
_check_dict(obj, schema, path)
|
||||
elif schema is callable:
|
||||
_check_callable(obj, path)
|
||||
else:
|
||||
raise ValueError(f"{path}: 不支持的 schema 类型 {type(schema)}")
|
||||
|
||||
|
||||
def _check_type(obj, expected, path):
|
||||
assert isinstance(obj, expected), f"{path}: 期望 {expected.__name__}, 实际 {type(obj).__name__}"
|
||||
|
||||
|
||||
def _check_list(obj, schema, path):
|
||||
assert isinstance(obj, list), f"{path}: 期望 list, 实际 {type(obj).__name__}"
|
||||
if len(schema) == 1:
|
||||
inner = schema[0]
|
||||
for i, item in enumerate(obj):
|
||||
assert_type_shape(item, inner, f"{path}[{i}]")
|
||||
|
||||
|
||||
def _check_tuple(obj, schema, path):
|
||||
assert isinstance(obj, tuple), f"{path}: 期望 tuple, 实际 {type(obj).__name__}"
|
||||
assert len(obj) == len(schema), f"{path}: 期望 tuple 长度 {len(schema)}, 实际 {len(obj)}"
|
||||
for i, (item, inner) in enumerate(zip(obj, schema)):
|
||||
assert_type_shape(item, inner, f"{path}[{i}]")
|
||||
|
||||
|
||||
def _check_dict(obj, schema, path):
|
||||
assert isinstance(obj, dict), f"{path}: 期望 dict, 实际 {type(obj).__name__}"
|
||||
for key, inner in schema.items():
|
||||
assert key in obj, f"{path}: 缺少键 '{key}'"
|
||||
assert_type_shape(obj[key], inner, f"{path}['{key}']")
|
||||
|
||||
|
||||
def _check_callable(obj, path):
|
||||
assert callable(obj), f"{path}: 期望 callable, 实际 {type(obj).__name__}"
|
||||
|
||||
|
||||
# ---- TestCase mixin ----
|
||||
|
||||
|
||||
class TypeShapeAssertions:
|
||||
"""提供 assert_type_shape 便捷方法的 mixin."""
|
||||
|
||||
def assertTypeShape(self, obj, schema, path=""):
|
||||
"""断言 obj 的类型形状与 schema 一致."""
|
||||
assert_type_shape(obj, schema, path)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,267 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
对象标识测试:验证同一 Rc 底层数据通过不同路径访问时,
|
||||
Python 侧始终返回相同的 PyObject(`is` 比较为 True)。
|
||||
|
||||
涉及的 Rc 包装类型:
|
||||
- K线 (Rc<K线>) — 原始OHLCV数据
|
||||
- 缠论K线 (Rc<缠论K线>) — 包含处理后的K线
|
||||
- 分型 (Rc<分型>) — 顶底分型
|
||||
- 虚线 (Rc<虚线>) — 笔/线段的通用抽象
|
||||
- 中枢 (Rc<中枢>) — 三段虚线重叠区间
|
||||
- 线段特征 (Rc<线段特征>) — 线段特征序列元素
|
||||
- 特征分型 (Rc<特征分型>) — 特征序列的分型
|
||||
|
||||
路径示例:
|
||||
- 缠K序列[i] vs 分型序列[j].中 (同一根缠K)
|
||||
- 分型序列[i] vs 笔序列[j].文 (同一个分型)
|
||||
- 笔序列[i] vs 中枢[k].基础序列[m] (同一条虚线)
|
||||
"""
|
||||
|
||||
import chanlun
|
||||
import math
|
||||
|
||||
|
||||
def create_observer(symbol="btcusd", period=14400, n_bars=500):
|
||||
"""创建观察者并喂入模拟K线数据。"""
|
||||
cfg = chanlun.缠论配置()
|
||||
obs = chanlun.观察者(symbol, period, cfg)
|
||||
|
||||
for i in range(n_bars):
|
||||
trend = i * 3
|
||||
wave = math.sin(i * 0.05) * 2000
|
||||
mid = 68000.0 + trend + wave
|
||||
high = mid + abs(math.cos(i * 0.3)) * 400 + 100
|
||||
low = mid - abs(math.sin(i * 0.5)) * 400 - 100
|
||||
k = chanlun.K线(
|
||||
标识=symbol,
|
||||
周期=period,
|
||||
时间戳=1771675200 + i * period,
|
||||
开盘价=mid - 50,
|
||||
高=high,
|
||||
低=low,
|
||||
收盘价=mid + 50,
|
||||
成交量=abs(math.sin(i)) * 1000,
|
||||
)
|
||||
obs.增加原始K线(k)
|
||||
|
||||
return obs
|
||||
|
||||
|
||||
class Test缠K身份:
|
||||
"""缠论K线: 从序列、分型、笔端点、中枢等不同路径访问。"""
|
||||
|
||||
def test_序列重复获取(self):
|
||||
"""同一序列获取两次,元素应相同。"""
|
||||
obs = create_observer()
|
||||
seq1 = obs.缠论K线序列
|
||||
seq2 = obs.缠论K线序列
|
||||
for i in range(min(len(seq1), 10)):
|
||||
assert seq1[i] is seq2[i], f"缠K序列[{i}] 身份不一致"
|
||||
|
||||
def test_分型中K(self):
|
||||
"""分型.中 与 缠K序列 对应元素应相同。"""
|
||||
obs = create_observer()
|
||||
seq = obs.缠论K线序列
|
||||
分序 = obs.分型序列
|
||||
for fx in 分序[:10]:
|
||||
中 = fx.中
|
||||
for ck in seq:
|
||||
if ck.时间戳 == 中.时间戳:
|
||||
assert ck is 中, f"分型.中 (ts={中.时间戳}) 与序列中元素不匹配"
|
||||
break
|
||||
|
||||
def test_笔端点钟K(self):
|
||||
"""笔的端点分型的中间K线应与序列元素相同。"""
|
||||
obs = create_observer()
|
||||
seq = obs.缠论K线序列
|
||||
for bi in obs.笔序列:
|
||||
for nm, getter in [("文", lambda b=bi: b.文), ("武", lambda b=bi: b.武)]:
|
||||
ep = getter()
|
||||
if ep is None:
|
||||
continue
|
||||
中 = ep.中
|
||||
for ck in seq:
|
||||
if ck.时间戳 == 中.时间戳:
|
||||
assert ck is 中, f"笔.{nm}.中 (ts={中.时间戳}) 与序列中元素不匹配"
|
||||
break
|
||||
|
||||
def test_getter重复调用(self):
|
||||
"""同一getter调用两次返回同一对象。"""
|
||||
obs = create_observer()
|
||||
for fx in obs.分型序列[:5]:
|
||||
中1 = fx.中
|
||||
中2 = fx.中
|
||||
assert 中1 is 中2, "分型.中 两次调用返回不同对象"
|
||||
|
||||
|
||||
class Test分型身份:
|
||||
"""分型: 从分型序列、笔/线段端点、买卖点等不同路径访问。"""
|
||||
|
||||
def test_序列重复获取(self):
|
||||
"""同一序列获取两次,元素应相同。"""
|
||||
obs = create_observer()
|
||||
seq1 = obs.分型序列
|
||||
seq2 = obs.分型序列
|
||||
for i in range(min(len(seq1), 9)):
|
||||
assert seq1[i] is seq2[i], f"分型序列[{i}] 身份不一致"
|
||||
|
||||
def test_笔端点与序列(self):
|
||||
"""笔.文 / 笔.武 应与分型序列中对应元素相同。"""
|
||||
obs = create_observer()
|
||||
分序 = obs.分型序列
|
||||
for bi in obs.笔序列:
|
||||
for nm in ["文", "武"]:
|
||||
ep = getattr(bi, nm)
|
||||
if ep is None:
|
||||
continue
|
||||
matched = False
|
||||
for fx in 分序:
|
||||
if fx.时间戳 == ep.时间戳 and fx.结构 == ep.结构:
|
||||
assert fx is ep, f"笔.{nm} (ts={ep.时间戳}) 与分型序列中元素不匹配"
|
||||
matched = True
|
||||
break
|
||||
assert matched, f"笔.{nm} (ts={ep.时间戳}) 在分型序列中未找到"
|
||||
|
||||
def test_段端点与序列(self):
|
||||
"""段.文 / 段.武 应与分型序列中对应元素相同。"""
|
||||
obs = create_observer()
|
||||
分序 = obs.分型序列
|
||||
for duan in obs.线段序列:
|
||||
for nm in ["文", "武"]:
|
||||
ep = getattr(duan, nm)
|
||||
if ep is None:
|
||||
continue
|
||||
matched = False
|
||||
for fx in 分序:
|
||||
if fx.时间戳 == ep.时间戳 and fx.结构 == ep.结构:
|
||||
assert fx is ep, f"段.{nm} (ts={ep.时间戳}) 与分型序列中元素不匹配"
|
||||
matched = True
|
||||
break
|
||||
assert matched, f"段.{nm} (ts={ep.时间戳}) 在分型序列中未找到"
|
||||
|
||||
def test_getter重复调用(self):
|
||||
"""同一getter调用两次返回同一对象。"""
|
||||
obs = create_observer()
|
||||
for bi in obs.笔序列:
|
||||
文1 = bi.文
|
||||
文2 = bi.文
|
||||
assert 文1 is 文2, "笔.文 两次调用返回不同对象"
|
||||
武1 = bi.武
|
||||
武2 = bi.武
|
||||
assert 武1 is 武2, "笔.武 两次调用返回不同对象"
|
||||
break # 只测第一笔
|
||||
|
||||
|
||||
class Test虚线身份:
|
||||
"""虚线(笔/线段): 从笔序列、线段序列、中枢内部序列等不同路径访问。"""
|
||||
|
||||
def test_笔序列重复获取(self):
|
||||
obs = create_observer()
|
||||
seq1 = obs.笔序列
|
||||
seq2 = obs.笔序列
|
||||
for i in range(min(len(seq1), 8)):
|
||||
assert seq1[i] is seq2[i], f"笔序列[{i}] 身份不一致"
|
||||
|
||||
def test_线段序列重复获取(self):
|
||||
obs = create_observer()
|
||||
seq1 = obs.线段序列
|
||||
seq2 = obs.线段序列
|
||||
for i in range(min(len(seq1), 5)):
|
||||
assert seq1[i] is seq2[i], f"线段序列[{i}] 身份不一致"
|
||||
|
||||
def test_多个扩展序列(self):
|
||||
"""扩展线段的不同序列获取同一虚线应相同。"""
|
||||
obs = create_observer()
|
||||
s1 = obs.扩展线段序列
|
||||
s2 = obs.扩展线段序列_线段
|
||||
s3 = obs.扩展线段序列_扩展线段
|
||||
# 这些序列可能包含不同的虚线,但如果同一个 Rc 出现在两个序列中应该相同
|
||||
for d1 in s1:
|
||||
for d2 in s2:
|
||||
if d1.序号 == d2.序号:
|
||||
assert d1 is d2, f"扩展线段序列[{d1.序号}] 跨序列身份不一致"
|
||||
break
|
||||
|
||||
|
||||
class TestK线身份:
|
||||
"""原始K线: 从序列、买卖点、缠K标的等不同路径访问。"""
|
||||
|
||||
def test_序列重复获取(self):
|
||||
obs = create_observer()
|
||||
seq1 = obs.普通K线序列
|
||||
seq2 = obs.普通K线序列
|
||||
for i in range(min(len(seq1), 10)):
|
||||
assert seq1[i] is seq2[i], f"普K序列[{i}] 身份不一致"
|
||||
|
||||
|
||||
class Test中枢身份:
|
||||
"""中枢: 从中枢序列、分型关联、笔中枢/线段中枢等不同路径访问。"""
|
||||
|
||||
def test_序列重复获取(self):
|
||||
obs = create_observer(period=3600, n_bars=800)
|
||||
seq1 = obs.中枢序列
|
||||
seq2 = obs.中枢序列
|
||||
for i in range(min(len(seq1), 5)):
|
||||
assert seq1[i] is seq2[i], f"中枢序列[{i}] 身份不一致"
|
||||
|
||||
def test_笔中枢与线段中枢(self):
|
||||
obs = create_observer(period=3600, n_bars=800)
|
||||
笔中 = obs.笔_中枢序列
|
||||
段中 = obs.线段_中枢序列
|
||||
扩展中 = obs.扩展中枢序列
|
||||
# 验证同一次获取内的身份
|
||||
for zs in 笔中:
|
||||
文1 = zs.文
|
||||
文2 = zs.文
|
||||
assert 文1 is 文2, f"笔中枢.文 两次调用不同"
|
||||
break
|
||||
for zs in 段中:
|
||||
文1 = zs.文
|
||||
文2 = zs.文
|
||||
assert 文1 is 文2, f"段中枢.文 两次调用不同"
|
||||
break
|
||||
|
||||
|
||||
class Test整体身份:
|
||||
"""跨类型综合身份测试。"""
|
||||
|
||||
def test_买卖点分型(self):
|
||||
"""验证买卖点的关联分型身份。"""
|
||||
obs = create_observer(period=3600, n_bars=800)
|
||||
# 尝试访问可用的结构
|
||||
分序 = obs.分型序列
|
||||
笔序 = obs.笔序列
|
||||
assert len(分序) >= 0 and len(笔序) >= 0 # 至少不崩溃
|
||||
|
||||
def test_全链路一致性(self):
|
||||
"""缠K → 分型 → 笔 → 段 链路中所有对象身份一致。"""
|
||||
obs = create_observer()
|
||||
seq = obs.缠论K线序列
|
||||
|
||||
for bi in obs.笔序列:
|
||||
# 笔的端点分型
|
||||
for nm, getter in [("文", lambda b=bi: b.文), ("武", lambda b=bi: b.武)]:
|
||||
ep = getter()
|
||||
if ep is None:
|
||||
continue
|
||||
# ep 中的 中 是一根缠K,应能在序列中找到相同对象
|
||||
中 = ep.中
|
||||
for ck in seq:
|
||||
if ck.时间戳 == 中.时间戳:
|
||||
assert ck is 中
|
||||
break
|
||||
# 左也应该是可访问的
|
||||
左 = ep.左
|
||||
if 左 is not None:
|
||||
for ck in seq:
|
||||
if ck.时间戳 == 左.时间戳:
|
||||
assert ck is 左
|
||||
break
|
||||
# 右也应该是可访问的
|
||||
右 = ep.右
|
||||
if 右 is not None:
|
||||
for ck in seq:
|
||||
if ck.时间戳 == 右.时间戳:
|
||||
assert ck is 右
|
||||
break
|
||||
@@ -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()
|
||||
}
|
||||
+8
-3
@@ -1,8 +1,7 @@
|
||||
[package]
|
||||
name = "chanlun"
|
||||
version = "26.5.3"
|
||||
edition = "2021"
|
||||
rust-version = "1.70"
|
||||
version = "26.6.4"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
description = "基于缠论(缠中说禅)理论的量化技术分析核心库,支持流式数据处理和多周期联立分析。"
|
||||
readme = "README.md"
|
||||
@@ -18,3 +17,9 @@ serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
byteorder = "1"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
parking_lot = "0.12"
|
||||
tracing = "0.1"
|
||||
fastrand = "2"
|
||||
sha2 = "0.10"
|
||||
inventory = "0.3"
|
||||
chanlun-signal-macros = { path = "../chanlun-signal-macros" }
|
||||
|
||||
+1034
-317
File diff suppressed because it is too large
Load Diff
+505
-512
File diff suppressed because it is too large
Load Diff
@@ -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(ref macd) = k.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()
|
||||
}
|
||||
}
|
||||
|
||||
+346
-217
@@ -25,18 +25,34 @@
|
||||
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};
|
||||
|
||||
/// 中枢 — 三段虚线重叠区间构成的价格中枢
|
||||
/// 可变字段使用 Cell/RefCell 实现内部可变性,确保 Rc 指针身份一致
|
||||
///
|
||||
/// 可变字段使用 AtomicI64 / RwLock 实现内部可变性,确保多 Arc 共享时可修改。
|
||||
///
|
||||
/// 字段:
|
||||
/// - 序号: 中枢序号,同一级别内递增
|
||||
/// - 标识: 中枢标识,格式如 "笔中枢<0>" 或 "线段中枢<1>"
|
||||
/// - 级别: 中枢级别(笔中枢=1,线段中枢=2 等)
|
||||
/// - 基础序列: 构成中枢的虚线序列(至少 3 根,延伸后可多至 9 根甚至更多)
|
||||
/// - 第三买卖线: 第三类买卖点对应的虚线(离开中枢后不回中枢的虚线)
|
||||
/// - 本级_第三买卖线: 本级第三类买卖点对应的虚线
|
||||
#[derive(Debug)]
|
||||
pub struct 中枢 {
|
||||
/// 中枢序号,同一级别内递增
|
||||
pub 序号: AtomicI64,
|
||||
/// 中枢标识,格式如 "笔中枢<0>" 或 "线段中枢<1>"
|
||||
pub 标识: RwLock<String>,
|
||||
/// 中枢级别(笔中枢=1,线段中枢=2 等)
|
||||
pub 级别: AtomicI64,
|
||||
/// 构成中枢的虚线序列(至少 3 根,延伸后可多至 9+ 根)
|
||||
pub 基础序列: RwLock<Vec<Arc<虚线>>>,
|
||||
/// 第三类买卖点对应的虚线(离开中枢后不回中枢的虚线)
|
||||
pub 第三买卖线: RwLock<Option<Arc<虚线>>>,
|
||||
/// 本级第三类买卖点对应的虚线
|
||||
pub 本级_第三买卖线: RwLock<Option<Arc<虚线>>>,
|
||||
}
|
||||
|
||||
@@ -44,16 +60,17 @@ 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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl 中枢 {
|
||||
/// 创建新中枢(最多取前 3 根虚线作为基础序列)
|
||||
pub fn new(序号: i64, 标识: String, 级别: i64, 基础序列: Vec<Arc<虚线>>) -> Self {
|
||||
Self {
|
||||
序号: AtomicI64::new(序号),
|
||||
@@ -65,109 +82,121 @@ impl 中枢 {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn 添加虚线(&self, 实线: Arc<虚线>) {
|
||||
self.基础序列.write().unwrap().push(实线);
|
||||
*self.本级_第三买卖线.write().unwrap() = None;
|
||||
*self.第三买卖线.write().unwrap() = None;
|
||||
/// 向基础序列尾部添加虚线(中枢延伸),并清除第三买卖线
|
||||
pub fn _添加虚线(&self, 实线: Arc<虚线>) {
|
||||
self.基础序列.write().push(实线);
|
||||
*self.本级_第三买卖线.write() = None;
|
||||
*self.第三买卖线.write() = None;
|
||||
}
|
||||
|
||||
/// 返回图表标题字符串,格式为 "文.标识:文.周期:中枢标识:序号"
|
||||
pub fn 图表标题(&self) -> String {
|
||||
format!(
|
||||
"{}:{}:{}:{}",
|
||||
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))
|
||||
.unwrap_or(0.0)
|
||||
}
|
||||
|
||||
/// 中枢下沿 = 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))
|
||||
.unwrap_or(0.0)
|
||||
}
|
||||
|
||||
/// 中枢最高点 = max(所有段的高)
|
||||
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))
|
||||
.unwrap_or(0.0)
|
||||
}
|
||||
|
||||
/// 中枢最低点 = min(所有段的低)
|
||||
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))
|
||||
.unwrap_or(0.0)
|
||||
}
|
||||
|
||||
/// 返回基础序列第一段的起点分型
|
||||
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, 线: Arc<虚线>) {
|
||||
*self.第三买卖线.write().unwrap() = Some(线);
|
||||
/// 设置第三类买卖点对应的虚线
|
||||
pub fn 设置第三买卖线(&self, 线: Option<Arc<虚线>>) {
|
||||
*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.文().时间戳,
|
||||
self.文().时间戳(),
|
||||
crate::utils::format_f64_g(self.文().分型特征值),
|
||||
self.武().时间戳,
|
||||
self.武().时间戳(),
|
||||
crate::utils::format_f64_g(self.武().分型特征值),
|
||||
第三买卖线_str,
|
||||
本级_第三买卖线_str,
|
||||
@@ -175,68 +204,64 @@ impl 中枢 {
|
||||
}
|
||||
|
||||
/// 校验中枢合法性
|
||||
pub fn 校验合法性(&self, 序列: &[Arc<虚线>]) -> bool {
|
||||
let mut 有效序列 = self.基础序列.read().unwrap().clone();
|
||||
pub fn _校验合法性(&self, 序列: &[Arc<虚线>]) -> bool {
|
||||
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.第三买卖线.write().unwrap() = None;
|
||||
*self.本级_第三买卖线.write().unwrap() = None;
|
||||
self.设置第三买卖线(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 重叠低 > 重叠高 {
|
||||
@@ -244,11 +269,13 @@ 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().之后是(三买线) {
|
||||
*self.第三买卖线.write().unwrap() = None;
|
||||
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.高(),
|
||||
self.低(),
|
||||
@@ -257,11 +284,11 @@ impl 中枢 {
|
||||
)
|
||||
.是否缺口()
|
||||
{
|
||||
self.添加虚线(Arc::clone(三买线));
|
||||
*self.第三买卖线.write().unwrap() = None;
|
||||
self._添加虚线(Arc::clone(三买线));
|
||||
self.设置第三买卖线(None);
|
||||
}
|
||||
} else {
|
||||
*self.第三买卖线.write().unwrap() = None;
|
||||
self.设置第三买卖线(None);
|
||||
}
|
||||
}
|
||||
true
|
||||
@@ -270,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;
|
||||
}
|
||||
@@ -302,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::相对方向::分析(
|
||||
@@ -359,16 +400,17 @@ impl 中枢 {
|
||||
pub fn 创建(
|
||||
左: Arc<虚线>, 中: Arc<虚线>, 右: Arc<虚线>, 级别: i64, 标识: &str
|
||||
) -> Self {
|
||||
assert!(Self::基础检查(&左, &中, &右), "中枢.创建 基础检查失败");
|
||||
Self::new(
|
||||
0,
|
||||
format!("{}中枢<{}>", 标识, 中.标识.read().unwrap()),
|
||||
format!("{}中枢<{}>", 标识, 中.标识.read()),
|
||||
级别,
|
||||
vec![左, 中, 右],
|
||||
)
|
||||
}
|
||||
|
||||
/// 从序列中获取中枢
|
||||
pub fn 从序列中获取中枢(
|
||||
/// _从序列中获取中枢
|
||||
pub fn _从序列中获取中枢(
|
||||
虚线序列: &[Arc<虚线>],
|
||||
起始方向: 相对方向,
|
||||
标识: &str,
|
||||
@@ -385,37 +427,32 @@ impl 中枢 {
|
||||
None
|
||||
}
|
||||
|
||||
/// 向中枢序列尾部添加
|
||||
pub fn 向中枢序列尾部添加(
|
||||
中枢序列: &mut Vec<Arc<中枢>>, mut 待添加中枢: Arc<中枢>
|
||||
/// _向中枢序列尾部添加
|
||||
pub fn _向中枢序列尾部添加(
|
||||
中枢序列: &mut Vec<Arc<中枢>>, 待添加中枢: Arc<中枢>
|
||||
) {
|
||||
if let Some(前一个) = 中枢序列.last() {
|
||||
待添加中枢
|
||||
.序号
|
||||
.store(前一个.序号.load(Ordering::Relaxed) + 1, Ordering::Relaxed);
|
||||
// Python: assert seq[-1].获取序列()[-1].序号 <= new.获取序列()[-1].序号
|
||||
let 前_seq = 前一个.获取序列();
|
||||
let new_seq = 待添加中枢.获取序列();
|
||||
if let (Some(前_last), Some(new_last)) = (前_seq.last(), new_seq.last()) {
|
||||
if 前_last.序号.load(Ordering::Relaxed) > new_last.序号.load(Ordering::Relaxed)
|
||||
{
|
||||
panic!(
|
||||
"向中枢序列尾部添加 序号错误 前last={} > new_last={}",
|
||||
前_last.序号.load(Ordering::Relaxed),
|
||||
new_last.序号.load(Ordering::Relaxed)
|
||||
);
|
||||
}
|
||||
let 前_last_序号 = 前一个.最后().unwrap().序号.load(Ordering::Relaxed);
|
||||
let new_last_序号 = 待添加中枢.最后().unwrap().序号.load(Ordering::Relaxed);
|
||||
if 前_last_序号 > new_last_序号 {
|
||||
panic!(
|
||||
"向中枢序列尾部添加 序号错误 前last={} > new_last={}",
|
||||
前_last_序号, new_last_序号
|
||||
);
|
||||
}
|
||||
}
|
||||
中枢序列.push(待添加中枢);
|
||||
}
|
||||
|
||||
/// 从中枢序列尾部弹出
|
||||
pub fn 从中枢序列尾部弹出(
|
||||
pub fn _从中枢序列尾部弹出(
|
||||
中枢序列: &mut Vec<Arc<中枢>>,
|
||||
待弹出: &Arc<中枢>,
|
||||
) -> Option<Arc<中枢>> {
|
||||
if 中枢序列.last().map(|x| Arc::as_ptr(x)) == Some(Arc::as_ptr(待弹出)) {
|
||||
if 中枢序列.last().map(Arc::as_ptr) == Some(Arc::as_ptr(待弹出)) {
|
||||
中枢序列.pop()
|
||||
} else {
|
||||
None
|
||||
@@ -430,7 +467,7 @@ impl 中枢 {
|
||||
中枢序列: &mut Vec<Arc<中枢>>,
|
||||
跳过首部: bool,
|
||||
标识: &str,
|
||||
层级: i64,
|
||||
_层级: i64,
|
||||
) {
|
||||
if 虚线序列.len() < 3 {
|
||||
return;
|
||||
@@ -444,11 +481,8 @@ impl 中枢 {
|
||||
let 右 = &虚线序列[i + 1];
|
||||
|
||||
if Self::基础检查(左, 中, 右) {
|
||||
// Python: 序号 = 虚线序列.index(左)
|
||||
let 序号 = 虚线序列
|
||||
.iter()
|
||||
.position(|x| Arc::as_ptr(x) == Arc::as_ptr(左))
|
||||
.unwrap_or(i - 1);
|
||||
// Python: 序号 = 线段._索引(虚线序列, 左)
|
||||
let 序号: usize = crate::algorithm::segment::线段::_索引(虚线序列, 左);
|
||||
if 跳过首部 && (左.序号.load(Ordering::Relaxed) == 0 || 序号 == 0) {
|
||||
continue;
|
||||
}
|
||||
@@ -470,9 +504,9 @@ impl 中枢 {
|
||||
中.级别.load(Ordering::Relaxed),
|
||||
标识,
|
||||
));
|
||||
Self::向中枢序列尾部添加(中枢序列, 新中枢);
|
||||
Self::_向中枢序列尾部添加(中枢序列, 新中枢);
|
||||
// Python: return 中枢递归分析(虚线序列, 中枢序列, ...)
|
||||
Self::分析(虚线序列, 中枢序列, 跳过首部, 标识, 层级);
|
||||
Self::分析(虚线序列, 中枢序列, 跳过首部, 标识, _层级);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -483,33 +517,27 @@ impl 中枢 {
|
||||
let mut 当前中枢_idx = 中枢序列.len() - 1;
|
||||
|
||||
// Validate via shared reference (中枢 uses RwLock internally)
|
||||
let needs_pop = !中枢序列[当前中枢_idx].校验合法性(虚线序列);
|
||||
let needs_pop = !中枢序列[当前中枢_idx]._校验合法性(虚线序列);
|
||||
if needs_pop {
|
||||
let 当前中枢 = Arc::clone(&中枢序列[当前中枢_idx]);
|
||||
Self::从中枢序列尾部弹出(中枢序列, &当前中枢);
|
||||
Self::分析(虚线序列, 中枢序列, 跳过首部, 标识, 层级);
|
||||
Self::_从中枢序列尾部弹出(中枢序列, &当前中枢);
|
||||
Self::分析(虚线序列, 中枢序列, 跳过首部, 标识, _层级);
|
||||
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 i in 起始索引..虚线序列.len() {
|
||||
let 当前虚线 = Arc::clone(&虚线序列[i]);
|
||||
for 当前虚线_ref in &虚线序列[起始索引..] {
|
||||
let 当前虚线 = Arc::clone(当前虚线_ref);
|
||||
|
||||
// 检查是否超出中枢范围(缺口)
|
||||
if crate::types::相对方向::分析(中枢高, 中枢低, 当前虚线.高(), 当前虚线.低()).是否缺口()
|
||||
@@ -519,20 +547,26 @@ impl 中枢 {
|
||||
// Python: if 当前中枢.基础序列[-1].之后是(当前虚线):
|
||||
let needs_三买 = {
|
||||
let cur = &中枢序列[当前中枢_idx];
|
||||
cur.基础序列
|
||||
.read()
|
||||
.unwrap()
|
||||
.last()
|
||||
.unwrap()
|
||||
.之后是(&当前虚线)
|
||||
cur.基础序列.read().last().unwrap().之后是(&当前虚线)
|
||||
};
|
||||
if needs_三买 {
|
||||
中枢序列[当前中枢_idx].设置第三买卖线(当前虚线.clone());
|
||||
中枢序列[当前中枢_idx].设置第三买卖线(Some(当前虚线.clone()));
|
||||
}
|
||||
} else {
|
||||
if 候选序列.is_empty() {
|
||||
// 仍在范围内:延伸中枢
|
||||
中枢序列[当前中枢_idx].添加虚线(当前虚线);
|
||||
assert!(
|
||||
中枢序列[当前中枢_idx]
|
||||
.基础序列
|
||||
.read()
|
||||
.last()
|
||||
.unwrap()
|
||||
.之后是(&当前虚线),
|
||||
"中枢延伸: 不连续 {}, {}",
|
||||
中枢序列[当前中枢_idx].基础序列.read().last().unwrap(),
|
||||
当前虚线
|
||||
);
|
||||
中枢序列[当前中枢_idx]._添加虚线(当前虚线);
|
||||
} else {
|
||||
候选序列.push(当前虚线);
|
||||
}
|
||||
@@ -543,14 +577,13 @@ impl 中枢 {
|
||||
let 起始方向 = 中枢序列[当前中枢_idx]
|
||||
.基础序列
|
||||
.read()
|
||||
.unwrap()
|
||||
.last()
|
||||
.unwrap()
|
||||
.方向()
|
||||
.翻转();
|
||||
match Self::从序列中获取中枢(&候选序列, 起始方向, 标识) {
|
||||
match Self::_从序列中获取中枢(&候选序列, 起始方向, 标识) {
|
||||
Some(新中枢) => {
|
||||
Self::向中枢序列尾部添加(中枢序列, 新中枢);
|
||||
Self::_向中枢序列尾部添加(中枢序列, 新中枢);
|
||||
// Python: 当前中枢 = 新中枢
|
||||
当前中枢_idx = 中枢序列.len() - 1;
|
||||
中枢高 = 中枢序列[当前中枢_idx].高();
|
||||
@@ -564,6 +597,128 @@ 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 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(),
|
||||
crate::utils::format_f64_g(self.高()),
|
||||
crate::utils::format_f64_g(self.低()),
|
||||
len,
|
||||
序列_str,
|
||||
self.文(),
|
||||
self.武(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -575,13 +730,14 @@ mod tests {
|
||||
use crate::types::分型结构;
|
||||
|
||||
fn 辅助_创建K线(时间戳: i64, 高: f64, 低: f64, 开: f64, 收: f64) -> K线 {
|
||||
let mut k = K线::default();
|
||||
k.时间戳 = 时间戳;
|
||||
k.高 = 高;
|
||||
k.低 = 低;
|
||||
k.开盘价 = 开;
|
||||
k.收盘价 = 收;
|
||||
k
|
||||
K线 {
|
||||
时间戳,
|
||||
高,
|
||||
低,
|
||||
开盘价: 开,
|
||||
收盘价: 收,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn 辅助_创建缠K(
|
||||
@@ -682,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]
|
||||
@@ -707,17 +863,17 @@ mod tests {
|
||||
assert_eq!(中枢.序号.load(Ordering::Relaxed), 99);
|
||||
|
||||
// RefCell 第三买卖线读写
|
||||
中枢.设置第三买卖线(Arc::clone(&笔1));
|
||||
assert!(中枢.第三买卖线.read().unwrap().is_some());
|
||||
中枢.设置第三买卖线(Some(Arc::clone(&笔1)));
|
||||
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());
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -737,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)
|
||||
);
|
||||
中枢._添加虚线(Arc::clone(&笔4));
|
||||
assert_eq!(中枢.基础序列.read().len(), 4);
|
||||
assert_eq!(Arc::as_ptr(&中枢.基础序列.read()[3]), Arc::as_ptr(&笔4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -760,15 +913,15 @@ mod tests {
|
||||
1,
|
||||
vec![Arc::clone(&笔1), Arc::clone(&笔2), Arc::clone(&笔3)],
|
||||
);
|
||||
中枢.设置第三买卖线(Arc::clone(&笔1));
|
||||
*中枢.本级_第三买卖线.write().unwrap() = Some(Arc::clone(&笔2));
|
||||
assert!(中枢.第三买卖线.read().unwrap().is_some());
|
||||
assert!(中枢.本级_第三买卖线.read().unwrap().is_some());
|
||||
中枢.设置第三买卖线(Some(Arc::clone(&笔1)));
|
||||
*中枢.本级_第三买卖线.write() = Some(Arc::clone(&笔2));
|
||||
assert!(中枢.第三买卖线.read().is_some());
|
||||
assert!(中枢.本级_第三买卖线.read().is_some());
|
||||
|
||||
中枢.添加虚线(Arc::clone(&笔4));
|
||||
中枢._添加虚线(Arc::clone(&笔4));
|
||||
// 添加虚线后第三买卖线被清除
|
||||
assert!(中枢.第三买卖线.read().unwrap().is_none());
|
||||
assert!(中枢.本级_第三买卖线.read().unwrap().is_none());
|
||||
assert!(中枢.第三买卖线.read().is_none());
|
||||
assert!(中枢.本级_第三买卖线.read().is_none());
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -787,22 +940,22 @@ mod tests {
|
||||
1,
|
||||
vec![Arc::clone(&笔1), Arc::clone(&笔2), Arc::clone(&笔3)],
|
||||
);
|
||||
中枢.设置第三买卖线(Arc::clone(&笔1));
|
||||
中枢.设置第三买卖线(Some(Arc::clone(&笔1)));
|
||||
|
||||
let 克隆 = 中枢.clone();
|
||||
|
||||
// 基础序列中的 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())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -860,37 +1013,13 @@ mod tests {
|
||||
assert_eq!(中枢2.序号.load(Ordering::Relaxed), 88);
|
||||
|
||||
// 通过 rc1 添加虚线
|
||||
中枢1.添加虚线(Arc::clone(&笔4));
|
||||
assert_eq!(中枢2.基础序列.read().unwrap().len(), 4);
|
||||
中枢1._添加虚线(Arc::clone(&笔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])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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(", ");
|
||||
write!(
|
||||
f,
|
||||
"{}({}, {}, 元素数量: {}, [{}], {} ===>>> {})",
|
||||
self.标识.read().unwrap(),
|
||||
crate::utils::format_f64_g(self.高()),
|
||||
crate::utils::format_f64_g(self.低()),
|
||||
self.基础序列.read().unwrap().len(),
|
||||
序列_str,
|
||||
self.文(),
|
||||
self.武(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+938
-928
File diff suppressed because it is too large
Load Diff
+396
-7
@@ -27,24 +27,39 @@ use crate::kline::chan_kline::缠论K线;
|
||||
use crate::structure::fractal_obj::分型;
|
||||
use crate::types::bsp_type::买卖点类型;
|
||||
use crate::types::分型结构;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
/// 基础买卖点 — 买卖点的基础数据结构
|
||||
///
|
||||
/// 包含买卖点的完整信息:类型、关联分型/K线、失效与终结状态等。
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct 基础买卖点 {
|
||||
/// 买卖点备注文本
|
||||
pub 备注: String,
|
||||
/// 买卖点类型(一买/一卖/二买/二卖/三买/三卖/T1/T2/T3 等)
|
||||
pub 类型: 买卖点类型,
|
||||
/// 买卖点对应的分型
|
||||
pub 买卖点分型: Arc<分型>,
|
||||
/// 买卖点对应的缠论K线(即分型的中缠K)
|
||||
pub 买卖点K线: Arc<缠论K线>,
|
||||
/// 当前K线(买卖点生成时的K线)
|
||||
pub 当前K线: Arc<K线>,
|
||||
/// 失效K线(买卖点失效时设置)
|
||||
pub 失效K线: Option<Arc<K线>>,
|
||||
/// 终结K线(买卖点终结时设置)
|
||||
pub 终结K线: Option<Arc<K线>>,
|
||||
/// 中枢破位值
|
||||
pub 破位值: f64,
|
||||
/// 分型结构(可选,用于补充确认)
|
||||
pub 结构: Option<分型结构>,
|
||||
/// 创建时的缠K序号,用于偏移计算(与买卖点K线.序号同尺度)
|
||||
/// None 时退化为使用当前K线.序号(bar序号,旧行为)
|
||||
pub 当前缠K序号: Option<i64>,
|
||||
}
|
||||
|
||||
impl 基础买卖点 {
|
||||
/// 创建基础买卖点,买卖点K线自动取自买卖点分型的中缠K
|
||||
pub fn new(
|
||||
类型: 买卖点类型,
|
||||
当前K线: Arc<K线>,
|
||||
@@ -63,12 +78,17 @@ impl 基础买卖点 {
|
||||
终结K线: None,
|
||||
破位值: 中枢破位值,
|
||||
结构: None,
|
||||
当前缠K序号: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 偏移 — 当前K线与买卖点K线的序号差
|
||||
/// 偏移 — 当前缠K序号与买卖点K线序号的差
|
||||
/// 如果设置了当前缠K序号(来自生成买卖点),使用缠K序号;否则退化为使用 bar序号
|
||||
pub fn 偏移(&self) -> i64 {
|
||||
self.当前K线.序号 - self.买卖点K线.序号.load(Ordering::Relaxed)
|
||||
match self.当前缠K序号 {
|
||||
Some(ck_idx) => ck_idx - self.买卖点K线.序号.load(Ordering::Relaxed),
|
||||
None => self.当前K线.序号 - self.买卖点K线.序号.load(Ordering::Relaxed),
|
||||
}
|
||||
}
|
||||
|
||||
/// 失效偏移
|
||||
@@ -108,10 +128,11 @@ impl std::fmt::Display for 基础买卖点 {
|
||||
}
|
||||
}
|
||||
|
||||
/// 买卖点 — 包含一二三类买卖点的工厂方法
|
||||
/// 买卖点 — 包含全部 18 种买卖点类型的工厂方法
|
||||
pub struct 买卖点;
|
||||
|
||||
impl 买卖点 {
|
||||
/// 创建 一卖 类型的基础买卖点
|
||||
pub fn 一卖点(
|
||||
买卖点分型: Arc<分型>,
|
||||
当前K线: Arc<K线>,
|
||||
@@ -122,6 +143,7 @@ impl 买卖点 {
|
||||
基础买卖点::new(买卖点类型::一卖, 当前K线, 买卖点分型, 备注, 中枢破位值)
|
||||
}
|
||||
|
||||
/// 创建 一买 类型的基础买卖点
|
||||
pub fn 一买点(
|
||||
买卖点分型: Arc<分型>,
|
||||
当前K线: Arc<K线>,
|
||||
@@ -132,6 +154,7 @@ impl 买卖点 {
|
||||
基础买卖点::new(买卖点类型::一买, 当前K线, 买卖点分型, 备注, 中枢破位值)
|
||||
}
|
||||
|
||||
/// 创建 二卖 类型的基础买卖点
|
||||
pub fn 二卖点(
|
||||
买卖点分型: Arc<分型>,
|
||||
当前K线: Arc<K线>,
|
||||
@@ -142,6 +165,7 @@ impl 买卖点 {
|
||||
基础买卖点::new(买卖点类型::二卖, 当前K线, 买卖点分型, 备注, 中枢破位值)
|
||||
}
|
||||
|
||||
/// 创建 二买 类型的基础买卖点
|
||||
pub fn 二买点(
|
||||
买卖点分型: Arc<分型>,
|
||||
当前K线: Arc<K线>,
|
||||
@@ -152,6 +176,7 @@ impl 买卖点 {
|
||||
基础买卖点::new(买卖点类型::二买, 当前K线, 买卖点分型, 备注, 中枢破位值)
|
||||
}
|
||||
|
||||
/// 创建 三卖 类型的基础买卖点
|
||||
pub fn 三卖点(
|
||||
买卖点分型: Arc<分型>,
|
||||
当前K线: Arc<K线>,
|
||||
@@ -162,6 +187,7 @@ impl 买卖点 {
|
||||
基础买卖点::new(买卖点类型::三卖, 当前K线, 买卖点分型, 备注, 中枢破位值)
|
||||
}
|
||||
|
||||
/// 创建 三买 类型的基础买卖点
|
||||
pub fn 三买点(
|
||||
买卖点分型: Arc<分型>,
|
||||
当前K线: Arc<K线>,
|
||||
@@ -172,6 +198,138 @@ impl 买卖点 {
|
||||
基础买卖点::new(买卖点类型::三买, 当前K线, 买卖点分型, 备注, 中枢破位值)
|
||||
}
|
||||
|
||||
/// 创建 T1卖 类型的基础买卖点
|
||||
pub fn T1卖点(
|
||||
买卖点分型: Arc<分型>,
|
||||
当前K线: Arc<K线>,
|
||||
_标识: &str,
|
||||
备注: String,
|
||||
中枢破位值: f64,
|
||||
) -> 基础买卖点 {
|
||||
基础买卖点::new(买卖点类型::T1卖, 当前K线, 买卖点分型, 备注, 中枢破位值)
|
||||
}
|
||||
|
||||
/// 创建 T1买 类型的基础买卖点
|
||||
pub fn T1买点(
|
||||
买卖点分型: Arc<分型>,
|
||||
当前K线: Arc<K线>,
|
||||
_标识: &str,
|
||||
备注: String,
|
||||
中枢破位值: f64,
|
||||
) -> 基础买卖点 {
|
||||
基础买卖点::new(买卖点类型::T1买, 当前K线, 买卖点分型, 备注, 中枢破位值)
|
||||
}
|
||||
|
||||
/// 创建 T1P卖 类型的基础买卖点
|
||||
pub fn T1P卖点(
|
||||
买卖点分型: Arc<分型>,
|
||||
当前K线: Arc<K线>,
|
||||
_标识: &str,
|
||||
备注: String,
|
||||
中枢破位值: f64,
|
||||
) -> 基础买卖点 {
|
||||
基础买卖点::new(买卖点类型::T1P卖, 当前K线, 买卖点分型, 备注, 中枢破位值)
|
||||
}
|
||||
|
||||
/// 创建 T1P买 类型的基础买卖点
|
||||
pub fn T1P买点(
|
||||
买卖点分型: Arc<分型>,
|
||||
当前K线: Arc<K线>,
|
||||
_标识: &str,
|
||||
备注: String,
|
||||
中枢破位值: f64,
|
||||
) -> 基础买卖点 {
|
||||
基础买卖点::new(买卖点类型::T1P买, 当前K线, 买卖点分型, 备注, 中枢破位值)
|
||||
}
|
||||
|
||||
/// 创建 T2卖 类型的基础买卖点
|
||||
pub fn T2卖点(
|
||||
买卖点分型: Arc<分型>,
|
||||
当前K线: Arc<K线>,
|
||||
_标识: &str,
|
||||
备注: String,
|
||||
中枢破位值: f64,
|
||||
) -> 基础买卖点 {
|
||||
基础买卖点::new(买卖点类型::T2卖, 当前K线, 买卖点分型, 备注, 中枢破位值)
|
||||
}
|
||||
|
||||
/// 创建 T2买 类型的基础买卖点
|
||||
pub fn T2买点(
|
||||
买卖点分型: Arc<分型>,
|
||||
当前K线: Arc<K线>,
|
||||
_标识: &str,
|
||||
备注: String,
|
||||
中枢破位值: f64,
|
||||
) -> 基础买卖点 {
|
||||
基础买卖点::new(买卖点类型::T2买, 当前K线, 买卖点分型, 备注, 中枢破位值)
|
||||
}
|
||||
|
||||
/// 创建 T2S卖 类型的基础买卖点
|
||||
pub fn T2S卖点(
|
||||
买卖点分型: Arc<分型>,
|
||||
当前K线: Arc<K线>,
|
||||
_标识: &str,
|
||||
备注: String,
|
||||
中枢破位值: f64,
|
||||
) -> 基础买卖点 {
|
||||
基础买卖点::new(买卖点类型::T2S卖, 当前K线, 买卖点分型, 备注, 中枢破位值)
|
||||
}
|
||||
|
||||
/// 创建 T2S买 类型的基础买卖点
|
||||
pub fn T2S买点(
|
||||
买卖点分型: Arc<分型>,
|
||||
当前K线: Arc<K线>,
|
||||
_标识: &str,
|
||||
备注: String,
|
||||
中枢破位值: f64,
|
||||
) -> 基础买卖点 {
|
||||
基础买卖点::new(买卖点类型::T2S买, 当前K线, 买卖点分型, 备注, 中枢破位值)
|
||||
}
|
||||
|
||||
/// 创建 T3A卖 类型的基础买卖点
|
||||
pub fn T3A卖点(
|
||||
买卖点分型: Arc<分型>,
|
||||
当前K线: Arc<K线>,
|
||||
_标识: &str,
|
||||
备注: String,
|
||||
中枢破位值: f64,
|
||||
) -> 基础买卖点 {
|
||||
基础买卖点::new(买卖点类型::T3A卖, 当前K线, 买卖点分型, 备注, 中枢破位值)
|
||||
}
|
||||
|
||||
/// 创建 T3A买 类型的基础买卖点
|
||||
pub fn T3A买点(
|
||||
买卖点分型: Arc<分型>,
|
||||
当前K线: Arc<K线>,
|
||||
_标识: &str,
|
||||
备注: String,
|
||||
中枢破位值: f64,
|
||||
) -> 基础买卖点 {
|
||||
基础买卖点::new(买卖点类型::T3A买, 当前K线, 买卖点分型, 备注, 中枢破位值)
|
||||
}
|
||||
|
||||
/// 创建 T3B卖 类型的基础买卖点
|
||||
pub fn T3B卖点(
|
||||
买卖点分型: Arc<分型>,
|
||||
当前K线: Arc<K线>,
|
||||
_标识: &str,
|
||||
备注: String,
|
||||
中枢破位值: f64,
|
||||
) -> 基础买卖点 {
|
||||
基础买卖点::new(买卖点类型::T3B卖, 当前K线, 买卖点分型, 备注, 中枢破位值)
|
||||
}
|
||||
|
||||
/// 创建 T3B买 类型的基础买卖点
|
||||
pub fn T3B买点(
|
||||
买卖点分型: Arc<分型>,
|
||||
当前K线: Arc<K线>,
|
||||
_标识: &str,
|
||||
备注: String,
|
||||
中枢破位值: f64,
|
||||
) -> 基础买卖点 {
|
||||
基础买卖点::new(买卖点类型::T3B买, 当前K线, 买卖点分型, 备注, 中枢破位值)
|
||||
}
|
||||
|
||||
/// 生成买卖点 — 根据参数自动选择类型
|
||||
pub fn 生成买卖点(
|
||||
特征: &str,
|
||||
@@ -186,10 +344,12 @@ impl 买卖点 {
|
||||
"卖"
|
||||
};
|
||||
let 备注 = format!("{}_{}{}{}", 特征, 级别, 序号, 买卖);
|
||||
let 破位值 = 买卖点分型.分型特征值;
|
||||
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);
|
||||
|
||||
let 类型 = match (序号, 买卖) {
|
||||
("一", "买") => 买卖点类型::一买,
|
||||
@@ -198,9 +358,238 @@ impl 买卖点 {
|
||||
("二", "卖") => 买卖点类型::二卖,
|
||||
("三", "买") => 买卖点类型::三买,
|
||||
("三", "卖") => 买卖点类型::三卖,
|
||||
("T1", "买") => 买卖点类型::T1买,
|
||||
("T1", "卖") => 买卖点类型::T1卖,
|
||||
("T1P", "买") => 买卖点类型::T1P买,
|
||||
("T1P", "卖") => 买卖点类型::T1P卖,
|
||||
("T2", "买") => 买卖点类型::T2买,
|
||||
("T2", "卖") => 买卖点类型::T2卖,
|
||||
("T2S", "买") => 买卖点类型::T2S买,
|
||||
("T2S", "卖") => 买卖点类型::T2S卖,
|
||||
("T3A", "买") => 买卖点类型::T3A买,
|
||||
("T3A", "卖") => 买卖点类型::T3A卖,
|
||||
("T3B", "买") => 买卖点类型::T3B买,
|
||||
("T3B", "卖") => 买卖点类型::T3B卖,
|
||||
_ => 买卖点类型::一买, // fallback
|
||||
};
|
||||
|
||||
基础买卖点::new(类型, 当前K线, 买卖点分型, 备注, 破位值)
|
||||
let mut bsp = 基础买卖点::new(类型, 当前K线, 买卖点分型, 备注, 破位值);
|
||||
bsp.当前缠K序号 = Some(当前缠K序号);
|
||||
bsp
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::kline::bar::K线;
|
||||
use crate::kline::chan_kline::缠论K线;
|
||||
use crate::structure::fractal_obj::分型;
|
||||
use crate::types::分型结构;
|
||||
use crate::types::相对方向;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
fn 辅助_创建普K(时间戳: i64, 序号: i64) -> Arc<K线> {
|
||||
Arc::new(K线 {
|
||||
时间戳,
|
||||
序号,
|
||||
高: 100.0,
|
||||
低: 90.0,
|
||||
开盘价: 95.0,
|
||||
收盘价: 95.0,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn 辅助_创建缠K(
|
||||
序号: i64,
|
||||
时间戳: i64,
|
||||
高: f64,
|
||||
低: f64,
|
||||
分型: Option<分型结构>,
|
||||
) -> Arc<缠论K线> {
|
||||
let 普K = 辅助_创建普K(时间戳, 0);
|
||||
Arc::new(缠论K线::创建缠K(
|
||||
时间戳,
|
||||
高,
|
||||
低,
|
||||
相对方向::向上,
|
||||
分型,
|
||||
序号,
|
||||
普K,
|
||||
None,
|
||||
))
|
||||
}
|
||||
|
||||
fn 辅助_创建底分型_中(序号: i64, 时间戳: i64) -> Arc<分型> {
|
||||
let 中 = 辅助_创建缠K(序号, 时间戳, 100.0, 90.0, Some(分型结构::底));
|
||||
中.序号.store(序号, Ordering::Relaxed);
|
||||
中.分型特征值.set(90.0);
|
||||
let 左 = 辅助_创建缠K(序号 - 1, 时间戳 - 100, 100.0, 92.0, Some(分型结构::下));
|
||||
左.序号.store(序号 - 1, Ordering::Relaxed);
|
||||
let 右 = 辅助_创建缠K(序号 + 1, 时间戳 + 100, 100.0, 92.0, Some(分型结构::上));
|
||||
右.序号.store(序号 + 1, Ordering::Relaxed);
|
||||
Arc::new(分型::new(Some(左), 中, Some(右)))
|
||||
}
|
||||
|
||||
fn 辅助_创建顶分型_中(序号: i64, 时间戳: i64) -> Arc<分型> {
|
||||
let 中 = 辅助_创建缠K(序号, 时间戳, 100.0, 90.0, Some(分型结构::顶));
|
||||
中.序号.store(序号, Ordering::Relaxed);
|
||||
中.分型特征值.set(100.0);
|
||||
let 左 = 辅助_创建缠K(序号 - 1, 时间戳 - 100, 98.0, 88.0, Some(分型结构::上));
|
||||
左.序号.store(序号 - 1, Ordering::Relaxed);
|
||||
let 右 = 辅助_创建缠K(序号 + 1, 时间戳 + 100, 98.0, 88.0, Some(分型结构::下));
|
||||
右.序号.store(序号 + 1, Ordering::Relaxed);
|
||||
Arc::new(分型::new(Some(左), 中, Some(右)))
|
||||
}
|
||||
|
||||
// ========== 基础买卖点 构造测试 ==========
|
||||
|
||||
#[test]
|
||||
fn test_基础买卖点_new() {
|
||||
let 分型 = 辅助_创建底分型_中(10, 1000);
|
||||
let 当前K = 辅助_创建普K(1100, 0);
|
||||
let bsp = 基础买卖点::new(买卖点类型::一买, 当前K, 分型.clone(), "测试".into(), 90.0);
|
||||
assert_eq!(bsp.备注, "测试");
|
||||
assert_eq!(bsp.类型, 买卖点类型::一买);
|
||||
assert_eq!(bsp.破位值, 90.0);
|
||||
assert!(bsp.失效K线.is_none());
|
||||
assert!(!bsp.有效性());
|
||||
}
|
||||
|
||||
// ========== 偏移 测试 ==========
|
||||
|
||||
#[test]
|
||||
fn test_偏移_无缠K序号_退化为bar序号差值() {
|
||||
let 分型 = 辅助_创建底分型_中(10, 1000);
|
||||
let 当前K = 辅助_创建普K(1100, 15); // bar 序号=15
|
||||
let bsp = 基础买卖点::new(买卖点类型::一买, 当前K, 分型.clone(), "".into(), 0.0);
|
||||
// 无当前缠K序号: 偏移 = 15 - 10 = 5
|
||||
assert_eq!(bsp.偏移(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_偏移_有缠K序号_使用缠K序号差值() {
|
||||
let 分型 = 辅助_创建底分型_中(10, 1000);
|
||||
let 当前K = 辅助_创建普K(1100, 100); // bar 序号(不使用)
|
||||
let mut bsp = 基础买卖点::new(买卖点类型::一买, 当前K, 分型.clone(), "".into(), 0.0);
|
||||
bsp.当前缠K序号 = Some(15);
|
||||
// 有当前缠K序号: 偏移 = 15 - 10 = 5
|
||||
assert_eq!(bsp.偏移(), 5);
|
||||
}
|
||||
|
||||
// ========== 失效偏移 测试 ==========
|
||||
|
||||
#[test]
|
||||
fn test_失效偏移_无失效K线返回负一() {
|
||||
let 分型 = 辅助_创建底分型_中(10, 1000);
|
||||
let 当前K = 辅助_创建普K(1100, 0);
|
||||
let bsp = 基础买卖点::new(买卖点类型::一买, 当前K, 分型.clone(), "".into(), 0.0);
|
||||
assert_eq!(bsp.失效偏移(), -1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_失效偏移_有失效K线() {
|
||||
let 分型 = 辅助_创建底分型_中(10, 1000);
|
||||
let 当前K = 辅助_创建普K(1100, 0);
|
||||
let mut bsp = 基础买卖点::new(买卖点类型::一买, 当前K, 分型.clone(), "".into(), 0.0);
|
||||
bsp.失效K线 = Some(辅助_创建普K(1200, 20)); // 序号=20
|
||||
// 失效偏移 = 20 - 10 = 10
|
||||
assert_eq!(bsp.失效偏移(), 10);
|
||||
}
|
||||
|
||||
// ========== 有效性 测试 ==========
|
||||
|
||||
#[test]
|
||||
fn test_有效性_无失效K线为false() {
|
||||
let 分型 = 辅助_创建底分型_中(10, 1000);
|
||||
let 当前K = 辅助_创建普K(1100, 0);
|
||||
let bsp = 基础买卖点::new(买卖点类型::一买, 当前K, 分型.clone(), "".into(), 0.0);
|
||||
assert!(!bsp.有效性());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_有效性_有失效K线为true() {
|
||||
let 分型 = 辅助_创建底分型_中(10, 1000);
|
||||
let 当前K = 辅助_创建普K(1100, 0);
|
||||
let mut bsp = 基础买卖点::new(买卖点类型::一买, 当前K, 分型.clone(), "".into(), 0.0);
|
||||
bsp.失效K线 = Some(辅助_创建普K(1200, 0));
|
||||
assert!(bsp.有效性());
|
||||
}
|
||||
|
||||
// ========== 生成买卖点 测试 ==========
|
||||
|
||||
#[test]
|
||||
fn test_生成买卖点_一买() {
|
||||
let 分型 = 辅助_创建底分型_中(5, 1000);
|
||||
let 当前缠K = 分型.中.clone();
|
||||
let bsp = 买卖点::生成买卖点("特征A", "一", "本级", 分型.clone(), 当前缠K);
|
||||
assert_eq!(bsp.类型, 买卖点类型::一买);
|
||||
assert_eq!(bsp.当前缠K序号, Some(5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_生成买卖点_一卖() {
|
||||
let 分型 = 辅助_创建顶分型_中(5, 1000);
|
||||
let 当前缠K = 分型.中.clone();
|
||||
let bsp = 买卖点::生成买卖点("特征A", "一", "本级", 分型.clone(), 当前缠K);
|
||||
assert_eq!(bsp.类型, 买卖点类型::一卖);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_生成买卖点_二买() {
|
||||
let 分型 = 辅助_创建底分型_中(5, 1000);
|
||||
let 当前缠K = 分型.中.clone();
|
||||
let bsp = 买卖点::生成买卖点("特征B", "二", "同级", 分型.clone(), 当前缠K);
|
||||
assert_eq!(bsp.类型, 买卖点类型::二买);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_生成买卖点_三卖() {
|
||||
let 分型 = 辅助_创建顶分型_中(5, 1000);
|
||||
let 当前缠K = 分型.中.clone();
|
||||
let bsp = 买卖点::生成买卖点("特征C", "三", "本级", 分型.clone(), 当前缠K);
|
||||
assert_eq!(bsp.类型, 买卖点类型::三卖);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_生成买卖点_T1买() {
|
||||
let 分型 = 辅助_创建底分型_中(5, 1000);
|
||||
let 当前缠K = 分型.中.clone();
|
||||
let bsp = 买卖点::生成买卖点("事后", "T1", "次级", 分型.clone(), 当前缠K);
|
||||
assert_eq!(bsp.类型, 买卖点类型::T1买);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_生成买卖点_T2S卖() {
|
||||
let 分型 = 辅助_创建顶分型_中(5, 1000);
|
||||
let 当前缠K = 分型.中.clone();
|
||||
let bsp = 买卖点::生成买卖点("特征D", "T2S", "同级", 分型.clone(), 当前缠K);
|
||||
assert_eq!(bsp.类型, 买卖点类型::T2S卖);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_生成买卖点_T3A买() {
|
||||
let 分型 = 辅助_创建底分型_中(5, 1000);
|
||||
let 当前缠K = 分型.中.clone();
|
||||
let bsp = 买卖点::生成买卖点("特征E", "T3A", "本级", 分型.clone(), 当前缠K);
|
||||
assert_eq!(bsp.类型, 买卖点类型::T3A买);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_生成买卖点_T3B买() {
|
||||
let 分型 = 辅助_创建底分型_中(5, 1000);
|
||||
let 当前缠K = 分型.中.clone();
|
||||
let bsp = 买卖点::生成买卖点("特征F", "T3B", "本级", 分型.clone(), 当前缠K);
|
||||
assert_eq!(bsp.类型, 买卖点类型::T3B买);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_生成买卖点_破位值来自分型特征值() {
|
||||
let 分型 = 辅助_创建底分型_中(5, 1000);
|
||||
let 当前缠K = 分型.中.clone();
|
||||
let bsp = 买卖点::生成买卖点("特征G", "一", "本级", 分型.clone(), 当前缠K);
|
||||
assert_eq!(bsp.破位值, 分型.分型特征值());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,22 +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;
|
||||
|
||||
/// 立体分析器 — 多周期协调器
|
||||
///
|
||||
/// 包含一个K线合成器和每周期一个观察者。
|
||||
/// 输入最小周期K线,合成大周期后分发到对应观察者。
|
||||
pub struct 立体分析器 {
|
||||
pub 周期组: Vec<i64>,
|
||||
输入周期: i64,
|
||||
K线合成器: K线合成器,
|
||||
单体分析器: HashMap<i64, Arc<RwLock<观察者>>>,
|
||||
pub K线合成器: K线合成器,
|
||||
pub 单体分析器: HashMap<i64, Arc<RwLock<观察者>>>,
|
||||
}
|
||||
|
||||
impl 立体分析器 {
|
||||
/// 创建立体分析器 — 对应 Python 立体分析器.__init__
|
||||
pub fn new(
|
||||
符号: String,
|
||||
周期组: Vec<i64>,
|
||||
@@ -51,26 +50,61 @@ impl 立体分析器 {
|
||||
let mut 周期组 = 周期组;
|
||||
周期组.sort();
|
||||
let 输入周期 = 周期组[0];
|
||||
let 显示周期 = 周期组[1];
|
||||
|
||||
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(), 周期, 当前配置);
|
||||
单体分析器.insert(周期, 观察员);
|
||||
}
|
||||
|
||||
// 显示周期特殊配置
|
||||
{
|
||||
let 显示观察员 = 单体分析器.get(&显示周期).expect("显示周期观察者不存在");
|
||||
let mut guard = 显示观察员.write();
|
||||
guard.配置.图表展示标签 = None; // None = 全部展示
|
||||
guard.配置.图表展示 = true;
|
||||
guard.重置基础序列();
|
||||
}
|
||||
|
||||
// 非显示周期的基础缠K序列对齐至显示周期
|
||||
{
|
||||
let 显示缠K序列 = 单体分析器
|
||||
.get(&显示周期)
|
||||
.map(|o| o.read().缠论K线序列.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
for &周期 in &周期组 {
|
||||
if 周期 != 显示周期
|
||||
&& let Some(观察员) = 单体分析器.get(&周期)
|
||||
{
|
||||
观察员.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 {
|
||||
周期组,
|
||||
输入周期,
|
||||
@@ -79,26 +113,36 @@ 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.输入周期 {
|
||||
eprintln!(
|
||||
panic!(
|
||||
"立体分析器.投喂K线 周期不匹配 {} != {}",
|
||||
普K.周期, self.输入周期
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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线);
|
||||
}
|
||||
}
|
||||
self.K线合成器.投喂K线(普K);
|
||||
}
|
||||
|
||||
/// 获取指定周期的观察者
|
||||
@@ -106,40 +150,41 @@ impl 立体分析器 {
|
||||
self.单体分析器.get(&周期).cloned()
|
||||
}
|
||||
|
||||
/// 测试_保存数据 — 多级别数据拆分保存
|
||||
/// 创建父目录 PyM_{标识}_{起始时间}_{结束时间},各周期观察者保存到子目录
|
||||
pub fn 测试_保存数据(&self) {
|
||||
let 根目录 = std::env::var("CHANLUN_DATA_DIR")
|
||||
.map(std::path::PathBuf::from)
|
||||
.unwrap_or_else(|_| std::env::temp_dir());
|
||||
/// 测试_保存数据 — 对应 Python 立体分析器.测试_保存数据
|
||||
pub fn 测试_保存数据(&self, root: Option<&str>) {
|
||||
let 根目录 = match root {
|
||||
Some(r) => std::path::PathBuf::from(r),
|
||||
None => std::env::var("CHANLUN_DATA_DIR")
|
||||
.map(std::path::PathBuf::from)
|
||||
.unwrap_or_else(|_| std::env::temp_dir()),
|
||||
};
|
||||
|
||||
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_{}:{}_{}_{}", 标识, 周期, 起始时间, 结束时间);
|
||||
let 保存路径 = 根目录.join(&目录标识);
|
||||
|
||||
if let Err(e) = std::fs::create_dir_all(&保存路径) {
|
||||
eprintln!("创建目录失败: {} -> {}", 保存路径.display(), e);
|
||||
error!("创建目录失败: {} -> {}", 保存路径.display(), e);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -147,11 +192,36 @@ impl 立体分析器 {
|
||||
if let Some(观察员) = self.单体分析器.get(周期) {
|
||||
观察员
|
||||
.read()
|
||||
.unwrap()
|
||||
.测试_保存数据(Some(&保存路径.to_string_lossy()));
|
||||
}
|
||||
}
|
||||
|
||||
println!("多级别数据拆分保存完成,目录:{}", 保存路径.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!("{标签}:所有周期观察者全量校验全部一致"))
|
||||
}
|
||||
}
|
||||
|
||||
+775
-389
File diff suppressed because it is too large
Load Diff
@@ -23,18 +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线合成器 {
|
||||
pub fn new(标识: String, 周期组: Vec<i64>) -> Self {
|
||||
/// 创建K线合成器 — 对应 Python K线合成器.__init__(标识, 周期组, 事件回调=None)
|
||||
pub fn new(
|
||||
标识: String, 周期组: Vec<i64>, 事件回调: Option<合成器事件回调>
|
||||
) -> Self {
|
||||
let mut 周期组 = 周期组;
|
||||
周期组.sort();
|
||||
|
||||
@@ -50,23 +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) {
|
||||
let 普K = K线::创建普K(&self.标识, 时间戳, 开, 高, 低, 收, 量, 0, 0);
|
||||
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()
|
||||
@@ -76,26 +94,25 @@ 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 {
|
||||
return 时间戳;
|
||||
panic!("_对齐时间戳: 周期不能为0");
|
||||
}
|
||||
(时间戳 / 周期) * 周期
|
||||
}
|
||||
|
||||
/// 创建新K线 — 对应 Python `_创建新K线`
|
||||
fn _创建新K线(&self, 周期: i64, 时间戳: i64, 普K: &K线) -> K线 {
|
||||
let 序号 = self
|
||||
.合成K线列表
|
||||
@@ -117,6 +134,7 @@ impl K线合成器 {
|
||||
)
|
||||
}
|
||||
|
||||
/// 更新K线 — 对应 Python `_更新K线`
|
||||
fn _更新K线(当前K线: &mut K线, 新数据: &K线) {
|
||||
当前K线.高 = 当前K线.高.max(新数据.高);
|
||||
当前K线.低 = 当前K线.低.min(新数据.低);
|
||||
@@ -124,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(&周期)
|
||||
@@ -136,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());
|
||||
}
|
||||
}
|
||||
|
||||
+410
-126
@@ -22,126 +22,137 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
use crate::warn;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
|
||||
fn is_infinite_f64(v: &f64) -> bool {
|
||||
v.is_infinite()
|
||||
}
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// 缠论配置 —— 控制所有分析阶段的行为
|
||||
///
|
||||
/// 所有字段带默认值,使用 `#[serde(default)]` 实现缺失字段容错
|
||||
/// 50+ 参数集中控制缠K合并、笔/线段划分、中枢识别、买卖点生成等所有阶段。
|
||||
/// 所有字段带默认值,使用 `#[serde(default)]` 实现缺失字段容错。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct 缠论配置 {
|
||||
// ---- 基础 ----
|
||||
/// 品种标识(如 "btcusd")
|
||||
pub 标识: String,
|
||||
|
||||
// ---- 缠K ----
|
||||
/// 包含处理时使用合并替换模式(而非添加模式)
|
||||
pub 缠K合并替换: bool,
|
||||
|
||||
// ---- 笔 ----
|
||||
/// 笔内最少缠K数量(含端点)
|
||||
pub 笔内元素数量: i64,
|
||||
/// 笔内相同终点取舍开关
|
||||
pub 笔内相同终点取舍: bool,
|
||||
/// 笔内起始分型包含整笔
|
||||
pub 笔内起始分型包含整笔: bool,
|
||||
/// 笔内起始分型包含整笔(含右端点)
|
||||
pub 笔内起始分型包含整笔_包括右: bool,
|
||||
/// 笔内原始K线包含整笔
|
||||
pub 笔内原始K线包含整笔: bool,
|
||||
/// 笔次级成笔(允许在非分型处成笔)
|
||||
pub 笔次级成笔: bool,
|
||||
/// 笔弱化开关(允许更少元素成笔)
|
||||
pub 笔弱化: bool,
|
||||
/// 笔弱化模式下的最小原始K线数
|
||||
pub 笔弱化_原始数量: i64,
|
||||
|
||||
// ---- 线段 ----
|
||||
/// 线段非缺口下的穿刺处理
|
||||
pub 线段_非缺口下穿刺: bool,
|
||||
/// 线段特征序列忽略老阴老阳
|
||||
pub 线段_特征序列忽视老阴老阳: bool,
|
||||
/// 线段缺口后紧急修正
|
||||
pub 线段_缺口后紧急修正: bool,
|
||||
/// 线段修正开关
|
||||
pub 线段_修正: bool,
|
||||
/// 线段内部中枢图显示
|
||||
pub 线段内部中枢图显: bool,
|
||||
/// 扩展线段当下分析模式
|
||||
pub 扩展线段_当下分析: bool,
|
||||
|
||||
// ---- 分析开关 ----
|
||||
/// 是否分析笔
|
||||
pub 分析笔: bool,
|
||||
/// 是否分析线段
|
||||
pub 分析线段: bool,
|
||||
/// 是否分析扩展线段
|
||||
pub 分析扩展线段: bool,
|
||||
/// 是否分析笔中枢
|
||||
pub 分析笔中枢: bool,
|
||||
/// 是否分析线段中枢
|
||||
pub 分析线段中枢: bool,
|
||||
|
||||
// ---- 终止 ----
|
||||
/// 手动终止时间(时间字符串,非空时生效)
|
||||
pub 手动终止: String,
|
||||
|
||||
// ---- 指标 ----
|
||||
/// 是否计算技术指标
|
||||
pub 计算指标: bool,
|
||||
/// 指标计算方式(均线使用,MACD/RSI/KDJ/BOLL 在参数元组中指定)
|
||||
#[serde(deserialize_with = "deserialize_指标计算方式")]
|
||||
pub 指标计算方式: String,
|
||||
|
||||
// ---- MACD ----
|
||||
pub 平滑异同移动平均线_快线周期: i64,
|
||||
pub 平滑异同移动平均线_慢线周期: i64,
|
||||
pub 平滑异同移动平均线_信号周期: i64,
|
||||
/// MACD 参数列表 (key, 计算方式, 快线, 慢线, 信号)
|
||||
#[serde(default)]
|
||||
pub MACD_参数列表: Vec<(String, String, i64, i64, i64)>,
|
||||
|
||||
// ---- RSI ----
|
||||
pub 相对强弱指数_周期: i64,
|
||||
pub 相对强弱指数_移动平均线周期: i64,
|
||||
pub 相对强弱指数_超买阈值: f64,
|
||||
pub 相对强弱指数_超卖阈值: f64,
|
||||
/// RSI 参数列表 (key, 计算方式, 周期, MA周期, 超买, 超卖)
|
||||
#[serde(default)]
|
||||
pub RSI_周期列表: Vec<(String, String, i64, i64, f64, f64)>,
|
||||
|
||||
// ---- KDJ ----
|
||||
pub 随机指标_RSV周期: i64,
|
||||
pub 随机指标_K值平滑周期: i64,
|
||||
pub 随机指标_D值平滑周期: i64,
|
||||
pub 随机指标_超买阈值: f64,
|
||||
pub 随机指标_超卖阈值: f64,
|
||||
/// KDJ 参数列表 (key, 计算方式, RSV, K平滑, D平滑, 超买, 超卖)
|
||||
#[serde(default)]
|
||||
pub KDJ_参数列表: Vec<(String, String, i64, i64, i64, f64, f64)>,
|
||||
|
||||
/// BOLL 参数列表 (key, 计算方式, 周期, 标准差倍数)
|
||||
#[serde(default)]
|
||||
pub BOLL_参数列表: Vec<(String, String, i64, f64)>,
|
||||
|
||||
/// 均线参数列表 (key, 计算方式, 类型, 周期) — 如 ("SMA_5", "收", "SMA", 5)
|
||||
#[serde(default)]
|
||||
pub 均线参数列表: Vec<(String, String, String, i64)>,
|
||||
|
||||
// ---- 推送/显示 ----
|
||||
pub 图表展示: bool,
|
||||
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>>,
|
||||
|
||||
// ---- 买卖点 ----
|
||||
/// 买卖点偏移量
|
||||
pub 买卖点偏移: i64,
|
||||
/// 买卖点激进识别模式
|
||||
pub 买卖点激进识别: bool,
|
||||
/// 买卖点与MACD柱强相关
|
||||
pub 买卖点与MACD柱强相关: bool,
|
||||
/// 买卖点错过误差值
|
||||
pub 买卖点错过误差值: f64,
|
||||
/// 买卖点指标模式(任意/配置/全量/相对)
|
||||
#[serde(deserialize_with = "deserialize_买卖点_指标模式")]
|
||||
pub 买卖点_指标模式: String,
|
||||
/// 买卖点指标匹配 MACD
|
||||
pub 买卖点_指标匹配_MACD: bool,
|
||||
/// 买卖点指标匹配 KDJ
|
||||
pub 买卖点_指标匹配_KDJ: bool,
|
||||
/// 买卖点指标匹配 RSI
|
||||
pub 买卖点_指标匹配_RSI: bool,
|
||||
#[serde(skip_serializing_if = "is_infinite_f64")]
|
||||
pub 买卖点_背离率: f64,
|
||||
pub 买卖点_T2_回调阈值: f64,
|
||||
pub 买卖点_T2S_最大层级: i64,
|
||||
pub 买卖点_峰值条件: bool,
|
||||
pub 买卖点_计算方式: String,
|
||||
pub 买卖点_计算线段BSP1: bool,
|
||||
pub 买卖点_处理BSP2: bool,
|
||||
pub 买卖点_计算线段BSP3: bool,
|
||||
pub 买卖点_依赖T1: bool,
|
||||
pub 买卖点_中枢来源: String,
|
||||
pub 买卖点_调试输出: bool,
|
||||
|
||||
// ---- 背驰 ----
|
||||
/// 线段内部背驰使用 MACD
|
||||
pub 线段内部背驰_MACD: bool,
|
||||
/// 线段内部背驰使用斜率
|
||||
pub 线段内部背驰_斜率: bool,
|
||||
/// 线段内部背驰使用测度
|
||||
pub 线段内部背驰_测度: bool,
|
||||
/// 线段内部背驰模式(任意/配置/全量/相对)
|
||||
#[serde(deserialize_with = "deserialize_线段内部背驰_模式")]
|
||||
pub 线段内部背驰_模式: String,
|
||||
|
||||
// ---- 文件 ----
|
||||
/// 加载数据文件路径
|
||||
pub 加载文件路径: String,
|
||||
}
|
||||
|
||||
@@ -163,7 +174,9 @@ where
|
||||
if VALID.contains(&s.as_str()) {
|
||||
Ok(s)
|
||||
} else {
|
||||
eprintln!("\x1b[33m[配置警告]\x1b[m 指标计算方式: \"{s}\" 不在有效值 {VALID:?} 内,已使用默认值 \"{DEFAULT}\"");
|
||||
warn!(
|
||||
"[配置警告] 指标计算方式: \"{s}\" 不在有效值 {VALID:?} 内,已使用默认值 \"{DEFAULT}\""
|
||||
);
|
||||
Ok(DEFAULT.to_string())
|
||||
}
|
||||
}
|
||||
@@ -178,7 +191,9 @@ where
|
||||
if VALID.contains(&s.as_str()) {
|
||||
Ok(s)
|
||||
} else {
|
||||
eprintln!("\x1b[33m[配置警告]\x1b[m 买卖点_指标模式: \"{s}\" 不在有效值 {VALID:?} 内,已使用默认值 \"{DEFAULT}\"");
|
||||
warn!(
|
||||
"[配置警告] 买卖点_指标模式: \"{s}\" 不在有效值 {VALID:?} 内,已使用默认值 \"{DEFAULT}\""
|
||||
);
|
||||
Ok(DEFAULT.to_string())
|
||||
}
|
||||
}
|
||||
@@ -193,7 +208,9 @@ where
|
||||
if VALID.contains(&s.as_str()) {
|
||||
Ok(s)
|
||||
} else {
|
||||
eprintln!("\x1b[33m[配置警告]\x1b[m 线段内部背驰_模式: \"{s}\" 不在有效值 {VALID:?} 内,已使用默认值 \"{DEFAULT}\"");
|
||||
warn!(
|
||||
"[配置警告] 线段内部背驰_模式: \"{s}\" 不在有效值 {VALID:?} 内,已使用默认值 \"{DEFAULT}\""
|
||||
);
|
||||
Ok(DEFAULT.to_string())
|
||||
}
|
||||
}
|
||||
@@ -225,34 +242,13 @@ impl Default for 缠论配置 {
|
||||
手动终止: String::new(),
|
||||
计算指标: true,
|
||||
指标计算方式: "收".into(),
|
||||
平滑异同移动平均线_快线周期: 13,
|
||||
平滑异同移动平均线_慢线周期: 31,
|
||||
平滑异同移动平均线_信号周期: 11,
|
||||
相对强弱指数_周期: 13,
|
||||
相对强弱指数_移动平均线周期: 13,
|
||||
相对强弱指数_超买阈值: 75.0,
|
||||
相对强弱指数_超卖阈值: 25.0,
|
||||
随机指标_RSV周期: 13,
|
||||
随机指标_K值平滑周期: 5,
|
||||
随机指标_D值平滑周期: 5,
|
||||
随机指标_超买阈值: 80.0,
|
||||
随机指标_超卖阈值: 20.0,
|
||||
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,
|
||||
@@ -261,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,
|
||||
@@ -282,43 +267,194 @@ impl Default for 缠论配置 {
|
||||
}
|
||||
|
||||
impl 缠论配置 {
|
||||
/// 展示标签判定 — None=全部, [] = 全关
|
||||
pub fn 展示标签(&self, 标签: &str) -> bool {
|
||||
match &self.图表展示标签 {
|
||||
None => true,
|
||||
Some(tags) => tags.iter().any(|t| t == 标签),
|
||||
}
|
||||
}
|
||||
|
||||
/// 统一设置所有指标参数(对应 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// 序列化为 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
|
||||
}
|
||||
}
|
||||
|
||||
/// 从 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())
|
||||
}
|
||||
}
|
||||
|
||||
/// 验证并修正字段值(对应 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 字符串
|
||||
pub fn to_json(&self) -> String {
|
||||
serde_json::to_string_pretty(self).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// 从 JSON 字符串反序列化
|
||||
pub fn from_json(json_str: &str) -> Result<Self, serde_json::Error> {
|
||||
serde_json::from_str(json_str)
|
||||
}
|
||||
|
||||
/// 保存配置到 JSON 文件
|
||||
pub fn 保存配置(&self, path: &str) -> std::io::Result<()> {
|
||||
std::fs::write(path, self.to_json())
|
||||
}
|
||||
|
||||
/// 从 JSON 文件加载配置
|
||||
pub fn 加载配置(path: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let config = Self::from_json(&content)?;
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -336,11 +472,11 @@ impl 缠论配置 {
|
||||
serde_json::Map<String, serde_json::Value>,
|
||||
> = std::collections::BTreeMap::new();
|
||||
for (key, value) in map {
|
||||
if let Some(pos) = key.find('_') {
|
||||
if let Ok(num) = key[..pos].parse::<i64>() {
|
||||
let field = key[pos + 1..].to_string();
|
||||
groups.entry(num).or_default().insert(field, value.clone());
|
||||
}
|
||||
if let Some(pos) = key.find('_')
|
||||
&& let Ok(num) = key[..pos].parse::<i64>()
|
||||
{
|
||||
let field = key[pos + 1..].to_string();
|
||||
groups.entry(num).or_default().insert(field, value.clone());
|
||||
}
|
||||
}
|
||||
for (num, fields) in groups {
|
||||
@@ -357,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) {
|
||||
if self_val != other_val {
|
||||
diffs.push(key.clone());
|
||||
}
|
||||
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.insert(key.to_string(), v.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -395,7 +533,6 @@ mod tests {
|
||||
let config = 缠论配置::default();
|
||||
assert_eq!(config.标识, "bar");
|
||||
assert_eq!(config.笔内元素数量, 5);
|
||||
assert!(config.买卖点_背离率.is_infinite());
|
||||
assert_eq!(config.指标计算方式, "收");
|
||||
}
|
||||
|
||||
@@ -442,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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* 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::kline::bar::K线;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// 布林带(BOLL)— 基于移动平均和标准差的波动率通道
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct 布林带 {
|
||||
/// 数据时间戳
|
||||
pub 时间戳: i64,
|
||||
/// 计算周期
|
||||
pub 周期: usize,
|
||||
/// 标准差倍数(通常为 2.0)
|
||||
pub 标准差倍数: f64,
|
||||
/// 上轨(中轨 + 倍数 × 标准差)
|
||||
pub 上轨: f64,
|
||||
/// 中轨(移动平均线)
|
||||
pub 中轨: f64,
|
||||
/// 下轨(中轨 - 倍数 × 标准差)
|
||||
pub 下轨: f64,
|
||||
/// 内部历史队列(不序列化)
|
||||
#[serde(skip)]
|
||||
_历史队列: VecDeque<f64>,
|
||||
/// 内部均值缓存(不序列化)
|
||||
#[serde(skip)]
|
||||
_均值: f64,
|
||||
/// 内部方差和缓存(不序列化)
|
||||
#[serde(skip)]
|
||||
_方差和: f64,
|
||||
}
|
||||
|
||||
impl Default for 布林带 {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
时间戳: 0,
|
||||
周期: 20,
|
||||
标准差倍数: 2.0,
|
||||
上轨: 0.0,
|
||||
中轨: 0.0,
|
||||
下轨: 0.0,
|
||||
_历史队列: VecDeque::new(),
|
||||
_均值: 0.0,
|
||||
_方差和: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl 布林带 {
|
||||
/// 首次计算 BOLL 指标 — 从 K线 取值后计算
|
||||
pub fn 首次计算_K线(
|
||||
k线: &K线, 计算方式: &str, 周期: usize, 标准差倍数: f64
|
||||
) -> Self {
|
||||
let 价格 = crate::indicators::K线取值(k线.开盘价, k线.高, k线.低, k线.收盘价, 计算方式);
|
||||
Self::首次计算(k线.时间戳, 价格, 周期, 标准差倍数)
|
||||
}
|
||||
|
||||
/// 增量计算 BOLL 指标 — 从 K线 取值后递推
|
||||
pub fn 增量计算_K线(prev: &布林带, 当前K线: &K线, 计算方式: &str) -> Self {
|
||||
let 价格 = crate::indicators::K线取值(
|
||||
当前K线.开盘价,
|
||||
当前K线.高,
|
||||
当前K线.低,
|
||||
当前K线.收盘价,
|
||||
计算方式,
|
||||
);
|
||||
Self::增量计算(prev, 当前K线.时间戳, 价格)
|
||||
}
|
||||
|
||||
/// 首次计算 — 初始时上中下轨都等于当前价格
|
||||
pub fn 首次计算(时间戳: i64, 价格: f64, 周期: usize, 标准差倍数: f64) -> Self {
|
||||
Self {
|
||||
时间戳,
|
||||
周期,
|
||||
标准差倍数,
|
||||
上轨: 价格,
|
||||
中轨: 价格,
|
||||
下轨: 价格,
|
||||
_历史队列: VecDeque::from([价格]),
|
||||
_均值: 价格,
|
||||
_方差和: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// 增量计算 — 基于前一个布林带状态递推计算新的布林带
|
||||
pub fn 增量计算(prev: &布林带, 时间戳: i64, 价格: f64) -> Self {
|
||||
let 周期 = prev.周期;
|
||||
let 标准差倍数 = prev.标准差倍数;
|
||||
|
||||
let mut q = prev._历史队列.clone();
|
||||
q.push_back(价格);
|
||||
if q.len() > 周期 {
|
||||
q.pop_front();
|
||||
}
|
||||
|
||||
let (_均值, _方差和) = if q.len() < 周期 {
|
||||
let mean = q.iter().sum::<f64>() / q.len() as f64;
|
||||
let var_sum = q.iter().map(|v| (v - mean).powi(2)).sum();
|
||||
(mean, var_sum)
|
||||
} else {
|
||||
let n = 周期 as f64;
|
||||
let old_val = if prev._历史队列.len() >= 周期 {
|
||||
prev._历史队列[0]
|
||||
} else {
|
||||
q[0]
|
||||
};
|
||||
let new_mean = prev._均值 + (价格 - old_val) / n;
|
||||
let new_var =
|
||||
prev._方差和 + (价格 - old_val) * (价格 - new_mean + old_val - prev._均值);
|
||||
(new_mean, new_var)
|
||||
};
|
||||
|
||||
let std = (_方差和 / q.len() as f64).sqrt();
|
||||
Self {
|
||||
时间戳,
|
||||
周期,
|
||||
标准差倍数,
|
||||
中轨: _均值,
|
||||
上轨: _均值 + 标准差倍数 * std,
|
||||
下轨: _均值 - 标准差倍数 * std,
|
||||
_历史队列: q,
|
||||
_均值,
|
||||
_方差和,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_boll_first() {
|
||||
let b = 布林带::首次计算(1000, 100.0, 20, 2.0);
|
||||
assert!((b.上轨 - 100.0).abs() < 0.01);
|
||||
assert!((b.中轨 - 100.0).abs() < 0.01);
|
||||
assert!((b.下轨 - 100.0).abs() < 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_boll_incremental() {
|
||||
let b1 = 布林带::首次计算(1000, 100.0, 5, 2.0);
|
||||
let b2 = 布林带::增量计算(&b1, 1001, 102.0);
|
||||
assert!(b2.上轨 >= b2.中轨);
|
||||
assert!(b2.下轨 <= b2.中轨);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,698 @@
|
||||
/*
|
||||
* 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 super::container::{指标值, 指标容器};
|
||||
use super::{布林带, 平滑异同移动平均线, 相对强弱指数, 随机指标};
|
||||
use crate::config::缠论配置;
|
||||
use crate::kline::bar::K线;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// 指标计算器 — 在缠K合并之前,增量计算所有开启的指标并挂载到K线上
|
||||
pub struct 指标计算器;
|
||||
|
||||
impl 指标计算器 {
|
||||
/// 增量计算所有开启的指标,将结果写入每一根 K 线。
|
||||
///
|
||||
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::_回填新指标(全序列, 配置);
|
||||
}
|
||||
}
|
||||
|
||||
fn _计算MACD组(当前K线: &K线, prev: Option<&指标容器>, 配置: &缠论配置) {
|
||||
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,
|
||||
super::K线取值(
|
||||
当前K线.开盘价,
|
||||
当前K线.高,
|
||||
当前K线.低,
|
||||
当前K线.收盘价,
|
||||
计算方式,
|
||||
),
|
||||
当前K线.时间戳,
|
||||
))
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
指标值::MACD(平滑异同移动平均线::首次计算(
|
||||
super::K线取值(
|
||||
当前K线.开盘价,
|
||||
当前K线.高,
|
||||
当前K线.低,
|
||||
当前K线.收盘价,
|
||||
计算方式,
|
||||
),
|
||||
当前K线.时间戳,
|
||||
*快,
|
||||
*慢,
|
||||
*信号,
|
||||
))
|
||||
};
|
||||
当前K线.指标.write().设置(key, val.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn _计算RSI组(当前K线: &K线, prev: Option<&指标容器>, 配置: &缠论配置) {
|
||||
for (key, 计算方式, 周期, ma周期, 超买, 超卖) in 配置.RSI_周期列表.iter()
|
||||
{
|
||||
let val = if let Some(prev_val) = prev.and_then(|p| p.获取(key)) {
|
||||
if let 指标值::RSI(prev_rsi) = prev_val {
|
||||
指标值::RSI(相对强弱指数::增量计算(
|
||||
prev_rsi,
|
||||
super::K线取值(
|
||||
当前K线.开盘价,
|
||||
当前K线.高,
|
||||
当前K线.低,
|
||||
当前K线.收盘价,
|
||||
计算方式,
|
||||
),
|
||||
当前K线.时间戳,
|
||||
))
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
指标值::RSI(相对强弱指数::首次计算(
|
||||
super::K线取值(
|
||||
当前K线.开盘价,
|
||||
当前K线.高,
|
||||
当前K线.低,
|
||||
当前K线.收盘价,
|
||||
计算方式,
|
||||
),
|
||||
当前K线.时间戳,
|
||||
*周期,
|
||||
*超买,
|
||||
*超卖,
|
||||
Some(*ma周期),
|
||||
))
|
||||
};
|
||||
当前K线.指标.write().设置(key, val.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn _计算KDJ组(当前K线: &K线, prev: Option<&指标容器>, 配置: &缠论配置) {
|
||||
for (key, _fm, rsv, k平滑, d平滑, 超买, 超卖) in 配置.KDJ_参数列表.iter() {
|
||||
let val = if let Some(prev_val) = prev.and_then(|p| p.获取(key)) {
|
||||
if let 指标值::KDJ(prev_kdj) = prev_val {
|
||||
指标值::KDJ(随机指标::增量计算(
|
||||
prev_kdj,
|
||||
当前K线.高,
|
||||
当前K线.低,
|
||||
当前K线.收盘价,
|
||||
当前K线.时间戳,
|
||||
))
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
指标值::KDJ(随机指标::首次计算(
|
||||
当前K线.高,
|
||||
当前K线.低,
|
||||
当前K线.收盘价,
|
||||
当前K线.时间戳,
|
||||
*rsv,
|
||||
*k平滑,
|
||||
*d平滑,
|
||||
*超买,
|
||||
*超卖,
|
||||
))
|
||||
};
|
||||
当前K线.指标.write().设置(key, val.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn _计算BOLL组(当前K线: &K线, prev: Option<&指标容器>, 配置: &缠论配置) {
|
||||
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,
|
||||
当前K线.时间戳,
|
||||
super::K线取值(
|
||||
当前K线.开盘价,
|
||||
当前K线.高,
|
||||
当前K线.低,
|
||||
当前K线.收盘价,
|
||||
计算方式,
|
||||
),
|
||||
))
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
指标值::BOLL(布林带::首次计算(
|
||||
当前K线.时间戳,
|
||||
super::K线取值(
|
||||
当前K线.开盘价,
|
||||
当前K线.高,
|
||||
当前K线.低,
|
||||
当前K线.收盘价,
|
||||
计算方式,
|
||||
),
|
||||
*周期 as usize,
|
||||
*标准差倍数,
|
||||
))
|
||||
};
|
||||
当前K线.指标.write().设置(key, val.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn _更新均线(当前K线: &K线, 现有序列: &[Arc<K线>], 配置: &缠论配置) {
|
||||
if 配置.均线参数列表.is_empty() {
|
||||
return;
|
||||
}
|
||||
for (key, 计算方式, ma_type, period) in &配置.均线参数列表 {
|
||||
let 值 = match ma_type.as_str() {
|
||||
"SMA" => Self::_增量SMA(当前K线, 现有序列, 计算方式, *period, key),
|
||||
"EMA" => Self::_增量EMA(当前K线, 现有序列, 计算方式, *period, key),
|
||||
_ => continue,
|
||||
};
|
||||
if let Some(均线_map) = 当前K线.指标.write().均线_mut() {
|
||||
均线_map.insert(key.clone(), 值);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn _增量SMA(
|
||||
当前K线: &K线,
|
||||
现有序列: &[Arc<K线>],
|
||||
计算方式: &str,
|
||||
period: i64,
|
||||
prev_key: &str,
|
||||
) -> f64 {
|
||||
let 当前价 = super::K线取值(
|
||||
当前K线.开盘价,
|
||||
当前K线.高,
|
||||
当前K线.低,
|
||||
当前K线.收盘价,
|
||||
计算方式,
|
||||
);
|
||||
let existing_len = 现有序列.len();
|
||||
let p = period as usize;
|
||||
if existing_len < p {
|
||||
let sum: f64 = 现有序列
|
||||
.iter()
|
||||
.map(|k| super::K线取值(k.开盘价, k.高, k.低, k.收盘价, 计算方式))
|
||||
.sum::<f64>()
|
||||
+ 当前价;
|
||||
return sum / ((existing_len + 1) as f64).max(1.0);
|
||||
}
|
||||
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].高,
|
||||
现有序列[existing_len - p].低,
|
||||
现有序列[existing_len - p].收盘价,
|
||||
计算方式,
|
||||
);
|
||||
return prev_sma + (当前价 - oldest) / period as f64;
|
||||
}
|
||||
let sum: f64 = 现有序列[existing_len.saturating_sub(p.saturating_sub(1))..]
|
||||
.iter()
|
||||
.map(|k| super::K线取值(k.开盘价, k.高, k.低, k.收盘价, 计算方式))
|
||||
.sum::<f64>()
|
||||
+ 当前价;
|
||||
sum / ((existing_len + 1) as f64).min(p as f64)
|
||||
}
|
||||
|
||||
fn _增量EMA(
|
||||
当前K线: &K线,
|
||||
现有序列: &[Arc<K线>],
|
||||
计算方式: &str,
|
||||
period: i64,
|
||||
prev_key: &str,
|
||||
) -> f64 {
|
||||
let 当前价 = super::K线取值(
|
||||
当前K线.开盘价,
|
||||
当前K线.高,
|
||||
当前K线.低,
|
||||
当前K线.收盘价,
|
||||
计算方式,
|
||||
);
|
||||
let 前值 = 现有序列
|
||||
.last()
|
||||
.and_then(|k| k.指标.read().均线().and_then(|m| m.get(prev_key)).copied());
|
||||
match 前值 {
|
||||
None => 当前价,
|
||||
Some(prev) => {
|
||||
let k = 2.0 / (period as f64 + 1.0);
|
||||
当前价 * k + prev * (1.0 - k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 运行中新增指标参数时,回填所有历史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.下轨,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* 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 super::{布林带, 平滑异同移动平均线, 相对强弱指数, 随机指标};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// 统一指标值 — 支持所有指标类型的动态注册
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum 指标值 {
|
||||
/// MACD 指标
|
||||
MACD(平滑异同移动平均线),
|
||||
/// RSI 指标
|
||||
RSI(相对强弱指数),
|
||||
/// KDJ 指标
|
||||
KDJ(随机指标),
|
||||
/// 布林带指标
|
||||
BOLL(布林带),
|
||||
/// 均线组 (key → 值)
|
||||
均线(HashMap<String, f64>),
|
||||
/// 单值指标组 (key → 值)
|
||||
单值(HashMap<String, f64>),
|
||||
}
|
||||
|
||||
/// 指标容器 — 挂载在每根 K线上,基于注册表模式持有该时刻所有指标快照
|
||||
///
|
||||
/// 与 Python `指标容器` 保持一致:
|
||||
/// - 复杂指标:MACD/RSI/KDJ/BOLL,通过默认 key("macd"/"rsi"/"kdj"/"boll")访问
|
||||
/// - 多参数变体:key 格式 "MACD_{快}_{慢}_{信号}" / "RSI_{周期}" 等
|
||||
/// - 均线组:通过 `均线` 子映射访问,key 格式 "{类型}_{周期}"
|
||||
/// - 单值指标:通过 `单值` 子映射访问,key 格式 "{名称}_{周期}"
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct 指标容器 {
|
||||
pub _数据: HashMap<String, Option<指标值>>,
|
||||
}
|
||||
|
||||
impl 指标容器 {
|
||||
/// 创建指标容器,预注册 macd/rsi/kdj/boll/均线/单值 默认槽位
|
||||
pub fn new() -> Self {
|
||||
let mut _数据 = HashMap::new();
|
||||
_数据.insert("macd".into(), None);
|
||||
_数据.insert("rsi".into(), None);
|
||||
_数据.insert("kdj".into(), None);
|
||||
_数据.insert("boll".into(), None);
|
||||
_数据.insert("均线".into(), Some(指标值::均线(HashMap::new())));
|
||||
_数据.insert("单值".into(), Some(指标值::单值(HashMap::new())));
|
||||
Self { _数据 }
|
||||
}
|
||||
|
||||
/// 预注册指标(不覆盖已有值)
|
||||
pub fn 注册(&mut self, 名称: &str, 默认值: Option<指标值>) {
|
||||
self._数据.entry(名称.to_string()).or_insert(默认值);
|
||||
}
|
||||
|
||||
/// 按名称获取指标值
|
||||
pub fn 获取(&self, 名称: &str) -> Option<&指标值> {
|
||||
self._数据.get(名称).and_then(|v| v.as_ref())
|
||||
}
|
||||
|
||||
/// 按名称设置指标值
|
||||
pub fn 设置(&mut self, 名称: &str, 值: 指标值) {
|
||||
self._数据.insert(名称.to_string(), Some(值));
|
||||
}
|
||||
|
||||
/// 检查是否包含指定名称的指标
|
||||
pub fn 包含(&self, 名称: &str) -> bool {
|
||||
self._数据.contains_key(名称)
|
||||
}
|
||||
|
||||
// ---- 默认指标便捷访问 ----
|
||||
|
||||
/// 获取默认 MACD 指标
|
||||
pub fn macd(&self) -> Option<&平滑异同移动平均线> {
|
||||
match self._数据.get("macd")?.as_ref()? {
|
||||
指标值::MACD(m) => Some(m),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 克隆获取默认 MACD 指标
|
||||
pub fn macd_cloned(&self) -> Option<平滑异同移动平均线> {
|
||||
self.macd().cloned()
|
||||
}
|
||||
|
||||
/// 设置默认 MACD 指标
|
||||
pub fn set_macd(&mut self, m: 平滑异同移动平均线) {
|
||||
self._数据.insert("macd".into(), Some(指标值::MACD(m)));
|
||||
}
|
||||
|
||||
/// 获取默认 RSI 指标
|
||||
pub fn rsi(&self) -> Option<&相对强弱指数> {
|
||||
match self._数据.get("rsi")?.as_ref()? {
|
||||
指标值::RSI(r) => Some(r),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 克隆获取默认 RSI 指标
|
||||
pub fn rsi_cloned(&self) -> Option<相对强弱指数> {
|
||||
self.rsi().cloned()
|
||||
}
|
||||
|
||||
/// 设置默认 RSI 指标
|
||||
pub fn set_rsi(&mut self, r: 相对强弱指数) {
|
||||
self._数据.insert("rsi".into(), Some(指标值::RSI(r)));
|
||||
}
|
||||
|
||||
/// 获取默认 KDJ 指标
|
||||
pub fn kdj(&self) -> Option<&随机指标> {
|
||||
match self._数据.get("kdj")?.as_ref()? {
|
||||
指标值::KDJ(k) => Some(k),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 克隆获取默认 KDJ 指标
|
||||
pub fn kdj_cloned(&self) -> Option<随机指标> {
|
||||
self.kdj().cloned()
|
||||
}
|
||||
|
||||
/// 设置默认 KDJ 指标
|
||||
pub fn set_kdj(&mut self, k: 随机指标) {
|
||||
self._数据.insert("kdj".into(), Some(指标值::KDJ(k)));
|
||||
}
|
||||
|
||||
/// 获取默认布林带指标
|
||||
pub fn boll(&self) -> Option<&布林带> {
|
||||
match self._数据.get("boll")?.as_ref()? {
|
||||
指标值::BOLL(b) => Some(b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 克隆获取默认布林带指标
|
||||
pub fn boll_cloned(&self) -> Option<布林带> {
|
||||
self.boll().cloned()
|
||||
}
|
||||
|
||||
/// 设置默认布林带指标
|
||||
pub fn set_boll(&mut self, b: 布林带) {
|
||||
self._数据.insert("boll".into(), Some(指标值::BOLL(b)));
|
||||
}
|
||||
|
||||
/// 获取均线组
|
||||
pub fn 均线(&self) -> Option<&HashMap<String, f64>> {
|
||||
match self._数据.get("均线")?.as_ref()? {
|
||||
指标值::均线(m) => Some(m),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取均线组可变引用
|
||||
pub fn 均线_mut(&mut self) -> Option<&mut HashMap<String, f64>> {
|
||||
match self._数据.get_mut("均线")?.as_mut()? {
|
||||
指标值::均线(m) => Some(m),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取单值指标组
|
||||
pub fn 单值(&self) -> Option<&HashMap<String, f64>> {
|
||||
match self._数据.get("单值")?.as_ref()? {
|
||||
指标值::单值(s) => Some(s),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for 指标容器 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let keys: Vec<&str> = self
|
||||
._数据
|
||||
.iter()
|
||||
.filter(|(_, v)| v.is_some())
|
||||
.map(|(k, _)| k.as_str())
|
||||
.collect();
|
||||
write!(f, "指标容器({})", keys.join(", "))
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,9 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
use crate::kline::bar::K线;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// 随机指标 (KDJ)
|
||||
///
|
||||
@@ -30,23 +32,41 @@ use serde::{Deserialize, Serialize};
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct 随机指标 {
|
||||
/// 数据时间戳
|
||||
pub 时间戳: i64,
|
||||
/// 最高价
|
||||
pub 最高价: f64,
|
||||
/// 最低价
|
||||
pub 最低价: f64,
|
||||
/// 收盘价
|
||||
pub 收盘价: f64,
|
||||
/// RSV 周期
|
||||
pub N: i64,
|
||||
/// K 值平滑周期
|
||||
pub M1: i64,
|
||||
/// D 值平滑周期
|
||||
pub M2: i64,
|
||||
/// 超买阈值
|
||||
pub 超买阈值: f64,
|
||||
/// 超卖阈值
|
||||
pub 超卖阈值: f64,
|
||||
/// RSV 值(未成熟随机值)
|
||||
pub RSV: Option<f64>,
|
||||
/// K 值
|
||||
pub K: Option<f64>,
|
||||
/// D 值
|
||||
pub D: Option<f64>,
|
||||
/// J 值 (3K - 2D)
|
||||
pub J: Option<f64>,
|
||||
pub 历史最高价队列: Vec<f64>,
|
||||
pub 历史最低价队列: Vec<f64>,
|
||||
/// 历史最高价队列(滑动窗口)
|
||||
pub 历史最高价队列: VecDeque<f64>,
|
||||
/// 历史最低价队列(滑动窗口)
|
||||
pub 历史最低价队列: VecDeque<f64>,
|
||||
/// 前一个 RSV(用于平滑递推)
|
||||
pub 前一个RSV: Option<f64>,
|
||||
/// 前一个 K(用于平滑递推)
|
||||
pub 前一个K: Option<f64>,
|
||||
/// 前一个 D(用于平滑递推)
|
||||
pub 前一个D: Option<f64>,
|
||||
}
|
||||
|
||||
@@ -66,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,
|
||||
@@ -77,6 +97,9 @@ impl Default for 随机指标 {
|
||||
|
||||
impl 随机指标 {
|
||||
/// 首次计算 KDJ(无历史数据时)
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
/// 首次计算 KDJ — 无历史数据时的初始计算
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn 首次计算(
|
||||
初始最高价: f64,
|
||||
初始最低价: f64,
|
||||
@@ -102,14 +125,47 @@ impl 随机指标 {
|
||||
K: None,
|
||||
D: None,
|
||||
J: None,
|
||||
历史最高价队列: vec![初始最高价],
|
||||
历史最低价队列: vec![初始最低价],
|
||||
历史最高价队列: VecDeque::from([初始最高价]),
|
||||
历史最低价队列: VecDeque::from([初始最低价]),
|
||||
前一个RSV: None,
|
||||
前一个K: None,
|
||||
前一个D: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 首次计算 KDJ 指标 — 从 K线 取值后计算(KDJ 始终使用 高/低/收)
|
||||
pub fn 首次计算_K线(
|
||||
k线: &K线,
|
||||
RSV周期: i64,
|
||||
K值平滑周期: i64,
|
||||
D值平滑周期: i64,
|
||||
超买阈值: f64,
|
||||
超卖阈值: f64,
|
||||
) -> Self {
|
||||
Self::首次计算(
|
||||
k线.高,
|
||||
k线.低,
|
||||
k线.收盘价,
|
||||
k线.时间戳,
|
||||
RSV周期,
|
||||
K值平滑周期,
|
||||
D值平滑周期,
|
||||
超买阈值,
|
||||
超卖阈值,
|
||||
)
|
||||
}
|
||||
|
||||
/// 增量计算 KDJ 指标 — 从 K线 取值后递推
|
||||
pub fn 增量计算_K线(前一个KDJ: &Self, 当前K线: &K线) -> Self {
|
||||
Self::增量计算(
|
||||
前一个KDJ,
|
||||
当前K线.高,
|
||||
当前K线.低,
|
||||
当前K线.收盘价,
|
||||
当前K线.时间戳,
|
||||
)
|
||||
}
|
||||
|
||||
/// 基于前一个 KDJ 增量计算当前 KDJ
|
||||
pub fn 增量计算(
|
||||
前一个KDJ: &Self,
|
||||
@@ -126,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
|
||||
@@ -205,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
use crate::kline::bar::K线;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 平滑异同移动平均线 (MACD)
|
||||
@@ -30,18 +31,29 @@ use serde::{Deserialize, Serialize};
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct 平滑异同移动平均线 {
|
||||
/// 数据时间戳
|
||||
pub 时间戳: i64,
|
||||
/// 收盘价
|
||||
pub 收盘价: f64,
|
||||
/// 快线 EMA 周期
|
||||
pub 快线周期: i64,
|
||||
/// 慢线 EMA 周期
|
||||
pub 慢线周期: i64,
|
||||
/// 信号线周期
|
||||
pub 信号周期: i64,
|
||||
/// DIF 值(快线 - 慢线)
|
||||
pub DIF: Option<f64>,
|
||||
/// DEA 值(DIF 的信号线)
|
||||
pub DEA: Option<f64>,
|
||||
/// MACD 柱(2 * (DIF - DEA))
|
||||
#[serde(rename = "MACD柱")]
|
||||
#[serde(default)]
|
||||
pub MACD柱: f64,
|
||||
/// 快线 EMA 值
|
||||
pub 快线EMA: Option<f64>,
|
||||
/// 慢线 EMA 值
|
||||
pub 慢线EMA: Option<f64>,
|
||||
/// DEA EMA 值
|
||||
pub DEA_EMA: Option<f64>,
|
||||
}
|
||||
|
||||
@@ -97,6 +109,30 @@ impl 平滑异同移动平均线 {
|
||||
}
|
||||
}
|
||||
|
||||
/// 首次计算 MACD 指标 — 从 K线 取值后计算
|
||||
pub fn 首次计算_K线(
|
||||
k线: &K线,
|
||||
计算方式: &str,
|
||||
快线周期: i64,
|
||||
慢线周期: i64,
|
||||
信号周期: i64,
|
||||
) -> Self {
|
||||
let 价格 = super::K线取值(k线.开盘价, k线.高, k线.低, k线.收盘价, 计算方式);
|
||||
Self::首次计算(价格, k线.时间戳, 快线周期, 慢线周期, 信号周期)
|
||||
}
|
||||
|
||||
/// 增量计算 MACD 指标 — 从 K线 取值后递推
|
||||
pub fn 增量计算_K线(前一个MACD: &Self, 当前K线: &K线, 计算方式: &str) -> Self {
|
||||
let 价格 = super::K线取值(
|
||||
当前K线.开盘价,
|
||||
当前K线.高,
|
||||
当前K线.低,
|
||||
当前K线.收盘价,
|
||||
计算方式,
|
||||
);
|
||||
Self::增量计算(前一个MACD, 价格, 当前K线.时间戳)
|
||||
}
|
||||
|
||||
/// 基于前一个 MACD 指标增量计算当前 MACD
|
||||
pub fn 增量计算(前一个MACD: &Self, 当前收盘价: f64, 当前时间: i64) -> Self {
|
||||
// 快线 EMA
|
||||
|
||||
@@ -22,10 +22,16 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
pub mod boll;
|
||||
pub mod calculator;
|
||||
pub mod container;
|
||||
pub mod kdj;
|
||||
pub mod macd;
|
||||
pub mod rsi;
|
||||
|
||||
pub use boll::布林带;
|
||||
pub use calculator::指标计算器;
|
||||
pub use container::{指标值, 指标容器};
|
||||
pub use kdj::随机指标;
|
||||
pub use macd::平滑异同移动平均线;
|
||||
pub use rsi::相对强弱指数;
|
||||
|
||||
@@ -22,7 +22,9 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
use crate::kline::bar::K线;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// 相对强弱指数 (RSI)
|
||||
///
|
||||
@@ -30,20 +32,36 @@ use serde::{Deserialize, Serialize};
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct 相对强弱指数 {
|
||||
/// 数据时间戳
|
||||
pub 时间戳: i64,
|
||||
/// 收盘价
|
||||
pub 收盘价: f64,
|
||||
/// RSI 计算周期
|
||||
pub 周期: i64,
|
||||
/// 超买阈值
|
||||
pub 超买阈值: f64,
|
||||
/// 超卖阈值
|
||||
pub 超卖阈值: f64,
|
||||
/// RSI SMA 平滑周期
|
||||
pub RSI_SMA周期: Option<i64>,
|
||||
/// RSI 值
|
||||
pub RSI: Option<f64>,
|
||||
/// 平均上涨幅度
|
||||
pub 平均上涨: Option<f64>,
|
||||
/// 平均下跌幅度
|
||||
pub 平均下跌: Option<f64>,
|
||||
/// 当前上涨幅度
|
||||
pub 上涨幅度: f64,
|
||||
/// 当前下跌幅度
|
||||
pub 下跌幅度: f64,
|
||||
/// 平滑系数 (1/周期)
|
||||
pub 平滑系数: f64,
|
||||
/// RSI SMA 值
|
||||
pub RSI_SMA: Option<f64>,
|
||||
pub RSI历史队列: Vec<f64>,
|
||||
/// RSI 历史队列(用于滚动计算)
|
||||
pub RSI历史队列: VecDeque<f64>,
|
||||
/// RSI 历史队列运行和(O(1) SMA)
|
||||
pub RSI和: f64,
|
||||
}
|
||||
|
||||
impl Default for 相对强弱指数 {
|
||||
@@ -62,7 +80,8 @@ impl Default for 相对强弱指数 {
|
||||
下跌幅度: 0.0,
|
||||
平滑系数: 0.0,
|
||||
RSI_SMA: None,
|
||||
RSI历史队列: Vec::new(),
|
||||
RSI历史队列: VecDeque::new(),
|
||||
RSI和: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,10 +110,36 @@ impl 相对强弱指数 {
|
||||
下跌幅度: 0.0,
|
||||
平滑系数: 1.0 / 周期 as f64,
|
||||
RSI_SMA: None,
|
||||
RSI历史队列: Vec::new(),
|
||||
RSI历史队列: VecDeque::new(),
|
||||
RSI和: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// 首次计算 RSI 指标 — 从 K线 取值后计算
|
||||
pub fn 首次计算_K线(
|
||||
k线: &K线,
|
||||
计算方式: &str,
|
||||
周期: i64,
|
||||
超买阈值: f64,
|
||||
超卖阈值: f64,
|
||||
RSI_SMA周期: Option<i64>,
|
||||
) -> Self {
|
||||
let 价格 = super::K线取值(k线.开盘价, k线.高, k线.低, k线.收盘价, 计算方式);
|
||||
Self::首次计算(价格, k线.时间戳, 周期, 超买阈值, 超卖阈值, RSI_SMA周期)
|
||||
}
|
||||
|
||||
/// 增量计算 RSI 指标 — 从 K线 取值后递推
|
||||
pub fn 增量计算_K线(前一个RSI: &Self, 当前K线: &K线, 计算方式: &str) -> Self {
|
||||
let 价格 = super::K线取值(
|
||||
当前K线.开盘价,
|
||||
当前K线.高,
|
||||
当前K线.低,
|
||||
当前K线.收盘价,
|
||||
计算方式,
|
||||
);
|
||||
Self::增量计算(前一个RSI, 价格, 当前K线.时间戳)
|
||||
}
|
||||
|
||||
/// 基于前一个 RSI 增量计算当前 RSI
|
||||
pub fn 增量计算(前一个RSI: &Self, 当前收盘价: f64, 当前时间: i64) -> Self {
|
||||
let 周期 = 前一个RSI.周期;
|
||||
@@ -120,32 +165,32 @@ impl 相对强弱指数 {
|
||||
|
||||
// RSI
|
||||
let RSI = if 平均下跌 == 0.0 {
|
||||
if 平均上涨 > 0.0 {
|
||||
100.0
|
||||
} else {
|
||||
50.0
|
||||
}
|
||||
if 平均上涨 > 0.0 { 100.0 } else { 50.0 }
|
||||
} else {
|
||||
let RS = 平均上涨 / 平均下跌;
|
||||
100.0 - (100.0 / (1.0 + RS))
|
||||
};
|
||||
|
||||
// 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 {
|
||||
@@ -163,6 +208,7 @@ impl 相对强弱指数 {
|
||||
平滑系数,
|
||||
RSI_SMA,
|
||||
RSI历史队列,
|
||||
RSI和,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+337
-13
@@ -22,30 +22,71 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
use crate::indicators::{平滑异同移动平均线, 相对强弱指数, 随机指标};
|
||||
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;
|
||||
|
||||
/// 原始K线 (OHLCV + 指标)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
mod rwlock_container_serde {
|
||||
use parking_lot::RwLock;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
/// Serde 序列化辅助(RwLock<指标容器> → 序列化器)
|
||||
pub fn serialize<S>(
|
||||
val: &RwLock<crate::indicators::指标容器>,
|
||||
ser: S,
|
||||
) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
val.read().serialize(ser)
|
||||
}
|
||||
|
||||
/// Serde 反序列化辅助(反序列化器 → RwLock<指标容器>)
|
||||
pub fn deserialize<'de, D>(de: D) -> Result<RwLock<crate::indicators::指标容器>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Ok(RwLock::new(crate::indicators::指标容器::deserialize(
|
||||
de,
|
||||
)?))
|
||||
}
|
||||
}
|
||||
|
||||
/// 原始K线 (OHLCV + 指标容器)
|
||||
///
|
||||
/// 所有指标统一通过 `指标容器` 访问。指标容器使用 RwLock 实现内部可变性,
|
||||
/// 使 `计算并挂载` 能以 `&K线` 共享引用写入指标值。
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub struct K线 {
|
||||
/// 品种标识(如 "btcusd")
|
||||
pub 标识: String,
|
||||
/// K线序号(在序列中的位置)
|
||||
pub 序号: i64,
|
||||
/// 周期(秒),如 300=5分钟, 86400=日线
|
||||
pub 周期: i64,
|
||||
/// Unix 时间戳(秒)
|
||||
pub 时间戳: i64,
|
||||
/// 最高价
|
||||
pub 高: f64,
|
||||
/// 最低价
|
||||
pub 低: f64,
|
||||
/// 开盘价
|
||||
pub 开盘价: f64,
|
||||
/// 收盘价
|
||||
pub 收盘价: f64,
|
||||
/// 成交量
|
||||
pub 成交量: f64,
|
||||
pub macd: Option<平滑异同移动平均线>,
|
||||
pub rsi: Option<相对强弱指数>,
|
||||
pub kdj: Option<随机指标>,
|
||||
/// 指标容器(MACD/RSI/KDJ/BOLL/均线等)
|
||||
#[serde(with = "rwlock_container_serde")]
|
||||
pub 指标: RwLock<指标容器>,
|
||||
}
|
||||
|
||||
impl Default for K线 {
|
||||
@@ -60,9 +101,24 @@ impl Default for K线 {
|
||||
开盘价: 0.0,
|
||||
收盘价: 0.0,
|
||||
成交量: 0.0,
|
||||
macd: None,
|
||||
rsi: None,
|
||||
kdj: None,
|
||||
指标: RwLock::new(指标容器::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for K线 {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
标识: self.标识.clone(),
|
||||
序号: self.序号,
|
||||
周期: self.周期,
|
||||
时间戳: self.时间戳,
|
||||
高: self.高,
|
||||
低: self.低,
|
||||
开盘价: self.开盘价,
|
||||
收盘价: self.收盘价,
|
||||
成交量: self.成交量,
|
||||
指标: RwLock::new(self.指标.read().clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,6 +135,7 @@ impl K线 {
|
||||
|
||||
/// 序列化为大端字节序 48 字节
|
||||
/// 格式: >6d (时间戳, 开盘价, 高, 低, 收盘价, 成交量)
|
||||
/// TODO: 对齐 Python round(x, 8) 再序列化
|
||||
pub fn to_bytes(&self) -> [u8; 48] {
|
||||
let mut buf = [0u8; 48];
|
||||
{
|
||||
@@ -125,7 +182,23 @@ impl K线 {
|
||||
Self::from_bytes(字节组, 周期, 标识)
|
||||
}
|
||||
|
||||
/// 解析原始数据 — 只提取时间戳+OHLCV,不构造 K线
|
||||
pub fn 解析原始数据(字节组: &[u8]) -> Option<(i64, f64, f64, f64, f64, f64)> {
|
||||
if 字节组.len() < 48 {
|
||||
return None;
|
||||
}
|
||||
let mut reader = &字节组[..48];
|
||||
let 时间戳 = reader.read_f64::<BigEndian>().ok()? as i64;
|
||||
let 开 = reader.read_f64::<BigEndian>().ok()?;
|
||||
let 高 = reader.read_f64::<BigEndian>().ok()?;
|
||||
let 低 = reader.read_f64::<BigEndian>().ok()?;
|
||||
let 收 = reader.read_f64::<BigEndian>().ok()?;
|
||||
let 量 = reader.read_f64::<BigEndian>().ok()?;
|
||||
Some((时间戳, 开, 高, 低, 收, 量))
|
||||
}
|
||||
|
||||
/// 创建普通K线
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn 创建普K(
|
||||
标识: &str,
|
||||
时间戳: i64,
|
||||
@@ -147,14 +220,13 @@ impl K线 {
|
||||
开盘价,
|
||||
收盘价,
|
||||
成交量,
|
||||
macd: None,
|
||||
rsi: None,
|
||||
kdj: None,
|
||||
指标: RwLock::new(指标容器::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 保存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())?;
|
||||
@@ -177,7 +249,7 @@ impl K线 {
|
||||
let mut 阳 = 0.0f64;
|
||||
let mut 阴 = 0.0f64;
|
||||
for k in 基序 {
|
||||
if let Some(ref macd) = k.macd {
|
||||
if let Some(macd) = k.指标.read().macd() {
|
||||
let hist = macd.MACD柱;
|
||||
if hist >= 0.0 {
|
||||
阳 += hist;
|
||||
@@ -202,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(始);
|
||||
@@ -213,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线 {
|
||||
@@ -273,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());
|
||||
}
|
||||
}
|
||||
|
||||
+234
-176
@@ -23,36 +23,49 @@
|
||||
*/
|
||||
|
||||
use crate::config::缠论配置;
|
||||
use crate::indicators::{
|
||||
平滑异同移动平均线, 相对强弱指数, 随机指标, K线取值
|
||||
};
|
||||
use crate::indicators::指标计算器;
|
||||
use crate::kline::bar::K线;
|
||||
use crate::structure::fractal_obj::分型;
|
||||
use crate::types::SyncF64;
|
||||
use crate::types::分型结构;
|
||||
use crate::types::相对方向;
|
||||
use crate::types::SyncF64;
|
||||
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线
|
||||
///
|
||||
/// 部分字段使用 Cell/RefCell 实现内部可变性,确保包含处理原地修改时
|
||||
/// Rc 指针不变,所有持有该 Rc 的引用(如分型.右)能看到最新数据。
|
||||
/// 部分字段使用 AtomicI64 / SyncF64 / RwLock 实现内部可变性,确保包含处理
|
||||
/// 原地修改时 Rc 指针不变,所有持有该 Rc 的引用(如分型.右)能看到最新数据。
|
||||
#[derive(Debug)]
|
||||
pub struct 缠论K线 {
|
||||
/// 缠K序号(在缠论K线序列中的位置)
|
||||
pub 序号: AtomicI64,
|
||||
/// Unix 时间戳(秒)
|
||||
pub 时间戳: AtomicI64,
|
||||
/// 缠K最高价(经包含处理后可能高于原始K线)
|
||||
pub 高: SyncF64,
|
||||
/// 缠K最低价(经包含处理后可能低于原始K线)
|
||||
pub 低: SyncF64,
|
||||
/// 缠K方向(向上/向下)
|
||||
pub 方向: RwLock<相对方向>,
|
||||
/// 分型结构(顶/底/上/下/散)
|
||||
pub 分型: RwLock<Option<分型结构>>,
|
||||
/// 周期(秒)
|
||||
pub 周期: i64,
|
||||
/// 品种标识
|
||||
pub 标识: String,
|
||||
/// 分型特征值(历史高低点极值,用于背驰判断)
|
||||
pub 分型特征值: SyncF64,
|
||||
/// 原始K线起始序号(包含处理前)
|
||||
pub 原始起始序号: i64,
|
||||
/// 原始K线结束序号(包含处理后更新)
|
||||
pub 原始结束序号: AtomicI64,
|
||||
/// 标的原始K线(该缠K对应的普K)
|
||||
pub 标的K线: RwLock<Arc<K线>>,
|
||||
pub 买卖点信息: RwLock<Vec<String>>,
|
||||
/// 买卖点信息集合
|
||||
pub 买卖点信息: RwLock<HashSet<String>>,
|
||||
}
|
||||
|
||||
impl Clone for 缠论K线 {
|
||||
@@ -62,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()),
|
||||
分型特征值: 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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -85,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())
|
||||
@@ -104,30 +116,32 @@ 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()),
|
||||
分型特征值: 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 {
|
||||
match *self.分型.read().unwrap() {
|
||||
let 标 = self.标的K线.read();
|
||||
let 容器 = 标.指标.read();
|
||||
match *self.分型.read() {
|
||||
Some(分型结构::底) | Some(分型结构::下) => {
|
||||
if let Some(ref macd) = self.标的K线.read().unwrap().macd {
|
||||
if let Some(macd) = 容器.macd() {
|
||||
macd.MACD柱 < 0.0
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Some(分型结构::顶) | Some(分型结构::上) => {
|
||||
if let Some(ref macd) = self.标的K线.read().unwrap().macd {
|
||||
if let Some(macd) = 容器.macd() {
|
||||
macd.MACD柱 > 0.0
|
||||
} else {
|
||||
false
|
||||
@@ -139,9 +153,11 @@ impl 缠论K线 {
|
||||
|
||||
/// 与RSI匹配 — 底分型时RSI应低于SMA, 顶分型时高于SMA
|
||||
pub fn 与RSI匹配(&self) -> bool {
|
||||
match *self.分型.read().unwrap() {
|
||||
let 标 = self.标的K线.read();
|
||||
let 容器 = 标.指标.read();
|
||||
match *self.分型.read() {
|
||||
Some(分型结构::底) | Some(分型结构::下) => {
|
||||
if let Some(ref rsi) = self.标的K线.read().unwrap().rsi {
|
||||
if let Some(rsi) = 容器.rsi() {
|
||||
match (rsi.RSI, rsi.RSI_SMA) {
|
||||
(Some(r), Some(sma)) => r < sma,
|
||||
_ => false,
|
||||
@@ -151,7 +167,7 @@ impl 缠论K线 {
|
||||
}
|
||||
}
|
||||
Some(分型结构::顶) | Some(分型结构::上) => {
|
||||
if let Some(ref rsi) = self.标的K线.read().unwrap().rsi {
|
||||
if let Some(rsi) = 容器.rsi() {
|
||||
match (rsi.RSI, rsi.RSI_SMA) {
|
||||
(Some(r), Some(sma)) => r > sma,
|
||||
_ => false,
|
||||
@@ -166,9 +182,11 @@ impl 缠论K线 {
|
||||
|
||||
/// 与KDJ匹配 — 底分型时K应低于D(死叉后), 顶分型时K应高于D(金叉后)
|
||||
pub fn 与KDJ匹配(&self) -> bool {
|
||||
match *self.分型.read().unwrap() {
|
||||
let 标 = self.标的K线.read();
|
||||
let 容器 = 标.指标.read();
|
||||
match *self.分型.read() {
|
||||
Some(分型结构::底) | Some(分型结构::下) => {
|
||||
if let Some(ref kdj) = self.标的K线.read().unwrap().kdj {
|
||||
if let Some(kdj) = 容器.kdj() {
|
||||
match (kdj.K, kdj.D) {
|
||||
(Some(k), Some(d)) => k < d,
|
||||
_ => false,
|
||||
@@ -178,7 +196,7 @@ impl 缠论K线 {
|
||||
}
|
||||
}
|
||||
Some(分型结构::顶) | Some(分型结构::上) => {
|
||||
if let Some(ref kdj) = self.标的K线.read().unwrap().kdj {
|
||||
if let Some(kdj) = 容器.kdj() {
|
||||
match (kdj.K, kdj.D) {
|
||||
(Some(k), Some(d)) => k > d,
|
||||
_ => false,
|
||||
@@ -199,19 +217,16 @@ impl 缠论K线 {
|
||||
if k线.时间戳.load(Ordering::Relaxed) <= k.时间戳.load(Ordering::Relaxed)
|
||||
&& k.时间戳.load(Ordering::Relaxed)
|
||||
<= k线.时间戳.load(Ordering::Relaxed) + k线.周期
|
||||
&& (k线.分型特征值.get() - k.分型特征值.get()).abs() < f64::EPSILON
|
||||
{
|
||||
if (k线.分型特征值.get() - k.分型特征值.get()).abs() < f64::EPSILON
|
||||
{
|
||||
return k.时间戳.load(Ordering::Relaxed);
|
||||
}
|
||||
return k.时间戳.load(Ordering::Relaxed);
|
||||
}
|
||||
} else if k.时间戳.load(Ordering::Relaxed) <= k线.时间戳.load(Ordering::Relaxed)
|
||||
&& k线.时间戳.load(Ordering::Relaxed)
|
||||
<= k.时间戳.load(Ordering::Relaxed) + k.周期
|
||||
&& (k线.分型特征值.get() - k.分型特征值.get()).abs() < f64::EPSILON
|
||||
{
|
||||
if (k线.分型特征值.get() - k.分型特征值.get()).abs() < f64::EPSILON {
|
||||
return k.时间戳.load(Ordering::Relaxed);
|
||||
}
|
||||
return k.时间戳.load(Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -219,6 +234,7 @@ impl 缠论K线 {
|
||||
}
|
||||
|
||||
/// 创建缠K
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn 创建缠K(
|
||||
时间戳: i64,
|
||||
高: f64,
|
||||
@@ -250,7 +266,7 @@ impl 缠论K线 {
|
||||
原始起始序号: 原始序号,
|
||||
原始结束序号: AtomicI64::new(原始序号),
|
||||
标的K线: RwLock::new(普k),
|
||||
买卖点信息: RwLock::new(Vec::new()),
|
||||
买卖点信息: RwLock::new(HashSet::new()),
|
||||
};
|
||||
|
||||
if let Some(之前) = 之前 {
|
||||
@@ -272,7 +288,7 @@ impl 缠论K线 {
|
||||
/// 兼并(合并)处理 — 缠论包含处理的核心算法
|
||||
///
|
||||
/// 返回 (新缠K, 模式) — 模式: "添加"/"替换"/None
|
||||
pub fn 兼并(
|
||||
pub fn _兼并(
|
||||
之前缠K: Option<&缠论K线>,
|
||||
当前缠K: &缠论K线,
|
||||
当前普K: &Arc<K线>,
|
||||
@@ -305,7 +321,7 @@ impl 缠论K线 {
|
||||
|
||||
// 重复提交检测 — 当序号相同时认为是重复提交K线
|
||||
if 当前普K.序号 == 当前缠K.原始结束序号.load(Ordering::Relaxed) {
|
||||
return (None, None);
|
||||
// no-op: 对齐 Python ... (Ellipsis)
|
||||
}
|
||||
|
||||
// 序号连续性检查
|
||||
@@ -340,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
|
||||
@@ -372,93 +388,14 @@ impl 缠论K线 {
|
||||
当前K线.标识 = 配置.标识.clone();
|
||||
|
||||
// ---- 阶段1: 普K序列管理 + 指标增量计算 ----
|
||||
// 对齐 Python: 先推入序列,再计算指标
|
||||
if 普K序列.is_empty() {
|
||||
if 配置.计算指标 {
|
||||
当前K线.macd = Some(平滑异同移动平均线::首次计算(
|
||||
K线取值(
|
||||
当前K线.开盘价,
|
||||
当前K线.高,
|
||||
当前K线.低,
|
||||
当前K线.收盘价,
|
||||
&配置.指标计算方式,
|
||||
),
|
||||
当前K线.时间戳,
|
||||
配置.平滑异同移动平均线_快线周期,
|
||||
配置.平滑异同移动平均线_慢线周期,
|
||||
配置.平滑异同移动平均线_信号周期,
|
||||
));
|
||||
当前K线.rsi = Some(相对强弱指数::首次计算(
|
||||
K线取值(
|
||||
当前K线.开盘价,
|
||||
当前K线.高,
|
||||
当前K线.低,
|
||||
当前K线.收盘价,
|
||||
&配置.指标计算方式,
|
||||
),
|
||||
当前K线.时间戳,
|
||||
配置.相对强弱指数_周期,
|
||||
配置.相对强弱指数_超买阈值,
|
||||
配置.相对强弱指数_超卖阈值,
|
||||
Some(配置.相对强弱指数_移动平均线周期),
|
||||
));
|
||||
当前K线.kdj = Some(随机指标::首次计算(
|
||||
当前K线.高,
|
||||
当前K线.低,
|
||||
当前K线.收盘价,
|
||||
当前K线.时间戳,
|
||||
配置.随机指标_RSV周期,
|
||||
配置.随机指标_K值平滑周期,
|
||||
配置.随机指标_D值平滑周期,
|
||||
配置.随机指标_超买阈值,
|
||||
配置.随机指标_超卖阈值,
|
||||
));
|
||||
}
|
||||
let 当前K线_rc = Arc::new(当前K线);
|
||||
普K序列.push(当前K线_rc);
|
||||
普K序列.push(Arc::new(当前K线));
|
||||
} else {
|
||||
let 之前普K = 普K序列.last().unwrap();
|
||||
if 之前普K.时间戳 == 当前K线.时间戳 {
|
||||
// 同时间戳更新
|
||||
// 同时间戳更新 — 替换 [-1]
|
||||
当前K线.序号 = 之前普K.序号;
|
||||
if 配置.计算指标 {
|
||||
if 普K序列.len() >= 2 {
|
||||
if let Some(ref prev_macd) = 普K序列[普K序列.len() - 2].macd {
|
||||
当前K线.macd = Some(平滑异同移动平均线::增量计算(
|
||||
prev_macd,
|
||||
K线取值(
|
||||
当前K线.开盘价,
|
||||
当前K线.高,
|
||||
当前K线.低,
|
||||
当前K线.收盘价,
|
||||
&配置.指标计算方式,
|
||||
),
|
||||
当前K线.时间戳,
|
||||
));
|
||||
}
|
||||
if let Some(ref prev_rsi) = 普K序列[普K序列.len() - 2].rsi {
|
||||
当前K线.rsi = Some(相对强弱指数::增量计算(
|
||||
prev_rsi,
|
||||
K线取值(
|
||||
当前K线.开盘价,
|
||||
当前K线.高,
|
||||
当前K线.低,
|
||||
当前K线.收盘价,
|
||||
&配置.指标计算方式,
|
||||
),
|
||||
当前K线.时间戳,
|
||||
));
|
||||
}
|
||||
if let Some(ref prev_kdj) = 普K序列[普K序列.len() - 2].kdj {
|
||||
当前K线.kdj = Some(随机指标::增量计算(
|
||||
prev_kdj,
|
||||
当前K线.高,
|
||||
当前K线.低,
|
||||
当前K线.收盘价,
|
||||
当前K线.时间戳,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
普K序列.pop();
|
||||
普K序列.push(Arc::new(当前K线));
|
||||
} else {
|
||||
@@ -466,46 +403,13 @@ impl 缠论K线 {
|
||||
panic!("时序错误: 之前={}, 当前={}", 之前普K.时间戳, 当前K线.时间戳);
|
||||
}
|
||||
当前K线.序号 = 之前普K.序号 + 1;
|
||||
if 配置.计算指标 {
|
||||
if let Some(ref prev_macd) = 之前普K.macd {
|
||||
当前K线.macd = Some(平滑异同移动平均线::增量计算(
|
||||
prev_macd,
|
||||
K线取值(
|
||||
当前K线.开盘价,
|
||||
当前K线.高,
|
||||
当前K线.低,
|
||||
当前K线.收盘价,
|
||||
&配置.指标计算方式,
|
||||
),
|
||||
当前K线.时间戳,
|
||||
));
|
||||
}
|
||||
if let Some(ref prev_rsi) = 之前普K.rsi {
|
||||
当前K线.rsi = Some(相对强弱指数::增量计算(
|
||||
prev_rsi,
|
||||
K线取值(
|
||||
当前K线.开盘价,
|
||||
当前K线.高,
|
||||
当前K线.低,
|
||||
当前K线.收盘价,
|
||||
&配置.指标计算方式,
|
||||
),
|
||||
当前K线.时间戳,
|
||||
));
|
||||
}
|
||||
if let Some(ref prev_kdj) = 之前普K.kdj {
|
||||
当前K线.kdj = Some(随机指标::增量计算(
|
||||
prev_kdj,
|
||||
当前K线.高,
|
||||
当前K线.低,
|
||||
当前K线.收盘价,
|
||||
当前K线.时间戳,
|
||||
));
|
||||
}
|
||||
}
|
||||
普K序列.push(Arc::new(当前K线));
|
||||
}
|
||||
}
|
||||
// 计算指标: 对齐 Python,仅当 计算指标 开启时执行
|
||||
if 配置.计算指标 {
|
||||
指标计算器::计算并挂载(普K序列, 配置);
|
||||
}
|
||||
|
||||
// ---- 阶段2: 缠K合并 ----
|
||||
let 状态: String;
|
||||
@@ -514,9 +418,9 @@ impl 缠论K线 {
|
||||
if !缠K序列.is_empty() {
|
||||
let len = 缠K序列.len();
|
||||
let (左边, 右边) = 缠K序列.split_at_mut(len - 1);
|
||||
let 之前缠K: Option<&缠论K线> = 左边.last().map(|rc| Arc::as_ref(rc));
|
||||
let 之前缠K: Option<&缠论K线> = 左边.last().map(Arc::as_ref);
|
||||
let 最后一个缠K = &*右边[0];
|
||||
let (新缠K, 模式) = Self::兼并(之前缠K, 最后一个缠K, 当前K线_ref, 配置);
|
||||
let (新缠K, 模式) = Self::_兼并(之前缠K, 最后一个缠K, 当前K线_ref, 配置);
|
||||
|
||||
if let Some(k) = 新缠K {
|
||||
match 模式.as_deref() {
|
||||
@@ -525,8 +429,7 @@ impl 缠论K线 {
|
||||
状态 = "创建".into();
|
||||
}
|
||||
Some("替换") => {
|
||||
// Cell::set 已原地更新数据,无需 pop+push 打破 Rc 身份
|
||||
状态 = "兼并".into();
|
||||
状态 = "替换".into();
|
||||
}
|
||||
_ => {
|
||||
状态 = "兼并".into();
|
||||
@@ -563,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(分型结构::底);
|
||||
}
|
||||
分型结构::散 => {}
|
||||
}
|
||||
@@ -614,7 +517,7 @@ impl 缠论K线 {
|
||||
Arc::clone(&缠K序列[idx - 2]),
|
||||
Some(Arc::clone(&缠K序列[idx - 1])),
|
||||
));
|
||||
return (状态, Some(形态));
|
||||
(状态, Some(形态))
|
||||
}
|
||||
|
||||
/// 截取缠K序列从始到终
|
||||
@@ -623,14 +526,169 @@ impl 缠论K线 {
|
||||
始: &缠论K线,
|
||||
终: &缠论K线,
|
||||
) -> Option<Vec<Arc<缠论K线>>> {
|
||||
let 始_idx = 序列
|
||||
.iter()
|
||||
.position(|k| Arc::as_ptr(k) == (始 as *const _))?;
|
||||
let 终_idx = 序列
|
||||
.iter()
|
||||
.position(|k| Arc::as_ptr(k) == (终 as *const _))?;
|
||||
let 始_idx = 序列.iter().position(|k| std::ptr::eq(Arc::as_ptr(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)*),
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
+32
-35
@@ -77,38 +77,35 @@ fn 测试_读取数据(文件路径: &str) {
|
||||
let 启动时间 = Instant::now();
|
||||
|
||||
let 配置 = 缠论配置::default().不推送();
|
||||
match 观察者::读取数据文件(文件路径, Some(配置)) {
|
||||
Ok(观察员) => {
|
||||
let 观察员 = 观察员.read().unwrap();
|
||||
let 消耗用时 = 启动时间.elapsed();
|
||||
println!(
|
||||
"测试_读取数据 耗时 {:.2?} 普K数量 {}",
|
||||
消耗用时,
|
||||
观察员.普通K线序列.len()
|
||||
);
|
||||
println!("符号: {}", 观察员.符号);
|
||||
println!("周期: {}", 观察员.周期);
|
||||
println!("缠K数量: {}", 观察员.缠论K线序列.len());
|
||||
println!("分型数量: {}", 观察员.分型序列.len());
|
||||
println!("笔数量: {}", 观察员.笔序列.len());
|
||||
println!("笔中枢数量: {}", 观察员.笔_中枢序列.len());
|
||||
println!("线段数量: {}", 观察员.线段序列.len());
|
||||
println!("中枢数量: {}", 观察员.中枢序列.len());
|
||||
println!("扩展线段数量: {}", 观察员.扩展线段序列.len());
|
||||
println!("线段_线段序列数量: {}", 观察员.线段_线段序列.len());
|
||||
println!(
|
||||
"扩展线段_扩展线段数量: {}",
|
||||
观察员.扩展线段序列_扩展线段.len()
|
||||
);
|
||||
let 观察员 = 观察者::new("".into(), 0, 缠论配置::default());
|
||||
观察员
|
||||
.write()
|
||||
.读取数据文件(文件路径, 配置)
|
||||
.expect("读取数据文件失败");
|
||||
let 观察员 = 观察员.read();
|
||||
let 消耗用时 = 启动时间.elapsed();
|
||||
println!(
|
||||
"测试_读取数据 耗时 {:.2?} 普K数量 {}",
|
||||
消耗用时,
|
||||
观察员.普通K线序列.len()
|
||||
);
|
||||
println!("符号: {}", 观察员.符号);
|
||||
println!("周期: {}", 观察员.周期);
|
||||
println!("缠K数量: {}", 观察员.缠论K线序列.len());
|
||||
println!("分型数量: {}", 观察员.分型序列.len());
|
||||
println!("笔数量: {}", 观察员.笔序列.len());
|
||||
println!("笔中枢数量: {}", 观察员.笔_中枢序列.len());
|
||||
println!("线段数量: {}", 观察员.线段序列().len());
|
||||
println!("中枢数量: {}", 观察员.中枢序列().len());
|
||||
println!("扩展线段数量: {}", 观察员.扩展线段序列().len());
|
||||
println!("线段_线段序列数量: {}", 观察员.线段_线段序列().len());
|
||||
println!(
|
||||
"扩展线段_扩展线段数量: {}",
|
||||
观察员.扩展线段序列_扩展线段().len()
|
||||
);
|
||||
|
||||
println!("\n===== 保存分析数据 =====\n");
|
||||
观察员.测试_保存数据(None);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("读取失败: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
println!("\n===== 保存分析数据 =====\n");
|
||||
观察员.测试_保存数据(None);
|
||||
}
|
||||
|
||||
/// 测试_周期合成 — 多周期合成分析
|
||||
@@ -162,19 +159,19 @@ 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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n===== 保存分析数据 =====\n");
|
||||
多级别分析.测试_保存数据();
|
||||
多级别分析.测试_保存数据(None);
|
||||
}
|
||||
|
||||
@@ -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
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user