Spaces:
Running
Running
| # LLM.py (Updated to V5.4 - Fixed Re-Analysis TP/SL Wipe Bug) | |
| import os, traceback, asyncio, json, time | |
| import re | |
| from datetime import datetime | |
| from functools import wraps | |
| from backoff import on_exception, expo | |
| from openai import OpenAI, RateLimitError, APITimeoutError | |
| import numpy as np | |
| from sentiment_news import NewsFetcher | |
| from helpers import validate_required_fields, format_technical_indicators, format_strategy_scores, format_candle_data_for_pattern_analysis, format_whale_analysis_for_llm, parse_json_from_response | |
| from ml_engine.processor import safe_json_parse | |
| NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY") | |
| PRIMARY_MODEL = "nvidia/llama-3.1-nemotron-ultra-253b-v1" | |
| # (PatternAnalysisEngine - لا تغيير) | |
| class PatternAnalysisEngine: | |
| def __init__(self, llm_service): | |
| self.llm = llm_service | |
| def _format_chart_data_for_llm(self, ohlcv_data): | |
| if not ohlcv_data: return "Insufficient chart data for pattern analysis" | |
| try: | |
| all_timeframes = [] | |
| for timeframe, candles in ohlcv_data.items(): | |
| if candles and len(candles) >= 10: | |
| raw_candle_summary = self._format_raw_candle_data(candles, timeframe) | |
| all_timeframes.append(f"=== {timeframe.upper()} TIMEFRAME ({len(candles)} CANDLES) ===\n{raw_candle_summary}") | |
| return "\n\n".join(all_timeframes) if all_timeframes else "No sufficient timeframe data available" | |
| except Exception as e: return f"Error formatting chart data: {str(e)}" | |
| def _format_raw_candle_data(self, candles, timeframe): | |
| try: | |
| if len(candles) < 10: return f"Only {len(candles)} candles available - insufficient for deep pattern analysis" | |
| analysis_candles = candles[-50:] if len(candles) > 50 else candles | |
| summary = []; summary.append(f"Total candles: {len(candles)} (showing last {len(analysis_candles)})"); summary.append("Recent candles (newest to oldest):") | |
| for i in range(min(15, len(analysis_candles))): | |
| idx = len(analysis_candles) - 1 - i; candle = analysis_candles[idx] | |
| try: timestamp = datetime.fromtimestamp(candle[0] / 1000).strftime('%Y-%m-%d %H:%M:%S') | |
| except: timestamp = "unknown" | |
| open_price, high, low, close, volume = candle[1], candle[2], candle[3], candle[4], candle[5] | |
| candle_type = "🟢 BULLISH" if close > open_price else "🔴 BEARISH" if close < open_price else "⚪ NEUTRAL" | |
| body_size = abs(close - open_price); body_percent = (body_size / open_price * 100) if open_price > 0 else 0 | |
| wick_upper = high - max(open_price, close); wick_lower = min(open_price, close) - low; total_range = high - low | |
| if total_range > 0: body_ratio = (body_size / total_range) * 100; upper_wick_ratio = (wick_upper / total_range) * 100; lower_wick_ratio = (wick_lower / total_range) * 100 | |
| else: body_ratio = upper_wick_ratio = lower_wick_ratio = 0 | |
| summary.append(f"{i+1:2d}. {timestamp} | {candle_type}"); summary.append(f" O:{open_price:.8f} H:{high:.8f} L:{low:.8f} C:{close:.8f}"); summary.append(f" Body: {body_percent:.2f}% | Body/Range: {body_ratio:.1f}%"); summary.append(f" Wicks: Upper {upper_wick_ratio:.1f}% / Lower {lower_wick_ratio:.1f}%"); summary.append(f" Volume: {volume:,.0f}") | |
| if len(analysis_candles) >= 20: | |
| stats = self._calculate_candle_statistics(analysis_candles) | |
| summary.append(f"\n📊 STATISTICAL ANALYSIS:"); summary.append(f"• Price Change: {stats['price_change']:+.2f}%"); summary.append(f"• Average Body Size: {stats['avg_body']:.4f}%"); summary.append(f"• Volatility (ATR): {stats['atr']:.6f}"); summary.append(f"• Trend: {stats['trend']}"); summary.append(f"• Support: {stats['support']:.6f}"); summary.append(f"• Resistance: {stats['resistance']:.6f}") | |
| return "\n".join(summary) | |
| except Exception as e: return f"Error formatting raw candle data: {str(e)}" | |
| def _calculate_candle_statistics(self, candles): | |
| try: | |
| closes = [c[4] for c in candles]; opens = [c[1] for c in candles]; highs = [c[2] for c in candles]; lows = [c[3] for c in candles] | |
| first_close = closes[0]; last_close = closes[-1]; price_change = ((last_close - first_close) / first_close) * 100 | |
| body_sizes = [abs(close - open) for open, close in zip(opens, closes)]; avg_body = (sum(body_sizes) / len(body_sizes)) / first_close * 100 if first_close > 0 else 0 | |
| true_ranges = []; | |
| for i in range(1, len(candles)): high, low, prev_close = highs[i], lows[i], closes[i-1]; tr1 = high - low; tr2 = abs(high - prev_close); tr3 = abs(low - prev_close); true_ranges.append(max(tr1, tr2, tr3)) | |
| atr = sum(true_ranges) / len(true_ranges) if true_ranges else 0 | |
| if price_change > 3: trend = "STRONG UPTREND" | |
| elif price_change > 1: trend = "UPTREND" | |
| elif price_change < -3: trend = "STRONG DOWNTREND" | |
| elif price_change < -1: trend = "DOWNTREND" | |
| else: trend = "SIDEWAYS" | |
| support = min(lows); resistance = max(highs) | |
| return {'price_change': price_change, 'avg_body': avg_body, 'atr': atr, 'trend': trend, 'support': support, 'resistance': resistance} | |
| except Exception as e: return {'price_change': 0, 'avg_body': 0, 'atr': 0, 'trend': 'UNKNOWN', 'support': 0, 'resistance': 0} | |
| async def analyze_chart_patterns(self, symbol, ohlcv_data): pass | |
| def _parse_pattern_response(self, response_text): pass | |
| class LLMService: | |
| def __init__(self, api_key=NVIDIA_API_KEY, model_name=PRIMARY_MODEL, temperature=0.7): | |
| self.api_key = api_key | |
| self.model_name = model_name | |
| self.temperature = temperature | |
| self.client = OpenAI(base_url="https://integrate.api.nvidia.com/v1", api_key=self.api_key) | |
| self.news_fetcher = NewsFetcher() | |
| self.pattern_engine = PatternAnalysisEngine(self) | |
| self.semaphore = asyncio.Semaphore(5) | |
| self.r2_service = None | |
| self.learning_hub = None | |
| def _rate_limit_nvidia_api(func): | |
| async def wrapper(*args, **kwargs): | |
| return await func(*args, **kwargs) | |
| return wrapper | |
| async def get_trading_decision(self, data_payload: dict): | |
| try: | |
| symbol = data_payload.get('symbol', 'unknown') | |
| target_strategy = data_payload.get('target_strategy', 'GENERIC') | |
| ohlcv_data = data_payload.get('raw_ohlcv') or data_payload.get('ohlcv') | |
| if not ohlcv_data: return None | |
| total_candles = sum(len(data) for data in ohlcv_data.values() if data) if ohlcv_data else 0 | |
| if total_candles < 30: return None | |
| news_text = await self.news_fetcher.get_news_for_symbol(symbol) | |
| whale_data = data_payload.get('whale_data', {}) | |
| statistical_feedback = "No statistical learning data yet." | |
| active_context_playbook = "No active learning rules available." | |
| if self.learning_hub and self.learning_hub.initialized: | |
| statistical_feedback = await self.learning_hub.get_statistical_feedback_for_llm(target_strategy) | |
| active_context_playbook = await self.learning_hub.get_active_context_for_llm( | |
| domain="strategy", query=f"{target_strategy} {symbol}" | |
| ) | |
| prompt = self._create_comprehensive_sentry_prompt( | |
| data_payload, news_text, None, whale_data, | |
| statistical_feedback, active_context_playbook | |
| ) | |
| if self.r2_service: | |
| analysis_data = { 'symbol': symbol, 'target_strategy': target_strategy, 'statistical_feedback': statistical_feedback, 'active_context_playbook': active_context_playbook } | |
| await self.r2_service.save_llm_prompts_async( | |
| symbol, 'sentry_watchlist_decision_v5', prompt, analysis_data | |
| ) | |
| async with self.semaphore: | |
| response = await self._call_llm(prompt) | |
| decision_dict = self._parse_llm_response_enhanced(response, target_strategy, symbol) | |
| if decision_dict: | |
| if decision_dict.get('action') == 'WATCH' and 'strategy_to_watch' not in decision_dict: | |
| print(f" ⚠️ LLM {symbol}: Action is WATCH but strategy_to_watch is missing. Forcing HOLD.") | |
| decision_dict['action'] = 'HOLD' | |
| decision_dict['model_source'] = self.model_name | |
| decision_dict['whale_data_integrated'] = whale_data.get('data_available', False) | |
| return decision_dict | |
| else: | |
| print(f"❌ LLM parsing failed for {symbol} - no fallback decisions") | |
| return None | |
| except Exception as e: | |
| print(f"❌ Error in get_trading_decision for {data_payload.get('symbol', 'unknown')}: {e}"); traceback.print_exc() | |
| return None | |
| def _parse_llm_response_enhanced(self, response_text: str, fallback_strategy: str, symbol: str) -> dict: | |
| try: | |
| json_str = parse_json_from_response(response_text) | |
| if not json_str: | |
| print(f"❌ Failed to extract JSON from LLM response for {symbol}") | |
| return None | |
| decision_data = safe_json_parse(json_str) | |
| if not decision_data: | |
| print(f"❌ Failed to parse JSON (safe_json_parse) for {symbol}: {response_text}") | |
| return None | |
| if fallback_strategy == "reflection" or fallback_strategy == "distillation": | |
| return decision_data | |
| required_fields = ['action', 'reasoning', 'confidence_level', 'pattern_identified_by_llm'] | |
| if decision_data.get('action') == 'WATCH': | |
| required_fields.append('strategy_to_watch') | |
| elif decision_data.get('action') == 'BUY': # (احتياطي للنظام القديم) | |
| required_fields.extend(['risk_assessment', 'stop_loss', 'take_profit', 'expected_target_minutes', 'exit_profile', 'exit_parameters']) | |
| if not validate_required_fields(decision_data, required_fields): | |
| print(f"❌ Missing required fields in LLM response for {symbol}") | |
| missing = [f for f in required_fields if f not in decision_data] | |
| print(f" MIA: {missing}") | |
| return None | |
| action = decision_data.get('action') | |
| if action not in ['WATCH', 'HOLD']: | |
| # (السماح بـ 'BUY' كإجراء احتياطي إذا فشل النموذج في فهم 'WATCH') | |
| if action == 'BUY': | |
| print(f"⚠️ LLM {symbol} returned 'BUY' instead of 'WATCH'. Converting to 'WATCH'...") | |
| decision_data['action'] = 'WATCH' | |
| decision_data['strategy_to_watch'] = decision_data.get('strategy', fallback_strategy) | |
| else: | |
| print(f"⚠️ LLM suggested unsupported action ({action}) for {symbol}. Forcing HOLD.") | |
| decision_data['action'] = 'HOLD' | |
| if decision_data.get('action') == 'BUY': # (معالجة إضافية للحالة الاحتياطية) | |
| decision_data['trade_type'] = 'LONG' | |
| else: | |
| decision_data['trade_type'] = None | |
| # (تعديل: استخدام 'strategy_to_watch' بدلاً من 'strategy') | |
| strategy_value = decision_data.get('strategy_to_watch') if decision_data.get('action') == 'WATCH' else decision_data.get('strategy') | |
| if not strategy_value or strategy_value == 'unknown': | |
| decision_data['strategy'] = fallback_strategy | |
| if decision_data.get('action') == 'WATCH': | |
| decision_data['strategy_to_watch'] = fallback_strategy | |
| return decision_data | |
| except Exception as e: | |
| print(f"❌ Error parsing LLM response for {symbol}: {e}") | |
| return None | |
| async def _get_pattern_analysis(self, data_payload): | |
| try: | |
| symbol = data_payload['symbol'] | |
| ohlcv_data = data_payload.get('raw_ohlcv') or data_payload.get('ohlcv') | |
| if ohlcv_data: return None | |
| return None | |
| except Exception as e: | |
| print(f"❌ Pattern analysis failed for {data_payload.get('symbol')}: {e}") | |
| return None | |
| def _create_comprehensive_sentry_prompt( | |
| self, | |
| payload: dict, | |
| news_text: str, | |
| pattern_analysis: dict, | |
| whale_data: dict, | |
| statistical_feedback: str, | |
| active_context_playbook: str | |
| ) -> str: | |
| symbol = payload.get('symbol', 'N/A') | |
| current_price = payload.get('current_price', 'N/A') | |
| price_change_24h_raw = payload.get('price_change_24h', 0) | |
| price_change_24h_display = f"{price_change_24h_raw:+.2f}%" if isinstance(price_change_24h_raw, (int, float)) else "N/A" | |
| reasons = payload.get('reasons_for_candidacy', []) | |
| sentiment_data = payload.get('sentiment_data', {}) | |
| advanced_indicators = payload.get('advanced_indicators', {}) | |
| strategy_scores = payload.get('strategy_scores', {}) | |
| recommended_strategy = payload.get('recommended_strategy', 'N/A') | |
| target_strategy = payload.get('target_strategy', 'GENERIC') | |
| enhanced_final_score = payload.get('enhanced_final_score', 0) | |
| enhanced_score_display = f"{enhanced_final_score:.3f}" if isinstance(enhanced_final_score, (int, float)) else str(enhanced_final_score) | |
| indicators_summary = format_technical_indicators(advanced_indicators) | |
| strategies_summary = format_strategy_scores(strategy_scores, recommended_strategy) | |
| whale_analysis_section = format_whale_analysis_for_llm(whale_data) | |
| ohlcv_data = payload.get('raw_ohlcv') or payload.get('ohlcv', {}) | |
| candle_data_section = self._format_candle_data_comprehensive(ohlcv_data) | |
| market_context_section = self._format_market_context(sentiment_data) | |
| statistical_feedback_section = f"🧠 STATISTICAL FEEDBACK (Slow-Learner):\n{statistical_feedback}" | |
| playbook_section = f"📚 LEARNING PLAYBOOK (Fast-Learner Active Rules):\n{active_context_playbook}" | |
| exhaustion_warning = "" | |
| try: | |
| rsi_1d = advanced_indicators.get('1d', {}).get('rsi', 50) | |
| rsi_4h = advanced_indicators.get('4h', {}).get('rsi', 50) | |
| if price_change_24h_raw > 40 and (rsi_1d > 75 or rsi_4h > 75): | |
| exhaustion_warning = ( | |
| "🚩 **تنبيه استراتيجي: تم رصد زخم مرتفع (احتمال إرهاق)** 🚩\n" | |
| f"الأصل مرتفع {price_change_24h_display} خلال 24 ساعة ومؤشر RSI على 1D/4H في منطقة تشبع شرائي.\n" | |
| "هذا ليس أمر 'إيقاف'، بل هو 'تحدي تحليلي'. مهمتك هي التحقيق وتحديد ما إذا كان هذا:\n" | |
| "1. **فخ إرهاق (Exhaustion Trap):** (يجب 'HOLD')\n" | |
| "2. **اختراق استمراري حقيقي (Sustainable Continuation):** (يمكن 'WATCH')\n" | |
| "------------------------------------------------------------------" | |
| ) | |
| except Exception: | |
| pass | |
| prompt = f""" | |
| COMPREHENSIVE STRATEGIC ANALYSIS FOR {symbol} (FOR SENTRY WATCHLIST) | |
| 🚨 IMPORTANT: You are a STRATEGIC EXPLORER. Your job is NOT to execute a trade. Your job is to decide if this asset is interesting enough to be passed to the "SENTRY" (a high-speed tactical agent) for real-time monitoring and execution. | |
| {exhaustion_warning} | |
| 🎯 STRATEGY CONTEXT: | |
| * Target Strategy: {target_strategy} | |
| * Recommended Strategy (from ML): {recommended_strategy} | |
| * Current Price: ${current_price} | |
| * 24H Price Change: {price_change_24h_display} | |
| * Enhanced System Score: {enhanced_score_display} | |
| --- LEARNING HUB INPUT (CRITICAL) --- | |
| {playbook_section} | |
| {statistical_feedback_section} | |
| --- END OF LEARNING INPUT --- | |
| 📊 TECHNICAL INDICATORS (ALL TIMEFRAMES): | |
| {indicators_summary} | |
| 📈 RAW CANDLE DATA SUMMARY & STATISTICS (FOR YOUR PATTERN ANALYSIS): | |
| {candle_data_section} | |
| {chr(10)}--- END OF CANDLE DATA ---{chr(10)} | |
| 🎯 STRATEGY ANALYSIS (System's recommendation based on various factors): | |
| {strategies_summary} | |
| 🐋 WHALE ACTIVITY ANALYSIS: | |
| {whale_analysis_section} | |
| 🌍 MARKET CONTEXT: | |
| {market_context_section if market_context_section and "No market context" not in market_context_section else "Market context data not available for this analysis."} | |
| --- | |
| 🎯 SENTRY DECISION INSTRUCTIONS (WATCH or HOLD): | |
| 1. **PERFORM CHART PATTERN ANALYSIS:** Based *ONLY* on the provided 'RAW CANDLE DATA SUMMARY & STATISTICS', identify relevant patterns. | |
| 2. **[CRITICAL] INVESTIGATE THE STRATEGIC ALERT (if present):** | |
| * ** للتحقق من الاستمرارية (Continuation):** هل الارتفاع مدعوم بـ 'volume_ratio' عالي (موجود في المؤشرات)؟ هل هو اختراق واضح لنمط تجميعي (مثل 'Bull Flag' أو 'Consolidation Breakout')؟ هل الشموع قوية (أجسام كبيرة)؟ | |
| * ** للتحقق من الإرهاق (Exhaustion):** هل ترى 'Bearish Divergence' (السعر يصنع قمة جديدة بينما RSI/MACD لا يفعل)؟ هل يضعف 'volume_ratio' مع الصعود؟ هل تظهر شموع انعكاسية (Doji, Shooting Star) على 4H/1D؟ | |
| 3. **INTEGRATE ALL DATA:** ادمج 'تحقيقك' مع باقي البيانات (Learning Hub, Whale Activity). | |
| 4. **DECIDE ACTION (WATCH or HOLD):** | |
| * **WATCH:** فقط إذا أكد تحقيقك أنها 'Sustainable Continuation' ولديك ثقة عالية (>= 0.75). | |
| * **HOLD:** إذا أظهر تحقيقك أنها 'Exhaustion Trap'، أو إذا كان الوضع غير واضح ومحفوف بالمخاطر. | |
| 5. **DEFINE STRATEGY:** If (and only if) action is 'WATCH', you MUST specify which strategy the Sentry should use (e.g., 'breakout_momentum', 'mean_reversion'). | |
| 6. **SELF-CRITIQUE:** Justify your decision. Why is this strong enough for the Sentry? | |
| 7. **[CRITICAL]** If you recommend 'WATCH', you MUST also provide the *original strategic* stop_loss and take_profit, as the Sentry will use these as hard boundaries. | |
| OUTPUT FORMAT (JSON - SENTRY DECISION): | |
| {{ | |
| "action": "WATCH/HOLD", | |
| "reasoning": "Detailed explanation integrating ALL data sources. If the Strategic Alert was present, *explicitly state your investigation findings* (e.g., 'I confirmed this is continuation because volume is increasing and a bull flag is forming on 1H...')", | |
| "pattern_identified_by_llm": "Name of the primary pattern(s) identified (e.g., 'Bull Flag on 1H', 'No Clear Pattern')", | |
| "confidence_level": 0.85, | |
| "strategy_to_watch": "breakout_momentum", | |
| "stop_loss": 0.000000, | |
| "take_profit": 0.000000, | |
| "exit_profile": "ATR_TRAILING", | |
| "exit_parameters": {{ "atr_multiplier": 2.0, "atr_period": 14, "break_even_trigger_percent": 1.5 }}, | |
| "self_critique": {{ | |
| "failure_modes": [ | |
| "What is the first reason this 'WATCH' decision could fail? (e.g., 'The identified pattern is a false breakout.')", | |
| "What is the second reason? (e.g., 'The Sentry might enter too late.')" | |
| ], | |
| "confidence_adjustment_reason": "Brief reason if confidence was adjusted post-critique." | |
| }} | |
| }} | |
| """ | |
| return prompt | |
| def _format_candle_data_comprehensive(self, ohlcv_data): | |
| if not ohlcv_data: return "No raw candle data available for analysis" | |
| try: | |
| timeframes_available = []; total_candles = 0 | |
| for timeframe, candles in ohlcv_data.items(): | |
| if candles and len(candles) >= 5: timeframes_available.append(f"{timeframe.upper()} ({len(candles)} candles)"); total_candles += len(candles) | |
| if not timeframes_available: return "Insufficient candle data across all timeframes" | |
| summary = f"📊 Available Timeframes: {', '.join(timeframes_available)}\n" | |
| summary += f"📈 Total Candles Available: {total_candles}\n\n" | |
| raw_candle_analysis_text = self.pattern_engine._format_chart_data_for_llm(ohlcv_data) | |
| summary += raw_candle_analysis_text | |
| return summary | |
| except Exception as e: return f"Error formatting raw candle data: {str(e)}" | |
| def _analyze_timeframe_candles(self, candles, timeframe): | |
| # (دالة مساعدة - لا تغيير) | |
| return "" # (تم اختصارها) | |
| def _format_market_context(self, sentiment_data): | |
| if not sentiment_data or sentiment_data.get('data_quality', 'LOW') == 'LOW': return "Market context data not available or incomplete." | |
| btc_sentiment = sentiment_data.get('btc_sentiment', 'N/A'); fear_greed = sentiment_data.get('fear_and_greed_index', 'N/A'); market_trend = sentiment_data.get('market_trend', 'N/A') | |
| lines = [f"• Bitcoin Sentiment: {btc_sentiment}", f"• Fear & Greed Index: {fear_greed} ({sentiment_data.get('sentiment_class', 'Neutral')})", f"• Overall Market Trend: {market_trend.replace('_', ' ').title() if isinstance(market_trend, str) else 'N/A'}"] | |
| general_whale = sentiment_data.get('general_whale_activity', {}); | |
| if general_whale and general_whale.get('sentiment') != 'NEUTRAL': | |
| whale_sentiment = general_whale.get('sentiment', 'N/A'); critical_alert = general_whale.get('critical_alert', False) | |
| lines.append(f"• General Whale Sentiment: {whale_sentiment.replace('_', ' ').title() if isinstance(whale_sentiment, str) else 'N/A'}"); | |
| if critical_alert: lines.append(" ⚠️ CRITICAL WHALE ALERT ACTIVE") | |
| return "\n".join(lines) | |
| async def re_analyze_trade_async(self, trade_data: dict, processed_data: dict): | |
| try: | |
| symbol = trade_data['symbol']; original_strategy = trade_data.get('strategy', 'GENERIC') | |
| ohlcv_data = processed_data.get('raw_ohlcv') or processed_data.get('ohlcv') | |
| if not ohlcv_data: return None | |
| news_text = await self.news_fetcher.get_news_for_symbol(symbol) | |
| pattern_analysis = await self._get_pattern_analysis(processed_data) | |
| whale_data = processed_data.get('whale_data', {}) | |
| statistical_feedback = "No statistical learning data yet." | |
| active_context_playbook = "No active learning rules available." | |
| if self.learning_hub and self.learning_hub.initialized: | |
| statistical_feedback = await self.learning_hub.get_statistical_feedback_for_llm(original_strategy) | |
| active_context_playbook = await self.learning_hub.get_active_context_for_llm( | |
| domain="strategy", query=f"{original_strategy} {symbol} re-analysis" | |
| ) | |
| prompt = self._create_re_analysis_prompt( | |
| trade_data, processed_data, news_text, pattern_analysis, | |
| whale_data, statistical_feedback, active_context_playbook | |
| ) | |
| if self.r2_service: | |
| analysis_data = { 'symbol': symbol, 'original_strategy': original_strategy } | |
| await self.r2_service.save_llm_prompts_async( | |
| symbol, 'trade_reanalysis_v5_hub', prompt, analysis_data | |
| ) | |
| async with self.semaphore: | |
| response = await self._call_llm(prompt) | |
| # 🔴 --- START OF CHANGE (V5.4) --- 🔴 | |
| # (تمرير كائن الصفقة الأصلي بالكامل لنسخ القيم القديمة إذا لزم الأمر) | |
| re_analysis_dict = self._parse_re_analysis_response(response, original_strategy, symbol, trade_data) | |
| # 🔴 --- END OF CHANGE --- 🔴 | |
| if re_analysis_dict: | |
| re_analysis_dict['model_source'] = self.model_name | |
| return re_analysis_dict | |
| else: | |
| print(f"❌ LLM re-analysis parsing failed for {symbol}") | |
| return None | |
| except Exception as e: | |
| print(f"❌ Error in LLM re-analysis: {e}"); traceback.print_exc() | |
| return None | |
| # 🔴 --- START OF CHANGE (V5.4) --- 🔴 | |
| def _parse_re_analysis_response(self, response_text: str, fallback_strategy: str, symbol: str, original_trade: dict) -> dict: | |
| """(محدث V5.4) يضمن عدم مسح قيم SL/TP أبداً.""" | |
| try: | |
| json_str = parse_json_from_response(response_text) | |
| if not json_str: return None | |
| decision_data = safe_json_parse(json_str) | |
| if not decision_data: print(f"❌ Failed to parse JSON (safe_json_parse) for re-analysis of {symbol}: {response_text}"); return None | |
| action = decision_data.get('action') | |
| if action not in ['HOLD', 'CLOSE_TRADE', 'UPDATE_TRADE']: | |
| print(f"⚠️ LLM suggested unsupported re-analysis action ({action}) for {symbol}. Forcing HOLD.") | |
| decision_data['action'] = 'HOLD' | |
| # (منطق التحقق الجديد V5.4) | |
| if action == 'UPDATE_TRADE': | |
| required_update_fields = ['new_stop_loss', 'new_take_profit', 'new_exit_profile', 'new_exit_parameters'] | |
| if not validate_required_fields(decision_data, required_update_fields): | |
| print(f"❌ Missing required fields for UPDATE_TRADE for {symbol}"); decision_data['action'] = 'HOLD' | |
| elif not isinstance(decision_data['new_exit_parameters'], dict): | |
| print(f"❌ 'new_exit_parameters' is not a valid dict for {symbol}"); decision_data['action'] = 'HOLD' | |
| # (آلية الحماية من المسح) | |
| if action in ['HOLD', 'UPDATE_TRADE']: | |
| # التحقق من new_stop_loss | |
| new_sl = decision_data.get('new_stop_loss') | |
| if not isinstance(new_sl, (int, float)) or new_sl <= 0: | |
| print(f"⚠️ LLM Re-Analysis {symbol}: new_stop_loss is invalid ({new_sl}). Reverting to original SL.") | |
| decision_data['new_stop_loss'] = original_trade.get('stop_loss') | |
| # التحقق من new_take_profit | |
| new_tp = decision_data.get('new_take_profit') | |
| if not isinstance(new_tp, (int, float)) or new_tp <= 0: | |
| print(f"⚠️ LLM Re-Analysis {symbol}: new_take_profit is invalid ({new_tp}). Reverting to original TP.") | |
| decision_data['new_take_profit'] = original_trade.get('take_profit') | |
| strategy_value = decision_data.get('strategy') | |
| if not strategy_value or strategy_value == 'unknown': | |
| decision_data['strategy'] = fallback_strategy | |
| return decision_data | |
| except Exception as e: | |
| print(f"Error parsing re-analysis response for {symbol}: {e}") | |
| return None | |
| # 🔴 --- END OF CHANGE --- 🔴 | |
| def _create_re_analysis_prompt( | |
| self, | |
| trade_data: dict, processed_data: dict, news_text: str, | |
| pattern_analysis: dict, whale_data: dict, | |
| statistical_feedback: str, active_context_playbook: str | |
| ) -> str: | |
| symbol = trade_data.get('symbol', 'N/A'); entry_price = trade_data.get('entry_price', 'N/A'); current_price = processed_data.get('current_price', 'N/A'); strategy = trade_data.get('strategy', 'GENERIC'); current_exit_profile = trade_data.get('decision_data', {}).get('exit_profile', 'N/A'); current_exit_params = json.dumps(trade_data.get('decision_data', {}).get('exit_parameters', {})) | |
| # 🔴 --- START OF CHANGE (V5.4) --- 🔴 | |
| # (تمرير الأهداف الحالية إلى النموذج) | |
| current_sl = trade_data.get('stop_loss', 'N/A') | |
| current_tp = trade_data.get('take_profit', 'N/A') | |
| # 🔴 --- END OF CHANGE --- 🔴 | |
| statistical_feedback_section = f"🧠 STATISTICAL FEEDBACK (Slow-Learner):\n{statistical_feedback}"; playbook_section = f"📚 LEARNING PLAYBOOK (Fast-Learner Active Rules):\n{active_context_playbook}" | |
| try: price_change = ((current_price - entry_price) / entry_price) * 100 if entry_price else 0; price_change_display = f"{price_change:+.2f}%" | |
| except (TypeError, ZeroDivisionError): price_change_display = "N/A" | |
| price_change_24h_raw = processed_data.get('price_change_24h', 0) | |
| price_change_24h_display = f"{price_change_24h_raw:+.2f}%" if isinstance(price_change_24h_raw, (int, float)) else "N/A" | |
| indicators_summary = format_technical_indicators(processed_data.get('advanced_indicators', {})); pattern_summary = self._format_pattern_analysis(pattern_analysis) if pattern_analysis else "Pattern analysis data not available for re-analysis."; whale_analysis_section = format_whale_analysis_for_llm(whale_data); market_context_section = self._format_market_context(processed_data.get('sentiment_data', {})); ohlcv_data = processed_data.get('raw_ohlcv') or processed_data.get('ohlcv', {}); candle_data_section = self._format_candle_data_comprehensive(ohlcv_data) | |
| exhaustion_warning = "" | |
| try: | |
| rsi_1d = processed_data.get('advanced_indicators', {}).get('1d', {}).get('rsi', 50) | |
| if price_change_24h_raw > 40 and rsi_1d > 75: | |
| exhaustion_warning = ( | |
| "🚩 **تنبيه استراتيجي: تم رصد زخم مرتفع (احتمال إرهاق)** 🚩\n" | |
| f"الأصل مرتفع {price_change_24h_display} خلال 24 ساعة ومؤشر RSI على 1D/4H في منطقة تشبع شرائي.\n" | |
| "هذا يزيد من خطورة الاستمرار في الصفقة. قم بالتحقيق في قوة الاتجاه الحالية.\n" | |
| "هل بدأ الحجم (Volume) يضعف؟ هل تظهر إشارات انعكاس على 1H/4H؟\n" | |
| "------------------------------------------------------------------" | |
| ) | |
| except Exception: | |
| pass | |
| # 🔴 --- START OF CHANGE (V5.4) --- 🔴 | |
| prompt = f""" | |
| TRADE RE-ANALYSIS FOR {symbol} (SPOT ONLY - Currently Open LONG Position) | |
| {exhaustion_warning} | |
| 📊 CURRENT TRADE CONTEXT: | |
| * Strategy: {strategy} | |
| * Entry Price: {entry_price} (LONG position) | |
| * Current Price: {current_price} | |
| * 24H Price Change: {price_change_24h_display} | |
| * Current Performance: {price_change_display} | |
| * Current Exit Profile: {current_exit_profile} | |
| * CURRENT Stop Loss: {current_sl} | |
| * CURRENT Take Profit: {current_tp} | |
| --- LEARNING HUB INPUT (CRITICAL) --- | |
| {playbook_section} | |
| {statistical_feedback_section} | |
| --- END OF LEARNING INPUT --- | |
| 🔄 UPDATED TECHNICAL ANALYSIS: | |
| {indicators_summary} | |
| 📈 UPDATED RAW CANDLE DATA SUMMARY & STATISTICS: | |
| {candle_data_section} | |
| {chr(10)}--- END OF CANDLE DATA ---{chr(10)} | |
| 🐋 UPDATED WHALE ACTIVITY: | |
| {whale_analysis_section} | |
| 🌍 UPDATED MARKET CONTEXT: | |
| {market_context_section if market_context_section and "No market context" not in market_context_section else "Market context data not available for this re-analysis."} | |
| --- | |
| 🎯 RE-ANALYSIS INSTRUCTIONS (SPOT - LONG POSITION): | |
| 1. **Analyze the Data:** Review all new data (Indicators, Candles, Whale, Market). | |
| 2. **Evaluate Current State:** Is the original reason for entry still valid? Has the risk changed? | |
| 3. **Investigate Alert (if present):** If the 'Exhaustion Alert' is active, determine if the trend is weakening (exit) or just consolidating (hold). | |
| 4. **Decide Action (HOLD, CLOSE_TRADE, or UPDATE_TRADE):** | |
| * **HOLD:** The trade is still valid. The current strategy/targets are fine. | |
| * **CLOSE_TRADE:** The trade is invalidated (e.g., trend reversal, risk too high). | |
| * **UPDATE_TRADE:** The trade is valid, but the exit parameters need adjustment (e.g., raise stop loss to lock profit, or change exit profile). | |
| **[CRITICAL OUTPUT RULES - YOU MUST FOLLOW THESE]:** | |
| 1. You **MUST** return one of three actions: `HOLD`, `CLOSE_TRADE`, or `UPDATE_TRADE`. | |
| 2. If `action` is `HOLD` or `UPDATE_TRADE`, you **MUST** provide valid (non-zero) numeric values for `new_stop_loss` and `new_take_profit`. | |
| 3. **If `action` is `HOLD` and you do not want to change the targets,** you **MUST** return the *CURRENT* values (Current SL: {current_sl}, Current TP: {current_tp}) in the `new_stop_loss` and `new_take_profit` fields. | |
| 4. If `action` is `CLOSE_TRADE`, the values for targets/exit profile are irrelevant. | |
| OUTPUT FORMAT (JSON - SPOT RE-ANALYSIS): | |
| {{ | |
| "action": "HOLD/CLOSE_TRADE/UPDATE_TRADE", | |
| "reasoning": "Comprehensive justification. If 'HOLD' or 'UPDATE', justify the new (or existing) SL/TP values.", | |
| "new_stop_loss": 0.000000, | |
| "new_take_profit": 0.000000, | |
| "new_exit_profile": "The new exit profile (or the existing one if HOLD)", | |
| "new_exit_parameters": {{ "example_key": "example_value" }}, | |
| "new_expected_minutes": 15, | |
| "confidence_level": 0.85, | |
| "strategy": "{strategy}", | |
| "self_critique": {{ | |
| "failure_modes": ["Primary risk of this new decision?", "Second risk?"], | |
| "confidence_adjustment_reason": "Brief reason if confidence was adjusted." | |
| }} | |
| }} | |
| """ | |
| # 🔴 --- END OF CHANGE --- 🔴 | |
| return prompt | |
| def _format_pattern_analysis(self, pattern_analysis): | |
| if not pattern_analysis or not pattern_analysis.get('pattern_detected') or pattern_analysis.get('pattern_detected') == 'no_clear_pattern': return "No clear chart pattern detected by the system." | |
| pattern = pattern_analysis.get('pattern_detected', 'N/A'); confidence = pattern_analysis.get('pattern_confidence', 0); direction = pattern_analysis.get('predicted_direction', 'N/A'); timeframe = pattern_analysis.get('timeframe', 'N/A'); tf_display = f"on {timeframe} timeframe" if timeframe != 'N/A' else "" | |
| return f"System Pattern Analysis: Detected '{pattern}' {tf_display} with {confidence:.2f} confidence. Predicted direction: {direction}." | |
| async def _call_llm(self, prompt: str) -> str: | |
| try: | |
| for attempt in range(2): | |
| try: | |
| response = self.client.chat.completions.create( | |
| model=self.model_name, | |
| messages=[{"role": "user", "content": prompt}], | |
| temperature=self.temperature, | |
| seed=int(time.time()), | |
| max_tokens=4000 | |
| ) | |
| content = None | |
| if response.choices and response.choices[0].message: | |
| content = response.choices[0].message.content | |
| if content and '{' in content and '}' in content: | |
| return content | |
| else: | |
| if content is None: | |
| print(f"⚠️ LLM returned NO content (None) (attempt {attempt+1}). Check content filters or API status.") | |
| else: | |
| print(f"⚠️ LLM returned invalid content (not JSON) (attempt {attempt+1}): {content[:100]}...") | |
| if attempt == 0: await asyncio.sleep(1) | |
| except (RateLimitError, APITimeoutError) as e: | |
| print(f"❌ LLM API Error (Rate Limit/Timeout): {e}. Retrying via backoff...") | |
| raise | |
| except Exception as e: | |
| print(f"❌ Unexpected LLM API error (attempt {attempt+1}): {e}") | |
| if attempt == 0: await asyncio.sleep(2) | |
| elif attempt == 1: raise | |
| print("❌ LLM failed to return valid content after retries.") | |
| return "" | |
| except Exception as e: | |
| print(f"❌ Final failure in _call_llm after backoff retries: {e}") | |
| raise | |
| print("✅ LLM Service loaded - V5.4 (Fixed Re-Analysis TP/SL Wipe Bug)") |