Spaces:
Sleeping
Sleeping
| # ============================================================================== | |
| # 💼 smart_portfolio.py (V38.0 - GEM-Architect: Interface Compatibility) | |
| # ============================================================================== | |
| # - Added public 'sync_state' method to fix AttributeError in TradeManager. | |
| # - Streamlined capital allocation logic. | |
| # ============================================================================== | |
| import asyncio | |
| import json | |
| import httpx | |
| import traceback | |
| import pandas as pd | |
| from datetime import datetime, timedelta | |
| from typing import Dict, Any, Tuple, Optional | |
| # استيراد الدستور المركزي لقراءة حالة السوق | |
| try: | |
| from ml_engine.processor import SystemLimits | |
| except ImportError: | |
| # Fallback في حال التشغيل المنفصل | |
| class SystemLimits: | |
| CURRENT_REGIME = "RANGE" | |
| class SmartPortfolio: | |
| def __init__(self, r2_service, data_manager): | |
| self.r2 = r2_service | |
| self.data_manager = data_manager | |
| # ⚙️ إعدادات المحفظة الأساسية | |
| self.MIN_CAPITAL_FOR_SPLIT = 20.0 | |
| self.DAILY_LOSS_LIMIT_PCT = 0.20 | |
| # حالة السوق | |
| self.market_trend = "NEUTRAL" | |
| self.fear_greed_index = 50 | |
| self.fear_greed_label = "Neutral" | |
| self.capital_lock = asyncio.Lock() | |
| # 📂 حالة المحفظة | |
| self.state = { | |
| "current_capital": 10.0, | |
| "allocated_capital_usd": 0.0, | |
| "session_start_balance": 10.0, | |
| "last_session_reset": datetime.now().isoformat(), | |
| "daily_net_pnl": 0.0, | |
| "is_trading_halted": False, | |
| "halt_reason": None | |
| } | |
| print("💼 [SmartPortfolio V38.0] Interface Compatibility Fixed.") | |
| # ============================================================================== | |
| # 🔌 Public Interface (واجهة الاتصال العامة) | |
| # ============================================================================== | |
| async def initialize(self): | |
| """التهيئة الأولية""" | |
| await self.sync_state() # استخدام الواجهة العامة | |
| await self._check_daily_reset() | |
| asyncio.create_task(self._market_monitor_loop()) | |
| async def sync_state(self): | |
| """ | |
| [FIX] الواجهة العامة التي يطلبها TradeManager. | |
| تقوم بمزامنة حالة المحفظة مع R2. | |
| """ | |
| await self._sync_state_from_r2() | |
| # ============================================================================== | |
| # 🔄 Internal Logic | |
| # ============================================================================== | |
| async def _market_monitor_loop(self): | |
| """مراقبة مؤشر الخوف والجشع فقط""" | |
| print("🦅 [SmartPortfolio] Sentiment Sentinel Started.") | |
| async with httpx.AsyncClient() as client: | |
| while True: | |
| try: | |
| regime = getattr(SystemLimits, 'CURRENT_REGIME', 'RANGE') | |
| self.market_trend = regime | |
| try: | |
| resp = await client.get("https://api.alternative.me/fng/?limit=1", timeout=10) | |
| data = resp.json() | |
| if data['data']: | |
| self.fear_greed_index = int(data['data'][0]['value']) | |
| self.fear_greed_label = data['data'][0]['value_classification'] | |
| except Exception: pass | |
| await asyncio.sleep(300) | |
| except Exception as e: | |
| print(f"⚠️ [Market Monitor] Error: {e}") | |
| await asyncio.sleep(60) | |
| # ============================================================================== | |
| # 🧠 Core Logic: Capital Allocation | |
| # ============================================================================== | |
| # [FIX] تم تعديل هذه الدالة لتعمل مع TradeManager الجديد | |
| # TradeManager يستدعيها باسم allocate_capital وليس request_entry_approval مباشرة | |
| def allocate_capital(self, candidate_data: Dict[str, Any]) -> Dict[str, float]: | |
| """ | |
| حساب المبلغ المخصص للصفقة بناءً على القواعد. | |
| هذه الدالة متزامنة (Synchronous) لأنها لا تتصل بالشبكة، وتستخدم الحالة المخزنة. | |
| """ | |
| # إذا كنا نريد منطقاً معقداً، يمكننا استخدام request_entry_approval | |
| # لكن TradeManager V70 يتوقع رداً سريعاً لتحديد الحجم | |
| current_cap = float(self.state["current_capital"]) | |
| allocated = float(self.state.get("allocated_capital_usd", 0.0)) | |
| free_capital = max(0.0, current_cap - allocated) | |
| # 1. تحديد عدد الفتحات (Slots) بناءً على الريجيم | |
| regime = candidate_data.get('asset_regime', "RANGE") | |
| if regime == "BULL": max_slots = 6 | |
| elif regime == "BEAR": max_slots = 3 | |
| else: max_slots = 4 | |
| if current_cap < self.MIN_CAPITAL_FOR_SPLIT: max_slots = 1 # All-in for small accounts | |
| # 2. الحجم الأساسي | |
| base_size = current_cap / max_slots | |
| # 3. التأكد من توفر الرصيد | |
| final_size = min(base_size, free_capital) | |
| # 4. الحد الأدنى للتداول | |
| if final_size < 5.0 and free_capital >= 5.0: | |
| final_size = 5.0 # محاولة الرفع للحد الأدنى | |
| return {"amount_usd": final_size} | |
| async def request_entry_approval(self, signal_data: Dict[str, Any], open_positions_count: int) -> Tuple[bool, Dict[str, Any]]: | |
| """ | |
| (متقدم) تطلب الموافقة وتحدد الحجم بناءً على جودة الحوكمة (Grade). | |
| """ | |
| async with self.capital_lock: | |
| # Circuit Breaker | |
| if self.state["is_trading_halted"]: | |
| return False, {"reason": f"Halted: {self.state['halt_reason']}"} | |
| current_cap = float(self.state["current_capital"]) | |
| # Governance Check | |
| gov_grade = signal_data.get('governance_grade', 'NORMAL') | |
| if gov_grade == 'REJECT': | |
| return False, {"reason": "Governance Rejected"} | |
| # Allocation Calculation (Reuse logic) | |
| allocation = self.allocate_capital(signal_data) | |
| size_usd = allocation['amount_usd'] | |
| if size_usd < 5.0: | |
| return False, {"reason": f"Insufficient Capital (${size_usd:.2f})"} | |
| return True, { | |
| "approved_size_usd": size_usd, | |
| "approved_tp": 0.0, # سيحدده TradeManager | |
| "system_confidence": signal_data.get('governance_score', 50)/100.0, | |
| "risk_multiplier": 1.0, | |
| "market_mood": self.market_trend | |
| } | |
| # ============================================================================== | |
| # 🔒 Capital Tracking | |
| # ============================================================================== | |
| def register_trade_entry(self, size_usd: float): | |
| """تسجيل دخول صفقة وحجز المبلغ""" | |
| self.state["allocated_capital_usd"] = float(self.state.get("allocated_capital_usd", 0.0)) + float(size_usd) | |
| # تصحيح في حال تجاوز الإجمالي بسبب أخطاء التقريب | |
| if self.state["allocated_capital_usd"] > self.state["current_capital"]: | |
| self.state["allocated_capital_usd"] = self.state["current_capital"] | |
| def register_trade_exit(self, capital_returned: float, net_pnl: float, is_win: bool): | |
| """تسجيل خروج وتحرير المبلغ""" | |
| # تحرير المبلغ المحجوز (تقديري، لأننا لا نعرف المبلغ الدقيق المحجوز لكل صفقة هنا بسهولة، لذا نخصم العائد التقريبي) | |
| # الأفضل: TradeManager يرسل المبلغ الأصلي المحجوز | |
| # هنا سنفترض أن capital_returned يشمل رأس المال + الربح | |
| original_invested = capital_returned - net_pnl | |
| current_allocated = float(self.state.get("allocated_capital_usd", 0.0)) | |
| self.state["allocated_capital_usd"] = max(0.0, current_allocated - original_invested) | |
| self.state["current_capital"] += net_pnl | |
| self.state["daily_net_pnl"] += net_pnl | |
| # التحقق من الإيقاف اليومي | |
| start = self.state["session_start_balance"] | |
| if start > 0: | |
| dd = (start - self.state["current_capital"]) / start | |
| if dd >= self.DAILY_LOSS_LIMIT_PCT: | |
| self.state["is_trading_halted"] = True | |
| self.state["halt_reason"] = "Daily Limit Hit After Exit" | |
| async def can_trade(self) -> bool: | |
| """هل التداول مسموح؟""" | |
| return not self.state["is_trading_halted"] | |
| # ============================================================================== | |
| # 💾 Persistence | |
| # ============================================================================== | |
| async def _check_daily_reset(self): | |
| last_reset = datetime.fromisoformat(self.state.get("last_session_reset", datetime.now().isoformat())) | |
| if datetime.now() - last_reset > timedelta(hours=24): | |
| self.state["session_start_balance"] = self.state["current_capital"] | |
| self.state["daily_net_pnl"] = 0.0 | |
| self.state["is_trading_halted"] = False | |
| self.state["last_session_reset"] = datetime.now().isoformat() | |
| await self._save_state_to_r2() | |
| async def _sync_state_from_r2(self): | |
| try: | |
| # محاولة قراءة الحالة الجديدة | |
| data = await self.r2.get_file_json_async("smart_portfolio_state.json") | |
| if data: | |
| self.state.update(data) | |
| else: | |
| # محاولة قراءة الحالة القديمة (Legacy) | |
| old = await self.r2.get_portfolio_state_async() | |
| if old: | |
| self.state["current_capital"] = float(old.get("current_capital_usd", 10.0)) | |
| self.state["session_start_balance"] = self.state["current_capital"] | |
| except: pass | |
| async def _save_state_to_r2(self): | |
| try: | |
| await self.r2.upload_json_async(self.state, "smart_portfolio_state.json") | |
| except: pass |