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

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