From eb0d41aed5797fc76791da6d454c18c37ab25ce8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 6 Dec 2025 19:15:54 +0000 Subject: [PATCH] feat: Add v11 test sweep system (256 combinations) with office hours scheduling Co-authored-by: mindesbunister <32161838+mindesbunister@users.noreply.github.com> --- .../v11_moneyline_all_filters.cpython-312.pyc | Bin 0 -> 13292 bytes backtester/v11_moneyline_all_filters.py | 321 +++++++++++++ .../v11_test_coordinator.cpython-312.pyc | Bin 0 -> 15022 bytes .../v11_test_worker.cpython-312.pyc | Bin 0 -> 9877 bytes cluster/run_v11_test_sweep.sh | 57 +++ cluster/v11_test_coordinator.py | 437 ++++++++++++++++++ cluster/v11_test_worker.py | 298 ++++++++++++ 7 files changed, 1113 insertions(+) create mode 100644 backtester/__pycache__/v11_moneyline_all_filters.cpython-312.pyc create mode 100644 backtester/v11_moneyline_all_filters.py create mode 100644 cluster/__pycache__/v11_test_coordinator.cpython-312.pyc create mode 100644 cluster/__pycache__/v11_test_worker.cpython-312.pyc create mode 100755 cluster/run_v11_test_sweep.sh create mode 100755 cluster/v11_test_coordinator.py create mode 100755 cluster/v11_test_worker.py diff --git a/backtester/__pycache__/v11_moneyline_all_filters.cpython-312.pyc b/backtester/__pycache__/v11_moneyline_all_filters.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..86afecd237b8d9e2705e47e7089cddd8051ad546 GIT binary patch literal 13292 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 literal 0 HcmV?d00001 diff --git a/backtester/v11_moneyline_all_filters.py b/backtester/v11_moneyline_all_filters.py new file mode 100644 index 0000000..5677715 --- /dev/null +++ b/backtester/v11_moneyline_all_filters.py @@ -0,0 +1,321 @@ +""" +v11 "Money Line All Filters" indicator implementation for backtesting. + +CRITICAL DIFFERENCE FROM v9: +- v11: ALL filters actually applied to signals (useQualityFilters toggle) +- v9 bug: Filters calculated but signals ignored them + +Based on moneyline_v11_all_filters.pinescript lines 271-272: + finalLongSignal = buyReady and (not useQualityFilters or (longOk and adxOk and longBufferOk and longPositionOk and volumeOk and rsiLongOk)) + finalShortSignal = sellReady and (not useQualityFilters or (shortOk and adxOk and shortBufferOk and shortPositionOk and volumeOk and rsiShortOk)) + +Test sweep parameters (8 params × 2 values = 256 combinations): +- flip_threshold: 0.5, 0.6 +- adx_min: 18, 21 +- long_pos_max: 75, 80 +- short_pos_min: 20, 25 +- vol_min: 0.8, 1.0 +- entry_buffer_atr: 0.15, 0.20 +- rsi_long_min: 35, 40 +- rsi_short_max: 65, 70 +""" +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 MoneyLineV11Inputs: + """v11 Money Line indicator parameters for test sweep.""" + + # Basic Money Line parameters (fixed for test) + confirm_bars: int = 0 # Immediate signals + cooldown_bars: int = 3 # Prevent overtrading + + # ATR profile (fixed for test - 5-minute chart defaults) + atr_period: int = 12 # ATR calculation length + multiplier: float = 3.8 # ATR band multiplier + + # Filter parameters (8 parameters being optimized) + flip_threshold: float = 0.5 # % price must move to flip (TEST: 0.5, 0.6) + adx_min: float = 21 # Minimum ADX for signal (TEST: 18, 21) + long_pos_max: float = 75 # Don't long above X% of range (TEST: 75, 80) + short_pos_min: float = 20 # Don't short below X% of range (TEST: 20, 25) + vol_min: float = 1.0 # Minimum volume ratio (TEST: 0.8, 1.0) + entry_buffer_atr: float = 0.20 # ATR buffer beyond line (TEST: 0.15, 0.20) + rsi_long_min: float = 35 # RSI minimum for longs (TEST: 35, 40) + rsi_short_max: float = 70 # RSI maximum for shorts (TEST: 65, 70) + + # Fixed filter parameters (not being optimized in test) + adx_length: int = 16 # ADX calculation length + rsi_length: int = 14 # RSI calculation length + vol_max: float = 3.5 # Maximum volume ratio + rsi_long_max: float = 70 # RSI maximum for longs + rsi_short_min: float = 30 # RSI minimum for shorts + + +@dataclass +class MoneyLineV11Signal: + timestamp: pd.Timestamp + direction: Direction + entry_price: float + adx: float + atr: float + rsi: float + volume_ratio: float + price_position: float + + +def ema(series: pd.Series, length: int) -> pd.Series: + """Exponential Moving Average.""" + return series.ewm(span=length, adjust=False).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 supertrend_v11(df: pd.DataFrame, atr_period: int, multiplier: float, + flip_threshold: float, confirm_bars: int) -> tuple[pd.Series, pd.Series]: + """ + Calculate v11 Money Line (Supertrend with flip threshold). + + Returns: + (supertrend_line, trend): Line values and trend direction (1=bull, -1=bear) + """ + # Use chart prices (not Heikin Ashi for test) + high, low, close = df['high'], df['low'], df['close'] + + # Calculate ATR + 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 / 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_v11_signals(df: pd.DataFrame, inputs: Optional[MoneyLineV11Inputs] = None) -> list[MoneyLineV11Signal]: + """ + v11 "Money Line All Filters" signal generation. + + CRITICAL: ALL filters applied to signals (this is what makes v11 different from v9 bug). + + From pinescript lines 271-272: + finalLongSignal = buyReady and (longOk and adxOk and longBufferOk and longPositionOk and volumeOk and rsiLongOk) + finalShortSignal = sellReady and (shortOk and adxOk and shortBufferOk and shortPositionOk and volumeOk and rsiShortOk) + + Filters applied: + - ADX minimum (trend strength) + - Entry buffer (price beyond line by X*ATR) + - Price position (don't chase extremes) + - Volume ratio (avoid dead/overheated) + - RSI boundaries (momentum confirmation) + """ + if inputs is None: + inputs = MoneyLineV11Inputs() + + data = df.copy() + data = data.sort_index() + + # Calculate Money Line + supertrend, trend = supertrend_v11( + data, + inputs.atr_period, + inputs.multiplier, + inputs.flip_threshold, + inputs.confirm_bars + ) + data['supertrend'] = supertrend + data['trend'] = trend + + # Calculate indicators + 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"]) + + signals: list[MoneyLineV11Signal] = [] + cooldown_remaining = 0 + + # Skip warmup period (200 bars for price position) + warmup_bars = 200 + + for idx in range(max(1, warmup_bars), len(data)): + row = data.iloc[idx] + prev = data.iloc[idx - 1] + + # Detect trend flip (buyReady/sellReady in pinescript) + 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 + + # V11 CRITICAL: Apply ALL filters (this is what was broken in v9) + + # ADX filter (adxOk) + adx_ok = row.adx >= inputs.adx_min + + # Volume filter (volumeOk) + volume_ok = inputs.vol_min <= row.volume_ratio <= inputs.vol_max + + if flip_long: + # Entry buffer check (longBufferOk) + entry_buffer_ok = row.close > (row.supertrend + inputs.entry_buffer_atr * row.atr) + + # Long filters + rsi_ok = inputs.rsi_long_min <= row.rsi <= inputs.rsi_long_max # rsiLongOk + pos_ok = row.price_position < inputs.long_pos_max # longPositionOk + + # V11: ALL filters must pass (this is the fix from v9) + if adx_ok and volume_ok and rsi_ok and pos_ok and entry_buffer_ok: + signals.append( + MoneyLineV11Signal( + 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), + ) + ) + cooldown_remaining = inputs.cooldown_bars + + elif flip_short: + # Entry buffer check (shortBufferOk) + entry_buffer_ok = row.close < (row.supertrend - inputs.entry_buffer_atr * row.atr) + + # Short filters + rsi_ok = inputs.rsi_short_min <= row.rsi <= inputs.rsi_short_max # rsiShortOk + pos_ok = row.price_position > inputs.short_pos_min # shortPositionOk + + # V11: ALL filters must pass (this is the fix from v9) + if adx_ok and volume_ok and rsi_ok and pos_ok and entry_buffer_ok: + signals.append( + MoneyLineV11Signal( + 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), + ) + ) + cooldown_remaining = inputs.cooldown_bars + + return signals diff --git a/cluster/__pycache__/v11_test_coordinator.cpython-312.pyc b/cluster/__pycache__/v11_test_coordinator.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f0720cc3611fe9f157a9dd6a48c0cb4df716d254 GIT binary patch literal 15022 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 F{x2*=8cqNJ literal 0 HcmV?d00001 diff --git a/cluster/__pycache__/v11_test_worker.cpython-312.pyc b/cluster/__pycache__/v11_test_worker.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e6a4f33fe3085145a6f3de4c882c02e54ea91274 GIT binary patch literal 9877 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 UtJ#_R+kZ**t@#MbuPo+&1HRL{K>z>% literal 0 HcmV?d00001 diff --git a/cluster/run_v11_test_sweep.sh b/cluster/run_v11_test_sweep.sh new file mode 100755 index 0000000..441d290 --- /dev/null +++ b/cluster/run_v11_test_sweep.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# V11 Test Parameter Sweep Launch Script +# Initializes database and starts coordinator for 256-combination test sweep + +set -e # Exit on error + +echo "================================================================" +echo "V11 TEST PARAMETER SWEEP" +echo "================================================================" +echo "Combinations: 256 (2^8 parameters)" +echo "Chunks: 2 × 128 combinations" +echo "Worker 1: Always available (27 cores)" +echo "Worker 2: Office hours aware (27 cores nights/weekends only)" +echo "Expected runtime: 6-25 minutes" +echo "================================================================" +echo "" + +cd "$(dirname "$0")" + +# Check if data file exists +if [ ! -f "data/solusdt_5m.csv" ]; then + echo "✗ Error: data/solusdt_5m.csv not found" + echo " Please ensure market data is available" + exit 1 +fi + +echo "✓ Market data found" + +# Check if coordinator script exists +if [ ! -f "v11_test_coordinator.py" ]; then + echo "✗ Error: v11_test_coordinator.py not found" + exit 1 +fi + +echo "✓ Coordinator script found" + +# Launch coordinator in background +echo "" +echo "🚀 Starting coordinator..." +nohup python3 v11_test_coordinator.py > coordinator_v11_test.log 2>&1 & +COORDINATOR_PID=$! + +echo "✓ Coordinator started (PID: $COORDINATOR_PID)" +echo "" +echo "================================================================" +echo "MONITORING" +echo "================================================================" +echo "Log file: tail -f coordinator_v11_test.log" +echo "Database: sqlite3 exploration.db" +echo "Results: cluster/v11_test_results/*.csv" +echo "" +echo "To check status:" +echo " sqlite3 exploration.db \"SELECT * FROM v11_test_chunks\"" +echo "" +echo "To stop sweep:" +echo " kill $COORDINATOR_PID" +echo "================================================================" diff --git a/cluster/v11_test_coordinator.py b/cluster/v11_test_coordinator.py new file mode 100755 index 0000000..dce3c55 --- /dev/null +++ b/cluster/v11_test_coordinator.py @@ -0,0 +1,437 @@ +#!/usr/bin/env python3 +""" +V11 Test Parameter Sweep Coordinator + +Coordinates 256-combination test sweep across 2 workers with smart scheduling. +Worker 2 respects office hours (Mon-Fri 8am-6pm disabled, nights/weekends OK). + +Test sweep: 2 chunks × 128 combinations = 256 total +Expected runtime: 6-25 minutes depending on worker availability +""" + +import sqlite3 +import subprocess +import time +import signal +import sys +from pathlib import Path +from datetime import datetime +import urllib.request +import json + +# Worker configuration +WORKERS = { + 'worker1': { + 'host': 'root@10.10.254.106', + 'workspace': '/home/comprehensive_sweep', + 'cores': 27, + }, + 'worker2': { + 'host': 'root@10.20.254.100', + 'workspace': '/home/backtest_dual/backtest', + 'ssh_hop': 'root@10.10.254.106', + 'cores': 27, + 'time_restricted': True, + 'allowed_start_hour': 18, # 6 PM + 'allowed_end_hour': 8, # 8 AM + } +} + +DATA_FILE = 'data/solusdt_5m.csv' +DB_PATH = 'exploration.db' +CHUNK_SIZE = 128 # Each chunk processes 128 combinations + +# Telegram configuration +TELEGRAM_BOT_TOKEN = '8240234365:AAEm6hg_XOm54x8ctnwpNYreFKRAEvWU3uY' +TELEGRAM_CHAT_ID = '579304651' + + +def send_telegram_message(message: str): + """Send notification to Telegram""" + try: + url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage" + data = { + 'chat_id': TELEGRAM_CHAT_ID, + 'text': message, + 'parse_mode': 'HTML' + } + + req = urllib.request.Request( + url, + data=json.dumps(data).encode('utf-8'), + headers={'Content-Type': 'application/json'} + ) + + with urllib.request.urlopen(req, timeout=10) as response: + if response.status == 200: + print(f"✓ Telegram notification sent") + else: + print(f"⚠️ Telegram notification failed: {response.status}") + except Exception as e: + print(f"⚠️ Error sending Telegram notification: {e}") + + +def is_worker2_available() -> bool: + """Check if Worker 2 can run (respects office hours)""" + now = datetime.now() + + # Weekend (Sat=5, Sun=6): Available 24/7 + if now.weekday() >= 5: + return True + + # Weekday: Only 6 PM - 8 AM (avoid office hours 8am-6pm) + hour = now.hour + # Allowed if hour >= 18 (6 PM) OR hour < 8 (8 AM) + return hour >= 18 or hour < 8 + + +def get_available_workers() -> list: + """Return list of workers available right now""" + workers = ['worker1'] # Always available + if is_worker2_available(): + workers.append('worker2') + print("✓ Worker 2 available (outside office hours)") + else: + print("⚠️ Worker 2 unavailable (office hours Mon-Fri 8am-6pm)") + return workers + + +def init_database(): + """Initialize database tables for v11 test sweep""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + # Drop existing test tables if present + cursor.execute("DROP TABLE IF EXISTS v11_test_chunks") + cursor.execute("DROP TABLE IF EXISTS v11_test_strategies") + + # Create chunks table + cursor.execute(""" + CREATE TABLE v11_test_chunks ( + id TEXT PRIMARY KEY, + start_combo INTEGER, + end_combo INTEGER, + total_combos INTEGER, + status TEXT, + assigned_worker TEXT, + started_at INTEGER, + completed_at INTEGER + ) + """) + + # Create strategies table + cursor.execute(""" + CREATE TABLE v11_test_strategies ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + chunk_id TEXT, + params TEXT, + pnl REAL, + win_rate REAL, + profit_factor REAL, + max_drawdown REAL, + total_trades INTEGER, + FOREIGN KEY (chunk_id) REFERENCES v11_test_chunks(id) + ) + """) + + # Register 2 chunks (256 combinations total) + chunks = [ + ('v11_test_chunk_0000', 0, 128, 128), + ('v11_test_chunk_0001', 128, 256, 128), + ] + + for chunk_id, start, end, total in chunks: + cursor.execute( + "INSERT INTO v11_test_chunks (id, start_combo, end_combo, total_combos, status) VALUES (?, ?, ?, ?, 'pending')", + (chunk_id, start, end, total) + ) + + conn.commit() + conn.close() + print("✓ Database initialized with 2 chunks") + + +def get_pending_chunks() -> list: + """Get list of pending chunks""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute("SELECT id, start_combo FROM v11_test_chunks WHERE status='pending'") + chunks = cursor.fetchall() + conn.close() + return chunks + + +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 v11_test_chunks SET status='running', assigned_worker=?, started_at=? WHERE id=?", + (worker_name, int(time.time()), chunk_id) + ) + conn.commit() + conn.close() + + +def deploy_worker(worker_name: str, chunk_id: str, start_combo: int): + """Deploy worker to EPYC server via SSH""" + worker = WORKERS[worker_name] + + print(f"\n{'='*60}") + print(f"Deploying {worker_name} for {chunk_id}") + print(f"{'='*60}") + + # Build SSH command + workspace = worker['workspace'] + + # Copy v11 test worker script + print(f"📦 Copying v11_test_worker.py to {worker_name}...") + + if 'ssh_hop' in worker: + # Worker 2: Use SSH hop through worker 1 + scp_cmd = [ + 'scp', + '-o', 'StrictHostKeyChecking=no', + '-o', f'ProxyJump={worker["ssh_hop"]}', + 'cluster/v11_test_worker.py', + f'{worker["host"]}:{workspace}/' + ] + else: + # Worker 1: Direct connection + scp_cmd = [ + 'scp', + '-o', 'StrictHostKeyChecking=no', + 'cluster/v11_test_worker.py', + f'{worker["host"]}:{workspace}/' + ] + + result = subprocess.run(scp_cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"✗ Failed to copy worker script: {result.stderr}") + return False + + print(f"✓ Worker script deployed") + + # Copy v11 indicator module + print(f"📦 Copying v11 indicator to {worker_name}...") + + if 'ssh_hop' in worker: + scp_cmd = [ + 'scp', + '-o', 'StrictHostKeyChecking=no', + '-o', f'ProxyJump={worker["ssh_hop"]}', + 'backtester/v11_moneyline_all_filters.py', + f'{worker["host"]}:{workspace}/backtester/' + ] + else: + scp_cmd = [ + 'scp', + '-o', 'StrictHostKeyChecking=no', + 'backtester/v11_moneyline_all_filters.py', + f'{worker["host"]}:{workspace}/backtester/' + ] + + result = subprocess.run(scp_cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"✗ Failed to copy indicator: {result.stderr}") + return False + + print(f"✓ Indicator deployed") + + # Start worker + print(f"🚀 Starting worker process...") + + worker_cmd = f"cd {workspace} && nohup python3 v11_test_worker.py {DATA_FILE} {chunk_id} {start_combo} > {chunk_id}_worker.log 2>&1 &" + + if 'ssh_hop' in worker: + ssh_cmd = [ + 'ssh', + '-o', 'StrictHostKeyChecking=no', + '-o', f'ProxyJump={worker["ssh_hop"]}', + worker['host'], + worker_cmd + ] + else: + ssh_cmd = [ + 'ssh', + '-o', 'StrictHostKeyChecking=no', + worker['host'], + worker_cmd + ] + + result = subprocess.run(ssh_cmd, capture_output=True, text=True) + if result.returncode != 0: + print(f"✗ Failed to start worker: {result.stderr}") + return False + + print(f"✓ Worker started on {worker_name}") + return True + + +def check_chunk_completion(worker_name: str, chunk_id: str) -> bool: + """Check if chunk has completed by looking for results CSV""" + worker = WORKERS[worker_name] + workspace = worker['workspace'] + + check_cmd = f"test -f {workspace}/v11_test_results/{chunk_id}_results.csv && echo 'exists'" + + if 'ssh_hop' in worker: + ssh_cmd = [ + 'ssh', + '-o', 'StrictHostKeyChecking=no', + '-o', f'ProxyJump={worker["ssh_hop"]}', + worker['host'], + check_cmd + ] + else: + ssh_cmd = [ + 'ssh', + '-o', 'StrictHostKeyChecking=no', + worker['host'], + check_cmd + ] + + result = subprocess.run(ssh_cmd, capture_output=True, text=True, timeout=10) + return 'exists' in result.stdout + + +def mark_chunk_complete(chunk_id: str): + """Mark chunk as completed in database""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute( + "UPDATE v11_test_chunks SET status='completed', completed_at=? WHERE id=?", + (int(time.time()), chunk_id) + ) + conn.commit() + conn.close() + + +def signal_handler(sig, frame): + """Handle termination signals""" + message = ( + "⚠️ V11 Test Sweep STOPPED\n\n" + "Coordinator received termination signal.\n" + "Sweep stopped prematurely.\n\n" + f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + send_telegram_message(message) + sys.exit(0) + + +def main(): + """Main coordinator loop""" + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + print("\n" + "="*60) + print("V11 TEST PARAMETER SWEEP COORDINATOR") + print("="*60) + print(f"Total combinations: 256 (2^8)") + print(f"Chunks: 2 × 128 combinations") + print(f"Workers: 2 × 27 cores (85% CPU)") + print(f"Expected runtime: 6-25 minutes") + print("="*60 + "\n") + + # Initialize database + print("📊 Initializing database...") + init_database() + + # Send start notification + available_workers = get_available_workers() + start_msg = ( + f"🚀 V11 Test Sweep STARTED\n\n" + f"Combinations: 256 (2^8)\n" + f"Chunks: 2 × 128 combos\n" + f"Workers: {len(available_workers)} available\n" + f"- Worker 1: Always on (27 cores)\n" + ) + if 'worker2' in available_workers: + start_msg += f"- Worker 2: Active (27 cores)\n" + else: + start_msg += f"- Worker 2: Office hours (waiting for 6 PM)\n" + start_msg += f"\nStart: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + + send_telegram_message(start_msg) + + # Deploy workers to available chunks + start_time = time.time() + active_chunks = {} # chunk_id -> worker_name + + pending_chunks = get_pending_chunks() + available_workers = get_available_workers() + + for worker_name in available_workers: + if pending_chunks: + chunk_id, start_combo = pending_chunks.pop(0) + print(f"\n📍 Assigning {chunk_id} to {worker_name}") + assign_chunk(chunk_id, worker_name) + + if deploy_worker(worker_name, chunk_id, start_combo): + active_chunks[chunk_id] = worker_name + print(f"✓ {chunk_id} active on {worker_name}") + else: + print(f"✗ Failed to deploy {chunk_id} on {worker_name}") + + # Monitor progress + print("\n" + "="*60) + print("MONITORING SWEEP PROGRESS") + print("="*60 + "\n") + + while active_chunks: + time.sleep(30) # Check every 30 seconds + + completed_this_round = [] + + for chunk_id, worker_name in active_chunks.items(): + if check_chunk_completion(worker_name, chunk_id): + print(f"✓ {chunk_id} COMPLETED on {worker_name}") + mark_chunk_complete(chunk_id) + completed_this_round.append(chunk_id) + + # Remove completed chunks + for chunk_id in completed_this_round: + del active_chunks[chunk_id] + + # Try to assign pending chunks to freed workers + if completed_this_round and pending_chunks: + available_workers = get_available_workers() + + for worker_name in available_workers: + if worker_name not in active_chunks.values() and pending_chunks: + chunk_id, start_combo = pending_chunks.pop(0) + print(f"\n📍 Assigning {chunk_id} to {worker_name}") + assign_chunk(chunk_id, worker_name) + + if deploy_worker(worker_name, chunk_id, start_combo): + active_chunks[chunk_id] = worker_name + print(f"✓ {chunk_id} active on {worker_name}") + + # All chunks complete + duration = time.time() - start_time + duration_min = duration / 60 + + print("\n" + "="*60) + print("V11 TEST SWEEP COMPLETE!") + print("="*60) + print(f"Duration: {duration_min:.1f} minutes") + print(f"Chunks: 2/2 completed") + print(f"Strategies: 256 tested") + print("="*60 + "\n") + + # Send completion notification + complete_msg = ( + f"✅ V11 Test Sweep COMPLETE\n\n" + f"Duration: {duration_min:.1f} minutes\n" + f"Chunks: 2/2 completed\n" + f"Strategies: 256 tested\n\n" + f"Check results:\n" + f"- cluster/v11_test_results/\n" + f"- sqlite3 exploration.db\n\n" + f"Completed: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + ) + send_telegram_message(complete_msg) + + +if __name__ == '__main__': + main() diff --git a/cluster/v11_test_worker.py b/cluster/v11_test_worker.py new file mode 100755 index 0000000..c13de9b --- /dev/null +++ b/cluster/v11_test_worker.py @@ -0,0 +1,298 @@ +#!/usr/bin/env python3 +""" +V11 Test Parameter Sweep Worker + +Processes chunks of v11 test parameter configurations (256 combinations total). +Uses 27 cores (85% CPU) for multiprocessing. + +Test parameter grid (2 values each = 2^8 = 256 combinations): +- flip_threshold: 0.5, 0.6 +- adx_min: 18, 21 +- long_pos_max: 75, 80 +- short_pos_min: 20, 25 +- vol_min: 0.8, 1.0 +- entry_buffer_atr: 0.15, 0.20 +- rsi_long_min: 35, 40 +- rsi_short_max: 65, 70 +""" + +import sys +import csv +import pandas as pd +from pathlib import Path +from typing import Dict, List, Any +from multiprocessing import Pool +import functools +import itertools + +# Add backtester to path +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from backtester.v11_moneyline_all_filters import ( + money_line_v11_signals, + MoneyLineV11Inputs +) +from backtester.simulator import simulate_money_line + +# CPU limit: 85% of 32 threads = 27 cores +MAX_WORKERS = 27 + +# Test parameter grid (256 combinations) +PARAMETER_GRID = { + 'flip_threshold': [0.5, 0.6], + 'adx_min': [18, 21], + 'long_pos_max': [75, 80], + 'short_pos_min': [20, 25], + 'vol_min': [0.8, 1.0], + 'entry_buffer_atr': [0.15, 0.20], + 'rsi_long_min': [35, 40], + 'rsi_short_max': [65, 70], +} + + +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']) + + df = df.set_index('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 v11 test parameter configuration + + Returns dict with: + - params: original config dict + - pnl: total P&L + - trades: number of trades + - win_rate: % winners + - profit_factor: wins/losses ratio + - max_drawdown: max drawdown $ + """ + try: + # Create v11 inputs + inputs = MoneyLineV11Inputs( + flip_threshold=config['flip_threshold'], + adx_min=config['adx_min'], + long_pos_max=config['long_pos_max'], + short_pos_min=config['short_pos_min'], + vol_min=config['vol_min'], + entry_buffer_atr=config['entry_buffer_atr'], + rsi_long_min=config['rsi_long_min'], + rsi_short_max=config['rsi_short_max'], + ) + + # Generate signals + signals = money_line_v11_signals(df, inputs) + + if not signals: + return { + 'params': config, + 'pnl': 0.0, + 'trades': 0, + 'win_rate': 0.0, + 'profit_factor': 0.0, + 'max_drawdown': 0.0, + } + + # Simple backtesting: track equity curve + equity = 1000.0 # Starting capital + peak_equity = equity + max_drawdown = 0.0 + wins = 0 + losses = 0 + win_pnl = 0.0 + loss_pnl = 0.0 + + for signal in signals: + # Simple trade simulation + # TP1 at +0.86%, SL at -1.29% (ATR-based defaults) + entry = signal.entry_price + + # Look ahead in data to see if TP or SL hit + signal_idx = df.index.get_loc(signal.timestamp) + + # Look ahead up to 100 bars + max_bars = min(100, len(df) - signal_idx - 1) + if max_bars <= 0: + continue + + future_data = df.iloc[signal_idx+1:signal_idx+1+max_bars] + + if signal.direction == "long": + tp_price = entry * 1.0086 # +0.86% + sl_price = entry * 0.9871 # -1.29% + + # Check if TP or SL hit + hit_tp = (future_data['high'] >= tp_price).any() + hit_sl = (future_data['low'] <= sl_price).any() + + if hit_tp: + pnl = 1000.0 * 0.0086 # $8.60 on $1000 position + equity += pnl + wins += 1 + win_pnl += pnl + elif hit_sl: + pnl = -1000.0 * 0.0129 # -$12.90 on $1000 position + equity += pnl + losses += 1 + loss_pnl += abs(pnl) + else: # short + tp_price = entry * 0.9914 # -0.86% + sl_price = entry * 1.0129 # +1.29% + + # Check if TP or SL hit + hit_tp = (future_data['low'] <= tp_price).any() + hit_sl = (future_data['high'] >= sl_price).any() + + if hit_tp: + pnl = 1000.0 * 0.0086 # $8.60 on $1000 position + equity += pnl + wins += 1 + win_pnl += pnl + elif hit_sl: + pnl = -1000.0 * 0.0129 # -$12.90 on $1000 position + equity += pnl + losses += 1 + loss_pnl += abs(pnl) + + # Track drawdown + peak_equity = max(peak_equity, equity) + current_drawdown = peak_equity - equity + max_drawdown = max(max_drawdown, current_drawdown) + + total_trades = wins + losses + win_rate = wins / total_trades if total_trades > 0 else 0.0 + profit_factor = win_pnl / loss_pnl if loss_pnl > 0 else (float('inf') if win_pnl > 0 else 0.0) + total_pnl = equity - 1000.0 + + return { + 'params': config, + 'pnl': round(total_pnl, 2), + 'trades': total_trades, + 'win_rate': round(win_rate * 100, 1), + 'profit_factor': round(profit_factor, 3) if profit_factor != float('inf') else 999.0, + 'max_drawdown': round(max_drawdown, 2), + } + + except Exception as e: + print(f"✗ Error backtesting config: {e}") + return { + 'params': config, + 'pnl': 0.0, + 'trades': 0, + 'win_rate': 0.0, + 'profit_factor': 0.0, + 'max_drawdown': 0.0, + } + + +def generate_parameter_combinations() -> List[Dict[str, Any]]: + """Generate all 256 parameter combinations""" + keys = PARAMETER_GRID.keys() + values = PARAMETER_GRID.values() + + combinations = [] + for combo in itertools.product(*values): + config = dict(zip(keys, combo)) + combinations.append(config) + + return combinations + + +def process_chunk(data_file: str, chunk_id: str, start_idx: int, end_idx: int): + """Process a chunk of parameter combinations""" + print(f"\n{'='*60}") + print(f"V11 Test Worker - {chunk_id}") + print(f"Processing combinations {start_idx} to {end_idx-1}") + print(f"{'='*60}\n") + + # Load market data + df = load_market_data(data_file) + + # Generate all combinations + all_combos = generate_parameter_combinations() + print(f"✓ Generated {len(all_combos)} total combinations") + + # Get this chunk's combinations + chunk_combos = all_combos[start_idx:end_idx] + print(f"✓ Processing {len(chunk_combos)} combinations in this chunk\n") + + # Backtest with multiprocessing + print(f"⚡ Starting {MAX_WORKERS}-core backtest...\n") + + with Pool(processes=MAX_WORKERS) as pool: + backtest_func = functools.partial(backtest_config, df) + results = pool.map(backtest_func, chunk_combos) + + print(f"\n✓ Completed {len(results)} backtests") + + # Write results to CSV + output_dir = Path('v11_test_results') + output_dir.mkdir(exist_ok=True) + + csv_file = output_dir / f"{chunk_id}_results.csv" + + with open(csv_file, 'w', newline='') as f: + writer = csv.writer(f) + + # Header + writer.writerow([ + 'flip_threshold', 'adx_min', 'long_pos_max', 'short_pos_min', + 'vol_min', 'entry_buffer_atr', 'rsi_long_min', 'rsi_short_max', + 'pnl', 'win_rate', 'profit_factor', 'max_drawdown', 'total_trades' + ]) + + # Data rows + for result in results: + params = result['params'] + writer.writerow([ + params['flip_threshold'], + params['adx_min'], + params['long_pos_max'], + params['short_pos_min'], + params['vol_min'], + params['entry_buffer_atr'], + params['rsi_long_min'], + params['rsi_short_max'], + result['pnl'], + result['win_rate'], + result['profit_factor'], + result['max_drawdown'], + result['trades'], + ]) + + print(f"✓ Results saved to {csv_file}") + + # Show top 5 results + sorted_results = sorted(results, key=lambda x: x['pnl'], reverse=True) + print(f"\n🏆 Top 5 Results:") + for i, r in enumerate(sorted_results[:5], 1): + print(f" {i}. PnL: ${r['pnl']:,.2f} | Trades: {r['trades']} | WR: {r['win_rate']}%") + + +if __name__ == '__main__': + if len(sys.argv) != 4: + print("Usage: python v11_test_worker.py ") + sys.exit(1) + + data_file = sys.argv[1] + chunk_id = sys.argv[2] + start_idx = int(sys.argv[3]) + + # Calculate end index (128 combos per chunk) + end_idx = start_idx + 128 + + process_chunk(data_file, chunk_id, start_idx, end_idx)