4. Quote Rule & the Lee-Ready Algorithm

4. Quote Rule & the Lee-Ready Algorithm

In the previous post, I discussed how to use polars to work out the Tick Rule given tick data (time, price and volume).  In this post, I will show how to use the Quote Rule and the Lee-Ready algorithm.

Note that the data that we have for this must have the bid and ask price along with the timestamp, trade price and volume of the trade.

As an alternative to the Tick Rule, the Quote Rule uses the current price relative to the midpoint of the bid and ask to determine if the trade was initiated by a buyer or seller. Let's start with an outline followed by the polars recipe.

  1. Calculate the midpoint between the bid and ask price
  2. Find the direction of the trade:
    1. If the current price > the midpoint, then the trade direction is 1,
    2. If the current price < the midpoint, then the trade direction is -1,
    3. Otherwise, the trade direction is 0
  3. Multiply the volume times the trade direction to yield the volume of the trade as negative (sellers) or positive (buyers).
lf_quote_rule = (pl.scan_parquet(filename)
    # Quote Rule ####################
                
    # calculate the midpoint
    .with_columns(
        ((pl.col("Bid") + pl.col("Ask")) / 2.0).alias("Midpoint")   
    )
    # If the current trade price > midpoint, then Trade Direction = 1
    # if the current trade price < midpoint, then Trade Direction = -1
    # otherwise Trade Direction = 0
    .with_columns(
        pl.when(pl.col("Price") > pl.col("Midpoint"))
            .then(pl.lit(1))
            .when(pl.col("Price") < pl.col("Midpoint"))
            .then(pl.lit(-1))
            .otherwise(pl.lit(0))
            .alias("Trade Direction")
    )
    # create an Order Flow column by multiplying the direction by the volume
    #  negative values signify sell orders
    .with_columns(
        (pl.col("Trade Direction") * pl.col("Volume")).alias("Order Flow")
    )
)

The obvious question to ask here is what happens when we have 0 for trade direction?  The Quote Rule can't resolve that.  This is where the Lee-Ready algorithm steps in. The algorithm is actually quite simple: use the Quote rule to determine the buy/sell volume where the direction is non-zero, otherwise apply the Tick Rule.  The combined polars recipe is as follows:

lf_lee_ready = (pl.scan_parquet(filename)
         
    # Tick Rule ##################
                
    # Use the difference between the previous price to get a direction
    .with_columns(
        (pl.col("Price").diff()).alias("Price Diff")
    )
    # if the price difference > 0 -> Tick Direction = 1
    # if the price difference < 0 -> Tick Direction = -1
    # otherwise, Tick Direction = None
    # Polars sets the value to null when using None.
    .with_columns(
        pl.when(pl.col("Price Diff") > 0)
            .then(pl.lit(1))
            .when(pl.col("Price Diff") < 0)
            .then(pl.lit(-1))
            .otherwise(pl.lit(None))
            .alias("Tick Direction")
    )
    # The None values (nulls) can be forward filled from
    # the preceeding non-null value
    .with_columns(
        pl.col("Tick Direction").fill_null(strategy="forward")
    )
    # we lose out on the inital values that can't be determined
    # so drop these
    .drop_nulls()
         
    # Quote Rule ####################
                
    # calculate the midpoint
    .with_columns(
        ((pl.col("Bid") + pl.col("Ask")) / 2.0).alias("Midpoint")   
    )
    # If the current trade price > midpoint, then Trade Direction = 1
    # if the current trade price < midpoint, then Trade Direction = -1
    # otherwise Trade Direction = 0
    .with_columns(
        pl.when(pl.col("Price") > pl.col("Midpoint"))
            .then(pl.lit(1))
            .when(pl.col("Price") < pl.col("Midpoint"))
            .then(pl.lit(-1))
            .otherwise(pl.lit(0))
            .alias("Trade Direction")
    )
    
    # Lee Ready Algorithm ################

    # if the Quote Rule is non-zero then use it to get the order flow
    # otherwise, use the Tick Rule
    .with_columns(
        pl.when(pl.col("Trade Direction") != 0)
            .then(pl.col("Volume") * pl.col("Trade Direction"))
            .otherwise(pl.col("Volume") * pl.col("Tick Direction"))
            .alias("Order Flow")
    )
)

Sample output of applying the above: