feat: Add Telegram notifications for position closures

Implemented direct Telegram notifications when Position Manager closes positions:
- New helper: lib/notifications/telegram.ts with sendPositionClosedNotification()
- Integrated into Position Manager's executeExit() for all closure types
- Also sends notifications for ghost position cleanups

Notification includes:
- Symbol, direction, entry/exit prices
- P&L amount and percentage
- Position size and hold time
- Exit reason (TP1, TP2, SL, manual, ghost cleanup, etc.)
- MAE/MFE stats (max gain/drawdown during trade)

User request: Receive P&L notifications on position closures via Telegram bot
Previously: Only opening notifications via n8n workflow
Now: All closures (TP/SL/manual/ghost) send notifications directly
This commit is contained in:
mindesbunister
2025-11-16 00:51:56 +01:00
parent 9db5f8566d
commit b1ca454a6f
2 changed files with 138 additions and 0 deletions

View File

@@ -9,6 +9,7 @@ import { closePosition } from '../drift/orders'
import { getPythPriceMonitor, PriceUpdate } from '../pyth/price-monitor'
import { getMergedConfig, TradingConfig, getMarketConfig } from '../../config/trading'
import { updateTradeExit, updateTradeState, getOpenTrades } from '../database/trades'
import { sendPositionClosedNotification } from '../notifications/telegram'
export interface ActiveTrade {
id: string
@@ -328,6 +329,20 @@ export class PositionManager {
maxAdversePrice: trade.maxAdversePrice,
})
console.log(`💾 Ghost closure saved to database`)
// Send Telegram notification for ghost closure
await sendPositionClosedNotification({
symbol: trade.symbol,
direction: trade.direction,
entryPrice: trade.entryPrice,
exitPrice: trade.lastPrice,
positionSize: trade.currentSize,
realizedPnL: estimatedPnL,
exitReason: reason, // e.g., "Ghost position cleanup", "Layer 2: Ghost detected via Drift API"
holdTimeSeconds: Math.floor((Date.now() - trade.entryTime) / 1000),
maxDrawdown: Math.abs(Math.min(0, trade.maxAdverseExcursion)),
maxGain: Math.max(0, trade.maxFavorableExcursion),
})
} catch (dbError) {
console.error('❌ Failed to save ghost closure:', dbError)
}
@@ -1214,6 +1229,20 @@ export class PositionManager {
await this.removeTrade(trade.id)
console.log(`✅ Position closed | P&L: $${trade.realizedPnL.toFixed(2)} | Reason: ${reason}`)
// Send Telegram notification
await sendPositionClosedNotification({
symbol: trade.symbol,
direction: trade.direction,
entryPrice: trade.entryPrice,
exitPrice: result.closePrice || currentPrice,
positionSize: trade.positionSize,
realizedPnL: trade.realizedPnL,
exitReason: reason,
holdTimeSeconds: Math.floor((Date.now() - trade.entryTime) / 1000),
maxDrawdown: Math.abs(Math.min(0, trade.maxAdverseExcursion)),
maxGain: Math.max(0, trade.maxFavorableExcursion),
})
} else {
// Partial close (TP1)
trade.realizedPnL += result.realizedPnL || 0