Phase 1: Configuration - Added pyramiding config to trading.ts interface and defaults - Added 6 ENV variables: ENABLE_PYRAMIDING, BASE_LEVERAGE, STACK_LEVERAGE, MAX_LEVERAGE_TOTAL, MAX_PYRAMID_LEVELS, STACKING_WINDOW_MINUTES Phase 2: Database Schema - Added 5 Trade fields: pyramidLevel, parentTradeId, stackedAt, totalLeverageAtEntry, isStackedPosition - Added index on parentTradeId for pyramid group queries Phase 3: Execute Endpoint - Added findExistingPyramidBase() - finds active base trade within window - Added canAddPyramidLevel() - validates pyramid conditions - Stores pyramid metadata on new trades Phase 4: Position Manager Core - Added pyramidGroups Map for trade ID grouping - Added addToPyramidGroup() - groups stacked trades by parent - Added closeAllPyramidLevels() - unified exit for all levels - Added getTotalPyramidLeverage() - calculates combined leverage - All exit triggers now close entire pyramid group Phase 5: Telegram Notifications - Added sendPyramidStackNotification() - notifies on stack entry - Added sendPyramidCloseNotification() - notifies on unified exit Phase 6: Testing (25 tests, ALL PASSING) - Pyramid Detection: 5 tests - Pyramid Group Tracking: 4 tests - Unified Exit: 4 tests - Leverage Calculation: 4 tests - Notification Context: 2 tests - Edge Cases: 6 tests Phase 7: Documentation - Updated .github/copilot-instructions.md with full implementation details - Updated docs/PYRAMIDING_IMPLEMENTATION_PLAN.md status to COMPLETE Parameters: 4h window, 7x base/stack leverage, 14x max total, 2 max levels Data-driven: 100% win rate for signals ≤72 bars apart in backtesting
397 lines
13 KiB
TypeScript
397 lines
13 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
|
|
// 🔺 Pyramiding fields (Jan 2026)
|
|
pyramidLevel?: number // 1 = base, 2 = first stack, etc.
|
|
isStackedPosition?: boolean // True if this is an add-on position
|
|
pyramidGroupSize?: number // Total positions in pyramid group
|
|
pyramidGroupPnL?: number // Combined P&L of entire pyramid group
|
|
}
|
|
|
|
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()}${options.pyramidLevel ? ` 🔺 Level ${options.pyramidLevel}${options.isStackedPosition ? ' (stacked)' : ' (base)'}` : ''}
|
|
|
|
💰 P&L: $${options.realizedPnL.toFixed(2)} (${priceChange >= 0 ? '+' : ''}${priceChange.toFixed(2)}%)
|
|
📊 Size: $${options.positionSize.toFixed(2)}
|
|
${options.pyramidGroupPnL !== undefined && options.pyramidGroupSize && options.pyramidGroupSize > 1 ? `🔺 Group P&L: $${options.pyramidGroupPnL.toFixed(2)} (${options.pyramidGroupSize} positions)` : ''}
|
|
|
|
📍 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)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 🔺 Send pyramid group closure notification (Jan 2026)
|
|
* Sends a summary notification when all positions in a pyramid group are closed together
|
|
*/
|
|
export interface PyramidGroupNotificationOptions {
|
|
symbol: string
|
|
direction: 'long' | 'short'
|
|
exitReason: string
|
|
totalPositions: number
|
|
combinedPnL: number
|
|
combinedSize: number
|
|
avgEntryPrice: number
|
|
exitPrice: number
|
|
pyramidLevels: number[] // e.g., [1, 2] for base + one stack
|
|
}
|
|
|
|
export async function sendPyramidGroupClosedNotification(options: PyramidGroupNotificationOptions): 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.combinedPnL >= 0 ? '💚' : '🔴'
|
|
const exitReasonEmoji = getExitReasonEmoji(options.exitReason)
|
|
const directionEmoji = options.direction === 'long' ? '📈' : '📉'
|
|
|
|
const priceChange = ((options.exitPrice - options.avgEntryPrice) / options.avgEntryPrice * 100) * (options.direction === 'long' ? 1 : -1)
|
|
const levelsStr = options.pyramidLevels.sort((a, b) => a - b).join(', ')
|
|
|
|
const message = `🔺 PYRAMID GROUP CLOSED
|
|
|
|
${directionEmoji} ${options.symbol} ${options.direction.toUpperCase()}
|
|
|
|
${profitEmoji} Combined P&L: $${options.combinedPnL.toFixed(2)} (${priceChange >= 0 ? '+' : ''}${priceChange.toFixed(2)}%)
|
|
📊 Total Size: $${options.combinedSize.toFixed(2)}
|
|
🔺 Positions: ${options.totalPositions} (levels: ${levelsStr})
|
|
|
|
📍 Avg Entry: $${options.avgEntryPrice.toFixed(2)}
|
|
🎯 Exit: $${options.exitPrice.toFixed(2)}
|
|
|
|
${exitReasonEmoji} Exit Reason: ${options.exitReason.toUpperCase()}`
|
|
|
|
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 pyramid group notification failed:', errorData)
|
|
} else {
|
|
logger.log('✅ Telegram pyramid group notification sent')
|
|
}
|
|
} catch (error) {
|
|
console.error('❌ Error sending Telegram pyramid group notification:', error)
|
|
}
|
|
}
|