Add CVI - Chaikin's Volatility class and tests

Add implementation of CVI - Chaikin's Volatility class and related tests.

* **Cvi Class Implementation:**
  - Add `Cvi` class in `lib/volatility/Cvi.cs` to calculate Chaikin's Volatility.
  - Use high and low prices for calculation.
  - Include a constructor with `period` parameter.
  - Add a method to calculate Chaikin's Volatility.

* **Quantower Indicator:**
  - Add `CviIndicator` class in `quantower/Volatility/CviIndicator.cs`.
  - Use `Cvi` class for calculation.
  - Add input parameters for `period` and `showColdValues`.
  - Implement `OnInit` and `OnUpdate` methods.

* **Tests:**
  - Add a test method for `Cvi` class in `Tests/test_updates_volatility.cs`.
  - Use random updates to test `Cvi`.
  - Ensure initial and final values are equal.

---

For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/mihakralj/QuanTAlib?shareId=XXXX-XXXX-XXXX-XXXX).
This commit is contained in:
Miha Kralj
2024-10-31 11:19:40 -07:00
parent 784f3e0956
commit f55f630ac6
3 changed files with 179 additions and 0 deletions
+16
View File
@@ -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);
}
}
+109
View File
@@ -0,0 +1,109 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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
/// </remarks>
[SkipLocalsInit]
public sealed class Cvi : AbstractBase
{
private readonly int _period;
private readonly Ema _ema;
private readonly CircularBuffer _buffer;
private double _prevEma;
/// <param name="period">The number of periods for CVI calculation.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when period is less than 1.</exception>
[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})";
}
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The number of periods for CVI calculation.</param>
[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;
}
}
+54
View File
@@ -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);
}
}