Files
2026-07-13 13:02:50 +08:00

3040 lines
157 KiB
C#

/*
* 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<TestCaseData> 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;
}
}
}