init
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user