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
This commit is contained in:
240
app/api/trading/validate-positions/route.ts
Normal file
240
app/api/trading/validate-positions/route.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user