import os import json import numpy as np from modules.services.ollama import OllamaManager from modules.analysis.technical import TechnicalAnalyzer from modules.analysis.deep_learning import PricePredictor # [최적화] 워커 프로세스별 전역 변수 (LSTM 모델 캐싱) _lstm_predictor = None def get_predictor(): """워커 프로세스 내에서 PricePredictor 인스턴스를 싱글톤으로 관리""" global _lstm_predictor if _lstm_predictor is None: print(f"[Worker {os.getpid()}] Initializing LSTM Predictor...") _lstm_predictor = PricePredictor() print(f"[Worker {os.getpid()}] LSTM Device: {_lstm_predictor.device}" f" | AMP: {_lstm_predictor.use_amp}") return _lstm_predictor def calculate_position_size(total_capital, current_price, volatility, score, ai_confidence, max_per_stock=3000000): """ [v2.0] 변동성 기반 포지션 사이징 (Modified Kelly Criterion) 핵심 원칙: 1. 변동성이 높으면 → 적은 수량 (리스크 관리) 2. 확신도(score)가 높으면 → 많은 수량 (기회 포착) 3. AI 신뢰도가 높으면 → 가산 비중 4. 절대 한 종목에 전체 자산의 15% 이상 투자하지 않음 Returns: int: 매수 수량 (0이면 매수 안 함) """ if current_price <= 0 or total_capital <= 0: return 0 # 1. 기본 투자금 (전체 자산의 10%) base_invest = total_capital * 0.10 # 2. 변동성 조절 계수 (변동성 높을수록 투자금 감소) # 변동성 1% → 1.0배, 2% → 0.75배, 3% → 0.5배, 5%+ → 0.3배 if volatility <= 1.0: vol_factor = 1.2 # 안정적 종목은 약간 증가 elif volatility <= 2.0: vol_factor = 1.0 elif volatility <= 3.0: vol_factor = 0.7 elif volatility <= 5.0: vol_factor = 0.45 else: vol_factor = 0.3 # 고변동 종목 # 3. 확신도 조절 계수 (score가 높을수록 투자금 증가) # score 0.6 → 0.5배, 0.7 → 1.0배, 0.8 → 1.5배, 0.9+ → 2.0배 if score >= 0.85: conf_factor = 2.0 elif score >= 0.75: conf_factor = 1.5 elif score >= 0.65: conf_factor = 1.0 else: conf_factor = 0.5 # 4. AI 신뢰도 가산 ai_bonus = 1.0 if ai_confidence >= 0.85: ai_bonus = 1.3 elif ai_confidence >= 0.7: ai_bonus = 1.1 # 5. 최종 투자금 계산 invest_amount = base_invest * vol_factor * conf_factor * ai_bonus # 상한 제한 invest_amount = min(invest_amount, max_per_stock) invest_amount = min(invest_amount, total_capital * 0.15) # 최대 15% invest_amount = min(invest_amount, total_capital) # 잔고 초과 방지 # 수량 계산 qty = int(invest_amount / current_price) return max(0, qty) def analyze_stock_process(ticker, prices, news_items, investor_trend=None, macro_status=None, holding_info=None): """ [v2.0] 종목 분석 + 매매 판단 (ProcessPoolExecutor에서 실행) [v2.0 개선사항] 1. ATR 기반 동적 손절/익절 + 트레일링 스탑 2. 포지션 사이징 (변동성 + 확신도 기반) 3. 시장상황별 동적 매수/매도 임계값 4. 보유종목에 대한 분석 기반 매도 판단 5. ADX/OBV/MTF 통합 기술적 분석 6. 강화된 AI 프롬프트 (종목 고유 뉴스 분석) """ try: print(f"⚙️ [Bot Process] Analyzing {ticker} ({len(prices)} candles)...") # ===== 1. 기술적 지표 계산 ===== current_price = prices[-1] if prices else 0 tech_score, rsi, volatility, vol_ratio, ma_info = TechnicalAnalyzer.get_technical_score( current_price, prices, volume_history=None) # ===== 2. ATR 기반 동적 손절/익절 ===== sl_tp = TechnicalAnalyzer.calculate_dynamic_sl_tp(prices) # ===== 3. LSTM 주가 예측 ===== lstm_predictor = get_predictor() if lstm_predictor: lstm_predictor.training_status['current_ticker'] = ticker pred_result = lstm_predictor.train_and_predict(prices, ticker=ticker) lstm_score = 0.5 ai_confidence = 0.5 ai_loss = 1.0 if pred_result: ai_confidence = pred_result.get('confidence', 0.5) ai_loss = pred_result.get('loss', 1.0) change_magnitude = min(abs(pred_result['change_rate']), 5.0) / 5.0 if pred_result['trend'] == 'UP': lstm_score = 0.5 + (change_magnitude * ai_confidence * 0.4) else: lstm_score = 0.5 - (change_magnitude * ai_confidence * 0.4) lstm_score = max(0.0, min(1.0, lstm_score)) # ===== 4. 수급 분석 (외인/기관) ===== investor_score = 0.0 frgn_net_buy = 0 orgn_net_buy = 0 consecutive_frgn_buy = 0 consecutive_orgn_buy = 0 if investor_trend: for day in investor_trend: frgn_net_buy += day['foreigner'] orgn_net_buy += day['institutional'] if day['foreigner'] > 0: consecutive_frgn_buy += 1 if day['institutional'] > 0: consecutive_orgn_buy += 1 # 외인 수급 점수 (강화) if frgn_net_buy > 0: investor_score += 0.03 if consecutive_frgn_buy >= 3: investor_score += 0.04 if consecutive_frgn_buy >= 5: investor_score += 0.03 # 5일 연속 매수 = 추가 보너스 # 기관 수급 점수 (신규) if orgn_net_buy > 0: investor_score += 0.02 if consecutive_orgn_buy >= 3: investor_score += 0.03 # 외인+기관 동시 순매수 = 강력 신호 if frgn_net_buy > 0 and orgn_net_buy > 0: investor_score += 0.03 print(f" 💰 [Investor] Both Foreign & Institutional Buying!") # ===== 5. AI 뉴스 분석 (강화된 프롬프트) ===== if pred_result: pred_price = pred_result.get('predicted', 0) pred_change = pred_result.get('change_rate', 0) else: pred_price = current_price pred_change = 0.0 ollama = OllamaManager() prompt = f""" [System Instruction] 1. Role: You are a legendary quant trader with 30 years of experience in Korean stock market. 2. You MUST analyze the data objectively and respond with a JSON object. [Market Data for Stock {ticker}] - Current Price: {current_price:,.0f} KRW - Technical Score: {tech_score:.3f} (RSI: {rsi:.1f}) - Moving Average: {ma_info['trend']} (Price is {ma_info['position']}) - ADX Trend Strength: {ma_info.get('adx', 20):.1f} ({ma_info.get('adx_trend', 'N/A')}) - Multi-Timeframe: {ma_info.get('mtf_alignment', 'N/A')} - AI Prediction: {pred_price:.0f} KRW ({pred_change:+.2f}%) - AI Confidence: {ai_confidence:.2f} (Training Loss: {ai_loss:.4f}) - Volatility: {volatility:.2f}% - Volume Ratio: {vol_ratio:.1f}x - ATR Stop Loss: {sl_tp['stop_loss_pct']:.1f}% / Take Profit: {sl_tp['take_profit_pct']:.1f}% - Investor Trend (5 Days): Foreigner Net Buy {frgn_net_buy}, Institutional Net Buy {orgn_net_buy} [Decision Framework] - Strong BUY signals: Foreigners+Institutions buying, Golden Cross, ADX>25 with bullish trend, AI high confidence UP - Moderate BUY: RSI<40 with bullish reversal, Price near Bollinger Lower Band - SELL signals: RSI>70, Dead Cross, ADX>25 with bearish trend, Foreigners selling - AVOID/HOLD: ADX<20 (sideways), Mixed signals, Low confidence [News Data] {json.dumps(news_items[:5] if news_items else [], ensure_ascii=False)} [Response Format - JSON Only] {{"sentiment_score": 0.0 to 1.0, "reason": "Brief analysis reason"}} """ ai_resp = ollama.request_inference(prompt) sentiment_score = 0.5 ai_reason = "" try: data = json.loads(ai_resp) sentiment_score = float(data.get("sentiment_score", 0.5)) sentiment_score = max(0.0, min(1.0, sentiment_score)) # 범위 강제 ai_reason = data.get("reason", "") except Exception: print(f" ⚠️ AI response parse failed, using neutral (0.5)") # ===== 6. 통합 점수 (동적 가중치 v2.0) ===== # ADX가 높으면 (추세가 강하면) LSTM과 기술적 분석 비중 증가 adx_val = ma_info.get('adx', 20) if ai_confidence >= 0.85 and adx_val >= 25: # 강한 추세 + 높은 AI 신뢰도: AI 최우선 w_tech, w_news, w_ai = 0.15, 0.15, 0.70 print(f" 🤖 [Ultra High Confidence + Strong Trend] AI Weight 70%") elif ai_confidence >= 0.85: w_tech, w_news, w_ai = 0.20, 0.20, 0.60 print(f" 🤖 [High Confidence] AI Weight 60%") elif adx_val >= 30: # 매우 강한 추세: 기술적 분석 우선 w_tech, w_news, w_ai = 0.50, 0.20, 0.30 print(f" 📊 [Very Strong Trend ADX={adx_val:.0f}] Tech Weight 50%") elif adx_val < 20: # 비추세/횡보: 뉴스와 수급 중시 w_tech, w_news, w_ai = 0.30, 0.40, 0.30 print(f" 📰 [Sideways ADX={adx_val:.0f}] News Weight 40%") else: w_tech, w_news, w_ai = 0.35, 0.30, 0.35 total_score = (w_tech * tech_score) + (w_news * sentiment_score) + (w_ai * lstm_score) # 수급 가산점 (최대 +0.15) total_score += min(investor_score, 0.15) total_score = min(total_score, 1.0) # ===== 7. 시장 상황별 동적 임계값 ===== buy_threshold = 0.60 sell_threshold = 0.30 if macro_status: macro_state = macro_status.get('status', 'SAFE') if macro_state == 'DANGER': buy_threshold = 999.0 # 매수 완전 차단 sell_threshold = 0.45 # 매도 기준 상향 (빨리 탈출) print(f" 🚨 [DANGER Market] Buy BLOCKED, Sell threshold raised to 0.45") elif macro_state == 'CAUTION': buy_threshold = 0.72 # 매수 기준 대폭 상향 (보수적) sell_threshold = 0.38 # 매도 기준도 상향 print(f" ⚠️ [CAUTION Market] Buy threshold raised to 0.72") # ===== 8. 매매 결정 ===== decision = "HOLD" decision_reason = "" # --- 보유 종목 분석 기반 매도 (신규) --- if holding_info: holding_yield = holding_info.get('yield', 0.0) holding_qty = holding_info.get('qty', 0) peak_price = holding_info.get('peak_price', current_price) if holding_qty > 0: # A. 동적 손절 (ATR 기반) if holding_yield <= sl_tp['stop_loss_pct']: decision = "SELL" decision_reason = f"Dynamic Stop Loss ({holding_yield:.1f}% <= {sl_tp['stop_loss_pct']:.1f}%)" # B. 동적 익절 (ATR 기반) elif holding_yield >= sl_tp['take_profit_pct']: decision = "SELL" decision_reason = f"Dynamic Take Profit ({holding_yield:.1f}% >= {sl_tp['take_profit_pct']:.1f}%)" # C. 트레일링 스탑 (최고가 대비 하락) elif peak_price > 0: drop_from_peak = ((current_price - peak_price) / peak_price) * 100 if drop_from_peak <= -sl_tp['trailing_stop_pct'] and holding_yield > 2.0: # 수익 상태에서만 트레일링 스탑 작동 (2% 이상 수익 확보) decision = "SELL" decision_reason = (f"Trailing Stop ({drop_from_peak:.1f}% from peak, " f"threshold: -{sl_tp['trailing_stop_pct']:.1f}%)") # D. 분석 기반 매도 (점수가 매도 임계값 이하) if decision == "HOLD" and total_score <= sell_threshold: decision = "SELL" decision_reason = f"Analysis Signal (Score: {total_score:.2f} <= {sell_threshold:.2f})" # E. 추세 반전 매도 (ADX 강한 하락추세) if decision == "HOLD" and adx_val >= 30: plus_di = ma_info.get('adx', 0) # 참고용 mtf_align = ma_info.get('mtf_alignment', '') if mtf_align == 'STRONG_BEAR' and holding_yield < 0: decision = "SELL" decision_reason = f"Strong Bear Trend Reversal (MTF: {mtf_align})" # --- 매수 판단 --- if decision == "HOLD": # 강한 단일 신호 매수 (기준 강화) strong_signal = False strong_reason = "" # [강화] 복합 조건 매수 (단일 지표가 아닌 복합 조건) if tech_score >= 0.75 and lstm_score >= 0.6 and sentiment_score >= 0.6: strong_signal = True strong_reason = "Triple Confirmation (Tech+AI+News)" elif lstm_score >= 0.80 and ai_confidence >= 0.85 and adx_val >= 25: strong_signal = True strong_reason = f"High Confidence AI + Strong Trend (ADX={adx_val:.0f})" elif investor_score >= 0.10 and tech_score >= 0.60 and total_score >= 0.60: strong_signal = True strong_reason = "Institutional Buying + Good Fundamentals" elif ma_info.get('mtf_alignment') == 'STRONG_BULL' and tech_score >= 0.60: strong_signal = True strong_reason = f"Strong Multi-Timeframe Bullish + Tech {tech_score:.2f}" if strong_signal and total_score >= buy_threshold - 0.05: # 강한 신호는 임계값 약간 완화 허용 decision = "BUY" decision_reason = strong_reason print(f" 🎯 [{strong_reason}] → BUY!") elif total_score >= buy_threshold: decision = "BUY" decision_reason = f"Score {total_score:.2f} >= threshold {buy_threshold:.2f}" # ===== 9. 포지션 사이징 ===== suggested_qty = 0 if decision == "BUY": # 기본 자산 1000만원으로 가정 (실제 run_cycle에서 덮어씀) suggested_qty = calculate_position_size( total_capital=10000000, current_price=current_price, volatility=volatility, score=total_score, ai_confidence=ai_confidence ) if suggested_qty == 0: decision = "HOLD" decision_reason = "Position size too small" print(f" └─ Scores: Tech={tech_score:.2f} News={sentiment_score:.2f} " f"LSTM={lstm_score:.2f} Inv={investor_score:.2f} → " f"Total={total_score:.2f} [{decision}]" f"{f' ({decision_reason})' if decision_reason else ''}") return { "ticker": ticker, "score": total_score, "tech": tech_score, "sentiment": sentiment_score, "lstm_score": lstm_score, "investor_score": investor_score, "volatility": volatility, "volume_ratio": vol_ratio, "prediction": pred_result, "decision": decision, "decision_reason": decision_reason, "current_price": current_price, "ma_info": ma_info, "sl_tp": sl_tp, "suggested_qty": suggested_qty, "ai_confidence": ai_confidence, "ai_reason": ai_reason } except Exception as e: print(f"❌ [Worker Error] Failed to analyze {ticker}: {e}") import traceback traceback.print_exc() return { "ticker": ticker, "score": 0.0, "decision": "HOLD", "decision_reason": f"Error: {str(e)}", "current_price": 0, "sl_tp": {'stop_loss_pct': -5.0, 'take_profit_pct': 8.0, 'trailing_stop_pct': 3.0}, "suggested_qty": 0, "error": str(e) }