Overall Statistics |
Total Trades 1 Average Win 0% Average Loss 0% Compounding Annual Return -87.105% Drawdown 10.600% Expectancy 0 Net Profit -5.457% Sharpe Ratio -1.423 Probabilistic Sharpe Ratio 27.166% Loss Rate 0% Win Rate 0% Profit-Loss Ratio 0 Alpha 0 Beta 0 Annual Standard Deviation 0.594 Annual Variance 0.353 Information Ratio -1.423 Tracking Error 0.594 Treynor Ratio 0 Total Fees $19.38 Estimated Strategy Capacity $3100.00 Lowest Capacity Asset RRF R735QTJ8XC9X |
namespace QuantConnect { public class OneCancelsOtherTicketSet { public OneCancelsOtherTicketSet(params OrderTicket[] orderTickets) { this.OrderTickets = orderTickets; } private OrderTicket[] OrderTickets { get; set; } public void Filled() { // Cancel all the outstanding tickets. foreach (var orderTicket in this.OrderTickets) { if (orderTicket.Status == OrderStatus.Submitted) { orderTicket.Cancel(); } } } } }
/* //////////////////////////////////////////////// // Volume-Based Pullback Test // ----------------------------- // // Entry: // ------- // 30 mins into market open, open a Buy limit order // with price set to the bottom of the Value Area // // Exit: // ------ // Set Take profit at the POC price. // Set Stop Loss at equal distance below entry (POC price - entry price) // // // Notes / Credit: // ---------------- // Makes use of volume profiler from KCTrader // https://www.quantconnect.com/forum/discussion/10590/volume-profile-indicator-implementation-in-c // // Borrows OCO code from Levitikon: // https://www.quantconnect.com/forum/discussion/1700/support-for-one-cancels-the-other-order-oco/p1 // -------------------------------------------------------- */ using QuantConnect.Data; using QuantConnect.Indicators; namespace QuantConnect.Algorithm.CSharp { public class VolumeProfileAlgorithm : QCAlgorithm { private Symbol _symbol; private VolumeProfileIndicator _vp30; private OrderTicket EntryOrder { get; set; } // private Func<QCAlgorithm, string, decimal, OneCancelsOtherTicketSet> OnOrderFilledEvent { get; set; } // private OneCancelsOtherTicketSet ProfitLossOrders { get; set; } public override void Initialize() { SetStartDate(2021, 3, 31); //Set Start Date SetEndDate(2021, 4, 10); SetCash(10000); //Set Strategy Cash SetBrokerageModel(new Brokerages.InteractiveBrokersBrokerageModel()); _symbol = AddEquity("IHT", Resolution.Minute).Symbol; SetWarmup(30 * 390); _vp30 = new VolumeProfileIndicator(1 * 390, 30); // 1 day at minute resolution RegisterIndicator(_symbol, _vp30, Resolution.Minute); this.Schedule.On(this.DateRules.EveryDay(), this.TimeRules.AfterMarketOpen("IHT", 30), this.OpenPositionAtOpen); } public void OpenPositionAtOpen() { if (!Portfolio.Invested) { int quantity = (int)Math.Floor(Portfolio.Cash / CurrentSlice["IHT"].Close); var va = _vp30.ValueArea; // var entryPrice = 2.3963375m; var entryPrice = va.Item1; // Bottom of the value area this.EntryOrder = LimitOrder(_symbol, quantity, entryPrice, "Entry Limit Order"); } } } }
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; public DateTime lastRefresh = DateTime.MinValue; /// <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() { _bars.Reset(); _maxIndicator.Reset(); _minIndicator.Reset(); _periodsPassed = 0; _binSize = 0M; _profile = new decimal[0]; _upProfile = new decimal[0]; _downProfile = new decimal[0]; _pointOfControl = 0M; _lowerValueAreaBound = 0M; _upperValueAreaBound = 0M; lastRefresh = DateTime.MinValue; } /// <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; lastRefresh = input.Time; // 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(); } } }