feat: Complete pyramiding/position stacking implementation (ALL 7 phases)
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
This commit is contained in:
@@ -17,6 +17,11 @@ interface TelegramNotificationOptions {
|
||||
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 {
|
||||
@@ -51,10 +56,11 @@ export async function sendPositionClosedNotification(options: TelegramNotificati
|
||||
|
||||
const message = `${exitReasonEmoji} POSITION CLOSED
|
||||
|
||||
${directionEmoji} ${options.symbol} ${options.direction.toUpperCase()}
|
||||
${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)}
|
||||
@@ -320,3 +326,71 @@ export async function sendTelegramMessage(message: string): Promise<void> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user