From 478dca51e7f18ea0e225b92fa763ffc4b404b629 Mon Sep 17 00:00:00 2001 From: cryptooda Date: Mon, 24 Nov 2025 17:31:17 +0700 Subject: [PATCH] Add BollingerBandsPercentBMomentumBreakout indicator support across application - Introduced BollingerBandsPercentBMomentumBreakout indicator in GeneticService with configuration settings for period and multiplier. - Updated ScenarioHelpers to handle creation and validation of the new indicator type. - Enhanced CustomScenario, backtest, and scenario pages to include BollingerBandsPercentBMomentumBreakout in indicator lists and parameter mappings. - Modified API and types to reflect the addition of the new indicator in relevant enums and mappings. --- src/Managing.Application/GeneticService.cs | 13 +- src/Managing.Common/Enums.cs | 3 +- .../BollingerBandsPercentBMomentumBreakout.cs | 189 ++++++++++++++++++ .../Scenarios/ScenarioHelpers.cs | 3 + .../CustomScenario/CustomScenario.tsx | 1 + .../Trading/TradeChart/TradeChart.tsx | 72 +++++++ .../src/generated/ManagingApi.ts | 1 + .../src/generated/ManagingApiTypes.ts | 1 + .../pages/backtestPage/backtestGenetic.tsx | 2 + .../backtestPage/backtestGeneticBundle.tsx | 1 + .../src/pages/scenarioPage/indicatorList.tsx | 3 +- 11 files changed, 286 insertions(+), 3 deletions(-) create mode 100644 src/Managing.Domain/Indicators/Signals/BollingerBandsPercentBMomentumBreakout.cs diff --git a/src/Managing.Application/GeneticService.cs b/src/Managing.Application/GeneticService.cs index d2ad31e3..8595b335 100644 --- a/src/Managing.Application/GeneticService.cs +++ b/src/Managing.Application/GeneticService.cs @@ -103,6 +103,11 @@ public class GeneticService : IGeneticService ["cyclePeriods"] = 10.0, ["fastPeriods"] = 12.0, ["slowPeriods"] = 26.0 + }, + [IndicatorType.BollingerBandsPercentBMomentumBreakout] = new() + { + ["period"] = 20.0, + ["multiplier"] = 2.0 } }; @@ -186,6 +191,11 @@ public class GeneticService : IGeneticService ["cyclePeriods"] = (5.0, 30.0), ["fastPeriods"] = (5.0, 50.0), ["slowPeriods"] = (10.0, 100.0) + }, + [IndicatorType.BollingerBandsPercentBMomentumBreakout] = new() + { + ["period"] = (5.0, 50.0), + ["multiplier"] = (1.0, 5.0) } }; @@ -206,7 +216,8 @@ public class GeneticService : IGeneticService [IndicatorType.StochRsiTrend] = ["period", "stochPeriods", "signalPeriods", "smoothPeriods"], [IndicatorType.StochasticCross] = ["stochPeriods", "signalPeriods", "smoothPeriods", "kFactor", "dFactor"], [IndicatorType.Stc] = ["cyclePeriods", "fastPeriods", "slowPeriods"], - [IndicatorType.LaggingStc] = ["cyclePeriods", "fastPeriods", "slowPeriods"] + [IndicatorType.LaggingStc] = ["cyclePeriods", "fastPeriods", "slowPeriods"], + [IndicatorType.BollingerBandsPercentBMomentumBreakout] = ["period", "multiplier"] }; public GeneticService( diff --git a/src/Managing.Common/Enums.cs b/src/Managing.Common/Enums.cs index 8ff9299f..69eb4bf7 100644 --- a/src/Managing.Common/Enums.cs +++ b/src/Managing.Common/Enums.cs @@ -65,7 +65,8 @@ public static class Enums StDev, LaggingStc, SuperTrendCrossEma, - DualEmaCross + DualEmaCross, + BollingerBandsPercentBMomentumBreakout } public enum SignalType diff --git a/src/Managing.Domain/Indicators/Signals/BollingerBandsPercentBMomentumBreakout.cs b/src/Managing.Domain/Indicators/Signals/BollingerBandsPercentBMomentumBreakout.cs new file mode 100644 index 00000000..8344e89b --- /dev/null +++ b/src/Managing.Domain/Indicators/Signals/BollingerBandsPercentBMomentumBreakout.cs @@ -0,0 +1,189 @@ +using Managing.Core; +using Managing.Domain.Candles; +using Managing.Domain.Indicators; +using Managing.Domain.Shared.Rules; +using Managing.Domain.Strategies.Base; +using Skender.Stock.Indicators; +using static Managing.Common.Enums; + +namespace Managing.Domain.Strategies.Signals; + +public class BollingerBandsPercentBMomentumBreakout : IndicatorBase +{ + public List Signals { get; set; } + + public BollingerBandsPercentBMomentumBreakout( + string name, + int period, + double stdDev) : base(name, IndicatorType.BollingerBandsPercentBMomentumBreakout) + { + Signals = new List(); + Period = period; + Multiplier = stdDev; // Using Multiplier property for stdDev since it's a double + } + + public override List Run(HashSet candles) + { + if (candles.Count <= 10 * Period.Value + 50) + { + return null; + } + + try + { + var bbResults = candles + .GetBollingerBands(Period.Value, (double)Multiplier.Value) + .RemoveWarmupPeriods() + .ToList(); + + if (bbResults.Count == 0) + return null; + + ProcessBollingerBandsSignals(bbResults, candles); + + return Signals.ToList(); + } + catch (RuleException) + { + return null; + } + } + + public override List Run(HashSet candles, IndicatorsResultBase preCalculatedValues) + { + if (candles.Count <= 10 * Period.Value + 50) + { + return null; + } + + try + { + // Use pre-calculated Bollinger Bands values if available + List bbResults = null; + if (preCalculatedValues?.BollingerBands != null && preCalculatedValues.BollingerBands.Any()) + { + // Filter pre-calculated values to match the candles we're processing + bbResults = preCalculatedValues.BollingerBands + .Where(bb => bb.UpperBand.HasValue && bb.LowerBand.HasValue && bb.Sma.HasValue && + candles.Any(c => c.Date == bb.Date)) + .ToList(); + } + + // If no pre-calculated values or they don't match, fall back to regular calculation + if (bbResults == null || !bbResults.Any()) + { + return Run(candles); + } + + ProcessBollingerBandsSignals(bbResults, candles); + + return Signals.ToList(); + } + catch (RuleException) + { + return null; + } + } + + /// + /// Processes Bollinger Bands %B signals based on momentum breakouts. + /// Long signals: %B crosses above 0.8 after being below (strong upward momentum) + /// Short signals: %B crosses below 0.2 after being above (strong downward momentum) + /// + private void ProcessBollingerBandsSignals(List bbResults, HashSet candles) + { + var bbCandles = MapBollingerBandsToCandle(bbResults, candles.TakeLast(Period.Value)); + + if (bbCandles.Count < 2) + return; + + var previousCandle = bbCandles[0]; + foreach (var currentCandle in bbCandles.Skip(1)) + { + // Long signal: %B crosses above 0.8 after being below + if (previousCandle.PercentB < 0.8 && currentCandle.PercentB >= 0.8) + { + AddSignal(currentCandle, TradeDirection.Long, Confidence.Medium); + } + + // Short signal: %B crosses below 0.2 after being above + if (previousCandle.PercentB > 0.2 && currentCandle.PercentB <= 0.2) + { + AddSignal(currentCandle, TradeDirection.Short, Confidence.Medium); + } + + previousCandle = currentCandle; + } + } + + public override IndicatorsResultBase GetIndicatorValues(HashSet candles) + { + return new IndicatorsResultBase() + { + BollingerBands = candles.GetBollingerBands(Period.Value, (double)Multiplier.Value) + .ToList() + }; + } + + private List MapBollingerBandsToCandle(IEnumerable bbResults, IEnumerable candles) + { + var bbCandles = new List(); + foreach (var candle in candles) + { + var currentBB = bbResults.Find(candle.Date); + if (currentBB != null && currentBB.UpperBand.HasValue && currentBB.LowerBand.HasValue && currentBB.Sma.HasValue) + { + // Calculate %B = (Price - LowerBand) / (UpperBand - LowerBand) + var price = (double)candle.Close; + var upperBand = (double)currentBB.UpperBand.Value; + var lowerBand = (double)currentBB.LowerBand.Value; + var percentB = (double)currentBB.PercentB.Value; + + // Avoid division by zero + if (upperBand != lowerBand) + { + bbCandles.Add(new CandleBollingerBands() + { + Close = candle.Close, + Open = candle.Open, + Date = candle.Date, + Ticker = candle.Ticker, + Exchange = candle.Exchange, + PercentB = percentB, + UpperBand = upperBand, + LowerBand = lowerBand, + Sma = currentBB.Sma.Value + }); + } + } + } + + return bbCandles; + } + + private void AddSignal(CandleBollingerBands candleSignal, TradeDirection direction, Confidence confidence) + { + var signal = new LightSignal( + candleSignal.Ticker, + direction, + confidence, + candleSignal, + candleSignal.Date, + candleSignal.Exchange, + Type, + SignalType, + Name); + if (!Signals.Any(s => s.Identifier == signal.Identifier)) + { + Signals.AddItem(signal); + } + } + + private class CandleBollingerBands : Candle + { + public double PercentB { get; internal set; } + public double UpperBand { get; internal set; } + public double LowerBand { get; internal set; } + public double Sma { get; internal set; } + } +} diff --git a/src/Managing.Domain/Scenarios/ScenarioHelpers.cs b/src/Managing.Domain/Scenarios/ScenarioHelpers.cs index 2b9b2acc..7257efc9 100644 --- a/src/Managing.Domain/Scenarios/ScenarioHelpers.cs +++ b/src/Managing.Domain/Scenarios/ScenarioHelpers.cs @@ -100,6 +100,8 @@ public static class ScenarioHelpers indicator.FastPeriods.Value, indicator.SlowPeriods.Value), IndicatorType.SuperTrendCrossEma => new SuperTrendCrossEma(indicator.Name, indicator.Period.Value, indicator.Multiplier.Value), + IndicatorType.BollingerBandsPercentBMomentumBreakout => new BollingerBandsPercentBMomentumBreakout(indicator.Name, + indicator.Period.Value, indicator.Multiplier.Value), _ => throw new NotImplementedException(), }; @@ -289,6 +291,7 @@ public static class ScenarioHelpers IndicatorType.StDev => SignalType.Context, IndicatorType.LaggingStc => SignalType.Signal, IndicatorType.SuperTrendCrossEma => SignalType.Signal, + IndicatorType.BollingerBandsPercentBMomentumBreakout => SignalType.Signal, _ => throw new NotImplementedException(), }; } diff --git a/src/Managing.WebApp/src/components/organism/CustomScenario/CustomScenario.tsx b/src/Managing.WebApp/src/components/organism/CustomScenario/CustomScenario.tsx index ed868491..940a494f 100644 --- a/src/Managing.WebApp/src/components/organism/CustomScenario/CustomScenario.tsx +++ b/src/Managing.WebApp/src/components/organism/CustomScenario/CustomScenario.tsx @@ -47,6 +47,7 @@ const CustomScenario: React.FC = ({ case IndicatorType.SuperTrend: case IndicatorType.SuperTrendCrossEma: case IndicatorType.ChandelierExit: + case IndicatorType.BollingerBandsPercentBMomentumBreakout: params = ['period', 'multiplier']; break; diff --git a/src/Managing.WebApp/src/components/organism/Trading/TradeChart/TradeChart.tsx b/src/Managing.WebApp/src/components/organism/Trading/TradeChart/TradeChart.tsx index bb4ef610..782fed73 100644 --- a/src/Managing.WebApp/src/components/organism/Trading/TradeChart/TradeChart.tsx +++ b/src/Managing.WebApp/src/components/organism/Trading/TradeChart/TradeChart.tsx @@ -485,6 +485,47 @@ const TradeChart = ({ chandelierExitsShortsSeries.setData(chandelierExitsShorts) } + // Display Bollinger Bands on price chart + if (indicatorsValues?.BollingerBandsPercentBMomentumBreakout != null) { + const upperBandSeries = chart.current.addLineSeries({ + color: theme.error, + lineWidth: 1, + priceLineVisible: false, + priceLineWidth: 1, + title: 'Upper Band', + pane: 0, + lineStyle: LineStyle.Dashed, + }) + + const upperBandData = indicatorsValues.BollingerBandsPercentBMomentumBreakout.bollingerBands?.map((w) => { + return { + time: moment(w.date).unix(), + value: w.upperBand, + } + }) + // @ts-ignore + upperBandSeries.setData(upperBandData) + + const lowerBandSeries = chart.current.addLineSeries({ + color: theme.success, + lineWidth: 1, + priceLineVisible: false, + priceLineWidth: 1, + title: 'Lower Band', + pane: 0, + lineStyle: LineStyle.Dashed, + }) + + const lowerBandData = indicatorsValues.BollingerBandsPercentBMomentumBreakout.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) } @@ -843,6 +884,37 @@ const TradeChart = ({ }) paneCount++ } + + // Display Bollinger Bands %B + if (indicatorsValues?.BollingerBandsPercentBMomentumBreakout != null) { + const percentBSeries = chart.current.addLineSeries({ + color: theme.primary, + lineWidth: 1, + priceLineVisible: false, + priceLineWidth: 1, + title: '%B', + pane: paneCount, + priceFormat: { + precision: 2, + type: 'price', + }, + }) + + const percentBData = indicatorsValues.BollingerBandsPercentBMomentumBreakout.bollingerBands?.map((w) => { + return { + time: moment(w.date).unix(), + value: w.percentB, + } + }) + // @ts-ignore + percentBSeries.setData(percentBData) + + // Add reference lines for momentum thresholds + percentBSeries.createPriceLine(buildLine(theme.error, 0.8, 'Upper Threshold')) + percentBSeries.createPriceLine(buildLine(theme.success, 0.2, 'Lower Threshold')) + + paneCount++ + } } return ( diff --git a/src/Managing.WebApp/src/generated/ManagingApi.ts b/src/Managing.WebApp/src/generated/ManagingApi.ts index e43de3ce..abd48d1d 100644 --- a/src/Managing.WebApp/src/generated/ManagingApi.ts +++ b/src/Managing.WebApp/src/generated/ManagingApi.ts @@ -4984,6 +4984,7 @@ export enum IndicatorType { LaggingStc = "LaggingStc", SuperTrendCrossEma = "SuperTrendCrossEma", DualEmaCross = "DualEmaCross", + BollingerBandsPercentBMomentumBreakout = "BollingerBandsPercentBMomentumBreakout", } export enum SignalType { diff --git a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts index c8c846fd..e6926e2e 100644 --- a/src/Managing.WebApp/src/generated/ManagingApiTypes.ts +++ b/src/Managing.WebApp/src/generated/ManagingApiTypes.ts @@ -450,6 +450,7 @@ export enum IndicatorType { LaggingStc = "LaggingStc", SuperTrendCrossEma = "SuperTrendCrossEma", DualEmaCross = "DualEmaCross", + BollingerBandsPercentBMomentumBreakout = "BollingerBandsPercentBMomentumBreakout", } export enum SignalType { diff --git a/src/Managing.WebApp/src/pages/backtestPage/backtestGenetic.tsx b/src/Managing.WebApp/src/pages/backtestPage/backtestGenetic.tsx index 84697734..4a8dd326 100644 --- a/src/Managing.WebApp/src/pages/backtestPage/backtestGenetic.tsx +++ b/src/Managing.WebApp/src/pages/backtestPage/backtestGenetic.tsx @@ -104,6 +104,7 @@ const ALL_INDICATORS = [ IndicatorType.SuperTrendCrossEma, IndicatorType.DualEmaCross, IndicatorType.StochasticCross, + IndicatorType.BollingerBandsPercentBMomentumBreakout, ] // Indicator type to parameter mapping @@ -119,6 +120,7 @@ const INDICATOR_PARAM_MAPPING = { [IndicatorType.SuperTrend]: ['period', 'multiplier'], [IndicatorType.SuperTrendCrossEma]: ['period', 'multiplier'], [IndicatorType.ChandelierExit]: ['period', 'multiplier'], + [IndicatorType.BollingerBandsPercentBMomentumBreakout]: ['period', 'multiplier'], [IndicatorType.StochRsiTrend]: ['period', 'stochPeriods', 'signalPeriods', 'smoothPeriods'], [IndicatorType.StochasticCross]: ['stochPeriods', 'signalPeriods', 'smoothPeriods', 'kFactor', 'dFactor'], [IndicatorType.Stc]: ['cyclePeriods', 'fastPeriods', 'slowPeriods'], diff --git a/src/Managing.WebApp/src/pages/backtestPage/backtestGeneticBundle.tsx b/src/Managing.WebApp/src/pages/backtestPage/backtestGeneticBundle.tsx index 39f7c181..b3876684 100644 --- a/src/Managing.WebApp/src/pages/backtestPage/backtestGeneticBundle.tsx +++ b/src/Managing.WebApp/src/pages/backtestPage/backtestGeneticBundle.tsx @@ -43,6 +43,7 @@ const ALL_INDICATORS = [ IndicatorType.SuperTrendCrossEma, IndicatorType.DualEmaCross, IndicatorType.StochasticCross, + IndicatorType.BollingerBandsPercentBMomentumBreakout, ] // Form Interface diff --git a/src/Managing.WebApp/src/pages/scenarioPage/indicatorList.tsx b/src/Managing.WebApp/src/pages/scenarioPage/indicatorList.tsx index c9fa3926..324251b1 100644 --- a/src/Managing.WebApp/src/pages/scenarioPage/indicatorList.tsx +++ b/src/Managing.WebApp/src/pages/scenarioPage/indicatorList.tsx @@ -314,7 +314,8 @@ const IndicatorList: React.FC = () => { {indicatorType == IndicatorType.SuperTrend || indicatorType == IndicatorType.SuperTrendCrossEma || - indicatorType == IndicatorType.ChandelierExit ? ( + indicatorType == IndicatorType.ChandelierExit || + indicatorType == IndicatorType.BollingerBandsPercentBMomentumBreakout ? ( <>