diff --git a/Algorithm.CSharp/PeggedToMidpointRegressionAlgorithm.cs b/Algorithm.CSharp/PeggedToMidpointRegressionAlgorithm.cs
new file mode 100644
index 000000000000..919294d53b9f
--- /dev/null
+++ b/Algorithm.CSharp/PeggedToMidpointRegressionAlgorithm.cs
@@ -0,0 +1,145 @@
+/*
+ * 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.Collections.Generic;
+using QuantConnect.Data;
+using QuantConnect.Interfaces;
+using QuantConnect.Orders;
+
+namespace QuantConnect.Algorithm.CSharp
+{
+ ///
+ /// Basic algorithm demonstrating how to place Pegged-to-Midpoint orders.
+ ///
+ ///
+ ///
+ ///
+ public class PeggedToMidpointRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
+ {
+ private Symbol _symbol;
+ private OrderTicket _buyOrderTicket;
+ private OrderTicket _sellOrderTicket;
+ protected virtual bool AsynchronousOrders => false;
+
+ public override void Initialize()
+ {
+ SetStartDate(2013, 10, 07);
+ SetEndDate(2013, 10, 11);
+ SetCash(100000);
+ _symbol = AddEquity("SPY").Symbol;
+ }
+
+ public override void OnData(Slice slice)
+ {
+ if (!slice.ContainsKey(_symbol))
+ {
+ return;
+ }
+
+ if (_buyOrderTicket == null)
+ {
+ _buyOrderTicket = PeggedToMidpointOrder(_symbol, 1, limitPrice: 0m, limitPriceOffset: 0m,
+ asynchronous: AsynchronousOrders);
+ }
+ else if (_sellOrderTicket == null && Portfolio.Invested)
+ {
+ _sellOrderTicket = PeggedToMidpointOrder(_symbol, -1, limitPrice: 0m, limitPriceOffset: 0m,
+ asynchronous: AsynchronousOrders);
+ }
+ }
+
+ public override void OnOrderEvent(OrderEvent orderEvent)
+ {
+ if (orderEvent.Status != OrderStatus.Filled)
+ {
+ return;
+ }
+
+ var order = Transactions.GetOrderById(orderEvent.OrderId);
+ if (order is not Orders.PeggedToMidpointOrder)
+ {
+ throw new RegressionTestException($"Expected PeggedToMidpointOrder but got {order.GetType().Name}");
+ }
+ }
+
+ public override void OnEndOfAlgorithm()
+ {
+ if (_sellOrderTicket?.Status != OrderStatus.Filled)
+ {
+ throw new RegressionTestException("Expected sell PeggedToMidpoint order to be filled by end of algorithm.");
+ }
+ }
+
+ ///
+ /// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm.
+ ///
+ public bool CanRunLocally => true;
+
+ ///
+ /// This is used by the regression test system to indicate which languages this algorithm is written in.
+ ///
+ public List Languages { get; } = new() { Language.CSharp, Language.Python };
+
+ ///
+ /// Data Points count of all timeslices of algorithm
+ ///
+ public long DataPoints => 3943;
+
+ ///
+ /// Data Points count of the algorithm history
+ ///
+ public int AlgorithmHistoryDataPoints => 0;
+
+ ///
+ /// Final status of the algorithm
+ ///
+ public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed;
+
+ ///
+ /// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
+ ///
+ public Dictionary ExpectedStatistics => new Dictionary
+ {
+ {"Total Orders", "2"},
+ {"Average Win", "0%"},
+ {"Average Loss", "0.00%"},
+ {"Compounding Annual Return", "-0.198%"},
+ {"Drawdown", "0.000%"},
+ {"Expectancy", "-1"},
+ {"Start Equity", "100000"},
+ {"End Equity", "99997.46"},
+ {"Net Profit", "-0.003%"},
+ {"Sharpe Ratio", "-108.977"},
+ {"Sortino Ratio", "0"},
+ {"Probabilistic Sharpe Ratio", "1.216%"},
+ {"Loss Rate", "100%"},
+ {"Win Rate", "0%"},
+ {"Profit-Loss Ratio", "0"},
+ {"Alpha", "-0.009"},
+ {"Beta", "0"},
+ {"Annual Standard Deviation", "0"},
+ {"Annual Variance", "0"},
+ {"Information Ratio", "-8.915"},
+ {"Tracking Error", "0.222"},
+ {"Treynor Ratio", "-32.005"},
+ {"Total Fees", "$2.00"},
+ {"Estimated Strategy Capacity", "$17000000.00"},
+ {"Lowest Capacity Asset", "SPY R735QTJ8XC9X"},
+ {"Portfolio Turnover", "0.06%"},
+ {"Drawdown Recovery", "0"},
+ {"OrderListHash", "d310603dec0f507a6b9b8a9eb21c4717"}
+ };
+ }
+}
diff --git a/Algorithm.Python/PeggedToMidpointRegressionAlgorithm.py b/Algorithm.Python/PeggedToMidpointRegressionAlgorithm.py
new file mode 100644
index 000000000000..0ca5df963c54
--- /dev/null
+++ b/Algorithm.Python/PeggedToMidpointRegressionAlgorithm.py
@@ -0,0 +1,52 @@
+# 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.
+
+from AlgorithmImports import *
+
+###
+### Basic algorithm demonstrating how to place Pegged-to-Midpoint orders.
+###
+###
+###
+###
+class PeggedToMidpointRegressionAlgorithm(QCAlgorithm):
+
+ _buy_order_ticket = None
+ _sell_order_ticket = None
+
+ def initialize(self):
+ self.set_start_date(2013, 10, 7)
+ self.set_end_date(2013, 10, 11)
+ self.set_cash(100000)
+ self._symbol = self.add_equity("SPY").symbol
+
+ def on_data(self, data):
+ if not data.contains_key(self._symbol):
+ return
+
+ if self._buy_order_ticket is None:
+ self._buy_order_ticket = self.pegged_to_midpoint_order(self._symbol, 1, limit_price=0, limit_price_offset=0)
+ elif self._sell_order_ticket is None and self.portfolio.invested:
+ self._sell_order_ticket = self.pegged_to_midpoint_order(self._symbol, -1, limit_price=0, limit_price_offset=0)
+
+ def on_order_event(self, order_event):
+ if order_event.status != OrderStatus.FILLED:
+ return
+
+ order = self.transactions.get_order_by_id(order_event.order_id)
+ if not isinstance(order, PeggedToMidpointOrder):
+ raise AssertionError(f"Expected PeggedToMidpointOrder but got {type(order).__name__}")
+
+ def on_end_of_algorithm(self):
+ if self._sell_order_ticket is None or self._sell_order_ticket.status != OrderStatus.FILLED:
+ raise AssertionError("Expected sell PeggedToMidpoint order to be filled by end of algorithm.")
diff --git a/Algorithm/QCAlgorithm.Trading.cs b/Algorithm/QCAlgorithm.Trading.cs
index 2956504f09b9..9b257dbfd25d 100644
--- a/Algorithm/QCAlgorithm.Trading.cs
+++ b/Algorithm/QCAlgorithm.Trading.cs
@@ -723,6 +723,64 @@ public OrderTicket LimitIfTouchedOrder(Symbol symbol, decimal quantity, decimal
return SubmitOrderRequest(request);
}
+ ///
+ /// Send a pegged-to-midpoint order to the transaction handler:
+ ///
+ /// Symbol for the asset
+ /// Quantity of shares for the order
+ /// Optional limit price cap (buy) or floor (sell)
+ /// Offset from the midpoint
+ /// Send the order asynchronously (false). Otherwise we'll block until it is fully submitted
+ /// String tag for the order (optional)
+ /// The order properties to use. Defaults to
+ /// The order ticket instance.
+ public OrderTicket PeggedToMidpointOrder(Symbol symbol, int quantity, decimal limitPrice, decimal limitPriceOffset,
+ bool asynchronous = false, string tag = "", IOrderProperties orderProperties = null)
+ {
+ return PeggedToMidpointOrder(symbol, (decimal)quantity, limitPrice, limitPriceOffset, asynchronous, tag, orderProperties);
+ }
+
+ ///
+ /// Send a pegged-to-midpoint order to the transaction handler:
+ ///
+ /// Symbol for the asset
+ /// Quantity of shares for the order
+ /// Optional limit price cap (buy) or floor (sell)
+ /// Offset from the midpoint
+ /// Send the order asynchronously (false). Otherwise we'll block until it is fully submitted
+ /// String tag for the order (optional)
+ /// The order properties to use. Defaults to
+ /// The order ticket instance.
+ public OrderTicket PeggedToMidpointOrder(Symbol symbol, double quantity, decimal limitPrice, decimal limitPriceOffset,
+ bool asynchronous = false, string tag = "", IOrderProperties orderProperties = null)
+ {
+ return PeggedToMidpointOrder(symbol, quantity.SafeDecimalCast(), limitPrice, limitPriceOffset, asynchronous, tag, orderProperties);
+ }
+
+ ///
+ /// Send a pegged-to-midpoint order to the transaction handler:
+ ///
+ /// Symbol for the asset
+ /// Quantity of shares for the order
+ /// Optional limit price cap (buy) or floor (sell)
+ /// Offset from the midpoint
+ /// Send the order asynchronously (false). Otherwise we'll block until it is fully submitted
+ /// String tag for the order (optional)
+ /// The order properties to use. Defaults to
+ /// The order ticket instance.
+ [DocumentationAttribute(TradingAndOrders)]
+ public OrderTicket PeggedToMidpointOrder(Symbol symbol, decimal quantity, decimal limitPrice, decimal limitPriceOffset,
+ bool asynchronous = false, string tag = "", IOrderProperties orderProperties = null)
+ {
+ var security = Securities[symbol];
+ var request = CreateSubmitOrderRequest(OrderType.PeggedToMidpoint, security, quantity, tag,
+ limitPrice: limitPrice, triggerPrice: limitPriceOffset,
+ properties: orderProperties ?? DefaultOrderProperties?.Clone(),
+ asynchronous: asynchronous);
+
+ return SubmitOrderRequest(request);
+ }
+
///
/// Send an exercise order to the transaction handler
///
diff --git a/Common/Brokerages/InteractiveBrokersBrokerageModel.cs b/Common/Brokerages/InteractiveBrokersBrokerageModel.cs
index 80f193f6eafb..c2a5939fe177 100644
--- a/Common/Brokerages/InteractiveBrokersBrokerageModel.cs
+++ b/Common/Brokerages/InteractiveBrokersBrokerageModel.cs
@@ -83,6 +83,7 @@ public class InteractiveBrokersBrokerageModel : DefaultBrokerageModel
OrderType.StopLimit,
OrderType.TrailingStop,
OrderType.LimitIfTouched,
+ OrderType.PeggedToMidpoint,
OrderType.ComboMarket,
OrderType.ComboLimit,
OrderType.ComboLegLimit,
diff --git a/Common/Orders/Fills/FillModel.cs b/Common/Orders/Fills/FillModel.cs
index 2ea61602b6b0..1d16feb9f038 100644
--- a/Common/Orders/Fills/FillModel.cs
+++ b/Common/Orders/Fills/FillModel.cs
@@ -118,6 +118,11 @@ public virtual Fill Fill(FillModelParameters parameters)
? PythonWrapper.ComboLegLimitFill(parameters.Order, parameters)
: ComboLegLimitFill(parameters.Order, parameters);
break;
+ case OrderType.PeggedToMidpoint:
+ orderEvents.Add(PythonWrapper != null
+ ? PythonWrapper.PeggedToMidpointFill(parameters.Security, parameters.Order as PeggedToMidpointOrder)
+ : PeggedToMidpointFill(parameters.Security, parameters.Order as PeggedToMidpointOrder));
+ break;
default:
throw new ArgumentOutOfRangeException();
}
@@ -644,6 +649,59 @@ public virtual OrderEvent LimitIfTouchedFill(Security asset, LimitIfTouchedOrder
return fill;
}
+ ///
+ /// Pegged to midpoint fill model. Fills at the NBBO midpoint adjusted by .
+ /// If is set it acts as a cap for buy orders and a floor for sell orders;
+ /// the order will not fill if the effective midpoint price would exceed the cap (buy) or fall below the floor (sell).
+ ///
+ /// Security asset we're filling
+ /// Order packet to model
+ /// Order fill information detailing the average price and quantity filled.
+ public virtual OrderEvent PeggedToMidpointFill(Security asset, PeggedToMidpointOrder order)
+ {
+ var utcTime = asset.LocalTime.ConvertToUtc(asset.Exchange.TimeZone);
+ var fill = new OrderEvent(order, utcTime, OrderFee.Zero);
+
+ if (order.Status == OrderStatus.Canceled) return fill;
+
+ if (!IsExchangeOpen(asset)) return fill;
+
+ var askPrice = GetAskPrice(asset, out var askEndTime);
+ var bidPrice = GetBidPrice(asset, out var bidEndTime);
+ var pricesEndTime = askEndTime > bidEndTime ? askEndTime : bidEndTime;
+
+ // Do not fill on stale data
+ if (pricesEndTime <= order.Time) return fill;
+
+ var midpoint = (bidPrice + askPrice) / 2m;
+
+ switch (order.Direction)
+ {
+ case OrderDirection.Buy:
+ {
+ var fillPrice = midpoint + order.LimitPriceOffset;
+ // LimitPrice acts as a cap: skip fill if effective price exceeds it
+ if (order.LimitPrice > 0 && fillPrice > order.LimitPrice) break;
+ fill.Status = OrderStatus.Filled;
+ fill.FillPrice = fillPrice;
+ fill.FillQuantity = order.Quantity;
+ break;
+ }
+ case OrderDirection.Sell:
+ {
+ var fillPrice = midpoint - order.LimitPriceOffset;
+ // LimitPrice acts as a floor: skip fill if effective price falls below it
+ if (order.LimitPrice > 0 && fillPrice < order.LimitPrice) break;
+ fill.Status = OrderStatus.Filled;
+ fill.FillPrice = fillPrice;
+ fill.FillQuantity = order.Quantity;
+ break;
+ }
+ }
+
+ return fill;
+ }
+
///
/// Default limit order fill model in the base security class.
///
diff --git a/Common/Orders/Order.cs b/Common/Orders/Order.cs
index dfddcba0655c..87d41ed6ec4c 100644
--- a/Common/Orders/Order.cs
+++ b/Common/Orders/Order.cs
@@ -472,6 +472,10 @@ private static Order CreateOrder(int orderId, OrderType type, Symbol symbol, dec
order = new ComboMarketOrder(symbol, quantity, time, groupOrderManager, tag, properties);
break;
+ case OrderType.PeggedToMidpoint:
+ order = new PeggedToMidpointOrder(symbol, quantity, limitPrice, triggerPrice, time, tag, properties);
+ break;
+
default:
throw new ArgumentOutOfRangeException();
}
diff --git a/Common/Orders/OrderJsonConverter.cs b/Common/Orders/OrderJsonConverter.cs
index 96b64eb2a5ad..e43fa22beed3 100644
--- a/Common/Orders/OrderJsonConverter.cs
+++ b/Common/Orders/OrderJsonConverter.cs
@@ -319,6 +319,14 @@ private static Order CreateOrder(OrderType orderType, JObject jObject)
};
break;
+ case OrderType.PeggedToMidpoint:
+ order = new PeggedToMidpointOrder
+ {
+ LimitPrice = jObject["limitPrice"]?.Value() ?? jObject["LimitPrice"]?.Value() ?? default(decimal),
+ LimitPriceOffset = jObject["limitPriceOffset"]?.Value() ?? jObject["LimitPriceOffset"]?.Value() ?? default(decimal)
+ };
+ break;
+
default:
throw new ArgumentOutOfRangeException();
}
diff --git a/Common/Orders/OrderTypes.cs b/Common/Orders/OrderTypes.cs
index d37be01542ef..80c53065d07f 100644
--- a/Common/Orders/OrderTypes.cs
+++ b/Common/Orders/OrderTypes.cs
@@ -78,7 +78,12 @@ public enum OrderType
///
/// Trailing Stop Order Type - (11)
///
- TrailingStop
+ TrailingStop,
+
+ ///
+ /// Pegged to Midpoint Order Type - (12)
+ ///
+ PeggedToMidpoint
}
///
diff --git a/Common/Orders/PeggedToMidpointOrder.cs b/Common/Orders/PeggedToMidpointOrder.cs
new file mode 100644
index 000000000000..36e18ec33718
--- /dev/null
+++ b/Common/Orders/PeggedToMidpointOrder.cs
@@ -0,0 +1,105 @@
+/*
+ * 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 Newtonsoft.Json;
+using QuantConnect.Interfaces;
+using QuantConnect.Securities;
+
+namespace QuantConnect.Orders
+{
+ ///
+ /// Pegged to midpoint order type definition
+ ///
+ public class PeggedToMidpointOrder : Order
+ {
+ ///
+ /// Limit price for this order.
+ ///
+ [JsonProperty(PropertyName = "limitPrice")]
+ public decimal LimitPrice { get; internal set; }
+
+ ///
+ /// Offset for this order.
+ ///
+ [JsonProperty(PropertyName = "limitPriceOffset")]
+ public decimal LimitPriceOffset { get; internal set; }
+
+ ///
+ /// Pegged To Midpoint Order Type
+ ///
+ public override OrderType Type
+ {
+ get { return OrderType.PeggedToMidpoint; }
+ }
+
+ ///
+ /// Added a default constructor for JSON Deserialization:
+ ///
+ public PeggedToMidpointOrder()
+ {
+ }
+
+ ///
+ /// New Pegged To Midpoint order constructor
+ ///
+ /// Symbol asset we're seeking to trade
+ /// Quantity of the asset we're seeking to trade
+ /// Time the order was placed
+ /// Price the order should be filled at if a limit order
+ /// Offset to the midpoint
+ /// User defined data tag for this order
+ /// The order properties for this order
+ public PeggedToMidpointOrder(Symbol symbol, decimal quantity, decimal limitPrice, decimal limitPriceOffset, DateTime time, string tag = "", IOrderProperties properties = null)
+ : base(symbol, quantity, time, tag, properties)
+ {
+ LimitPrice = limitPrice;
+ LimitPriceOffset = limitPriceOffset;
+ }
+
+ ///
+ /// Gets the order value in units of the security's quote currency
+ ///
+ /// The security matching this order's symbol
+ protected override decimal GetValueImpl(Security security)
+ {
+ return LimitOrder.CalculateOrderValue(Quantity, LimitPrice, security.Price);
+ }
+
+ ///
+ /// Modifies the state of this order to match the update request
+ ///
+ /// The request to update this order object
+ public override void ApplyUpdateOrderRequest(UpdateOrderRequest request)
+ {
+ base.ApplyUpdateOrderRequest(request);
+ if (request.LimitPrice.HasValue)
+ {
+ LimitPrice = request.LimitPrice.Value;
+ }
+ }
+
+ ///
+ /// Creates a deep-copy clone of this order
+ ///
+ /// A copy of this order
+ public override Order Clone()
+ {
+ var order = new PeggedToMidpointOrder { LimitPrice = LimitPrice, LimitPriceOffset = LimitPriceOffset };
+ CopyTo(order);
+ return order;
+ }
+ }
+}
diff --git a/Common/Python/FillModelPythonWrapper.cs b/Common/Python/FillModelPythonWrapper.cs
index 0c4ba6c5b08c..0ed3fe1e8b2d 100644
--- a/Common/Python/FillModelPythonWrapper.cs
+++ b/Common/Python/FillModelPythonWrapper.cs
@@ -140,6 +140,17 @@ public override OrderEvent TrailingStopFill(Security asset, TrailingStopOrder or
return _model.InvokeMethod(nameof(TrailingStopFill), asset, order);
}
+ ///
+ /// Pegged to Midpoint Fill Model. Return an order event with the fill details.
+ ///
+ /// Asset we're trading this order
+ /// Pegged to Midpoint Order to fill
+ /// Order fill information detailing the average price and quantity filled.
+ public override OrderEvent PeggedToMidpointFill(Security asset, PeggedToMidpointOrder order)
+ {
+ return _model.InvokeMethod(nameof(PeggedToMidpointFill), asset, order);
+ }
+
///
/// Default combo market fill model for the base security class. Fills at the last traded price for each leg.
///
diff --git a/Tests/Brokerages/PeggedToMidpointOrderTestParameters.cs b/Tests/Brokerages/PeggedToMidpointOrderTestParameters.cs
new file mode 100644
index 000000000000..dee5d2659a72
--- /dev/null
+++ b/Tests/Brokerages/PeggedToMidpointOrderTestParameters.cs
@@ -0,0 +1,87 @@
+/*
+ * 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 QuantConnect.Interfaces;
+using QuantConnect.Orders;
+
+namespace QuantConnect.Tests.Brokerages
+{
+ public class PeggedToMidpointOrderTestParameters : OrderTestParameters
+ {
+ private readonly decimal _limitPrice;
+ private readonly decimal _limitPriceOffset;
+
+ public PeggedToMidpointOrderTestParameters(
+ Symbol symbol,
+ decimal limitPrice,
+ decimal limitPriceOffset = 0m,
+ IOrderProperties properties = null,
+ OrderSubmissionData orderSubmissionData = null)
+ : base(symbol, properties, orderSubmissionData)
+ {
+ _limitPrice = limitPrice;
+ _limitPriceOffset = limitPriceOffset;
+ }
+
+ public override Order CreateShortOrder(decimal quantity)
+ {
+ return new PeggedToMidpointOrder(Symbol, -Math.Abs(quantity), _limitPrice, _limitPriceOffset, DateTime.UtcNow,
+ properties: Properties)
+ {
+ Status = OrderStatus.New,
+ OrderSubmissionData = OrderSubmissionData,
+ PriceCurrency = GetSymbolProperties(Symbol).QuoteCurrency
+ };
+ }
+
+ public override Order CreateLongOrder(decimal quantity)
+ {
+ return new PeggedToMidpointOrder(Symbol, Math.Abs(quantity), _limitPrice, _limitPriceOffset, DateTime.UtcNow,
+ properties: Properties)
+ {
+ Status = OrderStatus.New,
+ OrderSubmissionData = OrderSubmissionData,
+ PriceCurrency = GetSymbolProperties(Symbol).QuoteCurrency
+ };
+ }
+
+ public override bool ModifyOrderToFill(IBrokerage brokerage, Order order, decimal lastMarketPrice)
+ {
+ var peg = (PeggedToMidpointOrder)order;
+ var previous = peg.LimitPrice;
+ // Adjust the limit price cap/floor toward market price to allow the peg to fill
+ if (order.Quantity > 0)
+ {
+ peg.LimitPrice = Math.Max(peg.LimitPrice, lastMarketPrice);
+ }
+ else
+ {
+ peg.LimitPrice = Math.Min(peg.LimitPrice, lastMarketPrice);
+ }
+ return peg.LimitPrice != previous;
+ }
+
+ // PEG MID orders are submitted to the exchange and won't fill immediately in test
+ public override OrderStatus ExpectedStatus => OrderStatus.Submitted;
+
+ public override bool ExpectedCancellationResult => true;
+
+ public override string ToString()
+ {
+ return $"{OrderType.PeggedToMidpoint}: {SecurityType}, {Symbol}";
+ }
+ }
+}
diff --git a/Tests/Common/Brokerages/InteractiveBrokersBrokerageModelTests.cs b/Tests/Common/Brokerages/InteractiveBrokersBrokerageModelTests.cs
index fecf694da5d8..5e03c55d4165 100644
--- a/Tests/Common/Brokerages/InteractiveBrokersBrokerageModelTests.cs
+++ b/Tests/Common/Brokerages/InteractiveBrokersBrokerageModelTests.cs
@@ -229,6 +229,19 @@ public void CanSubmitCfdOrder()
Assert.IsTrue(canSubmit);
}
+ [TestCase("SPY", SecurityType.Equity)]
+ [TestCase("SPY", SecurityType.Option)]
+ public void CanSubmitPeggedToMidpointOrder(string ticker, SecurityType securityType)
+ {
+ var algo = new AlgorithmStub();
+ var security = algo.AddSecurity(securityType, ticker);
+ var order = new PeggedToMidpointOrder(security.Symbol, 1, 100m, 0.01m, new DateTime(2024, 1, 1));
+
+ var result = _interactiveBrokersBrokerageModel.CanSubmitOrder(security, order, out var message);
+
+ Assert.IsTrue(result);
+ }
+
private static List GetUnsupportedOptions()
{
// Index option
diff --git a/Tests/Common/Orders/OrderTests.cs b/Tests/Common/Orders/OrderTests.cs
index 4d5d65d81079..5ed657acd384 100644
--- a/Tests/Common/Orders/OrderTests.cs
+++ b/Tests/Common/Orders/OrderTests.cs
@@ -80,6 +80,15 @@ public void TrailingStopOrder_UpdatesStopPriceIfNecessary(OrderDirection directi
}
}
+ [Test]
+ public void PeggedToMidpointOrder_ClonesCorrectly()
+ {
+ var order = new PeggedToMidpointOrder(Symbols.SPY, 100, 10, 2, DateTime.Now);
+ var clone = (PeggedToMidpointOrder)order.Clone();
+ Assert.AreEqual(order.LimitPrice, clone.LimitPrice);
+ Assert.AreEqual(order.LimitPriceOffset, clone.LimitPriceOffset);
+ }
+
private static TestCaseData[] GetValueTestParameters()
{
const decimal delta = 1m;
@@ -165,6 +174,8 @@ private static TestCaseData[] GetValueTestParameters()
new ValueTestParameters("EquityShortTrailingStopOrderPriceMinusDelta", equity, new TrailingStopOrder(Symbols.SPY, -quantity, priceMinusDelta, 0.1m, true, time), -quantity*price),
new ValueTestParameters("EquityLongLimitIfTouchedOrder", equity, new LimitIfTouchedOrder(Symbols.SPY, quantity, 1.5m*pricePlusDelta, priceMinusDelta, time), quantity*priceMinusDelta),
new ValueTestParameters("EquityShortLimitIfTouchedOrder", equity, new LimitIfTouchedOrder(Symbols.SPY, -quantity, .5m*priceMinusDelta, pricePlusDelta, time), -quantity*pricePlusDelta),
+ new ValueTestParameters("EquityLongPeggedToMidpointOrder", equity, new PeggedToMidpointOrder(Symbols.SPY, quantity, priceMinusDelta, 0, time), quantity*priceMinusDelta),
+ new ValueTestParameters("EquityShortPeggedToMidpointOrder", equity, new PeggedToMidpointOrder(Symbols.SPY, -quantity, pricePlusDelta, 0, time), -quantity*pricePlusDelta),
// forex orders
new ValueTestParameters("ForexLongMarketOrder", forex, new MarketOrder(Symbols.EURGBP, quantity, time), quantity*price*forex.QuoteCurrency.ConversionRate),
diff --git a/Tests/Engine/BrokerageTransactionHandlerTests/BrokerageTransactionHandlerTests.cs b/Tests/Engine/BrokerageTransactionHandlerTests/BrokerageTransactionHandlerTests.cs
index ffb9fc557da2..d664bccbcf1c 100644
--- a/Tests/Engine/BrokerageTransactionHandlerTests/BrokerageTransactionHandlerTests.cs
+++ b/Tests/Engine/BrokerageTransactionHandlerTests/BrokerageTransactionHandlerTests.cs
@@ -89,6 +89,7 @@ private static SubmitOrderRequest MakeOrderRequest(Security security, OrderType
OrderType.ComboLimit => new SubmitOrderRequest(OrderType.ComboLimit, security.Type, security.Symbol, 1, 295, 0, date, "", groupOrderManager: groupOrderManager),
OrderType.ComboLegLimit => new SubmitOrderRequest(OrderType.ComboLegLimit, security.Type, security.Symbol, 1, 295, 0, date, "", groupOrderManager: groupOrderManager),
OrderType.TrailingStop => new SubmitOrderRequest(OrderType.TrailingStop, security.Type, security.Symbol, 1, 305, 0, 305, date, ""),
+ OrderType.PeggedToMidpoint => new SubmitOrderRequest(OrderType.PeggedToMidpoint, security.Type, security.Symbol, 1, 0, 300, 0, date, ""),
_ => throw new ArgumentOutOfRangeException(nameof(orderType), orderType, null)
};
}