Riy777 commited on
Commit
fc2dff6
·
verified ·
1 Parent(s): eace6a3

Create smart_portfolio.py

Browse files
Files changed (1) hide show
  1. smart_portfolio.py +312 -0
smart_portfolio.py ADDED
@@ -0,0 +1,312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ==============================================================================
2
+ # 💼 smart_portfolio.py (V1.2 - GEM-Architect: True Risk & Market Mood)
3
+ # ==============================================================================
4
+ # التحديثات الجذرية:
5
+ # 1. إدارة صارمة لرأس المال (Allocated vs Free).
6
+ # 2. دمج مؤشر الخوف والجشع (Fear & Greed Index) في حساب الثقة.
7
+ # 3. فرض حقيقي لعدد الخانات (Slots Enforcement).
8
+ # 4. معالجة الرسوم بشكل منفصل ودقيق.
9
+ # ==============================================================================
10
+
11
+ import asyncio
12
+ import json
13
+ import httpx
14
+ import traceback
15
+ import pandas as pd
16
+ import pandas_ta as ta
17
+ from datetime import datetime, timedelta
18
+ from typing import Dict, Any, Tuple, Optional
19
+
20
+ class SmartPortfolio:
21
+ def __init__(self, r2_service, data_manager):
22
+ self.r2 = r2_service
23
+ self.data_manager = data_manager
24
+
25
+ # ⚙️ إعدادات المحفظة (الدستور المالي)
26
+ self.MIN_CAPITAL_FOR_SPLIT = 20.0
27
+ self.MAX_SLOTS_BELOW_THRESHOLD = 1
28
+ self.MAX_SLOTS_ABOVE_THRESHOLD = 5
29
+
30
+ self.DAILY_LOSS_LIMIT_PCT = 0.20
31
+
32
+ # 📊 أوزان الثقة المركبة (تم التحديث لإضافة Market Mood)
33
+ self.WEIGHTS = {
34
+ "L2_TECHNICAL": 0.25,
35
+ "L3_ORACLE": 0.35,
36
+ "L4_SNIPER": 0.20,
37
+ "CONTEXT": 0.10, # حيتان وأخبار
38
+ "MARKET_MOOD": 0.10 # Fear & Greed + BTC Trend
39
+ }
40
+
41
+ # حالة السوق والمزاج العام
42
+ self.market_trend = "NEUTRAL"
43
+ self.fear_greed_index = 50 # 0-100 (50 = Neutral)
44
+ self.fear_greed_label = "Neutral"
45
+
46
+ self.capital_lock = asyncio.Lock()
47
+
48
+ # 📂 حالة المحفظة (تمت إضافة allocated_capital_usd)
49
+ self.state = {
50
+ "current_capital": 10.0, # إجمالي حقوق الملكية (Equity)
51
+ "allocated_capital_usd": 0.0, # الأموال المحجوزة في صفقات نشطة
52
+ "session_start_balance": 10.0,
53
+ "last_session_reset": datetime.now().isoformat(),
54
+ "daily_net_pnl": 0.0,
55
+ "is_trading_halted": False,
56
+ "halt_reason": None
57
+ }
58
+
59
+ print("💼 [SmartPortfolio V1.2] True Risk & Market Mood Initialized.")
60
+
61
+ async def initialize(self):
62
+ await self._sync_state_from_r2()
63
+ await self._check_daily_reset()
64
+ # تشغيل المراقبين
65
+ asyncio.create_task(self._market_monitor_loop())
66
+
67
+ # ==============================================================================
68
+ # 🦅 Market Monitor: BTC Trend + Fear & Greed
69
+ # ==============================================================================
70
+ async def _market_monitor_loop(self):
71
+ """مراقبة خلفية لاتجاه البيتكوين ومؤشر الخوف والجشع"""
72
+ print("🦅 [SmartPortfolio] Market Sentinel Started.")
73
+ async with httpx.AsyncClient() as client:
74
+ while True:
75
+ try:
76
+ # 1. تحليل ترند BTC
77
+ ohlcv = await self.data_manager.get_latest_ohlcv("BTC/USDT", "4h", limit=50)
78
+ if ohlcv:
79
+ df = pd.DataFrame(ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
80
+ ema20 = df['close'].ewm(span=20).mean().iloc[-1]
81
+ ema50 = df['close'].ewm(span=50).mean().iloc[-1]
82
+ close = df['close'].iloc[-1]
83
+
84
+ if close > ema20 and ema20 > ema50: self.market_trend = "BULLISH"
85
+ elif close < ema20 and ema20 < ema50: self.market_trend = "BEARISH"
86
+ else: self.market_trend = "NEUTRAL"
87
+
88
+ # 2. جلب مؤشر الخوف والجشع (Fear & Greed API)
89
+ # نجلبه بمعدل أقل لتجنب الحظر، لكن هنا في نفس الحلقة للتبسيط
90
+ try:
91
+ resp = await client.get("https://api.alternative.me/fng/?limit=1", timeout=5)
92
+ data = resp.json()
93
+ if data['data']:
94
+ self.fear_greed_index = int(data['data'][0]['value'])
95
+ self.fear_greed_label = data['data'][0]['value_classification']
96
+ except Exception:
97
+ pass # الحفاظ على القيمة السابقة عند الفشل
98
+
99
+ await asyncio.sleep(300) # كل 5 دقائق
100
+ except Exception as e:
101
+ print(f"⚠️ [Market Monitor] Error: {e}")
102
+ await asyncio.sleep(60)
103
+
104
+ # ==============================================================================
105
+ # 🧮 The Consensus Engine (Updated with Mood)
106
+ # ==============================================================================
107
+ def _calculate_composite_confidence(self, signal_data: Dict[str, Any]) -> float:
108
+ try:
109
+ l2_score = float(signal_data.get('enhanced_final_score', 0.5))
110
+ oracle_conf = float(signal_data.get('confidence', 0.5))
111
+ sniper_score = float(signal_data.get('sniper_score', 0.5))
112
+
113
+ # تطبيع سياق الحيتان والأخبار (0.0 - 1.0)
114
+ whale_impact = float(signal_data.get('whale_score', 0.0))
115
+ news_impact = float(signal_data.get('news_score', 0.0))
116
+ context_val = max(0.0, min(1.0, 0.5 + whale_impact + news_impact))
117
+
118
+ # حساب سكور "مزاج السوق" (Market Mood Score)
119
+ # الخوف الشديد (أقل من 20) قد يكون فرصة شراء ممتازة (Contrarian)
120
+ # الجشع الشديد (فوق 80) قد يكون خطر تصحيح
121
+ mood_score = 0.5
122
+ if self.market_trend == "BULLISH":
123
+ mood_score += 0.2
124
+ elif self.market_trend == "BEARISH":
125
+ mood_score -= 0.2
126
+
127
+ # دمج F&G: إذا السوق صاعد وجشع معتدل = جيد. جشع مفرط = سيء.
128
+ fg = self.fear_greed_index
129
+ if fg < 20: mood_score += 0.1 ( # خوف شديد = فرصة ارتداد محتملة
130
+ elif fg > 80: mood_score -= 0.1 # جشع شديد = خطر القمة
131
+
132
+ mood_score = max(0.0, min(1.0, mood_score))
133
+
134
+ final_conf = (
135
+ (l2_score * self.WEIGHTS["L2_TECHNICAL"]) +
136
+ (oracle_conf * self.WEIGHTS["L3_ORACLE"]) +
137
+ (sniper_score * self.WEIGHTS["L4_SNIPER"]) +
138
+ (context_val * self.WEIGHTS["CONTEXT"]) +
139
+ (mood_score * self.WEIGHTS["MARKET_MOOD"])
140
+ )
141
+ return round(final_conf, 3)
142
+
143
+ except Exception:
144
+ return float(signal_data.get('confidence', 0.5))
145
+
146
+ # ==============================================================================
147
+ # 🧠 Core Logic: Entry Approval (True Risk)
148
+ # ==============================================================================
149
+ async def request_entry_approval(self, signal_data: Dict[str, Any], open_positions_count: int) -> Tuple[bool, Dict[str, Any]]:
150
+ """
151
+ تطلب الموافقة مع مراعاة رأس المال الحر (Free Capital) وعدد الخانات المشغولة.
152
+ """
153
+ async with self.capital_lock:
154
+ # 1. Circuit Breaker Check
155
+ if self.state["is_trading_halted"]:
156
+ return False, {"reason": f"Halted: {self.state['halt_reason']}"}
157
+
158
+ # 2. Daily Loss Limit Check
159
+ current_cap = float(self.state["current_capital"])
160
+ start_cap = float(self.state["session_start_balance"])
161
+
162
+ drawdown = (start_cap - current_cap) / start_cap if start_cap > 0 else 0
163
+ if drawdown >= self.DAILY_LOSS_LIMIT_PCT:
164
+ self.state["is_trading_halted"] = True
165
+ self.state["halt_reason"] = "Daily Loss Limit (-20%)"
166
+ await self._save_state_to_r2()
167
+ return False, {"reason": "Daily Limit Hit"}
168
+
169
+ # 3. Slot Availability Check (Hard Limit)
170
+ max_slots = self.MAX_SLOTS_ABOVE_THRESHOLD if current_cap >= self.MIN_CAPITAL_FOR_SPLIT else self.MAX_SLOTS_BELOW_THRESHOLD
171
+
172
+ if open_positions_count >= max_slots:
173
+ return False, {"reason": f"Max slots reached ({open_positions_count}/{max_slots})"}
174
+
175
+ # 4. Free Capital Check
176
+ allocated = float(self.state.get("allocated_capital_usd", 0.0))
177
+ free_capital = max(0.0, current_cap - allocated)
178
+
179
+ if free_capital < 5.0:
180
+ return False, {"reason": f"Insufficient Free Capital (${free_capital:.2f})"}
181
+
182
+ # 5. Position Sizing (Island Logic)
183
+ # نقسم رأس المال الحر على عدد الخانات المتبقية لضمان التوزيع العادل
184
+ remaining_slots = max_slots - open_positions_count
185
+ base_allocation = 0.0
186
+
187
+ if current_cap >= self.MIN_CAPITAL_FOR_SPLIT:
188
+ # توزيع ذكي: رأس المال الكلي / عدد الخانات الكلي (للحفاظ على حجم ثابت)
189
+ # لكن بشرط ألا يتجاوز رأس المال الحر
190
+ target_size = current_cap / self.MAX_SLOTS_ABOVE_THRESHOLD
191
+ base_allocation = min(target_size, free_capital)
192
+ else:
193
+ # حساب النمو (All-in Safe)
194
+ base_allocation = free_capital * 0.95
195
+
196
+ # 6. Consensus & Risk Multiplier
197
+ system_confidence = self._calculate_composite_confidence(signal_data)
198
+ risk_multiplier = 1.0
199
+
200
+ if system_confidence >= 0.80: risk_multiplier = 1.35
201
+ elif system_confidence >= 0.70: risk_multiplier = 1.0
202
+ elif system_confidence >= 0.60: risk_multiplier = 0.80
203
+ else: risk_multiplier = 0.50
204
+
205
+ if self.market_trend == "BEARISH": risk_multiplier *= 0.5
206
+
207
+ # الحجم النهائي (لا يتجاوز Free Capital ولا 98% من الإجمالي)
208
+ final_size_usd = base_allocation * risk_multiplier
209
+ final_size_usd = min(final_size_usd, free_capital, current_cap * 0.98)
210
+
211
+ if final_size_usd < 5.0:
212
+ return False, {"reason": "Calculated size too small"}
213
+
214
+ # 7. Dynamic TP Selection
215
+ entry_price = float(signal_data.get('sniper_entry_price') or signal_data.get('current_price'))
216
+ tp_map = signal_data.get('tp_map', {})
217
+
218
+ # منطق الأهداف مع F&G
219
+ # إذا كان الخوف شديد (القاع) + إشارة قوية -> استهدف القمر (TP4)
220
+ if system_confidence >= 0.85 and (self.market_trend == "BULLISH" or self.fear_greed_index < 20):
221
+ selected_tp = tp_map.get('TP4') or tp_map.get('TP3')
222
+ target_label = "TP4 (Moonbag)"
223
+ elif system_confidence >= 0.75:
224
+ selected_tp = tp_map.get('TP3')
225
+ target_label = "TP3"
226
+ elif system_confidence >= 0.65:
227
+ selected_tp = tp_map.get('TP2')
228
+ target_label = "TP2"
229
+ else:
230
+ selected_tp = tp_map.get('TP1')
231
+ target_label = "TP1"
232
+
233
+ if not selected_tp or selected_tp <= entry_price: selected_tp = entry_price * 1.02
234
+
235
+ return True, {
236
+ "approved_size_usd": float(final_size_usd),
237
+ "approved_tp": float(selected_tp),
238
+ "target_label": target_label,
239
+ "system_confidence": system_confidence,
240
+ "risk_multiplier": risk_multiplier,
241
+ "market_mood": f"{self.market_trend} | FG:{self.fear_greed_index}"
242
+ }
243
+
244
+ # ==============================================================================
245
+ # 🔒 Capital Tracking (Atomic Operations)
246
+ # ==============================================================================
247
+ async def register_new_position(self, size_usd: float):
248
+ """يتم استدعاؤها عند فتح الصفقة لحجز رأس المال"""
249
+ async with self.capital_lock:
250
+ self.state["allocated_capital_usd"] = float(self.state.get("allocated_capital_usd", 0.0)) + float(size_usd)
251
+ # حماية من الأخطاء الحسابية (لا يتجاوز الإجمالي)
252
+ if self.state["allocated_capital_usd"] > self.state["current_capital"]:
253
+ self.state["allocated_capital_usd"] = self.state["current_capital"]
254
+ await self._save_state_to_r2()
255
+
256
+ async def register_closed_position(self, released_capital_usd: float, net_pnl: float, fees: float):
257
+ """يتم استدعاؤها عند الإغلاق لتحرير رأس المال وتحديث الربح"""
258
+ async with self.capital_lock:
259
+ # 1. تحرير رأس المال المحجوز
260
+ current_allocated = float(self.state.get("allocated_capital_usd", 0.0))
261
+ self.state["allocated_capital_usd"] = max(0.0, current_allocated - released_capital_usd)
262
+
263
+ # 2. تحديث الرصيد الإجمالي (الصافي بعد الرسوم)
264
+ net_impact = net_pnl - fees # نخصم الرسوم هنا لضمان الدقة
265
+ self.state["current_capital"] += net_impact
266
+ self.state["daily_net_pnl"] += net_impact
267
+
268
+ # 3. فحص الخسارة اليومية
269
+ start = self.state["session_start_balance"]
270
+ dd = (start - self.state["current_capital"]) / start if start > 0 else 0
271
+ if dd >= self.DAILY_LOSS_LIMIT_PCT:
272
+ self.state["is_trading_halted"] = True
273
+ self.state["halt_reason"] = "Daily Limit Hit After Exit"
274
+
275
+ await self._save_state_to_r2()
276
+ print(f"💰 [Portfolio] Capital Released: ${released_capital_usd:.2f} | Net Change: ${net_impact:+.2f}")
277
+
278
+ # ==============================================================================
279
+ # 💾 Utilities
280
+ # ==============================================================================
281
+ async def _check_daily_reset(self):
282
+ last_reset = datetime.fromisoformat(self.state.get("last_session_reset", datetime.now().isoformat()))
283
+ if datetime.now() - last_reset > timedelta(hours=24):
284
+ self.state["session_start_balance"] = self.state["current_capital"]
285
+ self.state["daily_net_pnl"] = 0.0
286
+ self.state["is_trading_halted"] = False
287
+ self.state["last_session_reset"] = datetime.now().isoformat()
288
+ await self._save_state_to_r2()
289
+
290
+ async def _sync_state_from_r2(self):
291
+ try:
292
+ data = await self.r2.get_file_async("smart_portfolio_state.json")
293
+ if data:
294
+ loaded = json.loads(data)
295
+ self.state.update(loaded)
296
+ # ضمان وجود المفاتيح الجديدة في حال الترقية من نسخة قديمة
297
+ if "allocated_capital_usd" not in self.state: self.state["allocated_capital_usd"] = 0.0
298
+ else:
299
+ old = await self.r2.get_portfolio_state_async()
300
+ if old:
301
+ self.state["current_capital"] = float(old.get("current_capital_usd", 10.0))
302
+ self.state["session_start_balance"] = self.state["current_capital"]
303
+ except: pass
304
+
305
+ async def _save_state_to_r2(self):
306
+ try:
307
+ await self.r2.upload_json_async(self.state, "smart_portfolio_state.json")
308
+ # تحديث الملف القديم للتوافق
309
+ existing = await self.r2.get_portfolio_state_async()
310
+ existing["current_capital_usd"] = self.state["current_capital"]
311
+ await self.r2.save_portfolio_state_async(existing)
312
+ except: pass