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:
@@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user