17 using System.Collections.Generic;
35 private class Position
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; }
45 PendingTrades =
new List<Trade>();
46 PendingFills =
new List<OrderEvent>();
50 private const int LiveModeMaxTradeCount = 10000;
51 private const int LiveModeMaxTradeAgeMonths = 12;
52 private const int MaxOrderIdCacheSize = 1000;
54 private readonly List<Trade> _closedTrades =
new List<Trade>();
55 private readonly Dictionary<Symbol, Position> _positions =
new Dictionary<Symbol, Position>();
60 private bool _liveMode;
67 _groupingMethod = groupingMethod;
68 _matchingMethod = matchingMethod;
86 _securities = securities;
98 return new List<Trade>(_closedTrades);
111 if (!_positions.TryGetValue(symbol, out position))
return false;
114 return position.PendingTrades.Count > 0;
116 return position.PendingFills.Count > 0;
127 if (!_positions.TryGetValue(symbol, out position))
return;
129 if (price > position.MaxPrice)
130 position.MaxPrice = price;
131 else if (price < position.MinPrice)
132 position.MinPrice = price;
146 !_positions.TryGetValue(split.
Symbol, out var position))
154 foreach (var trade
in position.PendingTrades)
156 trade.Quantity /= split.SplitFactor;
157 trade.EntryPrice *= split.SplitFactor;
158 trade.ExitPrice *= split.SplitFactor;
161 foreach (var pendingFill
in position.PendingFills)
163 pendingFill.FillQuantity /= split.SplitFactor;
164 pendingFill.FillPrice *= split.SplitFactor;
166 if (pendingFill.LimitPrice.HasValue)
168 pendingFill.LimitPrice *= split.SplitFactor;
170 if (pendingFill.StopPrice.HasValue)
172 pendingFill.StopPrice *= split.SplitFactor;
174 if (pendingFill.TriggerPrice.HasValue)
176 pendingFill.TriggerPrice *= split.SplitFactor;
189 decimal securityConversionRate,
190 decimal feeInAccountCurrency,
191 decimal multiplier = 1.0m)
196 if (!_ordersWithFeesAssigned.Contains(fill.
OrderId))
198 orderFee = feeInAccountCurrency;
199 _ordersWithFeesAssigned.Add(fill.
OrderId);
202 switch (_groupingMethod)
205 ProcessFillUsingFillToFill(fill.
Clone(), orderFee, securityConversionRate, multiplier);
209 ProcessFillUsingFlatToFlat(fill.
Clone(), orderFee, securityConversionRate, multiplier);
213 ProcessFillUsingFlatToReduced(fill.
Clone(), orderFee, securityConversionRate, multiplier);
218 private void ProcessFillUsingFillToFill(
OrderEvent fill, decimal orderFee, decimal conversionRate, decimal multiplier)
221 if (!_positions.TryGetValue(fill.
Symbol, out position) || position.PendingTrades.Count == 0)
224 _positions[fill.
Symbol] =
new Position
226 PendingTrades =
new List<Trade>
246 var index = _matchingMethod ==
FillMatchingMethod.FIFO ? 0 : position.PendingTrades.Count - 1;
251 position.PendingTrades.Add(
new Trade
264 var totalExecutedQuantity = 0m;
265 var orderFeeAssigned =
false;
266 while (position.PendingTrades.Count > 0 && Math.Abs(totalExecutedQuantity) < fill.
AbsoluteFillQuantity)
268 var trade = position.PendingTrades[index];
271 if (absoluteUnexecutedQuantity >= trade.Quantity)
273 totalExecutedQuantity -= trade.Quantity * (trade.Direction ==
TradeDirection.Long ? +1 : -1);
274 position.PendingTrades.RemoveAt(index);
280 trade.ProfitLoss = Math.Round((trade.ExitPrice - trade.EntryPrice) * trade.Quantity * (trade.Direction ==
TradeDirection.Long ? +1 : -1) * conversionRate * multiplier, 2);
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);
286 AddNewTrade(trade, fill);
290 totalExecutedQuantity += absoluteUnexecutedQuantity * (trade.Direction ==
TradeDirection.Long ? -1 : +1);
291 trade.Quantity -= absoluteUnexecutedQuantity;
293 var newTrade =
new Trade
295 Symbol = trade.Symbol,
296 EntryTime = trade.EntryTime,
297 EntryPrice = trade.EntryPrice,
299 Quantity = absoluteUnexecutedQuantity,
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)
308 AddNewTrade(newTrade, fill);
313 orderFeeAssigned =
true;
316 if (Math.Abs(totalExecutedQuantity) == fill.
AbsoluteFillQuantity && position.PendingTrades.Count == 0)
318 _positions.Remove(fill.
Symbol);
324 position.PendingTrades =
new List<Trade>
342 private void ProcessFillUsingFlatToFlat(
OrderEvent fill, decimal orderFee, decimal conversionRate, decimal multiplier)
345 if (!_positions.TryGetValue(fill.
Symbol, out position) || position.PendingFills.Count == 0)
348 _positions[fill.
Symbol] =
new Position
350 PendingFills =
new List<OrderEvent> { fill },
351 TotalFees = orderFee,
360 if (Math.Sign(position.PendingFills[0].FillQuantity) == Math.Sign(fill.
FillQuantity))
363 position.PendingFills.Add(fill);
364 position.TotalFees += orderFee;
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)))
372 position.PendingFills.Add(fill);
373 position.TotalFees += orderFee;
375 var reverseQuantity = position.PendingFills.Sum(x => x.FillQuantity);
377 var index = _matchingMethod ==
FillMatchingMethod.FIFO ? 0 : position.PendingFills.Count - 1;
379 var entryTime = position.PendingFills[0].UtcTime;
380 var totalEntryQuantity = 0m;
381 var totalExitQuantity = 0m;
382 var entryAveragePrice = 0m;
383 var exitAveragePrice = 0m;
385 while (position.PendingFills.Count > 0)
387 if (Math.Sign(position.PendingFills[index].FillQuantity) != Math.Sign(fill.
FillQuantity))
390 totalEntryQuantity += position.PendingFills[index].FillQuantity;
391 entryAveragePrice += (position.PendingFills[index].FillPrice - entryAveragePrice) * position.PendingFills[index].FillQuantity / totalEntryQuantity;
396 totalExitQuantity += position.PendingFills[index].FillQuantity;
397 exitAveragePrice += (position.PendingFills[index].FillPrice - exitAveragePrice) * position.PendingFills[index].FillQuantity / totalExitQuantity;
399 position.PendingFills.RemoveAt(index);
405 var trade =
new Trade
408 EntryTime = entryTime,
409 EntryPrice = entryAveragePrice,
411 Quantity = Math.Abs(totalEntryQuantity),
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),
420 AddNewTrade(trade, fill);
422 _positions.Remove(fill.
Symbol);
424 if (reverseQuantity != 0)
428 _positions[fill.
Symbol] =
new Position
430 PendingFills =
new List<OrderEvent> { fill },
440 position.PendingFills.Add(fill);
441 position.TotalFees += orderFee;
446 private void ProcessFillUsingFlatToReduced(
OrderEvent fill, decimal orderFee, decimal conversionRate, decimal multiplier)
449 if (!_positions.TryGetValue(fill.
Symbol, out position) || position.PendingFills.Count == 0)
452 _positions[fill.
Symbol] =
new Position
454 PendingFills =
new List<OrderEvent> { fill },
455 TotalFees = orderFee,
464 var index = _matchingMethod ==
FillMatchingMethod.FIFO ? 0 : position.PendingFills.Count - 1;
466 if (Math.Sign(fill.
FillQuantity) == Math.Sign(position.PendingFills[index].FillQuantity))
469 position.PendingFills.Add(fill);
470 position.TotalFees += orderFee;
475 var entryTime = position.PendingFills[index].UtcTime;
476 var totalExecutedQuantity = 0m;
478 position.TotalFees += orderFee;
480 while (position.PendingFills.Count > 0 && Math.Abs(totalExecutedQuantity) < fill.
AbsoluteFillQuantity)
483 if (absoluteUnexecutedQuantity >= Math.Abs(position.PendingFills[index].FillQuantity))
486 entryTime = position.PendingFills[index].UtcTime;
488 totalExecutedQuantity -= position.PendingFills[index].FillQuantity;
489 entryPrice -= (position.PendingFills[index].FillPrice - entryPrice) * position.PendingFills[index].FillQuantity / totalExecutedQuantity;
490 position.PendingFills.RemoveAt(index);
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;
504 var trade =
new Trade
507 EntryTime = entryTime,
508 EntryPrice = entryPrice,
510 Quantity = Math.Abs(totalExecutedQuantity),
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)
519 AddNewTrade(trade, fill);
525 position.PendingFills =
new List<OrderEvent> { fill };
526 position.TotalFees = 0;
532 if (position.PendingFills.Count == 0)
533 _positions.Remove(fill.
Symbol);
535 position.TotalFees = 0;
543 private void AddNewTrade(Trade trade,
OrderEvent fill)
547 trade.IsWin = _securities !=
null && _securities.
TryGetValue(trade.Symbol, out var security)
548 ? fill.IsWin(security, trade.ProfitLoss)
549 : trade.ProfitLoss > 0;
551 _closedTrades.Add(trade);
558 if (_closedTrades.Count > LiveModeMaxTradeCount)
560 _closedTrades.RemoveRange(0, _closedTrades.Count - LiveModeMaxTradeCount);
564 while (_closedTrades.Count > 0 && _closedTrades[0].ExitTime.Date.AddMonths(LiveModeMaxTradeAgeMonths) < DateTime.Today)
566 _closedTrades.RemoveAt(0);