Files
trading_bot_v4/app/api/trading/validate-positions/route.ts
mindesbunister dde25ad2c1 Add position validation endpoint and Telegram /validate command
- New API endpoint: /api/trading/validate-positions
- Validates TP1, TP2, SL, leverage, and position size against current settings
- Fixed position size calculation: config stores collateral, positions store total value
- Added /validate command to Telegram bot for remote checking
- Returns detailed report of any mismatches with expected vs actual values
2025-10-27 19:20:36 +01:00

241 lines
8.3 KiB
TypeScript

/**
* Validate Positions API Endpoint
*
* Compares current open positions against configured settings
* POST /api/trading/validate-positions
*/
import { NextRequest, NextResponse } from 'next/server'
import { getMergedConfig } from '@/config/trading'
import { getInitializedPositionManager } from '@/lib/trading/position-manager'
import { getDriftService } from '@/lib/drift/client'
interface ValidationIssue {
type: 'error' | 'warning'
field: string
expected: number | string
actual: number | string
message: string
}
interface PositionValidation {
symbol: string
direction: 'long' | 'short'
entryPrice: number
isValid: boolean
issues: ValidationIssue[]
}
interface ValidationResponse {
success: boolean
timestamp: string
config: {
leverage: number
positionSize: number
tp1Percent: number
tp2Percent: number
stopLossPercent: number
useDualStops: boolean
hardStopPercent?: number
}
positions: PositionValidation[]
summary: {
totalPositions: number
validPositions: number
positionsWithIssues: number
}
}
function calculateExpectedPrice(entry: number, percent: number, direction: 'long' | 'short'): number {
if (direction === 'long') {
return entry * (1 + percent / 100)
} else {
return entry * (1 - percent / 100)
}
}
function calculateActualPercent(entry: number, price: number, direction: 'long' | 'short'): number {
if (direction === 'long') {
return ((price - entry) / entry) * 100
} else {
return ((entry - price) / entry) * 100
}
}
export async function POST(request: NextRequest): Promise<NextResponse<ValidationResponse>> {
try {
// Verify authorization
const authHeader = request.headers.get('authorization')
const expectedAuth = `Bearer ${process.env.API_SECRET_KEY}`
if (!authHeader || authHeader !== expectedAuth) {
return NextResponse.json(
{
success: false,
timestamp: new Date().toISOString(),
config: {} as any,
positions: [],
summary: {
totalPositions: 0,
validPositions: 0,
positionsWithIssues: 0,
},
},
{ status: 401 }
)
}
console.log('🔍 Validating positions against settings...')
// Get current configuration
const config = getMergedConfig()
// Get active positions from Position Manager
const positionManager = await getInitializedPositionManager()
const activeTrades = Array.from(positionManager.getActiveTrades().values())
console.log(`📊 Found ${activeTrades.length} active positions to validate`)
const validations: PositionValidation[] = []
for (const trade of activeTrades) {
const issues: ValidationIssue[] = []
// Validate leverage
const expectedLeverage = config.leverage
if (trade.leverage !== expectedLeverage) {
issues.push({
type: 'warning',
field: 'leverage',
expected: expectedLeverage,
actual: trade.leverage,
message: `Leverage mismatch: expected ${expectedLeverage}x, got ${trade.leverage}x`,
})
}
// Calculate expected prices based on current config
const expectedTP1 = calculateExpectedPrice(trade.entryPrice, config.takeProfit1Percent, trade.direction)
const expectedTP2 = calculateExpectedPrice(trade.entryPrice, config.takeProfit2Percent, trade.direction)
const expectedSL = calculateExpectedPrice(trade.entryPrice, config.stopLossPercent, trade.direction)
// Validate TP1 (allow 0.1% tolerance)
const tp1Diff = Math.abs((trade.tp1Price - expectedTP1) / expectedTP1) * 100
if (tp1Diff > 0.1) {
const actualTP1Percent = calculateActualPercent(trade.entryPrice, trade.tp1Price, trade.direction)
issues.push({
type: 'error',
field: 'takeProfit1',
expected: `${config.takeProfit1Percent}% ($${expectedTP1.toFixed(2)})`,
actual: `${actualTP1Percent.toFixed(2)}% ($${trade.tp1Price.toFixed(2)})`,
message: `TP1 price mismatch: expected ${config.takeProfit1Percent}%, actual ${actualTP1Percent.toFixed(2)}%`,
})
}
// Validate TP2 (allow 0.1% tolerance)
const tp2Diff = Math.abs((trade.tp2Price - expectedTP2) / expectedTP2) * 100
if (tp2Diff > 0.1) {
const actualTP2Percent = calculateActualPercent(trade.entryPrice, trade.tp2Price, trade.direction)
issues.push({
type: 'error',
field: 'takeProfit2',
expected: `${config.takeProfit2Percent}% ($${expectedTP2.toFixed(2)})`,
actual: `${actualTP2Percent.toFixed(2)}% ($${trade.tp2Price.toFixed(2)})`,
message: `TP2 price mismatch: expected ${config.takeProfit2Percent}%, actual ${actualTP2Percent.toFixed(2)}%`,
})
}
// Validate Stop Loss (allow 0.1% tolerance)
const slDiff = Math.abs((trade.stopLossPrice - expectedSL) / expectedSL) * 100
if (slDiff > 0.1) {
const actualSLPercent = Math.abs(calculateActualPercent(trade.entryPrice, trade.stopLossPrice, trade.direction))
issues.push({
type: 'error',
field: 'stopLoss',
expected: `${Math.abs(config.stopLossPercent)}% ($${expectedSL.toFixed(2)})`,
actual: `${actualSLPercent.toFixed(2)}% ($${trade.stopLossPrice.toFixed(2)})`,
message: `Stop loss mismatch: expected ${Math.abs(config.stopLossPercent)}%, actual ${actualSLPercent.toFixed(2)}%`,
})
}
// Validate position size
// Note: trade.positionSize is the TOTAL position value in USD (e.g., $800 with 10x leverage)
// config.positionSize is the COLLATERAL amount (e.g., $80)
// So: expectedPositionValueUSD = config.positionSize * config.leverage
const expectedPositionValueUSD = config.positionSize * config.leverage
const actualPositionValueUSD = trade.positionSize
const sizeDiff = Math.abs((actualPositionValueUSD - expectedPositionValueUSD) / expectedPositionValueUSD) * 100
if (sizeDiff > 5) { // Allow 5% tolerance for position size
issues.push({
type: 'warning',
field: 'positionSize',
expected: `$${expectedPositionValueUSD.toFixed(2)}`,
actual: `$${actualPositionValueUSD.toFixed(2)}`,
message: `Position size mismatch: expected $${expectedPositionValueUSD.toFixed(2)}, got $${actualPositionValueUSD.toFixed(2)}`,
})
}
const validation: PositionValidation = {
symbol: trade.symbol,
direction: trade.direction,
entryPrice: trade.entryPrice,
isValid: issues.length === 0,
issues,
}
validations.push(validation)
if (issues.length > 0) {
console.log(`⚠️ Position ${trade.symbol} ${trade.direction} has ${issues.length} issue(s):`)
issues.forEach(issue => {
console.log(` ${issue.type === 'error' ? '❌' : '⚠️'} ${issue.message}`)
})
} else {
console.log(`✅ Position ${trade.symbol} ${trade.direction} is valid`)
}
}
const summary = {
totalPositions: validations.length,
validPositions: validations.filter(v => v.isValid).length,
positionsWithIssues: validations.filter(v => !v.isValid).length,
}
console.log(`📊 Validation complete: ${summary.validPositions}/${summary.totalPositions} positions valid`)
return NextResponse.json({
success: true,
timestamp: new Date().toISOString(),
config: {
leverage: config.leverage,
positionSize: config.positionSize,
tp1Percent: config.takeProfit1Percent,
tp2Percent: config.takeProfit2Percent,
stopLossPercent: config.stopLossPercent,
useDualStops: config.useDualStops,
hardStopPercent: config.useDualStops ? config.hardStopPercent : undefined,
},
positions: validations,
summary,
})
} catch (error) {
console.error('❌ Position validation error:', error)
return NextResponse.json(
{
success: false,
timestamp: new Date().toISOString(),
config: {} as any,
positions: [],
summary: {
totalPositions: 0,
validPositions: 0,
positionsWithIssues: 0,
},
},
{ status: 500 }
)
}
}