/* * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using NUnit.Framework; using QuantConnect.Data; using QuantConnect.Data.Market; using QuantConnect.Orders; using QuantConnect.Orders.Fees; using QuantConnect.Securities; using QuantConnect.Securities.Option; using QuantConnect.Statistics; namespace QuantConnect.Tests.Common.Statistics { [TestFixture] public class TradeBuilderTests { private readonly OrderFee _orderFee = new OrderFee(new CashAmount(1, Currencies.USD)); private const decimal ConversionRate = 1; private readonly DateTime _startTime = new DateTime(2015, 08, 06, 15, 30, 0); private SecurityManager _securityManager; [SetUp] public void SetUp() { _securityManager = new SecurityManager(new TimeKeeper(_startTime)); } [Test] public void AllInAllOutLong( [Values] FillGroupingMethod groupingMethod, [Values] FillMatchingMethod matchingMethod, // 0 for no split [Values(0, 0.5, 0.333)] double splitFactor) { // Buy 1k, Sell 1k var builder = new TradeBuilder(groupingMethod, matchingMethod); builder.SetSecurityManager(_securityManager); var time = _startTime; // Buy 1k builder.ProcessFill( new OrderEvent(1, Symbols.SPY, time, OrderStatus.Filled, OrderDirection.Buy, fillPrice: 1.08m, fillQuantity: 1000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.075m); builder.SetMarketPrice(Symbols.SPY, 1.10m); Split split = null; if (splitFactor != 0) { // apply a 2:1 split split = new Split(Symbols.SPY, time.AddMinutes(5), 1.10m, (decimal)splitFactor, SplitType.SplitOccurred); builder.ApplySplit(split, false, DataNormalizationMode.Raw); } // Sell 1k builder.ProcessFill( new OrderEvent(2, Symbols.SPY, time.AddMinutes(10), OrderStatus.Filled, OrderDirection.Sell, fillPrice: AdjustPriceToSplit(1.09m, split), fillQuantity: AdjustQuantityToSplit(-1000, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsFalse(builder.HasOpenPosition(Symbols.SPY)); Assert.AreEqual(1, builder.ClosedTrades.Count); var trade = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade.Symbol); Assert.AreEqual(time, trade.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.08m, split), trade.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade.Quantity); Assert.AreEqual(time.AddMinutes(10), trade.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade.ExitPrice); Assert.AreEqual(10, trade.ProfitLoss); Assert.AreEqual(2, trade.TotalFees); if (groupingMethod == FillGroupingMethod.FillToFill) { Assert.AreEqual(-5, trade.MAE); Assert.AreEqual(20m, trade.MFE); } else { Assert.AreEqual(0, trade.MAE); Assert.AreEqual(0, trade.MFE); } CollectionAssert.AreEquivalent(new[] { 1, 2 }, trade.OrderIds); } [Test] public void AllInAllOutShort( [Values] FillGroupingMethod groupingMethod, [Values] FillMatchingMethod matchingMethod, // 0 for no split [Values(0, 0.5, 0.333)] double splitFactor) { // Sell 1k, Buy 1k var builder = new TradeBuilder(groupingMethod, matchingMethod); builder.SetSecurityManager(_securityManager); var time = _startTime; // Sell 1k builder.ProcessFill( new OrderEvent(1, Symbols.SPY, time, OrderStatus.Filled, OrderDirection.Sell, fillPrice: 1.08m, fillQuantity: -1000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.075m); builder.SetMarketPrice(Symbols.SPY, 1.10m); Split split = null; if (splitFactor != 0) { // apply a 2:1 split split = new Split(Symbols.SPY, time.AddMinutes(5), 1.10m, (decimal)splitFactor, SplitType.SplitOccurred); builder.ApplySplit(split, false, DataNormalizationMode.Raw); } // Buy 1k builder.ProcessFill( new OrderEvent(2, Symbols.SPY, time.AddMinutes(10), OrderStatus.Filled, OrderDirection.Buy, fillPrice: AdjustPriceToSplit(1.09m, split), fillQuantity: AdjustQuantityToSplit(1000, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsFalse(builder.HasOpenPosition(Symbols.SPY)); Assert.AreEqual(1, builder.ClosedTrades.Count); var trade = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade.Symbol); Assert.AreEqual(time, trade.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.08m, split), trade.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade.Quantity); Assert.AreEqual(time.AddMinutes(10), trade.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade.ExitPrice); Assert.AreEqual(-10, trade.ProfitLoss); Assert.AreEqual(2, trade.TotalFees); if (groupingMethod == FillGroupingMethod.FillToFill) { Assert.AreEqual(-20, trade.MAE); Assert.AreEqual(5, trade.MFE); } else { Assert.AreEqual(0, trade.MAE); Assert.AreEqual(0, trade.MFE); } CollectionAssert.AreEquivalent(new[] { 1, 2 }, trade.OrderIds); } [Test] public void ScaleInAllOutLong( [Values] FillGroupingMethod groupingMethod, [Values] FillMatchingMethod matchingMethod, // 0 for no split [Values(0, 0.5, 0.333)] double splitFactor) { // Buy 1k, Buy 1k, Sell 2k var builder = new TradeBuilder(groupingMethod, matchingMethod); builder.SetSecurityManager(_securityManager); var time = _startTime; // Buy 1k builder.ProcessFill( new OrderEvent(1, Symbols.SPY, time, OrderStatus.Filled, OrderDirection.Buy, fillPrice: 1.08m, fillQuantity: 1000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.075m); // Buy 1k builder.ProcessFill( new OrderEvent(2, Symbols.SPY, time.AddMinutes(10), OrderStatus.Filled, OrderDirection.Buy, fillPrice: 1.07m, fillQuantity: 1000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.065m); builder.SetMarketPrice(Symbols.SPY, 1.10m); Split split = null; if (splitFactor != 0) { // apply a 2:1 split split = new Split(Symbols.SPY, time.AddMinutes(5), 1.10m, (decimal)splitFactor, SplitType.SplitOccurred); builder.ApplySplit(split, false, DataNormalizationMode.Raw); } // Sell 2k builder.ProcessFill( new OrderEvent(3, Symbols.SPY, time.AddMinutes(20), OrderStatus.Filled, OrderDirection.Sell, fillPrice: AdjustPriceToSplit(1.09m, split), fillQuantity: AdjustQuantityToSplit(-2000, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsFalse(builder.HasOpenPosition(Symbols.SPY)); if (groupingMethod == FillGroupingMethod.FillToFill) { Assert.AreEqual(2, builder.ClosedTrades.Count); var trade1 = builder.ClosedTrades[matchingMethod == FillMatchingMethod.FIFO ? 0 : 1]; Assert.AreEqual(Symbols.SPY, trade1.Symbol); Assert.AreEqual(time, trade1.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.08m, split), trade1.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade1.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade1.Quantity); Assert.AreEqual(time.AddMinutes(20), trade1.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade1.ExitPrice); Assert.AreEqual(10, trade1.ProfitLoss); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 2 : 1, trade1.TotalFees); Assert.AreEqual(-15, trade1.MAE); Assert.AreEqual(20, trade1.MFE); CollectionAssert.AreEquivalent(new[] { 1, 3 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[matchingMethod == FillMatchingMethod.FIFO ? 1 : 0]; Assert.AreEqual(Symbols.SPY, trade2.Symbol); Assert.AreEqual(time.AddMinutes(10), trade2.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.07m, split), trade2.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade2.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade2.Quantity); Assert.AreEqual(time.AddMinutes(20), trade2.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(20, trade2.ProfitLoss); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 1 : 2, trade2.TotalFees); Assert.AreEqual(-5, trade2.MAE); Assert.AreEqual(30, trade2.MFE); CollectionAssert.AreEquivalent(new[] { 2, 3 }, trade2.OrderIds); } else { Assert.AreEqual(1, builder.ClosedTrades.Count); var trade = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade.Symbol); Assert.AreEqual(time, trade.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.075m, split), trade.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade.Direction); Assert.AreEqual(AdjustQuantityToSplit(2000, split), trade.Quantity); Assert.AreEqual(time.AddMinutes(20), trade.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade.ExitPrice); Assert.AreEqual(30, trade.ProfitLoss); Assert.AreEqual(3, trade.TotalFees); Assert.AreEqual(0, trade.MAE); Assert.AreEqual(0, trade.MFE); CollectionAssert.AreEquivalent(new[] { 1, 2, 3 }, trade.OrderIds); } } [Test] public void ScaleInAllOutShort( [Values] FillGroupingMethod groupingMethod, [Values] FillMatchingMethod matchingMethod, // 0 for no split [Values(0, 0.5, 0.333)] double splitFactor) { // Sell 1k, Sell 1k, Buy 2k var builder = new TradeBuilder(groupingMethod, matchingMethod); builder.SetSecurityManager(_securityManager); var time = _startTime; // Sell 1k builder.ProcessFill( new OrderEvent(1, Symbols.SPY, time, OrderStatus.Filled, OrderDirection.Sell, fillPrice: 1.08m, fillQuantity: -1000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.075m); // Sell 1k builder.ProcessFill( new OrderEvent(2, Symbols.SPY, time.AddMinutes(10), OrderStatus.Filled, OrderDirection.Sell, fillPrice: 1.07m, fillQuantity: -1000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.065m); builder.SetMarketPrice(Symbols.SPY, 1.10m); Split split = null; if (splitFactor != 0) { // apply a 2:1 split split = new Split(Symbols.SPY, time.AddMinutes(5), 1.10m, (decimal)splitFactor, SplitType.SplitOccurred); builder.ApplySplit(split, false, DataNormalizationMode.Raw); } // Buy 2k builder.ProcessFill( new OrderEvent(3, Symbols.SPY, time.AddMinutes(20), OrderStatus.Filled, OrderDirection.Buy, fillPrice: AdjustPriceToSplit(1.09m, split), fillQuantity: AdjustQuantityToSplit(2000, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsFalse(builder.HasOpenPosition(Symbols.SPY)); if (groupingMethod == FillGroupingMethod.FillToFill) { Assert.AreEqual(2, builder.ClosedTrades.Count); var trade1 = builder.ClosedTrades[matchingMethod == FillMatchingMethod.FIFO ? 0 : 1]; Assert.AreEqual(Symbols.SPY, trade1.Symbol); Assert.AreEqual(time, trade1.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.08m, split), trade1.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade1.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade1.Quantity); Assert.AreEqual(time.AddMinutes(20), trade1.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade1.ExitPrice); Assert.AreEqual(-10, trade1.ProfitLoss); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 2 : 1, trade1.TotalFees); Assert.AreEqual(-20, trade1.MAE); Assert.AreEqual(15, trade1.MFE); CollectionAssert.AreEquivalent(new[] { 1, 3 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[matchingMethod == FillMatchingMethod.FIFO ? 1 : 0]; Assert.AreEqual(Symbols.SPY, trade2.Symbol); Assert.AreEqual(time.AddMinutes(10), trade2.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.07m, split), trade2.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade2.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade2.Quantity); Assert.AreEqual(time.AddMinutes(20), trade2.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(-20, trade2.ProfitLoss); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 1 : 2, trade2.TotalFees); Assert.AreEqual(-30, trade2.MAE); Assert.AreEqual(5, trade2.MFE); CollectionAssert.AreEquivalent(new[] { 2, 3 }, trade2.OrderIds); } else { Assert.AreEqual(1, builder.ClosedTrades.Count); var trade = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade.Symbol); Assert.AreEqual(time, trade.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.075m, split), trade.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade.Direction); Assert.AreEqual(AdjustQuantityToSplit(2000, split), trade.Quantity); Assert.AreEqual(time.AddMinutes(20), trade.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade.ExitPrice); Assert.AreEqual(-30, trade.ProfitLoss); Assert.AreEqual(3, trade.TotalFees); Assert.AreEqual(0, trade.MAE); Assert.AreEqual(0, trade.MFE); CollectionAssert.AreEquivalent(new[] { 1, 2, 3 }, trade.OrderIds); } } [Test] public void AllInScaleOutLong( [Values] FillGroupingMethod groupingMethod, [Values] FillMatchingMethod matchingMethod, // 0 for no split [Values(0, 0.5, 0.333)] double splitFactor) { // Buy 2k, Sell 1k, Sell 1k var builder = new TradeBuilder(groupingMethod, matchingMethod); builder.SetSecurityManager(_securityManager); var time = _startTime; // Buy 2k builder.ProcessFill( new OrderEvent(1, Symbols.SPY, time, OrderStatus.Filled, OrderDirection.Buy, fillPrice: 1.07m, fillQuantity: 2000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.075m); // Sell 1k builder.ProcessFill( new OrderEvent(2, Symbols.SPY, time.AddMinutes(10), OrderStatus.Filled, OrderDirection.Sell, fillPrice: 1.08m, fillQuantity: -1000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.065m); builder.SetMarketPrice(Symbols.SPY, 1.10m); Split split = null; if (splitFactor != 0) { // apply a 2:1 split split = new Split(Symbols.SPY, time.AddMinutes(5), 1.10m, (decimal)splitFactor, SplitType.SplitOccurred); builder.ApplySplit(split, false, DataNormalizationMode.Raw); } // Sell 1k builder.ProcessFill( new OrderEvent(3, Symbols.SPY, time.AddMinutes(20), OrderStatus.Filled, OrderDirection.Sell, fillPrice: AdjustPriceToSplit(1.09m, split), fillQuantity: AdjustQuantityToSplit(-1000, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsFalse(builder.HasOpenPosition(Symbols.SPY)); if (groupingMethod == FillGroupingMethod.FlatToFlat) { Assert.AreEqual(1, builder.ClosedTrades.Count); var trade = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade.Symbol); Assert.AreEqual(time, trade.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.07m, split), trade.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade.Direction); Assert.AreEqual(AdjustQuantityToSplit(2000, split), trade.Quantity); Assert.AreEqual(time.AddMinutes(20), trade.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.085m, split), trade.ExitPrice); Assert.AreEqual(30, trade.ProfitLoss); Assert.AreEqual(3, trade.TotalFees); Assert.AreEqual(0, trade.MAE); Assert.AreEqual(0, trade.MFE); CollectionAssert.AreEquivalent(new[] { 1, 2, 3 }, trade.OrderIds); } else { Assert.AreEqual(2, builder.ClosedTrades.Count); // This first trade was closed before the split var trade1 = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade1.Symbol); Assert.AreEqual(time, trade1.EntryTime); Assert.AreEqual(1.07m, trade1.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade1.Direction); Assert.AreEqual(1000, trade1.Quantity); Assert.AreEqual(time.AddMinutes(10), trade1.ExitTime); Assert.AreEqual(1.08m, trade1.ExitPrice); Assert.AreEqual(10, trade1.ProfitLoss); Assert.AreEqual(2, trade1.TotalFees); if (groupingMethod == FillGroupingMethod.FillToFill) { Assert.AreEqual(0, trade1.MAE); Assert.AreEqual(10, trade1.MFE); } else { Assert.AreEqual(0, trade1.MAE); Assert.AreEqual(0, trade1.MFE); } CollectionAssert.AreEquivalent(new[] { 1, 2 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; Assert.AreEqual(Symbols.SPY, trade2.Symbol); Assert.AreEqual(time, trade2.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.07m, split), trade2.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade2.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade2.Quantity); Assert.AreEqual(time.AddMinutes(20), trade2.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(20, trade2.ProfitLoss); Assert.AreEqual(1, trade2.TotalFees); if (groupingMethod == FillGroupingMethod.FillToFill) { Assert.AreEqual(-5, trade2.MAE); Assert.AreEqual(30, trade2.MFE); } else { Assert.AreEqual(0, trade2.MAE); Assert.AreEqual(0, trade2.MFE); } CollectionAssert.AreEquivalent(groupingMethod == FillGroupingMethod.FlatToReduced ? [1, 3] : new[] { 1, 3 }, trade2.OrderIds); } } [Test] public void AllInScaleOutShort( [Values] FillGroupingMethod groupingMethod, [Values] FillMatchingMethod matchingMethod, // 0 for no split [Values(0, 0.5, 0.333)] double splitFactor) { // Sell 2k, Buy 1k, Buy 1k var builder = new TradeBuilder(groupingMethod, matchingMethod); builder.SetSecurityManager(_securityManager); var time = _startTime; // Sell 2k builder.ProcessFill( new OrderEvent(1, Symbols.SPY, time, OrderStatus.Filled, OrderDirection.Sell, fillPrice: 1.07m, fillQuantity: -2000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.075m); // Buy 1k builder.ProcessFill( new OrderEvent(2, Symbols.SPY, time.AddMinutes(10), OrderStatus.Filled, OrderDirection.Buy, fillPrice: 1.08m, fillQuantity: 1000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.065m); builder.SetMarketPrice(Symbols.SPY, 1.10m); Split split = null; if (splitFactor != 0) { // apply a 2:1 split split = new Split(Symbols.SPY, time.AddMinutes(5), 1.10m, (decimal)splitFactor, SplitType.SplitOccurred); builder.ApplySplit(split, false, DataNormalizationMode.Raw); } // Buy 1k builder.ProcessFill( new OrderEvent(3, Symbols.SPY, time.AddMinutes(20), OrderStatus.Filled, OrderDirection.Buy, fillPrice: AdjustPriceToSplit(1.09m, split), fillQuantity: AdjustQuantityToSplit(1000, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsFalse(builder.HasOpenPosition(Symbols.SPY)); if (groupingMethod == FillGroupingMethod.FlatToFlat) { Assert.AreEqual(1, builder.ClosedTrades.Count); var trade = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade.Symbol); Assert.AreEqual(time, trade.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.07m, split), trade.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade.Direction); Assert.AreEqual(AdjustQuantityToSplit(2000, split), trade.Quantity); Assert.AreEqual(time.AddMinutes(20), trade.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.085m, split), trade.ExitPrice); Assert.AreEqual(-30, trade.ProfitLoss); Assert.AreEqual(3, trade.TotalFees); Assert.AreEqual(0, trade.MAE); Assert.AreEqual(0, trade.MFE); CollectionAssert.AreEquivalent(new[] { 1, 2, 3 }, trade.OrderIds); } else { Assert.AreEqual(2, builder.ClosedTrades.Count); // This first trade was closed before the split var trade1 = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade1.Symbol); Assert.AreEqual(time, trade1.EntryTime); Assert.AreEqual(1.07m, trade1.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade1.Direction); Assert.AreEqual(1000, trade1.Quantity); Assert.AreEqual(time.AddMinutes(10), trade1.ExitTime); Assert.AreEqual(1.08m, trade1.ExitPrice); Assert.AreEqual(-10, trade1.ProfitLoss); Assert.AreEqual(2, trade1.TotalFees); if (groupingMethod == FillGroupingMethod.FillToFill) { Assert.AreEqual(-10, trade1.MAE); Assert.AreEqual(0, trade1.MFE); } else { Assert.AreEqual(0, trade1.MAE); Assert.AreEqual(0, trade1.MFE); } CollectionAssert.AreEquivalent(new[] { 1, 2 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; Assert.AreEqual(Symbols.SPY, trade2.Symbol); Assert.AreEqual(time, trade2.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.07m, split), trade2.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade2.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade2.Quantity); Assert.AreEqual(time.AddMinutes(20), trade2.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(-20, trade2.ProfitLoss); Assert.AreEqual(1, trade2.TotalFees); if (groupingMethod == FillGroupingMethod.FillToFill) { Assert.AreEqual(-30, trade2.MAE); Assert.AreEqual(5, trade2.MFE); } else { Assert.AreEqual(0, trade2.MAE); Assert.AreEqual(0, trade2.MFE); } CollectionAssert.AreEquivalent(new[] { 1, 3 }, trade2.OrderIds); } } [Test] public void ReversalLongToShort( [Values] FillGroupingMethod groupingMethod, [Values] FillMatchingMethod matchingMethod, // 0 for no split [Values(0, 0.5, 0.333)] double splitFactor) { // Buy 1k, Sell 2k, Buy 1k var builder = new TradeBuilder(groupingMethod, matchingMethod); builder.SetSecurityManager(_securityManager); var time = _startTime; // Buy 1k builder.ProcessFill( new OrderEvent(1, Symbols.SPY, time, OrderStatus.Filled, OrderDirection.Buy, fillPrice: 1.07m, fillQuantity: 1000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.075m); // Sell 2k builder.ProcessFill( new OrderEvent(2, Symbols.SPY, time.AddMinutes(10), OrderStatus.Filled, OrderDirection.Sell, fillPrice: 1.08m, fillQuantity: -2000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.065m); builder.SetMarketPrice(Symbols.SPY, 1.10m); Split split = null; if (splitFactor != 0) { // apply a 2:1 split split = new Split(Symbols.SPY, time.AddMinutes(5), 1.10m, (decimal)splitFactor, SplitType.SplitOccurred); builder.ApplySplit(split, false, DataNormalizationMode.Raw); } // Buy 1k builder.ProcessFill( new OrderEvent(3, Symbols.SPY, time.AddMinutes(20), OrderStatus.Filled, OrderDirection.Buy, fillPrice: AdjustPriceToSplit(1.09m, split), fillQuantity: AdjustQuantityToSplit(1000, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsFalse(builder.HasOpenPosition(Symbols.SPY)); Assert.AreEqual(2, builder.ClosedTrades.Count); // This first trade was closed before the split var trade1 = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade1.Symbol); Assert.AreEqual(time, trade1.EntryTime); Assert.AreEqual(1.07m, trade1.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade1.Direction); Assert.AreEqual(1000, trade1.Quantity); Assert.AreEqual(time.AddMinutes(10), trade1.ExitTime); Assert.AreEqual(1.08m, trade1.ExitPrice); Assert.AreEqual(10, trade1.ProfitLoss); Assert.AreEqual(2, trade1.TotalFees); if (groupingMethod == FillGroupingMethod.FillToFill) { Assert.AreEqual(0, trade1.MAE); Assert.AreEqual(10, trade1.MFE); } else { Assert.AreEqual(0, trade1.MAE); Assert.AreEqual(0, trade1.MFE); } CollectionAssert.AreEquivalent(new[] { 1, 2 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; Assert.AreEqual(Symbols.SPY, trade2.Symbol); Assert.AreEqual(time.AddMinutes(10), trade2.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.08m, split), trade2.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade2.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade2.Quantity); Assert.AreEqual(time.AddMinutes(20), trade2.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(-10, trade2.ProfitLoss); Assert.AreEqual(1, trade2.TotalFees); if (groupingMethod == FillGroupingMethod.FillToFill) { Assert.AreEqual(-20, trade2.MAE); Assert.AreEqual(15, trade2.MFE); } else { Assert.AreEqual(0, trade2.MAE); Assert.AreEqual(0, trade2.MFE); } CollectionAssert.AreEquivalent(new[] { 2, 3 }, trade2.OrderIds); } [Test] public void ReversalShortToLong( [Values] FillGroupingMethod groupingMethod, [Values] FillMatchingMethod matchingMethod, // 0 for no split [Values(0, 0.5, 0.333)] double splitFactor) { // Sell 1k, Buy 2k, Sell 1k var builder = new TradeBuilder(groupingMethod, matchingMethod); builder.SetSecurityManager(_securityManager); var time = _startTime; // Sell 1k builder.ProcessFill( new OrderEvent(1, Symbols.SPY, time, OrderStatus.Filled, OrderDirection.Sell, fillPrice: 1.07m, fillQuantity: -1000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.075m); // Buy 2k builder.ProcessFill( new OrderEvent(2, Symbols.SPY, time.AddMinutes(10), OrderStatus.Filled, OrderDirection.Buy, fillPrice: 1.08m, fillQuantity: 2000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.065m); builder.SetMarketPrice(Symbols.SPY, 1.10m); Split split = null; if (splitFactor != 0) { // apply a 2:1 split split = new Split(Symbols.SPY, time.AddMinutes(5), 1.10m, (decimal)splitFactor, SplitType.SplitOccurred); builder.ApplySplit(split, false, DataNormalizationMode.Raw); } // Sell 1k builder.ProcessFill( new OrderEvent(3, Symbols.SPY, time.AddMinutes(20), OrderStatus.Filled, OrderDirection.Sell, fillPrice: AdjustPriceToSplit(1.09m, split), fillQuantity: AdjustQuantityToSplit(-1000, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsFalse(builder.HasOpenPosition(Symbols.SPY)); Assert.AreEqual(2, builder.ClosedTrades.Count); // This first trade was closed before the split var trade1 = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade1.Symbol); Assert.AreEqual(time, trade1.EntryTime); Assert.AreEqual(1.07m, trade1.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade1.Direction); Assert.AreEqual(1000, trade1.Quantity); Assert.AreEqual(time.AddMinutes(10), trade1.ExitTime); Assert.AreEqual(1.08m, trade1.ExitPrice); Assert.AreEqual(-10, trade1.ProfitLoss); Assert.AreEqual(2, trade1.TotalFees); if (groupingMethod == FillGroupingMethod.FillToFill) { Assert.AreEqual(-10, trade1.MAE); Assert.AreEqual(0, trade1.MFE); } else { Assert.AreEqual(0, trade1.MAE); Assert.AreEqual(0, trade1.MFE); } CollectionAssert.AreEquivalent(new[] { 1, 2 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; Assert.AreEqual(Symbols.SPY, trade2.Symbol); Assert.AreEqual(time.AddMinutes(10), trade2.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.08m, split), trade2.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade2.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade2.Quantity); Assert.AreEqual(time.AddMinutes(20), trade2.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(10, trade2.ProfitLoss); Assert.AreEqual(1, trade2.TotalFees); if (groupingMethod == FillGroupingMethod.FillToFill) { Assert.AreEqual(-15, trade2.MAE); Assert.AreEqual(20, trade2.MFE); } else { Assert.AreEqual(0, trade2.MAE); Assert.AreEqual(0, trade2.MFE); } CollectionAssert.AreEquivalent(new[] { 2, 3 }, trade2.OrderIds); } [Test] public void ScaleInScaleOut1Long( [Values] FillGroupingMethod groupingMethod, [Values] FillMatchingMethod matchingMethod, // 0 for no split [Values(0, 0.5, 0.333)] double splitFactor) { // Buy 1k, Buy 1k, Sell 1k, Buy 1k, Sell 2k var builder = new TradeBuilder(groupingMethod, matchingMethod); builder.SetSecurityManager(_securityManager); var time = _startTime; // Buy 1k builder.ProcessFill( new OrderEvent(1, Symbols.SPY, time, OrderStatus.Filled, OrderDirection.Buy, fillPrice: 1.07m, fillQuantity: 1000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.075m); // Buy 1k builder.ProcessFill( new OrderEvent(2, Symbols.SPY, time.AddMinutes(10), OrderStatus.Filled, OrderDirection.Buy, fillPrice: 1.08m, fillQuantity: 1000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.065m); builder.SetMarketPrice(Symbols.SPY, 1.10m); Split split = null; if (splitFactor != 0) { // apply a 2:1 split split = new Split(Symbols.SPY, time.AddMinutes(5), 1.10m, (decimal)splitFactor, SplitType.SplitOccurred); builder.ApplySplit(split, false, DataNormalizationMode.Raw); } // Sell 1k builder.ProcessFill( new OrderEvent(3, Symbols.SPY, time.AddMinutes(20), OrderStatus.Filled, OrderDirection.Sell, fillPrice: AdjustPriceToSplit(1.09m, split), fillQuantity: AdjustQuantityToSplit(-1000, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); // Buy 1k builder.ProcessFill( new OrderEvent(4, Symbols.SPY, time.AddMinutes(30), OrderStatus.Filled, OrderDirection.Buy, fillPrice: AdjustPriceToSplit(1.08m, split), fillQuantity: AdjustQuantityToSplit(1000, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); // Sell 2k builder.ProcessFill( new OrderEvent(5, Symbols.SPY, time.AddMinutes(40), OrderStatus.Filled, OrderDirection.Sell, fillPrice: AdjustPriceToSplit(1.09m, split), fillQuantity: AdjustQuantityToSplit(-2000, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsFalse(builder.HasOpenPosition(Symbols.SPY)); switch (groupingMethod) { case FillGroupingMethod.FillToFill: { Assert.AreEqual(3, builder.ClosedTrades.Count); var trade1 = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade1.Symbol); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? time : time.AddMinutes(10), trade1.EntryTime); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? AdjustPriceToSplit(1.07m, split) : AdjustPriceToSplit(1.08m, split), trade1.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade1.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade1.Quantity); Assert.AreEqual(time.AddMinutes(20), trade1.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade1.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 20 : 10, trade1.ProfitLoss); Assert.AreEqual(2, trade1.TotalFees); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -5 : -15, trade1.MAE); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 30 : 20, trade1.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [1, 3] : new[] { 2, 3 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; Assert.AreEqual(Symbols.SPY, trade2.Symbol); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? time.AddMinutes(10) : time.AddMinutes(30), trade2.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.08m, split), trade2.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade2.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade2.Quantity); Assert.AreEqual(time.AddMinutes(40), trade2.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(10, trade2.ProfitLoss); Assert.AreEqual(2, trade2.TotalFees); Assert.AreEqual(-15, trade2.MAE); Assert.AreEqual(20, trade2.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [2, 5] : new[] { 4, 5 }, trade2.OrderIds); var trade3 = builder.ClosedTrades[2]; Assert.AreEqual(Symbols.SPY, trade3.Symbol); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? time.AddMinutes(30) : time, trade3.EntryTime); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? AdjustPriceToSplit(1.08m, split) : AdjustPriceToSplit(1.07m, split), trade3.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade3.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade3.Quantity); Assert.AreEqual(time.AddMinutes(40), trade3.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade3.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 10 : 20, trade3.ProfitLoss); Assert.AreEqual(1, trade3.TotalFees); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -15 : -5, trade3.MAE); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 20 : 30, trade3.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [4, 5] : new[] { 1, 5 }, trade3.OrderIds); } break; case FillGroupingMethod.FlatToFlat: { Assert.AreEqual(1, builder.ClosedTrades.Count); var trade = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade.Symbol); Assert.AreEqual(time, trade.EntryTime); Assert.Less( Math.Abs(AdjustPriceToSplit(1.0766666666666666666666666667m, split) - trade.EntryPrice), 1e-27m); Assert.AreEqual(TradeDirection.Long, trade.Direction); Assert.AreEqual(AdjustQuantityToSplit(3000, split), trade.Quantity); Assert.AreEqual(time.AddMinutes(40), trade.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade.ExitPrice); Assert.AreEqual(40, trade.ProfitLoss); Assert.AreEqual(5, trade.TotalFees); Assert.AreEqual(0, trade.MAE); Assert.AreEqual(0, trade.MFE); CollectionAssert.AreEquivalent(new[] { 1, 2, 3, 4, 5 }, trade.OrderIds); } break; case FillGroupingMethod.FlatToReduced: { Assert.AreEqual(2, builder.ClosedTrades.Count); var trade1 = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade1.Symbol); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? time : time.AddMinutes(10), trade1.EntryTime); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? AdjustPriceToSplit(1.07m, split) : AdjustPriceToSplit(1.08m, split), trade1.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade1.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade1.Quantity); Assert.AreEqual(time.AddMinutes(20), trade1.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade1.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 20 : 10, trade1.ProfitLoss); Assert.AreEqual(3, trade1.TotalFees); Assert.AreEqual(0, trade1.MAE); Assert.AreEqual(0, trade1.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [3, 1] : new[] { 3, 2 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; Assert.AreEqual(Symbols.SPY, trade2.Symbol); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? time.AddMinutes(10) : time, trade2.EntryTime); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? AdjustPriceToSplit(1.08m, split) : AdjustPriceToSplit(1.075m, split), trade2.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade2.Direction); Assert.AreEqual(AdjustQuantityToSplit(2000, split), trade2.Quantity); Assert.AreEqual(time.AddMinutes(40), trade2.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 20 : 30, trade2.ProfitLoss); Assert.AreEqual(2, trade2.TotalFees); Assert.AreEqual(0, trade2.MAE); Assert.AreEqual(0, trade2.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [2, 4, 5] : new[] { 1, 4, 5 }, trade2.OrderIds); } break; } } [Test] public void ScaleInScaleOut1Short( [Values] FillGroupingMethod groupingMethod, [Values] FillMatchingMethod matchingMethod, // 0 for no split [Values(0, 0.5, 0.333)] double splitFactor) { // Sell 1k, Sell 1k, Buy 1k, Sell 1k, Buy 2k var builder = new TradeBuilder(groupingMethod, matchingMethod); builder.SetSecurityManager(_securityManager); var time = _startTime; // Sell 1k builder.ProcessFill( new OrderEvent(1, Symbols.SPY, time, OrderStatus.Filled, OrderDirection.Sell, fillPrice: 1.07m, fillQuantity: -1000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.075m); // Sell 1k builder.ProcessFill( new OrderEvent(2, Symbols.SPY, time.AddMinutes(10), OrderStatus.Filled, OrderDirection.Buy, fillPrice: 1.08m, fillQuantity: -1000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.065m); builder.SetMarketPrice(Symbols.SPY, 1.10m); Split split = null; if (splitFactor != 0) { // apply a 2:1 split split = new Split(Symbols.SPY, time.AddMinutes(5), 1.10m, (decimal)splitFactor, SplitType.SplitOccurred); builder.ApplySplit(split, false, DataNormalizationMode.Raw); } // Buy 1k builder.ProcessFill( new OrderEvent(3, Symbols.SPY, time.AddMinutes(20), OrderStatus.Filled, OrderDirection.Buy, fillPrice: AdjustPriceToSplit(1.09m, split), fillQuantity: AdjustQuantityToSplit(1000, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); // Sell 1k builder.ProcessFill( new OrderEvent(4, Symbols.SPY, time.AddMinutes(30), OrderStatus.Filled, OrderDirection.Sell, fillPrice: AdjustPriceToSplit(1.08m, split), fillQuantity: AdjustQuantityToSplit(-1000, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); // Buy 2k builder.ProcessFill( new OrderEvent(5, Symbols.SPY, time.AddMinutes(40), OrderStatus.Filled, OrderDirection.Buy, fillPrice: AdjustPriceToSplit(1.09m, split), fillQuantity: AdjustQuantityToSplit(2000, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsFalse(builder.HasOpenPosition(Symbols.SPY)); switch (groupingMethod) { case FillGroupingMethod.FillToFill: { Assert.AreEqual(3, builder.ClosedTrades.Count); var trade1 = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade1.Symbol); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? time : time.AddMinutes(10), trade1.EntryTime); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? AdjustPriceToSplit(1.07m, split) : AdjustPriceToSplit(1.08m, split), trade1.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade1.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade1.Quantity); Assert.AreEqual(time.AddMinutes(20), trade1.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade1.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -20 : -10, trade1.ProfitLoss); Assert.AreEqual(2, trade1.TotalFees); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -30 : -20, trade1.MAE); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 5 : 15, trade1.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [1, 3] : new[] { 2, 3 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; Assert.AreEqual(Symbols.SPY, trade2.Symbol); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? time.AddMinutes(10) : time.AddMinutes(30), trade2.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.08m, split), trade2.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade2.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade2.Quantity); Assert.AreEqual(time.AddMinutes(40), trade2.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(-10, trade2.ProfitLoss); Assert.AreEqual(2, trade2.TotalFees); Assert.AreEqual(-20, trade2.MAE); Assert.AreEqual(15, trade2.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [2, 5] : new[] { 4, 5 }, trade2.OrderIds); var trade3 = builder.ClosedTrades[2]; Assert.AreEqual(Symbols.SPY, trade3.Symbol); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? time.AddMinutes(30) : time, trade3.EntryTime); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? AdjustPriceToSplit(1.08m, split) : AdjustPriceToSplit(1.07m, split), trade3.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade3.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade3.Quantity); Assert.AreEqual(time.AddMinutes(40), trade3.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade3.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -10 : -20, trade3.ProfitLoss); Assert.AreEqual(1, trade3.TotalFees); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -20 : -30, trade3.MAE); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 15 : 5, trade3.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [4, 5] : new[] { 1, 5 }, trade3.OrderIds); } break; case FillGroupingMethod.FlatToFlat: { Assert.AreEqual(1, builder.ClosedTrades.Count); var trade = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade.Symbol); Assert.AreEqual(time, trade.EntryTime); Assert.Less( Math.Abs(AdjustPriceToSplit(1.0766666666666666666666666667m, split) - trade.EntryPrice), 1e-27m); Assert.AreEqual(TradeDirection.Short, trade.Direction); Assert.AreEqual(AdjustQuantityToSplit(3000, split), trade.Quantity); Assert.AreEqual(time.AddMinutes(40), trade.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade.ExitPrice); Assert.AreEqual(-40, trade.ProfitLoss); Assert.AreEqual(5, trade.TotalFees); Assert.AreEqual(0, trade.MAE); Assert.AreEqual(0, trade.MFE); CollectionAssert.AreEquivalent(new[] { 1, 2, 3, 4, 5 }, trade.OrderIds); } break; case FillGroupingMethod.FlatToReduced: { Assert.AreEqual(2, builder.ClosedTrades.Count); var trade1 = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade1.Symbol); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? time : time.AddMinutes(10), trade1.EntryTime); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? AdjustPriceToSplit(1.07m, split) : AdjustPriceToSplit(1.08m, split), trade1.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade1.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade1.Quantity); Assert.AreEqual(time.AddMinutes(20), trade1.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade1.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -20 : -10, trade1.ProfitLoss); Assert.AreEqual(3, trade1.TotalFees); Assert.AreEqual(0, trade1.MAE); Assert.AreEqual(0, trade1.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [1, 3] : new[] { 2, 3 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; Assert.AreEqual(Symbols.SPY, trade2.Symbol); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? time.AddMinutes(10) : time, trade2.EntryTime); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? AdjustPriceToSplit(1.08m, split) : AdjustPriceToSplit(1.075m, split), trade2.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade2.Direction); Assert.AreEqual(AdjustQuantityToSplit(2000, split), trade2.Quantity); Assert.AreEqual(time.AddMinutes(40), trade2.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -20 : -30, trade2.ProfitLoss); Assert.AreEqual(2, trade2.TotalFees); Assert.AreEqual(0, trade2.MAE); Assert.AreEqual(0, trade2.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [2, 4, 5] : new[] { 1, 4, 5 }, trade2.OrderIds); } break; } } [Test] public void ScaleInScaleOut2Long( [Values] FillGroupingMethod groupingMethod, [Values] FillMatchingMethod matchingMethod, // 0 for no split [Values(0, 0.5, 0.333)] double splitFactor) { // Buy 1k, Buy 2k, Sell 1k, Buy 1k, Sell 3k var builder = new TradeBuilder(groupingMethod, matchingMethod); builder.SetSecurityManager(_securityManager); var time = _startTime; // Buy 1k builder.ProcessFill(new OrderEvent(1, Symbols.SPY, time, OrderStatus.Filled, OrderDirection.Buy, fillPrice: 1.07m, fillQuantity: 1000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.075m); // Buy 2k builder.ProcessFill(new OrderEvent(2, Symbols.SPY, time.AddMinutes(10), OrderStatus.Filled, OrderDirection.Buy, fillPrice: 1.08m, fillQuantity: 2000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.065m); builder.SetMarketPrice(Symbols.SPY, 1.10m); Split split = null; if (splitFactor != 0) { // apply a 2:1 split split = new Split(Symbols.SPY, time.AddMinutes(5), 1.10m, (decimal)splitFactor, SplitType.SplitOccurred); builder.ApplySplit(split, false, DataNormalizationMode.Raw); } // Sell 1k builder.ProcessFill(new OrderEvent(3, Symbols.SPY, time.AddMinutes(20), OrderStatus.Filled, OrderDirection.Sell, fillPrice: AdjustPriceToSplit(1.09m, split), fillQuantity: AdjustQuantityToSplit(-1000, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); // Buy 1k builder.ProcessFill(new OrderEvent(4, Symbols.SPY, time.AddMinutes(30), OrderStatus.Filled, OrderDirection.Buy, fillPrice: AdjustPriceToSplit(1.08m, split), fillQuantity: AdjustQuantityToSplit(1000, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); // Sell 3k builder.ProcessFill(new OrderEvent(5, Symbols.SPY, time.AddMinutes(40), OrderStatus.Filled, OrderDirection.Sell, fillPrice: AdjustPriceToSplit(1.09m, split), fillQuantity: AdjustQuantityToSplit(-3000, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsFalse(builder.HasOpenPosition(Symbols.SPY)); switch (groupingMethod) { case FillGroupingMethod.FillToFill: { if (matchingMethod == FillMatchingMethod.FIFO) { Assert.AreEqual(3, builder.ClosedTrades.Count); var trade1 = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade1.Symbol); Assert.AreEqual(time, trade1.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.07m, split), trade1.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade1.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade1.Quantity); Assert.AreEqual(time.AddMinutes(20), trade1.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade1.ExitPrice); Assert.AreEqual(20, trade1.ProfitLoss); Assert.AreEqual(2, trade1.TotalFees); Assert.AreEqual(-5, trade1.MAE); Assert.AreEqual(30, trade1.MFE); CollectionAssert.AreEquivalent(new[] { 1, 3 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; Assert.AreEqual(Symbols.SPY, trade2.Symbol); Assert.AreEqual(time.AddMinutes(10), trade2.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.08m, split), trade2.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade2.Direction); Assert.AreEqual(AdjustQuantityToSplit(2000, split), trade2.Quantity); Assert.AreEqual(time.AddMinutes(40), trade2.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(20, trade2.ProfitLoss); Assert.AreEqual(2, trade2.TotalFees); Assert.AreEqual(-30, trade2.MAE); Assert.AreEqual(40, trade2.MFE); CollectionAssert.AreEquivalent(new[] { 2, 5 }, trade2.OrderIds); var trade3 = builder.ClosedTrades[2]; Assert.AreEqual(Symbols.SPY, trade3.Symbol); Assert.AreEqual(time.AddMinutes(30), trade3.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.08m, split), trade3.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade3.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade3.Quantity); Assert.AreEqual(time.AddMinutes(40), trade3.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade3.ExitPrice); Assert.AreEqual(10, trade3.ProfitLoss); Assert.AreEqual(1, trade3.TotalFees); Assert.AreEqual(-15, trade3.MAE); Assert.AreEqual(20, trade3.MFE); CollectionAssert.AreEquivalent(new[] { 4, 5 }, trade3.OrderIds); } else { Assert.AreEqual(4, builder.ClosedTrades.Count); var trade1 = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade1.Symbol); Assert.AreEqual(time.AddMinutes(10), trade1.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.08m, split), trade1.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade1.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade1.Quantity); Assert.AreEqual(time.AddMinutes(20), trade1.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade1.ExitPrice); Assert.AreEqual(10, trade1.ProfitLoss); Assert.AreEqual(2, trade1.TotalFees); Assert.AreEqual(-15, trade1.MAE); Assert.AreEqual(20, trade1.MFE); CollectionAssert.AreEquivalent(new[] { 2, 3 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; Assert.AreEqual(Symbols.SPY, trade2.Symbol); Assert.AreEqual(time.AddMinutes(30), trade2.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.08m, split), trade2.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade2.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade2.Quantity); Assert.AreEqual(time.AddMinutes(40), trade2.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(10, trade2.ProfitLoss); Assert.AreEqual(2, trade2.TotalFees); Assert.AreEqual(-15, trade2.MAE); Assert.AreEqual(20, trade2.MFE); CollectionAssert.AreEquivalent(new[] { 4, 5 }, trade2.OrderIds); var trade3 = builder.ClosedTrades[2]; Assert.AreEqual(Symbols.SPY, trade3.Symbol); Assert.AreEqual(time.AddMinutes(10), trade3.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.08m, split), trade3.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade3.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade3.Quantity); Assert.AreEqual(time.AddMinutes(40), trade3.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade3.ExitPrice); Assert.AreEqual(10, trade3.ProfitLoss); Assert.AreEqual(0, trade3.TotalFees); Assert.AreEqual(-15, trade3.MAE); Assert.AreEqual(20, trade3.MFE); CollectionAssert.AreEquivalent(new[] { 2, 5 }, trade3.OrderIds); var trade4 = builder.ClosedTrades[3]; Assert.AreEqual(Symbols.SPY, trade4.Symbol); Assert.AreEqual(time, trade4.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.07m, split), trade4.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade4.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade4.Quantity); Assert.AreEqual(time.AddMinutes(40), trade4.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade4.ExitPrice); Assert.AreEqual(20, trade4.ProfitLoss); Assert.AreEqual(1, trade4.TotalFees); Assert.AreEqual(-5, trade4.MAE); Assert.AreEqual(30, trade4.MFE); CollectionAssert.AreEquivalent(new[] { 1, 5 }, trade4.OrderIds); } } break; case FillGroupingMethod.FlatToFlat: { Assert.AreEqual(1, builder.ClosedTrades.Count); var trade = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade.Symbol); Assert.AreEqual(time, trade.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.0775m, split), trade.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade.Direction); Assert.AreEqual(AdjustQuantityToSplit(4000, split), trade.Quantity); Assert.AreEqual(time.AddMinutes(40), trade.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade.ExitPrice); Assert.AreEqual(50, trade.ProfitLoss); Assert.AreEqual(5, trade.TotalFees); Assert.AreEqual(0, trade.MAE); Assert.AreEqual(0, trade.MFE); CollectionAssert.AreEquivalent(new[] { 1, 2, 3, 4, 5 }, trade.OrderIds); } break; case FillGroupingMethod.FlatToReduced: { Assert.AreEqual(2, builder.ClosedTrades.Count); var trade1 = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade1.Symbol); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? time : time.AddMinutes(10), trade1.EntryTime); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? AdjustPriceToSplit(1.07m, split) : AdjustPriceToSplit(1.08m, split), trade1.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade1.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade1.Quantity); Assert.AreEqual(time.AddMinutes(20), trade1.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade1.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 20 : 10, trade1.ProfitLoss); Assert.AreEqual(3, trade1.TotalFees); Assert.AreEqual(0, trade1.MAE); Assert.AreEqual(0, trade1.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [1, 3] : new[] { 2, 3 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; Assert.AreEqual(Symbols.SPY, trade2.Symbol); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? time.AddMinutes(10) : time, trade2.EntryTime); if (matchingMethod == FillMatchingMethod.FIFO) { Assert.AreEqual(AdjustPriceToSplit(1.08m, split), trade2.EntryPrice); } else { Assert.Less( Math.Abs(AdjustPriceToSplit(1.0766666666666666666666666667m, split) - trade2.EntryPrice), 1e-27m); } Assert.AreEqual(TradeDirection.Long, trade2.Direction); Assert.AreEqual(AdjustQuantityToSplit(3000, split), trade2.Quantity); Assert.AreEqual(time.AddMinutes(40), trade2.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 30 : 40, trade2.ProfitLoss); Assert.AreEqual(2, trade2.TotalFees); Assert.AreEqual(0, trade2.MAE); Assert.AreEqual(0, trade2.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [2, 4, 5] : new[] { 1, 2, 4, 5 }, trade2.OrderIds); } break; } } [Test] public void ScaleInScaleOut2Short( [Values] FillGroupingMethod groupingMethod, [Values] FillMatchingMethod matchingMethod, // 0 for no split [Values(0, 0.5, 0.333)] double splitFactor) { // Sell 1k, Sell 2k, Buy 1k, Sell 1k, Buy 3k var builder = new TradeBuilder(groupingMethod, matchingMethod); builder.SetSecurityManager(_securityManager); var time = _startTime; // Sell 1k builder.ProcessFill( new OrderEvent(1, Symbols.SPY, time, OrderStatus.Filled, OrderDirection.Sell, fillPrice: 1.07m, fillQuantity: -1000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.075m); // Sell 2k builder.ProcessFill( new OrderEvent(2, Symbols.SPY, time.AddMinutes(10), OrderStatus.Filled, OrderDirection.Sell, fillPrice: 1.08m, fillQuantity: -2000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.065m); builder.SetMarketPrice(Symbols.SPY, 1.10m); Split split = null; if (splitFactor != 0) { // apply a 2:1 split split = new Split(Symbols.SPY, time.AddMinutes(5), 1.10m, (decimal)splitFactor, SplitType.SplitOccurred); builder.ApplySplit(split, false, DataNormalizationMode.Raw); } // Buy 1k builder.ProcessFill( new OrderEvent(3, Symbols.SPY, time.AddMinutes(20), OrderStatus.Filled, OrderDirection.Buy, fillPrice: AdjustPriceToSplit(1.09m, split), fillQuantity: AdjustQuantityToSplit(1000, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); // Sell 1k builder.ProcessFill( new OrderEvent(4, Symbols.SPY, time.AddMinutes(30), OrderStatus.Filled, OrderDirection.Sell, fillPrice: AdjustPriceToSplit(1.08m, split), fillQuantity: AdjustQuantityToSplit(-1000, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); // Buy 3k builder.ProcessFill( new OrderEvent(5, Symbols.SPY, time.AddMinutes(40), OrderStatus.Filled, OrderDirection.Buy, fillPrice: AdjustPriceToSplit(1.09m, split), fillQuantity: AdjustQuantityToSplit(3000, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsFalse(builder.HasOpenPosition(Symbols.SPY)); switch (groupingMethod) { case FillGroupingMethod.FillToFill: { if (matchingMethod == FillMatchingMethod.FIFO) { Assert.AreEqual(3, builder.ClosedTrades.Count); var trade1 = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade1.Symbol); Assert.AreEqual(time, trade1.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.07m, split), trade1.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade1.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade1.Quantity); Assert.AreEqual(time.AddMinutes(20), trade1.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade1.ExitPrice); Assert.AreEqual(-20, trade1.ProfitLoss); Assert.AreEqual(2, trade1.TotalFees); Assert.AreEqual(-30, trade1.MAE); Assert.AreEqual(5, trade1.MFE); CollectionAssert.AreEquivalent(new[] { 1, 3 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; Assert.AreEqual(Symbols.SPY, trade2.Symbol); Assert.AreEqual(time.AddMinutes(10), trade2.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.08m, split), trade2.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade2.Direction); Assert.AreEqual(AdjustQuantityToSplit(2000, split), trade2.Quantity); Assert.AreEqual(time.AddMinutes(40), trade2.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(-20, trade2.ProfitLoss); Assert.AreEqual(2, trade2.TotalFees); Assert.AreEqual(-40, trade2.MAE); Assert.AreEqual(30, trade2.MFE); CollectionAssert.AreEquivalent(new[] { 2, 5 }, trade2.OrderIds); var trade3 = builder.ClosedTrades[2]; Assert.AreEqual(Symbols.SPY, trade3.Symbol); Assert.AreEqual(time.AddMinutes(30), trade3.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.08m, split), trade3.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade3.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade3.Quantity); Assert.AreEqual(time.AddMinutes(40), trade3.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade3.ExitPrice); Assert.AreEqual(-10, trade3.ProfitLoss); Assert.AreEqual(1, trade3.TotalFees); Assert.AreEqual(-20, trade3.MAE); Assert.AreEqual(15, trade3.MFE); CollectionAssert.AreEquivalent(new[] { 4, 5 }, trade3.OrderIds); } else { Assert.AreEqual(4, builder.ClosedTrades.Count); var trade1 = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade1.Symbol); Assert.AreEqual(time.AddMinutes(10), trade1.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.08m, split), trade1.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade1.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade1.Quantity); Assert.AreEqual(time.AddMinutes(20), trade1.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade1.ExitPrice); Assert.AreEqual(-10, trade1.ProfitLoss); Assert.AreEqual(2, trade1.TotalFees); Assert.AreEqual(-20, trade1.MAE); Assert.AreEqual(15, trade1.MFE); CollectionAssert.AreEquivalent(new[] { 2, 3 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; Assert.AreEqual(Symbols.SPY, trade2.Symbol); Assert.AreEqual(time.AddMinutes(30), trade2.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.08m, split), trade2.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade2.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade2.Quantity); Assert.AreEqual(time.AddMinutes(40), trade2.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(-10, trade2.ProfitLoss); Assert.AreEqual(2, trade2.TotalFees); Assert.AreEqual(-20, trade2.MAE); Assert.AreEqual(15, trade2.MFE); CollectionAssert.AreEquivalent(new[] { 4, 5 }, trade2.OrderIds); var trade3 = builder.ClosedTrades[2]; Assert.AreEqual(Symbols.SPY, trade3.Symbol); Assert.AreEqual(time.AddMinutes(10), trade3.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.08m, split), trade3.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade3.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade3.Quantity); Assert.AreEqual(time.AddMinutes(40), trade3.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade3.ExitPrice); Assert.AreEqual(-10, trade3.ProfitLoss); Assert.AreEqual(0, trade3.TotalFees); Assert.AreEqual(-20, trade3.MAE); Assert.AreEqual(15, trade3.MFE); CollectionAssert.AreEquivalent(new[] { 2, 5 }, trade3.OrderIds); var trade4 = builder.ClosedTrades[3]; Assert.AreEqual(Symbols.SPY, trade4.Symbol); Assert.AreEqual(time, trade4.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.07m, split), trade4.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade4.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade4.Quantity); Assert.AreEqual(time.AddMinutes(40), trade4.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade4.ExitPrice); Assert.AreEqual(-20, trade4.ProfitLoss); Assert.AreEqual(1, trade4.TotalFees); Assert.AreEqual(-30, trade4.MAE); Assert.AreEqual(5, trade4.MFE); CollectionAssert.AreEquivalent(new[] { 1, 5 }, trade4.OrderIds); } } break; case FillGroupingMethod.FlatToFlat: { Assert.AreEqual(1, builder.ClosedTrades.Count); var trade = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade.Symbol); Assert.AreEqual(time, trade.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.0775m, split), trade.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade.Direction); Assert.AreEqual(AdjustQuantityToSplit(4000, split), trade.Quantity); Assert.AreEqual(time.AddMinutes(40), trade.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade.ExitPrice); Assert.AreEqual(-50, trade.ProfitLoss); Assert.AreEqual(5, trade.TotalFees); Assert.AreEqual(0, trade.MAE); Assert.AreEqual(0, trade.MFE); CollectionAssert.AreEquivalent(new[] { 1, 2, 3, 4, 5 }, trade.OrderIds); } break; case FillGroupingMethod.FlatToReduced: { Assert.AreEqual(2, builder.ClosedTrades.Count); var trade1 = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade1.Symbol); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? time : time.AddMinutes(10), trade1.EntryTime); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? AdjustPriceToSplit(1.07m, split) : AdjustPriceToSplit(1.08m, split), trade1.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade1.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade1.Quantity); Assert.AreEqual(time.AddMinutes(20), trade1.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade1.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -20 : -10, trade1.ProfitLoss); Assert.AreEqual(3, trade1.TotalFees); Assert.AreEqual(0, trade1.MAE); Assert.AreEqual(0, trade1.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [3, 1] : new[] { 2, 3 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; Assert.AreEqual(Symbols.SPY, trade2.Symbol); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? time.AddMinutes(10) : time, trade2.EntryTime); if (matchingMethod == FillMatchingMethod.FIFO) { Assert.AreEqual(AdjustPriceToSplit(1.08m, split), trade2.EntryPrice); } else { Assert.Less( Math.Abs(AdjustPriceToSplit(1.0766666666666666666666666667m, split) - trade2.EntryPrice), 1e-27m); } Assert.AreEqual(TradeDirection.Short, trade2.Direction); Assert.AreEqual(AdjustQuantityToSplit(3000, split), trade2.Quantity); Assert.AreEqual(time.AddMinutes(40), trade2.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -30 : -40, trade2.ProfitLoss); Assert.AreEqual(2, trade2.TotalFees); Assert.AreEqual(0, trade2.MAE); Assert.AreEqual(0, trade2.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [2, 4, 5] : new[] { 1, 2, 4, 5 }, trade2.OrderIds); } break; } } [Test] public void ScaleInScaleOut3Long( [Values] FillGroupingMethod groupingMethod, [Values] FillMatchingMethod matchingMethod, // 0 for no split [Values(0, 0.5, 0.333)] double splitFactor) { // Buy 1k, Buy 1k, Buy 1k, Sell 2k, Buy 1k, Sell 2k var builder = new TradeBuilder(groupingMethod, matchingMethod); builder.SetSecurityManager(_securityManager); var time = _startTime; // Buy 1k builder.ProcessFill(new OrderEvent(1, Symbols.SPY, time, OrderStatus.Filled, OrderDirection.Buy, fillPrice: 1.07m, fillQuantity: 1000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.075m); // Buy 1k builder.ProcessFill(new OrderEvent(2, Symbols.SPY, time.AddMinutes(10), OrderStatus.Filled, OrderDirection.Buy, fillPrice: 1.08m, fillQuantity: 1000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.065m); builder.SetMarketPrice(Symbols.SPY, 1.10m); Split split = null; if (splitFactor != 0) { // apply a 2:1 split split = new Split(Symbols.SPY, time.AddMinutes(5), 1.10m, (decimal)splitFactor, SplitType.SplitOccurred); builder.ApplySplit(split, false, DataNormalizationMode.Raw); } // Buy 1k builder.ProcessFill(new OrderEvent(3, Symbols.SPY, time.AddMinutes(20), OrderStatus.Filled, OrderDirection.Buy, fillPrice: AdjustPriceToSplit(1.09m, split), fillQuantity: AdjustQuantityToSplit(1000, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); // Sell 2k builder.ProcessFill(new OrderEvent(4, Symbols.SPY, time.AddMinutes(30), OrderStatus.Filled, OrderDirection.Sell, fillPrice: AdjustPriceToSplit(1.10m, split), fillQuantity: AdjustQuantityToSplit(-2000, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); // Buy 1k builder.ProcessFill(new OrderEvent(5, Symbols.SPY, time.AddMinutes(40), OrderStatus.Filled, OrderDirection.Buy, fillPrice: AdjustPriceToSplit(1.08m, split), fillQuantity: AdjustQuantityToSplit(1000, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); // Sell 2k builder.ProcessFill(new OrderEvent(6, Symbols.SPY, time.AddMinutes(50), OrderStatus.Filled, OrderDirection.Sell, fillPrice: AdjustPriceToSplit(1.09m, split), fillQuantity: AdjustQuantityToSplit(-2000, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsFalse(builder.HasOpenPosition(Symbols.SPY)); switch (groupingMethod) { case FillGroupingMethod.FillToFill: { Assert.AreEqual(4, builder.ClosedTrades.Count); var trade1 = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade1.Symbol); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? time : time.AddMinutes(20), trade1.EntryTime); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? AdjustPriceToSplit(1.07m, split) : AdjustPriceToSplit(1.09m, split), trade1.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade1.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade1.Quantity); Assert.AreEqual(time.AddMinutes(30), trade1.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.10m, split), trade1.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 30 : 10, trade1.ProfitLoss); Assert.AreEqual(2, trade1.TotalFees); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -5 : -25, trade1.MAE); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 30 : 10, trade1.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [1, 4] : new[] { 3, 4 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; Assert.AreEqual(Symbols.SPY, trade2.Symbol); Assert.AreEqual(time.AddMinutes(10), trade2.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.08m, split), trade2.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade2.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade2.Quantity); Assert.AreEqual(time.AddMinutes(30), trade2.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.10m, split), trade2.ExitPrice); Assert.AreEqual(20, trade2.ProfitLoss); Assert.AreEqual(1, trade2.TotalFees); Assert.AreEqual(-15, trade2.MAE); Assert.AreEqual(20, trade2.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [2, 4] : new[] { 2, 4 }, trade2.OrderIds); var trade3 = builder.ClosedTrades[2]; Assert.AreEqual(Symbols.SPY, trade3.Symbol); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? time.AddMinutes(20) : time.AddMinutes(40), trade3.EntryTime); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? AdjustPriceToSplit(1.09m, split) : AdjustPriceToSplit(1.08m, split), trade3.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade3.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade3.Quantity); Assert.AreEqual(time.AddMinutes(50), trade3.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade3.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 0 : 10, trade3.ProfitLoss); Assert.AreEqual(2, trade3.TotalFees); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -25 : -15, trade3.MAE); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 10 : 20, trade3.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [3, 6] : new[] { 5, 6 }, trade3.OrderIds); var trade4 = builder.ClosedTrades[3]; Assert.AreEqual(Symbols.SPY, trade4.Symbol); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? time.AddMinutes(40) : time, trade4.EntryTime); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? AdjustPriceToSplit(1.08m, split) : AdjustPriceToSplit(1.07m, split), trade4.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade4.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade4.Quantity); Assert.AreEqual(time.AddMinutes(50), trade4.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade4.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 10 : 20, trade4.ProfitLoss); Assert.AreEqual(1, trade4.TotalFees); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -15 : -5, trade4.MAE); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 20 : 30, trade4.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [5, 6] : new[] { 1, 6 }, trade4.OrderIds); } break; case FillGroupingMethod.FlatToFlat: { Assert.AreEqual(1, builder.ClosedTrades.Count); var trade = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade.Symbol); Assert.AreEqual(time, trade.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.08m, split), trade.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade.Direction); Assert.AreEqual(AdjustQuantityToSplit(4000, split), trade.Quantity); Assert.AreEqual(time.AddMinutes(50), trade.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.095m, split), trade.ExitPrice); Assert.AreEqual(60, trade.ProfitLoss); Assert.AreEqual(6, trade.TotalFees); Assert.AreEqual(0, trade.MAE); Assert.AreEqual(0, trade.MFE); CollectionAssert.AreEquivalent(new[] { 1, 2, 3, 4, 5, 6 }, trade.OrderIds); } break; case FillGroupingMethod.FlatToReduced: { Assert.AreEqual(2, builder.ClosedTrades.Count); var trade1 = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade1.Symbol); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? time : time.AddMinutes(10), trade1.EntryTime); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? AdjustPriceToSplit(1.075m, split) : AdjustPriceToSplit(1.085m, split), trade1.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade1.Direction); Assert.AreEqual(AdjustQuantityToSplit(2000, split), trade1.Quantity); Assert.AreEqual(time.AddMinutes(30), trade1.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.10m, split), trade1.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 50 : 30, trade1.ProfitLoss); Assert.AreEqual(4, trade1.TotalFees); Assert.AreEqual(0, trade1.MAE); Assert.AreEqual(0, trade1.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [1, 2, 4] : new[] { 2, 3, 4 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; Assert.AreEqual(Symbols.SPY, trade2.Symbol); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? time.AddMinutes(20) : time, trade2.EntryTime); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? AdjustPriceToSplit(1.085m, split) : AdjustPriceToSplit(1.075m, split), trade2.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade2.Direction); Assert.AreEqual(AdjustQuantityToSplit(2000, split), trade2.Quantity); Assert.AreEqual(time.AddMinutes(50), trade2.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 10 : 30, trade2.ProfitLoss); Assert.AreEqual(2, trade2.TotalFees); Assert.AreEqual(0, trade2.MAE); Assert.AreEqual(0, trade2.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [3, 5, 6] : new[] { 1, 5, 6 }, trade2.OrderIds); } break; } } [Test] public void ScaleInScaleOut3Short( [Values] FillGroupingMethod groupingMethod, [Values] FillMatchingMethod matchingMethod, // 0 for no split [Values(0, 0.5, 0.333)] double splitFactor) { // Sell 1k, Sell 1k, Sell 1k, Buy 2k, Sell 1k, Buy 2k var builder = new TradeBuilder(groupingMethod, matchingMethod); builder.SetSecurityManager(_securityManager); var time = _startTime; // Sell 1k builder.ProcessFill( new OrderEvent(1, Symbols.SPY, time, OrderStatus.Filled, OrderDirection.Sell, fillPrice: 1.07m, fillQuantity: -1000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.075m); // Sell 1k builder.ProcessFill( new OrderEvent(2, Symbols.SPY, time.AddMinutes(10), OrderStatus.Filled, OrderDirection.Sell, fillPrice: 1.08m, fillQuantity: -1000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.065m); builder.SetMarketPrice(Symbols.SPY, 1.10m); Split split = null; if (splitFactor != 0) { // apply a 2:1 split split = new Split(Symbols.SPY, time.AddMinutes(5), 1.10m, (decimal)splitFactor, SplitType.SplitOccurred); builder.ApplySplit(split, false, DataNormalizationMode.Raw); } // Sell 1k builder.ProcessFill( new OrderEvent(3, Symbols.SPY, time.AddMinutes(20), OrderStatus.Filled, OrderDirection.Buy, fillPrice: AdjustPriceToSplit(1.09m, split), fillQuantity: AdjustQuantityToSplit(-1000, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); // Buy 2k builder.ProcessFill( new OrderEvent(4, Symbols.SPY, time.AddMinutes(30), OrderStatus.Filled, OrderDirection.Buy, fillPrice: AdjustPriceToSplit(1.10m, split), fillQuantity: AdjustQuantityToSplit(2000, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); // Sell 1k builder.ProcessFill( new OrderEvent(5, Symbols.SPY, time.AddMinutes(40), OrderStatus.Filled, OrderDirection.Sell, fillPrice: AdjustPriceToSplit(1.08m, split), fillQuantity: AdjustQuantityToSplit(-1000, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); // Buy 2k builder.ProcessFill( new OrderEvent(6, Symbols.SPY, time.AddMinutes(50), OrderStatus.Filled, OrderDirection.Buy, fillPrice: AdjustPriceToSplit(1.09m, split), fillQuantity: AdjustQuantityToSplit(2000, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsFalse(builder.HasOpenPosition(Symbols.SPY)); switch (groupingMethod) { case FillGroupingMethod.FillToFill: { Assert.AreEqual(4, builder.ClosedTrades.Count); var trade1 = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade1.Symbol); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? time : time.AddMinutes(20), trade1.EntryTime); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? AdjustPriceToSplit(1.07m, split) : AdjustPriceToSplit(1.09m, split), trade1.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade1.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade1.Quantity); Assert.AreEqual(time.AddMinutes(30), trade1.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.10m, split), trade1.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -30 : -10, trade1.ProfitLoss); Assert.AreEqual(2, trade1.TotalFees); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -30 : -10, trade1.MAE); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 5 : 25, trade1.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [1, 4] : new[] { 3, 4 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; Assert.AreEqual(Symbols.SPY, trade2.Symbol); Assert.AreEqual(time.AddMinutes(10), trade2.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.08m, split), trade2.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade2.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade2.Quantity); Assert.AreEqual(time.AddMinutes(30), trade2.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.10m, split), trade2.ExitPrice); Assert.AreEqual(-20, trade2.ProfitLoss); Assert.AreEqual(1, trade2.TotalFees); Assert.AreEqual(-20, trade2.MAE); Assert.AreEqual(15, trade2.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [2, 4] : new[] { 2, 4 }, trade2.OrderIds); var trade3 = builder.ClosedTrades[2]; Assert.AreEqual(Symbols.SPY, trade3.Symbol); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? time.AddMinutes(20) : time.AddMinutes(40), trade3.EntryTime); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? AdjustPriceToSplit(1.09m, split) : AdjustPriceToSplit(1.08m, split), trade3.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade3.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade3.Quantity); Assert.AreEqual(time.AddMinutes(50), trade3.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade3.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 0 : -10, trade3.ProfitLoss); Assert.AreEqual(2, trade3.TotalFees); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -10 : -20, trade3.MAE); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 25 : 15, trade3.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [3, 6] : new[] { 5, 6 }, trade3.OrderIds); var trade4 = builder.ClosedTrades[3]; Assert.AreEqual(Symbols.SPY, trade4.Symbol); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? time.AddMinutes(40) : time, trade4.EntryTime); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? AdjustPriceToSplit(1.08m, split) : AdjustPriceToSplit(1.07m, split), trade4.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade4.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade4.Quantity); Assert.AreEqual(time.AddMinutes(50), trade4.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade4.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -10 : -20, trade4.ProfitLoss); Assert.AreEqual(1, trade4.TotalFees); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -20 : -30, trade4.MAE); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 15 : 5, trade4.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [5, 6] : new[] { 1, 6 }, trade4.OrderIds); } break; case FillGroupingMethod.FlatToFlat: { Assert.AreEqual(1, builder.ClosedTrades.Count); var trade = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade.Symbol); Assert.AreEqual(time, trade.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.08m, split), trade.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade.Direction); Assert.AreEqual(AdjustQuantityToSplit(4000, split), trade.Quantity); Assert.AreEqual(time.AddMinutes(50), trade.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.095m, split), trade.ExitPrice); Assert.AreEqual(-60, trade.ProfitLoss); Assert.AreEqual(6, trade.TotalFees); Assert.AreEqual(0, trade.MAE); Assert.AreEqual(0, trade.MFE); CollectionAssert.AreEquivalent(new[] { 1, 2, 3, 4, 5, 6 }, trade.OrderIds); } break; case FillGroupingMethod.FlatToReduced: { Assert.AreEqual(2, builder.ClosedTrades.Count); var trade1 = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade1.Symbol); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? time : time.AddMinutes(10), trade1.EntryTime); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? AdjustPriceToSplit(1.075m, split) : AdjustPriceToSplit(1.085m, split), trade1.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade1.Direction); Assert.AreEqual(AdjustQuantityToSplit(2000, split), trade1.Quantity); Assert.AreEqual(time.AddMinutes(30), trade1.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.10m, split), trade1.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -50 : -30, trade1.ProfitLoss); Assert.AreEqual(4, trade1.TotalFees); Assert.AreEqual(0, trade1.MAE); Assert.AreEqual(0, trade1.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [1, 2, 4] : new[] { 2, 3, 4 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; Assert.AreEqual(Symbols.SPY, trade2.Symbol); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? time.AddMinutes(20) : time, trade2.EntryTime); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? AdjustPriceToSplit(1.085m, split) : AdjustPriceToSplit(1.075m, split), trade2.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade2.Direction); Assert.AreEqual(AdjustQuantityToSplit(2000, split), trade2.Quantity); Assert.AreEqual(time.AddMinutes(50), trade2.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -10 : -30, trade2.ProfitLoss); Assert.AreEqual(2, trade2.TotalFees); Assert.AreEqual(0, trade2.MAE); Assert.AreEqual(0, trade2.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [3, 5, 6] : new[] { 1, 5, 6 }, trade2.OrderIds); } break; } } [Test] public void ScaleInScaleOut4Long( [Values] FillGroupingMethod groupingMethod, [Values] FillMatchingMethod matchingMethod, // 0 for no split [Values(0, 0.5, 0.333)] double splitFactor) { // Buy 1k, Buy 1k, Sell 1.5k, Sell 0.5k var builder = new TradeBuilder(groupingMethod, matchingMethod); builder.SetSecurityManager(_securityManager); var time = _startTime; // Buy 1k builder.ProcessFill( new OrderEvent(1, Symbols.SPY, time, OrderStatus.Filled, OrderDirection.Buy, fillPrice: 1.07m, fillQuantity: 1000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.075m); // Buy 1k builder.ProcessFill( new OrderEvent(2, Symbols.SPY, time.AddMinutes(10), OrderStatus.Filled, OrderDirection.Buy, fillPrice: 1.08m, fillQuantity: 1000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.065m); builder.SetMarketPrice(Symbols.SPY, 1.10m); Split split = null; if (splitFactor != 0) { // apply a 2:1 split split = new Split(Symbols.SPY, time.AddMinutes(5), 1.10m, (decimal)splitFactor, SplitType.SplitOccurred); builder.ApplySplit(split, false, DataNormalizationMode.Raw); } // Sell 1.5k builder.ProcessFill( new OrderEvent(3, Symbols.SPY, time.AddMinutes(20), OrderStatus.Filled, OrderDirection.Sell, fillPrice: AdjustPriceToSplit(1.09m, split), fillQuantity: AdjustQuantityToSplit(-1500, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); // Sell 0.5k builder.ProcessFill( new OrderEvent(4, Symbols.SPY, time.AddMinutes(30), OrderStatus.Filled, OrderDirection.Sell, fillPrice: AdjustPriceToSplit(1.10m, split), fillQuantity: AdjustQuantityToSplit(-500, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsFalse(builder.HasOpenPosition(Symbols.SPY)); switch (groupingMethod) { case FillGroupingMethod.FillToFill: { Assert.AreEqual(3, builder.ClosedTrades.Count); var trade1 = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade1.Symbol); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? time : time.AddMinutes(10), trade1.EntryTime); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? AdjustPriceToSplit(1.07m, split) : AdjustPriceToSplit(1.08m, split), trade1.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade1.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade1.Quantity); Assert.AreEqual(time.AddMinutes(20), trade1.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade1.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 20 : 10, trade1.ProfitLoss); Assert.AreEqual(2, trade1.TotalFees); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -5 : -15, trade1.MAE); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 30 : 20, trade1.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [1, 3] : new[] { 2, 3 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; Assert.AreEqual(Symbols.SPY, trade2.Symbol); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? time.AddMinutes(10) : time, trade2.EntryTime); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? AdjustPriceToSplit(1.08m, split) : AdjustPriceToSplit(1.07m, split), trade2.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade2.Direction); Assert.AreEqual(AdjustQuantityToSplit(500, split), trade2.Quantity); Assert.AreEqual(time.AddMinutes(20), trade2.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 5 : 10, trade2.ProfitLoss); Assert.AreEqual(1, trade2.TotalFees); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -7.5 : -2.5, trade2.MAE); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 10 : 15, trade2.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [2, 3] : new[] { 1, 3 }, trade2.OrderIds); var trade3 = builder.ClosedTrades[2]; Assert.AreEqual(Symbols.SPY, trade3.Symbol); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? time.AddMinutes(10) : time, trade3.EntryTime); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? AdjustPriceToSplit(1.08m, split) : AdjustPriceToSplit(1.07m, split), trade3.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade3.Direction); Assert.AreEqual(AdjustQuantityToSplit(500, split), trade3.Quantity); Assert.AreEqual(time.AddMinutes(30), trade3.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.10m, split), trade3.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 10 : 15, trade3.ProfitLoss); Assert.AreEqual(1, trade3.TotalFees); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -7.5 : -2.5, trade3.MAE); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 10 : 15, trade3.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [2, 4] : new[] { 1, 4 }, trade3.OrderIds); } break; case FillGroupingMethod.FlatToFlat: { Assert.AreEqual(1, builder.ClosedTrades.Count); var trade = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade.Symbol); Assert.AreEqual(time, trade.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.075m, split), trade.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade.Direction); Assert.AreEqual(AdjustQuantityToSplit(2000, split), trade.Quantity); Assert.AreEqual(time.AddMinutes(30), trade.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.0925m, split), trade.ExitPrice); Assert.AreEqual(35, trade.ProfitLoss); Assert.AreEqual(4, trade.TotalFees); Assert.AreEqual(0, trade.MAE); Assert.AreEqual(0, trade.MFE); CollectionAssert.AreEquivalent(new[] { 1, 2, 3, 4 }, trade.OrderIds); } break; case FillGroupingMethod.FlatToReduced: { Assert.AreEqual(2, builder.ClosedTrades.Count); var trade1 = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade1.Symbol); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? time : time.AddMinutes(10), trade1.EntryTime); Assert.Less( Math.Abs(AdjustPriceToSplit(matchingMethod == FillMatchingMethod.FIFO ? 1.0733333333333333333333333333m : 1.0766666666666666666666666667m, split) - trade1.EntryPrice), 1e-27m); Assert.AreEqual(TradeDirection.Long, trade1.Direction); Assert.AreEqual(AdjustQuantityToSplit(1500, split), trade1.Quantity); Assert.AreEqual(time.AddMinutes(20), trade1.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade1.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 25 : 20, trade1.ProfitLoss); Assert.AreEqual(3, trade1.TotalFees); Assert.AreEqual(0, trade1.MAE); Assert.AreEqual(0, trade1.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [1, 2, 3] : new[] { 1, 2, 3 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; Assert.AreEqual(Symbols.SPY, trade2.Symbol); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? time.AddMinutes(10) : time, trade2.EntryTime); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? AdjustPriceToSplit(1.08m, split) : AdjustPriceToSplit(1.07m, split), trade2.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade2.Direction); Assert.AreEqual(AdjustQuantityToSplit(500, split), trade2.Quantity); Assert.AreEqual(time.AddMinutes(30), trade2.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.10m, split), trade2.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 10 : 15, trade2.ProfitLoss); Assert.AreEqual(1, trade2.TotalFees); Assert.AreEqual(0, trade2.MAE); Assert.AreEqual(0, trade2.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [2, 4] : new[] { 1, 4 }, trade2.OrderIds); } break; } } [Test] public void ScaleInScaleOut4Short( [Values] FillGroupingMethod groupingMethod, [Values] FillMatchingMethod matchingMethod, // 0 for no split [Values(0, 0.5, 0.333)] double splitFactor) { // Sell 1k, Sell 1k, Buy 1.5k, Buy 0.5k var builder = new TradeBuilder(groupingMethod, matchingMethod); builder.SetSecurityManager(_securityManager); var time = _startTime; // Sell 1k builder.ProcessFill( new OrderEvent(1, Symbols.SPY, time, OrderStatus.Filled, OrderDirection.Buy, fillPrice: 1.07m, fillQuantity: -1000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.075m); // Sell 1k builder.ProcessFill( new OrderEvent(2, Symbols.SPY, time.AddMinutes(10), OrderStatus.Filled, OrderDirection.Buy, fillPrice: 1.08m, fillQuantity: -1000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.065m); builder.SetMarketPrice(Symbols.SPY, 1.10m); Split split = null; if (splitFactor != 0) { // apply a 2:1 split split = new Split(Symbols.SPY, time.AddMinutes(5), 1.10m, (decimal)splitFactor, SplitType.SplitOccurred); builder.ApplySplit(split, false, DataNormalizationMode.Raw); } // Buy 1.5k builder.ProcessFill( new OrderEvent(3, Symbols.SPY, time.AddMinutes(20), OrderStatus.Filled, OrderDirection.Sell, fillPrice: AdjustPriceToSplit(1.09m, split), fillQuantity: AdjustQuantityToSplit(1500, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); // Buy 0.5k builder.ProcessFill( new OrderEvent(4, Symbols.SPY, time.AddMinutes(30), OrderStatus.Filled, OrderDirection.Sell, fillPrice: AdjustPriceToSplit(1.10m, split), fillQuantity: AdjustQuantityToSplit(500, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsFalse(builder.HasOpenPosition(Symbols.SPY)); switch (groupingMethod) { case FillGroupingMethod.FillToFill: { Assert.AreEqual(3, builder.ClosedTrades.Count); var trade1 = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade1.Symbol); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? time : time.AddMinutes(10), trade1.EntryTime); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? AdjustPriceToSplit(1.07m, split) : AdjustPriceToSplit(1.08m, split), trade1.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade1.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade1.Quantity); Assert.AreEqual(time.AddMinutes(20), trade1.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade1.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -20 : -10, trade1.ProfitLoss); Assert.AreEqual(2, trade1.TotalFees); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -30 : -20, trade1.MAE); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 5 : 15, trade1.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [1, 3] : new[] { 2, 3 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; Assert.AreEqual(Symbols.SPY, trade2.Symbol); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? time.AddMinutes(10) : time, trade2.EntryTime); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? AdjustPriceToSplit(1.08m, split) : AdjustPriceToSplit(1.07m, split), trade2.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade2.Direction); Assert.AreEqual(AdjustQuantityToSplit(500, split), trade2.Quantity); Assert.AreEqual(time.AddMinutes(20), trade2.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade2.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -5 : -10, trade2.ProfitLoss); Assert.AreEqual(1, trade2.TotalFees); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -10 : -15, trade2.MAE); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 7.5 : 2.5, trade2.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [2, 3] : new[] { 1, 3 }, trade2.OrderIds); var trade3 = builder.ClosedTrades[2]; Assert.AreEqual(Symbols.SPY, trade3.Symbol); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? time.AddMinutes(10) : time, trade3.EntryTime); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? AdjustPriceToSplit(1.08m, split) : AdjustPriceToSplit(1.07m, split), trade3.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade3.Direction); Assert.AreEqual(AdjustQuantityToSplit(500, split), trade3.Quantity); Assert.AreEqual(time.AddMinutes(30), trade3.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.10m, split), trade3.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -10 : -15, trade3.ProfitLoss); Assert.AreEqual(1, trade3.TotalFees); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -10 : -15, trade3.MAE); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? 7.5 : 2.5, trade3.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [2, 4] : new[] { 1, 4 }, trade3.OrderIds); } break; case FillGroupingMethod.FlatToFlat: { Assert.AreEqual(1, builder.ClosedTrades.Count); var trade = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade.Symbol); Assert.AreEqual(time, trade.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.075m, split), trade.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade.Direction); Assert.AreEqual(AdjustQuantityToSplit(2000, split), trade.Quantity); Assert.AreEqual(time.AddMinutes(30), trade.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.0925m, split), trade.ExitPrice); Assert.AreEqual(-35, trade.ProfitLoss); Assert.AreEqual(4, trade.TotalFees); Assert.AreEqual(0, trade.MAE); Assert.AreEqual(0, trade.MFE); CollectionAssert.AreEquivalent(new[] { 1, 2, 3, 4 }, trade.OrderIds); } break; case FillGroupingMethod.FlatToReduced: { Assert.AreEqual(2, builder.ClosedTrades.Count); var trade1 = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade1.Symbol); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? time : time.AddMinutes(10), trade1.EntryTime); Assert.Less( Math.Abs(AdjustPriceToSplit(matchingMethod == FillMatchingMethod.FIFO ? 1.0733333333333333333333333333m : 1.0766666666666666666666666667m, split) - trade1.EntryPrice), 1e-27m); Assert.AreEqual(TradeDirection.Short, trade1.Direction); Assert.AreEqual(AdjustQuantityToSplit(1500, split), trade1.Quantity); Assert.AreEqual(time.AddMinutes(20), trade1.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade1.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -25 : -20, trade1.ProfitLoss); Assert.AreEqual(3, trade1.TotalFees); Assert.AreEqual(0, trade1.MAE); Assert.AreEqual(0, trade1.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [1, 2, 3] : new[] { 1, 2, 3 }, trade1.OrderIds); var trade2 = builder.ClosedTrades[1]; Assert.AreEqual(Symbols.SPY, trade2.Symbol); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? time.AddMinutes(10) : time, trade2.EntryTime); Assert.AreEqual( matchingMethod == FillMatchingMethod.FIFO ? AdjustPriceToSplit(1.08m, split) : AdjustPriceToSplit(1.07m, split), trade2.EntryPrice); Assert.AreEqual(TradeDirection.Short, trade2.Direction); Assert.AreEqual(AdjustQuantityToSplit(500, split), trade2.Quantity); Assert.AreEqual(time.AddMinutes(30), trade2.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.10m, split), trade2.ExitPrice); Assert.AreEqual(matchingMethod == FillMatchingMethod.FIFO ? -10 : -15, trade2.ProfitLoss); Assert.AreEqual(1, trade2.TotalFees); Assert.AreEqual(0, trade2.MAE); Assert.AreEqual(0, trade2.MFE); CollectionAssert.AreEquivalent(matchingMethod == FillMatchingMethod.FIFO ? [2, 4] : new[] { 1, 4 }, trade2.OrderIds); } break; } } [Test] public void AllInAllOutLongWithMultiplier( [Values] FillGroupingMethod groupingMethod, [Values] FillMatchingMethod matchingMethod, // 0 for no split [Values(0, 0.5, 0.333)] double splitFactor) { var multiplier = 10; // Buy 1k, Sell 1k var builder = new TradeBuilder(groupingMethod, matchingMethod); builder.SetSecurityManager(_securityManager); var time = _startTime; // Buy 1k builder.ProcessFill( new OrderEvent(1, Symbols.SPY, time, OrderStatus.Filled, OrderDirection.Buy, fillPrice: 1.08m, fillQuantity: 1000, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount, multiplier); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); builder.SetMarketPrice(Symbols.SPY, 1.075m); builder.SetMarketPrice(Symbols.SPY, 1.10m); Split split = null; if (splitFactor != 0) { // apply a 2:1 split split = new Split(Symbols.SPY, time.AddMinutes(5), 1.10m, (decimal)splitFactor, SplitType.SplitOccurred); builder.ApplySplit(split, false, DataNormalizationMode.Raw); } // Sell 1k builder.ProcessFill( new OrderEvent(2, Symbols.SPY, time.AddMinutes(10), OrderStatus.Filled, OrderDirection.Sell, fillPrice: AdjustPriceToSplit(1.09m, split), fillQuantity: AdjustQuantityToSplit(-1000, split), orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount, multiplier); Assert.IsFalse(builder.HasOpenPosition(Symbols.SPY)); Assert.AreEqual(1, builder.ClosedTrades.Count); var trade = builder.ClosedTrades[0]; Assert.AreEqual(Symbols.SPY, trade.Symbol); Assert.AreEqual(time, trade.EntryTime); Assert.AreEqual(AdjustPriceToSplit(1.08m, split), trade.EntryPrice); Assert.AreEqual(TradeDirection.Long, trade.Direction); Assert.AreEqual(AdjustQuantityToSplit(1000, split), trade.Quantity); Assert.AreEqual(time.AddMinutes(10), trade.ExitTime); Assert.AreEqual(AdjustPriceToSplit(1.09m, split), trade.ExitPrice); Assert.AreEqual(10 * multiplier, trade.ProfitLoss); Assert.AreEqual(2, trade.TotalFees); if (groupingMethod == FillGroupingMethod.FillToFill) { Assert.AreEqual(-5 * multiplier, trade.MAE); Assert.AreEqual(20m * multiplier, trade.MFE); } else { Assert.AreEqual(0, trade.MAE); Assert.AreEqual(0, trade.MFE); } CollectionAssert.AreEquivalent(new[] { 1, 2 }, trade.OrderIds); } [Test] public void ITMOptionAssignment( [Values(OrderDirection.Buy, OrderDirection.Sell)] OrderDirection orderDirection, [Values] bool win) { var time = _startTime; var option = GetOption(); var underlying = option.Underlying; option.SetMarketPrice(new Tick { Value = 100m }); var underlyingPrice = 0m; if (win) { underlyingPrice = orderDirection == OrderDirection.Buy ? 300m : 290m; } else { underlyingPrice = orderDirection == OrderDirection.Buy ? 290m : 300m; } underlying.SetMarketPrice(new Tick { Value = underlyingPrice }); var builder = new TradeBuilder(FillGroupingMethod.FillToFill, FillMatchingMethod.FIFO); builder.SetSecurityManager(_securityManager); var quantity = orderDirection == OrderDirection.Buy ? 10 : -10; builder.ProcessFill( new OrderEvent(1, option.Symbol, time, OrderStatus.Filled, orderDirection, 100m, quantity, _orderFee) { IsInTheMoney = true }, ConversionRate, _orderFee.Value.Amount, 100m); Assert.IsTrue(builder.HasOpenPosition(option.Symbol)); var closingOrderDirection = orderDirection == OrderDirection.Buy ? OrderDirection.Sell : OrderDirection.Buy; var ticket = new OrderTicket(null, new SubmitOrderRequest(OrderType.OptionExercise, option.Type, option.Symbol, -quantity, 0, 0, time, "")); builder.ProcessFill( new OrderEvent(2, option.Symbol, time.AddMinutes(10), OrderStatus.Filled, closingOrderDirection, 0m, -quantity, _orderFee) { IsInTheMoney = true, Ticket = ticket }, ConversionRate, _orderFee.Value.Amount, 100m); Assert.IsFalse(builder.HasOpenPosition(option.Symbol)); Assert.AreEqual(1, builder.ClosedTrades.Count); var trade = builder.ClosedTrades[0]; Assert.AreEqual(option.Symbol, trade.Symbol); Assert.AreEqual(win, trade.IsWin); Assert.AreEqual(time, trade.EntryTime); Assert.AreEqual(100m, trade.EntryPrice); Assert.AreEqual(orderDirection == OrderDirection.Buy ? TradeDirection.Long : TradeDirection.Short, trade.Direction); Assert.AreEqual(10, trade.Quantity); Assert.AreEqual(time.AddMinutes(10), trade.ExitTime); Assert.AreEqual(0, trade.ExitPrice); Assert.AreEqual(Math.Sign(quantity) * -100000m, trade.ProfitLoss); Assert.AreEqual(2m, trade.TotalFees); Assert.AreEqual(orderDirection == OrderDirection.Buy ? -100000m : 0m, trade.MAE); Assert.AreEqual(orderDirection == OrderDirection.Buy ? 0m : 100000m, trade.MFE); CollectionAssert.AreEquivalent(new[] { 1, 2 }, trade.OrderIds); } [TestCase(OrderDirection.Buy)] [TestCase(OrderDirection.Sell)] public void OTMOptionAssignment(OrderDirection orderDirection) { var time = _startTime; var option = GetOption(); var underlying = option.Underlying; option.SetMarketPrice(new Tick { Value = 100m }); underlying.SetMarketPrice(new Tick { Value = 150 }); var builder = new TradeBuilder(FillGroupingMethod.FillToFill, FillMatchingMethod.FIFO); builder.SetSecurityManager(_securityManager); var quantity = orderDirection == OrderDirection.Buy ? 10 : -10; builder.ProcessFill( new OrderEvent(1, option.Symbol, time, OrderStatus.Filled, orderDirection, 100m, quantity, _orderFee) { IsInTheMoney = true }, ConversionRate, _orderFee.Value.Amount, 100m); Assert.IsTrue(builder.HasOpenPosition(option.Symbol)); var closingOrderDirection = orderDirection == OrderDirection.Buy ? OrderDirection.Sell : OrderDirection.Buy; var ticket = new OrderTicket(null, new SubmitOrderRequest(OrderType.OptionExercise, option.Type, option.Symbol, -quantity, 0, 0, time, "")); builder.ProcessFill( new OrderEvent(2, option.Symbol, time.AddMinutes(10), OrderStatus.Filled, closingOrderDirection, 0m, -quantity, _orderFee) { IsInTheMoney = true, Ticket = ticket }, ConversionRate, _orderFee.Value.Amount, 100m); Assert.IsFalse(builder.HasOpenPosition(option.Symbol)); Assert.AreEqual(1, builder.ClosedTrades.Count); var trade = builder.ClosedTrades[0]; Assert.AreEqual(option.Symbol, trade.Symbol); Assert.AreEqual(orderDirection == OrderDirection.Buy ? false : true, trade.IsWin); Assert.AreEqual(time, trade.EntryTime); Assert.AreEqual(100m, trade.EntryPrice); Assert.AreEqual(orderDirection == OrderDirection.Buy ? TradeDirection.Long : TradeDirection.Short, trade.Direction); Assert.AreEqual(10, trade.Quantity); Assert.AreEqual(time.AddMinutes(10), trade.ExitTime); Assert.AreEqual(0, trade.ExitPrice); Assert.AreEqual(Math.Sign(quantity) * -100000m, trade.ProfitLoss); Assert.AreEqual(2m, trade.TotalFees); Assert.AreEqual(orderDirection == OrderDirection.Buy ? -100000m : 0m, trade.MAE); Assert.AreEqual(orderDirection == OrderDirection.Buy ? 0m : 100000m, trade.MFE); CollectionAssert.AreEquivalent(new[] { 1, 2 }, trade.OrderIds); } [Test] public void OptionPositionCloseWithoutExercise( [Values(OrderDirection.Buy, OrderDirection.Sell)] OrderDirection orderDirection, [Values] bool win) { var time = _startTime; var option = GetOption(); var underlying = option.Underlying; underlying.SetMarketPrice(new Tick { Value = 300m }); var builder = new TradeBuilder(FillGroupingMethod.FillToFill, FillMatchingMethod.FIFO); builder.SetSecurityManager(_securityManager); var initialOptionPrice = 100m; option.SetMarketPrice(new Tick { Value = initialOptionPrice }); var quantity = orderDirection == OrderDirection.Buy ? 10 : -10; builder.ProcessFill( new OrderEvent(1, option.Symbol, time, OrderStatus.Filled, orderDirection, 100m, quantity, _orderFee) { IsInTheMoney = true }, ConversionRate, _orderFee.Value.Amount, 100m); Assert.IsTrue(builder.HasOpenPosition(option.Symbol)); // Before closing, update option market price var finalOptionPrice = 0m; if (win) { finalOptionPrice = orderDirection == OrderDirection.Buy ? 150m : 50m; } else { finalOptionPrice = orderDirection == OrderDirection.Buy ? 50m : 150m; } option.SetMarketPrice(new Tick { Value = finalOptionPrice }); var closingOrderDirection = orderDirection == OrderDirection.Buy ? OrderDirection.Sell : OrderDirection.Buy; var ticket = new OrderTicket(null, new SubmitOrderRequest(OrderType.Market, option.Type, option.Symbol, -quantity, 0, 0, time, "")); builder.ProcessFill( new OrderEvent(2, option.Symbol, time.AddMinutes(10), OrderStatus.Filled, closingOrderDirection, finalOptionPrice, -quantity, _orderFee) { IsInTheMoney = true, Ticket = ticket, }, ConversionRate, _orderFee.Value.Amount, 100m); Assert.IsFalse(builder.HasOpenPosition(option.Symbol)); Assert.AreEqual(1, builder.ClosedTrades.Count); var trade = builder.ClosedTrades[0]; var expectedProfitLoss = (finalOptionPrice - initialOptionPrice) * quantity * 100m; Assert.AreEqual(option.Symbol, trade.Symbol); Assert.AreEqual(win, trade.IsWin); Assert.AreEqual(time, trade.EntryTime); Assert.AreEqual(initialOptionPrice, trade.EntryPrice); Assert.AreEqual(orderDirection == OrderDirection.Buy ? TradeDirection.Long : TradeDirection.Short, trade.Direction); Assert.AreEqual(10, trade.Quantity); Assert.AreEqual(time.AddMinutes(10), trade.ExitTime); Assert.AreEqual(finalOptionPrice, trade.ExitPrice); Assert.AreEqual(expectedProfitLoss, trade.ProfitLoss); CollectionAssert.AreEquivalent(new[] { 1, 2 }, trade.OrderIds); } [TestCaseSource(nameof(DrawdownTestCases))] public void DrawdownCalculation(PositionSide entrySide, decimal[] prices, decimal expectedDrawdown) { if (prices.Length < 2) { Assert.Fail("At least two prices are required to perform the test."); } // Buy 1k, Sell 1k (entrySide == Long) or Sell 1k, Buy 1k (entrySide == Short) var builder = new TradeBuilder(FillGroupingMethod.FillToFill, FillMatchingMethod.FIFO); builder.SetSecurityManager(_securityManager); var time = _startTime; var quantity = (entrySide == PositionSide.Long ? 1 : -1) * 1000m; // Open position builder.ProcessFill( new OrderEvent(1, Symbols.SPY, time, OrderStatus.Filled, entrySide == PositionSide.Long ? OrderDirection.Buy : OrderDirection.Sell, fillPrice: prices[0], fillQuantity: quantity, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsTrue(builder.HasOpenPosition(Symbols.SPY)); for (int i = 1; i < prices.Length - 1; i++) { builder.SetMarketPrice(Symbols.SPY, prices[i]); } // Close position builder.ProcessFill( new OrderEvent(2, Symbols.SPY, time.AddMinutes(10), OrderStatus.Filled, entrySide == PositionSide.Long ? OrderDirection.Sell : OrderDirection.Buy, fillPrice: prices[^1], fillQuantity: -quantity, orderFee: _orderFee), ConversionRate, _orderFee.Value.Amount); Assert.IsFalse(builder.HasOpenPosition(Symbols.SPY)); Assert.AreEqual(1, builder.ClosedTrades.Count); var trade = builder.ClosedTrades[0]; Assert.AreEqual(expectedDrawdown * Math.Abs(quantity), trade.EndTradeDrawdown); } private static IEnumerable DrawdownTestCases { get { // Long trades // ------------------------------- // Price 100 -> 120 -> 110 // /\ // / \ // / ---- // / // ---- // We expect a drawdown of 10 (from 120 to 110) yield return new TestCaseData(PositionSide.Long, new[] { 100m, 120m, 110m }, 10m).SetName($"DrawdownLongTrade_SingleDrawdown"); // Price 100 -> 140 -> 120 -> 130 -> 110 // /\ // / \ // / \ /\ // / \/ \ // / \ // / \ // / ---- // / // ---- // We expect a drawdown of 30 (from 140 to 110) yield return new TestCaseData(PositionSide.Long, new[] { 100m, 140m, 120m, 130m, 110m }, 30m).SetName($"DrawdownLongTrade_MultipleDrawdownsOnSingleHighestPrice"); // Price 100 -> 120 -> 110 -> 120 -> 140 -> 115 // /\ // / \ // / \ // / \ // /\ / \ // / \/ \ // / \ // / ---- // ---- // We expect a drawdown of 25 (from 140 to 115) yield return new TestCaseData(PositionSide.Long, new[] { 100m, 120m, 110m, 120m, 140m, 115m }, 25m).SetName($"DrawdownLongTrade_HighestDrawdownOnNewHighestPrice"); // Price 100 -> 120 -> 110 -> 120 -> 130 -> 125 // /\ // / ---- // /\ / // / \/ // / // / // ---- // We expect a drawdown of 10 (from 120 to 110) yield return new TestCaseData(PositionSide.Long, new[] { 100m, 120m, 110m, 120m, 130m, 125m }, 10m).SetName($"DrawdownLongTrade_LowerDrawdownOnNewHighestPrice"); // Price 100 -> 80 -> 110 // ---- // / // ---- / // \ / // \ / // \ / // \/ // We expect a drawdown of 20 (from 100 to 80) yield return new TestCaseData(PositionSide.Long, new[] { 100m, 80m, 110m }, 20m).SetName($"DrawdownLongTrade_PriceGoesBelowEntryPrice"); // Price 100 -> 90 -> 130 -> 110 // /\ // / \ // / \ // / \ // / ---- // / // ---- / // \ / // \/ // We expect a drawdown of 20 (from 130 to 110 which is higher than the first one from 100 to 90) yield return new TestCaseData(PositionSide.Long, new[] { 100m, 90m, 130m, 110m }, 20m).SetName($"DrawdownLongTrade_HigherDrawdownAfterPriceGoesBelowEntryPrice"); // Short trades // ------------------------------- // Price 100 -> 80 -> 90 // ---- // \ // \ ---- // \ / // \/ // We expect a drawdown of 10 (from 80 to 90) yield return new TestCaseData(PositionSide.Short, new[] { 100m, 80m, 90m }, 10m).SetName($"DrawdownShortTrade_SingleDrawdown"); // Price 100 -> 60 -> 80 -> 70 -> 90 // ---- // \ // \ ---- // \ / // \ / // \ /\ / // \ / \/ // \ / // \/ // We expect a drawdown of 30 (from 60 to 90) yield return new TestCaseData(PositionSide.Short, new[] { 100m, 60m, 80m, 70m, 90m }, 30m).SetName($"DrawdownShortTrade_MultipleDrawdownsOnSingleLowestPrice"); // Price 100 -> 80 -> 90 -> 80 -> 60 -> 85 // ---- // \ ---- // \ / // \ /\ / // \/ \ / // \ / // \/ // We expect a drawdown of 25 (from 60 to 85) yield return new TestCaseData(PositionSide.Short, new[] { 100m, 80m, 90m, 80m, 60m, 85m }, 25m).SetName($"DrawdownShortTrade_HighestDrawdownOnNewLowestPrice"); // Price 100 -> 80 -> 90 -> 80 -> 70 -> 75 // ---- // \ // \ // \ /\ // \/ \ // \ ---- // \/ // We expect a drawdown of 10 (from 80 to 90) yield return new TestCaseData(PositionSide.Short, new[] { 100m, 80m, 90m, 80m, 70m, 75m }, 10m).SetName($"DrawdownShortTrade_LowerDrawdownOnNewLowestPrice"); // Price 100 -> 120 -> 90 // /\ // / \ // / \ // / \ // ---- \ // \ // \ // ---- // We expect a drawdown of 20 (from 100 to 120) yield return new TestCaseData(PositionSide.Short, new[] { 100m, 120m, 90m }, 20m).SetName($"DrawdownShortTrade_PriceGoesAboveEntryPrice"); // Price 100 -> 110 -> 70 -> 90 // /\ // / \ // ---- \ // \ // \ ---- // \ / // \ / // \ / // \/ // We expect a drawdown of 20 (from 70 to 90 which is higher than the first one from 100 to 110) yield return new TestCaseData(PositionSide.Short, new[] { 100m, 110m, 70m, 90m }, 20m).SetName($"DrawdownShortTrade_HigherDrawdownAfterPriceGoesAboveEntryPrice"); } } private Option GetOption() { var underlying = new Security( SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork), new SubscriptionDataConfig( typeof(TradeBar), Symbols.SPY, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, true, false), new Cash(Currencies.USD, 0, 1m), SymbolProperties.GetDefault(Currencies.USD), ErrorCurrencyConverter.Instance, RegisteredSecurityDataTypesProvider.Null, new SecurityCache() ); var option = new Option( Symbols.SPY_C_192_Feb19_2016, SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork), new Cash(Currencies.USD, 0, 1m), new OptionSymbolProperties("", Currencies.USD, 100, 0.01m, 1), ErrorCurrencyConverter.Instance, RegisteredSecurityDataTypesProvider.Null, new SecurityCache(), underlying ); _securityManager.Add(underlying); _securityManager.Add(option); return option; } private static decimal AdjustQuantityToSplit(decimal quantity, Split split) { return split == null ? quantity : quantity / split.SplitFactor; } private static decimal AdjustPriceToSplit(decimal price, Split split) { return split == null ? price : price * split.SplitFactor; } } }