Enhance converging triangle analysis with detailed mode and outlier removal algorithm
- Added `--show-details` parameter to `pipeline_converging_triangle.py` for generating detailed charts that display all pivot points and fitting lines. - Implemented an iterative outlier removal algorithm in `fit_pivot_line` to improve the accuracy of pivot point fitting by eliminating weak points. - Updated `USAGE.md` to include new command examples for the detailed mode. - Revised multiple documentation files to reflect recent changes and improvements in the pivot detection and visualization processes.
This commit is contained in:
parent
6d545eb231
commit
95d13b2cce
132
.cursor/plans/4-a1a89b85.plan.md
Normal file
132
.cursor/plans/4-a1a89b85.plan.md
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
<!-- a1a89b85-9ffb-45cf-b4ce-7e9456990b83 3ae86d9a-d078-42f4-a070-42c33828036f -->
|
||||||
|
# 拟合线迭代离群点移除优化
|
||||||
|
|
||||||
|
## 问题描述
|
||||||
|
|
||||||
|
当前的分段选择算法会选中一些"弱"枢轴点用于拟合:
|
||||||
|
|
||||||
|
- 上沿线:某个高点虽然是时间段内最高,但明显低于其他高点(如图中第二个点 5.8 元)
|
||||||
|
- 这些点会拉低/拉高拟合线,导致与主观判断不符
|
||||||
|
|
||||||
|
## 解决方案:迭代离群点移除
|
||||||
|
|
||||||
|
### 核心逻辑
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 初始拟合:用所有枢轴点做线性回归
|
||||||
|
2. 计算残差:每个点到拟合线的偏差
|
||||||
|
3. 识别离群点:
|
||||||
|
- 上沿线:价格明显低于拟合线的点 = 弱高点
|
||||||
|
- 下沿线:价格明显高于拟合线的点 = 弱低点
|
||||||
|
4. 移除最差的离群点
|
||||||
|
5. 重新拟合
|
||||||
|
6. 重复直到收敛
|
||||||
|
```
|
||||||
|
|
||||||
|
### 算法流程图
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[输入枢轴点] --> B[初始线性回归]
|
||||||
|
B --> C[计算残差]
|
||||||
|
C --> D{存在离群点?}
|
||||||
|
D -->|是| E[移除最大离群点]
|
||||||
|
E --> F{剩余点 >= 3?}
|
||||||
|
F -->|是| G{迭代次数 < 3?}
|
||||||
|
G -->|是| B
|
||||||
|
G -->|否| H[返回当前拟合]
|
||||||
|
F -->|否| H
|
||||||
|
D -->|否| H
|
||||||
|
```
|
||||||
|
|
||||||
|
## 代码修改
|
||||||
|
|
||||||
|
### 文件: [src/converging_triangle.py](src/converging_triangle.py)
|
||||||
|
|
||||||
|
重写 `fit_pivot_line` 函数(第 230-350 行):
|
||||||
|
|
||||||
|
```python
|
||||||
|
def fit_pivot_line(
|
||||||
|
pivot_indices: np.ndarray,
|
||||||
|
pivot_values: np.ndarray,
|
||||||
|
mode: str = "upper",
|
||||||
|
min_points: int = 2,
|
||||||
|
outlier_threshold: float = 1.5, # 新增:离群点阈值(标准差倍数)
|
||||||
|
max_iterations: int = 3, # 新增:最大迭代次数
|
||||||
|
) -> Tuple[float, float, np.ndarray]:
|
||||||
|
"""
|
||||||
|
迭代离群点移除的枢轴点拟合算法
|
||||||
|
|
||||||
|
策略:
|
||||||
|
1. 先用所有点做初始拟合
|
||||||
|
2. 识别并移除偏离拟合线的"弱"点
|
||||||
|
3. 迭代直到收敛
|
||||||
|
|
||||||
|
对于上沿线:移除价格明显低于拟合线的点
|
||||||
|
对于下沿线:移除价格明显高于拟合线的点
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
### 关键参数
|
||||||
|
|
||||||
|
| 参数 | 默认值 | 说明 |
|
||||||
|
|
||||||
|
|------|--------|------|
|
||||||
|
|
||||||
|
| `outlier_threshold` | 1.5 | 残差超过 1.5 倍标准差视为离群点 |
|
||||||
|
|
||||||
|
| `max_iterations` | 3 | 最多迭代 3 次,避免过度过滤 |
|
||||||
|
|
||||||
|
| `min_points` | 3 | 至少保留 3 个点用于拟合 |
|
||||||
|
|
||||||
|
### 离群点判定逻辑
|
||||||
|
|
||||||
|
**上沿线(upper)**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 残差 = 拟合值 - 实际值
|
||||||
|
# 正残差表示点在拟合线下方(弱高点)
|
||||||
|
residuals = fitted_values - actual_values
|
||||||
|
outliers = residuals > threshold # 弱高点
|
||||||
|
```
|
||||||
|
|
||||||
|
**下沿线(lower)**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 残差 = 实际值 - 拟合值
|
||||||
|
# 正残差表示点在拟合线上方(弱低点)
|
||||||
|
residuals = actual_values - fitted_values
|
||||||
|
outliers = residuals > threshold # 弱低点
|
||||||
|
```
|
||||||
|
|
||||||
|
### 预期效果
|
||||||
|
|
||||||
|
以图中 SZ300278 为例:
|
||||||
|
|
||||||
|
- 第二个点(5.8元)明显低于拟合线
|
||||||
|
- 在第一次迭代后会被识别为离群点并移除
|
||||||
|
- 最终拟合线只使用剩余 3 个更有代表性的高点
|
||||||
|
|
||||||
|
## 测试计划
|
||||||
|
|
||||||
|
1. 使用 SZ300278 验证修复效果
|
||||||
|
2. 对比修改前后的图表
|
||||||
|
3. 确保不会过度过滤正常的枢轴点
|
||||||
|
|
||||||
|
## 文档更新
|
||||||
|
|
||||||
|
更新 [docs/枢轴点分段选择算法详解.md](docs/枢轴点分段选择算法详解.md),添加迭代离群点移除的说明。
|
||||||
|
|
||||||
|
### To-dos
|
||||||
|
|
||||||
|
- [x] 在ConvergingTriangleResult中增加detection_mode等3个新字段
|
||||||
|
- [x] 实现pivots_fractal_hybrid()函数
|
||||||
|
- [x] 修改detect_converging_triangle()支持real_time_mode参数
|
||||||
|
- [x] 修改detect_converging_triangle_batch()传递实时模式参数
|
||||||
|
- [x] 在triangle_config.py中添加REALTIME_MODE和FLEXIBLE_ZONE配置
|
||||||
|
- [x] 更新run_converging_triangle.py导入和使用实时配置
|
||||||
|
- [x] 创建test_realtime_mode.py对比测试脚本
|
||||||
|
- [x] 更新README.md和创建实时模式使用指南
|
||||||
|
- [ ] 重写 fit_pivot_line 函数,实现迭代离群点移除算法
|
||||||
|
- [ ] 测试 SZ300278,验证修复效果
|
||||||
|
- [ ] 更新算法文档,添加迭代离群点移除说明
|
||||||
6
USAGE.md
6
USAGE.md
@ -37,6 +37,12 @@ python scripts/pipeline_converging_triangle.py
|
|||||||
# 指定日期
|
# 指定日期
|
||||||
python scripts/pipeline_converging_triangle.py --date 20260120
|
python scripts/pipeline_converging_triangle.py --date 20260120
|
||||||
|
|
||||||
|
# 生成详情模式图片(显示所有枢轴点和拟合点)
|
||||||
|
python scripts/pipeline_converging_triangle.py --show-details
|
||||||
|
|
||||||
|
# 组合使用
|
||||||
|
python scripts/pipeline_converging_triangle.py --date 20260120 --show-details
|
||||||
|
|
||||||
# 跳过检测(仅生成报告与图表)
|
# 跳过检测(仅生成报告与图表)
|
||||||
python scripts/pipeline_converging_triangle.py --skip-detection
|
python scripts/pipeline_converging_triangle.py --skip-detection
|
||||||
```
|
```
|
||||||
|
|||||||
@ -1,16 +1,61 @@
|
|||||||
|
## 问题1: 末端枢轴点未识别
|
||||||
|
|
||||||

|

|
||||||
下沿线,明显不对。
|
|
||||||
|
**现象**: 图表最右边明显的低点/高点没有被标记为枢轴点
|
||||||
|
|
||||||
|
**根因**:
|
||||||
|
1. 数据中有空值(NaN)导致比较失效
|
||||||
|
2. 只检测窗口前235天的枢轴点,最后5天被忽略
|
||||||
|
3. 绘图时丢弃了末端的"候选枢轴点"
|
||||||
|
|
||||||
|
**解决**:
|
||||||
|
1. 用 `nanmin/nanmax` 替代 `min/max`,自动跳过空值
|
||||||
|
2. 把"灵活区域"从5天扩大到15天,覆盖更多末端数据
|
||||||
|
3. 对称短窗口:右边只有N天时,左边也只看N天(而不是固定15天)
|
||||||
|
4. 绘图时把"确认枢轴点"和"候选枢轴点"合并显示
|
||||||
|
|
||||||
|
详见 [枢轴点检测与可视化修复](../docs/2026-01-26_枢轴点检测与可视化修复.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 问题2: 拟合点选择不合理
|
||||||
|
|
||||||

|

|
||||||
拟合线的时候,有些点应该去掉,跟主观判断不对齐,比如图中第二个点
|
|
||||||
|
**现象**: 某些"弱"枢轴点(如5.8元的低高点)被用于拟合,拉偏趋势线
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
**解决**: 迭代离群点移除算法
|
||||||
|
1. 先用所有点画一条拟合线
|
||||||
|
2. 找出离拟合线太远的"异常点"
|
||||||
|
3. 去掉最差的那个点,重新画线
|
||||||
|
4. 重复2-3次,直到没有异常点
|
||||||
|
|
||||||
### 强度分:是否符合三角形的形态 + 突破强度
|
详见 [枢轴点拟合算法详解](../docs/枢轴点分段选择算法详解.md)
|
||||||
|
|
||||||
108个个股按照分数排序
|
---
|
||||||
|
|
||||||
可调参数、速度快
|
## 问题3: 非收敛形态误判
|
||||||
后续回测后继续调优
|
|
||||||
|
|
||||||
历史曲线的每个点,都需要计算强度分。
|
**现象**: "上升三角形"(上沿水平)被误判为"收敛三角形"
|
||||||
|

|
||||||
|
**解决**: 收紧斜率限制
|
||||||
|
- 上沿必须向下(或至少水平),不能向上
|
||||||
|
- 下沿必须向上(或至少水平),不能向下
|
||||||
|
- 这样就能过滤掉"上升三角形"和"下降通道"等非收敛形态
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 待办: 突破强度评分
|
||||||
|
|
||||||
|
| 分量 | 权重 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 价格突破 | 60% | 突破幅度 |
|
||||||
|
| 收敛程度 | 25% | 蓄势充分度 |
|
||||||
|
| 成交量 | 15% | 放量确认 |
|
||||||
|
|
||||||
|
- 枢轴点和拟合线距离。
|
||||||
|
|
||||||
|
后续回测调优。
|
||||||
|
|||||||
BIN
discuss/images/2026-01-26-18-36-50.png
Normal file
BIN
discuss/images/2026-01-26-18-36-50.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 338 KiB |
@ -2,15 +2,292 @@
|
|||||||
|
|
||||||
**日期**: 2026-01-26
|
**日期**: 2026-01-26
|
||||||
**功能**: 图表可视化改进 - 简洁模式与详细模式
|
**功能**: 图表可视化改进 - 简洁模式与详细模式
|
||||||
|
**版本**: v2.0 (移除分段竖线 + 流水线支持)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📋 功能概述
|
## 📋 功能概述
|
||||||
|
|
||||||
为了满足不同使用场景的需求,我们为收敛三角形图表增加了**简洁模式**和**详细模式**两种显示方式:
|
为了满足不同使用场景的需求,我们为收敛三角形图表增加了**简洁模式**和**详细模式**两种显示方式:
|
||||||
|
|
||||||
- **简洁模式(默认)**: 仅显示收盘价、上沿线、下沿线,图表清爽易读
|
- **简洁模式(默认)**: 仅显示收盘价、上沿线、下沿线,图表清爽易读
|
||||||
- **详细模式**: 显示所有枢轴点、拟合点、分段线等调试信息,便于理解算法
|
- **详细模式**: 显示所有枢轴点、拟合点,便于理解算法
|
||||||
|
|
||||||
|
**v2.0 更新**: 移除分段竖线(算法已改为迭代离群点移除)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 使用场景
|
||||||
|
|
||||||
|
### 简洁模式
|
||||||
|
- ✅ 日常使用和实盘选股
|
||||||
|
- ✅ 快速查看三角形形态
|
||||||
|
- ✅ 对外展示和报告
|
||||||
|
- ✅ 减少视觉干扰
|
||||||
|
|
||||||
|
### 详细模式
|
||||||
|
- ✅ 算法调试和验证
|
||||||
|
- ✅ 理解枢轴点识别逻辑
|
||||||
|
- ✅ 验证迭代拟合算法
|
||||||
|
- ✅ 学习和教学用途
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 两种模式对比
|
||||||
|
|
||||||
|
| 显示元素 | 简洁模式 | 详细模式 | 说明 |
|
||||||
|
|---------|---------|---------|------|
|
||||||
|
| **收盘价线** | ✅ | ✅ | 黑色实线 |
|
||||||
|
| **上沿线** | ✅ | ✅ | 红色虚线 |
|
||||||
|
| **下沿线** | ✅ | ✅ | 绿色虚线 |
|
||||||
|
| **所有高点枢轴点** | ❌ | ✅ | 浅红色小实心圆 |
|
||||||
|
| **所有低点枢轴点** | ❌ | ✅ | 浅绿色小实心圆 |
|
||||||
|
| **上沿拟合点** | ❌ | ✅ | 深红色大空心圆(迭代算法选出) |
|
||||||
|
| **下沿拟合点** | ❌ | ✅ | 深绿色大空心圆(迭代算法选出) |
|
||||||
|
| **分段竖线** | ❌ | ❌ | v2.0已移除 |
|
||||||
|
| **输出文件名** | `YYYYMMDD_代码_名称.png` | `YYYYMMDD_代码_名称_detail.png` |
|
||||||
|
|
||||||
|
**文件名说明**:
|
||||||
|
- 简洁模式文件不带后缀
|
||||||
|
- 详细模式文件带 `_detail` 后缀
|
||||||
|
- 两种模式可以同时保留,方便对比查看
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ 如何启用
|
||||||
|
|
||||||
|
### 方法1: 流水线脚本(推荐)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 简洁模式(默认)
|
||||||
|
python scripts/pipeline_converging_triangle.py
|
||||||
|
|
||||||
|
# 详细模式
|
||||||
|
python scripts/pipeline_converging_triangle.py --show-details
|
||||||
|
|
||||||
|
# 组合使用
|
||||||
|
python scripts/pipeline_converging_triangle.py --date 20260120 --show-details
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方法2: 单独绘图脚本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 简洁模式(默认)
|
||||||
|
python scripts/plot_converging_triangles.py
|
||||||
|
|
||||||
|
# 详细模式
|
||||||
|
python scripts/plot_converging_triangles.py --show-details
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方法3: 配置文件(默认设置)
|
||||||
|
|
||||||
|
编辑 `scripts/triangle_config.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 图表详细模式(显示枢轴点等调试信息)
|
||||||
|
SHOW_CHART_DETAILS = False # False=简洁模式(默认),True=详细模式
|
||||||
|
```
|
||||||
|
|
||||||
|
**优先级**: 命令行参数 > 配置文件
|
||||||
|
|
||||||
|
**智能清理**:
|
||||||
|
- 简洁模式运行时,只清理简洁模式的旧图片
|
||||||
|
- 详细模式运行时,只清理详细模式的旧图片
|
||||||
|
- 两种模式互不影响,可以共存
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📈 详细模式显示元素说明
|
||||||
|
|
||||||
|
### 1. 所有枢轴点(小实心圆)
|
||||||
|
|
||||||
|
**作用**: 显示算法识别的所有局部高点和低点
|
||||||
|
|
||||||
|
- **高点枢轴点**: 浅红色,size=50,alpha=0.4
|
||||||
|
- **低点枢轴点**: 浅绿色,size=50,alpha=0.4
|
||||||
|
|
||||||
|
**标签**: `所有高点枢轴点(N)` / `所有低点枢轴点(N)`
|
||||||
|
|
||||||
|
### 2. 拟合点(大空心圆)
|
||||||
|
|
||||||
|
**作用**: 显示迭代离群点移除算法最终选出的关键点
|
||||||
|
|
||||||
|
- **上沿拟合点**: 深红色空心圆,size=120,linewidth=2.5
|
||||||
|
- **下沿拟合点**: 深绿色空心圆,size=120,linewidth=2.5
|
||||||
|
|
||||||
|
**标签**: `上沿拟合点(N)` / `下沿拟合点(N)`
|
||||||
|
|
||||||
|
**说明**:
|
||||||
|
- 使用迭代离群点移除算法选出代表性强的枢轴点
|
||||||
|
- 自动过滤"弱"枢轴点(偏离趋势线过大的点)
|
||||||
|
- 详见 [枢轴点拟合算法详解](./枢轴点分段选择算法详解.md)
|
||||||
|
|
||||||
|
### 3. ~~分段竖线~~(已移除)
|
||||||
|
|
||||||
|
**v2.0 更新**: 由于算法改为迭代离群点移除,不再使用分段策略,因此移除了分段竖线。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 图表解读示例
|
||||||
|
|
||||||
|
### 简洁模式(默认)
|
||||||
|
|
||||||
|
```
|
||||||
|
图表内容:
|
||||||
|
├─ 黑色实线:收盘价
|
||||||
|
├─ 红色虚线:上沿线(向下)
|
||||||
|
└─ 绿色虚线:下沿线(向上)
|
||||||
|
|
||||||
|
图例:
|
||||||
|
- 收盘价
|
||||||
|
- 上沿
|
||||||
|
- 下沿
|
||||||
|
```
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
- 清晰直观
|
||||||
|
- 无视觉干扰
|
||||||
|
- 适合日常使用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 详细模式(`--show-details`)
|
||||||
|
|
||||||
|
```
|
||||||
|
图表内容:
|
||||||
|
├─ 黑色实线:收盘价
|
||||||
|
├─ 红色虚线:上沿线
|
||||||
|
├─ 绿色虚线:下沿线
|
||||||
|
├─ 浅红小圆:所有高点枢轴点
|
||||||
|
├─ 浅绿小圆:所有低点枢轴点
|
||||||
|
├─ 深红大圆:上沿拟合点(迭代算法选出)
|
||||||
|
└─ 深绿大圆:下沿拟合点(迭代算法选出)
|
||||||
|
|
||||||
|
图例:
|
||||||
|
- 收盘价
|
||||||
|
- 上沿
|
||||||
|
- 下沿
|
||||||
|
- 所有高点枢轴点(N)
|
||||||
|
- 所有低点枢轴点(N)
|
||||||
|
- 上沿拟合点(N)
|
||||||
|
- 下沿拟合点(N)
|
||||||
|
```
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
- 完整展示算法逻辑
|
||||||
|
- 便于验证和调试
|
||||||
|
- 有助于理解原理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 视觉层次设计
|
||||||
|
|
||||||
|
图表元素的 Z-order(从后到前):
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 网格线(alpha=0.3)
|
||||||
|
2. 价格曲线(黑色,zorder=默认)
|
||||||
|
3. 所有枢轴点(浅色小圆,alpha=0.4,zorder=4)
|
||||||
|
4. 拟合点(深色大圆,zorder=5)
|
||||||
|
5. 趋势线(红/绿虚线,zorder=默认)
|
||||||
|
```
|
||||||
|
|
||||||
|
**设计原则**:
|
||||||
|
- 详细信息放在后层(不遮挡主要信息)
|
||||||
|
- 关键信息(拟合点)突出显示(zorder高)
|
||||||
|
- 使用透明度(alpha)区分重要性
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 代码实现要点
|
||||||
|
|
||||||
|
### 1. 流水线参数传递
|
||||||
|
|
||||||
|
```python
|
||||||
|
# pipeline_converging_triangle.py
|
||||||
|
parser.add_argument("--show-details", action="store_true")
|
||||||
|
|
||||||
|
# 传递给绘图脚本
|
||||||
|
if args.show_details:
|
||||||
|
cmd_args.append("--show-details")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 绘图脚本参数
|
||||||
|
|
||||||
|
```python
|
||||||
|
# plot_converging_triangles.py
|
||||||
|
parser.add_argument(
|
||||||
|
"--show-details",
|
||||||
|
action="store_true",
|
||||||
|
help="显示详细调试信息(枢轴点、拟合点等)",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 条件渲染
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 详细模式:显示所有枢轴点和拟合点
|
||||||
|
if show_details:
|
||||||
|
# 绘制所有枢轴点
|
||||||
|
ax1.scatter(ph_display_idx, high_win[ph_idx], ...)
|
||||||
|
|
||||||
|
# 绘制拟合点
|
||||||
|
ax1.scatter(selected_ph_display, high_win[selected_ph_pos], ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 测试验证
|
||||||
|
|
||||||
|
### 测试用例1: 流水线简洁模式(默认)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/pipeline_converging_triangle.py
|
||||||
|
```
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- ✅ 控制台显示:"图表模式: 简洁模式(仅显示价格和趋势线)"
|
||||||
|
- ✅ 图表只显示收盘价、上沿、下沿
|
||||||
|
- ✅ 输出文件: `20260120_SZ300278_华昌达.png`
|
||||||
|
|
||||||
|
### 测试用例2: 流水线详细模式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/pipeline_converging_triangle.py --show-details
|
||||||
|
```
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
- ✅ 控制台显示:"图表模式: 详情模式(显示所有枢轴点)"
|
||||||
|
- ✅ 图表显示所有枢轴点和拟合点
|
||||||
|
- ✅ 输出文件: `20260120_SZ300278_华昌达_detail.png`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 相关文档
|
||||||
|
|
||||||
|
- [README.md](../README.md) - 项目概述
|
||||||
|
- [USAGE.md](../USAGE.md) - 使用指南
|
||||||
|
- [枢轴点拟合算法详解.md](./枢轴点分段选择算法详解.md) - 迭代离群点移除算法 ⭐
|
||||||
|
- [枢轴点检测原理.md](./枢轴点检测原理.md) - 枢轴点算法说明
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 总结
|
||||||
|
|
||||||
|
通过添加**简洁模式**和**详细模式**,图表功能更加灵活:
|
||||||
|
|
||||||
|
- **对于日常用户**: 简洁清爽的图表,快速查看形态
|
||||||
|
- **对于研究者**: 完整的算法细节,深入理解逻辑
|
||||||
|
- **对于开发者**: 便于调试和验证算法正确性
|
||||||
|
|
||||||
|
**v2.0 改进**:
|
||||||
|
- ✅ 移除了不再适用的分段竖线
|
||||||
|
- ✅ 流水线脚本支持详情模式
|
||||||
|
- ✅ 更符合新的迭代拟合算法
|
||||||
|
|
||||||
|
这个功能提升了项目的易用性和专业性!🎯
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
157
docs/2026-01-26_枢轴点检测与可视化修复.md
Normal file
157
docs/2026-01-26_枢轴点检测与可视化修复.md
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
# 枢轴点检测与可视化修复
|
||||||
|
|
||||||
|
**日期**: 2026-01-26
|
||||||
|
**版本**: v1.0
|
||||||
|
|
||||||
|
## 问题概述
|
||||||
|
|
||||||
|
收敛三角形检测系统存在多个问题,导致:
|
||||||
|
1. 非收敛形态(如上升三角形)被误判
|
||||||
|
2. 绘图与检测结果不一致
|
||||||
|
3. 窗口末端枢轴点无法识别
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 问题1: 斜率约束过于宽松
|
||||||
|
|
||||||
|
### 现象
|
||||||
|
"上升三角形"(上沿水平+下沿向上)被误判为"收敛三角形"。
|
||||||
|
|
||||||
|
### 原因
|
||||||
|
```python
|
||||||
|
upper_slope_max=0.10, # 允许上沿向上
|
||||||
|
lower_slope_min=-0.10, # 允许下沿向下
|
||||||
|
```
|
||||||
|
|
||||||
|
### 解决方案
|
||||||
|
**文件**: `scripts/triangle_config.py`
|
||||||
|
```python
|
||||||
|
upper_slope_max=0, # 上沿必须向下或水平(≤0)
|
||||||
|
lower_slope_min=0, # 下沿必须向上或水平(≥0)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 问题2: 绘图与检测枢轴点不一致
|
||||||
|
|
||||||
|
### 现象
|
||||||
|
图表显示的枢轴点和趋势线与实际检测结果不同。
|
||||||
|
|
||||||
|
### 原因
|
||||||
|
- 检测代码使用 `pivots_fractal_hybrid`(实时模式)
|
||||||
|
- 绘图代码使用 `pivots_fractal`(标准模式)
|
||||||
|
|
||||||
|
### 解决方案
|
||||||
|
**文件**: `scripts/plot_converging_triangles.py`
|
||||||
|
```python
|
||||||
|
from triangle_config import REALTIME_MODE, FLEXIBLE_ZONE
|
||||||
|
|
||||||
|
if REALTIME_MODE:
|
||||||
|
confirmed_ph, confirmed_pl, candidate_ph, candidate_pl = pivots_fractal_hybrid(
|
||||||
|
high_win, low_win, k=params.pivot_k, flexible_zone=FLEXIBLE_ZONE
|
||||||
|
)
|
||||||
|
# 合并确认枢轴点和候选枢轴点
|
||||||
|
ph_idx = np.concatenate([confirmed_ph, candidate_ph]) if len(candidate_ph) > 0 else confirmed_ph
|
||||||
|
pl_idx = np.concatenate([confirmed_pl, candidate_pl]) if len(candidate_pl) > 0 else confirmed_pl
|
||||||
|
ph_idx = np.sort(ph_idx)
|
||||||
|
pl_idx = np.sort(pl_idx)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 问题3: 窗口末端枢轴点识别失败
|
||||||
|
|
||||||
|
### 现象
|
||||||
|
图表最右边明显的低点/高点没有被识别为枢轴点。
|
||||||
|
|
||||||
|
### 原因
|
||||||
|
`FLEXIBLE_ZONE=5` 太小,末端枢轴点超出灵活区域范围。
|
||||||
|
|
||||||
|
### 解决方案
|
||||||
|
**文件**: `scripts/triangle_config.py`
|
||||||
|
```python
|
||||||
|
FLEXIBLE_ZONE = 15 # 与 pivot_k 一致,确保末端枢轴点能被识别
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 问题4: NaN值导致比较失败
|
||||||
|
|
||||||
|
### 现象
|
||||||
|
包含 NaN 值的数据窗口中,枢轴点检测完全失效。
|
||||||
|
|
||||||
|
### 原因
|
||||||
|
```python
|
||||||
|
np.min([30.0, np.nan, 29.0]) # 返回 nan
|
||||||
|
30.0 == nan # 永远为 False
|
||||||
|
```
|
||||||
|
|
||||||
|
### 解决方案
|
||||||
|
**文件**: `src/converging_triangle.py`
|
||||||
|
```python
|
||||||
|
# 使用 nanmin/nanmax 忽略 NaN 值
|
||||||
|
if not np.isnan(high[i]) and high[i] == np.nanmax(high[i - k : i + k + 1]):
|
||||||
|
confirmed_ph.append(i)
|
||||||
|
if not np.isnan(low[i]) and low[i] == np.nanmin(low[i - k : i + k + 1]):
|
||||||
|
confirmed_pl.append(i)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 问题5: 候选枢轴点未合并
|
||||||
|
|
||||||
|
### 现象
|
||||||
|
`pivots_fractal_hybrid` 返回的候选枢轴点在绘图时被丢弃。
|
||||||
|
|
||||||
|
### 原因
|
||||||
|
```python
|
||||||
|
ph_idx, pl_idx, _, _ = pivots_fractal_hybrid(...) # 候选点被忽略
|
||||||
|
```
|
||||||
|
|
||||||
|
### 解决方案
|
||||||
|
见问题2的解决方案,合并 confirmed 和 candidate。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 问题6: 对称窗口逻辑优化
|
||||||
|
|
||||||
|
### 现象
|
||||||
|
窗口末端的局部低点/高点,因右侧数据不足无法识别。
|
||||||
|
|
||||||
|
### 解决方案
|
||||||
|
**文件**: `src/converging_triangle.py`
|
||||||
|
```python
|
||||||
|
# 候选枢轴点使用对称的短窗口
|
||||||
|
for i in range(max(k, n - flexible_zone), n):
|
||||||
|
right_avail = n - 1 - i
|
||||||
|
left_look = min(k, max(right_avail + 1, 3)) # 至少看3天
|
||||||
|
left_start = max(0, i - left_look)
|
||||||
|
right_end = min(n, i + right_avail + 1)
|
||||||
|
|
||||||
|
if high[i] == np.nanmax(high[left_start : right_end]):
|
||||||
|
candidate_ph.append(i)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 修改文件清单
|
||||||
|
|
||||||
|
| 文件 | 修改内容 |
|
||||||
|
|------|----------|
|
||||||
|
| `scripts/triangle_config.py` | 斜率约束(`upper_slope_max=0`, `lower_slope_min=0`)、`FLEXIBLE_ZONE=15` |
|
||||||
|
| `scripts/plot_converging_triangles.py` | 实时模式枢轴点检测、候选点合并、排序 |
|
||||||
|
| `src/converging_triangle.py` | NaN处理(`nanmin/nanmax`)、对称窗口逻辑 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 效果验证
|
||||||
|
|
||||||
|
修复前后对比:
|
||||||
|
|
||||||
|
| 指标 | 修复前 | 修复后 |
|
||||||
|
|------|--------|--------|
|
||||||
|
| SZ300892 低点枢轴点 | 4个 | 7个 |
|
||||||
|
| 末端低点识别 | ❌ | ✅ |
|
||||||
|
| 图表/检测一致性 | ❌ | ✅ |
|
||||||
|
| 非收敛形态过滤 | ❌ | ✅ |
|
||||||
|
|
||||||
214
docs/2026-01-26_流水线详情模式支持.md
Normal file
214
docs/2026-01-26_流水线详情模式支持.md
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
# 流水线脚本详情模式支持
|
||||||
|
|
||||||
|
**日期**: 2026-01-26
|
||||||
|
**功能**: 为 `pipeline_converging_triangle.py` 添加 `--show-details` 参数
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 需求背景
|
||||||
|
|
||||||
|
用户希望在使用流水线脚本 `pipeline_converging_triangle.py` 时,能够通过参数控制是否生成详情模式的图片,而不是默认生成。
|
||||||
|
|
||||||
|
**之前**:流水线脚本没有详情模式参数,只能生成简洁模式图片。
|
||||||
|
|
||||||
|
**现在**:流水线脚本支持 `--show-details` 参数,可以按需生成详情模式图片。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 实现内容
|
||||||
|
|
||||||
|
### 1. 添加命令行参数
|
||||||
|
|
||||||
|
在 `pipeline_converging_triangle.py` 中添加:
|
||||||
|
|
||||||
|
```python
|
||||||
|
parser.add_argument(
|
||||||
|
"--show-details",
|
||||||
|
action="store_true",
|
||||||
|
help="生成详情模式图片(显示所有枢轴点和拟合点)",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 参数传递逻辑
|
||||||
|
|
||||||
|
将参数传递给绘图脚本:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 步骤 3: 绘制图表
|
||||||
|
cmd_args = [sys.argv[0]]
|
||||||
|
if args.date:
|
||||||
|
cmd_args.extend(["--date", str(args.date)])
|
||||||
|
if args.show_details:
|
||||||
|
cmd_args.append("--show-details")
|
||||||
|
|
||||||
|
sys.argv = cmd_args
|
||||||
|
run_plot()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 控制台提示信息
|
||||||
|
|
||||||
|
在流水线开始时显示当前模式:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if args.show_details:
|
||||||
|
print(f"图表模式: 详情模式(显示所有枢轴点)")
|
||||||
|
else:
|
||||||
|
print(f"图表模式: 简洁模式(仅显示价格和趋势线)")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ 使用方法
|
||||||
|
|
||||||
|
### 基本用法
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 简洁模式(默认)- 不生成详情图片
|
||||||
|
python scripts/pipeline_converging_triangle.py
|
||||||
|
|
||||||
|
# 详情模式 - 生成详情图片
|
||||||
|
python scripts/pipeline_converging_triangle.py --show-details
|
||||||
|
```
|
||||||
|
|
||||||
|
### 组合参数
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 指定日期 + 详情模式
|
||||||
|
python scripts/pipeline_converging_triangle.py --date 20260120 --show-details
|
||||||
|
|
||||||
|
# 跳过检测 + 详情模式
|
||||||
|
python scripts/pipeline_converging_triangle.py --skip-detection --show-details
|
||||||
|
|
||||||
|
# 跳过检测和报告,只生成详情图表
|
||||||
|
python scripts/pipeline_converging_triangle.py --skip-detection --skip-report --show-details
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 输出对比
|
||||||
|
|
||||||
|
### 简洁模式(默认)
|
||||||
|
|
||||||
|
```
|
||||||
|
输出文件: outputs/converging_triangles/charts/20260120_SZ300278_华昌达.png
|
||||||
|
文件内容: 收盘价 + 上沿线 + 下沿线
|
||||||
|
```
|
||||||
|
|
||||||
|
### 详情模式(--show-details)
|
||||||
|
|
||||||
|
```
|
||||||
|
输出文件: outputs/converging_triangles/charts/20260120_SZ300278_华昌达_detail.png
|
||||||
|
文件内容: 收盘价 + 上沿线 + 下沿线 + 所有枢轴点 + 拟合点
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**:两种模式的文件可以共存,互不覆盖。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 参数优先级
|
||||||
|
|
||||||
|
流水线脚本的参数传递链:
|
||||||
|
|
||||||
|
```
|
||||||
|
用户命令行
|
||||||
|
└─ pipeline_converging_triangle.py
|
||||||
|
├─ --show-details (可选)
|
||||||
|
└─ 传递给 plot_converging_triangles.py
|
||||||
|
├─ --show-details (如果用户指定)
|
||||||
|
└─ 调用 plot_triangle(show_details=True/False)
|
||||||
|
```
|
||||||
|
|
||||||
|
**优先级规则**:
|
||||||
|
1. 流水线脚本的 `--show-details` 参数
|
||||||
|
2. 传递到绘图脚本
|
||||||
|
3. 绘图脚本的配置文件 `SHOW_CHART_DETAILS`(命令行参数优先)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 测试验证
|
||||||
|
|
||||||
|
### 测试1:帮助信息
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/pipeline_converging_triangle.py --help
|
||||||
|
```
|
||||||
|
|
||||||
|
**预期**:显示 `--show-details` 参数说明
|
||||||
|
|
||||||
|
### 测试2:简洁模式(默认)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/pipeline_converging_triangle.py --skip-detection --skip-report
|
||||||
|
```
|
||||||
|
|
||||||
|
**预期**:
|
||||||
|
- ✅ 控制台显示:"图表模式: 简洁模式(仅显示价格和趋势线)"
|
||||||
|
- ✅ 生成不带 `_detail` 后缀的图片
|
||||||
|
|
||||||
|
### 测试3:详情模式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/pipeline_converging_triangle.py --skip-detection --skip-report --show-details
|
||||||
|
```
|
||||||
|
|
||||||
|
**预期**:
|
||||||
|
- ✅ 控制台显示:"图表模式: 详情模式(显示所有枢轴点)"
|
||||||
|
- ✅ 控制台显示:"详细模式: 开启 (--show-details)"
|
||||||
|
- ✅ 生成带 `_detail` 后缀的图片
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 修改文件列表
|
||||||
|
|
||||||
|
1. **scripts/pipeline_converging_triangle.py**
|
||||||
|
- 添加 `--show-details` 参数
|
||||||
|
- 添加模式提示信息
|
||||||
|
- 参数传递逻辑
|
||||||
|
|
||||||
|
2. **USAGE.md**
|
||||||
|
- 更新流水线脚本的使用说明
|
||||||
|
- 添加 `--show-details` 参数示例
|
||||||
|
|
||||||
|
3. **docs/2026-01-26_图表详细模式功能.md**
|
||||||
|
- 更新文档,添加流水线使用方法
|
||||||
|
- 标注为 v2.0 版本
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎉 效果总结
|
||||||
|
|
||||||
|
### 用户体验改进
|
||||||
|
|
||||||
|
**之前**:
|
||||||
|
```bash
|
||||||
|
# 想要详情图片
|
||||||
|
python scripts/pipeline_converging_triangle.py # ❌ 只能生成简洁模式
|
||||||
|
# 需要单独运行绘图脚本
|
||||||
|
python scripts/plot_converging_triangles.py --show-details
|
||||||
|
```
|
||||||
|
|
||||||
|
**现在**:
|
||||||
|
```bash
|
||||||
|
# 一键生成详情图片
|
||||||
|
python scripts/pipeline_converging_triangle.py --show-details # ✅ 直接搞定
|
||||||
|
```
|
||||||
|
|
||||||
|
### 优势
|
||||||
|
|
||||||
|
1. **一致性**:流水线和绘图脚本参数统一
|
||||||
|
2. **便捷性**:无需分步操作
|
||||||
|
3. **灵活性**:默认简洁,按需详细
|
||||||
|
4. **清晰性**:控制台明确提示当前模式
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 相关文档
|
||||||
|
|
||||||
|
- [USAGE.md](../USAGE.md) - 使用指南
|
||||||
|
- [2026-01-26_图表详细模式功能.md](./2026-01-26_图表详细模式功能.md) - 详情模式完整说明
|
||||||
|
- [枢轴点拟合算法详解.md](./枢轴点分段选择算法详解.md) - 拟合算法原理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**总结**:通过添加 `--show-details` 参数,流水线脚本现在完全支持详情模式,用户可以更方便地按需生成详细的调试图表。✅
|
||||||
|
|
||||||
117
docs/拟合线迭代离群点移除优化.md
Normal file
117
docs/拟合线迭代离群点移除优化.md
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
# 拟合线迭代离群点移除优化
|
||||||
|
|
||||||
|
## 问题描述
|
||||||
|
|
||||||
|
当前的分段选择算法会选中一些"弱"枢轴点用于拟合:
|
||||||
|
|
||||||
|
- 上沿线:某个高点虽然是时间段内最高,但明显低于其他高点(如图中第二个点 5.8 元)
|
||||||
|
- 这些点会拉低/拉高拟合线,导致与主观判断不符
|
||||||
|
|
||||||
|
## 解决方案:迭代离群点移除
|
||||||
|
|
||||||
|
### 核心逻辑
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 初始拟合:用所有枢轴点做线性回归
|
||||||
|
2. 计算残差:每个点到拟合线的偏差
|
||||||
|
3. 识别离群点:
|
||||||
|
- 上沿线:价格明显低于拟合线的点 = 弱高点
|
||||||
|
- 下沿线:价格明显高于拟合线的点 = 弱低点
|
||||||
|
4. 移除最差的离群点
|
||||||
|
5. 重新拟合
|
||||||
|
6. 重复直到收敛
|
||||||
|
```
|
||||||
|
|
||||||
|
### 算法流程图
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[输入枢轴点] --> B[初始线性回归]
|
||||||
|
B --> C[计算残差]
|
||||||
|
C --> D{存在离群点?}
|
||||||
|
D -->|是| E[移除最大离群点]
|
||||||
|
E --> F{剩余点 >= 3?}
|
||||||
|
F -->|是| G{迭代次数 < 3?}
|
||||||
|
G -->|是| B
|
||||||
|
G -->|否| H[返回当前拟合]
|
||||||
|
F -->|否| H
|
||||||
|
D -->|否| H
|
||||||
|
```
|
||||||
|
|
||||||
|
## 代码修改
|
||||||
|
|
||||||
|
### 文件: [src/converging_triangle.py](src/converging_triangle.py)
|
||||||
|
|
||||||
|
重写 `fit_pivot_line` 函数(第 230-350 行):
|
||||||
|
|
||||||
|
```python
|
||||||
|
def fit_pivot_line(
|
||||||
|
pivot_indices: np.ndarray,
|
||||||
|
pivot_values: np.ndarray,
|
||||||
|
mode: str = "upper",
|
||||||
|
min_points: int = 2,
|
||||||
|
outlier_threshold: float = 1.5, # 新增:离群点阈值(标准差倍数)
|
||||||
|
max_iterations: int = 3, # 新增:最大迭代次数
|
||||||
|
) -> Tuple[float, float, np.ndarray]:
|
||||||
|
"""
|
||||||
|
迭代离群点移除的枢轴点拟合算法
|
||||||
|
|
||||||
|
策略:
|
||||||
|
1. 先用所有点做初始拟合
|
||||||
|
2. 识别并移除偏离拟合线的"弱"点
|
||||||
|
3. 迭代直到收敛
|
||||||
|
|
||||||
|
对于上沿线:移除价格明显低于拟合线的点
|
||||||
|
对于下沿线:移除价格明显高于拟合线的点
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
### 关键参数
|
||||||
|
|
||||||
|
| 参数 | 默认值 | 说明 |
|
||||||
|
|
||||||
|
|------|--------|------|
|
||||||
|
|
||||||
|
| `outlier_threshold` | 1.5 | 残差超过 1.5 倍标准差视为离群点 |
|
||||||
|
|
||||||
|
| `max_iterations` | 3 | 最多迭代 3 次,避免过度过滤 |
|
||||||
|
|
||||||
|
| `min_points` | 3 | 至少保留 3 个点用于拟合 |
|
||||||
|
|
||||||
|
### 离群点判定逻辑
|
||||||
|
|
||||||
|
**上沿线(upper)**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 残差 = 拟合值 - 实际值
|
||||||
|
# 正残差表示点在拟合线下方(弱高点)
|
||||||
|
residuals = fitted_values - actual_values
|
||||||
|
outliers = residuals > threshold # 弱高点
|
||||||
|
```
|
||||||
|
|
||||||
|
**下沿线(lower)**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 残差 = 实际值 - 拟合值
|
||||||
|
# 正残差表示点在拟合线上方(弱低点)
|
||||||
|
residuals = actual_values - fitted_values
|
||||||
|
outliers = residuals > threshold # 弱低点
|
||||||
|
```
|
||||||
|
|
||||||
|
### 预期效果
|
||||||
|
|
||||||
|
以图中 SZ300278 为例:
|
||||||
|
|
||||||
|
- 第二个点(5.8元)明显低于拟合线
|
||||||
|
- 在第一次迭代后会被识别为离群点并移除
|
||||||
|
- 最终拟合线只使用剩余 3 个更有代表性的高点
|
||||||
|
|
||||||
|
## 测试计划
|
||||||
|
|
||||||
|
1. 使用 SZ300278 验证修复效果
|
||||||
|
2. 对比修改前后的图表
|
||||||
|
3. 确保不会过度过滤正常的枢轴点
|
||||||
|
|
||||||
|
## 文档更新
|
||||||
|
|
||||||
|
更新 [docs/枢轴点分段选择算法详解.md](docs/枢轴点分段选择算法详解.md),添加迭代离群点移除的说明。
|
||||||
@ -140,6 +140,12 @@
|
|||||||
- 文档更新说明
|
- 文档更新说明
|
||||||
- 项目结构整理
|
- 项目结构整理
|
||||||
|
|
||||||
|
7. **[2026-01-26_枢轴点检测与可视化修复.md](./2026-01-26_枢轴点检测与可视化修复.md)** 🔧
|
||||||
|
- 斜率约束收紧
|
||||||
|
- NaN值处理修复
|
||||||
|
- 候选枢轴点合并
|
||||||
|
- 对称窗口逻辑优化
|
||||||
|
|
||||||
### 实时模式实施
|
### 实时模式实施
|
||||||
|
|
||||||
7. **[方案4实施完成报告.md](./方案4实施完成报告.md)**
|
7. **[方案4实施完成报告.md](./方案4实施完成报告.md)**
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# 枢轴点分段选择算法详解
|
# 枢轴点拟合算法详解
|
||||||
|
|
||||||
**日期**: 2026-01-26
|
**日期**: 2026-01-26 (v2 更新)
|
||||||
**文件**: `src/converging_triangle.py` - `fit_pivot_line()` 函数
|
**文件**: `src/converging_triangle.py` - `fit_pivot_line()` 函数
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -8,9 +8,9 @@
|
|||||||
## 📋 目录
|
## 📋 目录
|
||||||
|
|
||||||
1. [算法概述](#算法概述)
|
1. [算法概述](#算法概述)
|
||||||
2. [为什么需要分段](#为什么需要分段)
|
2. [迭代离群点移除(当前算法)](#迭代离群点移除当前算法)
|
||||||
3. [分段算法详解](#分段算法详解)
|
3. [历史版本:分段选择算法](#历史版本分段选择算法)
|
||||||
4. [独立分段机制](#独立分段机制)
|
4. [独立处理机制](#独立处理机制)
|
||||||
5. [代码实现](#代码实现)
|
5. [代码实现](#代码实现)
|
||||||
6. [实际案例分析](#实际案例分析)
|
6. [实际案例分析](#实际案例分析)
|
||||||
7. [边界情况处理](#边界情况处理)
|
7. [边界情况处理](#边界情况处理)
|
||||||
@ -22,13 +22,131 @@
|
|||||||
|
|
||||||
### 核心思想
|
### 核心思想
|
||||||
|
|
||||||
对于识别出的所有枢轴点,我们不是简单地用所有点或仅用首尾两点来拟合趋势线,而是:
|
对于识别出的所有枢轴点,我们采用**迭代离群点移除**算法来拟合趋势线:
|
||||||
|
|
||||||
|
1. **初始拟合**:先用所有枢轴点做线性回归
|
||||||
|
2. **识别离群点**:计算残差,找出偏离拟合线的"弱"点
|
||||||
|
3. **移除离群点**:移除最差的离群点
|
||||||
|
4. **迭代优化**:重复步骤1-3直到收敛
|
||||||
|
|
||||||
|
### 设计目标
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 上沿线:移除价格明显低于拟合线的点(弱高点)
|
||||||
|
# 下沿线:移除价格明显高于拟合线的点(弱低点)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 迭代离群点移除(当前算法)
|
||||||
|
|
||||||
|
### 为什么需要这个算法?
|
||||||
|
|
||||||
|
**问题场景**:
|
||||||
|
|
||||||
|
```
|
||||||
|
价格
|
||||||
|
^
|
||||||
|
| * ← 强高点 (7.9元)
|
||||||
|
| \
|
||||||
|
| * ← 弱高点 (5.8元) ← 问题!
|
||||||
|
| \
|
||||||
|
| * ← 强高点 (6.8元)
|
||||||
|
| \
|
||||||
|
| * ← 强高点 (6.0元)
|
||||||
|
└──────────────> 时间
|
||||||
|
```
|
||||||
|
|
||||||
|
如果使用所有4个点拟合,5.8元的"弱高点"会把上沿线拉低,导致:
|
||||||
|
- ❌ 拟合线不是真正的上边界
|
||||||
|
- ❌ 与主观判断不符
|
||||||
|
- ❌ 可能错过真实的突破信号
|
||||||
|
|
||||||
|
### 解决方案:迭代移除
|
||||||
|
|
||||||
|
```
|
||||||
|
第1次迭代:
|
||||||
|
拟合线: y = ax + b (用全部4点)
|
||||||
|
残差: [0.1, 1.8, 0.2, 0.3] ← 5.8元点的残差最大(1.8)
|
||||||
|
判定: 1.8 > 1.5*std → 离群点
|
||||||
|
操作: 移除5.8元点
|
||||||
|
|
||||||
|
第2次迭代:
|
||||||
|
拟合线: y = a'x + b' (用剩余3点)
|
||||||
|
残差: [0.05, 0.08, 0.12]
|
||||||
|
判定: 无离群点 → 收敛
|
||||||
|
|
||||||
|
最终结果: 只用3个强高点拟合
|
||||||
|
```
|
||||||
|
|
||||||
|
### 算法流程图
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[输入全部枢轴点] --> B[线性回归拟合]
|
||||||
|
B --> C[计算方向性残差]
|
||||||
|
C --> D{残差 > 阈值?}
|
||||||
|
D -->|是| E[移除最大残差点]
|
||||||
|
E --> F{剩余点 >= 3?}
|
||||||
|
F -->|是| G{迭代次数 < 3?}
|
||||||
|
G -->|是| B
|
||||||
|
G -->|否| H[返回当前拟合]
|
||||||
|
F -->|否| H
|
||||||
|
D -->|否| H
|
||||||
|
```
|
||||||
|
|
||||||
|
### 关键参数
|
||||||
|
|
||||||
|
| 参数 | 默认值 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| `outlier_threshold` | 1.5 | 残差超过 1.5 倍标准差视为离群点 |
|
||||||
|
| `max_iterations` | 3 | 最多迭代 3 次,避免过度过滤 |
|
||||||
|
| `min_keep` | 3 | 至少保留 3 个点用于拟合 |
|
||||||
|
|
||||||
|
### 残差计算逻辑
|
||||||
|
|
||||||
|
**上沿线(upper)**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 残差 = 拟合值 - 实际值
|
||||||
|
# 正残差 = 点在拟合线下方 = 弱高点(应移除)
|
||||||
|
residuals = fitted_values - actual_values
|
||||||
|
outliers = residuals > threshold
|
||||||
|
```
|
||||||
|
|
||||||
|
**下沿线(lower)**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 残差 = 实际值 - 拟合值
|
||||||
|
# 正残差 = 点在拟合线上方 = 弱低点(应移除)
|
||||||
|
residuals = actual_values - fitted_values
|
||||||
|
outliers = residuals > threshold
|
||||||
|
```
|
||||||
|
|
||||||
|
### 算法优势
|
||||||
|
|
||||||
|
| 优势 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **自适应** | 自动识别偏离点,无需人工干预 |
|
||||||
|
| **稳健性** | 不会被单个异常点影响整条线 |
|
||||||
|
| **可控性** | 通过阈值和迭代次数控制过滤强度 |
|
||||||
|
| **保守性** | 至少保留3个点,避免过度过滤 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 历史版本:分段选择算法
|
||||||
|
|
||||||
|
> **注意**:以下是 v1 版本的分段算法,已被迭代离群点移除算法替代。保留此节供参考。
|
||||||
|
|
||||||
|
### 原核心思想
|
||||||
|
|
||||||
|
对于识别出的所有枢轴点:
|
||||||
|
|
||||||
1. **分段策略**:将枢轴点按时间顺序分成 **3 个时间段**
|
1. **分段策略**:将枢轴点按时间顺序分成 **3 个时间段**
|
||||||
2. **代表点选择**:从每段中选出 **最极端的点**(上沿选最高,下沿选最低)
|
2. **代表点选择**:从每段中选出 **最极端的点**(上沿选最高,下沿选最低)
|
||||||
3. **线性回归**:用这 3 个代表点进行线性回归,拟合趋势线
|
3. **线性回归**:用这 3 个代表点进行线性回归,拟合趋势线
|
||||||
|
|
||||||
### 触发条件
|
### 原触发条件
|
||||||
|
|
||||||
```python
|
```python
|
||||||
if 枢轴点数量 > 4:
|
if 枢轴点数量 > 4:
|
||||||
@ -37,6 +155,12 @@ else:
|
|||||||
使用全部枢轴点
|
使用全部枢轴点
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 原算法的局限性
|
||||||
|
|
||||||
|
分段算法会选中一些"弱"枢轴点:
|
||||||
|
- 某个高点虽然是时间段内最高,但可能明显低于其他时间段的高点
|
||||||
|
- 这些点会拉低/拉高拟合线,导致与主观判断不符
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 为什么需要分段
|
## 为什么需要分段
|
||||||
@ -254,7 +378,7 @@ n = 4 ≤ 4,不分段,全部使用 ✓
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 独立分段机制
|
## 独立处理机制
|
||||||
|
|
||||||
### 关键点:高点和低点分别独立处理
|
### 关键点:高点和低点分别独立处理
|
||||||
|
|
||||||
@ -323,29 +447,28 @@ else:
|
|||||||
|
|
||||||
## 代码实现
|
## 代码实现
|
||||||
|
|
||||||
### 完整代码(带详细注释)
|
### 当前算法:迭代离群点移除(v2)
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def fit_pivot_line(
|
def fit_pivot_line(
|
||||||
pivot_indices: np.ndarray, # 枢轴点的时间索引
|
pivot_indices: np.ndarray,
|
||||||
pivot_values: np.ndarray, # 枢轴点的价格
|
pivot_values: np.ndarray,
|
||||||
mode: str = "upper", # "upper" 或 "lower"
|
mode: str = "upper",
|
||||||
min_points: int = 2,
|
min_points: int = 2,
|
||||||
|
outlier_threshold: float = 1.5, # 离群点阈值(标准差倍数)
|
||||||
|
max_iterations: int = 3, # 最大迭代次数
|
||||||
) -> Tuple[float, float, np.ndarray]:
|
) -> Tuple[float, float, np.ndarray]:
|
||||||
"""
|
"""
|
||||||
拟合枢轴点趋势线(使用分段选择策略)
|
迭代离群点移除的枢轴点拟合算法
|
||||||
|
|
||||||
策略:
|
策略:
|
||||||
- 如果枢轴点 > 4:分3段,每段选1个极值点(共3个拟合点)
|
1. 先用所有点做初始拟合
|
||||||
- 如果枢轴点 ≤ 4:全部使用
|
2. 识别并移除偏离拟合线的"弱"点
|
||||||
|
3. 迭代直到收敛
|
||||||
|
|
||||||
Returns:
|
对于上沿线:移除价格明显低于拟合线的点(弱高点)
|
||||||
(斜率a, 截距b, 选中的枢轴点索引)
|
对于下沿线:移除价格明显高于拟合线的点(弱低点)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────
|
|
||||||
# 第1步:基本检查和排序
|
|
||||||
# ─────────────────────────────────────────────────────────
|
|
||||||
if len(pivot_indices) < min_points:
|
if len(pivot_indices) < min_points:
|
||||||
return 0.0, 0.0, np.array([])
|
return 0.0, 0.0, np.array([])
|
||||||
|
|
||||||
@ -353,115 +476,113 @@ def fit_pivot_line(
|
|||||||
sort_idx = np.argsort(pivot_indices)
|
sort_idx = np.argsort(pivot_indices)
|
||||||
x_sorted = pivot_indices[sort_idx].astype(float)
|
x_sorted = pivot_indices[sort_idx].astype(float)
|
||||||
y_sorted = pivot_values[sort_idx]
|
y_sorted = pivot_values[sort_idx]
|
||||||
|
|
||||||
n = len(x_sorted)
|
n = len(x_sorted)
|
||||||
|
|
||||||
if n < 2:
|
if n < 2:
|
||||||
return 0.0, 0.0, np.array([])
|
return 0.0, 0.0, np.array([])
|
||||||
|
|
||||||
|
# 初始化:所有点都参与
|
||||||
|
active_mask = np.ones(n, dtype=bool)
|
||||||
|
min_keep = max(3, min_points) # 至少保留3个点
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────
|
||||||
# 第2步:决定是否分段
|
# 迭代离群点移除
|
||||||
# ─────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────
|
||||||
if n <= 4:
|
for iteration in range(max_iterations):
|
||||||
# 点数少,全部使用
|
active_indices = np.where(active_mask)[0]
|
||||||
selected_mask = np.ones(n, dtype=bool)
|
if len(active_indices) <= min_keep:
|
||||||
|
break
|
||||||
|
|
||||||
|
# 当前活跃点
|
||||||
|
x_active = x_sorted[active_mask]
|
||||||
|
y_active = y_sorted[active_mask]
|
||||||
|
|
||||||
|
# 线性回归
|
||||||
|
a, b = fit_line(x_active, y_active)
|
||||||
|
|
||||||
|
# 计算拟合值
|
||||||
|
fitted_values = a * x_active + b
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────
|
||||||
|
# 计算方向性残差
|
||||||
|
# ─────────────────────────────────────────────────────
|
||||||
|
if mode == "upper":
|
||||||
|
# 上沿:残差 = 拟合值 - 实际值
|
||||||
|
# 正残差表示点在拟合线下方(弱高点)
|
||||||
|
residuals = fitted_values - y_active
|
||||||
else:
|
else:
|
||||||
# 点数多,使用分段策略
|
# 下沿:残差 = 实际值 - 拟合值
|
||||||
selected_mask = np.zeros(n, dtype=bool)
|
# 正残差表示点在拟合线上方(弱低点)
|
||||||
|
residuals = y_active - fitted_values
|
||||||
|
|
||||||
# 计算每段大小
|
# 计算标准差
|
||||||
segment_size = n // 3
|
std_residual = np.std(residuals)
|
||||||
if segment_size < 1:
|
if std_residual < 1e-10:
|
||||||
segment_size = 1
|
# 所有点几乎在一条线上,无需移除
|
||||||
|
break
|
||||||
# 定义三个时间段
|
|
||||||
segments = [
|
|
||||||
range(0, min(segment_size, n)), # 第1段
|
|
||||||
range(segment_size, min(2 * segment_size, n)), # 第2段
|
|
||||||
range(2 * segment_size, n), # 第3段
|
|
||||||
]
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────
|
||||||
# 第3步:从每段选择极值点
|
# 识别并移除最大离群点
|
||||||
# ─────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────
|
||||||
for seg in segments:
|
threshold = outlier_threshold * std_residual
|
||||||
if len(seg) == 0:
|
max_outlier_idx = np.argmax(residuals)
|
||||||
continue
|
|
||||||
|
|
||||||
seg_list = list(seg)
|
if residuals[max_outlier_idx] <= threshold:
|
||||||
seg_y = y_sorted[seg_list]
|
# 最大残差不超过阈值,收敛
|
||||||
|
break
|
||||||
|
|
||||||
if mode == "upper":
|
if np.sum(active_mask) <= min_keep:
|
||||||
# 上沿:选该段最高点
|
break
|
||||||
best_idx_in_seg = np.argmax(seg_y)
|
|
||||||
else: # mode == "lower"
|
|
||||||
# 下沿:选该段最低点
|
|
||||||
best_idx_in_seg = np.argmin(seg_y)
|
|
||||||
|
|
||||||
# 标记选中
|
# 移除离群点
|
||||||
selected_mask[seg_list[best_idx_in_seg]] = True
|
original_idx = active_indices[max_outlier_idx]
|
||||||
|
active_mask[original_idx] = False
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────
|
||||||
# 第4步:提取选中的点
|
# 最终拟合
|
||||||
# ─────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────
|
||||||
selected_x = x_sorted[selected_mask]
|
selected_indices_sorted = np.where(active_mask)[0]
|
||||||
selected_y = y_sorted[selected_mask]
|
selected_x = x_sorted[active_mask]
|
||||||
selected_indices_sorted = np.where(selected_mask)[0]
|
selected_y = y_sorted[active_mask]
|
||||||
|
|
||||||
# 保底:至少选首尾两点
|
|
||||||
if len(selected_x) < 2:
|
if len(selected_x) < 2:
|
||||||
selected_mask = np.zeros(n, dtype=bool)
|
# 兜底:使用首尾两点
|
||||||
selected_mask[0] = True
|
active_mask = np.zeros(n, dtype=bool)
|
||||||
selected_mask[-1] = True
|
active_mask[0] = True
|
||||||
selected_x = x_sorted[selected_mask]
|
active_mask[-1] = True
|
||||||
selected_y = y_sorted[selected_mask]
|
selected_x = x_sorted[active_mask]
|
||||||
selected_indices_sorted = np.where(selected_mask)[0]
|
selected_y = y_sorted[active_mask]
|
||||||
|
selected_indices_sorted = np.where(active_mask)[0]
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────
|
|
||||||
# 第5步:线性回归
|
|
||||||
# ─────────────────────────────────────────────────────────
|
|
||||||
a, b = fit_line(selected_x, selected_y)
|
a, b = fit_line(selected_x, selected_y)
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────
|
# 返回原始索引
|
||||||
# 第6步:覆盖性验证(确保线不穿透任何枢轴点)
|
|
||||||
# ─────────────────────────────────────────────────────────
|
|
||||||
fitted_all = a * x_sorted + b
|
|
||||||
tolerance = 0.03 # 3%容差
|
|
||||||
|
|
||||||
if mode == "upper":
|
|
||||||
# 上沿线不应低于任何高点
|
|
||||||
violations = y_sorted > fitted_all + tolerance * np.mean(y_sorted)
|
|
||||||
if np.any(violations):
|
|
||||||
# 强制包含全局最高点
|
|
||||||
global_max_idx = np.argmax(y_sorted)
|
|
||||||
if not selected_mask[global_max_idx]:
|
|
||||||
selected_mask[global_max_idx] = True
|
|
||||||
selected_x = x_sorted[selected_mask]
|
|
||||||
selected_y = y_sorted[selected_mask]
|
|
||||||
selected_indices_sorted = np.where(selected_mask)[0]
|
|
||||||
a, b = fit_line(selected_x, selected_y)
|
|
||||||
else: # mode == "lower"
|
|
||||||
# 下沿线不应高于任何低点
|
|
||||||
violations = y_sorted < fitted_all - tolerance * np.mean(y_sorted)
|
|
||||||
if np.any(violations):
|
|
||||||
# 强制包含全局最低点
|
|
||||||
global_min_idx = np.argmin(y_sorted)
|
|
||||||
if not selected_mask[global_min_idx]:
|
|
||||||
selected_mask[global_min_idx] = True
|
|
||||||
selected_x = x_sorted[selected_mask]
|
|
||||||
selected_y = y_sorted[selected_mask]
|
|
||||||
selected_indices_sorted = np.where(selected_mask)[0]
|
|
||||||
a, b = fit_line(selected_x, selected_y)
|
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────
|
|
||||||
# 第7步:返回结果
|
|
||||||
# ─────────────────────────────────────────────────────────
|
|
||||||
# 将排序后的索引映射回原始索引
|
|
||||||
selected_original = sort_idx[selected_indices_sorted]
|
selected_original = sort_idx[selected_indices_sorted]
|
||||||
|
|
||||||
return float(a), float(b), selected_original
|
return float(a), float(b), selected_original
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 历史算法:分段选择(v1)
|
||||||
|
|
||||||
|
> 以下代码已被替代,保留供参考。
|
||||||
|
|
||||||
|
```python
|
||||||
|
def fit_pivot_line_v1(
|
||||||
|
pivot_indices: np.ndarray,
|
||||||
|
pivot_values: np.ndarray,
|
||||||
|
mode: str = "upper",
|
||||||
|
min_points: int = 2,
|
||||||
|
) -> Tuple[float, float, np.ndarray]:
|
||||||
|
"""
|
||||||
|
分段选择策略(已废弃)
|
||||||
|
|
||||||
|
策略:
|
||||||
|
- 如果枢轴点 > 4:分3段,每段选1个极值点(共3个拟合点)
|
||||||
|
- 如果枢轴点 ≤ 4:全部使用
|
||||||
|
"""
|
||||||
|
# ... 详见历史版本
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 实际案例分析
|
## 实际案例分析
|
||||||
@ -616,90 +737,45 @@ if len(selected_x) < 2:
|
|||||||
```
|
```
|
||||||
图表元素 对应内容
|
图表元素 对应内容
|
||||||
─────────────────────────────────────────────
|
─────────────────────────────────────────────
|
||||||
浅红色小实心圆(6个) 所有高点枢轴点
|
浅红色小实心圆 所有高点枢轴点
|
||||||
深红色大空心圆(3个) 上沿拟合点(从6个中选出)
|
深红色大空心圆 上沿拟合点(迭代算法选出)
|
||||||
红色点划竖线 高点分段边界
|
浅绿色小实心圆 所有低点枢轴点
|
||||||
- "高1|2" 标签 第1段和第2段分界
|
深绿色大空心圆 下沿拟合点(迭代算法选出)
|
||||||
- "高2|3" 标签 第2段和第3段分界
|
|
||||||
|
|
||||||
浅绿色小实心圆(4个) 所有低点枢轴点
|
|
||||||
深绿色大空心圆(4个) 下沿拟合点(全部使用,因≤4)
|
|
||||||
(无绿色竖线) 低点不分段(≤4)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 分段线的位置
|
**注意**:自 v2.0 起,分段竖线已被移除(因算法不再使用分段策略)。
|
||||||
|
|
||||||
**分段边界的时间索引**:
|
|
||||||
|
|
||||||
```python
|
|
||||||
# 以高点为例,n=6, segment_size=2
|
|
||||||
排序后的高点索引: [10, 30, 50, 60, 80, 90]
|
|
||||||
位置0 1 2 3 4 5
|
|
||||||
|
|
||||||
第1段结束 = 第2段开始:
|
|
||||||
boundary_1 = 索引[segment_size] = 索引[2] = 50
|
|
||||||
→ 在时间50处画竖线
|
|
||||||
|
|
||||||
第2段结束 = 第3段开始:
|
|
||||||
boundary_2 = 索引[2*segment_size] = 索引[4] = 80
|
|
||||||
→ 在时间80处画竖线
|
|
||||||
|
|
||||||
分段结果:
|
|
||||||
第1段: [10, 30] | 第2段: [50, 60] | 第3段: [80, 90]
|
|
||||||
↑ boundary_1 ↑ boundary_2
|
|
||||||
```
|
|
||||||
|
|
||||||
**代码实现**:
|
|
||||||
|
|
||||||
```python
|
|
||||||
if len(ph_idx) > 4:
|
|
||||||
n_high = len(ph_idx)
|
|
||||||
segment_size_high = n_high // 3
|
|
||||||
|
|
||||||
# 第1条竖线
|
|
||||||
if segment_size_high < n_high:
|
|
||||||
boundary_1 = ph_idx[segment_size_high]
|
|
||||||
ax.axvline(boundary_1, color='red', linestyle='-.', ...)
|
|
||||||
ax.text(boundary_1, y_max*0.96, '高1|2', ...)
|
|
||||||
|
|
||||||
# 第2条竖线
|
|
||||||
if 2 * segment_size_high < n_high:
|
|
||||||
boundary_2 = ph_idx[2 * segment_size_high]
|
|
||||||
ax.axvline(boundary_2, color='red', linestyle='-.', ...)
|
|
||||||
ax.text(boundary_2, y_max*0.96, '高2|3', ...)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 总结
|
## 总结
|
||||||
|
|
||||||
### 分段策略的核心要点
|
### 迭代离群点移除算法的核心要点
|
||||||
|
|
||||||
1. **触发条件**: 枢轴点 > 4
|
1. **初始状态**: 所有枢轴点参与拟合
|
||||||
2. **分段数量**: 固定3段
|
2. **迭代移除**: 每次移除最大残差的离群点
|
||||||
3. **分段方式**: 时间均分(每段 `n//3` 个点)
|
3. **方向性残差**: 上沿移除低于线的点,下沿移除高于线的点
|
||||||
4. **选择策略**: 每段选1个极值点
|
4. **收敛条件**: 无离群点 / 达到最大迭代次数 / 点数达到下限
|
||||||
5. **独立性**: 高点和低点各自独立分段
|
5. **独立性**: 高点和低点各自独立处理
|
||||||
6. **保底机制**: 覆盖性验证 + 全局极值保护
|
6. **保底机制**: 至少保留3个点用于拟合
|
||||||
|
|
||||||
### 算法优势
|
### 算法优势
|
||||||
|
|
||||||
| 优势 | 说明 |
|
| 优势 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| **时间均衡** | 前、中、后三个时期都有代表点 |
|
| **自适应** | 自动识别并移除偏离点 |
|
||||||
| **代表性强** | 每段选最极端的点,确保边界性 |
|
| **稳健性** | 不受单个异常点影响 |
|
||||||
| **抗噪性好** | 不易被局部密集点影响 |
|
| **可控性** | 通过阈值和迭代次数控制 |
|
||||||
| **稳健性高** | 多点回归比两点连线更稳定 |
|
| **保守性** | 至少保留3个点,避免过度过滤 |
|
||||||
| **可扩展性** | 点数增加时仍保持3个拟合点 |
|
| **符合直觉** | 拟合结果与主观判断一致 |
|
||||||
|
|
||||||
### 与其他方法的对比
|
### 与其他方法的对比
|
||||||
|
|
||||||
| 方法 | 优点 | 缺点 |
|
| 方法 | 优点 | 缺点 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| **两点连线** | 简单快速 | 忽略中间点,易受噪声影响 |
|
| **两点连线** | 简单快速 | 忽略中间点,易受噪声影响 |
|
||||||
| **全部点回归** | 利用所有信息 | 权重不均,点密集区域主导 |
|
| **全部点回归** | 利用所有信息 | 权重不均,异常点影响大 |
|
||||||
| **分段选择(当前)** | 时间均衡,代表性强 | 略复杂,需要分段逻辑 |
|
| **分段选择(v1)** | 时间均衡 | 可能选中弱枢轴点 |
|
||||||
| **滑动窗口** | 平滑效果好 | 计算复杂,参数敏感 |
|
| **迭代离群点移除(当前)** | 自适应,稳健 | 需要迭代计算 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -711,6 +787,9 @@ if len(ph_idx) > 4:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**文档版本**: v1.0
|
**文档版本**: v2.0
|
||||||
**最后更新**: 2026-01-26
|
**最后更新**: 2026-01-26
|
||||||
|
**变更记录**:
|
||||||
|
- v2.0: 采用迭代离群点移除算法,替代原分段选择策略
|
||||||
|
- v1.0: 初始版本,分段选择算法
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -9,6 +9,7 @@
|
|||||||
用法:
|
用法:
|
||||||
python scripts/pipeline_converging_triangle.py
|
python scripts/pipeline_converging_triangle.py
|
||||||
python scripts/pipeline_converging_triangle.py --date 20260120
|
python scripts/pipeline_converging_triangle.py --date 20260120
|
||||||
|
python scripts/pipeline_converging_triangle.py --show-details # 生成详情模式图片
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@ -54,6 +55,11 @@ def main() -> None:
|
|||||||
default=None,
|
default=None,
|
||||||
help="指定日期(YYYYMMDD),用于报告和图表生成(默认为数据最新日)",
|
help="指定日期(YYYYMMDD),用于报告和图表生成(默认为数据最新日)",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--show-details",
|
||||||
|
action="store_true",
|
||||||
|
help="生成详情模式图片(显示所有枢轴点和拟合点)",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--skip-detection",
|
"--skip-detection",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
@ -79,6 +85,10 @@ def main() -> None:
|
|||||||
print(f"开始时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
print(f"开始时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
if args.date:
|
if args.date:
|
||||||
print(f"指定日期: {args.date}")
|
print(f"指定日期: {args.date}")
|
||||||
|
if args.show_details:
|
||||||
|
print(f"图表模式: 详情模式(显示所有枢轴点)")
|
||||||
|
else:
|
||||||
|
print(f"图表模式: 简洁模式(仅显示价格和趋势线)")
|
||||||
print("=" * 80)
|
print("=" * 80)
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
@ -149,10 +159,13 @@ def main() -> None:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# 设置命令行参数
|
# 设置命令行参数
|
||||||
|
cmd_args = [sys.argv[0]]
|
||||||
if args.date:
|
if args.date:
|
||||||
sys.argv = [sys.argv[0], "--date", str(args.date)]
|
cmd_args.extend(["--date", str(args.date)])
|
||||||
else:
|
if args.show_details:
|
||||||
sys.argv = [sys.argv[0]]
|
cmd_args.append("--show-details")
|
||||||
|
|
||||||
|
sys.argv = cmd_args
|
||||||
|
|
||||||
run_plot()
|
run_plot()
|
||||||
success = True
|
success = True
|
||||||
|
|||||||
@ -37,10 +37,11 @@ from converging_triangle import (
|
|||||||
fit_pivot_line,
|
fit_pivot_line,
|
||||||
line_y,
|
line_y,
|
||||||
pivots_fractal,
|
pivots_fractal,
|
||||||
|
pivots_fractal_hybrid,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 导入统一的参数配置
|
# 导入统一的参数配置
|
||||||
from triangle_config import DETECTION_PARAMS, DISPLAY_WINDOW, SHOW_CHART_DETAILS
|
from triangle_config import DETECTION_PARAMS, DISPLAY_WINDOW, SHOW_CHART_DETAILS, REALTIME_MODE, FLEXIBLE_ZONE
|
||||||
|
|
||||||
|
|
||||||
class FakeModule:
|
class FakeModule:
|
||||||
@ -159,7 +160,11 @@ def plot_triangle(
|
|||||||
display_volume = volume_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]]
|
display_dates = dates[valid_indices[display_start:valid_end + 1]]
|
||||||
|
|
||||||
# 检测三角形(使用检测窗口数据)
|
# ========================================================================
|
||||||
|
# 计算三角形参数(用于绘图)
|
||||||
|
# 注意:不验证 is_valid,因为CSV中已经验证通过了
|
||||||
|
# 这里只是重新计算参数用于可视化
|
||||||
|
# ========================================================================
|
||||||
result = detect_converging_triangle(
|
result = detect_converging_triangle(
|
||||||
high=high_win,
|
high=high_win,
|
||||||
low=low_win,
|
low=low_win,
|
||||||
@ -170,9 +175,8 @@ def plot_triangle(
|
|||||||
date_idx=date_idx,
|
date_idx=date_idx,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not result.is_valid:
|
# 不再检查 is_valid,直接绘图
|
||||||
print(f" [跳过] {stock_code} {stock_name}: 未识别到有效三角形")
|
# 原因:CSV中已经包含了通过验证的股票,这里只需要可视化
|
||||||
return
|
|
||||||
|
|
||||||
# 绘图准备
|
# 绘图准备
|
||||||
x_display = np.arange(len(display_close), dtype=float)
|
x_display = np.arange(len(display_close), dtype=float)
|
||||||
@ -184,7 +188,18 @@ def plot_triangle(
|
|||||||
n = len(close_win)
|
n = len(close_win)
|
||||||
x_win = np.arange(n, dtype=float)
|
x_win = np.arange(n, dtype=float)
|
||||||
|
|
||||||
# 计算枢轴点(与检测算法一致)
|
# 计算枢轴点(与检测算法一致,考虑实时模式)
|
||||||
|
if REALTIME_MODE:
|
||||||
|
confirmed_ph, confirmed_pl, candidate_ph, candidate_pl = pivots_fractal_hybrid(
|
||||||
|
high_win, low_win, k=params.pivot_k, flexible_zone=FLEXIBLE_ZONE
|
||||||
|
)
|
||||||
|
# 合并确认枢轴点和候选枢轴点
|
||||||
|
ph_idx = np.concatenate([confirmed_ph, candidate_ph]) if len(candidate_ph) > 0 else confirmed_ph
|
||||||
|
pl_idx = np.concatenate([confirmed_pl, candidate_pl]) if len(candidate_pl) > 0 else confirmed_pl
|
||||||
|
# 排序以保证顺序
|
||||||
|
ph_idx = np.sort(ph_idx)
|
||||||
|
pl_idx = np.sort(pl_idx)
|
||||||
|
else:
|
||||||
ph_idx, pl_idx = pivots_fractal(high_win, low_win, k=params.pivot_k)
|
ph_idx, pl_idx = pivots_fractal(high_win, low_win, k=params.pivot_k)
|
||||||
|
|
||||||
# 使用枢轴点连线法拟合边界线(与检测算法一致)
|
# 使用枢轴点连线法拟合边界线(与检测算法一致)
|
||||||
@ -282,108 +297,6 @@ def plot_triangle(
|
|||||||
label=f'下沿拟合点({len(selected_pl_pos)})',
|
label=f'下沿拟合点({len(selected_pl_pos)})',
|
||||||
)
|
)
|
||||||
|
|
||||||
# 绘制分段竖线(显示算法如何分段选择枢轴点)
|
|
||||||
# 高点和低点分别独立分段,用不同颜色显示
|
|
||||||
y_min, y_max = ax1.get_ylim()
|
|
||||||
|
|
||||||
# 绘制高点枢轴点的分段线(红色)
|
|
||||||
if len(ph_idx) > 4:
|
|
||||||
n_high = len(ph_idx)
|
|
||||||
segment_size_high = n_high // 3
|
|
||||||
|
|
||||||
# 第1段结束 = 第2段开始
|
|
||||||
if segment_size_high < n_high:
|
|
||||||
boundary_1 = ph_idx[segment_size_high] + triangle_offset
|
|
||||||
ax1.axvline(
|
|
||||||
boundary_1,
|
|
||||||
color='red',
|
|
||||||
linestyle='-.',
|
|
||||||
linewidth=1.2,
|
|
||||||
alpha=0.4,
|
|
||||||
zorder=3,
|
|
||||||
)
|
|
||||||
ax1.text(
|
|
||||||
boundary_1,
|
|
||||||
y_max * 0.96,
|
|
||||||
'高1|2',
|
|
||||||
ha='center',
|
|
||||||
va='top',
|
|
||||||
fontsize=7,
|
|
||||||
color='red',
|
|
||||||
bbox=dict(boxstyle='round,pad=0.2', facecolor='white', edgecolor='red', alpha=0.7),
|
|
||||||
)
|
|
||||||
|
|
||||||
# 第2段结束 = 第3段开始
|
|
||||||
if 2 * segment_size_high < n_high:
|
|
||||||
boundary_2 = ph_idx[2 * segment_size_high] + triangle_offset
|
|
||||||
ax1.axvline(
|
|
||||||
boundary_2,
|
|
||||||
color='red',
|
|
||||||
linestyle='-.',
|
|
||||||
linewidth=1.2,
|
|
||||||
alpha=0.4,
|
|
||||||
zorder=3,
|
|
||||||
)
|
|
||||||
ax1.text(
|
|
||||||
boundary_2,
|
|
||||||
y_max * 0.96,
|
|
||||||
'高2|3',
|
|
||||||
ha='center',
|
|
||||||
va='top',
|
|
||||||
fontsize=7,
|
|
||||||
color='red',
|
|
||||||
bbox=dict(boxstyle='round,pad=0.2', facecolor='white', edgecolor='red', alpha=0.7),
|
|
||||||
)
|
|
||||||
|
|
||||||
# 绘制低点枢轴点的分段线(绿色)
|
|
||||||
if len(pl_idx) > 4:
|
|
||||||
n_low = len(pl_idx)
|
|
||||||
segment_size_low = n_low // 3
|
|
||||||
|
|
||||||
# 第1段结束 = 第2段开始
|
|
||||||
if segment_size_low < n_low:
|
|
||||||
boundary_1 = pl_idx[segment_size_low] + triangle_offset
|
|
||||||
ax1.axvline(
|
|
||||||
boundary_1,
|
|
||||||
color='green',
|
|
||||||
linestyle='-.',
|
|
||||||
linewidth=1.2,
|
|
||||||
alpha=0.4,
|
|
||||||
zorder=3,
|
|
||||||
)
|
|
||||||
ax1.text(
|
|
||||||
boundary_1,
|
|
||||||
y_min + (y_max - y_min) * 0.04,
|
|
||||||
'低1|2',
|
|
||||||
ha='center',
|
|
||||||
va='bottom',
|
|
||||||
fontsize=7,
|
|
||||||
color='green',
|
|
||||||
bbox=dict(boxstyle='round,pad=0.2', facecolor='white', edgecolor='green', alpha=0.7),
|
|
||||||
)
|
|
||||||
|
|
||||||
# 第2段结束 = 第3段开始
|
|
||||||
if 2 * segment_size_low < n_low:
|
|
||||||
boundary_2 = pl_idx[2 * segment_size_low] + triangle_offset
|
|
||||||
ax1.axvline(
|
|
||||||
boundary_2,
|
|
||||||
color='green',
|
|
||||||
linestyle='-.',
|
|
||||||
linewidth=1.2,
|
|
||||||
alpha=0.4,
|
|
||||||
zorder=3,
|
|
||||||
)
|
|
||||||
ax1.text(
|
|
||||||
boundary_2,
|
|
||||||
y_min + (y_max - y_min) * 0.04,
|
|
||||||
'低2|3',
|
|
||||||
ha='center',
|
|
||||||
va='bottom',
|
|
||||||
fontsize=7,
|
|
||||||
color='green',
|
|
||||||
bbox=dict(boxstyle='round,pad=0.2', facecolor='white', edgecolor='green', alpha=0.7),
|
|
||||||
)
|
|
||||||
|
|
||||||
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)}个交易日) "
|
||||||
|
|||||||
@ -31,9 +31,9 @@ DETECTION_PARAMS = ConvergingTriangleParams(
|
|||||||
boundary_n_segments=2, # 边界线分段数
|
boundary_n_segments=2, # 边界线分段数
|
||||||
boundary_source="full", # 边界拟合数据源: "full"(全部) 或 "pivot"(仅枢轴点)
|
boundary_source="full", # 边界拟合数据源: "full"(全部) 或 "pivot"(仅枢轴点)
|
||||||
|
|
||||||
# 斜率约束
|
# 斜率约束(严格收敛三角形)
|
||||||
upper_slope_max=0.10, # 上沿最大斜率(正值,向上倾斜)
|
upper_slope_max=0, # 上沿必须向下或水平(≤0)
|
||||||
lower_slope_min=-0.10, # 下沿最小斜率(负值,向下倾斜)
|
lower_slope_min=0, # 下沿必须向上或水平(≥0)
|
||||||
# 注意:算法会自动过滤"同向通道"(上下沿都向上或都向下)
|
# 注意:算法会自动过滤"同向通道"(上下沿都向上或都向下)
|
||||||
# 只保留真正的收敛形态(上下沿相向运动)
|
# 只保留真正的收敛形态(上下沿相向运动)
|
||||||
|
|
||||||
@ -92,8 +92,8 @@ DISPLAY_WINDOW = 500
|
|||||||
REALTIME_MODE = True # True=实时模式(默认), False=标准模式
|
REALTIME_MODE = True # True=实时模式(默认), False=标准模式
|
||||||
|
|
||||||
# 灵活区域大小(仅在实时模式下生效)
|
# 灵活区域大小(仅在实时模式下生效)
|
||||||
FLEXIBLE_ZONE = 5 # 最近5天使用降低标准
|
FLEXIBLE_ZONE = 15 # 最近15天使用降低标准(与pivot_k一致)
|
||||||
# 建议: 3-7天,太大会引入噪音
|
# 说明: 设为与pivot_k相同,确保窗口末端的所有潜在枢轴点都能被识别
|
||||||
|
|
||||||
# 实时模式说明:
|
# 实时模式说明:
|
||||||
# - 实时模式(推荐):使用确认+候选枢轴点(允许右边数据不完整),适合实时选股
|
# - 实时模式(推荐):使用确认+候选枢轴点(允许右边数据不完整),适合实时选股
|
||||||
|
|||||||
Binary file not shown.
@ -103,14 +103,17 @@ class ConvergingTriangleResult:
|
|||||||
def pivots_fractal(
|
def pivots_fractal(
|
||||||
high: np.ndarray, low: np.ndarray, k: int = 3
|
high: np.ndarray, low: np.ndarray, k: int = 3
|
||||||
) -> Tuple[np.ndarray, np.ndarray]:
|
) -> Tuple[np.ndarray, np.ndarray]:
|
||||||
"""左右窗口分形:返回 pivot_high_idx, pivot_low_idx"""
|
"""左右窗口分形:返回 pivot_high_idx, pivot_low_idx
|
||||||
|
|
||||||
|
使用 nanmax/nanmin 来忽略 NaN 值
|
||||||
|
"""
|
||||||
n = len(high)
|
n = len(high)
|
||||||
ph: List[int] = []
|
ph: List[int] = []
|
||||||
pl: List[int] = []
|
pl: List[int] = []
|
||||||
for i in range(k, n - k):
|
for i in range(k, n - k):
|
||||||
if high[i] == np.max(high[i - k : i + k + 1]):
|
if not np.isnan(high[i]) and high[i] == np.nanmax(high[i - k : i + k + 1]):
|
||||||
ph.append(i)
|
ph.append(i)
|
||||||
if low[i] == np.min(low[i - k : i + k + 1]):
|
if not np.isnan(low[i]) and low[i] == np.nanmin(low[i - k : i + k + 1]):
|
||||||
pl.append(i)
|
pl.append(i)
|
||||||
return np.array(ph, dtype=int), np.array(pl, dtype=int)
|
return np.array(ph, dtype=int), np.array(pl, dtype=int)
|
||||||
|
|
||||||
@ -138,22 +141,36 @@ def pivots_fractal_hybrid(
|
|||||||
n = len(high)
|
n = len(high)
|
||||||
|
|
||||||
# 确认枢轴点(完整窗口)
|
# 确认枢轴点(完整窗口)
|
||||||
|
# 使用 nanmax/nanmin 来忽略 NaN 值
|
||||||
confirmed_ph: List[int] = []
|
confirmed_ph: List[int] = []
|
||||||
confirmed_pl: List[int] = []
|
confirmed_pl: List[int] = []
|
||||||
for i in range(k, n - k):
|
for i in range(k, n - k):
|
||||||
if high[i] == np.max(high[i - k : i + k + 1]):
|
if not np.isnan(high[i]) and high[i] == np.nanmax(high[i - k : i + k + 1]):
|
||||||
confirmed_ph.append(i)
|
confirmed_ph.append(i)
|
||||||
if low[i] == np.min(low[i - k : i + k + 1]):
|
if not np.isnan(low[i]) and low[i] == np.nanmin(low[i - k : i + k + 1]):
|
||||||
confirmed_pl.append(i)
|
confirmed_pl.append(i)
|
||||||
|
|
||||||
# 候选枢轴点(灵活窗口,最近flexible_zone天)
|
# 候选枢轴点(灵活窗口,最近flexible_zone天)
|
||||||
|
# 优化逻辑:使用对称窗口,左右各看 min(k, right_avail+1) 天
|
||||||
|
# 这样可以识别出窗口末端的局部枢轴点
|
||||||
|
# 使用 nanmax/nanmin 来忽略 NaN 值
|
||||||
candidate_ph: List[int] = []
|
candidate_ph: List[int] = []
|
||||||
candidate_pl: List[int] = []
|
candidate_pl: List[int] = []
|
||||||
for i in range(max(k, n - flexible_zone), n):
|
for i in range(max(k, n - flexible_zone), n):
|
||||||
|
if np.isnan(high[i]) or np.isnan(low[i]):
|
||||||
|
continue # 跳过 NaN 点
|
||||||
|
|
||||||
right_avail = n - 1 - i
|
right_avail = n - 1 - i
|
||||||
if high[i] == np.max(high[i - k : i + right_avail + 1]):
|
# 使用对称的短窗口:左右各看 right_avail+1 天(至少1天)
|
||||||
|
# 但左边最多看 k 天(保证有足够数据)
|
||||||
|
left_look = min(k, max(right_avail + 1, 3)) # 至少看3天
|
||||||
|
left_start = max(0, i - left_look)
|
||||||
|
right_end = min(n, i + right_avail + 1)
|
||||||
|
|
||||||
|
# 在对称窗口内是最大/最小即为候选枢轴点
|
||||||
|
if high[i] == np.nanmax(high[left_start : right_end]):
|
||||||
candidate_ph.append(i)
|
candidate_ph.append(i)
|
||||||
if low[i] == np.min(low[i - k : i + right_avail + 1]):
|
if low[i] == np.nanmin(low[left_start : right_end]):
|
||||||
candidate_pl.append(i)
|
candidate_pl.append(i)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -232,21 +249,27 @@ def fit_pivot_line(
|
|||||||
pivot_values: np.ndarray,
|
pivot_values: np.ndarray,
|
||||||
mode: str = "upper",
|
mode: str = "upper",
|
||||||
min_points: int = 2,
|
min_points: int = 2,
|
||||||
|
outlier_threshold: float = 2.5,
|
||||||
|
max_iterations: int = 2,
|
||||||
) -> Tuple[float, float, np.ndarray]:
|
) -> Tuple[float, float, np.ndarray]:
|
||||||
"""
|
"""
|
||||||
枢轴点连线法:选择合适的枢轴点连成边界线
|
迭代离群点移除的枢轴点拟合算法
|
||||||
|
|
||||||
改进策略(2026-01-26):
|
改进策略(2026-01-26 v2):
|
||||||
- 使用多个枢轴点进行线性回归,而不是只选2个点
|
1. 先用所有点做初始拟合
|
||||||
- 将时间轴分为3段,每段选择最具代表性的点
|
2. 识别并移除偏离拟合线的"弱"点
|
||||||
- 上沿选最高点,下沿选最低点
|
3. 迭代直到收敛
|
||||||
- 使用选中的点进行线性回归,充分利用所有信息
|
|
||||||
|
对于上沿线:移除价格明显低于拟合线的点(弱高点)
|
||||||
|
对于下沿线:移除价格明显高于拟合线的点(弱低点)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
pivot_indices: 枢轴点的X坐标(索引)
|
pivot_indices: 枢轴点的X坐标(索引)
|
||||||
pivot_values: 枢轴点的Y值(价格)
|
pivot_values: 枢轴点的Y值(价格)
|
||||||
mode: "upper"(上沿) 或 "lower"(下沿)
|
mode: "upper"(上沿) 或 "lower"(下沿)
|
||||||
min_points: 最少需要的枢轴点数
|
min_points: 最少需要的枢轴点数(默认2,实际保留至少3个)
|
||||||
|
outlier_threshold: 离群点阈值(标准差倍数,默认2.5,更宽松)
|
||||||
|
max_iterations: 最大迭代次数(默认2,避免过度过滤)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(slope, intercept, selected_indices): 斜率、截距、选中的枢轴点索引
|
(slope, intercept, selected_indices): 斜率、截距、选中的枢轴点索引
|
||||||
@ -263,87 +286,82 @@ def fit_pivot_line(
|
|||||||
if n < 2:
|
if n < 2:
|
||||||
return 0.0, 0.0, np.array([])
|
return 0.0, 0.0, np.array([])
|
||||||
|
|
||||||
# 新策略:分段选择代表性点进行回归
|
# 初始化:所有点都参与
|
||||||
if n <= 4:
|
active_mask = np.ones(n, dtype=bool)
|
||||||
# 点数少,直接用所有点
|
min_keep = max(3, min_points) # 至少保留3个点
|
||||||
selected_mask = np.ones(n, dtype=bool)
|
|
||||||
else:
|
|
||||||
# 点数多,分3段选择
|
|
||||||
selected_mask = np.zeros(n, dtype=bool)
|
|
||||||
|
|
||||||
# 分3段
|
# 迭代离群点移除
|
||||||
segment_size = n // 3
|
for iteration in range(max_iterations):
|
||||||
if segment_size < 1:
|
active_indices = np.where(active_mask)[0]
|
||||||
segment_size = 1
|
if len(active_indices) <= min_keep:
|
||||||
|
break
|
||||||
|
|
||||||
segments = [
|
# 当前活跃点
|
||||||
range(0, min(segment_size, n)),
|
x_active = x_sorted[active_mask]
|
||||||
range(segment_size, min(2 * segment_size, n)),
|
y_active = y_sorted[active_mask]
|
||||||
range(2 * segment_size, n),
|
|
||||||
]
|
|
||||||
|
|
||||||
for seg in segments:
|
# 线性回归
|
||||||
if len(seg) == 0:
|
a, b = fit_line(x_active, y_active)
|
||||||
continue
|
|
||||||
|
|
||||||
seg_list = list(seg)
|
# 计算拟合值
|
||||||
seg_y = y_sorted[seg_list]
|
fitted_values = a * x_active + b
|
||||||
|
|
||||||
|
# 计算残差(方向性残差)
|
||||||
if mode == "upper":
|
if mode == "upper":
|
||||||
# 上沿:选该段最高点
|
# 上沿:残差 = 拟合值 - 实际值
|
||||||
best_idx_in_seg = np.argmax(seg_y)
|
# 正残差表示点在拟合线下方(弱高点)
|
||||||
|
residuals = fitted_values - y_active
|
||||||
else:
|
else:
|
||||||
# 下沿:选该段最低点
|
# 下沿:残差 = 实际值 - 拟合值
|
||||||
best_idx_in_seg = np.argmin(seg_y)
|
# 正残差表示点在拟合线上方(弱低点)
|
||||||
|
residuals = y_active - fitted_values
|
||||||
|
|
||||||
selected_mask[seg_list[best_idx_in_seg]] = True
|
# 计算标准差
|
||||||
|
std_residual = np.std(residuals)
|
||||||
|
if std_residual < 1e-10:
|
||||||
|
# 所有点几乎在一条线上,无需移除
|
||||||
|
break
|
||||||
|
|
||||||
# 获取选中的点
|
# 识别离群点(残差 > threshold * std)
|
||||||
selected_x = x_sorted[selected_mask]
|
threshold = outlier_threshold * std_residual
|
||||||
selected_y = y_sorted[selected_mask]
|
outlier_mask_active = residuals > threshold
|
||||||
selected_indices_sorted = np.where(selected_mask)[0]
|
|
||||||
|
if not np.any(outlier_mask_active):
|
||||||
|
# 没有离群点,收敛
|
||||||
|
break
|
||||||
|
|
||||||
|
# 找到最大离群点
|
||||||
|
max_outlier_idx_in_active = np.argmax(residuals)
|
||||||
|
|
||||||
|
# 检查是否确实是离群点
|
||||||
|
if residuals[max_outlier_idx_in_active] <= threshold:
|
||||||
|
break
|
||||||
|
|
||||||
|
# 检查移除后是否仍有足够的点
|
||||||
|
if np.sum(active_mask) <= min_keep:
|
||||||
|
break
|
||||||
|
|
||||||
|
# 在原始索引中标记移除
|
||||||
|
original_idx = active_indices[max_outlier_idx_in_active]
|
||||||
|
active_mask[original_idx] = False
|
||||||
|
|
||||||
|
# 最终拟合
|
||||||
|
selected_indices_sorted = np.where(active_mask)[0]
|
||||||
|
selected_x = x_sorted[active_mask]
|
||||||
|
selected_y = y_sorted[active_mask]
|
||||||
|
|
||||||
if len(selected_x) < 2:
|
if len(selected_x) < 2:
|
||||||
# 兜底:使用首尾两点
|
# 兜底:使用首尾两点
|
||||||
selected_mask = np.zeros(n, dtype=bool)
|
active_mask = np.zeros(n, dtype=bool)
|
||||||
selected_mask[0] = True
|
active_mask[0] = True
|
||||||
selected_mask[-1] = True
|
active_mask[-1] = True
|
||||||
selected_x = x_sorted[selected_mask]
|
selected_x = x_sorted[active_mask]
|
||||||
selected_y = y_sorted[selected_mask]
|
selected_y = y_sorted[active_mask]
|
||||||
selected_indices_sorted = np.where(selected_mask)[0]
|
selected_indices_sorted = np.where(active_mask)[0]
|
||||||
|
|
||||||
# 线性回归
|
# 线性回归
|
||||||
a, b = fit_line(selected_x, selected_y)
|
a, b = fit_line(selected_x, selected_y)
|
||||||
|
|
||||||
# 验证覆盖情况
|
|
||||||
fitted_all = a * x_sorted + b
|
|
||||||
tolerance = 0.03 # 3% 容差
|
|
||||||
|
|
||||||
if mode == "upper":
|
|
||||||
# 上沿:确保所有点在线下方或附近
|
|
||||||
violations = y_sorted > fitted_all + tolerance * np.mean(y_sorted)
|
|
||||||
if np.any(violations):
|
|
||||||
# 有点严重超出,添加全局最高点重新拟合
|
|
||||||
global_max_idx = np.argmax(y_sorted)
|
|
||||||
if not selected_mask[global_max_idx]:
|
|
||||||
selected_mask[global_max_idx] = True
|
|
||||||
selected_x = x_sorted[selected_mask]
|
|
||||||
selected_y = y_sorted[selected_mask]
|
|
||||||
selected_indices_sorted = np.where(selected_mask)[0]
|
|
||||||
a, b = fit_line(selected_x, selected_y)
|
|
||||||
else:
|
|
||||||
# 下沿:确保所有点在线上方或附近
|
|
||||||
violations = y_sorted < fitted_all - tolerance * np.mean(y_sorted)
|
|
||||||
if np.any(violations):
|
|
||||||
# 有点严重低于,添加全局最低点重新拟合
|
|
||||||
global_min_idx = np.argmin(y_sorted)
|
|
||||||
if not selected_mask[global_min_idx]:
|
|
||||||
selected_mask[global_min_idx] = True
|
|
||||||
selected_x = x_sorted[selected_mask]
|
|
||||||
selected_y = y_sorted[selected_mask]
|
|
||||||
selected_indices_sorted = np.where(selected_mask)[0]
|
|
||||||
a, b = fit_line(selected_x, selected_y)
|
|
||||||
|
|
||||||
# 返回原始索引
|
# 返回原始索引
|
||||||
selected_original = sort_idx[selected_indices_sorted]
|
selected_original = sort_idx[selected_indices_sorted]
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user