6 Commits

Author SHA1 Message Date
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
43 changed files with 7896 additions and 2292 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 的截图、日志或对比数据。
+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 代码对齐
+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` 使用是否合理
## 偏好语言
请使用简体中文或英文撰写报告。
+2134 -826
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "chanlun-py"
version = "26.6.17"
version = "26.6.44"
edition = "2024"
description = "缠论技术分析库 — Rust 高性能 Python 绑定"
authors = ["YuYuKunKun"]
@@ -12,7 +12,8 @@ crate-type = ["cdylib"]
name = "chanlun"
[dependencies]
chanlun = "26.6.1" # { path = "../chanlun" }
chanlun = "26.6.2" #{ path = "../chanlun" }
lru = "0.18"
pyo3 = { version = "0.28", features = ["experimental-inspect"] }
serde_json = "1"
chrono = "0.4"
+7
View File
@@ -12,6 +12,13 @@ def get_分型模式() -> bool: ...
def set_分型模式(value: bool) -> None: ...
def 转化为时间戳(ts: Any) -> int: ...
def 转化为时间戳_数字(ts: Any) -> int: ...
def K线相等(A: K线, B: K线, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
def 缠论K线相等(A: 缠论K线, B: 缠论K线, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
def 分型相等(A: 分型, B: 分型, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
def 缺口相等(A: 缺口, B: 缺口, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
def 线段特征相等(A: 线段特征, B: 线段特征, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
def 中枢相等(A: 中枢, B: 中枢, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
def 虚线相等(A: 虚线, B: 虚线, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
# ========== Enum-like types ==========
+8 -3
View File
@@ -14,7 +14,6 @@ __all__ = [
"指标容器",
"指标计算器",
"均线工具",
"测试_读取数据",
"相对强弱指数",
"相对方向",
"立体分析器",
@@ -37,13 +36,19 @@ __all__ = [
"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
from .chan import set_log_level, get_log_level
def set_rs_log_level(level: str):
+38 -6
View File
@@ -5,13 +5,19 @@ from typing import Any, ClassVar, Optional, List, Dict, Tuple, Union
from datetime import datetime
# ========== Module-level functions ==========
def get_rs_log_level() -> str: ...
def set_rs_log_level(level: str) -> None: ...
def get_log_level() -> str: ...
def set_log_level(level: str) -> None: ...
def get_分型模式() -> bool: ...
def set_分型模式(value: bool) -> None: ...
def 转化为时间戳(ts: Any) -> int: ...
def 转化为时间戳_数字(ts: Any) -> int: ...
def K线相等(A: K线, B: K线, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
def 缠论K线相等(A: 缠论K线, B: 缠论K线, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
def 分型相等(A: 分型, B: 分型, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
def 缺口相等(A: 缺口, B: 缺口, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
def 线段特征相等(A: 线段特征, B: 线段特征, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
def 中枢相等(A: 中枢, B: 中枢, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
def 虚线相等(A: 虚线, B: 虚线, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
# ========== Enum-like types ==========
@@ -429,9 +435,9 @@ class 虚线:
@property
def 模式(self) -> str: ...
@property
def 特征序列_显示(self) -> bool: ...
@特征序列_显示.setter
def 特征序列_显示(self, value: bool) -> None: ...
def _特征序列_显示(self) -> bool: ...
@_特征序列_显示.setter
def _特征序列_显示(self, value: bool) -> None: ...
@property
def 特征序列(self) -> List[Optional[线段特征]]: ...
@property
@@ -517,6 +523,8 @@ class 虚线:
class 线段特征:
@property
def 序号(self) -> int: ...
@序号.setter
def 序号(self, value: int) -> None: ...
@property
def 标识(self) -> str: ...
@标识.setter
@@ -784,6 +792,30 @@ class 观察者:
def 扩展线段序列_扩展线段(self) -> List[虚线]: ...
@property
def 扩展中枢序列_扩展线段(self) -> List[中枢]: ...
@property
def 线段分析层次(self) -> int: ...
@线段分析层次.setter
def 线段分析层次(self, value: int) -> None: ...
@property
def 扩展线段分析层次(self) -> int: ...
@扩展线段分析层次.setter
def 扩展线段分析层次(self, value: int) -> None: ...
@property
def 混合扩展线段分析层次(self) -> int: ...
@混合扩展线段分析层次.setter
def 混合扩展线段分析层次(self, value: int) -> None: ...
@property
def 线段序列组(self) -> List[List[虚线]]: ...
@property
def 中枢序列组(self) -> List[List[中枢]]: ...
@property
def 扩展线段序列组(self) -> List[List[虚线]]: ...
@property
def 扩展中枢序列组(self) -> List[List[中枢]]: ...
@property
def 混合扩展线段序列组(self) -> List[List[虚线]]: ...
@property
def 混合扩展中枢序列组(self) -> List[List[中枢]]: ...
def 重置基础序列(self) -> None: ...
def 增加原始K线(self, 普K: K线) -> None: ...
def 投喂原始数据(self, 时间戳: int, : float, : float, : float, : float, : float) -> None: ...
+600 -110
View File
@@ -89,6 +89,13 @@ __all__ = [
"布林带",
"set_log_level",
"get_log_level",
"K线相等",
"缠论K线相等",
"分型相等",
"缺口相等",
"线段特征相等",
"中枢相等",
"虚线相等",
]
# 日志级别映射: 名称 → loguru 级别名
@@ -133,6 +140,312 @@ def get_log_level() -> str:
return _当前日志级别
@lru_cache(128)
def K线相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]:
"""原始K线相等校验:字段完备→浮点容错→普通全等"""
比对字段 = ["标识", "序号", "周期", "时间戳", "", "", "开盘价", "收盘价", "成交量"]
for 字段 in 比对字段:
a有 = hasattr(A, 字段)
b有 = hasattr(B, 字段)
if a有 and not b有:
return False, f"K线校验:字段[{字段}]A存在属性、B缺失属性"
if not a有 and b有:
return False, f"K线校验:字段[{字段}]B存在属性、A缺失属性"
if not (a有 and b有):
continue
valA = getattr(A, 字段)
valB = getattr(B, 字段)
# 双浮点容错对比
if isinstance(valA, float) and isinstance(valB, float):
差值 = abs(valA - valB)
if 差值 > 浮点容差:
return False, f"K线校验:字段[{字段}]浮点超限,容差={浮点容差:.2e}A={valA:.10f},B={valB:.10f},差值={差值:.10f}"
elif 字段 == "时间戳":
if int(valA) != int(valB):
return False, f"K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
else:
if valA != valB:
return False, f"K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
return True, "K线:全部字段结构、数值校验完全一致"
def 缠论K线相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]:
"""缠论K线:基础字段+标的K线递归校验"""
比对字段 = ["序号", "时间戳", "", "", "方向", "分型", "周期", "标识", "分型特征值", "原始起始序号", "原始结束序号", "标的K线", "买卖点信息"]
for 字段 in 比对字段:
a有 = hasattr(A, 字段)
b有 = hasattr(B, 字段)
if a有 and not b有:
return False, f"缠论K线校验:字段[{字段}]A存在、B缺失属性"
if not a有 and b有:
return False, f"缠论K线校验:字段[{字段}]B存在、A缺失属性"
if not (a有 and b有):
continue
valA = getattr(A, 字段)
valB = getattr(B, 字段)
if isinstance(valA, float) and isinstance(valB, float):
差值 = abs(valA - valB)
if 差值 > 浮点容差:
return False, f"缠论K线校验:[{字段}]浮点超限,容差={浮点容差:.2e}A={valA:.10f},B={valB:.10f},差值={差值:.10f}"
elif 字段 == "标的K线":
if valA is None and valB is None:
continue
if valA is None or valB is None:
return False, f"缠论K线校验:[标的K线]单边空值,A={valA is None},B={valB is None}"
eq_flag, msg = K线相等(valA, valB, 浮点容差)
if not eq_flag:
return False, f"缠论K线校验:标的K线子项异常 >> {msg}"
elif 字段 == "时间戳":
if int(valA) != int(valB):
return False, f"缠论K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
elif 字段 == "方向":
if str(valA) != str(valB):
return False, f"缠论K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
elif 字段 == "分型":
if str(valA) != str(valB):
return False, f"缠论K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
elif 字段 == "买卖点信息":
if set(valA) != set(valB):
return False, f"缠论K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
else:
if valA != valB:
return False, f"缠论K线校验:[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
return True, "缠论K线:全部字段、嵌套原始K线校验一致"
def 分型相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]:
"""分型:左/中/右缠论K线递归 + 自有字段"""
比对字段 = ["", "", "", "_结构", "_时间戳", "_分型特征值"]
for 字段 in 比对字段:
a存在 = hasattr(A, 字段)
b存在 = hasattr(B, 字段)
if a存在 and not b存在:
return False, f"分型校验:[{字段}]A有属性、B缺失"
if not a存在 and b存在:
return False, f"分型校验:[{字段}]B有属性、A缺失"
if not (a存在 and b存在):
continue
valA = getattr(A, 字段)
valB = getattr(B, 字段)
if isinstance(valA, float) and isinstance(valB, float):
差值 = abs(valA - valB)
if 差值 > 浮点容差:
return False, f"分型校验:[{字段}]浮点超限,容差={浮点容差:.2e}A={valA:.10f},B={valB:.10f},差值={差值:.10f}"
elif 字段 in ("", "", ""):
if valA is None and valB is None:
continue
if valA is None or valB is None:
return False, f"分型校验:[{字段}]空值不一致,A={valA is None},B={valB is None}"
eq_ok, msg = 缠论K线相等(valA, valB, 浮点容差)
if not eq_ok:
return False, f"分型校验:[{字段}]缠论K线子项异常 >> {msg}"
elif 字段 == "_结构":
if str(valA) != str(valB):
return False, f"分型K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
elif 字段 == "_时间戳":
if int(valA) != int(valB):
return False, f"分型K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
else:
if valA != valB:
return False, f"分型校验:[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
return True, "分型:自有字段+三根缠论K线全部校验一致"
@lru_cache(4096)
def 缺口相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]:
"""缺口:高、低浮点校验"""
比对字段 = ["", ""]
for 字段 in 比对字段:
a有 = hasattr(A, 字段)
b有 = hasattr(B, 字段)
if a有 and not b有:
return False, f"缺口校验:[{字段}]A存在、B缺失属性"
if not a有 and b有:
return False, f"缺口校验:[{字段}]B存在、A缺失属性"
if not (a有 and b有):
continue
valA = getattr(A, 字段)
valB = getattr(B, 字段)
if isinstance(valA, float) and isinstance(valB, float):
差值 = abs(valA - valB)
if 差值 > 浮点容差:
return False, f"缺口校验:[{字段}]浮点超限,容差={浮点容差:.2e}A={valA:.10f},B={valB:.10f},差值={差值:.10f}"
else:
if valA != valB:
return False, f"缺口校验:[{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
return True, "缺口:上下沿价格校验完全一致"
def 线段特征相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]:
"""线段特征:基础序列虚线列表逐项校验"""
比对字段 = ["序号", "标识", "线段方向", "基础序列"]
标签 = f"线段特征校验[A标识={A.标识},B标识={B.标识}]"
for 字段 in 比对字段:
a有 = hasattr(A, 字段)
b有 = hasattr(B, 字段)
if a有 and not b有:
return False, f"{标签}: [{字段}]A存在、B缺失属性"
if not a有 and b有:
return False, f"{标签}: [{字段}]B存在、A缺失属性"
if not (a有 and b有):
continue
valA = getattr(A, 字段)
valB = getattr(B, 字段)
if 字段 == "基础序列":
if len(valA) != len(valB):
return False, f"{标签}: [基础序列]列表长度不一致,A长度={len(valA)},B长度={len(valB)}"
for idx, (itemA, itemB) in enumerate(zip(valA, valB)):
eq, msg = 虚线相等(itemA, itemB, 浮点容差)
if not eq:
return False, f"{标签}:基础序列[{idx}]子虚线异常 >> {msg}"
elif 字段 == "线段方向":
if str(valA) != str(valB):
return False, f"{标签}: [{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
else:
if valA != valB:
return False, f"{标签}: [{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
return True, f"{标签}:字段与内部虚线序列全部一致"
def 中枢相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]:
"""中枢:基础序列虚线列表+第三买卖线单虚线"""
比对字段 = ["序号", "标识", "级别", "基础序列", "第三买卖线", "本级_第三买卖线"]
标签 = f"中枢校验[A标识={A.标识},B标识={B.标识}]"
for 字段 in 比对字段:
a有 = hasattr(A, 字段)
b有 = hasattr(B, 字段)
if a有 and not b有:
return False, f"{标签}: [{字段}]A存在、B缺失属性"
if not a有 and b有:
return False, f"{标签}: [{字段}]B存在、A缺失属性"
if not (a有 and b有):
continue
valA = getattr(A, 字段)
valB = getattr(B, 字段)
if 字段 == "基础序列":
if len(valA) != len(valB):
return False, f"{标签}: [基础序列]长度不一致 A={len(valA)},B={len(valB)}"
for idx, (itemA, itemB) in enumerate(zip(valA, valB)):
eq, msg = 虚线相等(itemA, itemB, 浮点容差)
if not eq:
return False, f"{标签}:基础序列[{idx}]虚线异常 >> {msg}"
elif 字段 in ("第三买卖线", "本级_第三买卖线"):
if valA is None and valB is None:
continue
if valA is None or valB is None:
return False, f"{标签}: [{字段}]空值不一致 A={valA is None},B={valB is None}"
eq, msg = 虚线相等(valA, valB, 浮点容差)
if not eq:
return False, f"{标签}: [{字段}]子虚线异常 >> {msg}"
else:
if valA != valB:
return False, f"{标签}: [{字段}]数值不等,A={repr(valA)},B={repr(valB)}"
return True, f"{标签}:基础序列+第三买卖线全部校验一致"
def 虚线相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]:
"""虚线(笔/线段):全量字段、分型/缺口/K线/列表嵌套精细化报错"""
比对字段 = ["标识", "序号", "级别", "", "", "有效性", "基础序列", "特征序列", "实_中枢序列", "虚_中枢序列", "合_中枢序列", "确认K线", "模式", "_特征序列_显示", "前一缺口", "前一结束位置", "短路修正"]
标签 = f"虚线校验[A标识={A.标识},B标识={B.标识}]"
for 字段 in 比对字段:
a有 = hasattr(A, 字段)
b有 = hasattr(B, 字段)
if a有 and not b有:
return False, f"{标签}: [{字段}]A存在属性、B缺失属性"
if not a有 and b有:
return False, f"{标签}: [{字段}]B存在属性、A缺失属性"
if not (a有 and b有):
continue
valA = getattr(A, 字段)
valB = getattr(B, 字段)
# 文/武:分型
if 字段 in ("", ""):
if valA is None and valB is None:
continue
if valA is None or valB is None:
return False, f"{标签}: [{字段}]分型空值不一致 A={valA is None},B={valB is None}"
eq, msg = 分型相等(valA, valB, 浮点容差)
if not eq:
return False, f"{标签}: [{字段}]子分型异常 >> {msg}"
# 前一缺口
elif 字段 == "前一缺口":
if valA is None and valB is None:
continue
if valA is None or valB is None:
return False, f"{标签}: [前一缺口]空值不一致 A={valA is None},B={valB is None}"
eq, msg = 缺口相等(valA, valB, 浮点容差)
if not eq:
return False, f"{标签}: [前一缺口]子缺口异常 >> {msg}"
# 前一缺口
elif 字段 == "前一结束位置":
if valA is None and valB is None:
continue
if valA is None or valB is None:
return False, f"{标签}: [前一结束位置]空值不一致 A={valA is None},B={valB is None}"
eq, msg = 虚线相等(valA, valB, 浮点容差)
if not eq:
return False, f"{标签}: [前一结束位置]异常 >> {msg}"
# 确认K线
elif 字段 == "确认K线":
if valA is None and valB is None:
continue
if valA is None or valB is None:
return False, f"{标签}: [确认K线]空值不一致 A={valA is None},B={valB is None}"
eq, msg = 缠论K线相等(valA, valB, 浮点容差)
if not eq:
return False, f"{标签}: [确认K线]子缠论K线异常 >> {msg}"
# 各类列表
elif 字段 in ("基础序列", "实_中枢序列", "虚_中枢序列", "合_中枢序列", "特征序列"):
if len(valA) != len(valB):
return False, f"{标签}: [{字段}]列表长度不一致 A={len(valA)},B={len(valB)}"
for idx, (itemA, itemB) in enumerate(zip(valA, valB)):
if itemA is None and itemB is None:
continue
if itemA is None or itemB is None:
return False, f"{标签}: [{字段}][{idx}]单项空值不一致 A={itemA is None},B={itemB is None}"
if 字段 == "基础序列":
eq, msg = 虚线相等(itemA, itemB, 浮点容差)
elif "中枢" in 字段:
eq, msg = 中枢相等(itemA, itemB, 浮点容差)
else:
eq, msg = 线段特征相等(itemA, itemB, 浮点容差)
if not eq:
return False, f"{标签}: [{字段}][{idx}]子项异常 >> {msg}"
# 普通字段
else:
if valA != valB:
return False, f"{标签}: [{字段}]数值不等 A={repr(valA)},B={repr(valB)}"
return True, f"{标签}:全字段、所有嵌套子结构校验全部一致"
class 买卖点类型(str, Enum):
"""买卖点类型 — 缠论的三类买卖点及扩展类型。
@@ -2073,6 +2386,9 @@ class 指标计算器:
指标计算器._更新均线(当前K线, 全序列, 配置)
if prev is not None:
指标计算器._回填新指标(全序列, 配置)
@staticmethod
def _计算MACD组(当前K线: K线, prev: Optional[指标容器], 配置: 缠论配置):
idx = 当前K线.指标
@@ -2147,6 +2463,86 @@ class 指标计算器:
前值 = 普K序列[-2].指标.均线.get(key)
当前K线.指标.均线[key] = 均线工具.增量EMA(普K序列, period, 配置.指标计算方式, 前值)
@staticmethod
def _回填新指标(全序列: List[K线], 配置: 缠论配置):
"""运行中新增指标参数时,回填所有历史K线。
比较首尾K线的指标键检测运行中动态添加到配置的新指标参数
然后从第一根K线开始逐根重新计算使历史K线也能获得新指标值
"""
首K指标 = 全序列[0].指标
尾K指标 = 全序列[-1].指标
if 首K指标 is None or 尾K指标 is None:
return
def _新键(尾指标, 首指标, 参数列表):
新参数 = []
for params in 参数列表:
key = params[0]
if key in 尾指标 and key not in 首指标:
新参数.append(params)
return 新参数
新MACD = _新键(尾K指标, 首K指标, 配置._解析MACD参数列表())
新RSI = _新键(尾K指标, 首K指标, 配置._解析RSI周期列表())
新KDJ = _新键(尾K指标, 首K指标, 配置._解析KDJ参数列表())
新BOLL = _新键(尾K指标, 首K指标, 配置._解析BOLL参数列表())
if not (新MACD or 新RSI or 新KDJ or 新BOLL):
return
计算方式 = 配置.指标计算方式
for i, k线 in enumerate(全序列):
if k线.指标 is None:
k线.指标 = 指标容器()
idx = k线.指标
prev = 全序列[i - 1].指标 if i > 0 else None
for key, , , 信号 in 新MACD:
prev_val = prev[key] if prev is not None and key in prev else None
if prev_val is not None:
idx[key] = 平滑异同移动平均线.增量计算_K线(prev_val, k线, 计算方式)
else:
idx[key] = 平滑异同移动平均线.首次计算_K线(k线, 计算方式, , , 信号)
for key, 周期 in 新RSI:
prev_val = prev[key] if prev is not None and key in prev else None
if prev_val is not None:
idx[key] = 相对强弱指数.增量计算_K线(prev_val, k线, 计算方式)
else:
idx[key] = 相对强弱指数.首次计算_K线(
k线,
计算方式,
周期,
配置.相对强弱指数_超买阈值,
配置.相对强弱指数_超卖阈值,
配置.相对强弱指数_移动平均线周期,
)
for key, rsv, k平滑, d平滑 in 新KDJ:
prev_val = prev[key] if prev is not None and key in prev else None
if prev_val is not None:
idx[key] = 随机指标.增量计算_K线(prev_val, k线, 计算方式)
else:
idx[key] = 随机指标.首次计算_K线(
k线,
计算方式,
rsv,
k平滑,
d平滑,
配置.随机指标_超买阈值,
配置.随机指标_超卖阈值,
)
for key, 周期, 标准差倍数 in 新BOLL:
prev_val = prev[key] if prev is not None and key in prev else None
if prev_val is not None:
idx[key] = 布林带.增量计算(prev_val, k线, 计算方式)
else:
idx[key] = 布林带.首次计算(k线, 计算方式, 周期, 标准差倍数)
class 背驰分析:
"""静态方法容器,提供背驰/背离检测算法。
@@ -2433,7 +2829,7 @@ class K线:
)
@classmethod
def 创建普K(cls, 标识: str, 时间戳: datetime, 开盘价: float, 最高价: float, 最低价: float, 收盘价: float, 成交量: float, 序号: int, 周期: int) -> K线:
def 创建普K(cls, 标识: str, 时间戳: datetime | int, 开盘价: float, 最高价: float, 最低价: float, 收盘价: float, 成交量: float, 序号: int, 周期: int) -> K线:
"""快捷构造普通K线
:param 标识: K线标识符
@@ -2451,7 +2847,7 @@ class K线:
标识=标识,
序号=序号,
周期=周期,
时间戳=时间戳,
时间戳=时间戳 if isinstance(时间戳, datetime) else 转化为时间戳(时间戳), # 注意此处只为兼容Rust绑定
开盘价=开盘价,
最高价=最高价,
最低价=最低价,
@@ -3513,7 +3909,6 @@ class 虚线:
return 结果
@classmethod
@lru_cache(maxsize=128)
def 买卖意义(cls, 实线: 虚线, 观察员: 观察者) -> Tuple[bool, str]:
"""
静止是相对的而运动是绝对的
@@ -3575,6 +3970,14 @@ class 虚线:
结果.append(当前段柱子)
当前段柱子 = [k线序列[i].macd.MACD柱]
当前符号 = 新符号
if 当前段柱子:
结果.append(当前段柱子)
"""a = [x for sub in 结果 for x in sub]
b = [sub.macd.MACD柱 for sub in k线序列]
if list(a) != list(b):
for i,(j,k) in enumerate(zip(a, b)):
if j is not k:
raise RuntimeError( f"序列不一致,{len(a)}, {len(b)}, {(i,j,k)}")"""
return tuple(结果)
@classmethod
@@ -4768,7 +5171,7 @@ class 线段:
@classmethod
def _缺口突破(cls, 线段序列: List[虚线], 配置: 缠论配置, 层级: int) -> bool:
"""内部方法:处理缺口突破修正, 正常修正
除此之外的修正皆为短路修正此修正为正常处理
:param 线段序列: 线段列表
:param 配置: 缠论配置
:param 层级: 递归深度
@@ -4863,6 +5266,7 @@ class 线段:
for 临时虚线 in 基础序列[基础序列.index(当前线段.基础序列[-1]) + 1 :]:
线段._添加虚线(当前线段, 临时虚线)
线段._刷新(当前线段, 配置)
当前线段.短路修正 = True
if 当前线段.特征序列[2] is not None:
= 虚线.创建线段([, , ])
@@ -4984,6 +5388,12 @@ class 线段:
return None
# raise RuntimeError("线段分析 层级过深")
# if len(笔序列) < 3:return None
try:
笔序列[2]
except IndexError:
return None
线段递归分析 = 线段.分析
# -------------------- 1. 初始化第一个线段 --------------------
@@ -5193,7 +5603,7 @@ class 线段:
if not 配置.扩展线段_当下分析:
, , = 当前线段.基础序列[:3]
if not 相对方向.分析(., ., ., .).是否缺口():
当前线段.基础序列[:] = 当前线段.序列[:3]
当前线段.基础序列[:] = 当前线段.基础序列[:3]
线段._武终(当前线段, sys._getframe().f_lineno)
else:
线段._弹出扩展线段(线段序列, 当前线段, sys._getframe().f_lineno)
@@ -5224,7 +5634,6 @@ class 线段:
return 线段递归扩展分析(虚线序列, 线段序列, 配置)
@classmethod
@lru_cache(maxsize=128)
def 判断线段内部是否背驰(cls, 当前段: 虚线, 观察员: 观察者) -> bool:
"""判断线段内部是否发生背驰(基于内部中枢和MACD)
@@ -5528,11 +5937,10 @@ class 中枢:
线段.扩展分析(self.基础序列, 扩展线段, 配置)
中枢.分析(扩展线段, 扩展中枢, False, f"{self.标识}_扩展中枢_")
def _校验合法性(self, 序列: Sequence[虚线], 中枢序列: List[中枢]) -> bool:
def _校验合法性(self, 序列: Sequence[虚线]) -> bool:
"""校验当前中枢在给定序列中是否仍然合法
:param 序列: 基础虚线序列
:param 中枢序列: 中枢列表
:return: 合法为True不合法会原地裁剪基础序列
"""
有效序列 = self.基础序列[:]
@@ -5747,7 +6155,7 @@ class 中枢:
当前中枢 = 中枢序列[-1]
if not 当前中枢._校验合法性(虚线序列, 中枢序列):
if not 当前中枢._校验合法性(虚线序列):
中枢._从中枢序列尾部弹出(中枢序列, 当前中枢)
return 中枢递归分析(虚线序列, 中枢序列, 跳过首部, 标识, 层级 + 1)
@@ -5822,8 +6230,41 @@ class 观察者:
self.配置: 缠论配置 = 配置
self.__终止时间戳: Optional[datetime] = 转化为时间戳(self.配置.手动终止) if self.配置.手动终止 else None
self.线段分析层次 = 3
self.扩展线段分析层次 = 3
self.混合扩展线段分析层次 = 3
self.重置基础序列()
def 重置基础序列(self):
"""清空所有分析序列,重置为初始状态"""
self.基础缠K序列: List[缠论K线] = []
self.普通K线序列: List[K线] = []
self.缠论K线序列: List[缠论K线] = []
self.分型序列: List[分型] = []
self.笔序列: List[虚线] = []
self.笔_中枢序列: List[中枢] = []
self.线段序列组: List[List[虚线],] = [] # 线段, 线段<线段>,线段<线段<线段>>...
self.中枢序列组: List[List[中枢],] = []
for i in range(self.线段分析层次):
self.线段序列组.append(list())
self.中枢序列组.append(list())
self.扩展线段序列组: List[List[虚线],] = [] # 扩展线段, 扩展线段<扩展线段>, 扩展线段<扩展线段<扩展线段>>...
self.扩展中枢序列组: List[List[中枢],] = []
for i in range(self.扩展线段分析层次):
self.扩展线段序列组.append(list())
self.扩展中枢序列组.append(list())
self.混合扩展线段序列组: List[List[虚线],] = [] # 扩展线段<线段>, 扩展线段<线段<线段>>, 扩展线段<线段<线段<线段>>>...
self.混合扩展中枢序列组: List[List[中枢],] = []
for i in range(self.混合扩展线段分析层次):
self.混合扩展线段序列组.append(list())
self.混合扩展中枢序列组.append(list())
@property
def 观察员(self):
"""观察员(自引用)"""
@@ -5850,32 +6291,45 @@ class 观察者:
"""
return self.缠论K线序列[-1] if self.缠论K线序列 else None
def 重置基础序列(self):
"""清空所有分析序列,重置为初始状态"""
self.基础缠K序列: List[缠论K线] = []
@property
def 线段序列(self) -> List[虚线]:
return self.线段序列组[0]
self.普通K线序列: List[K线] = []
self.缠论K线序列: List[缠论K线] = []
@property
def 中枢序列(self) -> List[中枢]:
return self.中枢序列组[0]
self.分型序列: List[分型] = []
@property
def 扩展线段序列(self) -> List[虚线]:
return self.扩展线段序列组[0]
self.笔序列: List[虚线] = []
self.笔_中枢序列: List[中枢] = []
@property
def 扩展中枢序列(self) -> List[中枢]:
return self.扩展中枢序列组[0]
self.线段序列: List[虚线] = []
self.中枢序列: List[中枢] = []
@property
def 扩展线段序列_线段(self) -> List[虚线]:
return self.混合扩展线段序列组[0]
self.扩展线段序列: List[虚线] = []
self.扩展中枢序列: List[中枢] = []
@property
def 扩展中枢序列_线段(self) -> List[中枢]:
return self.混合扩展中枢序列组[0]
self.扩展线段序列_线段: List[虚线] = []
self.扩展中枢序列_线段: List[中枢] = []
@property
def 线段_线段序列(self) -> List[虚线]:
return self.线段序列组[1]
self.线段_线段序列: List[虚线] = []
self.线段_中枢序列: List[中枢] = []
@property
def 线段_中枢序列(self) -> List[中枢]:
return self.中枢序列组[1]
self.扩展线段序列_扩展线段: List[虚线] = []
self.扩展中枢序列_扩展线段: List[中枢] = []
@property
def 扩展线段序列_扩展线段(self) -> List[虚线]:
return self.扩展线段序列组[1]
@property
def 扩展中枢序列_扩展线段(self) -> List[中枢]:
return self.扩展中枢序列组[1]
def 投喂原始数据(self, 时间戳: datetime, : float, : float, : float, : float, : float):
"""便捷入口,直接从 OHLCV 创建 K线 并投喂
@@ -5905,28 +6359,33 @@ class 观察者:
if 当前分型 is None:
return
self.配置.分析笔 and .分析(当前分型, self.分型序列, self.笔序列, self.缠论K线序列, self.普通K线序列, 0, self.配置)
.分析(当前分型, self.分型序列, self.笔序列, self.缠论K线序列, self.普通K线序列, 0, self.配置)
if not self.分型序列:
return
self.配置.分析笔中枢 and 中枢.分析(self.笔序列, self.笔_中枢序列)
中枢.分析(self.笔序列, self.笔_中枢序列, True, "", 0)
if not self.笔序列:
return
self.配置.分析线段 and 线段.分析(self.笔序列, self.线段序列, self.配置)
self.配置.分析线段中枢 and 中枢.分析(self.线段序列, self.中枢序列)
for i in range(self.线段分析层次):
if i == 0:
线段.分析(self.笔序列, self.线段序列组[i], self.配置)
中枢.分析(self.线段序列组[i], self.中枢序列组[i], True, "", 0)
continue
线段.分析(self.线段序列组[i - 1], self.线段序列组[i], self.配置)
中枢.分析(self.线段序列组[i], self.中枢序列组[i], True, "", 0)
self.配置.分析扩展线段 and 线段.扩展分析(self.笔序列, self.扩展线段序列, self.配置)
self.配置.分析线段中枢 and 中枢.分析(self.扩展线段序列, self.扩展中枢序列)
for i in range(self.扩展线段分析层次):
if i == 0:
线段.扩展分析(self.笔序列, self.扩展线段序列组[i], self.配置)
中枢.分析(self.扩展线段序列组[i], self.扩展中枢序列组[i], True, "", 0)
continue
线段.扩展分析(self.扩展线段序列组[i - 1], self.扩展线段序列组[i], self.配置)
中枢.分析(self.扩展线段序列组[i], self.扩展中枢序列组[i], True, "", 0)
self.配置.分析扩展线段 and 线段.扩展分析(self.线段序列, self.扩展线段序列_线段, self.配置)
self.配置.分析线段中枢 and 中枢.分析(self.扩展线段序列_线段, self.扩展中枢序列_线段)
self.配置.分析线段 and 线段.分析(self.线段序列, self.线段_线段序列, self.配置)
self.配置.分析线段中枢 and 中枢.分析(self.线段_线段序列, self.线段_中枢序列)
self.配置.分析扩展线段 and 线段.扩展分析(self.扩展线段序列, self.扩展线段序列_扩展线段, self.配置)
self.配置.分析线段中枢 and 中枢.分析(self.扩展线段序列_扩展线段, self.扩展中枢序列_扩展线段)
for i in range(self.混合扩展线段分析层次):
线段.扩展分析(self.线段序列[i], self.混合扩展线段序列组[i], self.配置)
中枢.分析(self.混合扩展线段序列组[i], self.混合扩展中枢序列组[i], True, "", 0)
def 测试_保存数据(self, root: str = None) -> str:
"""拆分各序列数据,单独存文件,文件名为对应变量名
@@ -5934,30 +6393,15 @@ class 观察者:
:param root: 保存根目录可选
:return: 保存目录
"""
# 提取各类文本数据
笔序列_文本数据 = [.获取数据文本() for in self.笔序列]
线段序列_文本数据 = [实线.获取数据文本() for 实线 in self.线段序列]
扩展线段序列_数据文本 = [实线.获取数据文本() for 实线 in self.扩展线段序列]
扩展线段序列_线段_数据文本 = [实线.获取数据文本() for 实线 in self.扩展线段序列_线段]
线段_线段序列_数据文本 = [实线.获取数据文本() for 实线 in self.线段_线段序列]
扩展线段序列_扩展线段_数据文本 = [实线.获取数据文本() for 实线 in self.扩展线段序列_扩展线段]
笔_中枢序列_数据文本 = [当前中枢.获取数据文本() for 当前中枢 in self.笔_中枢序列]
中枢序列_数据文本 = [当前中枢.获取数据文本() for 当前中枢 in self.中枢序列]
扩展中枢序列_数据文本 = [当前中枢.获取数据文本() for 当前中枢 in self.扩展中枢序列]
扩展中枢序列_线段_数据文本 = [当前中枢.获取数据文本() for 当前中枢 in self.扩展中枢序列_线段]
线段_中枢序列_数据文本 = [当前中枢.获取数据文本() for 当前中枢 in self.线段_中枢序列]
扩展中枢序列_扩展线段_数据文本 = [当前中枢.获取数据文本() for 当前中枢 in self.扩展中枢序列_扩展线段]
# ===================== 优化点:优先使用传入的 root 目录 =====================
if root is not None:
# 使用用户指定的根目录
根目录 = Path(root)
else:
# 默认:当前脚本所在目录
根目录 = Path(__file__).parent
# 默认:系统临时目录
根目录 = tempfile.gettempdir()
# 生成子目录名称(不变)
# 生成子目录名称
起始时间 = int(self.普通K线序列[0].时间戳.timestamp())
结束时间 = int(self.普通K线序列[-1].时间戳.timestamp())
目录标识 = f"Py_{self.标识}_{起始时间}_{结束时间}"
@@ -5966,28 +6410,29 @@ class 观察者:
保存路径 = 根目录 / 目录标识
保存路径.mkdir(exist_ok=True, parents=True) # parents=True 支持多级目录自动创建
# 映射:变量名 -> 数据列表
数据映射 = [
("笔序列_文本数据", 笔序列_文本数据),
("线段序列_文本数据", 线段序列_文本数据),
("扩展线段序列_数据文本", 扩展线段序列_数据文本),
("扩展线段序列_线段_数据文本", 扩展线段序列_线段_数据文本),
("线段_线段序列_数据文本", 线段_线段序列_数据文本),
("扩展线段序列_扩展线段_数据文本", 扩展线段序列_扩展线段_数据文本),
("笔_中枢序列_数据文本", 笔_中枢序列_数据文本),
("中枢序列_数据文本", 中枢序列_数据文本),
("扩展中枢序列_数据文本", 扩展中枢序列_数据文本),
("扩展中枢序列_线段_数据文本", 扩展中枢序列_线段_数据文本),
("线段_中枢序列_数据文本", 线段_中枢序列_数据文本),
("扩展中枢序列_扩展线段_数据文本", 扩展中枢序列_扩展线段_数据文本),
]
# 逐个写入独立文件
for 文件名, 数据列表 in 数据映射:
def 保存序列(序列):
if len(序列) == 0:
return
数据列表 = [对象.获取数据文本() for 对象 in 序列]
文件名 = 序列[0].标识
文件全路径 = 保存路径 / f"{文件名}.txt"
# 逐个写入独立文件
with open(文件全路径, "w", encoding="utf-8") as f:
f.write("\n".join(数据列表))
f.write("\n") # 向 C99 对齐
保存序列(self.笔序列)
保存序列(self.笔_中枢序列)
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])
logger.warning(f"全部数据拆分保存完成,目录:{保存路径.resolve()}")
return str(保存路径.resolve())
@@ -5996,44 +6441,55 @@ class 观察者:
pass
def 静态重新分析(self):
"""静态重新分析(占位方法)"""
"""静态重新分析"""
self.分型序列: List[分型] = []
self.笔序列: List[虚线] = []
self.笔_中枢序列: List[中枢] = []
self.线段序列: List[虚线] = []
self.中枢序列: List[中枢] = []
self.线段序列: List[List[虚线],] = [] # 线段, 线段<线段>,线段<线段<线段>>...
self.中枢序列: List[List[中枢],] = []
for i in range(self.线段分析层次):
self.线段序列组.append(list())
self.中枢序列组.append(list())
self.扩展线段序列: List[虚线] = []
self.扩展中枢序列: List[中枢] = []
self.扩展线段序列: List[List[虚线],] = [] # 扩展线段, 扩展线段<扩展线段>, 扩展线段<扩展线段<扩展线段>>...
self.扩展中枢序列: List[List[中枢],] = []
for i in range(self.扩展线段分析层次):
self.扩展线段序列组.append(list())
self.扩展中枢序列组.append(list())
self.扩展线段序列_线段: List[虚线] = []
self.扩展中枢序列_线段: List[中枢] = []
self.线段_线段序列: List[虚线] = []
self.线段_中枢序列: List[中枢] = []
self.扩展线段序列_扩展线段: List[虚线] = []
self.扩展中枢序列_扩展线段: List[中枢] = []
self.混合扩展线段序列: List[List[虚线],] = [] # 扩展线段<线段>, 扩展线段<线段<线段>>, 扩展线段<线段<线段<线段>>>...
self.混合扩展中枢序列: List[List[中枢],] = []
for i in range(self.混合扩展线段分析层次):
self.混合扩展线段序列组.append(list())
self.混合扩展中枢序列组.append(list())
for i in range(1, len(self.缠论K线序列) - 1):
当前分型 = 分型(self.缠论K线序列[i - 1], self.缠论K线序列[i], self.缠论K线序列[i + 1])
.分析(当前分型, self.分型序列, self.笔序列, self.缠论K线序列, self.普通K线序列, 0, self.配置)
self.配置.分析笔中枢 and 中枢.分析(self.笔序列, self.笔_中枢序列)
中枢.分析(self.笔序列, self.笔_中枢序列, True, "", 0)
self.配置.分析线段 and 线段.分析(self.笔序列, self.线段序列, self.配置)
self.配置.分析线段中枢 and 中枢.分析(self.线段序列, self.中枢序列)
for i in range(self.线段分析层次):
if i == 0:
线段.分析(self.笔序列, self.线段序列组[i], self.配置)
中枢.分析(self.线段序列组[i], self.中枢序列组[i], True, "", 0)
continue
线段.分析(self.线段序列组[i - 1], self.线段序列组[i], self.配置)
中枢.分析(self.线段序列组[i], self.中枢序列组[i], True, "", 0)
self.配置.分析扩展线段 and 线段.扩展分析(self.笔序列, self.扩展线段序列, self.配置)
self.配置.分析线段中枢 and 中枢.分析(self.扩展线段序列, self.扩展中枢序列)
for i in range(self.扩展线段分析层次):
if i == 0:
线段.扩展分析(self.笔序列, self.扩展线段序列组[i], self.配置)
中枢.分析(self.扩展线段序列组[i], self.扩展中枢序列组[i], True, "", 0)
continue
线段.扩展分析(self.扩展线段序列组[i - 1], self.扩展线段序列组[i], self.配置)
中枢.分析(self.扩展线段序列组[i], self.扩展中枢序列组[i], True, "", 0)
self.配置.分析扩展线段 and 线段.扩展分析(self.线段序列, self.扩展线段序列_线段, self.配置)
self.配置.分析线段中枢 and 中枢.分析(self.扩展线段序列_线段, self.扩展中枢序列_线段)
self.配置.分析线段 and 线段.分析(self.线段序列, self.线段_线段序列, self.配置)
self.配置.分析线段中枢 and 中枢.分析(self.线段_线段序列, self.线段_中枢序列)
for i in range(self.混合扩展线段分析层次):
线段.扩展分析(self.线段序列[i], self.混合扩展线段序列组[i], self.配置)
中枢.分析(self.混合扩展线段序列组[i], self.混合扩展中枢序列组[i], True, "", 0)
def 加载本地数据(self, 文件路径: str):
"""重置基础序列后加载数据文件
@@ -6048,18 +6504,21 @@ class 观察者:
self.投喂原始数据(转化为时间戳(int(时间戳)), 开盘价, 最高价, 最低价, 收盘价, 成交量)
@classmethod
def 读取数据文件(cls, 观察员: 观察者, 文件路径: str, 配置=缠论配置()) -> Self:
def 读取数据文件(cls, 文件路径: str, 配置=缠论配置(), *, 观察员: Optional[观察者] = None) -> Self:
"""加载数据文件
:param 观察员: 观察者
:param 文件路径: 数据文件路径 格式如: btcusd-300-1631772074-1632222374.nb
:param 配置: 缠论配置
:param 观察员: 可选已有观察者实例不传则自动创建
:return: 观察者实例
"""
name = Path(文件路径).name.split(".")[0]
符号, 周期, 起始时间戳, 结束时间戳 = name.split("-")
观察员.符号 = 符号
观察员.周期 = int(周期)
观察员.配置 = 配置
if 观察员 is None:
观察员 = cls(符号, int(周期), 配置)
else:
观察员.符号 = 符号
观察员.周期 = int(周期)
观察员.配置 = 配置
观察员.加载本地数据(文件路径)
return 观察员
@@ -6316,13 +6775,13 @@ class 立体分析器:
:return: 数据保存目录路径
"""
# 生成存储根目录
脚本目录 = Path(__file__).parent if not root else root # 取当前脚本所在文件夹
脚本目录 = tempfile.gettempdir() if not root else root # 默认系统临时目录
起始时间 = int(self._单体分析器[self.__输入周期].普通K线序列[0].时间戳.timestamp())
结束时间 = int(self._单体分析器[self.__输入周期].普通K线序列[-1].时间戳.timestamp())
目录标识 = f"PyM_{self._单体分析器[self.__输入周期].标识}_{起始时间}_{结束时间}"
# 最终保存路径 = 脚本目录 / 自动生成的文件夹
保存路径 = 脚本目录 / 目录标识
保存路径 = Path(os.path.join(脚本目录, 目录标识))
保存路径.mkdir(exist_ok=True)
for 周期 in self.周期组:
@@ -6340,7 +6799,7 @@ def 测试_读取数据(观察员: 观察者, 配置: 缠论配置) -> Callable[
def 魔法():
启动时间 = datetime.now()
观察者.读取数据文件(观察员, 配置.加载文件路径, 配置)
观察者.读取数据文件(配置.加载文件路径, 配置, 观察员=观察员)
消耗用时 = datetime.now() - 启动时间
logger.info(f"测试_读取数据 耗时 {消耗用时} 普K数量 {len(观察员.普通K线序列)}")
return 观察员
@@ -6377,9 +6836,40 @@ def 测试_周期合成(配置: 缠论配置, 配置组: Dict[int, 缠论配置]
return 魔法
def 测试_指标挂载(配置: 缠论配置):
文件路径 = 配置.加载文件路径
name = Path(文件路径).name.split(".")[0]
符号, 周期, 起始时间戳, 结束时间戳 = name.split("-")
周期 = int(周期)
观察员 = 观察者(符号, 周期, 配置)
def 魔法():
启动时间 = datetime.now()
with open(文件路径, "rb") as f:
buffer = f.read()
size = struct.calcsize(">6d")
for i in range(len(buffer) // size):
if i == 500:
配置.MACD_参数列表 = [("macd", 配置.平滑异同移动平均线_快线周期, 配置.平滑异同移动平均线_慢线周期, 配置.平滑异同移动平均线_信号周期)]
配置.MACD_参数列表.append(("macd_12_26_9", 12, 26, 9))
k线 = K线.读取大端字节数组(buffer[i * size : i * size + size], 周期, 符号)
观察员.增加原始K线(k线)
if i == 500:
assert 观察员.普通K线序列[0].指标.macd_12_26_9 is not None, "指标挂载失败"
print(观察员.普通K线序列[0].指标["macd_12_26_9"])
break
消耗用时 = datetime.now() - 启动时间
logger.info(f"测试_指标挂载 耗时 {消耗用时} 普K数量 {len(观察员.普通K线序列)}")
return 观察员
return 魔法
if __name__ == "__main__":
当前配置 = 缠论配置.不推送()
当前配置.加载文件路径 = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "tests", "btcusd-300-1761327300-1776327900.nb")
with tempfile.TemporaryDirectory() as tmpdir:
测试_读取数据(观察者("", 0, 当前配置), 当前配置)().测试_保存数据(tmpdir)
测试_周期合成(当前配置)().测试_保存数据(tmpdir)
# 测试_读取数据(观察者("", 0, 当前配置), 当前配置)().测试_保存数据(tmpdir)
# 测试_周期合成(当前配置)().测试_保存数据(tmpdir)
测试_指标挂载(当前配置)().测试_保存数据(tmpdir)
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "chanlun"
version = "2606.17"
version = "2606.44"
description = "缠论技术分析库 — Rust 高性能实现"
readme = { file = "README.md", content-type = "text/markdown" }
license = { file = "LICENSE", content-type = "text/plain" }
+93 -35
View File
@@ -25,7 +25,7 @@
use crate::kline_py::chan_kline_to_py;
use crate::structure_py::{dashed_to_py, fractal_to_py};
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyType};
use pyo3::types::{PyDict, PyList, PyType};
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::RwLock;
@@ -308,11 +308,13 @@ impl 笔Py {
#[classmethod]
#[pyo3(signature = (当前分型, 分型序列, 笔序列, 缠K序列, 普K序列, 递归层次, 配置))]
/// 笔划分核心递归算法
/// 分型序列/笔序列 原地修改(与 chan.py 行为一致)
/// :return: 递归层次
fn (
_cls: &Bound<'_, PyType>,
: Option<&Bound<'_, Py>>,
: Vec<Py<Py>>,
: Vec<Py<线Py>>,
: &Bound<'_, PyList>,
: &Bound<'_, PyList>,
K序列: Vec<Py<crate::kline_py::K线Py>>,
K序列: Vec<Py<K线Py>>,
: i64,
@@ -321,14 +323,19 @@ impl 笔Py {
) -> PyResult<i64> {
let _ = ; // Python API 兼容参数,核心从0开始计数
let _rc = .map(|f| Arc::clone(&f.borrow().inner));
let mut fr_seq: Vec<Arc<chanlun::structure::fractal_obj::>> =
.iter()
.map(|f| Arc::clone(&f.bind(py).borrow().inner))
.collect();
let mut bi_seq: Vec<Arc<chanlun::structure::dash_line::线>> =
.iter()
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
.collect();
// 从 Python 列表提取
let mut fr_seq = Vec::with_capacity(.len());
for item in .iter() {
let f: PyRef<'_, Py> = item.extract()?;
fr_seq.push(Arc::clone(&f.inner));
}
let mut bi_seq = Vec::with_capacity(.len());
for item in .iter() {
let d: PyRef<'_, 线Py> = item.extract()?;
bi_seq.push(Arc::clone(&d.inner));
}
let ck_list: Vec<Arc<chanlun::kline::chan_kline::K线>> = K序列
.iter()
.map(|k| Arc::clone(&k.bind(py).borrow().inner))
@@ -338,8 +345,8 @@ impl 笔Py {
.map(|k| k.bind(py).borrow().inner.clone())
.collect();
let config = .borrow().to_rust_config(py)?;
match _rc {
Some(fr) => Ok(chanlun::algorithm::bi::::(
let depth = match _rc {
Some(fr) => chanlun::algorithm::bi::::(
fr,
&mut fr_seq,
&mut bi_seq,
@@ -347,9 +354,21 @@ impl 笔Py {
&bar_list,
,
&config,
)),
None => Ok(),
),
None => ,
};
// 写回 Python 列表
.call_method0("clear")?;
for f in fr_seq {
.call_method1("append", (fractal_to_py(py, f),))?;
}
.call_method0("clear")?;
for d in bi_seq {
.call_method1("append", (dashed_to_py(py, d),))?;
}
Ok(depth)
}
#[classmethod]
@@ -502,11 +521,13 @@ impl 线段Py {
}
#[classmethod]
#[pyo3(signature = (笔序列, 线段序列, 配置, 层级 = 0, 关系序列 = None))]
/// 线段划分核心递归算法
/// 线段序列 原地修改(与 chan.py 行为一致)
fn (
_cls: &Bound<'_, PyType>,
: Vec<Py<线Py>>,
线: Vec<Py<线Py>>,
线: &Bound<'_, PyList>,
: &Bound<'_, Py>,
: i64,
: Option<Vec<Py>>,
@@ -516,10 +537,13 @@ impl 线段Py {
.iter()
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
.collect();
let mut seg_seq: Vec<Arc<chanlun::structure::dash_line::线>> = 线
.iter()
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
.collect();
let mut seg_seq = Vec::with_capacity(线.len());
for item in 线.iter() {
let d: PyRef<'_, 线Py> = item.extract()?;
seg_seq.push(Arc::clone(&d.inner));
}
let config = .borrow().to_rust_config(py)?;
let default_rel = vec![
chanlun::types::::,
@@ -535,15 +559,22 @@ impl 线段Py {
,
&rel_list,
);
// 写回 Python 列表
线.call_method0("clear")?;
for d in seg_seq {
线.call_method1("append", (dashed_to_py(py, d),))?;
}
Ok(())
}
#[classmethod]
/// 即同级别分析
/// 线段序列 原地修改(与 chan.py 行为一致)
fn (
_cls: &Bound<'_, PyType>,
线: Vec<Py<线Py>>,
线: Vec<Py<线Py>>,
线: &Bound<'_, PyList>,
: &Bound<'_, Py>,
py: Python<'_>,
) -> PyResult<()> {
@@ -551,12 +582,21 @@ impl 线段Py {
.iter()
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
.collect();
let mut seg_seq: Vec<Arc<chanlun::structure::dash_line::线>> = 线
.iter()
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
.collect();
let mut seg_seq = Vec::with_capacity(线.len());
for item in 线.iter() {
let d: PyRef<'_, 线Py> = item.extract()?;
seg_seq.push(Arc::clone(&d.inner));
}
let config = .borrow().to_rust_config(py)?;
chanlun::algorithm::segment::线::(&dash_list, &mut seg_seq, &config);
// 写回 Python 列表
线.call_method0("clear")?;
for d in seg_seq {
线.call_method1("append", (dashed_to_py(py, d),))?;
}
Ok(())
}
@@ -792,18 +832,26 @@ impl 中枢Py {
}
/// 当基础序列>=9时,从中枢中提取扩展线段中枢
/// 扩展中枢 原地修改(与 chan.py 行为一致)
fn (
&self,
: Vec<Py<Self>>,
: &Bound<'_, PyList>,
: &Bound<'_, crate::config_py::Py>,
py: Python<'_>,
) -> PyResult<()> {
let mut hub_seq: Vec<Arc<chanlun::algorithm::hub::>> =
.iter()
.map(|h| Arc::clone(&h.bind(py).borrow().inner))
.collect();
let mut hub_seq = Vec::with_capacity(.len());
for item in .iter() {
let h: PyRef<'_, Py> = item.extract()?;
hub_seq.push(Arc::clone(&h.inner));
}
let config = .borrow().to_rust_config(.py())?;
self.inner.(&mut hub_seq, &config);
// 写回 Python 列表
.call_method0("clear")?;
for h in hub_seq {
.call_method1("append", (hub_to_py(py, h),))?;
}
Ok(())
}
@@ -889,10 +937,11 @@ impl 中枢Py {
#[classmethod]
#[pyo3(signature = (虚线序列, 中枢序列, 跳过首部 = true, 标识 = "", 层级 = 0))]
/// 中枢识别核心递归算法
/// 中枢序列 原地修改(与 chan.py 行为一致)
fn (
_cls: &Bound<'_, PyType>,
线: Vec<Py<线Py>>,
: Vec<Py<Self>>,
: &Bound<'_, PyList>,
: bool,
: &str,
: i64,
@@ -902,11 +951,20 @@ impl 中枢Py {
.iter()
.map(|d| Arc::clone(&d.bind(py).borrow().inner))
.collect();
let mut hub_seq: Vec<Arc<chanlun::algorithm::hub::>> =
.iter()
.map(|h| Arc::clone(&h.bind(py).borrow().inner))
.collect();
let mut hub_seq = Vec::with_capacity(.len());
for item in .iter() {
let h: PyRef<'_, Py> = item.extract()?;
hub_seq.push(Arc::clone(&h.inner));
}
chanlun::algorithm::hub::::(&rc_list, &mut hub_seq, , , );
// 写回 Python 列表
.call_method0("clear")?;
for h in hub_seq {
.call_method1("append", (hub_to_py(py, h),))?;
}
Ok(())
}
+187 -29
View File
@@ -675,6 +675,13 @@ impl 买卖点Py {
/// 线段_线段序列 / 线段_中枢序列
/// 扩展线段序列_扩展线段 / 扩展中枢序列_扩展线段
///
/// 分析层次(可读写):
/// 线段分析层次: int (默认 3) / 扩展线段分析层次: int (默认 3) / 混合扩展线段分析层次: int (默认 3)
///
/// 序列组(只读,返回 list[list[...]]:
/// 线段序列组 / 中枢序列组 / 扩展线段序列组 / 扩展中枢序列组
/// 混合扩展线段序列组 / 混合扩展中枢序列组
///
/// 核心方法:
/// 增加原始K线(普K) — 喂入一根普通K线,触发全层级增量计算
/// 重置基础序列() — 清空所有计算状态和序列
@@ -686,6 +693,7 @@ impl 买卖点Py {
#[pyclass(name = "观察者", module = "chanlun._chanlun", subclass)]
pub struct Py {
pub(crate) inner: Option<Arc<RwLock<chanlun::business::observer::>>>,
: std::sync::Mutex<Option<Py<Py>>>,
}
impl Py {
@@ -762,6 +770,7 @@ impl 观察者Py {
inner: Some(chanlun::business::observer::::new(
, , config,
)),
: std::sync::Mutex::new(None),
})
}
@@ -814,8 +823,27 @@ impl 观察者Py {
}
#[getter]
fn (&self) -> PyResult<Py> {
Py::from_rust_config(&self.obs().)
fn (&self, py: Python<'_>) -> PyResult<Py<Py>> {
let mut cache = self..lock().unwrap();
if let Some(ref cached) = *cache {
Ok(cached.clone_ref(py))
} else {
let cfg_py = Py::from_rust_config(&self.obs().)?;
let obj = Py::new(py, cfg_py)?;
*cache = Some(obj.clone_ref(py));
Ok(obj)
}
}
#[setter]
fn set_配置(&self, value: &Bound<'_, Py>) -> PyResult<()> {
let config = value.borrow().to_rust_config(value.py())?;
self.obs_mut(). = config;
self.
.lock()
.unwrap()
.replace(value.clone().unbind());
Ok(())
}
/// 清空所有分析序列,重置为初始状态(内部实现)
@@ -840,6 +868,16 @@ impl 观察者Py {
/// 核心入口 — 投喂一根原始K线,增量更新所有层级(公开分发器,支持子类重写)
fn K线(slf: &Bound<'_, Self>, K: &Bound<'_, K线Py>) -> PyResult<()> {
// 同步缓存的 Python 配置到 Rust 观察者(支持 obs.配置 直接修改)
{
let me = slf.borrow();
if let Some(ref cached) = *me..lock().unwrap() {
let py = slf.py();
if let Ok(config) = cached.bind(py).borrow().to_rust_config(py) {
me.obs_mut(). = config;
}
}
}
slf.call_method1("_增加原始K线", (K,))?;
Ok(())
}
@@ -918,16 +956,16 @@ impl 观察者Py {
}
#[classmethod]
#[pyo3(signature = (观察员, 文件路径, 配置 = None))]
/// :param 观察员: 观察者实例
#[pyo3(signature = (文件路径, 配置 = None, 观察员 = None))]
/// :param 文件路径: 数据文件路径 格式如: btcusd-300-1631772074-1632222374.nb
/// :param 配置: 缠论配置
/// :param 观察员: 可选,已有观察者实例;不传则自动创建
/// :return: 观察者实例
fn (
_cls: &Bound<'_, PyType>,
: &Bound<'_, Self>,
: &str,
: Option<&Bound<'_, Py>>,
: Option<&Bound<'_, Self>>,
py: Python<'_>,
) -> PyResult<Py<PyAny>> {
let config = match {
@@ -953,19 +991,37 @@ impl 观察者Py {
.parse()
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("parse period: {}", e)))?;
// 设置观察员属性
{
let slf_ref = .borrow_mut();
let mut obs_mut = slf_ref.obs_mut();
obs_mut. = ;
obs_mut. = ;
obs_mut. = config;
}
let obs_ref = match {
Some(obs) => {
// 更新已有观察员属性
{
let slf_ref = obs.borrow_mut();
let mut obs_mut = slf_ref.obs_mut();
obs_mut. = ;
obs_mut. = ;
obs_mut. = config;
}
obs.clone()
}
None => {
// 创建新观察者:调用 cls(符号, 周期),配置后续通过 obs_mut 设置
let obj = _cls.call1((.as_str(), ))?;
let obs: &Bound<'_, Self> = obj.cast().map_err(|_| {
pyo3::exceptions::PyTypeError::new_err("failed to create 观察者")
})?;
{
let slf_ref = obs.borrow_mut();
let mut obs_mut = slf_ref.obs_mut();
obs_mut. = config;
}
obs.clone()
}
};
// 调用加载本地数据
.call_method1("加载本地数据", (,))?;
// 调用加载本地数据(通过 Python dispatch,支持子类重写)
obs_ref.call_method1("加载本地数据", (,))?;
Ok(.clone().unbind().into())
Ok(obs_ref.unbind().into())
}
// ---- 序列 getters ----
@@ -1040,7 +1096,7 @@ impl 观察者Py {
#[getter]
fn 线(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let list = pyo3::types::PyList::empty(py);
for d in &self.obs().线 {
for d in self.obs().线() {
list.append(dashed_to_py(py, Arc::clone(d)))?;
}
Ok(list.into())
@@ -1049,7 +1105,7 @@ impl 观察者Py {
#[getter]
fn (&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let list = pyo3::types::PyList::empty(py);
for h in &self.obs(). {
for h in self.obs().() {
list.append(hub_to_py(py, Arc::clone(h)))?;
}
Ok(list.into())
@@ -1058,7 +1114,7 @@ impl 观察者Py {
#[getter]
fn 线(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let list = pyo3::types::PyList::empty(py);
for d in &self.obs().线 {
for d in self.obs().线() {
list.append(dashed_to_py(py, Arc::clone(d)))?;
}
Ok(list.into())
@@ -1067,7 +1123,7 @@ impl 观察者Py {
#[getter]
fn (&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let list = pyo3::types::PyList::empty(py);
for h in &self.obs(). {
for h in self.obs().() {
list.append(hub_to_py(py, Arc::clone(h)))?;
}
Ok(list.into())
@@ -1076,7 +1132,7 @@ impl 观察者Py {
#[getter]
fn 线_线段(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let list = pyo3::types::PyList::empty(py);
for d in &self.obs().线_线段 {
for d in self.obs().线_线段() {
list.append(dashed_to_py(py, Arc::clone(d)))?;
}
Ok(list.into())
@@ -1085,7 +1141,7 @@ impl 观察者Py {
#[getter]
fn _线段(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let list = pyo3::types::PyList::empty(py);
for h in &self.obs()._线段 {
for h in self.obs()._线段() {
list.append(hub_to_py(py, Arc::clone(h)))?;
}
Ok(list.into())
@@ -1094,7 +1150,7 @@ impl 观察者Py {
#[getter]
fn 线_线段序列(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let list = pyo3::types::PyList::empty(py);
for d in &self.obs().线_线段序列 {
for d in self.obs().线_线段序列() {
list.append(dashed_to_py(py, Arc::clone(d)))?;
}
Ok(list.into())
@@ -1103,7 +1159,7 @@ impl 观察者Py {
#[getter]
fn 线_中枢序列(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let list = pyo3::types::PyList::empty(py);
for h in &self.obs().线_中枢序列 {
for h in self.obs().线_中枢序列() {
list.append(hub_to_py(py, Arc::clone(h)))?;
}
Ok(list.into())
@@ -1112,7 +1168,7 @@ impl 观察者Py {
#[getter]
fn 线_扩展线段(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let list = pyo3::types::PyList::empty(py);
for d in &self.obs().线_扩展线段 {
for d in self.obs().线_扩展线段() {
list.append(dashed_to_py(py, Arc::clone(d)))?;
}
Ok(list.into())
@@ -1121,11 +1177,112 @@ impl 观察者Py {
#[getter]
fn _扩展线段(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
let list = pyo3::types::PyList::empty(py);
for h in &self.obs()._扩展线段 {
for h in self.obs()._扩展线段() {
list.append(hub_to_py(py, Arc::clone(h)))?;
}
Ok(list.into())
}
// ---- 分析层次 getter/setter ----
#[getter]
fn 线(&self) -> usize {
self.obs().线
}
#[setter]
#[pyo3(name = "线段分析层次")]
fn _线段分析层次(&mut self, value: usize) {
self.obs_mut().线 = value;
}
#[getter]
fn 线(&self) -> usize {
self.obs().线
}
#[setter]
#[pyo3(name = "扩展线段分析层次")]
fn _扩展线段分析层次(&mut self, value: usize) {
self.obs_mut().线 = value;
}
#[getter]
fn 线(&self) -> usize {
self.obs().线
}
#[setter]
#[pyo3(name = "混合扩展线段分析层次")]
fn _混合扩展线段分析层次(&mut self, value: usize) {
self.obs_mut().线 = value;
}
// ---- 序列组 getters (返回 list[list[...]]) ----
#[getter]
fn 线(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
self._虚线序列组_to_py(&self.obs().线, py)
}
#[getter]
fn (&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
self._中枢序列组_to_py(&self.obs()., py)
}
#[getter]
fn 线(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
self._虚线序列组_to_py(&self.obs().线, py)
}
#[getter]
fn (&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
self._中枢序列组_to_py(&self.obs()., py)
}
#[getter]
fn 线(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
self._虚线序列组_to_py(&self.obs().线, py)
}
#[getter]
fn (&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
self._中枢序列组_to_py(&self.obs()., py)
}
}
impl Py {
fn _虚线序列组_to_py(
&self,
: &Vec<Vec<Arc<chanlun::structure::dash_line::线>>>,
py: Python<'_>,
) -> PyResult<Py<PyAny>> {
let outer = pyo3::types::PyList::empty(py);
for inner_vec in {
let inner_list = pyo3::types::PyList::empty(py);
for d in inner_vec {
inner_list.append(dashed_to_py(py, Arc::clone(d)))?;
}
outer.append(inner_list)?;
}
Ok(outer.into())
}
fn _中枢序列组_to_py(
&self,
: &Vec<Vec<Arc<chanlun::algorithm::hub::>>>,
py: Python<'_>,
) -> PyResult<Py<PyAny>> {
let outer = pyo3::types::PyList::empty(py);
for inner_vec in {
let inner_list = pyo3::types::PyList::empty(py);
for h in inner_vec {
inner_list.append(hub_to_py(py, Arc::clone(h)))?;
}
outer.append(inner_list)?;
}
Ok(outer.into())
}
}
// ========== K线合成器 ==========
@@ -1274,9 +1431,10 @@ impl 立体分析器Py {
}
fn (&self, : i64) -> Option<Py> {
self.inner
.()
.map(|rc| Py { inner: Some(rc) })
self.inner.().map(|rc| Py {
inner: Some(rc),
: std::sync::Mutex::new(None),
})
}
/// 拆分各序列数据,单独存文件,文件名为对应变量名
+5 -1
View File
@@ -414,6 +414,8 @@ fn validate_field(
(Value::Bool(_), Value::Bool(_)) => return Ok(()),
(Value::Number(_), Value::Number(_)) => return Ok(()),
(Value::String(_), Value::String(_)) => return Ok(()),
(Value::Array(_), Value::Array(_)) => return Ok(()),
(Value::Object(_), Value::Object(_)) => return Ok(()),
_ => {}
}
@@ -430,7 +432,9 @@ fn validate_field(
Value::Bool(_) => "布尔",
Value::Number(_) => "数值",
Value::String(_) => "字符串",
_ => "其他",
Value::Array(_) => "数组",
Value::Object(_) => "字典",
Value::Null => "null",
};
Err(format!("类型不匹配(需要 {expected},收到 {type_name}"))
}
+780
View File
@@ -0,0 +1,780 @@
/*
* 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::num::NonZeroUsize;
use std::sync::Mutex;
use lru::LruCache;
use pyo3::prelude::*;
/// 缓存辅助宏:在调用点创建静态 LruCache,先查后存
macro_rules! with_cache {
($cache:ident, $size:literal, $key_expr:expr, $compute:expr) => {{
use std::sync::LazyLock;
static $cache: LazyLock<Mutex<LruCache<(usize, usize, i64), (bool, String)>>> =
LazyLock::new(|| Mutex::new(LruCache::new(NonZeroUsize::new($size).unwrap())));
let key = $key_expr;
if let Some(cached) = $cache.lock().unwrap().get(&key) {
return Ok(cached.clone());
}
let result: PyResult<(bool, String)> = $compute;
if let Ok(ref r) = result {
$cache.lock().unwrap().put(key, r.clone());
}
result
}};
}
/// 从 Python 值中提取时间戳(兼容 i64 和 datetime 两种类型)
fn (val: &Bound<'_, PyAny>) -> PyResult<i64> {
if let Ok(ts) = val.extract::<i64>() {
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)> {
with_cache!(
C_KLINE,
128,
(
A.as_ptr() as usize,
B.as_ptr() as usize,
.to_bits() as i64
),
{
// 快速路径
if let (Ok(a), Ok(b)) = (
A.cast::<crate::kline_py::K线Py>(),
B.cast::<crate::kline_py::K线Py>(),
) {
return Ok(a.borrow().inner.(&b.borrow().inner, ));
}
// 回退路径
let = "K线校验";
let = [
"标识",
"序号",
"周期",
"时间戳",
"",
"",
"开盘价",
"收盘价",
"成交量",
];
for & in & {
let (a有, b有) = (A.hasattr()?, B.hasattr()?);
if a有 && !b有 {
return Ok((false, format!("{标签}: [{字段}] A存在属性 B缺失属性")));
}
if !a有 && b有 {
return Ok((false, format!("{标签}: [{字段}] B存在属性 A缺失属性")));
}
if !a有 && !b有 {
continue;
}
let valA = A.getattr()?;
let valB = B.getattr()?;
if let Some(r) = (&valA, &valB, ) {
let (ok, m) = r?;
if !ok {
return Ok((false, format!("{标签}: [{字段}]{}", m)));
}
} else if == "时间戳" {
let a = (&valA).unwrap_or(0);
let b = (&valB).unwrap_or(0);
if a != b {
return Ok((false, format!("{标签}: [{字段}] 数值不等 A={a},B={b}")));
}
} else {
let eq: bool = valA.eq(&valB)?;
if !eq {
let ra = valA.repr()?.extract::<String>().unwrap_or_default();
let rb = valB.repr()?.extract::<String>().unwrap_or_default();
return Ok((false, format!("{标签}: [{字段}] 数值不等 A={ra},B={rb}")));
}
}
}
Ok((true, format!("{标签}: 全部字段一致")))
}
)
}
// ========== 缠论K线相等 ==========
#[pyfunction]
#[pyo3(signature = (A, B, 浮点容差 = 1e-9))]
fn K线相等(
A: &Bound<'_, PyAny>,
B: &Bound<'_, PyAny>,
: f64,
) -> PyResult<(bool, String)> {
with_cache!(
C_CHAN_K,
4096,
(
A.as_ptr() as usize,
B.as_ptr() as usize,
.to_bits() as i64
),
{
if let (Ok(a), Ok(b)) = (
A.cast::<crate::kline_py::K线Py>(),
B.cast::<crate::kline_py::K线Py>(),
) {
return Ok(a.borrow().inner.(&b.borrow().inner, ));
}
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)> {
with_cache!(
C_FRACTAL,
4096,
(
A.as_ptr() as usize,
B.as_ptr() as usize,
.to_bits() as i64
),
{
if let (Ok(a), Ok(b)) = (
A.cast::<crate::structure_py::Py>(),
B.cast::<crate::structure_py::Py>(),
) {
return Ok(a.borrow().inner.(&b.borrow().inner, ));
}
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)> {
with_cache!(
C_GAP,
4096,
(
A.as_ptr() as usize,
B.as_ptr() as usize,
.to_bits() as i64
),
{
if let (Ok(a), Ok(b)) = (
A.cast::<crate::types_py::Py>(),
B.cast::<crate::types_py::Py>(),
) {
return Ok(a.borrow().inner.(&b.borrow().inner, ));
}
let = "缺口校验";
for & in &["", ""] {
let (a有, b有) = (A.hasattr()?, B.hasattr()?);
if a有 && !b有 {
return Ok((false, format!("{标签}: [{字段}] A存在 B缺失属性")));
}
if !a有 && b有 {
return Ok((false, format!("{标签}: [{字段}] B存在 A缺失属性")));
}
if !a有 && !b有 {
continue;
}
let valA = A.getattr()?;
let valB = B.getattr()?;
if let Some(r) = (&valA, &valB, ) {
let (ok, m) = r?;
if !ok {
return Ok((false, format!("{标签}: [{字段}]{m}")));
}
} else {
let eq: bool = valA.eq(&valB)?;
if !eq {
let ra = valA.repr()?.extract::<String>().unwrap_or_default();
let rb = valB.repr()?.extract::<String>().unwrap_or_default();
return Ok((false, format!("{标签}: [{字段}] 数值不等 A={ra},B={rb}")));
}
}
}
Ok((true, format!("{标签}: 上下沿价格校验完全一致")))
}
)
}
// ========== 线段特征相等 ==========
#[pyfunction]
#[pyo3(signature = (A, B, 浮点容差 = 1e-9))]
fn 线(
A: &Bound<'_, PyAny>,
B: &Bound<'_, PyAny>,
: f64,
) -> PyResult<(bool, String)> {
with_cache!(
C_SEG_FEAT,
4096,
(
A.as_ptr() as usize,
B.as_ptr() as usize,
.to_bits() as i64
),
{
if let (Ok(a), Ok(b)) = (
A.cast::<crate::structure_py::线Py>(),
B.cast::<crate::structure_py::线Py>(),
) {
return Ok(a.borrow().inner.(&b.borrow().inner, ));
}
let = "线段特征校验";
for & in &["序号", "标识", "线段方向", "基础序列"] {
let (a有, b有) = (A.hasattr()?, B.hasattr()?);
if a有 && !b有 {
return Ok((false, format!("{标签}: [{字段}] A存在 B缺失属性")));
}
if !a有 && b有 {
return Ok((false, format!("{标签}: [{字段}] B存在 A缺失属性")));
}
if !a有 && !b有 {
continue;
}
let valA = A.getattr()?;
let valB = B.getattr()?;
if == "基础序列" {
let len_a: usize = valA.len()?;
let len_b: usize = valB.len()?;
if len_a != len_b {
return Ok((
false,
format!("{标签}: [基础序列] 列表长度不一致 A={len_a},B={len_b}"),
));
}
for idx in 0..len_a {
let itemA = valA.get_item(idx)?;
let itemB = valB.get_item(idx)?;
let (eq, msg) = 线(&itemA, &itemB, )?;
if !eq {
return Ok((
false,
format!("{标签}: 基础序列[{idx}]子虚线异常 >> {msg}"),
));
}
}
} else if == "线段方向" {
let sa = valA.str()?.extract::<String>().unwrap_or_default();
let sb = valB.str()?.extract::<String>().unwrap_or_default();
if sa != sb {
return Ok((false, format!("{标签}: [{字段}] 数值不等 A={sa},B={sb}")));
}
} else {
let eq: bool = valA.eq(&valB)?;
if !eq {
let ra = valA.repr()?.extract::<String>().unwrap_or_default();
let rb = valB.repr()?.extract::<String>().unwrap_or_default();
return Ok((false, format!("{标签}: [{字段}] 数值不等 A={ra},B={rb}")));
}
}
}
Ok((true, format!("{标签}: 字段与内部虚线序列全部一致")))
}
)
}
// ========== 中枢相等 ==========
#[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!("{标签}: 全字段所有嵌套子结构校验一致")))
}
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)?)?;
Ok(())
}
+6 -15
View File
@@ -985,26 +985,17 @@ impl 指标计算器Py {
/// 增量计算所有开启的指标,将结果写入 当前K线.指标
#[staticmethod]
fn (
K线: &Bound<'_, crate::kline_py::K线Py>,
_当前K线: &Bound<'_, crate::kline_py::K线Py>,
: Vec<Py<crate::kline_py::K线Py>>,
: &Bound<'_, crate::config_py::Py>,
py: Python<'_>,
) -> PyResult<()> {
let config = .borrow().to_rust_config(py)?;
// 全序列包含 当前K线 在末尾;Rust 计算并挂载 的 现有序列 不含当前K线
let : Vec<Arc<chanlun::kline::bar::K线>> = if .len() > 1 {
[...len() - 1]
.iter()
.map(|k| k.bind(py).borrow().inner.clone())
.collect()
} else {
Vec::new()
};
chanlun::indicators::::(
&K线.borrow().inner,
&,
&config,
);
let _rust: Vec<Arc<chanlun::kline::bar::K线>> =
.iter()
.map(|k| k.bind(py).borrow().inner.clone())
.collect();
chanlun::indicators::::(&_rust, &config);
Ok(())
}
}
+26 -11
View File
@@ -23,7 +23,7 @@
*/
use pyo3::prelude::*;
use pyo3::types::{PyBytes, PyDict, PyType};
use pyo3::types::{PyBytes, PyDict, PyList, PyType};
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::RwLock;
@@ -663,25 +663,30 @@ impl 缠论K线Py {
#[classmethod]
/// 分析K线,执行指标计算+包含处理+分型判定
/// 缠K序列/普K序列 原地修改(与 chan.py 行为一致)
/// :return: (状态, 分型|None)
fn (
_cls: &Bound<'_, PyType>,
K线: &Bound<'_, K线Py>,
K序列: Vec<Py<Self>>,
K序列: Vec<Py<K线Py>>,
K序列: &Bound<'_, PyList>,
K序列: &Bound<'_, PyList>,
: &Bound<'_, Py>,
py: Python<'_>,
) -> PyResult<(String, Option<Py<PyAny>>)> {
let ck_inner = (*K线.borrow().inner).clone();
let config = .borrow().to_rust_config(py)?;
let mut ck_seq: Vec<_> = K序列
.iter()
.map(|k| std::sync::Arc::clone(&k.bind(py).borrow().inner))
.collect();
let mut bar_seq: Vec<_> = K序列
.iter()
.map(|k| k.bind(py).borrow().inner.clone())
.collect();
// 从 Python 列表提取
let mut ck_seq = Vec::with_capacity(K序列.len());
for item in K序列.iter() {
let ck: PyRef<'_, Self> = item.extract()?;
ck_seq.push(std::sync::Arc::clone(&ck.inner));
}
let mut bar_seq = Vec::with_capacity(K序列.len());
for item in K序列.iter() {
let bar: PyRef<'_, K线Py> = item.extract()?;
bar_seq.push(bar.inner.clone());
}
let (status, fractal) = chanlun::kline::chan_kline::K线::(
ck_inner,
@@ -690,6 +695,16 @@ impl 缠论K线Py {
&config,
);
// 写回 Python 列表(clear + extend
K序列.call_method0("clear")?;
for k in ck_seq {
K序列.call_method1("append", (chan_kline_to_py(py, k),))?;
}
K序列.call_method0("clear")?;
for k in bar_seq {
K序列.call_method1("append", (bar_to_py(py, k),))?;
}
Ok((status, fractal.map(|f| fractal_to_py(py, f).into_any())))
}
+3
View File
@@ -103,6 +103,7 @@ fn init_tracing() {
mod algorithm_py;
mod business_py;
mod config_py;
mod equality_py;
mod indicators_py;
mod kline_py;
mod structure_py;
@@ -175,6 +176,8 @@ fn _chanlun(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
algorithm_py::register(m)?;
// 阶段 7: 业务
business_py::register(m)?;
// 阶段 8: 相等校验函数
equality_py::register(m)?;
Ok(())
}
+7 -1
View File
@@ -994,7 +994,13 @@ pub struct 线段特征Py {
impl 线Py {
#[getter]
fn (&self) -> i64 {
self.inner.
self.inner..load(Ordering::Relaxed)
}
#[setter]
#[pyo3(name = "序号")]
fn set_序号(&self, value: i64) {
self.inner..store(value, Ordering::Relaxed);
}
#[getter]
Binary file not shown.
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "chanlun"
version = "26.6.1"
version = "26.6.2"
edition = "2024"
license = "MIT"
description = "基于缠论(缠中说禅)理论的量化技术分析核心库,支持流式数据处理和多周期联立分析。"
+816 -284
View File
File diff suppressed because it is too large Load Diff
+6 -4
View File
@@ -645,11 +645,13 @@ impl 笔 {
}
if let Some() = .last() {
let seg = Arc::make_mut(&mut );
seg.
let = Arc::make_mut(&mut );
.
.store(..load(Ordering::Relaxed) + 1, Ordering::Relaxed);
if seg..read().unwrap()..is_none() || seg..read().unwrap()..is_none() {
seg..store(false, Ordering::Relaxed);
if ..read().unwrap()..is_none() || ..read().unwrap()..is_none()
{
..store(false, Ordering::Relaxed);
}
if matches!(
..read().unwrap().(),
+1 -1
View File
@@ -222,6 +222,6 @@ struct MACD面积 {
impl MACD面积 {
fn (&self) -> f64 {
self. + self.
self. + self..abs()
}
}
+95 -2
View File
@@ -387,7 +387,7 @@ impl 中枢 {
pub fn (
: Arc<线>, : Arc<线>, : Arc<线>, : i64, : &str
) -> Self {
debug_assert!(Self::(&, &, &), "中枢.创建 基础检查失败");
assert!(Self::(&, &, &), "中枢.创建 基础检查失败");
Self::new(
0,
format!("{}中枢<{}>", , ..read().unwrap()),
@@ -566,7 +566,7 @@ impl 中枢 {
} else {
if .is_empty() {
// 仍在范围内:延伸中枢
debug_assert!(
assert!(
[_idx]
.
.read()
@@ -615,6 +615,99 @@ 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().unwrap() != *other..read().unwrap() {
return (
false,
format!(
"中枢: [标识] 不等 A={},B={}",
self..read().unwrap(),
other..read().unwrap()
),
);
}
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().unwrap();
let b_seq = other..read().unwrap();
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().unwrap(),
&other.线.read().unwrap(),
,
)
.map_err(|e| (false, e))
.ok();
(
"本级_第三买卖线",
&self._第三买卖线.read().unwrap(),
&other._第三买卖线.read().unwrap(),
,
)
.map_err(|e| (false, e))
.ok();
(true, "中枢: 全部字段一致".into())
}
}
impl std::fmt::Display for {
+12 -35
View File
@@ -32,9 +32,8 @@ use crate::structure::dash_line::虚线;
use crate::structure::fractal_obj::;
use crate::structure::segment_feat::线;
use crate::types::{, , };
use cached::stores::LruCache;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::sync::{Arc, LazyLock, Mutex};
use tracing::warn;
/// 线段 — 从笔生成线段的算法集合(静态方法命名空间)
@@ -48,10 +47,6 @@ type 分割结果 = (
);
type = (Vec<Arc<>>, Vec<Arc<>>, Vec<Arc<>>);
/// 线段内部背驰判断缓存 — 仿照 dash_line.rs 的 买卖意义缓存 模式
static 线: LazyLock<Mutex<LruCache<(usize, usize), bool>>> =
LazyLock::new(|| Mutex::new(LruCache::with_size(128)));
impl 线 {
// ================================================================
// 基础操作
@@ -785,7 +780,7 @@ impl 线段 {
let 线 = 线.last().unwrap();
debug_assert!(
assert!(
线..read().unwrap()[2].is_some()
|| 线..load(Ordering::Relaxed),
"线段._向序列中添加[{}], 之前线段.右 = None {}",
@@ -849,10 +844,9 @@ impl 线段 {
}
}
let mut = 线.pop().unwrap();
let seg = Arc::make_mut(&mut );
*seg..write().unwrap() = None;
seg..store(false, Ordering::Relaxed);
let = 线.pop().unwrap();
..store(false, Ordering::Relaxed);
*..write().unwrap() = None;
Some()
}
@@ -1051,6 +1045,7 @@ impl 线段 {
Self::_刷新(&线[idx], );
let 线 = Arc::clone(&线[idx]);
线..store(true, Ordering::Relaxed);
if 线..read().unwrap()[2].is_some() {
let = 线::线(&[
Arc::clone(&[.len() - 3]),
@@ -1510,9 +1505,8 @@ impl 线段 {
}
if Arc::as_ptr(线.last().unwrap()) == Arc::as_ptr(线) {
let mut drop = 线.pop().unwrap();
let seg = Arc::make_mut(&mut drop);
seg..store(false, Ordering::Relaxed);
let drop = 线.pop().unwrap();
drop..store(false, Ordering::Relaxed);
Some(drop)
} else {
panic!("线段._从序列中删除 弹出数据不在列表中 {}", 线);
@@ -1692,22 +1686,7 @@ impl 线段 {
///
/// 分析线段的内部中枢和MACD柱分段,判断是否发生内部背驰
pub fn 线(: &线, : &) -> bool {
let key = (
as *const 线 as usize,
as *const as usize,
);
{
let mut cache = 线.lock().unwrap();
if let Some(val) = cached::Cached::cache_get(&mut *cache, &key) {
return *val;
}
}
let result = Self::线_impl(, );
let mut cache = 线.lock().unwrap();
cached::Cached::cache_set(&mut *cache, key, result);
result
Self::线_impl(, )
}
fn 线_impl(: &线, : &) -> bool {
@@ -1724,7 +1703,7 @@ impl 线段 {
let = &[.len() - 3];
let = &[.len() - 1];
debug_assert!(
assert!(
..load(Ordering::Relaxed) < ..load(Ordering::Relaxed),
"进入段.序号 >= 离开段.序号"
);
@@ -1780,7 +1759,7 @@ impl 线段 {
{
let = &..read().unwrap()[ - 1];
let = &[.len() - 1];
debug_assert!(
assert!(
..load(Ordering::Relaxed) < ..load(Ordering::Relaxed)
);
if .() != .() {
@@ -1807,9 +1786,7 @@ impl 线段 {
// 第三买卖点后盘整背驰
let = &[.len() - 3];
let = &[.len() - 1];
debug_assert!(
..load(Ordering::Relaxed) < ..load(Ordering::Relaxed)
);
assert!(..load(Ordering::Relaxed) < ..load(Ordering::Relaxed));
if .() != .() {
return crate::algorithm::divergence::::(
, ,
+291 -236
View File
@@ -52,25 +52,22 @@ pub struct 观察者 {
pub : Vec<Arc<线>>,
pub _中枢序列: Vec<Arc<>>,
// 线段
pub 线: Vec<Arc<线>>,
pub : Vec<Arc<>>,
// 分析层次配置
pub 线: usize,
pub 线: usize,
pub 线: usize,
// 扩展线段(笔级)
pub 线: Vec<Arc<线>>,
pub : Vec<Arc<>>,
// 线段组: [0]=线段, [1]=线段<线段>, [2]=线段<线段<线段>>
pub 线: Vec<Vec<Arc<线>>>,
pub : Vec<Vec<Arc<>>>,
// 扩展线段(线段级)
pub 线_线段: Vec<Arc<线>>,
pub _线段: Vec<Arc<>>,
// 扩展线段组: [0]=扩展线段, [1]=扩展线段<扩展线段>, [2]=扩展线段<扩展线段<扩展线段>>
pub 线: Vec<Vec<Arc<线>>>,
pub : Vec<Vec<Arc<>>>,
// 线段线段
pub 线_线段序列: Vec<Arc<线>>,
pub 线_中枢序列: Vec<Arc<>>,
// 扩展线段之扩展线段
pub 线_扩展线段: Vec<Arc<线>>,
pub _扩展线段: Vec<Arc<>>,
// 混合扩展线段组: [0]=扩展线段<线段>, [1]=扩展线段<线段<线段>>, [2]=扩展线段<线段<线段<线段>>>
pub 线: Vec<Vec<Arc<线>>>,
pub : Vec<Vec<Arc<>>>,
// 终止时间戳
: Option<i64>,
@@ -85,6 +82,30 @@ impl 观察者 {
None
};
let 线 = 3usize;
let 线 = 3usize;
let 线 = 3usize;
let mut 线: Vec<Vec<Arc<线>>> = Vec::with_capacity(线);
let mut : Vec<Vec<Arc<>>> = Vec::with_capacity(线);
let mut 线: Vec<Vec<Arc<线>>> = Vec::with_capacity(线);
let mut : Vec<Vec<Arc<>>> = Vec::with_capacity(线);
let mut 线: Vec<Vec<Arc<线>>> = Vec::with_capacity(线);
let mut : Vec<Vec<Arc<>>> = Vec::with_capacity(线);
for _ in 0..线 {
线.push(Vec::new());
.push(Vec::new());
}
for _ in 0..线 {
线.push(Vec::new());
.push(Vec::new());
}
for _ in 0..线 {
线.push(Vec::new());
.push(Vec::new());
}
let mut instance = Self {
: .clone(),
,
@@ -95,16 +116,15 @@ impl 观察者 {
: Vec::new(),
: Vec::new(),
_中枢序列: Vec::new(),
线: Vec::new(),
: Vec::new(),
线: Vec::new(),
: Vec::new(),
线_线段: Vec::new(),
_线段: Vec::new(),
线_线段序列: Vec::new(),
线_中枢序列: Vec::new(),
线_扩展线段: Vec::new(),
_扩展线段: Vec::new(),
线,
线,
线,
线,
,
线,
,
线,
,
,
};
instance.. = ;
@@ -126,6 +146,58 @@ impl 观察者 {
self.K线序列.last()
}
// ---- 向后兼容的属性访问器 ----
/// 线段序列 (线段序列组[0])
pub fn 线(&self) -> &Vec<Arc<线>> {
&self.线[0]
}
/// 中枢序列 (中枢序列组[0])
pub fn (&self) -> &Vec<Arc<>> {
&self.[0]
}
/// 线段_线段序列 (线段序列组[1])
pub fn 线_线段序列(&self) -> &Vec<Arc<线>> {
&self.线[1]
}
/// 线段_中枢序列 (中枢序列组[1])
pub fn 线_中枢序列(&self) -> &Vec<Arc<>> {
&self.[1]
}
/// 扩展线段序列 (扩展线段序列组[0])
pub fn 线(&self) -> &Vec<Arc<线>> {
&self.线[0]
}
/// 扩展中枢序列 (扩展中枢序列组[0])
pub fn (&self) -> &Vec<Arc<>> {
&self.[0]
}
/// 扩展线段序列_扩展线段 (扩展线段序列组[1])
pub fn 线_扩展线段(&self) -> &Vec<Arc<线>> {
&self.线[1]
}
/// 扩展中枢序列_扩展线段 (扩展中枢序列组[1])
pub fn _扩展线段(&self) -> &Vec<Arc<>> {
&self.[1]
}
/// 扩展线段序列_线段 (混合扩展线段序列组[0])
pub fn 线_线段(&self) -> &Vec<Arc<线>> {
&self.线[0]
}
/// 扩展中枢序列_线段 (混合扩展中枢序列组[0])
pub fn _线段(&self) -> &Vec<Arc<>> {
&self.[0]
}
/// 重置基础序列
pub fn (&mut self) {
self.K线序列.clear();
@@ -134,16 +206,27 @@ impl 观察者 {
self..clear();
self..clear();
self._中枢序列.clear();
self.线.clear();
self..clear();
self.线.clear();
self..clear();
self.线_线段.clear();
self._线段.clear();
self.线_线段序列.clear();
self.线_中枢序列.clear();
self.线_扩展线段.clear();
self._扩展线段.clear();
self.线.clear();
self..clear();
for _ in 0..self.线 {
self.线.push(Vec::new());
self..push(Vec::new());
}
self.线.clear();
self..clear();
for _ in 0..self.线 {
self.线.push(Vec::new());
self..push(Vec::new());
}
self.线.clear();
self..clear();
for _ in 0..self.线 {
self.线.push(Vec::new());
self..push(Vec::new());
}
}
/// 增加原始K线 — 单根K线投喂入口
@@ -166,7 +249,7 @@ impl 观察者 {
/// 核心数据处理管道
fn __处理数据(&mut self, K: K线) {
// Step 1: 缠论K线分析 (普K is consumed by 分析 as &mut)
// Step 1: 缠论K线分析
let (_, ) = K线::(
K,
&mut self.K线序列,
@@ -178,92 +261,73 @@ impl 观察者 {
None => return,
};
// Step 2: 笔分析
if self.. {
::(
,
&mut self.,
&mut self.,
&self.K线序列,
&self.K线序列,
0,
&self.,
);
}
// Step 2: 笔分析(无条件)
::(
,
&mut self.,
&mut self.,
&self.K线序列,
&self.K线序列,
0,
&self.,
);
if self..is_empty() {
return;
}
// Step 3: 笔中枢分析
if self.. {
::(&self., &mut self._中枢序列, true, "", 0);
}
// Step 3: 笔中枢分析(无条件)
::(&self., &mut self._中枢序列, true, "", 0);
if self..is_empty() {
return;
}
// Step 4: 线段分析
if self..线 {
线::(
&self.,
&mut self.线,
&self.,
0,
&[::, ::],
);
}
if self..线 {
::(&self.线, &mut self., true, "", 0);
// Step 4: 线段分析 — 3 级递归
for i in 0..self.线 {
if i == 0 {
线::(
&self.,
&mut self.线[i],
&self.,
0,
&[::, ::],
);
} else {
let = self.线[i - 1].clone();
线::(
&,
&mut self.线[i],
&self.,
0,
&[::, ::],
);
}
::(&self.线[i], &mut self.[i], true, "", 0);
}
// Step 5: 扩展线段(笔级)
if self..线 {
线::(&self., &mut self.线, &self.);
}
if self..线 {
::(&self.线, &mut self., true, "", 0);
}
// Step 6: 扩展线段(线段级)
if self..线 {
线::(&self.线, &mut self.线_线段, &self.);
}
if self..线 {
// Step 5: 扩展线段分析 — 3 级递归
for i in 0..self.线 {
if i == 0 {
线::(&self., &mut self.线[i], &self.);
} else {
let = self.线[i - 1].clone();
线::(&, &mut self.线[i], &self.);
}
::(
&self.线_线段,
&mut self._线段,
&self.线[i],
&mut self.[i],
true,
"",
0,
);
}
// Step 7: 线段之线段
if self..线 {
线::(
&self.线,
&mut self.线_线段序列,
&self.,
0,
&[::, ::],
);
}
if self..线 {
::(&self.线_线段序列, &mut self.线_中枢序列, true, "", 0);
}
// Step 8: 扩展线段之扩展线段
if self..线 {
线::(
&self.线,
&mut self.线_扩展线段,
&self.,
);
}
if self..线 {
// Step 6: 混合扩展线段分析 — 3 级递归 (源 = 线段序列组[i])
for i in 0..self.线 {
let = self.线[i].clone();
线::(&, &mut self.线[i], &self.);
::(
&self.线_扩展线段,
&mut self._扩展线段,
&self.线[i],
&mut self.[i],
true,
"",
0,
@@ -279,16 +343,27 @@ impl 观察者 {
self..clear();
self..clear();
self._中枢序列.clear();
self.线.clear();
self..clear();
self.线.clear();
self..clear();
self.线_线段.clear();
self._线段.clear();
self.线_线段序列.clear();
self.线_中枢序列.clear();
self.线_扩展线段.clear();
self._扩展线段.clear();
self.线.clear();
self..clear();
for _ in 0..self.线 {
self.线.push(Vec::new());
self..push(Vec::new());
}
self.线.clear();
self..clear();
for _ in 0..self.线 {
self.线.push(Vec::new());
self..push(Vec::new());
}
self.线.clear();
self..clear();
for _ in 0..self.线 {
self.线.push(Vec::new());
self..push(Vec::new());
}
for i in 1..self.K线序列.len() - 1 {
let = ::new(
@@ -307,102 +382,61 @@ impl 观察者 {
);
}
if self.. {
::(&self., &mut self._中枢序列, true, "", 0);
::(&self., &mut self._中枢序列, true, "", 0);
for i in 0..self.线 {
if i == 0 {
线::(
&self.,
&mut self.线[i],
&self.,
0,
&[::, ::],
);
} else {
let = self.线[i - 1].clone();
线::(
&,
&mut self.线[i],
&self.,
0,
&[::, ::],
);
}
::(&self.线[i], &mut self.[i], true, "", 0);
}
if self..线 {
线::(
&self.,
&mut self.线,
&self.,
0,
&[::, ::],
);
}
if self..线 {
::(&self.线, &mut self., true, "", 0);
}
if self..线 {
线::(&self., &mut self.线, &self.);
}
if self..线 {
::(&self.线, &mut self., true, "", 0);
}
if self..线 {
线::(&self.线, &mut self.线_线段, &self.);
}
if self..线 {
for i in 0..self.线 {
if i == 0 {
线::(&self., &mut self.线[i], &self.);
} else {
let = self.线[i - 1].clone();
线::(&, &mut self.线[i], &self.);
}
::(
&self.线_线段,
&mut self._线段,
&self.线[i],
&mut self.[i],
true,
"",
0,
);
}
if self..线 {
线::(
&self.线,
&mut self.线_线段序列,
&self.,
for i in 0..self.线 {
let = self.线[i].clone();
线::(&, &mut self.线[i], &self.);
::(
&self.线[i],
&mut self.[i],
true,
"",
0,
&[::, ::],
);
}
if self..线 {
::(&self.线_线段序列, &mut self.线_中枢序列, true, "", 0);
}
}
/// 测试_保存数据 — 输出各序列数据文本到文件
pub fn _保存数据(&self, root: Option<&str>) -> String {
let _文本数据: Vec<String> = self..iter().map(|b| b.()).collect();
let 线_文本数据: Vec<String> =
self.线.iter().map(|s| s.()).collect();
let 线_数据文本: Vec<String> =
self.线.iter().map(|s| s.()).collect();
let 线_线段_数据文本: Vec<String> = self
.线_线段
.iter()
.map(|s| s.())
.collect();
let 线_线段序列_数据文本: Vec<String> = self
.线_线段序列
.iter()
.map(|s| s.())
.collect();
let 线_扩展线段_数据文本: Vec<String> = self
.线_扩展线段
.iter()
.map(|s| s.())
.collect();
let _中枢序列_数据文本: Vec<String> =
self._中枢序列.iter().map(|h| h.()).collect();
let _数据文本: Vec<String> =
self..iter().map(|h| h.()).collect();
let _数据文本: Vec<String> =
self..iter().map(|h| h.()).collect();
let _线段_数据文本: Vec<String> = self
._线段
.iter()
.map(|h| h.())
.collect();
let 线_中枢序列_数据文本: Vec<String> = self
.线_中枢序列
.iter()
.map(|h| h.())
.collect();
let _扩展线段_数据文本: Vec<String> = self
._扩展线段
.iter()
.map(|h| h.())
.collect();
// 确定根目录
let = match root {
Some(r) => std::path::PathBuf::from(r),
@@ -420,7 +454,50 @@ impl 观察者 {
return String::new();
}
// 缠K data for debugging
// 辅助:保存序列到文件 (Python: 保存序列(序列))
let =
|: &Vec<Arc<线>>, : &std::path::Path| -> Result<(), std::io::Error> {
if .is_empty() {
return Ok(());
}
let : Vec<String> = .iter().map(|d| d.()).collect();
let = format!("{}.txt", [0]..read().unwrap());
std::fs::write(.join(&), .join("\n") + "\n")?;
Ok(())
};
let =
|: &Vec<Arc<>>, : &std::path::Path| -> Result<(), std::io::Error> {
if .is_empty() {
return Ok(());
}
let : Vec<String> = .iter().map(|h| h.()).collect();
let = format!("{}.txt", [0]..read().unwrap());
std::fs::write(.join(&), .join("\n") + "\n")?;
Ok(())
};
// 保存笔/笔中枢
let _ = (&self., &);
let _ = (&self._中枢序列, &);
// 保存线段组
for i in 0..self.线 {
let _ = (&self.线[i], &);
let _ = (&self.[i], &);
}
// 保存扩展线段组
for i in 0..self.线 {
let _ = (&self.线[i], &);
let _ = (&self.[i], &);
}
// 保存混合扩展线段组
for i in 0..self.线 {
let _ = (&self.线[i], &);
let _ = (&self.[i], &);
}
// 缠K/分型 debug data
let K序列_数据文本: Vec<String> = self
.K线序列
.iter()
@@ -438,6 +515,11 @@ impl 观察者 {
)
})
.collect();
let _ = std::fs::write(
.join("缠K序列_数据文本.txt"),
K序列_数据文本.join("\n") + "\n",
);
let _数据文本: Vec<String> = self
.
.iter()
@@ -454,37 +536,10 @@ impl 观察者 {
)
})
.collect();
let : Vec<(&str, &[String])> = vec![
("笔序列_文本数据", &_文本数据),
("线段序列_文本数据", &线_文本数据),
("扩展线段序列_数据文本", &线_数据文本),
("扩展线段序列_线段_数据文本", &线_线段_数据文本),
("线段_线段序列_数据文本", &线_线段序列_数据文本),
(
"扩展线段序列_扩展线段_数据文本",
&线_扩展线段_数据文本,
),
("笔_中枢序列_数据文本", &_中枢序列_数据文本),
("中枢序列_数据文本", &_数据文本),
("扩展中枢序列_数据文本", &_数据文本),
("扩展中枢序列_线段_数据文本", &_线段_数据文本),
("线段_中枢序列_数据文本", &线_中枢序列_数据文本),
(
"扩展中枢序列_扩展线段_数据文本",
&_扩展线段_数据文本,
),
("缠K序列_数据文本", &K序列_数据文本),
("分型序列_数据文本", &_数据文本),
];
for (, ) in & {
let = .join(format!("{}.txt", ));
let = .join("\n") + "\n";
if let Err(e) = std::fs::write(&, &) {
error!("写入文件失败: {} -> {}", .display(), e);
}
}
let _ = std::fs::write(
.join("分型序列_数据文本.txt"),
_数据文本.join("\n") + "\n",
);
info!("全部数据拆分保存完成,目录:{}", .display());
.display().to_string()
@@ -672,7 +727,7 @@ mod tests {
let obs_ref = obs.read().unwrap();
// 每个线段的基础序列中的笔 Rc 指针必须在 笔序列 中
for (i, seg) in obs_ref.线.iter().enumerate() {
for (i, seg) in obs_ref.线().iter().enumerate() {
for (j, bi_in_seg) in seg..read().unwrap().iter().enumerate() {
let bi_ptr = Arc::as_ptr(bi_in_seg);
let found = obs_ref..iter().any(|b| Arc::as_ptr(b) == bi_ptr);
@@ -706,10 +761,10 @@ mod tests {
}
}
for (i, hub) in obs_ref..iter().enumerate() {
for (i, hub) in obs_ref.().iter().enumerate() {
for (j, seg_in_hub) in hub..read().unwrap().iter().enumerate() {
let seg_ptr = Arc::as_ptr(seg_in_hub);
let found = obs_ref.线.iter().any(|s| Arc::as_ptr(s) == seg_ptr);
let found = obs_ref.线().iter().any(|s| Arc::as_ptr(s) == seg_ptr);
if !found {
info!("线段中枢 {} 的基础序列[{}] 不在线段序列中!", i, j);
}
@@ -738,8 +793,8 @@ mod tests {
let obs = obs_ref.read().unwrap();
(
obs..len(),
obs.线.len(),
obs..len(),
obs.线().len(),
obs.().len(),
obs._中枢序列.len(),
)
};
@@ -778,12 +833,12 @@ mod tests {
}
let = obs_ref.read().unwrap()..len();
let = obs_ref.read().unwrap().线.len();
let = obs_ref.read().unwrap().线().len();
// 重置
obs_ref.write().unwrap().();
assert_eq!(obs_ref.read().unwrap()..len(), 0);
assert_eq!(obs_ref.read().unwrap().线.len(), 0);
assert_eq!(obs_ref.read().unwrap().线().len(), 0);
// 重新投喂
for i in 0..data.len() / size {
@@ -794,7 +849,7 @@ mod tests {
}
let = obs_ref.read().unwrap()..len();
let = obs_ref.read().unwrap().线.len();
let = obs_ref.read().unwrap().线().len();
assert_eq!(, , "重置后重新投喂笔数不一致");
assert_eq!(, , "重置后重新投喂线段数不一致");
@@ -831,7 +886,7 @@ mod tests {
let _短路 = bi..load(Ordering::Relaxed);
let _前一缺口 = *bi..read().unwrap();
}
for seg in &obs_ref.线 {
for seg in obs_ref.线() {
let _ = seg..read().unwrap().clone();
let _ = seg..read().unwrap().len();
}
+185 -10
View File
@@ -34,18 +34,35 @@ pub struct 指标计算器;
impl {
/// 增量计算所有开启的指标,将结果写入 当前K线.指标
///
/// `现有序列` 包含当前K线;prev 取自 现有序列.last()
/// `序列` 包含当前K线(在末尾)prev 取自 全序列[..-1].last()
/// 通过 RwLock 内部可变性,以 `&K线` 共享引用写入指标值
pub fn (K线: &K线, : &[Arc<K线>], : &) {
let prev_guard = .last().map(|k| k..read().unwrap());
let prev = prev_guard.as_deref();
if . {
Self::_计算MACD组(K线, prev, );
Self::_计算RSI组(K线, prev, );
Self::_计算KDJ组(K线, prev, );
Self::_计算BOLL组(K线, prev, );
pub fn (: &[Arc<K线>], : &) {
let n = .len();
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().unwrap())
} else {
None
};
let prev = prev_guard.as_deref();
if . {
Self::_计算MACD组(K线, prev, );
Self::_计算RSI组(K线, prev, );
Self::_计算KDJ组(K线, prev, );
Self::_计算BOLL组(K线, prev, );
}
Self::_更新均线(K线, , );
has_prev = n > 1;
} // prev_guard dropped here
if has_prev {
Self::_回填新指标(, );
}
Self::_更新均线(K线, , );
}
fn _计算MACD组(K线: &K线, prev: Option<&>, : &) {
@@ -298,4 +315,162 @@ impl 指标计算器 {
}
}
}
/// 运行中新增指标参数时,回填所有历史K线
fn _回填新指标(: &[Arc<K线>], : &) {
// 作用域化首尾读锁:在回填写循环之前释放,避免读锁与写锁冲突
let (MACD, RSI, KDJ, BOLL) = {
let K_guard = [0]..read().unwrap();
let K_guard = [.len() - 1]..read().unwrap();
let MACD: Vec<_> =
._解析MACD参数列表()
.into_iter()
.filter(|(key, _, _, _)| K_guard.(key) && !K_guard.(key))
.collect();
let RSI: Vec<_> =
._解析RSI周期列表()
.into_iter()
.filter(|(key, _)| K_guard.(key) && !K_guard.(key))
.collect();
let KDJ: Vec<_> =
._解析KDJ参数列表()
.into_iter()
.filter(|(key, _, _, _)| K_guard.(key) && !K_guard.(key))
.collect();
let BOLL: Vec<_> =
._解析BOLL参数列表()
.into_iter()
.filter(|(key, _, _)| K_guard.(key) && !K_guard.(key))
.collect();
(MACD, RSI, KDJ, BOLL)
}; // 首K_guard, 尾K_guard dropped here
if MACD.is_empty() && RSI.is_empty() && KDJ.is_empty() && BOLL.is_empty() {
return;
}
let = &.;
// 从第一根K线开始逐根回填,每次只持有一根prev读锁
for i in 0...len() {
let k线 = &[i];
let prev_guard = if i > 0 {
Some([i - 1]..read().unwrap())
} else {
None
};
for (key, , , ) in &MACD {
let val = if let Some(ref prev) = prev_guard {
if let Some(::MACD(prev_macd)) = prev.(key) {
::MACD(线::_K线(
prev_macd,
k线,
,
))
} else {
::MACD(线::_K线(
k线,
,
*,
*,
*,
))
}
} else {
::MACD(线::_K线(
k线,
,
*,
*,
*,
))
};
k线..write().unwrap().(key, val);
}
for (key, ) in &RSI {
let val = if let Some(ref prev) = prev_guard {
if let Some(::RSI(prev_rsi)) = prev.(key) {
::RSI(::_K线(
prev_rsi,
k线,
,
))
} else {
::RSI(::_K线(
k线,
,
*,
._超买阈值,
._超卖阈值,
Some(._移动平均线周期),
))
}
} else {
::RSI(::_K线(
k线,
,
*,
._超买阈值,
._超卖阈值,
Some(._移动平均线周期),
))
};
k线..write().unwrap().(key, val);
}
for (key, rsv, k平滑, d平滑) in &KDJ {
let val = if let Some(ref prev) = prev_guard {
if let Some(::KDJ(prev_kdj)) = prev.(key) {
::KDJ(::_K线(prev_kdj, k线))
} else {
::KDJ(::_K线(
k线,
*rsv,
*k平滑,
*d平滑,
._超买阈值,
._超卖阈值,
))
}
} else {
::KDJ(::_K线(
k线,
*rsv,
*k平滑,
*d平滑,
._超买阈值,
._超卖阈值,
))
};
k线..write().unwrap().(key, val);
}
for (key, , ) in &BOLL {
let val = if let Some(ref prev) = prev_guard {
if let Some(::BOLL(prev_boll)) = prev.(key) {
::BOLL(::_K线(prev_boll, k线, ))
} else {
::BOLL(::_K线(
k线,
,
* as usize,
*,
))
}
} else {
::BOLL(::_K线(
k线,
,
* as usize,
*,
))
};
k线..write().unwrap().(key, val);
}
}
}
}
+44
View File
@@ -270,6 +270,50 @@ 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())
}
/// 截取Arc<K线>序列中从始到终的片段
pub fn rc(: &[Arc<Self>], : &Arc<Self>, : &Arc<Self>) -> Vec<Arc<Self>> {
let _ptr = Arc::as_ptr();
+132 -2
View File
@@ -408,8 +408,7 @@ impl 缠论K线 {
}
// 计算指标: 对齐 Python,仅当 计算指标 开启时执行
if . {
let n = K序列.len();
::(&K序列[n - 1], &K序列[..n - 1], );
::(K序列, );
}
// ---- 阶段2: 缠K合并 ----
@@ -531,6 +530,137 @@ impl 缠论K线 {
let _idx = .iter().position(|k| std::ptr::eq(Arc::as_ptr(k), ))?;
Some([_idx..=_idx].to_vec())
}
/// 结构化相等校验 — 比对所有字段,浮点容差,递归校验标的K线,返回 (是否相等, 差异描述)
pub fn (&self, other: &Self, : f64) -> (bool, String) {
if self..load(Ordering::Relaxed) != other..load(Ordering::Relaxed) {
return (
false,
format!(
"缠论K线: [序号] 不等 A={},B={}",
self..load(Ordering::Relaxed),
other..load(Ordering::Relaxed)
),
);
}
if self..load(Ordering::Relaxed) != other..load(Ordering::Relaxed) {
return (
false,
format!(
"缠论K线: [时间戳] 不等 A={},B={}",
self..load(Ordering::Relaxed),
other..load(Ordering::Relaxed)
),
);
}
if (self..get() - other..get()).abs() > {
return (
false,
format!(
"缠论K线: [高] 浮点超限 容差={浮点容差:.2e} A={:.10},B={:.10}",
self..get(),
other..get()
),
);
}
if (self..get() - other..get()).abs() > {
return (
false,
format!(
"缠论K线: [低] 浮点超限 容差={浮点容差:.2e} A={:.10},B={:.10}",
self..get(),
other..get()
),
);
}
if *self..read().unwrap() != *other..read().unwrap() {
return (
false,
format!(
"缠论K线: [方向] 不等 A={},B={}",
self..read().unwrap(),
other..read().unwrap()
),
);
}
if *self..read().unwrap() != *other..read().unwrap() {
return (
false,
format!(
"缠论K线: [分型] 不等 A={:?},B={:?}",
self..read().unwrap(),
other..read().unwrap()
),
);
}
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()
.unwrap()
.(&other.K线.read().unwrap(), );
if !eq {
return (false, format!("缠论K线: 标的K线子项异常 >> {msg}"));
}
// 买卖点信息
let a_guard = self..read().unwrap();
let b_guard = other..read().unwrap();
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().unwrap(),
other..read().unwrap()
),
);
}
(true, "缠论K线: 全部字段一致".into())
}
}
impl crate::types::fractal:: for K线 {
+7 -7
View File
@@ -96,13 +96,13 @@ fn 测试_读取数据(文件路径: &str) {
println!("分型数量: {}", ..len());
println!("笔数量: {}", ..len());
println!("笔中枢数量: {}", ._中枢序列.len());
println!("线段数量: {}", .线.len());
println!("中枢数量: {}", ..len());
println!("扩展线段数量: {}", .线.len());
println!("线段_线段序列数量: {}", .线_线段序列.len());
println!("线段数量: {}", .线().len());
println!("中枢数量: {}", .().len());
println!("扩展线段数量: {}", .线().len());
println!("线段_线段序列数量: {}", .线_线段序列().len());
println!(
"扩展线段_扩展线段数量: {}",
.线_扩展线段.len()
.线_扩展线段().len()
);
println!("\n===== 保存分析数据 =====\n");
@@ -167,8 +167,8 @@ fn 测试_周期合成(文件路径: &str) {
.K线序列.len(),
..len(),
..len(),
.线.len(),
..len(),
.线().len(),
.().len(),
);
}
}
+235
View File
@@ -1150,6 +1150,241 @@ impl 虚线 {
(, "".into())
}
/// 结构化相等校验 — 递归校验所有子结构(分型/缺口/缠K/中枢/线段特征/虚线),返回 (是否相等, 差异描述)
pub fn (&self, other: &Self, : f64) -> (bool, String) {
if *self..read().unwrap() != *other..read().unwrap() {
return (
false,
format!(
"虚线: [标识] 不等 A={},B={}",
self..read().unwrap(),
other..read().unwrap()
),
);
}
if self..load(Ordering::Relaxed) != other..load(Ordering::Relaxed) {
return (
false,
format!(
"虚线: [序号] 不等 A={},B={}",
self..load(Ordering::Relaxed),
other..load(Ordering::Relaxed)
),
);
}
if self..load(Ordering::Relaxed) != other..load(Ordering::Relaxed) {
return (
false,
format!(
"虚线: [级别] 不等 A={},B={}",
self..load(Ordering::Relaxed),
other..load(Ordering::Relaxed)
),
);
}
// 文
{
let (eq, msg) = self..(&other., );
if !eq {
return (false, format!("虚线: [文]分型异常 >> {msg}"));
}
}
// 武
{
let (eq, msg) = self
.
.read()
.unwrap()
.(&other..read().unwrap(), );
if !eq {
return (false, format!("虚线: [武]分型异常 >> {msg}"));
}
}
if self..load(Ordering::Relaxed) != other..load(Ordering::Relaxed) {
return (
false,
format!(
"虚线: [有效性] 不等 A={},B={}",
self..load(Ordering::Relaxed),
other..load(Ordering::Relaxed)
),
);
}
// 基础序列
{
let a = self..read().unwrap();
let b = other..read().unwrap();
if a.len() != b.len() {
return (
false,
format!("虚线: [基础序列] 长度不一致 A={},B={}", a.len(), b.len()),
);
}
for (idx, (x, y)) in a.iter().zip(b.iter()).enumerate() {
let (eq, msg) = x.(y, );
if !eq {
return (false, format!("虚线: 基础序列[{idx}]虚线异常 >> {msg}"));
}
}
}
// 特征序列
{
let a = self..read().unwrap();
let b = other..read().unwrap();
if a.len() != b.len() {
return (
false,
format!("虚线: [特征序列] 长度不一致 A={},B={}", a.len(), b.len()),
);
}
for (idx, (x, y)) in a.iter().zip(b.iter()).enumerate() {
match (x, y) {
(None, None) => {}
(Some(xx), Some(yy)) => {
let (eq, msg) = xx.(yy, );
if !eq {
return (false, format!("虚线: 特征序列[{idx}]线段特征异常 >> {msg}"));
}
}
_ => return (false, format!("虚线: 特征序列[{idx}]空值不一致")),
}
}
}
// 中枢序列
let =
|: &str, a: &[Arc<>], b: &[Arc<>], : f64| -> Result<(), String> {
if a.len() != b.len() {
return Err(format!(
"虚线: [{名}] 长度不一致 A={},B={}",
a.len(),
b.len()
));
}
for (idx, (x, y)) in a.iter().zip(b.iter()).enumerate() {
let (eq, msg) = x.(y, );
if !eq {
return Err(format!("虚线: {名}[{idx}]中枢异常 >> {msg}"));
}
}
Ok(())
};
(
"实_中枢序列",
&self._中枢序列.read().unwrap(),
&other._中枢序列.read().unwrap(),
,
)
.map_err(|e| (false, e))
.ok();
(
"虚_中枢序列",
&self._中枢序列.read().unwrap(),
&other._中枢序列.read().unwrap(),
,
)
.map_err(|e| (false, e))
.ok();
(
"合_中枢序列",
&self._中枢序列.read().unwrap(),
&other._中枢序列.read().unwrap(),
,
)
.map_err(|e| (false, e))
.ok();
// 确认K线
match (
&*self.K线.read().unwrap(),
&*other.K线.read().unwrap(),
) {
(None, None) => {}
(Some(a), Some(b)) => {
let (eq, msg) = a.(b, );
if !eq {
return (false, format!("虚线: [确认K线]缠论K线异常 >> {msg}"));
}
}
(a, b) => {
return (
false,
format!(
"虚线: [确认K线]空值不一致 A={},B={}",
a.is_some(),
b.is_some()
),
);
}
}
// 模式
if *self..read().unwrap() != *other..read().unwrap() {
return (
false,
format!(
"虚线: [模式] 不等 A={},B={}",
self..read().unwrap(),
other..read().unwrap()
),
);
}
// _特征序列_显示
if self._特征序列_显示.load(Ordering::Relaxed)
!= other._特征序列_显示.load(Ordering::Relaxed)
{
return (false, "虚线: [_特征序列_显示] 不等".to_string());
}
// 前一缺口
match (
&*self..read().unwrap(),
&*other..read().unwrap(),
) {
(None, None) => {}
(Some(a), Some(b)) => {
let (eq, msg) = a.(b, );
if !eq {
return (false, format!("虚线: [前一缺口]缺口异常 >> {msg}"));
}
}
(a, b) => {
return (
false,
format!(
"虚线: [前一缺口]空值不一致 A={},B={}",
a.is_some(),
b.is_some()
),
);
}
}
// 前一结束位置
match (
&*self..read().unwrap(),
&*other..read().unwrap(),
) {
(None, None) => {}
(Some(a), Some(b)) => {
let (eq, msg) = a.(b, );
if !eq {
return (false, format!("虚线: [前一结束位置]虚线异常 >> {msg}"));
}
}
(a, b) => {
return (
false,
format!(
"虚线: [前一结束位置]空值不一致 A={},B={}",
a.is_some(),
b.is_some()
),
);
}
}
// 短路修正
if self..load(Ordering::Relaxed) != other..load(Ordering::Relaxed) {
return (false, "虚线: [短路修正] 不等".to_string());
}
(true, "虚线: 全量字段嵌套校验一致".into())
}
}
impl std::fmt::Display for 线 {
+63 -1
View File
@@ -66,7 +66,7 @@ impl 分型 {
: Option<Arc<K线>>, : Arc<K线>, : Option<Arc<K线>>
) -> Self {
if let (Some(), Some()) = (&, &) {
debug_assert!(
assert!(
..load(Ordering::Relaxed) < ..load(Ordering::Relaxed)
&& ..load(Ordering::Relaxed) < ..load(Ordering::Relaxed),
"分型时间戳断言失败: 左={}, 中={}, 右={}",
@@ -256,6 +256,68 @@ impl 分型 {
}
.push();
}
/// 结构化相等校验 — 递归校验左/中/右缠伦K线 + 自有缓存字段,返回 (是否相等, 差异描述)
pub fn (&self, other: &Self, : f64) -> (bool, String) {
match (&self., &other.) {
(None, None) => {}
(Some(a), Some(b)) => {
let (eq, msg) = a.(b, );
if !eq {
return (false, format!("分型: [左]缠论K线异常 >> {msg}"));
}
}
(a, b) => {
return (
false,
format!("分型: [左]空值不一致 A={},B={}", a.is_some(), b.is_some()),
);
}
}
{
let (eq, msg) = self..(&other., );
if !eq {
return (false, format!("分型: [中]缠论K线异常 >> {msg}"));
}
}
match (&self., &other.) {
(None, None) => {}
(Some(a), Some(b)) => {
let (eq, msg) = a.(b, );
if !eq {
return (false, format!("分型: [右]缠论K线异常 >> {msg}"));
}
}
(a, b) => {
return (
false,
format!("分型: [右]空值不一致 A={},B={}", a.is_some(), b.is_some()),
);
}
}
if self. != other. {
return (
false,
format!("分型: [结构] 不等 A={},B={}", self., other.),
);
}
if self. != other. {
return (
false,
format!("分型: [时间戳] 不等 A={},B={}", self., other.),
);
}
if (self. - other.).abs() > {
return (
false,
format!(
"分型: [分型特征值] 浮点超限 A={:.10},B={:.10}",
self., other.
),
);
}
(true, "分型: 全部字段一致".into())
}
}
impl crate::types::fractal:: for {
+54 -3
View File
@@ -26,6 +26,7 @@ use crate::structure::dash_line::虚线;
use crate::structure::feat_fractal::;
use crate::structure::fractal_obj::;
use crate::types::{, };
use std::sync::atomic::AtomicI64;
use std::sync::atomic::Ordering;
use std::sync::{Arc, RwLock};
@@ -48,7 +49,7 @@ use std::sync::{Arc, RwLock};
#[derive(Debug)]
pub struct 线 {
/// 特征序列元素编号
pub : i64,
pub : AtomicI64,
/// 标识字符串(如 "特征<虚线>"
pub : RwLock<String>,
/// 所属线段的方向
@@ -60,7 +61,7 @@ pub struct 线段特征 {
impl Clone for 线 {
fn clone(&self) -> Self {
Self {
: self.,
: AtomicI64::new(self..load(Ordering::Relaxed)),
: RwLock::new(self..read().unwrap().clone()),
线: self.线,
: self..clone(),
@@ -72,7 +73,7 @@ impl 线段特征 {
/// 新建线段特征(给定标识、基础序列和线段方向)
pub fn new(: String, : Vec<Arc<线>>, 线: ) -> Self {
Self {
: 0,
: AtomicI64::new(0),
: RwLock::new(),
线,
,
@@ -316,6 +317,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().unwrap() != *other..read().unwrap() {
return (
false,
format!(
"线段特征: [标识] 不等 A={},B={}",
self..read().unwrap(),
other..read().unwrap()
),
);
}
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 线 {
+23
View File
@@ -64,6 +64,29 @@ impl 缺口 {
Some(Self::new(.max(), .min()))
}
/// 结构化相等校验 — 浮点容差比较高/低,返回 (是否相等, 差异描述)
pub fn (&self, other: &Self, : f64) -> (bool, String) {
if (self. - other.).abs() > {
return (
false,
format!(
"缺口: [高] 浮点超限 容差={浮点容差:.2e} A={:.10},B={:.10}",
self., other.
),
);
}
if (self. - other.).abs() > {
return (
false,
format!(
"缺口: [低] 浮点超限 容差={浮点容差:.2e} A={:.10},B={:.10}",
self., other.
),
);
}
(true, "缺口: 高低价格一致".into())
}
}
impl std::fmt::Display for {
+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())
+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)
+2 -2
View File
@@ -1001,7 +1001,7 @@ class 回测(高级策略基类):
def 检查买信号(self):
if self.p.观察员.笔序列:
k线 = self.p.观察员.缠论K线序列[-1]
self.日志(f"检查买信号 当前笔 {self.p.观察员.笔序列[-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
@@ -1024,7 +1024,7 @@ class 回测(高级策略基类):
def 检查卖信号(self):
if self.p.观察员.笔序列:
self.日志(f"检查卖信号 当前笔 {self.p.观察员.笔序列[-1]}")
# self.日志(f"检查卖信号 当前笔 {self.p.观察员.笔序列[-1]}")
k线 = self.p.观察员.缠论K线序列[-1]
if k线.买卖点信息:
print(f"回测-首 {self.p.观察员.__class__.__name__}", k线.买卖点信息)
+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});