""" DOLPHIN NG VBT Real Data Integration ===================================== VectorBT-based backtesting system for DOLPHIN NG trading strategies. Runs on real eigenvalue scan data with Parquet caching. Target file: dolphin_vbt_real.py VBT version: 0.28.4 Date: 2026-02-10 Sections: 0. Constants & Configuration 1. Data Loading Pipeline 2. VBT Custom Indicator 3. Signal Generation 4. Numba Callbacks 5. Maker Fill Filtering 6. Portfolio Simulation 7. Parameter Sweep 8. Validation 9. CLI Entry Point """ # ═══════════════════════════════════════════════════════════════════════════════ # SECTION 0: CONSTANTS & CONFIGURATION # ═══════════════════════════════════════════════════════════════════════════════ import os import sys import json import argparse import warnings from pathlib import Path from datetime import datetime from dataclasses import dataclass, field from typing import Dict, List, Optional, Tuple, Union, Callable from concurrent.futures import ProcessPoolExecutor, as_completed from itertools import product import time import traceback import numpy as np import pandas as pd from numba import njit, prange import numba.types as nt # Optional: faster JSON parsing try: import orjson HAS_ORJSON = True def json_loads(s): return orjson.loads(s) except ImportError: HAS_ORJSON = False def json_loads(s): return json.loads(s) # Parquet support try: import pyarrow as pa import pyarrow.parquet as pq HAS_PYARROW = True except ImportError: HAS_PYARROW = False warnings.warn("PyArrow not installed. Parquet caching disabled.") import vectorbt as vbt from vectorbt.portfolio.enums import AdjustSLContext, Direction # Suppress FLINT warning warnings.filterwarnings('ignore', message='python-flint 0.8.0 is installed') # ── Path Configuration ───────────────────────────────────────────────────────── # Data source path (JSON eigenvalue scan files) DATA_PATH = Path(r'C:\Users\Lenovo\Documents\- Dolphin NG HD (NG3)\correlation_arb512\eigenvalues') # Cache directory for Parquet files (project root) PROJECT_ROOT = Path(r'C:\Users\Lenovo\Documents\- DOLPHIN NG HD HCM TSF Predict') CACHE_DIR = PROJECT_ROOT / 'vbt_cache' RESULTS_DIR = PROJECT_ROOT / 'vbt_results' # Create directories CACHE_DIR.mkdir(exist_ok=True) RESULTS_DIR.mkdir(exist_ok=True) # ── Fee Constants ────────────────────────────────────────────────────────────── FEE_MAKER = 0.0002 # 0.02% maker fee FEE_TAKER = 0.0005 # 0.05% taker fee FEE_RATE_REALISTIC = 0.0008 # 0.08% round-trip (non-SP mode) # ── Slippage Constants ───────────────────────────────────────────────────────── SLIPPAGE_ENTRY = 0.0002 # 0.02% adverse for entry SLIPPAGE_EXIT = 0.0002 # 0.02% adverse for normal exit SLIPPAGE_STOP = 0.0005 # 0.05% adverse for stop exit # ── SmartPlacer Constants ─────────────────────────────────────────────────────── SP_CONFIDENCE_MAKER_THRESHOLD = 0.40 SP_CONFIDENCE_TAKER_THRESHOLD = 0.85 SP_MAKER_FILL_RATE = 0.62 SP_MAKER_EXIT_RATE = 0.50 # 50% of non-stop exits fill as maker SP_FILL_DISCOUNT = 0.80 # ── OB Edge Constants ───────────────────────────────────────────────────────── OB_CONFIRM_RATE = 0.40 # 40% of trades get OB confirmation # ── IRP Constants ───────────────────────────────────────────────────────────── IRP_LOOKBACK = 50 # Bars of price history for IRP IRP_NOISE_MAX = 500.0 # Hard gate: max noise IRP_LATENCY_MAX = 20 # Hard gate: max latency (bars) IRP_ALIGNMENT_MIN = 0.20 # Hard gate: min alignment # ── RCDD Constants ──────────────────────────────────────────────────────────── RCDD_LOOKBACK = 100 # Bars for adverse/favorable calc # ── Alpha Engine Constants ─────────────────────────────────────────────────── EXTREME_VD = -0.05 # Extreme vel_div threshold for alpha layers VD_TREND_LOOKBACK = 10 # Bars lookback for vel_div trend # ── Excluded Assets ───────────────────────────────────────────────────────────── EXCLUDED_ASSETS = {'TUSDUSDT', 'USDCUSDT'} # Stablecoins # ── Strategy Dataclass ───────────────────────────────────────────────────────── @dataclass class Strategy: """Trading strategy configuration.""" name: str vel_div_threshold: float = -0.02 direction: str = 'SHORT' # 'SHORT' or 'LONG' leverage: float = 2.5 fraction: float = 0.15 stop_pct: float = 0.002 max_hold: int = 120 # Trailing stop use_trailing: bool = True trail_activation: float = 0.0003 # 3bps trail_distance: float = 0.0003 # 3bps # Filters vol_filter: str = 'all' # 'all', 'high', 'low', 'low_normal' lookback: int = 100 # Features use_rcdd: bool = False use_sp_fees: bool = False use_sp_slippage: bool = False use_maker_filter: bool = False use_ob_edge: bool = False ob_edge_bps: float = 3.0 # RCDD rcdd_multiplier: float = 1.5 rcdd_min_stop: float = 0.001 rcdd_trail: bool = False rcdd_trail_mult: float = 1.0 rcdd_activation_mult: float = 0.5 trail_dist_floor: float = 0.0003 trail_act_floor: float = 0.0003 # Asset selection use_asset_selection: bool = False min_irp_alignment: float = 0.45 # Dynamic leverage (alpha engine) dynamic_leverage: bool = False max_leverage: float = 5.0 min_leverage: float = 1.0 leverage_convexity: float = 1.0 # 1.0=linear, 2.0=quadratic, 3.0=cubic (higher = more concentrated on strong signals) # Alpha layers (bucket_boost, streak_mult, trend_mult, confidence sizing) use_alpha_layers: bool = False # RCDD target (early exit on favorable move) use_rcdd_target: bool = False # Fixed take-profit (exit when PnL reaches target) use_fixed_tp: bool = False fixed_tp_pct: float = 0.0 # as decimal (e.g., 0.002 = 20bps = 0.20%) # Fee override (-1 = compute from sp_fees, >=0 = use this per-side rate) fee_rate_override: float = -1.0 # Passive entry (SmartPlacer OB-based: "let price move to us") use_passive_entry: bool = False passive_timeout_bars: int = 5 # Wait up to N bars for maker fill (5=25s) passive_offset_bps: float = 1.0 # Place limit N bps inside spread passive_abort_bps: float = 5.0 # Abort if price moves N bps against us passive_fill_discount: float = 0.80 # Queue position discount (0.80 = 80% of crosses fill) passive_fallback_taker: bool = True # On timeout: taker fallback (False=abort) maker_fee_rate: float = 0.0002 # 0.02% maker fee per side taker_fee_rate: float = 0.0005 # 0.05% taker fee per side # Direction confirmation (OB imbalance proxy via price momentum) use_direction_confirm: bool = False dc_lookback_bars: int = 5 # N-bar price momentum for direction check dc_min_magnitude_bps: float = 2.0 # Min price change (bps) to classify as confirm/contradict dc_skip_contradicts: bool = True # True=skip contradicted trades, False=reduce leverage dc_leverage_boost: float = 1.5 # Leverage multiplier when OB confirms direction dc_leverage_reduce: float = 0.5 # Leverage multiplier when contradicted (if not skipping) # ═══════════════════════════════════════════════════════════════════════════════ # SECTION 1: DATA LOADING PIPELINE # ═══════════════════════════════════════════════════════════════════════════════ def _process_date(date_dir: Path) -> Optional[pd.DataFrame]: """ Process all scan JSON files in one date directory. Steps: 1. List and sort all scan_*.json files 2. For each file: parse, extract fields, validate 3. Build DataFrame, forward-fill prices 4. Return DataFrame Args: date_dir: Path to date directory (e.g., 2026-01-01) Returns: DataFrame with scan data, or None if no valid scans """ date_str = date_dir.name # List and sort scan files scan_files = sorted(date_dir.glob('scan_*.json')) if not scan_files: return None rows = [] last_prices = {} # For forward-filling for scan_file in scan_files: try: with open(scan_file, 'rb') as f: data = json_loads(f.read()) # Extract scan metadata scan_number = data.get('scan_number') timestamp_str = data.get('timestamp') # Parse timestamp try: timestamp = pd.Timestamp(timestamp_str) except (ValueError, TypeError): # Try with space replacement if needed timestamp = pd.Timestamp(timestamp_str.replace(' ', 'T') if timestamp_str else None) # Extract windows data windows = data.get('windows', {}) # Get v50 and v150 lambda_max_velocity w50 = windows.get('50', {}).get('tracking_data', {}) w150 = windows.get('150', {}).get('tracking_data', {}) w300 = windows.get('300', {}).get('tracking_data', {}) w750 = windows.get('750', {}).get('tracking_data', {}) v50 = w50.get('lambda_max_velocity') v150 = w150.get('lambda_max_velocity') v300 = w300.get('lambda_max_velocity') v750 = w750.get('lambda_max_velocity') # Validation: skip if v50 or v150 is None if v50 is None or v150 is None: continue # Extract BTC price for validation pricing = data.get('pricing_data', {}) current_prices = pricing.get('current_prices', {}) btc_price = current_prices.get('BTCUSDT') # Validation: skip if BTC price is missing or <= 0 if btc_price is None or btc_price <= 0: continue # Compute vel_div vel_div = float(v50) - float(v150) # Extract instability scores r50 = windows.get('50', {}).get('regime_signals', {}) r150 = windows.get('150', {}).get('regime_signals', {}) inst50 = r50.get('instability_score') inst150 = r150.get('instability_score') # Build row row = { 'timestamp': timestamp, 'scan_number': scan_number, 'v50_lambda_max_velocity': float(v50), 'v150_lambda_max_velocity': float(v150), 'v300_lambda_max_velocity': float(v300) if v300 is not None else np.nan, 'v750_lambda_max_velocity': float(v750) if v750 is not None else np.nan, 'vel_div': vel_div, 'instability_50': inst50 if inst50 is not None else np.nan, 'instability_150': inst150 if inst150 is not None else np.nan, } # Add asset prices (forward-fill missing) for asset, price in current_prices.items(): if asset in EXCLUDED_ASSETS: continue if price is None or price <= 0: # Forward fill from last known price price = last_prices.get(asset) else: last_prices[asset] = price if price is not None: row[asset] = float(price) rows.append(row) except Exception as e: # Skip malformed files continue if not rows: return None # Build DataFrame df = pd.DataFrame(rows) df = df.sort_values('timestamp').reset_index(drop=True) # Forward-fill any remaining NaN prices price_cols = [c for c in df.columns if c not in ['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']] df[price_cols] = df[price_cols].ffill() # Only keep columns with full alignment (same count as BTCUSDT) btc_count = df['BTCUSDT'].notna().sum() if 'BTCUSDT' in df.columns else 0 valid_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'] for col in price_cols: if col in df.columns and df[col].notna().sum() == btc_count: valid_cols.append(col) df = df[valid_cols] return df def build_parquet_cache( data_path: Path = DATA_PATH, cache_dir: Path = CACHE_DIR, max_workers: int = 4, dates: Optional[List[str]] = None, force: bool = False ) -> Dict: """ Build or update Parquet cache from JSON scan files. Args: data_path: Root eigenvalues directory cache_dir: Output directory for Parquet files max_workers: Number of parallel processes for JSON loading dates: Optional list of specific dates to process (default: all) force: If True, reprocess even if cache file exists Returns: Dict with stats """ if not HAS_PYARROW: raise RuntimeError("PyArrow required for Parquet cache. Install: pip install pyarrow") start_time = time.time() # Find date directories to process if dates: # Process only specified dates date_dirs = [] for d in data_path.iterdir(): if d.is_dir() and not d.name.endswith('_SKIP') and d.name in dates: date_dirs.append(d) print(f"Processing {len(date_dirs)} specified date directories") else: # Find all date directories (excluding _SKIP) if force: date_dirs = sorted([d for d in data_path.iterdir() if d.is_dir() and not d.name.endswith('_SKIP')]) print(f"Force rebuild: Processing all {len(date_dirs)} date directories") else: # Only process dates that don't have cache or are stale stale_dates = check_cache_freshness(data_path, cache_dir) if stale_dates: date_dirs = [d for d in data_path.iterdir() if d.is_dir() and d.name in stale_dates] print(f"Incremental update: {len(date_dirs)} dates need updating") print(f" Missing/stale: {', '.join(stale_dates[:5])}{'...' if len(stale_dates) > 5 else ''}") else: print("Cache is up to date! No dates need processing.") return { 'dates_processed': 0, 'dates_skipped': 0, 'total_scans': 0, 'elapsed_s': 0, 'elapsed_min': 0, 'mode': 'incremental (up-to-date)' } total_scans = 0 skipped_scans = 0 processed_dates = 0 # Process dates in parallel with ProcessPoolExecutor(max_workers=max_workers) as executor: future_to_date = {executor.submit(_process_date, d): d for d in date_dirs} for future in as_completed(future_to_date): date_dir = future_to_date[future] date_str = date_dir.name try: df = future.result() if df is not None and len(df) > 0: # Save to Parquet cache_file = cache_dir / f"{date_str}.parquet" df.to_parquet(cache_file, engine='pyarrow', compression='snappy') total_scans += len(df) processed_dates += 1 print(f" {date_str}: {len(df):,} scans -> {cache_file.name}") else: skipped_scans += 1 print(f" {date_str}: No valid scans") except Exception as e: print(f" {date_str}: ERROR - {e}") skipped_scans += 1 elapsed = time.time() - start_time stats = { 'dates_processed': processed_dates, 'dates_skipped': skipped_scans, 'total_scans': total_scans, 'elapsed_s': elapsed, 'elapsed_min': elapsed / 60 } print(f"\nCache build complete:") print(f" Dates processed: {processed_dates}") print(f" Total scans: {total_scans:,}") print(f" Time: {elapsed:.1f}s ({elapsed/60:.1f} min)") return stats def load_all_data( cache_dir: Path = CACHE_DIR, dates: Optional[List[str]] = None, assets: Optional[List[str]] = None ) -> pd.DataFrame: """ Load cached Parquet files into a single DataFrame. Args: cache_dir: Directory containing .parquet files dates: Optional list of date strings to load (default: all) assets: Optional list of asset columns to include (default: all) Returns: pd.DataFrame with DatetimeIndex, sorted chronologically Expected load time: ~3-5 seconds for all data (~264K rows) Expected memory: ~130MB uncompressed """ if not HAS_PYARROW: raise RuntimeError("PyArrow required. Install: pip install pyarrow") cache_files = sorted(cache_dir.glob('*.parquet')) if dates: # Filter to specific dates date_set = set(dates) cache_files = [f for f in cache_files if f.stem in date_set] if not cache_files: raise ValueError(f"No Parquet files found in {cache_dir}") print(f"Loading {len(cache_files)} Parquet files...") dfs = [] for cf in cache_files: df = pd.read_parquet(cf) dfs.append(df) # Concatenate and sort full_df = pd.concat(dfs, ignore_index=True) full_df = full_df.sort_values('timestamp').reset_index(drop=True) # Filter to specific assets if requested if assets: core_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'] keep_cols = core_cols + [a for a in assets if a in full_df.columns] full_df = full_df[[c for c in keep_cols if c in full_df.columns]] print(f"Loaded {len(full_df):,} rows, {len(full_df.columns)} columns") return full_df def check_cache_freshness( data_path: Path = DATA_PATH, cache_dir: Path = CACHE_DIR ) -> List[str]: """ Compare date directory modification times to cache file times. Returns list of dates that need rebuilding. """ stale_dates = [] # Get all date directories date_dirs = {d.name: d for d in data_path.iterdir() if d.is_dir() and not d.name.endswith('_SKIP')} # Check each cache file for date_str, date_dir in date_dirs.items(): cache_file = cache_dir / f"{date_str}.parquet" if not cache_file.exists(): stale_dates.append(date_str) continue # Compare mtimes data_mtime = date_dir.stat().st_mtime cache_mtime = cache_file.stat().st_mtime if data_mtime > cache_mtime: stale_dates.append(date_str) return stale_dates # ═══════════════════════════════════════════════════════════════════════════════ # SECTION 2: VBT CUSTOM INDICATOR # ═══════════════════════════════════════════════════════════════════════════════ def compute_vel_div_signals(v50_vel, v150_vel, threshold=-0.02): """ Compute vel_div and entry signal (vectorized, non-Numba). Args: v50_vel: pd.Series or np.ndarray - window-50 lambda_max_velocity v150_vel: pd.Series or np.ndarray - window-150 lambda_max_velocity threshold: float - entry threshold (e.g., -0.02) Returns: vel_div: pd.Series or np.ndarray - v50 - v150 signal: pd.Series or np.ndarray (bool) - True where vel_div < threshold """ vel_div = v50_vel - v150_vel signal = vel_div < threshold return vel_div, signal # VBT IndicatorFactory registration (simplified for parameter sweeps) VelDivIndicator = vbt.IndicatorFactory( class_name='VelDiv', short_name='vd', input_names=['v50_vel', 'v150_vel'], param_names=['threshold'], output_names=['vel_div', 'signal'] ).from_apply_func( compute_vel_div_signals, # Default parameter value threshold=-0.02 ) # ═══════════════════════════════════════════════════════════════════════════════ # SECTION 3: SIGNAL GENERATION # ═══════════════════════════════════════════════════════════════════════════════ def precompute_volatility(prices: pd.Series, window: int = 50) -> pd.Series: """ Vectorized rolling realized volatility matching itest_v7 exactly. itest_v7.compute_volatility(prices, i, 50): seg = prices[max(0, i-50):i] # 50 prices ending at i-1 (NOT i) rets = np.diff(seg) / seg[:-1] # 49 returns return np.std(rets) # ddof=0 Equivalent: std of the 49 most recent returns BEFORE bar i. = returns.rolling(49).std(ddof=0).shift(1) Args: prices: Price series window: Rolling window size (50 = look at 50 prices = 49 returns) Returns: Volatility series (std dev of returns) """ returns = prices.pct_change() # window-1 returns from the 'window' prices, shifted by 1 to exclude bar i's return vol = returns.rolling(window=window - 1, min_periods=max(9, (window - 1) // 2)).std(ddof=0).shift(1) return vol def classify_vol_regime(vol: pd.Series, vol_percentiles: Dict) -> pd.Series: """ Map volatility to regime labels. Matches itest_v7.py classify_vol_regime: - vol <= p20: 'very_low' - vol <= p40: 'low' - vol <= p60: 'normal' - vol <= p80: 'elevated' - vol > p80: 'high' Args: vol: Volatility series vol_percentiles: Dict with p20, p40, p60, p80 thresholds Returns: Series of regime labels """ p20 = vol_percentiles.get('p20', vol.quantile(0.2)) p40 = vol_percentiles.get('p40', vol.quantile(0.4)) p60 = vol_percentiles.get('p60', vol.quantile(0.6)) p80 = vol_percentiles.get('p80', vol.quantile(0.8)) regimes = pd.Series(index=vol.index, dtype='object') regimes[vol <= p20] = 'very_low' regimes[(vol > p20) & (vol <= p40)] = 'low' regimes[(vol > p40) & (vol <= p60)] = 'normal' regimes[(vol > p60) & (vol <= p80)] = 'elevated' regimes[vol > p80] = 'high' return regimes def compute_vol_percentiles( df: pd.DataFrame, sample_dates: int = 2, price_col: str = 'BTCUSDT' ) -> Dict: """ Compute volatility percentiles for regime classification. Matches itest_v7.py Phase 1 (lines 925-952): - Sample first 2 dates, ALL scans per date (not truncated) - For each bar from 60 onwards, compute_volatility(prices, i, 50) - Return dict with p20, p40, p60, p80 Args: df: Full DataFrame sample_dates: Number of dates to sample price_col: Price column to use Returns: Dict with percentile thresholds """ # Group by date (from timestamp) df_copy = df.copy() df_copy['date'] = df_copy['timestamp'].dt.date # Get unique dates dates = sorted(df_copy['date'].unique())[:sample_dates] all_vols = [] for date in dates: date_df = df_copy[df_copy['date'] == date] prices = date_df[price_col].values if len(prices) < 100: continue # Match itest_v7: for i in range(60, len(p)), compute vol from prices[max(0,i-50):i] for i in range(60, len(prices)): start = max(0, i - 50) seg = prices[start:i] if len(seg) < 10: continue rets = np.diff(seg) / seg[:-1] v = float(np.std(rets)) # ddof=0, matching itest_v7 if v > 0: all_vols.append(v) if not all_vols: # Fallback: use full data prices_full = df[price_col] vol = precompute_volatility(prices_full, window=50) all_vols = vol.dropna().values.tolist() return { 'p20': float(np.percentile(all_vols, 20)), 'p40': float(np.percentile(all_vols, 40)), 'p60': float(np.percentile(all_vols, 60)), 'p80': float(np.percentile(all_vols, 80)), } def build_entry_signals( df: pd.DataFrame, vel_div_threshold: float = -0.02, vol_filter: str = 'all', lookback: int = 100, vol_percentiles: Optional[Dict] = None, direction: str = 'SHORT' ) -> pd.Series: """ Build boolean entry signal array. Logic (matching itest_v7): 1. SHORT: vel_div < threshold (negative) 2. LONG: vel_div > threshold (positive) 3. vol_regime matches vol_filter 4. bar_index >= lookback (skip first 100 bars per date) Args: df: Full DataFrame with 'vel_div' and price columns vel_div_threshold: Signal threshold (use negative for SHORT, positive for LONG) vol_filter: 'all', 'high', 'low', 'low_normal' lookback: Minimum bars before first signal vol_percentiles: Dict with volatility percentiles direction: 'SHORT' or 'LONG' Returns: pd.Series of bool, same index as df """ # Signal: vel_div < threshold for ALL directions (matching itest_v7 line 1017) # Direction determines what to DO (short or long), not the signal condition entries = df['vel_div'] < vel_div_threshold # Add bar index within each date df = df.copy() df['date'] = df['timestamp'].dt.date df['bar_idx'] = df.groupby('date').cumcount() # Lookback filter entries = entries & (df['bar_idx'] >= lookback) # Volatility filter if vol_filter != 'all' and 'BTCUSDT' in df.columns: if vol_percentiles is None: vol_percentiles = compute_vol_percentiles(df) # Compute volatility PER DATE (matching itest_v7 which loads each date separately) # This prevents cross-date rolling window contamination at date boundaries vol = pd.Series(np.nan, index=df.index, dtype=np.float64) for date_val, grp in df.groupby('date'): date_prices = grp['BTCUSDT'] date_vol = precompute_volatility(date_prices, window=50) vol.loc[grp.index] = date_vol.values regimes = classify_vol_regime(vol, vol_percentiles) if vol_filter == 'high': # itest_v7 line 1025: vol_regime not in ('elevated', 'high') -> skip # So 'high' filter accepts BOTH 'elevated' and 'high' entries = entries & ((regimes == 'elevated') | (regimes == 'high')) elif vol_filter == 'low': entries = entries & ((regimes == 'low') | (regimes == 'very_low')) elif vol_filter == 'low_normal': entries = entries & ((regimes == 'low') | (regimes == 'normal') | (regimes == 'very_low')) elif vol_filter == 'elevated': entries = entries & ((regimes == 'elevated') | (regimes == 'high')) # NOTE: Do NOT edge-detect here. Re-entry after trade exit is handled by # dolphin_order_func_nb's position_now == 0 check. Edge detection would # kill re-entry when vel_div stays below threshold after a trade exits. return entries # ═══════════════════════════════════════════════════════════════════════════════ # SECTION 3B: IRP (Instrument Responsiveness Profile) # ═══════════════════════════════════════════════════════════════════════════════ @njit def compute_irp_nb(price_segment, direction): """ Compute IRP metrics for a price segment. Matches itest_v7 lines 477-508. Args: price_segment: 1D float64 array of prices (last N bars) direction: int (-1 for SHORT/bearish, +1 for LONG/bullish) Returns: (efficiency, alignment, noise, latency, mfe, mae) """ n = len(price_segment) if n < 3: return 0.0, 0.0, 0.0, 50.0, 0.0, 0.0 # Direction-aligned returns n_ret = n - 1 dir_returns = np.empty(n_ret, dtype=np.float64) for i in range(n_ret): dir_returns[i] = (price_segment[i + 1] - price_segment[i]) * direction # Cumulative P&L with leading zero cumulative = np.empty(n_ret, dtype=np.float64) cumulative[0] = dir_returns[0] for i in range(1, n_ret): cumulative[i] = cumulative[i - 1] + dir_returns[i] # MFE / MAE (include zero start) mfe = 0.0 min_val = 0.0 for i in range(n_ret): if cumulative[i] > mfe: mfe = cumulative[i] if cumulative[i] < min_val: min_val = cumulative[i] mae = abs(min_val) if min_val < 0 else 0.0 # Efficiency efficiency = mfe / (mae + 1e-6) # Alignment: fraction of ticks moving in desired direction aligned = 0 for i in range(n_ret): if dir_returns[i] > 0: aligned += 1 alignment = float(aligned) / float(n_ret) # Noise (variance of dir_returns) mean_r = 0.0 for i in range(n_ret): mean_r += dir_returns[i] mean_r /= n_ret noise = 0.0 for i in range(n_ret): noise += (dir_returns[i] - mean_r) ** 2 noise /= n_ret # Latency: bars to reach 10% of MFE latency = 50.0 if mfe > 0: target = mfe * 0.1 for i in range(n_ret): if cumulative[i] >= target: latency = float(i + 1) break return efficiency, alignment, noise, latency, mfe, mae @njit def compute_ars_nb(efficiency, alignment, noise): """ Compute Asset Responsiveness Score. Matches itest_v7 lines 511-514. 50% log(efficiency), 35% alignment, -15% noise*1000. """ eff = np.log1p(efficiency) return 0.5 * eff + 0.35 * alignment - 0.15 * noise * 1000.0 @njit def rank_assets_irp_nb( all_prices_2d, # (n_bars, n_assets) float64 idx, # current bar index regime_direction, # -1 (bearish) or +1 (bullish) irp_lookback, # 50 noise_max, # 500.0 latency_max, # 20 alignment_min, # 0.20 ): """ Rank all assets by ARS. Returns (n_valid, 5) array: col0=asset_idx, col1=ars, col2=trade_direction, col3=alignment, col4=efficiency. Matches itest_v7 lines 517-571. """ n_assets = all_prices_2d.shape[1] results = np.empty((n_assets, 5), dtype=np.float64) n_valid = 0 seg_start = max(0, idx - irp_lookback) if idx - seg_start < 3: return results[:0] for a in range(n_assets): segment = all_prices_2d[seg_start:idx, a] if segment[-1] <= 0: continue # Evaluate DIRECT (with regime) d_eff, d_align, d_noise, d_lat, d_mfe, d_mae = compute_irp_nb(segment, regime_direction) d_ars = compute_ars_nb(d_eff, d_align, d_noise) # Evaluate INVERSE (against regime) i_eff, i_align, i_noise, i_lat, i_mfe, i_mae = compute_irp_nb(segment, -regime_direction) i_ars = compute_ars_nb(i_eff, i_align, i_noise) # Pick best orientation if d_ars >= i_ars: ars = d_ars trade_dir = float(regime_direction) best_align = d_align best_noise = d_noise best_lat = d_lat best_eff = d_eff else: ars = i_ars trade_dir = float(-regime_direction) best_align = i_align best_noise = i_noise best_lat = i_lat best_eff = i_eff # Hard gates if best_noise > noise_max: continue if best_lat > latency_max: continue if best_align < alignment_min: continue results[n_valid, 0] = float(a) results[n_valid, 1] = ars results[n_valid, 2] = trade_dir results[n_valid, 3] = best_align results[n_valid, 4] = best_eff n_valid += 1 if n_valid == 0: return results[:0] # Sort by ARS descending (simple insertion sort, n_valid is small) valid = results[:n_valid].copy() for i in range(n_valid): for j in range(i + 1, n_valid): if valid[j, 1] > valid[i, 1]: for k in range(5): tmp = valid[i, k] valid[i, k] = valid[j, k] valid[j, k] = tmp return valid # ═══════════════════════════════════════════════════════════════════════════════ # SECTION 3C: RCDD HELPERS # ═══════════════════════════════════════════════════════════════════════════════ @njit def calculate_adverse_moves_nb(prices, entry_price, direction): """ Event-based average adverse excursion. Matches itest_v7 lines 674-702. Groups contiguous adverse bars into events, records peak per event, averages. direction: -1 (SHORT) or +1 (LONG). """ n = len(prices) total = 0.0 count = 0 i = 0 if direction == -1: # SHORT: adverse = price > entry while i < n: if prices[i] > entry_price: peak = prices[i] while i < n and prices[i] > entry_price: if prices[i] > peak: peak = prices[i] i += 1 total += peak - entry_price count += 1 else: i += 1 else: # LONG: adverse = price < entry while i < n: if prices[i] < entry_price: trough = prices[i] while i < n and prices[i] < entry_price: if prices[i] < trough: trough = prices[i] i += 1 total += entry_price - trough count += 1 else: i += 1 if count == 0: return entry_price * 0.002 # Default return total / count @njit def calculate_favorable_moves_nb(prices, entry_price, direction): """ Event-based average favorable excursion. Matches itest_v7 lines 705-733. Groups contiguous favorable bars into events, records peak per event, averages. direction: -1 (SHORT) or +1 (LONG). """ n = len(prices) total = 0.0 count = 0 i = 0 if direction == -1: # SHORT: favorable = price < entry while i < n: if prices[i] < entry_price: trough = prices[i] while i < n and prices[i] < entry_price: if prices[i] < trough: trough = prices[i] i += 1 total += entry_price - trough count += 1 else: i += 1 else: # LONG: favorable = price > entry while i < n: if prices[i] > entry_price: peak = prices[i] while i < n and prices[i] > entry_price: if prices[i] > peak: peak = prices[i] i += 1 total += peak - entry_price count += 1 else: i += 1 if count == 0: return entry_price * 0.001 # Default return total / count # ═══════════════════════════════════════════════════════════════════════════════ # SECTION 4: NUMBA CALLBACKS # ═══════════════════════════════════════════════════════════════════════════════ @njit def dolphin_adjust_sl_nb( c, trail_activation, trail_distance, max_hold, is_short ): """ Custom stop-loss adjustment callback for VBT from_signals(). Implements: 1. Max hold timeout (forced exit after N bars) 2. Trailing stop activation (only after profit >= trail_activation) 3. Trailing stop exit (pullback from peak >= trail_distance) Args: c: AdjustSLContext with fields: - i: current global row index - col: current column index - position_now: current position size - val_price_now: current valuation price (close) - init_i: row index when position was opened - init_price: entry price - curr_i: current row index - curr_price: current price - curr_stop: current stop level (as fraction) - curr_trail: whether trailing is currently active trail_activation: float - min profit % to activate trailing (e.g., 0.0003) trail_distance: float - pullback % from peak to trigger exit (e.g., 0.0003) max_hold: int - max bars before forced exit (e.g., 120) is_short: bool - True if position is short Returns: tuple(new_stop: float, new_trail: bool) """ bars_held = c.curr_i - c.init_i # ── MAX HOLD: Force exit ── # Setting stop to a tiny value forces VBT to exit at next bar if bars_held >= max_hold: return np.float64(1e-10), False # ── Compute unrealized P&L ── if is_short: pnl_pct = (c.init_price - c.curr_price) / c.init_price else: pnl_pct = (c.curr_price - c.init_price) / c.init_price # ── TRAILING ACTIVATION ── # Only activate trailing after profit exceeds trail_activation if pnl_pct >= trail_activation and not c.curr_trail: # Activate trailing: VBT will now track the peak and apply trail_distance return np.float64(trail_distance), True # ── Keep current state ── return c.curr_stop, c.curr_trail # ═══════════════════════════════════════════════════════════════════════════════ # SECTION 4B: PHASE 2 - CUSTOM ORDER FUNCTION (from_order_func) # ═══════════════════════════════════════════════════════════════════════════════ from vectorbt.portfolio import nb as vbt_nb from vectorbt.portfolio.enums import OrderContext @njit def dolphin_order_func_nb( c, signal_arr, lev_notional, stop_pct, max_hold, trail_activation, trail_distance, fee_rate, is_short, use_trailing, entry_price_arr, # Track entry prices entry_idx_arr, # Track entry indices max_favorable_arr # Track max favorable for trailing ): """ Custom order function for VBT from_order_func(). Phase 2: Full control over entry/exit logic with proper state tracking. Fixes SHORT re-entry bug and implements accurate max_hold/trailing. Args: c: OrderContext - contains position state, prices, etc. signal_arr: int8 array (0=no signal, 1=entry signal) lev_notional: float - position size in dollars stop_pct: float - stop loss percentage (e.g., 0.002) max_hold: int - max bars to hold position trail_activation: float - profit % to activate trailing trail_distance: float - pullback % to trigger exit fee_rate: float - fee percentage per trade is_short: bool - True for SHORT positions entry_price_arr: float array - tracks entry prices per column entry_idx_arr: int array - tracks entry indices per column max_favorable_arr: float array - tracks max favorable PnL % Returns: Order object """ # Access context position_now = c.position_now val_price_now = c.val_price_now i = c.i col = c.col if position_now == 0: # Not in position - check for entry signal if signal_arr[i] == 1: # Guard: don't enter if we can't hold for max_hold bars # (matches itest_v7 line 760-761: entry_idx + max_hold >= len(prices)) if i + max_hold >= len(signal_arr): return vbt_nb.NoOrder # Apply entry slippage (always adverse) # SHORT: sell lower than mid -> entry_price = mid * (1 - slippage) # LONG: buy higher than mid -> entry_price = mid * (1 + slippage) slippage_entry = 0.0002 # 0.02% adverse if is_short: entry_price = val_price_now * (1.0 - slippage_entry) else: entry_price = val_price_now * (1.0 + slippage_entry) # Record entry info (use slipped price for PnL tracking) entry_price_arr[col] = entry_price entry_idx_arr[col] = i max_favorable_arr[col] = 0.0 # Calculate amount from notional value target_amount = lev_notional / val_price_now if is_short: return vbt_nb.order_nb( size=-target_amount, price=entry_price, size_type=0, fees=fee_rate ) else: return vbt_nb.order_nb( size=target_amount, price=entry_price, size_type=0, fees=fee_rate ) else: # In position - check exit conditions entry_price = entry_price_arr[col] entry_idx = entry_idx_arr[col] bars_held = i - entry_idx if entry_price > 0: # Calculate current PnL % (against slipped entry price) if is_short: pnl_pct = (entry_price - val_price_now) / entry_price else: pnl_pct = (val_price_now - entry_price) / entry_price # Update max favorable if pnl_pct > max_favorable_arr[col]: max_favorable_arr[col] = pnl_pct # ── 1. STOP LOSS (checked first - tail risk protection) ── loss_pct = -pnl_pct if loss_pct >= stop_pct: close_size = -position_now # Stop exit: worse slippage (0.05% adverse) slippage_stop = 0.0005 if is_short: # Stop price = entry * (1 + stop_pct), then add stop slippage exit_price = entry_price * (1.0 + stop_pct) * (1.0 + slippage_stop) else: exit_price = entry_price * (1.0 - stop_pct) * (1.0 - slippage_stop) return vbt_nb.order_nb( size=close_size, price=exit_price, size_type=0, fees=fee_rate ) # ── 2. TRAILING STOP ── if use_trailing: max_fav = max_favorable_arr[col] if max_fav >= trail_activation: pullback = max_fav - pnl_pct if pullback >= trail_distance: close_size = -position_now # Normal exit slippage (0.02% adverse) slippage_exit = 0.0002 if is_short: exit_price = val_price_now * (1.0 + slippage_exit) else: exit_price = val_price_now * (1.0 - slippage_exit) return vbt_nb.order_nb( size=close_size, price=exit_price, size_type=0, fees=fee_rate ) # ── 3. MAX HOLD ── if bars_held >= max_hold: close_size = -position_now # Normal exit slippage slippage_exit = 0.0002 if is_short: exit_price = val_price_now * (1.0 + slippage_exit) else: exit_price = val_price_now * (1.0 - slippage_exit) return vbt_nb.order_nb( size=close_size, price=exit_price, size_type=0, fees=fee_rate ) # No exit - hold position return vbt_nb.NoOrder # Default: no order return vbt_nb.NoOrder # ═══════════════════════════════════════════════════════════════════════════════ # SECTION 5: MAKER FILL FILTERING # ═══════════════════════════════════════════════════════════════════════════════ def vel_div_to_confidence(vel_div: float, threshold: float = -0.02, extreme: float = -0.05) -> float: """ Map vel_div to a [0, 1] confidence score. Mapping: - vel_div >= threshold: 0.0 (no signal) - vel_div == threshold: 0.50 (borderline) - vel_div == extreme: 0.90 (strong) - vel_div < extreme: 0.95 (very strong) Args: vel_div: Velocity divergence value threshold: Entry threshold (e.g., -0.02) extreme: Extreme threshold (e.g., -0.05) Returns: Confidence score [0, 1] """ if vel_div >= threshold: return 0.0 ratio = min(1.0, (threshold - vel_div) / (threshold - extreme)) return 0.50 + ratio * 0.40 # Range: [0.50, 0.90] @njit def apply_maker_filter_nb( entries, vel_div_values, threshold, extreme, maker_fill_rate, fill_discount, seed ): """ For each True in entries, determine fill type. Args: entries: bool array - entry signals vel_div_values: float64 array - vel_div values threshold: float - vel_div threshold extreme: float - extreme vel_div maker_fill_rate: float - probability of maker fill (0.62) fill_discount: float - queue position discount (0.80) seed: int - random seed Returns: filtered_entries: bool array entry_fees: float64 array (fee rate for each bar) fill_types: int8 array (0=no entry, 1=maker, 2=taker) """ np.random.seed(seed) n = len(entries) filtered_entries = np.empty(n, dtype=np.bool_) entry_fees = np.empty(n, dtype=np.float64) fill_types = np.empty(n, dtype=np.int8) for i in range(n): if not entries[i]: filtered_entries[i] = False entry_fees[i] = 0.0 fill_types[i] = 0 continue # Compute confidence vel_div = vel_div_values[i] if vel_div >= threshold: conf = 0.0 else: ratio = min(1.0, (threshold - vel_div) / (threshold - extreme)) conf = 0.50 + ratio * 0.40 # Decision if conf >= 0.85: # TAKER filtered_entries[i] = True entry_fees[i] = FEE_TAKER fill_types[i] = 2 elif conf < 0.40: # SKIP (rare - most signals > 0.50) filtered_entries[i] = False entry_fees[i] = 0.0 fill_types[i] = 0 else: # Try MAKER effective_rate = maker_fill_rate * fill_discount if np.random.random() < effective_rate: # MAKER fill filtered_entries[i] = True entry_fees[i] = FEE_MAKER fill_types[i] = 1 else: # TAKER fallback filtered_entries[i] = True entry_fees[i] = FEE_TAKER fill_types[i] = 2 return filtered_entries, entry_fees, fill_types class MakerFillSimulator: """ Phase 2: Replace probabilistic model with actual OB snapshot simulation. This class is a SCAFFOLD. Current implementation uses probabilistic fills. Future implementation will use real OB snapshot data. Interface matches fill_simulator.py: simulate(signal_time, limit_price, direction, timeout_s) -> FillResult """ def __init__(self, ob_data=None, fill_discount=0.80, adverse_abort_bps=5.0): """ Args: ob_data: Optional pd.DataFrame of OB snapshots. If None, uses probabilistic model (Phase 1). fill_discount: Queue position discount factor adverse_abort_bps: Abort threshold for adverse moves """ self.ob_data = ob_data self.fill_discount = fill_discount self.adverse_abort_bps = adverse_abort_bps self._use_real_ob = ob_data is not None def simulate(self, signal_time, limit_price, direction, timeout_s=25.0): """ Returns FillResult (filled, method, fill_price, fill_time_s, fees_paid) Phase 1: Probabilistic Phase 2: Walk through ob_data snapshots """ if self._use_real_ob: return self._simulate_with_ob(signal_time, limit_price, direction, timeout_s) else: return self._simulate_probabilistic(direction) def _simulate_probabilistic(self, direction): """Phase 1: Simple probabilistic fill.""" import random if random.random() < SP_MAKER_FILL_RATE * self.fill_discount: return { 'filled': True, 'method': 'maker', 'fill_price': None, 'fill_time_s': 0, 'fees_paid': 0 } else: return { 'filled': True, 'method': 'taker', 'fill_price': None, 'fill_time_s': 0, 'fees_paid': 0 } def _simulate_with_ob(self, signal_time, limit_price, direction, timeout_s): """Phase 2: Real OB simulation (TODO).""" raise NotImplementedError("Real OB simulation not yet implemented") # ═══════════════════════════════════════════════════════════════════════════════ # SECTION 6: PORTFOLIO SIMULATION # ═══════════════════════════════════════════════════════════════════════════════ def run_backtest( df: pd.DataFrame, # Strategy parameters asset: str = 'BTCUSDT', vel_div_threshold: float = -0.02, direction: str = 'SHORT', stop_pct: float = 0.002, max_hold: int = 120, use_trailing: bool = True, trail_activation: float = 0.0003, trail_distance: float = 0.0003, vol_filter: str = 'all', lookback: int = 100, # Position sizing leverage: float = 2.5, fraction: float = 0.15, init_cash: float = 10000.0, # Fee model use_sp_fees: bool = False, fee_rate: float = 0.0004, fee_maker: float = FEE_MAKER, fee_taker: float = FEE_TAKER, # Maker filter use_maker_filter: bool = False, maker_fill_rate: float = SP_MAKER_FILL_RATE, fill_discount: float = SP_FILL_DISCOUNT, # Vol percentiles (cached) vol_percentiles: Optional[Dict] = None, # Reproducibility seed: int = 42, # Debug verbose: bool = False ) -> vbt.Portfolio: """ Run a single VBT backtest with DOLPHIN parameters. Args: df: Full DataFrame with vel_div and price data asset: Asset to trade (e.g., 'BTCUSDT') vel_div_threshold: Signal threshold (e.g., -0.02) direction: 'SHORT' or 'LONG' stop_pct: Stop loss percentage (e.g., 0.002 = 0.2%) max_hold: Max bars to hold position use_trailing: Enable trailing stop trail_activation: Profit % to activate trailing (e.g., 0.0003) trail_distance: Pullback % to trigger exit (e.g., 0.0003) vol_filter: 'all', 'high', 'low', 'low_normal' lookback: Bars to skip at start of each date leverage: Position leverage fraction: Fraction of capital to use init_cash: Initial capital use_sp_fees: Use SmartPlacer fee model fee_rate: Base fee rate (per side) use_maker_filter: Enable maker fill filtering seed: Random seed verbose: Print debug info Returns: vbt.Portfolio object with full analytics """ if verbose: print(f"Running backtest: asset={asset}, direction={direction}") print(f" vel_div_threshold={vel_div_threshold}, vol_filter={vol_filter}") print(f" trailing={use_trailing}, trail_act={trail_activation}, trail_dist={trail_distance}") # ── 1. Extract price series ───────────────────────────────────────────────── if asset not in df.columns: raise ValueError(f"Asset {asset} not found in DataFrame") price_series = df[asset].copy() # ── 2. Compute vol percentiles if needed ──────────────────────────────────── if vol_filter != 'all' and vol_percentiles is None: vol_percentiles = compute_vol_percentiles(df, price_col=asset) # ── 3. Precompute vol_regime array ────────────────────────────────────────── if vol_filter != 'all': vol = precompute_volatility(price_series, window=50) regimes = classify_vol_regime(vol, vol_percentiles) else: regimes = pd.Series('all', index=price_series.index) # ── 4. Build entry signals ────────────────────────────────────────────────── entries = build_entry_signals( df, vel_div_threshold=vel_div_threshold, vol_filter=vol_filter, lookback=lookback, vol_percentiles=vol_percentiles, direction=direction ) # ── 5. Build exit signals (all False, exits via stops) ────────────────────── exits = pd.Series(False, index=price_series.index) # ── 6. Apply maker fill filter if enabled ─────────────────────────────────── if use_maker_filter: vel_div_values = df['vel_div'].values entries_arr = entries.values filtered_entries_arr, entry_fees, fill_types = apply_maker_filter_nb( entries_arr, vel_div_values, vel_div_threshold, -0.05, # extreme threshold maker_fill_rate, fill_discount, seed ) entries = pd.Series(filtered_entries_arr, index=entries.index) fee_array = entry_fees else: # Uniform fee if use_sp_fees: # SmartPlacer blended entry fee fee_array = fee_maker * 0.62 + fee_taker * 0.38 else: fee_array = fee_rate # ── 7. Determine VBT direction ────────────────────────────────────────────── is_short = direction == 'SHORT' # ── 8. Compute effective position size ────────────────────────────────────── # Approach A: Fixed notional sizing (Phase 1, simpler) lev_notional = init_cash * fraction * leverage # ── 9. Call VBT ───────────────────────────────────────────────────────────── # PHASE 2: Use from_order_func() for full control # This fixes SHORT re-entry and implements accurate max_hold/trailing # Convert entries to int8 signal array (0=no signal, 1=entry) signal_arr = entries.astype(np.int8).values # Compute the per-side fee rate for the order function if use_sp_fees: # SmartPlacer blended: entry = 62% maker + 38% taker, exit varies # Use average per-side fee for simplicity fee_rate_val = (fee_maker * 0.62 + fee_taker * 0.38 + fee_maker * 0.50 + fee_taker * 0.50) / 2.0 elif isinstance(fee_array, (int, float)): fee_rate_val = float(fee_array) else: fee_rate_val = fee_rate n_cols = 1 # Single column for now entry_price_arr = np.full(n_cols, 0.0, dtype=np.float64) entry_idx_arr = np.full(n_cols, -1, dtype=np.int64) max_favorable_arr = np.full(n_cols, 0.0, dtype=np.float64) pf = vbt.Portfolio.from_order_func( price_series, # close dolphin_order_func_nb, # order_func_nb # *order_args (positional args passed to order_func_nb) signal_arr, np.float64(lev_notional), np.float64(stop_pct), np.int64(max_hold), np.float64(trail_activation), np.float64(trail_distance), np.float64(fee_rate_val), # Per-side fee rate np.bool_(is_short), np.bool_(use_trailing), entry_price_arr, entry_idx_arr, max_favorable_arr, # Other kwargs init_cash=init_cash, freq='11s', seed=seed, ) return pf def extract_metrics(pf: vbt.Portfolio, strategy_name: str = '') -> Dict: """ Extract metrics matching itest_v7 output format. Args: pf: VBT Portfolio object strategy_name: Optional strategy name Returns: Dict with metrics """ trades_count = pf.trades.count() if trades_count > 0: trades = pf.trades win_rate = float(trades.win_rate()) profit_factor = float(trades.profit_factor()) avg_trade_return = float(trades.returns.mean()) else: win_rate = 0.0 profit_factor = 0.0 avg_trade_return = 0.0 metrics = { 'strategy': strategy_name, 'trades': int(trades_count), 'win_rate': win_rate, 'profit_factor': profit_factor, 'total_return': float(pf.total_return()), 'max_drawdown': float(pf.max_drawdown()), 'sharpe_ratio': float(pf.sharpe_ratio()), 'calmar_ratio': float(pf.calmar_ratio()), 'final_capital': float(pf.final_value()), 'avg_trade_return': avg_trade_return, } return metrics # ═══════════════════════════════════════════════════════════════════════════════ # SECTION 6B: MULTI-ASSET SIMULATION (Phase II) # ═══════════════════════════════════════════════════════════════════════════════ # ── Alpha Engine Helpers ───────────────────────────────────────────────────── @njit def get_signal_bucket_nb(vel_div, threshold, extreme_vd): """Classify signal into bucket: 0=extreme, 1=strong, 2=moderate, 3=weak.""" if vel_div <= extreme_vd * 1.5: # <= -0.075 return 0 # extreme elif vel_div <= extreme_vd: # <= -0.05 return 1 # strong elif vel_div <= (threshold + extreme_vd) / 2: # <= -0.035 return 2 # moderate return 3 # weak @njit def get_bucket_boost_nb(bucket_wins, bucket_losses, bucket_idx): """Get sizing multiplier based on bucket win rate history.""" w = bucket_wins[bucket_idx] l = bucket_losses[bucket_idx] total = w + l if total == 0: return 1.0 wr = float(w) / float(total) if wr > 0.60: return 1.3 elif wr > 0.55: return 1.1 elif wr < 0.40: return 0.7 elif wr < 0.45: return 0.85 return 1.0 @njit def get_streak_mult_nb(recent_pnls, recent_count): """Get sizing multiplier based on recent trade streak.""" if recent_count < 5: return 1.0 losses = 0 start = max(0, recent_count - 5) for k in range(start, recent_count): if recent_pnls[k % 5] < 0: losses += 1 if losses >= 4: return 0.5 elif losses >= 3: return 0.7 elif losses <= 1: return 1.1 return 1.0 @njit def get_trend_mult_nb(vel_div_arr, i, lookback=10): """Get sizing multiplier based on vel_div trend direction.""" if i < lookback: return 1.0 vd_trend = vel_div_arr[i] - vel_div_arr[i - lookback] if vd_trend < -0.01: return 1.3 # Trend worsening -> stronger signal elif vd_trend > 0.01: return 0.7 # Trend improving -> weaker signal return 1.0 @njit def simulate_multi_asset_nb( all_prices_2d, # (n_bars, n_assets) float64 signal_arr, # (n_bars,) int8 - 1=entry signal, 0=none bar_date_ids, # (n_bars,) int32 - date ID per bar (for lookback reset) # Strategy params stop_pct, # float64 max_hold, # int64 use_trailing, # bool trail_activation, # float64 trail_distance, # float64 fee_rate, # float64 - per-side fee leverage, # float64 fraction, # float64 init_cash, # float64 # IRP use_asset_selection,# bool irp_lookback, # int64 noise_max, # float64 latency_max, # int64 alignment_min, # float64 min_irp_alignment, # float64 # OB edge use_ob_edge, # bool ob_edge_bps, # float64 ob_confirm_rate, # float64 # SP fees & slippage use_sp_fees, # bool - use SP blended fees (vs flat fee_rate) use_sp_slippage, # bool sp_maker_entry_rate,# float64 sp_maker_exit_rate, # float64 # RCDD use_rcdd, # bool rcdd_multiplier, # float64 rcdd_min_stop, # float64 rcdd_lookback, # int64 rcdd_trail, # bool rcdd_trail_mult, # float64 rcdd_activation_mult,# float64 trail_dist_floor, # float64 trail_act_floor, # float64 # Other lookback, # int64 - skip first N bars per date seed, # int64 default_asset_idx, # int64 - BTCUSDT index (fallback when no IRP) date_bar_counts, # (n_dates,) int32 - total bars per date (for end-of-date cutoff) # Alpha engine params vel_div_arr, # (n_bars,) float64 - vel_div per bar use_dynamic_leverage, # bool min_leverage, # float64 max_leverage, # float64 leverage_convexity, # float64 - 1.0=linear, 2.0=quadratic, 3.0=cubic use_alpha_layers, # bool extreme_vd, # float64 use_rcdd_target, # bool vel_div_threshold, # float64 (needed for strength_score) base_fraction, # float64 (strat.fraction, needed for alpha sizing) # Fixed take-profit use_fixed_tp, # bool fixed_tp_pct, # float64 - TP threshold as decimal (e.g., 0.002 for 20bps) # Direction enforcement enforce_direction, # int64 - 0=any (IRP picks), -1=SHORT only, +1=LONG only # Passive entry (SmartPlacer OB-based) use_passive_entry, # bool passive_timeout_bars, # int64 passive_offset_bps, # float64 passive_abort_bps, # float64 passive_fill_discount, # float64 passive_fallback_taker, # bool maker_fee_rate, # float64 taker_fee_rate, # float64 # Direction confirmation (OB imbalance proxy via price momentum) use_direction_confirm, # bool dc_lookback_bars, # int64 dc_min_magnitude_bps, # float64 dc_skip_contradicts, # bool dc_leverage_boost, # float64 dc_leverage_reduce, # float64 ): """ Full multi-asset simulation matching itest_v7. Single trade at a time across all assets, IRP asset selection. """ np.random.seed(seed) n_bars = all_prices_2d.shape[0] capital = init_cash # lev_notional is computed dynamically per trade from current capital trade_lev_notional = 0.0 # Set at entry, used until exit # Trade result storage max_trades = 20000 trade_pnls = np.empty(max_trades, dtype=np.float64) trade_assets = np.empty(max_trades, dtype=np.int64) trade_dirs = np.empty(max_trades, dtype=np.int64) trade_entry_bars = np.empty(max_trades, dtype=np.int64) trade_exit_bars = np.empty(max_trades, dtype=np.int64) trade_exit_types = np.empty(max_trades, dtype=np.int64) # 1=stop,2=trail,3=hold n_trades = 0 wins = 0 stop_exits = 0 trail_exits = 0 hold_exits = 0 total_fees = 0.0 total_slippage_cost = 0.0 long_trades = 0 short_trades = 0 long_pnl = 0.0 short_pnl = 0.0 target_exits = 0 tp_exits = 0 maker_entries = 0 taker_entries = 0 aborted_entries = 0 dc_confirmed = 0 dc_contradicted = 0 dc_neutral = 0 # Alpha layers state (Numba-compatible arrays) bucket_wins = np.zeros(4, dtype=np.int64) # 4 buckets: extreme/strong/moderate/weak bucket_losses = np.zeros(4, dtype=np.int64) recent_pnls = np.zeros(5, dtype=np.float64) # Circular buffer of last 5 trade PnLs recent_count = 0 # State in_trade = False last_exit = -1 entry_price = 0.0 entry_idx = 0 trade_asset_idx = -1 trade_direction = 0 # -1=SHORT, +1=LONG max_favorable = 0.0 # Passive entry state trade_fee_type = 1 # 0=maker, 1=taker (determines entry fee rate) # RCDD-computed params (set at entry, fixed for trade duration) eff_stop = stop_pct eff_trail_dist = trail_distance eff_trail_act = trail_activation target_pct = 0.0 # RCDD target exit threshold # Track date boundaries for lookback gating prev_date_id = -1 bars_in_date = 0 for i in range(n_bars): # Track date boundary cur_date_id = bar_date_ids[i] if cur_date_id != prev_date_id: bars_in_date = 0 prev_date_id = cur_date_id else: bars_in_date += 1 if in_trade: # Skip bars before actual fill (passive entry wait period) if i < entry_idx: continue # Current price of the traded asset curr_price = all_prices_2d[i, trade_asset_idx] if curr_price <= 0: continue bars_held = i - entry_idx # PnL if trade_direction == -1: # SHORT pnl_pct = (entry_price - curr_price) / entry_price else: pnl_pct = (curr_price - entry_price) / entry_price # Update max favorable if pnl_pct > max_favorable: max_favorable = pnl_pct # eff_stop / eff_trail_dist / eff_trail_act are set at entry time # (RCDD computed once, fixed for trade duration - matches itest_v7) exit_type = 0 # 0=none # 0.5 FIXED TAKE-PROFIT if use_fixed_tp and fixed_tp_pct > 0: if pnl_pct >= fixed_tp_pct: exit_type = 5 # 1. STOP LOSS loss_pct = -pnl_pct if exit_type == 0 and loss_pct >= eff_stop: exit_type = 1 # 2. TRAILING STOP if exit_type == 0 and use_trailing: if max_favorable >= eff_trail_act: pullback = max_favorable - pnl_pct if pullback >= eff_trail_dist: exit_type = 2 # 2.5 RCDD TARGET EXIT if exit_type == 0 and use_rcdd_target and target_pct > 0: if pnl_pct >= target_pct: exit_type = 4 # target # 3. MAX HOLD if exit_type == 0 and bars_held >= max_hold: exit_type = 3 if exit_type > 0: # Slippage if exit_type == 1: slippage = 0.0005 # Stop: worse slippage else: slippage = 0.0002 # Normal exit if trade_direction == -1: exit_price = curr_price * (1.0 + slippage) else: exit_price = curr_price * (1.0 - slippage) # Raw PnL if trade_direction == -1: pnl_pct_raw = (entry_price - exit_price) / entry_price else: pnl_pct_raw = (exit_price - entry_price) / entry_price # SP slippage refund (skip if passive entry - already modeled) sp_slip_saved = 0.0 if use_sp_slippage and not use_passive_entry: if np.random.random() < sp_maker_entry_rate: pnl_pct_raw += 0.0002 sp_slip_saved += 0.0002 * trade_lev_notional if exit_type != 1: # Non-stop exits only if np.random.random() < sp_maker_exit_rate: pnl_pct_raw += 0.0002 sp_slip_saved += 0.0002 * trade_lev_notional # OB edge (skip if passive entry - already modeled via limit offset) if use_ob_edge and not use_passive_entry: if np.random.random() < ob_confirm_rate: ob_boost = ob_edge_bps * 1e-4 pnl_pct_raw += ob_boost # Gross PnL (uses trade's lev_notional, set at entry) gross_pnl = pnl_pct_raw * trade_lev_notional # Fees if use_passive_entry: # Passive entry: use actual maker/taker based on fill type if trade_fee_type == 0: # maker fill entry_fee_val = maker_fee_rate * trade_lev_notional else: # taker fill/fallback entry_fee_val = taker_fee_rate * trade_lev_notional # Exit: stop=always taker, other=50% maker blend if exit_type == 1: exit_fee_val = taker_fee_rate * trade_lev_notional else: exit_fee_val = (maker_fee_rate * 0.5 + taker_fee_rate * 0.5) * trade_lev_notional elif use_sp_fees: # SP blended: match itest_v7 lines 1195-1205 entry_fee_val = (0.0002 * sp_maker_entry_rate + 0.0005 * (1.0 - sp_maker_entry_rate)) * trade_lev_notional if exit_type == 1: # stop = always taker exit_fee_val = 0.0005 * trade_lev_notional else: exit_fee_val = (0.0002 * sp_maker_exit_rate + 0.0005 * (1.0 - sp_maker_exit_rate)) * trade_lev_notional else: # Flat fee_rate per side (entry + exit) entry_fee_val = fee_rate * trade_lev_notional exit_fee_val = fee_rate * trade_lev_notional trade_fee = entry_fee_val + exit_fee_val net_pnl = gross_pnl - trade_fee # Track capital += net_pnl total_fees += trade_fee total_slippage_cost += slippage * trade_lev_notional if n_trades < max_trades: trade_pnls[n_trades] = net_pnl trade_assets[n_trades] = trade_asset_idx trade_dirs[n_trades] = trade_direction trade_entry_bars[n_trades] = entry_idx trade_exit_bars[n_trades] = i trade_exit_types[n_trades] = exit_type n_trades += 1 if net_pnl > 0: wins += 1 if exit_type == 1: stop_exits += 1 elif exit_type == 2: trail_exits += 1 elif exit_type == 4: target_exits += 1 elif exit_type == 5: tp_exits += 1 else: hold_exits += 1 if trade_direction == -1: short_trades += 1 short_pnl += net_pnl else: long_trades += 1 long_pnl += net_pnl # Alpha layers: record trade outcome per bucket if use_alpha_layers: bucket_idx = get_signal_bucket_nb( vel_div_arr[entry_idx], vel_div_threshold, extreme_vd) if net_pnl > 0: bucket_wins[bucket_idx] += 1 else: bucket_losses[bucket_idx] += 1 recent_pnls[recent_count % 5] = net_pnl recent_count += 1 in_trade = False last_exit = i else: # Not in trade - check for entry if signal_arr[i] != 1: continue if i <= last_exit: continue if bars_in_date < lookback: continue if i + max_hold >= n_bars: continue # Asset selection if use_asset_selection: rankings = rank_assets_irp_nb( all_prices_2d, i, -1, # -1 = bearish regime irp_lookback, noise_max, latency_max, alignment_min ) if len(rankings) == 0: continue # Find best asset matching direction constraint found_asset = False for ri in range(len(rankings)): r_asset = int(rankings[ri, 0]) r_dir = int(rankings[ri, 2]) r_align = rankings[ri, 3] # Direction enforcement: skip if direction doesn't match if enforce_direction != 0 and r_dir != enforce_direction: continue if min_irp_alignment > 0 and r_align < min_irp_alignment: continue top_asset_idx = r_asset top_direction = r_dir top_alignment = r_align found_asset = True break if not found_asset: continue else: top_asset_idx = default_asset_idx if enforce_direction != 0: top_direction = enforce_direction else: top_direction = -1 # Default: SHORT entry_raw = all_prices_2d[i, top_asset_idx] if entry_raw <= 0: continue # Dynamic position sizing (matches itest_v7 line 1107) if capital <= 0: continue # Bankrupt # Alpha engine: compute effective leverage and fraction eff_leverage = leverage eff_fraction = fraction if use_dynamic_leverage or use_alpha_layers: vd = vel_div_arr[i] if vd <= extreme_vd: strength_score = 1.0 else: denom = vel_div_threshold - extreme_vd if denom != 0.0: strength_score = (vel_div_threshold - vd) / denom else: strength_score = 0.5 strength_score = max(0.0, min(1.0, strength_score)) if use_dynamic_leverage: # Convex scaling: strength_score^convexity concentrates leverage on strong signals # convexity=1.0: linear, 2.0: quadratic, 3.0: cubic scaled_score = strength_score ** leverage_convexity eff_leverage = min_leverage + scaled_score * (max_leverage - min_leverage) eff_leverage = min(eff_leverage, max_leverage) if use_alpha_layers: is_extreme = vd <= extreme_vd confidence = 0.7 if is_extreme else 0.55 confidence_mult = confidence / 0.95 extreme_boost = 2.0 if is_extreme else 1.0 bucket_idx = get_signal_bucket_nb(vd, vel_div_threshold, extreme_vd) bb = get_bucket_boost_nb(bucket_wins, bucket_losses, bucket_idx) sm = get_streak_mult_nb(recent_pnls, recent_count) tm = get_trend_mult_nb(vel_div_arr, i) base_frac = 0.02 + strength_score * (base_fraction - 0.02) eff_fraction = base_frac * confidence_mult * extreme_boost * tm * bb * sm eff_fraction = max(0.02, min(eff_fraction, base_fraction)) # Direction confirmation (OB imbalance proxy via price momentum) if use_direction_confirm: dc_start_idx = max(0, i - dc_lookback_bars) if dc_start_idx < i: dc_p0 = all_prices_2d[dc_start_idx, top_asset_idx] dc_p1 = all_prices_2d[i, top_asset_idx] if dc_p0 > 0 and dc_p1 > 0: dc_chg_bps = (dc_p1 - dc_p0) / dc_p0 * 10000.0 # SHORT: falling price = sell pressure = CONFIRMS # LONG: rising price = buy pressure = CONFIRMS if top_direction == -1: # SHORT dc_is_confirm = dc_chg_bps < -dc_min_magnitude_bps dc_is_contradict = dc_chg_bps > dc_min_magnitude_bps else: # LONG dc_is_confirm = dc_chg_bps > dc_min_magnitude_bps dc_is_contradict = dc_chg_bps < -dc_min_magnitude_bps if dc_is_confirm: dc_confirmed += 1 eff_leverage = min(eff_leverage * dc_leverage_boost, max_leverage) elif dc_is_contradict: dc_contradicted += 1 if dc_skip_contradicts: continue # Skip this trade entirely else: eff_leverage *= dc_leverage_reduce else: dc_neutral += 1 trade_lev_notional = capital * eff_fraction * eff_leverage # ── ENTRY EXECUTION ────────────────────────────────────── trade_fee_type = 1 # default: taker if use_passive_entry: # Passive entry: place limit order, wait for fill if top_direction == -1: # SHORT: sell at higher price limit_price = entry_raw * (1.0 + passive_offset_bps / 10000.0) else: # LONG: buy at lower price limit_price = entry_raw * (1.0 - passive_offset_bps / 10000.0) filled_maker = False aborted = False fill_bar = i for wi in range(1, passive_timeout_bars + 1): j = i + wi if j >= n_bars: break wp = all_prices_2d[j, top_asset_idx] if wp <= 0: continue # Adverse move check (from signal bar price) move_bps = (wp - entry_raw) / entry_raw * 10000.0 if top_direction == -1: # SHORT: adverse = price UP if move_bps > passive_abort_bps: aborted = True break # Fill: price rose to our ask if wp >= limit_price: if np.random.random() < passive_fill_discount: filled_maker = True fill_bar = j break else: # LONG: adverse = price DOWN if move_bps < -passive_abort_bps: aborted = True break # Fill: price dipped to our bid if wp <= limit_price: if np.random.random() < passive_fill_discount: filled_maker = True fill_bar = j break if aborted: last_exit = i + passive_timeout_bars aborted_entries += 1 continue # Skip this trade if filled_maker: entry_price = limit_price # Better entry at our limit entry_idx = fill_bar trade_fee_type = 0 # maker maker_entries += 1 elif passive_fallback_taker: fb_bar = min(i + passive_timeout_bars, n_bars - 1) fb_price = all_prices_2d[fb_bar, top_asset_idx] if fb_price <= 0: continue # Taker entry with slippage if top_direction == -1: entry_price = fb_price * (1.0 - 0.0002) else: entry_price = fb_price * (1.0 + 0.0002) entry_idx = fb_bar trade_fee_type = 1 # taker fallback taker_entries += 1 else: # Abort on timeout (no fallback) last_exit = i + passive_timeout_bars aborted_entries += 1 continue else: # Immediate taker entry (current behavior) if top_direction == -1: entry_price = entry_raw * (1.0 - 0.0002) else: entry_price = entry_raw * (1.0 + 0.0002) entry_idx = i taker_entries += 1 trade_asset_idx = top_asset_idx trade_direction = top_direction max_favorable = 0.0 # Compute RCDD at entry (fixed for trade duration, matches itest_v7) eff_stop = stop_pct eff_trail_dist = trail_distance eff_trail_act = trail_activation target_pct = 0.0 if use_rcdd: hist_start = max(0, i - rcdd_lookback) history = all_prices_2d[hist_start:i, top_asset_idx] if len(history) > 10: avg_adv = calculate_adverse_moves_nb( history, entry_raw, trade_direction) rcdd_stop_val = (avg_adv / entry_raw) * rcdd_multiplier rcdd_stop_val = max(rcdd_stop_val, rcdd_min_stop) eff_stop = max(stop_pct, rcdd_stop_val) if rcdd_trail and use_trailing: avg_fav = calculate_favorable_moves_nb( history, entry_raw, trade_direction) rcdd_td = (avg_adv / entry_raw) * rcdd_trail_mult eff_trail_dist = max(trail_dist_floor, min(0.005, rcdd_td)) rcdd_act = (avg_fav / entry_raw) * rcdd_activation_mult eff_trail_act = max(trail_act_floor, min(0.01, rcdd_act)) # RCDD target: early exit on favorable move if use_rcdd_target: avg_fav_t = calculate_favorable_moves_nb( history, entry_raw, trade_direction) target_pct = avg_fav_t / entry_raw in_trade = True # Compute summary win_rate = float(wins) / float(n_trades) if n_trades > 0 else 0.0 gross_wins = 0.0 gross_losses = 0.0 for j in range(n_trades): if trade_pnls[j] > 0: gross_wins += trade_pnls[j] else: gross_losses += abs(trade_pnls[j]) profit_factor = gross_wins / gross_losses if gross_losses > 0 else 0.0 return (capital, n_trades, wins, win_rate, profit_factor, stop_exits, trail_exits, hold_exits, total_fees, total_slippage_cost, long_trades, short_trades, long_pnl, short_pnl, target_exits, tp_exits, maker_entries, taker_entries, aborted_entries, dc_confirmed, dc_contradicted, dc_neutral) def run_full_backtest( df: pd.DataFrame, strategy: Strategy, init_cash: float = 10000.0, seed: int = 42, verbose: bool = True, ) -> Dict: """ Run full multi-asset backtest matching itest_v7 logic. Args: df: Full DataFrame from load_all_data() strategy: Strategy config init_cash: Starting capital seed: Random seed verbose: Print progress Returns: Dict with itest_v7-compatible metrics """ if verbose: print(f" Strategy: {strategy.name}") print(f" asset_selection={strategy.use_asset_selection}, " f"sp_fees={strategy.use_sp_fees}, ob_edge={strategy.use_ob_edge}, " f"rcdd={strategy.use_rcdd}") if strategy.dynamic_leverage or strategy.use_alpha_layers: print(f" dynamic_lev={strategy.dynamic_leverage} " f"(min={strategy.min_leverage}, max={strategy.max_leverage}, " f"convex={strategy.leverage_convexity}), " f"alpha_layers={strategy.use_alpha_layers}, " f"rcdd_target={strategy.use_rcdd_target}") if strategy.use_passive_entry: print(f" passive_entry: timeout={strategy.passive_timeout_bars}bars " f"offset={strategy.passive_offset_bps}bps abort={strategy.passive_abort_bps}bps " f"fill_disc={strategy.passive_fill_discount} " f"fallback={'taker' if strategy.passive_fallback_taker else 'abort'}") if strategy.use_direction_confirm: action = 'skip' if strategy.dc_skip_contradicts else f'reduce×{strategy.dc_leverage_reduce}' print(f" dir_confirm: lookback={strategy.dc_lookback_bars}bars " f"mag={strategy.dc_min_magnitude_bps}bps boost×{strategy.dc_leverage_boost} " f"contradict={action}") # Identify asset columns (exclude meta columns) meta_cols = {'timestamp', 'scan_number', 'v50_vel', 'v150_vel', 'vel_div', 'date_str', 'instability_50', 'instability_150', 'v50_lambda_max_velocity', 'v150_lambda_max_velocity', 'v300_lambda_max_velocity', 'v750_lambda_max_velocity'} asset_cols = [c for c in df.columns if c not in meta_cols and c.endswith('USDT')] asset_cols = sorted(asset_cols) if verbose: print(f" Assets: {len(asset_cols)}") # Build 2D price array all_prices_2d = df[asset_cols].values.astype(np.float64) # Find default asset index (BTCUSDT) default_asset_idx = asset_cols.index('BTCUSDT') if 'BTCUSDT' in asset_cols else 0 # Build signal array entries = build_entry_signals( df, vel_div_threshold=strategy.vel_div_threshold, vol_filter=strategy.vol_filter, lookback=0, # Lookback handled inside simulation ) signal_arr = entries.astype(np.int8).values # Build date ID array for lookback gating if 'date_str' in df.columns: date_strings = df['date_str'].values else: date_strings = df['timestamp'].dt.date.astype(str).values unique_dates = np.unique(date_strings) date_map = {d: i for i, d in enumerate(unique_dates)} bar_date_ids = np.array([date_map[d] for d in date_strings], dtype=np.int32) # Build per-date bar counts (for end-of-date cutoff, matching itest_v7) n_unique_dates = len(unique_dates) date_bar_counts = np.zeros(n_unique_dates, dtype=np.int32) for did in bar_date_ids: date_bar_counts[did] += 1 # Fee rate if strategy.fee_rate_override >= 0: fee_rate = strategy.fee_rate_override elif strategy.use_sp_fees: entry_fee = SP_MAKER_FILL_RATE * FEE_MAKER + (1 - SP_MAKER_FILL_RATE) * FEE_TAKER exit_fee = SP_MAKER_EXIT_RATE * FEE_MAKER + (1 - SP_MAKER_EXIT_RATE) * FEE_TAKER fee_rate = (entry_fee + exit_fee) / 2.0 else: fee_rate = FEE_RATE_REALISTIC / 2.0 # per-side if verbose: print(f" Fee rate (per-side): {fee_rate*100:.4f}%") print(f" Signals: {signal_arr.sum()}") # Build vel_div array for alpha engine vel_div_arr = df['vel_div'].values.astype(np.float64) t0 = time.time() result = simulate_multi_asset_nb( all_prices_2d, signal_arr, bar_date_ids, np.float64(strategy.stop_pct), np.int64(strategy.max_hold), np.bool_(strategy.use_trailing), np.float64(strategy.trail_activation), np.float64(strategy.trail_distance), np.float64(fee_rate), np.float64(strategy.leverage), np.float64(strategy.fraction), np.float64(init_cash), np.bool_(strategy.use_asset_selection), np.int64(IRP_LOOKBACK), np.float64(IRP_NOISE_MAX), np.int64(IRP_LATENCY_MAX), np.float64(IRP_ALIGNMENT_MIN), np.float64(strategy.min_irp_alignment), np.bool_(strategy.use_ob_edge), np.float64(strategy.ob_edge_bps), np.float64(OB_CONFIRM_RATE), np.bool_(strategy.use_sp_fees), np.bool_(strategy.use_sp_slippage), np.float64(SP_MAKER_FILL_RATE), np.float64(SP_MAKER_EXIT_RATE), np.bool_(strategy.use_rcdd), np.float64(strategy.rcdd_multiplier), np.float64(strategy.rcdd_min_stop), np.int64(RCDD_LOOKBACK), np.bool_(strategy.rcdd_trail), np.float64(strategy.rcdd_trail_mult), np.float64(strategy.rcdd_activation_mult), np.float64(strategy.trail_dist_floor), np.float64(strategy.trail_act_floor), np.int64(strategy.lookback), np.int64(seed), np.int64(default_asset_idx), date_bar_counts, # Alpha engine params vel_div_arr, np.bool_(strategy.dynamic_leverage), np.float64(strategy.min_leverage), np.float64(strategy.max_leverage), np.float64(strategy.leverage_convexity), np.bool_(strategy.use_alpha_layers), np.float64(EXTREME_VD), np.bool_(strategy.use_rcdd_target), np.float64(strategy.vel_div_threshold), np.float64(strategy.fraction), # Fixed take-profit np.bool_(strategy.use_fixed_tp), np.float64(strategy.fixed_tp_pct), # Direction enforcement np.int64(-1 if strategy.direction == 'SHORT' else (1 if strategy.direction == 'LONG' else 0)), # Passive entry (SmartPlacer OB-based) np.bool_(strategy.use_passive_entry), np.int64(strategy.passive_timeout_bars), np.float64(strategy.passive_offset_bps), np.float64(strategy.passive_abort_bps), np.float64(strategy.passive_fill_discount), np.bool_(strategy.passive_fallback_taker), np.float64(strategy.maker_fee_rate), np.float64(strategy.taker_fee_rate), # Direction confirmation (OB imbalance proxy) np.bool_(strategy.use_direction_confirm), np.int64(strategy.dc_lookback_bars), np.float64(strategy.dc_min_magnitude_bps), np.bool_(strategy.dc_skip_contradicts), np.float64(strategy.dc_leverage_boost), np.float64(strategy.dc_leverage_reduce), ) elapsed = time.time() - t0 (capital, n_trades, n_wins, win_rate, profit_factor, n_stop, n_trail, n_hold, total_fees, total_slippage, n_long, n_short, pnl_long, pnl_short, n_target, n_tp, n_maker_entries, n_taker_entries, n_aborted_entries, n_dc_confirmed, n_dc_contradicted, n_dc_neutral) = result roi_pct = (capital - init_cash) / init_cash * 100.0 metrics = { 'strategy': strategy.name, 'capital': capital, 'roi_pct': roi_pct, 'trades': n_trades, 'wins': n_wins, 'win_rate': win_rate * 100.0, 'profit_factor': profit_factor, 'stop_exits': n_stop, 'trailing_exits': n_trail, 'hold_exits': n_hold, 'target_exits': n_target, 'tp_exits': n_tp, 'long_trades': n_long, 'short_trades': n_short, 'long_pnl': pnl_long, 'short_pnl': pnl_short, 'total_fees': total_fees, 'total_slippage_cost': total_slippage, 'maker_entries': n_maker_entries, 'taker_entries': n_taker_entries, 'aborted_entries': n_aborted_entries, 'dc_confirmed': n_dc_confirmed, 'dc_contradicted': n_dc_contradicted, 'dc_neutral': n_dc_neutral, 'elapsed_sec': elapsed, } if verbose: target_str = f" Tgt:{n_target}" if n_target > 0 else "" tp_str = f" TP:{n_tp}" if n_tp > 0 else "" print(f" Trades: {n_trades} (W:{n_wins} S:{n_stop} T:{n_trail} H:{n_hold}{target_str}{tp_str})") print(f" WR: {win_rate*100:.2f}% PF: {profit_factor:.4f}") print(f" Capital: ${capital:.2f} ROI: {roi_pct:.2f}%") print(f" Long: {n_long} (${pnl_long:.2f}) Short: {n_short} (${pnl_short:.2f})") print(f" Fees: ${total_fees:.2f} Slippage: ${total_slippage:.2f}") if strategy.use_passive_entry: total_attempts = n_maker_entries + n_taker_entries + n_aborted_entries maker_pct = n_maker_entries / total_attempts * 100 if total_attempts > 0 else 0 abort_pct = n_aborted_entries / total_attempts * 100 if total_attempts > 0 else 0 print(f" Passive: maker={n_maker_entries}({maker_pct:.0f}%) taker={n_taker_entries} aborted={n_aborted_entries}({abort_pct:.0f}%)") if strategy.use_direction_confirm: dc_total = n_dc_confirmed + n_dc_contradicted + n_dc_neutral dc_conf_pct = n_dc_confirmed / dc_total * 100 if dc_total > 0 else 0 dc_contr_pct = n_dc_contradicted / dc_total * 100 if dc_total > 0 else 0 print(f" DirConfirm: confirmed={n_dc_confirmed}({dc_conf_pct:.0f}%) " f"contradicted={n_dc_contradicted}({dc_contr_pct:.0f}%) neutral={n_dc_neutral}") print(f" Time: {elapsed:.2f}s") return metrics # ── V7 Strategy Configurations ──────────────────────────────────────────────── # Exact replicas of itest_v7_results.json configs V7_STRATEGIES = { 'no_trail_control': Strategy( name='no_trail_control', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, stop_pct=0.002, max_hold=120, use_trailing=False, trail_activation=0.0003, trail_distance=0.0003, vol_filter='high', use_rcdd=True, rcdd_multiplier=1.5, rcdd_min_stop=0.001, rcdd_trail=False, trail_dist_floor=0.0003, trail_act_floor=0.0003, use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ), 'tight_3_3': Strategy( name='tight_3_3', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0003, trail_distance=0.0003, vol_filter='high', use_rcdd=True, rcdd_multiplier=1.5, rcdd_min_stop=0.001, rcdd_trail=False, trail_dist_floor=0.0003, trail_act_floor=0.0003, use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ), 'tight_3_3_no_rcdd': Strategy( name='tight_3_3_no_rcdd', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0003, trail_distance=0.0003, vol_filter='high', use_rcdd=False, rcdd_trail=False, trail_dist_floor=0.0003, trail_act_floor=0.0003, use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ), 'tight_2_2': Strategy( name='tight_2_2', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0002, trail_distance=0.0002, vol_filter='high', use_rcdd=True, rcdd_multiplier=1.5, rcdd_min_stop=0.001, rcdd_trail=False, trail_dist_floor=0.0003, trail_act_floor=0.0003, use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ), 'tight_4_4': Strategy( name='tight_4_4', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0004, trail_distance=0.0004, vol_filter='high', use_rcdd=True, rcdd_multiplier=1.5, rcdd_min_stop=0.001, rcdd_trail=False, trail_dist_floor=0.0003, trail_act_floor=0.0003, use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ), 'tight_3_3_allvol': Strategy( name='tight_3_3_allvol', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0003, trail_distance=0.0003, vol_filter='all', use_rcdd=True, rcdd_multiplier=1.5, rcdd_min_stop=0.001, rcdd_trail=False, trail_dist_floor=0.0003, trail_act_floor=0.0003, use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ), 'tight_3_3_h50': Strategy( name='tight_3_3_h50', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, stop_pct=0.002, max_hold=50, use_trailing=True, trail_activation=0.0003, trail_distance=0.0003, vol_filter='high', use_rcdd=True, rcdd_multiplier=1.5, rcdd_min_stop=0.001, rcdd_trail=False, trail_dist_floor=0.0003, trail_act_floor=0.0003, use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ), 'tight_3_3_rcdd_stop': Strategy( name='tight_3_3_rcdd_stop', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0003, trail_distance=0.0003, vol_filter='high', use_rcdd=True, rcdd_multiplier=1.5, rcdd_min_stop=0.003, # 0.3% min rcdd_trail=False, trail_dist_floor=0.0003, trail_act_floor=0.0003, use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ), 'tight_3_3_rtb05': Strategy( name='tight_3_3_rtb05', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0003, trail_distance=0.0003, vol_filter='high', use_rcdd=True, rcdd_multiplier=1.5, rcdd_min_stop=0.005, # 0.5% min rcdd_trail=False, trail_dist_floor=0.0003, trail_act_floor=0.0003, use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ), 'tight_3_5': Strategy( name='tight_3_5', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0003, trail_distance=0.0005, vol_filter='high', use_rcdd=True, rcdd_multiplier=1.5, rcdd_min_stop=0.001, rcdd_trail=False, trail_dist_floor=0.0003, trail_act_floor=0.0003, use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ), 'tight_5_3': Strategy( name='tight_5_3', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0005, trail_distance=0.0003, vol_filter='high', use_rcdd=True, rcdd_multiplier=1.5, rcdd_min_stop=0.001, rcdd_trail=False, trail_dist_floor=0.0003, trail_act_floor=0.0003, use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ), 'OLD_wrong_5_15': Strategy( name='OLD_wrong_5_15', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0005, trail_distance=0.0015, vol_filter='high', use_rcdd=True, rcdd_multiplier=1.5, rcdd_min_stop=0.001, rcdd_trail=True, rcdd_trail_mult=1.0, rcdd_activation_mult=0.5, trail_dist_floor=0.0005, trail_act_floor=0.0005, use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ), } # ── Alpha Engine Strategies (v5 benchmarks) ────────────────────────────────── ALPHA_STRATEGIES = { 'v2_alpha': Strategy( name='v2_alpha', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, stop_pct=0.002, max_hold=50, use_trailing=False, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='all', use_asset_selection=False, use_sp_fees=True, use_sp_slippage=True, ), 'rcdd_alpha_600': Strategy( name='rcdd_alpha_600', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0005, trail_distance=0.0015, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, use_rcdd=True, rcdd_multiplier=1.5, rcdd_trail=True, rcdd_trail_mult=1.0, vol_filter='all', use_asset_selection=False, use_sp_fees=True, use_sp_slippage=True, ), 'asset_alpha_600': Strategy( name='asset_alpha_600', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0005, trail_distance=0.0015, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, use_rcdd=True, rcdd_multiplier=1.5, rcdd_trail=True, rcdd_trail_mult=1.0, vol_filter='all', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ), } # ── New VBT-Native Strategies (exploit speed for exploration) ──────────────── NEW_STRATEGIES = { # Combine v7's profitable 3bps trailing with alpha layers 'alpha_tight_3_3': Strategy( name='alpha_tight_3_3', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0003, trail_distance=0.0003, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, use_rcdd=True, rcdd_multiplier=1.5, rcdd_min_stop=0.001, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ), # Conservative leverage bounds (2-4x instead of 1-5x) 'alpha_tight_3_3_conservative': Strategy( name='alpha_tight_3_3_conservative', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0003, trail_distance=0.0003, dynamic_leverage=True, max_leverage=4.0, min_leverage=2.0, use_alpha_layers=True, use_rcdd=True, rcdd_multiplier=1.5, rcdd_min_stop=0.001, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ), # Alpha + RCDD target (early exit on favorable move) 'alpha_tight_3_3_target': Strategy( name='alpha_tight_3_3_target', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0003, trail_distance=0.0003, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, use_rcdd=True, rcdd_multiplier=1.5, rcdd_min_stop=0.001, use_rcdd_target=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ), # All vol filter (more trades + alpha layers to manage risk) 'alpha_tight_3_3_allvol': Strategy( name='alpha_tight_3_3_allvol', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0003, trail_distance=0.0003, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, use_rcdd=True, rcdd_multiplier=1.5, rcdd_min_stop=0.001, vol_filter='all', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ), # Tighter threshold (stronger signals only) + alpha leverage 'alpha_tight_3_3_strong': Strategy( name='alpha_tight_3_3_strong', vel_div_threshold=-0.03, direction='SHORT', leverage=2.5, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0003, trail_distance=0.0003, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, use_rcdd=True, rcdd_multiplier=1.5, rcdd_min_stop=0.001, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ), # Alpha layers only (no dynamic leverage - test isolation) 'alpha_only_tight_3_3': Strategy( name='alpha_only_tight_3_3', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0003, trail_distance=0.0003, dynamic_leverage=False, use_alpha_layers=True, use_rcdd=True, rcdd_multiplier=1.5, rcdd_min_stop=0.001, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ), # Dynamic leverage only (no alpha layers - test isolation) 'dynlev_only_tight_3_3': Strategy( name='dynlev_only_tight_3_3', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0003, trail_distance=0.0003, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=False, use_rcdd=True, rcdd_multiplier=1.5, rcdd_min_stop=0.001, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ), } # ── Putative v8 Replication Strategies ─────────────────────────────────────── # From PUTATIVE_v8_Results_FINDING_SUMMARY__AGENTS_START_HERE.md # v8 was run on synthetic data; these replicate its configs on real eigenvalue scans. V8_STRATEGIES = { # v6 baseline: 5bps/15bps trailing (claimed PF 0.98, unprofitable) 'v8_v6_baseline': Strategy( name='v8_v6_baseline', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0005, trail_distance=0.0015, vol_filter='high', use_asset_selection=False, use_sp_fees=True, use_sp_slippage=True, use_maker_filter=True, ), # v8 breakthrough: 3bps/3bps trailing, no asset selection (claimed PF 1.09) 'v8_base_3_3': Strategy( name='v8_base_3_3', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0003, trail_distance=0.0003, vol_filter='high', use_asset_selection=False, use_sp_fees=True, use_sp_slippage=True, use_maker_filter=True, ), # v8 base + asset selection (test if IRP helps the v8 config) 'v8_base_3_3_irp': Strategy( name='v8_base_3_3_irp', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0003, trail_distance=0.0003, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ), # v8 base + RCDD (v8 used simple RCDD, add ours) 'v8_base_3_3_rcdd': Strategy( name='v8_base_3_3_rcdd', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0003, trail_distance=0.0003, use_rcdd=True, rcdd_multiplier=1.5, rcdd_min_stop=0.001, vol_filter='high', use_asset_selection=False, use_sp_fees=True, use_sp_slippage=True, ), # v8 "convex approximation" via alpha layers (strength-based sizing ≈ quintile sizing) # Alpha layers skip/reduce weak signals, boost strong ones - same idea as Q1=skip, Q5=50% 'v8_convex_alpha': Strategy( name='v8_convex_alpha', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0003, trail_distance=0.0003, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=False, use_sp_fees=True, use_sp_slippage=True, ), # v8 convex alpha + IRP + RCDD + OB (full stack on v8 base) 'v8_full_stack': Strategy( name='v8_full_stack', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0003, trail_distance=0.0003, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, use_rcdd=True, rcdd_multiplier=1.5, rcdd_min_stop=0.001, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ), # v8 full stack + RCDD target exit 'v8_full_stack_target': Strategy( name='v8_full_stack_target', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0003, trail_distance=0.0003, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, use_rcdd=True, rcdd_multiplier=1.5, rcdd_min_stop=0.001, use_rcdd_target=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ), # v8 all-vol (v8 tested high vol only; test if all-vol + alpha can manage risk) 'v8_allvol_alpha': Strategy( name='v8_allvol_alpha', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0003, trail_distance=0.0003, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='all', use_asset_selection=False, use_sp_fees=True, use_sp_slippage=True, ), # ── Proven Edge Replications ───────────────────────────────────────────── # From alpha_engine_10k_liquidation_results.json (PF 1.098-1.379) # Key: stop-only exit, no trailing, 0.02% per side fee (maker-only), # no asset selection, SHORT-only, all vol, 120 bar hold # Exact match: Fixed 2.5x (PF 1.098, WR 50.6%, 1600 trades, +5.2% ROI) # No SP fees/slippage = lower friction. No trailing = stop+hold exits only. 'proven_2_5x': Strategy( name='proven_2_5x', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=0.002, max_hold=120, use_trailing=False, vol_filter='all', use_asset_selection=False, use_sp_fees=False, use_sp_slippage=False, ), # Fixed 5x (PF 1.201, WR 52.3%, +13.8% ROI) 'proven_5x': Strategy( name='proven_5x', vel_div_threshold=-0.02, direction='SHORT', leverage=5.0, fraction=0.10, stop_pct=0.002, max_hold=120, use_trailing=False, vol_filter='all', use_asset_selection=False, use_sp_fees=False, use_sp_slippage=False, ), # Fixed 10x (PF 1.256, WR 53.6%, +17.5% ROI) 'proven_10x': Strategy( name='proven_10x', vel_div_threshold=-0.02, direction='SHORT', leverage=10.0, fraction=0.05, stop_pct=0.002, max_hold=120, use_trailing=False, vol_filter='all', use_asset_selection=False, use_sp_fees=False, use_sp_slippage=False, ), # Alpha Dynamic 25x (PF 1.337, WR 52.6%, +88.8% ROI, avg_lev 14.9x) 'proven_alpha_25x': Strategy( name='proven_alpha_25x', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.20, stop_pct=0.002, max_hold=120, use_trailing=False, dynamic_leverage=True, max_leverage=25.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='all', use_asset_selection=False, use_sp_fees=False, use_sp_slippage=False, ), # Proven + trailing 3/3 (test if trailing helps on proven base) 'proven_trail_3_3': Strategy( name='proven_trail_3_3', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0003, trail_distance=0.0003, vol_filter='all', use_asset_selection=False, use_sp_fees=False, use_sp_slippage=False, ), # Proven + IRP (test if asset selection helps) 'proven_2_5x_irp': Strategy( name='proven_2_5x_irp', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=0.002, max_hold=120, use_trailing=False, vol_filter='all', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=False, use_sp_slippage=False, ), # Proven + alpha + trailing (full stack on proven base, low fees) 'proven_alpha_trail': Strategy( name='proven_alpha_trail', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0003, trail_distance=0.0003, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='all', use_asset_selection=False, use_sp_fees=False, use_sp_slippage=False, ), } # ── Grid Search: Systematic Profitability Optimization ──────────────────────── def generate_grid_strategies() -> Dict[str, Strategy]: """ Generate comprehensive grid of strategy configs for profitability sweep. Theory: ExitMatrix accidentally proved that 'take small wins quickly' is profitable with this signal. We systematically explore: - Fixed TP levels (the ExitMatrix "take-profit" replication) - Trailing TP combos (the v7 approach) - Fixed TP + Trailing combos (synergy test) - Stop levels (none / wide / current / tight) - Hold times (short to long) - Signal thresholds (standard to aggressive) - Fee regimes (SP blended vs flat maker) - Filter combinations - Alpha layer overlays """ strats = {} # ── PHASE 1: Fixed TP sweep (the ExitMatrix replication) ──────────── # The ExitMatrix "stop_0.20%" for SHORT was accidentally a TP at 20bps. # Replicate this in VBT: no stop (wide), fixed TP, max_hold exit for losers. for tp_bps in [5, 8, 10, 12, 15, 20, 25, 30]: tp_pct = tp_bps * 1e-4 # bps -> decimal for stop in [1.0, 0.005, 0.002]: stop_label = 'nostop' if stop >= 0.5 else f's{int(stop*10000)}' for mh in [50, 120]: name = f'ftp{tp_bps}_{stop_label}_h{mh}' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=stop, max_hold=mh, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) # ── PHASE 2: Trailing TP sweep (wider than v7's 3/3 only) ────────── trail_combos = [ (2, 2), (3, 2), (3, 3), (3, 5), (5, 3), (5, 5), (8, 5), (10, 5), (10, 10), (15, 10), (20, 10), (20, 15), ] for act_bps, dist_bps in trail_combos: act = act_bps * 1e-4 dist = dist_bps * 1e-4 for stop in [1.0, 0.002]: stop_label = 'nostop' if stop >= 0.5 else 's20' name = f'trail_{act_bps}_{dist_bps}_{stop_label}' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=stop, max_hold=120, use_trailing=True, trail_activation=act, trail_distance=dist, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) # ── PHASE 3: Combined Fixed TP + Trailing (synergy test) ──────────── # Fixed TP captures clean moves, trailing captures smaller pullbacks for tp_bps in [10, 15, 20, 25]: tp_pct = tp_bps * 1e-4 for act_bps, dist_bps in [(3, 3), (5, 3), (5, 5), (10, 5)]: act = act_bps * 1e-4 dist = dist_bps * 1e-4 for stop in [1.0, 0.002]: stop_label = 'nostop' if stop >= 0.5 else 's20' name = f'combo_tp{tp_bps}_t{act_bps}{dist_bps}_{stop_label}' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=stop, max_hold=120, use_trailing=True, trail_activation=act, trail_distance=dist, use_fixed_tp=True, fixed_tp_pct=tp_pct, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) # ── PHASE 4: Signal strength / filter variations ──────────────────── # Test best exit configs across thresholds, vol filters, and assets for thresh in [-0.02, -0.03, -0.04]: thresh_label = f't{int(abs(thresh)*100)}' for vol in ['all', 'high']: for tp_bps in [15, 20]: tp_pct = tp_bps * 1e-4 name = f'ftp{tp_bps}_{thresh_label}_{vol}_nostop' strats[name] = Strategy( name=name, vel_div_threshold=thresh, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, vol_filter=vol, use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) # Also trailing combos at different thresholds for act_bps, dist_bps in [(5, 3), (10, 5)]: act = act_bps * 1e-4 dist = dist_bps * 1e-4 name = f'trail_{act_bps}_{dist_bps}_{thresh_label}_{vol}' strats[name] = Strategy( name=name, vel_div_threshold=thresh, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=True, trail_activation=act, trail_distance=dist, vol_filter=vol, use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) # ── PHASE 5: No asset selection (BTC only) + no IRP gate ─────────── for tp_bps in [10, 15, 20]: tp_pct = tp_bps * 1e-4 name = f'ftp{tp_bps}_btconly_nostop' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, vol_filter='high', use_asset_selection=False, use_sp_fees=True, use_sp_slippage=True, ) # ── PHASE 6: Fee regime test ──────────────────────────────────────── # Flat maker fees (0.02%/side = 4bps RT) vs SP blended (~6.8bps RT) for tp_bps in [10, 15, 20]: tp_pct = tp_bps * 1e-4 name = f'ftp{tp_bps}_flatmaker_nostop' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=False, use_sp_slippage=False, ) # ── PHASE 7: Hold time sweep (for best TP levels) ────────────────── for mh in [25, 35, 50, 75, 100, 150, 200]: for tp_bps in [15, 20]: tp_pct = tp_bps * 1e-4 name = f'ftp{tp_bps}_nostop_h{mh}' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=mh, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) # ── PHASE 8: Leverage sweep with best TP ──────────────────────────── for lev in [1.5, 2.0, 3.0, 4.0, 5.0]: for tp_bps in [15, 20]: tp_pct = tp_bps * 1e-4 frac = min(0.15, 0.375 / lev) # Keep notional ~constant name = f'ftp{tp_bps}_nostop_lev{int(lev*10)}' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=lev, fraction=frac, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) # ── PHASE 9: Alpha layers on best fixed TP configs ────────────────── for tp_bps in [15, 20, 25]: tp_pct = tp_bps * 1e-4 # Alpha layers only name = f'ftp{tp_bps}_alpha_nostop' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) # Dynamic leverage + alpha name = f'ftp{tp_bps}_dynalpha_nostop' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) # Combo: Fixed TP + trailing + alpha name = f'combo_tp{tp_bps}_t53_alpha_nostop' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=True, trail_activation=0.0005, trail_distance=0.0003, use_fixed_tp=True, fixed_tp_pct=tp_pct, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) # ── PHASE 10: Fraction sweep (risk per trade) ─────────────────────── for frac in [0.05, 0.08, 0.10, 0.20, 0.25]: for tp_bps in [15, 20]: tp_pct = tp_bps * 1e-4 name = f'ftp{tp_bps}_f{int(frac*100)}_nostop' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=frac, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) return strats def generate_grid2_strategies() -> Dict[str, Strategy]: """ Round 2: Focused grid targeting the discovered sweet spot. Round 1 findings: - Best: ftp25_dynalpha_nostop PF=0.870 (13% from breakeven) - Fixed TP 20-30bps > trailing > small TP - No stop > any stop (stops are pure bleed) - Alpha layers + dynamic leverage add ~14% PF - Direction enforcement (SHORT only) added ~32% PF Round 2 explores: - Larger TPs (30-75bps) — push the TP capture higher - Longer max_hold (200-600 bars) — more time for TP to trigger - Higher alpha leverage bounds (10x, 15x, 25x) - Tighter IRP alignment (0.50, 0.60, 0.70) — better asset quality - RCDD adaptive exits with TP - Vol filter combinations with alpha - Very aggressive configs for extreme signals only (-0.05, -0.06) """ strats = {} # ── R2-A: Larger TP sweep with alpha+dynlev (the winning combo) ───── for tp_bps in [25, 30, 35, 40, 50, 60, 75]: tp_pct = tp_bps * 1e-4 for max_lev in [5.0, 10.0, 15.0, 25.0]: ml_label = f'ml{int(max_lev)}' name = f'r2_ftp{tp_bps}_{ml_label}_da' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, dynamic_leverage=True, max_leverage=max_lev, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) # ── R2-B: Longer hold times (more time for TP to trigger) ─────────── for tp_bps in [25, 30, 40, 50]: tp_pct = tp_bps * 1e-4 for mh in [200, 300, 400, 600]: name = f'r2_ftp{tp_bps}_h{mh}_da' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=mh, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) # ── R2-C: Tighter IRP alignment (better asset quality) ───────────── for tp_bps in [25, 30, 40]: tp_pct = tp_bps * 1e-4 for align in [0.50, 0.55, 0.60, 0.65, 0.70]: name = f'r2_ftp{tp_bps}_irp{int(align*100)}_da' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=align, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) # ── R2-D: Extreme signals only (-0.05, -0.06) ────────────────────── for thresh in [-0.05, -0.06, -0.07]: thresh_label = f't{int(abs(thresh)*100)}' for tp_bps in [20, 25, 30, 40, 50]: tp_pct = tp_bps * 1e-4 for vol in ['all', 'high']: name = f'r2_ftp{tp_bps}_{thresh_label}_{vol}_da' strats[name] = Strategy( name=name, vel_div_threshold=thresh, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter=vol, use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) # ── R2-E: RCDD adaptive + fixed TP (dynamic stop based on history) ─ for tp_bps in [25, 30, 40]: tp_pct = tp_bps * 1e-4 for rcdd_mult in [1.5, 2.0, 3.0]: name = f'r2_ftp{tp_bps}_rcdd{int(rcdd_mult*10)}_da' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, use_rcdd=True, rcdd_multiplier=rcdd_mult, rcdd_min_stop=0.001, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) # ── R2-F: RCDD target exit + fixed TP (adaptive early exit) ───────── for tp_bps in [25, 30, 40]: tp_pct = tp_bps * 1e-4 name = f'r2_ftp{tp_bps}_rcddt_da' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, use_rcdd=True, rcdd_multiplier=1.5, rcdd_min_stop=0.001, use_rcdd_target=True, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) # ── R2-G: All vol + alpha (more trades, alpha manages risk) ───────── for tp_bps in [25, 30, 40]: tp_pct = tp_bps * 1e-4 for mh in [120, 200]: name = f'r2_ftp{tp_bps}_allvol_h{mh}_da' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=mh, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='all', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) # ── R2-H: Small fraction (capital preservation) + high leverage ───── for tp_bps in [25, 30, 40]: tp_pct = tp_bps * 1e-4 for frac, lev in [(0.03, 10.0), (0.05, 7.5), (0.02, 15.0)]: fl = f'f{int(frac*100)}l{int(lev*10)}' name = f'r2_ftp{tp_bps}_{fl}_da' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=lev, fraction=frac, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, dynamic_leverage=True, max_leverage=lev * 2.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) # ── R2-I: Best config combos (TP + hold + threshold + alpha) ──────── # The "kitchen sink" configs targeting maximum PF for tp_bps in [25, 30, 35, 40]: tp_pct = tp_bps * 1e-4 for mh in [120, 200, 300]: for align in [0.45, 0.55, 0.65]: name = f'r2_best_tp{tp_bps}_h{mh}_a{int(align*100)}' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=mh, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=align, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) return strats def generate_grid3_strategies() -> Dict[str, Strategy]: """ Round 3: Diagnostic grid testing inverse plays + fee tiers. Key hypotheses: 1. IRP inverse plays (LONG on inversely-correlated assets) are valid when the IRP correctly identifies them — test direction='BOTH' 2. Fee drag is the primary gap to profitability — test zero/low fees 3. Hyperliquid fees (0.015% maker / 0.045% taker) may close the gap """ strats = {} # Hyperliquid fee constants HL_MAKER = 0.00015 # 0.015% HL_TAKER = 0.00045 # 0.045% HL_BLENDED = (HL_MAKER * 0.62 + HL_TAKER * 0.38 + # entry HL_MAKER * 0.50 + HL_TAKER * 0.50) / 2 # exit avg # ── DIAGNOSTIC: Zero fees (raw signal edge) ──────────────────────── for tp_bps in [25, 40, 60]: tp_pct = tp_bps * 1e-4 # SHORT only, zero fees name = f'diag_ftp{tp_bps}_zerofee_short' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=False, use_sp_slippage=False, fee_rate_override=0.0, # ZERO FEES ) # BOTH directions, zero fees (test inverse plays) name = f'diag_ftp{tp_bps}_zerofee_both' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='BOTH', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=False, use_sp_slippage=False, fee_rate_override=0.0, # ZERO FEES ) # ── INVERSE PLAY TEST: BOTH directions with current fees ─────────── for tp_bps in [25, 30, 40, 50, 60]: tp_pct = tp_bps * 1e-4 for align in [0.45, 0.55, 0.65]: # BOTH directions (IRP picks direction per asset) name = f'inv_ftp{tp_bps}_both_a{int(align*100)}_da' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='BOTH', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=align, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) # ── INVERSE + LONGER HOLD (more time for inverse plays to develop) ─ for tp_bps in [25, 40, 60]: tp_pct = tp_bps * 1e-4 for mh in [200, 300]: name = f'inv_ftp{tp_bps}_both_h{mh}_da' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='BOTH', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=mh, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) # ── HYPERLIQUID FEES: Test with lower fee structure ──────────────── for tp_bps in [25, 30, 40, 50, 60]: tp_pct = tp_bps * 1e-4 # Hyperliquid blended (SmartPlacer-equivalent) name = f'hl_ftp{tp_bps}_short_da' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=False, use_sp_slippage=True, fee_rate_override=HL_BLENDED, ) # Hyperliquid BOTH directions name = f'hl_ftp{tp_bps}_both_da' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='BOTH', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=False, use_sp_slippage=True, fee_rate_override=HL_BLENDED, ) # Hyperliquid pure maker rebate (-0.001%) name = f'hlr_ftp{tp_bps}_short_da' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=False, use_sp_slippage=True, fee_rate_override=-0.00001, # MAKER REBATE: -0.001% per side ) # ── BEST COMBOS: Hyperliquid + inverse + optimal params ──────────── for tp_bps in [25, 40, 60]: tp_pct = tp_bps * 1e-4 for mh in [120, 200, 300]: # HL fees + both directions + alpha name = f'hlbest_ftp{tp_bps}_h{mh}_both' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='BOTH', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=mh, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=False, use_sp_slippage=True, fee_rate_override=HL_BLENDED, ) # ── ALL VOL + BOTH DIRS (maximum trade count) ────────────────────── for tp_bps in [25, 40, 60]: tp_pct = tp_bps * 1e-4 name = f'hlmax_ftp{tp_bps}_allvol_both' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='BOTH', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='all', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=False, use_sp_slippage=True, fee_rate_override=HL_BLENDED, ) return strats # ═══════════════════════════════════════════════════════════════════════════════ # SECTION 7: PARAMETER SWEEP # ═══════════════════════════════════════════════════════════════════════════════ DEFAULT_SWEEP_GRID = { 'vel_div_threshold': [-0.02, -0.03, -0.04, -0.05], 'trail_activation': [0.0002, 0.0003, 0.0004, 0.0005], 'trail_distance': [0.0002, 0.0003, 0.0004, 0.0005], 'max_hold': [50, 80, 120], 'stop_pct': [0.001, 0.002, 0.003, 0.005], } # Total: 4 * 4 * 4 * 3 * 4 = 768 combinations def generate_grid4_strategies() -> Dict[str, Strategy]: """ Grid 4: Passive entry (SmartPlacer "let price move to us") simulation. Tests bar-by-bar maker fill with adverse move filtering. """ strats = {} # Base config (best from grid2: 60bps TP, dynamic alpha) base = dict( vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=0.002, max_hold=120, use_trailing=False, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, use_asset_selection=True, min_irp_alignment=0.45, vol_filter='high', # Passive entry replaces SP fees/slippage/OB edge use_sp_fees=False, use_sp_slippage=False, use_ob_edge=False, use_passive_entry=True, ) # ── Phase A: Offset sweep (how far inside spread to place limit) ───── for offset in [0.5, 1.0, 1.5, 2.0, 3.0]: for tp in [0.0025, 0.004, 0.006]: tp_label = f"{int(tp*10000)}" strats[f'pe_off{offset}_tp{tp_label}'] = Strategy( name=f'pe_off{offset}_tp{tp_label}', **base, use_fixed_tp=True, fixed_tp_pct=tp, passive_offset_bps=offset, passive_timeout_bars=5, passive_abort_bps=5.0, ) # ── Phase B: Timeout sweep (how long to wait for fill) ─────────────── for timeout in [3, 5, 8, 10, 15]: strats[f'pe_t{timeout}_tp40'] = Strategy( name=f'pe_t{timeout}_tp40', **base, use_fixed_tp=True, fixed_tp_pct=0.004, passive_offset_bps=1.0, passive_timeout_bars=timeout, passive_abort_bps=5.0, ) # ── Phase C: Abort threshold sweep (filter sensitivity) ────────────── for abort in [2.0, 3.0, 5.0, 8.0, 10.0, 15.0]: strats[f'pe_ab{int(abort)}_tp40'] = Strategy( name=f'pe_ab{int(abort)}_tp40', **base, use_fixed_tp=True, fixed_tp_pct=0.004, passive_offset_bps=1.0, passive_timeout_bars=5, passive_abort_bps=abort, ) # ── Phase D: Fallback mode (taker vs abort on timeout) ─────────────── for fallback in [True, False]: label = 'fb' if fallback else 'noFb' strats[f'pe_{label}_tp40'] = Strategy( name=f'pe_{label}_tp40', **base, use_fixed_tp=True, fixed_tp_pct=0.004, passive_offset_bps=1.0, passive_timeout_bars=5, passive_abort_bps=5.0, passive_fallback_taker=fallback, ) # ── Phase E: Fill discount sweep (queue position modeling) ─────────── for disc in [0.60, 0.70, 0.80, 0.90, 1.00]: strats[f'pe_fd{int(disc*100)}_tp40'] = Strategy( name=f'pe_fd{int(disc*100)}_tp40', **base, use_fixed_tp=True, fixed_tp_pct=0.004, passive_offset_bps=1.0, passive_timeout_bars=5, passive_abort_bps=5.0, passive_fill_discount=disc, ) # ── Phase F: No TP (trailing only + passive entry) ─────────────────── for offset in [0.5, 1.0, 2.0]: for abort in [3.0, 5.0, 10.0]: strats[f'pe_trail_off{offset}_ab{int(abort)}'] = Strategy( name=f'pe_trail_off{offset}_ab{int(abort)}', **{**base, 'use_trailing': True, 'trail_activation': 0.0003, 'trail_distance': 0.0003}, use_fixed_tp=False, passive_offset_bps=offset, passive_timeout_bars=5, passive_abort_bps=abort, ) # ── Phase G: Hyperliquid fees + passive entry ──────────────────────── for offset in [0.5, 1.0, 2.0]: for tp in [0.004, 0.006]: tp_label = f"{int(tp*10000)}" strats[f'pe_hl_off{offset}_tp{tp_label}'] = Strategy( name=f'pe_hl_off{offset}_tp{tp_label}', **base, use_fixed_tp=True, fixed_tp_pct=tp, passive_offset_bps=offset, passive_timeout_bars=5, passive_abort_bps=5.0, maker_fee_rate=0.00015, # Hyperliquid: 0.015% taker_fee_rate=0.00045, # Hyperliquid: 0.045% ) # ── Phase H: Best combos — long hold + passive + no stop ───────────── for hold in [120, 200, 300]: for tp in [0.004, 0.006]: tp_label = f"{int(tp*10000)}" strats[f'pe_best_h{hold}_tp{tp_label}'] = Strategy( name=f'pe_best_h{hold}_tp{tp_label}', **{**base, 'max_hold': hold, 'stop_pct': 0.01}, use_fixed_tp=True, fixed_tp_pct=tp, passive_offset_bps=1.0, passive_timeout_bars=5, passive_abort_bps=5.0, ) # ── Phase I: Diagnostic — passive vs non-passive control ───────────── # Control: same config WITHOUT passive entry (SP fees instead) strats['control_sp_tp40'] = Strategy( name='control_sp_tp40', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=0.002, max_hold=120, use_trailing=False, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, use_asset_selection=True, min_irp_alignment=0.45, vol_filter='high', use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_fixed_tp=True, fixed_tp_pct=0.004, use_passive_entry=False, ) strats['control_sp_tp60'] = Strategy( name='control_sp_tp60', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=0.002, max_hold=120, use_trailing=False, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, use_asset_selection=True, min_irp_alignment=0.45, vol_filter='high', use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_fixed_tp=True, fixed_tp_pct=0.006, use_passive_entry=False, ) # ── Phase J: All-vol with passive entry ────────────────────────────── for tp in [0.004, 0.006]: tp_label = f"{int(tp*10000)}" strats[f'pe_allvol_tp{tp_label}'] = Strategy( name=f'pe_allvol_tp{tp_label}', **{**base, 'vol_filter': 'all'}, use_fixed_tp=True, fixed_tp_pct=tp, passive_offset_bps=1.0, passive_timeout_bars=5, passive_abort_bps=5.0, ) return strats def generate_grid5_strategies() -> Dict[str, Strategy]: """ Grid 5: HIGH LEVERAGE + DIRECTION CONFIRMATION + ASSET QUALITY Three innovations from original system research: 1. Leverage ceiling 25x (production) / 50x (diagnostic) vs current 5x 2. Direction confirmation filter (OB imbalance proxy via price momentum) 3. Signal-strength-based leverage concentrates capital on best signals Original Alpha Dynamic 25x achieved PF=1.32, +83.76% returns. """ strats: Dict[str, Strategy] = {} # ══════════════════════════════════════════════════════════════════════════ # PHASE A: HIGH LEVERAGE SWEEP (biggest potential gain) # Test max_leverage at 10, 15, 20, 25, 50 with dynamic leverage # Uses best params from Grid 2 (FTP60, SHORT, SP fees, alpha layers) # ══════════════════════════════════════════════════════════════════════════ for max_lev in [10, 15, 20, 25, 50]: name = f'g5_lev{max_lev}_ftp60' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.006, # 60bps dynamic_leverage=True, max_leverage=float(max_lev), min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) # High leverage with smaller TPs (capture quick wins with big size) for max_lev in [25, 50]: for tp_bps in [25, 40]: tp_pct = tp_bps * 1e-4 name = f'g5_lev{max_lev}_ftp{tp_bps}' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, dynamic_leverage=True, max_leverage=float(max_lev), min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) # ══════════════════════════════════════════════════════════════════════════ # PHASE B: DIRECTION CONFIRMATION SWEEP (OB proxy) # Filter trades where price momentum contradicts signal direction # Test lookback × magnitude × skip-vs-reduce # ══════════════════════════════════════════════════════════════════════════ for lb in [3, 5, 10]: for mag in [1, 2, 5]: name = f'g5_dc_lb{lb}_m{mag}_skip' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.006, # 60bps dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, # Direction confirmation use_direction_confirm=True, dc_lookback_bars=lb, dc_min_magnitude_bps=float(mag), dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) # Best DC params with reduce instead of skip for lb in [3, 5]: name = f'g5_dc_lb{lb}_m2_reduce' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.006, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=True, dc_lookback_bars=lb, dc_min_magnitude_bps=2.0, dc_skip_contradicts=False, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) # ══════════════════════════════════════════════════════════════════════════ # PHASE C: HIGH LEVERAGE + DIRECTION CONFIRMATION COMBO # The killer combo: 25x leverage ceiling + only trade when OB confirms # ══════════════════════════════════════════════════════════════════════════ for max_lev in [25, 50]: for lb, mag in [(3, 2), (5, 2), (5, 1)]: name = f'g5_lev{max_lev}_dc{lb}m{mag}_skip' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.006, dynamic_leverage=True, max_leverage=float(max_lev), min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=True, dc_lookback_bars=lb, dc_min_magnitude_bps=float(mag), dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) # High lev + DC + boost only (no skip, just boost confirmed + reduce contradicted) for max_lev in [25, 50]: name = f'g5_lev{max_lev}_dc5m2_boost' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.006, dynamic_leverage=True, max_leverage=float(max_lev), min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=False, dc_leverage_boost=2.0, # Big boost when confirmed dc_leverage_reduce=0.3, # Strong reduction when contradicted ) # ══════════════════════════════════════════════════════════════════════════ # PHASE D: EXTREME SIGNALS ONLY + ULTRA-HIGH LEVERAGE # Only trade vel_div ≤ -0.05 (extreme signals, ~60% WR) # These are the highest-quality signals that deserve maximum leverage # ══════════════════════════════════════════════════════════════════════════ for max_lev in [25, 50]: name = f'g5_extreme_lev{max_lev}_ftp60' strats[name] = Strategy( name=name, vel_div_threshold=-0.05, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.006, dynamic_leverage=True, max_leverage=float(max_lev), min_leverage=5.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) # Extreme + DC for max_lev in [25, 50]: name = f'g5_extreme_lev{max_lev}_dc5m2' strats[name] = Strategy( name=name, vel_div_threshold=-0.05, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.006, dynamic_leverage=True, max_leverage=float(max_lev), min_leverage=5.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) # ══════════════════════════════════════════════════════════════════════════ # PHASE E: ALL-VOL + HIGH LEVERAGE (more trades, leverage concentrates) # Alpha layers + high leverage should make even weak-vol trades manageable # ══════════════════════════════════════════════════════════════════════════ for max_lev in [25, 50]: name = f'g5_allvol_lev{max_lev}_ftp60' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.006, dynamic_leverage=True, max_leverage=float(max_lev), min_leverage=1.0, use_alpha_layers=True, vol_filter='all', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) # ══════════════════════════════════════════════════════════════════════════ # PHASE F: HIGH LEVERAGE + TRAILING (instead of fixed TP) # 3bps trailing with high leverage = capture quick moves with big size # ══════════════════════════════════════════════════════════════════════════ for max_lev in [25, 50]: name = f'g5_lev{max_lev}_trail33' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=0.0003, trail_distance=0.0003, dynamic_leverage=True, max_leverage=float(max_lev), min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) # ══════════════════════════════════════════════════════════════════════════ # PHASE G: HIGH LEVERAGE WITH DIFFERENT MIN LEVERAGE FLOORS # Test if higher min_leverage (never below 5x) concentrates better # ══════════════════════════════════════════════════════════════════════════ for min_lev in [2, 5, 10]: name = f'g5_lev25_min{min_lev}_ftp60' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.006, dynamic_leverage=True, max_leverage=25.0, min_leverage=float(min_lev), use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) # ══════════════════════════════════════════════════════════════════════════ # PHASE H: CONTROLS (no dynamic leverage, no DC - baseline comparison) # ══════════════════════════════════════════════════════════════════════════ # Control: best Grid 2 config (flat 2.5x leverage) strats['g5_control_flat25'] = Strategy( name='g5_control_flat25', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.006, dynamic_leverage=False, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) # Control: dynamic lev 5x (what we've been testing) strats['g5_control_dyn5'] = Strategy( name='g5_control_dyn5', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.006, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) # Zero-fee diagnostic with 25x leverage strats['g5_zerofee_lev25'] = Strategy( name='g5_zerofee_lev25', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.006, dynamic_leverage=True, max_leverage=25.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=False, use_sp_slippage=False, use_ob_edge=False, fee_rate_override=0.0, # ZERO FEES ) # ══════════════════════════════════════════════════════════════════════════ # PHASE I: FRACTION SWEEP WITH HIGH LEVERAGE # Original system used 2% min fraction. Test different base fractions. # Higher lev × lower fraction = same notional but better risk distribution # ══════════════════════════════════════════════════════════════════════════ for frac in [0.05, 0.10, 0.20]: name = f'g5_lev25_frac{int(frac*100)}_ftp60' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=frac, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.006, dynamic_leverage=True, max_leverage=25.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, ) return strats def generate_grid5b_strategies() -> Dict[str, Strategy]: """ Grid 5B: BRIDGE THE FINAL 1.4% GAP Best from Grid 5: g5_dc_lb5_m2_skip at PF=0.986 (1.4% from breakeven) Direction confirmation with lookback=5, magnitude=2bps, skip contradicted. Now refine: DC + lower fees, DC + different TPs, DC only-confirmed trades, DC + stronger boost, DC + trailing, DC + soft stop, DC + Hyperliquid. """ strats: Dict[str, Strategy] = {} # ══════════════════════════════════════════════════════════════════════════ # PHASE A: DC + ONLY CONFIRMED TRADES (skip both contradicted AND neutral) # Even more aggressive filtering - only trade when direction is confirmed # ══════════════════════════════════════════════════════════════════════════ # This requires a code change - for now, simulate by requiring very low magnitude # (everything classified as confirm or contradict, barely any neutral) # Actually, we can use magnitude=0.0 to make EVERYTHING confirm or contradict for lb in [3, 5, 7]: name = f'g5b_dc_lb{lb}_m0_skip' # m0 = any movement counts strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.006, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=True, dc_lookback_bars=lb, dc_min_magnitude_bps=0.1, # Tiny threshold = classify almost everything dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) # ══════════════════════════════════════════════════════════════════════════ # PHASE B: DC + DIFFERENT TP SIZES # Maybe 60bps TP is suboptimal with DC filter? Confirmed trades might # deserve different exit thresholds # ══════════════════════════════════════════════════════════════════════════ for tp_bps in [25, 40, 80, 100, 150]: tp_pct = tp_bps * 1e-4 name = f'g5b_dc5m2_ftp{tp_bps}' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) # ══════════════════════════════════════════════════════════════════════════ # PHASE C: DC + TRAILING (instead of fixed TP) # 3bps trailing with DC might work - confirmed trades run further # ══════════════════════════════════════════════════════════════════════════ for trail_bps in [3, 5, 10]: td = trail_bps * 1e-4 name = f'g5b_dc5m2_trail{trail_bps}' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=0.002, max_hold=120, use_trailing=True, trail_activation=td, trail_distance=td, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) # ══════════════════════════════════════════════════════════════════════════ # PHASE D: DC + STRONGER BOOST (bigger reward for confirmed trades) # ══════════════════════════════════════════════════════════════════════════ for boost in [2.0, 3.0, 4.0]: name = f'g5b_dc5m2_boost{boost:.0f}x' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.006, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=boost, dc_leverage_reduce=0.5, ) # ══════════════════════════════════════════════════════════════════════════ # PHASE E: DC + HYPERLIQUID FEES (lower fee floor) # HL taker = 0.035%, maker = 0.02% # ══════════════════════════════════════════════════════════════════════════ name = 'g5b_dc5m2_hl_fees' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.006, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=False, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, fee_rate_override=0.00035, # Hyperliquid taker 0.035% use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) # HL with maker rate (confirmed trades = maker fills = better fee) name = 'g5b_dc5m2_hl_maker' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.006, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=False, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, fee_rate_override=0.0002, # HL maker 0.02% use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) # ══════════════════════════════════════════════════════════════════════════ # PHASE F: DC + NO ALPHA LAYERS (isolation test - is DC orthogonal?) # ══════════════════════════════════════════════════════════════════════════ name = 'g5b_dc5m2_noalpha' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.006, dynamic_leverage=False, use_alpha_layers=False, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=1.0, # No boost (flat leverage) dc_leverage_reduce=1.0, ) # ══════════════════════════════════════════════════════════════════════════ # PHASE G: DC + LONGER HOLD (300, 600 bars = 25min, 50min) # Confirmed trades might benefit from longer holding # ══════════════════════════════════════════════════════════════════════════ for hold in [300, 600]: name = f'g5b_dc5m2_hold{hold}' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=hold, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.006, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) # ══════════════════════════════════════════════════════════════════════════ # PHASE H: DC + ALL VOL (more trades with quality filter) # ══════════════════════════════════════════════════════════════════════════ name = 'g5b_dc5m2_allvol' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.006, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='all', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) # ══════════════════════════════════════════════════════════════════════════ # PHASE I: DC + ZERO FEES (diagnostic - what's the max PF?) # ══════════════════════════════════════════════════════════════════════════ name = 'g5b_dc5m2_zerofee' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.006, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=False, use_sp_slippage=False, use_ob_edge=False, fee_rate_override=0.0, use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) # ══════════════════════════════════════════════════════════════════════════ # PHASE J: DC + SOFT STOP (1% instead of 0.2%) # DC-confirmed trades are higher quality, use wider stop # ══════════════════════════════════════════════════════════════════════════ for stop in [0.005, 0.01]: stop_bps = int(stop * 10000) name = f'g5b_dc5m2_stop{stop_bps}bps' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=stop, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.006, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) # ══════════════════════════════════════════════════════════════════════════ # PHASE K: BEST COMBO - DC + multiple refinements # Combine DC with the best from each grid round # ══════════════════════════════════════════════════════════════════════════ # DC + no dynamic leverage (flat 2.5x) + no alpha (cleanest test) name = 'g5b_dc5m2_flat25_clean' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.006, dynamic_leverage=False, use_alpha_layers=False, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=1.0, # No boost dc_leverage_reduce=1.0, ) # DC + looser alignment (more trades pass IRP gate) name = 'g5b_dc5m2_align30' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.006, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.30, # Looser gate use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) # DC + wider lookback sweep around optimal (4, 6, 8 bars) for lb in [4, 6, 8]: name = f'g5b_dc_lb{lb}_m2_skip' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.006, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=True, dc_lookback_bars=lb, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) # DC + magnitude fine-tuning around 2bps optimal (1.5, 3, 4) for mag in [1.5, 3.0, 4.0]: name = f'g5b_dc5_m{mag:.1f}_skip' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.006, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=mag, dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) return strats def generate_grid5c_strategies() -> Dict[str, Strategy]: """ Grid 5C: MASSIVE SWEEP around profitable g5b_dc5m2_ftp100 (PF=1.060) Part 1: Controlled "WHY" experiments (feature isolation) Part 2: Massive parameter sweep to maximize PF """ strats: Dict[str, Strategy] = {} # ══════════════════════════════════════════════════════════════════════════ # PART 1: WHY IS IT PROFITABLE? (Controlled experiments) # Toggle each feature on/off to measure individual contribution # ══════════════════════════════════════════════════════════════════════════ # Control: the profitable strategy (replicate) strats['why_FULL'] = Strategy( name='why_FULL', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.010, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) # No DC (isolate DC contribution) strats['why_noDC'] = Strategy( name='why_noDC', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.010, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=False, ) # No FTP100 (60bps TP + DC) strats['why_noFTP100'] = Strategy( name='why_noFTP100', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.006, # 60bps instead of 100bps dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) # No alpha layers (DC + FTP100 only) strats['why_noAlpha'] = Strategy( name='why_noAlpha', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.010, dynamic_leverage=False, use_alpha_layers=False, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=1.0, dc_leverage_reduce=1.0, ) # No asset selection (BTC-only) strats['why_noIRP'] = Strategy( name='why_noIRP', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.010, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=False, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) # No SP fees/slippage (pure taker) strats['why_noSP'] = Strategy( name='why_noSP', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.010, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=False, use_sp_slippage=False, use_ob_edge=False, use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) # No vol filter (all vol) strats['why_allVol'] = Strategy( name='why_allVol', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.010, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='all', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) # NAKED: DC + FTP100 only, everything else off strats['why_NAKED'] = Strategy( name='why_NAKED', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.010, dynamic_leverage=False, use_alpha_layers=False, vol_filter='all', use_asset_selection=False, use_sp_fees=False, use_sp_slippage=False, use_ob_edge=False, use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=1.0, dc_leverage_reduce=1.0, ) # ZERO FEE diagnostic (max theoretical edge) strats['why_ZEROFEE'] = Strategy( name='why_ZEROFEE', vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.010, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=False, use_sp_slippage=False, use_ob_edge=False, fee_rate_override=0.0, use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) # ══════════════════════════════════════════════════════════════════════════ # PART 2: MASSIVE PARAMETER SWEEP # ══════════════════════════════════════════════════════════════════════════ # ── SWEEP 1: TP size fine-tuning (14 values × 1 base config) ────────── for tp_bps in [70, 80, 85, 90, 95, 100, 105, 110, 115, 120, 130, 140, 160, 200]: tp_pct = tp_bps * 1e-4 name = f'sw_tp{tp_bps}' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) # ── SWEEP 2: DC lookback × DC magnitude (4×5=20) at 100bps TP ──────── for lb in [3, 4, 5, 7]: for mag in [0.5, 1.0, 1.5, 2.0, 3.0]: name = f'sw_dc{lb}m{mag:.1f}_tp100' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.010, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=True, dc_lookback_bars=lb, dc_min_magnitude_bps=mag, dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) # ── SWEEP 3: Max hold (7 values) at best DC+TP ─────────────────────── for hold in [60, 80, 100, 150, 200, 300, 600]: name = f'sw_hold{hold}_tp100' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=hold, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.010, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) # ── SWEEP 4: Fraction (5 values) ───────────────────────────────────── for frac in [0.05, 0.08, 0.10, 0.20, 0.25]: name = f'sw_frac{int(frac*100)}_tp100' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=frac, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.010, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) # ── SWEEP 5: IRP alignment threshold (5 values) ────────────────────── for align in [0.20, 0.30, 0.40, 0.50, 0.60]: name = f'sw_align{int(align*100)}_tp100' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.010, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=align, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) # ── SWEEP 6: DC boost strength (5 values) ──────────────────────────── for boost in [1.0, 1.25, 1.5, 2.0, 3.0]: name = f'sw_boost{boost:.2f}_tp100' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.010, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=boost, dc_leverage_reduce=0.5, ) # ── SWEEP 7: Vel_div threshold (signal sensitivity) ────────────────── for vdt in [-0.015, -0.02, -0.025, -0.03, -0.04]: name = f'sw_vdt{abs(vdt):.3f}_tp100' strats[name] = Strategy( name=name, vel_div_threshold=vdt, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.010, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) # ── SWEEP 8: TP × hold cross (top TPs × top holds = 4×4=16) ───────── for tp_bps in [90, 100, 110, 120]: for hold in [100, 120, 150, 200]: if tp_bps == 100 and hold == 120: continue # Already in sweep 1/3 tp_pct = tp_bps * 1e-4 name = f'sw_tp{tp_bps}_h{hold}' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=hold, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=tp_pct, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) # ── SWEEP 9: Fee model sensitivity ──────────────────────────────────── for label, fee_or in [('binSP', -1.0), ('binTaker', 0.0005), ('hlTaker', 0.00035), ('hlMaker', 0.0002), ('hlVIP', 0.000275)]: name = f'sw_fee_{label}_tp100' use_sp = (fee_or < 0) strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.010, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=use_sp, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, fee_rate_override=fee_or if fee_or >= 0 else -1.0, use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) # ── SWEEP 10: Robustness across random seeds ───────────────────────── # (handled at runtime - add 5 seed variants of the best config) # ── SWEEP 11: OB edge bps sensitivity ───────────────────────────────── for ob_bps in [0, 1, 2, 3, 5, 8]: name = f'sw_ob{ob_bps}_tp100' strats[name] = Strategy( name=name, vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, fraction=0.15, stop_pct=1.0, max_hold=120, use_trailing=False, use_fixed_tp=True, fixed_tp_pct=0.010, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=(ob_bps > 0), ob_edge_bps=float(ob_bps), use_direction_confirm=True, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_skip_contradicts=True, dc_leverage_boost=1.5, dc_leverage_reduce=0.5, ) return strats def generate_grid5d_strategies() -> Dict[str, Strategy]: """ Grid 5D: OPTIMAL COMBINATIONS from 5C sweep findings. Key discoveries from 5C: 1. DC boost=1.0 (no boost) → PF=1.078 (+1.8% vs boost=1.5) 2. DC lb=7, mag=1.0 → PF=1.061 (slightly better than 5/2.0) 3. IRP is most important feature (PF 1.060→0.677 without it) 4. OB edge scales linearly (ob5=1.086, ob8=1.135) 5. Fraction 20% → PF=1.058, ROI=+12.5% """ strats: Dict[str, Strategy] = {} base = dict( vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, stop_pct=1.0, # Effectively disabled (100% = never triggers) use_trailing=False, # No trailing - only FTP + max_hold exits vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=3.0, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, use_fixed_tp=True, fixed_tp_pct=0.01, # 100bps use_direction_confirm=True, dc_skip_contradicts=True, dc_leverage_reduce=0.5, ) # ── Phase A: Best individual improvements ───────────────────────────── # A1: No boost (best from 5C: PF=1.078) strats['d_noboost'] = Strategy(name='d_noboost', max_hold=120, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_leverage_boost=1.0, **base) # A2: DC7m1 (best DC params from 5C: PF=1.061) strats['d_dc7m1'] = Strategy(name='d_dc7m1', max_hold=120, dc_lookback_bars=7, dc_min_magnitude_bps=1.0, dc_leverage_boost=1.5, **base) # A3: DC7m1 + no boost (combine two best) strats['d_dc7m1_noboost'] = Strategy(name='d_dc7m1_noboost', max_hold=120, dc_lookback_bars=7, dc_min_magnitude_bps=1.0, dc_leverage_boost=1.0, **base) # ── Phase B: DC7m1_noboost + fraction sweep ────────────────────────── for frac in [0.10, 0.15, 0.20, 0.25]: n = f'd_dc7m1_nb_f{int(frac*100)}' strats[n] = Strategy(name=n, max_hold=120, fraction=frac, dc_lookback_bars=7, dc_min_magnitude_bps=1.0, dc_leverage_boost=1.0, **base) # ── Phase C: No boost + TP fine-tuning around sweet spot ───────────── for tp in [93, 95, 97, 100, 103, 105, 108]: n = f'd_nb_tp{tp}' b2 = {k: v for k, v in base.items() if k != 'fixed_tp_pct'} strats[n] = Strategy(name=n, max_hold=120, fixed_tp_pct=tp/10000.0, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_leverage_boost=1.0, **b2) # ── Phase D: DC param fine-tune around best (7/1.0 and 5/2.0) ──────── for lb in [6, 7, 8, 9, 10]: for mag in [0.5, 1.0, 1.5, 2.0]: n = f'd_nb_dc{lb}m{mag}' strats[n] = Strategy(name=n, max_hold=120, dc_lookback_bars=lb, dc_min_magnitude_bps=mag, dc_leverage_boost=1.0, **base) # ── Phase E: Dynamic leverage range sweep (with no boost) ──────────── for mn, mx in [(1.0, 3.0), (1.0, 5.0), (1.5, 4.0), (2.0, 5.0), (2.5, 5.0), (1.0, 7.0)]: n = f'd_nb_lev{mn:.0f}_{mx:.0f}' b2 = {k: v for k, v in base.items() if k not in ('min_leverage', 'max_leverage')} strats[n] = Strategy(name=n, max_hold=120, min_leverage=mn, max_leverage=mx, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_leverage_boost=1.0, **b2) # ── Phase F: No dynamic leverage at all (fixed lev) + no boost ─────── for lev in [1.5, 2.0, 2.5, 3.0, 4.0]: n = f'd_nb_fixlev{lev:.1f}' b2 = {**base, 'dynamic_leverage': False, 'leverage': lev} strats[n] = Strategy(name=n, max_hold=120, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_leverage_boost=1.0, **b2) # ── Phase G: Best combo candidates (combine multiple improvements) ─── # G1: DC7m1 + no boost + higher OB edge for ob in [5.0, 8.0]: n = f'd_dc7m1_nb_ob{int(ob)}' b2 = {**base, 'ob_edge_bps': ob} strats[n] = Strategy(name=n, max_hold=120, dc_lookback_bars=7, dc_min_magnitude_bps=1.0, dc_leverage_boost=1.0, **b2) # G2: No boost + no dynamic leverage (pure signal filter) strats['d_nb_nodynlev'] = Strategy(name='d_nb_nodynlev', max_hold=120, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_leverage_boost=1.0, **{**base, 'dynamic_leverage': False}) # G3: No boost + no alpha layers (test if alpha helps with no boost) strats['d_nb_noalpha'] = Strategy(name='d_nb_noalpha', max_hold=120, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_leverage_boost=1.0, **{**base, 'use_alpha_layers': False}) # G4: Ultimate combo: DC7m1 + no boost + fraction 20% + OB5 b_ob5 = {k: v for k, v in base.items() if k != 'ob_edge_bps'} strats['d_ultimate'] = Strategy(name='d_ultimate', max_hold=120, fraction=0.20, dc_lookback_bars=7, dc_min_magnitude_bps=1.0, dc_leverage_boost=1.0, ob_edge_bps=5.0, **b_ob5) # G5: Ultimate + TP fine-tune for tp in [95, 100, 105]: n = f'd_ultimate_tp{tp}' b_ob5_tp = {k: v for k, v in base.items() if k not in ('ob_edge_bps', 'fixed_tp_pct')} strats[n] = Strategy(name=n, max_hold=120, fraction=0.20, fixed_tp_pct=tp/10000.0, dc_lookback_bars=7, dc_min_magnitude_bps=1.0, dc_leverage_boost=1.0, ob_edge_bps=5.0, **b_ob5_tp) # ── Phase H: DC soft (don't skip contradicts, just reduce leverage) ── for reduce in [0.3, 0.5, 0.7]: n = f'd_nb_soft{int(reduce*10)}' strats[n] = Strategy(name=n, max_hold=120, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_leverage_boost=1.0, dc_skip_contradicts=False, dc_leverage_reduce=reduce, **{k: v for k, v in base.items() if k not in ('dc_skip_contradicts', 'dc_leverage_reduce')}) # ── Phase I: Add trailing ON TOP of FTP (test if it helps) ──────────── # Base has trailing=False. Test if adding tight trailing helps strats['d_nb_withtrail'] = Strategy(name='d_nb_withtrail', max_hold=120, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_leverage_boost=1.0, **{**base, 'use_trailing': True, 'trail_activation': 0.0003, 'trail_distance': 0.0003}) # Wider trailing (won't interfere with 100bps TP) strats['d_nb_widetrail'] = Strategy(name='d_nb_widetrail', max_hold=120, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_leverage_boost=1.0, **{**base, 'use_trailing': True, 'trail_activation': 0.005, 'trail_distance': 0.002}) # Also test with actual stop (20bps) to confirm stops hurt strats['d_nb_withstop'] = Strategy(name='d_nb_withstop', max_hold=120, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_leverage_boost=1.0, **{**base, 'stop_pct': 0.002}) # ── Phase J: IRP alignment sweep with no boost ─────────────────────── for align in [0.30, 0.35, 0.40, 0.50, 0.55]: n = f'd_nb_align{int(align*100)}' b2 = {k: v for k, v in base.items() if k != 'min_irp_alignment'} strats[n] = Strategy(name=n, max_hold=120, min_irp_alignment=align, dc_lookback_bars=5, dc_min_magnitude_bps=2.0, dc_leverage_boost=1.0, **b2) return strats def generate_grid5f_strategies() -> Dict[str, Strategy]: """ Grid 5F: FINAL COMBOS — combine best TP × DC × OB discoveries. Best individual: tp99 (PF=1.136), dc6m0.5 (PF=1.124), dc7m0.75 (PF=1.120) """ strats: Dict[str, Strategy] = {} base = dict( vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, stop_pct=1.0, max_hold=120, fraction=0.20, use_trailing=False, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, use_fixed_tp=True, use_direction_confirm=True, dc_skip_contradicts=True, dc_leverage_boost=1.0, dc_leverage_reduce=0.5, ) # Best TP × Best DC combos (with OB edge 3 and 5) for tp in [96, 97, 98, 99, 100]: for lb, mag in [(6, 0.5), (7, 0.75), (7, 1.0)]: for ob in [3.0, 5.0]: n = f'f_tp{tp}_dc{lb}m{mag}_ob{int(ob)}' strats[n] = Strategy(name=n, fixed_tp_pct=tp/10000.0, dc_lookback_bars=lb, dc_min_magnitude_bps=mag, ob_edge_bps=ob, **base) # Best combo + leverage range variants for mn, mx in [(0.5, 3.0), (0.5, 5.0), (1.0, 5.0)]: n = f'f_tp99_dc7m075_ob5_lev{mn:.0f}_{mx:.0f}' b2 = {k: v for k, v in base.items() if k not in ('min_leverage', 'max_leverage')} strats[n] = Strategy(name=n, fixed_tp_pct=0.0099, dc_lookback_bars=7, dc_min_magnitude_bps=0.75, ob_edge_bps=5.0, min_leverage=mn, max_leverage=mx, **b2) # Best combo + fraction variants for frac in [0.15, 0.25, 0.30]: n = f'f_tp99_dc7m075_ob5_f{int(frac*100)}' b2 = {k: v for k, v in base.items() if k != 'fraction'} strats[n] = Strategy(name=n, fraction=frac, fixed_tp_pct=0.0099, dc_lookback_bars=7, dc_min_magnitude_bps=0.75, ob_edge_bps=5.0, **b2) # Zero OB edge floor test (worst-case realistic) n = 'f_tp99_dc7m075_ob0' strats[n] = Strategy(name=n, fixed_tp_pct=0.0099, dc_lookback_bars=7, dc_min_magnitude_bps=0.75, ob_edge_bps=0.0, **base) return strats def generate_grid5g_strategies() -> Dict[str, Strategy]: """ Grid 5G: CONVEX LEVERAGE + HIGH CEILING — per-signal-strength leverage up to 25x. Convex scaling concentrates leverage on strongest signals (Kelly-optimal). Linear (1.0): weak=0.5x, mid=2.75x, strong=5x Quadratic (2.0): weak=0.5x, mid=1.63x, strong=5x (punishes mediocre signals) Cubic (3.0): weak=0.5x, mid=1.06x, strong=5x (extreme concentration) Risk management: higher max_leverage only hits on extreme signals (vel_div <= -0.05), which have historically highest win rates. Convexity ensures weak signals get minimal leverage. """ strats: Dict[str, Strategy] = {} base = dict( vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, stop_pct=1.0, max_hold=120, fraction=0.20, use_trailing=False, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=5.0, dynamic_leverage=True, min_leverage=0.5, use_alpha_layers=True, use_fixed_tp=True, fixed_tp_pct=0.0099, use_direction_confirm=True, dc_skip_contradicts=True, dc_leverage_boost=1.0, dc_leverage_reduce=0.5, dc_lookback_bars=7, dc_min_magnitude_bps=0.75, ) # ── Phase A: Convexity sweep at current max=5x ────────────────────────── # Baseline comparison: what does convexity alone do at 5x ceiling? for cvx in [1.0, 1.5, 2.0, 2.5, 3.0]: n = f'g_cvx{cvx:.1f}_max5' b2 = {k: v for k, v in base.items() if k not in ('max_leverage', 'leverage_convexity')} strats[n] = Strategy(name=n, max_leverage=5.0, leverage_convexity=cvx, **b2) # ── Phase B: Max leverage sweep at best convexities ───────────────────── # Test higher ceilings (10x, 15x, 20x, 25x) with convex curves for cvx in [1.5, 2.0, 2.5, 3.0]: for mx in [10.0, 15.0, 20.0, 25.0]: n = f'g_cvx{cvx:.1f}_max{int(mx)}' b2 = {k: v for k, v in base.items() if k not in ('max_leverage', 'leverage_convexity')} strats[n] = Strategy(name=n, max_leverage=mx, leverage_convexity=cvx, **b2) # ── Phase C: High leverage + reduced fraction (risk control) ──────────── # With 25x max, reduce fraction to control total notional exposure for frac in [0.05, 0.10, 0.15]: for cvx in [2.0, 3.0]: n = f'g_cvx{cvx:.0f}_max25_f{int(frac*100)}' b2 = {k: v for k, v in base.items() if k not in ('max_leverage', 'leverage_convexity', 'fraction')} strats[n] = Strategy(name=n, max_leverage=25.0, leverage_convexity=cvx, fraction=frac, **b2) # ── Phase D: Linear high leverage (danger test — should underperform convex) ── for mx in [10.0, 15.0, 25.0]: n = f'g_linear_max{int(mx)}' b2 = {k: v for k, v in base.items() if k not in ('max_leverage', 'leverage_convexity')} strats[n] = Strategy(name=n, max_leverage=mx, leverage_convexity=1.0, **b2) # ── Phase E: Best convex + zero OB edge (robustness floor) ────────────── for cvx in [2.0, 3.0]: for mx in [15.0, 25.0]: n = f'g_cvx{cvx:.0f}_max{int(mx)}_ob0' b2 = {k: v for k, v in base.items() if k not in ('max_leverage', 'leverage_convexity', 'ob_edge_bps')} strats[n] = Strategy(name=n, max_leverage=mx, leverage_convexity=cvx, ob_edge_bps=0.0, **b2) return strats def generate_grid5e_strategies() -> Dict[str, Strategy]: """ Grid 5E: FINAL FINE-TUNING around d_ultimate (PF=1.116, ROI=+25.1%) Base: DC7/1.0, no boost, frac=0.20, OB5, dynamic_lev 1-5, alpha ON, stop_pct=1.0, trailing=False, FTP=100bps, max_hold=120 """ strats: Dict[str, Strategy] = {} base = dict( vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, stop_pct=1.0, max_hold=120, fraction=0.20, use_trailing=False, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=5.0, dynamic_leverage=True, max_leverage=5.0, min_leverage=1.0, use_alpha_layers=True, use_fixed_tp=True, fixed_tp_pct=0.01, use_direction_confirm=True, dc_skip_contradicts=True, dc_leverage_boost=1.0, dc_leverage_reduce=0.5, dc_lookback_bars=7, dc_min_magnitude_bps=1.0, ) # ── Replicate baseline ──────────────────────────────────────────────── strats['e_base'] = Strategy(name='e_base', **base) # ── Phase A: TP fine-tune (93-108, step 1) ─────────────────────────── for tp in [90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 105, 108]: n = f'e_tp{tp}' b2 = {k: v for k, v in base.items() if k != 'fixed_tp_pct'} strats[n] = Strategy(name=n, fixed_tp_pct=tp/10000.0, **b2) # ── Phase B: DC lookback fine-tune (6-8, step 1) ───────────────────── for lb in [5, 6, 7, 8]: for mag in [0.5, 0.75, 1.0, 1.25, 1.5]: n = f'e_dc{lb}m{mag}' b2 = {k: v for k, v in base.items() if k not in ('dc_lookback_bars', 'dc_min_magnitude_bps')} strats[n] = Strategy(name=n, dc_lookback_bars=lb, dc_min_magnitude_bps=mag, **b2) # ── Phase C: OB edge sweep (realistic range 0-8) ──────────────────── for ob in [0, 1, 2, 3, 4, 5, 6, 7, 8, 10]: n = f'e_ob{ob}' b2 = {k: v for k, v in base.items() if k != 'ob_edge_bps'} strats[n] = Strategy(name=n, ob_edge_bps=float(ob), **b2) # ── Phase D: Fraction (capital utilization) ────────────────────────── for frac in [0.10, 0.15, 0.20, 0.25, 0.30, 0.35, 0.40]: n = f'e_frac{int(frac*100)}' b2 = {k: v for k, v in base.items() if k != 'fraction'} strats[n] = Strategy(name=n, fraction=frac, **b2) # ── Phase E: Dynamic leverage bounds ───────────────────────────────── for mn, mx in [(0.5, 3.0), (0.5, 5.0), (1.0, 3.0), (1.0, 5.0), (1.0, 7.0), (1.0, 10.0), (2.0, 5.0), (2.0, 7.0)]: n = f'e_lev{mn:.0f}_{mx:.0f}' b2 = {k: v for k, v in base.items() if k not in ('min_leverage', 'max_leverage')} strats[n] = Strategy(name=n, min_leverage=mn, max_leverage=mx, **b2) # ── Phase F: Best TP × DC cross (top combos only) ─────────────────── for tp in [93, 95, 100]: for lb, mag in [(6, 0.5), (7, 0.75), (7, 1.0)]: n = f'e_tp{tp}_dc{lb}m{mag}' b2 = {k: v for k, v in base.items() if k not in ('fixed_tp_pct', 'dc_lookback_bars', 'dc_min_magnitude_bps')} strats[n] = Strategy(name=n, fixed_tp_pct=tp/10000.0, dc_lookback_bars=lb, dc_min_magnitude_bps=mag, **b2) # ── Phase G: Hold time sweep ───────────────────────────────────────── for hold in [80, 100, 120, 140, 160, 200]: n = f'e_hold{hold}' b2 = {k: v for k, v in base.items() if k != 'max_hold'} strats[n] = Strategy(name=n, max_hold=hold, **b2) # ── Phase H: Vol filter ────────────────────────────────────────────── for vf in ['high', 'all', 'low_normal']: n = f'e_vol_{vf}' b2 = {k: v for k, v in base.items() if k != 'vol_filter'} strats[n] = Strategy(name=n, vol_filter=vf, **b2) # ── Phase I: VDT threshold ─────────────────────────────────────────── for vdt in [0.015, 0.018, 0.020, 0.022, 0.025]: n = f'e_vdt{vdt}' b2 = {k: v for k, v in base.items() if k != 'vel_div_threshold'} strats[n] = Strategy(name=n, vel_div_threshold=-vdt, **b2) return strats def run_sweep( df: pd.DataFrame, param_grid: Optional[Dict] = None, asset: str = 'BTCUSDT', direction: str = 'SHORT', vol_percentiles: Optional[Dict] = None, batch_size: int = 100, verbose: bool = True ) -> pd.DataFrame: """ Run parameter sweep using VBT column broadcasting. Args: df: Full DataFrame param_grid: Dict of parameter lists (default: DEFAULT_SWEEP_GRID) asset: Asset to trade direction: 'SHORT' or 'LONG' vol_percentiles: Pre-computed volatility percentiles batch_size: Process in batches to manage memory verbose: Print progress Returns: DataFrame with one row per parameter combo """ if param_grid is None: param_grid = DEFAULT_SWEEP_GRID # Generate all combinations param_names = list(param_grid.keys()) param_values = list(param_grid.values()) combos = list(product(*param_values)) n_combos = len(combos) if verbose: print(f"Running sweep: {n_combos} combinations") print(f" Parameters: {param_names}") print(f" Batch size: {batch_size}") results = [] # Process in batches for batch_start in range(0, n_combos, batch_size): batch_end = min(batch_start + batch_size, n_combos) batch_combos = combos[batch_start:batch_end] if verbose: print(f" Batch {batch_start//batch_size + 1}: combos {batch_start}-{batch_end-1}") # Prepare broadcasted data for this batch price_series = df[asset] n_batch = len(batch_combos) # Replicate price series for each combo price_broadcast = pd.concat([price_series] * n_batch, axis=1) price_broadcast.columns = range(n_batch) # Build entry signals for each combo (only vel_div_threshold varies entries) entries_list = [] for combo in batch_combos: params = dict(zip(param_names, combo)) entries = build_entry_signals( df, vel_div_threshold=params['vel_div_threshold'], vol_filter='all', # Simplified for sweep lookback=100 ) entries_list.append(entries) entries_broadcast = pd.concat(entries_list, axis=1) entries_broadcast.columns = range(n_batch) # Build parameter arrays for adjust_sl_func_nb trail_act_arr = np.array([c[1] for c in batch_combos]) # trail_activation trail_dist_arr = np.array([c[2] for c in batch_combos]) # trail_distance max_hold_arr = np.array([c[3] for c in batch_combos]) # max_hold stop_pct_arr = np.array([c[4] for c in batch_combos]) # stop_pct # Determine direction is_short = direction == 'SHORT' # Build adjust_sl_args for each column # VBT will broadcast these arrays across columns adjust_sl_args_per_col = [] for i in range(n_batch): adjust_sl_args_per_col.append(( np.float64(trail_act_arr[i]), np.float64(trail_dist_arr[i]), np.int64(max_hold_arr[i]), np.bool_(is_short) )) # For VBT broadcasting, we need to handle this differently # The adjust_sl_func_nb receives the same args for all columns # So we need to run each combo separately or use a different approach # Simpler approach: loop within batch (still faster than full loop due to VBT speed) for i, combo in enumerate(batch_combos): params = dict(zip(param_names, combo)) try: pf = run_backtest( df, asset=asset, vel_div_threshold=params['vel_div_threshold'], direction=direction, stop_pct=params['stop_pct'], max_hold=params['max_hold'], use_trailing=True, trail_activation=params['trail_activation'], trail_distance=params['trail_distance'], vol_filter='all', verbose=False ) metrics = extract_metrics(pf) metrics.update(params) # Add parameters to results results.append(metrics) except Exception as e: if verbose: print(f" Error on combo {combo}: {e}") # Add failed result metrics = {**params, 'trades': 0, 'win_rate': 0, 'profit_factor': 0} results.append(metrics) results_df = pd.DataFrame(results) if verbose: print(f"Sweep complete: {len(results_df)} results") return results_df # ═══════════════════════════════════════════════════════════════════════════════ # SECTION 8: VALIDATION # ═══════════════════════════════════════════════════════════════════════════════ def validate_against_itest( vbt_pf: vbt.Portfolio, expected_trades: int = None, expected_win_rate: float = None, expected_pf: float = None, expected_capital: float = None, tolerance: Dict = None ) -> Dict: """ Compare VBT Portfolio metrics to expected itest_v7 results. Args: vbt_pf: VBT Portfolio expected_trades: Expected trade count expected_win_rate: Expected win rate (0-1) expected_pf: Expected profit factor expected_capital: Expected final capital tolerance: Dict with tolerance values Returns: Dict with validation results """ if tolerance is None: tolerance = { 'trade_count_pct': 0.05, 'win_rate_abs': 0.02, 'profit_factor_pct': 0.10, 'capital_pct': 0.10, } metrics = extract_metrics(vbt_pf) results = { 'passed': True, 'details': [] } # Check trades if expected_trades is not None: trade_diff = abs(metrics['trades'] - expected_trades) / expected_trades passed = trade_diff <= tolerance['trade_count_pct'] results['details'].append({ 'metric': 'trades', 'expected': expected_trades, 'actual': metrics['trades'], 'diff_pct': trade_diff * 100, 'passed': passed }) results['passed'] = results['passed'] and passed # Check win rate if expected_win_rate is not None: wr_diff = abs(metrics['win_rate'] - expected_win_rate) passed = wr_diff <= tolerance['win_rate_abs'] results['details'].append({ 'metric': 'win_rate', 'expected': expected_win_rate, 'actual': metrics['win_rate'], 'diff': wr_diff, 'passed': passed }) results['passed'] = results['passed'] and passed # Check profit factor if expected_pf is not None: pf_diff = abs(metrics['profit_factor'] - expected_pf) / expected_pf if expected_pf != 0 else 0 passed = pf_diff <= tolerance['profit_factor_pct'] results['details'].append({ 'metric': 'profit_factor', 'expected': expected_pf, 'actual': metrics['profit_factor'], 'diff_pct': pf_diff * 100, 'passed': passed }) results['passed'] = results['passed'] and passed return results # ═══════════════════════════════════════════════════════════════════════════════ # SECTION 9: CLI ENTRY POINT # ═══════════════════════════════════════════════════════════════════════════════ if __name__ == '__main__': parser = argparse.ArgumentParser(description='DOLPHIN NG VBT Real Data Backtester') subparsers = parser.add_subparsers(dest='command') # Build cache command (full rebuild) build_parser = subparsers.add_parser('build-cache', help='Build Parquet cache from JSON scans (full rebuild)') build_parser.add_argument('--workers', type=int, default=4, help='Number of parallel workers') build_parser.add_argument('--path', type=str, default=str(DATA_PATH), help='Path to eigenvalues data') build_parser.add_argument('--cache', type=str, default=str(CACHE_DIR), help='Cache output directory') # Update cache command (incremental) update_parser = subparsers.add_parser('update-cache', help='Update Parquet cache with new/modified dates only') update_parser.add_argument('--workers', type=int, default=4, help='Number of parallel workers') update_parser.add_argument('--path', type=str, default=str(DATA_PATH), help='Path to eigenvalues data') update_parser.add_argument('--cache', type=str, default=str(CACHE_DIR), help='Cache output directory') update_parser.add_argument('--date', type=str, action='append', help='Specific date to update (can use multiple times)') update_parser.add_argument('--force', action='store_true', help='Force reprocess even if cache exists') # Check cache status command status_parser = subparsers.add_parser('cache-status', help='Check cache status and freshness') status_parser.add_argument('--path', type=str, default=str(DATA_PATH), help='Path to eigenvalues data') status_parser.add_argument('--cache', type=str, default=str(CACHE_DIR), help='Cache output directory') # Run single backtest command run_parser = subparsers.add_parser('run', help='Run single strategy backtest') run_parser.add_argument('--asset', default='BTCUSDT', help='Asset to trade') run_parser.add_argument('--threshold', type=float, default=-0.02, help='Vel_div threshold') run_parser.add_argument('--direction', default='SHORT', choices=['SHORT', 'LONG'], help='Trade direction') run_parser.add_argument('--stop', type=float, default=0.002, help='Stop loss %% (e.g., 0.002 = 0.2%%)') run_parser.add_argument('--max-hold', type=int, default=120, help='Max bars to hold') run_parser.add_argument('--trail-act', type=float, default=0.0003, help='Trailing activation %%') run_parser.add_argument('--trail-dist', type=float, default=0.0003, help='Trailing distance %%') run_parser.add_argument('--no-trailing', action='store_true', help='Disable trailing stop') run_parser.add_argument('--vol-filter', default='all', choices=['all', 'high', 'low', 'low_normal'], help='Vol filter') run_parser.add_argument('--maker-filter', action='store_true', help='Enable maker fill filtering') run_parser.add_argument('--output', default=None, help='JSON output file') # Parameter sweep command sweep_parser = subparsers.add_parser('sweep', help='Run parameter sweep') sweep_parser.add_argument('--asset', default='BTCUSDT', help='Asset to trade') sweep_parser.add_argument('--direction', default='SHORT', choices=['SHORT', 'LONG']) sweep_parser.add_argument('--batch-size', type=int, default=100, help='Batch size for memory management') sweep_parser.add_argument('--output', default='vbt_sweep_results.json', help='Output JSON file') # Validate command (Phase 1 - single asset) val_parser = subparsers.add_parser('validate', help='Validate against itest_v7 expected results') val_parser.add_argument('--strategy', default='no_trail_control', choices=['no_trail_control', 'tight_3_3', 'tight_3_3_no_rcdd'], help='Strategy to validate') # Phase II: Full v7 replication (multi-asset) v7_parser = subparsers.add_parser('run-v7', help='Run full v7 multi-asset simulation') v7_parser.add_argument('--strategy', default='all', help='Strategy name or "all" to run all') v7_parser.add_argument('--alpha', action='store_true', help='Run v5 alpha benchmark strategies') v7_parser.add_argument('--new', action='store_true', help='Run new VBT-native alpha strategies') v7_parser.add_argument('--v8', action='store_true', help='Run putative v8 replication strategies') v7_parser.add_argument('--all', action='store_true', help='Run v7 + alpha + new + v8 strategies') v7_parser.add_argument('--grid', action='store_true', help='Run comprehensive grid search for profitability') v7_parser.add_argument('--grid2', action='store_true', help='Run Round 2 focused grid (larger TP, longer hold, aggressive alpha)') v7_parser.add_argument('--grid3', action='store_true', help='Run Round 3 diagnostic (inverse plays, zero/HL fees)') v7_parser.add_argument('--grid4', action='store_true', help='Run Round 4 passive entry (SmartPlacer bar-by-bar fill sim)') v7_parser.add_argument('--grid5', action='store_true', help='Run Round 5 high leverage + direction confirmation + asset quality') v7_parser.add_argument('--grid5b', action='store_true', help='Run Round 5B refined DC to bridge final gap to profitability') v7_parser.add_argument('--grid5c', action='store_true', help='Run Round 5C WHY analysis + massive parameter sweep') v7_parser.add_argument('--grid5d', action='store_true', help='Run Round 5D optimal combinations from 5C findings') v7_parser.add_argument('--grid5e', action='store_true', help='Run Round 5E final fine-tuning around d_ultimate') v7_parser.add_argument('--grid5f', action='store_true', help='Run Round 5F final combos (best TP x DC x OB)') v7_parser.add_argument('--grid5g', action='store_true', help='Run Round 5G convex leverage + high ceiling') v7_parser.add_argument('--output', default=None, help='JSON output file') args = parser.parse_args() if args.command == 'build-cache': print("Building Parquet cache (FULL REBUILD)...") stats = build_parquet_cache( data_path=Path(args.path), cache_dir=Path(args.cache), max_workers=args.workers, force=True ) print(f"\nCache build stats: {stats}") elif args.command == 'update-cache': print("Updating Parquet cache (INCREMENTAL)...") dates = args.date if args.date else None stats = build_parquet_cache( data_path=Path(args.path), cache_dir=Path(args.cache), max_workers=args.workers, dates=dates, force=args.force ) print(f"\nCache update stats: {stats}") elif args.command == 'cache-status': print("Checking cache status...") data_path = Path(args.path) cache_dir = Path(args.cache) # Count source dates all_dates = sorted([d.name for d in data_path.iterdir() if d.is_dir() and not d.name.endswith('_SKIP')]) # Count cached dates cached_dates = sorted([f.stem for f in cache_dir.glob('*.parquet')]) # Find stale dates stale_dates = check_cache_freshness(data_path, cache_dir) print(f"\nSource dates: {len(all_dates)}") print(f"Cached dates: {len(cached_dates)}") print(f"Stale/missing: {len(stale_dates)}") if stale_dates: print(f"\nDates needing update:") for d in stale_dates[:10]: print(f" - {d}") if len(stale_dates) > 10: print(f" ... and {len(stale_dates)-10} more") else: print("\nCache is UP TO DATE!") # Show cache size total_size = sum(f.stat().st_size for f in cache_dir.glob('*.parquet')) print(f"\nTotal cache size: {total_size/1024/1024:.1f} MB") elif args.command == 'run': print("Loading data...") df = load_all_data() print(f"Running backtest: {args.asset} {args.direction}") pf = run_backtest( df, asset=args.asset, vel_div_threshold=args.threshold, direction=args.direction, stop_pct=args.stop, max_hold=args.max_hold, use_trailing=not args.no_trailing, trail_activation=args.trail_act, trail_distance=args.trail_dist, vol_filter=args.vol_filter, use_maker_filter=args.maker_filter, verbose=True ) metrics = extract_metrics(pf, strategy_name='cli_run') print("\n" + "="*50) print("BACKTEST RESULTS") print("="*50) for key, val in metrics.items(): if isinstance(val, float): print(f" {key}: {val:.4f}") else: print(f" {key}: {val}") if args.output: import json with open(args.output, 'w') as f: json.dump(metrics, f, indent=2) print(f"\nResults saved to {args.output}") elif args.command == 'sweep': print("Loading data...") df = load_all_data() print(f"Running parameter sweep...") results_df = run_sweep( df, asset=args.asset, direction=args.direction, batch_size=args.batch_size ) # Save results output_path = RESULTS_DIR / args.output results_df.to_json(output_path, orient='records', indent=2) print(f"\nSweep results saved to {output_path}") # Show top 5 by profit factor print("\nTop 5 by Profit Factor:") top5 = results_df.nlargest(5, 'profit_factor')[ ['vel_div_threshold', 'trail_activation', 'trail_distance', 'max_hold', 'stop_pct', 'profit_factor', 'win_rate', 'trades'] ] print(top5.to_string()) elif args.command == 'validate': # ────────────────────────────────────────────────────────────────────── # IMPORTANT: itest_v7 strategies ALL use features not yet in VBT Phase 1: # - use_asset_selection=True (trades 400 assets, not just BTCUSDT) # - use_sp_fees/slippage/ob_edge (probabilistic fee/slippage adjustments) # - use_rcdd (dynamic stop distances) # # VBT Phase 1 trades BTCUSDT only with fixed stops and no SP/OB features. # Expect DIRECTIONAL agreement (both unprofitable, PF < 1) but not exact match. # Tolerances are wider to account for feature differences. # ────────────────────────────────────────────────────────────────────── # itest_v7 reference values (with asset selection + SP + OB + RCDD) VALIDATION_TARGETS = { 'no_trail_control': { 'trades': 1531, # itest_v7 (with asset selection from 400 assets) 'win_rate': 0.3573, 'profit_factor': 0.734, 'capital': 6325, 'vol_filter': 'high', # itest_v7 uses high vol filter 'use_trailing': False, }, 'tight_3_3_no_rcdd': { 'trades': 5073, 'win_rate': 0.2833, 'profit_factor': 0.400, 'capital': 2621, 'vol_filter': 'high', 'use_trailing': True, }, 'tight_3_3': { 'trades': 4009, 'win_rate': 0.3198, 'profit_factor': 0.364, 'capital': 2391, 'vol_filter': 'high', 'use_trailing': True, } } target = VALIDATION_TARGETS[args.strategy] print("Loading data...") df = load_all_data() print(f"Validating strategy: {args.strategy}") print(f" NOTE: itest_v7 uses asset selection (400 assets) + SP + OB + RCDD") print(f" VBT Phase 1 uses BTCUSDT only, no SP/OB/RCDD") print(f" Expect directional agreement, not exact match") # Configure based on strategy - match vol_filter from itest_v7 pf = run_backtest( df, asset='BTCUSDT', vel_div_threshold=-0.02, direction='SHORT', stop_pct=0.002, max_hold=120, use_trailing=target['use_trailing'], trail_activation=0.0003, trail_distance=0.0003, vol_filter=target['vol_filter'], verbose=True ) # Use wider tolerances due to feature differences wide_tolerance = { 'trade_count_pct': 0.50, # 50% - asset selection changes trade count a lot 'win_rate_abs': 0.10, # 10pp - different assets = different WR 'profit_factor_pct': 0.50, # 50% - fees/slippage differ 'capital_pct': 0.50, } validation = validate_against_itest( pf, expected_trades=target['trades'], expected_win_rate=target['win_rate'], expected_pf=target['profit_factor'], expected_capital=target['capital'], tolerance=wide_tolerance ) metrics = extract_metrics(pf, strategy_name=args.strategy) print("\n" + "="*60) print(f"VALIDATION: {args.strategy}") print("="*60) print(f"\n{'Metric':<20} {'itest_v7':>12} {'VBT':>12} {'Diff':>12}") print("-"*60) for detail in validation['details']: expected = detail.get('expected', 0) actual = detail.get('actual', 0) if 'diff_pct' in detail: diff_str = f"{detail['diff_pct']:.1f}%" elif 'diff' in detail: diff_str = f"{detail['diff']:.4f}" else: diff_str = "N/A" status = "OK" if detail['passed'] else "DIFF" fmt_exp = f"{expected:.4f}" if isinstance(expected, float) else str(expected) fmt_act = f"{actual:.4f}" if isinstance(actual, float) else str(actual) print(f" {detail['metric']:<18} {fmt_exp:>12} {fmt_act:>12} {diff_str:>10} [{status}]") print(f"\nDirectional check: Both PF < 1.0? " f"{'YES' if metrics['profit_factor'] < 1.0 else 'NO'}") elif args.command == 'run-v7': print("Loading data...") df = load_all_data() # Load itest_v7 reference results ref_path = PROJECT_ROOT / 'itest_v7_results.json' ref_data = {} if ref_path.exists(): with open(ref_path) as f: ref_data = json.load(f).get('strategies', {}) # Build combined strategy pool based on flags # Merge all requested strategy dicts, keyed by name run_all = getattr(args, 'all', False) run_alpha = getattr(args, 'alpha', False) run_new = getattr(args, 'new', False) run_v8 = getattr(args, 'v8', False) run_grid = getattr(args, 'grid', False) run_grid2 = getattr(args, 'grid2', False) run_grid3 = getattr(args, 'grid3', False) run_grid4 = getattr(args, 'grid4', False) run_grid5 = getattr(args, 'grid5', False) run_grid5b = getattr(args, 'grid5b', False) run_grid5c = getattr(args, 'grid5c', False) run_grid5d = getattr(args, 'grid5d', False) run_grid5e = getattr(args, 'grid5e', False) run_grid5f = getattr(args, 'grid5f', False) run_grid5g = getattr(args, 'grid5g', False) all_strats = {} # Default: run v7 strategies (unless a specific pool flag is set) if run_all or (not run_alpha and not run_new and not run_v8 and not run_grid and not run_grid2 and not run_grid3 and not run_grid4 and not run_grid5 and not run_grid5b and not run_grid5c and not run_grid5d and not run_grid5e and not run_grid5f and not run_grid5g): if args.strategy == 'all': all_strats.update(V7_STRATEGIES) else: # Check if named strategy is in any pool for pool in [V7_STRATEGIES, ALPHA_STRATEGIES, NEW_STRATEGIES, V8_STRATEGIES]: if args.strategy in pool: all_strats[args.strategy] = pool[args.strategy] if run_all or run_alpha: all_strats.update(ALPHA_STRATEGIES) if run_all or run_new: all_strats.update(NEW_STRATEGIES) if run_all or run_v8: all_strats.update(V8_STRATEGIES) if run_grid: all_strats.update(generate_grid_strategies()) if run_grid2: all_strats.update(generate_grid2_strategies()) if run_grid3: all_strats.update(generate_grid3_strategies()) if run_grid4: all_strats.update(generate_grid4_strategies()) if run_grid5: all_strats.update(generate_grid5_strategies()) if run_grid5b: all_strats.update(generate_grid5b_strategies()) if run_grid5c: all_strats.update(generate_grid5c_strategies()) if run_grid5d: all_strats.update(generate_grid5d_strategies()) if run_grid5e: all_strats.update(generate_grid5e_strategies()) if run_grid5f: all_strats.update(generate_grid5f_strategies()) if run_grid5g: all_strats.update(generate_grid5g_strategies()) all_results = {} print(f"\nRunning {len(all_strats)} strategies (multi-asset, full features)...") print("=" * 80) for sname, strat in all_strats.items(): result = run_full_backtest(df, strat, seed=42) all_results[sname] = result print() # Print comparison table print("\n" + "=" * 110) print(f"{'Strategy':<30} {'Trades':>7} {'WR%':>7} {'PF':>7} {'Capital':>10} " f"{'ROI%':>8} {'Stop':>5} {'Trail':>6} {'Hold':>5} {'Tgt':>4} {'TP':>4} {'Time':>6}") print("-" * 115) for sname, r in all_results.items(): ref = ref_data.get(sname, {}) ref_trades = ref.get('trades', '') ref_pf = ref.get('profit_factor', '') ref_wr = ref.get('win_rate', '') n_tgt = r.get('target_exits', 0) n_tp = r.get('tp_exits', 0) print(f" VBT {sname:<24} {r['trades']:>7} {r['win_rate']:>6.1f} " f"{r['profit_factor']:>7.3f} {r['capital']:>10.0f} " f"{r['roi_pct']:>7.1f} {r['stop_exits']:>5} " f"{r['trailing_exits']:>6} {r['hold_exits']:>5} " f"{n_tgt:>4} {n_tp:>4} {r['elapsed_sec']:>5.1f}s") if ref: print(f" v7 {sname:<24} {ref_trades:>7} {ref_wr:>6.1f} " f"{ref_pf:>7.3f} {ref.get('capital', 0):>10.0f} " f"{ref.get('roi_pct', 0):>7.1f} {ref.get('stop_exits', 0):>5} " f"{ref.get('trailing_exits', 0):>6} {ref.get('hold_exits', 0):>5}") print() # Grid search: specialized sorted output if (run_grid or run_grid2) and len(all_results) > 20: # Sort by PF descending sorted_results = sorted(all_results.items(), key=lambda x: x[1].get('profit_factor', 0), reverse=True) print("\n" + "=" * 120) print("GRID SEARCH RESULTS - SORTED BY PROFIT FACTOR") print("=" * 120) print(f" Total configs tested: {len(sorted_results)}") # Count profitable profitable = [s for s in sorted_results if s[1].get('profit_factor', 0) > 1.0] print(f" Profitable (PF > 1.0): {len(profitable)}") if profitable: print(f"\n *** PROFITABLE CONFIGURATIONS FOUND ***") print(f"\n{'#':>3} {'Strategy':<40} {'Trades':>6} {'WR%':>6} {'PF':>7} " f"{'Capital':>9} {'ROI%':>7} {'S':>4} {'T':>4} {'H':>4} {'TP':>4}") print("-" * 120) # Show top 40 (or all if fewer) for rank, (sname, r) in enumerate(sorted_results[:40], 1): pf = r.get('profit_factor', 0) marker = ' *' if pf > 1.0 else ' ' n_tp = r.get('tp_exits', 0) print(f"{rank:>3}{marker}{sname:<38} {r['trades']:>6} " f"{r['win_rate']:>5.1f} {pf:>7.4f} " f"${r['capital']:>8.0f} {r['roi_pct']:>6.1f} " f"{r['stop_exits']:>4} {r['trailing_exits']:>4} " f"{r['hold_exits']:>4} {n_tp:>4}") if len(sorted_results) > 40: # Show worst 5 for context print(f"\n ... ({len(sorted_results) - 45} more) ...\n") for rank, (sname, r) in enumerate(sorted_results[-5:], len(sorted_results) - 4): pf = r.get('profit_factor', 0) n_tp = r.get('tp_exits', 0) print(f"{rank:>3} {sname:<38} {r['trades']:>6} " f"{r['win_rate']:>5.1f} {pf:>7.4f} " f"${r['capital']:>8.0f} {r['roi_pct']:>6.1f} " f"{r['stop_exits']:>4} {r['trailing_exits']:>4} " f"{r['hold_exits']:>4} {n_tp:>4}") # Summary stats all_pfs = [r.get('profit_factor', 0) for _, r in sorted_results] all_caps = [r.get('capital', 0) for _, r in sorted_results] print(f"\n PF range: {min(all_pfs):.4f} - {max(all_pfs):.4f}") print(f" Capital range: ${min(all_caps):.0f} - ${max(all_caps):.0f}") if profitable: print(f"\n BEST CONFIG: {profitable[0][0]}") best = profitable[0][1] print(f" PF={best['profit_factor']:.4f} WR={best['win_rate']:.1f}% " f"Capital=${best['capital']:.0f} ROI={best['roi_pct']:.1f}% " f"Trades={best['trades']}") # Save results output_file = args.output if (run_grid or run_grid2) and not output_file: output_file = 'grid_search_results.json' if output_file: output_path = RESULTS_DIR / output_file with open(output_path, 'w') as f: json.dump(all_results, f, indent=2) print(f"\nResults saved to {output_path}") else: parser.print_help()