QuantConnect / Lean

Lean Algorithmic Trading Engine by QuantConnect (Python, C#)
https://lean.io
Apache License 2.0
8.94k stars 3.13k forks source link

Refactor Fill Models #4567

Closed AlexCatarino closed 1 year ago

AlexCatarino commented 3 years ago

Expected Behavior

Stop Market, Limit, and Stop Limit Orders should be triggered by trade data.

Actual Behavior

Stop Market, Limit, and Stop Limit Orders should be triggered by quote data.

Potential Solution

FillModel.GetPrices method should be smart and return the relevant price(s). For the referenced orders types, the trigger price should be the High and Low of the last TradeBar. Since the time of the High and Low and unknown, perhaps we should consider filling with the close price.

Reproducing the Problem

N/A

Checklist

adam-may commented 3 years ago

Hi @AlexCatarino , I looked into this in some depth a while back. A good deal of the markets I've looked at will trigger the stops when the price is touched by the quote (i.e. it doesn't have to trade at that price for the stop to be triggered).

You also mention Limit orders in the description, but I think that's a mistake as they don't have anything to do with quotes?

AlexCatarino commented 3 years ago

Here is the behavior as described by Interactive Brokers for Equity:

Limit Orders:

Step 1 – Enter a Limit Buy Order XYZ stock has a current Ask price of 34.00 and you want to use a Limit order to buy 100 shares when the market price falls to 33.50. You create the Limit order as shown above. Step 2 – Order Transmitted, Market Price Begins to Fall As soon as you transmit your Limit order, the market price of XYZ stock begins to fall. Step 3 – Market Price Falls to Limit Price, Order Filled The market price of XYZ continues to fall until it touches your Limit Price of 33.50. Your order for 100 shares is filled. where the market price is the market offer.

Stop Market

A Stop order is an instruction to submit a buy or sell market order if and when the user-specified stop trigger price is attained or penetrated. A Stop order is not guaranteed a specific execution price and may execute significantly away from its stop price. A Sell Stop order is always placed below the current market price and is typically used to limit a loss or protect a profit on a long stock position. A Buy Stop order is always placed above the current market price. It is typically used to limit a loss or help protect a profit on a short sale.

Stop-Limit Orders

A Stop-Limit order is an instruction to submit a buy or sell limit order when the user-specified stop trigger price is attained or penetrated. The order has two basic components: the stop price and the limit price. When a trade has occurred at or through the stop price, the order becomes executable and enters the market as a limit order, which is an order to buy or sell at a specified price or better.

We can conclude that:

gsalaz98 commented 3 years ago

On gap-down of data, the default fill model does not take into account an existing limit order, and can potentially execute the limit order at a price better at or worse than the expected price. This can cause issues when backtesting with Tick data, as well as whenever limit orders are left open past market close.

I've attached an example unit test demonstrating this issue. Also, I contacted IBKR about their Smart routing algorithm to confirm my suspicions that the current fill model was made with Smart routing in mind.

IBKR response:

Dear Mr. Salazar,

SMART Routing is designed for products, such as equities, that are listed on multiple exchanges. Futures products, are listed on a single exchange, therefore, SMART Routing is not possible. If you see an option to SMART route, for futures, make sure to select the Direct Route option.

Regards
Wayne B

IBKR Client Services

Unit test (failing):

[Test]
public void GapDownBarLimitOrderFillsAtLimitPrice()
{
    var model = new ImmediateFillModel();
    var order = new LimitOrder(Symbols.SPY, 10, 100m, Noon);
    var config = CreateQuoteBarConfig(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 QuoteBar
    {
        Bid = new Bar
        {
            Open = 95m,
            High = 95m,
            Low = 95m,
            Close = 95m
        },
        Ask = new Bar
        {
            Open = 102m,
            High = 102m,
            Low = 102m,
            Close = 102m
        },
        LastBidSize = 10m,
        LastAskSize = 10m,
        Symbol = Symbols.SPY,
        EndTime = Noon
    });
    var fill = model.Fill(new FillModelParameters(
        security,
        order,
        new MockSubscriptionDataConfigProvider(config),
        Time.OneHour)).OrderEvent;
    Assert.AreEqual(0m, fill.Quantity);
    Assert.AreEqual(OrderStatus.None, fill.Status);
    Assert.AreEqual(0m, fill.FillPrice);
    security.SetMarketPrice(new QuoteBar
    {
        Bid = new Bar
        {
            Open = 95m,
            High = 95m,
            Low = 95m,
            Close = 95m
        },
        Ask = new Bar
        {
            Open = 98m,
            High = 98m,
            Low = 98m,
            Close = 98m
        },
        LastBidSize = 10m,
        LastAskSize = 10m,
        Symbol = Symbols.SPY,
        EndTime = Noon + TimeSpan.FromDays(1)
    });
    fill = model.LimitFill(security, order);
    // Fills on bars with a gap between two bars will be filled at either an optimistic or a pessimistic price,
    // rather than the limit order submitted to the markets.
    //
    // In the case of submitting a buy Limit Order for SPY @ $100 USD:
    // The bid and ask at the time are:
    //
    // BID    |     ASK
    // -------+--------
    // $95.00 | $102.00
    //
    // We submit an order of 10 SPY @ $100 USD. Since it sits in the middle of the bid and ask,
    // We expect a fill of $100 USD (minus some cents per share in case we're modeling some sort of smart order routing algorithm).
    // (Note: we'd expect a fill of $102.00 if we set the limit order to $103.00)
    //
    // Whenever the bar gaps down on the next day, the limit order will execute at the ASK's High, rather than the
    // limit price we set.
    //
    // This is an issue when trading a security using only Tick data since it's prone to gapping down, because
    // each Tick has no knowledge of the previous quote's data. That means that Ticks can and will gap down as a result
    // of a thin orderbook when a price level is removed from the orderbook.
    //
    // This is also an issue for orders left open overnight on securities that are prone to gapping down at the market open.
    Assert.AreEqual(order.Quantity, fill.FillQuantity);
    Assert.AreEqual(order.LimitPrice, fill.FillPrice);
    Assert.AreEqual(OrderStatus.Filled, fill.Status);
}
Martin-Molinero commented 3 years ago

On gap-down of data, the default fill model does not take into account an existing limit order, and can potentially execute the limit order at a price better at or worse than the expected price. This can cause issues when backtesting with Tick data, as well as whenever limit orders are left open past market close.

Another case to keep in mind for limit orders I think, where it does fill at a better price, is when the requested price is already crossed and can fill at better price. Say buy limit at 10 and ask is at 9, my understanding is it should fill at 9?

gsalaz98 commented 3 years ago

Another case to keep in mind for limit orders I think, where it does fill at a better price, is when the requested price is already crossed and can fill at better price. Say buy limit at 10 and ask is at 9, my understanding is it should fill at 9?

Yes, in this case we should fill at $9.00 (and also apply slippage upwards (i.e. $9.01, $9.02, etc.) if we take more than the QuoteBar says the market has at that time).

We'll need some sort of state tracking to know whether it's an initial order submit request or if the order's already been placed but the limit price has already been crossed in order to support this case

jaredbroad commented 1 year ago

Closing this one for now. @AlexCatarino please make separate individual issues for any remaining fill models and link them here.

AlexCatarino commented 1 year ago

LimitIfTouchedFill doesn't have the same issue: using quote data instead of trade.