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.
This commit is contained in:
2025-11-24 17:31:17 +07:00
parent 9f7e345457
commit 478dca51e7
11 changed files with 286 additions and 3 deletions

View File

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

View File

@@ -65,7 +65,8 @@ public static class Enums
StDev,
LaggingStc,
SuperTrendCrossEma,
DualEmaCross
DualEmaCross,
BollingerBandsPercentBMomentumBreakout
}
public enum SignalType

View File

@@ -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<LightSignal> Signals { get; set; }
public BollingerBandsPercentBMomentumBreakout(
string name,
int period,
double stdDev) : base(name, IndicatorType.BollingerBandsPercentBMomentumBreakout)
{
Signals = new List<LightSignal>();
Period = period;
Multiplier = stdDev; // Using Multiplier property for stdDev since it's a double
}
public override List<LightSignal> Run(HashSet<Candle> 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<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
{
if (candles.Count <= 10 * Period.Value + 50)
{
return null;
}
try
{
// Use pre-calculated Bollinger Bands values if available
List<BollingerBandsResult> 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;
}
}
/// <summary>
/// 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)
/// </summary>
private void ProcessBollingerBandsSignals(List<BollingerBandsResult> bbResults, HashSet<Candle> 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<Candle> candles)
{
return new IndicatorsResultBase()
{
BollingerBands = candles.GetBollingerBands(Period.Value, (double)Multiplier.Value)
.ToList()
};
}
private List<CandleBollingerBands> MapBollingerBandsToCandle(IEnumerable<BollingerBandsResult> bbResults, IEnumerable<Candle> candles)
{
var bbCandles = new List<CandleBollingerBands>();
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; }
}
}

View File

@@ -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(),
};
}

View File

@@ -47,6 +47,7 @@ const CustomScenario: React.FC<ICustomScenario> = ({
case IndicatorType.SuperTrend:
case IndicatorType.SuperTrendCrossEma:
case IndicatorType.ChandelierExit:
case IndicatorType.BollingerBandsPercentBMomentumBreakout:
params = ['period', 'multiplier'];
break;

View File

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

View File

@@ -4984,6 +4984,7 @@ export enum IndicatorType {
LaggingStc = "LaggingStc",
SuperTrendCrossEma = "SuperTrendCrossEma",
DualEmaCross = "DualEmaCross",
BollingerBandsPercentBMomentumBreakout = "BollingerBandsPercentBMomentumBreakout",
}
export enum SignalType {

View File

@@ -450,6 +450,7 @@ export enum IndicatorType {
LaggingStc = "LaggingStc",
SuperTrendCrossEma = "SuperTrendCrossEma",
DualEmaCross = "DualEmaCross",
BollingerBandsPercentBMomentumBreakout = "BollingerBandsPercentBMomentumBreakout",
}
export enum SignalType {

View File

@@ -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'],

View File

@@ -43,6 +43,7 @@ const ALL_INDICATORS = [
IndicatorType.SuperTrendCrossEma,
IndicatorType.DualEmaCross,
IndicatorType.StochasticCross,
IndicatorType.BollingerBandsPercentBMomentumBreakout,
]
// Form Interface

View File

@@ -314,7 +314,8 @@ const IndicatorList: React.FC = () => {
{indicatorType == IndicatorType.SuperTrend ||
indicatorType == IndicatorType.SuperTrendCrossEma ||
indicatorType == IndicatorType.ChandelierExit ? (
indicatorType == IndicatorType.ChandelierExit ||
indicatorType == IndicatorType.BollingerBandsPercentBMomentumBreakout ? (
<>
<div className="form-control">
<div className="input-group">