Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c972c2673 | |||
| 0f69cfda79 | |||
| e44f9dd837 | |||
| 1477cde86b | |||
| da5222d960 | |||
| 15a1d43b1d | |||
| a8df8cd187 | |||
| 9c7686d104 | |||
| 34c42ecd68 | |||
| 8a2afb9ed0 | |||
| 11d897ebaa | |||
| 4bf9461009 | |||
| c83cddbcc4 | |||
| 0a7e9dc896 | |||
| dd40f95742 | |||
| e33b1f0744 | |||
| 823a24d364 | |||
| 58c85dc7cd | |||
| c3e9ae8d77 | |||
| b7c4e60420 | |||
| 15dc44e8e0 | |||
| bd4fceab02 | |||
| af3ebf13c8 | |||
| 22109d8b30 | |||
| c2c09fc8ba | |||
| 0eb52cc06a | |||
| 1e6025a968 |
@@ -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 代码对齐
|
||||
@@ -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` 格式,如 `btcusd-300-1631772074-1632222374.nb`)
|
||||
obs = chanlun.观察者.读取数据文件("path/to/btcusd-300-1631772074-1632222374.nb", config)
|
||||
|
||||
# 查看各层级序列
|
||||
print(f"K线数量: {len(obs.普通K线序列)}")
|
||||
@@ -29,7 +29,7 @@ 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 线...
|
||||
```
|
||||
|
||||
@@ -40,6 +40,10 @@ analyzer = chanlun.立体分析器("BTCUSD", ["1min", "5min", "30min"], config)
|
||||
```bash
|
||||
pip install maturin
|
||||
|
||||
# 推荐:一键清理缓存 + 构建 + 安装
|
||||
./clean_install.sh
|
||||
|
||||
# 或手动:
|
||||
# 开发模式(直接安装到当前 venv)
|
||||
maturin develop
|
||||
|
||||
@@ -48,12 +52,17 @@ 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
|
||||
./build.sh test # 运行集成测试
|
||||
```
|
||||
|
||||
## 导出类
|
||||
@@ -70,8 +79,8 @@ pip install target/wheels/chanlun-*.whl
|
||||
## 兼容性
|
||||
|
||||
- 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` 使用是否合理
|
||||
|
||||
## 偏好语言
|
||||
|
||||
请使用简体中文或英文撰写报告。
|
||||
@@ -3,3 +3,5 @@ __pycache__/
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
Cargo.lock
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "chanlun-py"
|
||||
version = "26.5.94"
|
||||
edition = "2021"
|
||||
version = "26.6.87"
|
||||
edition = "2024"
|
||||
description = "缠论技术分析库 — Rust 高性能 Python 绑定"
|
||||
authors = ["YuYuKunKun"]
|
||||
license = "MIT"
|
||||
@@ -12,7 +12,12 @@ crate-type = ["cdylib"]
|
||||
name = "chanlun"
|
||||
|
||||
[dependencies]
|
||||
chanlun = "26.5.4" # { 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,150 @@
|
||||
# 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
|
||||
|
||||
# 开发模式(直接安装到当前 venv)
|
||||
maturin develop
|
||||
|
||||
# 或构建 wheel
|
||||
maturin build --release
|
||||
pip install target/wheels/chanlun-*.whl
|
||||
```
|
||||
|
||||
也可使用项目内的 `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,39 @@ __all__ = [
|
||||
"转化为时间戳",
|
||||
"转化为时间戳_数字",
|
||||
"随机指标",
|
||||
"布林带",
|
||||
"get_分型模式",
|
||||
"set_分型模式",
|
||||
"get_扩展线段模式",
|
||||
"set_扩展线段模式",
|
||||
"get_log_level",
|
||||
"set_log_level",
|
||||
"get_rs_log_level",
|
||||
"set_rs_log_level",
|
||||
"K线相等",
|
||||
"缠论K线相等",
|
||||
"分型相等",
|
||||
"缺口相等",
|
||||
"线段特征相等",
|
||||
"中枢相等",
|
||||
"虚线相等",
|
||||
"chan",
|
||||
]
|
||||
|
||||
from ._chanlun import *
|
||||
from ._chanlun import set_log_level as _rs_set_log_level, get_log_level as _rs_get_log_level
|
||||
from . import chan
|
||||
from .chan import set_log_level, get_log_level
|
||||
|
||||
|
||||
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()
|
||||
|
||||
+811
-2234
File diff suppressed because it is too large
Load Diff
+2468
-941
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "chanlun"
|
||||
version = "2605.94"
|
||||
version = "2606.87"
|
||||
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"
|
||||
@@ -41,3 +47,11 @@ 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
|
||||
+399
-703
File diff suppressed because it is too large
Load Diff
+636
-152
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,199 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+101
-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,36 @@ 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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,15 +325,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 +394,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 +442,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 +463,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 +481,9 @@ fn validate_field(
|
||||
Value::Bool(_) => "布尔",
|
||||
Value::Number(_) => "数值",
|
||||
Value::String(_) => "字符串",
|
||||
_ => "其他",
|
||||
Value::Array(_) => "数组",
|
||||
Value::Object(_) => "字典",
|
||||
Value::Null => "null",
|
||||
};
|
||||
Err(format!("类型不匹配(需要 {expected},收到 {type_name})"))
|
||||
}
|
||||
@@ -449,10 +507,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 +549,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(())
|
||||
}
|
||||
|
||||
+127
-114
@@ -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
|
||||
};
|
||||
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,43 @@ 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 })
|
||||
}
|
||||
|
||||
/// 指标容器 — 包含所有已注册指标(MACD/RSI/KDJ/BOLL/均线/单值)
|
||||
#[getter]
|
||||
fn 指标(&self) -> 指标容器Py {
|
||||
指标容器Py {
|
||||
inner: self.inner.指标.read().clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// pandas 兼容 — 返回所有字段构成的字典
|
||||
@@ -182,7 +192,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 +202,7 @@ impl K线Py {
|
||||
if let Some(v) = self.kdj() {
|
||||
dict.set_item("kdj", v)?;
|
||||
}
|
||||
dict.set_item("指标", self.指标())?;
|
||||
Ok(dict.into())
|
||||
}
|
||||
|
||||
@@ -325,6 +336,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 +393,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 +431,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,10 +463,8 @@ 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]
|
||||
@@ -454,7 +472,6 @@ impl 缠论K线Py {
|
||||
self.inner
|
||||
.分型
|
||||
.read()
|
||||
.unwrap()
|
||||
.map(|f| crate::types_py::获取分型结构单例(py, f))
|
||||
}
|
||||
|
||||
@@ -485,7 +502,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 +513,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.分型特征值())?;
|
||||
@@ -534,17 +551,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 +590,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 +652,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]
|
||||
|
||||
+216
-30
@@ -22,12 +22,89 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
#![allow(non_snake_case, clippy::too_many_arguments)]
|
||||
|
||||
use pyo3::prelude::*;
|
||||
use std::sync::atomic::Ordering;
|
||||
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 structure_py;
|
||||
@@ -45,12 +122,118 @@ 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)?;
|
||||
// 阶段 2: 配置
|
||||
@@ -65,45 +248,48 @@ fn _chanlun(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
algorithm_py::register(m)?;
|
||||
// 阶段 7: 业务
|
||||
business_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 仍失败");
|
||||
}
|
||||
}
|
||||
|
||||
+92
-359
@@ -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};
|
||||
|
||||
// ========== 分型 ==========
|
||||
@@ -185,17 +131,7 @@ impl 分型Py {
|
||||
|
||||
#[getter]
|
||||
fn 结构(&self, py: Python<'_>) -> Py<分型结构Py> {
|
||||
let inner = if chanlun::structure::fractal_obj::分型模式.load(Ordering::Relaxed) {
|
||||
self.inner.结构
|
||||
} else {
|
||||
self.inner
|
||||
.中
|
||||
.分型
|
||||
.read()
|
||||
.unwrap()
|
||||
.unwrap_or(chanlun::types::分型结构::散)
|
||||
};
|
||||
crate::types_py::获取分型结构单例(py, inner)
|
||||
crate::types_py::获取分型结构单例(py, self.inner.结构())
|
||||
}
|
||||
|
||||
#[getter]
|
||||
@@ -205,11 +141,25 @@ impl 分型Py {
|
||||
|
||||
#[getter]
|
||||
fn 分型特征值(&self) -> f64 {
|
||||
if chanlun::structure::fractal_obj::分型模式.load(Ordering::Relaxed) {
|
||||
self.inner.分型特征值
|
||||
} else {
|
||||
self.inner.中.分型特征值.get()
|
||||
}
|
||||
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.分型特征值
|
||||
}
|
||||
|
||||
fn __str__(&self) -> String {
|
||||
@@ -233,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),
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -271,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())
|
||||
@@ -389,7 +342,7 @@ impl 虚线Py {
|
||||
|
||||
#[getter]
|
||||
fn 标识(&self) -> String {
|
||||
self.inner.标识.read().unwrap().clone()
|
||||
self.inner.标识.read().clone()
|
||||
}
|
||||
|
||||
#[getter]
|
||||
@@ -409,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]
|
||||
@@ -419,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 => {
|
||||
@@ -451,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 ----
|
||||
@@ -482,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())
|
||||
}
|
||||
@@ -496,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())
|
||||
@@ -505,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())
|
||||
@@ -514,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())
|
||||
@@ -526,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())
|
||||
}
|
||||
@@ -545,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]
|
||||
@@ -581,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())
|
||||
}
|
||||
@@ -972,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -986,15 +923,10 @@ impl 虚线Py {
|
||||
/// 文: 分型 — 特征序列的起点分型
|
||||
/// 武: 分型 — 特征序列的终点分型
|
||||
/// 方向: 相对方向 / 高: float / 低: float
|
||||
///
|
||||
/// 方法:
|
||||
/// 添加(虚线) — 向特征序列追加虚线元素
|
||||
/// 删除(虚线) — 从特征序列移除虚线元素
|
||||
/// 基本序列: list[虚线] (基础序列)
|
||||
///
|
||||
/// 类方法:
|
||||
/// 新建(序号, 文, 武, 基础序列?) -> 线段特征
|
||||
/// 静态分析(虚线序列, 配置) -> 线段特征|None
|
||||
/// 获取分型序列(虚线序列, 配置) -> list[线段特征]
|
||||
/// :meth:`静态分析` — 对虚线序列进行静态特征分析,返回 线段特征 列表
|
||||
#[pyclass(name = "线段特征", module = "chanlun._chanlun", from_py_object)]
|
||||
#[derive(Clone)]
|
||||
pub struct 线段特征Py {
|
||||
@@ -1003,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())
|
||||
@@ -1059,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 {
|
||||
@@ -1128,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]
|
||||
@@ -1146,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>>,
|
||||
@@ -1194,117 +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: Python<'_>) -> Py<分型结构Py> {
|
||||
crate::types_py::获取分型结构单例(py, self.inner.结构)
|
||||
}
|
||||
|
||||
/// pandas 兼容 — 返回关键标量字段构成的字典
|
||||
#[getter]
|
||||
fn __dict__(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
|
||||
let dict = PyDict::new(py);
|
||||
dict.set_item("结构", self.结构(py))?;
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1312,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(())
|
||||
}
|
||||
|
||||
+185
-95
@@ -22,12 +22,12 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use pyo3::basic::CompareOp;
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::types::{PyDict, PyType};
|
||||
use pyo3::types::{PyBool, PyDict, PyType};
|
||||
|
||||
// ========== 单例缓存 ==========
|
||||
|
||||
@@ -37,7 +37,7 @@ pub fn 获取分型结构单例(
|
||||
py: Python<'_>,
|
||||
inner: chanlun::types::分型结构,
|
||||
) -> Py<分型结构Py> {
|
||||
let mut guard = 分型结构_单例缓存.lock().unwrap();
|
||||
let mut guard = 分型结构_单例缓存.lock();
|
||||
if let Some(ref map) = *guard {
|
||||
return map[&(inner as u8)].clone_ref(py);
|
||||
}
|
||||
@@ -61,6 +61,40 @@ pub fn 获取分型结构单例(
|
||||
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
|
||||
}
|
||||
|
||||
// ========== 买卖点类型 ==========
|
||||
|
||||
/// 买卖点类型 — 缠论的三类买卖点及扩展类型。
|
||||
@@ -89,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()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,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 {
|
||||
@@ -189,10 +252,8 @@ impl 相对方向Py {
|
||||
}
|
||||
|
||||
/// 返回方向的对立面(向上↔向下, 缺口↔反向缺口, 衔接↔反向衔接)。
|
||||
fn 翻转(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner.翻转(),
|
||||
}
|
||||
fn 翻转(&self, py: Python<'_>) -> Py<Self> {
|
||||
获取相对方向单例(py, self.inner.翻转())
|
||||
}
|
||||
|
||||
/// 判断是否为向上方向(向上/向上缺口/衔接向上)
|
||||
@@ -245,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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,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 {
|
||||
@@ -341,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 }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,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.低),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,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::买卖点类型::一买),
|
||||
@@ -535,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))?;
|
||||
@@ -544,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::相对方向::向下),
|
||||
@@ -557,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))?;
|
||||
@@ -566,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::分型结构::下),
|
||||
@@ -575,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))?;
|
||||
|
||||
Binary file not shown.
+1631
-15
File diff suppressed because it is too large
Load Diff
+5
-3
@@ -1,8 +1,7 @@
|
||||
[package]
|
||||
name = "chanlun"
|
||||
version = "26.5.4"
|
||||
edition = "2021"
|
||||
rust-version = "1.70"
|
||||
version = "26.6.4"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
description = "基于缠论(缠中说禅)理论的量化技术分析核心库,支持流式数据处理和多周期联立分析。"
|
||||
readme = "README.md"
|
||||
@@ -18,3 +17,6 @@ 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"
|
||||
|
||||
+816
-284
File diff suppressed because it is too large
Load Diff
+502
-511
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()
|
||||
}
|
||||
}
|
||||
|
||||
+343
-214
@@ -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,104 +82,116 @@ 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.文().时间戳(),
|
||||
@@ -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 向中枢序列尾部添加(
|
||||
/// _向中枢序列尾部添加
|
||||
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.武(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+939
-924
File diff suppressed because it is too large
Load Diff
+402
-67
@@ -27,33 +27,45 @@ 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的序号 — 与 Python 一致,偏移计算使用缠论K线序号而非标的K线序号
|
||||
pub 当前序号: i64,
|
||||
/// 创建时的缠K序号,用于偏移计算(与买卖点K线.序号同尺度)
|
||||
/// None 时退化为使用当前K线.序号(bar序号,旧行为)
|
||||
pub 当前缠K序号: Option<i64>,
|
||||
}
|
||||
|
||||
impl 基础买卖点 {
|
||||
/// 创建基础买卖点,买卖点K线自动取自买卖点分型的中缠K
|
||||
pub fn new(
|
||||
类型: 买卖点类型,
|
||||
当前K线: Arc<K线>,
|
||||
买卖点分型: Arc<分型>,
|
||||
备注: String,
|
||||
中枢破位值: f64,
|
||||
当前序号: i64,
|
||||
) -> Self {
|
||||
let 买卖点K线 = Arc::clone(&买卖点分型.中);
|
||||
Self {
|
||||
@@ -66,13 +78,17 @@ impl 基础买卖点 {
|
||||
终结K线: None,
|
||||
破位值: 中枢破位值,
|
||||
结构: None,
|
||||
当前序号,
|
||||
当前缠K序号: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 偏移 — 当前缠K序号与买卖点K线序号的差(对齐 Python,使用缠论K线序号)
|
||||
/// 偏移 — 当前缠K序号与买卖点K线序号的差
|
||||
/// 如果设置了当前缠K序号(来自生成买卖点),使用缠K序号;否则退化为使用 bar序号
|
||||
pub fn 偏移(&self) -> i64 {
|
||||
self.当前序号 - 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),
|
||||
}
|
||||
}
|
||||
|
||||
/// 失效偏移
|
||||
@@ -112,116 +128,206 @@ impl std::fmt::Display for 基础买卖点 {
|
||||
}
|
||||
}
|
||||
|
||||
/// 买卖点 — 包含一二三类买卖点的工厂方法
|
||||
/// 买卖点 — 包含全部 18 种买卖点类型的工厂方法
|
||||
pub struct 买卖点;
|
||||
|
||||
impl 买卖点 {
|
||||
/// 创建 一卖 类型的基础买卖点
|
||||
pub fn 一卖点(
|
||||
买卖点分型: Arc<分型>,
|
||||
当前K线: Arc<K线>,
|
||||
_标识: &str,
|
||||
备注: String,
|
||||
中枢破位值: f64,
|
||||
当前序号: i64,
|
||||
) -> 基础买卖点 {
|
||||
基础买卖点::new(
|
||||
买卖点类型::一卖,
|
||||
当前K线,
|
||||
买卖点分型,
|
||||
备注,
|
||||
中枢破位值,
|
||||
当前序号,
|
||||
)
|
||||
基础买卖点::new(买卖点类型::一卖, 当前K线, 买卖点分型, 备注, 中枢破位值)
|
||||
}
|
||||
|
||||
/// 创建 一买 类型的基础买卖点
|
||||
pub fn 一买点(
|
||||
买卖点分型: Arc<分型>,
|
||||
当前K线: Arc<K线>,
|
||||
_标识: &str,
|
||||
备注: String,
|
||||
中枢破位值: f64,
|
||||
当前序号: i64,
|
||||
) -> 基础买卖点 {
|
||||
基础买卖点::new(
|
||||
买卖点类型::一买,
|
||||
当前K线,
|
||||
买卖点分型,
|
||||
备注,
|
||||
中枢破位值,
|
||||
当前序号,
|
||||
)
|
||||
基础买卖点::new(买卖点类型::一买, 当前K线, 买卖点分型, 备注, 中枢破位值)
|
||||
}
|
||||
|
||||
/// 创建 二卖 类型的基础买卖点
|
||||
pub fn 二卖点(
|
||||
买卖点分型: Arc<分型>,
|
||||
当前K线: Arc<K线>,
|
||||
_标识: &str,
|
||||
备注: String,
|
||||
中枢破位值: f64,
|
||||
当前序号: i64,
|
||||
) -> 基础买卖点 {
|
||||
基础买卖点::new(
|
||||
买卖点类型::二卖,
|
||||
当前K线,
|
||||
买卖点分型,
|
||||
备注,
|
||||
中枢破位值,
|
||||
当前序号,
|
||||
)
|
||||
基础买卖点::new(买卖点类型::二卖, 当前K线, 买卖点分型, 备注, 中枢破位值)
|
||||
}
|
||||
|
||||
/// 创建 二买 类型的基础买卖点
|
||||
pub fn 二买点(
|
||||
买卖点分型: Arc<分型>,
|
||||
当前K线: Arc<K线>,
|
||||
_标识: &str,
|
||||
备注: String,
|
||||
中枢破位值: f64,
|
||||
当前序号: i64,
|
||||
) -> 基础买卖点 {
|
||||
基础买卖点::new(
|
||||
买卖点类型::二买,
|
||||
当前K线,
|
||||
买卖点分型,
|
||||
备注,
|
||||
中枢破位值,
|
||||
当前序号,
|
||||
)
|
||||
基础买卖点::new(买卖点类型::二买, 当前K线, 买卖点分型, 备注, 中枢破位值)
|
||||
}
|
||||
|
||||
/// 创建 三卖 类型的基础买卖点
|
||||
pub fn 三卖点(
|
||||
买卖点分型: Arc<分型>,
|
||||
当前K线: Arc<K线>,
|
||||
_标识: &str,
|
||||
备注: String,
|
||||
中枢破位值: f64,
|
||||
当前序号: i64,
|
||||
) -> 基础买卖点 {
|
||||
基础买卖点::new(
|
||||
买卖点类型::三卖,
|
||||
当前K线,
|
||||
买卖点分型,
|
||||
备注,
|
||||
中枢破位值,
|
||||
当前序号,
|
||||
)
|
||||
基础买卖点::new(买卖点类型::三卖, 当前K线, 买卖点分型, 备注, 中枢破位值)
|
||||
}
|
||||
|
||||
/// 创建 三买 类型的基础买卖点
|
||||
pub fn 三买点(
|
||||
买卖点分型: Arc<分型>,
|
||||
当前K线: Arc<K线>,
|
||||
_标识: &str,
|
||||
备注: String,
|
||||
中枢破位值: f64,
|
||||
当前序号: i64,
|
||||
) -> 基础买卖点 {
|
||||
基础买卖点::new(
|
||||
买卖点类型::三买,
|
||||
当前K线,
|
||||
买卖点分型,
|
||||
备注,
|
||||
中枢破位值,
|
||||
当前序号,
|
||||
)
|
||||
基础买卖点::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线, 买卖点分型, 备注, 中枢破位值)
|
||||
}
|
||||
|
||||
/// 生成买卖点 — 根据参数自动选择类型
|
||||
@@ -238,12 +344,12 @@ impl 买卖点 {
|
||||
"卖"
|
||||
};
|
||||
let 备注 = format!("{}_{}{}{}", 特征, 级别, 序号, 买卖);
|
||||
let 破位值 = 买卖点分型.分型特征值;
|
||||
let 破位值 = 买卖点分型.分型特征值();
|
||||
|
||||
// 当前K线 — 从缠K获取其标的K线
|
||||
let 当前K线 = Arc::clone(&*当前缠K.标的K线.read().unwrap());
|
||||
// 当前序号 — 使用缠论K线序号(对齐 Python 偏移计算)
|
||||
let 当前序号 = 当前缠K.序号.load(Ordering::Relaxed);
|
||||
let 当前K线 = Arc::clone(&*当前缠K.标的K线.read());
|
||||
// 当前缠K序号 — 与买卖点K线(分型.中.序号)同尺度,用于偏移计算
|
||||
let 当前缠K序号 = 当前缠K.序号.load(Ordering::Relaxed);
|
||||
|
||||
let 类型 = match (序号, 买卖) {
|
||||
("一", "买") => 买卖点类型::一买,
|
||||
@@ -252,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!("{标签}:所有周期观察者全量校验全部一致"))
|
||||
}
|
||||
}
|
||||
|
||||
+764
-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());
|
||||
}
|
||||
}
|
||||
|
||||
+401
-124
@@ -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,183 @@ impl Default for 缠论配置 {
|
||||
}
|
||||
|
||||
impl 缠论配置 {
|
||||
/// 展示标签判定 — None=全部, [] = 全关
|
||||
pub fn 展示标签(&self, 标签: &str) -> bool {
|
||||
match &self.图表展示标签 {
|
||||
None => true,
|
||||
Some(tags) => tags.iter().any(|t| t == 标签),
|
||||
}
|
||||
}
|
||||
|
||||
/// 序列化为 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线包含整笔",
|
||||
"笔次级成笔",
|
||||
"笔弱化",
|
||||
"笔弱化_原始数量",
|
||||
"线段_非缺口下穿刺",
|
||||
"线段_特征序列忽视老阴老阳",
|
||||
"线段_缺口后紧急修正",
|
||||
"线段_修正",
|
||||
"线段内部中枢图显",
|
||||
"扩展线段_当下分析",
|
||||
"分析笔",
|
||||
"分析线段",
|
||||
"分析扩展线段",
|
||||
"分析笔中枢",
|
||||
"分析线段中枢",
|
||||
"手动终止",
|
||||
"计算指标",
|
||||
"计算BOLL",
|
||||
"指标计算方式",
|
||||
"平滑异同移动平均线_快线周期",
|
||||
"平滑异同移动平均线_慢线周期",
|
||||
"平滑异同移动平均线_信号周期",
|
||||
"相对强弱指数_周期",
|
||||
"相对强弱指数_移动平均线周期",
|
||||
"相对强弱指数_超买阈值",
|
||||
"相对强弱指数_超卖阈值",
|
||||
"随机指标_RSV周期",
|
||||
"随机指标_K值平滑周期",
|
||||
"随机指标_D值平滑周期",
|
||||
"随机指标_超买阈值",
|
||||
"随机指标_超卖阈值",
|
||||
"布林带_周期",
|
||||
"布林带_标准差倍数",
|
||||
"MACD_参数列表",
|
||||
"RSI_周期列表",
|
||||
"KDJ_参数列表",
|
||||
"BOLL_参数列表",
|
||||
"均线_类型列表",
|
||||
"均线_周期列表",
|
||||
"图表展示",
|
||||
"推送K线",
|
||||
"推送笔",
|
||||
"推送线段",
|
||||
"推送中枢",
|
||||
"图表展示_笔",
|
||||
"图表展示_线段",
|
||||
"图表展示_扩展线段",
|
||||
"图表展示_扩展线段_线段",
|
||||
"图表展示_线段_线段",
|
||||
"图表展示_中枢_笔",
|
||||
"图表展示_中枢_线段",
|
||||
"图表展示_中枢_扩展线段",
|
||||
"图表展示_中枢_扩展线段_线段",
|
||||
"图表展示_中枢_线段_线段",
|
||||
"图表展示_中枢_线段内部",
|
||||
"买卖点偏移",
|
||||
"买卖点激进识别",
|
||||
"买卖点与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 +461,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 +482,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 +522,6 @@ mod tests {
|
||||
let config = 缠论配置::default();
|
||||
assert_eq!(config.标识, "bar");
|
||||
assert_eq!(config.笔内元素数量, 5);
|
||||
assert!(config.买卖点_背离率.is_infinite());
|
||||
assert_eq!(config.指标计算方式, "收");
|
||||
}
|
||||
|
||||
@@ -442,6 +568,66 @@ 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("推送K线".into(), serde_json::json!(false));
|
||||
update.insert("笔内元素数量".into(), serde_json::json!(10));
|
||||
|
||||
let config = 缠论配置::default();
|
||||
let copied = config.model_copy(&update);
|
||||
|
||||
assert_eq!(copied.标识, "custom");
|
||||
assert!(!copied.推送K线);
|
||||
assert_eq!(copied.笔内元素数量, 10);
|
||||
// 未指定字段保持不变
|
||||
assert_eq!(copied.买卖点偏移, 1);
|
||||
assert!(copied.推送笔);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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();
|
||||
@@ -449,7 +635,98 @@ mod tests {
|
||||
assert!(!muted.推送K线);
|
||||
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.推送K线 = false;
|
||||
b.图表展示 = false;
|
||||
|
||||
let diff = a.对比(&b);
|
||||
assert_eq!(diff.len(), 2);
|
||||
assert_eq!(diff["推送K线"], serde_json::json!(false));
|
||||
assert_eq!(diff["图表展示"], serde_json::json!(false));
|
||||
}
|
||||
|
||||
#[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,404 @@
|
||||
/*
|
||||
* 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线.指标
|
||||
///
|
||||
/// `全序列` 包含当前K线(在末尾);prev 取自 全序列[..-1].last()
|
||||
/// 通过 RwLock 内部可变性,以 `&K线` 共享引用写入指标值
|
||||
pub fn 计算并挂载(全序列: &[Arc<K线>], 配置: &缠论配置) {
|
||||
let n = 全序列.len();
|
||||
let 当前K线 = &全序列[n - 1];
|
||||
let 现有序列 = if n > 1 { &全序列[..n - 1] } else { &[] };
|
||||
|
||||
// 作用域化 prev_guard:在 _回填新指标 之前释放,避免读锁与回填写锁冲突
|
||||
let has_prev;
|
||||
{
|
||||
let prev_guard = if n > 1 {
|
||||
Some(全序列[n - 2].指标.read())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let prev = prev_guard.as_deref();
|
||||
if 配置.计算指标 {
|
||||
Self::_计算MACD组(当前K线, prev, 配置);
|
||||
Self::_计算RSI组(当前K线, prev, 配置);
|
||||
Self::_计算KDJ组(当前K线, prev, 配置);
|
||||
Self::_计算BOLL组(当前K线, prev, 配置);
|
||||
}
|
||||
Self::_更新均线(当前K线, 现有序列, 配置); // 当前K线在 全序列[n-1], 现有序列不含它
|
||||
has_prev = n > 1;
|
||||
} // prev_guard dropped here
|
||||
|
||||
if has_prev {
|
||||
Self::_回填新指标(全序列, 配置);
|
||||
}
|
||||
}
|
||||
|
||||
fn _计算MACD组(当前K线: &K线, prev: Option<&指标容器>, 配置: &缠论配置) {
|
||||
for (i, (key, 计算方式, 快, 慢, 信号)) in 配置.MACD_参数列表.iter().enumerate()
|
||||
{
|
||||
let val = if let Some(prev_val) = prev.and_then(|p| p.获取(key)) {
|
||||
if let 指标值::MACD(prev_macd) = prev_val {
|
||||
指标值::MACD(平滑异同移动平均线::增量计算(
|
||||
prev_macd,
|
||||
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 + 1 <= 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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和,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+310
-13
@@ -22,30 +22,70 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
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 +100,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 +134,7 @@ impl K线 {
|
||||
|
||||
/// 序列化为大端字节序 48 字节
|
||||
/// 格式: >6d (时间戳, 开盘价, 高, 低, 收盘价, 成交量)
|
||||
/// TODO: 对齐 Python round(x, 8) 再序列化
|
||||
pub fn to_bytes(&self) -> [u8; 48] {
|
||||
let mut buf = [0u8; 48];
|
||||
{
|
||||
@@ -125,7 +181,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 +219,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 +248,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 +273,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(始);
|
||||
@@ -237,6 +419,7 @@ impl std::fmt::Display for K线 {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{error, info, warn};
|
||||
|
||||
#[test]
|
||||
fn test_方向() {
|
||||
@@ -273,4 +456,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());
|
||||
}
|
||||
}
|
||||
|
||||
+202
-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,137 @@ 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())
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::types::fractal::有高低 for 缠论K线 {
|
||||
|
||||
@@ -30,6 +30,7 @@ pub mod business;
|
||||
pub mod config;
|
||||
pub mod indicators;
|
||||
pub mod kline;
|
||||
pub mod log;
|
||||
pub mod structure;
|
||||
pub mod types;
|
||||
pub mod utils;
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
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);
|
||||
}
|
||||
|
||||
+859
-269
File diff suppressed because it is too large
Load Diff
@@ -26,12 +26,22 @@ use crate::structure::segment_feat::线段特征;
|
||||
use crate::types::分型结构;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// 特征分型 — 由三个线段特征元素构成的分型
|
||||
/// 特征分型 — 由三个线段特征元素构成的分型,用于线段特征序列的分型分析。
|
||||
///
|
||||
/// 字段:
|
||||
/// 左: 左侧线段特征元素
|
||||
/// 中: 中间线段特征元素
|
||||
/// 右: 右侧线段特征元素
|
||||
/// 结构: 由左中右三元素分析得出的分型结构(顶/底/散)
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct 特征分型 {
|
||||
/// 左侧线段特征元素
|
||||
pub 左: Arc<线段特征>,
|
||||
/// 中间线段特征元素
|
||||
pub 中: Arc<线段特征>,
|
||||
/// 右侧线段特征元素
|
||||
pub 右: Arc<线段特征>,
|
||||
/// 由左中右分析得出的分型结构
|
||||
pub 结构: 分型结构,
|
||||
}
|
||||
|
||||
|
||||
@@ -25,21 +25,39 @@
|
||||
use crate::kline::chan_kline::缠论K线;
|
||||
use crate::types::分型结构;
|
||||
use crate::types::相对方向;
|
||||
use crate::warn;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// 分型模式 — True 时使用构造时缓存值(默认),False 时从 中 缠K 实时读取
|
||||
pub static 分型模式: AtomicBool = AtomicBool::new(true);
|
||||
|
||||
/// 分型 — 由三根缠K构成(可能缺左或右)
|
||||
/// 分型 — 由左中右三根缠论K线构成的顶/底分型。
|
||||
///
|
||||
/// 字段:
|
||||
/// 左: 左侧缠K(可能为 None,表示序列起始处分型左翼缺失)
|
||||
/// 中: 中间缠K(必须存在),分型的核心K线,其 `分型` 字段记录了该位置的分型结构
|
||||
/// 右: 右侧缠K(可能为 None,表示序列末尾处分型右翼缺失)
|
||||
/// 结构: 构造时从 `中.分型` 缓存的分型结构(上/下/顶/底/散)
|
||||
/// 时间戳: 构造时从 `中.时间戳` 缓存的时刻
|
||||
/// 分型特征值: 构造时从 `中.分型特征值` 缓存的值
|
||||
///
|
||||
/// "分型模式" 全局开关控制 时间戳/结构/分型特征值 的读取行为:
|
||||
/// True(默认)— 返回构造时缓存值;False — 从 中 缠K 实时读取
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct 分型 {
|
||||
/// 左侧缠K(序列起始处分型可能为 None)
|
||||
pub 左: Option<Arc<缠论K线>>,
|
||||
/// 中间缠K(必须存在)
|
||||
pub 中: Arc<缠论K线>,
|
||||
/// 右侧缠K(序列末尾处分型可能为 None)
|
||||
pub 右: Option<Arc<缠论K线>>,
|
||||
/// 构造时缓存的 分型结构(上/下/顶/底/散)
|
||||
pub 结构: 分型结构,
|
||||
/// 构造时缓存的 时间戳
|
||||
pub 时间戳: i64,
|
||||
/// 构造时缓存的 分型特征值(历史高低点极值)
|
||||
pub 分型特征值: f64,
|
||||
}
|
||||
|
||||
@@ -47,7 +65,17 @@ impl 分型 {
|
||||
pub fn new(
|
||||
左: Option<Arc<缠论K线>>, 中: Arc<缠论K线>, 右: Option<Arc<缠论K线>>
|
||||
) -> Self {
|
||||
let 结构 = 中.分型.read().unwrap().unwrap_or(分型结构::散);
|
||||
if let (Some(左), Some(右)) = (&左, &右) {
|
||||
assert!(
|
||||
左.时间戳.load(Ordering::Relaxed) < 中.时间戳.load(Ordering::Relaxed)
|
||||
&& 中.时间戳.load(Ordering::Relaxed) < 右.时间戳.load(Ordering::Relaxed),
|
||||
"分型时间戳断言失败: 左={}, 中={}, 右={}",
|
||||
左.时间戳.load(Ordering::Relaxed),
|
||||
中.时间戳.load(Ordering::Relaxed),
|
||||
右.时间戳.load(Ordering::Relaxed),
|
||||
);
|
||||
}
|
||||
let 结构 = 中.分型.read().unwrap_or(分型结构::散);
|
||||
let 时间戳 = 中.时间戳.load(Ordering::Relaxed);
|
||||
let 分型特征值 = 中.分型特征值.get();
|
||||
Self {
|
||||
@@ -69,6 +97,24 @@ impl 分型 {
|
||||
}
|
||||
}
|
||||
|
||||
/// 结构 — 根据 分型模式 决定返回缓存值(True)或实时值(False)
|
||||
pub fn 结构(&self) -> 分型结构 {
|
||||
if 分型模式.load(Ordering::Relaxed) {
|
||||
self.结构
|
||||
} else {
|
||||
self.中.分型.read().unwrap_or(分型结构::散) // FIXME 错误
|
||||
}
|
||||
}
|
||||
|
||||
/// 分型特征值 — 根据 分型模式 决定返回缓存值(True)或实时值(False)
|
||||
pub fn 分型特征值(&self) -> f64 {
|
||||
if 分型模式.load(Ordering::Relaxed) {
|
||||
self.分型特征值
|
||||
} else {
|
||||
self.中.分型特征值.get()
|
||||
}
|
||||
}
|
||||
|
||||
/// 左中右三组关系
|
||||
pub fn 关系组(&self) -> Option<(相对方向, 相对方向, 相对方向)> {
|
||||
let 左 = self.左.as_ref()?;
|
||||
@@ -82,7 +128,7 @@ impl 分型 {
|
||||
|
||||
/// 分型强度 — 返回 "强"/"中"/"弱"/"未知"
|
||||
pub fn 强度(&self) -> &str {
|
||||
if self.结构 != 分型结构::底 && self.结构 != 分型结构::顶 {
|
||||
if self.结构() != 分型结构::底 && self.结构() != 分型结构::顶 {
|
||||
return "未知";
|
||||
}
|
||||
if self.右.is_none() || self.左.is_none() {
|
||||
@@ -90,7 +136,7 @@ impl 分型 {
|
||||
}
|
||||
|
||||
if let Some(ref 关系组) = self.关系组() {
|
||||
if self.结构 == 分型结构::底 {
|
||||
if self.结构() == 分型结构::底 {
|
||||
if 关系组.2.是否向下() {
|
||||
return "弱";
|
||||
} else if 关系组.2.是否向上() {
|
||||
@@ -98,7 +144,7 @@ impl 分型 {
|
||||
} else {
|
||||
return "中";
|
||||
}
|
||||
} else if self.结构 == 分型结构::顶 {
|
||||
} else if self.结构() == 分型结构::顶 {
|
||||
if 关系组.2.是否向上() {
|
||||
return "弱";
|
||||
} else if 关系组.2.是否向下() {
|
||||
@@ -109,21 +155,19 @@ impl 分型 {
|
||||
}
|
||||
}
|
||||
|
||||
if let (Some(ref 左), Some(ref 右)) = (&self.左, &self.右) {
|
||||
if self.结构 == 分型结构::底 {
|
||||
if 右.标的K线.read().unwrap().收盘价 > 左.标的K线.read().unwrap().高 {
|
||||
if let (Some(左), Some(右)) = (&self.左, &self.右) {
|
||||
if self.结构() == 分型结构::底 {
|
||||
if 右.标的K线.read().收盘价 > 左.标的K线.read().高 {
|
||||
return "强";
|
||||
} else if 右.标的K线.read().unwrap().收盘价 > self.中.标的K线.read().unwrap().高
|
||||
{
|
||||
} else if 右.标的K线.read().收盘价 > self.中.标的K线.read().高 {
|
||||
return "中";
|
||||
} else {
|
||||
return "弱";
|
||||
}
|
||||
} else if self.结构 == 分型结构::顶 {
|
||||
if 右.标的K线.read().unwrap().收盘价 < 左.标的K线.read().unwrap().低 {
|
||||
} else if self.结构() == 分型结构::顶 {
|
||||
if 右.标的K线.read().收盘价 < 左.标的K线.read().低 {
|
||||
return "强";
|
||||
} else if 右.标的K线.read().unwrap().收盘价 < self.中.标的K线.read().unwrap().低
|
||||
{
|
||||
} else if 右.标的K线.read().收盘价 < self.中.标的K线.read().低 {
|
||||
return "中";
|
||||
} else {
|
||||
return "弱";
|
||||
@@ -135,23 +179,29 @@ impl 分型 {
|
||||
|
||||
/// MACD柱子分型匹配 — 检查左中右MACD柱形成顶/底形态
|
||||
pub fn 与MACD柱子分型匹配(&self) -> bool {
|
||||
if let (Some(ref 左), Some(ref 右)) = (&self.左, &self.右) {
|
||||
if self.结构 == 分型结构::底 {
|
||||
let 左_k = 左.标的K线.read().unwrap();
|
||||
let 中_k = self.中.标的K线.read().unwrap();
|
||||
let 右_k = 右.标的K线.read().unwrap();
|
||||
if let (Some(ref 左macd), Some(ref 中macd), Some(ref 右macd)) =
|
||||
(&左_k.macd, &中_k.macd, &右_k.macd)
|
||||
if let (Some(左), Some(右)) = (&self.左, &self.右) {
|
||||
if self.结构() == 分型结构::底 {
|
||||
let 左_k = 左.标的K线.read();
|
||||
let 中_k = self.中.标的K线.read();
|
||||
let 右_k = 右.标的K线.read();
|
||||
let 左_m = 左_k.指标.read();
|
||||
let 中_m = 中_k.指标.read();
|
||||
let 右_m = 右_k.指标.read();
|
||||
if let (Some(左macd), Some(中macd), Some(右macd)) =
|
||||
(左_m.macd(), 中_m.macd(), 右_m.macd())
|
||||
{
|
||||
return 左macd.MACD柱 > 中macd.MACD柱 && 中macd.MACD柱 < 右macd.MACD柱;
|
||||
}
|
||||
}
|
||||
if self.结构 == 分型结构::顶 {
|
||||
let 左_k = 左.标的K线.read().unwrap();
|
||||
let 中_k = self.中.标的K线.read().unwrap();
|
||||
let 右_k = 右.标的K线.read().unwrap();
|
||||
if let (Some(ref 左macd), Some(ref 中macd), Some(ref 右macd)) =
|
||||
(&左_k.macd, &中_k.macd, &右_k.macd)
|
||||
if self.结构() == 分型结构::顶 {
|
||||
let 左_k = 左.标的K线.read();
|
||||
let 中_k = self.中.标的K线.read();
|
||||
let 右_k = 右.标的K线.read();
|
||||
let 左_m = 左_k.指标.read();
|
||||
let 中_m = 中_k.指标.read();
|
||||
let 右_m = 右_k.指标.read();
|
||||
if let (Some(左macd), Some(中macd), Some(右macd)) =
|
||||
(左_m.macd(), 中_m.macd(), 右_m.macd())
|
||||
{
|
||||
return 左macd.MACD柱 < 中macd.MACD柱 && 中macd.MACD柱 > 右macd.MACD柱;
|
||||
}
|
||||
@@ -161,11 +211,8 @@ impl 分型 {
|
||||
}
|
||||
|
||||
/// 判断两个分型是否匹配
|
||||
pub fn 判断分型(左: &Arc<分型>, 右: &Arc<分型>, 模式: &str) -> bool {
|
||||
match 模式 {
|
||||
"中" => Arc::as_ptr(左) == Arc::as_ptr(右),
|
||||
_ => false,
|
||||
}
|
||||
pub fn 判断分型(左: &Arc<分型>, 右: &Arc<分型>, _模式: &str) -> bool {
|
||||
Arc::ptr_eq(左, 右)
|
||||
}
|
||||
|
||||
/// 从缠K序列中获取以指定缠K为中元素的分型
|
||||
@@ -173,9 +220,7 @@ impl 分型 {
|
||||
K线序列: &[Arc<缠论K线>],
|
||||
中: &Arc<缠论K线>,
|
||||
) -> Option<Self> {
|
||||
let idx = K线序列
|
||||
.iter()
|
||||
.position(|k| Arc::as_ptr(k) == Arc::as_ptr(中))?;
|
||||
let idx = K线序列.iter().position(|k| Arc::ptr_eq(k, 中))?;
|
||||
let 左 = if idx > 0 {
|
||||
Some(Arc::clone(&K线序列[idx - 1]))
|
||||
} else {
|
||||
@@ -192,21 +237,83 @@ impl 分型 {
|
||||
/// 向分型序列中添加新分型
|
||||
pub fn 向序列中添加(分型序列: &mut Vec<Arc<分型>>, 当前分型: Arc<分型>) {
|
||||
if 分型序列.is_empty() {
|
||||
if 当前分型.结构 != 分型结构::顶 && 当前分型.结构 != 分型结构::底
|
||||
if 当前分型.结构() != 分型结构::顶 && 当前分型.结构() != 分型结构::底
|
||||
{
|
||||
panic!("首次添加分型不为 顶底 {:?}", 当前分型);
|
||||
}
|
||||
} else {
|
||||
let 前一个 = &分型序列[分型序列.len() - 1];
|
||||
if 前一个.结构 == 当前分型.结构 {
|
||||
if 前一个.结构() == 当前分型.结构() {
|
||||
panic!("分型相同无法添加 {:?} {:?}", 前一个, 当前分型);
|
||||
}
|
||||
if 前一个.右.is_none() {
|
||||
eprintln!("分型.向序列中添加, 分型异常 {:?}", 前一个);
|
||||
warn!("分型.向序列中添加, 分型异常 {:?}", 前一个);
|
||||
}
|
||||
}
|
||||
分型序列.push(当前分型);
|
||||
}
|
||||
|
||||
/// 结构化相等校验 — 递归校验左/中/右缠伦K线 + 自有缓存字段,返回 (是否相等, 差异描述)
|
||||
pub fn 相等(&self, other: &Self, 浮点容差: f64) -> (bool, String) {
|
||||
match (&self.左, &other.左) {
|
||||
(None, None) => {}
|
||||
(Some(a), Some(b)) => {
|
||||
let (eq, msg) = a.相等(b, 浮点容差);
|
||||
if !eq {
|
||||
return (false, format!("分型: [左]缠论K线异常 >> {msg}"));
|
||||
}
|
||||
}
|
||||
(a, b) => {
|
||||
return (
|
||||
false,
|
||||
format!("分型: [左]空值不一致 A={},B={}", a.is_some(), b.is_some()),
|
||||
);
|
||||
}
|
||||
}
|
||||
{
|
||||
let (eq, msg) = self.中.相等(&other.中, 浮点容差);
|
||||
if !eq {
|
||||
return (false, format!("分型: [中]缠论K线异常 >> {msg}"));
|
||||
}
|
||||
}
|
||||
match (&self.右, &other.右) {
|
||||
(None, None) => {}
|
||||
(Some(a), Some(b)) => {
|
||||
let (eq, msg) = a.相等(b, 浮点容差);
|
||||
if !eq {
|
||||
return (false, format!("分型: [右]缠论K线异常 >> {msg}"));
|
||||
}
|
||||
}
|
||||
(a, b) => {
|
||||
return (
|
||||
false,
|
||||
format!("分型: [右]空值不一致 A={},B={}", a.is_some(), b.is_some()),
|
||||
);
|
||||
}
|
||||
}
|
||||
if self.结构 != other.结构 {
|
||||
return (
|
||||
false,
|
||||
format!("分型: [结构] 不等 A={},B={}", self.结构, other.结构),
|
||||
);
|
||||
}
|
||||
if self.时间戳 != other.时间戳 {
|
||||
return (
|
||||
false,
|
||||
format!("分型: [时间戳] 不等 A={},B={}", self.时间戳, other.时间戳),
|
||||
);
|
||||
}
|
||||
if (self.分型特征值 - other.分型特征值).abs() > 浮点容差 {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"分型: [分型特征值] 浮点超限 A={:.10},B={:.10}",
|
||||
self.分型特征值, other.分型特征值
|
||||
),
|
||||
);
|
||||
}
|
||||
(true, "分型: 全部字段一致".into())
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::types::fractal::有高低 for 分型 {
|
||||
@@ -223,13 +330,9 @@ impl std::fmt::Display for 分型 {
|
||||
write!(
|
||||
f,
|
||||
"{}<{}, {}, None: {}, None: {}>",
|
||||
self.中
|
||||
.分型
|
||||
.read()
|
||||
.unwrap()
|
||||
.unwrap_or(crate::types::分型结构::散),
|
||||
self.结构(),
|
||||
self.时间戳(),
|
||||
crate::utils::format_f64_g(self.分型特征值),
|
||||
crate::utils::format_f64_g(self.分型特征值()),
|
||||
if self.左.is_none() { "True" } else { "False" },
|
||||
if self.右.is_none() { "True" } else { "False" },
|
||||
)
|
||||
|
||||
@@ -26,37 +26,71 @@ use crate::structure::dash_line::虚线;
|
||||
use crate::structure::feat_fractal::特征分型;
|
||||
use crate::structure::fractal_obj::分型;
|
||||
use crate::types::{分型结构, 相对方向};
|
||||
use std::sync::atomic::Ordering;
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicI64;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
/// 线段特征 — 特征序列元素(内部是虚线的集合)
|
||||
#[derive(Debug, Clone)]
|
||||
/// 线段特征 — 特征序列元素,内部是虚线的集合。
|
||||
///
|
||||
/// 在线段划分算法中,将同方向的笔归入一个特征序列元素,
|
||||
/// 然后通过特征序列元素之间的分型关系判定线段终结。
|
||||
///
|
||||
/// 字段:
|
||||
/// 序号: 特征序列元素编号
|
||||
/// 标识: 标识字符串(如 "特征<虚线>")
|
||||
/// 线段方向: 所属线段的方向(与元素包含的虚线方向相反)
|
||||
/// 基础序列: 构成该特征序列元素的虚线序列
|
||||
///
|
||||
/// 计算属性:
|
||||
/// 文 — 起点分型(取特征序列中分型特征值极值)
|
||||
/// 武 — 终点分型(取特征序列中分型特征值极值)
|
||||
/// 方向 — 线段方向的翻转
|
||||
/// 高 / 低 — 文/武 中分型特征值的较大/较小者
|
||||
#[derive(Debug)]
|
||||
pub struct 线段特征 {
|
||||
pub 序号: i64,
|
||||
pub 标识: String,
|
||||
/// 特征序列元素编号
|
||||
pub 序号: AtomicI64,
|
||||
/// 标识字符串(如 "特征<虚线>")
|
||||
pub 标识: RwLock<String>,
|
||||
/// 所属线段的方向
|
||||
pub 线段方向: 相对方向,
|
||||
pub 元素: Vec<Arc<虚线>>,
|
||||
/// 构成该元素的虚线序列
|
||||
pub 基础序列: Vec<Arc<虚线>>,
|
||||
}
|
||||
|
||||
impl Clone for 线段特征 {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
序号: AtomicI64::new(self.序号.load(Ordering::Relaxed)),
|
||||
标识: RwLock::new(self.标识.read().clone()),
|
||||
线段方向: self.线段方向,
|
||||
基础序列: self.基础序列.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl 线段特征 {
|
||||
/// 新建线段特征(给定标识、基础序列和线段方向)
|
||||
pub fn new(标识: String, 基础序列: Vec<Arc<虚线>>, 线段方向: 相对方向) -> Self {
|
||||
Self {
|
||||
序号: 0,
|
||||
标识,
|
||||
序号: AtomicI64::new(0),
|
||||
标识: RwLock::new(标识),
|
||||
线段方向,
|
||||
元素: 基础序列,
|
||||
基础序列,
|
||||
}
|
||||
}
|
||||
|
||||
/// 图表标题 — 返回标识字符串
|
||||
pub fn 图表标题(&self) -> String {
|
||||
self.标识.clone()
|
||||
self.标识.read().clone()
|
||||
}
|
||||
|
||||
/// 文 — 取特征序列元素中分型特征值最大/最小的文分型
|
||||
/// tiebreaker: later时间戳 wins when特征值 equal (matches Python)
|
||||
pub fn 文(&self) -> Arc<分型> {
|
||||
if self.线段方向.是否向上() {
|
||||
self.元素
|
||||
self.基础序列
|
||||
.iter()
|
||||
.max_by(|a, b| {
|
||||
a.文
|
||||
@@ -66,74 +100,60 @@ impl 线段特征 {
|
||||
.then_with(|| a.文.时间戳().cmp(&b.文.时间戳()))
|
||||
})
|
||||
.map(|x| Arc::clone(&x.文))
|
||||
.unwrap_or_else(|| Arc::clone(&self.元素[0].文))
|
||||
.unwrap_or_else(|| Arc::clone(&self.基础序列[0].文))
|
||||
} else {
|
||||
self.元素
|
||||
self.基础序列
|
||||
.iter()
|
||||
.min_by(|a, b| {
|
||||
a.文
|
||||
.max_by(|a, b| {
|
||||
b.文
|
||||
.分型特征值
|
||||
.partial_cmp(&b.文.分型特征值)
|
||||
.partial_cmp(&a.文.分型特征值)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
.then_with(|| b.文.时间戳().cmp(&a.文.时间戳()))
|
||||
.then_with(|| a.文.时间戳().cmp(&b.文.时间戳()))
|
||||
})
|
||||
.map(|x| Arc::clone(&x.文))
|
||||
.unwrap_or_else(|| Arc::clone(&self.元素[0].文))
|
||||
.unwrap_or_else(|| Arc::clone(&self.基础序列[0].文))
|
||||
}
|
||||
}
|
||||
|
||||
/// 武 — 取特征序列元素中分型特征值最大/最小的武分型
|
||||
/// tiebreaker: later时间戳 wins when特征值 equal (matches Python)
|
||||
pub fn 武(&self) -> Arc<分型> {
|
||||
if self.线段方向.是否向上() {
|
||||
self.元素
|
||||
.iter()
|
||||
.max_by(|a, b| {
|
||||
a.武
|
||||
.read()
|
||||
.unwrap()
|
||||
.分型特征值
|
||||
.partial_cmp(&b.武.read().unwrap().分型特征值)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
.then_with(|| {
|
||||
a.武
|
||||
.read()
|
||||
.unwrap()
|
||||
.时间戳()
|
||||
.cmp(&b.武.read().unwrap().时间戳())
|
||||
})
|
||||
})
|
||||
.map(|x| x.武.read().unwrap().clone())
|
||||
.unwrap_or_else(|| self.元素[0].武.read().unwrap().clone())
|
||||
let best = if self.线段方向.是否向上() {
|
||||
self.基础序列.iter().max_by(|a, b| {
|
||||
let a_武 = a.武.read();
|
||||
let b_武 = b.武.read();
|
||||
a_武
|
||||
.分型特征值
|
||||
.partial_cmp(&b_武.分型特征值)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
.then_with(|| a_武.时间戳().cmp(&b_武.时间戳()))
|
||||
})
|
||||
} else {
|
||||
self.元素
|
||||
.iter()
|
||||
.min_by(|a, b| {
|
||||
a.武
|
||||
.read()
|
||||
.unwrap()
|
||||
.分型特征值
|
||||
.partial_cmp(&b.武.read().unwrap().分型特征值)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
.then_with(|| {
|
||||
b.武
|
||||
.read()
|
||||
.unwrap()
|
||||
.时间戳()
|
||||
.cmp(&a.武.read().unwrap().时间戳())
|
||||
})
|
||||
})
|
||||
.map(|x| x.武.read().unwrap().clone())
|
||||
.unwrap_or_else(|| self.元素[0].武.read().unwrap().clone())
|
||||
}
|
||||
self.基础序列.iter().max_by(|a, b| {
|
||||
let a_武 = a.武.read();
|
||||
let b_武 = b.武.read();
|
||||
b_武
|
||||
.分型特征值
|
||||
.partial_cmp(&a_武.分型特征值)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
.then_with(|| a_武.时间戳().cmp(&b_武.时间戳()))
|
||||
})
|
||||
};
|
||||
best.map_or_else(
|
||||
|| self.基础序列[0].武.read().clone(),
|
||||
|x| x.武.read().clone(),
|
||||
)
|
||||
}
|
||||
|
||||
/// 高 — 文和武中分型特征值的较大者
|
||||
pub fn 高(&self) -> f64 {
|
||||
let 文 = self.文();
|
||||
let 武 = self.武();
|
||||
文.分型特征值.max(武.分型特征值)
|
||||
}
|
||||
|
||||
/// 低 — 文和武中分型特征值的较小者
|
||||
pub fn 低(&self) -> f64 {
|
||||
let 文 = self.文();
|
||||
let 武 = self.武();
|
||||
@@ -146,25 +166,25 @@ impl 线段特征 {
|
||||
}
|
||||
|
||||
/// 向特征序列元素中添加虚线
|
||||
pub fn 添加(&mut self, 待添加虚线: Arc<虚线>) -> Result<(), String> {
|
||||
pub fn _添加(&mut self, 待添加虚线: Arc<虚线>) -> Result<(), String> {
|
||||
if 待添加虚线.方向() == self.线段方向 {
|
||||
return Err("添加方向与线段方向相同".into());
|
||||
}
|
||||
self.元素.push(待添加虚线);
|
||||
self.基础序列.push(待添加虚线);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 从特征序列元素中删除虚线
|
||||
pub fn 删除(&mut self, 待删除虚线: &Arc<虚线>) -> Result<(), String> {
|
||||
pub fn _删除(&mut self, 待删除虚线: &Arc<虚线>) -> Result<(), String> {
|
||||
if 待删除虚线.方向() == self.方向() {
|
||||
return Err("删除方向与特征序列方向相同".into());
|
||||
}
|
||||
if let Some(pos) = self
|
||||
.元素
|
||||
.基础序列
|
||||
.iter()
|
||||
.position(|x| Arc::as_ptr(x) == Arc::as_ptr(待删除虚线))
|
||||
.position(|x| Arc::ptr_eq(x, 待删除虚线))
|
||||
{
|
||||
self.元素.remove(pos);
|
||||
self.基础序列.remove(pos);
|
||||
Ok(())
|
||||
} else {
|
||||
Err("待删除虚线不在特征序列中".into())
|
||||
@@ -173,7 +193,7 @@ impl 线段特征 {
|
||||
|
||||
/// 新建特征序列元素
|
||||
pub fn 新建(虚线序列: Vec<Arc<虚线>>, 线段方向: 相对方向) -> Self {
|
||||
let 标识 = format!("特征<虚线>");
|
||||
let 标识 = "特征<虚线>".to_string();
|
||||
Self::new(标识, 虚线序列, 线段方向)
|
||||
}
|
||||
|
||||
@@ -210,18 +230,18 @@ impl 线段特征 {
|
||||
|
||||
if 应替换 {
|
||||
let 小号虚线 = 中
|
||||
.元素
|
||||
.基础序列
|
||||
.iter()
|
||||
.min_by_key(|o| o.序号.load(Ordering::Relaxed))
|
||||
.unwrap();
|
||||
let 大号虚线 = 右
|
||||
.元素
|
||||
.基础序列
|
||||
.iter()
|
||||
.max_by_key(|o| o.序号.load(Ordering::Relaxed))
|
||||
.unwrap();
|
||||
let fake = 虚线::创建笔(
|
||||
Arc::clone(&小号虚线.文),
|
||||
大号虚线.武.read().unwrap().clone(),
|
||||
大号虚线.武.read().clone(),
|
||||
false,
|
||||
);
|
||||
结果.pop();
|
||||
@@ -250,7 +270,7 @@ impl 线段特征 {
|
||||
)) {
|
||||
// Clone-modify-replace
|
||||
let mut 新特征 = (*结果[最后_idx]).clone();
|
||||
let _ = 新特征.添加(Arc::clone(虚线));
|
||||
let _ = 新特征._添加(Arc::clone(虚线));
|
||||
结果[最后_idx] = Arc::new(新特征);
|
||||
} else {
|
||||
结果.push(Arc::new(Self::新建(vec![Arc::clone(虚线)], 线段方向)));
|
||||
@@ -260,7 +280,7 @@ impl 线段特征 {
|
||||
结果
|
||||
}
|
||||
|
||||
/// 获取分型序列
|
||||
/// 获取分型序列 — 从连续的特征序列元素中提取特征分型
|
||||
pub fn 获取分型序列(特征序列: &[Arc<线段特征>]) -> Vec<特征分型> {
|
||||
let mut 结果 = Vec::new();
|
||||
if 特征序列.len() < 3 {
|
||||
@@ -282,6 +302,56 @@ impl 线段特征 {
|
||||
}
|
||||
结果
|
||||
}
|
||||
|
||||
/// 结构化相等校验 — 逐项递归校验基础序列中的虚线,返回 (是否相等, 差异描述)
|
||||
pub fn 相等(&self, other: &Self, 浮点容差: f64) -> (bool, String) {
|
||||
if self.序号.load(Ordering::Relaxed) != other.序号.load(Ordering::Relaxed) {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"线段特征: [序号] 不等 A={},B={}",
|
||||
self.序号.load(Ordering::Relaxed),
|
||||
other.序号.load(Ordering::Relaxed)
|
||||
),
|
||||
);
|
||||
}
|
||||
if *self.标识.read() != *other.标识.read() {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"线段特征: [标识] 不等 A={},B={}",
|
||||
self.标识.read(),
|
||||
other.标识.read()
|
||||
),
|
||||
);
|
||||
}
|
||||
if self.线段方向 != other.线段方向 {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"线段特征: [线段方向] 不等 A={},B={}",
|
||||
self.线段方向, other.线段方向
|
||||
),
|
||||
);
|
||||
}
|
||||
if self.基础序列.len() != other.基础序列.len() {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"线段特征: [基础序列] 长度不一致 A={},B={}",
|
||||
self.基础序列.len(),
|
||||
other.基础序列.len()
|
||||
),
|
||||
);
|
||||
}
|
||||
for (idx, (a, b)) in self.基础序列.iter().zip(other.基础序列.iter()).enumerate() {
|
||||
let (eq, msg) = a.相等(b, 浮点容差);
|
||||
if !eq {
|
||||
return (false, format!("线段特征: 基础序列[{idx}]虚线异常 >> {msg}"));
|
||||
}
|
||||
}
|
||||
(true, "线段特征: 全部字段一致".into())
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::types::fractal::有高低 for 线段特征 {
|
||||
@@ -295,17 +365,17 @@ impl crate::types::fractal::有高低 for 线段特征 {
|
||||
|
||||
impl std::fmt::Display for 线段特征 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.元素.is_empty() {
|
||||
write!(f, "{}<{}, 空>", self.标识, self.线段方向)
|
||||
if self.基础序列.is_empty() {
|
||||
write!(f, "{}<{}, 空>", self.标识.read(), self.线段方向)
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"{}<{}, {}, {}, {}>",
|
||||
self.标识,
|
||||
self.标识.read(),
|
||||
self.线段方向,
|
||||
self.文(),
|
||||
self.武(),
|
||||
self.元素.len()
|
||||
self.基础序列.len()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -320,13 +390,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(
|
||||
@@ -557,7 +628,7 @@ mod tests {
|
||||
let 笔1 = 辅助_创建笔(100, 100.0, 90.0, 200, 90.0, 80.0);
|
||||
let mut feat = 线段特征::new("测试".into(), vec![], 相对方向::向上);
|
||||
|
||||
let result = feat.添加(Arc::clone(&笔1));
|
||||
let result = feat._添加(Arc::clone(&笔1));
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
@@ -567,7 +638,7 @@ mod tests {
|
||||
let 笔1 = 辅助_创建笔(100, 100.0, 90.0, 200, 90.0, 80.0);
|
||||
let mut feat = 线段特征::new("测试".into(), vec![], 相对方向::向下);
|
||||
|
||||
let result = feat.添加(Arc::clone(&笔1));
|
||||
let result = feat._添加(Arc::clone(&笔1));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +91,7 @@ impl std::fmt::Display for 买卖点类型 {
|
||||
}
|
||||
|
||||
impl 买卖点类型 {
|
||||
/// 判断是否为买点类型
|
||||
pub fn 是买点(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
@@ -106,6 +107,7 @@ impl 买卖点类型 {
|
||||
)
|
||||
}
|
||||
|
||||
/// 判断是否为卖点类型
|
||||
pub fn 是卖点(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
|
||||
@@ -48,6 +48,7 @@ pub enum 相对方向 {
|
||||
}
|
||||
|
||||
impl 相对方向 {
|
||||
/// 翻转方向(向上→向下,缺口向上→缺口向下,顺→逆 等)
|
||||
pub fn 翻转(&self) -> Self {
|
||||
match self {
|
||||
Self::向上 => Self::向下,
|
||||
@@ -62,32 +63,33 @@ impl 相对方向 {
|
||||
}
|
||||
}
|
||||
|
||||
/// 是否向上方向(向上/向上缺口/衔接向上)
|
||||
pub fn 是否向上(&self) -> bool {
|
||||
matches!(self, Self::向上 | Self::向上缺口 | Self::衔接向上)
|
||||
}
|
||||
|
||||
/// 是否向下方向(向下/向下缺口/衔接向下)
|
||||
pub fn 是否向下(&self) -> bool {
|
||||
matches!(self, Self::向下 | Self::向下缺口 | Self::衔接向下)
|
||||
}
|
||||
|
||||
/// 是否包含关系(顺/逆/同)
|
||||
pub fn 是否包含(&self) -> bool {
|
||||
matches!(self, Self::顺 | Self::逆 | Self::同)
|
||||
}
|
||||
|
||||
/// 是否缺口(向上缺口/向下缺口)
|
||||
pub fn 是否缺口(&self) -> bool {
|
||||
matches!(self, Self::向下缺口 | Self::向上缺口)
|
||||
}
|
||||
|
||||
/// 是否衔接(衔接向上/衔接向下)
|
||||
pub fn 是否衔接(&self) -> bool {
|
||||
matches!(self, Self::衔接向下 | Self::衔接向上)
|
||||
}
|
||||
|
||||
/// 分析两个K线之间的相对方向
|
||||
pub fn 分析(前高: f64, 前低: f64, 后高: f64, 后低: f64) -> Self {
|
||||
// NaN 值无法判断方向,视为"同"避免 panic
|
||||
if 前高.is_nan() || 前低.is_nan() || 后高.is_nan() || 后低.is_nan() {
|
||||
return Self::同;
|
||||
}
|
||||
if 前高 == 后高 && 前低 == 后低 {
|
||||
return Self::同;
|
||||
}
|
||||
@@ -120,6 +122,34 @@ impl 相对方向 {
|
||||
前高, 前低, 后高, 后低
|
||||
);
|
||||
}
|
||||
|
||||
/// 从可选方向序列中随机选取指定数量(与 chan.py 对齐)
|
||||
pub fn 从序列中机选(
|
||||
数量: usize,
|
||||
可选方向: &[相对方向],
|
||||
可重复: bool,
|
||||
) -> Vec<相对方向> {
|
||||
if 数量 == 0 || 可选方向.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
if !可重复 && 数量 > 可选方向.len() {
|
||||
panic!("数量超过可选方向数");
|
||||
}
|
||||
let mut result = Vec::with_capacity(数量);
|
||||
if 可重复 {
|
||||
for _ in 0..数量 {
|
||||
let idx = fastrand::usize(..可选方向.len());
|
||||
result.push(可选方向[idx]);
|
||||
}
|
||||
} else {
|
||||
let mut indices: Vec<usize> = (0..可选方向.len()).collect();
|
||||
fastrand::shuffle(&mut indices);
|
||||
for &idx in indices.iter().take(数量) {
|
||||
result.push(可选方向[idx]);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for 相对方向 {
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
use crate::warn;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 分型结构 —— 三根K线构成的结构形态
|
||||
@@ -92,7 +93,10 @@ impl 分型结构 {
|
||||
)
|
||||
}
|
||||
|
||||
fn 分析_内部(
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
/// 分析三K线的分型结构(内部实现,接收裸f64值)
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn 分析_内部(
|
||||
左高: f64,
|
||||
左低: f64,
|
||||
中高: f64,
|
||||
@@ -111,36 +115,23 @@ impl 分型结构 {
|
||||
let 向下类 = |d: 相对方向| d.是否向下();
|
||||
|
||||
match (左中关系, 中右关系) {
|
||||
// 顺序包含 — 忽视时可以绕过
|
||||
(相对方向::顺, _) if !忽视顺序包含 => {
|
||||
panic!("顺序包含: {:?} {:?}", 左中关系, 中右关系);
|
||||
panic!("顺序包含 左中相对方向");
|
||||
}
|
||||
(_, 相对方向::顺) if !忽视顺序包含 => {
|
||||
panic!("顺序包含: {:?} {:?}", 左中关系, 中右关系);
|
||||
panic!("顺序包含 中右相对方向");
|
||||
}
|
||||
// 向上 + 向上 = 三连上
|
||||
(a, b) if 向上类(a) && 向上类(b) => Some(Self::上),
|
||||
// 向上 + 向下 = 顶分型
|
||||
(a, b) if 向上类(a) && 向下类(b) => Some(Self::顶),
|
||||
// 向上 + 逆序包含 = 上
|
||||
(a, 相对方向::逆) if 向上类(a) && 可以逆序包含 => Some(Self::上),
|
||||
// 向下 + 向上 = 底分型
|
||||
(a, b) if 向下类(a) && 向上类(b) => Some(Self::底),
|
||||
// 向下 + 向下 = 三连下
|
||||
(a, b) if 向下类(a) && 向下类(b) => Some(Self::下),
|
||||
// 向下 + 逆序包含 = 下
|
||||
(a, 相对方向::逆) if 向下类(a) && 可以逆序包含 => Some(Self::下),
|
||||
// 逆序包含 + 向上 = 底
|
||||
(相对方向::逆, a) if 向上类(a) && 可以逆序包含 => Some(Self::底),
|
||||
// 逆序包含 + 向下 = 顶
|
||||
(相对方向::逆, a) if 向下类(a) && 可以逆序包含 => Some(Self::顶),
|
||||
// 逆序包含 + 逆序包含 = 散
|
||||
(相对方向::逆, 相对方向::逆) if 可以逆序包含 => Some(Self::散),
|
||||
_ => {
|
||||
eprintln!(
|
||||
"无法识别的分型结构: 左中={:?}, 中右={:?}",
|
||||
左中关系, 中右关系
|
||||
);
|
||||
warn!("未匹配的关系 {}, {}, {}", 可以逆序包含, 左中关系, 中右关系);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,11 +27,14 @@ use serde::{Deserialize, Serialize};
|
||||
/// 缺口 —— 两个价格区间之间的空隙
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct 缺口 {
|
||||
/// 缺口上沿
|
||||
pub 高: f64,
|
||||
/// 缺口下沿
|
||||
pub 低: f64,
|
||||
}
|
||||
|
||||
impl 缺口 {
|
||||
/// 创建缺口,高必须大于低
|
||||
pub fn new(高: f64, 低: f64) -> Self {
|
||||
assert!(高 > 低, "缺口高必须大于低: 高={高}, 低={低}");
|
||||
Self { 高, 低 }
|
||||
@@ -61,6 +64,29 @@ impl 缺口 {
|
||||
|
||||
Some(Self::new(上界.max(下界), 上界.min(下界)))
|
||||
}
|
||||
|
||||
/// 结构化相等校验 — 浮点容差比较高/低,返回 (是否相等, 差异描述)
|
||||
pub fn 相等(&self, other: &Self, 浮点容差: f64) -> (bool, String) {
|
||||
if (self.高 - other.高).abs() > 浮点容差 {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"缺口: [高] 浮点超限 容差={浮点容差:.2e} A={:.10},B={:.10}",
|
||||
self.高, other.高
|
||||
),
|
||||
);
|
||||
}
|
||||
if (self.低 - other.低).abs() > 浮点容差 {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"缺口: [低] 浮点超限 容差={浮点容差:.2e} A={:.10},B={:.10}",
|
||||
self.低, other.低
|
||||
),
|
||||
);
|
||||
}
|
||||
(true, "缺口: 高低价格一致".into())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for 缺口 {
|
||||
|
||||
@@ -1,18 +1,46 @@
|
||||
/*
|
||||
* 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::{AtomicU64, Ordering};
|
||||
|
||||
/// f64 原子类型 — 基于 AtomicU64 + 位转换,API 与 `Cell<f64>` 一致。
|
||||
#[derive(Debug, Default)]
|
||||
/// 线程安全的 f64 原子容器
|
||||
pub struct SyncF64(AtomicU64);
|
||||
|
||||
impl SyncF64 {
|
||||
/// 创建 SyncF64
|
||||
pub fn new(v: f64) -> Self {
|
||||
Self(AtomicU64::new(v.to_bits()))
|
||||
}
|
||||
|
||||
/// 原子读取 f64 值
|
||||
pub fn get(&self) -> f64 {
|
||||
f64::from_bits(self.0.load(Ordering::Relaxed))
|
||||
}
|
||||
|
||||
/// 原子写入 f64 值
|
||||
pub fn set(&self, v: f64) {
|
||||
self.0.store(v.to_bits(), Ordering::Relaxed);
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ pub fn format_f64_g(value: f64) -> String {
|
||||
let exp = abs.log10().floor() as i32;
|
||||
|
||||
// Python :g 科学计数法边界: exp < -4 或 exp >= p (=6)
|
||||
if exp < -4 || exp >= 6 {
|
||||
if !(-4..6).contains(&exp) {
|
||||
let significand = value / 10_f64.powi(exp);
|
||||
let s = format!("{:.5}", significand);
|
||||
let s = s.trim_end_matches('0').trim_end_matches('.');
|
||||
@@ -57,12 +57,12 @@ pub fn format_f64_g(value: f64) -> String {
|
||||
}
|
||||
let s = format!("{:.prec$}", value, prec = 6 - int_digits);
|
||||
let s = s.trim_end_matches('0');
|
||||
return s.trim_end_matches('.').to_string();
|
||||
s.trim_end_matches('.').to_string()
|
||||
} else {
|
||||
let leading_zeros = (-exp) as usize;
|
||||
let s = format!("{:.prec$}", value, prec = leading_zeros + 5);
|
||||
let s = s.trim_end_matches('0');
|
||||
return s.trim_end_matches('.').to_string();
|
||||
s.trim_end_matches('.').to_string()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env python3
|
||||
"""检测 Rust 源码文件头部是否有 MIT 协议,若无则自动注入。
|
||||
|
||||
用法:
|
||||
python3 check_license.py # 检测 chanlun/ 和 chanlun-py/ 下所有 .rs
|
||||
python3 check_license.py --check-only # 仅检测,不修改
|
||||
python3 check_license.py --fix # 检测并修复
|
||||
python3 check_license.py path/to/dir # 指定目录
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def find_repo_root() -> Path:
|
||||
"""从脚本位置向上查找仓库根目录(含 LICENSE 文件的目录)。"""
|
||||
current = Path(__file__).resolve().parent
|
||||
while current != current.parent:
|
||||
if (current / "LICENSE").exists():
|
||||
return current
|
||||
current = current.parent
|
||||
# Fallback: 脚本所在目录的父目录
|
||||
return Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def build_license_header(license_path: Path) -> str:
|
||||
"""读取 LICENSE 文件并格式化为 Rust 块注释头。"""
|
||||
lines = license_path.read_text(encoding="utf-8").rstrip("\n").split("\n")
|
||||
header_lines = ["/*"]
|
||||
for line in lines:
|
||||
if line.strip():
|
||||
header_lines.append(f" * {line}")
|
||||
else:
|
||||
header_lines.append(" *")
|
||||
header_lines.append(" */")
|
||||
header_lines.append("") # 末尾空行分隔
|
||||
return "\n".join(header_lines) + "\n"
|
||||
|
||||
|
||||
def has_license_header(file_path: Path) -> bool:
|
||||
"""检测文件头部是否已包含块注释风格的 MIT License。"""
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
head = f.read(512)
|
||||
except (OSError, UnicodeDecodeError):
|
||||
return True
|
||||
return head.lstrip().startswith("/*") and "MIT License" in head
|
||||
|
||||
|
||||
def strip_old_license_header(text: str) -> str:
|
||||
"""去除文件中已有的 // 风格 license header(重新注入前调用)。"""
|
||||
stripped = text.lstrip("\n")
|
||||
if stripped.startswith("// MIT License"):
|
||||
# 找到 // 注释块结束位置(第一个非 // 非空行)
|
||||
lines = stripped.split("\n")
|
||||
end_idx = 0
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith("//") or line.strip() == "":
|
||||
end_idx = i + 1
|
||||
else:
|
||||
break
|
||||
return "\n".join(lines[end_idx:])
|
||||
return text
|
||||
|
||||
|
||||
def inject_license(file_path: Path, header: str) -> bool:
|
||||
"""将 license header 注入文件头部。返回 True 表示已修改。"""
|
||||
original = file_path.read_text(encoding="utf-8")
|
||||
# 已有块注释风格则跳过
|
||||
if original.lstrip().startswith("/*") and "MIT License" in original[:512]:
|
||||
return False
|
||||
# 去除旧的 // 风格 header(如果存在)
|
||||
cleaned = strip_old_license_header(original)
|
||||
file_path.write_text(header + cleaned, encoding="utf-8")
|
||||
return True
|
||||
|
||||
|
||||
def collect_rs_files(roots: list[Path]) -> list[Path]:
|
||||
"""递归收集所有 .rs 文件,排除 target/ 等构建产物目录。"""
|
||||
exclude_dirs = {"target", ".git", "__pycache__", "dist", "build", ".venv", "venv"}
|
||||
files = []
|
||||
for root in roots:
|
||||
if not root.is_dir():
|
||||
continue
|
||||
for path in root.rglob("*.rs"):
|
||||
if any(excl in path.parts for excl in exclude_dirs):
|
||||
continue
|
||||
files.append(path)
|
||||
return sorted(files)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="检测 Rust 源码 MIT 协议头")
|
||||
parser.add_argument(
|
||||
"paths",
|
||||
nargs="*",
|
||||
help="要检测的目录(默认: chanlun 和 chanlun-py 源码目录)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--check-only",
|
||||
action="store_true",
|
||||
help="仅检测,不修改文件",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fix",
|
||||
action="store_true",
|
||||
help="检测并自动注入缺失的协议头(默认行为)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = find_repo_root()
|
||||
license_path = repo_root / "LICENSE"
|
||||
|
||||
if not license_path.exists():
|
||||
print(f"错误: 未找到 LICENSE 文件 ({license_path})", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
header = build_license_header(license_path)
|
||||
|
||||
# 确定扫描目录
|
||||
if args.paths:
|
||||
roots = [Path(p).resolve() for p in args.paths]
|
||||
else:
|
||||
roots = [
|
||||
repo_root / "chanlun" / "src",
|
||||
repo_root / "chanlun-py" / "src",
|
||||
repo_root / "chanlun" / "tests",
|
||||
repo_root / "chanlun-py" / "tests",
|
||||
]
|
||||
roots = [r for r in roots if r.is_dir()]
|
||||
|
||||
if not roots:
|
||||
print("错误: 未找到任何源码目录", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
rs_files = collect_rs_files(roots)
|
||||
|
||||
if not rs_files:
|
||||
print("未找到 .rs 文件")
|
||||
return 0
|
||||
|
||||
missing = []
|
||||
injected = []
|
||||
|
||||
for f in rs_files:
|
||||
if has_license_header(f):
|
||||
continue
|
||||
missing.append(f)
|
||||
if not args.check_only:
|
||||
if inject_license(f, header):
|
||||
injected.append(f)
|
||||
|
||||
if args.check_only:
|
||||
if missing:
|
||||
print(f"缺失 MIT 协议头: {len(missing)} 个文件")
|
||||
for f in missing:
|
||||
print(f" {f}")
|
||||
return 1
|
||||
else:
|
||||
print(f"全部 {len(rs_files)} 个 .rs 文件均已包含 MIT 协议头")
|
||||
return 0
|
||||
else:
|
||||
if injected:
|
||||
print(f"已注入 MIT 协议头: {len(injected)} 个文件")
|
||||
for f in injected:
|
||||
print(f" {f}")
|
||||
if missing:
|
||||
already = len(missing) - len(injected)
|
||||
if already > 0:
|
||||
print(f"已有协议头: {already} 个文件(无需修改)")
|
||||
total = len(rs_files) - len(missing)
|
||||
print(f"总计: {len(rs_files)} 个 .rs 文件, {total} 个已含协议头")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo "=== 1/4 清除 Python 缓存 ==="
|
||||
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null
|
||||
find . -type f -name "*.pyc" -delete 2>/dev/null
|
||||
echo " Python 缓存已清除"
|
||||
|
||||
echo "=== 2/4 清除 Cargo 编译缓存 ==="
|
||||
rm -rf chanlun/target chanlun-py/target
|
||||
echo " target/ 已清除"
|
||||
|
||||
echo "=== 3/4 构建 Release ==="
|
||||
cd chanlun-py
|
||||
maturin build --release
|
||||
echo " 构建完成"
|
||||
|
||||
echo "=== 4/4 安装 ==="
|
||||
pip install --break-system-packages --force-reinstall --no-deps \
|
||||
target/wheels/chanlun-*.whl
|
||||
echo " 安装完成"
|
||||
|
||||
echo
|
||||
echo "✓ 清理 + 构建 + 安装完毕"
|
||||
echo " pip show chanlun | grep Version"
|
||||
pip show chanlun 2>/dev/null | grep Version
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
# API 参考
|
||||
|
||||
## 枚举
|
||||
|
||||
| 类 | 说明 |
|
||||
|---|------|
|
||||
| `买卖点类型` | 一卖点、一买点、二卖点、二买点、三卖点、三买点 |
|
||||
| `相对方向` | 向上、向下、向上缺口、向下缺口、衔接向上、衔接向下 |
|
||||
| `分型结构` | 顶分型、底分型、散(无分型) |
|
||||
|
||||
## 数据
|
||||
|
||||
| 类 | 说明 |
|
||||
|---|------|
|
||||
| `K线` | 原始 OHLCV 数据,内嵌 MACD/RSI/KDJ 指标 |
|
||||
| `缠论K线` | 经包含处理后的 K 线,有方向(向上/向下)、分型结构标记 |
|
||||
| `缺口` | 前后两段区间的相对位置关系 |
|
||||
|
||||
## 结构
|
||||
|
||||
| 类 | 说明 |
|
||||
|---|------|
|
||||
| `分型` | 顶/底分型,由左中右三根缠K构成 |
|
||||
| `虚线` | 笔/线段的通用数据结构 |
|
||||
| `线段特征` | 线段特征序列,用于分析线段力度 |
|
||||
| `特征分型` | 特征分型结构 |
|
||||
|
||||
## 指标
|
||||
|
||||
| 类 | 说明 |
|
||||
|---|------|
|
||||
| `平滑异同移动平均线` | MACD — EMA 快慢线 + 信号线 + 柱状图 |
|
||||
| `相对强弱指数` | RSI — Wilder SMA 平滑,含超买超卖阈值 |
|
||||
| `随机指标` | KDJ — RSV → K → D → J,含超买超卖阈值 |
|
||||
| `指标` | 指标工具类 — `K线取值` 等静态方法 |
|
||||
|
||||
## 算法
|
||||
|
||||
| 类 | 说明 |
|
||||
|---|------|
|
||||
| `笔` | 笔划分算法(创建、分析、弱化、分析前检查) |
|
||||
| `线段` | 线段划分算法(分析、武斗、武终、分割序列) |
|
||||
| `中枢` | 中枢识别算法(延伸、扩展、第三买卖线) |
|
||||
| `背驰分析` | MACD/斜率/测度背离检测 |
|
||||
|
||||
## 业务
|
||||
|
||||
| 类 | 说明 |
|
||||
|---|------|
|
||||
| `缠论配置` | 50+ 参数集中控制所有分析阶段的行为 |
|
||||
| `观察者` | 单周期分析器,持有所有层级序列 |
|
||||
| `基础买卖点` | 买卖点数据结构 |
|
||||
| `买卖点` | 继承基础买卖点,添加工厂类方法 |
|
||||
| `K线合成器` | 将小周期 K 线合成为大周期 K 线 |
|
||||
| `立体分析器` | 多周期分析器,含 K线合成器 + 每周期观察者 |
|
||||
@@ -0,0 +1,46 @@
|
||||
# Sphinx 配置 — chanlun 缠论技术分析库
|
||||
#
|
||||
# 使用 MyST 解析器支持 Markdown,Furo 主题
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 确保文档构建时能 import chanlun
|
||||
sys.path.insert(0, os.path.abspath(".."))
|
||||
|
||||
# ---- 项目信息 ----
|
||||
project = "chanlun"
|
||||
copyright = "2026, YuWuKunCheng"
|
||||
author = "YuWuKunCheng"
|
||||
release = "26.5.103"
|
||||
language = "zh_CN"
|
||||
|
||||
# ---- 扩展 ----
|
||||
extensions = [
|
||||
"myst_parser", # Markdown 支持
|
||||
"sphinx.ext.viewcode", # 添加源码链接
|
||||
"sphinx.ext.intersphinx", # 跨项目文档链接
|
||||
]
|
||||
|
||||
# ---- MyST 配置 ----
|
||||
myst_enable_extensions = [
|
||||
"colon_fence", # ::: 围栏代码块
|
||||
"fieldlist", # 字段列表
|
||||
]
|
||||
myst_heading_anchors = 3
|
||||
|
||||
# ---- 主题 ----
|
||||
html_theme = "furo"
|
||||
html_title = "chanlun — 缠论技术分析库"
|
||||
html_theme_options = {
|
||||
"source_repository": "https://github.com/yuwukuncheng/chanlun.rs",
|
||||
"source_branch": "main",
|
||||
"source_directory": "docs/",
|
||||
}
|
||||
|
||||
# ---- 跨项目链接 ----
|
||||
intersphinx_mapping = {
|
||||
"python": ("https://docs.python.org/3", None),
|
||||
}
|
||||
|
||||
add_module_names = False
|
||||
@@ -0,0 +1,45 @@
|
||||
# chanlun — 缠论技术分析库
|
||||
|
||||
基于 Rust + PyO3 的高性能缠论技术分析 Python 绑定,API 参考 `chan.py` 设计,高度兼容。
|
||||
|
||||
## 特性
|
||||
|
||||
- **高性能**:核心算法用 Rust 实现,通过 PyO3 暴露为 Python 原生模块
|
||||
- **完全兼容**:类名、方法名、字段名与 `chan.py` 保持一致
|
||||
- **流式计算**:逐 K 线增量更新,无需重复计算
|
||||
- **多周期分析**:内置 K 线合成器和立体分析器,支持跨周期联动
|
||||
|
||||
## 安装
|
||||
|
||||
```bash
|
||||
pip install chanlun
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
```python
|
||||
import chanlun
|
||||
|
||||
config = chanlun.缠论配置()
|
||||
obs = chanlun.观察者.读取数据文件("BTCUSD-300.nb", config)
|
||||
|
||||
print(f"K线: {len(obs.普通K线序列)}")
|
||||
print(f"笔: {len(obs.笔序列)}")
|
||||
print(f"线段: {len(obs.线段序列)}")
|
||||
print(f"中枢: {len(obs.中枢序列)}")
|
||||
```
|
||||
|
||||
```{toctree}
|
||||
:hidden:
|
||||
:caption: 文档导航
|
||||
|
||||
api
|
||||
```
|
||||
|
||||
```{toctree}
|
||||
:hidden:
|
||||
:caption: 外部链接
|
||||
|
||||
GitHub <https://github.com/yuwukuncheng/chanlun.rs>
|
||||
PyPI <https://pypi.org/project/chanlun/>
|
||||
```
|
||||
@@ -0,0 +1,3 @@
|
||||
sphinx>=8.0
|
||||
myst-parser>=4.0
|
||||
furo>=2024.0
|
||||
@@ -46,10 +46,7 @@ from jinja2 import Environment, FileSystemLoader
|
||||
from pydantic import BaseModel
|
||||
import backtrader as bt
|
||||
|
||||
try:
|
||||
from chanlun import *
|
||||
except ImportError:
|
||||
from chan import *
|
||||
from chanlun import *
|
||||
|
||||
from strategies import *
|
||||
|
||||
@@ -61,6 +58,11 @@ def Nil(*args, **kwargs):
|
||||
def 获取模块版本():
|
||||
versions = {}
|
||||
|
||||
# 1.
|
||||
try:
|
||||
versions["chanlun"] = importlib.metadata.version("chanlun")
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
pass
|
||||
# 2.
|
||||
try:
|
||||
versions["fastapi"] = importlib.metadata.version("fastapi")
|
||||
@@ -321,471 +323,6 @@ class 时间周期:
|
||||
return 数值 * 60
|
||||
|
||||
|
||||
class BSP点:
|
||||
"""缠论买卖点(独立于旧买卖点类,对标 CBS_Point)。支持同一笔上叠加多种类型。"""
|
||||
|
||||
__slots__ = ("笔", "K线单元", "是否买点", "类型列表", "关联T1", "特征字典")
|
||||
|
||||
def __init__(self, 笔: "虚线", 是否买点: bool, 类型: "买卖点类型", 关联T1: "Optional[BSP点]" = None, 特征字典: dict = None):
|
||||
self.笔 = 笔
|
||||
self.K线单元 = 笔.武.中
|
||||
self.是否买点 = 是否买点
|
||||
self.类型列表: List["买卖点类型"] = [类型]
|
||||
self.关联T1: "Optional[BSP点]" = 关联T1
|
||||
self.特征字典 = 特征字典 or {}
|
||||
|
||||
def 添加类型(self, 类型: "买卖点类型", 关联T1: "Optional[BSP点]" = None):
|
||||
if 类型 not in self.类型列表:
|
||||
self.类型列表.append(类型)
|
||||
if self.关联T1 is None:
|
||||
self.关联T1 = 关联T1
|
||||
elif 关联T1 is not None:
|
||||
assert self.关联T1.K线单元.序号 == 关联T1.K线单元.序号
|
||||
|
||||
@property
|
||||
def 类型字符串(self) -> str:
|
||||
return ",".join([t.value for t in self.类型列表])
|
||||
|
||||
@property
|
||||
def 备注(self) -> str:
|
||||
return self.类型字符串
|
||||
|
||||
|
||||
class 买卖点识别器:
|
||||
"""缠论六类买卖点识别引擎。对标 ./对标/chan.py BuySellPoint 逻辑。"""
|
||||
|
||||
_最后确认位置: Dict[str, int] = {} # 对标 last_sure_pos
|
||||
_BSP1字典: Dict[str, Dict[int, "BSP点"]] = {} # 对标 bsp1_dict: key → 笔序号 → BSP点
|
||||
|
||||
# ================================================================
|
||||
# 主入口
|
||||
# ================================================================
|
||||
|
||||
@staticmethod
|
||||
def 计算(obs: "观察者") -> None:
|
||||
if not obs.线段序列 or not obs.普通K线序列:
|
||||
return
|
||||
|
||||
key = obs.标识
|
||||
最后确认 = 买卖点识别器._最后确认位置.get(key, -1)
|
||||
|
||||
# 新确认的线段(仅用于 T1 增量识别,避免重复创建 T1)
|
||||
新确认线段 = [seg for i, seg in enumerate(obs.线段序列) if i > 最后确认 and seg.特征序列[2] is not None]
|
||||
|
||||
if not 新确认线段:
|
||||
return
|
||||
|
||||
买卖点识别器._最后确认位置[key] = obs.线段序列.index(新确认线段[-1])
|
||||
|
||||
配置 = obs.配置
|
||||
|
||||
if 配置.买卖点_计算线段BSP1:
|
||||
买卖点识别器._计算线段BSP1(obs, 新确认线段)
|
||||
|
||||
# T2 / T3 需要全部已确认线段(含历史),以便在后续笔到达后重新检查
|
||||
全部已确认 = [seg for seg in obs.线段序列 if seg.特征序列[2] is not None]
|
||||
全量T1列表 = list(买卖点识别器._BSP1字典.get(key, {}).values()) if 配置.买卖点_依赖T1 else []
|
||||
|
||||
if 配置.买卖点_处理BSP2:
|
||||
买卖点识别器._处理BSP2(obs, 全量T1列表, 全部已确认)
|
||||
|
||||
if 配置.买卖点_计算线段BSP3:
|
||||
买卖点识别器._计算线段BSP3(obs, 全量T1列表, 全部已确认)
|
||||
|
||||
# ================================================================
|
||||
# MACD 指标计算
|
||||
# ================================================================
|
||||
|
||||
@staticmethod
|
||||
def _计算MACD指标(笔: "虚线", K线序列: "List[K线]", 方式: str) -> float:
|
||||
始K = 笔.文.中.标的K线
|
||||
终K = 笔.武.中.标的K线
|
||||
try:
|
||||
始索引 = K线序列.index(始K)
|
||||
终索引 = K线序列.index(终K)
|
||||
except ValueError:
|
||||
return float("inf")
|
||||
|
||||
if 方式 == "峰":
|
||||
峰值 = 0.0
|
||||
for i in range(始索引, 终索引 + 1):
|
||||
k = K线序列[i]
|
||||
if k.macd and k.macd.MACD柱 is not None:
|
||||
v = abs(k.macd.MACD柱)
|
||||
if v > 峰值:
|
||||
峰值 = v
|
||||
return 峰值
|
||||
else:
|
||||
macd_dict = K线.获取MACD(K线序列, 始K, 终K)
|
||||
return abs(macd_dict.get("总", 0.0))
|
||||
|
||||
# ================================================================
|
||||
# 中枢查找工具
|
||||
# ================================================================
|
||||
|
||||
@staticmethod
|
||||
def _取线段内部中枢序列(段: "虚线", 来源: str) -> "List[中枢]":
|
||||
if 来源 == "实":
|
||||
return 段.实_中枢序列
|
||||
elif 来源 == "虚":
|
||||
return 段.虚_中枢序列
|
||||
else:
|
||||
return 段.合_中枢序列
|
||||
|
||||
@staticmethod
|
||||
def _找多笔中枢(中枢序列: "List[中枢]") -> "Optional[中枢]":
|
||||
for zs in reversed(中枢序列):
|
||||
if len(zs) >= 3:
|
||||
return zs
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _全局索引(笔: "虚线", 笔序列: "List[虚线]") -> int:
|
||||
"""返回笔在全局笔序列中的索引。"""
|
||||
try:
|
||||
return 笔序列.index(笔)
|
||||
except ValueError:
|
||||
return -1
|
||||
|
||||
@staticmethod
|
||||
def _笔振幅(笔: "虚线") -> float:
|
||||
"""笔的振幅 = |武分型特征值 - 文分型特征值|。对标 bi.amp()。"""
|
||||
return abs(笔.武.分型特征值 - 笔.文.分型特征值)
|
||||
|
||||
@staticmethod
|
||||
def _有重叠(低1: float, 高1: float, 低2: float, 高2: float) -> bool:
|
||||
"""两区间是否有交集。对标 has_overlap。"""
|
||||
return not (高1 < 低2 or 高2 < 低1)
|
||||
|
||||
@staticmethod
|
||||
def _段末同向笔(段: "虚线") -> "虚线":
|
||||
"""对标 seg.end_bi:当前段内最后一个与段方向相同的笔。
|
||||
|
||||
段.基础序列包含两段笔(当前段 + 后一段开头),不能直接用 [-1]/[-2]。
|
||||
必须用 分割序列 获取「前」——仅当前段的笔。
|
||||
"""
|
||||
前, _, _, _ = 线段.分割序列(段)
|
||||
for 筆 in reversed(前):
|
||||
if 筆.方向 == 段.方向:
|
||||
return 筆
|
||||
return 前[-1] # 理论上不会到这里,前[-1] 总是同向
|
||||
|
||||
# ================================================================
|
||||
# BSP 创建 / 去重 — 对标 add_bs
|
||||
# ================================================================
|
||||
|
||||
@staticmethod
|
||||
def _创建BSP(obs: "观察者", 类型: "买卖点类型", 笔: "虚线", 关联T1: "Optional[BSP点]" = None, 特征字典: dict = None):
|
||||
"""对标 add_bs:创建或追加 BSP 类型(支持一笔多类型叠加)。"""
|
||||
is_buy = 笔.方向.是否向下()
|
||||
if 笔.序号 in obs.BSP字典:
|
||||
exist_bsp = obs.BSP字典[笔.序号]
|
||||
assert exist_bsp.是否买点 == is_buy, f"买卖方向冲突: {exist_bsp.类型字符串} vs {类型.value}"
|
||||
exist_bsp.添加类型(类型, 关联T1)
|
||||
return exist_bsp
|
||||
|
||||
bsp = BSP点(笔=笔, 是否买点=is_buy, 类型=类型, 关联T1=关联T1, 特征字典=特征字典)
|
||||
obs.BSP字典[笔.序号] = bsp
|
||||
return bsp
|
||||
|
||||
# ================================================================
|
||||
# T1 / T1P — 对标 cal_seg_bs1point → treat_bsp1 / treat_pz_bsp1
|
||||
# ================================================================
|
||||
|
||||
@staticmethod
|
||||
def _计算线段BSP1(obs: "观察者", 已确认线段: "List[虚线]") -> "List[BSP点]":
|
||||
"""一类买卖点(T1中枢突破背离)和一类盘整买卖点(T1P)。对标 cal_seg_bs1point。"""
|
||||
结果: "List[BSP点]" = []
|
||||
配置 = obs.配置
|
||||
来源 = 配置.买卖点_中枢来源
|
||||
笔序列 = obs.笔序列
|
||||
|
||||
for 段 in 已确认线段:
|
||||
if not 段.基础序列 or len(段.基础序列) < 3:
|
||||
continue
|
||||
|
||||
is_buy = 段.方向.是否向下()
|
||||
if not is_buy and not 段.方向.是否向上():
|
||||
continue
|
||||
|
||||
段末笔 = 买卖点识别器._段末同向笔(段)
|
||||
|
||||
中枢序列 = 买卖点识别器._取线段内部中枢序列(段, 来源)
|
||||
|
||||
# 确定触发条件 — 对标 cal_single_bs1point
|
||||
goto_T1 = False
|
||||
相关中枢 = None
|
||||
if 中枢序列:
|
||||
相关中枢 = 中枢序列[-1] # zs_lst[-1]
|
||||
if len(相关中枢) >= 3: # not is_one_bi_zs
|
||||
进入笔全局索引 = 买卖点识别器._全局索引(相关中枢[0], 笔序列)
|
||||
if 进入笔全局索引 > 0:
|
||||
进入笔 = 笔序列[进入笔全局索引 - 1]
|
||||
段末笔全局索引 = 买卖点识别器._全局索引(段末笔, 笔序列)
|
||||
中枢末笔 = 相关中枢[-1]
|
||||
中枢末笔全局索引 = 买卖点识别器._全局索引(中枢末笔, 笔序列)
|
||||
bi_out = 笔序列[中枢末笔全局索引 + 1] if 中枢末笔全局索引 >= 0 and 中枢末笔全局索引 + 1 < len(笔序列) else None
|
||||
中枢到达段末 = (bi_out is not None and bi_out.序号 >= 段末笔.序号) or 中枢末笔.序号 >= 段末笔.序号
|
||||
if 中枢到达段末 and 段末笔.序号 - 进入笔.序号 > 2:
|
||||
goto_T1 = True
|
||||
|
||||
if goto_T1 and 相关中枢 is not None:
|
||||
# ---- T1 路径(对标 treat_bsp1)----
|
||||
进入笔全局索引 = 买卖点识别器._全局索引(相关中枢[0], 笔序列)
|
||||
进入笔 = 笔序列[进入笔全局索引 - 1]
|
||||
|
||||
# 突破检查 — 对标 end_bi_break
|
||||
if is_buy:
|
||||
if not (段末笔.低 < 相关中枢.低):
|
||||
continue
|
||||
else:
|
||||
if not (段末笔.高 > 相关中枢.高):
|
||||
continue
|
||||
|
||||
# 可选峰值条件 — 对标 out_bi_is_peak
|
||||
if 配置.买卖点_峰值条件:
|
||||
if is_buy:
|
||||
if not all(笔.低 >= 段末笔.低 for 笔 in 相关中枢 if 笔.序号 <= 段末笔.序号):
|
||||
continue
|
||||
else:
|
||||
if not all(笔.高 <= 段末笔.高 for 笔 in 相关中枢 if 笔.序号 <= 段末笔.序号):
|
||||
continue
|
||||
|
||||
# MACD背离 — 对标 is_divergence
|
||||
进入指标 = 买卖点识别器._计算MACD指标(进入笔, obs.普通K线序列, 配置.买卖点_计算方式)
|
||||
离开指标 = 买卖点识别器._计算MACD指标(段末笔, obs.普通K线序列, 配置.买卖点_计算方式)
|
||||
if not (离开指标 <= 配置.买卖点_背离率 * 进入指标):
|
||||
continue
|
||||
|
||||
类型 = 买卖点类型.T1买 if is_buy else 买卖点类型.T1卖
|
||||
bsp = 买卖点识别器._创建BSP(obs, 类型=类型, 笔=段末笔, 特征字典={"divergence_rate": 离开指标 / (进入指标 + 1e-7)})
|
||||
结果.append(bsp)
|
||||
买卖点识别器._BSP1字典.setdefault(obs.标识, {})[段末笔.序号] = bsp
|
||||
|
||||
else:
|
||||
# ---- T1P 盘整背离路径(对标 treat_pz_bsp1)----
|
||||
离开笔 = 段末笔
|
||||
段末笔全局索引 = 买卖点识别器._全局索引(段末笔, 笔序列)
|
||||
if 段末笔全局索引 < 2:
|
||||
continue
|
||||
进入笔 = 笔序列[段末笔全局索引 - 2]
|
||||
|
||||
if 离开笔.方向 != 段.方向:
|
||||
continue
|
||||
|
||||
# 创新低/高检查 — 对标 treat_pz_bsp1
|
||||
if is_buy:
|
||||
if 离开笔.低 > 进入笔.低:
|
||||
continue
|
||||
else:
|
||||
if 离开笔.高 < 进入笔.高:
|
||||
continue
|
||||
|
||||
# MACD背离 — 对标 in_metric vs out_metric
|
||||
进入指标 = 买卖点识别器._计算MACD指标(进入笔, obs.普通K线序列, 配置.买卖点_计算方式)
|
||||
离开指标 = 买卖点识别器._计算MACD指标(离开笔, obs.普通K线序列, 配置.买卖点_计算方式)
|
||||
if not (离开指标 <= 配置.买卖点_背离率 * 进入指标):
|
||||
continue
|
||||
|
||||
类型 = 买卖点类型.T1P买 if is_buy else 买卖点类型.T1P卖
|
||||
bsp = 买卖点识别器._创建BSP(obs, 类型=类型, 笔=离开笔, 特征字典={"divergence_rate": 离开指标 / (进入指标 + 1e-7)})
|
||||
结果.append(bsp)
|
||||
买卖点识别器._BSP1字典.setdefault(obs.标识, {})[段末笔.序号] = bsp
|
||||
|
||||
return 结果
|
||||
|
||||
# ================================================================
|
||||
# T2 / T2S — 对标 treat_bsp2 / treat_bsp2s
|
||||
# ================================================================
|
||||
|
||||
@staticmethod
|
||||
def _处理BSP2(obs: "观察者", T1列表: "List[BSP点]", 已确认线段: "List[虚线]") -> "List[BSP点]":
|
||||
"""二类买卖点(T2回调确认)和二类特殊买卖点(T2S多级次二类)。"""
|
||||
结果: "List[BSP点]" = []
|
||||
配置 = obs.配置
|
||||
阈值 = 配置.买卖点_T2_回调阈值
|
||||
最大层级 = 配置.买卖点_T2S_最大层级
|
||||
笔序列 = obs.笔序列
|
||||
|
||||
for bsp1 in T1列表:
|
||||
is_buy = bsp1.是否买点
|
||||
bsp1_笔 = bsp1.笔
|
||||
|
||||
bsp1_笔全局索引 = 买卖点识别器._全局索引(bsp1_笔, 笔序列)
|
||||
if bsp1_笔全局索引 < 0:
|
||||
continue
|
||||
|
||||
# 对标:break_bi = bi_list[bsp1_bi.idx + 1]; bsp2_bi = bi_list[bsp1_bi.idx + 2]
|
||||
if bsp1_笔全局索引 + 2 >= len(笔序列):
|
||||
continue
|
||||
突破笔 = 笔序列[bsp1_笔全局索引 + 1]
|
||||
回调笔 = 笔序列[bsp1_笔全局索引 + 2]
|
||||
|
||||
# 对标:bsp2_bi.amp() / break_bi.amp() <= max_bs2_rate
|
||||
突破振幅 = 买卖点识别器._笔振幅(突破笔)
|
||||
if 突破振幅 == 0:
|
||||
continue
|
||||
回调率 = 买卖点识别器._笔振幅(回调笔) / 突破振幅
|
||||
|
||||
if 回调率 <= 阈值:
|
||||
# ---- T2 成立 ----
|
||||
类型 = 买卖点类型.T2买 if is_buy else 买卖点类型.T2卖
|
||||
bsp = 买卖点识别器._创建BSP(obs, 类型=类型, 笔=回调笔, 关联T1=bsp1)
|
||||
结果.append(bsp)
|
||||
continue
|
||||
|
||||
# ---- T2S 多级次二类(对标 treat_bsp2s)----
|
||||
if 最大层级 is None or 最大层级 <= 0:
|
||||
continue
|
||||
|
||||
# 对标:初始重叠检查
|
||||
if not 买卖点识别器._有重叠(回调笔.低, 回调笔.高, 突破笔.低, 突破笔.高):
|
||||
continue
|
||||
|
||||
重叠区低 = max(回调笔.低, 突破笔.低) if is_buy else 回调笔.低
|
||||
重叠区高 = min(回调笔.高, 突破笔.高) if not is_buy else 回调笔.高
|
||||
|
||||
bias = 2 # 对标:从 bsp2_bi.idx + 2 开始,步进 2
|
||||
while 回调笔.序号 + bias < len(笔序列):
|
||||
bsp2s_笔 = 笔序列[买卖点识别器._全局索引(回调笔, 笔序列) + bias]
|
||||
|
||||
if 最大层级 is not None and bias // 2 > 最大层级:
|
||||
break
|
||||
|
||||
# 对标:首次重叠建立 _low/_high,之后检查
|
||||
if bias == 2:
|
||||
if not 买卖点识别器._有重叠(回调笔.低, 回调笔.高, bsp2s_笔.低, bsp2s_笔.高):
|
||||
break
|
||||
重叠区低 = max(回调笔.低, bsp2s_笔.低)
|
||||
重叠区高 = min(回调笔.高, bsp2s_笔.高)
|
||||
else:
|
||||
if not 买卖点识别器._有重叠(重叠区低, 重叠区高, bsp2s_笔.低, bsp2s_笔.高):
|
||||
break
|
||||
|
||||
# 对标:bsp2s_break_bsp1 = 不能突破突破笔极值
|
||||
if is_buy and bsp2s_笔.低 < 突破笔.低:
|
||||
break
|
||||
if not is_buy and bsp2s_笔.高 > 突破笔.高:
|
||||
break
|
||||
|
||||
# 对标:回调率检查
|
||||
bsp2s_回调率 = abs(bsp2s_笔.武.分型特征值 - 突破笔.武.分型特征值) / 突破振幅
|
||||
if bsp2s_回调率 > 阈值:
|
||||
break
|
||||
|
||||
# T2S成立
|
||||
重叠区低 = max(重叠区低, bsp2s_笔.低)
|
||||
重叠区高 = min(重叠区高, bsp2s_笔.高)
|
||||
|
||||
类型 = 买卖点类型.T2S买 if is_buy else 买卖点类型.T2S卖
|
||||
bsp = 买卖点识别器._创建BSP(obs, 类型=类型, 笔=bsp2s_笔, 关联T1=bsp1)
|
||||
结果.append(bsp)
|
||||
|
||||
bias += 2 # 对标:每次+2(同方向笔)
|
||||
|
||||
return 结果
|
||||
|
||||
# ================================================================
|
||||
# T3A / T3B — 对标 cal_seg_bs3point → treat_bsp3_after / treat_bsp3_before
|
||||
# ================================================================
|
||||
|
||||
@staticmethod
|
||||
def _计算线段BSP3(obs: "观察者", T1列表: "List[BSP点]", 已确认线段: "List[虚线]") -> None:
|
||||
"""三类买卖点:T3A(后方回踩)和T3B(前方反抽)。"""
|
||||
配置 = obs.配置
|
||||
来源 = 配置.买卖点_中枢来源
|
||||
笔序列 = obs.笔序列
|
||||
|
||||
for 段 in 已确认线段:
|
||||
seg_idx = obs.线段序列.index(段) if 段 in obs.线段序列 else -1
|
||||
is_buy = 段.方向.是否向下()
|
||||
|
||||
# ================================================================
|
||||
# T3A:后段中枢回踩 — 对标 treat_bsp3_after
|
||||
# ================================================================
|
||||
if seg_idx >= 0 and seg_idx + 1 < len(obs.线段序列):
|
||||
# 对标:if BSP_CONF.bsp3_follow_1 and bsp1_bi.idx not in bsp_store_flat_dict → skip
|
||||
bsp1_笔 = 买卖点识别器._段末同向笔(段)
|
||||
if not 配置.买卖点_依赖T1 or bsp1_笔.序号 in 买卖点识别器._BSP1字典.get(obs.标识, {}):
|
||||
后段 = obs.线段序列[seg_idx + 1]
|
||||
后段内部中枢 = 买卖点识别器._取线段内部中枢序列(后段, 来源)
|
||||
|
||||
# 对标:first_zs = next_seg.get_first_multi_bi_zs()
|
||||
for 中枢_candidate in 后段内部中枢:
|
||||
if len(中枢_candidate) < 3:
|
||||
continue
|
||||
中枢末笔全局索引 = 买卖点识别器._全局索引(中枢_candidate[-1], 笔序列)
|
||||
bi_out = 笔序列[中枢末笔全局索引 + 1] if 中枢末笔全局索引 >= 0 and 中枢末笔全局索引 + 1 < len(笔序列) else None
|
||||
if bi_out is None or 买卖点识别器._全局索引(bi_out, 笔序列) + 1 >= len(笔序列):
|
||||
continue
|
||||
|
||||
bsp3_笔 = 笔序列[买卖点识别器._全局索引(bi_out, 笔序列) + 1]
|
||||
|
||||
# 对标:bsp3_bi.dir == next_seg.dir → break
|
||||
if bsp3_笔.方向 == 后段.方向:
|
||||
break
|
||||
|
||||
# 对标:bsp3_back2zs(bsp3_bi, zs)
|
||||
if is_buy:
|
||||
if bsp3_笔.低 < 中枢_candidate.高:
|
||||
continue
|
||||
else:
|
||||
if bsp3_笔.高 > 中枢_candidate.低:
|
||||
continue
|
||||
|
||||
# 对标:bs3_peak 检查
|
||||
if 配置.买卖点_峰值条件:
|
||||
if is_buy:
|
||||
if not (bsp3_笔.高 >= 中枢_candidate.高高):
|
||||
continue
|
||||
else:
|
||||
if not (bsp3_笔.低 <= 中枢_candidate.低低):
|
||||
continue
|
||||
|
||||
类型 = 买卖点类型.T3A买 if is_buy else 买卖点类型.T3A卖
|
||||
bsp1 = 买卖点识别器._BSP1字典.get(obs.标识, {}).get(bsp1_笔.序号)
|
||||
买卖点识别器._创建BSP(obs, 类型=类型, 笔=bsp3_笔, 关联T1=bsp1)
|
||||
break # 对标:找到一个就跳出
|
||||
|
||||
# ================================================================
|
||||
# T3B:前序买卖点位置向前搜索 — 对标 treat_bsp3_before
|
||||
# ================================================================
|
||||
段末笔 = 买卖点识别器._段末同向笔(段)
|
||||
段内部中枢 = 买卖点识别器._取线段内部中枢序列(段, 来源)
|
||||
cmp_中枢 = 买卖点识别器._找多笔中枢(段内部中枢)
|
||||
if cmp_中枢 is None:
|
||||
continue
|
||||
|
||||
# 对标:if BSP_CONF.bsp3_follow_1 and bsp1_bi.idx not in bsp_store_flat_dict → skip
|
||||
if 配置.买卖点_依赖T1 and 段末笔.序号 not in 买卖点识别器._BSP1字典.get(obs.标识, {}):
|
||||
continue
|
||||
|
||||
bsp1_全局索引 = 买卖点识别器._全局索引(段末笔, 笔序列)
|
||||
if bsp1_全局索引 < 0:
|
||||
continue
|
||||
|
||||
# 对标:bsp3_peak check on cmp_zs
|
||||
# 对标:从 bsp1_bi.idx+2 开始,步进 2
|
||||
for offset in range(2, len(笔序列) - bsp1_全局索引, 2):
|
||||
bsp3_笔全局索引 = bsp1_全局索引 + offset
|
||||
if bsp3_笔全局索引 >= len(笔序列):
|
||||
break
|
||||
bsp3_笔 = 笔序列[bsp3_笔全局索引]
|
||||
|
||||
# 对标:bsp3_back2zs(bsp3_bi, cmp_zs)
|
||||
if is_buy:
|
||||
if bsp3_笔.低 < cmp_中枢.高:
|
||||
continue
|
||||
else:
|
||||
if bsp3_笔.高 > cmp_中枢.低:
|
||||
continue
|
||||
|
||||
类型 = 买卖点类型.T3B买 if is_buy else 买卖点类型.T3B卖
|
||||
bsp1 = 买卖点识别器._BSP1字典.get(obs.标识, {}).get(段末笔.序号)
|
||||
买卖点识别器._创建BSP(obs, 类型=类型, 笔=bsp3_笔, 关联T1=bsp1)
|
||||
break # 对标:找到一个就跳出
|
||||
|
||||
|
||||
class 指令:
|
||||
增: Final[str] = "APPEND"
|
||||
改: Final[str] = "MODIFY"
|
||||
@@ -823,19 +360,18 @@ class 观察者(观察者):
|
||||
self.数据队列: queue.Queue = 数据队列
|
||||
super().__init__(符号, 周期, 配置)
|
||||
self.__终止时间戳: Optional[datetime] = 转化为时间戳(self.配置.手动终止) if self.配置.手动终止 else None
|
||||
|
||||
def 识别买卖点(self):
|
||||
pass
|
||||
self.买卖点字典 = dict()
|
||||
|
||||
@final
|
||||
def 增加原始K线(self, 普K: K线):
|
||||
if self.__终止时间戳 and 普K.时间戳 > self.__终止时间戳:
|
||||
return
|
||||
|
||||
super().增加原始K线(普K)
|
||||
if self.配置.推送K线:
|
||||
self.报信(普K, 指令.添加("RawBar"), sys._getframe().f_lineno, 周期=普K.周期)
|
||||
|
||||
super().增加原始K线(普K)
|
||||
|
||||
try:
|
||||
self.数据队列 and self.数据队列.put((普K.时间戳, 普K.开盘价, 普K.高, 普K.低, 普K.收盘价, 普K.成交量, 0))
|
||||
if self.数据通道 is not None and self.配置.图表展示:
|
||||
@@ -843,8 +379,6 @@ class 观察者(观察者):
|
||||
try:
|
||||
self.图表刷新()
|
||||
self.识别买卖点()
|
||||
# 买卖点识别器.计算(self)
|
||||
# self.标注买卖点()
|
||||
except:
|
||||
print("~~~~~~~~~~~~~~", self.当前K线)
|
||||
traceback.print_exc()
|
||||
@@ -867,26 +401,12 @@ class 观察者(观察者):
|
||||
|
||||
def 重置基础序列(self):
|
||||
self.买卖点字典 = dict()
|
||||
self.BSP字典: Dict[int, "BSP点"] = dict()
|
||||
self._买卖点最后确认线段索引: int = -1
|
||||
买卖点识别器._最后确认位置.pop(self.标识, None)
|
||||
买卖点识别器._BSP1字典.pop(self.标识, None)
|
||||
self._已标注BSP序号: set = set()
|
||||
super().重置基础序列()
|
||||
|
||||
def 读取任意数据(self, 魔法, **魔法参数):
|
||||
魔法(**魔法参数)
|
||||
return self
|
||||
|
||||
def 加载本地数据(self, 文件路径: str):
|
||||
self.重置基础序列()
|
||||
with open(文件路径, "rb") as f:
|
||||
buffer = f.read()
|
||||
size = struct.calcsize(">6d")
|
||||
for i in range(len(buffer) // size):
|
||||
k线 = K线.读取大端字节数组(buffer[i * size : i * size + size], self.周期)
|
||||
self.增加原始K线(k线)
|
||||
|
||||
def 静态重新分析(self):
|
||||
self.买卖点字典 = dict()
|
||||
super().静态重新分析()
|
||||
@@ -932,50 +452,25 @@ class 观察者(观察者):
|
||||
if 当前买卖点.买卖点K线.时间戳 not in 活跃时间戳序列:
|
||||
买卖点序列.add(当前买卖点)
|
||||
当前买卖点.买卖点K线.买卖点信息.add(当前买卖点.备注)
|
||||
self.报信(当前买卖点, 指令.添加(当前买卖点.备注), sys._getframe().f_lineno)
|
||||
|
||||
def 图表刷新(self):
|
||||
# ===================== 笔(你原来的写法) =====================
|
||||
for 筆 in self.笔序列[-3:]:
|
||||
self.报信(筆, 指令.添加(筆.标识), 0)
|
||||
def 报信(序列):
|
||||
for 对象 in 序列[-3:]:
|
||||
self.报信(对象, 指令.添加(对象.标识), 0)
|
||||
|
||||
# ===================== 笔中枢 =====================
|
||||
for 中枢对象 in self.笔_中枢序列[-3:]:
|
||||
self.报信(中枢对象, 指令.添加(中枢对象.标识), 0)
|
||||
报信(self.笔序列)
|
||||
报信(self.笔_中枢序列)
|
||||
|
||||
# ===================== 线段、中枢 =====================
|
||||
for 线段对象 in self.线段序列[-3:]:
|
||||
self.报信(线段对象, 指令.添加(线段对象.标识), 0)
|
||||
|
||||
for 中枢对象 in self.中枢序列[-3:]:
|
||||
self.报信(中枢对象, 指令.添加(中枢对象.标识), 0)
|
||||
|
||||
# ===================== 扩展线段、扩展中枢 =====================
|
||||
for 线段对象 in self.扩展线段序列[-3:]:
|
||||
self.报信(线段对象, 指令.添加(线段对象.标识), 0)
|
||||
|
||||
for 中枢对象 in self.扩展中枢序列[-3:]:
|
||||
self.报信(中枢对象, 指令.添加(中枢对象.标识), 0)
|
||||
|
||||
# ===================== 扩展线段(线段级)、扩展中枢(线段级) =====================
|
||||
for 线段对象 in self.扩展线段序列_线段[-3:]:
|
||||
self.报信(线段对象, 指令.添加(线段对象.标识), 0)
|
||||
|
||||
for 中枢对象 in self.扩展中枢序列_线段[-3:]:
|
||||
self.报信(中枢对象, 指令.添加(中枢对象.标识), 0)
|
||||
|
||||
# ===================== 线段线段、线段中枢 =====================
|
||||
for 线段对象 in self.线段_线段序列[-3:]:
|
||||
self.报信(线段对象, 指令.添加(线段对象.标识), 0)
|
||||
|
||||
for 中枢对象 in self.线段_中枢序列[-3:]:
|
||||
self.报信(中枢对象, 指令.添加(中枢对象.标识), 0)
|
||||
|
||||
# ===================== 扩展线段(扩展级)、扩展中枢(扩展级) =====================
|
||||
for 线段对象 in self.扩展线段序列_扩展线段[-3:]:
|
||||
self.报信(线段对象, 指令.添加(线段对象.标识), 0)
|
||||
|
||||
for 中枢对象 in self.扩展中枢序列_扩展线段[-3:]:
|
||||
self.报信(中枢对象, 指令.添加(中枢对象.标识), 0)
|
||||
for i in range(self.线段分析层次):
|
||||
报信(self.线段序列组[i])
|
||||
报信(self.中枢序列组[i])
|
||||
for i in range(self.扩展线段分析层次):
|
||||
报信(self.扩展线段序列组[i])
|
||||
报信(self.扩展中枢序列组[i])
|
||||
for i in range(self.混合扩展线段分析层次):
|
||||
报信(self.混合扩展线段序列组[i])
|
||||
报信(self.混合扩展中枢序列组[i])
|
||||
|
||||
# self.将图表数据固化到本地()
|
||||
|
||||
@@ -1022,23 +517,6 @@ class 观察者(观察者):
|
||||
"showLabel": False if 对象.偏移 <= 1 else True,
|
||||
}
|
||||
|
||||
if type(对象) is BSP点:
|
||||
message["type"] = "shape"
|
||||
message["cmd"] = 命令.指令.upper()
|
||||
message["id"] = f"BSP_{id(对象)}"
|
||||
message["name"] = "arrow_down" if not 对象.是否买点 else "arrow_up"
|
||||
k线 = 对象.K线单元
|
||||
message["points"] = [{"time": int(k线.时间戳), "price": k线.分型特征值}]
|
||||
arrowColor = "#FF2800" if not 对象.是否买点 else "#00FF22"
|
||||
text = f"{对象.类型字符串}, {对象.特征字典.get('divergence_rate', '')}"
|
||||
message["overrides"] = {
|
||||
"color": "#FFA500",
|
||||
"arrowColor": "#FFA500",
|
||||
"text": text,
|
||||
"title": 对象.类型字符串,
|
||||
"showLabel": True,
|
||||
}
|
||||
|
||||
if type(对象) is 虚线 and 对象.标识 == "笔" and not self.配置.推送笔:
|
||||
return
|
||||
|
||||
@@ -1084,8 +562,8 @@ class 观察者(观察者):
|
||||
message["name"] = "trend_line" if type(对象) is not 中枢 else "rectangle"
|
||||
if 命令.指令 != 指令.删:
|
||||
message["points"] = [
|
||||
{"time": int(缠论K线.时间戳对齐(self.缠论K线序列, 对象.文.中)), "price": 对象.文.分型特征值 if type(对象) is not 中枢 else 对象.高},
|
||||
{"time": int(缠论K线.时间戳对齐(self.缠论K线序列, 对象.武.中)), "price": 对象.武.分型特征值 if type(对象) is not 中枢 else 对象.低},
|
||||
{"time": int(缠论K线.时间戳对齐(self.基础缠K序列, 对象.文.中)), "price": 对象.文.分型特征值 if type(对象) is not 中枢 else 对象.高},
|
||||
{"time": int(缠论K线.时间戳对齐(self.基础缠K序列, 对象.武.中)), "price": 对象.武.分型特征值 if type(对象) is not 中枢 else 对象.低},
|
||||
]
|
||||
linewidths = {"笔": 1, "线段": 2, "走势": 3, "线段特征": 2}
|
||||
message["overrides"] = {
|
||||
@@ -1107,6 +585,9 @@ class 观察者(观察者):
|
||||
if type(对象) is not 线段特征:
|
||||
message["overrides"]["text"] = f"{对象.标识} {对象.序号} 周期:{self.周期} {getattr(对象, '四象', '')} {getattr(对象, '特征序列状态', '')} {getattr(对象, '级别', '')} {getattr(对象, '备注', '')}"
|
||||
|
||||
if type(对象) is 中枢:
|
||||
message["overrides"]["text"] = f"{对象.标识} {对象.序号} 周期:{self.周期} 基础序列数量: {len(对象.基础序列)}"
|
||||
|
||||
if 对象.标识 in ("线段", "线段<线段>"):
|
||||
message["overrides"]["text"] = f"{对象.标识} {对象.序号} 周期:{self.周期} {线段.四象(对象)} {线段.特征序列状态(对象)} {getattr(对象, '级别', '')} {getattr(对象, '备注', '')}"
|
||||
|
||||
@@ -1133,17 +614,6 @@ class 观察者(观察者):
|
||||
asyncio.ensure_future(self.数据通道.send_text(json.dumps(message)))
|
||||
return
|
||||
|
||||
def 标注买卖点(self):
|
||||
"""将 BSP字典 中的买卖点推送到图表。与旧的 添加买卖点/报信 独立。"""
|
||||
if not self.配置.图表展示 or self.数据通道 is None:
|
||||
return
|
||||
已标注 = getattr(self, "_已标注BSP序号", set())
|
||||
for 笔序号, bsp in self.BSP字典.items():
|
||||
if 笔序号 in 已标注:
|
||||
continue
|
||||
已标注.add(笔序号)
|
||||
self.报信(bsp, 指令.添加(bsp.备注), sys._getframe().f_lineno)
|
||||
|
||||
def 将图表数据固化到本地(self, static_shapes=None):
|
||||
template_path = "./templates/static.html"
|
||||
# 初始化 Jinja2 环境,模板目录为当前目录
|
||||
@@ -1170,7 +640,6 @@ class 观察者(观察者):
|
||||
全部 = []
|
||||
for o in self.买卖点字典.values():
|
||||
全部.extend(o)
|
||||
全部.extend(self.BSP字典.values())
|
||||
|
||||
for 对象 in 全部:
|
||||
if type(对象) in (笔, 线段, 中枢, 线段特征):
|
||||
@@ -1234,23 +703,6 @@ class 观察者(观察者):
|
||||
static_shapes.append(message)
|
||||
continue
|
||||
|
||||
if type(对象) is BSP点:
|
||||
message = dict()
|
||||
message["type"] = "shape"
|
||||
message["id"] = f"BSP_{id(对象)}"
|
||||
message["shapeType"] = "arrow_down" if not 对象.是否买点 else "arrow_up"
|
||||
k线 = 对象.K线单元
|
||||
message["points"] = [{"time": int(k线.时间戳), "price": k线.分型特征值}]
|
||||
arrowColor = "#FF2800" if not 对象.是否买点 else "#00FF22"
|
||||
text = f"{对象.类型字符串}"
|
||||
message["overrides"] = {
|
||||
"color": "#FFA500",
|
||||
"arrowColor": arrowColor,
|
||||
"text": text,
|
||||
"title": 对象.类型字符串,
|
||||
"showLabel": True,
|
||||
}
|
||||
static_shapes.append(message)
|
||||
else:
|
||||
print(type(对象), 对象)
|
||||
for item in static_shapes:
|
||||
@@ -1284,7 +736,7 @@ class 观察者(观察者):
|
||||
buffer = f.read()
|
||||
size = struct.calcsize(">6d")
|
||||
for i in range(len(buffer) // size):
|
||||
k线 = K线.读取大端字节数组(buffer[i * size : i * size + size], int(周期))
|
||||
k线 = K线.读取大端字节数组(buffer[i * size : i * size + size], int(周期), 符号)
|
||||
实例.增加原始K线(k线)
|
||||
|
||||
return 实例
|
||||
@@ -1858,7 +1310,7 @@ def 测试_随机生成(symbol: str = "btcusd", limit: int = 5000, freq: Support
|
||||
def 魔法():
|
||||
随机生成实例 = 观察者(symbol + "_gen", 周期=int(freq), 数据通道=ws, 配置=配置)
|
||||
dt = datetime(2008, 8, 8)
|
||||
原始K线 = K线.创建普K("随机", dt, 8888.55, 10000.00, 9000.22, 9527.33, 888, 0, int(freq))
|
||||
原始K线 = K线.创建普K("随机", int(dt.timestamp()), 8888.55, 10000.00, 9000.22, 9527.33, 888, 0, int(freq))
|
||||
随机生成实例.增加原始K线(原始K线)
|
||||
for 方向 in 从序列中机选(
|
||||
int(limit),
|
||||
@@ -1910,7 +1362,7 @@ class Bitstamp:
|
||||
for bar in data["data"]["ohlc"]:
|
||||
K = K线.创建普K(
|
||||
观察员.符号,
|
||||
转化为时间戳(int(bar["timestamp"])),
|
||||
int(bar["timestamp"]),
|
||||
float(bar["open"]),
|
||||
float(bar["high"]),
|
||||
float(bar["low"]),
|
||||
@@ -2547,65 +1999,53 @@ async def 处理图表消息(用户标识: str, 消息字典: Dict, websocket: W
|
||||
待发送消息 = {}
|
||||
if 数据类型 == "中枢<笔>":
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.笔_中枢序列[序号])})
|
||||
if 数据类型 == "中枢<线段>":
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.中枢序列[序号])})
|
||||
|
||||
if 数据类型 == "中枢<线段<线段>>":
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.线段_中枢序列[序号])})
|
||||
|
||||
if 数据类型 == "中枢<扩展线段>":
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.扩展中枢序列[序号])})
|
||||
if 数据类型 == "中枢<扩展线段<线段>>":
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.扩展中枢序列_线段[序号])})
|
||||
|
||||
if 数据类型 == "笔":
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.笔序列[序号])})
|
||||
if 数据类型 == "线段":
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.线段序列[序号])})
|
||||
段: 虚线 = 观察员.线段序列[序号]
|
||||
if 段._特征序列_显示:
|
||||
段._特征序列_显示 = False
|
||||
for 特征 in 段.特征序列:
|
||||
if 特征 is not None:
|
||||
观察员 and 观察员.报信(特征, 指令.删除(特征.标识), sys._getframe().f_lineno)
|
||||
|
||||
else:
|
||||
段._特征序列_显示 = True
|
||||
序号 = 0
|
||||
for 特征 in 段.特征序列:
|
||||
if 特征 is not None:
|
||||
特征.序号 = 序号
|
||||
特征.标识 = f"{段.文.中.标识}:{段.文.中.周期}:{段.标识}_特征序列_{序号}:{段.序号}"
|
||||
观察员 and 观察员.报信(特征, 指令.添加(特征.标识), sys._getframe().f_lineno)
|
||||
序号 += 1
|
||||
if 数据类型 == "扩展线段":
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.扩展线段序列[序号])})
|
||||
if 数据类型 == "扩展线段<线段>":
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.扩展线段序列_线段[序号])})
|
||||
if "中枢" in 数据类型 and 数据类型 != "中枢<笔>":
|
||||
for i in range(观察员.中枢分析层次):
|
||||
if 观察员.中枢序列组[i] and 观察员.中枢序列组[i][0].标识 == 数据类型:
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.中枢序列组[i][序号])})
|
||||
for i in range(观察员.扩展中枢分析层次):
|
||||
if 观察员.扩展中枢序列组[i] and 观察员.扩展中枢序列组[i][0].标识 == 数据类型:
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.扩展中枢序列组[i][序号])})
|
||||
for i in range(观察员.混合扩展中枢分析层次):
|
||||
if 观察员.混合扩展中枢序列组[i] and 观察员.混合扩展中枢序列组[i][0].标识 == 数据类型:
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.混合扩展中枢序列组[i][序号])})
|
||||
|
||||
if 数据类型 == "线段<线段>":
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.线段_线段序列[序号])})
|
||||
段 = 观察员.线段_线段序列[序号]
|
||||
if 段._特征序列_显示:
|
||||
段._特征序列_显示 = False
|
||||
for 特征 in 段.特征序列:
|
||||
if 特征 is not None:
|
||||
观察员 and 观察员.报信(特征, 指令.删除(特征.标识), sys._getframe().f_lineno)
|
||||
elif "线段" in 数据类型 and 数据类型 != "笔":
|
||||
for i in range(观察员.线段分析层次):
|
||||
if 观察员.线段序列组[i] and 观察员.线段序列组[i][0].标识 == 数据类型:
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.线段序列组[i][序号])})
|
||||
段 = 观察员.线段序列组[i][序号]
|
||||
if 段._特征序列_显示:
|
||||
段._特征序列_显示 = False
|
||||
for 特征 in 段.特征序列:
|
||||
if 特征 is not None:
|
||||
观察员 and 观察员.报信(特征, 指令.删除(特征.标识), sys._getframe().f_lineno)
|
||||
|
||||
else:
|
||||
段._特征序列_显示 = True
|
||||
序号 = 0
|
||||
for 特征 in 段.特征序列:
|
||||
if 特征 is not None:
|
||||
特征.序号 = 序号
|
||||
特征.标识 = f"{段.文.右.标识}:{段.文.中.周期}:{段.标识}_特征序列_{序号}:{段.序号}"
|
||||
观察员 and 观察员.报信(特征, 指令.添加(特征.标识), sys._getframe().f_lineno)
|
||||
序号 += 1
|
||||
else:
|
||||
段._特征序列_显示 = True
|
||||
序号 = 0
|
||||
for 特征 in 段.特征序列:
|
||||
if 特征 is not None:
|
||||
特征.序号 = 序号
|
||||
特征.标识 = f"{段.文.中.标识}:{段.文.中.周期}:{段.标识}_特征序列_{序号}:{段.序号}"
|
||||
观察员 and 观察员.报信(特征, 指令.添加(特征.标识), sys._getframe().f_lineno)
|
||||
序号 += 1
|
||||
|
||||
for i in range(观察员.扩展线段分析层次):
|
||||
if 观察员.扩展线段序列组[i] and 观察员.扩展线段序列组[i][0].标识 == 数据类型:
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.扩展线段序列组[i][序号])})
|
||||
for i in range(观察员.混合扩展线段分析层次):
|
||||
if 观察员.混合扩展线段序列组[i] and 观察员.混合扩展线段序列组[i][0].标识 == 数据类型:
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.混合扩展线段序列组[i][序号])})
|
||||
|
||||
if "_" in 数据类型 and "中枢" in 数据类型: # 线段_0_实_中枢<笔>
|
||||
数据类型, 线序, 虚实合, 类型 = 数据类型.split("_")
|
||||
|
||||
段序号 = int(线序)
|
||||
|
||||
if 数据类型 == "线段":
|
||||
段: 虚线 = 观察员.线段序列[段序号]
|
||||
zs = getattr(段, f"{虚实合}_中枢序列")[序号]
|
||||
@@ -2616,6 +2056,26 @@ async def 处理图表消息(用户标识: str, 消息字典: Dict, websocket: W
|
||||
zs = getattr(段, f"{虚实合}_中枢序列")[序号]
|
||||
待发送消息.update({"index": 序号, "data": str(zs)})
|
||||
|
||||
for i in range(观察员.线段分析层次):
|
||||
if 观察员.线段序列组[i] and 观察员.线段序列组[i][0].标识 == 数据类型:
|
||||
段 = 观察员.线段序列组[i][段序号]
|
||||
zs = getattr(段, f"{虚实合}_中枢序列")[序号]
|
||||
待发送消息.update({"index": 序号, "data": str(zs)})
|
||||
|
||||
for i in range(观察员.扩展线段分析层次):
|
||||
if 观察员.扩展线段序列组[i] and 观察员.扩展线段序列组[i][0].标识 == 数据类型:
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.扩展线段序列组[i][序号])})
|
||||
段 = 观察员.扩展线段序列组[i][序号]
|
||||
zs = getattr(段, f"{虚实合}_中枢序列")[序号]
|
||||
待发送消息.update({"index": 序号, "data": str(zs)})
|
||||
|
||||
for i in range(观察员.混合扩展线段分析层次):
|
||||
if 观察员.混合扩展线段序列组[i] and 观察员.混合扩展线段序列组[i][0].标识 == 数据类型:
|
||||
待发送消息.update({"index": 序号, "data": str(观察员.混合扩展线段序列组[i][序号])})
|
||||
段 = 观察员.混合扩展线段序列组[i][序号]
|
||||
zs = getattr(段, f"{虚实合}_中枢序列")[序号]
|
||||
待发送消息.update({"index": 序号, "data": str(zs)})
|
||||
|
||||
await 全局连接管理器.发送信息(用户标识, {"type": "query_result", "success": True, "data_type": 数据类型, "data": 待发送消息})
|
||||
|
||||
except IndexError:
|
||||
@@ -2779,6 +2239,7 @@ if __name__ == "__main__":
|
||||
本地随机 = random.Random(os.urandom(64))
|
||||
配置 = 随机配置(本地随机)
|
||||
print(f"[线程{线程编号:02d}] 开始 ...")
|
||||
print(f"[线程{线程编号:02d}] ", 配置.to_dict())
|
||||
测试函数 = 测试_随机生成(symbol="btcusd", limit=10000, freq=时间周期.分(5), ws=None, 配置=配置)
|
||||
结果 = 测试函数() # 实际执行
|
||||
print(f"[线程{线程编号:02d}] 完成 | 笔序列长度: {len(结果.笔序列)}")
|
||||
@@ -2786,6 +2247,7 @@ if __name__ == "__main__":
|
||||
print(f"[线程{线程编号:02d}] 异常: {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
start = datetime.now()
|
||||
# 创建并启动 50 个线程
|
||||
线程列表 = []
|
||||
for i in range(1, 51):
|
||||
@@ -2797,4 +2259,4 @@ if __name__ == "__main__":
|
||||
for 线程 in 线程列表:
|
||||
线程.join()
|
||||
|
||||
print("\n全部 50 个随机回测线程已完成。")
|
||||
print("\n全部 50 个随机回测线程已完成。", datetime.now() - start)
|
||||
|
||||
+63
-34
@@ -657,35 +657,56 @@ class 随机数据(bt.feeds.DataBase):
|
||||
class 自定义实时数据源(bt.feed.DataBase):
|
||||
"""
|
||||
一个用于模拟实时数据推送的数据源,继承自Backtrader的DataBase。
|
||||
在实际应用中,你需要将“模拟生成数据”的部分,替换为接收WebSocket等推送数据的代码。
|
||||
|
||||
支持两种数据输入方式:
|
||||
1. 手动投喂:调用 投喂数据(时间戳, 开, 高, 低, 收, 量) 从 WebSocket 回调等外部代码推送
|
||||
2. 后台线程:传入 观察员 和 魔法 参数,start() 时自动启动线程获取历史数据
|
||||
(注意:观察员 需实现 读取任意数据 方法)
|
||||
|
||||
时间戳格式:Unix 时间戳(秒),为 int 类型。
|
||||
"""
|
||||
|
||||
def __init__(self, 数据队列: queue.Queue, 观察员: "观察者", 魔法, **魔法参数):
|
||||
# 调用父类初始化方法
|
||||
def __init__(self, 数据队列: queue.Queue = None, 观察员: "观察者" = None, 魔法=None, **魔法参数):
|
||||
super(自定义实时数据源, self).__init__()
|
||||
# 存储外部传入的数据队列,用于接收实时数据
|
||||
self.数据队列 = 数据队列
|
||||
# 定义一个标志,用于控制数据加载的停止
|
||||
self.数据队列 = 数据队列 or queue.Queue()
|
||||
self.正在运行 = False
|
||||
self.观察员 = 观察员
|
||||
self.魔法 = 魔法
|
||||
self.__魔法参数 = 魔法参数
|
||||
self.已有数据 = False
|
||||
|
||||
def 投喂数据(self, 时间戳: int, 开盘价: float, 最高价: float, 最低价: float, 收盘价: float, 成交量: float, 持仓量: float = 0):
|
||||
"""线程安全的外部数据推送接口,供 WebSocket 回调等外部代码调用。
|
||||
|
||||
:param 时间戳: Unix 时间戳(秒,int 类型)
|
||||
:param 开盘价: 开盘价
|
||||
:param 最高价: 最高价
|
||||
:param 最低价: 最低价
|
||||
:param 收盘价: 收盘价
|
||||
:param 成交量: 成交量
|
||||
:param 持仓量: 持仓量(默认为 0)
|
||||
"""
|
||||
self.数据队列.put((时间戳, 开盘价, 最高价, 最低价, 收盘价, 成交量, 持仓量))
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
数据源开始工作时的初始化操作。
|
||||
这里可以不执行任何操作,或者启动数据接收线程等。
|
||||
"""
|
||||
print(f"[{datetime.now()}] 自定义数据源已启动...")
|
||||
self.正在运行 = True
|
||||
|
||||
def 运行回测():
|
||||
self.观察员.读取任意数据(self.魔法, **self.__魔法参数)
|
||||
self.正在运行 = False
|
||||
if self.观察员 is not None and self.魔法 is not None:
|
||||
if not hasattr(self.观察员, "读取任意数据"):
|
||||
print(f"[{datetime.now()}] 警告: 观察员没有 '读取任意数据' 方法,后台线程不会启动")
|
||||
return
|
||||
|
||||
回测线程 = threading.Thread(target=运行回测, daemon=True)
|
||||
回测线程.start()
|
||||
def 运行回测():
|
||||
try:
|
||||
self.观察员.读取任意数据(self.魔法, **self.__魔法参数)
|
||||
except Exception as e:
|
||||
print(f"[{datetime.now()}] 后台数据线程异常: {e}")
|
||||
finally:
|
||||
self.正在运行 = False
|
||||
|
||||
回测线程 = threading.Thread(target=运行回测, daemon=True)
|
||||
回测线程.start()
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
@@ -696,32 +717,25 @@ class 自定义实时数据源(bt.feed.DataBase):
|
||||
print(f"[{datetime.now()}] 自定义数据源已停止。")
|
||||
|
||||
def _load(self):
|
||||
"""
|
||||
(核心方法) Backtrader会循环调用此方法来获取数据。
|
||||
每次被调用时,都应该返回新的数据行(一个K线/一个数据点)。
|
||||
如果没有新数据,返回 False,Backtrader会等待下次调用。
|
||||
"""
|
||||
"""Backtrader 核心回调 — 阻塞等待数据,有数据时返回 True,数据源停止时返回 False。"""
|
||||
if not self.正在运行:
|
||||
return False
|
||||
# 当没有新数据且系统仍在运行时,进入等待
|
||||
# 从外部队列获取新数据
|
||||
|
||||
while True:
|
||||
try:
|
||||
data_point = self.数据队列.get(timeout=0.5)
|
||||
break
|
||||
except queue.Empty:
|
||||
if not self.已有数据:
|
||||
continue
|
||||
else:
|
||||
if self.正在运行:
|
||||
continue
|
||||
if not self.正在运行:
|
||||
return False
|
||||
# 解析数据元组
|
||||
dt, o, h, l, c, v, oi = data_point
|
||||
|
||||
# 将数据写入Backtrader的数据线 (lines)
|
||||
self.lines.datetime[0] = bt.date2num(dt)
|
||||
try:
|
||||
dt, o, h, l, c, v, oi = data_point
|
||||
except (ValueError, TypeError) as e:
|
||||
print(f"[{datetime.now()}] 自定义数据源: 数据格式错误,跳过: {e}")
|
||||
return True
|
||||
|
||||
self.lines.datetime[0] = bt.date2num(datetime.utcfromtimestamp(int(dt)))
|
||||
self.lines.open[0] = o
|
||||
self.lines.high[0] = h
|
||||
self.lines.low[0] = l
|
||||
@@ -729,7 +743,6 @@ class 自定义实时数据源(bt.feed.DataBase):
|
||||
self.lines.volume[0] = v
|
||||
self.lines.openinterest[0] = oi
|
||||
self.已有数据 = True
|
||||
# 返回 True 表示成功加载一行数据
|
||||
return True
|
||||
|
||||
|
||||
@@ -757,7 +770,7 @@ class 高级策略基类(bt.Strategy):
|
||||
|
||||
def 日志(self, 文本, 时间=None):
|
||||
时间 = 时间 or self.datas[0].datetime.datetime(0)
|
||||
print(f"{时间} {文本}")
|
||||
print(f"{时间} {self.p.观察员.__class__.__name__}: {文本}")
|
||||
|
||||
def 计算目标数量(self, 价格):
|
||||
"""根据资金类型和仓位比例计算目标数量"""
|
||||
@@ -946,6 +959,7 @@ class 回测(高级策略基类):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.已处理信号 = set()
|
||||
print(f"{self.p.观察员.__class__.__name__}: 加载完成...")
|
||||
|
||||
def 获取开仓限价(self, 是否做多):
|
||||
# 根据缠论分型计算限价,若无则返回 None 使用基类默认逻辑
|
||||
@@ -957,6 +971,7 @@ class 回测(高级策略基类):
|
||||
return None
|
||||
|
||||
def next(self):
|
||||
# self.日志(f"{self.p.观察员.__class__.__name__} called next ")
|
||||
# 1. 更新移动止损(基类方法)
|
||||
if self.position:
|
||||
self.更新止损订单(self.position.size > 0, self.data.close[0])
|
||||
@@ -986,15 +1001,22 @@ class 回测(高级策略基类):
|
||||
def 检查买信号(self):
|
||||
if self.p.观察员.笔序列:
|
||||
k线 = self.p.观察员.缠论K线序列[-1]
|
||||
# self.日志(f"检查买信号 当前笔 {self.p.观察员.笔序列[-1]}")
|
||||
if k线.买卖点信息:
|
||||
print(f"回测-首 {self.p.观察员.__class__.__name__}", k线.买卖点信息)
|
||||
首 = True if k线.买卖点信息 and "买" in next(iter(k线.买卖点信息)) else False
|
||||
if 首:
|
||||
print(k线)
|
||||
原始差值 = self.p.观察员.当前K线.序号 - k线.标的K线.序号
|
||||
差值 = self.p.观察员.当前缠K.序号 - k线.序号
|
||||
self.日志(f"首_买入信号差值: {原始差值}, {差值}, 观察员.当前K线 时间戳: {self.p.观察员.当前K线.时间戳}")
|
||||
k线 = self.p.观察员.缠论K线序列[-2]
|
||||
if k线.买卖点信息:
|
||||
print(f"回测-尾 {self.p.观察员.__class__.__name__}", k线.买卖点信息)
|
||||
|
||||
尾 = True if self.p.观察员.缠论K线序列[-2].买卖点信息 and "买" in next(iter(self.p.观察员.缠论K线序列[-2].买卖点信息)) else False
|
||||
if 尾:
|
||||
print(k线)
|
||||
原始差值 = self.p.观察员.当前K线.序号 - k线.标的K线.序号
|
||||
差值 = self.p.观察员.当前缠K.序号 - k线.序号
|
||||
self.日志(f"尾_买入信号差值: {原始差值}, {差值}, 观察员.当前K线 时间戳: {self.p.观察员.当前K线.时间戳}")
|
||||
@@ -1002,16 +1024,23 @@ class 回测(高级策略基类):
|
||||
|
||||
def 检查卖信号(self):
|
||||
if self.p.观察员.笔序列:
|
||||
# self.日志(f"检查卖信号 当前笔 {self.p.观察员.笔序列[-1]}")
|
||||
k线 = self.p.观察员.缠论K线序列[-1]
|
||||
if k线.买卖点信息:
|
||||
print(f"回测-首 {self.p.观察员.__class__.__name__}", k线.买卖点信息)
|
||||
首 = True if k线.买卖点信息 and "卖" in next(iter(k线.买卖点信息)) else False
|
||||
if 首:
|
||||
print(k线)
|
||||
原始差值 = self.p.观察员.当前K线.序号 - k线.标的K线.序号
|
||||
差值 = self.p.观察员.当前缠K.序号 - k线.序号
|
||||
self.日志(f"首_买入信号差值: {原始差值}, {差值}, 观察员.当前K线 时间戳: {self.p.观察员.当前K线.时间戳}")
|
||||
k线 = self.p.观察员.缠论K线序列[-2]
|
||||
if k线.买卖点信息:
|
||||
print(f"回测-尾 {self.p.观察员.__class__.__name__}", k线.买卖点信息)
|
||||
|
||||
尾 = True if self.p.观察员.缠论K线序列[-2].买卖点信息 and "卖" in next(iter(self.p.观察员.缠论K线序列[-2].买卖点信息)) else False
|
||||
if 尾:
|
||||
print(k线)
|
||||
原始差值 = self.p.观察员.当前K线.序号 - k线.标的K线.序号
|
||||
差值 = self.p.观察员.当前缠K.序号 - k线.序号
|
||||
self.日志(f"尾_买入信号差值: {原始差值}, {差值}, 观察员.当前K线 时间戳: {self.p.观察员.当前K线.时间戳}")
|
||||
@@ -1019,7 +1048,7 @@ class 回测(高级策略基类):
|
||||
|
||||
def log(self, 文本, dt=None):
|
||||
dt = dt or bt.num2date(self.data.datetime[0])
|
||||
print(f"[{dt.strftime('%Y-%m-%d %H:%M')}] {self.p.符号} | {文本}")
|
||||
print(f"[{dt.strftime('%Y-%m-%d %H:%M')}] {self.p.观察员.__class__.__name__}: {self.p.符号} | {文本}")
|
||||
|
||||
|
||||
# ==================== 回测运行入口 ====================
|
||||
|
||||
+52
-19
@@ -848,6 +848,7 @@ print("=== 趋势分析 ===")
|
||||
this.websocketManager = websocketManager;
|
||||
this.widget = null;
|
||||
this.shapeIds = new Map();
|
||||
this._pendingShapeCreate = new Map(); // id → Promise, 追踪正在创建的图形
|
||||
this.onRealtimeCallback = null;
|
||||
this.initDataFeed();
|
||||
this.websocketManager.on('chart', (message) => {
|
||||
@@ -1052,7 +1053,7 @@ print("=== 趋势分析 ===")
|
||||
}
|
||||
}
|
||||
|
||||
handleChartMessage(message) {
|
||||
async handleChartMessage(message) {
|
||||
Utils.log("处理图表消息:", message.type);
|
||||
switch (message.type) {
|
||||
case 'realtime':
|
||||
@@ -1070,11 +1071,11 @@ print("=== 趋势分析 ===")
|
||||
break;
|
||||
case 'shape':
|
||||
if (message.cmd === "APPEND") {
|
||||
this.addShape(message.id, message.points, message.name, message.overrides);
|
||||
await this.addShape(message.id, message.points, message.name, message.overrides);
|
||||
} else if (message.cmd === "REMOVE") {
|
||||
this.removeShape(message.id);
|
||||
await this.removeShape(message.id);
|
||||
} else if (message.cmd === "MODIFY") {
|
||||
this.updateShape(message.id, message.points, message.overrides);
|
||||
await this.updateShape(message.id, message.points, message.overrides);
|
||||
}
|
||||
break;
|
||||
case 'query_result':
|
||||
@@ -1104,6 +1105,12 @@ print("=== 趋势分析 ===")
|
||||
return;
|
||||
}
|
||||
|
||||
// 等待之前相同 ID 的创建完成(避免竞态)
|
||||
const prevPending = this._pendingShapeCreate.get(id);
|
||||
if (prevPending) {
|
||||
await prevPending;
|
||||
}
|
||||
|
||||
// 重复判断
|
||||
const existShapeId = this.shapeIds.get(id);
|
||||
if (existShapeId) {
|
||||
@@ -1112,26 +1119,40 @@ print("=== 趋势分析 ===")
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建图形
|
||||
const shapeId = await this.widget.chart().createMultipointShape(points, options);
|
||||
const shape = this.widget.chart().getShapeById(shapeId);
|
||||
// 记录 pending 状态,供 removeShape/updateShape 等待
|
||||
const createPromise = (async () => {
|
||||
// 创建图形
|
||||
const shapeId = await this.widget.chart().createMultipointShape(points, options);
|
||||
const shape = this.widget.chart().getShapeById(shapeId);
|
||||
|
||||
if (!shape) {
|
||||
Utils.error("添加形状失败:shape 为 null", {id, shapeId});
|
||||
return;
|
||||
if (!shape) {
|
||||
Utils.error("添加形状失败:shape 为 null", {id, shapeId});
|
||||
return null;
|
||||
}
|
||||
|
||||
// 设置属性
|
||||
//Utils.log("shapeId:", shape.getProperties());
|
||||
//shape.setProperties(properties);
|
||||
shape.bringToFront();
|
||||
|
||||
// 保存映射
|
||||
this.shapeIds.set(id, shapeId);
|
||||
Utils.log("添加形状", {id, shapeId});
|
||||
|
||||
return shapeId;
|
||||
})();
|
||||
|
||||
this._pendingShapeCreate.set(id, createPromise);
|
||||
|
||||
const shapeId = await createPromise;
|
||||
if (shapeId === null) {
|
||||
// shape creation failed
|
||||
}
|
||||
|
||||
// 设置属性
|
||||
//Utils.log("shapeId:", shape.getProperties());
|
||||
//shape.setProperties(properties);
|
||||
shape.bringToFront();
|
||||
|
||||
// 保存映射
|
||||
this.shapeIds.set(id, shapeId);
|
||||
Utils.log("添加形状", {id, shapeId});
|
||||
|
||||
} catch (error) {
|
||||
Utils.error("添加形状失败", error);
|
||||
} finally {
|
||||
this._pendingShapeCreate.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1139,6 +1160,12 @@ print("=== 趋势分析 ===")
|
||||
try {
|
||||
if (!this.widget || !this.widget.chart) return;
|
||||
|
||||
// 等待该 ID 的 addShape 完成(避免竞态)
|
||||
const pending = this._pendingShapeCreate.get(id);
|
||||
if (pending) {
|
||||
await pending;
|
||||
}
|
||||
|
||||
const shapeId = this.shapeIds.get(id);
|
||||
if (!shapeId) {
|
||||
Utils.log("图形不存在,无需删除", {id});
|
||||
@@ -1162,6 +1189,12 @@ print("=== 趋势分析 ===")
|
||||
try {
|
||||
if (!this.widget || !this.widget.chart) return;
|
||||
|
||||
// 等待该 ID 的 addShape 完成(避免竞态)
|
||||
const pending = this._pendingShapeCreate.get(id);
|
||||
if (pending) {
|
||||
await pending;
|
||||
}
|
||||
|
||||
const shapeId = this.shapeIds.get(id);
|
||||
if (!shapeId) {
|
||||
Utils.error("更新失败:图形不存在", {id});
|
||||
|
||||
Reference in New Issue
Block a user