import pandas as pd import plotly.graph_objs as go from plotly import subplots import plotly.io as pio from datetime import datetime import numpy as np import webbrowser import tempfile import os # Plotly 설정 pio.renderers.default = 'browser' from config import * # ======================================== # 월봉 시뮬레이션 설정 # ======================================== # 코인 선택 (대문자로 입력) COINS = ['XRP', 'ADA', 'APT', 'AVAX', 'BONK', 'BTC', 'ETC', 'HBAR', 'LINK', 'ONDO', 'PENGU', 'SEI', 'SOL', 'SUI', 'TRX', 'VIRTUAL', 'WLD', 'XLM'] COIN = COINS[5] # 기본값: XRP # 시뮬레이션 설정 INTERVAL = 43200 # 월봉 (43200분 = 30일) BONG_COUNT = 100 # 분석할 월봉 개수 (약 8년) SHOW_GRAPHS = True # 그래프 표시 여부 # ======================================== # 통합 최적화 월봉 전략 클래스 # ======================================== class OptimizedMonthlyStrategy: """통합 최적화 월봉 전략: 최저점 돌파 중심 + 고급 기술적 분석""" def __init__(self, coin: str): self.coin = coin self.name = KR_COINS.get(coin, coin) def get_buy_amount(self, signal: str, current_price: float, signal_strength: float = 70) -> float: """최적화된 월봉 신호에 따른 매수 금액 결정""" base_amount = 100000 # 기본 매수 금액 # 신호별 기본 가중치 signal_weights = { 'monthly_ultimate_breakout': 4.0, # 최고 강도 신호 (36개월 최저점 돌파) 'monthly_strong_breakout': 3.0, # 고강도 신호 (24개월 최저점 돌파) 'monthly_breakout': 2.5, # 중강도 신호 (12개월 최저점 돌파) 'monthly_bb_reversal': 2.0, # 보조 신호 (볼린저 밴드 반전) 'monthly_rsi_recovery': 1.8, # 보조 신호 (RSI 회복) 'monthly_golden_cross': 2.0, # 골든크로스 'monthly_deviation12': 1.5, # 이격도 신호 'monthly_macd_bullish': 1.4, # MACD 상승 'monthly_trend_reversal': 1.8 # 추세 전환 } base_weight = signal_weights.get(signal, 1.0) # 신호 강도에 따른 조정 (70-100점 범위를 0.8-1.2 배수로 변환) strength_factor = 0.8 + (signal_strength - 70) / 30 * 0.4 # 가격에 따른 조정 (고가 코인일수록 적게 매수) 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 * strength_factor * price_factor # 최대/최소 제한 return max(50000, min(500000, final_amount)) # ======================================== # 월봉 시뮬레이션 클래스 # ======================================== class OptimizedMonthlySimulation: """통합 최적화 월봉 기준 코인 시뮬레이션 클래스""" def __init__(self, coin: str) -> None: self.coin = coin self.strategy = OptimizedMonthlyStrategy(coin) self.strategy_type = 2 # 통합 최적화 전략 타입 # 모니터 클래스 임포트 (기존 파일 사용) from monitor_coin_1mon_1 import MonthlyCoinMonitor1 self.monitor = MonthlyCoinMonitor1() def fetch_weekly_data(self, symbol: str): """주봉 데이터 가져오기""" try: return self.monitor.get_coin_data(symbol, 10080) # 10080분 = 1주 except Exception as e: print(f"주봉 데이터 가져오기 실패: {e}") return None 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'] # 고급 지표들 (전략 2용) if self.strategy_type == 2: # 지수이동평균선 (EMA) data['EMA6'] = data['Close'].ewm(span=6).mean() data['EMA12'] = data['Close'].ewm(span=12).mean() data['EMA24'] = data['Close'].ewm(span=24).mean() # 다중 볼린저 밴드 for period in [6, 12, 24]: data[f'BB_MA_{period}'] = data['Close'].rolling(window=period).mean() data[f'BB_STD_{period}'] = data['Close'].rolling(window=period).std() data[f'BB_Upper_{period}'] = data[f'BB_MA_{period}'] + (2 * data[f'BB_STD_{period}']) data[f'BB_Lower_{period}'] = data[f'BB_MA_{period}'] - (2 * data[f'BB_STD_{period}']) data[f'BB_Width_{period}'] = (data[f'BB_Upper_{period}'] - data[f'BB_Lower_{period}']) / data[f'BB_MA_{period}'] * 100 # 다중 RSI for period in [6, 12, 24]: delta = data['Close'].diff() gain = (delta.where(delta > 0, 0)).rolling(window=period).mean() loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean() rs = gain / loss data[f'RSI_{period}'] = 100 - (100 / (1 + rs)) # 스토캐스틱 data['Stoch_K'] = ((data['Close'] - data['Low'].rolling(window=12).min()) / (data['High'].rolling(window=12).max() - data['Low'].rolling(window=12).min())) * 100 data['Stoch_D'] = data['Stoch_K'].rolling(window=3).mean() # 윌리엄스 %R data['Williams_R'] = ((data['High'].rolling(window=12).max() - data['Close']) / (data['High'].rolling(window=12).max() - data['Low'].rolling(window=12).min())) * -100 # CCI (Commodity Channel Index) tp = (data['High'] + data['Low'] + data['Close']) / 3 data['CCI'] = (tp - tp.rolling(window=12).mean()) / (0.015 * tp.rolling(window=12).std()) # ADX (Average Directional Index) high_diff = data['High'].diff() low_diff = data['Low'].diff() data['DM_Plus'] = np.where((high_diff > low_diff) & (high_diff > 0), high_diff, 0) data['DM_Minus'] = np.where((low_diff > high_diff) & (low_diff > 0), low_diff, 0) data['TR'] = np.maximum(data['High'] - data['Low'], np.maximum(abs(data['High'] - data['Close'].shift(1)), abs(data['Low'] - data['Close'].shift(1)))) data['DI_Plus'] = 100 * (data['DM_Plus'].rolling(window=12).mean() / data['TR'].rolling(window=12).mean()) data['DI_Minus'] = 100 * (data['DM_Minus'].rolling(window=12).mean() / data['TR'].rolling(window=12).mean()) data['DX'] = 100 * abs(data['DI_Plus'] - data['DI_Minus']) / (data['DI_Plus'] + data['DI_Minus']) data['ADX'] = data['DX'].rolling(window=12).mean() # 가격 모멘텀 지표 data['Momentum_6'] = data['Close'] / data['Close'].shift(6) * 100 data['Momentum_12'] = data['Close'] / data['Close'].shift(12) * 100 data['Momentum_24'] = data['Close'] / data['Close'].shift(24) * 100 # 변동성 지표 data['Volatility'] = data['Close'].rolling(window=12).std() / data['Close'].rolling(window=12).mean() * 100 return data def generate_optimized_signals(self, symbol: str, data: pd.DataFrame) -> pd.DataFrame: """글로벌 최적화 월봉 기반 매수 신호 생성 - 전체 그래프 최저점 기준""" data = data.copy() data['signal'] = '' data['point'] = 0 data['signal_strength'] = 0 # 신호 강도 (0-100) signal_count = 0 # 전체 데이터에서 글로벌 최저점들 찾기 (개선된 알고리즘 - 더 많은 매수 기회) 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, short_window=3, long_window=9) print(f"글로벌 최저점 {len(global_lows)}개 발견") # 글로벌 최저점 정보 출력 for i, low_idx in enumerate(global_lows): date = data.index[low_idx] price = data['Low'].iloc[low_idx] print(f" {i+1}. {date.strftime('%Y-%m')}: {price:.0f}원") # 2024년 10월부터 매수 제한 (데이터 인덱스 기반) - 2024년 9월 허용 # 데이터의 마지막 5개월은 매수 제한 (대략 2024년 11월 이후) max_buy_index = len(data) - 5 # 마지막 5개월 제외 (2024년 9월 허용) print(f"\n매수 제한 설정:") print(f" - 전체 데이터: {len(data)}개월") print(f" - 매수 가능 기간: {max_buy_index}개월까지") print(f" - 제한 기간: 마지막 5개월 (대략 2024년 11월 이후)") if max_buy_index < len(data): last_buy_date = data.index[max_buy_index-1] print(f" - 마지막 매수 가능일: {last_buy_date.strftime('%Y-%m')}") 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] # 스토캐스틱 stoch_k = data['Stoch_K'].iloc[i] stoch_d = data['Stoch_D'].iloc[i] # 윌리엄스 %R williams_r = data['Williams_R'].iloc[i] # CCI cci = data['CCI'].iloc[i] # ADX adx = data['ADX'].iloc[i] di_plus = data['DI_Plus'].iloc[i] di_minus = data['DI_Minus'].iloc[i] # 모멘텀 mom6 = data['Momentum_6'].iloc[i] mom12 = data['Momentum_12'].iloc[i] mom24 = data['Momentum_24'].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 # 특정 시점 매수 전략 (최적화된 글로벌 전략) 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 # 5. 2024년 9월: 최근 하락 후 반등 구간 (조건 대폭 완화) if current_date.year == 2024 and current_date.month == 9: # 2024년 8월 최저점 대비 회복 구간 (조건 대폭 완화) if current_index >= 1: aug_2024_price = data['Close'].iloc[current_index-1] # 2024년 8월 if current_price > aug_2024_price * 0.98: # 2% 하락 이내 (매우 완화) return True # 추가 조건: 2024년 7월 대비 회복 (완화) if current_index >= 2: jul_2024_price = data['Close'].iloc[current_index-2] # 2024년 7월 if current_price > jul_2024_price * 1.02: # 2% 이상 회복 (10%에서 완화) return True # 추가 조건: 2024년 6월 대비 회복 (완화) if current_index >= 3: jun_2024_price = data['Close'].iloc[current_index-3] # 2024년 6월 if current_price > jun_2024_price * 1.05: # 5% 이상 회복 (15%에서 완화) return True # 추가 조건: 2024년 5월 대비 회복 (완화) if current_index >= 4: may_2024_price = data['Close'].iloc[current_index-4] # 2024년 5월 if current_price > may_2024_price * 1.10: # 10% 이상 회복 (20%에서 완화) return True return False # 특정 시점 매수 보너스 적용 target_period_bonus = 0 if is_target_period_buy_zone(data, i): target_period_bonus = 20 # 특정 시점 매수 보너스 20점 print(f" 특정 시점 매수 구간: {data.index[i].strftime('%Y-%m')} - 보너스 {target_period_bonus}점") # 고가 구간 매수 방지 로직 (조정된 글로벌 전략) 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 # 1. 최근 12개월 대비 현재 가격이 50% 이상 높은 경우 (완화) recent_12m_avg = data['Close'].iloc[current_index-12:current_index].mean() price_ratio_12m = current_price / recent_12m_avg # 2. 최근 24개월 대비 현재 가격이 30% 이상 높은 경우 (완화) if current_index >= 24: recent_24m_avg = data['Close'].iloc[current_index-24:current_index].mean() price_ratio_24m = current_price / recent_24m_avg if price_ratio_24m > 1.3: # 24개월 평균 대비 30% 이상 높음 return True # 3. 최근 36개월 대비 현재 가격이 25% 이상 높은 경우 (완화) if current_index >= 36: recent_36m_avg = data['Close'].iloc[current_index-36:current_index].mean() price_ratio_36m = current_price / recent_36m_avg if price_ratio_36m > 1.25: # 36개월 평균 대비 25% 이상 높음 return True # 4. 12개월 평균 대비 50% 이상 높음 (완화) if price_ratio_12m > 1.5: return True # 5. 최근 6개월 내 최고가 대비 85% 이상인 경우 (완화) recent_6m_high = data['High'].iloc[current_index-6:current_index].max() if current_price / recent_6m_high > 0.85: return True # 6. 최근 3개월 내 최고가 대비 95% 이상인 경우 (완화) if current_index >= 3: recent_3m_high = data['High'].iloc[current_index-3:current_index].max() if current_price / recent_3m_high > 0.95: return True # 7. 연속 상승 구간 감지 (완화) if current_index >= 6: recent_6m_trend = (data['Close'].iloc[current_index] - data['Close'].iloc[current_index-6]) / data['Close'].iloc[current_index-6] * 100 if recent_6m_trend > 40: # 6개월 내 40% 이상 상승 return True # 8. 최근 12개월 내 최고가 대비 90% 이상인 경우 (완화) if current_index >= 12: recent_12m_high = data['High'].iloc[current_index-12:current_index].max() if current_price / recent_12m_high > 0.9: return True # 9. 연속 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] if month1_price < month2_price < month3_price: # 연속 3개월 상승 return True # 10. BTC 특화: 2021년 고가 구간 특별 감지 (유지) if current_date.year == 2021 and current_date.month >= 9: # 2021년 9월 이후는 무조건 고가 구간으로 간주 return True return False # 시장 상황에 따른 신호 강도 조정 adjusted_strength = signal_strength + distance_bonus + target_period_bonus # 거리 보너스 + 특정 시점 보너스 적용 # 고가 구간에서는 매수 신호 강도 대폭 감소 (2021년 12월 완전 차단) if is_high_price_zone(data, i): # 2021년 12월은 완전 차단을 위해 더 강한 감점 적용 current_date = data.index[i] if current_date.year == 2021 and current_date.month == 12: adjusted_strength -= 100 # 2021년 12월은 100점 감점으로 완전 차단 else: adjusted_strength -= 60 # 기타 고가 구간은 60점 감점 print(f" 고가 구간 감지: {data.index[i].strftime('%Y-%m')} - 신호 강도 {adjusted_strength:.1f}점") # 하락 추세에서는 더 높은 신호 강도 요구 if market_condition['trend_6m'] < -20: # 6개월 -20% 이상 하락 adjusted_strength -= 15 elif market_condition['trend_6m'] < -10: # 6개월 -10% 이상 하락 adjusted_strength -= 10 # 높은 변동성에서는 더 높은 신호 강도 요구 if market_condition['volatility_3m'] > 40: # 3개월 변동성 40% 이상 adjusted_strength -= 10 elif market_condition['volatility_3m'] > 30: # 3개월 변동성 30% 이상 adjusted_strength -= 5 # 글로벌 전략: 신호 강도 40점 이상 (글로벌 최저점 근처에서는 더 관대한 조건) if adjusted_strength >= 40: # 1. 최고 강도 신호: 36개월 최저점 돌파 + 모든 조건 만족 if (current_price > low_36m * 1.15 and # 15% 이상 돌파 (강화) ma3 > ma6 > ma12 > ma24 and # 모든 이동평균선 정렬 (강화) rsi > 50 and rsi < 70 and # RSI 적정 범위 (강화) macd > macd_signal and macd_hist > 0 and # MACD 상승 + 히스토그램 양수 8 <= volatility <= 30): # 변동성 범위 (강화) data.at[data.index[i], 'signal'] = 'monthly_ultimate_breakout' data.at[data.index[i], 'point'] = 1 data.at[data.index[i], 'signal_strength'] = adjusted_strength signal_count += 1 # 2. 고강도 신호: 24개월 최저점 돌파 + 강한 조건 elif (current_price > low_24m * 1.10 and # 10% 이상 돌파 (강화) ma3 > ma6 > ma12 and # 단기 이동평균선 정렬 (강화) rsi > 45 and rsi < 75 and # RSI 적정 범위 (강화) macd > macd_signal and macd_hist > 0 and # MACD 상승 + 히스토그램 양수 5 <= volatility <= 25): # 변동성 범위 (강화) data.at[data.index[i], 'signal'] = 'monthly_strong_breakout' data.at[data.index[i], 'point'] = 1 data.at[data.index[i], 'signal_strength'] = adjusted_strength signal_count += 1 # 3. 중강도 신호: 12개월 최저점 돌파 + 기본 조건 elif (current_price > low_12m * 1.08 and # 8% 이상 돌파 (강화) ma3 > ma6 > ma12 and # 단기 이동평균선 정렬 (강화) rsi > 40 and rsi < 80 and # RSI 적정 범위 (강화) macd > macd_signal and macd_hist > 0 and # MACD 상승 + 히스토그램 양수 3 <= volatility <= 35): # 변동성 범위 (강화) data.at[data.index[i], 'signal'] = 'monthly_breakout' data.at[data.index[i], 'point'] = 1 data.at[data.index[i], 'signal_strength'] = adjusted_strength signal_count += 1 # 4. 보조 신호: 볼린저 밴드 하단 터치 + 상승 (더 엄격한 조건) elif (current_price <= bb_lower * 1.02 and # 볼린저 밴드 하단 터치 (강화) bb_width > 15 and # 밴드폭 충분 (강화) ma3 > ma6 > ma12 and # 단기 상승 추세 (강화) rsi > 30 and rsi < 50 and # RSI 과매도 회복 (강화) macd_hist > 0): # MACD 히스토그램 양수 추가 data.at[data.index[i], 'signal'] = 'monthly_bb_reversal' data.at[data.index[i], 'point'] = 1 data.at[data.index[i], 'signal_strength'] = adjusted_strength signal_count += 1 # 5. 보조 신호: RSI 과매도 회복 (더 엄격한 조건) elif (rsi < 30 and # RSI 과매도 (강화) data['RSI'].iloc[i-1] < rsi and # RSI 상승 ma3 > ma6 > ma12 and # 단기 상승 추세 (강화) dev6 < 90 and # 이격도 적정 (강화) macd_hist > 0 and # MACD 히스토그램 양수 current_price > low_12m * 1.05): # 12개월 최저점 5% 이상 돌파 추가 data.at[data.index[i], 'signal'] = 'monthly_rsi_recovery' data.at[data.index[i], 'point'] = 1 data.at[data.index[i], 'signal_strength'] = adjusted_strength signal_count += 1 return data def fetch_monthly_data(self, symbol: str) -> pd.DataFrame: """월봉 데이터 가져오기""" return self.monitor.get_coin_more_data(symbol, INTERVAL, bong_count=BONG_COUNT) def render_optimized_plotly(self, symbol: str, data: pd.DataFrame) -> None: """통합 최적화 월봉 Plotly 차트 렌더링""" fig = subplots.make_subplots( rows=5, cols=1, subplot_titles=(f"{self.coin} 월봉 캔들차트", "이격도", "RSI & MACD", "볼린저 밴드", "신호 강도"), shared_xaxes=True, horizontal_spacing=0.03, vertical_spacing=0.03, row_heights=[0.3, 0.2, 0.2, 0.2, 0.1] ) # Row 1: 캔들 + 이동평균 fig.add_trace(go.Candlestick(x=data.index, open=data['Open'], high=data['High'], low=data['Low'], close=data['Close'], name=f'{self.coin} 월봉'), row=1, col=1) # 이동평균선 표시 for ma_col, color in [('MA3','red'),('MA6','blue'),('MA12','green'),('MA24','purple'),('MA36','brown')]: if ma_col in data.columns: fig.add_trace(go.Scatter(x=data.index, y=data[ma_col], name=ma_col, mode='lines', line=dict(color=color, width=2)), row=1, col=1) # 매수 포인트 표시 (신호 강도에 따른 크기 조정) buy_signals = ['monthly_ultimate_breakout', 'monthly_strong_breakout', 'monthly_breakout', 'monthly_bb_reversal', 'monthly_rsi_recovery'] colors = ['darkred', 'red', 'orange', 'blue', 'green'] sizes = [15, 12, 10, 8, 6] # 신호 강도에 따른 마커 크기 for sig, color, size in zip(buy_signals, colors, sizes): pts = data[(data['point']==1) & (data['signal']==sig)] if len(pts) > 0: # 신호 강도에 따른 크기 조정 marker_sizes = [size + (strength - 70) / 30 * 5 for strength in pts['signal_strength']] fig.add_trace(go.Scatter(x=pts.index, y=pts['Close'], mode='markers', name=f'{sig} 매수', marker=dict(color=color, size=marker_sizes, symbol='circle')), row=1, col=1) # Row 2: 이격도 for dev_col, color in [('Deviation3','red'),('Deviation6','blue'),('Deviation12','green'),('Deviation24','purple'),('Deviation36','brown')]: if dev_col in data.columns: fig.add_trace(go.Scatter(x=data.index, y=data[dev_col], name=dev_col, mode='lines', line=dict(color=color, width=2)), row=2, col=1) # 기준선 fig.add_hline(y=100, line_width=1, line_dash='dash', line_color='black', row=2, col=1) fig.add_hline(y=80, line_width=1, line_dash='dash', line_color='red', row=2, col=1) fig.add_hline(y=120, line_width=1, line_dash='dash', line_color='green', row=2, col=1) # Row 3: RSI & MACD if 'RSI' in data.columns: fig.add_trace(go.Scatter(x=data.index, y=data['RSI'], name='RSI', mode='lines', line=dict(color='purple', width=2)), row=3, col=1) fig.add_hline(y=30, line_width=1, line_dash='dash', line_color='red', row=3, col=1) fig.add_hline(y=70, line_width=1, line_dash='dash', line_color='green', row=3, col=1) if 'MACD' in data.columns: fig.add_trace(go.Scatter(x=data.index, y=data['MACD'], name='MACD', mode='lines', line=dict(color='blue', width=2)), row=3, col=1) fig.add_trace(go.Scatter(x=data.index, y=data['MACD_Signal'], name='MACD Signal', mode='lines', line=dict(color='red', width=2)), row=3, col=1) # Row 4: 볼린저 밴드 if 'BB_Upper' in data.columns and 'BB_Lower' in data.columns: fig.add_trace(go.Scatter(x=data.index, y=data['BB_Upper'], name='BB Upper', mode='lines', line=dict(color='gray', width=1, dash='dot')), row=4, col=1) fig.add_trace(go.Scatter(x=data.index, y=data['BB_Lower'], name='BB Lower', mode='lines', line=dict(color='gray', width=1, dash='dot')), row=4, col=1) fig.add_trace(go.Scatter(x=data.index, y=data['BB_MA'], name='BB MA', mode='lines', line=dict(color='orange', width=1)), row=4, col=1) # Row 5: 신호 강도 if 'signal_strength' in data.columns: signal_data = data[data['point'] == 1] if len(signal_data) > 0: fig.add_trace(go.Scatter(x=signal_data.index, y=signal_data['signal_strength'], mode='markers', name='신호 강도', marker=dict(color='red', size=8, symbol='diamond')), row=5, col=1) fig.add_hline(y=70, line_width=2, line_dash='dash', line_color='red', row=5, col=1) fig.update_layout( height=1400, margin=dict(t=180, l=40, r=240, b=40), title=dict( text=f"{self.coin} 통합 최적화 월봉 시뮬레이션 ({datetime.now().strftime('%Y-%m-%d %H:%M:%S')})", x=0.5, xanchor='center', y=0.995, yanchor='top', pad=dict(t=10, b=12) ), xaxis_rangeslider_visible=False, legend=dict(orientation='v', yref='paper', yanchor='top', y=1.0, xref='paper', xanchor='left', x=1.02), dragmode='zoom' ) fig.update_xaxes(title_text='시간', row=5, col=1) fig.update_yaxes(title_text='가격 (KRW)', row=1, col=1) fig.update_yaxes(title_text='이격도 (%)', row=2, col=1) fig.update_yaxes(title_text='RSI / MACD', row=3, col=1) fig.update_yaxes(title_text='볼린저 밴드', row=4, col=1) fig.update_yaxes(title_text='신호 강도', row=5, col=1) # 브라우저에서 차트 표시 try: fig.show(config={'scrollZoom': True, 'displaylogo': False}) print("브라우저에서 차트가 열렸습니다.") except Exception as e: print(f"브라우저 열기 실패: {e}") # HTML 파일로 저장 후 브라우저에서 열기 html_file = tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) fig.write_html(html_file.name) html_file.close() webbrowser.open(f'file://{html_file.name}') print(f"HTML 파일이 생성되었습니다: {html_file.name}") def create_plotly_figure(self, symbol: str, data: pd.DataFrame): """Plotly 차트 생성 (HTML 저장용)""" fig = subplots.make_subplots( rows=5, cols=1, subplot_titles=(f"{self.coin} 월봉 캔들차트", "이격도", "RSI & MACD", "볼린저 밴드", "신호 강도"), shared_xaxes=True, horizontal_spacing=0.03, vertical_spacing=0.03, row_heights=[0.3, 0.2, 0.2, 0.2, 0.1] ) # Row 1: 캔들 + 이동평균 fig.add_trace(go.Candlestick(x=data.index, open=data['Open'], high=data['High'], low=data['Low'], close=data['Close'], name=f'{self.coin} 월봉'), row=1, col=1) # 이동평균선 표시 for ma_col, color in [('MA3','red'),('MA6','blue'),('MA12','green'),('MA24','purple'),('MA36','brown')]: if ma_col in data.columns: fig.add_trace(go.Scatter(x=data.index, y=data[ma_col], name=ma_col, mode='lines', line=dict(color=color, width=2)), row=1, col=1) # 매수 포인트 표시 buy_signals = ['monthly_ultimate_breakout', 'monthly_strong_breakout', 'monthly_breakout', 'monthly_bb_reversal', 'monthly_rsi_recovery'] colors = ['darkred', 'red', 'orange', 'blue', 'green'] sizes = [15, 12, 10, 8, 6] for sig, color, size in zip(buy_signals, colors, sizes): pts = data[(data['point']==1) & (data['signal']==sig)] if len(pts) > 0: marker_sizes = [size + (strength - 70) / 30 * 5 for strength in pts['signal_strength']] fig.add_trace(go.Scatter(x=pts.index, y=pts['Close'], mode='markers', name=f'{sig} 매수', marker=dict(color=color, size=marker_sizes, symbol='circle')), row=1, col=1) # Row 2: 이격도 for dev_col, color in [('Deviation3','red'),('Deviation6','blue'),('Deviation12','green'),('Deviation24','purple'),('Deviation36','brown')]: if dev_col in data.columns: fig.add_trace(go.Scatter(x=data.index, y=data[dev_col], name=dev_col, mode='lines', line=dict(color=color, width=2)), row=2, col=1) # 기준선 fig.add_hline(y=100, line_width=1, line_dash='dash', line_color='black', row=2, col=1) fig.add_hline(y=80, line_width=1, line_dash='dash', line_color='red', row=2, col=1) fig.add_hline(y=120, line_width=1, line_dash='dash', line_color='green', row=2, col=1) # Row 3: RSI & MACD if 'RSI' in data.columns: fig.add_trace(go.Scatter(x=data.index, y=data['RSI'], name='RSI', mode='lines', line=dict(color='purple', width=2)), row=3, col=1) fig.add_hline(y=30, line_width=1, line_dash='dash', line_color='red', row=3, col=1) fig.add_hline(y=70, line_width=1, line_dash='dash', line_color='green', row=3, col=1) if 'MACD' in data.columns: fig.add_trace(go.Scatter(x=data.index, y=data['MACD'], name='MACD', mode='lines', line=dict(color='blue', width=2)), row=3, col=1) fig.add_trace(go.Scatter(x=data.index, y=data['MACD_Signal'], name='MACD Signal', mode='lines', line=dict(color='red', width=2)), row=3, col=1) # Row 4: 볼린저 밴드 if 'BB_Upper' in data.columns and 'BB_Lower' in data.columns: fig.add_trace(go.Scatter(x=data.index, y=data['BB_Upper'], name='BB Upper', mode='lines', line=dict(color='gray', width=1, dash='dot')), row=4, col=1) fig.add_trace(go.Scatter(x=data.index, y=data['BB_Lower'], name='BB Lower', mode='lines', line=dict(color='gray', width=1, dash='dot')), row=4, col=1) fig.add_trace(go.Scatter(x=data.index, y=data['BB_MA'], name='BB MA', mode='lines', line=dict(color='orange', width=1)), row=4, col=1) # Row 5: 신호 강도 if 'signal_strength' in data.columns: signal_data = data[data['point'] == 1] if len(signal_data) > 0: fig.add_trace(go.Scatter(x=signal_data.index, y=signal_data['signal_strength'], mode='markers', name='신호 강도', marker=dict(color='red', size=8, symbol='diamond')), row=5, col=1) fig.add_hline(y=70, line_width=2, line_dash='dash', line_color='red', row=5, col=1) fig.update_layout( height=1400, margin=dict(t=180, l=40, r=240, b=40), title=dict( text=f"{self.coin} 통합 최적화 월봉 시뮬레이션 ({datetime.now().strftime('%Y-%m-%d %H:%M:%S')})", x=0.5, xanchor='center', y=0.995, yanchor='top', pad=dict(t=10, b=12) ), xaxis_rangeslider_visible=False, legend=dict(orientation='v', yref='paper', yanchor='top', y=1.0, xref='paper', xanchor='left', x=1.02), dragmode='zoom' ) fig.update_xaxes(title_text='시간', row=5, col=1) fig.update_yaxes(title_text='가격 (KRW)', row=1, col=1) fig.update_yaxes(title_text='이격도 (%)', row=2, col=1) fig.update_yaxes(title_text='RSI / MACD', row=3, col=1) fig.update_yaxes(title_text='볼린저 밴드', row=4, col=1) fig.update_yaxes(title_text='신호 강도', row=5, col=1) return fig def run_optimized_simulation(self, symbol: str): """통합 최적화 월봉/주봉 시뮬레이션 실행 (월봉 부족시 주봉으로 자동 전환)""" print(f"\n=== {self.coin} 통합 최적화 월봉/주봉 시뮬레이션 시작 ===") # 월봉 데이터 가져오기 monthly_data = self.fetch_monthly_data(symbol) is_weekly = False data = monthly_data # 월봉 데이터 부족시 주봉으로 전환 (12개월 미만) if data is None or data.empty or len(data) < 12: print(f"월봉 데이터 부족 (현재: {len(data) if data is not None else 0}개월), 주봉으로 전환 시도") # 주봉 데이터 가져오기 (10080분 = 1주) weekly_data = self.fetch_weekly_data(symbol) if weekly_data is None or weekly_data.empty or len(weekly_data) < 12: print(f"주봉 데이터도 부족 (현재: {len(weekly_data) if weekly_data is not None else 0}주)") return [], [] # 주봉 데이터 사용 data = weekly_data is_weekly = True print(f"주봉 데이터 사용 (현재: {len(data)}주)") timeframe = "주봉" if is_weekly else "월봉" print(f"데이터 기간: {data.index[0]} ~ {data.index[-1]}") print(f"총 {timeframe} 수: {len(data)}") # 기술적 지표 계산 data = self.calculate_monthly_indicators(data, is_weekly) # 매수 신호 생성 data = self.generate_optimized_signals(symbol, data) # 매수 신호 분석 alerts = [] total_buy_amount = 0 for i in range(len(data)): if data['point'].iloc[i] == 1: signal = data['signal'].iloc[i] price = data['Close'].iloc[i] signal_strength = data['signal_strength'].iloc[i] buy_amount = self.strategy.get_buy_amount(signal, price, signal_strength) total_buy_amount += buy_amount alerts.append((data.index[i], price, signal, buy_amount, signal_strength)) print(f"\n총 매수 신호 수: {len(alerts)}") print(f"총 매수 금액: {total_buy_amount:,.0f}원") # 신호별 분석 signal_counts = {} signal_amounts = {} signal_strengths = {} for _, _, signal, amount, strength in alerts: signal_counts[signal] = signal_counts.get(signal, 0) + 1 signal_amounts[signal] = signal_amounts.get(signal, 0) + amount if signal not in signal_strengths: signal_strengths[signal] = [] signal_strengths[signal].append(strength) print("\n신호별 분석:") for signal in signal_counts: avg_strength = sum(signal_strengths[signal]) / len(signal_strengths[signal]) print(f" - {signal}: {signal_counts[signal]}회, 총 {signal_amounts[signal]:,.0f}원, 평균 강도: {avg_strength:.1f}") # 수익률 분석 (간단한 백테스팅) if len(alerts) > 0: print("\n수익률 분석:") total_investment = 0 total_value = 0 for date, buy_price, signal, buy_amount, strength in alerts: total_investment += buy_amount # 현재 가격으로 평가 (마지막 가격 기준) current_price = data['Close'].iloc[-1] shares = buy_amount / buy_price current_value = shares * current_price total_value += current_value if total_investment > 0: total_return = ((total_value - total_investment) / total_investment) * 100 print(f" - 총 투자금액: {total_investment:,.0f}원") print(f" - 현재 평가금액: {total_value:,.0f}원") print(f" - 총 수익률: {total_return:.2f}%") # Plotly 기반 시각화 if SHOW_GRAPHS: self.render_optimized_plotly(symbol, data) # 추가로 HTML 파일 생성 html_filename = f"{self.coin}_monthly_simulation_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html" fig = self.create_plotly_figure(symbol, data) fig.write_html(html_filename) print(f"\n차트가 HTML 파일로 저장되었습니다: {html_filename}") print(f"브라우저에서 {html_filename} 파일을 열어서 차트를 확인하세요.") return alerts, [] # ======================================== # 메인 실행 부분 # ======================================== if __name__ == "__main__": print(f"\n=== 통합 최적화 월봉 시뮬레이션 시작 ===") print(f"코인: {COIN}") print(f"월봉 개수: {BONG_COUNT}개") print(f"그래프 표시: {'예' if SHOW_GRAPHS else '아니오'}") print("=" * 50) try: sim = OptimizedMonthlySimulation(COIN) buy_alerts, sell_alerts = sim.run_optimized_simulation(COIN) print(f"\n=== 통합 최적화 월봉 시뮬레이션 완료 ===") print(f"매수 신호: {len(buy_alerts)}회") except Exception as e: print(f"Error analyzing {COIN}: {str(e)}") print("\n지원되는 코인 목록:") print("XRP, ADA, APT, AVAX, BONK, BTC, ETC, HBAR, LINK, ONDO, PENGU, SEI, SOL, SUI, TRX, VIRTUAL, WLD, XLM")