Enhancement #4: Failed Revenge Tracking - Added 3 database fields: revengeOutcome, revengePnL, revengeFailedReason - Added updateRevengeOutcome() method in stop-hunt-tracker.ts - Position Manager hooks revenge trade closes, records outcome - Enables data-driven analysis of revenge success rate Enhancement #10: Metadata Persistence - Added 4 database fields: firstCrossTime, lowestInZone, highestInZone, zoneResetCount - Migrated 90-second zone tracking from in-memory to database - Rewrote shouldExecuteRevenge() with database persistence - Container restarts now preserve exact zone tracking state Technical Details: - Prisma schema updated with 7 new StopHunt fields - Added signalSource field to ActiveTrade interface - All zone metadata persisted in real-time to database - Build verified successful (no TypeScript errors) Files Changed: - prisma/schema.prisma (StopHunt model + index) - lib/trading/stop-hunt-tracker.ts (DB persistence + outcome tracking) - lib/trading/position-manager.ts (revenge hook + interface) - docs/REVENGE_ENHANCEMENTS_EXPLAINED.md (comprehensive guide) Pending User Decision: - Enhancement #1: ADX confirmation (3 options explained in docs) - Enhancement #6: SL distance validation (2× ATR recommended) Status: Ready for deployment after Prisma migration Date: Nov 27, 2025
12 KiB
Revenge Trade System - Enhancement Explanations
Status: Enhancements #4 and #10 IMPLEMENTED (Nov 27, 2025)
Pending: Enhancements #1 and #6 awaiting user decision
✅ IMPLEMENTED: Enhancement #4 - Failed Revenge Tracking
Problem Solved: System had no way to analyze revenge trade success rate or learn from failures.
Implementation:
-
Database Fields Added:
revengeOutcome(String): TP1, TP2, SL, TRAILING_SL, manual, emergencyrevengePnL(Float): Actual P&L from revenge traderevengeFailedReason(String): Why it failed (e.g., "stopped_again", "manually_closed")
-
Code Changes:
stop-hunt-tracker.ts: AddedupdateRevengeOutcome()helper methodposition-manager.ts: Added hook inexecuteExit()to record outcome when revenge trade closes- Automatic detection: Checks
signalSource === 'stop_hunt_revenge'
How It Works:
// When revenge trade closes (TP1, TP2, SL, etc.)
if (trade.signalSource === 'stop_hunt_revenge') {
await tracker.updateRevengeOutcome({
revengeTradeId: trade.id,
outcome: 'SL', // or 'TP1', 'TP2', etc.
pnl: -138.35, // Actual profit/loss
failedReason: 'stopped_again' // If outcome = SL
})
}
Benefits:
- Analytics: Query database to see revenge win rate vs regular trades
- Learning: Identify which conditions lead to successful revenge (ADX, time of day, etc.)
- Optimization: Data-driven improvements to revenge system parameters
Example SQL Analysis:
-- Revenge trade success rate
SELECT
COUNT(*) as total_revenge_trades,
SUM(CASE WHEN "revengeOutcome" IN ('TP1', 'TP2') THEN 1 ELSE 0 END) as wins,
SUM(CASE WHEN "revengeOutcome" = 'SL' THEN 1 ELSE 0 END) as losses,
ROUND(AVG("revengePnL"), 2) as avg_pnl,
ROUND(SUM("revengePnL"), 2) as total_pnl
FROM "StopHunt"
WHERE "revengeExecuted" = true;
-- Compare original stop vs revenge outcome
SELECT
"symbol",
"direction",
"originalQualityScore",
"stopLossAmount" as original_loss,
"revengePnL" as revenge_result,
("stopLossAmount" + "revengePnL") as net_result
FROM "StopHunt"
WHERE "revengeExecuted" = true
ORDER BY net_result DESC;
✅ IMPLEMENTED: Enhancement #10 - Metadata Persistence
Problem Solved: Container restarts lost in-memory zone tracking data (firstCrossTime, high/low tracking, reset count).
Implementation:
-
Database Fields Added:
firstCrossTime(DateTime): When price first entered revenge zonelowestInZone(Float): Lowest price while in zone (for analysis)highestInZone(Float): Highest price while in zone (for analysis)zoneResetCount(Int): How many times price left and re-entered zone
-
Code Changes:
- Rewrote
shouldExecuteRevenge()to use database persistence instead of in-memoryrevengeMetadata - All zone tracking now written to database in real-time
- Container restart = continues tracking from database state
- Rewrote
How It Works (90-Second Confirmation):
// BEFORE (LOST ON RESTART):
revengeMetadata.set(stopHunt.id, {
firstCrossTime: Date.now(),
lowestInZone: currentPrice,
highestInZone: currentPrice,
resetCount: 0
})
// AFTER (SURVIVES RESTART):
await prisma.stopHunt.update({
where: { id: stopHunt.id },
data: {
firstCrossTime: new Date(),
lowestInZone: currentPrice,
highestInZone: currentPrice,
zoneResetCount: 0
}
})
90-Second Zone Logic (Persisted):
- First Cross: Price enters revenge zone → record
firstCrossTimeto DB - Sustained: Every 30s check: Still in zone? Update
lowestInZone/highestInZone - Timer Check: If in zone for 90+ seconds → Execute revenge
- Reset: If price leaves zone → clear
firstCrossTime, incrementzoneResetCount - Container Restart: Read from DB, continue tracking from exact state
Benefits:
- Reliability: No data loss on container restarts (bot can restart during 90s window)
- Analysis: Zone behavior patterns visible in database (how often resets, price extremes)
- Debugging: Full audit trail of zone entry/exit timestamps
❓ PENDING: Enhancement #1 - ADX Confirmation
Problem: System doesn't validate trend strength before revenge entry. Could re-enter when trend already weakened (ADX dropped from 26 to 15).
Two Options for Implementation:
Option A: Fetch Fresh ADX from Cache (RECOMMENDED)
How It Works:
// In shouldExecuteRevenge(), before executing revenge:
const cache = getMarketDataCache()
const freshData = cache.get(stopHunt.symbol)
if (!freshData || !freshData.adx) {
console.log('⚠️ No fresh ADX data, skipping validation')
// Proceed without ADX check OR block revenge (user choice)
} else if (freshData.adx < 20) {
console.log(`⚠️ ADX weakened: ${freshData.adx.toFixed(1)} < 20, blocking revenge`)
return false // Don't revenge weak trends
} else {
console.log(`✅ ADX confirmation: ${freshData.adx.toFixed(1)} (strong trend, revenge approved)`)
// Continue with revenge execution
}
Data Source:
- TradingView sends market data every 1-5 minutes to
/api/trading/market-data - Cache stores: ADX, ATR, RSI, volumeRatio, pricePosition, currentPrice
- Cache expiry: 5 minutes (fresh data or null)
Pros:
- ✅ Real-time trend validation (what's happening NOW, not 2 hours ago)
- ✅ Adapts to changing conditions (trend weakening = skip revenge)
- ✅ Uses existing infrastructure (market-data-cache.ts already working)
- ✅ Same data source as re-entry analytics (proven reliable)
Cons:
- ❌ Requires TradingView sending data reliably every 1-5min
- ❌ Cache could be stale if TradingView alert fails
- ❌ Need fallback logic when cache empty
Fallback Strategy:
if (!freshData || freshData.timestamp < Date.now() - 300000) {
// No data or stale (>5min old)
// Option 1: Proceed without ADX check (less strict)
// Option 2: Block revenge (more conservative)
// Option 3: Use originalADX threshold (hybrid)
}
Option B: Compare to Original ADX (SIMPLER)
How It Works:
// Just validate original signal had strong ADX
if (stopHunt.originalADX && stopHunt.originalADX < 20) {
console.log(`⚠️ Original ADX was weak (${stopHunt.originalADX.toFixed(1)}), blocking revenge`)
return false
}
// No current ADX check - assumes trend still valid
Pros:
- ✅ Simple, no external dependencies
- ✅ Always available (stored at stop-out time)
- ✅ Filters weak original signals (quality control)
Cons:
- ❌ Doesn't validate current trend strength
- ❌ Revenge could enter when trend already died
- ❌ Less sophisticated than real-time validation
Use Case:
- Best as fallback when fresh ADX unavailable
- Or as minimum filter (block quality <85 + ADX <20 at origin)
Recommendation: Hybrid Approach
Best of Both Worlds:
// 1. Filter at recording time (don't even create StopHunt if ADX was weak)
if (trade.adxAtEntry < 20) {
console.log('⚠️ ADX too weak for revenge system, skipping')
return // Don't record stop hunt
}
// 2. Validate at revenge time (check current ADX before entering)
const freshData = cache.get(stopHunt.symbol)
if (freshData && freshData.adx < 20) {
console.log(`⚠️ ADX weakened: ${freshData.adx} < 20, waiting...`)
return false
}
// 3. Fallback if no fresh data (use original as proxy)
if (!freshData && stopHunt.originalADX < 23) {
console.log('⚠️ No fresh ADX, original was borderline, being conservative')
return false
}
Effect:
- Layer 1: Block weak signals at origin (ADX <20 at stop-out)
- Layer 2: Real-time validation before revenge entry (current ADX <20)
- Layer 3: Conservative fallback when no fresh data (original <23)
❓ PENDING: Enhancement #6 - Stop Loss Distance Validation
Problem: Revenge enters too close to original stop-loss price, gets stopped by same wick/volatility.
Example Scenario:
- Original LONG entry: $141.37
- Stop-out: $142.48 (1.11 points above entry)
- Revenge entry: $141.50 (0.98 points above stop-out)
- Problem: Only 0.13 points between revenge entry and stop zone = instant re-stop
Solution: Require Minimum Safe Distance
// In shouldExecuteRevenge(), before executing revenge:
// Calculate distance from entry to stop zone
const slDistance = stopHunt.direction === 'long'
? currentPrice - stopHunt.stopHuntPrice // How much room BELOW entry
: stopHunt.stopHuntPrice - currentPrice // How much room ABOVE entry
// Require minimum 2× ATR breathing room
const minSafeDistance = stopHunt.originalATR * 2.0 // Conservative multiplier
if (slDistance < minSafeDistance) {
console.log(`⚠️ Too close to stop zone:`)
console.log(` Distance: $${slDistance.toFixed(2)}`)
console.log(` Required: $${minSafeDistance.toFixed(2)} (2× ATR)`)
console.log(` Waiting for deeper reversal...`)
return false // Don't revenge yet
}
console.log(`✅ Safe distance confirmed: $${slDistance.toFixed(2)} > $${minSafeDistance.toFixed(2)}`)
Visual Example (SOL @ ATR 0.60):
Original Stop: $142.48 (LONG stopped out)
↑
| 2× ATR = $1.20 minimum distance
↓
Safe Zone: $141.28 or lower (revenge approved)
Current: $141.50 ❌ TOO CLOSE (only $0.78 away)
Current: $141.00 ✅ SAFE (1.48 away > 1.20 required)
Multiplier Options:
- 1.5× ATR: Aggressive (tight entry, higher re-stop risk)
- 2.0× ATR: Balanced (RECOMMENDED - proven with trailing stop system)
- 2.5× ATR: Conservative (waits for deep reversal, may miss some)
Trade-offs:
- ✅ Reduces re-stop-outs: Revenge enters with breathing room
- ✅ ATR-adaptive: SOL (high vol) vs BTC (low vol) automatically adjusted
- ❌ Misses shallow reversals: Some revenge opportunities skip if not deep enough
- ❌ Tighter windows: 4-hour revenge window = less time for deep reversal
Real-World Test (Nov 26 incident):
- Original stop: $138.00 (LONG)
- Immediate reversal: $136.32 (1.68 distance = 2.8× ATR)
- Result: Would PASS validation ✅
- Retest bounce: $137.50 (0.50 distance = 0.83× ATR)
- Result: Would BLOCK ❌ (prevented re-stop-out)
Data to Monitor:
-- After implementing: Track how often we block vs success rate
SELECT
COUNT(*) as total_opportunities,
SUM(CASE WHEN blocked_by_distance THEN 1 ELSE 0 END) as blocked_count,
SUM(CASE WHEN revengeExecuted THEN 1 ELSE 0 END) as executed_count,
AVG(CASE WHEN revengeExecuted THEN revengePnL ELSE NULL END) as avg_pnl
FROM stop_hunt_analysis_view;
Decision Required
Enhancement #1 (ADX Confirmation):
- Option A: Fetch fresh ADX from cache (real-time validation)
- Option B: Use original ADX only (simple filter)
- Option C: Hybrid approach (both layers)
Enhancement #6 (SL Distance Validation):
- Implement with 2.0× ATR multiplier (recommended)
- Use different multiplier: _____× ATR
- Skip this enhancement (too restrictive)
Questions for User:
- ADX source: Fresh cache vs original vs hybrid?
- TradingView data flow: Are market-data alerts reliable (1-5min frequency)?
- Distance multiplier: 2.0× ATR acceptable or adjust?
- Risk tolerance: Prefer catching more revenge trades (looser filters) or better win rate (stricter filters)?
Next Steps After Decision
- If approved: Implement #1 and #6 with chosen options
- Deploy: Docker rebuild with all 4 enhancements
- Monitor: First quality 85+ stop-out will test all systems
- Analyze: After 10-20 revenge trades, review success rate and adjust
Files Modified (Ready to Deploy):
- ✅
prisma/schema.prisma(7 new fields) - ✅
lib/trading/stop-hunt-tracker.ts(DB persistence + outcome tracking) - ✅
lib/trading/position-manager.ts(revenge outcome hook + signalSource field) - ⏳ Build successful, awaiting user decision on #1 and #6