增强绘图功能:支持自定义边界线拟合数据源

新增功能:
- 添加 --plot-boundary-source 参数,允许选择边界线拟合数据源(高低价/收盘价)
- 添加 --show-high-low 参数,可选显示日内高低价范围
- 当使用收盘价拟合时,自动重新计算贴合度指标
- 图例自动标注当前使用的数据源
- 详细模式下,枢轴点标注位置根据数据源自适应调整

技术细节:
- 检测算法仍使用高低价(保持一致性)
- 新参数仅影响图表展示,不影响检测结果
- 贴合度计算逻辑根据数据源动态调整

文件修改:
- scripts/pipeline_converging_triangle.py: 添加参数支持和参数传递
- scripts/plot_converging_triangles.py: 实现核心绘图逻辑增强

使用示例:
python scripts/pipeline_converging_triangle.py --plot-boundary-source close
python scripts/plot_converging_triangles.py --plot-boundary-source close --show-high-low
This commit is contained in:
褚宏光 2026-01-28 16:35:54 +08:00
parent 24652b5790
commit e5788b8811
2 changed files with 105 additions and 11 deletions

View File

@ -10,6 +10,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 # 生成详情模式图片 python scripts/pipeline_converging_triangle.py --show-details # 生成详情模式图片
python scripts/pipeline_converging_triangle.py --plot-boundary-source close # 使用收盘价拟合边界线
""" """
from __future__ import annotations from __future__ import annotations
@ -91,6 +92,12 @@ def main() -> None:
action="store_true", action="store_true",
help="运行前清空outputs文件夹", help="运行前清空outputs文件夹",
) )
parser.add_argument(
"--plot-boundary-source",
choices=["hl", "close"],
default="hl",
help="绘图时边界线拟合数据源: hl=高低价, close=收盘价(不影响检测)",
)
args = parser.parse_args() args = parser.parse_args()
pipeline_start = time.time() pipeline_start = time.time()
@ -109,6 +116,8 @@ def main() -> None:
print(f"图表范围: 所有108只股票包括不满足条件的") print(f"图表范围: 所有108只股票包括不满足条件的")
else: else:
print(f"图表范围: 仅满足收敛三角形条件的股票") print(f"图表范围: 仅满足收敛三角形条件的股票")
boundary_source_name = "收盘价" if args.plot_boundary_source == "close" else "高低价"
print(f"边界线拟合数据源: {boundary_source_name}")
print("=" * 80) print("=" * 80)
# ======================================================================== # ========================================================================
@ -199,6 +208,8 @@ def main() -> None:
cmd_args.append("--show-details") cmd_args.append("--show-details")
if args.all_stocks: if args.all_stocks:
cmd_args.append("--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 sys.argv = cmd_args

View File

@ -33,6 +33,7 @@ sys.path.append(os.path.join(os.path.dirname(__file__), "..", "src"))
from converging_triangle import ( from converging_triangle import (
ConvergingTriangleParams, ConvergingTriangleParams,
calc_fitting_adherence,
detect_converging_triangle, detect_converging_triangle,
fit_pivot_line_dispatch, fit_pivot_line_dispatch,
line_y, line_y,
@ -120,6 +121,8 @@ def plot_triangle(
display_window: int = 500, # 显示窗口大小 display_window: int = 500, # 显示窗口大小
show_details: bool = False, # 是否显示详细调试信息 show_details: bool = False, # 是否显示详细调试信息
force_plot: bool = False, # 强制绘图(即使不满足三角形条件) force_plot: bool = False, # 强制绘图(即使不满足三角形条件)
plot_boundary_source: str = "hl", # 边界线拟合数据源: "hl" | "close"
show_high_low: bool = False, # 是否显示日内高低价范围
) -> None: ) -> None:
"""绘制单只股票的收敛三角形图""" """绘制单只股票的收敛三角形图"""
@ -183,6 +186,7 @@ def plot_triangle(
# ======================================================================== # ========================================================================
result = None result = None
has_triangle = False has_triangle = False
fitting_adherence_plot = 0.0 # 初始化贴合度
if has_enough_data: if has_enough_data:
result = detect_converging_triangle( result = detect_converging_triangle(
@ -231,21 +235,32 @@ def plot_triangle(
# 使用枢轴点连线法拟合边界线(与检测算法一致) # 使用枢轴点连线法拟合边界线(与检测算法一致)
# 注意:绘图用的是检测窗口数据,因此 window_start=0, window_end=n-1 # 注意:绘图用的是检测窗口数据,因此 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( a_u, b_u, selected_ph = fit_pivot_line_dispatch(
pivot_indices=ph_idx, pivot_indices=ph_idx,
pivot_values=high_win[ph_idx], pivot_values=upper_fit_values,
mode="upper", mode="upper",
method=params.fitting_method, method=params.fitting_method,
all_prices=high_win, all_prices=upper_all_prices,
window_start=0, window_start=0,
window_end=n - 1, window_end=n - 1,
) )
a_l, b_l, selected_pl = fit_pivot_line_dispatch( a_l, b_l, selected_pl = fit_pivot_line_dispatch(
pivot_indices=pl_idx, pivot_indices=pl_idx,
pivot_values=low_win[pl_idx], pivot_values=lower_fit_values,
mode="lower", mode="lower",
method=params.fitting_method, method=params.fitting_method,
all_prices=low_win, all_prices=lower_all_prices,
window_start=0, window_start=0,
window_end=n - 1, window_end=n - 1,
) )
@ -266,6 +281,29 @@ def plot_triangle(
# 三角形检测窗口的日期范围(用于标题) # 三角形检测窗口的日期范围(用于标题)
detect_dates = dates[valid_indices[detect_start:valid_end + 1]] 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), 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) 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: if has_triangle and has_enough_data:
ax1.plot(xw_in_display, upper_line, linewidth=2, label='上沿', color='red', linestyle='--') boundary_label = "收盘价" if plot_boundary_source == "close" else "高低价"
ax1.plot(xw_in_display, lower_line, linewidth=2, label='下沿', color='green', linestyle='--') 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) ax1.axvline(len(display_close) - 1, color='gray', linestyle=':', linewidth=1, alpha=0.5)
@ -285,11 +333,23 @@ def plot_triangle(
# 详细模式:显示拟合点(仅在 show_details=True 且有三角形时) # 详细模式:显示拟合点(仅在 show_details=True 且有三角形时)
# ======================================================================== # ========================================================================
if show_details and has_triangle and has_enough_data: 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: if len(ph_display_idx) > 0:
ax1.scatter( ax1.scatter(
ph_display_idx, ph_display_idx,
high_win[ph_idx], ph_pivot_y,
marker='x', marker='x',
s=60, s=60,
color='red', color='red',
@ -300,7 +360,7 @@ def plot_triangle(
if len(pl_display_idx) > 0: if len(pl_display_idx) > 0:
ax1.scatter( ax1.scatter(
pl_display_idx, pl_display_idx,
low_win[pl_idx], pl_pivot_y,
marker='x', marker='x',
s=60, s=60,
color='green', color='green',
@ -313,7 +373,7 @@ def plot_triangle(
if len(selected_ph_display) >= 2: if len(selected_ph_display) >= 2:
ax1.scatter( ax1.scatter(
selected_ph_display, selected_ph_display,
high_win[selected_ph_pos], selected_ph_y,
marker='o', marker='o',
s=120, s=120,
facecolors='none', facecolors='none',
@ -325,7 +385,7 @@ def plot_triangle(
if len(selected_pl_display) >= 2: if len(selected_pl_display) >= 2:
ax1.scatter( ax1.scatter(
selected_pl_display, selected_pl_display,
low_win[selected_pl_pos], selected_pl_y,
marker='o', marker='o',
s=120, s=120,
facecolors='none', facecolors='none',
@ -356,6 +416,14 @@ def plot_triangle(
else: else:
utilization_penalty = 1.0 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( 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)}个交易日) "
@ -364,7 +432,7 @@ def plot_triangle(
f"放量确认: {'' if result.volume_confirmed else '' if result.volume_confirmed is False else '-'}\n" f"放量确认: {'' if result.volume_confirmed else '' if result.volume_confirmed is False else '-'}\n"
f"强度分: {strength:.3f} " f"强度分: {strength:.3f} "
f"(价格: {price_score:.3f}×50% + 收敛: {result.convergence_score:.3f}×15% + " 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}", f"边界利用率: {boundary_util:.3f}×15%) × 利用率惩罚: {utilization_penalty:.2f}",
fontsize=11, pad=10 fontsize=11, pad=10
) )
@ -424,6 +492,17 @@ def main() -> None:
action="store_true", action="store_true",
help="显示详细调试信息(枢轴点、拟合点、分段线等)", 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( parser.add_argument(
"--all-stocks", "--all-stocks",
action="store_true", action="store_true",
@ -439,6 +518,8 @@ def main() -> None:
# 确定是否显示详细信息(命令行参数优先) # 确定是否显示详细信息(命令行参数优先)
show_details = args.show_details if hasattr(args, 'show_details') else SHOW_CHART_DETAILS 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 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("=" * 70)
print("收敛三角形图表生成") print("收敛三角形图表生成")
@ -565,6 +646,8 @@ def main() -> None:
display_window=DISPLAY_WINDOW, # 从配置文件读取 display_window=DISPLAY_WINDOW, # 从配置文件读取
show_details=show_details, # 传递详细模式参数 show_details=show_details, # 传递详细模式参数
force_plot=all_stocks, # 在all_stocks模式下强制绘图 force_plot=all_stocks, # 在all_stocks模式下强制绘图
plot_boundary_source=plot_boundary_source,
show_high_low=show_high_low,
) )
except Exception as e: except Exception as e:
print(f" [错误] {stock_code} {stock_name}: {e}") print(f" [错误] {stock_code} {stock_name}: {e}")