Spaces:
Paused
Paused
| # ============================================================================== | |
| # 💼 smart_portfolio.py (V37.2 - GEM-Architect: Regime-Aware Targeting) | |
| # ============================================================================== | |
| import asyncio | |
| import json | |
| import httpx | |
| import traceback | |
| import pandas as pd | |
| import pandas_ta as ta | |
| 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 V37.2] Regime-Aware Sizing & Targeting Initialized.") | |
| async def initialize(self): | |
| await self._sync_state_from_r2() | |
| await self._check_daily_reset() | |
| asyncio.create_task(self._market_monitor_loop()) | |
| 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: Entry Approval (Grade-Based + Regime-Aware TP) | |
| # ============================================================================== | |
| async def request_entry_approval(self, signal_data: Dict[str, Any], open_positions_count: int) -> Tuple[bool, Dict[str, Any]]: | |
| """ | |
| تطلب الموافقة وتحدد الحجم بناءً على جودة الحوكمة (Grade). | |
| وتحدد الهدف (TP) بناءً على الريجيم الحالي (Context). | |
| """ | |
| async with self.capital_lock: | |
| # 1. Circuit Breaker | |
| if self.state["is_trading_halted"]: | |
| return False, {"reason": f"Halted: {self.state['halt_reason']}"} | |
| # 2. Daily Loss Limit Check | |
| current_cap = float(self.state["current_capital"]) | |
| start_cap = float(self.state["session_start_balance"]) | |
| drawdown = (start_cap - current_cap) / start_cap if start_cap > 0 else 0 | |
| if drawdown >= self.DAILY_LOSS_LIMIT_PCT: | |
| self.state["is_trading_halted"] = True | |
| self.state["halt_reason"] = "Daily Loss Limit (-20%)" | |
| await self._save_state_to_r2() | |
| return False, {"reason": "Daily Limit Hit"} | |
| # ✅ 3. Governance Check (Quality Control) | |
| gov_grade = signal_data.get('governance_grade', 'NORMAL') | |
| gov_score = signal_data.get('governance_score', 50.0) | |
| if gov_grade == 'REJECT': | |
| return False, {"reason": f"Governance Rejected (Score: {gov_score})"} | |
| # 4. Regime-Based Slots | |
| # محاولة استخراج الريجيم من الإشارة نفسها (الأدق) أو العودة للنظام العام | |
| regime = signal_data.get('asset_regime', getattr(SystemLimits, 'CURRENT_REGIME', 'RANGE')) | |
| if regime == "BULL": max_slots = 6 | |
| elif regime == "BEAR": max_slots = 3 | |
| elif regime == "DEAD": max_slots = 2 | |
| else: max_slots = 4 # RANGE | |
| if current_cap < self.MIN_CAPITAL_FOR_SPLIT: max_slots = min(max_slots, 2) | |
| if open_positions_count >= max_slots: | |
| return False, {"reason": f"Max slots reached for {regime} ({open_positions_count}/{max_slots})"} | |
| # 5. Free Capital Check | |
| allocated = float(self.state.get("allocated_capital_usd", 0.0)) | |
| free_capital = max(0.0, current_cap - allocated) | |
| if free_capital < 5.0: | |
| return False, {"reason": f"Insufficient Free Capital (${free_capital:.2f})"} | |
| # ✅ 6. Position Sizing (Grade Logic) | |
| target_slot_size = 0.0 | |
| if current_cap >= self.MIN_CAPITAL_FOR_SPLIT: | |
| target_slot_size = current_cap / max_slots | |
| else: | |
| target_slot_size = free_capital * 0.95 # All-in for small accounts | |
| # مضاعف الجودة | |
| quality_multiplier = 0.5 # Default NORMAL | |
| if gov_grade == "ULTRA": quality_multiplier = 1.0 # 100% of slot | |
| elif gov_grade == "STRONG": quality_multiplier = 0.75 # 75% of slot | |
| elif gov_grade == "NORMAL": quality_multiplier = 0.50 # 50% of slot | |
| elif gov_grade == "WEAK": quality_multiplier = 0.25 # 25% of slot | |
| # حساب الحجم النهائي | |
| final_size_usd = target_slot_size * quality_multiplier | |
| final_size_usd = min(final_size_usd, free_capital) # لا نتجاوز المتوفر | |
| if final_size_usd < 5.0: | |
| # إذا كانت النسبة صغيرة جداً ولكن الحساب يسمح، نرفعها للحد الأدنى | |
| if free_capital >= 5.0: final_size_usd = 5.0 | |
| else: return False, {"reason": "Calculated size too small"} | |
| # ✅ 7. Dynamic TP Selection (The Fix) | |
| # إصلاح المنطق: في أسواق Range/Dead/Bear، نلتزم بـ TP1 لضمان الخروج بربح. | |
| entry_price = float(signal_data.get('sniper_entry_price') or signal_data.get('current_price')) | |
| tp_map = signal_data.get('tp_map', {}) | |
| if regime == "BULL" and gov_grade in ["STRONG", "ULTRA"]: | |
| # فقط في البول رن القوي نستهدف الأهداف البعيدة | |
| selected_tp = tp_map.get('TP3') or tp_map.get('TP4') | |
| target_label = "TP3/4 (Bull Run)" | |
| elif regime in ["RANGE", "DEAD", "BEAR"]: | |
| # في الأسواق العرضية والميتة والهابطة، نأخذ أول هدف ونخرج | |
| selected_tp = tp_map.get('TP1') | |
| target_label = f"TP1 ({regime} Scalp)" | |
| else: | |
| # الحالة الطبيعية | |
| selected_tp = tp_map.get('TP2') | |
| target_label = "TP2" | |
| if not selected_tp or selected_tp <= entry_price: selected_tp = entry_price * 1.02 | |
| return True, { | |
| "approved_size_usd": float(final_size_usd), | |
| "approved_tp": float(selected_tp), | |
| "target_label": target_label, | |
| "system_confidence": gov_score / 100.0, | |
| "risk_multiplier": quality_multiplier, | |
| "market_mood": f"{regime} | Grade: {gov_grade}" | |
| } | |
| # ============================================================================== | |
| # 🔒 Capital Tracking | |
| # ============================================================================== | |
| async def register_new_position(self, size_usd: float): | |
| async with self.capital_lock: | |
| 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"] | |
| await self._save_state_to_r2() | |
| async def register_closed_position(self, released_capital_usd: float, net_pnl: float, fees: float): | |
| async with self.capital_lock: | |
| current_allocated = float(self.state.get("allocated_capital_usd", 0.0)) | |
| self.state["allocated_capital_usd"] = max(0.0, current_allocated - released_capital_usd) | |
| net_impact = net_pnl - fees | |
| self.state["current_capital"] += net_impact | |
| self.state["daily_net_pnl"] += net_impact | |
| start = self.state["session_start_balance"] | |
| dd = (start - self.state["current_capital"]) / start if start > 0 else 0 | |
| if dd >= self.DAILY_LOSS_LIMIT_PCT: | |
| self.state["is_trading_halted"] = True | |
| self.state["halt_reason"] = "Daily Limit Hit After Exit" | |
| await self._save_state_to_r2() | |
| 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_async("smart_portfolio_state.json") | |
| if data: | |
| loaded = json.loads(data) | |
| self.state.update(loaded) | |
| else: | |
| 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 |