Files
DOLPHIN/nautilus_dolphin/test_pf_klines_2y_experiment.py
hjnormey 01c19662cb initial: import DOLPHIN baseline 2026-04-21 from dolphinng5_predict working tree
Includes core prod + GREEN/BLUE subsystems:
- prod/ (BLUE harness, configs, scripts, docs)
- nautilus_dolphin/ (GREEN Nautilus-native impl + dvae/ preserved)
- adaptive_exit/ (AEM engine + models/bucket_assignments.pkl)
- Observability/ (EsoF advisor, TUI, dashboards)
- external_factors/ (EsoF producer)
- mc_forewarning_qlabs_fork/ (MC regime/envelope)

Excludes runtime caches, logs, backups, and reproducible artifacts per .gitignore.
2026-04-21 16:58:38 +02:00

349 lines
17 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""2-Year Klines Fractal Experiment — Eigen Multi-Scale Divergence at 1-Minute Timescale.
Hypothesis: the eigenvalue velocity divergence principle (vel_div = w50_vel - w150_vel)
holds fractally at any timescale. At 1-minute cadence:
- w50 = 50 one-minute bars ≈ 50 min clock time (vs ~4 min in live DOLPHIN)
- w150 = 150 one-minute bars ≈ 2.5 hr clock time (vs ~12.5 min in live DOLPHIN)
Threshold adaptation (NOT normalization — different timescale, not different universe):
klines vel_div distribution (30-day sample, Jan 2024):
median=+0.016, std=0.467, range [-8.13, +9.52]
p7 ≈ -0.50 (matches champion ~7% signal rate)
p2 ≈ -1.00 (matches champion extreme-rate ~2%)
Champion NG3 thresholds: vel_div_threshold=-0.02 (p~7%), vel_div_extreme=-0.05
Adapted klines thresholds: vel_div_threshold=-0.50, vel_div_extreme=-1.25
Data: vbt_cache_klines/ 2024-01-01 to 2026-03-05 (~795 days, 1-min klines → ARB512 eigenvalues)
Asset universe: ~50 symbols (2024 = pre-STXUSDT era, NKNUSDT present)
Note: universe shift mid-experiment expected — handled by ARS at daily level.
Full engine stack unchanged: ACBv6 + OB 4D (MockOB) + MC-Forewarner + EsoF(neutral) + ExF(neutral fallback)
"""
import sys, time, math, json, csv
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
from pathlib import Path
from datetime import datetime
from collections import defaultdict
import numpy as np
import pandas as pd
sys.path.insert(0, str(Path(__file__).parent))
print("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 (
OBFeatureEngine, 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,
)
from nautilus_dolphin.nautilus.ob_provider import MockOBProvider
_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.55, -0.50, -1.25, 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)
_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], dtype=np.float64), 2)
compute_withdrawal_velocity_nb(np.array([100.0, 110.0], dtype=np.float64), 1)
compute_market_agreement_nb(np.array([0.1, -0.05], dtype=np.float64), 2)
compute_cascade_signal_nb(np.array([-0.05, -0.15], dtype=np.float64), 2, -0.10)
print(f" JIT: {time.time() - t0c:.1f}s")
from nautilus_dolphin.nautilus.esf_alpha_orchestrator import NDAlphaEngine
from nautilus_dolphin.nautilus.adaptive_circuit_breaker import AdaptiveCircuitBreaker
from mc.mc_ml import DolphinForewarner
# ── Config ─────────────────────────────────────────────────────────────────────
# klines cache: 2024-01-01 to 2026-03-05 stored in vbt_cache_klines/
# (vbt_cache_klines = pure klines-derived parquets, 1-min cadence, no live 5s NG5 data mixed in)
VBT_DIR = Path(r"C:\Users\Lenovo\Documents\- DOLPHIN NG HD HCM TSF Predict\vbt_cache_klines")
DATE_START = '2024-01-01'
DATE_END = '2026-03-05'
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'}
# Threshold adapted for 1-min timescale (see module docstring for derivation)
VD_THRESHOLD = -0.50 # p~7 (champion: -0.02 at NG3 scale)
VD_EXTREME = -1.25 # p~2 (champion: -0.05 at NG3 scale, same 2.5× ratio)
ENGINE_KWARGS = dict(
initial_capital=25000.0,
vel_div_threshold=VD_THRESHOLD,
vel_div_extreme=VD_EXTREME,
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,
)
MC_MODELS_DIR = str(Path(r"C:\Users\Lenovo\Documents\- DOLPHIN NG HD HCM TSF Predict\nautilus_dolphin\mc_results\models"))
MC_BASE_CFG = {
'trial_id': 0,
# MC-Forewarner was trained on champion range (-0.02/-0.05). The klines threshold
# adaptation (-0.50/-1.25) is a timescale rescaling, not a capital-risk change.
# Pass champion thresholds so MC assesses leverage/fraction risk correctly.
'vel_div_threshold': -0.02, 'vel_div_extreme': -0.05,
'use_direction_confirm': True, 'dc_lookback_bars': 7,
'dc_min_magnitude_bps': 0.75, 'dc_skip_contradicts': True,
'dc_leverage_boost': 1.00, 'dc_leverage_reduce': 0.50,
'vd_trend_lookback': 10, 'min_leverage': 0.50,
'max_leverage': 5.00,
'leverage_convexity': 3.00, 'fraction': 0.20,
'use_alpha_layers': True, 'use_dynamic_leverage': True,
'fixed_tp_pct': 0.0099, 'stop_pct': 1.00, 'max_hold_bars': 120,
'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.00, 'ob_confirm_rate': 0.40,
'ob_imbalance_bias': -0.09, 'ob_depth_scale': 1.00,
'use_asset_selection': True, 'min_irp_alignment': 0.45, 'lookback': 100,
'acb_beta_high': 0.80, 'acb_beta_low': 0.20, 'acb_w750_threshold_pct': 60,
}
print("\nLoading MC-Forewarner trained models...")
forewarner = DolphinForewarner(models_dir=MC_MODELS_DIR)
print(" MC-Forewarner ready")
# ── Load klines parquet files (2024-2025 only) ─────────────────────────────────
parquet_files = sorted(
p for p in VBT_DIR.glob("*.parquet")
if 'catalog' not in str(p) and DATE_START <= p.stem <= DATE_END
)
print(f"\nKlines parquet files: {len(parquet_files)} dates ({parquet_files[0].stem} to {parquet_files[-1].stem})")
# ── ACB init ───────────────────────────────────────────────────────────────────
print("\nInitializing ACB v6...")
acb = AdaptiveCircuitBreaker()
date_strings = [pf.stem for pf in parquet_files]
acb.preload_w750(date_strings)
print(f" w750 p60 threshold: {acb._w750_threshold:.6f}")
print(f" Dates with w750 data: {sum(1 for v in acb._w750_vel_cache.values() if v != 0.0)}/{len(date_strings)}")
# ── Vol p60 calibration from first 5 dates ─────────────────────────────────────
all_vols = []
for pf in parquet_files[:5]:
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 1e-4
print(f"\nVol p60 (klines calibration): {vol_p60:.6f}")
# ── Pre-load all parquet data ───────────────────────────────────────────────────
print(f"\nPre-loading {len(parquet_files)} parquet files...")
t_load = time.time()
pq_data = {}
for i, pf in enumerate(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 j in range(50, len(bp)):
seg = bp[max(0,j-50):j]
if len(seg) < 10: continue
dv[j] = float(np.std(np.diff(seg)/seg[:-1]))
pq_data[pf.stem] = (df, ac, dv)
if (i+1) % 100 == 0:
print(f" Loaded {i+1}/{len(parquet_files)} dates...")
print(f" Done in {time.time()-t_load:.1f}s")
# ── ACB w750 cache: populate from klines parquet v750 column ───────────────────
# NG3 NPZ indicator files don't exist for 2024-2025 klines dates, so preload_w750
# above returned all-zero. Override with parquet-derived klines w750 velocities.
print("\nPopulating ACB w750 cache from klines v750_lambda_max_velocity...")
for date_str, (df, _, _) in pq_data.items():
if 'v750_lambda_max_velocity' in df.columns:
v750_vals = df['v750_lambda_max_velocity'].dropna()
if len(v750_vals) > 0:
acb._w750_vel_cache[date_str] = float(v750_vals.median())
# Recompute threshold from klines w750 distribution
_w750_vals = [v for v in acb._w750_vel_cache.values() if v != 0.0]
if _w750_vals:
acb._w750_threshold = float(np.percentile(_w750_vals, acb.config.W750_THRESHOLD_PCT))
print(f" w750 klines p60 threshold: {acb._w750_threshold:.6f}")
print(f" Dates with klines w750 data: {len(_w750_vals)}/{len(date_strings)}")
else:
print(" WARNING: no klines w750 data found — ACB beta will be constant")
# ── OB engine ──────────────────────────────────────────────────────────────────
OB_ASSETS = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT"]
_mock_ob = MockOBProvider(
imbalance_bias=-0.09, depth_scale=1.0, assets=OB_ASSETS,
imbalance_biases={"BTCUSDT": -0.086, "ETHUSDT": -0.092,
"BNBUSDT": +0.05, "SOLUSDT": +0.05},
)
ob_eng = OBFeatureEngine(_mock_ob)
ob_eng.preload_date("mock", OB_ASSETS)
# ── Full stack ─────────────────────────────────────────────────────────────────
print(f"\n=== 2Y KLINES EXPERIMENT: ACBv6 + OB 4D + MC-Forewarner + EsoF(neutral) ===")
print(f" Period: {DATE_START} to {DATE_END} ({len(parquet_files)} days)")
print(f" vel_div threshold: {VD_THRESHOLD} (klines-adapted) extreme: {VD_EXTREME}")
print(f" Timescale: 1-min bars w50=50min w150=2.5h max_hold=120min")
t0 = time.time()
engine = NDAlphaEngine(**ENGINE_KWARGS)
engine.set_ob_engine(ob_eng)
engine.set_acb(acb)
engine.set_mc_forewarner(forewarner, MC_BASE_CFG)
engine.set_esoteric_hazard_multiplier(0.0) # EsoF neutral
# ── Per-date loop ──────────────────────────────────────────────────────────────
all_daily = []
all_trades = []
for date_str in date_strings:
if date_str not in pq_data:
continue
df, asset_cols, dvol_arr = pq_data[date_str]
# Vol regime: per-bar boolean array matching champion API
vol_ok = np.where(np.isfinite(dvol_arr), dvol_arr > vol_p60, False)
result = engine.process_day(date_str, df, asset_cols, vol_regime_ok=vol_ok)
all_daily.append({
'date': date_str,
'pnl': result.get('pnl', 0.0),
'capital': result.get('capital', ENGINE_KWARGS['initial_capital']),
'trades': result.get('trades', 0),
'boost': result.get('boost', 1.0),
'beta': result.get('beta', 0.0),
'mc_status': result.get('mc_status', 'GREEN'),
})
# trade_log not in process_day result — accumulate from engine.trade_history per day
if len(all_daily) % 50 == 0:
recent = all_daily[-50:]
cum_cap = all_daily[-1]['capital']
roi = (cum_cap - ENGINE_KWARGS['initial_capital']) / ENGINE_KWARGS['initial_capital'] * 100
ntrades = sum(r['trades'] for r in recent)
print(f" [{date_str}] Day {len(all_daily)}/{len(date_strings)} | ROI={roi:+.1f}% | Last-50d trades={ntrades}")
t_elapsed = time.time() - t0
# Collect all trades from engine history
all_trades = [
{'pnl': t.pnl_absolute, 'pnl_pct': t.pnl_pct * 100,
'asset': t.asset, 'bars_held': t.bars_held,
'entry_bar': t.entry_bar, 'exit_bar': t.exit_bar,
'exit_reason': t.exit_reason, 'leverage': t.leverage}
for t in engine.trade_history
]
# ── Summary stats ──────────────────────────────────────────────────────────────
capitals = [r['capital'] for r in all_daily]
pnls = [r['pnl'] for r in all_daily]
n_trades = sum(r['trades'] for r in all_daily)
final_cap = capitals[-1] if capitals else ENGINE_KWARGS['initial_capital']
roi = (final_cap - ENGINE_KWARGS['initial_capital']) / ENGINE_KWARGS['initial_capital'] * 100
# Drawdown
peak = ENGINE_KWARGS['initial_capital']
max_dd = 0.0
for c in capitals:
if c > peak: peak = c
dd = (peak - c) / peak * 100
if dd > max_dd: max_dd = dd
# Sharpe (daily PnL / std)
pnl_arr = np.array(pnls)
sharpe = (pnl_arr.mean() / pnl_arr.std() * np.sqrt(252)) if pnl_arr.std() > 0 else 0.0
# Win rate
wins = [r for r in all_trades if r.get('pnl', 0) > 0] if all_trades else []
losses = [r for r in all_trades if r.get('pnl', 0) <= 0] if all_trades else []
wr = len(wins) / (len(wins) + len(losses)) * 100 if (wins or losses) else 0.0
# Profit factor
gross_win = sum(t.get('pnl', 0) for t in wins)
gross_loss = abs(sum(t.get('pnl', 0) for t in losses))
pf = gross_win / gross_loss if gross_loss > 0 else float('inf')
# Half-year comparison
h1_dates = [r for r in all_daily if r['date'] < '2025-01-01']
h2_dates = [r for r in all_daily if r['date'] >= '2025-01-01']
h1_roi = sum(r['pnl'] for r in h1_dates) / ENGINE_KWARGS['initial_capital'] * 100
h2_roi = sum(r['pnl'] for r in h2_dates) / ENGINE_KWARGS['initial_capital'] * 100
print(f"\n{'='*65}")
print(f" 2Y KLINES EXPERIMENT RESULTS ({DATE_START} to {DATE_END})")
print(f"{'='*65}")
print(f" ROI: {roi:+.2f}%")
print(f" PF: {pf:.3f}")
print(f" Max DD: {max_dd:.2f}%")
print(f" Sharpe: {sharpe:.2f}")
print(f" Win Rate: {wr:.1f}%")
print(f" Trades: {n_trades:,} ({n_trades/len(date_strings):.1f}/day avg)")
print(f" Days: {len(all_daily)}")
print(f" H1 ROI (2024): {h1_roi:+.2f}%")
print(f" H2 ROI (2025): {h2_roi:+.2f}%")
print(f" H2/H1 ratio: {h2_roi/h1_roi:.2f}x" if h1_roi != 0 else " H2/H1: N/A")
print(f" Runtime: {t_elapsed/60:.1f} min")
print(f"{'='*65}")
print(f"\n Champion (55d NG3): ROI=+44.89% PF=1.123 DD=14.95% Sharpe=2.50 WR=49.3%")
print(f" Timescale: klines 1-min vs champion 5s (12× longer bars)")
print(f" Threshold: {VD_THRESHOLD} (klines p~7) vs champion -0.02 (NG3 p~7)")
# ── Save results ───────────────────────────────────────────────────────────────
ts = datetime.now().strftime('%Y%m%d_%H%M%S')
run_dir = Path(__file__).parent / 'run_logs'
run_dir.mkdir(exist_ok=True)
summary = {
'experiment': '2y_klines_fractal',
'date_range': f'{DATE_START}_to_{DATE_END}',
'timescale': '1min_klines',
'vel_div_threshold': VD_THRESHOLD,
'vel_div_extreme': VD_EXTREME,
'roi_pct': roi, 'pf': pf, 'max_dd_pct': max_dd,
'sharpe': sharpe, 'win_rate_pct': wr,
'n_trades': n_trades, 'n_days': len(all_daily),
'trades_per_day': n_trades / len(all_daily) if all_daily else 0,
'h1_roi_pct': h1_roi, 'h2_roi_pct': h2_roi,
'engine_kwargs': ENGINE_KWARGS,
'runtime_s': t_elapsed,
'run_ts': ts,
}
summary_path = run_dir / f'klines_2y_{ts}.json'
with open(summary_path, 'w') as f:
json.dump(summary, f, indent=2)
print(f"\n Summary: {summary_path}")
if all_daily:
daily_path = run_dir / f'klines_2y_daily_{ts}.csv'
with open(daily_path, 'w', newline='') as f:
w = csv.DictWriter(f, fieldnames=all_daily[0].keys())
w.writeheader(); w.writerows(all_daily)
print(f" Daily: {daily_path}")
if all_trades:
trades_path = run_dir / f'klines_2y_trades_{ts}.csv'
with open(trades_path, 'w', newline='') as f:
w = csv.DictWriter(f, fieldnames=all_trades[0].keys())
w.writeheader(); w.writerows(all_trades)
print(f" Trades: {trades_path}")