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:
mindesbunister
2026-01-09 13:53:05 +01:00
parent b2ff3026c6
commit 96d1667ae6
17 changed files with 2384 additions and 56 deletions

View File

@@ -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')