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