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);
+ }
+}