init
This commit is contained in:
@@ -167,10 +167,45 @@ def run_simulation(symbol: str, interval_minutes: int, days: int = 30):
|
||||
|
||||
print(f"\n총 매수 신호 수: {len(alerts)}")
|
||||
|
||||
# 서브플롯 생성
|
||||
fig, ax1 = plt.subplots(figsize=(15, 8))
|
||||
# 서브플롯 생성 (가격 + Deviation)
|
||||
fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=(15, 8), height_ratios=[3, 1])
|
||||
fig.suptitle(f"{symbol} - 시뮬레이션 {interval_minutes}분봉", fontsize=14)
|
||||
|
||||
# ----------------- 마우스 휠 확대/축소 -----------------
|
||||
def on_scroll(event):
|
||||
# event.button: 'up' -> zoom in, 'down' -> zoom out
|
||||
if event.inaxes not in [ax1, ax2]:
|
||||
return
|
||||
ax = event.inaxes
|
||||
# x 축만 두 축을 동시에 조정
|
||||
cur_xlim = ax1.get_xlim()
|
||||
xdata = event.xdata
|
||||
if xdata is None:
|
||||
return
|
||||
scale_factor = 0.9 if event.button == 'up' else 1/0.9
|
||||
new_width = (cur_xlim[1] - cur_xlim[0]) * scale_factor
|
||||
relx = (cur_xlim[1] - xdata) / (cur_xlim[1] - cur_xlim[0])
|
||||
new_left = xdata - new_width * (1 - relx)
|
||||
new_right = xdata + new_width * relx
|
||||
# 데이터 영역 벗어나지 않도록 클램프
|
||||
xmin, xmax = matplotlib.dates.date2num(data.index[0]), matplotlib.dates.date2num(data.index[-1])
|
||||
if new_left < xmin:
|
||||
new_left = xmin
|
||||
if new_right > xmax:
|
||||
new_right = xmax
|
||||
ax1.set_xlim([new_left, new_right])
|
||||
ax2.set_xlim([new_left, new_right])
|
||||
# y축은 해당 축만 줌
|
||||
cur_ylim = ax.get_ylim()
|
||||
ydata = event.ydata
|
||||
if ydata is not None:
|
||||
new_height = (cur_ylim[1] - cur_ylim[0]) * scale_factor
|
||||
rely = (cur_ylim[1] - ydata) / (cur_ylim[1] - cur_ylim[0])
|
||||
ax.set_ylim([ydata - new_height * (1 - rely), ydata + new_height * rely])
|
||||
ax.figure.canvas.draw_idle()
|
||||
|
||||
fig.canvas.mpl_connect('scroll_event', on_scroll)
|
||||
|
||||
# 메인 차트 (가격, 이동평균선, 볼린저 밴드)
|
||||
line_close = ax1.plot(data.index, data["Close"], label="종가", color="black", linewidth=1.5)[0]
|
||||
line_ma5 = ax1.plot(data.index, data["MA5"], label="MA5", color="red", linewidth=1)[0]
|
||||
@@ -232,10 +267,124 @@ def run_simulation(symbol: str, interval_minutes: int, days: int = 30):
|
||||
cursor3.connect("remove", lambda sel: sel.annotation.set_visible(False))
|
||||
|
||||
ax1.set_ylabel("가격")
|
||||
ax1.legend(loc='upper left', fontsize=10)
|
||||
# --- 범례 생성 및 인터랙티브 토글 ---
|
||||
legend = ax1.legend(loc='upper left', fontsize=10)
|
||||
ax1.grid(True, linestyle='--', alpha=0.5)
|
||||
|
||||
# 범례 클릭 시 해당 선 토글 기능
|
||||
lined = {}
|
||||
# legend 핸들과 실제 plot 선을 매핑 (생성 순서가 동일하다고 가정)
|
||||
# Matplotlib 버전에 따라 legend 객체의 핸들 보유 프로퍼티가 다를 수 있음
|
||||
if hasattr(legend, "legend_handles"):
|
||||
legend_handles = legend.legend_handles
|
||||
elif hasattr(legend, "legendHandles"):
|
||||
legend_handles = legend.legendHandles
|
||||
else:
|
||||
# 마지막 방어(일반적으로 선만 리턴)
|
||||
legend_handles = legend.get_lines()
|
||||
plot_lines = [line_close, line_ma5, line_ma20, line_ma40, line_ma120,
|
||||
line_ma200, line_ma240, line_ma720, line_ma1440,
|
||||
line_upper, line_lower, scatter_buy_points]
|
||||
# 매수신호 scatter가 있으면 포함
|
||||
if scatter_buy is not None:
|
||||
plot_lines.append(scatter_buy)
|
||||
|
||||
# zip 길이가 짧은 쪽에 맞춰 매핑
|
||||
for leg_handle, orig in zip(legend_handles, plot_lines):
|
||||
leg_handle.set_picker(True) # 클릭 이벤트 활성화
|
||||
lined[leg_handle] = orig
|
||||
|
||||
def on_pick(event):
|
||||
leg_handle = event.artist
|
||||
orig = lined.get(leg_handle)
|
||||
if orig is None:
|
||||
return
|
||||
vis = not orig.get_visible()
|
||||
orig.set_visible(vis)
|
||||
# 범례 아이콘 투명도 조정
|
||||
leg_handle.set_alpha(1.0 if vis else 0.2)
|
||||
fig.canvas.draw_idle()
|
||||
|
||||
fig.canvas.mpl_connect('pick_event', on_pick)
|
||||
|
||||
# Deviation subplot
|
||||
line_dev20 = ax2.plot(data.index, data['Deviation20'], color='orange', label='Dev20(C/MA20×100)')[0]
|
||||
line_dev40 = ax2.plot(data.index, data['Deviation40'], color='blue', label='Dev40(C/MA40×100)')[0]
|
||||
cursor_dev = mplcursors.cursor([line_dev20, line_dev40], hover=True)
|
||||
cursor_dev.connect("add", lambda sel: sel.annotation.set_text(
|
||||
f"{sel.artist.get_label()}\n날짜: {matplotlib.dates.num2date(sel.target[0]).replace(tzinfo=None).strftime('%Y-%m-%d %H:%M')}\n값: {sel.target[1]:.2f}"
|
||||
))
|
||||
line_h98 = ax2.axhline(90, color='red', linestyle='--', linewidth=1, label='90')
|
||||
line_h97 = ax2.axhline(95, color='green', linestyle='--', linewidth=1, label='93')
|
||||
ax2.set_ylabel('Deviation %')
|
||||
legend2 = ax2.legend(loc='upper left', fontsize=9)
|
||||
|
||||
# Deviation subplot 범례 클릭 토글 기능
|
||||
if legend2 is not None:
|
||||
if hasattr(legend2, "legend_handles"):
|
||||
legend2_handles = legend2.legend_handles
|
||||
elif hasattr(legend2, "legendHandles"):
|
||||
legend2_handles = legend2.legendHandles
|
||||
else:
|
||||
legend2_handles = legend2.get_lines()
|
||||
plot_lines2 = [line_dev20, line_dev40, line_h98, line_h97]
|
||||
# 레이블 기준으로 안정적 매핑
|
||||
for leg_handle in legend2_handles:
|
||||
label = leg_handle.get_label()
|
||||
target_line = next((pl for pl in plot_lines2 if pl.get_label() == label), None)
|
||||
if target_line is not None:
|
||||
leg_handle.set_picker(True)
|
||||
lined[leg_handle] = target_line
|
||||
ax2.grid(True, linestyle='--', alpha=0.3)
|
||||
|
||||
plt.tight_layout()
|
||||
|
||||
# -------- 확대/축소 및 이동 기능 --------
|
||||
press = {}
|
||||
|
||||
def on_scroll(event):
|
||||
ax = event.inaxes
|
||||
if ax is None:
|
||||
return
|
||||
x_left, x_right = ax.get_xlim()
|
||||
x_range = (x_right - x_left)
|
||||
if event.button == 'up': # zoom in
|
||||
scale = 0.8
|
||||
elif event.button == 'down': # zoom out
|
||||
scale = 1.25
|
||||
else:
|
||||
scale = 1.0
|
||||
new_range = x_range * scale
|
||||
center = event.xdata if event.xdata is not None else (x_left + x_right) / 2
|
||||
ax.set_xlim(center - new_range / 2, center + new_range / 2)
|
||||
# 다른 축들도 동일 적용 (shared x)
|
||||
for other_ax in fig.axes:
|
||||
if other_ax is not ax:
|
||||
other_ax.set_xlim(ax.get_xlim())
|
||||
fig.canvas.draw_idle()
|
||||
|
||||
def on_press(event):
|
||||
if event.button == 1 and event.inaxes is not None:
|
||||
press['xpress'] = event.xdata
|
||||
press['axes'] = event.inaxes
|
||||
|
||||
def on_motion(event):
|
||||
if 'xpress' not in press or press.get('axes') is None or event.inaxes is None:
|
||||
return
|
||||
dx = press['xpress'] - event.xdata
|
||||
for ax in fig.axes:
|
||||
x_left, x_right = ax.get_xlim()
|
||||
ax.set_xlim(x_left + dx, x_right + dx)
|
||||
fig.canvas.draw_idle()
|
||||
|
||||
def on_release(event):
|
||||
press.clear()
|
||||
|
||||
fig.canvas.mpl_connect('scroll_event', on_scroll)
|
||||
fig.canvas.mpl_connect('button_press_event', on_press)
|
||||
fig.canvas.mpl_connect('motion_notify_event', on_motion)
|
||||
fig.canvas.mpl_connect('button_release_event', on_release)
|
||||
|
||||
plt.show()
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user