Order Types
Limit if Touched Orders
Introduction
Limit if touched (LIT) orders are instructions to place a limit order once an asset touches a specific price level. A buy LIT order has a trigger price below the current market price and a sell LIT order has a trigger price above the current market price. In effect, LIT orders are the opposite of stop limit orders.
Place Orders
To send a LIT order, call the LimitIfTouchedOrderlimit_if_touched_order method and provide a Symbol, quantity, trigger price, and limit price. You can also provide a tag and order properties to the LimitIfTouchedOrderlimit_if_touched_order method. If you do not have sufficient capital for the order, it's rejected.
LimitIfTouchedOrder(symbol, quantity, triggerPrice, limitPrice, tag, orderProperties);
self.limit_if_touched_order(symbol, quantity, trigger_price, limit_price, tag, order_properties)
To buy an asset with a LIT order that has a marketable limit price, set the trigger price below the current market price and set the limit price above the trigger price.
// Once Bitcoin trades down to $36,000, place a limit order to buy 1 Bitcoin at $38,000
LimitIfTouchedOrder("BTCUSD", 1, 33000, 36000);
# Once Bitcoin trades down to $36,000, place a limit order to buy 1 Bitcoin at $38,000
self.limit_if_touched_order("BTCUSD", 1, 33000, 36000)
To sell an asset with a LIT order that has a marketable limit price, set the trigger price above the current market price and set the limit price below the trigger price.
// Once Bitcoin trades up to $62,000, place a limit order to sell 1 Bitcoin at $59,000
LimitIfTouchedOrder("BTCUSD", -1, 62000, 59000); # Once Bitcoin trades up to $62,000, place a limit order to sell 1 Bitcoin at $59,000
self.limit_if_touched_order("BTCUSD", -1, 62000, 59000)
To buy an asset with a LIT order that has an unmarketable limit price, set the trigger price below the current market price and set the limit price below the trigger price.
// Once Bitcoin trades down to $46,000, place a limit order to buy 1 Bitcoin at $36,000
LimitIfTouchedOrder("BTCUSD", 1, 46000, 36000); # Once Bitcoin trades down to $46,000, place a limit order to buy 1 Bitcoin at $36,000
self.limit_if_touched_order("BTCUSD", 1, 46000, 36000)
To sell an asset with a LIT order that has an unmarketable limit price, set the trigger price above the current market price and set the limit price above the trigger price.
// Once Bitcoin trades up to $54,000, place a limit order to sell 1 Bitcoin at $62,000
LimitIfTouchedOrder("BTCUSD", -1, 54000, 62000); # Once Bitcoin trades up to $54,000, place a limit order to sell 1 Bitcoin at $62,000
self.limit_if_touched_order("BTCUSD", -1, 54000, 62000)
Monitor Order Fills
LIT orders fill when the security price touches the trigger price and is at least as favorable as the limit price. To monitor the fills of your order, save a reference to the order ticket.
// Once SPY trades down to $425, place a limit order to buy 10 shares at $415
var ticket = LimitIfTouchedOrder("SPY", 10, 425, 415);
Debug($"Quantity filled: {ticket.QuantityFilled}; Fill price: {ticket.AverageFillPrice}"); # Once SPY trades down to $425, place a limit order to buy 10 shares at $415
ticket = self.limit_if_touched_order("SPY", 10, 425, 415)
self.debug(f"Quantity filled: {ticket.quantity_filled}; Fill price: {ticket.average_fill_price}")
For more information about how LEAN models order fills in backtests, see Trade Fills.
Update Orders
You can update the quantity, trigger price, limit price, and tag of LIT orders until the order fills or the brokerage prevents modifications. To update an order, pass an UpdateOrderFields object to the Updateupdate method on the OrderTicket. If you don't have the order ticket, get it from the transaction manager. The Updateupdate method returns an OrderResponse to signal the success or failure of the update request.
// Create a new order and save the order ticket
var ticket = LimitIfTouchedOrder("SPY", 100, 350, 340, tag: "Original tag");
// Update the order
var updateOrderFields = new UpdateOrderFields()
{
Quantity = 80,
TriggerPrice = 380,
LimitPrice = 370,
Tag = "New tag"
}
var response = ticket.Update(updateOrderFields);
// Check the OrderResponse
if (response.IsSuccess)
{
Debug("Order updated successfully");
} # Create a new order and save the order ticket
ticket = self.limit_if_touched_order("SPY", 100, 350, 340, tag="Original tag")
# Update the order
update_order_fields = UpdateOrderFields()
update_order_fields.quantity = 80
update_order_fields.trigger_price = 380
update_order_fields.limit_price = 370
update_order_fields.tag = "New tag"
response = ticket.update(update_settings)
# Check the OrderResponse
if response.is_success:
self.debug("Order updated successfully")
To update individual fields of an order, call any of the following methods:
UpdateLimitPriceupdate_limit_priceUpdateQuantityupdate_quantityUpdateTriggerPriceupdate_trigger_priceUpdateTagupdate_tag
var limitResponse = ticket.UpdateLimitPrice(limitPrice, tag); var quantityResponse = ticket.UpdateQuantity(quantity, tag); var triggerResponse = ticket.UpdateTriggerPrice(triggerPrice, tag); var tagResponse = ticket.UpdateTag(tag);
response = ticket.update_limit_price(limit_price, tag) response = ticket.update_quantity(quantity, tag) response = ticket.update_trigger_price(trigger_price, tag) response = ticket.update_tag(tag)
When you update an order, LEAN creates an UpdateOrderRequest object, which have the following attributes:
To get a list of UpdateOrderRequest objects for an order, call the UpdateRequestsupdate_requests method.
var updateRequests = ticket.UpdateRequests();
update_requests = ticket.update_requests()
Cancel Orders
To cancel a LIT order, call the Cancelcancel method on the OrderTicket. If you don't have the order ticket, get it from the transaction manager. The Cancelcancel method returns an OrderResponse object to signal the success or failure of the cancel request.
var response = ticket.Cancel("Cancelled trade");
if (response.IsSuccess)
{
Debug("Order successfully cancelled");
} response = ticket.cancel("Cancelled Trade")
if response.is_success:
self.debug("Order successfully cancelled")
When you cancel an order, LEAN creates a CancelOrderRequest, which have the following attributes:
To get the CancelOrderRequest for an order, call the CancelRequestcancel_order_request method on the order ticket. The method returns nullNone if the order hasn't been cancelled.
var request = ticket.cancel_order_request();
request = ticket.cancel_order_request()
Asynchronous Orders
When you trade a large portfolio of assets, you may want to send orders in batches and not wait for the response of each one. To send asynchronous orders, set the asynchronous argument to trueTrue.
LimitIfTouchedOrder("SPY", 10, 429.0m, 428.95m, asynchronous: true); self.limit_if_touched_order("SPY", 10, 429.0, 428.95, asynchronous=True);
Brokerage Support
Each brokerage has a set of assets and order types they support. To avoid issues with LIT orders, set the brokerage model to a brokerage that supports them.
SetBrokerageModel(BrokerageName.QuantConnectBrokerage);
self.set_brokerage_model(BrokerageName.QUANTCONNECT_BROKERAGE)
To check if your brokerage has any special requirements for LIT orders, see the Orders section of the brokerage model documentation.
Examples
The following backtest verifies the
LimitIfTouchedOrder
limit_if_touched_order
behavior. The following table shows the trades in the backtest:
| Submitted Time | Filled Time | Symbol | Trigger Price | Limit Price | Filled Price | Quantity | Type | Status | Value | Tag |
|---|---|---|---|---|---|---|---|---|---|---|
| 2021-07-01T09:31:00Z | 2021-07-01T09:38:00Z | SPY | 429.00 | 428.95 | 428.95 | 10 | Limit if touched | Filled | 4285.00 |
Trigger Price: ¤429.00
Limit Price: ¤428.95 |
| 2021-07-02T09:31:00Z | 2021-07-02T09:49:00Z | SPY | 431.70 | 431.75 | 431.84 | -10 | Limit if touched | Filled | -4318.40 |
Trigger Price: ¤431.70
Limit Price: ¤431.75 |
| 2021-07-02T09:31:00Z | / | SPY | 431.70 | 400.00 | / | 10 | Limit if touched | Submitted | / |
Trigger Price: ¤431.70
Limit Price: ¤400.00 |
| / | / | SPY | 400.00 | 400.00 | / | 10 | Limit if touched | Submitted | / |
Trigger Price: ¤400.00
Limit Price: ¤400.00 |
On July 1, 2021 at 9:31 AM Eastern Time (ET), the algorithm places a LIT order to buy SPY with a trigger price of $429 and a limit price of $428.95. At the time of the order submission, the low price was $428.80 and the bid close price was $429.10. The order fills at 9:38 AM ET at a price of $428.95. The fill model sets the limit price for buy orders when the low price is less than or equal to the trigger price, fills the order when the bid close price is less than or equal to the limit price, and sets the fill price to the minimum of the ask price and the limit price.
On July 2, 2021 at 9:31 AM ET, the algorithm places a LIT order to sell SPY with a trigger price of $431.70 and a limit price of $431.75. At the time of the order submission, the high price was $431.78 and the ask close price was $431.55. The order fills at 9:49 AM ET at a price of $431.84. The fill model sets the limit price for sell orders when the high price is greater than or equal to the trigger price, fills the order when the ask close price is greater than or equal to the limit price, and sets the fill price to the maximum of the bid price and the limit price.
On July 2, 2021, the algorithm places a third LIT order to buy SPY with a trigger price of $431.70 and a limit price of $400. The fill model sets the fill price, but it doesn't fill the order because the limit price is too low. On the same day, the algorithm places a fourth LIT order to buy SPY with a trigger price of $400 and a limit price of $400. The fill model doesn't set the limit price for this order because SPY doesn't touch the trigger price before the backtest ends.
To reproduce these results, backtest the following algorithm:
public class LimitOrderAlgorithm : QCAlgorithm
{
private Symbol _symbol;
public override void Initialize()
{
SetStartDate(2024, 9, 1);
SetEndDate(2024, 9, 4);
SetCash(100000);
UniverseSettings.DataNormalizationMode = DataNormalizationMode.Raw;
_symbol = AddEquity("SPY").Symbol;
}
public override void OnData(Slice data)
{
if (Time.Day == 1 && Time.Hour == 9 && Time.Minute == 31)
{
LimitIfTouchedOrder(_symbol, 10, 429.0m, 428.95m);
}
else if (Time.Day == 2 && Time.Hour == 9 && Time.Minute == 31)
{
LimitIfTouchedOrder(_symbol, -10, 431.7m, 431.75m);
LimitIfTouchedOrder(_symbol, 10, 431.7m, 400.0m);
LimitIfTouchedOrder(_symbol, 10, 400.0m, 400.0m);
}
}
} class LimitIfTouchedOrderAlgorithm(QCAlgorithm):
def initialize(self):
self.set_start_date(2024, 9, 1)
self.set_end_date(2024, 9, 4)
self.set_cash(100000)
self.universe_settings.data_normalization_mode = DataNormalizationMode.RAW
self.add_equity("SPY")
def on_data(self, data):
if self.time.day == 1 and self.time.hour == 9 and self.time.minute == 31:
self.limit_if_touched_order("SPY", 10, 429.0, 428.95)
elif self.time.day == 2 and self.time.hour == 9 and self.time.minute == 31:
self.limit_if_touched_order("SPY", -10, 431.7, 431.75)
self.limit_if_touched_order("SPY", 10, 431.7, 400)
self.limit_if_touched_order("SPY", 10, 400, 400)