-- Optimal Exit Level Analysis -- Run this to determine data-driven SL/TP settings -- Execute: docker exec trading-bot-postgres psql -U postgres -d trading_bot_v4 -f /path/to/this/file.sql \echo '==========================================' \echo '1. MFE/MAE DISTRIBUTION ANALYSIS' \echo 'Where do trades actually move?' \echo '==========================================' SELECT direction, COUNT(*) as total_trades, ROUND(AVG("maxFavorableExcursion")::numeric, 2) as avg_best_profit, ROUND(PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY "maxFavorableExcursion")::numeric, 2) as q25_mfe, ROUND(PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY "maxFavorableExcursion")::numeric, 2) as median_mfe, ROUND(PERCENTILE_CONT(0.75) WITHIN GROUP (ORDER BY "maxFavorableExcursion")::numeric, 2) as q75_mfe, ROUND(MAX("maxFavorableExcursion")::numeric, 2) as max_mfe, ROUND(AVG("maxAdverseExcursion")::numeric, 2) as avg_worst_loss, ROUND(PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY "maxAdverseExcursion")::numeric, 2) as q25_mae, ROUND(MIN("maxAdverseExcursion")::numeric, 2) as min_mae FROM "Trade" WHERE "exitReason" IS NOT NULL AND "maxFavorableExcursion" IS NOT NULL AND "maxAdverseExcursion" IS NOT NULL GROUP BY direction; \echo '' \echo 'Interpretation:' \echo '- median_mfe = Where 50% of trades profit reaches (set TP2 here)' \echo '- q75_mfe = Where top 25% reaches (runner territory)' \echo '- q25_mae = Where 25% of worst losses occur (set SL here + buffer)' \echo '' \echo '==========================================' \echo '2. QUALITY SCORE vs PERFORMANCE' \echo 'Do high quality signals move further?' \echo '==========================================' SELECT CASE WHEN "signalQualityScore" >= 80 THEN 'High (80-100)' WHEN "signalQualityScore" >= 70 THEN 'Medium (70-79)' ELSE 'Low (60-69)' END as quality_tier, COUNT(*) as trades, ROUND(AVG("realizedPnL")::numeric, 2) as avg_pnl, ROUND(AVG("maxFavorableExcursion")::numeric, 2) as avg_mfe, ROUND(100.0 * SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) / COUNT(*)::numeric, 1) as win_rate, -- How many went beyond current TP1 (+0.4%)? ROUND(100.0 * SUM(CASE WHEN "maxFavorableExcursion" > 0.4 THEN 1 ELSE 0 END) / COUNT(*)::numeric, 1) as pct_exceeded_tp1, -- How many went beyond current TP2 (+0.7%)? ROUND(100.0 * SUM(CASE WHEN "maxFavorableExcursion" > 0.7 THEN 1 ELSE 0 END) / COUNT(*)::numeric, 1) as pct_exceeded_tp2, -- How many reached runner territory (2%+)? ROUND(100.0 * SUM(CASE WHEN "maxFavorableExcursion" > 2.0 THEN 1 ELSE 0 END) / COUNT(*)::numeric, 1) as pct_runner_potential FROM "Trade" WHERE "signalQualityScore" IS NOT NULL AND "exitReason" IS NOT NULL AND "maxFavorableExcursion" IS NOT NULL GROUP BY quality_tier ORDER BY quality_tier; \echo '' \echo 'Interpretation:' \echo '- If High Quality has higher pct_exceeded_tp2 → Use quality-based tiers' \echo '- If pct_runner_potential > 40% → Runners make sense' \echo '- If pct_runner_potential < 20% → Quick exits better' \echo '' \echo '==========================================' \echo '3. RUNNER POTENTIAL BY EXIT REASON' \echo 'Are we leaving money on the table?' \echo '==========================================' SELECT direction, "exitReason", COUNT(*) as count, ROUND(AVG("maxFavorableExcursion")::numeric, 2) as avg_mfe, ROUND(AVG("realizedPnL")::numeric, 2) as avg_pnl, ROUND(AVG(("maxFavorableExcursion" - ABS(("exitPrice" - "entryPrice") / "entryPrice" * 100)))::numeric, 2) as avg_mfe_vs_exit_gap, SUM(CASE WHEN "maxFavorableExcursion" > 1.5 THEN 1 ELSE 0 END) as moved_beyond_1_5pct, SUM(CASE WHEN "maxFavorableExcursion" > 2.0 THEN 1 ELSE 0 END) as moved_beyond_2pct, SUM(CASE WHEN "maxFavorableExcursion" > 3.0 THEN 1 ELSE 0 END) as moved_beyond_3pct FROM "Trade" WHERE "exitReason" IS NOT NULL AND "maxFavorableExcursion" IS NOT NULL AND "exitPrice" IS NOT NULL GROUP BY direction, "exitReason" ORDER BY direction, count DESC; \echo '' \echo 'Interpretation:' \echo '- avg_mfe_vs_exit_gap = How much profit left on table' \echo '- If TP1/TP2 exits have large gap → Move targets wider' \echo '- If SL exits have positive MFE → SL too tight' \echo '' \echo '==========================================' \echo '4. ATR CORRELATION WITH MOVEMENT' \echo 'Does volatility predict move size?' \echo '==========================================' SELECT CASE WHEN atr < 0.3 THEN 'Low (<0.3%)' WHEN atr < 0.6 THEN 'Medium (0.3-0.6%)' ELSE 'High (>0.6%)' END as atr_bucket, COUNT(*) as trades, ROUND(AVG("maxFavorableExcursion")::numeric, 2) as avg_mfe, ROUND(AVG(ABS("maxAdverseExcursion"))::numeric, 2) as avg_mae_abs, ROUND(AVG(atr)::numeric, 3) as avg_atr, -- MFE to ATR ratio (how many ATRs did price move?) ROUND(AVG("maxFavorableExcursion" / atr)::numeric, 1) as mfe_to_atr_ratio FROM "Trade" WHERE atr IS NOT NULL AND atr > 0 AND "exitReason" IS NOT NULL AND "maxFavorableExcursion" IS NOT NULL GROUP BY atr_bucket ORDER BY avg_atr; \echo '' \echo 'Interpretation:' \echo '- mfe_to_atr_ratio = How many ATRs price typically moves' \echo '- If ratio > 3 → Price moves 3x ATR, use ATR-based targets' \echo '- If ratio ~1-2 → Fixed targets may work better' \echo '' \echo '==========================================' \echo '5. DIRECTION-SPECIFIC PERFORMANCE' \echo 'Should longs vs shorts have different exits?' \echo '==========================================' SELECT direction, COUNT(*) as trades, ROUND(100.0 * SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) / COUNT(*)::numeric, 1) as win_rate, ROUND(AVG("realizedPnL")::numeric, 2) as avg_pnl, ROUND(AVG("maxFavorableExcursion")::numeric, 2) as avg_mfe, ROUND(AVG("maxAdverseExcursion")::numeric, 2) as avg_mae, ROUND(AVG("holdTimeSeconds")::numeric / 60, 1) as avg_hold_minutes, -- How many hit TP vs SL? SUM(CASE WHEN "exitReason" IN ('TP1', 'TP2') THEN 1 ELSE 0 END) as tp_exits, SUM(CASE WHEN "exitReason" IN ('SL', 'SOFT_SL', 'HARD_SL') THEN 1 ELSE 0 END) as sl_exits FROM "Trade" WHERE "exitReason" IS NOT NULL AND "maxFavorableExcursion" IS NOT NULL GROUP BY direction; \echo '' \echo 'Interpretation:' \echo '- If one direction has much higher MFE → Give it wider TP2' \echo '- If one direction has worse MAE → Tighten its SL' \echo '- Different avg_hold_minutes → May need different trailing stops' \echo '' \echo '==========================================' \echo '6. CURRENT TP/SL HIT ANALYSIS' \echo 'How often do current levels trigger?' \echo '==========================================' SELECT CASE WHEN "maxFavorableExcursion" < 0.4 THEN 'Never reached TP1' WHEN "maxFavorableExcursion" >= 0.4 AND "maxFavorableExcursion" < 0.7 THEN 'Reached TP1 only' WHEN "maxFavorableExcursion" >= 0.7 AND "maxFavorableExcursion" < 2.0 THEN 'Reached TP2' ELSE 'Runner territory (2%+)' END as price_action, COUNT(*) as trades, ROUND(100.0 * COUNT(*) / SUM(COUNT(*)) OVER ()::numeric, 1) as pct_of_total, ROUND(AVG("maxFavorableExcursion")::numeric, 2) as avg_mfe, ROUND(AVG("realizedPnL")::numeric, 2) as avg_pnl FROM "Trade" WHERE "exitReason" IS NOT NULL AND "maxFavorableExcursion" IS NOT NULL GROUP BY price_action ORDER BY avg_mfe; \echo '' \echo 'Interpretation:' \echo '- If most trades in "Never reached TP1" → TP1 too wide' \echo '- If most trades in "Runner territory" → We are exiting too early' \echo '- Ideal: ~60% reach TP1, ~30% reach TP2, ~10% runners' \echo '' \echo '==========================================' \echo '7. RECENT PERFORMANCE TREND' \echo 'Is strategy improving over time?' \echo '==========================================' SELECT DATE("createdAt") as trade_date, COUNT(*) as trades, ROUND(AVG("signalQualityScore")::numeric, 1) as avg_score, ROUND(100.0 * SUM(CASE WHEN "realizedPnL" > 0 THEN 1 ELSE 0 END) / COUNT(*)::numeric, 1) as win_rate, ROUND(SUM("realizedPnL")::numeric, 2) as daily_pnl FROM "Trade" WHERE "exitReason" IS NOT NULL AND "createdAt" > NOW() - INTERVAL '30 days' GROUP BY trade_date ORDER BY trade_date DESC LIMIT 10; \echo '' \echo 'Interpretation:' \echo '- Rising avg_score + rising win_rate → Quality scoring working' \echo '- Declining daily_pnl despite good win_rate → Exits may need adjustment' \echo '' \echo '==========================================' \echo 'SUMMARY RECOMMENDATIONS' \echo '==========================================' \echo 'Based on the analysis above, consider:' \echo '' \echo '1. TP1 Setting:' \echo ' - Should be at ~25th percentile of MFE (where most trades reach)' \echo ' - Currently: +0.4%' \echo '' \echo '2. TP2 Setting:' \echo ' - Should be at ~median MFE (where 50% reach)' \echo ' - Currently: +0.7% (with ATR-based dynamic up to 3%)' \echo '' \echo '3. Runner Strategy:' \echo ' - Only use if >40% trades reach "Runner territory"' \echo ' - Currently: 25% position with ATR trailing' \echo '' \echo '4. Stop Loss:' \echo ' - Should be at ~25th percentile of MAE (protect from worst 25%)' \echo ' - Currently: -1.5% soft, -2.5% hard' \echo '' \echo '5. Quality-Based Tiers:' \echo ' - Only implement if High Quality tier has significantly better metrics' \echo ' - Check pct_runner_potential difference across tiers' \echo ''