diff --git a/scripts/pipeline_converging_triangle.py b/scripts/pipeline_converging_triangle.py index be2ea0c..5ba39ac 100644 --- a/scripts/pipeline_converging_triangle.py +++ b/scripts/pipeline_converging_triangle.py @@ -10,6 +10,7 @@ python scripts/pipeline_converging_triangle.py python scripts/pipeline_converging_triangle.py --date 20260120 python scripts/pipeline_converging_triangle.py --show-details # 生成详情模式图片 + python scripts/pipeline_converging_triangle.py --plot-boundary-source close # 使用收盘价拟合边界线 """ from __future__ import annotations @@ -91,6 +92,12 @@ def main() -> None: action="store_true", help="运行前清空outputs文件夹", ) + parser.add_argument( + "--plot-boundary-source", + choices=["hl", "close"], + default="hl", + help="绘图时边界线拟合数据源: hl=高低价, close=收盘价(不影响检测)", + ) args = parser.parse_args() pipeline_start = time.time() @@ -109,6 +116,8 @@ def main() -> None: print(f"图表范围: 所有108只股票(包括不满足条件的)") else: print(f"图表范围: 仅满足收敛三角形条件的股票") + boundary_source_name = "收盘价" if args.plot_boundary_source == "close" else "高低价" + print(f"边界线拟合数据源: {boundary_source_name}") print("=" * 80) # ======================================================================== @@ -199,6 +208,8 @@ def main() -> None: cmd_args.append("--show-details") if args.all_stocks: cmd_args.append("--all-stocks") + if args.plot_boundary_source: + cmd_args.extend(["--plot-boundary-source", args.plot_boundary_source]) sys.argv = cmd_args diff --git a/scripts/plot_converging_triangles.py b/scripts/plot_converging_triangles.py index a508774..8e24aa2 100644 --- a/scripts/plot_converging_triangles.py +++ b/scripts/plot_converging_triangles.py @@ -33,6 +33,7 @@ sys.path.append(os.path.join(os.path.dirname(__file__), "..", "src")) from converging_triangle import ( ConvergingTriangleParams, + calc_fitting_adherence, detect_converging_triangle, fit_pivot_line_dispatch, line_y, @@ -120,6 +121,8 @@ def plot_triangle( display_window: int = 500, # 显示窗口大小 show_details: bool = False, # 是否显示详细调试信息 force_plot: bool = False, # 强制绘图(即使不满足三角形条件) + plot_boundary_source: str = "hl", # 边界线拟合数据源: "hl" | "close" + show_high_low: bool = False, # 是否显示日内高低价范围 ) -> None: """绘制单只股票的收敛三角形图""" @@ -183,6 +186,7 @@ def plot_triangle( # ======================================================================== result = None has_triangle = False + fitting_adherence_plot = 0.0 # 初始化贴合度 if has_enough_data: result = detect_converging_triangle( @@ -231,21 +235,32 @@ def plot_triangle( # 使用枢轴点连线法拟合边界线(与检测算法一致) # 注意:绘图用的是检测窗口数据,因此 window_start=0, window_end=n-1 + if plot_boundary_source == "close": + upper_fit_values = close_win[ph_idx] + lower_fit_values = close_win[pl_idx] + upper_all_prices = close_win + lower_all_prices = close_win + else: + upper_fit_values = high_win[ph_idx] + lower_fit_values = low_win[pl_idx] + upper_all_prices = high_win + lower_all_prices = low_win + a_u, b_u, selected_ph = fit_pivot_line_dispatch( pivot_indices=ph_idx, - pivot_values=high_win[ph_idx], + pivot_values=upper_fit_values, mode="upper", method=params.fitting_method, - all_prices=high_win, + all_prices=upper_all_prices, window_start=0, window_end=n - 1, ) a_l, b_l, selected_pl = fit_pivot_line_dispatch( pivot_indices=pl_idx, - pivot_values=low_win[pl_idx], + pivot_values=lower_fit_values, mode="lower", method=params.fitting_method, - all_prices=low_win, + all_prices=lower_all_prices, window_start=0, window_end=n - 1, ) @@ -266,6 +281,29 @@ def plot_triangle( # 三角形检测窗口的日期范围(用于标题) detect_dates = dates[valid_indices[detect_start:valid_end + 1]] + + # 如果使用收盘价拟合,重新计算贴合度(基于实际拟合线) + if plot_boundary_source == "close" and len(selected_ph) > 0 and len(selected_pl) > 0: + # 使用收盘价重新计算贴合度 + adherence_upper_close = calc_fitting_adherence( + pivot_indices=selected_ph_pos.astype(float), + pivot_values=close_win[selected_ph_pos], + slope=a_u, + intercept=b_u, + ) + adherence_lower_close = calc_fitting_adherence( + pivot_indices=selected_pl_pos.astype(float), + pivot_values=close_win[selected_pl_pos], + slope=a_l, + intercept=b_l, + ) + fitting_adherence_plot = (adherence_upper_close + adherence_lower_close) / 2.0 + else: + # 使用检测算法计算的贴合度 + fitting_adherence_plot = result.fitting_score if result else 0.0 + else: + # 无三角形时,贴合度为0 + fitting_adherence_plot = 0.0 # 创建图表 fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 8), @@ -273,11 +311,21 @@ def plot_triangle( # 主图:价格和趋势线(使用显示窗口数据) ax1.plot(x_display, display_close, linewidth=1.5, label='收盘价', color='black', alpha=0.7) + if show_high_low: + ax1.fill_between( + x_display, + display_low, + display_high, + color='gray', + alpha=0.12, + label='日内高低范围', + ) # 只在有三角形时绘制趋势线 if has_triangle and has_enough_data: - ax1.plot(xw_in_display, upper_line, linewidth=2, label='上沿', color='red', linestyle='--') - ax1.plot(xw_in_display, lower_line, linewidth=2, label='下沿', color='green', linestyle='--') + boundary_label = "收盘价" if plot_boundary_source == "close" else "高低价" + ax1.plot(xw_in_display, upper_line, linewidth=2, label=f'上沿({boundary_label})', color='red', linestyle='--') + ax1.plot(xw_in_display, lower_line, linewidth=2, label=f'下沿({boundary_label})', color='green', linestyle='--') ax1.axvline(len(display_close) - 1, color='gray', linestyle=':', linewidth=1, alpha=0.5) @@ -285,11 +333,23 @@ def plot_triangle( # 详细模式:显示拟合点(仅在 show_details=True 且有三角形时) # ======================================================================== if show_details and has_triangle and has_enough_data: + # 根据数据源选择枢轴点的Y坐标 + if plot_boundary_source == "close": + ph_pivot_y = close_win[ph_idx] + pl_pivot_y = close_win[pl_idx] + selected_ph_y = close_win[selected_ph_pos] if len(selected_ph_pos) > 0 else np.array([]) + selected_pl_y = close_win[selected_pl_pos] if len(selected_pl_pos) > 0 else np.array([]) + else: + ph_pivot_y = high_win[ph_idx] + pl_pivot_y = low_win[pl_idx] + selected_ph_y = high_win[selected_ph_pos] if len(selected_ph_pos) > 0 else np.array([]) + selected_pl_y = low_win[selected_pl_pos] if len(selected_pl_pos) > 0 else np.array([]) + # 标注所有枢轴点(用于查看拐点分布) if len(ph_display_idx) > 0: ax1.scatter( ph_display_idx, - high_win[ph_idx], + ph_pivot_y, marker='x', s=60, color='red', @@ -300,7 +360,7 @@ def plot_triangle( if len(pl_display_idx) > 0: ax1.scatter( pl_display_idx, - low_win[pl_idx], + pl_pivot_y, marker='x', s=60, color='green', @@ -313,7 +373,7 @@ def plot_triangle( if len(selected_ph_display) >= 2: ax1.scatter( selected_ph_display, - high_win[selected_ph_pos], + selected_ph_y, marker='o', s=120, facecolors='none', @@ -325,7 +385,7 @@ def plot_triangle( if len(selected_pl_display) >= 2: ax1.scatter( selected_pl_display, - low_win[selected_pl_pos], + selected_pl_y, marker='o', s=120, facecolors='none', @@ -356,6 +416,14 @@ def plot_triangle( else: utilization_penalty = 1.0 + # 选择显示的贴合度:如果使用收盘价拟合,显示重新计算的贴合度 + if plot_boundary_source == "close" and has_triangle and has_enough_data: + display_fitting_score = fitting_adherence_plot + fitting_note = f"拟合贴合度(收盘价): {display_fitting_score:.3f}" + else: + display_fitting_score = result.fitting_score + fitting_note = f"拟合贴合度: {display_fitting_score:.3f}" + ax1.set_title( f"{stock_code} {stock_name} - 收敛三角形 (检测窗口: {detect_dates[0]} ~ {detect_dates[-1]})\n" f"显示范围: {display_dates[0]} ~ {display_dates[-1]} ({len(display_dates)}个交易日) " @@ -364,7 +432,7 @@ def plot_triangle( f"放量确认: {'是' if result.volume_confirmed else '否' if result.volume_confirmed is False else '-'}\n" f"强度分: {strength:.3f} " f"(价格: {price_score:.3f}×50% + 收敛: {result.convergence_score:.3f}×15% + " - f"成交量: {result.volume_score:.3f}×10% + 拟合贴合度: {result.fitting_score:.3f}×10% + " + f"成交量: {result.volume_score:.3f}×10% + {fitting_note}×10% + " f"边界利用率: {boundary_util:.3f}×15%) × 利用率惩罚: {utilization_penalty:.2f}", fontsize=11, pad=10 ) @@ -424,6 +492,17 @@ def main() -> None: action="store_true", help="显示详细调试信息(枢轴点、拟合点、分段线等)", ) + parser.add_argument( + "--plot-boundary-source", + choices=["hl", "close"], + default="hl", + help="绘图时边界线拟合数据源: hl=高低价, close=收盘价(不影响检测)", + ) + parser.add_argument( + "--show-high-low", + action="store_true", + help="显示日内高低价范围(仅影响图形展示)", + ) parser.add_argument( "--all-stocks", action="store_true", @@ -439,6 +518,8 @@ def main() -> None: # 确定是否显示详细信息(命令行参数优先) show_details = args.show_details if hasattr(args, 'show_details') else SHOW_CHART_DETAILS all_stocks = args.all_stocks if hasattr(args, 'all_stocks') else False + plot_boundary_source = args.plot_boundary_source if hasattr(args, 'plot_boundary_source') else "hl" + show_high_low = args.show_high_low if hasattr(args, 'show_high_low') else False print("=" * 70) print("收敛三角形图表生成") @@ -565,6 +646,8 @@ def main() -> None: display_window=DISPLAY_WINDOW, # 从配置文件读取 show_details=show_details, # 传递详细模式参数 force_plot=all_stocks, # 在all_stocks模式下强制绘图 + plot_boundary_source=plot_boundary_source, + show_high_low=show_high_low, ) except Exception as e: print(f" [错误] {stock_code} {stock_name}: {e}")