Add Bollinger Bands Volatility Protection indicator support

- Introduced BollingerBandsVolatilityProtection indicator in GeneticService with configuration settings for period and standard deviation (stdev).
- Updated ScenarioHelpers to handle creation and validation of the new indicator type.
- Enhanced CustomScenario, backtest, and scenario pages to include BollingerBandsVolatilityProtection in indicator lists and parameter mappings.
- Modified API and types to reflect the addition of the new indicator in relevant enums and mappings.
- Updated frontend components to support new parameters and visualization for Bollinger Bands.
This commit is contained in:
2025-11-25 02:12:57 +07:00
parent 3ec1da531a
commit 6376e13b07
21 changed files with 618 additions and 220 deletions

View File

@@ -56,6 +56,11 @@ public class IndicatorRequest
/// </summary>
public double? Multiplier { get; set; }
/// <summary>
/// Standard deviation parameter for indicators like Bollinger Bands
/// </summary>
public double? StDev { get; set; }
/// <summary>
/// Smooth periods parameter
/// </summary>
@@ -70,4 +75,45 @@ public class IndicatorRequest
/// Cycle periods parameter
/// </summary>
public int? CyclePeriods { get; set; }
/// <summary>
/// K factor parameter for Stochastic Cross
/// </summary>
public double? KFactor { get; set; }
/// <summary>
/// D factor parameter for Stochastic Cross
/// </summary>
public double? DFactor { get; set; }
// Ichimoku-specific parameters
/// <summary>
/// Tenkan periods for Ichimoku
/// </summary>
public int? TenkanPeriods { get; set; }
/// <summary>
/// Kijun periods for Ichimoku
/// </summary>
public int? KijunPeriods { get; set; }
/// <summary>
/// Senkou B periods for Ichimoku
/// </summary>
public int? SenkouBPeriods { get; set; }
/// <summary>
/// Offset periods for Ichimoku
/// </summary>
public int? OffsetPeriods { get; set; }
/// <summary>
/// Senkou offset for Ichimoku
/// </summary>
public int? SenkouOffset { get; set; }
/// <summary>
/// Chikou offset for Ichimoku
/// </summary>
public int? ChikouOffset { get; set; }
}

View File

@@ -0,0 +1,162 @@
using Managing.Core;
using Managing.Domain.Candles;
using Managing.Domain.Indicators;
using Managing.Domain.Shared.Rules;
using Skender.Stock.Indicators;
using static Managing.Common.Enums;
namespace Managing.Domain.Strategies.Base;
public abstract class BollingerBandsBase : IndicatorBase
{
public List<LightSignal> Signals { get; set; }
protected BollingerBandsBase(string name, IndicatorType type, int period, double stdev) : base(name, type)
{
Signals = new List<LightSignal>();
Period = period;
StDev = stdev;
}
public override List<LightSignal> Run(HashSet<Candle> candles)
{
if (candles.Count <= Period)
{
return null;
}
try
{
var bbResults = candles
.GetBollingerBands(Period.Value, StDev.Value)
.RemoveWarmupPeriods()
.ToList();
if (bbResults.Count == 0)
return null;
ProcessBollingerBandsSignals(bbResults, candles);
return Signals.Where(s => s.Confidence != Confidence.None).ToList();
}
catch (RuleException)
{
return null;
}
}
public override List<LightSignal> Run(HashSet<Candle> candles, IndicatorsResultBase preCalculatedValues)
{
if (candles.Count <= Period)
{
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.Where(s => s.Confidence != Confidence.None).ToList();
}
catch (RuleException)
{
return null;
}
}
public override IndicatorsResultBase GetIndicatorValues(HashSet<Candle> candles)
{
return new IndicatorsResultBase()
{
BollingerBands = candles.GetBollingerBands(Period.Value, StDev.Value)
.ToList()
};
}
/// <summary>
/// Abstract method for processing Bollinger Bands signals - implemented by child classes
/// </summary>
protected abstract void ProcessBollingerBandsSignals(List<BollingerBandsResult> bbResults, HashSet<Candle> candles);
/// <summary>
/// Maps Bollinger Bands results to candle objects with all BollingerBandsResult properties
/// </summary>
protected virtual IEnumerable<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.Sma.HasValue)
{
bbCandles.Add(new CandleBollingerBands()
{
Close = candle.Close,
Open = candle.Open,
Date = candle.Date,
Ticker = candle.Ticker,
Exchange = candle.Exchange,
Sma = currentBB.Sma,
UpperBand = currentBB.UpperBand,
LowerBand = currentBB.LowerBand,
PercentB = currentBB.PercentB,
ZScore = currentBB.ZScore,
Width = currentBB.Width
});
}
}
return bbCandles;
}
/// <summary>
/// Shared method for adding signals with duplicate prevention
/// </summary>
protected void AddSignal(Candle 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);
}
}
/// <summary>
/// Base candle class for Bollinger Bands indicators with all BollingerBandsResult properties
/// </summary>
public class CandleBollingerBands : Candle
{
public double? Sma { get; internal set; }
public double? UpperBand { get; internal set; }
public double? LowerBand { get; internal set; }
public double? PercentB { get; internal set; }
public double? ZScore { get; internal set; }
public double? Width { get; internal set; }
}
}

View File

@@ -0,0 +1,71 @@
using Managing.Domain.Candles;
using Managing.Domain.Strategies.Base;
using Skender.Stock.Indicators;
using static Managing.Common.Enums;
namespace Managing.Domain.Strategies.Context;
public class BollingerBandsVolatilityProtection : BollingerBandsBase
{
public BollingerBandsVolatilityProtection(string name, int period, double stdev) : base(name, IndicatorType.BollingerBandsVolatilityProtection, period, stdev)
{
}
/// <summary>
/// Processes Bollinger Bands volatility protection signals based on bandwidth analysis.
/// This method applies a veto filter during periods of market extremes:
/// - Blocks signals when bandwidth is extremely high (dangerous expansion) or extremely low (dead market)
/// - Validates signals when bandwidth is normal to low (excluding squeeze extremes)
/// Bandwidth = (UpperBand - LowerBand) / Sma represents market volatility
/// </summary>
/// <param name="bbResults">List of Bollinger Bands calculation results</param>
/// <param name="candles">Candles to process</param>
protected override void ProcessBollingerBandsSignals(List<BollingerBandsResult> bbResults, HashSet<Candle> candles)
{
var bbCandles = MapBollingerBandsToCandle(bbResults, candles.TakeLast(Period.Value)).ToList();
if (bbCandles.Count == 0)
return;
foreach (var currentCandle in bbCandles)
{
var width = currentCandle.Width ?? 0;
// Determine confidence based on width levels (bandwidth as % of SMA)
// Lower width = less volatility = higher confidence for trading
// Higher width = more volatility = lower confidence for trading
Confidence confidence;
if (width >= 0.15) // Extremely high volatility - dangerous expansion
{
// Block all signals during dangerous volatility expansion
confidence = Confidence.None;
}
else if (width <= 0.02) // Extremely low volatility - dead market/squeeze
{
// Block all signals in dead markets or extreme squeezes
confidence = Confidence.None;
}
else if (width <= 0.05) // Low to normal volatility - good for trading
{
// Validate signals during low volatility trending conditions
confidence = Confidence.High;
}
else if (width <= 0.10) // Normal volatility - acceptable for trading
{
// Validate signals during normal volatility conditions
confidence = Confidence.Medium;
}
else // Moderate high volatility (0.10 - 0.15)
{
// Lower confidence during elevated but not extreme volatility
confidence = Confidence.Low;
}
// Context strategies always return TradeDirection.None
// The confidence level indicates the quality of market conditions
AddSignal(currentCandle, TradeDirection.None, confidence);
}
}
}

View File

@@ -33,6 +33,8 @@ namespace Managing.Domain.Strategies
public double? Multiplier { get; set; }
public double? StDev { get; set; }
public int? SmoothPeriods { get; set; }
public int? StochPeriods { get; set; }

View File

@@ -36,23 +36,25 @@ public class LightIndicator
[Id(8)] public double? Multiplier { get; set; }
[Id(9)] public int? SmoothPeriods { get; set; }
[Id(9)] public double? StDev { get; set; }
[Id(10)] public int? StochPeriods { get; set; }
[Id(10)] public int? SmoothPeriods { get; set; }
[Id(11)] public int? CyclePeriods { get; set; }
[Id(11)] public int? StochPeriods { get; set; }
[Id(12)] public double? KFactor { get; set; }
[Id(12)] public int? CyclePeriods { get; set; }
[Id(13)] public double? DFactor { get; set; }
[Id(13)] public double? KFactor { get; set; }
[Id(14)] public double? DFactor { get; set; }
// Ichimoku-specific parameters
[Id(14)] public int? TenkanPeriods { get; set; }
[Id(15)] public int? KijunPeriods { get; set; }
[Id(16)] public int? SenkouBPeriods { get; set; }
[Id(17)] public int? OffsetPeriods { get; set; }
[Id(18)] public int? SenkouOffset { get; set; }
[Id(19)] public int? ChikouOffset { get; set; }
[Id(15)] public int? TenkanPeriods { get; set; }
[Id(16)] public int? KijunPeriods { get; set; }
[Id(17)] public int? SenkouBPeriods { get; set; }
[Id(18)] public int? OffsetPeriods { get; set; }
[Id(19)] public int? SenkouOffset { get; set; }
[Id(20)] public int? ChikouOffset { get; set; }
/// <summary>
/// Converts a LightIndicator back to a full Indicator
@@ -74,6 +76,7 @@ public class LightIndicator
SlowPeriods = SlowPeriods,
SignalPeriods = SignalPeriods,
Multiplier = Multiplier,
StDev = StDev,
SmoothPeriods = SmoothPeriods,
StochPeriods = StochPeriods,
CyclePeriods = CyclePeriods,

View File

@@ -1,88 +1,17 @@
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 class BollingerBandsPercentBMomentumBreakout : BollingerBandsBase
{
public List<LightSignal> Signals { get; set; }
public BollingerBandsPercentBMomentumBreakout(
string name,
int period,
double stdDev) : base(name, IndicatorType.BollingerBandsPercentBMomentumBreakout)
double stdev) : base(name, IndicatorType.BollingerBandsPercentBMomentumBreakout, period, stdev)
{
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>
@@ -90,9 +19,9 @@ public class BollingerBandsPercentBMomentumBreakout : IndicatorBase
/// 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)
protected override void ProcessBollingerBandsSignals(List<BollingerBandsResult> bbResults, HashSet<Candle> candles)
{
var bbCandles = MapBollingerBandsToCandle(bbResults, candles.TakeLast(Period.Value));
var bbCandles = MapBollingerBandsToCandle(bbResults, candles.TakeLast(Period.Value)).ToList();
if (bbCandles.Count < 2)
return;
@@ -116,74 +45,4 @@ public class BollingerBandsPercentBMomentumBreakout : IndicatorBase
}
}
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,8 +100,11 @@ 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),
IndicatorType.BollingerBandsPercentBMomentumBreakout => new BollingerBandsPercentBMomentumBreakout(
indicator.Name,
indicator.Period.Value, indicator.StDev.Value),
IndicatorType.BollingerBandsVolatilityProtection => new BollingerBandsVolatilityProtection(indicator.Name,
indicator.Period.Value, indicator.StDev.Value),
IndicatorType.IchimokuKumoTrend => new IchimokuKumoTrend(indicator.Name,
indicator.TenkanPeriods ?? 9,
indicator.KijunPeriods ?? 26,
@@ -112,6 +115,8 @@ public static class ScenarioHelpers
_ => throw new NotImplementedException(),
};
result.SignalType = GetSignalType(indicator.Type);
return result;
}
@@ -122,16 +127,24 @@ public static class ScenarioHelpers
{
return new LightIndicator(indicatorBase.Name, indicatorBase.Type)
{
SignalType = indicatorBase.SignalType,
MinimumHistory = indicatorBase.MinimumHistory,
Period = indicatorBase.Period,
FastPeriods = indicatorBase.FastPeriods,
SlowPeriods = indicatorBase.SlowPeriods,
SignalPeriods = indicatorBase.SignalPeriods,
Multiplier = indicatorBase.Multiplier,
StDev = indicatorBase.StDev,
SmoothPeriods = indicatorBase.SmoothPeriods,
StochPeriods = indicatorBase.StochPeriods,
CyclePeriods = indicatorBase.CyclePeriods
CyclePeriods = indicatorBase.CyclePeriods,
KFactor = indicatorBase.KFactor,
DFactor = indicatorBase.DFactor,
TenkanPeriods = indicatorBase.TenkanPeriods,
KijunPeriods = indicatorBase.KijunPeriods,
SenkouBPeriods = indicatorBase.SenkouBPeriods,
OffsetPeriods = indicatorBase.OffsetPeriods,
SenkouOffset = indicatorBase.SenkouOffset,
ChikouOffset = indicatorBase.ChikouOffset
};
}
@@ -143,11 +156,18 @@ public static class ScenarioHelpers
int? slowPeriods = null,
int? signalPeriods = null,
double? multiplier = null,
double? stdev = null,
int? stochPeriods = null,
int? smoothPeriods = null,
int? cyclePeriods = null,
double? kFactor = null,
double? dFactor = null)
double? dFactor = null,
int? tenkanPeriods = null,
int? kijunPeriods = null,
int? senkouBPeriods = null,
int? offsetPeriods = null,
int? senkouOffset = null,
int? chikouOffset = null)
{
IIndicator indicator = new IndicatorBase(name, type);
@@ -249,12 +269,43 @@ public static class ScenarioHelpers
{
throw new Exception($"kFactor must be greater than 0 for {indicator.Type} strategy type");
}
if (indicator.DFactor <= 0)
{
throw new Exception($"dFactor must be greater than 0 for {indicator.Type} strategy type");
}
}
break;
case IndicatorType.BollingerBandsPercentBMomentumBreakout:
case IndicatorType.BollingerBandsVolatilityProtection:
if (!period.HasValue || !stdev.HasValue)
{
throw new Exception($"Missing period or stdev for {indicator.Type} strategy type");
}
else
{
((IndicatorBase)indicator).Period = period;
((IndicatorBase)indicator).StDev = stdev;
}
break;
case IndicatorType.IchimokuKumoTrend:
if (!tenkanPeriods.HasValue || !kijunPeriods.HasValue || !senkouBPeriods.HasValue ||
!offsetPeriods.HasValue)
{
throw new Exception($"Missing Ichimoku parameters for {indicator.Type} strategy type");
}
else
{
((IndicatorBase)indicator).TenkanPeriods = tenkanPeriods;
((IndicatorBase)indicator).KijunPeriods = kijunPeriods;
((IndicatorBase)indicator).SenkouBPeriods = senkouBPeriods;
((IndicatorBase)indicator).OffsetPeriods = offsetPeriods;
((IndicatorBase)indicator).SenkouOffset = senkouOffset;
((IndicatorBase)indicator).ChikouOffset = chikouOffset;
}
break;
case IndicatorType.Stc:
case IndicatorType.LaggingStc:
@@ -299,6 +350,7 @@ public static class ScenarioHelpers
IndicatorType.LaggingStc => SignalType.Signal,
IndicatorType.SuperTrendCrossEma => SignalType.Signal,
IndicatorType.BollingerBandsPercentBMomentumBreakout => SignalType.Signal,
IndicatorType.BollingerBandsVolatilityProtection => SignalType.Context,
IndicatorType.IchimokuKumoTrend => SignalType.Trend,
_ => throw new NotImplementedException(),
};

View File

@@ -37,7 +37,7 @@ public class IndicatorComboConfig
/// <summary>
/// Minimum confidence level to return a signal (default: Low)
/// </summary>
public Confidence MinimumConfidence { get; set; } = Confidence.Low;
public Confidence MinimumConfidence { get; set; } = Confidence.Medium;
/// <summary>
/// Minimum confidence level required from context strategies (default: Medium)