Generate code in 3 minutes
Enter your idea and AI generates runnable code.
In QuantConnect's LEAN engine, limit orders during a backtest are processed using a Fill Model. Unless you specify a custom fill model, LEAN uses the default ImmediateFillModel (or asset-specific variations like EquityFillModel or ForexFillModel).
Here is the detailed logic of how limit orders are filled in the default environment.
For data resolutions of Minute, Hour, or Daily, the engine does not know the exact path the price took within the bar. Instead, it uses the High and Low prices of the bar to determine if the limit price was touched.
Bar.Low <= LimitPriceBar.High >= LimitPriceThe price at which your order is executed depends on where the market opened relative to your limit price.
The default EquityFillModel includes volume checks to ensure realistic execution:
PartialFillModel is applied), but the fill quantity will be capped at Bar.Volume. If your order size exceeds the bar's volume, it may be partially filled, with the remainder carrying over to the next bar.If you are running a backtest with Tick resolution:
Limit orders will not fill if there is no data flowing for that asset. If an asset is illiquid and no trades occur (no bars generated), the limit order remains pending until new data arrives.
Below is a simple algorithm demonstrating how to place limit orders. The logic relies on the default fill model described above.
from AlgorithmImports import *
class LimitOrderFillLogic(QCAlgorithm):
def initialize(self):
self.set_start_date(2023, 1, 1)
self.set_end_date(2023, 2, 1)
self.set_cash(100000)
# Add Equity with Minute resolution
self.spy = self.add_equity("SPY", Resolution.MINUTE).symbol
def on_data(self, data: Slice):
if not self.portfolio.invested and self.spy in data.bars:
bar = data.bars[self.spy]
# Example: Place a Buy Limit order 1% below the current close
limit_price = bar.close * 0.99
self.limit_order(self.spy, 100, limit_price)
self.log(f"Placed Limit Buy for 100 SPY at {limit_price}")
def on_order_event(self, order_event: OrderEvent):
if order_event.status == OrderStatus.FILLED:
# Log the actual fill price to compare with the requested limit price
self.log(f"Order Filled: {order_event.symbol} at {order_event.fill_price} "
f"(Quantity: {order_event.fill_quantity})")
| Order Type | Condition to Fill (Default Model) | Execution Price |
|---|---|---|
| Buy Limit | Bar.Low <= Limit Price |
min(Open Price, Limit Price) |
| Sell Limit | Bar.High >= Limit Price |
max(Open Price, Limit Price) |
Q: Why did my limit order fill at a better price than I requested?
A: This occurs when the market "gaps" past your limit price. For example, if you have a Buy Limit at $100, but the market opens the next minute at $98, the engine fills you at $98 (the Open price), assuming you would have been executed immediately upon the market opening at that favorable price.
Q: Can I change how limit orders are filled?
A: Yes. You can implement a custom Fill Model by creating a class that inherits from FillModel and overriding the MarketFill, LimitFill, and StopMarketFill methods. You then assign this model to your security using self.securities["SPY"].set_fill_model(MyCustomFillModel()).
Q: Does the fill model account for bid/ask spread?
A: In Resolution.MINUTE or DAILY (Trade Bars), the default model uses trade prices (Open/High/Low/Close). It does not explicitly model the spread unless you are using Resolution.TICK or SECOND with Quote Bars, or if you use a custom slippage model. However, since Limit orders are passive, spread cost is usually less of a concern than with Market orders.
Q: Why didn't my limit order fill even though the price touched my limit?
A: This can happen if the volume of the bar was zero or insufficient to cover your order size (if using a strict volume fill model), or if the data feed contains "bad ticks" that were filtered out by LEAN's data sanitization filters. Additionally, ensure you are looking at the correct data type (Trade vs Quote) for the asset class.