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:
378
backtester/indicators/money_line_v9.py
Normal file
378
backtester/indicators/money_line_v9.py
Normal 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
|
||||
Reference in New Issue
Block a user