Problem Description
Solution
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.
1. The High/Low Logic (Non-Tick Data)
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.
- Buy Limit Orders: The order fills if the Low price of the bar is less than or equal to your Limit Price.
- Logic:
Bar.Low <= LimitPrice
- Logic:
- Sell Limit Orders: The order fills if the High price of the bar is greater than or equal to your Limit Price.
- Logic:
Bar.High >= LimitPrice
- Logic:
2. The Fill Price (Limit vs. Market Gaps)
The price at which your order is executed depends on where the market opened relative to your limit price.
- Standard Fill: If the bar opens worse than your limit but trades through it (e.g., Buy Limit at $100, Bar opens at $101 and drops to $99), the order fills exactly at your Limit Price ($100).
- Gap Fill (Better Price): If the market gaps past your limit order (e.g., Buy Limit at $100, but the next bar opens at $95), the default model simulates a fill at the Open Price ($95). This reflects the reality that you would have received a better entry price at the market open.
3. Volume Constraints
The default EquityFillModel includes volume checks to ensure realistic execution:
- Volume Limit: You cannot buy/sell more shares in a single bar than the total traded volume of that bar.
- Ratio: By default, LEAN often allows you to take up to 100% of the bar's volume (unless a
PartialFillModelis applied), but the fill quantity will be capped atBar.Volume. If your order size exceeds the bar's volume, it may be partially filled, with the remainder carrying over to the next bar.
4. Tick Data Resolution
If you are running a backtest with Tick resolution:
- The engine compares your limit price against every individual trade tick.
- This provides the most accurate simulation of fill timing and pricing but significantly increases backtest runtime.
5. Stale Data
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.
Code Example: Setting Limit Orders
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})")
Summary of Fill Logic
| 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&A: QuantConnect Limit Order Fills
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.