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,
|
["cyclePeriods"] = 10.0,
|
||||||
["fastPeriods"] = 12.0,
|
["fastPeriods"] = 12.0,
|
||||||
["slowPeriods"] = 26.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),
|
["cyclePeriods"] = (5.0, 30.0),
|
||||||
["fastPeriods"] = (5.0, 50.0),
|
["fastPeriods"] = (5.0, 50.0),
|
||||||
["slowPeriods"] = (10.0, 100.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.StochRsiTrend] = ["period", "stochPeriods", "signalPeriods", "smoothPeriods"],
|
||||||
[IndicatorType.StochasticCross] = ["stochPeriods", "signalPeriods", "smoothPeriods", "kFactor", "dFactor"],
|
[IndicatorType.StochasticCross] = ["stochPeriods", "signalPeriods", "smoothPeriods", "kFactor", "dFactor"],
|
||||||
[IndicatorType.Stc] = ["cyclePeriods", "fastPeriods", "slowPeriods"],
|
[IndicatorType.Stc] = ["cyclePeriods", "fastPeriods", "slowPeriods"],
|
||||||
[IndicatorType.LaggingStc] = ["cyclePeriods", "fastPeriods", "slowPeriods"]
|
[IndicatorType.LaggingStc] = ["cyclePeriods", "fastPeriods", "slowPeriods"],
|
||||||
|
[IndicatorType.BollingerBandsPercentBMomentumBreakout] = ["period", "multiplier"]
|
||||||
};
|
};
|
||||||
|
|
||||||
public GeneticService(
|
public GeneticService(
|
||||||
|
|||||||
@@ -65,7 +65,8 @@ public static class Enums
|
|||||||
StDev,
|
StDev,
|
||||||
LaggingStc,
|
LaggingStc,
|
||||||
SuperTrendCrossEma,
|
SuperTrendCrossEma,
|
||||||
DualEmaCross
|
DualEmaCross,
|
||||||
|
BollingerBandsPercentBMomentumBreakout
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum SignalType
|
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),
|
indicator.FastPeriods.Value, indicator.SlowPeriods.Value),
|
||||||
IndicatorType.SuperTrendCrossEma => new SuperTrendCrossEma(indicator.Name,
|
IndicatorType.SuperTrendCrossEma => new SuperTrendCrossEma(indicator.Name,
|
||||||
indicator.Period.Value, indicator.Multiplier.Value),
|
indicator.Period.Value, indicator.Multiplier.Value),
|
||||||
|
IndicatorType.BollingerBandsPercentBMomentumBreakout => new BollingerBandsPercentBMomentumBreakout(indicator.Name,
|
||||||
|
indicator.Period.Value, indicator.Multiplier.Value),
|
||||||
_ => throw new NotImplementedException(),
|
_ => throw new NotImplementedException(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -289,6 +291,7 @@ public static class ScenarioHelpers
|
|||||||
IndicatorType.StDev => SignalType.Context,
|
IndicatorType.StDev => SignalType.Context,
|
||||||
IndicatorType.LaggingStc => SignalType.Signal,
|
IndicatorType.LaggingStc => SignalType.Signal,
|
||||||
IndicatorType.SuperTrendCrossEma => SignalType.Signal,
|
IndicatorType.SuperTrendCrossEma => SignalType.Signal,
|
||||||
|
IndicatorType.BollingerBandsPercentBMomentumBreakout => SignalType.Signal,
|
||||||
_ => throw new NotImplementedException(),
|
_ => throw new NotImplementedException(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ const CustomScenario: React.FC<ICustomScenario> = ({
|
|||||||
case IndicatorType.SuperTrend:
|
case IndicatorType.SuperTrend:
|
||||||
case IndicatorType.SuperTrendCrossEma:
|
case IndicatorType.SuperTrendCrossEma:
|
||||||
case IndicatorType.ChandelierExit:
|
case IndicatorType.ChandelierExit:
|
||||||
|
case IndicatorType.BollingerBandsPercentBMomentumBreakout:
|
||||||
params = ['period', 'multiplier'];
|
params = ['period', 'multiplier'];
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
@@ -485,6 +485,47 @@ const TradeChart = ({
|
|||||||
chandelierExitsShortsSeries.setData(chandelierExitsShorts)
|
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) {
|
if (markers.length > 0) {
|
||||||
series1.current.setMarkers(markers)
|
series1.current.setMarkers(markers)
|
||||||
}
|
}
|
||||||
@@ -843,6 +884,37 @@ const TradeChart = ({
|
|||||||
})
|
})
|
||||||
paneCount++
|
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 (
|
return (
|
||||||
|
|||||||
@@ -4984,6 +4984,7 @@ export enum IndicatorType {
|
|||||||
LaggingStc = "LaggingStc",
|
LaggingStc = "LaggingStc",
|
||||||
SuperTrendCrossEma = "SuperTrendCrossEma",
|
SuperTrendCrossEma = "SuperTrendCrossEma",
|
||||||
DualEmaCross = "DualEmaCross",
|
DualEmaCross = "DualEmaCross",
|
||||||
|
BollingerBandsPercentBMomentumBreakout = "BollingerBandsPercentBMomentumBreakout",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SignalType {
|
export enum SignalType {
|
||||||
|
|||||||
@@ -450,6 +450,7 @@ export enum IndicatorType {
|
|||||||
LaggingStc = "LaggingStc",
|
LaggingStc = "LaggingStc",
|
||||||
SuperTrendCrossEma = "SuperTrendCrossEma",
|
SuperTrendCrossEma = "SuperTrendCrossEma",
|
||||||
DualEmaCross = "DualEmaCross",
|
DualEmaCross = "DualEmaCross",
|
||||||
|
BollingerBandsPercentBMomentumBreakout = "BollingerBandsPercentBMomentumBreakout",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum SignalType {
|
export enum SignalType {
|
||||||
|
|||||||
@@ -104,6 +104,7 @@ const ALL_INDICATORS = [
|
|||||||
IndicatorType.SuperTrendCrossEma,
|
IndicatorType.SuperTrendCrossEma,
|
||||||
IndicatorType.DualEmaCross,
|
IndicatorType.DualEmaCross,
|
||||||
IndicatorType.StochasticCross,
|
IndicatorType.StochasticCross,
|
||||||
|
IndicatorType.BollingerBandsPercentBMomentumBreakout,
|
||||||
]
|
]
|
||||||
|
|
||||||
// Indicator type to parameter mapping
|
// Indicator type to parameter mapping
|
||||||
@@ -119,6 +120,7 @@ const INDICATOR_PARAM_MAPPING = {
|
|||||||
[IndicatorType.SuperTrend]: ['period', 'multiplier'],
|
[IndicatorType.SuperTrend]: ['period', 'multiplier'],
|
||||||
[IndicatorType.SuperTrendCrossEma]: ['period', 'multiplier'],
|
[IndicatorType.SuperTrendCrossEma]: ['period', 'multiplier'],
|
||||||
[IndicatorType.ChandelierExit]: ['period', 'multiplier'],
|
[IndicatorType.ChandelierExit]: ['period', 'multiplier'],
|
||||||
|
[IndicatorType.BollingerBandsPercentBMomentumBreakout]: ['period', 'multiplier'],
|
||||||
[IndicatorType.StochRsiTrend]: ['period', 'stochPeriods', 'signalPeriods', 'smoothPeriods'],
|
[IndicatorType.StochRsiTrend]: ['period', 'stochPeriods', 'signalPeriods', 'smoothPeriods'],
|
||||||
[IndicatorType.StochasticCross]: ['stochPeriods', 'signalPeriods', 'smoothPeriods', 'kFactor', 'dFactor'],
|
[IndicatorType.StochasticCross]: ['stochPeriods', 'signalPeriods', 'smoothPeriods', 'kFactor', 'dFactor'],
|
||||||
[IndicatorType.Stc]: ['cyclePeriods', 'fastPeriods', 'slowPeriods'],
|
[IndicatorType.Stc]: ['cyclePeriods', 'fastPeriods', 'slowPeriods'],
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ const ALL_INDICATORS = [
|
|||||||
IndicatorType.SuperTrendCrossEma,
|
IndicatorType.SuperTrendCrossEma,
|
||||||
IndicatorType.DualEmaCross,
|
IndicatorType.DualEmaCross,
|
||||||
IndicatorType.StochasticCross,
|
IndicatorType.StochasticCross,
|
||||||
|
IndicatorType.BollingerBandsPercentBMomentumBreakout,
|
||||||
]
|
]
|
||||||
|
|
||||||
// Form Interface
|
// Form Interface
|
||||||
|
|||||||
@@ -314,7 +314,8 @@ const IndicatorList: React.FC = () => {
|
|||||||
|
|
||||||
{indicatorType == IndicatorType.SuperTrend ||
|
{indicatorType == IndicatorType.SuperTrend ||
|
||||||
indicatorType == IndicatorType.SuperTrendCrossEma ||
|
indicatorType == IndicatorType.SuperTrendCrossEma ||
|
||||||
indicatorType == IndicatorType.ChandelierExit ? (
|
indicatorType == IndicatorType.ChandelierExit ||
|
||||||
|
indicatorType == IndicatorType.BollingerBandsPercentBMomentumBreakout ? (
|
||||||
<>
|
<>
|
||||||
<div className="form-control">
|
<div className="form-control">
|
||||||
<div className="input-group">
|
<div className="input-group">
|
||||||
|
|||||||
Reference in New Issue
Block a user