Expressive Bidding
Single-Stock Execution with Market Data
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:
- NBBO consensus: price dynamically based on agreement between exchanges' bids/offers
- Imbalance: trade at mid or passive depending on NBBO imbalance
- Spread: adjust limit quantity as a function of momentary bid/ask spread
- Price dislocation: trade / don't trade if momentary price deviates significantly
- Relative Value: enter price/qty indifference curve based on price of reference basket
- 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:
type bidder_params = {
key_venues: list(mkt_src)
};
Bidder Logic:
/* 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:
type imbal_data = {
imbal_radius: int,
}
Bidder Logic:
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:
type mkt_coherence_data = {
my_nbb: price,
my_nbo: price,
}
Bidder Logic:
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:
type spread_discretion_data = {
max_spread_bps: int,
};
Bidder Logic:
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:
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:
/* 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:
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
- 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.
- Modify to your needs and reach out to the OneChronos team at [email protected] for usage in production.