Riy777 commited on
Commit
708a613
·
1 Parent(s): 0e0d803

Update trade_manager.py

Browse files
Files changed (1) hide show
  1. trade_manager.py +273 -118
trade_manager.py CHANGED
@@ -1,40 +1,227 @@
1
- # trade_manager.py
2
  import asyncio
3
  import json
4
  import time
5
  import traceback
6
  from datetime import datetime, timedelta
7
  from helpers import safe_float_conversion, _apply_patience_logic
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
  class TradeManager:
10
- def __init__(self, r2_service, learning_engine=None, data_manager=None):
11
  self.r2_service = r2_service
12
  self.learning_engine = learning_engine
13
  self.data_manager = data_manager
 
14
  self.monitoring_tasks = {}
15
  self.is_running = False
16
  self.monitoring_errors = {}
17
  self.max_consecutive_errors = 5
 
18
 
19
  async def open_trade(self, symbol, decision, current_price):
20
  try:
21
- # الإصلاح: التحقق من نوع الصفقة ومنع صفقات SHORT
22
- trade_type = decision.get("trade_type", "LONG") # الافتراضي هو LONG
23
  if trade_type == "SHORT":
24
  print(f"⚠️ تم رفض فتح صفقة SHORT لـ {symbol}. النظام مصمم لـ SPOT فقط.")
25
  await self.r2_service.save_system_logs_async({
26
- "trade_open_rejected": True,
27
- "reason": "SHORT trade not allowed for SPOT system",
28
- "symbol": symbol,
29
- "llm_decision": decision
30
  })
31
- return None # لا تفتح الصفقة
32
 
33
  portfolio_state = await self.r2_service.get_portfolio_state_async()
34
  available_capital = portfolio_state.get("current_capital_usd", 0)
35
 
36
  if available_capital < 1:
37
- print(f"❌ رأس المال غير كافي لفتح صفقة لـ {symbol}: {available_capital}")
38
  return None
39
 
40
  expected_target_minutes = decision.get('expected_target_minutes', 15)
@@ -44,6 +231,10 @@ class TradeManager:
44
  strategy = decision.get('strategy')
45
  if not strategy or strategy == 'unknown':
46
  strategy = 'GENERIC'
 
 
 
 
47
 
48
  trades = await self.r2_service.get_open_trades_async()
49
  new_trade = {
@@ -51,11 +242,12 @@ class TradeManager:
51
  "symbol": symbol,
52
  "entry_price": current_price,
53
  "entry_timestamp": datetime.now().isoformat(),
54
- "decision_data": decision,
55
  "status": "OPEN",
56
- "stop_loss": decision.get("stop_loss"),
57
- "take_profit": decision.get("take_profit"),
58
- "trade_type": "LONG", # الإصلاح: فرض LONG دائماً لـ SPOT
 
59
  "position_size_usd": available_capital,
60
  "expected_target_minutes": expected_target_minutes,
61
  "expected_target_time": expected_target_time,
@@ -73,23 +265,17 @@ class TradeManager:
73
  await self.r2_service.save_portfolio_state_async(portfolio_state)
74
 
75
  await self.r2_service.save_system_logs_async({
76
- "new_trade_opened": True,
77
- "symbol": symbol,
78
- "position_size": available_capital,
79
- "expected_minutes": expected_target_minutes,
80
- "trade_type": "LONG", # تم فرض LONG
81
- "strategy": strategy
82
  })
83
 
84
- print(f"✅ تم فتح صفقة جديدة (LONG) لـ {symbol} باستراتيجية {strategy}")
85
  return new_trade
86
 
87
  except Exception as e:
88
  print(f"❌ فشل فتح صفقة لـ {symbol}: {e}")
89
  await self.r2_service.save_system_logs_async({
90
- "trade_open_failed": True,
91
- "symbol": symbol,
92
- "error": str(e)
93
  })
94
  raise
95
 
@@ -103,8 +289,7 @@ class TradeManager:
103
 
104
  entry_price = trade_to_close['entry_price']
105
  position_size = trade_to_close['position_size_usd']
106
- # الإصلاح: بما أن النظام SPOT فقط، نفترض دائماً LONG لحساب PnL
107
- trade_type = "LONG" # trade_to_close.get('trade_type', 'LONG')
108
  strategy = trade_to_close.get('strategy', 'unknown')
109
 
110
  pnl = 0.0
@@ -112,14 +297,8 @@ class TradeManager:
112
 
113
  if entry_price and entry_price > 0 and close_price and close_price > 0:
114
  try:
115
- # ✅ الإصلاح: استخدام معادلة LONG فقط
116
- # if trade_type == 'LONG':
117
  pnl_percent = ((close_price - entry_price) / entry_price) * 100
118
  pnl = position_size * (pnl_percent / 100)
119
- # elif trade_type == 'SHORT': # <-- إزالة هذا القسم
120
- # pnl_percent = ((entry_price - close_price) / entry_price) * 100
121
- # pnl = position_size * (pnl_percent / 100)
122
-
123
  except (TypeError, ZeroDivisionError) as calc_error:
124
  pnl = 0.0
125
  pnl_percent = 0.0
@@ -132,7 +311,6 @@ class TradeManager:
132
 
133
  portfolio_state = await self.r2_service.get_portfolio_state_async()
134
  current_capital = portfolio_state.get("current_capital_usd", 0)
135
- invested_capital = portfolio_state.get("invested_capital_usd", 0)
136
 
137
  new_capital = current_capital + position_size + pnl
138
 
@@ -151,27 +329,18 @@ class TradeManager:
151
  trades_to_keep = [t for t in open_trades if t.get('id') != trade_to_close.get('id')]
152
  await self.r2_service.save_open_trades_async(trades_to_keep)
153
 
154
- # إزالة من المهام النشطة
155
  if symbol in self.monitoring_tasks:
156
  del self.monitoring_tasks[symbol]
157
  if symbol in self.monitoring_errors:
158
  del self.monitoring_errors[symbol]
159
 
160
  await self.r2_service.save_system_logs_async({
161
- "trade_closed": True,
162
- "symbol": symbol,
163
- "entry_price": entry_price,
164
- "close_price": close_price,
165
- "pnl_usd": pnl,
166
- "pnl_percent": pnl_percent,
167
- "new_capital": new_capital,
168
- "strategy": strategy,
169
- "position_size": position_size,
170
- "trade_type": trade_type, # سيظل LONG دائماً
171
- "reason": reason
172
  })
173
 
174
  if self.learning_engine and self.learning_engine.initialized:
 
175
  await self.learning_engine.analyze_trade_outcome(trade_to_close, reason)
176
 
177
  print(f"✅ تم إغلاق صفقة {symbol} - السبب: {reason} - الربح: {pnl_percent:+.2f}%")
@@ -180,19 +349,26 @@ class TradeManager:
180
  except Exception as e:
181
  print(f"❌ فشل إغلاق صفقة {trade_to_close.get('symbol')}: {e}")
182
  await self.r2_service.save_system_logs_async({
183
- "trade_close_failed": True,
184
- "symbol": trade_to_close.get('symbol'),
185
- "error": str(e)
186
  })
187
  raise
188
 
189
  async def update_trade(self, trade_to_update, re_analysis_decision):
190
  try:
191
  symbol = trade_to_update.get('symbol')
192
- if re_analysis_decision.get('new_stop_loss'):
 
 
193
  trade_to_update['stop_loss'] = re_analysis_decision['new_stop_loss']
194
- if re_analysis_decision.get('new_take_profit'):
195
  trade_to_update['take_profit'] = re_analysis_decision['new_take_profit']
 
 
 
 
 
 
 
 
196
 
197
  new_expected_minutes = re_analysis_decision.get('new_expected_minutes')
198
  if new_expected_minutes:
@@ -205,9 +381,9 @@ class TradeManager:
205
  original_strategy = re_analysis_decision.get('strategy', 'GENERIC')
206
 
207
  trade_to_update['strategy'] = original_strategy
208
- trade_to_update['decision_data'] = re_analysis_decision
209
  trade_to_update['is_monitored'] = True
210
- trade_to_update['trade_type'] = "LONG" # ✅ الإصلاح: التأكد من بقائها LONG
211
 
212
  open_trades = await self.r2_service.get_open_trades_async()
213
  for i, trade in enumerate(open_trades):
@@ -218,11 +394,9 @@ class TradeManager:
218
  await self.r2_service.save_open_trades_async(open_trades)
219
 
220
  await self.r2_service.save_system_logs_async({
221
- "trade_updated": True,
222
- "symbol": symbol,
223
- "new_expected_minutes": new_expected_minutes,
224
- "action": "UPDATE_TRADE",
225
- "strategy": original_strategy
226
  })
227
 
228
  print(f"✅ تم تحديث صفقة {symbol}")
@@ -256,7 +430,7 @@ class TradeManager:
256
 
257
  async def start_trade_monitoring(self):
258
  self.is_running = True
259
- print("🔍 بدء مراقبة الصفقات...")
260
 
261
  while self.is_running:
262
  try:
@@ -266,23 +440,20 @@ class TradeManager:
266
  for trade in open_trades:
267
  symbol = trade['symbol']
268
 
269
- # تخطي الصفقات التي تجاوزت حد الأخطاء
270
  if self.monitoring_errors.get(symbol, 0) >= self.max_consecutive_errors:
271
  if symbol in self.monitoring_tasks:
272
  del self.monitoring_tasks[symbol]
273
  continue
274
 
275
- # بدء المراقبة إذا لم تكن نشطة
276
  if symbol not in self.monitoring_tasks:
277
  task = asyncio.create_task(self._monitor_single_trade(trade))
278
  self.monitoring_tasks[symbol] = {
279
  'task': task,
280
  'start_time': current_time,
281
- 'trade': trade
282
  }
283
  trade['monitoring_started'] = True
284
 
285
- # تنظيف المهام المنتهية
286
  current_symbols = {trade['symbol'] for trade in open_trades}
287
  for symbol in list(self.monitoring_tasks.keys()):
288
  if symbol not in current_symbols:
@@ -293,31 +464,40 @@ class TradeManager:
293
  if symbol in self.monitoring_errors:
294
  del self.monitoring_errors[symbol]
295
 
296
- await asyncio.sleep(10)
297
 
298
  except Exception as error:
299
  print(f"❌ خطأ في مراقبة الصفقات: {error}")
300
  await asyncio.sleep(30)
301
 
302
- async def _monitor_single_trade(self, trade):
303
- symbol = trade['symbol']
304
- max_monitoring_time = 3600 # أقصى وقت مراقبة: ساعة واحدة
 
 
 
 
 
305
 
306
- print(f"🔍 بدء مراقبة الصفقة: {symbol}")
307
 
308
  while (symbol in self.monitoring_tasks and
309
  self.is_running and
310
  self.monitoring_errors.get(symbol, 0) < self.max_consecutive_errors):
311
 
312
  try:
313
- start_time = time.time()
314
-
 
 
 
 
315
  if not self.data_manager:
316
  print(f"⚠️ DataManager غير متوفر لـ {symbol}")
317
  await asyncio.sleep(15)
318
  continue
319
 
320
- # الحصول على السعر الحالي مع مهلة زمنية
321
  try:
322
  current_price = await asyncio.wait_for(
323
  self.data_manager.get_latest_price_async(symbol),
@@ -335,39 +515,27 @@ class TradeManager:
335
  await asyncio.sleep(15)
336
  continue
337
 
338
- entry_price = trade['entry_price']
339
- stop_loss = trade.get('stop_loss')
340
- take_profit = trade.get('take_profit')
341
- # ✅ الإصلاح: نفترض LONG دائماً عند التحقق من الإغلاق لـ SPOT
342
- trade_type = "LONG"
343
- should_close, close_reason = False, ""
344
-
345
- # التحقق من شروط الإغلاق (مع افتراض LONG)
346
- if stop_loss and current_price <= stop_loss:
347
- should_close, close_reason = True, f"وصول وقف الخسارة (LONG): {current_price} <= {stop_loss}"
348
- elif take_profit and current_price >= take_profit:
349
- should_close, close_reason = True, f"وصول جني الأرباح (LONG): {current_price} >= {take_profit}"
350
 
351
- # تحديث وقف الخسارة الديناميكي (Trailing Stop Loss) لصفقات LONG
352
- if not should_close and current_price > entry_price:
353
- # مثال بسيط: تحديد وقف خسارة عند 2% تحت السعر الحالي إذا كان أعلى من وقف الخسارة الأصلي
354
- potential_new_stop = current_price * 0.98
355
- current_stop_loss = trade.get('stop_loss', 0) or 0 # التعامل مع None
356
- if potential_new_stop > current_stop_loss:
357
- trade['stop_loss'] = potential_new_stop
358
- print(f"📈 {symbol}: تم تحديث وقف الخسارة الديناميكي إلى {potential_new_stop:.6f}")
359
- # ملاحظة: هذا التحديث محلي للمهمة، قد تحتاج لحفظه في R2 إذا أردت استمراره
360
 
361
- # إغلاق الصفقة إذا لزم الأمر
362
  if should_close:
363
- if self.r2_service.acquire_lock():
 
364
  try:
365
- # قبل الإغلاق، تأكد من جلب أحدث نسخة من الصفقة من R2
366
  latest_trade_data = await self.r2_service.get_trade_by_symbol_async(symbol)
367
  if latest_trade_data and latest_trade_data['status'] == 'OPEN':
368
- await self.immediate_close_trade(symbol, current_price, close_reason)
369
  else:
370
- print(f"⚠️ الصفقة {symbol} لم تعد مفتوحة أو غير موجودة، تم إلغاء الإغلاق.")
371
  except Exception as close_error:
372
  print(f"❌ فشل الإغلاق التلقائي لـ {symbol}: {close_error}")
373
  finally:
@@ -375,42 +543,28 @@ class TradeManager:
375
 
376
  break # الخروج من حلقة المراقبة بعد محاولة الإغلاق
377
 
378
- # إعادة تعيين عداد الأخطاء عند النجاح
379
  if symbol in self.monitoring_errors:
380
  self.monitoring_errors[symbol] = 0
381
 
382
- # التحقق من وقت المراقبة الطويل
383
- monitoring_duration = time.time() - self.monitoring_tasks[symbol]['start_time'] # استخدام وقت البدء الأصلي
384
- if monitoring_duration > max_monitoring_time:
385
- print(f"🕒 انتهى وقت مراقبة الصفقة {symbol} ({monitoring_duration:.0f}s > {max_monitoring_time}s)")
386
- # يمكنك إضافة منطق هنا لإغلاق الصفقة إذا طالت مدتها، أو تركها لإعادة التحليل
387
- break
388
-
389
- await asyncio.sleep(15) # انتظار 15 ثانية بين كل فحص
390
 
391
  except Exception as error:
392
  error_count = self._increment_monitoring_error(symbol)
393
  print(f"❌ خطأ في مراقبة {symbol} (الخطأ #{error_count}): {error}")
 
394
 
395
  if error_count >= self.max_consecutive_errors:
396
  print(f"🚨 إيقاف مراقبة {symbol} بسبب الأخطاء المتتالية")
397
  await self.r2_service.save_system_logs_async({
398
- "monitoring_stopped": True,
399
- "symbol": symbol,
400
- "error_count": error_count,
401
- "error": str(error)
402
  })
403
- # اختياري: محاولة إغلاق الصفقة كإجراء أخير
404
- # current_price_fallback = await self.data_manager.get_latest_price_async(symbol)
405
- # if current_price_fallback and self.r2_service.acquire_lock():
406
- # try: await self.immediate_close_trade(symbol, current_price_fallback, "Forced close due to monitoring errors")
407
- # finally: self.r2_service.release_lock()
408
  break # الخروج من الحلقة بسبب الأخطاء
409
 
410
  await asyncio.sleep(30) # انتظار أطول بعد الخطأ
411
 
412
- print(f"🛑 توقيف مراقبة الصفقة: {symbol}")
413
- # التأكد من إزالة المهمة عند الخروج من الحلقة
414
  if symbol in self.monitoring_tasks:
415
  del self.monitoring_tasks[symbol]
416
  if symbol in self.monitoring_errors:
@@ -446,7 +600,6 @@ class TradeManager:
446
 
447
  history.append(closed_trade)
448
 
449
- # حفظ آخر 1000 صفقة فقط
450
  if len(history) > 1000:
451
  history = history[-1000:]
452
 
@@ -528,4 +681,6 @@ class TradeManager:
528
  'active_tasks': len(self.monitoring_tasks),
529
  'monitoring_errors': dict(self.monitoring_errors),
530
  'max_consecutive_errors': self.max_consecutive_errors
531
- }
 
 
 
1
+ # trade_manager.py (محدث بالكامل مع محرك الخروج الديناميكي وإدارة التعارض)
2
  import asyncio
3
  import json
4
  import time
5
  import traceback
6
  from datetime import datetime, timedelta
7
  from helpers import safe_float_conversion, _apply_patience_logic
8
+ # 🔴 جديد: نحتاج إلى pandas لحساب ATR في المراقب
9
+ try:
10
+ import pandas as pd
11
+ import pandas_ta as ta
12
+ PANDAS_TA_AVAILABLE = True
13
+ except ImportError:
14
+ PANDAS_TA_AVAILABLE = False
15
+ print("⚠️ pandas_ta not available. ATR Trailing Stop will be disabled.")
16
+
17
+
18
+ # 🔴 جديد: محرك الخروج الديناميكي (كلاس داخلي أو مساعد)
19
+ class _DynamicExitEngine:
20
+ def __init__(self, data_manager):
21
+ self.data_manager = data_manager
22
+
23
+ async def evaluate_exit(self, trade: dict, current_price: float):
24
+ """
25
+ يقيّم ما إذا كان يجب إغلاق الصفقة بناءً على ملف الخروج الخاص بها.
26
+ يرجع: (should_close, close_reason, updated_trade_object)
27
+ """
28
+ if not PANDAS_TA_AVAILABLE:
29
+ # إذا لم تكن المكتبات متاحة، نعود إلى الوقف الثابت البسيط
30
+ return await self._evaluate_fixed_exit(trade, current_price)
31
+
32
+ try:
33
+ exit_profile = trade.get('decision_data', {}).get('exit_profile', 'FIXED_TARGET')
34
+ exit_params = trade.get('decision_data', {}).get('exit_parameters', {})
35
+
36
+ # 1. التحقق من الوقف الكارثي (Hard Stop) والهدف النهائي (Take Profit) دائماً
37
+ hard_stop = trade.get('stop_loss')
38
+ take_profit = trade.get('take_profit')
39
+
40
+ if hard_stop and current_price <= hard_stop:
41
+ return True, f"Hard Stop Loss hit: {current_price} <= {hard_stop}", trade
42
+ if take_profit and current_price >= take_profit:
43
+ return True, f"Final Take Profit hit: {current_price} >= {take_profit}", trade
44
+
45
+ # 2. التحقق من الوقف الديناميكي (المتحرك) إذا كان موجوداً
46
+ dynamic_stop = trade.get('dynamic_stop_loss')
47
+ if dynamic_stop and current_price <= dynamic_stop:
48
+ return True, f"Dynamic Trailing Stop hit: {current_price} <= {dynamic_stop}", trade
49
+
50
+ # 3. تنفيذ منطق ملف الخروج
51
+ if exit_profile == "ATR_TRAILING":
52
+ return await self._evaluate_atr_trailing(trade, current_price, exit_params)
53
+ elif exit_profile == "TIME_BASED":
54
+ return await self._evaluate_time_based(trade, current_price, exit_params)
55
+ elif exit_profile == "FIXED_TARGET":
56
+ # تم التعامل معه بالفعل (TP/SL)، لكن يمكن إضافة منطق نقطة التعادل
57
+ return await self._evaluate_break_even(trade, current_price, exit_params.get("break_even_trigger_percent", 0))
58
+ elif exit_profile == "SIGNAL_BASED":
59
+ # (منطق الطوارئ - يمكن توسيعه لاحقاً)
60
+ # مثال: التحقق من ارتفاع حجم البيع
61
+ # volume_spike = await self._check_emergency_signals(trade['symbol'])
62
+ # if volume_spike:
63
+ # return True, f"Emergency Signal Stop (Volume Spike)", trade
64
+ pass
65
+
66
+ # إذا لم يتم تشغيل أي شيء، تحقق من نقطة التعادل (إذا لم تكن جزءاً من ATR)
67
+ if exit_profile != "ATR_TRAILING":
68
+ return await self._evaluate_break_even(trade, current_price, exit_params.get("break_even_trigger_percent", 0))
69
+
70
+ return False, "No exit criteria met", trade
71
+
72
+ except Exception as e:
73
+ print(f"❌ Error in DynamicExitEngine for {trade.get('symbol')}: {e}")
74
+ traceback.print_exc()
75
+ # في حالة الخطأ، نعود إلى الوقف الثابت الآمن
76
+ return await self._evaluate_fixed_exit(trade, current_price)
77
+
78
+ async def _evaluate_fixed_exit(self, trade: dict, current_price: float):
79
+ """الخروج الآمن (الافتراضي): الوقف الثابت والهدف الثابت فقط"""
80
+ hard_stop = trade.get('stop_loss')
81
+ take_profit = trade.get('take_profit')
82
+
83
+ if hard_stop and current_price <= hard_stop:
84
+ return True, f"Hard Stop Loss hit: {current_price} <= {hard_stop}", trade
85
+ if take_profit and current_price >= take_profit:
86
+ return True, f"Final Take Profit hit: {current_price} >= {take_profit}", trade
87
+
88
+ # التحقق من الوقف الديناميكي (إذا تم تعيينه مسبقاً)
89
+ dynamic_stop = trade.get('dynamic_stop_loss')
90
+ if dynamic_stop and current_price <= dynamic_stop:
91
+ return True, f"Dynamic Stop hit: {current_price} <= {dynamic_stop}", trade
92
+
93
+ return False, "No exit criteria met", trade
94
+
95
+ async def _evaluate_atr_trailing(self, trade: dict, current_price: float, params: dict):
96
+ """حساب الوقف المتحرك بناءً على ATR"""
97
+ try:
98
+ atr_period = params.get("atr_period", 14)
99
+ atr_multiplier = params.get("atr_multiplier", 2.0)
100
+ break_even_trigger_percent = params.get("break_even_trigger_percent", 0) # مثال: 1.5%
101
+
102
+ # 1. جلب بيانات الشموع لحساب ATR (نستخدم إطار زمني قصير مثل 15m)
103
+ ohlcv_data = await self._fetch_ohlcv_for_atr(trade['symbol'], '15m', atr_period + 50) # +50 لتهيئة المؤشر
104
+ if ohlcv_data is None:
105
+ return False, "ATR data unavailable", trade # لا يمكن الحساب، استمر
106
+
107
+ # 2. حساب ATR
108
+ df = pd.DataFrame(ohlcv_data, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
109
+ df['atr'] = ta.atr(df['high'], df['low'], df['close'], length=atr_period)
110
+
111
+ if df['atr'].dropna().empty:
112
+ return False, "ATR calculation failed", trade
113
+
114
+ current_atr = df['atr'].iloc[-1]
115
+ if current_atr is None or current_atr == 0:
116
+ return False, "Invalid ATR value", trade
117
+
118
+ # 3. حساب سعر الوقف الجديد بناءً على ATR
119
+ new_atr_stop = current_price - (current_atr * atr_multiplier)
120
+
121
+ # 4. التحقق من نقطة التعادل (Break Even)
122
+ current_dynamic_stop = trade.get('dynamic_stop_loss', 0)
123
+ entry_price = trade.get('entry_price')
124
+
125
+ if break_even_trigger_percent > 0:
126
+ trigger_price = entry_price * (1 + break_even_trigger_percent / 100)
127
+ if current_price >= trigger_price:
128
+ # إذا وصلنا لهدف نقطة التعادل، الوقف الجديد *على الأقل* سعر الدخول
129
+ new_atr_stop = max(new_atr_stop, entry_price)
130
+
131
+ # 5. تحديث الوقف الديناميكي
132
+ # الوقف المتحرك يجب أن يرتفع فقط، لا ينخفض
133
+ if new_atr_stop > current_dynamic_stop:
134
+ trade['dynamic_stop_loss'] = new_atr_stop
135
+ print(f"📈 {trade['symbol']}: ATR Trailing Stop updated to {new_atr_stop:.6f}")
136
+ # ملاحظة: هذا التحديث محلي للمهمة فقط (لا يحفظ في R2 كل دقيقة)
137
+
138
+ # سيتم التحقق من الوقف المحدث في الدورة التالية (أو في بداية هذه الدالة)
139
+ return False, "ATR Trailing logic executed", trade
140
+
141
+ except Exception as e:
142
+ print(f"❌ Error in ATR Trailing logic for {trade.get('symbol')}: {e}")
143
+ return False, "ATR logic failed", trade # فشل آمن، لا تغلق الصفقة
144
+
145
+ async def _evaluate_time_based(self, trade: dict, current_price: float, params: dict):
146
+ """التحقق من الوقف الزمني"""
147
+ exit_after_minutes = params.get("exit_after_minutes", 0)
148
+ if exit_after_minutes <= 0:
149
+ return False, "Time stop not configured", trade
150
+
151
+ entry_time = datetime.fromisoformat(trade.get('entry_timestamp'))
152
+ elapsed_minutes = (datetime.now() - entry_time).total_seconds() / 60
153
+
154
+ if elapsed_minutes >= exit_after_minutes:
155
+ return True, f"Time Stop hit: {elapsed_minutes:.1f}m >= {exit_after_minutes}m", trade
156
+
157
+ return False, "Time stop not hit", trade
158
+
159
+ async def _evaluate_break_even(self, trade: dict, current_price: float, break_even_trigger_percent: float):
160
+ """منطق نقطة التعادل (إذا لم يكن جزءاً من ATR)"""
161
+ if break_even_trigger_percent <= 0:
162
+ return False, "Break-even not configured", trade
163
+
164
+ entry_price = trade.get('entry_price')
165
+ current_dynamic_stop = trade.get('dynamic_stop_loss', 0)
166
+
167
+ # إذا كان الوقف الحالي أقل من سعر الدخول
168
+ if current_dynamic_stop < entry_price:
169
+ trigger_price = entry_price * (1 + break_even_trigger_percent / 100)
170
+ if current_price >= trigger_price:
171
+ trade['dynamic_stop_loss'] = entry_price # ارفع الوقف إلى سعر الدخول
172
+ print(f"📈 {trade['symbol']}: Break-Even Stop activated at {entry_price:.6f}")
173
+
174
+ return False, "Break-even logic executed", trade
175
+
176
+ async def _fetch_ohlcv_for_atr(self, symbol, timeframe, limit):
177
+ """جلب بيانات الشموع لحساب ATR (دالة مساعدة)"""
178
+ try:
179
+ if not self.data_manager or not self.data_manager.exchange:
180
+ return None
181
+ # جلب البيان��ت بشكل متزامن (لأننا داخل مهمة asyncio بالفعل)
182
+ # هذه الدالة (fetch_ohlcv) في ccxt ليست async
183
+ loop = asyncio.get_event_loop()
184
+ ohlcv_data = await loop.run_in_executor(
185
+ None,
186
+ lambda: self.data_manager.exchange.fetch_ohlcv(symbol, timeframe, limit=limit)
187
+ )
188
+
189
+ if ohlcv_data and len(ohlcv_data) >= limit:
190
+ return ohlcv_data
191
+ else:
192
+ return None
193
+ except Exception as e:
194
+ print(f"⚠️ Failed to fetch OHLCV for ATR ({symbol}): {e}")
195
+ return None
196
+
197
 
198
  class TradeManager:
199
+ def __init__(self, r2_service, learning_engine=None, data_manager=None, state_manager=None): # 🔴 جديد
200
  self.r2_service = r2_service
201
  self.learning_engine = learning_engine
202
  self.data_manager = data_manager
203
+ self.state_manager = state_manager # 🔴 جديد: لإدارة التعارض
204
  self.monitoring_tasks = {}
205
  self.is_running = False
206
  self.monitoring_errors = {}
207
  self.max_consecutive_errors = 5
208
+ self.exit_engine = _DynamicExitEngine(self.data_manager) # 🔴 جديد: تهيئة محرك الخروج
209
 
210
  async def open_trade(self, symbol, decision, current_price):
211
  try:
212
+ trade_type = decision.get("trade_type", "LONG")
 
213
  if trade_type == "SHORT":
214
  print(f"⚠️ تم رفض فتح صفقة SHORT لـ {symbol}. النظام مصمم لـ SPOT فقط.")
215
  await self.r2_service.save_system_logs_async({
216
+ "trade_open_rejected": True, "reason": "SHORT trade not allowed", "symbol": symbol
 
 
 
217
  })
218
+ return None
219
 
220
  portfolio_state = await self.r2_service.get_portfolio_state_async()
221
  available_capital = portfolio_state.get("current_capital_usd", 0)
222
 
223
  if available_capital < 1:
224
+ print(f"❌ رأس المال غير كافي لـ {symbol}: {available_capital}")
225
  return None
226
 
227
  expected_target_minutes = decision.get('expected_target_minutes', 15)
 
231
  strategy = decision.get('strategy')
232
  if not strategy or strategy == 'unknown':
233
  strategy = 'GENERIC'
234
+
235
+ # 🔴 جديد: جلب ملف الخروج والمعاملات
236
+ exit_profile = decision.get('exit_profile', 'FIXED_TARGET') # الافتراضي هو الوقف الثابت
237
+ exit_parameters = decision.get('exit_parameters', {})
238
 
239
  trades = await self.r2_service.get_open_trades_async()
240
  new_trade = {
 
242
  "symbol": symbol,
243
  "entry_price": current_price,
244
  "entry_timestamp": datetime.now().isoformat(),
245
+ "decision_data": decision, # 🔴 هذا يحتوي الآن على ملف الخروج
246
  "status": "OPEN",
247
+ "stop_loss": decision.get("stop_loss"), # الوقف الكارثي
248
+ "take_profit": decision.get("take_profit"), # الهدف النهائي
249
+ "dynamic_stop_loss": decision.get("stop_loss"), # 🔴 جديد: يبدأ الوقف المتحرك عند الوقف الكارثي
250
+ "trade_type": "LONG",
251
  "position_size_usd": available_capital,
252
  "expected_target_minutes": expected_target_minutes,
253
  "expected_target_time": expected_target_time,
 
265
  await self.r2_service.save_portfolio_state_async(portfolio_state)
266
 
267
  await self.r2_service.save_system_logs_async({
268
+ "new_trade_opened": True, "symbol": symbol, "position_size": available_capital,
269
+ "strategy": strategy, "exit_profile": exit_profile # 🔴 جديد: تسجيل ملف الخروج
 
 
 
 
270
  })
271
 
272
+ print(f"✅ تم فتح صفقة جديدة (LONG) لـ {symbol} (استراتيجية: {strategy} | ملف خروج: {exit_profile})")
273
  return new_trade
274
 
275
  except Exception as e:
276
  print(f"❌ فشل فتح صفقة لـ {symbol}: {e}")
277
  await self.r2_service.save_system_logs_async({
278
+ "trade_open_failed": True, "symbol": symbol, "error": str(e)
 
 
279
  })
280
  raise
281
 
 
289
 
290
  entry_price = trade_to_close['entry_price']
291
  position_size = trade_to_close['position_size_usd']
292
+ trade_type = "LONG"
 
293
  strategy = trade_to_close.get('strategy', 'unknown')
294
 
295
  pnl = 0.0
 
297
 
298
  if entry_price and entry_price > 0 and close_price and close_price > 0:
299
  try:
 
 
300
  pnl_percent = ((close_price - entry_price) / entry_price) * 100
301
  pnl = position_size * (pnl_percent / 100)
 
 
 
 
302
  except (TypeError, ZeroDivisionError) as calc_error:
303
  pnl = 0.0
304
  pnl_percent = 0.0
 
311
 
312
  portfolio_state = await self.r2_service.get_portfolio_state_async()
313
  current_capital = portfolio_state.get("current_capital_usd", 0)
 
314
 
315
  new_capital = current_capital + position_size + pnl
316
 
 
329
  trades_to_keep = [t for t in open_trades if t.get('id') != trade_to_close.get('id')]
330
  await self.r2_service.save_open_trades_async(trades_to_keep)
331
 
 
332
  if symbol in self.monitoring_tasks:
333
  del self.monitoring_tasks[symbol]
334
  if symbol in self.monitoring_errors:
335
  del self.monitoring_errors[symbol]
336
 
337
  await self.r2_service.save_system_logs_async({
338
+ "trade_closed": True, "symbol": symbol, "pnl_usd": pnl, "pnl_percent": pnl_percent,
339
+ "new_capital": new_capital, "strategy": strategy, "trade_type": trade_type, "reason": reason
 
 
 
 
 
 
 
 
 
340
  })
341
 
342
  if self.learning_engine and self.learning_engine.initialized:
343
+ # 🔴 نمرر الصفقة كاملة (تحتوي على decision_data -> exit_profile)
344
  await self.learning_engine.analyze_trade_outcome(trade_to_close, reason)
345
 
346
  print(f"✅ تم إغلاق صفقة {symbol} - السبب: {reason} - الربح: {pnl_percent:+.2f}%")
 
349
  except Exception as e:
350
  print(f"❌ فشل إغلاق صفقة {trade_to_close.get('symbol')}: {e}")
351
  await self.r2_service.save_system_logs_async({
352
+ "trade_close_failed": True, "symbol": trade_to_close.get('symbol'), "error": str(e)
 
 
353
  })
354
  raise
355
 
356
  async def update_trade(self, trade_to_update, re_analysis_decision):
357
  try:
358
  symbol = trade_to_update.get('symbol')
359
+
360
+ # 🔴 جديد: تحديث ملف الخروج والمعاملات
361
+ if re_analysis_decision.get('action') == "UPDATE_TRADE":
362
  trade_to_update['stop_loss'] = re_analysis_decision['new_stop_loss']
 
363
  trade_to_update['take_profit'] = re_analysis_decision['new_take_profit']
364
+
365
+ # تحديث الوقف المتحرك ليبدأ من الوقف الجديد
366
+ trade_to_update['dynamic_stop_loss'] = re_analysis_decision['new_stop_loss']
367
+
368
+ trade_to_update['decision_data']['exit_profile'] = re_analysis_decision['new_exit_profile']
369
+ trade_to_update['decision_data']['exit_parameters'] = re_analysis_decision['new_exit_parameters']
370
+
371
+ print(f" 🔄 {symbol}: تم تحديث ملف الخروج إلى {re_analysis_decision['new_exit_profile']}")
372
 
373
  new_expected_minutes = re_analysis_decision.get('new_expected_minutes')
374
  if new_expected_minutes:
 
381
  original_strategy = re_analysis_decision.get('strategy', 'GENERIC')
382
 
383
  trade_to_update['strategy'] = original_strategy
384
+ trade_to_update['decision_data']['reasoning'] = re_analysis_decision.get('reasoning') # تحديث سبب القرار
385
  trade_to_update['is_monitored'] = True
386
+ trade_to_update['trade_type'] = "LONG"
387
 
388
  open_trades = await self.r2_service.get_open_trades_async()
389
  for i, trade in enumerate(open_trades):
 
394
  await self.r2_service.save_open_trades_async(open_trades)
395
 
396
  await self.r2_service.save_system_logs_async({
397
+ "trade_updated": True, "symbol": symbol, "action": "UPDATE_TRADE",
398
+ "strategy": original_strategy,
399
+ "new_exit_profile": re_analysis_decision.get('new_exit_profile', 'N/A')
 
 
400
  })
401
 
402
  print(f"✅ تم تحديث صفقة {symbol}")
 
430
 
431
  async def start_trade_monitoring(self):
432
  self.is_running = True
433
+ print(f"🔍 بدء مراقبة الصفقات (Dynamic Exit Engine) (Pandas: {PANDAS_TA_AVAILABLE})...")
434
 
435
  while self.is_running:
436
  try:
 
440
  for trade in open_trades:
441
  symbol = trade['symbol']
442
 
 
443
  if self.monitoring_errors.get(symbol, 0) >= self.max_consecutive_errors:
444
  if symbol in self.monitoring_tasks:
445
  del self.monitoring_tasks[symbol]
446
  continue
447
 
 
448
  if symbol not in self.monitoring_tasks:
449
  task = asyncio.create_task(self._monitor_single_trade(trade))
450
  self.monitoring_tasks[symbol] = {
451
  'task': task,
452
  'start_time': current_time,
453
+ 'trade_object': trade # 🔴 حفظ نسخة محلية من الصفقة
454
  }
455
  trade['monitoring_started'] = True
456
 
 
457
  current_symbols = {trade['symbol'] for trade in open_trades}
458
  for symbol in list(self.monitoring_tasks.keys()):
459
  if symbol not in current_symbols:
 
464
  if symbol in self.monitoring_errors:
465
  del self.monitoring_errors[symbol]
466
 
467
+ await asyncio.sleep(10) # الفحص الرئيسي كل 10 ثوانٍ
468
 
469
  except Exception as error:
470
  print(f"❌ خطأ في مراقبة الصفقات: {error}")
471
  await asyncio.sleep(30)
472
 
473
+ async def _monitor_single_trade(self, trade_object):
474
+ """
475
+ 🔴 (أعيدت كتابته بالكامل)
476
+ المراقب التكتيكي (1 دقيقة) الذي ينفذ ملف الخروج الديناميكي.
477
+ """
478
+ symbol = trade_object['symbol']
479
+ # 🔴 نستخدم النسخة المحلية من الصفقة لتتبع الوقف المتحرك
480
+ local_trade = trade_object.copy()
481
 
482
+ print(f"🔍 بدء مراقبة الصفقة (تكتيكي): {symbol} (ملف: {local_trade.get('decision_data', {}).get('exit_profile', 'N/A')})")
483
 
484
  while (symbol in self.monitoring_tasks and
485
  self.is_running and
486
  self.monitoring_errors.get(symbol, 0) < self.max_consecutive_errors):
487
 
488
  try:
489
+ # 🔴 1. التحقق من قفل التحليل الاستراتيجي (منع التعارض)
490
+ if self.state_manager and self.state_manager.trade_analysis_lock.locked():
491
+ print(f"⏸️ [Monitor] إيقاف مؤقت لـ {symbol} (التحليل الاستراتيجي يعمل...)")
492
+ await asyncio.sleep(10) # انتظار انتهاء التحليل
493
+ continue # تخطي هذه الدورة
494
+
495
  if not self.data_manager:
496
  print(f"⚠️ DataManager غير متوفر لـ {symbol}")
497
  await asyncio.sleep(15)
498
  continue
499
 
500
+ # 🔴 2. جلب السعر الحالي
501
  try:
502
  current_price = await asyncio.wait_for(
503
  self.data_manager.get_latest_price_async(symbol),
 
515
  await asyncio.sleep(15)
516
  continue
517
 
518
+ # 🔴 3. تنفيذ محرك الخروج الديناميكي
519
+ # نمرر النسخة المحلية من الصفقة (local_trade)
520
+ should_close, close_reason, updated_local_trade = await self.exit_engine.evaluate_exit(
521
+ local_trade,
522
+ current_price
523
+ )
 
 
 
 
 
 
524
 
525
+ # تحديث النسخة المحلية (تحتوي على dynamic_stop_loss المحدث)
526
+ local_trade = updated_local_trade
 
 
 
 
 
 
 
527
 
528
+ # 🔴 4. اتخاذ قرار الإغلاق
529
  if should_close:
530
+ print(f"🛑 [Monitor] قرار إغلاق لـ {symbol}: {close_reason}")
531
+ if self.r2_service.acquire_lock(): # محاولة قفل R2 للإغلاق
532
  try:
533
+ # قبل الإغلاق، تأكد من جلب أحدث نسخة من R2
534
  latest_trade_data = await self.r2_service.get_trade_by_symbol_async(symbol)
535
  if latest_trade_data and latest_trade_data['status'] == 'OPEN':
536
+ await self.immediate_close_trade(symbol, current_price, f"Tactical Monitor: {close_reason}")
537
  else:
538
+ print(f"⚠️ الصفقة {symbol} لم تعد مفتوحة، تم إلغاء الإغلاق التكتيكي.")
539
  except Exception as close_error:
540
  print(f"❌ فشل الإغلاق التلقائي لـ {symbol}: {close_error}")
541
  finally:
 
543
 
544
  break # الخروج من حلقة المراقبة بعد محاولة الإغلاق
545
 
546
+ # 🔴 5. إعادة تعيين عداد الأخطاء والانتظار
547
  if symbol in self.monitoring_errors:
548
  self.monitoring_errors[symbol] = 0
549
 
550
+ # انتظار 60 ثانية (دورة المراقب التكتيكي)
551
+ await asyncio.sleep(60)
 
 
 
 
 
 
552
 
553
  except Exception as error:
554
  error_count = self._increment_monitoring_error(symbol)
555
  print(f"❌ خطأ في مراقبة {symbol} (الخطأ #{error_count}): {error}")
556
+ traceback.print_exc()
557
 
558
  if error_count >= self.max_consecutive_errors:
559
  print(f"🚨 إيقاف مراقبة {symbol} بسبب الأخطاء المتتالية")
560
  await self.r2_service.save_system_logs_async({
561
+ "monitoring_stopped": True, "symbol": symbol, "error_count": error_count, "error": str(error)
 
 
 
562
  })
 
 
 
 
 
563
  break # الخروج من الحلقة بسبب الأخطاء
564
 
565
  await asyncio.sleep(30) # انتظار أطول بعد الخطأ
566
 
567
+ print(f"🛑 توقيف مراقبة الصفقة (تكتيكي): {symbol}")
 
568
  if symbol in self.monitoring_tasks:
569
  del self.monitoring_tasks[symbol]
570
  if symbol in self.monitoring_errors:
 
600
 
601
  history.append(closed_trade)
602
 
 
603
  if len(history) > 1000:
604
  history = history[-1000:]
605
 
 
681
  'active_tasks': len(self.monitoring_tasks),
682
  'monitoring_errors': dict(self.monitoring_errors),
683
  'max_consecutive_errors': self.max_consecutive_errors
684
+ }
685
+
686
+ print(f"✅ Trade Manager loaded - V2 (Dynamic Exit Engine & Lock-Aware Monitor / Pandas: {PANDAS_TA_AVAILABLE})")