From dde25ad2c1587227109e00fb251f47d9c0b8fec6 Mon Sep 17 00:00:00 2001 From: mindesbunister Date: Mon, 27 Oct 2025 19:20:36 +0100 Subject: [PATCH] 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 --- app/api/trading/validate-positions/route.ts | 240 ++++++++++++++++++++ telegram_command_bot.py | 78 +++++++ 2 files changed, 318 insertions(+) create mode 100644 app/api/trading/validate-positions/route.ts diff --git a/app/api/trading/validate-positions/route.ts b/app/api/trading/validate-positions/route.ts new file mode 100644 index 0000000..be09b91 --- /dev/null +++ b/app/api/trading/validate-positions/route.ts @@ -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> { + 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 } + ) + } +} diff --git a/telegram_command_bot.py b/telegram_command_bot.py index 15d2f7c..8e021a9 100644 --- a/telegram_command_bot.py +++ b/telegram_command_bot.py @@ -99,6 +99,82 @@ async def status_command(update: Update, context: ContextTypes.DEFAULT_TYPE): print(f"āŒ Error: {e}", flush=True) await update.message.reply_text(f"āŒ Error: {str(e)}") +async def validate_command(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Handle /validate command - check if positions match settings""" + + # Only process from YOUR chat + if update.message.chat_id != ALLOWED_CHAT_ID: + await update.message.reply_text("āŒ Unauthorized") + return + + print(f"šŸ” /validate command received", flush=True) + + try: + # Fetch validation from trading bot API + response = requests.post( + f"{TRADING_BOT_URL}/api/trading/validate-positions", + headers={'Authorization': f'Bearer {API_SECRET_KEY}'}, + timeout=10 + ) + + print(f"šŸ“„ API Response: {response.status_code}", flush=True) + + if not response.ok: + await update.message.reply_text(f"āŒ Error validating positions: {response.status_code}") + return + + data = response.json() + + if not data.get('success'): + await update.message.reply_text("āŒ Failed to validate positions") + return + + # Get summary + summary = data.get('summary', {}) + config = data.get('config', {}) + positions = data.get('positions', []) + + if not positions: + await update.message.reply_text("šŸ“Š *No positions to validate*\n\nAll clear!", parse_mode='Markdown') + return + + # Build validation report + message = "šŸ” *Position Validation Report*\n\n" + message += f"*Current Settings:*\n" + message += f" Leverage: {config.get('leverage')}x\n" + message += f" Position Size: ${config.get('positionSize')}\n" + message += f" TP1: {config.get('tp1Percent')}%\n" + message += f" TP2: {config.get('tp2Percent')}%\n" + message += f" SL: {config.get('stopLossPercent')}%\n\n" + + message += f"*Summary:*\n" + message += f" Total: {summary.get('totalPositions')}\n" + message += f" āœ… Valid: {summary.get('validPositions')}\n" + message += f" āš ļø Issues: {summary.get('positionsWithIssues')}\n\n" + + # Show each position with issues + for pos in positions: + if not pos['isValid']: + message += f"*{pos['symbol']} {pos['direction'].upper()}*\n" + message += f"Entry: ${pos['entryPrice']:.4f}\n" + + for issue in pos['issues']: + emoji = "āŒ" if issue['type'] == 'error' else "āš ļø" + message += f"{emoji} {issue['message']}\n" + + message += "\n" + + if summary.get('validPositions') == summary.get('totalPositions'): + message = "āœ… *All positions valid!*\n\n" + message + + await update.message.reply_text(message, parse_mode='Markdown') + + print(f"āœ… Validation sent", flush=True) + + except Exception as e: + print(f"āŒ Error: {e}", flush=True) + await update.message.reply_text(f"āŒ Error: {str(e)}") + async def trade_command(update: Update, context: ContextTypes.DEFAULT_TYPE): """Handle trade commands like /buySOL, /sellBTC, etc.""" @@ -161,6 +237,7 @@ def main(): print(f"šŸ¤– Trading Bot: {TRADING_BOT_URL}", flush=True) print(f"\nāœ… Commands:", flush=True) print(f" /status - Show open positions", flush=True) + print(f" /validate - Validate positions against settings", flush=True) print(f" /buySOL, /sellSOL", flush=True) print(f" /buyBTC, /sellBTC", flush=True) print(f" /buyETH, /sellETH", flush=True) @@ -170,6 +247,7 @@ def main(): # Add command handlers application.add_handler(CommandHandler("status", status_command)) + application.add_handler(CommandHandler("validate", validate_command)) application.add_handler(CommandHandler("buySOL", trade_command)) application.add_handler(CommandHandler("sellSOL", trade_command)) application.add_handler(CommandHandler("buyBTC", trade_command))