This commit is contained in:
dsyoon
2025-08-04 22:10:29 +09:00
parent ddb0a40a4a
commit 8faabc2fe1
2 changed files with 13 additions and 74 deletions

View File

@@ -84,6 +84,7 @@ def calculate_technical_indicators(data):
exp2 = data['Close'].ewm(span=26, adjust=False).mean() exp2 = data['Close'].ewm(span=26, adjust=False).mean()
data['MACD'] = exp1 - exp2 data['MACD'] = exp1 - exp2
data['Signal'] = data['MACD'].ewm(span=9, adjust=False).mean() data['Signal'] = data['MACD'].ewm(span=9, adjust=False).mean()
data['MACD_Hist'] = data['MACD'] - data['Signal']
# 이동평균선 # 이동평균선
data['MA5'] = data['Close'].rolling(window=5).mean() data['MA5'] = data['Close'].rolling(window=5).mean()
@@ -96,45 +97,6 @@ def calculate_technical_indicators(data):
return data return data
def check_ma_alert(symbol, data, interval=0):
"""1시간봉 기준 이동평균선 조건 알림
- 5봉 이동평균(MA5) 상승
- 20봉 이동평균(MA20) 상승
- 40봉 이동평균(MA40)이 하락세에서 상승세로 전환되는 시점 (직전 기울기 < 0 and 현재 기울기 ≥ 0)
"""
# 40 이동평균선의 기울기를 계산하기 위해 최소 41개 캔들이 필요합니다.
if len(data) < 41:
return None
# 이동평균선 값 추출
ma5_current, ma5_prev = data['MA5'].iloc[-1], data['MA5'].iloc[-2]
ma20_current, ma20_prev = data['MA20'].iloc[-1], data['MA20'].iloc[-2]
ma40_current = data['MA40'].iloc[-1]
ma40_prev1, ma40_prev2 = data['MA40'].iloc[-2], data['MA40'].iloc[-3]
# 조건 계산
up5 = ma5_current > ma5_prev
up20 = ma20_current > ma20_prev
slope_prev = ma40_prev1 - ma40_prev2 # 직전 기울기 (음수: 하락)
slope_now = ma40_current - ma40_prev1 # 현재 기울기
turning = (slope_prev < 0) and (slope_now >= 0)
alert = up5 and up20 and turning
return {
'symbol': symbol,
'price': data['Close'].iloc[-1],
'alert': alert,
'details': {
'interval': interval,
'up5': up5,
'up20': up20,
'ma40_turning': turning
}
}
def check_buy_signals(symbol, data): def check_buy_signals(symbol, data):
if len(data) < 60: # 최소 60일치 데이터 필요 if len(data) < 60: # 최소 60일치 데이터 필요
return None return None
@@ -397,7 +359,7 @@ def monitor_us_stocks():
if data is not None and not data.empty: if data is not None and not data.empty:
try: try:
data = calculate_technical_indicators(data) data = calculate_technical_indicators(data)
info = check_ma_alert(symbol, data, 0) info = detect_turnaround_signal(symbol, data, 0)
if info is None: if info is None:
continue continue
info['name'] = US_STOCKS[symbol] info['name'] = US_STOCKS[symbol]
@@ -431,7 +393,7 @@ def monitor_kr_stocks():
if data is not None and not data.empty: if data is not None and not data.empty:
try: try:
data = calculate_technical_indicators(data) data = calculate_technical_indicators(data)
info = check_ma_alert(symbol, data, 0) info = detect_turnaround_signal(symbol, data, 0)
if info is None: if info is None:
continue continue
info['name'] = KR_ETFS[symbol] info['name'] = KR_ETFS[symbol]
@@ -472,7 +434,7 @@ def monitor_coins():
if data is not None and not data.empty: if data is not None and not data.empty:
try: try:
data = calculate_technical_indicators(data) data = calculate_technical_indicators(data)
info = check_ma_alert(symbol, data, interval) info = detect_turnaround_signal(symbol, data, interval)
if info is None: if info is None:
continue continue
info['name'] = KR_COINS[symbol] info['name'] = KR_COINS[symbol]
@@ -493,7 +455,7 @@ def monitor_coins():
if data is not None and not data.empty: if data is not None and not data.empty:
try: try:
data = calculate_technical_indicators(data) data = calculate_technical_indicators(data)
info = check_ma_alert(symbol, data, interval) info = detect_turnaround_signal(symbol, data, interval)
if info is None: if info is None:
continue continue
info['name'] = KR_COINS[symbol] info['name'] = KR_COINS[symbol]
@@ -528,32 +490,6 @@ def calculate_bollinger_bands(data: pd.DataFrame, period: int = 20, std: int = 2
data['Lower'] = data['MA'] - std * data['STD'] data['Lower'] = data['MA'] - std * data['STD']
return data return data
def calculate_technical_indicators(data: pd.DataFrame):
"""Add MA5/20/40, RSI14, MACD+Hist, Bollinger"""
data = calculate_bollinger_bands(data)
# RSI(14)
delta = data['Close'].diff()
gain = delta.where(delta > 0, 0).rolling(14).mean()
loss = (-delta.where(delta < 0, 0)).rolling(14).mean()
rs = gain / loss
data['RSI'] = 100 - 100 / (1 + rs)
# MACD
exp1 = data['Close'].ewm(span=12, adjust=False).mean()
exp2 = data['Close'].ewm(span=26, adjust=False).mean()
data['MACD'] = exp1 - exp2
data['Signal'] = data['MACD'].ewm(span=9, adjust=False).mean()
data['MACD_Hist'] = data['MACD'] - data['Signal']
# Moving averages
data['MA5'] = data['Close'].rolling(5).mean()
data['MA20'] = data['Close'].rolling(20).mean()
data['MA40'] = data['Close'].rolling(40).mean()
return data
# ---------------------- # ----------------------
# Turnaround Detector v6 # Turnaround Detector v6
# ---------------------- # ----------------------
@@ -595,8 +531,11 @@ def detect_turnaround_signal(symbol, data, interval=0):
ma5_cross_ma20 = (prev.MA5 <= prev.MA20) and (cur.MA5 > cur.MA20) ma5_cross_ma20 = (prev.MA5 <= prev.MA20) and (cur.MA5 > cur.MA20)
# 매수 신호 조건 (저점 + 단기 MA 돌파 + RSI 회복) # 매수 신호 조건 (저점 + 단기 MA 돌파 + RSI 회복)
# 매수 신호 조건: 전봉 하단 밴드 터치 + 반등 + MA5 돌파 + RSI 회복 # 볼린저 밴드 포지션
alert = touch_lower and rebound and ma5_break and rsi_recover distance = (cur.Close - cur.Lower) / (cur.Upper - cur.Lower)
# 매수 신호 조건: 전봉 하단 밴드 터치 + 반등 + MA5 돌파 + RSI 회복 + 밴드 위치 ≤ 0.35
alert = touch_lower and rebound and ma5_break and rsi_recover and (distance <= 0.35)
return { return {
"symbol": symbol, "symbol": symbol,

View File

@@ -95,8 +95,8 @@ def run_simulation(symbol: str, interval_minutes: int, days: int = 7):
if info and info["alert"]: if info and info["alert"]:
alerts.append((slice_df.index[-1], slice_df["Close"].iloc[-1])) alerts.append((slice_df.index[-1], slice_df["Close"].iloc[-1]))
# 8월 3일 이후의 매수 신호만 고려 # 모든 매수 신호를 표시
alerts = [(time, price) for time, price in alerts if time > pd.Timestamp('2025-08-03')] # 기존 필터 제거하여 전체 기간 매수 신호 사용
# Plot # Plot
plt.figure(figsize=(12, 6)) plt.figure(figsize=(12, 6))
@@ -124,6 +124,6 @@ def run_simulation(symbol: str, interval_minutes: int, days: int = 7):
if __name__ == "__main__": if __name__ == "__main__":
symbol = 'WLD' symbol = 'WLD'
interval = 240 interval = 60
days = 7 days = 7
run_simulation(symbol, interval, days) run_simulation(symbol, interval, days)