/** * Trading Validation Utilities for autonoma Agent Core * * Validation functions for trading data, orders, and parameters. */ import { OrderRequest, Portfolio, RiskLimits } from './types.js'; // ============================================================================= // Order Validation // ============================================================================= export interface ValidationResult { isValid: boolean; errors: string[]; warnings?: string[]; } /** * Validate order request */ export function validateOrderRequest(orderRequest: OrderRequest): ValidationResult { const errors: string[] = []; const warnings: string[] = []; // Basic required fields if (!orderRequest.symbol) { errors.push('Symbol is required'); } if (!orderRequest.side || !['buy', 'sell'].includes(orderRequest.side)) { errors.push('Valid side (buy/sell) is required'); } if (!orderRequest.type || !['market', 'limit', 'stop', 'stop-limit'].includes(orderRequest.type)) { errors.push('Valid order type is required'); } if (!orderRequest.quantity || orderRequest.quantity <= 0) { errors.push('Quantity must be positive'); } // Price validation for limit orders if (orderRequest.type === 'limit' && (!orderRequest.price || orderRequest.price <= 0)) { errors.push('Price is required for limit orders'); } // Stop price validation for stop orders if (orderRequest.type === 'stop' && (!orderRequest.stopPrice || orderRequest.stopPrice <= 0)) { errors.push('Stop price is required for stop orders'); } // Symbol format validation if (orderRequest.symbol && !isValidSymbolFormat(orderRequest.symbol)) { errors.push('Invalid symbol format. Expected format: BASE/QUOTE'); } // Warnings for potential issues if (orderRequest.quantity && orderRequest.quantity > 1000000) { warnings.push('Large order quantity may cause market impact'); } return { isValid: errors.length === 0, errors, warnings }; } /** * Validate symbol format */ function isValidSymbolFormat(symbol: string): boolean { const regex = /^[A-Z]{2,10}\/[A-Z]{2,10}$/; return regex.test(symbol); } /** * Validate order against risk limits */ export function validateOrderAgainstRiskLimits( orderRequest: OrderRequest, portfolio: Portfolio, riskLimits: RiskLimits ): ValidationResult { const errors: string[] = []; const warnings: string[] = []; // Check if symbol is blacklisted if (riskLimits.blacklistedSymbols.includes(orderRequest.symbol)) { errors.push(`Symbol ${orderRequest.symbol} is blacklisted`); } // Calculate order value const orderValue = orderRequest.price ? orderRequest.price * orderRequest.quantity : 0; // Check position size limit if (orderValue > 0) { const positionSizePercent = (orderValue / portfolio.totalValue) * 100; if (positionSizePercent > riskLimits.maxPositionSize) { errors.push(`Order would exceed maximum position size of ${riskLimits.maxPositionSize}%`); } } // Check available cash for buy orders if (orderRequest.side === 'buy' && orderValue > portfolio.cash) { errors.push('Insufficient cash for buy order'); } // Check position availability for sell orders if (orderRequest.side === 'sell') { const position = portfolio.positions.find(p => p.symbol === orderRequest.symbol); if (!position || position.quantity < orderRequest.quantity) { errors.push('Insufficient position for sell order'); } } return { isValid: errors.length === 0, errors, warnings }; } // ============================================================================= // Portfolio Validation // ============================================================================= /** * Validate portfolio consistency */ export function validatePortfolio(portfolio: Portfolio): ValidationResult { const errors: string[] = []; const warnings: string[] = []; // Check for negative cash if (portfolio.cash < 0) { errors.push('Portfolio cash cannot be negative'); } // Check for negative positions portfolio.positions.forEach((position, index) => { if (position.quantity < 0) { errors.push(`Position ${index} has negative quantity`); } if (position.averagePrice <= 0) { errors.push(`Position ${index} has invalid average price`); } }); // Check for duplicate positions const symbols = portfolio.positions.map(p => p.symbol); const duplicates = symbols.filter((symbol, index) => symbols.indexOf(symbol) !== index); if (duplicates.length > 0) { errors.push(`Duplicate positions found: ${duplicates.join(', ')}`); } // Warnings for portfolio health const totalValue = portfolio.totalValue; if (totalValue > 0) { const cashPercent = (portfolio.cash / totalValue) * 100; if (cashPercent > 90) { warnings.push('Portfolio is highly concentrated in cash'); } else if (cashPercent < 5) { warnings.push('Portfolio has very low cash reserves'); } } return { isValid: errors.length === 0, errors, warnings }; } // ============================================================================= // Risk Validation // ============================================================================= /** * Validate risk limits configuration */ export function validateRiskLimits(riskLimits: RiskLimits): ValidationResult { const errors: string[] = []; const warnings: string[] = []; // Check percentage limits if (riskLimits.maxPositionSize <= 0 || riskLimits.maxPositionSize > 100) { errors.push('Max position size must be between 0 and 100 percent'); } if (riskLimits.maxDrawdown <= 0 || riskLimits.maxDrawdown > 100) { errors.push('Max drawdown must be between 0 and 100 percent'); } // Check dollar limits if (riskLimits.maxDailyLoss <= 0) { errors.push('Max daily loss must be positive'); } // Check leverage if (riskLimits.maxLeverage <= 0) { errors.push('Max leverage must be positive'); } // Warnings for conservative limits if (riskLimits.maxPositionSize > 20) { warnings.push('Position size limit above 20% may be risky'); } if (riskLimits.maxLeverage > 10) { warnings.push('High leverage increases risk significantly'); } return { isValid: errors.length === 0, errors, warnings }; } /** * Check if portfolio violates risk limits */ export function checkRiskLimitViolations( portfolio: Portfolio, riskLimits: RiskLimits, dailyPnL: number ): ValidationResult { const errors: string[] = []; const warnings: string[] = []; // Check daily loss limit if (dailyPnL < 0 && Math.abs(dailyPnL) > riskLimits.maxDailyLoss) { errors.push(`Daily loss of $${Math.abs(dailyPnL)} exceeds limit of $${riskLimits.maxDailyLoss}`); } // Check position concentration portfolio.positions.forEach(position => { const positionPercent = (position.marketValue / portfolio.totalValue) * 100; if (positionPercent > riskLimits.maxPositionSize) { errors.push(`Position in ${position.symbol} (${positionPercent.toFixed(1)}%) exceeds limit of ${riskLimits.maxPositionSize}%`); } }); // Check for blacklisted symbols const blacklistedPositions = portfolio.positions.filter(p => riskLimits.blacklistedSymbols.includes(p.symbol) ); if (blacklistedPositions.length > 0) { errors.push(`Positions in blacklisted symbols: ${blacklistedPositions.map(p => p.symbol).join(', ')}`); } return { isValid: errors.length === 0, errors, warnings }; } // ============================================================================= // Price Validation // ============================================================================= /** * Validate price data */ export function validatePriceData(prices: number[]): ValidationResult { const errors: string[] = []; const warnings: string[] = []; if (prices.length === 0) { errors.push('Price data is empty'); return { isValid: false, errors, warnings }; } // Check for negative prices const negativeCount = prices.filter(price => price <= 0).length; if (negativeCount > 0) { errors.push(`Found ${negativeCount} non-positive prices`); } // Check for extreme outliers const mean = prices.reduce((sum, price) => sum + price, 0) / prices.length; const outliers = prices.filter(price => Math.abs(price - mean) > mean * 2); if (outliers.length > 0) { warnings.push(`Found ${outliers.length} potential price outliers`); } return { isValid: errors.length === 0, errors, warnings }; } /** * Validate price is within reasonable bounds */ export function validatePriceRange( price: number, referencePrice: number, maxDeviationPercent: number = 10 ): ValidationResult { const errors: string[] = []; if (price <= 0) { errors.push('Price must be positive'); } const deviation = Math.abs((price - referencePrice) / referencePrice) * 100; if (deviation > maxDeviationPercent) { errors.push(`Price deviation of ${deviation.toFixed(1)}% exceeds maximum of ${maxDeviationPercent}%`); } return { isValid: errors.length === 0, errors }; }