588 lines
26 KiB
Python
588 lines
26 KiB
Python
|
|
"""OB Intelligence — 4-Subsystem Test Suite.
|
||
|
|
|
||
|
|
Phase 1: Data validation (CSVOBProvider)
|
||
|
|
Phase 2: Feature computation (all 4 sub-systems)
|
||
|
|
Phase 3: Signal quality (forward return correlation)
|
||
|
|
Phase 4: Per-asset placement quality
|
||
|
|
Phase 5: Market-wide detection
|
||
|
|
Phase 6: Macro regime detection
|
||
|
|
Phase 7: Engine integration (no regression vs baseline)
|
||
|
|
"""
|
||
|
|
import sys, time, math
|
||
|
|
from pathlib import Path
|
||
|
|
import numpy as np
|
||
|
|
import pandas as pd
|
||
|
|
|
||
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
||
|
|
|
||
|
|
OB_DATA_DIR = Path(r"C:\Users\Lenovo\Documents\- DOLPHIN NG HD HCM TSF Predict\ob_data")
|
||
|
|
VBT_DIR = Path(r"C:\Users\Lenovo\Documents\- DOLPHIN NG HD HCM TSF Predict\vbt_cache")
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# Phase 0: Compile numba kernels
|
||
|
|
# ============================================================================
|
||
|
|
print("=" * 70)
|
||
|
|
print(" OB INTELLIGENCE 4-SUBSYSTEM TEST SUITE")
|
||
|
|
print("=" * 70)
|
||
|
|
print("\nPhase 0: Compiling numba kernels...")
|
||
|
|
t0c = time.time()
|
||
|
|
|
||
|
|
from nautilus_dolphin.nautilus.alpha_asset_selector import compute_irp_nb, compute_ars_nb, rank_assets_irp_nb
|
||
|
|
from nautilus_dolphin.nautilus.alpha_bet_sizer import compute_sizing_nb
|
||
|
|
from nautilus_dolphin.nautilus.alpha_signal_generator import check_dc_nb
|
||
|
|
from nautilus_dolphin.nautilus.ob_features import (
|
||
|
|
compute_imbalance_nb, compute_depth_1pct_nb, compute_depth_quality_nb,
|
||
|
|
compute_fill_probability_nb, compute_spread_proxy_nb, compute_depth_asymmetry_nb,
|
||
|
|
compute_imbalance_persistence_nb, compute_withdrawal_velocity_nb,
|
||
|
|
compute_market_agreement_nb, compute_cascade_signal_nb,
|
||
|
|
OBPlacementFeatures, OBSignalFeatures, OBMarketFeatures, OBMacroFeatures,
|
||
|
|
NEUTRAL_PLACEMENT, NEUTRAL_SIGNAL, NEUTRAL_MARKET, NEUTRAL_MACRO,
|
||
|
|
OBFeatureEngine,
|
||
|
|
)
|
||
|
|
from nautilus_dolphin.nautilus.ob_provider import CSVOBProvider, MockOBProvider
|
||
|
|
from nautilus_dolphin.nautilus.ob_placer import OBPlacer
|
||
|
|
|
||
|
|
# Warmup JIT
|
||
|
|
_p = np.array([1.0, 2.0, 3.0], dtype=np.float64)
|
||
|
|
compute_irp_nb(_p, -1); compute_ars_nb(1.0, 0.5, 0.01)
|
||
|
|
rank_assets_irp_nb(np.ones((10, 2), dtype=np.float64), 8, -1, 5, 500.0, 20, 0.20)
|
||
|
|
compute_sizing_nb(-0.03, -0.02, -0.05, 3.0, 0.5, 5.0, 0.20, True, True, 0.0,
|
||
|
|
np.zeros(4, dtype=np.int64), np.zeros(4, dtype=np.int64),
|
||
|
|
np.zeros(5, dtype=np.float64), 0, -1, 0.01, 0.04)
|
||
|
|
check_dc_nb(_p, 3, 1, 0.75)
|
||
|
|
|
||
|
|
# Warmup OB kernels
|
||
|
|
_b = np.array([100.0, 200.0, 300.0, 400.0, 500.0], dtype=np.float64)
|
||
|
|
_a = np.array([110.0, 190.0, 310.0, 390.0, 510.0], dtype=np.float64)
|
||
|
|
compute_imbalance_nb(_b, _a)
|
||
|
|
compute_depth_1pct_nb(_b, _a)
|
||
|
|
compute_depth_quality_nb(210.0, 200.0)
|
||
|
|
compute_fill_probability_nb(1.0)
|
||
|
|
compute_spread_proxy_nb(_b, _a)
|
||
|
|
compute_depth_asymmetry_nb(_b, _a)
|
||
|
|
compute_imbalance_persistence_nb(np.array([0.1, -0.1, 0.2, 0.3], dtype=np.float64), 4)
|
||
|
|
compute_withdrawal_velocity_nb(np.array([100.0, 110.0, 105.0], dtype=np.float64), 2)
|
||
|
|
compute_market_agreement_nb(np.array([0.1, -0.05, 0.2, 0.15], dtype=np.float64), 4)
|
||
|
|
compute_cascade_signal_nb(np.array([-0.05, -0.15, 0.02, -0.12], dtype=np.float64), 4, -0.10)
|
||
|
|
|
||
|
|
print(f" JIT compile: {time.time() - t0c:.1f}s")
|
||
|
|
|
||
|
|
from nautilus_dolphin.nautilus.alpha_orchestrator import NDAlphaEngine
|
||
|
|
from nautilus_dolphin.nautilus.adaptive_circuit_breaker import AdaptiveCircuitBreaker
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# Phase 1: Data Validation
|
||
|
|
# ============================================================================
|
||
|
|
print(f"\n{'='*70}")
|
||
|
|
print(" PHASE 1: DATA VALIDATION (CSVOBProvider)")
|
||
|
|
print(f"{'='*70}")
|
||
|
|
|
||
|
|
csv_provider = CSVOBProvider(str(OB_DATA_DIR))
|
||
|
|
csv_assets = csv_provider.get_assets()
|
||
|
|
print(f"\n Available assets: {csv_assets}")
|
||
|
|
|
||
|
|
phase1_ok = True
|
||
|
|
for asset in csv_assets:
|
||
|
|
ts = csv_provider.get_all_timestamps(asset)
|
||
|
|
n = len(ts)
|
||
|
|
snap0 = csv_provider.get_snapshot(asset, ts[0]) if n > 0 else None
|
||
|
|
snap_last = csv_provider.get_snapshot(asset, ts[-1]) if n > 0 else None
|
||
|
|
|
||
|
|
# Check integrity
|
||
|
|
nan_count = 0
|
||
|
|
neg_count = 0
|
||
|
|
for i in range(min(n, 100)): # Sample first 100
|
||
|
|
s = csv_provider.get_snapshot(asset, ts[i])
|
||
|
|
if s is None:
|
||
|
|
continue
|
||
|
|
if np.any(np.isnan(s.bid_notional)) or np.any(np.isnan(s.ask_notional)):
|
||
|
|
nan_count += 1
|
||
|
|
if np.any(s.bid_notional < 0) or np.any(s.ask_notional < 0):
|
||
|
|
neg_count += 1
|
||
|
|
|
||
|
|
t_range = (ts[-1] - ts[0]) / 3600 if n > 1 else 0
|
||
|
|
print(f"\n {asset}:")
|
||
|
|
print(f" Snapshots: {n}")
|
||
|
|
print(f" Time range: {t_range:.1f} hours")
|
||
|
|
if snap0:
|
||
|
|
print(f" Bid depth (1%): ${snap0.bid_notional[0]:,.0f}")
|
||
|
|
print(f" Ask depth (1%): ${snap0.ask_notional[0]:,.0f}")
|
||
|
|
print(f" Total 5-level: ${sum(snap0.bid_notional) + sum(snap0.ask_notional):,.0f}")
|
||
|
|
if nan_count > 0:
|
||
|
|
print(f" WARNING: {nan_count} snapshots with NaN")
|
||
|
|
phase1_ok = False
|
||
|
|
if neg_count > 0:
|
||
|
|
print(f" WARNING: {neg_count} snapshots with negative notional")
|
||
|
|
phase1_ok = False
|
||
|
|
if n == 0:
|
||
|
|
print(f" WARNING: No data")
|
||
|
|
phase1_ok = False
|
||
|
|
|
||
|
|
print(f"\n Phase 1: {'PASS' if phase1_ok else 'FAIL'}")
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# Phase 2: Feature Computation (All 4 Sub-systems)
|
||
|
|
# ============================================================================
|
||
|
|
print(f"\n{'='*70}")
|
||
|
|
print(" PHASE 2: FEATURE COMPUTATION (4 SUB-SYSTEMS)")
|
||
|
|
print(f"{'='*70}")
|
||
|
|
|
||
|
|
t2 = time.time()
|
||
|
|
# Use CSVOBProvider for real data feature computation
|
||
|
|
ob_engine_csv = OBFeatureEngine(csv_provider)
|
||
|
|
if csv_assets:
|
||
|
|
ob_engine_csv.preload_date("2025-01-15", csv_assets)
|
||
|
|
print(f"\n Preload time: {time.time() - t2:.2f}s")
|
||
|
|
|
||
|
|
phase2_ok = True
|
||
|
|
for asset in csv_assets:
|
||
|
|
n = csv_provider.get_snapshot_count(asset)
|
||
|
|
if n == 0:
|
||
|
|
continue
|
||
|
|
|
||
|
|
# Collect features across all snapshots
|
||
|
|
imbalances = []
|
||
|
|
imbalances_ma5 = []
|
||
|
|
persistences = []
|
||
|
|
depth_qualities = []
|
||
|
|
fill_probs = []
|
||
|
|
spreads = []
|
||
|
|
|
||
|
|
for snap_idx in range(n):
|
||
|
|
p = ob_engine_csv._preloaded_placement.get(asset, {}).get(snap_idx)
|
||
|
|
s = ob_engine_csv._preloaded_signal.get(asset, {}).get(snap_idx)
|
||
|
|
if p:
|
||
|
|
depth_qualities.append(p.depth_quality)
|
||
|
|
fill_probs.append(p.fill_probability)
|
||
|
|
spreads.append(p.spread_proxy_bps)
|
||
|
|
if s:
|
||
|
|
imbalances.append(s.imbalance)
|
||
|
|
imbalances_ma5.append(s.imbalance_ma5)
|
||
|
|
persistences.append(s.imbalance_persistence)
|
||
|
|
|
||
|
|
print(f"\n {asset} Feature Distributions:")
|
||
|
|
if imbalances:
|
||
|
|
imb = np.array(imbalances)
|
||
|
|
print(f" Sub-2 Imbalance: mean={np.mean(imb):.4f} std={np.std(imb):.4f} "
|
||
|
|
f"p5={np.percentile(imb,5):.4f} p50={np.percentile(imb,50):.4f} p95={np.percentile(imb,95):.4f}")
|
||
|
|
# Validate ranges
|
||
|
|
if np.min(imb) < -1.0 or np.max(imb) > 1.0:
|
||
|
|
print(f" FAIL: imbalance out of [-1, 1]")
|
||
|
|
phase2_ok = False
|
||
|
|
if persistences:
|
||
|
|
per = np.array(persistences)
|
||
|
|
print(f" Sub-2 Persistence: mean={np.mean(per):.4f} std={np.std(per):.4f} "
|
||
|
|
f"p5={np.percentile(per,5):.4f} p50={np.percentile(per,50):.4f} p95={np.percentile(per,95):.4f}")
|
||
|
|
if np.min(per) < 0 or np.max(per) > 1.0:
|
||
|
|
print(f" FAIL: persistence out of [0, 1]")
|
||
|
|
phase2_ok = False
|
||
|
|
if depth_qualities:
|
||
|
|
dq = np.array(depth_qualities)
|
||
|
|
print(f" Sub-1 DepthQual: mean={np.mean(dq):.4f} std={np.std(dq):.4f} "
|
||
|
|
f"p5={np.percentile(dq,5):.4f} p50={np.percentile(dq,50):.4f} p95={np.percentile(dq,95):.4f}")
|
||
|
|
if np.min(dq) < 0:
|
||
|
|
print(f" FAIL: depth_quality < 0")
|
||
|
|
phase2_ok = False
|
||
|
|
if fill_probs:
|
||
|
|
fp = np.array(fill_probs)
|
||
|
|
print(f" Sub-1 FillProb: mean={np.mean(fp):.4f} std={np.std(fp):.4f} "
|
||
|
|
f"p5={np.percentile(fp,5):.4f} p50={np.percentile(fp,50):.4f} p95={np.percentile(fp,95):.4f}")
|
||
|
|
if np.min(fp) < 0 or np.max(fp) > 1.0:
|
||
|
|
print(f" FAIL: fill_probability out of [0, 1]")
|
||
|
|
phase2_ok = False
|
||
|
|
|
||
|
|
# Market-wide features (Sub-3)
|
||
|
|
print(f"\n Sub-3 Market-Wide Features:")
|
||
|
|
mkt_med_imb = []
|
||
|
|
mkt_agree = []
|
||
|
|
n_ref = csv_provider.get_snapshot_count(csv_assets[0]) if csv_assets else 0
|
||
|
|
for snap_idx in range(n_ref):
|
||
|
|
m = ob_engine_csv._preloaded_market.get(snap_idx)
|
||
|
|
if m:
|
||
|
|
mkt_med_imb.append(m.median_imbalance)
|
||
|
|
mkt_agree.append(m.agreement_pct)
|
||
|
|
|
||
|
|
if mkt_med_imb:
|
||
|
|
mi = np.array(mkt_med_imb)
|
||
|
|
ag = np.array(mkt_agree)
|
||
|
|
print(f" Median imbalance: mean={np.mean(mi):.4f} std={np.std(mi):.4f}")
|
||
|
|
print(f" Agreement pct: mean={np.mean(ag):.4f} std={np.std(ag):.4f} "
|
||
|
|
f"p5={np.percentile(ag,5):.4f} p95={np.percentile(ag,95):.4f}")
|
||
|
|
high_agree = np.sum(ag > 0.8) / len(ag) * 100
|
||
|
|
print(f" Snapshots with agreement > 0.8: {high_agree:.1f}%")
|
||
|
|
if np.min(ag) < 0 or np.max(ag) > 1.0:
|
||
|
|
print(f" FAIL: agreement out of [0, 1]")
|
||
|
|
phase2_ok = False
|
||
|
|
|
||
|
|
# Sub-4 Macro
|
||
|
|
macro = ob_engine_csv.get_macro()
|
||
|
|
print(f"\n Sub-4 Macro Regime (final state):")
|
||
|
|
print(f" Depth velocity: {macro.depth_velocity:.4f}")
|
||
|
|
print(f" Cascade count: {macro.cascade_count}")
|
||
|
|
print(f" Acceleration: {macro.acceleration:.6f}")
|
||
|
|
print(f" Regime signal: {macro.regime_signal} ({['CALM','NEUTRAL','STRESS'][macro.regime_signal + 1]})")
|
||
|
|
|
||
|
|
print(f"\n Phase 2: {'PASS' if phase2_ok else 'FAIL'}")
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# Phase 3: Signal Quality (OB Feature Correlation with Itself)
|
||
|
|
# ============================================================================
|
||
|
|
print(f"\n{'='*70}")
|
||
|
|
print(" PHASE 3: OB FEATURE INTERNAL CONSISTENCY")
|
||
|
|
print(f"{'='*70}")
|
||
|
|
|
||
|
|
# Check that imbalance and imbalance_ma5 are correlated (sanity check)
|
||
|
|
if csv_assets:
|
||
|
|
asset = csv_assets[0]
|
||
|
|
n = csv_provider.get_snapshot_count(asset)
|
||
|
|
raw_imb = []
|
||
|
|
ma5_imb = []
|
||
|
|
for snap_idx in range(n):
|
||
|
|
s = ob_engine_csv._preloaded_signal.get(asset, {}).get(snap_idx)
|
||
|
|
if s:
|
||
|
|
raw_imb.append(s.imbalance)
|
||
|
|
ma5_imb.append(s.imbalance_ma5)
|
||
|
|
if len(raw_imb) > 10:
|
||
|
|
from scipy import stats
|
||
|
|
corr, pval = stats.spearmanr(raw_imb, ma5_imb)
|
||
|
|
print(f"\n {asset}: imbalance vs imbalance_ma5 Spearman r={corr:.4f} (p={pval:.2e})")
|
||
|
|
if corr < 0.5:
|
||
|
|
print(f" WARNING: Low correlation between raw and smoothed imbalance")
|
||
|
|
|
||
|
|
# Check fill_prob vs depth_quality monotonicity
|
||
|
|
dqs = []
|
||
|
|
fps = []
|
||
|
|
for snap_idx in range(n):
|
||
|
|
p = ob_engine_csv._preloaded_placement.get(asset, {}).get(snap_idx)
|
||
|
|
if p:
|
||
|
|
dqs.append(p.depth_quality)
|
||
|
|
fps.append(p.fill_probability)
|
||
|
|
if len(dqs) > 10:
|
||
|
|
corr2, pval2 = stats.spearmanr(dqs, fps)
|
||
|
|
print(f" {asset}: depth_quality vs fill_prob Spearman r={corr2:.4f} (p={pval2:.2e})")
|
||
|
|
if corr2 < 0.9:
|
||
|
|
print(f" WARNING: fill_prob should be monotonically related to depth_quality")
|
||
|
|
|
||
|
|
print(f"\n Phase 3: PASS (informational)")
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# Phase 4: Per-Asset Placement Quality
|
||
|
|
# ============================================================================
|
||
|
|
print(f"\n{'='*70}")
|
||
|
|
print(" PHASE 4: PER-ASSET PLACEMENT QUALITY")
|
||
|
|
print(f"{'='*70}")
|
||
|
|
|
||
|
|
for asset in csv_assets:
|
||
|
|
n = csv_provider.get_snapshot_count(asset)
|
||
|
|
if n == 0:
|
||
|
|
continue
|
||
|
|
dqs = [ob_engine_csv._preloaded_placement.get(asset, {}).get(i) for i in range(n)]
|
||
|
|
dqs = [d for d in dqs if d is not None]
|
||
|
|
mean_dq = np.mean([d.depth_quality for d in dqs])
|
||
|
|
mean_fp = np.mean([d.fill_probability for d in dqs])
|
||
|
|
mean_sp = np.mean([d.spread_proxy_bps for d in dqs])
|
||
|
|
print(f"\n {asset}:")
|
||
|
|
print(f" Mean depth quality: {mean_dq:.3f}")
|
||
|
|
print(f" Mean fill prob: {mean_fp:.3f}")
|
||
|
|
print(f" Mean spread proxy: {mean_sp:.3f} bps")
|
||
|
|
|
||
|
|
# SmartPlacer advice test
|
||
|
|
print(f"\n SmartPlacer Advice (sample):")
|
||
|
|
placer = OBPlacer()
|
||
|
|
if csv_assets:
|
||
|
|
asset = csv_assets[0]
|
||
|
|
n = csv_provider.get_snapshot_count(asset)
|
||
|
|
mid = n // 2
|
||
|
|
p = ob_engine_csv._preloaded_placement.get(asset, {}).get(mid, NEUTRAL_PLACEMENT)
|
||
|
|
s = ob_engine_csv._preloaded_signal.get(asset, {}).get(mid, NEUTRAL_SIGNAL)
|
||
|
|
for conf in [0.3, 0.5, 0.7, 0.9]:
|
||
|
|
advice = placer.advise(p, s, signal_confidence=conf, direction=-1)
|
||
|
|
print(f" conf={conf:.1f}: {advice.method:15s} offset={advice.offset_bps:.2f}bps "
|
||
|
|
f"timeout={advice.timeout_s:.0f}s reason={advice.reason}")
|
||
|
|
|
||
|
|
print(f"\n Phase 4: PASS (informational)")
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# Phase 5: Market-Wide Agreement Detection
|
||
|
|
# ============================================================================
|
||
|
|
print(f"\n{'='*70}")
|
||
|
|
print(" PHASE 5: MARKET-WIDE AGREEMENT DETECTION")
|
||
|
|
print(f"{'='*70}")
|
||
|
|
|
||
|
|
if mkt_agree:
|
||
|
|
ag = np.array(mkt_agree)
|
||
|
|
mi = np.array(mkt_med_imb)
|
||
|
|
|
||
|
|
# Top 10 high-agreement moments
|
||
|
|
sorted_idx = np.argsort(ag)[::-1]
|
||
|
|
print(f"\n Top 10 high-agreement snapshots:")
|
||
|
|
for i in range(min(10, len(sorted_idx))):
|
||
|
|
idx = sorted_idx[i]
|
||
|
|
print(f" snap {idx:5d}: agreement={ag[idx]:.3f} median_imb={mi[idx]:+.4f}")
|
||
|
|
|
||
|
|
# Distribution of agreement
|
||
|
|
print(f"\n Agreement distribution:")
|
||
|
|
for thresh in [0.5, 0.6, 0.7, 0.8, 0.9, 1.0]:
|
||
|
|
pct = np.sum(ag >= thresh) / len(ag) * 100
|
||
|
|
print(f" >= {thresh:.1f}: {pct:5.1f}%")
|
||
|
|
|
||
|
|
print(f"\n Phase 5: PASS (informational)")
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# Phase 6: Macro Regime Detection (Sub-4)
|
||
|
|
# ============================================================================
|
||
|
|
print(f"\n{'='*70}")
|
||
|
|
print(" PHASE 6: MACRO REGIME DETECTION")
|
||
|
|
print(f"{'='*70}")
|
||
|
|
|
||
|
|
# Trace macro features through time using the preloaded data
|
||
|
|
# We can examine the final macro state and trace snapshots for regime transitions
|
||
|
|
print(f"\n Final macro state:")
|
||
|
|
print(f" Depth velocity: {macro.depth_velocity:+.4f}")
|
||
|
|
print(f" Cascade count: {macro.cascade_count}")
|
||
|
|
print(f" Regime signal: {macro.regime_signal}")
|
||
|
|
|
||
|
|
# Simulate macro tracking across snapshots using Mock
|
||
|
|
mock_stress = MockOBProvider(imbalance_bias=-0.3, depth_scale=0.3, assets=csv_assets[:4] or ["BTCUSDT", "ETHUSDT"])
|
||
|
|
mock_calm = MockOBProvider(imbalance_bias=0.1, depth_scale=2.0, assets=csv_assets[:4] or ["BTCUSDT", "ETHUSDT"])
|
||
|
|
mock_neutral = MockOBProvider(imbalance_bias=0.0, depth_scale=1.0, assets=csv_assets[:4] or ["BTCUSDT", "ETHUSDT"])
|
||
|
|
|
||
|
|
for label, provider in [("STRESS", mock_stress), ("CALM", mock_calm), ("NEUTRAL", mock_neutral)]:
|
||
|
|
engine = OBFeatureEngine(provider)
|
||
|
|
engine.preload_date("test", provider.get_assets())
|
||
|
|
m = engine.get_macro()
|
||
|
|
print(f"\n Mock {label}: regime={m.regime_signal} velocity={m.depth_velocity:+.4f} cascade={m.cascade_count}")
|
||
|
|
|
||
|
|
print(f"\n Phase 6: PASS (informational)")
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# Phase 7: Engine Integration (No Regression)
|
||
|
|
# ============================================================================
|
||
|
|
print(f"\n{'='*70}")
|
||
|
|
print(" PHASE 7: ENGINE INTEGRATION (BASELINE REGRESSION CHECK)")
|
||
|
|
print(f"{'='*70}")
|
||
|
|
|
||
|
|
META_COLS = {'timestamp', 'scan_number', 'v50_lambda_max_velocity', 'v150_lambda_max_velocity',
|
||
|
|
'v300_lambda_max_velocity', 'v750_lambda_max_velocity', 'vel_div',
|
||
|
|
'instability_50', 'instability_150'}
|
||
|
|
ENGINE_KWARGS = dict(
|
||
|
|
initial_capital=25000.0, vel_div_threshold=-0.02, vel_div_extreme=-0.05,
|
||
|
|
min_leverage=0.5, max_leverage=5.0, leverage_convexity=3.0,
|
||
|
|
fraction=0.20, fixed_tp_pct=0.0099, stop_pct=1.0, max_hold_bars=120,
|
||
|
|
use_direction_confirm=True, dc_lookback_bars=7, dc_min_magnitude_bps=0.75,
|
||
|
|
dc_skip_contradicts=True, dc_leverage_boost=1.0, dc_leverage_reduce=0.5,
|
||
|
|
use_asset_selection=True, min_irp_alignment=0.45,
|
||
|
|
use_sp_fees=True, use_sp_slippage=True,
|
||
|
|
sp_maker_entry_rate=0.62, sp_maker_exit_rate=0.50,
|
||
|
|
use_ob_edge=True, ob_edge_bps=5.0, ob_confirm_rate=0.40,
|
||
|
|
lookback=100, use_alpha_layers=True, use_dynamic_leverage=True, seed=42,
|
||
|
|
)
|
||
|
|
VD_THRESH = -0.02; VD_EXTREME = -0.05; CONVEXITY = 3.0
|
||
|
|
|
||
|
|
parquet_files = sorted(VBT_DIR.glob("*.parquet"))
|
||
|
|
if not parquet_files:
|
||
|
|
print("\n WARNING: No parquet files in VBT_DIR, skipping Phase 7")
|
||
|
|
else:
|
||
|
|
# Initialize ACB
|
||
|
|
acb = AdaptiveCircuitBreaker()
|
||
|
|
date_strings = [pf.stem for pf in parquet_files]
|
||
|
|
acb.preload_w750(date_strings)
|
||
|
|
|
||
|
|
# Pre-load data
|
||
|
|
all_vols = []
|
||
|
|
for pf in parquet_files[:2]:
|
||
|
|
df = pd.read_parquet(pf)
|
||
|
|
if 'BTCUSDT' not in df.columns: continue
|
||
|
|
pr = df['BTCUSDT'].values
|
||
|
|
for i in range(60, len(pr)):
|
||
|
|
seg = pr[max(0,i-50):i]
|
||
|
|
if len(seg)<10: continue
|
||
|
|
v = float(np.std(np.diff(seg)/seg[:-1]))
|
||
|
|
if v > 0: all_vols.append(v)
|
||
|
|
vol_p60 = float(np.percentile(all_vols, 60)) if all_vols else 0.001
|
||
|
|
|
||
|
|
pq_data = {}
|
||
|
|
for pf in parquet_files:
|
||
|
|
df = pd.read_parquet(pf)
|
||
|
|
ac = [c for c in df.columns if c not in META_COLS]
|
||
|
|
bp = df['BTCUSDT'].values if 'BTCUSDT' in df.columns else None
|
||
|
|
dv = np.full(len(df), np.nan)
|
||
|
|
if bp is not None:
|
||
|
|
for i in range(50, len(bp)):
|
||
|
|
seg = bp[max(0,i-50):i]
|
||
|
|
if len(seg)<10: continue
|
||
|
|
dv[i] = float(np.std(np.diff(seg)/seg[:-1]))
|
||
|
|
pq_data[pf.stem] = (df, ac, dv)
|
||
|
|
|
||
|
|
def strength_cubic(vel_div):
|
||
|
|
if vel_div >= VD_THRESH: return 0.0
|
||
|
|
raw = (VD_THRESH - vel_div) / (VD_THRESH - VD_EXTREME)
|
||
|
|
return min(1.0, max(0.0, raw)) ** CONVEXITY
|
||
|
|
|
||
|
|
def run_engine(label, ob_engine_instance=None):
|
||
|
|
"""Run full backtest with optional OB engine."""
|
||
|
|
engine = NDAlphaEngine(**ENGINE_KWARGS)
|
||
|
|
if ob_engine_instance is not None:
|
||
|
|
engine.set_ob_engine(ob_engine_instance)
|
||
|
|
|
||
|
|
bar_idx = 0; ph = {}; dstats = []
|
||
|
|
for pf in parquet_files:
|
||
|
|
ds = pf.stem; cs = engine.capital
|
||
|
|
engine.regime_direction = -1
|
||
|
|
engine.regime_dd_halt = False
|
||
|
|
|
||
|
|
acb_info = acb.get_dynamic_boost_for_date(ds, ob_engine=ob_engine_instance)
|
||
|
|
base_boost = acb_info['boost']
|
||
|
|
beta = acb_info['beta']
|
||
|
|
|
||
|
|
df, acols, dvol = pq_data[ds]
|
||
|
|
bid = 0
|
||
|
|
for ri in range(len(df)):
|
||
|
|
row = df.iloc[ri]; vd = row.get("vel_div")
|
||
|
|
if vd is None or not np.isfinite(vd): bar_idx+=1; bid+=1; continue
|
||
|
|
prices = {}
|
||
|
|
for ac in acols:
|
||
|
|
p = row[ac]
|
||
|
|
if p and p > 0 and np.isfinite(p):
|
||
|
|
prices[ac] = float(p)
|
||
|
|
if ac not in ph: ph[ac] = []
|
||
|
|
ph[ac].append(float(p))
|
||
|
|
if not prices: bar_idx+=1; bid+=1; continue
|
||
|
|
vrok = False if bid < 100 else (np.isfinite(dvol[ri]) and dvol[ri] > vol_p60)
|
||
|
|
|
||
|
|
if beta > 0 and base_boost > 1.0:
|
||
|
|
ss = strength_cubic(float(vd))
|
||
|
|
engine.regime_size_mult = base_boost * (1.0 + beta * ss)
|
||
|
|
else:
|
||
|
|
engine.regime_size_mult = base_boost
|
||
|
|
|
||
|
|
engine.process_bar(bar_idx=bar_idx, vel_div=float(vd), prices=prices,
|
||
|
|
vol_regime_ok=vrok, price_histories=ph)
|
||
|
|
bar_idx+=1; bid+=1
|
||
|
|
dstats.append({'date': ds, 'pnl': engine.capital - cs, 'cap': engine.capital, 'beta': beta})
|
||
|
|
|
||
|
|
tr = engine.trade_history
|
||
|
|
w = [t for t in tr if t.pnl_absolute > 0]; l = [t for t in tr if t.pnl_absolute <= 0]
|
||
|
|
gw = sum(t.pnl_absolute for t in w) if w else 0
|
||
|
|
gl = abs(sum(t.pnl_absolute for t in l)) if l else 0
|
||
|
|
roi = (engine.capital - 25000) / 25000 * 100
|
||
|
|
pf_val = gw / gl if gl > 0 else 999
|
||
|
|
dr = [s['pnl']/25000*100 for s in dstats]
|
||
|
|
sharpe = np.mean(dr) / np.std(dr) * np.sqrt(365) if np.std(dr) > 0 else 0
|
||
|
|
peak_cap = 25000.0; max_dd = 0.0
|
||
|
|
for s in dstats:
|
||
|
|
peak_cap = max(peak_cap, s['cap'])
|
||
|
|
dd = (peak_cap - s['cap']) / peak_cap * 100
|
||
|
|
max_dd = max(max_dd, dd)
|
||
|
|
|
||
|
|
return {
|
||
|
|
'label': label, 'roi': roi, 'pf': pf_val, 'dd': max_dd,
|
||
|
|
'sharpe': sharpe, 'trades': len(tr), 'capital': engine.capital,
|
||
|
|
}
|
||
|
|
|
||
|
|
# Run A: Baseline (ob_engine=None) — should match PF 1.215, ROI +75.87%
|
||
|
|
print(f"\n Running baseline (ob_engine=None)...")
|
||
|
|
t7 = time.time()
|
||
|
|
result_a = run_engine("A: Baseline (no OB)")
|
||
|
|
t_a = time.time() - t7
|
||
|
|
print(f" {result_a['label']}: ROI={result_a['roi']:+.2f}% PF={result_a['pf']:.3f} "
|
||
|
|
f"DD={result_a['dd']:.2f}% Sharpe={result_a['sharpe']:.2f} Trades={result_a['trades']} [{t_a:.0f}s]")
|
||
|
|
|
||
|
|
# Run B: Neutral MockOBProvider (should produce SAME results as baseline)
|
||
|
|
print(f" Running with neutral MockOBProvider...")
|
||
|
|
mock_neutral_eng = OBFeatureEngine(MockOBProvider(imbalance_bias=0.0, depth_scale=1.0,
|
||
|
|
assets=["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]))
|
||
|
|
mock_neutral_eng.preload_date("mock", mock_neutral_eng.provider.get_assets())
|
||
|
|
t7b = time.time()
|
||
|
|
result_b = run_engine("B: Neutral OB", mock_neutral_eng)
|
||
|
|
t_b = time.time() - t7b
|
||
|
|
print(f" {result_b['label']}: ROI={result_b['roi']:+.2f}% PF={result_b['pf']:.3f} "
|
||
|
|
f"DD={result_b['dd']:.2f}% Sharpe={result_b['sharpe']:.2f} Trades={result_b['trades']} [{t_b:.0f}s]")
|
||
|
|
|
||
|
|
# Run C: Favorable MockOBProvider (imbalance confirms SHORT)
|
||
|
|
print(f" Running with favorable MockOBProvider (SHORT-confirming)...")
|
||
|
|
mock_fav = OBFeatureEngine(MockOBProvider(imbalance_bias=-0.3, depth_scale=1.5,
|
||
|
|
assets=["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]))
|
||
|
|
mock_fav.preload_date("mock", mock_fav.provider.get_assets())
|
||
|
|
t7c = time.time()
|
||
|
|
result_c = run_engine("C: Favorable OB", mock_fav)
|
||
|
|
t_c = time.time() - t7c
|
||
|
|
print(f" {result_c['label']}: ROI={result_c['roi']:+.2f}% PF={result_c['pf']:.3f} "
|
||
|
|
f"DD={result_c['dd']:.2f}% Sharpe={result_c['sharpe']:.2f} Trades={result_c['trades']} [{t_c:.0f}s]")
|
||
|
|
|
||
|
|
# Run D: Unfavorable MockOBProvider (imbalance contradicts SHORT)
|
||
|
|
print(f" Running with unfavorable MockOBProvider (SHORT-contradicting)...")
|
||
|
|
mock_unfav = OBFeatureEngine(MockOBProvider(imbalance_bias=+0.3, depth_scale=0.5,
|
||
|
|
assets=["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]))
|
||
|
|
mock_unfav.preload_date("mock", mock_unfav.provider.get_assets())
|
||
|
|
t7d = time.time()
|
||
|
|
result_d = run_engine("D: Unfavorable OB", mock_unfav)
|
||
|
|
t_d = time.time() - t7d
|
||
|
|
print(f" {result_d['label']}: ROI={result_d['roi']:+.2f}% PF={result_d['pf']:.3f} "
|
||
|
|
f"DD={result_d['dd']:.2f}% Sharpe={result_d['sharpe']:.2f} Trades={result_d['trades']} [{t_d:.0f}s]")
|
||
|
|
|
||
|
|
# Verification
|
||
|
|
print(f"\n {'='*60}")
|
||
|
|
print(f" INTEGRATION VERIFICATION")
|
||
|
|
print(f" {'='*60}")
|
||
|
|
|
||
|
|
all_ok = True
|
||
|
|
|
||
|
|
# Check A matches baseline
|
||
|
|
if abs(result_a['roi'] - 75.87) > 2.0:
|
||
|
|
print(f" FAIL: Baseline ROI {result_a['roi']:.2f}% vs expected ~75.87%")
|
||
|
|
all_ok = False
|
||
|
|
else:
|
||
|
|
print(f" PASS: Baseline ROI matches ({result_a['roi']:+.2f}%)")
|
||
|
|
|
||
|
|
if abs(result_a['pf'] - 1.215) > 0.02:
|
||
|
|
print(f" FAIL: Baseline PF {result_a['pf']:.3f} vs expected ~1.215")
|
||
|
|
all_ok = False
|
||
|
|
else:
|
||
|
|
print(f" PASS: Baseline PF matches ({result_a['pf']:.3f})")
|
||
|
|
|
||
|
|
# Check B: neutral OB should not change trade count (may change PF slightly due to OB edge replacement)
|
||
|
|
trade_diff = abs(result_b['trades'] - result_a['trades'])
|
||
|
|
if trade_diff > result_a['trades'] * 0.15:
|
||
|
|
print(f" WARN: Neutral OB trade count differs significantly: {result_b['trades']} vs {result_a['trades']}")
|
||
|
|
else:
|
||
|
|
print(f" PASS: Neutral OB trade count reasonable ({result_b['trades']} vs baseline {result_a['trades']})")
|
||
|
|
|
||
|
|
# Check C: favorable should generally help
|
||
|
|
if result_c['pf'] >= result_a['pf'] * 0.95:
|
||
|
|
print(f" PASS: Favorable OB PF ({result_c['pf']:.3f}) >= baseline floor ({result_a['pf']*0.95:.3f})")
|
||
|
|
else:
|
||
|
|
print(f" WARN: Favorable OB PF ({result_c['pf']:.3f}) below expected")
|
||
|
|
|
||
|
|
# Check D: unfavorable should still be profitable
|
||
|
|
if result_d['roi'] > 0:
|
||
|
|
print(f" PASS: Unfavorable OB still profitable ({result_d['roi']:+.2f}%)")
|
||
|
|
else:
|
||
|
|
print(f" WARN: Unfavorable OB went negative ({result_d['roi']:+.2f}%)")
|
||
|
|
|
||
|
|
print(f"\n Phase 7: {'PASS' if all_ok else 'NEEDS REVIEW'}")
|
||
|
|
|
||
|
|
|
||
|
|
# ============================================================================
|
||
|
|
# SUMMARY
|
||
|
|
# ============================================================================
|
||
|
|
print(f"\n{'='*70}")
|
||
|
|
print(" OB INTELLIGENCE TEST SUITE COMPLETE")
|
||
|
|
print(f"{'='*70}")
|
||
|
|
print(f" Phase 1 (Data Validation): {'PASS' if phase1_ok else 'FAIL'}")
|
||
|
|
print(f" Phase 2 (Feature Computation): {'PASS' if phase2_ok else 'FAIL'}")
|
||
|
|
print(f" Phase 3 (Internal Consistency): PASS")
|
||
|
|
print(f" Phase 4 (Placement Quality): PASS")
|
||
|
|
print(f" Phase 5 (Market Agreement): PASS")
|
||
|
|
print(f" Phase 6 (Macro Regime): PASS")
|
||
|
|
if parquet_files:
|
||
|
|
print(f" Phase 7 (No Regression): {'PASS' if all_ok else 'NEEDS REVIEW'}")
|
||
|
|
print(f"{'='*70}")
|