Files
quantconnect--lean/Tests/Common/Orders/Fills/EquityFillModelTests.StopMarketFill.cs
2026-07-13 13:02:50 +08:00

329 lines
14 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.Linq;
using NUnit.Framework;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Optimizer.Parameters;
using QuantConnect.Orders;
using QuantConnect.Orders.Fills;
using QuantConnect.Securities;
using QuantConnect.Tests.Common.Data;
using QuantConnect.Tests.Common.Securities;
namespace QuantConnect.Tests.Common.Orders.Fills
{
[TestFixture]
public partial class EquityFillModelTests
{
[Test]
public void PerformsStopMarketFillBuy()
{
var model = new EquityFillModel();
var order = new StopMarketOrder(Symbols.SPY, 100, 101.5m, Noon);
var parameters = GetFillModelParameters(order);
var security = parameters.Security;
security.SetLocalTimeKeeper(TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
security.SetMarketPrice(new TradeBar(Noon, Symbols.SPY, 101m, 101m, 101m, 101m, 100));
var fill = model.Fill(parameters).Single();
Assert.AreEqual(0, fill.FillQuantity);
Assert.AreEqual(0, fill.FillPrice);
Assert.AreEqual(OrderStatus.None, fill.Status);
security.SetMarketPrice(new TradeBar(Noon, Symbols.SPY, 101m, 102.5m, 101m, 102m, 100));
fill = model.StopMarketFill(security, order);
Assert.AreEqual(order.Quantity, fill.FillQuantity);
Assert.AreEqual(order.StopPrice, fill.FillPrice);
Assert.AreEqual(OrderStatus.Filled, fill.Status);
}
[Test]
public void PerformsStopMarketFillSell()
{
var model = new EquityFillModel();
var order = new StopMarketOrder(Symbols.SPY, -100, 101.5m, Noon);
var parameters = GetFillModelParameters(order);
var security = parameters.Security;
security.SetLocalTimeKeeper(TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
security.SetMarketPrice(new TradeBar(Noon, Symbols.SPY, 102m, 102m, 102m, 102m, 100));
var fill = model.Fill(parameters).Single();
Assert.AreEqual(0, fill.FillQuantity);
Assert.AreEqual(0, fill.FillPrice);
Assert.AreEqual(OrderStatus.None, fill.Status);
security.SetMarketPrice(new TradeBar(Noon, Symbols.SPY, 102m, 102.5m, 101m, 101.5m, 100));
fill = model.StopMarketFill(security, order);
Assert.AreEqual(order.Quantity, fill.FillQuantity);
Assert.AreEqual(order.StopPrice, fill.FillPrice);
Assert.AreEqual(OrderStatus.Filled, fill.Status);
}
[TestCase(100, 290.50)]
[TestCase(-100, 291.50)]
public void PerformsStopMarketFillWithTickTradeData(decimal orderQuantity, decimal stopPrice)
{
var time = new DateTime(2018, 9, 24, 9, 30, 0);
var timeKeeper = new TimeKeeper(time.ConvertToUtc(TimeZones.NewYork), TimeZones.NewYork);
var fillModel = new EquityFillModel();
var order = new StopMarketOrder(Symbols.SPY, orderQuantity, stopPrice, time.ConvertToUtc(TimeZones.NewYork));
var configTick = CreateTickConfig(Symbols.SPY);
var equity = CreateEquity(configTick);
equity.SetLocalTimeKeeper(timeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
// The order will not fill with this price
var tradeTick = new Tick { TickType = TickType.Trade, Time = time, Value = 291m };
equity.SetMarketPrice(tradeTick);
time += TimeSpan.FromMinutes(1);
timeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
var fill = fillModel.Fill(new FillModelParameters(
equity,
order,
new MockSubscriptionDataConfigProvider(configTick),
Time.OneHour,
null)).Single();
// Do not fill on stale data
Assert.AreEqual(0, fill.FillQuantity);
Assert.AreEqual(0, fill.FillPrice);
Assert.AreEqual(OrderStatus.None, fill.Status);
time += TimeSpan.FromMinutes(2);
timeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
// Create a series of price where the last value will not fill
// and the fill model need to use the minimum/maximum instead
var trades = new[] { 0.1m, -0.1m, 0m, 0.1m, 0m }
.Select(delta => new Tick
{
TickType = TickType.Trade,
Time = time,
Value = stopPrice - delta * Math.Sign(orderQuantity)
})
.ToList();
equity.Update(trades, typeof(Tick));
fill = fillModel.StopMarketFill(equity, order);
Assert.AreEqual(OrderStatus.Filled, fill.Status);
Assert.AreEqual(order.Quantity, fill.FillQuantity);
// Fill at the stop price because the equity price matches it
Assert.AreEqual(stopPrice, fill.FillPrice);
}
[TestCase(100, 290.50)]
[TestCase(-100, 291.50)]
public void StopMarketOrderDoesNotFillUsingQuoteBar(decimal orderQuantity, decimal stopPrice)
{
var time = new DateTime(2018, 9, 24, 9, 30, 0);
var timeKeeper = new TimeKeeper(time.ConvertToUtc(TimeZones.NewYork), TimeZones.NewYork);
var fillModel = new EquityFillModel();
var order = new StopMarketOrder(Symbols.SPY, orderQuantity, stopPrice, time.ConvertToUtc(TimeZones.NewYork));
var parameters = GetFillModelParameters(order);
var equity = parameters.Security;
equity.SetLocalTimeKeeper(timeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
// The order will not fill with these prices
var tradeBar = new TradeBar(time.AddMinutes(-10), Symbols.SPY, 291m, 291m, 291m, 291m, 12345);
equity.SetMarketPrice(tradeBar);
time += TimeSpan.FromMinutes(1);
timeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
var fill = fillModel.Fill(parameters).Single();
// Do not fill on stale data
Assert.AreEqual(0, fill.FillQuantity);
Assert.AreEqual(0, fill.FillPrice);
Assert.AreEqual(OrderStatus.None, fill.Status);
time += TimeSpan.FromMinutes(2);
timeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
var quoteBar = new QuoteBar(time, Symbols.SPY,
new Bar(290m, 292m, 289m, 291m), 12345,
new Bar(290m, 292m, 289m, 291m), 12345);
equity.SetMarketPrice(quoteBar);
fill = fillModel.StopMarketFill(equity, order);
// Stop market orders don't fill with QuoteBar:
Assert.AreEqual(0, fill.FillQuantity);
Assert.AreEqual(0, fill.FillPrice);
Assert.AreEqual(OrderStatus.None, fill.Status);
}
[TestCase(100, 290.50)]
[TestCase(-100, 291.50)]
public void StopMarketOrderDoesNotFillUsingTickTypeQuote(decimal orderQuantity, decimal stopPrice)
{
var time = new DateTime(2018, 9, 24, 9, 30, 0);
var timeKeeper = new TimeKeeper(time.ConvertToUtc(TimeZones.NewYork), TimeZones.NewYork);
var fillModel = new EquityFillModel();
var order = new StopMarketOrder(Symbols.SPY, orderQuantity, stopPrice, time.ConvertToUtc(TimeZones.NewYork));
var configTick = CreateTickConfig(Symbols.SPY);
var equity = CreateEquity(configTick);
equity.SetLocalTimeKeeper(timeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
// The order will not fill with this price
var tradeTick = new Tick { TickType = TickType.Trade, Time = time, Value = 291m };
equity.SetMarketPrice(tradeTick);
time += TimeSpan.FromMinutes(1);
timeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
var fill = fillModel.Fill(new FillModelParameters(
equity,
order,
new MockSubscriptionDataConfigProvider(configTick),
Time.OneHour,
null)).Single();
// Do not fill on stale data
Assert.AreEqual(0, fill.FillQuantity);
Assert.AreEqual(0, fill.FillPrice);
Assert.AreEqual(OrderStatus.None, fill.Status);
time += TimeSpan.FromMinutes(2);
timeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
var price = stopPrice - 0.1m * Math.Sign(orderQuantity);
var quoteTick = new Tick { TickType = TickType.Quote, Time = time, BidPrice = price, AskPrice = price };
equity.SetMarketPrice(quoteTick);
fill = fillModel.StopMarketFill(equity, order);
// Stop market orders don't fill with TickType.Quote:
Assert.AreEqual(0, fill.FillQuantity);
Assert.AreEqual(0, fill.FillPrice);
Assert.AreEqual(OrderStatus.None, fill.Status);
}
[TestCase(100, 290.50)]
[TestCase(-100, 291.50)]
public void StopMarketOrderFillsAtOpenWithUnfavourableGap(decimal orderQuantity, decimal stopPrice)
{
// See https://github.com/QuantConnect/Lean/issues/4545
var time = new DateTime(2018, 9, 24, 9, 30, 0);
var timeKeeper = new TimeKeeper(time.ConvertToUtc(TimeZones.NewYork), TimeZones.NewYork);
var fillModel = new EquityFillModel();
var order = new StopMarketOrder(Symbols.SPY, orderQuantity, stopPrice, time.ConvertToUtc(TimeZones.NewYork));
var parameters = GetFillModelParameters(order);
var equity = parameters.Security;
equity.SetLocalTimeKeeper(timeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
// The order will not fill with these prices
equity.SetMarketPrice(new TradeBar(time.AddMinutes(-10), Symbols.SPY, 291m, 291m, 291m, 291m, 12345));
var fill = fillModel.Fill(parameters).Single();
// Do not fill on stale data
Assert.AreEqual(0, fill.FillQuantity);
Assert.AreEqual(0, fill.FillPrice);
Assert.AreEqual(OrderStatus.None, fill.Status);
time += TimeSpan.FromMinutes(2);
timeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
// The Gap TradeBar has all prices below/above the stop price
var gapTradeBar = Math.Sign(orderQuantity) switch
{
1 => new TradeBar(time, Symbols.SPY, stopPrice + 1, stopPrice + 2, stopPrice + 1, stopPrice + 1, 12345),
-1 => new TradeBar(time, Symbols.SPY, stopPrice - 1, stopPrice - 1, stopPrice - 2, stopPrice - 1, 12345),
};
equity.SetMarketPrice(gapTradeBar);
fill = fillModel.StopMarketFill(equity, order);
// Fills at the open
Assert.AreEqual(gapTradeBar.Open, fill.FillPrice);
Assert.AreEqual(order.Quantity, fill.FillQuantity);
Assert.AreEqual(OrderStatus.Filled, fill.Status);
}
[TestCase(100, 291.50)]
[TestCase(-100, 290.50)]
public void StopMarketOrderDoesNotFillUsingDataBeforeSubmitTime(decimal orderQuantity, decimal stopPrice)
{
var time = new DateTime(2018, 9, 24, 9, 30, 0);
var timeKeeper = new TimeKeeper(time.ConvertToUtc(TimeZones.NewYork), TimeZones.NewYork);
var fillModel = new EquityFillModel();
var order = new StopMarketOrder(Symbols.SPY, orderQuantity, stopPrice, time.ConvertToUtc(TimeZones.NewYork));
var parameters = GetFillModelParameters(order);
var security = parameters.Security;
security.SetLocalTimeKeeper(timeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
var tradeBar = new TradeBar(time.AddMinutes(-10), Symbols.SPY, 290m, 292m, 289m, 291m, 12345);
security.SetMarketPrice(tradeBar);
time += TimeSpan.FromMinutes(1);
timeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
var fillForwardBar = (TradeBar)tradeBar.Clone(true);
security.SetMarketPrice(fillForwardBar);
var fill = fillModel.Fill(parameters).Single();
Assert.AreEqual(0, fill.FillQuantity);
Assert.AreEqual(0, fill.FillPrice);
Assert.AreEqual(OrderStatus.None, fill.Status);
time += TimeSpan.FromMinutes(1);
timeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
tradeBar = new TradeBar(time, Symbols.SPY, stopPrice - 0.01m * Math.Sign(orderQuantity), 292m, 289m, stopPrice, 12345);
security.SetMarketPrice(tradeBar);
fill = fillModel.StopMarketFill(security, order);
Assert.AreEqual(orderQuantity, fill.FillQuantity);
Assert.AreEqual(stopPrice, fill.FillPrice);
Assert.AreEqual(OrderStatus.Filled, fill.Status);
}
}
}