Here's an indicator you can use to detect price consolidation. See the code comments for details. Most of this was written using GitHub CoPilot chat. All I had to do was coax it along the way. It came up with the ideas of incorporating RSI and Bollinger Bands, and then added the code. It was nice to avoid all of the mundane coding tedium.
CODE:
using WealthLab.Core; using WealthLab.Indicators; namespace WLUtility.Indicators { /* * PriceConsolidation Indicator * * This indicator detects stock price consolidation using Average True Range (ATR), price range, * Relative Strength Index (RSI), and Bollinger Bands analysis. It provides a histogram-style plot * to visualize consolidation zones. * * Parameters: * - Bars (BarHistory): The historical price data. * - ATR Period (int): The period for calculating ATR. * - ATR Threshold (double): The maximum ATR value to consider consolidation. * - Range Multiplier (double): Multiplier for ATR to define the price range threshold. * - RSI Period (int): The period for calculating RSI. * - Use RSI (bool): Whether to include RSI in the consolidation detection. * - RSI Lower Bound (double): The lower bound of the RSI neutral range. * - RSI Upper Bound (double): The upper bound of the RSI neutral range. * - BB Period (int): The period for calculating Bollinger Bands. * - BB Width (double): The width multiplier for Bollinger Bands. * - Use Bollinger Bands (bool): Whether to include Bollinger Bands in the consolidation detection. * - BB ATR Threshold (double): The maximum Bollinger Band width (relative to ATR) to consider consolidation. * - BB Price Component (PriceComponent): The price component to use for Bollinger Band calculations. * * Usage: * Instantiate the PriceConsolidation class with the desired parameters or use the static Series method * to generate the indicator for a given BarHistory. The indicator values will be 1.0 for detected * consolidation zones and 0.0 otherwise. */ public class PriceConsolidation : IndicatorBase { public PriceConsolidation() { // Default constructor for WL8 } public PriceConsolidation(BarHistory bars, int atrPeriod, double atrThreshold, double rangeMultiplier, int rsiPeriod, bool useRsi, double rsiLowerBound, double rsiUpperBound, int bbPeriod, double bbWidth, bool useBollinger, double bbAtrThreshold, PriceComponent bbPriceComponent) { Parameters[0].Value = bars; Parameters[1].Value = atrPeriod; Parameters[2].Value = atrThreshold; Parameters[3].Value = rangeMultiplier; Parameters[4].Value = rsiPeriod; Parameters[5].Value = useRsi; Parameters[6].Value = rsiLowerBound; Parameters[7].Value = rsiUpperBound; Parameters[8].Value = bbPeriod; Parameters[9].Value = bbWidth; Parameters[10].Value = useBollinger; Parameters[11].Value = bbAtrThreshold; Parameters[12].Value = bbPriceComponent; Populate(); } public override string Name => "Price Consolidation"; public override string Abbreviation => "PriceConsolidation"; public override string HelpDescription => "Detects stock price consolidation using ATR, price range, RSI, and Bollinger Bands analysis."; public override string PaneTag => "Consolidation"; public override PlotStyle DefaultPlotStyle => PlotStyle.Histogram; public override void Populate() { var bars = Parameters[0].AsBarHistory; var atrPeriod = Parameters[1].AsInt; var atrThreshold = Parameters[2].AsDouble; var rangeMultiplier = Parameters[3].AsDouble; var rsiPeriod = Parameters[4].AsInt; var useRsi = Parameters[5].AsBoolean; var rsiLowerBound = Parameters[6].AsDouble; var rsiUpperBound = Parameters[7].AsDouble; var bbPeriod = Parameters[8].AsInt; var bbWidth = Parameters[9].AsDouble; var useBollinger = Parameters[10].AsBoolean; var bbAtrThreshold = Parameters[11].AsDouble; var bbPriceComponent = Parameters[12].AsPriceComponent; DateTimes = bars.DateTimes; if (bars.Count < atrPeriod || (useRsi && bars.Count < rsiPeriod) || (useBollinger && bars.Count < bbPeriod)) { return; } var atr = ATR.Series(bars, atrPeriod); var highest = Highest.Series(bars.Close, atrPeriod); var lowest = Lowest.Series(bars.Close, atrPeriod); var rsi = useRsi ? RSI.Series(bars.Close, rsiPeriod) : null; var priceSeries = bbPriceComponent switch { PriceComponent.Open => bars.Open, PriceComponent.High => bars.High, PriceComponent.Low => bars.Low, PriceComponent.Close => bars.Close, PriceComponent.Volume => bars.Volume, PriceComponent.AveragePriceOHLC => bars.AveragePriceOHLC, PriceComponent.AveragePriceHLC => bars.AveragePriceHLC, PriceComponent.AveragePriceHL => bars.AveragePriceHL, PriceComponent.AveragePriceOC => bars.AveragePriceOC, PriceComponent.AveragePriceHLCC => bars.AveragePriceHLCC, _ => bars.Close }; var bbUpper = useBollinger ? BBUpper.Series(priceSeries, bbPeriod, bbWidth) : null; var bbLower = useBollinger ? BBLower.Series(priceSeries, bbPeriod, bbWidth) : null; for (var i = atrPeriod - 1; i < bars.Count; i++) { var priceRange = highest[i] - lowest[i]; var isAtrConsolidation = atr[i] <= atrThreshold && priceRange <= atr[i] * rangeMultiplier; var isRsiNeutral = !useRsi || (rsi[i] >= rsiLowerBound && rsi[i] <= rsiUpperBound); var bbWidthInAtr = useBollinger ? (bbUpper[i] - bbLower[i]) / atr[i] : double.MaxValue; var isBbSqueeze = useBollinger && bbWidthInAtr <= bbAtrThreshold; Values[i] = (isAtrConsolidation || isBbSqueeze) && isRsiNeutral ? 1.0 : 0.0; // Consolidation detected } } protected override void GenerateParameters() { AddParameter("Bars", ParameterType.BarHistory, null); AddParameter("ATR Period", ParameterType.Int32, 14); AddParameter("ATR Threshold", ParameterType.Double, 1.0); AddParameter("Range Multiplier", ParameterType.Double, 2.0); AddParameter("RSI Period", ParameterType.Int32, 14); AddParameter("Use RSI", ParameterType.Boolean, true); AddParameter("RSI Lower Bound", ParameterType.Double, 40.0); AddParameter("RSI Upper Bound", ParameterType.Double, 60.0); AddParameter("BB Period", ParameterType.Int32, 20); AddParameter("BB Width", ParameterType.Double, 2.0); AddParameter("Use Bollinger Bands", ParameterType.Boolean, true); AddParameter("BB ATR Threshold", ParameterType.Double, 0.5); AddParameter("BB Price Component", ParameterType.PriceComponent, PriceComponent.Close); } public static PriceConsolidation Series(BarHistory bars, int atrPeriod, double atrThreshold, double rangeMultiplier, int rsiPeriod, bool useRsi, double rsiLowerBound, double rsiUpperBound, int bbPeriod, double bbWidth, bool useBollinger, double bbAtrThreshold, PriceComponent bbPriceComponent) { var key = CacheKey("PriceConsolidation", atrPeriod, atrThreshold, rangeMultiplier, rsiPeriod, useRsi, rsiLowerBound, rsiUpperBound, bbPeriod, bbWidth, useBollinger, bbAtrThreshold, bbPriceComponent); if (bars.Cache.TryGetValue(key, out var obj)) { return (PriceConsolidation) obj; } var result = new PriceConsolidation(bars, atrPeriod, atrThreshold, rangeMultiplier, rsiPeriod, useRsi, rsiLowerBound, rsiUpperBound, bbPeriod, bbWidth, useBollinger, bbAtrThreshold, bbPriceComponent); bars.Cache[key] = result; return result; } } }
Rename
An update is below. This fixes the default PaneTag name, adds default values for the constructor's and Series method's parameters to match the indicator parameter defaults, and adds argument range checks for the indicator parameters. There are a few other changes dealing with comments and constants for array indexes.
These tedious changes were made simple using GitHub CoPilot. For example, I asked it:
It generated the changes instantly. I didn't have to type all the stupid defaults one-by-one. Same for the constructor.
These tedious changes were made simple using GitHub CoPilot. For example, I asked it:
QUOTE:
Can you setup defaults for the parameters of the PriceConsolidation.Series() method? You can use the parameter values that are initialized in the GenerateParameters() method. The ordering is the same.
It generated the changes instantly. I didn't have to type all the stupid defaults one-by-one. Same for the constructor.
CODE:
using System; using WealthLab.Core; using WealthLab.Indicators; namespace WLUtility.Indicators { /* * PriceConsolidation Indicator * * This indicator detects stock price consolidation using Average True Range (ATR), price range, * Relative Strength Index (RSI), and Bollinger Bands analysis. It provides a histogram-style plot * to visualize consolidation zones. * * Parameters: * - Bars (BarHistory): The historical price data. * - ATR Period (int): The period for calculating ATR. Default: 14. * - ATR Threshold (double): The maximum ATR value to consider consolidation. Default: 1.0. * - Range Multiplier (double): Multiplier for ATR to define the price range threshold. Default: 2.0. * - RSI Period (int): The period for calculating RSI. Default: 14. * - Use RSI (bool): Whether to include RSI in the consolidation detection. Default: true. * - RSI Lower Bound (double): The lower bound of the RSI neutral range. Default: 40.0. * - RSI Upper Bound (double): The upper bound of the RSI neutral range. Default: 60.0. * - BB Period (int): The period for calculating Bollinger Bands. Default: 20. * - BB Width (double): The width multiplier for Bollinger Bands. Default: 2.0. * - Use Bollinger Bands (bool): Whether to include Bollinger Bands in the consolidation detection. Default: true. * - BB ATR Threshold (double): The maximum Bollinger Band width (relative to ATR) to consider consolidation. Default: 0.5. * - BB Price Component (PriceComponent): The price component to use for Bollinger Band calculations. Default: PriceComponent.Close. * * Usage: * Instantiate the PriceConsolidation class with the desired parameters or use the static Series method * to generate the indicator for a given BarHistory. The indicator values will be 1.0 for detected * consolidation zones and 0.0 otherwise. */ public class PriceConsolidation : IndicatorBase { // Constants representing the indexes of the parameters in the Parameters array. private const int BarsIndex = 0; private const int AtrPeriodIndex = 1; private const int AtrThresholdIndex = 2; private const int RangeMultiplierIndex = 3; private const int RsiPeriodIndex = 4; private const int UseRsiIndex = 5; private const int RsiLowerBoundIndex = 6; private const int RsiUpperBoundIndex = 7; private const int BbPeriodIndex = 8; private const int BbWidthIndex = 9; private const int UseBollingerIndex = 10; private const int BbAtrThresholdIndex = 11; private const int BbPriceComponentIndex = 12; public PriceConsolidation() { // Default constructor for WL8 } public PriceConsolidation( BarHistory bars, int atrPeriod = 14, double atrThreshold = 1.0, double rangeMultiplier = 2.0, int rsiPeriod = 14, bool useRsi = true, double rsiLowerBound = 40.0, double rsiUpperBound = 60.0, int bbPeriod = 20, double bbWidth = 2.0, bool useBollinger = true, double bbAtrThreshold = 0.5, PriceComponent bbPriceComponent = PriceComponent.Close) { Parameters[BarsIndex].Value = bars; Parameters[AtrPeriodIndex].Value = atrPeriod; Parameters[AtrThresholdIndex].Value = atrThreshold; Parameters[RangeMultiplierIndex].Value = rangeMultiplier; Parameters[RsiPeriodIndex].Value = rsiPeriod; Parameters[UseRsiIndex].Value = useRsi; Parameters[RsiLowerBoundIndex].Value = rsiLowerBound; Parameters[RsiUpperBoundIndex].Value = rsiUpperBound; Parameters[BbPeriodIndex].Value = bbPeriod; Parameters[BbWidthIndex].Value = bbWidth; Parameters[UseBollingerIndex].Value = useBollinger; Parameters[BbAtrThresholdIndex].Value = bbAtrThreshold; Parameters[BbPriceComponentIndex].Value = bbPriceComponent; Populate(); } public override string Name => "Price Consolidation"; public override string Abbreviation => "PriceConsolidation"; public override string HelpDescription => "Detects stock price consolidation using ATR, price range, RSI, and Bollinger Bands analysis."; public override string PaneTag => "PriceConsolidation"; public override PlotStyle DefaultPlotStyle => PlotStyle.Histogram; public override void Populate() { var bars = Parameters[BarsIndex].AsBarHistory; var atrPeriod = Parameters[AtrPeriodIndex].AsInt; var atrThreshold = Parameters[AtrThresholdIndex].AsDouble; var rangeMultiplier = Parameters[RangeMultiplierIndex].AsDouble; var rsiPeriod = Parameters[RsiPeriodIndex].AsInt; var useRsi = Parameters[UseRsiIndex].AsBoolean; var rsiLowerBound = Parameters[RsiLowerBoundIndex].AsDouble; var rsiUpperBound = Parameters[RsiUpperBoundIndex].AsDouble; var bbPeriod = Parameters[BbPeriodIndex].AsInt; var bbWidth = Parameters[BbWidthIndex].AsDouble; var useBollinger = Parameters[UseBollingerIndex].AsBoolean; var bbAtrThreshold = Parameters[BbAtrThresholdIndex].AsDouble; var bbPriceComponent = Parameters[BbPriceComponentIndex].AsPriceComponent; ValidateParameters(bars, atrPeriod, atrThreshold, rangeMultiplier, rsiPeriod, rsiLowerBound, rsiUpperBound, bbPeriod, bbWidth, bbAtrThreshold); DateTimes = bars.DateTimes; if (bars.Count < atrPeriod || (useRsi && bars.Count < rsiPeriod) || (useBollinger && bars.Count < bbPeriod)) { return; } var atr = ATR.Series(bars, atrPeriod); var highest = Highest.Series(bars.Close, atrPeriod); var lowest = Lowest.Series(bars.Close, atrPeriod); var rsi = useRsi ? RSI.Series(bars.Close, rsiPeriod) : null; var priceSeries = bbPriceComponent switch { PriceComponent.Open => bars.Open, PriceComponent.High => bars.High, PriceComponent.Low => bars.Low, PriceComponent.Close => bars.Close, PriceComponent.Volume => bars.Volume, PriceComponent.AveragePriceOHLC => bars.AveragePriceOHLC, PriceComponent.AveragePriceHLC => bars.AveragePriceHLC, PriceComponent.AveragePriceHL => bars.AveragePriceHL, PriceComponent.AveragePriceOC => bars.AveragePriceOC, PriceComponent.AveragePriceHLCC => bars.AveragePriceHLCC, _ => bars.Close }; var bbUpper = useBollinger ? BBUpper.Series(priceSeries, bbPeriod, bbWidth) : null; var bbLower = useBollinger ? BBLower.Series(priceSeries, bbPeriod, bbWidth) : null; var maxPeriod = Math.Max(atrPeriod, Math.Max(rsiPeriod, bbPeriod)); for (var i = maxPeriod - 1; i < bars.Count; i++) { var priceRange = highest[i] - lowest[i]; var isAtrConsolidation = atr[i] <= atrThreshold && priceRange <= atr[i] * rangeMultiplier; var isRsiNeutral = !useRsi || (rsi[i] >= rsiLowerBound && rsi[i] <= rsiUpperBound); var bbWidthInAtr = useBollinger ? (bbUpper[i] - bbLower[i]) / atr[i] : double.MaxValue; var isBbSqueeze = useBollinger && bbWidthInAtr <= bbAtrThreshold; Values[i] = (isAtrConsolidation || isBbSqueeze) && isRsiNeutral ? 1.0 : 0.0; // Consolidation detected } } protected override void GenerateParameters() { AddParameter("Bars", ParameterType.BarHistory, null); AddParameter("ATR Period", ParameterType.Int32, 14); AddParameter("ATR Threshold", ParameterType.Double, 1.0); AddParameter("Range Multiplier", ParameterType.Double, 2.0); AddParameter("RSI Period", ParameterType.Int32, 14); AddParameter("Use RSI", ParameterType.Boolean, true); AddParameter("RSI Lower Bound", ParameterType.Double, 40.0); AddParameter("RSI Upper Bound", ParameterType.Double, 60.0); AddParameter("BB Period", ParameterType.Int32, 20); AddParameter("BB Width", ParameterType.Double, 2.0); AddParameter("Use Bollinger Bands", ParameterType.Boolean, true); AddParameter("BB ATR Threshold", ParameterType.Double, 0.5); AddParameter("BB Price Component", ParameterType.PriceComponent, PriceComponent.Close); } public static PriceConsolidation Series( BarHistory bars, int atrPeriod = 14, double atrThreshold = 1.0, double rangeMultiplier = 2.0, int rsiPeriod = 14, bool useRsi = true, double rsiLowerBound = 40.0, double rsiUpperBound = 60.0, int bbPeriod = 20, double bbWidth = 2.0, bool useBollinger = true, double bbAtrThreshold = 0.5, PriceComponent bbPriceComponent = PriceComponent.Close) { var key = CacheKey("PriceConsolidation", atrPeriod, atrThreshold, rangeMultiplier, rsiPeriod, useRsi, rsiLowerBound, rsiUpperBound, bbPeriod, bbWidth, useBollinger, bbAtrThreshold, bbPriceComponent); if (bars.Cache.TryGetValue(key, out var obj)) { return (PriceConsolidation) obj; } var result = new PriceConsolidation(bars, atrPeriod, atrThreshold, rangeMultiplier, rsiPeriod, useRsi, rsiLowerBound, rsiUpperBound, bbPeriod, bbWidth, useBollinger, bbAtrThreshold, bbPriceComponent); bars.Cache[key] = result; return result; } private static void ValidateParameters(BarHistory bars, int atrPeriod, double atrThreshold, double rangeMultiplier, int rsiPeriod, double rsiLowerBound, double rsiUpperBound, int bbPeriod, double bbWidth, double bbAtrThreshold) { if (bars == null) { throw new ArgumentNullException(nameof(bars), "Bars must not be null."); } if (atrPeriod <= 0) { throw new ArgumentOutOfRangeException(nameof(atrPeriod), "ATR Period must be greater than 0."); } if (atrThreshold <= 0) { throw new ArgumentOutOfRangeException(nameof(atrThreshold), "ATR Threshold must be greater than 0."); } if (rangeMultiplier <= 0) { throw new ArgumentOutOfRangeException(nameof(rangeMultiplier), "Range Multiplier must be greater than 0."); } if (rsiPeriod <= 0) { throw new ArgumentOutOfRangeException(nameof(rsiPeriod), "RSI Period must be greater than 0."); } if (rsiLowerBound is < 0 or > 100) { throw new ArgumentOutOfRangeException(nameof(rsiLowerBound), "RSI Lower Bound must be between 0 and 100 inclusive."); } if (rsiUpperBound is < 0 or > 100) { throw new ArgumentOutOfRangeException(nameof(rsiUpperBound), "RSI Upper Bound must be between 0 and 100 inclusive."); } if (bbPeriod <= 0) { throw new ArgumentOutOfRangeException(nameof(bbPeriod), "BB Period must be greater than 0."); } if (bbWidth <= 0) { throw new ArgumentOutOfRangeException(nameof(bbWidth), "BB Width must be greater than 0."); } if (bbAtrThreshold <= 0) { throw new ArgumentOutOfRangeException(nameof(bbAtrThreshold), "BB ATR Threshold must be greater than 0."); } } } }
Your Response
Post
Edit Post
Login is required