Expressive Bidding

Single-Stock Execution with Market Data


Production Status: Expressive Bidding is currently enabled for "Pairs" only (i.e. Expressive Orders consisting of two symbols with constraints on relative price and quantity), and does not currently include the `mkt()` function. Examples are included beyond what is currently live to facilitate discussion of expected use cases and testing. For details on currently available functionality, please see our Form ATS-N.

Single-stock execution algorithms incorporate several signals to determine how, when, and at what terms to execute. Some of those signals are based on market conditions such as imbalance, per-exchange differences in NBBO, price changes, etc. Using market data measured by OneChronos, Expressive Bids can respond immediately to momentary market conditions as part of the match process. This document provides several examples of how bidder logic can adjust participation (price, quantity, side, trade vs. don't trade) based on:

  1. NBBO consensus: price dynamically based on agreement between exchanges' bids/offers
  2. Imbalance: trade at mid or passive depending on NBBO imbalance
  3. Spread: adjust limit quantity as a function of momentary bid/ask spread
  4. Price dislocation: trade / don't trade if momentary price deviates significantly
  5. Relative Value: enter price/qty indifference curve based on price of reference basket
  6. Combinations of the above

Each of these bidder logic examples acts on a single security, and can be invoked using a single FIX order message with appropriate Target Order tags.

NBBO Consensus

Objective: set the limit price at specific points throughout the bid/ask spread, as a function the proportion of exchanges agreeing on NBBO.

Consider the momentary BBO across exchanges as a signal indicating how aggressively to price an order. The larger the proportion of exchanges agreeing on NBBO, the more aggressively the order is priced (and vice versa). This simplistic example adjusts price linearly across the entire spread according to NBBO consensus.

Note: the limit price on the underlying Target Order is always enforced.

Bidder Data:

reason
type bidder_params = {
    key_venues: list(mkt_src)
};

Bidder Logic:

reason
/* Simple model: count the exchanges deviating from NBBO and divide by total */
let nbbo_deviation_rate = (venues: list(mkt_price_point), nbb, nbo) => {
    let diff = (venue) => if(nbo != venue.offer && nbb != venue.bid) { 1; } else { 0; };
    List.fold_left((n, venue) => n + diff(venue), 0, venues) / List.length(venues);
};

let consensus_peg: bidder(_) = (arg, ~mkt) => {
    let o = List.hd(arg.orders);
    let all_mkts = mkt(o.order.symbol);

    /* get composite NBBO and Mid */
    let (nbb_px, nbo_px) = (all_mkts |> nbb, all_mkts |> nbo);
    let spread = nbo_px - nbb_px;

    /* filter markets of interest */
    let key_mkts_data = List.filter(px_data =>
        List.mem(px_data.src, arg.bidder_data.key_venues), all_mkts);

    let score = nbbo_deviation_rate(key_mkts_data, nbb_px, nbo_px);
    /* set price within spread price based on scoring (btwn 0..1) */
    let dynamic_px = switch (o.order.side) {
        | Buy => nbb_px + (spread * score);
        | Sell => nbo_px - (spread * score);
    };
    Ok (Bid.([place_notional(dynamic_px * qty(o))]));
};

Imbalance Discretion

Objective: trade at either the midpoint or the passive NBBO price, depending on magnitude of displayed imbalance.

If imbalance in either direction is greater than some proportion provided via bidder data, trade at the passive price. If it is less than the given proportion, trade at the midpoint.

Bidder Data:

reason
type imbal_data = {
    imbal_radius: int,
}

Bidder Logic:

reason
let imbal_discretion: bidder(_) = (arg, ~mkt) => {
    let o = List.hd(arg.orders);
    let r = arg.bidder_data.imbal_radius;

    /* Fetch composite NBBO, Mid, and NBBO qtys measured at auction time */
    let m = mkt(o.order.symbol) |> composite;
    let passive = switch(o.order.side) { | Buy => m.bid | Sell => m.offer};
    let mid = mkt(o.order.symbol) |> mid;

    /* Imbal normed to [0..1], with acceptable radius around 0.5 */
    let imbal = m.bid_qty / (m.bid_qty + m.offer_qty);
    let acceptable_imbal = 1/2 - r <= imbal && imbal <= 1/2 + r;

    /* Compute dynamic price based on order side and imbalance */
    let mid_or_passive = if (acceptable_imbal) { mid } else { passive };
    Ok (Bid.([place_notional(mid_or_passive * qty(o))]));
};

Market Coherence

Objective: only participate if the submitter's view of NBBO is the same as the venue's view of NBBO.

The user (order submitter) attaches their own most recent view of NBB and NBO for the given symbol as bidder data. If the venue has a different view of NBB and/or NBO, then the order does not participate. Otherwise, it participates at the terms of the underlying Target Order.

Bidder Data:

reason
type mkt_coherence_data = {
    my_nbb: price,
    my_nbo: price,
}

Bidder Logic:

reason
let mkt_coherence: bidder(_) = (arg, ~mkt) => {
    let o = List.hd(arg.orders);

    /* fetch auction-time NBB and NBO */
    let (nbb, nbo) = (mkt(o.order.symbol) |> nbb, mkt(o.order.symbol) |> nbb);

    open Bid;
    /* True if auction NBBO matches our view at order submission */
    let nbbo_coherence = const(arg.bidder_data.my_nbb) == const(nbb)
                         && const(arg.bidder_data.my_nbo) == const(nbo);

    Ok ([subject_to(nbbo_coherence,
        place_orders([o]))]);
};

Bid/Ask Spread Discretion

Objective: determine maximum quantity to trade based upon bid/ask spread for the given symbol.

This example adjusts the limit quantity of the order as a function of measured NBBO spread for the given symbol. The wider the bid/ask spread the lower the limit quantity (and vice versa), down to zero once the maximum spread is reached. Maximum spread is provided as bidder data.

Bidder Data:

reason
type spread_discretion_data = {
    max_spread_bps: int,
};

Bidder Logic:

reason
let spread_discretion: bidder(_) = (arg, ~mkt) => {
    let o = List.hd(arg.orders);
    let all_mkts = mkt(o.order.symbol);

    let (nbb_px, nbo_px) = (all_mkts |> nbb, all_mkts |> nbo);
    let spread_bps = Bid.abs_val(nbo_px - nbb_px) / nbo_px * 10000;

    /* Smaller spread means larger qty coeff (i.e. larger quoted qty) */
    let qty_coeff = Bid.abs_val(arg.bidder_data.max_spread_bps - spread_bps)
                    / arg.bidder_data.max_spread_bps;

    /* qty_coeff in range [0..1] */
    let dynamic_limit = qty_coeff * o.order.qty;

    open Bid;
    Ok ([subject_to(qty(o) <= const(dynamic_limit),
        place_orders([o]))]);
};

Price Variance Detection

Objective: determine whether or not to trade based on the security's deviation from it's mean price.

If the price of the stock has momentarily dislocated beyond some given threshold, don't participate in the auction. Otherwise participate at the terms given by the parameters of the underlying order.

Bidder Data:

reason
type security_and_reference_data = {
    reference_metric: int,       // e.g. 20-min moving average price
    reference_threshold: int,    // max deviation from reference_metric
};

Bidder Logic:

reason
/* Simple example model: reference price dislocation from mean */
let compare = (current_price, reference_metric) => {
    abs(current_price - reference_metric)
};

let price_variance: bidder(_) = (arg, ~mkt) => {
    let o = List.hd(arg.orders);
    let current_px = mkt(o.order.symbol) |> mid;
    let signal_indicator = compare(current_px, arg.bidder_data.reference_metric);

    open Bid;
    let trade_signal = const(signal_indicator) <= const(arg.bidder_data.reference_threshold);
    Ok ([subject_to(trade_signal, place_orders([o]))]);
};

Market Signal Synthesis

Objective: consider multiple market data properties to determine constraints and participation.

Multiple signals like those above can be considered simultaneously to create custom behaviors. This example uses a combination of NBBO imbalance and NBBO consensus (agreement among exchanges) to determine whether to trade and at what price / quantity terms.

Bidder Logic:

reason
let signal_synthesis: bidder(_) = (arg, ~mkt) => {
    let o = List.hd(arg.orders);

    /* Fetch composite NBBO, Mid, and NBBO qtys measured at auction time */
    let all_mkts = mkt(o.order.symbol);
    let comp = all_mkts |> composite;
    let spread = comp.offer - comp.bid;

    /* Large near-side imbal -> aggressive price and qty */
    let (dynamic_px, dynamic_qty) = switch (o.order.side) {
        | Buy =>
            let bid_imbal = comp.bid_qty / (comp.offer_qty + comp.bid_qty);
            (comp.bid + (spread * bid_imbal), o.order.qty * bid_imbal);
        | Sell =>
            let offer_imbal = comp.offer_qty / (comp.offer_qty + comp.bid_qty);
            (comp.offer + (spread * offer_imbal), o.order.qty * offer_imbal);
    };

    /* nbbo_deviation_rate defined above: proportion of exchanges w/ NBBO */
    let nbbo_consensus = nbbo_deviation_rate(all_mkts, comp.offer, comp.bid) >= 1/2;

    if (nbbo_consensus) {
        open Bid;
        Ok ([subject_to(qty(o) <= const(dynamic_qty),
            place_notional(dynamic_px * qty(o)))]);
    } else { Error ("NBBO consensus is < 50%"); };
};

Next Steps

  1. Try this code in a simulated auction. Load the simulator walkthrough, and replace the bidder logic and data type with an example of your choosing. Tune the runtime arguments and auction parameters to your liking, and run the auction to see results.
  2. Modify to your needs and reach out to the OneChronos team at [email protected] for usage in production.