init
This commit is contained in:
@@ -10,7 +10,7 @@ STOCK_TELEGRAM_CHAT_ID = '574661323'
|
|||||||
# 볼린저 밴드 설정
|
# 볼린저 밴드 설정
|
||||||
BOLLINGER_PERIOD = 20 # 볼린저 밴드 기간
|
BOLLINGER_PERIOD = 20 # 볼린저 밴드 기간
|
||||||
BOLLINGER_STD = 2 # 표준편차 승수
|
BOLLINGER_STD = 2 # 표준편차 승수
|
||||||
ALERT_THRESHOLD = 0.10 # 하단 밴드 대비 10% 근접 시 알림
|
BOLLINGER_THRESHOLD = 0.10 # 하단 밴드 대비 10% 근접 시 알림
|
||||||
BUY_THRESHOLD = 0.15
|
BUY_THRESHOLD = 0.15
|
||||||
|
|
||||||
KR_COINS = {
|
KR_COINS = {
|
||||||
@@ -171,7 +171,6 @@ KR_ETFS = {
|
|||||||
"034020.KS": "두산에너빌리티 / 원전,친환경",
|
"034020.KS": "두산에너빌리티 / 원전,친환경",
|
||||||
"160550.KQ": "NEW / 미디어,콘텐츠",
|
"160550.KQ": "NEW / 미디어,콘텐츠",
|
||||||
"089980.KQ": "상아프론테크 / 2차전지,소재",
|
"089980.KQ": "상아프론테크 / 2차전지,소재",
|
||||||
"066970.KQ": "엘앤에프 / 2차전지 소재",
|
|
||||||
"131970.KQ": "테스나 / 반도체 테스트",
|
"131970.KQ": "테스나 / 반도체 테스트",
|
||||||
"036930.KQ": "주성엔지니어링 / 반도체 장비",
|
"036930.KQ": "주성엔지니어링 / 반도체 장비",
|
||||||
"078600.KQ": "대주전자재료 / 2차전지 소재",
|
"078600.KQ": "대주전자재료 / 2차전지 소재",
|
||||||
@@ -182,7 +181,6 @@ KR_ETFS = {
|
|||||||
"068760.KQ": "셀트리온제약 / 바이오,제약",
|
"068760.KQ": "셀트리온제약 / 바이오,제약",
|
||||||
"032500.KQ": "케이엠더블유 / 5G,통신장비",
|
"032500.KQ": "케이엠더블유 / 5G,통신장비",
|
||||||
"178320.KQ": "서진시스템 / 5G,전장,통신",
|
"178320.KQ": "서진시스템 / 5G,전장,통신",
|
||||||
"091990.KQ": "셀트리온헬스케어 / 바이오,유통",
|
|
||||||
"095700.KQ": "제넥신 / 바이오,백신",
|
"095700.KQ": "제넥신 / 바이오,백신",
|
||||||
"084370.KQ": "유진테크 / 반도체 장비",
|
"084370.KQ": "유진테크 / 반도체 장비",
|
||||||
"069080.KQ": "웹젠 / 게임",
|
"069080.KQ": "웹젠 / 게임",
|
||||||
|
|||||||
158
stock_monitor.py
158
stock_monitor.py
@@ -38,23 +38,42 @@ def calculate_bollinger_bands(data):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
def check_bollinger_bands(symbol, data):
|
def calculate_technical_indicators(data):
|
||||||
if len(data) < BOLLINGER_PERIOD:
|
# 볼린저 밴드 계산
|
||||||
|
data = calculate_bollinger_bands(data)
|
||||||
|
|
||||||
|
# RSI 계산 (14일 기준)
|
||||||
|
delta = data['Close'].diff()
|
||||||
|
gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
|
||||||
|
loss = (-delta.where(delta < 0, 0)).rolling(window=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['MA5'] = data['Close'].rolling(window=5).mean()
|
||||||
|
data['MA20'] = data['Close'].rolling(window=20).mean()
|
||||||
|
data['MA60'] = data['Close'].rolling(window=60).mean()
|
||||||
|
|
||||||
|
# 거래량 이동평균
|
||||||
|
data['Volume_MA5'] = data['Volume'].rolling(window=5).mean()
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def check_buy_signals(symbol, data):
|
||||||
|
if len(data) < 60: # 최소 60일치 데이터 필요
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# 과거 10개 봉에서 ALERT_THRESHOLD 아래로 빠진 적이 있는지 체크
|
|
||||||
check = False
|
|
||||||
for i in range(-1, -2, -1):
|
|
||||||
past = data.iloc[i]
|
|
||||||
upper_band = past['Upper']
|
|
||||||
lower_band = past['Lower']
|
|
||||||
price = past['Close']
|
|
||||||
distance = (price - lower_band) / (upper_band - lower_band)
|
|
||||||
if distance < ALERT_THRESHOLD:
|
|
||||||
check = True
|
|
||||||
break
|
|
||||||
|
|
||||||
latest = data.iloc[-1]
|
latest = data.iloc[-1]
|
||||||
|
prev = data.iloc[-2]
|
||||||
|
|
||||||
|
# 볼린저 밴드 신호
|
||||||
|
bb_signal = False
|
||||||
if isinstance(latest['Upper'], float):
|
if isinstance(latest['Upper'], float):
|
||||||
upper_band = latest['Upper']
|
upper_band = latest['Upper']
|
||||||
lower_band = latest['Lower']
|
lower_band = latest['Lower']
|
||||||
@@ -63,20 +82,70 @@ def check_bollinger_bands(symbol, data):
|
|||||||
upper_band = latest['Upper'].iloc[0]
|
upper_band = latest['Upper'].iloc[0]
|
||||||
lower_band = latest['Lower'].iloc[0]
|
lower_band = latest['Lower'].iloc[0]
|
||||||
current_price = latest['Close'].iloc[0]
|
current_price = latest['Close'].iloc[0]
|
||||||
distance = (current_price - lower_band) / (upper_band - lower_band)
|
|
||||||
|
|
||||||
buy = False
|
distance = (current_price - lower_band) / (upper_band - lower_band)
|
||||||
if check and BUY_THRESHOLD < distance:
|
bb_signal = distance < BOLLINGER_THRESHOLD
|
||||||
buy = True
|
|
||||||
|
# RSI 과매도 신호 (RSI < 30)
|
||||||
|
rsi_signal = latest['RSI'].iloc[0] < 30
|
||||||
|
|
||||||
|
# MACD 신호 (MACD가 시그널 라인을 상향 돌파)
|
||||||
|
macd_signal = (prev['MACD'].iloc[0] < prev['Signal'].iloc[0]) and (latest['MACD'].iloc[0] > latest['Signal'].iloc[0])
|
||||||
|
|
||||||
|
# 이동평균선 골든크로스 임박 또는 발생
|
||||||
|
ma_signal = (prev['MA5'].iloc[0] < prev['MA20'].iloc[0]) and (latest['MA5'].iloc[0] >= latest['MA20'].iloc[0])
|
||||||
|
|
||||||
|
# 거래량 증가 신호 (5일 평균 대비 150% 이상)
|
||||||
|
volume_signal = latest['Volume'].iloc[0] > (latest['Volume_MA5'].iloc[0] * 1.5)
|
||||||
|
|
||||||
|
# 종합 신호
|
||||||
|
buy_signals = {
|
||||||
|
'bb_signal': bb_signal,
|
||||||
|
'rsi_signal': rsi_signal,
|
||||||
|
'macd_signal': macd_signal,
|
||||||
|
'ma_signal': ma_signal,
|
||||||
|
'volume_signal': volume_signal
|
||||||
|
}
|
||||||
|
|
||||||
|
# 최소 3개 이상의 신호가 동시에 발생할 때 매수 신호로 간주
|
||||||
|
signal_count = sum(1 for signal in buy_signals.values() if signal)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'symbol': symbol,
|
'symbol': symbol,
|
||||||
'price': current_price,
|
'price': current_price,
|
||||||
'lower_band': lower_band,
|
'lower_band': lower_band,
|
||||||
'distance': distance,
|
'distance': distance,
|
||||||
'buy': buy
|
'rsi': latest['RSI'].iloc[0],
|
||||||
|
'macd': latest['MACD'].iloc[0],
|
||||||
|
'signal_line': latest['Signal'].iloc[0],
|
||||||
|
'buy_signals': buy_signals,
|
||||||
|
'signal_count': signal_count,
|
||||||
|
'buy': signal_count >= 3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def format_message(info, market_type):
|
||||||
|
message = ""
|
||||||
|
if info['buy']:
|
||||||
|
message += '🛒 '
|
||||||
|
|
||||||
|
message += f"[{market_type}] {info['name']} ({info['symbol']}) "
|
||||||
|
message += f"현재가: {'$' if market_type == 'US' else '₩'}{info['price']:.2f}\n"
|
||||||
|
|
||||||
|
# 매수 신호 상세 정보
|
||||||
|
if any(info['buy_signals'].values()):
|
||||||
|
message += "📊 매수 신호:\n"
|
||||||
|
if info['buy_signals']['bb_signal']:
|
||||||
|
message += "- 볼린저 밴드 하단 근접 (근접도: {:.1f}%)\n".format(info['distance'] * 100)
|
||||||
|
if info['buy_signals']['rsi_signal']:
|
||||||
|
message += f"- RSI 과매도 구간 (RSI: {info['rsi']:.1f})\n"
|
||||||
|
if info['buy_signals']['macd_signal']:
|
||||||
|
message += "- MACD 골든크로스\n"
|
||||||
|
if info['buy_signals']['ma_signal']:
|
||||||
|
message += "- 이동평균선 골든크로스\n"
|
||||||
|
if info['buy_signals']['volume_signal']:
|
||||||
|
message += "- 거래량 급증\n"
|
||||||
|
|
||||||
|
return message
|
||||||
|
|
||||||
def get_coin_data(symbol, retries=3):
|
def get_coin_data(symbol, retries=3):
|
||||||
for attempt in range(retries):
|
for attempt in range(retries):
|
||||||
@@ -119,7 +188,7 @@ def get_stock_data(symbol, retries=3):
|
|||||||
for attempt in range(retries):
|
for attempt in range(retries):
|
||||||
try:
|
try:
|
||||||
end = datetime.now()
|
end = datetime.now()
|
||||||
start = end - timedelta(days=60)
|
start = end - timedelta(days=300)
|
||||||
data = yf.download(
|
data = yf.download(
|
||||||
symbol,
|
symbol,
|
||||||
start=start.strftime('%Y-%m-%d'),
|
start=start.strftime('%Y-%m-%d'),
|
||||||
@@ -141,29 +210,24 @@ def get_stock_data(symbol, retries=3):
|
|||||||
|
|
||||||
def monitor_us_stocks():
|
def monitor_us_stocks():
|
||||||
message = ""
|
message = ""
|
||||||
|
|
||||||
# 미국 주식 모니터링
|
|
||||||
print("US Stocks {}".format(datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
|
print("US Stocks {}".format(datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
|
||||||
|
|
||||||
for symbol in US_STOCKS:
|
for symbol in US_STOCKS:
|
||||||
data = get_stock_data(symbol)
|
data = get_stock_data(symbol)
|
||||||
if data is not None and not data.empty:
|
if data is not None and not data.empty:
|
||||||
try:
|
try:
|
||||||
data = calculate_bollinger_bands(data)
|
data = calculate_technical_indicators(data)
|
||||||
info = check_bollinger_bands(symbol, data)
|
info = check_buy_signals(symbol, data)
|
||||||
info['name'] = US_STOCKS[symbol]
|
info['name'] = US_STOCKS[symbol]
|
||||||
print(" - {} ({}): {:.2f} ({:.2f})".format(info['name'], symbol, info['price'], info['distance']))
|
print(f" - {info['name']} ({symbol}): {info['price']:.2f} -> {info['signal_count']}")
|
||||||
|
|
||||||
if info['buy']:
|
if info['buy'] or any(info['buy_signals'].values()):
|
||||||
message += '🛒'
|
message += format_message(info, 'US')
|
||||||
if info['distance'] < ALERT_THRESHOLD:
|
|
||||||
message += "🔔"
|
|
||||||
message += "[{}] {} ({}) 현재가: ${:.2f}, 근접도: {:.2f}%\n".format('US', info['name'], info['symbol'], info['price'], info['distance'] * 100)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error processing data for {symbol}: {str(e)}")
|
print(f"Error processing data for {symbol}: {str(e)}")
|
||||||
else:
|
|
||||||
print(f"Data for {symbol} is empty or None.")
|
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
if message:
|
||||||
try:
|
try:
|
||||||
send_stock_telegram_message(message)
|
send_stock_telegram_message(message)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -181,16 +245,13 @@ def monitor_kr_stocks():
|
|||||||
data = get_stock_data(symbol)
|
data = get_stock_data(symbol)
|
||||||
if data is not None and not data.empty:
|
if data is not None and not data.empty:
|
||||||
try:
|
try:
|
||||||
data = calculate_bollinger_bands(data)
|
data = calculate_technical_indicators(data)
|
||||||
info = check_bollinger_bands(symbol, data)
|
info = check_buy_signals(symbol, data)
|
||||||
info['name'] = KR_ETFS[symbol]
|
info['name'] = KR_ETFS[symbol]
|
||||||
print(" - {} ({}): {:.2f} ({:.2f})".format(info['name'], symbol, info['price'], info['distance']))
|
print(f" - {info['name']} ({symbol}): {info['price']:.2f} -> {info['signal_count']}")
|
||||||
|
|
||||||
if info['buy']:
|
if info['buy'] or any(info['buy_signals'].values()):
|
||||||
message += '🛒'
|
message += format_message(info, 'KR')
|
||||||
if info['distance'] < ALERT_THRESHOLD:
|
|
||||||
message += "🔔"
|
|
||||||
message += "[{}] {} ({}) 현재가: ${:.2f}, 근접도: {:.2f}%\n".format('KR', info['name'], info['symbol'], info['price'], info['distance'] * 100)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error processing data for {symbol}: {str(e)}")
|
print(f"Error processing data for {symbol}: {str(e)}")
|
||||||
@@ -215,17 +276,12 @@ def monitor_coins():
|
|||||||
data = get_coin_data(symbol)
|
data = get_coin_data(symbol)
|
||||||
if data is not None and not data.empty:
|
if data is not None and not data.empty:
|
||||||
try:
|
try:
|
||||||
data = calculate_bollinger_bands(data)
|
data = calculate_technical_indicators(data)
|
||||||
info = check_bollinger_bands(symbol, data)
|
info = check_buy_signals(symbol, data)
|
||||||
info['name'] = KR_COINS[symbol]
|
info['name'] = KR_COINS[symbol]
|
||||||
print(" - {} ({}): {:.2f} ({:.2f})".format(info['name'], symbol, info['price'], info['distance']))
|
print(f" - {info['name']} ({symbol}): {info['price']:.2f} -> {info['signal_count']}")
|
||||||
|
|
||||||
message += "· {} ({}) 현재가: ₩{}, 근접도: {:.2f}%".format(info['name'], info['symbol'], info['price'], info['distance'] * 100)
|
message += format_message(info, 'KR')
|
||||||
if info['buy']:
|
|
||||||
message += ' (🛒)'
|
|
||||||
if info['distance'] < ALERT_THRESHOLD:
|
|
||||||
message += "(🔔)"
|
|
||||||
message += '\n'
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error processing data for {symbol}: {str(e)}")
|
print(f"Error processing data for {symbol}: {str(e)}")
|
||||||
else:
|
else:
|
||||||
@@ -241,6 +297,10 @@ def monitor_coins():
|
|||||||
|
|
||||||
|
|
||||||
def run_schedule():
|
def run_schedule():
|
||||||
|
monitor_kr_stocks()
|
||||||
|
monitor_us_stocks()
|
||||||
|
monitor_coins()
|
||||||
|
|
||||||
# 코인 모니터링 스케줄 (매시간 1분, 11분, 21분, 31분, 41분, 51분)
|
# 코인 모니터링 스케줄 (매시간 1분, 11분, 21분, 31분, 41분, 51분)
|
||||||
for minute in [1, 11, 21, 31, 41, 51]:
|
for minute in [1, 11, 21, 31, 41, 51]:
|
||||||
schedule.every().hour.at(f":{minute:02d}").do(monitor_coins)
|
schedule.every().hour.at(f":{minute:02d}").do(monitor_coins)
|
||||||
|
|||||||
Reference in New Issue
Block a user