- 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.
21 KiB
21 KiB
枢轴点分段选择算法详解
日期: 2026-01-26
文件: src/converging_triangle.py - fit_pivot_line() 函数
📋 目录
算法概述
核心思想
对于识别出的所有枢轴点,我们不是简单地用所有点或仅用首尾两点来拟合趋势线,而是:
- 分段策略:将枢轴点按时间顺序分成 3 个时间段
- 代表点选择:从每段中选出 最极端的点(上沿选最高,下沿选最低)
- 线性回归:用这 3 个代表点进行线性回归,拟合趋势线
触发条件
if 枢轴点数量 > 4:
使用分段策略(3段,每段1个代表点)
else:
使用全部枢轴点
为什么需要分段
问题1: 仅用两点的缺陷
如果只用首尾两点画线:
价格
^
| * * ← 两个高点
| \ /
| \ /
| X ← 两点连线
| / \
| / \
| * * ← 被忽略的中间高点
└──────────────> 时间
问题:
- ❌ 中间的极值点被忽略
- ❌ 线可能不是真正的边界
- ❌ 容易被噪声影响(首尾点恰好是噪声)
问题2: 使用全部点的问题
如果用所有枢轴点进行回归:
价格
^
| * * * * * * ← 6个高点,但分布不均
| └─┬─┘ └──┬──┘
| 前期 后期
| 密集 稀疏
└──────────────> 时间
问题:
- ❌ 某些时间段的点过多,权重过大
- ❌ 回归结果偏向点密集的区域
- ❌ 不能均衡反映整个周期的趋势
解决方案: 时间均衡分段
价格
^
| * * * * * * ← 6个高点
| └─┬─┘ └┬┘ └┬┘
| 第1段 第2段 第3段
| ↓ ↓ ↓
| * * * ← 每段选1个最高点(3个拟合点)
└──────────────> 时间
优点:
- ✅ 时间均衡(前、中、后都有代表点)
- ✅ 代表性强(每段选最极端的点)
- ✅ 稳健性好(不易被局部噪声影响)
- ✅ 覆盖性好(确保边界线包络所有点)
分段算法详解
第1步: 排序枢轴点
# 按时间顺序排序(从早到晚)
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步: 计算分段大小
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步: 定义三个时间段
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步: 从每段选择极值点
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,不分段,全部使用 ✓
独立分段机制
关键点:高点和低点分别独立处理
# 伪代码展示独立性
高点枢轴点 = [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: 含义不同
- 高点定义上沿(压力线)
- 低点定义下沿(支撑线)
- 两条线的拟合完全独立
独立分段的实现
# 1. 高点独立处理
if len(高点枢轴) > 4:
高点分3段 → 选3个高点拟合上沿线
else:
全部高点拟合上沿线
# 2. 低点独立处理(完全独立的逻辑)
if len(低点枢轴) > 4:
低点分3段 → 选3个低点拟合下沿线
else:
全部低点拟合下沿线
代码实现
完整代码(带详细注释)
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
n = 4 # 不分段
selected_mask = np.ones(4, dtype=bool) # 全部选中
理由:
- 4个点已经足够稳定
- 分3段会导致某些段只有1个点
- 全部使用能获得更好的拟合效果
情况2: 点数为5
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个)
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: 分段后某段为空
for seg in segments:
if len(seg) == 0: # 跳过空段
continue
# 处理非空段...
何时发生:
- 理论上不会发生(
segment_size ≥ 1) - 但代码仍然防御性地检查
情况5: 选中点少于2个
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)
分段线的位置
分段边界的时间索引:
# 以高点为例,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
代码实现:
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', ...)
总结
分段策略的核心要点
- 触发条件: 枢轴点 > 4
- 分段数量: 固定3段
- 分段方式: 时间均分(每段
n//3个点) - 选择策略: 每段选1个极值点
- 独立性: 高点和低点各自独立分段
- 保底机制: 覆盖性验证 + 全局极值保护
算法优势
| 优势 | 说明 |
|---|---|
| 时间均衡 | 前、中、后三个时期都有代表点 |
| 代表性强 | 每段选最极端的点,确保边界性 |
| 抗噪性好 | 不易被局部密集点影响 |
| 稳健性高 | 多点回归比两点连线更稳定 |
| 可扩展性 | 点数增加时仍保持3个拟合点 |
与其他方法的对比
| 方法 | 优点 | 缺点 |
|---|---|---|
| 两点连线 | 简单快速 | 忽略中间点,易受噪声影响 |
| 全部点回归 | 利用所有信息 | 权重不均,点密集区域主导 |
| 分段选择(当前) | 时间均衡,代表性强 | 略复杂,需要分段逻辑 |
| 滑动窗口 | 平滑效果好 | 计算复杂,参数敏感 |
参考资料
- 枢轴点拟合改进.md - 改进历程
- 图表详细模式功能.md - 可视化说明
- converging_triangle.py - 源代码实现
文档版本: v1.0
最后更新: 2026-01-26