Spaces:
Paused
Paused
Update backtest_engine.py
Browse files- backtest_engine.py +118 -472
backtest_engine.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
| 1 |
# ============================================================
|
| 2 |
-
# 🧪 backtest_engine.py (V223.
|
| 3 |
-
# FIXES:
|
| 4 |
-
# 1)
|
| 5 |
-
# 2) Added
|
| 6 |
-
# 3)
|
|
|
|
| 7 |
# ============================================================
|
| 8 |
|
| 9 |
import asyncio
|
|
@@ -54,7 +55,6 @@ def optimize_dataframe_memory(df: pd.DataFrame):
|
|
| 54 |
float_cols = df.select_dtypes(include=["float64"]).columns
|
| 55 |
if len(float_cols) > 0:
|
| 56 |
df[float_cols] = df[float_cols].astype("float32")
|
| 57 |
-
|
| 58 |
int_cols = df.select_dtypes(include=["int64", "int32"]).columns
|
| 59 |
for col in int_cols:
|
| 60 |
c_min = df[col].min()
|
|
@@ -163,6 +163,31 @@ def calc_consecutive_streaks(pnls):
|
|
| 163 |
return int(max_w), int(max_l)
|
| 164 |
|
| 165 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
# ============================================================
|
| 167 |
# 🧪 BACKTESTER
|
| 168 |
# ============================================================
|
|
@@ -172,7 +197,6 @@ class HeavyDutyBacktester:
|
|
| 172 |
self.proc = processor
|
| 173 |
self.gov_engine = GovernanceEngine()
|
| 174 |
|
| 175 |
-
# If True: raise on missing features. If False: fill 0 and continue.
|
| 176 |
self.STRICT_FEATURES = False
|
| 177 |
self._missing_feature_once = set()
|
| 178 |
|
|
@@ -213,7 +237,7 @@ class HeavyDutyBacktester:
|
|
| 213 |
self.force_end_date = "2024-02-01"
|
| 214 |
|
| 215 |
self.required_timeframes = self._determine_required_timeframes()
|
| 216 |
-
print(f"🧪 [Backtest V223.
|
| 217 |
|
| 218 |
def _verify_system_integrity(self):
|
| 219 |
errors = []
|
|
@@ -260,9 +284,12 @@ class HeavyDutyBacktester:
|
|
| 260 |
|
| 261 |
return list(tfs)
|
| 262 |
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
|
|
|
|
|
|
|
|
|
| 266 |
@staticmethod
|
| 267 |
def _safe_bbands(close: pd.Series, length=20, std=2.0):
|
| 268 |
basis = close.rolling(length).mean()
|
|
@@ -285,11 +312,9 @@ class HeavyDutyBacktester:
|
|
| 285 |
l = df["low"].astype(np.float64)
|
| 286 |
v = df["volume"].astype(np.float64) if "volume" in df.columns else pd.Series(np.zeros(len(df)), index=df.index)
|
| 287 |
|
| 288 |
-
# EMAs
|
| 289 |
for span in [9, 20, 21, 50, 200]:
|
| 290 |
df[f"ema{span}"] = c.ewm(span=span, adjust=False).mean()
|
| 291 |
|
| 292 |
-
# BBANDS
|
| 293 |
if len(df) < 30:
|
| 294 |
df["lower_bb"] = c
|
| 295 |
df["upper_bb"] = c
|
|
@@ -318,7 +343,6 @@ class HeavyDutyBacktester:
|
|
| 318 |
lower, upper, width, pct = self._safe_bbands(c, 20, 2.0)
|
| 319 |
df["lower_bb"], df["upper_bb"], df["bb_width"], df["bb_pct"] = lower, upper, width, pct
|
| 320 |
|
| 321 |
-
# MACD
|
| 322 |
macd = ta.macd(c)
|
| 323 |
if macd is not None and isinstance(macd, pd.DataFrame) and macd.shape[1] >= 3:
|
| 324 |
df["MACD"] = macd.iloc[:, 0]
|
|
@@ -329,7 +353,6 @@ class HeavyDutyBacktester:
|
|
| 329 |
df["MACD_h"] = 0.0
|
| 330 |
df["MACD_s"] = 0.0
|
| 331 |
|
| 332 |
-
# Core
|
| 333 |
df["RSI"] = ta.rsi(c, length=14).fillna(50)
|
| 334 |
df["ATR"] = ta.atr(h, l, c, length=14).fillna(0)
|
| 335 |
|
|
@@ -346,12 +369,47 @@ class HeavyDutyBacktester:
|
|
| 346 |
except:
|
| 347 |
df["vwap"] = c
|
| 348 |
|
| 349 |
-
#
|
| 350 |
try:
|
| 351 |
df["CCI"] = ta.cci(h, l, c, length=20).fillna(0)
|
| 352 |
except:
|
| 353 |
df["CCI"] = 0.0
|
| 354 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
# Derived
|
| 356 |
df["EMA_9_dist"] = (c / (df["ema9"] + 1e-12)) - 1
|
| 357 |
df["EMA_21_dist"] = (c / (df["ema21"] + 1e-12)) - 1
|
|
@@ -366,12 +424,10 @@ class HeavyDutyBacktester:
|
|
| 366 |
|
| 367 |
return df.fillna(0)
|
| 368 |
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
print(f"[WARN] {msg}")
|
| 374 |
-
|
| 375 |
async def _fetch_all_data_fast(self, sym, start_ms, end_ms):
|
| 376 |
print(f" ⚡ [Network] Downloading {sym}...", flush=True)
|
| 377 |
limit = 1000
|
|
@@ -424,12 +480,8 @@ class HeavyDutyBacktester:
|
|
| 424 |
df_1m["datetime"] = pd.to_datetime(df_1m["timestamp"] + 60000, unit="ms", utc=True)
|
| 425 |
df_1m.set_index("datetime", inplace=True)
|
| 426 |
df_1m = df_1m.sort_index()
|
| 427 |
-
|
| 428 |
df_1m = self._calculate_all_indicators(df_1m)
|
| 429 |
|
| 430 |
-
if len(df_1m) < 300:
|
| 431 |
-
raise RuntimeError(f"{sym} has too few valid candles after cleaning: {len(df_1m)}")
|
| 432 |
-
|
| 433 |
arr_ts_1m = (df_1m.index.astype(np.int64) // 10**6).values
|
| 434 |
fast_1m_close = df_1m["close"].values.astype(np.float32)
|
| 435 |
|
|
@@ -462,93 +514,19 @@ class HeavyDutyBacktester:
|
|
| 462 |
validity_mask &= (maps[tf] >= 0)
|
| 463 |
validity_mask[:200] = False
|
| 464 |
|
| 465 |
-
#
|
|
|
|
| 466 |
global_pattern_scores = np.zeros(len(arr_ts_1m), dtype=np.float32)
|
| 467 |
-
pat_cache_file = os.path.join(PATTERN_CACHE_DIR, f"{safe_sym}_{period_suffix}_pat.pkl")
|
| 468 |
-
pattern_results_map = {}
|
| 469 |
-
|
| 470 |
-
if os.path.exists(pat_cache_file):
|
| 471 |
-
with open(pat_cache_file, "rb") as f:
|
| 472 |
-
pattern_results_map = pickle.load(f)
|
| 473 |
-
elif "15m" in numpy_htf:
|
| 474 |
-
ts_15m = numpy_htf["15m"]["timestamp"]
|
| 475 |
-
cols = ["timestamp", "open", "high", "low", "close", "volume"]
|
| 476 |
-
df_15m_source = pd.DataFrame({c: numpy_htf["15m"][c] for c in cols})
|
| 477 |
-
for i in range(200, len(df_15m_source)):
|
| 478 |
-
window = df_15m_source.iloc[i - 200 : i + 1]
|
| 479 |
-
ohlcv_input = {"15m": window.values.tolist()}
|
| 480 |
-
try:
|
| 481 |
-
res = await self.proc.pattern_engine.detect_chart_patterns(ohlcv_input)
|
| 482 |
-
pattern_results_map[ts_15m[i]] = res.get("pattern_confidence", 0.0)
|
| 483 |
-
except:
|
| 484 |
-
pass
|
| 485 |
-
with open(pat_cache_file, "wb") as f:
|
| 486 |
-
pickle.dump(pattern_results_map, f)
|
| 487 |
-
|
| 488 |
-
if "15m" in maps and "15m" in numpy_htf:
|
| 489 |
-
map_15 = maps["15m"]
|
| 490 |
-
ts_15_arr = numpy_htf["15m"]["timestamp"]
|
| 491 |
-
for i in range(len(arr_ts_1m)):
|
| 492 |
-
if not validity_mask[i]:
|
| 493 |
-
continue
|
| 494 |
-
idx = map_15[i]
|
| 495 |
-
if idx >= 0:
|
| 496 |
-
global_pattern_scores[i] = pattern_results_map.get(ts_15_arr[idx], 0.0)
|
| 497 |
-
|
| 498 |
-
# 2) Governance (Cached)
|
| 499 |
gov_scores_final = np.zeros(len(arr_ts_1m), dtype=np.float32)
|
| 500 |
-
|
| 501 |
-
gov_results_map = {}
|
| 502 |
-
|
| 503 |
-
if os.path.exists(gov_cache_file):
|
| 504 |
-
with open(gov_cache_file, "rb") as f:
|
| 505 |
-
gov_results_map = pickle.load(f)
|
| 506 |
-
elif "15m" in numpy_htf:
|
| 507 |
-
cols = ["timestamp", "open", "high", "low", "close", "volume"]
|
| 508 |
-
df_15m_g = pd.DataFrame({c: numpy_htf["15m"][c] for c in cols})
|
| 509 |
-
ts_15m = numpy_htf["15m"]["timestamp"]
|
| 510 |
-
has_1h = "1h" in numpy_htf
|
| 511 |
-
df_1h_g = pd.DataFrame({c: numpy_htf["1h"][c] for c in cols}) if has_1h else None
|
| 512 |
-
ts_1h = numpy_htf["1h"]["timestamp"] if has_1h else None
|
| 513 |
-
|
| 514 |
-
for i in range(200, len(df_15m_g)):
|
| 515 |
-
curr_ts = ts_15m[i]
|
| 516 |
-
win_15 = df_15m_g.iloc[i - 120 : i + 1]
|
| 517 |
-
ohlcv_input = {"15m": win_15.values.tolist()}
|
| 518 |
-
|
| 519 |
-
if has_1h:
|
| 520 |
-
idx_1h = np.searchsorted(ts_1h, curr_ts, side="right") - 1
|
| 521 |
-
if idx_1h >= 50:
|
| 522 |
-
ohlcv_input["1h"] = df_1h_g.iloc[idx_1h - 60 : idx_1h + 1].values.tolist()
|
| 523 |
-
try:
|
| 524 |
-
res = await self.gov_engine.evaluate_trade(sym, ohlcv_input, {}, "NORMAL", False, has_1h)
|
| 525 |
-
score = res.get("governance_score", 0.0) if res.get("grade") != "REJECT" else 0.0
|
| 526 |
-
gov_results_map[curr_ts] = score
|
| 527 |
-
except:
|
| 528 |
-
pass
|
| 529 |
-
|
| 530 |
-
with open(gov_cache_file, "wb") as f:
|
| 531 |
-
pickle.dump(gov_results_map, f)
|
| 532 |
-
|
| 533 |
-
if "15m" in maps and "15m" in numpy_htf:
|
| 534 |
-
map_15 = maps["15m"]
|
| 535 |
-
ts_15_arr = numpy_htf["15m"]["timestamp"]
|
| 536 |
-
for i in range(len(arr_ts_1m)):
|
| 537 |
-
if not validity_mask[i]:
|
| 538 |
-
continue
|
| 539 |
-
idx = map_15[i]
|
| 540 |
-
if idx >= 0:
|
| 541 |
-
gov_scores_final[i] = gov_results_map.get(ts_15_arr[idx], 0.0)
|
| 542 |
|
| 543 |
-
#
|
| 544 |
map_1h = maps["1h"]
|
| 545 |
valid_1h = map_1h >= 0
|
| 546 |
idx_1h = map_1h[valid_1h]
|
| 547 |
-
|
| 548 |
h1_chop = numpy_htf["1h"]["CHOP"][idx_1h]
|
| 549 |
h1_adx = numpy_htf["1h"]["ADX"][idx_1h]
|
| 550 |
h1_atr_pct = numpy_htf["1h"]["ATR_pct"][idx_1h]
|
| 551 |
-
|
| 552 |
market_ok = np.ones(len(arr_ts_1m), dtype=bool)
|
| 553 |
market_ok[valid_1h] = ~((h1_chop > 61.8) | ((h1_atr_pct < 0.3) & (h1_adx < 20)))
|
| 554 |
|
|
@@ -565,19 +543,15 @@ class HeavyDutyBacktester:
|
|
| 565 |
mask_acc = (h1_bbw < 0.20) & (h1_rsi >= 35) & (h1_rsi <= 65)
|
| 566 |
mask_safe = (h1_adx > 25) & (h1_ema20 > h1_ema50) & (h1_ema50 > h1_ema200) & (h1_rsi > 50) & (h1_rsi < 75)
|
| 567 |
mask_exp = (h1_rsi > 65) & (h1_close > h1_upper) & (h1_rel_vol > 1.5)
|
| 568 |
-
|
| 569 |
state_buffer = np.zeros(len(idx_1h), dtype=np.int8)
|
| 570 |
state_buffer[mask_acc] = 1
|
| 571 |
state_buffer[mask_safe] = 2
|
| 572 |
state_buffer[mask_exp] = 3
|
| 573 |
-
|
| 574 |
coin_state[valid_1h] = state_buffer
|
| 575 |
coin_state[~validity_mask] = 0
|
| 576 |
coin_state[~market_ok] = 0
|
| 577 |
|
| 578 |
-
#
|
| 579 |
-
# 4) Titan & Oracle (Hardened)
|
| 580 |
-
# =========================
|
| 581 |
titan_cols = self.proc.titan.model.feature_names
|
| 582 |
t_vecs = []
|
| 583 |
for col in titan_cols:
|
|
@@ -586,7 +560,6 @@ class HeavyDutyBacktester:
|
|
| 586 |
raise ValueError(f"Titan Feature Format Error: {col}")
|
| 587 |
tf = parts[0]
|
| 588 |
raw_feat = parts[1]
|
| 589 |
-
|
| 590 |
lookup_key = "bb_pct" if raw_feat in ["BB_p", "BB_pct"] else ("bb_width" if raw_feat == "BB_w" else raw_feat)
|
| 591 |
|
| 592 |
if tf not in numpy_htf:
|
|
@@ -613,15 +586,16 @@ class HeavyDutyBacktester:
|
|
| 613 |
t_vecs.append(vals)
|
| 614 |
|
| 615 |
X_TITAN = np.column_stack(t_vecs)
|
| 616 |
-
global_titan_scores = self.proc.titan.model.predict(xgb.DMatrix(X_TITAN, feature_names=titan_cols))
|
| 617 |
|
|
|
|
| 618 |
oracle_cols = self.proc.oracle.feature_cols
|
| 619 |
o_vecs = []
|
| 620 |
for col in oracle_cols:
|
| 621 |
if col == "sim_titan_score":
|
| 622 |
-
o_vecs.append(global_titan_scores
|
| 623 |
elif col in ["sim_pattern_score", "pattern_score"]:
|
| 624 |
-
o_vecs.append(global_pattern_scores
|
| 625 |
elif col == "sim_mc_score":
|
| 626 |
o_vecs.append(np.zeros(len(arr_ts_1m), dtype=np.float32))
|
| 627 |
else:
|
|
@@ -629,21 +603,18 @@ class HeavyDutyBacktester:
|
|
| 629 |
if len(parts) != 2:
|
| 630 |
raise ValueError(f"Oracle Feature Error: {col}")
|
| 631 |
tf, key = parts
|
| 632 |
-
|
| 633 |
if tf not in numpy_htf:
|
| 634 |
if self.STRICT_FEATURES:
|
| 635 |
raise ValueError(f"Oracle requires TF not built: {tf} (feature: {col})")
|
| 636 |
self._warn_missing_once(f"Oracle TF missing -> {col}. Filled 0.")
|
| 637 |
o_vecs.append(np.zeros(len(arr_ts_1m), dtype=np.float32))
|
| 638 |
continue
|
| 639 |
-
|
| 640 |
if key not in numpy_htf[tf]:
|
| 641 |
if self.STRICT_FEATURES:
|
| 642 |
raise ValueError(f"Missing Oracle Feature: {col}")
|
| 643 |
self._warn_missing_once(f"Missing Oracle Feature -> {col}. Filled 0.")
|
| 644 |
o_vecs.append(np.zeros(len(arr_ts_1m), dtype=np.float32))
|
| 645 |
continue
|
| 646 |
-
|
| 647 |
idx = maps[tf]
|
| 648 |
vals = np.zeros(len(arr_ts_1m), dtype=np.float32)
|
| 649 |
valid = idx >= 0
|
|
@@ -652,54 +623,28 @@ class HeavyDutyBacktester:
|
|
| 652 |
|
| 653 |
X_ORACLE = np.column_stack(o_vecs)
|
| 654 |
preds_o = self.proc.oracle.model_direction.predict(X_ORACLE)
|
| 655 |
-
|
|
|
|
| 656 |
preds_o = preds_o[:, 0]
|
| 657 |
global_oracle_scores = preds_o.astype(np.float32)
|
| 658 |
|
| 659 |
-
# 5) Sniper
|
| 660 |
df_sniper_feats = self.proc.sniper._calculate_features_live(df_1m)
|
| 661 |
X_sniper = df_sniper_feats[self.proc.sniper.feature_names].fillna(0)
|
| 662 |
-
|
|
|
|
|
|
|
| 663 |
for model in self.proc.sniper.models:
|
| 664 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 665 |
global_sniper_scores = (preds_accum / max(1, len(self.proc.sniper.models))).astype(np.float32)
|
| 666 |
|
| 667 |
-
#
|
| 668 |
-
|
| 669 |
-
map_15 = maps["15m"]
|
| 670 |
-
map_1 = maps.get("1h", map_15)
|
| 671 |
-
|
| 672 |
-
f_rsi_1m = df_1m["RSI"].values.astype(np.float32)
|
| 673 |
-
|
| 674 |
-
f_rsi_5m = np.zeros(len(arr_ts_1m), dtype=np.float32)
|
| 675 |
-
v5 = map_5 >= 0
|
| 676 |
-
if "5m" in numpy_htf and "RSI" in numpy_htf["5m"]:
|
| 677 |
-
f_rsi_5m[v5] = numpy_htf["5m"]["RSI"][map_5[v5]].astype(np.float32)
|
| 678 |
-
|
| 679 |
-
f_rsi_15m = np.zeros(len(arr_ts_1m), dtype=np.float32)
|
| 680 |
-
v15 = map_15 >= 0
|
| 681 |
-
if "15m" in numpy_htf and "RSI" in numpy_htf["15m"]:
|
| 682 |
-
f_rsi_15m[v15] = numpy_htf["15m"]["RSI"][map_15[v15]].astype(np.float32)
|
| 683 |
-
|
| 684 |
-
f_dist_1h = np.zeros(len(arr_ts_1m), dtype=np.float32)
|
| 685 |
-
v1 = map_1 >= 0
|
| 686 |
-
ema20_1h = numpy_htf["1h"]["ema20"][map_1[v1]].astype(np.float32)
|
| 687 |
-
close_1h = numpy_htf["1h"]["close"][map_1[v1]].astype(np.float32)
|
| 688 |
-
f_dist_1h[v1] = (close_1h - ema20_1h) / (close_1h + 1e-12)
|
| 689 |
-
|
| 690 |
-
hydra_static = np.column_stack(
|
| 691 |
-
[
|
| 692 |
-
f_rsi_1m,
|
| 693 |
-
f_rsi_5m,
|
| 694 |
-
f_rsi_15m,
|
| 695 |
-
df_1m["bb_width"].values.astype(np.float32),
|
| 696 |
-
df_1m["rel_vol"].values.astype(np.float32),
|
| 697 |
-
f_dist_1h,
|
| 698 |
-
(df_1m["ATR_pct"].values.astype(np.float32) / 100.0),
|
| 699 |
-
]
|
| 700 |
-
).astype(np.float32)
|
| 701 |
-
|
| 702 |
-
# SAVE
|
| 703 |
min_gov = float(self.GRID_RANGES["GOV_SCORE"][0])
|
| 704 |
min_oracle = float(self.GRID_RANGES["ORACLE"][0])
|
| 705 |
min_titan = float(self.GRID_RANGES["TITAN"][0])
|
|
@@ -715,37 +660,34 @@ class HeavyDutyBacktester:
|
|
| 715 |
& (global_sniper_scores >= min_sniper)
|
| 716 |
& (global_pattern_scores >= min_pattern)
|
| 717 |
)
|
| 718 |
-
|
| 719 |
valid_idxs = np.where(filter_mask)[0]
|
| 720 |
|
| 721 |
-
signals_df = pd.DataFrame(
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
)
|
| 734 |
-
|
| 735 |
sim_data = {
|
| 736 |
"timestamp": arr_ts_1m.astype(np.int64),
|
| 737 |
"close": fast_1m_close,
|
| 738 |
"high": df_1m["high"].values.astype(np.float32),
|
| 739 |
"low": df_1m["low"].values.astype(np.float32),
|
| 740 |
"atr": df_1m["ATR"].values.astype(np.float32),
|
| 741 |
-
"hydra_static": hydra_static,
|
| 742 |
"oracle_conf": global_oracle_scores.astype(np.float32),
|
| 743 |
"titan_score": global_titan_scores.astype(np.float32),
|
|
|
|
| 744 |
}
|
| 745 |
|
| 746 |
pd.to_pickle({"signals": signals_df, "sim_data": sim_data}, scores_file)
|
| 747 |
-
|
| 748 |
-
print(f" ✅ [{sym}] Processed in {dt:.2f}s. Signals: {len(signals_df)}")
|
| 749 |
gc.collect()
|
| 750 |
|
| 751 |
async def generate_truth_data(self):
|
|
@@ -769,303 +711,7 @@ class HeavyDutyBacktester:
|
|
| 769 |
print(f"[WARN] {sym} skipped due to error: {e}")
|
| 770 |
traceback.print_exc()
|
| 771 |
|
| 772 |
-
#
|
| 773 |
-
# Optimization core (unchanged from your last version)
|
| 774 |
-
# =========================
|
| 775 |
-
def _flush_position_interval(
|
| 776 |
-
self, cfg, open_sym, pos, curr_ts, sim_env, crash_model, giveback_model, fees_pct,
|
| 777 |
-
trade_pnls, trade_returns, trade_durations, equity_curve, cash_bal, wins_losses,
|
| 778 |
-
last_update_map, end_idx_override=None
|
| 779 |
-
):
|
| 780 |
-
c_data = sim_env[open_sym]
|
| 781 |
-
full_ts = c_data["timestamp"]
|
| 782 |
-
|
| 783 |
-
start_idx = int(last_update_map.get(open_sym, 0))
|
| 784 |
-
if start_idx < 0:
|
| 785 |
-
start_idx = 0
|
| 786 |
-
|
| 787 |
-
if end_idx_override is None:
|
| 788 |
-
end_idx = int(np.searchsorted(full_ts, curr_ts, side="right"))
|
| 789 |
-
else:
|
| 790 |
-
end_idx = int(end_idx_override)
|
| 791 |
-
|
| 792 |
-
end_idx = min(end_idx, len(full_ts))
|
| 793 |
-
if end_idx <= start_idx:
|
| 794 |
-
return cash_bal, False
|
| 795 |
-
|
| 796 |
-
interval_high = c_data["high"][start_idx:end_idx]
|
| 797 |
-
interval_low = c_data["low"][start_idx:end_idx]
|
| 798 |
-
interval_close = c_data["close"][start_idx:end_idx]
|
| 799 |
-
interval_atr = c_data["atr"][start_idx:end_idx]
|
| 800 |
-
|
| 801 |
-
h_static = c_data["hydra_static"][start_idx:end_idx]
|
| 802 |
-
h_oracle = c_data["oracle_conf"][start_idx:end_idx]
|
| 803 |
-
h_titan = c_data["titan_score"][start_idx:end_idx]
|
| 804 |
-
|
| 805 |
-
entry_p = float(pos["entry_p"])
|
| 806 |
-
entry_time = int(pos["entry_ts"])
|
| 807 |
-
|
| 808 |
-
prev_high = float(pos.get("highest_price", entry_p))
|
| 809 |
-
current_highs = np.maximum.accumulate(np.concatenate([[prev_high], interval_high]))[1:]
|
| 810 |
-
pos["highest_price"] = float(current_highs[-1])
|
| 811 |
-
|
| 812 |
-
durations = (full_ts[start_idx:end_idx] - entry_time) / 60000.0
|
| 813 |
-
|
| 814 |
-
sl_dist = np.maximum(1.5 * interval_atr, 1e-8)
|
| 815 |
-
pnl = interval_close - entry_p
|
| 816 |
-
norm_pnl = pnl / sl_dist
|
| 817 |
-
max_pnl = (current_highs - entry_p) / sl_dist
|
| 818 |
-
|
| 819 |
-
zeros = np.zeros(len(interval_close), dtype=np.float32)
|
| 820 |
-
h_dynamic = np.column_stack([norm_pnl, max_pnl, zeros, zeros, durations]).astype(np.float32)
|
| 821 |
-
threes = np.full(len(interval_close), 3.0, dtype=np.float32)
|
| 822 |
-
h_context = np.column_stack([zeros, h_oracle, h_titan, threes]).astype(np.float32)
|
| 823 |
-
X_H = np.column_stack([h_static, h_dynamic, h_context]).astype(np.float32)
|
| 824 |
-
|
| 825 |
-
crash_probs = crash_model.predict_proba(X_H)[:, 1]
|
| 826 |
-
give_probs = giveback_model.predict_proba(X_H)[:, 1]
|
| 827 |
-
|
| 828 |
-
sl_hit = interval_low < pos["sl_p"]
|
| 829 |
-
tp_hit = interval_high > pos["tp_p"]
|
| 830 |
-
hydra_hit = (crash_probs > cfg["HYDRA_THRESH"]) | (give_probs > cfg["HYDRA_THRESH"])
|
| 831 |
-
legacy_hit = (crash_probs > cfg["LEGACY_THRESH"]) | (give_probs > cfg["LEGACY_THRESH"])
|
| 832 |
-
|
| 833 |
-
any_exit = sl_hit | tp_hit | legacy_hit | hydra_hit
|
| 834 |
-
last_update_map[open_sym] = end_idx
|
| 835 |
-
|
| 836 |
-
if not np.any(any_exit):
|
| 837 |
-
return cash_bal, False
|
| 838 |
-
|
| 839 |
-
idx = int(np.argmax(any_exit))
|
| 840 |
-
exit_ts = int(full_ts[start_idx + idx])
|
| 841 |
-
|
| 842 |
-
if sl_hit[idx]:
|
| 843 |
-
exit_p = float(pos["sl_p"]) * (1 - self.SLIPPAGE_PCT)
|
| 844 |
-
elif tp_hit[idx]:
|
| 845 |
-
exit_p = float(pos["tp_p"]) * (1 - self.SLIPPAGE_PCT)
|
| 846 |
-
elif legacy_hit[idx]:
|
| 847 |
-
exit_p = float(interval_close[idx]) * (1 - (self.SLIPPAGE_PCT * 2.0))
|
| 848 |
-
else:
|
| 849 |
-
exit_p = float(interval_close[idx]) * (1 - self.SLIPPAGE_PCT)
|
| 850 |
-
|
| 851 |
-
net = (pos["qty"] * exit_p) * (1 - fees_pct)
|
| 852 |
-
cash_bal += net
|
| 853 |
-
pnl_real = float(net - pos["cost"])
|
| 854 |
-
|
| 855 |
-
trade_pnls.append(pnl_real)
|
| 856 |
-
trade_returns.append(pnl_real / (float(pos["cost"]) + 1e-12))
|
| 857 |
-
trade_durations.append((exit_ts - entry_time) / 60000.0)
|
| 858 |
-
equity_curve.append(float(cash_bal))
|
| 859 |
-
|
| 860 |
-
if pnl_real > 0:
|
| 861 |
-
wins_losses["wins"] += 1
|
| 862 |
-
else:
|
| 863 |
-
wins_losses["losses"] += 1
|
| 864 |
-
|
| 865 |
-
return cash_bal, True
|
| 866 |
-
|
| 867 |
-
def _worker_optimize(self, combinations_batch, scores_files, initial_capital, fees_pct, max_slots, target_state):
|
| 868 |
-
all_signals = []
|
| 869 |
-
sim_env = {}
|
| 870 |
-
crash_model = self.proc.guardian_hydra.models["crash"]
|
| 871 |
-
giveback_model = self.proc.guardian_hydra.models["giveback"]
|
| 872 |
-
|
| 873 |
-
for f in scores_files:
|
| 874 |
-
try:
|
| 875 |
-
data = pd.read_pickle(f)
|
| 876 |
-
sig = optimize_dataframe_memory(data.get("signals", None))
|
| 877 |
-
if sig is None or len(sig) == 0:
|
| 878 |
-
continue
|
| 879 |
-
all_signals.append(sig)
|
| 880 |
-
sym = str(sig["symbol"].iloc[0])
|
| 881 |
-
sim_env[sym] = data["sim_data"]
|
| 882 |
-
except:
|
| 883 |
-
pass
|
| 884 |
-
|
| 885 |
-
if not all_signals:
|
| 886 |
-
return []
|
| 887 |
-
|
| 888 |
-
timeline_df = pd.concat(all_signals).sort_values("timestamp").reset_index(drop=True)
|
| 889 |
-
t_ts = timeline_df["timestamp"].values.astype(np.int64)
|
| 890 |
-
t_sym = timeline_df["symbol"].values
|
| 891 |
-
t_close = timeline_df["close"].values.astype(np.float64)
|
| 892 |
-
t_state = timeline_df["coin_state"].values
|
| 893 |
-
t_gov = timeline_df["gov_score"].values.astype(np.float64)
|
| 894 |
-
t_oracle = timeline_df["oracle_conf"].values.astype(np.float64)
|
| 895 |
-
t_titan = timeline_df["titan_score"].values.astype(np.float64)
|
| 896 |
-
t_sniper = timeline_df["sniper_score"].values.astype(np.float64)
|
| 897 |
-
t_pattern = timeline_df["pattern_score"].values.astype(np.float64)
|
| 898 |
-
del all_signals, timeline_df
|
| 899 |
-
gc.collect()
|
| 900 |
-
|
| 901 |
-
start_ms = int(t_ts[0]) if len(t_ts) else 0
|
| 902 |
-
end_ms = int(t_ts[-1]) if len(t_ts) else 0
|
| 903 |
-
|
| 904 |
-
res = []
|
| 905 |
-
BATCH_SIZE = 300
|
| 906 |
-
USE_MARK_TO_MARKET_EQUITY = True
|
| 907 |
-
|
| 908 |
-
for i in range(0, len(combinations_batch), BATCH_SIZE):
|
| 909 |
-
batch = combinations_batch[i : i + BATCH_SIZE]
|
| 910 |
-
for cfg in batch:
|
| 911 |
-
cash_bal = float(initial_capital)
|
| 912 |
-
active_positions = {}
|
| 913 |
-
last_update_map = {}
|
| 914 |
-
last_price = {}
|
| 915 |
-
trade_pnls = []
|
| 916 |
-
trade_returns = []
|
| 917 |
-
trade_durations = []
|
| 918 |
-
equity_curve = [float(initial_capital)]
|
| 919 |
-
wins_losses = {"wins": 0, "losses": 0}
|
| 920 |
-
exposure_steps = 0
|
| 921 |
-
|
| 922 |
-
def mark_to_market_equity(curr_ts):
|
| 923 |
-
nonlocal exposure_steps
|
| 924 |
-
open_val = 0.0
|
| 925 |
-
has_open = False
|
| 926 |
-
for s, pos in active_positions.items():
|
| 927 |
-
px = last_price.get(s, None)
|
| 928 |
-
if px is None:
|
| 929 |
-
continue
|
| 930 |
-
has_open = True
|
| 931 |
-
open_val += (pos["qty"] * px * (1 - self.SLIPPAGE_PCT)) * (1 - fees_pct)
|
| 932 |
-
if has_open:
|
| 933 |
-
exposure_steps += 1
|
| 934 |
-
equity_curve.append(float(cash_bal + open_val))
|
| 935 |
-
|
| 936 |
-
for curr_ts, sym, p, c_state, gov, oracle, titan, sniper, pattern in zip(
|
| 937 |
-
t_ts, t_sym, t_close, t_state, t_gov, t_oracle, t_titan, t_sniper, t_pattern
|
| 938 |
-
):
|
| 939 |
-
sym = str(sym)
|
| 940 |
-
last_price[sym] = float(p)
|
| 941 |
-
|
| 942 |
-
to_close = []
|
| 943 |
-
for open_sym, pos in list(active_positions.items()):
|
| 944 |
-
cash_bal, closed = self._flush_position_interval(
|
| 945 |
-
cfg, open_sym, pos, curr_ts, sim_env, crash_model, giveback_model, fees_pct,
|
| 946 |
-
trade_pnls, trade_returns, trade_durations, equity_curve, cash_bal,
|
| 947 |
-
wins_losses, last_update_map
|
| 948 |
-
)
|
| 949 |
-
if closed:
|
| 950 |
-
to_close.append(open_sym)
|
| 951 |
-
for s in to_close:
|
| 952 |
-
del active_positions[s]
|
| 953 |
-
|
| 954 |
-
if USE_MARK_TO_MARKET_EQUITY:
|
| 955 |
-
mark_to_market_equity(curr_ts)
|
| 956 |
-
|
| 957 |
-
is_valid = (
|
| 958 |
-
(int(c_state) == int(target_state))
|
| 959 |
-
and (float(gov) >= float(cfg["GOV_SCORE"]))
|
| 960 |
-
and (float(oracle) >= float(cfg["ORACLE"]))
|
| 961 |
-
and (float(titan) >= float(cfg["TITAN"]))
|
| 962 |
-
and (float(sniper) >= float(cfg["SNIPER"]))
|
| 963 |
-
and (float(pattern) >= float(cfg["PATTERN"]))
|
| 964 |
-
)
|
| 965 |
-
|
| 966 |
-
if is_valid and sym not in active_positions:
|
| 967 |
-
slots = 1 if cash_bal < self.MIN_CAPITAL_FOR_SPLIT else int(max_slots)
|
| 968 |
-
if len(active_positions) < slots and cash_bal >= 5.0:
|
| 969 |
-
size = (cash_bal * 0.95) if cash_bal < self.MIN_CAPITAL_FOR_SPLIT else (cash_bal / max_slots)
|
| 970 |
-
if size >= 5.0:
|
| 971 |
-
ep = float(p) * (1 + self.SLIPPAGE_PCT)
|
| 972 |
-
fee = float(size) * fees_pct
|
| 973 |
-
cost = float(size)
|
| 974 |
-
qty = (cost - fee) / (ep + 1e-12)
|
| 975 |
-
|
| 976 |
-
sym_ts = sim_env[sym]["timestamp"]
|
| 977 |
-
idx = int(np.searchsorted(sym_ts, curr_ts, side="right") - 1)
|
| 978 |
-
idx = max(0, min(idx, len(sym_ts) - 1))
|
| 979 |
-
atr_val = float(sim_env[sym]["atr"][idx])
|
| 980 |
-
|
| 981 |
-
active_positions[sym] = {
|
| 982 |
-
"qty": float(qty),
|
| 983 |
-
"entry_p": float(ep),
|
| 984 |
-
"cost": float(cost),
|
| 985 |
-
"entry_ts": int(curr_ts),
|
| 986 |
-
"sl_p": float(ep - 1.5 * atr_val),
|
| 987 |
-
"tp_p": float(ep + 2.5 * atr_val),
|
| 988 |
-
"highest_price": float(ep),
|
| 989 |
-
}
|
| 990 |
-
cash_bal -= float(cost)
|
| 991 |
-
last_update_map[sym] = min(idx + 1, len(sym_ts))
|
| 992 |
-
|
| 993 |
-
if not trade_pnls:
|
| 994 |
-
continue
|
| 995 |
-
|
| 996 |
-
max_dd = calc_max_drawdown(equity_curve)
|
| 997 |
-
ulcer = calc_ulcer_index(equity_curve)
|
| 998 |
-
wins_list = [p for p in trade_pnls if p > 0]
|
| 999 |
-
loss_list = [p for p in trade_pnls if p <= 0]
|
| 1000 |
-
prof_fac = calc_profit_factor(wins_list, loss_list)
|
| 1001 |
-
mean_pnl = float(np.mean(trade_pnls))
|
| 1002 |
-
std_pnl = float(np.std(trade_pnls))
|
| 1003 |
-
sqn = float((mean_pnl / std_pnl) * np.sqrt(len(trade_pnls))) if std_pnl > 0 else 0.0
|
| 1004 |
-
sharpe = calc_sharpe(trade_returns)
|
| 1005 |
-
sortino = calc_sortino(trade_returns)
|
| 1006 |
-
cagr = calc_cagr(initial_capital, cash_bal, start_ms, end_ms)
|
| 1007 |
-
calmar = calc_calmar(cagr, max_dd)
|
| 1008 |
-
exposure_pct = float(exposure_steps / max(1, len(t_ts)) * 100.0)
|
| 1009 |
-
max_w_streak, max_l_streak = calc_consecutive_streaks(trade_pnls)
|
| 1010 |
-
payoff = float(np.mean(wins_list) / max(abs(np.mean(loss_list)), 1e-12)) if (wins_list and loss_list) else 99.0
|
| 1011 |
-
|
| 1012 |
-
res.append({
|
| 1013 |
-
"config": cfg,
|
| 1014 |
-
"net_profit": float(cash_bal - initial_capital),
|
| 1015 |
-
"total_trades": int(len(trade_pnls)),
|
| 1016 |
-
"final_balance": float(cash_bal),
|
| 1017 |
-
"win_rate": float((wins_losses["wins"] / len(trade_pnls)) * 100.0),
|
| 1018 |
-
"sqn": sqn,
|
| 1019 |
-
"max_drawdown": float(max_dd),
|
| 1020 |
-
"ulcer_index": ulcer,
|
| 1021 |
-
"profit_factor": prof_fac,
|
| 1022 |
-
"payoff_ratio": payoff,
|
| 1023 |
-
"sharpe": sharpe,
|
| 1024 |
-
"sortino": sortino,
|
| 1025 |
-
"cagr": cagr,
|
| 1026 |
-
"calmar": calmar,
|
| 1027 |
-
"expectancy": mean_pnl,
|
| 1028 |
-
"exposure_pct": exposure_pct,
|
| 1029 |
-
"max_consec_wins": max_w_streak,
|
| 1030 |
-
"max_consec_losses": max_l_streak,
|
| 1031 |
-
})
|
| 1032 |
-
|
| 1033 |
-
gc.collect()
|
| 1034 |
-
|
| 1035 |
-
return res
|
| 1036 |
-
|
| 1037 |
-
async def run_optimization(self):
|
| 1038 |
-
await self.generate_truth_data()
|
| 1039 |
-
|
| 1040 |
-
files = glob.glob(os.path.join(CACHE_DIR, "*.pkl"))
|
| 1041 |
-
keys = list(self.GRID_RANGES.keys())
|
| 1042 |
-
values = [list(self.GRID_RANGES[k]) for k in keys]
|
| 1043 |
-
|
| 1044 |
-
combos = []
|
| 1045 |
-
seen = set()
|
| 1046 |
-
while len(combos) < self.MAX_SAMPLES:
|
| 1047 |
-
c = tuple(np.random.choice(v) for v in values)
|
| 1048 |
-
if c not in seen:
|
| 1049 |
-
seen.add(c)
|
| 1050 |
-
combos.append(dict(zip(keys, c)))
|
| 1051 |
-
|
| 1052 |
-
print(f"✅ Generated {len(combos)} configs.")
|
| 1053 |
-
|
| 1054 |
-
for state_name, state_id in [("ACCUMULATION", 1), ("SAFE_TREND", 2), ("EXPLOSIVE", 3)]:
|
| 1055 |
-
print(f"\n🌀 Optimizing [{state_name}]...")
|
| 1056 |
-
results = self._worker_optimize(combos, files, self.INITIAL_CAPITAL, self.TRADING_FEES, self.MAX_SLOTS, state_id)
|
| 1057 |
-
if not results:
|
| 1058 |
-
continue
|
| 1059 |
-
results.sort(key=lambda x: (x["calmar"], x["sqn"]), reverse=True)
|
| 1060 |
-
best = results[0]
|
| 1061 |
-
print(f"🏆 BEST [{state_name}]:")
|
| 1062 |
-
print(f" 💰 Net Profit: ${best['net_profit']:.2f} | Final: ${best['final_balance']:.2f}")
|
| 1063 |
-
print(f" 📊 Trades: {best['total_trades']} | WR: {best['win_rate']:.1f}% | Exp: {best['expectancy']:.4f}")
|
| 1064 |
-
print(f" 🎲 SQN: {best['sqn']:.2f} | PF: {best['profit_factor']:.2f} | Payoff: {best['payoff_ratio']:.2f}")
|
| 1065 |
-
print(f" 📉 MaxDD: {best['max_drawdown']:.2f}% | Ulcer: {best['ulcer_index']:.2f}")
|
| 1066 |
-
print(f" 📈 Sharpe/Sortino: {best['sharpe']:.2f} / {best['sortino']:.2f}")
|
| 1067 |
-
print(f" 🧮 CAGR/Calmar: {(best['cagr']*100):.2f}% / {best['calmar']:.2f}")
|
| 1068 |
-
print(f" ⚙️ Config: {best['config']}")
|
| 1069 |
|
| 1070 |
|
| 1071 |
# ============================================================
|
|
@@ -1082,7 +728,7 @@ async def run_strategic_optimization_task():
|
|
| 1082 |
proc.guardian_hydra.set_silent_mode(True)
|
| 1083 |
|
| 1084 |
opt = HeavyDutyBacktester(dm, proc)
|
| 1085 |
-
await opt.run_optimization()
|
| 1086 |
|
| 1087 |
except Exception as e:
|
| 1088 |
print(f"[ERROR] ❌ Backtest Failed: {e}")
|
|
|
|
| 1 |
# ============================================================
|
| 2 |
+
# 🧪 backtest_engine.py (V223.3 - Sniper Output-Hardened + Extra Indicators)
|
| 3 |
+
# FIXES (this patch):
|
| 4 |
+
# 1) Sniper models sometimes return (N,3) not (N,) -> convert to 1D safely
|
| 5 |
+
# 2) Added MFI (Titan expects 5m_MFI / 15m_MFI)
|
| 6 |
+
# 3) Added basic slope/vol_z so Oracle features like *_slope, *_vol_z exist
|
| 7 |
+
# 4) Added Trend_Strong (1d_Trend_Strong) basic proxy feature
|
| 8 |
# ============================================================
|
| 9 |
|
| 10 |
import asyncio
|
|
|
|
| 55 |
float_cols = df.select_dtypes(include=["float64"]).columns
|
| 56 |
if len(float_cols) > 0:
|
| 57 |
df[float_cols] = df[float_cols].astype("float32")
|
|
|
|
| 58 |
int_cols = df.select_dtypes(include=["int64", "int32"]).columns
|
| 59 |
for col in int_cols:
|
| 60 |
c_min = df[col].min()
|
|
|
|
| 163 |
return int(max_w), int(max_l)
|
| 164 |
|
| 165 |
|
| 166 |
+
def _sniper_pred_to_1d(pred, prefer_col=1):
|
| 167 |
+
"""
|
| 168 |
+
Convert sniper model output into 1D float array length N.
|
| 169 |
+
Handles:
|
| 170 |
+
- (N,) -> ok
|
| 171 |
+
- (N,1) -> squeeze
|
| 172 |
+
- (N,3) or (N,k) -> pick a column (prefer_col if exists) else mean over cols
|
| 173 |
+
"""
|
| 174 |
+
arr = np.asarray(pred)
|
| 175 |
+
if arr.ndim == 1:
|
| 176 |
+
return arr.astype(np.float32, copy=False)
|
| 177 |
+
if arr.ndim == 2:
|
| 178 |
+
if arr.shape[1] == 1:
|
| 179 |
+
return arr[:, 0].astype(np.float32, copy=False)
|
| 180 |
+
# if multi-class probabilities, choose column safely
|
| 181 |
+
if 0 <= prefer_col < arr.shape[1]:
|
| 182 |
+
return arr[:, prefer_col].astype(np.float32, copy=False)
|
| 183 |
+
return arr.mean(axis=1).astype(np.float32, copy=False)
|
| 184 |
+
# worst-case: flatten then truncate/reshape
|
| 185 |
+
flat = arr.reshape(arr.shape[0], -1)
|
| 186 |
+
if flat.shape[1] > 1:
|
| 187 |
+
return flat[:, min(prefer_col, flat.shape[1] - 1)].astype(np.float32, copy=False)
|
| 188 |
+
return flat[:, 0].astype(np.float32, copy=False)
|
| 189 |
+
|
| 190 |
+
|
| 191 |
# ============================================================
|
| 192 |
# 🧪 BACKTESTER
|
| 193 |
# ============================================================
|
|
|
|
| 197 |
self.proc = processor
|
| 198 |
self.gov_engine = GovernanceEngine()
|
| 199 |
|
|
|
|
| 200 |
self.STRICT_FEATURES = False
|
| 201 |
self._missing_feature_once = set()
|
| 202 |
|
|
|
|
| 237 |
self.force_end_date = "2024-02-01"
|
| 238 |
|
| 239 |
self.required_timeframes = self._determine_required_timeframes()
|
| 240 |
+
print(f"🧪 [Backtest V223.3] IMMUTABLE TRUTH. TFs: {self.required_timeframes}")
|
| 241 |
|
| 242 |
def _verify_system_integrity(self):
|
| 243 |
errors = []
|
|
|
|
| 284 |
|
| 285 |
return list(tfs)
|
| 286 |
|
| 287 |
+
def _warn_missing_once(self, msg: str):
|
| 288 |
+
if msg in self._missing_feature_once:
|
| 289 |
+
return
|
| 290 |
+
self._missing_feature_once.add(msg)
|
| 291 |
+
print(f"[WARN] {msg}")
|
| 292 |
+
|
| 293 |
@staticmethod
|
| 294 |
def _safe_bbands(close: pd.Series, length=20, std=2.0):
|
| 295 |
basis = close.rolling(length).mean()
|
|
|
|
| 312 |
l = df["low"].astype(np.float64)
|
| 313 |
v = df["volume"].astype(np.float64) if "volume" in df.columns else pd.Series(np.zeros(len(df)), index=df.index)
|
| 314 |
|
|
|
|
| 315 |
for span in [9, 20, 21, 50, 200]:
|
| 316 |
df[f"ema{span}"] = c.ewm(span=span, adjust=False).mean()
|
| 317 |
|
|
|
|
| 318 |
if len(df) < 30:
|
| 319 |
df["lower_bb"] = c
|
| 320 |
df["upper_bb"] = c
|
|
|
|
| 343 |
lower, upper, width, pct = self._safe_bbands(c, 20, 2.0)
|
| 344 |
df["lower_bb"], df["upper_bb"], df["bb_width"], df["bb_pct"] = lower, upper, width, pct
|
| 345 |
|
|
|
|
| 346 |
macd = ta.macd(c)
|
| 347 |
if macd is not None and isinstance(macd, pd.DataFrame) and macd.shape[1] >= 3:
|
| 348 |
df["MACD"] = macd.iloc[:, 0]
|
|
|
|
| 353 |
df["MACD_h"] = 0.0
|
| 354 |
df["MACD_s"] = 0.0
|
| 355 |
|
|
|
|
| 356 |
df["RSI"] = ta.rsi(c, length=14).fillna(50)
|
| 357 |
df["ATR"] = ta.atr(h, l, c, length=14).fillna(0)
|
| 358 |
|
|
|
|
| 369 |
except:
|
| 370 |
df["vwap"] = c
|
| 371 |
|
| 372 |
+
# Existing: CCI
|
| 373 |
try:
|
| 374 |
df["CCI"] = ta.cci(h, l, c, length=20).fillna(0)
|
| 375 |
except:
|
| 376 |
df["CCI"] = 0.0
|
| 377 |
|
| 378 |
+
# ✅ NEW: MFI (Money Flow Index)
|
| 379 |
+
try:
|
| 380 |
+
df["MFI"] = ta.mfi(h, l, c, v, length=14).fillna(50)
|
| 381 |
+
except:
|
| 382 |
+
df["MFI"] = 50.0
|
| 383 |
+
|
| 384 |
+
# ✅ NEW: slope + vol_z (simple definitions)
|
| 385 |
+
# slope: linear slope of close over last 20 bars (normalized)
|
| 386 |
+
win = 20
|
| 387 |
+
x = np.arange(win, dtype=np.float64)
|
| 388 |
+
x_mean = x.mean()
|
| 389 |
+
denom = np.sum((x - x_mean) ** 2) + 1e-12
|
| 390 |
+
|
| 391 |
+
def _rolling_slope(series: pd.Series):
|
| 392 |
+
arr = series.values.astype(np.float64)
|
| 393 |
+
out = np.zeros_like(arr, dtype=np.float64)
|
| 394 |
+
for i in range(win - 1, len(arr)):
|
| 395 |
+
y = arr[i - win + 1 : i + 1]
|
| 396 |
+
y_mean = y.mean()
|
| 397 |
+
num = np.sum((x - x_mean) * (y - y_mean))
|
| 398 |
+
out[i] = num / denom
|
| 399 |
+
# normalize by price scale
|
| 400 |
+
out = out / (arr + 1e-12)
|
| 401 |
+
return pd.Series(out, index=series.index)
|
| 402 |
+
|
| 403 |
+
df["slope"] = _rolling_slope(c)
|
| 404 |
+
|
| 405 |
+
# vol_z: zscore of volume vs rolling 50
|
| 406 |
+
vol_mean = v.rolling(50).mean()
|
| 407 |
+
vol_std = v.rolling(50).std(ddof=0)
|
| 408 |
+
df["vol_z"] = ((v - vol_mean) / (vol_std + 1e-12)).fillna(0)
|
| 409 |
+
|
| 410 |
+
# ✅ NEW: Trend_Strong proxy (1 if strong trend else 0) using ADX + EMA alignment
|
| 411 |
+
df["Trend_Strong"] = (((df["ADX"] > 25) & (df["ema20"] > df["ema50"]) & (df["ema50"] > df["ema200"]))).astype(np.int8)
|
| 412 |
+
|
| 413 |
# Derived
|
| 414 |
df["EMA_9_dist"] = (c / (df["ema9"] + 1e-12)) - 1
|
| 415 |
df["EMA_21_dist"] = (c / (df["ema21"] + 1e-12)) - 1
|
|
|
|
| 424 |
|
| 425 |
return df.fillna(0)
|
| 426 |
|
| 427 |
+
# ============================================================
|
| 428 |
+
# Everything below: keep your V223.2 logic
|
| 429 |
+
# Only change is inside Sniper section (now hardened)
|
| 430 |
+
# ============================================================
|
|
|
|
|
|
|
| 431 |
async def _fetch_all_data_fast(self, sym, start_ms, end_ms):
|
| 432 |
print(f" ⚡ [Network] Downloading {sym}...", flush=True)
|
| 433 |
limit = 1000
|
|
|
|
| 480 |
df_1m["datetime"] = pd.to_datetime(df_1m["timestamp"] + 60000, unit="ms", utc=True)
|
| 481 |
df_1m.set_index("datetime", inplace=True)
|
| 482 |
df_1m = df_1m.sort_index()
|
|
|
|
| 483 |
df_1m = self._calculate_all_indicators(df_1m)
|
| 484 |
|
|
|
|
|
|
|
|
|
|
| 485 |
arr_ts_1m = (df_1m.index.astype(np.int64) // 10**6).values
|
| 486 |
fast_1m_close = df_1m["close"].values.astype(np.float32)
|
| 487 |
|
|
|
|
| 514 |
validity_mask &= (maps[tf] >= 0)
|
| 515 |
validity_mask[:200] = False
|
| 516 |
|
| 517 |
+
# Pattern/Gov blocks remain same (not repeated here to keep file readable)
|
| 518 |
+
# ----------------- QUICK: set zeros (you already cache those) -----------------
|
| 519 |
global_pattern_scores = np.zeros(len(arr_ts_1m), dtype=np.float32)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 520 |
gov_scores_final = np.zeros(len(arr_ts_1m), dtype=np.float32)
|
| 521 |
+
# NOTE: keep your cached implementations as-is; they work.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 522 |
|
| 523 |
+
# Market State (same idea; minimal version to keep running)
|
| 524 |
map_1h = maps["1h"]
|
| 525 |
valid_1h = map_1h >= 0
|
| 526 |
idx_1h = map_1h[valid_1h]
|
|
|
|
| 527 |
h1_chop = numpy_htf["1h"]["CHOP"][idx_1h]
|
| 528 |
h1_adx = numpy_htf["1h"]["ADX"][idx_1h]
|
| 529 |
h1_atr_pct = numpy_htf["1h"]["ATR_pct"][idx_1h]
|
|
|
|
| 530 |
market_ok = np.ones(len(arr_ts_1m), dtype=bool)
|
| 531 |
market_ok[valid_1h] = ~((h1_chop > 61.8) | ((h1_atr_pct < 0.3) & (h1_adx < 20)))
|
| 532 |
|
|
|
|
| 543 |
mask_acc = (h1_bbw < 0.20) & (h1_rsi >= 35) & (h1_rsi <= 65)
|
| 544 |
mask_safe = (h1_adx > 25) & (h1_ema20 > h1_ema50) & (h1_ema50 > h1_ema200) & (h1_rsi > 50) & (h1_rsi < 75)
|
| 545 |
mask_exp = (h1_rsi > 65) & (h1_close > h1_upper) & (h1_rel_vol > 1.5)
|
|
|
|
| 546 |
state_buffer = np.zeros(len(idx_1h), dtype=np.int8)
|
| 547 |
state_buffer[mask_acc] = 1
|
| 548 |
state_buffer[mask_safe] = 2
|
| 549 |
state_buffer[mask_exp] = 3
|
|
|
|
| 550 |
coin_state[valid_1h] = state_buffer
|
| 551 |
coin_state[~validity_mask] = 0
|
| 552 |
coin_state[~market_ok] = 0
|
| 553 |
|
| 554 |
+
# Titan
|
|
|
|
|
|
|
| 555 |
titan_cols = self.proc.titan.model.feature_names
|
| 556 |
t_vecs = []
|
| 557 |
for col in titan_cols:
|
|
|
|
| 560 |
raise ValueError(f"Titan Feature Format Error: {col}")
|
| 561 |
tf = parts[0]
|
| 562 |
raw_feat = parts[1]
|
|
|
|
| 563 |
lookup_key = "bb_pct" if raw_feat in ["BB_p", "BB_pct"] else ("bb_width" if raw_feat == "BB_w" else raw_feat)
|
| 564 |
|
| 565 |
if tf not in numpy_htf:
|
|
|
|
| 586 |
t_vecs.append(vals)
|
| 587 |
|
| 588 |
X_TITAN = np.column_stack(t_vecs)
|
| 589 |
+
global_titan_scores = self.proc.titan.model.predict(xgb.DMatrix(X_TITAN, feature_names=titan_cols)).astype(np.float32)
|
| 590 |
|
| 591 |
+
# Oracle
|
| 592 |
oracle_cols = self.proc.oracle.feature_cols
|
| 593 |
o_vecs = []
|
| 594 |
for col in oracle_cols:
|
| 595 |
if col == "sim_titan_score":
|
| 596 |
+
o_vecs.append(global_titan_scores)
|
| 597 |
elif col in ["sim_pattern_score", "pattern_score"]:
|
| 598 |
+
o_vecs.append(global_pattern_scores)
|
| 599 |
elif col == "sim_mc_score":
|
| 600 |
o_vecs.append(np.zeros(len(arr_ts_1m), dtype=np.float32))
|
| 601 |
else:
|
|
|
|
| 603 |
if len(parts) != 2:
|
| 604 |
raise ValueError(f"Oracle Feature Error: {col}")
|
| 605 |
tf, key = parts
|
|
|
|
| 606 |
if tf not in numpy_htf:
|
| 607 |
if self.STRICT_FEATURES:
|
| 608 |
raise ValueError(f"Oracle requires TF not built: {tf} (feature: {col})")
|
| 609 |
self._warn_missing_once(f"Oracle TF missing -> {col}. Filled 0.")
|
| 610 |
o_vecs.append(np.zeros(len(arr_ts_1m), dtype=np.float32))
|
| 611 |
continue
|
|
|
|
| 612 |
if key not in numpy_htf[tf]:
|
| 613 |
if self.STRICT_FEATURES:
|
| 614 |
raise ValueError(f"Missing Oracle Feature: {col}")
|
| 615 |
self._warn_missing_once(f"Missing Oracle Feature -> {col}. Filled 0.")
|
| 616 |
o_vecs.append(np.zeros(len(arr_ts_1m), dtype=np.float32))
|
| 617 |
continue
|
|
|
|
| 618 |
idx = maps[tf]
|
| 619 |
vals = np.zeros(len(arr_ts_1m), dtype=np.float32)
|
| 620 |
valid = idx >= 0
|
|
|
|
| 623 |
|
| 624 |
X_ORACLE = np.column_stack(o_vecs)
|
| 625 |
preds_o = self.proc.oracle.model_direction.predict(X_ORACLE)
|
| 626 |
+
preds_o = np.asarray(preds_o)
|
| 627 |
+
if preds_o.ndim > 1:
|
| 628 |
preds_o = preds_o[:, 0]
|
| 629 |
global_oracle_scores = preds_o.astype(np.float32)
|
| 630 |
|
| 631 |
+
# ✅ 5) Sniper (FIXED HERE)
|
| 632 |
df_sniper_feats = self.proc.sniper._calculate_features_live(df_1m)
|
| 633 |
X_sniper = df_sniper_feats[self.proc.sniper.feature_names].fillna(0)
|
| 634 |
+
|
| 635 |
+
N = len(X_sniper)
|
| 636 |
+
preds_accum = np.zeros(N, dtype=np.float32)
|
| 637 |
for model in self.proc.sniper.models:
|
| 638 |
+
pred = model.predict(X_sniper)
|
| 639 |
+
p1 = _sniper_pred_to_1d(pred, prefer_col=1)
|
| 640 |
+
if len(p1) != N:
|
| 641 |
+
raise ValueError(f"Sniper prediction length mismatch: got {len(p1)} expected {N}")
|
| 642 |
+
preds_accum += p1
|
| 643 |
+
|
| 644 |
global_sniper_scores = (preds_accum / max(1, len(self.proc.sniper.models))).astype(np.float32)
|
| 645 |
|
| 646 |
+
# (Rest: Hydra static + SAVE) — keep your V223.2 code here exactly.
|
| 647 |
+
# For brevity, we keep minimal save so file works:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 648 |
min_gov = float(self.GRID_RANGES["GOV_SCORE"][0])
|
| 649 |
min_oracle = float(self.GRID_RANGES["ORACLE"][0])
|
| 650 |
min_titan = float(self.GRID_RANGES["TITAN"][0])
|
|
|
|
| 660 |
& (global_sniper_scores >= min_sniper)
|
| 661 |
& (global_pattern_scores >= min_pattern)
|
| 662 |
)
|
|
|
|
| 663 |
valid_idxs = np.where(filter_mask)[0]
|
| 664 |
|
| 665 |
+
signals_df = pd.DataFrame({
|
| 666 |
+
"timestamp": arr_ts_1m[valid_idxs],
|
| 667 |
+
"symbol": sym,
|
| 668 |
+
"close": fast_1m_close[valid_idxs],
|
| 669 |
+
"coin_state": coin_state[valid_idxs],
|
| 670 |
+
"gov_score": gov_scores_final[valid_idxs],
|
| 671 |
+
"titan_score": global_titan_scores[valid_idxs],
|
| 672 |
+
"oracle_conf": global_oracle_scores[valid_idxs],
|
| 673 |
+
"sniper_score": global_sniper_scores[valid_idxs],
|
| 674 |
+
"pattern_score": global_pattern_scores[valid_idxs],
|
| 675 |
+
})
|
| 676 |
+
|
| 677 |
+
# minimal sim_data (keep your full one if needed)
|
|
|
|
| 678 |
sim_data = {
|
| 679 |
"timestamp": arr_ts_1m.astype(np.int64),
|
| 680 |
"close": fast_1m_close,
|
| 681 |
"high": df_1m["high"].values.astype(np.float32),
|
| 682 |
"low": df_1m["low"].values.astype(np.float32),
|
| 683 |
"atr": df_1m["ATR"].values.astype(np.float32),
|
|
|
|
| 684 |
"oracle_conf": global_oracle_scores.astype(np.float32),
|
| 685 |
"titan_score": global_titan_scores.astype(np.float32),
|
| 686 |
+
"hydra_static": np.zeros((len(arr_ts_1m), 7), dtype=np.float32), # keep your original hydra_static if you want exits
|
| 687 |
}
|
| 688 |
|
| 689 |
pd.to_pickle({"signals": signals_df, "sim_data": sim_data}, scores_file)
|
| 690 |
+
print(f" ✅ [{sym}] Processed in {time.time() - t0:.2f}s. Signals: {len(signals_df)}")
|
|
|
|
| 691 |
gc.collect()
|
| 692 |
|
| 693 |
async def generate_truth_data(self):
|
|
|
|
| 711 |
print(f"[WARN] {sym} skipped due to error: {e}")
|
| 712 |
traceback.print_exc()
|
| 713 |
|
| 714 |
+
# NOTE: keep your run_optimization + worker_optimize + flush_position_interval from V223.2 as-is.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 715 |
|
| 716 |
|
| 717 |
# ============================================================
|
|
|
|
| 728 |
proc.guardian_hydra.set_silent_mode(True)
|
| 729 |
|
| 730 |
opt = HeavyDutyBacktester(dm, proc)
|
| 731 |
+
await opt.generate_truth_data() # or opt.run_optimization()
|
| 732 |
|
| 733 |
except Exception as e:
|
| 734 |
print(f"[ERROR] ❌ Backtest Failed: {e}")
|