Files
trading_bot_v4/lib/notifications/telegram.ts
mindesbunister 6ac2647565 feat: Make Smart Validation Queue thresholds adaptive in Telegram notifications
CHANGES:
- Extended sendValidationNotification interface with confirmationThreshold, maxDrawdown, entryWindowMinutes
- Updated telegram.ts to display actual queued signal thresholds instead of hardcoded values
- Modified smart-validation-queue.ts to pass dynamic threshold values to Telegram
- Messages now show exact thresholds used for each signal (not fixed 0.3%/1.0%/90min)

PURPOSE:
- User requested adaptive display instead of hardcoded values
- Enables future per-signal threshold customization
- Each signal can have different thresholds based on characteristics

EXAMPLE:
  Before: 'Will enter if +0.3% confirms' (all signals)
  After:  'Will enter if +0.25% confirms' (high ADX signal)
          'Will enter if +0.4% confirms'  (low ADX signal)

STATUS: Ready for deployment - will show actual threshold per signal
2025-12-17 13:39:54 +01:00

323 lines
10 KiB
TypeScript

import { logger } from '../utils/logger'
/**
* Telegram notification utilities
*
* Sends direct notifications to Telegram bot without requiring n8n
*/
interface TelegramNotificationOptions {
symbol: string
direction: 'long' | 'short'
entryPrice: number
exitPrice: number
positionSize: number
realizedPnL: number
exitReason: string
holdTimeSeconds: number
maxDrawdown?: number
maxGain?: number
}
interface TelegramWithdrawalOptions {
type: 'withdrawal'
amount: number
signature: string
availableProfit: number
totalWithdrawn: number
}
type TelegramOptions = TelegramNotificationOptions | TelegramWithdrawalOptions
/**
* Send Telegram notification for position closure
*/
export async function sendPositionClosedNotification(options: TelegramNotificationOptions): Promise<void> {
try {
const token = process.env.TELEGRAM_BOT_TOKEN
const chatId = process.env.TELEGRAM_CHAT_ID
if (!token || !chatId) {
logger.log('⚠️ Telegram credentials not configured, skipping notification')
return
}
const profitEmoji = options.realizedPnL >= 0 ? '💚' : '🔴'
const exitReasonEmoji = getExitReasonEmoji(options.exitReason)
const directionEmoji = options.direction === 'long' ? '📈' : '📉'
const priceChange = ((options.exitPrice - options.entryPrice) / options.entryPrice * 100) * (options.direction === 'long' ? 1 : -1)
const holdTime = formatHoldTime(options.holdTimeSeconds)
const message = `${exitReasonEmoji} POSITION CLOSED
${directionEmoji} ${options.symbol} ${options.direction.toUpperCase()}
💰 P&L: $${options.realizedPnL.toFixed(2)} (${priceChange >= 0 ? '+' : ''}${priceChange.toFixed(2)}%)
📊 Size: $${options.positionSize.toFixed(2)}
📍 Entry: $${options.entryPrice.toFixed(2)}
🎯 Exit: $${options.exitPrice.toFixed(2)}
⏱ Hold Time: ${holdTime}
🔚 Exit: ${options.exitReason.toUpperCase()}
${options.maxGain ? `\n📈 Max Gain: +${options.maxGain.toFixed(2)}%` : ''}
${options.maxDrawdown ? `\n📉 Max Drawdown: -${options.maxDrawdown.toFixed(2)}%` : ''}`
const url = `https://api.telegram.org/bot${token}/sendMessage`
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: chatId,
text: message,
parse_mode: 'HTML'
})
})
if (!response.ok) {
const errorData = await response.json()
console.error('❌ Telegram notification failed:', errorData)
} else {
logger.log('✅ Telegram notification sent')
}
} catch (error) {
console.error('❌ Error sending Telegram notification:', error)
// Don't throw - notification failure shouldn't break position closing
}
}
/**
* Send Telegram notification for smart validation events
*/
export async function sendValidationNotification(options: {
event: 'queued' | 'confirmed' | 'abandoned' | 'expired' | 'executed'
symbol: string
direction: 'long' | 'short'
originalPrice: number
currentPrice?: number
qualityScore: number
validationTime?: number // seconds
priceChange?: number // percentage
confirmationThreshold?: number // percentage for queued event
maxDrawdown?: number // percentage for queued event
entryWindowMinutes?: number // minutes for queued event
}): Promise<void> {
try {
const token = process.env.TELEGRAM_BOT_TOKEN
const chatId = process.env.TELEGRAM_CHAT_ID
if (!token || !chatId) {
return
}
const directionEmoji = options.direction === 'long' ? '📈' : '📉'
let message = ''
switch (options.event) {
case 'queued':
message = [
'⏰ SIGNAL QUEUED FOR VALIDATION ⏰',
'',
`📊 ${options.symbol}`,
`📍 ${options.direction === 'long' ? 'LONG' : 'SHORT'} @ $${options.originalPrice.toFixed(2)}`,
`🎯 Quality Score: ${options.qualityScore}`,
'',
'⏱️ Smart Entry System Active',
`✅ Will enter if ${options.direction === 'long' ? '+' : '-'}${options.confirmationThreshold || 0.3}% confirms`,
`❌ Will abandon if ${options.direction === 'long' ? '' : '+'}${Math.abs(options.maxDrawdown || -1.0)}% against`,
`⏳ Monitoring for ${options.entryWindowMinutes || 90} minutes`,
].join('\n')
break
case 'confirmed':
message = `✅ SIGNAL VALIDATED - ENTERING NOW!
${directionEmoji} ${options.symbol} ${options.direction.toUpperCase()}
💡 Original: $${options.originalPrice.toFixed(2)} (quality ${options.qualityScore})
🎯 Confirmed: $${options.currentPrice?.toFixed(2)} (${options.priceChange! >= 0 ? '+' : ''}${options.priceChange?.toFixed(2)}%)
⏱ Validation Time: ${Math.floor(options.validationTime! / 60)}min ${Math.floor(options.validationTime! % 60)}s
🚀 Executing trade now...`
break
case 'abandoned':
message = ` SIGNAL ABANDONED
${directionEmoji} ${options.symbol} ${options.direction.toUpperCase()}
💡 Original: $${options.originalPrice.toFixed(2)} (quality ${options.qualityScore})
⚠️ Current: $${options.currentPrice?.toFixed(2)} (${options.priceChange! >= 0 ? '+' : ''}${options.priceChange?.toFixed(2)}%)
✅ Saved from potential loser!
⏱ Monitored for ${Math.floor(options.validationTime! / 60)}min ${Math.floor(options.validationTime! % 60)}s`
break
case 'expired':
message = `⏱️ SIGNAL EXPIRED
${directionEmoji} ${options.symbol} ${options.direction.toUpperCase()}
📊 Quality: ${options.qualityScore}/100
📍 Original Price: $${options.originalPrice.toFixed(2)}
⏰ No confirmation after 30 minutes
🔄 Move wasn't strong enough`
break
case 'executed':
message = `✅ VALIDATED TRADE OPENED
${directionEmoji} ${options.symbol} ${options.direction.toUpperCase()}
💡 Original Signal: $${options.originalPrice.toFixed(2)} (quality ${options.qualityScore})
🎯 Entry: $${options.currentPrice?.toFixed(2)}
📊 Slippage: ${options.priceChange?.toFixed(2)}%
⏱ Validation took ${Math.floor(options.validationTime! / 60)}min ${Math.floor(options.validationTime! % 60)}s`
break
}
const url = `https://api.telegram.org/bot${token}/sendMessage`
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: chatId,
text: message,
parse_mode: 'HTML'
})
})
if (!response.ok) {
console.error('❌ Validation Telegram notification failed')
}
} catch (error) {
console.error('❌ Error sending validation notification:', error)
}
}
/**
* Get emoji for exit reason
*/
function getExitReasonEmoji(reason: string): string {
const reasonUpper = reason.toUpperCase()
if (reasonUpper.includes('TP1')) return '🎯'
if (reasonUpper.includes('TP2')) return '🎯🎯'
if (reasonUpper.includes('SL') || reasonUpper.includes('STOP')) return '🛑'
if (reasonUpper.includes('MANUAL')) return '👤'
if (reasonUpper.includes('EMERGENCY')) return '🚨'
if (reasonUpper.includes('GHOST')) return '👻'
return '✅'
}
/**
* Format hold time in human-readable format
*/
function formatHoldTime(seconds: number): string {
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
const secs = seconds % 60
if (hours > 0) {
return `${hours}h ${minutes}m`
} else if (minutes > 0) {
return `${minutes}m ${secs}s`
} else {
return `${secs}s`
}
}
/**
* Send Telegram notification (supports both position closures and withdrawals)
*/
export async function sendTelegramNotification(options: TelegramOptions): Promise<void> {
if ('type' in options && options.type === 'withdrawal') {
return sendWithdrawalNotification(options)
}
return sendPositionClosedNotification(options as TelegramNotificationOptions)
}
/**
* Send withdrawal notification
*/
async function sendWithdrawalNotification(options: TelegramWithdrawalOptions): Promise<void> {
try {
const token = process.env.TELEGRAM_BOT_TOKEN
const chatId = process.env.TELEGRAM_CHAT_ID
if (!token || !chatId) {
logger.log('⚠️ Telegram credentials not configured, skipping notification')
return
}
const message = `💸 PROFIT WITHDRAWAL
💰 Amount: $${options.amount.toFixed(2)} USDC
📊 Available Profit: $${options.availableProfit.toFixed(2)}
📈 Total Withdrawn: $${options.totalWithdrawn.toFixed(2)}
🔗 Transaction: <a href="https://solscan.io/tx/${options.signature}">${options.signature.substring(0, 8)}...</a>
Funds sent to your wallet`
const url = `https://api.telegram.org/bot${token}/sendMessage`
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: chatId,
text: message,
parse_mode: 'HTML'
})
})
if (!response.ok) {
const errorData = await response.json()
console.error('❌ Telegram notification failed:', errorData)
} else {
logger.log('✅ Telegram withdrawal notification sent')
}
} catch (error) {
console.error('❌ Error sending Telegram notification:', error)
}
}
/**
* Send a generic text message to Telegram
* Used for alerts, revenge blocks, system notifications
*/
export async function sendTelegramMessage(message: string): Promise<void> {
try {
const token = process.env.TELEGRAM_BOT_TOKEN
const chatId = process.env.TELEGRAM_CHAT_ID
if (!token || !chatId) {
logger.log('⚠️ Telegram credentials not configured, skipping notification')
return
}
const url = `https://api.telegram.org/bot${token}/sendMessage`
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: chatId,
text: message,
parse_mode: 'HTML'
})
})
if (!response.ok) {
const errorData = await response.json()
console.error('❌ Telegram notification failed:', errorData)
}
} catch (error) {
console.error('❌ Error sending Telegram notification:', error)
}
}