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['MA720'] = data['Close'].rolling(window=720).mean()
|
||||||
data['MA1440'] = data['Close'].rolling(window=1440).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))
|
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)}")
|
print(f"\n총 매수 신호 수: {len(alerts)}")
|
||||||
|
|
||||||
# 서브플롯 생성
|
# 서브플롯 생성 (가격 + Deviation)
|
||||||
fig, ax1 = plt.subplots(figsize=(15, 8))
|
fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=(15, 8), height_ratios=[3, 1])
|
||||||
fig.suptitle(f"{symbol} - 시뮬레이션 {interval_minutes}분봉", fontsize=14)
|
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_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]
|
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))
|
cursor3.connect("remove", lambda sel: sel.annotation.set_visible(False))
|
||||||
|
|
||||||
ax1.set_ylabel("가격")
|
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)
|
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()
|
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()
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user