Compare commits
2 Commits
543572667b
...
5455f8e456
| Author | SHA1 | Date | |
|---|---|---|---|
| 5455f8e456 | |||
| 8dea3fbccb |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# 收敛三角形图表输出(本地生成,不推送)
|
||||
outputs/converging_triangles/charts/
|
||||
63
docs/2026-01-22_枢轴点绘图问题记录.md
Normal file
63
docs/2026-01-22_枢轴点绘图问题记录.md
Normal 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
|
||||
```
|
||||
|
||||
验证要点:
|
||||
- 红/绿空心圆应落在真实高点/低点位置
|
||||
- 不再出现“所有枢轴点挤在一起”的情况
|
||||
|
||||
---
|
||||
|
||||
如需进一步增强可视化,可新增“显示所有枢轴点”的开关,用于诊断与验证。
|
||||
347
docs/收敛三角形检测系统-使用指南.md
Normal file
347
docs/收敛三角形检测系统-使用指南.md
Normal 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) - 算法设计文档
|
||||
286
docs/突破强度计算方法.md
286
docs/突破强度计算方法.md
@ -1,5 +1,8 @@
|
||||
# 突破强度计算方法
|
||||
|
||||
> 最后更新:2026-01-22
|
||||
> 版本:v2.0(加权求和 + tanh 非线性归一化)
|
||||
|
||||
## 概述
|
||||
|
||||
突破强度是一个 **0~1** 的连续分数,用于衡量收敛三角形突破的有效性。
|
||||
@ -7,164 +10,225 @@
|
||||
|
||||
---
|
||||
|
||||
## 计算公式
|
||||
## 设计演进
|
||||
|
||||
### v1.0 乘法组合(已弃用)
|
||||
|
||||
```python
|
||||
# 旧公式
|
||||
strength = price_score × 5 × (1 + convergence_bonus × 0.5) × (1 + vol_bonus × 0.5)
|
||||
```
|
||||
|
||||
最终结果限制在 `[0, 1]` 范围内。
|
||||
**问题**:乘法组合 + 高乘数(×5)导致 **73-76% 的突破都是满分 1.0**,无法有效区分突破质量。
|
||||
|
||||
---
|
||||
|
||||
## 三个影响因素
|
||||
|
||||
### 1. 价格突破幅度 (price_score)
|
||||
|
||||
衡量收盘价突破趋势线的程度。
|
||||
### v2.0 加权求和 + tanh 归一化(当前版本)
|
||||
|
||||
```python
|
||||
# 向上突破
|
||||
price_up = max(0, (close - upper_line) / upper_line)
|
||||
|
||||
# 向下突破
|
||||
price_down = max(0, (lower_line - close) / lower_line)
|
||||
# 新公式
|
||||
strength = 0.60 × tanh(突破幅度% × 15) + # 价格分 (60%)
|
||||
0.25 × (1 - width_ratio) + # 收敛分 (25%)
|
||||
0.15 × vol_bonus # 成交量分 (15%)
|
||||
```
|
||||
|
||||
| 突破幅度 | price_score |
|
||||
|----------|-------------|
|
||||
| 未突破 | 0 |
|
||||
| 突破 1% | 0.01 |
|
||||
| 突破 5% | 0.05 |
|
||||
| 突破 10% | 0.10 |
|
||||
**改进效果**:
|
||||
|
||||
| 指标 | v1.0 | v2.0 |
|
||||
|------|------|------|
|
||||
| 满分比例(>0.9) | **73-76%** | **0.9-6.0%** |
|
||||
| 平均强度 | ~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
|
||||
convergence_bonus = max(0, 1 - width_ratio)
|
||||
convergence_score = max(0, 1 - width_ratio)
|
||||
```
|
||||
|
||||
| width_ratio | 收敛程度 | convergence_bonus |
|
||||
|-------------|----------|-------------------|
|
||||
| 0.8 | 较弱 | 0.2 |
|
||||
| 0.5 | 中等 | 0.5 |
|
||||
| 0.2 | 很强 | 0.8 |
|
||||
| 0.1 | 极强 | 0.9 |
|
||||
| width_ratio | 收敛程度 | convergence_score | 贡献分数 (×0.25) |
|
||||
|-------------|----------|-------------------|------------------|
|
||||
| 0.8 | 较弱 | 0.20 | 0.05 |
|
||||
| 0.6 | 中等 | 0.40 | 0.10 |
|
||||
| 0.4 | 较强 | 0.60 | 0.15 |
|
||||
| 0.2 | 很强 | 0.80 | 0.20 |
|
||||
| 0.1 | 极强 | 0.90 | 0.23 |
|
||||
| 0.05 | 极度收敛 | 0.95 | 0.24 |
|
||||
|
||||
**width_ratio** = 三角形末端宽度 / 起始宽度
|
||||
|
||||
---
|
||||
|
||||
### 3. 成交量加成 (vol_bonus)
|
||||
### 3. 成交量分数(权重 15%)
|
||||
|
||||
放量突破更可信。
|
||||
|
||||
```python
|
||||
vol_bonus = min(1, max(0, volume_ratio - 1))
|
||||
vol_score = min(1, max(0, volume_ratio - 1))
|
||||
```
|
||||
|
||||
| volume_ratio | 成交量状态 | vol_bonus |
|
||||
|--------------|------------|-----------|
|
||||
| 0.8 | 缩量 | 0 |
|
||||
| 1.0 | 平量 | 0 |
|
||||
| 1.5 | 放量 50% | 0.5 |
|
||||
| 2.0 | 放量 100% | 1.0 (满分) |
|
||||
| 3.0 | 放量 200% | 1.0 (上限) |
|
||||
| volume_ratio | 成交量状态 | vol_score | 贡献分数 (×0.15) |
|
||||
|--------------|------------|-----------|------------------|
|
||||
| 0.8 | 缩量 | 0 | 0 |
|
||||
| 1.0 | 平量 | 0 | 0 |
|
||||
| 1.5 | 放量 50% | 0.5 | 0.075 |
|
||||
| 2.0 | 放量 100% | 1.0 | 0.15 |
|
||||
| 3.0 | 放量 200% | 1.0 | 0.15 (上限) |
|
||||
|
||||
**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%)
|
||||
width_ratio = 0.2 (收敛很强)
|
||||
volume_ratio = 1.8 (放量 80%)
|
||||
突破幅度 ≈ 8% (close 大幅高于 upper_line)
|
||||
width_ratio = 0.0465 (极度收敛)
|
||||
volume_ratio > 1.5 (放量确认)
|
||||
|
||||
计算:
|
||||
price_up = (10.5 - 10.0) / 10.0 = 0.05
|
||||
convergence_bonus = 1 - 0.2 = 0.8
|
||||
vol_bonus = min(1, 1.8 - 1) = 0.8
|
||||
price_score = tanh(0.08 × 15) = tanh(1.2) ≈ 0.83
|
||||
convergence_score = 1 - 0.0465 = 0.9535
|
||||
vol_score = min(1, 1.5 - 1) = 0.5
|
||||
|
||||
strength_up = 0.05 × 5 × (1 + 0.8 × 0.5) × (1 + 0.8 × 0.5)
|
||||
= 0.25 × 1.4 × 1.4
|
||||
= 0.49
|
||||
strength = 0.60 × 0.83 + 0.25 × 0.9535 + 0.15 × 0.5
|
||||
= 0.498 + 0.238 + 0.075
|
||||
= 0.811
|
||||
|
||||
结果: 向上突破强度 = 0.49 (中度突破)
|
||||
实际结果: 0.9882 (因实际突破幅度更大)
|
||||
```
|
||||
|
||||
### 示例 2:弱势突破
|
||||
### 示例 2:中等突破(五芳斋 2026-01-20)
|
||||
|
||||
```
|
||||
输入:
|
||||
close = 10.1, upper_line = 10.0 (突破 1%)
|
||||
width_ratio = 0.7 (收敛较弱)
|
||||
volume_ratio = 0.9 (缩量)
|
||||
突破幅度 ≈ 3%
|
||||
width_ratio = 0.2090
|
||||
volume_ratio ≈ 1.0 (未放量)
|
||||
|
||||
计算:
|
||||
price_up = 0.01
|
||||
convergence_bonus = 0.3
|
||||
vol_bonus = 0
|
||||
price_score = tanh(0.03 × 15) = tanh(0.45) ≈ 0.42
|
||||
convergence_score = 1 - 0.2090 = 0.791
|
||||
vol_score = 0
|
||||
|
||||
strength_up = 0.01 × 5 × (1 + 0.3 × 0.5) × (1 + 0)
|
||||
= 0.05 × 1.15 × 1.0
|
||||
= 0.0575
|
||||
strength = 0.60 × 0.42 + 0.25 × 0.791 + 0.15 × 0
|
||||
= 0.252 + 0.198 + 0
|
||||
= 0.450
|
||||
|
||||
结果: 向上突破强度 = 0.06 (微弱突破)
|
||||
实际结果: 0.5816 (因实际突破幅度略大于 3%)
|
||||
```
|
||||
|
||||
### 示例 3:极强突破
|
||||
### 示例 3:弱势突破(康华生物 2026-01-20)
|
||||
|
||||
```
|
||||
输入:
|
||||
close = 11.0, upper_line = 10.0 (突破 10%)
|
||||
width_ratio = 0.15 (极度收敛)
|
||||
volume_ratio = 2.5 (放量 150%)
|
||||
突破幅度 ≈ 2%
|
||||
width_ratio = 0.1338
|
||||
volume_ratio ≈ 1.0 (未放量)
|
||||
|
||||
计算:
|
||||
price_up = 0.10
|
||||
convergence_bonus = 0.85
|
||||
vol_bonus = 1.0
|
||||
price_score = tanh(0.02 × 15) = tanh(0.30) ≈ 0.29
|
||||
convergence_score = 1 - 0.1338 = 0.866
|
||||
vol_score = 0
|
||||
|
||||
strength_up = 0.10 × 5 × (1 + 0.85 × 0.5) × (1 + 1.0 × 0.5)
|
||||
= 0.50 × 1.425 × 1.5
|
||||
= 1.07 → 截断为 1.0
|
||||
strength = 0.60 × 0.29 + 0.25 × 0.866 + 0.15 × 0
|
||||
= 0.174 + 0.217 + 0
|
||||
= 0.391
|
||||
|
||||
结果: 向上突破强度 = 1.0 (满分)
|
||||
实际结果: 0.4797 (因实际突破幅度略大于 2%)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 强度等级参考
|
||||
|
||||
| 强度范围 | 等级 | 含义 |
|
||||
|----------|------|------|
|
||||
| 0 ~ 0.1 | 无/微弱 | 未突破或假突破风险高 |
|
||||
| 0.1 ~ 0.3 | 轻度 | 有突破迹象,需观察确认 |
|
||||
| 0.3 ~ 0.6 | 中度 | 有效突破,可作为参考信号 |
|
||||
| 0.6 ~ 1.0 | 强势 | 高置信度突破,值得关注 |
|
||||
| 强度范围 | 等级 | 含义 | 占比参考 |
|
||||
|----------|------|------|----------|
|
||||
| 0 ~ 0.3 | 微弱 | 假突破风险高,需谨慎 | ~8% |
|
||||
| 0.3 ~ 0.5 | 轻度 | 有突破迹象,建议观察 | ~27% |
|
||||
| 0.5 ~ 0.7 | 中度 | 有效突破,可作为参考 | ~28% |
|
||||
| 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
|
||||
├── calc_breakout_strength() # 突破强度计算函数
|
||||
└── detect_converging_triangle() # 调用位置
|
||||
├── calc_breakout_strength() # 突破强度计算函数 (第 180-260 行)
|
||||
└── detect_converging_triangle() # 调用位置 (第 401 行)
|
||||
|
||||
scripts/triangle_config.py # 参数配置(严格模式/默认模式/宽松模式)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 相关参数
|
||||
## 相关参数配置
|
||||
|
||||
| 参数 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `vol_window` | 20 | 计算成交量均值的窗口大小 |
|
||||
| `vol_k` | 1.3 | 成交量确认阈值(volume_confirmed 判断用) |
|
||||
| `break_tol` | 0.001 | 突破判定容差 |
|
||||
| 参数 | 严格模式 | 默认模式 | 说明 |
|
||||
|------|----------|----------|------|
|
||||
| `window` | 120 | 120 | 检测窗口大小(交易日) |
|
||||
| `shrink_ratio` | 0.6 | 0.8 | 收敛比例阈值(越小越严格) |
|
||||
| `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
@ -1,71 +1,57 @@
|
||||
# 收敛三角形突破强度选股简报
|
||||
# 收敛三角形当日选股简报
|
||||
|
||||
- 生成时间:2026-01-21 17:55
|
||||
- 数据范围:20240909 ~ 20260120
|
||||
- 记录数:1837
|
||||
- 突破方向统计:上破 258 / 下破 251 / 无突破 1328
|
||||
## 数据说明
|
||||
|
||||
## 筛选条件
|
||||
- 强度阈值:> 0.3
|
||||
- 每方向最多输出:20 只(按单只股票的最高强度去重)
|
||||
- **股票池**:108 只个股(从万得全A按顺序索引,等距50取样)
|
||||
- **检测窗口**:120 个交易日
|
||||
- **检测算法**:收敛三角形形态识别(基于枢轴点、趋势线拟合与收敛度判定)
|
||||
|
||||
## 综合突破强度候选
|
||||
| 排名 | 股票 | 日期 | 方向 | 综合强度 | 宽度比 | 放量确认 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| 1 | SZ300027 华谊兄弟 | 20260120 | down | 1.0000 | 0.1490 | 否 |
|
||||
| 2 | SZ002802 洪汇新材 | 20251211 | down | 1.0000 | 0.5451 | 否 |
|
||||
| 3 | SH688550 瑞联新材 | 20251208 | down | 1.0000 | 0.0508 | 否 |
|
||||
| 4 | SZ002242 九阳股份 | 20251112 | up | 1.0000 | 0.1145 | 是 |
|
||||
| 5 | SH600395 盘江股份 | 20250916 | up | 1.0000 | 0.0011 | 否 |
|
||||
| 6 | SZ002493 荣盛石化 | 20250820 | up | 1.0000 | 0.2808 | 是 |
|
||||
| 7 | SZ002192 融捷股份 | 20250808 | up | 1.0000 | 0.0833 | 否 |
|
||||
| 8 | SH600846 同济科技 | 20241128 | up | 1.0000 | 0.6111 | 否 |
|
||||
| 9 | SH600588 用友网络 | 20241104 | up | 1.0000 | 0.1045 | 否 |
|
||||
| 10 | SH603237 五芳斋 | 20241104 | up | 1.0000 | 0.0482 | 否 |
|
||||
| 11 | SZ300379 *ST东通 | 20241018 | up | 1.0000 | 0.7060 | 是 |
|
||||
| 12 | SH603707 健友股份 | 20241008 | up | 1.0000 | 0.3252 | 是 |
|
||||
| 13 | SZ002092 中泰化学 | 20241008 | up | 1.0000 | 0.1271 | 是 |
|
||||
| 14 | SH601236 红塔证券 | 20240927 | up | 1.0000 | 0.0672 | 是 |
|
||||
| 15 | SZ002694 顾地科技 | 20251120 | up | 0.8276 | 0.0439 | 否 |
|
||||
| 16 | SZ002293 罗莱生活 | 20250925 | down | 0.7794 | 0.1341 | 否 |
|
||||
| 17 | SH600984 建设机械 | 20260120 | down | 0.7263 | 0.2516 | 否 |
|
||||
| 18 | SZ002966 苏州银行 | 20260116 | down | 0.6074 | 0.5513 | 是 |
|
||||
| 19 | SH600475 华光环能 | 20250519 | up | 0.5628 | 0.2505 | 是 |
|
||||
| 20 | SZ002544 普天科技 | 20250723 | down | 0.5161 | 0.0167 | 否 |
|
||||
## 20260120 当日统计
|
||||
|
||||
## 向上突破候选
|
||||
| 排名 | 股票 | 日期 | 强度 | 宽度比 | 触碰(上/下) | 放量确认 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| 1 | SZ002242 九阳股份 | 20251112 | 1.0000 | 0.1145 | 8/5 | 是 |
|
||||
| 2 | SH600395 盘江股份 | 20250916 | 1.0000 | 0.0011 | 6/6 | 否 |
|
||||
| 3 | SZ002493 荣盛石化 | 20250820 | 1.0000 | 0.2808 | 5/5 | 是 |
|
||||
| 4 | SZ002192 融捷股份 | 20250808 | 1.0000 | 0.0833 | 4/3 | 否 |
|
||||
| 5 | SH600846 同济科技 | 20241128 | 1.0000 | 0.6111 | 8/6 | 否 |
|
||||
| 6 | SH600588 用友网络 | 20241104 | 1.0000 | 0.1045 | 3/5 | 否 |
|
||||
| 7 | SH603237 五芳斋 | 20241104 | 1.0000 | 0.0482 | 4/5 | 否 |
|
||||
| 8 | SZ300379 *ST东通 | 20241018 | 1.0000 | 0.7060 | 2/2 | 是 |
|
||||
| 9 | SH603707 健友股份 | 20241008 | 1.0000 | 0.3252 | 4/8 | 是 |
|
||||
| 10 | SZ002092 中泰化学 | 20241008 | 1.0000 | 0.1271 | 4/2 | 是 |
|
||||
| 11 | SH601236 红塔证券 | 20240927 | 1.0000 | 0.0672 | 4/7 | 是 |
|
||||
| 12 | SZ002694 顾地科技 | 20251120 | 0.8276 | 0.0439 | 3/3 | 否 |
|
||||
| 13 | SH600475 华光环能 | 20250519 | 0.5628 | 0.2505 | 7/6 | 是 |
|
||||
| 14 | SZ002544 普天科技 | 20250818 | 0.3096 | 0.0912 | 5/5 | 是 |
|
||||
- 生成时间:2026-01-22 10:44
|
||||
- 当日满足收敛三角形的个股:23 只
|
||||
- 向上突破:18 只
|
||||
- 向下突破:3 只
|
||||
- 无突破(形态成立但未突破):2 只
|
||||
|
||||
## 向下突破候选
|
||||
| 排名 | 股票 | 日期 | 强度 | 宽度比 | 触碰(上/下) | 放量确认 |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| 1 | SZ300027 华谊兄弟 | 20260120 | 1.0000 | 0.1490 | 4/3 | 否 |
|
||||
| 2 | SZ002802 洪汇新材 | 20251211 | 1.0000 | 0.5451 | 3/6 | 否 |
|
||||
| 3 | SH688550 瑞联新材 | 20251208 | 1.0000 | 0.0508 | 6/2 | 否 |
|
||||
| 4 | SZ002293 罗莱生活 | 20250925 | 0.7794 | 0.1341 | 4/3 | 否 |
|
||||
| 5 | SH600984 建设机械 | 20260120 | 0.7263 | 0.2516 | 5/3 | 否 |
|
||||
| 6 | SZ002966 苏州银行 | 20260116 | 0.6074 | 0.5513 | 4/5 | 是 |
|
||||
| 7 | SZ002092 中泰化学 | 20251216 | 0.5789 | 0.2660 | 7/5 | 否 |
|
||||
| 8 | SZ002544 普天科技 | 20250723 | 0.5161 | 0.0167 | 4/4 | 否 |
|
||||
| 9 | SZ002694 顾地科技 | 20251212 | 0.5158 | 0.0057 | 3/3 | 是 |
|
||||
## 向上突破
|
||||
| 排名 | 股票 | 突破强度 | 宽度比 | 触碰(上/下) | 放量确认 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 1 | SH600744 华银电力 | 0.9963 | 0.0125 | 4/2 | 是 |
|
||||
| 2 | SZ300530 领湃科技 | 0.9882 | 0.0465 | 2/3 | 是 |
|
||||
| 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 | SZ000796 凯撒旅业 | 0.7365 | 0.2816 | 2/2 | 否 |
|
||||
| 2 | SZ300892 品渥食品 | 0.3195 | 0.1505 | 2/2 | 否 |
|
||||
| 3 | SZ002966 苏州银行 | 0.2008 | 0.5492 | 2/2 | 否 |
|
||||
|
||||
## 无突破(形态成立)
|
||||
| 排名 | 股票 | 宽度比 | 触碰(上/下) |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | SH600281 华阳新材 | 0.5081 | 3/2 |
|
||||
| 2 | SZ300841 康华生物 | 0.2940 | 2/2 |
|
||||
|
||||
## 说明
|
||||
- 强度由价格突破幅度、收敛程度与成交量放大综合计算。
|
||||
- 仅统计 `breakout_dir` 与方向一致的记录。
|
||||
- 每只股票仅保留强度最高的一条记录(同强度取最新日期)。
|
||||
- 综合强度 = max(向上强度, 向下强度)。
|
||||
|
||||
- **突破强度**:价格突破幅度、收敛程度与成交量放大综合计算(0~1)
|
||||
- **宽度比**:三角形末端宽度 / 起始宽度(越小越收敛)
|
||||
- **触碰(上/下)**:价格触碰上沿和下沿的次数
|
||||
- **放量确认**:突破时成交量是否显著放大
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
BIN
scripts/__pycache__/plot_converging_triangles.cpython-313.pyc
Normal file
BIN
scripts/__pycache__/plot_converging_triangles.cpython-313.pyc
Normal file
Binary file not shown.
BIN
scripts/__pycache__/report_converging_triangles.cpython-313.pyc
Normal file
BIN
scripts/__pycache__/report_converging_triangles.cpython-313.pyc
Normal file
Binary file not shown.
BIN
scripts/__pycache__/run_converging_triangle.cpython-313.pyc
Normal file
BIN
scripts/__pycache__/run_converging_triangle.cpython-313.pyc
Normal file
Binary file not shown.
BIN
scripts/__pycache__/triangle_config.cpython-313.pyc
Normal file
BIN
scripts/__pycache__/triangle_config.cpython-313.pyc
Normal file
Binary file not shown.
213
scripts/pipeline_converging_triangle.py
Normal file
213
scripts/pipeline_converging_triangle.py
Normal 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()
|
||||
387
scripts/plot_converging_triangles.py
Normal file
387
scripts/plot_converging_triangles.py
Normal file
@ -0,0 +1,387 @@
|
||||
"""
|
||||
为当日满足收敛三角形的个股生成图表
|
||||
|
||||
用法:
|
||||
python scripts/plot_converging_triangles.py
|
||||
python scripts/plot_converging_triangles.py --date 20260120
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import os
|
||||
import pickle
|
||||
import sys
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
# 配置中文字体
|
||||
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'Arial Unicode MS'] # 支持中文
|
||||
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
|
||||
|
||||
# 让脚本能找到 src/ 下的模块
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "src"))
|
||||
|
||||
from converging_triangle import (
|
||||
ConvergingTriangleParams,
|
||||
detect_converging_triangle,
|
||||
fit_pivot_line,
|
||||
line_y,
|
||||
pivots_fractal,
|
||||
)
|
||||
|
||||
# 导入统一的参数配置
|
||||
from triangle_config import DETECTION_PARAMS, DISPLAY_WINDOW
|
||||
|
||||
|
||||
class FakeModule:
|
||||
"""空壳模块,绕过 model 依赖"""
|
||||
ndarray = np.ndarray
|
||||
|
||||
|
||||
def load_pkl(pkl_path: str) -> dict:
|
||||
"""加载 pkl 文件"""
|
||||
sys.modules['model'] = FakeModule()
|
||||
sys.modules['model.index_info'] = FakeModule()
|
||||
|
||||
with open(pkl_path, 'rb') as f:
|
||||
data = pickle.load(f)
|
||||
return data
|
||||
|
||||
|
||||
def load_ohlcv_from_pkl(data_dir: str) -> tuple:
|
||||
"""从 pkl 文件加载 OHLCV 数据"""
|
||||
open_data = load_pkl(os.path.join(data_dir, "open.pkl"))
|
||||
high_data = load_pkl(os.path.join(data_dir, "high.pkl"))
|
||||
low_data = load_pkl(os.path.join(data_dir, "low.pkl"))
|
||||
close_data = load_pkl(os.path.join(data_dir, "close.pkl"))
|
||||
volume_data = load_pkl(os.path.join(data_dir, "volume.pkl"))
|
||||
|
||||
dates = close_data["dtes"]
|
||||
tkrs = close_data["tkrs"]
|
||||
tkrs_name = close_data["tkrs_name"]
|
||||
|
||||
return (
|
||||
open_data["mtx"],
|
||||
high_data["mtx"],
|
||||
low_data["mtx"],
|
||||
close_data["mtx"],
|
||||
volume_data["mtx"],
|
||||
dates,
|
||||
tkrs,
|
||||
tkrs_name,
|
||||
)
|
||||
|
||||
|
||||
def load_daily_stocks(csv_path: str, target_date: int) -> List[Dict]:
|
||||
"""从CSV读取指定日期的股票列表"""
|
||||
stocks = []
|
||||
with open(csv_path, newline="", encoding="utf-8-sig") as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
try:
|
||||
date = int(row.get("date", "0"))
|
||||
if date == target_date:
|
||||
stocks.append({
|
||||
"stock_idx": int(row.get("stock_idx", "0")),
|
||||
"stock_code": row.get("stock_code", ""),
|
||||
"stock_name": row.get("stock_name", ""),
|
||||
"breakout_dir": row.get("breakout_dir", "none"),
|
||||
"breakout_strength_up": float(row.get("breakout_strength_up", "0")),
|
||||
"breakout_strength_down": float(row.get("breakout_strength_down", "0")),
|
||||
})
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
return stocks
|
||||
|
||||
|
||||
def plot_triangle(
|
||||
stock_idx: int,
|
||||
stock_code: str,
|
||||
stock_name: str,
|
||||
date_idx: int,
|
||||
high_mtx: np.ndarray,
|
||||
low_mtx: np.ndarray,
|
||||
close_mtx: np.ndarray,
|
||||
volume_mtx: np.ndarray,
|
||||
dates: np.ndarray,
|
||||
params: ConvergingTriangleParams,
|
||||
output_path: str,
|
||||
display_window: int = 500, # 新增:显示窗口大小
|
||||
) -> None:
|
||||
"""绘制单只股票的收敛三角形图"""
|
||||
|
||||
# 提取该股票数据并过滤NaN
|
||||
high_stock = high_mtx[stock_idx, :]
|
||||
low_stock = low_mtx[stock_idx, :]
|
||||
close_stock = close_mtx[stock_idx, :]
|
||||
volume_stock = volume_mtx[stock_idx, :]
|
||||
|
||||
valid_mask = ~np.isnan(close_stock)
|
||||
valid_indices = np.where(valid_mask)[0]
|
||||
|
||||
if len(valid_indices) < params.window:
|
||||
print(f" [跳过] {stock_code} {stock_name}: 有效数据不足")
|
||||
return
|
||||
|
||||
# 找到date_idx在有效数据中的位置
|
||||
if date_idx not in valid_indices:
|
||||
print(f" [跳过] {stock_code} {stock_name}: date_idx不在有效范围")
|
||||
return
|
||||
|
||||
valid_end = np.where(valid_indices == date_idx)[0][0]
|
||||
if valid_end < params.window - 1:
|
||||
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]
|
||||
|
||||
# 提取显示窗口数据(用于绘图,更长的历史)
|
||||
display_start = max(0, valid_end - display_window + 1)
|
||||
display_high = high_stock[valid_mask][display_start:valid_end + 1]
|
||||
display_low = low_stock[valid_mask][display_start:valid_end + 1]
|
||||
display_close = close_stock[valid_mask][display_start:valid_end + 1]
|
||||
display_volume = volume_stock[valid_mask][display_start:valid_end + 1]
|
||||
display_dates = dates[valid_indices[display_start:valid_end + 1]]
|
||||
|
||||
# 检测三角形(使用检测窗口数据)
|
||||
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,
|
||||
)
|
||||
|
||||
if not result.is_valid:
|
||||
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)
|
||||
|
||||
# 计算枢轴点(与检测算法一致)
|
||||
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]]
|
||||
|
||||
# 创建图表
|
||||
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 8),
|
||||
gridspec_kw={'height_ratios': [3, 1]})
|
||||
|
||||
# 主图:价格和趋势线(使用显示窗口数据)
|
||||
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='--')
|
||||
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(
|
||||
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"触碰: 上{result.touches_upper}/下{result.touches_lower} "
|
||||
f"放量确认: {'是' if result.volume_confirmed else '否' if result.volume_confirmed is False else '-'}",
|
||||
fontsize=11, pad=10
|
||||
)
|
||||
ax1.set_ylabel('价格', fontsize=10)
|
||||
ax1.legend(loc='best', fontsize=9)
|
||||
ax1.grid(True, alpha=0.3)
|
||||
|
||||
# X轴日期标签(稀疏显示,基于显示窗口)
|
||||
step = max(1, len(display_dates) // 10)
|
||||
tick_indices = np.arange(0, len(display_dates), step)
|
||||
ax1.set_xticks(tick_indices)
|
||||
ax1.set_xticklabels(display_dates[tick_indices], rotation=45, ha='right', fontsize=8)
|
||||
|
||||
# 副图:成交量(使用显示窗口数据)
|
||||
ax2.bar(x_display, display_volume, width=0.8, color='skyblue', alpha=0.6)
|
||||
ax2.set_ylabel('成交量', fontsize=10)
|
||||
ax2.set_xlabel('交易日', fontsize=10)
|
||||
ax2.grid(True, alpha=0.3, axis='y')
|
||||
ax2.set_xticks(tick_indices)
|
||||
ax2.set_xticklabels(display_dates[tick_indices], rotation=45, ha='right', fontsize=8)
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig(output_path, dpi=120)
|
||||
plt.close()
|
||||
|
||||
print(f" [完成] {stock_code} {stock_name} -> {output_path}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="为当日满足收敛三角形的个股生成图表")
|
||||
parser.add_argument(
|
||||
"--input",
|
||||
default=os.path.join("outputs", "converging_triangles", "all_results.csv"),
|
||||
help="输入 CSV 路径",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--date",
|
||||
type=int,
|
||||
default=None,
|
||||
help="指定日期(YYYYMMDD),默认为数据最新日",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
default=os.path.join("outputs", "converging_triangles", "charts"),
|
||||
help="图表输出目录",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
print("=" * 70)
|
||||
print("收敛三角形图表生成")
|
||||
print("=" * 70)
|
||||
|
||||
# 1. 加载数据
|
||||
print("\n[1] 加载 OHLCV 数据...")
|
||||
data_dir = os.path.join(os.path.dirname(__file__), "..", "data")
|
||||
open_mtx, high_mtx, low_mtx, close_mtx, volume_mtx, dates, tkrs, tkrs_name = load_ohlcv_from_pkl(data_dir)
|
||||
print(f" 股票数: {len(tkrs)}, 交易日数: {len(dates)}")
|
||||
|
||||
# 2. 确定目标日期
|
||||
if args.date:
|
||||
target_date = args.date
|
||||
else:
|
||||
# 从CSV读取最新日期
|
||||
with open(args.input, newline="", encoding="utf-8-sig") as f:
|
||||
reader = csv.DictReader(f)
|
||||
all_dates = [int(r.get("date", "0")) for r in reader if r.get("date") and r["date"].isdigit()]
|
||||
target_date = max(all_dates) if all_dates else 0
|
||||
|
||||
if target_date == 0:
|
||||
print("错误: 无法确定目标日期")
|
||||
return
|
||||
|
||||
print(f"\n[2] 目标日期: {target_date}")
|
||||
|
||||
# 3. 加载当日股票列表
|
||||
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)
|
||||
|
||||
# 清空目录中的旧图片
|
||||
print(f"\n[4] 清空输出目录...")
|
||||
old_files = [f for f in os.listdir(args.output_dir) if f.endswith('.png')]
|
||||
for f in old_files:
|
||||
os.remove(os.path.join(args.output_dir, f))
|
||||
print(f" 已删除 {len(old_files)} 个旧图片")
|
||||
|
||||
# 5. 检测参数(从统一配置导入)
|
||||
params = DETECTION_PARAMS
|
||||
|
||||
# 6. 找到target_date在dates中的索引
|
||||
date_idx = np.where(dates == target_date)[0]
|
||||
if len(date_idx) == 0:
|
||||
print(f"错误: 日期 {target_date} 不在数据范围内")
|
||||
return
|
||||
date_idx = date_idx[0]
|
||||
|
||||
# 7. 生成图表
|
||||
print(f"\n[3] 生成图表...")
|
||||
for stock in stocks:
|
||||
stock_idx = stock["stock_idx"]
|
||||
stock_code = stock["stock_code"]
|
||||
stock_name = stock["stock_name"]
|
||||
|
||||
output_filename = f"{target_date}_{stock_code}_{stock_name}.png"
|
||||
output_path = os.path.join(args.output_dir, output_filename)
|
||||
|
||||
try:
|
||||
plot_triangle(
|
||||
stock_idx=stock_idx,
|
||||
stock_code=stock_code,
|
||||
stock_name=stock_name,
|
||||
date_idx=date_idx,
|
||||
high_mtx=high_mtx,
|
||||
low_mtx=low_mtx,
|
||||
close_mtx=close_mtx,
|
||||
volume_mtx=volume_mtx,
|
||||
dates=dates,
|
||||
params=params,
|
||||
output_path=output_path,
|
||||
display_window=DISPLAY_WINDOW, # 从配置文件读取
|
||||
)
|
||||
except Exception as e:
|
||||
print(f" [错误] {stock_code} {stock_name}: {e}")
|
||||
|
||||
print(f"\n完成!图表已保存至: {args.output_dir}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,6 +1,21 @@
|
||||
"""
|
||||
生成收敛三角形突破强度选股简报(Markdown)
|
||||
默认读取 outputs/converging_triangles/all_results.csv
|
||||
|
||||
用法示例:
|
||||
1) 使用默认参数生成最新交易日排名
|
||||
python scripts/report_converging_triangles.py
|
||||
|
||||
2) 指定强度阈值与输出数量
|
||||
python scripts/report_converging_triangles.py --threshold 0.4 --top-n 30
|
||||
|
||||
3) 指定报告日期(YYYYMMDD)
|
||||
python scripts/report_converging_triangles.py --report-date 20260120
|
||||
|
||||
4) 指定输入与输出路径
|
||||
python scripts/report_converging_triangles.py \
|
||||
--input outputs/converging_triangles/all_results.csv \
|
||||
--output outputs/converging_triangles/report.md
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@ -8,9 +23,16 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import csv
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
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]:
|
||||
if value is None or value == "":
|
||||
@ -93,41 +115,30 @@ def best_by_stock(
|
||||
)
|
||||
|
||||
|
||||
def best_combined_by_stock(
|
||||
def daily_rank_by_strength(
|
||||
rows: List[Dict[str, Any]],
|
||||
threshold: float,
|
||||
target_date: int,
|
||||
top_n: int,
|
||||
) -> List[Dict[str, Any]]:
|
||||
filtered: List[Dict[str, Any]] = []
|
||||
candidates: List[Dict[str, Any]] = []
|
||||
for r in rows:
|
||||
if r.get("date") != target_date:
|
||||
continue
|
||||
if r.get("breakout_dir") not in ("up", "down"):
|
||||
continue
|
||||
strength_up = r.get("breakout_strength_up", 0.0)
|
||||
strength_down = r.get("breakout_strength_down", 0.0)
|
||||
combined = max(strength_up, strength_down)
|
||||
if combined <= threshold:
|
||||
continue
|
||||
r = dict(r)
|
||||
r["combined_strength"] = combined
|
||||
r["combined_dir"] = "up" if strength_up >= strength_down else "down"
|
||||
filtered.append(r)
|
||||
|
||||
best_map: Dict[str, Dict[str, Any]] = {}
|
||||
for r in filtered:
|
||||
key = r.get("stock_code") or f"IDX{r.get('stock_idx', '')}"
|
||||
prev = best_map.get(key)
|
||||
if prev is None:
|
||||
best_map[key] = r
|
||||
continue
|
||||
if r["combined_strength"] > prev["combined_strength"]:
|
||||
best_map[key] = r
|
||||
elif r["combined_strength"] == prev["combined_strength"] and r.get("date", 0) > prev.get("date", 0):
|
||||
best_map[key] = r
|
||||
candidates.append(r)
|
||||
|
||||
return sorted(
|
||||
best_map.values(),
|
||||
candidates,
|
||||
key=lambda x: (x.get("combined_strength", 0.0), x.get("date", 0)),
|
||||
reverse=True,
|
||||
)
|
||||
)[:top_n]
|
||||
|
||||
|
||||
def count_by_dir(rows: List[Dict[str, Any]]) -> Dict[str, int]:
|
||||
@ -152,90 +163,118 @@ def build_report(
|
||||
rows: List[Dict[str, Any]],
|
||||
threshold: float,
|
||||
top_n: int,
|
||||
report_date: Optional[int],
|
||||
output_path: str,
|
||||
) -> None:
|
||||
total = len(rows)
|
||||
if total == 0:
|
||||
content = "# 收敛三角形突破强度选股简报\n\n数据为空。\n"
|
||||
content = "# 收敛三角形当日选股简报\n\n数据为空。\n"
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
return
|
||||
|
||||
dates = [r.get("date", 0) for r in rows if r.get("date", 0) > 0]
|
||||
date_min = min(dates) if dates else 0
|
||||
date_max = max(dates) if dates else 0
|
||||
counts = count_by_dir(rows)
|
||||
|
||||
up_picks = best_by_stock(rows, "breakout_strength_up", threshold, "up")[:top_n]
|
||||
down_picks = best_by_stock(rows, "breakout_strength_down", threshold, "down")[:top_n]
|
||||
combined_picks = best_combined_by_stock(rows, threshold)[:top_n]
|
||||
target_date = report_date or date_max
|
||||
daily_rows = [r for r in rows if r.get("date") == target_date]
|
||||
daily_counts = count_by_dir(daily_rows)
|
||||
|
||||
# 按方向分类并排序
|
||||
daily_up = sorted(
|
||||
[r for r in daily_rows if r.get("breakout_dir") == "up"],
|
||||
key=lambda x: x.get("breakout_strength_up", 0.0),
|
||||
reverse=True
|
||||
)
|
||||
daily_down = sorted(
|
||||
[r for r in daily_rows if r.get("breakout_dir") == "down"],
|
||||
key=lambda x: x.get("breakout_strength_down", 0.0),
|
||||
reverse=True
|
||||
)
|
||||
daily_none = sorted(
|
||||
[r for r in daily_rows if r.get("breakout_dir") == "none"],
|
||||
key=lambda x: max(x.get("breakout_strength_up", 0.0), x.get("breakout_strength_down", 0.0)),
|
||||
reverse=True
|
||||
)
|
||||
|
||||
now_str = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
lines: List[str] = []
|
||||
lines.append("# 收敛三角形突破强度选股简报")
|
||||
lines.append("# 收敛三角形当日选股简报")
|
||||
lines.append("")
|
||||
lines.append("## 数据说明")
|
||||
lines.append("")
|
||||
lines.append("- **股票池**:108 只个股(从万得全A按顺序索引,等距50取样)")
|
||||
lines.append(f"- **检测窗口**:{DETECTION_PARAMS.window} 个交易日")
|
||||
lines.append("- **检测算法**:收敛三角形形态识别(基于枢轴点、趋势线拟合与收敛度判定)")
|
||||
lines.append("")
|
||||
lines.append(f"## {target_date} 当日统计")
|
||||
lines.append("")
|
||||
lines.append(f"- 生成时间:{now_str}")
|
||||
if date_min and date_max:
|
||||
lines.append(f"- 数据范围:{date_min} ~ {date_max}")
|
||||
lines.append(f"- 记录数:{total}")
|
||||
lines.append(f"- 突破方向统计:上破 {counts.get('up', 0)} / 下破 {counts.get('down', 0)} / 无突破 {counts.get('none', 0)}")
|
||||
lines.append(f"- 当日满足收敛三角形的个股:{len(daily_rows)} 只")
|
||||
lines.append(f" - 向上突破:{daily_counts.get('up', 0)} 只")
|
||||
lines.append(f" - 向下突破:{daily_counts.get('down', 0)} 只")
|
||||
lines.append(f" - 无突破(形态成立但未突破):{daily_counts.get('none', 0)} 只")
|
||||
lines.append("")
|
||||
lines.append("## 筛选条件")
|
||||
lines.append(f"- 强度阈值:> {threshold}")
|
||||
lines.append(f"- 每方向最多输出:{top_n} 只(按单只股票的最高强度去重)")
|
||||
lines.append("")
|
||||
lines.append("## 综合突破强度候选")
|
||||
if combined_picks:
|
||||
lines.append("| 排名 | 股票 | 日期 | 方向 | 综合强度 | 宽度比 | 放量确认 |")
|
||||
lines.append("| --- | --- | --- | --- | --- | --- | --- |")
|
||||
for i, r in enumerate(combined_picks, start=1):
|
||||
|
||||
# 向上突破
|
||||
lines.append("## 向上突破")
|
||||
if daily_up:
|
||||
lines.append("| 排名 | 股票 | 突破强度 | 宽度比 | 触碰(上/下) | 放量确认 |")
|
||||
lines.append("| --- | --- | --- | --- | --- | --- |")
|
||||
for i, r in enumerate(daily_up, start=1):
|
||||
stock = f"{r.get('stock_code', '')} {r.get('stock_name', '')}".strip()
|
||||
lines.append(
|
||||
f"| {i} | {stock} | {r.get('date', '')} | "
|
||||
f"{r.get('combined_dir', '')} | {r.get('combined_strength', 0.0):.4f} | "
|
||||
f"{r.get('width_ratio', 0.0):.4f} | {fmt_bool(r.get('volume_confirmed'))} |"
|
||||
)
|
||||
else:
|
||||
lines.append("无满足条件的综合突破记录。")
|
||||
lines.append("")
|
||||
lines.append("## 向上突破候选")
|
||||
if up_picks:
|
||||
lines.append("| 排名 | 股票 | 日期 | 强度 | 宽度比 | 触碰(上/下) | 放量确认 |")
|
||||
lines.append("| --- | --- | --- | --- | --- | --- | --- |")
|
||||
for i, r in enumerate(up_picks, start=1):
|
||||
stock = f"{r.get('stock_code', '')} {r.get('stock_name', '')}".strip()
|
||||
lines.append(
|
||||
f"| {i} | {stock} | {r.get('date', '')} | "
|
||||
f"| {i} | {stock} | "
|
||||
f"{r.get('breakout_strength_up', 0.0):.4f} | "
|
||||
f"{r.get('width_ratio', 0.0):.4f} | "
|
||||
f"{r.get('touches_upper', 0)}/{r.get('touches_lower', 0)} | "
|
||||
f"{fmt_bool(r.get('volume_confirmed'))} |"
|
||||
)
|
||||
else:
|
||||
lines.append("无满足条件的向上突破记录。")
|
||||
lines.append("当日无向上突破记录。")
|
||||
lines.append("")
|
||||
lines.append("## 向下突破候选")
|
||||
if down_picks:
|
||||
lines.append("| 排名 | 股票 | 日期 | 强度 | 宽度比 | 触碰(上/下) | 放量确认 |")
|
||||
lines.append("| --- | --- | --- | --- | --- | --- | --- |")
|
||||
for i, r in enumerate(down_picks, start=1):
|
||||
|
||||
# 向下突破
|
||||
lines.append("## 向下突破")
|
||||
if daily_down:
|
||||
lines.append("| 排名 | 股票 | 突破强度 | 宽度比 | 触碰(上/下) | 放量确认 |")
|
||||
lines.append("| --- | --- | --- | --- | --- | --- |")
|
||||
for i, r in enumerate(daily_down, start=1):
|
||||
stock = f"{r.get('stock_code', '')} {r.get('stock_name', '')}".strip()
|
||||
lines.append(
|
||||
f"| {i} | {stock} | {r.get('date', '')} | "
|
||||
f"| {i} | {stock} | "
|
||||
f"{r.get('breakout_strength_down', 0.0):.4f} | "
|
||||
f"{r.get('width_ratio', 0.0):.4f} | "
|
||||
f"{r.get('touches_upper', 0)}/{r.get('touches_lower', 0)} | "
|
||||
f"{fmt_bool(r.get('volume_confirmed'))} |"
|
||||
)
|
||||
else:
|
||||
lines.append("无满足条件的向下突破记录。")
|
||||
lines.append("当日无向下突破记录。")
|
||||
lines.append("")
|
||||
|
||||
# 无突破(形态成立)
|
||||
lines.append("## 无突破(形态成立)")
|
||||
if daily_none:
|
||||
lines.append("| 排名 | 股票 | 宽度比 | 触碰(上/下) |")
|
||||
lines.append("| --- | --- | --- | --- |")
|
||||
for i, r in enumerate(daily_none, start=1):
|
||||
stock = f"{r.get('stock_code', '')} {r.get('stock_name', '')}".strip()
|
||||
lines.append(
|
||||
f"| {i} | {stock} | "
|
||||
f"{r.get('width_ratio', 0.0):.4f} | "
|
||||
f"{r.get('touches_upper', 0)}/{r.get('touches_lower', 0)} |"
|
||||
)
|
||||
else:
|
||||
lines.append("当日无未突破记录。")
|
||||
lines.append("")
|
||||
|
||||
lines.append("## 说明")
|
||||
lines.append("- 强度由价格突破幅度、收敛程度与成交量放大综合计算。")
|
||||
lines.append("- 仅统计 `breakout_dir` 与方向一致的记录。")
|
||||
lines.append("- 每只股票仅保留强度最高的一条记录(同强度取最新日期)。")
|
||||
lines.append("- 综合强度 = max(向上强度, 向下强度)。")
|
||||
lines.append("")
|
||||
lines.append("- **突破强度**:价格突破幅度、收敛程度与成交量放大综合计算(0~1)")
|
||||
lines.append("- **宽度比**:三角形末端宽度 / 起始宽度(越小越收敛)")
|
||||
lines.append("- **触碰(上/下)**:价格触碰上沿和下沿的次数")
|
||||
lines.append("- **放量确认**:突破时成交量是否显著放大")
|
||||
lines.append("")
|
||||
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
@ -256,6 +295,7 @@ def main() -> None:
|
||||
)
|
||||
parser.add_argument("--threshold", type=float, default=0.3, help="强度阈值")
|
||||
parser.add_argument("--top-n", type=int, default=20, help="每方向输出数量")
|
||||
parser.add_argument("--report-date", type=int, default=None, help="指定报告日期(YYYYMMDD)")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not os.path.exists(args.input):
|
||||
@ -265,7 +305,7 @@ def main() -> None:
|
||||
output_dir = os.path.dirname(args.output)
|
||||
if output_dir:
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
build_report(rows, args.threshold, args.top_n, args.output)
|
||||
build_report(rows, args.threshold, args.top_n, args.report_date, args.output)
|
||||
print(f"Report saved to: {args.output}")
|
||||
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
import os
|
||||
import sys
|
||||
import pickle
|
||||
import time
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
@ -25,31 +26,14 @@ from converging_triangle import (
|
||||
# --- 数据源 ---
|
||||
DATA_DIR = os.path.join(os.path.dirname(__file__), "..", "data")
|
||||
|
||||
# --- 检测参数 ---
|
||||
PARAMS = ConvergingTriangleParams(
|
||||
window=400,
|
||||
pivot_k=20,
|
||||
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,
|
||||
# --- 导入统一的参数配置 ---
|
||||
from triangle_config import (
|
||||
DETECTION_PARAMS as PARAMS,
|
||||
RECENT_DAYS,
|
||||
ONLY_VALID,
|
||||
VERBOSE,
|
||||
)
|
||||
|
||||
# --- 计算范围 ---
|
||||
# 设为 None 表示计算全部历史;设为具体数字可以只算最近 N 天
|
||||
RECENT_DAYS = 500 # 默认只算最近 500 天,避免计算时间过长
|
||||
|
||||
# --- 输出控制 ---
|
||||
ONLY_VALID = True # True: 只输出识别到三角形的记录; False: 输出所有记录
|
||||
VERBOSE = True
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# pkl 数据加载
|
||||
@ -108,17 +92,22 @@ def load_ohlcv_from_pkl(data_dir: str) -> tuple:
|
||||
# ============================================================================
|
||||
|
||||
def main() -> None:
|
||||
start_time = time.time()
|
||||
|
||||
print("=" * 70)
|
||||
print("Converging Triangle Batch Detection")
|
||||
print("=" * 70)
|
||||
|
||||
# 1. 加载数据
|
||||
print("\n[1] Loading OHLCV pkl files...")
|
||||
load_start = time.time()
|
||||
open_mtx, high_mtx, low_mtx, close_mtx, volume_mtx, dates, tkrs, tkrs_name = load_ohlcv_from_pkl(DATA_DIR)
|
||||
load_time = time.time() - load_start
|
||||
n_stocks, n_days = close_mtx.shape
|
||||
print(f" Stocks: {n_stocks}")
|
||||
print(f" Days: {n_days}")
|
||||
print(f" Date range: {dates[0]} ~ {dates[-1]}")
|
||||
print(f" 加载耗时: {load_time:.2f} 秒")
|
||||
|
||||
# 2. 找有效数据范围(排除全 NaN 的列)
|
||||
any_valid = np.any(~np.isnan(close_mtx), axis=0)
|
||||
@ -142,6 +131,7 @@ def main() -> None:
|
||||
|
||||
# 3. 批量检测
|
||||
print("\n[3] Running batch detection...")
|
||||
detect_start = time.time()
|
||||
df = detect_converging_triangle_batch(
|
||||
open_mtx=open_mtx,
|
||||
high_mtx=high_mtx,
|
||||
@ -154,6 +144,8 @@ def main() -> None:
|
||||
only_valid=ONLY_VALID,
|
||||
verbose=VERBOSE,
|
||||
)
|
||||
detect_time = time.time() - detect_start
|
||||
print(f" 检测耗时: {detect_time:.2f} 秒")
|
||||
|
||||
# 4. 添加股票代码、名称和真实日期
|
||||
if len(df) > 0:
|
||||
@ -225,6 +217,11 @@ def main() -> None:
|
||||
display_cols = [c for c in display_cols if c in df.columns]
|
||||
print(df[display_cols].head(10).to_string(index=False))
|
||||
|
||||
total_time = time.time() - start_time
|
||||
print("\n" + "=" * 70)
|
||||
print(f"总耗时: {total_time:.2f} 秒 ({total_time/60:.2f} 分钟)")
|
||||
print("=" * 70)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
166
scripts/triangle_config.py
Normal file
166
scripts/triangle_config.py
Normal 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)
|
||||
Binary file not shown.
@ -173,6 +173,118 @@ def line_y(a: float, b: float, x: np.ndarray) -> np.ndarray:
|
||||
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)
|
||||
|
||||
综合考虑:
|
||||
- 价格突破幅度 (close 相对于上/下沿的距离)
|
||||
- 成交量放大倍数
|
||||
- 收敛程度 (width_ratio 越小越强)
|
||||
使用加权求和,各分量权重:
|
||||
- 突破幅度分 (60%): tanh 非线性归一化,3%突破≈0.42,5%突破≈0.64,10%突破≈0.91
|
||||
- 收敛分 (25%): 1 - 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:
|
||||
(strength_up, strength_down)
|
||||
"""
|
||||
# 价格突破分数
|
||||
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
|
||||
import math
|
||||
|
||||
# 收敛加成 (越收敛, 突破越有效)
|
||||
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倍=满分)
|
||||
vol_bonus = min(1, max(0, volume_ratio - 1)) if volume_ratio > 0 else 0
|
||||
# 1. 价格突破分数(tanh 非线性归一化)
|
||||
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
|
||||
|
||||
# 加权合成
|
||||
# 基础分数 * 收敛加成 * 成交量加成
|
||||
strength_up = min(1.0, price_up * 5 * (1 + convergence_bonus * 0.5) * (1 + vol_bonus * 0.5))
|
||||
strength_down = min(1.0, price_down * 5 * (1 + convergence_bonus * 0.5) * (1 + vol_bonus * 0.5))
|
||||
if lower_line > 0:
|
||||
pct_down = max(0.0, (lower_line - close) / lower_line)
|
||||
price_score_down = math.tanh(pct_down * TANH_SCALE)
|
||||
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:
|
||||
return invalid_result
|
||||
|
||||
# 拟合边界线
|
||||
if params.boundary_source == "full":
|
||||
x_upper = x_all[start : end + 1]
|
||||
y_upper = high[start : end + 1]
|
||||
x_lower = x_all[start : end + 1]
|
||||
y_lower = low[start : end + 1]
|
||||
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, selected_ph = fit_pivot_line(
|
||||
pivot_indices=ph_in,
|
||||
pivot_values=high[ph_in],
|
||||
mode="upper",
|
||||
)
|
||||
|
||||
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):
|
||||
@ -304,21 +466,30 @@ def detect_converging_triangle(
|
||||
if width_ratio > params.shrink_ratio:
|
||||
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
|
||||
)
|
||||
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
|
||||
)
|
||||
# 触碰检测(枢轴点连线法)
|
||||
# 上沿:检查高点是否在上沿线下方或接近(不能大幅超过)
|
||||
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)
|
||||
# 触碰 = 高点接近上沿线(在容差范围内)
|
||||
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
|
||||
|
||||
# Apex 计算
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user