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

@@ -44,6 +44,17 @@ Oversold Threshold: 20
Overbought Threshold: 80
```
**Bollinger Bands Example:**
```
Type: Context
Label: Bollinger Bands Volatility Protection
Core Logic: Uses the Bandwidth (distance between Upper and Lower bands) to measure market volatility and apply veto filters during extreme conditions.
Context Confidence Levels: Block signals when bandwidth is extremely high (>0.15) or low (<0.02), validate when normal (0.02-0.15).
Parameters:
Period (default: 20)
StDev (default: 2.0)
```
### Step 2: Determine Implementation Details
**Check Existing Indicators:**
@@ -103,13 +114,26 @@ public class {IndicatorName} : IndicatorBase
base(name, IndicatorType.{EnumName})
{
Signals = new List<LightSignal>();
// Initialize parameters
// Initialize parameters (e.g., Period, Multiplier, StDev)
}
// Implementation methods...
}
```
**For Bollinger Bands (use shared base):**
```csharp
public class {IndicatorName} : BollingerBandsBase
{
public {IndicatorName}(string name, int period, double stdev) :
base(name, IndicatorType.{EnumName}, period, stdev)
{
}
// Only implement ProcessBollingerBandsSignals method
}
```
**Shared Base Class Pattern (use only if mapping is shared):**
If another indicator uses the same candle result mapping, extend from a shared base class:
@@ -174,32 +198,69 @@ public enum IndicatorType
}
```
**Update IndicatorBase.cs:**
- Add any new parameter properties needed (e.g., `StDev` for Bollinger Bands)
**Update LightIndicator.cs:**
- Add any new parameter properties with proper Id attributes for Orleans serialization
- Update `LightToBase()` method to copy new properties
**Update IndicatorRequest.cs:**
- Add any new parameter properties to match LightIndicator
**Update ScenarioHelpers.cs:**
- Add case to `BuildIndicator()` method: `IndicatorType.{EnumName} => new {IndicatorName}(indicator.Name, {parameters})`
- Add case to `GetSignalType()` method: `IndicatorType.{EnumName} => SignalType.{Type}`
- Add parameters to `BuildIndicator()` overload if needed
- Add parameter validation in `BuildIndicator()` method switch statement
- Add new parameters to `BuildIndicator()` method signature if needed
- Update `BaseToLight()` method to copy all LightIndicator properties
**Update BacktestJobService.cs:**
- Update LightIndicator creation in bundle job creation to include all new properties
- Ensure all indicator parameters are properly mapped from requests
**Update GeneticService.cs:**
- Add default values to `DefaultIndicatorValues`: `[IndicatorType.{EnumName}] = new() { {param_mappings} }`
- Add parameter ranges to `IndicatorParameterRanges`: `[IndicatorType.{EnumName}] = new() { {param_ranges} }`
- Add parameter mapping to `IndicatorParamMapping`: `[IndicatorType.{EnumName}] = [{param_names}]`
- Update `TradingBotChromosome.GetSelectedIndicators()` to handle new parameters
**Update Frontend Files:**
*CustomScenario.tsx:*
- Add new parameters to indicator type definitions
- Update parameter input handling (float vs int parsing)
- Add default values for new parameters
*TradeChart.tsx (if applicable):*
- Add visualization logic for new indicator bands/lines
- Use appropriate colors and styles for differentiation
### Step 5: Test and Validate
**Compile Check:**
```bash
# Backend compilation
dotnet build
# Frontend compilation
cd src/Managing.WebApp && npm run build
```
**Basic Validation:**
- Verify indicator appears in GeneticService configurations
- Check that BuildIndicator methods work correctly
- Ensure proper SignalType assignment
- Verify LightIndicator serialization works (Orleans Id attributes)
- Check parameter validation in ScenarioHelpers.BuildIndicator
- Confirm frontend parameter handling works correctly
**Integration Test:**
- Create a simple backtest using the new indicator
- Verify signals are generated correctly
- Check parameter handling and validation
- Test frontend scenario creation with new parameters
- Verify chart visualization displays correctly (if applicable)
## Available Skender.Stock.Indicators
@@ -298,7 +359,7 @@ When implementing a new indicator, search the [Skender documentation](https://do
- **Moving Averages**: Period 5-300 (shorter = responsive, longer = smooth)
- **Oscillators**: Period 5-50 (RSI: 14, Stoch: 14, CCI: 20)
- **Trend Following**: Period 10-50, Multiplier 1.0-5.0
- **Volatility**: Period 5-50, Standard Deviations 1.0-3.0
- **Volatility**: Period 5-50, Standard Deviations (StDev) 1.0-3.0 (Bollinger Bands)
- **Volume**: Period 5-50 (OBV uses no period)
**Testing Parameters:**
@@ -343,6 +404,7 @@ When creating your `Candle{Indicator}` mapping class, include all relevant resul
- `GetStc(cyclePeriod, fastPeriod, slowPeriod)``StcResult` - Used in STC, Lagging STC
- `GetStdDev(period)``StdDevResult` - Used in StDev Context
- `GetChandelier(period, multiplier, type)``ChandelierResult` - Used in Chandelier Exit
- `GetBollingerBands(period, stdev)``BollingerBandsResult` - Used in Bollinger Bands indicators
- `GetAdx(period)``AdxResult` - Used in SuperTrend Cross EMA
**Available But Unused:**
@@ -390,6 +452,31 @@ public class StochasticFiltered : StochasticBase { /* Specific logic */ }
public class AnotherStochasticIndicator : StochasticBase { /* Specific logic */ }
```
**Bollinger Bands Example (Implemented):**
```csharp
public abstract class BollingerBandsBase : IndicatorBase
{
protected double Stdev { get; set; }
protected BollingerBandsBase(string name, IndicatorType type, int period, double stdev)
: base(name, type)
{
Stdev = stdev;
Period = period;
}
protected virtual IEnumerable<CandleBollingerBands> MapBollingerBandsToCandle(
IEnumerable<BollingerBandsResult> bbResults, IEnumerable<Candle> candles)
{
// Shared Bollinger Bands mapping logic with all properties
// (Sma, UpperBand, LowerBand, PercentB, ZScore, Width)
}
}
public class BollingerBandsPercentBMomentumBreakout : BollingerBandsBase { /* %B momentum logic */ }
public class BollingerBandsVolatilityProtection : BollingerBandsBase { /* Volatility protection logic */ }
```
**When NOT to Use:**
- Indicators have different result types (Stoch vs StochRsi)
- Mapping logic differs significantly
@@ -410,6 +497,13 @@ public class AnotherStochasticIndicator : StochasticBase { /* Specific logic */
- [ ] Constructor parameters match IIndicator interface
- [ ] SignalType correctly assigned
- [ ] Enum added to IndicatorType
- [ ] BuildIndicator methods updated
- [ ] GeneticService configurations updated
- [ ] Compiles without errors
- [ ] IndicatorBase.cs properties added if needed
- [ ] LightIndicator.cs properties added with proper Id attributes
- [ ] IndicatorRequest.cs properties added
- [ ] ScenarioHelpers.cs BuildIndicator and BaseToLight methods updated
- [ ] BacktestJobService.cs LightIndicator mapping updated
- [ ] GeneticService.cs configurations updated (defaults, ranges, mappings)
- [ ] Frontend CustomScenario.tsx updated for new parameters
- [ ] Frontend TradeChart.tsx updated for visualization if needed
- [ ] Compiles without errors (backend and frontend)
- [ ] TypeScript types properly aligned

View File

@@ -666,6 +666,16 @@ public class BacktestController : BaseController
}
}
// Normalize SignalType for all indicators based on their IndicatorType
// This ensures the correct SignalType is saved regardless of what the frontend sent
if (request.UniversalConfig.Scenario?.Indicators != null)
{
foreach (var indicator in request.UniversalConfig.Scenario.Indicators)
{
indicator.SignalType = ScenarioHelpers.GetSignalType(indicator.Type);
}
}
// Create the bundle backtest request
var bundleRequest = new BundleBacktestRequest
{

View File

@@ -155,16 +155,24 @@ public class JobService
{
Indicators = sReq.Indicators?.Select(ind => new LightIndicator(ind.Name, ind.Type)
{
SignalType = ind.SignalType,
MinimumHistory = ind.MinimumHistory,
Period = ind.Period,
FastPeriods = ind.FastPeriods,
SlowPeriods = ind.SlowPeriods,
SignalPeriods = ind.SignalPeriods,
Multiplier = ind.Multiplier,
StDev = ind.StDev,
SmoothPeriods = ind.SmoothPeriods,
StochPeriods = ind.StochPeriods,
CyclePeriods = ind.CyclePeriods
CyclePeriods = ind.CyclePeriods,
KFactor = ind.KFactor,
DFactor = ind.DFactor,
TenkanPeriods = ind.TenkanPeriods,
KijunPeriods = ind.KijunPeriods,
SenkouBPeriods = ind.SenkouBPeriods,
OffsetPeriods = ind.OffsetPeriods,
SenkouOffset = ind.SenkouOffset,
ChikouOffset = ind.ChikouOffset
}).ToList() ?? new List<LightIndicator>()
};
}

View File

@@ -107,7 +107,12 @@ public class GeneticService : IGeneticService
[IndicatorType.BollingerBandsPercentBMomentumBreakout] = new()
{
["period"] = 20.0,
["multiplier"] = 2.0
["stdev"] = 2.0
},
[IndicatorType.BollingerBandsVolatilityProtection] = new()
{
["period"] = 20.0,
["stdev"] = 2.0
},
[IndicatorType.IchimokuKumoTrend] = new()
{
@@ -202,7 +207,12 @@ public class GeneticService : IGeneticService
[IndicatorType.BollingerBandsPercentBMomentumBreakout] = new()
{
["period"] = (5.0, 50.0),
["multiplier"] = (1.0, 5.0)
["stdev"] = (1.0, 5.0)
},
[IndicatorType.BollingerBandsVolatilityProtection] = new()
{
["period"] = (10.0, 50.0),
["stdev"] = (1.5, 3.0)
},
[IndicatorType.IchimokuKumoTrend] = new()
{
@@ -231,7 +241,8 @@ public class GeneticService : IGeneticService
[IndicatorType.StochasticCross] = ["stochPeriods", "signalPeriods", "smoothPeriods", "kFactor", "dFactor"],
[IndicatorType.Stc] = ["cyclePeriods", "fastPeriods", "slowPeriods"],
[IndicatorType.LaggingStc] = ["cyclePeriods", "fastPeriods", "slowPeriods"],
[IndicatorType.BollingerBandsPercentBMomentumBreakout] = ["period", "multiplier"],
[IndicatorType.BollingerBandsPercentBMomentumBreakout] = ["period", "stdev"],
[IndicatorType.BollingerBandsVolatilityProtection] = ["period", "stdev"],
[IndicatorType.IchimokuKumoTrend] = ["tenkanPeriods", "kijunPeriods", "senkouBPeriods", "offsetPeriods"]
};
@@ -860,10 +871,10 @@ public class TradingBotChromosome : ChromosomeBase
indicator.SignalPeriods = Convert.ToInt32(genes[baseIndex + 3].Value);
}
paramName = GetParameterName(4); // multiplier
paramName = GetParameterName(4); // stdev
if (paramName != null && HasParameter(_eligibleIndicators[i], paramName))
{
indicator.Multiplier = Convert.ToDouble(genes[baseIndex + 4].Value);
indicator.StDev = Convert.ToDouble(genes[baseIndex + 4].Value);
}
paramName = GetParameterName(5); // stochPeriods
@@ -983,7 +994,7 @@ public class TradingBotChromosome : ChromosomeBase
1 => "fastPeriods",
2 => "slowPeriods",
3 => "signalPeriods",
4 => "multiplier",
4 => "stdev",
5 => "stochPeriods",
6 => "smoothPeriods",
7 => "cyclePeriods",

View File

@@ -324,7 +324,6 @@ public class BundleBacktestGrain : Grain, IBundleBacktestGrain, IRemindable
{
Indicators = sReq.Indicators?.Select(i => new LightIndicator(i.Name, i.Type)
{
SignalType = i.SignalType,
MinimumHistory = i.MinimumHistory,
Period = i.Period,
FastPeriods = i.FastPeriods,

View File

@@ -229,7 +229,6 @@ public class BundleBacktestWorker : BaseWorker<BundleBacktestWorker>
{
Indicators = sReq.Indicators?.Select(i => new LightIndicator(i.Name, i.Type)
{
SignalType = i.SignalType,
MinimumHistory = i.MinimumHistory,
Period = i.Period,
FastPeriods = i.FastPeriods,

View File

@@ -67,6 +67,7 @@ public static class Enums
SuperTrendCrossEma,
DualEmaCross,
BollingerBandsPercentBMomentumBreakout,
BollingerBandsVolatilityProtection,
IchimokuKumoTrend
}

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)

View File

@@ -48,7 +48,8 @@ const CustomScenario: React.FC<ICustomScenario> = ({
case IndicatorType.SuperTrendCrossEma:
case IndicatorType.ChandelierExit:
case IndicatorType.BollingerBandsPercentBMomentumBreakout:
params = ['period', 'multiplier'];
case IndicatorType.BollingerBandsVolatilityProtection:
params = ['period', 'stdev'];
break;
case IndicatorType.StochRsiTrend:
@@ -147,6 +148,7 @@ const CustomScenario: React.FC<ICustomScenario> = ({
slowPeriods: 26,
signalPeriods: 9,
multiplier: 3.0,
stDev: 2.0,
stochPeriods: 14,
smoothPeriods: 3,
cyclePeriods: 10,
@@ -284,10 +286,10 @@ const CustomScenario: React.FC<ICustomScenario> = ({
<FormInput key={param} label={param.charAt(0).toUpperCase() + param.slice(1)} htmlFor={`${param}-${index}`} inline={false}>
<input
value={indicator[param as keyof LightIndicator] as number || ''}
onChange={(e) => updateIndicator(index, param, param.includes('multiplier') ? parseFloat(e.target.value) : parseInt(e.target.value))}
onChange={(e) => updateIndicator(index, param, param.includes('multiplier') || param.includes('stdev') ? parseFloat(e.target.value) : parseInt(e.target.value))}
type='number'
step={param.includes('multiplier') ? '0.1' : '1'}
min={param.includes('multiplier') ? '0.1' : '1'}
step={param.includes('multiplier') || param.includes('stdev') ? '0.1' : '1'}
min={param.includes('multiplier') || param.includes('stdev') ? '0.1' : '1'}
className='input input-bordered w-full'
/>
</FormInput>

View File

@@ -39,7 +39,6 @@ import useTheme from '../../../../hooks/useTheme'
// }
type ITradeChartProps = {
candles: Candle[]
positions: Position[]
@@ -69,18 +68,18 @@ const TradeChart = ({
const series1 = useRef<ISeriesApi<'Candlestick'>>()
const [timeDiff, setTimeDiff] = useState<number>(0)
const [candleCount, setCandleCount] = useState<number>(candles.length)
const [chartDimensions, setChartDimensions] = useState({ width: 0, height: 0 })
const [chartDimensions, setChartDimensions] = useState({width: 0, height: 0})
// Get responsive dimensions
const getResponsiveDimensions = () => {
if (!containerRef.current) return { width: width || 510, height: height || 300 }
if (!containerRef.current) return {width: width || 510, height: height || 300}
const containerWidth = containerRef.current.offsetWidth
const containerHeight = containerRef.current.offsetHeight
// Use provided dimensions if available, otherwise calculate responsive ones
if (width && height) {
return { width, height }
return {width, height}
}
// For responsive mode, calculate based on container
@@ -231,7 +230,7 @@ const TradeChart = ({
} else {
color = negativeColor
}
}else if (status == PositionStatus.Filled) {
} else if (status == PositionStatus.Filled) {
color = theme.warning
}
}
@@ -393,9 +392,8 @@ const TradeChart = ({
}
// Price panel
if (indicatorsValues?.EmaTrend != null || indicatorsValues?.EmaCross != null)
{
const emaSeries = chart.current.addLineSeries({
if (indicatorsValues?.EmaTrend != null || indicatorsValues?.EmaCross != null) {
const emaSeries = chart.current.addLineSeries({
color: theme.secondary,
lineWidth: 1,
priceLineVisible: true,
@@ -417,8 +415,7 @@ const TradeChart = ({
}
})
if (emaData != null)
{
if (emaData != null) {
// @ts-ignore
emaSeries.setData(emaData)
}
@@ -446,8 +443,8 @@ const TradeChart = ({
superTrendSeries.setData(superTrend)
}
// Display chandeliers exits
if (indicatorsValues?.ChandelierExit != null) {
// Display chandeliers exits
if (indicatorsValues?.ChandelierExit != null) {
const chandelierExitsLongsSeries = chart.current.addLineSeries({
color: theme.info,
lineWidth: 1,
@@ -568,6 +565,47 @@ const TradeChart = ({
lowerBandSeries.setData(lowerBandData)
}
// Display Bollinger Bands on price chart for Volatility Protection
if (indicatorsValues?.BollingerBandsVolatilityProtection != null) {
const upperBandSeries = chart.current.addLineSeries({
color: '#FF6B6B', // Lighter red for volatility protection bands
lineWidth: 1,
priceLineVisible: false,
priceLineWidth: 1,
title: 'Volatility Protection Upper Band',
pane: 0,
lineStyle: LineStyle.Dotted,
})
const upperBandData = indicatorsValues.BollingerBandsVolatilityProtection.bollingerBands?.map((w) => {
return {
time: moment(w.date).unix(),
value: w.upperBand,
}
})
// @ts-ignore
upperBandSeries.setData(upperBandData)
const lowerBandSeries = chart.current.addLineSeries({
color: '#4ECDC4', // Teal for volatility protection bands
lineWidth: 1,
priceLineVisible: false,
priceLineWidth: 1,
title: 'Volatility Protection Lower Band',
pane: 0,
lineStyle: LineStyle.Dotted,
})
const lowerBandData = indicatorsValues.BollingerBandsVolatilityProtection.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)
}
@@ -576,8 +614,7 @@ const TradeChart = ({
var paneCount = 1
if (indicatorsValues?.RsiDivergence != null || indicatorsValues?.RsiDivergenceConfirm != null)
{
if (indicatorsValues?.RsiDivergence != null || indicatorsValues?.RsiDivergenceConfirm != null) {
const rsiSeries = chart.current.addLineSeries({
pane: paneCount,
title: 'RSI',
@@ -690,7 +727,7 @@ const TradeChart = ({
})
var priceOptions = {
scaleMargins:{
scaleMargins: {
top: 0.7,
bottom: 0.02,
}
@@ -751,7 +788,7 @@ const TradeChart = ({
paneCount++
}
if (indicatorsValues?.StochRsiTrend){
if (indicatorsValues?.StochRsiTrend) {
const stochRsiSeries = chart.current.addLineSeries({
...baselineOptions,
priceLineVisible: false,
@@ -963,9 +1000,9 @@ const TradeChart = ({
<div
ref={containerRef}
className="w-full h-full"
style={{ minHeight: height || 250 }}
style={{minHeight: height || 250}}
>
<div ref={chartRef} className="w-full h-full" />
<div ref={chartRef} className="w-full h-full"/>
</div>
)
}

View File

@@ -572,9 +572,18 @@ const UnifiedTradingModal: React.FC<UnifiedTradingModalProps> = ({
slowPeriods: indicator.slowPeriods,
signalPeriods: indicator.signalPeriods,
multiplier: indicator.multiplier,
stDev: indicator.stDev,
smoothPeriods: indicator.smoothPeriods,
stochPeriods: indicator.stochPeriods,
cyclePeriods: indicator.cyclePeriods,
kFactor: indicator.kFactor,
dFactor: indicator.dFactor,
tenkanPeriods: indicator.tenkanPeriods,
kijunPeriods: indicator.kijunPeriods,
senkouBPeriods: indicator.senkouBPeriods,
offsetPeriods: indicator.offsetPeriods,
senkouOffset: indicator.senkouOffset,
chikouOffset: indicator.chikouOffset,
})).filter(indicator => indicator.type) || [] // Only filter out indicators without type
};
};

View File

@@ -4960,6 +4960,7 @@ export interface LightIndicator {
slowPeriods?: number | null;
signalPeriods?: number | null;
multiplier?: number | null;
stDev?: number | null;
smoothPeriods?: number | null;
stochPeriods?: number | null;
cyclePeriods?: number | null;
@@ -4991,6 +4992,7 @@ export enum IndicatorType {
SuperTrendCrossEma = "SuperTrendCrossEma",
DualEmaCross = "DualEmaCross",
BollingerBandsPercentBMomentumBreakout = "BollingerBandsPercentBMomentumBreakout",
BollingerBandsVolatilityProtection = "BollingerBandsVolatilityProtection",
IchimokuKumoTrend = "IchimokuKumoTrend",
}
@@ -5263,9 +5265,18 @@ export interface IndicatorRequest {
slowPeriods?: number | null;
signalPeriods?: number | null;
multiplier?: number | null;
stDev?: number | null;
smoothPeriods?: number | null;
stochPeriods?: number | null;
cyclePeriods?: number | null;
kFactor?: number | null;
dFactor?: number | null;
tenkanPeriods?: number | null;
kijunPeriods?: number | null;
senkouBPeriods?: number | null;
offsetPeriods?: number | null;
senkouOffset?: number | null;
chikouOffset?: number | null;
}
export interface MoneyManagementRequest {
@@ -5577,6 +5588,7 @@ export interface IndicatorBase {
slowPeriods?: number | null;
signalPeriods?: number | null;
multiplier?: number | null;
stDev?: number | null;
smoothPeriods?: number | null;
stochPeriods?: number | null;
cyclePeriods?: number | null;

View File

@@ -426,6 +426,7 @@ export interface LightIndicator {
slowPeriods?: number | null;
signalPeriods?: number | null;
multiplier?: number | null;
stDev?: number | null;
smoothPeriods?: number | null;
stochPeriods?: number | null;
cyclePeriods?: number | null;
@@ -457,6 +458,7 @@ export enum IndicatorType {
SuperTrendCrossEma = "SuperTrendCrossEma",
DualEmaCross = "DualEmaCross",
BollingerBandsPercentBMomentumBreakout = "BollingerBandsPercentBMomentumBreakout",
BollingerBandsVolatilityProtection = "BollingerBandsVolatilityProtection",
IchimokuKumoTrend = "IchimokuKumoTrend",
}
@@ -729,9 +731,18 @@ export interface IndicatorRequest {
slowPeriods?: number | null;
signalPeriods?: number | null;
multiplier?: number | null;
stDev?: number | null;
smoothPeriods?: number | null;
stochPeriods?: number | null;
cyclePeriods?: number | null;
kFactor?: number | null;
dFactor?: number | null;
tenkanPeriods?: number | null;
kijunPeriods?: number | null;
senkouBPeriods?: number | null;
offsetPeriods?: number | null;
senkouOffset?: number | null;
chikouOffset?: number | null;
}
export interface MoneyManagementRequest {
@@ -1043,6 +1054,7 @@ export interface IndicatorBase {
slowPeriods?: number | null;
signalPeriods?: number | null;
multiplier?: number | null;
stDev?: number | null;
smoothPeriods?: number | null;
stochPeriods?: number | null;
cyclePeriods?: number | null;

View File

@@ -231,9 +231,18 @@ const BundleRequestModal: React.FC<BundleRequestModalProps> = ({
slowPeriods: indicator.slowPeriods || 26,
signalPeriods: indicator.signalPeriods || 9,
multiplier: indicator.multiplier || 3.0,
stDev: indicator.stDev || 2.0,
stochPeriods: indicator.stochPeriods || 14,
smoothPeriods: indicator.smoothPeriods || 3,
cyclePeriods: indicator.cyclePeriods || 10
cyclePeriods: indicator.cyclePeriods || 10,
kFactor: indicator.kFactor || 3.0,
dFactor: indicator.dFactor || 3.0,
tenkanPeriods: indicator.tenkanPeriods || 9,
kijunPeriods: indicator.kijunPeriods || 26,
senkouBPeriods: indicator.senkouBPeriods || 52,
offsetPeriods: indicator.offsetPeriods || 26,
senkouOffset: indicator.senkouOffset || 26,
chikouOffset: indicator.chikouOffset || 26
})),
loopbackPeriod: scenario.loopbackPeriod || 1
} : undefined,