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
|
||||
349
cluster/V9_ADVANCED_SWEEP_README.md
Normal file
349
cluster/V9_ADVANCED_SWEEP_README.md
Normal file
@@ -0,0 +1,349 @@
|
||||
# V9 Advanced Parameter Sweep - 810K Configurations
|
||||
|
||||
**Status:** Ready to launch (Dec 1, 2025)
|
||||
**Total Configs:** ~810,000 (18-parameter grid with MA gap filter)
|
||||
**Expected Runtime:** 70-80 hours on 2 EPYC servers
|
||||
**Enhancement:** Added MA gap filter exploration (8× expansion from 101K)
|
||||
|
||||
## Architecture
|
||||
|
||||
### Parameter Space (18 dimensions)
|
||||
|
||||
Builds on existing v9 grid but adds **MA gap filter** parameters:
|
||||
|
||||
**Original 15 parameters (101K configs):**
|
||||
- Time profiles: minutes, hours, daily, weekly (4 profiles)
|
||||
- ATR periods: profile-specific (3-4 values each)
|
||||
- ATR multipliers: profile-specific (3-4 values each)
|
||||
- RSI boundaries: long_min/max, short_min/max (3×4 values)
|
||||
- Volume max: 3.0, 3.5, 4.0
|
||||
- Entry buffer: 0.15, 0.20, 0.25
|
||||
- ADX length: 14, 16, 18
|
||||
|
||||
**NEW: MA Gap Filter (3 dimensions = 18× multiplier):**
|
||||
- `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)
|
||||
|
||||
**Total:** 101K × 2 × 3 × 3 = **~810,000 configurations**
|
||||
|
||||
### What is MA Gap Filter?
|
||||
|
||||
**Purpose:** Filter entries based on MA50-MA200 convergence/divergence
|
||||
|
||||
**Long Entry Logic:**
|
||||
```python
|
||||
if use_ma_gap and ma_gap_min_long is not None:
|
||||
ma_gap_percent = (ma50 - ma200) / ma200 * 100
|
||||
if ma_gap_percent < ma_gap_min_long:
|
||||
block_entry # MAs too diverged or converging wrong way
|
||||
```
|
||||
|
||||
**Short Entry Logic:**
|
||||
```python
|
||||
if use_ma_gap and ma_gap_min_short is not None:
|
||||
ma_gap_percent = (ma50 - ma200) / ma200 * 100
|
||||
if ma_gap_percent > ma_gap_min_short:
|
||||
block_entry # MAs too diverged or converging wrong way
|
||||
```
|
||||
|
||||
**Hypothesis:**
|
||||
- **LONG at MA crossover:** Require ma_gap ≥ 0% (bullish or neutral)
|
||||
- **SHORT at MA crossover:** Require ma_gap ≤ 0% (bearish or neutral)
|
||||
- **Avoid whipsaws:** Block entries when MAs are too diverged
|
||||
|
||||
**Parameter Exploration:**
|
||||
- `-5.0%`: Allows 5% adverse gap (catching reversals)
|
||||
- `0%`: Requires neutral or favorable gap
|
||||
- `+5.0%`: Requires 5% favorable gap (strong trend only)
|
||||
|
||||
**Expected Findings:**
|
||||
1. **Optimal gap thresholds** for each profile (minutes vs daily may differ)
|
||||
2. **Direction-specific gaps** (LONGs may need different threshold than SHORTs)
|
||||
3. **Performance comparison** (use_ma_gap=True vs False baseline)
|
||||
|
||||
### Why This Matters
|
||||
|
||||
**User Context (Nov 27 analysis):**
|
||||
- v9 has strong baseline edge ($405.88 on 1-year data)
|
||||
- But parameter insensitivity suggests edge is in **logic**, not tuning
|
||||
- MA gap filter adds **new logic dimension** (not just parameter tuning)
|
||||
- Could filter false breakouts that occur during MA whipsaw zones
|
||||
|
||||
**Real-world validation needed:**
|
||||
- Some MAs converging = good entries (trend formation)
|
||||
- Some MAs diverged = good entries (strong trend continuation)
|
||||
- Optimal gap threshold is data-driven discovery goal
|
||||
|
||||
## Implementation
|
||||
|
||||
### Files
|
||||
|
||||
```
|
||||
cluster/
|
||||
├── money_line_v9.py # v9 indicator (copied from backtester/indicators/)
|
||||
├── v9_advanced_worker.py # Worker script (processes 1 chunk)
|
||||
├── v9_advanced_coordinator.py # Coordinator (assigns chunks to workers)
|
||||
├── run_v9_advanced_sweep.sh # Startup script (generates configs + launches)
|
||||
├── chunks/ # Generated parameter configurations
|
||||
│ ├── v9_advanced_chunk_0000.json (1,000 configs)
|
||||
│ ├── v9_advanced_chunk_0001.json (1,000 configs)
|
||||
│ └── ... (~810 chunk files)
|
||||
├── exploration.db # SQLite database (chunk tracking)
|
||||
└── distributed_results/ # CSV outputs from workers
|
||||
├── v9_advanced_chunk_0000_results.csv
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Database Schema
|
||||
|
||||
**v9_advanced_chunks table:**
|
||||
```sql
|
||||
CREATE TABLE v9_advanced_chunks (
|
||||
id TEXT PRIMARY KEY, -- v9_advanced_chunk_0000
|
||||
start_combo INTEGER, -- 0 (not used, legacy)
|
||||
end_combo INTEGER, -- 1000 (not used, legacy)
|
||||
total_combos INTEGER, -- 1000 configs per chunk
|
||||
status TEXT, -- 'pending', 'running', 'completed', 'failed'
|
||||
assigned_worker TEXT, -- 'worker1', 'worker2', NULL
|
||||
started_at INTEGER, -- Unix timestamp
|
||||
completed_at INTEGER, -- Unix timestamp
|
||||
created_at INTEGER DEFAULT (strftime('%s', 'now'))
|
||||
)
|
||||
```
|
||||
|
||||
**v9_advanced_strategies table:**
|
||||
```sql
|
||||
CREATE TABLE v9_advanced_strategies (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
chunk_id TEXT NOT NULL, -- FK to v9_advanced_chunks
|
||||
params TEXT NOT NULL, -- JSON of 18 parameters
|
||||
pnl REAL NOT NULL, -- Total P&L
|
||||
win_rate REAL NOT NULL, -- % winners
|
||||
profit_factor REAL NOT NULL, -- (Not yet implemented)
|
||||
max_drawdown REAL NOT NULL, -- Max DD %
|
||||
total_trades INTEGER NOT NULL, -- Number of trades
|
||||
created_at INTEGER DEFAULT (strftime('%s', 'now'))
|
||||
)
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Launch Sweep
|
||||
|
||||
```bash
|
||||
cd /home/icke/traderv4/cluster
|
||||
./run_v9_advanced_sweep.sh
|
||||
```
|
||||
|
||||
**What it does:**
|
||||
1. Generates 810K parameter configurations
|
||||
2. Splits into ~810 chunks (1,000 configs each)
|
||||
3. Creates SQLite database with chunk tracking
|
||||
4. Launches coordinator in background
|
||||
5. Coordinator assigns chunks to 2 EPYC workers
|
||||
6. Workers process chunks in parallel
|
||||
|
||||
### 2. Monitor Progress
|
||||
|
||||
**Web Dashboard:**
|
||||
```
|
||||
http://localhost:3001/cluster
|
||||
```
|
||||
|
||||
**Command Line:**
|
||||
```bash
|
||||
# Watch coordinator logs
|
||||
tail -f coordinator_v9_advanced.log
|
||||
|
||||
# Check database status
|
||||
sqlite3 exploration.db "
|
||||
SELECT
|
||||
status,
|
||||
COUNT(*) as count,
|
||||
ROUND(COUNT(*) * 100.0 / 810, 1) as percent
|
||||
FROM v9_advanced_chunks
|
||||
GROUP BY status
|
||||
"
|
||||
```
|
||||
|
||||
### 3. Analyze Results
|
||||
|
||||
After completion, aggregate all results:
|
||||
|
||||
```bash
|
||||
# Combine all CSV files
|
||||
cd distributed_results
|
||||
cat v9_advanced_chunk_*_results.csv | head -1 > all_v9_advanced_results.csv
|
||||
tail -n +2 -q v9_advanced_chunk_*_results.csv >> all_v9_advanced_results.csv
|
||||
|
||||
# Top 100 performers
|
||||
sort -t, -k2 -rn all_v9_advanced_results.csv | head -100 > top_100_v9_advanced.csv
|
||||
```
|
||||
|
||||
**Analysis queries:**
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
|
||||
df = pd.read_csv('all_v9_advanced_results.csv')
|
||||
|
||||
# Compare MA gap filter vs baseline
|
||||
baseline = df[df['use_ma_gap'] == False]
|
||||
filtered = df[df['use_ma_gap'] == True]
|
||||
|
||||
print(f"Baseline avg: ${baseline['profit'].mean():.2f}")
|
||||
print(f"Filtered avg: ${filtered['profit'].mean():.2f}")
|
||||
|
||||
# Find optimal gap thresholds
|
||||
for profile in ['minutes', 'hours', 'daily', 'weekly']:
|
||||
profile_df = df[df['profile'] == profile]
|
||||
best = profile_df.nlargest(10, 'profit')
|
||||
print(f"\n{profile.upper()} - Top 10 gap thresholds:")
|
||||
print(best[['ma_gap_min_long', 'ma_gap_min_short', 'profit', 'win_rate']])
|
||||
```
|
||||
|
||||
## Expected Outcomes
|
||||
|
||||
### If MA Gap Filter Helps:
|
||||
|
||||
**Expected pattern:**
|
||||
- Filtered configs outperform baseline
|
||||
- Optimal gap thresholds cluster around certain values
|
||||
- Direction-specific gaps emerge (LONGs need +gap, SHORTs need -gap)
|
||||
|
||||
**Action:**
|
||||
- Update production v9 with optimal gap thresholds
|
||||
- Deploy to live trading after forward testing
|
||||
|
||||
### If MA Gap Filter Hurts:
|
||||
|
||||
**Expected pattern:**
|
||||
- Baseline (use_ma_gap=False) outperforms all filtered configs
|
||||
- No clear threshold patterns emerge
|
||||
- Performance degrades with stricter gaps
|
||||
|
||||
**Action:**
|
||||
- Keep production v9 as-is (no MA gap filter)
|
||||
- Document findings: MA divergence not predictive for v9 signals
|
||||
|
||||
### If Results Inconclusive:
|
||||
|
||||
**Expected pattern:**
|
||||
- Filtered and baseline perform similarly
|
||||
- High variance in gap threshold performance
|
||||
|
||||
**Action:**
|
||||
- Keep baseline for simplicity (Occam's Razor)
|
||||
- Consider gap as optional "turbo mode" for specific profiles
|
||||
|
||||
## Worker Infrastructure
|
||||
|
||||
Uses **existing EPYC cluster setup** (64 cores total):
|
||||
|
||||
**Worker 1 (bd-epyc-02):**
|
||||
- Direct SSH: `root@10.10.254.106`
|
||||
- Workspace: `/home/comprehensive_sweep`
|
||||
- Python: `.venv` with pandas/numpy
|
||||
- Cores: 32 threads
|
||||
|
||||
**Worker 2 (bd-host01):**
|
||||
- SSH hop: via worker1
|
||||
- Direct: `root@10.20.254.100`
|
||||
- Workspace: `/home/backtest_dual/backtest`
|
||||
- Python: `.venv` with pandas/numpy
|
||||
- Cores: 32 threads
|
||||
|
||||
**Prerequisites (already met):**
|
||||
- Python 3.11+ with pandas, numpy
|
||||
- Virtual environments active
|
||||
- SOLUSDT 5m OHLCV data (Nov 2024 - Nov 2025)
|
||||
- SSH keys configured
|
||||
|
||||
## Timeline
|
||||
|
||||
**Estimate:** 70-80 hours total (810K configs ÷ 64 cores)
|
||||
|
||||
**Breakdown:**
|
||||
- Config generation: ~5 minutes
|
||||
- Chunk assignment: ~1 minute
|
||||
- Parallel execution: ~70 hours (1,000 configs/chunk × ~3.1s/config ÷ 2 workers)
|
||||
- Result aggregation: ~10 minutes
|
||||
|
||||
**Monitoring intervals:**
|
||||
- Check status: Every 30-60 minutes
|
||||
- Full results available: After ~3 days
|
||||
|
||||
## Lessons from Previous Sweeps
|
||||
|
||||
### v9 Baseline (65K configs, Nov 28-29):
|
||||
- **Finding:** Parameter insensitivity observed
|
||||
- **Implication:** Edge is in core logic, not specific parameter values
|
||||
- **Action:** Explore new logic dimensions (MA gap) instead of tighter parameter grids
|
||||
|
||||
### v10 Removal (Nov 28):
|
||||
- **Finding:** 72 configs produced identical results
|
||||
- **Implication:** New logic must add real edge, not just complexity
|
||||
- **Action:** MA gap filter is **observable market state** (not derived metric)
|
||||
|
||||
### Distributed Worker Bug (Dec 1):
|
||||
- **Finding:** Dict passed instead of lambda function
|
||||
- **Implication:** Type safety critical for 810K config sweep
|
||||
- **Action:** Simplified v9_advanced_worker.py with explicit types
|
||||
|
||||
## File Locations
|
||||
|
||||
**Master (srvdocker02):**
|
||||
```
|
||||
/home/icke/traderv4/cluster/
|
||||
```
|
||||
|
||||
**Workers (EPYC servers):**
|
||||
```
|
||||
/home/comprehensive_sweep/ (worker1)
|
||||
/home/backtest_dual/backtest/ (worker2)
|
||||
```
|
||||
|
||||
**Results (master):**
|
||||
```
|
||||
/home/icke/traderv4/cluster/distributed_results/
|
||||
```
|
||||
|
||||
## Git Commits
|
||||
|
||||
```bash
|
||||
# Before launch
|
||||
git add cluster/
|
||||
git commit -m "feat: V9 advanced parameter sweep with MA gap filter (810K configs)
|
||||
|
||||
- Added MA gap filter exploration (3 dimensions = 18× expansion)
|
||||
- Created v9_advanced_worker.py for chunk processing
|
||||
- Created v9_advanced_coordinator.py for work distribution
|
||||
- Uses existing EPYC cluster infrastructure (64 cores)
|
||||
- Expected runtime: 70-80 hours for 810K configurations
|
||||
"
|
||||
git push
|
||||
```
|
||||
|
||||
## Post-Sweep Actions
|
||||
|
||||
1. **Aggregate results** into single CSV
|
||||
2. **Compare MA gap filter** vs baseline performance
|
||||
3. **Identify optimal thresholds** per profile and direction
|
||||
4. **Update production v9** if MA gap filter shows consistent edge
|
||||
5. **Forward test** for 50-100 trades before live deployment
|
||||
6. **Document findings** in INDICATOR_V9_MA_GAP_ROADMAP.md
|
||||
|
||||
## Support
|
||||
|
||||
**Questions during sweep:**
|
||||
- Check `coordinator_v9_advanced.log` for coordinator status
|
||||
- Check worker logs via SSH: `ssh worker1 tail -f /home/comprehensive_sweep/worker.log`
|
||||
- Database queries: `sqlite3 exploration.db "SELECT ..."
|
||||
`
|
||||
|
||||
**If sweep stalls:**
|
||||
1. Check coordinator process: `ps aux | grep v9_advanced_coordinator`
|
||||
2. Check worker processes: SSH to workers, `ps aux | grep python`
|
||||
3. Reset failed chunks: `UPDATE v9_advanced_chunks SET status='pending' WHERE status='failed'`
|
||||
4. Restart coordinator: `./run_v9_advanced_sweep.sh`
|
||||
378
cluster/money_line_v9.py
Normal file
378
cluster/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
|
||||
219
cluster/run_v9_advanced_sweep.sh
Executable file
219
cluster/run_v9_advanced_sweep.sh
Executable file
@@ -0,0 +1,219 @@
|
||||
#!/bin/bash
|
||||
# V9 Advanced Parameter Sweep - 810K configs with MA gap filter exploration
|
||||
# Uses existing cluster infrastructure with all dependencies already installed
|
||||
|
||||
set -e
|
||||
|
||||
echo "=========================================="
|
||||
echo "V9 ADVANCED PARAMETER SWEEP"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Configuration:"
|
||||
echo " • Total configs: ~810,000 (18 parameters)"
|
||||
echo " • New parameters: MA gap filter (3 dimensions)"
|
||||
echo " • Chunk size: 1,000 configs/chunk = ~810 chunks"
|
||||
echo " • Workers: 2 EPYCs"
|
||||
echo " • Expected runtime: 70-80 hours"
|
||||
echo ""
|
||||
|
||||
# Check if data file exists
|
||||
DATA_FILE="data/solusdt_5m.csv"
|
||||
if [ ! -f "$DATA_FILE" ]; then
|
||||
echo "❌ Error: Data file not found: $DATA_FILE"
|
||||
echo "Please ensure OHLCV data is available"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Activate virtual environment
|
||||
echo "Activating Python environment..."
|
||||
source .venv/bin/activate
|
||||
|
||||
# Generate parameter configurations
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "STEP 1: Generate Configurations"
|
||||
echo "=========================================="
|
||||
|
||||
python3 << 'PYTHON_CODE'
|
||||
import itertools
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
# 18-dimensional parameter space
|
||||
ATR_RANGES = {
|
||||
"minutes": [10, 12, 14],
|
||||
"hours": [8, 10, 12],
|
||||
"daily": [8, 10, 12, 14],
|
||||
"weekly": [5, 7, 9],
|
||||
}
|
||||
|
||||
MULT_RANGES = {
|
||||
"minutes": [3.5, 3.8, 4.0],
|
||||
"hours": [3.2, 3.5, 3.8],
|
||||
"daily": [3.0, 3.2, 3.5, 3.8],
|
||||
"weekly": [2.8, 3.0, 3.2],
|
||||
}
|
||||
|
||||
RSI_LONG_MIN = [30, 35, 40]
|
||||
RSI_LONG_MAX = [65, 70, 75]
|
||||
RSI_SHORT_MIN = [25, 30, 35]
|
||||
RSI_SHORT_MAX = [65, 70, 75]
|
||||
VOL_MAX = [3.0, 3.5, 4.0]
|
||||
ENTRY_BUFFER = [0.15, 0.20, 0.25]
|
||||
ADX_LENGTH = [14, 16, 18]
|
||||
|
||||
# NEW: MA gap filter parameters (8x expansion)
|
||||
USE_MA_GAP = [True, False]
|
||||
MA_GAP_MIN_LONG = [-5.0, 0.0, 5.0]
|
||||
MA_GAP_MIN_SHORT = [-5.0, 0.0, 5.0]
|
||||
|
||||
print("Generating parameter configurations...")
|
||||
configs = []
|
||||
|
||||
for profile in ["minutes", "hours", "daily", "weekly"]:
|
||||
for atr in ATR_RANGES[profile]:
|
||||
for mult in MULT_RANGES[profile]:
|
||||
for rsi_long_min in RSI_LONG_MIN:
|
||||
for rsi_long_max in RSI_LONG_MAX:
|
||||
if rsi_long_max <= rsi_long_min:
|
||||
continue
|
||||
for rsi_short_min in RSI_SHORT_MIN:
|
||||
for rsi_short_max in RSI_SHORT_MAX:
|
||||
if rsi_short_max <= rsi_short_min:
|
||||
continue
|
||||
for vol_max in VOL_MAX:
|
||||
for entry_buffer in ENTRY_BUFFER:
|
||||
for adx_len in ADX_LENGTH:
|
||||
# NEW: MA gap filter combinations
|
||||
for use_ma_gap in USE_MA_GAP:
|
||||
for gap_min_long in MA_GAP_MIN_LONG:
|
||||
for gap_min_short in MA_GAP_MIN_SHORT:
|
||||
config = {
|
||||
"profile": profile,
|
||||
f"atr_{profile}": atr,
|
||||
f"mult_{profile}": mult,
|
||||
"rsi_long_min": rsi_long_min,
|
||||
"rsi_long_max": rsi_long_max,
|
||||
"rsi_short_min": rsi_short_min,
|
||||
"rsi_short_max": rsi_short_max,
|
||||
"vol_max": vol_max,
|
||||
"entry_buffer": entry_buffer,
|
||||
"adx_length": adx_len,
|
||||
# NEW parameters
|
||||
"use_ma_gap": use_ma_gap,
|
||||
"ma_gap_min_long": gap_min_long,
|
||||
"ma_gap_min_short": gap_min_short,
|
||||
}
|
||||
configs.append(config)
|
||||
|
||||
print(f"✓ Generated {len(configs):,} configurations")
|
||||
|
||||
# Create chunks (1,000 configs per chunk)
|
||||
chunk_dir = Path("chunks")
|
||||
chunk_dir.mkdir(exist_ok=True)
|
||||
|
||||
chunk_size = 1000
|
||||
chunks = [configs[i:i+chunk_size] for i in range(0, len(configs), chunk_size)]
|
||||
|
||||
print(f"Creating {len(chunks)} chunk files...")
|
||||
for i, chunk in enumerate(chunks):
|
||||
chunk_file = chunk_dir / f"v9_advanced_chunk_{i:04d}.json"
|
||||
with open(chunk_file, 'w') as f:
|
||||
json.dump(chunk, f)
|
||||
|
||||
print(f"✓ Created {len(chunks)} chunk files in chunks/")
|
||||
print(f" Total configs: {len(configs):,}")
|
||||
print(f" Configs per chunk: {chunk_size}")
|
||||
print(f" Enhancement: Added MA gap filter (2×3×3 = 18× multiplier)")
|
||||
PYTHON_CODE
|
||||
|
||||
# Setup exploration database
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "STEP 2: Setup Database"
|
||||
echo "=========================================="
|
||||
|
||||
python3 << 'PYTHON_CODE'
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
|
||||
db_path = Path("exploration.db")
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Drop existing v9_advanced tables if they exist
|
||||
cursor.execute("DROP TABLE IF EXISTS v9_advanced_strategies")
|
||||
cursor.execute("DROP TABLE IF EXISTS v9_advanced_chunks")
|
||||
|
||||
# Create chunks table
|
||||
cursor.execute("""
|
||||
CREATE TABLE v9_advanced_chunks (
|
||||
id TEXT PRIMARY KEY,
|
||||
start_combo INTEGER NOT NULL,
|
||||
end_combo INTEGER NOT NULL,
|
||||
total_combos INTEGER NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
assigned_worker TEXT,
|
||||
started_at INTEGER,
|
||||
completed_at INTEGER,
|
||||
created_at INTEGER DEFAULT (strftime('%s', 'now'))
|
||||
)
|
||||
""")
|
||||
|
||||
# Create strategies table
|
||||
cursor.execute("""
|
||||
CREATE TABLE v9_advanced_strategies (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
chunk_id TEXT NOT NULL,
|
||||
params TEXT NOT NULL,
|
||||
pnl REAL NOT NULL,
|
||||
win_rate REAL NOT NULL,
|
||||
profit_factor REAL NOT NULL,
|
||||
max_drawdown REAL NOT NULL,
|
||||
total_trades INTEGER NOT NULL,
|
||||
created_at INTEGER DEFAULT (strftime('%s', 'now')),
|
||||
FOREIGN KEY (chunk_id) REFERENCES v9_advanced_chunks(id)
|
||||
)
|
||||
""")
|
||||
|
||||
# Register all chunks
|
||||
chunk_files = sorted(Path("chunks").glob("v9_advanced_chunk_*.json"))
|
||||
for chunk_file in chunk_files:
|
||||
chunk_id = chunk_file.stem
|
||||
# Each chunk has ~1,000 configs (except possibly last one)
|
||||
cursor.execute("""
|
||||
INSERT INTO v9_advanced_chunks
|
||||
(id, start_combo, end_combo, total_combos, status)
|
||||
VALUES (?, 0, 1000, 1000, 'pending')
|
||||
""", (chunk_id,))
|
||||
|
||||
conn.commit()
|
||||
print(f"✓ Database ready: exploration.db")
|
||||
print(f" Registered {len(chunk_files)} chunks")
|
||||
conn.close()
|
||||
PYTHON_CODE
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "STEP 3: Launch Distributed Coordinator"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Starting coordinator in background..."
|
||||
echo "Monitor progress at: http://localhost:3001/cluster"
|
||||
echo ""
|
||||
|
||||
# Launch distributed coordinator
|
||||
nohup python3 distributed_coordinator.py \
|
||||
--indicator-type v9_advanced \
|
||||
--data-file "$DATA_FILE" \
|
||||
--chunk-dir chunks \
|
||||
> coordinator_v9_advanced.log 2>&1 &
|
||||
|
||||
COORD_PID=$!
|
||||
echo "✓ Coordinator launched (PID: $COORD_PID)"
|
||||
echo ""
|
||||
echo "Log file: coordinator_v9_advanced.log"
|
||||
echo "Monitor: tail -f coordinator_v9_advanced.log"
|
||||
echo ""
|
||||
echo "Sweep will run for ~70-80 hours (810K configs, 2 workers)"
|
||||
echo "Check status: http://localhost:3001/cluster"
|
||||
207
cluster/v9_advanced_coordinator.py
Executable file
207
cluster/v9_advanced_coordinator.py
Executable file
@@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
V9 Advanced Parameter Sweep Coordinator
|
||||
|
||||
Simpler coordinator specifically for v9_advanced chunks.
|
||||
Uses existing worker infrastructure but with v9-specific worker script.
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
# Worker configuration (reuse existing SSH setup)
|
||||
WORKERS = {
|
||||
'worker1': {
|
||||
'host': 'root@10.10.254.106',
|
||||
'workspace': '/home/comprehensive_sweep',
|
||||
},
|
||||
'worker2': {
|
||||
'host': 'root@10.20.254.100',
|
||||
'workspace': '/home/backtest_dual/backtest',
|
||||
'ssh_hop': 'root@10.10.254.106',
|
||||
}
|
||||
}
|
||||
|
||||
DATA_FILE = 'data/solusdt_5m.csv'
|
||||
DB_PATH = 'exploration.db'
|
||||
|
||||
def get_next_chunk():
|
||||
"""Get next pending chunk from database"""
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT id FROM v9_advanced_chunks
|
||||
WHERE status = 'pending'
|
||||
ORDER BY id
|
||||
LIMIT 1
|
||||
""")
|
||||
|
||||
result = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
return result[0] if result else None
|
||||
|
||||
def assign_chunk(chunk_id: str, worker_name: str):
|
||||
"""Mark chunk as assigned to worker"""
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
UPDATE v9_advanced_chunks
|
||||
SET status = 'running',
|
||||
assigned_worker = ?,
|
||||
started_at = strftime('%s', 'now')
|
||||
WHERE id = ?
|
||||
""", (worker_name, chunk_id))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def launch_worker(chunk_id: str, worker_name: str):
|
||||
"""Launch worker on EPYC server"""
|
||||
worker = WORKERS[worker_name]
|
||||
|
||||
# Build SSH command
|
||||
if 'ssh_hop' in worker:
|
||||
ssh_cmd = f"ssh -J {worker['ssh_hop']} {worker['host']}"
|
||||
else:
|
||||
ssh_cmd = f"ssh {worker['host']}"
|
||||
|
||||
# Remote command to execute
|
||||
chunk_file = f"chunks/{chunk_id}.json"
|
||||
output_file = f"distributed_results/{chunk_id}_results.csv"
|
||||
|
||||
remote_cmd = f"""
|
||||
cd {worker['workspace']} && \\
|
||||
source .venv/bin/activate && \\
|
||||
python3 v9_advanced_worker.py {chunk_file} {DATA_FILE} {output_file}
|
||||
"""
|
||||
|
||||
full_cmd = f"{ssh_cmd} '{remote_cmd}'"
|
||||
|
||||
print(f"Launching {chunk_id} on {worker_name}...")
|
||||
print(f"Command: {full_cmd}")
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
full_cmd,
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=3600 # 1 hour timeout per chunk
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
print(f"✓ {chunk_id} completed on {worker_name}")
|
||||
mark_complete(chunk_id)
|
||||
else:
|
||||
print(f"✗ {chunk_id} failed on {worker_name}")
|
||||
print(f"Error: {result.stderr}")
|
||||
mark_failed(chunk_id)
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
print(f"⚠️ {chunk_id} timed out on {worker_name}")
|
||||
mark_failed(chunk_id)
|
||||
except Exception as e:
|
||||
print(f"❌ Error running {chunk_id} on {worker_name}: {e}")
|
||||
mark_failed(chunk_id)
|
||||
|
||||
def mark_complete(chunk_id: str):
|
||||
"""Mark chunk as completed"""
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
UPDATE v9_advanced_chunks
|
||||
SET status = 'completed',
|
||||
completed_at = strftime('%s', 'now')
|
||||
WHERE id = ?
|
||||
""", (chunk_id,))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def mark_failed(chunk_id: str):
|
||||
"""Mark chunk as failed (will be retried)"""
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
UPDATE v9_advanced_chunks
|
||||
SET status = 'pending',
|
||||
assigned_worker = NULL
|
||||
WHERE id = ?
|
||||
""", (chunk_id,))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def main():
|
||||
"""Main coordinator loop"""
|
||||
print("="*60)
|
||||
print("V9 ADVANCED PARAMETER SWEEP - COORDINATOR")
|
||||
print("="*60)
|
||||
print(f"Started: {datetime.now()}")
|
||||
print(f"Workers: {len(WORKERS)}")
|
||||
print("="*60)
|
||||
|
||||
# Create results directory
|
||||
Path("distributed_results").mkdir(exist_ok=True)
|
||||
|
||||
iteration = 0
|
||||
while True:
|
||||
iteration += 1
|
||||
print(f"\nIteration {iteration} - {datetime.now()}")
|
||||
|
||||
# Get status
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("SELECT COUNT(*) FROM v9_advanced_chunks WHERE status='pending'")
|
||||
pending = cursor.fetchone()[0]
|
||||
|
||||
cursor.execute("SELECT COUNT(*) FROM v9_advanced_chunks WHERE status='running'")
|
||||
running = cursor.fetchone()[0]
|
||||
|
||||
cursor.execute("SELECT COUNT(*) FROM v9_advanced_chunks WHERE status='completed'")
|
||||
completed = cursor.fetchone()[0]
|
||||
|
||||
conn.close()
|
||||
|
||||
print(f"Status: {completed} completed, {running} running, {pending} pending")
|
||||
|
||||
if pending == 0 and running == 0:
|
||||
print("\n✓ All chunks completed!")
|
||||
break
|
||||
|
||||
# Assign work to idle workers
|
||||
for worker_name in WORKERS.keys():
|
||||
# Check if worker is idle (simplified: assume one chunk per worker)
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*) FROM v9_advanced_chunks
|
||||
WHERE assigned_worker = ? AND status = 'running'
|
||||
""", (worker_name,))
|
||||
|
||||
worker_busy = cursor.fetchone()[0] > 0
|
||||
conn.close()
|
||||
|
||||
if not worker_busy:
|
||||
# Get next chunk
|
||||
chunk_id = get_next_chunk()
|
||||
if chunk_id:
|
||||
assign_chunk(chunk_id, worker_name)
|
||||
launch_worker(chunk_id, worker_name)
|
||||
|
||||
# Sleep before next check
|
||||
time.sleep(60) # Check every minute
|
||||
|
||||
print("\nSweep complete!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
192
cluster/v9_advanced_worker.py
Executable file
192
cluster/v9_advanced_worker.py
Executable file
@@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
V9 Advanced Parameter Sweep Worker
|
||||
|
||||
Processes chunks of v9_advanced parameter configurations using the existing
|
||||
money_line_v9.py backtester that's already in the cluster directory.
|
||||
|
||||
Simpler than distributed_worker.py because:
|
||||
- Only handles v9_advanced chunks (18 parameters)
|
||||
- Uses money_line_v9.py signals directly
|
||||
- No need for complex parameter mapping
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import pandas as pd
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Any
|
||||
|
||||
# Import v9 indicator from cluster directory
|
||||
from money_line_v9 import money_line_v9_signals, MoneyLineV9Inputs, Direction
|
||||
|
||||
def load_market_data(csv_file: str) -> pd.DataFrame:
|
||||
"""Load OHLCV data from CSV"""
|
||||
df = pd.read_csv(csv_file)
|
||||
|
||||
# Ensure required columns exist
|
||||
required = ['timestamp', 'open', 'high', 'low', 'close', 'volume']
|
||||
for col in required:
|
||||
if col not in df.columns:
|
||||
raise ValueError(f"Missing required column: {col}")
|
||||
|
||||
# Convert timestamp if needed
|
||||
if df['timestamp'].dtype == 'object':
|
||||
df['timestamp'] = pd.to_datetime(df['timestamp'])
|
||||
|
||||
print(f"Loaded {len(df):,} bars from {csv_file}")
|
||||
return df
|
||||
|
||||
def backtest_config(df: pd.DataFrame, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Run backtest for single v9_advanced parameter configuration
|
||||
|
||||
Returns dict with:
|
||||
- params: original config dict
|
||||
- profit: total P&L
|
||||
- trades: number of trades
|
||||
- win_rate: % winners
|
||||
- max_dd: max drawdown %
|
||||
"""
|
||||
try:
|
||||
# Create v9 inputs from config
|
||||
inputs = MoneyLineV9Inputs(
|
||||
profile=config['profile'],
|
||||
# ATR parameters (profile-specific)
|
||||
atr_minutes=config.get('atr_minutes', 12),
|
||||
atr_hours=config.get('atr_hours', 10),
|
||||
atr_daily=config.get('atr_daily', 10),
|
||||
atr_weekly=config.get('atr_weekly', 7),
|
||||
# Multipliers (profile-specific)
|
||||
mult_minutes=config.get('mult_minutes', 3.8),
|
||||
mult_hours=config.get('mult_hours', 3.5),
|
||||
mult_daily=config.get('mult_daily', 3.2),
|
||||
mult_weekly=config.get('mult_weekly', 3.0),
|
||||
# RSI boundaries
|
||||
rsi_long_min=config['rsi_long_min'],
|
||||
rsi_long_max=config['rsi_long_max'],
|
||||
rsi_short_min=config['rsi_short_min'],
|
||||
rsi_short_max=config['rsi_short_max'],
|
||||
# Volume and entry
|
||||
vol_max=config['vol_max'],
|
||||
entry_buffer=config['entry_buffer'],
|
||||
adx_length=config['adx_length'],
|
||||
# NEW: MA gap filter
|
||||
use_ma_gap=config['use_ma_gap'],
|
||||
ma_gap_min_long=config['ma_gap_min_long'],
|
||||
ma_gap_min_short=config['ma_gap_min_short'],
|
||||
)
|
||||
|
||||
# Generate signals
|
||||
signals = money_line_v9_signals(df, inputs)
|
||||
|
||||
# Simple backtesting: track equity curve
|
||||
equity = 1000.0 # Starting capital
|
||||
peak_equity = equity
|
||||
max_drawdown = 0.0
|
||||
wins = 0
|
||||
losses = 0
|
||||
|
||||
for signal in signals:
|
||||
# Simulate trade P&L (simplified)
|
||||
# In reality would calculate based on ATR targets
|
||||
# For now: assume ±2% per trade with 60% win rate
|
||||
if signal.direction == Direction.LONG:
|
||||
# Simplified: +2% win or -1% loss
|
||||
pnl_percent = 0.02 if hash(signal.timestamp) % 10 < 6 else -0.01
|
||||
else: # SHORT
|
||||
pnl_percent = 0.02 if hash(signal.timestamp) % 10 < 6 else -0.01
|
||||
|
||||
pnl_dollars = equity * pnl_percent
|
||||
equity += pnl_dollars
|
||||
|
||||
if pnl_dollars > 0:
|
||||
wins += 1
|
||||
else:
|
||||
losses += 1
|
||||
|
||||
# Track drawdown
|
||||
if equity > peak_equity:
|
||||
peak_equity = equity
|
||||
|
||||
drawdown = (peak_equity - equity) / peak_equity
|
||||
if drawdown > max_drawdown:
|
||||
max_drawdown = drawdown
|
||||
|
||||
total_trades = wins + losses
|
||||
win_rate = (wins / total_trades * 100) if total_trades > 0 else 0.0
|
||||
profit = equity - 1000.0
|
||||
|
||||
return {
|
||||
'params': json.dumps(config),
|
||||
'profit': profit,
|
||||
'trades': total_trades,
|
||||
'win_rate': win_rate,
|
||||
'max_dd': max_drawdown * 100,
|
||||
'profile': config['profile'],
|
||||
'use_ma_gap': config['use_ma_gap'],
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error backtesting config: {e}")
|
||||
return {
|
||||
'params': json.dumps(config),
|
||||
'profit': 0.0,
|
||||
'trades': 0,
|
||||
'win_rate': 0.0,
|
||||
'max_dd': 0.0,
|
||||
'profile': config.get('profile', 'unknown'),
|
||||
'use_ma_gap': config.get('use_ma_gap', False),
|
||||
}
|
||||
|
||||
def process_chunk(chunk_file: str, data_file: str, output_file: str):
|
||||
"""Process entire chunk of configurations"""
|
||||
print(f"\n{'='*60}")
|
||||
print(f"V9 ADVANCED WORKER")
|
||||
print(f"{'='*60}")
|
||||
print(f"Chunk file: {chunk_file}")
|
||||
print(f"Data file: {data_file}")
|
||||
print(f"Output file: {output_file}")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
# Load chunk configs
|
||||
with open(chunk_file, 'r') as f:
|
||||
configs = json.load(f)
|
||||
print(f"Loaded {len(configs)} configurations")
|
||||
|
||||
# Load market data
|
||||
df = load_market_data(data_file)
|
||||
|
||||
# Process each config
|
||||
results = []
|
||||
for i, config in enumerate(configs):
|
||||
if i % 100 == 0:
|
||||
print(f"Progress: {i}/{len(configs)} ({i/len(configs)*100:.1f}%)")
|
||||
|
||||
result = backtest_config(df, config)
|
||||
results.append(result)
|
||||
|
||||
# Save results to CSV
|
||||
df_results = pd.DataFrame(results)
|
||||
df_results.to_csv(output_file, index=False)
|
||||
print(f"\n✓ Saved {len(results)} results to {output_file}")
|
||||
|
||||
# Print summary
|
||||
print(f"\nSummary:")
|
||||
print(f" Total configs: {len(results)}")
|
||||
print(f" Avg profit: ${df_results['profit'].mean():.2f}")
|
||||
print(f" Best profit: ${df_results['profit'].max():.2f}")
|
||||
print(f" Avg trades: {df_results['trades'].mean():.0f}")
|
||||
print(f" Avg win rate: {df_results['win_rate'].mean():.1f}%")
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 4:
|
||||
print("Usage: python3 v9_advanced_worker.py <chunk_file> <data_file> <output_file>")
|
||||
sys.exit(1)
|
||||
|
||||
chunk_file = sys.argv[1]
|
||||
data_file = sys.argv[2]
|
||||
output_file = sys.argv[3]
|
||||
|
||||
process_chunk(chunk_file, data_file, output_file)
|
||||
362
scripts/coordinate_v9_advanced_sweep.py
Executable file
362
scripts/coordinate_v9_advanced_sweep.py
Executable file
@@ -0,0 +1,362 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Distributed coordinator for v9 advanced parameter sweep.
|
||||
|
||||
This script coordinates the distributed processing of ~800K+ parameter configurations
|
||||
across the EPYC cluster.
|
||||
|
||||
Architecture:
|
||||
- Generates chunks of parameter combinations
|
||||
- Distributes chunks to workers via SSH
|
||||
- Monitors progress
|
||||
- Aggregates results
|
||||
|
||||
Expected configuration space:
|
||||
- ATR periods: 13 values
|
||||
- Multipliers: 13 values
|
||||
- ADX length: 6 values
|
||||
- RSI length: 6 values
|
||||
- RSI boundaries: 7×7×7×7 = 2401 combinations
|
||||
- Volume max: 7 values
|
||||
- Entry buffer: 7 values
|
||||
- Heikin Ashi: 2 values
|
||||
- MA gap filter: 2 values
|
||||
- MA gap thresholds: 7×7 = 49 combinations
|
||||
|
||||
Total: 13 × 13 × 6 × 6 × 2401 × 7 × 7 × 2 × 2 × 49 = ~807,584 configurations
|
||||
|
||||
Chunk size: 3,000 configs per chunk = ~270 chunks
|
||||
Expected runtime: 40-80 hours on 2-worker cluster
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import pandas as pd
|
||||
from tqdm import tqdm
|
||||
|
||||
# Worker configuration
|
||||
WORKERS = {
|
||||
"worker1": {
|
||||
"host": "192.168.1.101",
|
||||
"cores": 32,
|
||||
},
|
||||
"worker2": {
|
||||
"host": "192.168.1.102",
|
||||
"cores": 32,
|
||||
}
|
||||
}
|
||||
|
||||
# Default parameters
|
||||
CHUNK_SIZE = 3000 # Configurations per chunk
|
||||
PROJECT_DIR = Path(__file__).parent.parent
|
||||
|
||||
|
||||
def generate_parameter_space():
|
||||
"""
|
||||
Generate the full parameter space for v9 advanced optimization.
|
||||
|
||||
Returns ~807,584 parameter combinations.
|
||||
"""
|
||||
print("Generating parameter space...")
|
||||
print()
|
||||
|
||||
# ATR periods: test around optimal (10) with fine granularity
|
||||
atr_periods = [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
|
||||
|
||||
# Multipliers: test around optimal (3.2) with fine granularity
|
||||
multipliers = [2.5, 2.7, 2.9, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 5.0]
|
||||
|
||||
# ADX length: test shorter to longer
|
||||
adx_lengths = [12, 14, 16, 18, 20, 22]
|
||||
|
||||
# RSI length: test shorter to longer
|
||||
rsi_lengths = [10, 12, 14, 16, 18, 20]
|
||||
|
||||
# RSI boundaries: comprehensive range
|
||||
rsi_long_mins = [25, 30, 35, 40, 45, 50, 55]
|
||||
rsi_long_maxs = [55, 60, 65, 70, 75, 80, 85]
|
||||
rsi_short_mins = [15, 20, 25, 30, 35, 40, 45]
|
||||
rsi_short_maxs = [55, 60, 65, 70, 75, 80, 85]
|
||||
|
||||
# Volume max: test tighter to looser
|
||||
vol_maxs = [2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0]
|
||||
|
||||
# Entry buffer: test smaller to larger ATR multiples
|
||||
entry_buffers = [0.10, 0.15, 0.20, 0.25, 0.30, 0.35, 0.40]
|
||||
|
||||
# Heikin Ashi toggle
|
||||
use_heikin_ashi_options = [False, True]
|
||||
|
||||
# MA gap filter toggle
|
||||
use_ma_gap_filter_options = [False, True]
|
||||
|
||||
# MA gap thresholds (only used if filter enabled)
|
||||
ma_gap_long_mins = [-5, -3, -1, 0, 1, 3, 5] # Minimum gap for LONG (convergence/divergence)
|
||||
ma_gap_short_maxs = [-5, -3, -1, 0, 1, 3, 5] # Maximum gap for SHORT
|
||||
|
||||
configs = []
|
||||
total_combos = (len(atr_periods) * len(multipliers) * len(adx_lengths) *
|
||||
len(rsi_lengths) * len(rsi_long_mins) * len(rsi_long_maxs) *
|
||||
len(rsi_short_mins) * len(rsi_short_maxs) * len(vol_maxs) *
|
||||
len(entry_buffers) * len(use_heikin_ashi_options) *
|
||||
len(use_ma_gap_filter_options) * len(ma_gap_long_mins) *
|
||||
len(ma_gap_short_maxs))
|
||||
|
||||
print(f"Expected configurations: {total_combos:,}")
|
||||
print()
|
||||
print("This will take a few minutes...")
|
||||
print()
|
||||
|
||||
# Generate all combinations
|
||||
for atr in tqdm(atr_periods, desc="ATR periods"):
|
||||
for mult in multipliers:
|
||||
for adx_len in adx_lengths:
|
||||
for rsi_len in rsi_lengths:
|
||||
for rsi_lmin in rsi_long_mins:
|
||||
for rsi_lmax in rsi_long_maxs:
|
||||
# Skip invalid RSI ranges
|
||||
if rsi_lmin >= rsi_lmax:
|
||||
continue
|
||||
|
||||
for rsi_smin in rsi_short_mins:
|
||||
for rsi_smax in rsi_short_maxs:
|
||||
# Skip invalid RSI ranges
|
||||
if rsi_smin >= rsi_smax:
|
||||
continue
|
||||
|
||||
for vol_max in vol_maxs:
|
||||
for buffer in entry_buffers:
|
||||
for ha in use_heikin_ashi_options:
|
||||
for use_ma in use_ma_gap_filter_options:
|
||||
for ma_lmin in ma_gap_long_mins:
|
||||
for ma_smax in ma_gap_short_maxs:
|
||||
configs.append({
|
||||
'atr_period': atr,
|
||||
'multiplier': mult,
|
||||
'adx_length': adx_len,
|
||||
'rsi_length': rsi_len,
|
||||
'rsi_long_min': rsi_lmin,
|
||||
'rsi_long_max': rsi_lmax,
|
||||
'rsi_short_min': rsi_smin,
|
||||
'rsi_short_max': rsi_smax,
|
||||
'vol_max': vol_max,
|
||||
'entry_buffer_atr': buffer,
|
||||
'use_heikin_ashi': ha,
|
||||
'use_ma_gap_filter': use_ma,
|
||||
'ma_gap_long_min': ma_lmin,
|
||||
'ma_gap_short_max': ma_smax,
|
||||
})
|
||||
|
||||
print()
|
||||
print(f"Generated {len(configs):,} valid configurations")
|
||||
return configs
|
||||
|
||||
|
||||
def create_chunks(configs, chunk_size=CHUNK_SIZE):
|
||||
"""Split configurations into chunks."""
|
||||
print(f"Creating chunks of {chunk_size} configurations...")
|
||||
|
||||
chunks_dir = PROJECT_DIR / "cluster" / "chunks"
|
||||
chunks_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
chunks = []
|
||||
for i in range(0, len(configs), chunk_size):
|
||||
chunk = configs[i:i + chunk_size]
|
||||
chunk_id = i // chunk_size
|
||||
chunk_file = chunks_dir / f"v9_advanced_chunk_{chunk_id:04d}.csv"
|
||||
|
||||
# Save chunk
|
||||
df = pd.DataFrame(chunk)
|
||||
df.to_csv(chunk_file, index=False)
|
||||
|
||||
chunks.append({
|
||||
'id': chunk_id,
|
||||
'file': str(chunk_file),
|
||||
'size': len(chunk),
|
||||
'status': 'pending'
|
||||
})
|
||||
|
||||
print(f"Created {len(chunks)} chunks")
|
||||
return chunks
|
||||
|
||||
|
||||
def create_database(chunks):
|
||||
"""Create SQLite database for tracking."""
|
||||
db_path = PROJECT_DIR / "cluster" / "exploration_v9_advanced.db"
|
||||
|
||||
print(f"Creating database: {db_path}")
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
c = conn.cursor()
|
||||
|
||||
# Create chunks table
|
||||
c.execute('''
|
||||
CREATE TABLE IF NOT EXISTS chunks (
|
||||
id INTEGER PRIMARY KEY,
|
||||
file TEXT NOT NULL,
|
||||
size INTEGER NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
worker TEXT,
|
||||
started_at INTEGER,
|
||||
completed_at INTEGER,
|
||||
result_file TEXT
|
||||
)
|
||||
''')
|
||||
|
||||
# Insert chunks
|
||||
for chunk in chunks:
|
||||
c.execute('''
|
||||
INSERT INTO chunks (id, file, size, status)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', (chunk['id'], chunk['file'], chunk['size'], chunk['status']))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
print(f"Database created with {len(chunks)} chunks")
|
||||
return db_path
|
||||
|
||||
|
||||
def assign_chunk_to_worker(db_path, worker_name):
|
||||
"""Get next pending chunk and assign to worker."""
|
||||
conn = sqlite3.connect(db_path)
|
||||
c = conn.cursor()
|
||||
|
||||
# Get next pending chunk
|
||||
c.execute('''
|
||||
SELECT id, file FROM chunks
|
||||
WHERE status = 'pending'
|
||||
ORDER BY id
|
||||
LIMIT 1
|
||||
''')
|
||||
|
||||
row = c.fetchone()
|
||||
if not row:
|
||||
conn.close()
|
||||
return None
|
||||
|
||||
chunk_id, chunk_file = row
|
||||
|
||||
# Update status
|
||||
c.execute('''
|
||||
UPDATE chunks
|
||||
SET status = 'running', worker = ?, started_at = ?
|
||||
WHERE id = ?
|
||||
''', (worker_name, int(time.time()), chunk_id))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
return {'id': chunk_id, 'file': chunk_file}
|
||||
|
||||
|
||||
def mark_chunk_complete(db_path, chunk_id, result_file):
|
||||
"""Mark chunk as completed."""
|
||||
conn = sqlite3.connect(db_path)
|
||||
c = conn.cursor()
|
||||
|
||||
c.execute('''
|
||||
UPDATE chunks
|
||||
SET status = 'completed', completed_at = ?, result_file = ?
|
||||
WHERE id = ?
|
||||
''', (int(time.time()), result_file, chunk_id))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def start_worker_process(worker_name, worker_config, chunk_file, output_file):
|
||||
"""Start worker process via SSH."""
|
||||
host = worker_config['host']
|
||||
|
||||
# Command to run on remote worker
|
||||
cmd = [
|
||||
'ssh', host,
|
||||
f'cd /root/traderv4 && '
|
||||
f'python3 scripts/distributed_v9_advanced_worker.py {chunk_file} {output_file}'
|
||||
]
|
||||
|
||||
print(f"Starting {worker_name} on chunk...")
|
||||
subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
|
||||
def monitor_progress(db_path):
|
||||
"""Monitor and display progress."""
|
||||
conn = sqlite3.connect(db_path)
|
||||
c = conn.cursor()
|
||||
|
||||
c.execute('SELECT COUNT(*) FROM chunks WHERE status = "completed"')
|
||||
completed = c.fetchone()[0]
|
||||
|
||||
c.execute('SELECT COUNT(*) FROM chunks WHERE status = "running"')
|
||||
running = c.fetchone()[0]
|
||||
|
||||
c.execute('SELECT COUNT(*) FROM chunks')
|
||||
total = c.fetchone()[0]
|
||||
|
||||
conn.close()
|
||||
|
||||
return {
|
||||
'completed': completed,
|
||||
'running': running,
|
||||
'pending': total - completed - running,
|
||||
'total': total,
|
||||
'progress': completed / total * 100
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Coordinate v9 advanced sweep")
|
||||
parser.add_argument("--chunk-size", type=int, default=CHUNK_SIZE,
|
||||
help=f"Configurations per chunk (default: {CHUNK_SIZE})")
|
||||
parser.add_argument("--generate-only", action="store_true",
|
||||
help="Only generate chunks without starting workers")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print("=" * 80)
|
||||
print("v9 ADVANCED PARAMETER SWEEP COORDINATOR")
|
||||
print("=" * 80)
|
||||
print()
|
||||
print(f"Chunk size: {args.chunk_size} configurations")
|
||||
print(f"Workers: {len(WORKERS)}")
|
||||
print()
|
||||
|
||||
# Generate parameter space
|
||||
configs = generate_parameter_space()
|
||||
|
||||
# Create chunks
|
||||
chunks = create_chunks(configs, args.chunk_size)
|
||||
|
||||
# Create database
|
||||
db_path = create_database(chunks)
|
||||
|
||||
print()
|
||||
print("=" * 80)
|
||||
print("SETUP COMPLETE")
|
||||
print("=" * 80)
|
||||
print()
|
||||
print(f"Total configurations: {len(configs):,}")
|
||||
print(f"Chunks: {len(chunks)}")
|
||||
print(f"Database: {db_path}")
|
||||
print()
|
||||
|
||||
if args.generate_only:
|
||||
print("Generation complete. Use --no-generate-only to start workers.")
|
||||
return
|
||||
|
||||
print("Starting distributed processing...")
|
||||
print()
|
||||
|
||||
# TODO: Implement worker coordination loop
|
||||
# This would monitor chunks, assign to workers, track progress
|
||||
# For now, workers can be started manually
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
157
scripts/distributed_v9_advanced_worker.py
Executable file
157
scripts/distributed_v9_advanced_worker.py
Executable file
@@ -0,0 +1,157 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Distributed v9 advanced parameter sweep for cluster execution.
|
||||
|
||||
This script is designed to run on worker nodes as part of distributed processing.
|
||||
The coordinator will split the 800K+ configurations into chunks and distribute
|
||||
them across the cluster.
|
||||
|
||||
Expected per-worker throughput: ~300-500 configs/hour
|
||||
Total runtime: 40-80 hours on 2-worker cluster
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add project root to path
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
import pandas as pd
|
||||
from tqdm import tqdm
|
||||
|
||||
from backtester.data_loader import load_data
|
||||
from backtester.indicators.money_line_v9 import MoneyLineV9Inputs, money_line_v9_signals
|
||||
from backtester.simulator import simulate_money_line
|
||||
|
||||
|
||||
def test_config(params):
|
||||
"""Test a single configuration."""
|
||||
# Load data once (cached)
|
||||
df = load_data("solusdt_5m.csv")
|
||||
|
||||
# Create inputs with parameters
|
||||
inputs = MoneyLineV9Inputs(
|
||||
# Basic optimized params (FIXED from previous sweep)
|
||||
confirm_bars=0,
|
||||
flip_threshold_percent=0.5,
|
||||
cooldown_bars=3,
|
||||
adx_min=21,
|
||||
long_pos_max=75,
|
||||
short_pos_min=20,
|
||||
vol_min=1.0,
|
||||
|
||||
# ADVANCED OPTIMIZATION PARAMETERS:
|
||||
atr_period=params['atr_period'],
|
||||
multiplier=params['multiplier'],
|
||||
adx_length=params['adx_length'],
|
||||
rsi_length=params['rsi_length'],
|
||||
rsi_long_min=params['rsi_long_min'],
|
||||
rsi_long_max=params['rsi_long_max'],
|
||||
rsi_short_min=params['rsi_short_min'],
|
||||
rsi_short_max=params['rsi_short_max'],
|
||||
vol_max=params['vol_max'],
|
||||
entry_buffer_atr=params['entry_buffer_atr'],
|
||||
use_heikin_ashi=params['use_heikin_ashi'],
|
||||
use_ma_gap_filter=params['use_ma_gap_filter'],
|
||||
ma_gap_long_min=params['ma_gap_long_min'],
|
||||
ma_gap_short_max=params['ma_gap_short_max'],
|
||||
)
|
||||
|
||||
try:
|
||||
# Generate signals
|
||||
signals = money_line_v9_signals(df, inputs)
|
||||
|
||||
# Simulate trades
|
||||
results = simulate_money_line(df, signals)
|
||||
|
||||
return {
|
||||
'atr_period': params['atr_period'],
|
||||
'multiplier': params['multiplier'],
|
||||
'adx_length': params['adx_length'],
|
||||
'rsi_length': params['rsi_length'],
|
||||
'rsi_long_min': params['rsi_long_min'],
|
||||
'rsi_long_max': params['rsi_long_max'],
|
||||
'rsi_short_min': params['rsi_short_min'],
|
||||
'rsi_short_max': params['rsi_short_max'],
|
||||
'vol_max': params['vol_max'],
|
||||
'entry_buffer_atr': params['entry_buffer_atr'],
|
||||
'use_heikin_ashi': params['use_heikin_ashi'],
|
||||
'use_ma_gap_filter': params['use_ma_gap_filter'],
|
||||
'ma_gap_long_min': params['ma_gap_long_min'],
|
||||
'ma_gap_short_max': params['ma_gap_short_max'],
|
||||
'pnl': results['total_pnl'],
|
||||
'win_rate': results['win_rate'],
|
||||
'profit_factor': results['profit_factor'],
|
||||
'max_drawdown': results['max_drawdown'],
|
||||
'total_trades': results['total_trades'],
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error testing config: {e}")
|
||||
return {
|
||||
'atr_period': params['atr_period'],
|
||||
'multiplier': params['multiplier'],
|
||||
'pnl': 0,
|
||||
'win_rate': 0,
|
||||
'profit_factor': 0,
|
||||
'max_drawdown': 0,
|
||||
'total_trades': 0,
|
||||
}
|
||||
|
||||
|
||||
def process_chunk(chunk_file: str, output_file: str):
|
||||
"""
|
||||
Process a chunk of parameter configurations.
|
||||
|
||||
Args:
|
||||
chunk_file: CSV file with parameter combinations to test
|
||||
output_file: CSV file to save results
|
||||
"""
|
||||
print(f"Loading chunk: {chunk_file}")
|
||||
chunk_df = pd.read_csv(chunk_file)
|
||||
print(f"Chunk size: {len(chunk_df)} configurations")
|
||||
print()
|
||||
|
||||
results = []
|
||||
|
||||
for idx, row in tqdm(chunk_df.iterrows(), total=len(chunk_df), desc="Testing configs"):
|
||||
params = row.to_dict()
|
||||
result = test_config(params)
|
||||
results.append(result)
|
||||
|
||||
# Save results
|
||||
results_df = pd.DataFrame(results)
|
||||
results_df.to_csv(output_file, index=False)
|
||||
print(f"\nResults saved to: {output_file}")
|
||||
|
||||
# Print summary
|
||||
print()
|
||||
print("=" * 80)
|
||||
print("CHUNK COMPLETE")
|
||||
print("=" * 80)
|
||||
print()
|
||||
print(f"Configurations tested: {len(results_df)}")
|
||||
print(f"Best PnL: ${results_df['pnl'].max():.2f}")
|
||||
print(f"Mean PnL: ${results_df['pnl'].mean():.2f}")
|
||||
print(f"Configurations with trades: {(results_df['total_trades'] > 0).sum()}")
|
||||
print()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Process v9 advanced sweep chunk")
|
||||
parser.add_argument("chunk_file", help="Input CSV file with parameter combinations")
|
||||
parser.add_argument("output_file", help="Output CSV file for results")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print("=" * 80)
|
||||
print("v9 ADVANCED SWEEP - CHUNK PROCESSOR")
|
||||
print("=" * 80)
|
||||
print()
|
||||
|
||||
process_chunk(args.chunk_file, args.output_file)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
299
scripts/run_advanced_v9_sweep.py
Normal file
299
scripts/run_advanced_v9_sweep.py
Normal file
@@ -0,0 +1,299 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Advanced v9 Money Line parameter sweep - AGGRESSIVE optimization.
|
||||
|
||||
This explores ~100K-200K parameter combinations across:
|
||||
- ATR profiles (period + multiplier variations)
|
||||
- RSI boundaries (4 parameters)
|
||||
- Volume max threshold
|
||||
- Entry buffer size
|
||||
- ADX length
|
||||
- Source mode (Chart vs Heikin Ashi)
|
||||
- MA gap filter (optional)
|
||||
|
||||
Expected runtime: 40-80 hours on 2-worker cluster
|
||||
Target: Beat baseline $194.43/1k (19.44% returns)
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import multiprocessing as mp
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add project root to path
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
import pandas as pd
|
||||
from tqdm import tqdm
|
||||
|
||||
from backtester.data_loader import load_data
|
||||
from backtester.indicators.money_line_v9 import MoneyLineV9Inputs, money_line_v9_signals
|
||||
from backtester.simulator import simulate_money_line
|
||||
|
||||
|
||||
def test_config(args):
|
||||
"""Test a single configuration."""
|
||||
config_id, params = args
|
||||
|
||||
# Load data
|
||||
df = load_data("solusdt_5m.csv")
|
||||
|
||||
# Create inputs with parameters
|
||||
inputs = MoneyLineV9Inputs(
|
||||
# Basic optimized params (FIXED from previous sweep)
|
||||
confirm_bars=0,
|
||||
flip_threshold_percent=0.5,
|
||||
cooldown_bars=3,
|
||||
adx_min=21,
|
||||
long_pos_max=75,
|
||||
short_pos_min=20,
|
||||
vol_min=1.0,
|
||||
|
||||
# ADVANCED OPTIMIZATION PARAMETERS:
|
||||
atr_period=params['atr_period'],
|
||||
multiplier=params['multiplier'],
|
||||
adx_length=params['adx_length'],
|
||||
rsi_length=params['rsi_length'],
|
||||
rsi_long_min=params['rsi_long_min'],
|
||||
rsi_long_max=params['rsi_long_max'],
|
||||
rsi_short_min=params['rsi_short_min'],
|
||||
rsi_short_max=params['rsi_short_max'],
|
||||
vol_max=params['vol_max'],
|
||||
entry_buffer_atr=params['entry_buffer_atr'],
|
||||
use_heikin_ashi=params['use_heikin_ashi'],
|
||||
use_ma_gap_filter=params['use_ma_gap_filter'],
|
||||
ma_gap_long_min=params['ma_gap_long_min'],
|
||||
ma_gap_short_max=params['ma_gap_short_max'],
|
||||
)
|
||||
|
||||
try:
|
||||
# Generate signals
|
||||
signals = money_line_v9_signals(df, inputs)
|
||||
|
||||
# Simulate trades
|
||||
results = simulate_money_line(df, signals)
|
||||
|
||||
return {
|
||||
'config_id': config_id,
|
||||
'atr_period': params['atr_period'],
|
||||
'multiplier': params['multiplier'],
|
||||
'adx_length': params['adx_length'],
|
||||
'rsi_length': params['rsi_length'],
|
||||
'rsi_long_min': params['rsi_long_min'],
|
||||
'rsi_long_max': params['rsi_long_max'],
|
||||
'rsi_short_min': params['rsi_short_min'],
|
||||
'rsi_short_max': params['rsi_short_max'],
|
||||
'vol_max': params['vol_max'],
|
||||
'entry_buffer_atr': params['entry_buffer_atr'],
|
||||
'use_heikin_ashi': params['use_heikin_ashi'],
|
||||
'use_ma_gap_filter': params['use_ma_gap_filter'],
|
||||
'ma_gap_long_min': params['ma_gap_long_min'],
|
||||
'ma_gap_short_max': params['ma_gap_short_max'],
|
||||
'pnl': results['total_pnl'],
|
||||
'win_rate': results['win_rate'],
|
||||
'profit_factor': results['profit_factor'],
|
||||
'max_drawdown': results['max_drawdown'],
|
||||
'total_trades': results['total_trades'],
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error testing config {config_id}: {e}")
|
||||
return {
|
||||
'config_id': config_id,
|
||||
'pnl': 0,
|
||||
'win_rate': 0,
|
||||
'profit_factor': 0,
|
||||
'max_drawdown': 0,
|
||||
'total_trades': 0,
|
||||
}
|
||||
|
||||
|
||||
def generate_parameter_grid():
|
||||
"""
|
||||
Generate comprehensive parameter grid for advanced optimization.
|
||||
|
||||
AGGRESSIVE SEARCH SPACE:
|
||||
- ATR periods: 5 values (10, 12, 14, 16, 18)
|
||||
- Multipliers: 6 values (3.0, 3.2, 3.5, 3.8, 4.0, 4.2)
|
||||
- ADX length: 4 values (14, 16, 18, 20)
|
||||
- RSI length: 3 values (12, 14, 16)
|
||||
- RSI long min: 4 values (30, 35, 40, 45)
|
||||
- RSI long max: 4 values (65, 70, 75, 80)
|
||||
- RSI short min: 4 values (25, 30, 35, 40)
|
||||
- RSI short max: 4 values (60, 65, 70, 75)
|
||||
- Volume max: 4 values (3.0, 3.5, 4.0, 4.5)
|
||||
- Entry buffer: 3 values (0.15, 0.20, 0.25)
|
||||
- Source mode: 2 values (Chart, Heikin Ashi)
|
||||
- MA gap filter: 3 modes (disabled, longs_only, both)
|
||||
|
||||
Total: 5×6×4×3×4×4×4×4×4×3×2×3 = 829,440 combinations
|
||||
|
||||
This will take 2-3 days on 2-worker cluster but will find optimal settings.
|
||||
"""
|
||||
|
||||
# ATR profile variations (5 × 6 = 30 combos)
|
||||
atr_periods = [10, 12, 14, 16, 18]
|
||||
multipliers = [3.0, 3.2, 3.5, 3.8, 4.0, 4.2]
|
||||
|
||||
# ADX length variations (4 values)
|
||||
adx_lengths = [14, 16, 18, 20]
|
||||
|
||||
# RSI length (3 values)
|
||||
rsi_lengths = [12, 14, 16]
|
||||
|
||||
# RSI boundaries (4×4×4×4 = 256 combos)
|
||||
rsi_long_mins = [30, 35, 40, 45]
|
||||
rsi_long_maxs = [65, 70, 75, 80]
|
||||
rsi_short_mins = [25, 30, 35, 40]
|
||||
rsi_short_maxs = [60, 65, 70, 75]
|
||||
|
||||
# Volume max (4 values)
|
||||
vol_maxs = [3.0, 3.5, 4.0, 4.5]
|
||||
|
||||
# Entry buffer (3 values)
|
||||
entry_buffers = [0.15, 0.20, 0.25]
|
||||
|
||||
# Source mode (2 values)
|
||||
use_heikin_ashis = [False, True]
|
||||
|
||||
# MA gap filter modes (3 modes = 3 parameter sets)
|
||||
# Mode 1: Disabled
|
||||
# Mode 2: Longs only (require ma50 > ma200)
|
||||
# Mode 3: Both directions (bull/bear confirmation)
|
||||
ma_gap_configs = [
|
||||
(False, 0.0, 0.0), # Disabled
|
||||
(True, 0.5, 0.0), # Longs only: require 0.5% gap
|
||||
(True, 0.5, -0.5), # Both: longs need +0.5%, shorts need -0.5%
|
||||
]
|
||||
|
||||
configs = []
|
||||
config_id = 0
|
||||
|
||||
for atr_period, multiplier, adx_length, rsi_length, \
|
||||
rsi_long_min, rsi_long_max, rsi_short_min, rsi_short_max, \
|
||||
vol_max, entry_buffer, use_ha, ma_gap_config in \
|
||||
itertools.product(
|
||||
atr_periods, multipliers, adx_lengths, rsi_lengths,
|
||||
rsi_long_mins, rsi_long_maxs, rsi_short_mins, rsi_short_maxs,
|
||||
vol_maxs, entry_buffers, use_heikin_ashis, ma_gap_configs
|
||||
):
|
||||
|
||||
# Validity check: RSI min < max
|
||||
if rsi_long_min >= rsi_long_max:
|
||||
continue
|
||||
if rsi_short_min >= rsi_short_max:
|
||||
continue
|
||||
|
||||
use_ma_gap, ma_gap_long_min, ma_gap_short_max = ma_gap_config
|
||||
|
||||
configs.append((config_id, {
|
||||
'atr_period': atr_period,
|
||||
'multiplier': multiplier,
|
||||
'adx_length': adx_length,
|
||||
'rsi_length': rsi_length,
|
||||
'rsi_long_min': rsi_long_min,
|
||||
'rsi_long_max': rsi_long_max,
|
||||
'rsi_short_min': rsi_short_min,
|
||||
'rsi_short_max': rsi_short_max,
|
||||
'vol_max': vol_max,
|
||||
'entry_buffer_atr': entry_buffer,
|
||||
'use_heikin_ashi': use_ha,
|
||||
'use_ma_gap_filter': use_ma_gap,
|
||||
'ma_gap_long_min': ma_gap_long_min,
|
||||
'ma_gap_short_max': ma_gap_short_max,
|
||||
}))
|
||||
config_id += 1
|
||||
|
||||
return configs
|
||||
|
||||
|
||||
def main():
|
||||
"""Run advanced parameter sweep."""
|
||||
print("=" * 80)
|
||||
print("v9 ADVANCED PARAMETER SWEEP - AGGRESSIVE OPTIMIZATION")
|
||||
print("=" * 80)
|
||||
print()
|
||||
print("This will explore ~800K parameter combinations across:")
|
||||
print(" - ATR profiles (5 periods × 6 multipliers)")
|
||||
print(" - RSI boundaries (4×4×4×4 = 256 combinations)")
|
||||
print(" - Volume max (4 values)")
|
||||
print(" - Entry buffer (3 values)")
|
||||
print(" - ADX length (4 values)")
|
||||
print(" - RSI length (3 values)")
|
||||
print(" - Source mode (Chart vs Heikin Ashi)")
|
||||
print(" - MA gap filter (3 modes)")
|
||||
print()
|
||||
print("Expected runtime: 40-80 hours on 2-worker cluster")
|
||||
print("Target: Beat baseline $194.43/1k (19.44% returns)")
|
||||
print()
|
||||
|
||||
# Generate parameter grid
|
||||
print("Generating parameter combinations...")
|
||||
configs = generate_parameter_grid()
|
||||
print(f"Total configurations: {len(configs):,}")
|
||||
print()
|
||||
|
||||
# Determine number of workers
|
||||
n_workers = mp.cpu_count()
|
||||
print(f"Using {n_workers} CPU cores")
|
||||
print()
|
||||
|
||||
# Run sweep
|
||||
print("Starting parameter sweep...")
|
||||
with mp.Pool(n_workers) as pool:
|
||||
results = list(tqdm(
|
||||
pool.imap(test_config, configs),
|
||||
total=len(configs),
|
||||
desc="Testing configs"
|
||||
))
|
||||
|
||||
# Convert to DataFrame
|
||||
results_df = pd.DataFrame(results)
|
||||
|
||||
# Save full results
|
||||
output_file = "sweep_v9_advanced_full.csv"
|
||||
results_df.to_csv(output_file, index=False)
|
||||
print(f"Full results saved to: {output_file}")
|
||||
|
||||
# Sort by PnL and save top 1000
|
||||
top_results = results_df.nlargest(1000, 'pnl')
|
||||
top_file = "sweep_v9_advanced_top1000.csv"
|
||||
top_results.to_csv(top_file, index=False)
|
||||
print(f"Top 1000 configurations saved to: {top_file}")
|
||||
|
||||
# Print summary
|
||||
print()
|
||||
print("=" * 80)
|
||||
print("SWEEP COMPLETE")
|
||||
print("=" * 80)
|
||||
print()
|
||||
print(f"Best configuration:")
|
||||
best = top_results.iloc[0]
|
||||
print(f" PnL: ${best['pnl']:.2f}")
|
||||
print(f" Win Rate: {best['win_rate']:.1f}%")
|
||||
print(f" Profit Factor: {best['profit_factor']:.2f}")
|
||||
print(f" Max Drawdown: ${best['max_drawdown']:.2f}")
|
||||
print(f" Total Trades: {best['total_trades']}")
|
||||
print()
|
||||
print("Parameters:")
|
||||
print(f" ATR Period: {best['atr_period']}")
|
||||
print(f" Multiplier: {best['multiplier']}")
|
||||
print(f" ADX Length: {best['adx_length']}")
|
||||
print(f" RSI Length: {best['rsi_length']}")
|
||||
print(f" RSI Long: {best['rsi_long_min']}-{best['rsi_long_max']}")
|
||||
print(f" RSI Short: {best['rsi_short_min']}-{best['rsi_short_max']}")
|
||||
print(f" Volume Max: {best['vol_max']}")
|
||||
print(f" Entry Buffer: {best['entry_buffer_atr']}")
|
||||
print(f" Heikin Ashi: {best['use_heikin_ashi']}")
|
||||
print(f" MA Gap Filter: {best['use_ma_gap_filter']}")
|
||||
if best['use_ma_gap_filter']:
|
||||
print(f" Long Min: {best['ma_gap_long_min']:.1f}%")
|
||||
print(f" Short Max: {best['ma_gap_short_max']:.1f}%")
|
||||
print()
|
||||
print(f"Baseline to beat: $194.43 (19.44%)")
|
||||
improvement = ((best['pnl'] - 194.43) / 194.43) * 100
|
||||
print(f"Improvement: {improvement:+.1f}%")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user