Implement Strategy tab with Ping-Pong backtesting and crossover-based signal logic
- Add 'Strategy' tab to sidebar for backtesting simulations - Create strategy-panel.js for Ping-Pong and Accumulation mode simulations - Refactor all indicators (MA, HTS, RSI, MACD, BB, STOCH, Hurst) to use strict crossover-based signal calculation - Update chart.js with setSimulationMarkers and clearSimulationMarkers support - Implement single-entry rule in Ping-Pong simulation mode
This commit is contained in:
@ -32,35 +32,38 @@ class BaseIndicator {
|
||||
}
|
||||
|
||||
// Signal calculation for Bollinger Bands
|
||||
function calculateBollingerBandsSignal(indicator, lastCandle, prevCandle, values) {
|
||||
function calculateBollingerBandsSignal(indicator, lastCandle, prevCandle, values, prevValues) {
|
||||
const close = lastCandle.close;
|
||||
const prevClose = prevCandle?.close;
|
||||
const upper = values?.upper;
|
||||
const lower = values?.lower;
|
||||
const middle = values?.middle;
|
||||
const prevUpper = prevValues?.upper;
|
||||
const prevLower = prevValues?.lower;
|
||||
|
||||
if (!upper || !lower || !middle) {
|
||||
if (!upper || !lower || prevUpper === undefined || prevLower === undefined || prevClose === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bandwidth = (upper - lower) / middle * 100;
|
||||
|
||||
if (close <= lower) {
|
||||
// BUY: Price crosses DOWN through lower band (reversal/bounce play)
|
||||
if (prevClose > prevLower && close <= lower) {
|
||||
return {
|
||||
type: SIGNAL_TYPES.BUY,
|
||||
strength: Math.min(50 + (lower - close) / close * 1000, 100),
|
||||
strength: 70,
|
||||
value: close,
|
||||
reasoning: `Price (${close.toFixed(2)}) at or below lower band (${lower.toFixed(2)}), bandwidth: ${bandwidth.toFixed(1)}%`
|
||||
reasoning: `Price crossed DOWN through lower Bollinger Band`
|
||||
};
|
||||
} else if (close >= upper) {
|
||||
}
|
||||
// SELL: Price crosses UP through upper band (overextended play)
|
||||
else if (prevClose < prevUpper && close >= upper) {
|
||||
return {
|
||||
type: SIGNAL_TYPES.SELL,
|
||||
strength: Math.min(50 + (close - upper) / close * 1000, 100),
|
||||
strength: 70,
|
||||
value: close,
|
||||
reasoning: `Price (${close.toFixed(2)}) at or above upper band (${upper.toFixed(2)}), bandwidth: ${bandwidth.toFixed(1)}%`
|
||||
reasoning: `Price crossed UP through upper Bollinger Band`
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Bollinger Bands Indicator class
|
||||
|
||||
@ -126,35 +126,41 @@ function getMA(type, candles, period, source = 'close') {
|
||||
}
|
||||
|
||||
// Signal calculation for HTS
|
||||
function calculateHTSSignal(indicator, lastCandle, prevCandle, values) {
|
||||
const fastHigh = values?.fastHigh;
|
||||
const fastLow = values?.fastLow;
|
||||
const slowHigh = values?.slowHigh;
|
||||
function calculateHTSSignal(indicator, lastCandle, prevCandle, values, prevValues) {
|
||||
const slowLow = values?.slowLow;
|
||||
const slowHigh = values?.slowHigh;
|
||||
const prevSlowLow = prevValues?.slowLow;
|
||||
const prevSlowHigh = prevValues?.slowHigh;
|
||||
|
||||
if (!fastHigh || !fastLow || !slowHigh || !slowLow) {
|
||||
if (!slowLow || !slowHigh || !prevSlowLow || !prevSlowHigh) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const close = lastCandle.close;
|
||||
const prevClose = prevCandle?.close;
|
||||
|
||||
if (close > slowLow) {
|
||||
if (prevClose === undefined) return null;
|
||||
|
||||
// BUY: Price crosses UP through slow low
|
||||
if (prevClose <= prevSlowLow && close > slowLow) {
|
||||
return {
|
||||
type: SIGNAL_TYPES.BUY,
|
||||
strength: Math.min(60 + (close - slowLow) / slowLow * 500, 100),
|
||||
strength: 85,
|
||||
value: close,
|
||||
reasoning: `Price (${close.toFixed(2)}) is above slow low (${slowLow.toFixed(2)})`
|
||||
reasoning: `Price crossed UP through slow low`
|
||||
};
|
||||
} else if (close < slowHigh) {
|
||||
}
|
||||
// SELL: Price crosses DOWN through slow high
|
||||
else if (prevClose >= prevSlowHigh && close < slowHigh) {
|
||||
return {
|
||||
type: SIGNAL_TYPES.SELL,
|
||||
strength: Math.min(60 + (slowHigh - close) / close * 500, 100),
|
||||
strength: 85,
|
||||
value: close,
|
||||
reasoning: `Price (${close.toFixed(2)}) is below slow high (${slowHigh.toFixed(2)})`
|
||||
reasoning: `Price crossed DOWN through slow high`
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// HTS Indicator class
|
||||
|
||||
@ -63,26 +63,30 @@ function calculateHurstSignal(indicator, lastCandle, prevCandle, values, prevVal
|
||||
const prevClose = prevCandle?.close;
|
||||
const upper = values?.upper;
|
||||
const lower = values?.lower;
|
||||
const prevUpper = prevValues?.upper;
|
||||
const prevLower = prevValues?.lower;
|
||||
|
||||
if (!upper || !lower || prevClose === undefined) {
|
||||
if (close === undefined || prevClose === undefined || !upper || !lower || !prevUpper || !prevLower) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (prevClose > lower && close < lower) {
|
||||
// BUY: Price crosses DOWN through lower Hurst Band
|
||||
if (prevClose > prevLower && close <= lower) {
|
||||
return {
|
||||
type: 'buy',
|
||||
strength: 75,
|
||||
value: close,
|
||||
reasoning: `Price crossed down below lower Hurst Band (${lower.toFixed(2)}), expect bounce`
|
||||
reasoning: `Price crossed DOWN through lower Hurst Band`
|
||||
};
|
||||
}
|
||||
|
||||
if (prevClose > upper && close < upper) {
|
||||
// SELL: Price crosses DOWN through upper Hurst Band (reversal from top)
|
||||
if (prevClose > prevUpper && close <= upper) {
|
||||
return {
|
||||
type: 'sell',
|
||||
strength: 75,
|
||||
value: close,
|
||||
reasoning: `Price crossed down below upper Hurst Band (${upper.toFixed(2)}), expect reversal`
|
||||
reasoning: `Price crossed DOWN through upper Hurst Band`
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -50,32 +50,37 @@ function calculateEMAInline(data, period) {
|
||||
}
|
||||
|
||||
// Signal calculation for MACD
|
||||
function calculateMACDSignal(indicator, lastCandle, prevCandle, values) {
|
||||
function calculateMACDSignal(indicator, lastCandle, prevCandle, values, prevValues) {
|
||||
const macd = values?.macd;
|
||||
const signal = values?.signal;
|
||||
const histogram = values?.histogram;
|
||||
const prevMacd = prevValues?.macd;
|
||||
const prevSignal = prevValues?.signal;
|
||||
|
||||
if (!macd || macd === null || !signal || signal === null) {
|
||||
if (macd === undefined || macd === null || signal === undefined || signal === null ||
|
||||
prevMacd === undefined || prevMacd === null || prevSignal === undefined || prevSignal === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let signalType, strength, reasoning;
|
||||
|
||||
const prevCandleHistogram = prevCandle ? values?.histogram : null;
|
||||
|
||||
if (macd > signal) {
|
||||
signalType = SIGNAL_TYPES.BUY;
|
||||
strength = Math.min(50 + histogram * 500, 100);
|
||||
reasoning = `MACD (${macd.toFixed(2)}) is above Signal (${signal.toFixed(2)})`;
|
||||
} else if (macd < signal) {
|
||||
signalType = SIGNAL_TYPES.SELL;
|
||||
strength = Math.min(50 + Math.abs(histogram) * 500, 100);
|
||||
reasoning = `MACD (${macd.toFixed(2)}) is below Signal (${signal.toFixed(2)})`;
|
||||
} else {
|
||||
return null;
|
||||
// BUY: MACD crosses UP through Signal line
|
||||
if (prevMacd <= prevSignal && macd > signal) {
|
||||
return {
|
||||
type: SIGNAL_TYPES.BUY,
|
||||
strength: 80,
|
||||
value: macd,
|
||||
reasoning: `MACD crossed UP through Signal line`
|
||||
};
|
||||
}
|
||||
// SELL: MACD crosses DOWN through Signal line
|
||||
else if (prevMacd >= prevSignal && macd < signal) {
|
||||
return {
|
||||
type: SIGNAL_TYPES.SELL,
|
||||
strength: 80,
|
||||
value: macd,
|
||||
reasoning: `MACD crossed DOWN through Signal line`
|
||||
};
|
||||
}
|
||||
|
||||
return { type: signalType, strength, value: macd, reasoning };
|
||||
return null;
|
||||
}
|
||||
|
||||
// MACD Indicator class
|
||||
|
||||
@ -114,31 +114,35 @@ function calculateVWMA(candles, period, source = 'close') {
|
||||
}
|
||||
|
||||
// Signal calculation for Moving Average
|
||||
function calculateMASignal(indicator, lastCandle, prevCandle, values) {
|
||||
function calculateMASignal(indicator, lastCandle, prevCandle, values, prevValues) {
|
||||
const close = lastCandle.close;
|
||||
const prevClose = prevCandle?.close;
|
||||
const ma = values?.ma;
|
||||
const prevMa = prevValues?.ma;
|
||||
|
||||
if (!ma && ma !== 0) {
|
||||
return null;
|
||||
}
|
||||
if (!ma && ma !== 0) return null;
|
||||
if (prevClose === undefined || prevMa === undefined || prevMa === null) return null;
|
||||
|
||||
if (close > ma) {
|
||||
// BUY: Price crosses UP through MA
|
||||
if (prevClose <= prevMa && close > ma) {
|
||||
return {
|
||||
type: SIGNAL_TYPES.BUY,
|
||||
strength: Math.min(60 + ((close - ma) / ma) * 500, 100),
|
||||
strength: 80,
|
||||
value: close,
|
||||
reasoning: `Price (${close.toFixed(2)}) is above MA (${ma.toFixed(2)})`
|
||||
reasoning: `Price crossed UP through MA`
|
||||
};
|
||||
} else if (close < ma) {
|
||||
}
|
||||
// SELL: Price crosses DOWN through MA
|
||||
else if (prevClose >= prevMa && close < ma) {
|
||||
return {
|
||||
type: SIGNAL_TYPES.SELL,
|
||||
strength: Math.min(60 + ((ma - close) / ma) * 500, 100),
|
||||
strength: 80,
|
||||
value: close,
|
||||
reasoning: `Price (${close.toFixed(2)}) is below MA (${ma.toFixed(2)})`
|
||||
reasoning: `Price crossed DOWN through MA`
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// MA Indicator class
|
||||
|
||||
@ -38,42 +38,30 @@ function calculateRSISignal(indicator, lastCandle, prevCandle, values, prevValue
|
||||
const overbought = indicator.params?.overbought || 70;
|
||||
const oversold = indicator.params?.oversold || 30;
|
||||
|
||||
if (!rsi || rsi === null) {
|
||||
if (rsi === undefined || rsi === null || prevRsi === undefined || prevRsi === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let signalType, strength, reasoning;
|
||||
|
||||
// BUY when RSI crosses UP through oversold band (bottom band)
|
||||
// RSI was below oversold, now above oversold
|
||||
if (prevRsi !== undefined && prevRsi !== null && prevRsi < oversold && rsi >= oversold) {
|
||||
signalType = SIGNAL_TYPES.BUY;
|
||||
strength = Math.min(50 + (rsi - oversold) * 2, 100);
|
||||
reasoning = `RSI (${rsi.toFixed(2)}) crossed up through oversold level (${oversold})`;
|
||||
// BUY when RSI crosses UP through oversold level
|
||||
if (prevRsi < oversold && rsi >= oversold) {
|
||||
return {
|
||||
type: SIGNAL_TYPES.BUY,
|
||||
strength: 75,
|
||||
value: rsi,
|
||||
reasoning: `RSI crossed UP through oversold level (${oversold})`
|
||||
};
|
||||
}
|
||||
// SELL when RSI crosses DOWN through overbought band (top band)
|
||||
// RSI was above overbought, now below overbought
|
||||
else if (prevRsi !== undefined && prevRsi !== null && prevRsi > overbought && rsi <= overbought) {
|
||||
signalType = SIGNAL_TYPES.SELL;
|
||||
strength = Math.min(50 + (overbought - rsi) * 2, 100);
|
||||
reasoning = `RSI (${rsi.toFixed(2)}) crossed down through overbought level (${overbought})`;
|
||||
}
|
||||
// When RSI is in oversold territory but no crossover - strong BUY
|
||||
else if (rsi < oversold) {
|
||||
signalType = SIGNAL_TYPES.BUY;
|
||||
strength = Math.min(40 + (oversold - rsi) * 1.5, 80);
|
||||
reasoning = `RSI (${rsi.toFixed(2)}) is oversold (<${oversold})`;
|
||||
}
|
||||
// When RSI is in overbought territory but no crossover - strong SELL
|
||||
else if (rsi > overbought) {
|
||||
signalType = SIGNAL_TYPES.SELL;
|
||||
strength = Math.min(40 + (rsi - overbought) * 1.5, 80);
|
||||
reasoning = `RSI (${rsi.toFixed(2)}) is overbought (>${overbought})`;
|
||||
} else {
|
||||
return null;
|
||||
// SELL when RSI crosses DOWN through overbought level
|
||||
else if (prevRsi > overbought && rsi <= overbought) {
|
||||
return {
|
||||
type: SIGNAL_TYPES.SELL,
|
||||
strength: 75,
|
||||
value: rsi,
|
||||
reasoning: `RSI crossed DOWN through overbought level (${overbought})`
|
||||
};
|
||||
}
|
||||
|
||||
return { type: signalType, strength, value: rsi, reasoning };
|
||||
return null;
|
||||
}
|
||||
|
||||
// RSI Indicator class
|
||||
|
||||
@ -32,33 +32,38 @@ class BaseIndicator {
|
||||
}
|
||||
|
||||
// Signal calculation for Stochastic
|
||||
function calculateStochSignal(indicator, lastCandle, prevCandle, values) {
|
||||
function calculateStochSignal(indicator, lastCandle, prevCandle, values, prevValues) {
|
||||
const k = values?.k;
|
||||
const d = values?.d;
|
||||
const prevK = prevValues?.k;
|
||||
const prevD = prevValues?.d;
|
||||
const overbought = indicator.params?.overbought || 80;
|
||||
const oversold = indicator.params?.oversold || 20;
|
||||
|
||||
if (!k || !d) {
|
||||
if (k === undefined || d === undefined || prevK === undefined || prevD === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (k < oversold && d < oversold) {
|
||||
// BUY: %K crosses UP through %D while both are oversold
|
||||
if (prevK <= prevD && k > d && k < oversold) {
|
||||
return {
|
||||
type: SIGNAL_TYPES.BUY,
|
||||
strength: Math.min(50 + (oversold - k) * 2, 100),
|
||||
strength: 80,
|
||||
value: k,
|
||||
reasoning: `Stochastic %K (${k.toFixed(2)}) and %D (${d.toFixed(2)}) oversold (<${oversold})`
|
||||
reasoning: `Stochastic %K crossed UP through %D in oversold zone`
|
||||
};
|
||||
} else if (k > overbought && d > overbought) {
|
||||
}
|
||||
// SELL: %K crosses DOWN through %D while both are overbought
|
||||
else if (prevK >= prevD && k < d && k > overbought) {
|
||||
return {
|
||||
type: SIGNAL_TYPES.SELL,
|
||||
strength: Math.min(50 + (k - overbought) * 2, 100),
|
||||
strength: 80,
|
||||
value: k,
|
||||
reasoning: `Stochastic %K (${k.toFixed(2)}) and %D (${d.toFixed(2)}) overbought (>${overbought})`
|
||||
reasoning: `Stochastic %K crossed DOWN through %D in overbought zone`
|
||||
};
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Stochastic Oscillator Indicator class
|
||||
|
||||
Reference in New Issue
Block a user