CRITICAL FIX (Nov 30, 2025):
- Dashboard showed 'idle' despite 22+ worker processes running
- Root cause: SSH-based worker detection timing out
- Solution: Check database for running chunks FIRST
Changes:
1. app/api/cluster/status/route.ts:
- Query exploration database before SSH detection
- If running chunks exist, mark workers 'active' even if SSH fails
- Override worker status: 'offline' → 'active' when chunks running
- Log: '✅ Cluster status: ACTIVE (database shows running chunks)'
- Database is source of truth, SSH only for supplementary metrics
2. app/cluster/page.tsx:
- Stop button ALREADY EXISTS (conditionally shown)
- Shows Start when status='idle', Stop when status='active'
- No code changes needed - fixed by status detection
Result:
- Dashboard now shows 'ACTIVE' with 2 workers (correct)
- Workers show 'active' status (was 'offline')
- Stop button automatically visible when cluster active
- System resilient to SSH timeouts/network issues
Verified:
- Container restarted: Nov 30 21:18 UTC
- API tested: Returns status='active', activeWorkers=2
- Logs confirm: Database-first logic working
- Workers confirmed running: 22+ processes on worker1, workers on worker2
143 lines
5.3 KiB
Python
143 lines
5.3 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Quick comparison: v8 "Sticky Trend" vs v9 "MA Gap" baseline performance.
|
|
|
|
Runs both indicators with default parameters on the same dataset.
|
|
"""
|
|
from pathlib import Path
|
|
import pandas as pd
|
|
from backtester.data_loader import load_csv
|
|
from backtester.simulator import simulate_money_line, TradeConfig, SimulationResult
|
|
from backtester.indicators.money_line import money_line_signals, MoneyLineInputs
|
|
from backtester.indicators.money_line_v8 import money_line_v8_signals, MoneyLineV8Inputs
|
|
|
|
# Load data
|
|
print("Loading SOLUSDT 5m data (Aug 1 - Nov 28, 2025)...")
|
|
data_slice = load_csv(Path("data/solusdt_5m.csv"), "SOLUSDT", "5m")
|
|
df = data_slice.data
|
|
print(f"Loaded {len(df)} candles\n")
|
|
|
|
# Run v9 baseline
|
|
print("=" * 60)
|
|
print("v9 'MA Gap' - Baseline (Default Parameters)")
|
|
print("=" * 60)
|
|
v9_inputs = MoneyLineInputs(
|
|
flip_threshold_percent=0.6,
|
|
cooldown_bars=2,
|
|
ma_gap_threshold=0.35,
|
|
momentum_min_adx=23.0,
|
|
momentum_long_max_pos=70.0,
|
|
momentum_short_min_pos=25.0,
|
|
)
|
|
v9_result = simulate_money_line(df, "SOLUSDT", v9_inputs)
|
|
v9_trades = v9_result.trades
|
|
print(f"Generated {len(v9_trades)} trades")
|
|
|
|
wins_v9 = sum(1 for t in v9_trades if t.realized_pnl > 0)
|
|
losses_v9 = sum(1 for t in v9_trades if t.realized_pnl <= 0)
|
|
gross_wins_v9 = sum(t.realized_pnl for t in v9_trades if t.realized_pnl > 0)
|
|
gross_losses_v9 = abs(sum(t.realized_pnl for t in v9_trades if t.realized_pnl <= 0))
|
|
pf_v9 = gross_wins_v9 / gross_losses_v9 if gross_losses_v9 > 0 else 0.0
|
|
wr_v9 = (wins_v9 / len(v9_trades) * 100) if v9_trades else 0.0
|
|
avg_win_v9 = (gross_wins_v9 / wins_v9) if wins_v9 > 0 else 0.0
|
|
avg_loss_v9 = (gross_losses_v9 / losses_v9) if losses_v9 > 0 else 0.0
|
|
|
|
print(f"\nResults:")
|
|
print(f" Total P&L: ${v9_result.total_pnl:.2f}")
|
|
print(f" Total Trades: {len(v9_trades)}")
|
|
print(f" Win Rate: {wr_v9:.2f}%")
|
|
print(f" Profit Factor: {pf_v9:.3f}")
|
|
print(f" Max Drawdown: ${v9_result.max_drawdown:.2f}")
|
|
print(f" Avg Win: ${avg_win_v9:.2f}")
|
|
print(f" Avg Loss: ${avg_loss_v9:.2f}")
|
|
|
|
# Run v8 baseline
|
|
print("\n" + "=" * 60)
|
|
print("v8 'Sticky Trend' - Baseline (Default Parameters)")
|
|
print("=" * 60)
|
|
|
|
# Create custom simulator for v8 since it doesn't use MoneyLineInputs
|
|
v8_signals = money_line_v8_signals(df, MoneyLineV8Inputs())
|
|
print(f"Generated {len(v8_signals)} signals")
|
|
|
|
# Manually simulate v8 trades using same logic as v9
|
|
from backtester.simulator import _simulate_trade, TradeConfig
|
|
v8_trades = []
|
|
config = TradeConfig()
|
|
index_positions = {ts: idx for idx, ts in enumerate(df.index)}
|
|
next_available = 0
|
|
|
|
for sig in v8_signals:
|
|
if sig.timestamp not in index_positions:
|
|
continue
|
|
idx = index_positions[sig.timestamp]
|
|
if idx < next_available:
|
|
continue
|
|
# Convert v8 signal to v9 signal format for simulator
|
|
from backtester.indicators.money_line import MoneyLineSignal
|
|
ml_sig = MoneyLineSignal(
|
|
timestamp=sig.timestamp,
|
|
direction=sig.direction,
|
|
entry_price=sig.entry_price,
|
|
adx=sig.adx,
|
|
atr=sig.atr,
|
|
rsi=sig.rsi,
|
|
volume_ratio=sig.volume_ratio,
|
|
price_position=sig.price_position,
|
|
signal_type="primary"
|
|
)
|
|
trade = _simulate_trade(df, idx, ml_sig, "SOLUSDT", config)
|
|
if trade:
|
|
v8_trades.append(trade)
|
|
next_available = trade._exit_index
|
|
|
|
v8_total_pnl = sum(t.realized_pnl for t in v8_trades)
|
|
v8_max_dd = 0.0
|
|
equity = 0.0
|
|
peak = 0.0
|
|
for t in v8_trades:
|
|
equity += t.realized_pnl
|
|
peak = max(peak, equity)
|
|
v8_max_dd = min(v8_max_dd, equity - peak)
|
|
|
|
wins_v8 = sum(1 for t in v8_trades if t.realized_pnl > 0)
|
|
losses_v8 = sum(1 for t in v8_trades if t.realized_pnl <= 0)
|
|
gross_wins_v8 = sum(t.realized_pnl for t in v8_trades if t.realized_pnl > 0)
|
|
gross_losses_v8 = abs(sum(t.realized_pnl for t in v8_trades if t.realized_pnl <= 0))
|
|
pf_v8 = gross_wins_v8 / gross_losses_v8 if gross_losses_v8 > 0 else 0.0
|
|
wr_v8 = (wins_v8 / len(v8_trades) * 100) if v8_trades else 0.0
|
|
avg_win_v8 = (gross_wins_v8 / wins_v8) if wins_v8 > 0 else 0.0
|
|
avg_loss_v8 = (gross_losses_v8 / losses_v8) if losses_v8 > 0 else 0.0
|
|
|
|
print(f"\nResults:")
|
|
print(f" Total P&L: ${v8_total_pnl:.2f}")
|
|
print(f" Total Trades: {len(v8_trades)}")
|
|
print(f" Win Rate: {wr_v8:.2f}%")
|
|
print(f" Profit Factor: {pf_v8:.3f}")
|
|
print(f" Max Drawdown: ${v8_max_dd:.2f}")
|
|
print(f" Avg Win: ${avg_win_v8:.2f}")
|
|
print(f" Avg Loss: ${avg_loss_v8:.2f}")
|
|
|
|
# Comparison
|
|
print("\n" + "=" * 60)
|
|
print("HEAD-TO-HEAD COMPARISON")
|
|
print("=" * 60)
|
|
pnl_diff = v9_result.total_pnl - v8_total_pnl
|
|
pnl_diff_pct = (pnl_diff / abs(v8_total_pnl)) * 100 if v8_total_pnl != 0 else 0
|
|
trade_diff = len(v9_trades) - len(v8_trades)
|
|
wr_diff = wr_v9 - wr_v8
|
|
|
|
print(f"P&L: v9 ${v9_result.total_pnl:.2f} vs v8 ${v8_total_pnl:.2f} (Δ ${pnl_diff:+.2f}, {pnl_diff_pct:+.1f}%)")
|
|
print(f"Trades: v9 {len(v9_trades)} vs v8 {len(v8_trades)} (Δ {trade_diff:+d})")
|
|
print(f"Win Rate: v9 {wr_v9:.2f}% vs v8 {wr_v8:.2f}% (Δ {wr_diff:+.2f}%)")
|
|
print(f"Profit Factor: v9 {pf_v9:.3f} vs v8 {pf_v8:.3f}")
|
|
|
|
print("\n" + "=" * 60)
|
|
if pnl_diff > 0:
|
|
print(f"WINNER: v9 'MA Gap' by ${pnl_diff:.2f} ({pnl_diff_pct:.1f}%)")
|
|
print("v9's faster entries + MA gap context outperform v8's conservative approach")
|
|
else:
|
|
print(f"WINNER: v8 'Sticky Trend' by ${-pnl_diff:.2f} ({-pnl_diff_pct:.1f}%)")
|
|
print("v8's conservative confirmation bars outperform v9's speed")
|
|
print("=" * 60)
|