Lean  $LEAN_TAG$
TradeBuilder.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;
21 using QuantConnect.Orders;
23 using QuantConnect.Util;
24 
26 {
27  /// <summary>
28  /// The <see cref="TradeBuilder"/> class generates trades from executions and market price updates
29  /// </summary>
30  public class TradeBuilder : ITradeBuilder
31  {
32  /// <summary>
33  /// Helper class to manage pending trades and market price updates for a symbol
34  /// </summary>
35  private class Position
36  {
37  internal List<Trade> PendingTrades { get; set; }
38  internal List<OrderEvent> PendingFills { get; set; }
39  internal decimal TotalFees { get; set; }
40  internal decimal MaxPrice { get; set; }
41  internal decimal MinPrice { get; set; }
42 
43  public Position()
44  {
45  PendingTrades = new List<Trade>();
46  PendingFills = new List<OrderEvent>();
47  }
48  }
49 
50  private const int LiveModeMaxTradeCount = 10000;
51  private const int LiveModeMaxTradeAgeMonths = 12;
52  private const int MaxOrderIdCacheSize = 1000;
53 
54  private readonly List<Trade> _closedTrades = new List<Trade>();
55  private readonly Dictionary<Symbol, Position> _positions = new Dictionary<Symbol, Position>();
56  private readonly FixedSizeHashQueue<int> _ordersWithFeesAssigned = new FixedSizeHashQueue<int>(MaxOrderIdCacheSize);
57  private readonly FillGroupingMethod _groupingMethod;
58  private readonly FillMatchingMethod _matchingMethod;
59  private SecurityManager _securities;
60  private bool _liveMode;
61 
62  /// <summary>
63  /// Initializes a new instance of the <see cref="TradeBuilder"/> class
64  /// </summary>
65  public TradeBuilder(FillGroupingMethod groupingMethod, FillMatchingMethod matchingMethod)
66  {
67  _groupingMethod = groupingMethod;
68  _matchingMethod = matchingMethod;
69  }
70 
71  /// <summary>
72  /// Sets the live mode flag
73  /// </summary>
74  /// <param name="live">The live mode flag</param>
75  public void SetLiveMode(bool live)
76  {
77  _liveMode = live;
78  }
79 
80  /// <summary>
81  /// Sets the security manager instance
82  /// </summary>
83  /// <param name="securities">The security manager</param>
84  public void SetSecurityManager(SecurityManager securities)
85  {
86  _securities = securities;
87  }
88 
89  /// <summary>
90  /// The list of closed trades
91  /// </summary>
92  public List<Trade> ClosedTrades
93  {
94  get
95  {
96  lock (_closedTrades)
97  {
98  return new List<Trade>(_closedTrades);
99  }
100  }
101  }
102 
103  /// <summary>
104  /// Returns true if there is an open position for the symbol
105  /// </summary>
106  /// <param name="symbol">The symbol</param>
107  /// <returns>true if there is an open position for the symbol</returns>
108  public bool HasOpenPosition(Symbol symbol)
109  {
110  Position position;
111  if (!_positions.TryGetValue(symbol, out position)) return false;
112 
113  if (_groupingMethod == FillGroupingMethod.FillToFill)
114  return position.PendingTrades.Count > 0;
115 
116  return position.PendingFills.Count > 0;
117  }
118 
119  /// <summary>
120  /// Sets the current market price for the symbol
121  /// </summary>
122  /// <param name="symbol"></param>
123  /// <param name="price"></param>
124  public void SetMarketPrice(Symbol symbol, decimal price)
125  {
126  Position position;
127  if (!_positions.TryGetValue(symbol, out position)) return;
128 
129  if (price > position.MaxPrice)
130  position.MaxPrice = price;
131  else if (price < position.MinPrice)
132  position.MinPrice = price;
133  }
134 
135  /// <summary>
136  /// Applies a split to the trade builder
137  /// </summary>
138  /// <param name="split">The split to be applied</param>
139  /// <param name="liveMode">True if live mode, false for backtest</param>
140  /// <param name="dataNormalizationMode">The <see cref="DataNormalizationMode"/> for this security</param>
141  public void ApplySplit(Split split, bool liveMode, DataNormalizationMode dataNormalizationMode)
142  {
143  // only apply splits to equities, in live or raw data mode, and for open positions
144  if (split.Symbol.SecurityType != SecurityType.Equity ||
145  (!liveMode && dataNormalizationMode != DataNormalizationMode.Raw) ||
146  !_positions.TryGetValue(split.Symbol, out var position))
147  {
148  return;
149  }
150 
151  position.MinPrice *= split.SplitFactor;
152  position.MaxPrice *= split.SplitFactor;
153 
154  foreach (var trade in position.PendingTrades)
155  {
156  trade.Quantity /= split.SplitFactor;
157  trade.EntryPrice *= split.SplitFactor;
158  trade.ExitPrice *= split.SplitFactor;
159  }
160 
161  foreach (var pendingFill in position.PendingFills)
162  {
163  pendingFill.FillQuantity /= split.SplitFactor;
164  pendingFill.FillPrice *= split.SplitFactor;
165 
166  if (pendingFill.LimitPrice.HasValue)
167  {
168  pendingFill.LimitPrice *= split.SplitFactor;
169  }
170  if (pendingFill.StopPrice.HasValue)
171  {
172  pendingFill.StopPrice *= split.SplitFactor;
173  }
174  if (pendingFill.TriggerPrice.HasValue)
175  {
176  pendingFill.TriggerPrice *= split.SplitFactor;
177  }
178  }
179  }
180 
181  /// <summary>
182  /// Processes a new fill, eventually creating new trades
183  /// </summary>
184  /// <param name="fill">The new fill order event</param>
185  /// <param name="securityConversionRate">The current security market conversion rate into the account currency</param>
186  /// <param name="feeInAccountCurrency">The current order fee in the account currency</param>
187  /// <param name="multiplier">The contract multiplier</param>
188  public void ProcessFill(OrderEvent fill,
189  decimal securityConversionRate,
190  decimal feeInAccountCurrency,
191  decimal multiplier = 1.0m)
192  {
193  // If we have multiple fills per order, we assign the order fee only to its first fill
194  // to avoid counting the same order fee multiple times.
195  var orderFee = 0m;
196  if (!_ordersWithFeesAssigned.Contains(fill.OrderId))
197  {
198  orderFee = feeInAccountCurrency;
199  _ordersWithFeesAssigned.Add(fill.OrderId);
200  }
201 
202  switch (_groupingMethod)
203  {
204  case FillGroupingMethod.FillToFill:
205  ProcessFillUsingFillToFill(fill.Clone(), orderFee, securityConversionRate, multiplier);
206  break;
207 
208  case FillGroupingMethod.FlatToFlat:
209  ProcessFillUsingFlatToFlat(fill.Clone(), orderFee, securityConversionRate, multiplier);
210  break;
211 
212  case FillGroupingMethod.FlatToReduced:
213  ProcessFillUsingFlatToReduced(fill.Clone(), orderFee, securityConversionRate, multiplier);
214  break;
215  }
216  }
217 
218  private void ProcessFillUsingFillToFill(OrderEvent fill, decimal orderFee, decimal conversionRate, decimal multiplier)
219  {
220  Position position;
221  if (!_positions.TryGetValue(fill.Symbol, out position) || position.PendingTrades.Count == 0)
222  {
223  // no pending trades for symbol
224  _positions[fill.Symbol] = new Position
225  {
226  PendingTrades = new List<Trade>
227  {
228  new Trade
229  {
230  Symbol = 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  }
237  },
238  MinPrice = fill.FillPrice,
239  MaxPrice = fill.FillPrice
240  };
241  return;
242  }
243 
244  SetMarketPrice(fill.Symbol, fill.FillPrice);
245 
246  var index = _matchingMethod == FillMatchingMethod.FIFO ? 0 : position.PendingTrades.Count - 1;
247 
248  if (Math.Sign(fill.FillQuantity) == (position.PendingTrades[index].Direction == TradeDirection.Long ? +1 : -1))
249  {
250  // execution has same direction of trade
251  position.PendingTrades.Add(new Trade
252  {
253  Symbol = fill.Symbol,
254  EntryTime = fill.UtcTime,
255  EntryPrice = fill.FillPrice,
256  Direction = fill.FillQuantity > 0 ? TradeDirection.Long : TradeDirection.Short,
258  TotalFees = orderFee
259  });
260  }
261  else
262  {
263  // execution has opposite direction of trade
264  var totalExecutedQuantity = 0m;
265  var orderFeeAssigned = false;
266  while (position.PendingTrades.Count > 0 && Math.Abs(totalExecutedQuantity) < fill.AbsoluteFillQuantity)
267  {
268  var trade = position.PendingTrades[index];
269  var absoluteUnexecutedQuantity = fill.AbsoluteFillQuantity - Math.Abs(totalExecutedQuantity);
270 
271  if (absoluteUnexecutedQuantity >= trade.Quantity)
272  {
273  totalExecutedQuantity -= trade.Quantity * (trade.Direction == TradeDirection.Long ? +1 : -1);
274  position.PendingTrades.RemoveAt(index);
275 
276  if (index > 0 && _matchingMethod == FillMatchingMethod.LIFO) index--;
277 
278  trade.ExitTime = fill.UtcTime;
279  trade.ExitPrice = fill.FillPrice;
280  trade.ProfitLoss = Math.Round((trade.ExitPrice - trade.EntryPrice) * trade.Quantity * (trade.Direction == TradeDirection.Long ? +1 : -1) * conversionRate * multiplier, 2);
281  // if closing multiple trades with the same order, assign order fee only once
282  trade.TotalFees += orderFeeAssigned ? 0 : orderFee;
283  trade.MAE = Math.Round((trade.Direction == TradeDirection.Long ? position.MinPrice - trade.EntryPrice : trade.EntryPrice - position.MaxPrice) * trade.Quantity * conversionRate * multiplier, 2);
284  trade.MFE = Math.Round((trade.Direction == TradeDirection.Long ? position.MaxPrice - trade.EntryPrice : trade.EntryPrice - position.MinPrice) * trade.Quantity * conversionRate * multiplier, 2);
285 
286  AddNewTrade(trade, fill);
287  }
288  else
289  {
290  totalExecutedQuantity += absoluteUnexecutedQuantity * (trade.Direction == TradeDirection.Long ? -1 : +1);
291  trade.Quantity -= absoluteUnexecutedQuantity;
292 
293  var newTrade = new Trade
294  {
295  Symbol = trade.Symbol,
296  EntryTime = trade.EntryTime,
297  EntryPrice = trade.EntryPrice,
298  Direction = trade.Direction,
299  Quantity = absoluteUnexecutedQuantity,
300  ExitTime = fill.UtcTime,
301  ExitPrice = fill.FillPrice,
302  ProfitLoss = Math.Round((fill.FillPrice - trade.EntryPrice) * absoluteUnexecutedQuantity * (trade.Direction == TradeDirection.Long ? +1 : -1) * conversionRate * multiplier, 2),
303  TotalFees = trade.TotalFees + (orderFeeAssigned ? 0 : orderFee),
304  MAE = Math.Round((trade.Direction == TradeDirection.Long ? position.MinPrice - trade.EntryPrice : trade.EntryPrice - position.MaxPrice) * absoluteUnexecutedQuantity * conversionRate * multiplier, 2),
305  MFE = Math.Round((trade.Direction == TradeDirection.Long ? position.MaxPrice - trade.EntryPrice : trade.EntryPrice - position.MinPrice) * absoluteUnexecutedQuantity * conversionRate * multiplier, 2)
306  };
307 
308  AddNewTrade(newTrade, fill);
309 
310  trade.TotalFees = 0;
311  }
312 
313  orderFeeAssigned = true;
314  }
315 
316  if (Math.Abs(totalExecutedQuantity) == fill.AbsoluteFillQuantity && position.PendingTrades.Count == 0)
317  {
318  _positions.Remove(fill.Symbol);
319  }
320  else if (Math.Abs(totalExecutedQuantity) < fill.AbsoluteFillQuantity)
321  {
322  // direction reversal
323  fill.FillQuantity -= totalExecutedQuantity;
324  position.PendingTrades = new List<Trade>
325  {
326  new Trade
327  {
328  Symbol = fill.Symbol,
329  EntryTime = fill.UtcTime,
330  EntryPrice = fill.FillPrice,
331  Direction = fill.FillQuantity > 0 ? TradeDirection.Long : TradeDirection.Short,
333  TotalFees = 0
334  }
335  };
336  position.MinPrice = fill.FillPrice;
337  position.MaxPrice = fill.FillPrice;
338  }
339  }
340  }
341 
342  private void ProcessFillUsingFlatToFlat(OrderEvent fill, decimal orderFee, decimal conversionRate, decimal multiplier)
343  {
344  Position position;
345  if (!_positions.TryGetValue(fill.Symbol, out position) || position.PendingFills.Count == 0)
346  {
347  // no pending executions for symbol
348  _positions[fill.Symbol] = new Position
349  {
350  PendingFills = new List<OrderEvent> { fill },
351  TotalFees = orderFee,
352  MinPrice = fill.FillPrice,
353  MaxPrice = fill.FillPrice
354  };
355  return;
356  }
357 
358  SetMarketPrice(fill.Symbol, fill.FillPrice);
359 
360  if (Math.Sign(position.PendingFills[0].FillQuantity) == Math.Sign(fill.FillQuantity))
361  {
362  // execution has same direction of trade
363  position.PendingFills.Add(fill);
364  position.TotalFees += orderFee;
365  }
366  else
367  {
368  // execution has opposite direction of trade
369  if (position.PendingFills.Aggregate(0m, (d, x) => d + x.FillQuantity) + fill.FillQuantity == 0 || fill.AbsoluteFillQuantity > Math.Abs(position.PendingFills.Aggregate(0m, (d, x) => d + x.FillQuantity)))
370  {
371  // trade closed
372  position.PendingFills.Add(fill);
373  position.TotalFees += orderFee;
374 
375  var reverseQuantity = position.PendingFills.Sum(x => x.FillQuantity);
376 
377  var index = _matchingMethod == FillMatchingMethod.FIFO ? 0 : position.PendingFills.Count - 1;
378 
379  var entryTime = position.PendingFills[0].UtcTime;
380  var totalEntryQuantity = 0m;
381  var totalExitQuantity = 0m;
382  var entryAveragePrice = 0m;
383  var exitAveragePrice = 0m;
384 
385  while (position.PendingFills.Count > 0)
386  {
387  if (Math.Sign(position.PendingFills[index].FillQuantity) != Math.Sign(fill.FillQuantity))
388  {
389  // entry
390  totalEntryQuantity += position.PendingFills[index].FillQuantity;
391  entryAveragePrice += (position.PendingFills[index].FillPrice - entryAveragePrice) * position.PendingFills[index].FillQuantity / totalEntryQuantity;
392  }
393  else
394  {
395  // exit
396  totalExitQuantity += position.PendingFills[index].FillQuantity;
397  exitAveragePrice += (position.PendingFills[index].FillPrice - exitAveragePrice) * position.PendingFills[index].FillQuantity / totalExitQuantity;
398  }
399  position.PendingFills.RemoveAt(index);
400 
401  if (_matchingMethod == FillMatchingMethod.LIFO && index > 0) index--;
402  }
403 
404  var direction = Math.Sign(fill.FillQuantity) < 0 ? TradeDirection.Long : TradeDirection.Short;
405  var trade = new Trade
406  {
407  Symbol = fill.Symbol,
408  EntryTime = entryTime,
409  EntryPrice = entryAveragePrice,
410  Direction = direction,
411  Quantity = Math.Abs(totalEntryQuantity),
412  ExitTime = fill.UtcTime,
413  ExitPrice = exitAveragePrice,
414  ProfitLoss = Math.Round((exitAveragePrice - entryAveragePrice) * Math.Abs(totalEntryQuantity) * Math.Sign(totalEntryQuantity) * conversionRate * multiplier, 2),
415  TotalFees = position.TotalFees,
416  MAE = Math.Round((direction == TradeDirection.Long ? position.MinPrice - entryAveragePrice : entryAveragePrice - position.MaxPrice) * Math.Abs(totalEntryQuantity) * conversionRate * multiplier, 2),
417  MFE = Math.Round((direction == TradeDirection.Long ? position.MaxPrice - entryAveragePrice : entryAveragePrice - position.MinPrice) * Math.Abs(totalEntryQuantity) * conversionRate * multiplier, 2),
418  };
419 
420  AddNewTrade(trade, fill);
421 
422  _positions.Remove(fill.Symbol);
423 
424  if (reverseQuantity != 0)
425  {
426  // direction reversal
427  fill.FillQuantity = reverseQuantity;
428  _positions[fill.Symbol] = new Position
429  {
430  PendingFills = new List<OrderEvent> { fill },
431  TotalFees = 0,
432  MinPrice = fill.FillPrice,
433  MaxPrice = fill.FillPrice
434  };
435  }
436  }
437  else
438  {
439  // trade open
440  position.PendingFills.Add(fill);
441  position.TotalFees += orderFee;
442  }
443  }
444  }
445 
446  private void ProcessFillUsingFlatToReduced(OrderEvent fill, decimal orderFee, decimal conversionRate, decimal multiplier)
447  {
448  Position position;
449  if (!_positions.TryGetValue(fill.Symbol, out position) || position.PendingFills.Count == 0)
450  {
451  // no pending executions for symbol
452  _positions[fill.Symbol] = new Position
453  {
454  PendingFills = new List<OrderEvent> { fill },
455  TotalFees = orderFee,
456  MinPrice = fill.FillPrice,
457  MaxPrice = fill.FillPrice
458  };
459  return;
460  }
461 
462  SetMarketPrice(fill.Symbol, fill.FillPrice);
463 
464  var index = _matchingMethod == FillMatchingMethod.FIFO ? 0 : position.PendingFills.Count - 1;
465 
466  if (Math.Sign(fill.FillQuantity) == Math.Sign(position.PendingFills[index].FillQuantity))
467  {
468  // execution has same direction of trade
469  position.PendingFills.Add(fill);
470  position.TotalFees += orderFee;
471  }
472  else
473  {
474  // execution has opposite direction of trade
475  var entryTime = position.PendingFills[index].UtcTime;
476  var totalExecutedQuantity = 0m;
477  var entryPrice = 0m;
478  position.TotalFees += orderFee;
479 
480  while (position.PendingFills.Count > 0 && Math.Abs(totalExecutedQuantity) < fill.AbsoluteFillQuantity)
481  {
482  var absoluteUnexecutedQuantity = fill.AbsoluteFillQuantity - Math.Abs(totalExecutedQuantity);
483  if (absoluteUnexecutedQuantity >= Math.Abs(position.PendingFills[index].FillQuantity))
484  {
485  if (_matchingMethod == FillMatchingMethod.LIFO)
486  entryTime = position.PendingFills[index].UtcTime;
487 
488  totalExecutedQuantity -= position.PendingFills[index].FillQuantity;
489  entryPrice -= (position.PendingFills[index].FillPrice - entryPrice) * position.PendingFills[index].FillQuantity / totalExecutedQuantity;
490  position.PendingFills.RemoveAt(index);
491 
492  if (_matchingMethod == FillMatchingMethod.LIFO && index > 0) index--;
493  }
494  else
495  {
496  var executedQuantity = absoluteUnexecutedQuantity * Math.Sign(fill.FillQuantity);
497  totalExecutedQuantity += executedQuantity;
498  entryPrice += (position.PendingFills[index].FillPrice - entryPrice) * executedQuantity / totalExecutedQuantity;
499  position.PendingFills[index].FillQuantity += executedQuantity;
500  }
501  }
502 
503  var direction = totalExecutedQuantity < 0 ? TradeDirection.Long : TradeDirection.Short;
504  var trade = new Trade
505  {
506  Symbol = fill.Symbol,
507  EntryTime = entryTime,
508  EntryPrice = entryPrice,
509  Direction = direction,
510  Quantity = Math.Abs(totalExecutedQuantity),
511  ExitTime = fill.UtcTime,
512  ExitPrice = fill.FillPrice,
513  ProfitLoss = Math.Round((fill.FillPrice - entryPrice) * Math.Abs(totalExecutedQuantity) * Math.Sign(-totalExecutedQuantity) * conversionRate * multiplier, 2),
514  TotalFees = position.TotalFees,
515  MAE = Math.Round((direction == TradeDirection.Long ? position.MinPrice - entryPrice : entryPrice - position.MaxPrice) * Math.Abs(totalExecutedQuantity) * conversionRate * multiplier, 2),
516  MFE = Math.Round((direction == TradeDirection.Long ? position.MaxPrice - entryPrice : entryPrice - position.MinPrice) * Math.Abs(totalExecutedQuantity) * conversionRate * multiplier, 2)
517  };
518 
519  AddNewTrade(trade, fill);
520 
521  if (Math.Abs(totalExecutedQuantity) < fill.AbsoluteFillQuantity)
522  {
523  // direction reversal
524  fill.FillQuantity -= totalExecutedQuantity;
525  position.PendingFills = new List<OrderEvent> { fill };
526  position.TotalFees = 0;
527  position.MinPrice = fill.FillPrice;
528  position.MaxPrice = fill.FillPrice;
529  }
530  else if (Math.Abs(totalExecutedQuantity) == fill.AbsoluteFillQuantity)
531  {
532  if (position.PendingFills.Count == 0)
533  _positions.Remove(fill.Symbol);
534  else
535  position.TotalFees = 0;
536  }
537  }
538  }
539 
540  /// <summary>
541  /// Adds a trade to the list of closed trades, capping the total number only in live mode
542  /// </summary>
543  private void AddNewTrade(Trade trade, OrderEvent fill)
544  {
545  lock (_closedTrades)
546  {
547  trade.IsWin = _securities != null && _securities.TryGetValue(trade.Symbol, out var security)
548  ? fill.IsWin(security, trade.ProfitLoss)
549  : trade.ProfitLoss > 0;
550 
551  _closedTrades.Add(trade);
552 
553  // Due to memory constraints in live mode, we cap the number of trades
554  if (!_liveMode)
555  return;
556 
557  // maximum number of trades
558  if (_closedTrades.Count > LiveModeMaxTradeCount)
559  {
560  _closedTrades.RemoveRange(0, _closedTrades.Count - LiveModeMaxTradeCount);
561  }
562 
563  // maximum age of trades
564  while (_closedTrades.Count > 0 && _closedTrades[0].ExitTime.Date.AddMonths(LiveModeMaxTradeAgeMonths) < DateTime.Today)
565  {
566  _closedTrades.RemoveAt(0);
567  }
568  }
569  }
570  }
571 }