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

1531 lines
74 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.Brokerages;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Indicators;
using QuantConnect.Orders;
using QuantConnect.Orders.Fills;
using QuantConnect.Securities;
using QuantConnect.Securities.Forex;
using QuantConnect.Tests.Common.Data;
using QuantConnect.Tests.Common.Securities;
using QuantConnect.Securities.Equity;
using System.Linq;
using QuantConnect.Data.Auxiliary;
namespace QuantConnect.Tests.Common.Orders.Fills
{
[TestFixture]
public partial class EquityFillModelTests
{
private static readonly DateTime Noon = new DateTime(2014, 6, 24, 12, 0, 0);
private TimeKeeper TimeKeeper;
[SetUp]
public void Setup()
{
TimeKeeper = new TimeKeeper(Noon.ConvertToUtc(TimeZones.NewYork), new[] { TimeZones.NewYork });
}
[TestCase(11, 11, 11, "")]
[TestCase(12, 11, 11, "")]
[TestCase(12, 10, 11, "Warning: No quote information")]
// Note: a (12, 10, 10) case where the only data is a stale (>1h old) minute bar no longer fills; it now waits
// for fresh data. That behavior is covered by MarketOrderWaitsForFreshDataWhenStaleByMoreThanResolution.
public void PerformsMarketFillBuy(int orderHour, int quoteBarHour, int tradeBarHour, string message)
{
var configTradeBar = CreateTradeBarConfig(Symbols.SPY);
var configQuoteBar = new SubscriptionDataConfig(configTradeBar, typeof(QuoteBar));
var configProvider = new MockSubscriptionDataConfigProvider(configQuoteBar);
configProvider.SubscriptionDataConfigs.Add(configTradeBar);
var equity = CreateEquity(configTradeBar);
var orderTime = new DateTime(2014, 6, 24, orderHour, 0, 0).ConvertToUtc(equity.Exchange.TimeZone);
var quoteBarTime = new DateTime(2014, 6, 24, quoteBarHour, 0, 0).AddMinutes(-1);
var tradeBarTime = new DateTime(2014, 6, 24, tradeBarHour, 0, 0).AddMinutes(-1);
var model = (EquityFillModel)equity.FillModel;
var order = new MarketOrder(Symbols.SPY, 100, orderTime);
var parameters = new FillModelParameters(equity, order, configProvider, Time.OneHour, null);
// Sets price at time zero
equity.SetLocalTimeKeeper(TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
equity.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, 101.123m));
// IndicatorDataPoint is not market data
Assert.Throws<InvalidOperationException>(() => model.Fill(parameters),
$"Cannot get ask price to perform fill for {equity.Symbol} because no market data subscription were found.");
const decimal close = 101.234m;
var bidBar = new Bar(101.123m, 101.123m, 101.123m, 101.123m);
var askBar = new Bar(101.234m, 101.234m, 101.234m, close);
var tradeBar = new TradeBar(tradeBarTime, Symbols.SPY, 101.123m, 101.123m, 101.123m, close, 100);
equity.SetMarketPrice(new QuoteBar(quoteBarTime, Symbols.SPY, bidBar, 0, askBar, 0));
equity.SetMarketPrice(tradeBar);
var fill = model.Fill(parameters).Single();
Assert.AreEqual(order.Quantity, fill.FillQuantity);
Assert.AreEqual(close, fill.FillPrice);
Assert.AreEqual(OrderStatus.Filled, fill.Status);
Assert.IsTrue(fill.Message.StartsWith(message, StringComparison.InvariantCultureIgnoreCase));
}
[TestCase(11, 11, 11, "")]
[TestCase(12, 11, 11, "")]
[TestCase(12, 10, 11, "Warning: No quote information")]
// Note: a (12, 10, 10) case where the only data is a stale (>1h old) minute bar no longer fills; it now waits
// for fresh data. That behavior is covered by MarketOrderWaitsForFreshDataWhenStaleByMoreThanResolution.
public void PerformsMarketFillSell(int orderHour, int quoteBarHour, int tradeBarHour, string message)
{
var configTradeBar = CreateTradeBarConfig(Symbols.SPY);
var configQuoteBar = new SubscriptionDataConfig(configTradeBar, typeof(QuoteBar));
var configProvider = new MockSubscriptionDataConfigProvider(configQuoteBar);
configProvider.SubscriptionDataConfigs.Add(configTradeBar);
var equity = CreateEquity(configTradeBar);
var orderTime = new DateTime(2014, 6, 24, orderHour, 0, 0).ConvertToUtc(equity.Exchange.TimeZone);
var quoteBarTime = new DateTime(2014, 6, 24, quoteBarHour, 0, 0).AddMinutes(-1);
var tradeBarTime = new DateTime(2014, 6, 24, tradeBarHour, 0, 0).AddMinutes(-1);
var model = (EquityFillModel)equity.FillModel;
var order = new MarketOrder(Symbols.SPY, -100, orderTime);
var parameters = new FillModelParameters(equity, order, configProvider, Time.OneHour, null);
// Sets price at time zero
equity.SetLocalTimeKeeper(TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
equity.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, 101.123m));
// IndicatorDataPoint is not market data
Assert.Throws<InvalidOperationException>(() => model.Fill(parameters),
$"Cannot get bid price to perform fill for {equity.Symbol} because no market data subscription were found.");
const decimal close = 101.123m;
var bidBar = new Bar(101.123m, 101.123m, 101.123m, close);
var askBar = new Bar(101.234m, 101.234m, 101.234m, 101.234m);
var tradeBar = new TradeBar(tradeBarTime, Symbols.SPY, 101.234m, 101.234m, 101.234m, close, 100);
equity.SetMarketPrice(new QuoteBar(quoteBarTime, Symbols.SPY, bidBar, 0, askBar, 0));
equity.SetMarketPrice(tradeBar);
var fill = model.Fill(parameters).Single();
Assert.AreEqual(order.Quantity, fill.FillQuantity);
Assert.AreEqual(close, fill.FillPrice);
Assert.AreEqual(OrderStatus.Filled, fill.Status);
Assert.IsTrue(fill.Message.StartsWith(message, StringComparison.InvariantCultureIgnoreCase));
}
[Test]
public void PerformsStopLimitFillBuy()
{
var model = new EquityFillModel();
var order = new StopLimitOrder(Symbols.SPY, 100, 101.5m, 101.75m, Noon);
var config = CreateTradeBarConfig(Symbols.SPY);
var security = new Security(
SecurityExchangeHoursTests.CreateUsEquitySecurityExchangeHours(),
config,
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
);
security.SetLocalTimeKeeper(TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, 100m));
var fill = model.Fill(new FillModelParameters(
security,
order,
new MockSubscriptionDataConfigProvider(config),
Time.OneHour,
null)).Single();
Assert.AreEqual(0, fill.FillQuantity);
Assert.AreEqual(0, fill.FillPrice);
Assert.AreEqual(OrderStatus.None, fill.Status);
security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, 102m));
fill = model.Fill(new FillModelParameters(
security,
order,
new MockSubscriptionDataConfigProvider(config),
Time.OneHour,
null)).Single();
Assert.AreEqual(0, fill.FillQuantity);
Assert.AreEqual(0, fill.FillPrice);
Assert.AreEqual(OrderStatus.None, fill.Status);
security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, 101.66m));
fill = model.StopLimitFill(security, order);
// this fills worst case scenario, so it's at the limit price
Assert.AreEqual(order.Quantity, fill.FillQuantity);
Assert.AreEqual(security.High, fill.FillPrice);
Assert.AreEqual(OrderStatus.Filled, fill.Status);
}
[Test]
public void PerformsStopLimitFillSell()
{
var model = new EquityFillModel();
var order = new StopLimitOrder(Symbols.SPY, -100, 101.75m, 101.50m, Noon);
var config = CreateTradeBarConfig(Symbols.SPY);
var security = new Security(
SecurityExchangeHoursTests.CreateUsEquitySecurityExchangeHours(),
config,
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
);
security.SetLocalTimeKeeper(TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, 102m));
var fill = model.Fill(new FillModelParameters(
security,
order,
new MockSubscriptionDataConfigProvider(config),
Time.OneHour,
null)).Single();
Assert.AreEqual(0, fill.FillQuantity);
Assert.AreEqual(0, fill.FillPrice);
Assert.AreEqual(OrderStatus.None, fill.Status);
security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, 101m));
fill = model.Fill(new FillModelParameters(
security,
order,
new MockSubscriptionDataConfigProvider(config),
Time.OneHour,
null)).Single();
Assert.AreEqual(0, fill.FillQuantity);
Assert.AreEqual(0, fill.FillPrice);
Assert.AreEqual(OrderStatus.None, fill.Status);
security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, 101.66m));
fill = model.StopLimitFill(security, order);
// this fills worst case scenario, so it's at the limit price
Assert.AreEqual(order.Quantity, fill.FillQuantity);
Assert.AreEqual(security.Low, fill.FillPrice);
Assert.AreEqual(OrderStatus.Filled, fill.Status);
}
[Test]
public void PerformsLimitIfTouchedSell()
{
var model = new EquityFillModel();
var order = new LimitIfTouchedOrder(Symbols.SPY, -100, 101.5m, 105m, Noon);
var configTradeBar = CreateTradeBarConfig(Symbols.SPY);
var configQuoteBar = new SubscriptionDataConfig(configTradeBar, typeof(QuoteBar));
var configProvider = new MockSubscriptionDataConfigProvider(configQuoteBar);
var security = new Security(
SecurityExchangeHoursTests.CreateUsEquitySecurityExchangeHours(),
configTradeBar,
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
);
// Sets price at time zero
security.SetLocalTimeKeeper(TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
security.SetMarketPrice(new TradeBar(Noon, Symbols.SPY, 100m, 100m, 90m, 90m, 100));
configProvider.SubscriptionDataConfigs.Add(configTradeBar);
var fill = model.Fill(new FillModelParameters(
security,
order,
configProvider,
Time.OneHour,
null)).Single();
Assert.AreEqual(0, fill.FillQuantity);
Assert.AreEqual(0, fill.FillPrice);
Assert.AreEqual(OrderStatus.None, fill.Status);
// Time jump => trigger touched but not limit
security.SetMarketPrice(new TradeBar(Noon, Symbols.SPY, 102m, 103m, 102m, 102m, 100));
security.SetMarketPrice(new QuoteBar(Noon, Symbols.SPY,
new Bar(101m, 102m, 100m, 100m), 100, // Bid bar
new Bar(103m, 104m, 102m, 102m), 100) // Ask bar
);
Assert.AreEqual(0, fill.FillQuantity);
Assert.AreEqual(0, fill.FillPrice);
Assert.AreEqual(OrderStatus.None, fill.Status);
fill = model.Fill(new FillModelParameters(
security,
order,
configProvider,
Time.OneHour,
null)).Single();
Assert.AreEqual(0, fill.FillQuantity);
Assert.AreEqual(0, fill.FillPrice);
Assert.AreEqual(OrderStatus.None, fill.Status);
// Time jump => limit reached, security bought
// |---> First, ensure that price data are not used to fill
security.SetMarketPrice(new TradeBar(Noon, Symbols.SPY, 100m, 100m, 99m, 99m, 100));
fill = model.LimitIfTouchedFill(security, order);
Assert.AreEqual(0, fill.FillQuantity);
Assert.AreEqual(0, fill.FillPrice);
Assert.AreEqual(OrderStatus.None, fill.Status);
// |---> Lastly, ensure that quote data used to fill
security.SetMarketPrice(new QuoteBar(Noon, Symbols.SPY,
new Bar(105m, 105m, 105m, 105m), 100, // Bid bar
new Bar(105m, 105m, 105m, 105m), 100) // Ask bar
);
fill = model.LimitIfTouchedFill(security, order);
Assert.AreEqual(order.Quantity, fill.FillQuantity);
Assert.AreEqual(order.LimitPrice, fill.FillPrice);
Assert.AreEqual(OrderStatus.Filled, fill.Status);
}
[Test]
public void PerformsLimitIfTouchedBuy()
{
var model = new EquityFillModel();
var order = new LimitIfTouchedOrder(Symbols.SPY, 100, 101.5m, 100m, Noon);
var configTradeBar = CreateTradeBarConfig(Symbols.SPY);
var configQuoteBar = new SubscriptionDataConfig(configTradeBar, typeof(QuoteBar));
var configProvider = new MockSubscriptionDataConfigProvider(configQuoteBar);
var security = new Security(
SecurityExchangeHoursTests.CreateUsEquitySecurityExchangeHours(),
configTradeBar,
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
);
// Sets price at time zero
security.SetLocalTimeKeeper(TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
security.SetMarketPrice(new TradeBar(Noon, Symbols.SPY, 102m, 102m, 102m, 102m, 100));
configProvider.SubscriptionDataConfigs.Add(configTradeBar);
var fill = model.Fill(new FillModelParameters(
security,
order,
configProvider,
Time.OneHour,
null)).Single();
Assert.AreEqual(0, fill.FillQuantity);
Assert.AreEqual(0, fill.FillPrice);
Assert.AreEqual(OrderStatus.None, fill.Status);
// Time jump => trigger touched but not limit
security.SetMarketPrice(new TradeBar(Noon, Symbols.SPY, 101m, 101m, 100.5m, 101m, 100));
security.SetMarketPrice(new QuoteBar(Noon, Symbols.SPY,
new Bar(101m, 101m, 100.5m, 101m), 100, // Bid bar
new Bar(101m, 101m, 100.5m, 101m), 100) // Ask bar
);
Assert.AreEqual(0, fill.FillQuantity);
Assert.AreEqual(0, fill.FillPrice);
Assert.AreEqual(OrderStatus.None, fill.Status);
fill = model.Fill(new FillModelParameters(
security,
order,
configProvider,
Time.OneHour,
null)).Single();
Assert.AreEqual(0, fill.FillQuantity);
Assert.AreEqual(0, fill.FillPrice);
Assert.AreEqual(OrderStatus.None, fill.Status);
// Time jump => limit reached, security bought
// |---> First, ensure that price data are not used to fill
security.SetMarketPrice(new TradeBar(Noon, Symbols.SPY, 100m, 100m, 99m, 99m, 100));
fill = model.LimitIfTouchedFill(security, order);
Assert.AreEqual(0, fill.FillQuantity);
Assert.AreEqual(0, fill.FillPrice);
Assert.AreEqual(OrderStatus.None, fill.Status);
// |---> Lastly, ensure that quote data used to fill
security.SetMarketPrice(new QuoteBar(Noon, Symbols.SPY,
new Bar(100m, 100m, 100m, 100m), 100, // Bid bar
new Bar(100m, 100m, 100m, 100m), 100) // Ask bar
);
fill = model.LimitIfTouchedFill(security, order);
Assert.AreEqual(order.Quantity, fill.FillQuantity);
Assert.AreEqual(order.LimitPrice, fill.FillPrice);
Assert.AreEqual(OrderStatus.Filled, fill.Status);
}
[TestCase(-100)]
[TestCase(100)]
public void PerformsMarketOnOpenUsingOpenPriceWithMinuteSubscription(int quantity)
{
var reference = new DateTime(2015, 06, 05, 9, 0, 0); // before market open
var configTradeBar = CreateTradeBarConfig(Symbols.SPY);
var equity = CreateEquity(configTradeBar);
var model = (EquityFillModel)equity.FillModel;
var order = new MarketOnOpenOrder(Symbols.SPY, quantity, reference);
var time = reference;
TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
equity.SetMarketPrice(new TradeBar(time, Symbols.SPY, 1m, 2m, 0.5m, 1.33m, 100));
var configQuoteBar = new SubscriptionDataConfig(configTradeBar, typeof(QuoteBar));
var configProvider = new MockSubscriptionDataConfigProvider(configQuoteBar);
configProvider.SubscriptionDataConfigs.Add(configTradeBar);
var fill = model.Fill(new FillModelParameters(
equity,
order,
configProvider,
Time.OneHour,
null)).Single();
Assert.AreEqual(0, fill.FillQuantity);
// market opens after 30min, so this is just before market open
time = reference.AddMinutes(29);
TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
equity.SetMarketPrice(new TradeBar(time, Symbols.SPY, 1.33m, 2.75m, 1.15m, 1.45m, 100));
fill = model.Fill(new FillModelParameters(
equity,
order,
configProvider,
Time.OneHour,
null)).Single();
Assert.AreEqual(0, fill.FillQuantity);
const decimal expected = 1.45m;
// market opens after 30min
time = reference.AddMinutes(30);
TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
// Does not fill with quote data
equity.SetMarketPrice(new QuoteBar(time, Symbols.SPY,
new Bar(1.45m, 1.99m, 1.09m, 1.39m), 100,
new Bar(1.46m, 2.01m, 1.11m, 1.41m), 100));
fill = model.MarketOnOpenFill(equity, order);
Assert.AreEqual(0, fill.FillQuantity);
// Fill with trade bar
equity.SetMarketPrice(new TradeBar(time, Symbols.SPY, expected, 2.0m, 1.1m, 1.40m, 100));
fill = model.MarketOnOpenFill(equity, order);
Assert.AreEqual(order.Quantity, fill.FillQuantity);
Assert.AreEqual(expected, fill.FillPrice);
}
[TestCase(-100)]
[TestCase(100)]
public void PerformsMarketOnOpenUsingOpenPriceWithDailySubscription(int quantity)
{
Func<DateTime, decimal, TradeBar> getTradeBar = (t, o) => new TradeBar(t.RoundDown(Time.OneDay),
Symbols.SPY, o, 2m, 0.5m, 1.33m, 100, Time.OneDay);
var reference = new DateTime(2015, 06, 05, 12, 0, 0); // market is open
var config = CreateTradeBarConfig(Symbols.SPY, Resolution.Daily);
var equity = CreateEquity(config);
var model = (EquityFillModel)equity.FillModel;
var order = new MarketOnOpenOrder(Symbols.SPY, quantity, reference);
var time = reference;
TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
equity.SetMarketPrice(getTradeBar(time, 2m));
// Will not fill because the order was placed before the bar is closed
var fill = model.Fill(new FillModelParameters(
equity,
order,
new MockSubscriptionDataConfigProvider(config),
Time.OneHour,
null)).Single();
Assert.AreEqual(0, fill.FillQuantity);
// It will not fill in the next morning because needs to wait for day to close
const decimal expected = 1m;
time = equity.Exchange.Hours.GetNextMarketOpen(time, false);
TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
fill = model.MarketOnOpenFill(equity, order);
Assert.AreEqual(0, fill.FillQuantity);
// Fill once the equity is updated with the day bar
equity.SetMarketPrice(getTradeBar(time, expected));
fill = model.MarketOnOpenFill(equity, order);
Assert.AreEqual(order.Quantity, fill.FillQuantity);
Assert.AreEqual(expected, fill.FillPrice);
}
// Official open
[TestCase(-100, TradeConditionFlags.OfficialOpen)]
[TestCase(100, TradeConditionFlags.OfficialOpen)]
// Opening prints
[TestCase(-100, TradeConditionFlags.OpeningPrints)]
[TestCase(100, TradeConditionFlags.OpeningPrints)]
// Official open and regular
[TestCase(-100, TradeConditionFlags.OfficialOpen | TradeConditionFlags.Regular)]
[TestCase(100, TradeConditionFlags.OfficialOpen | TradeConditionFlags.Regular)]
// Opening prints and regular
[TestCase(-100, TradeConditionFlags.OpeningPrints | TradeConditionFlags.Regular)]
[TestCase(100, TradeConditionFlags.OpeningPrints | TradeConditionFlags.Regular)]
// Any other random combination of flags that include OfficialOpen
[TestCase(-100,
TradeConditionFlags.OfficialOpen | TradeConditionFlags.Regular | TradeConditionFlags.Cash | TradeConditionFlags.Cross |
TradeConditionFlags.DerivativelyPriced)]
[TestCase(100,
TradeConditionFlags.OfficialOpen | TradeConditionFlags.Regular | TradeConditionFlags.Cash | TradeConditionFlags.Cross |
TradeConditionFlags.DerivativelyPriced)]
// Any other random combination of flags that include OpeningPrints
[TestCase(-100,
TradeConditionFlags.OpeningPrints | TradeConditionFlags.Regular | TradeConditionFlags.Cash | TradeConditionFlags.Cross |
TradeConditionFlags.DerivativelyPriced)]
[TestCase(100,
TradeConditionFlags.OpeningPrints | TradeConditionFlags.Regular | TradeConditionFlags.Cash | TradeConditionFlags.Cross |
TradeConditionFlags.DerivativelyPriced)]
public void PerformsMarketOnOpenUsingOpenPriceWithTickSubscription(int quantity, long numericSaleCondition)
{
var reference = new DateTime(2015, 06, 05, 9, 0, 0); // before market open
var config = CreateTickConfig(Symbols.SPY);
var equity = CreateEquity(config);
var model = (EquityFillModel)equity.FillModel;
var order = new MarketOnOpenOrder(Symbols.SPY, quantity, reference);
var time = reference;
TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
var saleCondition = Convert.ToString(numericSaleCondition, 16);
equity.Update(new List<Tick>
{
new Tick(time, Symbols.SPY, saleCondition, "P", 100, 1m),
new Tick(time, Symbols.SPY, 1m, 0.9m, 1.1m)
}, typeof(Tick));
var fill = model.Fill(new FillModelParameters(
equity,
order,
new MockSubscriptionDataConfigProvider(config),
Time.OneHour,
null)).Single();
Assert.AreEqual(0, fill.FillQuantity);
// market opens after 30min, so this is just before market open
time = reference.AddMinutes(29);
TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
equity.Update(new List<Tick>
{
new Tick(time, Symbols.SPY, saleCondition, "P", 100, 1m),
new Tick(time, Symbols.SPY, 1m, 0.9m, 1.1m)
}, typeof(Tick));
fill = model.MarketOnOpenFill(equity, order);
Assert.AreEqual(0, fill.FillQuantity);
const decimal expected = 1m;
// market opens after 30min
time = reference.AddMinutes(30);
TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
// The quote is received after the market is open, but the trade is not
equity.Update(new List<Tick>
{
new Tick(time.AddMinutes(-1), Symbols.SPY, saleCondition, "P", 100, expected),
new Tick(time, Symbols.SPY, 1m, 0.9m, 1.1m)
}, typeof(Tick));
fill = model.MarketOnOpenFill(equity, order);
Assert.AreEqual(0, fill.FillQuantity);
// The trade is received after the market is open, but it is not have a official open flag
equity.Update(new List<Tick>
{
new Tick(time, Symbols.SPY, "", "P", 100, expected),
new Tick(time, Symbols.SPY, 1m, 0.9m, 1.1m)
}, typeof(Tick));
fill = model.MarketOnOpenFill(equity, order);
Assert.AreEqual(0, fill.FillQuantity);
Assert.AreEqual("No trade with the OfficialOpen or OpeningPrints flag within the 1-minute timeout.", fill.Message);
// One quote and some trades with different conditions are received after the market is open,
// but there is trade prior to that with different price
equity.Update(new List<Tick>
{
new Tick(time.AddMinutes(-1), Symbols.SPY, "80000001", "P", 100, 0.9m), // Not Open
new Tick(time, Symbols.SPY, saleCondition, "Q", 100, 0.95m), // Open but not primary exchange
new Tick(time, Symbols.SPY, saleCondition, "P", 100, expected), // Fill with this tick
new Tick(time, Symbols.SPY, 1m, 0.9m, 1.1m),
new Tick(time, Symbols.SPY, "80000001", "P", 100, 0.95m), // Open but not primary exchange
}, typeof(Tick));
fill = model.MarketOnOpenFill(equity, order);
Assert.AreEqual(order.Quantity, fill.FillQuantity);
Assert.AreEqual(expected, fill.FillPrice);
}
[TestCase(-100, 0.9)]
[TestCase(100, 1.1)]
public void PerformsMarketOnOpenUsingOpenPriceWithTickSubscriptionButNoSalesCondition(int quantity, decimal expected)
{
var reference = new DateTime(2015, 06, 05, 9, 0, 0); // before market open
var config = CreateTickConfig(Symbols.SPY);
var equity = CreateEquity(config);
var model = (EquityFillModel)equity.FillModel;
var order = new MarketOnOpenOrder(Symbols.SPY, quantity, reference);
var time = reference;
TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
// No Sales Condition
var saleCondition = "";
equity.Update(new List<Tick>
{
new Tick(time, Symbols.SPY, "80000001", "P", 100, 1m),
new Tick(time, Symbols.SPY, 1m, 0.9m, 1.1m)
}, typeof(Tick));
var fill = model.Fill(new FillModelParameters(
equity,
order,
new MockSubscriptionDataConfigProvider(config),
Time.OneHour,
null)).Single();
Assert.AreEqual(0, fill.FillQuantity);
const decimal price = 1m;
// market opens after 30min. 1 minute after open to accept the last trade
time = reference.AddMinutes(32);
TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
equity.Update(new List<Tick>
{
new Tick(time, Symbols.SPY, "80000001", "P", 100, 0.9m), // Not Close
new Tick(time, Symbols.SPY, saleCondition, "P", 100, 0.95m),
new Tick(time, Symbols.SPY, 1m, 0.9m, 1.1m),
new Tick(time, Symbols.SPY, "80000001", "P", 100, 0.95m), // Open but not primary exchange
new Tick(time, Symbols.SPY, saleCondition, "Q", 100, price), // Fill with this tick
}, typeof(Tick));
fill = model.MarketOnOpenFill(equity, order);
Assert.AreEqual(order.Quantity, fill.FillQuantity);
Assert.AreEqual(price, fill.FillPrice);
// Test whether it fills on the bid/ask if there is no trade
equity.Cache.Reset();
equity.Update(new List<Tick>
{
new Tick(time, Symbols.SPY, 1m, 0.9m, 1.1m),
}, typeof(Tick));
fill = model.MarketOnOpenFill(equity, order);
Assert.AreEqual(order.Quantity, fill.FillQuantity);
Assert.AreEqual(expected, fill.FillPrice);
Assert.IsTrue(fill.Message.Contains("Fill with last Quote data.", StringComparison.InvariantCulture));
}
[TestCase(Resolution.Minute, 3, 17, 0, 9, 29)]
[TestCase(Resolution.Minute, 4, 0, 0, 9, 29)]
[TestCase(Resolution.Minute, 4, 8, 0, 9, 29)]
[TestCase(Resolution.Minute, 4, 9, 30, 9, 29, true)]
[TestCase(Resolution.Minute, 4, 9, 30, 9, 31, true)]
[TestCase(Resolution.Hour, 3, 17, 0, 8, 29)]
[TestCase(Resolution.Hour, 4, 0, 0, 8, 0)]
[TestCase(Resolution.Hour, 4, 8, 0, 8, 0)]
[TestCase(Resolution.Hour, 4, 9, 30, 8, 0, true)]
[TestCase(Resolution.Hour, 4, 9, 30, 11, 0, true)]
[TestCase(Resolution.Daily, 3, 17, 0, 8, 0)]
[TestCase(Resolution.Daily, 4, 0, 0, 8, 0)]
[TestCase(Resolution.Daily, 4, 8, 0, 8, 0)]
[TestCase(Resolution.Daily, 4, 9, 30, 8, 0)]
public void PerformsMarketOnOpenUsingOpenPriceWithDifferentOrderSubmissionDateTime(Resolution resolution, int day, int hour, int minute, int ref_hour, int ref_minute, bool nextDay = false)
{
var period = resolution.ToTimeSpan();
var configTradeBar = CreateTradeBarConfig(Symbols.SPY, resolution);
var equity = CreateEquity(configTradeBar);
var model = (EquityFillModel)equity.FillModel;
var orderTime = new DateTime(2015, 6, day, hour, minute, 0).ConvertToUtc(TimeZones.NewYork);
var order = new MarketOnOpenOrder(Symbols.SPY, 100, orderTime);
var reference = new DateTime(2015, 6, 4, ref_hour, ref_minute, 0).RoundDown(period);
var time = reference;
TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
equity.SetMarketPrice(new TradeBar(time, Symbols.SPY, 1m, 2m, 0.5m, 1.33m, 100, period));
var configQuoteBar = new SubscriptionDataConfig(configTradeBar, typeof(QuoteBar));
var configProvider = new MockSubscriptionDataConfigProvider(configQuoteBar);
configProvider.SubscriptionDataConfigs.Add(configTradeBar);
var fill = model.Fill(new FillModelParameters(
equity,
order,
configProvider,
Time.OneHour,
null)).Single();
Assert.AreEqual(0, fill.FillQuantity);
const decimal expected = 1.45m;
var tradeBar = new TradeBar(reference.Add(period), Symbols.SPY, expected, 2.75m, 1.15m, 1.45m, 100, period);
equity.SetMarketPrice(tradeBar);
TimeKeeper.SetUtcDateTime(tradeBar.EndTime.ConvertToUtc(TimeZones.NewYork));
fill = model.MarketOnOpenFill(equity, order);
// Special case when the order exactly when the market opens.
// Should only fill on the next day
if (nextDay)
{
Assert.AreEqual(0, fill.FillQuantity);
return;
}
Assert.AreEqual(order.Quantity, fill.FillQuantity);
Assert.AreEqual(expected, fill.FillPrice);
}
[TestCase(-100)]
[TestCase(100)]
public void PerformsMarketOnCloseUsingClosingPriceWithDailySubscription(int quantity)
{
Func<DateTime, decimal, TradeBar> getTradeBar = (t, c) => new TradeBar(t.RoundDown(Time.OneDay * 2),
Symbols.SPY, 1.33m, 2m, 0.5m, c, 100, Time.OneDay);
var reference = new DateTime(2015, 06, 05, 15, 0, 0); // before market close
var config = CreateTradeBarConfig(Symbols.SPY, Resolution.Daily);
var equity = CreateEquity(config);
var model = (EquityFillModel)equity.FillModel;
var order = new MarketOnCloseOrder(Symbols.SPY, quantity, reference);
var time = reference;
TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
equity.SetMarketPrice(getTradeBar(time, 2m));
var fill = model.Fill(new FillModelParameters(
equity,
order,
new MockSubscriptionDataConfigProvider(config),
Time.OneHour,
null)).Single();
Assert.AreEqual(0, fill.FillQuantity);
// market closes after 60min, so this is just before market Close
time = reference.AddMinutes(59);
TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
equity.SetMarketPrice(getTradeBar(time, 1.45m));
fill = model.MarketOnCloseFill(equity, order);
Assert.AreEqual(0, fill.FillQuantity);
// market closes
const decimal expected = 1.40m;
time = reference.AddMinutes(60);
TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
equity.SetMarketPrice(getTradeBar(time, expected));
fill = model.MarketOnCloseFill(equity, order);
Assert.AreEqual(order.Quantity, fill.FillQuantity);
Assert.AreEqual(expected, fill.FillPrice);
}
[TestCase(-100)]
[TestCase(100)]
public void PerformsMarketOnCloseUsingClosingPriceWithMinuteTradeBarSubscription(int quantity)
{
var reference = new DateTime(2015, 06, 05, 15, 0, 0); // before market close
var config = CreateTradeBarConfig(Symbols.SPY);
var equity = CreateEquity(config);
var model = (EquityFillModel)equity.FillModel;
var order = new MarketOnCloseOrder(Symbols.SPY, quantity, reference);
var time = reference;
TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
equity.SetMarketPrice(new TradeBar(time - config.Increment, Symbols.SPY, 1m, 2m, 0.5m, 1.33m, 100, config.Increment));
var fill = model.Fill(new FillModelParameters(
equity,
order,
new MockSubscriptionDataConfigProvider(config),
Time.OneHour,
null)).Single();
Assert.AreEqual(0, fill.FillQuantity);
// market closes after 60min, so this is just before market Close
time = reference.AddMinutes(59);
TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
equity.SetMarketPrice(new TradeBar(time - config.Increment, Symbols.SPY, 1.33m, 2.75m, 1.15m, 1.45m, 100, config.Increment));
fill = model.MarketOnCloseFill(equity, order);
Assert.AreEqual(0, fill.FillQuantity);
// market closes
time = reference.AddMinutes(60);
TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
equity.SetMarketPrice(new TradeBar(time - config.Increment, Symbols.SPY, 1.45m, 2.0m, 1.1m, 1.40m, 100, config.Increment));
fill = model.MarketOnCloseFill(equity, order);
Assert.AreEqual(order.Quantity, fill.FillQuantity);
Assert.AreEqual(equity.Close, fill.FillPrice);
}
[TestCase(-100)]
[TestCase(100)]
public void PerformsMarketOnCloseUsingClosingPriceWithMinuteQuoteBarSubscription(int quantity)
{
var reference = new DateTime(2015, 06, 05, 15, 0, 0); // before market close
var config = CreateQuoteBarConfig(Symbols.SPY);
var equity = CreateEquity(config);
var model = (EquityFillModel)equity.FillModel;
var order = new MarketOnCloseOrder(Symbols.SPY, quantity, reference);
var time = reference;
TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
equity.SetMarketPrice(new QuoteBar(time, Symbols.SPY,
new Bar(1.45m, 1.99m, 1.09m, 1.39m), 100,
new Bar(1.46m, 2.01m, 1.11m, 1.41m), 100, config.Increment));
var fill = model.Fill(new FillModelParameters(
equity,
order,
new MockSubscriptionDataConfigProvider(config),
Time.OneHour,
null)).Single();
Assert.AreEqual(0, fill.FillQuantity);
// market closes after 60min, so this is just before market Close
time = reference.AddMinutes(59);
TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
equity.SetMarketPrice(new QuoteBar(time - config.Increment, Symbols.SPY,
new Bar(1.45m, 1.99m, 1.09m, 1.39m), 100,
new Bar(1.46m, 2.01m, 1.11m, 1.41m), 100, config.Increment));
fill = model.MarketOnCloseFill(equity, order);
Assert.AreEqual(0, fill.FillQuantity);
const decimal expected = 1.4m;
// market closes
time = reference.AddMinutes(60);
TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
equity.SetMarketPrice(new TradeBar(time - config.Increment, Symbols.SPY, 1.45m, 2.0m, 1.1m, 1.40m, 100, config.Increment));
equity.SetMarketPrice(new QuoteBar(time, Symbols.SPY,
new Bar(1.45m, 1.99m, 1.09m, expected), 100,
new Bar(1.46m, 2.01m, 1.11m, expected), 100, config.Increment));
fill = model.MarketOnCloseFill(equity, order);
Assert.AreEqual(order.Quantity, fill.FillQuantity);
Assert.AreEqual(expected, fill.FillPrice);
Assert.IsTrue(fill.Message.StartsWith("Warning: No trade information available", StringComparison.InvariantCulture));
}
// Official close
[TestCase(-100, true, TradeConditionFlags.OfficialClose)]
[TestCase(100, true, TradeConditionFlags.OfficialClose)]
[TestCase(-100, false, TradeConditionFlags.OfficialClose)]
[TestCase(100, false, TradeConditionFlags.OfficialClose)]
// Closing prints
[TestCase(-100, true, TradeConditionFlags.ClosingPrints)]
[TestCase(100, true, TradeConditionFlags.ClosingPrints)]
[TestCase(-100, false, TradeConditionFlags.ClosingPrints)]
[TestCase(100, false, TradeConditionFlags.ClosingPrints)]
// Official close and regular
[TestCase(-100, true, TradeConditionFlags.OfficialClose | TradeConditionFlags.Regular)]
[TestCase(100, true, TradeConditionFlags.OfficialClose | TradeConditionFlags.Regular)]
[TestCase(-100, false, TradeConditionFlags.OfficialClose | TradeConditionFlags.Regular)]
[TestCase(100, false, TradeConditionFlags.OfficialClose | TradeConditionFlags.Regular)]
// Closing prints and regular
[TestCase(-100, true, TradeConditionFlags.ClosingPrints | TradeConditionFlags.Regular)]
[TestCase(100, true, TradeConditionFlags.ClosingPrints | TradeConditionFlags.Regular)]
[TestCase(-100, false, TradeConditionFlags.ClosingPrints | TradeConditionFlags.Regular)]
[TestCase(100, false, TradeConditionFlags.ClosingPrints | TradeConditionFlags.Regular)]
// Any other random combination of flags that include OfficialClose
[TestCase(-100, true,
TradeConditionFlags.OfficialClose | TradeConditionFlags.Regular | TradeConditionFlags.Cash | TradeConditionFlags.Cross |
TradeConditionFlags.DerivativelyPriced)]
[TestCase(100, true,
TradeConditionFlags.OfficialClose | TradeConditionFlags.Regular | TradeConditionFlags.Cash | TradeConditionFlags.Cross |
TradeConditionFlags.DerivativelyPriced)]
[TestCase(-100, false,
TradeConditionFlags.OfficialClose | TradeConditionFlags.Regular | TradeConditionFlags.Cash | TradeConditionFlags.Cross |
TradeConditionFlags.DerivativelyPriced)]
[TestCase(100, false,
TradeConditionFlags.OfficialClose | TradeConditionFlags.Regular | TradeConditionFlags.Cash | TradeConditionFlags.Cross |
TradeConditionFlags.DerivativelyPriced)]
// Any other random combination of flags that include ClosingPrints
[TestCase(-100, true,
TradeConditionFlags.ClosingPrints | TradeConditionFlags.Regular | TradeConditionFlags.Cash | TradeConditionFlags.Cross |
TradeConditionFlags.DerivativelyPriced)]
[TestCase(100, true,
TradeConditionFlags.ClosingPrints | TradeConditionFlags.Regular | TradeConditionFlags.Cash | TradeConditionFlags.Cross |
TradeConditionFlags.DerivativelyPriced)]
[TestCase(-100, false,
TradeConditionFlags.ClosingPrints | TradeConditionFlags.Regular | TradeConditionFlags.Cash | TradeConditionFlags.Cross |
TradeConditionFlags.DerivativelyPriced)]
[TestCase(100, false,
TradeConditionFlags.ClosingPrints | TradeConditionFlags.Regular | TradeConditionFlags.Cash | TradeConditionFlags.Cross |
TradeConditionFlags.DerivativelyPriced)]
public void PerformsMarketOnCloseUsingClosingPriceWithTickSubscription(int quantity, bool extendedHours, long numericSaleCondition)
{
var reference = new DateTime(2015, 06, 05, 15, 0, 0); // before market close
var config = CreateTickConfig(Symbols.SPY, extendedHours: extendedHours);
var equity = CreateEquity(config);
var model = (EquityFillModel)equity.FillModel;
var order = new MarketOnCloseOrder(Symbols.SPY, quantity, reference);
var time = reference;
TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
var saleCondition = Convert.ToString(numericSaleCondition, 16);
equity.Update(new List<Tick>
{
new Tick(time, Symbols.SPY, "80000001", "P", 100, 1m),
new Tick(time, Symbols.SPY, 1m, 0.9m, 1.1m)
}, typeof(Tick));
var fill = model.Fill(new FillModelParameters(
equity,
order,
new MockSubscriptionDataConfigProvider(config),
Time.OneHour,
null)).Single();
Assert.AreEqual(0, fill.FillQuantity);
// market closes after 60min, so this is just before market Close
time = reference.AddMinutes(59);
TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
equity.Update(new List<Tick>
{
// It should fill with this tick based on sales condition but the market is still open
new Tick(time, Symbols.SPY, saleCondition, "P", 100, 1)
}, typeof(Tick));
fill = model.MarketOnCloseFill(equity, order);
Assert.AreEqual(0, fill.FillQuantity);
const decimal expected = 1m;
// market closes
time = reference.AddMinutes(60);
TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
equity.Update(new List<Tick>
{
new Tick(time, Symbols.SPY, "80000001", "P", 100, 0.9m),
new Tick(time, Symbols.SPY, "80000001", "Q", 100, 0.95m),
new Tick(time, Symbols.SPY, "80000001", "P", 100, 0.98m),
new Tick(time, Symbols.SPY, 1m, 0.9m, 1.1m),
new Tick(time, Symbols.SPY, "80000001", "P", 100, expected),
}, typeof(Tick));
// If the subscriptions doesn't include extended hours, fills with the last tick
if (!extendedHours)
{
fill = model.MarketOnCloseFill(equity, order);
Assert.AreEqual(order.Quantity, fill.FillQuantity);
Assert.AreEqual(expected, fill.FillPrice);
Assert.AreEqual("No trade with the OfficialClose or ClosingPrints flag for data that does not include extended market hours. Fill with last Trade data.", fill.Message);
return;
}
equity.Update(new List<Tick>
{
new Tick(time, Symbols.SPY, "80000001", "P", 100, 0.9m), // Not Close
new Tick(time, Symbols.SPY, saleCondition, "Q", 100, 0.95m), // Close but not primary exchange
new Tick(time, Symbols.SPY, saleCondition, "P", 100, expected), // Fill with this tick
new Tick(time, Symbols.SPY, 1m, 0.9m, 1.1m),
new Tick(time, Symbols.SPY, "80000001", "P", 100, 0.95m),
}, typeof(Tick));
fill = model.MarketOnCloseFill(equity, order);
Assert.AreEqual(order.Quantity, fill.FillQuantity);
Assert.AreEqual(expected, fill.FillPrice);
}
[TestCase(-100)]
[TestCase(100)]
public void PerformsMarketOnCloseUsingClosingPriceWithTickSubscriptionButNoSalesCondition(int quantity)
{
var reference = new DateTime(2015, 06, 05, 15, 0, 0); // before market close
var config = CreateTickConfig(Symbols.SPY);
var equity = CreateEquity(config);
var model = (EquityFillModel)equity.FillModel;
var order = new MarketOnCloseOrder(Symbols.SPY, quantity, reference);
var time = reference;
TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
// No Sales Condition
var saleCondition = "";
equity.Update(new List<Tick>
{
new Tick(time, Symbols.SPY, "80000001", "P", 100, 1m),
new Tick(time, Symbols.SPY, 1m, 0.9m, 1.1m)
}, typeof(Tick));
var fill = model.Fill(new FillModelParameters(
equity,
order,
new MockSubscriptionDataConfigProvider(config),
Time.OneHour,
null)).Single();
Assert.AreEqual(0, fill.FillQuantity);
const decimal expected = 1m;
// market closes
time = reference.AddMinutes(60).AddMilliseconds(100);
TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
equity.Update(new List<Tick>
{
new Tick(time, Symbols.SPY, "80000001", "P", 100, 0.9m), // Not Close
new Tick(time, Symbols.SPY, saleCondition, "Q", 100, 0.95m), // Close but not primary exchange
new Tick(time, Symbols.SPY, saleCondition, "P", 100, expected), // Fill with this tick
new Tick(time, Symbols.SPY, 1m, 0.9m, 1.1m),
new Tick(time, Symbols.SPY, "80000001", "P", 100, 0.95m), // Open but not primary exchange
}, typeof(Tick));
fill = model.MarketOnCloseFill(equity, order);
Assert.AreEqual(0, fill.FillQuantity);
Assert.AreEqual("No trade with the OfficialClose or ClosingPrints flag within the 1-minute timeout.", fill.Message);
// 2 minutes after the close
time = reference.AddMinutes(62);
TimeKeeper.SetUtcDateTime(time.ConvertToUtc(TimeZones.NewYork));
equity.Update(new List<Tick>
{
new Tick(time, Symbols.SPY, "80000001", "P", 100, 0.9m), // Not Close
new Tick(time, Symbols.SPY, saleCondition, "P", 100, 0.95m),
new Tick(time, Symbols.SPY, 1m, 0.9m, 1.1m),
new Tick(time, Symbols.SPY, "80000001", "P", 100, 0.95m), // Open but not primary exchange
new Tick(time, Symbols.SPY, saleCondition, "Q", 100, expected), // Fill with this tick
}, typeof(Tick));
fill = model.MarketOnCloseFill(equity, order);
Assert.AreEqual(order.Quantity, fill.FillQuantity);
Assert.AreEqual(expected, fill.FillPrice);
Assert.IsTrue(fill.Message.Contains("Fill with last Trade data.", StringComparison.InvariantCulture));
}
[TestCase(OrderDirection.Buy)]
[TestCase(OrderDirection.Sell)]
public void MarketOrderFillsAtBidAsk(OrderDirection direction)
{
var symbol = Symbol.Create("EURUSD", SecurityType.Forex, "fxcm");
var exchangeHours = SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork);
var quoteCash = new Cash(Currencies.USD, 1000, 1);
var symbolProperties = SymbolProperties.GetDefault(Currencies.USD);
var config = new SubscriptionDataConfig(typeof(Tick), symbol, Resolution.Tick, TimeZones.NewYork, TimeZones.NewYork, true, true, false);
var security = new Forex(exchangeHours, quoteCash, new Cash("EUR", 0, 0), config, symbolProperties, ErrorCurrencyConverter.Instance, RegisteredSecurityDataTypesProvider.Null);
var reference = DateTime.Now;
var referenceUtc = reference.ConvertToUtc(TimeZones.NewYork);
var timeKeeper = new TimeKeeper(referenceUtc);
security.SetLocalTimeKeeper(timeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
var brokerageModel = new FxcmBrokerageModel();
var fillModel = brokerageModel.GetFillModel(security);
const decimal bidPrice = 1.13739m;
const decimal askPrice = 1.13746m;
security.SetMarketPrice(new Tick(DateTime.Now, symbol, bidPrice, askPrice));
var quantity = direction == OrderDirection.Buy ? 1 : -1;
var order = new MarketOrder(symbol, quantity, DateTime.Now);
var fill = fillModel.Fill(new FillModelParameters(
security,
order,
new MockSubscriptionDataConfigProvider(config),
Time.OneHour,
null)).Single();
var expected = direction == OrderDirection.Buy ? askPrice : bidPrice;
Assert.AreEqual(expected, fill.FillPrice);
Assert.AreEqual(0, fill.OrderFee.Value.Amount);
}
[Test]
public void EquityFillModelUsesPriceForTicksWhenBidAskSpreadsAreNotAvailable()
{
var noon = new DateTime(2014, 6, 24, 12, 0, 0);
var timeKeeper = new TimeKeeper(noon.ConvertToUtc(TimeZones.NewYork), new[] { TimeZones.NewYork });
var symbol = Symbol.Create("SPY", SecurityType.Equity, Market.USA);
var config = new SubscriptionDataConfig(typeof(Tick), Symbols.SPY, Resolution.Tick, TimeZones.NewYork, TimeZones.NewYork, true, true, false);
var security = new Security(
SecurityExchangeHoursTests.CreateUsEquitySecurityExchangeHours(),
config,
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
);
security.SetLocalTimeKeeper(timeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, noon, 101.123m));
// Add both a tradebar and a tick to the security cache
// This is the case when a tick is seeded with minute data in an algorithm.
// Use fresh timestamps (data within one resolution bar of the current time) so the fill is not held
// back as stale; this test is about which data type is used, not stale-data handling.
security.Cache.AddData(new TradeBar(noon.AddMinutes(-1), symbol, 1.0m, 1.0m, 1.0m, 1.0m, 1.0m, Time.OneMinute));
security.Cache.AddData(new Tick(config, "42525000,1000000,100,A,@,0", noon.Date));
var fillModel = new EquityFillModel();
var order = new MarketOrder(symbol, 1000, noon);
var fill = fillModel.Fill(new FillModelParameters(
security,
order,
new MockSubscriptionDataConfigProvider(config),
Time.OneHour,
null)).Single();
// The fill model should use the tick.Price
Assert.AreEqual(fill.FillPrice, 100m);
Assert.AreEqual(0, fill.OrderFee.Value.Amount);
}
[Test]
public void EquityFillModelDoesNotUseTicksWhenThereIsNoTickSubscription()
{
var noon = new DateTime(2014, 6, 24, 12, 0, 0);
var timeKeeper = new TimeKeeper(noon.ConvertToUtc(TimeZones.NewYork), new[] { TimeZones.NewYork });
var symbol = Symbol.Create("SPY", SecurityType.Equity, Market.USA);
// Minute subscription
var config = new SubscriptionDataConfig(typeof(TradeBar), Symbols.SPY, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, true, false);
var security = new Security(
SecurityExchangeHoursTests.CreateUsEquitySecurityExchangeHours(),
config,
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
);
security.SetLocalTimeKeeper(timeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, noon, 101.123m));
// This is the case when a tick is seeded with minute data in an algorithm.
// Use fresh timestamps (data within one resolution bar of the current time) so the fill is not held
// back as stale; this test is about which data type is used, not stale-data handling.
security.Cache.AddData(new TradeBar(noon.AddMinutes(-1), symbol, 1.0m, 1.0m, 1.0m, 1.0m, 1.0m, Time.OneMinute));
security.Cache.AddData(new Tick(config, "42525000,1000000,100,A,@,0", noon.Date));
var fillModel = new EquityFillModel();
var order = new MarketOrder(symbol, 1000, noon);
var fill = fillModel.Fill(new FillModelParameters(
security,
order,
new MockSubscriptionDataConfigProvider(config),
Time.OneHour,
null)).Single();
// The fill model should use the tick.Price
Assert.AreEqual(fill.FillPrice, 1.0m);
Assert.AreEqual(0, fill.OrderFee.Value.Amount);
}
[TestCase(100, 291.50, 291.75)]
[TestCase(-100, 290.50, 290.25)]
public void StopLimitOrderDoesNotFillUsingDataBeforeSubmitTime(decimal orderQuantity, decimal stopPrice, decimal limitPrice)
{
var time = new DateTime(2018, 9, 24, 9, 30, 0);
var timeKeeper = new TimeKeeper(time.ConvertToUtc(TimeZones.NewYork), TimeZones.NewYork);
var symbol = Symbol.Create("SPY", SecurityType.Equity, Market.USA);
var config = new SubscriptionDataConfig(typeof(TradeBar), symbol, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, true, false);
var security = new Security(
SecurityExchangeHoursTests.CreateUsEquitySecurityExchangeHours(),
config,
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
);
security.SetLocalTimeKeeper(timeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
var tradeBar = new TradeBar(time, symbol, 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 fillModel = new EquityFillModel();
var order = new StopLimitOrder(symbol, orderQuantity, stopPrice, limitPrice, time.ConvertToUtc(TimeZones.NewYork));
var fill = fillModel.Fill(new FillModelParameters(
security,
order,
new MockSubscriptionDataConfigProvider(config),
Time.OneHour,
null)).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, symbol, 290m, 292m, 289m, 291m, 12345);
security.SetMarketPrice(tradeBar);
fill = fillModel.StopLimitFill(security, order);
Assert.AreEqual(orderQuantity, fill.FillQuantity);
Assert.AreEqual(limitPrice, fill.FillPrice);
Assert.AreEqual(OrderStatus.Filled, fill.Status);
Assert.AreEqual(0, fill.OrderFee.Value.Amount);
}
[Test]
public void MarketOrderFillWithStalePriceHasWarningMessage()
{
var model = new EquityFillModel();
var order = new MarketOrder(Symbols.SPY, -100, Noon.ConvertToUtc(TimeZones.NewYork).AddMinutes(61));
var config = CreateTickConfig(Symbols.SPY);
var security = new Security(
SecurityExchangeHoursTests.CreateUsEquitySecurityExchangeHours(),
config,
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
);
security.SetLocalTimeKeeper(TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
security.SetMarketPrice(new Tick(Noon, Symbols.SPY, 101.123m, 101.456m));
var fill = model.Fill(new FillModelParameters(
security,
order,
new MockSubscriptionDataConfigProvider(config),
Time.OneHour,
null)).Single();
Assert.IsTrue(fill.Message.Contains("Warning: fill at stale price"));
}
// A market order whose only available data is stale by more than one resolution bar must wait for fresh data
// instead of filling on the stale price. This is independent of the time of day:
// - "market open": order one second after the open, only the previous session's bar is available (overnight gap)
// - "mid-session": order at noon, the latest bar is two hours old (e.g. an intraday data gap)
[TestCase(true, OrderDirection.Buy)]
[TestCase(true, OrderDirection.Sell)]
[TestCase(false, OrderDirection.Buy)]
[TestCase(false, OrderDirection.Sell)]
public void MarketOrderWaitsForFreshDataWhenStaleByMoreThanResolution(bool atMarketOpen, OrderDirection direction)
{
var config = CreateTradeBarConfig(Symbols.SPY, Resolution.Minute);
var configProvider = new MockSubscriptionDataConfigProvider(config);
configProvider.SubscriptionDataConfigs.Add(config);
var equity = CreateEquity(config);
var model = (EquityFillModel)equity.FillModel;
// US equity market opens at 9:30
var orderTime = atMarketOpen
? new DateTime(2014, 6, 24, 9, 30, 1) // one second after the open
: new DateTime(2014, 6, 24, 12, 0, 0); // mid-session
var staleBarEnd = atMarketOpen
? new DateTime(2014, 6, 23, 16, 0, 0) // previous session close (overnight gap)
: new DateTime(2014, 6, 24, 10, 0, 0); // two hours old
var timeKeeper = TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork);
timeKeeper.UpdateTime(orderTime.ConvertToUtc(TimeZones.NewYork));
equity.SetLocalTimeKeeper(timeKeeper);
const decimal staleClose = 101.123m;
equity.SetMarketPrice(new TradeBar(staleBarEnd.Add(-Time.OneMinute), Symbols.SPY,
101m, 101.2m, 100.9m, staleClose, 100, Time.OneMinute));
var quantity = direction == OrderDirection.Buy ? 100 : -100;
var order = new MarketOrder(Symbols.SPY, quantity, orderTime.ConvertToUtc(equity.Exchange.TimeZone));
var parameters = new FillModelParameters(equity, order, configProvider, Time.OneHour, null);
// The latest data is more than one resolution bar (minute) behind: must not fill on the stale price
var fill = model.Fill(parameters).Single();
Assert.AreNotEqual(OrderStatus.Filled, fill.Status);
Assert.AreNotEqual(OrderStatus.PartiallyFilled, fill.Status);
Assert.AreEqual(0, fill.FillQuantity);
// Once a fresh bar (within one resolution of the current time) is available, the order fills on it
const decimal freshClose = 102.345m;
var freshBarEnd = orderTime.RoundDown(Time.OneMinute).Add(Time.OneMinute);
timeKeeper.UpdateTime(freshBarEnd.ConvertToUtc(TimeZones.NewYork));
equity.SetLocalTimeKeeper(timeKeeper);
equity.SetMarketPrice(new TradeBar(freshBarEnd.Add(-Time.OneMinute), Symbols.SPY,
102m, 102.5m, 101.9m, freshClose, 100, Time.OneMinute));
fill = model.Fill(parameters).Single();
Assert.AreEqual(OrderStatus.Filled, fill.Status);
Assert.AreEqual(order.Quantity, fill.FillQuantity);
Assert.AreEqual(freshClose, fill.FillPrice);
}
// A market order fills on stale data when the gap to the current time is within one resolution bar (the data is
// detected as stale by the stale-price window, but a fresh bar is not yet expected).
[TestCase(OrderDirection.Buy)]
[TestCase(OrderDirection.Sell)]
public void MarketOrderFillsOnStaleDataWithinOneResolutionBar(OrderDirection direction)
{
var config = CreateTradeBarConfig(Symbols.SPY, Resolution.Minute);
var configProvider = new MockSubscriptionDataConfigProvider(config);
configProvider.SubscriptionDataConfigs.Add(config);
var equity = CreateEquity(config);
var model = (EquityFillModel)equity.FillModel;
var orderTime = new DateTime(2014, 6, 24, 12, 0, 0);
var timeKeeper = TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork);
timeKeeper.UpdateTime(orderTime.ConvertToUtc(TimeZones.NewYork));
equity.SetLocalTimeKeeper(timeKeeper);
// Data 30 seconds old: with a zero stale-price window it is detected as stale, but the gap is smaller than
// the minute resolution, so a fresh bar is not yet expected and the order should fill on the stale price.
const decimal staleClose = 101.123m;
var staleBarEnd = orderTime.AddSeconds(-30);
equity.SetMarketPrice(new TradeBar(staleBarEnd.Add(-Time.OneMinute), Symbols.SPY,
101m, 101.2m, 100.9m, staleClose, 100, Time.OneMinute));
var quantity = direction == OrderDirection.Buy ? 100 : -100;
var order = new MarketOrder(Symbols.SPY, quantity, orderTime.ConvertToUtc(equity.Exchange.TimeZone));
var parameters = new FillModelParameters(equity, order, configProvider, TimeSpan.Zero, null);
var fill = model.Fill(parameters).Single();
Assert.AreEqual(OrderStatus.Filled, fill.Status);
Assert.AreEqual(order.Quantity, fill.FillQuantity);
Assert.AreEqual(staleClose, fill.FillPrice);
}
[TestCase(OrderDirection.Sell, 11)]
[TestCase(OrderDirection.Buy, 21)]
// uses the trade bar last close
[TestCase(OrderDirection.Hold, 291)]
public void PriceReturnsQuoteBarsIfPresent(OrderDirection orderDirection, decimal expected)
{
var time = new DateTime(2018, 9, 24, 9, 30, 0);
var timeKeeper = new TimeKeeper(time.ConvertToUtc(TimeZones.NewYork), TimeZones.NewYork);
var symbol = Symbol.Create("SPY", SecurityType.Equity, Market.USA);
var configTradeBar = new SubscriptionDataConfig(typeof(TradeBar), symbol, Resolution.Minute, TimeZones.NewYork, TimeZones.NewYork, true, true, false);
var configQuoteBar = new SubscriptionDataConfig(configTradeBar, typeof(QuoteBar));
var security = new Security(
SecurityExchangeHoursTests.CreateUsEquitySecurityExchangeHours(),
configQuoteBar,
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
);
security.SetLocalTimeKeeper(timeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
var tradeBar = new TradeBar(time, symbol, 290m, 292m, 289m, 291m, 12345);
security.SetMarketPrice(tradeBar);
var quoteBar = new QuoteBar(time, symbol,
new Bar(10, 15, 5, 11),
100,
new Bar(20, 25, 15, 21),
100);
security.SetMarketPrice(quoteBar);
var configProvider = new MockSubscriptionDataConfigProvider(configQuoteBar);
configProvider.SubscriptionDataConfigs.Add(configTradeBar);
var testFillModel = new TestFillModel();
testFillModel.SetParameters(new FillModelParameters(security,
null,
configProvider,
TimeSpan.FromDays(1),
null));
var result = testFillModel.GetPricesPublic(security, orderDirection);
Assert.AreEqual(expected, result.Close);
}
[TestCase(Resolution.Tick, false)]
[TestCase(Resolution.Second, false)]
[TestCase(Resolution.Minute, false)]
[TestCase(Resolution.Hour, false)]
[TestCase(Resolution.Daily, true)]
public void PerformFillOutsideRegularAndExtendedHours(Resolution resolution, bool shouldFill)
{
var config = CreateTradeBarConfig(Symbols.SPY, resolution: resolution);
var configProvider = new MockSubscriptionDataConfigProvider(config);
configProvider.SubscriptionDataConfigs.Add(config);
var equity = CreateEquity(config);
var baseTime = resolution == Resolution.Daily ? new DateTime(2014, 6, 25) : new DateTime(2014, 6, 24, 12, 0, 0);
var orderTime = baseTime.ConvertToUtc(equity.Exchange.TimeZone);
var resolutionTimeSpan = resolution.ToTimeSpan();
var tradeBarTime = baseTime.Subtract(resolutionTimeSpan);
var model = (EquityFillModel)equity.FillModel;
var order = new MarketOrder(Symbols.SPY, 100, orderTime);
var parameters = new FillModelParameters(equity, order, configProvider, Time.OneHour, null);
var timeKeeper = TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork);
// midnight, shouldn't be able to fill for resolutions < daily
timeKeeper.UpdateTime(new DateTime(2014, 6, 25).ConvertToUtc(TimeZones.NewYork));
equity.SetLocalTimeKeeper(timeKeeper);
const decimal close = 101.234m;
equity.SetMarketPrice(new TradeBar(tradeBarTime, Symbols.SPY, 101.123m, 101.123m, 101.123m, close, 100, resolutionTimeSpan));
var fill = model.Fill(parameters).Single();
if (shouldFill)
{
Assert.AreEqual(OrderStatus.Filled, fill.Status);
Assert.AreEqual(order.Quantity, fill.FillQuantity);
Assert.AreEqual(close, fill.FillPrice);
}
else
{
Assert.AreNotEqual(OrderStatus.Filled, fill.Status);
Assert.AreNotEqual(OrderStatus.PartiallyFilled, fill.Status);
Assert.AreEqual(0, fill.FillQuantity);
Assert.AreEqual(0, fill.FillPrice);
}
}
[TestCase(Resolution.Second)]
[TestCase(Resolution.Minute)]
[TestCase(Resolution.Hour)]
public void EquityFillModelFillsMOCAtOrAfterMarketCloseTime(Resolution resolution)
{
var model = new EquityFillModel();
var config = CreateTradeBarConfig(Symbols.SPY);
var security = new Security(
SecurityExchangeHoursTests.CreateUsEquitySecurityExchangeHours(),
config,
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
);
var timeOffset = resolution.ToTimeSpan();
var desiredTime = new DateTime(2025, 7, 8, 16, 0, 0);
// Submit MOC order an hour before close
var order = new MarketOnCloseOrder(Symbols.SPY, -100, desiredTime.AddMinutes(-60));
// Set LocalTime to slightly after market close
var localTime = desiredTime + timeOffset - TimeSpan.FromTicks(1);
var utcTime = localTime.ConvertToUtc(TimeZones.NewYork);
var timeKeeper = new TimeKeeper(utcTime, new[] { TimeZones.NewYork });
security.SetLocalTimeKeeper(timeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
// Seed last regular bar
security.SetMarketPrice(new TradeBar(desiredTime - timeOffset, Symbols.SPY, 101.123m, 101.123m, 101.123m, 100, 100, timeOffset));
var fill = model.Fill(new FillModelParameters(
security,
order,
new MockSubscriptionDataConfigProvider(config),
Time.OneHour,
null)).Single();
Assert.AreEqual(OrderStatus.Filled, fill.Status);
}
private Equity CreateEquity(SubscriptionDataConfig config)
{
var equity = new Equity(
SecurityExchangeHoursTests.CreateUsEquitySecurityExchangeHours(),
config,
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
Exchange.ARCA
);
equity.SetLocalTimeKeeper(TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
return equity;
}
private SubscriptionDataConfig CreateTickConfig(Symbol symbol, bool extendedHours = true)
{
return new SubscriptionDataConfig(typeof(Tick), symbol, Resolution.Tick, TimeZones.NewYork, TimeZones.NewYork, true, extendedHours, false);
}
private SubscriptionDataConfig CreateQuoteBarConfig(Symbol symbol, Resolution resolution = Resolution.Minute, bool extendedHours = true)
{
return new SubscriptionDataConfig(typeof(QuoteBar), symbol, resolution, TimeZones.NewYork, TimeZones.NewYork, true, extendedHours, false);
}
private SubscriptionDataConfig CreateTradeBarConfig(Symbol symbol, Resolution resolution = Resolution.Minute, bool extendedHours = true)
{
return new SubscriptionDataConfig(typeof(TradeBar), symbol, resolution, TimeZones.NewYork, TimeZones.NewYork, true, extendedHours, false);
}
private FillModelParameters GetFillModelParameters(Order order)
{
var configTradeBar = CreateTradeBarConfig(order.Symbol);
var configQuoteBar = new SubscriptionDataConfig(configTradeBar, typeof(QuoteBar));
var configProvider = new MockSubscriptionDataConfigProvider(configQuoteBar);
configProvider.SubscriptionDataConfigs.Add(configTradeBar);
var security = CreateEquity(configTradeBar);
return new FillModelParameters(
security,
order,
configProvider,
Time.OneHour,
null);
}
private class TestFillModel : EquityFillModel
{
public void SetParameters(FillModelParameters parameters)
{
Parameters = parameters;
}
public Prices GetPricesPublic(Security asset, OrderDirection direction)
{
return base.GetPrices(asset, direction);
}
}
}
}