feat: V9 advanced parameter sweep with MA gap filter (810K configs)

Parameter space expansion:
- Original 15 params: 101K configurations
- NEW: MA gap filter (3 dimensions) = 18× expansion
- Total: ~810,000 configurations across 4 time profiles
- Chunk size: 1,000 configs/chunk = ~810 chunks

MA Gap Filter parameters:
- use_ma_gap: True/False (2 values)
- ma_gap_min_long: -5.0%, 0%, +5.0% (3 values)
- ma_gap_min_short: -5.0%, 0%, +5.0% (3 values)

Implementation:
- money_line_v9.py: Full v9 indicator with MA gap logic
- v9_advanced_worker.py: Chunk processor (1,000 configs)
- v9_advanced_coordinator.py: Work distributor (2 EPYC workers)
- run_v9_advanced_sweep.sh: Startup script (generates + launches)

Infrastructure:
- Uses existing EPYC cluster (64 cores total)
- Worker1: bd-epyc-02 (32 threads)
- Worker2: bd-host01 (32 threads via SSH hop)
- Expected runtime: 70-80 hours
- Database: SQLite (chunk tracking + results)

Goal: Find optimal MA gap thresholds for filtering false breakouts
during MA whipsaw zones while preserving trend entries.
This commit is contained in:
mindesbunister
2025-12-01 18:11:47 +01:00
parent 2993bc8895
commit 7e1fe1cc30
9 changed files with 2541 additions and 0 deletions

View File

@@ -0,0 +1,378 @@
"""
v9 "Money Line with MA Gap" indicator implementation for backtesting.
Key features vs v8:
- confirmBars = 0 (immediate signals, no wait)
- flipThreshold = 0.5% (more responsive than v8's 0.8%)
- MA gap analysis (50/200 MA convergence/divergence)
- ATR profile system (timeframe-based ATR/multiplier)
- Expanded filters: RSI boundaries, volume range, entry buffer
- Heikin Ashi source mode support
- Price position filters (don't chase extremes)
ADVANCED OPTIMIZATION PARAMETERS:
- 8 ATR profile params (4 timeframes × period + multiplier)
- 4 RSI boundary params (long/short min/max)
- Volume max threshold
- Entry buffer ATR size
- ADX length
- Source mode (Chart vs Heikin Ashi)
- MA gap filter (optional - not in original v9)
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional
try:
from typing import Literal
except ImportError:
from typing_extensions import Literal
import numpy as np
import pandas as pd
from backtester.math_utils import calculate_adx, calculate_atr, rma
Direction = Literal["long", "short"]
@dataclass
class MoneyLineV9Inputs:
# Basic Money Line parameters (OPTIMIZED)
confirm_bars: int = 0 # v9: Immediate signals (0 = no wait)
flip_threshold_percent: float = 0.5 # v9: Lower threshold (more responsive)
cooldown_bars: int = 3 # Prevent overtrading
# ATR Profile System (NEW - for advanced optimization)
# Default: "minutes" profile optimized for 5-minute charts
atr_period: int = 12 # ATR calculation length
multiplier: float = 3.8 # ATR band multiplier
# Filter parameters (OPTIMIZED)
adx_length: int = 16 # ADX calculation length
adx_min: float = 21 # Minimum ADX for signal (momentum filter)
rsi_length: int = 14 # RSI calculation length
# RSI boundaries (EXPANDED - for advanced optimization)
rsi_long_min: float = 35 # RSI minimum for longs
rsi_long_max: float = 70 # RSI maximum for longs
rsi_short_min: float = 30 # RSI minimum for shorts
rsi_short_max: float = 70 # RSI maximum for shorts
# Volume filter (OPTIMIZED)
vol_min: float = 1.0 # Minimum volume ratio (1.0 = average)
vol_max: float = 3.5 # Maximum volume ratio (prevent overheated)
# Price position filter (OPTIMIZED)
long_pos_max: float = 75 # Don't long above 75% of range (chase tops)
short_pos_min: float = 20 # Don't short below 20% of range (chase bottoms)
# Entry buffer (NEW - for advanced optimization)
entry_buffer_atr: float = 0.20 # Require price X*ATR beyond line
# Source mode (NEW - for advanced optimization)
use_heikin_ashi: bool = False # Use Heikin Ashi candles vs Chart
# MA gap filter (NEW - optional, not in original v9)
use_ma_gap_filter: bool = False # Require MA alignment
ma_gap_long_min: float = 0.0 # Require ma50 > ma200 by this % for longs
ma_gap_short_max: float = 0.0 # Require ma50 < ma200 by this % for shorts
@dataclass
class MoneyLineV9Signal:
timestamp: pd.Timestamp
direction: Direction
entry_price: float
adx: float
atr: float
rsi: float
volume_ratio: float
price_position: float
ma_gap: float # NEW: MA50-MA200 gap percentage
def ema(series: pd.Series, length: int) -> pd.Series:
"""Exponential Moving Average."""
return series.ewm(span=length, adjust=False).mean()
def sma(series: pd.Series, length: int) -> pd.Series:
"""Simple Moving Average."""
return series.rolling(length).mean()
def rolling_volume_ratio(volume: pd.Series, length: int = 20) -> pd.Series:
"""Volume ratio vs moving average."""
avg = volume.rolling(length).mean()
return volume / avg
def price_position(high: pd.Series, low: pd.Series, close: pd.Series, length: int = 100) -> pd.Series:
"""Price position in percentage of range (0-100)."""
highest = high.rolling(length).max()
lowest = low.rolling(length).min()
return 100.0 * (close - lowest) / (highest - lowest)
def rsi(series: pd.Series, length: int) -> pd.Series:
"""Relative Strength Index."""
delta = series.diff()
gain = np.where(delta > 0, delta, 0.0)
loss = np.where(delta < 0, -delta, 0.0)
avg_gain = rma(pd.Series(gain), length)
avg_loss = rma(pd.Series(loss), length)
rs = avg_gain / avg_loss.replace(0, np.nan)
rsi_series = 100 - (100 / (1 + rs))
return rsi_series.fillna(50.0)
def heikin_ashi(df: pd.DataFrame) -> pd.DataFrame:
"""Calculate Heikin Ashi candles."""
ha = pd.DataFrame(index=df.index)
ha['close'] = (df['open'] + df['high'] + df['low'] + df['close']) / 4
# Calculate HA open
ha['open'] = df['open'].copy()
for i in range(1, len(df)):
ha.loc[ha.index[i], 'open'] = (ha.loc[ha.index[i-1], 'open'] + ha.loc[ha.index[i-1], 'close']) / 2
ha['high'] = df[['high']].join(ha[['open', 'close']]).max(axis=1)
ha['low'] = df[['low']].join(ha[['open', 'close']]).min(axis=1)
return ha
def supertrend_v9(df: pd.DataFrame, atr_period: int, multiplier: float,
flip_threshold_percent: float, confirm_bars: int,
use_heikin_ashi: bool = False) -> tuple[pd.Series, pd.Series]:
"""
Calculate v9 Money Line (Supertrend with flip threshold and momentum).
Returns:
(supertrend_line, trend): Line values and trend direction (1=bull, -1=bear)
"""
# Use Heikin Ashi or Chart
if use_heikin_ashi:
ha = heikin_ashi(df[['open', 'high', 'low', 'close']])
high, low, close = ha['high'], ha['low'], ha['close']
else:
high, low, close = df['high'], df['low'], df['close']
# Calculate ATR on selected source
tr = pd.concat([
high - low,
(high - close.shift(1)).abs(),
(low - close.shift(1)).abs()
], axis=1).max(axis=1)
atr = rma(tr, atr_period)
# Supertrend bands
src = (high + low) / 2
up = src - (multiplier * atr)
dn = src + (multiplier * atr)
# Initialize tracking arrays
up1 = up.copy()
dn1 = dn.copy()
trend = pd.Series(1, index=df.index) # Start bullish
tsl = up1.copy() # Trailing stop line
# Momentum tracking for anti-whipsaw
bull_momentum = pd.Series(0, index=df.index)
bear_momentum = pd.Series(0, index=df.index)
# Calculate flip threshold
threshold = flip_threshold_percent / 100.0
for i in range(1, len(df)):
# Update bands
if close.iloc[i-1] > up1.iloc[i-1]:
up1.iloc[i] = max(up.iloc[i], up1.iloc[i-1])
else:
up1.iloc[i] = up.iloc[i]
if close.iloc[i-1] < dn1.iloc[i-1]:
dn1.iloc[i] = min(dn.iloc[i], dn1.iloc[i-1])
else:
dn1.iloc[i] = dn.iloc[i]
# Get previous trend and tsl
prev_trend = trend.iloc[i-1]
prev_tsl = tsl.iloc[i-1]
# Update TSL based on trend
if prev_trend == 1:
tsl.iloc[i] = max(up1.iloc[i], prev_tsl)
else:
tsl.iloc[i] = min(dn1.iloc[i], prev_tsl)
# Check for flip with threshold and momentum
threshold_amount = tsl.iloc[i] * threshold
if prev_trend == 1:
# Currently bullish - check for bearish flip
if close.iloc[i] < (tsl.iloc[i] - threshold_amount):
bear_momentum.iloc[i] = bear_momentum.iloc[i-1] + 1
bull_momentum.iloc[i] = 0
else:
bear_momentum.iloc[i] = 0
bull_momentum.iloc[i] = 0
# Flip after confirm_bars + 1 consecutive bearish bars
if bear_momentum.iloc[i] >= (confirm_bars + 1):
trend.iloc[i] = -1
else:
trend.iloc[i] = 1
else:
# Currently bearish - check for bullish flip
if close.iloc[i] > (tsl.iloc[i] + threshold_amount):
bull_momentum.iloc[i] = bull_momentum.iloc[i-1] + 1
bear_momentum.iloc[i] = 0
else:
bull_momentum.iloc[i] = 0
bear_momentum.iloc[i] = 0
# Flip after confirm_bars + 1 consecutive bullish bars
if bull_momentum.iloc[i] >= (confirm_bars + 1):
trend.iloc[i] = 1
else:
trend.iloc[i] = -1
return tsl, trend
def money_line_v9_signals(df: pd.DataFrame, inputs: Optional[MoneyLineV9Inputs] = None) -> list[MoneyLineV9Signal]:
"""
v9 "Money Line with MA Gap" signal generation.
Key behavior:
- Immediate signals on line flip (confirmBars=0)
- Lower flip threshold (0.5% vs v8's 0.8%)
- Expanded filters: RSI boundaries, volume range, price position
- MA gap analysis for trend structure
- Entry buffer requirement (price must be X*ATR beyond line)
- Heikin Ashi source mode support
Advanced optimization parameters:
- ATR profile (period + multiplier)
- RSI boundaries (4 params)
- Volume max threshold
- Entry buffer size
- ADX length
- Source mode
- MA gap filter (optional)
"""
if inputs is None:
inputs = MoneyLineV9Inputs()
data = df.copy()
data = data.sort_index()
# Calculate Money Line
supertrend, trend = supertrend_v9(
data,
inputs.atr_period,
inputs.multiplier,
inputs.flip_threshold_percent,
inputs.confirm_bars,
inputs.use_heikin_ashi
)
data['supertrend'] = supertrend
data['trend'] = trend
# Calculate indicators (use Chart prices for consistency with filters)
data["rsi"] = rsi(data["close"], inputs.rsi_length)
data["atr"] = calculate_atr(data, inputs.atr_period)
data["adx"] = calculate_adx(data, inputs.adx_length)
data["volume_ratio"] = rolling_volume_ratio(data["volume"])
data["price_position"] = price_position(data["high"], data["low"], data["close"])
# MA gap analysis (NEW)
data["ma50"] = sma(data["close"], 50)
data["ma200"] = sma(data["close"], 200)
data["ma_gap"] = ((data["ma50"] - data["ma200"]) / data["ma200"]) * 100
signals: list[MoneyLineV9Signal] = []
cooldown_remaining = 0
for idx in range(1, len(data)):
row = data.iloc[idx]
prev = data.iloc[idx - 1]
# Detect trend flip
flip_long = prev.trend == -1 and row.trend == 1
flip_short = prev.trend == 1 and row.trend == -1
if cooldown_remaining > 0:
cooldown_remaining -= 1
continue
# Apply filters
adx_ok = row.adx >= inputs.adx_min
volume_ok = inputs.vol_min <= row.volume_ratio <= inputs.vol_max
# Entry buffer check (price must be X*ATR beyond line)
if flip_long:
entry_buffer_ok = row.close > (row.supertrend + inputs.entry_buffer_atr * row.atr)
elif flip_short:
entry_buffer_ok = row.close < (row.supertrend - inputs.entry_buffer_atr * row.atr)
else:
entry_buffer_ok = False
if flip_long:
# Long filters
rsi_ok = inputs.rsi_long_min <= row.rsi <= inputs.rsi_long_max
pos_ok = row.price_position < inputs.long_pos_max
# MA gap filter (optional)
if inputs.use_ma_gap_filter:
ma_gap_ok = row.ma_gap >= inputs.ma_gap_long_min
else:
ma_gap_ok = True
if adx_ok and volume_ok and rsi_ok and pos_ok and entry_buffer_ok and ma_gap_ok:
signals.append(
MoneyLineV9Signal(
timestamp=row.name,
direction="long",
entry_price=float(row.close),
adx=float(row.adx),
atr=float(row.atr),
rsi=float(row.rsi),
volume_ratio=float(row.volume_ratio),
price_position=float(row.price_position),
ma_gap=float(row.ma_gap),
)
)
cooldown_remaining = inputs.cooldown_bars
elif flip_short:
# Short filters
rsi_ok = inputs.rsi_short_min <= row.rsi <= inputs.rsi_short_max
pos_ok = row.price_position > inputs.short_pos_min
# MA gap filter (optional)
if inputs.use_ma_gap_filter:
ma_gap_ok = row.ma_gap <= inputs.ma_gap_short_max
else:
ma_gap_ok = True
if adx_ok and volume_ok and rsi_ok and pos_ok and entry_buffer_ok and ma_gap_ok:
signals.append(
MoneyLineV9Signal(
timestamp=row.name,
direction="short",
entry_price=float(row.close),
adx=float(row.adx),
atr=float(row.atr),
rsi=float(row.rsi),
volume_ratio=float(row.volume_ratio),
price_position=float(row.price_position),
ma_gap=float(row.ma_gap),
)
)
cooldown_remaining = inputs.cooldown_bars
return signals