Lean  $LEAN_TAG$
TradeStatistics.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 Newtonsoft.Json;
19 using QuantConnect.Util;
20 
22 {
23  /// <summary>
24  /// The <see cref="TradeStatistics"/> class represents a set of statistics calculated from a list of closed trades
25  /// </summary>
26  public class TradeStatistics
27  {
28  /// <summary>
29  /// The entry date/time of the first trade
30  /// </summary>
31  public DateTime? StartDateTime { get; set; }
32 
33  /// <summary>
34  /// The exit date/time of the last trade
35  /// </summary>
36  public DateTime? EndDateTime { get; set; }
37 
38  /// <summary>
39  /// The total number of trades
40  /// </summary>
41  public int TotalNumberOfTrades { get; set; }
42 
43  /// <summary>
44  /// The total number of winning trades
45  /// </summary>
46  public int NumberOfWinningTrades { get; set; }
47 
48  /// <summary>
49  /// The total number of losing trades
50  /// </summary>
51  public int NumberOfLosingTrades { get; set; }
52 
53  /// <summary>
54  /// The total profit/loss for all trades (as symbol currency)
55  /// </summary>
56  [JsonConverter(typeof(JsonRoundingConverter))]
57  public decimal TotalProfitLoss { get; set; }
58 
59  /// <summary>
60  /// The total profit for all winning trades (as symbol currency)
61  /// </summary>
62  [JsonConverter(typeof(JsonRoundingConverter))]
63  public decimal TotalProfit { get; set; }
64 
65  /// <summary>
66  /// The total loss for all losing trades (as symbol currency)
67  /// </summary>
68  [JsonConverter(typeof(JsonRoundingConverter))]
69  public decimal TotalLoss { get; set; }
70 
71  /// <summary>
72  /// The largest profit in a single trade (as symbol currency)
73  /// </summary>
74  [JsonConverter(typeof(JsonRoundingConverter))]
75  public decimal LargestProfit { get; set; }
76 
77  /// <summary>
78  /// The largest loss in a single trade (as symbol currency)
79  /// </summary>
80  [JsonConverter(typeof(JsonRoundingConverter))]
81  public decimal LargestLoss { get; set; }
82 
83  /// <summary>
84  /// The average profit/loss (a.k.a. Expectancy or Average Trade) for all trades (as symbol currency)
85  /// </summary>
86  [JsonConverter(typeof(JsonRoundingConverter))]
87  public decimal AverageProfitLoss { get; set; }
88 
89  /// <summary>
90  /// The average profit for all winning trades (as symbol currency)
91  /// </summary>
92  [JsonConverter(typeof(JsonRoundingConverter))]
93  public decimal AverageProfit { get; set; }
94 
95  /// <summary>
96  /// The average loss for all winning trades (as symbol currency)
97  /// </summary>
98  [JsonConverter(typeof(JsonRoundingConverter))]
99  public decimal AverageLoss { get; set; }
100 
101  /// <summary>
102  /// The average duration for all trades
103  /// </summary>
104  public TimeSpan AverageTradeDuration { get; set; }
105 
106  /// <summary>
107  /// The average duration for all winning trades
108  /// </summary>
109  public TimeSpan AverageWinningTradeDuration { get; set; }
110 
111  /// <summary>
112  /// The average duration for all losing trades
113  /// </summary>
114  public TimeSpan AverageLosingTradeDuration { get; set; }
115 
116  /// <summary>
117  /// The median duration for all trades
118  /// </summary>
119  public TimeSpan MedianTradeDuration { get; set; }
120 
121  /// <summary>
122  /// The median duration for all winning trades
123  /// </summary>
124  public TimeSpan MedianWinningTradeDuration { get; set; }
125 
126  /// <summary>
127  /// The median duration for all losing trades
128  /// </summary>
129  public TimeSpan MedianLosingTradeDuration { get; set; }
130 
131  /// <summary>
132  /// The maximum number of consecutive winning trades
133  /// </summary>
134  public int MaxConsecutiveWinningTrades { get; set; }
135 
136  /// <summary>
137  /// The maximum number of consecutive losing trades
138  /// </summary>
139  public int MaxConsecutiveLosingTrades { get; set; }
140 
141  /// <summary>
142  /// The ratio of the average profit per trade to the average loss per trade
143  /// </summary>
144  /// <remarks>If the average loss is zero, ProfitLossRatio is set to 0</remarks>
145  [JsonConverter(typeof(JsonRoundingConverter))]
146  public decimal ProfitLossRatio { get; set; }
147 
148  /// <summary>
149  /// The ratio of the number of winning trades to the number of losing trades
150  /// </summary>
151  /// <remarks>If the total number of trades is zero, WinLossRatio is set to zero</remarks>
152  /// <remarks>If the number of losing trades is zero and the number of winning trades is nonzero, WinLossRatio is set to 10</remarks>
153  [JsonConverter(typeof(JsonRoundingConverter))]
154  public decimal WinLossRatio { get; set; }
155 
156  /// <summary>
157  /// The ratio of the number of winning trades to the total number of trades
158  /// </summary>
159  /// <remarks>If the total number of trades is zero, WinRate is set to zero</remarks>
160  [JsonConverter(typeof(JsonRoundingConverter))]
161  public decimal WinRate { get; set; }
162 
163  /// <summary>
164  /// The ratio of the number of losing trades to the total number of trades
165  /// </summary>
166  /// <remarks>If the total number of trades is zero, LossRate is set to zero</remarks>
167  [JsonConverter(typeof(JsonRoundingConverter))]
168  public decimal LossRate { get; set; }
169 
170  /// <summary>
171  /// The average Maximum Adverse Excursion for all trades
172  /// </summary>
173  [JsonConverter(typeof(JsonRoundingConverter))]
174  public decimal AverageMAE { get; set; }
175 
176  /// <summary>
177  /// The average Maximum Favorable Excursion for all trades
178  /// </summary>
179  [JsonConverter(typeof(JsonRoundingConverter))]
180  public decimal AverageMFE { get; set; }
181 
182  /// <summary>
183  /// The largest Maximum Adverse Excursion in a single trade (as symbol currency)
184  /// </summary>
185  [JsonConverter(typeof(JsonRoundingConverter))]
186  public decimal LargestMAE { get; set; }
187 
188  /// <summary>
189  /// The largest Maximum Favorable Excursion in a single trade (as symbol currency)
190  /// </summary>
191  [JsonConverter(typeof(JsonRoundingConverter))]
192  public decimal LargestMFE { get; set; }
193 
194  /// <summary>
195  /// The maximum closed-trade drawdown for all trades (as symbol currency)
196  /// </summary>
197  /// <remarks>The calculation only takes into account the profit/loss of each trade</remarks>
198  [JsonConverter(typeof(JsonRoundingConverter))]
199  public decimal MaximumClosedTradeDrawdown { get; set; }
200 
201  /// <summary>
202  /// The maximum intra-trade drawdown for all trades (as symbol currency)
203  /// </summary>
204  /// <remarks>The calculation takes into account MAE and MFE of each trade</remarks>
205  [JsonConverter(typeof(JsonRoundingConverter))]
206  public decimal MaximumIntraTradeDrawdown { get; set; }
207 
208  /// <summary>
209  /// The standard deviation of the profits/losses for all trades (as symbol currency)
210  /// </summary>
211  [JsonConverter(typeof(JsonRoundingConverter))]
212  public decimal ProfitLossStandardDeviation { get; set; }
213 
214  /// <summary>
215  /// The downside deviation of the profits/losses for all trades (as symbol currency)
216  /// </summary>
217  /// <remarks>This metric only considers deviations of losing trades</remarks>
218  [JsonConverter(typeof(JsonRoundingConverter))]
219  public decimal ProfitLossDownsideDeviation { get; set; }
220 
221  /// <summary>
222  /// The ratio of the total profit to the total loss
223  /// </summary>
224  /// <remarks>If the total profit is zero, ProfitFactor is set to zero</remarks>
225  /// <remarks>if the total loss is zero and the total profit is nonzero, ProfitFactor is set to 10</remarks>
226  [JsonConverter(typeof(JsonRoundingConverter))]
227  public decimal ProfitFactor { get; set; }
228 
229  /// <summary>
230  /// The ratio of the average profit/loss to the standard deviation
231  /// </summary>
232  [JsonConverter(typeof(JsonRoundingConverter))]
233  public decimal SharpeRatio { get; set; }
234 
235  /// <summary>
236  /// The ratio of the average profit/loss to the downside deviation
237  /// </summary>
238  [JsonConverter(typeof(JsonRoundingConverter))]
239  public decimal SortinoRatio { get; set; }
240 
241  /// <summary>
242  /// The ratio of the total profit/loss to the maximum closed trade drawdown
243  /// </summary>
244  /// <remarks>If the total profit/loss is zero, ProfitToMaxDrawdownRatio is set to zero</remarks>
245  /// <remarks>if the drawdown is zero and the total profit is nonzero, ProfitToMaxDrawdownRatio is set to 10</remarks>
246  [JsonConverter(typeof(JsonRoundingConverter))]
247  public decimal ProfitToMaxDrawdownRatio { get; set; }
248 
249  /// <summary>
250  /// The maximum amount of profit given back by a single trade before exit (as symbol currency)
251  /// </summary>
252  [JsonConverter(typeof(JsonRoundingConverter))]
253  public decimal MaximumEndTradeDrawdown { get; set; }
254 
255  /// <summary>
256  /// The average amount of profit given back by all trades before exit (as symbol currency)
257  /// </summary>
258  [JsonConverter(typeof(JsonRoundingConverter))]
259  public decimal AverageEndTradeDrawdown { get; set; }
260 
261  /// <summary>
262  /// The maximum amount of time to recover from a drawdown (longest time between new equity highs or peaks)
263  /// </summary>
264  public TimeSpan MaximumDrawdownDuration { get; set; }
265 
266  /// <summary>
267  /// The sum of fees for all trades
268  /// </summary>
269  [JsonConverter(typeof(JsonRoundingConverter))]
270  public decimal TotalFees { get; set; }
271 
272  /// <summary>
273  /// Initializes a new instance of the <see cref="TradeStatistics"/> class
274  /// </summary>
275  /// <param name="trades">The list of closed trades</param>
276  public TradeStatistics(IEnumerable<Trade> trades)
277  {
278  var maxConsecutiveWinners = 0;
279  var maxConsecutiveLosers = 0;
280  var maxTotalProfitLoss = 0m;
281  var maxTotalProfitLossWithMfe = 0m;
282  var sumForVariance = 0m;
283  var sumForDownsideVariance = 0m;
284  var lastPeakTime = DateTime.MinValue;
285  var isInDrawdown = false;
286  var allTradeDurationsTicks = new List<long>();
287  var winningTradeDurationsTicks = new List<long>();
288  var losingTradeDurationsTicks = new List<long>();
289  var numberOfITMOptionsWinningTrades = 0;
290 
291  foreach (var trade in trades)
292  {
293  if (lastPeakTime == DateTime.MinValue) lastPeakTime = trade.EntryTime;
294 
295  if (StartDateTime == null || trade.EntryTime < StartDateTime)
296  StartDateTime = trade.EntryTime;
297 
298  if (EndDateTime == null || trade.ExitTime > EndDateTime)
299  EndDateTime = trade.ExitTime;
300 
302 
303  if (TotalProfitLoss + trade.MFE > maxTotalProfitLossWithMfe)
304  maxTotalProfitLossWithMfe = TotalProfitLoss + trade.MFE;
305 
306  if (TotalProfitLoss + trade.MAE - maxTotalProfitLossWithMfe < MaximumIntraTradeDrawdown)
307  MaximumIntraTradeDrawdown = TotalProfitLoss + trade.MAE - maxTotalProfitLossWithMfe;
308 
309  if (trade.ProfitLoss > 0)
310  {
311  // winning trade
313 
314  TotalProfitLoss += trade.ProfitLoss;
315  TotalProfit += trade.ProfitLoss;
316  AverageProfit += (trade.ProfitLoss - AverageProfit) / NumberOfWinningTrades;
317 
318  AverageWinningTradeDuration += TimeSpan.FromSeconds((trade.Duration.TotalSeconds - AverageWinningTradeDuration.TotalSeconds) / NumberOfWinningTrades);
319 
320  winningTradeDurationsTicks.Add(trade.Duration.Ticks);
321 
322  if (trade.ProfitLoss > LargestProfit)
323  LargestProfit = trade.ProfitLoss;
324 
325  maxConsecutiveWinners++;
326  maxConsecutiveLosers = 0;
327  if (maxConsecutiveWinners > MaxConsecutiveWinningTrades)
328  MaxConsecutiveWinningTrades = maxConsecutiveWinners;
329 
330  if (TotalProfitLoss > maxTotalProfitLoss)
331  {
332  // new equity high
333  maxTotalProfitLoss = TotalProfitLoss;
334 
335  if (isInDrawdown && trade.ExitTime - lastPeakTime > MaximumDrawdownDuration)
336  MaximumDrawdownDuration = trade.ExitTime - lastPeakTime;
337 
338  lastPeakTime = trade.ExitTime;
339  isInDrawdown = false;
340  }
341  }
342  else
343  {
344  // losing trade
346 
347  TotalProfitLoss += trade.ProfitLoss;
348  TotalLoss += trade.ProfitLoss;
349  var prevAverageLoss = AverageLoss;
350  AverageLoss += (trade.ProfitLoss - AverageLoss) / NumberOfLosingTrades;
351 
352  sumForDownsideVariance += (trade.ProfitLoss - prevAverageLoss) * (trade.ProfitLoss - AverageLoss);
353  var downsideVariance = NumberOfLosingTrades > 1 ? sumForDownsideVariance / (NumberOfLosingTrades - 1) : 0;
354  ProfitLossDownsideDeviation = (decimal)Math.Sqrt((double)downsideVariance);
355 
356  AverageLosingTradeDuration += TimeSpan.FromSeconds((trade.Duration.TotalSeconds - AverageLosingTradeDuration.TotalSeconds) / NumberOfLosingTrades);
357 
358  losingTradeDurationsTicks.Add(trade.Duration.Ticks);
359 
360  if (trade.ProfitLoss < LargestLoss)
361  LargestLoss = trade.ProfitLoss;
362 
363  // even though losing money, an ITM option trade is a winning trade,
364  // so IsWin for an ITM OptionTrade will return true even if the trade was not profitable.
365  if (trade.IsWin)
366  {
367  numberOfITMOptionsWinningTrades++;
368  maxConsecutiveLosers = 0;
369  maxConsecutiveWinners++;
370  if (maxConsecutiveWinners > MaxConsecutiveWinningTrades)
371  MaxConsecutiveWinningTrades = maxConsecutiveWinners;
372  }
373  else
374  {
375  maxConsecutiveWinners = 0;
376  maxConsecutiveLosers++;
377  if (maxConsecutiveLosers > MaxConsecutiveLosingTrades)
378  MaxConsecutiveLosingTrades = maxConsecutiveLosers;
379  }
380 
381  if (TotalProfitLoss - maxTotalProfitLoss < MaximumClosedTradeDrawdown)
382  MaximumClosedTradeDrawdown = TotalProfitLoss - maxTotalProfitLoss;
383 
384  isInDrawdown = true;
385  }
386 
387  var prevAverageProfitLoss = AverageProfitLoss;
388  AverageProfitLoss += (trade.ProfitLoss - AverageProfitLoss) / TotalNumberOfTrades;
389 
390  sumForVariance += (trade.ProfitLoss - prevAverageProfitLoss) * (trade.ProfitLoss - AverageProfitLoss);
391  var variance = TotalNumberOfTrades > 1 ? sumForVariance / (TotalNumberOfTrades - 1) : 0;
392  ProfitLossStandardDeviation = (decimal)Math.Sqrt((double)variance);
393 
394  AverageTradeDuration += TimeSpan.FromSeconds((trade.Duration.TotalSeconds - AverageTradeDuration.TotalSeconds) / TotalNumberOfTrades);
395  allTradeDurationsTicks.Add(trade.Duration.Ticks);
396  AverageMAE += (trade.MAE - AverageMAE) / TotalNumberOfTrades;
397  AverageMFE += (trade.MFE - AverageMFE) / TotalNumberOfTrades;
398 
399  if (trade.MAE < LargestMAE)
400  LargestMAE = trade.MAE;
401 
402  if (trade.MFE > LargestMFE)
403  LargestMFE = trade.MFE;
404 
405  if (trade.EndTradeDrawdown < MaximumEndTradeDrawdown)
406  MaximumEndTradeDrawdown = trade.EndTradeDrawdown;
407 
408  TotalFees += trade.TotalFees;
409  }
410 
411  // Adjust number of winning and losing trades: ITM options assignment loss counts as a loss for profit and loss calculations,
412  // but adds a win to the wins count since this is an actual win even though premium paid is a loss.
413  NumberOfWinningTrades += numberOfITMOptionsWinningTrades;
414  NumberOfLosingTrades -= numberOfITMOptionsWinningTrades;
415 
416  ProfitLossRatio = AverageLoss == 0 ? 0 : AverageProfit / Math.Abs(AverageLoss);
419  LossRate = TotalNumberOfTrades > 0 ? 1 - WinRate : 0;
420  ProfitFactor = TotalProfit == 0 ? 0 : (TotalLoss < 0 ? TotalProfit / Math.Abs(TotalLoss) : 10);
424 
426 
427  if (allTradeDurationsTicks.Count > 0)
428  MedianTradeDuration = TimeSpan.FromTicks(allTradeDurationsTicks.Median());
429  if (winningTradeDurationsTicks.Count > 0)
430  MedianWinningTradeDuration = TimeSpan.FromTicks(winningTradeDurationsTicks.Median());
431  if (losingTradeDurationsTicks.Count > 0)
432  MedianLosingTradeDuration = TimeSpan.FromTicks(losingTradeDurationsTicks.Median());
433  }
434 
435  /// <summary>
436  /// Initializes a new instance of the <see cref="TradeStatistics"/> class
437  /// </summary>
439  {
440  }
441 
442  }
443 }