/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using QuantConnect.Data.Market;
using System;
namespace QuantConnect.Indicators
{
///
/// The Klinger Volume Oscillator (KVO) is a technical indicator that analyzes the relationship between
/// price movement and trading volume to assess the strength of market trends and identify potential
/// trend reversals. As a volume-based oscillator, it measures the force behind price movements by
/// incorporating volume data adjusted for price trends and specific conditions. Traders use the KVO
/// to analyze its behavior relative to price action, looking for patterns such as divergences or
/// crossovers that can provide insights into market trends and potential turning points.
///
public class KlingerVolumeOscillator : TradeBarIndicator, IIndicatorWarmUpPeriodProvider
{
private readonly ExponentialMovingAverage _fastEma;
private readonly ExponentialMovingAverage _slowEma;
private readonly RollingWindow _priceIndex;
private readonly RollingWindow _rangeWindow;
private readonly RollingWindow _trendWindow;
private decimal _cumulativeMovement;
// Minimum cumulative movement value to avoid division by zero and near zero in volume force calculation
private const decimal MinCumulativeForDivision = 1e-8m;
///
/// Gets the public signal line (EMA of KVO)
///
public ExponentialMovingAverage Signal { get; }
///
/// Gets the warm-up period required for the indicator to be ready.
///
public int WarmUpPeriod { get; }
///
/// Gets a value indicating whether the indicator is ready and has enough data.
///
public override bool IsReady => _fastEma.IsReady && _slowEma.IsReady && Signal.IsReady;
///
/// Initializes a new instance of the class with specified fast, slow periods.
///
/// The name of the indicator.
/// The fast EMA period.
/// The slow EMA period.
/// The signal line period.
public KlingerVolumeOscillator(string name, int fastPeriod, int slowPeriod, int signalPeriod)
: base(name)
{
_fastEma = new ExponentialMovingAverage(name + "_FastEma", fastPeriod);
_slowEma = new ExponentialMovingAverage(name + "_SlowEma", slowPeriod);
Signal = new ExponentialMovingAverage(name + "_Signal", signalPeriod);
_priceIndex = new RollingWindow(2);
_rangeWindow = new RollingWindow(2);
_trendWindow = new RollingWindow(2);
WarmUpPeriod = Math.Max(fastPeriod, Math.Max(slowPeriod, signalPeriod)) + 2;
}
///
/// Initializes a new instance of the class with specified fast, slow periods.
///
/// The fast EMA period.
/// The slow EMA period.
/// The signal line period (default is 13).
public KlingerVolumeOscillator(int fastPeriod, int slowPeriod, int signalPeriod = 13)
: this($"KVO({fastPeriod},{slowPeriod},{signalPeriod})", fastPeriod, slowPeriod, signalPeriod)
{
}
///
/// Computes the next value of the Klinger Volume Oscillator based on the input data point.
///
protected override decimal ComputeNextValue(TradeBar bar)
{
// daily movement range
var todaysMovement = bar.High - bar.Low;
_rangeWindow.Add(todaysMovement);
// price index value, used to compare current and previous price trends
var hlc = bar.High + bar.Low + bar.Close;
_priceIndex.Add(hlc);
if (!_priceIndex.IsReady)
{
// Not enough data
return 0m;
}
// determine if the price trend is going up or down, 1 for up, -1 for down
var currentTrend = _priceIndex[0] > _priceIndex[1] ? 1 : -1;
_trendWindow.Add(currentTrend);
if (!_trendWindow.IsReady)
{
// Not enough data
return 0m;
}
// Data is ready to calculate KVO
var hasMovement = _cumulativeMovement != 0;
var trendChanged = _trendWindow[0] != _trendWindow[1];
var yesterdaysRange = _rangeWindow[1];
if (!hasMovement || trendChanged)
{
// Start new flow accumulation with the previous daily movement
_cumulativeMovement = todaysMovement + yesterdaysRange;
}
else
{
// Continue flow accumulation in the same trend direction
_cumulativeMovement += todaysMovement;
}
// Volume force: strength of volume flow in the direction of the trend.
// There are various definitions of volume force, this is what we used in our implementation:
// https://github.com/nardew/talipp/blob/70dc9a26889c9c9329e44321e1362c4db43dbcc3/talipp/indicators/KVO.py#L85
// https://www.tradingview.com/support/solutions/43000589157-klinger-oscillator/
// Protect from division by zero and near zero from blowing up the volume force calculation
var denom = Math.Abs(_cumulativeMovement) < MinCumulativeForDivision ? MinCumulativeForDivision : _cumulativeMovement;
var volumeForce = bar.Volume * Math.Abs(2m * (todaysMovement / denom - 1m)) * currentTrend * 100m;
// update moving averages
var dataPoint = new IndicatorDataPoint(bar.EndTime, volumeForce);
_fastEma.Update(dataPoint);
_slowEma.Update(dataPoint);
if (!_fastEma.IsReady || !_slowEma.IsReady)
{
Signal.Update(new IndicatorDataPoint(bar.EndTime, 0m));
return 0m;
}
// Calculate KVO value as the difference between fast and slow EMAs
var kvo = _fastEma.Current.Value - _slowEma.Current.Value;
Signal.Update(new IndicatorDataPoint(bar.EndTime, kvo));
return kvo;
}
///
/// Resets the indicator to its initial state.
///
public override void Reset()
{
base.Reset();
Signal.Reset();
_fastEma.Reset();
_slowEma.Reset();
_priceIndex.Reset();
_rangeWindow.Reset();
_trendWindow.Reset();
_cumulativeMovement = 0m;
}
}
}