Spaces:
Running
Running
Update LLM.py
Browse files
LLM.py
CHANGED
|
@@ -1,176 +1,72 @@
|
|
| 1 |
-
# LLM.py (Updated to
|
| 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 |
-
# ✅ تعديل الاستيراد: 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:
|
| 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 |
-
|
| 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 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 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 |
-
|
| 108 |
-
|
| 109 |
-
|
| 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 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 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
|
| 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 |
-
#
|
| 240 |
-
prompt
|
| 241 |
-
|
| 242 |
-
news_text,
|
| 243 |
-
|
| 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, '
|
| 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 |
-
# (
|
| 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 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 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 |
-
|
| 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 ['
|
| 320 |
print(f"⚠️ LLM suggested unsupported action ({action}) for {symbol}. Forcing HOLD.")
|
| 321 |
decision_data['action'] = 'HOLD'
|
|
|
|
| 322 |
|
| 323 |
-
if decision_data
|
| 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
|
| 351 |
self,
|
| 352 |
payload: dict,
|
| 353 |
news_text: str,
|
| 354 |
-
pattern_analysis: dict,
|
| 355 |
whale_data: dict,
|
| 356 |
-
statistical_feedback: str,
|
| 357 |
-
active_context_playbook: str
|
| 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
|
| 386 |
|
| 387 |
-
🚨 IMPORTANT
|
| 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 |
-
|
| 417 |
-
|
| 418 |
|
| 419 |
-
|
| 420 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 550 |
-
|
| 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 |
-
|
| 566 |
-
lines.append(
|
| 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 |
-
|
| 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, '
|
| 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 |
-
#
|
| 682 |
def _create_re_analysis_prompt(
|
| 683 |
self,
|
| 684 |
-
trade_data: dict,
|
| 685 |
-
|
| 686 |
-
|
| 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 |
-
|
| 695 |
-
|
| 696 |
-
|
| 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 |
-
|
| 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 |
-
"
|
| 802 |
-
|
| 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 -
|
|
|
|
| 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')")
|