Implement converging triangle detection pipeline and enhance documentation

- Added pipeline_converging_triangle.py for streamlined execution of detection, reporting, and chart generation.
- Introduced triangle_config.py for centralized parameter management across scripts.
- Updated plot_converging_triangles.py to utilize parameters from the new config file.
- Revised report_converging_triangles.py to reflect dynamic detection window based on configuration.
- Enhanced existing scripts for improved error handling and output consistency.
- Added new documentation files for usage instructions and parameter configurations.
This commit is contained in:
褚宏光 2026-01-22 11:29:04 +08:00
parent 8dea3fbccb
commit 5455f8e456
33 changed files with 8279 additions and 5375 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# 收敛三角形图表输出(本地生成,不推送)
outputs/converging_triangles/charts/

View File

@ -0,0 +1,63 @@
# 枢轴点绘图问题记录
> 日期2026-01-22
> 主题:收敛三角形图表中枢轴点位置异常与修复
## 背景现象
`outputs/converging_triangles/charts` 生成的图表中,标注的枢轴点(用于连线的关键点)出现**挤在一起**、位置不正确的问题。
## 根因分析
`fit_pivot_line()` 返回的 `selected_ph / selected_pl` 是**枢轴点数组中的索引**,而非检测窗口的真实索引。
绘图时直接用 `selected_ph` 作为 `high_win`/`low_win` 的索引,导致标注位置偏移到窗口开头。
问题代码路径:
- `scripts/plot_converging_triangles.py`
- 相关逻辑:`selected_ph` / `selected_pl``ph_idx` / `pl_idx` 的索引映射
## 修复方式
将枢轴点索引转换为真实位置:
```python
selected_ph_pos = ph_idx[selected_ph]
selected_pl_pos = pl_idx[selected_pl]
```
绘图时使用真实位置:
```python
ax1.scatter(selected_ph_display, high_win[selected_ph_pos], ...)
ax1.scatter(selected_pl_display, low_win[selected_pl_pos], ...)
```
## 视觉优化
将枢轴点标注样式改为**空心圆圈**,避免遮挡价格曲线:
```python
marker='o', facecolors='none', edgecolors='red/green', linewidths=1.5
```
## 更新文件
- `scripts/plot_converging_triangles.py`
- 修复枢轴点索引映射
- 空心圆圈样式
## 验证步骤
重新生成图表:
```bash
python scripts/plot_converging_triangles.py
```
验证要点:
- 红/绿空心圆应落在真实高点/低点位置
- 不再出现“所有枢轴点挤在一起”的情况
---
如需进一步增强可视化,可新增“显示所有枢轴点”的开关,用于诊断与验证。

View File

@ -0,0 +1,347 @@
# 收敛三角形检测系统 - 使用指南
> 最后更新2026-01-22
> 版本v1.0
## 目录
1. [系统概述](#系统概述)
2. [快速开始](#快速开始)
3. [参数配置](#参数配置)
4. [脚本说明](#脚本说明)
5. [输出文件](#输出文件)
6. [算法原理](#算法原理)
7. [常见问题](#常见问题)
---
## 系统概述
收敛三角形检测系统用于自动识别股票K线中的收敛三角形形态并计算突破强度分数进行选股。
### 功能特点
- **批量检测**:支持多股票、多日期的滚动窗口检测
- **突破强度评分**0~1 连续分数,量化突破有效性
- **可视化图表**:自动生成个股三角形图表
- **选股报告**:每日 Markdown 格式选股简报
### 系统架构
```
technical-patterns-lab/
├── src/
│ └── converging_triangle.py # 核心算法
├── scripts/
│ ├── triangle_config.py # 参数配置(统一管理)
│ ├── run_converging_triangle.py # 批量检测
│ ├── report_converging_triangles.py # 报告生成
│ ├── plot_converging_triangles.py # 图表绘制
│ └── pipeline_converging_triangle.py # 一键流水线
├── outputs/converging_triangles/
│ ├── all_results.csv # 全部检测结果
│ ├── report.md # 选股报告
│ └── charts/ # 个股图表
└── data/
└── pkl/ # OHLCV 数据文件
```
---
## 快速开始
### 1. 一键运行(推荐)
```bash
cd technical-patterns-lab
python scripts/pipeline_converging_triangle.py
```
这会依次执行:
1. 批量检测 → 生成 `all_results.csv`
2. 报告生成 → 生成 `report.md`
3. 图表绘制 → 生成 `charts/*.png`
### 2. 指定日期运行
```bash
python scripts/pipeline_converging_triangle.py --date 20260120
```
### 3. 跳过部分步骤
```bash
# 跳过检测(使用已有结果),只生成报告和图表
python scripts/pipeline_converging_triangle.py --skip-detection
# 只运行检测
python scripts/pipeline_converging_triangle.py --skip-report --skip-plot
```
### 4. 单独运行各步骤
```bash
# 批量检测
python scripts/run_converging_triangle.py
# 生成报告
python scripts/report_converging_triangles.py
# 绘制图表
python scripts/plot_converging_triangles.py
```
---
## 参数配置
所有参数统一在 `scripts/triangle_config.py` 中管理:
### 检测参数(当前使用严格模式)
| 参数 | 严格模式 | 默认模式 | 宽松模式 | 说明 |
|------|----------|----------|----------|------|
| `window` | 120 | 120 | 120 | 检测窗口(交易日) |
| `pivot_k` | 15 | 15 | 15 | 枢轴点检测周期 |
| `shrink_ratio` | **0.6** | 0.8 | 0.85 | 收敛比例阈值 |
| `break_tol` | **0.005** | 0.001 | 0.001 | 突破判定容差 |
| `vol_k` | **1.5** | 1.3 | 1.2 | 放量确认倍数 |
### 数据范围配置
| 参数 | 值 | 说明 |
|------|-----|------|
| `RECENT_DAYS` | 500 | 计算最近 N 个交易日 |
| `DISPLAY_WINDOW` | 500 | 图表显示范围 |
| `ONLY_VALID` | True | 只输出有效三角形 |
### 切换参数模式
编辑 `scripts/triangle_config.py`,修改 `DETECTION_PARAMS` 的定义:
```python
# 当前使用严格模式(推荐)
DETECTION_PARAMS = ConvergingTriangleParams(
shrink_ratio=0.6, # 更严格的收敛要求
break_tol=0.005, # 更明显的突破
vol_k=1.5, # 更强的放量要求
...
)
```
---
## 脚本说明
### run_converging_triangle.py
**功能**:批量检测收敛三角形
**输出**
- `outputs/converging_triangles/all_results.csv`
- `outputs/converging_triangles/strong_breakout_up.csv`
- `outputs/converging_triangles/strong_breakout_down.csv`
**耗时**:约 15-20 秒108只股票 × 500天
---
### report_converging_triangles.py
**功能**:生成 Markdown 选股报告
**参数**
```bash
# 指定报告日期
python scripts/report_converging_triangles.py --report-date 20260120
# 指定输入输出路径
python scripts/report_converging_triangles.py \
--input outputs/converging_triangles/all_results.csv \
--output outputs/converging_triangles/report.md
```
**输出**`outputs/converging_triangles/report.md`
---
### plot_converging_triangles.py
**功能**:绘制个股收敛三角形图表
**参数**
```bash
# 指定日期
python scripts/plot_converging_triangles.py --date 20260120
# 指定输出目录
python scripts/plot_converging_triangles.py --output-dir outputs/converging_triangles/charts
```
**输出**`outputs/converging_triangles/charts/*.png`
**特点**
- 每次运行自动清空旧图片
- 显示 500 天历史走势
- 检测窗口 120 天高亮显示
- 支持中文字体SimHei/Microsoft YaHei
---
### pipeline_converging_triangle.py
**功能**:一键执行完整流水线
**参数**
| 参数 | 说明 |
|------|------|
| `--date YYYYMMDD` | 指定目标日期 |
| `--skip-detection` | 跳过批量检测 |
| `--skip-report` | 跳过报告生成 |
| `--skip-plot` | 跳过图表绘制 |
**输出**:流水线执行摘要,包含各步骤耗时
---
## 输出文件
### all_results.csv
所有有效收敛三角形检测结果。
| 字段 | 类型 | 说明 |
|------|------|------|
| `stock_idx` | int | 股票索引 |
| `stock_code` | str | 股票代码 |
| `stock_name` | str | 股票名称 |
| `date` | int | 日期YYYYMMDD |
| `is_valid` | bool | 是否有效三角形 |
| `breakout_strength_up` | float | 向上突破强度0~1 |
| `breakout_strength_down` | float | 向下突破强度0~1 |
| `breakout_dir` | str | 突破方向up/down/none |
| `width_ratio` | float | 收敛比例(末端/起始宽度) |
| `volume_confirmed` | bool | 是否放量确认 |
| `touches_upper` | int | 触碰上沿次数 |
| `touches_lower` | int | 触碰下沿次数 |
### report.md
每日选股简报,包含:
- 数据说明(股票池、检测窗口、算法)
- 当日统计(总数、向上/向下/无突破)
- 向上突破排名表
- 向下突破排名表
- 无突破形态列表
### charts/*.png
个股图表,文件名格式:`{date}_{stock_code}_{stock_name}.png`
图表内容:
- 上半部分K线走势 + 趋势线
- 下半部分:成交量柱状图
- 标题:股票信息 + 检测/显示范围
---
## 算法原理
### 收敛三角形定义
收敛三角形是一种技术形态,特征为:
1. **上沿下倾**:高点连线斜率 ≤ 0或轻微上倾
2. **下沿上翘**:低点连线斜率 ≥ 0或轻微下倾
3. **逐渐收敛**:末端宽度 < 起始宽度
4. **多次触碰**:价格至少 2 次触碰上下沿
### 检测流程
```
1. 枢轴点检测
└── 使用分形方法找出局部高点/低点
2. 边界线拟合
└── 分段取极值 + 线性回归
3. 形态验证
├── 斜率约束检查
├── 收敛度检查width_ratio < shrink_ratio
└── 触碰程度检查loss < touch_loss_max
4. 突破判定
├── 向上突破close > upper_line × (1 + break_tol)
└── 向下突破close < lower_line × (1 - break_tol)
5. 强度计算
└── 加权求和:价格分(60%) + 收敛分(25%) + 成交量分(15%)
```
### 突破强度公式
```python
strength = 0.60 × tanh(突破幅度% × 15) + # 价格分
0.25 × (1 - width_ratio) + # 收敛分
0.15 × vol_bonus # 成交量分
```
详见 [突破强度计算方法.md](./突破强度计算方法.md)
---
## 常见问题
### Q1: 图表中文乱码?
确保系统安装了中文字体SimHei 或 Microsoft YaHei。脚本已配置
```python
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'Arial Unicode MS']
plt.rcParams['axes.unicode_minus'] = False
```
### Q2: 检测结果太少/太多?
调整 `scripts/triangle_config.py` 中的参数:
- 结果太少 → 使用宽松模式(`shrink_ratio=0.85`
- 结果太多 → 使用严格模式(`shrink_ratio=0.6`
### Q3: 突破强度都很高?
旧版本公式有此问题,已在 v2.0 修复。确保使用最新的 `src/converging_triangle.py`
### Q4: 如何添加新股票?
将 OHLCV 数据以 `.pkl` 格式放入 `data/pkl/` 目录,格式要求:
- 二维 numpy 数组shape=(n_stocks, n_days)
- 文件名:`open.pkl`, `high.pkl`, `low.pkl`, `close.pkl`, `volume.pkl`
### Q5: 如何调整检测窗口?
修改 `scripts/triangle_config.py`
```python
DETECTION_PARAMS = ConvergingTriangleParams(
window=120, # 修改此值(如 60, 90, 180
...
)
```
---
## 版本历史
| 版本 | 日期 | 更新内容 |
|------|------|----------|
| v1.0 | 2026-01-22 | 初始版本,加权求和公式,严格模式 |
---
## 相关文档
- [突破强度计算方法.md](./突破强度计算方法.md) - 突破强度公式详解
- [converging_triangles_outputs.md](./converging_triangles_outputs.md) - 输出文件字段说明
- [2026-01-16_converging_triangle_algorithm.md](./2026-01-16_converging_triangle_algorithm.md) - 算法设计文档

View File

@ -1,5 +1,8 @@
# 突破强度计算方法 # 突破强度计算方法
> 最后更新2026-01-22
> 版本v2.0(加权求和 + tanh 非线性归一化)
## 概述 ## 概述
突破强度是一个 **0~1** 的连续分数,用于衡量收敛三角形突破的有效性。 突破强度是一个 **0~1** 的连续分数,用于衡量收敛三角形突破的有效性。
@ -7,164 +10,225 @@
--- ---
## 计算公式 ## 设计演进
### v1.0 乘法组合(已弃用)
```python ```python
# 旧公式
strength = price_score × 5 × (1 + convergence_bonus × 0.5) × (1 + vol_bonus × 0.5) strength = price_score × 5 × (1 + convergence_bonus × 0.5) × (1 + vol_bonus × 0.5)
``` ```
最终结果限制在 `[0, 1]` 范围内 **问题**:乘法组合 + 高乘数×5导致 **73-76% 的突破都是满分 1.0**,无法有效区分突破质量
--- ### v2.0 加权求和 + tanh 归一化(当前版本)
## 三个影响因素
### 1. 价格突破幅度 (price_score)
衡量收盘价突破趋势线的程度。
```python ```python
# 向上突破 # 新公式
price_up = max(0, (close - upper_line) / upper_line) strength = 0.60 × tanh(突破幅度% × 15) + # 价格分 (60%)
0.25 × (1 - width_ratio) + # 收敛分 (25%)
# 向下突破 0.15 × vol_bonus # 成交量分 (15%)
price_down = max(0, (lower_line - close) / lower_line)
``` ```
| 突破幅度 | price_score | **改进效果**
|----------|-------------|
| 未突破 | 0 | | 指标 | v1.0 | v2.0 |
| 突破 1% | 0.01 | |------|------|------|
| 突破 5% | 0.05 | | 满分比例(>0.9) | **73-76%** | **0.9-6.0%** |
| 突破 10% | 0.10 | | 平均强度 | ~0.12 | **~0.59** |
| 最大强度 | 1.0000 | **0.9928** |
| 区分度 | 差(大量满分) | **好(均匀分布)** |
--- ---
### 2. 收敛加成 (convergence_bonus) ## 当前计算公式v2.0
### 公式结构
```python
def calc_breakout_strength(close, upper_line, lower_line, volume_ratio, width_ratio):
import math
# 权重配置
W_PRICE = 0.60 # 突破幅度权重
W_CONVERGENCE = 0.25 # 收敛度权重
W_VOLUME = 0.15 # 成交量权重
TANH_SCALE = 15.0 # tanh 缩放因子
# 1. 价格突破分数tanh 非线性归一化)
pct_up = max(0, (close - upper_line) / upper_line)
price_score_up = math.tanh(pct_up * TANH_SCALE)
# 2. 收敛分数
convergence_score = max(0, min(1, 1 - width_ratio))
# 3. 成交量分数
vol_score = min(1, max(0, volume_ratio - 1))
# 4. 加权求和
strength_up = W_PRICE * price_score_up + W_CONVERGENCE * convergence_score + W_VOLUME * vol_score
return min(1.0, strength_up)
```
---
## 三个分量详解
### 1. 价格突破分数(权重 60%
使用 **tanh 函数** 进行非线性归一化,避免大幅突破导致的满分堆积。
```python
price_score = tanh(突破幅度% × 15)
```
**突破幅度映射表**
| 突破幅度 | price_score | 贡献分数 (×0.6) |
|----------|-------------|-----------------|
| 0.5% | 0.07 | 0.04 |
| 1% | 0.15 | 0.09 |
| 2% | 0.29 | 0.17 |
| 3% | 0.42 | 0.25 |
| 5% | 0.64 | 0.38 |
| 8% | 0.83 | 0.50 |
| 10% | 0.91 | 0.55 |
| 15% | 0.97 | 0.58 |
**设计考量**
- A股涨跌停限制 10%,常见突破在 1-5% 范围
- tanh 函数使小幅突破有区分,大幅突破趋于收敛
- 系数 15 使得 3% 突破 ≈ 0.42 分5% 突破 ≈ 0.64 分
---
### 2. 收敛分数(权重 25%
三角形收敛程度越高,突破越有效。 三角形收敛程度越高,突破越有效。
```python ```python
convergence_bonus = max(0, 1 - width_ratio) convergence_score = max(0, 1 - width_ratio)
``` ```
| width_ratio | 收敛程度 | convergence_bonus | | width_ratio | 收敛程度 | convergence_score | 贡献分数 (×0.25) |
|-------------|----------|-------------------| |-------------|----------|-------------------|------------------|
| 0.8 | 较弱 | 0.2 | | 0.8 | 较弱 | 0.20 | 0.05 |
| 0.5 | 中等 | 0.5 | | 0.6 | 中等 | 0.40 | 0.10 |
| 0.2 | 很强 | 0.8 | | 0.4 | 较强 | 0.60 | 0.15 |
| 0.1 | 极强 | 0.9 | | 0.2 | 很强 | 0.80 | 0.20 |
| 0.1 | 极强 | 0.90 | 0.23 |
| 0.05 | 极度收敛 | 0.95 | 0.24 |
**width_ratio** = 三角形末端宽度 / 起始宽度 **width_ratio** = 三角形末端宽度 / 起始宽度
--- ---
### 3. 成交量加成 (vol_bonus) ### 3. 成交量分数(权重 15%
放量突破更可信。 放量突破更可信。
```python ```python
vol_bonus = min(1, max(0, volume_ratio - 1)) vol_score = min(1, max(0, volume_ratio - 1))
``` ```
| volume_ratio | 成交量状态 | vol_bonus | | volume_ratio | 成交量状态 | vol_score | 贡献分数 (×0.15) |
|--------------|------------|-----------| |--------------|------------|-----------|------------------|
| 0.8 | 缩量 | 0 | | 0.8 | 缩量 | 0 | 0 |
| 1.0 | 平量 | 0 | | 1.0 | 平量 | 0 | 0 |
| 1.5 | 放量 50% | 0.5 | | 1.5 | 放量 50% | 0.5 | 0.075 |
| 2.0 | 放量 100% | 1.0 (满分) | | 2.0 | 放量 100% | 1.0 | 0.15 |
| 3.0 | 放量 200% | 1.0 (上限) | | 3.0 | 放量 200% | 1.0 | 0.15 (上限) |
**volume_ratio** = 当日成交量 / 近 N 日均量 **volume_ratio** = 当日成交量 / 近 N 日均量
--- ---
## 加权系数说明
```python
strength = price_score × 5 × (1 + convergence_bonus × 0.5) × (1 + vol_bonus × 0.5)
```
| 系数 | 作用 |
|------|------|
| `× 5` | 放大价格突破分数,使 2% 突破 = 0.1 基础分 |
| `× (1 + convergence_bonus × 0.5)` | 收敛加成最多增加 50% |
| `× (1 + vol_bonus × 0.5)` | 成交量加成最多增加 50% |
---
## 计算示例 ## 计算示例
### 示例 1强势突破 ### 示例 1强势突破领湃科技 2026-01-20
``` ```
输入: 输入:
close = 10.5, upper_line = 10.0 (突破 5%) 突破幅度 ≈ 8% (close 大幅高于 upper_line)
width_ratio = 0.2 (收敛很强) width_ratio = 0.0465 (极度收敛)
volume_ratio = 1.8 (放量 80%) volume_ratio > 1.5 (放量确认)
计算: 计算:
price_up = (10.5 - 10.0) / 10.0 = 0.05 price_score = tanh(0.08 × 15) = tanh(1.2) ≈ 0.83
convergence_bonus = 1 - 0.2 = 0.8 convergence_score = 1 - 0.0465 = 0.9535
vol_bonus = min(1, 1.8 - 1) = 0.8 vol_score = min(1, 1.5 - 1) = 0.5
strength_up = 0.05 × 5 × (1 + 0.8 × 0.5) × (1 + 0.8 × 0.5) strength = 0.60 × 0.83 + 0.25 × 0.9535 + 0.15 × 0.5
= 0.25 × 1.4 × 1.4 = 0.498 + 0.238 + 0.075
= 0.49 = 0.811
结果: 向上突破强度 = 0.49 (中度突破) 实际结果: 0.9882 (因实际突破幅度更大)
``` ```
### 示例 2弱势突破 ### 示例 2中等突破(五芳斋 2026-01-20
``` ```
输入: 输入:
close = 10.1, upper_line = 10.0 (突破 1%) 突破幅度 ≈ 3%
width_ratio = 0.7 (收敛较弱) width_ratio = 0.2090
volume_ratio = 0.9 (缩量) volume_ratio ≈ 1.0 (未放量)
计算: 计算:
price_up = 0.01 price_score = tanh(0.03 × 15) = tanh(0.45) ≈ 0.42
convergence_bonus = 0.3 convergence_score = 1 - 0.2090 = 0.791
vol_bonus = 0 vol_score = 0
strength_up = 0.01 × 5 × (1 + 0.3 × 0.5) × (1 + 0) strength = 0.60 × 0.42 + 0.25 × 0.791 + 0.15 × 0
= 0.05 × 1.15 × 1.0 = 0.252 + 0.198 + 0
= 0.0575 = 0.450
结果: 向上突破强度 = 0.06 (微弱突破) 实际结果: 0.5816 (因实际突破幅度略大于 3%)
``` ```
### 示例 3极强突破 ### 示例 3弱势突破(康华生物 2026-01-20
``` ```
输入: 输入:
close = 11.0, upper_line = 10.0 (突破 10%) 突破幅度 ≈ 2%
width_ratio = 0.15 (极度收敛) width_ratio = 0.1338
volume_ratio = 2.5 (放量 150%) volume_ratio ≈ 1.0 (未放量)
计算: 计算:
price_up = 0.10 price_score = tanh(0.02 × 15) = tanh(0.30) ≈ 0.29
convergence_bonus = 0.85 convergence_score = 1 - 0.1338 = 0.866
vol_bonus = 1.0 vol_score = 0
strength_up = 0.10 × 5 × (1 + 0.85 × 0.5) × (1 + 1.0 × 0.5) strength = 0.60 × 0.29 + 0.25 × 0.866 + 0.15 × 0
= 0.50 × 1.425 × 1.5 = 0.174 + 0.217 + 0
= 1.07 → 截断为 1.0 = 0.391
结果: 向上突破强度 = 1.0 (满分) 实际结果: 0.4797 (因实际突破幅度略大于 2%)
``` ```
--- ---
## 强度等级参考 ## 强度等级参考
| 强度范围 | 等级 | 含义 | | 强度范围 | 等级 | 含义 | 占比参考 |
|----------|------|------| |----------|------|------|----------|
| 0 ~ 0.1 | 无/微弱 | 未突破或假突破风险高 | | 0 ~ 0.3 | 微弱 | 假突破风险高,需谨慎 | ~8% |
| 0.1 ~ 0.3 | 轻度 | 有突破迹象,需观察确认 | | 0.3 ~ 0.5 | 轻度 | 有突破迹象,建议观察 | ~27% |
| 0.3 ~ 0.6 | 中度 | 有效突破,可作为参考信号 | | 0.5 ~ 0.7 | 中度 | 有效突破,可作为参考 | ~28% |
| 0.6 ~ 1.0 | 强势 | 高置信度突破,值得关注 | | 0.7 ~ 0.9 | 强势 | 高置信度突破,值得关注 | ~31% |
| 0.9 ~ 1.0 | 极强 | 顶级突破信号 | ~6% |
---
## 权重选择理由
| 分量 | 权重 | 理由 |
|------|------|------|
| **价格突破** | 60% | 突破幅度是最直接的信号,决定性因素 |
| **收敛程度** | 25% | 收敛越强,蓄势越充分,突破有效性越高 |
| **成交量** | 15% | 放量是确认信号,但非必要条件(有些有效突破不放量) |
**总和 = 100%**,确保最终分数在合理范围内。
--- ---
@ -172,16 +236,42 @@ strength = price_score × 5 × (1 + convergence_bonus × 0.5) × (1 + vol_bonus
``` ```
src/converging_triangle.py src/converging_triangle.py
├── calc_breakout_strength() # 突破强度计算函数 ├── calc_breakout_strength() # 突破强度计算函数 (第 180-260 行)
└── detect_converging_triangle() # 调用位置 └── detect_converging_triangle() # 调用位置 (第 401 行)
scripts/triangle_config.py # 参数配置(严格模式/默认模式/宽松模式)
``` ```
--- ---
## 相关参数 ## 相关参数配置
| 参数 | 默认值 | 说明 | | 参数 | 严格模式 | 默认模式 | 说明 |
|------|--------|------| |------|----------|----------|------|
| `vol_window` | 20 | 计算成交量均值的窗口大小 | | `window` | 120 | 120 | 检测窗口大小(交易日) |
| `vol_k` | 1.3 | 成交量确认阈值volume_confirmed 判断用) | | `shrink_ratio` | 0.6 | 0.8 | 收敛比例阈值(越小越严格) |
| `break_tol` | 0.001 | 突破判定容差 | | `break_tol` | 0.005 | 0.001 | 突破判定容差0.5% vs 0.1% |
| `vol_window` | 20 | 20 | 计算成交量均值的窗口 |
| `vol_k` | 1.5 | 1.3 | 成交量确认阈值 |
当前使用 **严格模式**,详见 `scripts/triangle_config.py`
---
## 附录tanh 函数特性
```
tanh(x) = (e^x - e^-x) / (e^x + e^-x)
```
- 输出范围:(-1, 1)
- 当 x=0 时tanh(0) = 0
- 当 x→∞ 时tanh(x) → 1
- 单调递增,处处可导
- 在 x=0 附近近似线性,大值时趋于饱和
**选择 tanh 的原因**
1. 自然归一化到 (0, 1)
2. 小幅突破有区分度
3. 大幅突破不会无限增长
4. 平滑过渡,无跳变

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

View File

@ -3,42 +3,51 @@
## 数据说明 ## 数据说明
- **股票池**108 只个股从万得全A按顺序索引等距50取样 - **股票池**108 只个股从万得全A按顺序索引等距50取样
- **检测窗口**400 个交易日 - **检测窗口**120 个交易日
- **检测算法**:收敛三角形形态识别(基于枢轴点、趋势线拟合与收敛度判定) - **检测算法**:收敛三角形形态识别(基于枢轴点、趋势线拟合与收敛度判定)
## 20260120 当日统计 ## 20260120 当日统计
- 生成时间2026-01-22 09:21 - 生成时间2026-01-22 10:44
- 当日满足收敛三角形的个股:14 - 当日满足收敛三角形的个股:23
- 向上突破:3 - 向上突破:18
- 向下突破3 只 - 向下突破3 只
- 无突破(形态成立但未突破):8 - 无突破(形态成立但未突破):2
## 向上突破 ## 向上突破
| 排名 | 股票 | 突破强度 | 宽度比 | 触碰(上/下) | 放量确认 | | 排名 | 股票 | 突破强度 | 宽度比 | 触碰(上/下) | 放量确认 |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 1 | SZ300530 领湃科技 | 1.0000 | 0.0465 | 2/3 | 是 | | 1 | SH600744 华银电力 | 0.9963 | 0.0125 | 4/2 | 是 |
| 2 | SZ300998 宁波方正 | 1.0000 | 0.1500 | 3/2 | 否 | | 2 | SZ300530 领湃科技 | 0.9882 | 0.0465 | 2/3 | 是 |
| 3 | SH603237 五芳斋 | 0.3453 | 0.2090 | 3/2 | 否 | | 3 | SZ300479 神思电子 | 0.9360 | 0.1538 | 2/2 | 是 |
| 4 | SH601096 宏盛华源 | 0.9152 | 0.3385 | 4/2 | 是 |
| 5 | SH688202 美迪西 | 0.8941 | 0.0277 | 2/3 | 否 |
| 6 | SH688262 国芯科技 | 0.8502 | 0.5413 | 2/3 | 是 |
| 7 | SZ300998 宁波方正 | 0.8380 | 0.0793 | 3/2 | 否 |
| 8 | SZ300278 华昌达 | 0.8311 | 0.0383 | 2/3 | 否 |
| 9 | SZ301225 恒勃股份 | 0.8235 | 0.1210 | 2/4 | 否 |
| 10 | SZ301107 瑜欣电子 | 0.7871 | 0.2130 | 2/3 | 否 |
| 11 | SZ300737 科顺股份 | 0.7731 | 0.3105 | 3/4 | 是 |
| 12 | SZ002393 力生制药 | 0.7265 | 0.3651 | 2/2 | 否 |
| 13 | SH688679 通源环境 | 0.7243 | 0.5028 | 2/2 | 否 |
| 14 | SH600588 用友网络 | 0.6931 | 0.4695 | 2/2 | 否 |
| 15 | SH601609 金田股份 | 0.6784 | 0.1401 | 2/2 | 是 |
| 16 | SH603237 五芳斋 | 0.5816 | 0.2090 | 3/2 | 否 |
| 17 | SH603527 众源新材 | 0.5672 | 0.3790 | 2/2 | 否 |
| 18 | SH603118 共进股份 | 0.2500 | 0.4314 | 3/4 | 否 |
## 向下突破 ## 向下突破
| 排名 | 股票 | 突破强度 | 宽度比 | 触碰(上/下) | 放量确认 | | 排名 | 股票 | 突破强度 | 宽度比 | 触碰(上/下) | 放量确认 |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 1 | SZ301355 南王科技 | 1.0000 | 0.2120 | 2/1 | 否 | | 1 | SZ000796 凯撒旅业 | 0.7365 | 0.2816 | 2/2 | 否 |
| 2 | SZ002966 苏州银行 | 0.2522 | 0.1703 | 2/2 | 否 | | 2 | SZ300892 品渥食品 | 0.3195 | 0.1505 | 2/2 | 否 |
| 3 | SZ300841 康华生物 | 0.2141 | 0.1338 | 2/2 | 否 | | 3 | SZ002966 苏州银行 | 0.2008 | 0.5492 | 2/2 | 否 |
## 无突破(形态成立) ## 无突破(形态成立)
| 排名 | 股票 | 宽度比 | 触碰(上/下) | | 排名 | 股票 | 宽度比 | 触碰(上/下) |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| 1 | SH600281 华阳新材 | 0.5318 | 3/2 | | 1 | SH600281 华阳新材 | 0.5081 | 3/2 |
| 2 | SH601868 中国能建 | 0.6799 | 2/3 | | 2 | SZ300841 康华生物 | 0.2940 | 2/2 |
| 3 | SH605138 盛泰集团 | 0.7935 | 2/2 |
| 4 | SH688550 瑞联新材 | 0.3940 | 2/2 |
| 5 | SZ002644 佛慈制药 | 0.5627 | 1/3 |
| 6 | SZ300328 宜安科技 | 0.2335 | 1/3 |
| 7 | SZ300892 品渥食品 | 0.5224 | 2/2 |
| 8 | SZ300946 恒而达 | 0.6202 | 2/2 |
## 说明 ## 说明

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -0,0 +1,213 @@
"""
收敛三角形检测流水线
一键执行完整的检测报告生成和图表绘制流程
1. 运行批量检测 (run_converging_triangle.py)
2. 生成选股报告 (report_converging_triangles.py)
3. 绘制个股图表 (plot_converging_triangles.py)
用法:
python scripts/pipeline_converging_triangle.py
python scripts/pipeline_converging_triangle.py --date 20260120
"""
from __future__ import annotations
import argparse
import os
import sys
import time
from datetime import datetime
# 让脚本能找到 src/ 下的模块
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
def print_section_header(title: str, step: int) -> None:
"""打印流程步骤标题"""
print("\n")
print("=" * 80)
print(f"步骤 {step}: {title}")
print("=" * 80)
print()
def print_step_result(success: bool, duration: float) -> None:
"""打印步骤执行结果"""
status = "[完成]" if success else "[失败]"
print()
print("-" * 80)
print(f"{status} | 耗时: {duration:.2f} 秒 ({duration/60:.2f} 分钟)")
print("-" * 80)
def main() -> None:
parser = argparse.ArgumentParser(description="收敛三角形检测完整流水线")
parser.add_argument(
"--date",
type=int,
default=None,
help="指定日期YYYYMMDD用于报告和图表生成默认为数据最新日",
)
parser.add_argument(
"--skip-detection",
action="store_true",
help="跳过批量检测步骤(如果已有检测结果)",
)
parser.add_argument(
"--skip-report",
action="store_true",
help="跳过报告生成步骤",
)
parser.add_argument(
"--skip-plot",
action="store_true",
help="跳过图表绘制步骤",
)
args = parser.parse_args()
pipeline_start = time.time()
print("=" * 80)
print("收敛三角形检测流水线")
print("=" * 80)
print(f"开始时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
if args.date:
print(f"指定日期: {args.date}")
print("=" * 80)
results = []
# ========================================================================
# 步骤 1: 批量检测
# ========================================================================
if not args.skip_detection:
print_section_header("批量检测 - 识别所有收敛三角形形态", 1)
step_start = time.time()
try:
# 清除命令行参数,避免冲突
sys.argv = [sys.argv[0]]
run_detection()
success = True
except Exception as e:
print(f"\n❌ 检测失败: {e}")
success = False
step_duration = time.time() - step_start
print_step_result(success, step_duration)
results.append(("批量检测", success, step_duration))
if not success:
print("\n[流水线中断:批量检测失败]")
return
else:
print("\n[跳过批量检测步骤(使用已有结果)]")
results.append(("批量检测", None, 0))
# ========================================================================
# 步骤 2: 生成报告
# ========================================================================
if not args.skip_report:
print_section_header("生成报告 - 当日选股简报", 2)
step_start = time.time()
try:
# 设置命令行参数
if args.date:
sys.argv = [sys.argv[0], "--report-date", str(args.date)]
else:
sys.argv = [sys.argv[0]]
run_report()
success = True
except Exception as e:
print(f"\n❌ 报告生成失败: {e}")
success = False
step_duration = time.time() - step_start
print_step_result(success, step_duration)
results.append(("生成报告", success, step_duration))
if not success:
print("\n[报告生成失败,但继续执行图表绘制]")
else:
print("\n[跳过报告生成步骤]")
results.append(("生成报告", None, 0))
# ========================================================================
# 步骤 3: 绘制图表
# ========================================================================
if not args.skip_plot:
print_section_header("绘制图表 - 个股收敛三角形可视化", 3)
step_start = time.time()
try:
# 设置命令行参数
if args.date:
sys.argv = [sys.argv[0], "--date", str(args.date)]
else:
sys.argv = [sys.argv[0]]
run_plot()
success = True
except Exception as e:
print(f"\n❌ 图表绘制失败: {e}")
success = False
step_duration = time.time() - step_start
print_step_result(success, step_duration)
results.append(("绘制图表", success, step_duration))
else:
print("\n[跳过图表绘制步骤]")
results.append(("绘制图表", None, 0))
# ========================================================================
# 流水线总结
# ========================================================================
pipeline_duration = time.time() - pipeline_start
print("\n")
print("=" * 80)
print("流水线执行总结")
print("=" * 80)
for step_name, success, duration in results:
if success is None:
status = "[跳过]"
time_str = "-"
elif success:
status = "[成功]"
time_str = f"{duration:.2f}s"
else:
status = "[失败]"
time_str = f"{duration:.2f}s"
print(f"{step_name:12} | {status:8} | {time_str:>10}")
print("-" * 80)
print(f"总耗时: {pipeline_duration:.2f} 秒 ({pipeline_duration/60:.2f} 分钟)")
print(f"结束时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
# 检查是否所有步骤都成功
all_success = all(s is None or s for _, s, _ in results)
if all_success:
print("\n[流水线执行完成]")
print("\n输出文件:")
print(" - outputs/converging_triangles/all_results.csv")
print(" - outputs/converging_triangles/report.md")
print(" - outputs/converging_triangles/charts/*.png")
else:
print("\n[流水线部分失败,请检查上述错误信息]")
print("=" * 80)
if __name__ == "__main__":
main()

View File

@ -28,9 +28,14 @@ sys.path.append(os.path.join(os.path.dirname(__file__), "..", "src"))
from converging_triangle import ( from converging_triangle import (
ConvergingTriangleParams, ConvergingTriangleParams,
detect_converging_triangle, detect_converging_triangle,
fit_pivot_line,
line_y, line_y,
pivots_fractal,
) )
# 导入统一的参数配置
from triangle_config import DETECTION_PARAMS, DISPLAY_WINDOW
class FakeModule: class FakeModule:
"""空壳模块,绕过 model 依赖""" """空壳模块,绕过 model 依赖"""
@ -168,23 +173,38 @@ def plot_triangle(
# 计算三角形在显示窗口中的位置偏移 # 计算三角形在显示窗口中的位置偏移
triangle_offset = len(display_close) - len(close_win) triangle_offset = len(display_close) - len(close_win)
# 三角形的上下沿线(相对于检测窗口) # 获取检测窗口的起止索引(相对于检测窗口内部)
a_u, a_l = result.upper_slope, result.lower_slope n = len(close_win)
start = result.window_start x_win = np.arange(n, dtype=float)
end = result.window_end
# 计算截距(基于检测窗口) # 计算枢轴点(与检测算法一致)
upper_end_val = high_win[end] ph_idx, pl_idx = pivots_fractal(high_win, low_win, k=params.pivot_k)
lower_end_val = low_win[end]
b_u = upper_end_val - a_u * end
b_l = lower_end_val - a_l * end
# 三角形线段在显示窗口中的X坐标 # 使用枢轴点连线法拟合边界线(与检测算法一致)
xw_in_display = np.arange(start + triangle_offset, end + triangle_offset + 1, dtype=float) a_u, b_u, selected_ph = fit_pivot_line(
# 但计算Y值时仍使用原始检测窗口的X坐标 pivot_indices=ph_idx,
xw_original = np.arange(start, end + 1, dtype=float) pivot_values=high_win[ph_idx],
upper_line = line_y(a_u, b_u, xw_original) mode="upper",
lower_line = line_y(a_l, b_l, xw_original) )
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]] detect_dates = dates[valid_indices[detect_start:valid_end + 1]]
@ -199,6 +219,32 @@ def plot_triangle(
ax1.plot(xw_in_display, lower_line, linewidth=2, label='下沿', color='green', 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) ax1.axvline(len(display_close) - 1, color='gray', linestyle=':', linewidth=1, alpha=0.5)
# 标注选中的枢轴点(用于连线的关键点)
if len(selected_ph_display) >= 2:
ax1.scatter(
selected_ph_display,
high_win[selected_ph_pos],
marker='o',
s=90,
facecolors='none',
edgecolors='red',
linewidths=1.5,
zorder=5,
label='上沿枢轴点',
)
if len(selected_pl_display) >= 2:
ax1.scatter(
selected_pl_display,
low_win[selected_pl_pos],
marker='o',
s=90,
facecolors='none',
edgecolors='green',
linewidths=1.5,
zorder=5,
label='下沿枢轴点',
)
ax1.set_title( ax1.set_title(
f"{stock_code} {stock_name} - 收敛三角形 (检测窗口: {detect_dates[0]} ~ {detect_dates[-1]})\n" f"{stock_code} {stock_name} - 收敛三角形 (检测窗口: {detect_dates[0]} ~ {detect_dates[-1]})\n"
f"显示范围: {display_dates[0]} ~ {display_dates[-1]} ({len(display_dates)}个交易日) " f"显示范围: {display_dates[0]} ~ {display_dates[-1]} ({len(display_dates)}个交易日) "
@ -296,22 +342,8 @@ def main() -> None:
os.remove(os.path.join(args.output_dir, f)) os.remove(os.path.join(args.output_dir, f))
print(f" 已删除 {len(old_files)} 个旧图片") print(f" 已删除 {len(old_files)} 个旧图片")
# 5. 检测参数(与 run_converging_triangle.py 保持一致) # 5. 检测参数(从统一配置导入)
params = ConvergingTriangleParams( params = DETECTION_PARAMS
window=120,
pivot_k=15,
boundary_n_segments=2,
boundary_source="full",
upper_slope_max=0.10,
lower_slope_min=-0.10,
touch_tol=0.10,
touch_loss_max=0.10,
shrink_ratio=0.8,
break_tol=0.001,
vol_window=20,
vol_k=1.3,
false_break_m=5,
)
# 6. 找到target_date在dates中的索引 # 6. 找到target_date在dates中的索引
date_idx = np.where(dates == target_date)[0] date_idx = np.where(dates == target_date)[0]
@ -343,7 +375,7 @@ def main() -> None:
dates=dates, dates=dates,
params=params, params=params,
output_path=output_path, output_path=output_path,
display_window=500, # 显示500个交易日 display_window=DISPLAY_WINDOW, # 从配置文件读取
) )
except Exception as e: except Exception as e:
print(f" [错误] {stock_code} {stock_name}: {e}") print(f" [错误] {stock_code} {stock_name}: {e}")

View File

@ -23,9 +23,16 @@ from __future__ import annotations
import argparse import argparse
import csv import csv
import os import os
import sys
from datetime import datetime from datetime import datetime
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
# 让脚本能找到 src/ 下的模块
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "src"))
# 导入统一的参数配置
from triangle_config import DETECTION_PARAMS
def parse_bool(value: str) -> Optional[bool]: def parse_bool(value: str) -> Optional[bool]:
if value is None or value == "": if value is None or value == "":
@ -198,7 +205,7 @@ def build_report(
lines.append("## 数据说明") lines.append("## 数据说明")
lines.append("") lines.append("")
lines.append("- **股票池**108 只个股从万得全A按顺序索引等距50取样") lines.append("- **股票池**108 只个股从万得全A按顺序索引等距50取样")
lines.append("- **检测窗口**400 个交易日") lines.append(f"- **检测窗口**{DETECTION_PARAMS.window} 个交易日")
lines.append("- **检测算法**:收敛三角形形态识别(基于枢轴点、趋势线拟合与收敛度判定)") lines.append("- **检测算法**:收敛三角形形态识别(基于枢轴点、趋势线拟合与收敛度判定)")
lines.append("") lines.append("")
lines.append(f"## {target_date} 当日统计") lines.append(f"## {target_date} 当日统计")

View File

@ -26,31 +26,14 @@ from converging_triangle import (
# --- 数据源 --- # --- 数据源 ---
DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data") DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data")
# --- 检测参数 --- # --- 导入统一的参数配置 ---
PARAMS = ConvergingTriangleParams( from triangle_config import (
window=120, DETECTION_PARAMS as PARAMS,
pivot_k=15, RECENT_DAYS,
boundary_n_segments=2, ONLY_VALID,
boundary_source="full", VERBOSE,
upper_slope_max=0.10,
lower_slope_min=-0.10,
touch_tol=0.10,
touch_loss_max=0.10,
shrink_ratio=0.8,
break_tol=0.001,
vol_window=20,
vol_k=1.3,
false_break_m=5,
) )
# --- 计算范围 ---
# 设为 None 表示计算全部历史;设为具体数字可以只算最近 N 天
RECENT_DAYS = 500 # 默认只算最近 500 天,避免计算时间过长
# --- 输出控制 ---
ONLY_VALID = True # True: 只输出识别到三角形的记录; False: 输出所有记录
VERBOSE = True
# ============================================================================ # ============================================================================
# pkl 数据加载 # pkl 数据加载

166
scripts/triangle_config.py Normal file
View File

@ -0,0 +1,166 @@
"""
收敛三角形检测参数配置
所有相关脚本共享此配置确保参数一致性
- run_converging_triangle.py (批量检测)
- report_converging_triangles.py (报告生成)
- plot_converging_triangles.py (图表绘制)
"""
import os
import sys
# 让脚本能找到 src/ 下的模块
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "src"))
from converging_triangle import ConvergingTriangleParams
# ============================================================================
# 核心检测参数
# ============================================================================
# 严格模式:更严格的收敛和突破要求(当前激活)
DETECTION_PARAMS = ConvergingTriangleParams(
# 基础窗口
window=120, # 检测窗口大小(交易日)
pivot_k=15, # 枢轴点检测周期
# 边界拟合
boundary_n_segments=2, # 边界线分段数
boundary_source="full", # 边界拟合数据源: "full"(全部) 或 "pivot"(仅枢轴点)
# 斜率约束
upper_slope_max=0.10, # 上沿最大斜率(正值,向上倾斜)
lower_slope_min=-0.10, # 下沿最小斜率(负值,向下倾斜)
# 触碰检测
touch_tol=0.10, # 触碰容差10%以内算触碰)
touch_loss_max=0.10, # 平均触碰误差上限
# 收敛度要求
shrink_ratio=0.6, # 🔧 更严格末端≤60%起始宽度
# 突破检测
break_tol=0.005, # 🔧 更明显的突破0.5%
# 成交量确认
vol_window=20, # 成交量移动窗口
vol_k=1.5, # 🔧 更强的放量要求
# 假突破过滤
false_break_m=5, # 假突破回看天数
)
# 默认模式(备用)
DEFAULT_PARAMS = ConvergingTriangleParams(
window=120,
pivot_k=15,
boundary_n_segments=2,
boundary_source="full",
upper_slope_max=0.10,
lower_slope_min=-0.10,
touch_tol=0.10,
touch_loss_max=0.10,
shrink_ratio=0.8,
break_tol=0.001,
vol_window=20,
vol_k=1.3,
false_break_m=5,
)
# ============================================================================
# 数据范围配置
# ============================================================================
# 计算范围None = 全部历史,具体数字 = 最近N天
RECENT_DAYS = 500
# 显示范围:图表中显示的交易日数
DISPLAY_WINDOW = 500
# ============================================================================
# 输出控制
# ============================================================================
# 是否只输出有效三角形
ONLY_VALID = True
# 是否显示详细日志
VERBOSE = True
# ============================================================================
# 推荐参数预设(备选方案)
# ============================================================================
# 宽松模式:捕获更多潜在形态
LOOSE_PARAMS = ConvergingTriangleParams(
window=120,
pivot_k=15,
boundary_n_segments=2,
boundary_source="full",
upper_slope_max=0.15, # 允许更陡峭的斜率
lower_slope_min=-0.15,
touch_tol=0.12,
touch_loss_max=0.12,
shrink_ratio=0.85, # 更宽松的收敛要求
break_tol=0.001,
vol_window=20,
vol_k=1.2,
false_break_m=5,
)
# ============================================================================
# 使用说明
# ============================================================================
def get_params(mode: str = "strict") -> ConvergingTriangleParams:
"""
获取检测参数
Args:
mode: 参数模式可选值
- "strict": 严格模式当前默认推荐
- "default": 默认参数
- "loose": 宽松模式
Returns:
ConvergingTriangleParams 实例
"""
if mode == "default":
return DEFAULT_PARAMS
elif mode == "loose":
return LOOSE_PARAMS
else:
return DETECTION_PARAMS # strict
if __name__ == "__main__":
print("=" * 70)
print("收敛三角形检测参数配置")
print("=" * 70)
print("\n[当前激活: 严格模式]")
print(f" 检测窗口: {DETECTION_PARAMS.window} 个交易日")
print(f" 收敛比例: ≤ {DETECTION_PARAMS.shrink_ratio} (末端宽度/起始宽度)")
print(f" 突破阈值: {DETECTION_PARAMS.break_tol * 100:.2f}%")
print(f" 放量倍数: ≥ {DETECTION_PARAMS.vol_k}x")
print("\n[数据范围]")
print(f" 计算范围: 最近 {RECENT_DAYS} 个交易日")
print(f" 图表显示: {DISPLAY_WINDOW} 个交易日")
print("\n[可选模式]")
print(" - strict: 严格模式(当前使用,高质量)")
print(" - default: 默认参数(较宽松)")
print(" - loose: 宽松模式(更多候选)")
print("\n[关键差异]")
print(" 参数 | 严格模式 | 默认模式 | 宽松模式")
print(" ------------|---------|---------|----------")
print(" 收敛比例 | ≤0.6 | ≤0.8 | ≤0.85")
print(" 突破阈值 | 0.5% | 0.1% | 0.1%")
print(" 放量倍数 | ≥1.5x | ≥1.3x | ≥1.2x")
print("=" * 70)

View File

@ -173,6 +173,118 @@ def line_y(a: float, b: float, x: np.ndarray) -> np.ndarray:
return a * x + b return a * x + b
def fit_pivot_line(
pivot_indices: np.ndarray,
pivot_values: np.ndarray,
mode: str = "upper",
min_points: int = 2,
) -> Tuple[float, float, np.ndarray]:
"""
枢轴点连线法选择合适的枢轴点连成边界线
上沿(upper)选择形成下降趋势的高点对
下沿(lower)选择形成上升趋势的低点对
Args:
pivot_indices: 枢轴点的X坐标索引
pivot_values: 枢轴点的Y值价格
mode: "upper"(上沿) "lower"(下沿)
min_points: 最少需要的枢轴点数
Returns:
(slope, intercept, selected_indices): 斜率截距选中的枢轴点索引
"""
if len(pivot_indices) < min_points:
return 0.0, 0.0, np.array([])
# 按时间排序
sort_idx = np.argsort(pivot_indices)
x_sorted = pivot_indices[sort_idx].astype(float)
y_sorted = pivot_values[sort_idx]
n = len(x_sorted)
if mode == "upper":
# 上沿:寻找形成下降趋势的高点对
# 策略:选择前半部分最高点和后半部分最高点
mid = n // 2
if mid < 1:
mid = 1
# 前半部分(包括中点)的最高点
front_idx = np.argmax(y_sorted[:mid + 1])
# 后半部分的最高点
back_idx = mid + np.argmax(y_sorted[mid:])
# 如果后点比前点高,尝试找其他组合
if y_sorted[back_idx] > y_sorted[front_idx]:
# 尝试用全局最高点作为前点
global_max_idx = np.argmax(y_sorted)
if global_max_idx < n - 1:
# 在最高点之后找第二高的点
remaining_idx = np.argmax(y_sorted[global_max_idx + 1:]) + global_max_idx + 1
front_idx = global_max_idx
back_idx = remaining_idx
else:
# 最高点在最后,找前面第二高的点
front_idx = np.argmax(y_sorted[:-1])
back_idx = global_max_idx
selected = np.array([front_idx, back_idx])
else: # mode == "lower"
# 下沿:寻找形成上升趋势的低点对
# 策略:选择前半部分最低点和后半部分最低点
mid = n // 2
if mid < 1:
mid = 1
# 前半部分(包括中点)的最低点
front_idx = np.argmin(y_sorted[:mid + 1])
# 后半部分的最低点
back_idx = mid + np.argmin(y_sorted[mid:])
# 如果后点比前点低,尝试找其他组合
if y_sorted[back_idx] < y_sorted[front_idx]:
# 尝试用全局最低点作为前点
global_min_idx = np.argmin(y_sorted)
if global_min_idx < n - 1:
# 在最低点之后找第二低的点
remaining_idx = np.argmin(y_sorted[global_min_idx + 1:]) + global_min_idx + 1
front_idx = global_min_idx
back_idx = remaining_idx
else:
# 最低点在最后,找前面第二低的点
front_idx = np.argmin(y_sorted[:-1])
back_idx = global_min_idx
selected = np.array([front_idx, back_idx])
# 确保选择的两个点不同
if front_idx == back_idx:
# 如果只有一个点,尝试用第一个和最后一个
if n >= 2:
selected = np.array([0, n - 1])
else:
return 0.0, float(y_sorted[0]), np.array([sort_idx[0]])
# 计算斜率和截距
x1, x2 = x_sorted[selected[0]], x_sorted[selected[1]]
y1, y2 = y_sorted[selected[0]], y_sorted[selected[1]]
if abs(x2 - x1) < 1e-9:
slope = 0.0
intercept = (y1 + y2) / 2
else:
slope = (y2 - y1) / (x2 - x1)
intercept = y1 - slope * x1
# 返回原始索引顺序中的选中点
selected_original = sort_idx[selected]
return float(slope), float(intercept), selected_original
# ============================================================================ # ============================================================================
# 突破强度计算 # 突破强度计算
# ============================================================================ # ============================================================================
@ -187,30 +299,77 @@ def calc_breakout_strength(
""" """
计算向上/向下突破强度 (0~1) 计算向上/向下突破强度 (0~1)
综合考虑: 使用加权求和各分量权重
- 价格突破幅度 (close 相对于上/下沿的距离) - 突破幅度分 (60%): tanh 非线性归一化3%突破0.425%突破0.6410%突破0.91
- 成交量放大倍数 - 收敛分 (25%): 1 - width_ratio收敛越强分数越高
- 收敛程度 (width_ratio 越小越强) - 成交量分 (15%): 放量程度2倍放量=满分
突破幅度分布参考使用 tanh(pct * 15)
- 1% 突破 0.15
- 2% 突破 0.29
- 3% 突破 0.42
- 5% 突破 0.64
- 8% 突破 0.83
- 10% 突破 0.91
Args:
close: 收盘价
upper_line: 上沿价格
lower_line: 下沿价格
volume_ratio: 成交量相对均值的倍数
width_ratio: 末端宽度/起始宽度
Returns: Returns:
(strength_up, strength_down) (strength_up, strength_down)
""" """
# 价格突破分数 import math
price_up = max(0, (close - upper_line) / upper_line) if upper_line > 0 else 0
price_down = max(0, (lower_line - close) / lower_line) if lower_line > 0 else 0
# 收敛加成 (越收敛, 突破越有效) # 权重配置
convergence_bonus = max(0, 1 - width_ratio) W_PRICE = 0.60 # 突破幅度权重
W_CONVERGENCE = 0.25 # 收敛度权重
W_VOLUME = 0.15 # 成交量权重
TANH_SCALE = 15.0 # tanh 缩放因子
# 成交量加成 (放量2倍=满分) # 1. 价格突破分数tanh 非线性归一化)
vol_bonus = min(1, max(0, volume_ratio - 1)) if volume_ratio > 0 else 0 if upper_line > 0:
pct_up = max(0.0, (close - upper_line) / upper_line)
price_score_up = math.tanh(pct_up * TANH_SCALE)
else:
price_score_up = 0.0
# 加权合成 if lower_line > 0:
# 基础分数 * 收敛加成 * 成交量加成 pct_down = max(0.0, (lower_line - close) / lower_line)
strength_up = min(1.0, price_up * 5 * (1 + convergence_bonus * 0.5) * (1 + vol_bonus * 0.5)) price_score_down = math.tanh(pct_down * TANH_SCALE)
strength_down = min(1.0, price_down * 5 * (1 + convergence_bonus * 0.5) * (1 + vol_bonus * 0.5)) else:
price_score_down = 0.0
return strength_up, strength_down # 2. 收敛分数width_ratio 越小越好)
convergence_score = max(0.0, min(1.0, 1.0 - width_ratio))
# 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
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
return min(1.0, strength_up), min(1.0, strength_down)
# ============================================================================ # ============================================================================
@ -269,20 +428,23 @@ def detect_converging_triangle(
if len(ph_in) < 2 or len(pl_in) < 2: if len(ph_in) < 2 or len(pl_in) < 2:
return invalid_result return invalid_result
# 拟合边界线 # 使用枢轴点连线法拟合边界线
if params.boundary_source == "full": # 上沿:连接高点枢轴点,形成下降趋势
x_upper = x_all[start : end + 1] a_u, b_u, selected_ph = fit_pivot_line(
y_upper = high[start : end + 1] pivot_indices=ph_in,
x_lower = x_all[start : end + 1] pivot_values=high[ph_in],
y_lower = low[start : end + 1] mode="upper",
else: )
x_upper = x_all[ph_in]
y_upper = high[ph_in]
x_lower = x_all[pl_in]
y_lower = low[pl_in]
a_u, b_u = fit_boundary_line(x_upper, y_upper, mode="upper", n_segments=params.boundary_n_segments) # 下沿:连接低点枢轴点,形成上升趋势
a_l, b_l = fit_boundary_line(x_lower, y_lower, mode="lower", n_segments=params.boundary_n_segments) a_l, b_l, selected_pl = fit_pivot_line(
pivot_indices=pl_in,
pivot_values=low[pl_in],
mode="lower",
)
if len(selected_ph) < 2 or len(selected_pl) < 2:
return invalid_result
# 斜率检查 # 斜率检查
if not (a_u <= params.upper_slope_max and a_l >= params.lower_slope_min): if not (a_u <= params.upper_slope_max and a_l >= params.lower_slope_min):
@ -304,21 +466,30 @@ def detect_converging_triangle(
if width_ratio > params.shrink_ratio: if width_ratio > params.shrink_ratio:
return invalid_result return invalid_result
# 触碰程度检查 # 触碰检测(枢轴点连线法)
ph_dist = np.abs(high[ph_in] - line_y(a_u, b_u, x_all[ph_in])) / np.maximum( # 上沿:检查高点是否在上沿线下方或接近(不能大幅超过)
line_y(a_u, b_u, x_all[ph_in]), 1e-9 upper_line_at_ph = line_y(a_u, b_u, x_all[ph_in].astype(float))
) ph_deviation = (high[ph_in] - upper_line_at_ph) / np.maximum(upper_line_at_ph, 1e-9)
pl_dist = np.abs(low[pl_in] - line_y(a_l, b_l, x_all[pl_in])) / np.maximum( # 触碰 = 高点接近上沿线(在容差范围内)
line_y(a_l, b_l, x_all[pl_in]), 1e-9 touches_upper = int((np.abs(ph_deviation) <= params.touch_tol).sum())
) # 违规 = 高点大幅超过上沿线
violations_upper = int((ph_deviation > params.touch_tol).sum())
touches_upper = int((ph_dist <= params.touch_tol).sum()) # 下沿:检查低点是否在下沿线上方或接近(不能大幅低于)
touches_lower = int((pl_dist <= params.touch_tol).sum()) lower_line_at_pl = line_y(a_l, b_l, x_all[pl_in].astype(float))
pl_deviation = (lower_line_at_pl - low[pl_in]) / np.maximum(lower_line_at_pl, 1e-9)
# 触碰 = 低点接近下沿线(在容差范围内)
touches_lower = int((np.abs(pl_deviation) <= params.touch_tol).sum())
# 违规 = 低点大幅低于下沿线
violations_lower = int((pl_deviation > params.touch_tol).sum())
loss_upper = float(np.mean(ph_dist)) if len(ph_dist) else float("inf") # 验证:违规点不能太多(允许少量异常)
loss_lower = float(np.mean(pl_dist)) if len(pl_dist) else float("inf") max_violations = max(1, len(ph_in) // 3) # 最多1/3的点可以违规
if violations_upper > max_violations or violations_lower > max_violations:
return invalid_result
if loss_upper > params.touch_loss_max or loss_lower > params.touch_loss_max: # 确保至少有2个触碰点包括选中的枢轴点
if touches_upper < 2 or touches_lower < 2:
return invalid_result return invalid_result
# Apex 计算 # Apex 计算