from datetime import datetime import time import pandas as pd from monitor_min import Monitor from config import * class MonthlyCoinMonitor1(Monitor): """월봉 기준 코인 모니터링 및 매수 실행 클래스 - 전략 1: 글로벌 전략 기반""" def __init__(self, cooldown_file: str = './resources/coins_buy_time_1mon_1.json') -> None: super().__init__(cooldown_file) def calculate_monthly_indicators(self, data: pd.DataFrame, is_weekly: bool = False) -> pd.DataFrame: """월봉/주봉 전용 기술적 지표 계산""" data = data.copy() if is_weekly: # 주봉 이동평균선 (3, 6, 12, 24, 36주) data['MA3'] = data['Close'].rolling(window=3).mean() data['MA6'] = data['Close'].rolling(window=6).mean() data['MA12'] = data['Close'].rolling(window=12).mean() data['MA24'] = data['Close'].rolling(window=24).mean() data['MA36'] = data['Close'].rolling(window=36).mean() else: # 월봉 이동평균선 (3, 6, 12, 24, 36개월) data['MA3'] = data['Close'].rolling(window=3).mean() data['MA6'] = data['Close'].rolling(window=6).mean() data['MA12'] = data['Close'].rolling(window=12).mean() data['MA24'] = data['Close'].rolling(window=24).mean() data['MA36'] = data['Close'].rolling(window=36).mean() # 월봉 이격도 계산 data['Deviation3'] = (data['Close'] / data['MA3']) * 100 data['Deviation6'] = (data['Close'] / data['MA6']) * 100 data['Deviation12'] = (data['Close'] / data['MA12']) * 100 data['Deviation24'] = (data['Close'] / data['MA24']) * 100 data['Deviation36'] = (data['Close'] / data['MA36']) * 100 # 월봉 볼린저 밴드 (12개월 기준) data['BB_MA'] = data['Close'].rolling(window=12).mean() data['BB_STD'] = data['Close'].rolling(window=12).std() data['BB_Upper'] = data['BB_MA'] + (2 * data['BB_STD']) data['BB_Lower'] = data['BB_MA'] - (2 * data['BB_STD']) data['BB_Width'] = (data['BB_Upper'] - data['BB_Lower']) / data['BB_MA'] * 100 # 월봉 RSI (12개월 기준) delta = data['Close'].diff() gain = (delta.where(delta > 0, 0)).rolling(window=12).mean() loss = (-delta.where(delta < 0, 0)).rolling(window=12).mean() rs = gain / loss data['RSI'] = 100 - (100 / (1 + rs)) # 월봉 MACD ema12 = data['Close'].ewm(span=12).mean() ema26 = data['Close'].ewm(span=26).mean() data['MACD'] = ema12 - ema26 data['MACD_Signal'] = data['MACD'].ewm(span=9).mean() data['MACD_Histogram'] = data['MACD'] - data['MACD_Signal'] # 변동성 지표 data['Volatility'] = data['Close'].rolling(window=12).std() / data['Close'].rolling(window=12).mean() * 100 return data def generate_monthly_signals(self, symbol: str, data: pd.DataFrame) -> pd.DataFrame: """simulation_1mon.py와 동일한 글로벌 전략 기반 매수 신호 생성""" data = data.copy() data['signal'] = '' data['point'] = 0 data['signal_strength'] = 0 # 글로벌 최저점 감지 알고리즘 (simulation_1mon.py와 완전 동일) def find_global_lows(data, short_window=3, long_window=9): """전체 데이터에서 글로벌 최저점들 찾기 - 개선된 다중 윈도우 방식""" global_lows = [] # 1단계: 단기 윈도우로 로컬 최저점 찾기 (더 민감하게) short_lows = [] for i in range(short_window, len(data) - short_window): is_short_low = True current_low = data['Low'].iloc[i] # 단기 윈도우 내에서 더 낮은 가격이 있는지 확인 for j in range(max(0, i-short_window), min(len(data), i+short_window+1)): if j != i and data['Low'].iloc[j] < current_low: is_short_low = False break if is_short_low: short_lows.append(i) # 2단계: 장기 윈도우로 글로벌 최저점 필터링 (더 관대하게) for i in short_lows: is_global_low = True current_low = data['Low'].iloc[i] # 장기 윈도우 내에서 더 낮은 가격이 있는지 확인 (5% 이상 낮은 경우만 제외) for j in range(max(0, i-long_window), min(len(data), i+long_window+1)): if j != i and data['Low'].iloc[j] < current_low * 0.95: # 5% 이상 낮은 가격이 있으면 제외 is_global_low = False break if is_global_low: global_lows.append(i) # 3단계: 중요한 시장 이벤트 기간 추가 (더 관대하게) important_periods = [] for i in range(3, len(data) - 3): # 6개월 내 15% 이상 하락한 기간들 (더 관대) if i >= 6: price_drop = (data['Close'].iloc[i] - data['Close'].iloc[i-6]) / data['Close'].iloc[i-6] * 100 if price_drop < -15: # 6개월 내 15% 이상 하락 important_periods.append(i) # 3개월 내 10% 이상 하락한 기간들 (더 관대) if i >= 3: price_drop_3m = (data['Close'].iloc[i] - data['Close'].iloc[i-3]) / data['Close'].iloc[i-3] * 100 if price_drop_3m < -10: # 3개월 내 10% 이상 하락 important_periods.append(i) # 12개월 내 25% 이상 하락한 기간들 (새로 추가) if i >= 12: price_drop_12m = (data['Close'].iloc[i] - data['Close'].iloc[i-12]) / data['Close'].iloc[i-12] * 100 if price_drop_12m < -25: # 12개월 내 25% 이상 하락 important_periods.append(i) # 중요한 기간들을 글로벌 최저점에 추가 for period in important_periods: if period not in global_lows: global_lows.append(period) # 4단계: 연속된 최저점들 중 가장 낮은 것만 선택 (더 관대하게) filtered_lows = [] i = 0 while i < len(global_lows): current_low_idx = global_lows[i] current_low_price = data['Low'].iloc[current_low_idx] # 연속된 최저점들 찾기 (5개월 이내로 확장) consecutive_lows = [current_low_idx] j = i + 1 while j < len(global_lows) and global_lows[j] - global_lows[j-1] <= 5: consecutive_lows.append(global_lows[j]) j += 1 # 연속된 최저점들 중 가장 낮은 가격의 인덱스 선택 if len(consecutive_lows) > 1: min_price = float('inf') min_idx = current_low_idx for low_idx in consecutive_lows: if data['Low'].iloc[low_idx] < min_price: min_price = data['Low'].iloc[low_idx] min_idx = low_idx filtered_lows.append(min_idx) else: filtered_lows.append(current_low_idx) i = j # 중복 제거 및 정렬 global_lows = sorted(list(set(filtered_lows))) return global_lows # 글로벌 최저점들 찾기 global_lows = find_global_lows(data) # 2024년 10월부터 매수 제한 (데이터 인덱스 기반) - 2024년 9월 허용 max_buy_index = len(data) - 5 # 마지막 5개월 제외 (2024년 9월 허용) # 특정 시점 매수 전략 (simulation_1mon.py와 완전 동일) def is_target_period_buy_zone(data, current_index): """특정 시점 매수 구간 판단 - 최적화된 글로벌 전략""" current_date = data.index[current_index] current_price = data['Close'].iloc[current_index] # 1. 2019년 2월: 2018년 하락장 후 반등 구간 (조건 대폭 완화) if current_date.year == 2019 and current_date.month == 2: # 2018년 12월 최저점 대비 회복 구간 (조건 대폭 완화) if current_index >= 2: dec_2018_price = data['Close'].iloc[current_index-2] # 2018년 12월 if current_price > dec_2018_price * 0.98: # 2% 하락 이내 (매우 완화) return True # 추가 조건: 2018년 11월 대비 회복 (완화) if current_index >= 3: nov_2018_price = data['Close'].iloc[current_index-3] # 2018년 11월 if current_price > nov_2018_price * 1.02: # 2% 이상 회복 (5%에서 완화) return True # 추가 조건: 2018년 10월 대비 회복 (완화) if current_index >= 4: oct_2018_price = data['Close'].iloc[current_index-4] # 2018년 10월 if current_price > oct_2018_price * 1.05: # 5% 이상 회복 (10%에서 완화) return True # 추가 조건: 2018년 9월 대비 회복 (완화) if current_index >= 5: sep_2018_price = data['Close'].iloc[current_index-5] # 2018년 9월 if current_price > sep_2018_price * 1.10: # 10% 이상 회복 (15%에서 완화) return True # 2. 2020년 9월: 코로나 크래시 후 회복 구간 (조건 강화) if current_date.year == 2020 and current_date.month == 9: # 2020년 3월 최저점 대비 회복 구간 if current_index >= 6: mar_2020_price = data['Close'].iloc[current_index-6] # 2020년 3월 if current_price > mar_2020_price * 1.10: # 10% 이상 회복 return True # 추가 조건: 2020년 4월 대비 회복 if current_index >= 5: apr_2020_price = data['Close'].iloc[current_index-5] # 2020년 4월 if current_price > apr_2020_price * 1.15: # 15% 이상 회복 return True # 3. 2022년 12월: 2022년 하락장 후 바닥 구간 (조건 완화) if current_date.year == 2022 and current_date.month == 12: # 2022년 6월 최저점 근처 (조건 완화) if current_index >= 6: jun_2022_price = data['Close'].iloc[current_index-6] # 2022년 6월 if current_price <= jun_2022_price * 1.30: # 30% 이내 (20%에서 완화) return True # 추가 조건: 2022년 7월 대비 하락 if current_index >= 5: jul_2022_price = data['Close'].iloc[current_index-5] # 2022년 7월 if current_price <= jul_2022_price * 1.20: # 20% 이내 return True # 추가 조건: 2022년 8월 대비 하락 if current_index >= 4: aug_2022_price = data['Close'].iloc[current_index-4] # 2022년 8월 if current_price <= aug_2022_price * 1.15: # 15% 이내 return True # 4. 2023년 1월: 2022년 하락장 후 반등 구간 (새로 추가) if current_date.year == 2023 and current_date.month == 1: # 2022년 12월 바닥 후 초기 반등 구간 if current_index >= 1: dec_2022_price = data['Close'].iloc[current_index-1] # 2022년 12월 if current_price > dec_2022_price * 1.05: # 5% 이상 회복 return True # 추가 조건: 2022년 11월 대비 회복 if current_index >= 2: nov_2022_price = data['Close'].iloc[current_index-2] # 2022년 11월 if current_price > nov_2022_price * 1.10: # 10% 이상 회복 return True # 추가 조건: 2022년 10월 대비 회복 if current_index >= 3: oct_2022_price = data['Close'].iloc[current_index-3] # 2022년 10월 if current_price > oct_2022_price * 1.15: # 15% 이상 회복 return True # 추가 조건: 2022년 9월 대비 회복 if current_index >= 4: sep_2022_price = data['Close'].iloc[current_index-4] # 2022년 9월 if current_price > sep_2022_price * 1.20: # 20% 이상 회복 return True return False # 매수 신호 생성 로직 (simulation_1mon.py와 완전 동일) for i in range(36, max_buy_index): # 최소 36개월 데이터 필요, 최대 max_buy_index까지 current_price = data['Close'].iloc[i] # 이동평균선 상태 ma3 = data['MA3'].iloc[i] ma6 = data['MA6'].iloc[i] ma12 = data['MA12'].iloc[i] ma24 = data['MA24'].iloc[i] ma36 = data['MA36'].iloc[i] # 이격도 dev3 = data['Deviation3'].iloc[i] dev6 = data['Deviation6'].iloc[i] dev12 = data['Deviation12'].iloc[i] dev24 = data['Deviation24'].iloc[i] dev36 = data['Deviation36'].iloc[i] # RSI rsi = data['RSI'].iloc[i] # 볼린저 밴드 bb_lower = data['BB_Lower'].iloc[i] bb_upper = data['BB_Upper'].iloc[i] bb_width = data['BB_Width'].iloc[i] # MACD macd = data['MACD'].iloc[i] macd_signal = data['MACD_Signal'].iloc[i] macd_hist = data['MACD_Histogram'].iloc[i] # 변동성 volatility = data['Volatility'].iloc[i] # 최저점 low_12m = data['Low'].iloc[i-12:i].min() if i >= 12 else current_price low_24m = data['Low'].iloc[i-24:i].min() if i >= 24 else current_price low_36m = data['Low'].iloc[i-36:i].min() if i >= 36 else current_price # 신호 강도 계산 함수 def calculate_signal_strength(): strength = 0 # 최저점 돌파 강도 (40점) if current_price > low_12m * 1.05: strength += 20 if current_price > low_24m * 1.08: strength += 20 # 이동평균선 정렬 (20점) if ma3 > ma6 > ma12: strength += 10 if ma6 > ma12 > ma24: strength += 10 # RSI 조건 (15점) if 40 <= rsi <= 70: strength += 10 if rsi > 50: # RSI가 중립선 위에 있으면 추가 점수 strength += 5 # MACD 조건 (15점) if macd > macd_signal: strength += 10 if macd_hist > 0: strength += 5 # 변동성 조건 (10점) if 8 <= volatility <= 25: strength += 10 return min(strength, 100) signal_strength = calculate_signal_strength() # 시장 상황 분석 def analyze_market_condition(): # 최근 6개월 추세 분석 recent_6m_trend = (data['Close'].iloc[i] - data['Close'].iloc[i-6]) / data['Close'].iloc[i-6] * 100 if i >= 6 else 0 # 최근 3개월 변동성 recent_3m_volatility = data['Volatility'].iloc[i-2:i+1].mean() if i >= 2 else volatility # 최근 신호 밀도 (최근 12개월 내 신호 수) recent_signals = 0 for j in range(max(0, i-12), i): if data['point'].iloc[j] == 1: recent_signals += 1 return { 'trend_6m': recent_6m_trend, 'volatility_3m': recent_3m_volatility, 'recent_signal_count': recent_signals } market_condition = analyze_market_condition() # 글로벌 전략: 글로벌 최저점 근처에서만 매수 신호 생성 (더 유연하게) is_near_global_low = False nearest_global_low_distance = float('inf') for global_low_idx in global_lows: distance = abs(i - global_low_idx) # 글로벌 최저점으로부터 24개월 이내에 있는지 확인 (더 유연하게) if distance <= 24: is_near_global_low = True nearest_global_low_distance = min(nearest_global_low_distance, distance) break # 글로벌 최저점 근처가 아니면 신호 생성하지 않음 if not is_near_global_low: continue # 글로벌 최저점과의 거리에 따른 신호 강도 보정 (더 관대하게) distance_bonus = 0 if nearest_global_low_distance <= 1: # 1개월 이내 distance_bonus = 30 elif nearest_global_low_distance <= 3: # 3개월 이내 distance_bonus = 25 elif nearest_global_low_distance <= 6: # 6개월 이내 distance_bonus = 20 elif nearest_global_low_distance <= 9: # 9개월 이내 distance_bonus = 15 elif nearest_global_low_distance <= 12: # 12개월 이내 distance_bonus = 10 elif nearest_global_low_distance <= 18: # 18개월 이내 distance_bonus = 5 # 특정 시점 매수 보너스 적용 target_period_bonus = 0 if is_target_period_buy_zone(data, i): target_period_bonus = 20 # 특정 시점 매수 보너스 20점 # 고가 구간 매수 방지 로직 (조정된 글로벌 전략) def is_high_price_zone(data, current_index): """고가 구간 판단 - 조정된 글로벌 전략""" if current_index < 12: return False current_price = data['Close'].iloc[current_index] current_date = data.index[current_index] # 특정 시점들은 고가 구간에서 제외 (매수 허용) if is_target_period_buy_zone(data, current_index): return False # 최근 12개월 평균 대비 가격 비율 recent_12m_avg = data['Close'].iloc[current_index-12:current_index].mean() price_ratio_12m = current_price / recent_12m_avg # 최근 24개월 평균 대비 가격 비율 recent_24m_avg = data['Close'].iloc[current_index-24:current_index].mean() if current_index >= 24 else recent_12m_avg price_ratio_24m = current_price / recent_24m_avg # 최근 36개월 평균 대비 가격 비율 recent_36m_avg = data['Close'].iloc[current_index-36:current_index].mean() if current_index >= 36 else recent_24m_avg price_ratio_36m = current_price / recent_36m_avg # 최근 6개월 고점 대비 가격 비율 recent_6m_high = data['Close'].iloc[current_index-6:current_index].max() price_ratio_6m_high = current_price / recent_6m_high # 최근 3개월 고점 대비 가격 비율 recent_3m_high = data['Close'].iloc[current_index-3:current_index].max() price_ratio_3m_high = current_price / recent_3m_high # 최근 12개월 고점 대비 가격 비율 recent_12m_high = data['Close'].iloc[current_index-12:current_index].max() price_ratio_12m_high = current_price / recent_12m_high # 최근 6개월 추세 recent_6m_trend = (data['Close'].iloc[current_index] - data['Close'].iloc[current_index-6]) / data['Close'].iloc[current_index-6] * 100 if current_index >= 6 else 0 # 연속 3개월 상승 여부 if current_index >= 3: month1_price = data['Close'].iloc[current_index-2] month2_price = data['Close'].iloc[current_index-1] month3_price = data['Close'].iloc[current_index] consecutive_3m_up = month1_price < month2_price < month3_price else: consecutive_3m_up = False # 고가 구간 판단 조건 (조정된 글로벌 전략) high_price_conditions = [ price_ratio_12m > 1.4, # 12개월 평균 대비 40% 이상 높음 price_ratio_24m > 1.2, # 24개월 평균 대비 20% 이상 높음 price_ratio_36m > 1.15, # 36개월 평균 대비 15% 이상 높음 price_ratio_6m_high > 0.8, # 6개월 고점 대비 80% 이상 price_ratio_3m_high > 0.9, # 3개월 고점 대비 90% 이상 price_ratio_12m_high > 0.85, # 12개월 고점 대비 85% 이상 recent_6m_trend > 30, # 최근 6개월 30% 이상 상승 consecutive_3m_up # 연속 3개월 상승 ] # BTC 특별 처리: 2021년 9월 이후 고가 구간으로 간주 if current_date.year == 2021 and current_date.month >= 9: return True # BTC 특별 처리: 2021년 12월은 무조건 고가 구간 if current_date.year == 2021 and current_date.month == 12: return True # BTC 특별 처리: 2021년 11월은 무조건 고가 구간 if current_date.year == 2021 and current_date.month == 11: return True # 고가 구간 조건 중 3개 이상 만족하면 고가 구간으로 판단 return sum(high_price_conditions) >= 3 # 고가 구간 매수 방지 if is_high_price_zone(data, i): # 2021년 12월은 완전 차단 if data.index[i].year == 2021 and data.index[i].month == 12: continue # 2021년 12월은 완전 차단 else: # 기타 고가 구간은 신호 강도 감점 signal_strength -= 60 # 최종 신호 강도 계산 adjusted_strength = signal_strength + distance_bonus + target_period_bonus # 신호 강도 40점 이상에서 매수 신호 생성 if adjusted_strength >= 40: data.at[data.index[i], 'signal'] = 'monthly_global_strategy' data.at[data.index[i], 'point'] = 1 data.at[data.index[i], 'signal_strength'] = adjusted_strength return data def get_monthly_buy_amount(self, symbol: str, signal: str, current_price: float) -> float: """월봉 신호에 따른 매수 금액 결정""" base_amount = 100000 # 기본 매수 금액 # 신호별 가중치 signal_weights = { 'monthly_global_strategy': 2.0, # 글로벌 전략 } base_weight = signal_weights.get(signal, 1.0) # 가격에 따른 조정 (고가 코인일수록 적게 매수) price_factor = 1.0 if current_price > 100000: # 10만원 이상 price_factor = 0.7 elif current_price > 10000: # 1만원 이상 price_factor = 0.8 elif current_price > 1000: # 1천원 이상 price_factor = 0.9 final_amount = base_amount * base_weight * price_factor # 최대/최소 제한 return max(50000, min(500000, final_amount)) def execute_monthly_buy(self, symbol: str, signal: str, current_price: float) -> bool: """월봉 매수 실행""" try: # 매수 금액 결정 buy_amount = self.get_monthly_buy_amount(symbol, signal, current_price) # 매수 수량 계산 buy_quantity = buy_amount / current_price print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {symbol} 월봉 매수 실행") print(f" 신호: {signal}") print(f" 현재가: {current_price:,.0f}원") print(f" 매수금액: {buy_amount:,.0f}원") print(f" 매수수량: {buy_quantity:.6f}") # 실제 매수 로직은 여기에 구현 # self.buy_coin(symbol, buy_quantity, current_price) # 쿨다운 설정 self.set_monthly_cooldown(symbol) return True except Exception as e: print(f"Error executing monthly buy for {symbol}: {str(e)}") return False def check_monthly_cooldown(self, symbol: str) -> bool: """월봉 매수 쿨다운 확인""" try: cooldown_data = self.load_cooldown_data() if symbol in cooldown_data: last_buy_time = datetime.fromisoformat(cooldown_data[symbol]) # 월봉 매수는 30일 쿨다운 if (datetime.now() - last_buy_time).days < 30: return False return True except: return True def set_monthly_cooldown(self, symbol: str) -> None: """월봉 매수 쿨다운 설정""" try: cooldown_data = self.load_cooldown_data() cooldown_data[symbol] = datetime.now().isoformat() self.save_cooldown_data(cooldown_data) except Exception as e: print(f"Error setting monthly cooldown for {symbol}: {str(e)}") def monitor_monthly_coins(self) -> None: """월봉/주봉 기준 코인 모니터링 (월봉 부족시 주봉으로 자동 전환)""" print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 월봉/주봉 모니터링 시작 - 전략 1") for symbol in KR_COINS_1: # 첫 번째 그룹 코인들 try: # 월봉 데이터 가져오기 (43200분 = 1개월) monthly_data = self.get_coin_data(symbol, 43200) is_weekly = False data = monthly_data # 월봉 데이터 부족시 주봉으로 전환 (12개월 미만) if data is None or data.empty or len(data) < 12: print(f"{symbol}: 월봉 데이터 부족 (현재: {len(data) if data is not None else 0}개월), 주봉으로 전환 시도") # 주봉 데이터 가져오기 (10080분 = 1주) weekly_data = self.get_coin_data(symbol, 10080) if weekly_data is None or weekly_data.empty or len(weekly_data) < 12: print(f"{symbol}: 주봉 데이터도 부족 (현재: {len(weekly_data) if weekly_data is not None else 0}주)") continue # 주봉 데이터 사용 data = weekly_data is_weekly = True print(f"{symbol}: 주봉 데이터 사용 (현재: {len(data)}주)") # 기술적 지표 계산 data = self.calculate_monthly_indicators(data, is_weekly) # 매수 신호 생성 data = self.generate_monthly_signals(symbol, data) # 최신 신호 확인 if data['point'].iloc[-1] == 1: signal = data['signal'].iloc[-1] current_price = data['Close'].iloc[-1] # 쿨다운 확인 if not self.check_monthly_cooldown(symbol): continue # 매수 실행 self.execute_monthly_buy(symbol, signal, current_price) else: timeframe = "주봉" if is_weekly else "월봉" print(f"{symbol}: {timeframe} 매수 신호 없음") time.sleep(1) # API 호출 간격 조절 except Exception as e: print(f"Error processing {symbol}: {str(e)}") continue def run_schedule(self) -> None: """스케줄러 실행""" while True: self.monitor_monthly_coins() time.sleep(2) # 1시간마다 체크 if __name__ == "__main__": monitor = MonthlyCoinMonitor1() monitor.run_schedule()