UNPKG

skayn-trading-sdk

Version:

Professional Bitcoin trading strategy backtesting framework with institutional-grade architecture

292 lines (250 loc) 9.48 kB
const logger = require('./logger'); class ProductionErrorHandler { constructor() { this.errorCounts = new Map(); this.circuitBreakers = new Map(); this.retryConfig = { maxRetries: 3, baseDelay: 1000, maxDelay: 30000 }; } // Classify errors for better handling classifyError(error, context = {}) { const classification = { type: 'unknown', severity: 'medium', recoverable: true, userMessage: 'An unexpected error occurred', action: 'retry', context }; // Handle null/undefined errors if (!error) { classification.type = 'null_error'; classification.severity = 'high'; classification.userMessage = '🚨 Null error - API call failed completely'; classification.action = 'retry'; return classification; } // LN Markets API errors if (error.status === 500 && error.message?.includes('Internal error')) { classification.type = 'lnmarkets_internal_error'; classification.severity = 'high'; classification.userMessage = '🚨 Trading API error - checking balance and retrying...'; classification.action = 'check_balance_retry'; } // Insufficient balance (already handled well in lnmarkets.js) if (error.message?.includes('Insufficient balance') || error.message?.includes('need') && error.message?.includes('more sats')) { classification.type = 'insufficient_balance'; classification.severity = 'medium'; classification.recoverable = false; classification.userMessage = '💰 Insufficient balance for trade - deposit more sats to continue'; classification.action = 'require_deposit'; } // Network/connectivity errors if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED' || error.message?.includes('timeout')) { classification.type = 'network_error'; classification.severity = 'high'; classification.userMessage = '🌐 Network connectivity issue - will retry automatically'; classification.action = 'retry_with_backoff'; } // Market data errors if (context.component === 'market_data' || error.message?.includes('price')) { classification.type = 'market_data_error'; classification.severity = 'critical'; classification.recoverable = false; classification.userMessage = '📊 Cannot fetch Bitcoin price - trading paused for safety'; classification.action = 'halt_trading'; } // Authentication errors if (error.status === 401 || error.status === 403 || error.message?.includes('unauthorized')) { classification.type = 'auth_error'; classification.severity = 'critical'; classification.recoverable = false; classification.userMessage = '🔐 API authentication failed - check your credentials'; classification.action = 'require_config'; } // Strategy execution errors if (context.component === 'strategy' || context.component === 'adaptive_strategy') { classification.type = 'strategy_error'; classification.severity = 'medium'; classification.userMessage = '🧠 Strategy analysis failed - falling back to conservative mode'; classification.action = 'fallback_strategy'; } return classification; } // Handle errors with retry logic and circuit breaking async handleError(error, context = {}, retryFunction = null) { const classification = this.classifyError(error, context); const errorKey = `${classification.type}_${context.component || 'unknown'}`; // Track error frequency this.errorCounts.set(errorKey, (this.errorCounts.get(errorKey) || 0) + 1); // Log with structured data logger.error('🚨 Production Error Handled - FULL DEBUG', { originalError: error.message, errorStack: error.stack, errorStatus: error.status, errorCode: error.code, fullError: error, error: { message: error.message, stack: error.stack, status: error.status, code: error.code }, classification, frequency: this.errorCounts.get(errorKey), context, timestamp: new Date().toISOString() }); // Check circuit breaker if (this.shouldCircuitBreak(errorKey)) { return { success: false, error: `🔴 Circuit breaker activated for ${classification.type}`, classification, circuitBreaker: true }; } // Handle based on classification switch (classification.action) { case 'retry_with_backoff': if (retryFunction) { return await this.retryWithBackoff(retryFunction, context); } break; case 'check_balance_retry': return await this.handleBalanceCheckRetry(error, context, retryFunction); case 'halt_trading': return this.haltTrading(classification); case 'require_deposit': return this.requireDeposit(classification); case 'fallback_strategy': return this.fallbackStrategy(classification); default: break; } return { success: false, error: classification.userMessage, classification, needsUserAction: !classification.recoverable }; } async retryWithBackoff(retryFunction, context, attempt = 1) { if (attempt > this.retryConfig.maxRetries) { return { success: false, error: '🔴 Max retries exceeded', exhausted: true }; } const delay = Math.min( this.retryConfig.baseDelay * Math.pow(2, attempt - 1), this.retryConfig.maxDelay ); logger.info(`⏳ Retry attempt ${attempt}/${this.retryConfig.maxRetries} in ${delay}ms`, { context }); await new Promise(resolve => setTimeout(resolve, delay)); try { const result = await retryFunction(); logger.info('✅ Retry successful', { attempt, context }); return { success: true, result, retryAttempt: attempt }; } catch (retryError) { logger.warn(`❌ Retry ${attempt} failed: ${retryError.message}`, { context }); return await this.retryWithBackoff(retryFunction, context, attempt + 1); } } async handleBalanceCheckRetry(error, context, retryFunction) { // For LN Markets 500 errors, check balance first if (context.lnMarketsClient) { try { await context.lnMarketsClient.updateBalance(); logger.info('💰 Balance refreshed before retry', { balance: context.lnMarketsClient.balance }); } catch (balanceError) { logger.warn('⚠️ Could not refresh balance', { error: balanceError.message }); } } return await this.retryWithBackoff(retryFunction, context); } haltTrading(classification) { logger.error('🛑 TRADING HALTED', { reason: classification.userMessage }); return { success: false, error: classification.userMessage, action: 'halt', critical: true }; } requireDeposit(classification) { return { success: false, error: classification.userMessage, action: 'deposit', needsDeposit: true }; } fallbackStrategy(classification) { logger.warn('🔄 Falling back to conservative strategy', { reason: classification.userMessage }); return { success: false, error: classification.userMessage, action: 'fallback', fallbackStrategy: 'conservative' }; } shouldCircuitBreak(errorKey) { const count = this.errorCounts.get(errorKey) || 0; const threshold = 5; // Circuit break after 5 errors of same type if (count >= threshold) { const breakerKey = `breaker_${errorKey}`; const lastBreak = this.circuitBreakers.get(breakerKey); const cooldownPeriod = 5 * 60 * 1000; // 5 minutes if (!lastBreak || (Date.now() - lastBreak) > cooldownPeriod) { this.circuitBreakers.set(breakerKey, Date.now()); this.errorCounts.set(errorKey, 0); // Reset count return false; // Allow one more try after cooldown } return true; // Still in circuit break period } return false; } // Get user-friendly error messages for CLI/UI getUserFriendlyMessage(error, context = {}) { const classification = this.classifyError(error, context); return { message: classification.userMessage, type: classification.type, severity: classification.severity, actionRequired: !classification.recoverable }; } // Get system health status getSystemHealth() { const now = Date.now(); const recentErrors = Array.from(this.errorCounts.entries()) .filter(([key, count]) => count > 0); const activeCircuitBreakers = Array.from(this.circuitBreakers.entries()) .filter(([key, timestamp]) => (now - timestamp) < 5 * 60 * 1000); return { healthy: recentErrors.length === 0 && activeCircuitBreakers.length === 0, recentErrors: recentErrors.length, circuitBreakers: activeCircuitBreakers.length, details: { errorCounts: Object.fromEntries(this.errorCounts), activeBreakers: activeCircuitBreakers.map(([key]) => key) } }; } // Reset error tracking (for testing or manual reset) reset() { this.errorCounts.clear(); this.circuitBreakers.clear(); logger.info('🔄 Error handler state reset'); } } module.exports = new ProductionErrorHandler();