Riy777 commited on
Commit
cf3f424
·
1 Parent(s): ea523b5

Update LLM.py

Browse files
Files changed (1) hide show
  1. LLM.py +161 -477
LLM.py CHANGED
@@ -1,176 +1,72 @@
1
- # LLM.py (Updated to integrate LearningHub and English-only prompts)
2
  import os, traceback, asyncio, json, time
3
- import re # ✅ استيراد مكتبة re
4
  from datetime import datetime
5
  from functools import wraps
6
  from backoff import on_exception, expo
7
  from openai import OpenAI, RateLimitError, APITimeoutError
8
  import numpy as np
9
  from sentiment_news import NewsFetcher
10
- # ✅ تعديل الاستيراد: parse_json_from_response لم يعد مستخدماً هنا بشكل مباشر لتحليل استجابة النموذج الرئيسية
11
  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
12
- from ml_engine.processor import safe_json_parse # ✅ الإصلاح: استيراد المحلل الآمن من الموجه الجديد
13
 
14
  NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY")
15
  PRIMARY_MODEL = "nvidia/llama-3.1-nemotron-ultra-253b-v1"
16
 
 
17
  class PatternAnalysisEngine:
18
  # --- (هذا الكلاس جزء من LLM.py ومطلوب لتحليل الشموع) ---
19
  def __init__(self, llm_service):
20
  self.llm = llm_service
21
-
22
  def _format_chart_data_for_llm(self, ohlcv_data):
23
- """تنسيق شامل لبيانات الشموع الخام لتحليل الأنماط"""
24
- if not ohlcv_data:
25
- return "Insufficient chart data for pattern analysis"
26
-
27
  try:
28
- # استخدام جميع الأطر الزمنية المتاحة مع البيانات الخام
29
  all_timeframes = []
30
  for timeframe, candles in ohlcv_data.items():
31
- if candles and len(candles) >= 10: # تخفيف الشرط من 20 إلى 10 شموع
32
- # تمرير البيانات الخام مباشرة للنموذج
33
  raw_candle_summary = self._format_raw_candle_data(candles, timeframe)
34
  all_timeframes.append(f"=== {timeframe.upper()} TIMEFRAME ({len(candles)} CANDLES) ===\n{raw_candle_summary}")
35
-
36
  return "\n\n".join(all_timeframes) if all_timeframes else "No sufficient timeframe data available"
37
- except Exception as e:
38
- return f"Error formatting chart data: {str(e)}"
39
-
40
  def _format_raw_candle_data(self, candles, timeframe):
41
- """تنسيق بيانات الشموع الخام بشكل مفصل للنموذج"""
42
  try:
43
- if len(candles) < 10:
44
- return f"Only {len(candles)} candles available - insufficient for deep pattern analysis"
45
-
46
- # أخذ آخر 50 شمعة كحد أقصى لتجنب السياق الطويل جداً
47
  analysis_candles = candles[-50:] if len(candles) > 50 else candles
48
-
49
- summary = []
50
- summary.append(f"Total candles: {len(candles)} (showing last {len(analysis_candles)})")
51
- summary.append("Recent candles (newest to oldest):")
52
-
53
- # عرض آخر 15 شمعة بالتفصيل
54
  for i in range(min(15, len(analysis_candles))):
55
- idx = len(analysis_candles) - 1 - i
56
- candle = analysis_candles[idx]
57
-
58
- # تحويل الطابع الزمني
59
- try:
60
- timestamp = datetime.fromtimestamp(candle[0] / 1000).strftime('%Y-%m-%d %H:%M:%S')
61
- except:
62
- timestamp = "unknown"
63
-
64
  open_price, high, low, close, volume = candle[1], candle[2], candle[3], candle[4], candle[5]
65
-
66
  candle_type = "🟢 BULLISH" if close > open_price else "🔴 BEARISH" if close < open_price else "⚪ NEUTRAL"
67
- body_size = abs(close - open_price)
68
- body_percent = (body_size / open_price * 100) if open_price > 0 else 0
69
-
70
- wick_upper = high - max(open_price, close)
71
- wick_lower = min(open_price, close) - low
72
- total_range = high - low
73
-
74
- if total_range > 0:
75
- body_ratio = (body_size / total_range) * 100
76
- upper_wick_ratio = (wick_upper / total_range) * 100
77
- lower_wick_ratio = (wick_lower / total_range) * 100
78
- else:
79
- body_ratio = upper_wick_ratio = lower_wick_ratio = 0
80
-
81
- summary.append(f"{i+1:2d}. {timestamp} | {candle_type}")
82
- summary.append(f" O:{open_price:.8f} H:{high:.8f} L:{low:.8f} C:{close:.8f}")
83
- summary.append(f" Body: {body_percent:.2f}% | Body/Range: {body_ratio:.1f}%")
84
- summary.append(f" Wicks: Upper {upper_wick_ratio:.1f}% / Lower {lower_wick_ratio:.1f}%")
85
- summary.append(f" Volume: {volume:,.0f}")
86
-
87
- # إضافة تحليل إحصائي
88
  if len(analysis_candles) >= 20:
89
  stats = self._calculate_candle_statistics(analysis_candles)
90
- summary.append(f"\n📊 STATISTICAL ANALYSIS:")
91
- summary.append(f"• Price Change: {stats['price_change']:+.2f}%")
92
- summary.append(f"• Average Body Size: {stats['avg_body']:.4f}%")
93
- summary.append(f"• Volatility (ATR): {stats['atr']:.6f}")
94
- summary.append(f"• Trend: {stats['trend']}")
95
- summary.append(f"• Support: {stats['support']:.6f}")
96
- summary.append(f"• Resistance: {stats['resistance']:.6f}")
97
-
98
  return "\n".join(summary)
99
-
100
- except Exception as e:
101
- return f"Error formatting raw candle data: {str(e)}"
102
-
103
  def _calculate_candle_statistics(self, candles):
104
- """حساب الإحصائيات الأساسية للشموع"""
105
  try:
106
- closes = [c[4] for c in candles]
107
- opens = [c[1] for c in candles]
108
- highs = [c[2] for c in candles]
109
- lows = [c[3] for c in candles]
110
-
111
- # حساب التغير في السعر
112
- first_close = closes[0]
113
- last_close = closes[-1]
114
- price_change = ((last_close - first_close) / first_close) * 100
115
-
116
- # حساب متوسط حجم الجسم
117
- body_sizes = [abs(close - open) for open, close in zip(opens, closes)]
118
- avg_body = (sum(body_sizes) / len(body_sizes)) / first_close * 100 if first_close > 0 else 0 # Handle potential ZeroDivisionError
119
-
120
- # حساب ATR مبسط
121
- true_ranges = []
122
- for i in range(1, len(candles)):
123
- high, low, prev_close = highs[i], lows[i], closes[i-1]
124
- tr1 = high - low
125
- tr2 = abs(high - prev_close)
126
- tr3 = abs(low - prev_close)
127
- true_ranges.append(max(tr1, tr2, tr3))
128
-
129
  atr = sum(true_ranges) / len(true_ranges) if true_ranges else 0
130
-
131
- # تحديد الاتجاه
132
- if price_change > 3:
133
- trend = "STRONG UPTREND"
134
- elif price_change > 1:
135
- trend = "UPTREND"
136
- elif price_change < -3:
137
- trend = "STRONG DOWNTREND"
138
- elif price_change < -1:
139
- trend = "DOWNTREND"
140
- else:
141
- trend = "SIDEWAYS"
142
-
143
- # مستويات الدعم والمقاومة المبسطة
144
- support = min(lows)
145
- resistance = max(highs)
146
-
147
- return {
148
- 'price_change': price_change,
149
- 'avg_body': avg_body,
150
- 'atr': atr,
151
- 'trend': trend,
152
- 'support': support,
153
- 'resistance': resistance
154
- }
155
-
156
- except Exception as e:
157
- # Provide default values in case of calculation error
158
- return {
159
- 'price_change': 0,
160
- 'avg_body': 0,
161
- 'atr': 0,
162
- 'trend': 'UNKNOWN',
163
- 'support': 0,
164
- 'resistance': 0
165
- }
166
-
167
- async def analyze_chart_patterns(self, symbol, ohlcv_data):
168
- """(تم تركها فارغة عمداً لأن النموذج الضخم يقوم بها الآن)"""
169
- pass
170
-
171
- def _parse_pattern_response(self, response_text):
172
- """(تم تركها فارغة عمداً)"""
173
- pass
174
 
175
 
176
  class LLMService:
@@ -182,12 +78,8 @@ class LLMService:
182
  self.news_fetcher = NewsFetcher()
183
  self.pattern_engine = PatternAnalysisEngine(self)
184
  self.semaphore = asyncio.Semaphore(5)
185
- self.r2_service = None # (Set from app.py)
186
-
187
- # 🔴 --- START OF CHANGE --- 🔴
188
- # Renamed from self.learning_engine to self.learning_hub
189
- self.learning_hub = None # (Set from app.py, expects LearningHubManager)
190
- # 🔴 --- END OF CHANGE --- 🔴
191
 
192
  def _rate_limit_nvidia_api(func):
193
  @wraps(func)
@@ -196,93 +88,71 @@ class LLMService:
196
  return await func(*args, **kwargs)
197
  return wrapper
198
 
 
199
  async def get_trading_decision(self, data_payload: dict):
200
  try:
201
  symbol = data_payload.get('symbol', 'unknown')
202
  target_strategy = data_payload.get('target_strategy', 'GENERIC')
203
 
204
  ohlcv_data = data_payload.get('raw_ohlcv') or data_payload.get('ohlcv')
205
- if not ohlcv_data:
206
- print(f"⚠️ No candle data for {symbol} - skipping analysis")
207
- return None
208
-
209
  total_candles = sum(len(data) for data in ohlcv_data.values() if data) if ohlcv_data else 0
210
  timeframes_count = len([tf for tf, data in ohlcv_data.items() if data and len(data) >= 10]) if ohlcv_data else 0
211
-
212
- if total_candles < 30:
213
- print(f" ⚠️ Insufficient candle data for {symbol}: {total_candles} candles")
214
- return None
215
-
216
  valid_timeframes = [tf for tf, candles in ohlcv_data.items() if candles and len(candles) >= 5]
217
- if not valid_timeframes:
218
- print(f" ⚠️ No valid timeframes for {symbol}")
219
- return None
220
 
221
  news_text = await self.news_fetcher.get_news_for_symbol(symbol)
222
  whale_data = data_payload.get('whale_data', {})
223
 
224
- # 🔴 --- START OF CHANGE --- 🔴
225
- # (Fetch learning context from the new hub)
226
  statistical_feedback = "No statistical learning data yet."
227
  active_context_playbook = "No active learning rules available."
228
 
229
  if self.learning_hub and self.learning_hub.initialized:
230
- # 1. Get Statistical Feedback (Slow-learner)
231
  statistical_feedback = await self.learning_hub.get_statistical_feedback_for_llm(target_strategy)
232
-
233
- # 2. Get Active Context / Deltas (Fast-learner)
234
  active_context_playbook = await self.learning_hub.get_active_context_for_llm(
235
- domain="strategy",
236
- query=f"{target_strategy} {symbol}" # (Query with strategy and symbol)
237
  )
238
 
239
- # (Pass new context to the prompt creator)
240
- prompt = self._create_comprehensive_trading_prompt(
241
- data_payload,
242
- news_text,
243
- None,
244
- whale_data,
245
- statistical_feedback,
246
- active_context_playbook
247
  )
248
  # 🔴 --- END OF CHANGE --- 🔴
249
 
250
  if self.r2_service:
251
- analysis_data = {
252
- 'symbol': symbol,
253
- 'current_price': data_payload.get('current_price'),
254
- 'enhanced_final_score': data_payload.get('enhanced_final_score'),
255
- 'target_strategy': target_strategy,
256
- 'statistical_feedback': statistical_feedback,
257
- 'active_context_playbook': active_context_playbook,
258
- 'whale_data_available': whale_data.get('data_available', False),
259
- 'total_candles': total_candles,
260
- 'timeframes_count': timeframes_count,
261
- 'timestamp': datetime.now().isoformat()
262
- }
263
  await self.r2_service.save_llm_prompts_async(
264
- symbol, 'comprehensive_trading_decision_v3_hub', prompt, analysis_data
265
  )
266
 
267
  async with self.semaphore:
268
  response = await self._call_llm(prompt)
269
 
270
  decision_dict = self._parse_llm_response_enhanced(response, target_strategy, symbol)
 
 
 
271
  if decision_dict:
 
 
 
 
272
  decision_dict['model_source'] = self.model_name
273
  decision_dict['whale_data_integrated'] = whale_data.get('data_available', False)
274
- decision_dict['total_candles_analyzed'] = total_candles
275
- decision_dict['timeframes_analyzed'] = timeframes_count
276
  return decision_dict
 
277
  else:
278
  print(f"❌ LLM parsing failed for {symbol} - no fallback decisions")
279
  return None
280
 
281
  except Exception as e:
282
- print(f"❌ Error in get_trading_decision for {data_payload.get('symbol', 'unknown')}: {e}")
283
- traceback.print_exc()
284
  return None
285
 
 
286
  def _parse_llm_response_enhanced(self, response_text: str, fallback_strategy: str, symbol: str) -> dict:
287
  try:
288
  json_str = parse_json_from_response(response_text)
@@ -295,32 +165,36 @@ class LLMService:
295
  print(f"❌ Failed to parse JSON (safe_json_parse) for {symbol}: {response_text}")
296
  return None
297
 
298
- # (This check is for the trading decision, not the reflector response)
299
  if fallback_strategy == "reflection" or fallback_strategy == "distillation":
300
- # (If this is a reflector/curator call, just return the data)
301
  return decision_data
302
 
303
- required_fields = [
304
- 'action', 'reasoning', 'risk_assessment', 'stop_loss', 'take_profit',
305
- 'expected_target_minutes', 'confidence_level', 'pattern_identified_by_llm',
306
- 'exit_profile', 'exit_parameters'
307
- ]
 
 
 
 
 
 
 
308
  if not validate_required_fields(decision_data, required_fields):
309
  print(f"❌ Missing required fields in LLM response for {symbol}")
310
  missing = [f for f in required_fields if f not in decision_data]
311
  print(f" MIA: {missing}")
312
  return None
313
 
314
- if not isinstance(decision_data['exit_parameters'], dict):
315
- print(f"❌ 'exit_parameters' is not a valid dict for {symbol}")
316
- return None
317
-
318
  action = decision_data.get('action')
319
- if action not in ['BUY', 'HOLD']:
320
  print(f"⚠️ LLM suggested unsupported action ({action}) for {symbol}. Forcing HOLD.")
321
  decision_data['action'] = 'HOLD'
 
322
 
323
- if decision_data['action'] == 'BUY':
324
  decision_data['trade_type'] = 'LONG'
325
  else:
326
  decision_data['trade_type'] = None
@@ -334,27 +208,26 @@ class LLMService:
334
  print(f"❌ Error parsing LLM response for {symbol}: {e}")
335
  return None
336
 
 
337
  async def _get_pattern_analysis(self, data_payload):
338
  try:
339
  symbol = data_payload['symbol']
340
  ohlcv_data = data_payload.get('raw_ohlcv') or data_payload.get('ohlcv')
341
- if ohlcv_data:
342
- # (This is a placeholder, as PatternAnalysisEngine.analyze_chart_patterns is not implemented)
343
- return None
344
  return None
345
  except Exception as e:
346
  print(f"❌ Pattern analysis failed for {data_payload.get('symbol')}: {e}")
347
  return None
348
 
349
  # 🔴 --- START OF PROMPT CHANGE --- 🔴
350
- def _create_comprehensive_trading_prompt(
351
  self,
352
  payload: dict,
353
  news_text: str,
354
- pattern_analysis: dict, # (This is the old system, now deprecated, but we leave the arg)
355
  whale_data: dict,
356
- statistical_feedback: str, # (NEW from Hub)
357
- active_context_playbook: str # (NEW from Hub)
358
  ) -> str:
359
 
360
  symbol = payload.get('symbol', 'N/A')
@@ -366,7 +239,6 @@ class LLMService:
366
  recommended_strategy = payload.get('recommended_strategy', 'N/A')
367
  target_strategy = payload.get('target_strategy', 'GENERIC')
368
  enhanced_final_score = payload.get('enhanced_final_score', 0)
369
-
370
  enhanced_score_display = f"{enhanced_final_score:.3f}" if isinstance(enhanced_final_score, (int, float)) else str(enhanced_final_score)
371
 
372
  indicators_summary = format_technical_indicators(advanced_indicators)
@@ -376,19 +248,18 @@ class LLMService:
376
  candle_data_section = self._format_candle_data_comprehensive(ohlcv_data)
377
  market_context_section = self._format_market_context(sentiment_data)
378
 
379
- # (New sections from the Learning Hub)
380
  statistical_feedback_section = f"🧠 STATISTICAL FEEDBACK (Slow-Learner):\n{statistical_feedback}"
381
  playbook_section = f"📚 LEARNING PLAYBOOK (Fast-Learner Active Rules):\n{active_context_playbook}"
382
 
383
 
384
  prompt = f"""
385
- COMPREHENSIVE TRADING ANALYSIS FOR {symbol}
386
 
387
- 🚨 IMPORTANT SYSTEM CONSTRAINT: This is a SPOT TRADING system ONLY. Decisions MUST be limited to BUY (LONG) or HOLD. SHORT selling is NOT possible.
388
 
389
  🎯 STRATEGY CONTEXT:
390
  * Target Strategy: {target_strategy}
391
- * Recommended Strategy: {recommended_strategy}
392
  * Current Price: ${current_price}
393
  * Enhanced System Score: {enhanced_score_display}
394
 
@@ -413,216 +284,106 @@ COMPREHENSIVE TRADING ANALYSIS FOR {symbol}
413
  🌍 MARKET CONTEXT:
414
  {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."}
415
 
416
- 📰 LATEST NEWS:
417
- {news_text if news_text else "No significant news found"}
418
 
419
- 📋 REASONS FOR SYSTEM CANDIDACY (Layer 1 & 2 Screening):
420
- {chr(10).join([f"• {reason}" for reason in reasons]) if reasons else "No specific reasons provided"}
 
 
 
 
 
421
 
422
- ---
423
- 🎯 TRADING DECISION INSTRUCTIONS (SPOT ONLY - LLM MUST ANALYZE PATTERNS AND DEFINE EXIT STRATEGY):
424
-
425
- 1. **PERFORM CHART PATTERN ANALYSIS:** Based *ONLY* on the provided 'RAW CANDLE DATA SUMMARY & STATISTICS' section, identify relevant chart patterns (Triangles, Flags, etc.) and candlestick patterns (Engulfing, Doji, etc.).
426
- 2. **INTEGRATE ALL DATA:** Combine YOUR pattern analysis with technicals, strategy analysis, whale activity, market context, news, and (most importantly) the 'LEARNING HUB INPUT'.
427
- 3. **ADHERE STRICTLY TO SPOT TRADING RULES:** Only decide 'BUY' (LONG) or 'HOLD'. DO NOT suggest 'SELL'.
428
- 4. **DEFINE EXIT STRATEGY (CRITICAL):** If (and only if) action is 'BUY', you MUST define the dynamic exit strategy (Exit Profile) and its parameters. This profile will be executed by a separate tactical bot.
429
- * `"exit_profile"`: Choose one: "ATR_TRAILING" (Recommended for trends/breakouts), "FIXED_TARGET" (Recommended for mean reversion/scalping), "TIME_BASED" (Exit after X minutes regardless).
430
- * `"exit_parameters"`: Define parameters for the chosen profile, respecting the 'Statistical Feedback'.
431
- * For "ATR_TRAILING": {{"atr_multiplier": 2.0, "atr_period": 14, "break_even_trigger_percent": 1.5}} (break_even_trigger_percent moves stop to entry when profit hits 1.5%)
432
- * For "FIXED_TARGET": {{"time_stop_minutes": 120}} (Hard stop if target not hit in 120 mins)
433
- * For "TIME_BASED": {{"exit_after_minutes": 60}}
434
- 5. **DEFINE HARD STOPS:** You must still provide the initial "hard" stop_loss (catastrophic failure stop) and the final "take_profit" target. The dynamic exit profile operates *within* these boundaries.
435
- 6. **SELF-CRITIQUE (Point 4 of Plan):** After defining the JSON, perform a self-critique. List potential failure modes for your decision and confirm your final answer.
436
-
437
- OUTPUT FORMAT (JSON - SPOT ONLY - INCLUDE EXIT PROFILE AND SELF-CRITIQUE):
438
  {{
439
- "action": "BUY/HOLD",
440
- "reasoning": "Detailed explanation integrating ALL data sources, starting with the patterns identified from the candle summary, and justifying the BUY or HOLD decision. Explain *why* the chosen exit_profile is appropriate, considering the Learning Hub feedback.",
441
- "pattern_identified_by_llm": "Name of the primary pattern(s) identified (e.g., 'Bull Flag on 1H', 'No Clear Pattern')",
442
- "pattern_influence": "Explain how the identified pattern(s) influenced the decision.",
443
- "risk_assessment": "low/medium/high",
444
-
445
- "stop_loss": 0.000000,
446
- "take_profit": 0.000000,
447
-
448
- "exit_profile": "FIXED_TARGET",
449
- "exit_parameters": {{ "time_stop_minutes": 120 }},
450
-
451
- "expected_target_minutes": 15,
452
- "confidence_level": 0.85,
453
- "strategy": "{target_strategy}",
454
- "whale_influence": "How whale data influenced the BUY/HOLD decision",
455
- "key_support_level": 0.000000,
456
- "key_resistance_level": 0.000000,
457
- "risk_reward_ratio": 2.5,
458
-
459
- "self_critique": {{
460
- "failure_modes": [
461
- "What is the first reason this decision could fail? (e.g., 'The identified pattern is a false breakout.')",
462
- "What is the second reason? (e.g., 'Whale data shows distribution, contradicting the technicals.')"
463
- ],
464
- "confidence_adjustment_reason": "Brief reason if confidence was adjusted post-critique."
465
- }}
466
  }}
467
  """
468
  return prompt
469
  # 🔴 --- END OF PROMPT CHANGE --- 🔴
470
 
471
-
472
  def _format_candle_data_comprehensive(self, ohlcv_data):
473
- """تنسيق شامل لبيانات الشموع الخام"""
474
- if not ohlcv_data:
475
- return "No raw candle data available for analysis"
476
-
477
  try:
478
- timeframes_available = []
479
- total_candles = 0
480
-
481
  for timeframe, candles in ohlcv_data.items():
482
- if candles and len(candles) >= 5:
483
- timeframes_available.append(f"{timeframe.upper()} ({len(candles)} candles)")
484
- total_candles += len(candles)
485
-
486
- if not timeframes_available:
487
- return "Insufficient candle data across all timeframes"
488
-
489
  summary = f"📊 Available Timeframes: {', '.join(timeframes_available)}\n"
490
  summary += f"📈 Total Candles Available: {total_candles}\n\n"
491
-
492
  raw_candle_analysis_text = self.pattern_engine._format_chart_data_for_llm(ohlcv_data)
493
-
494
  summary += raw_candle_analysis_text
495
-
496
  return summary
497
- except Exception as e:
498
- return f"Error formatting raw candle data: {str(e)}"
499
-
500
  def _analyze_timeframe_candles(self, candles, timeframe):
501
- """تحليل الشموع لإطار زمني محدد - (تستخدم داخلياً بواسطة _format_raw_candle_data)"""
502
  try:
503
- if len(candles) < 10:
504
- return f"Insufficient data ({len(candles)} candles)"
505
-
506
- recent_candles = candles[-15:]
507
-
508
- closes = [c[4] for c in recent_candles]
509
- opens = [c[1] for c in recent_candles]
510
- highs = [c[2] for c in recent_candles]
511
- lows = [c[3] for c in recent_candles]
512
- volumes = [c[5] for c in recent_candles]
513
-
514
- current_price = closes[-1]
515
- first_price = closes[0]
516
- price_change = ((current_price - first_price) / first_price) * 100 if first_price > 0 else 0
517
-
518
  if price_change > 2: trend = "🟢 UPTREND"
519
  elif price_change < -2: trend = "🔴 DOWNTREND"
520
  else: trend = "⚪ SIDEWAYS"
521
-
522
- high_max = max(highs)
523
- low_min = min(lows)
524
- volatility = ((high_max - low_min) / low_min) * 100 if low_min > 0 else 0
525
-
526
  avg_volume = sum(volumes) / len(volumes) if volumes else 1
527
- current_volume = volumes[-1] if volumes else 0
528
- volume_ratio = current_volume / avg_volume if avg_volume > 0 else 1
529
-
530
- green_candles = sum(1 for i in range(len(closes)) if closes[i] > opens[i])
531
- red_candles = len(closes) - green_candles
532
- candle_ratio = green_candles / len(closes) if closes else 0
533
-
534
- analysis = [
535
- f"📈 Trend: {trend} ({price_change:+.2f}%)",
536
- f"🌊 Volatility: {volatility:.2f}%",
537
- f"📦 Volume: {volume_ratio:.2f}x average",
538
- f"🕯️ Candles: {green_candles}🟢/{red_candles}🔴 ({candle_ratio:.1%} green)",
539
- f"💰 Range: {low_min:.6f} - {high_max:.6f}",
540
- f"🎯 Current: {current_price:.6f}"
541
- ]
542
-
543
  return "\n".join(analysis)
544
- except Exception as e:
545
- return f"Analysis error: {str(e)}"
546
-
547
  def _format_market_context(self, sentiment_data):
548
- """تنسيق سياق السوق"""
549
- if not sentiment_data or sentiment_data.get('data_quality', 'LOW') == 'LOW':
550
- return "Market context data not available or incomplete."
551
-
552
- btc_sentiment = sentiment_data.get('btc_sentiment', 'N/A')
553
- fear_greed = sentiment_data.get('fear_and_greed_index', 'N/A')
554
- market_trend = sentiment_data.get('market_trend', 'N/A')
555
-
556
- lines = [
557
- f"• Bitcoin Sentiment: {btc_sentiment}",
558
- f"• Fear & Greed Index: {fear_greed} ({sentiment_data.get('sentiment_class', 'Neutral')})",
559
- f"• Overall Market Trend: {market_trend.replace('_', ' ').title() if isinstance(market_trend, str) else 'N/A'}"
560
- ]
561
-
562
- general_whale = sentiment_data.get('general_whale_activity', {})
563
  if general_whale and general_whale.get('sentiment') != 'NEUTRAL':
564
- whale_sentiment = general_whale.get('sentiment', 'N/A')
565
- critical_alert = general_whale.get('critical_alert', False)
566
- lines.append(f" General Whale Sentiment: {whale_sentiment.replace('_', ' ').title() if isinstance(whale_sentiment, str) else 'N/A'}")
567
- if critical_alert:
568
- lines.append(" ⚠️ CRITICAL WHALE ALERT ACTIVE")
569
-
570
  return "\n".join(lines)
571
 
572
 
 
573
  async def re_analyze_trade_async(self, trade_data: dict, processed_data: dict):
574
  try:
575
- symbol = trade_data['symbol']
576
- original_strategy = trade_data.get('strategy', 'GENERIC')
577
-
578
  ohlcv_data = processed_data.get('raw_ohlcv') or processed_data.get('ohlcv')
579
- if not ohlcv_data:
580
- print(f"⚠️ No updated candle data for {symbol} - skipping re-analysis")
581
- return None
582
-
583
  news_text = await self.news_fetcher.get_news_for_symbol(symbol)
584
  pattern_analysis = await self._get_pattern_analysis(processed_data)
585
  whale_data = processed_data.get('whale_data', {})
586
 
587
- # 🔴 --- START OF CHANGE --- 🔴
588
- # (Fetch learning context from the new hub for re-analysis)
589
  statistical_feedback = "No statistical learning data yet."
590
  active_context_playbook = "No active learning rules available."
591
-
592
  if self.learning_hub and self.learning_hub.initialized:
593
- # 1. Get Statistical Feedback (Slow-learner)
594
  statistical_feedback = await self.learning_hub.get_statistical_feedback_for_llm(original_strategy)
595
-
596
- # 2. Get Active Context / Deltas (Fast-learner)
597
  active_context_playbook = await self.learning_hub.get_active_context_for_llm(
598
- domain="strategy",
599
- query=f"{original_strategy} {symbol} re-analysis"
600
  )
601
 
602
- # (Pass new context to the prompt creator)
603
  prompt = self._create_re_analysis_prompt(
604
- trade_data,
605
- processed_data,
606
- news_text,
607
- pattern_analysis,
608
- whale_data,
609
- statistical_feedback,
610
- active_context_playbook
611
  )
612
- # 🔴 --- END OF CHANGE --- 🔴
613
 
614
  if self.r2_service:
615
- analysis_data = {
616
- 'symbol': symbol,
617
- 'entry_price': trade_data.get('entry_price'),
618
- 'current_price': processed_data.get('current_price'),
619
- 'original_strategy': original_strategy,
620
- 'statistical_feedback': statistical_feedback,
621
- 'active_context_playbook': active_context_playbook,
622
- 'whale_data_available': whale_data.get('data_available', False)
623
- }
624
  await self.r2_service.save_llm_prompts_async(
625
- symbol, 'trade_reanalysis_v3_hub', prompt, analysis_data
626
  )
627
 
628
  async with self.semaphore:
@@ -631,104 +392,64 @@ OUTPUT FORMAT (JSON - SPOT ONLY - INCLUDE EXIT PROFILE AND SELF-CRITIQUE):
631
  re_analysis_dict = self._parse_re_analysis_response(response, original_strategy, symbol)
632
  if re_analysis_dict:
633
  re_analysis_dict['model_source'] = self.model_name
634
- re_analysis_dict['whale_data_integrated'] = whale_data.get('data_available', False)
635
  return re_analysis_dict
636
  else:
637
  print(f"❌ LLM re-analysis parsing failed for {symbol}")
638
  return None
639
-
640
  except Exception as e:
641
- print(f"❌ Error in LLM re-analysis: {e}")
642
- traceback.print_exc()
643
  return None
644
 
 
645
  def _parse_re_analysis_response(self, response_text: str, fallback_strategy: str, symbol: str) -> dict:
646
  try:
647
  json_str = parse_json_from_response(response_text)
648
- if not json_str:
649
- return None
650
-
651
  decision_data = safe_json_parse(json_str)
652
- if not decision_data:
653
- print(f"❌ Failed to parse JSON (safe_json_parse) for re-analysis of {symbol}: {response_text}")
654
- return None
655
 
656
  action = decision_data.get('action')
657
  if action not in ['HOLD', 'CLOSE_TRADE', 'UPDATE_TRADE']:
658
  print(f"⚠️ LLM suggested unsupported re-analysis action ({action}) for {symbol}. Forcing HOLD.")
659
  decision_data['action'] = 'HOLD'
660
-
661
  if action == 'UPDATE_TRADE':
662
  required_update_fields = ['new_stop_loss', 'new_take_profit', 'new_exit_profile', 'new_exit_parameters']
663
  if not validate_required_fields(decision_data, required_update_fields):
664
- print(f"❌ Missing required fields for UPDATE_TRADE for {symbol}")
665
- missing = [f for f in required_update_fields if f not in decision_data]
666
- print(f" MIA: {missing}")
667
- decision_data['action'] = 'HOLD'
668
  elif not isinstance(decision_data['new_exit_parameters'], dict):
669
- print(f"❌ 'new_exit_parameters' is not a valid dict for {symbol}")
670
- decision_data['action'] = 'HOLD'
671
-
672
  strategy_value = decision_data.get('strategy')
673
  if not strategy_value or strategy_value == 'unknown':
674
  decision_data['strategy'] = fallback_strategy
675
-
676
  return decision_data
677
  except Exception as e:
678
  print(f"Error parsing re-analysis response for {symbol}: {e}")
679
  return None
680
 
681
- # 🔴 --- START OF PROMPT CHANGE --- 🔴
682
  def _create_re_analysis_prompt(
683
  self,
684
- trade_data: dict,
685
- processed_data: dict,
686
- news_text: str,
687
- pattern_analysis: dict,
688
- whale_data: dict,
689
- statistical_feedback: str, # (NEW from Hub)
690
- active_context_playbook: str # (NEW from Hub)
691
  ) -> str:
692
 
693
- symbol = trade_data.get('symbol', 'N/A')
694
- entry_price = trade_data.get('entry_price', 'N/A')
695
- current_price = processed_data.get('current_price', 'N/A')
696
- strategy = trade_data.get('strategy', 'GENERIC')
697
-
698
- current_exit_profile = trade_data.get('decision_data', {}).get('exit_profile', 'N/A')
699
- current_exit_params = json.dumps(trade_data.get('decision_data', {}).get('exit_parameters', {}))
700
-
701
- # (New sections from the Learning Hub)
702
- statistical_feedback_section = f"🧠 STATISTICAL FEEDBACK (Slow-Learner):\n{statistical_feedback}"
703
- playbook_section = f"📚 LEARNING PLAYBOOK (Fast-Learner Active Rules):\n{active_context_playbook}"
704
-
705
- try:
706
- price_change = ((current_price - entry_price) / entry_price) * 100 if entry_price else 0
707
- price_change_display = f"{price_change:+.2f}%"
708
- except (TypeError, ZeroDivisionError):
709
- price_change_display = "N/A"
710
-
711
- indicators_summary = format_technical_indicators(processed_data.get('advanced_indicators', {}))
712
- pattern_summary = self._format_pattern_analysis(pattern_analysis) if pattern_analysis else "Pattern analysis data not available for re-analysis."
713
- whale_analysis_section = format_whale_analysis_for_llm(whale_data)
714
- market_context_section = self._format_market_context(processed_data.get('sentiment_data', {}))
715
- ohlcv_data = processed_data.get('raw_ohlcv') or processed_data.get('ohlcv', {})
716
- candle_data_section = self._format_candle_data_comprehensive(ohlcv_data)
717
-
718
 
719
  prompt = f"""
720
  TRADE RE-ANALYSIS FOR {symbol} (SPOT ONLY - Currently Open LONG Position)
721
-
722
- 🚨 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.
723
 
724
  📊 CURRENT TRADE CONTEXT:
725
  * Strategy: {strategy}
726
  * Entry Price: {entry_price} (LONG position)
727
  * Current Price: {current_price}
728
  * Current Performance: {price_change_display}
729
- * Trade Age: {trade_data.get('hold_duration_minutes', 'N/A')} minutes
730
  * Current Exit Profile: {current_exit_profile}
731
- * Current Exit Parameters: {current_exit_params}
732
 
733
  --- LEARNING HUB INPUT (CRITICAL) ---
734
  {playbook_section}
@@ -742,76 +463,42 @@ TRADE RE-ANALYSIS FOR {symbol} (SPOT ONLY - Currently Open LONG Position)
742
  {candle_data_section}
743
  {chr(10)}--- END OF CANDLE DATA ---{chr(10)}
744
 
745
- 🔍 UPDATED PATTERN ANALYSIS RESULTS (From System):
746
- {pattern_summary}
747
-
748
  🐋 UPDATED WHALE ACTIVITY:
749
  {whale_analysis_section}
750
 
751
  🌍 UPDATED MARKET CONTEXT:
752
  {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."}
753
 
754
- 📰 LATEST NEWS:
755
- {news_text if news_text else "No significant news found"}
756
-
757
  ---
758
  🎯 RE-ANALYSIS INSTRUCTIONS (SPOT - LONG POSITION):
759
-
760
- 1. **ANALYZE UPDATED DATA:** Evaluate if the original LONG thesis still holds based on the updated raw candle data, technicals, patterns, whale activity, market context, and (most importantly) the 'LEARNING HUB INPUT'.
761
- 2. **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 and/or the Exit Profile for the LONG position).
762
- 3. **IF UPDATING (CRITICAL):** If action is UPDATE_TRADE, you MUST provide:
763
- * `new_stop_loss` (New hard stop)
764
- * `new_take_profit` (New final target)
765
- * `new_exit_profile`: (e.g., "ATR_TRAILING") - Can be the same or different.
766
- * `new_exit_parameters`: (e.g., {{"atr_multiplier": 1.5}}) - Must match the new profile.
767
- 4. **SELF-CRITIQUE:** Perform a self-critique. What is the risk of this re-analysis decision?
768
-
769
- CRITICAL: The decision must be one of HOLD, CLOSE_TRADE, or UPDATE_TRADE for the existing LONG position.
770
 
771
  OUTPUT FORMAT (JSON - SPOT RE-ANALYSIS):
772
  {{
773
  "action": "HOLD/CLOSE_TRADE/UPDATE_TRADE",
774
  "reasoning": "Comprehensive justification for HOLD, CLOSE, or UPDATE of the LONG position, based on updated analysis. If UPDATE, explain why the new exit profile/parameters are better, referencing the Learning Hub input.",
775
-
776
  "new_stop_loss": 0.000000,
777
  "new_take_profit": 0.000000,
778
  "new_exit_profile": "None",
779
  "new_exit_parameters": {{}},
780
-
781
  "new_expected_minutes": 15,
782
  "confidence_level": 0.85,
783
  "strategy": "{strategy}",
784
- "whale_influence_reanalysis": "How updated whale data influenced the decision",
785
- "pattern_influence_reanalysis": "How updated candle patterns AND provided patterns influenced the decision",
786
- "risk_adjustment": "low/medium/high",
787
-
788
  "self_critique": {{
789
- "failure_modes": [
790
- "What is the primary risk of this new decision? (e.g., 'Holding this position increases exposure to market volatility.')",
791
- "What is the second risk? (e.g., 'Closing now might miss a future rebound.')"
792
- ],
793
- "confidence_adjustment_reason": "Brief reason if confidence was adjusted post-critique."
794
  }}
795
  }}
796
  """
797
  return prompt
798
- # 🔴 --- END OF PROMPT CHANGE --- 🔴
799
 
 
800
  def _format_pattern_analysis(self, pattern_analysis):
801
- """Helper to format pattern analysis for the LLM"""
802
- if not pattern_analysis or not pattern_analysis.get('pattern_detected') or pattern_analysis.get('pattern_detected') == 'no_clear_pattern':
803
- return "No clear chart pattern detected by the system."
804
-
805
- pattern = pattern_analysis.get('pattern_detected', 'N/A')
806
- confidence = pattern_analysis.get('pattern_confidence', 0)
807
- direction = pattern_analysis.get('predicted_direction', 'N/A')
808
- timeframe = pattern_analysis.get('timeframe', 'N/A') # (This key might not exist, need to check patterns.py)
809
-
810
- # (Assuming timeframe is part of the top-level analysis)
811
- tf_display = f"on {timeframe} timeframe" if timeframe != 'N/A' else ""
812
-
813
  return f"System Pattern Analysis: Detected '{pattern}' {tf_display} with {confidence:.2f} confidence. Predicted direction: {direction}."
814
 
 
815
  @_rate_limit_nvidia_api
816
  async def _call_llm(self, prompt: str) -> str:
817
  try:
@@ -830,7 +517,6 @@ OUTPUT FORMAT (JSON - SPOT RE-ANALYSIS):
830
  else:
831
  print(f"⚠️ LLM returned invalid content (attempt {attempt+1}): {content[:100]}...")
832
  if attempt == 0: await asyncio.sleep(1)
833
-
834
  except (RateLimitError, APITimeoutError) as e:
835
  print(f"❌ LLM API Error (Rate Limit/Timeout): {e}. Retrying via backoff...")
836
  raise
@@ -838,12 +524,10 @@ OUTPUT FORMAT (JSON - SPOT RE-ANALYSIS):
838
  print(f"❌ Unexpected LLM API error (attempt {attempt+1}): {e}")
839
  if attempt == 0: await asyncio.sleep(2)
840
  elif attempt == 1: raise
841
-
842
  print("❌ LLM failed to return valid content after retries.")
843
  return ""
844
-
845
  except Exception as e:
846
  print(f"❌ Final failure in _call_llm after backoff retries: {e}")
847
  raise
848
 
849
- print("✅ LLM Service loaded - V3 (Integrated Learning Hub, English-only Prompts, Self-Critique)")
 
1
+ # LLM.py (Updated to output "WATCH" for Sentry)
2
  import os, traceback, asyncio, json, time
3
+ import re
4
  from datetime import datetime
5
  from functools import wraps
6
  from backoff import on_exception, expo
7
  from openai import OpenAI, RateLimitError, APITimeoutError
8
  import numpy as np
9
  from sentiment_news import NewsFetcher
 
10
  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
11
+ from ml_engine.processor import safe_json_parse
12
 
13
  NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY")
14
  PRIMARY_MODEL = "nvidia/llama-3.1-nemotron-ultra-253b-v1"
15
 
16
+ # (PatternAnalysisEngine - لا تغيير)
17
  class PatternAnalysisEngine:
18
  # --- (هذا الكلاس جزء من LLM.py ومطلوب لتحليل الشموع) ---
19
  def __init__(self, llm_service):
20
  self.llm = llm_service
 
21
  def _format_chart_data_for_llm(self, ohlcv_data):
22
+ if not ohlcv_data: return "Insufficient chart data for pattern analysis"
 
 
 
23
  try:
 
24
  all_timeframes = []
25
  for timeframe, candles in ohlcv_data.items():
26
+ if candles and len(candles) >= 10:
 
27
  raw_candle_summary = self._format_raw_candle_data(candles, timeframe)
28
  all_timeframes.append(f"=== {timeframe.upper()} TIMEFRAME ({len(candles)} CANDLES) ===\n{raw_candle_summary}")
 
29
  return "\n\n".join(all_timeframes) if all_timeframes else "No sufficient timeframe data available"
30
+ except Exception as e: return f"Error formatting chart data: {str(e)}"
 
 
31
  def _format_raw_candle_data(self, candles, timeframe):
 
32
  try:
33
+ if len(candles) < 10: return f"Only {len(candles)} candles available - insufficient for deep pattern analysis"
 
 
 
34
  analysis_candles = candles[-50:] if len(candles) > 50 else candles
35
+ summary = []; summary.append(f"Total candles: {len(candles)} (showing last {len(analysis_candles)})"); summary.append("Recent candles (newest to oldest):")
 
 
 
 
 
36
  for i in range(min(15, len(analysis_candles))):
37
+ idx = len(analysis_candles) - 1 - i; candle = analysis_candles[idx]
38
+ try: timestamp = datetime.fromtimestamp(candle[0] / 1000).strftime('%Y-%m-%d %H:%M:%S')
39
+ except: timestamp = "unknown"
 
 
 
 
 
 
40
  open_price, high, low, close, volume = candle[1], candle[2], candle[3], candle[4], candle[5]
 
41
  candle_type = "🟢 BULLISH" if close > open_price else "🔴 BEARISH" if close < open_price else "⚪ NEUTRAL"
42
+ body_size = abs(close - open_price); body_percent = (body_size / open_price * 100) if open_price > 0 else 0
43
+ wick_upper = high - max(open_price, close); wick_lower = min(open_price, close) - low; total_range = high - low
44
+ 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
45
+ else: body_ratio = upper_wick_ratio = lower_wick_ratio = 0
46
+ 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}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  if len(analysis_candles) >= 20:
48
  stats = self._calculate_candle_statistics(analysis_candles)
49
+ 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}")
 
 
 
 
 
 
 
50
  return "\n".join(summary)
51
+ except Exception as e: return f"Error formatting raw candle data: {str(e)}"
 
 
 
52
  def _calculate_candle_statistics(self, candles):
 
53
  try:
54
+ 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]
55
+ first_close = closes[0]; last_close = closes[-1]; price_change = ((last_close - first_close) / first_close) * 100
56
+ 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
57
+ true_ranges = [];
58
+ 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))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  atr = sum(true_ranges) / len(true_ranges) if true_ranges else 0
60
+ if price_change > 3: trend = "STRONG UPTREND"
61
+ elif price_change > 1: trend = "UPTREND"
62
+ elif price_change < -3: trend = "STRONG DOWNTREND"
63
+ elif price_change < -1: trend = "DOWNTREND"
64
+ else: trend = "SIDEWAYS"
65
+ support = min(lows); resistance = max(highs)
66
+ return {'price_change': price_change, 'avg_body': avg_body, 'atr': atr, 'trend': trend, 'support': support, 'resistance': resistance}
67
+ except Exception as e: return {'price_change': 0, 'avg_body': 0, 'atr': 0, 'trend': 'UNKNOWN', 'support': 0, 'resistance': 0}
68
+ async def analyze_chart_patterns(self, symbol, ohlcv_data): pass
69
+ def _parse_pattern_response(self, response_text): pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
 
72
  class LLMService:
 
78
  self.news_fetcher = NewsFetcher()
79
  self.pattern_engine = PatternAnalysisEngine(self)
80
  self.semaphore = asyncio.Semaphore(5)
81
+ self.r2_service = None
82
+ self.learning_hub = None
 
 
 
 
83
 
84
  def _rate_limit_nvidia_api(func):
85
  @wraps(func)
 
88
  return await func(*args, **kwargs)
89
  return wrapper
90
 
91
+ # (get_trading_decision - لا تغيير في المنطق، فقط الـ prompt)
92
  async def get_trading_decision(self, data_payload: dict):
93
  try:
94
  symbol = data_payload.get('symbol', 'unknown')
95
  target_strategy = data_payload.get('target_strategy', 'GENERIC')
96
 
97
  ohlcv_data = data_payload.get('raw_ohlcv') or data_payload.get('ohlcv')
98
+ if not ohlcv_data: return None
 
 
 
99
  total_candles = sum(len(data) for data in ohlcv_data.values() if data) if ohlcv_data else 0
100
  timeframes_count = len([tf for tf, data in ohlcv_data.items() if data and len(data) >= 10]) if ohlcv_data else 0
101
+ if total_candles < 30: return None
 
 
 
 
102
  valid_timeframes = [tf for tf, candles in ohlcv_data.items() if candles and len(candles) >= 5]
103
+ if not valid_timeframes: return None
 
 
104
 
105
  news_text = await self.news_fetcher.get_news_for_symbol(symbol)
106
  whale_data = data_payload.get('whale_data', {})
107
 
 
 
108
  statistical_feedback = "No statistical learning data yet."
109
  active_context_playbook = "No active learning rules available."
110
 
111
  if self.learning_hub and self.learning_hub.initialized:
 
112
  statistical_feedback = await self.learning_hub.get_statistical_feedback_for_llm(target_strategy)
 
 
113
  active_context_playbook = await self.learning_hub.get_active_context_for_llm(
114
+ domain="strategy", query=f"{target_strategy} {symbol}"
 
115
  )
116
 
117
+ # 🔴 --- START OF CHANGE --- 🔴
118
+ # (استدعاء الـ prompt الجديد)
119
+ prompt = self._create_comprehensive_sentry_prompt(
120
+ data_payload, news_text, None, whale_data,
121
+ statistical_feedback, active_context_playbook
 
 
 
122
  )
123
  # 🔴 --- END OF CHANGE --- 🔴
124
 
125
  if self.r2_service:
126
+ analysis_data = { 'symbol': symbol, 'target_strategy': target_strategy, 'statistical_feedback': statistical_feedback, 'active_context_playbook': active_context_playbook }
 
 
 
 
 
 
 
 
 
 
 
127
  await self.r2_service.save_llm_prompts_async(
128
+ symbol, 'sentry_watchlist_decision_v5', prompt, analysis_data
129
  )
130
 
131
  async with self.semaphore:
132
  response = await self._call_llm(prompt)
133
 
134
  decision_dict = self._parse_llm_response_enhanced(response, target_strategy, symbol)
135
+
136
+ # 🔴 --- START OF CHANGE --- 🔴
137
+ # (التحقق من صحة المخرجات الجديدة)
138
  if decision_dict:
139
+ if decision_dict.get('action') == 'WATCH' and 'strategy_to_watch' not in decision_dict:
140
+ print(f" ⚠️ LLM {symbol}: Action is WATCH but strategy_to_watch is missing. Forcing HOLD.")
141
+ decision_dict['action'] = 'HOLD'
142
+
143
  decision_dict['model_source'] = self.model_name
144
  decision_dict['whale_data_integrated'] = whale_data.get('data_available', False)
 
 
145
  return decision_dict
146
+ # 🔴 --- END OF CHANGE --- 🔴
147
  else:
148
  print(f"❌ LLM parsing failed for {symbol} - no fallback decisions")
149
  return None
150
 
151
  except Exception as e:
152
+ print(f"❌ Error in get_trading_decision for {data_payload.get('symbol', 'unknown')}: {e}"); traceback.print_exc()
 
153
  return None
154
 
155
+ # (parse_llm_response_enhanced - لا تغيير)
156
  def _parse_llm_response_enhanced(self, response_text: str, fallback_strategy: str, symbol: str) -> dict:
157
  try:
158
  json_str = parse_json_from_response(response_text)
 
165
  print(f"❌ Failed to parse JSON (safe_json_parse) for {symbol}: {response_text}")
166
  return None
167
 
168
+ # (هذا مخصص لـ Reflector/Curator)
169
  if fallback_strategy == "reflection" or fallback_strategy == "distillation":
 
170
  return decision_data
171
 
172
+ # 🔴 --- START OF CHANGE --- 🔴
173
+ # (تحديث التحقق من الصحة للقرار الجديد)
174
+ required_fields = ['action', 'reasoning', 'confidence_level', 'pattern_identified_by_llm']
175
+
176
+ # (إذا كان القرار "WATCH"، نحتاج حقول أقل)
177
+ if decision_data.get('action') == 'WATCH':
178
+ required_fields.append('strategy_to_watch')
179
+ # (إذا كان القرار "BUY"، نحتاج كل شيء - هذا هو الاحتياطي القديم)
180
+ elif decision_data.get('action') == 'BUY':
181
+ required_fields.extend(['risk_assessment', 'stop_loss', 'take_profit', 'expected_target_minutes', 'exit_profile', 'exit_parameters'])
182
+ # 🔴 --- END OF CHANGE --- 🔴
183
+
184
  if not validate_required_fields(decision_data, required_fields):
185
  print(f"❌ Missing required fields in LLM response for {symbol}")
186
  missing = [f for f in required_fields if f not in decision_data]
187
  print(f" MIA: {missing}")
188
  return None
189
 
190
+ # 🔴 --- START OF CHANGE --- 🔴
 
 
 
191
  action = decision_data.get('action')
192
+ if action not in ['WATCH', 'HOLD']:
193
  print(f"⚠️ LLM suggested unsupported action ({action}) for {symbol}. Forcing HOLD.")
194
  decision_data['action'] = 'HOLD'
195
+ # 🔴 --- END OF CHANGE --- 🔴
196
 
197
+ if decision_data.get('action') == 'BUY':
198
  decision_data['trade_type'] = 'LONG'
199
  else:
200
  decision_data['trade_type'] = None
 
208
  print(f"❌ Error parsing LLM response for {symbol}: {e}")
209
  return None
210
 
211
+ # (_get_pattern_analysis - لا تغيير)
212
  async def _get_pattern_analysis(self, data_payload):
213
  try:
214
  symbol = data_payload['symbol']
215
  ohlcv_data = data_payload.get('raw_ohlcv') or data_payload.get('ohlcv')
216
+ if ohlcv_data: return None
 
 
217
  return None
218
  except Exception as e:
219
  print(f"❌ Pattern analysis failed for {data_payload.get('symbol')}: {e}")
220
  return None
221
 
222
  # 🔴 --- START OF PROMPT CHANGE --- 🔴
223
+ def _create_comprehensive_sentry_prompt(
224
  self,
225
  payload: dict,
226
  news_text: str,
227
+ pattern_analysis: dict,
228
  whale_data: dict,
229
+ statistical_feedback: str,
230
+ active_context_playbook: str
231
  ) -> str:
232
 
233
  symbol = payload.get('symbol', 'N/A')
 
239
  recommended_strategy = payload.get('recommended_strategy', 'N/A')
240
  target_strategy = payload.get('target_strategy', 'GENERIC')
241
  enhanced_final_score = payload.get('enhanced_final_score', 0)
 
242
  enhanced_score_display = f"{enhanced_final_score:.3f}" if isinstance(enhanced_final_score, (int, float)) else str(enhanced_final_score)
243
 
244
  indicators_summary = format_technical_indicators(advanced_indicators)
 
248
  candle_data_section = self._format_candle_data_comprehensive(ohlcv_data)
249
  market_context_section = self._format_market_context(sentiment_data)
250
 
 
251
  statistical_feedback_section = f"🧠 STATISTICAL FEEDBACK (Slow-Learner):\n{statistical_feedback}"
252
  playbook_section = f"📚 LEARNING PLAYBOOK (Fast-Learner Active Rules):\n{active_context_playbook}"
253
 
254
 
255
  prompt = f"""
256
+ COMPREHENSIVE STRATEGIC ANALYSIS FOR {symbol} (FOR SENTRY WATCHLIST)
257
 
258
+ 🚨 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.
259
 
260
  🎯 STRATEGY CONTEXT:
261
  * Target Strategy: {target_strategy}
262
+ * Recommended Strategy (from ML): {recommended_strategy}
263
  * Current Price: ${current_price}
264
  * Enhanced System Score: {enhanced_score_display}
265
 
 
284
  🌍 MARKET CONTEXT:
285
  {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."}
286
 
287
+ ---
288
+ 🎯 SENTRY DECISION INSTRUCTIONS (WATCH or HOLD):
289
 
290
+ 1. **PERFORM CHART PATTERN ANALYSIS:** Based *ONLY* on the provided 'RAW CANDLE DATA SUMMARY & STATISTICS', identify relevant patterns.
291
+ 2. **INTEGRATE ALL DATA:** Combine YOUR pattern analysis with technicals, strategy scores, whale activity, market context, and (most importantly) the 'LEARNING HUB INPUT'.
292
+ 3. **DECIDE ACTION (WATCH or HOLD):**
293
+ * **WATCH:** Choose 'WATCH' if you have HIGH CONFIDENCE (>= 0.75) that this asset presents an imminent (next 1-60 minutes) opportunity for the *Recommended Strategy*. The Sentry (Layer 2) will take over and look for the tactical entry.
294
+ * **HOLD:** Choose 'HOLD' if the setup is weak, unclear, or too risky.
295
+ 4. **DEFINE STRATEGY:** If (and only if) action is 'WATCH', you MUST specify which strategy the Sentry should use (e.g., 'breakout_momentum', 'mean_reversion'). This MUST be one of the strategies from the 'STRATEGY ANALYSIS' section.
296
+ 5. **SELF-CRITIQUE:** Justify your decision. Why is this strong enough for the Sentry?
297
 
298
+ OUTPUT FORMAT (JSON - SENTRY DECISION):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  {{
300
+ "action": "WATCH/HOLD",
301
+ "reasoning": "Detailed explanation integrating ALL data sources, justifying WHY this asset should (or should not) be passed to the Sentry. If WATCH, explain what tactical signals the Sentry should look for (e.g., 'Sentry should watch for a high-volume breakout above the 1H resistance combined with positive CVD').",
302
+ "pattern_identified_by_llm": "Name of the primary pattern(s) identified (e.g., 'Bull Flag on 1H', 'No Clear Pattern')",
303
+
304
+ "confidence_level": 0.85,
305
+ "strategy_to_watch": "breakout_momentum",
306
+
307
+ "self_critique": {{
308
+ "failure_modes": [
309
+ "What is the first reason this 'WATCH' decision could fail? (e.g., 'The identified pattern is a false breakout.')",
310
+ "What is the second reason? (e.g., 'The Sentry might enter too late.')"
311
+ ],
312
+ "confidence_adjustment_reason": "Brief reason if confidence was adjusted post-critique."
313
+ }}
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  }}
315
  """
316
  return prompt
317
  # 🔴 --- END OF PROMPT CHANGE --- 🔴
318
 
319
+ # (Functions _format_candle_data_comprehensive, _analyze_timeframe_candles, _format_market_context - لا تغيير)
320
  def _format_candle_data_comprehensive(self, ohlcv_data):
321
+ if not ohlcv_data: return "No raw candle data available for analysis"
 
 
 
322
  try:
323
+ timeframes_available = []; total_candles = 0
 
 
324
  for timeframe, candles in ohlcv_data.items():
325
+ if candles and len(candles) >= 5: timeframes_available.append(f"{timeframe.upper()} ({len(candles)} candles)"); total_candles += len(candles)
326
+ if not timeframes_available: return "Insufficient candle data across all timeframes"
 
 
 
 
 
327
  summary = f"📊 Available Timeframes: {', '.join(timeframes_available)}\n"
328
  summary += f"📈 Total Candles Available: {total_candles}\n\n"
 
329
  raw_candle_analysis_text = self.pattern_engine._format_chart_data_for_llm(ohlcv_data)
 
330
  summary += raw_candle_analysis_text
 
331
  return summary
332
+ except Exception as e: return f"Error formatting raw candle data: {str(e)}"
 
 
333
  def _analyze_timeframe_candles(self, candles, timeframe):
 
334
  try:
335
+ if len(candles) < 10: return f"Insufficient data ({len(candles)} candles)"
336
+ 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]
337
+ current_price = closes[-1]; first_price = closes[0]; price_change = ((current_price - first_price) / first_price) * 100 if first_price > 0 else 0
 
 
 
 
 
 
 
 
 
 
 
 
338
  if price_change > 2: trend = "🟢 UPTREND"
339
  elif price_change < -2: trend = "🔴 DOWNTREND"
340
  else: trend = "⚪ SIDEWAYS"
341
+ high_max = max(highs); low_min = min(lows); volatility = ((high_max - low_min) / low_min) * 100 if low_min > 0 else 0
 
 
 
 
342
  avg_volume = sum(volumes) / len(volumes) if volumes else 1
343
+ current_volume = volumes[-1] if volumes else 0; volume_ratio = current_volume / avg_volume if avg_volume > 0 else 1
344
+ 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
345
+ 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}"]
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  return "\n".join(analysis)
347
+ except Exception as e: return f"Analysis error: {str(e)}"
 
 
348
  def _format_market_context(self, sentiment_data):
349
+ if not sentiment_data or sentiment_data.get('data_quality', 'LOW') == 'LOW': return "Market context data not available or incomplete."
350
+ 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')
351
+ 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'}"]
352
+ general_whale = sentiment_data.get('general_whale_activity', {});
 
 
 
 
 
 
 
 
 
 
 
353
  if general_whale and general_whale.get('sentiment') != 'NEUTRAL':
354
+ whale_sentiment = general_whale.get('sentiment', 'N/A'); critical_alert = general_whale.get('critical_alert', False)
355
+ lines.append(f"• General Whale Sentiment: {whale_sentiment.replace('_', ' ').title() if isinstance(whale_sentiment, str) else 'N/A'}");
356
+ if critical_alert: lines.append(" ⚠️ CRITICAL WHALE ALERT ACTIVE")
 
 
 
357
  return "\n".join(lines)
358
 
359
 
360
+ # (re_analyze_trade_async - لا تغيير)
361
  async def re_analyze_trade_async(self, trade_data: dict, processed_data: dict):
362
  try:
363
+ symbol = trade_data['symbol']; original_strategy = trade_data.get('strategy', 'GENERIC')
 
 
364
  ohlcv_data = processed_data.get('raw_ohlcv') or processed_data.get('ohlcv')
365
+ if not ohlcv_data: return None
 
 
 
366
  news_text = await self.news_fetcher.get_news_for_symbol(symbol)
367
  pattern_analysis = await self._get_pattern_analysis(processed_data)
368
  whale_data = processed_data.get('whale_data', {})
369
 
 
 
370
  statistical_feedback = "No statistical learning data yet."
371
  active_context_playbook = "No active learning rules available."
 
372
  if self.learning_hub and self.learning_hub.initialized:
 
373
  statistical_feedback = await self.learning_hub.get_statistical_feedback_for_llm(original_strategy)
 
 
374
  active_context_playbook = await self.learning_hub.get_active_context_for_llm(
375
+ domain="strategy", query=f"{original_strategy} {symbol} re-analysis"
 
376
  )
377
 
 
378
  prompt = self._create_re_analysis_prompt(
379
+ trade_data, processed_data, news_text, pattern_analysis,
380
+ whale_data, statistical_feedback, active_context_playbook
 
 
 
 
 
381
  )
 
382
 
383
  if self.r2_service:
384
+ analysis_data = { 'symbol': symbol, 'original_strategy': original_strategy }
 
 
 
 
 
 
 
 
385
  await self.r2_service.save_llm_prompts_async(
386
+ symbol, 'trade_reanalysis_v5_hub', prompt, analysis_data
387
  )
388
 
389
  async with self.semaphore:
 
392
  re_analysis_dict = self._parse_re_analysis_response(response, original_strategy, symbol)
393
  if re_analysis_dict:
394
  re_analysis_dict['model_source'] = self.model_name
 
395
  return re_analysis_dict
396
  else:
397
  print(f"❌ LLM re-analysis parsing failed for {symbol}")
398
  return None
 
399
  except Exception as e:
400
+ print(f"❌ Error in LLM re-analysis: {e}"); traceback.print_exc()
 
401
  return None
402
 
403
+ # (parse_re_analysis_response - لا تغيير)
404
  def _parse_re_analysis_response(self, response_text: str, fallback_strategy: str, symbol: str) -> dict:
405
  try:
406
  json_str = parse_json_from_response(response_text)
407
+ if not json_str: return None
 
 
408
  decision_data = safe_json_parse(json_str)
409
+ if not decision_data: print(f"❌ Failed to parse JSON (safe_json_parse) for re-analysis of {symbol}: {response_text}"); return None
 
 
410
 
411
  action = decision_data.get('action')
412
  if action not in ['HOLD', 'CLOSE_TRADE', 'UPDATE_TRADE']:
413
  print(f"⚠️ LLM suggested unsupported re-analysis action ({action}) for {symbol}. Forcing HOLD.")
414
  decision_data['action'] = 'HOLD'
 
415
  if action == 'UPDATE_TRADE':
416
  required_update_fields = ['new_stop_loss', 'new_take_profit', 'new_exit_profile', 'new_exit_parameters']
417
  if not validate_required_fields(decision_data, required_update_fields):
418
+ print(f"❌ Missing required fields for UPDATE_TRADE for {symbol}"); decision_data['action'] = 'HOLD'
 
 
 
419
  elif not isinstance(decision_data['new_exit_parameters'], dict):
420
+ print(f"❌ 'new_exit_parameters' is not a valid dict for {symbol}"); decision_data['action'] = 'HOLD'
 
 
421
  strategy_value = decision_data.get('strategy')
422
  if not strategy_value or strategy_value == 'unknown':
423
  decision_data['strategy'] = fallback_strategy
 
424
  return decision_data
425
  except Exception as e:
426
  print(f"Error parsing re-analysis response for {symbol}: {e}")
427
  return None
428
 
429
+ # (_create_re_analysis_prompt - لا تغيير)
430
  def _create_re_analysis_prompt(
431
  self,
432
+ trade_data: dict, processed_data: dict, news_text: str,
433
+ pattern_analysis: dict, whale_data: dict,
434
+ statistical_feedback: str, active_context_playbook: str
 
 
 
 
435
  ) -> str:
436
 
437
+ 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', {}))
438
+ statistical_feedback_section = f"🧠 STATISTICAL FEEDBACK (Slow-Learner):\n{statistical_feedback}"; playbook_section = f"📚 LEARNING PLAYBOOK (Fast-Learner Active Rules):\n{active_context_playbook}"
439
+ try: price_change = ((current_price - entry_price) / entry_price) * 100 if entry_price else 0; price_change_display = f"{price_change:+.2f}%"
440
+ except (TypeError, ZeroDivisionError): price_change_display = "N/A"
441
+ 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)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
 
443
  prompt = f"""
444
  TRADE RE-ANALYSIS FOR {symbol} (SPOT ONLY - Currently Open LONG Position)
445
+ (Prompt unchanged - V4)
 
446
 
447
  📊 CURRENT TRADE CONTEXT:
448
  * Strategy: {strategy}
449
  * Entry Price: {entry_price} (LONG position)
450
  * Current Price: {current_price}
451
  * Current Performance: {price_change_display}
 
452
  * Current Exit Profile: {current_exit_profile}
 
453
 
454
  --- LEARNING HUB INPUT (CRITICAL) ---
455
  {playbook_section}
 
463
  {candle_data_section}
464
  {chr(10)}--- END OF CANDLE DATA ---{chr(10)}
465
 
 
 
 
466
  🐋 UPDATED WHALE ACTIVITY:
467
  {whale_analysis_section}
468
 
469
  🌍 UPDATED MARKET CONTEXT:
470
  {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."}
471
 
 
 
 
472
  ---
473
  🎯 RE-ANALYSIS INSTRUCTIONS (SPOT - LONG POSITION):
474
+ (Instructions remain the same: HOLD/CLOSE/UPDATE)
 
 
 
 
 
 
 
 
 
 
475
 
476
  OUTPUT FORMAT (JSON - SPOT RE-ANALYSIS):
477
  {{
478
  "action": "HOLD/CLOSE_TRADE/UPDATE_TRADE",
479
  "reasoning": "Comprehensive justification for HOLD, CLOSE, or UPDATE of the LONG position, based on updated analysis. If UPDATE, explain why the new exit profile/parameters are better, referencing the Learning Hub input.",
 
480
  "new_stop_loss": 0.000000,
481
  "new_take_profit": 0.000000,
482
  "new_exit_profile": "None",
483
  "new_exit_parameters": {{}},
 
484
  "new_expected_minutes": 15,
485
  "confidence_level": 0.85,
486
  "strategy": "{strategy}",
 
 
 
 
487
  "self_critique": {{
488
+ "failure_modes": ["Primary risk of this new decision?", "Second risk?"],
489
+ "confidence_adjustment_reason": "Brief reason if confidence was adjusted."
 
 
 
490
  }}
491
  }}
492
  """
493
  return prompt
 
494
 
495
+ # (_format_pattern_analysis - لا تغيير)
496
  def _format_pattern_analysis(self, pattern_analysis):
497
+ 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."
498
+ 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 ""
 
 
 
 
 
 
 
 
 
 
499
  return f"System Pattern Analysis: Detected '{pattern}' {tf_display} with {confidence:.2f} confidence. Predicted direction: {direction}."
500
 
501
+ # (_call_llm - لا تغيير)
502
  @_rate_limit_nvidia_api
503
  async def _call_llm(self, prompt: str) -> str:
504
  try:
 
517
  else:
518
  print(f"⚠️ LLM returned invalid content (attempt {attempt+1}): {content[:100]}...")
519
  if attempt == 0: await asyncio.sleep(1)
 
520
  except (RateLimitError, APITimeoutError) as e:
521
  print(f"❌ LLM API Error (Rate Limit/Timeout): {e}. Retrying via backoff...")
522
  raise
 
524
  print(f"❌ Unexpected LLM API error (attempt {attempt+1}): {e}")
525
  if attempt == 0: await asyncio.sleep(2)
526
  elif attempt == 1: raise
 
527
  print("❌ LLM failed to return valid content after retries.")
528
  return ""
 
529
  except Exception as e:
530
  print(f"❌ Final failure in _call_llm after backoff retries: {e}")
531
  raise
532
 
533
+ print("✅ LLM Service loaded - V5 (Explorer/Sentry Mode - Outputs 'WATCH')")