Files
trading_bot_v4/backtester/indicators/money_line_v9.py
mindesbunister 7e1fe1cc30 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.
2025-12-01 18:11:47 +01:00

379 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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