32 Commits

Author SHA1 Message Date
YuWuKunCheng 9c972c2673 移除 配置兼容 2026-06-19 20:59:25 +08:00
YuWuKunCheng 0f69cfda79 Nothing 2026-06-16 11:57:19 +08:00
YuWuKunCheng e44f9dd837 修复 中枢完整性 2026-06-14 11:48:47 +08:00
YuWuKunCheng 1477cde86b 优化代码逻辑 2026-06-12 14:41:50 +08:00
YuWuKunCheng da5222d960 添加 全局变量“扩展线段模式” 默认为 True
线段 扩展分析采用“端点高, 端点低” 作为高低取值。中枢分析时高低取值不变,此举在中枢<扩展**> 中更贴合原文同级别分级时的中枢高低取值
2026-06-10 23:21:33 +08:00
YuWuKunCheng 15a1d43b1d Nothing 2026-06-09 15:13:49 +08:00
YuWuKunCheng a8df8cd187 移除 无意义配置项 2026-06-09 15:03:34 +08:00
YuWuKunCheng 9c7686d104 Nothing 2026-06-09 01:06:52 +08:00
YuWuKunCheng 34c42ecd68 修复 异常BUG 2026-06-08 23:29:00 +08:00
YuWuKunCheng 8a2afb9ed0 修复 观察者混合扩展边界问题 2026-06-07 20:45:09 +08:00
YuWuKunCheng 11d897ebaa 第十版 2026-06-07 17:02:10 +08:00
YuWuKunCheng 4bf9461009 规范文件 2026-06-07 14:34:05 +08:00
YuWuKunCheng c83cddbcc4 观察者 线段分析/扩展分析 支持无限递归,保留之前序列为@property 形式
移除 某些函数的缓存机制
2026-06-07 13:34:28 +08:00
YuWuKunCheng 0a7e9dc896 观察者 线段分析/扩展分析 支持无限递归,保留之前序列为@property 形式 2026-06-06 22:02:47 +08:00
YuWuKunCheng dd40f95742 观察者 线段分析/扩展分析 支持无限递归,保留之前序列为@property 形式 2026-06-06 17:50:08 +08:00
YuWuKunCheng e33b1f0744 修复 Arc::make_mut 遗留问题
增加 基础数据对比方法
2026-06-05 16:35:31 +08:00
YuWuKunCheng 823a24d364 第九版 2026-06-05 09:02:54 +08:00
YuWuKunCheng 58c85dc7cd 添加 Read the docs 配置 2026-05-31 12:05:48 +08:00
YuWuKunCheng c3e9ae8d77 Nothing v26.5.103 2026-05-30 22:34:10 +08:00
YuWuKunCheng b7c4e60420 1. thread_local! 导致跨线程缓存不可见(主要问题)
kline_py.rs 中 BSP_CACHE、KLINE_IDENTITY、BAR_IDENTITY 使用了 thread_local!。主线程调用 识别买卖点() →
  买卖点信息.add() 写入的是主线程的 PySet,backtrader 策略线程读取的是自己线程独立的空 PySet。

  修复:将三个缓存从 thread_local! 改为全局 static + std::sync::LazyLock<RwLock<HashMap<...>>>

  影响分析

  这些身份缓存的影响与 KLINE_IDENTITY/BAR_IDENTITY 不同——它们只影响 Python 对象身份(is
  比较),不直接影响数据内容(因为 Rust 数据通过 Arc 共享,读写都是同一份)。

  具体后果:
  - 不同线程访问同一个 Rust Arc 会得到不同的 Python wrapper 对象
  - a is b 跨线程比较返回 False,哪怕它们包装同一个底层 Rust 对象
  - 每个线程维护一份独立缓存,内存浪费(不过 wrapper 很小)
2026-05-30 22:15:09 +08:00
YuWuKunCheng 15dc44e8e0 回测测试 2026-05-30 20:06:59 +08:00
YuWuKunCheng bd4fceab02 全量支持 观察者在python端的重写机制 2026-05-30 15:51:18 +08:00
YuWuKunCheng af3ebf13c8 修复 “cargo clippy -- -D warnings” 产生的错误 2026-05-30 13:25:57 +08:00
YuWuKunCheng 22109d8b30 合并 工作流 2026-05-30 13:03:37 +08:00
YuWuKunCheng c2c09fc8ba 合并 工作流 2026-05-30 12:17:22 +08:00
YuWuKunCheng 0eb52cc06a 添加 自动发包 2026-05-30 11:56:06 +08:00
YuWuKunCheng 1e6025a968 添加 原始chan到模块
修复 相对方向的一致性
添加 观察者.投喂原始数据
2026-05-30 11:26:59 +08:00
YuWuKunCheng 14279f3df6 完善 测试类 2026-05-29 23:28:59 +08:00
YuWuKunCheng fca62f3141 修复 买卖点一致性对齐至chan.py 2026-05-29 20:42:34 +08:00
YuWuKunCheng e50172e923 修复 中枢一致性 2026-05-29 20:19:42 +08:00
YuWuKunCheng c87fb66d34 修复 分型时间戳 2026-05-29 17:30:58 +08:00
YuWuKunCheng 9900266516 修复 买卖点 2026-05-29 15:01:02 +08:00
85 changed files with 27031 additions and 12325 deletions
+182
View File
@@ -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 的上下文、截图或补充说明。
+36
View File
@@ -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` / 其他 |
| 其他 | |
## 补充信息
任何有助于更好理解或解决此问题的信息。
+62
View File
@@ -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`)
## 补充信息
任何参考链接、图表、伪代码或其他有助于说明该功能的内容。
+70
View File
@@ -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 的截图、日志或对比数据。
+176 -7
View File
@@ -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: 下载所有产物
+22
View File
@@ -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-pymaturin develop
- method: pip
path: chanlun-py
+94
View File
@@ -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
View File
@@ -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 代码对齐
+16 -7
View File
@@ -3,7 +3,7 @@
[![PyPI](https://img.shields.io/pypi/v/chanlun)](https://pypi.org/project/chanlun/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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
View File
@@ -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` 使用是否合理
## 偏好语言
请使用简体中文或英文撰写报告。
+2497 -946
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -3,3 +3,5 @@ __pycache__/
*.egg-info/
dist/
build/
Cargo.lock
+9 -4
View File
@@ -1,7 +1,7 @@
[package]
name = "chanlun-py"
version = "26.5.86"
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.3" # { path = "../chanlun" }
pyo3 = { version = "0.28", features = ["extension-module", "experimental-inspect"] }
chanlun = { path = "../chanlun" }
parking_lot = "0.12"
tracing-subscriber = { version = "0.3", features = ["env-filter", "ansi", "std", "registry"] }
tracing-core = "0.1"
dashmap = "6"
tracing = "0.1"
pyo3 = { version = "0.28", features = ["experimental-inspect"] }
serde_json = "1"
chrono = "0.4"
-1
View File
@@ -1 +0,0 @@
../LICENSE
+21
View File
@@ -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
View File
@@ -1 +0,0 @@
../README.md
+150
View File
@@ -0,0 +1,150 @@
# chanlun — 缠论技术分析 Python 绑定
[![PyPI](https://img.shields.io/pypi/v/chanlun)](https://pypi.org/project/chanlun/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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)、parseMIT)、termcolorMIT)。
详见 [NOTICE](../NOTICE) 和 [LICENSES/](../LICENSES/) 目录。
+4 -4
View File
@@ -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
View File
Executable → Regular
View File
+811 -2234
View File
File diff suppressed because it is too large Load Diff
+37 -2
View File
@@ -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()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+19 -1
View File
@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "chanlun"
version = "2605.86"
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"
@@ -37,3 +43,15 @@ features = ["pyo3/extension-module"]
python-source = "."
module-name = "chanlun._chanlun"
manifest-path = "Cargo.toml"
[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = ["-v", "--tb=short", "--durations=10"]
[tool.ruff.lint]
# 启用 FA (flake8-future-annotations) 和 UP (pyupgrade) 规则
select = ["FA", "UP"]
# [tool.ruff.lint.flake8-future-annotations]
# 强制在所有文件中注入 from __future__ import annotations
# force-future-annotations = true
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+199
View File
@@ -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
View File
@@ -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())
+744
View File
@@ -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(())
}
+379 -6
View File
@@ -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(())
}
+130 -117
View File
@@ -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,19 +463,16 @@ impl 缠论K线Py {
}
#[getter]
fn (&self) -> Py {
Py {
inner: *self.inner..read().unwrap(),
}
fn (&self, py: Python<'_>) -> Py<Py> {
crate::types_py::(py, *self.inner..read())
}
#[getter]
fn (&self) -> Option<crate::types_py::Py> {
fn (&self, py: Python<'_>) -> Option<Py<crate::types_py::Py>> {
self.inner
.
.read()
.unwrap()
.map(|f| crate::types_py::Py { inner: f })
.map(|f| crate::types_py::(py, f))
}
#[getter]
@@ -485,7 +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.())?;
@@ -506,7 +523,7 @@ impl 缠论K线Py {
dict.set_item("与RSI匹配", self.RSI匹配())?;
dict.set_item("与KDJ匹配", self.KDJ匹配())?;
if let Some(v) = self.() {
if let Some(v) = self.(py) {
dict.set_item("分型", v)?;
}
Ok(dict.into())
@@ -534,17 +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]
+230 -29
View File
@@ -22,20 +22,218 @@
* SOFTWARE.
*/
#![allow(non_snake_case, clippy::too_many_arguments)]
use pyo3::prelude::*;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::{Mutex, Once, OnceLock};
/// 日志级别: 0=trace, 1=debug, 2=info, 3=warn, 4=error, 5=off
static LOG_LEVEL: AtomicU8 = AtomicU8::new(2); // 默认 info
type =
tracing_subscriber::reload::Handle<tracing_subscriber::EnvFilter, tracing_subscriber::Registry>;
static : OnceLock<Mutex<>> = OnceLock::new();
static TRACING_INIT: Once = Once::new();
fn (n: u8) -> &'static str {
match n {
0 => "trace",
1 => "debug",
2 => "info",
3 => "warn",
4 => "error",
5 => "off",
_ => "unknown",
}
}
fn (name: &str) -> Option<u8> {
match name.to_lowercase().as_str() {
"trace" => Some(0),
"debug" => Some(1),
"info" => Some(2),
"warn" => Some(3),
"error" => Some(4),
"off" => Some(5),
_ => None,
}
}
fn init_tracing() {
TRACING_INIT.call_once(|| {
use chrono::Local;
use std::fmt;
use tracing_subscriber::fmt::format::Format;
use tracing_subscriber::fmt::format::Writer;
use tracing_subscriber::fmt::time::FormatTime;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
struct ;
impl FormatTime for {
fn format_time(&self, w: &mut Writer<'_>) -> fmt::Result {
write!(w, "{}", Local::now().format("%Y-%m-%d %H:%M:%S%.3f"))
}
}
let format = Format::default()
.with_timer()
.with_target(false)
.with_file(true)
.with_line_number(true)
.with_ansi(true)
.compact();
let = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
let (, ) = tracing_subscriber::reload::Layer::new();
.set(Mutex::new())
.expect("过滤器句柄锁只能设置一次");
tracing_subscriber::registry()
.with()
.with(tracing_subscriber::fmt::layer().event_format(format))
.init();
});
}
mod algorithm_py;
mod business_py;
pub(crate) mod cache;
mod config_py;
mod equality_py;
mod indicators_py;
mod kline_py;
mod structure_py;
mod types_py;
/// 分型模式 — True 时使用构造时缓存值,False 时从 中 缠K 实时读取
#[pyfunction]
fn get_分型模式() -> bool {
chanlun::structure::fractal_obj::.load(Ordering::Relaxed)
}
/// 设置 分型模式
#[pyfunction]
fn set_分型模式(value: bool) {
chanlun::structure::fractal_obj::.store(value, Ordering::Relaxed);
}
/// 扩展线段模式 — 控制虚线高低取值方式,默认 False
#[pyfunction]
fn get_扩展线段模式() -> bool {
chanlun::structure::dash_line::线.load(Ordering::Relaxed)
}
/// 设置 扩展线段模式
#[pyfunction]
fn set_扩展线段模式(value: bool) {
chanlun::structure::dash_line::线.store(value, Ordering::Relaxed);
}
/// 获取当前日志级别 ("trace" / "debug" / "info" / "warn" / "error" / "off")
#[pyfunction]
fn get_log_level() -> &'static str {
(LOG_LEVEL.load(Ordering::Relaxed))
}
/// 设置日志级别 — 自动启用日志,同步更新 tracing subscriber
#[pyfunction]
fn set_log_level(level: &str) -> PyResult<()> {
let = (level).ok_or_else(|| {
pyo3::exceptions::PyValueError::new_err(format!(
"无效日志级别 '{}',有效值: trace, debug, info, warn, error, off",
level
))
})?;
LOG_LEVEL.store(, Ordering::Relaxed);
chanlun::log::.store( < 5, Ordering::Relaxed);
// 同步更新 tracing subscriber
if let Some(guard) = .get() {
let handle = guard.lock().unwrap();
let = ();
let filter = tracing_subscriber::EnvFilter::new();
let _ = handle.reload(filter);
}
Ok(())
}
/// 获取日志输出模式 ("off", "simple", "tracing")
#[pyfunction]
fn get_log_mode() -> &'static str {
match chanlun::log::get_log_mode() {
0 => "off",
1 => "simple",
2 => "tracing",
_ => "unknown",
}
}
/// 设置日志输出模式(必须在任何日志输出之前调用)
/// - "off": 不输出
/// - "simple": 直接 eprintln/println(默认)
/// - "tracing": 带时间戳和格式化的 tracing subscriber
#[pyfunction]
fn set_log_mode(mode: &str) -> PyResult<()> {
let m = match mode.to_lowercase().as_str() {
"off" | "0" => 0u8,
"simple" | "on" | "1" => 1u8,
"tracing" | "2" => 2u8,
_ => {
return Err(pyo3::exceptions::PyValueError::new_err(
"无效日志模式,有效值: 'off', 'simple', 'tracing'",
));
}
};
if m == 2 {
init_tracing();
}
chanlun::log::set_log_mode(m);
Ok(())
}
/// 获取缓存模式 ("thread_local" 或 "global")
#[pyfunction]
fn get_cache_mode() -> &'static str {
match crate::cache::peek_mode().unwrap_or(&crate::cache::CacheMode::ThreadLocal) {
crate::cache::CacheMode::ThreadLocal => "thread_local",
crate::cache::CacheMode::Global => "global",
}
}
/// 设置缓存模式(必须在创建任何观察者之前调用)
#[pyfunction]
fn set_cache_mode(mode: &str) -> PyResult<()> {
let m = match mode.to_lowercase().as_str() {
"thread_local" | "local" => crate::cache::CacheMode::ThreadLocal,
"global" => crate::cache::CacheMode::Global,
_ => {
return Err(pyo3::exceptions::PyValueError::new_err(
"无效缓存模式,有效值: 'thread_local', 'global'",
));
}
};
crate::cache::set_mode(m).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e))
}
/// 缠论技术分析库 — Rust 高性能实现
#[pymodule]
/// 缠论技术分析库 — Rust 高性能实现
fn _chanlun(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
chanlun::log::init_from_env();
m.add_function(wrap_pyfunction!(get_分型模式, m)?)?;
m.add_function(wrap_pyfunction!(set_分型模式, m)?)?;
m.add_function(wrap_pyfunction!(get_扩展线段模式, m)?)?;
m.add_function(wrap_pyfunction!(set_扩展线段模式, m)?)?;
m.add_function(wrap_pyfunction!(get_log_level, m)?)?;
m.add_function(wrap_pyfunction!(set_log_level, m)?)?;
m.add_function(wrap_pyfunction!(get_log_mode, m)?)?;
m.add_function(wrap_pyfunction!(set_log_mode, m)?)?;
m.add_function(wrap_pyfunction!(get_cache_mode, m)?)?;
m.add_function(wrap_pyfunction!(set_cache_mode, m)?)?;
// 阶段 1: 枚举和基础类型
types_py::register(m)?;
// 阶段 2: 配置
@@ -50,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 仍失败");
}
}
+94 -351
View File
@@ -24,42 +24,27 @@
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyType};
use std::collections::HashMap;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::RwLock;
use std::sync::atomic::Ordering;
use crate::algorithm_py::{hub_to_py, Py};
use crate::algorithm_py::hub_to_py;
use crate::config_py::Py;
use crate::kline_py::{K线Py, K线Py};
use crate::kline_py::{K线Py, bar_to_py, K线Py};
// ---- 身份缓存 (弱引用:通过 refcnt 检测存活,仅缓存持有则视为过期) ----
thread_local! {
static FRACTAL_IDENTITY: RwLock<HashMap<usize, Py<Py>>> = RwLock::new(HashMap::new());
static DASHED_IDENTITY: RwLock<HashMap<usize, Py<线Py>>> = RwLock::new(HashMap::new());
static SEGFEAT_IDENTITY: RwLock<HashMap<usize, Py<线Py>>> = RwLock::new(HashMap::new());
static FEATFRAC_IDENTITY: RwLock<HashMap<usize, Py<Py>>> = RwLock::new(HashMap::new());
}
// 缓存通过 crate::cache 模块管理(支持 thread_local / global 运行时切换)
pub(crate) fn fractal_to_py(
py: Python<'_>,
inner: Arc<chanlun::structure::fractal_obj::>,
) -> Py<Py> {
let key = Arc::as_ptr(&inner) as usize;
if let Some(cached) =
FRACTAL_IDENTITY.with(|c| c.read().unwrap().get(&key).map(|p| p.clone_ref(py)))
{
if let Some(cached) = crate::cache::fractal_get(py, key) {
return cached;
}
// 清理 refcnt==1 的过期条目(仅缓存持有,Python 侧已无引用)
FRACTAL_IDENTITY.with(|c| {
c.write().unwrap().retain(|_, v| v.get_refcnt(py) > 1);
});
let obj = Py::new(py, Py { inner }).unwrap();
FRACTAL_IDENTITY.with(|c| {
c.write().unwrap().insert(key, obj.clone_ref(py));
});
crate::cache::fractal_insert(py, key, &obj);
obj
}
@@ -68,18 +53,11 @@ pub(crate) fn dashed_to_py(
inner: Arc<chanlun::structure::dash_line::线>,
) -> Py<线Py> {
let key = Arc::as_ptr(&inner) as usize;
if let Some(cached) =
DASHED_IDENTITY.with(|c| c.read().unwrap().get(&key).map(|p| p.clone_ref(py)))
{
if let Some(cached) = crate::cache::dashed_get(py, key) {
return cached;
}
DASHED_IDENTITY.with(|c| {
c.write().unwrap().retain(|_, v| v.get_refcnt(py) > 1);
});
let obj = Py::new(py, 线Py { inner }).unwrap();
DASHED_IDENTITY.with(|c| {
c.write().unwrap().insert(key, obj.clone_ref(py));
});
crate::cache::dashed_insert(py, key, &obj);
obj
}
@@ -87,41 +65,9 @@ pub(crate) fn segfeat_to_py(
py: Python<'_>,
inner: Arc<chanlun::structure::segment_feat::线>,
) -> Py<线Py> {
let key = Arc::as_ptr(&inner) as usize;
if let Some(cached) =
SEGFEAT_IDENTITY.with(|c| c.read().unwrap().get(&key).map(|p| p.clone_ref(py)))
{
return cached;
}
SEGFEAT_IDENTITY.with(|c| {
c.write().unwrap().retain(|_, v| v.get_refcnt(py) > 1);
});
let obj = Py::new(py, 线Py { inner }).unwrap();
SEGFEAT_IDENTITY.with(|c| {
c.write().unwrap().insert(key, obj.clone_ref(py));
});
obj
Py::new(py, 线Py { inner }).unwrap()
}
pub(crate) fn featfrac_to_py(
py: Python<'_>,
inner: Arc<chanlun::structure::feat_fractal::>,
) -> Py<Py> {
let key = Arc::as_ptr(&inner) as usize;
if let Some(cached) =
FEATFRAC_IDENTITY.with(|c| c.read().unwrap().get(&key).map(|p| p.clone_ref(py)))
{
return cached;
}
FEATFRAC_IDENTITY.with(|c| {
c.write().unwrap().retain(|_, v| v.get_refcnt(py) > 1);
});
let obj = Py::new(py, Py { inner }).unwrap();
FEATFRAC_IDENTITY.with(|c| {
c.write().unwrap().insert(key, obj.clone_ref(py));
});
obj
}
use crate::types_py::{Py, Py, Py};
// ========== 分型 ==========
@@ -184,19 +130,35 @@ impl 分型Py {
}
#[getter]
fn (&self) -> Py {
Py {
inner: self.inner.,
}
fn (&self, py: Python<'_>) -> Py<Py> {
crate::types_py::(py, self.inner.())
}
#[getter]
fn (&self) -> i64 {
self.inner.
self.inner.()
}
#[getter]
fn (&self) -> f64 {
self.inner.()
}
#[getter]
/// 构造时缓存的 _结构(不受 分型模式 影响)
fn _结构(&self, py: Python<'_>) -> Py<Py> {
crate::types_py::(py, self.inner.)
}
#[getter]
/// 构造时缓存的 _时间戳(不受 分型模式 影响)
fn _时间戳(&self) -> i64 {
self.inner.
}
#[getter]
/// 构造时缓存的 _分型特征值(不受 分型模式 影响)
fn _分型特征值(&self) -> f64 {
self.inner.
}
@@ -221,12 +183,15 @@ impl 分型Py {
#[getter]
/// 左、中、右三对相对方向关系
fn (&self) -> Option<(Py, Py, Py)> {
fn (
&self,
py: Python<'_>,
) -> Option<(Py<Py>, Py<Py>, Py<Py>)> {
self.inner.().map(|(a, b, c)| {
(
Py { inner: a },
Py { inner: b },
Py { inner: c },
crate::types_py::(py, a),
crate::types_py::(py, b),
crate::types_py::(py, c),
)
})
}
@@ -247,7 +212,7 @@ impl 分型Py {
#[getter]
fn __dict__(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
let dict = PyDict::new(py);
dict.set_item("结构", self.())?;
dict.set_item("结构", self.(py))?;
dict.set_item("时间戳", self.())?;
dict.set_item("分型特征值", self.())?;
dict.set_item("强度", self.())?;
@@ -259,7 +224,7 @@ impl 分型Py {
if let Some(v) = self.(py) {
dict.set_item("", v)?;
}
if let Some(v) = self.() {
if let Some(v) = self.(py) {
dict.set_item("关系组", v)?;
}
Ok(dict.into())
@@ -377,7 +342,7 @@ impl 虚线Py {
#[getter]
fn (&self) -> String {
self.inner..read().unwrap().clone()
self.inner..read().clone()
}
#[getter]
@@ -397,7 +362,7 @@ impl 虚线Py {
#[getter]
fn (&self, py: Python<'_>) -> Py<Py> {
fractal_to_py(py, Arc::clone(&*self.inner..read().unwrap()))
fractal_to_py(py, Arc::clone(&*self.inner..read()))
}
#[getter]
@@ -407,18 +372,23 @@ impl 虚线Py {
#[getter]
fn (&self) -> String {
self.inner..read().unwrap().clone()
self.inner..read().clone()
}
#[getter]
fn _特征序列_显示(&self) -> bool {
#[getter(_特征序列_显示)]
fn get_特征序列_显示(&self) -> bool {
self.inner._特征序列_显示.load(Ordering::Relaxed)
}
#[setter(_特征序列_显示)]
fn set_特征序列_显示(&mut self, value: bool) {
self.inner._特征序列_显示.store(value, Ordering::Relaxed);
}
#[getter]
fn (&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let list = pyo3::types::PyList::empty(py);
for item in self.inner..read().unwrap().iter() {
for item in self.inner..read().iter() {
match item {
Some(feat) => list.append(segfeat_to_py(py, Arc::clone(feat)))?,
None => {
@@ -439,30 +409,22 @@ impl 虚线Py {
self.inner
.K线
.read()
.unwrap()
.as_ref()
.map(|k| crate::kline_py::chan_kline_to_py(py, Arc::clone(k)))
}
#[getter]
fn (&self) -> Option<Py> {
self.inner
.
.read()
.unwrap()
.map(|q| Py { inner: q })
self.inner..read().map(|q| Py { inner: q })
}
#[getter]
fn (&self) -> Option<Self> {
fn (&self, py: Python<'_>) -> Option<Py<线Py>> {
self.inner
.
.read()
.unwrap()
.as_ref()
.map(|d| Self {
inner: Arc::clone(d),
})
.map(|d| dashed_to_py(py, Arc::clone(d)))
}
// ---- 序列 getters ----
@@ -470,13 +432,8 @@ impl 虚线Py {
#[getter]
fn (&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let list = pyo3::types::PyList::empty(py);
for d in self.inner..read().unwrap().iter() {
list.append(Py::new(
py,
Self {
inner: Arc::clone(d),
},
)?)?;
for d in self.inner..read().iter() {
list.append(dashed_to_py(py, Arc::clone(d)))?;
}
Ok(list.into())
}
@@ -484,7 +441,7 @@ impl 虚线Py {
#[getter]
fn _中枢序列(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let list = pyo3::types::PyList::empty(py);
for h in self.inner._中枢序列.read().unwrap().iter() {
for h in self.inner._中枢序列.read().iter() {
list.append(hub_to_py(py, Arc::clone(h)))?;
}
Ok(list.into())
@@ -493,7 +450,7 @@ impl 虚线Py {
#[getter]
fn _中枢序列(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let list = pyo3::types::PyList::empty(py);
for h in self.inner._中枢序列.read().unwrap().iter() {
for h in self.inner._中枢序列.read().iter() {
list.append(hub_to_py(py, Arc::clone(h)))?;
}
Ok(list.into())
@@ -502,7 +459,7 @@ impl 虚线Py {
#[getter]
fn _中枢序列(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let list = pyo3::types::PyList::empty(py);
for h in self.inner._中枢序列.read().unwrap().iter() {
for h in self.inner._中枢序列.read().iter() {
list.append(hub_to_py(py, Arc::clone(h)))?;
}
Ok(list.into())
@@ -514,13 +471,8 @@ impl 虚线Py {
/// 笔序列
fn (&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let list = pyo3::types::PyList::empty(py);
for d in self.inner..read().unwrap().iter() {
list.append(Py::new(
py,
Self {
inner: Arc::clone(d),
},
)?)?;
for d in self.inner..read().iter() {
list.append(dashed_to_py(py, Arc::clone(d)))?;
}
Ok(list.into())
}
@@ -533,10 +485,8 @@ impl 虚线Py {
#[getter]
/// :return: 运行方向
fn (&self) -> Py {
Py {
inner: self.inner.(),
}
fn (&self, py: Python<'_>) -> Py<Py> {
crate::types_py::(py, self.inner.())
}
#[getter]
@@ -569,11 +519,10 @@ impl 虚线Py {
let obs_ref = .borrow();
let observer_inner = obs_ref.obs();
let result = self.inner.K序列(&observer_inner.K线序列);
let list = pyo3::types::PyList::empty(.py());
let py = .py();
let list = pyo3::types::PyList::empty(py);
for k in &result {
list.append(K线Py {
inner: Arc::clone(k),
})?;
list.append(bar_to_py(py, Arc::clone(k)))?;
}
Ok(list.into())
}
@@ -960,7 +909,7 @@ impl 虚线Py {
) -> (bool, String) {
let obs = .borrow();
let obs_ref = obs.obs();
chanlun::structure::dash_line::线::(&线.borrow().inner, &*obs_ref)
chanlun::structure::dash_line::线::(&线.borrow().inner, &obs_ref)
}
}
@@ -974,15 +923,10 @@ impl 虚线Py {
/// 文: 分型 — 特征序列的起点分型
/// 武: 分型 — 特征序列的终点分型
/// 方向: 相对方向 / 高: float / 低: float
///
/// 方法:
/// 添加(虚线) — 向特征序列追加虚线元素
/// 删除(虚线) — 从特征序列移除虚线元素
/// 基本序列: list[虚线] (基础序列)
///
/// 类方法:
/// 新建(序号, 文, 武, 基础序列?) -> 线段特征
/// 静态分析(虚线序列, 配置) -> 线段特征|None
/// 获取分型序列(虚线序列, 配置) -> list[线段特征]
/// :meth:`静态分析` — 对虚线序列进行静态特征分析,返回 线段特征 列表
#[pyclass(name = "线段特征", module = "chanlun._chanlun", from_py_object)]
#[derive(Clone)]
pub struct 线Py {
@@ -991,49 +935,36 @@ pub struct 线段特征Py {
#[pymethods]
impl 线Py {
#[new]
fn new(
: String,
: Vec<Py<线Py>>,
线: &Bound<'_, Py>,
py: Python<'_>,
) -> Self {
let rc_list: Vec<Arc<chanlun::structure::dash_line::线>> =
.iter()
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
.collect();
Self {
inner: Arc::new(chanlun::structure::segment_feat::线::new(
,
rc_list,
线.borrow().inner,
)),
}
}
// ---- getters ----
#[getter]
fn (&self) -> i64 {
self.inner.
self.inner..load(Ordering::Relaxed)
}
#[setter]
#[pyo3(name = "序号")]
fn set_序号(&self, value: i64) {
self.inner..store(value, Ordering::Relaxed);
}
#[getter]
fn (&self) -> String {
self.inner..clone()
self.inner..read().clone()
}
#[setter]
fn set_标识(&self, value: String) {
*self.inner..write() = value;
}
#[getter]
fn 线(&self) -> Py {
Py {
inner: self.inner.线,
}
fn 线(&self, py: Python<'_>) -> Py<Py> {
crate::types_py::(py, self.inner.线)
}
#[getter]
fn (&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
fn (&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let list = pyo3::types::PyList::empty(py);
for d in &self.inner. {
for d in &self.inner. {
list.append(dashed_to_py(py, Arc::clone(d)))?;
}
Ok(list.into())
@@ -1047,55 +978,6 @@ impl 线段特征Py {
self.__str__()
}
fn __len__(&self) -> usize {
self.inner..len()
}
fn __getitem__(&self, index: isize, py: Python<'_>) -> PyResult<Py<PyAny>> {
let len = self.inner..len() as isize;
let idx = if index < 0 { index + len } else { index };
if idx < 0 || idx >= len {
return Err(pyo3::exceptions::PyIndexError::new_err(format!(
"线段特征 index {index} out of range (len={len})"
)));
}
let dash = &self.inner.[idx as usize];
let obj: Py<PyAny> = dashed_to_py(py, Arc::clone(dash)).into();
Ok(obj)
}
fn __iter__(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let list = pyo3::types::PyList::empty(py);
for d in &self.inner. {
list.append(dashed_to_py(py, Arc::clone(d)))?;
}
list.call_method0("__iter__").map(|iter| iter.into())
}
fn __eq__(&self, other: &Bound<'_, PyAny>) -> bool {
if let Ok(other) = other.extract::<PyRef<'_, Self>>() {
return Arc::as_ptr(&self.inner) == Arc::as_ptr(&other.inner);
}
false
}
fn __hash__(&self) -> u64 {
Arc::as_ptr(&self.inner) as u64
}
/// pandas 兼容 — 返回关键标量字段构成的字典
#[getter]
fn __dict__(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
let dict = PyDict::new(py);
dict.set_item("序号", self.())?;
dict.set_item("标识", self.())?;
dict.set_item("线段方向", self.线())?;
dict.set_item("图表标题", self.())?;
Ok(dict.into())
}
// ---- instance methods ----
#[getter]
/// :return: 图表标题
fn (&self) -> String {
@@ -1116,10 +998,8 @@ impl 线段特征Py {
#[getter]
/// :return: 特征序列方向(线段方向的翻转)
fn (&self) -> Py {
Py {
inner: self.inner.(),
}
fn (&self, py: Python<'_>) -> Py<Py> {
crate::types_py::(py, self.inner.())
}
#[getter]
@@ -1134,47 +1014,8 @@ impl 线段特征Py {
self.inner.()
}
/// :param 待添加虚线: 待添加的虚线
fn (&mut self, 线: &Bound<'_, 线Py>) -> PyResult<()> {
let inner = Arc::make_mut(&mut self.inner);
inner
.(Arc::clone(&线.borrow().inner))
.map_err(|e| pyo3::exceptions::PyValueError::new_err(e))
}
/// :param 待删除虚线: 待删除的虚线
fn (&mut self, 线: &Bound<'_, 线Py>) -> PyResult<()> {
let inner = Arc::make_mut(&mut self.inner);
inner
.(&Arc::clone(&线.borrow().inner))
.map_err(|e| pyo3::exceptions::PyValueError::new_err(e))
}
// ---- classmethods ----
#[classmethod]
/// :param 虚线序列: 基础虚线列表
fn (
_cls: &Bound<'_, PyType>,
线: Vec<Py<线Py>>,
线: &Bound<'_, Py>,
py: Python<'_>,
) -> Self {
let rc_list: Vec<Arc<chanlun::structure::dash_line::线>> = 线
.iter()
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
.collect();
Self {
inner: Arc::new(chanlun::structure::segment_feat::线::(
rc_list,
线.borrow().inner,
)),
}
}
#[classmethod]
#[pyo3(signature = (虚线序列, 线段方向, 四象, 是否忽视 = false))]
/// 静态分析虚线序列,生成特征序列
fn (
_cls: &Bound<'_, PyType>,
线: Vec<Py<线Py>>,
@@ -1182,119 +1023,22 @@ impl 线段特征Py {
: &str,
: bool,
py: Python<'_>,
) -> Vec<Self> {
) -> PyResult<Py<PyAny>> {
let rc_list: Vec<Arc<chanlun::structure::dash_line::线>> = 线
.iter()
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
.collect();
chanlun::structure::segment_feat::线::(
let result = chanlun::structure::segment_feat::线::(
&rc_list,
线.borrow().inner,
,
,
)
.into_iter()
.map(|inner| Self { inner })
.collect()
}
#[classmethod]
/// 从特征序列提取特征分型序列
fn (
_cls: &Bound<'_, PyType>,
: Vec<Py<Self>>,
py: Python<'_>,
) -> Vec<Py<Py>> {
let rc_list: Vec<Arc<chanlun::structure::segment_feat::线>> =
.iter()
.map(|s| Arc::clone(&s.bind(py).borrow().inner))
.collect();
chanlun::structure::segment_feat::线::(&rc_list)
.into_iter()
.map(|inner| featfrac_to_py(py, Arc::new(inner)))
.collect()
}
}
// ========== 特征分型 ==========
/// 特征分型 — 线段特征序列中的分型节点。
///
/// 属性 (只读):
/// 左: 线段特征|None / 中: 线段特征 / 右: 线段特征|None
/// 结构: 分型结构 — 顶/底分型判定结果
#[pyclass(name = "特征分型", module = "chanlun._chanlun", from_py_object)]
#[derive(Clone)]
pub struct Py {
pub(crate) inner: Arc<chanlun::structure::feat_fractal::>,
}
#[pymethods]
impl Py {
#[new]
fn new(
: &Bound<'_, 线Py>,
: &Bound<'_, 线Py>,
: &Bound<'_, 线Py>,
: &Bound<'_, Py>,
) -> Self {
Self {
inner: Arc::new(chanlun::structure::feat_fractal::::new(
Arc::clone(&.borrow().inner),
Arc::clone(&.borrow().inner),
Arc::clone(&.borrow().inner),
.borrow().inner,
)),
);
let list = pyo3::types::PyList::empty(py);
for sf in result {
list.append(segfeat_to_py(py, sf))?;
}
}
#[getter]
fn (&self, py: Python<'_>) -> Py<线Py> {
segfeat_to_py(py, Arc::clone(&self.inner.))
}
#[getter]
fn (&self, py: Python<'_>) -> Py<线Py> {
segfeat_to_py(py, Arc::clone(&self.inner.))
}
#[getter]
fn (&self, py: Python<'_>) -> Py<线Py> {
segfeat_to_py(py, Arc::clone(&self.inner.))
}
#[getter]
fn (&self) -> Py {
Py {
inner: self.inner.,
}
}
/// pandas 兼容 — 返回关键标量字段构成的字典
#[getter]
fn __dict__(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
let dict = PyDict::new(py);
dict.set_item("结构", self.())?;
Ok(dict.into())
}
fn __str__(&self) -> String {
format!("{}", self.inner)
}
fn __repr__(&self) -> String {
self.__str__()
}
fn __eq__(&self, other: &Bound<'_, PyAny>) -> bool {
if let Ok(other) = other.extract::<PyRef<'_, Self>>() {
return Arc::as_ptr(&self.inner) == Arc::as_ptr(&other.inner);
}
false
}
fn __hash__(&self) -> u64 {
Arc::as_ptr(&self.inner) as u64
Ok(list.into())
}
}
@@ -1302,6 +1046,5 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<Py>()?;
m.add_class::<线Py>()?;
m.add_class::<线Py>()?;
m.add_class::<Py>()?;
Ok(())
}
+218 -93
View File
@@ -22,9 +22,78 @@
* SOFTWARE.
*/
use parking_lot::Mutex;
use std::collections::HashMap;
use pyo3::basic::CompareOp;
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyType};
use pyo3::types::{PyBool, PyDict, PyType};
// ========== 单例缓存 ==========
static _单例缓存: Mutex<Option<HashMap<u8, Py<Py>>>> = Mutex::new(None);
pub fn (
py: Python<'_>,
inner: chanlun::types::,
) -> Py<Py> {
let mut guard = _单例缓存.lock();
if let Some(ref map) = *guard {
return map[&(inner as u8)].clone_ref(py);
}
// 首次访问时从类属性加载单例
let module = py.import("chanlun._chanlun").unwrap();
let class = module.getattr("分型结构").unwrap();
let mut map = HashMap::new();
for (name, variant) in &[
("", chanlun::types::::),
("", chanlun::types::::),
("", chanlun::types::::),
("", chanlun::types::::),
("", chanlun::types::::),
] {
let instance: Py<Py> = class.getattr(*name).unwrap().extract().unwrap();
map.insert(*variant as u8, instance);
}
let result = map[&(inner as u8)].clone_ref(py);
*guard = Some(map);
result
}
static _单例缓存: Mutex<Option<HashMap<u8, Py<Py>>>> = Mutex::new(None);
pub fn (
py: Python<'_>,
inner: chanlun::types::,
) -> Py<Py> {
let mut guard = _单例缓存.lock();
if let Some(ref map) = *guard {
return map[&(inner as u8)].clone_ref(py);
}
// 首次访问时从类属性加载单例
let module = py.import("chanlun._chanlun").unwrap();
let class = module.getattr("相对方向").unwrap();
let mut map = HashMap::new();
for (name, variant) in &[
("向上", chanlun::types::::),
("向下", chanlun::types::::),
("向上缺口", chanlun::types::::),
("向下缺口", chanlun::types::::),
("衔接向上", chanlun::types::::),
("衔接向下", chanlun::types::::),
("", chanlun::types::::),
("", chanlun::types::::),
("", chanlun::types::::),
] {
let instance: Py<Py> = class.getattr(*name).unwrap().extract().unwrap();
map.insert(*variant as u8, instance);
}
let result = map[&(inner as u8)].clone_ref(py);
*guard = Some(map);
result
}
// ========== 买卖点类型 ==========
@@ -54,18 +123,22 @@ impl 买卖点类型Py {
self.inner.to_string()
}
fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult<bool> {
fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult<Py<PyAny>> {
let py = other.py();
// 比较:先尝试字符串(Python Enum(str)),再同类型,再 name 属性
let eq = if let Ok(s) = other.extract::<String>() {
self.inner.to_string() == s
} else if let Ok(other) = other.extract::<PyRef<'_, Self>>() {
self.inner == other.inner
} else if let Ok(py_name) = other.getattr("name").and_then(|n| n.extract::<String>()) {
self.inner.to_string() == py_name
} else {
return Err(pyo3::exceptions::PyNotImplementedError::new_err(""));
return Ok(py.NotImplemented());
};
match op {
CompareOp::Eq => Ok(eq),
CompareOp::Ne => Ok(!eq),
_ => Err(pyo3::exceptions::PyNotImplementedError::new_err("")),
CompareOp::Eq => Ok(PyBool::new(py, eq).as_any().to_owned().unbind()),
CompareOp::Ne => Ok(PyBool::new(py, !eq).as_any().to_owned().unbind()),
_ => Ok(py.NotImplemented()),
}
}
@@ -134,19 +207,44 @@ impl 相对方向Py {
format!("{}", self.inner)
}
fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult<bool> {
let Ok(other) = other.extract::<PyRef<'_, Self>>() else {
return Err(pyo3::exceptions::PyNotImplementedError::new_err(""));
};
let eq = self.inner == other.inner;
match op {
CompareOp::Eq => Ok(eq),
CompareOp::Ne => Ok(!eq),
CompareOp::Lt => Ok((self.inner as u8) < (other.inner as u8)),
CompareOp::Le => Ok((self.inner as u8) <= (other.inner as u8)),
CompareOp::Gt => Ok((self.inner as u8) > (other.inner as u8)),
CompareOp::Ge => Ok((self.inner as u8) >= (other.inner as u8)),
fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult<Py<PyAny>> {
let py = other.py();
// 同类型比较
if let Ok(other) = other.extract::<PyRef<'_, Self>>() {
let eq = self.inner == other.inner;
return Ok(match op {
CompareOp::Eq => PyBool::new(py, eq).as_any().to_owned().unbind(),
CompareOp::Ne => PyBool::new(py, !eq).as_any().to_owned().unbind(),
CompareOp::Lt => PyBool::new(py, (self.inner as u8) < (other.inner as u8))
.as_any()
.to_owned()
.unbind(),
CompareOp::Le => PyBool::new(py, (self.inner as u8) <= (other.inner as u8))
.as_any()
.to_owned()
.unbind(),
CompareOp::Gt => PyBool::new(py, (self.inner as u8) > (other.inner as u8))
.as_any()
.to_owned()
.unbind(),
CompareOp::Ge => PyBool::new(py, (self.inner as u8) >= (other.inner as u8))
.as_any()
.to_owned()
.unbind(),
});
}
// 跨模块比较:通过 name 属性匹配 Python Enum(如 chan.chan.相对方向)
if let Ok(py_name) = other.getattr("name").and_then(|n| n.extract::<String>()) {
let self_name = format!("{:?}", self.inner);
let eq = self_name == py_name;
return Ok(match op {
CompareOp::Eq => PyBool::new(py, eq).as_any().to_owned().unbind(),
CompareOp::Ne => PyBool::new(py, !eq).as_any().to_owned().unbind(),
_ => py.NotImplemented(),
});
}
// 回退:返回 Python NotImplemented
Ok(py.NotImplemented())
}
fn __hash__(&self) -> u64 {
@@ -154,10 +252,8 @@ impl 相对方向Py {
}
/// 返回方向的对立面(向上↔向下, 缺口↔反向缺口, 衔接↔反向衔接)。
fn (&self) -> Self {
Self {
inner: self.inner.(),
}
fn (&self, py: Python<'_>) -> Py<Self> {
(py, self.inner.())
}
/// 判断是否为向上方向(向上/向上缺口/衔接向上)
@@ -210,10 +306,27 @@ impl 相对方向Py {
#[classmethod]
fn (
_cls: &Bound<'_, PyType>, : f64, : f64, : f64, : f64
) -> Self {
Self {
inner: chanlun::types::::(, , , ),
}
) -> Py<Self> {
(
_cls.py(),
chanlun::types::::(, , , ),
)
}
/// 从可选方向序列中随机选取指定数量
#[classmethod]
#[pyo3(signature = (数量, 可选方向, 可重复 = true))]
fn (
_cls: &Bound<'_, PyType>,
: usize,
: Vec<Py<Self>>,
: bool,
py: Python<'_>,
) -> Vec<Py<Self>> {
let dirs: Vec<chanlun::types::> =
.iter().map(|d| d.borrow(py).inner).collect();
let result = chanlun::types::::(, &dirs, );
result.iter().map(|d| (py, *d)).collect()
}
}
@@ -247,19 +360,43 @@ impl 分型结构Py {
self.inner.to_string()
}
fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult<bool> {
let Ok(other) = other.extract::<PyRef<'_, Self>>() else {
return Err(pyo3::exceptions::PyNotImplementedError::new_err(""));
};
let eq = self.inner == other.inner;
match op {
CompareOp::Eq => Ok(eq),
CompareOp::Ne => Ok(!eq),
CompareOp::Lt => Ok((self.inner as u8) < (other.inner as u8)),
CompareOp::Le => Ok((self.inner as u8) <= (other.inner as u8)),
CompareOp::Gt => Ok((self.inner as u8) > (other.inner as u8)),
CompareOp::Ge => Ok((self.inner as u8) >= (other.inner as u8)),
fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult<Py<PyAny>> {
let py = other.py();
// 同类型比较
if let Ok(other) = other.extract::<PyRef<'_, Self>>() {
let eq = self.inner == other.inner;
return Ok(match op {
CompareOp::Eq => PyBool::new(py, eq).as_any().to_owned().unbind(),
CompareOp::Ne => PyBool::new(py, !eq).as_any().to_owned().unbind(),
CompareOp::Lt => PyBool::new(py, (self.inner as u8) < (other.inner as u8))
.as_any()
.to_owned()
.unbind(),
CompareOp::Le => PyBool::new(py, (self.inner as u8) <= (other.inner as u8))
.as_any()
.to_owned()
.unbind(),
CompareOp::Gt => PyBool::new(py, (self.inner as u8) > (other.inner as u8))
.as_any()
.to_owned()
.unbind(),
CompareOp::Ge => PyBool::new(py, (self.inner as u8) >= (other.inner as u8))
.as_any()
.to_owned()
.unbind(),
});
}
// 跨模块比较:通过 name 属性匹配 Python Enum(如 chan.chan.分型结构)
if let Ok(py_name) = other.getattr("name").and_then(|n| n.extract::<String>()) {
let self_name = self.inner.to_string();
let eq = self_name == py_name;
return Ok(match op {
CompareOp::Eq => PyBool::new(py, eq).as_any().to_owned().unbind(),
CompareOp::Ne => PyBool::new(py, !eq).as_any().to_owned().unbind(),
_ => py.NotImplemented(),
});
}
Ok(py.NotImplemented())
}
fn __hash__(&self) -> u64 {
@@ -306,41 +443,17 @@ impl 分型结构Py {
let (, ) = get_hl()?;
let (, ) = get_hl()?;
let = chanlun::types::::(, , , );
let = chanlun::types::::(, , , );
let = |d: chanlun::types::| d.();
let = |d: chanlun::types::| d.();
let result = match (, ) {
(d1, d2) if matches!(d1, chanlun::types::::) && ! => {
panic!("顺序包含: {:?} {:?}", d1, d2);
}
(d1, d2) if matches!(d2, chanlun::types::::) && ! => {
panic!("顺序包含: {:?} {:?}", d1, d2);
}
(a, b) if (a) && (b) => chanlun::types::::,
(a, b) if (a) && (b) => chanlun::types::::,
(a, chanlun::types::::) if (a) && => {
chanlun::types::::
}
(a, b) if (a) && (b) => chanlun::types::::,
(a, b) if (a) && (b) => chanlun::types::::,
(a, chanlun::types::::) if (a) && => {
chanlun::types::::
}
(chanlun::types::::, a) if (a) && => {
chanlun::types::::
}
(chanlun::types::::, a) if (a) && => {
chanlun::types::::
}
(chanlun::types::::, chanlun::types::::) if => {
chanlun::types::::
}
_ => return Ok(None),
};
Ok(Some(Self { inner: result }))
Ok(chanlun::types::::_内部(
,
,
,
,
,
,
,
,
)
.map(|inner| Self { inner }))
}
}
@@ -383,23 +496,35 @@ impl 缺口Py {
format!("{}", self.inner)
}
fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult<bool> {
fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult<Py<PyAny>> {
let py = other.py();
let Ok(other) = other.extract::<PyRef<'_, Self>>() else {
return Err(pyo3::exceptions::PyNotImplementedError::new_err(""));
return Ok(py.NotImplemented());
};
let to_bool = |b: bool| PyBool::new(py, b).as_any().to_owned().unbind();
match op {
CompareOp::Eq => Ok(self.inner. == other.inner. && self.inner. == other.inner.),
CompareOp::Ne => {
Ok(!(self.inner. == other.inner. && self.inner. == other.inner.))
}
CompareOp::Lt => Ok(self.inner. < other.inner.
|| (self.inner. == other.inner. && self.inner. < other.inner.)),
CompareOp::Le => Ok(self.inner. < other.inner.
|| (self.inner. == other.inner. && self.inner. <= other.inner.)),
CompareOp::Gt => Ok(self.inner. > other.inner.
|| (self.inner. == other.inner. && self.inner. > other.inner.)),
CompareOp::Ge => Ok(self.inner. > other.inner.
|| (self.inner. == other.inner. && self.inner. >= other.inner.)),
CompareOp::Eq => Ok(to_bool(
self.inner. == other.inner. && self.inner. == other.inner.,
)),
CompareOp::Ne => Ok(to_bool(
!(self.inner. == other.inner. && self.inner. == other.inner.),
)),
CompareOp::Lt => Ok(to_bool(
self.inner. < other.inner.
|| (self.inner. == other.inner. && self.inner. < other.inner.),
)),
CompareOp::Le => Ok(to_bool(
self.inner. < other.inner.
|| (self.inner. == other.inner. && self.inner. <= other.inner.),
)),
CompareOp::Gt => Ok(to_bool(
self.inner. > other.inner.
|| (self.inner. == other.inner. && self.inner. > other.inner.),
)),
CompareOp::Ge => Ok(to_bool(
self.inner. > other.inner.
|| (self.inner. == other.inner. && self.inner. >= other.inner.),
)),
}
}
@@ -477,7 +602,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
// 买卖点类型 class attributes (singleton instances)
let py = m.py();
let bsp_class = m.getattr("买卖点类型")?;
let bsp_class = bsp_class.downcast_into::<PyType>()?;
let bsp_class = bsp_class.cast_into::<PyType>()?;
let variants: &[(&str, chanlun::types::)] = &[
("一买", chanlun::types::::),
@@ -500,7 +625,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
("T3B卖", chanlun::types::::T3B卖),
];
let mut bsp_members = PyDict::new(py);
let bsp_members = PyDict::new(py);
for (name, value) in variants {
let instance = Py::new(py, Py { inner: *value })?;
bsp_class.setattr(*name, instance.clone_ref(py))?;
@@ -509,7 +634,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
bsp_class.setattr("__members__", bsp_members)?;
// 相对方向 class attributes
let dir_class = m.getattr("相对方向")?.downcast_into::<PyType>()?.clone();
let dir_class = m.getattr("相对方向")?.cast_into::<PyType>()?.clone();
let dir_variants: &[(&str, chanlun::types::)] = &[
("向上", chanlun::types::::),
("向下", chanlun::types::::),
@@ -522,7 +647,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
("", chanlun::types::::),
];
let mut dir_members = PyDict::new(py);
let dir_members = PyDict::new(py);
for (name, value) in dir_variants {
let instance = Py::new(py, Py { inner: *value })?;
dir_class.setattr(*name, instance.clone_ref(py))?;
@@ -531,7 +656,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
dir_class.setattr("__members__", dir_members)?;
// 分型结构 class attributes
let frac_class = m.getattr("分型结构")?.downcast_into::<PyType>()?.clone();
let frac_class = m.getattr("分型结构")?.cast_into::<PyType>()?.clone();
let frac_variants: &[(&str, chanlun::types::)] = &[
("", chanlun::types::::),
("", chanlun::types::::),
@@ -540,7 +665,7 @@ pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
("", chanlun::types::::),
];
let mut frac_members = PyDict::new(py);
let frac_members = PyDict::new(py);
for (name, value) in frac_variants {
let instance = Py::new(py, Py { inner: *value })?;
frac_class.setattr(*name, instance.clone_ref(py))?;
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
+25
View File
@@ -0,0 +1,25 @@
"""pyo3_test_helpers — 可复用的 PyO3 测试工具包。
提供四个核心模块:
rc_identity Rc/Arc 指针身份一致性测试 Mixin
subclass PyO3 #[pyclass(subclass)] 子类化兼容性测试 Mixin
type_shape 返回值类型形状验证工具
api_consistency 两个模块间 API 描述符类型一致性测试 Mixin
所有 Mixin 都是纯 Python不依赖 pytest unittest.TestCase 配合使用
下游项目复制此目录即可复用
"""
from .api_consistency import ApiConsistencyMixin
from .rc_identity import RcIdentityMixin
from .subclass import PyO3SubclassMixin
from .type_shape import assert_type_shape, TypeShapeAssertions
__all__ = [
"ApiConsistencyMixin",
"RcIdentityMixin",
"PyO3SubclassMixin",
"assert_type_shape",
"TypeShapeAssertions",
]
+196
View File
@@ -0,0 +1,196 @@
"""API 一致性测试 Mixin。
验证两个模块中同名类的公开成员描述符类型一致
典型用途对比 Python 参考实现 (chan.py) Rust/PyO3 移植 (chanlun) API 兼容性
用法::
class TestApi一致性(ApiConsistencyMixin, unittest.TestCase):
reference_module = mylib.ref # Python 参考实现
target_module = mylib # Rust/PyO3 移植
# 可选: 已知差异(不会报错)
known_missing_in_target = {
"SomeClass": {"old_deprecated_method"},
}
known_descriptor_diffs = {
# (class_name, member, ref_type, target_type)
}
# 可选: 成员名过滤(匹配则跳过,支持前缀用 "prefix_" 表示)
noise_filters = ["model_", "parse_", "from_orm"]
"""
import unittest
def _classify_member(cls, attr_name):
"""返回描述符类型: property / classmethod / staticmethod / regular_method / None(data)."""
# 优先检查元类字典中的描述符
for klass in type(cls).__mro__:
if attr_name in klass.__dict__:
raw = klass.__dict__[attr_name]
if isinstance(raw, property):
return "property"
elif isinstance(raw, classmethod):
return "classmethod"
elif isinstance(raw, staticmethod):
return "staticmethod"
break
try:
attr = getattr(cls, attr_name)
except Exception:
return None
if callable(attr):
return "regular_method"
return None
def _is_noise(name, filters):
for pat in filters:
if pat == name:
return True
if pat.endswith("_") and name.startswith(pat):
return True
return False
def _get_classes(mod):
"""获取模块中所有公开的 type."""
return {n: getattr(mod, n) for n in dir(mod) if not n.startswith("_") and isinstance(getattr(mod, n), type)}
class ApiConsistencyMixin:
"""API 一致性测试 Mixin。
子类必须定义:
reference_module: 参考模块 (Python 实现)
target_module: 目标模块 (Rust/PyO3 移植)
子类可选定义:
known_missing_in_target: dict[str, set[str]] 已知 target 中缺失的成员
known_descriptor_diffs: set[tuple] 已知描述符类型差异
noise_filters: list[str] 噪音成员名过滤
"""
reference_module = None
target_module = None
known_missing_in_target: dict = {}
known_descriptor_diffs: set = set()
noise_filters: list = []
@classmethod
def setUpClass(cls):
if cls.reference_module is None or cls.target_module is None:
raise unittest.SkipTest(f"{cls.__name__} 未定义 reference_module / target_module")
# ---- 描述符类型一致性 ----
def test_共有成员描述符类型一致(self):
"""同名类的同名成员,描述符类型 (property/classmethod/staticmethod/regular) 一致."""
ref_classes = _get_classes(self.reference_module)
tgt_classes = _get_classes(self.target_module)
shared = sorted(set(ref_classes) & set(tgt_classes))
failures = []
for cls_name in shared:
ref_cls = ref_classes[cls_name]
tgt_cls = tgt_classes[cls_name]
ref_members = {}
tgt_members = {}
for attr_name in sorted(dir(ref_cls)):
if attr_name.startswith("_") or _is_noise(attr_name, self.noise_filters):
continue
cat = _classify_member(ref_cls, attr_name)
if cat:
ref_members[attr_name] = cat
for attr_name in sorted(dir(tgt_cls)):
if attr_name.startswith("_") or _is_noise(attr_name, self.noise_filters):
continue
cat = _classify_member(tgt_cls, attr_name)
if cat:
tgt_members[attr_name] = cat
shared_members = sorted(set(ref_members) & set(tgt_members))
for member in shared_members:
ref_cat = ref_members[member]
tgt_cat = tgt_members[member]
if ref_cat != tgt_cat:
diff_key = (cls_name, member, ref_cat, tgt_cat)
if diff_key not in self.known_descriptor_diffs:
failures.append(f"{cls_name}.{member}: ref={ref_cat}, tgt={tgt_cat}")
if failures:
self.fail("描述符类型不一致:\n " + "\n ".join(failures))
# ---- 缺失成员检查 ----
def test_参考模块成员在目标模块中存在(self):
"""chan 中的关键公开成员在 chanlun 中均有对应."""
ref_classes = _get_classes(self.reference_module)
tgt_classes = _get_classes(self.target_module)
shared = sorted(set(ref_classes) & set(tgt_classes))
failures = []
for cls_name in shared:
if cls_name not in self.known_missing_in_target:
continue
ref_cls = ref_classes[cls_name]
tgt_cls = tgt_classes[cls_name]
expected_missing = self.known_missing_in_target.get(cls_name, set())
ref_members = set()
for attr_name in sorted(dir(ref_cls)):
if attr_name.startswith("_") or _is_noise(attr_name, self.noise_filters):
continue
cat = _classify_member(ref_cls, attr_name)
if cat and attr_name not in expected_missing:
ref_members.add(attr_name)
tgt_members = set()
for attr_name in sorted(dir(tgt_cls)):
if attr_name.startswith("_") or _is_noise(attr_name, self.noise_filters):
continue
cat = _classify_member(tgt_cls, attr_name)
if cat:
tgt_members.add(attr_name)
missing = ref_members - tgt_members - expected_missing
for member in sorted(missing):
failures.append(f"{cls_name}.{member}: ref={_classify_member(ref_cls, member)}, tgt=未导出")
if failures:
self.fail("参考模块中的成员在目标模块中缺失:\n " + "\n ".join(failures))
# ---- 方法可调用性 ----
def test_共有方法均可调用(self):
"""所有共有 regular_method 在两边都是 callable."""
ref_classes = _get_classes(self.reference_module)
tgt_classes = _get_classes(self.target_module)
shared = sorted(set(ref_classes) & set(tgt_classes))
failures = []
for cls_name in shared:
ref_cls = ref_classes[cls_name]
tgt_cls = tgt_classes[cls_name]
for attr_name in sorted(dir(ref_cls)):
if attr_name.startswith("_") or _is_noise(attr_name, self.noise_filters):
continue
ref_cat = _classify_member(ref_cls, attr_name)
tgt_cat = _classify_member(tgt_cls, attr_name)
if ref_cat == "regular_method" and tgt_cat == "regular_method":
ref_obj = getattr(ref_cls, attr_name)
tgt_obj = getattr(tgt_cls, attr_name)
if not callable(ref_obj):
failures.append(f"{cls_name}.{attr_name}: ref 不是 callable")
if not callable(tgt_obj):
failures.append(f"{cls_name}.{attr_name}: tgt 不是 callable")
if failures:
self.fail("方法不可调用:\n " + "\n ".join(failures))
+126
View File
@@ -0,0 +1,126 @@
"""Rc/Arc 指针身份一致性测试 Mixin。
验证同一个 Rust Rc<T>/Arc<T> 无论通过哪条路径到达 Python
始终返回相同的 PyObject`a is b` True
用法::
class TestMyLib(RcIdentityMixin, unittest.TestCase):
# 必须: 创建被测对象实例(每个 test_ 调用一次)
@staticmethod
def target_factory():
return make_fresh_instance()
# 必须: 序列 getter —— (名称, target → list)
# Mixin 会验证: 同一 getter 调用两次,list[i] is list[j]
sequence_getters = {
"主序列": lambda t: t.items,
"子序列": lambda t: t.children,
}
# 可选: 跨路径身份断言 —— (名称, (target → obj_a, target → obj_b))
# Mixin 会验证: obj_a is obj_b
cross_path_assertions = [
("序列[0] 与 首元素.父", lambda t: t.items[0], lambda t: t.items[0].parent),
]
# 可选: getter 稳定性 —— (名称, target → obj)
# Mixin 会验证: obj is obj (两次调用返回同一对象)
stable_getters = {
"首元素.属性": lambda t: t.items[0].attr,
}
# 可选: 序列长度检查的最小值(默认不检查,设为 >0 开启)
min_sequence_lengths = {
"主序列": 3,
"子序列": 2,
}
"""
import unittest
class RcIdentityMixin:
"""Rc/Arc 指针身份一致性测试 Mixin。
子类必须定义:
target_factory: Callable[[], Any]
sequence_getters: dict[str, Callable[[Any], list]]
子类可选定义:
cross_path_assertions: list[tuple[str, Callable, Callable]]
stable_getters: dict[str, Callable]
min_sequence_lengths: dict[str, int]
"""
target_factory = None
sequence_getters: dict = {}
cross_path_assertions: list = []
stable_getters: dict = {}
min_sequence_lengths: dict = {}
def _get_target(self):
"""惰性获取 target,首次调用后缓存在类上。避免 setUpClass MRO 冲突."""
cls = type(self)
# 每次测试重新创建——但这会太慢。用类级别缓存。
# 子类应在 setUpClass 中调用 self._get_target() 或自己设置 cls._cached_target。
if not hasattr(cls, "_cached_target"):
if cls.target_factory is None:
raise unittest.SkipTest(f"{cls.__name__} 未定义 target_factory")
cls._cached_target = cls.target_factory()
return cls._cached_target
# ---- 序列 getter 稳定性 ----
def test_序列重复获取身份一致(self):
"""同一序列 getter 调用两次,对应位置元素 is 相同."""
t = self._get_target()
for name, getter in self.sequence_getters.items():
seq1 = getter(t)
seq2 = getter(t)
self.assertEqual(len(seq1), len(seq2), f"{name}: 两次获取长度不同")
check_n = min(len(seq1), 10)
for i in range(check_n):
self.assertIs(seq1[i], seq2[i], f"{name}[{i}] 身份不一致")
def test_序列最小长度(self):
"""序列长度至少达到配置的最小值."""
t = self._get_target()
for name, getter in self.sequence_getters.items():
if name in self.min_sequence_lengths:
min_len = self.min_sequence_lengths[name]
actual = len(getter(t))
self.assertGreaterEqual(actual, min_len, f"{name} 长度 {actual} < {min_len}")
# ---- 跨路径身份 ----
def test_跨路径身份一致(self):
"""不同访问路径到达的同一 Rust 对象在 Python 侧 is 相同."""
t = self._get_target()
for i, (label, path_a, path_b) in enumerate(self.cross_path_assertions):
obj_a = path_a(t)
obj_b = path_b(t)
self.assertIsNotNone(obj_a, f"[{i}] {label}: path_a 返回 None")
self.assertIsNotNone(obj_b, f"[{i}] {label}: path_b 返回 None")
self.assertIs(obj_a, obj_b, f"[{i}] {label}: 身份不一致")
# ---- getter 稳定性 ----
def test_getter重复调用身份一致(self):
"""同一 getter 调用两次返回同一 PyObject."""
t = self._get_target()
for name, getter in self.stable_getters.items():
obj1 = getter(t)
obj2 = getter(t)
self.assertIs(obj1, obj2, f"{name}: 两次调用返回不同对象")
# ---- list.index 基于 is ----
def test_list_index_基于身份(self):
"""list.index(elem) 正常工作(依赖 __eq__ 基于 is 比较)."""
t = self._get_target()
for name, getter in self.sequence_getters.items():
seq = getter(t)
if len(seq) >= 2:
self.assertEqual(seq.index(seq[0]), 0, f"{name}: index(seq[0]) != 0")
self.assertEqual(seq.index(seq[-1]), len(seq) - 1, f"{name}: index(seq[-1]) != {len(seq) - 1}")
+247
View File
@@ -0,0 +1,247 @@
"""PyO3 #[pyclass(subclass)] 子类化兼容性测试 Mixin。
验证: Python 端可以正常子类化 PyO3 导出的类__new__/__init__ 协作
super() 委托MRO property/method 重写等全部正确
用法::
class TestMyObserver(PyO3SubclassMixin, unittest.TestCase):
base_class = mylib.Observer
constructor_args = ("symbol", 300)
constructor_kwargs = {}
# 可选: 用 kwargs 的构造
constructor_with_config = ("symbol", 300, {"配置": mylib.Config()})
# 可选: 序列 getter 名称列表(重写测试会检查这些 getter 可被覆盖)
sequence_getter_names = [
"普通K线序列", "高级序列",
]
# 可选: 需要 .nb 数据文件才能运行的测试会检查这个
@staticmethod
def has_data_file():
return os.path.isfile("data.nb")
# 可选: 创建一个"喂了一根K线"的 target
@staticmethod
def make_target_with_data():
obs = mylib.Observer("sym", 300)
k = mylib.KLine(...)
obs.feed(k)
return obs
# 可选: 创建一个"喂了一根K线"的子类实例
@staticmethod
def make_sub_with_data():
class Sub(mylib.Observer):
pass
obs = Sub("sym", 300)
k = mylib.KLine(...)
obs.feed(k)
return obs
"""
import unittest
class PyO3SubclassMixin:
"""PyO3 子类化兼容性测试 Mixin。
子类必须定义:
base_class: type
constructor_args: tuple
constructor_kwargs: dict
子类可选定义:
sequence_getter_names: list[str]
has_data_file: Callable[[], bool]
make_target_with_data: Callable[[], Any]
make_sub_with_data: Callable[[], Any]
make_data_item: Callable[[], Any] # 创建一根可喂入的数据项
feed_method_name: str # 喂数据的方法名,默认 "增加原始K线"
property_getters: list[str] # 需要逐一下覆写的 property 名
method_overrides: list[str] # 需要逐一重写的方法名
"""
base_class: type = None
constructor_args: tuple = ()
constructor_kwargs: dict = {}
sequence_getter_names: list = []
# 可选 hooks
has_data_file = None
make_target_with_data = None
make_sub_with_data = None
make_data_item = None
feed_method_name = "增加原始K线"
property_getters: list = []
method_overrides: list = []
@classmethod
def setUpClass(cls):
if cls.base_class is None:
raise unittest.SkipTest(f"{cls.__name__} 未定义 base_class")
# ---- 基础子类化 ----
def test_子类可实例化(self):
"""子类可创建,isinstance 正确."""
Base = self.base_class
class Sub(Base):
pass
obs = Sub(*self.constructor_args, **self.constructor_kwargs)
self.assertIsInstance(obs, Base)
self.assertEqual(type(obs).__name__, "Sub")
def test_子类_init_可添加自定义属性(self):
"""子类 __init__ 可添加自定义属性,基类字段不受影响."""
Base = self.base_class
args = self.constructor_args
kwargs = self.constructor_kwargs
class Sub(Base):
def __init__(self, *a, **kw):
self.tag = "custom"
self.count = 0
obs = Sub(*args, **kwargs)
self.assertEqual(obs.tag, "custom")
self.assertEqual(obs.count, 0)
def test_子类_new_过滤_kwargs(self):
"""__new__ 过滤子类专属参数,只把父类需要的传给 super().__new__."""
Base = self.base_class
args = self.constructor_args
class Sub(Base):
def __new__(cls, *a, extra=None, **kw):
return super().__new__(cls, *a)
def __init__(self, *a, extra=None, **kw):
self.extra = extra
obs = Sub(*args, extra={"debug": True})
self.assertEqual(obs.extra, {"debug": True})
obs2 = Sub(*args)
self.assertIsNone(obs2.extra)
# ---- 方法重写 ----
def test_方法重写_super调用(self):
"""重写方法,super() 调用父类."""
if self.make_target_with_data is None or self.make_sub_with_data is None:
self.skipTest("未定义 make_target_with_data / make_sub_with_data")
base_obs = self.make_target_with_data()
sub_obs = self.make_sub_with_data()
for attr in self.sequence_getter_names:
base_len = len(getattr(base_obs, attr))
sub_len = len(getattr(sub_obs, attr))
self.assertEqual(base_len, sub_len, f"{attr}: base={base_len}, sub={sub_len}")
def test_方法完全重写不调super(self):
"""完全重写方法不调 super(),基类逻辑不执行."""
Base = self.base_class
args = self.constructor_args
kwargs = self.constructor_kwargs
class Sub(Base):
def __init__(self, *a, **kw):
self.log = []
obs = Sub(*args, **kwargs)
self.assertEqual(obs.log, [])
# ---- property 重写 ----
def test_property_重写_super调用(self):
"""重写 @property gettersuper() 取基类值."""
Base = self.base_class
args = self.constructor_args
kwargs = self.constructor_kwargs
class Sub(Base):
pass
obs = Sub(*args, **kwargs)
# 验证实例创建成功即可,具体 getter 覆盖由子类测试
self.assertIsInstance(obs, Base)
def test_str_repr_重写(self):
"""重写 __str__ / __repr__."""
Base = self.base_class
args = self.constructor_args
kwargs = self.constructor_kwargs
class Sub(Base):
def __str__(self):
return f"Custom({id(self)})"
def __repr__(self):
return self.__str__()
obs = Sub(*args, **kwargs)
self.assertIn("Custom", str(obs))
self.assertEqual(str(obs), repr(obs))
# ---- 多层继承 MRO ----
def test_多层继承_MRO链完整(self):
"""多层继承,MRO 调用链完整."""
if self.make_data_item is None:
self.skipTest("未定义 make_data_item")
Base = self.base_class
args = self.constructor_args
kwargs = self.constructor_kwargs
feed_name = self.feed_method_name
class L1(Base):
def __init__(self, *a, **kw):
self._l1_called = False
class L2(L1):
def __init__(self, *a, **kw):
super().__init__(*a, **kw)
self._l2_called = True
obs = L2(*args, **kwargs)
self.assertTrue(obs._l2_called)
def test_未重写方法直接继承(self):
"""未重写的方法从基类直接继承."""
if self.make_data_item is None:
self.skipTest("未定义 make_data_item")
Base = self.base_class
args = self.constructor_args
kwargs = self.constructor_kwargs
class Sub(Base):
pass
obs = Sub(*args, **kwargs)
self.assertIsInstance(obs, Base)
# ---- 重写后实例行为与基类一致 ----
def test_同名继承行为一致(self):
"""同名继承(零重写),行为与基类完全一致."""
if self.make_target_with_data is None:
self.skipTest("未定义 make_target_with_data")
Base = self.base_class
args = self.constructor_args
kwargs = self.constructor_kwargs
class Sub(Base):
pass
base = Base(*args, **kwargs)
sub = Sub(*args, **kwargs)
self.assertIsInstance(sub, Base)
+88
View File
@@ -0,0 +1,88 @@
"""PyO3 返回值的 Python 类型形状验证工具。
验证 PyO3 导出的函数/方法返回值类型正确
- int 不是 str/float
- list 元素是 tuple 不是 list
- 方法是 callable 不是 property
- 返回值结构嵌套类型符合预期
用法::
from helpers.type_shape import assert_type_shape
result = mylib.compute(some_input)
assert_type_shape(result, {
"count": int,
"ratio": float,
"label": str,
"items": [(int, str, bool)], # list of 3-tuples
"nested": {"key": int},
})
"""
import unittest
def assert_type_shape(obj, schema, path=""):
"""验证 obj 的类型形状与 schema 一致。
schema 支持:
- type: obj 必须是该类型实例
- [inner]: obj 必须是 list每个元素验证 inner
- (t1, t2, ...): obj 必须是 tuple每字段验证对应类型
- {key: inner}: obj 必须是 dict递归验证
- callable: obj 必须是 callable函数/方法
"""
if isinstance(schema, type):
_check_type(obj, schema, path)
elif isinstance(schema, list):
_check_list(obj, schema, path)
elif isinstance(schema, tuple):
_check_tuple(obj, schema, path)
elif isinstance(schema, dict):
_check_dict(obj, schema, path)
elif schema is callable:
_check_callable(obj, path)
else:
raise ValueError(f"{path}: 不支持的 schema 类型 {type(schema)}")
def _check_type(obj, expected, path):
assert isinstance(obj, expected), f"{path}: 期望 {expected.__name__}, 实际 {type(obj).__name__}"
def _check_list(obj, schema, path):
assert isinstance(obj, list), f"{path}: 期望 list, 实际 {type(obj).__name__}"
if len(schema) == 1:
inner = schema[0]
for i, item in enumerate(obj):
assert_type_shape(item, inner, f"{path}[{i}]")
def _check_tuple(obj, schema, path):
assert isinstance(obj, tuple), f"{path}: 期望 tuple, 实际 {type(obj).__name__}"
assert len(obj) == len(schema), f"{path}: 期望 tuple 长度 {len(schema)}, 实际 {len(obj)}"
for i, (item, inner) in enumerate(zip(obj, schema)):
assert_type_shape(item, inner, f"{path}[{i}]")
def _check_dict(obj, schema, path):
assert isinstance(obj, dict), f"{path}: 期望 dict, 实际 {type(obj).__name__}"
for key, inner in schema.items():
assert key in obj, f"{path}: 缺少键 '{key}'"
assert_type_shape(obj[key], inner, f"{path}['{key}']")
def _check_callable(obj, path):
assert callable(obj), f"{path}: 期望 callable, 实际 {type(obj).__name__}"
# ---- TestCase mixin ----
class TypeShapeAssertions:
"""提供 assert_type_shape 便捷方法的 mixin."""
def assertTypeShape(self, obj, schema, path=""):
"""断言 obj 的类型形状与 schema 一致."""
assert_type_shape(obj, schema, path)
File diff suppressed because it is too large Load Diff
-267
View File
@@ -1,267 +0,0 @@
#!/usr/bin/env python3
"""
对象标识测试验证同一 Rc 底层数据通过不同路径访问时
Python 侧始终返回相同的 PyObject`is` 比较为 True
涉及的 Rc 包装类型
- K线 (Rc<K线>) 原始OHLCV数据
- 缠论K线 (Rc<缠论K线>) 包含处理后的K线
- 分型 (Rc<分型>) 顶底分型
- 虚线 (Rc<虚线>) /线段的通用抽象
- 中枢 (Rc<中枢>) 三段虚线重叠区间
- 线段特征 (Rc<线段特征>) 线段特征序列元素
- 特征分型 (Rc<特征分型>) 特征序列的分型
路径示例
- 缠K序列[i] vs 分型序列[j]. (同一根缠K)
- 分型序列[i] vs 笔序列[j]. (同一个分型)
- 笔序列[i] vs 中枢[k].基础序列[m] (同一条虚线)
"""
import chanlun
import math
def create_observer(symbol="btcusd", period=14400, n_bars=500):
"""创建观察者并喂入模拟K线数据。"""
cfg = chanlun.缠论配置()
obs = chanlun.观察者(symbol, period, cfg)
for i in range(n_bars):
trend = i * 3
wave = math.sin(i * 0.05) * 2000
mid = 68000.0 + trend + wave
high = mid + abs(math.cos(i * 0.3)) * 400 + 100
low = mid - abs(math.sin(i * 0.5)) * 400 - 100
k = chanlun.K线(
标识=symbol,
周期=period,
时间戳=1771675200 + i * period,
开盘价=mid - 50,
=high,
=low,
收盘价=mid + 50,
成交量=abs(math.sin(i)) * 1000,
)
obs.增加原始K线(k)
return obs
class Test缠K身份:
"""缠论K线: 从序列、分型、笔端点、中枢等不同路径访问。"""
def test_序列重复获取(self):
"""同一序列获取两次,元素应相同。"""
obs = create_observer()
seq1 = obs.缠论K线序列
seq2 = obs.缠论K线序列
for i in range(min(len(seq1), 10)):
assert seq1[i] is seq2[i], f"缠K序列[{i}] 身份不一致"
def test_分型中K(self):
"""分型.中 与 缠K序列 对应元素应相同。"""
obs = create_observer()
seq = obs.缠论K线序列
分序 = obs.分型序列
for fx in 分序[:10]:
= fx.
for ck in seq:
if ck.时间戳 == .时间戳:
assert ck is , f"分型.中 (ts={.时间戳}) 与序列中元素不匹配"
break
def test_笔端点钟K(self):
"""笔的端点分型的中间K线应与序列元素相同。"""
obs = create_observer()
seq = obs.缠论K线序列
for bi in obs.笔序列:
for nm, getter in [("", lambda b=bi: b.), ("", lambda b=bi: b.)]:
ep = getter()
if ep is None:
continue
= ep.
for ck in seq:
if ck.时间戳 == .时间戳:
assert ck is , f"笔.{nm}.中 (ts={.时间戳}) 与序列中元素不匹配"
break
def test_getter重复调用(self):
"""同一getter调用两次返回同一对象。"""
obs = create_observer()
for fx in obs.分型序列[:5]:
中1 = fx.
中2 = fx.
assert 中1 is 中2, "分型.中 两次调用返回不同对象"
class Test分型身份:
"""分型: 从分型序列、笔/线段端点、买卖点等不同路径访问。"""
def test_序列重复获取(self):
"""同一序列获取两次,元素应相同。"""
obs = create_observer()
seq1 = obs.分型序列
seq2 = obs.分型序列
for i in range(min(len(seq1), 9)):
assert seq1[i] is seq2[i], f"分型序列[{i}] 身份不一致"
def test_笔端点与序列(self):
"""笔.文 / 笔.武 应与分型序列中对应元素相同。"""
obs = create_observer()
分序 = obs.分型序列
for bi in obs.笔序列:
for nm in ["", ""]:
ep = getattr(bi, nm)
if ep is None:
continue
matched = False
for fx in 分序:
if fx.时间戳 == ep.时间戳 and fx.结构 == ep.结构:
assert fx is ep, f"笔.{nm} (ts={ep.时间戳}) 与分型序列中元素不匹配"
matched = True
break
assert matched, f"笔.{nm} (ts={ep.时间戳}) 在分型序列中未找到"
def test_段端点与序列(self):
"""段.文 / 段.武 应与分型序列中对应元素相同。"""
obs = create_observer()
分序 = obs.分型序列
for duan in obs.线段序列:
for nm in ["", ""]:
ep = getattr(duan, nm)
if ep is None:
continue
matched = False
for fx in 分序:
if fx.时间戳 == ep.时间戳 and fx.结构 == ep.结构:
assert fx is ep, f"段.{nm} (ts={ep.时间戳}) 与分型序列中元素不匹配"
matched = True
break
assert matched, f"段.{nm} (ts={ep.时间戳}) 在分型序列中未找到"
def test_getter重复调用(self):
"""同一getter调用两次返回同一对象。"""
obs = create_observer()
for bi in obs.笔序列:
文1 = bi.
文2 = bi.
assert 文1 is 文2, "笔.文 两次调用返回不同对象"
武1 = bi.
武2 = bi.
assert 武1 is 武2, "笔.武 两次调用返回不同对象"
break # 只测第一笔
class Test虚线身份:
"""虚线(笔/线段): 从笔序列、线段序列、中枢内部序列等不同路径访问。"""
def test_笔序列重复获取(self):
obs = create_observer()
seq1 = obs.笔序列
seq2 = obs.笔序列
for i in range(min(len(seq1), 8)):
assert seq1[i] is seq2[i], f"笔序列[{i}] 身份不一致"
def test_线段序列重复获取(self):
obs = create_observer()
seq1 = obs.线段序列
seq2 = obs.线段序列
for i in range(min(len(seq1), 5)):
assert seq1[i] is seq2[i], f"线段序列[{i}] 身份不一致"
def test_多个扩展序列(self):
"""扩展线段的不同序列获取同一虚线应相同。"""
obs = create_observer()
s1 = obs.扩展线段序列
s2 = obs.扩展线段序列_线段
s3 = obs.扩展线段序列_扩展线段
# 这些序列可能包含不同的虚线,但如果同一个 Rc 出现在两个序列中应该相同
for d1 in s1:
for d2 in s2:
if d1.序号 == d2.序号:
assert d1 is d2, f"扩展线段序列[{d1.序号}] 跨序列身份不一致"
break
class TestK线身份:
"""原始K线: 从序列、买卖点、缠K标的等不同路径访问。"""
def test_序列重复获取(self):
obs = create_observer()
seq1 = obs.普通K线序列
seq2 = obs.普通K线序列
for i in range(min(len(seq1), 10)):
assert seq1[i] is seq2[i], f"普K序列[{i}] 身份不一致"
class Test中枢身份:
"""中枢: 从中枢序列、分型关联、笔中枢/线段中枢等不同路径访问。"""
def test_序列重复获取(self):
obs = create_observer(period=3600, n_bars=800)
seq1 = obs.中枢序列
seq2 = obs.中枢序列
for i in range(min(len(seq1), 5)):
assert seq1[i] is seq2[i], f"中枢序列[{i}] 身份不一致"
def test_笔中枢与线段中枢(self):
obs = create_observer(period=3600, n_bars=800)
笔中 = obs.笔_中枢序列
段中 = obs.线段_中枢序列
扩展中 = obs.扩展中枢序列
# 验证同一次获取内的身份
for zs in 笔中:
文1 = zs.
文2 = zs.
assert 文1 is 文2, f"笔中枢.文 两次调用不同"
break
for zs in 段中:
文1 = zs.
文2 = zs.
assert 文1 is 文2, f"段中枢.文 两次调用不同"
break
class Test整体身份:
"""跨类型综合身份测试。"""
def test_买卖点分型(self):
"""验证买卖点的关联分型身份。"""
obs = create_observer(period=3600, n_bars=800)
# 尝试访问可用的结构
分序 = obs.分型序列
笔序 = obs.笔序列
assert len(分序) >= 0 and len(笔序) >= 0 # 至少不崩溃
def test_全链路一致性(self):
"""缠K → 分型 → 笔 → 段 链路中所有对象身份一致。"""
obs = create_observer()
seq = obs.缠论K线序列
for bi in obs.笔序列:
# 笔的端点分型
for nm, getter in [("", lambda b=bi: b.), ("", lambda b=bi: b.)]:
ep = getter()
if ep is None:
continue
# ep 中的 中 是一根缠K,应能在序列中找到相同对象
= ep.
for ck in seq:
if ck.时间戳 == .时间戳:
assert ck is
break
# 左也应该是可访问的
= ep.
if is not None:
for ck in seq:
if ck.时间戳 == .时间戳:
assert ck is
break
# 右也应该是可访问的
= ep.
if is not None:
for ck in seq:
if ck.时间戳 == .时间戳:
assert ck is
break
+5 -3
View File
@@ -1,8 +1,7 @@
[package]
name = "chanlun"
version = "26.5.3"
edition = "2021"
rust-version = "1.70"
version = "26.6.4"
edition = "2024"
license = "MIT"
description = "基于缠论(缠中说禅)理论的量化技术分析核心库,支持流式数据处理和多周期联立分析。"
readme = "README.md"
@@ -18,3 +17,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
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+16 -20
View File
@@ -39,13 +39,13 @@ impl 背驰分析 {
) -> bool {
let MACD = Self::_获取MACD面积(
K线序列,
&*...K线.read().unwrap(),
&*..read().unwrap()..K线.read().unwrap(),
&...K线.read(),
&..read()..K线.read(),
);
let MACD = Self::_获取MACD面积(
K线序列,
&*...K线.read().unwrap(),
&*..read().unwrap()..K线.read().unwrap(),
&...K线.read(),
&..read()..K线.read(),
);
// 计算面积(绝对值求和)
@@ -69,18 +69,18 @@ impl 背驰分析 {
/// 斜率背驰 — 价格斜率背驰
pub fn (: &线, : &线) -> bool {
let dx = (..read().unwrap(). - ..) as f64;
let dx = (..read().() - ..()) as f64;
if dx == 0.0 {
return false;
}
let dy = ..read().unwrap(). - ..;
let dy = ..read(). - ..;
let = dy / dx;
let dx = (..read().unwrap(). - ..) as f64;
let dx = (..read().() - ..()) as f64;
if dx == 0.0 {
return false;
}
let dy = ..read().unwrap(). - ..;
let dy = ..read(). - ..;
let = dy / dx;
if .() == :: {
@@ -92,12 +92,12 @@ impl 背驰分析 {
/// 测度背驰 — 价格时间测度背驰
pub fn (: &线, : &线) -> bool {
let dx = (..read().unwrap(). - ..) as f64;
let dy = ..read().unwrap(). - ..;
let dx = (..read().() - ..()) as f64;
let dy = ..read(). - ..;
let = (dx * dx + dy * dy).sqrt();
let dx = (..read().unwrap(). - ..) as f64;
let dy = ..read().unwrap(). - ..;
let dx = (..read().() - ..()) as f64;
let dy = ..read(). - ..;
let = (dx * dx + dy * dy).sqrt();
if .() == :: {
@@ -187,12 +187,8 @@ impl 背驰分析 {
// ---- 内部辅助 ----
fn _获取MACD面积(K线序列: &[Arc<K线>], : &Arc<K线>, : &Arc<K线>) -> MACD面积 {
let _idx = K线序列
.iter()
.position(|k| Arc::as_ptr(k) == Arc::as_ptr());
let _idx = K线序列
.iter()
.position(|k| Arc::as_ptr(k) == Arc::as_ptr());
let _idx = K线序列.iter().position(|k| Arc::ptr_eq(k, ));
let _idx = K线序列.iter().position(|k| Arc::ptr_eq(k, ));
let mut = 0.0f64;
let mut = 0.0f64;
@@ -200,7 +196,7 @@ impl 背驰分析 {
if let (Some(), Some()) = (_idx, _idx) {
let (, ) = if <= { (, ) } else { (, ) };
for k in &K线序列[..=] {
if let Some(ref macd) = k.macd {
if let Some(macd) = k..read().macd() {
let hist = macd.MACD柱;
if hist >= 0.0 {
+= hist;
@@ -222,6 +218,6 @@ struct MACD面积 {
impl MACD面积 {
fn (&self) -> f64 {
self. + self.
self. + self..abs()
}
}
+346 -217
View File
@@ -25,18 +25,34 @@
use crate::structure::dash_line::线;
use crate::structure::fractal_obj::;
use crate::types::;
use parking_lot::RwLock;
use std::sync::Arc;
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::{Arc, RwLock};
/// 中枢 — 三段虚线重叠区间构成的价格中枢
/// 可变字段使用 Cell/RefCell 实现内部可变性,确保 Rc 指针身份一致
///
/// 可变字段使用 AtomicI64 / RwLock 实现内部可变性,确保多 Arc 共享时可修改。
///
/// 字段:
/// - 序号: 中枢序号,同一级别内递增
/// - 标识: 中枢标识,格式如 "笔中枢<0>" 或 "线段中枢<1>"
/// - 级别: 中枢级别(笔中枢=1,线段中枢=2 等)
/// - 基础序列: 构成中枢的虚线序列(至少 3 根,延伸后可多至 9 根甚至更多)
/// - 第三买卖线: 第三类买卖点对应的虚线(离开中枢后不回中枢的虚线)
/// - 本级_第三买卖线: 本级第三类买卖点对应的虚线
#[derive(Debug)]
pub struct {
/// 中枢序号,同一级别内递增
pub : AtomicI64,
/// 中枢标识,格式如 "笔中枢<0>" 或 "线段中枢<1>"
pub : RwLock<String>,
/// 中枢级别(笔中枢=1,线段中枢=2 等)
pub : AtomicI64,
/// 构成中枢的虚线序列(至少 3 根,延伸后可多至 9+ 根)
pub : RwLock<Vec<Arc<线>>>,
/// 第三类买卖点对应的虚线(离开中枢后不回中枢的虚线)
pub 线: RwLock<Option<Arc<线>>>,
/// 本级第三类买卖点对应的虚线
pub _第三买卖线: RwLock<Option<Arc<线>>>,
}
@@ -44,16 +60,17 @@ impl Clone for 中枢 {
fn clone(&self) -> Self {
Self {
: AtomicI64::new(self..load(Ordering::Relaxed)),
: RwLock::new(self..read().unwrap().clone()),
: RwLock::new(self..read().clone()),
: AtomicI64::new(self..load(Ordering::Relaxed)),
: RwLock::new(self..read().unwrap().clone()),
线: RwLock::new(self.线.read().unwrap().clone()),
_第三买卖线: RwLock::new(self._第三买卖线.read().unwrap().clone()),
: RwLock::new(self..read().clone()),
线: RwLock::new(self.线.read().clone()),
_第三买卖线: RwLock::new(self._第三买卖线.read().clone()),
}
}
}
impl {
/// 创建新中枢(最多取前 3 根虚线作为基础序列)
pub fn new(: i64, : String, : i64, : Vec<Arc<线>>) -> Self {
Self {
: AtomicI64::new(),
@@ -65,109 +82,121 @@ impl 中枢 {
}
}
pub fn 线(&self, 线: Arc<线>) {
self..write().unwrap().push(线);
*self._第三买卖线.write().unwrap() = None;
*self.线.write().unwrap() = None;
/// 向基础序列尾部添加虚线(中枢延伸),并清除第三买卖线
pub fn _添加虚线(&self, 线: Arc<线>) {
self..write().push(线);
*self._第三买卖线.write() = None;
*self.线.write() = None;
}
/// 返回图表标题字符串,格式为 "文.标识:文.周期:中枢标识:序号"
pub fn (&self) -> String {
format!(
"{}:{}:{}:{}",
self.()..,
self.()..,
self..read().unwrap(),
self..read(),
self..load(Ordering::Relaxed)
)
}
/// 返回基础序列的最后一根虚线(当前离开段)
pub fn (&self) -> Arc<线> {
Arc::clone(&self..read().unwrap()[self..read().unwrap().len() - 1])
let guard = self..read();
Arc::clone(&guard[guard.len() - 1])
}
/// 返回中枢方向(与基础序列第一段方向相反)
pub fn (&self) -> {
self..read().unwrap()[0].().()
self..read()[0].().()
}
/// 中枢上沿 = min(前三段的高)
pub fn (&self) -> f64 {
self..read().unwrap()[..3]
self..read()[..3]
.iter()
.map(|x| x.())
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(0.0)
}
/// 中枢下沿 = max(前三段的低)
pub fn (&self) -> f64 {
self..read().unwrap()[..3]
self..read()[..3]
.iter()
.map(|x| x.())
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(0.0)
}
/// 中枢最高点 = max(所有段的高)
pub fn (&self) -> f64 {
self.
.read()
.unwrap()
.iter()
.map(|x| x.())
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(0.0)
}
/// 中枢最低点 = min(所有段的低)
pub fn (&self) -> f64 {
self.
.read()
.unwrap()
.iter()
.map(|x| x.())
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(0.0)
}
/// 返回基础序列第一段的起点分型
pub fn (&self) -> Arc<> {
Arc::clone(&self..read().unwrap()[0].)
Arc::clone(&self..read()[0].)
}
/// 返回基础序列最后一段的终点分型
pub fn (&self) -> Arc<> {
Arc::clone(
&*self..read().unwrap()[self..read().unwrap().len() - 1]
.
.read()
.unwrap(),
)
let guard = self..read();
Arc::clone(&*guard[guard.len() - 1]..read())
}
pub fn 线(&self, 线: Arc<线>) {
*self.线.write().unwrap() = Some(线);
/// 设置第三类买卖点对应的虚线
pub fn 线(&self, 线: Option<Arc<线>>) {
*self.线.write() = 线;
}
/// 获取序列 — 基础序列 + 第三买卖线(若有)
pub fn (&self) -> Vec<Arc<线>> {
let mut : Vec<Arc<线>> = self..read().unwrap().clone();
if let Some(ref ) = *self.线.read().unwrap() {
let mut : Vec<Arc<线>> = self..read().clone();
if let Some(ref ) = *self.线.read() {
.push(Arc::clone());
}
}
/// 获取基础序列最后一个元素
pub fn (&self) -> Option<Arc<线>> {
self..read().last().cloned()
}
/// 返回序列化数据文本,用于调试和存储
pub fn (&self) -> String {
let 线_str = match &*self.线.read().unwrap() {
let 线_str = match &*self.线.read() {
Some(x) => format!("{}", x),
None => "None".to_string(),
};
let _第三买卖线_str = match &*self._第三买卖线.read().unwrap() {
let _第三买卖线_str = match &*self._第三买卖线.read() {
Some(x) => format!("{}", x),
None => "None".to_string(),
};
format!(
"{}, {}, {}, 文:({},{}), 武:({},{}), {}, {}",
self..read().unwrap(),
self..read(),
self..load(Ordering::Relaxed),
self..load(Ordering::Relaxed),
self.().,
self.().(),
crate::utils::format_f64_g(self.().),
self.().,
self.().(),
crate::utils::format_f64_g(self.().),
线_str,
_第三买卖线_str,
@@ -175,68 +204,64 @@ impl 中枢 {
}
/// 校验中枢合法性
pub fn (&self, : &[Arc<线>]) -> bool {
let mut = self..read().unwrap().clone();
pub fn _校验合法性(&self, : &[Arc<线>]) -> bool {
let guard = self..read();
let mut = guard.clone();
let mut : Vec<Arc<线>> = Vec::new();
for in self..read().unwrap().iter() {
if !.iter().any(|x| Arc::as_ptr(x) == Arc::as_ptr()) {
let = [0]..load(Ordering::Relaxed);
for in guard.iter() {
let idx = (..load(Ordering::Relaxed) - ) as usize;
if idx >= .len() || !Arc::ptr_eq(&[idx], ) {
.push(Arc::clone());
}
}
if !.is_empty() {
let = &[0];
if let Some(pos) = self
.
.read()
.unwrap()
.iter()
.position(|x| Arc::as_ptr(x) == Arc::as_ptr())
{
= self..read().unwrap()[..pos].to_vec();
}
// Python: 序号 = 线段._索引(self.基础序列, 无效)
let pos = crate::algorithm::segment::线::_索引(&guard, );
= guard[..pos].to_vec();
}
drop(guard);
if .len() < 3 {
*self.线.write().unwrap() = None;
*self._第三买卖线.write().unwrap() = None;
self.线(None);
*self._第三买卖线.write() = None;
return false;
}
*self..write().unwrap() = ;
*self..write() = ;
let = self.();
let = self.();
= Vec::new();
for in self..read().unwrap().iter() {
for in self..read().iter() {
if crate::types::::(, , .(), .()).()
{
break;
}
.push(Arc::clone());
}
*self..write().unwrap() = ;
*self..write() = ;
if self..read().unwrap().len() < 3 {
return false;
}
for i in 1..self..read().unwrap().len() {
let = &self..read().unwrap()[i - 1];
let = &self..read().unwrap()[i];
if !.() {
let = {
let guard = self..read();
if guard.len() < 3 {
return false;
}
}
if !crate::types::::(
self..read().unwrap()[0].(),
self..read().unwrap()[0].(),
self..read().unwrap()[2].(),
self..read().unwrap()[2].(),
)
.()
{
for i in 1..guard.len() {
if !guard[i - 1].(&guard[i]) {
return false;
}
}
crate::types::::(
guard[0].(),
guard[0].(),
guard[2].(),
guard[2].(),
)
.()
};
if ! {
let = self.();
let = self.();
if > {
@@ -244,11 +269,13 @@ impl 中枢 {
}
}
let 线_opt = self.线.read().unwrap().clone();
let 线_opt = self.线.read().clone();
if let Some(ref 线) = 线_opt {
if .iter().any(|x| Arc::as_ptr(x) == Arc::as_ptr(线)) {
if !self..read().unwrap().last().unwrap().(线) {
*self.线.write().unwrap() = None;
let = [0]..load(Ordering::Relaxed);
let idx = (线..load(Ordering::Relaxed) - ) as usize;
if idx < .len() && Arc::ptr_eq(&[idx], 线) {
if !self..read().last().unwrap().(线) {
self.线(None);
} else if !crate::types::::(
self.(),
self.(),
@@ -257,11 +284,11 @@ impl 中枢 {
)
.()
{
self.线(Arc::clone(线));
*self.线.write().unwrap() = None;
self._添加虚线(Arc::clone(线));
self.线(None);
}
} else {
*self.线.write().unwrap() = None;
self.线(None);
}
}
true
@@ -270,25 +297,44 @@ impl 中枢 {
/// 完整性 — 详见教你炒股票43:有关背驰的补习课
/// 不完整时下一个中枢大概率会与当前中枢发生扩展
pub fn (&self, : &str) -> bool {
if *self..read().unwrap()[0]..read().unwrap() == "" {
return self.线.read().unwrap().is_some();
if *self..read()[0]..read() == "" {
return self.线.read().is_some();
}
let _ref = self..read().unwrap();
// if self.本级_第三买卖线: return True # 暂未启用
let = self.();
if == "中枢之中" {
return false;
}
let _ref = self..read();
let = _ref.last().unwrap();
let _vec = if == "" {
._中枢序列.read().unwrap()
._中枢序列.read()
} else {
._中枢序列.read().unwrap()
._中枢序列.read()
};
if _vec.is_empty() {
return false;
}
let = self.();
let = self.();
for in _vec.iter() {
if crate::types::::(
self.(),
self.(),
.(),
.(),
)
.()
let = .();
let = .();
if == "中枢之下" {
if <= {
continue;
}
} else {
// 中枢之上
if >= {
continue;
}
}
if crate::types::::(, , , ).()
{
return true;
}
@@ -302,24 +348,19 @@ impl 中枢 {
: &mut Vec<Arc<>>,
: &crate::config::,
) {
if self..read().unwrap().len() >= 9 {
if self..read().len() >= 9 {
let mut 线: Vec<Arc<线>> = Vec::new();
let _ref = self..read().unwrap();
let _ref = self..read();
crate::algorithm::segment::线::(&_ref, &mut 线, );
::(
&线,
,
false,
&format!("{}_扩展中枢_", self..read().unwrap()),
0,
);
let = format!("{}_扩展中枢_", self..read());
::(&线, , false, &, 0);
}
}
/// 当前状态 — 详见教你炒股票49:利润率最大的操作模式
/// 返回当前中枢最后一段所处的位置关系:中枢之中/中枢之上/中枢之下
pub fn (&self) -> &str {
let _ref = self..read().unwrap();
let _ref = self..read();
let = Arc::clone(_ref.last().unwrap());
let = ._武();
let = crate::types::::(
@@ -359,16 +400,17 @@ impl 中枢 {
pub fn (
: Arc<线>, : Arc<线>, : Arc<线>, : i64, : &str
) -> Self {
assert!(Self::(&, &, &), "中枢.创建 基础检查失败");
Self::new(
0,
format!("{}中枢<{}>", , ..read().unwrap()),
format!("{}中枢<{}>", , ..read()),
,
vec![, , ],
)
}
/// 从序列中获取中枢
pub fn (
/// _从序列中获取中枢
pub fn _从序列中获取中枢(
线: &[Arc<线>],
: ,
: &str,
@@ -385,37 +427,32 @@ impl 中枢 {
None
}
/// 向中枢序列尾部添加
pub fn (
: &mut Vec<Arc<>>, mut : Arc<>
/// _向中枢序列尾部添加
pub fn _向中枢序列尾部添加(
: &mut Vec<Arc<>>, : Arc<>
) {
if let Some() = .last() {
.
.store(..load(Ordering::Relaxed) + 1, Ordering::Relaxed);
// Python: assert seq[-1].获取序列()[-1].序号 <= new.获取序列()[-1].序号
let _seq = .();
let new_seq = .();
if let (Some(_last), Some(new_last)) = (_seq.last(), new_seq.last()) {
if _last..load(Ordering::Relaxed) > new_last..load(Ordering::Relaxed)
{
panic!(
"向中枢序列尾部添加 序号错误 前last={} > new_last={}",
_last..load(Ordering::Relaxed),
new_last..load(Ordering::Relaxed)
);
}
let _last_序号 = .().unwrap()..load(Ordering::Relaxed);
let new_last_序号 = .().unwrap()..load(Ordering::Relaxed);
if _last_序号 > new_last_序号 {
panic!(
"向中枢序列尾部添加 序号错误 前last={} > new_last={}",
_last_序号, new_last_序号
);
}
}
.push();
}
/// 从中枢序列尾部弹出
pub fn (
pub fn _从中枢序列尾部弹出(
: &mut Vec<Arc<>>,
: &Arc<>,
) -> Option<Arc<>> {
if .last().map(|x| Arc::as_ptr(x)) == Some(Arc::as_ptr()) {
if .last().map(Arc::as_ptr) == Some(Arc::as_ptr()) {
.pop()
} else {
None
@@ -430,7 +467,7 @@ impl 中枢 {
: &mut Vec<Arc<>>,
: bool,
: &str,
: i64,
_层级: i64,
) {
if 线.len() < 3 {
return;
@@ -444,11 +481,8 @@ impl 中枢 {
let = &线[i + 1];
if Self::(, , ) {
// Python: 序号 = 虚线序列.index(左)
let = 线
.iter()
.position(|x| Arc::as_ptr(x) == Arc::as_ptr())
.unwrap_or(i - 1);
// Python: 序号 = 线段._索引(虚线序列, 左)
let : usize = crate::algorithm::segment::线::_索引(线, );
if && (..load(Ordering::Relaxed) == 0 || == 0) {
continue;
}
@@ -470,9 +504,9 @@ impl 中枢 {
..load(Ordering::Relaxed),
,
));
Self::(, );
Self::_向中枢序列尾部添加(, );
// Python: return 中枢递归分析(虚线序列, 中枢序列, ...)
Self::(线, , , , );
Self::(线, , , , _层级);
return;
}
}
@@ -483,33 +517,27 @@ impl 中枢 {
let mut _idx = .len() - 1;
// Validate via shared reference (中枢 uses RwLock internally)
let needs_pop = ![_idx].(线);
let needs_pop = ![_idx]._校验合法性(线);
if needs_pop {
let = Arc::clone(&[_idx]);
Self::(, &);
Self::(线, , , , );
Self::_从中枢序列尾部弹出(, &);
Self::(线, , , , _层级);
return;
}
// 找到当前中枢最后一个元素在虚线序列中的位置
// Python: 序号 = 线段._索引(虚线序列, 当前中枢.基础序列[-1]) + 1
let = {
let cur = &[_idx];
let = &cur..read().unwrap()[cur..read().unwrap().len() - 1];
match 线
.iter()
.position(|x| Arc::as_ptr(x) == Arc::as_ptr())
{
Some(idx) => idx + 1,
None => return,
}
let guard = cur..read();
crate::algorithm::segment::线::_索引(线, &guard[guard.len() - 1]) + 1
};
let mut = [_idx].();
let mut = [_idx].();
let mut : Vec<Arc<线>> = Vec::new();
let mut = Vec::with_capacity(3);
for i in ..线.len() {
let 线 = Arc::clone(&线[i]);
for 线_ref in &线[..] {
let 线 = Arc::clone(线_ref);
// 检查是否超出中枢范围(缺口)
if crate::types::::(, , 线.(), 线.()).()
@@ -519,20 +547,26 @@ impl 中枢 {
// Python: if 当前中枢.基础序列[-1].之后是(当前虚线):
let needs_三买 = {
let cur = &[_idx];
cur.
.read()
.unwrap()
.last()
.unwrap()
.(&线)
cur..read().last().unwrap().(&线)
};
if needs_三买 {
[_idx].线(线.clone());
[_idx].线(Some(线.clone()));
}
} else {
if .is_empty() {
// 仍在范围内:延伸中枢
[_idx].线(线);
assert!(
[_idx]
.
.read()
.last()
.unwrap()
.(&线),
"中枢延伸: 不连续 {}, {}",
[_idx]..read().last().unwrap(),
线
);
[_idx]._添加虚线(线);
} else {
.push(线);
}
@@ -543,14 +577,13 @@ impl 中枢 {
let = [_idx]
.
.read()
.unwrap()
.last()
.unwrap()
.()
.();
match Self::(&, , ) {
match Self::_从序列中获取中枢(&, , ) {
Some() => {
Self::(, );
Self::_向中枢序列尾部添加(, );
// Python: 当前中枢 = 新中枢
_idx = .len() - 1;
= [_idx].();
@@ -564,6 +597,128 @@ impl 中枢 {
}
}
}
/// 结构化相等校验 — 递归校验基础序列虚线和第三买卖线,返回 (是否相等, 差异描述)
pub fn (&self, other: &Self, : f64) -> (bool, String) {
if self..load(Ordering::Relaxed) != other..load(Ordering::Relaxed) {
return (
false,
format!(
"中枢: [序号] 不等 A={},B={}",
self..load(Ordering::Relaxed),
other..load(Ordering::Relaxed)
),
);
}
if *self..read() != *other..read() {
return (
false,
format!(
"中枢: [标识] 不等 A={},B={}",
self..read(),
other..read()
),
);
}
if self..load(Ordering::Relaxed) != other..load(Ordering::Relaxed) {
return (
false,
format!(
"中枢: [级别] 不等 A={},B={}",
self..load(Ordering::Relaxed),
other..load(Ordering::Relaxed)
),
);
}
// 基础序列
let a_seq = self..read();
let b_seq = other..read();
if a_seq.len() != b_seq.len() {
return (
false,
format!(
"中枢: [基础序列] 长度不一致 A={},B={}",
a_seq.len(),
b_seq.len()
),
);
}
for (idx, (a, b)) in a_seq.iter().zip(b_seq.iter()).enumerate() {
let (eq, msg) = a.(b, );
if !eq {
return (false, format!("中枢: 基础序列[{idx}]虚线异常 >> {msg}"));
}
}
// 第三买卖线
let = |: &str,
a: &Option<Arc<线>>,
b: &Option<Arc<线>>,
: f64|
-> Result<(), String> {
match (a, b) {
(None, None) => Ok(()),
(Some(x), Some(y)) => {
let (eq, msg) = x.(y, );
if eq {
Ok(())
} else {
Err(format!("中枢: [{名}]子虚线异常 >> {msg}"))
}
}
_ => Err(format!(
"中枢: [{名}]空值不一致 A={},B={}",
a.is_some(),
b.is_some()
)),
}
};
(
"第三买卖线",
&self.线.read(),
&other.线.read(),
,
)
.map_err(|e| (false, e))
.ok();
(
"本级_第三买卖线",
&self._第三买卖线.read(),
&other._第三买卖线.read(),
,
)
.map_err(|e| (false, e))
.ok();
(true, "中枢: 全部字段一致".into())
}
}
impl std::fmt::Display for {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let guard = self..read();
let len = guard.len();
let _str = if let Some((first, rest)) = guard.split_first() {
let mut s = format!("{}", first);
for d in rest {
use std::fmt::Write;
write!(&mut s, ", {}", d).unwrap();
}
s
} else {
String::new()
};
drop(guard);
write!(
f,
"{}({}, {}, 元素数量: {}, [{}], {} ===>>> {})",
self..read(),
crate::utils::format_f64_g(self.()),
crate::utils::format_f64_g(self.()),
len,
_str,
self.(),
self.(),
)
}
}
#[cfg(test)]
@@ -575,13 +730,14 @@ mod tests {
use crate::types::;
fn _创建K线(: i64, : f64, : f64, : f64, : f64) -> K线 {
let mut k = K线::default();
k. = ;
k. = ;
k. = ;
k. = ;
k. = ;
k
K线 {
,
,
,
: ,
: ,
..Default::default()
}
}
fn _创建缠K(
@@ -682,11 +838,11 @@ mod tests {
);
assert_eq!(..load(Ordering::Relaxed), 1);
assert_eq!(*..read().unwrap(), "测试中枢");
assert_eq!(*..read(), "测试中枢");
assert_eq!(..load(Ordering::Relaxed), 1);
assert_eq!(..read().unwrap().len(), 3);
assert!(.线.read().unwrap().is_none());
assert!(._第三买卖线.read().unwrap().is_none());
assert_eq!(..read().len(), 3);
assert!(.线.read().is_none());
assert!(._第三买卖线.read().is_none());
}
#[test]
@@ -707,17 +863,17 @@ mod tests {
assert_eq!(..load(Ordering::Relaxed), 99);
// RefCell 第三买卖线读写
.线(Arc::clone(&1));
assert!(.线.read().unwrap().is_some());
.线(Some(Arc::clone(&1)));
assert!(.线.read().is_some());
assert_eq!(
Arc::as_ptr(&*.线.read().unwrap().as_ref().unwrap()),
Arc::as_ptr(.线.read().as_ref().unwrap()),
Arc::as_ptr(&1)
);
// 本级_第三买卖线
assert!(._第三买卖线.read().unwrap().is_none());
*._第三买卖线.write().unwrap() = Some(Arc::clone(&3));
assert!(._第三买卖线.read().unwrap().is_some());
assert!(._第三买卖线.read().is_none());
*._第三买卖线.write() = Some(Arc::clone(&3));
assert!(._第三买卖线.read().is_some());
}
// ============================================================
@@ -737,14 +893,11 @@ mod tests {
1,
vec![Arc::clone(&1), Arc::clone(&2), Arc::clone(&3)],
);
assert_eq!(..read().unwrap().len(), 3);
assert_eq!(..read().len(), 3);
.线(Arc::clone(&4));
assert_eq!(..read().unwrap().len(), 4);
assert_eq!(
Arc::as_ptr(&..read().unwrap()[3]),
Arc::as_ptr(&4)
);
._添加虚线(Arc::clone(&4));
assert_eq!(..read().len(), 4);
assert_eq!(Arc::as_ptr(&..read()[3]), Arc::as_ptr(&4));
}
#[test]
@@ -760,15 +913,15 @@ mod tests {
1,
vec![Arc::clone(&1), Arc::clone(&2), Arc::clone(&3)],
);
.线(Arc::clone(&1));
*._第三买卖线.write().unwrap() = Some(Arc::clone(&2));
assert!(.线.read().unwrap().is_some());
assert!(._第三买卖线.read().unwrap().is_some());
.线(Some(Arc::clone(&1)));
*._第三买卖线.write() = Some(Arc::clone(&2));
assert!(.线.read().is_some());
assert!(._第三买卖线.read().is_some());
.线(Arc::clone(&4));
._添加虚线(Arc::clone(&4));
// 添加虚线后第三买卖线被清除
assert!(.线.read().unwrap().is_none());
assert!(._第三买卖线.read().unwrap().is_none());
assert!(.线.read().is_none());
assert!(._第三买卖线.read().is_none());
}
// ============================================================
@@ -787,22 +940,22 @@ mod tests {
1,
vec![Arc::clone(&1), Arc::clone(&2), Arc::clone(&3)],
);
.线(Arc::clone(&1));
.线(Some(Arc::clone(&1)));
let = .clone();
// 基础序列中的 Rc 指针应一致
for i in 0..3 {
assert_eq!(
Arc::as_ptr(&..read().unwrap()[i]),
Arc::as_ptr(&..read().unwrap()[i])
Arc::as_ptr(&..read()[i]),
Arc::as_ptr(&..read()[i])
);
}
// 第三买卖线 Rc 指针应一致
assert_eq!(
Arc::as_ptr(.线.read().unwrap().as_ref().unwrap()),
Arc::as_ptr(.线.read().unwrap().as_ref().unwrap())
Arc::as_ptr(.线.read().as_ref().unwrap()),
Arc::as_ptr(.线.read().as_ref().unwrap())
);
}
@@ -860,37 +1013,13 @@ mod tests {
assert_eq!(2..load(Ordering::Relaxed), 88);
// 通过 rc1 添加虚线
1.线(Arc::clone(&4));
assert_eq!(2..read().unwrap().len(), 4);
1._线(Arc::clone(&4));
assert_eq!(2..read().len(), 4);
// 验证共享的 Arc<虚线> 指针一致
assert_eq!(
Arc::as_ptr(&1..read().unwrap()[3]),
Arc::as_ptr(&2..read().unwrap()[3])
Arc::as_ptr(&1..read()[3]),
Arc::as_ptr(&2..read()[3])
);
}
}
impl std::fmt::Display for {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let _str = self
.
.read()
.unwrap()
.iter()
.map(|d| format!("{}", d))
.collect::<Vec<_>>()
.join(", ");
write!(
f,
"{}({}, {}, 元素数量: {}, [{}], {} ===>>> {})",
self..read().unwrap(),
crate::utils::format_f64_g(self.()),
crate::utils::format_f64_g(self.()),
self..read().unwrap().len(),
_str,
self.(),
self.(),
)
}
}
File diff suppressed because it is too large Load Diff
+396 -7
View File
@@ -27,24 +27,39 @@ use crate::kline::chan_kline::缠论K线;
use crate::structure::fractal_obj::;
use crate::types::bsp_type::;
use crate::types::;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use std::sync::atomic::Ordering;
/// 基础买卖点 — 买卖点的基础数据结构
///
/// 包含买卖点的完整信息:类型、关联分型/K线、失效与终结状态等。
#[derive(Debug, Clone)]
pub struct {
/// 买卖点备注文本
pub : String,
/// 买卖点类型(一买/一卖/二买/二卖/三买/三卖/T1/T2/T3 等)
pub : ,
/// 买卖点对应的分型
pub : Arc<>,
/// 买卖点对应的缠论K线(即分型的中缠K)
pub K线: Arc<K线>,
/// 当前K线(买卖点生成时的K线)
pub K线: Arc<K线>,
/// 失效K线(买卖点失效时设置)
pub K线: Option<Arc<K线>>,
/// 终结K线(买卖点终结时设置)
pub K线: Option<Arc<K线>>,
/// 中枢破位值
pub : f64,
/// 分型结构(可选,用于补充确认)
pub : Option<>,
/// 创建时的缠K序号,用于偏移计算(与买卖点K线.序号同尺度)
/// None 时退化为使用当前K线.序号(bar序号,旧行为)
pub K序号: Option<i64>,
}
impl {
/// 创建基础买卖点,买卖点K线自动取自买卖点分型的中缠K
pub fn new(
: ,
K线: Arc<K线>,
@@ -63,12 +78,17 @@ impl 基础买卖点 {
K线: None,
: ,
: None,
K序号: None,
}
}
/// 偏移 — 当前K线与买卖点K线序号差
/// 偏移 — 当前缠K序号与买卖点K线序号
/// 如果设置了当前缠K序号(来自生成买卖点),使用缠K序号;否则退化为使用 bar序号
pub fn (&self) -> i64 {
self.K线. - self.K线..load(Ordering::Relaxed)
match self.K序号 {
Some(ck_idx) => ck_idx - self.K线..load(Ordering::Relaxed),
None => self.K线. - self.K线..load(Ordering::Relaxed),
}
}
/// 失效偏移
@@ -108,10 +128,11 @@ impl std::fmt::Display for 基础买卖点 {
}
}
/// 买卖点 — 包含一二三类买卖点的工厂方法
/// 买卖点 — 包含全部 18 种买卖点类型的工厂方法
pub struct ;
impl {
/// 创建 一卖 类型的基础买卖点
pub fn (
: Arc<>,
K线: Arc<K线>,
@@ -122,6 +143,7 @@ impl 买卖点 {
::new(::, K线, , , )
}
/// 创建 一买 类型的基础买卖点
pub fn (
: Arc<>,
K线: Arc<K线>,
@@ -132,6 +154,7 @@ impl 买卖点 {
::new(::, K线, , , )
}
/// 创建 二卖 类型的基础买卖点
pub fn (
: Arc<>,
K线: Arc<K线>,
@@ -142,6 +165,7 @@ impl 买卖点 {
::new(::, K线, , , )
}
/// 创建 二买 类型的基础买卖点
pub fn (
: Arc<>,
K线: Arc<K线>,
@@ -152,6 +176,7 @@ impl 买卖点 {
::new(::, K线, , , )
}
/// 创建 三卖 类型的基础买卖点
pub fn (
: Arc<>,
K线: Arc<K线>,
@@ -162,6 +187,7 @@ impl 买卖点 {
::new(::, K线, , , )
}
/// 创建 三买 类型的基础买卖点
pub fn (
: Arc<>,
K线: Arc<K线>,
@@ -172,6 +198,138 @@ impl 买卖点 {
::new(::, K线, , , )
}
/// 创建 T1卖 类型的基础买卖点
pub fn T1卖点(
: Arc<>,
K线: Arc<K线>,
_标识: &str,
: String,
: f64,
) -> {
::new(::T1卖, K线, , , )
}
/// 创建 T1买 类型的基础买卖点
pub fn T1买点(
: Arc<>,
K线: Arc<K线>,
_标识: &str,
: String,
: f64,
) -> {
::new(::T1买, K线, , , )
}
/// 创建 T1P卖 类型的基础买卖点
pub fn T1P卖点(
: Arc<>,
K线: Arc<K线>,
_标识: &str,
: String,
: f64,
) -> {
::new(::T1P卖, K线, , , )
}
/// 创建 T1P买 类型的基础买卖点
pub fn T1P买点(
: Arc<>,
K线: Arc<K线>,
_标识: &str,
: String,
: f64,
) -> {
::new(::T1P买, K线, , , )
}
/// 创建 T2卖 类型的基础买卖点
pub fn T2卖点(
: Arc<>,
K线: Arc<K线>,
_标识: &str,
: String,
: f64,
) -> {
::new(::T2卖, K线, , , )
}
/// 创建 T2买 类型的基础买卖点
pub fn T2买点(
: Arc<>,
K线: Arc<K线>,
_标识: &str,
: String,
: f64,
) -> {
::new(::T2买, K线, , , )
}
/// 创建 T2S卖 类型的基础买卖点
pub fn T2S卖点(
: Arc<>,
K线: Arc<K线>,
_标识: &str,
: String,
: f64,
) -> {
::new(::T2S卖, K线, , , )
}
/// 创建 T2S买 类型的基础买卖点
pub fn T2S买点(
: Arc<>,
K线: Arc<K线>,
_标识: &str,
: String,
: f64,
) -> {
::new(::T2S买, K线, , , )
}
/// 创建 T3A卖 类型的基础买卖点
pub fn T3A卖点(
: Arc<>,
K线: Arc<K线>,
_标识: &str,
: String,
: f64,
) -> {
::new(::T3A卖, K线, , , )
}
/// 创建 T3A买 类型的基础买卖点
pub fn T3A买点(
: Arc<>,
K线: Arc<K线>,
_标识: &str,
: String,
: f64,
) -> {
::new(::T3A买, K线, , , )
}
/// 创建 T3B卖 类型的基础买卖点
pub fn T3B卖点(
: Arc<>,
K线: Arc<K线>,
_标识: &str,
: String,
: f64,
) -> {
::new(::T3B卖, K线, , , )
}
/// 创建 T3B买 类型的基础买卖点
pub fn T3B买点(
: Arc<>,
K线: Arc<K线>,
_标识: &str,
: String,
: f64,
) -> {
::new(::T3B买, K线, , , )
}
/// 生成买卖点 — 根据参数自动选择类型
pub fn (
: &str,
@@ -186,10 +344,12 @@ impl 买卖点 {
""
};
let = format!("{}_{}{}{}", , , , );
let = .;
let = .();
// 当前K线 — 从缠K获取其标的K线
let K线 = Arc::clone(&*K.K线.read().unwrap());
let K线 = Arc::clone(&*K.K线.read());
// 当前缠K序号 — 与买卖点K线(分型.中.序号)同尺度,用于偏移计算
let K序号 = K..load(Ordering::Relaxed);
let = match (, ) {
("", "") => ::,
@@ -198,9 +358,238 @@ impl 买卖点 {
("", "") => ::,
("", "") => ::,
("", "") => ::,
("T1", "") => ::T1买,
("T1", "") => ::T1卖,
("T1P", "") => ::T1P买,
("T1P", "") => ::T1P卖,
("T2", "") => ::T2买,
("T2", "") => ::T2卖,
("T2S", "") => ::T2S买,
("T2S", "") => ::T2S卖,
("T3A", "") => ::T3A买,
("T3A", "") => ::T3A卖,
("T3B", "") => ::T3B买,
("T3B", "") => ::T3B卖,
_ => ::, // fallback
};
::new(, K线, , , )
let mut bsp = ::new(, K线, , , );
bsp.K序号 = Some(K序号);
bsp
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kline::bar::K线;
use crate::kline::chan_kline::K线;
use crate::structure::fractal_obj::;
use crate::types::;
use crate::types::;
use std::sync::Arc;
use std::sync::atomic::Ordering;
fn _创建普K(: i64, : i64) -> Arc<K线> {
Arc::new(K线 {
,
,
: 100.0,
: 90.0,
: 95.0,
: 95.0,
..Default::default()
})
}
fn _创建缠K(
: i64,
: i64,
: f64,
: f64,
: Option<>,
) -> Arc<K线> {
let K = _创建普K(, 0);
Arc::new(K线::K(
,
,
,
::,
,
,
K,
None,
))
}
fn _创建底分型_中(: i64, : i64) -> Arc<> {
let = _创建缠K(, , 100.0, 90.0, Some(::));
..store(, Ordering::Relaxed);
..set(90.0);
let = _创建缠K( - 1, - 100, 100.0, 92.0, Some(::));
..store( - 1, Ordering::Relaxed);
let = _创建缠K( + 1, + 100, 100.0, 92.0, Some(::));
..store( + 1, Ordering::Relaxed);
Arc::new(::new(Some(), , Some()))
}
fn _创建顶分型_中(: i64, : i64) -> Arc<> {
let = _创建缠K(, , 100.0, 90.0, Some(::));
..store(, Ordering::Relaxed);
..set(100.0);
let = _创建缠K( - 1, - 100, 98.0, 88.0, Some(::));
..store( - 1, Ordering::Relaxed);
let = _创建缠K( + 1, + 100, 98.0, 88.0, Some(::));
..store( + 1, Ordering::Relaxed);
Arc::new(::new(Some(), , Some()))
}
// ========== 基础买卖点 构造测试 ==========
#[test]
fn test_基础买卖点_new() {
let = _创建底分型_中(10, 1000);
let K = _创建普K(1100, 0);
let bsp = ::new(::, K, .clone(), "测试".into(), 90.0);
assert_eq!(bsp., "测试");
assert_eq!(bsp., ::);
assert_eq!(bsp., 90.0);
assert!(bsp.K线.is_none());
assert!(!bsp.());
}
// ========== 偏移 测试 ==========
#[test]
fn test_偏移_无缠K序号_退化为bar序号差值() {
let = _创建底分型_中(10, 1000);
let K = _创建普K(1100, 15); // bar 序号=15
let bsp = ::new(::, K, .clone(), "".into(), 0.0);
// 无当前缠K序号: 偏移 = 15 - 10 = 5
assert_eq!(bsp.(), 5);
}
#[test]
fn test_偏移_有缠K序号_使用缠K序号差值() {
let = _创建底分型_中(10, 1000);
let K = _创建普K(1100, 100); // bar 序号(不使用)
let mut bsp = ::new(::, K, .clone(), "".into(), 0.0);
bsp.K序号 = Some(15);
// 有当前缠K序号: 偏移 = 15 - 10 = 5
assert_eq!(bsp.(), 5);
}
// ========== 失效偏移 测试 ==========
#[test]
fn test_失效偏移_无失效K线返回负一() {
let = _创建底分型_中(10, 1000);
let K = _创建普K(1100, 0);
let bsp = ::new(::, K, .clone(), "".into(), 0.0);
assert_eq!(bsp.(), -1);
}
#[test]
fn test_失效偏移_有失效K线() {
let = _创建底分型_中(10, 1000);
let K = _创建普K(1100, 0);
let mut bsp = ::new(::, K, .clone(), "".into(), 0.0);
bsp.K线 = Some(_创建普K(1200, 20)); // 序号=20
// 失效偏移 = 20 - 10 = 10
assert_eq!(bsp.(), 10);
}
// ========== 有效性 测试 ==========
#[test]
fn test_有效性_无失效K线为false() {
let = _创建底分型_中(10, 1000);
let K = _创建普K(1100, 0);
let bsp = ::new(::, K, .clone(), "".into(), 0.0);
assert!(!bsp.());
}
#[test]
fn test_有效性_有失效K线为true() {
let = _创建底分型_中(10, 1000);
let K = _创建普K(1100, 0);
let mut bsp = ::new(::, K, .clone(), "".into(), 0.0);
bsp.K线 = Some(_创建普K(1200, 0));
assert!(bsp.());
}
// ========== 生成买卖点 测试 ==========
#[test]
fn test_生成买卖点_一买() {
let = _创建底分型_中(5, 1000);
let K = ..clone();
let bsp = ::("特征A", "", "本级", .clone(), K);
assert_eq!(bsp., ::);
assert_eq!(bsp.K序号, Some(5));
}
#[test]
fn test_生成买卖点_一卖() {
let = _创建顶分型_中(5, 1000);
let K = ..clone();
let bsp = ::("特征A", "", "本级", .clone(), K);
assert_eq!(bsp., ::);
}
#[test]
fn test_生成买卖点_二买() {
let = _创建底分型_中(5, 1000);
let K = ..clone();
let bsp = ::("特征B", "", "同级", .clone(), K);
assert_eq!(bsp., ::);
}
#[test]
fn test_生成买卖点_三卖() {
let = _创建顶分型_中(5, 1000);
let K = ..clone();
let bsp = ::("特征C", "", "本级", .clone(), K);
assert_eq!(bsp., ::);
}
#[test]
fn test_生成买卖点_T1买() {
let = _创建底分型_中(5, 1000);
let K = ..clone();
let bsp = ::("事后", "T1", "次级", .clone(), K);
assert_eq!(bsp., ::T1买);
}
#[test]
fn test_生成买卖点_T2S卖() {
let = _创建顶分型_中(5, 1000);
let K = ..clone();
let bsp = ::("特征D", "T2S", "同级", .clone(), K);
assert_eq!(bsp., ::T2S卖);
}
#[test]
fn test_生成买卖点_T3A买() {
let = _创建底分型_中(5, 1000);
let K = ..clone();
let bsp = ::("特征E", "T3A", "本级", .clone(), K);
assert_eq!(bsp., ::T3A买);
}
#[test]
fn test_生成买卖点_T3B买() {
let = _创建底分型_中(5, 1000);
let K = ..clone();
let bsp = ::("特征F", "T3B", "本级", .clone(), K);
assert_eq!(bsp., ::T3B买);
}
#[test]
fn test_生成买卖点_破位值来自分型特征值() {
let = _创建底分型_中(5, 1000);
let K = ..clone();
let bsp = ::("特征G", "", "本级", .clone(), K);
assert_eq!(bsp., .());
}
}
+109 -39
View File
@@ -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!("{标签}:所有周期观察者全量校验全部一致"))
}
}
File diff suppressed because it is too large Load Diff
+172 -18
View File
@@ -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
View File
@@ -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"
);
}
}
+170
View File
@@ -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.);
}
}
+404
View File
@@ -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);
}
}
}
}
+200
View File
@@ -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(", "))
}
}
+68 -12
View File
@@ -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);
}
+36
View File
@@ -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
+6
View File
@@ -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::;
+61 -15
View File
@@ -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
View File
@@ -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
View File
@@ -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线 {
+1
View File
@@ -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;
+68
View File
@@ -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
View File
@@ -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);
}
File diff suppressed because it is too large Load Diff
+11 -1
View File
@@ -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 : ,
}
+162 -46
View File
@@ -25,17 +25,39 @@
use crate::kline::chan_kline::K线;
use crate::types::;
use crate::types::;
use std::sync::atomic::Ordering;
use crate::warn;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
/// 分型 — 由三根缠K构成(可能缺左或右)
/// 分型模式True 时使用构造时缓存值(默认),False 时从 中 缠K 实时读取
pub static : AtomicBool = AtomicBool::new(true);
/// 分型 — 由左中右三根缠论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,
}
@@ -43,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 {
@@ -56,6 +88,33 @@ impl 分型 {
}
}
/// 时间戳 — 根据 分型模式 决定返回缓存值(True)或实时值(False)
pub fn (&self) -> i64 {
if .load(Ordering::Relaxed) {
self.
} else {
self...load(Ordering::Relaxed)
}
}
/// 结构 — 根据 分型模式 决定返回缓存值(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()?;
@@ -69,7 +128,7 @@ impl 分型 {
/// 分型强度 — 返回 "强"/"中"/"弱"/"未知"
pub fn (&self) -> &str {
if self. != :: && self. != :: {
if self.() != :: && self.() != :: {
return "未知";
}
if self..is_none() || self..is_none() {
@@ -77,7 +136,7 @@ impl 分型 {
}
if let Some(ref ) = self.() {
if self. == :: {
if self.() == :: {
if .2.() {
return "";
} else if .2.() {
@@ -85,7 +144,7 @@ impl 分型 {
} else {
return "";
}
} else if self. == :: {
} else if self.() == :: {
if .2.() {
return "";
} else if .2.() {
@@ -96,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 "";
@@ -122,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柱;
}
@@ -148,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为中元素的分型
@@ -160,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 {
@@ -179,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 {
@@ -210,13 +330,9 @@ impl std::fmt::Display for 分型 {
write!(
f,
"{}<{}, {}, None: {}, None: {}>",
self.
.
.read()
.unwrap()
.unwrap_or(crate::types::::),
self.,
crate::utils::format_f64_g(self.),
self.(),
self.(),
crate::utils::format_f64_g(self.()),
if self..is_none() { "True" } else { "False" },
if self..is_none() { "True" } else { "False" },
)
+157 -86
View File
@@ -26,114 +26,134 @@ 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.
.
.partial_cmp(&b..)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a...cmp(&b..))
.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(
@@ -527,7 +598,7 @@ mod tests {
let = feat.();
// 向上取最大特征值:都是100 → tiebreaker取后时间戳 → 笔2.文(300)
assert_eq!(., 300);
assert_eq!(.(), 300);
}
#[test]
@@ -544,7 +615,7 @@ mod tests {
let = feat.();
// 向上取最大特征值:都是80 → tiebreaker取后时间戳 → 笔2.武(400)
assert_eq!(., 400);
assert_eq!(.(), 400);
}
// ============================================================
@@ -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());
}
+2
View File
@@ -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,
+34 -4
View File
@@ -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 {
+8 -17
View File
@@ -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
}
}
+26
View File
@@ -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 {
+28
View File
@@ -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);
}
+3 -3
View File
@@ -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()
}
}
+179
View File
@@ -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())
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -e
cd "$(dirname "$0")"
echo "=== 1/4 清除 Python 缓存 ==="
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null
find . -type f -name "*.pyc" -delete 2>/dev/null
echo " Python 缓存已清除"
echo "=== 2/4 清除 Cargo 编译缓存 ==="
rm -rf chanlun/target chanlun-py/target
echo " target/ 已清除"
echo "=== 3/4 构建 Release ==="
cd chanlun-py
maturin build --release
echo " 构建完成"
echo "=== 4/4 安装 ==="
pip install --break-system-packages --force-reinstall --no-deps \
target/wheels/chanlun-*.whl
echo " 安装完成"
echo
echo "✓ 清理 + 构建 + 安装完毕"
echo " pip show chanlun | grep Version"
pip show chanlun 2>/dev/null | grep Version
+55
View File
@@ -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线合成器 + 每周期观察者 |
+46
View File
@@ -0,0 +1,46 @@
# Sphinx 配置 — chanlun 缠论技术分析库
#
# 使用 MyST 解析器支持 MarkdownFuro 主题
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
+45
View File
@@ -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/>
```
+3
View File
@@ -0,0 +1,3 @@
sphinx>=8.0
myst-parser>=4.0
furo>=2024.0
+92 -630
View File
@@ -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
View File
@@ -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线/一个数据点
如果没有新数据返回 FalseBacktrader会等待下次调用
"""
"""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
View File
@@ -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});