Riy777 commited on
Commit
e14cb89
·
1 Parent(s): 6714a95

Update trade_manager.py

Browse files
Files changed (1) hide show
  1. trade_manager.py +165 -97
trade_manager.py CHANGED
@@ -1,4 +1,4 @@
1
- # trade_manager.py (Updated to V6.6 - Fixed Take-Profit Execution Logic)
2
  import asyncio
3
  import json
4
  import time
@@ -8,12 +8,21 @@ 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:
15
  print("❌❌❌ خطأ فادح: فشل استيراد 'ccxt.async_support'. ❌❌❌")
16
- print("يرجى التأكد من تثبيت 'ccxt' (الإصدار 4 أو أحدث) بنجاح.")
17
  CCXT_ASYNC_AVAILABLE = False
18
 
19
  import numpy as np
@@ -21,15 +30,15 @@ from helpers import safe_float_conversion
21
 
22
  class TacticalData:
23
  """
24
- (محدث) لتخزين بيانات تأكيد من مصادر متعددة بدلاً من Binance فقط.
 
25
  """
26
  def __init__(self, symbol):
27
  self.symbol = symbol
28
- self.order_book = None # (بيانات KuCoin الأساسية)
29
- self.trades = deque(maxlen=100) # (بيانات KuCoin الأساسية)
30
- self.cvd = 0.0 # (بيانات KuCoin الأساسية)
31
  self.large_trades = []
32
- self.one_min_rsi = 50.0
33
  self.last_update = time.time()
34
 
35
  self.confirmation_trades = defaultdict(lambda: deque(maxlen=50))
@@ -37,12 +46,20 @@ class TacticalData:
37
 
38
  self.last_kucoin_trade_id = None
39
  self.last_confirmation_trade_ids = defaultdict(lambda: None)
 
 
 
 
 
 
 
 
40
 
41
  def add_trade(self, trade):
42
  """إضافة صفقة KuCoin (الأساسية)"""
43
  trade_id = trade.get('id')
44
  if trade_id and trade_id == self.last_kucoin_trade_id:
45
- return # صفقة مكررة
46
  self.last_kucoin_trade_id = trade_id
47
 
48
  self.trades.append(trade)
@@ -63,7 +80,7 @@ class TacticalData:
63
  """(جديد) إضافة صفقة تأكيد (Bybit, OKX, etc.)"""
64
  trade_id = trade.get('id')
65
  if trade_id and trade_id == self.last_confirmation_trade_ids[exchange_id]:
66
- return # صفقة مكررة
67
  self.last_confirmation_trade_ids[exchange_id] = trade_id
68
 
69
  self.confirmation_trades[exchange_id].append(trade)
@@ -78,43 +95,87 @@ class TacticalData:
78
  self.last_update = time.time()
79
 
80
  def analyze_order_book(self):
81
- if not self.order_book: return {"bids_depth": 0, "asks_depth": 0, "top_wall": "None"}
82
  try:
83
  bids = self.order_book.get('bids', []); asks = self.order_book.get('asks', [])
84
  bids_depth = sum(price * amount for price, amount in bids[:10])
85
  asks_depth = sum(price * amount for price, amount in asks[:10])
86
  return {"bids_depth": bids_depth, "asks_depth": asks_depth}
87
- except Exception: return {"bids_depth": 0, "asks_depth": 0, "top_wall": "Error"}
88
 
89
- def get_1m_rsi(self):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  try:
91
- if len(self.trades) < 20: return 50.0
92
- closes = np.array([trade['price'] for trade in self.trades if 'price' in trade])[-20:]
93
- if len(closes) < 15: return 50.0
94
- deltas = np.diff(closes); seed = deltas[:14]
95
- gains = np.sum(seed[seed >= 0]); losses = np.sum(np.abs(seed[seed < 0]))
96
- if losses == 0: self.one_min_rsi = 100.0; return 100.0
97
- if gains == 0: self.one_min_rsi = 0.0; return 0.0
98
- rs = gains / losses; rsi = 100.0 - (100.0 / (1.0 + rs))
99
- for delta in deltas[14:]:
100
- gain = max(delta, 0); loss = max(-delta, 0)
101
- gains = (gains * 13 + gain) / 14; losses = (losses * 13 + loss) / 14
102
- if losses == 0: rsi = 100.0
103
- else: rs = gains / losses; rsi = 100.0 - (100.0 / (1.0 + rs))
104
- self.one_min_rsi = rsi; return rsi
105
- except Exception: return self.one_min_rsi
 
 
 
 
 
 
 
 
 
 
 
106
 
107
  def get_tactical_snapshot(self):
108
- """(محدث) لإرجاع بيانات التأكيد المجمعة"""
109
  agg_cvd = sum(self.confirmation_cvd.values())
 
 
110
  return {
111
  "cvd_kucoin": self.cvd,
112
  "cvd_confirmation_sources": dict(self.confirmation_cvd),
113
  "cvd_confirmation_aggregate": agg_cvd,
114
  "large_trades_count_5m": len([t for t in self.large_trades if t.get('timestamp') and (time.time() - t['timestamp']/1000) < 300]),
115
- "rsi_1m_approx": self.get_1m_rsi(),
 
116
  "ob_analysis": self.analyze_order_book()
117
  }
 
118
 
119
 
120
  class TradeManager:
@@ -139,9 +200,6 @@ class TradeManager:
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
 
@@ -179,7 +237,6 @@ class TradeManager:
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:
@@ -228,7 +285,6 @@ class TradeManager:
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()
@@ -249,13 +305,11 @@ class TradeManager:
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
 
@@ -268,9 +322,6 @@ class TradeManager:
268
  }
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
 
@@ -295,7 +346,7 @@ class TradeManager:
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:
@@ -303,18 +354,34 @@ class TradeManager:
303
  await asyncio.sleep(10)
304
  continue
305
 
306
- # 1. جلب دفتر الطلبات
307
- ob = await self.kucoin_rest.fetch_order_book(symbol, limit=20)
308
- if symbol in self.tactical_data_cache:
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'])
316
  for trade in trades:
317
  self.tactical_data_cache[symbol].add_trade(trade)
 
 
 
 
 
318
 
319
  await asyncio.sleep(self.polling_interval)
320
 
@@ -328,9 +395,7 @@ class TradeManager:
328
  await asyncio.sleep(5)
329
 
330
  async def _poll_confirmation_data(self, symbol):
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)
@@ -351,7 +416,6 @@ class TradeManager:
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
@@ -374,9 +438,9 @@ class TradeManager:
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:
381
  if self.state_manager.trade_analysis_lock.locked(): continue
382
  trade = await self.get_trade_by_symbol(symbol)
@@ -386,22 +450,16 @@ class TradeManager:
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
 
@@ -414,6 +472,8 @@ class TradeManager:
414
  is_still_on_watchlist = symbol in self.sentry_watchlist
415
 
416
  if is_still_on_watchlist:
 
 
417
  trigger = self._check_entry_trigger(symbol, strategy_hint, snapshot)
418
  if trigger:
419
  print(f"✅ [Sentry] زناد دخول تكتيكي لـ {symbol} (استراتيجية: {strategy_hint})")
@@ -429,40 +489,62 @@ class TradeManager:
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)
436
  cvd_kucoin = data.get('cvd_kucoin', 0)
437
- large_trades = data.get('large_trades_count_5m', 0)
438
 
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
449
-
450
- print(f" [Trigger] {symbol} Breakout/Trend: K_CVD={cvd_kucoin:.0f}, C_CVD_Agg={cvd_conf_agg:.0f}, RSI={rsi:.1f}");
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
 
@@ -470,44 +552,34 @@ class TradeManager:
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):
@@ -557,7 +629,6 @@ class TradeManager:
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
@@ -663,7 +734,6 @@ class TradeManager:
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'
669
  trade_to_close['close_price'] = close_price; trade_to_close['close_timestamp'] = datetime.now().isoformat()
@@ -698,7 +768,6 @@ class TradeManager:
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:
704
  open_trades = await self.r2_service.get_open_trades_async()
@@ -711,7 +780,6 @@ class TradeManager:
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')
717
  if re_analysis_decision.get('action') == "UPDATE_TRADE":
@@ -771,4 +839,4 @@ class TradeManager:
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})")
 
1
+ # trade_manager.py (Updated to V6.7 - 1M Indicator & Order Book Trigger)
2
  import asyncio
3
  import json
4
  import time
 
8
  from typing import Dict, Any, List
9
  from collections import deque, defaultdict
10
 
11
+ # 🔴 --- START OF CHANGE (V6.7) --- 🔴
12
+ # (إضافة pandas لتحليل مؤشرات 1-دقيقة)
13
+ import pandas as pd
14
+ try:
15
+ import pandas_ta as ta
16
+ except ImportError:
17
+ print("⚠️ مكتبة pandas_ta غير موجودة، مؤشرات الحارس (Sentry 1m) ستفشل.")
18
+ ta = None
19
+ # 🔴 --- END OF CHANGE --- 🔴
20
+
21
  try:
22
  import ccxt.async_support as ccxtasync
23
  CCXT_ASYNC_AVAILABLE = True
24
  except ImportError:
25
  print("❌❌❌ خطأ فادح: فشل استيراد 'ccxt.async_support'. ❌❌❌")
 
26
  CCXT_ASYNC_AVAILABLE = False
27
 
28
  import numpy as np
 
30
 
31
  class TacticalData:
32
  """
33
+ (محدث V6.7)
34
+ لتخزين بيانات 1-دقيقة الحقيقية ومؤشراتها.
35
  """
36
  def __init__(self, symbol):
37
  self.symbol = symbol
38
+ self.order_book = None
39
+ self.trades = deque(maxlen=100)
40
+ self.cvd = 0.0
41
  self.large_trades = []
 
42
  self.last_update = time.time()
43
 
44
  self.confirmation_trades = defaultdict(lambda: deque(maxlen=50))
 
46
 
47
  self.last_kucoin_trade_id = None
48
  self.last_confirmation_trade_ids = defaultdict(lambda: None)
49
+
50
+ # 🔴 --- START OF CHANGE (V6.7) --- 🔴
51
+ # (حذف: self.one_min_rsi)
52
+ # (إضافة: بيانات ومؤشرات 1-دقيقة)
53
+ self.ohlcv_1m = deque(maxlen=100) # (لتخزين 100 شمعة 1-دقيقة)
54
+ self.indicators_1m = {} # (لتخزين EMA 9, EMA 21, MACD Hist)
55
+ self.last_1m_candle_timestamp = None
56
+ # 🔴 --- END OF CHANGE --- 🔴
57
 
58
  def add_trade(self, trade):
59
  """إضافة صفقة KuCoin (الأساسية)"""
60
  trade_id = trade.get('id')
61
  if trade_id and trade_id == self.last_kucoin_trade_id:
62
+ return
63
  self.last_kucoin_trade_id = trade_id
64
 
65
  self.trades.append(trade)
 
80
  """(جديد) إضافة صفقة تأكيد (Bybit, OKX, etc.)"""
81
  trade_id = trade.get('id')
82
  if trade_id and trade_id == self.last_confirmation_trade_ids[exchange_id]:
83
+ return
84
  self.last_confirmation_trade_ids[exchange_id] = trade_id
85
 
86
  self.confirmation_trades[exchange_id].append(trade)
 
95
  self.last_update = time.time()
96
 
97
  def analyze_order_book(self):
98
+ if not self.order_book: return {"bids_depth": 0, "asks_depth": 0}
99
  try:
100
  bids = self.order_book.get('bids', []); asks = self.order_book.get('asks', [])
101
  bids_depth = sum(price * amount for price, amount in bids[:10])
102
  asks_depth = sum(price * amount for price, amount in asks[:10])
103
  return {"bids_depth": bids_depth, "asks_depth": asks_depth}
104
+ except Exception: return {"bids_depth": 0, "asks_depth": 0}
105
 
106
+ # 🔴 --- START OF CHANGE (V6.7) --- 🔴
107
+ # (حذف دالة get_1m_rsi التقريبية)
108
+
109
+ def add_1m_ohlcv(self, ohlcv_data: List):
110
+ """(جديد V6.7) إضافة شموع 1-دقيقة وحساب المؤشرات"""
111
+ if not ohlcv_data:
112
+ return
113
+
114
+ # (إضافة الشموع الجديدة فقط)
115
+ new_candles_added = False
116
+ for candle in ohlcv_data:
117
+ timestamp = candle[0]
118
+ if timestamp and timestamp != self.last_1m_candle_timestamp:
119
+ if self.ohlcv_1m and timestamp < self.ohlcv_1m[-1][0]:
120
+ continue # (تجاهل الشموع القديمة إذا حدث تداخل)
121
+
122
+ self.ohlcv_1m.append(candle)
123
+ self.last_1m_candle_timestamp = timestamp
124
+ new_candles_added = True
125
+
126
+ # (حساب المؤشرات فقط إذا تغيرت البيانات)
127
+ if new_candles_added and len(self.ohlcv_1m) >= 26: # (26 كافية لـ EMA 21 و MACD)
128
+ self._analyze_1m_indicators()
129
+
130
+ def _analyze_1m_indicators(self):
131
+ """(جديد V6.7) حساب مؤشرات 1-دقيقة الحقيقية"""
132
+ if ta is None or len(self.ohlcv_1m) < 26:
133
+ self.indicators_1m = {}
134
+ return
135
+
136
  try:
137
+ # (تحويل deque إلى DataFrame للمعالجة)
138
+ df = pd.DataFrame(list(self.ohlcv_1m), columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
139
+ df[['open', 'high', 'low', 'close', 'volume']] = df[['open', 'high', 'low', 'close', 'volume']].astype(float)
140
+ close = df['close']
141
+
142
+ # (حساب المؤشرات المطلوبة)
143
+ ema_9 = ta.ema(close, length=9)
144
+ ema_21 = ta.ema(close, length=21)
145
+ macd_data = ta.macd(close, fast=12, slow=26, signal=9)
146
+
147
+ if ema_9 is not None and not ema_9.empty and \
148
+ ema_21 is not None and not ema_21.empty and \
149
+ macd_data is not None and not macd_data.empty:
150
+
151
+ self.indicators_1m = {
152
+ 'ema_9': ema_9.iloc[-1],
153
+ 'ema_21': ema_21.iloc[-1],
154
+ 'macd_hist': macd_data['MACDh_12_26_9'].iloc[-1]
155
+ }
156
+ else:
157
+ self.indicators_1m = {}
158
+
159
+ except Exception as e:
160
+ # print(f"⚠️ [Sentry] خطأ في حساب مؤشرات 1m لـ {self.symbol}: {e}")
161
+ self.indicators_1m = {}
162
+ # 🔴 --- END OF CHANGE --- 🔴
163
 
164
  def get_tactical_snapshot(self):
165
+ """(محدث V6.7) لإرجاع مؤشرات 1-دقيقة الحقيقية"""
166
  agg_cvd = sum(self.confirmation_cvd.values())
167
+
168
+ # 🔴 --- (تغيير V6.7) --- 🔴
169
  return {
170
  "cvd_kucoin": self.cvd,
171
  "cvd_confirmation_sources": dict(self.confirmation_cvd),
172
  "cvd_confirmation_aggregate": agg_cvd,
173
  "large_trades_count_5m": len([t for t in self.large_trades if t.get('timestamp') and (time.time() - t['timestamp']/1000) < 300]),
174
+ # (حذف rsi_1m_approx)
175
+ "indicators_1m": self.indicators_1m, # (إضافة المؤشرات الجديدة)
176
  "ob_analysis": self.analyze_order_book()
177
  }
178
+ # 🔴 --- نهاية التغيير --- 🔴
179
 
180
 
181
  class TradeManager:
 
200
  self.confirmation_polling_interval = 3.0
201
 
202
  async def initialize_sentry_exchanges(self):
 
 
 
203
  try:
204
  print("🔄 [Sentry] تهيئة منصات التداول (KuCoin REST ومنصات التأكيد)...")
205
 
 
237
  raise
238
 
239
  async def start_sentry_and_monitoring_loops(self):
 
240
  self.is_running = True
241
  print(f"✅ [Sentry] بدء حلقات المراقبة التكتيكية (Layer 2 - API Polling)...")
242
  while self.is_running:
 
285
  print(f"❌ [Sentry] خطأ في الحلقة الرئيسية: {error}"); traceback.print_exc(); await asyncio.sleep(60)
286
 
287
  async def stop_sentry_loops(self):
 
288
  self.is_running = False
289
  print("🛑 [Sentry] إيقاف جميع حلقات المراقبة...")
290
  for task in self.sentry_tasks.values(): task.cancel()
 
305
  except Exception as e: print(f"⚠️ [Sentry] خطأ أثناء إغلاق الاتصالات: {e}")
306
 
307
  async def update_sentry_watchlist(self, candidates: List[Dict]):
 
308
  async with self.sentry_lock:
309
  self.sentry_watchlist = {c['symbol']: c for c in candidates}
310
  print(f"ℹ️ [Sentry] تم تحديث Watchlist. عدد المرشحين: {len(self.sentry_watchlist)}")
311
 
312
  def get_sentry_status(self):
 
313
  active_monitoring_count = len(self.sentry_tasks)
314
  watchlist_symbols_list = list(self.sentry_watchlist.keys())
315
 
 
322
  }
323
 
324
  async def _monitor_symbol_activity_polling(self, symbol: str, strategy_hint: str):
 
 
 
325
  if symbol not in self.tactical_data_cache:
326
  self.tactical_data_cache[symbol] = TacticalData(symbol)
327
 
 
346
  del self.tactical_data_cache[symbol]
347
 
348
  async def _poll_kucoin_data(self, symbol):
349
+ """(محدث V6.7) حلقة استقصاء (Polling) لبيانات KuCoin (تتضمن 1m OHLCV)"""
350
  while self.is_running:
351
  try:
352
  if not self.kucoin_rest:
 
354
  await asyncio.sleep(10)
355
  continue
356
 
357
+ # 🔴 --- START OF CHANGE (V6.7) --- 🔴
358
+ # (جلب 3 أنواع بيانات بالتوازي)
359
+ tasks = {
360
+ 'ob': asyncio.create_task(self.kucoin_rest.fetch_order_book(symbol, limit=20)),
361
+ 'trades': asyncio.create_task(self.kucoin_rest.fetch_trades(symbol, since=int((time.time() - 60) * 1000), limit=50)),
362
+ 'ohlcv_1m': asyncio.create_task(self.kucoin_rest.fetch_ohlcv(symbol, '1m', limit=50))
363
+ }
364
 
365
+ await asyncio.wait(tasks.values(), return_when=asyncio.ALL_COMPLETED)
366
+
367
+ if symbol not in self.tactical_data_cache:
368
+ continue # (ربما تم إيقاف المراقبة أثناء الجلب)
369
+
370
+ # 1. معالجة دفتر الطلبات
371
+ if not tasks['ob'].exception():
372
+ self.tactical_data_cache[symbol].set_order_book(tasks['ob'].result())
373
+
374
+ # 2. معالجة آخر الصفقات
375
+ if not tasks['trades'].exception():
376
+ trades = tasks['trades'].result()
377
  trades.sort(key=lambda x: x['timestamp'])
378
  for trade in trades:
379
  self.tactical_data_cache[symbol].add_trade(trade)
380
+
381
+ # 3. معالجة شموع 1-دقيقة
382
+ if not tasks['ohlcv_1m'].exception():
383
+ self.tactical_data_cache[symbol].add_1m_ohlcv(tasks['ohlcv_1m'].result())
384
+ # 🔴 --- END OF CHANGE --- 🔴
385
 
386
  await asyncio.sleep(self.polling_interval)
387
 
 
395
  await asyncio.sleep(5)
396
 
397
  async def _poll_confirmation_data(self, symbol):
 
398
  if not self.confirmation_exchanges:
 
399
  return
400
 
401
  await asyncio.sleep(self.confirmation_polling_interval / 2)
 
416
  await asyncio.sleep(10)
417
 
418
  async def _fetch_confirmation_trades(self, ex_id: str, exchange: ccxtasync.Exchange, symbol: str):
 
419
  try:
420
  if symbol not in exchange.markets:
421
  return
 
438
 
439
 
440
  async def _run_tactical_analysis_loop(self, symbol: str, strategy_hint: str):
441
+ """(محدث V6.7) (دماغ الحارس) يشغل التحليل التكتيكي كل ثانية."""
442
  while self.is_running:
443
+ await asyncio.sleep(1)
444
  try:
445
  if self.state_manager.trade_analysis_lock.locked(): continue
446
  trade = await self.get_trade_by_symbol(symbol)
 
450
  snapshot = tactical_data.get_tactical_snapshot()
451
 
452
  if trade:
 
 
453
  exit_reason = self._check_exit_trigger(trade, snapshot, tactical_data)
454
  if exit_reason:
455
  print(f"🛑 [Sentry] زناد خروج استراتيجي لـ {symbol}: {exit_reason}")
456
 
 
457
  current_price_to_close = None
458
  if "Take Profit" in exit_reason:
 
459
  current_price_to_close = trade.get('take_profit')
460
  elif tactical_data.order_book and tactical_data.order_book.get('bids') and len(tactical_data.order_book['bids']) > 0:
 
461
  current_price_to_close = tactical_data.order_book['bids'][0][0]
462
  else:
 
463
  if tactical_data.trades:
464
  current_price_to_close = tactical_data.trades[-1].get('price')
465
 
 
472
  is_still_on_watchlist = symbol in self.sentry_watchlist
473
 
474
  if is_still_on_watchlist:
475
+ # 🔴 --- (تغيير V6.7) --- 🔴
476
+ # (snapshot يحتوي الآن على مؤشرات 1-دقيقة الحقيقية)
477
  trigger = self._check_entry_trigger(symbol, strategy_hint, snapshot)
478
  if trigger:
479
  print(f"✅ [Sentry] زناد دخول تكتيكي لـ {symbol} (استراتيجية: {strategy_hint})")
 
489
  raise
490
  except Exception as e: print(f"❌ [Sentry] خطأ في حلقة التحليل التكتيكي لـ {symbol}: {e}"); traceback.print_exc()
491
 
492
+ # 🔴 --- START OF CHANGE (V6.7) --- 🔴
493
  def _check_entry_trigger(self, symbol: str, strategy_hint: str, data: Dict) -> bool:
494
+ """(محدث V6.7) زناد ثلاثي: CVD + دفتر الطلبات + مؤشرات 1-دقيقة"""
495
 
496
+ # --- جلب البيانات ---
497
  cvd_kucoin = data.get('cvd_kucoin', 0)
 
498
 
499
+ ob_analysis = data.get('ob_analysis', {})
500
+ bids_depth = ob_analysis.get('bids_depth', 0)
501
+ asks_depth = ob_analysis.get('asks_depth', 0)
502
+
503
+ indicators_1m = data.get('indicators_1m', {})
504
+ ema_9_1m = indicators_1m.get('ema_9')
505
+ ema_21_1m = indicators_1m.get('ema_21')
506
 
507
+ # (للاستراتيجيات الأخرى)
508
+ large_trades = data.get('large_trades_count_5m', 0)
509
+
510
+ # --- منطق الزناد ---
511
  if strategy_hint in ['breakout_momentum', 'trend_following']:
512
+
513
+ # (الشرط 0: يجب أن تتوفر بيانات 1-دقيقة)
514
+ if ema_9_1m is None or ema_21_1m is None:
515
+ return False
516
+
517
+ # (الشرط 1: زخم الصفقات - CVD)
518
+ cvd_check = (cvd_kucoin > 0)
519
+
520
+ # (الشرط 2: زخم دفتر الطلبات - OB Depth)
521
+ ob_check = (bids_depth > asks_depth)
522
+
523
+ # (الشرط 3: زخم الاتجاه - 1m EMAs)
524
+ ema_check = (ema_9_1m > ema_21_1m)
525
+
526
+ if cvd_check and ob_check and ema_check:
527
+ print(f" [Trigger] {symbol} (Momentum): CVD+, OB+, 1m_EMA+. الدخول!")
528
+ return True
529
+ # (إذا فشل، لا نطبع شيئاً لتقليل التشويش)
530
 
531
  elif strategy_hint == 'mean_reversion':
532
+ # (لم نطور منطق 1-دقيقة للانعكاس بعد، نستخدم المنطق القديم)
533
+ # (ملاحظة: مؤشر rsi_1m_approx لم يعد موجوداً، لذا هذا الزناد معطل مؤقتاً)
534
+ # rsi = data.get('rsi_1m_approx', 50) # (محذوف)
535
+ # if (rsi < 35):
536
+ # print(f" [Trigger] {symbol} Reversion: RSI={rsi:.1f}")
537
+ # return True
538
+ pass # (يبقى معطلاً حتى نضيف مؤشرات انعكاس 1-دقيقة)
539
 
540
  elif strategy_hint == 'volume_spike':
541
  if (large_trades > 0):
542
+ print(f" [Trigger] {symbol} Volume Spike: LargeTrades={large_trades}")
543
  return True
544
 
545
  return False
546
+ # 🔴 --- END OF CHANGE --- 🔴
547
 
 
548
  def _check_exit_trigger(self, trade: Dict, data: Dict, tactical_data: TacticalData) -> str:
549
  """(محدث V6.6) يراقب وقف الخسارة وجني الأرباح باستخدام (Bid) و (Last Trade Price)"""
550
 
 
552
  hard_stop = trade.get('stop_loss')
553
  take_profit = trade.get('take_profit')
554
 
 
 
 
555
  best_bid_price = None
556
  if tactical_data.order_book and tactical_data.order_book.get('bids') and len(tactical_data.order_book['bids']) > 0:
557
  best_bid_price = tactical_data.order_book['bids'][0][0]
558
 
 
559
  last_trade_price = None
560
+ if tactical_data.trades:
561
  try:
562
  last_trade_price = tactical_data.trades[-1].get('price')
563
  except (IndexError, AttributeError):
564
+ pass
565
 
 
566
  if best_bid_price is None and last_trade_price is None:
567
+ return None
568
 
 
569
  current_price_for_sl = best_bid_price if best_bid_price is not None else last_trade_price
570
 
 
571
  current_price_for_tp = max(
572
  filter(None, [best_bid_price, last_trade_price]),
573
  default=None
574
  )
575
 
 
576
  if hard_stop and current_price_for_sl and current_price_for_sl <= hard_stop:
577
  return f"Strategic Stop Loss hit: {current_price_for_sl} <= {hard_stop}"
578
 
 
579
  if take_profit and current_price_for_tp and current_price_for_tp >= take_profit:
580
  return f"Strategic Take Profit hit: {current_price_for_tp} >= {take_profit}"
581
 
582
+ return None
 
583
 
584
 
585
  async def _execute_smart_entry(self, symbol: str, strategy_hint: str, tactical_data: Dict, explorer_context: Dict):
 
629
  exit_profile = llm_decision.get('exit_profile', 'ATR_TRAILING')
630
  exit_parameters = llm_decision.get('exit_parameters', {})
631
 
 
632
  if not (stop_loss_price and take_profit_price):
633
  print(f"❌ [Executor] {symbol}: بيانات SL/TP غير صالحة من النموذج. تم الإلغاء.")
634
  return
 
734
  raise
735
 
736
  async def close_trade(self, trade_to_close, close_price, reason="System Close"):
 
737
  try:
738
  symbol = trade_to_close.get('symbol'); trade_to_close['status'] = 'CLOSED'
739
  trade_to_close['close_price'] = close_price; trade_to_close['close_timestamp'] = datetime.now().isoformat()
 
768
  except Exception as e: print(f"❌ [Executor] فشل فادح أثناء إغلاق الصفقة (الوهمية) {symbol}: {e}"); traceback.print_exc(); raise
769
 
770
  async def immediate_close_trade(self, symbol, close_price, reason="Immediate Close"):
 
771
  if not self.r2_service.acquire_lock(): print(f"⚠️ [Executor] فشل في الحصول على قفل R2 لـ {symbol} (Immediate Close)"); return False
772
  try:
773
  open_trades = await self.r2_service.get_open_trades_async()
 
780
  if self.r2_service.lock_acquired: self.r2_service.release_lock()
781
 
782
  async def update_trade_strategy(self, trade_to_update, re_analysis_decision):
 
783
  try:
784
  symbol = trade_to_update.get('symbol')
785
  if re_analysis_decision.get('action') == "UPDATE_TRADE":
 
839
  except Exception as e: print(f"❌ Failed to get trade by symbol {symbol}: {e}"); return None
840
 
841
 
842
+ print(f"✅ Trade Manager loaded - V6.7 (1M Indicator & OB Trigger) (ccxt.async_support: {CCXT_ASYNC_AVAILABLE})")