diff --git a/Common/Messages/Messages.Orders.Fills.cs b/Common/Messages/Messages.Orders.Fills.cs
index d9594ab2b7b9..88854ec4ce45 100644
--- a/Common/Messages/Messages.Orders.Fills.cs
+++ b/Common/Messages/Messages.Orders.Fills.cs
@@ -56,6 +56,14 @@ public static string MarketNeverCloses(Securities.Security security, OrderType o
return Invariant($"Market never closes for this symbol {security.Symbol}, can no submit a {nameof(orderType)} order.");
}
+ ///
+ /// Returns a string message saying that the order was filled using the open price due to a favorable gap
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static string FilledWithOpenDueToFavorableGap(Securities.Security security, Prices prices)
+ {
+ return Invariant($@"Due to a favorable gap at {prices.EndTime.ToStringInvariant()} {security.Exchange.TimeZone}, order filled using the open price ({prices.Open})");
+ }
///
/// Returns a string message containing the given subscribedTypes
///
diff --git a/Common/Orders/Fills/EquityFillModel.cs b/Common/Orders/Fills/EquityFillModel.cs
index a1c766b70d4f..4a785b1c9e2b 100644
--- a/Common/Orders/Fills/EquityFillModel.cs
+++ b/Common/Orders/Fills/EquityFillModel.cs
@@ -304,25 +304,41 @@ public override OrderEvent StopLimitFill(Security asset, StopLimitOrder order)
if (pricesEndTime <= order.Time) return fill;
//Check if the Stop Order was filled: opposite to a limit order
+ var triggeredOnEarlierBar = order.StopTriggered;
switch (order.Direction)
{
case OrderDirection.Buy:
//-> 1.2 Buy Stop: If Price Above Setpoint, Buy:
- if (prices.High > order.StopPrice || order.StopTriggered)
+ if (prices.High > order.StopPrice || triggeredOnEarlierBar)
{
- if (!order.StopTriggered)
+ if (!triggeredOnEarlierBar)
{
order.StopTriggered = true;
Parameters.OnOrderUpdated(order);
}
- // Fill the limit order, using closing price of bar:
- // Note > Can't use minimum price, because no way to be sure minimum wasn't before the stop triggered.
- if (prices.Current < order.LimitPrice)
+ if (triggeredOnEarlierBar)
{
+ // The entire bar is post-trigger, so the limit leg behaves as a resting limit order.
+ var tradeBar = GetBestEffortTradeBar(asset, order.Time);
+ if (tradeBar != null && tradeBar.Low < order.LimitPrice)
+ {
+ fill.Status = OrderStatus.Filled;
+ fill.FillPrice = order.LimitPrice;
+ fill.FillQuantity = order.Quantity;
+
+ if (tradeBar.Open < order.LimitPrice)
+ {
+ fill.FillPrice = tradeBar.Open;
+ fill.Message = Messages.EquityFillModel.FilledWithOpenDueToFavorableGap(asset, tradeBar);
+ }
+ }
+ }
+ else if (prices.Current < order.LimitPrice)
+ {
+ // On the trigger bar, preserve the conservative close-based test.
fill.Status = OrderStatus.Filled;
fill.FillPrice = Math.Min(prices.High, order.LimitPrice);
- // assume the order completely filled
fill.FillQuantity = order.Quantity;
}
}
@@ -330,27 +346,41 @@ public override OrderEvent StopLimitFill(Security asset, StopLimitOrder order)
case OrderDirection.Sell:
//-> 1.1 Sell Stop: If Price below setpoint, Sell:
- if (prices.Low < order.StopPrice || order.StopTriggered)
+ if (prices.Low < order.StopPrice || triggeredOnEarlierBar)
{
- if (!order.StopTriggered)
+ if (!triggeredOnEarlierBar)
{
order.StopTriggered = true;
Parameters.OnOrderUpdated(order);
}
- // Fill the limit order, using minimum price of the bar
- // Note > Can't use minimum price, because no way to be sure minimum wasn't before the stop triggered.
- if (prices.Current > order.LimitPrice)
+ if (triggeredOnEarlierBar)
{
+ // The entire bar is post-trigger, so the limit leg behaves as a resting limit order.
+ var tradeBar = GetBestEffortTradeBar(asset, order.Time);
+ if (tradeBar != null && tradeBar.High > order.LimitPrice)
+ {
+ fill.Status = OrderStatus.Filled;
+ fill.FillPrice = order.LimitPrice;
+ fill.FillQuantity = order.Quantity;
+
+ if (tradeBar.Open > order.LimitPrice)
+ {
+ fill.FillPrice = tradeBar.Open;
+ fill.Message = Messages.EquityFillModel.FilledWithOpenDueToFavorableGap(asset, tradeBar);
+ }
+ }
+ }
+ else if (prices.Current > order.LimitPrice)
+ {
+ // On the trigger bar, preserve the conservative close-based test.
fill.Status = OrderStatus.Filled;
fill.FillPrice = Math.Max(prices.Low, order.LimitPrice);
- // assume the order completely filled
fill.FillQuantity = order.Quantity;
}
}
break;
}
-
return fill;
}
diff --git a/Common/Orders/Fills/FillModel.cs b/Common/Orders/Fills/FillModel.cs
index e4cdf3381afd..b324dc8f7ec9 100644
--- a/Common/Orders/Fills/FillModel.cs
+++ b/Common/Orders/Fills/FillModel.cs
@@ -506,25 +506,40 @@ public virtual OrderEvent StopLimitFill(Security asset, StopLimitOrder order)
if (pricesEndTime <= order.Time) return fill;
//Check if the Stop Order was filled: opposite to a limit order
+ var triggeredOnEarlierBar = order.StopTriggered;
switch (order.Direction)
{
case OrderDirection.Buy:
//-> 1.2 Buy Stop: If Price Above Setpoint, Buy:
- if (prices.High > order.StopPrice || order.StopTriggered)
+ if (prices.High > order.StopPrice || triggeredOnEarlierBar)
{
- if (!order.StopTriggered)
+ if (!triggeredOnEarlierBar)
{
order.StopTriggered = true;
Parameters.OnOrderUpdated(order);
}
- // Fill the limit order, using closing price of bar:
- // Note > Can't use minimum price, because no way to be sure minimum wasn't before the stop triggered.
- if (prices.Current < order.LimitPrice)
+ if (triggeredOnEarlierBar)
{
+ // The entire bar is post-trigger, so the limit leg behaves as a resting limit order.
+ if (prices.Low < order.LimitPrice)
+ {
+ fill.Status = OrderStatus.Filled;
+ fill.FillPrice = order.LimitPrice;
+ fill.FillQuantity = order.Quantity;
+
+ if (prices.Open < order.LimitPrice)
+ {
+ fill.FillPrice = prices.Open;
+ fill.Message = Messages.FillModel.FilledWithOpenDueToFavorableGap(asset, prices);
+ }
+ }
+ }
+ else if (prices.Current < order.LimitPrice)
+ {
+ // On the trigger bar, preserve the conservative close-based test.
fill.Status = OrderStatus.Filled;
fill.FillPrice = Math.Min(prices.High, order.LimitPrice);
- // assume the order completely filled
fill.FillQuantity = order.Quantity;
}
}
@@ -532,27 +547,40 @@ public virtual OrderEvent StopLimitFill(Security asset, StopLimitOrder order)
case OrderDirection.Sell:
//-> 1.1 Sell Stop: If Price below setpoint, Sell:
- if (prices.Low < order.StopPrice || order.StopTriggered)
+ if (prices.Low < order.StopPrice || triggeredOnEarlierBar)
{
- if (!order.StopTriggered)
+ if (!triggeredOnEarlierBar)
{
order.StopTriggered = true;
Parameters.OnOrderUpdated(order);
}
- // Fill the limit order, using minimum price of the bar
- // Note > Can't use minimum price, because no way to be sure minimum wasn't before the stop triggered.
- if (prices.Current > order.LimitPrice)
+ if (triggeredOnEarlierBar)
{
+ // The entire bar is post-trigger, so the limit leg behaves as a resting limit order.
+ if (prices.High > order.LimitPrice)
+ {
+ fill.Status = OrderStatus.Filled;
+ fill.FillPrice = order.LimitPrice;
+ fill.FillQuantity = order.Quantity;
+
+ if (prices.Open > order.LimitPrice)
+ {
+ fill.FillPrice = prices.Open;
+ fill.Message = Messages.FillModel.FilledWithOpenDueToFavorableGap(asset, prices);
+ }
+ }
+ }
+ else if (prices.Current > order.LimitPrice)
+ {
+ // On the trigger bar, preserve the conservative close-based test.
fill.Status = OrderStatus.Filled;
fill.FillPrice = Math.Max(prices.Low, order.LimitPrice);
- // assume the order completely filled
fill.FillQuantity = order.Quantity;
}
}
break;
}
-
return fill;
}
diff --git a/Tests/Common/Orders/Fills/EquityFillModelTests.cs b/Tests/Common/Orders/Fills/EquityFillModelTests.cs
index 8dc4c81bbe7a..be1edb7a1d48 100644
--- a/Tests/Common/Orders/Fills/EquityFillModelTests.cs
+++ b/Tests/Common/Orders/Fills/EquityFillModelTests.cs
@@ -132,6 +132,161 @@ public void PerformsMarketFillSell(int orderHour, int quoteBarHour, int tradeBar
Assert.IsTrue(fill.Message.StartsWith(message, StringComparison.InvariantCultureIgnoreCase));
}
+ [Test]
+ public void StopLimitBuyFillsOnLimitPenetrationAfterEarlierTrigger()
+ {
+ var model = new EquityFillModel();
+ var order = new StopLimitOrder(Symbols.SPY, 100, 100m, 100.25m, 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));
+
+ // Trigger bar: stop crossed, but close remains above the buy limit.
+ security.SetMarketPrice(new TradeBar(
+ Noon, Symbols.SPY, 100.50m, 101.50m, 100.40m, 101.00m, 100));
+
+ var fill = model.Fill(new FillModelParameters(
+ security,
+ order,
+ new MockSubscriptionDataConfigProvider(config),
+ Time.OneHour,
+ null)).Single();
+
+ Assert.AreEqual(OrderStatus.None, fill.Status);
+ Assert.IsTrue(order.StopTriggered);
+
+ // Later bar: the resting limit is penetrated although the close remains above it.
+ security.SetMarketPrice(new TradeBar(
+ Noon.AddMinutes(1), Symbols.SPY, 100.60m, 100.80m, 99.80m, 100.60m, 100));
+
+ fill = model.Fill(new FillModelParameters(
+ security,
+ order,
+ new MockSubscriptionDataConfigProvider(config),
+ Time.OneHour,
+ null)).Single();
+
+ Assert.AreEqual(OrderStatus.Filled, fill.Status);
+ Assert.AreEqual(order.LimitPrice, fill.FillPrice);
+ Assert.AreEqual(order.Quantity, fill.FillQuantity);
+ }
+ [Test]
+ public void StopLimitBuyFillsAtOpenAfterEarlierTrigger()
+ {
+ var model = new EquityFillModel();
+ var order = new StopLimitOrder(Symbols.SPY, 100, 100m, 100.25m, 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 TradeBar(
+ Noon, Symbols.SPY, 100.50m, 101.50m, 100.40m, 101.00m, 100));
+
+ var fill = model.Fill(new FillModelParameters(
+ security, order, new MockSubscriptionDataConfigProvider(config),
+ Time.OneHour, null)).Single();
+
+ Assert.AreEqual(OrderStatus.None, fill.Status);
+ Assert.IsTrue(order.StopTriggered);
+
+ security.SetMarketPrice(new TradeBar(
+ Noon.AddMinutes(1), Symbols.SPY, 99.80m, 100.80m, 99.50m, 100.60m, 100));
+
+ fill = model.Fill(new FillModelParameters(
+ security, order, new MockSubscriptionDataConfigProvider(config),
+ Time.OneHour, null)).Single();
+
+ Assert.AreEqual(OrderStatus.Filled, fill.Status);
+ Assert.AreEqual(99.80m, fill.FillPrice);
+ Assert.IsTrue(fill.Message.Contains("favorable gap", StringComparison.InvariantCultureIgnoreCase));
+ }
+
+ [Test]
+ public void StopLimitBuyDoesNotUseLowOnTriggerBar()
+ {
+ var model = new EquityFillModel();
+ var order = new StopLimitOrder(Symbols.SPY, 100, 100m, 100.25m, 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));
+
+ // The bar crosses both stop and limit, but closes above the limit.
+ // We cannot know whether the low occurred before the stop trigger.
+ security.SetMarketPrice(new TradeBar(
+ Noon, Symbols.SPY, 100.50m, 101.50m, 99.80m, 101.00m, 100));
+
+ var fill = model.Fill(new FillModelParameters(
+ security, order, new MockSubscriptionDataConfigProvider(config),
+ Time.OneHour, null)).Single();
+
+ Assert.IsTrue(order.StopTriggered);
+ Assert.AreEqual(OrderStatus.None, fill.Status);
+ Assert.AreEqual(0, fill.FillQuantity);
+ }
+
+ [Test]
+ public void StopLimitSellFillsOnLimitPenetrationAfterEarlierTrigger()
+ {
+ var model = new EquityFillModel();
+ var order = new StopLimitOrder(Symbols.SPY, -100, 100m, 99.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 TradeBar(
+ Noon, Symbols.SPY, 100.20m, 100.30m, 98.50m, 99.50m, 100));
+
+ var fill = model.Fill(new FillModelParameters(
+ security, order, new MockSubscriptionDataConfigProvider(config),
+ Time.OneHour, null)).Single();
+
+ Assert.AreEqual(OrderStatus.None, fill.Status);
+ Assert.IsTrue(order.StopTriggered);
+
+ security.SetMarketPrice(new TradeBar(
+ Noon.AddMinutes(1), Symbols.SPY, 99.60m, 100.10m, 99.40m, 99.60m, 100));
+
+ fill = model.Fill(new FillModelParameters(
+ security, order, new MockSubscriptionDataConfigProvider(config),
+ Time.OneHour, null)).Single();
+
+ Assert.AreEqual(OrderStatus.Filled, fill.Status);
+ Assert.AreEqual(order.LimitPrice, fill.FillPrice);
+ Assert.AreEqual(order.Quantity, fill.FillQuantity);
+ }
[Test]
public void PerformsStopLimitFillBuy()
{
@@ -174,13 +329,14 @@ public void PerformsStopLimitFillBuy()
Assert.AreEqual(0, fill.FillPrice);
Assert.AreEqual(OrderStatus.None, fill.Status);
- security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, 101.66m));
+ security.SetMarketPrice(new TradeBar(
+ Noon, Symbols.SPY, 102m, 102m, 101.66m, 101.66m, 100));
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(order.LimitPrice, fill.FillPrice);
Assert.AreEqual(OrderStatus.Filled, fill.Status);
}
@@ -226,13 +382,14 @@ public void PerformsStopLimitFillSell()
Assert.AreEqual(0, fill.FillPrice);
Assert.AreEqual(OrderStatus.None, fill.Status);
- security.SetMarketPrice(new IndicatorDataPoint(Symbols.SPY, Noon, 101.66m));
+ security.SetMarketPrice(new TradeBar(
+ Noon, Symbols.SPY, 101m, 101.66m, 101m, 101.66m, 100));
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(order.LimitPrice, fill.FillPrice);
Assert.AreEqual(OrderStatus.Filled, fill.Status);
}
diff --git a/Tests/Common/Orders/Fills/ImmediateFillModelTests.cs b/Tests/Common/Orders/Fills/ImmediateFillModelTests.cs
index cda619762a64..08ce9f554438 100644
--- a/Tests/Common/Orders/Fills/ImmediateFillModelTests.cs
+++ b/Tests/Common/Orders/Fills/ImmediateFillModelTests.cs
@@ -227,6 +227,123 @@ public void PerformsLimitFillSell(bool isInternal)
Assert.AreEqual(OrderStatus.Filled, fill.Status);
}
+ [TestCase(true)]
+ [TestCase(false)]
+ public void StopLimitBuyFillsOnLimitPenetrationAfterEarlierTrigger(bool isInternal)
+ {
+ var model = new ImmediateFillModel();
+ var order = new StopLimitOrder(Symbols.SPY, 100, 100m, 100.25m, Noon);
+ var config = CreateTradeBarConfig(Symbols.SPY, isInternal);
+ var security = GetSecurity(config);
+ security.SetLocalTimeKeeper(TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
+
+ security.SetMarketPrice(new TradeBar(
+ Noon, Symbols.SPY, 100.50m, 101.50m, 100.40m, 101.00m, 100));
+
+ var fill = model.Fill(new FillModelParameters(
+ security, order, new MockSubscriptionDataConfigProvider(config),
+ Time.OneHour, null)).Single();
+
+ Assert.AreEqual(OrderStatus.None, fill.Status);
+ Assert.IsTrue(order.StopTriggered);
+
+ security.SetMarketPrice(new TradeBar(
+ Noon.AddMinutes(1), Symbols.SPY, 100.60m, 100.80m, 99.80m, 100.60m, 100));
+
+ fill = model.Fill(new FillModelParameters(
+ security, order, new MockSubscriptionDataConfigProvider(config),
+ Time.OneHour, null)).Single();
+
+ Assert.AreEqual(OrderStatus.Filled, fill.Status);
+ Assert.AreEqual(order.LimitPrice, fill.FillPrice);
+ Assert.AreEqual(order.Quantity, fill.FillQuantity);
+ }
+
+ [TestCase(true)]
+ [TestCase(false)]
+ public void StopLimitBuyFillsAtOpenAfterEarlierTrigger(bool isInternal)
+ {
+ var model = new ImmediateFillModel();
+ var order = new StopLimitOrder(Symbols.SPY, 100, 100m, 100.25m, Noon);
+ var config = CreateTradeBarConfig(Symbols.SPY, isInternal);
+ var security = GetSecurity(config);
+ security.SetLocalTimeKeeper(TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
+
+ security.SetMarketPrice(new TradeBar(
+ Noon, Symbols.SPY, 100.50m, 101.50m, 100.40m, 101.00m, 100));
+
+ var fill = model.Fill(new FillModelParameters(
+ security, order, new MockSubscriptionDataConfigProvider(config),
+ Time.OneHour, null)).Single();
+
+ Assert.AreEqual(OrderStatus.None, fill.Status);
+ Assert.IsTrue(order.StopTriggered);
+
+ security.SetMarketPrice(new TradeBar(
+ Noon.AddMinutes(1), Symbols.SPY, 99.80m, 100.80m, 99.50m, 100.60m, 100));
+
+ fill = model.Fill(new FillModelParameters(
+ security, order, new MockSubscriptionDataConfigProvider(config),
+ Time.OneHour, null)).Single();
+
+ Assert.AreEqual(OrderStatus.Filled, fill.Status);
+ Assert.AreEqual(99.80m, fill.FillPrice);
+ Assert.IsTrue(fill.Message.Contains("favorable gap", StringComparison.InvariantCultureIgnoreCase));
+ }
+
+ [TestCase(true)]
+ [TestCase(false)]
+ public void StopLimitBuyDoesNotUseLowOnTriggerBar(bool isInternal)
+ {
+ var model = new ImmediateFillModel();
+ var order = new StopLimitOrder(Symbols.SPY, 100, 100m, 100.25m, Noon);
+ var config = CreateTradeBarConfig(Symbols.SPY, isInternal);
+ var security = GetSecurity(config);
+ security.SetLocalTimeKeeper(TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
+
+ security.SetMarketPrice(new TradeBar(
+ Noon, Symbols.SPY, 100.50m, 101.50m, 99.80m, 101.00m, 100));
+
+ var fill = model.Fill(new FillModelParameters(
+ security, order, new MockSubscriptionDataConfigProvider(config),
+ Time.OneHour, null)).Single();
+
+ Assert.IsTrue(order.StopTriggered);
+ Assert.AreEqual(OrderStatus.None, fill.Status);
+ Assert.AreEqual(0, fill.FillQuantity);
+ }
+
+ [TestCase(true)]
+ [TestCase(false)]
+ public void StopLimitSellFillsOnLimitPenetrationAfterEarlierTrigger(bool isInternal)
+ {
+ var model = new ImmediateFillModel();
+ var order = new StopLimitOrder(Symbols.SPY, -100, 100m, 99.75m, Noon);
+ var config = CreateTradeBarConfig(Symbols.SPY, isInternal);
+ var security = GetSecurity(config);
+ security.SetLocalTimeKeeper(TimeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
+
+ security.SetMarketPrice(new TradeBar(
+ Noon, Symbols.SPY, 100.20m, 100.30m, 98.50m, 99.50m, 100));
+
+ var fill = model.Fill(new FillModelParameters(
+ security, order, new MockSubscriptionDataConfigProvider(config),
+ Time.OneHour, null)).Single();
+
+ Assert.AreEqual(OrderStatus.None, fill.Status);
+ Assert.IsTrue(order.StopTriggered);
+
+ security.SetMarketPrice(new TradeBar(
+ Noon.AddMinutes(1), Symbols.SPY, 99.60m, 100.10m, 99.40m, 99.60m, 100));
+
+ fill = model.Fill(new FillModelParameters(
+ security, order, new MockSubscriptionDataConfigProvider(config),
+ Time.OneHour, null)).Single();
+
+ Assert.AreEqual(OrderStatus.Filled, fill.Status);
+ Assert.AreEqual(order.LimitPrice, fill.FillPrice);
+ Assert.AreEqual(order.Quantity, fill.FillQuantity);
+ }
[TestCase(true)]
[TestCase(false)]
public void PerformsStopLimitFillBuy(bool isInternal)