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();
        }
    }
}