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:
@@ -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(
|
||||
|
||||
@@ -65,7 +65,8 @@ public static class Enums
|
||||
StDev,
|
||||
LaggingStc,
|
||||
SuperTrendCrossEma,
|
||||
DualEmaCross
|
||||
DualEmaCross,
|
||||
BollingerBandsPercentBMomentumBreakout
|
||||
}
|
||||
|
||||
public enum SignalType
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -4984,6 +4984,7 @@ export enum IndicatorType {
|
||||
LaggingStc = "LaggingStc",
|
||||
SuperTrendCrossEma = "SuperTrendCrossEma",
|
||||
DualEmaCross = "DualEmaCross",
|
||||
BollingerBandsPercentBMomentumBreakout = "BollingerBandsPercentBMomentumBreakout",
|
||||
}
|
||||
|
||||
export enum SignalType {
|
||||
|
||||
@@ -450,6 +450,7 @@ export enum IndicatorType {
|
||||
LaggingStc = "LaggingStc",
|
||||
SuperTrendCrossEma = "SuperTrendCrossEma",
|
||||
DualEmaCross = "DualEmaCross",
|
||||
BollingerBandsPercentBMomentumBreakout = "BollingerBandsPercentBMomentumBreakout",
|
||||
}
|
||||
|
||||
export enum SignalType {
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -43,6 +43,7 @@ const ALL_INDICATORS = [
|
||||
IndicatorType.SuperTrendCrossEma,
|
||||
IndicatorType.DualEmaCross,
|
||||
IndicatorType.StochasticCross,
|
||||
IndicatorType.BollingerBandsPercentBMomentumBreakout,
|
||||
]
|
||||
|
||||
// Form Interface
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user