fix: Database-first cluster status detection + Stop button clarification
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
This commit is contained in:
142
scripts/compare_v8_v9.py
Normal file
142
scripts/compare_v8_v9.py
Normal file
@@ -0,0 +1,142 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user