- Added support for a detailed chart mode in plot_converging_triangles.py, allowing users to visualize all pivot points and fitting lines. - Improved pivot fitting logic to utilize multiple representative points, enhancing detection accuracy and reducing false positives. - Introduced a new real-time detection mode with flexible zone parameters for better responsiveness in stock analysis. - Updated README.md and USAGE.md to reflect new features and usage instructions. - Added multiple documentation files detailing recent improvements, including pivot point fitting and visualization enhancements. - Cleaned up and archived outdated scripts to streamline the project structure.
717 lines
21 KiB
Markdown
717 lines
21 KiB
Markdown
# 枢轴点分段选择算法详解
|
||
|
||
**日期**: 2026-01-26
|
||
**文件**: `src/converging_triangle.py` - `fit_pivot_line()` 函数
|
||
|
||
---
|
||
|
||
## 📋 目录
|
||
|
||
1. [算法概述](#算法概述)
|
||
2. [为什么需要分段](#为什么需要分段)
|
||
3. [分段算法详解](#分段算法详解)
|
||
4. [独立分段机制](#独立分段机制)
|
||
5. [代码实现](#代码实现)
|
||
6. [实际案例分析](#实际案例分析)
|
||
7. [边界情况处理](#边界情况处理)
|
||
8. [可视化说明](#可视化说明)
|
||
|
||
---
|
||
|
||
## 算法概述
|
||
|
||
### 核心思想
|
||
|
||
对于识别出的所有枢轴点,我们不是简单地用所有点或仅用首尾两点来拟合趋势线,而是:
|
||
|
||
1. **分段策略**:将枢轴点按时间顺序分成 **3 个时间段**
|
||
2. **代表点选择**:从每段中选出 **最极端的点**(上沿选最高,下沿选最低)
|
||
3. **线性回归**:用这 3 个代表点进行线性回归,拟合趋势线
|
||
|
||
### 触发条件
|
||
|
||
```python
|
||
if 枢轴点数量 > 4:
|
||
使用分段策略(3段,每段1个代表点)
|
||
else:
|
||
使用全部枢轴点
|
||
```
|
||
|
||
---
|
||
|
||
## 为什么需要分段
|
||
|
||
### 问题1: 仅用两点的缺陷
|
||
|
||
如果只用首尾两点画线:
|
||
|
||
```
|
||
价格
|
||
^
|
||
| * * ← 两个高点
|
||
| \ /
|
||
| \ /
|
||
| X ← 两点连线
|
||
| / \
|
||
| / \
|
||
| * * ← 被忽略的中间高点
|
||
└──────────────> 时间
|
||
```
|
||
|
||
**问题**:
|
||
- ❌ 中间的极值点被忽略
|
||
- ❌ 线可能不是真正的边界
|
||
- ❌ 容易被噪声影响(首尾点恰好是噪声)
|
||
|
||
### 问题2: 使用全部点的问题
|
||
|
||
如果用所有枢轴点进行回归:
|
||
|
||
```
|
||
价格
|
||
^
|
||
| * * * * * * ← 6个高点,但分布不均
|
||
| └─┬─┘ └──┬──┘
|
||
| 前期 后期
|
||
| 密集 稀疏
|
||
└──────────────> 时间
|
||
```
|
||
|
||
**问题**:
|
||
- ❌ 某些时间段的点过多,权重过大
|
||
- ❌ 回归结果偏向点密集的区域
|
||
- ❌ 不能均衡反映整个周期的趋势
|
||
|
||
### 解决方案: 时间均衡分段
|
||
|
||
```
|
||
价格
|
||
^
|
||
| * * * * * * ← 6个高点
|
||
| └─┬─┘ └┬┘ └┬┘
|
||
| 第1段 第2段 第3段
|
||
| ↓ ↓ ↓
|
||
| * * * ← 每段选1个最高点(3个拟合点)
|
||
└──────────────> 时间
|
||
```
|
||
|
||
**优点**:
|
||
- ✅ 时间均衡(前、中、后都有代表点)
|
||
- ✅ 代表性强(每段选最极端的点)
|
||
- ✅ 稳健性好(不易被局部噪声影响)
|
||
- ✅ 覆盖性好(确保边界线包络所有点)
|
||
|
||
---
|
||
|
||
## 分段算法详解
|
||
|
||
### 第1步: 排序枢轴点
|
||
|
||
```python
|
||
# 按时间顺序排序(从早到晚)
|
||
sort_idx = np.argsort(pivot_indices)
|
||
x_sorted = pivot_indices[sort_idx] # 时间索引
|
||
y_sorted = pivot_values[sort_idx] # 价格值
|
||
```
|
||
|
||
**示例**:
|
||
```
|
||
原始枢轴点(未排序):
|
||
索引: [50, 10, 80, 30, 90, 60]
|
||
价格: [95, 100, 98, 96, 92, 94]
|
||
|
||
排序后:
|
||
索引: [10, 30, 50, 60, 80, 90] ← 时间从早到晚
|
||
价格: [100, 96, 95, 94, 98, 92]
|
||
```
|
||
|
||
### 第2步: 计算分段大小
|
||
|
||
```python
|
||
n = len(x_sorted) # 总点数,例如 n = 6
|
||
segment_size = n // 3 # 整除,segment_size = 6 // 3 = 2
|
||
```
|
||
|
||
**分段规则**:
|
||
```
|
||
总数 n → segment_size → 分段方式
|
||
─────────────────────────────
|
||
n = 5 → 1 → [0:1], [1:2], [2:5] (1,1,3个点)
|
||
n = 6 → 2 → [0:2], [2:4], [4:6] (2,2,2个点)
|
||
n = 7 → 2 → [0:2], [2:4], [4:7] (2,2,3个点)
|
||
n = 8 → 2 → [0:2], [2:4], [4:8] (2,2,4个点)
|
||
n = 9 → 3 → [0:3], [3:6], [6:9] (3,3,3个点)
|
||
n = 10 → 3 → [0:3], [3:6], [6:10] (3,3,4个点)
|
||
```
|
||
|
||
**重要特性**:
|
||
- 第3段可能比前两段多点(余数分配给第3段)
|
||
- 确保每段至少有1个点
|
||
- 第3段包含最新的数据点
|
||
|
||
### 第3步: 定义三个时间段
|
||
|
||
```python
|
||
segments = [
|
||
range(0, segment_size), # 第1段: [0, segment_size)
|
||
range(segment_size, 2 * segment_size), # 第2段: [segment_size, 2*segment_size)
|
||
range(2 * segment_size, n), # 第3段: [2*segment_size, n)
|
||
]
|
||
```
|
||
|
||
**以 n=6, segment_size=2 为例**:
|
||
|
||
```
|
||
索引位置: 0 1 | 2 3 | 4 5
|
||
[ 第1段 | 第2段 | 第3段 ]
|
||
时间: 早期 中期 晚期
|
||
范围: [0:2) [2:4) [4:6)
|
||
点数: 2个 2个 2个
|
||
```
|
||
|
||
**以 n=7, segment_size=2 为例**:
|
||
|
||
```
|
||
索引位置: 0 1 | 2 3 | 4 5 6
|
||
[ 第1段 | 第2段 | 第3段 ]
|
||
时间: 早期 中期 晚期
|
||
范围: [0:2) [2:4) [4:7)
|
||
点数: 2个 2个 3个 ← 第3段多1个
|
||
```
|
||
|
||
### 第4步: 从每段选择极值点
|
||
|
||
```python
|
||
for seg in segments:
|
||
seg_list = list(seg)
|
||
seg_y = y_sorted[seg_list] # 该段的所有价格
|
||
|
||
if mode == "upper":
|
||
# 上沿:选该段最高点
|
||
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
|
||
```
|
||
|
||
**上沿示例(选最高点)**:
|
||
|
||
```
|
||
第1段 [0:2):
|
||
索引0: 价格100 ← 最高 ✓
|
||
索引1: 价格96
|
||
|
||
第2段 [2:4):
|
||
索引2: 价格95
|
||
索引3: 价格94
|
||
|
||
第3段 [4:6):
|
||
索引4: 价格98 ← 最高 ✓
|
||
索引5: 价格92
|
||
|
||
结果: 选中索引 0, 3, 4 (没选错,第2段最高是索引2的95)
|
||
```
|
||
|
||
等等,让我重新计算:
|
||
|
||
```
|
||
排序后的数据:
|
||
索引: [10, 30, 50, 60, 80, 90]
|
||
价格: [100, 96, 95, 94, 98, 92]
|
||
位置: 0 1 2 3 4 5
|
||
|
||
第1段 [0:2) - 位置0,1:
|
||
位置0: 价格100 ← 最高 ✓
|
||
位置1: 价格96
|
||
|
||
第2段 [2:4) - 位置2,3:
|
||
位置2: 价格95 ← 最高 ✓
|
||
位置3: 价格94
|
||
|
||
第3段 [4:6) - 位置4,5:
|
||
位置4: 价格98 ← 最高 ✓
|
||
位置5: 价格92
|
||
|
||
选中的拟合点:
|
||
位置0 (时间10, 价格100)
|
||
位置2 (时间50, 价格95)
|
||
位置4 (时间80, 价格98)
|
||
```
|
||
|
||
**下沿示例(选最低点)**:
|
||
|
||
```
|
||
排序后的数据:
|
||
索引: [15, 35, 55, 75]
|
||
价格: [92, 90, 88, 86]
|
||
位置: 0 1 2 3
|
||
|
||
n = 4 ≤ 4,不分段,全部使用 ✓
|
||
```
|
||
|
||
---
|
||
|
||
## 独立分段机制
|
||
|
||
### 关键点:高点和低点分别独立处理
|
||
|
||
```python
|
||
# 伪代码展示独立性
|
||
高点枢轴点 = [6个] → 分3段 → 选3个拟合点
|
||
低点枢轴点 = [4个] → 不分段 → 全部4个拟合点
|
||
|
||
# 两者完全独立,互不影响
|
||
```
|
||
|
||
### 为什么要独立分段?
|
||
|
||
**原因1: 时间分布不同**
|
||
|
||
```
|
||
价格
|
||
^
|
||
| H H H H H H ← 6个高点(分布较均匀)
|
||
| \ / | \ /
|
||
| \ / | \ /
|
||
| \ / | \ /
|
||
| X | X
|
||
| / \ | / \
|
||
| / \ | / \
|
||
| L L L L ← 4个低点(集中在两侧)
|
||
└───────────────────────> 时间
|
||
```
|
||
|
||
- 高点和低点出现的时间点不同
|
||
- 如果混合分段,会破坏各自的时间均衡性
|
||
|
||
**原因2: 数量可能不同**
|
||
|
||
```
|
||
高点: 6个 > 4 → 需要分段
|
||
低点: 4个 ≤ 4 → 不需要分段
|
||
```
|
||
|
||
- 如果混合判断,要么都分段,要么都不分段
|
||
- 独立判断更灵活,更合理
|
||
|
||
**原因3: 含义不同**
|
||
|
||
- 高点定义上沿(压力线)
|
||
- 低点定义下沿(支撑线)
|
||
- 两条线的拟合完全独立
|
||
|
||
### 独立分段的实现
|
||
|
||
```python
|
||
# 1. 高点独立处理
|
||
if len(高点枢轴) > 4:
|
||
高点分3段 → 选3个高点拟合上沿线
|
||
else:
|
||
全部高点拟合上沿线
|
||
|
||
# 2. 低点独立处理(完全独立的逻辑)
|
||
if len(低点枢轴) > 4:
|
||
低点分3段 → 选3个低点拟合下沿线
|
||
else:
|
||
全部低点拟合下沿线
|
||
```
|
||
|
||
---
|
||
|
||
## 代码实现
|
||
|
||
### 完整代码(带详细注释)
|
||
|
||
```python
|
||
def fit_pivot_line(
|
||
pivot_indices: np.ndarray, # 枢轴点的时间索引
|
||
pivot_values: np.ndarray, # 枢轴点的价格
|
||
mode: str = "upper", # "upper" 或 "lower"
|
||
min_points: int = 2,
|
||
) -> Tuple[float, float, np.ndarray]:
|
||
"""
|
||
拟合枢轴点趋势线(使用分段选择策略)
|
||
|
||
策略:
|
||
- 如果枢轴点 > 4:分3段,每段选1个极值点(共3个拟合点)
|
||
- 如果枢轴点 ≤ 4:全部使用
|
||
|
||
Returns:
|
||
(斜率a, 截距b, 选中的枢轴点索引)
|
||
"""
|
||
|
||
# ─────────────────────────────────────────────────────────
|
||
# 第1步:基本检查和排序
|
||
# ─────────────────────────────────────────────────────────
|
||
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 n < 2:
|
||
return 0.0, 0.0, np.array([])
|
||
|
||
# ─────────────────────────────────────────────────────────
|
||
# 第2步:决定是否分段
|
||
# ─────────────────────────────────────────────────────────
|
||
if n <= 4:
|
||
# 点数少,全部使用
|
||
selected_mask = np.ones(n, dtype=bool)
|
||
else:
|
||
# 点数多,使用分段策略
|
||
selected_mask = np.zeros(n, dtype=bool)
|
||
|
||
# 计算每段大小
|
||
segment_size = n // 3
|
||
if segment_size < 1:
|
||
segment_size = 1
|
||
|
||
# 定义三个时间段
|
||
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:
|
||
if len(seg) == 0:
|
||
continue
|
||
|
||
seg_list = list(seg)
|
||
seg_y = y_sorted[seg_list]
|
||
|
||
if mode == "upper":
|
||
# 上沿:选该段最高点
|
||
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
|
||
|
||
# ─────────────────────────────────────────────────────────
|
||
# 第4步:提取选中的点
|
||
# ─────────────────────────────────────────────────────────
|
||
selected_x = x_sorted[selected_mask]
|
||
selected_y = y_sorted[selected_mask]
|
||
selected_indices_sorted = np.where(selected_mask)[0]
|
||
|
||
# 保底:至少选首尾两点
|
||
if len(selected_x) < 2:
|
||
selected_mask = np.zeros(n, dtype=bool)
|
||
selected_mask[0] = True
|
||
selected_mask[-1] = True
|
||
selected_x = x_sorted[selected_mask]
|
||
selected_y = y_sorted[selected_mask]
|
||
selected_indices_sorted = np.where(selected_mask)[0]
|
||
|
||
# ─────────────────────────────────────────────────────────
|
||
# 第5步:线性回归
|
||
# ─────────────────────────────────────────────────────────
|
||
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]
|
||
|
||
return float(a), float(b), selected_original
|
||
```
|
||
|
||
---
|
||
|
||
## 实际案例分析
|
||
|
||
### 案例1: SZ002343 慈文传媒
|
||
|
||
**高点枢轴点(6个)**:
|
||
|
||
```
|
||
时间索引: [2025-05-12, 2025-09-23, 2025-11-04, 2025-11-26, 2025-12-09, 2026-01-13]
|
||
价格: [9.50, 9.36, 10.07, 9.63, 9.21, 8.92]
|
||
位置: 0 1 2 3 4 5
|
||
```
|
||
|
||
**分段处理**:
|
||
|
||
```
|
||
n = 6 > 4,需要分段
|
||
segment_size = 6 // 3 = 2
|
||
|
||
第1段 [0:2) - 位置0,1(2025-05到2025-09):
|
||
位置0: 9.50
|
||
位置1: 9.36
|
||
→ 选最高: 位置0, 价格9.50 ✓
|
||
|
||
第2段 [2:4) - 位置2,3(2025-11):
|
||
位置2: 10.07 ← 最高 ✓
|
||
位置3: 9.63
|
||
→ 选最高: 位置2, 价格10.07 ✓
|
||
|
||
第3段 [4:6) - 位置4,5(2025-12到2026-01):
|
||
位置4: 9.21
|
||
位置5: 8.92
|
||
→ 选最高: 位置4, 价格9.21 ✓
|
||
|
||
拟合点(3个):
|
||
2025-05-12: 9.50 (早期高点)
|
||
2025-11-04: 10.07 (中期高点) ← 最高点
|
||
2025-12-09: 9.21 (晚期高点)
|
||
```
|
||
|
||
**低点枢轴点(4个)**:
|
||
|
||
```
|
||
时间索引: [2025-08-08, 2025-12-11, 2025-12-30, 2026-01-20]
|
||
价格: [5.17, 7.37, 7.42, 6.87]
|
||
位置: 0 1 2 3
|
||
```
|
||
|
||
**不分段处理**:
|
||
|
||
```
|
||
n = 4 ≤ 4,全部使用
|
||
|
||
拟合点(4个):
|
||
2025-08-08: 5.17 ✓
|
||
2025-12-11: 7.37 ✓
|
||
2025-12-30: 7.42 ✓
|
||
2026-01-20: 6.87 ✓
|
||
```
|
||
|
||
**结果对比**:
|
||
|
||
| 项目 | 高点枢轴 | 低点枢轴 |
|
||
|------|---------|---------|
|
||
| 总数 | 6个 | 4个 |
|
||
| 是否分段 | 是(>4) | 否(≤4) |
|
||
| 拟合点数 | 3个 | 4个 |
|
||
| 时间跨度 | 2025-05 → 2026-01 | 2025-08 → 2026-01 |
|
||
|
||
---
|
||
|
||
## 边界情况处理
|
||
|
||
### 情况1: 点数恰好等于4
|
||
|
||
```python
|
||
n = 4 # 不分段
|
||
selected_mask = np.ones(4, dtype=bool) # 全部选中
|
||
```
|
||
|
||
**理由**:
|
||
- 4个点已经足够稳定
|
||
- 分3段会导致某些段只有1个点
|
||
- 全部使用能获得更好的拟合效果
|
||
|
||
### 情况2: 点数为5
|
||
|
||
```python
|
||
n = 5 > 4 # 需要分段
|
||
segment_size = 5 // 3 = 1
|
||
|
||
第1段: [0:1) → 1个点 → 选1个
|
||
第2段: [1:2) → 1个点 → 选1个
|
||
第3段: [2:5) → 3个点 → 选最极值的1个
|
||
|
||
拟合点: 3个
|
||
```
|
||
|
||
### 情况3: 点数很多(如10个)
|
||
|
||
```python
|
||
n = 10 > 4 # 需要分段
|
||
segment_size = 10 // 3 = 3
|
||
|
||
第1段: [0:3) → 3个点 → 选最极值的1个
|
||
第2段: [3:6) → 3个点 → 选最极值的1个
|
||
第3段: [6:10) → 4个点 → 选最极值的1个
|
||
|
||
拟合点: 3个
|
||
```
|
||
|
||
**效果**:
|
||
- ✅ 无论点数多少,最终都是3个代表点
|
||
- ✅ 时间均衡(前1/3、中1/3、后1/3)
|
||
- ✅ 代表性强(每段最极端)
|
||
|
||
### 情况4: 分段后某段为空
|
||
|
||
```python
|
||
for seg in segments:
|
||
if len(seg) == 0: # 跳过空段
|
||
continue
|
||
# 处理非空段...
|
||
```
|
||
|
||
**何时发生**:
|
||
- 理论上不会发生(`segment_size ≥ 1`)
|
||
- 但代码仍然防御性地检查
|
||
|
||
### 情况5: 选中点少于2个
|
||
|
||
```python
|
||
if len(selected_x) < 2:
|
||
# 保底方案:强制选首尾两点
|
||
selected_mask[0] = True
|
||
selected_mask[-1] = True
|
||
```
|
||
|
||
**何时触发**:
|
||
- 极端异常情况(理论上不应发生)
|
||
- 确保至少有两点可以画线
|
||
|
||
---
|
||
|
||
## 可视化说明
|
||
|
||
### 图表元素对应关系
|
||
|
||
在详细模式(`--show-details`)下,图表显示:
|
||
|
||
```
|
||
图表元素 对应内容
|
||
─────────────────────────────────────────────
|
||
浅红色小实心圆(6个) 所有高点枢轴点
|
||
深红色大空心圆(3个) 上沿拟合点(从6个中选出)
|
||
红色点划竖线 高点分段边界
|
||
- "高1|2" 标签 第1段和第2段分界
|
||
- "高2|3" 标签 第2段和第3段分界
|
||
|
||
浅绿色小实心圆(4个) 所有低点枢轴点
|
||
深绿色大空心圆(4个) 下沿拟合点(全部使用,因≤4)
|
||
(无绿色竖线) 低点不分段(≤4)
|
||
```
|
||
|
||
### 分段线的位置
|
||
|
||
**分段边界的时间索引**:
|
||
|
||
```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
|
||
2. **分段数量**: 固定3段
|
||
3. **分段方式**: 时间均分(每段 `n//3` 个点)
|
||
4. **选择策略**: 每段选1个极值点
|
||
5. **独立性**: 高点和低点各自独立分段
|
||
6. **保底机制**: 覆盖性验证 + 全局极值保护
|
||
|
||
### 算法优势
|
||
|
||
| 优势 | 说明 |
|
||
|------|------|
|
||
| **时间均衡** | 前、中、后三个时期都有代表点 |
|
||
| **代表性强** | 每段选最极端的点,确保边界性 |
|
||
| **抗噪性好** | 不易被局部密集点影响 |
|
||
| **稳健性高** | 多点回归比两点连线更稳定 |
|
||
| **可扩展性** | 点数增加时仍保持3个拟合点 |
|
||
|
||
### 与其他方法的对比
|
||
|
||
| 方法 | 优点 | 缺点 |
|
||
|------|------|------|
|
||
| **两点连线** | 简单快速 | 忽略中间点,易受噪声影响 |
|
||
| **全部点回归** | 利用所有信息 | 权重不均,点密集区域主导 |
|
||
| **分段选择(当前)** | 时间均衡,代表性强 | 略复杂,需要分段逻辑 |
|
||
| **滑动窗口** | 平滑效果好 | 计算复杂,参数敏感 |
|
||
|
||
---
|
||
|
||
## 参考资料
|
||
|
||
- [枢轴点拟合改进.md](./2026-01-26_枢轴点拟合改进.md) - 改进历程
|
||
- [图表详细模式功能.md](./2026-01-26_图表详细模式功能.md) - 可视化说明
|
||
- [converging_triangle.py](../src/converging_triangle.py) - 源代码实现
|
||
|
||
---
|
||
|
||
**文档版本**: v1.0
|
||
**最后更新**: 2026-01-26
|
||
|