Spaces:
Running
Running
| # LLM.py | |
| import os, traceback, asyncio, json, time | |
| import re # ✅ استيراد مكتبة 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 | |
| # ✅ تعديل الاستيراد: parse_json_from_response لم يعد مستخدماً هنا بشكل مباشر لتحليل استجابة النموذج الرئيسية | |
| 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 import safe_json_parse # ✅ الإصلاح: استيراد المحلل الآمن | |
| NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY") | |
| PRIMARY_MODEL = "nvidia/llama-3.1-nemotron-ultra-253b-v1" | |
| class PatternAnalysisEngine: | |
| # --- (كود 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: # تخفيف الشرط من 20 إلى 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" | |
| # أخذ آخر 50 شمعة كحد أقصى لتجنب السياق الطويل جداً | |
| 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):") | |
| # عرض آخر 15 شمعة بالتفصيل | |
| 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 # Handle potential ZeroDivisionError | |
| # حساب ATR مبسط | |
| 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: | |
| # Provide default values in case of calculation error | |
| return { | |
| 'price_change': 0, | |
| 'avg_body': 0, | |
| 'atr': 0, | |
| 'trend': 'UNKNOWN', | |
| 'support': 0, | |
| 'resistance': 0 | |
| } | |
| # --- (analyze_chart_patterns و _parse_pattern_response لم تعد تُستخدم مباشرة للـ LLM prompt الأساسي، لكن قد تُستخدم في مكان آخر) --- | |
| async def analyze_chart_patterns(self, symbol, ohlcv_data): | |
| # ... (الكود الأصلي هنا - لم يتم تغييره) | |
| # هذا التحليل يتم الآن في الطبقة الثانية (MLProcessor) | |
| # ولا يتم إرسال نتيجته إلى الـ LLM في الطبقة الثالثة | |
| pass # Placeholder to indicate no change here | |
| def _parse_pattern_response(self, response_text): | |
| # ... (الكود الأصلي هنا - لم يتم تغييره) | |
| pass # Placeholder to indicate no change here | |
| 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) # Still needed for formatting candle data | |
| self.semaphore = asyncio.Semaphore(5) | |
| self.r2_service = None # سيتم تعيينه من app.py | |
| 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: | |
| print(f"⚠️ لا توجد بيانات شموع لـ {symbol} - تخطي التحليل") | |
| return None | |
| total_candles = sum(len(data) for data in ohlcv_data.values() if data) if ohlcv_data else 0 | |
| timeframes_count = len([tf for tf, data in ohlcv_data.items() if data and len(data) >= 10]) if ohlcv_data else 0 | |
| print(f" 📊 بيانات {symbol}: {total_candles} شمعة في {timeframes_count} إطار زمني") | |
| if total_candles < 30: | |
| print(f" ⚠️ بيانات شموع غير كافية لـ {symbol}: {total_candles} شمعة فقط") | |
| return None | |
| valid_timeframes = [tf for tf, candles in ohlcv_data.items() if candles and len(candles) >= 5] | |
| if not valid_timeframes: | |
| print(f" ⚠️ لا توجد أطر زمنية صالحة لـ {symbol}") | |
| return None | |
| print(f" ✅ أطر زمنية صالحة لـ {symbol}: {', '.join(valid_timeframes)}") | |
| news_text = await self.news_fetcher.get_news_for_symbol(symbol) | |
| # ❗ لا نستخدم _get_pattern_analysis هنا لأن التحليل المسبق لا يُرسل | |
| # pattern_analysis = await self._get_pattern_analysis(data_payload) # <-- تم إزالة هذا السطر | |
| whale_data = data_payload.get('whale_data', {}) | |
| # ❗ تعديل: تمرير None لـ pattern_analysis | |
| prompt = self._create_comprehensive_trading_prompt(data_payload, news_text, None, whale_data) | |
| if self.r2_service: | |
| analysis_data = { | |
| 'symbol': symbol, | |
| 'current_price': data_payload.get('current_price'), | |
| 'final_score': data_payload.get('final_score'), | |
| 'enhanced_final_score': data_payload.get('enhanced_final_score'), | |
| 'target_strategy': target_strategy, | |
| 'pattern_analysis': None, # ❗ تم التغيير: لا يوجد تحليل مسبق | |
| 'whale_data_available': whale_data.get('data_available', False), | |
| 'total_candles': total_candles, | |
| 'timeframes_count': timeframes_count, | |
| 'valid_timeframes': valid_timeframes, | |
| 'timestamp': datetime.now().isoformat() | |
| } | |
| await self.r2_service.save_llm_prompts_async( | |
| symbol, 'comprehensive_trading_decision_llm_pattern', 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: | |
| decision_dict['model_source'] = self.model_name | |
| # ❗ لا نضيف pattern_analysis هنا لأنه غير متوفر | |
| decision_dict['whale_data_integrated'] = whale_data.get('data_available', False) | |
| decision_dict['total_candles_analyzed'] = total_candles | |
| decision_dict['timeframes_analyzed'] = timeframes_count | |
| return decision_dict | |
| else: | |
| print(f"❌ فشل تحليل النموذج الضخم لـ {symbol} - لا توجد قرارات بديلة") | |
| return None | |
| except Exception as e: | |
| print(f"❌ خطأ في قرار التداول لـ {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"❌ فشل استخراج JSON من استجابة النموذج لـ {symbol}") | |
| return None | |
| decision_data = safe_json_parse(json_str) | |
| if not decision_data: | |
| print(f"❌ فشل تحليل JSON (safe_json_parse) لـ {symbol}: {response_text}") | |
| return None | |
| # ❗ تعديل: تحديث الحقول المطلوبة لتعكس التغييرات في الـ prompt | |
| required_fields = ['action', 'reasoning', 'risk_assessment', 'stop_loss', 'take_profit', 'expected_target_minutes', 'confidence_level', 'pattern_identified_by_llm'] | |
| if not validate_required_fields(decision_data, required_fields): | |
| print(f"❌ حقول مطلوبة مفقودة في استجابة النموذج لـ {symbol}") | |
| return None | |
| action = decision_data.get('action') | |
| if action not in ['BUY', 'HOLD']: | |
| print(f"⚠️ النموذج اقترح إجراء غير مدعوم ({action}) لـ {symbol}. سيتم اعتباره HOLD.") | |
| decision_data['action'] = 'HOLD' | |
| if decision_data['action'] == 'BUY': | |
| decision_data['trade_type'] = 'LONG' | |
| else: | |
| decision_data['trade_type'] = None | |
| 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"❌ خطأ في تحليل استجابة النموذج لـ {symbol}: {e}") | |
| return None | |
| # ❗ دالة _get_pattern_analysis لم تعد مستخدمة هنا، لكن قد تُستخدم في إعادة التحليل | |
| 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: | |
| # Note: This analysis result is used ONLY for re-analysis prompts now. | |
| return await self.pattern_engine.analyze_chart_patterns(symbol, ohlcv_data) | |
| return None | |
| except Exception as e: | |
| print(f"❌ فشل تحليل الأنماط (قد يكون لإعادة التحليل) لـ {data_payload.get('symbol')}: {e}") | |
| return None | |
| def _create_comprehensive_trading_prompt(self, payload: dict, news_text: str, pattern_analysis: dict, whale_data: dict) -> str: | |
| # ❗ pattern_analysis لم يعد مستخدماً هنا وسيتم تجاهله (أو يكون None) | |
| symbol = payload.get('symbol', 'N/A') | |
| current_price = payload.get('current_price', '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') | |
| final_score = payload.get('final_score', 'N/A') | |
| enhanced_final_score = payload.get('enhanced_final_score', 'N/A') | |
| ohlcv_data = payload.get('raw_ohlcv') or payload.get('ohlcv', {}) | |
| final_score_display = f"{final_score:.3f}" if isinstance(final_score, (int, float)) else str(final_score) | |
| 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) | |
| # ❗ تم حذف pattern_summary = self._format_pattern_analysis(pattern_analysis) | |
| whale_analysis_section = format_whale_analysis_for_llm(whale_data) | |
| candle_data_section = self._format_candle_data_comprehensive(ohlcv_data) # يعتمد على PatternAnalysisEngine للتنسيق | |
| market_context_section = self._format_market_context(sentiment_data) | |
| # ⚠️ ملاحظة: تأكد من أن market_context_section ليست فارغة | |
| prompt = f""" | |
| COMPREHENSIVE TRADING ANALYSIS FOR {symbol} | |
| 🚨 IMPORTANT SYSTEM CONSTRAINT: This is a SPOT TRADING system ONLY. Decisions MUST be limited to BUY (LONG) or HOLD. SHORT selling is NOT possible. | |
| 🎯 STRATEGY CONTEXT: | |
| - Target Strategy: {target_strategy} | |
| - Recommended Strategy: {recommended_strategy} | |
| - Current Price: ${current_price} | |
| - System Score: {final_score_display} | |
| - Enhanced Score: {enhanced_score_display} | |
| 📊 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."} | |
| 📰 LATEST NEWS: | |
| {news_text if news_text else "No significant news found"} | |
| 📋 REASONS FOR SYSTEM CANDIDACY (Layer 1 & 2 Screening): | |
| {chr(10).join([f"• {reason}" for reason in reasons]) if reasons else "No specific reasons provided"} | |
| 🎯 TRADING DECISION INSTRUCTIONS (SPOT ONLY - LLM MUST ANALYZE PATTERNS): | |
| 1. **PERFORM CHART PATTERN ANALYSIS:** Based *ONLY* on the provided 'RAW CANDLE DATA SUMMARY & STATISTICS' section above, identify relevant chart patterns (Triangles, Flags, Head & Shoulders, Double Tops/Bottoms, etc.) and candlestick patterns (Engulfing, Doji, Hammer, etc.) across the available timeframes. Determine the likely direction and strength implied by these patterns. | |
| 2. **INTEGRATE ALL DATA:** Combine YOUR pattern analysis with the technical indicators, strategy analysis, whale activity, market context, and news. | |
| 3. **ADHERE STRICTLY TO SPOT TRADING RULES:** Only decide 'BUY' (for a LONG position) if your comprehensive analysis indicates a high probability of upward movement. If the analysis suggests downward movement or high uncertainty, the ONLY valid action is 'HOLD'. DO NOT suggest 'SELL'. | |
| 4. **RISK ASSESSMENT:** Evaluate the risk-reward ratio specifically for a potential 'BUY' (LONG) trade based on support/resistance levels derived from the candle data summary. | |
| 5. **JUSTIFY YOUR DECISION:** Clearly explain your reasoning, detailing how YOUR pattern analysis and other data points led to the final 'BUY' or 'HOLD' decision. | |
| CRITICAL: You MUST explicitly state the pattern(s) you identified from the candle data summary and how they influenced your decision. Provide specific price levels (Stop Loss, Take Profit) suitable for a BUY/LONG trade if recommending 'BUY'. | |
| OUTPUT FORMAT (JSON - SPOT ONLY): | |
| {{ | |
| "action": "BUY/HOLD", | |
| "reasoning": "Detailed explanation integrating ALL data sources, starting with the patterns identified from the candle summary, and justifying the BUY or HOLD decision based on SPOT trading logic.", | |
| "pattern_identified_by_llm": "Name of the primary pattern(s) identified by analyzing the provided candle data summary (e.g., 'Bull Flag on 1H', 'Double Bottom on 4H', 'No Clear Pattern')", | |
| "pattern_influence": "Explain how the identified pattern(s) influenced the decision (e.g., 'Bull flag confirmed uptrend, supporting BUY', 'Descending triangle indicated weakness, leading to HOLD')", | |
| "risk_assessment": "low/medium/high", | |
| "stop_loss": 0.000000, # Required if action is BUY, 0 if HOLD | |
| "take_profit": 0.000000, # Required if action is BUY, 0 if HOLD | |
| "expected_target_minutes": 15, # Required if action is BUY, 0 if HOLD | |
| "confidence_level": 0.85, # Confidence in the BUY or HOLD decision | |
| "strategy": "{target_strategy}", # The strategy context provided | |
| "whale_influence": "How whale data influenced the BUY/HOLD decision (e.g., 'Lack of whale activity supported HOLD', 'Whale buying pressure reinforced BUY')", | |
| "key_support_level": 0.000000, # Derived from candle data analysis | |
| "key_resistance_level": 0.000000, # Derived from candle data analysis | |
| "risk_reward_ratio": 2.5 # Calculated ONLY for a potential BUY trade, 0 if HOLD | |
| }} | |
| """ | |
| return prompt | |
| # ❗ تم حذف دالة _format_pattern_analysis لأنها لم تعد مستخدمة هنا | |
| 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" | |
| # استخدام محرك الأنماط فقط لتنسيق البيانات الخام نصياً | |
| # The LLM will perform the actual pattern *analysis* based on this text. | |
| 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): | |
| """تحليل الشموع لإطار زمني محدد - (تستخدم داخلياً بواسطة _format_raw_candle_data)""" | |
| try: | |
| if len(candles) < 10: | |
| return f"Insufficient data ({len(candles)} candles)" | |
| recent_candles = candles[-15:] | |
| closes = [c[4] for c in recent_candles] | |
| opens = [c[1] for c in recent_candles] | |
| highs = [c[2] for c in recent_candles] | |
| lows = [c[3] for c in recent_candles] | |
| volumes = [c[5] for c in recent_candles] | |
| current_price = closes[-1] | |
| first_price = closes[0] | |
| price_change = ((current_price - first_price) / first_price) * 100 if first_price > 0 else 0 | |
| if price_change > 2: trend = "🟢 UPTREND" | |
| elif price_change < -2: trend = "🔴 DOWNTREND" | |
| else: trend = "⚪ SIDEWAYS" | |
| high_max = max(highs) | |
| low_min = min(lows) | |
| volatility = ((high_max - low_min) / low_min) * 100 if low_min > 0 else 0 | |
| avg_volume = sum(volumes) / len(volumes) if volumes else 1 | |
| current_volume = volumes[-1] if volumes else 0 | |
| volume_ratio = current_volume / avg_volume if avg_volume > 0 else 1 | |
| green_candles = sum(1 for i in range(len(closes)) if closes[i] > opens[i]) | |
| red_candles = len(closes) - green_candles | |
| candle_ratio = green_candles / len(closes) if closes else 0 | |
| analysis = [ | |
| f"📈 Trend: {trend} ({price_change:+.2f}%)", | |
| f"🌊 Volatility: {volatility:.2f}%", | |
| f"📦 Volume: {volume_ratio:.2f}x average", | |
| f"🕯️ Candles: {green_candles}🟢/{red_candles}🔴 ({candle_ratio:.1%} green)", | |
| f"💰 Range: {low_min:.6f} - {high_max:.6f}", | |
| f"🎯 Current: {current_price:.6f}" | |
| ] | |
| return "\n".join(analysis) | |
| except Exception as e: | |
| return f"Analysis error: {str(e)}" | |
| def _format_market_context(self, sentiment_data): | |
| """تنسيق سياق السوق""" | |
| # ⚠️ تأكد من أن هذه الدالة تتلقى بيانات صالحة من app.py/data_manager.py | |
| 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') # e.g., 'bull_market', 'bear_market', 'sideways_market' | |
| lines = [ | |
| # "🌍 GLOBAL MARKET CONTEXT:", # Changed title for clarity | |
| 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'}" | |
| ] | |
| # Including General Whale Activity Sentiment if available | |
| general_whale = sentiment_data.get('general_whale_activity', {}) | |
| if general_whale and general_whale.get('sentiment') != 'NEUTRAL': # Only show if not 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: | |
| print(f"⚠️ لا توجد بيانات شموع محدثة لـ {symbol} - تخطي إعادة التحليل") | |
| 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', {}) | |
| # ❗ تعديل: تمرير pattern_analysis لدالة إنشاء الـ prompt | |
| prompt = self._create_re_analysis_prompt(trade_data, processed_data, news_text, pattern_analysis, whale_data) | |
| if self.r2_service: | |
| analysis_data = { | |
| 'symbol': symbol, | |
| 'entry_price': trade_data.get('entry_price'), | |
| 'current_price': processed_data.get('current_price'), | |
| 'original_strategy': original_strategy, | |
| 'pattern_analysis': pattern_analysis, # نتيجة التحليل المسبق | |
| 'whale_data_available': whale_data.get('data_available', False) | |
| } | |
| await self.r2_service.save_llm_prompts_async( | |
| symbol, 'trade_reanalysis_with_pattern', prompt, analysis_data # تمييز النوع | |
| ) | |
| async with self.semaphore: | |
| response = await self._call_llm(prompt) | |
| re_analysis_dict = self._parse_re_analysis_response(response, original_strategy, symbol) | |
| if re_analysis_dict: | |
| re_analysis_dict['model_source'] = self.model_name | |
| re_analysis_dict['whale_data_integrated'] = whale_data.get('data_available', False) | |
| return re_analysis_dict | |
| else: | |
| print(f"❌ فشل إعادة تحليل النموذج الضخم لـ {symbol}") | |
| return None | |
| except Exception as e: | |
| print(f"❌ خطأ في إعادة تحليل LLM: {e}") | |
| traceback.print_exc() | |
| return None | |
| def _parse_re_analysis_response(self, response_text: str, fallback_strategy: str, symbol: str) -> dict: | |
| 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"❌ فشل تحليل JSON (safe_json_parse) لإعادة التحليل لـ {symbol}: {response_text}") | |
| return None | |
| action = decision_data.get('action') | |
| if action not in ['HOLD', 'CLOSE_TRADE', 'UPDATE_TRADE']: | |
| print(f"⚠️ النموذج اقترح إجراء إعادة تحليل غير مدعوم ({action}) لـ {symbol}. سيتم اعتباره HOLD.") | |
| decision_data['action'] = 'HOLD' | |
| 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 | |
| def _create_re_analysis_prompt(self, trade_data: dict, processed_data: dict, news_text: str, pattern_analysis: dict, whale_data: dict) -> str: | |
| # ❗ pattern_analysis مطلوب هنا | |
| 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') | |
| original_trade_type = "LONG" # SPOT only | |
| 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" | |
| indicators_summary = format_technical_indicators(processed_data.get('advanced_indicators', {})) | |
| # ❗ استخدام دالة التنسيق _format_pattern_analysis هنا | |
| 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) | |
| prompt = f""" | |
| TRADE RE-ANALYSIS FOR {symbol} (SPOT ONLY - Currently Open LONG Position) | |
| 🚨 IMPORTANT SYSTEM CONSTRAINT: This is a SPOT TRADING system ONLY. The open trade is LONG. Re-analysis should decide to HOLD, CLOSE, or UPDATE this LONG position. SHORT selling is NOT possible. | |
| 📊 CURRENT TRADE CONTEXT: | |
| - Strategy: {strategy} | |
| - Entry Price: {entry_price} (LONG position) | |
| - Current Price: {current_price} | |
| - Current Performance: {price_change_display} | |
| - Trade Age: {trade_data.get('hold_duration_minutes', 'N/A')} minutes | |
| 🔄 UPDATED TECHNICAL ANALYSIS: | |
| {indicators_summary} | |
| 📈 UPDATED RAW CANDLE DATA SUMMARY & STATISTICS: | |
| {candle_data_section} | |
| {chr(10)}--- END OF CANDLE DATA ---{chr(10)} | |
| 🔍 UPDATED PATTERN ANALYSIS RESULTS (From System): | |
| {pattern_summary} | |
| 🐋 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."} | |
| 📰 LATEST NEWS: | |
| {news_text if news_text else "No significant news found"} | |
| 🎯 RE-ANALYSIS INSTRUCTIONS (SPOT - LONG POSITION): | |
| 1. **ANALYZE UPDATED DATA:** Evaluate if the original LONG thesis still holds based on the updated raw candle data summary, technicals, patterns (provided above), whale activity, and market context. | |
| 2. **FOCUS ON RECENT PRICE ACTION:** Pay close attention to the latest candle formations and volume in the 'UPDATED RAW CANDLE DATA' section. | |
| 3. **VALIDATE PATTERNS:** Consider the 'UPDATED PATTERN ANALYSIS RESULTS' provided. Does the recent price action confirm or invalidate these patterns? | |
| 4. **ASSESS RISK:** Evaluate the current risk-reward ratio for HOLDING the LONG position using latest price action and updated support/resistance levels derived from the candle data. | |
| 5. **DECIDE ACTION (HOLD/CLOSE/UPDATE):** Based on the comprehensive analysis, decide whether to HOLD, CLOSE_TRADE (exit the LONG position), or UPDATE_TRADE (adjust SL/TP for the LONG position). DO NOT suggest SHORTING. | |
| 6. **PROVIDE DETAILS:** If updating, provide specific new SL/TP levels. Justify your decision clearly, integrating all data points. | |
| CRITICAL: The decision must be one of HOLD, CLOSE_TRADE, or UPDATE_TRADE for the existing LONG position. Explain how recent candle data and provided patterns influenced your decision. | |
| OUTPUT FORMAT (JSON - SPOT RE-ANALYSIS): | |
| {{ | |
| "action": "HOLD/CLOSE_TRADE/UPDATE_TRADE", | |
| "reasoning": "Comprehensive justification for HOLD, CLOSE, or UPDATE of the LONG position, based on updated analysis with emphasis on recent candle patterns and the provided pattern analysis results.", | |
| "new_stop_loss": 0.000000, # If action is UPDATE_TRADE, else 0 | |
| "new_take_profit": 0.000000, # If action is UPDATE_TRADE, else 0 | |
| "new_expected_minutes": 15, # If action is UPDATE_TRADE or HOLD (new expectation), else 0 | |
| "confidence_level": 0.85, # Confidence in the re-analysis decision | |
| "strategy": "{strategy}", # Original strategy context | |
| "whale_influence_reanalysis": "How updated whale data influenced the decision for the LONG trade", | |
| "pattern_influence_reanalysis": "How updated raw candle patterns AND the provided pattern analysis results influenced the decision", | |
| "risk_adjustment": "low/medium/high" # Current risk level if HOLDING | |
| }} | |
| """ | |
| return prompt | |
| async def _call_llm(self, prompt: str) -> str: | |
| try: | |
| # Simple retry mechanism within the call for non-rate limit errors | |
| for attempt in range(2): # Try twice | |
| try: | |
| response = self.client.chat.completions.create( | |
| model=self.model_name, | |
| messages=[{"role": "user", "content": prompt}], | |
| temperature=self.temperature, | |
| seed=int(time.time()), # Use time for seed | |
| max_tokens=4000 # Increased max_tokens slightly | |
| ) | |
| # Basic validation of response content | |
| content = response.choices[0].message.content | |
| if content and '{' in content and '}' in content: | |
| return content | |
| else: | |
| print(f"⚠️ LLM returned invalid content (attempt {attempt+1}): {content[:100]}...") | |
| if attempt == 0: await asyncio.sleep(1) # Wait before retry | |
| except (RateLimitError, APITimeoutError) as e: | |
| print(f"❌ LLM API Error (Rate Limit/Timeout): {e}. Retrying via backoff...") | |
| raise # Let backoff handle retries for these specific errors | |
| except Exception as e: | |
| print(f"❌ Unexpected LLM API error (attempt {attempt+1}): {e}") | |
| if attempt == 0: await asyncio.sleep(2) # Wait longer for unexpected errors | |
| elif attempt == 1: raise # Raise error after second failed attempt | |
| print("❌ LLM failed to return valid content after retries.") | |
| return "" # Return empty string if all attempts fail | |
| except Exception as e: | |
| print(f"❌ Final failure in _call_llm after backoff retries: {e}") | |
| raise # Re-raise the exception after backoff fails | |
| print("✅ LLM Service loaded - Updated Prompts for SPOT ONLY & LLM Pattern Analysis") |