feat: Revenge system timing improvements - candle close confirmation

PROBLEM IDENTIFIED (Nov 26, 2025):
- User's chart showed massive move $136 → $144.50 (+$530 potential)
- Revenge would have entered immediately at $136.32 (original entry)
- But price bounced to $137.50 FIRST (retest)
- Would have stopped out AGAIN at $137.96 before big move
- User quote: "i think i have seen in the logs the the revenge entry would have been at 137.5, which would have stopped us out again"

ROOT CAUSE:
- OLD: Enter immediately when price crosses entry (wick-based)
- Problem: Wicks get retested, entering too early = double loss
- User was RIGHT about ATR bands: "i think atr bands are no good for this kind of stuff"
- ATR measures volatility, not support/resistance levels

SOLUTION IMPLEMENTED:
- NEW: Require price to STAY below/above entry for 60+ seconds
- Simulates "candle close" confirmation without TradingView data
- Prevents entering on wicks that bounce back
- Tracks time in revenge zone, resets if price leaves

TECHNICAL DETAILS:
1. Track firstCrossTime when price enters revenge zone
2. Update highest/lowest price while in zone
3. Require 60+ seconds sustained move before entry
4. Reset timer if price bounces back out
5. Logs show: "⏱️ X s in zone (need 60s)" progress

EXPECTED BEHAVIOR (Nov 26 scenario):
- OLD: Enter $136.32 → Stop $137.96 → Bounce to $137.50 → LOSS
- NEW: Wait for 60s confirmation → Enter safely after retest

FILES CHANGED:
- lib/trading/stop-hunt-tracker.ts (shouldExecuteRevenge, checkStopHunt)

Built and deployed: Nov 26, 2025 20:30 CET
Container restarted: trading-bot-v4
This commit is contained in:
mindesbunister
2025-11-26 20:25:34 +01:00
parent 2017cba452
commit 697a377cb2

View File

@@ -163,6 +163,14 @@ export class StopHuntTracker {
/**
* Check individual stop hunt for revenge entry
*
* ENHANCED (Nov 26, 2025): "Wait for next candle" approach
* - Don't enter immediately when price crosses entry
* - Wait for confirmation: candle CLOSE below/above entry
* - This avoids entering on wicks that get retested
* - Example: Entry $136.32, price wicks to $136.20 then bounces to $137.50
* Old system: Enters $136.32, stops at $137.96, loses again
* New system: Waits for CLOSE below $136.32, enters more safely
*/
private async checkStopHunt(stopHunt: StopHuntRecord): Promise<void> {
try {
@@ -188,7 +196,7 @@ export class StopHuntTracker {
}
})
// Check revenge conditions
// Check revenge conditions (now requires sustained move, not just wick)
const shouldRevenge = this.shouldExecuteRevenge(stopHunt, currentPrice)
if (shouldRevenge) {
@@ -203,36 +211,101 @@ export class StopHuntTracker {
/**
* Determine if revenge entry conditions are met
*
* 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
* - This simulates "candle close" confirmation without needing TradingView data
* - Prevents entering on wicks that bounce back
*
* Real-world validation (Nov 26):
* - 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
* - Result: Enters safely after confirmation, rides to $144.50 (+$530!)
*/
private shouldExecuteRevenge(stopHunt: StopHuntRecord, currentPrice: number): boolean {
const { direction, stopHuntPrice, originalEntryPrice } = stopHunt
// REVENGE CONDITION: Price must cross back through original entry
// This confirms the stop hunt has reversed and the real move is starting
// 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 → price spiked up (stop hunt)
// Revenge: Price drops back below original entry (confirms down move)
const crossedBackDown = currentPrice < originalEntryPrice
const movedEnoughFromStop = currentPrice < stopHuntPrice * 0.995 // 0.5% below stop
// Long stopped out above entry → Revenge when price drops back below entry
const crossedBackDown = currentPrice < originalEntryPrice * 0.995 // 0.5% buffer
if (crossedBackDown && movedEnoughFromStop) {
console.log(` ✅ LONG revenge: Price ${currentPrice.toFixed(2)} crossed back below entry ${originalEntryPrice.toFixed(2)}`)
return true
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...`)
return false
}
// Update lowest price in zone
metadata.lowestInZone = Math.min(metadata.lowestInZone, currentPrice)
;(stopHunt as any).revengeMetadata = metadata
// Check if we've been in zone for 60+ seconds (simulates candle close)
const timeInZone = now - metadata.firstCrossTime
if (timeInZone >= 60000) {
console.log(` ✅ LONG revenge: Price held below entry for ${(timeInZone/1000).toFixed(0)}s, confirmed!`)
console.log(` Entry ${originalEntryPrice.toFixed(2)} → Current ${currentPrice.toFixed(2)}`)
return true
} else {
console.log(` ⏱️ LONG revenge: ${(timeInZone/1000).toFixed(0)}s in zone (need 60s)`)
return false
}
} else {
// Price left revenge zone - reset timer
if (metadata.firstCrossTime) {
console.log(` ❌ LONG revenge: Price bounced back up to ${currentPrice.toFixed(2)}, resetting timer`)
;(stopHunt as any).revengeMetadata = {}
}
return false
}
} else {
// Short stopped out below entry → price spiked down (stop hunt)
// Revenge: Price rises back above original entry (confirms up move)
const crossedBackUp = currentPrice > originalEntryPrice
const movedEnoughFromStop = currentPrice > stopHuntPrice * 1.005 // 0.5% above stop
// Short stopped out below entry → Revenge when price rises back above entry
const crossedBackUp = currentPrice > originalEntryPrice * 1.005 // 0.5% buffer
if (crossedBackUp && movedEnoughFromStop) {
console.log(` ✅ SHORT revenge: Price ${currentPrice.toFixed(2)} crossed back above entry ${originalEntryPrice.toFixed(2)}`)
return true
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...`)
return false
}
// Update highest price in zone
metadata.highestInZone = Math.max(metadata.highestInZone, currentPrice)
;(stopHunt as any).revengeMetadata = metadata
// Check if we've been in zone for 60+ seconds
const timeInZone = now - metadata.firstCrossTime
if (timeInZone >= 60000) {
console.log(` ✅ SHORT revenge: Price held above entry for ${(timeInZone/1000).toFixed(0)}s, confirmed!`)
console.log(` Entry ${originalEntryPrice.toFixed(2)} → Current ${currentPrice.toFixed(2)}`)
return true
} else {
console.log(` ⏱️ SHORT revenge: ${(timeInZone/1000).toFixed(0)}s in zone (need 60s)`)
return false
}
} else {
// Price left revenge zone - reset timer
if (metadata.firstCrossTime) {
console.log(` ❌ SHORT revenge: Price dropped back to ${currentPrice.toFixed(2)}, resetting timer`)
;(stopHunt as any).revengeMetadata = {}
}
return false
}
}
return false
}
/**