Riy777 commited on
Commit
86f8502
·
verified ·
1 Parent(s): 3f2975d

Update backtest_engine.py

Browse files
Files changed (1) hide show
  1. backtest_engine.py +116 -141
backtest_engine.py CHANGED
@@ -1,5 +1,5 @@
1
  # ============================================================
2
- # 🧪 backtest_engine.py (V67.0 - GEM-Architect: Portfolio Digital Twin)
3
  # ============================================================
4
 
5
  import asyncio
@@ -12,7 +12,6 @@ import os
12
  import concurrent.futures
13
  from typing import Dict, Any, List
14
 
15
- # استيراد الوحدات الأساسية
16
  try:
17
  from ml_engine.processor import MLProcessor, SystemLimits
18
  from ml_engine.data_manager import DataManager
@@ -28,13 +27,13 @@ class HeavyDutyBacktester:
28
  def __init__(self, data_manager, processor):
29
  self.dm = data_manager
30
  self.proc = processor
31
- self.GRID_DENSITY = 5 # تقليل الكثافة لأن المحاكاة أصبحت أثقل
32
- self.BACKTEST_DAYS = 7
33
 
34
- # 💰 إعدادات التوأم الرقمي (Portfolio Twin Settings)
35
  self.INITIAL_CAPITAL = 10.0
36
- self.TRADING_FEES = 0.001 # 0.1% Maker/Taker
37
- self.MAX_SLOTS = 4 # الحد الأقصى للصفقات المتزامنة (كما في النظام)
38
 
39
  self.TARGET_COINS = [
40
  'BTC/USDT', 'ETH/USDT', 'SOL/USDT', 'BNB/USDT', 'XRP/USDT',
@@ -42,43 +41,46 @@ class HeavyDutyBacktester:
42
  ]
43
 
44
  if not os.path.exists(CACHE_DIR): os.makedirs(CACHE_DIR)
45
- print(f"🧪 [Backtest V67.0] Portfolio Digital Twin Mode (Start: ${self.INITIAL_CAPITAL}).")
46
 
47
- # ... (نفس دوال المساعدة resample_data و df_to_list) ...
 
 
48
  def resample_data(self, df_1m, timeframe_str):
49
  if df_1m.empty: return pd.DataFrame()
50
  agg_dict = {'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last', 'volume': 'sum'}
51
  rule = timeframe_str.replace('m', 'T').replace('h', 'H').replace('d', 'D')
52
  try:
53
  resampled = df_1m.resample(rule).agg(agg_dict).dropna()
 
54
  resampled['timestamp'] = resampled.index.astype(np.int64) // 10**6
55
  return resampled
56
  except Exception: return pd.DataFrame()
57
 
58
  def df_to_list(self, df):
59
  if df.empty: return []
 
60
  return df[['timestamp', 'open', 'high', 'low', 'close', 'volume']].values.tolist()
61
 
62
  # ==============================================================
63
- # PHASE 1: Generate Truth Data (Standard)
64
  # ==============================================================
65
  async def generate_truth_data(self):
66
- # ... (نفس كود توليد البيانات السابق تماماً - لا تغيير هنا) ...
67
- # اختصاراً للمساحة سأضع الكود الأساسي فقط
68
- print(f"\n🚜 [Phase 1] Fetching & Structural Analysis ({self.BACKTEST_DAYS} Days)...")
69
  end_time_ms = int(time.time() * 1000)
70
  start_time_ms = end_time_ms - (self.BACKTEST_DAYS * 24 * 60 * 60 * 1000)
71
- test_regime = "RANGE"
72
 
73
  for sym in self.TARGET_COINS:
74
  safe_sym = sym.replace('/', '_')
75
- scores_file = f"{CACHE_DIR}/{safe_sym}_struct_scores_{self.BACKTEST_DAYS}d.pkl"
 
76
  if os.path.exists(scores_file):
77
  print(f" 📂 {sym} scores ready. Skipping.")
78
  continue
 
 
79
 
80
- print(f" ⚙️ Processing {sym}...", end="", flush=True)
81
- # ... (Fetching logic same as V66) ...
82
  all_candles_1m = []
83
  current_since = start_time_ms
84
  while current_since < end_time_ms:
@@ -97,92 +99,110 @@ class HeavyDutyBacktester:
97
  if not all_candles_1m:
98
  print(" No Data.")
99
  continue
100
-
101
  df_1m = pd.DataFrame(all_candles_1m, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
102
  df_1m['datetime'] = pd.to_datetime(df_1m['timestamp'], unit='ms')
103
  df_1m.set_index('datetime', inplace=True)
104
  df_1m = df_1m.sort_index()
105
 
106
  ai_results = []
 
107
  resample_freq = '15T'
108
  time_indices = df_1m.resample(resample_freq).last().dropna().index
109
 
110
- for t_idx in time_indices[100:]:
111
  current_slice_1m = df_1m.loc[:t_idx]
112
  if len(current_slice_1m) < 500: continue
113
  current_price = current_slice_1m['close'].iloc[-1]
114
 
115
- df_struct = self.resample_data(current_slice_1m.tail(5000), '15m')
116
- if len(df_struct) < 200: continue
117
-
118
- struct_score, _ = self.dm._calculate_structural_score(df_struct, sym, test_regime)
119
- norm_struct_score = max(0.0, min(1.0, (struct_score + 20) / 100.0))
120
 
121
- # ... (Titan simulation) ...
122
- titan_real = 0.5
123
 
124
- ai_results.append({
125
- 'timestamp': int(t_idx.timestamp() * 1000), # حفظ الوقت بدقة للترتيب الزمني
 
 
126
  'symbol': sym,
127
- 'close': current_price,
128
- 'real_titan': titan_real,
129
- 'real_struct_score': norm_struct_score
130
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
 
132
  if ai_results:
133
  pd.DataFrame(ai_results).to_pickle(scores_file)
134
- print(f" ✅ Saved.")
 
 
135
 
136
  # ==============================================================
137
  # PHASE 2: Portfolio Digital Twin Engine
138
  # ==============================================================
139
  @staticmethod
140
  def _worker_optimize(combinations_batch, scores_files, initial_capital, fees_pct, max_slots):
141
- """
142
- محاكاة دقيقة لإدارة المحفظة مع الزمن (Time-Series Portfolio Simulation).
143
- """
144
  results = []
145
 
146
- # 1. دمج جميع البيانات وترتيبها زمنياً (Time-Sorted Global Timeline)
147
- # لضمان محاكاة السوق بشكل واقعي (لا يمكننا معالجة كل عملة وحدها)
148
  all_data = []
149
  for fp in scores_files:
150
  try:
151
  df = pd.read_pickle(fp)
152
- all_data.append(df)
153
  except: pass
154
 
155
  if not all_data: return []
156
 
157
  global_df = pd.concat(all_data)
158
  global_df.sort_values('timestamp', inplace=True)
159
- # تجميع البيانات حسب الطابع الزمني (للمعالجة لحظة بلحظة)
160
  grouped_by_time = global_df.groupby('timestamp')
161
 
162
- # ----------------------------------------------------
163
-
164
  for config in combinations_batch:
165
- # 🏦 حالة المحفظة (Portfolio State)
166
  wallet = {
167
- "balance": initial_capital, # الرصيد الكلي
168
- "allocated": 0.0, # المحجوز في صفقات
169
- "positions": {}, # الصفقات المفتوحة: {symbol: {entry_p, size_usd}}
170
- "equity_curve": [initial_capital],
171
  "trades_history": []
172
  }
173
 
174
  w_titan = config['w_titan']
175
- w_struct = config['w_struct']
176
  entry_thresh = config['thresh']
177
 
178
- # ⏳ المرور عبر الزمن (Time-Step Simulation)
179
  for ts, group in grouped_by_time:
180
- # أ. إدارة الصفقات المفتوحة (Check Exits)
181
- # يجب استخدام نسخة من المفاتيح للتعديل أثناء الدوران
182
  active_symbols = list(wallet["positions"].keys())
183
-
184
- # نحتاج معرفة الأسعار الحالية للعملات المفتوحة
185
- # نستخرجها من المجموعة الحالية إذا توفرت
186
  current_prices = {row['symbol']: row['close'] for _, row in group.iterrows()}
187
 
188
  for sym in active_symbols:
@@ -190,125 +210,83 @@ class HeavyDutyBacktester:
190
  curr_p = current_prices[sym]
191
  pos = wallet["positions"][sym]
192
  entry_p = pos['entry_price']
193
-
194
  pct_change = (curr_p - entry_p) / entry_p
195
 
196
- # Exit Rules (TP 3% / SL 2%)
197
  if pct_change >= 0.03 or pct_change <= -0.02:
198
- # تنفيذ الخروج
199
  gross_pnl = pos['size_usd'] * pct_change
200
- fees = pos['size_usd'] * fees_pct * 2 # رسوم دخول وخروج تقريبية
201
  net_pnl = gross_pnl - fees
202
-
203
  wallet["allocated"] -= pos['size_usd']
204
- wallet["balance"] += net_pnl # الرصيد يتحدث بالصافي
205
-
206
  del wallet["positions"][sym]
207
-
208
- wallet["trades_history"].append({
209
- 'pnl': net_pnl,
210
- 'roi': (net_pnl / pos['size_usd']) * 100
211
- })
212
 
213
- # ب. البحث عن فرص جديدة (Check Entries)
214
- # فقط إذا كان لدينا خانات فارغة ورصيد كافٍ
215
  if len(wallet["positions"]) < max_slots:
216
  free_capital = wallet["balance"] - wallet["allocated"]
217
-
218
- # محاكاة منطق تقسيم رأس المال (Smart Portfolio Logic)
219
- # الحجم = الرصيد الحر / الخانات المتبقية (تقريبي)
220
  slots_left = max_slots - len(wallet["positions"])
221
- if slots_left > 0 and free_capital > 2.0: # الحد الأدنى للدخول
222
- # إذا كان الرصيد صغيراً جداً، ندخل بالكل المتاح (لأننا تحت 20$)
223
- if wallet["balance"] < 20.0:
224
- # إذا كان رصيد قليل، نأخذ ما يكفي لصفقة واحدة أو المتاح
225
- # لنقل نستخدم All-in للخانات المتبقية
226
- position_size = free_capital / slots_left
227
- else:
228
- # تقسيم متوازن
229
- position_size = wallet["balance"] / max_slots
230
- position_size = min(position_size, free_capital)
231
 
232
- # المرور على الفرص المتاحة في هذه اللحظة
233
  for _, row in group.iterrows():
234
  sym = row['symbol']
235
- if sym in wallet["positions"]: continue # لدينا صفقة بالفعل
 
 
 
 
 
236
 
237
- # حساب السكور
238
- real_titan = row.get('real_titan', 0.5)
239
- real_struct = row.get('real_struct_score', 0.0)
 
 
 
 
 
 
 
240
 
241
  score = 0.0
242
  if (w_titan + w_struct) > 0:
243
- score = ((real_titan * w_titan) + (real_struct * w_struct)) / (w_titan + w_struct)
244
 
245
  if score >= entry_thresh:
246
- # فتح صفقة جديدة (مع خصم رسوم الدخول من الحجم الفعلي)
247
- # نحجز المبلغ كاملاً، لكن الرسوم ستخصم عند الخروج لتبسيط المحاكاة
248
- wallet["positions"][sym] = {
249
- 'entry_price': row['close'],
250
- 'size_usd': position_size
251
- }
252
  wallet["allocated"] += position_size
253
-
254
- # نتوقف عن البحث في هذه اللحظة إذا امتلأت الخانات
255
  if len(wallet["positions"]) >= max_slots: break
 
 
256
 
257
- # تسجيل منحنى رأس المال (Equity Curve)
258
- # Equity = Balance (Unrealized PnL is ignored for speed, only Realized)
259
- # أو يمكن حساب Unrealized لدقة أكبر، لكن Realized يكفي للتحسين
260
- wallet["equity_curve"].append(wallet["balance"])
261
-
262
- # شرط الإفلاس
263
- if wallet["balance"] < 1.0 and len(wallet["positions"]) == 0:
264
- break
265
-
266
- # 3. تجميع النتائج
267
  trades = wallet["trades_history"]
268
  if trades:
269
  net_profit = wallet["balance"] - initial_capital
270
- wins = len([t for t in trades if t['pnl'] > 0])
271
- total = len(trades)
272
-
273
- # Max Drawdown
274
- peaks = pd.Series(wallet["equity_curve"]).cummax()
275
- drawdown = (wallet["equity_curve"] - peaks)
276
- max_dd = drawdown.min() # قيمة سالبة
277
-
278
  results.append({
279
  'config': config,
280
- 'net_profit': net_profit,
281
  'final_balance': wallet["balance"],
282
- 'total_trades': total,
283
- 'win_rate': (wins/total)*100,
284
- 'max_drawdown': max_dd,
285
- 'roi_pct': (net_profit / initial_capital) * 100
286
  })
287
  else:
288
- results.append({
289
- 'config': config,
290
- 'net_profit': 0.0,
291
- 'final_balance': initial_capital,
292
- 'total_trades': 0,
293
- 'win_rate': 0.0,
294
- 'max_drawdown': 0.0,
295
- 'roi_pct': 0.0
296
- })
297
 
298
  return results
299
 
300
  async def run_optimization(self):
301
  await self.generate_truth_data()
302
 
303
- score_files = [os.path.join(CACHE_DIR, f) for f in os.listdir(CACHE_DIR) if f.endswith(f'_scores_{self.BACKTEST_DAYS}d.pkl')]
304
  if not score_files:
305
- print("❌ No data found.")
306
  return None
307
 
308
- print(f"\n🧩 [Phase 2] Running Digital Twin Simulation...")
309
  print(f" 💰 Start Capital: ${self.INITIAL_CAPITAL}")
310
- print(f" 🎰 Max Slots: {self.MAX_SLOTS}")
311
- print(f" 💸 Fees: {self.TRADING_FEES*100}% per trade")
312
 
313
  w_titan_range = np.linspace(0.4, 0.9, num=self.GRID_DENSITY)
314
  w_struct_range = np.linspace(0.1, 0.6, num=self.GRID_DENSITY)
@@ -322,7 +300,7 @@ class HeavyDutyBacktester:
322
  print(f" 📊 Simulating {len(combinations):,} scenarios...")
323
 
324
  final_results = []
325
- batch_size = max(20, len(combinations) // (os.cpu_count() * 2)) # تقليل الدفعة لأن المحاكاة ثقيلة
326
  batches = [combinations[i:i+batch_size] for i in range(0, len(combinations), batch_size)]
327
 
328
  with concurrent.futures.ProcessPoolExecutor() as executor:
@@ -340,13 +318,10 @@ class HeavyDutyBacktester:
340
  best = sorted(final_results, key=lambda x: x['final_balance'], reverse=True)[0]
341
 
342
  print("\n" + "="*60)
343
- print(f"🏆 CHAMPION TWIN REPORT ({self.BACKTEST_DAYS} Days):")
344
- print(f" 💵 Initial: ${self.INITIAL_CAPITAL:,.2f}")
345
- print(f" 💰 Final: ${best['final_balance']:,.2f}")
346
- print(f" 🚀 Net PnL: ${best['net_profit']:,.2f} ({best['roi_pct']:,.2f}%)")
347
- print("-" * 60)
348
- print(f" 📊 Trades: {best['total_trades']} (WR: {best['win_rate']:.1f}%)")
349
- print(f" 📉 Max DD: ${best['max_drawdown']:,.2f}")
350
  print("-" * 60)
351
  print(f" ⚙️ Config: Titan={best['config']['w_titan']} | Struct={best['config']['w_struct']} | Thresh={best['config']['thresh']}")
352
  print("="*60)
@@ -354,7 +329,7 @@ class HeavyDutyBacktester:
354
  return best['config']
355
 
356
  async def run_strategic_optimization_task():
357
- print("\n🧪 [STRATEGIC BACKTEST] Starting Twin Optimization...")
358
  r2 = R2Service()
359
  dm = DataManager(None, None, r2)
360
  proc = MLProcessor(dm)
 
1
  # ============================================================
2
+ # 🧪 backtest_engine.py (V68.0 - GEM-Architect: Logic Tree Simulator)
3
  # ============================================================
4
 
5
  import asyncio
 
12
  import concurrent.futures
13
  from typing import Dict, Any, List
14
 
 
15
  try:
16
  from ml_engine.processor import MLProcessor, SystemLimits
17
  from ml_engine.data_manager import DataManager
 
27
  def __init__(self, data_manager, processor):
28
  self.dm = data_manager
29
  self.proc = processor
30
+ self.GRID_DENSITY = 5
31
+ self.BACKTEST_DAYS = 14
32
 
33
+ # 💰 إعدادات التوأم الرقمي
34
  self.INITIAL_CAPITAL = 10.0
35
+ self.TRADING_FEES = 0.001
36
+ self.MAX_SLOTS = 4
37
 
38
  self.TARGET_COINS = [
39
  'BTC/USDT', 'ETH/USDT', 'SOL/USDT', 'BNB/USDT', 'XRP/USDT',
 
41
  ]
42
 
43
  if not os.path.exists(CACHE_DIR): os.makedirs(CACHE_DIR)
44
+ print(f"🧪 [Backtest V68.0] Logic Tree Simulator (Anti-FOMO).")
45
 
46
+ # ==============================================================
47
+ # 🛠️ Helpers (Resampling Exact Match)
48
+ # ==============================================================
49
  def resample_data(self, df_1m, timeframe_str):
50
  if df_1m.empty: return pd.DataFrame()
51
  agg_dict = {'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last', 'volume': 'sum'}
52
  rule = timeframe_str.replace('m', 'T').replace('h', 'H').replace('d', 'D')
53
  try:
54
  resampled = df_1m.resample(rule).agg(agg_dict).dropna()
55
+ # التأكد من إعادة التسمية الصحيحة للأعمدة
56
  resampled['timestamp'] = resampled.index.astype(np.int64) // 10**6
57
  return resampled
58
  except Exception: return pd.DataFrame()
59
 
60
  def df_to_list(self, df):
61
  if df.empty: return []
62
+ # ترتيب الأعمدة كما يتوقعها DataManager القديم والجديد
63
  return df[['timestamp', 'open', 'high', 'low', 'close', 'volume']].values.tolist()
64
 
65
  # ==============================================================
66
+ # PHASE 1: Generate Truth Data (Strict Logic Tree)
67
  # ==============================================================
68
  async def generate_truth_data(self):
69
+ print(f"\n🚜 [Phase 1] Replicating V45.0 Logic Tree ({self.BACKTEST_DAYS} Days)...")
 
 
70
  end_time_ms = int(time.time() * 1000)
71
  start_time_ms = end_time_ms - (self.BACKTEST_DAYS * 24 * 60 * 60 * 1000)
 
72
 
73
  for sym in self.TARGET_COINS:
74
  safe_sym = sym.replace('/', '_')
75
+ scores_file = f"{CACHE_DIR}/{safe_sym}_logictree_scores.pkl"
76
+
77
  if os.path.exists(scores_file):
78
  print(f" 📂 {sym} scores ready. Skipping.")
79
  continue
80
+
81
+ print(f" ⚙️ Simulating {sym}...", end="", flush=True)
82
 
83
+ # 1. جلب بيانات الدقيقة (الخام)
 
84
  all_candles_1m = []
85
  current_since = start_time_ms
86
  while current_since < end_time_ms:
 
99
  if not all_candles_1m:
100
  print(" No Data.")
101
  continue
102
+
103
  df_1m = pd.DataFrame(all_candles_1m, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
104
  df_1m['datetime'] = pd.to_datetime(df_1m['timestamp'], unit='ms')
105
  df_1m.set_index('datetime', inplace=True)
106
  df_1m = df_1m.sort_index()
107
 
108
  ai_results = []
109
+ # نفحص كل 15 دقيقة (لأن الفلتر يعمل على إطار الربع ساعة والساعة)
110
  resample_freq = '15T'
111
  time_indices = df_1m.resample(resample_freq).last().dropna().index
112
 
113
+ for t_idx in time_indices[200:]: # نبدأ بعد فترة لضمان وجود بيانات كافية للمؤشرات
114
  current_slice_1m = df_1m.loc[:t_idx]
115
  if len(current_slice_1m) < 500: continue
116
  current_price = current_slice_1m['close'].iloc[-1]
117
 
118
+ # 🔥 المحاكاة الدقيقة: بناء المدخلات كما يطلبها _apply_logic_tree
119
+ # نحتاج شموع 1 ساعة و 15 دقيقة
120
+ # نأخذ بيانات كافية (آخر 100 شمعة لكل إطار)
 
 
121
 
122
+ df_1h = self.resample_data(current_slice_1m.tail(6000), '1h')
123
+ df_15m = self.resample_data(current_slice_1m.tail(1500), '15m')
124
 
125
+ if len(df_1h) < 60 or len(df_15m) < 60: continue
126
+
127
+ # تحويل البيانات إلى القوائم التي يتوقعها DataManager V45
128
+ simulated_data_packet = {
129
  'symbol': sym,
130
+ 'ohlcv_1h': self.df_to_list(df_1h.tail(60)),
131
+ 'ohlcv_15m': self.df_to_list(df_15m.tail(60)),
132
+ 'change_24h': 0.0 # (يمكن حسابه بدقة أكبر إذا لزم الأمر، لكن الفلتر يعتمد على 4H أكثر)
133
+ }
134
+
135
+ # حساب نسبة التغير اليومي للمحاكاة (للصرامة)
136
+ try:
137
+ price_24h_ago = df_1h.iloc[-24]['close'] if len(df_1h) >= 24 else df_1h.iloc[0]['close']
138
+ simulated_data_packet['change_24h'] = ((current_price - price_24h_ago) / price_24h_ago) * 100
139
+ except: pass
140
+
141
+ # 🔥 استدعاء المنطق الصارم مباشرة
142
+ # هذا يضمن أن الباكتست يرى بالضبط ما يراه النظام الحي (Breakout/Reversal/None)
143
+ logic_result = self.dm._apply_logic_tree(simulated_data_packet)
144
+
145
+ signal_type = logic_result.get('type', 'NONE')
146
+ l1_score = logic_result.get('score', 0.0)
147
+
148
+ # تخزين النتيجة فقط إذا كان هناك إشارة (لتوفير الذاكرة وتسريع المعالجة)
149
+ # أو تخزين الكل إذا أردنا تدريب Titan على الرفض
150
+
151
+ # محاكاة Titan (اختياري، للسرعة نضع قيمة افتراضية أو نستدعي النموذج)
152
+ titan_real = 0.5
153
+
154
+ # إذا كانت الإشارة مقبولة، نحفظها
155
+ if signal_type in ['BREAKOUT', 'REVERSAL']:
156
+ ai_results.append({
157
+ 'timestamp': int(t_idx.timestamp() * 1000),
158
+ 'symbol': sym,
159
+ 'close': current_price,
160
+ 'real_titan': titan_real,
161
+ 'signal_type': signal_type, # ✅ نوع الإشارة (مهم جداً)
162
+ 'l1_score': l1_score # ✅ قوة الإشارة
163
+ })
164
 
165
  if ai_results:
166
  pd.DataFrame(ai_results).to_pickle(scores_file)
167
+ print(f" ✅ Saved ({len(ai_results)} signals).")
168
+ else:
169
+ print(" ⚠️ No strict signals found (Expected behavior for Anti-FOMO).")
170
 
171
  # ==============================================================
172
  # PHASE 2: Portfolio Digital Twin Engine
173
  # ==============================================================
174
  @staticmethod
175
  def _worker_optimize(combinations_batch, scores_files, initial_capital, fees_pct, max_slots):
 
 
 
176
  results = []
177
 
 
 
178
  all_data = []
179
  for fp in scores_files:
180
  try:
181
  df = pd.read_pickle(fp)
182
+ if not df.empty: all_data.append(df)
183
  except: pass
184
 
185
  if not all_data: return []
186
 
187
  global_df = pd.concat(all_data)
188
  global_df.sort_values('timestamp', inplace=True)
 
189
  grouped_by_time = global_df.groupby('timestamp')
190
 
 
 
191
  for config in combinations_batch:
 
192
  wallet = {
193
+ "balance": initial_capital,
194
+ "allocated": 0.0,
195
+ "positions": {},
 
196
  "trades_history": []
197
  }
198
 
199
  w_titan = config['w_titan']
200
+ w_struct = config['w_struct'] # هذا سيكون وزناً لنوع الإشارة وقوتها
201
  entry_thresh = config['thresh']
202
 
 
203
  for ts, group in grouped_by_time:
204
+ # 1. Exit Logic
 
205
  active_symbols = list(wallet["positions"].keys())
 
 
 
206
  current_prices = {row['symbol']: row['close'] for _, row in group.iterrows()}
207
 
208
  for sym in active_symbols:
 
210
  curr_p = current_prices[sym]
211
  pos = wallet["positions"][sym]
212
  entry_p = pos['entry_price']
 
213
  pct_change = (curr_p - entry_p) / entry_p
214
 
 
215
  if pct_change >= 0.03 or pct_change <= -0.02:
 
216
  gross_pnl = pos['size_usd'] * pct_change
217
+ fees = pos['size_usd'] * fees_pct * 2
218
  net_pnl = gross_pnl - fees
 
219
  wallet["allocated"] -= pos['size_usd']
220
+ wallet["balance"] += net_pnl
 
221
  del wallet["positions"][sym]
222
+ wallet["trades_history"].append({'pnl': net_pnl})
 
 
 
 
223
 
224
+ # 2. Entry Logic (Strict)
 
225
  if len(wallet["positions"]) < max_slots:
226
  free_capital = wallet["balance"] - wallet["allocated"]
 
 
 
227
  slots_left = max_slots - len(wallet["positions"])
228
+
229
+ if slots_left > 0 and free_capital > 2.0:
230
+ position_size = wallet["balance"] / max_slots
231
+ if wallet["balance"] < 20.0: position_size = free_capital / slots_left
232
+ position_size = min(position_size, free_capital)
 
 
 
 
 
233
 
 
234
  for _, row in group.iterrows():
235
  sym = row['symbol']
236
+ if sym in wallet["positions"]: continue
237
+
238
+ # 🔥 استخدام البيانات الجديدة
239
+ sig_type = row['signal_type'] # BREAKOUT or REVERSAL
240
+ l1_raw_score = row['l1_score'] # Score from Logic Tree
241
+ real_titan = row['real_titan']
242
 
243
+ # تطبيع سكور الـ L1 ليكون متوافقاً مع المعادلة (0-1)
244
+ # Breakout score عادة يكون صغيراً (1.5 - 5.0 ratio)
245
+ # Reversal score (0-100)
246
+
247
+ norm_struct = 0.0
248
+ if sig_type == 'BREAKOUT':
249
+ # كلما زاد الفوليوم كان أفضل، لنقل 5.0 هو الحد الأقصى للطبيعي
250
+ norm_struct = min(1.0, l1_raw_score / 5.0)
251
+ elif sig_type == 'REVERSAL':
252
+ norm_struct = l1_raw_score / 100.0
253
 
254
  score = 0.0
255
  if (w_titan + w_struct) > 0:
256
+ score = ((real_titan * w_titan) + (norm_struct * w_struct)) / (w_titan + w_struct)
257
 
258
  if score >= entry_thresh:
259
+ wallet["positions"][sym] = {'entry_price': row['close'], 'size_usd': position_size}
 
 
 
 
 
260
  wallet["allocated"] += position_size
 
 
261
  if len(wallet["positions"]) >= max_slots: break
262
+
263
+ if wallet["balance"] < 1.0 and len(wallet["positions"]) == 0: break
264
 
265
+ # Statistics
 
 
 
 
 
 
 
 
 
266
  trades = wallet["trades_history"]
267
  if trades:
268
  net_profit = wallet["balance"] - initial_capital
 
 
 
 
 
 
 
 
269
  results.append({
270
  'config': config,
 
271
  'final_balance': wallet["balance"],
272
+ 'net_profit': net_profit,
273
+ 'total_trades': len(trades)
 
 
274
  })
275
  else:
276
+ results.append({'config': config, 'final_balance': initial_capital, 'net_profit': 0.0, 'total_trades': 0})
 
 
 
 
 
 
 
 
277
 
278
  return results
279
 
280
  async def run_optimization(self):
281
  await self.generate_truth_data()
282
 
283
+ score_files = [os.path.join(CACHE_DIR, f) for f in os.listdir(CACHE_DIR) if f.endswith(f'_logictree_scores.pkl')]
284
  if not score_files:
285
+ print("❌ No Strict Logic signals found. Try changing coins or extending days.")
286
  return None
287
 
288
+ print(f"\n🧩 [Phase 2] Running Strict Logic Simulation...")
289
  print(f" 💰 Start Capital: ${self.INITIAL_CAPITAL}")
 
 
290
 
291
  w_titan_range = np.linspace(0.4, 0.9, num=self.GRID_DENSITY)
292
  w_struct_range = np.linspace(0.1, 0.6, num=self.GRID_DENSITY)
 
300
  print(f" 📊 Simulating {len(combinations):,} scenarios...")
301
 
302
  final_results = []
303
+ batch_size = max(20, len(combinations) // (os.cpu_count() * 2))
304
  batches = [combinations[i:i+batch_size] for i in range(0, len(combinations), batch_size)]
305
 
306
  with concurrent.futures.ProcessPoolExecutor() as executor:
 
318
  best = sorted(final_results, key=lambda x: x['final_balance'], reverse=True)[0]
319
 
320
  print("\n" + "="*60)
321
+ print(f"🏆 CHAMPION STRICT REPORT ({self.BACKTEST_DAYS} Days):")
322
+ print(f" 💰 Final Balance: ${best['final_balance']:,.2f}")
323
+ print(f" 🚀 Net PnL: ${best['net_profit']:,.2f}")
324
+ print(f" 📊 Trades: {best['total_trades']}")
 
 
 
325
  print("-" * 60)
326
  print(f" ⚙️ Config: Titan={best['config']['w_titan']} | Struct={best['config']['w_struct']} | Thresh={best['config']['thresh']}")
327
  print("="*60)
 
329
  return best['config']
330
 
331
  async def run_strategic_optimization_task():
332
+ print("\n🧪 [STRATEGIC BACKTEST] Starting Logic Tree Optimization...")
333
  r2 = R2Service()
334
  dm = DataManager(None, None, r2)
335
  proc = MLProcessor(dm)