/*
* 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 System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Data.Market;
namespace QuantConnect.Indicators
{
///
/// Represents an Indicator of the Market Profile and its attributes
///
/// The concept of Market Profile stems from the idea that
/// markets have a form of organization determined by time,
/// price, and volume.Each day, the market will develop a range
/// for the day and a value area, which represents an equilibrium
/// point where there are an equal number of buyers and sellers.
/// In this area, prices never stay stagnant. They are constantly
/// diverging, and Market Profile records this activity for traders
/// to interpret.
///
/// It can be computed in two modes: TPO (Time Price Opportunity) or VOL (Volume Profile)
/// A discussion on the difference between TPO (Time Price Opportunity)
/// and VOL (Volume Profile) chart types: https://jimdaltontrading.com/tpo-vs-volume-profile
///
public abstract class MarketProfile : TradeBarIndicator, IIndicatorWarmUpPeriodProvider
{
///
/// Percentage of total volume contained in the ValueArea
///
private readonly decimal _valueAreaVolumePercentage;
///
/// The range of roundoff to the prices. i.e two decimal places, three decimal places
///
private readonly decimal _priceRangeRoundOff;
///
/// Rolling Window to erase old VolumePerPrice values out of the given period
/// First item is going to contain Data Point's close value
///
/// Second item is going to contain the Volume, which can be 1 or
/// the Data Point's volume value
///
private RollingWindow> _oldDataPoints { get; }
///
/// Close values and Volume values in the given period of time.
/// Close values are the keys and Volume values the values.
/// The list is sorted in ascending order of the keys
///
private SortedList _volumePerPrice;
///
/// A rolling sum of the Volume values for the given period
///
private IndicatorBase _totalVolume { get; }
///
/// POC Index
///
private int _pointOfControl;
///
/// Get a copy of the _volumePerPrice field
///
public SortedList VolumePerPrice => new SortedList(_volumePerPrice);
///
/// The highest reached close price level during the period.
/// That value is called Profile High
///
public decimal ProfileHigh { get; private set; }
///
/// The lowest reached close price level during the period.
/// That value is called Profile Low
///
public decimal ProfileLow { get; private set; }
///
/// Price where the most trading occured (Point of Control(POC))
/// This price is MarketProfile.Current.Value
///
public decimal POCPrice { get; private set; }
///
/// Volume where the most tradding occured (Point of Control(POC))
///
public decimal POCVolume { get; private set; }
///
/// The range of price levels in which a specified percentage of all volume
/// was traded during the time period. Typically, this percentage is set
/// to 70% however it is up to the trader’s discretion.
///
public decimal ValueAreaVolume { get; private set; }
///
/// The highest close price level within the value area
///
public decimal ValueAreaHigh { get; private set; }
///
/// The lowest close price level within the value area
///
public decimal ValueAreaLow { get; private set; }
///
/// Gets a flag indicating when the indicator is ready and fully initialized
///
public override bool IsReady => _totalVolume.IsReady;
///
/// Required period, in data points, for the indicator to be ready and fully initialized.
///
public int WarmUpPeriod { get; private set; }
///
/// Creates a new MarkProfile indicator with the specified period
///
/// The name of this indicator
/// The period of this indicator
/// The percentage of volume contained in the value area
/// How many digits you want to round and the precision.
/// i.e 0.01 round to two digits exactly. 0.05 by default.
protected MarketProfile(string name, int period, decimal valueAreaVolumePercentage = 0.70m, decimal priceRangeRoundOff = 0.05m)
: base(name)
{
// Check roundoff is positive
if (priceRangeRoundOff <= 0)
{
throw new ArgumentException("Must be strictly bigger than zero.", nameof(priceRangeRoundOff));
}
WarmUpPeriod = period;
_valueAreaVolumePercentage = valueAreaVolumePercentage;
_oldDataPoints = new RollingWindow>(period);
_volumePerPrice = new SortedList();
_totalVolume = new Sum(name + "_Sum", period);
_priceRangeRoundOff = 1 / priceRangeRoundOff;
}
///
/// Computes the next value for this indicator from the given state.
///
/// The input value to this indicator on this time step
/// A a value for this indicator, Point of Control (POC) price
protected override decimal ComputeNextValue(TradeBar input)
{
// Define Volume and add it to _volumePerPrice and _oldDataPoints
var VolumeQuantity = GetVolume(input);
Add(input, VolumeQuantity);
// Get the index of the close price with maximum volume
_pointOfControl = GetMax();
var volumePerPriceCount = VolumePerPrice.Count;
// Get the POC price and volume values
POCPrice = volumePerPriceCount != 0 ? VolumePerPrice.Keys[_pointOfControl] : 0;
POCVolume = volumePerPriceCount != 0 ? VolumePerPrice.Values[_pointOfControl] : 0;
// Get the highest and lowest close prices
ProfileHigh = volumePerPriceCount != 0 ? VolumePerPrice.Keys.Max() : 0;
ProfileLow = volumePerPriceCount != 0 ? VolumePerPrice.Keys.Min() : 0;
// Calculate the Value Area Volume and Value Area High and Low
CalculateValueArea();
return POCPrice;
}
///
/// Get the Volume value that's going to be used
///
/// Data
/// The Volume value it's going to be used
protected abstract decimal GetVolume(TradeBar input);
///
/// Add the new input value to the Close array and Volume dictionary.
///
/// The input value to this indicator on this time step
/// Volume quantity of the data point, it dependes of DefineVolume method.
private void Add(TradeBar input, decimal VolumeQuantity)
{
// Check if the RollingWindow _oldDataPoints has been filled to its capacity
var isFilled = _oldDataPoints.IsReady;
_oldDataPoints.Add(new Tuple(input.Close, VolumeQuantity));
var ClosePrice = Round(input.Close);
if (!_volumePerPrice.Keys.Contains(ClosePrice))
{
_volumePerPrice.Add(ClosePrice,VolumeQuantity);
}
else
{
_volumePerPrice[ClosePrice] += VolumeQuantity;
}
_totalVolume.Update(input.Time, VolumeQuantity);
// If isFilled is true it means that the capacity was full before we added a new data point
// so by this time the RollingWindow has already removed the first added data point, so we
// need to remove it from the sortedList _volumePerPrice
if (isFilled)
{
var RemovedDataPoint = _oldDataPoints.MostRecentlyRemoved;
ClosePrice = Round(RemovedDataPoint.Item1);
// Two equal points can be inserted in _oldDataPoints, where the volume of the second one is zero. Then
// when the first one is removed from _oldDataPoints, its value in _volumePerPrice is also removed as
// the remaining value is zero.
if (_volumePerPrice.ContainsKey(ClosePrice))
{
_volumePerPrice[ClosePrice] -= RemovedDataPoint.Item2;
if (_volumePerPrice[ClosePrice] == 0)
{
_volumePerPrice.Remove(ClosePrice);
}
}
}
}
///
/// Finds the close price with biggest volume value.
///
/// Index of the close price with biggest volume value
private int GetMax()
{
var maxIdx = 0;
for (int index = 0; index < VolumePerPrice.Values.Count; index++)
{
if (VolumePerPrice.Values[index] > VolumePerPrice.Values[maxIdx])
{
maxIdx = index;
}
else if(VolumePerPrice.Values[index] == VolumePerPrice.Values[maxIdx])
{
// Find the maximum with minimum distance to the center
var mid = VolumePerPrice.Count - 1;
if(Math.Abs(mid/2 - index)
/// Calculate the Value Area Volume and the highest and lowest prices within it (VAH and VAL).
///
private void CalculateValueArea()
{
// First ValueArea estimation
ValueAreaVolume = _totalVolume.Current.Value * _valueAreaVolumePercentage;
var currentVolume = POCVolume;
var minIndex = _pointOfControl;
var maxIndex = _pointOfControl;
int lastMin, lastMax;
int nextMinIndex, nextMaxIndex;
decimal lowVolume, highVolume;
// When this loop ends we will have a more accurate value of ValueAreaVolume
// but mainly the prices that delimite this area, ValueAreaLow and ValueAreaHigh
// so ValueArea, can also be seen as the range between ValueAreaLow and ValueAreaHigh
while (currentVolume <= ValueAreaVolume && ValueAreaVolume != 0)
{
lastMin = minIndex;
lastMax = maxIndex;
nextMinIndex = Math.Max(minIndex - 1, 0);
nextMaxIndex = Math.Min(maxIndex + 1, VolumePerPrice.Count - 1);
if (nextMinIndex != lastMin)
{
lowVolume = VolumePerPrice.Values[nextMinIndex];
}
else
{
lowVolume = 0;
}
if (nextMaxIndex != lastMax)
{
highVolume = VolumePerPrice.Values[nextMaxIndex];
}
else
{
highVolume = 0;
}
// Take the largest volume value between the above and below prices
// of the Point of Control (the initial maxIndex and minIndex respectively)
if ((highVolume == 0) || ((lowVolume != 0) && (lowVolume > highVolume)))
{
currentVolume += lowVolume;
minIndex = nextMinIndex;
}
else if ((lowVolume == 0) || ((highVolume != 0) && (highVolume >= lowVolume)))
{
currentVolume += highVolume;
maxIndex = nextMaxIndex;
}
else
{
break;
}
// We expand this range between minIndex and maxIndex until the sum of all volume values between
// them is bigger than the initial ValueAreaVolume value
}
ValueAreaHigh = VolumePerPrice.Count != 0 ? VolumePerPrice.Keys[maxIndex] : 0;
ValueAreaLow = VolumePerPrice.Count != 0 ? VolumePerPrice.Keys[minIndex] : 0;
}
///
/// Round the decimal number
///
/// The decimal number to round
/// The rounded decimal number
private decimal Round(decimal a)
{
return Math.Ceiling(a * _priceRangeRoundOff) / _priceRangeRoundOff;
}
///
/// Resets this indicator to its initial state
///
public override void Reset()
{
_oldDataPoints.Reset();
_volumePerPrice.Clear();
_totalVolume.Reset();
base.Reset();
}
}
}