Spaces:
Running
Running
| # trade_manager.py (Updated to V7.6 - Exponential Backoff for Timeouts) | |
| import asyncio | |
| import json | |
| import time | |
| import traceback | |
| import os | |
| from datetime import datetime, timedelta | |
| from typing import Dict, Any, List | |
| from collections import deque, defaultdict | |
| import pandas as pd | |
| try: | |
| import pandas_ta as ta | |
| except ImportError: | |
| print("⚠️ مكتبة pandas_ta غير موجودة، مؤشرات الحارس (Sentry 1m) ستفشل.") | |
| ta = None | |
| try: | |
| import ccxt.async_support as ccxtasync | |
| # 🔴 --- START OF CHANGE (V7.6) --- 🔴 | |
| # (نحتاج لاستيراد الأخطاء للتعامل معها) | |
| from ccxt.base.errors import RequestTimeout, RateLimitExceeded | |
| CCXT_ASYNC_AVAILABLE = True | |
| # 🔴 --- END OF CHANGE --- 🔴 | |
| except ImportError: | |
| print("❌❌❌ خطأ فادح: فشل استيراد 'ccxt.async_support'. ❌❌❌") | |
| CCXT_ASYNC_AVAILABLE = False | |
| import numpy as np | |
| from helpers import safe_float_conversion | |
| # (استيراد المحللات لتشغيل الكاشف المصغر 5m) | |
| from ml_engine.indicators import AdvancedTechnicalAnalyzer | |
| from ml_engine.patterns import ChartPatternAnalyzer | |
| class TacticalData: | |
| """ | |
| (محدث V7.0) | |
| لتخزين بيانات 1m (للدخول) و 5m (لحماية الأرباح). | |
| """ | |
| def __init__(self, symbol): | |
| self.symbol = symbol | |
| self.order_book = None | |
| self.trades = deque(maxlen=100) | |
| self.cvd = 0.0 | |
| self.large_trades = [] | |
| self.last_update = time.time() | |
| self.confirmation_trades = defaultdict(lambda: deque(maxlen=50)) | |
| self.confirmation_cvd = defaultdict(float) | |
| self.last_kucoin_trade_id = None | |
| self.last_confirmation_trade_ids = defaultdict(lambda: None) | |
| self.ohlcv_1m = deque(maxlen=100) | |
| self.indicators_1m = {} | |
| self.last_1m_candle_timestamp = None | |
| # (إضافة مؤشرات 1h لـ ATR Trailing) | |
| self.ohlcv_1h = deque(maxlen=100) | |
| self.indicators_1h = {} | |
| self.last_1h_candle_timestamp = None | |
| self.ohlcv_5m = deque(maxlen=100) | |
| self.last_5m_candle_timestamp = None | |
| self.new_5m_data_added = False | |
| def add_trade(self, trade): | |
| trade_id = trade.get('id') | |
| if trade_id and trade_id == self.last_kucoin_trade_id: | |
| return | |
| self.last_kucoin_trade_id = trade_id | |
| self.trades.append(trade) | |
| self.last_update = time.time() | |
| try: | |
| trade_amount = float(trade['amount']) | |
| if trade['side'] == 'buy': self.cvd += trade_amount | |
| else: self.cvd -= trade_amount | |
| trade_cost_usd = float(trade.get('cost', 0)) | |
| if trade_cost_usd == 0 and 'price' in trade: | |
| trade_cost_usd = float(trade['price']) * trade_amount | |
| if trade_cost_usd > 20000: | |
| self.large_trades.append(trade) | |
| if len(self.large_trades) > 20: self.large_trades.pop(0) | |
| except Exception: pass | |
| def add_confirmation_trade(self, exchange_id: str, trade: Dict): | |
| trade_id = trade.get('id') | |
| if trade_id and trade_id == self.last_confirmation_trade_ids[exchange_id]: | |
| return | |
| self.last_confirmation_trade_ids[exchange_id] = trade_id | |
| self.confirmation_trades[exchange_id].append(trade) | |
| try: | |
| trade_amount = float(trade['amount']) | |
| if trade['side'] == 'buy': self.confirmation_cvd[exchange_id] += trade_amount | |
| else: self.confirmation_cvd[exchange_id] -= trade_amount | |
| except Exception: pass | |
| def set_order_book(self, ob): | |
| self.order_book = ob | |
| self.last_update = time.time() | |
| def analyze_order_book(self): | |
| """(محدث V7.3) تحليل دفتر الطلبات وإرجاع درجة (0-1) ونسبة""" | |
| if not self.order_book: | |
| return {"bids_depth": 0, "asks_depth": 0, "ob_score": 0.0, "ob_ratio": 1.0} | |
| try: | |
| bids = self.order_book.get('bids', []); asks = self.order_book.get('asks', []) | |
| bids_depth = sum(price * amount for price, amount in bids[:10]) | |
| asks_depth = sum(price * amount for price, amount in asks[:10]) | |
| # (حساب النسبة والدرجة لزناد الدخول) | |
| ob_ratio = 1.0 | |
| ob_score = 0.0 | |
| if asks_depth > 0: | |
| ob_ratio = bids_depth / asks_depth | |
| # (من 1.0 إلى 2.0+) | |
| # (نطاق النتيجة: 0.0 -> 1.0) | |
| ob_score = min(1.0, max(0.0, (ob_ratio - 1.0))) | |
| return { | |
| "bids_depth": bids_depth, | |
| "asks_depth": asks_depth, | |
| "ob_ratio": ob_ratio, | |
| "ob_score": ob_score | |
| } | |
| except Exception: | |
| return {"bids_depth": 0, "asks_depth": 0, "ob_score": 0.0, "ob_ratio": 1.0} | |
| def add_1m_ohlcv(self, ohlcv_data: List): | |
| """إضافة شموع 1-دقيقة وحساب المؤشرات (للدخول)""" | |
| if not ohlcv_data: | |
| return | |
| new_candles_added = False | |
| for candle in ohlcv_data: | |
| timestamp = candle[0] | |
| if timestamp and timestamp != self.last_1m_candle_timestamp: | |
| if self.ohlcv_1m and timestamp < self.ohlcv_1m[-1][0]: | |
| continue | |
| self.ohlcv_1m.append(candle) | |
| self.last_1m_candle_timestamp = timestamp | |
| new_candles_added = True | |
| if new_candles_added and len(self.ohlcv_1m) >= 26: | |
| self._analyze_1m_indicators() | |
| def add_1h_ohlcv(self, ohlcv_data: List): | |
| """(جديد V7.5) إضافة شموع 1-ساعة (لـ ATR Trailing Stop)""" | |
| if not ohlcv_data: | |
| return | |
| new_candles_added = False | |
| for candle in ohlcv_data: | |
| timestamp = candle[0] | |
| if timestamp and timestamp != self.last_1h_candle_timestamp: | |
| if self.ohlcv_1h and timestamp < self.ohlcv_1h[-1][0]: | |
| continue | |
| self.ohlcv_1h.append(candle) | |
| self.last_1h_candle_timestamp = timestamp | |
| new_candles_added = True | |
| if new_candles_added and len(self.ohlcv_1h) >= 20: # (ATR يحتاج 14) | |
| self._analyze_1h_indicators() | |
| def _analyze_1m_indicators(self): | |
| """حساب مؤشرات 1-دقيقة الحقيقية (للدخول)""" | |
| if ta is None or len(self.ohlcv_1m) < 26: | |
| self.indicators_1m = {} | |
| return | |
| try: | |
| df = pd.DataFrame(list(self.ohlcv_1m), columns=['timestamp', 'open', 'high', 'low', 'close', 'volume']) | |
| df[['open', 'high', 'low', 'close', 'volume']] = df[['open', 'high', 'low', 'close', 'volume']].astype(float) | |
| close = df['close'] | |
| ema_9 = ta.ema(close, length=9) | |
| ema_21 = ta.ema(close, length=21) | |
| macd_data = ta.macd(close, fast=12, slow=26, signal=9) | |
| if ema_9 is not None and not ema_9.empty and \ | |
| ema_21 is not None and not ema_21.empty and \ | |
| macd_data is not None and not macd_data.empty: | |
| ema_9_val = ema_9.iloc[-1] | |
| ema_21_val = ema_21.iloc[-1] | |
| macd_hist_val = macd_data['MACDh_12_26_9'].iloc[-1] | |
| # (إضافة درجات محسوبة مسبقاً لزناد الدخول) | |
| ema_score = 1.0 if ema_9_val > ema_21_val else 0.0 | |
| macd_score = 1.0 if macd_hist_val > 0 else 0.0 | |
| self.indicators_1m = { | |
| 'ema_9': ema_9_val, | |
| 'ema_21': ema_21_val, | |
| 'macd_hist': macd_hist_val, | |
| 'ema_score_1m': ema_score, | |
| 'macd_score_1m': macd_score | |
| } | |
| else: | |
| self.indicators_1m = {} | |
| except Exception as e: | |
| self.indicators_1m = {} | |
| def _analyze_1h_indicators(self): | |
| """(جديد V7.5) حساب مؤشرات 1-ساعة (فقط ATR حالياً)""" | |
| if ta is None or len(self.ohlcv_1h) < 15: # (ATR يحتاج 14) | |
| self.indicators_1h = {} | |
| return | |
| try: | |
| df = pd.DataFrame(list(self.ohlcv_1h), columns=['timestamp', 'open', 'high', 'low', 'close', 'volume']) | |
| df[['open', 'high', 'low', 'close', 'volume']] = df[['open', 'high', 'low', 'close', 'volume']].astype(float) | |
| atr = ta.atr(df['high'], df['low'], df['close'], length=14) | |
| if atr is not None and not atr.empty: | |
| self.indicators_1h = { | |
| 'atr': atr.iloc[-1] | |
| } | |
| else: | |
| self.indicators_1h = {} | |
| except Exception as e: | |
| self.indicators_1h = {} | |
| def add_5m_ohlcv(self, ohlcv_data: List): | |
| """(جديد V7.0) إضافة شموع 5-دقائق (لحماية الأرباح)""" | |
| if not ohlcv_data: | |
| return | |
| for candle in ohlcv_data: | |
| timestamp = candle[0] | |
| if timestamp and timestamp != self.last_5m_candle_timestamp: | |
| if self.ohlcv_5m and timestamp < self.ohlcv_5m[-1][0]: | |
| continue | |
| self.ohlcv_5m.append(candle) | |
| self.last_5m_candle_timestamp = timestamp | |
| self.new_5m_data_added = True | |
| def get_tactical_snapshot(self): | |
| """(محدث V7.3) يحلل كل شيء ويعيد لقطة تكتيكية""" | |
| agg_cvd = sum(self.confirmation_cvd.values()) | |
| ob_analysis = self.analyze_order_book() | |
| # (حساب درجة CVD لزناد الدخول) | |
| # (نطاق النتيجة: 0.0 -> 1.0) | |
| cvd_score = min(1.0, max(0.0, self.cvd / 50000.0)) # (نفترض 50k كحد أقصى) | |
| return { | |
| "cvd_kucoin": self.cvd, | |
| "cvd_confirmation_sources": dict(self.confirmation_cvd), | |
| "cvd_confirmation_aggregate": agg_cvd, | |
| "cvd_score_1m": cvd_score, | |
| "large_trades_count_5m": len([t for t in self.large_trades if t.get('timestamp') and (time.time() - t['timestamp']/1000) < 300]), | |
| "indicators_1m": self.indicators_1m, | |
| "indicators_1h": self.indicators_1h, # (جديد V7.5) | |
| "ob_analysis": ob_analysis | |
| } | |
| class TradeManager: | |
| def __init__(self, r2_service, learning_hub=None, data_manager=None, state_manager=None, callback_on_close=None): | |
| if not CCXT_ASYNC_AVAILABLE: | |
| raise RuntimeError("مكتبة 'ccxt.async_support' غير متاحة.") | |
| self.r2_service = r2_service | |
| self.learning_hub = learning_hub | |
| self.data_manager = data_manager | |
| self.state_manager = state_manager | |
| self.callback_on_close = callback_on_close | |
| self.is_running = False | |
| self.sentry_watchlist = {} | |
| self.sentry_tasks = {} | |
| self.tactical_data_cache = {} | |
| self.sentry_lock = asyncio.Lock() | |
| self.kucoin_rest = None | |
| self.confirmation_exchanges = {} | |
| self.polling_interval = 1.5 | |
| self.confirmation_polling_interval = 3.0 | |
| # 🔴 --- START OF CHANGE (V7.6) --- 🔴 | |
| # (زيادة المهلة الافتراضية للاتصال العام) | |
| self.kucoin_rest_config = { | |
| 'enableRateLimit': True, | |
| 'timeout': 60000, # (زيادة المهلة إلى 60 ثانية) | |
| } | |
| # 🔴 --- END OF CHANGE --- 🔴 | |
| self.sentry_technical_analyzer = AdvancedTechnicalAnalyzer() | |
| if self.data_manager and self.data_manager.pattern_analyzer: | |
| self.sentry_pattern_analyzer = self.data_manager.pattern_analyzer | |
| print("✅ [Sentry V7.4] تم ربط محرك الأنماط V8 (الرئيسي) بالحارس.") | |
| else: | |
| print("⚠️ [Sentry V7.4] DataManager أو محرك V8 غير متاح. العودة إلى الوضع الآمن.") | |
| self.sentry_pattern_analyzer = ChartPatternAnalyzer(r2_service=None) | |
| self.optimized_weights = {} | |
| self.last_weights_update = 0 | |
| self.weights_lock = asyncio.Lock() | |
| self.weights_refresh_interval = 300 # (تحديث الأوزان كل 5 دقائق) | |
| async def initialize_sentry_exchanges(self): | |
| try: | |
| print("🔄 [Sentry] تهيئة منصات التداول (KuCoin REST ومنصات التأكيد)...") | |
| print(" [Sentry] تهيئة KuCoin للبيانات العامة (وضع المحاكاة).") | |
| # 🔴 --- START OF CHANGE (V7.6) --- 🔴 | |
| # (استخدام الإعدادات الجديدة مع المهلة الممددة) | |
| self.kucoin_rest = ccxtasync.kucoin(self.kucoin_rest_config) | |
| # 🔴 --- END OF CHANGE --- 🔴 | |
| await self.kucoin_rest.load_markets() | |
| print("✅ [Sentry] منصة REST الأساسية (KuCoin) جاهزة (بيانات عامة فقط).") | |
| self.confirmation_exchanges = {} | |
| confirmation_exchange_ids = ['bybit', 'okx', 'gateio'] | |
| print(f" [Sentry] تهيئة منصات التأكيد (Confirmation Exchanges): {', '.join(confirmation_exchange_ids)}") | |
| for ex_id in confirmation_exchange_ids: | |
| try: | |
| # (تطبيق نفس المهلة الممددة على منصات التأكيد أيضاً) | |
| exchange = getattr(ccxtasync, ex_id)(self.kucoin_rest_config) | |
| await exchange.load_markets() | |
| self.confirmation_exchanges[ex_id] = exchange | |
| print(f" ✅ [Sentry] منصة التأكيد {ex_id} جاهزة (REST).") | |
| except Exception as ex_err: | |
| print(f" ⚠️ [Sentry] فشل تهيئة منصة التأكيد {ex_id}: {ex_err}") | |
| if ex_id in self.confirmation_exchanges: | |
| try: | |
| await self.confirmation_exchanges[ex_id].close() | |
| except Exception: | |
| pass | |
| del self.confirmation_exchanges[ex_id] | |
| if not self.confirmation_exchanges: | |
| print(" ⚠️ [Sentry] تحذير: لم يتم تهيئة أي منصة تأكيد. سيعمل الحارس على بيانات KuCoin فقط.") | |
| await self._get_or_refresh_weights() | |
| print("✅ [Sentry] تم تحميل الأوزان المتعلمة (V7.3) لأول مرة.") | |
| except Exception as e: | |
| print(f"❌ [Sentry] فشل فادح في تهيئة KuCoin REST: {e}") | |
| if self.kucoin_rest: await self.kucoin_rest.close() | |
| for ex in self.confirmation_exchanges.values(): await ex.close() | |
| raise | |
| async def _get_or_refresh_weights(self) -> Dict: | |
| """ | |
| يجلب الأوزان المتعلمة من محور التعلم ويخزنها مؤقتاً. | |
| """ | |
| async with self.weights_lock: | |
| current_time = time.time() | |
| if (current_time - self.last_weights_update) > self.weights_refresh_interval or not self.optimized_weights: | |
| print("🔄 [Sentry V7.3] تحديث الأوزان المتكيفة من محور التعلم...") | |
| try: | |
| if not self.data_manager or not self.learning_hub: | |
| raise ValueError("DataManager or LearningHub not available") | |
| market_context = await self.data_manager.get_market_context_async() | |
| market_condition = market_context.get('market_trend', 'sideways_market') | |
| all_weights = await self.learning_hub.get_optimized_weights(market_condition) | |
| if not all_weights: | |
| raise ValueError("محور التعلم أعاد أوزاناً فارغة") | |
| self.optimized_weights = all_weights | |
| self.last_weights_update = current_time | |
| print("✅ [Sentry V7.3] تم تحديث الأوزان المتكيفة بنجاح.") | |
| except Exception as e: | |
| print(f"❌ [Sentry V7.3] فشل تحديث الأوزان: {e}. استخدام آخر أوزان معروفة (أو الافتراضية).") | |
| if not self.optimized_weights: | |
| self.optimized_weights = await self.learning_hub.get_optimized_weights("sideways_market") | |
| return self.optimized_weights | |
| async def start_sentry_and_monitoring_loops(self): | |
| self.is_running = True | |
| print(f"✅ [Sentry] بدء حلقات المراقبة التكتيكية (Layer 2 - API Polling)...") | |
| while self.is_running: | |
| try: | |
| async with self.sentry_lock: | |
| watchlist_symbols = set(self.sentry_watchlist.keys()) | |
| open_trades = await self.get_open_trades() | |
| open_trade_symbols = {t['symbol'] for t in open_trades} | |
| symbols_to_monitor = watchlist_symbols.union(open_trade_symbols) | |
| current_tasks = set(self.sentry_tasks.keys()) | |
| symbols_to_add = symbols_to_monitor - current_tasks | |
| for symbol in symbols_to_add: | |
| print(f" [Sentry] بدء المراقبة التكتيكية (Polling) لـ {symbol}") | |
| strategy_hint = 'generic' | |
| if symbol in watchlist_symbols: | |
| async with self.sentry_lock: | |
| if symbol in self.sentry_watchlist: | |
| strategy_hint = self.sentry_watchlist.get(symbol, {}).get('strategy_hint', 'generic') | |
| elif symbol in open_trade_symbols: | |
| trade = next((t for t in open_trades if t['symbol'] == symbol), None) | |
| if trade: | |
| strategy_hint = trade.get('strategy', 'generic') | |
| if symbol not in self.tactical_data_cache: | |
| self.tactical_data_cache[symbol] = TacticalData(symbol) | |
| task = asyncio.create_task(self._monitor_symbol_activity_polling(symbol, strategy_hint)) | |
| self.sentry_tasks[symbol] = task | |
| symbols_to_remove = current_tasks - symbols_to_monitor | |
| for symbol in symbols_to_remove: | |
| print(f" [Sentry] إيقاف المراقبة التكتيكية (Polling) لـ {symbol}") | |
| task = self.sentry_tasks.pop(symbol, None) | |
| if task: | |
| task.cancel() | |
| if symbol in self.tactical_data_cache: | |
| del self.tactical_data_cache[symbol] | |
| await asyncio.sleep(15) | |
| except Exception as error: | |
| print(f"❌ [Sentry] خطأ في الحلقة الرئيسية: {error}"); traceback.print_exc(); await asyncio.sleep(60) | |
| async def stop_sentry_loops(self): | |
| self.is_running = False | |
| print("🛑 [Sentry] إيقاف جميع حلقات المراقبة...") | |
| for task in self.sentry_tasks.values(): task.cancel() | |
| self.sentry_tasks.clear() | |
| try: | |
| if self.kucoin_rest: | |
| await self.kucoin_rest.close() | |
| print(" ✅ [Sentry] تم إغلاق اتصال KuCoin REST.") | |
| for ex_id, exchange in self.confirmation_exchanges.items(): | |
| try: | |
| await exchange.close() | |
| print(f" ✅ [Sentry] تم إغلاق اتصال {ex_id} REST.") | |
| except Exception as e: | |
| print(f"⚠️ [Sentry] خطأ أثناء إغلاق منصة {ex_id}: {e}") | |
| self.confirmation_exchanges.clear() | |
| print("✅ [Sentry] تم إغلاق اتصالات التداول (REST).") | |
| except Exception as e: print(f"⚠️ [Sentry] خطأ أثناء إغلاق الاتصالات: {e}") | |
| async def update_sentry_watchlist(self, candidates: List[Dict]): | |
| async with self.sentry_lock: | |
| self.sentry_watchlist = {c['symbol']: c for c in candidates} | |
| print(f"ℹ️ [Sentry] تم تحديث Watchlist. عدد المرشحين: {len(self.sentry_watchlist)}") | |
| def get_sentry_status(self): | |
| active_monitoring_count = len(self.sentry_tasks) | |
| watchlist_symbols_list = list(self.sentry_watchlist.keys()) | |
| return { | |
| 'is_running': self.is_running, | |
| 'active_monitoring_tasks': active_monitoring_count, | |
| 'watchlist_symbols': watchlist_symbols_list, | |
| 'monitored_symbols': list(self.sentry_tasks.keys()), | |
| 'confirmation_exchanges_active': list(self.confirmation_exchanges.keys()) | |
| } | |
| async def _monitor_symbol_activity_polling(self, symbol: str, strategy_hint: str): | |
| if symbol not in self.tactical_data_cache: | |
| self.tactical_data_cache[symbol] = TacticalData(symbol) | |
| # 🔴 --- START OF CHANGE (V7.6) --- 🔴 | |
| # (إضافة متغيرات التراجع الأسي الخاصة بهذه المهمة) | |
| kucoin_backoff_delay = 5.0 # (البدء بـ 5 ثوانٍ) | |
| MAX_BACKOFF = 60.0 # (الحد الأقصى 60 ثانية) | |
| # 🔴 --- END OF CHANGE --- 🔴 | |
| tasks_to_gather = [ | |
| # (تمرير متغيرات التراجع) | |
| self._poll_kucoin_data(symbol, kucoin_backoff_delay, MAX_BACKOFF), | |
| self._poll_confirmation_data(symbol), | |
| self._run_tactical_analysis_loop(symbol, strategy_hint) | |
| ] | |
| try: | |
| await asyncio.gather(*tasks_to_gather) | |
| except asyncio.CancelledError: | |
| print(f"ℹ️ [Sentry] تم إيقاف المراقبة (Polling) لـ {symbol}.") | |
| except Exception as e: | |
| print(f"❌ [Sentry] خطأ فادح في مراقبة (Polling) {symbol}: {e}") | |
| traceback.print_exc() | |
| finally: | |
| print(f"🛑 [Sentry] إنهاء جميع مهام (Polling) {symbol}") | |
| if symbol in self.sentry_tasks: | |
| self.sentry_tasks.pop(symbol, None) | |
| if symbol in self.tactical_data_cache: | |
| del self.tactical_data_cache[symbol] | |
| # 🔴 --- START OF CHANGE (V7.6) --- 🔴 | |
| # (تعديل الدالة لتقبل متغيرات التراجع) | |
| async def _poll_kucoin_data(self, symbol, kucoin_backoff_delay, MAX_BACKOFF): | |
| """(محدث V7.6) حلقة Polling لبيانات KuCoin (مع التراجع الأسي)""" | |
| base_polling_interval = self.polling_interval # (الفترة العادية) | |
| while self.is_running: | |
| try: | |
| if not self.kucoin_rest: | |
| print(f" [Sentry Polling] KuCoin REST غير متاح لـ {symbol}. الانتظار...") | |
| await asyncio.sleep(10) | |
| continue | |
| tasks = { | |
| 'ob': asyncio.create_task(self.kucoin_rest.fetch_order_book(symbol, limit=20)), | |
| 'trades': asyncio.create_task(self.kucoin_rest.fetch_trades(symbol, since=int((time.time() - 60) * 1000), limit=50)), | |
| 'ohlcv_1m': asyncio.create_task(self.kucoin_rest.fetch_ohlcv(symbol, '1m', limit=50)), | |
| 'ohlcv_5m': asyncio.create_task(self.kucoin_rest.fetch_ohlcv(symbol, '5m', limit=50)), | |
| 'ohlcv_1h': asyncio.create_task(self.kucoin_rest.fetch_ohlcv(symbol, '1h', limit=50)) | |
| } | |
| await asyncio.wait(tasks.values(), return_when=asyncio.ALL_COMPLETED) | |
| if symbol not in self.tactical_data_cache: | |
| continue | |
| # (التحقق من الأخطاء بشكل فردي لتجنب انهيار الحلقة) | |
| if not tasks['ob'].exception(): | |
| self.tactical_data_cache[symbol].set_order_book(tasks['ob'].result()) | |
| else: | |
| # (رمي الخطأ ليتم التقاطه أدناه) | |
| raise tasks['ob'].exception() | |
| if not tasks['trades'].exception(): | |
| trades = tasks['trades'].result() | |
| trades.sort(key=lambda x: x['timestamp']) | |
| for trade in trades: | |
| self.tactical_data_cache[symbol].add_trade(trade) | |
| else: | |
| raise tasks['trades'].exception() | |
| if not tasks['ohlcv_1m'].exception(): | |
| self.tactical_data_cache[symbol].add_1m_ohlcv(tasks['ohlcv_1m'].result()) | |
| if not tasks['ohlcv_5m'].exception(): | |
| self.tactical_data_cache[symbol].add_5m_ohlcv(tasks['ohlcv_5m'].result()) | |
| if not tasks['ohlcv_1h'].exception(): | |
| self.tactical_data_cache[symbol].add_1h_ohlcv(tasks['ohlcv_1h'].result()) | |
| # (نجاح! إعادة تعيين التراجع والانتظار العادي) | |
| kucoin_backoff_delay = base_polling_interval # (إعادة تعيين) | |
| await asyncio.sleep(base_polling_interval) | |
| except RateLimitExceeded as e: | |
| print(f"⏳ [Sentry Polling] {symbol} KuCoin Rate Limit Exceeded: {e}. زيادة فترة الانتظار (30 ثانية)...") | |
| await asyncio.sleep(30) | |
| # (لا نزيد التراجع الأسي هنا، ننتظر فقط) | |
| except RequestTimeout as e: | |
| print(f"⏳ [Sentry Polling] {symbol} KuCoin Request Timeout: {e}. التراجع الأسي (الانتظار {kucoin_backoff_delay:.1f} ثانية)...") | |
| await asyncio.sleep(kucoin_backoff_delay) | |
| # (مضاعفة فترة الانتظار للمرة القادمة، بحد أقصى) | |
| kucoin_backoff_delay = min(kucoin_backoff_delay * 2, MAX_BACKOFF) | |
| except asyncio.CancelledError: | |
| raise | |
| except Exception as e: | |
| # (التقاط أخطاء أخرى (مثل 404 Not Found أو أخطاء التحليل)) | |
| print(f"⚠️ [Sentry Polling] خطأ في {symbol} KuCoin data polling: {e}") | |
| # (إعادة تعيين التراجع عند الأخطاء العامة) | |
| kucoin_backoff_delay = base_polling_interval | |
| await asyncio.sleep(10) # (انتظار أطول من 5 ثوانٍ) | |
| # 🔴 --- END OF CHANGE --- 🔴 | |
| async def _poll_confirmation_data(self, symbol): | |
| if not self.confirmation_exchanges: | |
| return | |
| await asyncio.sleep(self.confirmation_polling_interval / 2) | |
| while self.is_running: | |
| try: | |
| tasks = [] | |
| for ex_id, exchange in self.confirmation_exchanges.items(): | |
| tasks.append(self._fetch_confirmation_trades(ex_id, exchange, symbol)) | |
| await asyncio.gather(*tasks) | |
| await asyncio.sleep(self.confirmation_polling_interval) | |
| except asyncio.CancelledError: | |
| raise | |
| except Exception as e: | |
| print(f"⚠️ [Sentry Conf] خطأ في حلقة التأكيد لـ {symbol}: {e}") | |
| await asyncio.sleep(10) | |
| async def _fetch_confirmation_trades(self, ex_id: str, exchange: ccxtasync.Exchange, symbol: str): | |
| try: | |
| if symbol not in exchange.markets: | |
| return | |
| since_timestamp = int((time.time() - 60) * 1000) | |
| trades = await exchange.fetch_trades(symbol, since=since_timestamp, limit=50) | |
| if symbol in self.tactical_data_cache: | |
| trades.sort(key=lambda x: x['timestamp']) | |
| for trade in trades: | |
| self.tactical_data_cache[symbol].add_confirmation_trade(ex_id, trade) | |
| # 🔴 --- START OF CHANGE (V7.6) --- 🔴 | |
| # (التعامل مع الأخطاء الشائعة هنا أيضاً) | |
| except RateLimitExceeded: | |
| print(f"⏳ [Sentry Conf] {ex_id} Rate Limit لـ {symbol}. الانتظار (30 ثانية)...") | |
| await asyncio.sleep(30) | |
| except RequestTimeout: | |
| print(f"⏳ [Sentry Conf] {ex_id} Timeout لـ {symbol}. الانتظار (15 ثانية)...") | |
| await asyncio.sleep(15) | |
| # 🔴 --- END OF CHANGE --- 🔴 | |
| except asyncio.CancelledError: | |
| raise | |
| except Exception as e: | |
| # (نتجاهل الأخطاء بصمت هنا حتى لا نوقف الحلقة الرئيسية) | |
| pass | |
| async def _run_tactical_analysis_loop(self, symbol: str, strategy_hint: str): | |
| """(محدث V7.5) (دماغ الحارس) يشغل التحليل التكتيكي + ATR Trailing""" | |
| while self.is_running: | |
| await asyncio.sleep(1) | |
| try: | |
| all_weights = await self._get_or_refresh_weights() | |
| if self.state_manager.trade_analysis_lock.locked(): continue | |
| trade = await self.get_trade_by_symbol(symbol) | |
| tactical_data = self.tactical_data_cache.get(symbol) | |
| if not tactical_data: continue | |
| snapshot = tactical_data.get_tactical_snapshot() | |
| if trade: | |
| # (تحديد السعر الحالي بدقة) | |
| current_price = None | |
| if tactical_data.order_book and tactical_data.order_book.get('bids') and len(tactical_data.order_book['bids']) > 0: | |
| current_price = tactical_data.order_book['bids'][0][0] | |
| elif tactical_data.trades: | |
| try: | |
| current_price = tactical_data.trades[-1].get('price') | |
| except (IndexError, AttributeError): | |
| pass | |
| if not current_price: continue # لا يمكن المتابعة بدون سعر | |
| # 1. التحقق من SL/TP (الأولوية القصوى) | |
| exit_reason = self._check_exit_trigger(trade, snapshot, tactical_data) | |
| # 2. (جديد V7.5) التحقق من وقف الخسارة المتحرك (ATR Trailing) | |
| if not exit_reason: | |
| exit_reason = await self._check_atr_trailing_stop( | |
| trade, | |
| snapshot, | |
| current_price | |
| ) | |
| # 3. إذا لم يتم الخروج، تحقق من "مراقب حماية الأرباح" 5m | |
| if not exit_reason and tactical_data.new_5m_data_added: | |
| analysis_result = await self._run_5m_profit_saver( | |
| trade, | |
| list(tactical_data.ohlcv_5m), | |
| tactical_data, | |
| all_weights | |
| ) | |
| tactical_data.new_5m_data_added = False | |
| if analysis_result: | |
| decision = analysis_result.get("decision") | |
| score = analysis_result.get("score", 0) | |
| reason = analysis_result.get("reason", "N/A") | |
| if decision == "EXIT": | |
| exit_reason = f"Tactical 5m Profit Save: {reason} (Score: {score:.2f})" | |
| else: | |
| print(f" [Sentry 5m] {symbol} (Profit-Saver): {decision}. {reason} (Score: {score:.2f})") | |
| if exit_reason: | |
| print(f"🛑 [Sentry] زناد خروج لـ {symbol}: {exit_reason}") | |
| # (نستخدم السعر الحالي الذي حددناه مسبقاً) | |
| await self.immediate_close_trade(symbol, current_price, f"Exit Trigger: {exit_reason}") | |
| else: # (لا توجد صفقة مفتوحة) | |
| async with self.sentry_lock: | |
| is_still_on_watchlist = symbol in self.sentry_watchlist | |
| if is_still_on_watchlist: | |
| trigger = self._check_entry_trigger( | |
| symbol, | |
| strategy_hint, | |
| snapshot, | |
| all_weights | |
| ) | |
| if trigger: | |
| print(f"✅ [Sentry] زناد دخول تكتيكي لـ {symbol} (استراتيجية: {strategy_hint})") | |
| watchlist_entry = None | |
| async with self.sentry_lock: | |
| watchlist_entry = self.sentry_watchlist.pop(symbol, None) | |
| if watchlist_entry: | |
| explorer_context = watchlist_entry.get('llm_decision_context', {}) | |
| await self._execute_smart_entry( | |
| symbol, | |
| strategy_hint, | |
| snapshot, | |
| explorer_context | |
| ) | |
| except asyncio.CancelledError: | |
| raise | |
| except Exception as e: print(f"❌ [Sentry] خطأ في حلقة التحليل التكتيكي لـ {symbol}: {e}"); traceback.print_exc() | |
| def _check_entry_trigger(self, symbol: str, strategy_hint: str, data: Dict, all_weights: Dict) -> bool: | |
| """(محدث V7.3) زناد الدخول الموزون (يتعلم)""" | |
| weights = all_weights.get("entry_trigger_weights", { | |
| "cvd": 0.25, "order_book": 0.25, "ema_1m": 0.25, "macd_1m": 0.25 | |
| }) | |
| threshold = all_weights.get("entry_trigger_threshold", 0.75) | |
| if strategy_hint in ['breakout_momentum', 'trend_following']: | |
| cvd_score = data.get('cvd_score_1m', 0.0) | |
| ob_score = data.get('ob_analysis', {}).get('ob_score', 0.0) | |
| indicators_1m = data.get('indicators_1m', {}) | |
| if not indicators_1m: | |
| return False | |
| ema_score = indicators_1m.get('ema_score_1m', 0.0) | |
| macd_score = indicators_1m.get('macd_score_1m', 0.0) | |
| final_score = ( | |
| (cvd_score * weights.get('cvd', 0.25)) + | |
| (ob_score * weights.get('order_book', 0.25)) + | |
| (ema_score * weights.get('ema_1m', 0.25)) + | |
| (macd_score * weights.get('macd_1m', 0.25)) | |
| ) | |
| if final_score >= threshold: | |
| print(f" [Trigger V7.3] {symbol} (Score: {final_score:.2f} >= {threshold})") | |
| print(f" - CVD: {cvd_score:.2f} (w: {weights.get('cvd', 0.25)})") | |
| print(f" - OB: {ob_score:.2f} (w: {weights.get('order_book', 0.25)})") | |
| print(f" - EMA: {ema_score:.2f} (w: {weights.get('ema_1m', 0.25)})") | |
| print(f" - MACD: {macd_score:.2f} (w: {weights.get('macd_1m', 0.25)})") | |
| return True | |
| elif strategy_hint == 'mean_reversion': | |
| pass | |
| elif strategy_hint == 'volume_spike': | |
| large_trades = data.get('large_trades_count_5m', 0) | |
| if (large_trades > 0): | |
| print(f" [Trigger] {symbol} Volume Spike: LargeTrades={large_trades}") | |
| return True | |
| return False | |
| def _check_exit_trigger(self, trade: Dict, data: Dict, tactical_data: TacticalData) -> str: | |
| """(محدث V7.5) يراقب وقف الخسارة وجني الأرباح (يتجاهل الصفر)""" | |
| symbol = trade['symbol'] | |
| hard_stop = trade.get('stop_loss') | |
| take_profit = trade.get('take_profit') | |
| best_bid_price = None | |
| if tactical_data.order_book and tactical_data.order_book.get('bids') and len(tactical_data.order_book['bids']) > 0: | |
| best_bid_price = tactical_data.order_book['bids'][0][0] | |
| last_trade_price = None | |
| if tactical_data.trades: | |
| try: | |
| last_trade_price = tactical_data.trades[-1].get('price') | |
| except (IndexError, AttributeError): | |
| pass | |
| if best_bid_price is None and last_trade_price is None: | |
| return None | |
| current_price_for_sl = best_bid_price if best_bid_price is not None else last_trade_price | |
| current_price_for_tp = max( | |
| filter(None, [best_bid_price, last_trade_price]), | |
| default=None | |
| ) | |
| # (التحقق فقط إذا كانت القيمة أكبر من صفر) | |
| if hard_stop and hard_stop > 0 and current_price_for_sl and current_price_for_sl <= hard_stop: | |
| return f"Strategic Stop Loss hit: {current_price_for_sl} <= {hard_stop}" | |
| if take_profit and take_profit > 0 and current_price_for_tp and current_price_for_tp >= take_profit: | |
| return f"Strategic Take Profit hit: {current_price_for_tp} >= {take_profit}" | |
| return None | |
| async def _check_atr_trailing_stop(self, trade: Dict, snapshot: Dict, current_price: float) -> str: | |
| """(جديد V7.5) يحسب ويحدث وقف الخسارة المتحرك ATR""" | |
| exit_profile = trade.get('decision_data', {}).get('exit_profile') | |
| if exit_profile != 'ATR_TRAILING': | |
| return None # هذه الصفقة لا تستخدم ATR | |
| try: | |
| # 1. جلب الإعدادات | |
| params = trade.get('decision_data', {}).get('exit_parameters', {}) | |
| atr_multiplier = params.get('atr_multiplier', 3.0) | |
| # 2. جلب مؤشر ATR من إطار 1h | |
| atr_1h = snapshot.get('indicators_1h', {}).get('atr') | |
| if not atr_1h or atr_1h <= 0: | |
| # (لا يمكن الحساب بدون ATR، لا تفعل شيئاً) | |
| return None | |
| # 3. حساب وقف الخسارة المتحرك الجديد | |
| # (السعر الحالي - (قيمة ATR * المضاعف)) | |
| new_trailing_stop = current_price - (atr_1h * atr_multiplier) | |
| # 4. جلب وقف الخsارة الحالي (الديناميكي) | |
| current_dynamic_sl = trade.get('dynamic_stop_loss', 0) | |
| # 5. منطق التحديث (لا تخفض الوقف أبداً) | |
| if new_trailing_stop > current_dynamic_sl: | |
| # (وجدنا سعراً أفضل لوقف الخسارة، قم بتحديثه) | |
| print(f" [Sentry ATR] {trade['symbol']}: Raising Dynamic SL to {new_trailing_stop:.6f} (from {current_dynamic_sl:.6f})") | |
| await self._update_trade_dynamic_sl_in_r2(trade['id'], new_trailing_stop) | |
| # (تحقق فوراً إذا كان السعر الحالي قد كسره) | |
| if current_price <= new_trailing_stop: | |
| return f"ATR Trailing Stop hit: {current_price} <= {new_trailing_stop}" | |
| # (تحقق إذا كان السعر قد كسر الوقف القديم) | |
| elif current_dynamic_sl > 0 and current_price <= current_dynamic_sl: | |
| return f"ATR Trailing Stop hit: {current_price} <= {current_dynamic_sl}" | |
| return None # (لم يتم كسر الوقف) | |
| except Exception as e: | |
| print(f"❌ [Sentry ATR] {trade['symbol']}: Error calculating ATR stop: {e}") | |
| return None | |
| def _create_dataframe_5m(self, candles: List) -> pd.DataFrame: | |
| """(جديد V7.0) دالة مساعدة لإنشاء DataFrame لتحليل 5m""" | |
| try: | |
| if not candles: return pd.DataFrame() | |
| df = pd.DataFrame(candles, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume']) | |
| df[['open', 'high', 'low', 'close', 'volume']] = df[['open', 'high', 'low', 'close', 'volume']].astype(float) | |
| df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms') | |
| df.set_index('timestamp', inplace=True) | |
| df.sort_index(inplace=True) | |
| return df | |
| except Exception: | |
| return pd.DataFrame() | |
| async def _run_5m_profit_saver(self, trade: Dict, ohlcv_5m_list: List, tactical_data: TacticalData, all_weights: Dict) -> Dict: | |
| """ | |
| (محدث V7.3) "مراقب الانعكاس" 5m. | |
| - يستخدم الآن الأوزان المتكيفة من محور التعلم. | |
| """ | |
| dynamic_weights = all_weights.get("reversal_indicator_weights", { | |
| "pattern": 0.4, "rsi": 0.3, "macd": 0.3 | |
| }) | |
| reversal_threshold = 0.70 | |
| try: | |
| best_bid_price = None | |
| if tactical_data.order_book and tactical_data.order_book.get('bids') and len(tactical_data.order_book['bids']) > 0: | |
| best_bid_price = tactical_data.order_book['bids'][0][0] | |
| if best_bid_price is None: | |
| return None | |
| entry_price = trade.get('entry_price') | |
| is_profitable = best_bid_price > entry_price | |
| if len(ohlcv_5m_list) < 26: | |
| return None | |
| df_5m = self._create_dataframe_5m(ohlcv_5m_list) | |
| if df_5m.empty: | |
| return None | |
| indicators_5m = self.sentry_technical_analyzer.calculate_all_indicators(df_5m, '5m') | |
| pattern_analysis_5m = await self.sentry_pattern_analyzer.detect_chart_patterns({'5m': ohlcv_5m_list}) | |
| weights = dynamic_weights | |
| pattern_score = 0.0 | |
| pattern_name = pattern_analysis_5m.get('pattern_detected', '') | |
| pattern_conf = pattern_analysis_5m.get('pattern_confidence', 0) | |
| if pattern_name in ['Double Top', 'Downtrend', 'Breakout Down', 'Near Resistance', 'Bearish Pattern'] and pattern_conf > 0.5: | |
| pattern_score = pattern_conf | |
| rsi_score = 0.0 | |
| rsi_5m = indicators_5m.get('rsi', 50) | |
| if rsi_5m < 50: | |
| rsi_score = min(1.0, (50 - rsi_5m) / 20.0) | |
| macd_score = 0.0 | |
| macd_hist_5m = indicators_5m.get('macd_hist', 0) | |
| if macd_hist_5m < 0: | |
| current_price = df_5m['close'].iloc[-1] | |
| if current_price > 0: | |
| normalized_macd_hist = abs(macd_hist_5m) / current_price | |
| macd_score = min(1.0, normalized_macd_hist / 0.001) | |
| reversal_score = ( | |
| (pattern_score * weights.get('pattern', 0.4)) + | |
| (rsi_score * weights.get('rsi', 0.3)) + | |
| (macd_score * weights.get('macd', 0.3)) | |
| ) | |
| if reversal_score >= reversal_threshold: | |
| if is_profitable: | |
| return {"decision": "EXIT", "score": reversal_score, "threshold": reversal_threshold, "reason": "Reversal signal detected and trade is profitable"} | |
| else: | |
| return {"decision": "HOLD", "score": reversal_score, "threshold": reversal_threshold, "reason": "Reversal signal detected, but trade is not profitable"} | |
| else: | |
| return {"decision": "HOLD", "score": reversal_score, "threshold": reversal_threshold, "reason": "Trend intact / Reversal score low"} | |
| except Exception as e: | |
| print(f"❌ [Sentry] خطأ في مراقب حماية الأرباح 5m: {e}") | |
| return {"decision": "HOLD", "score": 0.0, "threshold": reversal_threshold, "reason": f"Error in 5m analysis: {e}"} | |
| async def _execute_smart_entry(self, symbol: str, strategy_hint: str, tactical_data: Dict, explorer_context: Dict): | |
| """ | |
| (محدث V7.3) | |
| يحاكي تنفيذ الصفقة ويحفظها في R2 (مع سياق القرار للتعليم السريع). | |
| """ | |
| print(f"🚀 [Executor] بدء تنفيذ الدخول الذكي (وهمي) لـ {symbol}...") | |
| context_for_retry = explorer_context | |
| if self.state_manager.trade_analysis_lock.locked(): | |
| print(f"⚠️ [Executor] تم إلغاء الدخول لـ {symbol} بسبب قفل التحليل الاستراتيجي."); | |
| return | |
| if not self.r2_service.acquire_lock(): | |
| print(f"⚠️ [Executor] فشل في الحصول على قفل R2 لـ {symbol}. تم الإلغاء."); | |
| return | |
| try: | |
| if await self.get_trade_by_symbol(symbol): | |
| print(f"ℹ️ [Executor] الصفقة {symbol} مفتوحة بالفعل (وهمياً). تم الإلغاء."); | |
| return | |
| all_open_trades = await self.get_open_trades() | |
| if len(all_open_trades) > 0: | |
| print(f"❌ [Executor] يوجد صفقة أخرى مفتوحة ({all_open_trades[0]['symbol']}). لا يمكن فتح {symbol}."); | |
| return | |
| portfolio_state = await self.r2_service.get_portfolio_state_async() | |
| available_capital = portfolio_state.get("current_capital_usd", 0) | |
| if available_capital < 1: | |
| print(f"❌ [Executor] رأس مال وهمي غير كافٍ لـ {symbol}."); | |
| return | |
| current_ask_price = None | |
| if symbol in self.tactical_data_cache and self.tactical_data_cache[symbol].order_book: | |
| ob = self.tactical_data_cache[symbol].order_book | |
| if ob and ob.get('asks') and len(ob['asks']) > 0: | |
| current_ask_price = ob['asks'][0][0] | |
| if not current_ask_price: | |
| print(f"❌ [Executor] لا يمكن الحصول على السعر الحالي (من البيانات العامة) لـ {symbol}."); | |
| return | |
| llm_decision = explorer_context.get('decision', {}) | |
| stop_loss_price = llm_decision.get("stop_loss", current_ask_price * 0.98) | |
| take_profit_price = llm_decision.get("take_profit", current_ask_price * 1.03) | |
| exit_profile = llm_decision.get('exit_profile', 'ATR_TRAILING') | |
| exit_parameters = llm_decision.get('exit_parameters', {}) | |
| if not (stop_loss_price and take_profit_price): | |
| print(f"❌ [Executor] {symbol}: بيانات SL/TP غير صالحة من النموذج. تم الإلغاء.") | |
| return | |
| if current_ask_price >= take_profit_price: | |
| print(f"⚠️ [Executor] {symbol}: السعر الحالي ({current_ask_price}) أعلى من هدف الربح ({take_profit_price}). الفرصة ضاعت. تم الإلغاء.") | |
| return | |
| if current_ask_price <= stop_loss_price: | |
| print(f"⚠️ [Executor] {symbol}: السعر الحالي ({current_ask_price}) أقل من وقف الخسارة ({stop_loss_price}). الصفقة فاشلة. تم الإلغاء.") | |
| return | |
| final_entry_price = current_ask_price | |
| print(f"✅ [Executor] (SIMULATED) تم التنفيذ! {symbol} بسعر {final_entry_price}") | |
| indicators_at_decision = tactical_data.get('indicators_1m', {}) | |
| market_context_at_decision = explorer_context.get('full_candidate_data', {}).get('sentiment_data', {}) | |
| if 'full_candidate_data' in explorer_context: | |
| if 'ohlcv' in explorer_context['full_candidate_data']: | |
| del explorer_context['full_candidate_data']['ohlcv'] | |
| if 'raw_ohlcv' in explorer_context['full_candidate_data']: | |
| del explorer_context['full_candidate_data']['raw_ohlcv'] | |
| await self._save_trade_to_r2( | |
| symbol=symbol, entry_price=final_entry_price, position_size_usd=available_capital, | |
| strategy=strategy_hint, exit_profile=exit_profile, exit_parameters=exit_parameters, | |
| stop_loss=stop_loss_price, take_profit=take_profit_price, | |
| tactical_context=tactical_data, | |
| explorer_context=explorer_context, | |
| market_context_at_decision=market_context_at_decision, | |
| indicators_at_decision=indicators_at_decision | |
| ) | |
| print(f" [Executor] الصفقة {symbol} فُتحت. مسح باقي قائمة المراقبة (Watchlist)...") | |
| async with self.sentry_lock: | |
| self.sentry_watchlist.clear() | |
| print(" [Sentry] تم مسح Watchlist.") | |
| except Exception as e: | |
| print(f"❌ [Executor] فشل فادح أثناء التنفيذ (SIM) لـ {symbol}: {e}"); | |
| traceback.print_exc() | |
| print(f" [Sentry] إعادة {symbol} إلى Watchlist بعد فشل التنفيذ الوهمي.") | |
| async with self.sentry_lock: | |
| self.sentry_watchlist[symbol] = { | |
| "symbol": symbol, | |
| "strategy_hint": strategy_hint, | |
| "llm_decision_context": context_for_retry | |
| } | |
| finally: | |
| if self.r2_service.lock_acquired: | |
| self.r2_service.release_lock() | |
| async def _save_trade_to_r2(self, **kwargs): | |
| """ | |
| (محدث V7.3) | |
| يحفظ بيانات الصفقة الوهمية، متضمناً سياق القرار للتعليم السريع. | |
| """ | |
| try: | |
| symbol = kwargs.get('symbol') | |
| strategy = kwargs.get('strategy') | |
| exit_profile = kwargs.get('exit_profile') | |
| expected_target_time = (datetime.now() + timedelta(minutes=15)).isoformat() | |
| explorer_context_blob = kwargs.get('explorer_context', {}) | |
| llm_decision_only = explorer_context_blob.get('decision', {}) | |
| decision_data = { | |
| "reasoning": f"Tactical entry by Sentry based on {strategy}", | |
| "strategy": strategy, | |
| "exit_profile": exit_profile, | |
| "exit_parameters": kwargs.get('exit_parameters', {}), | |
| "tactical_context_at_decision": kwargs.get('tactical_context', {}), | |
| "explorer_llm_decision": llm_decision_only, | |
| "market_context_at_decision": kwargs.get('market_context_at_decision', {}), | |
| "indicators_at_decision": kwargs.get('indicators_at_decision', {}) | |
| } | |
| new_trade = { | |
| "id": str(int(datetime.now().timestamp())), | |
| "symbol": symbol, | |
| "entry_price": kwargs.get('entry_price'), | |
| "entry_timestamp": datetime.now().isoformat(), | |
| "decision_data": decision_data, | |
| "status": "OPEN", | |
| "stop_loss": kwargs.get('stop_loss'), | |
| "take_profit": kwargs.get('take_profit'), | |
| # (يتم تعيين الوقف الديناميكي الأولي بنفس قيمة الوقف الثابت) | |
| "dynamic_stop_loss": kwargs.get('stop_loss'), | |
| "trade_type": "LONG", | |
| "position_size_usd": kwargs.get('position_size_usd'), | |
| "expected_target_minutes": 15, | |
| "expected_target_time": expected_target_time, | |
| "is_monitored": True, | |
| "strategy": strategy, | |
| "monitoring_started": True | |
| } | |
| trades = await self.r2_service.get_open_trades_async() | |
| trades.append(new_trade) | |
| await self.r2_service.save_open_trades_async(trades) | |
| portfolio_state = await self.r2_service.get_portfolio_state_async() | |
| portfolio_state["invested_capital_usd"] = kwargs.get('position_size_usd') | |
| portfolio_state["current_capital_usd"] = 0.0 | |
| portfolio_state["total_trades"] = portfolio_state.get("total_trades", 0) + 1 | |
| await self.r2_service.save_portfolio_state_async(portfolio_state) | |
| await self.r2_service.save_system_logs_async({ | |
| "new_trade_opened_by_sentry": True, "symbol": symbol, | |
| "position_size": kwargs.get('position_size_usd'), | |
| "strategy": strategy, "exit_profile": exit_profile | |
| }) | |
| print(f"✅ [R2] تم حفظ الصفقة الجديدة (الوهمية) لـ {symbol} بنجاح (مع سياق التعليم).") | |
| except Exception as e: | |
| print(f"❌ [R2] فشل حفظ الصفقة لـ {symbol}: {e}"); | |
| traceback.print_exc() | |
| raise | |
| async def close_trade(self, trade_to_close, close_price, reason="System Close"): | |
| try: | |
| symbol = trade_to_close.get('symbol'); trade_to_close['status'] = 'CLOSED' | |
| trade_to_close['close_price'] = close_price; trade_to_close['close_timestamp'] = datetime.now().isoformat() | |
| trade_to_close['is_monitored'] = False; entry_price = trade_to_close['entry_price'] | |
| position_size = trade_to_close['position_size_usd']; strategy = trade_to_close.get('strategy', 'unknown') | |
| pnl = 0.0; pnl_percent = 0.0 | |
| if entry_price and entry_price > 0 and close_price and close_price > 0: | |
| try: pnl_percent = ((close_price - entry_price) / entry_price) * 100; pnl = position_size * (pnl_percent / 100) | |
| except (TypeError, ZeroDivisionError): pnl = 0.0; pnl_percent = 0.0 | |
| trade_to_close['pnl_usd'] = pnl; trade_to_close['pnl_percent'] = pnl_percent | |
| try: | |
| entry_dt = datetime.fromisoformat(trade_to_close['entry_timestamp']) | |
| close_dt = datetime.fromisoformat(trade_to_close['close_timestamp']) | |
| duration_minutes = (close_dt - entry_dt).total_seconds() / 60 | |
| trade_to_close['hold_duration_minutes'] = round(duration_minutes, 2) | |
| except Exception: | |
| trade_to_close['hold_duration_minutes'] = 'N/A' | |
| await self._archive_closed_trade(trade_to_close); await self._update_trade_summary(trade_to_close) | |
| portfolio_state = await self.r2_service.get_portfolio_state_async() | |
| current_capital = portfolio_state.get("current_capital_usd", 0); new_capital = current_capital + position_size + pnl | |
| portfolio_state["current_capital_usd"] = new_capital; portfolio_state["invested_capital_usd"] = 0.0 | |
| if pnl > 0: portfolio_state["winning_trades"] = portfolio_state.get("winning_trades", 0) + 1; portfolio_state["total_profit_usd"] = portfolio_state.get("total_profit_usd", 0.0) + pnl | |
| elif pnl < 0: portfolio_state["total_loss_usd"] = portfolio_state.get("total_loss_usd", 0.0) + abs(pnl) | |
| await self.r2_service.save_portfolio_state_async(portfolio_state) | |
| open_trades = await self.r2_service.get_open_trades_async() | |
| trades_to_keep = [t for t in open_trades if t.get('id') != trade_to_close.get('id')] | |
| await self.r2_service.save_open_trades_async(trades_to_keep) | |
| await self.r2_service.save_system_logs_async({ | |
| "trade_closed": True, "symbol": symbol, "pnl_usd": pnl, "pnl_percent": pnl_percent, | |
| "new_capital": new_capital, "strategy": strategy, "reason": reason | |
| }) | |
| if self.learning_hub and self.learning_hub.initialized: | |
| print(f"🧠 [LearningHub] تشغيل التعلم (Reflector+Stats) لـ {symbol}...") | |
| await self.learning_hub.analyze_trade_and_learn(trade_to_close, reason) | |
| else: print("⚠️ [Sentry] LearningHub غير متاح، تم تخطي التعلم.") | |
| if self.callback_on_close: | |
| print("🔄 [Executor] Trade closed. Scheduling immediate Explorer cycle...") | |
| asyncio.create_task(self.callback_on_close()) | |
| print(f"✅ [Executor] تم إغلاق الصفقة (الوهمية) {symbol} - السبب: {reason} - PnL: {pnl_percent:+.2f}%") | |
| return True | |
| except Exception as e: print(f"❌ [Executor] فشل فادح أثناء إغلاق الصفقة (الوهمية) {symbol}: {e}"); traceback.print_exc(); raise | |
| async def immediate_close_trade(self, symbol, close_price, reason="Immediate Close"): | |
| if not self.r2_service.acquire_lock(): print(f"⚠️ [Executor] فشل في الحصول على قفل R2 لـ {symbol} (Immediate Close)"); return False | |
| try: | |
| open_trades = await self.r2_service.get_open_trades_async() | |
| trade_to_close = next((t for t in open_trades if t['symbol'] == symbol and t['status'] == 'OPEN'), None) | |
| if not trade_to_close: print(f"⚠️ [Executor] لا توجد صفقة مفتوحة لـ {symbol} لإغلاقها."); return False | |
| await self.close_trade(trade_to_close, close_price, reason) | |
| return True | |
| except Exception as e: print(f"❌ [Executor] فشل في immediate_close {symbol}: {e}"); return False | |
| finally: | |
| if self.r2_service.lock_acquired: self.r2_service.release_lock() | |
| async def update_trade_strategy(self, trade_to_update, re_analysis_decision): | |
| """ | |
| (محدث V7.5) | |
| تحديث استراتيجية الصفقة بذكاء، مع تجنب مسح قيم SL/TP. | |
| """ | |
| try: | |
| symbol = trade_to_update.get('symbol') | |
| if re_analysis_decision.get('action') == "UPDATE_TRADE": | |
| # (الإصلاح 1: التحقق من new_stop_loss) | |
| new_sl = re_analysis_decision.get('new_stop_loss') | |
| if new_sl and isinstance(new_sl, (int, float)) and new_sl > 0: | |
| trade_to_update['stop_loss'] = new_sl | |
| trade_to_update['dynamic_stop_loss'] = new_sl # (إعادة ضبط الوقف المتحرك أيضاً) | |
| # (الإصلاح 2: التحقق من new_take_profit) | |
| new_tp = re_analysis_decision.get('new_take_profit') | |
| if new_tp and isinstance(new_tp, (int, float)) and new_tp > 0: | |
| trade_to_update['take_profit'] = new_tp | |
| trade_to_update['decision_data']['exit_profile'] = re_analysis_decision['new_exit_profile'] | |
| trade_to_update['decision_data']['exit_parameters'] = re_analysis_decision['new_exit_parameters'] | |
| print(f" 🔄 (Explorer) {symbol}: Exit profile updated to {re_analysis_decision['new_exit_profile']}") | |
| # (تحديث الوقت والسبب دائماً) | |
| new_expected_minutes = re_analysis_decision.get('new_expected_minutes', 15) | |
| trade_to_update['expected_target_minutes'] = new_expected_minutes | |
| trade_to_update['expected_target_time'] = (datetime.now() + timedelta(minutes=new_expected_minutes)).isoformat() | |
| trade_to_update['decision_data']['reasoning'] = re_analysis_decision.get('reasoning') | |
| # (حفظ التغييرات في R2) | |
| open_trades = await self.r2_service.get_open_trades_async() | |
| for i, trade in enumerate(open_trades): | |
| if trade.get('id') == trade_to_update.get('id'): | |
| open_trades[i] = trade_to_update | |
| break | |
| await self.r2_service.save_open_trades_async(open_trades) | |
| await self.r2_service.save_system_logs_async({"trade_strategy_updated": True, "symbol": symbol}) | |
| print(f"✅ (Explorer) تم تحديث الأهداف الاستراتيجية لـ {symbol}") | |
| return True | |
| except Exception as e: | |
| print(f"❌ (Explorer) فشل تحديث استراتيجية {symbol}: {e}"); | |
| raise | |
| async def _update_trade_dynamic_sl_in_r2(self, trade_id: str, new_dynamic_sl: float): | |
| """(جديد V7.5) دالة مساعدة لتحديث الوقف الديناميكي فقط في R2""" | |
| try: | |
| # (لا نحتاج قفل هنا لأننا داخل حلقة الحارس المحمية) | |
| open_trades = await self.r2_service.get_open_trades_async() | |
| trade_found = False | |
| for i, trade in enumerate(open_trades): | |
| if trade.get('id') == trade_id: | |
| trade['dynamic_stop_loss'] = new_dynamic_sl | |
| open_trades[i] = trade | |
| trade_found = True | |
| break | |
| if trade_found: | |
| await self.r2_service.save_open_trades_async(open_trades) | |
| except Exception as e: | |
| print(f"❌ [Sentry ATR] فشل حفظ الوقف المتحرك الجديد في R2: {e}") | |
| async def _archive_closed_trade(self, closed_trade): | |
| try: | |
| key = "closed_trades_history.json"; history = [] | |
| try: response = self.r2_service.s3_client.get_object(Bucket="trading", Key=key); history = json.loads(response['Body'].read()) | |
| except Exception: pass | |
| history.append(closed_trade); history = history[-1000:] | |
| data_json = json.dumps(history, indent=2).encode('utf-8') | |
| self.r2_service.s3_client.put_object(Bucket="trading", Key=key, Body=data_json, ContentType="application/json") | |
| except Exception as e: print(f"❌ Failed to archive trade: {e}") | |
| async def _update_trade_summary(self, closed_trade): | |
| try: | |
| key = "trade_summary.json"; summary = {"total_trades": 0, "winning_trades": 0, "losing_trades": 0, "total_profit_usd": 0.0, "total_loss_usd": 0.0, "win_percentage": 0.0, "avg_profit_per_trade": 0.0, "avg_loss_per_trade": 0.0, "largest_win": 0.0, "largest_loss": 0.0, "last_updated": datetime.now().isoformat()} | |
| try: response = self.r2_service.s3_client.get_object(Bucket="trading", Key=key); summary = json.loads(response['Body'].read()) | |
| except Exception: pass | |
| pnl = closed_trade.get('pnl_usd', 0.0); summary['total_trades'] += 1 | |
| if pnl >= 0: summary['winning_trades'] += 1; summary['total_profit_usd'] += pnl; summary['largest_win'] = max(summary.get('largest_win', 0), pnl) | |
| else: summary['losing_trades'] += 1; summary['total_loss_usd'] += abs(pnl); summary['largest_loss'] = max(summary.get('largest_loss', 0), abs(pnl)) | |
| if summary['total_trades'] > 0: summary['win_percentage'] = (summary['winning_trades'] / summary['total_trades']) * 100 | |
| if summary['winning_trades'] > 0: summary['avg_profit_per_trade'] = summary['total_profit_usd'] / summary['winning_trades'] | |
| if summary['losing_trades'] > 0: summary['avg_loss_per_trade'] = summary['total_loss_usd'] / summary['losing_trades'] | |
| summary['last_updated'] = datetime.now().isoformat() | |
| data_json = json.dumps(summary, indent=2).encode('utf-8') | |
| self.r2_service.s3_client.put_object(Bucket="trading", Key=key, Body=data_json, ContentType="application/json") | |
| except Exception as e: print(f"❌ Failed to update trade summary: {e}") | |
| async def get_open_trades(self): | |
| try: return await self.r2_service.get_open_trades_async() | |
| except Exception as e: print(f"❌ Failed to get open trades: {e}"); return [] | |
| async def get_trade_by_symbol(self, symbol): | |
| try: | |
| open_trades = await self.get_open_trades() | |
| return next((t for t in open_trades if t['symbol'] == symbol and t['status'] == 'OPEN'), None) | |
| except Exception as e: print(f"❌ Failed to get trade by symbol {symbol}: {e}"); return None | |
| print(f"✅ Trade Manager loaded - V7.6 (Exponential Backoff) (ccxt.async_support: {CCXT_ASYNC_AVAILABLE})") |