3040 lines
157 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|