init
This commit is contained in:
57
stock_downloader.py
Normal file
57
stock_downloader.py
Normal 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()
|
||||
@@ -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))
|
||||
|
||||
@@ -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