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