forked from QuantConnect/Lean
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathMarketProfile.cs
349 lines (304 loc) · 13.7 KB
/
MarketProfile.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
/*
* 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
{
/// <summary>
/// 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
/// </summary>
public abstract class MarketProfile : TradeBarIndicator, IIndicatorWarmUpPeriodProvider
{
/// <summary>
/// Percentage of total volume contained in the ValueArea
/// </summary>
private readonly decimal _valueAreaVolumePercentage;
/// <summary>
/// The range of roundoff to the prices. i.e two decimal places, three decimal places
/// </summary>
private readonly decimal _priceRangeRoundOff;
/// <summary>
/// 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
/// </summary>
private RollingWindow<Tuple<decimal, decimal>> _oldDataPoints { get; }
/// <summary>
/// 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
/// </summary>
private SortedList<decimal, decimal> _volumePerPrice;
/// <summary>
/// A rolling sum of the Volume values for the given period
/// </summary>
private IndicatorBase<IndicatorDataPoint> _totalVolume { get; }
/// <summary>
/// POC Index
/// </summary>
private int _pointOfControl;
/// <summary>
/// Get a copy of the _volumePerPrice field
/// </summary>
public SortedList<decimal, decimal> VolumePerPrice => new SortedList<decimal, decimal>(_volumePerPrice);
/// <summary>
/// The highest reached close price level during the period.
/// That value is called Profile High
/// </summary>
public decimal ProfileHigh { get; private set; }
/// <summary>
/// The lowest reached close price level during the period.
/// That value is called Profile Low
/// </summary>
public decimal ProfileLow { get; private set; }
/// <summary>
/// Price where the most trading occured (Point of Control(POC))
/// This price is MarketProfile.Current.Value
/// </summary>
public decimal POCPrice { get; private set; }
/// <summary>
/// Volume where the most tradding occured (Point of Control(POC))
/// </summary>
public decimal POCVolume { get; private set; }
/// <summary>
/// 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.
/// </summary>
public decimal ValueAreaVolume { get; private set; }
/// <summary>
/// The highest close price level within the value area
/// </summary>
public decimal ValueAreaHigh { get; private set; }
/// <summary>
/// The lowest close price level within the value area
/// </summary>
public decimal ValueAreaLow { get; private set; }
/// <summary>
/// Gets a flag indicating when the indicator is ready and fully initialized
/// </summary>
public override bool IsReady => _totalVolume.IsReady;
/// <summary>
/// Required period, in data points, for the indicator to be ready and fully initialized.
/// </summary>
public int WarmUpPeriod { get; private set; }
/// <summary>
/// Creates a new MarkProfile indicator with the specified period
/// </summary>
/// <param name="name">The name of this indicator</param>
/// <param name="period">The period of this indicator</param>
/// <param name="valueAreaVolumePercentage">The percentage of volume contained in the value area</param>
/// <param name="priceRangeRoundOff">How many digits you want to round and the precision.
/// i.e 0.01 round to two digits exactly. 0.05 by default.</param>
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<Tuple<decimal, decimal>>(period);
_volumePerPrice = new SortedList<decimal, decimal>();
_totalVolume = new Sum(name + "_Sum", period);
_priceRangeRoundOff = 1 / priceRangeRoundOff;
}
/// <summary>
/// Computes the next value for this indicator from the given state.
/// </summary>
/// <param name="input">The input value to this indicator on this time step</param>
/// <returns>A a value for this indicator, Point of Control (POC) price</returns>
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();
// Get the POC price and volume values
POCPrice = VolumePerPrice.Keys[_pointOfControl];
POCVolume = VolumePerPrice.Values[_pointOfControl];
// Get the highest and lowest close prices
ProfileHigh = VolumePerPrice.Keys.Max();
ProfileLow = VolumePerPrice.Keys.Min();
// Calculate the Value Area Volume and Value Area High and Low
CalculateValueArea();
return POCPrice;
}
/// <summary>
/// Get the Volume value that's going to be used
/// </summary>
/// <param name="input">Data</param>
/// <returns>The Volume value it's going to be used</returns>
protected abstract decimal GetVolume(TradeBar input);
/// <summary>
/// Add the new input value to the Close array and Volume dictionary.
/// </summary>
/// <param name="input">The input value to this indicator on this time step</param>
/// <param name="VolumeQuantity">Volume quantity of the data point, it dependes of DefineVolume method.</param>
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<decimal, decimal>(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);
_volumePerPrice[ClosePrice] -= RemovedDataPoint.Item2;
if (_volumePerPrice[ClosePrice] == 0)
{
_volumePerPrice.Remove(ClosePrice);
}
}
}
/// <summary>
/// Finds the close price with biggest volume value.
/// </summary>
/// <returns> Index of the close price with biggest volume value</returns>
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)<Math.Abs(mid/2 - maxIdx))
{
maxIdx = index;
}
}
}
return maxIdx;
}
/// <summary>
/// Calculate the Value Area Volume and the highest and lowest prices within it (VAH and VAL).
/// </summary>
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)
{
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.Keys[maxIndex];
ValueAreaLow = VolumePerPrice.Keys[minIndex];
}
/// <summary>
/// Round the decimal number
/// </summary>
/// <param name="a">The decimal number to round</param>
/// <returns>The rounded decimal number</returns>
private decimal Round(decimal a)
{
return Math.Ceiling(a * _priceRangeRoundOff) / _priceRangeRoundOff;
}
/// <summary>
/// Resets this indicator to its initial state
/// </summary>
public override void Reset()
{
_oldDataPoints.Reset();
_volumePerPrice.Clear();
_totalVolume.Reset();
base.Reset();
}
}
}