Spaces:
Paused
Paused
Update backtest_engine.py
Browse files- backtest_engine.py +61 -94
backtest_engine.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
# ============================================================
|
| 2 |
-
# ๐งช backtest_engine.py (V132.
|
| 3 |
# ============================================================
|
| 4 |
|
| 5 |
import asyncio
|
|
@@ -103,102 +103,68 @@ def _transform_window_for_pattern(df_window):
|
|
| 103 |
except: return None
|
| 104 |
|
| 105 |
def calculate_sniper_features_exact(df):
|
| 106 |
-
"""
|
| 107 |
-
Sniper Features Calculation - TRUE UNIVERSAL MODE (Z-SCORE).
|
| 108 |
-
Converts structural features to Z-Scores to bypass scale issues.
|
| 109 |
-
"""
|
| 110 |
-
# 1. Standard Features
|
| 111 |
d = df.copy()
|
| 112 |
c = d['close']; h = d['high']; l = d['low']; v = d['volume']; o = d['open']
|
| 113 |
|
| 114 |
-
|
| 115 |
-
def _z_roll(x, w=200): # Window 200 is standard for regime detection
|
| 116 |
r = x.rolling(w).mean()
|
| 117 |
s = x.rolling(w).std().replace(0, np.nan)
|
| 118 |
return ((x - r) / s).fillna(0)
|
| 119 |
|
| 120 |
-
# Basic Returns (Keep as percentages, trees handle these well)
|
| 121 |
d['return_1m'] = c.pct_change(1).fillna(0)
|
| 122 |
d['return_3m'] = c.pct_change(3).fillna(0)
|
| 123 |
d['return_5m'] = c.pct_change(5).fillna(0)
|
| 124 |
d['return_15m'] = c.pct_change(15).fillna(0)
|
| 125 |
|
| 126 |
-
# Technicals (RSI is bounded 0-100, usually safe)
|
| 127 |
d['rsi_14'] = ta.rsi(c, length=14).fillna(50)
|
| 128 |
-
|
| 129 |
ema_9 = ta.ema(c, length=9).fillna(c)
|
| 130 |
ema_21 = ta.ema(c, length=21).fillna(c)
|
| 131 |
-
|
| 132 |
-
# Slopes/Distances -> Normalized
|
| 133 |
d['ema_9_slope'] = ((ema_9 - ema_9.shift(1)) / ema_9.shift(1).replace(0, np.nan)).fillna(0)
|
| 134 |
d['ema_21_dist'] = ((c - ema_21) / ema_21.replace(0, np.nan)).fillna(0)
|
| 135 |
|
| 136 |
-
# --- TRANSFORM 1: ATR (Vol) -> Z-Score ---
|
| 137 |
-
# Instead of raw value or %, check if Volatility is spiking relative to history
|
| 138 |
atr_raw = ta.atr(h, l, c, length=100).fillna(0)
|
| 139 |
d['atr'] = _z_roll(atr_raw, 500)
|
| 140 |
-
|
| 141 |
-
# Volume Z-Score
|
| 142 |
d['vol_zscore_50'] = _z_roll(v, 50)
|
| 143 |
-
|
| 144 |
-
# Candle Geometry
|
| 145 |
rng = (h - l).replace(0, 1e-9)
|
| 146 |
-
d['candle_range'] = _z_roll(rng, 500)
|
| 147 |
d['close_pos_in_range'] = ((c - l) / rng).fillna(0.5)
|
| 148 |
|
| 149 |
-
# --- TRANSFORM 2: Liquidity Proxies -> Z-Score ---
|
| 150 |
-
# This fixes the Amihud 1e-8 issue completely.
|
| 151 |
-
|
| 152 |
-
# Amihud
|
| 153 |
d['dollar_vol'] = c * v
|
| 154 |
amihud_raw = (d['return_1m'].abs() / d['dollar_vol'].replace(0, np.nan)).fillna(0)
|
| 155 |
d['amihud'] = _z_roll(amihud_raw, 500)
|
| 156 |
|
| 157 |
-
# Roll Spread
|
| 158 |
dp = c.diff()
|
| 159 |
roll_cov = dp.rolling(64).cov(dp.shift(1)).fillna(0)
|
| 160 |
roll_spread_raw = (2 * np.sqrt(np.maximum(0, -roll_cov)))
|
| 161 |
d['roll_spread'] = _z_roll(roll_spread_raw, 500)
|
| 162 |
|
| 163 |
-
# OFI (Order Flow)
|
| 164 |
sign = np.sign(c.diff()).fillna(0)
|
| 165 |
d['signed_vol'] = sign * v
|
| 166 |
ofi_raw = d['signed_vol'].rolling(30).sum().fillna(0)
|
| 167 |
d['ofi'] = _z_roll(ofi_raw, 500)
|
| 168 |
|
| 169 |
-
# VPIN
|
| 170 |
buy_vol = (sign > 0) * v
|
| 171 |
sell_vol = (sign < 0) * v
|
| 172 |
imb = (buy_vol.rolling(60).sum() - sell_vol.rolling(60).sum()).abs()
|
| 173 |
tot = v.rolling(60).sum().replace(0, np.nan)
|
| 174 |
-
d['vpin'] = (imb / tot).fillna(0)
|
| 175 |
|
| 176 |
-
# Garman-Klass Volatility
|
| 177 |
rv_gk_raw = ((np.log(h / l)**2) / 2) - ((2 * np.log(2) - 1) * (np.log(c / o)**2))
|
| 178 |
d['rv_gk'] = _z_roll(rv_gk_raw.fillna(0), 500)
|
| 179 |
|
| 180 |
-
# VWAP Deviation
|
| 181 |
vwap_win = 20
|
| 182 |
vwap = (d['dollar_vol'].rolling(vwap_win).sum() / v.rolling(vwap_win).sum().replace(0, np.nan)).fillna(c)
|
| 183 |
d['vwap_dev'] = _z_roll((c - vwap), 500)
|
| 184 |
|
| 185 |
-
# Liquidity Score (Composite - Already using internal Z-scores in logic, but let's re-calc)
|
| 186 |
-
# Note: We use the already Z-scored columns now where possible
|
| 187 |
d['L_score'] = (
|
| 188 |
-
d['vol_zscore_50'] +
|
| 189 |
-
(-d['
|
| 190 |
-
(-d['roll_spread']) +
|
| 191 |
-
(-d['rv_gk'].abs()) +
|
| 192 |
-
(-d['vwap_dev'].abs()) +
|
| 193 |
-
d['ofi']
|
| 194 |
).fillna(0)
|
| 195 |
|
| 196 |
return sanitize_features(d)
|
| 197 |
|
| 198 |
def calculate_titan_features_real(df):
|
| 199 |
-
"""Titan features with strict Infinity handling."""
|
| 200 |
df = df.copy()
|
| 201 |
-
|
| 202 |
df['RSI'] = ta.rsi(df['close'], 14)
|
| 203 |
macd = ta.macd(df['close'])
|
| 204 |
if macd is not None:
|
|
@@ -269,15 +235,12 @@ def calculate_legacy_v2_vectorized(df_1m, df_5m, df_15m):
|
|
| 269 |
X_df = pd.concat(parts, axis=1)
|
| 270 |
return sanitize_features(X_df).values
|
| 271 |
except Exception as e:
|
| 272 |
-
print(f"Legacy V2 Vec Error: {e}")
|
| 273 |
return None
|
| 274 |
|
| 275 |
def calculate_legacy_v3_vectorized(df_1m, df_5m, df_15m):
|
| 276 |
-
"""Legacy V3 Safe Calc."""
|
| 277 |
try:
|
| 278 |
def calc_v3_base(df, prefix=""):
|
| 279 |
d = df.copy()
|
| 280 |
-
# Initialize All
|
| 281 |
targets = ['rsi', 'rsi_slope', 'macd_h', 'macd_h_slope', 'adx', 'dmp', 'dmn',
|
| 282 |
'trend_net_force', 'ema_20', 'ema_50', 'ema_200', 'dist_ema20',
|
| 283 |
'dist_ema50', 'dist_ema200', 'slope_ema50', 'atr', 'atr_rel',
|
|
@@ -398,7 +361,6 @@ class HeavyDutyBacktester:
|
|
| 398 |
return raw
|
| 399 |
return model.predict(X)
|
| 400 |
except Exception as e:
|
| 401 |
-
# print(f"โ ๏ธ {model_name} Error: {e}")
|
| 402 |
return np.zeros(len(X) if hasattr(X, '__len__') else 0)
|
| 403 |
|
| 404 |
def _extract_probs(self, raw_preds):
|
|
@@ -450,10 +412,8 @@ class HeavyDutyBacktester:
|
|
| 450 |
df_5m = df_1m.resample('5T').agg({'open':'first', 'high':'max', 'low':'min', 'close':'last', 'volume':'sum'}).dropna()
|
| 451 |
df_15m = df_1m.resample('15T').agg({'open':'first', 'high':'max', 'low':'min', 'close':'last', 'volume':'sum'}).dropna()
|
| 452 |
|
| 453 |
-
# 1. Sniper
|
| 454 |
df_sniper_feats = calculate_sniper_features_exact(df_1m)
|
| 455 |
-
|
| 456 |
-
# Calculate L1 Score manually (since new Sniper function returns pure features)
|
| 457 |
rel_vol = df_1m['volume'] / (df_1m['volume'].rolling(50).mean() + 1e-9)
|
| 458 |
l1_score = (rel_vol * 10) + ((ta.atr(df_1m['high'], df_1m['low'], df_1m['close'], 14)/df_1m['close']) * 1000)
|
| 459 |
|
|
@@ -497,72 +457,36 @@ class HeavyDutyBacktester:
|
|
| 497 |
res_titan = self.proc.titan.model.predict(xgb.DMatrix(X_titan_df.values, feature_names=feats))
|
| 498 |
except Exception as e: print(f"Titan Error: {e}")
|
| 499 |
|
| 500 |
-
# 4. Sniper
|
| 501 |
res_sniper = np.full(len(df_candidates), 0.5)
|
| 502 |
sniper_instance = self.proc.sniper
|
| 503 |
-
|
| 504 |
if sniper_instance and getattr(sniper_instance, 'models', []):
|
| 505 |
try:
|
| 506 |
-
# A. Identify Required Features
|
| 507 |
required_features = getattr(sniper_instance, 'feature_names', [])
|
| 508 |
if not required_features and hasattr(sniper_instance.models[0], 'feature_name'):
|
| 509 |
required_features = sniper_instance.models[0].feature_name()
|
| 510 |
|
| 511 |
-
# B. Build Maps
|
| 512 |
source_cols = df_sniper_feats.columns
|
| 513 |
def normalize_name(s): return s.lower().replace('_', '').replace('-', '').replace(' ', '')
|
| 514 |
col_map_lower = {col.lower(): col for col in source_cols}
|
| 515 |
col_map_fuzzy = {normalize_name(col): col for col in source_cols}
|
| 516 |
|
| 517 |
-
# C. Construct X_final
|
| 518 |
X_final = pd.DataFrame(index=df_candidates.index)
|
| 519 |
-
missing_feats = []
|
| 520 |
-
found_feats = []
|
| 521 |
-
|
| 522 |
if required_features:
|
| 523 |
for req_feat in required_features:
|
| 524 |
req_norm = normalize_name(req_feat)
|
| 525 |
-
if req_feat in source_cols:
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
X_final[req_feat] = df_sniper_feats.loc[df_candidates.index, real_col]
|
| 531 |
-
found_feats.append(f"{req_feat}->{real_col}")
|
| 532 |
-
elif req_norm in col_map_fuzzy:
|
| 533 |
-
real_col = col_map_fuzzy[req_norm]
|
| 534 |
-
X_final[req_feat] = df_sniper_feats.loc[df_candidates.index, real_col]
|
| 535 |
-
found_feats.append(f"{req_feat}~>{real_col}")
|
| 536 |
-
else:
|
| 537 |
-
missing_feats.append(req_feat)
|
| 538 |
-
X_final[req_feat] = 0.0
|
| 539 |
-
else:
|
| 540 |
-
X_final = df_sniper_feats.loc[df_candidates.index]
|
| 541 |
-
|
| 542 |
-
# D. Deep Diagnostics
|
| 543 |
-
print(f" ๐ต๏ธ [Sniper Diag] Expected {len(required_features)} | Found {len(required_features) - len(missing_feats)} | Missing {len(missing_feats)}")
|
| 544 |
-
if missing_feats:
|
| 545 |
-
print(f" โ MISSING: {missing_feats}")
|
| 546 |
-
if found_feats:
|
| 547 |
-
print(f" โ
MAPPED (First 5): {found_feats[:5]}")
|
| 548 |
-
|
| 549 |
-
# E. Data Stats Check
|
| 550 |
-
feat_stats = X_final.describe().T[['mean', 'min', 'max']]
|
| 551 |
-
print(f" ๐ [Sniper Input Stats] (First 3 Feats):\n{feat_stats.head(3)}")
|
| 552 |
|
| 553 |
-
# F. Predict
|
| 554 |
preds = []
|
| 555 |
for m in sniper_instance.models:
|
| 556 |
X_in = X_final.astype(np.float32)
|
| 557 |
raw_p = self._extract_probs(self._smart_predict(m, X_in, "Sniper"))
|
| 558 |
preds.append(raw_p)
|
| 559 |
-
|
| 560 |
res_sniper = np.mean(preds, axis=0)
|
| 561 |
-
|
| 562 |
-
p_min, p_max, p_mean = res_sniper.min(), res_sniper.max(), res_sniper.mean()
|
| 563 |
-
n_high = np.sum(res_sniper > 0.6)
|
| 564 |
-
print(f" ๐ฏ [Sniper Output] Min:{p_min:.4f} Max:{p_max:.4f} Mean:{p_mean:.4f} | Signals(>0.6): {n_high}")
|
| 565 |
-
|
| 566 |
except Exception as e:
|
| 567 |
print(f"โ Sniper Inference Error: {e}")
|
| 568 |
traceback.print_exc()
|
|
@@ -585,10 +509,10 @@ class HeavyDutyBacktester:
|
|
| 585 |
hydra_models = getattr(self.proc.guardian_hydra, 'models', {})
|
| 586 |
if hydra_models and 'crash' in hydra_models:
|
| 587 |
try:
|
| 588 |
-
df_sniper_base = df_1m.copy()
|
| 589 |
df_sniper_base['rsi_14'] = ta.rsi(df_sniper_base['close'], 14).fillna(50)
|
| 590 |
df_sniper_base['atr'] = ta.atr(df_sniper_base['high'], df_sniper_base['low'], df_sniper_base['close'], 14).fillna(0)
|
| 591 |
-
df_sniper_base['rel_vol'] = rel_vol
|
| 592 |
|
| 593 |
global_hydra_feats = np.column_stack([
|
| 594 |
df_sniper_base['rsi_14'], df_sniper_base['rsi_14'], df_sniper_base['rsi_14'],
|
|
@@ -728,10 +652,33 @@ class HeavyDutyBacktester:
|
|
| 728 |
|
| 729 |
final_bal = bal + alloc
|
| 730 |
profit = final_bal - initial_capital
|
|
|
|
|
|
|
| 731 |
tot = len(log)
|
| 732 |
-
|
| 733 |
-
|
| 734 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 735 |
cons_trades = [x for x in log if x['consensus']]
|
| 736 |
n_cons = len(cons_trades)
|
| 737 |
agree_rate = (n_cons/tot*100) if tot else 0
|
|
@@ -741,6 +688,10 @@ class HeavyDutyBacktester:
|
|
| 741 |
res.append({
|
| 742 |
'config': cfg, 'final_balance': final_bal, 'net_profit': profit,
|
| 743 |
'total_trades': tot, 'win_rate': win_rate, 'max_drawdown': 0,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 744 |
'consensus_agreement_rate': agree_rate,
|
| 745 |
'high_consensus_win_rate': cons_win_rate,
|
| 746 |
'high_consensus_avg_pnl': cons_avg_pnl
|
|
@@ -766,6 +717,18 @@ class HeavyDutyBacktester:
|
|
| 766 |
results_list.sort(key=lambda x: x['net_profit'], reverse=True)
|
| 767 |
best = results_list[0]
|
| 768 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 769 |
print("\n" + "="*60)
|
| 770 |
print(f"๐ CHAMPION REPORT [{target_regime}]:")
|
| 771 |
print(f" ๐ฐ Final Balance: ${best['final_balance']:,.2f}")
|
|
@@ -773,12 +736,16 @@ class HeavyDutyBacktester:
|
|
| 773 |
print("-" * 60)
|
| 774 |
print(f" ๐ Total Trades: {best['total_trades']}")
|
| 775 |
print(f" ๐ Win Rate: {best['win_rate']:.1f}%")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 776 |
print("-" * 60)
|
| 777 |
print(f" ๐ง CONSENSUS ANALYTICS:")
|
| 778 |
print(f" ๐ค Model Agreement Rate: {best['consensus_agreement_rate']:.1f}%")
|
| 779 |
print(f" ๐ High-Consensus Win Rate: {best['high_consensus_win_rate']:.1f}%")
|
| 780 |
-
print(f" ๐ High-Consensus Avg PnL: {best['high_consensus_avg_pnl']:.2f}%")
|
| 781 |
print("-" * 60)
|
|
|
|
| 782 |
print(f" โ๏ธ Oracle={best['config']['oracle_thresh']:.2f} | Sniper={best['config']['sniper_thresh']:.2f} | Hydra={best['config']['hydra_thresh']:.2f}")
|
| 783 |
print("="*60)
|
| 784 |
return best['config'], best
|
|
|
|
| 1 |
# ============================================================
|
| 2 |
+
# ๐งช backtest_engine.py (V132.5 - GEM-Architect: Full Diagnostics)
|
| 3 |
# ============================================================
|
| 4 |
|
| 5 |
import asyncio
|
|
|
|
| 103 |
except: return None
|
| 104 |
|
| 105 |
def calculate_sniper_features_exact(df):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
d = df.copy()
|
| 107 |
c = d['close']; h = d['high']; l = d['low']; v = d['volume']; o = d['open']
|
| 108 |
|
| 109 |
+
def _z_roll(x, w=200):
|
|
|
|
| 110 |
r = x.rolling(w).mean()
|
| 111 |
s = x.rolling(w).std().replace(0, np.nan)
|
| 112 |
return ((x - r) / s).fillna(0)
|
| 113 |
|
|
|
|
| 114 |
d['return_1m'] = c.pct_change(1).fillna(0)
|
| 115 |
d['return_3m'] = c.pct_change(3).fillna(0)
|
| 116 |
d['return_5m'] = c.pct_change(5).fillna(0)
|
| 117 |
d['return_15m'] = c.pct_change(15).fillna(0)
|
| 118 |
|
|
|
|
| 119 |
d['rsi_14'] = ta.rsi(c, length=14).fillna(50)
|
|
|
|
| 120 |
ema_9 = ta.ema(c, length=9).fillna(c)
|
| 121 |
ema_21 = ta.ema(c, length=21).fillna(c)
|
|
|
|
|
|
|
| 122 |
d['ema_9_slope'] = ((ema_9 - ema_9.shift(1)) / ema_9.shift(1).replace(0, np.nan)).fillna(0)
|
| 123 |
d['ema_21_dist'] = ((c - ema_21) / ema_21.replace(0, np.nan)).fillna(0)
|
| 124 |
|
|
|
|
|
|
|
| 125 |
atr_raw = ta.atr(h, l, c, length=100).fillna(0)
|
| 126 |
d['atr'] = _z_roll(atr_raw, 500)
|
|
|
|
|
|
|
| 127 |
d['vol_zscore_50'] = _z_roll(v, 50)
|
|
|
|
|
|
|
| 128 |
rng = (h - l).replace(0, 1e-9)
|
| 129 |
+
d['candle_range'] = _z_roll(rng, 500)
|
| 130 |
d['close_pos_in_range'] = ((c - l) / rng).fillna(0.5)
|
| 131 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
d['dollar_vol'] = c * v
|
| 133 |
amihud_raw = (d['return_1m'].abs() / d['dollar_vol'].replace(0, np.nan)).fillna(0)
|
| 134 |
d['amihud'] = _z_roll(amihud_raw, 500)
|
| 135 |
|
|
|
|
| 136 |
dp = c.diff()
|
| 137 |
roll_cov = dp.rolling(64).cov(dp.shift(1)).fillna(0)
|
| 138 |
roll_spread_raw = (2 * np.sqrt(np.maximum(0, -roll_cov)))
|
| 139 |
d['roll_spread'] = _z_roll(roll_spread_raw, 500)
|
| 140 |
|
|
|
|
| 141 |
sign = np.sign(c.diff()).fillna(0)
|
| 142 |
d['signed_vol'] = sign * v
|
| 143 |
ofi_raw = d['signed_vol'].rolling(30).sum().fillna(0)
|
| 144 |
d['ofi'] = _z_roll(ofi_raw, 500)
|
| 145 |
|
|
|
|
| 146 |
buy_vol = (sign > 0) * v
|
| 147 |
sell_vol = (sign < 0) * v
|
| 148 |
imb = (buy_vol.rolling(60).sum() - sell_vol.rolling(60).sum()).abs()
|
| 149 |
tot = v.rolling(60).sum().replace(0, np.nan)
|
| 150 |
+
d['vpin'] = (imb / tot).fillna(0)
|
| 151 |
|
|
|
|
| 152 |
rv_gk_raw = ((np.log(h / l)**2) / 2) - ((2 * np.log(2) - 1) * (np.log(c / o)**2))
|
| 153 |
d['rv_gk'] = _z_roll(rv_gk_raw.fillna(0), 500)
|
| 154 |
|
|
|
|
| 155 |
vwap_win = 20
|
| 156 |
vwap = (d['dollar_vol'].rolling(vwap_win).sum() / v.rolling(vwap_win).sum().replace(0, np.nan)).fillna(c)
|
| 157 |
d['vwap_dev'] = _z_roll((c - vwap), 500)
|
| 158 |
|
|
|
|
|
|
|
| 159 |
d['L_score'] = (
|
| 160 |
+
d['vol_zscore_50'] + (-d['amihud']) + (-d['roll_spread']) +
|
| 161 |
+
(-d['rv_gk'].abs()) + (-d['vwap_dev'].abs()) + d['ofi']
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
).fillna(0)
|
| 163 |
|
| 164 |
return sanitize_features(d)
|
| 165 |
|
| 166 |
def calculate_titan_features_real(df):
|
|
|
|
| 167 |
df = df.copy()
|
|
|
|
| 168 |
df['RSI'] = ta.rsi(df['close'], 14)
|
| 169 |
macd = ta.macd(df['close'])
|
| 170 |
if macd is not None:
|
|
|
|
| 235 |
X_df = pd.concat(parts, axis=1)
|
| 236 |
return sanitize_features(X_df).values
|
| 237 |
except Exception as e:
|
|
|
|
| 238 |
return None
|
| 239 |
|
| 240 |
def calculate_legacy_v3_vectorized(df_1m, df_5m, df_15m):
|
|
|
|
| 241 |
try:
|
| 242 |
def calc_v3_base(df, prefix=""):
|
| 243 |
d = df.copy()
|
|
|
|
| 244 |
targets = ['rsi', 'rsi_slope', 'macd_h', 'macd_h_slope', 'adx', 'dmp', 'dmn',
|
| 245 |
'trend_net_force', 'ema_20', 'ema_50', 'ema_200', 'dist_ema20',
|
| 246 |
'dist_ema50', 'dist_ema200', 'slope_ema50', 'atr', 'atr_rel',
|
|
|
|
| 361 |
return raw
|
| 362 |
return model.predict(X)
|
| 363 |
except Exception as e:
|
|
|
|
| 364 |
return np.zeros(len(X) if hasattr(X, '__len__') else 0)
|
| 365 |
|
| 366 |
def _extract_probs(self, raw_preds):
|
|
|
|
| 412 |
df_5m = df_1m.resample('5T').agg({'open':'first', 'high':'max', 'low':'min', 'close':'last', 'volume':'sum'}).dropna()
|
| 413 |
df_15m = df_1m.resample('15T').agg({'open':'first', 'high':'max', 'low':'min', 'close':'last', 'volume':'sum'}).dropna()
|
| 414 |
|
| 415 |
+
# 1. Sniper
|
| 416 |
df_sniper_feats = calculate_sniper_features_exact(df_1m)
|
|
|
|
|
|
|
| 417 |
rel_vol = df_1m['volume'] / (df_1m['volume'].rolling(50).mean() + 1e-9)
|
| 418 |
l1_score = (rel_vol * 10) + ((ta.atr(df_1m['high'], df_1m['low'], df_1m['close'], 14)/df_1m['close']) * 1000)
|
| 419 |
|
|
|
|
| 457 |
res_titan = self.proc.titan.model.predict(xgb.DMatrix(X_titan_df.values, feature_names=feats))
|
| 458 |
except Exception as e: print(f"Titan Error: {e}")
|
| 459 |
|
| 460 |
+
# 4. Sniper
|
| 461 |
res_sniper = np.full(len(df_candidates), 0.5)
|
| 462 |
sniper_instance = self.proc.sniper
|
|
|
|
| 463 |
if sniper_instance and getattr(sniper_instance, 'models', []):
|
| 464 |
try:
|
|
|
|
| 465 |
required_features = getattr(sniper_instance, 'feature_names', [])
|
| 466 |
if not required_features and hasattr(sniper_instance.models[0], 'feature_name'):
|
| 467 |
required_features = sniper_instance.models[0].feature_name()
|
| 468 |
|
|
|
|
| 469 |
source_cols = df_sniper_feats.columns
|
| 470 |
def normalize_name(s): return s.lower().replace('_', '').replace('-', '').replace(' ', '')
|
| 471 |
col_map_lower = {col.lower(): col for col in source_cols}
|
| 472 |
col_map_fuzzy = {normalize_name(col): col for col in source_cols}
|
| 473 |
|
|
|
|
| 474 |
X_final = pd.DataFrame(index=df_candidates.index)
|
|
|
|
|
|
|
|
|
|
| 475 |
if required_features:
|
| 476 |
for req_feat in required_features:
|
| 477 |
req_norm = normalize_name(req_feat)
|
| 478 |
+
if req_feat in source_cols: X_final[req_feat] = df_sniper_feats.loc[df_candidates.index, req_feat]
|
| 479 |
+
elif req_feat.lower() in col_map_lower: X_final[req_feat] = df_sniper_feats.loc[df_candidates.index, col_map_lower[req_feat.lower()]]
|
| 480 |
+
elif req_norm in col_map_fuzzy: X_final[req_feat] = df_sniper_feats.loc[df_candidates.index, col_map_fuzzy[req_norm]]
|
| 481 |
+
else: X_final[req_feat] = 0.0
|
| 482 |
+
else: X_final = df_sniper_feats.loc[df_candidates.index]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
|
|
|
|
| 484 |
preds = []
|
| 485 |
for m in sniper_instance.models:
|
| 486 |
X_in = X_final.astype(np.float32)
|
| 487 |
raw_p = self._extract_probs(self._smart_predict(m, X_in, "Sniper"))
|
| 488 |
preds.append(raw_p)
|
|
|
|
| 489 |
res_sniper = np.mean(preds, axis=0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 490 |
except Exception as e:
|
| 491 |
print(f"โ Sniper Inference Error: {e}")
|
| 492 |
traceback.print_exc()
|
|
|
|
| 509 |
hydra_models = getattr(self.proc.guardian_hydra, 'models', {})
|
| 510 |
if hydra_models and 'crash' in hydra_models:
|
| 511 |
try:
|
| 512 |
+
df_sniper_base = df_1m.copy()
|
| 513 |
df_sniper_base['rsi_14'] = ta.rsi(df_sniper_base['close'], 14).fillna(50)
|
| 514 |
df_sniper_base['atr'] = ta.atr(df_sniper_base['high'], df_sniper_base['low'], df_sniper_base['close'], 14).fillna(0)
|
| 515 |
+
df_sniper_base['rel_vol'] = rel_vol
|
| 516 |
|
| 517 |
global_hydra_feats = np.column_stack([
|
| 518 |
df_sniper_base['rsi_14'], df_sniper_base['rsi_14'], df_sniper_base['rsi_14'],
|
|
|
|
| 652 |
|
| 653 |
final_bal = bal + alloc
|
| 654 |
profit = final_bal - initial_capital
|
| 655 |
+
|
| 656 |
+
# --- Detailed Stats Calculation ---
|
| 657 |
tot = len(log)
|
| 658 |
+
winning_trades = [x for x in log if x['pnl'] > 0]
|
| 659 |
+
losing_trades = [x for x in log if x['pnl'] <= 0]
|
| 660 |
|
| 661 |
+
win_count = len(winning_trades)
|
| 662 |
+
loss_count = len(losing_trades)
|
| 663 |
+
win_rate = (win_count/tot*100) if tot else 0
|
| 664 |
+
|
| 665 |
+
avg_win = np.mean([x['pnl'] for x in winning_trades]) if winning_trades else 0
|
| 666 |
+
avg_loss = np.mean([x['pnl'] for x in losing_trades]) if losing_trades else 0
|
| 667 |
+
|
| 668 |
+
gross_profit = sum([x['pnl'] for x in winning_trades])
|
| 669 |
+
gross_loss = abs(sum([x['pnl'] for x in losing_trades]))
|
| 670 |
+
profit_factor = (gross_profit / gross_loss) if gross_loss > 0 else 99.9
|
| 671 |
+
|
| 672 |
+
# Streaks
|
| 673 |
+
max_win_s = 0; max_loss_s = 0; curr_w = 0; curr_l = 0
|
| 674 |
+
for t in log:
|
| 675 |
+
if t['pnl'] > 0:
|
| 676 |
+
curr_w += 1; curr_l = 0
|
| 677 |
+
if curr_w > max_win_s: max_win_s = curr_w
|
| 678 |
+
else:
|
| 679 |
+
curr_l += 1; curr_w = 0
|
| 680 |
+
if curr_l > max_loss_s: max_loss_s = curr_l
|
| 681 |
+
|
| 682 |
cons_trades = [x for x in log if x['consensus']]
|
| 683 |
n_cons = len(cons_trades)
|
| 684 |
agree_rate = (n_cons/tot*100) if tot else 0
|
|
|
|
| 688 |
res.append({
|
| 689 |
'config': cfg, 'final_balance': final_bal, 'net_profit': profit,
|
| 690 |
'total_trades': tot, 'win_rate': win_rate, 'max_drawdown': 0,
|
| 691 |
+
'win_count': win_count, 'loss_count': loss_count,
|
| 692 |
+
'avg_win': avg_win, 'avg_loss': avg_loss,
|
| 693 |
+
'max_win_streak': max_win_s, 'max_loss_streak': max_loss_s,
|
| 694 |
+
'profit_factor': profit_factor,
|
| 695 |
'consensus_agreement_rate': agree_rate,
|
| 696 |
'high_consensus_win_rate': cons_win_rate,
|
| 697 |
'high_consensus_avg_pnl': cons_avg_pnl
|
|
|
|
| 717 |
results_list.sort(key=lambda x: x['net_profit'], reverse=True)
|
| 718 |
best = results_list[0]
|
| 719 |
|
| 720 |
+
# --- AUTO-DIAGNOSIS LOGIC ---
|
| 721 |
+
diag = []
|
| 722 |
+
if best['total_trades'] > 2000 and best['net_profit'] < 10:
|
| 723 |
+
diag.append("โ ๏ธ Overtrading: Too many trades for low profit.")
|
| 724 |
+
if best['win_rate'] > 55 and best['net_profit'] < 0:
|
| 725 |
+
diag.append("โ ๏ธ Fee Burn: High win rate but fees are eating profits.")
|
| 726 |
+
if abs(best['avg_loss']) > best['avg_win']:
|
| 727 |
+
diag.append("โ ๏ธ Risk/Reward Inversion: Avg Loss > Avg Win.")
|
| 728 |
+
if best['max_loss_streak'] > 10:
|
| 729 |
+
diag.append("โ ๏ธ Consecutive Loss Risk: Strategy prone to streak failures.")
|
| 730 |
+
if not diag: diag.append("โ
System Healthy.")
|
| 731 |
+
|
| 732 |
print("\n" + "="*60)
|
| 733 |
print(f"๐ CHAMPION REPORT [{target_regime}]:")
|
| 734 |
print(f" ๐ฐ Final Balance: ${best['final_balance']:,.2f}")
|
|
|
|
| 736 |
print("-" * 60)
|
| 737 |
print(f" ๐ Total Trades: {best['total_trades']}")
|
| 738 |
print(f" ๐ Win Rate: {best['win_rate']:.1f}%")
|
| 739 |
+
print(f" โ
Winning Trades: {best['win_count']} (Avg: {best['avg_win']*100:.2f}%)")
|
| 740 |
+
print(f" โ Losing Trades: {best['loss_count']} (Avg: {best['avg_loss']*100:.2f}%)")
|
| 741 |
+
print(f" ๐ Max Streaks: Win {best['max_win_streak']} | Loss {best['max_loss_streak']}")
|
| 742 |
+
print(f" โ๏ธ Profit Factor: {best['profit_factor']:.2f}")
|
| 743 |
print("-" * 60)
|
| 744 |
print(f" ๐ง CONSENSUS ANALYTICS:")
|
| 745 |
print(f" ๐ค Model Agreement Rate: {best['consensus_agreement_rate']:.1f}%")
|
| 746 |
print(f" ๐ High-Consensus Win Rate: {best['high_consensus_win_rate']:.1f}%")
|
|
|
|
| 747 |
print("-" * 60)
|
| 748 |
+
print(f" ๐ฉบ DIAGNOSIS: {' '.join(diag)}")
|
| 749 |
print(f" โ๏ธ Oracle={best['config']['oracle_thresh']:.2f} | Sniper={best['config']['sniper_thresh']:.2f} | Hydra={best['config']['hydra_thresh']:.2f}")
|
| 750 |
print("="*60)
|
| 751 |
return best['config'], best
|