feat: Revenge system enhancements #4 and #10 - IMPLEMENTED

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
This commit is contained in:
mindesbunister
2025-11-27 08:08:37 +01:00
parent 2238261dfe
commit ceb84c3bc1
6 changed files with 586 additions and 33 deletions

View File

@@ -26,6 +26,7 @@ export interface ActiveTrade {
atrAtEntry?: number // ATR value at entry for ATR-based trailing stop
adxAtEntry?: number // ADX value at entry for trend strength multiplier
signalQualityScore?: number // Quality score for stop hunt tracking
signalSource?: string // Trade source: 'tradingview', 'manual', 'stop_hunt_revenge'
// Targets
stopLossPrice: number
@@ -1551,6 +1552,26 @@ export class PositionManager {
maxAdversePrice: trade.maxAdversePrice,
})
console.log('💾 Trade saved to database')
// 🔥 REVENGE OUTCOME TRACKING (Enhancement #4 - Nov 27, 2025)
// If this was a revenge trade, record the outcome in StopHunt table
if (trade.signalSource === 'stop_hunt_revenge') {
try {
const { getStopHuntTracker } = await import('./stop-hunt-tracker')
const tracker = getStopHuntTracker()
await tracker.updateRevengeOutcome({
revengeTradeId: trade.id,
outcome: reason as string,
pnl: trade.realizedPnL,
failedReason: reason === 'SL' ? 'stopped_again' : undefined
})
console.log(`🔥 Revenge outcome recorded: ${reason} (P&L: $${trade.realizedPnL.toFixed(2)})`)
} catch (revengeError) {
console.error('❌ Failed to record revenge outcome:', revengeError)
// Don't fail trade closure if revenge tracking fails
}
}
} catch (dbError) {
console.error('❌ Failed to save trade exit to database:', dbError)
// Don't fail the close if database fails

View File

@@ -34,6 +34,17 @@ interface StopHuntRecord {
revengeExpiresAt: Date
highestPriceAfterStop: number | null
lowestPriceAfterStop: number | null
// Zone tracking persistence (Enhancement #10)
firstCrossTime: Date | null
lowestInZone: number | null
highestInZone: number | null
zoneResetCount: number
// Revenge outcome tracking (Enhancement #4)
revengeOutcome: string | null
revengePnL: number | null
revengeFailedReason: string | null
}
let trackerInstance: StopHuntTracker | null = null
@@ -197,7 +208,7 @@ export class StopHuntTracker {
})
// Check revenge conditions (now requires sustained move, not just wick)
const shouldRevenge = this.shouldExecuteRevenge(stopHunt, currentPrice)
const shouldRevenge = await this.shouldExecuteRevenge(stopHunt, currentPrice)
if (shouldRevenge) {
console.log(`🔥 REVENGE CONDITIONS MET: ${stopHunt.symbol} ${stopHunt.direction.toUpperCase()}`)
@@ -212,9 +223,14 @@ export class StopHuntTracker {
/**
* Determine if revenge entry conditions are met
*
* ENHANCED (Nov 27, 2025): Database-persisted zone tracking
* - OLD: In-memory metadata lost on container restart
* - NEW: Persists firstCrossTime to database, survives restarts
* - Tracks zone entry/exit behavior for analysis
*
* ENHANCED (Nov 26, 2025): Candle close confirmation
* - OLD: Enters immediately when price crosses entry (gets stopped by retest)
* - NEW: Requires price to STAY below/above entry for 60+ seconds
* - NEW: Requires price to STAY below/above entry for 90+ seconds
* - This simulates "candle close" confirmation without needing TradingView data
* - Prevents entering on wicks that bounce back
*
@@ -222,37 +238,41 @@ export class StopHuntTracker {
* - Original SHORT entry: $136.32, stopped at $138.00
* - Price wicked to $136.20 then bounced to $137.50
* - OLD system: Would enter $136.32, stop at $137.96, LOSE AGAIN
* - NEW system: Requires price below $136.32 for 60s before entry
* - NEW system: Requires price below $136.32 for 90s before entry
* - Result: Enters safely after confirmation, rides to $144.50 (+$530!)
*/
private shouldExecuteRevenge(stopHunt: StopHuntRecord, currentPrice: number): boolean {
private async shouldExecuteRevenge(stopHunt: StopHuntRecord, currentPrice: number): Promise<boolean> {
const { direction, stopHuntPrice, originalEntryPrice } = stopHunt
// Track how long price has been in revenge zone
const now = Date.now()
const metadata = (stopHunt as any).revengeMetadata || {}
if (direction === 'long') {
// Long stopped out above entry → Revenge when price drops back below entry
const crossedBackDown = currentPrice < originalEntryPrice * 0.995 // 0.5% buffer
if (crossedBackDown) {
// Price is in revenge zone - track duration
if (!metadata.firstCrossTime) {
metadata.firstCrossTime = now
metadata.lowestInZone = currentPrice
// Update metadata in memory (not persisting to DB to avoid spam)
;(stopHunt as any).revengeMetadata = metadata
console.log(` ⏱️ LONG revenge zone entered at ${currentPrice.toFixed(2)}, waiting for 60s confirmation...`)
// Price is in revenge zone - persist to database
if (!stopHunt.firstCrossTime) {
await this.prisma.stopHunt.update({
where: { id: stopHunt.id },
data: {
firstCrossTime: new Date(),
lowestInZone: currentPrice,
}
})
console.log(` ⏱️ LONG revenge zone entered at ${currentPrice.toFixed(2)}, waiting for 90s confirmation...`)
return false
}
// Update lowest price in zone
metadata.lowestInZone = Math.min(metadata.lowestInZone, currentPrice)
;(stopHunt as any).revengeMetadata = metadata
const currentLowest = Math.min(stopHunt.lowestInZone || currentPrice, currentPrice)
await this.prisma.stopHunt.update({
where: { id: stopHunt.id },
data: { lowestInZone: currentLowest }
})
// Check if we've been in zone for 90+ seconds (1.5 minutes - partial candle confirmation)
const timeInZone = now - metadata.firstCrossTime
// Check if we've been in zone for 90+ seconds (1.5 minutes)
const timeInZone = now - stopHunt.firstCrossTime.getTime()
if (timeInZone >= 90000) { // 90 seconds = 1.5 minutes
console.log(` ✅ LONG revenge: Price held below entry for ${(timeInZone/60000).toFixed(1)}min, confirmed!`)
console.log(` Entry ${originalEntryPrice.toFixed(2)} → Current ${currentPrice.toFixed(2)}`)
@@ -262,10 +282,17 @@ export class StopHuntTracker {
return false
}
} else {
// Price left revenge zone - reset timer
if (metadata.firstCrossTime) {
// Price left revenge zone - reset timer and increment counter
if (stopHunt.firstCrossTime) {
console.log(` ❌ LONG revenge: Price bounced back up to ${currentPrice.toFixed(2)}, resetting timer`)
;(stopHunt as any).revengeMetadata = {}
await this.prisma.stopHunt.update({
where: { id: stopHunt.id },
data: {
firstCrossTime: null,
lowestInZone: null,
zoneResetCount: { increment: 1 }
}
})
}
return false
}
@@ -274,21 +301,28 @@ export class StopHuntTracker {
const crossedBackUp = currentPrice > originalEntryPrice * 1.005 // 0.5% buffer
if (crossedBackUp) {
// Price is in revenge zone - track duration
if (!metadata.firstCrossTime) {
metadata.firstCrossTime = now
metadata.highestInZone = currentPrice
;(stopHunt as any).revengeMetadata = metadata
console.log(` ⏱️ SHORT revenge zone entered at ${currentPrice.toFixed(2)}, waiting for 60s confirmation...`)
// Price is in revenge zone - persist to database
if (!stopHunt.firstCrossTime) {
await this.prisma.stopHunt.update({
where: { id: stopHunt.id },
data: {
firstCrossTime: new Date(),
highestInZone: currentPrice,
}
})
console.log(` ⏱️ SHORT revenge zone entered at ${currentPrice.toFixed(2)}, waiting for 90s confirmation...`)
return false
}
// Update highest price in zone
metadata.highestInZone = Math.max(metadata.highestInZone, currentPrice)
;(stopHunt as any).revengeMetadata = metadata
const currentHighest = Math.max(stopHunt.highestInZone || currentPrice, currentPrice)
await this.prisma.stopHunt.update({
where: { id: stopHunt.id },
data: { highestInZone: currentHighest }
})
// Check if we've been in zone for 90+ seconds (1.5 minutes - fast but filters wicks)
const timeInZone = now - metadata.firstCrossTime
// Check if we've been in zone for 90+ seconds (1.5 minutes)
const timeInZone = now - stopHunt.firstCrossTime.getTime()
if (timeInZone >= 90000) { // 90 seconds = 1.5 minutes
console.log(` ✅ SHORT revenge: Price held above entry for ${(timeInZone/60000).toFixed(1)}min, confirmed!`)
console.log(` Entry ${originalEntryPrice.toFixed(2)} → Current ${currentPrice.toFixed(2)}`)
@@ -298,10 +332,17 @@ export class StopHuntTracker {
return false
}
} else {
// Price left revenge zone - reset timer
if (metadata.firstCrossTime) {
// Price left revenge zone - reset timer and increment counter
if (stopHunt.firstCrossTime) {
console.log(` ❌ SHORT revenge: Price dropped back to ${currentPrice.toFixed(2)}, resetting timer`)
;(stopHunt as any).revengeMetadata = {}
await this.prisma.stopHunt.update({
where: { id: stopHunt.id },
data: {
firstCrossTime: null,
highestInZone: null,
zoneResetCount: { increment: 1 }
}
})
}
return false
}
@@ -414,6 +455,48 @@ Reversal Confirmed: Price crossed back through entry
}
}
/**
* Update revenge trade outcome when it closes
* Called by Position Manager when revenge trade exits
*/
async updateRevengeOutcome(params: {
revengeTradeId: string
outcome: string // "TP1", "TP2", "SL", "TRAILING_SL"
pnl: number
failedReason?: string
}): Promise<void> {
try {
// Find stop hunt by revenge trade ID
const stopHunt = await this.prisma.stopHunt.findFirst({
where: { revengeTradeId: params.revengeTradeId }
})
if (!stopHunt) {
console.log(`⚠️ No stop hunt found for revenge trade ${params.revengeTradeId}`)
return
}
await this.prisma.stopHunt.update({
where: { id: stopHunt.id },
data: {
revengeOutcome: params.outcome,
revengePnL: params.pnl,
revengeFailedReason: params.failedReason || null,
}
})
const emoji = params.outcome.includes('TP') ? '✅' : '❌'
console.log(`${emoji} REVENGE OUTCOME: ${params.outcome} (${params.pnl >= 0 ? '+' : ''}$${params.pnl.toFixed(2)})`)
if (params.failedReason) {
console.log(` Reason: ${params.failedReason}`)
}
} catch (error) {
console.error('❌ Error updating revenge outcome:', error)
}
}
/**
* Expire stop hunts that are past their 4-hour window
*/