/** * Risk Check Middleware * * Pre-trade risk validation to ensure orders comply with risk limits. * * @version 1.0.0 * @author Trading Platform Trading Platform */ import { riskConfig } from '../config'; import { getBinanceClient } from '../services/binance-client'; import { logger } from '../utils/logger'; // ========================================== // Types // ========================================== export interface RiskCheckParams { symbol: string; side: 'buy' | 'sell'; amount: number; price?: number; } export interface RiskCheckResult { allowed: boolean; reason?: string; warnings?: string[]; orderValue?: number; } // Daily volume tracking (in-memory, resets on restart) let dailyVolume = 0; let lastVolumeResetDate = new Date().toDateString(); // ========================================== // Risk Check Functions // ========================================== /** * Reset daily volume counter at midnight */ function checkAndResetDailyVolume(): void { const today = new Date().toDateString(); if (today !== lastVolumeResetDate) { dailyVolume = 0; lastVolumeResetDate = today; logger.info('Daily volume counter reset'); } } /** * Get the quote asset from a symbol (e.g., USDT from BTCUSDT) */ function getQuoteAsset(symbol: string): string { const stablecoins = ['USDT', 'BUSD', 'USDC', 'TUSD', 'DAI']; for (const stable of stablecoins) { if (symbol.endsWith(stable)) { return stable; } } return 'USDT'; } /** * Perform comprehensive risk check before order execution */ export async function performRiskCheck(params: RiskCheckParams): Promise { const { symbol, side, amount, price } = params; const warnings: string[] = []; try { checkAndResetDailyVolume(); const client = getBinanceClient(); // 1. Get current price if not provided let orderPrice = price; if (!orderPrice) { try { orderPrice = await client.getCurrentPrice(symbol); } catch (error) { logger.warn(`Could not fetch current price for ${symbol}, using amount as value estimate`); orderPrice = 1; // Fallback } } // 2. Calculate order value in quote currency (usually USDT) const orderValue = amount * orderPrice; // 3. Check maximum order value if (orderValue > riskConfig.maxOrderValueUsdt) { return { allowed: false, reason: `Order value ${orderValue.toFixed(2)} USDT exceeds maximum ${riskConfig.maxOrderValueUsdt} USDT`, orderValue, }; } // 4. Check daily volume limit if (dailyVolume + orderValue > riskConfig.maxDailyVolumeUsdt) { return { allowed: false, reason: `Daily volume limit reached. Current: ${dailyVolume.toFixed(2)} USDT, Limit: ${riskConfig.maxDailyVolumeUsdt} USDT`, orderValue, }; } // 5. Check if API keys are configured for trading if (!client.isConfigured()) { return { allowed: false, reason: 'Binance API keys are not configured. Cannot execute trades.', orderValue, }; } // 6. Verify we can connect to Binance const connected = await client.isConnected(); if (!connected) { return { allowed: false, reason: 'Cannot connect to Binance. Please check your network and API configuration.', orderValue, }; } // 7. Check balance for buy orders (if we have account access) if (side === 'buy') { try { const account = await client.getAccount(); const quoteAsset = getQuoteAsset(symbol); const quoteBalance = account.balances.find(b => b.asset === quoteAsset); const available = quoteBalance?.free ?? 0; if (available < orderValue) { return { allowed: false, reason: `Insufficient ${quoteAsset} balance. Required: ${orderValue.toFixed(2)}, Available: ${available.toFixed(2)}`, orderValue, }; } // Warning if using more than 50% of available balance if (orderValue > available * 0.5) { warnings.push(`This order uses ${((orderValue / available) * 100).toFixed(1)}% of your available ${quoteAsset}`); } } catch (error) { warnings.push('Could not verify account balance'); logger.warn('Balance check failed', { error }); } } // 8. Check for large order warning if (orderValue > riskConfig.maxOrderValueUsdt * 0.5) { warnings.push(`Large order: ${orderValue.toFixed(2)} USDT (${((orderValue / riskConfig.maxOrderValueUsdt) * 100).toFixed(0)}% of max)`); } // All checks passed return { allowed: true, orderValue, warnings: warnings.length > 0 ? warnings : undefined, }; } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; logger.error('Risk check failed', { error, params }); return { allowed: false, reason: `Risk check error: ${message}`, }; } } /** * Record executed trade volume */ export function recordTradeVolume(orderValue: number): void { checkAndResetDailyVolume(); dailyVolume += orderValue; logger.info(`Trade recorded. Daily volume: ${dailyVolume.toFixed(2)} USDT`); } /** * Get current daily volume */ export function getDailyVolume(): number { checkAndResetDailyVolume(); return dailyVolume; } /** * Get remaining daily volume allowance */ export function getRemainingDailyVolume(): number { checkAndResetDailyVolume(); return Math.max(0, riskConfig.maxDailyVolumeUsdt - dailyVolume); } // ========================================== // Exports // ========================================== export default { performRiskCheck, recordTradeVolume, getDailyVolume, getRemainingDailyVolume, };