|
|
|
|
|
import os, traceback, asyncio, json, time |
|
|
import re |
|
|
from datetime import datetime |
|
|
from functools import wraps |
|
|
from backoff import on_exception, expo |
|
|
from openai import OpenAI, RateLimitError, APITimeoutError |
|
|
import numpy as np |
|
|
from sentiment_news import NewsFetcher |
|
|
from helpers import validate_required_fields, format_technical_indicators, format_strategy_scores, format_candle_data_for_pattern_analysis, format_whale_analysis_for_llm, parse_json_from_response |
|
|
from ml_engine.processor import safe_json_parse |
|
|
|
|
|
NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY") |
|
|
PRIMARY_MODEL = "nvidia/llama-3.1-nemotron-ultra-253b-v1" |
|
|
|
|
|
class PatternAnalysisEngine: |
|
|
|
|
|
def __init__(self, llm_service): |
|
|
self.llm = llm_service |
|
|
|
|
|
def _format_chart_data_for_llm(self, ohlcv_data): |
|
|
"""تنسيق شامل لبيانات الشموع الخام لتحليل الأنماط""" |
|
|
if not ohlcv_data: |
|
|
return "Insufficient chart data for pattern analysis" |
|
|
|
|
|
try: |
|
|
|
|
|
all_timeframes = [] |
|
|
for timeframe, candles in ohlcv_data.items(): |
|
|
if candles and len(candles) >= 10: |
|
|
|
|
|
raw_candle_summary = self._format_raw_candle_data(candles, timeframe) |
|
|
all_timeframes.append(f"=== {timeframe.upper()} TIMEFRAME ({len(candles)} CANDLES) ===\n{raw_candle_summary}") |
|
|
|
|
|
return "\n\n".join(all_timeframes) if all_timeframes else "No sufficient timeframe data available" |
|
|
except Exception as e: |
|
|
return f"Error formatting chart data: {str(e)}" |
|
|
|
|
|
def _format_raw_candle_data(self, candles, timeframe): |
|
|
"""تنسيق بيانات الشموع الخام بشكل مفصل للنموذج""" |
|
|
try: |
|
|
if len(candles) < 10: |
|
|
return f"Only {len(candles)} candles available - insufficient for deep pattern analysis" |
|
|
|
|
|
|
|
|
analysis_candles = candles[-50:] if len(candles) > 50 else candles |
|
|
|
|
|
summary = [] |
|
|
summary.append(f"Total candles: {len(candles)} (showing last {len(analysis_candles)})") |
|
|
summary.append("Recent candles (newest to oldest):") |
|
|
|
|
|
|
|
|
for i in range(min(15, len(analysis_candles))): |
|
|
idx = len(analysis_candles) - 1 - i |
|
|
candle = analysis_candles[idx] |
|
|
|
|
|
|
|
|
try: |
|
|
timestamp = datetime.fromtimestamp(candle[0] / 1000).strftime('%Y-%m-%d %H:%M:%S') |
|
|
except: |
|
|
timestamp = "unknown" |
|
|
|
|
|
open_price, high, low, close, volume = candle[1], candle[2], candle[3], candle[4], candle[5] |
|
|
|
|
|
candle_type = "🟢 BULLISH" if close > open_price else "🔴 BEARISH" if close < open_price else "⚪ NEUTRAL" |
|
|
body_size = abs(close - open_price) |
|
|
body_percent = (body_size / open_price * 100) if open_price > 0 else 0 |
|
|
|
|
|
wick_upper = high - max(open_price, close) |
|
|
wick_lower = min(open_price, close) - low |
|
|
total_range = high - low |
|
|
|
|
|
if total_range > 0: |
|
|
body_ratio = (body_size / total_range) * 100 |
|
|
upper_wick_ratio = (wick_upper / total_range) * 100 |
|
|
lower_wick_ratio = (wick_lower / total_range) * 100 |
|
|
else: |
|
|
body_ratio = upper_wick_ratio = lower_wick_ratio = 0 |
|
|
|
|
|
summary.append(f"{i+1:2d}. {timestamp} | {candle_type}") |
|
|
summary.append(f" O:{open_price:.8f} H:{high:.8f} L:{low:.8f} C:{close:.8f}") |
|
|
summary.append(f" Body: {body_percent:.2f}% | Body/Range: {body_ratio:.1f}%") |
|
|
summary.append(f" Wicks: Upper {upper_wick_ratio:.1f}% / Lower {lower_wick_ratio:.1f}%") |
|
|
summary.append(f" Volume: {volume:,.0f}") |
|
|
|
|
|
|
|
|
if len(analysis_candles) >= 20: |
|
|
stats = self._calculate_candle_statistics(analysis_candles) |
|
|
summary.append(f"\n📊 STATISTICAL ANALYSIS:") |
|
|
summary.append(f"• Price Change: {stats['price_change']:+.2f}%") |
|
|
summary.append(f"• Average Body Size: {stats['avg_body']:.4f}%") |
|
|
summary.append(f"• Volatility (ATR): {stats['atr']:.6f}") |
|
|
summary.append(f"• Trend: {stats['trend']}") |
|
|
summary.append(f"• Support: {stats['support']:.6f}") |
|
|
summary.append(f"• Resistance: {stats['resistance']:.6f}") |
|
|
|
|
|
return "\n".join(summary) |
|
|
|
|
|
except Exception as e: |
|
|
return f"Error formatting raw candle data: {str(e)}" |
|
|
|
|
|
def _calculate_candle_statistics(self, candles): |
|
|
"""حساب الإحصائيات الأساسية للشموع""" |
|
|
try: |
|
|
closes = [c[4] for c in candles] |
|
|
opens = [c[1] for c in candles] |
|
|
highs = [c[2] for c in candles] |
|
|
lows = [c[3] for c in candles] |
|
|
|
|
|
|
|
|
first_close = closes[0] |
|
|
last_close = closes[-1] |
|
|
price_change = ((last_close - first_close) / first_close) * 100 |
|
|
|
|
|
|
|
|
body_sizes = [abs(close - open) for open, close in zip(opens, closes)] |
|
|
avg_body = (sum(body_sizes) / len(body_sizes)) / first_close * 100 if first_close > 0 else 0 |
|
|
|
|
|
|
|
|
true_ranges = [] |
|
|
for i in range(1, len(candles)): |
|
|
high, low, prev_close = highs[i], lows[i], closes[i-1] |
|
|
tr1 = high - low |
|
|
tr2 = abs(high - prev_close) |
|
|
tr3 = abs(low - prev_close) |
|
|
true_ranges.append(max(tr1, tr2, tr3)) |
|
|
|
|
|
atr = sum(true_ranges) / len(true_ranges) if true_ranges else 0 |
|
|
|
|
|
|
|
|
if price_change > 3: |
|
|
trend = "STRONG UPTREND" |
|
|
elif price_change > 1: |
|
|
trend = "UPTREND" |
|
|
elif price_change < -3: |
|
|
trend = "STRONG DOWNTREND" |
|
|
elif price_change < -1: |
|
|
trend = "DOWNTREND" |
|
|
else: |
|
|
trend = "SIDEWAYS" |
|
|
|
|
|
|
|
|
support = min(lows) |
|
|
resistance = max(highs) |
|
|
|
|
|
return { |
|
|
'price_change': price_change, |
|
|
'avg_body': avg_body, |
|
|
'atr': atr, |
|
|
'trend': trend, |
|
|
'support': support, |
|
|
'resistance': resistance |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
|
|
|
return { |
|
|
'price_change': 0, |
|
|
'avg_body': 0, |
|
|
'atr': 0, |
|
|
'trend': 'UNKNOWN', |
|
|
'support': 0, |
|
|
'resistance': 0 |
|
|
} |
|
|
|
|
|
async def analyze_chart_patterns(self, symbol, ohlcv_data): |
|
|
pass |
|
|
|
|
|
def _parse_pattern_response(self, response_text): |
|
|
pass |
|
|
|
|
|
|
|
|
class LLMService: |
|
|
def __init__(self, api_key=NVIDIA_API_KEY, model_name=PRIMARY_MODEL, temperature=0.7): |
|
|
self.api_key = api_key |
|
|
self.model_name = model_name |
|
|
self.temperature = temperature |
|
|
self.client = OpenAI(base_url="https://integrate.api.nvidia.com/v1", api_key=self.api_key) |
|
|
self.news_fetcher = NewsFetcher() |
|
|
self.pattern_engine = PatternAnalysisEngine(self) |
|
|
self.semaphore = asyncio.Semaphore(5) |
|
|
self.r2_service = None |
|
|
self.learning_engine = None |
|
|
|
|
|
def _rate_limit_nvidia_api(func): |
|
|
@wraps(func) |
|
|
@on_exception(expo, RateLimitError, max_tries=5) |
|
|
async def wrapper(*args, **kwargs): |
|
|
return await func(*args, **kwargs) |
|
|
return wrapper |
|
|
|
|
|
async def get_trading_decision(self, data_payload: dict): |
|
|
try: |
|
|
symbol = data_payload.get('symbol', 'unknown') |
|
|
target_strategy = data_payload.get('target_strategy', 'GENERIC') |
|
|
|
|
|
ohlcv_data = data_payload.get('raw_ohlcv') or data_payload.get('ohlcv') |
|
|
if not ohlcv_data: |
|
|
print(f"⚠️ لا توجد بيانات شموع لـ {symbol} - تخطي التحليل") |
|
|
return None |
|
|
|
|
|
total_candles = sum(len(data) for data in ohlcv_data.values() if data) if ohlcv_data else 0 |
|
|
timeframes_count = len([tf for tf, data in ohlcv_data.items() if data and len(data) >= 10]) if ohlcv_data else 0 |
|
|
|
|
|
print(f" 📊 بيانات {symbol}: {total_candles} شمعة في {timeframes_count} إطار زمني") |
|
|
|
|
|
if total_candles < 30: |
|
|
print(f" ⚠️ بيانات شموع غير كافية لـ {symbol}: {total_candles} شمعة فقط") |
|
|
return None |
|
|
|
|
|
valid_timeframes = [tf for tf, candles in ohlcv_data.items() if candles and len(candles) >= 5] |
|
|
if not valid_timeframes: |
|
|
print(f" ⚠️ لا توجد أطر زمنية صالحة لـ {symbol}") |
|
|
return None |
|
|
print(f" ✅ أطر زمنية صالحة لـ {symbol}: {', '.join(valid_timeframes)}") |
|
|
|
|
|
news_text = await self.news_fetcher.get_news_for_symbol(symbol) |
|
|
whale_data = data_payload.get('whale_data', {}) |
|
|
|
|
|
|
|
|
best_learned_exit = "None" |
|
|
learning_feedback = "No learning data yet." |
|
|
if self.learning_engine and self.learning_engine.initialized: |
|
|
best_learned_exit = await self.learning_engine.get_best_exit_profile(target_strategy) |
|
|
if best_learned_exit != "unknown": |
|
|
learning_feedback = f"Learning System Feedback: For the '{target_strategy}' strategy, the '{best_learned_exit}' exit profile has historically performed best. Please consider this." |
|
|
|
|
|
prompt = self._create_comprehensive_trading_prompt(data_payload, news_text, None, whale_data, learning_feedback) |
|
|
|
|
|
if self.r2_service: |
|
|
analysis_data = { |
|
|
'symbol': symbol, |
|
|
'current_price': data_payload.get('current_price'), |
|
|
'final_score': data_payload.get('final_score'), |
|
|
'enhanced_final_score': data_payload.get('enhanced_final_score'), |
|
|
'target_strategy': target_strategy, |
|
|
'learning_feedback_provided': learning_feedback, |
|
|
'whale_data_available': whale_data.get('data_available', False), |
|
|
'total_candles': total_candles, |
|
|
'timeframes_count': timeframes_count, |
|
|
'valid_timeframes': valid_timeframes, |
|
|
'timestamp': datetime.now().isoformat() |
|
|
} |
|
|
await self.r2_service.save_llm_prompts_async( |
|
|
symbol, 'comprehensive_trading_decision_v2', prompt, analysis_data |
|
|
) |
|
|
|
|
|
async with self.semaphore: |
|
|
response = await self._call_llm(prompt) |
|
|
|
|
|
decision_dict = self._parse_llm_response_enhanced(response, target_strategy, symbol) |
|
|
if decision_dict: |
|
|
decision_dict['model_source'] = self.model_name |
|
|
decision_dict['whale_data_integrated'] = whale_data.get('data_available', False) |
|
|
decision_dict['total_candles_analyzed'] = total_candles |
|
|
decision_dict['timeframes_analyzed'] = timeframes_count |
|
|
return decision_dict |
|
|
else: |
|
|
print(f"❌ فشل تحليل النموذج الضخم لـ {symbol} - لا توجد قرارات بديلة") |
|
|
return None |
|
|
|
|
|
except Exception as e: |
|
|
print(f"❌ خطأ في قرار التداول لـ {data_payload.get('symbol', 'unknown')}: {e}") |
|
|
traceback.print_exc() |
|
|
return None |
|
|
|
|
|
def _parse_llm_response_enhanced(self, response_text: str, fallback_strategy: str, symbol: str) -> dict: |
|
|
try: |
|
|
json_str = parse_json_from_response(response_text) |
|
|
if not json_str: |
|
|
print(f"❌ فشل استخراج JSON من استجابة النموذج لـ {symbol}") |
|
|
return None |
|
|
|
|
|
decision_data = safe_json_parse(json_str) |
|
|
if not decision_data: |
|
|
print(f"❌ فشل تحليل JSON (safe_json_parse) لـ {symbol}: {response_text}") |
|
|
return None |
|
|
|
|
|
|
|
|
required_fields = [ |
|
|
'action', 'reasoning', 'risk_assessment', 'stop_loss', 'take_profit', |
|
|
'expected_target_minutes', 'confidence_level', 'pattern_identified_by_llm', |
|
|
'exit_profile', 'exit_parameters' |
|
|
] |
|
|
if not validate_required_fields(decision_data, required_fields): |
|
|
print(f"❌ حقول مطلوبة مفقودة في استجابة النموذج لـ {symbol}") |
|
|
|
|
|
missing = [f for f in required_fields if f not in decision_data] |
|
|
print(f" MIA: {missing}") |
|
|
return None |
|
|
|
|
|
|
|
|
if not isinstance(decision_data['exit_parameters'], dict): |
|
|
print(f"❌ الحقل 'exit_parameters' ليس قاموساً (dict) صالحاً لـ {symbol}") |
|
|
return None |
|
|
|
|
|
action = decision_data.get('action') |
|
|
if action not in ['BUY', 'HOLD']: |
|
|
print(f"⚠️ النموذج اقترح إجراء غير مدعوم ({action}) لـ {symbol}. سيتم اعتباره HOLD.") |
|
|
decision_data['action'] = 'HOLD' |
|
|
|
|
|
if decision_data['action'] == 'BUY': |
|
|
decision_data['trade_type'] = 'LONG' |
|
|
else: |
|
|
decision_data['trade_type'] = None |
|
|
|
|
|
strategy_value = decision_data.get('strategy') |
|
|
if not strategy_value or strategy_value == 'unknown': |
|
|
decision_data['strategy'] = fallback_strategy |
|
|
|
|
|
return decision_data |
|
|
except Exception as e: |
|
|
print(f"❌ خطأ في تحليل استجابة النموذج لـ {symbol}: {e}") |
|
|
return None |
|
|
|
|
|
async def _get_pattern_analysis(self, data_payload): |
|
|
try: |
|
|
symbol = data_payload['symbol'] |
|
|
ohlcv_data = data_payload.get('raw_ohlcv') or data_payload.get('ohlcv') |
|
|
if ohlcv_data: |
|
|
return await self.pattern_engine.analyze_chart_patterns(symbol, ohlcv_data) |
|
|
return None |
|
|
except Exception as e: |
|
|
print(f"❌ فشل تحليل الأنماط (قد يكون لإعادة التحليل) لـ {data_payload.get('symbol')}: {e}") |
|
|
return None |
|
|
|
|
|
def _create_comprehensive_trading_prompt(self, payload: dict, news_text: str, pattern_analysis: dict, whale_data: dict, learning_feedback: str) -> str: |
|
|
symbol = payload.get('symbol', 'N/A') |
|
|
current_price = payload.get('current_price', 'N/A') |
|
|
reasons = payload.get('reasons_for_candidacy', []) |
|
|
sentiment_data = payload.get('sentiment_data', {}) |
|
|
advanced_indicators = payload.get('advanced_indicators', {}) |
|
|
strategy_scores = payload.get('strategy_scores', {}) |
|
|
recommended_strategy = payload.get('recommended_strategy', 'N/A') |
|
|
target_strategy = payload.get('target_strategy', 'GENERIC') |
|
|
final_score = payload.get('final_score', 'N/A') |
|
|
enhanced_final_score = payload.get('enhanced_final_score', 'N/A') |
|
|
ohlcv_data = payload.get('raw_ohlcv') or payload.get('ohlcv', {}) |
|
|
|
|
|
final_score_display = f"{final_score:.3f}" if isinstance(final_score, (int, float)) else str(final_score) |
|
|
enhanced_score_display = f"{enhanced_score_display:.3f}" if isinstance(enhanced_score, (int, float)) else str(enhanced_score) |
|
|
|
|
|
indicators_summary = format_technical_indicators(advanced_indicators) |
|
|
strategies_summary = format_strategy_scores(strategy_scores, recommended_strategy) |
|
|
whale_analysis_section = format_whale_analysis_for_llm(whale_data) |
|
|
candle_data_section = self._format_candle_data_comprehensive(ohlcv_data) |
|
|
market_context_section = self._format_market_context(sentiment_data) |
|
|
|
|
|
|
|
|
learning_feedback_section = f"🧠 LEARNING ENGINE FEEDBACK:\n{learning_feedback}" |
|
|
|
|
|
prompt = f""" |
|
|
COMPREHENSIVE TRADING ANALYSIS FOR {symbol} |
|
|
|
|
|
🚨 IMPORTANT SYSTEM CONSTRAINT: This is a SPOT TRADING system ONLY. Decisions MUST be limited to BUY (LONG) or HOLD. SHORT selling is NOT possible. |
|
|
|
|
|
🎯 STRATEGY CONTEXT: |
|
|
- Target Strategy: {target_strategy} |
|
|
- Recommended Strategy: {recommended_strategy} |
|
|
- Current Price: ${current_price} |
|
|
- System Score: {final_score_display} |
|
|
- Enhanced Score: {enhanced_score_display} |
|
|
|
|
|
{learning_feedback_section} |
|
|
|
|
|
📊 TECHNICAL INDICATORS (ALL TIMEFRAMES): |
|
|
{indicators_summary} |
|
|
|
|
|
📈 RAW CANDLE DATA SUMMARY & STATISTICS (FOR YOUR PATTERN ANALYSIS): |
|
|
{candle_data_section} |
|
|
{chr(10)}--- END OF CANDLE DATA ---{chr(10)} |
|
|
|
|
|
🎯 STRATEGY ANALYSIS (System's recommendation based on various factors): |
|
|
{strategies_summary} |
|
|
|
|
|
🐋 WHALE ACTIVITY ANALYSIS: |
|
|
{whale_analysis_section} |
|
|
|
|
|
🌍 MARKET CONTEXT: |
|
|
{market_context_section if market_context_section and "No market context" not in market_context_section else "Market context data not available for this analysis."} |
|
|
|
|
|
📰 LATEST NEWS: |
|
|
{news_text if news_text else "No significant news found"} |
|
|
|
|
|
📋 REASONS FOR SYSTEM CANDIDACY (Layer 1 & 2 Screening): |
|
|
{chr(10).join([f"• {reason}" for reason in reasons]) if reasons else "No specific reasons provided"} |
|
|
|
|
|
🎯 TRADING DECISION INSTRUCTIONS (SPOT ONLY - LLM MUST ANALYZE PATTERNS AND DEFINE EXIT STRATEGY): |
|
|
|
|
|
1. **PERFORM CHART PATTERN ANALYSIS:** Based *ONLY* on the provided 'RAW CANDLE DATA SUMMARY & STATISTICS' section above, identify relevant chart patterns (Triangles, Flags, Head & Shoulders, etc.) and candlestick patterns (Engulfing, Doji, etc.). |
|
|
2. **INTEGRATE ALL DATA:** Combine YOUR pattern analysis with technicals, strategy analysis, whale activity, market context, news, and the 'LEARNING ENGINE FEEDBACK'. |
|
|
3. **ADHERE STRICTLY TO SPOT TRADING RULES:** Only decide 'BUY' (LONG) or 'HOLD'. DO NOT suggest 'SELL'. |
|
|
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. |
|
|
- `"exit_profile"`: Choose one: "ATR_TRAILING" (Recommended for trends/breakouts), "FIXED_TARGET" (Recommended for mean reversion/scalping), "TIME_BASED" (Exit after X minutes regardless), "SIGNAL_BASED" (Emergency exit on opposite signal - *Use with caution*). |
|
|
- `"exit_parameters"`: Define parameters for the chosen profile. |
|
|
- 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%) |
|
|
- For "FIXED_TARGET": {{"time_stop_minutes": 120}} (Hard stop if target not hit in 120 mins) |
|
|
- For "TIME_BASED": {{"exit_after_minutes": 60}} |
|
|
- For "SIGNAL_BASED": {{"emergency_volume_spike_multiplier": 5.0}} (Exit if reverse volume spike > 5x average) |
|
|
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. |
|
|
|
|
|
OUTPUT FORMAT (JSON - SPOT ONLY - INCLUDE EXIT PROFILE): |
|
|
{{ |
|
|
"action": "BUY/HOLD", |
|
|
"reasoning": "Detailed explanation integrating ALL data sources, starting with the patterns identified from the candle summary, and justifying the BUY or HOLD decision. Explain *why* the chosen exit_profile is appropriate.", |
|
|
"pattern_identified_by_llm": "Name of the primary pattern(s) identified (e.g., 'Bull Flag on 1H', 'No Clear Pattern')", |
|
|
"pattern_influence": "Explain how the identified pattern(s) influenced the decision.", |
|
|
"risk_assessment": "low/medium/high", |
|
|
|
|
|
"stop_loss": 0.000000, # Required if action is BUY (Hard stop loss), 0 if HOLD |
|
|
"take_profit": 0.000000, # Required if action is BUY (Final target), 0 if HOLD |
|
|
|
|
|
"exit_profile": "FIXED_TARGET", # (Required if BUY, "None" if HOLD). Choose from: "ATR_TRAILING", "FIXED_TARGET", "TIME_BASED", "SIGNAL_BASED" |
|
|
"exit_parameters": {{ "time_stop_minutes": 120 }}, # (Required if BUY, {{}} if HOLD). Must match the chosen exit_profile. |
|
|
|
|
|
"expected_target_minutes": 15, # Required if action is BUY (Time to reach final TP), 0 if HOLD |
|
|
"confidence_level": 0.85, # Confidence in the BUY or HOLD decision |
|
|
"strategy": "{target_strategy}", # The strategy context provided |
|
|
"whale_influence": "How whale data influenced the BUY/HOLD decision", |
|
|
"key_support_level": 0.000000, # Derived from candle data analysis |
|
|
"key_resistance_level": 0.000000, # Derived from candle data analysis |
|
|
"risk_reward_ratio": 2.5 # Calculated for the HARD SL/TP, 0 if HOLD |
|
|
}} |
|
|
""" |
|
|
return prompt |
|
|
|
|
|
|
|
|
def _format_candle_data_comprehensive(self, ohlcv_data): |
|
|
"""تنسيق شامل لبيانات الشموع الخام""" |
|
|
if not ohlcv_data: |
|
|
return "No raw candle data available for analysis" |
|
|
|
|
|
try: |
|
|
timeframes_available = [] |
|
|
total_candles = 0 |
|
|
|
|
|
for timeframe, candles in ohlcv_data.items(): |
|
|
if candles and len(candles) >= 5: |
|
|
timeframes_available.append(f"{timeframe.upper()} ({len(candles)} candles)") |
|
|
total_candles += len(candles) |
|
|
|
|
|
if not timeframes_available: |
|
|
return "Insufficient candle data across all timeframes" |
|
|
|
|
|
summary = f"📊 Available Timeframes: {', '.join(timeframes_available)}\n" |
|
|
summary += f"📈 Total Candles Available: {total_candles}\n\n" |
|
|
|
|
|
raw_candle_analysis_text = self.pattern_engine._format_chart_data_for_llm(ohlcv_data) |
|
|
|
|
|
summary += raw_candle_analysis_text |
|
|
|
|
|
return summary |
|
|
except Exception as e: |
|
|
return f"Error formatting raw candle data: {str(e)}" |
|
|
|
|
|
def _analyze_timeframe_candles(self, candles, timeframe): |
|
|
"""تحليل الشموع لإطار زمني محدد - (تستخدم داخلياً بواسطة _format_raw_candle_data)""" |
|
|
try: |
|
|
if len(candles) < 10: |
|
|
return f"Insufficient data ({len(candles)} candles)" |
|
|
|
|
|
recent_candles = candles[-15:] |
|
|
|
|
|
closes = [c[4] for c in recent_candles] |
|
|
opens = [c[1] for c in recent_candles] |
|
|
highs = [c[2] for c in recent_candles] |
|
|
lows = [c[3] for c in recent_candles] |
|
|
volumes = [c[5] for c in recent_candles] |
|
|
|
|
|
current_price = closes[-1] |
|
|
first_price = closes[0] |
|
|
price_change = ((current_price - first_price) / first_price) * 100 if first_price > 0 else 0 |
|
|
|
|
|
if price_change > 2: trend = "🟢 UPTREND" |
|
|
elif price_change < -2: trend = "🔴 DOWNTREND" |
|
|
else: trend = "⚪ SIDEWAYS" |
|
|
|
|
|
high_max = max(highs) |
|
|
low_min = min(lows) |
|
|
volatility = ((high_max - low_min) / low_min) * 100 if low_min > 0 else 0 |
|
|
|
|
|
avg_volume = sum(volumes) / len(volumes) if volumes else 1 |
|
|
current_volume = volumes[-1] if volumes else 0 |
|
|
volume_ratio = current_volume / avg_volume if avg_volume > 0 else 1 |
|
|
|
|
|
green_candles = sum(1 for i in range(len(closes)) if closes[i] > opens[i]) |
|
|
red_candles = len(closes) - green_candles |
|
|
candle_ratio = green_candles / len(closes) if closes else 0 |
|
|
|
|
|
analysis = [ |
|
|
f"📈 Trend: {trend} ({price_change:+.2f}%)", |
|
|
f"🌊 Volatility: {volatility:.2f}%", |
|
|
f"📦 Volume: {volume_ratio:.2f}x average", |
|
|
f"🕯️ Candles: {green_candles}🟢/{red_candles}🔴 ({candle_ratio:.1%} green)", |
|
|
f"💰 Range: {low_min:.6f} - {high_max:.6f}", |
|
|
f"🎯 Current: {current_price:.6f}" |
|
|
] |
|
|
|
|
|
return "\n".join(analysis) |
|
|
except Exception as e: |
|
|
return f"Analysis error: {str(e)}" |
|
|
|
|
|
def _format_market_context(self, sentiment_data): |
|
|
"""تنسيق سياق السوق""" |
|
|
if not sentiment_data or sentiment_data.get('data_quality', 'LOW') == 'LOW': |
|
|
return "Market context data not available or incomplete." |
|
|
|
|
|
btc_sentiment = sentiment_data.get('btc_sentiment', 'N/A') |
|
|
fear_greed = sentiment_data.get('fear_and_greed_index', 'N/A') |
|
|
market_trend = sentiment_data.get('market_trend', 'N/A') |
|
|
|
|
|
lines = [ |
|
|
f"• Bitcoin Sentiment: {btc_sentiment}", |
|
|
f"• Fear & Greed Index: {fear_greed} ({sentiment_data.get('sentiment_class', 'Neutral')})", |
|
|
f"• Overall Market Trend: {market_trend.replace('_', ' ').title() if isinstance(market_trend, str) else 'N/A'}" |
|
|
] |
|
|
|
|
|
general_whale = sentiment_data.get('general_whale_activity', {}) |
|
|
if general_whale and general_whale.get('sentiment') != 'NEUTRAL': |
|
|
whale_sentiment = general_whale.get('sentiment', 'N/A') |
|
|
critical_alert = general_whale.get('critical_alert', False) |
|
|
lines.append(f"• General Whale Sentiment: {whale_sentiment.replace('_', ' ').title() if isinstance(whale_sentiment, str) else 'N/A'}") |
|
|
if critical_alert: |
|
|
lines.append(" ⚠️ CRITICAL WHALE ALERT ACTIVE") |
|
|
|
|
|
return "\n".join(lines) |
|
|
|
|
|
|
|
|
async def re_analyze_trade_async(self, trade_data: dict, processed_data: dict): |
|
|
try: |
|
|
symbol = trade_data['symbol'] |
|
|
original_strategy = trade_data.get('strategy', 'GENERIC') |
|
|
|
|
|
ohlcv_data = processed_data.get('raw_ohlcv') or processed_data.get('ohlcv') |
|
|
if not ohlcv_data: |
|
|
print(f"⚠️ لا توجد بيانات شموع محدثة لـ {symbol} - تخطي إعادة التحليل") |
|
|
return None |
|
|
|
|
|
news_text = await self.news_fetcher.get_news_for_symbol(symbol) |
|
|
pattern_analysis = await self._get_pattern_analysis(processed_data) |
|
|
whale_data = processed_data.get('whale_data', {}) |
|
|
|
|
|
|
|
|
best_learned_exit = "None" |
|
|
learning_feedback = "No learning data for re-analysis." |
|
|
if self.learning_engine and self.learning_engine.initialized: |
|
|
best_learned_exit = await self.learning_engine.get_best_exit_profile(original_strategy) |
|
|
if best_learned_exit != "unknown": |
|
|
learning_feedback = f"Learning System Feedback: For the '{original_strategy}' strategy, the '{best_learned_exit}' exit profile is typically best. Does this still apply?" |
|
|
|
|
|
prompt = self._create_re_analysis_prompt(trade_data, processed_data, news_text, pattern_analysis, whale_data, learning_feedback) |
|
|
|
|
|
if self.r2_service: |
|
|
analysis_data = { |
|
|
'symbol': symbol, |
|
|
'entry_price': trade_data.get('entry_price'), |
|
|
'current_price': processed_data.get('current_price'), |
|
|
'original_strategy': original_strategy, |
|
|
'learning_feedback_provided': learning_feedback, |
|
|
'pattern_analysis': pattern_analysis, |
|
|
'whale_data_available': whale_data.get('data_available', False) |
|
|
} |
|
|
await self.r2_service.save_llm_prompts_async( |
|
|
symbol, 'trade_reanalysis_v2', prompt, analysis_data |
|
|
) |
|
|
|
|
|
async with self.semaphore: |
|
|
response = await self._call_llm(prompt) |
|
|
|
|
|
re_analysis_dict = self._parse_re_analysis_response(response, original_strategy, symbol) |
|
|
if re_analysis_dict: |
|
|
re_analysis_dict['model_source'] = self.model_name |
|
|
re_analysis_dict['whale_data_integrated'] = whale_data.get('data_available', False) |
|
|
return re_analysis_dict |
|
|
else: |
|
|
print(f"❌ فشل إعادة تحليل النموذج الضخم لـ {symbol}") |
|
|
return None |
|
|
|
|
|
except Exception as e: |
|
|
print(f"❌ خطأ في إعادة تحليل LLM: {e}") |
|
|
traceback.print_exc() |
|
|
return None |
|
|
|
|
|
def _parse_re_analysis_response(self, response_text: str, fallback_strategy: str, symbol: str) -> dict: |
|
|
try: |
|
|
json_str = parse_json_from_response(response_text) |
|
|
if not json_str: |
|
|
return None |
|
|
|
|
|
decision_data = safe_json_parse(json_str) |
|
|
if not decision_data: |
|
|
print(f"❌ فشل تحليل JSON (safe_json_parse) لإعادة التحليل لـ {symbol}: {response_text}") |
|
|
return None |
|
|
|
|
|
action = decision_data.get('action') |
|
|
if action not in ['HOLD', 'CLOSE_TRADE', 'UPDATE_TRADE']: |
|
|
print(f"⚠️ النموذج اقترح إجراء إعادة تحليل غير مدعوم ({action}) لـ {symbol}. سيتم اعتباره HOLD.") |
|
|
decision_data['action'] = 'HOLD' |
|
|
|
|
|
|
|
|
if action == 'UPDATE_TRADE': |
|
|
required_update_fields = ['new_stop_loss', 'new_take_profit', 'new_exit_profile', 'new_exit_parameters'] |
|
|
if not validate_required_fields(decision_data, required_update_fields): |
|
|
print(f"❌ حقول مطلوبة مفقودة لـ UPDATE_TRADE لـ {symbol}") |
|
|
missing = [f for f in required_update_fields if f not in decision_data] |
|
|
print(f" MIA: {missing}") |
|
|
decision_data['action'] = 'HOLD' |
|
|
elif not isinstance(decision_data['new_exit_parameters'], dict): |
|
|
print(f"❌ الحقل 'new_exit_parameters' ليس قاموساً صالحاً لـ {symbol}") |
|
|
decision_data['action'] = 'HOLD' |
|
|
|
|
|
|
|
|
strategy_value = decision_data.get('strategy') |
|
|
if not strategy_value or strategy_value == 'unknown': |
|
|
decision_data['strategy'] = fallback_strategy |
|
|
|
|
|
return decision_data |
|
|
except Exception as e: |
|
|
print(f"Error parsing re-analysis response for {symbol}: {e}") |
|
|
return None |
|
|
|
|
|
def _create_re_analysis_prompt(self, trade_data: dict, processed_data: dict, news_text: str, pattern_analysis: dict, whale_data: dict, learning_feedback: str) -> str: |
|
|
symbol = trade_data.get('symbol', 'N/A') |
|
|
entry_price = trade_data.get('entry_price', 'N/A') |
|
|
current_price = processed_data.get('current_price', 'N/A') |
|
|
strategy = trade_data.get('strategy', 'GENERIC') |
|
|
original_trade_type = "LONG" |
|
|
|
|
|
|
|
|
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', {})) |
|
|
|
|
|
|
|
|
learning_feedback_section = f"🧠 LEARNING ENGINE FEEDBACK:\n{learning_feedback}" |
|
|
|
|
|
try: |
|
|
price_change = ((current_price - entry_price) / entry_price) * 100 if entry_price else 0 |
|
|
price_change_display = f"{price_change:+.2f}%" |
|
|
except (TypeError, ZeroDivisionError): |
|
|
price_change_display = "N/A" |
|
|
|
|
|
indicators_summary = format_technical_indicators(processed_data.get('advanced_indicators', {})) |
|
|
pattern_summary = self._format_pattern_analysis(pattern_analysis) if pattern_analysis else "Pattern analysis data not available for re-analysis." |
|
|
whale_analysis_section = format_whale_analysis_for_llm(whale_data) |
|
|
market_context_section = self._format_market_context(processed_data.get('sentiment_data', {})) |
|
|
ohlcv_data = processed_data.get('raw_ohlcv') or processed_data.get('ohlcv', {}) |
|
|
candle_data_section = self._format_candle_data_comprehensive(ohlcv_data) |
|
|
|
|
|
|
|
|
prompt = f""" |
|
|
TRADE RE-ANALYSIS FOR {symbol} (SPOT ONLY - Currently Open LONG Position) |
|
|
|
|
|
🚨 IMPORTANT SYSTEM CONSTRAINT: This is a SPOT TRADING system ONLY. The open trade is LONG. Re-analysis should decide to HOLD, CLOSE, or UPDATE this LONG position. SHORT selling is NOT possible. |
|
|
|
|
|
📊 CURRENT TRADE CONTEXT: |
|
|
- Strategy: {strategy} |
|
|
- Entry Price: {entry_price} (LONG position) |
|
|
- Current Price: {current_price} |
|
|
- Current Performance: {price_change_display} |
|
|
- Trade Age: {trade_data.get('hold_duration_minutes', 'N/A')} minutes |
|
|
- Current Exit Profile: {current_exit_profile} |
|
|
- Current Exit Parameters: {current_exit_params} |
|
|
|
|
|
{learning_feedback_section} |
|
|
|
|
|
🔄 UPDATED TECHNICAL ANALYSIS: |
|
|
{indicators_summary} |
|
|
|
|
|
📈 UPDATED RAW CANDLE DATA SUMMARY & STATISTICS: |
|
|
{candle_data_section} |
|
|
{chr(10)}--- END OF CANDLE DATA ---{chr(10)} |
|
|
|
|
|
🔍 UPDATED PATTERN ANALYSIS RESULTS (From System): |
|
|
{pattern_summary} |
|
|
|
|
|
🐋 UPDATED WHALE ACTIVITY: |
|
|
{whale_analysis_section} |
|
|
|
|
|
🌍 UPDATED MARKET CONTEXT: |
|
|
{market_context_section if market_context_section and "No market context" not in market_context_section else "Market context data not available for this re-analysis."} |
|
|
|
|
|
📰 LATEST NEWS: |
|
|
{news_text if news_text else "No significant news found"} |
|
|
|
|
|
🎯 RE-ANALYSIS INSTRUCTIONS (SPOT - LONG POSITION): |
|
|
|
|
|
1. **ANALYZE UPDATED DATA:** Evaluate if the original LONG thesis still holds based on the updated raw candle data summary, technicals, patterns (provided above), whale activity, market context, and learning feedback. |
|
|
2. **VALIDATE PATTERNS:** Consider the 'UPDATED PATTERN ANALYSIS RESULTS' provided. Does the recent price action confirm or invalidate these patterns? |
|
|
3. **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). |
|
|
4. **IF UPDATING (CRITICAL):** If action is UPDATE_TRADE, you MUST provide: |
|
|
- `new_stop_loss` (New hard stop) |
|
|
- `new_take_profit` (New final target) |
|
|
- `new_exit_profile`: (e.g., "ATR_TRAILING") - Can be the same or different. |
|
|
- `new_exit_parameters`: (e.g., {{"atr_multiplier": 1.5}}) - Must match the new profile. |
|
|
5. **PROVIDE DETAILS:** Justify your decision clearly, integrating all data points. |
|
|
|
|
|
CRITICAL: The decision must be one of HOLD, CLOSE_TRADE, or UPDATE_TRADE for the existing LONG position. |
|
|
|
|
|
OUTPUT FORMAT (JSON - SPOT RE-ANALYSIS): |
|
|
{{ |
|
|
"action": "HOLD/CLOSE_TRADE/UPDATE_TRADE", |
|
|
"reasoning": "Comprehensive justification for HOLD, CLOSE, or UPDATE of the LONG position, based on updated analysis. If UPDATE, explain why the new exit profile/parameters are better.", |
|
|
|
|
|
"new_stop_loss": 0.000000, # (Required if UPDATE_TRADE, else 0) |
|
|
"new_take_profit": 0.000000, # (Required if UPDATE_TRADE, else 0) |
|
|
"new_exit_profile": "None", # (Required if UPDATE_TRADE, else "None") |
|
|
"new_exit_parameters": {{}}, # (Required if UPDATE_TRADE, else {{}}) |
|
|
|
|
|
"new_expected_minutes": 15, # If action is UPDATE_TRADE or HOLD (new expectation), else 0 |
|
|
"confidence_level": 0.85, # Confidence in the re-analysis decision |
|
|
"strategy": "{strategy}", # Original strategy context |
|
|
"whale_influence_reanalysis": "How updated whale data influenced the decision", |
|
|
"pattern_influence_reanalysis": "How updated candle patterns AND provided patterns influenced the decision", |
|
|
"risk_adjustment": "low/medium/high" # Current risk level if HOLDING |
|
|
}} |
|
|
""" |
|
|
return prompt |
|
|
|
|
|
|
|
|
def _format_pattern_analysis(self, pattern_analysis): |
|
|
"""تنسيق تحليل الأنماط للنموذج الضخم""" |
|
|
if not pattern_analysis or not pattern_analysis.get('pattern_detected') or pattern_analysis.get('pattern_detected') == 'no_clear_pattern': |
|
|
return "No clear chart pattern detected by the system." |
|
|
|
|
|
pattern = pattern_analysis.get('pattern_detected', 'N/A') |
|
|
confidence = pattern_analysis.get('pattern_confidence', 0) |
|
|
direction = pattern_analysis.get('predicted_direction', 'N/A') |
|
|
timeframe = pattern_analysis.get('timeframe', 'N/A') |
|
|
|
|
|
return f"System Pattern Analysis: Detected '{pattern}' on {timeframe} timeframe with {confidence:.2f} confidence. Predicted direction: {direction}." |
|
|
|
|
|
@_rate_limit_nvidia_api |
|
|
async def _call_llm(self, prompt: str) -> str: |
|
|
try: |
|
|
|
|
|
for attempt in range(2): |
|
|
try: |
|
|
response = self.client.chat.completions.create( |
|
|
model=self.model_name, |
|
|
messages=[{"role": "user", "content": prompt}], |
|
|
temperature=self.temperature, |
|
|
seed=int(time.time()), |
|
|
max_tokens=4000 |
|
|
) |
|
|
content = response.choices[0].message.content |
|
|
if content and '{' in content and '}' in content: |
|
|
return content |
|
|
else: |
|
|
print(f"⚠️ LLM returned invalid content (attempt {attempt+1}): {content[:100]}...") |
|
|
if attempt == 0: await asyncio.sleep(1) |
|
|
|
|
|
except (RateLimitError, APITimeoutError) as e: |
|
|
print(f"❌ LLM API Error (Rate Limit/Timeout): {e}. Retrying via backoff...") |
|
|
raise |
|
|
except Exception as e: |
|
|
print(f"❌ Unexpected LLM API error (attempt {attempt+1}): {e}") |
|
|
if attempt == 0: await asyncio.sleep(2) |
|
|
elif attempt == 1: raise |
|
|
|
|
|
print("❌ LLM failed to return valid content after retries.") |
|
|
return "" |
|
|
|
|
|
except Exception as e: |
|
|
print(f"❌ Final failure in _call_llm after backoff retries: {e}") |
|
|
raise |
|
|
|
|
|
print("✅ LLM Service loaded - V2 (Dynamic Exit Profiles & Learning Feedback)") |