/** * Trading Formatters for autonoma Agent Core * * Formatting utilities for displaying trading data in a consistent manner. */ import { Order, Position, Portfolio, TokenPrice, YieldOpportunity } from './types.js'; // ============================================================================= // Number Formatting // ============================================================================= /** * Format currency amount with appropriate decimal places */ export function formatCurrency( amount: number, currency: string = 'USD', decimals?: number ): string { // Auto-determine decimal places based on amount if (decimals === undefined) { if (amount >= 1000) decimals = 0; else if (amount >= 1) decimals = 2; else if (amount >= 0.01) decimals = 4; else decimals = 8; } return new Intl.NumberFormat('en-US', { style: 'currency', currency: currency, minimumFractionDigits: decimals, maximumFractionDigits: decimals }).format(amount); } /** * Format cryptocurrency amount with appropriate precision */ export function formatCrypto(amount: number, symbol: string, decimals?: number): string { if (decimals === undefined) { // Common crypto decimal places const cryptoDecimals: Record = { 'BTC': 8, 'ETH': 6, 'USDT': 2, 'USDC': 2, 'BNB': 4, 'SOL': 4, 'ADA': 6, 'DOT': 4 }; decimals = cryptoDecimals[symbol.toUpperCase()] || 6; } return `${amount.toFixed(decimals)} ${symbol}`; } /** * Format percentage with color coding */ export function formatPercentage( value: number, decimals: number = 2, showSign: boolean = true ): { text: string; color: 'green' | 'red' | 'gray' } { const sign = showSign && value >= 0 ? '+' : ''; const text = `${sign}${value.toFixed(decimals)}%`; let color: 'green' | 'red' | 'gray'; if (value > 0) color = 'green'; else if (value < 0) color = 'red'; else color = 'gray'; return { text, color }; } /** * Format large numbers with appropriate units (K, M, B) */ export function formatLargeNumber(value: number, decimals: number = 1): string { const units = [ { threshold: 1e12, suffix: 'T' }, { threshold: 1e9, suffix: 'B' }, { threshold: 1e6, suffix: 'M' }, { threshold: 1e3, suffix: 'K' } ]; for (const unit of units) { if (Math.abs(value) >= unit.threshold) { return `${(value / unit.threshold).toFixed(decimals)}${unit.suffix}`; } } return value.toFixed(decimals); } // ============================================================================= // Date & Time Formatting // ============================================================================= /** * Format date for trading displays */ export function formatTradingDate(date: Date): string { return new Intl.DateTimeFormat('en-US', { month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false }).format(date); } /** * Format relative time (e.g., "2 minutes ago") */ export function formatRelativeTime(date: Date): string { const now = new Date(); const diffMs = now.getTime() - date.getTime(); const diffSeconds = Math.floor(diffMs / 1000); const diffMinutes = Math.floor(diffSeconds / 60); const diffHours = Math.floor(diffMinutes / 60); const diffDays = Math.floor(diffHours / 24); if (diffSeconds < 60) return 'just now'; if (diffMinutes < 60) return `${diffMinutes}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; return formatTradingDate(date); } /** * Format duration in human readable format */ export function formatDuration(milliseconds: number): string { const seconds = Math.floor(milliseconds / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); if (days > 0) return `${days}d ${hours % 24}h`; if (hours > 0) return `${hours}h ${minutes % 60}m`; if (minutes > 0) return `${minutes}m ${seconds % 60}s`; return `${seconds}s`; } // ============================================================================= // Trading Object Formatting // ============================================================================= /** * Format order for display */ export function formatOrder(order: Order): { id: string; symbol: string; side: string; type: string; amount: string; price: string; status: string; filled: string; time: string; } { return { id: order.id.slice(0, 8), symbol: order.symbol, side: order.side.toUpperCase(), type: order.type.toUpperCase(), amount: formatCrypto(order.quantity, order.symbol.split('/')[0] ?? ''), price: order.price ? formatCurrency(order.price) : 'Market', status: order.status.toUpperCase(), filled: `${((order.filledQuantity / order.quantity) * 100).toFixed(1)}%`, time: formatRelativeTime(order.createdAt) }; } /** * Format position for display */ export function formatPosition(position: Position): { symbol: string; quantity: string; avgPrice: string; marketValue: string; pnl: { text: string; color: 'green' | 'red' | 'gray' }; change: { text: string; color: 'green' | 'red' | 'gray' }; } { const currentPrice = position.marketValue / position.quantity; const change = ((currentPrice - position.averagePrice) / position.averagePrice) * 100; return { symbol: position.symbol, quantity: formatCrypto(position.quantity, position.symbol.split('/')[0] ?? ''), avgPrice: formatCurrency(position.averagePrice), marketValue: formatCurrency(position.marketValue), pnl: formatPercentage(position.unrealizedPnL), change: formatPercentage(change) }; } /** * Format portfolio summary */ export function formatPortfolioSummary(portfolio: Portfolio): { totalValue: string; cash: string; invested: string; dayPnL: { text: string; color: 'green' | 'red' | 'gray' }; totalPnL: { text: string; color: 'green' | 'red' | 'gray' }; lastUpdate: string; } { const investedValue = portfolio.totalValue - portfolio.cash; return { totalValue: formatCurrency(portfolio.totalValue), cash: formatCurrency(portfolio.cash), invested: formatCurrency(investedValue), dayPnL: formatPercentage(portfolio.dayPnL), totalPnL: formatPercentage(portfolio.totalPnL), lastUpdate: formatRelativeTime(portfolio.lastUpdate) }; } /** * Format token price for display */ export function formatTokenPrice(price: TokenPrice): { symbol: string; price: string; change: { text: string; color: 'green' | 'red' | 'gray' }; volume: string; marketCap: string; lastUpdate: string; } { return { symbol: price.symbol, price: formatCurrency(price.price), change: formatPercentage(price.change24h), volume: formatLargeNumber(price.volume24h), marketCap: price.marketCap ? formatLargeNumber(price.marketCap) : 'N/A', lastUpdate: formatRelativeTime(price.timestamp) }; } /** * Format yield opportunity for display */ export function formatYieldOpportunity(opportunity: YieldOpportunity): { protocol: string; pool: string; apy: { text: string; color: 'green' | 'red' | 'gray' }; tvl: string; risk: { text: string; color: 'green' | 'yellow' | 'red' }; category: string; blockchain: string; minDeposit: string; } { const riskColors = { low: 'green' as const, medium: 'yellow' as const, high: 'red' as const }; return { protocol: opportunity.protocol, pool: opportunity.pool, apy: { text: `${opportunity.apy.toFixed(2)}%`, color: 'green' }, tvl: formatLargeNumber(opportunity.tvl), risk: { text: opportunity.risk.toUpperCase(), color: riskColors[opportunity.risk] }, category: opportunity.category.charAt(0).toUpperCase() + opportunity.category.slice(1), blockchain: opportunity.blockchain, minDeposit: opportunity.minimumDeposit ? formatCurrency(opportunity.minimumDeposit) : 'No minimum' }; } // ============================================================================= // Status & Badge Formatting // ============================================================================= /** * Format order status with color coding */ export function formatOrderStatus(status: string): { text: string; color: 'green' | 'yellow' | 'red' | 'blue' | 'gray'; } { const statusMap = { pending: { text: 'PENDING', color: 'yellow' as const }, open: { text: 'OPEN', color: 'blue' as const }, filled: { text: 'FILLED', color: 'green' as const }, canceled: { text: 'CANCELED', color: 'gray' as const }, rejected: { text: 'REJECTED', color: 'red' as const } }; return statusMap[status.toLowerCase() as keyof typeof statusMap] || { text: status.toUpperCase(), color: 'gray' }; } /** * Format risk level with color coding */ export function formatRiskLevel(risk: string): { text: string; color: 'green' | 'yellow' | 'red'; icon: string; } { const riskMap = { low: { text: 'LOW RISK', color: 'green' as const, icon: '🟢' }, medium: { text: 'MEDIUM RISK', color: 'yellow' as const, icon: '🟡' }, high: { text: 'HIGH RISK', color: 'red' as const, icon: '🔴' } }; return riskMap[risk.toLowerCase() as keyof typeof riskMap] || { text: risk.toUpperCase(), color: 'yellow', icon: '⚪' }; } // ============================================================================= // Chart Data Formatting // ============================================================================= /** * Format data for chart display */ export function formatChartData( data: Array<{ timestamp: Date; value: number }>, type: 'price' | 'percentage' | 'volume' ): Array<{ x: string; y: number; formatted: string }> { return data.map(point => ({ x: point.timestamp.toISOString(), y: point.value, formatted: type === 'price' ? formatCurrency(point.value) : type === 'percentage' ? formatPercentage(point.value).text : formatLargeNumber(point.value) })); } /** * Format trading session summary */ export function formatTradingSession(session: { duration: number; trades: number; pnl: number; winRate: number; avgTradeSize: number; }): { duration: string; trades: string; pnl: { text: string; color: 'green' | 'red' | 'gray' }; winRate: { text: string; color: 'green' | 'red' | 'gray' }; avgTradeSize: string; } { return { duration: formatDuration(session.duration), trades: session.trades.toString(), pnl: formatPercentage(session.pnl), winRate: formatPercentage(session.winRate, 1), avgTradeSize: formatCurrency(session.avgTradeSize) }; }