UNPKG

skayn-trading-sdk

Version:

Professional Bitcoin trading strategy backtesting framework with institutional-grade architecture

477 lines (402 loc) 17.2 kB
const IStrategy = require('../interfaces/IStrategy'); /** * Momentum Swing Strategy - SDK Compliant * Quality over quantity: Swing trades only, no scalping * Focus on momentum swing (2.5%) and strong (4.0%) signals */ class MomentumSwingStrategy extends IStrategy { constructor(params = {}) { super(); this.parameters = { // EMA periods for trend detection emaFast: params.emaFast || 21, emaSlow: params.emaSlow || 50, emaLong: params.emaLong || 100, ema200: params.ema200 || 200, // Market regime detection // SIMPLIFIED momentum thresholds - Much more sensitive momentum: { mediumPeriod: params.momentum?.mediumPeriod || 7, longPeriod: params.momentum?.longPeriod || 14, swing: params.momentum?.swing || 0.005, // 0.5% VERY sensitive (was 1.5%) strong: params.momentum?.strong || 0.010 // 1.0% VERY sensitive (was 2.5%) }, // Volume confirmation volume: { enabled: params.volume?.enabled !== false, multiplier: params.volume?.multiplier || 1.2, // 1.2x average volume period: params.volume?.period || 20 }, // Breakout detection breakout: { enabled: params.breakout?.enabled !== false, lookback: params.breakout?.lookback || 10, threshold: params.breakout?.threshold || 0.005 // 0.5% }, // PERMISSIVE volatility filter - trade almost always minVolatility: params.minVolatility || 0.001, // 0.1% minimum (very low) // AGGRESSIVE exit rules - optimized for higher returns exits: { profitTarget: params.exits?.profitTarget || 0.15, // 15% profit target (MUCH higher) stopLoss: params.exits?.stopLoss || 0.08, // 8% stop loss (wider to avoid noise) timeStop: params.exits?.timeStop || 432000000 // 120 hours (5 days - let trends develop) } }; this.signalHistory = []; this.lastSignal = null; } async analyze(historicalBars, portfolioState) { try { if (!historicalBars || historicalBars.length < this.getRequiredWarmupPeriods()) { return this.createSignal('HOLD', 0, 'Insufficient historical data'); } const currentPrice = historicalBars[historicalBars.length - 1].close; // Quality filter: Only trade during high volatility periods const volatility = this.calculateVolatility(historicalBars.slice(-20)); if (volatility < this.parameters.minVolatility) { return this.createSignal('HOLD', 0, `Low volatility (${(volatility * 100).toFixed(2)}%) - waiting for movement`); } // Calculate all technical indicators const indicators = this.calculateTrendIndicators(historicalBars); if (!this.hasRequiredIndicators(indicators)) { return this.createSignal('HOLD', 0, 'Trend indicators not ready'); } // Detect market regime first - critical for bear market protection const marketRegime = this.detectMarketRegime(indicators, currentPrice); // Analyze swing signals only (no scalping) const swingSignal = this.analyzeSwingSignals(indicators, currentPrice, historicalBars); // Apply market regime filter const finalSignal = this.applyRegimeFilter(swingSignal, marketRegime); // Apply volume confirmation if enabled if (finalSignal.action !== 'HOLD' && this.parameters.volume.enabled) { const volumeConfirmation = this.checkVolumeConfirmation(indicators); if (!volumeConfirmation.confirmed) { return this.createSignal('HOLD', finalSignal.confidence * 0.5, `${finalSignal.reasons.join(', ')} but ${volumeConfirmation.reason}`); } } // Add exit rules if signal is not HOLD if (finalSignal.action !== 'HOLD') { finalSignal.exitRules = this.getExitRules(); finalSignal.tradeType = 'swing'; } // Store signal history for analysis this.signalHistory.push({ ...finalSignal, timestamp: new Date().toISOString(), price: currentPrice, regime: marketRegime, volatility: volatility }); // Keep history manageable if (this.signalHistory.length > 100) { this.signalHistory = this.signalHistory.slice(-50); } this.lastSignal = finalSignal; return finalSignal; } catch (error) { return this.createSignal('HOLD', 0, `Strategy error: ${error.message}`); } } calculateTrendIndicators(historicalBars) { const prices = historicalBars.map(bar => bar.close); const highs = historicalBars.map(bar => bar.high); const lows = historicalBars.map(bar => bar.low); const volumes = historicalBars.map(bar => bar.volume || 1000000); try { const indicators = { prices, highs, lows, volumes, // EMAs for trend direction emaFast: this.calculateEMA(prices, this.parameters.emaFast), emaSlow: this.calculateEMA(prices, this.parameters.emaSlow), emaLong: this.calculateEMA(prices, this.parameters.emaLong), // Market regime detection (only if enough data) ema200: prices.length >= this.parameters.ema200 ? this.calculateEMA(prices, this.parameters.ema200) : null, // Volume moving average volumeMA: this.calculateSMA(volumes, this.parameters.volume.period), // Price momentum calculations - SWING ONLY mediumMomentum: this.calculateMomentum(prices, this.parameters.momentum.mediumPeriod), longMomentum: this.calculateMomentum(prices, this.parameters.momentum.longPeriod) }; return indicators; } catch (error) { return null; } } detectMarketRegime(indicators, currentPrice) { const { emaFast, emaSlow, emaLong, ema200 } = indicators; // Get latest values const fastEma = emaFast[emaFast.length - 1]; const slowEma = emaSlow[emaSlow.length - 1]; const longEma = emaLong[emaLong.length - 1]; const ema200Current = ema200 ? ema200[ema200.length - 1] : null; // Regime classification with confidence scoring if (ema200Current && currentPrice > ema200Current) { // Above 200 EMA = Bull market context if (fastEma > slowEma && slowEma > longEma && currentPrice > fastEma) { return { trend: 'BULL', strength: 'STRONG', confidence: 0.9 }; } else if (fastEma > slowEma && currentPrice > fastEma) { return { trend: 'BULL', strength: 'MODERATE', confidence: 0.7 }; } else { return { trend: 'BULL', strength: 'WEAK', confidence: 0.5 }; } } else if (ema200Current && currentPrice < ema200Current) { // Below 200 EMA = Bear market context if (fastEma < slowEma && slowEma < longEma && currentPrice < fastEma) { return { trend: 'BEAR', strength: 'STRONG', confidence: 0.9 }; } else if (fastEma < slowEma && currentPrice < fastEma) { return { trend: 'BEAR', strength: 'MODERATE', confidence: 0.7 }; } else { return { trend: 'BEAR', strength: 'WEAK', confidence: 0.5 }; } } else { // No 200 EMA or sideways - use EMA alignment only if (fastEma > slowEma && slowEma > longEma) { return { trend: 'BULL', strength: 'MODERATE', confidence: 0.6 }; } else if (fastEma < slowEma && slowEma < longEma) { return { trend: 'BEAR', strength: 'MODERATE', confidence: 0.6 }; } else { return { trend: 'SIDEWAYS', strength: 'WEAK', confidence: 0.4 }; } } } analyzeSwingSignals(indicators, currentPrice, historicalBars) { const { emaFast, emaSlow, mediumMomentum, longMomentum, highs, lows } = indicators; const fastEma = emaFast[emaFast.length - 1]; const slowEma = emaSlow[emaSlow.length - 1]; const mediumMom = mediumMomentum[mediumMomentum.length - 1] || 0; const longMom = longMomentum[longMomentum.length - 1] || 0; // Calculate recent highs/lows for breakout detection const recentBars = Math.min(this.parameters.breakout.lookback, historicalBars.length); const recentHigh = Math.max(...highs.slice(-recentBars)); const recentLow = Math.min(...lows.slice(-recentBars)); // LONG swing signals if (currentPrice > fastEma && fastEma > slowEma) { // STRONG momentum + breakout if (longMom > this.parameters.momentum.strong && currentPrice > recentHigh * (1 + this.parameters.breakout.threshold)) { return this.createSignal('BUY', 0.9, [ 'STRONG bull momentum + breakout above recent high', `Long momentum: ${(longMom * 100).toFixed(2)}%`, 'EMA trend alignment confirmed' ]); } // SWING momentum + breakout else if (mediumMom > this.parameters.momentum.swing && currentPrice > recentHigh * (1 + this.parameters.breakout.threshold)) { return this.createSignal('BUY', 0.8, [ 'SWING bull momentum + breakout above recent high', `Medium momentum: ${(mediumMom * 100).toFixed(2)}%`, 'EMA trend alignment confirmed' ]); } // SWING momentum without breakout else if (mediumMom > this.parameters.momentum.swing) { return this.createSignal('BUY', 0.7, [ 'SWING bull momentum detected', `Medium momentum: ${(mediumMom * 100).toFixed(2)}%`, 'EMA trend alignment confirmed' ]); } } // SHORT swing signals if (currentPrice < fastEma && fastEma < slowEma) { // STRONG downward momentum + breakdown if (longMom < -this.parameters.momentum.strong && currentPrice < recentLow * (1 - this.parameters.breakout.threshold)) { return this.createSignal('SELL', 0.9, [ 'STRONG bear momentum + breakdown below recent low', `Long momentum: ${(longMom * 100).toFixed(2)}%`, 'EMA trend alignment confirmed' ]); } // SWING downward momentum + breakdown else if (mediumMom < -this.parameters.momentum.swing && currentPrice < recentLow * (1 - this.parameters.breakout.threshold)) { return this.createSignal('SELL', 0.8, [ 'SWING bear momentum + breakdown below recent low', `Medium momentum: ${(mediumMom * 100).toFixed(2)}%`, 'EMA trend alignment confirmed' ]); } // SWING downward momentum without breakdown else if (mediumMom < -this.parameters.momentum.swing) { return this.createSignal('SELL', 0.7, [ 'SWING bear momentum detected', `Medium momentum: ${(mediumMom * 100).toFixed(2)}%`, 'EMA trend alignment confirmed' ]); } } return this.createSignal('HOLD', 0, 'No qualifying swing momentum signals'); } applyRegimeFilter(signal, marketRegime) { if (signal.action === 'HOLD') return signal; // AGGRESSIVE MODE: Take most signals regardless of regime // Only filter extremely weak signals in bear markets if (marketRegime.trend === 'BEAR' && signal.action === 'BUY') { if (signal.confidence < 0.60) { // Much lower threshold (was 0.85) return this.createSignal('HOLD', 0.3, [ ...signal.reasons, `Filtered: Very weak signal in bear market (${signal.confidence.toFixed(2)} < 0.60)` ]); } } // Be less restrictive in sideways markets if (marketRegime.trend === 'SIDEWAYS' && signal.confidence < 0.50) { // Much lower (was 0.8) return this.createSignal('HOLD', 0.3, [ ...signal.reasons, `Filtered: Very weak signal in sideways market (${signal.confidence.toFixed(2)} < 0.50)` ]); } // Less regime confidence adjustment (don't penalize as much) const adjustedConfidence = signal.confidence * Math.max(0.8, marketRegime.confidence); return this.createSignal(signal.action, adjustedConfidence, [ ...signal.reasons, `Market regime: ${marketRegime.trend} (${marketRegime.strength})` ]); } checkVolumeConfirmation(indicators) { const { volumes, volumeMA } = indicators; if (!this.parameters.volume.enabled || !volumeMA || volumeMA.length === 0) { return { confirmed: true, reason: 'Volume confirmation disabled' }; } const currentVolume = volumes[volumes.length - 1]; const avgVolume = volumeMA[volumeMA.length - 1]; const confirmed = currentVolume >= avgVolume * this.parameters.volume.multiplier; const reason = confirmed ? 'Volume confirmation passed' : `Low volume (${(currentVolume / avgVolume).toFixed(2)}x avg)`; return { confirmed, reason }; } // Technical indicator helper methods calculateEMA(prices, period) { const k = 2 / (period + 1); const ema = [prices[0]]; for (let i = 1; i < prices.length; i++) { ema[i] = prices[i] * k + ema[i - 1] * (1 - k); } return ema; } calculateSMA(values, period) { const sma = []; for (let i = period - 1; i < values.length; i++) { const sum = values.slice(i - period + 1, i + 1).reduce((a, b) => a + b, 0); sma.push(sum / period); } return sma; } calculateMomentum(prices, period) { const momentum = []; for (let i = period; i < prices.length; i++) { const change = (prices[i] - prices[i - period]) / prices[i - period]; momentum.push(change); } return momentum; } calculateVolatility(historicalBars, period = 20) { if (historicalBars.length < 2) return 0; const returns = []; const recentBars = historicalBars.slice(-Math.min(period + 1, historicalBars.length)); for (let i = 1; i < recentBars.length; i++) { const return_ = (recentBars[i].close - recentBars[i - 1].close) / recentBars[i - 1].close; returns.push(return_); } if (returns.length === 0) return 0; const mean = returns.reduce((sum, ret) => sum + ret, 0) / returns.length; const variance = returns.reduce((sum, ret) => sum + Math.pow(ret - mean, 2), 0) / returns.length; return Math.sqrt(variance); } createSignal(action, confidence, reasons) { const reasonsArray = Array.isArray(reasons) ? reasons : [reasons]; return { action, confidence: Math.max(0, Math.min(1, confidence)), reasons: reasonsArray, strategy: 'momentum', timestamp: new Date().toISOString(), exitRules: action !== 'HOLD' ? this.getExitRules() : null }; } getExitRules() { return { stopLoss: this.parameters.exits.stopLoss, // 8% takeProfit: this.parameters.exits.profitTarget, // 15% timeStop: this.parameters.exits.timeStop // 120 hours }; } hasRequiredIndicators(indicators) { return indicators && indicators.emaFast && indicators.emaFast.length > 0 && indicators.emaSlow && indicators.emaSlow.length > 0 && indicators.emaLong && indicators.emaLong.length > 0 && indicators.mediumMomentum && indicators.mediumMomentum.length > 0; } getRequiredWarmupPeriods() { return Math.max( this.parameters.ema200, this.parameters.volume.period, this.parameters.momentum.longPeriod ) + 10; } getStrategyMetadata() { return { name: 'Momentum Swing Strategy', version: '2.0.0', description: 'Quality swing trades only - momentum thresholds 2.5% (swing) and 4.0% (strong)', parameters: this.parameters, riskLevel: 'medium', timeframes: ['1h', '4h'], markets: ['BTCUSD'], features: [ 'swing-trades-only', 'momentum-based', 'regime-detection', 'volume-confirmation', 'breakout-detection', 'volatility-filter' ], exitStrategy: '15% profit target, 8% stop loss, 120-hour time stop', tradeFrequency: 'Low frequency, high quality' }; } getStopLossPercentage() { return this.parameters.exits.stopLoss * 100; // 5% } getTakeProfitPercentage() { return this.parameters.exits.profitTarget * 100; // 7% } supportsMarketConditions(marketConditions) { const volatility = marketConditions.volatility || 0; return volatility >= this.parameters.minVolatility; } reset() { this.signalHistory = []; this.lastSignal = null; } validateConfiguration() { const issues = []; if (this.parameters.emaFast >= this.parameters.emaSlow) { issues.push('Fast EMA must be less than slow EMA'); } if (this.parameters.emaSlow >= this.parameters.emaLong) { issues.push('Slow EMA must be less than long EMA'); } if (this.parameters.exits.stopLoss >= this.parameters.exits.profitTarget) { issues.push('Stop loss (5%) should be less than profit target (7%) for positive risk/reward'); } if (this.parameters.momentum.swing >= this.parameters.momentum.strong) { issues.push('Swing momentum threshold should be less than strong momentum threshold'); } return { valid: issues.length === 0, issues }; } } module.exports = MomentumSwingStrategy;