| Overall Statistics |
|
Total Trades 0 Average Win 0% Average Loss 0% Compounding Annual Return 0% Drawdown 0% Expectancy 0 Net Profit 0% Sharpe Ratio 0 Probabilistic Sharpe Ratio 0% Loss Rate 0% Win Rate 0% Profit-Loss Ratio 0 Alpha 0 Beta 0 Annual Standard Deviation 0 Annual Variance 0 Information Ratio 0 Tracking Error 0 Treynor Ratio 0 Total Fees $0.00 |
using QuantConnect.Data;
using QuantConnect.Indicators;
namespace QuantConnect.Algorithm.CSharp
{
public class VolumeProfileAlgorithm : QCAlgorithm
{
private Symbol _aapl;
private VolumeProfileIndicator _vp30;
public override void Initialize()
{
SetStartDate(2021, 1, 1); //Set Start Date
SetEndDate(2021, 1, 30);
SetCash(10000); //Set Strategy Cash
SetBrokerageModel(new Brokerages.InteractiveBrokersBrokerageModel());
_aapl = AddEquity("AAPL", Resolution.Minute).Symbol;
SetWarmup(30 * 390);
var symbols = new Symbol[] { _aapl };
foreach (var symbol in symbols)
{
var security = Securities[symbol];
}
// 30 days at minute resolution. Recalculate every 30 minutes.
_vp30 = new VolumeProfileIndicator(30 * 390, 30);
RegisterIndicator(_aapl, _vp30, Resolution.Minute);
}
public override void OnData(Slice slice)
{
if (!IsWarmingUp && Time.Hour == 10 && Time.Minute == 0)
{
var va = _vp30.ValueArea;
var rv = _vp30.RelativeVolume();
var pos = _vp30.UpDownPercent();
var prob = _vp30.GetProbability();
Debug($"DATE: {Time.ToShortDateString()} POC: {_vp30:C2} VA: {va.Item1:C2} - {va.Item2:C2} RV: {rv:F2} POS:{pos:F2} PROB:{prob:F2}");
}
}
}
}using System;
using System.Linq;
using QuantConnect.Data.Market;
using Accord.Statistics.Distributions.DensityKernels;
using Accord.Statistics.Distributions.Multivariate;
using Accord.Statistics.Distributions.Univariate;
namespace QuantConnect.Indicators
{
/// <summary>
/// Produces a Volume Profile and returns the Point of Control.
/// </summary>
public class VolumeProfileIndicator : TradeBarIndicator, IIndicatorWarmUpPeriodProvider
{
private int _period = 0;
private int _recalculatePeriod = 0;
private int _periodsPassed = 0;
private RollingWindow<TradeBar> _bars;
private Maximum _maxIndicator;
private Minimum _minIndicator;
private decimal _min = 0m;
private decimal _max = 0m;
private int _numBins = 24;
private decimal _binSize = 0M;
private decimal[] _profile;
private decimal[] _upProfile;
private decimal[] _downProfile;
private decimal _pointOfControl = 0M;
private decimal _lowerValueAreaBound = 0M;
private decimal _upperValueAreaBound = 0M;
/// <summary>
/// Volume Profile Indicator
/// Implemented based on TradingView implementation
/// https://www.tradingview.com/support/solutions/43000480324-how-is-volume-profile-calculated/
/// </summary>
/// <param name="name">string - a name for the indicator</param>
/// <param name="period">int - the number of periods to calculate the VP</param>
/// <param name="recalculateAfterNPeriods">int - the number of periods to wait before recalculating.
/// If using daily or hourly resolution, set this to 1. If using minute, set it to 15-60 or performance will be slow. </param>
public VolumeProfileIndicator(string name, int period, int recalculateAfterNPeriods)
: base(name)
{
if (period < 2) throw new ArgumentException("The Volume Profile period should be greater or equal to 2", "period");
_period = period;
_recalculatePeriod = recalculateAfterNPeriods;
WarmUpPeriod = period;
_bars = new RollingWindow<TradeBar>(_period);
_maxIndicator = new Maximum(_period);
_minIndicator = new Minimum(_period);
}
/// <summary>
/// A Volume Profile Indicator
/// </summary>
/// <param name="period">int - the number of periods over which to calculate the VP</param>
/// /// <param name="recalculateAfterNPeriods">int - the number of periods to wait before recalculating.
/// If using daily or hourly resolution, set this to 1. If using minute, set it to 15-60 or performance will be slow. </param>
public VolumeProfileIndicator(int period, int recalculateAfterNPeriods)
: this($"VP({period})", period, recalculateAfterNPeriods)
{
}
/// <summary>
/// Resets this indicator to its initial state
/// </summary>
public override void Reset()
{
throw new Exception("Reset is unsupported for VolumeProfileIndicator.");
}
/// <summary>
/// Gets a flag indicating when this indicator is ready and fully initialized
/// </summary>
public override bool IsReady => _bars.IsReady;
/// <summary>
/// Required period, in data points, for the indicator to be ready and fully initialized.
/// </summary>
public int WarmUpPeriod { get; }
/// <summary>
/// Computes the next value of this indicator from the given state
/// </summary>
/// <param name="input">The input given to the indicator</param>
/// <returns>
/// A new value for this indicator
/// </returns>
protected override decimal ComputeNextValue(TradeBar input)
{
_bars.Add(input);
_periodsPassed++;
_maxIndicator.Update(input.Time, input.Close);
_minIndicator.Update(input.Time, input.Close);
if (!IsReady)
return 0m;
if (_periodsPassed < _recalculatePeriod)
return _pointOfControl;
// Set the max and min for the profile
_min = _minIndicator;
_max = _maxIndicator;
// Calculate size of each histogram bin
_binSize = (_max - _min) / _numBins;
_profile = new decimal[_numBins];
_upProfile = new decimal[_numBins];
_downProfile = new decimal[_numBins];
int binIndex = 0;
foreach (var bar in _bars)
{
// Determine histogram bin for this bar
binIndex = GetBinIndex(bar.Close);
// Add volume to the bin
_profile[binIndex] += bar.Volume;
// Add volume to the up or down profile
if (bar.Close >= bar.Open)
_upProfile[binIndex] += bar.Volume;
else
_downProfile[binIndex] += bar.Volume;
}
// Calculate Point of Control
int pocIndex = 0;
decimal maxVolume = 0;
for (int i = 0; i < _numBins; i++)
{
if (_profile[i] > maxVolume)
{
pocIndex = i;
maxVolume = _profile[i];
}
}
// POC will be midpoint of the bin
_pointOfControl = _min + (_binSize * pocIndex) + _binSize / 2;
CalculateValueArea(pocIndex);
_periodsPassed = 0;
return _pointOfControl;
}
private void CalculateValueArea(int pocIndex)
{
// Calculate Value Area
// https://www.oreilly.com/library/view/mind-over-markets/9781118659762/b01.html
int upperIdx = pocIndex;
int lowerIdx = pocIndex;
decimal totalVolume = _profile.Sum();
//
decimal valueAreaVolume = _profile[pocIndex];
decimal percentVolume = 0m;
while (percentVolume < .7m)
{
var lowerVolume = 0m;
var nextLowerIdx = lowerIdx;
var upperVolume = 0m;
var nextUpperIdx = upperIdx;
// Total the volume of the next two price bins above and below the value area.
if (lowerIdx >= 2)
{
lowerVolume = _profile[lowerIdx - 1] + _profile[lowerIdx - 2];
nextLowerIdx = lowerIdx - 2;
}
else if (lowerIdx == 1)
{
lowerVolume = _profile[lowerIdx - 1];
nextLowerIdx = lowerIdx - 1;
}
if (upperIdx <= _numBins - 3)
{
upperVolume = _profile[upperIdx + 1] + _profile[upperIdx + 2];
nextUpperIdx = upperIdx + 2;
}
else if (upperIdx == _numBins - 2)
{
upperVolume = _profile[upperIdx + 1];
nextUpperIdx = upperIdx + 1;
}
// Compare volume of the next upper and lower area. Add the higher to the value areas
if (upperVolume >= lowerVolume && upperIdx != _numBins - 1)
{
valueAreaVolume += upperVolume;
upperIdx = nextUpperIdx;
}
else
{
valueAreaVolume += lowerVolume;
lowerIdx = nextLowerIdx;
}
percentVolume = valueAreaVolume / totalVolume;
}
_lowerValueAreaBound = _min + lowerIdx * _binSize;
_upperValueAreaBound = _min + (upperIdx + 1) * _binSize;
}
/// <summary>
/// Returns a tuple where the first element is the lower bound of the value area and the second is the upper bound.
/// </summary>
/// <returns></returns>
public Tuple<decimal, decimal> ValueArea
{
get
{
return new Tuple<decimal, decimal>(_lowerValueAreaBound, _upperValueAreaBound);
}
}
/// <summary>
/// Returns the relative volume of a price area as a percentage of total volume in the profile
/// </summary>
/// <param name="price"></param>
/// <returns></returns>
public decimal RelativeVolume(decimal price)
{
if (!IsReady)
return 0M;
decimal totalVolume = _profile.Sum();
int binIndex = GetBinIndex(price);
return _profile[binIndex] / totalVolume;
}
/// <summary>
/// Returns the relative volume of current price area as a percentage of total volume in the profile
/// </summary>
/// <param name="price"></param>
/// <returns></returns>
public decimal RelativeVolume()
{
return RelativeVolume(_bars[0].Close);
}
/// <summary>
/// Returns the ratio of Up vs Down candles in the given price area. Over .5 means more Up candles.
/// </summary>
/// <param name="price"></param>
/// <returns></returns>
public decimal UpDownPercent(decimal price)
{
if (!IsReady)
return 0M;
int binIndex = GetBinIndex(price);
if (_profile[binIndex] == 0)
return 0;
return _upProfile[binIndex] / _profile[binIndex];
}
/// <summary>
/// Returns the ratio of Up vs Down candles in the current price area. Over .5 means more Up candles.
/// </summary>
/// <param name="price"></param>
/// <returns></returns>
public decimal UpDownPercent()
{
return UpDownPercent(_bars[0].Close);
}
/// <summary>
/// Get the histogram bin index for a given price area.
/// </summary>
/// <param name="price"></param>
/// <returns></returns>
private int GetBinIndex(decimal price)
{
int binIndex = (int)Math.Truncate((price - _min) / _binSize);
// In the case where the price is the maximum, the formula will push the index out of bounds. We return the max bin index in that case.
// There are also cases when we don't recalculate the profile that a price could be requested outside the profile range. Normalize those to the min or max
if (binIndex >= _numBins)
return _numBins -1;
else if (binIndex < 0)
return 0;
return binIndex;
}
/// <summary>
/// Creates a probability distribution based on price and volume. Returns the probability a price is in the current price range.
/// This number will be less than 1 and a higher number interpreted as more volume in the price range.
/// The advantage of this over using a discrete bin is we get a continuous curve rather than discrete bins.
/// </summary>
/// <returns></returns>
public decimal GetProbability()
{
double[] samples = new double[_period];
double[] weights = new double[_period];
for (int i = 0; i < _period; i++)
{
samples[i] = (double)_bars[i].Close;
weights[i] = (double)_bars[i].Volume;
}
// Create a multivariate Empirical distribution from the samples
var dist = new EmpiricalDistribution(samples, weights);
// pdf returns a weighted value (volume)
double pdf = dist.ProbabilityDensityFunction((double)_bars[0].Close);
// divide the weighted value by total volume for a probability.
return (decimal)pdf / _profile.Sum();
}
}
}