Update trade_manager.py
Browse files- 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 |
-
|
| 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"❌ رأس المال غير كافي
|
| 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 |
-
"
|
|
|
|
| 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 |
-
"
|
| 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}
|
| 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 |
-
|
| 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 |
-
"
|
| 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 |
-
|
|
|
|
|
|
|
| 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"
|
| 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 |
-
"
|
| 223 |
-
"
|
| 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 |
-
'
|
| 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,
|
| 303 |
-
|
| 304 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
|
| 306 |
-
print(f"🔍 بدء مراقبة
|
| 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 |
-
|
| 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 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 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 |
-
# تحديث
|
| 352 |
-
|
| 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 |
-
|
|
|
|
| 364 |
try:
|
| 365 |
-
# قبل الإغلاق، تأكد من جلب أحدث نسخة من
|
| 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 |
-
|
| 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"🛑 توقيف مراقبة
|
| 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})")
|