This commit is contained in:
dsyoon
2025-08-06 23:18:56 +09:00
parent 7d5a8eafcb
commit 7135bf71f0
3 changed files with 213 additions and 3 deletions

57
stock_downloader.py Normal file
View File

@@ -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()

View File

@@ -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))

View File

@@ -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()