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>
6008 lines
257 KiB
Python
6008 lines
257 KiB
Python
"""
|
||
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()
|