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