diff --git a/Tests/test_updates_volatility.cs b/Tests/test_updates_volatility.cs index f8877a8e..05d43c60 100644 --- a/Tests/test_updates_volatility.cs +++ b/Tests/test_updates_volatility.cs @@ -101,4 +101,20 @@ public class VolatilityUpdateTests Assert.Equal(initialValue, finalValue, precision); } + + [Fact] + public void Cvi_Update() + { + var indicator = new Cvi(period: 14); + TBar r = GetRandomBar(true); + double initialValue = indicator.Calc(r); + + for (int i = 0; i < RandomUpdates; i++) + { + indicator.Calc(GetRandomBar(IsNew: false)); + } + double finalValue = indicator.Calc(new TBar(r.Time, r.Open, r.High, r.Low, r.Close, r.Volume, IsNew: false)); + + Assert.Equal(initialValue, finalValue, precision); + } } diff --git a/lib/volatility/Cvi.cs b/lib/volatility/Cvi.cs new file mode 100644 index 00000000..a6895321 --- /dev/null +++ b/lib/volatility/Cvi.cs @@ -0,0 +1,109 @@ +using System.Runtime.CompilerServices; +namespace QuanTAlib; + +/// +/// CVI: Chaikin's Volatility +/// A technical indicator developed by Marc Chaikin that measures the volatility of a financial instrument by comparing the spread between the high and low prices. +/// +/// +/// The CVI calculation process: +/// 1. Calculates the difference between the high and low prices. +/// 2. Applies an exponential moving average (EMA) to the differences. +/// 3. Computes the percentage change in the EMA over a specified period. +/// +/// Key characteristics: +/// - Measures volatility +/// - Uses high and low prices +/// - Percentage-based +/// - EMA smoothing +/// +/// Formula: +/// CVI = (EMA(high - low, period) - EMA(high - low, period, offset)) / EMA(high - low, period, offset) * 100 +/// +/// Market Applications: +/// - Volatility assessment +/// - Trend confirmation +/// - Risk management +/// - Entry/exit timing +/// +/// Sources: +/// Marc Chaikin - Original development +/// https://www.investopedia.com/terms/c/chaikins-volatility.asp +/// +/// Note: Higher CVI values indicate higher volatility +/// + +[SkipLocalsInit] +public sealed class Cvi : AbstractBase +{ + private readonly int _period; + private readonly Ema _ema; + private readonly CircularBuffer _buffer; + private double _prevEma; + + /// The number of periods for CVI calculation. + /// Thrown when period is less than 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Cvi(int period) + { + if (period < 1) + { + throw new ArgumentOutOfRangeException(nameof(period), + "Period must be greater than or equal to 1."); + } + _period = period; + _ema = new Ema(period); + _buffer = new CircularBuffer(period); + WarmupPeriod = period; + Name = $"CVI({period})"; + } + + /// The data source object that publishes updates. + /// The number of periods for CVI calculation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Cvi(object source, int period) : this(period) + { + var pubEvent = source.GetType().GetEvent("Pub"); + pubEvent?.AddEventHandler(source, new BarSignal(Sub)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void Init() + { + base.Init(); + _ema.Init(); + _buffer.Clear(); + _prevEma = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void ManageState(bool isNew) + { + if (isNew) + { + _index++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] + protected override double Calculation() + { + ManageState(BarInput.IsNew); + + double highLowDiff = BarInput.High - BarInput.Low; + _buffer.Add(highLowDiff, BarInput.IsNew); + + double ema = _ema.Calc(new TValue(Input.Time, highLowDiff, BarInput.IsNew)).Value; + + double cvi = 0; + if (_index >= _period) + { + double prevEma = _buffer[_buffer.Count - _period]; + cvi = (ema - prevEma) / prevEma * 100; + } + + _prevEma = ema; + IsHot = _index >= WarmupPeriod; + return cvi; + } +} diff --git a/quantower/Volatility/CviIndicator.cs b/quantower/Volatility/CviIndicator.cs new file mode 100644 index 00000000..7a43523f --- /dev/null +++ b/quantower/Volatility/CviIndicator.cs @@ -0,0 +1,54 @@ +using System.Drawing; +using TradingPlatform.BusinessLayer; + +namespace QuanTAlib; + +public class CviIndicator : Indicator, IWatchlistIndicator +{ + [InputParameter("Periods", sortIndex: 1, 1, 2000, 1, 0)] + public int Periods { get; set; } = 20; + + [InputParameter("Show cold values", sortIndex: 21)] + public bool ShowColdValues { get; set; } = true; + + private Cvi? cvi; + protected LineSeries? CviSeries; + public int MinHistoryDepths => Math.Max(5, Periods * 2); + int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths; + + public CviIndicator() + { + Name = "CVI - Chaikin's Volatility"; + Description = "Measures the volatility of a financial instrument by comparing the spread between the high and low prices."; + SeparateWindow = true; + + CviSeries = new($"CVI {Periods}", Color.Blue, 2, LineStyle.Solid); + AddLineSeries(CviSeries); + } + + protected override void OnInit() + { + cvi = new Cvi(Periods); + base.OnInit(); + } + + protected override void OnUpdate(UpdateArgs args) + { + TBar input = IndicatorExtensions.GetInputBar(this, args); + TValue result = cvi!.Calc(input); + + CviSeries!.SetValue(result.Value); + CviSeries!.SetMarker(0, Color.Transparent); //OnPaintChart draws the line, hidden here + } + +#pragma warning disable CA1416 // Validate platform compatibility + + public override string ShortName => $"CVI ({Periods})"; + + public override void OnPaintChart(PaintChartEventArgs args) + { + base.OnPaintChart(args); + this.PaintHLine(args, 0.05, new Pen(Color.DarkRed, width: 2)); + this.PaintSmoothCurve(args, CviSeries!, cvi!.WarmupPeriod, showColdValues: ShowColdValues, tension: 0.2); + } +}