From 4bf94610096e28a5cc6c5b151fcf4ca2464e620b Mon Sep 17 00:00:00 2001 From: YuWuKunCheng Date: Sun, 7 Jun 2026 14:21:04 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=84=E8=8C=83=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/ISSUE_TEMPLATE/bug_report.md | 182 ++ .github/ISSUE_TEMPLATE/custom.md | 36 + .github/ISSUE_TEMPLATE/feature_request.md | 62 + .github/pull_request_template.md | 70 + CODE_OF_CONDUCT.md | 94 + CONTRIBUTING.md | 267 ++ SECURITY.md | 60 + chan.py | 2960 +++++++++++++++------ 8 files changed, 2905 insertions(+), 826 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/custom.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/pull_request_template.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 SECURITY.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..801df8e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -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 或代码形式)。这对于复现问题至关重要。 + +
+展开查看配置 JSON + +```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, + "线段内部背驰_模式": "相对", + "加载文件路径": "" +} +``` + +
+ +> **请修改上述 JSON 为您实际使用的配置值**,或直接粘贴通过 `config.to_json()` 输出的 JSON。 + +## 上下文 + +- [ ] 此 Bug 在 `chan.py` (Python 参考实现) 中也存在吗? + - Python 版行为: +- [ ] 此 Bug 是否与特定数据文件相关? + - 数据文件名/时间范围: + +## 日志 / Panic 输出 + +
+展开查看详细输出 + +``` +在此粘贴日志或 panic 输出 +``` + +
+ +## 补充信息 + +任何其他有助于理解此 Bug 的上下文、截图或补充说明。 diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 0000000..a0e69aa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -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` / 其他 | +| 其他 | | + +## 补充信息 + +任何有助于更好理解或解决此问题的信息。 diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..6ba94da --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.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`) + +## 补充信息 + +任何参考链接、图表、伪代码或其他有助于说明该功能的内容。 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..e991a8b --- /dev/null +++ b/.github/pull_request_template.md @@ -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 的截图、日志或对比数据。 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..b26adbe --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -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) 的工作。 + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a854119 --- /dev/null +++ b/CONTRIBUTING.md @@ -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>`,不使用 `thread_local!` +- 读写锁作用域化,防止死锁 + +--- + +## 测试指南 + +### 核心层测试 + +```bash +cd chanlun +cargo test # 运行所有测试 +cargo test -- # 运行匹配名称的测试 +``` + +测试应覆盖: +- 类型构造/字段读写/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 +``` + +类型示例: +- `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 代码对齐 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..86c5688 --- /dev/null +++ b/SECURITY.md @@ -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>` 全局缓存的锁竞争 +- `panic!` 导致的未预期崩溃 +- `Arc` 循环引用造成的内存泄漏 +- 大端字节序列化 (`to_bytes`/`from_bytes`) 的缓冲区越界 +- PyO3 FFI 边界的类型转换安全 +- `AtomicI64` / `SyncF64` 的 `Ordering::Relaxed` 使用是否合理 + +## 偏好语言 + +请使用简体中文或英文撰写报告。 diff --git a/chan.py b/chan.py index 3075d21..17d9736 100644 --- a/chan.py +++ b/chan.py @@ -21,10 +21,12 @@ 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. """ + # -*- coding: utf-8 -*- # @Time : 2024/10/15 16:45 # @Author : YuYuKunKun # @File : chan.py +from __future__ import annotations import json import math @@ -46,14 +48,14 @@ from typing import ( Dict, Any, Union, - Sequence, Callable, Set, ) +from collections.abc import Sequence -from pydantic import BaseModel, Field, model_validator, ValidationError, field_validator -from termcolor import colored +from chanlun.termcolor import colored +from loguru import logger __all__ = [ "K线", @@ -66,6 +68,7 @@ __all__ = [ "基础买卖点", "平滑异同移动平均线", "指标", + "指标容器", "测试_读取数据", "特征分型", "相对强弱指数", @@ -83,8 +86,365 @@ __all__ = [ "转化为时间戳", "转化为时间戳_数字", "随机指标", + "布林带", + "set_log_level", + "get_log_level", + "K线相等", + "缠论K线相等", + "分型相等", + "缺口相等", + "线段特征相等", + "中枢相等", + "虚线相等", ] +# 日志级别映射: 名称 → loguru 级别名 +_级别映射 = { + "trace": "TRACE", + "debug": "DEBUG", + "info": "INFO", + "warn": "WARNING", + "error": "ERROR", + "off": "OFF", +} +_有效级别 = frozenset(_级别映射.keys()) +_当前日志级别 = "info" + + +def set_log_level(level: str): + """设置 Python 侧日志级别。 + + :param level: 日志级别,不区分大小写 (trace / debug / info / warn / error / off) + """ + global _当前日志级别 + _level = level.lower() + if _level not in _有效级别: + raise ValueError(f"无效日志级别 '{level}',有效值: {', '.join(sorted(_有效级别))}") + + _当前日志级别 = _level + _loguru_level = _级别映射[_level] + + try: + logger.remove(0) + except ValueError: + pass + if _loguru_level != "OFF": + logger.add(sys.stderr, level=_loguru_level) + + +def get_log_level() -> str: + """获取 Python 侧当前日志级别。 + + :return: 日志级别字符串 (trace / debug / info / warn / error / off) + """ + return _当前日志级别 + + +@lru_cache(128) +def K线相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]: + """原始K线相等校验:字段完备→浮点容错→普通全等""" + 比对字段 = ["标识", "序号", "周期", "时间戳", "高", "低", "开盘价", "收盘价", "成交量"] + + for 字段 in 比对字段: + a有 = hasattr(A, 字段) + b有 = hasattr(B, 字段) + if a有 and not b有: + return False, f"K线校验:字段[{字段}],A存在属性、B缺失属性" + if not a有 and b有: + return False, f"K线校验:字段[{字段}],B存在属性、A缺失属性" + if not (a有 and b有): + continue + + valA = getattr(A, 字段) + valB = getattr(B, 字段) + # 双浮点容错对比 + if isinstance(valA, float) and isinstance(valB, float): + 差值 = abs(valA - valB) + if 差值 > 浮点容差: + return False, f"K线校验:字段[{字段}]浮点超限,容差={浮点容差:.2e},A={valA:.10f},B={valB:.10f},差值={差值:.10f}" + + elif 字段 == "时间戳": + if int(valA) != int(valB): + return False, f"K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}" + else: + if valA != valB: + return False, f"K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}" + + return True, "K线:全部字段结构、数值校验完全一致" + + +def 缠论K线相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]: + """缠论K线:基础字段+标的K线递归校验""" + 比对字段 = ["序号", "时间戳", "高", "低", "方向", "分型", "周期", "标识", "分型特征值", "原始起始序号", "原始结束序号", "标的K线", "买卖点信息"] + + for 字段 in 比对字段: + a有 = hasattr(A, 字段) + b有 = hasattr(B, 字段) + if a有 and not b有: + return False, f"缠论K线校验:字段[{字段}],A存在、B缺失属性" + if not a有 and b有: + return False, f"缠论K线校验:字段[{字段}],B存在、A缺失属性" + if not (a有 and b有): + continue + + valA = getattr(A, 字段) + valB = getattr(B, 字段) + + if isinstance(valA, float) and isinstance(valB, float): + 差值 = abs(valA - valB) + if 差值 > 浮点容差: + return False, f"缠论K线校验:[{字段}]浮点超限,容差={浮点容差:.2e},A={valA:.10f},B={valB:.10f},差值={差值:.10f}" + elif 字段 == "标的K线": + if valA is None and valB is None: + continue + if valA is None or valB is None: + return False, f"缠论K线校验:[标的K线]单边空值,A={valA is None},B={valB is None}" + eq_flag, msg = K线相等(valA, valB, 浮点容差) + if not eq_flag: + return False, f"缠论K线校验:标的K线子项异常 >> {msg}" + + elif 字段 == "时间戳": + if int(valA) != int(valB): + return False, f"缠论K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}" + + elif 字段 == "方向": + if str(valA) != str(valB): + return False, f"缠论K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}" + + elif 字段 == "分型": + if str(valA) != str(valB): + return False, f"缠论K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}" + + elif 字段 == "买卖点信息": + if set(valA) != set(valB): + return False, f"缠论K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}" + else: + if valA != valB: + return False, f"缠论K线校验:[{字段}]数值不等,A={repr(valA)},B={repr(valB)}" + + return True, "缠论K线:全部字段、嵌套原始K线校验一致" + + +def 分型相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]: + """分型:左/中/右缠论K线递归 + 自有字段""" + 比对字段 = ["左", "中", "右", "_结构", "_时间戳", "_分型特征值"] + + for 字段 in 比对字段: + a存在 = hasattr(A, 字段) + b存在 = hasattr(B, 字段) + if a存在 and not b存在: + return False, f"分型校验:[{字段}]A有属性、B缺失" + if not a存在 and b存在: + return False, f"分型校验:[{字段}]B有属性、A缺失" + if not (a存在 and b存在): + continue + + valA = getattr(A, 字段) + valB = getattr(B, 字段) + + if isinstance(valA, float) and isinstance(valB, float): + 差值 = abs(valA - valB) + if 差值 > 浮点容差: + return False, f"分型校验:[{字段}]浮点超限,容差={浮点容差:.2e},A={valA:.10f},B={valB:.10f},差值={差值:.10f}" + elif 字段 in ("左", "中", "右"): + if valA is None and valB is None: + continue + if valA is None or valB is None: + return False, f"分型校验:[{字段}]空值不一致,A={valA is None},B={valB is None}" + eq_ok, msg = 缠论K线相等(valA, valB, 浮点容差) + if not eq_ok: + return False, f"分型校验:[{字段}]缠论K线子项异常 >> {msg}" + + elif 字段 == "_结构": + if str(valA) != str(valB): + return False, f"分型K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}" + + elif 字段 == "_时间戳": + if int(valA) != int(valB): + return False, f"分型K线校验:字段[{字段}]数值不等,A={repr(valA)},B={repr(valB)}" + + else: + if valA != valB: + return False, f"分型校验:[{字段}]数值不等,A={repr(valA)},B={repr(valB)}" + + return True, "分型:自有字段+三根缠论K线全部校验一致" + + +@lru_cache(4096) +def 缺口相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]: + """缺口:高、低浮点校验""" + 比对字段 = ["高", "低"] + for 字段 in 比对字段: + a有 = hasattr(A, 字段) + b有 = hasattr(B, 字段) + if a有 and not b有: + return False, f"缺口校验:[{字段}]A存在、B缺失属性" + if not a有 and b有: + return False, f"缺口校验:[{字段}]B存在、A缺失属性" + if not (a有 and b有): + continue + + valA = getattr(A, 字段) + valB = getattr(B, 字段) + if isinstance(valA, float) and isinstance(valB, float): + 差值 = abs(valA - valB) + if 差值 > 浮点容差: + return False, f"缺口校验:[{字段}]浮点超限,容差={浮点容差:.2e},A={valA:.10f},B={valB:.10f},差值={差值:.10f}" + else: + if valA != valB: + return False, f"缺口校验:[{字段}]数值不等,A={repr(valA)},B={repr(valB)}" + return True, "缺口:上下沿价格校验完全一致" + + +def 线段特征相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]: + """线段特征:基础序列虚线列表逐项校验""" + 比对字段 = ["序号", "标识", "线段方向", "基础序列"] + 标签 = f"线段特征校验[A标识={A.标识},B标识={B.标识}]" + for 字段 in 比对字段: + a有 = hasattr(A, 字段) + b有 = hasattr(B, 字段) + if a有 and not b有: + return False, f"{标签}: [{字段}]A存在、B缺失属性" + if not a有 and b有: + return False, f"{标签}: [{字段}]B存在、A缺失属性" + if not (a有 and b有): + continue + + valA = getattr(A, 字段) + valB = getattr(B, 字段) + if 字段 == "基础序列": + if len(valA) != len(valB): + return False, f"{标签}: [基础序列]列表长度不一致,A长度={len(valA)},B长度={len(valB)}" + for idx, (itemA, itemB) in enumerate(zip(valA, valB)): + eq, msg = 虚线相等(itemA, itemB, 浮点容差) + if not eq: + return False, f"{标签}:基础序列[{idx}]子虚线异常 >> {msg}" + + elif 字段 == "线段方向": + if str(valA) != str(valB): + return False, f"{标签}: [{字段}]数值不等,A={repr(valA)},B={repr(valB)}" + else: + if valA != valB: + return False, f"{标签}: [{字段}]数值不等,A={repr(valA)},B={repr(valB)}" + return True, f"{标签}:字段与内部虚线序列全部一致" + + +def 中枢相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]: + """中枢:基础序列虚线列表+第三买卖线单虚线""" + 比对字段 = ["序号", "标识", "级别", "基础序列", "第三买卖线", "本级_第三买卖线"] + 标签 = f"中枢校验[A标识={A.标识},B标识={B.标识}]" + for 字段 in 比对字段: + a有 = hasattr(A, 字段) + b有 = hasattr(B, 字段) + if a有 and not b有: + return False, f"{标签}: [{字段}]A存在、B缺失属性" + if not a有 and b有: + return False, f"{标签}: [{字段}]B存在、A缺失属性" + if not (a有 and b有): + continue + + valA = getattr(A, 字段) + valB = getattr(B, 字段) + + if 字段 == "基础序列": + if len(valA) != len(valB): + return False, f"{标签}: [基础序列]长度不一致 A={len(valA)},B={len(valB)}" + for idx, (itemA, itemB) in enumerate(zip(valA, valB)): + eq, msg = 虚线相等(itemA, itemB, 浮点容差) + if not eq: + return False, f"{标签}:基础序列[{idx}]虚线异常 >> {msg}" + elif 字段 in ("第三买卖线", "本级_第三买卖线"): + if valA is None and valB is None: + continue + if valA is None or valB is None: + return False, f"{标签}: [{字段}]空值不一致 A={valA is None},B={valB is None}" + eq, msg = 虚线相等(valA, valB, 浮点容差) + if not eq: + return False, f"{标签}: [{字段}]子虚线异常 >> {msg}" + else: + if valA != valB: + return False, f"{标签}: [{字段}]数值不等,A={repr(valA)},B={repr(valB)}" + + return True, f"{标签}:基础序列+第三买卖线全部校验一致" + + +def 虚线相等(A, B, 浮点容差: float = 1e-9) -> tuple[bool, str]: + """虚线(笔/线段):全量字段、分型/缺口/K线/列表嵌套精细化报错""" + 比对字段 = ["标识", "序号", "级别", "文", "武", "有效性", "基础序列", "特征序列", "实_中枢序列", "虚_中枢序列", "合_中枢序列", "确认K线", "模式", "_特征序列_显示", "前一缺口", "前一结束位置", "短路修正"] + 标签 = f"虚线校验[A标识={A.标识},B标识={B.标识}]" + for 字段 in 比对字段: + a有 = hasattr(A, 字段) + b有 = hasattr(B, 字段) + if a有 and not b有: + return False, f"{标签}: [{字段}]A存在属性、B缺失属性" + if not a有 and b有: + return False, f"{标签}: [{字段}]B存在属性、A缺失属性" + if not (a有 and b有): + continue + + valA = getattr(A, 字段) + valB = getattr(B, 字段) + + # 文/武:分型 + if 字段 in ("文", "武"): + if valA is None and valB is None: + continue + if valA is None or valB is None: + return False, f"{标签}: [{字段}]分型空值不一致 A={valA is None},B={valB is None}" + eq, msg = 分型相等(valA, valB, 浮点容差) + if not eq: + return False, f"{标签}: [{字段}]子分型异常 >> {msg}" + # 前一缺口 + elif 字段 == "前一缺口": + if valA is None and valB is None: + continue + if valA is None or valB is None: + return False, f"{标签}: [前一缺口]空值不一致 A={valA is None},B={valB is None}" + eq, msg = 缺口相等(valA, valB, 浮点容差) + if not eq: + return False, f"{标签}: [前一缺口]子缺口异常 >> {msg}" + # 前一缺口 + elif 字段 == "前一结束位置": + if valA is None and valB is None: + continue + if valA is None or valB is None: + return False, f"{标签}: [前一结束位置]空值不一致 A={valA is None},B={valB is None}" + eq, msg = 虚线相等(valA, valB, 浮点容差) + if not eq: + return False, f"{标签}: [前一结束位置]异常 >> {msg}" + # 确认K线 + elif 字段 == "确认K线": + if valA is None and valB is None: + continue + if valA is None or valB is None: + return False, f"{标签}: [确认K线]空值不一致 A={valA is None},B={valB is None}" + eq, msg = 缠论K线相等(valA, valB, 浮点容差) + if not eq: + return False, f"{标签}: [确认K线]子缠论K线异常 >> {msg}" + # 各类列表 + elif 字段 in ("基础序列", "实_中枢序列", "虚_中枢序列", "合_中枢序列", "特征序列"): + if len(valA) != len(valB): + return False, f"{标签}: [{字段}]列表长度不一致 A={len(valA)},B={len(valB)}" + for idx, (itemA, itemB) in enumerate(zip(valA, valB)): + if itemA is None and itemB is None: + continue + if itemA is None or itemB is None: + return False, f"{标签}: [{字段}][{idx}]单项空值不一致 A={itemA is None},B={itemB is None}" + if 字段 == "基础序列": + eq, msg = 虚线相等(itemA, itemB, 浮点容差) + elif "中枢" in 字段: + eq, msg = 中枢相等(itemA, itemB, 浮点容差) + else: + eq, msg = 线段特征相等(itemA, itemB, 浮点容差) + if not eq: + return False, f"{标签}: [{字段}][{idx}]子项异常 >> {msg}" + # 普通字段 + else: + if valA != valB: + return False, f"{标签}: [{字段}]数值不等 A={repr(valA)},B={repr(valB)}" + + return True, f"{标签}:全字段、所有嵌套子结构校验全部一致" + class 买卖点类型(str, Enum): """买卖点类型 — 缠论的三类买卖点及扩展类型。 @@ -151,7 +511,7 @@ class 基础买卖点: :ivar 与MACD柱子分型匹配: MACD柱子分型是否匹配 """ - def __init__(self, 类型: 买卖点类型, 当前K线: "K线", 买卖点分型: "分型", 备注: str, 中枢破位值: float): + def __init__(self, 类型: 买卖点类型, 当前K线: K线, 买卖点分型: 分型, 备注: str, 中枢破位值: float): """ :param 类型: 买卖点类型 :param 当前K线: 触发买卖点的K线 @@ -163,9 +523,9 @@ class 基础买卖点: self.类型 = 类型 self.买卖点分型 = 买卖点分型 self.买卖点K线 = 买卖点分型.中 # .镜像 - self.__当前K线: "K线" = 当前K线 - self.失效K线: Optional["K线"] = None - self.终结K线: Optional["K线"] = None # 卖出 or 买入 + self.__当前K线: K线 = 当前K线 + self.失效K线: Optional[K线] = None + self.终结K线: Optional[K线] = None # 卖出 or 买入 self.__破位值 = 中枢破位值 self.结构 = None @@ -177,30 +537,30 @@ class 基础买卖点: @property def 当前K线(self): - """当前K线""" + """触发该买卖点的原始K线""" return self.__当前K线 @property def 破位值(self) -> float: - """破位值 + """中枢破位价格。 - :return: float + :return: 买卖点K线突破中枢边界的价位 """ return self.__破位值 @property def 偏移(self) -> int: - """偏移 + """当前K线与买卖点K线的序号差。 - :return: int + :return: 买卖点产生后经过的K线根数 """ return self.__当前K线.序号 - self.买卖点K线.序号 @property def 失效偏移(self) -> int: - """失效偏移 + """失效K线与买卖点K线的序号差。 - :return: int + :return: 序号差值,-1 表示尚未失效 """ if self.失效K线 is None: return -1 @@ -208,40 +568,45 @@ class 基础买卖点: @property def 有效性(self) -> bool: - """有效性 + """是否已失效。 - :return: bool + :return: 存在失效K线时返回 True """ return self.失效K线 is not None @property def 与MACD柱子匹配(self) -> bool: - """与MACD柱子匹配 + """MACD柱方向是否与买卖点方向一致。 - :return: bool + :return: 一致返回 True """ return self.买卖点K线.与MACD柱子匹配 @property def 与MACD柱子分型匹配(self) -> bool: - """与MACD柱子分型匹配 + """买卖点分型的MACD柱面积方向是否与买卖点方向一致。 - :return: bool + :return: 一致返回 True """ return self.买卖点分型.与MACD柱子分型匹配 @final class 买卖点(基础买卖点): - """买卖点 — 一二三类买卖点构造器。 + """一二三类买卖点及扩展类型(T1/T1P/T2/T2S/T3A/T3B)的构造器。 - 类方法: - 一卖点 / 一买点 / 二卖点 / 二买点 / 三卖点 / 三买点 — 创建对应买卖点实例 - 生成买卖点(特征, 序号, 级别, 分型, 当前缠K) — 根据特征路由到对应构造函数 + 提供18种类方法创建对应买卖点实例,以及一个通用路由方法: + + 主要方法: + :meth:`一卖点` / :meth:`一买点` / :meth:`二卖点` / :meth:`二买点` / :meth:`三卖点` / :meth:`三买点` + :meth:`T1卖点` / :meth:`T1买点` / :meth:`T1P卖点` / :meth:`T1P买点` + :meth:`T2卖点` / :meth:`T2买点` / :meth:`T2S卖点` / :meth:`T2S买点` + :meth:`T3A卖点` / :meth:`T3A买点` / :meth:`T3B卖点` / :meth:`T3B买点` + :meth:`生成买卖点` — 根据特征和序号路由到对应构造函数 """ @classmethod - def 一卖点(cls, 买卖点分型: "分型", 当前K线: "K线", 标识: str, 备注: str, 中枢破位值: float) -> "买卖点": + def 一卖点(cls, 买卖点分型: 分型, 当前K线: K线, 标识: str, 备注: str, 中枢破位值: float) -> 买卖点: """ :param 买卖点分型: 买卖点对应的分型 :param 当前K线: 当前K线 @@ -253,7 +618,7 @@ class 买卖点(基础买卖点): return 买卖点(备注=备注, 类型=买卖点类型.一卖, 买卖点分型=买卖点分型, 当前K线=当前K线, 中枢破位值=中枢破位值) @classmethod - def 一买点(cls, 买卖点分型: "分型", 当前K线: "K线", 标识: str, 备注: str, 中枢破位值: float) -> "买卖点": + def 一买点(cls, 买卖点分型: 分型, 当前K线: K线, 标识: str, 备注: str, 中枢破位值: float) -> 买卖点: """ :param 买卖点分型: 买卖点对应的分型 :param 当前K线: 当前K线 @@ -265,7 +630,7 @@ class 买卖点(基础买卖点): return 买卖点(备注=备注, 类型=买卖点类型.一买, 买卖点分型=买卖点分型, 当前K线=当前K线, 中枢破位值=中枢破位值) @classmethod - def 二卖点(cls, 买卖点分型: "分型", 当前K线: "K线", 标识: str, 备注: str, 中枢破位值: float) -> "买卖点": + def 二卖点(cls, 买卖点分型: 分型, 当前K线: K线, 标识: str, 备注: str, 中枢破位值: float) -> 买卖点: """ :param 买卖点分型: 买卖点对应的分型 :param 当前K线: 当前K线 @@ -277,7 +642,7 @@ class 买卖点(基础买卖点): return 买卖点(备注=备注, 类型=买卖点类型.二卖, 买卖点分型=买卖点分型, 当前K线=当前K线, 中枢破位值=中枢破位值) @classmethod - def 二买点(cls, 买卖点分型: "分型", 当前K线: "K线", 标识: str, 备注: str, 中枢破位值: float) -> "买卖点": + def 二买点(cls, 买卖点分型: 分型, 当前K线: K线, 标识: str, 备注: str, 中枢破位值: float) -> 买卖点: """ :param 买卖点分型: 买卖点对应的分型 :param 当前K线: 当前K线 @@ -289,7 +654,7 @@ class 买卖点(基础买卖点): return 买卖点(备注=备注, 类型=买卖点类型.二买, 买卖点分型=买卖点分型, 当前K线=当前K线, 中枢破位值=中枢破位值) @classmethod - def 三卖点(cls, 买卖点分型: "分型", 当前K线: "K线", 标识: str, 备注: str, 中枢破位值: float) -> "买卖点": + def 三卖点(cls, 买卖点分型: 分型, 当前K线: K线, 标识: str, 备注: str, 中枢破位值: float) -> 买卖点: """ :param 买卖点分型: 买卖点对应的分型 :param 当前K线: 当前K线 @@ -301,7 +666,7 @@ class 买卖点(基础买卖点): return 买卖点(备注=备注, 类型=买卖点类型.三卖, 买卖点分型=买卖点分型, 当前K线=当前K线, 中枢破位值=中枢破位值) @classmethod - def 三买点(cls, 买卖点分型: "分型", 当前K线: "K线", 标识: str, 备注: str, 中枢破位值: float) -> "买卖点": + def 三买点(cls, 买卖点分型: 分型, 当前K线: K线, 标识: str, 备注: str, 中枢破位值: float) -> 买卖点: """ :param 买卖点分型: 买卖点对应的分型 :param 当前K线: 当前K线 @@ -313,7 +678,7 @@ class 买卖点(基础买卖点): return 买卖点(备注=备注, 类型=买卖点类型.三买, 买卖点分型=买卖点分型, 当前K线=当前K线, 中枢破位值=中枢破位值) @classmethod - def 生成买卖点(cls, 特征: str, 序号: str, 级别: str, 买卖点分型: "分型", 当前缠K: "缠论K线"): + def 生成买卖点(cls, 特征: str, 序号: str, 级别: str, 买卖点分型: 分型, 当前缠K: 缠论K线): """ :param 特征: 特征字符串 :param 序号: 序号(如"一"、"二"、"三") @@ -382,234 +747,436 @@ def 转化为时间戳_数字(ts: Union[str, datetime, int, float]) -> int: raise TypeError(f"不支持的时间戳类型: {type(ts)}") +# 模拟 Pydantic 的 ValidationError,保持原有异常逻辑兼容 +class ValidationError(Exception): + pass + + @final -class 缠论配置(BaseModel): - """缠论配置 — 控制所有分析阶段行为的参数集(共 60+ 字段,均有默认值)。 +class 缠论配置: + """控制缠论分析各阶段行为的全局参数集。 - 字段分组: - [基础] 标识 - [缠K] 缠K合并替换 - [笔] 笔内元素数量, 笔弱化, 笔次成笔, 笔内相同终点取舍 等 - [线段] 线段_特征序列忽视老阴老阳, 线段_缺口后紧急修正, 线段内部中枢图显 等 - [分析开关] 分析笔, 分析线段, 分析扩展线段, 分析笔中枢, 分析线段中枢 - [指标] 计算指标, 指标计算方式, MACD/RSI/KDJ 参数 - [推送/显示] 图表展示, 推送K线/笔/线段/中枢, 图表展示_笔 等细分开关 - [买卖点] 买卖点偏移, 买卖点激进识别, 买卖点_背离率, 买卖点_计算方式 等 - [背驰] 线段内部背驰_MACD, 线段内部背驰_斜率 等 - [其他] 手动终止, 加载文件路径 + 共 60+ 个字段,均有合理默认值。按功能分组: - 方法: - to_dict() / to_json() / 保存配置(path?) / 加载配置(path?) (classmethod) - from_dict(data) (classmethod) / from_json(json_str) (classmethod) - 不推送() (classmethod) / 对比(other) -> dict + **[基础]** 标识 \\ + **[缠K]** 缠K合并替换 \\ + **[笔]** 笔内元素数量, 笔弱化, 笔次成笔, 笔内相同终点取舍 等 \\ + **[线段]** 线段_特征序列忽视老阴老阳, 线段_缺口后紧急修正, 线段内部中枢图显 等 \\ + **[分析开关]** 分析笔, 分析线段, 分析扩展线段, 分析笔中枢, 分析线段中枢 \\ + **[指标]** 计算指标, 指标计算方式, MACD/RSI/KDJ 参数 \\ + **[推送/显示]** 图表展示, 推送K线/笔/线段/中枢 等 \\ + **[买卖点]** 买卖点偏移, 买卖点激进识别, 买卖点_背离率, 买卖点_计算方式 等 \\ + **[背驰]** 线段内部背驰_MACD, 线段内部背驰_斜率 等 \\ + **[其他]** 手动终止, 加载文件路径 + + 主要方法: + :meth:`to_dict` / :meth:`to_json` — 序列化 \\ + :meth:`from_dict` / :meth:`from_json` — 反序列化 \\ + :meth:`保存配置` / :meth:`加载配置` — 文件读写 \\ + :meth:`不推送` — 创建静默配置 \\ + :meth:`对比` — 比较两个配置 """ - 标识: str = "bar" - 缠K合并替换: bool = False # False: 在原缠K上合并, True: 产出新缠K + def __init__( + self, + 标识: str = "bar", + 缠K合并替换: bool = False, # False: 在原缠K上合并, True: 产出新缠K + 笔内元素数量: int = 5, # 成笔最低长度 + 笔内相同终点取舍: bool = False, # 一笔终点存在多个终点时 True: last, False: first + 笔内起始分型包含整笔: bool = False, # True: 一笔起始分型高低包含整支笔对象则不成笔, False: 只判断分型中间数据是否包含 + 笔内起始分型包含整笔_包括右: bool = False, # True: 将笔之武.右纳入 + 笔内原始K线包含整笔: bool = False, # 在非 [笔内起始分型包含整笔] 时判断原始K线包含整笔的情况 + 笔次级成笔: bool = False, + 笔弱化: bool = False, + 笔弱化_原始数量: int = 3, + # 笔_必须对齐:bool = False # 强迫症设为True, 将获得无与伦比满足。。。 + 线段_非缺口下穿刺: bool = False, # True: 非缺口状态下[小阳, 少阴]时,存在贯穿伤与之后紧邻的三个元素有方向相同的线段时回退, 此举在当下是否有任何意义呢? + 线段_特征序列忽视老阴老阳: bool = False, # True 不用严格的特征序列包含,也就是忽视缺口全以无缺口对待 + 线段_缺口后紧急修正: bool = True, # True: 当 线段_特征序列忽视老阴老阳=False 时生效,同样 线段_特征序列忽视老阴老阳=True时等同于修正武斗不异常,但只产出一个线段 + 线段_修正: bool = False, # 短路修正,不建议使用,但此修正将走势显示的更加清晰 + 线段内部中枢图显: bool = True, + 扩展线段_当下分析: bool = False, # 以当下来看的分析规则,否则以事后来看 + 分析笔: bool = True, + 分析线段: bool = True, + 分析扩展线段: bool = True, + 分析笔中枢: bool = True, + 分析线段中枢: bool = True, + 手动终止: str = "", # 2099-12-31 00:00:00 + 计算指标: bool = True, + 指标计算方式: str = "收", # (开, 高, 低, 收, 高低均值, 高低收均值, 开高低收均值), 默认 收盘价 + 平滑异同移动平均线_快线周期: int = 13, + 平滑异同移动平均线_慢线周期: int = 31, + 平滑异同移动平均线_信号周期: int = 11, + 相对强弱指数_周期: int = 13, + 相对强弱指数_移动平均线周期: int = 13, + 相对强弱指数_超买阈值: float = 75.0, + 相对强弱指数_超卖阈值: float = 25.0, + 随机指标_RSV周期: int = 13, + 随机指标_K值平滑周期: int = 5, + 随机指标_D值平滑周期: int = 5, + 随机指标_超买阈值: float = 80.0, + 随机指标_超卖阈值: float = 20.0, + 计算BOLL: bool = False, + 布林带_周期: int = 20, + 布林带_标准差倍数: float = 2.0, + 均线_类型列表: List[str] = None, + 均线_周期列表: List[int] = None, + # 多参数指标列表(None/空列表 = 使用默认单参数) + MACD_参数列表: List[tuple] = None, # [(key, 快线, 慢线, 信号), ...] + RSI_周期列表: List[tuple] = None, # [(key, 周期), ...] + KDJ_参数列表: List[tuple] = None, # [(key, RSV周期, K平滑, D平滑), ...] + BOLL_参数列表: List[tuple] = None, # [(key, 周期, 标准差倍数), ...] + 图表展示: bool = True, + 推送K线: bool = True, + 推送笔: bool = True, + 推送线段: bool = True, + 推送中枢: bool = True, + 图表展示_笔: bool = True, + 图表展示_线段: bool = True, + 图表展示_扩展线段: bool = True, + 图表展示_扩展线段_线段: bool = True, + 图表展示_线段_线段: bool = True, + 图表展示_中枢_笔: bool = True, + 图表展示_中枢_线段: bool = True, + 图表展示_中枢_扩展线段: bool = True, + 图表展示_中枢_扩展线段_线段: bool = True, + 图表展示_中枢_线段_线段: bool = True, + 图表展示_中枢_线段内部: bool = True, + 买卖点偏移: int = 1, # 最大偏移 + 买卖点激进识别: bool = False, # 激进模式下将不考虑分型的完整性 + 买卖点与MACD柱强相关: bool = False, # True: 卖点需正值 买点需负值 + 买卖点错过误差值: float = 0.01, # 距离买卖点值上下之内 + 买卖点_指标模式: str = "配置", # 【任意,配置,全量, 相对】 对应K线 + 买卖点_指标匹配_MACD: bool = True, # 买在负,卖在正! + 买卖点_指标匹配_KDJ: bool = True, # 买在死叉之后,卖在金叉之后 + 买卖点_指标匹配_RSI: bool = True, # 买在均线之下,卖在均线之上 + # 以下字段酌情废弃 + 买卖点_背离率: float = float("inf"), + 买卖点_T2_回调阈值: float = 1.0, + 买卖点_T2S_最大层级: int = 3, + 买卖点_峰值条件: bool = False, + 买卖点_计算方式: str = "峰", + 买卖点_计算线段BSP1: bool = True, + 买卖点_处理BSP2: bool = True, + 买卖点_计算线段BSP3: bool = True, + 买卖点_依赖T1: bool = True, + 买卖点_中枢来源: str = "合", + 买卖点_调试输出: bool = False, + # 以上字段酌情废弃 + 线段内部背驰_MACD: bool = True, + 线段内部背驰_斜率: bool = True, + 线段内部背驰_测度: bool = True, + 线段内部背驰_模式: str = "相对", # 【任意,配置,全量,相对】 + 加载文件路径: str = "", + ): + # 所有字段赋值 + self.标识 = 标识 + self.缠K合并替换 = 缠K合并替换 + self.笔内元素数量 = 笔内元素数量 + self.笔内相同终点取舍 = 笔内相同终点取舍 + self.笔内起始分型包含整笔 = 笔内起始分型包含整笔 + self.笔内起始分型包含整笔_包括右 = 笔内起始分型包含整笔_包括右 + self.笔内原始K线包含整笔 = 笔内原始K线包含整笔 + self.笔次级成笔 = 笔次级成笔 + self.笔弱化 = 笔弱化 + self.笔弱化_原始数量 = 笔弱化_原始数量 + self.线段_非缺口下穿刺 = 线段_非缺口下穿刺 + self.线段_特征序列忽视老阴老阳 = 线段_特征序列忽视老阴老阳 + self.线段_缺口后紧急修正 = 线段_缺口后紧急修正 + self.线段_修正 = 线段_修正 + self.线段内部中枢图显 = 线段内部中枢图显 + self.扩展线段_当下分析 = 扩展线段_当下分析 + self.分析笔 = 分析笔 + self.分析线段 = 分析线段 + self.分析扩展线段 = 分析扩展线段 + self.分析笔中枢 = 分析笔中枢 + self.分析线段中枢 = 分析线段中枢 + self.手动终止 = 手动终止 + self.计算指标 = 计算指标 + self.指标计算方式 = 指标计算方式 + self.平滑异同移动平均线_快线周期 = 平滑异同移动平均线_快线周期 + self.平滑异同移动平均线_慢线周期 = 平滑异同移动平均线_慢线周期 + self.平滑异同移动平均线_信号周期 = 平滑异同移动平均线_信号周期 + self.相对强弱指数_周期 = 相对强弱指数_周期 + self.相对强弱指数_移动平均线周期 = 相对强弱指数_移动平均线周期 + self.相对强弱指数_超买阈值 = 相对强弱指数_超买阈值 + self.相对强弱指数_超卖阈值 = 相对强弱指数_超卖阈值 + self.随机指标_RSV周期 = 随机指标_RSV周期 + self.随机指标_K值平滑周期 = 随机指标_K值平滑周期 + self.随机指标_D值平滑周期 = 随机指标_D值平滑周期 + self.随机指标_超买阈值 = 随机指标_超买阈值 + self.随机指标_超卖阈值 = 随机指标_超卖阈值 + self.计算BOLL = 计算BOLL + self.布林带_周期 = 布林带_周期 + self.布林带_标准差倍数 = 布林带_标准差倍数 + self.均线_类型列表 = 均线_类型列表 if 均线_类型列表 is not None else [] + self.均线_周期列表 = 均线_周期列表 if 均线_周期列表 is not None else [] + self.MACD_参数列表 = MACD_参数列表 if MACD_参数列表 is not None else [] + self.RSI_周期列表 = RSI_周期列表 if RSI_周期列表 is not None else [] + self.KDJ_参数列表 = KDJ_参数列表 if KDJ_参数列表 is not None else [] + self.BOLL_参数列表 = BOLL_参数列表 if BOLL_参数列表 is not None else [] + self.图表展示 = 图表展示 + self.推送K线 = 推送K线 + self.推送笔 = 推送笔 + self.推送线段 = 推送线段 + self.推送中枢 = 推送中枢 + self.图表展示_笔 = 图表展示_笔 + self.图表展示_线段 = 图表展示_线段 + self.图表展示_扩展线段 = 图表展示_扩展线段 + self.图表展示_扩展线段_线段 = 图表展示_扩展线段_线段 + self.图表展示_线段_线段 = 图表展示_线段_线段 + self.图表展示_中枢_笔 = 图表展示_中枢_笔 + self.图表展示_中枢_线段 = 图表展示_中枢_线段 + self.图表展示_中枢_扩展线段 = 图表展示_中枢_扩展线段 + self.图表展示_中枢_扩展线段_线段 = 图表展示_中枢_扩展线段_线段 + self.图表展示_中枢_线段_线段 = 图表展示_中枢_线段_线段 + self.图表展示_中枢_线段内部 = 图表展示_中枢_线段内部 + self.买卖点偏移 = 买卖点偏移 + self.买卖点激进识别 = 买卖点激进识别 + self.买卖点与MACD柱强相关 = 买卖点与MACD柱强相关 + self.买卖点错过误差值 = 买卖点错过误差值 + self.买卖点_指标模式 = 买卖点_指标模式 + self.买卖点_指标匹配_MACD = 买卖点_指标匹配_MACD + self.买卖点_指标匹配_KDJ = 买卖点_指标匹配_KDJ + self.买卖点_指标匹配_RSI = 买卖点_指标匹配_RSI + self.买卖点_背离率 = 买卖点_背离率 + self.买卖点_T2_回调阈值 = 买卖点_T2_回调阈值 + self.买卖点_T2S_最大层级 = 买卖点_T2S_最大层级 + self.买卖点_峰值条件 = 买卖点_峰值条件 + self.买卖点_计算方式 = 买卖点_计算方式 + self.买卖点_计算线段BSP1 = 买卖点_计算线段BSP1 + self.买卖点_处理BSP2 = 买卖点_处理BSP2 + self.买卖点_计算线段BSP3 = 买卖点_计算线段BSP3 + self.买卖点_依赖T1 = 买卖点_依赖T1 + self.买卖点_中枢来源 = 买卖点_中枢来源 + self.买卖点_调试输出 = 买卖点_调试输出 + self.线段内部背驰_MACD = 线段内部背驰_MACD + self.线段内部背驰_斜率 = 线段内部背驰_斜率 + self.线段内部背驰_测度 = 线段内部背驰_测度 + self.线段内部背驰_模式 = 线段内部背驰_模式 + self.加载文件路径 = 加载文件路径 - 笔内元素数量: int = 5 # 成BI最低长度 + # 执行初始化验证 + self._validate_all_fields() - 笔内相同终点取舍: bool = False # 一笔终点存在多个终点时 True: last, False: first - 笔内起始分型包含整笔: bool = False # True: 一笔起始分型高低包含整支笔对象则不成笔, False: 只判断分型中间数据是否包含 - 笔内起始分型包含整笔_包括右: bool = False # True: 将笔之武.右纳入 - 笔内原始K线包含整笔: bool = False # 在非 [笔内起始分型包含整笔] 时判断原始K线包含整笔的情况 + # 定义所有字段名列表(替代 model_fields) + @classmethod + def model_fields(cls) -> Dict[str, Dict[str, Any]]: + """返回类所有字段的元信息。 - 笔次级成笔: bool = False - 笔弱化: bool = False - 笔弱化_原始数量: int = 3 - # 笔_必须对齐:bool = False # 强迫症设为True, 将获得无与伦比满足。。。 - - 线段_非缺口下穿刺: bool = False # True: 非缺口状态下[小阳, 少阴]时,存在贯穿伤与之后紧邻的三个元素有方向相同的线段时回退, 此举在当下是否有任何意义呢? - 线段_特征序列忽视老阴老阳: bool = False # True 不用严格的特征序列包含,也就是忽视缺口全以无缺口对待 - 线段_缺口后紧急修正: bool = True # True: 当 线段_特征序列忽视老阴老阳=False 时生效,同样 线段_特征序列忽视老阴老阳=True时等同于修正武斗不异常,但只产出一个线段 - 线段_修正: bool = False # 短路修正,不建议使用,但此修正将走势显示的更加清晰 - - 线段内部中枢图显: bool = True - 扩展线段_当下分析: bool = False # 以当下来看的分析规则,否则以事后来看 - - 分析笔: bool = True # 是否计算BI - 分析线段: bool = True # 是否计算XD - 分析扩展线段: bool = True # 是否计算XD - 分析笔中枢: bool = True # 是否计算BI中枢 - 分析线段中枢: bool = True # 是否计算XD中枢 - - 手动终止: str = "" # 2099-12-31 00:00:00 - 计算指标: bool = True - 指标计算方式: str = "收" # (开, 高, 低, 收, 高低均值, 高低收均值, 开高低收均值), 默认 收盘价 - 平滑异同移动平均线_快线周期: int = 13 - 平滑异同移动平均线_慢线周期: int = 31 - 平滑异同移动平均线_信号周期: int = 11 - - 相对强弱指数_周期: int = 13 - 相对强弱指数_移动平均线周期: int = 13 - 相对强弱指数_超买阈值: float = 75.0 - 相对强弱指数_超卖阈值: float = 25.0 - - 随机指标_RSV周期: int = 13 - 随机指标_K值平滑周期: int = 5 - 随机指标_D值平滑周期: int = 5 - 随机指标_超买阈值: float = 80.0 - 随机指标_超卖阈值: float = 20.0 - - 图表展示: bool = True - 推送K线: bool = True - 推送笔: bool = True - 推送线段: bool = True - 推送中枢: bool = True - - 图表展示_笔: bool = True - 图表展示_线段: bool = True - 图表展示_扩展线段: bool = True - 图表展示_扩展线段_线段: bool = True - 图表展示_线段_线段: bool = True - - 图表展示_中枢_笔: bool = True - 图表展示_中枢_线段: bool = True - 图表展示_中枢_扩展线段: bool = True - 图表展示_中枢_扩展线段_线段: bool = True - 图表展示_中枢_线段_线段: bool = True - 图表展示_中枢_线段内部: bool = True - - 买卖点偏移: int = 1 # 最大偏移 - 买卖点激进识别: bool = False # 激进模式下将不考虑分型的完整性 - 买卖点与MACD柱强相关: bool = False # True: 卖点需正值 买点需负值 - 买卖点错过误差值: float = 0.01 # 距离买卖点值上下之内 - - 买卖点_指标模式: str = "配置" # 【任意,配置,全量, 相对】 对应K线 - 买卖点_指标匹配_MACD: bool = True # 买在负,卖在正! - 买卖点_指标匹配_KDJ: bool = True # 买在死叉之后,卖在金叉之后 - 买卖点_指标匹配_RSI: bool = True # 买在均线之下,卖在均线之上 - - # --- 缠论六类买卖点识别参数 --- - 买卖点_背离率: float = float("inf") # T1/T1P背离率,默认无限大(不筛选) - 买卖点_T2_回调阈值: float = 1.0 # T2回调幅度/突破振幅上限 - 买卖点_T2S_最大层级: int = 3 # T2S最大搜索深度 - 买卖点_峰值条件: bool = False # T1是否要求突破中枢内所有元素的极值 - 买卖点_计算方式: str = "峰" # "峰"=最大MACD柱绝对值, "面"=MACD柱面积 - 买卖点_计算线段BSP1: bool = True # 是否启用T1/T1P识别 - 买卖点_处理BSP2: bool = True # 是否启用T2/T2S识别 - 买卖点_计算线段BSP3: bool = True # 是否启用T3A/T3B识别 - 买卖点_依赖T1: bool = True # T2是否必须依赖T1 - 买卖点_中枢来源: str = "合" # "实"/"虚"/"合" - 取线段的哪个内部中枢序列 - 买卖点_调试输出: bool = False # 是否打印调试信息 - - 线段内部背驰_MACD: bool = True - 线段内部背驰_斜率: bool = True - 线段内部背驰_测度: bool = True - 线段内部背驰_模式: str = "相对" # 【任意,配置,全量,相对】 - - 加载文件路径: str = "" - - @model_validator(mode="before") - def 兼容旧版本配置(cls, values: Dict[str, Any]) -> Dict[str, Any]: + :return: 字段名到类型/默认值元信息的映射 """ - 自动兼容: - 1. 旧版本少字段 → 使用默认值 - 2. 新版本多字段 → 自动忽略多余字段 - 3. 字段改名/删除 → 不报错 + return { + "标识": {"annotation": str, "default": "bar"}, + "缠K合并替换": {"annotation": bool, "default": False}, + "笔内元素数量": {"annotation": int, "default": 5}, + "笔内相同终点取舍": {"annotation": bool, "default": False}, + "笔内起始分型包含整笔": {"annotation": bool, "default": False}, + "笔内起始分型包含整笔_包括右": {"annotation": bool, "default": False}, + "笔内原始K线包含整笔": {"annotation": bool, "default": False}, + "笔次级成笔": {"annotation": bool, "default": False}, + "笔弱化": {"annotation": bool, "default": False}, + "笔弱化_原始数量": {"annotation": int, "default": 3}, + "线段_非缺口下穿刺": {"annotation": bool, "default": False}, + "线段_特征序列忽视老阴老阳": {"annotation": bool, "default": False}, + "线段_缺口后紧急修正": {"annotation": bool, "default": True}, + "线段_修正": {"annotation": bool, "default": False}, + "线段内部中枢图显": {"annotation": bool, "default": True}, + "扩展线段_当下分析": {"annotation": bool, "default": False}, + "分析笔": {"annotation": bool, "default": True}, + "分析线段": {"annotation": bool, "default": True}, + "分析扩展线段": {"annotation": bool, "default": True}, + "分析笔中枢": {"annotation": bool, "default": True}, + "分析线段中枢": {"annotation": bool, "default": True}, + "手动终止": {"annotation": str, "default": ""}, + "计算指标": {"annotation": bool, "default": True}, + "指标计算方式": {"annotation": str, "default": "收"}, + "平滑异同移动平均线_快线周期": {"annotation": int, "default": 13}, + "平滑异同移动平均线_慢线周期": {"annotation": int, "default": 31}, + "平滑异同移动平均线_信号周期": {"annotation": int, "default": 11}, + "相对强弱指数_周期": {"annotation": int, "default": 13}, + "相对强弱指数_移动平均线周期": {"annotation": int, "default": 13}, + "相对强弱指数_超买阈值": {"annotation": float, "default": 75.0}, + "相对强弱指数_超卖阈值": {"annotation": float, "default": 25.0}, + "随机指标_RSV周期": {"annotation": int, "default": 13}, + "随机指标_K值平滑周期": {"annotation": int, "default": 5}, + "随机指标_D值平滑周期": {"annotation": int, "default": 5}, + "随机指标_超买阈值": {"annotation": float, "default": 80.0}, + "随机指标_超卖阈值": {"annotation": float, "default": 20.0}, + "计算BOLL": {"annotation": bool, "default": False}, + "布林带_周期": {"annotation": int, "default": 20}, + "布林带_标准差倍数": {"annotation": float, "default": 2.0}, + "均线_类型列表": {"annotation": List[str], "default": []}, + "均线_周期列表": {"annotation": List[int], "default": []}, + "MACD_参数列表": {"annotation": List[tuple], "default": []}, + "RSI_周期列表": {"annotation": List[tuple], "default": []}, + "KDJ_参数列表": {"annotation": List[tuple], "default": []}, + "BOLL_参数列表": {"annotation": List[tuple], "default": []}, + "图表展示": {"annotation": bool, "default": True}, + "推送K线": {"annotation": bool, "default": True}, + "推送笔": {"annotation": bool, "default": True}, + "推送线段": {"annotation": bool, "default": True}, + "推送中枢": {"annotation": bool, "default": True}, + "图表展示_笔": {"annotation": bool, "default": True}, + "图表展示_线段": {"annotation": bool, "default": True}, + "图表展示_扩展线段": {"annotation": bool, "default": True}, + "图表展示_扩展线段_线段": {"annotation": bool, "default": True}, + "图表展示_线段_线段": {"annotation": bool, "default": True}, + "图表展示_中枢_笔": {"annotation": bool, "default": True}, + "图表展示_中枢_线段": {"annotation": bool, "default": True}, + "图表展示_中枢_扩展线段": {"annotation": bool, "default": True}, + "图表展示_中枢_扩展线段_线段": {"annotation": bool, "default": True}, + "图表展示_中枢_线段_线段": {"annotation": bool, "default": True}, + "图表展示_中枢_线段内部": {"annotation": bool, "default": True}, + "买卖点偏移": {"annotation": int, "default": 1}, + "买卖点激进识别": {"annotation": bool, "default": False}, + "买卖点与MACD柱强相关": {"annotation": bool, "default": False}, + "买卖点错过误差值": {"annotation": float, "default": 0.01}, + "买卖点_指标模式": {"annotation": str, "default": "配置"}, + "买卖点_指标匹配_MACD": {"annotation": bool, "default": True}, + "买卖点_指标匹配_KDJ": {"annotation": bool, "default": True}, + "买卖点_指标匹配_RSI": {"annotation": bool, "default": True}, + "买卖点_背离率": {"annotation": float, "default": float("inf")}, + "买卖点_T2_回调阈值": {"annotation": float, "default": 1.0}, + "买卖点_T2S_最大层级": {"annotation": int, "default": 3}, + "买卖点_峰值条件": {"annotation": bool, "default": False}, + "买卖点_计算方式": {"annotation": str, "default": "峰"}, + "买卖点_计算线段BSP1": {"annotation": bool, "default": True}, + "买卖点_处理BSP2": {"annotation": bool, "default": True}, + "买卖点_计算线段BSP3": {"annotation": bool, "default": True}, + "买卖点_依赖T1": {"annotation": bool, "default": True}, + "买卖点_中枢来源": {"annotation": str, "default": "合"}, + "买卖点_调试输出": {"annotation": bool, "default": False}, + "线段内部背驰_MACD": {"annotation": bool, "default": True}, + "线段内部背驰_斜率": {"annotation": bool, "default": True}, + "线段内部背驰_测度": {"annotation": bool, "default": True}, + "线段内部背驰_模式": {"annotation": str, "default": "相对"}, + "加载文件路径": {"annotation": str, "default": ""}, + } - :param values: 原始配置字典 - :return: 兼容后的字典 - """ - return values - - @field_validator("*", mode="wrap") - def bool_parse_fallback_default(cls, value, handler, info): - """ - :param value: 待验证的值 - :param handler: 默认验证器 - :param info: 字段信息 - :return: 验证后的值 - """ - field = cls.model_fields.get(info.field_name) - if not field: - return handler(value) + def _validate_all_fields(self): + """统一验证所有字段,替代原 field_validator""" 允许值 = { "指标计算方式": ["开", "高", "低", "收", "高低均值", "高低收均值", "开高低收均值"], } - # 字段类型 & 默认值 - type_ = field.annotation - default = field.default - fname = info.field_name + fields = self.model_fields() - try: - # --- 1. 处理 bool:使用内置 bool_parsing --- - if type_ is bool: - return handler(value) + for fname, field_info in fields.items(): + value = getattr(self, fname) + type_ = field_info["annotation"] + default = field_info["default"] - # --- 2. 处理 int:使用内置 int_parsing --- - elif type_ is int: - return handler(value) + try: + # 布尔类型验证 + if type_ is bool: + if not isinstance(value, bool): + setattr(self, fname, bool(value)) - # --- 3. 处理指定 str:必须在允许列表内 --- - elif fname in 允许值: - result = handler(value) - if result not in 允许值[fname]: - raise ValueError(f"值不在允许范围内: {result}") - return result + # 整数类型验证 + elif type_ is int: + if not isinstance(value, int): + setattr(self, fname, int(value)) - # 其他类型不处理 - else: - return handler(value) + # 枚举字符串验证 + elif fname in 允许值: + if value not in 允许值[fname]: + logger.warning(f"[{fname}] = {value} 值不在允许范围内,使用默认值:{default}") + setattr(self, fname, default) - # 验证失败 → 统一用默认值 - except ValidationError as e: - if "bool_parsing" in str(e) or "int_parsing" in str(e): - print(f"[{fname}] = {value} 解析失败,使用默认值:{default}") - return default - raise + except (ValueError, TypeError) as e: + logger.warning(f"[{fname}] = {value} 解析失败,使用默认值:{default}") + setattr(self, fname, default) - # 字符串不在允许列表 → 用默认值 - except ValueError as e: - print(f"[{fname}] = {value} {str(e)},使用默认值:{default}") - return default + # ---- 参数列表解析:多参数列表为空时回退到默认单参数 ---- + + def _解析MACD参数列表(self) -> List[tuple]: + if self.MACD_参数列表: + return self.MACD_参数列表 + return [("macd", self.平滑异同移动平均线_快线周期, self.平滑异同移动平均线_慢线周期, self.平滑异同移动平均线_信号周期)] + + def _解析RSI周期列表(self) -> List[tuple]: + if self.RSI_周期列表: + return self.RSI_周期列表 + return [("rsi", self.相对强弱指数_周期)] + + def _解析KDJ参数列表(self) -> List[tuple]: + if self.KDJ_参数列表: + return self.KDJ_参数列表 + return [("kdj", self.随机指标_RSV周期, self.随机指标_K值平滑周期, self.随机指标_D值平滑周期)] + + def _解析BOLL参数列表(self) -> List[tuple]: + if self.BOLL_参数列表: + return self.BOLL_参数列表 + return [("boll", self.布林带_周期, self.布林带_标准差倍数)] + + @classmethod + def 兼容旧版本配置(cls, values: Dict[str, Any]) -> Dict[str, Any]: + """过滤字典中不属于当前配置类的字段,实现前后版本兼容。 + + :param values: 包含配置键值对的原始字典 + :return: 仅保留当前类已知字段的字典 + """ + valid_fields = cls.model_fields().keys() + cleaned = {k: v for k, v in values.items() if k in valid_fields} + return cleaned def to_dict(self) -> dict: - """对象 → 字典""" - return self.model_dump() + """将配置序列化为字典。 + + :return: 包含所有配置字段的字典 + """ + return {k: getattr(self, k) for k in self.model_fields().keys()} def to_json(self) -> str: - """对象 → JSON字符串""" - return self.model_dump_json(ensure_ascii=False, indent=2) + """将配置序列化为 JSON 字符串。 + + :return: 格式化的 JSON 字符串 + """ + return json.dumps(self.to_dict(), ensure_ascii=False, indent=2) def 保存配置(self, path="缠论配置.json"): - """将配置保存为JSON文件 - :param path: 保存路径,默认"缠论配置.json" + """将配置保存为 JSON 文件。 + + :param path: 文件保存路径,默认 "缠论配置.json" """ with open(path, "w", encoding="utf-8") as f: f.write(self.to_json()) - @staticmethod - def 加载配置(path="缠论配置.json") -> "缠论配置": - """从JSON文件加载配置 - :param path: 配置文件路径 - :return: 缠论配置实例 + @classmethod + def 加载配置(cls, path="缠论配置.json") -> 缠论配置: + """从 JSON 文件加载配置。 + + :param path: JSON 文件路径,默认 "缠论配置.json" + :return: 从文件反序列化的配置对象 """ - with open(path, "r", encoding="utf-8") as f: - return 缠论配置.from_json(f.read()) + with open(path, encoding="utf-8") as f: + return cls.from_json(f.read()) @classmethod - def from_dict(cls, data: dict) -> "缠论配置": + def from_dict(cls, data: dict) -> 缠论配置: + """从字典构造配置对象。 + + :param data: 包含配置键值对的字典 + :return: 新的配置实例 """ - :param data: 字典数据 - :return: 缠论配置实例 - """ - return cls(**data) + cleaned_data = cls.兼容旧版本配置(data) + return cls(**cleaned_data) @classmethod - def from_json(cls, json_str: str) -> "缠论配置": - """ - :param json_str: JSON字符串 - :return: 缠论配置实例 + def from_json(cls, json_str: str) -> 缠论配置: + """从 JSON 字符串构造配置对象。 + + :param json_str: JSON 格式的配置字符串 + :return: 新的配置实例 """ data = json.loads(json_str) return cls.from_dict(data) @classmethod def 不推送(cls): - """创建不推送任何图表的静默配置(用于纯计算场景) + """创建不推送任何图表的静默配置。 - :return: 新缠论配置实例 + 所有图表推送开关关闭,适用于纯计算场景。 """ return cls( 线段内部中枢图显=False, @@ -631,83 +1198,61 @@ class 缠论配置(BaseModel): 图表展示_中枢_线段内部=False, ) + def model_copy(self, update: dict = None, deep: bool = True): + """创建配置副本,可选覆盖部分字段。 + + :param update: 需要覆盖的字段字典 + :param deep: 是否深拷贝(保留参数兼容性,内部始终深拷贝) + :return: 新的配置实例 + """ + data = self.to_dict() + if update: + data.update(update) + return 缠论配置.from_dict(data) + @classmethod def 按序号重组字典(cls, 默认配置, 原始字典: dict) -> dict: - """ - 将形如 "1_open", "1_close", "2_open", "name" 的字典重组为嵌套结构 - { - "1_open": 10, - "1_close": 11, - "2_open": 20, - "name": "BTC", # 无法拆分 - "time": 123456 # 无法拆分 - } - 转化为 - { - 1: {"open": 10, "close": 11}, - 2: {"open": 20}, - "无法拆分": { - "name": "BTC", - "time": 123456 - } - } + """将带数字前缀的扁平字典重组成按序号索引的配置组。 - :param 默认配置: 默认配置实例 - :param 原始字典: 待重组的原始字典 - :return: 重组后的字典 + 键名格式如 "1_笔模式" 会拆分出序号 1,同序号的键归入同一子配置。 + 无法拆分序号前缀的键合并为通用配置。 + + :param 默认配置: 用作模板的默认配置对象 + :param 原始字典: 带数字前缀的扁平键值对字典 + :return: dict[int, 缠论配置] 按序号索引的配置组 """ 结果 = {} 无法拆分项 = {} for 复合键, 值 in 原始字典.items(): - # 尝试拆分 if "_" in 复合键: 序号部分, 键部分 = 复合键.split("_", 1) try: 序号 = int(序号部分) - # 能正常拆分 → 分组 if 序号 not in 结果: 结果[序号] = {} 结果[序号][键部分] = 值 except: - # 格式异常 → 单独处理 无法拆分项[复合键] = 值 else: - # 无下划线 → 无法拆分 → 单独存放 无法拆分项[复合键] = 值 - # 把无法拆分的也放进结果顶层(你要的结构) - """if 无法拆分项: - 结果["无法拆分"] = 无法拆分项""" 配置组 = dict() for k, v in 结果.items(): - 配置组[k] = 默认配置.model_copy( - update=v, - deep=True, - ) + 配置组[k] = 默认配置.model_copy(update=v, deep=True) return 配置组 - def 对比(self, other: "缠论配置") -> dict: - """ - 比较当前配置与另一个配置的差异 + def 对比(self, other: 缠论配置) -> dict: + """比较两个配置的差异。 - :param other: 另一个配置实例 - :return: { - "字段名": { - "旧值": 当前配置的值, - "新值": 另一个配置的值 - }, - ... - } - 仅当值不同时才包含该字段 + :param other: 要比较的另一个配置对象 + :return: 差异字段及其在新配置中的值 """ diff_dict = {} - # 获取所有字段名 - for field_name in self.model_fields.keys(): + for field_name in self.model_fields().keys(): old_value = getattr(self, field_name) new_value = getattr(other, field_name) - # 直接比较值(支持 None) if old_value != new_value: diff_dict[field_name] = new_value return diff_dict @@ -741,8 +1286,11 @@ class 相对方向(Enum): def __repr__(self): return f"相对方向.{self.name}" - def 翻转(self) -> "相对方向": - """返回方向的对立面:向上↔向下, 向上缺口↔向下缺口, 顺↔逆, 衔接向上↔衔接向下, 同不变""" + def 翻转(self) -> 相对方向: + """返回方向的对立面。 + + :return: 对立方向 (向上↔向下, 向上缺口↔向下缺口, 顺↔逆, 衔接向上↔衔接向下, 同不变) + """ match self: case 相对方向.向上: return 相对方向.向下 @@ -764,27 +1312,42 @@ class 相对方向(Enum): return self def 是否向上(self) -> bool: - """判断是否为向上方向(向上/向上缺口/衔接向上)""" + """判断是否为向上方向。 + + :return: 方向为 向上/向上缺口/衔接向上 时返回 True + """ return self in (相对方向.向上, 相对方向.向上缺口, 相对方向.衔接向上) def 是否向下(self) -> bool: - """判断是否为向下方向(向下/向下缺口/衔接向下)""" + """判断是否为向下方向。 + + :return: 方向为 向下/向下缺口/衔接向下 时返回 True + """ return self in (相对方向.向下, 相对方向.向下缺口, 相对方向.衔接向下) def 是否包含(self) -> bool: - """判断是否为包含关系(顺/逆/同)""" + """判断是否为包含关系。 + + :return: 方向为 顺/逆/同 时返回 True + """ return self in (相对方向.顺, 相对方向.逆, 相对方向.同) def 是否缺口(self) -> bool: - """判断是否有缺口(向下缺口/向上缺口)""" + """判断是否有缺口。 + + :return: 方向为 向下缺口/向上缺口 时返回 True + """ return self in (相对方向.向下缺口, 相对方向.向上缺口) def 是否衔接(self) -> bool: - """判断是否为首尾衔接(衔接向下/衔接向上)""" + """判断是否为首尾衔接。 + + :return: 方向为 衔接向下/衔接向上 时返回 True + """ return self in (相对方向.衔接向下, 相对方向.衔接向上) @classmethod - def 分析(cls, 前高: float, 前低: float, 后高: float, 后低: float) -> "相对方向": + def 分析(cls, 前高: float, 前低: float, 后高: float, 后低: float) -> 相对方向: """分析两个价格区间(前、后)的相对位置关系。 :param 前高: 前方价格区间的最高价 @@ -820,9 +1383,13 @@ class 相对方向(Enum): class 分型结构(Enum): - """分型结构 — 描述三根K线构成的顶底分型形态。 + """描述三根K线构成的顶底分型形态。 - 类属性: 上, 下, 顶, 底, 散 + :ivar 上: 三连向上 — 左中右逐步抬高 + :ivar 下: 三连向下 — 左中右逐步走低 + :ivar 顶: 顶分型 — 左右低、中间高 + :ivar 底: 底分型 — 左右高、中间低 + :ivar 散: 向右扩散 — 无法判定 """ 上 = "三连向上" @@ -838,7 +1405,7 @@ class 分型结构(Enum): return self.name @classmethod - def 分析(cls, 左, 中, 右, 可以逆序包含: bool = False, 忽视顺序包含: bool = False) -> Optional["分型结构"]: + def 分析(cls, 左, 中, 右, 可以逆序包含: bool = False, 忽视顺序包含: bool = False) -> Optional[分型结构]: """分析左中右三个元素构成的分型形态。 :param 左: 左侧元素(必须有 高/低 属性) @@ -854,12 +1421,12 @@ class 分型结构(Enum): match (左中关系, 中右关系): case (相对方向.顺, _): if 忽视顺序包含: - ... # print("顺序包含 左中相对方向", 左, 中) + ... # logger.warning("顺序包含 左中相对方向", 左, 中) else: raise ValueError("顺序包含 左中相对方向", 左, 中) case (_, 相对方向.顺): if 忽视顺序包含: - ... # print("顺序包含 中右相对方向", 中, 右) + ... # logger.warning("顺序包含 中右相对方向", 中, 右) else: raise ValueError("顺序包含 中右相对方向", 中, 右) @@ -884,7 +1451,7 @@ class 分型结构(Enum): case (相对方向.逆, 相对方向.逆) if 可以逆序包含: return 分型结构.散 case _: - print("未匹配的关系", 可以逆序包含, 左中关系, 中右关系) + logger.warning(f"未匹配的关系 {可以逆序包含}, {左中关系}, {中右关系}") return None @@ -912,7 +1479,7 @@ class 缺口: return f"缺口区间<{self.低:g} <=> {self.高:g}>" @classmethod - def 居中截取区间(cls, 起点: float, 终点: float, 比例: float = 0.15) -> Optional["缺口"]: + def 居中截取区间(cls, 起点: float, 终点: float, 比例: float = 0.15) -> Optional[缺口]: """ 以原区间中心为基准,向两侧各取总长度的 `比例` 作为新区间。 如果新区间超出原边界则裁剪到边界;若完全越界则返回 None。 @@ -946,10 +1513,14 @@ class 缺口: class 指标: - """指标 — 静态工具类,提供K线取值的辅助方法。""" + """静态工具类,提供K线取值的辅助方法。 + + 主要方法: + :meth:`K线取值` — 根据计算方式从K线中取对应价格(开/高/低/收/均值) + """ @classmethod - def K线取值(cls, k线: "K线", 指标计算方式): + def K线取值(cls, k线: K线, 指标计算方式): """根据计算方式从K线中取值 :param k线: K线对象 @@ -975,51 +1546,59 @@ class 指标: return k线.收盘价 -class 平滑异同移动平均线(BaseModel): +class 平滑异同移动平均线: """平滑异同移动平均线(MACD)— 基于EMA快慢线差值的趋势指标。 - 计算字段(输入): - 时间戳: datetime / 收盘价: float - 快线周期: int (默认12) / 慢线周期: int (默认26) / 信号周期: int (默认9) + :ivar 时间戳: K线时间戳 + :ivar 收盘价: 当前收盘价 + :ivar 快线周期: 短期EMA周期(默认12) + :ivar 慢线周期: 长期EMA周期(默认26) + :ivar 信号周期: 信号线EMA周期(默认9) + :ivar EMA快线: 短期EMA值 + :ivar EMA慢线: 长期EMA值 + :ivar DIF: 快线减慢线 + :ivar DEA: DIF的信号线EMA + :ivar MACD柱子: DIF减DEA - 输出字段: - EMA快线: float — 短期EMA值 - EMA慢线: float — 长期EMA值 - DIF: float — 快线减慢线 - DEA: float — DIF的信号线EMA - MACD柱子: float — DIF减DEA + 主要方法: + :meth:`首次计算` / :meth:`增量计算` / :meth:`增量计算_K线` """ - # 原始数据 - 时间戳: datetime = Field(..., description="数据点对应的时间") - 收盘价: float = Field(..., description="当前K线的收盘价格") + def __init__( + self, + 时间戳: datetime, + 收盘价: float, + 快线周期: int = 12, + 慢线周期: int = 26, + 信号周期: int = 9, + DIF: Optional[float] = None, + DEA: Optional[float] = None, + MACD柱: Optional[float] = None, + 快线EMA: Optional[float] = None, + 慢线EMA: Optional[float] = None, + DEA_EMA: Optional[float] = None, + ): + # 原始数据 + self.时间戳 = 时间戳 + self.收盘价 = 收盘价 - # 计算参数 - 快线周期: int = Field(12, gt=0, description="短期EMA周期,默认为12") - 慢线周期: int = Field(26, gt=0, description="长期EMA周期,默认为26") - 信号周期: int = Field(9, gt=0, description="信号线EMA周期,默认为9") + # 计算参数 + self.快线周期 = 快线周期 + self.慢线周期 = 慢线周期 + self.信号周期 = 信号周期 - # 核心指标值 - DIF: Optional[float] = Field(None, description="差离值 = EMA(快线) - EMA(慢线)") - DEA: Optional[float] = Field(None, description="信号线 = EMA(DIF, 信号周期)") - MACD柱: Optional[float] = Field(None, description="MACD柱状图 = (DIF - DEA) * 2") + # 核心指标值 + self.DIF = DIF + self.DEA = DEA + self.MACD柱 = MACD柱 - # EMA中间值(用于增量计算) - 快线EMA: Optional[float] = Field(None, description="快线EMA的当前值") - 慢线EMA: Optional[float] = Field(None, description="慢线EMA的当前值") - DEA_EMA: Optional[float] = Field(None, description="DEA的EMA当前值") - - # 模型配置 - model_config = { - "arbitrary_types_allowed": True, # 允许特殊类型 - "json_encoders": { - datetime: lambda v: v.isoformat(), # 日期时间序列化 - Enum: lambda v: v.value, # 枚举值序列化 - }, - } + # EMA中间值(用于增量计算) + self.快线EMA = 快线EMA + self.慢线EMA = 慢线EMA + self.DEA_EMA = DEA_EMA @classmethod - def 首次计算(cls, 初始收盘价: float, 初始时间: datetime, 快线周期: int = 12, 慢线周期: int = 26, 信号周期: int = 9) -> "平滑异同移动平均线": + def 首次计算(cls, 初始收盘价: float, 初始时间: datetime, 快线周期: int = 12, 慢线周期: int = 26, 信号周期: int = 9) -> 平滑异同移动平均线: """ 首次计算MACD指标(没有历史数据时使用) @@ -1058,7 +1637,7 @@ class 平滑异同移动平均线(BaseModel): ) @classmethod - def 首次计算_K线(cls, k线: "K线", 计算方式: str, 快线周期: int = 12, 慢线周期: int = 26, 信号周期: int = 9) -> "平滑异同移动平均线": + def 首次计算_K线(cls, k线: K线, 计算方式: str, 快线周期: int = 12, 慢线周期: int = 26, 信号周期: int = 9) -> 平滑异同移动平均线: """ :param k线: 原始K线 :param 计算方式: 指标计算方式(开/高/低/收/均值等) @@ -1072,7 +1651,7 @@ class 平滑异同移动平均线(BaseModel): return cls.首次计算(初始收盘价, 初始时间, 快线周期, 慢线周期, 信号周期) @classmethod - def 增量计算(cls, 前一个MACD: "平滑异同移动平均线", 当前收盘价: float, 当前时间: datetime) -> "平滑异同移动平均线": + def 增量计算(cls, 前一个MACD: 平滑异同移动平均线, 当前收盘价: float, 当前时间: datetime) -> 平滑异同移动平均线: """ 基于前一个MACD指标增量计算当前MACD指标 适用于实时交易系统或流式数据处理 @@ -1093,8 +1672,6 @@ class 平滑异同移动平均线(BaseModel): 快线EMA = 当前收盘价 raise RuntimeError else: - # 快线EMA = 当前收盘价 * 平滑系数(前一个MACD.快线周期) + 前一个MACD.快线EMA * (1 - 平滑系数(前一个MACD.快线周期)) - # 快线EMA = (当前收盘价 - 前一个MACD.快线EMA) * 平滑系数(前一个MACD.快线周期) + 前一个MACD.快线EMA 快线EMA = 当前收盘价 * 平滑系数(前一个MACD.快线周期) + 前一个MACD.快线EMA * ((前一个MACD.快线周期 - 1) / (前一个MACD.快线周期 + 1)) # 计算慢线EMA @@ -1102,8 +1679,6 @@ class 平滑异同移动平均线(BaseModel): 慢线EMA = 当前收盘价 raise RuntimeError else: - # 慢线EMA = 当前收盘价 * 平滑系数(前一个MACD.慢线周期) + 前一个MACD.慢线EMA * (1 - 平滑系数(前一个MACD.慢线周期)) - # 慢线EMA = (当前收盘价 - 前一个MACD.慢线EMA) * 平滑系数(前一个MACD.慢线周期) + 前一个MACD.慢线EMA 慢线EMA = 当前收盘价 * 平滑系数(前一个MACD.慢线周期) + 前一个MACD.慢线EMA * ((前一个MACD.慢线周期 - 1) / (前一个MACD.慢线周期 + 1)) # 计算DIF @@ -1113,8 +1688,6 @@ class 平滑异同移动平均线(BaseModel): if 前一个MACD.DEA_EMA is None: DEA_EMA = DIF else: - # DEA_EMA = 当前收盘价 * 平滑系数(前一个MACD.信号周期) + 前一个MACD.DEA_EMA * (1 - 平滑系数(前一个MACD.信号周期)) - # DEA_EMA = (DIF - 前一个MACD.DEA_EMA) * 平滑系数(前一个MACD.信号周期) + 前一个MACD.DEA_EMA DEA_EMA = DIF * 平滑系数(前一个MACD.信号周期) + 前一个MACD.DEA_EMA * ((前一个MACD.信号周期 - 1) / (前一个MACD.信号周期 + 1)) # 计算MACD柱 @@ -1135,7 +1708,7 @@ class 平滑异同移动平均线(BaseModel): ) @classmethod - def 增量计算_K线(cls, 前一个MACD: "平滑异同移动平均线", 当前K线: "K线", 计算方式: "str") -> "平滑异同移动平均线": + def 增量计算_K线(cls, 前一个MACD: 平滑异同移动平均线, 当前K线: K线, 计算方式: str) -> 平滑异同移动平均线: """ :param 前一个MACD: 前一个MACD指标对象 :param 当前K线: 当前K线 @@ -1147,51 +1720,74 @@ class 平滑异同移动平均线(BaseModel): return cls.增量计算(前一个MACD, 当前收盘价, 当前时间) -class 相对强弱指数(BaseModel): - """ - 相对强弱指数 (RSI) 指标 - 使用 Wilder 平滑(RMA)进行增量计算,提供完整的中间平滑值, - 并支持对RSI值计算SMA(简单移动平均) +class 相对强弱指数: + """相对强弱指数(RSI)— 使用 Wilder 平滑(RMA)进行增量计算。 + + :ivar 时间戳: K线时间戳 + :ivar 收盘价: 当前收盘价 + :ivar 周期: RSI计算周期(默认14) + :ivar 超买阈值: 超买线(默认70.0) + :ivar 超卖阈值: 超卖线(默认30.0) + :ivar RSI: 当前RSI值 + :ivar RSI_SMA周期: RSI的SMA平滑周期(可选) + :ivar RSI_SMA: RSI的SMA值 + :ivar 平均上涨: Wilder平均上涨幅度 + :ivar 平均下跌: Wilder平均下跌幅度 + + 主要方法: + :meth:`首次计算` / :meth:`增量计算` / :meth:`增量计算_K线` """ - # 原始数据 - 时间戳: datetime = Field(..., description="数据点对应的时间") - 收盘价: float = Field(..., description="当前K线的收盘价格") + def __init__( + self, + 时间戳: datetime, + 收盘价: float, + 周期: int = 14, + 超买阈值: float = 70.0, + 超卖阈值: float = 30.0, + RSI_SMA周期: Optional[int] = None, + RSI: Optional[float] = None, + 平均上涨: Optional[float] = None, + 平均下跌: Optional[float] = None, + 上涨幅度: float = 0.0, + 下跌幅度: float = 0.0, + 平滑系数: float = 0.0, + RSI_SMA: Optional[float] = None, + RSI历史队列: List[float] = None, + ): + if RSI历史队列 is None: + RSI历史队列 = [] - # 参数 - 周期: int = Field(14, gt=0, description="RSI周期,默认为14") - 超买阈值: float = Field(70.0, description="超买阈值") - 超卖阈值: float = Field(30.0, description="超卖阈值") - RSI_SMA周期: Optional[int] = Field(None, description="RSI的SMA周期(可选),用于生成信号线") + # 原始数据 + self.时间戳 = 时间戳 + self.收盘价 = 收盘价 - # 核心指标值 - RSI: Optional[float] = Field(None, description="当前RSI值") + # 参数 + self.周期 = 周期 + self.超买阈值 = 超买阈值 + self.超卖阈值 = 超卖阈值 + self.RSI_SMA周期 = RSI_SMA周期 - # 中间平滑值(Wilder平滑) - 平均上涨: Optional[float] = Field(None, description="平均上涨幅度的平滑值") - 平均下跌: Optional[float] = Field(None, description="平均下跌幅度的平滑值") + # 核心指标值 + self.RSI = RSI - # 原始变化值(用于调试) - 上涨幅度: float = Field(0.0, description="当前价格变化中的上涨部分") - 下跌幅度: float = Field(0.0, description="当前价格变化中的下跌部分") + # 中间平滑值(Wilder平滑) + self.平均上涨 = 平均上涨 + self.平均下跌 = 平均下跌 - # 平滑系数(α = 1/周期) - 平滑系数: float = Field(0.0, description="Wilder平滑系数") + # 原始变化值(用于调试) + self.上涨幅度 = 上涨幅度 + self.下跌幅度 = 下跌幅度 - # RSI的SMA(信号线)相关字段 - RSI_SMA: Optional[float] = Field(None, description="RSI的简单移动平均值") - RSI历史队列: List[float] = Field(default_factory=list, description="用于计算SMA的RSI历史队列") + # 平滑系数(α = 1/周期) + self.平滑系数 = 平滑系数 - model_config = { - "arbitrary_types_allowed": True, - "json_encoders": { - datetime: lambda v: v.isoformat(), - Enum: lambda v: v.value, - }, - } + # RSI的SMA(信号线)相关字段 + self.RSI_SMA = RSI_SMA + self.RSI历史队列 = RSI历史队列 @classmethod - def 首次计算(cls, 初始收盘价: float, 初始时间: datetime, 周期: int = 14, 超买阈值: float = 70.0, 超卖阈值: float = 30.0, RSI_SMA周期: Optional[int] = None) -> "相对强弱指数": + def 首次计算(cls, 初始收盘价: float, 初始时间: datetime, 周期: int = 14, 超买阈值: float = 70.0, 超卖阈值: float = 30.0, RSI_SMA周期: Optional[int] = None) -> 相对强弱指数: """ 首次计算RSI(没有足够历史数据时使用) 此时无法计算真实RSI,设为 None,但记录初始收盘价作为起点 @@ -1222,7 +1818,7 @@ class 相对强弱指数(BaseModel): ) @classmethod - def 首次计算_K线(cls, k线: "K线", 计算方式: str, 周期: int = 14, 超买阈值: float = 70.0, 超卖阈值: float = 30.0, RSI_SMA周期: Optional[int] = None) -> "相对强弱指数": + def 首次计算_K线(cls, k线: K线, 计算方式: str, 周期: int = 14, 超买阈值: float = 70.0, 超卖阈值: float = 30.0, RSI_SMA周期: Optional[int] = None) -> 相对强弱指数: """ :param k线: 原始K线 :param 计算方式: 指标计算方式 @@ -1237,7 +1833,7 @@ class 相对强弱指数(BaseModel): return cls.首次计算(初始收盘价, 初始时间, 周期, 超买阈值, 超卖阈值, RSI_SMA周期) @classmethod - def 增量计算(cls, 前一个RSI: "相对强弱指数", 当前收盘价: float, 当前时间: datetime) -> "相对强弱指数": + def 增量计算(cls, 前一个RSI: 相对强弱指数, 当前收盘价: float, 当前时间: datetime) -> 相对强弱指数: """ 基于前一个RSI指标增量计算当前RSI 支持可选的RSI_SMA(简单移动平均) @@ -1308,7 +1904,7 @@ class 相对强弱指数(BaseModel): ) @classmethod - def 增量计算_K线(cls, 前一个RSI: "相对强弱指数", 当前K线: "K线", 计算方式: "str") -> "相对强弱指数": + def 增量计算_K线(cls, 前一个RSI: 相对强弱指数, 当前K线: K线, 计算方式: str) -> 相对强弱指数: """ :param 前一个RSI: 前一个RSI指标对象 :param 当前K线: 当前K线 @@ -1320,49 +1916,81 @@ class 相对强弱指数(BaseModel): return cls.增量计算(前一个RSI, 当前收盘价, 当前时间) -class 随机指标(BaseModel): - """ - KDJ 随机指标 (Stochastic Oscillator) - 使用标准参数:N=9, M1=3, M2=3 - 支持增量计算,需提供当前K线的最高价、最低价、收盘价 +class 随机指标: + """KDJ 随机指标(Stochastic Oscillator)— 基于RSV的三线指标。 + + :ivar 时间戳: K线时间戳 + :ivar 最高价: 当前最高价 + :ivar 最低价: 当前最低价 + :ivar 收盘价: 当前收盘价 + :ivar N: RSV计算周期(默认9) + :ivar M1: K值平滑周期(默认3) + :ivar M2: D值平滑周期(默认3) + :ivar 超买阈值: 超买线(默认80.0) + :ivar 超卖阈值: 超卖线(默认20.0) + :ivar RSV: 未成熟随机值 + :ivar K: K值(RSV的M1周期移动平均) + :ivar D: D值(K的M2周期移动平均) + :ivar J: J值(3K - 2D) + + 主要方法: + :meth:`首次计算` / :meth:`增量计算` / :meth:`增量计算_K线` """ - # 原始数据 - 时间戳: datetime = Field(..., description="数据点对应的时间") - 最高价: float = Field(..., description="当前K线的最高价") - 最低价: float = Field(..., description="当前K线的最低价") - 收盘价: float = Field(..., description="当前K线的收盘价") + def __init__( + self, + 时间戳: datetime, + 最高价: float, + 最低价: float, + 收盘价: float, + N: int = 9, + M1: int = 3, + M2: int = 3, + 超买阈值: float = 80.0, + 超卖阈值: float = 20.0, + RSV: Optional[float] = None, + K: Optional[float] = None, + D: Optional[float] = None, + J: Optional[float] = None, + 历史最高价队列: list[float] = None, + 历史最低价队列: list[float] = None, + 前一个RSV: Optional[float] = None, + 前一个K: Optional[float] = None, + 前一个D: Optional[float] = None, + ): + if 历史最高价队列 is None: + 历史最高价队列 = [] + if 历史最低价队列 is None: + 历史最低价队列 = [] - # 参数 - N: int = Field(9, ge=1, description="RSV的周期(取最近N根K线)") - M1: int = Field(3, ge=1, description="K值的平滑周期") - M2: int = Field(3, ge=1, description="D值的平滑周期") - 超买阈值: float = Field(80.0, description="超买阈值") - 超卖阈值: float = Field(20.0, description="超卖阈值") + # 原始数据 + self.时间戳 = 时间戳 + self.最高价 = 最高价 + self.最低价 = 最低价 + self.收盘价 = 收盘价 - # 核心指标值 - RSV: Optional[float] = Field(None, description="未成熟随机值") - K: Optional[float] = Field(None, description="K值(快速随机指标)") - D: Optional[float] = Field(None, description="D值(慢速随机指标)") - J: Optional[float] = Field(None, description="J值 = 3K - 2D") + # 参数 + self.N = N + self.M1 = M1 + self.M2 = M2 + self.超买阈值 = 超买阈值 + self.超卖阈值 = 超卖阈值 - # 中间状态(用于增量计算) - 历史最高价队列: list[float] = Field(default_factory=list, description="最近N根K线的最高价队列") - 历史最低价队列: list[float] = Field(default_factory=list, description="最近N根K线的最低价队列") - 前一个RSV: Optional[float] = Field(None, description="上一个RSV值(用于平滑K)") - 前一个K: Optional[float] = Field(None, description="上一个K值") - 前一个D: Optional[float] = Field(None, description="上一个D值") + # 核心指标值 + self.RSV = RSV + self.K = K + self.D = D + self.J = J - model_config = { - "arbitrary_types_allowed": True, - "json_encoders": { - datetime: lambda v: v.isoformat(), - Enum: lambda v: v.value, - }, - } + # 中间状态(用于增量计算) + self.历史最高价队列 = 历史最高价队列 + self.历史最低价队列 = 历史最低价队列 + self.前一个RSV = 前一个RSV + self.前一个K = 前一个K + self.前一个D = 前一个D @classmethod - def 首次计算(cls, 初始最高价: float, 初始最低价: float, 初始收盘价: float, 初始时间: datetime, N: int = 9, M1: int = 3, M2: int = 3, 超买阈值: float = 80.0, 超卖阈值: float = 20.0) -> "随机指标": + def 首次计算(cls, 初始最高价: float, 初始最低价: float, 初始收盘价: float, 初始时间: datetime, N: int = 9, M1: int = 3, M2: int = 3, 超买阈值: float = 80.0, 超卖阈值: float = 20.0) -> 随机指标: """ 首次计算KDJ(无历史数据时) 此时无法计算RSV和K/D/J,仅记录初始三价,初始化队列 @@ -1401,7 +2029,7 @@ class 随机指标(BaseModel): ) @classmethod - def 首次计算_K线(cls, k线: "K线", 计算方式: str, RSV周期: int = 9, K值平滑周期: int = 3, D值平滑周期: int = 3, 超买阈值: float = 80.0, 超卖阈值: float = 20.0) -> "随机指标": + def 首次计算_K线(cls, k线: K线, 计算方式: str, RSV周期: int = 9, K值平滑周期: int = 3, D值平滑周期: int = 3, 超买阈值: float = 80.0, 超卖阈值: float = 20.0) -> 随机指标: """ :param k线: 原始K线 :param 计算方式: 指标计算方式(未使用,仅为接口一致) @@ -1419,7 +2047,7 @@ class 随机指标(BaseModel): return cls.首次计算(初始最高价, 初始最低价, 初始收盘价, 初始时间, RSV周期, K值平滑周期, D值平滑周期, 超买阈值, 超卖阈值) @classmethod - def 增量计算(cls, 前一个KDJ: "随机指标", 当前最高价: float, 当前最低价: float, 当前收盘价: float, 当前时间: datetime) -> "随机指标": + def 增量计算(cls, 前一个KDJ: 随机指标, 当前最高价: float, 当前最低价: float, 当前收盘价: float, 当前时间: datetime) -> 随机指标: """ 基于前一个KDJ对象和当前三价,增量计算当前KDJ值 @@ -1509,7 +2137,7 @@ class 随机指标(BaseModel): ) @classmethod - def 增量计算_K线(cls, 前一个KDJ: "随机指标", 当前K线: "K线", 计算方式: "str") -> "随机指标": + def 增量计算_K线(cls, 前一个KDJ: 随机指标, 当前K线: K线, 计算方式: str) -> 随机指标: """ :param 前一个KDJ: 前一个KDJ指标对象 :param 当前K线: 当前K线 @@ -1523,19 +2151,412 @@ class 随机指标(BaseModel): return cls.增量计算(前一个KDJ, 当前最高价, 当前最低价, 当前收盘价, 当前时间) -class 背驰分析: - """背驰分析 — 静态方法容器,提供背驰/背离检测算法。 +class 布林带: + """布林带(BOLL)— 基于移动平均和标准差的波动率通道。 - 方法: - MACD背驰(进入段, 离开段, K线序列, 方式?) — MACD柱状线面积背驰 - 斜率背驰(进入段, 离开段, 序列, 配置?) — 线段斜率背驰 - 测度背驰(进入段, 离开段, 序列, 配置?) — 线段测度背驰 - 全量背驰(进入段, 离开段, ...) — 综合所有背驰检测方式 - 配置背驰(进入段, 离开段, ...) — 根据配置选择检测方式 + :ivar 时间戳: K线时间戳 + :ivar 周期: 移动平均周期(默认20) + :ivar 标准差倍数: 通道宽度倍数(默认2.0) + :ivar 上轨: 中轨 + 标准差倍数 × 标准差 + :ivar 中轨: 简单移动平均 + :ivar 下轨: 中轨 - 标准差倍数 × 标准差 + + 主要方法: + :meth:`首次计算` / :meth:`增量计算` + """ + + __slots__ = ["时间戳", "周期", "标准差倍数", "上轨", "中轨", "下轨", "_历史队列", "_均值", "_方差和"] + + def __init__(self, 时间戳: datetime, 周期: int = 20, 标准差倍数: float = 2.0, 上轨: float = 0.0, 中轨: float = 0.0, 下轨: float = 0.0, 历史队列: Optional[List[float]] = None, _均值: float = 0.0, _方差和: float = 0.0): + self.时间戳 = 时间戳 + self.周期 = 周期 + self.标准差倍数 = 标准差倍数 + self.上轨 = 上轨 + self.中轨 = 中轨 + self.下轨 = 下轨 + self._历史队列 = 历史队列 if 历史队列 is not None else [] + self._均值 = _均值 + self._方差和 = _方差和 + + @classmethod + def 首次计算(cls, k线: K线, 计算方式: str, 周期: int = 20, 标准差倍数: float = 2.0) -> 布林带: + """首次计算布林带,三条轨线初始化为当前价格。 + + :param k线: 当前K线 + :param 计算方式: 取值方式("收"/"高低均值" 等) + :param 周期: 移动平均周期 + :param 标准差倍数: 标准差倍数 + :return: 初始的布林带实例 + """ + 价格 = 指标.K线取值(k线, 计算方式) + return cls(时间戳=k线.时间戳, 周期=周期, 标准差倍数=标准差倍数, 上轨=价格, 中轨=价格, 下轨=价格, 历史队列=[价格]) + + @classmethod + def 增量计算(cls, prev: 布林带, 当前K线: K线, 计算方式: str) -> 布林带: + """基于前一个布林带实例增量计算当前布林带。 + + :param prev: 前一个布林带实例 + :param 当前K线: 当前K线 + :param 计算方式: 取值方式 + :return: 新的布林带实例 + """ + 当前价 = 指标.K线取值(当前K线, 计算方式) + 周期 = prev.周期 + 标准差倍数 = prev.标准差倍数 + + q = prev._历史队列.copy() + q.append(当前价) + if len(q) > 周期: + q.pop(0) + + # 增量均值和方差 + if len(q) < 周期: + _均值 = sum(q) / len(q) + _方差和 = sum((v - _均值) ** 2 for v in q) + else: + n = float(周期) + old_val = prev._历史队列[0] if len(prev._历史队列) >= 周期 else q[0] + new_mean = prev._均值 + (当前价 - old_val) / n + _均值 = new_mean + # Welford 增量方差 + _方差和 = prev._方差和 + (当前价 - old_val) * (当前价 - _均值 + old_val - prev._均值) + + std = (_方差和 / len(q)) ** 0.5 + return cls(时间戳=当前K线.时间戳, 周期=周期, 标准差倍数=标准差倍数, 中轨=_均值, 上轨=_均值 + 标准差倍数 * std, 下轨=_均值 - 标准差倍数 * std, 历史队列=q, _均值=_均值, _方差和=_方差和) + + +class 指标容器: + """挂载在每根K线上,持有该时刻所有指标的快照与增量计算内部状态。 + + 基于注册表模式,所有指标统一存储在内部 dict,通过 ``__getattr__`` / ``__setattr__`` 访问。 + 运行中可随时添加新指标,无需修改类定义。同指标不同参数通过 key 区分 + (如 ``k线.指标.均线["SMA_5"]`` vs ``k线.指标.均线["SMA_20"]``)。 + + :ivar macd: MACD指标快照 + :ivar rsi: RSI指标快照 + :ivar kdj: KDJ指标快照 + :ivar boll: BOLL指标快照 + :ivar 均线: 均线组 dict — key 格式 ``"{类型}_{周期}"``(如 ``"SMA_5"`` / ``"EMA_20"``) + :ivar 单值: 单值指标 dict — key 格式 ``"{名称}_{周期}"``(如 ``"CCI_14"`` / ``"ATR_14"``) + + 主要方法: + :meth:`获取` — 读取指标值,不存在时返回默认值 + :meth:`设置默认值` — 指标不存在时写入默认值 + :meth:`项目` — 返回所有非None指标的迭代器 + """ + + __slots__ = ["_数据"] + + def __init__(self): + object.__setattr__( + self, + "_数据", + { + "macd": None, + "rsi": None, + "kdj": None, + "boll": None, + "均线": {}, + "单值": {}, + }, + ) + + def __getattr__(self, name: str): + _数据 = object.__getattribute__(self, "_数据") + if name not in _数据: + raise AttributeError(f"指标 '{name}' 不存在于 指标容器 中") + return _数据[name] + + def __setattr__(self, name: str, value): + if name == "_数据": + object.__setattr__(self, name, value) + else: + object.__getattribute__(self, "_数据")[name] = value + + def __delattr__(self, name: str): + if name == "_数据": + object.__delattr__(self, name) + else: + object.__getattribute__(self, "_数据").pop(name, None) + + def __getitem__(self, name: str): + return self.__getattr__(name) + + def __setitem__(self, name: str, value): + self.__setattr__(name, value) + + def __contains__(self, name: str) -> bool: + return name in object.__getattribute__(self, "_数据") + + def __repr__(self) -> str: + _数据 = object.__getattribute__(self, "_数据") + keys = [k for k in _数据 if _数据[k] is not None] + return f"指标容器({', '.join(keys)})" + + def 注册(self, 名称: str, 默认值=None): + """预注册指标,让 __getattr__ 能查询到该指标,不存在时返回默认值而不抛出异常。 + + :param 名称: 指标名称 + :param 默认值: 指标不存在时返回的默认值 + """ + _数据 = object.__getattribute__(self, "_数据") + if 名称 not in _数据: + _数据[名称] = 默认值 + + def 项目(self): + """返回所有已注册的非 None 指标的 (名称, 值) 迭代器,供调试/遍历。""" + return ((k, v) for k, v in object.__getattribute__(self, "_数据").items() if v is not None) + + +class 均线工具: + """增量 SMA/EMA 计算的静态方法容器。 + + 主要方法: + :meth:`增量SMA` — 基于前一根K线的SMA值增量计算当前SMA + :meth:`增量EMA` — 用前一根K线的EMA值递推计算当前EMA """ @staticmethod - def MACD背驰(进入段: "虚线", 离开段: "虚线", K线序列: List["K线"], 方式: str = "总") -> bool: + def 增量SMA(普K序列: List[K线], period: int, 计算方式: str) -> float: + """基于前一根K线的 SMA 值,增量计算当前 SMA。 + + :param 普K序列: 原始K线序列(最近 period+1 根至少) + :param period: 移动平均周期 + :param 计算方式: 取值方式("收"/"高低均值" 等) + :return: 当前 SMA 值 + """ + if len(普K序列) <= period: + return sum(指标.K线取值(k, 计算方式) for k in 普K序列[-period:]) / max(len(普K序列), 1) + prev_key = f"SMA_{period}" + prev = 普K序列[-2].指标.均线.get(prev_key) + if prev is None: + 当前价序列 = [指标.K线取值(k, 计算方式) for k in 普K序列[-period:]] + return sum(当前价序列) / period + 当前价 = 指标.K线取值(普K序列[-1], 计算方式) + oldest = 指标.K线取值(普K序列[-period - 1], 计算方式) + return prev + (当前价 - oldest) / period + + @staticmethod + def 增量EMA(普K序列: List[K线], period: int, 计算方式: str, 前值: Optional[float] = None) -> float: + """用前一根K线的 EMA 值递推计算当前 EMA。 + + :param 普K序列: 原始K线序列 + :param period: 平滑周期 + :param 计算方式: 取值方式("收"/"高低均值" 等) + :param 前值: 前一根K线已缓存的 EMA 值,None 则自动查找 + :return: 当前 EMA 值 + """ + 当前价 = 指标.K线取值(普K序列[-1], 计算方式) + if 前值 is None: + return 当前价 + k = 2.0 / (period + 1) + return 当前价 * k + 前值 * (1.0 - k) + + +class 指标计算器: + """在缠K合并之前,增量计算所有开启的指标并挂载到K线上。 + + 遍历 :class:`缠论配置` 中的参数列表,支持同一指标多种参数变体共存。 + 参数列表为空时自动回退到默认单参数(向后兼容)。 + + 主要方法: + :meth:`计算并挂载` — 增量计算所有开启的指标,将结果写入 ``当前K线.指标`` + """ + + @staticmethod + def 计算并挂载(当前K线: K线, 全序列: List[K线], 配置: 缠论配置): + """增量计算所有开启的指标,将结果写入 当前K线.指标 + + :param 当前K线: 当前传入的K线(已在全序列末尾) + :param 全序列: 完整普K序列(包含当前K线) + :param 配置: 缠论配置 + """ + if 当前K线.指标 is None: + 当前K线.指标 = 指标容器() + + # 全序列[-2] 一定是不同时间戳的前一根K线 + # (同时间戳更新时,[-1]已被替换为新值,[-2]即为前一不同时间戳) + prev = 全序列[-2].指标 if len(全序列) > 1 else None + + if 配置.计算指标: + 指标计算器._计算MACD组(当前K线, prev, 配置) + 指标计算器._计算RSI组(当前K线, prev, 配置) + 指标计算器._计算KDJ组(当前K线, prev, 配置) + 指标计算器._计算BOLL组(当前K线, prev, 配置) + + 指标计算器._更新均线(当前K线, 全序列, 配置) + + if prev is not None: + 指标计算器._回填新指标(全序列, 配置) + + @staticmethod + def _计算MACD组(当前K线: K线, prev: Optional[指标容器], 配置: 缠论配置): + idx = 当前K线.指标 + 计算方式 = 配置.指标计算方式 + for key, 快, 慢, 信号 in 配置._解析MACD参数列表(): + prev_val = prev[key] if prev is not None and key in prev else None + if prev_val is not None: + idx[key] = 平滑异同移动平均线.增量计算_K线(prev_val, 当前K线, 计算方式) + else: + idx[key] = 平滑异同移动平均线.首次计算_K线(当前K线, 计算方式, 快, 慢, 信号) + + @staticmethod + def _计算RSI组(当前K线: K线, prev: Optional[指标容器], 配置: 缠论配置): + idx = 当前K线.指标 + 计算方式 = 配置.指标计算方式 + for key, 周期 in 配置._解析RSI周期列表(): + prev_val = prev[key] if prev is not None and key in prev else None + if prev_val is not None: + idx[key] = 相对强弱指数.增量计算_K线(prev_val, 当前K线, 计算方式) + else: + idx[key] = 相对强弱指数.首次计算_K线( + 当前K线, + 计算方式, + 周期, + 配置.相对强弱指数_超买阈值, + 配置.相对强弱指数_超卖阈值, + 配置.相对强弱指数_移动平均线周期, + ) + + @staticmethod + def _计算KDJ组(当前K线: K线, prev: Optional[指标容器], 配置: 缠论配置): + idx = 当前K线.指标 + 计算方式 = 配置.指标计算方式 + for key, rsv, k平滑, d平滑 in 配置._解析KDJ参数列表(): + prev_val = prev[key] if prev is not None and key in prev else None + if prev_val is not None: + idx[key] = 随机指标.增量计算_K线(prev_val, 当前K线, 计算方式) + else: + idx[key] = 随机指标.首次计算_K线( + 当前K线, + 计算方式, + rsv, + k平滑, + d平滑, + 配置.随机指标_超买阈值, + 配置.随机指标_超卖阈值, + ) + + @staticmethod + def _计算BOLL组(当前K线: K线, prev: Optional[指标容器], 配置: 缠论配置): + idx = 当前K线.指标 + 计算方式 = 配置.指标计算方式 + for key, 周期, 标准差倍数 in 配置._解析BOLL参数列表(): + prev_val = prev[key] if prev is not None and key in prev else None + if prev_val is not None: + idx[key] = 布林带.增量计算(prev_val, 当前K线, 计算方式) + else: + idx[key] = 布林带.首次计算(当前K线, 计算方式, 周期, 标准差倍数) + + @staticmethod + def _更新均线(当前K线: K线, 普K序列: List[K线], 配置: 缠论配置): + if not 配置.均线_类型列表 or not 配置.均线_周期列表: + return + for ma_type in 配置.均线_类型列表: + for period in 配置.均线_周期列表: + key = f"{ma_type}_{period}" + if ma_type == "SMA": + 当前K线.指标.均线[key] = 均线工具.增量SMA(普K序列, period, 配置.指标计算方式) + elif ma_type == "EMA": + 前值 = None + if len(普K序列) >= 2: + 前值 = 普K序列[-2].指标.均线.get(key) + 当前K线.指标.均线[key] = 均线工具.增量EMA(普K序列, period, 配置.指标计算方式, 前值) + + @staticmethod + def _回填新指标(全序列: List[K线], 配置: 缠论配置): + """运行中新增指标参数时,回填所有历史K线。 + + 比较首尾K线的指标键,检测运行中动态添加到配置的新指标参数, + 然后从第一根K线开始逐根重新计算,使历史K线也能获得新指标值。 + """ + 首K指标 = 全序列[0].指标 + 尾K指标 = 全序列[-1].指标 + if 首K指标 is None or 尾K指标 is None: + return + + def _新键(尾指标, 首指标, 参数列表): + 新参数 = [] + for params in 参数列表: + key = params[0] + if key in 尾指标 and key not in 首指标: + 新参数.append(params) + return 新参数 + + 新MACD = _新键(尾K指标, 首K指标, 配置._解析MACD参数列表()) + 新RSI = _新键(尾K指标, 首K指标, 配置._解析RSI周期列表()) + 新KDJ = _新键(尾K指标, 首K指标, 配置._解析KDJ参数列表()) + 新BOLL = _新键(尾K指标, 首K指标, 配置._解析BOLL参数列表()) + + if not (新MACD or 新RSI or 新KDJ or 新BOLL): + return + + 计算方式 = 配置.指标计算方式 + + for i, k线 in enumerate(全序列): + if k线.指标 is None: + k线.指标 = 指标容器() + + idx = k线.指标 + prev = 全序列[i - 1].指标 if i > 0 else None + + for key, 快, 慢, 信号 in 新MACD: + prev_val = prev[key] if prev is not None and key in prev else None + if prev_val is not None: + idx[key] = 平滑异同移动平均线.增量计算_K线(prev_val, k线, 计算方式) + else: + idx[key] = 平滑异同移动平均线.首次计算_K线(k线, 计算方式, 快, 慢, 信号) + + for key, 周期 in 新RSI: + prev_val = prev[key] if prev is not None and key in prev else None + if prev_val is not None: + idx[key] = 相对强弱指数.增量计算_K线(prev_val, k线, 计算方式) + else: + idx[key] = 相对强弱指数.首次计算_K线( + k线, + 计算方式, + 周期, + 配置.相对强弱指数_超买阈值, + 配置.相对强弱指数_超卖阈值, + 配置.相对强弱指数_移动平均线周期, + ) + + for key, rsv, k平滑, d平滑 in 新KDJ: + prev_val = prev[key] if prev is not None and key in prev else None + if prev_val is not None: + idx[key] = 随机指标.增量计算_K线(prev_val, k线, 计算方式) + else: + idx[key] = 随机指标.首次计算_K线( + k线, + 计算方式, + rsv, + k平滑, + d平滑, + 配置.随机指标_超买阈值, + 配置.随机指标_超卖阈值, + ) + + for key, 周期, 标准差倍数 in 新BOLL: + prev_val = prev[key] if prev is not None and key in prev else None + if prev_val is not None: + idx[key] = 布林带.增量计算(prev_val, k线, 计算方式) + else: + idx[key] = 布林带.首次计算(k线, 计算方式, 周期, 标准差倍数) + + +class 背驰分析: + """静态方法容器,提供背驰/背离检测算法。 + + 主要方法: + :meth:`MACD背驰` — MACD柱状线面积背驰 + :meth:`斜率背驰` — 线段斜率背驰 + :meth:`测度背驰` — 线段测度背驰 + :meth:`全量背驰` — 综合所有背驰检测方式 + :meth:`配置背驰` — 根据配置选择检测方式 + """ + + @staticmethod + def MACD背驰(进入段: 虚线, 离开段: 虚线, K线序列: List[K线], 方式: str = "总") -> bool: """MACD柱状线面积背驰 :param 进入段: 进入中枢的虚线 @@ -1554,7 +2575,7 @@ class 背驰分析: return 离开面积 < 进入面积 @staticmethod - def 斜率背驰(进入段: "虚线", 离开段: "虚线") -> bool: + def 斜率背驰(进入段: 虚线, 离开段: 虚线) -> bool: """价格斜率背驰 :param 进入段: 进入中枢的虚线 @@ -1579,7 +2600,7 @@ class 背驰分析: return False @staticmethod - def 测度背驰(进入段: "虚线", 离开段: "虚线") -> bool: + def 测度背驰(进入段: 虚线, 离开段: 虚线) -> bool: """价格测度背驰(欧氏距离) :param 进入段: 进入中枢的虚线 @@ -1603,7 +2624,7 @@ class 背驰分析: return False @staticmethod - def 全量背驰(进入段: "虚线", 离开段: "虚线", 普K序列: List["K线"]) -> bool: + def 全量背驰(进入段: 虚线, 离开段: 虚线, 普K序列: List[K线]) -> bool: """判断是否满足全部三种背驰条件(MACD + 测度 + 斜率) :param 进入段: 进入中枢的线段 @@ -1614,7 +2635,7 @@ class 背驰分析: return all([背驰分析.MACD背驰(进入段, 离开段, 普K序列), 背驰分析.测度背驰(进入段, 离开段), 背驰分析.斜率背驰(进入段, 离开段)]) @staticmethod - def 任意背驰(进入段: "虚线", 离开段: "虚线", 普K序列: List["K线"]) -> bool: + def 任意背驰(进入段: 虚线, 离开段: 虚线, 普K序列: List[K线]) -> bool: """判断是否满足任一背驰条件 :param 进入段: 进入中枢的线段 @@ -1625,7 +2646,7 @@ class 背驰分析: return any([背驰分析.MACD背驰(进入段, 离开段, 普K序列), 背驰分析.测度背驰(进入段, 离开段), 背驰分析.斜率背驰(进入段, 离开段)]) @staticmethod - def 配置背驰(进入段: "虚线", 离开段: "虚线", 普K序列: List["K线"], 配置: 缠论配置) -> bool: + def 配置背驰(进入段: 虚线, 离开段: 虚线, 普K序列: List[K线], 配置: 缠论配置) -> bool: """根据配置选择对应的背驰检测组合 :param 进入段: 进入中枢的线段 @@ -1658,7 +2679,7 @@ class 背驰分析: return False @staticmethod - def 任选背驰(进入段: "虚线", 离开段: "虚线", 普K序列: List["K线"]) -> bool: + def 任选背驰(进入段: 虚线, 离开段: 虚线, 普K序列: List[K线]) -> bool: """三个背驰条件中至少两个满足即视为背驰 :param 进入段: 进入中枢的线段 @@ -1670,7 +2691,7 @@ class 背驰分析: return len([背驰 for 背驰 in 混沌槽 if 背驰]) >= 2 @staticmethod - def 背驰模式(进入段: "虚线", 离开段: "虚线", 普K序列: List["K线"], 配置: 缠论配置, 模式: str) -> bool: + def 背驰模式(进入段: 虚线, 离开段: 虚线, 普K序列: List[K线], 配置: 缠论配置, 模式: str) -> bool: """根据模式字符串选择背驰检测策略 :param 进入段: 进入中枢的线段 @@ -1692,8 +2713,8 @@ class 背驰分析: return False -class K线(object): - """原始 K 线 — OHLCV 数据,可内嵌 MACD/RSI/KDJ 指标。 +class K线: + """原始 K 线 — OHLCV 数据,通过 :class:`指标容器` 挂载所有技术指标。 :ivar 标识: 标识符 :ivar 序号: 序号 @@ -1704,13 +2725,22 @@ class K线(object): :ivar 开盘价: 开盘价 :ivar 收盘价: 收盘价 :ivar 成交量: 成交量 - :ivar macd: MACD指标对象 - :ivar rsi: RSI指标对象 - :ivar kdj: KDJ指标对象 + :ivar 指标: 指标容器(含 macd/rsi/kdj/boll/均线/单值) :ivar 方向: 涨跌方向(只读) + + 构造: + :meth:`创建普K` — 创建原始K线实例 + + 序列化: + :meth:`保存到DAT文件` — 将K线序列批量保存为DAT文件 + :meth:`读取大端字节数组` — 从大端字节数组解析K线 + + 工具: + :meth:`获取MACD` — 计算指定区间内的MACD柱子面积 + :meth:`截取` — 截取起始K线到终止K线之间的子序列 """ - __slots__ = ["标识", "序号", "周期", "时间戳", "高", "低", "开盘价", "收盘价", "成交量", "macd", "rsi", "kdj"] + __slots__ = ["标识", "序号", "周期", "时间戳", "高", "低", "开盘价", "收盘价", "成交量", "指标"] def __init__( self, @@ -1723,9 +2753,6 @@ class K线(object): 最低价: float, 收盘价: float, 成交量: float, - macd: 平滑异同移动平均线 = None, - rsi: 相对强弱指数 = None, - kdj: 随机指标 = None, ): """ :param 标识: K线标识符 @@ -1737,9 +2764,9 @@ class K线(object): :param 最低价: 最低价 :param 收盘价: 收盘价 :param 成交量: 成交量 - :param macd: MACD指标对象(可选) - :param rsi: RSI指标对象(可选) - :param kdj: KDJ指标对象(可选) + :param macd: MACD指标对象(可选,存入 指标容器) + :param rsi: RSI指标对象(可选,存入 指标容器) + :param kdj: KDJ指标对象(可选,存入 指标容器) """ self.序号: int = 序号 self.标识: str = 标识 @@ -1750,9 +2777,32 @@ class K线(object): self.收盘价: float = 收盘价 self.成交量: float = 成交量 self.周期: int = 周期 - self.macd: 平滑异同移动平均线 = macd - self.rsi: 相对强弱指数 = rsi - self.kdj: 随机指标 = kdj + self.指标: 指标容器 = 指标容器() + + # ---- property 兼容层:k线.macd → k线.指标.macd ---- + @property + def macd(self) -> Optional[平滑异同移动平均线]: + """该K线绑定的 MACD 指标快照。 + + :return: MACD 实例,未计算时返回 None + """ + return self.指标.macd + + @property + def rsi(self) -> Optional[相对强弱指数]: + """该K线绑定的 RSI 指标快照。 + + :return: RSI 实例,未计算时返回 None + """ + return self.指标.rsi + + @property + def kdj(self) -> Optional[随机指标]: + """该K线绑定的 KDJ 指标快照。 + + :return: KDJ 实例,未计算时返回 None + """ + return self.指标.kdj def __str__(self): return f"{self.标识}<{self.序号}, {self.周期}, {self.方向}, {self.时间戳}, {self.开盘价:g}, {self.高:g}, {self.低:g}, {self.收盘价:g}>" @@ -1779,7 +2829,7 @@ class K线(object): ) @classmethod - def 创建普K(cls, 标识: str, 时间戳: datetime, 开盘价: float, 最高价: float, 最低价: float, 收盘价: float, 成交量: float, 序号: int, 周期: int) -> "K线": + def 创建普K(cls, 标识: str, 时间戳: datetime | int, 开盘价: float, 最高价: float, 最低价: float, 收盘价: float, 成交量: float, 序号: int, 周期: int) -> K线: """快捷构造普通K线 :param 标识: K线标识符 @@ -1797,7 +2847,7 @@ class K线(object): 标识=标识, 序号=序号, 周期=周期, - 时间戳=时间戳, + 时间戳=时间戳 if isinstance(时间戳, datetime) else 转化为时间戳(时间戳), # 注意此处只为兼容Rust绑定 开盘价=开盘价, 最高价=最高价, 最低价=最低价, @@ -1807,7 +2857,7 @@ class K线(object): return k线 @classmethod - def 保存到DAT文件(cls, 路径: str, K线序列: List["K线"]): + def 保存到DAT文件(cls, 路径: str, K线序列: List[K线]): """将K线序列保存为二进制DAT文件 :param 路径: 保存路径 @@ -1816,10 +2866,10 @@ class K线(object): with open(路径, "wb") as f: for K in K线序列: f.write(bytes(K)) - print(f"保存到DAT文件: {路径}") + logger.info(f"保存到DAT文件: {路径}") @classmethod - def 读取大端字节数组(cls, 字节组: bytes, 周期: int, 标识: str) -> "K线": + def 读取大端字节数组(cls, 字节组: bytes, 周期: int, 标识: str) -> K线: """从大端字节序二进制数据反序列化K线(兼容.dat/.nb文件格式) :param 字节组: 二进制数据(48字节) @@ -1842,7 +2892,7 @@ class K线(object): ) @classmethod - def 获取MACD(cls, K线序列: List["K线"], 始: "K线", 终: "K线") -> Dict[str, float]: + def 获取MACD(cls, K线序列: List[K线], 始: K线, 终: K线) -> Dict[str, float]: """计算指定K线区间的MACD柱面积 :param K线序列: 完整K线序列 @@ -1865,7 +2915,7 @@ class K线(object): return {"阳": 阳, "阴": 阴, "合": 合, "总": 阳 + abs(阴)} @staticmethod - def 截取(序列: List["K线"], 始: "K线", 终: "K线") -> List["K线"]: + def 截取(序列: List[K线], 始: K线, 终: K线) -> List[K线]: """按起止K线截取K线子序列 :param 序列: 完整K线序列 @@ -1876,8 +2926,8 @@ class K线(object): return 序列[序列.index(始) : 序列.index(终) + 1] -class 缠论K线(object): - """缠论K线 — 经包含处理后的标准化K线,有方向和分型结构标记。 +class 缠论K线: + """经包含处理后的标准化K线,有方向和分型结构标记。 :ivar 序号: 序号 :ivar 时间戳: 时间戳 @@ -1892,6 +2942,20 @@ class 缠论K线(object): :ivar 原始结束序号: 合并前的原始K线结束序号 :ivar 标的K线: 对应的原始K线 :ivar 买卖点信息: 买卖点信息(预留) + :ivar 镜像: 镜像缠K(底↔顶、上↔下,只读) + :ivar 与MACD柱子匹配: 是否与MACD柱子方向匹配(只读) + :ivar 与RSI匹配: 是否与RSI超买超卖匹配(只读) + :ivar 与KDJ匹配: 是否与KDJ超买超卖匹配(只读) + + 构造: + :meth:`创建缠K` — 从原始K线创建缠论K线(含包含继承) + + 算法: + :meth:`分析` — 包含处理 + 合并,增量更新缠K序列(主入口) + :meth:`时间戳对齐` — 将缠K序列时间戳对齐到指定基线 + + 工具: + :meth:`截取` — 截取起始缠K到终止缠K之间的子序列 """ __slots__ = ["序号", "时间戳", "高", "低", "方向", "分型", "周期", "标识", "分型特征值", "原始起始序号", "原始结束序号", "标的K线", "买卖点信息"] @@ -1903,7 +2967,7 @@ class 缠论K线(object): 最高价: float, 最低价: float, 最终方向: 相对方向, - 普K: "K线", + 普K: K线, 原始起始序号: int, 原始结束序号: int, 分型: Optional[分型结构] = None, @@ -1931,7 +2995,7 @@ class 缠论K线(object): self.原始起始序号: int = 原始起始序号 self.原始结束序号: int = 原始结束序号 - self.标的K线: "K线" = 普K + self.标的K线: K线 = 普K self.买卖点信息: Set[str] = set() def __str__(self): @@ -1989,7 +3053,7 @@ class 缠论K线(object): return False @classmethod - def 时间戳对齐(cls, 基线: List["缠论K线"], k线: "缠论K线"): + def 时间戳对齐(cls, 基线: List[缠论K线], k线: 缠论K线): """在基线序列中找到与k线时间戳对齐的时间戳 :param 基线: 参照缠K序列 @@ -2009,7 +3073,7 @@ class 缠论K线(object): return k线.时间戳 @classmethod - def 创建缠K(cls, 时间戳: datetime, 高: float, 低: float, 方向: 相对方向, 结构: 分型结构, 原始序号: int, 普k: "K线", 之前: Optional["缠论K线"] = None) -> "缠论K线": + def 创建缠K(cls, 时间戳: datetime, 高: float, 低: float, 方向: 相对方向, 结构: 分型结构, 原始序号: int, 普k: K线, 之前: Optional[缠论K线] = None) -> 缠论K线: """创建新的缠论K线 :param 时间戳: K线时间 @@ -2045,7 +3109,7 @@ class 缠论K线(object): return 当前 @classmethod - def 兼并(cls, 之前缠K: Optional["缠论K线"], 当前缠K: "缠论K线", 当前普K: "K线", 配置: 缠论配置) -> Tuple[Optional["缠论K线"], Optional[str]]: + def _兼并(cls, 之前缠K: Optional[缠论K线], 当前缠K: 缠论K线, 当前普K: K线, 配置: 缠论配置) -> Tuple[Optional[缠论K线], Optional[str]]: """K线包含处理(合并) :param 之前缠K: 前一缠K @@ -2098,7 +3162,7 @@ class 缠论K线(object): return None, None @classmethod - def 分析(cls, 当前K线: "K线", 缠K序列: List["缠论K线"], 普K序列: List["K线"], 配置: 缠论配置) -> tuple[str, Optional["分型"]]: + def 分析(cls, 当前K线: K线, 缠K序列: List[缠论K线], 普K序列: List[K线], 配置: 缠论配置) -> tuple[str, Optional[分型]]: """分析K线,执行指标计算+包含处理+分型判定 :param 当前K线: 新到的原始K线 @@ -2109,33 +3173,21 @@ class 缠论K线(object): """ 当前K线.标识 = 配置.标识 if not 普K序列: - if 配置.计算指标: - 当前K线.macd = 平滑异同移动平均线.首次计算_K线(当前K线, 配置.指标计算方式, 配置.平滑异同移动平均线_快线周期, 配置.平滑异同移动平均线_慢线周期, 配置.平滑异同移动平均线_信号周期) - 当前K线.rsi = 相对强弱指数.首次计算_K线(当前K线, 配置.指标计算方式, 配置.相对强弱指数_周期, 配置.相对强弱指数_超买阈值, 配置.相对强弱指数_超卖阈值, 配置.相对强弱指数_移动平均线周期) - 当前K线.kdj = 随机指标.首次计算_K线(当前K线, 配置.指标计算方式, 配置.随机指标_RSV周期, 配置.随机指标_K值平滑周期, 配置.随机指标_D值平滑周期, 配置.随机指标_超买阈值, 配置.随机指标_超卖阈值) 普K序列.append(当前K线) else: 之前普K = 普K序列[-1] if 之前普K.时间戳 == 当前K线.时间戳: 当前K线.序号 = 普K序列[-1].序号 普K序列[-1] = 当前K线 - if 配置.计算指标: - try: - 当前K线.macd = 平滑异同移动平均线.增量计算_K线(普K序列[-2].macd, 当前K线, 配置.指标计算方式) - 当前K线.rsi = 相对强弱指数.增量计算_K线(普K序列[-2].rsi, 当前K线, 配置.指标计算方式) - 当前K线.kdj = 随机指标.增量计算_K线(普K序列[-2].kdj, 当前K线, 配置.指标计算方式) - except IndexError: - pass # traceback.print_exc() else: if 之前普K.时间戳 > 当前K线.时间戳: raise RuntimeError("时序错误") 当前K线.序号 = 之前普K.序号 + 1 - if 配置.计算指标: - 当前K线.macd = 平滑异同移动平均线.增量计算_K线(之前普K.macd, 当前K线, 配置.指标计算方式) - 当前K线.rsi = 相对强弱指数.增量计算_K线(之前普K.rsi, 当前K线, 配置.指标计算方式) - 当前K线.kdj = 随机指标.增量计算_K线(之前普K.kdj, 当前K线, 配置.指标计算方式) 普K序列.append(当前K线) + if 配置.计算指标: + 指标计算器.计算并挂载(当前K线, 普K序列, 配置) + 之前缠K: Optional[缠论K线] = None 状态, 形态 = None, None if 缠K序列: @@ -2143,7 +3195,7 @@ class 缠论K线(object): 之前缠K = 缠K序列[-2] except IndexError: pass - 新缠K, 模式 = 缠论K线.兼并(之前缠K, 缠K序列[-1], 当前K线, 配置) + 新缠K, 模式 = 缠论K线._兼并(之前缠K, 缠K序列[-1], 当前K线, 配置) if 新缠K is not None: if 模式 == "添加": 缠K序列.append(新缠K) @@ -2194,7 +3246,7 @@ class 缠论K线(object): return 状态, 形态 @staticmethod - def 截取(序列: List["缠论K线"], 始: "缠论K线", 终: "缠论K线") -> List["缠论K线"]: + def 截取(序列: List[缠论K线], 始: 缠论K线, 终: 缠论K线) -> List[缠论K线]: """ :param 序列: 缠K序列 :param 始: 起始缠K @@ -2207,18 +3259,25 @@ class 缠论K线(object): 分型模式 = True -class 分型(object): - """分型 — 由左中右三根缠论K线构成的顶/底分型结构。 +class 分型: + """由左中右三根缠论K线构成的顶/底分型结构。 :ivar 左: 左侧缠K(可能为None) :ivar 中: 中间缠K(分型顶点/底点) :ivar 右: 右侧缠K(可能为None) - :ivar 结构: 分型结构(顶/底/上/下/包含) - :ivar 时间戳: 分型时间戳 - :ivar 分型特征值: 顶分型取最高价,底分型取最低价 - :ivar 关系组: 左中、中右、左右三对相对方向 - :ivar 强度: 分型强度(强/中/弱/未知) - :ivar 与MACD柱子分型匹配: 是否与MACD柱子分型匹配 + :ivar 结构: 分型结构(顶/底,受 :data:`分型模式` 控制缓存/实时读取) + :ivar 时间戳: 分型时间戳(受 :data:`分型模式` 控制) + :ivar 分型特征值: 顶分型取最高价,底分型取最低价(受 :data:`分型模式` 控制) + :ivar 关系组: 左中、中右、左右三对相对方向(只读) + :ivar 强度: 分型强度 — 强/中/弱/未知(只读) + :ivar 与MACD柱子分型匹配: 是否与MACD柱子分型匹配(只读) + + 构造: + :meth:`从缠K序列中获取分型` — 从缠K序列中按中间缠K定位分型 + + 工具: + :meth:`判断分型` — 判断左分型的武与右分型的文是否相连 + :meth:`向序列中添加` — 将分型添加到序列(自动去重) """ __slots__ = ["左", "中", "右", "_结构", "_时间戳", "_分型特征值"] @@ -2239,25 +3298,37 @@ class 分型(object): self._分型特征值 = 中.分型特征值 def __str__(self): - return f"{self.中.分型}<{self.时间戳}, {self.分型特征值:g}, None: {self.左 is None}, None: {self.右 is None}>" + return f"{self.结构}<{self.时间戳}, {self.分型特征值:g}, None: {self.左 is None}, None: {self.右 is None}>" def __repr__(self): - return f"{self.中.分型}<{self.时间戳}, {self.分型特征值:g}, None: {self.左 is None}, None: {self.右 is None}>" + return f"{self.结构}<{self.时间戳}, {self.分型特征值:g}, None: {self.左 is None}, None: {self.右 is None}>" @property def 时间戳(self): + """分型时间戳 — 分型模式为 True 时返回构造时缓存值,否则从中间缠K实时读取。 + + :return: 分型的时间戳 + """ if 分型模式: return self._时间戳 return self.中.时间戳 @property def 分型特征值(self): + """分型特征值 — 顶分型为最高价,底分型为最低价。 + + :return: 分型的特征价格 + """ if 分型模式: return self._分型特征值 return self.中.分型特征值 @property def 结构(self): + """分型结构 — 分型模式为 True 时返回构造时缓存值,否则从中间缠K实时读取。 + + :return: 分型结构(顶/底/上/下/散) + """ if 分型模式: return self._结构 return self.中.分型 @@ -2330,7 +3401,7 @@ class 分型(object): return False @classmethod - def 判断分型(cls, 左: "分型", 右: "分型", 模式: str = "中") -> bool: + def 判断分型(cls, 左: 分型, 右: 分型, 模式: str = "中") -> bool: """判断两个分型是否相同(identity比较) :param 左: 左分型 @@ -2341,7 +3412,7 @@ class 分型(object): return 左 is 右 @staticmethod - def 从缠K序列中获取分型(K线序列: List[缠论K线], 中: 缠论K线) -> "分型": + def 从缠K序列中获取分型(K线序列: List[缠论K线], 中: 缠论K线) -> 分型: """从缠K序列中提取以指定缠K为中元素的分型 :param K线序列: 缠K列表 @@ -2355,7 +3426,7 @@ class 分型(object): return 分型(左=K线序列[索引 - 1], 中=中, 右=None) @staticmethod - def 向序列中添加(分型序列: List["分型"], 当前分型: "分型"): + def 向序列中添加(分型序列: List[分型], 当前分型: 分型): """向分型序列尾部添加,自动校验顶底交替 :param 分型序列: 现有分型列表 @@ -2368,13 +3439,13 @@ class 分型(object): if 分型序列[-1].结构 is 当前分型.结构: raise ValueError("分型相同无法添加", 分型序列[-1], 当前分型) if 分型序列[-1].右 is None: - print("分型.向序列中添加, 分型异常", 分型序列[-1]) + logger.warning(f"分型.向序列中添加, 分型异常, {分型序列[-1]}") 分型序列.append(当前分型) -class 虚线(object): - """虚线 — 笔/线段的通用数据结构,持有一组分型端点(文=起点分型, 武=终点分型)。 +class 虚线: + """笔/线段的通用数据结构,持有一组分型端点(文=起点分型, 武=终点分型)。 :ivar 标识: 类型标识("笔"/"线段"/"扩展线段"等) :ivar 序号: 序号 @@ -2397,6 +3468,33 @@ class 虚线(object): :ivar 低: 最低价(只读) :ivar 笔序列: 所有笔(只读) :ivar 图表标题: 图表显示标题(只读) + + 构造: + :meth:`创建笔` — 从两个分型创建笔虚线 + :meth:`创建线段` — 从虚线序列创建线段虚线 + + 实例方法: + :meth:`之前是` / :meth:`之后是` — 判断两个虚线的先后关系 + :meth:`获取普K序列` — 获取虚线范围内的原始K线序列 + :meth:`获取缠K序列` — 获取虚线范围内的缠论K线序列 + :meth:`获取数据文本` — 返回关键数据的文本表示 + + 买卖点匹配: + :meth:`缠K买卖点模式` / :meth:`买卖点配置匹配` — 按配置模式匹配买卖点 + :meth:`买卖点任意匹配` / :meth:`买卖点全量匹配` / :meth:`买卖点相对匹配` — 不同匹配策略 + :meth:`买卖意义` — 判断虚线是否具有买卖意义 + + MACD计算: + :meth:`计算MACD柱子均值` / :meth:`计算MACD柱子均值_阴` / :meth:`计算MACD柱子均值_阳` + :meth:`武之全量MACD均值` / :meth:`武之MACD均值` / :meth:`武之MACD均值_阴` / :meth:`武之MACD均值_阳` + :meth:`武之MACD极值` — 基于MACD极值判断背驰 + :meth:`计算K线序列MACD趋向背驰` — 检测MACD趋向背驰 + :meth:`计算MACD柱子分段` — 将K线序列按MACD柱子符号分段 + :meth:`统计MACD行为` — 统计密集区域的MACD交叉行为 + + 辅助: + :meth:`密集区域按间隔` — 按最大间隔和最少交叉数查找密集区域 + :meth:`获取_武` — 获取虚线的有效武分型 """ __slots__ = ["标识", "序号", "级别", "文", "武", "有效性", "基础序列", "特征序列", "实_中枢序列", "虚_中枢序列", "合_中枢序列", "确认K线", "模式", "_特征序列_显示", "前一缺口", "前一结束位置", "短路修正"] @@ -2419,12 +3517,12 @@ class 虚线(object): self.有效性 = 有效性 - self.基础序列: List["虚线"] = [] - self.特征序列: List[Optional[线段特征]] = [] + self.基础序列: List[虚线] = [] + self.特征序列: List[Optional[线段特征]] = [None, None, None] - self.实_中枢序列: List["中枢"] = [] - self.虚_中枢序列: List["中枢"] = [] - self.合_中枢序列: List["中枢"] = [] + self.实_中枢序列: List[中枢] = [] + self.虚_中枢序列: List[中枢] = [] + self.合_中枢序列: List[中枢] = [] self.确认K线: Optional[缠论K线] = None self.模式: str = "文武" self._特征序列_显示 = False @@ -2454,7 +3552,7 @@ class 虚线(object): return f"{self.文.中.标识}:{self.文.中.周期}:{self.标识}:{self.序号}" @property - def 方向(self) -> "相对方向": + def 方向(self) -> 相对方向: """ :return: 运行方向 :raises RuntimeError: 无法识别的方向 @@ -2475,19 +3573,25 @@ class 虚线(object): @property def 高(self) -> float: - """虚线区间的最高价(向上线段为终点分型最高价,向下线段为起点分型最高价)""" + """虚线区间的最高价。 + + :return: 向上虚线取武.中.高,向下虚线取文.中.高 + """ if self.方向 is 相对方向.向上: return self.武.中.高 return self.文.中.高 @property def 低(self) -> float: - """虚线区间的最低价(向下线段为终点分型最低价,向上线段为起点分型最低价)""" + """虚线区间的最低价。 + + :return: 向下虚线取武.中.低,向上虚线取文.中.低 + """ if self.方向 is 相对方向.向下: return self.武.中.低 return self.文.中.低 - def 之前是(self, 之前: "虚线") -> bool: + def 之前是(self, 之前: 虚线) -> bool: """ :param 之前: 前一条虚线 :return: 当前虚线的起点是否为前一条虚线的终点 @@ -2496,7 +3600,7 @@ class 虚线(object): return 分型.判断分型(之前.武, self.文) return False - def 之后是(self, 之后: "虚线") -> bool: + def 之后是(self, 之后: 虚线) -> bool: """ :param 之后: 后一条虚线 :return: 当前虚线的终点是否为后一条虚线的起点 @@ -2505,14 +3609,14 @@ class 虚线(object): return 分型.判断分型(self.武, 之后.文) return False - def 获取普K序列(self, 观察员: "观察者") -> List[K线]: + def 获取普K序列(self, 观察员: 观察者) -> List[K线]: """ :param 观察员: 观察者实例 :return: 区间内的原始K线列表 """ return K线.截取(观察员.普通K线序列, self.文.中.标的K线, self.武.中.标的K线) - def 获取缠K序列(self, 观察员: "观察者") -> List[缠论K线]: + def 获取缠K序列(self, 观察员: 观察者) -> List[缠论K线]: """ :param 观察员: 观察者实例 :return: 区间内的缠K列表 @@ -2527,7 +3631,7 @@ class 虚线(object): return f"{self.标识}, {self.序号}, {self.级别}, 文:({int(self.文.时间戳.timestamp())},{self.文.分型特征值:g}), 武:({int(self.武.时间戳.timestamp())},{self.武.分型特征值:g}), {self.有效性}, {len(self.基础序列)}, {线段.特征序列状态(self)}, (前: {str(前)}, 后: {str(后)}, 三: {str(三)}, 伤: {str(贯穿伤)}), 实: {str(self.实_中枢序列)}, 虚: {str(self.虚_中枢序列)}, 合: {str(self.合_中枢序列)}, {self.模式}, {str(self.前一缺口)}, {str(self.前一结束位置)}, {self.短路修正}" @classmethod - def 创建笔(cls, 文: 分型, 武: 分型, 有效性: bool = True) -> "虚线": + def 创建笔(cls, 文: 分型, 武: 分型, 有效性: bool = True) -> 虚线: """ :param 文: 起点分型 :param 武: 终点分型 @@ -2537,11 +3641,16 @@ class 虚线(object): return 虚线(0, "笔", 文, 武, 1, 有效性) @classmethod - def 创建线段(cls, 虚线序列: List["虚线"]) -> "虚线": + def 创建线段(cls, 虚线序列: List[虚线]) -> 虚线: """ :param 虚线序列: 构成线段的虚线列表(笔) :return: 虚线实例(标识="线段"或"线段<...>") """ + 序列数量 = len(虚线序列) + if 序列数量 < 3: + raise RuntimeError(f"创建线段 虚线序列 数量 {序列数量} < 3") + if 序列数量 % 2 == 0: + raise RuntimeError(f"创建线段 虚线序列 数量 {序列数量} 不是单数!") 文 = 虚线序列[0].文 武 = 虚线序列[-1].武 assert 文.结构 != 武.结构 @@ -2555,7 +3664,7 @@ class 虚线(object): return 段 @classmethod - def 缠K买卖点模式(cls, 模式: str, 缠K: "缠论K线", 配置: 缠论配置): + def 缠K买卖点模式(cls, 模式: str, 缠K: 缠论K线, 配置: 缠论配置): """ :param 模式: "全量"/"任意"/"配置"/"相对" :param 缠K: 待检测的缠论K线 @@ -2574,8 +3683,13 @@ class 虚线(object): return False @classmethod - def 买卖点配置匹配(cls, 缠K: "缠论K线", 配置: 缠论配置): - """根据配置中的指标开关检测缠K匹配情况(MACD/KDJ/RSI组合)""" + def 买卖点配置匹配(cls, 缠K: 缠论K线, 配置: 缠论配置): + """根据配置中的指标开关检测缠K匹配情况。 + + :param 缠K: 待检测的缠论K线 + :param 配置: 包含指标开关的缠论配置 + :return: 是否满足配置中开启的指标匹配条件 + """ match 配置.买卖点_指标匹配_MACD, 配置.买卖点_指标匹配_KDJ, 配置.买卖点_指标匹配_RSI: case True, True, True: return 缠K.与MACD柱子匹配 and 缠K.与KDJ匹配 and 缠K.与RSI匹配 @@ -2599,7 +3713,7 @@ class 虚线(object): return False @classmethod - def 买卖点任意匹配(cls, 缠K: "缠论K线"): + def 买卖点任意匹配(cls, 缠K: 缠论K线): """ :param 缠K: 缠论K线 :return: MACD/KDJ/RSI 任一匹配 @@ -2607,7 +3721,7 @@ class 虚线(object): return any([缠K.与MACD柱子匹配, 缠K.与KDJ匹配, 缠K.与RSI匹配]) @classmethod - def 买卖点全量匹配(csl, 缠K: "缠论K线"): + def 买卖点全量匹配(csl, 缠K: 缠论K线): """ :param 缠K: 缠论K线 :return: MACD/KDJ/RSI 全部匹配 @@ -2615,7 +3729,7 @@ class 虚线(object): return all([缠K.与MACD柱子匹配, 缠K.与KDJ匹配, 缠K.与RSI匹配]) @classmethod - def 买卖点相对匹配(cls, 缠K: "缠论K线"): + def 买卖点相对匹配(cls, 缠K: 缠论K线): """ :param 缠K: 缠论K线 :return: 三个指标中至少两个匹配 @@ -2624,7 +3738,7 @@ class 虚线(object): return len([匹配 for 匹配 in 混沌槽 if 匹配]) >= 2 @classmethod - def 计算MACD柱子均值(cls, 普K序列: List[K线], 实线: "虚线") -> float: + def 计算MACD柱子均值(cls, 普K序列: List[K线], 实线: 虚线) -> float: """ :param 普K序列: 完整K线序列 :param 实线: 虚线(笔/线段) @@ -2634,7 +3748,7 @@ class 虚线(object): return sum([abs(K.macd.MACD柱) for K in K线序列]) / len(K线序列) @classmethod - def 计算MACD柱子均值_阴(cls, 普K序列: List[K线], 实线: "虚线") -> float: + def 计算MACD柱子均值_阴(cls, 普K序列: List[K线], 实线: 虚线) -> float: """ :param 普K序列: 完整K线序列 :param 实线: 虚线 @@ -2647,7 +3761,7 @@ class 虚线(object): return False @classmethod - def 计算MACD柱子均值_阳(cls, 普K序列: List[K线], 实线: "虚线") -> float: + def 计算MACD柱子均值_阳(cls, 普K序列: List[K线], 实线: 虚线) -> float: """ :param 普K序列: 完整K线序列 :param 实线: 虚线 @@ -2660,7 +3774,7 @@ class 虚线(object): return False @classmethod - def 武之全量MACD均值(cls, 普K序列: List[K线], 实线: "虚线") -> bool: + def 武之全量MACD均值(cls, 普K序列: List[K线], 实线: 虚线) -> bool: """ :param 普K序列: 完整K线序列 :param 实线: 虚线 @@ -2669,7 +3783,7 @@ class 虚线(object): return abs(实线.武.中.标的K线.macd.MACD柱) < cls.计算MACD柱子均值(普K序列, 实线) @classmethod - def 武之MACD均值(cls, 普K序列: List[K线], 实线: "虚线") -> bool: + def 武之MACD均值(cls, 普K序列: List[K线], 实线: 虚线) -> bool: """ :param 普K序列: 完整K线序列 :param 实线: 虚线 @@ -2681,7 +3795,7 @@ class 虚线(object): return cls.武之MACD均值_阴(普K序列, 实线) @classmethod - def 武之MACD均值_阴(cls, 普K序列: List[K线], 实线: "虚线") -> bool: + def 武之MACD均值_阴(cls, 普K序列: List[K线], 实线: 虚线) -> bool: """ :param 普K序列: 完整K线序列 :param 实线: 虚线 @@ -2690,7 +3804,7 @@ class 虚线(object): return abs(实线.武.中.标的K线.macd.MACD柱) < abs(cls.计算MACD柱子均值_阴(普K序列, 实线)) @classmethod - def 武之MACD均值_阳(cls, 普K序列: List[K线], 实线: "虚线") -> bool: + def 武之MACD均值_阳(cls, 普K序列: List[K线], 实线: 虚线) -> bool: """ :param 普K序列: 完整K线序列 :param 实线: 虚线 @@ -2699,7 +3813,7 @@ class 虚线(object): return abs(实线.武.中.标的K线.macd.MACD柱) < abs(cls.计算MACD柱子均值_阳(普K序列, 实线)) @classmethod - def 武之MACD极值(cls, 普K序列: List[K线], 实线: "虚线") -> bool: + def 武之MACD极值(cls, 普K序列: List[K线], 实线: 虚线) -> bool: """ :param 普K序列: 完整K线序列 :param 实线: 虚线 @@ -2716,7 +3830,7 @@ class 虚线(object): return False @classmethod - def 计算K线序列MACD趋向背驰(cls, 普K序列: Sequence["K线"], 方向: 相对方向): + def 计算K线序列MACD趋向背驰(cls, 普K序列: Sequence[K线], 方向: 相对方向): """计算K线序列的MACD柱/DIF/DEA趋向背驰(三元素判断) :param 普K序列: K线序列 @@ -2795,8 +3909,7 @@ class 虚线(object): return 结果 @classmethod - @lru_cache(maxsize=128) - def 买卖意义(cls, 实线: "虚线", 观察员: "观察者") -> Tuple[bool, str]: + def 买卖意义(cls, 实线: 虚线, 观察员: 观察者) -> Tuple[bool, str]: """ 静止是相对的,而运动是绝对的 @@ -2831,7 +3944,7 @@ class 虚线(object): return 结果, "" @classmethod - def 计算MACD柱子分段(cls, k线序列: List["K线"]) -> Tuple[List[List["K线"]], ...]: + def 计算MACD柱子分段(cls, k线序列: List[K线]) -> Tuple[List[List[K线]], ...]: """ :param k线序列: K线序列 :return: 按正负分段的MACD柱列表 @@ -2857,16 +3970,24 @@ class 虚线(object): 结果.append(当前段柱子) 当前段柱子 = [k线序列[i].macd.MACD柱] 当前符号 = 新符号 + if 当前段柱子: + 结果.append(当前段柱子) + """a = [x for sub in 结果 for x in sub] + b = [sub.macd.MACD柱 for sub in k线序列] + if list(a) != list(b): + for i,(j,k) in enumerate(zip(a, b)): + if j is not k: + raise RuntimeError( f"序列不一致,{len(a)}, {len(b)}, {(i,j,k)}")""" return tuple(结果) @classmethod def 密集区域按间隔(cls, 交叉标记: List[int], 最大间隔: int = 5, 最少交叉数: int = 3) -> List[Tuple[int, int, int]]: - """ - 交叉标记: 长度为len(macd_list)的列表,0=无交叉, 1=金叉, -1=死叉 - 最大间隔: 相邻交叉索引差 ≤ 此值则归入同一密集区 - 最少交叉数: 一个密集区内至少包含的交叉次数 + """将交叉信号按间隔聚类成密集区域。 - :return: [(起始交叉索引, 结束交叉索引, 区内交叉次数), ...] + :param 交叉标记: 长度为N的列表,0=无交叉, 1=金叉, -1=死叉 + :param 最大间隔: 相邻交叉索引差 ≤ 此值则归入同一密集区 + :param 最少交叉数: 一个密集区内至少包含的交叉次数 + :return: 密集区域列表 [(起始交叉索引, 结束交叉索引, 区内交叉次数), ...] """ # 提取所有交叉点的索引 交叉索引 = [i for i, v in enumerate(交叉标记) if v != 0] @@ -2963,23 +4084,22 @@ class 虚线(object): return tmp.武 -class 笔(object): - """笔 — 纯静态方法容器,提供笔划分算法的所有函数。 +class 笔: + """纯静态方法容器,提供笔划分算法的所有函数。 主要方法: - 获取缠K数量 — 获取笔内缠K数量 - 停顿 — 检测笔的停顿点 - 自检 — 校验笔的有效性 - 获取所有停顿位置 — 获取笔内所有停顿位置 - 获取起始分型 — 获取笔的起始分型 - 生成笔序列 — 从分型序列生成笔序列 - 是否背驰过 — 判断笔是否经过背驰 + :meth:`分析` — 流式分析,增量识别笔 + :meth:`以文会友` — 根据起始点找笔 + :meth:`以武会友` — 根据结束点找笔 + :meth:`根据缠K找笔` — 判断缠K是否在文武序号之间 + :meth:`获取所有停顿位置` — 获取笔内所有停顿位置 + :meth:`是否背驰过` — 判断笔是否经过背驰 """ __slots__ = [] @staticmethod - def 获取缠K数量(缠K序列: List[缠论K线], 笔序列: List[虚线], 配置: 缠论配置) -> int: + def _获取缠K数量(缠K序列: List[缠论K线], 笔序列: List[虚线], 配置: 缠论配置) -> int: """获取笔内有效缠K数量(考虑笔弱化等配置) :param 缠K序列: 候选笔的基础缠K序列 @@ -2992,27 +4112,27 @@ class 笔(object): return 实际数量 if 配置.笔弱化 and 实际数量 >= 3: - 实际高点 = 笔.实际高点(缠K序列, 配置.笔内相同终点取舍) - 实际低点 = 笔.实际低点(缠K序列, 配置.笔内相同终点取舍) - 原始数量 = 1 + abs(实际低点.标的K线.序号 - 实际高点.标的K线.序号) + _实际高点 = 笔._实际高点(缠K序列, 配置.笔内相同终点取舍) + _实际低点 = 笔._实际低点(缠K序列, 配置.笔内相同终点取舍) + 原始数量 = 1 + abs(_实际低点.标的K线.序号 - _实际高点.标的K线.序号) if 原始数量 >= 配置.笔内元素数量: return 配置.笔内元素数量 if 笔序列: - 筆 = 笔.根据缠K找笔(笔序列, 实际高点) or 笔.根据缠K找笔(笔序列, 实际低点) + 筆 = 笔.根据缠K找笔(笔序列, _实际高点) or 笔.根据缠K找笔(笔序列, _实际低点) if 筆: - if 筆.方向 is 相对方向.向上 and 实际低点.低 < 筆.低: + if 筆.方向 is 相对方向.向上 and _实际低点.低 < 筆.低: if 原始数量 >= 配置.笔弱化_原始数量: return 配置.笔内元素数量 - if 筆.方向 is 相对方向.向下 and 实际低点.低 > 筆.高: + if 筆.方向 is 相对方向.向下 and _实际低点.低 > 筆.高: if 原始数量 >= 配置.笔弱化_原始数量: return 配置.笔内元素数量 return 实际数量 @staticmethod - def 次高(缠K序列: List[缠论K线], 笔内相同终点取舍: bool) -> 缠论K线: - """次高 + def _次高(缠K序列: List[缠论K线], 笔内相同终点取舍: bool) -> 缠论K线: + """_次高 :param 缠K序列: 缠K序列 :param 笔内相同终点取舍: 终点取舍方式 @@ -3025,8 +4145,8 @@ class 笔(object): return highs[-1] if 笔内相同终点取舍 else highs[0] @staticmethod - def 次低(缠K序列: List[缠论K线], 笔内相同终点取舍: bool) -> 缠论K线: - """次低 + def _次低(缠K序列: List[缠论K线], 笔内相同终点取舍: bool) -> 缠论K线: + """_次低 :param 缠K序列: 缠K序列 :param 笔内相同终点取舍: 终点取舍方式 @@ -3039,8 +4159,8 @@ class 笔(object): return lows[-1] if 笔内相同终点取舍 else lows[0] @staticmethod - def 实际高点(缠K序列: List[缠论K线], 笔内相同终点取舍: bool) -> 缠论K线: - """实际高点 + def _实际高点(缠K序列: List[缠论K线], 笔内相同终点取舍: bool) -> 缠论K线: + """_实际高点 :param 缠K序列: 缠K序列 :param 笔内相同终点取舍: 终点取舍方式 @@ -3052,8 +4172,8 @@ class 笔(object): return highs[-1] if 笔内相同终点取舍 else highs[0] @staticmethod - def 实际低点(缠K序列: List[缠论K线], 笔内相同终点取舍: bool) -> 缠论K线: - """实际低点 + def _实际低点(缠K序列: List[缠论K线], 笔内相同终点取舍: bool) -> 缠论K线: + """_实际低点 :param 缠K序列: 缠K序列 :param 笔内相同终点取舍: 终点取舍方式 @@ -3065,7 +4185,7 @@ class 笔(object): return lows[-1] if 笔内相同终点取舍 else lows[0] @staticmethod - def 相对关系(筆: 虚线, 配置: 缠论配置) -> bool: + def _相对关系(筆: 虚线, 配置: 缠论配置) -> bool: """相对关系 :param 筆: 笔虚线 @@ -3102,7 +4222,7 @@ class 笔(object): 旧笔.有效性 = False @classmethod - def _添加新笔(cls, 分型序列: List[分型], 笔序列: List[虚线], 待添加分型: "分型", 待添加新笔: 虚线, 行号): + def _添加新笔(cls, 分型序列: List[分型], 笔序列: List[虚线], 待添加分型: 分型, 待添加新笔: 虚线, 行号): """内部方法:添加新分型和新笔 :param 分型序列: 分型列表 @@ -3118,7 +4238,7 @@ class 笔(object): if 分型序列[-1].结构 is 待添加分型.结构: raise ValueError("分型相同无法添加", 分型序列[-1], 待添加分型) if 分型序列[-1].右 is None: - print("分型.向序列中添加, 分型异常", 分型序列[-1]) + logger.warning(f"分型.向序列中添加, 分型异常 {分型序列[-1]}") 分型序列.append(待添加分型) if 笔序列 and not 笔序列[-1].之后是(待添加新笔): @@ -3126,10 +4246,10 @@ class 笔(object): if 笔序列: 待添加新笔.序号 = 笔序列[-1].序号 + 1 - if 待添加新笔.武.左 is None and 待添加新笔.武.右 is None: + if 待添加新笔.武.左 is None or 待添加新笔.武.右 is None: 待添加新笔.有效性 = False if 笔序列[-1].武.结构 in (分型结构.上, 分型结构.下): - print(f"_添加新笔[{行号}] 出现无效分型", 笔序列[-1]) + logger.error(f"_添加新笔[{行号}] 出现无效分型 {笔序列[-1]}") 笔序列.append(待添加新笔) @@ -3150,7 +4270,7 @@ class 笔(object): return 递归层次 if 递归层次 > 64: - print(f"笔.分析 递归深度超出 64 < {递归层次}") + logger.warning(f"笔.分析 递归深度超出 64 < {递归层次}") # return 递归层次 if 当前分型.结构 not in (分型结构.顶, 分型结构.底): @@ -3164,7 +4284,7 @@ class 笔(object): 笔递归分析 = 笔.分析 之前分型 = 分型序列[-1] - if (之前分型.中.时间戳 == 当前分型.中.时间戳) or (之前分型.结构 in (分型结构.上, 分型结构.下)): + if (之前分型.时间戳 == 当前分型.时间戳) or (之前分型.结构 in (分型结构.上, 分型结构.下)): 笔._弹出旧笔(分型序列, 笔序列, sys._getframe().f_lineno) if not 分型序列: if 当前分型.右 is not None: @@ -3172,9 +4292,9 @@ class 笔(object): return 递归层次 之前分型 = 分型序列[-1] - if 之前分型.中.时间戳 > 当前分型.中.时间戳 and 之前分型.中.序号 - 当前分型.中.序号 > 1: + if 之前分型.时间戳 > 当前分型.时间戳 and 之前分型.中.序号 - 当前分型.中.序号 > 1: # raise RuntimeError(f"时序错误-{递归层次}, {之前分型}, {当前分型}") - print(colored(f"时序错误-{递归层次}, {之前分型}, {当前分型}", "red")) + logger.error(f"时序错误-{递归层次}, {之前分型}, {当前分型}") return 递归层次 if 配置.笔弱化 and 笔序列: @@ -3187,11 +4307,11 @@ class 笔(object): if 之前分型.结构 is not 当前分型.结构: 基础序列 = 缠论K线.截取(缠K序列, 之前分型.中, 当前分型.中) 当前笔 = 虚线.创建笔(文=之前分型, 武=当前分型, 有效性=True) - if 笔.获取缠K数量(基础序列, 笔序列, 配置) >= 配置.笔内元素数量: + if 笔._获取缠K数量(基础序列, 笔序列, 配置) >= 配置.笔内元素数量: if 之前分型.结构 is 分型结构.顶 and 当前分型.结构 is 分型结构.底: - 文官 = 笔.实际高点(基础序列, False) + 文官 = 笔._实际高点(基础序列, False) else: - 文官 = 笔.实际低点(基础序列, False) + 文官 = 笔._实际低点(基础序列, False) if 文官 is not 之前分型.中: 临时分型 = 分型.从缠K序列中获取分型(缠K序列, 文官) @@ -3204,20 +4324,20 @@ class 笔(object): return 递归层次 if 之前分型.结构 is 分型结构.顶 and 当前分型.结构 is 分型结构.底: - 武将 = 笔.实际低点(基础序列, 配置.笔内相同终点取舍) + 武将 = 笔._实际低点(基础序列, 配置.笔内相同终点取舍) else: - 武将 = 笔.实际高点(基础序列, 配置.笔内相同终点取舍) + 武将 = 笔._实际高点(基础序列, 配置.笔内相同终点取舍) - if 笔.相对关系(当前笔, 配置) and 当前分型.中 is 武将: + if 笔._相对关系(当前笔, 配置) and 当前分型.中 is 武将: 笔._添加新笔(分型序列, 笔序列, 当前分型, 当前笔, sys._getframe().f_lineno) return 递归层次 if 配置.笔次级成笔: if 之前分型.结构 is 分型结构.顶 and 当前分型.结构 is 分型结构.底: - 武将 = 笔.次低(基础序列, 配置.笔内相同终点取舍) + 武将 = 笔._次低(基础序列, 配置.笔内相同终点取舍) else: - 武将 = 笔.次高(基础序列, 配置.笔内相同终点取舍) - if 笔.相对关系(当前笔, 配置) and 当前分型.中 is 武将: + 武将 = 笔._次高(基础序列, 配置.笔内相同终点取舍) + if 笔._相对关系(当前笔, 配置) and 当前分型.中 is 武将: 笔._添加新笔(分型序列, 笔序列, 当前分型, 当前笔, sys._getframe().f_lineno) return 递归层次 @@ -3233,9 +4353,9 @@ class 笔(object): 笔._弹出旧笔(分型序列, 笔序列, sys._getframe().f_lineno) k线序列 = 缠论K线.截取(缠K序列, 之前分型.中, 当前分型.中) if 之前分型.结构 is 分型结构.顶: - 武将 = 笔.实际低点(k线序列, 配置.笔内相同终点取舍) + 武将 = 笔._实际低点(k线序列, 配置.笔内相同终点取舍) else: - 武将 = 笔.实际高点(k线序列, 配置.笔内相同终点取舍) + 武将 = 笔._实际高点(k线序列, 配置.笔内相同终点取舍) 临时分型 = 分型.从缠K序列中获取分型(缠K序列, 武将) if 分型序列: @@ -3248,7 +4368,7 @@ class 笔(object): 递归层次 = 笔递归分析(临时分型, 分型序列, 笔序列, 缠K序列, 普K序列, 递归层次 + 1, 配置) if 分型序列 and 分型序列[-1] is 临时分型: """""" - # print("笔.分析 事后修复错过的笔", 临时分型, "当前分型", 当前分型) + # logger.warning("笔.分析 事后修复错过的笔", 临时分型, "当前分型", 当前分型) 递归层次 = 笔递归分析(当前分型, 分型序列, 笔序列, 缠K序列, 普K序列, 递归层次 + 1, 配置) return 递归层次 @@ -3284,7 +4404,7 @@ class 笔(object): return None @staticmethod - def 根据缠K找笔(笔序列: List[虚线], 缠K: "缠论K线", 偏移: int = 1): + def 根据缠K找笔(笔序列: List[虚线], 缠K: 缠论K线, 偏移: int = 1): """根据缠K找笔 :param 笔序列: 笔列表 @@ -3294,13 +4414,13 @@ class 笔(object): """ for 筆 in 笔序列[::-1]: if 筆.文.中.序号 - 偏移 <= 缠K.序号 <= 筆.武.中.序号: - # if 缠K in 筆.缠K序列[偏移:]: - return 筆 + if 筆.文.中.周期 == 缠K.周期 and 筆.文.中.标识 == 缠K.标识: + return 筆 return None @classmethod - def 自检(cls, 筆: 虚线, 观察员: "观察者") -> bool: + def _自检(cls, 筆: 虚线, 观察员: 观察者) -> bool: """校验笔的有效性:高/低点是否为实际极值 :param 筆: 待校验的笔 @@ -3310,15 +4430,15 @@ class 笔(object): 笔序列: List[虚线] = 观察员.笔序列 配置: 缠论配置 = 观察员.配置 基础序列 = 筆.获取缠K序列(观察员) - if 笔.获取缠K数量(基础序列, 笔序列, 配置) >= 配置.笔内元素数量: - if 筆.方向 is 相对方向.向下 and 筆.文.中 is 笔.实际高点(基础序列, False) and 筆.武.中 is 笔.实际低点(基础序列, 配置.笔内相同终点取舍): + if 笔._获取缠K数量(基础序列, 笔序列, 配置) >= 配置.笔内元素数量: + if 筆.方向 is 相对方向.向下 and 筆.文.中 is 笔._实际高点(基础序列, False) and 筆.武.中 is 笔._实际低点(基础序列, 配置.笔内相同终点取舍): return True - if 筆.方向 is 相对方向.向上 and 筆.文.中 is 笔.实际低点(基础序列, False) and 筆.武.中 is 笔.实际高点(基础序列, 配置.笔内相同终点取舍): + if 筆.方向 is 相对方向.向上 and 筆.文.中 is 笔._实际低点(基础序列, False) and 筆.武.中 is 笔._实际高点(基础序列, 配置.笔内相同终点取舍): return True return False @classmethod - def 获取所有停顿位置(cls, 筆: 虚线, 观察员: "观察者") -> List[虚线]: + def 获取所有停顿位置(cls, 筆: 虚线, 观察员: 观察者) -> List[虚线]: """获取笔内所有可能的停顿位置(用于背驰检测) :param 筆: 笔 @@ -3334,17 +4454,17 @@ class 笔(object): if K.分型 is 分型结构.顶 and 筆.方向 is 相对方向.向上: 当前笔 = 虚线.创建笔(文=文, 武=武) 当前笔.序号 = 筆.序号 - 笔.自检(当前笔, 观察员) and 笔序列.append(当前笔) + 笔._自检(当前笔, 观察员) and 笔序列.append(当前笔) if K.分型 is 分型结构.底 and 筆.方向 is 相对方向.向下: 当前笔 = 虚线.创建笔(文=文, 武=武) 当前笔.序号 = 筆.序号 - 笔.自检(当前笔, 观察员) and 笔序列.append(当前笔) + 笔._自检(当前笔, 观察员) and 笔序列.append(当前笔) return 笔序列 @classmethod - def 是否背驰过(cls, 当前筆: 虚线, 观察员: "观察者") -> List[缠论K线]: + def 是否背驰过(cls, 当前筆: 虚线, 观察员: 观察者) -> List[缠论K线]: """判断笔内是否发生过MACD趋向背驰 :param 当前筆: 笔 @@ -3360,23 +4480,32 @@ class 笔(object): return 结果 -class 线段特征(list): - """线段特征 — list 子类,持有一组同向虚线(笔),是线段划分的中间结构。 +class 线段特征: + """持有一组同向虚线(笔),是线段划分的中间结构。 - 继承 list,元素为虚线。特征序列用于线段划分算法中的包含处理。 + 特征序列用于线段划分算法中的包含处理和缺口判断。 :ivar 序号: 序号 :ivar 标识: 标识 :ivar 线段方向: 线段运行方向(特征序列方向为其翻转) + :ivar 基础序列: 虚线序列 :ivar 文: 第一个元素的起点分型(根据方向选取极值) :ivar 武: 最后一个元素的终点分型(根据方向选取极值) :ivar 高: 最高价 :ivar 低: 最低价 :ivar 方向: 特征序列方向(线段方向的翻转) :ivar 图表标题: 图表标题 + + 主要方法: + :meth:`新建` — 从虚线序列创建单个线段特征 + :meth:`静态分析` — 将虚线序列静态划分为线段特征列表(含包含处理) + :meth:`获取分型序列` — 从特征序列中提取分型序列 + + 注意: + Rust 绑定层仅导出 ``静态分析``,字段除了 ``标识`` 其余仅可读。 """ - __slots__ = ["序号", "标识", "线段方向"] + __slots__ = ["序号", "标识", "线段方向", "基础序列"] def __init__(self, 标识: str, 基础序列: List[虚线], 线段方向: 相对方向): """ @@ -3384,7 +4513,7 @@ class 线段特征(list): :param 基础序列: 基础虚线列表 :param 线段方向: 线段运行方向 """ - super().__init__(基础序列) + self.基础序列 = 基础序列[:] self.序号 = 0 self.标识: str = 标识 self.线段方向: 相对方向 = 线段方向 @@ -3408,30 +4537,36 @@ class 线段特征(list): @property def 文(self) -> 分型: - """起点分型(向上线段取高高中的最大者,向下线段取低低中的最小者)""" + """线段特征的起点分型。 + + :return: 向上取高高中的最大分型,向下取低低中的最小分型 + """ if self.线段方向 is 相对方向.向上: # 取高高 return max( - [线.文 for 线 in self], - key=lambda o: (o.中.分型特征值, o.中.时间戳), + [线.文 for 线 in self.基础序列], + key=lambda o: (o.分型特征值, o.时间戳), ) else: return min( - [线.文 for 线 in self], - key=lambda o: (o.中.分型特征值, -o.中.时间戳.timestamp()), + [线.文 for 线 in self.基础序列], + key=lambda o: (o.分型特征值, -o.时间戳.timestamp()), ) @property def 武(self) -> 分型: - """终点分型(向上线段取高高中的最大者,向下线段取低低中的最小者)""" + """线段特征的终点分型。 + + :return: 向上取高高中的最大分型,向下取低低中的最小分型 + """ if self.线段方向 is 相对方向.向上: return max( - [线.武 for 线 in self], - key=lambda o: (o.中.分型特征值, o.中.时间戳), + [线.武 for 线 in self.基础序列], + key=lambda o: (o.分型特征值, o.时间戳), ) else: return min( - [线.武 for 线 in self], - key=lambda o: (o.中.分型特征值, -o.中.时间戳.timestamp()), + [线.武 for 线 in self.基础序列], + key=lambda o: (o.分型特征值, -o.时间戳.timestamp()), ) @property @@ -3439,14 +4574,14 @@ class 线段特征(list): """ :return: 文和武中分型特征值的较大者 """ - return max([self.文, self.武], key=lambda fx: fx.中.分型特征值).中.分型特征值 + return max([self.文, self.武], key=lambda fx: fx.分型特征值).分型特征值 @property def 低(self) -> float: """ :return: 文和武中分型特征值的较小者 """ - return min([self.文, self.武], key=lambda fx: fx.中.分型特征值).中.分型特征值 + return min([self.文, self.武], key=lambda fx: fx.分型特征值).分型特征值 @property def 方向(self) -> 相对方向: @@ -3455,26 +4590,26 @@ class 线段特征(list): """ return self.线段方向.翻转() - def 添加(self, 待添加虚线: Union[虚线]): + def _添加(self, 待添加虚线: Union[虚线]): """ :param 待添加虚线: 待添加的虚线 :raises ValueError: 方向不匹配时抛出 """ if 待添加虚线.方向 == self.线段方向: raise ValueError("方向不匹配", self.线段方向, 待添加虚线, self) - self.append(待添加虚线) + self.基础序列.append(待添加虚线) - def 删除(self, 待删除虚线: Union[虚线]): + def _删除(self, 待删除虚线: Union[虚线]): """ :param 待删除虚线: 待删除的虚线 :raises ValueError: 方向不匹配时抛出 """ if 待删除虚线.方向 == self.方向: raise ValueError("方向不匹配", self.线段方向, 待删除虚线, self) - self.remove(待删除虚线) + self.基础序列.remove(待删除虚线) @classmethod - def 新建(cls, 虚线序列: List[虚线], 线段方向: 相对方向) -> "线段特征": + def 新建(cls, 虚线序列: List[虚线], 线段方向: 相对方向) -> 线段特征: """ :param 虚线序列: 基础虚线列表 :param 线段方向: 线段方向 @@ -3483,7 +4618,7 @@ class 线段特征(list): return 线段特征(标识=f"特征<{虚线序列[0].__class__.__name__}>", 基础序列=虚线序列, 线段方向=线段方向) @classmethod - def 静态分析(cls, 虚线序列: List[虚线], 线段方向: 相对方向, 四象: str, 是否忽视: bool = False) -> List["线段特征"]: + def 静态分析(cls, 虚线序列: List[虚线], 线段方向: 相对方向, 四象: str, 是否忽视: bool = False) -> List[线段特征]: """静态分析虚线序列,生成特征序列 :param 虚线序列: 笔/虚线列表 @@ -3514,8 +4649,8 @@ class 线段特征(list): # 条件:向上线段形成顶分型且当前虚线高点高于中,或向下线段形成底分型且当前虚线低点低于中 if (线段方向 is 相对方向.向上 and 结构 is 分型结构.顶 and 当前虚线.高 > 中.高) or (线段方向 is 相对方向.向下 and 结构 is 分型结构.底 and 当前虚线.低 < 中.低): - 小号虚线 = min(中, key=lambda o: o.序号) - 大号虚线 = max(右, key=lambda o: o.序号) + 小号虚线 = min(中.基础序列, key=lambda o: o.序号) + 大号虚线 = max(右.基础序列, key=lambda o: o.序号) fake = 虚线.创建笔(文=小号虚线.文, 武=大号虚线.武, 有效性=False) 特征序列.pop() 特征序列[-1] = 线段特征.新建([fake], 线段方向) @@ -3531,7 +4666,7 @@ class 线段特征(list): # 特征序列非空:检查与最后一个特征序列的方向关系 之前线段特征 = 特征序列[-1] if 相对方向.分析(之前线段特征.高, 之前线段特征.低, 当前虚线.高, 当前虚线.低) in 需要被合并方向序列: - 之前线段特征.添加(当前虚线) + 之前线段特征._添加(当前虚线) else: 特征序列.append(线段特征.新建([当前虚线], 线段方向)) @@ -3555,7 +4690,7 @@ class 线段特征(list): class 特征分型: """特征分型 — 由左右中三个线段特征构成的顶/底分型,用于线段划分算法。 - + 注意: Rust 绑定层不导! :ivar 左: 左侧特征序列 :ivar 中: 中间特征序列 :ivar 右: 右侧特征序列 @@ -3583,22 +4718,34 @@ class 特征分型: return f"特征分型<{self.结构}, {self.中}>" -class 线段(object): - """线段 — 纯静态方法容器,提供线段划分算法的所有函数。 +class 线段: + """纯静态方法容器,提供线段划分算法的所有函数。 - 主要方法: - 检查线段破坏(前一线段, 当前线段) — 检查线段是否被破坏 - 获取线段特征序列(笔序列, 线段方向, 需要被合并方向序列) — 提取特征序列 - 线段有缺口(特征序列, 线段方向, 当前分型序列, 特征分型序列) — 判断线段是否有缺口 - 判断新线段(笔序列) — 判断是否形成新线段 - 静态分析(观察员) — 静态线段分析 - 生成线段序列(笔序列, 观察员) — 从笔序列生成线段序列 + 核心方法: + :meth:`分析` — 流式分析,增量识别线段(主入口) + :meth:`扩展分析` — 流式扩展线段分析(基于线段生成更高级别线段) + + 特征序列: + :meth:`特征分型终结` — 判断线段是否被特征分型终结 + :meth:`特征序列状态` — 返回特征序列的缺口/突破/包含状态 + :meth:`获取缺口` — 获取线段特征序列之间的缺口 + :meth:`四象` — 判断线段的四象模式(涨跌盘等) + + 序列操作: + :meth:`分割序列` — 按中枢将线段基础序列分割为进入段/中枢段/离开段 + :meth:`查找贯穿伤` — 查找贯穿中枢的线段 + :meth:`获取内部中枢序列` — 获取线段内部的笔中枢/段中枢/扩展中枢 + + 辅助判断: + :meth:`判断线段内部是否背驰` — 判断线段内部是否存在背驰 + :meth:`获取所有停顿位置` — 获取线段内所有停顿位置(买卖点候选) + :meth:`是否背驰过` — 返回线段内背驰对应的缠K列表 """ __slots__ = [] @classmethod - def 添加虚线(cls, 段: 虚线, 筆: 虚线): + def _添加虚线(cls, 段: 虚线, 筆: 虚线): """向线段中添加一笔 :param 段: 线段 @@ -3613,32 +4760,33 @@ class 线段(object): 段.基础序列.append(筆) @classmethod - def 武斗(cls, 段: 虚线, 武: 分型, 行号: int): + def _武斗(cls, 段: 虚线, 武: 分型, 行号: int): """更新线段的终点分型(武) :param 段: 线段 :param 武: 新的终点分型 :param 行号: 调用行号(用于调试) """ - # print(f"{段.标识}.武斗[{行号}], ", 武) + # logger.warning(f"{段.标识}.武斗[{行号}], ", 武) if 段.武 is 武: - # print(f"{段.标识}.武斗[{行号}], 相同") + # logger.warning(f"{段.标识}.武斗[{行号}], 相同") return if 段.武.分型特征值 == 武.分型特征值 and 段.武.时间戳 != 武.时间戳: - print(f"{段.标识}.武斗[{行号}], 发现特征值相等但时间戳不同", 段.武, 武) + logger.warning(f"{段.标识}.武斗[{行号}], 发现特征值相等但时间戳不同, {段.武}, {武}") assert 段.文.结构 is not 武.结构, (f"文武结构相同 {行号}", 段.文, 武) if 武.右 is not None and 分型结构.分析(武.左, 武.中, 武.右) is not 武.结构: raise RuntimeError(分型结构.分析(武.左, 武.中, 武.右), 武.结构) if 段.方向 is 相对方向.向上: if 武.分型特征值 < 段.文.分型特征值: raise RuntimeError(f"向上{段.标识}, 结束点 小于 起点", 段.标识, 段.文, 武) - # if max([段.武, 武], key=lambda k: k.分型特征值) is not 武 and 段.模式 == "文武": - # pass # print(colored(f"{段.标识}.武斗[{行号}] 出现回退 从 {段.武} ==>>> {武}", "red", "on_green")) # raise RuntimeError(段.武, 武) + + if 段.武.分型特征值 > 武.分型特征值 and 段.模式 == "文武": + logger.warning(f"{段.标识}.武斗[{行号}] 出现回退 从 {段.武} ==>>> {武}") # raise RuntimeError(段.武, 武) else: if 武.分型特征值 > 段.文.分型特征值: raise RuntimeError(f"向下{段.标识}, 结束点 大于 起点", 段.标识, 段.文, 武) - # if min([段.武, 武], key=lambda k: k.分型特征值) is not 武 and 段.模式 == "文武": - # pass # print(colored(f"{段.标识}.武斗[{行号}] 出现回退 从 {段.武} ==>>> {武}", "red", "on_green")) # raise RuntimeError(段.武, 武) + if 段.武.分型特征值 < 武.分型特征值 and 段.模式 == "文武": + logger.warning(f"{段.标识}.武斗[{行号}] 出现回退 从 {段.武} ==>>> {武}") # raise RuntimeError(段.武, 武) 段.武 = 武 @classmethod @@ -3700,14 +4848,14 @@ class 线段(object): return "小阳" if 段.方向 is 相对方向.向上 else "少阴" @classmethod - def 设置特征序列(cls, 段: 虚线, 序列, 行号): + def _设置特征序列(cls, 段: 虚线, 序列, 行号): """设置特征序列 :param 段: 线段 :param 序列: 特征序列三元组 (左,中,右) :param 行号: 调用行号 """ - # print(f"线段.设置特征序列[{行号}]", self) + # logger.warning(f"线段.设置特征序列[{行号}]", self) if 段.模式 != "文武": return @@ -3718,11 +4866,11 @@ class 线段(object): 段.特征序列 = [左, 中, 右] if 右 is not None: 基础序列 = [] - if 右[-1] not in 段.基础序列: + if 右.基础序列[-1] not in 段.基础序列: raise ValueError() for 元素 in 段.基础序列: 基础序列.append(元素) - if 元素 is 右[-1]: + if 元素 is 右.基础序列[-1]: break if (len(基础序列) >= 6) and (len(基础序列) % 2 == 0): @@ -3733,7 +4881,7 @@ class 线段(object): pass @classmethod - def 刷新特征序列(cls, 段: 虚线, 配置: 缠论配置): + def _刷新特征序列(cls, 段: 虚线, 配置: 缠论配置): """刷新特征序列 :param 段: 线段 @@ -3749,16 +4897,16 @@ class 线段(object): if len(特征序列) >= 3: 分型序列 = 线段特征.获取分型序列(特征序列) if (段.方向 is 相对方向.向上 and 分型序列[-1].结构 is 分型结构.顶) or (段.方向 is 相对方向.向下 and 分型序列[-1].结构 is 分型结构.底): - 线段.设置特征序列(段, [分型序列[-1].左, 分型序列[-1].中, 分型序列[-1].右], sys._getframe().f_lineno) + 线段._设置特征序列(段, [分型序列[-1].左, 分型序列[-1].中, 分型序列[-1].右], sys._getframe().f_lineno) else: - 线段.设置特征序列(段, [特征序列[-2], 特征序列[-1], None], sys._getframe().f_lineno) + 线段._设置特征序列(段, [特征序列[-2], 特征序列[-1], None], sys._getframe().f_lineno) else: 特征序列.extend([None] * (3 - len(特征序列))) - 线段.设置特征序列(段, 特征序列, sys._getframe().f_lineno) + 线段._设置特征序列(段, 特征序列, sys._getframe().f_lineno) @classmethod - def 分割序列(cls, 段: 虚线, 所属中枢: Optional["中枢"] = None) -> Tuple[List[虚线], List[虚线], List[虚线], Optional[虚线]]: + def 分割序列(cls, 段: 虚线, 所属中枢: Optional[中枢] = None) -> Tuple[List[虚线], List[虚线], List[虚线], Optional[虚线]]: """将线段基础序列分割为前/后/第三买卖/贯穿伤四部分 :param 段: 线段 @@ -3767,8 +4915,7 @@ class 线段(object): """ if 段.模式 != "文武": return 段.基础序列[:], [], [], None - if len(段.基础序列) == 0: - print(段.标识, 段.序号) + assert 段.基础序列[0].文 is 段.文, (段.基础序列[0].文, 段.文) 前: List[虚线] = [] 后: List[虚线] = [] @@ -3836,7 +4983,7 @@ class 线段(object): return 前, 后, 第三买卖线, 贯穿伤 @classmethod - def 刷新(cls, 段: 虚线, 配置: 缠论配置): + def _刷新(cls, 段: 虚线, 配置: 缠论配置): """刷新线段的特征序列和内部中枢序列 :param 段: 线段 @@ -3845,36 +4992,36 @@ class 线段(object): if 段.模式 != "文武": return if not len(段.基础序列): - print(" 线段.刷新 基础序列为空") + logger.warning(" 线段.刷新 基础序列为空") return - 线段.刷新特征序列(段, 配置) + 线段._刷新特征序列(段, 配置) 有效特征序列 = [特征 for 特征 in 段.特征序列 if 特征 is not None] if len(有效特征序列) == 3: - 线段.武斗(段, 段.特征序列[1].文, sys._getframe().f_lineno) + 线段._武斗(段, 段.特征序列[1].文, sys._getframe().f_lineno) elif len(有效特征序列) >= 1: 最近特征 = 有效特征序列[-1] - if 最近特征[-1] not in 段.基础序列: - 特征后一笔 = 笔.以武会友(段.基础序列, 最近特征[-1].武) + if 最近特征.基础序列[-1] not in 段.基础序列: + 特征后一笔 = 笔.以武会友(段.基础序列, 最近特征.基础序列[-1].武) else: - 特征后一笔 = 最近特征[-1] + 特征后一笔 = 最近特征.基础序列[-1] if 特征后一笔 is not None: 序号 = 段.基础序列.index(特征后一笔) if 序号 < len(段.基础序列) - 1: 下一笔 = 段.基础序列[序号 + 1] if (段.方向 is 相对方向.向上 and 段.高 <= 下一笔.高) or (段.方向 is 相对方向.向下 and 段.低 >= 下一笔.低): - 线段.武斗(段, 下一笔.武, sys._getframe().f_lineno) + 线段._武斗(段, 下一笔.武, sys._getframe().f_lineno) else: - print(" 线段.刷新 特征后一笔 = None, ", 段, 有效特征序列) + logger.warning(f" 线段.刷新 特征后一笔 = None, {段}, {有效特征序列}") else: raise RuntimeError(len(有效特征序列)) 线段.获取内部中枢序列(段, 配置) @classmethod - def 序列重置(cls, 段: 虚线, 序列: Sequence): + def _序列重置(cls, 段: 虚线, 序列: Sequence): """序列重置 :param 段: 线段 @@ -3909,7 +5056,7 @@ class 线段(object): return None @classmethod - def 获取内部中枢序列(cls, 段: 虚线, 配置: 缠论配置) -> Tuple[List["中枢"], List["中枢"], List["中枢"]]: + def 获取内部中枢序列(cls, 段: 虚线, 配置: 缠论配置) -> Tuple[List[中枢], List[中枢], List[中枢]]: """获取内部中枢序列 :param 段: 线段 @@ -3927,7 +5074,7 @@ class 线段(object): return 段.虚_中枢序列, 段.实_中枢序列, 段.合_中枢序列 # 阴 阳 合 @classmethod - def 基础判断(cls, 左: 虚线, 中: 虚线, 右: 虚线, 关系序列: List[相对方向]) -> bool: + def _基础判断(cls, 左: 虚线, 中: 虚线, 右: 虚线, 关系序列: List[相对方向]) -> bool: """ 连续三笔且重叠 @@ -3991,7 +5138,7 @@ class 线段(object): 待添加线段.前一缺口 = None 线段序列.append(待添加线段) - # print(f"线段._向序列中添加[{行号}]", 待添加虚线) + # logger.warning(f"线段._向序列中添加[{行号}]", 待添加虚线) @classmethod def _弹出线段(cls, 线段序列: List[虚线], 待弹出线段: 虚线, 配置: 缠论配置, 行号: str): @@ -4013,7 +5160,7 @@ class 线段(object): if 右 is not None: 结构 = 分型结构.分析(左, 中, 右, True, True) if 结构 in (分型结构.顶, 分型结构.底) and not 相对方向.分析(左.高, 左.低, 中.高, 中.低).是否缺口(): - print(colored(f"[警告<{行号}>]:", "yellow"), colored("线段._从序列中删除 发现分型完毕, 且特征序列无缺口", "red"), 待弹出线段) + logger.warning(f"{colored(f'[警告<{行号}>]:', 'yellow')} {colored('线段._从序列中删除 发现分型完毕, 且特征序列无缺口', 'red')} {待弹出线段}") 线段序列.pop() 待弹出线段.前一结束位置 = None @@ -4023,14 +5170,15 @@ class 线段(object): @classmethod def _缺口突破(cls, 线段序列: List[虚线], 配置: 缠论配置, 层级: int) -> bool: - """内部方法:处理缺口突破修正 - + """内部方法:处理缺口突破修正, 正常修正 + 除此之外的修正皆为短路修正,此修正为正常处理 :param 线段序列: 线段列表 :param 配置: 缠论配置 :param 层级: 递归深度 :return: 是否执行了修正 """ 当前线段 = 线段序列[-1] + assert 当前线段.基础序列, "缺口突破: 当前线段.基础序列为空!" 当前虚线: 虚线 = 当前线段.基础序列[-1] 四象 = 线段.四象(当前线段) 同向 = 当前虚线.方向 is 当前线段.方向 @@ -4051,16 +5199,23 @@ class 线段(object): if not ((四象 == "老阳" and 当前虚线.低 < 当前线段.低) or (四象 == "老阴" and 当前虚线.高 > 当前线段.高)): return False + # 已被修正 + if 线段序列[-2].短路修正: + return False + # 执行修正 序列 = 当前线段.基础序列[:] 线段._弹出线段(线段序列, 当前线段, 配置, f"{sys._getframe().f_lineno}, {层级}") + assert 线段序列, "缺口突破: 线段序列为第二次空!" 当前线段 = 线段序列[-1] + assert 当前线段.特征序列[2] is not None 当前线段基础序列 = 线段.分割序列(当前线段)[0] + assert 当前线段基础序列[-1].之后是(序列[0]), "缺口突破: 子序列不连续!" 当前线段基础序列.extend(序列) 当前线段.基础序列[:] = 当前线段基础序列[:] - 线段.刷新(当前线段, 配置) + 线段._刷新(当前线段, 配置) return True @classmethod @@ -4072,6 +5227,7 @@ class 线段(object): :param 层级: 递归深度 :return: 是否执行了修正 """ + assert 线段序列, "非缺口下穿刺: 线段序列为空!" 当前线段 = 线段序列[-1] 四象 = 线段.四象(当前线段) @@ -4084,6 +5240,7 @@ class 线段(object): if not 贯穿伤: return False + assert 贯穿伤 in 当前线段.基础序列, "非缺口下穿刺: 贯穿伤不在基础序列中!" # 切割基础序列 基础序列 = 当前线段.基础序列[当前线段.基础序列.index(贯穿伤) :] @@ -4098,17 +5255,20 @@ class 线段(object): return False # 执行修正 - print(colored(f"[警告<{sys._getframe().f_lineno}, {层级}>]:", "yellow"), colored("线段.修复贯穿伤", "red"), 贯穿伤, 基础序列) # 异常弹出 + logger.warning(f"{colored(f'[警告<{sys._getframe().f_lineno}, {层级}>]:', 'yellow')} {colored('线段.修复贯穿伤', 'red')} {贯穿伤} {基础序列}") # 异常弹出 基础序列 = 当前线段.基础序列[:] 线段._弹出线段(线段序列, 当前线段, 配置, f"{sys._getframe().f_lineno}, {层级}") + assert 线段序列, "非缺口下穿刺: 第二次线段序列为空!" 当前线段 = 线段序列[-1] 当前线段.特征序列[2] = None + assert 当前线段.基础序列[-1] in 基础序列, "非缺口下穿刺: 当前线段.基础序列[-1] 不在 基础序列中!" for 临时虚线 in 基础序列[基础序列.index(当前线段.基础序列[-1]) + 1 :]: - 线段.添加虚线(当前线段, 临时虚线) - 线段.刷新(当前线段, 配置) + 线段._添加虚线(当前线段, 临时虚线) + 线段._刷新(当前线段, 配置) + 当前线段.短路修正 = True - if 当前线段.特征序列[2]: + if 当前线段.特征序列[2] is not None: 段 = 虚线.创建线段([左, 中, 右]) 线段._添加线段(线段序列, 段, 配置, f"{sys._getframe().f_lineno}, {层级}") 段.特征序列[0] = 线段特征.新建([中], 段.方向) @@ -4119,11 +5279,14 @@ class 线段(object): def _缺口后紧急修正(cls, 线段序列: List[虚线], 配置: 缠论配置, 层级: int) -> bool: """内部方法:处理缺口后紧急修正 + 执行逻辑: 在缺口 + :param 线段序列: 线段列表 :param 配置: 缠论配置 :param 层级: 递归深度 :return: 是否执行了修正 """ + assert 线段序列, "缺口后紧急修正: 线段序列为空!" 当前线段 = 线段序列[-1] 四象 = 线段.四象(当前线段) @@ -4159,12 +5322,15 @@ class 线段(object): @classmethod def _修正(cls, 线段序列: List[虚线], 配置: 缠论配置, 层级: int) -> bool: """内部方法:处理线段修正 + 执行逻辑: 当线段第二特征序列超过两次合并后且尾部元素出现与当前线段相同方向时执行修正, + 生成两个线段,生成的线段符合一段被一段破坏的基础逻辑 :param 线段序列: 线段列表 :param 配置: 缠论配置 :param 层级: 递归深度 :return: 是否执行了修正 """ + assert 线段序列, "修正: 线段序列为空!" 当前线段 = 线段序列[-1] # 条件1:配置允许修正且当前线段基础序列长度足够 @@ -4177,6 +5343,8 @@ class 线段(object): # 条件2:之后基础序列长度至少为6 if len(之后基础序列) < 6: return False + if len(之后基础序列) % 2 != 0: + return False # 取倒数第3和第1个元素 前, 后 = 之后基础序列[-3], 之后基础序列[-1] @@ -4205,27 +5373,34 @@ class 线段(object): @classmethod def 分析(cls, 笔序列: List[虚线], 线段序列: List[虚线], 配置: 缠论配置, 层级: int = 0, 关系序列=[相对方向.向上, 相对方向.向下]) -> None: - """线段划分核心递归算法 + """线段划分核心递归算法 — 从笔序列递归生成线段。 :param 笔序列: 笔列表 :param 线段序列: 线段列表(原地修改) :param 配置: 缠论配置 :param 层级: 递归深度 :param 关系序列: 允许的方向关系 + :return: None(结果写入线段序列) """ # 递归深度守卫 if 层级 > 256: - print("线段.分析 递归深度超出 256") + logger.warning("线段.分析 递归深度超出 256") return None # raise RuntimeError("线段分析 层级过深") + # if len(笔序列) < 3:return None + try: + 笔序列[2] + except IndexError: + return None + 线段递归分析 = 线段.分析 # -------------------- 1. 初始化第一个线段 -------------------- if not 线段序列: for i in range(1, len(笔序列) - 1): 左, 中, 右 = 笔序列[i - 1], 笔序列[i], 笔序列[i + 1] - if not 线段.基础判断(左, 中, 右, 关系序列): # FIXME 首个线段必须有明确方向 + if not 线段._基础判断(左, 中, 右, 关系序列): # FIXME 首个线段必须有明确方向 continue 段 = 虚线.创建线段([左, 中, 右]) 线段._添加线段(线段序列, 段, 配置, f"{sys._getframe().f_lineno}, {层级}") @@ -4246,7 +5421,7 @@ class 线段(object): # -------------------- 3. 确保当前线段有效 -------------------- 当前线段 = 线段序列[-1] - 线段.序列重置(当前线段, 笔序列) + 线段._序列重置(当前线段, 笔序列) if len(当前线段.基础序列) < 3: 线段._弹出线段(线段序列, 当前线段, 配置, f"{sys._getframe().f_lineno}, {层级}") @@ -4264,7 +5439,7 @@ class 线段(object): 新段.前一缺口 = None 当前线段 = 线段序列[-1] - 线段.刷新(当前线段, 配置) + 线段._刷新(当前线段, 配置) # -------------------- 5. 调用一次全局修正(不循环) -------------------- 线段._缺口突破(线段序列, 配置, 层级) @@ -4282,17 +5457,21 @@ class 线段(object): 当前线段 = 线段序列[-1] 四象 = 线段.四象(当前线段) - 线段.添加虚线(当前线段, 当前虚线) - 线段.刷新(当前线段, 配置) + 线段._添加虚线(当前线段, 当前虚线) + 线段._刷新(当前线段, 配置) # 依次尝试四种修正,任意一个成功则跳过后续处理 + 修正触发 = False if 线段._缺口突破(线段序列, 配置, 层级): - continue - if 线段._非缺口下穿刺(线段序列, 配置, 层级): - continue - if 线段._缺口后紧急修正(线段序列, 配置, 层级): - continue - if 线段._修正(线段序列, 配置, 层级): + 修正触发 = "缺口突破" + elif 线段._非缺口下穿刺(线段序列, 配置, 层级): + 修正触发 = "非缺口下穿刺" + elif 线段._缺口后紧急修正(线段序列, 配置, 层级): + 修正触发 = "缺口后紧急修正" + elif 线段._修正(线段序列, 配置, 层级): + 修正触发 = "修正" + if 修正触发: + logger.warning(f"分析.修正触发={修正触发}, 笔序列长度={len(笔序列)}, 线段序列长度={len(线段序列)}") continue # 无修正触发,且特征序列[2]已存在 → 创建新段 @@ -4309,24 +5488,24 @@ class 线段(object): if 新段.基础序列[-1] is not 当前虚线: if not 新段.基础序列[-1].之后是(当前虚线): return 线段递归分析(笔序列, 线段序列, 配置, 层级 + 1, 关系序列) - 线段.添加虚线(新段, 当前虚线) + 线段._添加虚线(新段, 当前虚线) - 线段.刷新(新段, 配置) + 线段._刷新(新段, 配置) return None @classmethod - def 武终(cls, 段: 虚线, 行号: int): + def _武终(cls, 段: 虚线, 行号: int): """武终 :param 段: 线段 :param 行号: 调用行号 """ if 段.模式 != "文武": - 线段.武斗(段, 段.基础序列[-1].武, 行号) + 线段._武斗(段, 段.基础序列[-1].武, 行号) @classmethod - def 验证序列(cls, 段: 虚线, 序列: Sequence): + def _验证序列(cls, 段: 虚线, 序列: Sequence): """验证序列 :param 段: 线段 @@ -4338,7 +5517,7 @@ class 线段(object): break if 基础序列: if not 基础序列[-1].之后是(元素): - print(" 线段._验证序列 数据不连续") + logger.warning(" 线段._验证序列 数据不连续") break 基础序列.append(元素) 段.基础序列[:] = 基础序列[:] @@ -4362,7 +5541,7 @@ class 线段(object): 待添加线段.序号 = 之前线段.序号 + 1 线段序列.append(待添加线段) - # print(f"线段._向序列中添加[{行号}]", 待添加线段) + # logger.warning(f"线段._向序列中添加[{行号}]", 待添加线段) @classmethod def _弹出扩展线段(cls, 线段序列: List[虚线], 待弹出线段: 虚线, 行号: int): @@ -4379,19 +5558,18 @@ class 线段(object): if 线段序列[-1] is 待弹出线段: drop = 线段序列.pop() 待弹出线段.有效性 = False - # print(f"线段._从序列中删除[{行号}]", 待弹出线段) + # logger.warning(f"线段._从序列中删除[{行号}]", 待弹出线段) return drop raise ValueError("线段._从序列中删除 弹出数据不在列表中", 待弹出线段) @classmethod def 扩展分析(cls, 虚线序列: List[虚线], 线段序列: List[虚线], 配置: 缠论配置) -> None: - """ - 即同级别分析 - 将笔看成线段 + """同级别分析 — 将虚线视为线段进行递归段划分。 :param 虚线序列: 基础虚线列表 :param 线段序列: 扩展线段列表(原地修改) :param 配置: 缠论配置 + :return: None(结果写入线段序列) """ if not 虚线序列: return None @@ -4417,7 +5595,7 @@ class 线段(object): return None 当前线段 = 线段序列[-1] - 线段.验证序列(当前线段, 虚线序列) + 线段._验证序列(当前线段, 虚线序列) if len(当前线段.基础序列) < 3: 线段._弹出扩展线段(线段序列, 当前线段, sys._getframe().f_lineno) return 线段递归扩展分析(虚线序列, 线段序列, 配置) @@ -4425,13 +5603,13 @@ class 线段(object): if not 配置.扩展线段_当下分析: 左, 中, 右 = 当前线段.基础序列[:3] if not 相对方向.分析(左.高, 左.低, 右.高, 右.低).是否缺口(): - 当前线段.基础序列[:] = 当前线段.笔序列[:3] - 线段.武终(当前线段, sys._getframe().f_lineno) + 当前线段.基础序列[:] = 当前线段.基础序列[:3] + 线段._武终(当前线段, sys._getframe().f_lineno) else: 线段._弹出扩展线段(线段序列, 当前线段, sys._getframe().f_lineno) return 线段递归扩展分析(虚线序列, 线段序列, 配置) - 线段.武终(当前线段, sys._getframe().f_lineno) + 线段._武终(当前线段, sys._getframe().f_lineno) if 当前线段.基础序列[-1].序号 + 3 > 虚线序列[-1].序号: return None @@ -4443,9 +5621,9 @@ class 线段(object): 左, 中, 右 = 虚线序列[i - 1], 虚线序列[i], 虚线序列[i + 1] 相对关系 = 相对方向.分析(左.高, 左.低, 右.高, 右.低) if 相对关系.是否缺口(): - 线段.添加虚线(当前线段, 左) - 线段.添加虚线(当前线段, 中) - 线段.武终(当前线段, sys._getframe().f_lineno) + 线段._添加虚线(当前线段, 左) + 线段._添加虚线(当前线段, 中) + 线段._武终(当前线段, sys._getframe().f_lineno) continue if 左 in 当前线段.基础序列: @@ -4456,7 +5634,6 @@ class 线段(object): return 线段递归扩展分析(虚线序列, 线段序列, 配置) @classmethod - @lru_cache(maxsize=128) def 判断线段内部是否背驰(cls, 当前段: 虚线, 观察员: 观察者) -> bool: """判断线段内部是否发生背驰(基于内部中枢和MACD) @@ -4527,7 +5704,7 @@ class 线段(object): return 背驰 or 盘整背驰 @classmethod - def 获取所有停顿位置(cls, 段: 虚线, 观察员: "观察者") -> List[虚线]: + def 获取所有停顿位置(cls, 段: 虚线, 观察员: 观察者) -> List[虚线]: """获取所有停顿位置 :param 段: 线段 @@ -4555,7 +5732,7 @@ class 线段(object): if 线段序列 and 线段序列[-1].武 is not 当前停顿 and len(线段序列[-1].基础序列) % 2 == 1: 新段 = 虚线.创建线段(线段序列[-1].基础序列[:]) 新段.序号 = self.序号 - 线段.刷新(新段, 观察员.配置) + 线段._刷新(新段, 观察员.配置) if 新段.方向 is self.方向: 结果.append(新段) 当前停顿 = 线段序列[-1].武 @@ -4567,7 +5744,7 @@ class 线段(object): return 结果 @classmethod - def 是否背驰过(cls, 当前段: 虚线, 观察员: "观察者") -> List[缠论K线]: + def 是否背驰过(cls, 当前段: 虚线, 观察员: 观察者) -> List[缠论K线]: """判断线段内是否发生过背驰(遍历所有停顿位置) :param 当前段: 线段 @@ -4584,8 +5761,8 @@ class 线段(object): return 结果 -class 中枢(object): - """中枢 — 三段虚线重叠区间构成的价格中枢,支持延伸和扩展。 +class 中枢: + """三段虚线重叠区间构成的价格中枢,支持延伸和扩展。 :ivar 序号: 序号 :ivar 标识: 标识 @@ -4601,6 +5778,22 @@ class 中枢(object): :ivar 武: 终点分型 :ivar 方向: 中枢方向(首条虚线的方向翻转) :ivar 离开段: 最后一条虚线 + + 构造方法: + :meth:`创建` — 从左右中三条虚线构造中枢 + :meth:`从序列中获取中枢` — 从虚线序列中提取第一个中枢 + + 流式分析: + :meth:`分析` — 流式分析,增量识别中枢(主入口) + :meth:`基础检查` — 检查三条虚线是否满足中枢重叠条件 + + 实例方法: + :meth:`获取序列` — 获取中枢包含的所有虚线 + :meth:`获取扩展中枢` — 获取中枢的扩展中枢序列 + :meth:`完整性` — 检查中枢虚实序列的完整性 + :meth:`获取数据文本` — 返回中枢关键数据的文本表示 + :meth:`当前状态` — 返回中枢当前状态描述 + :meth:`设置第三买卖线` — 设置第三类买卖点关联虚线 """ __slots__ = ["序号", "标识", "级别", "基础序列", "第三买卖线", "本级_第三买卖线"] @@ -4619,7 +5812,7 @@ class 中枢(object): self.第三买卖线: Optional[虚线] = None self.本级_第三买卖线: Optional[虚线] = None - def 添加虚线(self, 实线: 虚线): + def _添加虚线(self, 实线: 虚线): """向中枢添加新虚线(延伸),重置第三买卖线 :param 实线: 新虚线 @@ -4674,8 +5867,6 @@ class 中枢(object): """ :return: 全区间最高价 """ - if len(self.基础序列) > 3: - return max(self.基础序列, key=lambda o: o.高).高 return max(self.基础序列, key=lambda o: o.高).高 @property @@ -4683,8 +5874,6 @@ class 中枢(object): """ :return: 全区间最低价 """ - if len(self.基础序列) > 3: - return min(self.基础序列, key=lambda o: o.低).低 return min(self.基础序列, key=lambda o: o.低).低 @property @@ -4748,11 +5937,10 @@ class 中枢(object): 线段.扩展分析(self.基础序列, 扩展线段, 配置) 中枢.分析(扩展线段, 扩展中枢, False, f"{self.标识}_扩展中枢_") - def 校验合法性(self, 序列: Sequence[虚线], 中枢序列: List["中枢"]) -> bool: + def _校验合法性(self, 序列: Sequence[虚线]) -> bool: """校验当前中枢在给定序列中是否仍然合法 :param 序列: 基础虚线序列 - :param 中枢序列: 中枢列表 :return: 合法为True,不合法会原地裁剪基础序列 """ 有效序列 = self.基础序列[:] @@ -4801,7 +5989,7 @@ class 中枢(object): self.设置第三买卖线(None) else: if not 相对方向.分析(self.高, self.低, self.第三买卖线.高, self.第三买卖线.低).是否缺口(): - self.添加虚线(self.第三买卖线) + self._添加虚线(self.第三买卖线) self.设置第三买卖线(None) else: @@ -4866,7 +6054,7 @@ class 中枢(object): return 相对方向.分析(左.高, 左.低, 右.高, 右.低) in (相对方向.向下, 相对方向.向上, 相对方向.顺, 相对方向.逆, 相对方向.同) @classmethod - def 创建(cls, 左: 虚线, 中: 虚线, 右: 虚线, 级别: int, 标识: str = "") -> "中枢": + def 创建(cls, 左: 虚线, 中: 虚线, 右: 虚线, 级别: int, 标识: str = "") -> 中枢: """从三条连续且重叠的虚线创建中枢 :param 左: 左侧虚线 @@ -4885,7 +6073,7 @@ class 中枢(object): ) @classmethod - def 从序列中获取中枢(cls, 虚线序列: Sequence[虚线], 起始方向: 相对方向, 标识: str) -> Optional["中枢"]: + def 从序列中获取中枢(cls, 虚线序列: Sequence[虚线], 起始方向: 相对方向, 标识: str) -> Optional[中枢]: """从虚线序列中按起始方向查找第一个中枢 :param 虚线序列: 虚线列表 @@ -4905,7 +6093,7 @@ class 中枢(object): return None @classmethod - def 向中枢序列尾部添加(cls, 中枢序列: List["中枢"], 待添加中枢: "中枢"): + def _向中枢序列尾部添加(cls, 中枢序列: List[中枢], 待添加中枢: 中枢): """ :param 中枢序列: 中枢列表 :param 待添加中枢: 待添加的中枢 @@ -4917,7 +6105,7 @@ class 中枢(object): 中枢序列.append(待添加中枢) @classmethod - def 从中枢序列尾部弹出(cls, 中枢序列: List["中枢"], 待弹出中枢: "中枢") -> Optional["中枢"]: + def _从中枢序列尾部弹出(cls, 中枢序列: List[中枢], 待弹出中枢: 中枢) -> Optional[中枢]: """ :param 中枢序列: 中枢列表 :param 待弹出中枢: 待弹出的中枢 @@ -4930,14 +6118,15 @@ class 中枢(object): return None @classmethod - def 分析(cls, 虚线序列: Sequence[虚线], 中枢序列: List["中枢"], 跳过首部: bool = True, 标识: str = "", 层级: int = 0) -> None: - """中枢识别核心递归算法 + def 分析(cls, 虚线序列: Sequence[虚线], 中枢序列: List[中枢], 跳过首部: bool = True, 标识: str = "", 层级: int = 0) -> None: + """中枢识别核心递归算法 — 从虚线序列递归生成中枢。 :param 虚线序列: 基础虚线列表 :param 中枢序列: 中枢列表(原地修改) - :param 跳过首部: True: 跳过首元素中枢 + :param 跳过首部: True 时跳过首元素中枢 :param 标识: 中枢标识前缀 :param 层级: 递归深度 + :return: None(结果写入中枢序列) """ if len(虚线序列) < 3: return None @@ -4959,15 +6148,15 @@ class 中枢(object): if 同向相对关系.是否向下() and 左.方向.是否向下(): continue - 中枢.向中枢序列尾部添加(中枢序列, 新中枢) + 中枢._向中枢序列尾部添加(中枢序列, 新中枢) return 中枢递归分析(虚线序列, 中枢序列, 跳过首部, 标识, 层级 + 1) return None 当前中枢 = 中枢序列[-1] - if not 当前中枢.校验合法性(虚线序列, 中枢序列): - 中枢.从中枢序列尾部弹出(中枢序列, 当前中枢) + if not 当前中枢._校验合法性(虚线序列): + 中枢._从中枢序列尾部弹出(中枢序列, 当前中枢) return 中枢递归分析(虚线序列, 中枢序列, 跳过首部, 标识, 层级 + 1) 序号 = 虚线序列.index(当前中枢.基础序列[-1]) + 1 @@ -4983,7 +6172,7 @@ class 中枢(object): else: if not 基础序列: assert 当前中枢.基础序列[-1].之后是(当前虚线), (当前中枢.基础序列[-1], 当前虚线) - 当前中枢.添加虚线(当前虚线) + 当前中枢._添加虚线(当前虚线) else: 基础序列.append(当前虚线) @@ -4992,22 +6181,40 @@ class 中枢(object): if 新中枢 is None: 基础序列.pop(0) else: - 中枢.向中枢序列尾部添加(中枢序列, 新中枢) + 中枢._向中枢序列尾部添加(中枢序列, 新中枢) 当前中枢 = 新中枢 基础序列 = [] return None class 观察者: - """观察者 — 单周期缠论分析器,接收K线流式输入并逐层计算所有层级序列。 + """单周期缠论分析器,接收K线流式输入并逐层计算所有层级序列。 - 核心入口为 增加原始K线(普K),每收到一根新K线就增量更新: + 核心入口为 :meth:`增加原始K线`,每收到一根新K线就增量更新: 原始K线 → 缠论K线(包含处理+合并)→ 分型 → 笔 → 线段 → 中枢 → 买卖点 :ivar 标识: "{符号}:{周期}" - :ivar 当前K线: 最后一根原始K线 - :ivar 当前缠K: 最后一根缠论K线 - :ivar 普通K线序列 / 缠论K线序列 / 分型序列 / 笔序列 / 线段序列 / 中枢序列 等 + :ivar 当前K线: 最后一根原始K线(只读) + :ivar 当前缠K: 最后一根缠论K线(只读) + :ivar 观察员: 自身引用(只读,便于内部回调) + + 序列字段: + :ivar 普通K线序列 / :ivar 缠论K线序列 / :ivar 分型序列 + :ivar 笔序列 / :ivar 线段序列 / :ivar 中枢序列 + :ivar 买卖点序列 / :ivar 扩展线段序列 + + 数据输入: + :meth:`投喂原始数据` — 投喂原始OHLCV数据(自动创建K线) + :meth:`增加原始K线` — 投入一根K线,触发全链路增量分析(主入口) + + 控制: + :meth:`重置基础序列` — 清空并重置所有基础序列 + :meth:`识别买卖点` — 基于中枢和分型扫描买卖点 + :meth:`静态重新分析` — 清空后根据已有中枢和买卖点重新分析 + + 持久化: + :meth:`加载本地数据` — 从文件路径加载数据 + :meth:`读取数据文件` — 类方法,读取数据文件并返回观察者实例 """ def __init__(self, 符号: str, 周期: int, 配置: 缠论配置): @@ -5023,8 +6230,41 @@ class 观察者: self.配置: 缠论配置 = 配置 self.__终止时间戳: Optional[datetime] = 转化为时间戳(self.配置.手动终止) if self.配置.手动终止 else None + self.线段分析层次 = 3 + self.扩展线段分析层次 = 3 + self.混合扩展线段分析层次 = 3 self.重置基础序列() + def 重置基础序列(self): + """清空所有分析序列,重置为初始状态""" + self.基础缠K序列: List[缠论K线] = [] + + self.普通K线序列: List[K线] = [] + self.缠论K线序列: List[缠论K线] = [] + + self.分型序列: List[分型] = [] + + self.笔序列: List[虚线] = [] + self.笔_中枢序列: List[中枢] = [] + + self.线段序列组: List[List[虚线],] = [] # 线段, 线段<线段>,线段<线段<线段>>... + self.中枢序列组: List[List[中枢],] = [] + for i in range(self.线段分析层次): + self.线段序列组.append(list()) + self.中枢序列组.append(list()) + + self.扩展线段序列组: List[List[虚线],] = [] # 扩展线段, 扩展线段<扩展线段>, 扩展线段<扩展线段<扩展线段>>... + self.扩展中枢序列组: List[List[中枢],] = [] + for i in range(self.扩展线段分析层次): + self.扩展线段序列组.append(list()) + self.扩展中枢序列组.append(list()) + + self.混合扩展线段序列组: List[List[虚线],] = [] # 扩展线段<线段>, 扩展线段<线段<线段>>, 扩展线段<线段<线段<线段>>>... + self.混合扩展中枢序列组: List[List[中枢],] = [] + for i in range(self.混合扩展线段分析层次): + self.混合扩展线段序列组.append(list()) + self.混合扩展中枢序列组.append(list()) + @property def 观察员(self): """观察员(自引用)""" @@ -5038,45 +6278,58 @@ class 观察者: return f"{self.符号}:{self.周期}" @property - def 当前K线(self) -> Optional["K线"]: + def 当前K线(self) -> Optional[K线]: """ :return: 最后一根原始K线 """ return self.普通K线序列[-1] if self.普通K线序列 else None @property - def 当前缠K(self) -> Optional["缠论K线"]: + def 当前缠K(self) -> Optional[缠论K线]: """ :return: 最后一根缠论K线 """ return self.缠论K线序列[-1] if self.缠论K线序列 else None - def 重置基础序列(self): - """清空所有分析序列,重置为初始状态""" - self.基础缠K序列: List[缠论K线] = [] + @property + def 线段序列(self) -> List[虚线]: + return self.线段序列组[0] - self.普通K线序列: List[K线] = [] - self.缠论K线序列: List[缠论K线] = [] + @property + def 中枢序列(self) -> List[中枢]: + return self.中枢序列组[0] - self.分型序列: List[分型] = [] + @property + def 扩展线段序列(self) -> List[虚线]: + return self.扩展线段序列组[0] - self.笔序列: List[虚线] = [] - self.笔_中枢序列: List[中枢] = [] + @property + def 扩展中枢序列(self) -> List[中枢]: + return self.扩展中枢序列组[0] - self.线段序列: List[虚线] = [] - self.中枢序列: List[中枢] = [] + @property + def 扩展线段序列_线段(self) -> List[虚线]: + return self.混合扩展线段序列组[0] - self.扩展线段序列: List[虚线] = [] - self.扩展中枢序列: List[中枢] = [] + @property + def 扩展中枢序列_线段(self) -> List[中枢]: + return self.混合扩展中枢序列组[0] - self.扩展线段序列_线段: List[虚线] = [] - self.扩展中枢序列_线段: List[中枢] = [] + @property + def 线段_线段序列(self) -> List[虚线]: + return self.线段序列组[1] - self.线段_线段序列: List[虚线] = [] - self.线段_中枢序列: List[中枢] = [] + @property + def 线段_中枢序列(self) -> List[中枢]: + return self.中枢序列组[1] - self.扩展线段序列_扩展线段: List[虚线] = [] - self.扩展中枢序列_扩展线段: List[中枢] = [] + @property + def 扩展线段序列_扩展线段(self) -> List[虚线]: + return self.扩展线段序列组[1] + + @property + def 扩展中枢序列_扩展线段(self) -> List[中枢]: + return self.扩展中枢序列组[1] def 投喂原始数据(self, 时间戳: datetime, 开: float, 高: float, 低: float, 收: float, 量: float): """便捷入口,直接从 OHLCV 创建 K线 并投喂 @@ -5087,7 +6340,7 @@ class 观察者: :param 收: 收盘价 :param 量: 成交量 """ - self.增加原始K线(K线.创建普K(self.标识, 时间戳, 开, 高, 低, 收, 量, 0, self.周期)) + self.增加原始K线(K线.创建普K(self.符号, 时间戳, 开, 高, 低, 收, 量, 0, self.周期)) @final def 增加原始K线(self, 普K: K线): @@ -5106,58 +6359,49 @@ class 观察者: if 当前分型 is None: return - self.配置.分析笔 and 笔.分析(当前分型, self.分型序列, self.笔序列, self.缠论K线序列, self.普通K线序列, 0, self.配置) + 笔.分析(当前分型, self.分型序列, self.笔序列, self.缠论K线序列, self.普通K线序列, 0, self.配置) if not self.分型序列: return - self.配置.分析笔中枢 and 中枢.分析(self.笔序列, self.笔_中枢序列) + 中枢.分析(self.笔序列, self.笔_中枢序列, True, "", 0) if not self.笔序列: return - self.配置.分析线段 and 线段.分析(self.笔序列, self.线段序列, self.配置) - self.配置.分析线段中枢 and 中枢.分析(self.线段序列, self.中枢序列) + for i in range(self.线段分析层次): + if i == 0: + 线段.分析(self.笔序列, self.线段序列组[i], self.配置) + 中枢.分析(self.线段序列组[i], self.中枢序列组[i], True, "", 0) + continue + 线段.分析(self.线段序列组[i - 1], self.线段序列组[i], self.配置) + 中枢.分析(self.线段序列组[i], self.中枢序列组[i], True, "", 0) - self.配置.分析扩展线段 and 线段.扩展分析(self.笔序列, self.扩展线段序列, self.配置) - self.配置.分析线段中枢 and 中枢.分析(self.扩展线段序列, self.扩展中枢序列) + for i in range(self.扩展线段分析层次): + if i == 0: + 线段.扩展分析(self.笔序列, self.扩展线段序列组[i], self.配置) + 中枢.分析(self.扩展线段序列组[i], self.扩展中枢序列组[i], True, "", 0) + continue + 线段.扩展分析(self.扩展线段序列组[i - 1], self.扩展线段序列组[i], self.配置) + 中枢.分析(self.扩展线段序列组[i], self.扩展中枢序列组[i], True, "", 0) - self.配置.分析扩展线段 and 线段.扩展分析(self.线段序列, self.扩展线段序列_线段, self.配置) - self.配置.分析线段中枢 and 中枢.分析(self.扩展线段序列_线段, self.扩展中枢序列_线段) + for i in range(self.混合扩展线段分析层次): + 线段.扩展分析(self.线段序列组[i], self.混合扩展线段序列组[i], self.配置) + 中枢.分析(self.混合扩展线段序列组[i], self.混合扩展中枢序列组[i], True, "", 0) - self.配置.分析线段 and 线段.分析(self.线段序列, self.线段_线段序列, self.配置) - self.配置.分析线段中枢 and 中枢.分析(self.线段_线段序列, self.线段_中枢序列) - - self.配置.分析扩展线段 and 线段.扩展分析(self.扩展线段序列, self.扩展线段序列_扩展线段, self.配置) - self.配置.分析线段中枢 and 中枢.分析(self.扩展线段序列_扩展线段, self.扩展中枢序列_扩展线段) - - def 测试_保存数据(self, root: str = None): + def 测试_保存数据(self, root: str = None) -> str: """拆分各序列数据,单独存文件,文件名为对应变量名 :param root: 保存根目录(可选) + :return: 保存目录 """ - # 提取各类文本数据 - 笔序列_文本数据 = [筆.获取数据文本() for 筆 in self.笔序列] - 线段序列_文本数据 = [实线.获取数据文本() for 实线 in self.线段序列] - 扩展线段序列_数据文本 = [实线.获取数据文本() for 实线 in self.扩展线段序列] - 扩展线段序列_线段_数据文本 = [实线.获取数据文本() for 实线 in self.扩展线段序列_线段] - 线段_线段序列_数据文本 = [实线.获取数据文本() for 实线 in self.线段_线段序列] - 扩展线段序列_扩展线段_数据文本 = [实线.获取数据文本() for 实线 in self.扩展线段序列_扩展线段] - 笔_中枢序列_数据文本 = [当前中枢.获取数据文本() for 当前中枢 in self.笔_中枢序列] - 中枢序列_数据文本 = [当前中枢.获取数据文本() for 当前中枢 in self.中枢序列] - 扩展中枢序列_数据文本 = [当前中枢.获取数据文本() for 当前中枢 in self.扩展中枢序列] - 扩展中枢序列_线段_数据文本 = [当前中枢.获取数据文本() for 当前中枢 in self.扩展中枢序列_线段] - 线段_中枢序列_数据文本 = [当前中枢.获取数据文本() for 当前中枢 in self.线段_中枢序列] - 扩展中枢序列_扩展线段_数据文本 = [当前中枢.获取数据文本() for 当前中枢 in self.扩展中枢序列_扩展线段] - - # ===================== 优化点:优先使用传入的 root 目录 ===================== if root is not None: # 使用用户指定的根目录 根目录 = Path(root) else: - # 默认:当前脚本所在目录 - 根目录 = Path(__file__).parent + # 默认:系统临时目录 + 根目录 = tempfile.gettempdir() - # 生成子目录名称(不变) + # 生成子目录名称 起始时间 = int(self.普通K线序列[0].时间戳.timestamp()) 结束时间 = int(self.普通K线序列[-1].时间戳.timestamp()) 目录标识 = f"Py_{self.标识}_{起始时间}_{结束时间}" @@ -5166,73 +6410,86 @@ class 观察者: 保存路径 = 根目录 / 目录标识 保存路径.mkdir(exist_ok=True, parents=True) # parents=True 支持多级目录自动创建 - # 映射:变量名 -> 数据列表 - 数据映射 = [ - ("笔序列_文本数据", 笔序列_文本数据), - ("线段序列_文本数据", 线段序列_文本数据), - ("扩展线段序列_数据文本", 扩展线段序列_数据文本), - ("扩展线段序列_线段_数据文本", 扩展线段序列_线段_数据文本), - ("线段_线段序列_数据文本", 线段_线段序列_数据文本), - ("扩展线段序列_扩展线段_数据文本", 扩展线段序列_扩展线段_数据文本), - ("笔_中枢序列_数据文本", 笔_中枢序列_数据文本), - ("中枢序列_数据文本", 中枢序列_数据文本), - ("扩展中枢序列_数据文本", 扩展中枢序列_数据文本), - ("扩展中枢序列_线段_数据文本", 扩展中枢序列_线段_数据文本), - ("线段_中枢序列_数据文本", 线段_中枢序列_数据文本), - ("扩展中枢序列_扩展线段_数据文本", 扩展中枢序列_扩展线段_数据文本), - ] - - # 逐个写入独立文件 - for 文件名, 数据列表 in 数据映射: + def 保存序列(序列): + if len(序列) == 0: + return + 数据列表 = [对象.获取数据文本() for 对象 in 序列] + 文件名 = 序列[0].标识 文件全路径 = 保存路径 / f"{文件名}.txt" + # 逐个写入独立文件 with open(文件全路径, "w", encoding="utf-8") as f: f.write("\n".join(数据列表)) f.write("\n") # 向 C99 对齐 - print(f"全部数据拆分保存完成,目录:{保存路径.resolve()}") + + 保存序列(self.笔序列) + 保存序列(self.笔_中枢序列) + for i in range(self.线段分析层次): + 保存序列(self.线段序列组[i]) + 保存序列(self.中枢序列组[i]) + for i in range(self.扩展线段分析层次): + 保存序列(self.扩展线段序列组[i]) + 保存序列(self.扩展中枢序列组[i]) + for i in range(self.线段分析层次): + 保存序列(self.混合扩展线段序列组[i]) + 保存序列(self.混合扩展中枢序列组[i]) + + logger.warning(f"全部数据拆分保存完成,目录:{保存路径.resolve()}") + return str(保存路径.resolve()) def 识别买卖点(self): """识别买卖点(占位方法,具体逻辑在子类或Rust核心中实现)""" pass def 静态重新分析(self): - """静态重新分析(占位方法)""" + """静态重新分析""" self.分型序列: List[分型] = [] self.笔序列: List[虚线] = [] self.笔_中枢序列: List[中枢] = [] - self.线段序列: List[虚线] = [] - self.中枢序列: List[中枢] = [] + self.线段序列组: List[List[虚线],] = [] # 线段, 线段<线段>,线段<线段<线段>>... + self.中枢序列组: List[List[中枢],] = [] + for i in range(self.线段分析层次): + self.线段序列组.append(list()) + self.中枢序列组.append(list()) - self.扩展线段序列: List[虚线] = [] - self.扩展中枢序列: List[中枢] = [] + self.扩展线段序列组: List[List[虚线],] = [] # 扩展线段, 扩展线段<扩展线段>, 扩展线段<扩展线段<扩展线段>>... + self.扩展中枢序列组: List[List[中枢],] = [] + for i in range(self.扩展线段分析层次): + self.扩展线段序列组.append(list()) + self.扩展中枢序列组.append(list()) - self.扩展线段序列_线段: List[虚线] = [] - self.扩展中枢序列_线段: List[中枢] = [] - - self.线段_线段序列: List[虚线] = [] - self.线段_中枢序列: List[中枢] = [] - - self.扩展线段序列_扩展线段: List[虚线] = [] - self.扩展中枢序列_扩展线段: List[中枢] = [] + self.混合扩展线段序列组: List[List[虚线],] = [] # 扩展线段<线段>, 扩展线段<线段<线段>>, 扩展线段<线段<线段<线段>>>... + self.混合扩展中枢序列组: List[List[中枢],] = [] + for i in range(self.混合扩展线段分析层次): + self.混合扩展线段序列组.append(list()) + self.混合扩展中枢序列组.append(list()) for i in range(1, len(self.缠论K线序列) - 1): 当前分型 = 分型(self.缠论K线序列[i - 1], self.缠论K线序列[i], self.缠论K线序列[i + 1]) 笔.分析(当前分型, self.分型序列, self.笔序列, self.缠论K线序列, self.普通K线序列, 0, self.配置) - self.配置.分析笔中枢 and 中枢.分析(self.笔序列, self.笔_中枢序列) + 中枢.分析(self.笔序列, self.笔_中枢序列, True, "", 0) - self.配置.分析线段 and 线段.分析(self.笔序列, self.线段序列, self.配置) - self.配置.分析线段中枢 and 中枢.分析(self.线段序列, self.中枢序列) + for i in range(self.线段分析层次): + if i == 0: + 线段.分析(self.笔序列, self.线段序列组[i], self.配置) + 中枢.分析(self.线段序列组[i], self.中枢序列组[i], True, "", 0) + continue + 线段.分析(self.线段序列组[i - 1], self.线段序列组[i], self.配置) + 中枢.分析(self.线段序列组[i], self.中枢序列组[i], True, "", 0) - self.配置.分析扩展线段 and 线段.扩展分析(self.笔序列, self.扩展线段序列, self.配置) - self.配置.分析线段中枢 and 中枢.分析(self.扩展线段序列, self.扩展中枢序列) + for i in range(self.扩展线段分析层次): + if i == 0: + 线段.扩展分析(self.笔序列, self.扩展线段序列组[i], self.配置) + 中枢.分析(self.扩展线段序列组[i], self.扩展中枢序列组[i], True, "", 0) + continue + 线段.扩展分析(self.扩展线段序列组[i - 1], self.扩展线段序列组[i], self.配置) + 中枢.分析(self.扩展线段序列组[i], self.扩展中枢序列组[i], True, "", 0) - self.配置.分析扩展线段 and 线段.扩展分析(self.线段序列, self.扩展线段序列_线段, self.配置) - self.配置.分析线段中枢 and 中枢.分析(self.扩展线段序列_线段, self.扩展中枢序列_线段) - - self.配置.分析线段 and 线段.分析(self.线段序列, self.线段_线段序列, self.配置) - self.配置.分析线段中枢 and 中枢.分析(self.线段_线段序列, self.线段_中枢序列) + for i in range(self.混合扩展线段分析层次): + 线段.扩展分析(self.线段序列组[i], self.混合扩展线段序列组[i], self.配置) + 中枢.分析(self.混合扩展线段序列组[i], self.混合扩展中枢序列组[i], True, "", 0) def 加载本地数据(self, 文件路径: str): """重置基础序列后加载数据文件 @@ -5247,29 +6504,42 @@ class 观察者: self.投喂原始数据(转化为时间戳(int(时间戳)), 开盘价, 最高价, 最低价, 收盘价, 成交量) @classmethod - def 读取数据文件(cls, 观察员: "观察者", 文件路径: str, 配置=缠论配置()) -> Self: + def 读取数据文件(cls, 文件路径: str, 配置=缠论配置(), *, 观察员: Optional[观察者] = None) -> Self: """加载数据文件 - :param 观察员: 观察者 :param 文件路径: 数据文件路径 格式如: btcusd-300-1631772074-1632222374.nb :param 配置: 缠论配置 + :param 观察员: 可选,已有观察者实例;不传则自动创建 :return: 观察者实例 """ name = Path(文件路径).name.split(".")[0] 符号, 周期, 起始时间戳, 结束时间戳 = name.split("-") - 观察员.符号 = 符号 - 观察员.周期 = int(周期) - 观察员.配置 = 配置 + if 观察员 is None: + 观察员 = cls(符号, int(周期), 配置) + else: + 观察员.符号 = 符号 + 观察员.周期 = int(周期) + 观察员.配置 = 配置 观察员.加载本地数据(文件路径) return 观察员 class K线合成器: - """K线合成器 — 将小周期K线合成为大周期K线,支持多周期级联合成。 + """将小周期K线合成为大周期K线,支持多周期级联合成。 :ivar 标识: 合成器标识 :ivar 周期组: 从小到大排列的周期组 :ivar 当前K线: 各周期当前K线 :ivar 合成K线列表: 各周期已合成的K线列表 + + 数据输入: + :meth:`投喂` — 投喂原始OHLCV数据 + :meth:`投喂K线` — 投喂原始K线,触发多周期合成(主入口) + + 事件: + :meth:`设置事件回调` — 设置K线合成完成时的回调函数 + + 查询: + :meth:`获取当前K线` — 获取指定周期的当前K线 """ def __init__(self, 标识: str, 周期组: List[int], 事件回调: Optional[Callable] = None): @@ -5414,7 +6684,7 @@ class K线合成器: try: self.事件回调(信号类型="K线完成", 标识=self.标识, 周期=周期, 完成K线=完成K线) except Exception as e: - print(f"K线合成器信号回调错误: {e}") + logger.warning(f"K线合成器信号回调错误: {e}") def 获取当前K线(self, 周期: int) -> Optional[K线]: """获取指定周期当前正在合成的K线 @@ -5426,13 +6696,16 @@ class K线合成器: class 立体分析器: - """立体分析器 — 多周期缠论分析器,内部包含K线合成器 + 每周期一个观察者。 + """多周期缠论分析器,内部包含 :class:`K线合成器` + 每周期一个 :class:`观察者`。 通过最小周期合成大周期K线,各周期观察者独立分析,实现多周期联立。 :ivar 周期组: 所有分析周期 :ivar 观察员: 各周期对应的观察者 :ivar K线合成器: 内部K线合成器 + + 数据输入: + :meth:`投喂K线` — 投入最小周期K线,自动合成大周期并驱动各周期分析(主入口) """ def __init__(self, 符号: str, 周期组: List[int], 配置: 缠论配置 = 缠论配置(), 配置组: Dict[int, 缠论配置] = dict()): @@ -5496,24 +6769,28 @@ class 立体分析器: self._单体分析器[周期].增加原始K线(当前K线) def 测试_保存数据(self, root: str = None): - """拆分各序列数据,单独存文件,文件名为对应变量名""" + """拆分各序列数据,单独存文件。 + + :param root: 保存根目录,默认取脚本所在目录 + :return: 数据保存目录路径 + """ # 生成存储根目录 - 脚本目录 = Path(__file__).parent if not root else root # 取当前脚本所在文件夹 + 脚本目录 = tempfile.gettempdir() if not root else root # 默认系统临时目录 起始时间 = int(self._单体分析器[self.__输入周期].普通K线序列[0].时间戳.timestamp()) 结束时间 = int(self._单体分析器[self.__输入周期].普通K线序列[-1].时间戳.timestamp()) 目录标识 = f"PyM_{self._单体分析器[self.__输入周期].标识}_{起始时间}_{结束时间}" # 最终保存路径 = 脚本目录 / 自动生成的文件夹 - 保存路径 = 脚本目录 / 目录标识 + 保存路径 = Path(os.path.join(脚本目录, 目录标识)) 保存路径.mkdir(exist_ok=True) for 周期 in self.周期组: self._单体分析器[周期].测试_保存数据(保存路径) - print(f"多级别数据拆分保存完成,目录:{保存路径.resolve()}") + logger.warning(f"多级别数据拆分保存完成,目录:{保存路径.resolve()}") -def 测试_读取数据(观察员: 观察者, 配置: 缠论配置) -> Callable[[], "观察者"]: +def 测试_读取数据(观察员: 观察者, 配置: 缠论配置) -> Callable[[], 观察者]: """测试_读取数据 :param 观察员: 观察者 :param 配置: 缠论配置 @@ -5522,9 +6799,9 @@ def 测试_读取数据(观察员: 观察者, 配置: 缠论配置) -> Callable[ def 魔法(): 启动时间 = datetime.now() - 观察者.读取数据文件(观察员, 配置.加载文件路径, 配置) + 观察者.读取数据文件(配置.加载文件路径, 配置, 观察员=观察员) 消耗用时 = datetime.now() - 启动时间 - print("测试_读取数据 耗时", 消耗用时, "普K数量", len(观察员.普通K线序列)) + logger.info(f"测试_读取数据 耗时 {消耗用时} 普K数量 {len(观察员.普通K线序列)}") return 观察员 return 魔法 @@ -5553,15 +6830,46 @@ def 测试_周期合成(配置: 缠论配置, 配置组: Dict[int, 缠论配置] k线 = K线.读取大端字节数组(buffer[i * size : i * size + size], 周期, 符号) 多级别分析.投喂K线(k线) 消耗用时 = datetime.now() - 启动时间 - print("测试_周期合成", 消耗用时, "普K数量", len(多级别分析._单体分析器[周期].普通K线序列)) + logger.info(f"测试_周期合成 {消耗用时} 普K数量 {len(多级别分析._单体分析器[周期].普通K线序列)}") return 多级别分析 return 魔法 +def 测试_指标挂载(配置: 缠论配置): + 文件路径 = 配置.加载文件路径 + name = Path(文件路径).name.split(".")[0] + 符号, 周期, 起始时间戳, 结束时间戳 = name.split("-") + 周期 = int(周期) + 观察员 = 观察者(符号, 周期, 配置) + + def 魔法(): + 启动时间 = datetime.now() + with open(文件路径, "rb") as f: + buffer = f.read() + size = struct.calcsize(">6d") + for i in range(len(buffer) // size): + if i == 500: + 配置.MACD_参数列表 = [("macd", 配置.平滑异同移动平均线_快线周期, 配置.平滑异同移动平均线_慢线周期, 配置.平滑异同移动平均线_信号周期)] + 配置.MACD_参数列表.append(("macd_12_26_9", 12, 26, 9)) + k线 = K线.读取大端字节数组(buffer[i * size : i * size + size], 周期, 符号) + 观察员.增加原始K线(k线) + if i == 500: + assert 观察员.普通K线序列[0].指标.macd_12_26_9 is not None, "指标挂载失败" + print(观察员.普通K线序列[0].指标["macd_12_26_9"]) + break + + 消耗用时 = datetime.now() - 启动时间 + logger.info(f"测试_指标挂载 耗时 {消耗用时} 普K数量 {len(观察员.普通K线序列)}") + return 观察员 + + return 魔法 + + if __name__ == "__main__": 当前配置 = 缠论配置.不推送() 当前配置.加载文件路径 = str(Path(__file__).parent / "btcusd-300-1761327300-1776327900.nb") - 观察员 = 观察者("", 0, 当前配置) - 测试_读取数据(观察员, 当前配置)() # .测试_保存数据() - # 测试_周期合成(当前配置)().测试_保存数据() + with tempfile.TemporaryDirectory() as tmpdir: + # 测试_读取数据(观察者("", 0, 当前配置), 当前配置)().测试_保存数据(tmpdir) + # 测试_周期合成(当前配置)().测试_保存数据(tmpdir) + 测试_指标挂载(当前配置)().测试_保存数据(tmpdir)