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:
DiTus
2026-03-03 13:15:29 +01:00
parent 73f325ce19
commit d92af6903d
13 changed files with 626 additions and 104 deletions

View File

@ -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

View File

@ -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

View File

@ -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`
};
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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