Files
trading_bot_v4/scripts/trade_analysis.py
mindesbunister cc56b72df2 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
2025-11-30 22:23:01 +01:00

219 lines
8.2 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Trade-Level Analysis - Find Patterns in Losing Trades
Purpose: Identify what conditions lead to losses
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from typing import List, Dict
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.append(str(PROJECT_ROOT))
import pandas as pd
from pathlib import Path
from tqdm import tqdm
from backtester.data_loader import load_csv
from backtester.indicators.money_line import MoneyLineInputs, money_line_signals
from backtester.simulator import TradeConfig, simulate_money_line, SimulatedTrade
def analyze_trades(trades: List[SimulatedTrade]) -> Dict:
"""Deep analysis of trade performance."""
if not trades:
return {"error": "No trades to analyze"}
winners = [t for t in trades if t.pnl > 0]
losers = [t for t in trades if t.pnl < 0]
print("\n" + "="*80)
print("TRADE PERFORMANCE BREAKDOWN")
print("="*80)
print(f"Total Trades: {len(trades)}")
print(f"Winners: {len(winners)} ({len(winners)/len(trades)*100:.1f}%)")
print(f"Losers: {len(losers)} ({len(losers)/len(trades)*100:.1f}%)")
if winners:
print(f"\nWinner Stats:")
print(f" Avg PnL: ${sum(t.pnl for t in winners)/len(winners):.2f}")
print(f" Max Win: ${max(t.pnl for t in winners):.2f}")
print(f" Avg MFE: {sum(t.mfe for t in winners)/len(winners):.2f}%")
print(f" Avg Bars: {sum(t.bars_held for t in winners)/len(winners):.1f}")
if losers:
print(f"\nLoser Stats:")
print(f" Avg PnL: ${sum(t.pnl for t in losers)/len(losers):.2f}")
print(f" Max Loss: ${min(t.pnl for t in losers):.2f}")
print(f" Avg MAE: {sum(t.mae for t in losers)/len(losers):.2f}%")
print(f" Avg Bars: {sum(t.bars_held for t in losers)/len(losers):.1f}")
# Total P&L stats
total_pnl = sum(t.pnl for t in trades)
print(f"\nTotal P&L: ${total_pnl:.2f}")
print(f"Avg Trade: ${total_pnl/len(trades):.2f}")
# MAE/MFE Analysis
print("\n" + "="*80)
print("MAE/MFE ANALYSIS (Max Adverse/Favorable Excursion)")
print("="*80)
# Winners that gave back profit
if winners:
large_mfe_winners = [t for t in winners if t.mfe > 2.0]
if large_mfe_winners:
avg_giveback = sum(t.mfe - (t.pnl / 8100.0 * 100) for t in large_mfe_winners) / len(large_mfe_winners)
print(f"Winners with >2% MFE: {len(large_mfe_winners)}")
print(f" Average profit given back: {avg_giveback:.2f}%")
print(f" 💡 Consider: Wider trailing stop or earlier TP2 trigger")
# Losers that could have been winners
if losers:
positive_mfe_losers = [t for t in losers if t.mfe > 0.5]
if positive_mfe_losers:
avg_peak = sum(t.mfe for t in positive_mfe_losers) / len(positive_mfe_losers)
print(f"\nLosers that reached >0.5% profit: {len(positive_mfe_losers)}")
print(f" Average peak profit: {avg_peak:.2f}%")
print(f" 💡 Consider: Tighter TP1 or better stop management")
# Direction analysis
print("\n" + "="*80)
print("DIRECTION ANALYSIS")
print("="*80)
longs = [t for t in trades if t.direction == 'long']
shorts = [t for t in trades if t.direction == 'short']
if longs:
long_wr = sum(1 for t in longs if t.pnl > 0) / len(longs) * 100
long_pnl = sum(t.pnl for t in longs)
print(f"LONGS: {len(longs)} trades, {long_wr:.1f}% WR, ${long_pnl:.2f} PnL")
if shorts:
short_wr = sum(1 for t in shorts if t.pnl > 0) / len(shorts) * 100
short_pnl = sum(t.pnl for t in shorts)
print(f"SHORTS: {len(shorts)} trades, {short_wr:.1f}% WR, ${short_pnl:.2f} PnL")
if longs and shorts:
if long_wr > short_wr + 10:
print(f" 💡 LONGs outperform: Consider quality threshold adjustment")
elif short_wr > long_wr + 10:
print(f" 💡 SHORTs outperform: Consider quality threshold adjustment")
return {
"total": len(trades),
"winners": len(winners),
"losers": len(losers),
"win_rate": len(winners) / len(trades) * 100,
"total_pnl": total_pnl
}
def find_exit_opportunities(trades: List[SimulatedTrade]):
"""Analyze if different exit strategy could improve results."""
print("\n" + "="*80)
print("EXIT STRATEGY ANALYSIS")
print("="*80)
# Current strategy stats
tp1_hits = sum(1 for t in trades if t.exit_type == 'tp1')
tp2_hits = sum(1 for t in trades if t.exit_type == 'tp2')
sl_hits = sum(1 for t in trades if t.exit_type == 'sl')
max_bars = sum(1 for t in trades if t.exit_type == 'max_bars')
print(f"Current Exit Distribution:")
print(f" TP1: {tp1_hits} ({tp1_hits/len(trades)*100:.1f}%)")
print(f" TP2: {tp2_hits} ({tp2_hits/len(trades)*100:.1f}%)")
print(f" SL: {sl_hits} ({sl_hits/len(trades)*100:.1f}%)")
print(f" Max Bars: {max_bars} ({max_bars/len(trades)*100:.1f}%)")
# Simulate tighter TP1
could_take_tp1 = [t for t in trades if t.mfe >= 0.5 and t.pnl < 0]
if could_take_tp1:
saved_pnl = len(could_take_tp1) * 0.005 * 8100 # 0.5% profit instead of loss
print(f"\n💡 Tighter TP1 (0.5%): Would save {len(could_take_tp1)} losing trades")
print(f" Estimated improvement: +${saved_pnl:.2f}")
# Check if runners are worth it
tp2_trades = [t for t in trades if t.exit_type == 'tp2']
if tp2_trades:
tp2_avg = sum(t.pnl for t in tp2_trades) / len(tp2_trades)
print(f"\nTP2/Runner Performance:")
print(f" {len(tp2_trades)} trades reached TP2")
print(f" Average TP2 profit: ${tp2_avg:.2f}")
if tp2_avg < 100:
print(f" 💡 Runners not adding much value - consider 100% TP1 close")
def compare_to_baseline(csv_path: Path, symbol: str, best_inputs: MoneyLineInputs):
"""Compare best config to baseline."""
print("\n" + "="*80)
print("BASELINE COMPARISON")
print("="*80)
print("\nLoading data and running simulations...")
data_slice = load_csv(Path(csv_path), symbol, "5m")
df = data_slice.data
config = TradeConfig(position_size=8100.0, max_bars_per_trade=288)
baseline = MoneyLineInputs()
print("\nRunning baseline config...")
baseline_result = simulate_money_line(df, symbol, baseline, config)
print(f" Trades: {len(baseline_result.trades)}")
print(f" PnL: ${baseline_result.total_pnl:.2f}")
print(f" WR: {baseline_result.win_rate:.1f}%")
print("\nRunning best sweep config...")
best_result = simulate_money_line(df, symbol, best_inputs, config)
print(f" Trades: {len(best_result.trades)}")
print(f" PnL: ${best_result.total_pnl:.2f}")
print(f" WR: {best_result.win_rate:.1f}%")
improvement = best_result.total_pnl - baseline_result.total_pnl
print(f"\nImprovement: ${improvement:.2f} ({improvement/baseline_result.total_pnl*100:.1f}%)")
return best_result.trades
def main():
parser = argparse.ArgumentParser(description="Trade-level analysis")
parser.add_argument("--csv", type=Path, required=True, help="Path to OHLCV CSV")
parser.add_argument("--symbol", default="SOL-PERP", help="Symbol name")
args = parser.parse_args()
print("="*80)
print("V9 TRADE ANALYSIS")
print("="*80)
# Use "best" config from sweep
best_inputs = MoneyLineInputs(
flip_threshold_percent=0.6,
ma_gap_threshold=0.35,
momentum_min_adx=23.0,
momentum_long_max_pos=70.0,
momentum_short_min_pos=25.0,
cooldown_bars=2,
momentum_spacing=3,
momentum_cooldown=2
)
trades = compare_to_baseline(args.csv, args.symbol, best_inputs)
analyze_trades(trades)
find_exit_opportunities(trades)
print("\n" + "="*80)
print("ACTIONABLE RECOMMENDATIONS")
print("="*80)
print("Based on this analysis, the next optimization steps should focus on:")
print("1. Exit strategy improvements (TP1/TP2 levels)")
print("2. Direction-specific quality thresholds")
print("3. Stop loss positioning")
print("4. Entry timing refinement")
if __name__ == "__main__":
main()