skayn-trading-sdk
Version:
Professional Bitcoin trading strategy backtesting framework with institutional-grade architecture
292 lines (250 loc) • 9.48 kB
JavaScript
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();