/* * 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; } } }