Update trade_manager.py
Browse files- trade_manager.py +118 -129
trade_manager.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
# trade_manager.py (Updated to Sentry/Executor architecture)
|
| 2 |
import asyncio
|
| 3 |
import json
|
| 4 |
import time
|
|
@@ -8,20 +8,20 @@ from typing import Dict, Any, List
|
|
| 8 |
from collections import deque
|
| 9 |
|
| 10 |
# 🔴 --- START OF CHANGE --- 🔴
|
| 11 |
-
# (
|
|
|
|
| 12 |
try:
|
| 13 |
-
import ccxt.
|
| 14 |
CCXT_PRO_AVAILABLE = True
|
| 15 |
except ImportError:
|
| 16 |
-
print("❌❌❌ خطأ فادح:
|
| 17 |
-
print("يرجى
|
| 18 |
CCXT_PRO_AVAILABLE = False
|
| 19 |
# 🔴 --- END OF CHANGE --- 🔴
|
| 20 |
|
|
|
|
| 21 |
from helpers import safe_float_conversion
|
| 22 |
-
# (لم نعد بحاجة إلى _DynamicExitEngine القديم أو pandas_ta هنا)
|
| 23 |
|
| 24 |
-
# 🔴 --- START OF CHANGE --- 🔴
|
| 25 |
# (فئة مساعد مبسطة لتتبع البيانات اللحظية)
|
| 26 |
class TacticalData:
|
| 27 |
def __init__(self, symbol):
|
|
@@ -30,7 +30,7 @@ class TacticalData:
|
|
| 30 |
self.trades = deque(maxlen=100)
|
| 31 |
self.cvd = 0.0 # (Cumulative Volume Delta)
|
| 32 |
self.large_trades = []
|
| 33 |
-
self.one_min_rsi = 50.0
|
| 34 |
self.last_update = time.time()
|
| 35 |
self.binance_trades = deque(maxlen=50) # (للبيانات التأكيدية)
|
| 36 |
self.binance_cvd = 0.0
|
|
@@ -39,7 +39,6 @@ class TacticalData:
|
|
| 39 |
self.trades.append(trade)
|
| 40 |
self.last_update = time.time()
|
| 41 |
|
| 42 |
-
# حساب CVD
|
| 43 |
try:
|
| 44 |
trade_amount = float(trade['amount'])
|
| 45 |
if trade['side'] == 'buy':
|
|
@@ -47,14 +46,13 @@ class TacticalData:
|
|
| 47 |
else:
|
| 48 |
self.cvd -= trade_amount
|
| 49 |
|
| 50 |
-
# رصد صفقات الحيتان (Taker)
|
| 51 |
trade_cost_usd = float(trade['cost'])
|
| 52 |
if trade_cost_usd > 20000: # (عتبة 20,000 دولار)
|
| 53 |
self.large_trades.append(trade)
|
| 54 |
if len(self.large_trades) > 20: self.large_trades.pop(0)
|
| 55 |
|
| 56 |
except Exception:
|
| 57 |
-
pass
|
| 58 |
|
| 59 |
def add_binance_trade(self, trade):
|
| 60 |
self.binance_trades.append(trade)
|
|
@@ -80,7 +78,6 @@ class TacticalData:
|
|
| 80 |
bids = self.order_book.get('bids', [])
|
| 81 |
asks = self.order_book.get('asks', [])
|
| 82 |
|
| 83 |
-
# (يمكن إضافة منطق رصد الجدران هنا)
|
| 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 |
|
|
@@ -89,23 +86,40 @@ class TacticalData:
|
|
| 89 |
return {"bids_depth": 0, "asks_depth": 0, "top_wall": "Error"}
|
| 90 |
|
| 91 |
def get_1m_rsi(self):
|
| 92 |
-
|
| 93 |
-
# (هنا سنحاكيه بشكل مبسط جداً من آخر 20 صفقة)
|
| 94 |
try:
|
| 95 |
if len(self.trades) < 20:
|
| 96 |
return 50.0
|
| 97 |
|
| 98 |
-
|
| 99 |
-
|
|
|
|
| 100 |
|
| 101 |
deltas = np.diff(closes)
|
| 102 |
seed = deltas[:14]
|
| 103 |
gains = np.sum(seed[seed >= 0])
|
| 104 |
-
losses = np.sum(
|
| 105 |
|
| 106 |
-
if losses == 0:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
rs = gains / losses
|
| 108 |
rsi = 100.0 - (100.0 / (1.0 + rs))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
self.one_min_rsi = rsi
|
| 110 |
return rsi
|
| 111 |
except Exception:
|
|
@@ -119,49 +133,45 @@ class TacticalData:
|
|
| 119 |
"rsi_1m_approx": self.get_1m_rsi(),
|
| 120 |
"ob_analysis": self.analyze_order_book()
|
| 121 |
}
|
| 122 |
-
# 🔴 --- END OF CHANGE --- 🔴
|
| 123 |
|
| 124 |
|
| 125 |
class TradeManager:
|
| 126 |
def __init__(self, r2_service, learning_hub=None, data_manager=None, state_manager=None):
|
| 127 |
if not CCXT_PRO_AVAILABLE:
|
| 128 |
-
raise RuntimeError("مكتبة 'ccxt
|
| 129 |
|
| 130 |
self.r2_service = r2_service
|
| 131 |
self.learning_hub = learning_hub
|
| 132 |
-
self.data_manager = data_manager
|
| 133 |
self.state_manager = state_manager
|
| 134 |
|
| 135 |
self.is_running = False
|
| 136 |
-
self.sentry_watchlist = {} # (تتم إدارتها بواسطة app.py)
|
| 137 |
-
self.sentry_tasks = {} # (المهام قيد التشغيل)
|
| 138 |
self.tactical_data_cache = {} # (تخزين بيانات TacticalData)
|
| 139 |
|
| 140 |
-
# (سيتم تهيئة هذه في initialize_sentry_exchanges)
|
| 141 |
self.kucoin_ws = None
|
| 142 |
-
self.binance_ws = None
|
| 143 |
|
| 144 |
-
# 🔴 --- START OF CHANGE --- 🔴
|
| 145 |
async def initialize_sentry_exchanges(self):
|
| 146 |
"""تهيئة منصات ccxt.pro (WebSockets)"""
|
| 147 |
try:
|
| 148 |
-
print("🔄 [Sentry] تهيئة منصات WebSocket (ccxt.
|
|
|
|
| 149 |
self.kucoin_ws = ccxtpro.kucoin({'newUpdates': True})
|
| 150 |
self.binance_ws = ccxtpro.binance({'newUpdates': True})
|
| 151 |
-
|
| 152 |
await self.kucoin_ws.load_markets()
|
| 153 |
await self.binance_ws.load_markets()
|
| 154 |
print("✅ [Sentry] منصات WebSocket (KuCoin, Binance) جاهزة.")
|
| 155 |
except Exception as e:
|
| 156 |
-
print(f"❌ [Sentry] فشل تهيئة ccxt.
|
| 157 |
-
print(" -> تأكد من إضافة 'ccxt-pro' إلى requirements.txt")
|
| 158 |
if self.kucoin_ws: await self.kucoin_ws.close()
|
| 159 |
if self.binance_ws: await self.binance_ws.close()
|
| 160 |
raise
|
| 161 |
|
| 162 |
async def start_sentry_and_monitoring_loops(self):
|
| 163 |
"""
|
| 164 |
-
(يستبدل start_trade_monitoring)
|
| 165 |
الحلقة الرئيسية للحارس (Sentry) ومراقب الخروج (Exit Monitor).
|
| 166 |
"""
|
| 167 |
self.is_running = True
|
|
@@ -169,31 +179,20 @@ class TradeManager:
|
|
| 169 |
|
| 170 |
while self.is_running:
|
| 171 |
try:
|
| 172 |
-
# 1. جلب قائمة المراقبة (من المستكشف)
|
| 173 |
-
# (sentry_watchlist يتم تحديثه بواسطة update_sentry_watchlist)
|
| 174 |
watchlist_symbols = set(self.sentry_watchlist.keys())
|
| 175 |
-
|
| 176 |
-
# 2. جلب الصفقات المفتوحة (لمراقبة الخروج)
|
| 177 |
open_trades = await self.get_open_trades()
|
| 178 |
open_trade_symbols = {t['symbol'] for t in open_trades}
|
| 179 |
-
|
| 180 |
-
# 3. دمج القائمتين: كل ما نحتاج لمراقبته
|
| 181 |
symbols_to_monitor = watchlist_symbols.union(open_trade_symbols)
|
| 182 |
|
| 183 |
-
# 4. بدء المهام للرموز الجديدة
|
| 184 |
for symbol in symbols_to_monitor:
|
| 185 |
if symbol not in self.sentry_tasks:
|
| 186 |
print(f" [Sentry] بدء المراقبة التكتيكية لـ {symbol}")
|
| 187 |
strategy_hint = self.sentry_watchlist.get(symbol, {}).get('strategy_hint', 'generic')
|
| 188 |
-
|
| 189 |
-
# (إنشاء مخزن بيانات تكتيكية)
|
| 190 |
if symbol not in self.tactical_data_cache:
|
| 191 |
self.tactical_data_cache[symbol] = TacticalData(symbol)
|
| 192 |
-
|
| 193 |
task = asyncio.create_task(self._monitor_symbol_activity(symbol, strategy_hint))
|
| 194 |
self.sentry_tasks[symbol] = task
|
| 195 |
|
| 196 |
-
# 5. إيقاف المهام للرموز القديمة
|
| 197 |
for symbol in list(self.sentry_tasks.keys()):
|
| 198 |
if symbol not in symbols_to_monitor:
|
| 199 |
print(f" [Sentry] إيقاف المراقبة التكتيكية لـ {symbol}")
|
|
@@ -202,7 +201,7 @@ class TradeManager:
|
|
| 202 |
if symbol in self.tactical_data_cache:
|
| 203 |
del self.tactical_data_cache[symbol]
|
| 204 |
|
| 205 |
-
await asyncio.sleep(15)
|
| 206 |
|
| 207 |
except Exception as error:
|
| 208 |
print(f"❌ [Sentry] خطأ في الحلقة الرئيسية: {error}")
|
|
@@ -229,7 +228,6 @@ class TradeManager:
|
|
| 229 |
(يتم استدعاؤها من app.py)
|
| 230 |
تحديث قائمة المراقبة التي يستخدمها الحارس (Sentry).
|
| 231 |
"""
|
| 232 |
-
# (تحويل القائمة إلى قاموس لسهولة البحث)
|
| 233 |
self.sentry_watchlist = {c['symbol']: c for c in candidates}
|
| 234 |
print(f"ℹ️ [Sentry] تم تحديث Watchlist. عدد المرشحين: {len(self.sentry_watchlist)}")
|
| 235 |
|
|
@@ -242,8 +240,6 @@ class TradeManager:
|
|
| 242 |
'monitored_symbols': list(self.sentry_tasks.keys())
|
| 243 |
}
|
| 244 |
|
| 245 |
-
# 🔴 --- (نهاية دوال الإدارة، بدء دوال المراقبة) --- 🔴
|
| 246 |
-
|
| 247 |
async def _monitor_symbol_activity(self, symbol: str, strategy_hint: str):
|
| 248 |
"""
|
| 249 |
(قلب الحارس)
|
|
@@ -252,16 +248,14 @@ class TradeManager:
|
|
| 252 |
if symbol not in self.tactical_data_cache:
|
| 253 |
self.tactical_data_cache[symbol] = TacticalData(symbol)
|
| 254 |
|
| 255 |
-
|
| 256 |
-
binance_symbol = symbol
|
| 257 |
-
if symbol == 'BTC/USDT': binance_symbol = 'BTC/USDT' # (مثال، قد نحتاج لآلية أفضل)
|
| 258 |
|
| 259 |
try:
|
| 260 |
await asyncio.gather(
|
| 261 |
self._watch_kucoin_trades(symbol),
|
| 262 |
self._watch_kucoin_orderbook(symbol),
|
| 263 |
-
self._watch_binance_trades(binance_symbol),
|
| 264 |
-
self._run_tactical_analysis_loop(symbol, strategy_hint)
|
| 265 |
)
|
| 266 |
except asyncio.CancelledError:
|
| 267 |
print(f"ℹ️ [Sentry] تم إيقاف المراقبة لـ {symbol}.")
|
|
@@ -296,17 +290,16 @@ class TradeManager:
|
|
| 296 |
|
| 297 |
async def _watch_binance_trades(self, symbol):
|
| 298 |
"""حلقة مراقبة الصفقات (Binance - تأكيدية)"""
|
| 299 |
-
if symbol.endswith('/USDT'):
|
| 300 |
-
symbol = symbol.replace('/USDT', 'USDT')
|
| 301 |
-
|
| 302 |
while self.is_running:
|
| 303 |
try:
|
| 304 |
trades = await self.binance_ws.watch_trades(symbol)
|
| 305 |
if symbol in self.tactical_data_cache:
|
| 306 |
-
|
| 307 |
-
|
|
|
|
|
|
|
|
|
|
| 308 |
except Exception as e:
|
| 309 |
-
# (نتجاهل الأخطاء هنا لأنها بيانات تأكيدية)
|
| 310 |
await asyncio.sleep(30)
|
| 311 |
|
| 312 |
async def _run_tactical_analysis_loop(self, symbol: str, strategy_hint: str):
|
|
@@ -318,44 +311,36 @@ class TradeManager:
|
|
| 318 |
await asyncio.sleep(1) # (التحليل كل ثانية)
|
| 319 |
|
| 320 |
try:
|
| 321 |
-
# (تجنب التحليل إذا كانت هناك إعادة تحليل استراتيجي)
|
| 322 |
if self.state_manager.trade_analysis_lock.locked():
|
| 323 |
continue
|
| 324 |
|
| 325 |
-
# 1. جلب الصفقة المفتوحة (إن وجدت)
|
| 326 |
trade = await self.get_trade_by_symbol(symbol)
|
| 327 |
tactical_data = self.tactical_data_cache.get(symbol)
|
| 328 |
if not tactical_data:
|
| 329 |
continue
|
| 330 |
|
| 331 |
-
# 2. جلب أحدث البيانات التكتيكية
|
| 332 |
snapshot = tactical_data.get_tactical_snapshot()
|
| 333 |
|
| 334 |
if trade:
|
| 335 |
# --- وضع مراقب الخروج (Exit Monitor) ---
|
| 336 |
-
# (هنا نستخدم snapshot للتحقق من شروط الخروج)
|
| 337 |
exit_reason = self._check_exit_trigger(trade, snapshot)
|
| 338 |
if exit_reason:
|
| 339 |
print(f"🛑 [Sentry] زناد خروج تكتيكي لـ {symbol}: {exit_reason}")
|
| 340 |
-
|
| 341 |
-
current_price = tactical_data.order_book['bids'][0][0] if tactical_data.order_book else None
|
| 342 |
if current_price:
|
| 343 |
-
# (استدعاء دالة الإغلاق الفوري)
|
| 344 |
await self.immediate_close_trade(symbol, current_price, f"Tactical Exit: {exit_reason}")
|
| 345 |
-
# (بعد الإغلاق، ستتم إزالة الرمز من المراقبة في الحلقة الرئيسية)
|
| 346 |
|
| 347 |
else:
|
| 348 |
# --- وضع الحارس (Sentry Mode) ---
|
| 349 |
-
# (التحقق مما إذا كان الرمز لا يزال في قائمة المراقبة)
|
| 350 |
if symbol in self.sentry_watchlist:
|
| 351 |
trigger = self._check_entry_trigger(symbol, strategy_hint, snapshot)
|
| 352 |
if trigger:
|
| 353 |
print(f"✅ [Sentry] زناد دخول تكتيكي لـ {symbol} (استراتيجية: {strategy_hint})")
|
| 354 |
-
# (إزالة الرمز
|
| 355 |
-
del self.sentry_watchlist[symbol]
|
| 356 |
|
| 357 |
-
# (
|
| 358 |
-
|
|
|
|
| 359 |
|
| 360 |
except Exception as e:
|
| 361 |
print(f"❌ [Sentry] خطأ في حلقة التحليل التكتيكي لـ {symbol}: {e}")
|
|
@@ -366,7 +351,6 @@ class TradeManager:
|
|
| 366 |
(دماغ الزناد التكتيكي)
|
| 367 |
يحدد ما إذا كان يجب الدخول الآن بناءً على الاستراتيجية.
|
| 368 |
"""
|
| 369 |
-
# (هذا هو المكان الذي يتم فيه دمج المؤشرات اللحظية)
|
| 370 |
rsi = data.get('rsi_1m_approx', 50)
|
| 371 |
cvd_kucoin = data.get('cvd_kucoin', 0)
|
| 372 |
cvd_binance = data.get('cvd_binance', 0)
|
|
@@ -375,18 +359,22 @@ class TradeManager:
|
|
| 375 |
# (يمكننا قراءة "الدلتا التكتيكية" من LearningHub هنا)
|
| 376 |
|
| 377 |
if strategy_hint == 'breakout_momentum':
|
| 378 |
-
# (مثال: نريد زخم قوي على المنصتين + اختراق RSI)
|
| 379 |
if (cvd_kucoin > 0 and cvd_binance > 0 and
|
| 380 |
large_trades > 0 and rsi > 60):
|
| 381 |
print(f" [Trigger] {symbol} Breakout: CVD K={cvd_kucoin:.0f}, B={cvd_binance:.0f}, RSI={rsi:.1f}")
|
| 382 |
return True
|
| 383 |
|
| 384 |
elif strategy_hint == 'mean_reversion':
|
| 385 |
-
# (مثال: نريد هبوطاً حاداً مع انعكاس CVD)
|
| 386 |
if (rsi < 30 and (cvd_kucoin > 0 or cvd_binance > 0)):
|
| 387 |
print(f" [Trigger] {symbol} Reversion: RSI={rsi:.1f}, CVD K={cvd_kucoin:.0f}")
|
| 388 |
return True
|
| 389 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
return False
|
| 391 |
|
| 392 |
def _check_exit_trigger(self, trade: Dict, data: Dict) -> str:
|
|
@@ -394,39 +382,49 @@ class TradeManager:
|
|
| 394 |
rsi = data.get('rsi_1m_approx', 50)
|
| 395 |
cvd_kucoin = data.get('cvd_kucoin', 0)
|
| 396 |
|
| 397 |
-
# (
|
| 398 |
if trade.get('strategy') == 'breakout_momentum':
|
| 399 |
if rsi < 40 and cvd_kucoin < 0:
|
| 400 |
return "Momentum reversal (RSI < 40 + CVD Negative)"
|
| 401 |
|
| 402 |
-
# (
|
| 403 |
if trade.get('strategy') == 'mean_reversion':
|
| 404 |
if rsi > 75:
|
| 405 |
return "Mean Reversion Target Hit (RSI > 75)"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
|
| 407 |
-
|
|
|
|
| 408 |
|
| 409 |
-
|
|
|
|
|
|
|
|
|
|
| 410 |
|
| 411 |
-
|
|
|
|
|
|
|
| 412 |
"""
|
| 413 |
(المنفذ - Layer 3)
|
| 414 |
تنفيذ الصفقة بذكاء مع آلية انزلاق.
|
| 415 |
"""
|
| 416 |
print(f"🚀 [Executor] بدء تنفيذ الدخول الذكي لـ {symbol}...")
|
| 417 |
|
| 418 |
-
# (التحقق من عدم وجود قفل استراتيجي)
|
| 419 |
if self.state_manager.trade_analysis_lock.locked():
|
| 420 |
print(f"⚠️ [Executor] تم إلغاء الدخول لـ {symbol} بسبب قفل التحليل الاستراتيجي.")
|
| 421 |
return
|
| 422 |
|
| 423 |
-
# (الحصول على قفل R2 لضمان عدم وجود تداخل)
|
| 424 |
if not self.r2_service.acquire_lock():
|
| 425 |
print(f"⚠️ [Executor] فشل في الحصول على قفل R2 لـ {symbol}. تم الإلغاء.")
|
| 426 |
return
|
| 427 |
|
| 428 |
try:
|
| 429 |
-
# (التحقق مرة أخرى من عدم فتح الصفقة أثناء اتخاذ القرار)
|
| 430 |
if await self.get_trade_by_symbol(symbol):
|
| 431 |
print(f"ℹ️ [Executor] الصفقة {symbol} مفتوحة بالفعل. تم الإلغاء.")
|
| 432 |
return
|
|
@@ -437,57 +435,59 @@ class TradeManager:
|
|
| 437 |
print(f"❌ [Executor] رأس مال غير كافٍ لـ {symbol}.")
|
| 438 |
return
|
| 439 |
|
| 440 |
-
# (جلب السعر الحالي من البيانات اللحظية)
|
| 441 |
-
|
| 442 |
-
if not
|
| 443 |
print(f"❌ [Executor] لا يمكن الحصول على السعر الحالي لـ {symbol}.")
|
| 444 |
return
|
| 445 |
|
| 446 |
-
amount_to_buy = available_capital /
|
| 447 |
|
| 448 |
-
# (تحديد أهداف الخروج
|
| 449 |
-
|
| 450 |
-
stop_loss_price =
|
| 451 |
-
take_profit_price =
|
| 452 |
-
exit_profile =
|
|
|
|
| 453 |
|
| 454 |
# --- آلية الانزلاق (Slippage Mechanism) ---
|
| 455 |
max_slippage_percent = 0.005 # (0.5%)
|
| 456 |
-
|
| 457 |
|
| 458 |
-
print(f" [Executor] {symbol}: وضع أمر محدد (Limit Buy) بسعر {
|
| 459 |
|
| 460 |
-
|
| 461 |
-
order = await self.kucoin_ws.create_limit_buy_order(symbol, amount_to_buy, current_price)
|
| 462 |
|
| 463 |
-
# (انتظار التنفيذ - "مطاردة" مبسطة)
|
| 464 |
await asyncio.sleep(5) # (الانتظار 5 ثوانٍ)
|
| 465 |
|
| 466 |
order_status = await self.kucoin_ws.fetch_order(order['id'], symbol)
|
| 467 |
|
| 468 |
if order_status['status'] == 'closed':
|
| 469 |
-
# --- نجح التنفيذ ---
|
| 470 |
final_entry_price = order_status['average']
|
| 471 |
print(f"✅ [Executor] تم التنفيذ! {symbol} بسعر {final_entry_price}")
|
| 472 |
|
| 473 |
-
# (تسجيل الصفقة في R2 - استخدام الدالة الداخلية)
|
| 474 |
await self._save_trade_to_r2(
|
| 475 |
symbol=symbol,
|
| 476 |
entry_price=final_entry_price,
|
| 477 |
position_size_usd=available_capital,
|
| 478 |
strategy=strategy_hint,
|
| 479 |
exit_profile=exit_profile,
|
|
|
|
| 480 |
stop_loss=stop_loss_price,
|
| 481 |
take_profit=take_profit_price,
|
| 482 |
-
tactical_context=tactical_data
|
|
|
|
| 483 |
)
|
| 484 |
|
| 485 |
else:
|
| 486 |
# --- فشل التنفيذ (الانزلاق) ---
|
| 487 |
print(f"⚠️ [Executor] فشل تنفيذ الأمر المحدد لـ {symbol} في الوقت المناسب. إلغاء الأمر.")
|
| 488 |
await self.kucoin_ws.cancel_order(order['id'], symbol)
|
| 489 |
-
# (
|
| 490 |
-
|
|
|
|
|
|
|
|
|
|
| 491 |
|
| 492 |
except Exception as e:
|
| 493 |
print(f"❌ [Executor] فشل فادح أثناء التنفيذ لـ {symbol}: {e}")
|
|
@@ -503,45 +503,44 @@ class TradeManager:
|
|
| 503 |
strategy = kwargs.get('strategy')
|
| 504 |
exit_profile = kwargs.get('exit_profile')
|
| 505 |
|
| 506 |
-
|
| 507 |
-
expected_target_time = (datetime.now() + timedelta(minutes=15)).isoformat() # (افتراضي قصير)
|
| 508 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 509 |
new_trade = {
|
| 510 |
"id": str(int(datetime.now().timestamp())),
|
| 511 |
"symbol": symbol,
|
| 512 |
"entry_price": kwargs.get('entry_price'),
|
| 513 |
"entry_timestamp": datetime.now().isoformat(),
|
| 514 |
-
"decision_data":
|
| 515 |
-
# (هنا نضع بيانات المنفذ والحارس)
|
| 516 |
-
"reasoning": f"Tactical entry by Sentry based on {strategy}",
|
| 517 |
-
"strategy": strategy,
|
| 518 |
-
"exit_profile": exit_profile,
|
| 519 |
-
"exit_parameters": {}, # (يمكن ملؤها)
|
| 520 |
-
"tactical_context_at_decision": kwargs.get('tactical_context', {}),
|
| 521 |
-
# (البيانات من المستكشف (Layer 1) يجب تمريرها أيضاً)
|
| 522 |
-
"explorer_decision_context": self.sentry_watchlist.get(symbol, {}).get('llm_decision_context', {})
|
| 523 |
-
},
|
| 524 |
"status": "OPEN",
|
| 525 |
"stop_loss": kwargs.get('stop_loss'),
|
| 526 |
"take_profit": kwargs.get('take_profit'),
|
| 527 |
-
"dynamic_stop_loss": kwargs.get('stop_loss'),
|
| 528 |
"trade_type": "LONG",
|
| 529 |
"position_size_usd": kwargs.get('position_size_usd'),
|
| 530 |
"expected_target_minutes": 15,
|
| 531 |
"expected_target_time": expected_target_time,
|
| 532 |
-
"is_monitored": True,
|
| 533 |
"strategy": strategy,
|
| 534 |
-
"monitoring_started": True
|
| 535 |
}
|
| 536 |
|
| 537 |
-
# (تحديث R2)
|
| 538 |
trades = await self.r2_service.get_open_trades_async()
|
| 539 |
trades.append(new_trade)
|
| 540 |
await self.r2_service.save_open_trades_async(trades)
|
| 541 |
|
| 542 |
portfolio_state = await self.r2_service.get_portfolio_state_async()
|
| 543 |
portfolio_state["invested_capital_usd"] = kwargs.get('position_size_usd')
|
| 544 |
-
portfolio_state["current_capital_usd"] = 0.0
|
| 545 |
portfolio_state["total_trades"] = portfolio_state.get("total_trades", 0) + 1
|
| 546 |
await self.r2_service.save_portfolio_state_async(portfolio_state)
|
| 547 |
|
|
@@ -557,9 +556,6 @@ class TradeManager:
|
|
| 557 |
print(f"❌ [R2] فشل حفظ الصفقة لـ {symbol}: {e}")
|
| 558 |
traceback.print_exc()
|
| 559 |
|
| 560 |
-
|
| 561 |
-
# 🔴 --- (الدوال المتبقية هي من trade_manager القديم، مع تعديلات طفيفة) --- 🔴
|
| 562 |
-
|
| 563 |
async def close_trade(self, trade_to_close, close_price, reason="System Close"):
|
| 564 |
"""(لا تغيير جوهري) - لا يزال مسؤولاً عن حساب PnL وتحديث R2 وتشغيل LearningHub"""
|
| 565 |
try:
|
|
@@ -585,11 +581,9 @@ class TradeManager:
|
|
| 585 |
trade_to_close['pnl_usd'] = pnl
|
| 586 |
trade_to_close['pnl_percent'] = pnl_percent
|
| 587 |
|
| 588 |
-
# (الأرشيف وتحديث الملخص - لا تغيير)
|
| 589 |
await self._archive_closed_trade(trade_to_close)
|
| 590 |
await self._update_trade_summary(trade_to_close)
|
| 591 |
|
| 592 |
-
# (تحديث المحفظة - لا تغيير)
|
| 593 |
portfolio_state = await self.r2_service.get_portfolio_state_async()
|
| 594 |
current_capital = portfolio_state.get("current_capital_usd", 0)
|
| 595 |
new_capital = current_capital + position_size + pnl
|
|
@@ -602,12 +596,10 @@ class TradeManager:
|
|
| 602 |
portfolio_state["total_loss_usd"] = portfolio_state.get("total_loss_usd", 0.0) + abs(pnl)
|
| 603 |
await self.r2_service.save_portfolio_state_async(portfolio_state)
|
| 604 |
|
| 605 |
-
# (إزالة الصفقة من open_trades.json - لا تغيير)
|
| 606 |
open_trades = await self.r2_service.get_open_trades_async()
|
| 607 |
trades_to_keep = [t for t in open_trades if t.get('id') != trade_to_close.get('id')]
|
| 608 |
await self.r2_service.save_open_trades_async(trades_to_keep)
|
| 609 |
|
| 610 |
-
# (إيقاف المراقبة - الحلقة الرئيسية ستلتقط هذا)
|
| 611 |
if symbol in self.sentry_tasks:
|
| 612 |
print(f"ℹ️ [Sentry] الصفقة {symbol} أغلقت، ستتوقف المراقبة.")
|
| 613 |
|
|
@@ -616,7 +608,7 @@ class TradeManager:
|
|
| 616 |
"new_capital": new_capital, "strategy": strategy, "reason": reason
|
| 617 |
})
|
| 618 |
|
| 619 |
-
# (تشغيل التعلم - لا
|
| 620 |
if self.learning_hub and self.learning_hub.initialized:
|
| 621 |
print(f"🧠 [LearningHub] تشغيل التعلم (Reflector+Stats) لـ {symbol}...")
|
| 622 |
await self.learning_hub.analyze_trade_and_learn(trade_to_close, reason)
|
|
@@ -662,19 +654,16 @@ class TradeManager:
|
|
| 662 |
if re_analysis_decision.get('action') == "UPDATE_TRADE":
|
| 663 |
trade_to_update['stop_loss'] = re_analysis_decision['new_stop_loss']
|
| 664 |
trade_to_update['take_profit'] = re_analysis_decision['new_take_profit']
|
| 665 |
-
# (ملاحظة: dynamic_stop_loss يُدار بواسطة Sentry، لكننا نحدث القيمة الأولية)
|
| 666 |
trade_to_update['dynamic_stop_loss'] = re_analysis_decision['new_stop_loss']
|
| 667 |
trade_to_update['decision_data']['exit_profile'] = re_analysis_decision['new_exit_profile']
|
| 668 |
trade_to_update['decision_data']['exit_parameters'] = re_analysis_decision['new_exit_parameters']
|
| 669 |
print(f" 🔄 (Explorer) {symbol}: Exit profile updated to {re_analysis_decision['new_exit_profile']}")
|
| 670 |
|
| 671 |
-
# (تحديثات أخرى)
|
| 672 |
new_expected_minutes = re_analysis_decision.get('new_expected_minutes', 15)
|
| 673 |
trade_to_update['expected_target_minutes'] = new_expected_minutes
|
| 674 |
trade_to_update['expected_target_time'] = (datetime.now() + timedelta(minutes=new_expected_minutes)).isoformat()
|
| 675 |
trade_to_update['decision_data']['reasoning'] = re_analysis_decision.get('reasoning')
|
| 676 |
|
| 677 |
-
# (حفظ التغييرات في R2)
|
| 678 |
open_trades = await self.r2_service.get_open_trades_async()
|
| 679 |
for i, trade in enumerate(open_trades):
|
| 680 |
if trade.get('id') == trade_to_update.get('id'):
|
|
@@ -733,4 +722,4 @@ class TradeManager:
|
|
| 733 |
except Exception as e: print(f"❌ Failed to get trade by symbol {symbol}: {e}"); return None
|
| 734 |
|
| 735 |
|
| 736 |
-
print(f"✅ Trade Manager loaded - V5 (Sentry/Executor with ccxt.
|
|
|
|
| 1 |
+
# trade_manager.py (Updated to Sentry/Executor architecture V5.1 - Fixed ccxt import)
|
| 2 |
import asyncio
|
| 3 |
import json
|
| 4 |
import time
|
|
|
|
| 8 |
from collections import deque
|
| 9 |
|
| 10 |
# 🔴 --- START OF CHANGE --- 🔴
|
| 11 |
+
# (هذا هو التصحيح بناءً على خطأ البناء)
|
| 12 |
+
# (لقد استبدلنا 'ccxt.pro' بـ 'ccxt.async_support' لأن ccxt v4+ يدمجها)
|
| 13 |
try:
|
| 14 |
+
import ccxt.async_support as ccxtpro
|
| 15 |
CCXT_PRO_AVAILABLE = True
|
| 16 |
except ImportError:
|
| 17 |
+
print("❌❌❌ خطأ فادح: فشل استيراد 'ccxt.async_support'. ❌❌❌")
|
| 18 |
+
print("يرجى التأكد من تثبيت 'ccxt' (الإصدار 4 أو أحدث) بنجاح.")
|
| 19 |
CCXT_PRO_AVAILABLE = False
|
| 20 |
# 🔴 --- END OF CHANGE --- 🔴
|
| 21 |
|
| 22 |
+
import numpy as np # (نحتاج numpy لتحليل RSI البسيط)
|
| 23 |
from helpers import safe_float_conversion
|
|
|
|
| 24 |
|
|
|
|
| 25 |
# (فئة مساعد مبسطة لتتبع البيانات اللحظية)
|
| 26 |
class TacticalData:
|
| 27 |
def __init__(self, symbol):
|
|
|
|
| 30 |
self.trades = deque(maxlen=100)
|
| 31 |
self.cvd = 0.0 # (Cumulative Volume Delta)
|
| 32 |
self.large_trades = []
|
| 33 |
+
self.one_min_rsi = 50.0
|
| 34 |
self.last_update = time.time()
|
| 35 |
self.binance_trades = deque(maxlen=50) # (للبيانات التأكيدية)
|
| 36 |
self.binance_cvd = 0.0
|
|
|
|
| 39 |
self.trades.append(trade)
|
| 40 |
self.last_update = time.time()
|
| 41 |
|
|
|
|
| 42 |
try:
|
| 43 |
trade_amount = float(trade['amount'])
|
| 44 |
if trade['side'] == 'buy':
|
|
|
|
| 46 |
else:
|
| 47 |
self.cvd -= trade_amount
|
| 48 |
|
|
|
|
| 49 |
trade_cost_usd = float(trade['cost'])
|
| 50 |
if trade_cost_usd > 20000: # (عتبة 20,000 دولار)
|
| 51 |
self.large_trades.append(trade)
|
| 52 |
if len(self.large_trades) > 20: self.large_trades.pop(0)
|
| 53 |
|
| 54 |
except Exception:
|
| 55 |
+
pass
|
| 56 |
|
| 57 |
def add_binance_trade(self, trade):
|
| 58 |
self.binance_trades.append(trade)
|
|
|
|
| 78 |
bids = self.order_book.get('bids', [])
|
| 79 |
asks = self.order_book.get('asks', [])
|
| 80 |
|
|
|
|
| 81 |
bids_depth = sum(price * amount for price, amount in bids[:10])
|
| 82 |
asks_depth = sum(price * amount for price, amount in asks[:10])
|
| 83 |
|
|
|
|
| 86 |
return {"bids_depth": 0, "asks_depth": 0, "top_wall": "Error"}
|
| 87 |
|
| 88 |
def get_1m_rsi(self):
|
| 89 |
+
"""حساب RSI مبسط جداً من آخر 20 صفقة (كمؤشر للزخم اللحظي)"""
|
|
|
|
| 90 |
try:
|
| 91 |
if len(self.trades) < 20:
|
| 92 |
return 50.0
|
| 93 |
|
| 94 |
+
# (نستخدم سعر آخر 20 صفقة كبديل لشموع الدقيقة الواحدة)
|
| 95 |
+
closes = np.array([trade['price'] for trade in self.trades if 'price' in trade])[-20:]
|
| 96 |
+
if len(closes) < 15: return 50.0
|
| 97 |
|
| 98 |
deltas = np.diff(closes)
|
| 99 |
seed = deltas[:14]
|
| 100 |
gains = np.sum(seed[seed >= 0])
|
| 101 |
+
losses = np.sum(np.abs(seed[seed < 0]))
|
| 102 |
|
| 103 |
+
if losses == 0:
|
| 104 |
+
self.one_min_rsi = 100.0
|
| 105 |
+
return 100.0
|
| 106 |
+
if gains == 0:
|
| 107 |
+
self.one_min_rsi = 0.0
|
| 108 |
+
return 0.0
|
| 109 |
+
|
| 110 |
rs = gains / losses
|
| 111 |
rsi = 100.0 - (100.0 / (1.0 + rs))
|
| 112 |
+
|
| 113 |
+
# (تحديث سلس لباقي الصفقات)
|
| 114 |
+
for delta in deltas[14:]:
|
| 115 |
+
gain = max(delta, 0)
|
| 116 |
+
loss = max(-delta, 0)
|
| 117 |
+
gains = (gains * 13 + gain) / 14
|
| 118 |
+
losses = (losses * 13 + loss) / 14
|
| 119 |
+
|
| 120 |
+
if losses == 0: rsi = 100.0
|
| 121 |
+
else: rs = gains / losses; rsi = 100.0 - (100.0 / (1.0 + rs))
|
| 122 |
+
|
| 123 |
self.one_min_rsi = rsi
|
| 124 |
return rsi
|
| 125 |
except Exception:
|
|
|
|
| 133 |
"rsi_1m_approx": self.get_1m_rsi(),
|
| 134 |
"ob_analysis": self.analyze_order_book()
|
| 135 |
}
|
|
|
|
| 136 |
|
| 137 |
|
| 138 |
class TradeManager:
|
| 139 |
def __init__(self, r2_service, learning_hub=None, data_manager=None, state_manager=None):
|
| 140 |
if not CCXT_PRO_AVAILABLE:
|
| 141 |
+
raise RuntimeError("مكتبة 'ccxt.async_support' (جزء من ccxt) غير متاحة. لا يمكن تشغيل TradeManager.")
|
| 142 |
|
| 143 |
self.r2_service = r2_service
|
| 144 |
self.learning_hub = learning_hub
|
| 145 |
+
self.data_manager = data_manager
|
| 146 |
self.state_manager = state_manager
|
| 147 |
|
| 148 |
self.is_running = False
|
| 149 |
+
self.sentry_watchlist = {} # (تتم إدارتها بواسطة app.py)
|
| 150 |
+
self.sentry_tasks = {} # (المهام قيد التشغيل)
|
| 151 |
self.tactical_data_cache = {} # (تخزين بيانات TacticalData)
|
| 152 |
|
|
|
|
| 153 |
self.kucoin_ws = None
|
| 154 |
+
self.binance_ws = None
|
| 155 |
|
|
|
|
| 156 |
async def initialize_sentry_exchanges(self):
|
| 157 |
"""تهيئة منصات ccxt.pro (WebSockets)"""
|
| 158 |
try:
|
| 159 |
+
print("🔄 [Sentry] تهيئة منصات WebSocket (ccxt.async_support)...")
|
| 160 |
+
# (استخدام ccxtpro الذي هو الآن 'ccxt.async_support')
|
| 161 |
self.kucoin_ws = ccxtpro.kucoin({'newUpdates': True})
|
| 162 |
self.binance_ws = ccxtpro.binance({'newUpdates': True})
|
| 163 |
+
|
| 164 |
await self.kucoin_ws.load_markets()
|
| 165 |
await self.binance_ws.load_markets()
|
| 166 |
print("✅ [Sentry] منصات WebSocket (KuCoin, Binance) جاهزة.")
|
| 167 |
except Exception as e:
|
| 168 |
+
print(f"❌ [Sentry] فشل تهيئة ccxt.async_support: {e}")
|
|
|
|
| 169 |
if self.kucoin_ws: await self.kucoin_ws.close()
|
| 170 |
if self.binance_ws: await self.binance_ws.close()
|
| 171 |
raise
|
| 172 |
|
| 173 |
async def start_sentry_and_monitoring_loops(self):
|
| 174 |
"""
|
|
|
|
| 175 |
الحلقة الرئيسية للحارس (Sentry) ومراقب الخروج (Exit Monitor).
|
| 176 |
"""
|
| 177 |
self.is_running = True
|
|
|
|
| 179 |
|
| 180 |
while self.is_running:
|
| 181 |
try:
|
|
|
|
|
|
|
| 182 |
watchlist_symbols = set(self.sentry_watchlist.keys())
|
|
|
|
|
|
|
| 183 |
open_trades = await self.get_open_trades()
|
| 184 |
open_trade_symbols = {t['symbol'] for t in open_trades}
|
|
|
|
|
|
|
| 185 |
symbols_to_monitor = watchlist_symbols.union(open_trade_symbols)
|
| 186 |
|
|
|
|
| 187 |
for symbol in symbols_to_monitor:
|
| 188 |
if symbol not in self.sentry_tasks:
|
| 189 |
print(f" [Sentry] بدء المراقبة التكتيكية لـ {symbol}")
|
| 190 |
strategy_hint = self.sentry_watchlist.get(symbol, {}).get('strategy_hint', 'generic')
|
|
|
|
|
|
|
| 191 |
if symbol not in self.tactical_data_cache:
|
| 192 |
self.tactical_data_cache[symbol] = TacticalData(symbol)
|
|
|
|
| 193 |
task = asyncio.create_task(self._monitor_symbol_activity(symbol, strategy_hint))
|
| 194 |
self.sentry_tasks[symbol] = task
|
| 195 |
|
|
|
|
| 196 |
for symbol in list(self.sentry_tasks.keys()):
|
| 197 |
if symbol not in symbols_to_monitor:
|
| 198 |
print(f" [Sentry] إيقاف المراقبة التكتيكية لـ {symbol}")
|
|
|
|
| 201 |
if symbol in self.tactical_data_cache:
|
| 202 |
del self.tactical_data_cache[symbol]
|
| 203 |
|
| 204 |
+
await asyncio.sleep(15)
|
| 205 |
|
| 206 |
except Exception as error:
|
| 207 |
print(f"❌ [Sentry] خطأ في الحلقة الرئيسية: {error}")
|
|
|
|
| 228 |
(يتم استدعاؤها من app.py)
|
| 229 |
تحديث قائمة المراقبة التي يستخدمها الحارس (Sentry).
|
| 230 |
"""
|
|
|
|
| 231 |
self.sentry_watchlist = {c['symbol']: c for c in candidates}
|
| 232 |
print(f"ℹ️ [Sentry] تم تحديث Watchlist. عدد المرشحين: {len(self.sentry_watchlist)}")
|
| 233 |
|
|
|
|
| 240 |
'monitored_symbols': list(self.sentry_tasks.keys())
|
| 241 |
}
|
| 242 |
|
|
|
|
|
|
|
| 243 |
async def _monitor_symbol_activity(self, symbol: str, strategy_hint: str):
|
| 244 |
"""
|
| 245 |
(قلب الحارس)
|
|
|
|
| 248 |
if symbol not in self.tactical_data_cache:
|
| 249 |
self.tactical_data_cache[symbol] = TacticalData(symbol)
|
| 250 |
|
| 251 |
+
binance_symbol = symbol.replace('/USDT', 'USDT')
|
|
|
|
|
|
|
| 252 |
|
| 253 |
try:
|
| 254 |
await asyncio.gather(
|
| 255 |
self._watch_kucoin_trades(symbol),
|
| 256 |
self._watch_kucoin_orderbook(symbol),
|
| 257 |
+
self._watch_binance_trades(binance_symbol),
|
| 258 |
+
self._run_tactical_analysis_loop(symbol, strategy_hint)
|
| 259 |
)
|
| 260 |
except asyncio.CancelledError:
|
| 261 |
print(f"ℹ️ [Sentry] تم إيقاف المراقبة لـ {symbol}.")
|
|
|
|
| 290 |
|
| 291 |
async def _watch_binance_trades(self, symbol):
|
| 292 |
"""حلقة مراقبة الصفقات (Binance - تأكيدية)"""
|
|
|
|
|
|
|
|
|
|
| 293 |
while self.is_running:
|
| 294 |
try:
|
| 295 |
trades = await self.binance_ws.watch_trades(symbol)
|
| 296 |
if symbol in self.tactical_data_cache:
|
| 297 |
+
# (يجب تحويل الرمز مرة أخرى ليتطابق مع مفتاح القاموس)
|
| 298 |
+
cache_key = symbol.replace('USDT', '/USDT')
|
| 299 |
+
if cache_key in self.tactical_data_cache:
|
| 300 |
+
for trade in trades:
|
| 301 |
+
self.tactical_data_cache[cache_key].add_binance_trade(trade)
|
| 302 |
except Exception as e:
|
|
|
|
| 303 |
await asyncio.sleep(30)
|
| 304 |
|
| 305 |
async def _run_tactical_analysis_loop(self, symbol: str, strategy_hint: str):
|
|
|
|
| 311 |
await asyncio.sleep(1) # (التحليل كل ثانية)
|
| 312 |
|
| 313 |
try:
|
|
|
|
| 314 |
if self.state_manager.trade_analysis_lock.locked():
|
| 315 |
continue
|
| 316 |
|
|
|
|
| 317 |
trade = await self.get_trade_by_symbol(symbol)
|
| 318 |
tactical_data = self.tactical_data_cache.get(symbol)
|
| 319 |
if not tactical_data:
|
| 320 |
continue
|
| 321 |
|
|
|
|
| 322 |
snapshot = tactical_data.get_tactical_snapshot()
|
| 323 |
|
| 324 |
if trade:
|
| 325 |
# --- وضع مراقب الخروج (Exit Monitor) ---
|
|
|
|
| 326 |
exit_reason = self._check_exit_trigger(trade, snapshot)
|
| 327 |
if exit_reason:
|
| 328 |
print(f"🛑 [Sentry] زناد خروج تكتيكي لـ {symbol}: {exit_reason}")
|
| 329 |
+
current_price = tactical_data.order_book['bids'][0][0] if tactical_data.order_book and tactical_data.order_book['bids'] else None
|
|
|
|
| 330 |
if current_price:
|
|
|
|
| 331 |
await self.immediate_close_trade(symbol, current_price, f"Tactical Exit: {exit_reason}")
|
|
|
|
| 332 |
|
| 333 |
else:
|
| 334 |
# --- وضع الحارس (Sentry Mode) ---
|
|
|
|
| 335 |
if symbol in self.sentry_watchlist:
|
| 336 |
trigger = self._check_entry_trigger(symbol, strategy_hint, snapshot)
|
| 337 |
if trigger:
|
| 338 |
print(f"✅ [Sentry] زناد دخول تكتيكي لـ {symbol} (استراتيجية: {strategy_hint})")
|
| 339 |
+
watchlist_entry = self.sentry_watchlist.pop(symbol) # (إزالة الرمز لمنع الدخول المتعدد)
|
|
|
|
| 340 |
|
| 341 |
+
# (تمرير سياق المستكشف (LLM) إلى المنفذ)
|
| 342 |
+
explorer_context = watchlist_entry.get('llm_decision_context', {})
|
| 343 |
+
await self._execute_smart_entry(symbol, strategy_hint, snapshot, explorer_context)
|
| 344 |
|
| 345 |
except Exception as e:
|
| 346 |
print(f"❌ [Sentry] خطأ في حلقة التحليل التكتيكي لـ {symbol}: {e}")
|
|
|
|
| 351 |
(دماغ الزناد التكتيكي)
|
| 352 |
يحدد ما إذا كان يجب الدخول الآن بناءً على الاستراتيجية.
|
| 353 |
"""
|
|
|
|
| 354 |
rsi = data.get('rsi_1m_approx', 50)
|
| 355 |
cvd_kucoin = data.get('cvd_kucoin', 0)
|
| 356 |
cvd_binance = data.get('cvd_binance', 0)
|
|
|
|
| 359 |
# (يمكننا قراءة "الدلتا التكتيكية" من LearningHub هنا)
|
| 360 |
|
| 361 |
if strategy_hint == 'breakout_momentum':
|
|
|
|
| 362 |
if (cvd_kucoin > 0 and cvd_binance > 0 and
|
| 363 |
large_trades > 0 and rsi > 60):
|
| 364 |
print(f" [Trigger] {symbol} Breakout: CVD K={cvd_kucoin:.0f}, B={cvd_binance:.0f}, RSI={rsi:.1f}")
|
| 365 |
return True
|
| 366 |
|
| 367 |
elif strategy_hint == 'mean_reversion':
|
|
|
|
| 368 |
if (rsi < 30 and (cvd_kucoin > 0 or cvd_binance > 0)):
|
| 369 |
print(f" [Trigger] {symbol} Reversion: RSI={rsi:.1f}, CVD K={cvd_kucoin:.0f}")
|
| 370 |
return True
|
| 371 |
|
| 372 |
+
# (إضافة استراتيجيات أخرى)
|
| 373 |
+
elif strategy_hint == 'volume_spike':
|
| 374 |
+
if (large_trades > 2 and cvd_kucoin > 0):
|
| 375 |
+
print(f" [Trigger] {symbol} Volume Spike: LargeTrades={large_trades}, CVD K={cvd_kucoin:.0f}")
|
| 376 |
+
return True
|
| 377 |
+
|
| 378 |
return False
|
| 379 |
|
| 380 |
def _check_exit_trigger(self, trade: Dict, data: Dict) -> str:
|
|
|
|
| 382 |
rsi = data.get('rsi_1m_approx', 50)
|
| 383 |
cvd_kucoin = data.get('cvd_kucoin', 0)
|
| 384 |
|
| 385 |
+
# (مثال لخروج تكتيكي لاستراتيجية الاختراق)
|
| 386 |
if trade.get('strategy') == 'breakout_momentum':
|
| 387 |
if rsi < 40 and cvd_kucoin < 0:
|
| 388 |
return "Momentum reversal (RSI < 40 + CVD Negative)"
|
| 389 |
|
| 390 |
+
# (مثال لخروج تكتيكي لاستراتيجية الانعكاس)
|
| 391 |
if trade.get('strategy') == 'mean_reversion':
|
| 392 |
if rsi > 75:
|
| 393 |
return "Mean Reversion Target Hit (RSI > 75)"
|
| 394 |
+
|
| 395 |
+
# (التحقق من وقف الخسارة/الهدف الاستراتيجي)
|
| 396 |
+
# (نحتاج إلى سعر حالي دقيق)
|
| 397 |
+
if self.tactical_data_cache.get(trade['symbol']) and self.tactical_data_cache[trade['symbol']].order_book:
|
| 398 |
+
ob = self.tactical_data_cache[trade['symbol']].order_book
|
| 399 |
+
if ob['bids']:
|
| 400 |
+
current_price = ob['bids'][0][0] # (أفضل سعر شراء)
|
| 401 |
|
| 402 |
+
hard_stop = trade.get('stop_loss')
|
| 403 |
+
take_profit = trade.get('take_profit')
|
| 404 |
|
| 405 |
+
if hard_stop and current_price <= hard_stop:
|
| 406 |
+
return f"Strategic Stop Loss hit: {current_price} <= {hard_stop}"
|
| 407 |
+
if take_profit and current_price >= take_profit:
|
| 408 |
+
return f"Strategic Take Profit hit: {current_price} >= {take_profit}"
|
| 409 |
|
| 410 |
+
return None # (لا يوجد سبب للخروج التكتيكي)
|
| 411 |
+
|
| 412 |
+
async def _execute_smart_entry(self, symbol: str, strategy_hint: str, tactical_data: Dict, explorer_context: Dict):
|
| 413 |
"""
|
| 414 |
(المنفذ - Layer 3)
|
| 415 |
تنفيذ الصفقة بذكاء مع آلية انزلاق.
|
| 416 |
"""
|
| 417 |
print(f"🚀 [Executor] بدء تنفيذ الدخول الذكي لـ {symbol}...")
|
| 418 |
|
|
|
|
| 419 |
if self.state_manager.trade_analysis_lock.locked():
|
| 420 |
print(f"⚠️ [Executor] تم إلغاء الدخول لـ {symbol} بسبب قفل التحليل الاستراتيجي.")
|
| 421 |
return
|
| 422 |
|
|
|
|
| 423 |
if not self.r2_service.acquire_lock():
|
| 424 |
print(f"⚠️ [Executor] فشل في الحصول على قفل R2 لـ {symbol}. تم الإلغاء.")
|
| 425 |
return
|
| 426 |
|
| 427 |
try:
|
|
|
|
| 428 |
if await self.get_trade_by_symbol(symbol):
|
| 429 |
print(f"ℹ️ [Executor] الصفقة {symbol} مفتوحة بالفعل. تم الإلغاء.")
|
| 430 |
return
|
|
|
|
| 435 |
print(f"❌ [Executor] رأس مال غير كافٍ لـ {symbol}.")
|
| 436 |
return
|
| 437 |
|
| 438 |
+
# (جلب السعر الحالي من البيانات اللحظية - سعر البيع)
|
| 439 |
+
current_ask_price = self.tactical_data_cache[symbol].order_book['asks'][0][0]
|
| 440 |
+
if not current_ask_price:
|
| 441 |
print(f"❌ [Executor] لا يمكن الحصول على السعر الحالي لـ {symbol}.")
|
| 442 |
return
|
| 443 |
|
| 444 |
+
amount_to_buy = available_capital / current_ask_price
|
| 445 |
|
| 446 |
+
# (تحديد أهداف الخروج بناءً على سياق المستكشف)
|
| 447 |
+
llm_decision = explorer_context.get('llm_decision_context', {}).get('decision', {})
|
| 448 |
+
stop_loss_price = llm_decision.get("stop_loss", current_ask_price * 0.98) # (افتراضي 2%)
|
| 449 |
+
take_profit_price = llm_decision.get("take_profit", current_ask_price * 1.03) # (افتراضي 3%)
|
| 450 |
+
exit_profile = llm_decision.get('exit_profile', 'ATR_TRAILING')
|
| 451 |
+
exit_parameters = llm_decision.get('exit_parameters', {})
|
| 452 |
|
| 453 |
# --- آلية الانزلاق (Slippage Mechanism) ---
|
| 454 |
max_slippage_percent = 0.005 # (0.5%)
|
| 455 |
+
max_buy_price = current_ask_price * (1 + max_slippage_percent)
|
| 456 |
|
| 457 |
+
print(f" [Executor] {symbol}: وضع أمر محدد (Limit Buy) بسعر {current_ask_price} (الحد الأقصى {max_buy_price})")
|
| 458 |
|
| 459 |
+
order = await self.kucoin_ws.create_limit_buy_order(symbol, amount_to_buy, current_ask_price)
|
|
|
|
| 460 |
|
|
|
|
| 461 |
await asyncio.sleep(5) # (الانتظار 5 ثوانٍ)
|
| 462 |
|
| 463 |
order_status = await self.kucoin_ws.fetch_order(order['id'], symbol)
|
| 464 |
|
| 465 |
if order_status['status'] == 'closed':
|
|
|
|
| 466 |
final_entry_price = order_status['average']
|
| 467 |
print(f"✅ [Executor] تم التنفيذ! {symbol} بسعر {final_entry_price}")
|
| 468 |
|
|
|
|
| 469 |
await self._save_trade_to_r2(
|
| 470 |
symbol=symbol,
|
| 471 |
entry_price=final_entry_price,
|
| 472 |
position_size_usd=available_capital,
|
| 473 |
strategy=strategy_hint,
|
| 474 |
exit_profile=exit_profile,
|
| 475 |
+
exit_parameters=exit_parameters,
|
| 476 |
stop_loss=stop_loss_price,
|
| 477 |
take_profit=take_profit_price,
|
| 478 |
+
tactical_context=tactical_data,
|
| 479 |
+
explorer_context=explorer_context
|
| 480 |
)
|
| 481 |
|
| 482 |
else:
|
| 483 |
# --- فشل التنفيذ (الانزلاق) ---
|
| 484 |
print(f"⚠️ [Executor] فشل تنفيذ الأمر المحدد لـ {symbol} في الوقت المناسب. إلغاء الأمر.")
|
| 485 |
await self.kucoin_ws.cancel_order(order['id'], symbol)
|
| 486 |
+
# (إعادة العملة إلى قائمة المراقبة بعد فترة هدوء)
|
| 487 |
+
await asyncio.sleep(60)
|
| 488 |
+
if symbol not in await self.get_open_trades():
|
| 489 |
+
self.sentry_watchlist[symbol] = {"symbol": symbol, "strategy_hint": strategy_hint, "llm_decision_context": explorer_context}
|
| 490 |
+
|
| 491 |
|
| 492 |
except Exception as e:
|
| 493 |
print(f"❌ [Executor] فشل فادح أثناء التنفيذ لـ {symbol}: {e}")
|
|
|
|
| 503 |
strategy = kwargs.get('strategy')
|
| 504 |
exit_profile = kwargs.get('exit_profile')
|
| 505 |
|
| 506 |
+
expected_target_time = (datetime.now() + timedelta(minutes=15)).isoformat()
|
|
|
|
| 507 |
|
| 508 |
+
# (دمج سياق المستكشف (Layer 1) مع سياق الحارس (Layer 2))
|
| 509 |
+
decision_data = {
|
| 510 |
+
"reasoning": f"Tactical entry by Sentry based on {strategy}",
|
| 511 |
+
"strategy": strategy,
|
| 512 |
+
"exit_profile": exit_profile,
|
| 513 |
+
"exit_parameters": kwargs.get('exit_parameters', {}),
|
| 514 |
+
"tactical_context_at_decision": kwargs.get('tactical_context', {}),
|
| 515 |
+
"explorer_decision_context": kwargs.get('explorer_context', {})
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
new_trade = {
|
| 519 |
"id": str(int(datetime.now().timestamp())),
|
| 520 |
"symbol": symbol,
|
| 521 |
"entry_price": kwargs.get('entry_price'),
|
| 522 |
"entry_timestamp": datetime.now().isoformat(),
|
| 523 |
+
"decision_data": decision_data,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 524 |
"status": "OPEN",
|
| 525 |
"stop_loss": kwargs.get('stop_loss'),
|
| 526 |
"take_profit": kwargs.get('take_profit'),
|
| 527 |
+
"dynamic_stop_loss": kwargs.get('stop_loss'),
|
| 528 |
"trade_type": "LONG",
|
| 529 |
"position_size_usd": kwargs.get('position_size_usd'),
|
| 530 |
"expected_target_minutes": 15,
|
| 531 |
"expected_target_time": expected_target_time,
|
| 532 |
+
"is_monitored": True,
|
| 533 |
"strategy": strategy,
|
| 534 |
+
"monitoring_started": True
|
| 535 |
}
|
| 536 |
|
|
|
|
| 537 |
trades = await self.r2_service.get_open_trades_async()
|
| 538 |
trades.append(new_trade)
|
| 539 |
await self.r2_service.save_open_trades_async(trades)
|
| 540 |
|
| 541 |
portfolio_state = await self.r2_service.get_portfolio_state_async()
|
| 542 |
portfolio_state["invested_capital_usd"] = kwargs.get('position_size_usd')
|
| 543 |
+
portfolio_state["current_capital_usd"] = 0.0
|
| 544 |
portfolio_state["total_trades"] = portfolio_state.get("total_trades", 0) + 1
|
| 545 |
await self.r2_service.save_portfolio_state_async(portfolio_state)
|
| 546 |
|
|
|
|
| 556 |
print(f"❌ [R2] فشل حفظ الصفقة لـ {symbol}: {e}")
|
| 557 |
traceback.print_exc()
|
| 558 |
|
|
|
|
|
|
|
|
|
|
| 559 |
async def close_trade(self, trade_to_close, close_price, reason="System Close"):
|
| 560 |
"""(لا تغيير جوهري) - لا يزال مسؤولاً عن حساب PnL وتحديث R2 وتشغيل LearningHub"""
|
| 561 |
try:
|
|
|
|
| 581 |
trade_to_close['pnl_usd'] = pnl
|
| 582 |
trade_to_close['pnl_percent'] = pnl_percent
|
| 583 |
|
|
|
|
| 584 |
await self._archive_closed_trade(trade_to_close)
|
| 585 |
await self._update_trade_summary(trade_to_close)
|
| 586 |
|
|
|
|
| 587 |
portfolio_state = await self.r2_service.get_portfolio_state_async()
|
| 588 |
current_capital = portfolio_state.get("current_capital_usd", 0)
|
| 589 |
new_capital = current_capital + position_size + pnl
|
|
|
|
| 596 |
portfolio_state["total_loss_usd"] = portfolio_state.get("total_loss_usd", 0.0) + abs(pnl)
|
| 597 |
await self.r2_service.save_portfolio_state_async(portfolio_state)
|
| 598 |
|
|
|
|
| 599 |
open_trades = await self.r2_service.get_open_trades_async()
|
| 600 |
trades_to_keep = [t for t in open_trades if t.get('id') != trade_to_close.get('id')]
|
| 601 |
await self.r2_service.save_open_trades_async(trades_to_keep)
|
| 602 |
|
|
|
|
| 603 |
if symbol in self.sentry_tasks:
|
| 604 |
print(f"ℹ️ [Sentry] الصفقة {symbol} أغلقت، ستتوقف المراقبة.")
|
| 605 |
|
|
|
|
| 608 |
"new_capital": new_capital, "strategy": strategy, "reason": reason
|
| 609 |
})
|
| 610 |
|
| 611 |
+
# (تشغيل التعلم - لا تغيير)
|
| 612 |
if self.learning_hub and self.learning_hub.initialized:
|
| 613 |
print(f"🧠 [LearningHub] تشغيل التعلم (Reflector+Stats) لـ {symbol}...")
|
| 614 |
await self.learning_hub.analyze_trade_and_learn(trade_to_close, reason)
|
|
|
|
| 654 |
if re_analysis_decision.get('action') == "UPDATE_TRADE":
|
| 655 |
trade_to_update['stop_loss'] = re_analysis_decision['new_stop_loss']
|
| 656 |
trade_to_update['take_profit'] = re_analysis_decision['new_take_profit']
|
|
|
|
| 657 |
trade_to_update['dynamic_stop_loss'] = re_analysis_decision['new_stop_loss']
|
| 658 |
trade_to_update['decision_data']['exit_profile'] = re_analysis_decision['new_exit_profile']
|
| 659 |
trade_to_update['decision_data']['exit_parameters'] = re_analysis_decision['new_exit_parameters']
|
| 660 |
print(f" 🔄 (Explorer) {symbol}: Exit profile updated to {re_analysis_decision['new_exit_profile']}")
|
| 661 |
|
|
|
|
| 662 |
new_expected_minutes = re_analysis_decision.get('new_expected_minutes', 15)
|
| 663 |
trade_to_update['expected_target_minutes'] = new_expected_minutes
|
| 664 |
trade_to_update['expected_target_time'] = (datetime.now() + timedelta(minutes=new_expected_minutes)).isoformat()
|
| 665 |
trade_to_update['decision_data']['reasoning'] = re_analysis_decision.get('reasoning')
|
| 666 |
|
|
|
|
| 667 |
open_trades = await self.r2_service.get_open_trades_async()
|
| 668 |
for i, trade in enumerate(open_trades):
|
| 669 |
if trade.get('id') == trade_to_update.get('id'):
|
|
|
|
| 722 |
except Exception as e: print(f"❌ Failed to get trade by symbol {symbol}: {e}"); return None
|
| 723 |
|
| 724 |
|
| 725 |
+
print(f"✅ Trade Manager loaded - V5.1 (Sentry/Executor with ccxt.async_support: {CCXT_PRO_AVAILABLE})")
|