diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 891d2bb..4a85cad 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -6,7 +6,8 @@ "Bash(dir:*)", "Bash(set CLAUDE_CODE_GIT_BASH_PATH=D:installGitbinbash.exe)", "Bash($env:CLAUDE_CODE_GIT_BASH_PATH=\"D:\\\\install\\\\Git\\\\bin\\\\bash.exe\")", - "Bash(\"D:\\\\install\\\\Git\\\\bin\\\\bash.exe\" -c \"export CLAUDE_CODE_GIT_BASH_PATH=''D:\\\\install\\\\Git\\\\bin\\\\bash.exe'' && claude install\")" + "Bash(\"D:\\\\install\\\\Git\\\\bin\\\\bash.exe\" -c \"export CLAUDE_CODE_GIT_BASH_PATH=''D:\\\\install\\\\Git\\\\bin\\\\bash.exe'' && claude install\")", + "Bash(python scripts/generate_stock_viewer.py:*)" ] } } diff --git a/README.md b/README.md index 7dac138..3cda8ee 100644 --- a/README.md +++ b/README.md @@ -91,17 +91,36 @@ python scripts/pipeline_converging_triangle.py - 向上突破: N 次 - 向下突破: N 次 - 当日报告: outputs/converging_triangles/report.md +- 图表目录: outputs/converging_triangles/charts/ +- HTML查看器: outputs/converging_triangles/stock_viewer.html ⭐ ``` +**提示**:使用 `--all-stocks` 参数可以为所有108只股票生成图表和HTML查看器,方便全面查看。 + ## 相关文档 -- `docs/收敛三角形检测系统-使用指南.md` - 使用流程与参数说明 +### 📖 入门必读 +- **`docs/功能与文档总览.md`** - 完整功能与文档索引 ⭐⭐⭐ +- `USAGE.md` - 完整使用指南(参数说明、数据格式等)⭐ +- `outputs/converging_triangles/QUICK_START.md` - HTML查看器快速指南 🚀 + +### 核心功能 - `docs/突破强度计算方法.md` - 突破强度的计算逻辑 +- `docs/强度分计算示例.md` - 详细的计算步骤示例 - `docs/converging_triangles_outputs.md` - 输出字段说明 -- `docs/枢轴点分段选择算法详解.md` - 分段算法完整说明 ⭐ + +### 可视化 +- `outputs/converging_triangles/QUICK_START.md` - HTML查看器快速指南 🚀 +- `outputs/converging_triangles/README_viewer.md` - HTML查看器详细文档 +- `docs/all-stocks-feature.md` - 全股票图表功能说明 +- `docs/2026-01-27_HTML查看器功能.md` - HTML查看器设计与实现 - `docs/2026-01-26_图表详细模式功能.md` - 图表可视化改进 🎨 -- `docs/方案4-混合策略详解.md` - 实时模式完整说明 ⭐ -- `docs/实时模式使用指南.md` - 快速上手指南 -- `docs/2026-01-26_相向收敛约束改进.md` - 过滤通道形态的改进 + +### 算法原理 +- `docs/枢轴点分段选择算法详解.md` - 分段算法完整说明 ⭐ - `docs/枢轴点检测原理.md` - 枢轴点算法详解 - `docs/枢轴点边界问题分析.md` - 边界盲区问题与解决方案 +- `docs/2026-01-26_相向收敛约束改进.md` - 过滤通道形态的改进 +- `docs/方案4-混合策略详解.md` - 实时模式完整说明 ⭐ +- `docs/实时模式使用指南.md` - 快速上手指南 +- `docs/收敛三角形检测系统-使用指南.md` - 使用流程与参数说明 diff --git a/USAGE.md b/USAGE.md index 0831bd2..fbcbc51 100644 --- a/USAGE.md +++ b/USAGE.md @@ -7,6 +7,27 @@ python scripts/pipeline_converging_triangle.py ``` +**新功能**:清空输出目录并重新生成(确保使用最新算法) +```powershell +# 完整重新生成(清空旧数据) | CSV:所有108只股票的检测结果(包括不满足条件的)图表:只为满足条件的股票生成(比如31只)HTML:只显示满足条件的31只 +python scripts/pipeline_converging_triangle.py --clean + +# 清空后为所有股票生成图表 | CSV:所有108只股票的检测结果 图表:为所有108只股票生成图(包括不满足条件的)满足条件的:完整收敛三角形图 + 强度分 不满足条件的:基础K线图 + 强度分=0 HTML:显示所有108只股票 +python scripts/pipeline_converging_triangle.py --clean --all-stocks + +# 清空后指定日期生成 +python scripts/pipeline_converging_triangle.py --clean --date 20260120 --all-stocks +``` + +**为所有108只股票生成图表**(包括不满足条件的) +```powershell +# 生成所有股票的图表,每个都显示强度分 +python scripts/pipeline_converging_triangle.py --all-stocks + +# 指定日期,生成所有股票的图表 +python scripts/pipeline_converging_triangle.py --date 20260120 --all-stocks +``` + ## 1. 创建与激活虚拟环境 ```powershell @@ -40,13 +61,22 @@ python scripts/pipeline_converging_triangle.py --date 20260120 # 生成详情模式图片(显示所有枢轴点和拟合点) python scripts/pipeline_converging_triangle.py --show-details +# 为所有108只股票生成图表(包括不满足收敛三角形条件的) +python scripts/pipeline_converging_triangle.py --all-stocks + # 组合使用 -python scripts/pipeline_converging_triangle.py --date 20260120 --show-details +python scripts/pipeline_converging_triangle.py --date 20260120 --show-details --all-stocks # 跳过检测(仅生成报告与图表) python scripts/pipeline_converging_triangle.py --skip-detection ``` +**参数说明**: +- `--all-stocks`:为所有108只股票生成图表 + - 满足条件的:显示完整的收敛三角形和强度分 + - 不满足条件的:显示基础K线图,强度分为0 + - 适合全面查看整个股票池的情况 + ### 仅批量检测 ```powershell @@ -70,15 +100,23 @@ python scripts/plot_converging_triangles.py # 文件名格式: YYYYMMDD_股票代码_股票名称_detail.png python scripts/plot_converging_triangles.py --show-details +# 为所有108只股票生成图表(包括不满足条件的) +python scripts/plot_converging_triangles.py --all-stocks + # 指定日期 python scripts/plot_converging_triangles.py --date 20260120 +# 组合使用:所有股票 + 详细模式 +python scripts/plot_converging_triangles.py --all-stocks --show-details + # 同时生成两种模式进行对比(先后运行两次) python scripts/plot_converging_triangles.py # 生成简洁版 python scripts/plot_converging_triangles.py --show-details # 生成详细版 ``` -**提示**: 简洁模式和详细模式的文件名不同(详细模式带 `_detail` 后缀),两种模式的图表会同时保留,方便对比查看。 +**提示**: +- 简洁模式和详细模式的文件名不同(详细模式带 `_detail` 后缀),两种模式的图表会同时保留,方便对比查看 +- `--all-stocks` 会为所有108只股票生成图表,每个都显示强度分(不满足条件的显示0分) 输出(已被 `.gitignore` 忽略,默认不推送远程): - `outputs/converging_triangles/all_results.csv` @@ -86,6 +124,7 @@ python scripts/plot_converging_triangles.py --show-details # 生成详细版 - `outputs/converging_triangles/strong_breakout_down.csv` - `outputs/converging_triangles/report.md` - `outputs/converging_triangles/charts/*.png` +- `outputs/converging_triangles/stock_viewer.html` - 📊 **新增**:可视化查看器 ## 4. 参数调整 @@ -147,6 +186,41 @@ SHOW_CHART_DETAILS = False # 图表详细模式(False=简洁,True=详细) ## 6. 备注 +### 📊 可视化查看器 + +使用浏览器打开 `outputs/converging_triangles/stock_viewer.html` 可以: +- 🎚️ 通过滑块调整强度分阈值,实时筛选股票 +- 📈 查看所有股票的详细指标和图表 +- 🔍 点击图表可放大查看细节 +- 📊 实时显示统计信息(总数、筛选数、平均强度分) +- ✨ 支持两种模式:显示所有108只股票 或 仅满足条件的股票 + +**使用方法**: +```powershell +# 方式1:一键生成所有数据和HTML查看器(显示所有108只股票) +python scripts/pipeline_converging_triangle.py --all-stocks + +# 方式2:单独重新生成HTML +python scripts/generate_stock_viewer.py --all-stocks # 显示所有108只 +python scripts/generate_stock_viewer.py # 仅显示满足条件的 + +# 方式3:指定日期 +python scripts/generate_stock_viewer.py --date 20260120 --all-stocks + +# 打开查看器 +start outputs/converging_triangles/stock_viewer.html +``` + +**特点**: +- 📦 数据内嵌在HTML中,可直接双击打开,无需服务器 +- 🚀 响应式设计,自适应各种屏幕尺寸 +- 🎨 紫色渐变主题,视觉效果优雅 +- ⚡ 实时过滤和统计,性能优秀 + +详见:`outputs/converging_triangles/README_viewer.md` 或 `outputs/converging_triangles/QUICK_START.md` + +--- + - 关闭环境:`deactivate` - 权限问题(PowerShell): ```powershell diff --git a/discuss/20260126-讨论.md b/discuss/20260126-讨论.md index f0e4c58..6b984c3 100644 --- a/discuss/20260126-讨论.md +++ b/discuss/20260126-讨论.md @@ -48,14 +48,79 @@ --- -## 待办: 突破强度评分 +## ~~待办~~: 强度分评分系统(已完成) +![](images/2026-01-27-10-11-59.png) | 分量 | 权重 | 说明 | |------|------|------| -| 价格突破 | 60% | 突破幅度 | -| 收敛程度 | 25% | 蓄势充分度 | +| 价格突破 | 50% | 突破幅度(未突破时为0) | +| 收敛程度 | 20% | 蓄势充分度 | | 成交量 | 15% | 放量确认 | +| 拟合贴合度 | 15% | 枢轴点到拟合线的平均距离,反映形态纯度 | -- 枢轴点和拟合线距离。 +**强度分说明**: +- 综合评估收敛三角形的质量,无论是否突破都计算得分 +- 可用于评估"待突破"形态的潜在价值,或"已突破"形态的有效性 +- 公式:`强度分 = 价格×50% + 收敛×20% + 成交量×15% + 拟合贴合度×15%` + +**拟合贴合度计算**: +- 计算选中的枢轴点到对应拟合线(上沿/下沿)的相对距离 +- 距离越小,说明形态越标准、越"纯净" +- 距离大表示形态杂乱,枢轴点散乱分布 +- 使用指数衰减函数 `exp(-mean_rel_error * 20)` 归一化为 0~1 分数 + +**实施状态**: ✅ 已完成(2026-01-27) +- 代码实现:`src/converging_triangle.py` +- 详细文档:`docs/突破强度计算方法.md` (v3.0) +- **计算示例**:[强度分计算示例.md](../docs/强度分计算示例.md) - 包含完整计算步骤与多场景对比 ⭐ +- 图表显示:已集成到图表标题第三行 +- **可视化查看器**:`outputs/converging_triangles/stock_viewer.html` - 交互式强度分筛选 ⭐ + +--- + +## 新增功能: HTML可视化查看器(2026-01-27) + +![](images/stock-viewer-demo.png) + +**功能亮点**: +- 🎚️ **交互式滑块**:实时调整强度分阈值(0.00 ~ 1.00) +- 📊 **全股票模式**:显示所有108只股票,无形态的强度分为0 +- 📈 **统计面板**:总数、筛选数、平均强度分实时更新 +- 🔍 **图表放大**:点击图表全屏查看细节 +- 📱 **响应式设计**:自适应各种屏幕尺寸 +- 💾 **离线可用**:数据内嵌在HTML中,无需服务器 + +**使用方法**: +```powershell +# 一键生成(推荐) +python scripts/pipeline_converging_triangle.py --all-stocks + +# 单独生成HTML +python scripts/generate_stock_viewer.py --all-stocks # 显示所有108只 +python scripts/generate_stock_viewer.py # 仅显示满足条件的 + +# 打开查看器 +start outputs/converging_triangles/stock_viewer.html +``` + +**技术特性**: +- 数据内嵌方案:解决浏览器CORS限制,可直接双击打开 +- 紫色渐变主题:现代化视觉设计 +- 实时过滤:无延迟的交互体验 +- 颜色分级:绿色(≥0.5)/黄色(0.3~0.5)/灰色(<0.3) + +**相关文档**: +- 快速指南:`outputs/converging_triangles/QUICK_START.md` +- 详细文档:`outputs/converging_triangles/README_viewer.md` +- 功能说明:`docs/2026-01-27_HTML查看器功能.md` + +--- + +## 附注 + +**tanh函数**: +- 双曲正切函数,输出范围 (-1, 1),用于归一化价格突破幅度 +- 公式:`price_score = tanh(突破幅度% × 15)` +- 特点:小幅突破时敏感(分数增长快),大幅突破时饱和(分数增长慢) +- 示例:1%→0.15分,3%→0.42分,5%→0.64分,10%→0.91分 -后续回测调优。 diff --git a/discuss/20260727-讨论.md b/discuss/20260727-讨论.md new file mode 100644 index 0000000..57a7e15 --- /dev/null +++ b/discuss/20260727-讨论.md @@ -0,0 +1,81 @@ +![](images/2026-01-27-11-32-39.png) + +拟合线不好,需要使用 "凸优化经典算法"。 +最终是希望 上沿线或下沿线,包含大部分的 枢轴点。 + +--- + +## 已实现:凸优化拟合方法(2026-01-27) + +### 新增参数 + +```python +fitting_method: str = "iterative" # "iterative" | "lp" | "quantile" | "anchor" +``` + +### 拟合方法对比 + +| 方法 | 说明 | 优点 | 缺点 | +|------|------|------|------| +| **iterative** | 迭代离群点移除 + 最小二乘法 | 稳定保守,已有调参经验 | 线"穿过"数据而非"包住" | +| **lp** | 线性规划凸优化 | 数学严谨,保证边界包络 | 对极端值敏感 | +| **quantile** | 分位数回归 (上95%/下5%) | 统计稳健,抗异常值 | 计算稍慢 | +| **anchor** | 绝对极值锚点 + 斜率优化 | 锚点明确,线更贴近主趋势 | 对枢轴点数量较敏感 | + +### LP 方法数学原理 + +**上沿问题 (找"天花板",最紧的包络)**: +``` +minimize Σ(a*x_i + b - y_i) 线与点的总距离 +subject to y_i ≤ a * x_i + b 所有点在线下方 + -0.5 ≤ a ≤ 0.5 斜率限制 +``` + +**下沿问题 (找"地板",最紧的包络)**: +``` +minimize Σ(y_i - a*x_i - b) 线与点的总距离 +subject to y_i ≥ a * x_i + b 所有点在线上方 + -0.5 ≤ a ≤ 0.5 斜率限制 +``` + +这确保拟合线严格"包住"所有枢轴点,且尽量贴近数据,符合技术分析中"压力线/支撑线"的语义。 + +### Anchor 方法思路 + +**核心目标**:固定锚点,优化斜率,使大部分枢轴点在边界线正确一侧。 + +- 锚点:检测窗口内的绝对最高/最低点(排除最后1天用于突破判断) +- 上沿:找最“平缓”的下倾线,使 >=95% 枢轴高点在上沿线下方 +- 下沿:找最“平缓”的上倾线,使 >=95% 枢轴低点在下沿线上方 +- 实现:对斜率做二分搜索,满足覆盖率约束后取最贴近的一条线 + +### 测试验证 + +``` +上沿 LP: slope=-0.006667, intercept=10.5333 + 验证(线-点): [0.033, 0.000, 0.067, 0.033, 0.000] (全>=0,线在点上方) +下沿 LP: slope=0.005000, intercept=8.0000 + 验证(点-线): [0.00, 0.05, 0.00, 0.05, 0.00] (全>=0,线在点下方) +``` + +### 使用方法 + +```python +from src.converging_triangle import ConvergingTriangleParams, detect_converging_triangle + +# 使用凸优化/统计方法 +params = ConvergingTriangleParams( + fitting_method="lp", # 或 "quantile" / "anchor" + # ... 其他参数 +) + +result = detect_converging_triangle(high, low, close, volume, params) +``` + +### 实现位置 + +- 参数类: `ConvergingTriangleParams.fitting_method` +- LP拟合: `fit_boundary_lp()` +- 分位数回归: `fit_boundary_quantile()` +- 锚点拟合: `fit_boundary_anchor()` +- 分发函数: `fit_pivot_line_dispatch()` diff --git a/discuss/images/2026-01-27-10-11-59.png b/discuss/images/2026-01-27-10-11-59.png new file mode 100644 index 0000000..ecfa8ec Binary files /dev/null and b/discuss/images/2026-01-27-10-11-59.png differ diff --git a/discuss/images/2026-01-27-11-32-39.png b/discuss/images/2026-01-27-11-32-39.png new file mode 100644 index 0000000..e6ee289 Binary files /dev/null and b/discuss/images/2026-01-27-11-32-39.png differ diff --git a/docs/2026-01-27_HTML查看器功能.md b/docs/2026-01-27_HTML查看器功能.md new file mode 100644 index 0000000..d9570cf --- /dev/null +++ b/docs/2026-01-27_HTML查看器功能.md @@ -0,0 +1,301 @@ +# HTML股票查看器功能说明 + +> 创建时间:2026-01-27 +> 版本:v1.0 +> 相关PR/Issue:HTML可视化查看器 + +## 功能概述 + +新增交互式HTML股票查看器,提供可视化的强度分筛选和查看功能。支持显示所有108只股票或仅显示满足条件的股票。 + +## 核心特性 + +### 1. 两种显示模式 + +#### 默认模式 +```powershell +python scripts/generate_stock_viewer.py +``` +- 仅显示满足收敛三角形条件的股票(14只) +- 适合日常快速选股 +- 聚焦于有潜力的标的 + +#### 全股票模式(推荐) +```powershell +python scripts/generate_stock_viewer.py --all-stocks +``` +- 显示所有108只股票 +- 有形态:显示完整的强度分和指标 +- 无形态:强度分为0,显示基础K线图 +- 适合全面研究和对比分析 + +### 2. 交互式筛选 + +**强度分滑块**: +- 拖动范围:0.00 ~ 1.00 +- 实时过滤股票列表 +- 动态更新统计信息 + +**示例**: +- 滑块 = 0.00:显示全部108只股票 +- 滑块 = 0.30:显示强度分 ≥ 0.3 的股票(中等以上) +- 滑块 = 0.50:显示强度分 ≥ 0.5 的股票(强势股) + +### 3. 实时统计 + +顶部面板显示: +- **总股票数**:数据集中的总股票数(108) +- **显示股票数**:当前滑块筛选后的股票数 +- **平均强度分**:当前显示股票的平均强度分 + +### 4. 股票卡片 + +每张卡片包含: +- **头部**:股票名称、代码(紫色渐变背景) +- **指标**: + - 突破方向(↑向上 / ↓向下 / -无) + - 宽度比(收敛程度) + - 触碰次数(上沿/下沿) + - 放量确认(是/否) +- **强度分**:大号显示,颜色分级 + - 🟢 绿色(≥ 0.5):强势突破 + - 🟡 黄色(0.3 ~ 0.5):中等强度 + - ⚪ 灰色(< 0.3):微弱/无形态 +- **图表**:K线图和三角形趋势线(点击可放大) + +### 5. 图表查看 + +- 点击图表:全屏查看细节 +- 点击背景/按ESC:关闭全屏 +- 高清显示:原始分辨率 + +## 技术实现 + +### 数据内嵌方案 + +**问题**:浏览器安全限制(CORS)阻止直接读取本地CSV文件 + +**解决方案**: +1. Python脚本读取 `all_results.csv` +2. 将数据转换为JSON格式 +3. 内嵌到HTML的 ` + + \ No newline at end of file diff --git a/scripts/__pycache__/generate_stock_viewer.cpython-313.pyc b/scripts/__pycache__/generate_stock_viewer.cpython-313.pyc new file mode 100644 index 0000000..63b80fa Binary files /dev/null and b/scripts/__pycache__/generate_stock_viewer.cpython-313.pyc differ diff --git a/scripts/__pycache__/pipeline_converging_triangle.cpython-313.pyc b/scripts/__pycache__/pipeline_converging_triangle.cpython-313.pyc new file mode 100644 index 0000000..394b994 Binary files /dev/null and b/scripts/__pycache__/pipeline_converging_triangle.cpython-313.pyc differ diff --git a/scripts/__pycache__/plot_converging_triangles.cpython-313.pyc b/scripts/__pycache__/plot_converging_triangles.cpython-313.pyc index 2f82301..1073390 100644 Binary files a/scripts/__pycache__/plot_converging_triangles.cpython-313.pyc and b/scripts/__pycache__/plot_converging_triangles.cpython-313.pyc differ diff --git a/scripts/__pycache__/triangle_config.cpython-313.pyc b/scripts/__pycache__/triangle_config.cpython-313.pyc index 7434990..3b28c53 100644 Binary files a/scripts/__pycache__/triangle_config.cpython-313.pyc and b/scripts/__pycache__/triangle_config.cpython-313.pyc differ diff --git a/scripts/generate_stock_viewer.py b/scripts/generate_stock_viewer.py new file mode 100644 index 0000000..3ed2b87 --- /dev/null +++ b/scripts/generate_stock_viewer.py @@ -0,0 +1,1487 @@ +""" +生成包含内嵌数据的股票查看器HTML + +用法: + python scripts/generate_stock_viewer.py + python scripts/generate_stock_viewer.py --date 20260120 + python scripts/generate_stock_viewer.py --all-stocks # 显示所有108只股票 +""" + +from __future__ import annotations + +import argparse +import csv +import os +import json +import sys +import pickle +import numpy as np + +def load_all_stocks_list(data_dir: str) -> tuple: + """从close.pkl加载所有股票列表""" + # 创建假模块以加载pickle + sys.modules['model'] = type('FakeModule', (), {'ndarray': np.ndarray, '__path__': []})() + sys.modules['model.index_info'] = sys.modules['model'] + + close_path = os.path.join(data_dir, 'close.pkl') + with open(close_path, 'rb') as f: + data = pickle.load(f) + + return data['tkrs'], data['tkrs_name'] + +def load_stock_data(csv_path: str, target_date: int = None, all_stocks_mode: bool = False, data_dir: str = 'data') -> tuple: + """从CSV加载股票数据""" + stocks_map = {} + max_date = 0 + + with open(csv_path, 'r', encoding='utf-8-sig') as f: + reader = csv.DictReader(f) + for row in reader: + try: + date = int(row.get('date', '0')) + if date > max_date: + max_date = date + except: + continue + + # 使用指定日期或最新日期 + use_date = target_date if target_date else max_date + + # 从CSV读取有强度分的股票 + with open(csv_path, 'r', encoding='utf-8-sig') as f: + reader = csv.DictReader(f) + for row in reader: + try: + date = int(row.get('date', '0')) + if date != use_date: + continue + + stock_code = row.get('stock_code', '') + stock = { + 'idx': int(row.get('stock_idx', '0')), + 'code': stock_code, + 'name': row.get('stock_name', ''), + 'strengthUp': float(row.get('breakout_strength_up', '0')), + 'strengthDown': float(row.get('breakout_strength_down', '0')), + 'direction': row.get('breakout_dir', ''), + 'widthRatio': float(row.get('width_ratio', '0')), + 'touchesUpper': int(row.get('touches_upper', '0')), + 'touchesLower': int(row.get('touches_lower', '0')), + 'volumeConfirmed': row.get('volume_confirmed', ''), + 'date': date, + 'hasTriangle': True # 标记为有三角形形态 + } + + stock['strength'] = max(stock['strengthUp'], stock['strengthDown']) + + # 清理文件名中的非法字符 + clean_name = stock['name'].replace('*', '').replace('?', '').replace('"', '').replace('<', '').replace('>', '').replace('|', '').replace(':', '').replace('/', '').replace('\\', '') + stock['chartPath'] = f"charts/{date}_{stock_code}_{clean_name}.png" + + if stock_code not in stocks_map or stocks_map[stock_code]['strength'] < stock['strength']: + stocks_map[stock_code] = stock + + except Exception as e: + continue + + # 如果是all_stocks模式,添加所有股票 + if all_stocks_mode: + all_codes, all_names = load_all_stocks_list(data_dir) + for idx, (code, name) in enumerate(zip(all_codes, all_names)): + if code not in stocks_map: + # 没有三角形形态的股票,强度分为0 + clean_name = name.replace('*', '').replace('?', '').replace('"', '').replace('<', '').replace('>', '').replace('|', '').replace(':', '').replace('/', '').replace('\\', '') + stocks_map[code] = { + 'idx': idx, + 'code': code, + 'name': name, + 'strengthUp': 0.0, + 'strengthDown': 0.0, + 'strength': 0.0, + 'direction': 'none', + 'widthRatio': 0.0, + 'touchesUpper': 0, + 'touchesLower': 0, + 'volumeConfirmed': '', + 'date': use_date, + 'chartPath': f"charts/{use_date}_{code}_{clean_name}.png", + 'hasTriangle': False # 标记为无三角形形态 + } + + stocks = list(stocks_map.values()) + stocks.sort(key=lambda x: x['strength'], reverse=True) + + return stocks, use_date + +def generate_html(stocks: list, date: int, output_path: str): + """生成包含数据的HTML""" + + html_template = ''' + + + + + 收敛三角形强度分选股系统 + + + + + + +
+
+
+

收敛三角形选股系统

+
+ 数据日期: DATA_DATE + 监控股票: TOTAL_STOCKS +
+
+
+ +
+ +
+ +
+ 排序 + + +
+ +
+ + +
+ +
+
全部
+
↑ 向上
+
↓ 向下
+
— 无
+
+
+ + +
+ +
+
全部
+
✓ 已确认
+
✗ 未确认
+
+
+ + +
+ +
+
+ +
0.00
+
+
+ +
+
+
📊
+
+
Total
+
0
+
+
+
+
+
+
Showing
+
0
+
+
+
+
+
+
Average
+
0.00
+
+
+
+
+ +
+
+ +
+
+ + + + +''' + + # 替换数据 + stocks_json = json.dumps(stocks, ensure_ascii=False, indent=2) + html = html_template.replace('STOCK_DATA', stocks_json) + html = html.replace('DATA_DATE', str(date)) + html = html.replace('TOTAL_STOCKS', str(len(stocks))) + + with open(output_path, 'w', encoding='utf-8') as f: + f.write(html) + +def main(): + parser = argparse.ArgumentParser(description="生成包含内嵌数据的股票查看器HTML") + parser.add_argument( + "--date", + type=int, + default=None, + help="指定日期(YYYYMMDD),默认为最新日期" + ) + parser.add_argument( + "--input", + default=os.path.join("outputs", "converging_triangles", "all_results.csv"), + help="输入CSV路径" + ) + parser.add_argument( + "--output", + default=os.path.join("outputs", "converging_triangles", "stock_viewer.html"), + help="输出HTML路径" + ) + parser.add_argument( + "--all-stocks", + action="store_true", + help="显示所有108只股票(包括不满足条件的)" + ) + args = parser.parse_args() + + print("=" * 70) + print("生成股票查看器HTML") + print("=" * 70) + + if not os.path.exists(args.input): + print(f"错误: CSV文件不存在: {args.input}") + print("请先运行: python scripts/pipeline_converging_triangle.py") + return + + print(f"读取数据: {args.input}") + print(f"模式: {'所有股票' if args.all_stocks else '仅满足条件的股票'}") + + data_dir = "data" + stocks, date = load_stock_data(args.input, args.date, args.all_stocks, data_dir) + + print(f"数据日期: {date}") + print(f"股票数量: {len(stocks)}") + if args.all_stocks: + has_triangle = sum(1 for s in stocks if s.get('hasTriangle', False)) + print(f" - 有三角形形态: {has_triangle}") + print(f" - 无三角形形态: {len(stocks) - has_triangle}") + + print(f"生成HTML: {args.output}") + generate_html(stocks, date, args.output) + + print("\n完成!") + print(f"\n用浏览器打开: {args.output}") + print("=" * 70) + +if __name__ == "__main__": + main() diff --git a/scripts/pipeline_converging_triangle.py b/scripts/pipeline_converging_triangle.py index 197933f..be2ea0c 100644 --- a/scripts/pipeline_converging_triangle.py +++ b/scripts/pipeline_converging_triangle.py @@ -27,6 +27,7 @@ sys.path.append(os.path.join(os.path.dirname(__file__), "..", "src")) from run_converging_triangle import main as run_detection from report_converging_triangles import main as run_report from plot_converging_triangles import main as run_plot +from generate_stock_viewer import main as run_viewer def print_section_header(title: str, step: int) -> None: @@ -75,6 +76,21 @@ def main() -> None: action="store_true", help="跳过图表绘制步骤", ) + parser.add_argument( + "--all-stocks", + action="store_true", + help="为所有108只股票生成图表(包括不满足收敛三角形条件的)", + ) + parser.add_argument( + "--skip-viewer", + action="store_true", + help="跳过生成HTML查看器", + ) + parser.add_argument( + "--clean", + action="store_true", + help="运行前清空outputs文件夹", + ) args = parser.parse_args() pipeline_start = time.time() @@ -89,8 +105,25 @@ def main() -> None: print(f"图表模式: 详情模式(显示所有枢轴点)") else: print(f"图表模式: 简洁模式(仅显示价格和趋势线)") + if args.all_stocks: + print(f"图表范围: 所有108只股票(包括不满足条件的)") + else: + print(f"图表范围: 仅满足收敛三角形条件的股票") print("=" * 80) + # ======================================================================== + # 步骤 0: 清空输出目录(可选) + # ======================================================================== + if args.clean: + import shutil + output_base = os.path.join(os.path.dirname(__file__), "..", "outputs", "converging_triangles") + if os.path.exists(output_base): + print("\n[清空输出目录]") + print(f" 删除: {output_base}") + shutil.rmtree(output_base) + os.makedirs(output_base, exist_ok=True) + print(" [OK] 输出目录已清空\n") + results = [] # ======================================================================== @@ -164,6 +197,8 @@ def main() -> None: cmd_args.extend(["--date", str(args.date)]) if args.show_details: cmd_args.append("--show-details") + if args.all_stocks: + cmd_args.append("--all-stocks") sys.argv = cmd_args @@ -180,6 +215,36 @@ def main() -> None: print("\n[跳过图表绘制步骤]") results.append(("绘制图表", None, 0)) + # ======================================================================== + # 步骤 4: 生成HTML查看器 + # ======================================================================== + if not args.skip_viewer: + print_section_header("生成HTML查看器 - 可视化强度分筛选", 4) + step_start = time.time() + + try: + # 设置命令行参数 + cmd_args = [sys.argv[0]] + if args.date: + cmd_args.extend(["--date", str(args.date)]) + if args.all_stocks: + cmd_args.append("--all-stocks") + + sys.argv = cmd_args + + run_viewer() + success = True + except Exception as e: + print(f"\n❌ HTML查看器生成失败: {e}") + success = False + + step_duration = time.time() - step_start + print_step_result(success, step_duration) + results.append(("生成查看器", success, step_duration)) + else: + print("\n[跳过HTML查看器生成步骤]") + results.append(("生成查看器", None, 0)) + # ======================================================================== # 流水线总结 # ======================================================================== @@ -216,6 +281,7 @@ def main() -> None: print(" - outputs/converging_triangles/all_results.csv") print(" - outputs/converging_triangles/report.md") print(" - outputs/converging_triangles/charts/*.png") + print(" - outputs/converging_triangles/stock_viewer.html ← 用浏览器打开") else: print("\n[流水线部分失败,请检查上述错误信息]") diff --git a/scripts/plot_converging_triangles.py b/scripts/plot_converging_triangles.py index 0c24de0..d95b9a6 100644 --- a/scripts/plot_converging_triangles.py +++ b/scripts/plot_converging_triangles.py @@ -34,7 +34,7 @@ sys.path.append(os.path.join(os.path.dirname(__file__), "..", "src")) from converging_triangle import ( ConvergingTriangleParams, detect_converging_triangle, - fit_pivot_line, + fit_pivot_line_dispatch, line_y, pivots_fractal, pivots_fractal_hybrid, @@ -119,6 +119,7 @@ def plot_triangle( output_path: str, display_window: int = 500, # 显示窗口大小 show_details: bool = False, # 是否显示详细调试信息 + force_plot: bool = False, # 强制绘图(即使不满足三角形条件) ) -> None: """绘制单只股票的收敛三角形图""" @@ -132,8 +133,12 @@ def plot_triangle( valid_indices = np.where(valid_mask)[0] if len(valid_indices) < params.window: - print(f" [跳过] {stock_code} {stock_name}: 有效数据不足") - return + if force_plot: + print(f" [警告] {stock_code} {stock_name}: 有效数据不足,仅绘制基础K线") + # 继续绘制基础K线 + else: + print(f" [跳过] {stock_code} {stock_name}: 有效数据不足") + return # 找到date_idx在有效数据中的位置 if date_idx not in valid_indices: @@ -141,16 +146,28 @@ def plot_triangle( return valid_end = np.where(valid_indices == date_idx)[0][0] - if valid_end < params.window - 1: + + # 检查窗口数据是否充足 + has_enough_data = valid_end >= params.window - 1 + + if not has_enough_data and not force_plot: print(f" [跳过] {stock_code} {stock_name}: 窗口数据不足") return # 提取检测窗口数据(用于三角形检测) - detect_start = valid_end - params.window + 1 - high_win = high_stock[valid_mask][detect_start:valid_end + 1] - low_win = low_stock[valid_mask][detect_start:valid_end + 1] - close_win = close_stock[valid_mask][detect_start:valid_end + 1] - volume_win = volume_stock[valid_mask][detect_start:valid_end + 1] + if has_enough_data: + detect_start = valid_end - params.window + 1 + high_win = high_stock[valid_mask][detect_start:valid_end + 1] + low_win = low_stock[valid_mask][detect_start:valid_end + 1] + close_win = close_stock[valid_mask][detect_start:valid_end + 1] + volume_win = volume_stock[valid_mask][detect_start:valid_end + 1] + else: + # 数据不足,使用所有可用数据 + detect_start = 0 + high_win = high_stock[valid_mask][:valid_end + 1] + low_win = low_stock[valid_mask][:valid_end + 1] + close_win = close_stock[valid_mask][:valid_end + 1] + volume_win = volume_stock[valid_mask][:valid_end + 1] # 提取显示窗口数据(用于绘图,更长的历史) display_start = max(0, valid_end - display_window + 1) @@ -162,74 +179,93 @@ def plot_triangle( # ======================================================================== # 计算三角形参数(用于绘图) - # 注意:不验证 is_valid,因为CSV中已经验证通过了 - # 这里只是重新计算参数用于可视化 + # force_plot模式:即使不满足条件也尝试检测,检测失败则只画K线 # ======================================================================== - result = detect_converging_triangle( - high=high_win, - low=low_win, - close=close_win, - volume=volume_win, - params=params, - stock_idx=stock_idx, - date_idx=date_idx, - ) + result = None + has_triangle = False - # 不再检查 is_valid,直接绘图 - # 原因:CSV中已经包含了通过验证的股票,这里只需要可视化 + if has_enough_data: + result = detect_converging_triangle( + high=high_win, + low=low_win, + close=close_win, + volume=volume_win, + params=params, + stock_idx=stock_idx, + date_idx=date_idx, + real_time_mode=REALTIME_MODE, + flexible_zone=FLEXIBLE_ZONE, + ) + has_triangle = result.is_valid if result else False + + # 在force_plot模式下,即使没有有效三角形也继续绘图(仅K线) + if not force_plot and not has_triangle: + print(f" [跳过] {stock_code} {stock_name}: 不满足收敛三角形条件") + return # 绘图准备 x_display = np.arange(len(display_close), dtype=float) - # 计算三角形在显示窗口中的位置偏移 - triangle_offset = len(display_close) - len(close_win) - - # 获取检测窗口的起止索引(相对于检测窗口内部) - n = len(close_win) - x_win = np.arange(n, dtype=float) - - # 计算枢轴点(与检测算法一致,考虑实时模式) - if REALTIME_MODE: - confirmed_ph, confirmed_pl, candidate_ph, candidate_pl = pivots_fractal_hybrid( - high_win, low_win, k=params.pivot_k, flexible_zone=FLEXIBLE_ZONE + # 只在有三角形时计算三角形相关参数 + if has_triangle and has_enough_data: + # 计算三角形在显示窗口中的位置偏移 + triangle_offset = len(display_close) - len(close_win) + + # 获取检测窗口的起止索引(相对于检测窗口内部) + n = len(close_win) + x_win = np.arange(n, dtype=float) + + # 计算枢轴点(与检测算法一致,考虑实时模式) + if REALTIME_MODE: + confirmed_ph, confirmed_pl, candidate_ph, candidate_pl = pivots_fractal_hybrid( + high_win, low_win, k=params.pivot_k, flexible_zone=FLEXIBLE_ZONE + ) + # 合并确认枢轴点和候选枢轴点 + ph_idx = np.concatenate([confirmed_ph, candidate_ph]) if len(candidate_ph) > 0 else confirmed_ph + pl_idx = np.concatenate([confirmed_pl, candidate_pl]) if len(candidate_pl) > 0 else confirmed_pl + # 排序以保证顺序 + ph_idx = np.sort(ph_idx) + pl_idx = np.sort(pl_idx) + else: + ph_idx, pl_idx = pivots_fractal(high_win, low_win, k=params.pivot_k) + + # 使用枢轴点连线法拟合边界线(与检测算法一致) + # 注意:绘图用的是检测窗口数据,因此 window_start=0, window_end=n-1 + a_u, b_u, selected_ph = fit_pivot_line_dispatch( + pivot_indices=ph_idx, + pivot_values=high_win[ph_idx], + mode="upper", + method=params.fitting_method, + all_prices=high_win, + window_start=0, + window_end=n - 1, ) - # 合并确认枢轴点和候选枢轴点 - ph_idx = np.concatenate([confirmed_ph, candidate_ph]) if len(candidate_ph) > 0 else confirmed_ph - pl_idx = np.concatenate([confirmed_pl, candidate_pl]) if len(candidate_pl) > 0 else confirmed_pl - # 排序以保证顺序 - ph_idx = np.sort(ph_idx) - pl_idx = np.sort(pl_idx) - else: - ph_idx, pl_idx = pivots_fractal(high_win, low_win, k=params.pivot_k) - - # 使用枢轴点连线法拟合边界线(与检测算法一致) - a_u, b_u, selected_ph = fit_pivot_line( - pivot_indices=ph_idx, - pivot_values=high_win[ph_idx], - mode="upper", - ) - a_l, b_l, selected_pl = fit_pivot_line( - pivot_indices=pl_idx, - pivot_values=low_win[pl_idx], - mode="lower", - ) - - # 三角形线段在显示窗口中的X坐标(只画检测窗口范围) - xw_in_display = np.arange(triangle_offset, triangle_offset + n, dtype=float) - # 计算Y值使用检测窗口内部的X坐标 - upper_line = line_y(a_u, b_u, x_win) - lower_line = line_y(a_l, b_l, x_win) - - # 获取选中的枢轴点在显示窗口中的位置(用于标注) - ph_display_idx = ph_idx + triangle_offset - pl_display_idx = pl_idx + triangle_offset - selected_ph_pos = ph_idx[selected_ph] if len(selected_ph) > 0 else np.array([], dtype=int) - selected_pl_pos = pl_idx[selected_pl] if len(selected_pl) > 0 else np.array([], dtype=int) - selected_ph_display = selected_ph_pos + triangle_offset - selected_pl_display = selected_pl_pos + triangle_offset - - # 三角形检测窗口的日期范围(用于标题) - detect_dates = dates[valid_indices[detect_start:valid_end + 1]] + a_l, b_l, selected_pl = fit_pivot_line_dispatch( + pivot_indices=pl_idx, + pivot_values=low_win[pl_idx], + mode="lower", + method=params.fitting_method, + all_prices=low_win, + window_start=0, + window_end=n - 1, + ) + + # 三角形线段在显示窗口中的X坐标(只画检测窗口范围) + xw_in_display = np.arange(triangle_offset, triangle_offset + n, dtype=float) + # 计算Y值使用检测窗口内部的X坐标 + upper_line = line_y(a_u, b_u, x_win) + lower_line = line_y(a_l, b_l, x_win) + + # 获取选中的枢轴点在显示窗口中的位置(用于标注) + ph_display_idx = ph_idx + triangle_offset + pl_display_idx = pl_idx + triangle_offset + selected_ph_pos = ph_idx[selected_ph] if len(selected_ph) > 0 else np.array([], dtype=int) + selected_pl_pos = pl_idx[selected_pl] if len(selected_pl) > 0 else np.array([], dtype=int) + selected_ph_display = selected_ph_pos + triangle_offset + selected_pl_display = selected_pl_pos + triangle_offset + + # 三角形检测窗口的日期范围(用于标题) + detect_dates = dates[valid_indices[detect_start:valid_end + 1]] # 创建图表 fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 8), @@ -237,41 +273,19 @@ def plot_triangle( # 主图:价格和趋势线(使用显示窗口数据) ax1.plot(x_display, display_close, linewidth=1.5, label='收盘价', color='black', alpha=0.7) - ax1.plot(xw_in_display, upper_line, linewidth=2, label='上沿', color='red', linestyle='--') - ax1.plot(xw_in_display, lower_line, linewidth=2, label='下沿', color='green', linestyle='--') + + # 只在有三角形时绘制趋势线 + if has_triangle and has_enough_data: + ax1.plot(xw_in_display, upper_line, linewidth=2, label='上沿', color='red', linestyle='--') + ax1.plot(xw_in_display, lower_line, linewidth=2, label='下沿', color='green', linestyle='--') + ax1.axvline(len(display_close) - 1, color='gray', linestyle=':', linewidth=1, alpha=0.5) # ======================================================================== - # 详细模式:显示所有枢轴点、拟合点、分段线(仅在 show_details=True 时) + # 详细模式:显示拟合点(仅在 show_details=True 且有三角形时) # ======================================================================== - if show_details: - # 标注所有枢轴点(小实心点,较浅颜色) - if len(ph_display_idx) > 0: - ax1.scatter( - ph_display_idx, - high_win[ph_idx], - marker='o', - s=50, - facecolors='red', - edgecolors='none', - alpha=0.4, - zorder=4, - label=f'所有高点枢轴点({len(ph_idx)})', - ) - if len(pl_display_idx) > 0: - ax1.scatter( - pl_display_idx, - low_win[pl_idx], - marker='o', - s=50, - facecolors='green', - edgecolors='none', - alpha=0.4, - zorder=4, - label=f'所有低点枢轴点({len(pl_idx)})', - ) - - # 标注选中的枢轴点(用于拟合线的关键点,大空心圆) + if show_details and has_triangle and has_enough_data: + # 标注选中的枢轴点(用于拟合线的关键点) if len(selected_ph_display) >= 2: ax1.scatter( selected_ph_display, @@ -297,14 +311,38 @@ def plot_triangle( label=f'下沿拟合点({len(selected_pl_pos)})', ) - ax1.set_title( - f"{stock_code} {stock_name} - 收敛三角形 (检测窗口: {detect_dates[0]} ~ {detect_dates[-1]})\n" - f"显示范围: {display_dates[0]} ~ {display_dates[-1]} ({len(display_dates)}个交易日) " - f"突破方向: {result.breakout_dir} 宽度比: {result.width_ratio:.2f} " - f"枢轴点: 高{len(ph_idx)}/低{len(pl_idx)} 触碰: 上{result.touches_upper}/下{result.touches_lower} " - f"放量确认: {'是' if result.volume_confirmed else '否' if result.volume_confirmed is False else '-'}", - fontsize=11, pad=10 - ) + # 准备标题内容 + if has_triangle and has_enough_data and result: + # 有效三角形:显示完整信息和强度分 + if result.breakout_dir == "up": + strength = result.breakout_strength_up + price_score = result.price_score_up + elif result.breakout_dir == "down": + strength = result.breakout_strength_down + price_score = result.price_score_down + else: + strength = max(result.breakout_strength_up, result.breakout_strength_down) + price_score = max(result.price_score_up, result.price_score_down) + + ax1.set_title( + f"{stock_code} {stock_name} - 收敛三角形 (检测窗口: {detect_dates[0]} ~ {detect_dates[-1]})\n" + f"显示范围: {display_dates[0]} ~ {display_dates[-1]} ({len(display_dates)}个交易日) " + f"突破方向: {result.breakout_dir} 宽度比: {result.width_ratio:.2f} " + f"枢轴点: 高{len(ph_idx)}/低{len(pl_idx)} 触碰: 上{result.touches_upper}/下{result.touches_lower} " + f"放量确认: {'是' if result.volume_confirmed else '否' if result.volume_confirmed is False else '-'}\n" + f"强度分: {strength:.3f} " + f"(价格: {price_score:.3f}×50% + 收敛: {result.convergence_score:.3f}×20% + " + f"成交量: {result.volume_score:.3f}×15% + 拟合贴合度: {result.fitting_score:.3f}×15%)", + fontsize=11, pad=10 + ) + else: + # 无三角形:仅显示基础信息和强度分0分 + ax1.set_title( + f"{stock_code} {stock_name} - K线图(不满足收敛三角形条件)\n" + f"显示范围: {display_dates[0]} ~ {display_dates[-1]} ({len(display_dates)}个交易日)\n" + f"强度分: 0.000 (未检测到收敛三角形形态)", + fontsize=11, pad=10 + ) ax1.set_ylabel('价格', fontsize=10) ax1.legend(loc='best', fontsize=9) ax1.grid(True, alpha=0.3) @@ -353,15 +391,22 @@ def main() -> None: action="store_true", help="显示详细调试信息(枢轴点、拟合点、分段线等)", ) + parser.add_argument( + "--all-stocks", + action="store_true", + help="为所有108只股票生成图表(包括不满足收敛三角形条件的)", + ) args = parser.parse_args() # 确定是否显示详细信息(命令行参数优先) show_details = args.show_details if hasattr(args, 'show_details') else SHOW_CHART_DETAILS + all_stocks = args.all_stocks if hasattr(args, 'all_stocks') else False print("=" * 70) print("收敛三角形图表生成") print("=" * 70) print(f"详细模式: {'开启' if show_details else '关闭'} {'(--show-details)' if show_details else '(简洁模式)'}") + print(f"图表范围: {'所有108只股票' if all_stocks else '仅满足条件的股票'} {'(--all-stocks)' if all_stocks else ''}") # 1. 加载数据 print("\n[1] 加载 OHLCV 数据...") @@ -386,12 +431,41 @@ def main() -> None: print(f"\n[2] 目标日期: {target_date}") # 3. 加载当日股票列表 - stocks = load_daily_stocks(args.input, target_date) - print(f" 当日满足三角形的股票数: {len(stocks)}") - - if not stocks: - print("当日无满足条件的股票") - return + if all_stocks: + # 模式1: 绘制所有股票(包括不满足条件的) + print(f" 模式: 所有股票") + print(f" 股票总数: {len(tkrs)}") + + # 为所有股票创建stock字典(从CSV获取已有的强度分,未检测的置为0) + stocks = [] + csv_stocks = load_daily_stocks(args.input, target_date) + csv_map = {s["stock_idx"]: s for s in csv_stocks} + + for idx in range(len(tkrs)): + if idx in csv_map: + # 已检测过的股票,使用CSV数据 + stocks.append(csv_map[idx]) + else: + # 未检测过的股票,使用默认值 + stocks.append({ + "stock_idx": idx, + "stock_code": tkrs[idx], + "stock_name": tkrs_name[idx], + "breakout_dir": "none", + "breakout_strength_up": 0.0, + "breakout_strength_down": 0.0, + }) + + print(f" 其中满足三角形条件: {len(csv_stocks)} 只") + print(f" 不满足条件(将显示基础K线): {len(tkrs) - len(csv_stocks)} 只") + else: + # 模式2: 仅绘制满足条件的股票 + stocks = load_daily_stocks(args.input, target_date) + print(f" 当日满足三角形的股票数: {len(stocks)}") + + if not stocks: + print("当日无满足条件的股票") + return # 4. 创建输出目录并清空对应模式的旧图片 os.makedirs(args.output_dir, exist_ok=True) @@ -427,9 +501,12 @@ def main() -> None: stock_code = stock["stock_code"] stock_name = stock["stock_name"] + # 清理文件名中的非法字符(Windows文件名不允许: * ? " < > | : / \) + stock_name_clean = stock_name.replace('*', '').replace('?', '').replace('"', '').replace('<', '').replace('>', '').replace('|', '').replace(':', '').replace('/', '').replace('\\', '') + # 根据详细模式添加文件名后缀 suffix = "_detail" if show_details else "" - output_filename = f"{target_date}_{stock_code}_{stock_name}{suffix}.png" + output_filename = f"{target_date}_{stock_code}_{stock_name_clean}{suffix}.png" output_path = os.path.join(args.output_dir, output_filename) try: @@ -447,6 +524,7 @@ def main() -> None: output_path=output_path, display_window=DISPLAY_WINDOW, # 从配置文件读取 show_details=show_details, # 传递详细模式参数 + force_plot=all_stocks, # 在all_stocks模式下强制绘图 ) except Exception as e: print(f" [错误] {stock_code} {stock_name}: {e}") diff --git a/scripts/triangle_config.py b/scripts/triangle_config.py index 6ea155c..d1c8ee0 100644 --- a/scripts/triangle_config.py +++ b/scripts/triangle_config.py @@ -30,6 +30,8 @@ DETECTION_PARAMS = ConvergingTriangleParams( # 边界拟合 boundary_n_segments=2, # 边界线分段数 boundary_source="full", # 边界拟合数据源: "full"(全部) 或 "pivot"(仅枢轴点) + fitting_method="anchor", # 🆕 拟合方法: "iterative" | "lp" | "quantile" | "anchor" + # anchor方法:固定绝对极值点为锚点,二分搜索最优斜率,使95%点在正确一侧 # 斜率约束(严格收敛三角形) upper_slope_max=0, # 上沿必须向下或水平(≤0) @@ -42,7 +44,7 @@ DETECTION_PARAMS = ConvergingTriangleParams( touch_loss_max=0.10, # 平均触碰误差上限 # 收敛度要求 - shrink_ratio=0.6, # 🔧 更严格:末端≤60%起始宽度 + shrink_ratio=0.45, # 🔧 更严格:末端≤45%起始宽度 # 突破检测 break_tol=0.005, # 🔧 更明显的突破(0.5%) @@ -78,7 +80,7 @@ DEFAULT_PARAMS = ConvergingTriangleParams( # ============================================================================ # 计算范围:None = 全部历史,具体数字 = 最近N天 -RECENT_DAYS = 500 +RECENT_DAYS = 500 # 建议: 500天(默认) | 250天(快速) | 100天(极速) # 显示范围:图表中显示的交易日数 DISPLAY_WINDOW = 500 diff --git a/src/__pycache__/converging_triangle.cpython-313.pyc b/src/__pycache__/converging_triangle.cpython-313.pyc index 67896b3..85e298d 100644 Binary files a/src/__pycache__/converging_triangle.cpython-313.pyc and b/src/__pycache__/converging_triangle.cpython-313.pyc differ diff --git a/src/converging_triangle.py b/src/converging_triangle.py index 65ede3b..45bfe16 100644 --- a/src/converging_triangle.py +++ b/src/converging_triangle.py @@ -33,6 +33,11 @@ class ConvergingTriangleParams: # 边界线拟合 boundary_n_segments: int = 2 boundary_source: str = "full" # "full" | "pivots" + fitting_method: str = "iterative" # "iterative" | "lp" | "quantile" | "anchor" + # - iterative: 迭代离群点移除 + 最小二乘法 (默认) + # - lp: 线性规划凸优化,保证边界线包住所有枢轴点 + # - quantile: 分位数回归,上沿95%分位,下沿5%分位 + # - anchor: 锚点+最优斜率法,固定极值点,二分搜索最优斜率(推荐) # 斜率约束 upper_slope_max: float = 0.10 @@ -69,6 +74,13 @@ class ConvergingTriangleResult: breakout_strength_up: float = 0.0 breakout_strength_down: float = 0.0 + # 突破强度分量 (各维度分数,用于可视化和分析) + price_score_up: float = 0.0 # 价格突破分数(向上) + price_score_down: float = 0.0 # 价格突破分数(向下) + convergence_score: float = 0.0 # 收敛分数 + volume_score: float = 0.0 # 成交量分数 + fitting_score: float = 0.0 # 拟合贴合度分数 + # 几何属性 upper_slope: float = 0.0 lower_slope: float = 0.0 @@ -368,24 +380,456 @@ def fit_pivot_line( return float(a), float(b), selected_original +def fit_boundary_lp( + pivot_indices: np.ndarray, + pivot_values: np.ndarray, + mode: str = "upper", + slope_bound: float = 0.5, +) -> Tuple[float, float, np.ndarray]: + """ + 凸优化:线性规划拟合边界线 + + 核心思想: + - 上沿:找最紧的"天花板",使所有枢轴点都在线下方(或线上),且线尽量贴近数据 + - 下沿:找最紧的"地板",使所有枢轴点都在线上方(或线上),且线尽量贴近数据 + + 这比最小二乘法更符合"压力线/支撑线"的技术分析语义, + 因为边界线应该"包住"数据点,而不是"穿过"数据点。 + + 数学形式: + 上沿问题 (找"天花板",最小化线与点的总距离): + minimize Σ(a*x_i + b - y_i) 即 Σx_i * a + n * b - Σy_i + subject to y_i <= a * x_i + b for all i + -slope_bound <= a <= slope_bound + + 下沿问题 (找"地板",最小化线与点的总距离): + minimize Σ(y_i - a*x_i - b) 即 -Σx_i * a - n * b + Σy_i + subject to y_i >= a * x_i + b for all i + -slope_bound <= a <= slope_bound + + Args: + pivot_indices: 枢轴点的X坐标(索引) + pivot_values: 枢轴点的Y值(价格) + mode: "upper"(上沿) 或 "lower"(下沿) + slope_bound: 斜率绝对值上限(防止极端拟合) + + Returns: + (slope, intercept, selected_indices): 斜率、截距、所有枢轴点索引(LP使用全部点) + """ + from scipy.optimize import linprog + + n = len(pivot_indices) + if n < 2: + return 0.0, 0.0, np.array([]) + + # 数据归一化(避免数值问题) + x = pivot_indices.astype(float) + y = pivot_values.astype(float) + + x_min, x_max = x.min(), x.max() + y_min, y_max = y.min(), y.max() + + # 归一化到 [0, 1] 范围 + x_range = x_max - x_min if x_max > x_min else 1.0 + y_range = y_max - y_min if y_max > y_min else 1.0 + + x_norm = (x - x_min) / x_range + y_norm = (y - y_min) / y_range + + # 变量: [a, b] (斜率, 截距),在归一化空间中 + # 使用 linprog 标准形式: minimize c^T * x, subject to A_ub * x <= b_ub + + if mode == "upper": + # 上沿: minimize Σ(a*x_i + b - y_i) = Σx_i * a + n * b - Σy_i + # 约束: y_i <= a*x_i + b => -a*x_i - b <= -y_i + c = [np.sum(x_norm), n] # 目标函数系数 + A_ub = np.column_stack([-x_norm, -np.ones(n)]) + b_ub = -y_norm + else: + # 下沿: minimize Σ(y_i - a*x_i - b) = -Σx_i * a - n * b + Σy_i + # 约束: y_i >= a*x_i + b => a*x_i + b <= y_i + c = [-np.sum(x_norm), -n] # 目标函数系数 + A_ub = np.column_stack([x_norm, np.ones(n)]) + b_ub = y_norm + + # 斜率限制(归一化空间中) + slope_bound_norm = slope_bound * x_range / y_range + bounds = [(-slope_bound_norm, slope_bound_norm), (None, None)] + + try: + result = linprog(c, A_ub=A_ub, b_ub=b_ub, bounds=bounds, method='highs') + + if result.success: + a_norm, b_norm = result.x + + # 反归一化:从归一化空间转回原始空间 + # y_norm = a_norm * x_norm + b_norm + # (y - y_min)/y_range = a_norm * (x - x_min)/x_range + b_norm + # y = a_norm * y_range/x_range * (x - x_min) + b_norm * y_range + y_min + # y = a_norm * y_range/x_range * x - a_norm * y_range/x_range * x_min + b_norm * y_range + y_min + # y = a * x + b + # where a = a_norm * y_range / x_range + # b = -a_norm * y_range/x_range * x_min + b_norm * y_range + y_min + + a = a_norm * y_range / x_range + b = -a * x_min + b_norm * y_range + y_min + + return float(a), float(b), np.arange(n) + else: + # LP求解失败,回退到普通拟合 + a, b = fit_line(x, y) + return float(a), float(b), np.arange(n) + + except Exception: + # 异常情况回退 + a, b = fit_line(x, y) + return float(a), float(b), np.arange(n) + + +def fit_boundary_quantile( + pivot_indices: np.ndarray, + pivot_values: np.ndarray, + mode: str = "upper", + quantile: float = None, +) -> Tuple[float, float, np.ndarray]: + """ + 分位数回归拟合边界线 + + 核心思想: + - 上沿:拟合高分位数(如95%),使大部分点在线下方 + - 下沿:拟合低分位数(如5%),使大部分点在线上方 + + 相比LP方法,分位数回归对异常值更稳健。 + + 数学形式(分位数回归,也称Quantile Regression): + minimize Σ ρ_τ(y_i - a*x_i - b) + 其中 ρ_τ(u) = u*(τ - I(u<0)) 是分位数损失函数 + + Args: + pivot_indices: 枢轴点的X坐标(索引) + pivot_values: 枢轴点的Y值(价格) + mode: "upper"(上沿) 或 "lower"(下沿) + quantile: 分位数,默认上沿0.95,下沿0.05 + + Returns: + (slope, intercept, selected_indices): 斜率、截距、所有枢轴点索引 + """ + from scipy.optimize import minimize + + n = len(pivot_indices) + if n < 2: + return 0.0, 0.0, np.array([]) + + x = pivot_indices.astype(float) + y = pivot_values.astype(float) + + # 默认分位数 + if quantile is None: + quantile = 0.95 if mode == "upper" else 0.05 + + # 数据标准化(改善优化收敛性) + x_mean, x_std = x.mean(), x.std() if x.std() > 0 else 1.0 + y_mean, y_std = y.mean(), y.std() if y.std() > 0 else 1.0 + + x_scaled = (x - x_mean) / x_std + y_scaled = (y - y_mean) / y_std + + def quantile_loss(params): + """分位数损失函数""" + a, b = params + residuals = y_scaled - (a * x_scaled + b) + # ρ_τ(u) = u * (τ - I(u<0)) + loss = np.where( + residuals >= 0, + quantile * residuals, + (quantile - 1) * residuals + ) + return np.sum(loss) + + # 初始值:普通最小二乘 + a_init, b_init = fit_line(x_scaled, y_scaled) + + try: + result = minimize( + quantile_loss, + x0=[a_init, b_init], + method='Nelder-Mead', + options={'maxiter': 1000} + ) + + if result.success: + a_scaled, b_scaled = result.x + + # 反标准化 + a = a_scaled * y_std / x_std + b = y_mean - a * x_mean + b_scaled * y_std + + return float(a), float(b), np.arange(n) + else: + a, b = fit_line(x, y) + return float(a), float(b), np.arange(n) + + except Exception: + a, b = fit_line(x, y) + return float(a), float(b), np.arange(n) + + +def fit_boundary_anchor( + pivot_indices: np.ndarray, + pivot_values: np.ndarray, + all_prices: np.ndarray, + mode: str = "upper", + coverage: float = 0.95, + exclude_last: int = 1, + window_start: int = 0, + window_end: int = -1, +) -> Tuple[float, float, np.ndarray]: + """ + 锚点+最优斜率拟合法(2026-01-27 新增) + + 核心思路: + 1. 找到窗口内的绝对最高/最低点作为锚点 + 2. 固定锚点,用二分搜索找最优斜率 + 3. 约束:95%的枢轴点在线的正确一侧 + 4. 目标:线尽量贴近数据(斜率尽量平缓) + + Args: + pivot_indices: 枢轴点的X坐标(索引) + pivot_values: 枢轴点的Y值(价格) + all_prices: 全部价格数据(High用于上沿,Low用于下沿) + mode: "upper"(上沿) 或 "lower"(下沿) + coverage: 覆盖率,默认0.95表示95%的点需要在正确一侧 + exclude_last: 排除最后N天(用于突破判断),默认1天 + window_start: 检测窗口的起始索引 + window_end: 检测窗口的结束索引 + + Returns: + (slope, intercept, selected_indices): 斜率、截距、所有枢轴点索引 + """ + n_prices = len(all_prices) + n_pivots = len(pivot_indices) + + if n_pivots < 2 or n_prices < 2: + return 0.0, 0.0, np.array([]) + + # 确定搜索范围:仅在检测窗口内查找锚点 + if window_end < 0: + window_end = n_prices - 1 + + # 排除最后N天(用于突破判断) + search_end = window_end - exclude_last + 1 + search_start = window_start + + if search_end <= search_start: + search_end = window_end + 1 + + # 步骤1:找锚点(窗口内的绝对最高/最低点) + window_prices = all_prices[search_start:search_end] + if mode == "upper": + local_idx = int(np.argmax(window_prices)) + anchor_idx = search_start + local_idx + anchor_value = float(all_prices[anchor_idx]) + else: + local_idx = int(np.argmin(window_prices)) + anchor_idx = search_start + local_idx + anchor_value = float(all_prices[anchor_idx]) + + # 筛选用于拟合的枢轴点(在窗口内且排除最后N天) + valid_mask = (pivot_indices >= search_start) & (pivot_indices < search_end) + fit_indices = pivot_indices[valid_mask] + fit_values = pivot_values[valid_mask] + + if len(fit_indices) < 1: + # 没有有效的枢轴点,返回水平线 + return 0.0, anchor_value, np.array([]) + + n_fit = len(fit_indices) + # 需要包含的点数(95% => 向上取整,避免少量枢轴点时被放松) + target_count = max(1, int(np.ceil(n_fit * coverage))) + + # 步骤2:二分搜索最优斜率 + # 对于上沿:找最小的斜率(最平缓的下降线),使95%点在线下方 + # 对于下沿:找最大的斜率(最平缓的上升线),使95%点在线上方 + + if mode == "upper": + # 上沿:斜率范围 [-1, 0],越大(越接近0)越贴近数据 + slope_low, slope_high = -0.5, 0.5 + + def count_valid(slope): + """计算有多少点在线下方或线上""" + count = 0 + for i in range(n_fit): + x, y = fit_indices[i], fit_values[i] + line_y = slope * (x - anchor_idx) + anchor_value + if y <= line_y * 1.001: # 允许1‰的容差 + count += 1 + return count + + # 二分搜索:找最小的斜率使得count >= target_count + for _ in range(50): # 最多50次迭代 + slope_mid = (slope_low + slope_high) / 2 + if count_valid(slope_mid) >= target_count: + slope_high = slope_mid # 满足条件,尝试更小的斜率 + else: + slope_low = slope_mid # 不满足,需要更大的斜率 + + optimal_slope = slope_high + + else: + # 下沿:斜率范围 [-0.5, 0.5],越小越贴近数据 + slope_low, slope_high = -0.5, 0.5 + + def count_valid(slope): + """计算有多少点在线上方或线上""" + count = 0 + for i in range(n_fit): + x, y = fit_indices[i], fit_values[i] + line_y = slope * (x - anchor_idx) + anchor_value + if y >= line_y * 0.999: # 允许1‰的容差 + count += 1 + return count + + # 二分搜索:找最大的斜率使得count >= target_count + for _ in range(50): + slope_mid = (slope_low + slope_high) / 2 + if count_valid(slope_mid) >= target_count: + slope_low = slope_mid # 满足条件,尝试更大的斜率 + else: + slope_high = slope_mid # 不满足,需要更小的斜率 + + optimal_slope = slope_low + + # 计算截距:y = slope * (x - anchor_idx) + anchor_value + # 转换为 y = slope * x + intercept 形式 + intercept = anchor_value - optimal_slope * anchor_idx + + return float(optimal_slope), float(intercept), np.arange(n_pivots) + + +def fit_pivot_line_dispatch( + pivot_indices: np.ndarray, + pivot_values: np.ndarray, + mode: str = "upper", + method: str = "iterative", + **kwargs +) -> Tuple[float, float, np.ndarray]: + """ + 枢轴点拟合分发函数 + + 根据 method 参数选择不同的拟合算法: + - "iterative": 迭代离群点移除 + 最小二乘法(默认,保守稳定) + - "lp": 线性规划凸优化(数学严谨,保证边界包络) + - "quantile": 分位数回归(统计稳健,处理异常值好) + - "anchor": 锚点+最优斜率法(固定极值点,优化斜率) + + Args: + pivot_indices: 枢轴点的X坐标(索引) + pivot_values: 枢轴点的Y值(价格) + mode: "upper"(上沿) 或 "lower"(下沿) + method: 拟合方法 "iterative" | "lp" | "quantile" | "anchor" + **kwargs: 传递给具体拟合函数的参数 + + Returns: + (slope, intercept, selected_indices): 斜率、截距、选中的枢轴点索引 + """ + if method == "lp": + return fit_boundary_lp(pivot_indices, pivot_values, mode, **kwargs) + elif method == "quantile": + return fit_boundary_quantile(pivot_indices, pivot_values, mode, **kwargs) + elif method == "anchor": + # anchor方法需要额外的参数 + all_prices = kwargs.pop('all_prices', None) + window_start = kwargs.pop('window_start', 0) + window_end = kwargs.pop('window_end', -1) + if all_prices is None: + # 如果没有提供all_prices,回退到iterative + return fit_pivot_line(pivot_indices, pivot_values, mode, **kwargs) + return fit_boundary_anchor( + pivot_indices, pivot_values, all_prices, mode, + window_start=window_start, window_end=window_end, **kwargs + ) + else: + # 默认使用迭代法 + return fit_pivot_line(pivot_indices, pivot_values, mode, **kwargs) + + # ============================================================================ # 突破强度计算 # ============================================================================ +def calc_fitting_adherence( + pivot_indices: np.ndarray, + pivot_values: np.ndarray, + slope: float, + intercept: float, +) -> float: + """ + 计算枢轴点到拟合线的贴合度分数 (0~1) + + 使用平均相对误差来衡量枢轴点与拟合线的贴合程度。 + 贴合度越高,说明形态越标准、越纯净。 + + 计算步骤: + 1. 计算每个枢轴点在拟合线上的预测值:fitted = slope * x + intercept + 2. 计算相对误差:rel_error = abs(actual - fitted) / abs(fitted) + 3. 求平均相对误差:mean_rel_error = mean(rel_errors) + 4. 用指数函数归一化:score = exp(-mean_rel_error * scale_factor) + + 归一化映射(scale_factor = 20): + - 误差 0% → 分数 1.00 (完美拟合) + - 误差 2% → 分数 0.67 (良好拟合) + - 误差 5% → 分数 0.37 (一般拟合) + - 误差 10% → 分数 0.14 (较差拟合) + + Args: + pivot_indices: 选中枢轴点的X坐标(索引) + pivot_values: 选中枢轴点的Y值(价格) + slope: 拟合线斜率 + intercept: 拟合线截距 + + Returns: + adherence_score: 0~1 分数,越大表示枢轴点越贴合拟合线 + """ + import math + + if len(pivot_indices) == 0 or len(pivot_values) == 0: + return 0.0 + + # 计算拟合值 + fitted_values = slope * pivot_indices.astype(float) + intercept + + # 计算相对误差(避免除零) + rel_errors = np.abs(pivot_values - fitted_values) / np.maximum(np.abs(fitted_values), 1e-9) + + # 平均相对误差 + mean_rel_error = float(np.mean(rel_errors)) + + # 指数衰减归一化到 0~1 + SCALE_FACTOR = 20.0 # 控制衰减速度 + adherence_score = math.exp(-mean_rel_error * SCALE_FACTOR) + + return min(1.0, max(0.0, adherence_score)) + + def calc_breakout_strength( close: float, upper_line: float, lower_line: float, volume_ratio: float, width_ratio: float, -) -> Tuple[float, float]: + fitting_adherence: float, +) -> Tuple[float, float, float, float, float, float]: """ - 计算向上/向下突破强度 (0~1) + 计算形态强度分 (0~1) + + 综合评估收敛三角形的质量,无论是否突破都计算得分。 + 可用于评估"待突破"形态的潜在价值,或"已突破"形态的有效性。 使用加权求和,各分量权重: - - 突破幅度分 (60%): tanh 非线性归一化,3%突破≈0.42,5%突破≈0.64,10%突破≈0.91 - - 收敛分 (25%): 1 - width_ratio,收敛越强分数越高 + - 突破幅度分 (50%): tanh 非线性归一化,3%突破≈0.42,5%突破≈0.64,10%突破≈0.91 + - 收敛分 (20%): 1 - width_ratio,收敛越强分数越高 - 成交量分 (15%): 放量程度,2倍放量=满分 + - 拟合贴合度 (15%): 枢轴点到拟合线的贴合程度,形态纯度 突破幅度分布参考(使用 tanh(pct * 15)): - 1% 突破 → 0.15 @@ -401,16 +845,19 @@ def calc_breakout_strength( lower_line: 下沿价格 volume_ratio: 成交量相对均值的倍数 width_ratio: 末端宽度/起始宽度 + fitting_adherence: 拟合贴合度分数 (0~1) Returns: - (strength_up, strength_down) + (strength_up, strength_down, price_score_up, price_score_down, convergence_score, vol_score, fitting_score) + 返回总强度和各分量分数,用于可视化和分析 """ import math # 权重配置 - W_PRICE = 0.60 # 突破幅度权重 - W_CONVERGENCE = 0.25 # 收敛度权重 + W_PRICE = 0.50 # 突破幅度权重 + W_CONVERGENCE = 0.20 # 收敛度权重 W_VOLUME = 0.15 # 成交量权重 + W_FITTING = 0.15 # 拟合贴合度权重 TANH_SCALE = 15.0 # tanh 缩放因子 # 1. 价格突破分数(tanh 非线性归一化) @@ -432,27 +879,34 @@ def calc_breakout_strength( # 3. 成交量分数(vol_ratio > 1 时才有分) vol_score = min(1.0, max(0.0, volume_ratio - 1.0)) if volume_ratio > 0 else 0.0 - # 4. 加权求和 - # 只有发生突破(price_score > 0)时才计算完整强度 - if price_score_up > 0: - strength_up = ( - W_PRICE * price_score_up + - W_CONVERGENCE * convergence_score + - W_VOLUME * vol_score - ) - else: - strength_up = 0.0 + # 4. 拟合贴合度分数(直接使用传入的分数) + fitting_score = max(0.0, min(1.0, fitting_adherence)) - if price_score_down > 0: - strength_down = ( - W_PRICE * price_score_down + - W_CONVERGENCE * convergence_score + - W_VOLUME * vol_score - ) - else: - strength_down = 0.0 + # 5. 加权求和(计算综合强度分) + # 不再要求必须突破,而是计算形态的综合质量分数 + strength_up = ( + W_PRICE * price_score_up + + W_CONVERGENCE * convergence_score + + W_VOLUME * vol_score + + W_FITTING * fitting_score + ) - return min(1.0, strength_up), min(1.0, strength_down) + strength_down = ( + W_PRICE * price_score_down + + W_CONVERGENCE * convergence_score + + W_VOLUME * vol_score + + W_FITTING * fitting_score + ) + + return ( + min(1.0, strength_up), + min(1.0, strength_down), + price_score_up, + price_score_down, + convergence_score, + vol_score, + fitting_score + ) # ============================================================================ @@ -529,18 +983,32 @@ def detect_converging_triangle( return invalid_result # 使用枢轴点连线法拟合边界线 + # 根据 fitting_method 选择拟合算法: + # - "iterative": 迭代离群点移除 (默认) + # - "lp": 线性规划凸优化 + # - "quantile": 分位数回归 + # - "anchor": 锚点+最优斜率法(固定极值点,优化斜率) + # 上沿:连接高点枢轴点,形成下降趋势 - a_u, b_u, selected_ph = fit_pivot_line( + a_u, b_u, selected_ph = fit_pivot_line_dispatch( pivot_indices=ph_in, pivot_values=high[ph_in], mode="upper", + method=params.fitting_method, + all_prices=high, # anchor方法需要 + window_start=start, # anchor方法需要 + window_end=end, # anchor方法需要 ) # 下沿:连接低点枢轴点,形成上升趋势 - a_l, b_l, selected_pl = fit_pivot_line( + a_l, b_l, selected_pl = fit_pivot_line_dispatch( pivot_indices=pl_in, pivot_values=low[pl_in], mode="lower", + method=params.fitting_method, + all_prices=low, # anchor方法需要 + window_start=start, # anchor方法需要 + window_end=end, # anchor方法需要 ) if len(selected_ph) < 2 or len(selected_pl) < 2: @@ -639,13 +1107,32 @@ def detect_converging_triangle( # 注意: 这里是基于历史数据,无法检测假突破 # 假突破需要看"未来"数据,与当前设计不符 - # 计算突破强度 - strength_up, strength_down = calc_breakout_strength( + # 计算拟合贴合度(上下沿综合) + adherence_upper = calc_fitting_adherence( + pivot_indices=selected_ph, + pivot_values=high[selected_ph], + slope=a_u, + intercept=b_u, + ) + adherence_lower = calc_fitting_adherence( + pivot_indices=selected_pl, + pivot_values=low[selected_pl], + slope=a_l, + intercept=b_l, + ) + # 综合上下沿贴合度(取平均) + fitting_adherence = (adherence_upper + adherence_lower) / 2.0 + + # 计算突破强度(返回总强度和各分量分数) + (strength_up, strength_down, + price_score_up, price_score_down, + convergence_score, vol_score, fitting_score) = calc_breakout_strength( close=close[end], upper_line=upper_end, lower_line=lower_end, volume_ratio=volume_ratio, width_ratio=width_ratio, + fitting_adherence=fitting_adherence, ) return ConvergingTriangleResult( @@ -654,6 +1141,11 @@ def detect_converging_triangle( is_valid=True, breakout_strength_up=strength_up, breakout_strength_down=strength_down, + price_score_up=price_score_up, + price_score_down=price_score_down, + convergence_score=convergence_score, + volume_score=vol_score, + fitting_score=fitting_score, upper_slope=a_u, lower_slope=a_l, width_ratio=width_ratio, @@ -708,6 +1200,7 @@ def detect_converging_triangle_batch( - stock_idx, date_idx - is_valid - breakout_strength_up, breakout_strength_down + - price_score_up, price_score_down, convergence_score, volume_score, fitting_score - upper_slope, lower_slope, width_ratio - touches_upper, touches_lower, apex_x - breakout_dir, volume_confirmed, false_breakout