1313 * limitations under the License.
1414*/
1515
16- using System ;
17- using System . Collections . Generic ;
18- using System . Linq ;
1916using QuantConnect . Data . Market ;
2017using QuantConnect . Interfaces ;
2118using QuantConnect . Orders ;
2219using QuantConnect . Securities ;
2320using QuantConnect . Util ;
21+ using System ;
22+ using System . Collections . Generic ;
23+ using System . Linq ;
2424
2525namespace 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 ) ;
0 commit comments