diff --git a/app/api/trading/check-risk/route.ts b/app/api/trading/check-risk/route.ts index f4c22ca..666fb52 100644 --- a/app/api/trading/check-risk/route.ts +++ b/app/api/trading/check-risk/route.ts @@ -6,9 +6,10 @@ */ import { NextRequest, NextResponse } from 'next/server' -import { getMergedConfig } from '@/config/trading' -import { getInitializedPositionManager } from '@/lib/trading/position-manager' +import { getMergedConfig, TradingConfig } from '@/config/trading' +import { getInitializedPositionManager, ActiveTrade } from '@/lib/trading/position-manager' import { getLastTradeTime, getLastTradeTimeForSymbol, getTradesInLastHour, getTodayPnL } from '@/lib/database/trades' +import { getPythPriceMonitor } from '@/lib/pyth/price-monitor' export interface RiskCheckRequest { symbol: string @@ -29,6 +30,98 @@ export interface RiskCheckResponse { qualityReasons?: string[] } +/** + * Position Scaling Validation + * Determines if adding to an existing position is allowed + */ +function shouldAllowScaling( + existingTrade: ActiveTrade, + newSignal: RiskCheckRequest, + config: TradingConfig +): { allowed: boolean; reasons: string[]; qualityScore?: number; qualityReasons?: string[] } { + const reasons: string[] = [] + + // Check if we have context metrics + if (!newSignal.atr || !newSignal.adx || !newSignal.pricePosition) { + reasons.push('Missing signal metrics for scaling validation') + return { allowed: false, reasons } + } + + // 1. Calculate new signal quality score + const qualityScore = scoreSignalQuality({ + atr: newSignal.atr, + adx: newSignal.adx, + rsi: newSignal.rsi || 50, + volumeRatio: newSignal.volumeRatio || 1, + pricePosition: newSignal.pricePosition, + direction: newSignal.direction, + minScore: config.minScaleQualityScore, + }) + + // 2. Check quality score (higher bar than initial entry) + if (qualityScore.score < config.minScaleQualityScore) { + reasons.push(`Quality score too low: ${qualityScore.score} (need ${config.minScaleQualityScore}+)`) + return { allowed: false, reasons, qualityScore: qualityScore.score, qualityReasons: qualityScore.reasons } + } + + // 3. Check current position profitability + const priceMonitor = getPythPriceMonitor() + const latestPrice = priceMonitor.getCachedPrice(newSignal.symbol) + const currentPrice = latestPrice?.price + + if (!currentPrice) { + reasons.push('Unable to fetch current price') + return { allowed: false, reasons, qualityScore: qualityScore.score } + } + + const pnlPercent = existingTrade.direction === 'long' + ? ((currentPrice - existingTrade.entryPrice) / existingTrade.entryPrice) * 100 + : ((existingTrade.entryPrice - currentPrice) / existingTrade.entryPrice) * 100 + + if (pnlPercent < config.minProfitForScale) { + reasons.push(`Position not profitable enough: ${pnlPercent.toFixed(2)}% (need ${config.minProfitForScale}%+)`) + return { allowed: false, reasons, qualityScore: qualityScore.score } + } + + // 4. Check ADX trend strengthening + const originalAdx = existingTrade.originalAdx || 0 + const adxIncrease = newSignal.adx - originalAdx + + if (adxIncrease < config.minAdxIncrease) { + reasons.push(`ADX not strengthening enough: +${adxIncrease.toFixed(1)} (need +${config.minAdxIncrease})`) + return { allowed: false, reasons, qualityScore: qualityScore.score } + } + + // 5. Check price position (don't chase near resistance) + if (newSignal.pricePosition > config.maxPricePositionForScale) { + reasons.push(`Price too high in range: ${newSignal.pricePosition.toFixed(0)}% (max ${config.maxPricePositionForScale}%)`) + return { allowed: false, reasons, qualityScore: qualityScore.score } + } + + // 6. Check max position size (if already scaled) + const totalScaled = existingTrade.timesScaled || 0 + const currentMultiplier = 1 + (totalScaled * (config.scaleSizePercent / 100)) + const newMultiplier = currentMultiplier + (config.scaleSizePercent / 100) + + if (newMultiplier > config.maxScaleMultiplier) { + reasons.push(`Max position size reached: ${(currentMultiplier * 100).toFixed(0)}% (max ${(config.maxScaleMultiplier * 100).toFixed(0)}%)`) + return { allowed: false, reasons, qualityScore: qualityScore.score } + } + + // All checks passed! + reasons.push(`Quality: ${qualityScore.score}/100`) + reasons.push(`P&L: +${pnlPercent.toFixed(2)}%`) + reasons.push(`ADX increased: +${adxIncrease.toFixed(1)}`) + reasons.push(`Price position: ${newSignal.pricePosition.toFixed(0)}%`) + + return { + allowed: true, + reasons, + qualityScore: qualityScore.score, + qualityReasons: qualityScore.reasons + } +} + export async function POST(request: NextRequest): Promise> { try { // Verify authorization @@ -57,8 +150,33 @@ export async function POST(request: NextRequest): Promise trade.symbol === body.symbol) if (existingPosition) { - // Check if it's the SAME direction (duplicate - block it) + // SAME direction - check if position scaling is allowed if (existingPosition.direction === body.direction) { + // Position scaling feature + if (config.enablePositionScaling) { + const scalingCheck = shouldAllowScaling(existingPosition, body, config) + + if (scalingCheck.allowed) { + console.log('✅ Position scaling ALLOWED:', scalingCheck.reasons) + return NextResponse.json({ + allowed: true, + reason: 'Position scaling', + details: `Scaling into ${body.direction} position - ${scalingCheck.reasons.join(', ')}`, + qualityScore: scalingCheck.qualityScore, + qualityReasons: scalingCheck.qualityReasons, + }) + } else { + console.log('🚫 Position scaling BLOCKED:', scalingCheck.reasons) + return NextResponse.json({ + allowed: false, + reason: 'Scaling not allowed', + details: scalingCheck.reasons.join(', '), + qualityScore: scalingCheck.qualityScore, + }) + } + } + + // Scaling disabled - block duplicate position console.log('🚫 Risk check BLOCKED: Duplicate position (same direction)', { symbol: body.symbol, existingDirection: existingPosition.direction, @@ -69,7 +187,7 @@ export async function POST(request: NextRequest): Promise trade.symbol === driftSymbol && trade.direction !== body.direction ) - // SAFETY CHECK: Prevent multiple positions on same symbol + // Check for same direction position (scaling vs duplicate) const sameDirectionPosition = existingTrades.find( trade => trade.symbol === driftSymbol && trade.direction === body.direction ) if (sameDirectionPosition) { + // Position scaling enabled - scale into existing position + if (config.enablePositionScaling) { + console.log(`📈 POSITION SCALING: Adding to existing ${body.direction} position on ${driftSymbol}`) + + // Calculate scale size + const scaleSize = (positionSize * leverage) * (config.scaleSizePercent / 100) + + console.log(`💰 Scaling position:`) + console.log(` Original size: $${sameDirectionPosition.positionSize}`) + console.log(` Scale size: $${scaleSize} (${config.scaleSizePercent}% of original)`) + console.log(` Leverage: ${leverage}x`) + + // Open additional position + const scaleResult = await openPosition({ + symbol: driftSymbol, + direction: body.direction, + sizeUSD: scaleSize, + slippageTolerance: config.slippageTolerance, + }) + + if (!scaleResult.success) { + console.error('❌ Failed to scale position:', scaleResult.error) + return NextResponse.json( + { + success: false, + error: 'Position scaling failed', + message: scaleResult.error, + }, + { status: 500 } + ) + } + + console.log(`✅ Scaled into position at $${scaleResult.fillPrice?.toFixed(4)}`) + + // Update Position Manager tracking + const timesScaled = (sameDirectionPosition.timesScaled || 0) + 1 + const totalScaleAdded = (sameDirectionPosition.totalScaleAdded || 0) + scaleSize + const newTotalSize = sameDirectionPosition.currentSize + (scaleResult.fillSize || 0) + + // Update the trade tracking (simplified - just update the active trade object) + sameDirectionPosition.timesScaled = timesScaled + sameDirectionPosition.totalScaleAdded = totalScaleAdded + sameDirectionPosition.currentSize = newTotalSize + + console.log(`📊 Position scaled: ${timesScaled}x total, $${totalScaleAdded.toFixed(2)} added`) + + return NextResponse.json({ + success: true, + action: 'scaled', + positionId: sameDirectionPosition.positionId, + symbol: driftSymbol, + direction: body.direction, + scalePrice: scaleResult.fillPrice, + scaleSize: scaleSize, + totalSize: newTotalSize, + timesScaled: timesScaled, + timestamp: new Date().toISOString(), + }) + } + + // Scaling disabled - block duplicate console.log(`⛔ DUPLICATE POSITION BLOCKED: Already have ${body.direction} position on ${driftSymbol}`) return NextResponse.json( { success: false, error: 'Duplicate position detected', - message: `Already have an active ${body.direction} position on ${driftSymbol}. Close it first.`, + message: `Already have an active ${body.direction} position on ${driftSymbol}. Enable position scaling in settings to add to this position.`, }, { status: 400 } ) @@ -366,6 +427,10 @@ export async function POST(request: NextRequest): Promise70% of range (near resistance) + // DEX settings priceCheckIntervalMs: 2000, // Check every 2 seconds slippageTolerance: 1.0, // 1% max slippage on market orders @@ -306,6 +324,27 @@ export function getConfigFromEnv(): Partial { trailingStopActivation: process.env.TRAILING_STOP_ACTIVATION ? parseFloat(process.env.TRAILING_STOP_ACTIVATION) : undefined, + enablePositionScaling: process.env.ENABLE_POSITION_SCALING + ? process.env.ENABLE_POSITION_SCALING === 'true' + : undefined, + minScaleQualityScore: process.env.MIN_SCALE_QUALITY_SCORE + ? parseInt(process.env.MIN_SCALE_QUALITY_SCORE) + : undefined, + minProfitForScale: process.env.MIN_PROFIT_FOR_SCALE + ? parseFloat(process.env.MIN_PROFIT_FOR_SCALE) + : undefined, + maxScaleMultiplier: process.env.MAX_SCALE_MULTIPLIER + ? parseFloat(process.env.MAX_SCALE_MULTIPLIER) + : undefined, + scaleSizePercent: process.env.SCALE_SIZE_PERCENT + ? parseFloat(process.env.SCALE_SIZE_PERCENT) + : undefined, + minAdxIncrease: process.env.MIN_ADX_INCREASE + ? parseFloat(process.env.MIN_ADX_INCREASE) + : undefined, + maxPricePositionForScale: process.env.MAX_PRICE_POSITION_FOR_SCALE + ? parseFloat(process.env.MAX_PRICE_POSITION_FOR_SCALE) + : undefined, maxDailyDrawdown: process.env.MAX_DAILY_DRAWDOWN ? parseFloat(process.env.MAX_DAILY_DRAWDOWN) : undefined, diff --git a/lib/trading/position-manager.ts b/lib/trading/position-manager.ts index be92f97..4fc38f0 100644 --- a/lib/trading/position-manager.ts +++ b/lib/trading/position-manager.ts @@ -48,6 +48,11 @@ export interface ActiveTrade { maxFavorablePrice: number // Price at best profit maxAdversePrice: number // Price at worst loss + // Position scaling tracking + originalAdx?: number // ADX at initial entry (for scaling validation) + timesScaled?: number // How many times position has been scaled + totalScaleAdded?: number // Total USD added through scaling + // Monitoring priceCheckCount: number lastPrice: number