Riy777 commited on
Commit
cd798bf
·
verified ·
1 Parent(s): a1d34eb

Update smart_portfolio.py

Browse files
Files changed (1) hide show
  1. smart_portfolio.py +105 -111
smart_portfolio.py CHANGED
@@ -1,5 +1,8 @@
1
  # ==============================================================================
2
- # 💼 smart_portfolio.py (V37.2 - GEM-Architect: Regime-Aware Targeting)
 
 
 
3
  # ==============================================================================
4
 
5
  import asyncio
@@ -7,7 +10,6 @@ import json
7
  import httpx
8
  import traceback
9
  import pandas as pd
10
- import pandas_ta as ta
11
  from datetime import datetime, timedelta
12
  from typing import Dict, Any, Tuple, Optional
13
 
@@ -46,13 +48,28 @@ class SmartPortfolio:
46
  "halt_reason": None
47
  }
48
 
49
- print("💼 [SmartPortfolio V37.2] Regime-Aware Sizing & Targeting Initialized.")
50
 
 
 
 
 
51
  async def initialize(self):
52
- await self._sync_state_from_r2()
 
53
  await self._check_daily_reset()
54
  asyncio.create_task(self._market_monitor_loop())
55
 
 
 
 
 
 
 
 
 
 
 
56
  async def _market_monitor_loop(self):
57
  """مراقبة مؤشر الخوف والجشع فقط"""
58
  print("🦅 [SmartPortfolio] Sentiment Sentinel Started.")
@@ -76,136 +93,112 @@ class SmartPortfolio:
76
  await asyncio.sleep(60)
77
 
78
  # ==============================================================================
79
- # 🧠 Core Logic: Entry Approval (Grade-Based + Regime-Aware TP)
80
  # ==============================================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  async def request_entry_approval(self, signal_data: Dict[str, Any], open_positions_count: int) -> Tuple[bool, Dict[str, Any]]:
82
  """
83
- تطلب الموافقة وتحدد الحجم بناءً على جودة الحوكمة (Grade).
84
- وتحدد الهدف (TP) بناءً على الريجيم الحالي (Context).
85
  """
86
  async with self.capital_lock:
87
- # 1. Circuit Breaker
88
  if self.state["is_trading_halted"]:
89
  return False, {"reason": f"Halted: {self.state['halt_reason']}"}
90
 
91
- # 2. Daily Loss Limit Check
92
  current_cap = float(self.state["current_capital"])
93
- start_cap = float(self.state["session_start_balance"])
94
 
95
- drawdown = (start_cap - current_cap) / start_cap if start_cap > 0 else 0
96
- if drawdown >= self.DAILY_LOSS_LIMIT_PCT:
97
- self.state["is_trading_halted"] = True
98
- self.state["halt_reason"] = "Daily Loss Limit (-20%)"
99
- await self._save_state_to_r2()
100
- return False, {"reason": "Daily Limit Hit"}
101
-
102
- # ✅ 3. Governance Check (Quality Control)
103
  gov_grade = signal_data.get('governance_grade', 'NORMAL')
104
- gov_score = signal_data.get('governance_score', 50.0)
105
-
106
  if gov_grade == 'REJECT':
107
- return False, {"reason": f"Governance Rejected (Score: {gov_score})"}
108
-
109
- # 4. Regime-Based Slots
110
- # محاولة استخراج الريجيم من الإشارة نفسها (الأدق) أو العودة للنظام العام
111
- regime = signal_data.get('asset_regime', getattr(SystemLimits, 'CURRENT_REGIME', 'RANGE'))
112
-
113
- if regime == "BULL": max_slots = 6
114
- elif regime == "BEAR": max_slots = 3
115
- elif regime == "DEAD": max_slots = 2
116
- else: max_slots = 4 # RANGE
117
-
118
- if current_cap < self.MIN_CAPITAL_FOR_SPLIT: max_slots = min(max_slots, 2)
119
 
120
- if open_positions_count >= max_slots:
121
- return False, {"reason": f"Max slots reached for {regime} ({open_positions_count}/{max_slots})"}
 
122
 
123
- # 5. Free Capital Check
124
- allocated = float(self.state.get("allocated_capital_usd", 0.0))
125
- free_capital = max(0.0, current_cap - allocated)
126
-
127
- if free_capital < 5.0:
128
- return False, {"reason": f"Insufficient Free Capital (${free_capital:.2f})"}
129
-
130
- # ✅ 6. Position Sizing (Grade Logic)
131
- target_slot_size = 0.0
132
- if current_cap >= self.MIN_CAPITAL_FOR_SPLIT:
133
- target_slot_size = current_cap / max_slots
134
- else:
135
- target_slot_size = free_capital * 0.95 # All-in for small accounts
136
-
137
- # مضاعف الجودة
138
- quality_multiplier = 0.5 # Default NORMAL
139
- if gov_grade == "ULTRA": quality_multiplier = 1.0 # 100% of slot
140
- elif gov_grade == "STRONG": quality_multiplier = 0.75 # 75% of slot
141
- elif gov_grade == "NORMAL": quality_multiplier = 0.50 # 50% of slot
142
- elif gov_grade == "WEAK": quality_multiplier = 0.25 # 25% of slot
143
-
144
- # حساب الحجم النهائي
145
- final_size_usd = target_slot_size * quality_multiplier
146
- final_size_usd = min(final_size_usd, free_capital) # لا نتجاوز المتوفر
147
-
148
- if final_size_usd < 5.0:
149
- # إذا كانت النسبة صغيرة جداً ولكن الحساب يسمح، نرفعها للحد الأدنى
150
- if free_capital >= 5.0: final_size_usd = 5.0
151
- else: return False, {"reason": "Calculated size too small"}
152
-
153
- # ✅ 7. Dynamic TP Selection (The Fix)
154
- # إصلاح المنطق: في أسواق Range/Dead/Bear، نلتزم بـ TP1 لضمان الخروج بربح.
155
- entry_price = float(signal_data.get('sniper_entry_price') or signal_data.get('current_price'))
156
- tp_map = signal_data.get('tp_map', {})
157
-
158
- if regime == "BULL" and gov_grade in ["STRONG", "ULTRA"]:
159
- # فقط في البول رن القوي نستهدف الأهداف البعيدة
160
- selected_tp = tp_map.get('TP3') or tp_map.get('TP4')
161
- target_label = "TP3/4 (Bull Run)"
162
- elif regime in ["RANGE", "DEAD", "BEAR"]:
163
- # في الأسواق العرضية والميتة والهابطة، نأخذ أول هدف ونخرج
164
- selected_tp = tp_map.get('TP1')
165
- target_label = f"TP1 ({regime} Scalp)"
166
- else:
167
- # الحالة الطبيعية
168
- selected_tp = tp_map.get('TP2')
169
- target_label = "TP2"
170
-
171
- if not selected_tp or selected_tp <= entry_price: selected_tp = entry_price * 1.02
172
 
173
  return True, {
174
- "approved_size_usd": float(final_size_usd),
175
- "approved_tp": float(selected_tp),
176
- "target_label": target_label,
177
- "system_confidence": gov_score / 100.0,
178
- "risk_multiplier": quality_multiplier,
179
- "market_mood": f"{regime} | Grade: {gov_grade}"
180
  }
181
 
182
  # ==============================================================================
183
  # 🔒 Capital Tracking
184
  # ==============================================================================
185
- async def register_new_position(self, size_usd: float):
186
- async with self.capital_lock:
187
- self.state["allocated_capital_usd"] = float(self.state.get("allocated_capital_usd", 0.0)) + float(size_usd)
188
- if self.state["allocated_capital_usd"] > self.state["current_capital"]:
189
- self.state["allocated_capital_usd"] = self.state["current_capital"]
190
- await self._save_state_to_r2()
191
-
192
- async def register_closed_position(self, released_capital_usd: float, net_pnl: float, fees: float):
193
- async with self.capital_lock:
194
- current_allocated = float(self.state.get("allocated_capital_usd", 0.0))
195
- self.state["allocated_capital_usd"] = max(0.0, current_allocated - released_capital_usd)
196
-
197
- net_impact = net_pnl - fees
198
- self.state["current_capital"] += net_impact
199
- self.state["daily_net_pnl"] += net_impact
200
 
201
- start = self.state["session_start_balance"]
202
- dd = (start - self.state["current_capital"]) / start if start > 0 else 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  if dd >= self.DAILY_LOSS_LIMIT_PCT:
204
  self.state["is_trading_halted"] = True
205
  self.state["halt_reason"] = "Daily Limit Hit After Exit"
206
-
207
- await self._save_state_to_r2()
208
 
 
 
 
 
 
 
 
209
  async def _check_daily_reset(self):
210
  last_reset = datetime.fromisoformat(self.state.get("last_session_reset", datetime.now().isoformat()))
211
  if datetime.now() - last_reset > timedelta(hours=24):
@@ -217,11 +210,12 @@ class SmartPortfolio:
217
 
218
  async def _sync_state_from_r2(self):
219
  try:
220
- data = await self.r2.get_file_async("smart_portfolio_state.json")
 
221
  if data:
222
- loaded = json.loads(data)
223
- self.state.update(loaded)
224
  else:
 
225
  old = await self.r2.get_portfolio_state_async()
226
  if old:
227
  self.state["current_capital"] = float(old.get("current_capital_usd", 10.0))
 
1
  # ==============================================================================
2
+ # 💼 smart_portfolio.py (V38.0 - GEM-Architect: Interface Compatibility)
3
+ # ==============================================================================
4
+ # - Added public 'sync_state' method to fix AttributeError in TradeManager.
5
+ # - Streamlined capital allocation logic.
6
  # ==============================================================================
7
 
8
  import asyncio
 
10
  import httpx
11
  import traceback
12
  import pandas as pd
 
13
  from datetime import datetime, timedelta
14
  from typing import Dict, Any, Tuple, Optional
15
 
 
48
  "halt_reason": None
49
  }
50
 
51
+ print("💼 [SmartPortfolio V38.0] Interface Compatibility Fixed.")
52
 
53
+ # ==============================================================================
54
+ # 🔌 Public Interface (واجهة الاتصال العامة)
55
+ # ==============================================================================
56
+
57
  async def initialize(self):
58
+ """التهيئة الأولية"""
59
+ await self.sync_state() # استخدام الواجهة العامة
60
  await self._check_daily_reset()
61
  asyncio.create_task(self._market_monitor_loop())
62
 
63
+ async def sync_state(self):
64
+ """
65
+ [FIX] الواجهة العامة التي يطلبها TradeManager.
66
+ تقوم بمزامنة حالة المحفظة مع R2.
67
+ """
68
+ await self._sync_state_from_r2()
69
+
70
+ # ==============================================================================
71
+ # 🔄 Internal Logic
72
+ # ==============================================================================
73
  async def _market_monitor_loop(self):
74
  """مراقبة مؤشر الخوف والجشع فقط"""
75
  print("🦅 [SmartPortfolio] Sentiment Sentinel Started.")
 
93
  await asyncio.sleep(60)
94
 
95
  # ==============================================================================
96
+ # 🧠 Core Logic: Capital Allocation
97
  # ==============================================================================
98
+
99
+ # [FIX] تم تعديل هذه الدالة لتعمل مع TradeManager الجديد
100
+ # TradeManager يستدعيها باسم allocate_capital وليس request_entry_approval مباشرة
101
+ def allocate_capital(self, candidate_data: Dict[str, Any]) -> Dict[str, float]:
102
+ """
103
+ حساب المبلغ المخصص للصفقة بناءً على القواعد.
104
+ هذه الدالة متزامنة (Synchronous) لأنها لا تتصل بالشبكة، وتستخدم الحالة المخزنة.
105
+ """
106
+ # إذا كنا نريد منطقاً معقداً، يمكننا استخدام request_entry_approval
107
+ # لكن TradeManager V70 يتوقع رداً سريعاً لتحديد الحجم
108
+
109
+ current_cap = float(self.state["current_capital"])
110
+ allocated = float(self.state.get("allocated_capital_usd", 0.0))
111
+ free_capital = max(0.0, current_cap - allocated)
112
+
113
+ # 1. تحديد عدد الفتحات (Slots) بناءً على الريجيم
114
+ regime = candidate_data.get('asset_regime', "RANGE")
115
+ if regime == "BULL": max_slots = 6
116
+ elif regime == "BEAR": max_slots = 3
117
+ else: max_slots = 4
118
+
119
+ if current_cap < self.MIN_CAPITAL_FOR_SPLIT: max_slots = 1 # All-in for small accounts
120
+
121
+ # 2. الحجم الأساسي
122
+ base_size = current_cap / max_slots
123
+
124
+ # 3. التأكد من توفر الرصيد
125
+ final_size = min(base_size, free_capital)
126
+
127
+ # 4. الحد الأدنى للتداول
128
+ if final_size < 5.0 and free_capital >= 5.0:
129
+ final_size = 5.0 # محاولة الرفع للحد الأدنى
130
+
131
+ return {"amount_usd": final_size}
132
+
133
  async def request_entry_approval(self, signal_data: Dict[str, Any], open_positions_count: int) -> Tuple[bool, Dict[str, Any]]:
134
  """
135
+ (متقدم) تطلب الموافقة وتحدد الحجم بناءً على جودة الحوكمة (Grade).
 
136
  """
137
  async with self.capital_lock:
138
+ # Circuit Breaker
139
  if self.state["is_trading_halted"]:
140
  return False, {"reason": f"Halted: {self.state['halt_reason']}"}
141
 
 
142
  current_cap = float(self.state["current_capital"])
 
143
 
144
+ # Governance Check
 
 
 
 
 
 
 
145
  gov_grade = signal_data.get('governance_grade', 'NORMAL')
 
 
146
  if gov_grade == 'REJECT':
147
+ return False, {"reason": "Governance Rejected"}
 
 
 
 
 
 
 
 
 
 
 
148
 
149
+ # Allocation Calculation (Reuse logic)
150
+ allocation = self.allocate_capital(signal_data)
151
+ size_usd = allocation['amount_usd']
152
 
153
+ if size_usd < 5.0:
154
+ return False, {"reason": f"Insufficient Capital (${size_usd:.2f})"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
 
156
  return True, {
157
+ "approved_size_usd": size_usd,
158
+ "approved_tp": 0.0, # سيحدده TradeManager
159
+ "system_confidence": signal_data.get('governance_score', 50)/100.0,
160
+ "risk_multiplier": 1.0,
161
+ "market_mood": self.market_trend
 
162
  }
163
 
164
  # ==============================================================================
165
  # 🔒 Capital Tracking
166
  # ==============================================================================
167
+ def register_trade_entry(self, size_usd: float):
168
+ """تسجيل دخول صفقة وحجز المبلغ"""
169
+ self.state["allocated_capital_usd"] = float(self.state.get("allocated_capital_usd", 0.0)) + float(size_usd)
170
+ # تصحيح في حال تجاوز الإجمالي بسبب أخطاء التقريب
171
+ if self.state["allocated_capital_usd"] > self.state["current_capital"]:
172
+ self.state["allocated_capital_usd"] = self.state["current_capital"]
 
 
 
 
 
 
 
 
 
173
 
174
+ def register_trade_exit(self, capital_returned: float, net_pnl: float, is_win: bool):
175
+ """تسجيل خروج وتحرير المبلغ"""
176
+ # تحرير المبلغ المحجوز (تقديري، لأننا لا نعرف المبلغ الدقيق المحجوز لكل صفقة هنا بسهولة، لذا نخصم العائد التقريبي)
177
+ # الأفضل: TradeManager يرسل المبلغ الأصلي المحجوز
178
+ # هنا سنفترض أن capital_returned يشمل رأس المال + الربح
179
+ original_invested = capital_returned - net_pnl
180
+
181
+ current_allocated = float(self.state.get("allocated_capital_usd", 0.0))
182
+ self.state["allocated_capital_usd"] = max(0.0, current_allocated - original_invested)
183
+
184
+ self.state["current_capital"] += net_pnl
185
+ self.state["daily_net_pnl"] += net_pnl
186
+
187
+ # التحقق من الإيقاف اليومي
188
+ start = self.state["session_start_balance"]
189
+ if start > 0:
190
+ dd = (start - self.state["current_capital"]) / start
191
  if dd >= self.DAILY_LOSS_LIMIT_PCT:
192
  self.state["is_trading_halted"] = True
193
  self.state["halt_reason"] = "Daily Limit Hit After Exit"
 
 
194
 
195
+ async def can_trade(self) -> bool:
196
+ """هل التداول مسموح؟"""
197
+ return not self.state["is_trading_halted"]
198
+
199
+ # ==============================================================================
200
+ # 💾 Persistence
201
+ # ==============================================================================
202
  async def _check_daily_reset(self):
203
  last_reset = datetime.fromisoformat(self.state.get("last_session_reset", datetime.now().isoformat()))
204
  if datetime.now() - last_reset > timedelta(hours=24):
 
210
 
211
  async def _sync_state_from_r2(self):
212
  try:
213
+ # محاولة قراءة الحالة الجديدة
214
+ data = await self.r2.get_file_json_async("smart_portfolio_state.json")
215
  if data:
216
+ self.state.update(data)
 
217
  else:
218
+ # محاولة قراءة الحالة القديمة (Legacy)
219
  old = await self.r2.get_portfolio_state_async()
220
  if old:
221
  self.state["current_capital"] = float(old.get("current_capital_usd", 10.0))