Enhancement #6 - SL Distance Validation (Data Collection Phase): - Added slDistanceAtEntry field to StopHunt schema - Calculates distance from revenge entry to stop zone (LONG vs SHORT logic) - Logs distance in dollars + × ATR multiplier - Purpose: Collect 20+ revenge trade samples for optimal multiplier analysis - Created comprehensive analysis guide with SQL queries - Decision deferred until empirical data collected Enhancement #1 - ADX Confirmation (Implementation Plan): - Documented complete 1-minute TradingView alert strategy - Storage analysis: 19.44 MB/month for 3 symbols (negligible) - Two-phase approach: Cache-only MVP → Optional DB persistence - Provided TradingView Pine Script (ready to use) - Cost breakdown: Pro subscription $49.95/month required - Benefits: Real-time ADX, pattern recognition, ML features - Implementation checklist with validation phases Files Changed: - prisma/schema.prisma: +1 field (slDistanceAtEntry) - lib/trading/stop-hunt-tracker.ts: +10 lines (distance calculation + logging) - docs/1MIN_MARKET_DATA_IMPLEMENTATION.md: NEW (comprehensive plan) - docs/ENHANCEMENT_6_ANALYSIS_GUIDE.md: NEW (SQL queries + decision matrix) Status: Enhancement #4 and #10 deployed (previous commit) Enhancement #6 data collection enabled (this commit) Awaiting 20+ revenge trades for Enhancement #6 decision
572 lines
19 KiB
TypeScript
572 lines
19 KiB
TypeScript
/**
|
||
* Stop Hunt Revenge Tracker (Nov 20, 2025)
|
||
*
|
||
* Tracks high-quality stop-outs (score 85+) and automatically re-enters
|
||
* when the stop hunt reverses and the real move begins.
|
||
*
|
||
* How it works:
|
||
* 1. When quality 85+ trade gets stopped out → Create StopHunt record
|
||
* 2. Monitor price for 4 hours
|
||
* 3. If price crosses back through original entry + ADX rebuilds → Auto re-enter with 1.2x size
|
||
* 4. Send "🔥 REVENGE TRADE" notification
|
||
*
|
||
* Purpose: Catch the real move after getting swept by stop hunters
|
||
*/
|
||
|
||
import { getPrismaClient } from '../database/trades'
|
||
import { initializeDriftService } from '../drift/client'
|
||
import { getPythPriceMonitor } from '../pyth/price-monitor'
|
||
|
||
interface StopHuntRecord {
|
||
id: string
|
||
originalTradeId: string
|
||
symbol: string
|
||
direction: 'long' | 'short'
|
||
stopHuntPrice: number
|
||
originalEntryPrice: number
|
||
originalQualityScore: number
|
||
originalADX: number | null
|
||
originalATR: number | null
|
||
stopLossAmount: number
|
||
stopHuntTime: Date
|
||
revengeExecuted: boolean
|
||
revengeWindowExpired: boolean
|
||
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
|
||
let monitoringInterval: NodeJS.Timeout | null = null
|
||
|
||
export class StopHuntTracker {
|
||
private prisma = getPrismaClient()
|
||
private isMonitoring = false
|
||
|
||
/**
|
||
* Create stop hunt record when quality 85+ trade gets stopped out
|
||
*/
|
||
async recordStopHunt(params: {
|
||
originalTradeId: string
|
||
symbol: string
|
||
direction: 'long' | 'short'
|
||
stopHuntPrice: number
|
||
originalEntryPrice: number
|
||
originalQualityScore: number
|
||
originalADX?: number
|
||
originalATR?: number
|
||
stopLossAmount: number
|
||
}): Promise<void> {
|
||
// Only track quality 85+ stop-outs (high-confidence trades)
|
||
if (params.originalQualityScore < 85) {
|
||
console.log(`⚠️ Stop hunt not tracked: Quality ${params.originalQualityScore} < 85 threshold`)
|
||
return
|
||
}
|
||
|
||
const revengeExpiresAt = new Date(Date.now() + 4 * 60 * 60 * 1000) // 4 hours
|
||
|
||
try {
|
||
await this.prisma.stopHunt.create({
|
||
data: {
|
||
originalTradeId: params.originalTradeId,
|
||
symbol: params.symbol,
|
||
direction: params.direction,
|
||
stopHuntPrice: params.stopHuntPrice,
|
||
originalEntryPrice: params.originalEntryPrice,
|
||
originalQualityScore: params.originalQualityScore,
|
||
originalADX: params.originalADX || null,
|
||
originalATR: params.originalATR || null,
|
||
stopLossAmount: params.stopLossAmount,
|
||
stopHuntTime: new Date(),
|
||
revengeExpiresAt,
|
||
}
|
||
})
|
||
|
||
console.log(`🎯 STOP HUNT RECORDED: ${params.symbol} ${params.direction.toUpperCase()}`)
|
||
console.log(` Quality: ${params.originalQualityScore}, Loss: $${params.stopLossAmount.toFixed(2)}`)
|
||
console.log(` Revenge window: 4 hours (expires ${revengeExpiresAt.toLocaleTimeString()})`)
|
||
|
||
// Start monitoring if not already running
|
||
if (!this.isMonitoring) {
|
||
this.startMonitoring()
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ Failed to record stop hunt:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Start monitoring active stop hunts for revenge opportunities
|
||
*/
|
||
startMonitoring(): void {
|
||
if (this.isMonitoring) return
|
||
|
||
this.isMonitoring = true
|
||
console.log('🔍 Stop Hunt Revenge Tracker: Monitoring started')
|
||
|
||
// Check every 30 seconds
|
||
monitoringInterval = setInterval(async () => {
|
||
await this.checkRevengeOpportunities()
|
||
}, 30 * 1000)
|
||
}
|
||
|
||
/**
|
||
* Stop monitoring (cleanup on shutdown)
|
||
*/
|
||
stopMonitoring(): void {
|
||
if (monitoringInterval) {
|
||
clearInterval(monitoringInterval)
|
||
monitoringInterval = null
|
||
}
|
||
this.isMonitoring = false
|
||
console.log('🛑 Stop Hunt Revenge Tracker: Monitoring stopped')
|
||
}
|
||
|
||
/**
|
||
* Check all active stop hunts for revenge entry conditions
|
||
*/
|
||
private async checkRevengeOpportunities(): Promise<void> {
|
||
try {
|
||
// Get active stop hunts (not executed, not expired)
|
||
const activeStopHunts = await this.prisma.stopHunt.findMany({
|
||
where: {
|
||
revengeExecuted: false,
|
||
revengeWindowExpired: false,
|
||
revengeExpiresAt: {
|
||
gt: new Date() // Not expired yet
|
||
}
|
||
}
|
||
})
|
||
|
||
if (activeStopHunts.length === 0) {
|
||
// No active stop hunts, stop monitoring to save resources
|
||
if (this.isMonitoring) {
|
||
console.log('📊 No active stop hunts - pausing monitoring')
|
||
this.stopMonitoring()
|
||
}
|
||
return
|
||
}
|
||
|
||
console.log(`🔍 Checking ${activeStopHunts.length} active stop hunt(s)...`)
|
||
|
||
for (const stopHunt of activeStopHunts) {
|
||
await this.checkStopHunt(stopHunt as StopHuntRecord)
|
||
}
|
||
|
||
// Expire old stop hunts
|
||
await this.expireOldStopHunts()
|
||
|
||
} catch (error) {
|
||
console.error('❌ Error checking revenge opportunities:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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 {
|
||
// Get current price
|
||
const priceMonitor = getPythPriceMonitor()
|
||
const latestPrice = priceMonitor.getCachedPrice(stopHunt.symbol)
|
||
|
||
if (!latestPrice || !latestPrice.price) {
|
||
return // Price not available, skip
|
||
}
|
||
|
||
const currentPrice = latestPrice.price
|
||
|
||
// Update high/low tracking
|
||
const highestPrice = Math.max(currentPrice, stopHunt.highestPriceAfterStop || currentPrice)
|
||
const lowestPrice = Math.min(currentPrice, stopHunt.lowestPriceAfterStop || currentPrice)
|
||
|
||
await this.prisma.stopHunt.update({
|
||
where: { id: stopHunt.id },
|
||
data: {
|
||
highestPriceAfterStop: highestPrice,
|
||
lowestPriceAfterStop: lowestPrice,
|
||
}
|
||
})
|
||
|
||
// Check revenge conditions (now requires sustained move, not just wick)
|
||
const shouldRevenge = await this.shouldExecuteRevenge(stopHunt, currentPrice)
|
||
|
||
if (shouldRevenge) {
|
||
console.log(`🔥 REVENGE CONDITIONS MET: ${stopHunt.symbol} ${stopHunt.direction.toUpperCase()}`)
|
||
await this.executeRevengeTrade(stopHunt, currentPrice)
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error(`❌ Error checking stop hunt ${stopHunt.id}:`, error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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 90+ 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 90s before entry
|
||
* - Result: Enters safely after confirmation, rides to $144.50 (+$530!)
|
||
*/
|
||
private async shouldExecuteRevenge(stopHunt: StopHuntRecord, currentPrice: number): Promise<boolean> {
|
||
const { direction, stopHuntPrice, originalEntryPrice } = stopHunt
|
||
|
||
const now = Date.now()
|
||
|
||
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 - 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
|
||
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)
|
||
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)}`)
|
||
return true
|
||
} else {
|
||
console.log(` ⏱️ LONG revenge: ${(timeInZone/60000).toFixed(1)}min in zone (need 1.5min)`)
|
||
return false
|
||
}
|
||
} else {
|
||
// 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`)
|
||
await this.prisma.stopHunt.update({
|
||
where: { id: stopHunt.id },
|
||
data: {
|
||
firstCrossTime: null,
|
||
lowestInZone: null,
|
||
zoneResetCount: { increment: 1 }
|
||
}
|
||
})
|
||
}
|
||
return false
|
||
}
|
||
} else {
|
||
// Short stopped out below entry → Revenge when price rises back above entry
|
||
const crossedBackUp = currentPrice > originalEntryPrice * 1.005 // 0.5% buffer
|
||
|
||
if (crossedBackUp) {
|
||
// 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
|
||
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)
|
||
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)}`)
|
||
return true
|
||
} else {
|
||
console.log(` ⏱️ SHORT revenge: ${(timeInZone/60000).toFixed(1)}min in zone (need 1.5min)`)
|
||
return false
|
||
}
|
||
} else {
|
||
// 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`)
|
||
await this.prisma.stopHunt.update({
|
||
where: { id: stopHunt.id },
|
||
data: {
|
||
firstCrossTime: null,
|
||
highestInZone: null,
|
||
zoneResetCount: { increment: 1 }
|
||
}
|
||
})
|
||
}
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Execute revenge trade automatically
|
||
*/
|
||
private async executeRevengeTrade(stopHunt: StopHuntRecord, currentPrice: number): Promise<void> {
|
||
try {
|
||
console.log(`🔥 EXECUTING REVENGE TRADE: ${stopHunt.symbol} ${stopHunt.direction.toUpperCase()}`)
|
||
console.log(` Original loss: $${stopHunt.stopLossAmount.toFixed(2)}`)
|
||
console.log(` Revenge size: 1.2x (getting our money back!)`)
|
||
|
||
// Call execute endpoint with revenge parameters
|
||
const response = await fetch('http://localhost:3000/api/trading/execute', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${process.env.API_SECRET_KEY}`
|
||
},
|
||
body: JSON.stringify({
|
||
symbol: stopHunt.symbol,
|
||
direction: stopHunt.direction,
|
||
currentPrice,
|
||
timeframe: 'revenge', // Special timeframe for revenge trades
|
||
signalSource: 'stop_hunt_revenge',
|
||
|
||
// Use original quality metrics
|
||
atr: stopHunt.originalATR || 0.45,
|
||
adx: stopHunt.originalADX || 32,
|
||
rsi: stopHunt.direction === 'long' ? 58 : 42,
|
||
volumeRatio: 1.2,
|
||
pricePosition: 50,
|
||
|
||
// Metadata
|
||
revengeMetadata: {
|
||
originalTradeId: stopHunt.originalTradeId,
|
||
stopHuntId: stopHunt.id,
|
||
originalLoss: stopHunt.stopLossAmount,
|
||
sizingMultiplier: 1.0 // Same size as original (user at 100% allocation)
|
||
}
|
||
})
|
||
})
|
||
|
||
const result = await response.json()
|
||
|
||
if (result.success) {
|
||
// Calculate SL distance at entry (for Enhancement #6 analysis)
|
||
const slDistance = stopHunt.direction === 'long'
|
||
? currentPrice - stopHunt.stopHuntPrice // LONG: Room below entry
|
||
: stopHunt.stopHuntPrice - currentPrice // SHORT: Room above entry
|
||
|
||
// Mark revenge as executed
|
||
await this.prisma.stopHunt.update({
|
||
where: { id: stopHunt.id },
|
||
data: {
|
||
revengeExecuted: true,
|
||
revengeTradeId: result.trade?.id,
|
||
revengeEntryPrice: currentPrice,
|
||
revengeTime: new Date(),
|
||
slDistanceAtEntry: Math.abs(slDistance), // Store absolute distance
|
||
}
|
||
})
|
||
|
||
console.log(`✅ REVENGE TRADE EXECUTED: ${result.trade?.id}`)
|
||
console.log(`📊 SL Distance: $${Math.abs(slDistance).toFixed(2)} (${stopHunt.originalATR ? `${(Math.abs(slDistance) / stopHunt.originalATR).toFixed(2)}× ATR` : 'no ATR'})`)
|
||
console.log(`🔥 LET'S GET OUR MONEY BACK!`)
|
||
|
||
// Send special Telegram notification
|
||
await this.sendRevengeNotification(stopHunt, result.trade)
|
||
} else {
|
||
console.error(`❌ Revenge trade failed:`, result.error)
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error(`❌ Error executing revenge trade:`, error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Send special Telegram notification for revenge trades
|
||
*/
|
||
private async sendRevengeNotification(stopHunt: StopHuntRecord, trade: any): Promise<void> {
|
||
try {
|
||
const message = `
|
||
🔥 <b>REVENGE TRADE ACTIVATED</b> 🔥
|
||
|
||
<b>${stopHunt.symbol} ${stopHunt.direction.toUpperCase()}</b>
|
||
|
||
💀 Original Stop Hunt: -$${stopHunt.stopLossAmount.toFixed(2)}
|
||
🎯 Revenge Entry: $${trade.entryPrice.toFixed(2)}
|
||
💪 Position Size: $${trade.positionSizeUSD.toFixed(2)} (same as original)
|
||
|
||
⚔️ <b>TIME FOR PAYBACK!</b>
|
||
|
||
Original Quality: ${stopHunt.originalQualityScore}/100
|
||
Stop Hunt Price: $${stopHunt.stopHuntPrice.toFixed(4)}
|
||
Reversal Confirmed: Price crossed back through entry
|
||
|
||
<i>Let's get our money back! 💰</i>
|
||
`.trim()
|
||
|
||
await fetch(`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
chat_id: process.env.TELEGRAM_CHAT_ID,
|
||
text: message,
|
||
parse_mode: 'HTML'
|
||
})
|
||
})
|
||
|
||
} catch (error) {
|
||
console.error('❌ Failed to send revenge notification:', error)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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
|
||
*/
|
||
private async expireOldStopHunts(): Promise<void> {
|
||
try {
|
||
const expired = await this.prisma.stopHunt.updateMany({
|
||
where: {
|
||
revengeExecuted: false,
|
||
revengeWindowExpired: false,
|
||
revengeExpiresAt: {
|
||
lte: new Date()
|
||
}
|
||
},
|
||
data: {
|
||
revengeWindowExpired: true
|
||
}
|
||
})
|
||
|
||
if (expired.count > 0) {
|
||
console.log(`⏰ Expired ${expired.count} stop hunt revenge window(s)`)
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ Error expiring stop hunts:', error)
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get singleton instance
|
||
*/
|
||
export function getStopHuntTracker(): StopHuntTracker {
|
||
if (!trackerInstance) {
|
||
trackerInstance = new StopHuntTracker()
|
||
}
|
||
return trackerInstance
|
||
}
|
||
|
||
/**
|
||
* Start tracking (called on server startup)
|
||
*/
|
||
export async function startStopHuntTracking(): Promise<void> {
|
||
try {
|
||
const tracker = getStopHuntTracker()
|
||
const prisma = getPrismaClient()
|
||
|
||
const activeCount = await prisma.stopHunt.count({
|
||
where: {
|
||
revengeExecuted: false,
|
||
revengeWindowExpired: false,
|
||
revengeExpiresAt: {
|
||
gt: new Date()
|
||
}
|
||
}
|
||
})
|
||
|
||
if (activeCount > 0) {
|
||
console.log(`🎯 Found ${activeCount} active stop hunt(s) - starting revenge tracker`)
|
||
tracker.startMonitoring()
|
||
} else {
|
||
console.log('📊 No active stop hunts - tracker will start when needed')
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ Error starting stop hunt tracker:', error)
|
||
}
|
||
}
|