Riy777 commited on
Commit
29b8ecb
·
1 Parent(s): 3fd2d9a

Update trade_manager.py

Browse files
Files changed (1) hide show
  1. trade_manager.py +102 -124
trade_manager.py CHANGED
@@ -1,4 +1,4 @@
1
- # trade_manager.py (Updated to V6.5 - Added Sanity Check to Executor)
2
  import asyncio
3
  import json
4
  import time
@@ -6,10 +6,9 @@ import traceback
6
  import os
7
  from datetime import datetime, timedelta
8
  from typing import Dict, Any, List
9
- from collections import deque, defaultdict # (إضافة defaultdict)
10
 
11
  try:
12
- # (نستخدم async_support لوظائف fetch غير المتزامنة)
13
  import ccxt.async_support as ccxtasync
14
  CCXT_ASYNC_AVAILABLE = True
15
  except ImportError:
@@ -20,7 +19,6 @@ except ImportError:
20
  import numpy as np
21
  from helpers import safe_float_conversion
22
 
23
- # (TacticalData - لا تغيير)
24
  class TacticalData:
25
  """
26
  (محدث) لتخزين بيانات تأكيد من مصادر متعددة بدلاً من Binance فقط.
@@ -34,7 +32,6 @@ class TacticalData:
34
  self.one_min_rsi = 50.0
35
  self.last_update = time.time()
36
 
37
- # (بيانات التأكيد الجديدة)
38
  self.confirmation_trades = defaultdict(lambda: deque(maxlen=50))
39
  self.confirmation_cvd = defaultdict(float)
40
 
@@ -134,35 +131,31 @@ class TradeManager:
134
  self.sentry_tasks = {}
135
  self.tactical_data_cache = {}
136
 
137
- self.sentry_lock = asyncio.Lock() # (إصلاح V5.8)
138
 
139
- self.kucoin_rest = None # (المنصة الأساسية)
140
- self.confirmation_exchanges = {} # (المنصات الثانوية للتأكيد)
141
- self.polling_interval = 1.5 # (KuCoin - سريع)
142
- self.confirmation_polling_interval = 3.0 # (منصات التأكيد - أبطأ قليلاً)
143
 
144
- async def initialize_sentry_exchanges(self): # (محدث V6.1)
145
  """
146
  (محدث V6.1) تهيئة KuCoin (أساسي) ومنصات تأكيد متعددة (ثانوية).
147
- (تمت إزالة المصادقة - الوضع الوهمي)
148
  """
149
  try:
150
  print("🔄 [Sentry] تهيئة منصات التداول (KuCoin REST ومنصات التأكيد)...")
151
 
152
- # 1. تهيئة KuCoin (الأساسي) - (بدون مصادقة، للبيانات العامة فقط)
153
  print(" [Sentry] تهيئة KuCoin للبيانات العامة (وضع المحاكاة).")
154
  self.kucoin_rest = ccxtasync.kucoin()
155
  await self.kucoin_rest.load_markets()
156
  print("✅ [Sentry] منصة REST الأساسية (KuCoin) جاهزة (بيانات عامة فقط).")
157
 
158
- # 2. تهيئة منصات التأكيد (الثانوية)
159
  self.confirmation_exchanges = {}
160
  confirmation_exchange_ids = ['bybit', 'okx', 'gateio']
161
 
162
  print(f" [Sentry] تهيئة منصات التأكيد (Confirmation Exchanges): {', '.join(confirmation_exchange_ids)}")
163
  for ex_id in confirmation_exchange_ids:
164
  try:
165
- # (التهيئة بدون مفاتيح API للبيانات العامة)
166
  exchange = getattr(ccxtasync, ex_id)()
167
  await exchange.load_markets()
168
  self.confirmation_exchanges[ex_id] = exchange
@@ -185,37 +178,31 @@ class TradeManager:
185
  for ex in self.confirmation_exchanges.values(): await ex.close()
186
  raise
187
 
188
- async def start_sentry_and_monitoring_loops(self): # (محدث V5.8)
189
  """الحلقة الرئيسية للحارس (Sentry) ومراقب الخروج (Exit Monitor)."""
190
  self.is_running = True
191
  print(f"✅ [Sentry] بدء حلقات المراقبة التكتيكية (Layer 2 - API Polling)...")
192
  while self.is_running:
193
  try:
194
- # الحصول على قائمة الرموز المطلوب مراقبتها (داخل القفل)
195
  async with self.sentry_lock:
196
  watchlist_symbols = set(self.sentry_watchlist.keys())
197
 
198
- # (جلب الصفقات المفتوحة - هذا I/O، نتركه خارج القفل)
199
  open_trades = await self.get_open_trades()
200
  open_trade_symbols = {t['symbol'] for t in open_trades}
201
 
202
  symbols_to_monitor = watchlist_symbols.union(open_trade_symbols)
203
  current_tasks = set(self.sentry_tasks.keys())
204
 
205
- # الرموز التي يجب إضافتها
206
  symbols_to_add = symbols_to_monitor - current_tasks
207
  for symbol in symbols_to_add:
208
  print(f" [Sentry] بدء المراقبة التكتيكية (Polling) لـ {symbol}")
209
 
210
- # (الحصول على التلميح داخل القفل لضمان عدم تغييره أثناء القراءة)
211
- strategy_hint = 'generic' # قيمة افتراضية
212
  if symbol in watchlist_symbols:
213
  async with self.sentry_lock:
214
- # (التأكد من أن الرمز لا يزال موجوداً بعد تحرير القفل)
215
  if symbol in self.sentry_watchlist:
216
  strategy_hint = self.sentry_watchlist.get(symbol, {}).get('strategy_hint', 'generic')
217
  elif symbol in open_trade_symbols:
218
- # (إذا كانت صفقة مفتوحة، احصل على الاستراتيجية من بيانات الصفقة)
219
  trade = next((t for t in open_trades if t['symbol'] == symbol), None)
220
  if trade:
221
  strategy_hint = trade.get('strategy', 'generic')
@@ -226,7 +213,6 @@ class TradeManager:
226
  task = asyncio.create_task(self._monitor_symbol_activity_polling(symbol, strategy_hint))
227
  self.sentry_tasks[symbol] = task
228
 
229
- # الرموز التي يجب إزالتها
230
  symbols_to_remove = current_tasks - symbols_to_monitor
231
  for symbol in symbols_to_remove:
232
  print(f" [Sentry] إيقاف المراقبة التكتيكية (Polling) لـ {symbol}")
@@ -236,13 +222,13 @@ class TradeManager:
236
  if symbol in self.tactical_data_cache:
237
  del self.tactical_data_cache[symbol]
238
 
239
- await asyncio.sleep(15) # فاصل زمني للحلقة الرئيسية
240
 
241
  except Exception as error:
242
  print(f"❌ [Sentry] خطأ في الحلقة الرئيسية: {error}"); traceback.print_exc(); await asyncio.sleep(60)
243
 
244
  async def stop_sentry_loops(self):
245
- """(محدث) إيقاف جميع مهام المراقبة وإغلاق جميع اتصالات REST"""
246
  self.is_running = False
247
  print("🛑 [Sentry] إيقاف جميع حلقات المراقبة...")
248
  for task in self.sentry_tasks.values(): task.cancel()
@@ -262,15 +248,14 @@ class TradeManager:
262
  print("✅ [Sentry] تم إغلاق اتصالات التداول (REST).")
263
  except Exception as e: print(f"⚠️ [Sentry] خطأ أثناء إغلاق الاتصالات: {e}")
264
 
265
- async def update_sentry_watchlist(self, candidates: List[Dict]): # (محدث V5.8)
266
  """تحديث قائمة المراقبة التي يستخدمها الحارس (Sentry)."""
267
- async with self.sentry_lock: # (استخدام القفل قبل الكتابة)
268
  self.sentry_watchlist = {c['symbol']: c for c in candidates}
269
  print(f"ℹ️ [Sentry] تم تحديث Watchlist. عدد المرشحين: {len(self.sentry_watchlist)}")
270
 
271
  def get_sentry_status(self):
272
- """(محدث) لواجهة برمجة التطبيقات /system-status"""
273
- # (لا حاجة للقفل هنا، هذه قراءة سريعة لعدد العناصر فقط)
274
  active_monitoring_count = len(self.sentry_tasks)
275
  watchlist_symbols_list = list(self.sentry_watchlist.keys())
276
 
@@ -284,16 +269,15 @@ class TradeManager:
284
 
285
  async def _monitor_symbol_activity_polling(self, symbol: str, strategy_hint: str):
286
  """
287
- (محدث)
288
  يشغل حلقات Polling متوازية (KuCoin + منصات التأكيد) ويشغل منطق التحليل.
289
  """
290
  if symbol not in self.tactical_data_cache:
291
  self.tactical_data_cache[symbol] = TacticalData(symbol)
292
 
293
  tasks_to_gather = [
294
- self._poll_kucoin_data(symbol), # (الأساسي)
295
- self._poll_confirmation_data(symbol), # (الجديد - للتأكيد)
296
- self._run_tactical_analysis_loop(symbol, strategy_hint) # (الدماغ)
297
  ]
298
 
299
  try:
@@ -305,14 +289,13 @@ class TradeManager:
305
  traceback.print_exc()
306
  finally:
307
  print(f"🛑 [Sentry] إنهاء جميع مهام (Polling) {symbol}")
308
- # (التنظيف يحدث بالفعل في الحلقة الرئيسية، ولكن هذا ضمان إضافي)
309
  if symbol in self.sentry_tasks:
310
  self.sentry_tasks.pop(symbol, None)
311
  if symbol in self.tactical_data_cache:
312
  del self.tactical_data_cache[symbol]
313
 
314
  async def _poll_kucoin_data(self, symbol):
315
- """(لا تغيير) حلقة استقصاء (Polling) لبيانات KuCoin (الأساسية)"""
316
  while self.is_running:
317
  try:
318
  if not self.kucoin_rest:
@@ -326,7 +309,7 @@ class TradeManager:
326
  self.tactical_data_cache[symbol].set_order_book(ob)
327
 
328
  # 2. جلب آخر الصفقات
329
- since_timestamp = int((time.time() - 60) * 1000) # (آخر 60 ثانية)
330
  trades = await self.kucoin_rest.fetch_trades(symbol, since=since_timestamp, limit=50)
331
  if symbol in self.tactical_data_cache:
332
  trades.sort(key=lambda x: x['timestamp'])
@@ -339,7 +322,7 @@ class TradeManager:
339
  print(f"⏳ [Sentry Polling] {symbol} KuCoin Rate Limit Exceeded: {e}. زيادة فترة الانتظار...")
340
  await asyncio.sleep(10)
341
  except asyncio.CancelledError:
342
- raise # تمرير الإلغاء
343
  except Exception as e:
344
  print(f"⚠️ [Sentry Polling] خطأ في {symbol} KuCoin data polling: {e}")
345
  await asyncio.sleep(5)
@@ -348,59 +331,50 @@ class TradeManager:
348
  """(جديد) حلقة استقصاء (Polling) لبيانات منصات التأكيد (Bybit, OKX, etc.)"""
349
  if not self.confirmation_exchanges:
350
  print(f" [Sentry Conf] {symbol} - لا توجد منصات تأكيد، سيتم الاعتماد على KuCoin فقط.")
351
- return # إنهاء المهمة إذا لم يتم تهيئة أي منصة
352
 
353
- # (ننتظر قليلاً قبل بدء حلقة التأكيد لتوزيع الحمل)
354
  await asyncio.sleep(self.confirmation_polling_interval / 2)
355
 
356
  while self.is_running:
357
  try:
358
  tasks = []
359
- # (إنشاء مهام لجلب البيانات من كل منصة تأكيد بالتوازي)
360
  for ex_id, exchange in self.confirmation_exchanges.items():
361
  tasks.append(self._fetch_confirmation_trades(ex_id, exchange, symbol))
362
 
363
  await asyncio.gather(*tasks)
364
-
365
- # (الانتظار قبل الدورة التالية)
366
  await asyncio.sleep(self.confirmation_polling_interval)
367
 
368
  except asyncio.CancelledError:
369
- raise # (تمرير الإلغاء للخارج)
370
  except Exception as e:
371
  print(f"⚠️ [Sentry Conf] خطأ في حلقة التأكيد لـ {symbol}: {e}")
372
- await asyncio.sleep(10) # انتظار عند حدوث خطأ عام في الحلقة
373
 
374
  async def _fetch_confirmation_trades(self, ex_id: str, exchange: ccxtasync.Exchange, symbol: str):
375
  """(جديد) دالة مساعدة لجلب الصفقات من منصة تأكيد واحدة"""
376
  try:
377
- # التحقق مما إذا كانت المنصة تدعم هذا الرمز
378
  if symbol not in exchange.markets:
379
- # print(f" [Sentry Conf] {ex_id} لا تدعم {symbol}. إزالة من المراقبة.")
380
- # (الأفضل هو مجرد التجاهل)
381
  return
382
 
383
- since_timestamp = int((time.time() - 60) * 1000) # (آخر 60 ثانية)
384
  trades = await exchange.fetch_trades(symbol, since=since_timestamp, limit=50)
385
 
386
  if symbol in self.tactical_data_cache:
387
  trades.sort(key=lambda x: x['timestamp'])
388
  for trade in trades:
389
- # (إضافة البيانات إلى الكاش المخصص)
390
  self.tactical_data_cache[symbol].add_confirmation_trade(ex_id, trade)
391
 
392
  except ccxtasync.RateLimitExceeded:
393
  print(f"⏳ [Sentry Conf] {ex_id} Rate Limit لـ {symbol}. الانتظار...")
394
- await asyncio.sleep(15) # انتظار أطول لهذه المنصة
395
  except asyncio.CancelledError:
396
- raise # تمرير الإلغاء
397
  except Exception as e:
398
- # (الفشل في منصة واحدة لا يجب أن يوقف الحلقة، فقط نتجاهل بياناتها هذه المرة)
399
  pass
400
 
401
 
402
  async def _run_tactical_analysis_loop(self, symbol: str, strategy_hint: str):
403
- """(محدث V6.4) (دماغ الحارس) يشغل التحليل التكتيكي كل ثانية."""
404
  while self.is_running:
405
  await asyncio.sleep(1) # (التحليل السريع كل ثانية)
406
  try:
@@ -412,22 +386,30 @@ class TradeManager:
412
  snapshot = tactical_data.get_tactical_snapshot()
413
 
414
  if trade:
415
- # 🔴 --- LOGIC CHANGE (V6.4) --- 🔴
416
- # (منطق الخروج الآن يتحقق فقط من SL/TP)
417
- exit_reason = self._check_exit_trigger(trade, snapshot)
418
  if exit_reason:
419
  print(f"🛑 [Sentry] زناد خروج استراتيجي لـ {symbol}: {exit_reason}")
420
- current_price = tactical_data.order_book['bids'][0][0] if tactical_data.order_book and tactical_data.order_book.get('bids') and len(tactical_data.order_book['bids']) > 0 else None
421
- if current_price:
422
- await self.immediate_close_trade(symbol, current_price, f"Strategic Exit: {exit_reason}")
 
 
 
 
 
 
423
  else:
424
- # (احتياطي: إذا فشل دفتر الطلبات، استخدم آخر سعر صفقة)
425
  if tactical_data.trades:
426
- current_price = tactical_data.trades[-1].get('price')
427
- if current_price:
428
- await self.immediate_close_trade(symbol, current_price, f"Strategic Exit (Fallback Price): {exit_reason}")
 
 
 
429
  else:
430
- # (منطق الدخول - محدث V5.8)
431
  async with self.sentry_lock:
432
  is_still_on_watchlist = symbol in self.sentry_watchlist
433
 
@@ -436,7 +418,6 @@ class TradeManager:
436
  if trigger:
437
  print(f"✅ [Sentry] زناد دخول تكتيكي لـ {symbol} (استراتيجية: {strategy_hint})")
438
 
439
- # (إزالة العنصر من قائمة المراقبة باستخدام القفل)
440
  watchlist_entry = None
441
  async with self.sentry_lock:
442
  watchlist_entry = self.sentry_watchlist.pop(symbol, None)
@@ -445,10 +426,10 @@ class TradeManager:
445
  explorer_context = watchlist_entry.get('llm_decision_context', {})
446
  await self._execute_smart_entry(symbol, strategy_hint, snapshot, explorer_context)
447
  except asyncio.CancelledError:
448
- raise # تمرير الإلغاء
449
  except Exception as e: print(f"❌ [Sentry] خطأ في حلقة التحليل التكتيكي لـ {symbol}: {e}"); traceback.print_exc()
450
 
451
- def _check_entry_trigger(self, symbol: str, strategy_hint: str, data: Dict) -> bool: # (محدث V5.9)
452
  """(محدث V5.9) إضافة trend_following إلى منطق الزناد."""
453
 
454
  rsi = data.get('rsi_1m_approx', 50)
@@ -458,15 +439,10 @@ class TradeManager:
458
  cvd_conf_agg = data.get('cvd_confirmation_aggregate', 0)
459
  cvd_conf_sources_count = len(data.get('cvd_confirmation_sources', {}))
460
 
461
- # (إصلاح V5.9: دمج trend_following مع breakout_momentum)
462
  if strategy_hint in ['breakout_momentum', 'trend_following']:
463
- # استراتيجية الزخم *تتطلب* تدفقاً إيجابياً
464
  if cvd_kucoin <= 0:
465
- return False # (لا يوجد زخم إذا كان CVD سلبياً)
466
-
467
- # (الشروط المخففة: RSI > 55)
468
  if (rsi > 55):
469
- # (الفيتو المخفf: نوقف فقط إذا كانت المنصات الأخرى تبيع ضدنا بقوة)
470
  if cvd_conf_sources_count > 0 and cvd_conf_agg < (cvd_kucoin * -0.5):
471
  print(f" [Trigger Hold] {symbol} Breakout/Trend: KuCoin CVD ({cvd_kucoin:.0f}) إيجابي، لكن منصات التأكيد ({cvd_conf_agg:.0f}) تبيع بقوة.")
472
  return False
@@ -475,52 +451,69 @@ class TradeManager:
475
  return True
476
 
477
  elif strategy_hint == 'mean_reversion':
478
- # استراتيجية الانعكاس *لا* تتطلب CVD إيجابي
479
- # (الشروط المخففة: RSI < 35)
480
  if (rsi < 35):
481
  print(f" [Trigger] {symbol} Reversion: RSI={rsi:.1f}, K_CVD={cvd_kucoin:.0f} (CVD check ignored)")
482
  return True
483
 
484
  elif strategy_hint == 'volume_spike':
485
- # طفرة الحجم قد تكون بيعاً أو شراءً
486
- # (الشروط المخففة: large_trades > 0)
487
  if (large_trades > 0):
488
  print(f" [Trigger] {symbol} Volume Spike: LargeTrades={large_trades}, K_CVD={cvd_kucoin:.0f} (CVD check ignored)")
489
  return True
490
 
491
  return False
492
 
493
- # 🔴 --- START OF CHANGE (V6.4 - STRATEGIC EXITS ONLY) --- 🔴
494
- def _check_exit_trigger(self, trade: Dict, data: Dict) -> str:
495
- """(محدث V6.4) يراقب فقط وقف الخسارة وجني الأرباح الاستراتيجي."""
496
 
497
  symbol = trade['symbol']
 
 
498
 
499
- # --- 1. التحقق من وقف الخسارة/جني الأرباح الاستراتيجي (دائماً فعال) ---
500
- if symbol in self.tactical_data_cache and self.tactical_data_cache[symbol].order_book:
501
- ob = self.tactical_data_cache[symbol].order_book
502
- if ob and ob.get('bids') and len(ob['bids']) > 0: # (تحقق إضافي)
503
- current_price = ob['bids'][0][0]
504
- hard_stop = trade.get('stop_loss')
505
- take_profit = trade.get('take_profit')
506
-
507
- # (التأكد من أن القيم ليست None وقابلة للمقارنة)
508
- if hard_stop and current_price and current_price <= hard_stop:
509
- return f"Strategic Stop Loss hit: {current_price} <= {hard_stop}"
510
- if take_profit and current_price and current_price >= take_profit:
511
- return f"Strategic Take Profit hit: {current_price} >= {take_profit}"
512
 
513
- # --- 2. (تمت إزالة الخروج التكتيكي المستند إلى RSI/CVD بالكامل) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
514
 
515
  return None # لا يوجد سبب للخروج
516
  # 🔴 --- END OF CHANGE --- 🔴
517
 
518
 
519
- async def _execute_smart_entry(self, symbol: str, strategy_hint: str, tactical_data: Dict, explorer_context: Dict): # (محدث V6.5)
520
  """(المنفذ الوهمي - Layer 3) يحاكي تنفيذ الصفقة ويحفظها في R2."""
521
  print(f"🚀 [Executor] بدء تنفيذ الدخول الذكي (وهمي) لـ {symbol}...")
522
 
523
- # (إضافة explorer_context إلى متغير محلي لإعادة الاستخدام عند الفشل)
524
  context_for_retry = explorer_context
525
 
526
  if self.state_manager.trade_analysis_lock.locked():
@@ -532,12 +525,10 @@ class TradeManager:
532
  return
533
 
534
  try:
535
- # 1. التحقق من الصفقات المفتوحة ورأس المال (من R2)
536
  if await self.get_trade_by_symbol(symbol):
537
  print(f"ℹ️ [Executor] الصفقة {symbol} مفتوحة بالفعل (وهمياً). تم الإلغاء.");
538
  return
539
 
540
- # (تحقق إضافي للتأكد من عدم وجود أي صفقات أخرى مفتوحة قبل استهلاك رأس المال)
541
  all_open_trades = await self.get_open_trades()
542
  if len(all_open_trades) > 0:
543
  print(f"❌ [Executor] يوجد صفقة أخرى مفتوحة ({all_open_trades[0]['symbol']}). لا يمكن فتح {symbol}.");
@@ -550,7 +541,6 @@ class TradeManager:
550
  print(f"❌ [Executor] رأس مال وهمي غير كافٍ لـ {symbol}.");
551
  return
552
 
553
- # 2. الحصول على السعر الحالي من البيانات العامة
554
  current_ask_price = None
555
  if symbol in self.tactical_data_cache and self.tactical_data_cache[symbol].order_book:
556
  ob = self.tactical_data_cache[symbol].order_book
@@ -561,15 +551,13 @@ class TradeManager:
561
  print(f"❌ [Executor] لا يمكن الحصول على السعر الحالي (من البيانات العامة) لـ {symbol}.");
562
  return
563
 
564
- # 3. تحديد تفاصيل الصفقة الوهمية
565
  llm_decision = explorer_context.get('decision', {})
566
  stop_loss_price = llm_decision.get("stop_loss", current_ask_price * 0.98)
567
  take_profit_price = llm_decision.get("take_profit", current_ask_price * 1.03)
568
  exit_profile = llm_decision.get('exit_profile', 'ATR_TRAILING')
569
  exit_parameters = llm_decision.get('exit_parameters', {})
570
 
571
- # 🔴 --- START OF CHANGE (V6.5 - SANITY CHECK) --- 🔴
572
- # 4. فحص السلامة: التحقق من أن السعر الحالي يقع بين وقف الخسارة وجني الأرباح
573
  if not (stop_loss_price and take_profit_price):
574
  print(f"❌ [Executor] {symbol}: بيانات SL/TP غير صالحة من النموذج. تم الإلغاء.")
575
  return
@@ -581,13 +569,10 @@ class TradeManager:
581
  if current_ask_price <= stop_loss_price:
582
  print(f"⚠️ [Executor] {symbol}: السعر الحالي ({current_ask_price}) أقل من وقف الخسارة ({stop_loss_price}). الصفقة فاشلة. تم الإلغاء.")
583
  return
584
- # 🔴 --- END OF CHANGE --- 🔴
585
 
586
- # 5. (المحاكاة) - نفترض أن الصفقة تمت بنجاح فوري
587
  final_entry_price = current_ask_price
588
  print(f"✅ [Executor] (SIMULATED) تم التنفيذ! {symbol} بسعر {final_entry_price}")
589
 
590
- # 6. حفظ الصفقة الوهمية في R2 (هذا هو التنفيذ الفعلي لدينا)
591
  await self._save_trade_to_r2(
592
  symbol=symbol, entry_price=final_entry_price, position_size_usd=available_capital,
593
  strategy=strategy_hint, exit_profile=exit_profile, exit_parameters=exit_parameters,
@@ -595,7 +580,6 @@ class TradeManager:
595
  tactical_context=tactical_data, explorer_context=explorer_context
596
  )
597
 
598
- # 7. مسح قائمة المراقبة بعد فتح الصفقة بنجاح
599
  print(f" [Executor] الصفقة {symbol} فُتحت. مسح باقي قائمة المراقبة (Watchlist)...")
600
  async with self.sentry_lock:
601
  self.sentry_watchlist.clear()
@@ -605,14 +589,12 @@ class TradeManager:
605
  print(f"❌ [Executor] فشل فادح أثناء التنفيذ (SIM) لـ {symbol}: {e}");
606
  traceback.print_exc()
607
 
608
- # (إصلاح V6.0 / V6.1)
609
- # إذا فشلت عملية الحفظ في R2 (أو أي خطأ آخر)، أعد العملة إلى قائمة المراقبة
610
  print(f" [Sentry] إعادة {symbol} إلى Watchlist بعد فشل التنفيذ الوهمي.")
611
  async with self.sentry_lock:
612
  self.sentry_watchlist[symbol] = {
613
  "symbol": symbol,
614
  "strategy_hint": strategy_hint,
615
- "llm_decision_context": context_for_retry # (استخدام المتغير المحلي)
616
  }
617
 
618
  finally:
@@ -620,7 +602,7 @@ class TradeManager:
620
  self.r2_service.release_lock()
621
 
622
 
623
- async def _save_trade_to_r2(self, **kwargs): # (محدث V6.2)
624
  """(دالة داخلية - V6.2) تحفظ فقط البيانات الأساسية للصفقة الوهمية."""
625
  try:
626
  symbol = kwargs.get('symbol')
@@ -628,7 +610,6 @@ class TradeManager:
628
  exit_profile = kwargs.get('exit_profile')
629
  expected_target_time = (datetime.now() + timedelta(minutes=15)).isoformat()
630
 
631
- # (منطق جديد: استخراج قرار LLM فقط وتجاهل بيانات الشموع)
632
  explorer_context_blob = kwargs.get('explorer_context', {})
633
  llm_decision_only = explorer_context_blob.get('decision', {})
634
 
@@ -638,7 +619,7 @@ class TradeManager:
638
  "exit_profile": exit_profile,
639
  "exit_parameters": kwargs.get('exit_parameters', {}),
640
  "tactical_context_at_decision": kwargs.get('tactical_context', {}),
641
- "explorer_llm_decision": llm_decision_only # حفظ قرار LLM فقط
642
  }
643
 
644
  new_trade = {
@@ -679,10 +660,9 @@ class TradeManager:
679
  except Exception as e:
680
  print(f"❌ [R2] فشل حفظ الصفقة لـ {symbol}: {e}");
681
  traceback.print_exc()
682
- # (إثارة الخطأ لإعلام الدالة المستدعية بالفشل)
683
  raise
684
 
685
- async def close_trade(self, trade_to_close, close_price, reason="System Close"): # (لا تغيير هنا)
686
  """(لا تغيير جوهري) - لا يزال مسؤولاً عن حساب PnL وتحديث R2 وتشغيل LearningHub"""
687
  try:
688
  symbol = trade_to_close.get('symbol'); trade_to_close['status'] = 'CLOSED'
@@ -705,8 +685,6 @@ class TradeManager:
705
  trades_to_keep = [t for t in open_trades if t.get('id') != trade_to_close.get('id')]
706
  await self.r2_service.save_open_trades_async(trades_to_keep)
707
 
708
- # (لا حاجة لإزالة المهمة يدوياً، الحلقة الرئيسية ستفعل ذلك)
709
-
710
  await self.r2_service.save_system_logs_async({
711
  "trade_closed": True, "symbol": symbol, "pnl_usd": pnl, "pnl_percent": pnl_percent,
712
  "new_capital": new_capital, "strategy": strategy, "reason": reason
@@ -719,7 +697,7 @@ class TradeManager:
719
  return True
720
  except Exception as e: print(f"❌ [Executor] فشل فادح أثناء إغلاق الصفقة (الوهمية) {symbol}: {e}"); traceback.print_exc(); raise
721
 
722
- async def immediate_close_trade(self, symbol, close_price, reason="Immediate Close"): # (لا تغيير هنا)
723
  """(معدل) - للإغلاق الفوري بناءً على زناد Sentry"""
724
  if not self.r2_service.acquire_lock(): print(f"⚠️ [Executor] فشل في الحصول على قفل R2 لـ {symbol} (Immediate Close)"); return False
725
  try:
@@ -732,7 +710,7 @@ class TradeManager:
732
  finally:
733
  if self.r2_service.lock_acquired: self.r2_service.release_lock()
734
 
735
- async def update_trade_strategy(self, trade_to_update, re_analysis_decision): # (لا تغيير هنا)
736
  """(يستدعى من المستكشف) لتحديث الأهداف الاستراتيجية فقط"""
737
  try:
738
  symbol = trade_to_update.get('symbol')
@@ -756,7 +734,7 @@ class TradeManager:
756
  return True
757
  except Exception as e: print(f"❌ (Explorer) فشل تحديث استراتيجية {symbol}: {e}"); raise
758
 
759
- async def _archive_closed_trade(self, closed_trade): # (لا تغيير هنا)
760
  try:
761
  key = "closed_trades_history.json"; history = []
762
  try: response = self.r2_service.s3_client.get_object(Bucket="trading", Key=key); history = json.loads(response['Body'].read())
@@ -766,7 +744,7 @@ class TradeManager:
766
  self.r2_service.s3_client.put_object(Bucket="trading", Key=key, Body=data_json, ContentType="application/json")
767
  except Exception as e: print(f"❌ Failed to archive trade: {e}")
768
 
769
- async def _update_trade_summary(self, closed_trade): # (لا تغيير هنا)
770
  try:
771
  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()}
772
  try: response = self.r2_service.s3_client.get_object(Bucket="trading", Key=key); summary = json.loads(response['Body'].read())
@@ -782,15 +760,15 @@ class TradeManager:
782
  self.r2_service.s3_client.put_object(Bucket="trading", Key=key, Body=data_json, ContentType="application/json")
783
  except Exception as e: print(f"❌ Failed to update trade summary: {e}")
784
 
785
- async def get_open_trades(self): # (لا تغيير هنا)
786
  try: return await self.r2_service.get_open_trades_async()
787
  except Exception as e: print(f"❌ Failed to get open trades: {e}"); return []
788
 
789
- async def get_trade_by_symbol(self, symbol): # (لا تغيير هنا)
790
  try:
791
  open_trades = await self.get_open_trades()
792
  return next((t for t in open_trades if t['symbol'] == symbol and t['status'] == 'OPEN'), None)
793
  except Exception as e: print(f"❌ Failed to get trade by symbol {symbol}: {e}"); return None
794
 
795
 
796
- print(f"✅ Trade Manager loaded - V6.5 (Executor Sanity Check + Strategic Exit) (ccxt.async_support: {CCXT_ASYNC_AVAILABLE})")
 
1
+ # trade_manager.py (Updated to V6.6 - Fixed Take-Profit Execution Logic)
2
  import asyncio
3
  import json
4
  import time
 
6
  import os
7
  from datetime import datetime, timedelta
8
  from typing import Dict, Any, List
9
+ from collections import deque, defaultdict
10
 
11
  try:
 
12
  import ccxt.async_support as ccxtasync
13
  CCXT_ASYNC_AVAILABLE = True
14
  except ImportError:
 
19
  import numpy as np
20
  from helpers import safe_float_conversion
21
 
 
22
  class TacticalData:
23
  """
24
  (محدث) لتخزين بيانات تأكيد من مصادر متعددة بدلاً من Binance فقط.
 
32
  self.one_min_rsi = 50.0
33
  self.last_update = time.time()
34
 
 
35
  self.confirmation_trades = defaultdict(lambda: deque(maxlen=50))
36
  self.confirmation_cvd = defaultdict(float)
37
 
 
131
  self.sentry_tasks = {}
132
  self.tactical_data_cache = {}
133
 
134
+ self.sentry_lock = asyncio.Lock()
135
 
136
+ self.kucoin_rest = None
137
+ self.confirmation_exchanges = {}
138
+ self.polling_interval = 1.5
139
+ self.confirmation_polling_interval = 3.0
140
 
141
+ async def initialize_sentry_exchanges(self):
142
  """
143
  (محدث V6.1) تهيئة KuCoin (أساسي) ومنصات تأكيد متعددة (ثانوية).
 
144
  """
145
  try:
146
  print("🔄 [Sentry] تهيئة منصات التداول (KuCoin REST ومنصات التأكيد)...")
147
 
 
148
  print(" [Sentry] تهيئة KuCoin للبيانات العامة (وضع المحاكاة).")
149
  self.kucoin_rest = ccxtasync.kucoin()
150
  await self.kucoin_rest.load_markets()
151
  print("✅ [Sentry] منصة REST الأساسية (KuCoin) جاهزة (بيانات عامة فقط).")
152
 
 
153
  self.confirmation_exchanges = {}
154
  confirmation_exchange_ids = ['bybit', 'okx', 'gateio']
155
 
156
  print(f" [Sentry] تهيئة منصات التأكيد (Confirmation Exchanges): {', '.join(confirmation_exchange_ids)}")
157
  for ex_id in confirmation_exchange_ids:
158
  try:
 
159
  exchange = getattr(ccxtasync, ex_id)()
160
  await exchange.load_markets()
161
  self.confirmation_exchanges[ex_id] = exchange
 
178
  for ex in self.confirmation_exchanges.values(): await ex.close()
179
  raise
180
 
181
+ async def start_sentry_and_monitoring_loops(self):
182
  """الحلقة الرئيسية للحارس (Sentry) ومراقب الخروج (Exit Monitor)."""
183
  self.is_running = True
184
  print(f"✅ [Sentry] بدء حلقات المراقبة التكتيكية (Layer 2 - API Polling)...")
185
  while self.is_running:
186
  try:
 
187
  async with self.sentry_lock:
188
  watchlist_symbols = set(self.sentry_watchlist.keys())
189
 
 
190
  open_trades = await self.get_open_trades()
191
  open_trade_symbols = {t['symbol'] for t in open_trades}
192
 
193
  symbols_to_monitor = watchlist_symbols.union(open_trade_symbols)
194
  current_tasks = set(self.sentry_tasks.keys())
195
 
 
196
  symbols_to_add = symbols_to_monitor - current_tasks
197
  for symbol in symbols_to_add:
198
  print(f" [Sentry] بدء المراقبة التكتيكية (Polling) لـ {symbol}")
199
 
200
+ strategy_hint = 'generic'
 
201
  if symbol in watchlist_symbols:
202
  async with self.sentry_lock:
 
203
  if symbol in self.sentry_watchlist:
204
  strategy_hint = self.sentry_watchlist.get(symbol, {}).get('strategy_hint', 'generic')
205
  elif symbol in open_trade_symbols:
 
206
  trade = next((t for t in open_trades if t['symbol'] == symbol), None)
207
  if trade:
208
  strategy_hint = trade.get('strategy', 'generic')
 
213
  task = asyncio.create_task(self._monitor_symbol_activity_polling(symbol, strategy_hint))
214
  self.sentry_tasks[symbol] = task
215
 
 
216
  symbols_to_remove = current_tasks - symbols_to_monitor
217
  for symbol in symbols_to_remove:
218
  print(f" [Sentry] إيقاف المراقبة التكتيكية (Polling) لـ {symbol}")
 
222
  if symbol in self.tactical_data_cache:
223
  del self.tactical_data_cache[symbol]
224
 
225
+ await asyncio.sleep(15)
226
 
227
  except Exception as error:
228
  print(f"❌ [Sentry] خطأ في الحلقة الرئيسية: {error}"); traceback.print_exc(); await asyncio.sleep(60)
229
 
230
  async def stop_sentry_loops(self):
231
+ """إيقاف جميع مهام المراقبة وإغلاق جميع اتصالات REST"""
232
  self.is_running = False
233
  print("🛑 [Sentry] إيقاف جميع حلقات المراقبة...")
234
  for task in self.sentry_tasks.values(): task.cancel()
 
248
  print("✅ [Sentry] تم إغلاق اتصالات التداول (REST).")
249
  except Exception as e: print(f"⚠️ [Sentry] خطأ أثناء إغلاق الاتصالات: {e}")
250
 
251
+ async def update_sentry_watchlist(self, candidates: List[Dict]):
252
  """تحديث قائمة المراقبة التي يستخدمها الحارس (Sentry)."""
253
+ async with self.sentry_lock:
254
  self.sentry_watchlist = {c['symbol']: c for c in candidates}
255
  print(f"ℹ️ [Sentry] تم تحديث Watchlist. عدد المرشحين: {len(self.sentry_watchlist)}")
256
 
257
  def get_sentry_status(self):
258
+ """لواجهة برمجة التطبيقات /system-status"""
 
259
  active_monitoring_count = len(self.sentry_tasks)
260
  watchlist_symbols_list = list(self.sentry_watchlist.keys())
261
 
 
269
 
270
  async def _monitor_symbol_activity_polling(self, symbol: str, strategy_hint: str):
271
  """
 
272
  يشغل حلقات Polling متوازية (KuCoin + منصات التأكيد) ويشغل منطق التحليل.
273
  """
274
  if symbol not in self.tactical_data_cache:
275
  self.tactical_data_cache[symbol] = TacticalData(symbol)
276
 
277
  tasks_to_gather = [
278
+ self._poll_kucoin_data(symbol),
279
+ self._poll_confirmation_data(symbol),
280
+ self._run_tactical_analysis_loop(symbol, strategy_hint)
281
  ]
282
 
283
  try:
 
289
  traceback.print_exc()
290
  finally:
291
  print(f"🛑 [Sentry] إنهاء جميع مهام (Polling) {symbol}")
 
292
  if symbol in self.sentry_tasks:
293
  self.sentry_tasks.pop(symbol, None)
294
  if symbol in self.tactical_data_cache:
295
  del self.tactical_data_cache[symbol]
296
 
297
  async def _poll_kucoin_data(self, symbol):
298
+ """حلقة استقصاء (Polling) لبيانات KuCoin (الأساسية)"""
299
  while self.is_running:
300
  try:
301
  if not self.kucoin_rest:
 
309
  self.tactical_data_cache[symbol].set_order_book(ob)
310
 
311
  # 2. جلب آخر الصفقات
312
+ since_timestamp = int((time.time() - 60) * 1000)
313
  trades = await self.kucoin_rest.fetch_trades(symbol, since=since_timestamp, limit=50)
314
  if symbol in self.tactical_data_cache:
315
  trades.sort(key=lambda x: x['timestamp'])
 
322
  print(f"⏳ [Sentry Polling] {symbol} KuCoin Rate Limit Exceeded: {e}. زيادة فترة الانتظار...")
323
  await asyncio.sleep(10)
324
  except asyncio.CancelledError:
325
+ raise
326
  except Exception as e:
327
  print(f"⚠️ [Sentry Polling] خطأ في {symbol} KuCoin data polling: {e}")
328
  await asyncio.sleep(5)
 
331
  """(جديد) حلقة استقصاء (Polling) لبيانات منصات التأكيد (Bybit, OKX, etc.)"""
332
  if not self.confirmation_exchanges:
333
  print(f" [Sentry Conf] {symbol} - لا توجد منصات تأكيد، سيتم الاعتماد على KuCoin فقط.")
334
+ return
335
 
 
336
  await asyncio.sleep(self.confirmation_polling_interval / 2)
337
 
338
  while self.is_running:
339
  try:
340
  tasks = []
 
341
  for ex_id, exchange in self.confirmation_exchanges.items():
342
  tasks.append(self._fetch_confirmation_trades(ex_id, exchange, symbol))
343
 
344
  await asyncio.gather(*tasks)
 
 
345
  await asyncio.sleep(self.confirmation_polling_interval)
346
 
347
  except asyncio.CancelledError:
348
+ raise
349
  except Exception as e:
350
  print(f"⚠️ [Sentry Conf] خطأ في حلقة التأكيد لـ {symbol}: {e}")
351
+ await asyncio.sleep(10)
352
 
353
  async def _fetch_confirmation_trades(self, ex_id: str, exchange: ccxtasync.Exchange, symbol: str):
354
  """(جديد) دالة مساعدة لجلب الصفقات من منصة تأكيد واحدة"""
355
  try:
 
356
  if symbol not in exchange.markets:
 
 
357
  return
358
 
359
+ since_timestamp = int((time.time() - 60) * 1000)
360
  trades = await exchange.fetch_trades(symbol, since=since_timestamp, limit=50)
361
 
362
  if symbol in self.tactical_data_cache:
363
  trades.sort(key=lambda x: x['timestamp'])
364
  for trade in trades:
 
365
  self.tactical_data_cache[symbol].add_confirmation_trade(ex_id, trade)
366
 
367
  except ccxtasync.RateLimitExceeded:
368
  print(f"⏳ [Sentry Conf] {ex_id} Rate Limit لـ {symbol}. الانتظار...")
369
+ await asyncio.sleep(15)
370
  except asyncio.CancelledError:
371
+ raise
372
  except Exception as e:
 
373
  pass
374
 
375
 
376
  async def _run_tactical_analysis_loop(self, symbol: str, strategy_hint: str):
377
+ """(محدث V6.6) (دماغ الحارس) يشغل التحليل التكتيكي كل ثانية."""
378
  while self.is_running:
379
  await asyncio.sleep(1) # (التحليل السريع كل ثانية)
380
  try:
 
386
  snapshot = tactical_data.get_tactical_snapshot()
387
 
388
  if trade:
389
+ # 🔴 --- (الإصلاح V6.6) --- 🔴
390
+ # (سيتم تمرير snapshot إلى الدالة المحدثة)
391
+ exit_reason = self._check_exit_trigger(trade, snapshot, tactical_data)
392
  if exit_reason:
393
  print(f"🛑 [Sentry] زناد خروج استراتيجي لـ {symbol}: {exit_reason}")
394
+
395
+ # (تحديد السعر الحقيقي للإغلاق)
396
+ current_price_to_close = None
397
+ if "Take Profit" in exit_reason:
398
+ # إذا كان جني الأرباح، نستخدم السعر الذي حقق الهدف
399
+ current_price_to_close = trade.get('take_profit')
400
+ elif tactical_data.order_book and tactical_data.order_book.get('bids') and len(tactical_data.order_book['bids']) > 0:
401
+ # إذا كان وقف الخسارة، نستخدم أفضل سعر شراء (Bid)
402
+ current_price_to_close = tactical_data.order_book['bids'][0][0]
403
  else:
404
+ # (احتياطي: إذا فشل كل شيء، استخدم آخر سعر صفقة)
405
  if tactical_data.trades:
406
+ current_price_to_close = tactical_data.trades[-1].get('price')
407
+
408
+ if current_price_to_close:
409
+ await self.immediate_close_trade(symbol, current_price_to_close, f"Strategic Exit: {exit_reason}")
410
+ else:
411
+ print(f"⚠️ [Sentry] {symbol} زناد خروج ولكن لا يمكن تحديد سعر الإغلاق!")
412
  else:
 
413
  async with self.sentry_lock:
414
  is_still_on_watchlist = symbol in self.sentry_watchlist
415
 
 
418
  if trigger:
419
  print(f"✅ [Sentry] زناد دخول تكتيكي لـ {symbol} (استراتيجية: {strategy_hint})")
420
 
 
421
  watchlist_entry = None
422
  async with self.sentry_lock:
423
  watchlist_entry = self.sentry_watchlist.pop(symbol, None)
 
426
  explorer_context = watchlist_entry.get('llm_decision_context', {})
427
  await self._execute_smart_entry(symbol, strategy_hint, snapshot, explorer_context)
428
  except asyncio.CancelledError:
429
+ raise
430
  except Exception as e: print(f"❌ [Sentry] خطأ في حلقة التحليل التكتيكي لـ {symbol}: {e}"); traceback.print_exc()
431
 
432
+ def _check_entry_trigger(self, symbol: str, strategy_hint: str, data: Dict) -> bool:
433
  """(محدث V5.9) إضافة trend_following إلى منطق الزناد."""
434
 
435
  rsi = data.get('rsi_1m_approx', 50)
 
439
  cvd_conf_agg = data.get('cvd_confirmation_aggregate', 0)
440
  cvd_conf_sources_count = len(data.get('cvd_confirmation_sources', {}))
441
 
 
442
  if strategy_hint in ['breakout_momentum', 'trend_following']:
 
443
  if cvd_kucoin <= 0:
444
+ return False
 
 
445
  if (rsi > 55):
 
446
  if cvd_conf_sources_count > 0 and cvd_conf_agg < (cvd_kucoin * -0.5):
447
  print(f" [Trigger Hold] {symbol} Breakout/Trend: KuCoin CVD ({cvd_kucoin:.0f}) إيجابي، لكن منصات التأكيد ({cvd_conf_agg:.0f}) تبيع بقوة.")
448
  return False
 
451
  return True
452
 
453
  elif strategy_hint == 'mean_reversion':
 
 
454
  if (rsi < 35):
455
  print(f" [Trigger] {symbol} Reversion: RSI={rsi:.1f}, K_CVD={cvd_kucoin:.0f} (CVD check ignored)")
456
  return True
457
 
458
  elif strategy_hint == 'volume_spike':
 
 
459
  if (large_trades > 0):
460
  print(f" [Trigger] {symbol} Volume Spike: LargeTrades={large_trades}, K_CVD={cvd_kucoin:.0f} (CVD check ignored)")
461
  return True
462
 
463
  return False
464
 
465
+ # 🔴 --- START OF CHANGE (V6.6 - TP FIX) --- 🔴
466
+ def _check_exit_trigger(self, trade: Dict, data: Dict, tactical_data: TacticalData) -> str:
467
+ """(محدث V6.6) يراقب وقف الخسارة وجني الأرباح باستخدام (Bid) و (Last Trade Price)"""
468
 
469
  symbol = trade['symbol']
470
+ hard_stop = trade.get('stop_loss')
471
+ take_profit = trade.get('take_profit')
472
 
473
+ # --- 1. جلب الأسعار المتاحة ---
 
 
 
 
 
 
 
 
 
 
 
 
474
 
475
+ # السعر 1: أفضل سعر شراء (Bid) - (الأكثر أماناً لوقف الخسارة)
476
+ best_bid_price = None
477
+ if tactical_data.order_book and tactical_data.order_book.get('bids') and len(tactical_data.order_book['bids']) > 0:
478
+ best_bid_price = tactical_data.order_book['bids'][0][0]
479
+
480
+ # السعر 2: آخر سعر تداول (Last Trade) - (الأكثر دقة لجني الأرباح)
481
+ last_trade_price = None
482
+ if tactical_data.trades: # (trades هو deque)
483
+ try:
484
+ last_trade_price = tactical_data.trades[-1].get('price')
485
+ except (IndexError, AttributeError):
486
+ pass # (يبقى None إذا كان deque فارغاً)
487
+
488
+ # (يجب أن يكون لدينا سعر واحد على الأقل للمتابعة)
489
+ if best_bid_price is None and last_trade_price is None:
490
+ return None # لا يمكن تحديد السعر، انتظر الدورة التالية
491
+
492
+ # (استخدم bid إذا فشل last_trade، أو العكس)
493
+ current_price_for_sl = best_bid_price if best_bid_price is not None else last_trade_price
494
+
495
+ # (استخدم السعر الأعلى بينهما لجني الأرباح)
496
+ current_price_for_tp = max(
497
+ filter(None, [best_bid_price, last_trade_price]),
498
+ default=None
499
+ )
500
+
501
+ # --- 2. التحقق من وقف الخسارة الاستراتيجي (يستخدم سعر Bid الآمن) ---
502
+ if hard_stop and current_price_for_sl and current_price_for_sl <= hard_stop:
503
+ return f"Strategic Stop Loss hit: {current_price_for_sl} <= {hard_stop}"
504
+
505
+ # --- 3. التحقق من جني الأرباح الاستراتيجي (يستخدم السعر الأعلى المتاح) ---
506
+ if take_profit and current_price_for_tp and current_price_for_tp >= take_profit:
507
+ return f"Strategic Take Profit hit: {current_price_for_tp} >= {take_profit}"
508
 
509
  return None # لا يوجد سبب للخروج
510
  # 🔴 --- END OF CHANGE --- 🔴
511
 
512
 
513
+ async def _execute_smart_entry(self, symbol: str, strategy_hint: str, tactical_data: Dict, explorer_context: Dict):
514
  """(المنفذ الوهمي - Layer 3) يحاكي تنفيذ الصفقة ويحفظها في R2."""
515
  print(f"🚀 [Executor] بدء تنفيذ الدخول الذكي (وهمي) لـ {symbol}...")
516
 
 
517
  context_for_retry = explorer_context
518
 
519
  if self.state_manager.trade_analysis_lock.locked():
 
525
  return
526
 
527
  try:
 
528
  if await self.get_trade_by_symbol(symbol):
529
  print(f"ℹ️ [Executor] الصفقة {symbol} مفتوحة بالفعل (وهمياً). تم الإلغاء.");
530
  return
531
 
 
532
  all_open_trades = await self.get_open_trades()
533
  if len(all_open_trades) > 0:
534
  print(f"❌ [Executor] يوجد صفقة أخرى مفتوحة ({all_open_trades[0]['symbol']}). لا يمكن فتح {symbol}.");
 
541
  print(f"❌ [Executor] رأس مال وهمي غير كافٍ لـ {symbol}.");
542
  return
543
 
 
544
  current_ask_price = None
545
  if symbol in self.tactical_data_cache and self.tactical_data_cache[symbol].order_book:
546
  ob = self.tactical_data_cache[symbol].order_book
 
551
  print(f"❌ [Executor] لا يمكن الحصول على السعر الحالي (من البيانات العامة) لـ {symbol}.");
552
  return
553
 
 
554
  llm_decision = explorer_context.get('decision', {})
555
  stop_loss_price = llm_decision.get("stop_loss", current_ask_price * 0.98)
556
  take_profit_price = llm_decision.get("take_profit", current_ask_price * 1.03)
557
  exit_profile = llm_decision.get('exit_profile', 'ATR_TRAILING')
558
  exit_parameters = llm_decision.get('exit_parameters', {})
559
 
560
+ # (V6.5 - فحص السلامة)
 
561
  if not (stop_loss_price and take_profit_price):
562
  print(f"❌ [Executor] {symbol}: بيانات SL/TP غير صالحة من النموذج. تم الإلغاء.")
563
  return
 
569
  if current_ask_price <= stop_loss_price:
570
  print(f"⚠️ [Executor] {symbol}: السعر الحالي ({current_ask_price}) أقل من وقف الخسارة ({stop_loss_price}). الصفقة فاشلة. تم الإلغاء.")
571
  return
 
572
 
 
573
  final_entry_price = current_ask_price
574
  print(f"✅ [Executor] (SIMULATED) تم التنفيذ! {symbol} بسعر {final_entry_price}")
575
 
 
576
  await self._save_trade_to_r2(
577
  symbol=symbol, entry_price=final_entry_price, position_size_usd=available_capital,
578
  strategy=strategy_hint, exit_profile=exit_profile, exit_parameters=exit_parameters,
 
580
  tactical_context=tactical_data, explorer_context=explorer_context
581
  )
582
 
 
583
  print(f" [Executor] الصفقة {symbol} فُتحت. مسح باقي قائمة المراقبة (Watchlist)...")
584
  async with self.sentry_lock:
585
  self.sentry_watchlist.clear()
 
589
  print(f"❌ [Executor] فشل فادح أثناء التنفيذ (SIM) لـ {symbol}: {e}");
590
  traceback.print_exc()
591
 
 
 
592
  print(f" [Sentry] إعادة {symbol} إلى Watchlist بعد فشل التنفيذ الوهمي.")
593
  async with self.sentry_lock:
594
  self.sentry_watchlist[symbol] = {
595
  "symbol": symbol,
596
  "strategy_hint": strategy_hint,
597
+ "llm_decision_context": context_for_retry
598
  }
599
 
600
  finally:
 
602
  self.r2_service.release_lock()
603
 
604
 
605
+ async def _save_trade_to_r2(self, **kwargs):
606
  """(دالة داخلية - V6.2) تحفظ فقط البيانات الأساسية للصفقة الوهمية."""
607
  try:
608
  symbol = kwargs.get('symbol')
 
610
  exit_profile = kwargs.get('exit_profile')
611
  expected_target_time = (datetime.now() + timedelta(minutes=15)).isoformat()
612
 
 
613
  explorer_context_blob = kwargs.get('explorer_context', {})
614
  llm_decision_only = explorer_context_blob.get('decision', {})
615
 
 
619
  "exit_profile": exit_profile,
620
  "exit_parameters": kwargs.get('exit_parameters', {}),
621
  "tactical_context_at_decision": kwargs.get('tactical_context', {}),
622
+ "explorer_llm_decision": llm_decision_only
623
  }
624
 
625
  new_trade = {
 
660
  except Exception as e:
661
  print(f"❌ [R2] فشل حفظ الصفقة لـ {symbol}: {e}");
662
  traceback.print_exc()
 
663
  raise
664
 
665
+ async def close_trade(self, trade_to_close, close_price, reason="System Close"):
666
  """(لا تغيير جوهري) - لا يزال مسؤولاً عن حساب PnL وتحديث R2 وتشغيل LearningHub"""
667
  try:
668
  symbol = trade_to_close.get('symbol'); trade_to_close['status'] = 'CLOSED'
 
685
  trades_to_keep = [t for t in open_trades if t.get('id') != trade_to_close.get('id')]
686
  await self.r2_service.save_open_trades_async(trades_to_keep)
687
 
 
 
688
  await self.r2_service.save_system_logs_async({
689
  "trade_closed": True, "symbol": symbol, "pnl_usd": pnl, "pnl_percent": pnl_percent,
690
  "new_capital": new_capital, "strategy": strategy, "reason": reason
 
697
  return True
698
  except Exception as e: print(f"❌ [Executor] فشل فادح أثناء إغلاق الصفقة (الوهمية) {symbol}: {e}"); traceback.print_exc(); raise
699
 
700
+ async def immediate_close_trade(self, symbol, close_price, reason="Immediate Close"):
701
  """(معدل) - للإغلاق الفوري بناءً على زناد Sentry"""
702
  if not self.r2_service.acquire_lock(): print(f"⚠️ [Executor] فشل في الحصول على قفل R2 لـ {symbol} (Immediate Close)"); return False
703
  try:
 
710
  finally:
711
  if self.r2_service.lock_acquired: self.r2_service.release_lock()
712
 
713
+ async def update_trade_strategy(self, trade_to_update, re_analysis_decision):
714
  """(يستدعى من المستكشف) لتحديث الأهداف الاستراتيجية فقط"""
715
  try:
716
  symbol = trade_to_update.get('symbol')
 
734
  return True
735
  except Exception as e: print(f"❌ (Explorer) فشل تحديث استراتيجية {symbol}: {e}"); raise
736
 
737
+ async def _archive_closed_trade(self, closed_trade):
738
  try:
739
  key = "closed_trades_history.json"; history = []
740
  try: response = self.r2_service.s3_client.get_object(Bucket="trading", Key=key); history = json.loads(response['Body'].read())
 
744
  self.r2_service.s3_client.put_object(Bucket="trading", Key=key, Body=data_json, ContentType="application/json")
745
  except Exception as e: print(f"❌ Failed to archive trade: {e}")
746
 
747
+ async def _update_trade_summary(self, closed_trade):
748
  try:
749
  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()}
750
  try: response = self.r2_service.s3_client.get_object(Bucket="trading", Key=key); summary = json.loads(response['Body'].read())
 
760
  self.r2_service.s3_client.put_object(Bucket="trading", Key=key, Body=data_json, ContentType="application/json")
761
  except Exception as e: print(f"❌ Failed to update trade summary: {e}")
762
 
763
+ async def get_open_trades(self):
764
  try: return await self.r2_service.get_open_trades_async()
765
  except Exception as e: print(f"❌ Failed to get open trades: {e}"); return []
766
 
767
+ async def get_trade_by_symbol(self, symbol):
768
  try:
769
  open_trades = await self.get_open_trades()
770
  return next((t for t in open_trades if t['symbol'] == symbol and t['status'] == 'OPEN'), None)
771
  except Exception as e: print(f"❌ Failed to get trade by symbol {symbol}: {e}"); return None
772
 
773
 
774
+ print(f"✅ Trade Manager loaded - V6.6 (Fixed TP Execution Logic) (ccxt.async_support: {CCXT_ASYNC_AVAILABLE})")