From c19cba061ae74420de87cbce22c29769ebb42187 Mon Sep 17 00:00:00 2001 From: stoicAI1776 Date: Wed, 13 May 2026 11:39:23 +0530 Subject: [PATCH 1/4] Respect outside regular hours in fill models --- Common/Orders/Fills/EquityFillModel.cs | 10 +-- Common/Orders/Fills/FillModel.cs | 62 +++++++++++++++++-- .../EquityFillModelTests.StopMarketFill.cs | 34 ++++++++++ 3 files changed, 95 insertions(+), 11 deletions(-) diff --git a/Common/Orders/Fills/EquityFillModel.cs b/Common/Orders/Fills/EquityFillModel.cs index 50d593fd2260..4b388e065ce6 100644 --- a/Common/Orders/Fills/EquityFillModel.cs +++ b/Common/Orders/Fills/EquityFillModel.cs @@ -59,7 +59,7 @@ public override OrderEvent LimitIfTouchedFill(Security asset, LimitIfTouchedOrde if (order.Status == OrderStatus.Canceled) return fill; // Fill only if open or extended - if (!IsExchangeOpen(asset, + if (!IsExchangeOpen(asset, order, Parameters.ConfigProvider .GetSubscriptionDataConfigs(asset.Symbol) .IsExtendedMarketHours())) @@ -130,7 +130,7 @@ public override OrderEvent MarketFill(Security asset, MarketOrder order) if (order.Status == OrderStatus.Canceled) return fill; // Make sure the exchange is open/normal market hours before filling - if (!IsExchangeOpen(asset, false)) return fill; + if (!IsExchangeOpen(asset, order, false)) return fill; // Calculate the model slippage: e.g. 0.01c var slip = asset.SlippageModel.GetSlippageApproximation(asset, order); @@ -187,7 +187,7 @@ public override OrderEvent StopMarketFill(Security asset, StopMarketOrder order) if (order.Status == OrderStatus.Canceled) return fill; // Make sure the exchange is open/normal market hours before filling - if (!IsExchangeOpen(asset, false)) return fill; + if (!IsExchangeOpen(asset, order, false)) return fill; // Get the trade bar that closes after the order time var tradeBar = GetBestEffortTradeBar(asset, order.Time); @@ -266,7 +266,7 @@ public override OrderEvent StopLimitFill(Security asset, StopLimitOrder order) // make sure the exchange is open before filling -- allow pre/post market fills to occur if (!IsExchangeOpen( - asset, + asset, order, Parameters.ConfigProvider .GetSubscriptionDataConfigs(asset.Symbol) .IsExtendedMarketHours())) @@ -371,7 +371,7 @@ public override OrderEvent LimitFill(Security asset, LimitOrder order) if (order.Status == OrderStatus.Canceled) return fill; // make sure the exchange is open before filling -- allow pre/post market fills to occur - if (!IsExchangeOpen(asset, + if (!IsExchangeOpen(asset, order, Parameters.ConfigProvider .GetSubscriptionDataConfigs(asset.Symbol) .IsExtendedMarketHours())) diff --git a/Common/Orders/Fills/FillModel.cs b/Common/Orders/Fills/FillModel.cs index 2ea61602b6b0..2a366d6d9025 100644 --- a/Common/Orders/Fills/FillModel.cs +++ b/Common/Orders/Fills/FillModel.cs @@ -286,7 +286,7 @@ private OrderEvent InternalMarketFill(Security asset, Order order, decimal quant if (order.Status == OrderStatus.Canceled) return fill; // make sure the exchange is open/normal market hours before filling - if (!IsExchangeOpen(asset, false)) return fill; + if (!IsExchangeOpen(asset, order, false)) return fill; var orderDirection = order.Direction; var prices = GetPricesCheckingPythonWrapper(asset, orderDirection); @@ -339,7 +339,7 @@ public virtual OrderEvent StopMarketFill(Security asset, StopMarketOrder order) if (order.Status == OrderStatus.Canceled) return fill; // make sure the exchange is open/normal market hours before filling - if (!IsExchangeOpen(asset, false)) return fill; + if (!IsExchangeOpen(asset, order, false)) return fill; //Get the range of prices in the last bar: var prices = GetPricesCheckingPythonWrapper(asset, order.Direction); @@ -398,7 +398,7 @@ public virtual OrderEvent TrailingStopFill(Security asset, TrailingStopOrder ord if (order.Status == OrderStatus.Canceled) return fill; // Make sure the exchange is open/normal market hours before filling - if (!IsExchangeOpen(asset, false)) return fill; + if (!IsExchangeOpen(asset, order, false)) return fill; // Get the range of prices in the last bar: var prices = GetPricesCheckingPythonWrapper(asset, order.Direction); @@ -478,7 +478,7 @@ public virtual OrderEvent StopLimitFill(Security asset, StopLimitOrder order) if (order.Status == OrderStatus.Canceled) return fill; // make sure the exchange is open before filling -- allow pre/post market fills to occur - if (!IsExchangeOpen(asset)) + if (!IsExchangeOpen(asset, order)) { return fill; } @@ -568,7 +568,7 @@ public virtual OrderEvent LimitIfTouchedFill(Security asset, LimitIfTouchedOrder if (order.Status == OrderStatus.Canceled) return fill; // Fill only if open or extended - if (!IsExchangeOpen(asset)) + if (!IsExchangeOpen(asset, order)) { return fill; } @@ -670,7 +670,7 @@ private OrderEvent InternalLimitFill(Security asset, Order order, decimal limitP if (order.Status == OrderStatus.Canceled) return fill; // make sure the exchange is open before filling -- allow pre/post market fills to occur - if (!IsExchangeOpen(asset)) + if (!IsExchangeOpen(asset, order)) { return fill; } @@ -1095,6 +1095,11 @@ protected virtual bool IsExchangeOpen(Security asset, bool isExtendedMarketHours } var barSpan = currentBar.EndTime - currentBar.Time; + if (barSpan < Time.OneHour) + { + return false; + } + var isOnCurrentBar = barSpan > Time.OneHour // for fill purposes we consider the market open for daily bars if we are in the same day ? asset.LocalTime.Date == currentBar.EndTime.Date @@ -1107,6 +1112,51 @@ protected virtual bool IsExchangeOpen(Security asset, bool isExtendedMarketHours return true; } + /// + /// Determines if the exchange is open for the specified order using the current time of the asset + /// + protected virtual bool IsExchangeOpen(Security asset, Order order, bool isExtendedMarketHours) + { + if (TryGetOutsideRegularTradingHours(order, out var outsideRegularTradingHours)) + { + isExtendedMarketHours = outsideRegularTradingHours; + } + + return IsExchangeOpen(asset, isExtendedMarketHours); + } + + private bool IsExchangeOpen(Security asset, Order order) + { + if (TryGetOutsideRegularTradingHours(order, out var outsideRegularTradingHours)) + { + return IsExchangeOpen(asset, outsideRegularTradingHours); + } + + return IsExchangeOpen(asset); + } + + private static bool TryGetOutsideRegularTradingHours(Order order, out bool outsideRegularTradingHours) + { + switch (order.Properties) + { + case AlpacaOrderProperties properties: + outsideRegularTradingHours = properties.OutsideRegularTradingHours; + return true; + case InteractiveBrokersOrderProperties properties: + outsideRegularTradingHours = properties.OutsideRegularTradingHours; + return true; + case TradierOrderProperties properties: + outsideRegularTradingHours = properties.OutsideRegularTradingHours; + return true; + case TradeStationOrderProperties properties: + outsideRegularTradingHours = properties.OutsideRegularTradingHours; + return true; + default: + outsideRegularTradingHours = false; + return false; + } + } + private class ComboLimitOrderLegParameters { public Security Security { get; set; } diff --git a/Tests/Common/Orders/Fills/EquityFillModelTests.StopMarketFill.cs b/Tests/Common/Orders/Fills/EquityFillModelTests.StopMarketFill.cs index 76154371b457..09b4d7f4b868 100644 --- a/Tests/Common/Orders/Fills/EquityFillModelTests.StopMarketFill.cs +++ b/Tests/Common/Orders/Fills/EquityFillModelTests.StopMarketFill.cs @@ -236,6 +236,40 @@ public void StopMarketOrderDoesNotFillUsingTickTypeQuote(decimal orderQuantity, Assert.AreEqual(OrderStatus.None, fill.Status); } + [TestCase(false, OrderStatus.None, 0)] + [TestCase(true, OrderStatus.Filled, 10)] + public void StopMarketOrderRespectsOutsideRegularTradingHours(bool outsideRegularTradingHours, OrderStatus expectedStatus, decimal expectedQuantity) + { + var time = new DateTime(2026, 5, 5, 9, 29, 0); + var timeKeeper = new TimeKeeper(time.ConvertToUtc(TimeZones.NewYork), TimeZones.NewYork); + var fillModel = new EquityFillModel(); + var configTradeBar = CreateTradeBarConfig(Symbols.SPY, extendedHours: true); + var equity = CreateEquity(configTradeBar); + var order = new StopMarketOrder( + Symbols.SPY, + 10, + 1m, + time.ConvertToUtc(TimeZones.NewYork), + properties: new TradeStationOrderProperties + { + TimeInForce = TimeInForce.Day, + OutsideRegularTradingHours = outsideRegularTradingHours + }); + + equity.SetLocalTimeKeeper(timeKeeper.GetLocalTimeKeeper(TimeZones.NewYork)); + equity.SetMarketPrice(new TradeBar(time, Symbols.SPY, 100m, 101m, 99m, 100m, 100, Time.OneMinute)); + + var fill = fillModel.Fill(new FillModelParameters( + equity, + order, + new MockSubscriptionDataConfigProvider(configTradeBar), + Time.OneHour, + null)).Single(); + + Assert.AreEqual(expectedStatus, fill.Status); + Assert.AreEqual(expectedQuantity, fill.FillQuantity); + } + [TestCase(100, 290.50)] [TestCase(-100, 291.50)] public void StopMarketOrderFillsAtOpenWithUnfavourableGap(decimal orderQuantity, decimal stopPrice) From 2cf9ffa48b496e72f9ef75308e34ba9d2cafbcbe Mon Sep 17 00:00:00 2001 From: stoicAI1776 Date: Wed, 13 May 2026 12:04:43 +0530 Subject: [PATCH 2/4] Narrow fill timing change --- Common/Orders/Fills/FillModel.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Common/Orders/Fills/FillModel.cs b/Common/Orders/Fills/FillModel.cs index 2a366d6d9025..6a8cd6ca1a33 100644 --- a/Common/Orders/Fills/FillModel.cs +++ b/Common/Orders/Fills/FillModel.cs @@ -1095,11 +1095,6 @@ protected virtual bool IsExchangeOpen(Security asset, bool isExtendedMarketHours } var barSpan = currentBar.EndTime - currentBar.Time; - if (barSpan < Time.OneHour) - { - return false; - } - var isOnCurrentBar = barSpan > Time.OneHour // for fill purposes we consider the market open for daily bars if we are in the same day ? asset.LocalTime.Date == currentBar.EndTime.Date @@ -1119,7 +1114,9 @@ protected virtual bool IsExchangeOpen(Security asset, Order order, bool isExtend { if (TryGetOutsideRegularTradingHours(order, out var outsideRegularTradingHours)) { - isExtendedMarketHours = outsideRegularTradingHours; + return outsideRegularTradingHours + ? IsExchangeOpen(asset, true) + : asset.Exchange.Hours.IsOpen(asset.LocalTime, false); } return IsExchangeOpen(asset, isExtendedMarketHours); @@ -1129,7 +1126,9 @@ private bool IsExchangeOpen(Security asset, Order order) { if (TryGetOutsideRegularTradingHours(order, out var outsideRegularTradingHours)) { - return IsExchangeOpen(asset, outsideRegularTradingHours); + return outsideRegularTradingHours + ? IsExchangeOpen(asset, true) + : asset.Exchange.Hours.IsOpen(asset.LocalTime, false); } return IsExchangeOpen(asset); From 287efb683fe7db98a057df20ac750ed38a5a451d Mon Sep 17 00:00:00 2001 From: stoicAI1776 Date: Wed, 13 May 2026 12:43:26 +0530 Subject: [PATCH 3/4] Adjust outside hours fill regression --- .../EquityFillModelTests.StopMarketFill.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/Tests/Common/Orders/Fills/EquityFillModelTests.StopMarketFill.cs b/Tests/Common/Orders/Fills/EquityFillModelTests.StopMarketFill.cs index 09b4d7f4b868..694e5a4e84c2 100644 --- a/Tests/Common/Orders/Fills/EquityFillModelTests.StopMarketFill.cs +++ b/Tests/Common/Orders/Fills/EquityFillModelTests.StopMarketFill.cs @@ -236,25 +236,28 @@ public void StopMarketOrderDoesNotFillUsingTickTypeQuote(decimal orderQuantity, Assert.AreEqual(OrderStatus.None, fill.Status); } - [TestCase(false, OrderStatus.None, 0)] - [TestCase(true, OrderStatus.Filled, 10)] - public void StopMarketOrderRespectsOutsideRegularTradingHours(bool outsideRegularTradingHours, OrderStatus expectedStatus, decimal expectedQuantity) + [TestCase(true, OrderStatus.None, 0)] + [TestCase(false, OrderStatus.Filled, 10)] + public void StopMarketOrderRespectsOutsideRegularTradingHours(bool submitOutsideRegularHoursRestriction, OrderStatus expectedStatus, decimal expectedQuantity) { var time = new DateTime(2026, 5, 5, 9, 29, 0); var timeKeeper = new TimeKeeper(time.ConvertToUtc(TimeZones.NewYork), TimeZones.NewYork); var fillModel = new EquityFillModel(); var configTradeBar = CreateTradeBarConfig(Symbols.SPY, extendedHours: true); var equity = CreateEquity(configTradeBar); + var orderProperties = submitOutsideRegularHoursRestriction + ? new TradeStationOrderProperties + { + TimeInForce = TimeInForce.Day, + OutsideRegularTradingHours = false + } + : null; var order = new StopMarketOrder( Symbols.SPY, 10, 1m, time.ConvertToUtc(TimeZones.NewYork), - properties: new TradeStationOrderProperties - { - TimeInForce = TimeInForce.Day, - OutsideRegularTradingHours = outsideRegularTradingHours - }); + properties: orderProperties); equity.SetLocalTimeKeeper(timeKeeper.GetLocalTimeKeeper(TimeZones.NewYork)); equity.SetMarketPrice(new TradeBar(time, Symbols.SPY, 100m, 101m, 99m, 100m, 100, Time.OneMinute)); From 47d3e94a27d28296200263acbaeb327295f6f910 Mon Sep 17 00:00:00 2001 From: stoicAI1776 Date: Wed, 13 May 2026 13:13:28 +0530 Subject: [PATCH 4/4] Focus outside hours regression --- .../EquityFillModelTests.StopMarketFill.cs | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/Tests/Common/Orders/Fills/EquityFillModelTests.StopMarketFill.cs b/Tests/Common/Orders/Fills/EquityFillModelTests.StopMarketFill.cs index 694e5a4e84c2..1801c50a7c49 100644 --- a/Tests/Common/Orders/Fills/EquityFillModelTests.StopMarketFill.cs +++ b/Tests/Common/Orders/Fills/EquityFillModelTests.StopMarketFill.cs @@ -236,28 +236,23 @@ public void StopMarketOrderDoesNotFillUsingTickTypeQuote(decimal orderQuantity, Assert.AreEqual(OrderStatus.None, fill.Status); } - [TestCase(true, OrderStatus.None, 0)] - [TestCase(false, OrderStatus.Filled, 10)] - public void StopMarketOrderRespectsOutsideRegularTradingHours(bool submitOutsideRegularHoursRestriction, OrderStatus expectedStatus, decimal expectedQuantity) + public void StopMarketOrderRespectsOutsideRegularTradingHours() { var time = new DateTime(2026, 5, 5, 9, 29, 0); var timeKeeper = new TimeKeeper(time.ConvertToUtc(TimeZones.NewYork), TimeZones.NewYork); var fillModel = new EquityFillModel(); var configTradeBar = CreateTradeBarConfig(Symbols.SPY, extendedHours: true); var equity = CreateEquity(configTradeBar); - var orderProperties = submitOutsideRegularHoursRestriction - ? new TradeStationOrderProperties - { - TimeInForce = TimeInForce.Day, - OutsideRegularTradingHours = false - } - : null; var order = new StopMarketOrder( Symbols.SPY, 10, 1m, time.ConvertToUtc(TimeZones.NewYork), - properties: orderProperties); + properties: new TradeStationOrderProperties + { + TimeInForce = TimeInForce.Day, + OutsideRegularTradingHours = false + }); equity.SetLocalTimeKeeper(timeKeeper.GetLocalTimeKeeper(TimeZones.NewYork)); equity.SetMarketPrice(new TradeBar(time, Symbols.SPY, 100m, 101m, 99m, 100m, 100, Time.OneMinute)); @@ -269,8 +264,8 @@ public void StopMarketOrderRespectsOutsideRegularTradingHours(bool submitOutside Time.OneHour, null)).Single(); - Assert.AreEqual(expectedStatus, fill.Status); - Assert.AreEqual(expectedQuantity, fill.FillQuantity); + Assert.AreEqual(OrderStatus.None, fill.Status); + Assert.AreEqual(0, fill.FillQuantity); } [TestCase(100, 290.50)]