Skip to content

Commit ecb8e8d

Browse files
authored
Fix trades drawdown calculation (#9249)
* Fix trade drawdown calculation * Cleanup * Disable MAE. MFE and Drawdown calculation for FlatToFlat and FlatToReduced trade grouping methods * Minor test fixes
1 parent c6c4c1e commit ecb8e8d

5 files changed

Lines changed: 499 additions & 145 deletions

File tree

Common/Statistics/Trade.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,7 @@ public TimeSpan Duration
119119
/// <summary>
120120
/// Returns the amount of profit given back before the trade was closed
121121
/// </summary>
122-
public decimal EndTradeDrawdown
123-
{
124-
get { return ProfitLoss - MFE; }
125-
}
122+
public decimal EndTradeDrawdown { get; set; }
126123

127124
/// <summary>
128125
/// Returns whether the trade was profitable (is a win) or not (a loss)

Common/Statistics/TradeBuilder.cs

Lines changed: 95 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@
1313
* limitations under the License.
1414
*/
1515

16-
using System;
17-
using System.Collections.Generic;
18-
using System.Linq;
1916
using QuantConnect.Data.Market;
2017
using QuantConnect.Interfaces;
2118
using QuantConnect.Orders;
2219
using QuantConnect.Securities;
2320
using QuantConnect.Util;
21+
using System;
22+
using System.Collections.Generic;
23+
using System.Linq;
2424

2525
namespace QuantConnect.Statistics
2626
{
@@ -29,20 +29,48 @@ namespace QuantConnect.Statistics
2929
/// </summary>
3030
public class TradeBuilder : ITradeBuilder
3131
{
32+
private class TradeState
33+
{
34+
internal Trade Trade { get; set; }
35+
internal decimal MaxProfit { get; set; }
36+
internal decimal MaxDrawdown { get; set; }
37+
38+
/// <summary>
39+
/// Updates the drawdown state given the current profit
40+
/// </summary>
41+
public void UpdateDrawdown(decimal currentProfit)
42+
{
43+
if (currentProfit < MaxProfit)
44+
{
45+
// There is a drawdown, but we only care about the maximum drawdown
46+
var drawdown = MaxProfit - currentProfit;
47+
if (drawdown > MaxDrawdown)
48+
{
49+
MaxDrawdown = drawdown;
50+
}
51+
}
52+
else
53+
{
54+
// New maximum profit
55+
MaxProfit = currentProfit;
56+
}
57+
}
58+
}
59+
3260
/// <summary>
3361
/// Helper class to manage pending trades and market price updates for a symbol
3462
/// </summary>
3563
private class Position
3664
{
37-
internal List<Trade> PendingTrades { get; set; }
65+
internal List<TradeState> PendingTrades { get; set; }
3866
internal List<OrderEvent> PendingFills { get; set; }
3967
internal decimal TotalFees { get; set; }
4068
internal decimal MaxPrice { get; set; }
4169
internal decimal MinPrice { get; set; }
4270

4371
public Position()
4472
{
45-
PendingTrades = new List<Trade>();
73+
PendingTrades = new List<TradeState>();
4674
PendingFills = new List<OrderEvent>();
4775
}
4876
}
@@ -130,6 +158,14 @@ public void SetMarketPrice(Symbol symbol, decimal price)
130158
position.MaxPrice = price;
131159
else if (price < position.MinPrice)
132160
position.MinPrice = price;
161+
162+
for (var i = 0; i < position.PendingTrades.Count; i++)
163+
{
164+
var tradeState = position.PendingTrades[i];
165+
var trade = tradeState.Trade;
166+
var currentProfit = trade.Direction == TradeDirection.Long ? price - trade.EntryPrice : trade.EntryPrice - price;
167+
tradeState.UpdateDrawdown(currentProfit);
168+
}
133169
}
134170

135171
/// <summary>
@@ -151,11 +187,13 @@ public void ApplySplit(Split split, bool liveMode, DataNormalizationMode dataNor
151187
position.MinPrice *= split.SplitFactor;
152188
position.MaxPrice *= split.SplitFactor;
153189

154-
foreach (var trade in position.PendingTrades)
190+
foreach (var tradeState in position.PendingTrades)
155191
{
156-
trade.Quantity /= split.SplitFactor;
157-
trade.EntryPrice *= split.SplitFactor;
158-
trade.ExitPrice *= split.SplitFactor;
192+
tradeState.Trade.Quantity /= split.SplitFactor;
193+
tradeState.Trade.EntryPrice *= split.SplitFactor;
194+
tradeState.Trade.ExitPrice *= split.SplitFactor;
195+
tradeState.MaxProfit *= split.SplitFactor;
196+
tradeState.MaxDrawdown *= split.SplitFactor;
159197
}
160198

161199
foreach (var pendingFill in position.PendingFills)
@@ -223,17 +261,20 @@ private void ProcessFillUsingFillToFill(OrderEvent fill, decimal orderFee, decim
223261
// no pending trades for symbol
224262
_positions[fill.Symbol] = new Position
225263
{
226-
PendingTrades = new List<Trade>
264+
PendingTrades = new List<TradeState>
227265
{
228-
new Trade
266+
new TradeState
229267
{
230-
Symbols = [fill.Symbol],
231-
EntryTime = fill.UtcTime,
232-
EntryPrice = fill.FillPrice,
233-
Direction = fill.FillQuantity > 0 ? TradeDirection.Long : TradeDirection.Short,
234-
Quantity = fill.AbsoluteFillQuantity,
235-
TotalFees = orderFee,
236-
OrderIds = new HashSet<int>() { fill.OrderId }
268+
Trade = new Trade
269+
{
270+
Symbols = [fill.Symbol],
271+
EntryTime = fill.UtcTime,
272+
EntryPrice = fill.FillPrice,
273+
Direction = fill.FillQuantity > 0 ? TradeDirection.Long : TradeDirection.Short,
274+
Quantity = fill.AbsoluteFillQuantity,
275+
TotalFees = orderFee,
276+
OrderIds = new HashSet<int>() { fill.OrderId }
277+
}
237278
}
238279
},
239280
MinPrice = fill.FillPrice,
@@ -246,18 +287,21 @@ private void ProcessFillUsingFillToFill(OrderEvent fill, decimal orderFee, decim
246287

247288
var index = _matchingMethod == FillMatchingMethod.FIFO ? 0 : position.PendingTrades.Count - 1;
248289

249-
if (Math.Sign(fill.FillQuantity) == (position.PendingTrades[index].Direction == TradeDirection.Long ? +1 : -1))
290+
if (Math.Sign(fill.FillQuantity) == (position.PendingTrades[index].Trade.Direction == TradeDirection.Long ? +1 : -1))
250291
{
251292
// execution has same direction of trade
252-
position.PendingTrades.Add(new Trade
293+
position.PendingTrades.Add(new TradeState
253294
{
254-
Symbols = [fill.Symbol],
255-
EntryTime = fill.UtcTime,
256-
EntryPrice = fill.FillPrice,
257-
Direction = fill.FillQuantity > 0 ? TradeDirection.Long : TradeDirection.Short,
258-
Quantity = fill.AbsoluteFillQuantity,
259-
TotalFees = orderFee,
260-
OrderIds = new HashSet<int>() { fill.OrderId }
295+
Trade = new Trade
296+
{
297+
Symbols = [fill.Symbol],
298+
EntryTime = fill.UtcTime,
299+
EntryPrice = fill.FillPrice,
300+
Direction = fill.FillQuantity > 0 ? TradeDirection.Long : TradeDirection.Short,
301+
Quantity = fill.AbsoluteFillQuantity,
302+
TotalFees = orderFee,
303+
OrderIds = new HashSet<int>() { fill.OrderId }
304+
}
261305
});
262306
}
263307
else
@@ -267,7 +311,8 @@ private void ProcessFillUsingFillToFill(OrderEvent fill, decimal orderFee, decim
267311
var orderFeeAssigned = false;
268312
while (position.PendingTrades.Count > 0 && Math.Abs(totalExecutedQuantity) < fill.AbsoluteFillQuantity)
269313
{
270-
var trade = position.PendingTrades[index];
314+
var tradeState = position.PendingTrades[index];
315+
var trade = tradeState.Trade;
271316
var absoluteUnexecutedQuantity = fill.AbsoluteFillQuantity - Math.Abs(totalExecutedQuantity);
272317

273318
if (absoluteUnexecutedQuantity >= trade.Quantity)
@@ -285,6 +330,7 @@ private void ProcessFillUsingFillToFill(OrderEvent fill, decimal orderFee, decim
285330
trade.TotalFees += orderFeeAssigned ? 0 : orderFee;
286331
trade.MAE = Math.Round((trade.Direction == TradeDirection.Long ? position.MinPrice - trade.EntryPrice : trade.EntryPrice - position.MaxPrice) * trade.Quantity * conversionRate * multiplier, 2);
287332
trade.MFE = Math.Round((trade.Direction == TradeDirection.Long ? position.MaxPrice - trade.EntryPrice : trade.EntryPrice - position.MinPrice) * trade.Quantity * conversionRate * multiplier, 2);
333+
trade.EndTradeDrawdown = Math.Round(tradeState.MaxDrawdown * trade.Quantity * conversionRate * multiplier, 2);
288334

289335
AddNewTrade(trade, fill);
290336
}
@@ -306,6 +352,7 @@ private void ProcessFillUsingFillToFill(OrderEvent fill, decimal orderFee, decim
306352
TotalFees = trade.TotalFees + (orderFeeAssigned ? 0 : orderFee),
307353
MAE = Math.Round((trade.Direction == TradeDirection.Long ? position.MinPrice - trade.EntryPrice : trade.EntryPrice - position.MaxPrice) * absoluteUnexecutedQuantity * conversionRate * multiplier, 2),
308354
MFE = Math.Round((trade.Direction == TradeDirection.Long ? position.MaxPrice - trade.EntryPrice : trade.EntryPrice - position.MinPrice) * absoluteUnexecutedQuantity * conversionRate * multiplier, 2),
355+
EndTradeDrawdown = Math.Round(tradeState.MaxDrawdown * absoluteUnexecutedQuantity * conversionRate * multiplier, 2),
309356
OrderIds = new HashSet<int>([..trade.OrderIds, fill.OrderId])
310357
};
311358

@@ -325,17 +372,20 @@ private void ProcessFillUsingFillToFill(OrderEvent fill, decimal orderFee, decim
325372
{
326373
// direction reversal
327374
fill.FillQuantity -= totalExecutedQuantity;
328-
position.PendingTrades = new List<Trade>
375+
position.PendingTrades = new List<TradeState>
329376
{
330-
new Trade
377+
new TradeState
331378
{
332-
Symbols =[fill.Symbol],
333-
EntryTime = fill.UtcTime,
334-
EntryPrice = fill.FillPrice,
335-
Direction = fill.FillQuantity > 0 ? TradeDirection.Long : TradeDirection.Short,
336-
Quantity = fill.AbsoluteFillQuantity,
337-
TotalFees = 0,
338-
OrderIds = new HashSet<int>() { fill.OrderId }
379+
Trade = new Trade
380+
{
381+
Symbols =[fill.Symbol],
382+
EntryTime = fill.UtcTime,
383+
EntryPrice = fill.FillPrice,
384+
Direction = fill.FillQuantity > 0 ? TradeDirection.Long : TradeDirection.Short,
385+
Quantity = fill.AbsoluteFillQuantity,
386+
TotalFees = 0,
387+
OrderIds = new HashSet<int>() { fill.OrderId }
388+
}
339389
}
340390
};
341391
position.MinPrice = fill.FillPrice;
@@ -421,9 +471,12 @@ private void ProcessFillUsingFlatToFlat(OrderEvent fill, decimal orderFee, decim
421471
ExitPrice = exitAveragePrice,
422472
ProfitLoss = Math.Round((exitAveragePrice - entryAveragePrice) * Math.Abs(totalEntryQuantity) * Math.Sign(totalEntryQuantity) * conversionRate * multiplier, 2),
423473
TotalFees = position.TotalFees,
424-
MAE = Math.Round((direction == TradeDirection.Long ? position.MinPrice - entryAveragePrice : entryAveragePrice - position.MaxPrice) * Math.Abs(totalEntryQuantity) * conversionRate * multiplier, 2),
425-
MFE = Math.Round((direction == TradeDirection.Long ? position.MaxPrice - entryAveragePrice : entryAveragePrice - position.MinPrice) * Math.Abs(totalEntryQuantity) * conversionRate * multiplier, 2),
426474
OrderIds = relatedOrderIds
475+
// MAE, MFE, EndTradeDrawdown are zero for FlatToFlat grouping method.
476+
// WE can fix this in the future if needed, but it might require tracking market prices
477+
// during the life of the trade, so that we can compute these metrics accurately accounting for
478+
// time, each fill entry price and quantity, which affect profit and drawdown and
479+
// adds complexity and memory overhead.
427480
};
428481

429482
AddNewTrade(trade, fill);
@@ -524,9 +577,10 @@ private void ProcessFillUsingFlatToReduced(OrderEvent fill, decimal orderFee, de
524577
ExitPrice = fill.FillPrice,
525578
ProfitLoss = Math.Round((fill.FillPrice - entryPrice) * Math.Abs(totalExecutedQuantity) * Math.Sign(-totalExecutedQuantity) * conversionRate * multiplier, 2),
526579
TotalFees = position.TotalFees,
527-
MAE = Math.Round((direction == TradeDirection.Long ? position.MinPrice - entryPrice : entryPrice - position.MaxPrice) * Math.Abs(totalExecutedQuantity) * conversionRate * multiplier, 2),
528-
MFE = Math.Round((direction == TradeDirection.Long ? position.MaxPrice - entryPrice : entryPrice - position.MinPrice) * Math.Abs(totalExecutedQuantity) * conversionRate * multiplier, 2),
529580
OrderIds = relatedOrderIds
581+
582+
// MAE, MFE, EndTradeDrawdown are zero for FlatToReduce grouping method.
583+
// See comment in FlatToFlat method for more details.541
530584
};
531585

532586
AddNewTrade(trade, fill);

Common/Statistics/TradeStatistics.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ public TradeStatistics(IEnumerable<Trade> trades)
402402
if (trade.MFE > LargestMFE)
403403
LargestMFE = trade.MFE;
404404

405-
if (trade.EndTradeDrawdown < MaximumEndTradeDrawdown)
405+
if (trade.EndTradeDrawdown > MaximumEndTradeDrawdown)
406406
MaximumEndTradeDrawdown = trade.EndTradeDrawdown;
407407

408408
TotalFees += trade.TotalFees;

0 commit comments

Comments
 (0)