diff --git a/README.md b/README.md index 3a14a14..3c364e8 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ - 단/중/장기 이동평균선(MA5/20/60) - 거래량 MA5 - **추가 지표**: 스토캐스틱, OBV, ATR, MFI -5. **매수 후보 판정** (`check_buy_signals`) +5. **매수 후보 판정** (`check_signals`) - *아래 새로운 "매수 후보 전략" 섹션 참조* 6. **알림 발송** (`send_*_telegram_message`) multiprocessing Pool을 이용해 다중 메시지를 병렬로 전송합니다. diff --git a/monitor_stock.py b/monitor_stock.py index b48f024..d7486da 100644 --- a/monitor_stock.py +++ b/monitor_stock.py @@ -52,7 +52,7 @@ class MonitorStock (Monitor): continue print(f" - {US_STOCKS[symbol]} ({symbol}): {recent_data['Close'].iloc[-1]:.2f}") message_list.append( - self.format_message('US', symbol, US_STOCKS[symbol], recent_data['Close'].iloc[-1], recent_data['buy_signal'].iloc[-1]) + self.format_message('US', symbol, US_STOCKS[symbol], recent_data['Close'].iloc[-1], recent_data['signal'].iloc[-1]) ) except Exception as e: print(f"Error processing data for {symbol}: {str(e)}") @@ -78,7 +78,7 @@ class MonitorStock (Monitor): continue print(f" - {KR_ETFS[symbol]} ({symbol}): {recent_data['Close'].iloc[-1]:.2f}") message_list.append( - self.format_message('KR', symbol, KR_ETFS[symbol], recent_data['Close'].iloc[-1], recent_data['buy_signal'].iloc[-1]) + self.format_message('KR', symbol, KR_ETFS[symbol], recent_data['Close'].iloc[-1], recent_data['signal'].iloc[-1]) ) except Exception as e: print(f"Error processing data for {symbol}: {str(e)}") diff --git a/resources/coins_buy_ADA.json b/resources/coins_buy_ADA.json index c3e5047..a3bc558 100644 --- a/resources/coins_buy_ADA.json +++ b/resources/coins_buy_ADA.json @@ -1,6 +1,6 @@ { "ADA": { "datetime": "2025-08-14T22:41:28.363958", - "buy_signal": "fall_6p" + "signal": "fall_6p" } } \ No newline at end of file diff --git a/resources/coins_buy_APE.json b/resources/coins_buy_APE.json index e632b95..b099b16 100644 --- a/resources/coins_buy_APE.json +++ b/resources/coins_buy_APE.json @@ -1,6 +1,6 @@ { "APE": { "datetime": "2025-08-09T14:22:02.089619", - "buy_signal": "movingaverage" + "signal": "movingaverage" } } \ No newline at end of file diff --git a/resources/coins_buy_ARB.json b/resources/coins_buy_ARB.json index be4e8f7..e5183b8 100644 --- a/resources/coins_buy_ARB.json +++ b/resources/coins_buy_ARB.json @@ -1,6 +1,6 @@ { "ARB": { "datetime": "2025-08-14T22:43:59.078775", - "buy_signal": "fall_6p" + "signal": "fall_6p" } } \ No newline at end of file diff --git a/resources/coins_buy_BONK.json b/resources/coins_buy_BONK.json index 299c6c9..35b3aa6 100644 --- a/resources/coins_buy_BONK.json +++ b/resources/coins_buy_BONK.json @@ -1,6 +1,6 @@ { "BONK": { "datetime": "2025-08-14T22:41:42.247356", - "buy_signal": "fall_6p" + "signal": "fall_6p" } } \ No newline at end of file diff --git a/resources/coins_buy_ENA.json b/resources/coins_buy_ENA.json index 77de258..c0ddb1e 100644 --- a/resources/coins_buy_ENA.json +++ b/resources/coins_buy_ENA.json @@ -1,6 +1,6 @@ { "ENA": { "datetime": "2025-08-16T01:03:31.916209", - "buy_signal": "deviation240" + "signal": "deviation240" } } \ No newline at end of file diff --git a/resources/coins_buy_HBAR.json b/resources/coins_buy_HBAR.json index f02e788..74f5c81 100644 --- a/resources/coins_buy_HBAR.json +++ b/resources/coins_buy_HBAR.json @@ -1,6 +1,6 @@ { "HBAR": { "datetime": "2025-08-14T21:37:21.575425", - "buy_signal": "fall_6p" + "signal": "fall_6p" } } \ No newline at end of file diff --git a/resources/coins_buy_KAIA.json b/resources/coins_buy_KAIA.json index d0ca2ec..1a942d2 100644 --- a/resources/coins_buy_KAIA.json +++ b/resources/coins_buy_KAIA.json @@ -1,6 +1,6 @@ { "KAIA": { "datetime": "2025-08-14T22:42:27.079125", - "buy_signal": "fall_6p" + "signal": "fall_6p" } } \ No newline at end of file diff --git a/resources/coins_buy_LINK.json b/resources/coins_buy_LINK.json index c5a175f..86501d4 100644 --- a/resources/coins_buy_LINK.json +++ b/resources/coins_buy_LINK.json @@ -1,6 +1,6 @@ { "LINK": { "datetime": "2025-08-14T22:42:38.780771", - "buy_signal": "fall_6p" + "signal": "fall_6p" } } \ No newline at end of file diff --git a/resources/coins_buy_ONDO.json b/resources/coins_buy_ONDO.json index ff59b89..75b516b 100644 --- a/resources/coins_buy_ONDO.json +++ b/resources/coins_buy_ONDO.json @@ -1,6 +1,6 @@ { "ONDO": { "datetime": "2025-08-14T22:04:53.097618", - "buy_signal": "fall_6p" + "signal": "fall_6p" } } \ No newline at end of file diff --git a/resources/coins_buy_PENGU.json b/resources/coins_buy_PENGU.json index 54720d9..177b9ba 100644 --- a/resources/coins_buy_PENGU.json +++ b/resources/coins_buy_PENGU.json @@ -1,6 +1,6 @@ { "PENGU": { "datetime": "2025-08-16T07:53:40.994785", - "buy_signal": "deviation240" + "signal": "deviation240" } } \ No newline at end of file diff --git a/resources/coins_buy_PEPE.json b/resources/coins_buy_PEPE.json index d008a6e..ccad821 100644 --- a/resources/coins_buy_PEPE.json +++ b/resources/coins_buy_PEPE.json @@ -1,6 +1,6 @@ { "PEPE": { "datetime": "2025-08-14T22:06:10.012326", - "buy_signal": "fall_6p" + "signal": "fall_6p" } } \ No newline at end of file diff --git a/resources/coins_buy_SAND.json b/resources/coins_buy_SAND.json index 37ebcfa..92caecb 100644 --- a/resources/coins_buy_SAND.json +++ b/resources/coins_buy_SAND.json @@ -1,6 +1,6 @@ { "SAND": { "datetime": "2025-08-14T22:05:08.098364", - "buy_signal": "fall_6p" + "signal": "fall_6p" } } \ No newline at end of file diff --git a/resources/coins_buy_SEI.json b/resources/coins_buy_SEI.json index 56937b8..4b17c13 100644 --- a/resources/coins_buy_SEI.json +++ b/resources/coins_buy_SEI.json @@ -1,6 +1,6 @@ { "SEI": { "datetime": "2025-08-14T21:36:00.600483", - "buy_signal": "fall_6p" + "signal": "fall_6p" } } \ No newline at end of file diff --git a/resources/coins_buy_SHIB.json b/resources/coins_buy_SHIB.json index ab9a020..7bcb80a 100644 --- a/resources/coins_buy_SHIB.json +++ b/resources/coins_buy_SHIB.json @@ -1,6 +1,6 @@ { "SHIB": { "datetime": "2025-08-14T22:05:20.734073", - "buy_signal": "fall_6p" + "signal": "fall_6p" } } \ No newline at end of file diff --git a/resources/coins_buy_STORJ.json b/resources/coins_buy_STORJ.json index 466ae0c..f378355 100644 --- a/resources/coins_buy_STORJ.json +++ b/resources/coins_buy_STORJ.json @@ -1,6 +1,6 @@ { "STORJ": { "datetime": "2025-08-14T23:32:08.979598", - "buy_signal": "fall_6p" + "signal": "fall_6p" } } \ No newline at end of file diff --git a/resources/coins_buy_SUI.json b/resources/coins_buy_SUI.json index 54de943..ac30937 100644 --- a/resources/coins_buy_SUI.json +++ b/resources/coins_buy_SUI.json @@ -1,6 +1,6 @@ { "SUI": { "datetime": "2025-08-14T21:36:14.758922", - "buy_signal": "fall_6p" + "signal": "fall_6p" } } \ No newline at end of file diff --git a/resources/coins_buy_UXLINK.json b/resources/coins_buy_UXLINK.json index 80f6716..515bcaa 100644 --- a/resources/coins_buy_UXLINK.json +++ b/resources/coins_buy_UXLINK.json @@ -1,6 +1,6 @@ { "UXLINK": { "datetime": "2025-08-14T22:05:36.242448", - "buy_signal": "fall_6p" + "signal": "fall_6p" } } \ No newline at end of file diff --git a/resources/coins_buy_VIRTUAL.json b/resources/coins_buy_VIRTUAL.json index 521d3b2..51f6956 100644 --- a/resources/coins_buy_VIRTUAL.json +++ b/resources/coins_buy_VIRTUAL.json @@ -1,6 +1,6 @@ { "VIRTUAL": { "datetime": "2025-08-16T21:02:23.634183", - "buy_signal": "deviation1440" + "signal": "deviation1440" } } \ No newline at end of file diff --git a/resources/coins_buy_WLD.json b/resources/coins_buy_WLD.json index 4e38788..b98bc8d 100644 --- a/resources/coins_buy_WLD.json +++ b/resources/coins_buy_WLD.json @@ -1,6 +1,6 @@ { "WLD": { "datetime": "2025-08-14T22:43:33.737340", - "buy_signal": "fall_6p" + "signal": "fall_6p" } } \ No newline at end of file diff --git a/simulation.py b/simulation.py index d233076..3b99d5d 100644 --- a/simulation.py +++ b/simulation.py @@ -27,7 +27,7 @@ class Simulation: if current_data.get('point', 0) == 1: return { 'alert': True, - 'details': f"매수신호: {current_data.get('buy_signal', 'unknown')}" + 'details': f"매수신호: {current_data.get('signal', 'unknown')}" } return {'alert': False, 'details': "매수신호 없음"} @@ -99,6 +99,10 @@ class Simulation: def run_simulation(self, symbol: str, interval_minutes: int, days: int = 30): data = self.fetch_price_history(symbol, interval_minutes) + + inverseData = self.monitor.inverse_data(data) + inverseData = self.monitor.check_point(symbol, inverseData, simulation=True) + data = self.monitor.calculate_technical_indicators(data) data = self.monitor.check_point(symbol, data, simulation=True) print(f"데이터 기간: {data.index[0]} ~ {data.index[-1]}") @@ -108,10 +112,10 @@ class Simulation: if data['point'].iloc[i] == 1: alerts.append((data.index[i], data['Close'].iloc[i])) print(f"\n총 매수 신호 수: {len(alerts)}") - ma_signals = len(data[(data['point'] == 1) & (data['buy_signal'] == 'movingaverage')]) - dev40_signals = len(data[(data['point'] == 1) & (data['buy_signal'] == 'deviation40')]) - dev240_signals = len(data[(data['point'] == 1) & (data['buy_signal'] == 'deviation240')]) - dev1440_signals = len(data[(data['point'] == 1) & (data['buy_signal'] == 'deviation1440')]) + ma_signals = len(data[(data['point'] == 1) & (data['signal'] == 'movingaverage')]) + dev40_signals = len(data[(data['point'] == 1) & (data['signal'] == 'deviation40')]) + dev240_signals = len(data[(data['point'] == 1) & (data['signal'] == 'deviation240')]) + dev1440_signals = len(data[(data['point'] == 1) & (data['signal'] == 'deviation1440')]) print(f" - MA 신호: {ma_signals}") print(f" - Dev40 신호: {dev40_signals}") print(f" - Dev240 신호: {dev240_signals}") @@ -188,34 +192,7 @@ class Simulation: ax1.plot([date, date], [low, high], color='black', linewidth=1) normal_patches.append(ax1.lines[-1]) # 추가: 선도 저장 - # ----------------- 하이킨아시 캔들스틱 ----------------- - # 하이킨아시 OHLC 계산 - ha_df = pd.DataFrame(index=data.index, columns=['Open', 'High', 'Low', 'Close']) - ha_df['Close'] = (data['Open'] + data['High'] + data['Low'] + data['Close']) / 4 - # 첫 번째 HA Open 값 계산 - ha_df.loc[ha_df.index[0], 'Open'] = (data['Open'].iloc[0] + data['Close'].iloc[0]) / 2 - # 이후 HA Open 값은 이전 HA 캔들의 (Open+Close)/2 - for i in range(1, len(ha_df)): - prev_open = ha_df.loc[ha_df.index[i-1], 'Open'] - prev_close = ha_df.loc[ha_df.index[i-1], 'Close'] - ha_df.loc[ha_df.index[i], 'Open'] = (prev_open + prev_close) / 2 - # High/Low 계산 - ha_df['High'] = pd.concat([ha_df['Open'], ha_df['Close'], data['High']], axis=1).max(axis=1) - ha_df['Low'] = pd.concat([ha_df['Open'], ha_df['Close'], data['Low']], axis=1).min(axis=1) - - ha_patches = [] - for i, (idx, row) in enumerate(ha_df.iterrows()): - date = mdates.date2num(idx) - open_price, high, low, close = row['Open'], row['High'], row['Low'], row['Close'] - color = 'red' if close >= open_price else 'blue' - body_height = abs(close - open_price) - body_bottom = min(open_price, close) - rect = plt.Rectangle((date - 0.3, body_bottom), 0.6, body_height, - facecolor=color, edgecolor='black', alpha=0.7, visible=False) - ax1.add_patch(rect) - ha_patches.append(rect) - line = ax1.plot([date, date], [low, high], color='black', linewidth=1, visible=False)[0] - ha_patches.append(line) + # ----------------- 하이킨아시 캔들스틱 (제거됨) ----------------- # 메인 차트 (가격, 이동평균선, 볼린저 밴드) line_close = ax1.plot(data.index, data["Close"], label="종가", color="black", linewidth=1.5, alpha=0.8)[0] @@ -236,7 +213,7 @@ class Simulation: # 매수 포인트를 신호 유형별로 다르게 표시 (수직선 포함) # 이동평균선 기반 매수 포인트 - ma_points = data[(data['point'] == 1) & (data['buy_signal'] == 'movingaverage')] + ma_points = data[(data['point'] == 1) & (data['signal'] == 'movingaverage')] scatter_ma_points = None if len(ma_points) > 0: scatter_ma_points = ax1.scatter(ma_points.index, ma_points['Close'], color='red', s=150, zorder=10, label='MA 매수 포인트', marker='o') @@ -244,7 +221,7 @@ class Simulation: ax1.axvline(x=time, color='red', linestyle='-', alpha=0.5, linewidth=1) # Deviation40 기반 매수 포인트 - dev40_points = data[(data['point'] == 1) & (data['buy_signal'] == 'deviation40')] + dev40_points = data[(data['point'] == 1) & (data['signal'] == 'deviation40')] scatter_dev40_points = None if len(dev40_points) > 0: scatter_dev40_points = ax1.scatter(dev40_points.index, dev40_points['Close'], @@ -254,7 +231,7 @@ class Simulation: ax1.axvline(x=time, color='red', linestyle='--', alpha=0.5, linewidth=1) # Deviation240 기반 매수 포인트 - dev240_points = data[(data['point'] == 1) & (data['buy_signal'] == 'deviation240')] + dev240_points = data[(data['point'] == 1) & (data['signal'] == 'deviation240')] scatter_dev240_points = None if len(dev240_points) > 0: scatter_dev240_points = ax1.scatter(dev240_points.index, dev240_points['Close'], @@ -264,7 +241,7 @@ class Simulation: ax1.axvline(x=time, color='blue', linestyle='--', alpha=0.5, linewidth=1) # Deviation1440 기반 매수 포인트 - dev1440_points = data[(data['point'] == 1) & (data['buy_signal'] == 'deviation1440')] + dev1440_points = data[(data['point'] == 1) & (data['signal'] == 'deviation1440')] scatter_dev1440_points = None if len(dev1440_points) > 0: scatter_dev1440_points = ax1.scatter(dev1440_points.index, dev1440_points['Close'], @@ -321,9 +298,6 @@ class Simulation: bbox=dict(boxstyle="round,pad=0.3", facecolor='lightblue', alpha=0.8), fontsize=10, ha='left', va='top', picker=True) # 하이킨아시 토글 버튼 추가 - ha_button = ax1.text(0.06, 0.98, 'HA', transform=ax1.transAxes, - bbox=dict(boxstyle="round,pad=0.3", facecolor='orange', alpha=0.8), - fontsize=10, ha='left', va='top', picker=True) # --- 범례 생성 및 인터랙티브 토글 --- # 캔들 및 지표만 범례에 표시 (일반/하이킨아시 토글은 HA 버튼으로 제공) @@ -349,15 +323,50 @@ class Simulation: if scatter_dev1440_points is not None: plot_lines.append(scatter_dev1440_points) + # --------- 매도 포인트(인버스 데이터) 표시 --------- + # Deviation40 기반 매도 포인트 + sell_dev40_points = inverseData[(inverseData['point'] == 1) & (inverseData['signal'] == 'deviation40')] + scatter_sell_dev40_points = None + if len(sell_dev40_points) > 0: + scatter_sell_dev40_points = ax1.scatter(sell_dev40_points.index, + data.loc[sell_dev40_points.index, 'Close'], + facecolors='none', edgecolors='orange', marker='v', linewidth=2, + s=200, zorder=10, label='Dev40 매도 포인트') + for time in sell_dev40_points.index: + ax1.axvline(x=time, color='orange', linestyle='--', alpha=0.5, linewidth=1) + + # -------- 매도 포인트 마우스 오버 -------- + if scatter_sell_dev40_points is not None: + cursor_sell_dev40 = mplcursors.cursor(scatter_sell_dev40_points, hover=True) + cursor_sell_dev40.connect("add", lambda sel: sel.annotation.set_text( + f'Dev40 매도신호\n날짜: {matplotlib.dates.num2date(sel.target[0]).replace(tzinfo=None).strftime("%Y-%m-%d %H:%M")}\n가격: {sel.target[1]:.2f}' + )) + cursor_sell_dev40.connect("remove", lambda sel: sel.annotation.set_visible(False)) + + # -------- plot_lines 업데이트 -------- + if scatter_sell_dev40_points is not None: + plot_lines.append(scatter_sell_dev40_points) + + # 기존 범례 제거 후 새로 생성하여 매도 항목 포함 + if legend is not None: + legend.remove() + legend = ax1.legend(loc='upper left', bbox_to_anchor=(0.02, 0.85), fontsize=10) + # 새 legend_handles 추출 + if hasattr(legend, "legend_handles"): + legend_handles = legend.legend_handles + elif hasattr(legend, "legendHandles"): + legend_handles = legend.legendHandles + else: + legend_handles = legend.get_lines() + + # lined 사전에 새 핸들 매핑 추가 for leg_handle, orig in zip(legend_handles[:len(plot_lines)], plot_lines): leg_handle.set_picker(True) lined[leg_handle] = orig # 하이킨아시/일반 토글 상태 변수 - ha_mode = False def on_pick(event): - nonlocal ha_mode leg_handle = event.artist # 홈 버튼 동작 if leg_handle == home_button: @@ -370,21 +379,6 @@ class Simulation: ) fig.canvas.draw_idle() return - # 하이킨아시 토글 - if leg_handle == ha_button: - ha_mode = not ha_mode - if ha_mode: - for p in normal_patches: - p.set_visible(False) - for p in ha_patches: - p.set_visible(True) - else: - for p in normal_patches: - p.set_visible(True) - for p in ha_patches: - p.set_visible(False) - fig.canvas.draw_idle() - return # 선 토글 처리 orig = lined.get(leg_handle) if orig is None: @@ -501,14 +495,18 @@ if __name__ == "__main__": sim.run_simulation(symbol, interval, days) else: data = sim.fetch_price_history(symbol, interval, days) + + inverseData = sim.monitor.inverse_data(data) + inverseData = sim.monitor.check_point(symbol, inverseData, simulation=True) + data = sim.monitor.calculate_technical_indicators(data) data = sim.monitor.check_point(symbol, data, simulation=True) - total_buy_signals = len(data[data['point'] == 1]) - ma_signals = len(data[(data['point'] == 1) & (data['buy_signal'] == 'movingaverage')]) - dev40_signals = len(data[(data['point'] == 1) & (data['buy_signal'] == 'deviation40')]) - dev240_signals = len(data[(data['point'] == 1) & (data['buy_signal'] == 'deviation240')]) - dev1440_signals = len(data[(data['point'] == 1) & (data['buy_signal'] == 'deviation1440')]) - print(f"총 매수 신호: {total_buy_signals}") + total_signals = len(data[data['point'] == 1]) + ma_signals = len(data[(data['point'] == 1) & (data['signal'] == 'movingaverage')]) + dev40_signals = len(data[(data['point'] == 1) & (data['signal'] == 'deviation40')]) + dev240_signals = len(data[(data['point'] == 1) & (data['signal'] == 'deviation240')]) + dev1440_signals = len(data[(data['point'] == 1) & (data['signal'] == 'deviation1440')]) + print(f"총 매수 신호: {total_signals}") print(f" - MA 신호: {ma_signals}") print(f" - Dev40 신호: {dev40_signals}") print(f" - Dev240 신호: {dev240_signals}")