Update LLM.py
Browse files
LLM.py
CHANGED
|
@@ -1,19 +1,17 @@
|
|
| 1 |
-
# LLM.py (
|
| 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 |
# --- (كود PatternAnalysisEngine كما هو بدون تغيير) ---
|
| 19 |
def __init__(self, llm_service):
|
|
@@ -180,8 +178,12 @@ class LLMService:
|
|
| 180 |
self.news_fetcher = NewsFetcher()
|
| 181 |
self.pattern_engine = PatternAnalysisEngine(self)
|
| 182 |
self.semaphore = asyncio.Semaphore(5)
|
| 183 |
-
self.r2_service = None #
|
| 184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
|
| 186 |
def _rate_limit_nvidia_api(func):
|
| 187 |
@wraps(func)
|
|
@@ -197,53 +199,65 @@ class LLMService:
|
|
| 197 |
|
| 198 |
ohlcv_data = data_payload.get('raw_ohlcv') or data_payload.get('ohlcv')
|
| 199 |
if not ohlcv_data:
|
| 200 |
-
print(f"⚠️
|
| 201 |
return None
|
| 202 |
|
| 203 |
total_candles = sum(len(data) for data in ohlcv_data.values() if data) if ohlcv_data else 0
|
| 204 |
timeframes_count = len([tf for tf, data in ohlcv_data.items() if data and len(data) >= 10]) if ohlcv_data else 0
|
| 205 |
|
| 206 |
-
# 🔴 إصلاح: استخدام (timeframes_count) بدلاً من (0)
|
| 207 |
-
print(f" 📊 بيانات {symbol}: {total_candles} شمعة في {timeframes_count} إطار زمني")
|
| 208 |
-
|
| 209 |
if total_candles < 30:
|
| 210 |
-
print(f" ⚠️
|
| 211 |
return None
|
| 212 |
|
| 213 |
valid_timeframes = [tf for tf, candles in ohlcv_data.items() if candles and len(candles) >= 5]
|
| 214 |
if not valid_timeframes:
|
| 215 |
-
print(f" ⚠️
|
| 216 |
return None
|
| 217 |
-
print(f" ✅ أطر زمنية صالحة لـ {symbol}: {', '.join(valid_timeframes)}")
|
| 218 |
|
| 219 |
news_text = await self.news_fetcher.get_news_for_symbol(symbol)
|
| 220 |
whale_data = data_payload.get('whale_data', {})
|
| 221 |
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
if best_learned_exit != "unknown":
|
| 227 |
-
learning_feedback = f"Learning System Feedback: For the '{target_strategy}' strategy, the '{best_learned_exit}' exit profile has historically performed best. Please consider this."
|
| 228 |
|
| 229 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
if self.r2_service:
|
| 232 |
analysis_data = {
|
| 233 |
'symbol': symbol,
|
| 234 |
'current_price': data_payload.get('current_price'),
|
| 235 |
-
'final_score': data_payload.get('final_score'),
|
| 236 |
'enhanced_final_score': data_payload.get('enhanced_final_score'),
|
| 237 |
'target_strategy': target_strategy,
|
| 238 |
-
'
|
|
|
|
| 239 |
'whale_data_available': whale_data.get('data_available', False),
|
| 240 |
'total_candles': total_candles,
|
| 241 |
'timeframes_count': timeframes_count,
|
| 242 |
-
'valid_timeframes': valid_timeframes,
|
| 243 |
'timestamp': datetime.now().isoformat()
|
| 244 |
}
|
| 245 |
await self.r2_service.save_llm_prompts_async(
|
| 246 |
-
symbol, '
|
| 247 |
)
|
| 248 |
|
| 249 |
async with self.semaphore:
|
|
@@ -257,11 +271,11 @@ class LLMService:
|
|
| 257 |
decision_dict['timeframes_analyzed'] = timeframes_count
|
| 258 |
return decision_dict
|
| 259 |
else:
|
| 260 |
-
print(f"❌
|
| 261 |
return None
|
| 262 |
|
| 263 |
except Exception as e:
|
| 264 |
-
print(f"❌
|
| 265 |
traceback.print_exc()
|
| 266 |
return None
|
| 267 |
|
|
@@ -269,32 +283,37 @@ class LLMService:
|
|
| 269 |
try:
|
| 270 |
json_str = parse_json_from_response(response_text)
|
| 271 |
if not json_str:
|
| 272 |
-
print(f"❌
|
| 273 |
return None
|
| 274 |
|
| 275 |
decision_data = safe_json_parse(json_str)
|
| 276 |
if not decision_data:
|
| 277 |
-
print(f"❌
|
| 278 |
return None
|
| 279 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
required_fields = [
|
| 281 |
'action', 'reasoning', 'risk_assessment', 'stop_loss', 'take_profit',
|
| 282 |
'expected_target_minutes', 'confidence_level', 'pattern_identified_by_llm',
|
| 283 |
'exit_profile', 'exit_parameters'
|
| 284 |
]
|
| 285 |
if not validate_required_fields(decision_data, required_fields):
|
| 286 |
-
print(f"❌
|
| 287 |
missing = [f for f in required_fields if f not in decision_data]
|
| 288 |
print(f" MIA: {missing}")
|
| 289 |
return None
|
| 290 |
|
| 291 |
if not isinstance(decision_data['exit_parameters'], dict):
|
| 292 |
-
print(f"❌
|
| 293 |
return None
|
| 294 |
|
| 295 |
action = decision_data.get('action')
|
| 296 |
if action not in ['BUY', 'HOLD']:
|
| 297 |
-
print(f"⚠️
|
| 298 |
decision_data['action'] = 'HOLD'
|
| 299 |
|
| 300 |
if decision_data['action'] == 'BUY':
|
|
@@ -308,21 +327,24 @@ class LLMService:
|
|
| 308 |
|
| 309 |
return decision_data
|
| 310 |
except Exception as e:
|
| 311 |
-
print(f"❌
|
| 312 |
return None
|
| 313 |
|
| 314 |
async def _get_pattern_analysis(self, data_payload):
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
|
|
|
|
|
|
|
|
|
| 326 |
symbol = payload.get('symbol', 'N/A')
|
| 327 |
current_price = payload.get('current_price', 'N/A')
|
| 328 |
reasons = payload.get('reasons_for_candidacy', [])
|
|
@@ -331,24 +353,21 @@ class LLMService:
|
|
| 331 |
strategy_scores = payload.get('strategy_scores', {})
|
| 332 |
recommended_strategy = payload.get('recommended_strategy', 'N/A')
|
| 333 |
target_strategy = payload.get('target_strategy', 'GENERIC')
|
| 334 |
-
|
| 335 |
-
enhanced_final_score = payload.get('enhanced_final_score', 'N/A') # 🔴 هذا هو المتغير الصحيح
|
| 336 |
-
ohlcv_data = payload.get('raw_ohlcv') or payload.get('ohlcv', {})
|
| 337 |
|
| 338 |
-
final_score_display = f"{final_score:.3f}" if isinstance(final_score, (int, float)) else str(final_score)
|
| 339 |
-
|
| 340 |
-
# 🔴 --- بدء الإصلاح --- 🔴
|
| 341 |
-
# تم تصحيح الخطأ هنا: استخدام enhanced_final_score بدلاً من enhanced_score
|
| 342 |
enhanced_score_display = f"{enhanced_final_score:.3f}" if isinstance(enhanced_final_score, (int, float)) else str(enhanced_final_score)
|
| 343 |
-
# 🔴 --- نهاية الإصلاح --- 🔴
|
| 344 |
|
| 345 |
indicators_summary = format_technical_indicators(advanced_indicators)
|
| 346 |
strategies_summary = format_strategy_scores(strategy_scores, recommended_strategy)
|
| 347 |
whale_analysis_section = format_whale_analysis_for_llm(whale_data)
|
|
|
|
| 348 |
candle_data_section = self._format_candle_data_comprehensive(ohlcv_data)
|
| 349 |
market_context_section = self._format_market_context(sentiment_data)
|
| 350 |
|
| 351 |
-
|
|
|
|
|
|
|
|
|
|
| 352 |
|
| 353 |
prompt = f"""
|
| 354 |
COMPREHENSIVE TRADING ANALYSIS FOR {symbol}
|
|
@@ -356,13 +375,15 @@ COMPREHENSIVE TRADING ANALYSIS FOR {symbol}
|
|
| 356 |
🚨 IMPORTANT SYSTEM CONSTRAINT: This is a SPOT TRADING system ONLY. Decisions MUST be limited to BUY (LONG) or HOLD. SHORT selling is NOT possible.
|
| 357 |
|
| 358 |
🎯 STRATEGY CONTEXT:
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
- Enhanced Score: {enhanced_score_display}
|
| 364 |
|
| 365 |
-
|
|
|
|
|
|
|
|
|
|
| 366 |
|
| 367 |
📊 TECHNICAL INDICATORS (ALL TIMEFRAMES):
|
| 368 |
{indicators_summary}
|
|
@@ -386,145 +407,67 @@ COMPREHENSIVE TRADING ANALYSIS FOR {symbol}
|
|
| 386 |
📋 REASONS FOR SYSTEM CANDIDACY (Layer 1 & 2 Screening):
|
| 387 |
{chr(10).join([f"• {reason}" for reason in reasons]) if reasons else "No specific reasons provided"}
|
| 388 |
|
|
|
|
| 389 |
🎯 TRADING DECISION INSTRUCTIONS (SPOT ONLY - LLM MUST ANALYZE PATTERNS AND DEFINE EXIT STRATEGY):
|
| 390 |
|
| 391 |
-
1. **PERFORM CHART PATTERN ANALYSIS:** Based *ONLY* on the provided 'RAW CANDLE DATA SUMMARY & STATISTICS' section
|
| 392 |
-
2. **INTEGRATE ALL DATA:** Combine YOUR pattern analysis with technicals, strategy analysis, whale activity, market context, news, and the 'LEARNING
|
| 393 |
3. **ADHERE STRICTLY TO SPOT TRADING RULES:** Only decide 'BUY' (LONG) or 'HOLD'. DO NOT suggest 'SELL'.
|
| 394 |
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.
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
- For "SIGNAL_BASED": {{"emergency_volume_spike_multiplier": 5.0}} (Exit if reverse volume spike > 5x average)
|
| 401 |
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.
|
|
|
|
| 402 |
|
| 403 |
-
OUTPUT FORMAT (JSON - SPOT ONLY - INCLUDE EXIT PROFILE):
|
| 404 |
{{
|
| 405 |
"action": "BUY/HOLD",
|
| 406 |
-
"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.",
|
| 407 |
"pattern_identified_by_llm": "Name of the primary pattern(s) identified (e.g., 'Bull Flag on 1H', 'No Clear Pattern')",
|
| 408 |
"pattern_influence": "Explain how the identified pattern(s) influenced the decision.",
|
| 409 |
"risk_assessment": "low/medium/high",
|
| 410 |
|
| 411 |
-
"stop_loss": 0.000000,
|
| 412 |
-
"take_profit": 0.000000,
|
| 413 |
|
| 414 |
-
"exit_profile": "FIXED_TARGET",
|
| 415 |
-
"exit_parameters": {{ "time_stop_minutes": 120 }},
|
| 416 |
|
| 417 |
-
"expected_target_minutes": 15,
|
| 418 |
-
"confidence_level": 0.85,
|
| 419 |
-
"strategy": "{target_strategy}",
|
| 420 |
"whale_influence": "How whale data influenced the BUY/HOLD decision",
|
| 421 |
-
"key_support_level": 0.000000,
|
| 422 |
-
"key_resistance_level": 0.000000,
|
| 423 |
-
"risk_reward_ratio": 2.5
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 424 |
}}
|
| 425 |
"""
|
| 426 |
return prompt
|
|
|
|
| 427 |
|
| 428 |
|
| 429 |
def _format_candle_data_comprehensive(self, ohlcv_data):
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
return "No raw candle data available for analysis"
|
| 433 |
-
|
| 434 |
-
try:
|
| 435 |
-
timeframes_available = []
|
| 436 |
-
total_candles = 0
|
| 437 |
-
|
| 438 |
-
for timeframe, candles in ohlcv_data.items():
|
| 439 |
-
if candles and len(candles) >= 5:
|
| 440 |
-
timeframes_available.append(f"{timeframe.upper()} ({len(candles)} candles)")
|
| 441 |
-
total_candles += len(candles)
|
| 442 |
-
|
| 443 |
-
if not timeframes_available:
|
| 444 |
-
return "Insufficient candle data across all timeframes"
|
| 445 |
-
|
| 446 |
-
summary = f"📊 Available Timeframes: {', '.join(timeframes_available)}\n"
|
| 447 |
-
summary += f"📈 Total Candles Available: {total_candles}\n\n"
|
| 448 |
-
|
| 449 |
-
raw_candle_analysis_text = self.pattern_engine._format_chart_data_for_llm(ohlcv_data)
|
| 450 |
-
|
| 451 |
-
summary += raw_candle_analysis_text
|
| 452 |
-
|
| 453 |
-
return summary
|
| 454 |
-
except Exception as e:
|
| 455 |
-
return f"Error formatting raw candle data: {str(e)}"
|
| 456 |
|
| 457 |
def _analyze_timeframe_candles(self, candles, timeframe):
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
if len(candles) < 10:
|
| 461 |
-
return f"Insufficient data ({len(candles)} candles)"
|
| 462 |
-
|
| 463 |
-
recent_candles = candles[-15:]
|
| 464 |
-
|
| 465 |
-
closes = [c[4] for c in recent_candles]
|
| 466 |
-
opens = [c[1] for c in recent_candles]
|
| 467 |
-
highs = [c[2] for c in recent_candles]
|
| 468 |
-
lows = [c[3] for c in recent_candles]
|
| 469 |
-
volumes = [c[5] for c in recent_candles]
|
| 470 |
-
|
| 471 |
-
current_price = closes[-1]
|
| 472 |
-
first_price = closes[0]
|
| 473 |
-
price_change = ((current_price - first_price) / first_price) * 100 if first_price > 0 else 0
|
| 474 |
-
|
| 475 |
-
if price_change > 2: trend = "🟢 UPTREND"
|
| 476 |
-
elif price_change < -2: trend = "🔴 DOWNTREND"
|
| 477 |
-
else: trend = "⚪ SIDEWAYS"
|
| 478 |
-
|
| 479 |
-
high_max = max(highs)
|
| 480 |
-
low_min = min(lows)
|
| 481 |
-
volatility = ((high_max - low_min) / low_min) * 100 if low_min > 0 else 0
|
| 482 |
-
|
| 483 |
-
avg_volume = sum(volumes) / len(volumes) if volumes else 1
|
| 484 |
-
current_volume = volumes[-1] if volumes else 0
|
| 485 |
-
volume_ratio = current_volume / avg_volume if avg_volume > 0 else 1
|
| 486 |
-
|
| 487 |
-
green_candles = sum(1 for i in range(len(closes)) if closes[i] > opens[i])
|
| 488 |
-
red_candles = len(closes) - green_candles
|
| 489 |
-
candle_ratio = green_candles / len(closes) if closes else 0
|
| 490 |
-
|
| 491 |
-
analysis = [
|
| 492 |
-
f"📈 Trend: {trend} ({price_change:+.2f}%)",
|
| 493 |
-
f"🌊 Volatility: {volatility:.2f}%",
|
| 494 |
-
f"📦 Volume: {volume_ratio:.2f}x average",
|
| 495 |
-
f"🕯️ Candles: {green_candles}🟢/{red_candles}🔴 ({candle_ratio:.1%} green)",
|
| 496 |
-
f"💰 Range: {low_min:.6f} - {high_max:.6f}",
|
| 497 |
-
f"🎯 Current: {current_price:.6f}"
|
| 498 |
-
]
|
| 499 |
-
|
| 500 |
-
return "\n".join(analysis)
|
| 501 |
-
except Exception as e:
|
| 502 |
-
return f"Analysis error: {str(e)}"
|
| 503 |
|
| 504 |
def _format_market_context(self, sentiment_data):
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
return "Market context data not available or incomplete."
|
| 508 |
-
|
| 509 |
-
btc_sentiment = sentiment_data.get('btc_sentiment', 'N/A')
|
| 510 |
-
fear_greed = sentiment_data.get('fear_and_greed_index', 'N/A')
|
| 511 |
-
market_trend = sentiment_data.get('market_trend', 'N/A')
|
| 512 |
-
|
| 513 |
-
lines = [
|
| 514 |
-
f"• Bitcoin Sentiment: {btc_sentiment}",
|
| 515 |
-
f"• Fear & Greed Index: {fear_greed} ({sentiment_data.get('sentiment_class', 'Neutral')})",
|
| 516 |
-
f"• Overall Market Trend: {market_trend.replace('_', ' ').title() if isinstance(market_trend, str) else 'N/A'}"
|
| 517 |
-
]
|
| 518 |
-
|
| 519 |
-
general_whale = sentiment_data.get('general_whale_activity', {})
|
| 520 |
-
if general_whale and general_whale.get('sentiment') != 'NEUTRAL':
|
| 521 |
-
whale_sentiment = general_whale.get('sentiment', 'N/A')
|
| 522 |
-
critical_alert = general_whale.get('critical_alert', False)
|
| 523 |
-
lines.append(f"• General Whale Sentiment: {whale_sentiment.replace('_', ' ').title() if isinstance(whale_sentiment, str) else 'N/A'}")
|
| 524 |
-
if critical_alert:
|
| 525 |
-
lines.append(" ⚠️ CRITICAL WHALE ALERT ACTIVE")
|
| 526 |
-
|
| 527 |
-
return "\n".join(lines)
|
| 528 |
|
| 529 |
|
| 530 |
async def re_analyze_trade_async(self, trade_data: dict, processed_data: dict):
|
|
@@ -534,21 +477,39 @@ OUTPUT FORMAT (JSON - SPOT ONLY - INCLUDE EXIT PROFILE):
|
|
| 534 |
|
| 535 |
ohlcv_data = processed_data.get('raw_ohlcv') or processed_data.get('ohlcv')
|
| 536 |
if not ohlcv_data:
|
| 537 |
-
print(f"⚠️
|
| 538 |
return None
|
| 539 |
|
| 540 |
news_text = await self.news_fetcher.get_news_for_symbol(symbol)
|
| 541 |
pattern_analysis = await self._get_pattern_analysis(processed_data)
|
| 542 |
whale_data = processed_data.get('whale_data', {})
|
| 543 |
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 550 |
|
| 551 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 552 |
|
| 553 |
if self.r2_service:
|
| 554 |
analysis_data = {
|
|
@@ -556,12 +517,12 @@ OUTPUT FORMAT (JSON - SPOT ONLY - INCLUDE EXIT PROFILE):
|
|
| 556 |
'entry_price': trade_data.get('entry_price'),
|
| 557 |
'current_price': processed_data.get('current_price'),
|
| 558 |
'original_strategy': original_strategy,
|
| 559 |
-
'
|
| 560 |
-
'
|
| 561 |
'whale_data_available': whale_data.get('data_available', False)
|
| 562 |
}
|
| 563 |
await self.r2_service.save_llm_prompts_async(
|
| 564 |
-
symbol, '
|
| 565 |
)
|
| 566 |
|
| 567 |
async with self.semaphore:
|
|
@@ -573,11 +534,11 @@ OUTPUT FORMAT (JSON - SPOT ONLY - INCLUDE EXIT PROFILE):
|
|
| 573 |
re_analysis_dict['whale_data_integrated'] = whale_data.get('data_available', False)
|
| 574 |
return re_analysis_dict
|
| 575 |
else:
|
| 576 |
-
print(f"❌
|
| 577 |
return None
|
| 578 |
|
| 579 |
except Exception as e:
|
| 580 |
-
print(f"❌
|
| 581 |
traceback.print_exc()
|
| 582 |
return None
|
| 583 |
|
|
@@ -589,26 +550,25 @@ OUTPUT FORMAT (JSON - SPOT ONLY - INCLUDE EXIT PROFILE):
|
|
| 589 |
|
| 590 |
decision_data = safe_json_parse(json_str)
|
| 591 |
if not decision_data:
|
| 592 |
-
print(f"❌
|
| 593 |
return None
|
| 594 |
|
| 595 |
action = decision_data.get('action')
|
| 596 |
if action not in ['HOLD', 'CLOSE_TRADE', 'UPDATE_TRADE']:
|
| 597 |
-
print(f"⚠️
|
| 598 |
decision_data['action'] = 'HOLD'
|
| 599 |
|
| 600 |
if action == 'UPDATE_TRADE':
|
| 601 |
required_update_fields = ['new_stop_loss', 'new_take_profit', 'new_exit_profile', 'new_exit_parameters']
|
| 602 |
if not validate_required_fields(decision_data, required_update_fields):
|
| 603 |
-
print(f"❌
|
| 604 |
missing = [f for f in required_update_fields if f not in decision_data]
|
| 605 |
print(f" MIA: {missing}")
|
| 606 |
decision_data['action'] = 'HOLD'
|
| 607 |
elif not isinstance(decision_data['new_exit_parameters'], dict):
|
| 608 |
-
print(f"❌
|
| 609 |
decision_data['action'] = 'HOLD'
|
| 610 |
|
| 611 |
-
|
| 612 |
strategy_value = decision_data.get('strategy')
|
| 613 |
if not strategy_value or strategy_value == 'unknown':
|
| 614 |
decision_data['strategy'] = fallback_strategy
|
|
@@ -618,17 +578,29 @@ OUTPUT FORMAT (JSON - SPOT ONLY - INCLUDE EXIT PROFILE):
|
|
| 618 |
print(f"Error parsing re-analysis response for {symbol}: {e}")
|
| 619 |
return None
|
| 620 |
|
| 621 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 622 |
symbol = trade_data.get('symbol', 'N/A')
|
| 623 |
entry_price = trade_data.get('entry_price', 'N/A')
|
| 624 |
current_price = processed_data.get('current_price', 'N/A')
|
| 625 |
strategy = trade_data.get('strategy', 'GENERIC')
|
| 626 |
-
original_trade_type = "LONG" # SPOT only
|
| 627 |
|
| 628 |
current_exit_profile = trade_data.get('decision_data', {}).get('exit_profile', 'N/A')
|
| 629 |
current_exit_params = json.dumps(trade_data.get('decision_data', {}).get('exit_parameters', {}))
|
| 630 |
|
| 631 |
-
|
|
|
|
|
|
|
| 632 |
|
| 633 |
try:
|
| 634 |
price_change = ((current_price - entry_price) / entry_price) * 100 if entry_price else 0
|
|
@@ -650,15 +622,18 @@ TRADE RE-ANALYSIS FOR {symbol} (SPOT ONLY - Currently Open LONG Position)
|
|
| 650 |
🚨 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.
|
| 651 |
|
| 652 |
📊 CURRENT TRADE CONTEXT:
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
|
|
|
|
|
|
|
|
|
| 662 |
|
| 663 |
🔄 UPDATED TECHNICAL ANALYSIS:
|
| 664 |
{indicators_summary}
|
|
@@ -679,51 +654,63 @@ TRADE RE-ANALYSIS FOR {symbol} (SPOT ONLY - Currently Open LONG Position)
|
|
| 679 |
📰 LATEST NEWS:
|
| 680 |
{news_text if news_text else "No significant news found"}
|
| 681 |
|
|
|
|
| 682 |
🎯 RE-ANALYSIS INSTRUCTIONS (SPOT - LONG POSITION):
|
| 683 |
|
| 684 |
-
1. **ANALYZE UPDATED DATA:** Evaluate if the original LONG thesis still holds based on the updated raw candle data
|
| 685 |
-
2. **
|
| 686 |
-
3. **
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
5. **PROVIDE DETAILS:** Justify your decision clearly, integrating all data points.
|
| 693 |
|
| 694 |
CRITICAL: The decision must be one of HOLD, CLOSE_TRADE, or UPDATE_TRADE for the existing LONG position.
|
| 695 |
|
| 696 |
OUTPUT FORMAT (JSON - SPOT RE-ANALYSIS):
|
| 697 |
{{
|
| 698 |
"action": "HOLD/CLOSE_TRADE/UPDATE_TRADE",
|
| 699 |
-
"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.",
|
| 700 |
|
| 701 |
-
"new_stop_loss": 0.000000,
|
| 702 |
-
"new_take_profit": 0.000000,
|
| 703 |
-
"new_exit_profile": "None",
|
| 704 |
-
"new_exit_parameters": {{}},
|
| 705 |
|
| 706 |
-
"new_expected_minutes": 15,
|
| 707 |
-
"confidence_level": 0.85,
|
| 708 |
-
"strategy": "{strategy}",
|
| 709 |
"whale_influence_reanalysis": "How updated whale data influenced the decision",
|
| 710 |
"pattern_influence_reanalysis": "How updated candle patterns AND provided patterns influenced the decision",
|
| 711 |
-
"risk_adjustment": "low/medium/high"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 712 |
}}
|
| 713 |
"""
|
| 714 |
return prompt
|
|
|
|
| 715 |
|
| 716 |
def _format_pattern_analysis(self, pattern_analysis):
|
| 717 |
-
"""
|
| 718 |
if not pattern_analysis or not pattern_analysis.get('pattern_detected') or pattern_analysis.get('pattern_detected') == 'no_clear_pattern':
|
| 719 |
return "No clear chart pattern detected by the system."
|
| 720 |
|
| 721 |
pattern = pattern_analysis.get('pattern_detected', 'N/A')
|
| 722 |
confidence = pattern_analysis.get('pattern_confidence', 0)
|
| 723 |
direction = pattern_analysis.get('predicted_direction', 'N/A')
|
| 724 |
-
timeframe = pattern_analysis.get('timeframe', 'N/A')
|
|
|
|
|
|
|
|
|
|
| 725 |
|
| 726 |
-
return f"System Pattern Analysis: Detected '{pattern}'
|
| 727 |
|
| 728 |
@_rate_limit_nvidia_api
|
| 729 |
async def _call_llm(self, prompt: str) -> str:
|
|
@@ -759,4 +746,4 @@ OUTPUT FORMAT (JSON - SPOT RE-ANALYSIS):
|
|
| 759 |
print(f"❌ Final failure in _call_llm after backoff retries: {e}")
|
| 760 |
raise
|
| 761 |
|
| 762 |
-
print("✅ LLM Service loaded -
|
|
|
|
| 1 |
+
# LLM.py (Updated to integrate LearningHub and English-only prompts)
|
| 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 |
+
# (Note: PatternAnalysisEngine code remains unchanged, so it is omitted here for brevity)
|
| 14 |
+
# ... (PatternAnalysisEngine class code as before) ...
|
| 15 |
class PatternAnalysisEngine:
|
| 16 |
# --- (كود PatternAnalysisEngine كما هو بدون تغيير) ---
|
| 17 |
def __init__(self, llm_service):
|
|
|
|
| 178 |
self.news_fetcher = NewsFetcher()
|
| 179 |
self.pattern_engine = PatternAnalysisEngine(self)
|
| 180 |
self.semaphore = asyncio.Semaphore(5)
|
| 181 |
+
self.r2_service = None # (Set from app.py)
|
| 182 |
+
|
| 183 |
+
# 🔴 --- START OF CHANGE --- 🔴
|
| 184 |
+
# Renamed from self.learning_engine to self.learning_hub
|
| 185 |
+
self.learning_hub = None # (Set from app.py, expects LearningHubManager)
|
| 186 |
+
# 🔴 --- END OF CHANGE --- 🔴
|
| 187 |
|
| 188 |
def _rate_limit_nvidia_api(func):
|
| 189 |
@wraps(func)
|
|
|
|
| 199 |
|
| 200 |
ohlcv_data = data_payload.get('raw_ohlcv') or data_payload.get('ohlcv')
|
| 201 |
if not ohlcv_data:
|
| 202 |
+
print(f"⚠️ No candle data for {symbol} - skipping analysis")
|
| 203 |
return None
|
| 204 |
|
| 205 |
total_candles = sum(len(data) for data in ohlcv_data.values() if data) if ohlcv_data else 0
|
| 206 |
timeframes_count = len([tf for tf, data in ohlcv_data.items() if data and len(data) >= 10]) if ohlcv_data else 0
|
| 207 |
|
|
|
|
|
|
|
|
|
|
| 208 |
if total_candles < 30:
|
| 209 |
+
print(f" ⚠️ Insufficient candle data for {symbol}: {total_candles} candles")
|
| 210 |
return None
|
| 211 |
|
| 212 |
valid_timeframes = [tf for tf, candles in ohlcv_data.items() if candles and len(candles) >= 5]
|
| 213 |
if not valid_timeframes:
|
| 214 |
+
print(f" ⚠️ No valid timeframes for {symbol}")
|
| 215 |
return None
|
|
|
|
| 216 |
|
| 217 |
news_text = await self.news_fetcher.get_news_for_symbol(symbol)
|
| 218 |
whale_data = data_payload.get('whale_data', {})
|
| 219 |
|
| 220 |
+
# 🔴 --- START OF CHANGE --- 🔴
|
| 221 |
+
# (Fetch learning context from the new hub)
|
| 222 |
+
statistical_feedback = "No statistical learning data yet."
|
| 223 |
+
active_context_playbook = "No active learning rules available."
|
|
|
|
|
|
|
| 224 |
|
| 225 |
+
if self.learning_hub and self.learning_hub.initialized:
|
| 226 |
+
# 1. Get Statistical Feedback (Slow-learner)
|
| 227 |
+
statistical_feedback = await self.learning_hub.get_statistical_feedback_for_llm(target_strategy)
|
| 228 |
+
|
| 229 |
+
# 2. Get Active Context / Deltas (Fast-learner)
|
| 230 |
+
active_context_playbook = await self.learning_hub.get_active_context_for_llm(
|
| 231 |
+
domain="strategy",
|
| 232 |
+
query=f"{target_strategy} {symbol}" # (Query with strategy and symbol)
|
| 233 |
+
)
|
| 234 |
|
| 235 |
+
# (Pass new context to the prompt creator)
|
| 236 |
+
prompt = self._create_comprehensive_trading_prompt(
|
| 237 |
+
data_payload,
|
| 238 |
+
news_text,
|
| 239 |
+
None,
|
| 240 |
+
whale_data,
|
| 241 |
+
statistical_feedback,
|
| 242 |
+
active_context_playbook
|
| 243 |
+
)
|
| 244 |
+
# 🔴 --- END OF CHANGE --- 🔴
|
| 245 |
+
|
| 246 |
if self.r2_service:
|
| 247 |
analysis_data = {
|
| 248 |
'symbol': symbol,
|
| 249 |
'current_price': data_payload.get('current_price'),
|
|
|
|
| 250 |
'enhanced_final_score': data_payload.get('enhanced_final_score'),
|
| 251 |
'target_strategy': target_strategy,
|
| 252 |
+
'statistical_feedback': statistical_feedback,
|
| 253 |
+
'active_context_playbook': active_context_playbook,
|
| 254 |
'whale_data_available': whale_data.get('data_available', False),
|
| 255 |
'total_candles': total_candles,
|
| 256 |
'timeframes_count': timeframes_count,
|
|
|
|
| 257 |
'timestamp': datetime.now().isoformat()
|
| 258 |
}
|
| 259 |
await self.r2_service.save_llm_prompts_async(
|
| 260 |
+
symbol, 'comprehensive_trading_decision_v3_hub', prompt, analysis_data
|
| 261 |
)
|
| 262 |
|
| 263 |
async with self.semaphore:
|
|
|
|
| 271 |
decision_dict['timeframes_analyzed'] = timeframes_count
|
| 272 |
return decision_dict
|
| 273 |
else:
|
| 274 |
+
print(f"❌ LLM parsing failed for {symbol} - no fallback decisions")
|
| 275 |
return None
|
| 276 |
|
| 277 |
except Exception as e:
|
| 278 |
+
print(f"❌ Error in get_trading_decision for {data_payload.get('symbol', 'unknown')}: {e}")
|
| 279 |
traceback.print_exc()
|
| 280 |
return None
|
| 281 |
|
|
|
|
| 283 |
try:
|
| 284 |
json_str = parse_json_from_response(response_text)
|
| 285 |
if not json_str:
|
| 286 |
+
print(f"❌ Failed to extract JSON from LLM response for {symbol}")
|
| 287 |
return None
|
| 288 |
|
| 289 |
decision_data = safe_json_parse(json_str)
|
| 290 |
if not decision_data:
|
| 291 |
+
print(f"❌ Failed to parse JSON (safe_json_parse) for {symbol}: {response_text}")
|
| 292 |
return None
|
| 293 |
|
| 294 |
+
# (This check is for the trading decision, not the reflector response)
|
| 295 |
+
if fallback_strategy == "reflection" or fallback_strategy == "distillation":
|
| 296 |
+
# (If this is a reflector/curator call, just return the data)
|
| 297 |
+
return decision_data
|
| 298 |
+
|
| 299 |
required_fields = [
|
| 300 |
'action', 'reasoning', 'risk_assessment', 'stop_loss', 'take_profit',
|
| 301 |
'expected_target_minutes', 'confidence_level', 'pattern_identified_by_llm',
|
| 302 |
'exit_profile', 'exit_parameters'
|
| 303 |
]
|
| 304 |
if not validate_required_fields(decision_data, required_fields):
|
| 305 |
+
print(f"❌ Missing required fields in LLM response for {symbol}")
|
| 306 |
missing = [f for f in required_fields if f not in decision_data]
|
| 307 |
print(f" MIA: {missing}")
|
| 308 |
return None
|
| 309 |
|
| 310 |
if not isinstance(decision_data['exit_parameters'], dict):
|
| 311 |
+
print(f"❌ 'exit_parameters' is not a valid dict for {symbol}")
|
| 312 |
return None
|
| 313 |
|
| 314 |
action = decision_data.get('action')
|
| 315 |
if action not in ['BUY', 'HOLD']:
|
| 316 |
+
print(f"⚠️ LLM suggested unsupported action ({action}) for {symbol}. Forcing HOLD.")
|
| 317 |
decision_data['action'] = 'HOLD'
|
| 318 |
|
| 319 |
if decision_data['action'] == 'BUY':
|
|
|
|
| 327 |
|
| 328 |
return decision_data
|
| 329 |
except Exception as e:
|
| 330 |
+
print(f"❌ Error parsing LLM response for {symbol}: {e}")
|
| 331 |
return None
|
| 332 |
|
| 333 |
async def _get_pattern_analysis(self, data_payload):
|
| 334 |
+
# (Omitted for brevity - same as original file)
|
| 335 |
+
pass
|
| 336 |
+
|
| 337 |
+
# 🔴 --- START OF PROMPT CHANGE --- 🔴
|
| 338 |
+
def _create_comprehensive_trading_prompt(
|
| 339 |
+
self,
|
| 340 |
+
payload: dict,
|
| 341 |
+
news_text: str,
|
| 342 |
+
pattern_analysis: dict, # (This is the old system, now deprecated, but we leave the arg)
|
| 343 |
+
whale_data: dict,
|
| 344 |
+
statistical_feedback: str, # (NEW from Hub)
|
| 345 |
+
active_context_playbook: str # (NEW from Hub)
|
| 346 |
+
) -> str:
|
| 347 |
+
|
| 348 |
symbol = payload.get('symbol', 'N/A')
|
| 349 |
current_price = payload.get('current_price', 'N/A')
|
| 350 |
reasons = payload.get('reasons_for_candidacy', [])
|
|
|
|
| 353 |
strategy_scores = payload.get('strategy_scores', {})
|
| 354 |
recommended_strategy = payload.get('recommended_strategy', 'N/A')
|
| 355 |
target_strategy = payload.get('target_strategy', 'GENERIC')
|
| 356 |
+
enhanced_final_score = payload.get('enhanced_final_score', 0)
|
|
|
|
|
|
|
| 357 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
enhanced_score_display = f"{enhanced_final_score:.3f}" if isinstance(enhanced_final_score, (int, float)) else str(enhanced_final_score)
|
|
|
|
| 359 |
|
| 360 |
indicators_summary = format_technical_indicators(advanced_indicators)
|
| 361 |
strategies_summary = format_strategy_scores(strategy_scores, recommended_strategy)
|
| 362 |
whale_analysis_section = format_whale_analysis_for_llm(whale_data)
|
| 363 |
+
ohlcv_data = payload.get('raw_ohlcv') or payload.get('ohlcv', {})
|
| 364 |
candle_data_section = self._format_candle_data_comprehensive(ohlcv_data)
|
| 365 |
market_context_section = self._format_market_context(sentiment_data)
|
| 366 |
|
| 367 |
+
# (New sections from the Learning Hub)
|
| 368 |
+
statistical_feedback_section = f"🧠 STATISTICAL FEEDBACK (Slow-Learner):\n{statistical_feedback}"
|
| 369 |
+
playbook_section = f"📚 LEARNING PLAYBOOK (Fast-Learner Active Rules):\n{active_context_playbook}"
|
| 370 |
+
|
| 371 |
|
| 372 |
prompt = f"""
|
| 373 |
COMPREHENSIVE TRADING ANALYSIS FOR {symbol}
|
|
|
|
| 375 |
🚨 IMPORTANT SYSTEM CONSTRAINT: This is a SPOT TRADING system ONLY. Decisions MUST be limited to BUY (LONG) or HOLD. SHORT selling is NOT possible.
|
| 376 |
|
| 377 |
🎯 STRATEGY CONTEXT:
|
| 378 |
+
* Target Strategy: {target_strategy}
|
| 379 |
+
* Recommended Strategy: {recommended_strategy}
|
| 380 |
+
* Current Price: ${current_price}
|
| 381 |
+
* Enhanced System Score: {enhanced_score_display}
|
|
|
|
| 382 |
|
| 383 |
+
--- LEARNING HUB INPUT (CRITICAL) ---
|
| 384 |
+
{playbook_section}
|
| 385 |
+
{statistical_feedback_section}
|
| 386 |
+
--- END OF LEARNING INPUT ---
|
| 387 |
|
| 388 |
📊 TECHNICAL INDICATORS (ALL TIMEFRAMES):
|
| 389 |
{indicators_summary}
|
|
|
|
| 407 |
📋 REASONS FOR SYSTEM CANDIDACY (Layer 1 & 2 Screening):
|
| 408 |
{chr(10).join([f"• {reason}" for reason in reasons]) if reasons else "No specific reasons provided"}
|
| 409 |
|
| 410 |
+
---
|
| 411 |
🎯 TRADING DECISION INSTRUCTIONS (SPOT ONLY - LLM MUST ANALYZE PATTERNS AND DEFINE EXIT STRATEGY):
|
| 412 |
|
| 413 |
+
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.).
|
| 414 |
+
2. **INTEGRATE ALL DATA:** Combine YOUR pattern analysis with technicals, strategy analysis, whale activity, market context, news, and (most importantly) the 'LEARNING HUB INPUT'.
|
| 415 |
3. **ADHERE STRICTLY TO SPOT TRADING RULES:** Only decide 'BUY' (LONG) or 'HOLD'. DO NOT suggest 'SELL'.
|
| 416 |
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.
|
| 417 |
+
* `"exit_profile"`: Choose one: "ATR_TRAILING" (Recommended for trends/breakouts), "FIXED_TARGET" (Recommended for mean reversion/scalping), "TIME_BASED" (Exit after X minutes regardless).
|
| 418 |
+
* `"exit_parameters"`: Define parameters for the chosen profile, respecting the 'Statistical Feedback'.
|
| 419 |
+
* 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%)
|
| 420 |
+
* For "FIXED_TARGET": {{"time_stop_minutes": 120}} (Hard stop if target not hit in 120 mins)
|
| 421 |
+
* For "TIME_BASED": {{"exit_after_minutes": 60}}
|
|
|
|
| 422 |
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.
|
| 423 |
+
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.
|
| 424 |
|
| 425 |
+
OUTPUT FORMAT (JSON - SPOT ONLY - INCLUDE EXIT PROFILE AND SELF-CRITIQUE):
|
| 426 |
{{
|
| 427 |
"action": "BUY/HOLD",
|
| 428 |
+
"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.",
|
| 429 |
"pattern_identified_by_llm": "Name of the primary pattern(s) identified (e.g., 'Bull Flag on 1H', 'No Clear Pattern')",
|
| 430 |
"pattern_influence": "Explain how the identified pattern(s) influenced the decision.",
|
| 431 |
"risk_assessment": "low/medium/high",
|
| 432 |
|
| 433 |
+
"stop_loss": 0.000000,
|
| 434 |
+
"take_profit": 0.000000,
|
| 435 |
|
| 436 |
+
"exit_profile": "FIXED_TARGET",
|
| 437 |
+
"exit_parameters": {{ "time_stop_minutes": 120 }},
|
| 438 |
|
| 439 |
+
"expected_target_minutes": 15,
|
| 440 |
+
"confidence_level": 0.85,
|
| 441 |
+
"strategy": "{target_strategy}",
|
| 442 |
"whale_influence": "How whale data influenced the BUY/HOLD decision",
|
| 443 |
+
"key_support_level": 0.000000,
|
| 444 |
+
"key_resistance_level": 0.000000,
|
| 445 |
+
"risk_reward_ratio": 2.5,
|
| 446 |
+
|
| 447 |
+
"self_critique": {{
|
| 448 |
+
"failure_modes": [
|
| 449 |
+
"What is the first reason this decision could fail? (e.g., 'The identified pattern is a false breakout.')",
|
| 450 |
+
"What is the second reason? (e.g., 'Whale data shows distribution, contradicting the technicals.')"
|
| 451 |
+
],
|
| 452 |
+
"confidence_adjustment_reason": "Brief reason if confidence was adjusted post-critique."
|
| 453 |
+
}}
|
| 454 |
}}
|
| 455 |
"""
|
| 456 |
return prompt
|
| 457 |
+
# 🔴 --- END OF PROMPT CHANGE --- 🔴
|
| 458 |
|
| 459 |
|
| 460 |
def _format_candle_data_comprehensive(self, ohlcv_data):
|
| 461 |
+
# (Omitted for brevity - same as original file)
|
| 462 |
+
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 463 |
|
| 464 |
def _analyze_timeframe_candles(self, candles, timeframe):
|
| 465 |
+
# (Omitted for brevity - same as original file)
|
| 466 |
+
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 467 |
|
| 468 |
def _format_market_context(self, sentiment_data):
|
| 469 |
+
# (Omitted for brevity - same as original file)
|
| 470 |
+
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 471 |
|
| 472 |
|
| 473 |
async def re_analyze_trade_async(self, trade_data: dict, processed_data: dict):
|
|
|
|
| 477 |
|
| 478 |
ohlcv_data = processed_data.get('raw_ohlcv') or processed_data.get('ohlcv')
|
| 479 |
if not ohlcv_data:
|
| 480 |
+
print(f"⚠️ No updated candle data for {symbol} - skipping re-analysis")
|
| 481 |
return None
|
| 482 |
|
| 483 |
news_text = await self.news_fetcher.get_news_for_symbol(symbol)
|
| 484 |
pattern_analysis = await self._get_pattern_analysis(processed_data)
|
| 485 |
whale_data = processed_data.get('whale_data', {})
|
| 486 |
|
| 487 |
+
# 🔴 --- START OF CHANGE --- 🔴
|
| 488 |
+
# (Fetch learning context from the new hub for re-analysis)
|
| 489 |
+
statistical_feedback = "No statistical learning data yet."
|
| 490 |
+
active_context_playbook = "No active learning rules available."
|
| 491 |
+
|
| 492 |
+
if self.learning_hub and self.learning_hub.initialized:
|
| 493 |
+
# 1. Get Statistical Feedback (Slow-learner)
|
| 494 |
+
statistical_feedback = await self.learning_hub.get_statistical_feedback_for_llm(original_strategy)
|
| 495 |
+
|
| 496 |
+
# 2. Get Active Context / Deltas (Fast-learner)
|
| 497 |
+
active_context_playbook = await self.learning_hub.get_active_context_for_llm(
|
| 498 |
+
domain="strategy",
|
| 499 |
+
query=f"{original_strategy} {symbol} re-analysis"
|
| 500 |
+
)
|
| 501 |
|
| 502 |
+
# (Pass new context to the prompt creator)
|
| 503 |
+
prompt = self._create_re_analysis_prompt(
|
| 504 |
+
trade_data,
|
| 505 |
+
processed_data,
|
| 506 |
+
news_text,
|
| 507 |
+
pattern_analysis,
|
| 508 |
+
whale_data,
|
| 509 |
+
statistical_feedback,
|
| 510 |
+
active_context_playbook
|
| 511 |
+
)
|
| 512 |
+
# 🔴 --- END OF CHANGE --- 🔴
|
| 513 |
|
| 514 |
if self.r2_service:
|
| 515 |
analysis_data = {
|
|
|
|
| 517 |
'entry_price': trade_data.get('entry_price'),
|
| 518 |
'current_price': processed_data.get('current_price'),
|
| 519 |
'original_strategy': original_strategy,
|
| 520 |
+
'statistical_feedback': statistical_feedback,
|
| 521 |
+
'active_context_playbook': active_context_playbook,
|
| 522 |
'whale_data_available': whale_data.get('data_available', False)
|
| 523 |
}
|
| 524 |
await self.r2_service.save_llm_prompts_async(
|
| 525 |
+
symbol, 'trade_reanalysis_v3_hub', prompt, analysis_data
|
| 526 |
)
|
| 527 |
|
| 528 |
async with self.semaphore:
|
|
|
|
| 534 |
re_analysis_dict['whale_data_integrated'] = whale_data.get('data_available', False)
|
| 535 |
return re_analysis_dict
|
| 536 |
else:
|
| 537 |
+
print(f"❌ LLM re-analysis parsing failed for {symbol}")
|
| 538 |
return None
|
| 539 |
|
| 540 |
except Exception as e:
|
| 541 |
+
print(f"❌ Error in LLM re-analysis: {e}")
|
| 542 |
traceback.print_exc()
|
| 543 |
return None
|
| 544 |
|
|
|
|
| 550 |
|
| 551 |
decision_data = safe_json_parse(json_str)
|
| 552 |
if not decision_data:
|
| 553 |
+
print(f"❌ Failed to parse JSON (safe_json_parse) for re-analysis of {symbol}: {response_text}")
|
| 554 |
return None
|
| 555 |
|
| 556 |
action = decision_data.get('action')
|
| 557 |
if action not in ['HOLD', 'CLOSE_TRADE', 'UPDATE_TRADE']:
|
| 558 |
+
print(f"⚠️ LLM suggested unsupported re-analysis action ({action}) for {symbol}. Forcing HOLD.")
|
| 559 |
decision_data['action'] = 'HOLD'
|
| 560 |
|
| 561 |
if action == 'UPDATE_TRADE':
|
| 562 |
required_update_fields = ['new_stop_loss', 'new_take_profit', 'new_exit_profile', 'new_exit_parameters']
|
| 563 |
if not validate_required_fields(decision_data, required_update_fields):
|
| 564 |
+
print(f"❌ Missing required fields for UPDATE_TRADE for {symbol}")
|
| 565 |
missing = [f for f in required_update_fields if f not in decision_data]
|
| 566 |
print(f" MIA: {missing}")
|
| 567 |
decision_data['action'] = 'HOLD'
|
| 568 |
elif not isinstance(decision_data['new_exit_parameters'], dict):
|
| 569 |
+
print(f"❌ 'new_exit_parameters' is not a valid dict for {symbol}")
|
| 570 |
decision_data['action'] = 'HOLD'
|
| 571 |
|
|
|
|
| 572 |
strategy_value = decision_data.get('strategy')
|
| 573 |
if not strategy_value or strategy_value == 'unknown':
|
| 574 |
decision_data['strategy'] = fallback_strategy
|
|
|
|
| 578 |
print(f"Error parsing re-analysis response for {symbol}: {e}")
|
| 579 |
return None
|
| 580 |
|
| 581 |
+
# 🔴 --- START OF PROMPT CHANGE --- 🔴
|
| 582 |
+
def _create_re_analysis_prompt(
|
| 583 |
+
self,
|
| 584 |
+
trade_data: dict,
|
| 585 |
+
processed_data: dict,
|
| 586 |
+
news_text: str,
|
| 587 |
+
pattern_analysis: dict,
|
| 588 |
+
whale_data: dict,
|
| 589 |
+
statistical_feedback: str, # (NEW from Hub)
|
| 590 |
+
active_context_playbook: str # (NEW from Hub)
|
| 591 |
+
) -> str:
|
| 592 |
+
|
| 593 |
symbol = trade_data.get('symbol', 'N/A')
|
| 594 |
entry_price = trade_data.get('entry_price', 'N/A')
|
| 595 |
current_price = processed_data.get('current_price', 'N/A')
|
| 596 |
strategy = trade_data.get('strategy', 'GENERIC')
|
|
|
|
| 597 |
|
| 598 |
current_exit_profile = trade_data.get('decision_data', {}).get('exit_profile', 'N/A')
|
| 599 |
current_exit_params = json.dumps(trade_data.get('decision_data', {}).get('exit_parameters', {}))
|
| 600 |
|
| 601 |
+
# (New sections from the Learning Hub)
|
| 602 |
+
statistical_feedback_section = f"🧠 STATISTICAL FEEDBACK (Slow-Learner):\n{statistical_feedback}"
|
| 603 |
+
playbook_section = f"📚 LEARNING PLAYBOOK (Fast-Learner Active Rules):\n{active_context_playbook}"
|
| 604 |
|
| 605 |
try:
|
| 606 |
price_change = ((current_price - entry_price) / entry_price) * 100 if entry_price else 0
|
|
|
|
| 622 |
🚨 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.
|
| 623 |
|
| 624 |
📊 CURRENT TRADE CONTEXT:
|
| 625 |
+
* Strategy: {strategy}
|
| 626 |
+
* Entry Price: {entry_price} (LONG position)
|
| 627 |
+
* Current Price: {current_price}
|
| 628 |
+
* Current Performance: {price_change_display}
|
| 629 |
+
* Trade Age: {trade_data.get('hold_duration_minutes', 'N/A')} minutes
|
| 630 |
+
* Current Exit Profile: {current_exit_profile}
|
| 631 |
+
* Current Exit Parameters: {current_exit_params}
|
| 632 |
+
|
| 633 |
+
--- LEARNING HUB INPUT (CRITICAL) ---
|
| 634 |
+
{playbook_section}
|
| 635 |
+
{statistical_feedback_section}
|
| 636 |
+
--- END OF LEARNING INPUT ---
|
| 637 |
|
| 638 |
🔄 UPDATED TECHNICAL ANALYSIS:
|
| 639 |
{indicators_summary}
|
|
|
|
| 654 |
📰 LATEST NEWS:
|
| 655 |
{news_text if news_text else "No significant news found"}
|
| 656 |
|
| 657 |
+
---
|
| 658 |
🎯 RE-ANALYSIS INSTRUCTIONS (SPOT - LONG POSITION):
|
| 659 |
|
| 660 |
+
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'.
|
| 661 |
+
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).
|
| 662 |
+
3. **IF UPDATING (CRITICAL):** If action is UPDATE_TRADE, you MUST provide:
|
| 663 |
+
* `new_stop_loss` (New hard stop)
|
| 664 |
+
* `new_take_profit` (New final target)
|
| 665 |
+
* `new_exit_profile`: (e.g., "ATR_TRAILING") - Can be the same or different.
|
| 666 |
+
* `new_exit_parameters`: (e.g., {{"atr_multiplier": 1.5}}) - Must match the new profile.
|
| 667 |
+
4. **SELF-CRITIQUE:** Perform a self-critique. What is the risk of this re-analysis decision?
|
|
|
|
| 668 |
|
| 669 |
CRITICAL: The decision must be one of HOLD, CLOSE_TRADE, or UPDATE_TRADE for the existing LONG position.
|
| 670 |
|
| 671 |
OUTPUT FORMAT (JSON - SPOT RE-ANALYSIS):
|
| 672 |
{{
|
| 673 |
"action": "HOLD/CLOSE_TRADE/UPDATE_TRADE",
|
| 674 |
+
"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.",
|
| 675 |
|
| 676 |
+
"new_stop_loss": 0.000000,
|
| 677 |
+
"new_take_profit": 0.000000,
|
| 678 |
+
"new_exit_profile": "None",
|
| 679 |
+
"new_exit_parameters": {{}},
|
| 680 |
|
| 681 |
+
"new_expected_minutes": 15,
|
| 682 |
+
"confidence_level": 0.85,
|
| 683 |
+
"strategy": "{strategy}",
|
| 684 |
"whale_influence_reanalysis": "How updated whale data influenced the decision",
|
| 685 |
"pattern_influence_reanalysis": "How updated candle patterns AND provided patterns influenced the decision",
|
| 686 |
+
"risk_adjustment": "low/medium/high",
|
| 687 |
+
|
| 688 |
+
"self_critique": {{
|
| 689 |
+
"failure_modes": [
|
| 690 |
+
"What is the primary risk of this new decision? (e.g., 'Holding this position increases exposure to market volatility.')",
|
| 691 |
+
"What is the second risk? (e.g., 'Closing now might miss a future rebound.')"
|
| 692 |
+
],
|
| 693 |
+
"confidence_adjustment_reason": "Brief reason if confidence was adjusted post-critique."
|
| 694 |
+
}}
|
| 695 |
}}
|
| 696 |
"""
|
| 697 |
return prompt
|
| 698 |
+
# 🔴 --- END OF PROMPT CHANGE --- 🔴
|
| 699 |
|
| 700 |
def _format_pattern_analysis(self, pattern_analysis):
|
| 701 |
+
"""Helper to format pattern analysis for the LLM"""
|
| 702 |
if not pattern_analysis or not pattern_analysis.get('pattern_detected') or pattern_analysis.get('pattern_detected') == 'no_clear_pattern':
|
| 703 |
return "No clear chart pattern detected by the system."
|
| 704 |
|
| 705 |
pattern = pattern_analysis.get('pattern_detected', 'N/A')
|
| 706 |
confidence = pattern_analysis.get('pattern_confidence', 0)
|
| 707 |
direction = pattern_analysis.get('predicted_direction', 'N/A')
|
| 708 |
+
timeframe = pattern_analysis.get('timeframe', 'N/A') # (This key might not exist, need to check patterns.py)
|
| 709 |
+
|
| 710 |
+
# (Assuming timeframe is part of the top-level analysis)
|
| 711 |
+
tf_display = f"on {timeframe} timeframe" if timeframe != 'N/A' else ""
|
| 712 |
|
| 713 |
+
return f"System Pattern Analysis: Detected '{pattern}' {tf_display} with {confidence:.2f} confidence. Predicted direction: {direction}."
|
| 714 |
|
| 715 |
@_rate_limit_nvidia_api
|
| 716 |
async def _call_llm(self, prompt: str) -> str:
|
|
|
|
| 746 |
print(f"❌ Final failure in _call_llm after backoff retries: {e}")
|
| 747 |
raise
|
| 748 |
|
| 749 |
+
print("✅ LLM Service loaded - V3 (Integrated Learning Hub, English-only Prompts, Self-Critique)")
|