From 7135bf71f0960841b900ce84b06e428f0aa00f53 Mon Sep 17 00:00:00 2001 From: dsyoon Date: Wed, 6 Aug 2025 23:18:56 +0900 Subject: [PATCH] init --- stock_downloader.py | 57 ++++++++++++++++ stock_monitor.py | 4 ++ stock_simulation.py | 155 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 213 insertions(+), 3 deletions(-) create mode 100644 stock_downloader.py diff --git a/stock_downloader.py b/stock_downloader.py new file mode 100644 index 0000000..62428b6 --- /dev/null +++ b/stock_downloader.py @@ -0,0 +1,57 @@ +from HTS2 import HTS +from dateutil.relativedelta import relativedelta +from datetime import datetime +import sqlite3 +from stock_monitor import get_coin_more_data +from config import * + +hts = HTS() + +def inserData(symbol, interval, data): + conn = sqlite3.connect('coins.db') + cursor = conn.cursor() + + # 테이블/키 생성 + cursor.execute("CREATE TABLE IF NOT EXISTS " + symbol + " (period text, CODE text, NAME text, ymdhms datetime, ymd text, hms text, close REAL, open REAL, high REAL, low REAL, volume REAL)") + cursor.execute("CREATE INDEX IF NOT EXISTS " + symbol + "_idx on " + symbol + "(CODE, ymdhms)") + + for i in range(len(data)): + ymd = data.index[i].strftime('%Y%m%d') + hms = data.index[i].strftime('%H%M%S') + ymdhms = data.index[i].strftime('%Y-%m-%d %H:%M:%S') + open = data.Open.iloc[i] + high = data.High.iloc[i] + low = data.Low.iloc[i] + close = data.Close.iloc[i] + volume = data.Volume.iloc[i] + + cursor.execute("SELECT * from " + symbol + " where CODE = ? and ymdhms = ? and interval = ?", (symbol, ymdhms, interval)) + arr = cursor.fetchone() + if arr: + cursor.execute("UPDATE " + symbol + " SET close=?, open=?, high=?, low=?, volume=? where CODE=? and ymdhms=? and period=?", (close, open, high, low, volume, symbol, ymdhms, interval)) + else: + cursor.execute("INSERT INTO " + symbol + " (period, CODE, NAME, ymdhms, ymd, hms, close, open, high, low, volume) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", (interval, symbol, KR_COINS[symbol], ymdhms, ymd, hms, close, open, high, low, volume)) + + conn.commit() + cursor.close() + conn.close() + return + +def download(): + for symbol in KR_COINS: + + # 1시간 + interval = 60 + + data = get_coin_more_data(symbol, interval, bong_count=10000) + + if data is not None and not data.empty: + try: + inserData(symbol, interval, data) + except Exception as e: + print(f"Error processing data for {symbol}: {str(e)}") + + return + +if __name__ == "__main__": + download() diff --git a/stock_monitor.py b/stock_monitor.py index 1920c68..fed5a16 100644 --- a/stock_monitor.py +++ b/stock_monitor.py @@ -110,6 +110,10 @@ def calculate_technical_indicators(data): data['MA720'] = data['Close'].rolling(window=720).mean() data['MA1440'] = data['Close'].rolling(window=1440).mean() + # --- 이격도(Deviation) 계산 --- + data['Deviation20'] = (data['Close'] / data['MA20']) * 100 + data['Deviation40'] = (data['Close'] / data['MA40']) * 100 + # 매수 타이밍을 이동평균선으로 결정 # 골든크로스: 단기 이동평균선이 장기 이동평균선을 상향 돌파할 때 매수 data['golden_cross'] = (data['MA5'] > data['MA20']) & (data['MA5'].shift(1) <= data['MA20'].shift(1)) diff --git a/stock_simulation.py b/stock_simulation.py index 6464d2e..5ac6f1e 100644 --- a/stock_simulation.py +++ b/stock_simulation.py @@ -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()