Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions Common/Orders/Fills/EquityFillModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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()))
Expand Down Expand Up @@ -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()))
Expand Down
61 changes: 55 additions & 6 deletions Common/Orders/Fills/FillModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -1107,6 +1107,55 @@ protected virtual bool IsExchangeOpen(Security asset, bool isExtendedMarketHours
return true;
}

/// <summary>
/// Determines if the exchange is open for the specified order using the current time of the asset
/// </summary>
protected virtual bool IsExchangeOpen(Security asset, Order order, bool isExtendedMarketHours)
{
if (TryGetOutsideRegularTradingHours(order, out var outsideRegularTradingHours))
{
return outsideRegularTradingHours
? IsExchangeOpen(asset, true)
: asset.Exchange.Hours.IsOpen(asset.LocalTime, false);
}

return IsExchangeOpen(asset, isExtendedMarketHours);
}

private bool IsExchangeOpen(Security asset, Order order)
{
if (TryGetOutsideRegularTradingHours(order, out var outsideRegularTradingHours))
{
return outsideRegularTradingHours
? IsExchangeOpen(asset, true)
: asset.Exchange.Hours.IsOpen(asset.LocalTime, false);
}

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; }
Expand Down
32 changes: 32 additions & 0 deletions Tests/Common/Orders/Fills/EquityFillModelTests.StopMarketFill.cs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,38 @@ public void StopMarketOrderDoesNotFillUsingTickTypeQuote(decimal orderQuantity,
Assert.AreEqual(OrderStatus.None, fill.Status);
}

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 order = new StopMarketOrder(
Symbols.SPY,
10,
1m,
time.ConvertToUtc(TimeZones.NewYork),
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));

var fill = fillModel.Fill(new FillModelParameters(
equity,
order,
new MockSubscriptionDataConfigProvider(configTradeBar),
Time.OneHour,
null)).Single();

Assert.AreEqual(OrderStatus.None, fill.Status);
Assert.AreEqual(0, fill.FillQuantity);
}

[TestCase(100, 290.50)]
[TestCase(-100, 291.50)]
public void StopMarketOrderFillsAtOpenWithUnfavourableGap(decimal orderQuantity, decimal stopPrice)
Expand Down