This commit is contained in:
dsyoon
2025-08-23 22:43:16 +09:00
parent 6cb939d690
commit 487f6b9d43
22 changed files with 83 additions and 85 deletions

View File

@@ -35,7 +35,7 @@
- 단/중/장기 이동평균선(MA5/20/60)
- 거래량 MA5
- **추가 지표**: 스토캐스틱, OBV, ATR, MFI
5. **매수 후보 판정** (`check_buy_signals`)
5. **매수 후보 판정** (`check_signals`)
- *아래 새로운 "매수 후보 전략" 섹션 참조*
6. **알림 발송** (`send_*_telegram_message`)
multiprocessing Pool을 이용해 다중 메시지를 병렬로 전송합니다.

View File

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

View File

@@ -1,6 +1,6 @@
{
"ADA": {
"datetime": "2025-08-14T22:41:28.363958",
"buy_signal": "fall_6p"
"signal": "fall_6p"
}
}

View File

@@ -1,6 +1,6 @@
{
"APE": {
"datetime": "2025-08-09T14:22:02.089619",
"buy_signal": "movingaverage"
"signal": "movingaverage"
}
}

View File

@@ -1,6 +1,6 @@
{
"ARB": {
"datetime": "2025-08-14T22:43:59.078775",
"buy_signal": "fall_6p"
"signal": "fall_6p"
}
}

View File

@@ -1,6 +1,6 @@
{
"BONK": {
"datetime": "2025-08-14T22:41:42.247356",
"buy_signal": "fall_6p"
"signal": "fall_6p"
}
}

View File

@@ -1,6 +1,6 @@
{
"ENA": {
"datetime": "2025-08-16T01:03:31.916209",
"buy_signal": "deviation240"
"signal": "deviation240"
}
}

View File

@@ -1,6 +1,6 @@
{
"HBAR": {
"datetime": "2025-08-14T21:37:21.575425",
"buy_signal": "fall_6p"
"signal": "fall_6p"
}
}

View File

@@ -1,6 +1,6 @@
{
"KAIA": {
"datetime": "2025-08-14T22:42:27.079125",
"buy_signal": "fall_6p"
"signal": "fall_6p"
}
}

View File

@@ -1,6 +1,6 @@
{
"LINK": {
"datetime": "2025-08-14T22:42:38.780771",
"buy_signal": "fall_6p"
"signal": "fall_6p"
}
}

View File

@@ -1,6 +1,6 @@
{
"ONDO": {
"datetime": "2025-08-14T22:04:53.097618",
"buy_signal": "fall_6p"
"signal": "fall_6p"
}
}

View File

@@ -1,6 +1,6 @@
{
"PENGU": {
"datetime": "2025-08-16T07:53:40.994785",
"buy_signal": "deviation240"
"signal": "deviation240"
}
}

View File

@@ -1,6 +1,6 @@
{
"PEPE": {
"datetime": "2025-08-14T22:06:10.012326",
"buy_signal": "fall_6p"
"signal": "fall_6p"
}
}

View File

@@ -1,6 +1,6 @@
{
"SAND": {
"datetime": "2025-08-14T22:05:08.098364",
"buy_signal": "fall_6p"
"signal": "fall_6p"
}
}

View File

@@ -1,6 +1,6 @@
{
"SEI": {
"datetime": "2025-08-14T21:36:00.600483",
"buy_signal": "fall_6p"
"signal": "fall_6p"
}
}

View File

@@ -1,6 +1,6 @@
{
"SHIB": {
"datetime": "2025-08-14T22:05:20.734073",
"buy_signal": "fall_6p"
"signal": "fall_6p"
}
}

View File

@@ -1,6 +1,6 @@
{
"STORJ": {
"datetime": "2025-08-14T23:32:08.979598",
"buy_signal": "fall_6p"
"signal": "fall_6p"
}
}

View File

@@ -1,6 +1,6 @@
{
"SUI": {
"datetime": "2025-08-14T21:36:14.758922",
"buy_signal": "fall_6p"
"signal": "fall_6p"
}
}

View File

@@ -1,6 +1,6 @@
{
"UXLINK": {
"datetime": "2025-08-14T22:05:36.242448",
"buy_signal": "fall_6p"
"signal": "fall_6p"
}
}

View File

@@ -1,6 +1,6 @@
{
"VIRTUAL": {
"datetime": "2025-08-16T21:02:23.634183",
"buy_signal": "deviation1440"
"signal": "deviation1440"
}
}

View File

@@ -1,6 +1,6 @@
{
"WLD": {
"datetime": "2025-08-14T22:43:33.737340",
"buy_signal": "fall_6p"
"signal": "fall_6p"
}
}

View File

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