Add Bollinger Bands Volatility Protection indicator support

- Introduced BollingerBandsVolatilityProtection indicator in GeneticService with configuration settings for period and standard deviation (stdev).
- Updated ScenarioHelpers to handle creation and validation of the new indicator type.
- Enhanced CustomScenario, backtest, and scenario pages to include BollingerBandsVolatilityProtection in indicator lists and parameter mappings.
- Modified API and types to reflect the addition of the new indicator in relevant enums and mappings.
- Updated frontend components to support new parameters and visualization for Bollinger Bands.
This commit is contained in:
2025-11-25 02:12:57 +07:00
parent 3ec1da531a
commit 6376e13b07
21 changed files with 618 additions and 220 deletions

View File

@@ -48,7 +48,8 @@ const CustomScenario: React.FC<ICustomScenario> = ({
case IndicatorType.SuperTrendCrossEma:
case IndicatorType.ChandelierExit:
case IndicatorType.BollingerBandsPercentBMomentumBreakout:
params = ['period', 'multiplier'];
case IndicatorType.BollingerBandsVolatilityProtection:
params = ['period', 'stdev'];
break;
case IndicatorType.StochRsiTrend:
@@ -147,6 +148,7 @@ const CustomScenario: React.FC<ICustomScenario> = ({
slowPeriods: 26,
signalPeriods: 9,
multiplier: 3.0,
stDev: 2.0,
stochPeriods: 14,
smoothPeriods: 3,
cyclePeriods: 10,
@@ -284,10 +286,10 @@ const CustomScenario: React.FC<ICustomScenario> = ({
<FormInput key={param} label={param.charAt(0).toUpperCase() + param.slice(1)} htmlFor={`${param}-${index}`} inline={false}>
<input
value={indicator[param as keyof LightIndicator] as number || ''}
onChange={(e) => updateIndicator(index, param, param.includes('multiplier') ? parseFloat(e.target.value) : parseInt(e.target.value))}
onChange={(e) => updateIndicator(index, param, param.includes('multiplier') || param.includes('stdev') ? parseFloat(e.target.value) : parseInt(e.target.value))}
type='number'
step={param.includes('multiplier') ? '0.1' : '1'}
min={param.includes('multiplier') ? '0.1' : '1'}
step={param.includes('multiplier') || param.includes('stdev') ? '0.1' : '1'}
min={param.includes('multiplier') || param.includes('stdev') ? '0.1' : '1'}
className='input input-bordered w-full'
/>
</FormInput>

View File

@@ -39,7 +39,6 @@ import useTheme from '../../../../hooks/useTheme'
// }
type ITradeChartProps = {
candles: Candle[]
positions: Position[]
@@ -69,23 +68,23 @@ const TradeChart = ({
const series1 = useRef<ISeriesApi<'Candlestick'>>()
const [timeDiff, setTimeDiff] = useState<number>(0)
const [candleCount, setCandleCount] = useState<number>(candles.length)
const [chartDimensions, setChartDimensions] = useState({ width: 0, height: 0 })
const [chartDimensions, setChartDimensions] = useState({width: 0, height: 0})
// Get responsive dimensions
const getResponsiveDimensions = () => {
if (!containerRef.current) return { width: width || 510, height: height || 300 }
if (!containerRef.current) return {width: width || 510, height: height || 300}
const containerWidth = containerRef.current.offsetWidth
const containerHeight = containerRef.current.offsetHeight
// Use provided dimensions if available, otherwise calculate responsive ones
if (width && height) {
return { width, height }
return {width, height}
}
// For responsive mode, calculate based on container
const calculatedWidth = containerWidth > 0 ? containerWidth : 510
// Use different aspect ratios for different screen sizes
let aspectRatio = 0.6 // Default ratio (height/width)
if (containerWidth < 768) { // Mobile
@@ -93,9 +92,9 @@ const TradeChart = ({
} else if (containerWidth < 1024) { // Tablet
aspectRatio = 0.65
}
const calculatedHeight = height || Math.max(250, calculatedWidth * aspectRatio)
return {
width: calculatedWidth,
height: calculatedHeight
@@ -109,7 +108,7 @@ const TradeChart = ({
const resizeObserver = new ResizeObserver(() => {
const newDimensions = getResponsiveDimensions()
setChartDimensions(newDimensions)
if (chart.current) {
chart.current.applyOptions({
width: newDimensions.width,
@@ -131,7 +130,7 @@ const TradeChart = ({
setTimeout(() => {
const newDimensions = getResponsiveDimensions()
setChartDimensions(newDimensions)
if (chart.current) {
chart.current.applyOptions({
width: newDimensions.width,
@@ -231,7 +230,7 @@ const TradeChart = ({
} else {
color = negativeColor
}
}else if (status == PositionStatus.Filled) {
} else if (status == PositionStatus.Filled) {
color = theme.warning
}
}
@@ -243,7 +242,7 @@ const TradeChart = ({
if (chartRef.current && containerRef.current) {
const initialDimensions = getResponsiveDimensions()
setChartDimensions(initialDimensions)
const lineColor = theme['base-100']
chart.current = createChart(chartRef.current, {
crosshair: {
@@ -349,13 +348,13 @@ const TradeChart = ({
// Get the time range of candles
const firstCandleTime = moment(candles[0].date).unix()
const lastCandleTime = moment(candles[candles.length - 1].date).unix()
// Filter signals that are within the candle range
const filteredSignals = signals.filter((s) => {
const signalTime = moment(s.date).unix()
return signalTime >= firstCandleTime && signalTime <= lastCandleTime
})
const signalMarkers = filteredSignals.map((s) =>
buildMarker(
'circle',
@@ -393,9 +392,8 @@ const TradeChart = ({
}
// Price panel
if (indicatorsValues?.EmaTrend != null || indicatorsValues?.EmaCross != null)
{
const emaSeries = chart.current.addLineSeries({
if (indicatorsValues?.EmaTrend != null || indicatorsValues?.EmaCross != null) {
const emaSeries = chart.current.addLineSeries({
color: theme.secondary,
lineWidth: 1,
priceLineVisible: true,
@@ -417,8 +415,7 @@ const TradeChart = ({
}
})
if (emaData != null)
{
if (emaData != null) {
// @ts-ignore
emaSeries.setData(emaData)
}
@@ -446,8 +443,8 @@ const TradeChart = ({
superTrendSeries.setData(superTrend)
}
// Display chandeliers exits
if (indicatorsValues?.ChandelierExit != null) {
// Display chandeliers exits
if (indicatorsValues?.ChandelierExit != null) {
const chandelierExitsLongsSeries = chart.current.addLineSeries({
color: theme.info,
lineWidth: 1,
@@ -568,6 +565,47 @@ const TradeChart = ({
lowerBandSeries.setData(lowerBandData)
}
// Display Bollinger Bands on price chart for Volatility Protection
if (indicatorsValues?.BollingerBandsVolatilityProtection != null) {
const upperBandSeries = chart.current.addLineSeries({
color: '#FF6B6B', // Lighter red for volatility protection bands
lineWidth: 1,
priceLineVisible: false,
priceLineWidth: 1,
title: 'Volatility Protection Upper Band',
pane: 0,
lineStyle: LineStyle.Dotted,
})
const upperBandData = indicatorsValues.BollingerBandsVolatilityProtection.bollingerBands?.map((w) => {
return {
time: moment(w.date).unix(),
value: w.upperBand,
}
})
// @ts-ignore
upperBandSeries.setData(upperBandData)
const lowerBandSeries = chart.current.addLineSeries({
color: '#4ECDC4', // Teal for volatility protection bands
lineWidth: 1,
priceLineVisible: false,
priceLineWidth: 1,
title: 'Volatility Protection Lower Band',
pane: 0,
lineStyle: LineStyle.Dotted,
})
const lowerBandData = indicatorsValues.BollingerBandsVolatilityProtection.bollingerBands?.map((w) => {
return {
time: moment(w.date).unix(),
value: w.lowerBand,
}
})
// @ts-ignore
lowerBandSeries.setData(lowerBandData)
}
if (markers.length > 0) {
series1.current.setMarkers(markers)
}
@@ -575,9 +613,8 @@ const TradeChart = ({
// Indicator panel
var paneCount = 1
if (indicatorsValues?.RsiDivergence != null || indicatorsValues?.RsiDivergenceConfirm != null)
{
if (indicatorsValues?.RsiDivergence != null || indicatorsValues?.RsiDivergenceConfirm != null) {
const rsiSeries = chart.current.addLineSeries({
pane: paneCount,
title: 'RSI',
@@ -608,7 +645,7 @@ const TradeChart = ({
const stcSeries = chart.current.addBaselineSeries({
pane: paneCount,
baseValue: {price: 50, type: 'price'},
title: 'STC',
})
@@ -641,7 +678,7 @@ const TradeChart = ({
const laggingStcSeries = chart.current.addBaselineSeries({
pane: paneCount,
baseValue: {price: 50, type: 'price'},
title: 'Lagging STC',
})
@@ -690,7 +727,7 @@ const TradeChart = ({
})
var priceOptions = {
scaleMargins:{
scaleMargins: {
top: 0.7,
bottom: 0.02,
}
@@ -722,7 +759,7 @@ const TradeChart = ({
macdSeries.priceScale().applyOptions(priceOptions)
// @ts-ignore
macdSeries.setData(macdData)
macdSeries.setData(macdData)
const signalSeries = chart.current.addLineSeries({
color: theme.info,
@@ -747,11 +784,11 @@ const TradeChart = ({
signalSeries.priceScale().applyOptions(priceOptions)
// @ts-ignore
signalSeries.setData(signalData)
paneCount++
}
if (indicatorsValues?.StochRsiTrend){
if (indicatorsValues?.StochRsiTrend) {
const stochRsiSeries = chart.current.addLineSeries({
...baselineOptions,
priceLineVisible: false,
@@ -859,7 +896,7 @@ const TradeChart = ({
paneCount++
}
// Display dual EMA crossover
if (indicatorsValues?.DualEmaCross != null) {
const fastEmaSeries = chart.current.addLineSeries({
@@ -960,12 +997,12 @@ const TradeChart = ({
}
return (
<div
ref={containerRef}
<div
ref={containerRef}
className="w-full h-full"
style={{ minHeight: height || 250 }}
style={{minHeight: height || 250}}
>
<div ref={chartRef} className="w-full h-full" />
<div ref={chartRef} className="w-full h-full"/>
</div>
)
}

View File

@@ -572,9 +572,18 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
slowPeriods: indicator.slowPeriods,
signalPeriods: indicator.signalPeriods,
multiplier: indicator.multiplier,
stDev: indicator.stDev,
smoothPeriods: indicator.smoothPeriods,
stochPeriods: indicator.stochPeriods,
cyclePeriods: indicator.cyclePeriods,
kFactor: indicator.kFactor,
dFactor: indicator.dFactor,
tenkanPeriods: indicator.tenkanPeriods,
kijunPeriods: indicator.kijunPeriods,
senkouBPeriods: indicator.senkouBPeriods,
offsetPeriods: indicator.offsetPeriods,
senkouOffset: indicator.senkouOffset,
chikouOffset: indicator.chikouOffset,
})).filter(indicator => indicator.type) || [] // Only filter out indicators without type
};
};

View File

@@ -4960,6 +4960,7 @@ export interface LightIndicator {
slowPeriods?: number | null;
signalPeriods?: number | null;
multiplier?: number | null;
stDev?: number | null;
smoothPeriods?: number | null;
stochPeriods?: number | null;
cyclePeriods?: number | null;
@@ -4991,6 +4992,7 @@ export enum IndicatorType {
SuperTrendCrossEma = "SuperTrendCrossEma",
DualEmaCross = "DualEmaCross",
BollingerBandsPercentBMomentumBreakout = "BollingerBandsPercentBMomentumBreakout",
BollingerBandsVolatilityProtection = "BollingerBandsVolatilityProtection",
IchimokuKumoTrend = "IchimokuKumoTrend",
}
@@ -5263,9 +5265,18 @@ export interface IndicatorRequest {
slowPeriods?: number | null;
signalPeriods?: number | null;
multiplier?: number | null;
stDev?: number | null;
smoothPeriods?: number | null;
stochPeriods?: number | null;
cyclePeriods?: number | null;
kFactor?: number | null;
dFactor?: number | null;
tenkanPeriods?: number | null;
kijunPeriods?: number | null;
senkouBPeriods?: number | null;
offsetPeriods?: number | null;
senkouOffset?: number | null;
chikouOffset?: number | null;
}
export interface MoneyManagementRequest {
@@ -5577,6 +5588,7 @@ export interface IndicatorBase {
slowPeriods?: number | null;
signalPeriods?: number | null;
multiplier?: number | null;
stDev?: number | null;
smoothPeriods?: number | null;
stochPeriods?: number | null;
cyclePeriods?: number | null;

View File

@@ -426,6 +426,7 @@ export interface LightIndicator {
slowPeriods?: number | null;
signalPeriods?: number | null;
multiplier?: number | null;
stDev?: number | null;
smoothPeriods?: number | null;
stochPeriods?: number | null;
cyclePeriods?: number | null;
@@ -457,6 +458,7 @@ export enum IndicatorType {
SuperTrendCrossEma = "SuperTrendCrossEma",
DualEmaCross = "DualEmaCross",
BollingerBandsPercentBMomentumBreakout = "BollingerBandsPercentBMomentumBreakout",
BollingerBandsVolatilityProtection = "BollingerBandsVolatilityProtection",
IchimokuKumoTrend = "IchimokuKumoTrend",
}
@@ -729,9 +731,18 @@ export interface IndicatorRequest {
slowPeriods?: number | null;
signalPeriods?: number | null;
multiplier?: number | null;
stDev?: number | null;
smoothPeriods?: number | null;
stochPeriods?: number | null;
cyclePeriods?: number | null;
kFactor?: number | null;
dFactor?: number | null;
tenkanPeriods?: number | null;
kijunPeriods?: number | null;
senkouBPeriods?: number | null;
offsetPeriods?: number | null;
senkouOffset?: number | null;
chikouOffset?: number | null;
}
export interface MoneyManagementRequest {
@@ -1043,6 +1054,7 @@ export interface IndicatorBase {
slowPeriods?: number | null;
signalPeriods?: number | null;
multiplier?: number | null;
stDev?: number | null;
smoothPeriods?: number | null;
stochPeriods?: number | null;
cyclePeriods?: number | null;

View File

@@ -231,9 +231,18 @@ const BundleRequestModal: React.FC<BundleRequestModalProps> = ({
slowPeriods: indicator.slowPeriods || 26,
signalPeriods: indicator.signalPeriods || 9,
multiplier: indicator.multiplier || 3.0,
stDev: indicator.stDev || 2.0,
stochPeriods: indicator.stochPeriods || 14,
smoothPeriods: indicator.smoothPeriods || 3,
cyclePeriods: indicator.cyclePeriods || 10
cyclePeriods: indicator.cyclePeriods || 10,
kFactor: indicator.kFactor || 3.0,
dFactor: indicator.dFactor || 3.0,
tenkanPeriods: indicator.tenkanPeriods || 9,
kijunPeriods: indicator.kijunPeriods || 26,
senkouBPeriods: indicator.senkouBPeriods || 52,
offsetPeriods: indicator.offsetPeriods || 26,
senkouOffset: indicator.senkouOffset || 26,
chikouOffset: indicator.chikouOffset || 26
})),
loopbackPeriod: scenario.loopbackPeriod || 1
} : undefined,