Files
siloqy/dolphin_vbt_real.py
HJ Normey 351ce2044d chore: safety snapshot 2026-03-05 — HCM infrastructure before 2y klines experiment
Captures critical infrastructure surrounding the nautilus_dolphin core package:
- dolphin_vbt_real.py: VBT vectorized backtest engine (6008 lines)
- dolphin_paper_trade_adaptive_cb_v2.py: champion runner (champion_5x_f20)
- _update_vbt_cache.py / update_VBT_parquet_cache.bat: cache builder
- external_factors/: ExF system (all 85 indicator fetchers + NPZ cache)
- mc_forewarning_qlabs_fork/: QLabs-enhanced MC-Forewarner research fork
- DATA_LOCATIONS.md: source-of-truth path registry
- .gitignore: excludes vbt_cache*, backfilled_data, .venv, models, etc.

Note: nautilus_dolphin/ has own git repo (inner) — safety snapshot committed there separately.
Champion state: WR=49.3%, ROI=+44.89%, PF=1.123, DD=14.95%, Sharpe=2.50 (55d, full-stack, abs_max_lev=6.0).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 23:51:30 +01:00

6008 lines
257 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

"""
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()