# 枢轴点分段选择算法详解 **日期**: 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