Lean  $LEAN_TAG$
MarketProfile.cs
1 /*
2  * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3  * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14 */
15 
16 using System;
17 using System.Collections.Generic;
18 using System.Linq;
20 
22 {
23  /// <summary>
24  /// Represents an Indicator of the Market Profile and its attributes
25  ///
26  /// The concept of Market Profile stems from the idea that
27  /// markets have a form of organization determined by time,
28  /// price, and volume.Each day, the market will develop a range
29  /// for the day and a value area, which represents an equilibrium
30  /// point where there are an equal number of buyers and sellers.
31  /// In this area, prices never stay stagnant. They are constantly
32  /// diverging, and Market Profile records this activity for traders
33  /// to interpret.
34  ///
35  /// It can be computed in two modes: TPO (Time Price Opportunity) or VOL (Volume Profile)
36  /// A discussion on the difference between TPO (Time Price Opportunity)
37  /// and VOL (Volume Profile) chart types: https://jimdaltontrading.com/tpo-vs-volume-profile
38  /// </summary>
40  {
41  /// <summary>
42  /// Percentage of total volume contained in the ValueArea
43  /// </summary>
44  private readonly decimal _valueAreaVolumePercentage;
45 
46  /// <summary>
47  /// The range of roundoff to the prices. i.e two decimal places, three decimal places
48  /// </summary>
49  private readonly decimal _priceRangeRoundOff;
50 
51  /// <summary>
52  /// Rolling Window to erase old VolumePerPrice values out of the given period
53  /// First item is going to contain Data Point's close value
54  ///
55  /// Second item is going to contain the Volume, which can be 1 or
56  /// the Data Point's volume value
57  /// </summary>
58  private RollingWindow<Tuple<decimal, decimal>> _oldDataPoints { get; }
59 
60  /// <summary>
61  /// Close values and Volume values in the given period of time.
62  /// Close values are the keys and Volume values the values.
63  /// The list is sorted in ascending order of the keys
64  /// </summary>
65  private SortedList<decimal, decimal> _volumePerPrice;
66 
67  /// <summary>
68  /// A rolling sum of the Volume values for the given period
69  /// </summary>
70  private IndicatorBase<IndicatorDataPoint> _totalVolume { get; }
71 
72  /// <summary>
73  /// POC Index
74  /// </summary>
75  private int _pointOfControl;
76 
77  /// <summary>
78  /// Get a copy of the _volumePerPrice field
79  /// </summary>
80  public SortedList<decimal, decimal> VolumePerPrice => new SortedList<decimal, decimal>(_volumePerPrice);
81 
82  /// <summary>
83  /// The highest reached close price level during the period.
84  /// That value is called Profile High
85  /// </summary>
86  public decimal ProfileHigh { get; private set; }
87 
88  /// <summary>
89  /// The lowest reached close price level during the period.
90  /// That value is called Profile Low
91  /// </summary>
92  public decimal ProfileLow { get; private set; }
93 
94  /// <summary>
95  /// Price where the most trading occured (Point of Control(POC))
96  /// This price is MarketProfile.Current.Value
97  /// </summary>
98  public decimal POCPrice { get; private set; }
99 
100  /// <summary>
101  /// Volume where the most tradding occured (Point of Control(POC))
102  /// </summary>
103  public decimal POCVolume { get; private set; }
104 
105  /// <summary>
106  /// The range of price levels in which a specified percentage of all volume
107  /// was traded during the time period. Typically, this percentage is set
108  /// to 70% however it is up to the trader’s discretion.
109  /// </summary>
110  public decimal ValueAreaVolume { get; private set; }
111 
112  /// <summary>
113  /// The highest close price level within the value area
114  /// </summary>
115  public decimal ValueAreaHigh { get; private set; }
116 
117  /// <summary>
118  /// The lowest close price level within the value area
119  /// </summary>
120  public decimal ValueAreaLow { get; private set; }
121 
122  /// <summary>
123  /// Gets a flag indicating when the indicator is ready and fully initialized
124  /// </summary>
125  public override bool IsReady => _totalVolume.IsReady;
126 
127  /// <summary>
128  /// Required period, in data points, for the indicator to be ready and fully initialized.
129  /// </summary>
130  public int WarmUpPeriod { get; private set; }
131 
132  /// <summary>
133  /// Creates a new MarkProfile indicator with the specified period
134  /// </summary>
135  /// <param name="name">The name of this indicator</param>
136  /// <param name="period">The period of this indicator</param>
137  /// <param name="valueAreaVolumePercentage">The percentage of volume contained in the value area</param>
138  /// <param name="priceRangeRoundOff">How many digits you want to round and the precision.
139  /// i.e 0.01 round to two digits exactly. 0.05 by default.</param>
140  protected MarketProfile(string name, int period, decimal valueAreaVolumePercentage = 0.70m, decimal priceRangeRoundOff = 0.05m)
141  : base(name)
142  {
143  // Check roundoff is positive
144  if (priceRangeRoundOff <= 0)
145  {
146  throw new ArgumentException("Must be strictly bigger than zero.", nameof(priceRangeRoundOff));
147  }
148 
149  WarmUpPeriod = period;
150  _valueAreaVolumePercentage = valueAreaVolumePercentage;
151  _oldDataPoints = new RollingWindow<Tuple<decimal, decimal>>(period);
152  _volumePerPrice = new SortedList<decimal, decimal>();
153  _totalVolume = new Sum(name + "_Sum", period);
154  _priceRangeRoundOff = 1 / priceRangeRoundOff;
155  }
156 
157  /// <summary>
158  /// Computes the next value for this indicator from the given state.
159  /// </summary>
160  /// <param name="input">The input value to this indicator on this time step</param>
161  /// <returns>A a value for this indicator, Point of Control (POC) price</returns>
162  protected override decimal ComputeNextValue(TradeBar input)
163  {
164  // Define Volume and add it to _volumePerPrice and _oldDataPoints
165  var VolumeQuantity = GetVolume(input);
166  Add(input, VolumeQuantity);
167 
168  // Get the index of the close price with maximum volume
169  _pointOfControl = GetMax();
170 
171  var volumePerPriceCount = VolumePerPrice.Count;
172 
173  // Get the POC price and volume values
174  POCPrice = volumePerPriceCount != 0 ? VolumePerPrice.Keys[_pointOfControl] : 0;
175  POCVolume = volumePerPriceCount != 0 ? VolumePerPrice.Values[_pointOfControl] : 0;
176 
177  // Get the highest and lowest close prices
178  ProfileHigh = volumePerPriceCount != 0 ? VolumePerPrice.Keys.Max() : 0;
179  ProfileLow = volumePerPriceCount != 0 ? VolumePerPrice.Keys.Min() : 0;
180 
181  // Calculate the Value Area Volume and Value Area High and Low
182  CalculateValueArea();
183 
184  return POCPrice;
185  }
186 
187  /// <summary>
188  /// Get the Volume value that's going to be used
189  /// </summary>
190  /// <param name="input">Data</param>
191  /// <returns>The Volume value it's going to be used</returns>
192  protected abstract decimal GetVolume(TradeBar input);
193 
194  /// <summary>
195  /// Add the new input value to the Close array and Volume dictionary.
196  /// </summary>
197  /// <param name="input">The input value to this indicator on this time step</param>
198  /// <param name="VolumeQuantity">Volume quantity of the data point, it dependes of DefineVolume method.</param>
199  private void Add(TradeBar input, decimal VolumeQuantity)
200  {
201  // Check if the RollingWindow _oldDataPoints has been filled to its capacity
202  var isFilled = _oldDataPoints.IsReady;
203 
204  _oldDataPoints.Add(new Tuple<decimal, decimal>(input.Close, VolumeQuantity));
205 
206  var ClosePrice = Round(input.Close);
207  if (!_volumePerPrice.Keys.Contains(ClosePrice))
208  {
209  _volumePerPrice.Add(ClosePrice,VolumeQuantity);
210  }
211  else
212  {
213  _volumePerPrice[ClosePrice] += VolumeQuantity;
214  }
215 
216  _totalVolume.Update(input.Time, VolumeQuantity);
217 
218  // If isFilled is true it means that the capacity was full before we added a new data point
219  // so by this time the RollingWindow has already removed the first added data point, so we
220  // need to remove it from the sortedList _volumePerPrice
221  if (isFilled)
222  {
223  var RemovedDataPoint = _oldDataPoints.MostRecentlyRemoved;
224  ClosePrice = Round(RemovedDataPoint.Item1);
225  // Two equal points can be inserted in _oldDataPoints, where the volume of the second one is zero. Then
226  // when the first one is removed from _oldDataPoints, its value in _volumePerPrice is also removed as
227  // the remaining value is zero.
228  if (_volumePerPrice.ContainsKey(ClosePrice))
229  {
230  _volumePerPrice[ClosePrice] -= RemovedDataPoint.Item2;
231  if (_volumePerPrice[ClosePrice] == 0)
232  {
233  _volumePerPrice.Remove(ClosePrice);
234  }
235  }
236  }
237  }
238 
239  /// <summary>
240  /// Finds the close price with biggest volume value.
241  /// </summary>
242  /// <returns> Index of the close price with biggest volume value</returns>
243  private int GetMax()
244  {
245  var maxIdx = 0;
246  for (int index = 0; index < VolumePerPrice.Values.Count; index++)
247  {
248  if (VolumePerPrice.Values[index] > VolumePerPrice.Values[maxIdx])
249  {
250  maxIdx = index;
251  }
252  else if(VolumePerPrice.Values[index] == VolumePerPrice.Values[maxIdx])
253  {
254  // Find the maximum with minimum distance to the center
255  var mid = VolumePerPrice.Count - 1;
256  if(Math.Abs(mid/2 - index)<Math.Abs(mid/2 - maxIdx))
257  {
258  maxIdx = index;
259  }
260  }
261  }
262  return maxIdx;
263  }
264 
265  /// <summary>
266  /// Calculate the Value Area Volume and the highest and lowest prices within it (VAH and VAL).
267  /// </summary>
268  private void CalculateValueArea()
269  {
270  // First ValueArea estimation
271  ValueAreaVolume = _totalVolume.Current.Value * _valueAreaVolumePercentage;
272 
273  var currentVolume = POCVolume;
274 
275  var minIndex = _pointOfControl;
276  var maxIndex = _pointOfControl;
277 
278  int lastMin, lastMax;
279  int nextMinIndex, nextMaxIndex;
280 
281  decimal lowVolume, highVolume;
282 
283  // When this loop ends we will have a more accurate value of ValueAreaVolume
284  // but mainly the prices that delimite this area, ValueAreaLow and ValueAreaHigh
285  // so ValueArea, can also be seen as the range between ValueAreaLow and ValueAreaHigh
286  while (currentVolume <= ValueAreaVolume && ValueAreaVolume != 0)
287  {
288  lastMin = minIndex;
289  lastMax = maxIndex;
290 
291  nextMinIndex = Math.Max(minIndex - 1, 0);
292  nextMaxIndex = Math.Min(maxIndex + 1, VolumePerPrice.Count - 1);
293 
294  if (nextMinIndex != lastMin)
295  {
296  lowVolume = VolumePerPrice.Values[nextMinIndex];
297  }
298  else
299  {
300  lowVolume = 0;
301  }
302 
303  if (nextMaxIndex != lastMax)
304  {
305  highVolume = VolumePerPrice.Values[nextMaxIndex];
306  }
307  else
308  {
309  highVolume = 0;
310  }
311 
312  // Take the largest volume value between the above and below prices
313  // of the Point of Control (the initial maxIndex and minIndex respectively)
314 
315  if ((highVolume == 0) || ((lowVolume != 0) && (lowVolume > highVolume)))
316  {
317  currentVolume += lowVolume;
318  minIndex = nextMinIndex;
319  }
320  else if ((lowVolume == 0) || ((highVolume != 0) && (highVolume >= lowVolume)))
321  {
322  currentVolume += highVolume;
323  maxIndex = nextMaxIndex;
324  }
325  else
326  {
327  break;
328  }
329 
330  // We expand this range between minIndex and maxIndex until the sum of all volume values between
331  // them is bigger than the initial ValueAreaVolume value
332  }
333  ValueAreaHigh = VolumePerPrice.Count != 0 ? VolumePerPrice.Keys[maxIndex] : 0;
334  ValueAreaLow = VolumePerPrice.Count != 0 ? VolumePerPrice.Keys[minIndex] : 0;
335  }
336 
337  /// <summary>
338  /// Round the decimal number
339  /// </summary>
340  /// <param name="a">The decimal number to round</param>
341  /// <returns>The rounded decimal number</returns>
342  private decimal Round(decimal a)
343  {
344  return Math.Ceiling(a * _priceRangeRoundOff) / _priceRangeRoundOff;
345  }
346 
347  /// <summary>
348  /// Resets this indicator to its initial state
349  /// </summary>
350  public override void Reset()
351  {
352  _oldDataPoints.Reset();
353  _volumePerPrice.Clear();
354  _totalVolume.Reset();
355  base.Reset();
356  }
357  }
358 }