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:
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user