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:
@@ -359,7 +359,127 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
trade => trade.symbol === driftSymbol && trade.direction === body.direction
|
||||
)
|
||||
|
||||
if (sameDirectionPosition) {
|
||||
// ========================================
|
||||
// PYRAMIDING / POSITION STACKING SYSTEM
|
||||
// ========================================
|
||||
// Check if this is a stacking opportunity (same direction within time window)
|
||||
// Pyramiding creates NEW linked positions, NOT scaling same position
|
||||
let pyramidingInfo: {
|
||||
isStack: boolean;
|
||||
parentTradeId?: string;
|
||||
pyramidLevel: number;
|
||||
totalLeverageAtEntry: number;
|
||||
parentCreatedAt?: Date;
|
||||
} = {
|
||||
isStack: false,
|
||||
pyramidLevel: 1,
|
||||
totalLeverageAtEntry: leverage
|
||||
}
|
||||
|
||||
if (config.enablePyramiding && sameDirectionPosition) {
|
||||
console.log(`🔺 PYRAMIDING CHECK: Existing ${body.direction} position on ${driftSymbol}`)
|
||||
|
||||
// Get the parent trade from database to check timing and pyramid level
|
||||
const { getPrismaClient } = await import('@/lib/database/trades')
|
||||
const prisma = getPrismaClient()
|
||||
|
||||
const parentTrade = await prisma.trade.findFirst({
|
||||
where: {
|
||||
symbol: driftSymbol,
|
||||
direction: body.direction,
|
||||
exitReason: null, // Still open
|
||||
},
|
||||
orderBy: { createdAt: 'desc' }
|
||||
})
|
||||
|
||||
if (parentTrade) {
|
||||
const timeSinceParent = Date.now() - parentTrade.createdAt.getTime()
|
||||
const stackingWindowMs = (config.stackingWindowMinutes || 240) * 60 * 1000
|
||||
const currentPyramidLevel = (parentTrade.pyramidLevel || 1) + 1
|
||||
const maxLevels = config.maxPyramidLevels || 2
|
||||
|
||||
console.log(` Parent trade ID: ${parentTrade.id}`)
|
||||
console.log(` Time since parent: ${(timeSinceParent / 60000).toFixed(1)} minutes`)
|
||||
console.log(` Stacking window: ${config.stackingWindowMinutes || 240} minutes`)
|
||||
console.log(` Current pyramid level would be: ${currentPyramidLevel}`)
|
||||
console.log(` Max pyramid levels: ${maxLevels}`)
|
||||
|
||||
// Check if within stacking window and under max levels
|
||||
const withinWindow = timeSinceParent <= stackingWindowMs
|
||||
const underMaxLevels = currentPyramidLevel <= maxLevels
|
||||
|
||||
if (withinWindow && underMaxLevels) {
|
||||
// Calculate total leverage including parent
|
||||
const parentLeverage = parentTrade.leverage || leverage
|
||||
const stackLeverage = config.stackLeverage || 7
|
||||
const totalLeverage = parentLeverage + stackLeverage
|
||||
const maxTotalLeverage = config.maxLeverageTotal || 14
|
||||
|
||||
console.log(` Parent leverage: ${parentLeverage}x`)
|
||||
console.log(` Stack leverage: ${stackLeverage}x`)
|
||||
console.log(` Total leverage would be: ${totalLeverage}x`)
|
||||
console.log(` Max total leverage: ${maxTotalLeverage}x`)
|
||||
|
||||
if (totalLeverage <= maxTotalLeverage) {
|
||||
console.log(`✅ PYRAMIDING APPROVED: Opening stacked position level ${currentPyramidLevel}`)
|
||||
|
||||
pyramidingInfo = {
|
||||
isStack: true,
|
||||
parentTradeId: parentTrade.id,
|
||||
pyramidLevel: currentPyramidLevel,
|
||||
totalLeverageAtEntry: totalLeverage,
|
||||
parentCreatedAt: parentTrade.createdAt
|
||||
}
|
||||
|
||||
// Note: We continue to open the position below, NOT return here
|
||||
// The position will be created with pyramiding fields set
|
||||
} else {
|
||||
console.log(`⛔ PYRAMIDING BLOCKED: Would exceed max leverage (${totalLeverage}x > ${maxTotalLeverage}x)`)
|
||||
return NextResponse.json({
|
||||
success: false,
|
||||
error: 'Max leverage exceeded',
|
||||
message: `Stacking would result in ${totalLeverage}x total leverage, exceeding max of ${maxTotalLeverage}x`,
|
||||
}, { status: 400 })
|
||||
}
|
||||
} else {
|
||||
if (!withinWindow) {
|
||||
console.log(`⛔ PYRAMIDING BLOCKED: Outside stacking window (${(timeSinceParent / 60000).toFixed(1)} min > ${config.stackingWindowMinutes || 240} min)`)
|
||||
}
|
||||
if (!underMaxLevels) {
|
||||
console.log(`⛔ PYRAMIDING BLOCKED: Max pyramid levels reached (${currentPyramidLevel} > ${maxLevels})`)
|
||||
}
|
||||
// Fall through to existing position scaling / duplicate check
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a valid pyramiding stack, skip the duplicate/scaling checks
|
||||
// and proceed to open a NEW linked position
|
||||
let effectivePositionSize = positionSize
|
||||
let effectiveLeverage = leverage
|
||||
|
||||
if (pyramidingInfo.isStack) {
|
||||
// Use stack leverage instead of normal adaptive leverage
|
||||
const stackLeverage = config.stackLeverage || 7
|
||||
console.log(`🔺 Using stack leverage: ${stackLeverage}x (instead of ${leverage}x)`)
|
||||
|
||||
// Recalculate position size with stack leverage
|
||||
const { getActualPositionSizeForSymbol } = await import('@/config/trading')
|
||||
const stackSizing = await getActualPositionSizeForSymbol(
|
||||
driftSymbol,
|
||||
{ ...config, leverage: stackLeverage }, // Override leverage for stacking
|
||||
health.freeCollateral,
|
||||
qualityResult.score,
|
||||
body.direction
|
||||
)
|
||||
|
||||
// Update effective position size and leverage for the stacked position
|
||||
effectivePositionSize = stackSizing.size
|
||||
effectiveLeverage = stackLeverage
|
||||
console.log(` Stack position size: $${stackSizing.size.toFixed(2)}`)
|
||||
|
||||
// Continue to position opening section below (skip duplicate/scaling checks)
|
||||
} else if (sameDirectionPosition) {
|
||||
// Position scaling enabled - scale into existing position
|
||||
if (config.enablePositionScaling) {
|
||||
console.log(`📈 POSITION SCALING: Adding to existing ${body.direction} position on ${driftSymbol}`)
|
||||
@@ -530,13 +650,17 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
}
|
||||
|
||||
// Calculate position size with leverage
|
||||
const positionSizeUSD = positionSize * leverage
|
||||
// Use effective values which may be overridden for pyramiding stacks
|
||||
const positionSizeUSD = effectivePositionSize * effectiveLeverage
|
||||
|
||||
console.log(`💰 Opening ${body.direction} position:`)
|
||||
console.log(` Symbol: ${driftSymbol}`)
|
||||
console.log(` Base size: $${positionSize}`)
|
||||
console.log(` Leverage: ${leverage}x`)
|
||||
console.log(` Base size: $${effectivePositionSize}`)
|
||||
console.log(` Leverage: ${effectiveLeverage}x`)
|
||||
console.log(` Total position: $${positionSizeUSD}`)
|
||||
if (pyramidingInfo.isStack) {
|
||||
console.log(` 🔺 This is a STACKED position (parent: ${pyramidingInfo.parentTradeId})`)
|
||||
}
|
||||
|
||||
// 🎯 SMART ENTRY TIMING - Check if we should wait for better entry (Phase 2 - Nov 27, 2025)
|
||||
// BYPASS (Dec 27, 2025): Skip Smart Entry for v11.2+ signals with high indicator scores
|
||||
@@ -1070,7 +1194,7 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
direction: body.direction,
|
||||
entryPrice,
|
||||
positionSizeUSD: positionSizeUSD,
|
||||
leverage: leverage, // Use actual symbol-specific leverage, not global config
|
||||
leverage: effectiveLeverage, // Use effective leverage (may be stack leverage for pyramided positions)
|
||||
stopLossPrice,
|
||||
takeProfit1Price: tp1Price,
|
||||
takeProfit2Price: tp2Price,
|
||||
@@ -1096,6 +1220,12 @@ export async function POST(request: NextRequest): Promise<NextResponse<ExecuteTr
|
||||
pricePositionAtEntry: body.pricePosition,
|
||||
signalQualityScore: qualityResult.score,
|
||||
indicatorVersion: body.indicatorVersion || 'v5', // Default to v5 for backward compatibility
|
||||
// PYRAMIDING FIELDS (Phase 3)
|
||||
pyramidLevel: pyramidingInfo.pyramidLevel,
|
||||
parentTradeId: pyramidingInfo.parentTradeId || null,
|
||||
stackedAt: pyramidingInfo.isStack ? new Date() : null,
|
||||
totalLeverageAtEntry: pyramidingInfo.totalLeverageAtEntry,
|
||||
isStackedPosition: pyramidingInfo.isStack,
|
||||
})
|
||||
|
||||
console.log('🔍 DEBUG: createTrade() completed successfully')
|
||||
|
||||
Reference in New Issue
Block a user