Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a8df8cd187 | |||
| 9c7686d104 | |||
| 34c42ecd68 | |||
| 8a2afb9ed0 | |||
| 11d897ebaa | |||
| 4bf9461009 | |||
| c83cddbcc4 | |||
| 0a7e9dc896 | |||
| dd40f95742 | |||
| e33b1f0744 |
@@ -0,0 +1,182 @@
|
||||
---
|
||||
name: Bug 报告
|
||||
about: 创建一个 Bug 报告帮助我们改进
|
||||
title: "[Bug] "
|
||||
labels: bug
|
||||
assignees: []
|
||||
---
|
||||
|
||||
## Bug 描述
|
||||
|
||||
请简洁清晰地描述这个 Bug。
|
||||
|
||||
## 复现步骤
|
||||
|
||||
请提供最简化的复现步骤(代码片段或操作序列):
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
|
||||
## 预期行为
|
||||
|
||||
请描述您期望发生的结果。
|
||||
|
||||
## 实际行为
|
||||
|
||||
请描述实际发生的结果。包括完整的错误消息、panic 信息或日志输出。
|
||||
|
||||
## 环境
|
||||
|
||||
| 项目 | 版本 |
|
||||
|------|------|
|
||||
| Rust 版本 | `rustc --version` 输出 |
|
||||
| Crate 版本 | `chanlun` 版本号 |
|
||||
| 操作系统 | Linux / macOS / Windows |
|
||||
| 数据文件(.nb) | 文件路径或来源 |
|
||||
|
||||
## 复现代码
|
||||
|
||||
请粘贴可复现问题的最小 Rust 代码或 Python 代码。
|
||||
|
||||
**Rust:**
|
||||
|
||||
```rust
|
||||
use chanlun::config::缠论配置;
|
||||
use chanlun::kline::bar::K线;
|
||||
use chanlun::business::observer::观察者;
|
||||
|
||||
// 复现代码
|
||||
```
|
||||
|
||||
**Python (绑定层):**
|
||||
|
||||
```python
|
||||
import chanlun
|
||||
|
||||
# 复现代码
|
||||
```
|
||||
|
||||
## 缠论配置
|
||||
|
||||
请提供您使用的完整配置(JSON 或代码形式)。这对于复现问题至关重要。
|
||||
|
||||
<details>
|
||||
<summary>展开查看配置 JSON</summary>
|
||||
|
||||
```json
|
||||
{
|
||||
"标识": "btcusd",
|
||||
"缠K合并替换": false,
|
||||
"笔内元素数量": 5,
|
||||
"笔内相同终点取舍": false,
|
||||
"笔内起始分型包含整笔": false,
|
||||
"笔内起始分型包含整笔_包括右": false,
|
||||
"笔内原始K线包含整笔": false,
|
||||
"笔次级成笔": false,
|
||||
"笔弱化": false,
|
||||
"笔弱化_原始数量": 3,
|
||||
"线段_非缺口下穿刺": false,
|
||||
"线段_特征序列忽视老阴老阳": false,
|
||||
"线段_缺口后紧急修正": true,
|
||||
"线段_修正": false,
|
||||
"线段内部中枢图显": true,
|
||||
"扩展线段_当下分析": false,
|
||||
"分析笔": true,
|
||||
"分析线段": true,
|
||||
"分析扩展线段": true,
|
||||
"分析笔中枢": true,
|
||||
"分析线段中枢": true,
|
||||
"手动终止": "",
|
||||
"计算指标": true,
|
||||
"计算BOLL": false,
|
||||
"指标计算方式": "收",
|
||||
"平滑异同移动平均线_快线周期": 13,
|
||||
"平滑异同移动平均线_慢线周期": 31,
|
||||
"平滑异同移动平均线_信号周期": 11,
|
||||
"MACD_参数列表": [],
|
||||
"相对强弱指数_周期": 13,
|
||||
"相对强弱指数_移动平均线周期": 13,
|
||||
"相对强弱指数_超买阈值": 75.0,
|
||||
"相对强弱指数_超卖阈值": 25.0,
|
||||
"RSI_周期列表": [],
|
||||
"随机指标_RSV周期": 13,
|
||||
"随机指标_K值平滑周期": 5,
|
||||
"随机指标_D值平滑周期": 5,
|
||||
"随机指标_超买阈值": 80.0,
|
||||
"随机指标_超卖阈值": 20.0,
|
||||
"KDJ_参数列表": [],
|
||||
"布林带_周期": 20,
|
||||
"布林带_标准差倍数": 2.0,
|
||||
"BOLL_参数列表": [],
|
||||
"均线_类型列表": [],
|
||||
"均线_周期列表": [],
|
||||
"图表展示": true,
|
||||
"推送K线": true,
|
||||
"推送笔": true,
|
||||
"推送线段": true,
|
||||
"推送中枢": true,
|
||||
"图表展示_笔": true,
|
||||
"图表展示_线段": true,
|
||||
"图表展示_扩展线段": true,
|
||||
"图表展示_扩展线段_线段": true,
|
||||
"图表展示_线段_线段": true,
|
||||
"图表展示_中枢_笔": true,
|
||||
"图表展示_中枢_线段": true,
|
||||
"图表展示_中枢_扩展线段": true,
|
||||
"图表展示_中枢_扩展线段_线段": true,
|
||||
"图表展示_中枢_线段_线段": true,
|
||||
"图表展示_中枢_线段内部": true,
|
||||
"买卖点偏移": 1,
|
||||
"买卖点激进识别": false,
|
||||
"买卖点与MACD柱强相关": false,
|
||||
"买卖点错过误差值": 0.01,
|
||||
"买卖点_指标模式": "配置",
|
||||
"买卖点_指标匹配_MACD": true,
|
||||
"买卖点_指标匹配_KDJ": true,
|
||||
"买卖点_指标匹配_RSI": true,
|
||||
"买卖点_背离率": "Infinity",
|
||||
"买卖点_T2_回调阈值": 1.0,
|
||||
"买卖点_T2S_最大层级": 3,
|
||||
"买卖点_峰值条件": false,
|
||||
"买卖点_计算方式": "峰",
|
||||
"买卖点_计算线段BSP1": true,
|
||||
"买卖点_处理BSP2": true,
|
||||
"买卖点_计算线段BSP3": true,
|
||||
"买卖点_依赖T1": true,
|
||||
"买卖点_中枢来源": "合",
|
||||
"买卖点_调试输出": false,
|
||||
"线段内部背驰_MACD": true,
|
||||
"线段内部背驰_斜率": true,
|
||||
"线段内部背驰_测度": true,
|
||||
"线段内部背驰_模式": "相对",
|
||||
"加载文件路径": ""
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
> **请修改上述 JSON 为您实际使用的配置值**,或直接粘贴通过 `config.to_json()` 输出的 JSON。
|
||||
|
||||
## 上下文
|
||||
|
||||
- [ ] 此 Bug 在 `chan.py` (Python 参考实现) 中也存在吗?
|
||||
- Python 版行为:
|
||||
- [ ] 此 Bug 是否与特定数据文件相关?
|
||||
- 数据文件名/时间范围:
|
||||
|
||||
## 日志 / Panic 输出
|
||||
|
||||
<details>
|
||||
<summary>展开查看详细输出</summary>
|
||||
|
||||
```
|
||||
在此粘贴日志或 panic 输出
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## 补充信息
|
||||
|
||||
任何其他有助于理解此 Bug 的上下文、截图或补充说明。
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: 自定义问题
|
||||
about: 其他问题(问题咨询、文档改进、重构建议等)
|
||||
title: "[Question] "
|
||||
labels: question
|
||||
assignees: []
|
||||
---
|
||||
|
||||
## 问题概述
|
||||
|
||||
请描述您的需求。
|
||||
|
||||
## 问题类型
|
||||
|
||||
- [ ] 问题咨询 — 对 API 或算法的使用存在疑问
|
||||
- [ ] 文档 — 文档错误、缺失或改进建议
|
||||
- [ ] 重构 — 代码结构或设计调整建议
|
||||
- [ ] 兼容性 — Python 绑定层与 `chan.py` 的行为差异
|
||||
- [ ] 性能 — 运行效率或内存占用问题
|
||||
- [ ] 其他
|
||||
|
||||
## 涉及范围
|
||||
|
||||
> 可选择一项或多项。
|
||||
|
||||
| 层次 | 模块 |
|
||||
|------|------|
|
||||
| 核心层 | `types` / `kline` / `indicators` / `algorithm` / `structure` / `business` / `config` |
|
||||
| 绑定层 | `chanlun-py` (`src/business_py.rs` / `src/config_py.rs` / `src/structure_py.rs`) |
|
||||
| 测试 | `chanlun/src/*/tests` / `chanlun-py/tests/test_all.py` |
|
||||
| 文档 | `chanlun/README.md` / `CLAUDE.md` / 其他 |
|
||||
| 其他 | |
|
||||
|
||||
## 补充信息
|
||||
|
||||
任何有助于更好理解或解决此问题的信息。
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
name: 功能请求
|
||||
about: 提出一个新的功能或增强建议
|
||||
title: "[Feature] "
|
||||
labels: enhancement
|
||||
assignees: []
|
||||
---
|
||||
|
||||
## 动机
|
||||
|
||||
请描述这个功能要解决什么问题,或者满足什么使用场景。
|
||||
|
||||
## 提案
|
||||
|
||||
请描述您期望的功能或 API。
|
||||
|
||||
**Rust 核心层:**
|
||||
|
||||
```rust
|
||||
// 期望的 API 或行为
|
||||
```
|
||||
|
||||
**Python 绑定层 (如适用):**
|
||||
|
||||
```python
|
||||
# 期望的 API 或行为
|
||||
```
|
||||
|
||||
## 替代方案
|
||||
|
||||
是否有其他替代方案或现有机制可以满足需求?如果有,请描述。
|
||||
|
||||
## 与 chan.py 的关系
|
||||
|
||||
- [ ] `chan.py` (Python 参考实现) 中已有此功能
|
||||
- 相关代码位置: `chan.py` 行号或方法名
|
||||
- [ ] 这是绑定层 (`chanlun-py`) 的功能需求
|
||||
- [ ] 这是核心层 (`chanlun`) 的算法需求
|
||||
- [ ] 这是全新的功能提案
|
||||
|
||||
## 影响范围
|
||||
|
||||
> 请勾选可能受影响的模块。
|
||||
|
||||
- [ ] 类型定义 (`types/`)
|
||||
- [ ] K线层 (`kline/`)
|
||||
- [ ] 技术指标 (`indicators/`)
|
||||
- [ ] 笔划分 (`algorithm/bi`)
|
||||
- [ ] 线段划分 (`algorithm/segment`)
|
||||
- [ ] 中枢识别 (`algorithm/hub`)
|
||||
- [ ] 背驰检测 (`algorithm/divergence`)
|
||||
- [ ] 结构体 (`structure/`)
|
||||
- [ ] 观察者 (`business/observer`)
|
||||
- [ ] 买卖点 (`business/bsp`)
|
||||
- [ ] K线合成器 (`business/synthesizer`)
|
||||
- [ ] 立体分析器 (`business/multi_frame`)
|
||||
- [ ] 配置 (`config`)
|
||||
- [ ] Python 绑定 (`chanlun-py`)
|
||||
|
||||
## 补充信息
|
||||
|
||||
任何参考链接、图表、伪代码或其他有助于说明该功能的内容。
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
name: Pull Request
|
||||
about: 提交代码变更
|
||||
title: ""
|
||||
labels: []
|
||||
assignees: []
|
||||
---
|
||||
|
||||
## 描述
|
||||
|
||||
请简洁清晰地描述这个 PR 做了什么。
|
||||
|
||||
## 变更类型
|
||||
|
||||
- [ ] Bug 修复
|
||||
- [ ] 新功能
|
||||
- [ ] 重构 / 代码清理
|
||||
- [ ] 文档更新
|
||||
- [ ] 测试
|
||||
- [ ] 其他
|
||||
|
||||
## 变更范围
|
||||
|
||||
> 勾选涉及的文件或模块。
|
||||
|
||||
**核心层 (`chanlun/`):**
|
||||
|
||||
- [ ] `types/` — 基础类型
|
||||
- [ ] `kline/` — K线层
|
||||
- [ ] `indicators/` — 技术指标
|
||||
- [ ] `algorithm/bi` — 笔划分
|
||||
- [ ] `algorithm/segment` — 线段划分
|
||||
- [ ] `algorithm/hub` — 中枢识别
|
||||
- [ ] `algorithm/divergence` — 背驰检测
|
||||
- [ ] `structure/` — 结构体
|
||||
- [ ] `business/observer` — 观察者
|
||||
- [ ] `business/bsp` — 买卖点
|
||||
- [ ] `business/synthesizer` — K线合成器
|
||||
- [ ] `business/multi_frame` — 立体分析器
|
||||
- [ ] `config` — 配置
|
||||
|
||||
**绑定层 (`chanlun-py/`):**
|
||||
|
||||
- [ ] `src/lib.rs` — 模块注册
|
||||
- [ ] `src/business_py.rs` — 业务绑定
|
||||
- [ ] `src/config_py.rs` — 配置绑定
|
||||
- [ ] `src/structure_py.rs` — 结构体绑定
|
||||
|
||||
**其他:**
|
||||
|
||||
- [ ] 测试 (`chanlun/src/*/tests` 或 `chanlun-py/tests/`)
|
||||
- [ ] 文档 (`README.md` / `CLAUDE.md` / `.github/`)
|
||||
|
||||
## 测试
|
||||
|
||||
- [ ] 核心层测试通过 (`cargo test`)
|
||||
- [ ] 绑定层测试通过 (`python3 -m pytest chanlun-py/tests/test_all.py -v`)
|
||||
- [ ] `cargo clippy` 零警告
|
||||
- [ ] 与 `chan.py` 输出一致 (双端对比)
|
||||
- [ ] 新增了相关测试
|
||||
- [ ] 无新增测试(请说明原因):
|
||||
|
||||
## 破坏性变更
|
||||
|
||||
- [ ] 是(请在下文描述迁移步骤)
|
||||
- [ ] 否
|
||||
|
||||
## 补充信息
|
||||
|
||||
任何有助于审查者理解此 PR 的截图、日志或对比数据。
|
||||
@@ -1,5 +1,8 @@
|
||||
name: 构建发布
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
|
||||
# 贡献者公约
|
||||
|
||||
## 我们的承诺
|
||||
|
||||
我们承诺使我们的社区对所有人保持友好、安全且公平。
|
||||
|
||||
我们承诺营造一个尊重并促进每位个体尊严、权利与贡献的环境,无论其种族、民族、种姓、肤色、年龄、体貌特征、神经多样性、残疾状况、生理性别或社会性别、性别认同或表达、性取向、语言、哲学或宗教信仰、国籍或社会出身、社会经济地位、教育程度或其他身份属性。所有真诚参与并遵守本公约者,均享有同等的参与权利。
|
||||
|
||||
|
||||
## 受鼓励的行为
|
||||
|
||||
虽然各自的社会规范可能有差异,但我们都努力达到这个社区对积极行为的期待。我们也了解,因为文化、背景或母语的不同,别人对我们的言行的解读可能不同于我们的初衷。
|
||||
|
||||
考虑到以上这些,我们承诺以审慎的态度彼此相待,并以践行以下共同价值为行为准则:
|
||||
|
||||
1. 尊重我们**社区的宗旨**、各项活动及集会方式
|
||||
2. 以**善意与诚实**的态度与他人互动
|
||||
3. 尊重**不同的观点与经历**
|
||||
4. 对自己的言行及贡献**负责**
|
||||
5. 以得体的方式给予并接受**建设性意见**
|
||||
6. 承诺在造成伤害时进行**弥补**
|
||||
7. 采取其他有益于**社区福祉**的行为
|
||||
|
||||
|
||||
## 受限制的行为
|
||||
|
||||
我们同意在社区内限制以下行为。出现这些行为、威胁实施这些行为、或宣传这些行为,均视为违反本行为准则。
|
||||
|
||||
1. **骚扰**:在明确表达界限后仍侵犯这些界限,或在被清楚要求停止后,仍进行不必要的个人关注。
|
||||
2. **人身攻击**:针对社区成员或群体发表侮辱、贬低或带有蔑视性的言论。
|
||||
3. **刻板印象或歧视**:基于无法改变的身份或特征,来评判他人的性格或行为。
|
||||
4. **性化**:做出在社区场景或宗旨下普遍认为不恰当的亲密举动。
|
||||
5. **侵犯保密性**:未经允许分享或利用他人的个人或隐私信息。
|
||||
6. **危害行为**:对任何人或群体实施、煽动或威胁施加暴力及其他伤害。
|
||||
7. 其他威胁**社区福祉**的行为。
|
||||
|
||||
|
||||
### 其他限制行为
|
||||
|
||||
1. **虚假身份**:出于任何原因冒充他人,或假扮他人以规避监管措施。
|
||||
2. **未正确标明来源**:未正确标明所贡献内容的来源。
|
||||
3. **宣传材料**:以不符合社区规范的方式分享营销或其他商业内容。
|
||||
4. **不当传播**:未能以负责任的方式呈现包含、链接或描述任何其他受限制行为的内容。
|
||||
|
||||
|
||||
## 通报问题
|
||||
|
||||
即使社区成员之间尽其所能地合作,也仍然可能发生矛盾。并不是所有冲突都涉及违反行为准则,本准则旨在强化受鼓励的行为与规范,它们有助于预防冲突并将伤害降到最低。
|
||||
|
||||
当事件发生时,及时报告非常重要。要报告可能的违规行为,**youwukuncheng@163.com**。
|
||||
|
||||
社区管理员会严肃对待违规报告,并尽力及时回应。将对所有违反行为准则的报告展开调查,方式包括查阅消息、日志及录音,或访谈证人及其他相关参与人。社区管理员在优先保障安全与保密性的前提下,会尽可能保持调查与执行过程的透明度。为践行这些价值观,执行措施会在涉事各方的私下环境中进行,但如经各方同意,将事件通报全体社区也可以作为解决方案的一部分。
|
||||
|
||||
|
||||
## 处理与弥补伤害
|
||||
|
||||
**[注意:下文所列的处理办法与补救措施,是基于行为准则执行过程中的最佳做法而提出的建议。如果你们的社区已经有既定的执行流程,请确保修改本段内容,以描述你们自己的政策。]**
|
||||
|
||||
若社区管理员经调查确认存在违反此行为准则的行为,将参照以下"分级处理"机制,根据事件对相关人员及社区整体造成的影响程度,确定最适宜的伤害弥补方案。根据违规严重程度,可跳过较低级别的处理措施。
|
||||
|
||||
1) 警告
|
||||
1) **事件**:单次或连续违规行为
|
||||
2) **后果**:社区管理员将发出书面私信警告
|
||||
3) **弥补**:弥补的方式如书面私下致歉、坦承自己的责任,或主动确认清楚今后该如何做才符合期待。
|
||||
2) 暂时限制活动
|
||||
1) **事件**:重复造成先前已被警告的违规,或首次发生略为严重的违规行为。
|
||||
2) **后果**:发出私下的书面警告,并设定一个有时间限制的冷静期,旨在强调事态的严重性,并让相关社区成员有时间消化与处理该事件。冷静期可能是限制在特定的交流渠道,或限制与特定社区成员的互动。
|
||||
3) **弥补**:修复的方式可能包括道歉、利用冷静期反思自身行为及其影响,以及充分意识到在冷静期结束后如何重新进入社区空间。
|
||||
3) 暂时停权
|
||||
1) **事件**:出现社区管理员已多次警告后仍然重复违规的模式,或一次严重违规行为。
|
||||
2) **后果**:发出私下的书面警告,并附上恢复权限所需满足的条件。通常,临时停权旨在给予被停权者时间,反思其行为以及考虑可能的改正措施。
|
||||
3) **弥补**:弥补的条件包括尊重停权的意旨、达成恢复权限的指定条件,以及充分认识到在停权解除后如何重新融入社区。
|
||||
4) 永久封禁
|
||||
1) **事件**:多次违反行为准则,且其他分级处理措施均未能解决问题,或发生严重到社区管理员认定无法在该成员继续存在的情况下保障社区安全的违规行为。
|
||||
2) **后果**:撤销其对所有社区空间、工具及交流渠道的访问权限。一般而言,永久封禁应极少使用,必须有充分且有力的理由,且仅在其他弥补手段未能改变其行为时才作为最后手段实施。
|
||||
3) **弥补**:此类严重情形下,不存在可行的弥补途径。
|
||||
|
||||
本分级处理措施旨在作为指导方针,并不限制社区管理者在符合社区最大利益的前提下,运用其自主裁量权与判断力。
|
||||
|
||||
|
||||
## 施行范围
|
||||
|
||||
本行为准则适用于社区所有空间,同时也适用于个人在公共场合或其他场合正式代表社区的情况。代表社区的行为包括但不限于:使用官方电子邮件地址;通过官方社交媒体账号发布内容;作为指定代表出席线上或线下活动。
|
||||
|
||||
|
||||
## 贡献归属
|
||||
|
||||
本行为准则改编自 贡献者公约 3.0 版,该公约永久可在此查阅:[https://www.contributor-covenant.org/version/3/0/](https://www.contributor-covenant.org/version/3/0/)。
|
||||
|
||||
贡献者公约 由 Organization for Ethical Source 负责维护,并以 CC BY-SA 4.0 许可协议发布。查看该许可协议请访问:[https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/)。
|
||||
|
||||
关于 贡献者公约 的常见问题解答,请参阅:[https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq)。各语言版本译文请见:[https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations)。更多执行与社区指南资源请见:[https://www.contributor-covenant.org/resources](https://www.contributor-covenant.org/resources)。本分级措施的灵感来源于 [Mozilla 行为准则团队](https://github.com/mozilla/inclusion) 的工作。
|
||||
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
# 贡献指南
|
||||
|
||||
感谢你对 `chanlun` 项目的关注!
|
||||
|
||||
本项目将 Python 版缠论技术分析库 (`chan.py`) 完整移植为 Rust,同时通过 PyO3 绑定层保持 Python API 兼容。以下指南旨在帮助平滑贡献流程。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- [角色与分工](#角色与分工)
|
||||
- [开发环境](#开发环境)
|
||||
- [项目结构](#项目结构)
|
||||
- [开发流程](#开发流程)
|
||||
- [代码规范](#代码规范)
|
||||
- [测试指南](#测试指南)
|
||||
- [提交信息](#提交信息)
|
||||
- [双端对齐](#双端对齐)
|
||||
|
||||
---
|
||||
|
||||
## 角色与分工
|
||||
|
||||
| 角色 | 范围 | 联系 |
|
||||
|------|------|------|
|
||||
| 维护者 | 架构决策、代码审查、发布 | @YuWuKunCheng |
|
||||
| 贡献者 | 提交 PR、报告 Bug、改进文档 | 任何人 |
|
||||
|
||||
---
|
||||
|
||||
## 开发环境
|
||||
|
||||
### 必需工具
|
||||
|
||||
| 工具 | 最低版本 | 用途 |
|
||||
|------|---------|------|
|
||||
| Rust | 1.85+ | 核心层编译 |
|
||||
| Python | 3.10+ | 绑定层测试、对比验证 |
|
||||
| maturin | 1.x | PyO3 绑定开发与安装 |
|
||||
|
||||
### 初始化
|
||||
|
||||
```bash
|
||||
# 克隆仓库
|
||||
git clone https://github.com/YuYuKunKun/chanlun.rs.git
|
||||
cd chanlun.rs
|
||||
|
||||
# 核心层
|
||||
cd chanlun
|
||||
cargo build
|
||||
cargo test
|
||||
|
||||
# 绑定层
|
||||
cd ../chanlun-py
|
||||
maturin develop
|
||||
python3 -m pytest tests/test_all.py -v
|
||||
|
||||
# 确保 clippy 零警告
|
||||
cd ../chanlun
|
||||
cargo clippy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
chanlun.rs/
|
||||
├── chan.py # Python 参考实现 (~4200 行)
|
||||
├── chanlun/ # Rust 核心层
|
||||
│ ├── Cargo.toml
|
||||
│ └── src/
|
||||
│ ├── lib.rs # 模块注册
|
||||
│ ├── config.rs # 缠论配置 (62 字段, serde)
|
||||
│ ├── types/ # 基础类型
|
||||
│ ├── kline/ # K线层
|
||||
│ ├── indicators/ # 技术指标
|
||||
│ ├── algorithm/ # 核心算法 (笔/线段/中枢/背驰)
|
||||
│ ├── structure/ # 结构体 (虚线/分型/特征)
|
||||
│ ├── business/ # 业务层 (观察者/合成器/立体分析)
|
||||
│ └── utils/ # 工具
|
||||
├── chanlun-py/ # PyO3 Python 绑定
|
||||
│ ├── Cargo.toml
|
||||
│ ├── src/
|
||||
│ │ ├── lib.rs # 模块注册与导出
|
||||
│ │ ├── business_py.rs # 业务层 Python 封装
|
||||
│ │ ├── config_py.rs # 配置 Python 封装
|
||||
│ │ └── structure_py.rs # 结构体 Python 封装
|
||||
│ ├── chanlun/ # Python 存根模块
|
||||
│ │ └── __init__.py
|
||||
│ └── tests/
|
||||
│ └── test_all.py # 完整测试套件
|
||||
├── CLAUDE.md # AI 辅助开发指令
|
||||
├── .github/ # GitHub 模板
|
||||
│ ├── pull_request_template.md
|
||||
│ └── ISSUE_TEMPLATE/
|
||||
│ ├── bug_report.md
|
||||
│ ├── feature_request.md
|
||||
│ └── custom.md
|
||||
├── README.md
|
||||
├── SECURITY.md
|
||||
└── CODE_OF_CONDUCT.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 开发流程
|
||||
|
||||
### 从 Issue 开始
|
||||
|
||||
1. 查找或创建相关 Issue
|
||||
2. 在 Issue 中讨论方案,达成共识后再开始编码
|
||||
3. 避免在没有 Issue 的情况下提交大型 PR
|
||||
|
||||
### 分支策略
|
||||
|
||||
```bash
|
||||
# 从 develop 分支创建功能分支
|
||||
git checkout develop
|
||||
git pull origin develop
|
||||
git checkout -b feature/your-feature-name
|
||||
|
||||
# 或从 develop 分支创建修复分支
|
||||
git checkout -b fix/your-bug-fix
|
||||
```
|
||||
|
||||
### 提交 PR
|
||||
|
||||
1. 确保所有测试通过
|
||||
2. 确保 `cargo clippy` 零警告
|
||||
3. 推送到你的分支并发起 PR 到 `develop`
|
||||
4. 填写 PR 模板中的所有内容
|
||||
5. 等待审查并响应反馈
|
||||
|
||||
---
|
||||
|
||||
## 代码规范
|
||||
|
||||
### 中文标识符
|
||||
|
||||
所有类型名、方法名、字段名必须使用中文,与 `chan.py` 保持 1:1 对应:
|
||||
|
||||
```rust
|
||||
// ✓ 正确
|
||||
pub struct 缠论K线 { pub 高: SyncF64, pub 低: SyncF64 }
|
||||
pub fn 方向(&self) -> 相对方向 { ... }
|
||||
|
||||
// ✗ 错误 — 不允许英文
|
||||
pub struct ChanKline { pub high: f64 }
|
||||
```
|
||||
|
||||
### 许可证头部
|
||||
|
||||
每个 `.rs` 文件必须以 MIT 许可证头部开始:
|
||||
|
||||
```rust
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2026 YuYuKunKun
|
||||
* ...
|
||||
*/
|
||||
```
|
||||
|
||||
### 代码风格
|
||||
|
||||
- 使用 `cargo fmt` 自动格式化
|
||||
- 遵循 `cargo clippy` 建议(零警告)
|
||||
- 仅写必要注释 — 解释"为什么"而非"做什么"
|
||||
- 不对仅使用一次的代码做抽象
|
||||
- 不添加方案之外的特性和错误处理
|
||||
|
||||
### Rust 相关约定
|
||||
|
||||
- `#![allow(non_snake_case)]` 和 `#![allow(non_camel_case_types)]` 已在 `lib.rs` 中声明
|
||||
- 内部可变性优先用 `AtomicI64`/`AtomicBool`/`SyncF64`,复杂字段用 `RwLock`
|
||||
- `Arc<分型>` 通过 `Arc::as_ptr` 比较身份(而非值比较)
|
||||
- 全局缓存使用 `LazyLock<Mutex<>>`,不使用 `thread_local!`
|
||||
- 读写锁作用域化,防止死锁
|
||||
|
||||
---
|
||||
|
||||
## 测试指南
|
||||
|
||||
### 核心层测试
|
||||
|
||||
```bash
|
||||
cd chanlun
|
||||
cargo test # 运行所有测试
|
||||
cargo test -- <name> # 运行匹配名称的测试
|
||||
```
|
||||
|
||||
测试应覆盖:
|
||||
- 类型构造/字段读写/Clone 后指针一致性
|
||||
- 算法函数的边界情况(空序列、单元素、极端价格)
|
||||
- 流式增量结果与静态重新分析的一致性
|
||||
- `Send + Sync` 编译期断言
|
||||
- 跨线程读写不 panic
|
||||
|
||||
### 绑定层测试
|
||||
|
||||
```bash
|
||||
cd chanlun-py
|
||||
maturin develop
|
||||
python3 -m pytest tests/test_all.py -v
|
||||
```
|
||||
|
||||
测试应覆盖:
|
||||
- Python API 与 `chan.py` 的接口兼容性
|
||||
- 跨线程 `is` 身份一致性
|
||||
- 双端(Rust 绑定 vs `chan.py`)关键算法输出对比
|
||||
|
||||
### 双端对比
|
||||
|
||||
当我们修改算法层代码时,必须验证 Rust 输出与 Python 版一致:
|
||||
|
||||
```python
|
||||
# 典型双端对比模式
|
||||
from chanlun import 观察者 as 观察者Rust
|
||||
from chanlun.chan import 观察者 as 观察者Py
|
||||
|
||||
# 加载同样的数据
|
||||
obs_rust = 观察者Rust("btcusd", 300, config)
|
||||
obs_py = 观察者Py("btcusd", 300, config)
|
||||
|
||||
# 对比结果
|
||||
assert len(obs_rust.笔序列) == len(obs_py.笔序列)
|
||||
assert len(obs_rust.线段序列) == len(obs_py.线段序列)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 提交信息
|
||||
|
||||
使用简洁的中文,格式为:
|
||||
|
||||
```
|
||||
<类型>: <简要描述>
|
||||
|
||||
<详细说明(可选)>
|
||||
|
||||
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||||
```
|
||||
|
||||
类型示例:
|
||||
- `fix:` — Bug 修复
|
||||
- `feat:` — 新功能
|
||||
- `refactor:` — 重构(行为不变)
|
||||
- `test:` — 添加或修改测试
|
||||
- `docs:` — 文档更新
|
||||
- `chore:` — 构建/工具
|
||||
|
||||
所有提交必须以 `Co-Authored-By:` 行结尾,这是本项目对 AI 辅助开发的惯例。
|
||||
|
||||
---
|
||||
|
||||
## 双端对齐
|
||||
|
||||
本项目最核心的质量要求是 Rust 实现与 `chan.py` 行为完全一致。对齐时遵循:
|
||||
|
||||
1. **以 `chan.py` 为准** — Python 实现是 golden source
|
||||
2. **增量对齐** — 优先修复数量差异(笔数、线段数),再深入字段级对齐
|
||||
3. **算法差异分类**:
|
||||
- 核心公式错误:如 MACD 面积计算 `阳+阴` vs `阳+|阴|`
|
||||
- 边界条件遗漏:如 `计算MACD柱子分段` 末尾段未追加
|
||||
- 指针身份 vs 值索引:`position(|k| Arc::as_ptr(k) == ...)` vs `position(|k| k.序号 == ...)`
|
||||
4. **使用测试驱动** — 先写双端对比测试,确认差异存在,再改 Rust 代码对齐
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
# 安全策略
|
||||
|
||||
## 适用范围
|
||||
|
||||
本安全策略适用于 `chanlun` Rust 核心库、`chanlun-py` Python 绑定层,以及本仓库中的相关工具和脚本。
|
||||
|
||||
## 免责声明
|
||||
|
||||
本项目是一个**技术分析库**,用于价格走势的结构化分析。它不存储用户资金、不处理身份认证、不直接发起交易,但反馈任何潜在安全漏洞(如代码层面的崩溃、死锁、不安全内存操作等)仍非常重要。
|
||||
|
||||
## 支持的版本
|
||||
|
||||
| 版本 | 支持状态 |
|
||||
|------|---------|
|
||||
| `26.x` | 积极支持 |
|
||||
| `< 26.0` | 不再支持 |
|
||||
|
||||
## 报告漏洞
|
||||
|
||||
如果您发现了安全漏洞,请**不要**通过公开的 Issue 报告。请通过以下方式私密报告:
|
||||
|
||||
- **邮箱**: youwukuncheng@163.com
|
||||
- **主题**: `[SECURITY] — <简要描述>`
|
||||
|
||||
请在报告中包含:
|
||||
|
||||
1. **漏洞描述** — 清晰描述漏洞的性质
|
||||
2. **复现步骤** — 最小可复现的代码片段或操作序列
|
||||
3. **受影响版本** — 您正在使用的 `chanlun` 版本号
|
||||
4. **潜在影响** — 可能产生的后果(崩溃、数据泄露、死锁等)
|
||||
5. **建议修复** — 如果您有修复建议
|
||||
|
||||
## 处理流程
|
||||
|
||||
收到报告后,我们承诺:
|
||||
|
||||
1. **确认收到** — 3 个工作日内确认收到报告
|
||||
2. **初步评估** — 7 个工作日内完成漏洞严重性评估并通知报告者
|
||||
3. **修复时间线** — 根据严重程度:
|
||||
- 严重(可导致崩溃/死锁/未定义行为): 14 天内发布修复
|
||||
- 中等: 30 天内发布修复
|
||||
- 低风险: 在下一个常规版本中包含修复
|
||||
4. **公开披露** — 修复发布后,在 Release Notes 中致谢报告者(需经同意)
|
||||
|
||||
## 关注领域
|
||||
|
||||
以下类型的漏洞尤其值得关注:
|
||||
|
||||
- `unsafe` 代码块中的内存安全问题
|
||||
- `RwLock` 死锁(读锁中获取写锁)
|
||||
- `LazyLock<Mutex<>>` 全局缓存的锁竞争
|
||||
- `panic!` 导致的未预期崩溃
|
||||
- `Arc` 循环引用造成的内存泄漏
|
||||
- 大端字节序列化 (`to_bytes`/`from_bytes`) 的缓冲区越界
|
||||
- PyO3 FFI 边界的类型转换安全
|
||||
- `AtomicI64` / `SyncF64` 的 `Ordering::Relaxed` 使用是否合理
|
||||
|
||||
## 偏好语言
|
||||
|
||||
请使用简体中文或英文撰写报告。
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "chanlun-py"
|
||||
version = "26.6.17"
|
||||
version = "26.6.47"
|
||||
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.3" # { path = "../chanlun" }
|
||||
lru = "0.18"
|
||||
pyo3 = { version = "0.28", features = ["experimental-inspect"] }
|
||||
serde_json = "1"
|
||||
chrono = "0.4"
|
||||
|
||||
+15
-4
@@ -12,6 +12,15 @@ def get_分型模式() -> bool: ...
|
||||
def set_分型模式(value: bool) -> None: ...
|
||||
def 转化为时间戳(ts: Any) -> int: ...
|
||||
def 转化为时间戳_数字(ts: Any) -> int: ...
|
||||
def K线相等(A: K线, B: K线, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
|
||||
def 缠论K线相等(A: 缠论K线, B: 缠论K线, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
|
||||
def 分型相等(A: 分型, B: 分型, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
|
||||
def 缺口相等(A: 缺口, B: 缺口, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
|
||||
def 线段特征相等(A: 线段特征, B: 线段特征, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
|
||||
def 中枢相等(A: 中枢, B: 中枢, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
|
||||
def 虚线相等(A: 虚线, B: 虚线, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
|
||||
def 观察者相等(A: 观察者, B: 观察者, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
|
||||
def 立体分析器相等(A: 立体分析器, B: 立体分析器, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
|
||||
|
||||
# ========== Enum-like types ==========
|
||||
|
||||
@@ -799,16 +808,17 @@ class K线合成器:
|
||||
def 标识(self) -> str: ...
|
||||
@property
|
||||
def 周期组(self) -> List[int]: ...
|
||||
def 投喂K线(self, 普K: K线) -> List[Tuple[int, K线]]: ...
|
||||
def 投喂(self, 时间戳: int, 开: float, 高: float, 低: float, 收: float, 量: float) -> List[Tuple[int, K线]]: ...
|
||||
def 投喂K线(self, 普K: K线) -> None: ...
|
||||
def 投喂(self, 时间戳: int, 开: float, 高: float, 低: float, 收: float, 量: float) -> None: ...
|
||||
def 获取当前K线(self, 周期: int) -> Optional[K线]: ...
|
||||
|
||||
class 立体分析器:
|
||||
def __init__(self, 符号: str, 周期组: List[int], 配置: Optional[缠论配置] = None, 配置组: Optional[Dict[int, 缠论配置]] = None) -> None: ...
|
||||
@property
|
||||
def 周期组(self) -> List[int]: ...
|
||||
@property
|
||||
def _单体分析器(self) -> Dict[int, 观察者]: ...
|
||||
def 投喂K线(self, 普K: K线) -> None: ...
|
||||
def 获取观察者(self, 周期: int) -> Optional[观察者]: ...
|
||||
def 测试_保存数据(self, root: Optional[str] = None) -> None: ...
|
||||
|
||||
# ========== 缠论配置 (fields via __getattr__/__setattr__) ==========
|
||||
@@ -818,7 +828,8 @@ class 缠论配置:
|
||||
def to_dict(self) -> Dict[str, Any]: ...
|
||||
def to_json(self) -> str: ...
|
||||
def 保存配置(self, path: str = "缠论配置.json") -> None: ...
|
||||
def 对比(self, other: 缠论配置) -> Dict[str, Tuple[Any, Any]]: ...
|
||||
def 对比(self, other: 缠论配置) -> Dict[str, Any]: ...
|
||||
def model_copy(self, update: Optional[Dict[str, Any]] = None) -> 缠论配置: ...
|
||||
@classmethod
|
||||
def 加载配置(cls, path: str = "缠论配置.json") -> 缠论配置: ...
|
||||
@classmethod
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -5,13 +5,21 @@ 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]: ...
|
||||
def 观察者相等(A: 观察者, B: 观察者, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
|
||||
def 立体分析器相等(A: 立体分析器, B: 立体分析器, 浮点容差: float = 1e-9) -> Tuple[bool, str]: ...
|
||||
|
||||
# ========== Enum-like types ==========
|
||||
|
||||
@@ -429,9 +437,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 +525,8 @@ class 虚线:
|
||||
class 线段特征:
|
||||
@property
|
||||
def 序号(self) -> int: ...
|
||||
@序号.setter
|
||||
def 序号(self, value: int) -> None: ...
|
||||
@property
|
||||
def 标识(self) -> str: ...
|
||||
@标识.setter
|
||||
@@ -784,6 +794,30 @@ class 观察者:
|
||||
def 扩展线段序列_扩展线段(self) -> List[虚线]: ...
|
||||
@property
|
||||
def 扩展中枢序列_扩展线段(self) -> List[中枢]: ...
|
||||
@property
|
||||
def 线段分析层次(self) -> int: ...
|
||||
@线段分析层次.setter
|
||||
def 线段分析层次(self, value: int) -> None: ...
|
||||
@property
|
||||
def 扩展线段分析层次(self) -> int: ...
|
||||
@扩展线段分析层次.setter
|
||||
def 扩展线段分析层次(self, value: int) -> None: ...
|
||||
@property
|
||||
def 混合扩展线段分析层次(self) -> int: ...
|
||||
@混合扩展线段分析层次.setter
|
||||
def 混合扩展线段分析层次(self, value: int) -> None: ...
|
||||
@property
|
||||
def 线段序列组(self) -> List[List[虚线]]: ...
|
||||
@property
|
||||
def 中枢序列组(self) -> List[List[中枢]]: ...
|
||||
@property
|
||||
def 扩展线段序列组(self) -> List[List[虚线]]: ...
|
||||
@property
|
||||
def 扩展中枢序列组(self) -> List[List[中枢]]: ...
|
||||
@property
|
||||
def 混合扩展线段序列组(self) -> List[List[虚线]]: ...
|
||||
@property
|
||||
def 混合扩展中枢序列组(self) -> List[List[中枢]]: ...
|
||||
def 重置基础序列(self) -> None: ...
|
||||
def 增加原始K线(self, 普K: K线) -> None: ...
|
||||
def 投喂原始数据(self, 时间戳: int, 开: float, 高: float, 低: float, 收: float, 量: float) -> None: ...
|
||||
@@ -799,16 +833,17 @@ class K线合成器:
|
||||
def 标识(self) -> str: ...
|
||||
@property
|
||||
def 周期组(self) -> List[int]: ...
|
||||
def 投喂K线(self, 普K: K线) -> List[Tuple[int, K线]]: ...
|
||||
def 投喂(self, 时间戳: int, 开: float, 高: float, 低: float, 收: float, 量: float) -> List[Tuple[int, K线]]: ...
|
||||
def 投喂K线(self, 普K: K线) -> None: ...
|
||||
def 投喂(self, 时间戳: int, 开: float, 高: float, 低: float, 收: float, 量: float) -> None: ...
|
||||
def 获取当前K线(self, 周期: int) -> Optional[K线]: ...
|
||||
|
||||
class 立体分析器:
|
||||
def __init__(self, 符号: str, 周期组: List[int], 配置: Optional[缠论配置] = None, 配置组: Optional[Dict[int, 缠论配置]] = None) -> None: ...
|
||||
@property
|
||||
def 周期组(self) -> List[int]: ...
|
||||
@property
|
||||
def _单体分析器(self) -> Dict[int, 观察者]: ...
|
||||
def 投喂K线(self, 普K: K线) -> None: ...
|
||||
def 获取观察者(self, 周期: int) -> Optional[观察者]: ...
|
||||
def 测试_保存数据(self, root: Optional[str] = None) -> None: ...
|
||||
|
||||
# ========== 缠论配置 (fields via __getattr__/__setattr__) ==========
|
||||
@@ -818,7 +853,8 @@ class 缠论配置:
|
||||
def to_dict(self) -> Dict[str, Any]: ...
|
||||
def to_json(self) -> str: ...
|
||||
def 保存配置(self, path: str = "缠论配置.json") -> None: ...
|
||||
def 对比(self, other: 缠论配置) -> Dict[str, Tuple[Any, Any]]: ...
|
||||
def 对比(self, other: 缠论配置) -> Dict[str, Any]: ...
|
||||
def model_copy(self, update: Optional[Dict[str, Any]] = None) -> 缠论配置: ...
|
||||
@classmethod
|
||||
def 加载配置(cls, path: str = "缠论配置.json") -> 缠论配置: ...
|
||||
@classmethod
|
||||
|
||||
+701
-155
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "chanlun"
|
||||
version = "2606.17"
|
||||
version = "2606.47"
|
||||
description = "缠论技术分析库 — Rust 高性能实现"
|
||||
readme = { file = "README.md", content-type = "text/markdown" }
|
||||
license = { file = "LICENSE", content-type = "text/plain" }
|
||||
@@ -28,6 +28,7 @@ classifiers = [
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"termcolor>=3.0",
|
||||
"parse>=1.2",
|
||||
"loguru>=0.6",
|
||||
"backtrader==1.9.78.123",
|
||||
]
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
+205
-74
@@ -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线合成器 ==========
|
||||
@@ -1136,9 +1293,8 @@ impl 观察者Py {
|
||||
/// K线合成器(标识, 周期组) — 周期组为升序排列的整数列表(如 [60, 300, 900])
|
||||
///
|
||||
/// 方法:
|
||||
/// 投喂K线(普K) -> list[(周期, K线)] — 喂入普通K线,返回合成后的大周期K线
|
||||
/// 投喂(时间戳, 开盘价, 最高价, 最低价, 收盘价, 成交量) -> list[(周期, K线)]
|
||||
/// — 快捷入口,免去构造K线对象
|
||||
/// 投喂K线(普K) — 喂入普通K线,完成事件通过回调分发
|
||||
/// 投喂(时间戳, 开盘价, 最高价, 最低价, 收盘价, 成交量) — 快捷入口
|
||||
/// 获取当前K线(周期) -> K线|None — 获取指定周期的当前合成结果
|
||||
#[pyclass(name = "K线合成器", module = "chanlun._chanlun")]
|
||||
pub struct K线合成器Py {
|
||||
@@ -1150,51 +1306,18 @@ impl K线合成器Py {
|
||||
#[new]
|
||||
fn new(标识: String, 周期组: Vec<i64>) -> Self {
|
||||
Self {
|
||||
inner: chanlun::business::synthesizer::K线合成器::new(标识, 周期组),
|
||||
inner: chanlun::business::synthesizer::K线合成器::new(标识, 周期组, None),
|
||||
}
|
||||
}
|
||||
|
||||
/// 统一入口 — 投喂最小周期K线,自动合成大周期并分发给各周期观察者
|
||||
fn 投喂K线(
|
||||
&mut self,
|
||||
普K: &Bound<'_, K线Py>,
|
||||
py: Python<'_>,
|
||||
) -> PyResult<Vec<(i64, Py<K线Py>)>> {
|
||||
let results = self.inner.投喂K线((*普K.borrow().inner).clone());
|
||||
Ok(results
|
||||
.into_iter()
|
||||
.map(|(周期, k)| (周期, bar_to_py(py, Arc::new(k))))
|
||||
.collect())
|
||||
/// 投喂K线 — 输入最小周期K线,合成为所有目标周期,完成事件通过回调分发
|
||||
fn 投喂K线(&mut self, 普K: &Bound<'_, K线Py>) {
|
||||
self.inner.投喂K线((*普K.borrow().inner).clone());
|
||||
}
|
||||
|
||||
/// 投喂原始tick数据
|
||||
fn 投喂(
|
||||
&mut self,
|
||||
时间戳: i64,
|
||||
开: f64,
|
||||
高: f64,
|
||||
低: f64,
|
||||
收: f64,
|
||||
量: f64,
|
||||
py: Python<'_>,
|
||||
) -> Vec<(i64, Py<K线Py>)> {
|
||||
let min_cycle = self.inner.周期组.iter().copied().min().unwrap_or(1);
|
||||
let k = chanlun::kline::bar::K线::创建普K(
|
||||
&self.inner.标识,
|
||||
时间戳,
|
||||
开,
|
||||
高,
|
||||
低,
|
||||
收,
|
||||
量,
|
||||
0,
|
||||
min_cycle,
|
||||
);
|
||||
let results = self.inner.投喂K线(k);
|
||||
results
|
||||
.into_iter()
|
||||
.map(|(周期, k2)| (周期, bar_to_py(py, Arc::new(k2))))
|
||||
.collect()
|
||||
fn 投喂(&mut self, 时间戳: i64, 开: f64, 高: f64, 低: f64, 收: f64, 量: f64) {
|
||||
self.inner.投喂(时间戳, 开, 高, 低, 收, 量);
|
||||
}
|
||||
|
||||
/// 获取指定周期当前正在合成的K线
|
||||
@@ -1273,12 +1396,6 @@ impl 立体分析器Py {
|
||||
self.inner.投喂K线((*普K.borrow().inner).clone());
|
||||
}
|
||||
|
||||
fn 获取观察者(&self, 周期: i64) -> Option<观察者Py> {
|
||||
self.inner
|
||||
.获取观察者(周期)
|
||||
.map(|rc| 观察者Py { inner: Some(rc) })
|
||||
}
|
||||
|
||||
/// 拆分各序列数据,单独存文件,文件名为对应变量名
|
||||
fn 测试_保存数据(&self, root: Option<&str>) {
|
||||
self.inner.测试_保存数据(root);
|
||||
@@ -1288,6 +1405,20 @@ impl 立体分析器Py {
|
||||
fn 周期组(&self) -> Vec<i64> {
|
||||
self.inner.周期组.clone()
|
||||
}
|
||||
|
||||
/// _单体分析器 — 对应 Python 立体分析器._单体分析器: dict[周期, 观察者]
|
||||
#[getter(_单体分析器)]
|
||||
fn get_单体分析器(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
|
||||
let dict = pyo3::types::PyDict::new(py);
|
||||
for (周期, obs_rc) in &self.inner.单体分析器 {
|
||||
let obs_py = 观察者Py {
|
||||
inner: Some(obs_rc.clone()),
|
||||
配置缓存: std::sync::Mutex::new(None),
|
||||
};
|
||||
dict.set_item(周期, obs_py)?;
|
||||
}
|
||||
Ok(dict.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
|
||||
+34
-17
@@ -169,8 +169,11 @@ impl 缠论配置Py {
|
||||
/// 将配置导出为 Python 字典。
|
||||
fn to_dict(&self, py: Python<'_>) -> PyResult<Py<PyDict>> {
|
||||
let dict = PyDict::new(py);
|
||||
let valid = chanlun::config::缠论配置::model_fields();
|
||||
for (k, v) in &self.fields {
|
||||
dict.set_item(k, v.clone_ref(py))?;
|
||||
if valid.contains(&k.as_str()) {
|
||||
dict.set_item(k, v.clone_ref(py))?;
|
||||
}
|
||||
}
|
||||
Ok(dict.into())
|
||||
}
|
||||
@@ -252,26 +255,36 @@ impl 缠论配置Py {
|
||||
Ok(result.into())
|
||||
}
|
||||
|
||||
/// 比较当前配置与另一个配置的差异
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn 对比(
|
||||
&self,
|
||||
py: Python<'_>,
|
||||
other: &Bound<'_, 缠论配置Py>,
|
||||
) -> PyResult<HashMap<String, (Py<PyAny>, Py<PyAny>)>> {
|
||||
/// 创建当前配置的拷贝并可选择更新字段(对应 Python model_copy(update={...}, deep=True))
|
||||
#[pyo3(signature = (update = None))]
|
||||
fn model_copy(&self, py: Python<'_>, update: Option<&Bound<'_, PyDict>>) -> PyResult<Self> {
|
||||
let current = self.to_dict(py)?;
|
||||
if let Some(updates) = update {
|
||||
for (key, value) in updates.iter() {
|
||||
current.bind(py).set_item(key, value)?;
|
||||
}
|
||||
}
|
||||
Self::from_dict(&py.get_type::<Self>(), current.bind(py))
|
||||
}
|
||||
|
||||
/// 比较当前配置与另一个配置的差异(对应 Python 对比 → dict[字段名, 新值])
|
||||
fn 对比(&self, py: Python<'_>, other: &Bound<'_, 缠论配置Py>) -> PyResult<Py<PyAny>> {
|
||||
let other_ref = other.borrow();
|
||||
let mut diff = HashMap::new();
|
||||
for (key, val) in &self.fields {
|
||||
if let Some(other_val) = other_ref.fields.get(key) {
|
||||
let a = val.clone_ref(py);
|
||||
let dict = PyDict::new(py);
|
||||
let valid = chanlun::config::缠论配置::model_fields();
|
||||
for key in valid {
|
||||
if let (Some(self_val), Some(other_val)) =
|
||||
(self.fields.get(*key), other_ref.fields.get(*key))
|
||||
{
|
||||
let a = self_val.clone_ref(py);
|
||||
let b = other_val.clone_ref(py);
|
||||
let eq = a.bind(py).eq(b.bind(py))?;
|
||||
if !eq {
|
||||
diff.insert(key.clone(), (val.clone_ref(py), other_val.clone_ref(py)));
|
||||
dict.set_item(*key, b)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(diff)
|
||||
Ok(dict.into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,9 +406,9 @@ fn validate_field(
|
||||
) -> Result<(), String> {
|
||||
use serde_json::Value;
|
||||
|
||||
// 输入为 null → 跳过(保留默认)
|
||||
// 输入为 null → 保留(对应 Optional/Infinity 字段)
|
||||
if input.is_null() {
|
||||
return Err("值为 null".into());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 字符串字段:检查有效值白名单
|
||||
@@ -414,6 +427,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 +445,9 @@ fn validate_field(
|
||||
Value::Bool(_) => "布尔",
|
||||
Value::Number(_) => "数值",
|
||||
Value::String(_) => "字符串",
|
||||
_ => "其他",
|
||||
Value::Array(_) => "数组",
|
||||
Value::Object(_) => "字典",
|
||||
Value::Null => "null",
|
||||
};
|
||||
Err(format!("类型不匹配(需要 {expected},收到 {type_name})"))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,826 @@
|
||||
/*
|
||||
* 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 crate::business_py::立体分析器Py;
|
||||
use crate::business_py::观察者Py;
|
||||
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!("{标签}: 全字段所有嵌套子结构校验一致")))
|
||||
}
|
||||
|
||||
// ========== 观察者相等 ==========
|
||||
|
||||
#[pyfunction]
|
||||
fn 观察者相等(
|
||||
a: &Bound<'_, 观察者Py>,
|
||||
b: &Bound<'_, 观察者Py>,
|
||||
浮点容差: Option<f64>,
|
||||
) -> PyResult<(bool, String)> {
|
||||
let 容差 = 浮点容差.unwrap_or(1e-9);
|
||||
let arc_a = a
|
||||
.borrow()
|
||||
.inner
|
||||
.clone()
|
||||
.ok_or_else(|| pyo3::exceptions::PyValueError::new_err("观察者A 内部为空"))?;
|
||||
let arc_b = b
|
||||
.borrow()
|
||||
.inner
|
||||
.clone()
|
||||
.ok_or_else(|| pyo3::exceptions::PyValueError::new_err("观察者B 内部为空"))?;
|
||||
let obs_a = arc_a.read().unwrap();
|
||||
let obs_b = arc_b.read().unwrap();
|
||||
Ok(obs_a.相等(&obs_b, 容差))
|
||||
}
|
||||
|
||||
// ========== 立体分析器相等 ==========
|
||||
|
||||
#[pyfunction]
|
||||
fn 立体分析器相等(
|
||||
a: &Bound<'_, 立体分析器Py>,
|
||||
b: &Bound<'_, 立体分析器Py>,
|
||||
浮点容差: Option<f64>,
|
||||
) -> PyResult<(bool, String)> {
|
||||
let 容差 = 浮点容差.unwrap_or(1e-9);
|
||||
let result = {
|
||||
let ref_a = a.borrow();
|
||||
let ref_b = b.borrow();
|
||||
ref_a.inner.相等(&ref_b.inner, 容差)
|
||||
};
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> {
|
||||
m.add_function(wrap_pyfunction!(K线相等, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(缠论K线相等, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(分型相等, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(缺口相等, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(线段特征相等, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(中枢相等, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(虚线相等, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(观察者相等, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(立体分析器相等, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -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
@@ -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())))
|
||||
}
|
||||
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
+1562
-15
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "chanlun"
|
||||
version = "26.6.1"
|
||||
version = "26.6.3"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
description = "基于缠论(缠中说禅)理论的量化技术分析核心库,支持流式数据处理和多周期联立分析。"
|
||||
|
||||
+816
-284
File diff suppressed because it is too large
Load Diff
@@ -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().结构(),
|
||||
|
||||
@@ -222,6 +222,6 @@ struct MACD面积 {
|
||||
|
||||
impl MACD面积 {
|
||||
fn 总(&self) -> f64 {
|
||||
self.阳 + self.阴
|
||||
self.阳 + self.阴.abs()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 中枢 {
|
||||
|
||||
@@ -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 {}",
|
||||
@@ -805,7 +800,12 @@ impl 线段 {
|
||||
|
||||
seg.序号
|
||||
.store(之前线段.序号.load(Ordering::Relaxed) + 1, Ordering::Relaxed);
|
||||
*seg.前一缺口.write().unwrap() = Self::获取缺口(之前线段);
|
||||
*seg.前一缺口.write().unwrap() = if 之前线段.短路修正.load(Ordering::Relaxed)
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Self::获取缺口(之前线段)
|
||||
};
|
||||
*seg.前一结束位置.write().unwrap() = Some(Arc::clone(
|
||||
之前线段.基础序列.read().unwrap().last().unwrap(),
|
||||
));
|
||||
@@ -849,10 +849,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(弹出)
|
||||
}
|
||||
@@ -976,6 +975,7 @@ impl 线段 {
|
||||
}
|
||||
|
||||
let 左 = Arc::clone(&基础序列[基础序列.len() - 3]);
|
||||
let 中 = Arc::clone(&基础序列[基础序列.len() - 2]);
|
||||
let 右 = Arc::clone(&基础序列[基础序列.len() - 1]);
|
||||
|
||||
// 方向条件
|
||||
@@ -993,7 +993,8 @@ impl 线段 {
|
||||
基础序列
|
||||
);
|
||||
|
||||
let 原始基础序列 = 当前线段.基础序列.read().unwrap().clone();
|
||||
// Reassign to full copy (matching Python pattern)
|
||||
let 基础序列 = 当前线段.基础序列.read().unwrap().clone();
|
||||
Self::_弹出线段(
|
||||
线段序列,
|
||||
&Arc::clone(线段序列.last().unwrap()),
|
||||
@@ -1027,13 +1028,13 @@ impl 线段 {
|
||||
cur.特征序列.write().unwrap()[2] = None;
|
||||
|
||||
let 开始笔 = Arc::clone(cur.基础序列.read().unwrap().last().unwrap());
|
||||
let 开始序号 = 原始基础序列
|
||||
let 开始序号 = 基础序列
|
||||
.iter()
|
||||
.position(|x| Arc::as_ptr(x) == Arc::as_ptr(&开始笔));
|
||||
|
||||
开始序号_opt = 开始序号;
|
||||
if let Some(序号) = 开始序号 {
|
||||
待添加元素 = 原始基础序列[序号 + 1..].to_vec();
|
||||
待添加元素 = 基础序列[序号 + 1..].to_vec();
|
||||
} else {
|
||||
待添加元素 = Vec::new();
|
||||
}
|
||||
@@ -1051,20 +1052,16 @@ impl 线段 {
|
||||
Self::_刷新(&线段序列[idx], 配置);
|
||||
|
||||
let 当前线段 = Arc::clone(&线段序列[idx]);
|
||||
当前线段.短路修正.store(true, Ordering::Relaxed);
|
||||
if 当前线段.特征序列.read().unwrap()[2].is_some() {
|
||||
let 段 = 虚线::创建线段(&[
|
||||
Arc::clone(&基础序列[基础序列.len() - 3]),
|
||||
Arc::clone(&基础序列[基础序列.len() - 2]),
|
||||
Arc::clone(&基础序列[基础序列.len() - 1]),
|
||||
]);
|
||||
let 段 = 虚线::创建线段(&[Arc::clone(&左), Arc::clone(&中), Arc::clone(&右)]);
|
||||
let 段_rc = Arc::new(段);
|
||||
Self::_添加线段(线段序列, 段_rc, 配置, format!("{}, {}", line!(), 层级));
|
||||
|
||||
// Set feature sequence [0]
|
||||
let 新段 = Self::取段(线段序列.last_mut().unwrap());
|
||||
let 中笔 = Arc::clone(&基础序列[基础序列.len() - 2]);
|
||||
新段.特征序列.write().unwrap()[0] =
|
||||
Some(Arc::new(线段特征::新建(vec![中笔], 新段.方向())));
|
||||
Some(Arc::new(线段特征::新建(vec![中], 新段.方向())));
|
||||
}
|
||||
|
||||
true
|
||||
@@ -1510,9 +1507,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 +1688,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 +1705,7 @@ impl 线段 {
|
||||
|
||||
let 进入段 = &阳[阳.len() - 3];
|
||||
let 离开段 = &阳[阳.len() - 1];
|
||||
debug_assert!(
|
||||
assert!(
|
||||
进入段.序号.load(Ordering::Relaxed) < 离开段.序号.load(Ordering::Relaxed),
|
||||
"进入段.序号 >= 离开段.序号"
|
||||
);
|
||||
@@ -1780,7 +1761,7 @@ impl 线段 {
|
||||
{
|
||||
let 进入段 = &当前段.基础序列.read().unwrap()[序号 - 1];
|
||||
let 离开段 = &阳[阳.len() - 1];
|
||||
debug_assert!(
|
||||
assert!(
|
||||
进入段.序号.load(Ordering::Relaxed) < 离开段.序号.load(Ordering::Relaxed)
|
||||
);
|
||||
if 进入段.方向() != 离开段.方向() {
|
||||
@@ -1807,9 +1788,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::背驰分析::测度背驰(
|
||||
进入段, 离开段,
|
||||
|
||||
@@ -32,18 +32,15 @@ use std::sync::RwLock;
|
||||
use tracing::{error, info};
|
||||
|
||||
/// 立体分析器 — 多周期协调器
|
||||
///
|
||||
/// 包含一个K线合成器和每周期一个观察者。
|
||||
/// 输入最小周期K线,合成大周期后分发到对应观察者。
|
||||
pub struct 立体分析器 {
|
||||
pub 周期组: Vec<i64>,
|
||||
输入周期: i64,
|
||||
K线合成器: K线合成器,
|
||||
单体分析器: HashMap<i64, Arc<RwLock<观察者>>>,
|
||||
pub K线合成器: K线合成器,
|
||||
pub 单体分析器: HashMap<i64, Arc<RwLock<观察者>>>,
|
||||
}
|
||||
|
||||
impl 立体分析器 {
|
||||
/// 创建立体分析器,自动创建K线合成器 + 每周期一个观察者
|
||||
/// 创建立体分析器 — 对应 Python 立体分析器.__init__
|
||||
pub fn new(
|
||||
符号: String,
|
||||
周期组: Vec<i64>,
|
||||
@@ -58,9 +55,7 @@ impl 立体分析器 {
|
||||
let 默认配置 = 配置.unwrap_or_default();
|
||||
let 配置组 = 配置组.unwrap_or_default();
|
||||
|
||||
let K线合成器 = K线合成器::new(符号.clone(), 周期组.clone());
|
||||
|
||||
let mut 单体分析器 = HashMap::new();
|
||||
let mut 单体分析器: HashMap<i64, Arc<RwLock<观察者>>> = HashMap::new();
|
||||
for &周期 in &周期组 {
|
||||
let mut 当前配置 = 配置组
|
||||
.get(&周期)
|
||||
@@ -101,6 +96,18 @@ impl 立体分析器 {
|
||||
}
|
||||
}
|
||||
|
||||
// 对应 Python: K线合成器(符号, 周期组, self.__K线回调)
|
||||
let 单体分析器_回调 = 单体分析器.clone();
|
||||
let K线合成器 = K线合成器::new(
|
||||
符号.clone(),
|
||||
周期组.clone(),
|
||||
Some(Box::new(
|
||||
move |_信号类型: String, _标识: String, 周期: i64, 完成K线: K线| {
|
||||
立体分析器::__K线回调_调度(&单体分析器_回调, 周期, 完成K线);
|
||||
},
|
||||
)),
|
||||
);
|
||||
|
||||
Self {
|
||||
周期组,
|
||||
输入周期,
|
||||
@@ -109,8 +116,28 @@ impl 立体分析器 {
|
||||
}
|
||||
}
|
||||
|
||||
/// 投喂K线 — 统一入口,接收最小周期K线
|
||||
/// 匹配 Python __K线回调:合成器完成K线时喂给观察者
|
||||
/// __K线回调 — 对应 Python 立体分析器.__K线回调
|
||||
fn __K线回调(&self, _信号类型: String, _标识: String, 周期: i64, 完成K线: K线) {
|
||||
if let Some(观察员) = self.单体分析器.get(&周期) {
|
||||
let mut obs = 观察员.write().unwrap();
|
||||
obs.增加原始K线(完成K线);
|
||||
// 对应 Python: if 当前K线 := self._K线合成器.获取当前K线(周期)
|
||||
// _完成K线刚清空当前K线,获取当前K线返回 None,所以这里不添加
|
||||
}
|
||||
}
|
||||
|
||||
/// 静态调度版本 — 用于回调闭包
|
||||
fn __K线回调_调度(
|
||||
单体分析器: &HashMap<i64, Arc<RwLock<观察者>>>,
|
||||
周期: i64,
|
||||
完成K线: K线,
|
||||
) {
|
||||
if let Some(观察员) = 单体分析器.get(&周期) {
|
||||
观察员.write().unwrap().增加原始K线(完成K线);
|
||||
}
|
||||
}
|
||||
|
||||
/// 投喂K线 — 对应 Python 立体分析器.投喂K线
|
||||
pub fn 投喂K线(&mut self, 普K: K线) {
|
||||
if 普K.周期 != self.输入周期 {
|
||||
panic!(
|
||||
@@ -118,19 +145,7 @@ impl 立体分析器 {
|
||||
普K.周期, self.输入周期
|
||||
);
|
||||
}
|
||||
|
||||
// Feed to synthesizer, get completion events
|
||||
let 完成事件 = self.K线合成器.投喂K线(普K);
|
||||
|
||||
// Dispatch on completion events (matching Python's __K线回调)
|
||||
for (周期, 完成K线) in 完成事件 {
|
||||
if let Some(观察员) = self.单体分析器.get(&周期) {
|
||||
观察员.write().unwrap().增加原始K线(完成K线);
|
||||
if let Some(当前K线) = self.K线合成器.获取当前K线(周期) {
|
||||
观察员.write().unwrap().增加原始K线(当前K线.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
self.K线合成器.投喂K线(普K);
|
||||
}
|
||||
|
||||
/// 获取指定周期的观察者
|
||||
@@ -138,8 +153,7 @@ impl 立体分析器 {
|
||||
self.单体分析器.get(&周期).cloned()
|
||||
}
|
||||
|
||||
/// 测试_保存数据 — 多级别数据拆分保存
|
||||
/// 创建父目录 PyM_{标识}_{起始时间}_{结束时间},各周期观察者保存到子目录
|
||||
/// 测试_保存数据 — 对应 Python 立体分析器.测试_保存数据
|
||||
pub fn 测试_保存数据(&self, root: Option<&str>) {
|
||||
let 根目录 = match root {
|
||||
Some(r) => std::path::PathBuf::from(r),
|
||||
@@ -163,7 +177,6 @@ impl 立体分析器 {
|
||||
.get(&self.输入周期)
|
||||
.map(|o| o.read().unwrap().符号.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
let 周期 = self
|
||||
.单体分析器
|
||||
.get(&self.输入周期)
|
||||
@@ -189,4 +202,30 @@ impl 立体分析器 {
|
||||
|
||||
info!("多级别数据拆分保存完成,目录:{}", 保存路径.display());
|
||||
}
|
||||
|
||||
/// 相等 — 各周期观察者全量比对,对应 Python `立体分析器相等`
|
||||
pub fn 相等(&self, other: &Self, 浮点容差: f64) -> (bool, String) {
|
||||
let 标签 = format!("立体分析器校验[A={:?},B={:?}]", self.周期组, other.周期组);
|
||||
|
||||
if self.周期组 != other.周期组 {
|
||||
return (false, format!("{标签}: 周期组不一致"));
|
||||
}
|
||||
|
||||
for 周期 in &self.周期组 {
|
||||
let a_obs = match self.单体分析器.get(周期) {
|
||||
Some(o) => o.read().unwrap(),
|
||||
None => return (false, format!("{标签}: 周期{周期} 观察者不存在 (A)")),
|
||||
};
|
||||
let b_obs = match other.单体分析器.get(周期) {
|
||||
Some(o) => o.read().unwrap(),
|
||||
None => return (false, format!("{标签}: 周期{周期} 观察者不存在 (B)")),
|
||||
};
|
||||
let (eq, msg) = a_obs.相等(&b_obs, 浮点容差);
|
||||
if !eq {
|
||||
return (false, format!("{标签}: 周期{周期} >> {msg}"));
|
||||
}
|
||||
}
|
||||
|
||||
(true, format!("{标签}:所有周期观察者全量校验全部一致"))
|
||||
}
|
||||
}
|
||||
|
||||
+520
-248
@@ -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线序列,
|
||||
@@ -202,72 +285,79 @@ impl 观察者 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 4: 线段分析
|
||||
if self.配置.分析线段 {
|
||||
线段::分析(
|
||||
&self.笔序列,
|
||||
&mut self.线段序列,
|
||||
&self.配置,
|
||||
0,
|
||||
&[相对方向::向上, 相对方向::向下],
|
||||
);
|
||||
}
|
||||
if self.配置.分析线段中枢 {
|
||||
中枢::分析(&self.线段序列, &mut self.中枢序列, true, "", 0);
|
||||
// Step 4: 线段分析 — 3 级递归
|
||||
if self.配置.分析线段 || self.配置.分析线段中枢 {
|
||||
for i in 0..self.线段分析层次 {
|
||||
if i == 0 {
|
||||
if self.配置.分析线段 {
|
||||
线段::分析(
|
||||
&self.笔序列,
|
||||
&mut self.线段序列组[i],
|
||||
&self.配置,
|
||||
0,
|
||||
&[相对方向::向上, 相对方向::向下],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if self.配置.分析线段 {
|
||||
let 源序列 = self.线段序列组[i - 1].clone();
|
||||
线段::分析(
|
||||
&源序列,
|
||||
&mut self.线段序列组[i],
|
||||
&self.配置,
|
||||
0,
|
||||
&[相对方向::向上, 相对方向::向下],
|
||||
);
|
||||
}
|
||||
}
|
||||
if self.配置.分析线段中枢 {
|
||||
中枢::分析(&self.线段序列组[i], &mut self.中枢序列组[i], true, "", 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: 扩展线段(笔级)
|
||||
if self.配置.分析扩展线段 {
|
||||
线段::扩展分析(&self.笔序列, &mut self.扩展线段序列, &self.配置);
|
||||
}
|
||||
if self.配置.分析线段中枢 {
|
||||
中枢::分析(&self.扩展线段序列, &mut self.扩展中枢序列, true, "", 0);
|
||||
// Step 5: 扩展线段分析 — 3 级递归
|
||||
if self.配置.分析扩展线段 || self.配置.分析线段中枢 {
|
||||
for i in 0..self.扩展线段分析层次 {
|
||||
if i == 0 {
|
||||
if self.配置.分析扩展线段 {
|
||||
线段::扩展分析(&self.笔序列, &mut self.扩展线段序列组[i], &self.配置);
|
||||
}
|
||||
} else {
|
||||
if self.配置.分析扩展线段 {
|
||||
let 源序列 = self.扩展线段序列组[i - 1].clone();
|
||||
线段::扩展分析(&源序列, &mut self.扩展线段序列组[i], &self.配置);
|
||||
}
|
||||
}
|
||||
if self.配置.分析线段中枢 {
|
||||
中枢::分析(
|
||||
&self.扩展线段序列组[i],
|
||||
&mut self.扩展中枢序列组[i],
|
||||
true,
|
||||
"",
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 6: 扩展线段(线段级)
|
||||
if self.配置.分析扩展线段 {
|
||||
线段::扩展分析(&self.线段序列, &mut self.扩展线段序列_线段, &self.配置);
|
||||
}
|
||||
if self.配置.分析线段中枢 {
|
||||
中枢::分析(
|
||||
&self.扩展线段序列_线段,
|
||||
&mut self.扩展中枢序列_线段,
|
||||
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.配置.分析线段中枢 {
|
||||
中枢::分析(
|
||||
&self.扩展线段序列_扩展线段,
|
||||
&mut self.扩展中枢序列_扩展线段,
|
||||
true,
|
||||
"",
|
||||
0,
|
||||
);
|
||||
// Step 6: 混合扩展线段分析 — 3 级递归
|
||||
if self.配置.分析扩展线段 || self.配置.分析线段中枢 {
|
||||
for i in 0..self.混合扩展线段分析层次.min(self.线段序列组.len()) {
|
||||
if self.配置.分析扩展线段 {
|
||||
let 源序列 = self.线段序列组[i].clone();
|
||||
线段::扩展分析(&源序列, &mut self.混合扩展线段序列组[i], &self.配置);
|
||||
}
|
||||
if self.配置.分析线段中枢 {
|
||||
中枢::分析(
|
||||
&self.混合扩展线段序列组[i],
|
||||
&mut self.混合扩展中枢序列组[i],
|
||||
true,
|
||||
"",
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,130 +369,130 @@ 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();
|
||||
|
||||
for i in 1..self.缠论K线序列.len() - 1 {
|
||||
let 当前分型 = 分型::new(
|
||||
Some(Arc::clone(&self.缠论K线序列[i - 1])),
|
||||
Arc::clone(&self.缠论K线序列[i]),
|
||||
Some(Arc::clone(&self.缠论K线序列[i + 1])),
|
||||
);
|
||||
笔::分析(
|
||||
Arc::new(当前分型),
|
||||
&mut self.分型序列,
|
||||
&mut self.笔序列,
|
||||
&self.缠论K线序列,
|
||||
&self.普通K线序列,
|
||||
0,
|
||||
&self.配置,
|
||||
);
|
||||
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());
|
||||
}
|
||||
|
||||
if self.配置.分析笔 {
|
||||
for i in 1..self.缠论K线序列.len() - 1 {
|
||||
let 当前分型 = 分型::new(
|
||||
Some(Arc::clone(&self.缠论K线序列[i - 1])),
|
||||
Arc::clone(&self.缠论K线序列[i]),
|
||||
Some(Arc::clone(&self.缠论K线序列[i + 1])),
|
||||
);
|
||||
笔::分析(
|
||||
Arc::new(当前分型),
|
||||
&mut self.分型序列,
|
||||
&mut self.笔序列,
|
||||
&self.缠论K线序列,
|
||||
&self.普通K线序列,
|
||||
0,
|
||||
&self.配置,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if self.笔序列.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.配置.分析笔中枢 {
|
||||
中枢::分析(&self.笔序列, &mut self.笔_中枢序列, true, "", 0);
|
||||
}
|
||||
|
||||
if self.配置.分析线段 {
|
||||
线段::分析(
|
||||
&self.笔序列,
|
||||
&mut self.线段序列,
|
||||
&self.配置,
|
||||
0,
|
||||
&[相对方向::向上, 相对方向::向下],
|
||||
);
|
||||
}
|
||||
if self.配置.分析线段中枢 {
|
||||
中枢::分析(&self.线段序列, &mut self.中枢序列, true, "", 0);
|
||||
if self.配置.分析线段 || self.配置.分析线段中枢 {
|
||||
for i in 0..self.线段分析层次 {
|
||||
if i == 0 {
|
||||
if self.配置.分析线段 {
|
||||
线段::分析(
|
||||
&self.笔序列,
|
||||
&mut self.线段序列组[i],
|
||||
&self.配置,
|
||||
0,
|
||||
&[相对方向::向上, 相对方向::向下],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if self.配置.分析线段 {
|
||||
let 源序列 = self.线段序列组[i - 1].clone();
|
||||
线段::分析(
|
||||
&源序列,
|
||||
&mut self.线段序列组[i],
|
||||
&self.配置,
|
||||
0,
|
||||
&[相对方向::向上, 相对方向::向下],
|
||||
);
|
||||
}
|
||||
}
|
||||
if self.配置.分析线段中枢 {
|
||||
中枢::分析(&self.线段序列组[i], &mut self.中枢序列组[i], true, "", 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.配置.分析扩展线段 {
|
||||
线段::扩展分析(&self.笔序列, &mut self.扩展线段序列, &self.配置);
|
||||
}
|
||||
if self.配置.分析线段中枢 {
|
||||
中枢::分析(&self.扩展线段序列, &mut self.扩展中枢序列, true, "", 0);
|
||||
if self.配置.分析扩展线段 || self.配置.分析线段中枢 {
|
||||
for i in 0..self.扩展线段分析层次 {
|
||||
if i == 0 {
|
||||
if self.配置.分析扩展线段 {
|
||||
线段::扩展分析(&self.笔序列, &mut self.扩展线段序列组[i], &self.配置);
|
||||
}
|
||||
} else {
|
||||
if self.配置.分析扩展线段 {
|
||||
let 源序列 = self.扩展线段序列组[i - 1].clone();
|
||||
线段::扩展分析(&源序列, &mut self.扩展线段序列组[i], &self.配置);
|
||||
}
|
||||
}
|
||||
if self.配置.分析线段中枢 {
|
||||
中枢::分析(
|
||||
&self.扩展线段序列组[i],
|
||||
&mut self.扩展中枢序列组[i],
|
||||
true,
|
||||
"",
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.配置.分析扩展线段 {
|
||||
线段::扩展分析(&self.线段序列, &mut self.扩展线段序列_线段, &self.配置);
|
||||
}
|
||||
if self.配置.分析线段中枢 {
|
||||
中枢::分析(
|
||||
&self.扩展线段序列_线段,
|
||||
&mut self.扩展中枢序列_线段,
|
||||
true,
|
||||
"",
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
if self.配置.分析线段 {
|
||||
线段::分析(
|
||||
&self.线段序列,
|
||||
&mut self.线段_线段序列,
|
||||
&self.配置,
|
||||
0,
|
||||
&[相对方向::向上, 相对方向::向下],
|
||||
);
|
||||
}
|
||||
if self.配置.分析线段中枢 {
|
||||
中枢::分析(&self.线段_线段序列, &mut self.线段_中枢序列, true, "", 0);
|
||||
if self.配置.分析扩展线段 || self.配置.分析线段中枢 {
|
||||
for i in 0..self.混合扩展线段分析层次.min(self.线段序列组.len()) {
|
||||
if self.配置.分析扩展线段 {
|
||||
let 源序列 = self.线段序列组[i].clone();
|
||||
线段::扩展分析(&源序列, &mut self.混合扩展线段序列组[i], &self.配置);
|
||||
}
|
||||
if self.配置.分析线段中枢 {
|
||||
中枢::分析(
|
||||
&self.混合扩展线段序列组[i],
|
||||
&mut self.混合扩展中枢序列组[i],
|
||||
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 +510,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 +571,11 @@ impl 观察者 {
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let _ = std::fs::write(
|
||||
保存路径.join("缠K序列_数据文本.txt"),
|
||||
缠K序列_数据文本.join("\n") + "\n",
|
||||
);
|
||||
|
||||
let 分型序列_数据文本: Vec<String> = self
|
||||
.分型序列
|
||||
.iter()
|
||||
@@ -454,37 +592,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()
|
||||
@@ -526,6 +637,97 @@ impl 观察者 {
|
||||
self.配置 = 配置;
|
||||
self.加载本地数据(文件路径)
|
||||
}
|
||||
|
||||
/// 相等 — 全量序列逐项比对,双端一致性验证,对应 Python `观察者相等`
|
||||
pub fn 相等(&self, other: &Self, 浮点容差: f64) -> (bool, String) {
|
||||
let 标签 = format!("观察者校验[A={},B={}]", self.标识(), other.标识());
|
||||
|
||||
if self.缠论K线序列.len() != other.缠论K线序列.len() {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"{标签}: 缠K序列长度不一致 A={},B={}",
|
||||
self.缠论K线序列.len(),
|
||||
other.缠论K线序列.len()
|
||||
),
|
||||
);
|
||||
}
|
||||
if self.分型序列.len() != other.分型序列.len() {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"{标签}: 分型序列长度不一致 A={},B={}",
|
||||
self.分型序列.len(),
|
||||
other.分型序列.len()
|
||||
),
|
||||
);
|
||||
}
|
||||
if self.笔序列.len() != other.笔序列.len() {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"{标签}: 笔序列长度不一致 A={},B={}",
|
||||
self.笔序列.len(),
|
||||
other.笔序列.len()
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
for (i, (a, b)) in self.笔序列.iter().zip(other.笔序列.iter()).enumerate() {
|
||||
let (eq, msg) = a.相等(b, 浮点容差);
|
||||
if !eq {
|
||||
return (false, format!("{标签}: 笔#{i}不一致 >> {msg}"));
|
||||
}
|
||||
}
|
||||
|
||||
if self.笔_中枢序列.len() != other.笔_中枢序列.len() {
|
||||
return (false, format!("{标签}: 笔中枢序列长度不一致"));
|
||||
}
|
||||
for (i, (a, b)) in self
|
||||
.笔_中枢序列
|
||||
.iter()
|
||||
.zip(other.笔_中枢序列.iter())
|
||||
.enumerate()
|
||||
{
|
||||
let (eq, msg) = a.相等(b, 浮点容差);
|
||||
if !eq {
|
||||
return (false, format!("{标签}: 笔中枢#{i}不一致 >> {msg}"));
|
||||
}
|
||||
}
|
||||
|
||||
for level in 0..self.线段分析层次.min(other.线段分析层次) {
|
||||
let a_segs = &self.线段序列组[level];
|
||||
let b_segs = &other.线段序列组[level];
|
||||
if a_segs.len() != b_segs.len() {
|
||||
return (false, format!("{标签}: 线段序列组[{level}]长度不一致"));
|
||||
}
|
||||
for (i, (a, b)) in a_segs.iter().zip(b_segs.iter()).enumerate() {
|
||||
let (eq, msg) = a.相等(b, 浮点容差);
|
||||
if !eq {
|
||||
return (
|
||||
false,
|
||||
format!("{标签}: 线段序列组[{level}]#{i}不一致 >> {msg}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
let a_hubs = &self.中枢序列组[level];
|
||||
let b_hubs = &other.中枢序列组[level];
|
||||
if a_hubs.len() != b_hubs.len() {
|
||||
return (false, format!("{标签}: 中枢序列组[{level}]长度不一致"));
|
||||
}
|
||||
for (i, (a, b)) in a_hubs.iter().zip(b_hubs.iter()).enumerate() {
|
||||
let (eq, msg) = a.相等(b, 浮点容差);
|
||||
if !eq {
|
||||
return (
|
||||
false,
|
||||
format!("{标签}: 中枢序列组[{level}]#{i}不一致 >> {msg}"),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(true, format!("{标签}:全量序列校验全部一致"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -672,7 +874,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 +908,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 +940,8 @@ mod tests {
|
||||
let obs = obs_ref.read().unwrap();
|
||||
(
|
||||
obs.笔序列.len(),
|
||||
obs.线段序列.len(),
|
||||
obs.中枢序列.len(),
|
||||
obs.线段序列().len(),
|
||||
obs.中枢序列().len(),
|
||||
obs.笔_中枢序列.len(),
|
||||
)
|
||||
};
|
||||
@@ -778,12 +980,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 +996,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 +1033,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();
|
||||
}
|
||||
@@ -1080,4 +1282,74 @@ mod tests {
|
||||
assert_eq!(符号, "ethusd");
|
||||
assert_eq!(周期, 7200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_处理数据_线段分析层次为零_不崩溃() {
|
||||
let mut config = 缠论配置::default();
|
||||
config.加载文件路径 = test_data_path();
|
||||
let obs = 观察者::new("btcusd".into(), 300, config);
|
||||
let mut obs_w = obs.write().unwrap();
|
||||
obs_w.线段分析层次 = 0;
|
||||
obs_w.重置基础序列();
|
||||
drop(obs_w);
|
||||
|
||||
// 逐根投喂K线,不应因 线段分析层次=0 而 panic
|
||||
let data = std::fs::read(test_data_path()).unwrap();
|
||||
let size = 48;
|
||||
for i in 0..(data.len() / size).min(500) {
|
||||
let offset = i * size;
|
||||
if let Some(k线) = K线::from_bytes(&data[offset..offset + size], 300, "btcusd") {
|
||||
obs.write().unwrap().增加原始K线(k线);
|
||||
}
|
||||
}
|
||||
|
||||
let obs_r = obs.read().unwrap();
|
||||
assert!(obs_r.缠论K线序列.len() > 0, "缠K序列应有数据");
|
||||
assert!(obs_r.分型序列.len() > 0, "分型序列应有数据");
|
||||
assert!(obs_r.线段序列组.is_empty(), "线段序列组应为空");
|
||||
// 混合扩展线段序列组 有 3 个空 Vec(因为 混合扩展线段分析层次 仍是 3),
|
||||
// 但所有条目应为空(min(3, 0) = 0,循环未执行)
|
||||
assert!(
|
||||
obs_r.混合扩展线段序列组.iter().all(|s| s.is_empty()),
|
||||
"混合扩展线段序列组所有条目应为空"
|
||||
);
|
||||
info!(
|
||||
"线段分析层次=0 处理数据 OK: {} 缠K, {} 分型, {} 笔",
|
||||
obs_r.缠论K线序列.len(),
|
||||
obs_r.分型序列.len(),
|
||||
obs_r.笔序列.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_静态重新分析_线段分析层次为零_不崩溃() {
|
||||
let mut config = 缠论配置::default();
|
||||
config.加载文件路径 = test_data_path();
|
||||
let obs = 观察者::new("btcusd".into(), 300, config);
|
||||
|
||||
// 先正常投喂数据
|
||||
obs.write()
|
||||
.unwrap()
|
||||
.读取数据文件(&test_data_path(), Default::default())
|
||||
.unwrap();
|
||||
|
||||
// 设为0后执行静态重新分析,不应 panic
|
||||
let mut obs_w = obs.write().unwrap();
|
||||
obs_w.线段分析层次 = 0;
|
||||
obs_w.静态重新分析();
|
||||
drop(obs_w);
|
||||
|
||||
let obs_r = obs.read().unwrap();
|
||||
assert!(obs_r.分型序列.len() > 0, "静态重新分析后分型序列应有数据");
|
||||
assert!(obs_r.线段序列组.is_empty(), "线段序列组应为空");
|
||||
assert!(
|
||||
obs_r.混合扩展线段序列组.iter().all(|s| s.is_empty()),
|
||||
"混合扩展线段序列组所有条目应为空"
|
||||
);
|
||||
info!(
|
||||
"线段分析层次=0 静态重新分析 OK: {} 分型, {} 笔",
|
||||
obs_r.分型序列.len(),
|
||||
obs_r.笔序列.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@
|
||||
|
||||
use crate::kline::bar::K线;
|
||||
use std::collections::HashMap;
|
||||
use tracing;
|
||||
|
||||
/// 事件回调类型 — fn(信号类型, 标识, 周期, 完成K线)
|
||||
type 合成器事件回调 = Box<dyn Fn(String, String, i64, K线) + Send + Sync>;
|
||||
|
||||
/// K线合成器 — 将小周期K线合成为大周期K线
|
||||
pub struct K线合成器 {
|
||||
@@ -31,11 +35,17 @@ pub struct K线合成器 {
|
||||
pub 周期组: Vec<i64>,
|
||||
pub 当前K线: HashMap<i64, Option<K线>>,
|
||||
pub 合成K线列表: HashMap<i64, Vec<K线>>,
|
||||
/// 事件回调 — K线完成时触发,对应 Python K线合成器.事件回调
|
||||
/// 签名: fn(信号类型: str, 标识: str, 周期: i64, 完成K线: K线)
|
||||
/// 在 _完成K线 清空当前K线后、新K线创建前触发
|
||||
事件回调: Option<合成器事件回调>,
|
||||
}
|
||||
|
||||
impl K线合成器 {
|
||||
/// 创建K线合成器,按周期升序排列,初始化当前K线和合成K线列表
|
||||
pub fn new(标识: String, 周期组: Vec<i64>) -> Self {
|
||||
/// 创建K线合成器 — 对应 Python K线合成器.__init__(标识, 周期组, 事件回调=None)
|
||||
pub fn new(
|
||||
标识: String, 周期组: Vec<i64>, 事件回调: Option<合成器事件回调>
|
||||
) -> Self {
|
||||
let mut 周期组 = 周期组;
|
||||
周期组.sort();
|
||||
|
||||
@@ -51,37 +61,30 @@ impl K线合成器 {
|
||||
周期组,
|
||||
当前K线,
|
||||
合成K线列表,
|
||||
事件回调,
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置事件回调 — 对应 Python `设置事件回调`
|
||||
pub fn 设置事件回调(&mut self, 回调: 合成器事件回调) {
|
||||
self.事件回调 = Some(回调);
|
||||
}
|
||||
|
||||
/// 投喂 — 便捷入口,直接从 OHLCV 创建 K线 并投喂
|
||||
pub fn 投喂(
|
||||
&mut self,
|
||||
时间戳: i64,
|
||||
开: f64,
|
||||
高: f64,
|
||||
低: f64,
|
||||
收: f64,
|
||||
量: f64,
|
||||
) -> Vec<(i64, K线)> {
|
||||
pub fn 投喂(&mut self, 时间戳: i64, 开: f64, 高: f64, 低: f64, 收: f64, 量: f64) {
|
||||
let 普K = K线::创建普K(&self.标识, 时间戳, 开, 高, 低, 收, 量, 0, 0);
|
||||
self.投喂K线(普K)
|
||||
self.投喂K线(普K);
|
||||
}
|
||||
|
||||
/// 投喂K线 — 输入最小周期K线,合成为所有目标周期
|
||||
/// 返回本次投喂完成了哪些周期的K线(周期 → 完成K线)
|
||||
pub fn 投喂K线(&mut self, 普K: K线) -> Vec<(i64, K线)> {
|
||||
let mut 完成记录 = Vec::new();
|
||||
pub fn 投喂K线(&mut self, 普K: K线) {
|
||||
let 周期组 = self.周期组.clone();
|
||||
for 周期 in 周期组 {
|
||||
if let Some(完成K线) = self._处理单个周期(周期, &普K) {
|
||||
完成记录.push((周期, 完成K线));
|
||||
}
|
||||
self._处理单个周期(周期, &普K);
|
||||
}
|
||||
完成记录
|
||||
}
|
||||
|
||||
fn _处理单个周期(&mut self, 周期: i64, 普K: &K线) -> Option<K线> {
|
||||
fn _处理单个周期(&mut self, 周期: i64, 普K: &K线) {
|
||||
let 目标时间戳 = self._对齐时间戳(普K.时间戳, 周期);
|
||||
let 相同时间 = self.当前K线[&周期]
|
||||
.as_ref()
|
||||
@@ -91,19 +94,17 @@ impl K线合成器 {
|
||||
if self.当前K线[&周期].is_none() {
|
||||
let 新K线 = self._创建新K线(周期, 目标时间戳, 普K);
|
||||
self.当前K线.insert(周期, Some(新K线));
|
||||
None
|
||||
} else if 相同时间 {
|
||||
let ent = self.当前K线.get_mut(&周期).unwrap();
|
||||
Self::_更新K线(ent.as_mut().unwrap(), 普K);
|
||||
None
|
||||
} else {
|
||||
let 完成K线 = self._完成K线(周期);
|
||||
self._完成K线(周期);
|
||||
let 新K线 = self._创建新K线(周期, 目标时间戳, 普K);
|
||||
self.当前K线.insert(周期, Some(新K线));
|
||||
完成K线
|
||||
}
|
||||
}
|
||||
|
||||
/// 对齐时间戳到周期边界 — 对应 Python `_对齐时间戳`
|
||||
fn _对齐时间戳(&self, 时间戳: i64, 周期: i64) -> i64 {
|
||||
if 周期 == 0 {
|
||||
panic!("_对齐时间戳: 周期不能为0");
|
||||
@@ -111,6 +112,7 @@ impl K线合成器 {
|
||||
(时间戳 / 周期) * 周期
|
||||
}
|
||||
|
||||
/// 创建新K线 — 对应 Python `_创建新K线`
|
||||
fn _创建新K线(&self, 周期: i64, 时间戳: i64, 普K: &K线) -> K线 {
|
||||
let 序号 = self
|
||||
.合成K线列表
|
||||
@@ -132,6 +134,7 @@ impl K线合成器 {
|
||||
)
|
||||
}
|
||||
|
||||
/// 更新K线 — 对应 Python `_更新K线`
|
||||
fn _更新K线(当前K线: &mut K线, 新数据: &K线) {
|
||||
当前K线.高 = 当前K线.高.max(新数据.高);
|
||||
当前K线.低 = 当前K线.低.min(新数据.低);
|
||||
@@ -139,9 +142,14 @@ impl K线合成器 {
|
||||
当前K线.成交量 += 新数据.成交量;
|
||||
}
|
||||
|
||||
fn _完成K线(&mut self, 周期: i64) -> Option<K线> {
|
||||
/// 完成K线 — 对应 Python `_完成K线`
|
||||
/// 清空当前K线后,触发事件回调(此时获取当前K线返回 None)
|
||||
fn _完成K线(&mut self, 周期: i64) {
|
||||
let ent = self.当前K线.get_mut(&周期).unwrap();
|
||||
let mut k线 = ent.take()?;
|
||||
let mut k线 = match ent.take() {
|
||||
Some(k) => k,
|
||||
None => return,
|
||||
};
|
||||
k线.序号 = self
|
||||
.合成K线列表
|
||||
.get(&周期)
|
||||
@@ -151,11 +159,142 @@ impl K线合成器 {
|
||||
|
||||
let 完成K线 = k线.clone();
|
||||
self.合成K线列表.get_mut(&周期).unwrap().push(k线);
|
||||
Some(完成K线)
|
||||
|
||||
// 对应 Python _完成K线:清空当前K线后、新K线创建前触发回调
|
||||
self._产生完成K线信号(周期, 完成K线);
|
||||
}
|
||||
|
||||
/// 获取指定周期当前正在合成的K线
|
||||
/// 产生完成K线信号 — 对应 Python `_产生完成K线信号`
|
||||
/// 异常安全:若回调 panic,捕获并记录错误,不中断管线
|
||||
fn _产生完成K线信号(&self, 周期: i64, 完成K线: K线) {
|
||||
if let Some(ref cb) = self.事件回调 {
|
||||
let 标识 = self.标识.clone();
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
cb("K线完成".into(), 标识, 周期, 完成K线);
|
||||
}));
|
||||
if let Err(e) = result {
|
||||
let msg = e
|
||||
.downcast_ref::<&str>()
|
||||
.map(|s| s.to_string())
|
||||
.or_else(|| e.downcast_ref::<String>().cloned())
|
||||
.unwrap_or_else(|| "未知错误".into());
|
||||
tracing::error!("K线合成器 事件回调 异常: {}", msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取指定周期当前正在合成的K线 — 对应 Python `获取当前K线`
|
||||
pub fn 获取当前K线(&self, 周期: i64) -> Option<&K线> {
|
||||
self.当前K线.get(&周期).and_then(|k| k.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_创建合成器_初始状态正确() {
|
||||
let synth = K线合成器::new("btcusd".into(), vec![60, 300], None);
|
||||
assert_eq!(synth.标识, "btcusd");
|
||||
assert_eq!(synth.周期组, vec![60, 300]);
|
||||
assert!(synth.事件回调.is_none());
|
||||
assert!(synth.当前K线[&60].is_none());
|
||||
assert!(synth.当前K线[&300].is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_设置事件回调() {
|
||||
let mut synth = K线合成器::new("btcusd".into(), vec![60], None);
|
||||
assert!(synth.事件回调.is_none());
|
||||
synth.设置事件回调(Box::new(|_, _, _, _| {}));
|
||||
assert!(synth.事件回调.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_对齐时间戳() {
|
||||
let synth = K线合成器::new("t".into(), vec![300], None);
|
||||
assert_eq!(synth._对齐时间戳(1218124800, 300), 1218124800);
|
||||
assert_eq!(synth._对齐时间戳(1218124801, 300), 1218124800);
|
||||
assert_eq!(synth._对齐时间戳(1218125099, 300), 1218124800);
|
||||
assert_eq!(synth._对齐时间戳(1218125100, 300), 1218125100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_创建新K线_序号递进() {
|
||||
let mut synth = K线合成器::new("btcusd".into(), vec![300], None);
|
||||
{
|
||||
let first = K线::创建普K("btcusd", 0, 100.0, 110.0, 90.0, 105.0, 1000.0, 0, 300);
|
||||
synth.合成K线列表.get_mut(&300).unwrap().push(first);
|
||||
}
|
||||
let new_bar = K线::创建普K("btcusd", 100, 200.0, 210.0, 190.0, 205.0, 500.0, 0, 60);
|
||||
let created = synth._创建新K线(300, 300, &new_bar);
|
||||
assert_eq!(created.序号, 1);
|
||||
assert_eq!(created.时间戳, 300);
|
||||
assert_eq!(created.开盘价, 200.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_更新K线_高低更新() {
|
||||
let mut current = K线::创建普K("t", 0, 100.0, 110.0, 90.0, 105.0, 100.0, 0, 300);
|
||||
let new_data = K线::创建普K("t", 0, 102.0, 115.0, 85.0, 108.0, 50.0, 0, 60);
|
||||
K线合成器::_更新K线(&mut current, &new_data);
|
||||
assert_eq!(current.高, 115.0);
|
||||
assert_eq!(current.低, 85.0);
|
||||
assert_eq!(current.收盘价, 108.0);
|
||||
assert_eq!(current.成交量, 150.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_完成K线_返回完成K并将当前置空() {
|
||||
let mut synth = K线合成器::new("btcusd".into(), vec![300], None);
|
||||
let bar = K线::创建普K("btcusd", 300, 100.0, 110.0, 90.0, 105.0, 1000.0, 0, 300);
|
||||
synth.当前K线.insert(300, Some(bar));
|
||||
synth._完成K线(300);
|
||||
assert!(synth.当前K线[&300].is_none());
|
||||
assert_eq!(synth.合成K线列表[&300].len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_完成K线_事件回调触发() {
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
let callback_fired = Arc::new(AtomicBool::new(false));
|
||||
let cb_flag = Arc::clone(&callback_fired);
|
||||
|
||||
let mut synth = K线合成器::new(
|
||||
"btcusd".into(),
|
||||
vec![300],
|
||||
Some(Box::new(move |信号类型, 标识, 周期, _完成K线| {
|
||||
assert_eq!(信号类型, "K线完成");
|
||||
assert_eq!(标识, "btcusd");
|
||||
assert_eq!(周期, 300);
|
||||
cb_flag.store(true, Ordering::SeqCst);
|
||||
})),
|
||||
);
|
||||
|
||||
let bar1 = K线::创建普K("btcusd", 0, 100.0, 110.0, 90.0, 105.0, 1000.0, 0, 300);
|
||||
synth.当前K线.insert(300, Some(bar1));
|
||||
let bar2 = K线::创建普K("btcusd", 400, 200.0, 210.0, 190.0, 205.0, 500.0, 0, 60);
|
||||
synth.投喂K线(bar2);
|
||||
assert!(callback_fired.load(Ordering::SeqCst));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_投喂K线_多周期合成() {
|
||||
let mut synth = K线合成器::new("btcusd".into(), vec![60, 300], None);
|
||||
synth.投喂K线(K线::创建普K(
|
||||
"btcusd", 60, 100.0, 110.0, 90.0, 105.0, 100.0, 0, 60,
|
||||
));
|
||||
assert!(synth.获取当前K线(60).is_some());
|
||||
assert!(synth.获取当前K线(300).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_投喂_便捷方法() {
|
||||
let mut synth = K线合成器::new("btcusd".into(), vec![300], None);
|
||||
synth.投喂(1218124800, 100.0, 110.0, 90.0, 105.0, 1000.0);
|
||||
assert!(synth.获取当前K线(300).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
+308
-51
@@ -23,12 +23,9 @@
|
||||
*/
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use tracing::warn;
|
||||
|
||||
fn is_infinite_f64(v: &f64) -> bool {
|
||||
v.is_infinite()
|
||||
}
|
||||
|
||||
/// 缠论配置 —— 控制所有分析阶段的行为
|
||||
///
|
||||
/// 50+ 参数集中控制缠K合并、笔/线段划分、中枢识别、买卖点生成等所有阶段。
|
||||
@@ -211,29 +208,6 @@ pub struct 缠论配置 {
|
||||
pub 买卖点_指标匹配_KDJ: bool,
|
||||
/// 买卖点指标匹配 RSI
|
||||
pub 买卖点_指标匹配_RSI: bool,
|
||||
/// 买卖点背离率阈值(Infinity 表示不使用)
|
||||
#[serde(skip_serializing_if = "is_infinite_f64")]
|
||||
pub 买卖点_背离率: f64,
|
||||
/// 买卖点 T2 回调阈值
|
||||
pub 买卖点_T2_回调阈值: f64,
|
||||
/// 买卖点 T2S 最大层级
|
||||
pub 买卖点_T2S_最大层级: i64,
|
||||
/// 买卖点峰值条件
|
||||
pub 买卖点_峰值条件: bool,
|
||||
/// 买卖点计算方式(峰/谷等)
|
||||
pub 买卖点_计算方式: String,
|
||||
/// 是否计算线段BSP1
|
||||
pub 买卖点_计算线段BSP1: bool,
|
||||
/// 是否处理BSP2
|
||||
pub 买卖点_处理BSP2: bool,
|
||||
/// 是否计算线段BSP3
|
||||
pub 买卖点_计算线段BSP3: bool,
|
||||
/// 是否依赖T1买卖点
|
||||
pub 买卖点_依赖T1: bool,
|
||||
/// 买卖点中枢来源(实/虚/合)
|
||||
pub 买卖点_中枢来源: String,
|
||||
/// 买卖点调试输出
|
||||
pub 买卖点_调试输出: bool,
|
||||
|
||||
// ---- 背驰 ----
|
||||
/// 线段内部背驰使用 MACD
|
||||
@@ -382,17 +356,6 @@ impl Default for 缠论配置 {
|
||||
买卖点_指标匹配_MACD: true,
|
||||
买卖点_指标匹配_KDJ: true,
|
||||
买卖点_指标匹配_RSI: true,
|
||||
买卖点_背离率: f64::INFINITY,
|
||||
买卖点_T2_回调阈值: 1.0,
|
||||
买卖点_T2S_最大层级: 3,
|
||||
买卖点_峰值条件: false,
|
||||
买卖点_计算方式: "峰".into(),
|
||||
买卖点_计算线段BSP1: true,
|
||||
买卖点_处理BSP2: true,
|
||||
买卖点_计算线段BSP3: true,
|
||||
买卖点_依赖T1: true,
|
||||
买卖点_中枢来源: "合".into(),
|
||||
买卖点_调试输出: false,
|
||||
线段内部背驰_MACD: true,
|
||||
线段内部背驰_斜率: true,
|
||||
线段内部背驰_测度: true,
|
||||
@@ -445,6 +408,147 @@ impl 缠论配置 {
|
||||
vec![("boll".into(), self.布林带_周期, self.布林带_标准差倍数)]
|
||||
}
|
||||
|
||||
/// 序列化为 JSON 字典(对应 Python to_dict,仅返回 model_fields 中的字段)
|
||||
pub fn to_dict(&self) -> serde_json::Value {
|
||||
let full = serde_json::to_value(self).unwrap_or_default();
|
||||
let valid = Self::model_fields();
|
||||
if let serde_json::Value::Object(map) = full {
|
||||
let filtered: serde_json::Map<_, _> = map
|
||||
.into_iter()
|
||||
.filter(|(k, _)| valid.contains(&k.as_str()))
|
||||
.collect();
|
||||
serde_json::Value::Object(filtered)
|
||||
} else {
|
||||
full
|
||||
}
|
||||
}
|
||||
|
||||
/// 从 JSON 字典反序列化(对应 Python from_dict / 兼容旧版本配置)
|
||||
pub fn from_dict(value: &serde_json::Value) -> Result<Self, serde_json::Error> {
|
||||
if let serde_json::Value::Object(map) = value {
|
||||
let valid_fields = Self::model_fields();
|
||||
let cleaned: serde_json::Map<_, _> = map
|
||||
.iter()
|
||||
.filter(|(k, _)| valid_fields.contains(&k.as_str()))
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect();
|
||||
serde_json::from_value(serde_json::Value::Object(cleaned))
|
||||
} else {
|
||||
serde_json::from_value(value.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// 验证并修正字段值(对应 Python _validate_all_fields)
|
||||
pub fn _validate_all_fields(&mut self) {
|
||||
const 允许: &[&str] = &[
|
||||
"开",
|
||||
"高",
|
||||
"低",
|
||||
"收",
|
||||
"高低均值",
|
||||
"高低收均值",
|
||||
"开高低收均值",
|
||||
];
|
||||
if !允许.contains(&self.指标计算方式.as_str()) {
|
||||
warn!(
|
||||
"[指标计算方式] = {} 值不在允许范围内,使用默认值:收",
|
||||
self.指标计算方式
|
||||
);
|
||||
self.指标计算方式 = "收".into();
|
||||
}
|
||||
}
|
||||
|
||||
/// 返回字段名列表(对应 Python model_fields().keys())
|
||||
pub fn model_fields() -> &'static [&'static str] {
|
||||
&[
|
||||
"标识",
|
||||
"缠K合并替换",
|
||||
"笔内元素数量",
|
||||
"笔内相同终点取舍",
|
||||
"笔内起始分型包含整笔",
|
||||
"笔内起始分型包含整笔_包括右",
|
||||
"笔内原始K线包含整笔",
|
||||
"笔次级成笔",
|
||||
"笔弱化",
|
||||
"笔弱化_原始数量",
|
||||
"线段_非缺口下穿刺",
|
||||
"线段_特征序列忽视老阴老阳",
|
||||
"线段_缺口后紧急修正",
|
||||
"线段_修正",
|
||||
"线段内部中枢图显",
|
||||
"扩展线段_当下分析",
|
||||
"分析笔",
|
||||
"分析线段",
|
||||
"分析扩展线段",
|
||||
"分析笔中枢",
|
||||
"分析线段中枢",
|
||||
"手动终止",
|
||||
"计算指标",
|
||||
"计算BOLL",
|
||||
"指标计算方式",
|
||||
"平滑异同移动平均线_快线周期",
|
||||
"平滑异同移动平均线_慢线周期",
|
||||
"平滑异同移动平均线_信号周期",
|
||||
"相对强弱指数_周期",
|
||||
"相对强弱指数_移动平均线周期",
|
||||
"相对强弱指数_超买阈值",
|
||||
"相对强弱指数_超卖阈值",
|
||||
"随机指标_RSV周期",
|
||||
"随机指标_K值平滑周期",
|
||||
"随机指标_D值平滑周期",
|
||||
"随机指标_超买阈值",
|
||||
"随机指标_超卖阈值",
|
||||
"布林带_周期",
|
||||
"布林带_标准差倍数",
|
||||
"MACD_参数列表",
|
||||
"RSI_周期列表",
|
||||
"KDJ_参数列表",
|
||||
"BOLL_参数列表",
|
||||
"均线_类型列表",
|
||||
"均线_周期列表",
|
||||
"图表展示",
|
||||
"推送K线",
|
||||
"推送笔",
|
||||
"推送线段",
|
||||
"推送中枢",
|
||||
"图表展示_笔",
|
||||
"图表展示_线段",
|
||||
"图表展示_扩展线段",
|
||||
"图表展示_扩展线段_线段",
|
||||
"图表展示_线段_线段",
|
||||
"图表展示_中枢_笔",
|
||||
"图表展示_中枢_线段",
|
||||
"图表展示_中枢_扩展线段",
|
||||
"图表展示_中枢_扩展线段_线段",
|
||||
"图表展示_中枢_线段_线段",
|
||||
"图表展示_中枢_线段内部",
|
||||
"买卖点偏移",
|
||||
"买卖点激进识别",
|
||||
"买卖点与MACD柱强相关",
|
||||
"买卖点错过误差值",
|
||||
"买卖点_指标模式",
|
||||
"买卖点_指标匹配_MACD",
|
||||
"买卖点_指标匹配_KDJ",
|
||||
"买卖点_指标匹配_RSI",
|
||||
"线段内部背驰_MACD",
|
||||
"线段内部背驰_斜率",
|
||||
"线段内部背驰_测度",
|
||||
"线段内部背驰_模式",
|
||||
"加载文件路径",
|
||||
]
|
||||
}
|
||||
|
||||
/// 深拷贝并更新指定字段(对应 Python model_copy(update={...}, deep=True))
|
||||
pub fn model_copy(&self, update: &HashMap<String, serde_json::Value>) -> Self {
|
||||
let mut value = serde_json::to_value(self).unwrap_or_default();
|
||||
if let serde_json::Value::Object(ref mut map) = value {
|
||||
for (k, v) in update {
|
||||
map.insert(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
serde_json::from_value(value).unwrap_or_else(|_| self.clone())
|
||||
}
|
||||
|
||||
/// 序列化为 JSON 字符串
|
||||
pub fn to_json(&self) -> String {
|
||||
serde_json::to_string_pretty(self).unwrap_or_default()
|
||||
@@ -467,9 +571,10 @@ impl 缠论配置 {
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// 返回一个关闭所有推送/显示的新配置
|
||||
/// 返回一个关闭所有推送/显示的新配置(对应 Python 不推送)
|
||||
pub fn 不推送(&self) -> Self {
|
||||
Self {
|
||||
线段内部中枢图显: false,
|
||||
图表展示: false,
|
||||
推送K线: false,
|
||||
推送笔: false,
|
||||
@@ -524,19 +629,21 @@ impl 缠论配置 {
|
||||
result
|
||||
}
|
||||
|
||||
/// 对比两个配置,返回差异字段
|
||||
pub fn 对比(&self, other: &Self) -> Vec<String> {
|
||||
let mut diffs = Vec::new();
|
||||
let self_json = serde_json::to_value(self).unwrap();
|
||||
let other_json = serde_json::to_value(other).unwrap();
|
||||
/// 对比两个配置,返回差异字段及新值(对应 Python 对比 → dict[字段名, 新值])
|
||||
pub fn 对比(&self, other: &Self) -> HashMap<String, serde_json::Value> {
|
||||
let mut diffs = HashMap::new();
|
||||
let self_dict = self.to_dict();
|
||||
let other_dict = other.to_dict();
|
||||
if let (serde_json::Value::Object(self_map), serde_json::Value::Object(other_map)) =
|
||||
(&self_json, &other_json)
|
||||
(&self_dict, &other_dict)
|
||||
{
|
||||
for (key, self_val) in self_map {
|
||||
if let Some(other_val) = other_map.get(key)
|
||||
&& self_val != other_val
|
||||
for key in Self::model_fields() {
|
||||
let self_val = self_map.get(*key);
|
||||
let other_val = other_map.get(*key);
|
||||
if self_val != other_val
|
||||
&& let Some(v) = other_val
|
||||
{
|
||||
diffs.push(key.clone());
|
||||
diffs.insert(key.to_string(), v.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -562,7 +669,6 @@ mod tests {
|
||||
let config = 缠论配置::default();
|
||||
assert_eq!(config.标识, "bar");
|
||||
assert_eq!(config.笔内元素数量, 5);
|
||||
assert!(config.买卖点_背离率.is_infinite());
|
||||
assert_eq!(config.指标计算方式, "收");
|
||||
}
|
||||
|
||||
@@ -609,6 +715,66 @@ mod tests {
|
||||
assert_eq!(config.线段内部背驰_模式, "全量");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_dict_roundtrip() {
|
||||
let config = 缠论配置::default();
|
||||
let dict = config.to_dict();
|
||||
let restored = 缠论配置::from_dict(&dict).unwrap();
|
||||
assert_eq!(config.to_json(), restored.to_json());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_dict_filters_unknown_fields() {
|
||||
// 兼容旧版本配置 — unknown fields are silently dropped
|
||||
let json = serde_json::json!({
|
||||
"标识": "test",
|
||||
"不存在的字段": 42,
|
||||
"另一个废弃字段": "xxx",
|
||||
"笔内元素数量": 8,
|
||||
});
|
||||
let config = 缠论配置::from_dict(&json).unwrap();
|
||||
assert_eq!(config.标识, "test");
|
||||
assert_eq!(config.笔内元素数量, 8);
|
||||
// 未指定字段使用默认值
|
||||
assert_eq!(config.买卖点偏移, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_model_fields_contains_all() {
|
||||
let fields = 缠论配置::model_fields();
|
||||
assert!(fields.contains(&"标识"));
|
||||
assert!(fields.contains(&"笔内元素数量"));
|
||||
assert!(fields.contains(&"买卖点偏移"));
|
||||
assert!(fields.contains(&"线段内部背驰_MACD"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_model_copy() {
|
||||
let mut update = std::collections::HashMap::new();
|
||||
update.insert("标识".into(), serde_json::json!("custom"));
|
||||
update.insert("推送K线".into(), serde_json::json!(false));
|
||||
update.insert("笔内元素数量".into(), serde_json::json!(10));
|
||||
|
||||
let config = 缠论配置::default();
|
||||
let copied = config.model_copy(&update);
|
||||
|
||||
assert_eq!(copied.标识, "custom");
|
||||
assert!(!copied.推送K线);
|
||||
assert_eq!(copied.笔内元素数量, 10);
|
||||
// 未指定字段保持不变
|
||||
assert_eq!(copied.买卖点偏移, 1);
|
||||
assert!(copied.推送笔);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_dict_to_json_consistency() {
|
||||
let config = 缠论配置::default();
|
||||
let dict = config.to_dict();
|
||||
// to_dict → from_dict → to_json should equal original to_json
|
||||
let restored = 缠论配置::from_dict(&dict).unwrap();
|
||||
assert_eq!(config.to_json(), restored.to_json());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_不推送() {
|
||||
let config = 缠论配置::default();
|
||||
@@ -616,7 +782,98 @@ mod tests {
|
||||
assert!(!muted.推送K线);
|
||||
assert!(!muted.推送笔);
|
||||
assert!(!muted.图表展示);
|
||||
// 其他字段不变
|
||||
assert_eq!(muted.笔内元素数量, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_对比_无差异() {
|
||||
let a = 缠论配置::default();
|
||||
let b = 缠论配置::default();
|
||||
let diff = a.对比(&b);
|
||||
assert!(diff.is_empty(), "identical configs should have empty diff");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_对比_有差异() {
|
||||
let mut a = 缠论配置::default();
|
||||
let mut b = 缠论配置::default();
|
||||
b.标识 = "changed".into();
|
||||
b.笔内元素数量 = 99;
|
||||
|
||||
let diff = a.对比(&b);
|
||||
assert_eq!(diff.len(), 2);
|
||||
assert_eq!(diff.get("标识").unwrap().as_str().unwrap(), "changed");
|
||||
assert_eq!(diff.get("笔内元素数量").unwrap().as_i64().unwrap(), 99);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_对比_仅比较model_fields() {
|
||||
// 仅比较 model_fields 中的字段(Python 一致行为)
|
||||
let a = 缠论配置::default();
|
||||
let b = 缠论配置::default();
|
||||
let diff = a.对比(&b);
|
||||
// 验证不包含废弃字段(如已删除的 "买卖点_背离率" 等)
|
||||
assert!(!diff.contains_key("买卖点_背离率"));
|
||||
assert!(diff.is_empty(), "default configs should have no diff");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_dict_excludes_non_model_fields() {
|
||||
let config = 缠论配置::default();
|
||||
let dict = config.to_dict();
|
||||
let valid = 缠论配置::model_fields();
|
||||
if let serde_json::Value::Object(map) = &dict {
|
||||
for key in map.keys() {
|
||||
assert!(
|
||||
valid.contains(&key.as_str()),
|
||||
"{key} should not be in to_dict output"
|
||||
);
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
valid.len(),
|
||||
dict.as_object().map(|m| m.len()).unwrap_or(0),
|
||||
"to_dict should have exactly model_fields count"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_model_copy_then_对比() {
|
||||
let config = 缠论配置::default();
|
||||
let mut update = HashMap::new();
|
||||
update.insert("标识".into(), serde_json::json!("copied"));
|
||||
update.insert("笔内元素数量".into(), serde_json::json!(10));
|
||||
|
||||
let copied = config.model_copy(&update);
|
||||
let diff = config.对比(&copied);
|
||||
|
||||
assert_eq!(diff.len(), 2);
|
||||
assert_eq!(diff["标识"].as_str().unwrap(), "copied");
|
||||
assert_eq!(diff["笔内元素数量"].as_i64().unwrap(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_对比_boolean_difference() {
|
||||
let mut a = 缠论配置::default();
|
||||
let mut b = 缠论配置::default();
|
||||
b.推送K线 = false;
|
||||
b.图表展示 = false;
|
||||
|
||||
let diff = a.对比(&b);
|
||||
assert_eq!(diff.len(), 2);
|
||||
assert_eq!(diff["推送K线"], serde_json::json!(false));
|
||||
assert_eq!(diff["图表展示"], serde_json::json!(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_dict_from_dict_对比_roundtrip() {
|
||||
let config = 缠论配置::default();
|
||||
let dict = config.to_dict();
|
||||
let restored = 缠论配置::from_dict(&dict).unwrap();
|
||||
let diff = config.对比(&restored);
|
||||
assert!(
|
||||
diff.is_empty(),
|
||||
"to_dict→from_dict roundtrip should produce no diff"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(始);
|
||||
|
||||
@@ -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
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 虚线 {
|
||||
|
||||
@@ -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 分型 {
|
||||
|
||||
@@ -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 线段特征 {
|
||||
|
||||
@@ -64,6 +64,29 @@ impl 缺口 {
|
||||
|
||||
Some(Self::new(上界.max(下界), 上界.min(下界)))
|
||||
}
|
||||
|
||||
/// 结构化相等校验 — 浮点容差比较高/低,返回 (是否相等, 差异描述)
|
||||
pub fn 相等(&self, other: &Self, 浮点容差: f64) -> (bool, String) {
|
||||
if (self.高 - other.高).abs() > 浮点容差 {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"缺口: [高] 浮点超限 容差={浮点容差:.2e} A={:.10},B={:.10}",
|
||||
self.高, other.高
|
||||
),
|
||||
);
|
||||
}
|
||||
if (self.低 - other.低).abs() > 浮点容差 {
|
||||
return (
|
||||
false,
|
||||
format!(
|
||||
"缺口: [低] 浮点超限 容差={浮点容差:.2e} A={:.10},B={:.10}",
|
||||
self.低, other.低
|
||||
),
|
||||
);
|
||||
}
|
||||
(true, "缺口: 高低价格一致".into())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for 缺口 {
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env python3
|
||||
"""检测 Rust 源码文件头部是否有 MIT 协议,若无则自动注入。
|
||||
|
||||
用法:
|
||||
python3 check_license.py # 检测 chanlun/ 和 chanlun-py/ 下所有 .rs
|
||||
python3 check_license.py --check-only # 仅检测,不修改
|
||||
python3 check_license.py --fix # 检测并修复
|
||||
python3 check_license.py path/to/dir # 指定目录
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def find_repo_root() -> Path:
|
||||
"""从脚本位置向上查找仓库根目录(含 LICENSE 文件的目录)。"""
|
||||
current = Path(__file__).resolve().parent
|
||||
while current != current.parent:
|
||||
if (current / "LICENSE").exists():
|
||||
return current
|
||||
current = current.parent
|
||||
# Fallback: 脚本所在目录的父目录
|
||||
return Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def build_license_header(license_path: Path) -> str:
|
||||
"""读取 LICENSE 文件并格式化为 Rust 块注释头。"""
|
||||
lines = license_path.read_text(encoding="utf-8").rstrip("\n").split("\n")
|
||||
header_lines = ["/*"]
|
||||
for line in lines:
|
||||
if line.strip():
|
||||
header_lines.append(f" * {line}")
|
||||
else:
|
||||
header_lines.append(" *")
|
||||
header_lines.append(" */")
|
||||
header_lines.append("") # 末尾空行分隔
|
||||
return "\n".join(header_lines) + "\n"
|
||||
|
||||
|
||||
def has_license_header(file_path: Path) -> bool:
|
||||
"""检测文件头部是否已包含块注释风格的 MIT License。"""
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
head = f.read(512)
|
||||
except (OSError, UnicodeDecodeError):
|
||||
return True
|
||||
return head.lstrip().startswith("/*") and "MIT License" in head
|
||||
|
||||
|
||||
def strip_old_license_header(text: str) -> str:
|
||||
"""去除文件中已有的 // 风格 license header(重新注入前调用)。"""
|
||||
stripped = text.lstrip("\n")
|
||||
if stripped.startswith("// MIT License"):
|
||||
# 找到 // 注释块结束位置(第一个非 // 非空行)
|
||||
lines = stripped.split("\n")
|
||||
end_idx = 0
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith("//") or line.strip() == "":
|
||||
end_idx = i + 1
|
||||
else:
|
||||
break
|
||||
return "\n".join(lines[end_idx:])
|
||||
return text
|
||||
|
||||
|
||||
def inject_license(file_path: Path, header: str) -> bool:
|
||||
"""将 license header 注入文件头部。返回 True 表示已修改。"""
|
||||
original = file_path.read_text(encoding="utf-8")
|
||||
# 已有块注释风格则跳过
|
||||
if original.lstrip().startswith("/*") and "MIT License" in original[:512]:
|
||||
return False
|
||||
# 去除旧的 // 风格 header(如果存在)
|
||||
cleaned = strip_old_license_header(original)
|
||||
file_path.write_text(header + cleaned, encoding="utf-8")
|
||||
return True
|
||||
|
||||
|
||||
def collect_rs_files(roots: list[Path]) -> list[Path]:
|
||||
"""递归收集所有 .rs 文件,排除 target/ 等构建产物目录。"""
|
||||
exclude_dirs = {"target", ".git", "__pycache__", "dist", "build", ".venv", "venv"}
|
||||
files = []
|
||||
for root in roots:
|
||||
if not root.is_dir():
|
||||
continue
|
||||
for path in root.rglob("*.rs"):
|
||||
if any(excl in path.parts for excl in exclude_dirs):
|
||||
continue
|
||||
files.append(path)
|
||||
return sorted(files)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="检测 Rust 源码 MIT 协议头")
|
||||
parser.add_argument(
|
||||
"paths",
|
||||
nargs="*",
|
||||
help="要检测的目录(默认: chanlun 和 chanlun-py 源码目录)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--check-only",
|
||||
action="store_true",
|
||||
help="仅检测,不修改文件",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--fix",
|
||||
action="store_true",
|
||||
help="检测并自动注入缺失的协议头(默认行为)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
repo_root = find_repo_root()
|
||||
license_path = repo_root / "LICENSE"
|
||||
|
||||
if not license_path.exists():
|
||||
print(f"错误: 未找到 LICENSE 文件 ({license_path})", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
header = build_license_header(license_path)
|
||||
|
||||
# 确定扫描目录
|
||||
if args.paths:
|
||||
roots = [Path(p).resolve() for p in args.paths]
|
||||
else:
|
||||
roots = [
|
||||
repo_root / "chanlun" / "src",
|
||||
repo_root / "chanlun-py" / "src",
|
||||
repo_root / "chanlun" / "tests",
|
||||
repo_root / "chanlun-py" / "tests",
|
||||
]
|
||||
roots = [r for r in roots if r.is_dir()]
|
||||
|
||||
if not roots:
|
||||
print("错误: 未找到任何源码目录", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
rs_files = collect_rs_files(roots)
|
||||
|
||||
if not rs_files:
|
||||
print("未找到 .rs 文件")
|
||||
return 0
|
||||
|
||||
missing = []
|
||||
injected = []
|
||||
|
||||
for f in rs_files:
|
||||
if has_license_header(f):
|
||||
continue
|
||||
missing.append(f)
|
||||
if not args.check_only:
|
||||
if inject_license(f, header):
|
||||
injected.append(f)
|
||||
|
||||
if args.check_only:
|
||||
if missing:
|
||||
print(f"缺失 MIT 协议头: {len(missing)} 个文件")
|
||||
for f in missing:
|
||||
print(f" {f}")
|
||||
return 1
|
||||
else:
|
||||
print(f"全部 {len(rs_files)} 个 .rs 文件均已包含 MIT 协议头")
|
||||
return 0
|
||||
else:
|
||||
if injected:
|
||||
print(f"已注入 MIT 协议头: {len(injected)} 个文件")
|
||||
for f in injected:
|
||||
print(f" {f}")
|
||||
if missing:
|
||||
already = len(missing) - len(injected)
|
||||
if already > 0:
|
||||
print(f"已有协议头: {already} 个文件(无需修改)")
|
||||
total = len(rs_files) - len(missing)
|
||||
print(f"总计: {len(rs_files)} 个 .rs 文件, {total} 个已含协议头")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -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
@@ -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
@@ -848,6 +848,7 @@ print("=== 趋势分析 ===")
|
||||
this.websocketManager = websocketManager;
|
||||
this.widget = null;
|
||||
this.shapeIds = new Map();
|
||||
this._pendingShapeCreate = new Map(); // id → Promise, 追踪正在创建的图形
|
||||
this.onRealtimeCallback = null;
|
||||
this.initDataFeed();
|
||||
this.websocketManager.on('chart', (message) => {
|
||||
@@ -1052,7 +1053,7 @@ print("=== 趋势分析 ===")
|
||||
}
|
||||
}
|
||||
|
||||
handleChartMessage(message) {
|
||||
async handleChartMessage(message) {
|
||||
Utils.log("处理图表消息:", message.type);
|
||||
switch (message.type) {
|
||||
case 'realtime':
|
||||
@@ -1070,11 +1071,11 @@ print("=== 趋势分析 ===")
|
||||
break;
|
||||
case 'shape':
|
||||
if (message.cmd === "APPEND") {
|
||||
this.addShape(message.id, message.points, message.name, message.overrides);
|
||||
await this.addShape(message.id, message.points, message.name, message.overrides);
|
||||
} else if (message.cmd === "REMOVE") {
|
||||
this.removeShape(message.id);
|
||||
await this.removeShape(message.id);
|
||||
} else if (message.cmd === "MODIFY") {
|
||||
this.updateShape(message.id, message.points, message.overrides);
|
||||
await this.updateShape(message.id, message.points, message.overrides);
|
||||
}
|
||||
break;
|
||||
case 'query_result':
|
||||
@@ -1104,6 +1105,12 @@ print("=== 趋势分析 ===")
|
||||
return;
|
||||
}
|
||||
|
||||
// 等待之前相同 ID 的创建完成(避免竞态)
|
||||
const prevPending = this._pendingShapeCreate.get(id);
|
||||
if (prevPending) {
|
||||
await prevPending;
|
||||
}
|
||||
|
||||
// 重复判断
|
||||
const existShapeId = this.shapeIds.get(id);
|
||||
if (existShapeId) {
|
||||
@@ -1112,26 +1119,40 @@ print("=== 趋势分析 ===")
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建图形
|
||||
const shapeId = await this.widget.chart().createMultipointShape(points, options);
|
||||
const shape = this.widget.chart().getShapeById(shapeId);
|
||||
// 记录 pending 状态,供 removeShape/updateShape 等待
|
||||
const createPromise = (async () => {
|
||||
// 创建图形
|
||||
const shapeId = await this.widget.chart().createMultipointShape(points, options);
|
||||
const shape = this.widget.chart().getShapeById(shapeId);
|
||||
|
||||
if (!shape) {
|
||||
Utils.error("添加形状失败:shape 为 null", {id, shapeId});
|
||||
return;
|
||||
if (!shape) {
|
||||
Utils.error("添加形状失败:shape 为 null", {id, shapeId});
|
||||
return null;
|
||||
}
|
||||
|
||||
// 设置属性
|
||||
//Utils.log("shapeId:", shape.getProperties());
|
||||
//shape.setProperties(properties);
|
||||
shape.bringToFront();
|
||||
|
||||
// 保存映射
|
||||
this.shapeIds.set(id, shapeId);
|
||||
Utils.log("添加形状", {id, shapeId});
|
||||
|
||||
return shapeId;
|
||||
})();
|
||||
|
||||
this._pendingShapeCreate.set(id, createPromise);
|
||||
|
||||
const shapeId = await createPromise;
|
||||
if (shapeId === null) {
|
||||
// shape creation failed
|
||||
}
|
||||
|
||||
// 设置属性
|
||||
//Utils.log("shapeId:", shape.getProperties());
|
||||
//shape.setProperties(properties);
|
||||
shape.bringToFront();
|
||||
|
||||
// 保存映射
|
||||
this.shapeIds.set(id, shapeId);
|
||||
Utils.log("添加形状", {id, shapeId});
|
||||
|
||||
} catch (error) {
|
||||
Utils.error("添加形状失败", error);
|
||||
} finally {
|
||||
this._pendingShapeCreate.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1139,6 +1160,12 @@ print("=== 趋势分析 ===")
|
||||
try {
|
||||
if (!this.widget || !this.widget.chart) return;
|
||||
|
||||
// 等待该 ID 的 addShape 完成(避免竞态)
|
||||
const pending = this._pendingShapeCreate.get(id);
|
||||
if (pending) {
|
||||
await pending;
|
||||
}
|
||||
|
||||
const shapeId = this.shapeIds.get(id);
|
||||
if (!shapeId) {
|
||||
Utils.log("图形不存在,无需删除", {id});
|
||||
@@ -1162,6 +1189,12 @@ print("=== 趋势分析 ===")
|
||||
try {
|
||||
if (!this.widget || !this.widget.chart) return;
|
||||
|
||||
// 等待该 ID 的 addShape 完成(避免竞态)
|
||||
const pending = this._pendingShapeCreate.get(id);
|
||||
if (pending) {
|
||||
await pending;
|
||||
}
|
||||
|
||||
const shapeId = this.shapeIds.get(id);
|
||||
if (!shapeId) {
|
||||
Utils.error("更新失败:图形不存在", {id});
|
||||
|
||||
Reference in New Issue
Block a user