Trad / smart_portfolio.py
Riy777's picture
Update smart_portfolio.py
cd798bf verified
# ==============================================================================
# 💼 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