Expressive Bidding

Expressive Bidding Guide


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.

OneChronos enables orders to be "expressive" through computer code called bidder logic, which describes a set of bidding preferences and constraints to apply to data provided at runtime. There are four key concepts behind Expressive Bids:

  • bidder logic: ReasonML, OCaml, or use-case-specific code that describes the specifics of how you want to trade. Bidder logic can enforce constraints like "I only want to buy A if I can also sell B," can make decisions based on market conditions (e.g., NBBO) and user-supplied data at the time of each auction, express preferences contingent upon volume to be filled, and much more. There are detailed examples of bidder logic in the runnable templates for implementing various trading objectives.
  • target orders: standard limit orders submitted over FIX. Bidder logic can determine actions to take based on these target orders: their quantities, symbols, prices, etc. The target orders themselves act as outer bounds on the prices and quantities executed in their respective symbol.
  • bidder data: additional data can be provided to supplement target orders. Common examples include factor scores, basket parameters (e.g., aggregate notional max), portfolio composition targets, thresholds, index weightings. Bidder data is provided as a FIX tag on one or more target orders.
  • market data: bidder logic can fetch external market data measured by OneChronos at the time of each auction, and adjust behaviors according to momentary market conditions. Market data is accessed via a mkt() function, which can fetch for any given symbol: the composite NBBO and midpoint; NBBO quantities; and per-exchange bids, offers, and bid/offer quantities. This is detailed in the market data section of the developer reference.

Expressive Bid Diagram

Bidder logic receives inputs - market data, target orders, and bidder data - as runtime parameters and acts on them to produce actions. Those actions define and shape how you participate in an auction, with precise control over the set of possible fills. See the Expressive Bidding section of the FIX guide for more information on how target orders and bidder data are submitted.

Intro to Bidder Logic

Bidder logic is a short snippet of code: a single pure function that is given inputs (arguments) at runtime and returns its bidding intent and constraints as an action. One way to think about bidder logic is that it acts as an agent bidding on your behalf in the auction, according to the instructions in its code. It answers the question posed by the auction, "what would you pay or need to be paid for any specific package of securities that meets all of your constraints?" without having to enumerate all of the acceptable fill quantities and prices.

Here is a short snippet of bidder code that demonstrates this concept by enforcing two simple trading constraints using conditional logic: equal fill quantities in two securities—AAPL and MSFT—and a minimum fill in one of the securities.

reason
let equal_qtys = qty(aapl) == qty(msft);    /* Equal quantities of MSFT and AAPL */
let min_fill = qty(aapl) >= 5000;           /* minimum fill of 5000 in AAPL (and MSFT via `equal_qtys`) */

[
  subject_to(equal_qtys && min_fill,
    place_orders([aapl, msft]))             /* Limit prices, side, etc from target orders */
];

There are three important concepts in this snippet:

  1. the qty() function - a placeholder for the future quantity filled in a given symbol;
  2. input parameters Expressive Bid variables aapl and msft - these would come from the bidder logic argument (arg); and
  3. return value bidder action created by the subject_to and place_orders functions.

These are all described in the following few sections below.

Note: bidder logic can be written in ReasonML or OCaml, or in use-case-specific languages introduced by OneChronos. To learn more about these languages and why they were chosen, refer to the bidder logic FAQ. To get a feel for the languages themselves, visit sketch.sh for a user-friendly web runtime for both languages.

The qty Function

The qty() function represents your symbolic fill quantity in the given order. The function takes an order as its input (as discussed in the Input Parameters section). What do we mean by "symbolic?" When a bidder is being evaluated, regular variable names are bound to specific values. But because qty() represents your future fill quantity, its value isn't known until the auction concludes. So it remains symbolic prior to that point; a placeholder for a future value:

reason
let price = 540                               // Bound variable
let nflx_fill_qty = qty(nflx)                 // Symbolic variable; future fill quantity
[
  subject_to(nflx_fill_qty > 1000,            // Constraint: qty > 1000
    place_notional(price * nflx_fill_qty))    // Bidder action: nflx.order.price * qty(nflx)
]

Inside of bidder logic, qty() variables have their own types but can be used in boolean and arithmetic expressions as though they were regular numeric types. Our equal_qtys constraint above is an example of how qty(aapl) and qty(msft) can be used in logical expressions to generate constraints. This particular constraint semantically means "My fill quantity in AAPL must be equal to my fill quantity in MSFT."

The value represented by qty() variables is always positive, regardless of whether the order is a buy or sell order. Note that this value is different from the limit quantity from the corresponding target order and that limit quantity automatically acts as an outer bound on the value the associated qty() variable can take during the auction.

Input Parameters

The input to the bidder logic function (arg) contains structured records of the data provided over FIX:

  • bidder_data: optional user-supplied "bidder data," attached as a FIX tag to one or more target orders. Anything encodable as JSON and decodable as a concrete user-specified type can be used as bidder data. JSON is automatically deserialized according to the type definition. Examples of data include pricing coefficients, quantity ratios, mappings between symbols and factors.
  • orders: the list of target orders submitted over FIX, each represented as a record of type Expressive_order.t (or order for short). Each order's limit price and quantity act as outer bounds on executions, in that executions will not take place outside a target order's limit price and limit quantity. These bounds cannot be violated by any actions taken in bidder logic. Each order contains all relevant information from the target orders provided via FIX plus optional per-symbol bidder data. An order is also the input accepted by the qty() function.

The market data snapshot is made available by the mkt function, which is provided as an optional bidder logic argument as: let my_bidder: bidder(_) = (arg, ~mkt) => {...}. The ~ in ~mkt indicates that the argument is optional. When calling the function, the ~ is not included in the function name.

See the Bidder Logic Arguments section in the develop reference guide for details and examples.

Return Value

Every bidder logic function returns a simple list of bidder_actions. Each bidder action has two components:

  • A set of constraints, and
  • An action to take if those constraints are met.

Many bidders return only one action. When multiple bidder_actions are entered in the list, each bidder_action is mutually exclusive; all of them may be considered in an auction, but only one (or none) will be executed. The order of the bidder_actions in the list is not taken into account, so constraints should be constructed such that multiple actions do not overlap. In OCaml/ReasonML Bidder_actions are record types, and the best way to create them is using the subject_to as follows:

reason
[
  subject_to(constraint_set_1, action_1),   /* If constraint_set_1 is true, take action_1 */
  subject_to(constraint_set_2, action_2),   /* If constraint_set_2 is true, take action 2*/
  ...
];

Bidder Constraints

The constraints are logical expressions, like those that would appear in an if statement. Individual constraints like qty(nflx) <= 250 above can be combined using logical and && and or || operations.

A few examples of simple constraints:

  • Equal quantity in two symbols: subject_to(qty(a) == qty(b), action);
  • Maximum notional over two symbols: subject_to(price_a * qty(a) + price_b * qty(b) <= 1000000, action);
  • Dollar neutrality: subject_to(price_a * qty(a) == price_b * qty(b), action); and
  • Minimum fill: subject_to(qty(a) > 1000, action).

Note that variables created with qty() cannot be multiplied or divided by each other; constraints and actions must be linear over their quantity variables.

Bidder Actions

Actions represent the maximum amount you would pay or the minimum amount you would need to be paid, provided that your constraints are satisfied. They are expressed as a notional amount: a sum of price * quantity terms. There are three return functions available:

  • place_notional() - accepts a linear combination of qty() variables and prices. For example, place_notional(140 * qty(order_a) + 220 * qty(order_b)). Note that a price expressed here (as a numeric coefficient) cannot result in a fill at a price outside the limit defined in the corresponding target order. These prices (140 and 220 in our example) can only further constrain target order prices for the corresponding symbols.
  • place_orders() - accepts a list of order records, with the limit price on each acting as the bid / offered price. This semantically means "I want to participate in each of these symbols at the price specified in the corresponding orders, or better."
  • place_basket() - allows expression of a net price on a basket. Rather than defining prices for individual securities, a "unit" of a basket is defined as a list of quantity weighting and symbol pairs, and a net amount to pay or receive for each unit is specified. Note that fills in individual securities will still automatically be constrained to the prices defined (if any) in corresponding target orders and will adhere to NBBO constraints. This semantically means "I would like to trade a basket of securities as a whole and pay/receive X dollars for each unit of the basket, up to N units."

Regardless of the return type used, all constraints in the given bidder_action are enforced by the auction. Note that the place_notional and place_orders functions can produce the same results; place_orders is short-hand for using place_notional with the target order(s) limit prices. More detail on the return functions is provided in the return type section of the developer reference guide.

Bidder Logic Function Signature

Bidder logic is created by declaring a type for the bidder data parameter and the bidder logic code itself as follows:

reason
/* Bidder data type definition */
type my_bidder_data = {
  my_integer: int,
  /* etc... */
}

/* Bidder logic function declaration */
let my_bidder: bidder(_) = (arg, ~mkt) => {
  let x = arg.bidder_data.my_integer;
  /* etc... */
}

The type definition (type my_bidder_data) describes the type for the bidder logic argument's arg.bidder_data record. This type definition tells the bidder compiler what to allow in terms of how the bidder_data is used - the type can be given any name as it will be inferred when accessed via arg.bidder_data. It also tells the system how to interpret JSON-encoded data from target orders so there's. Hence, there's no ambiguity about data coming over FIX should be converted to a string vs. a numeric vs. a variant, and so on. The bidder logic declaration should always include the type (: bidder(_)) for the function.

Simulator Walkthrough

The best way to fully understand bidder logic is by directly using it in the Expressive Bidding Simulator. The simulator provides tools for creating Expressive Bids and running small auctions that those bids can participate in. This section walks through a simple Expressive Bid for portfolio trading, but the simulator functionality is available in all notebooks at try.onechronos.com. In this document, we will:

  1. Define a simple expressive bid
  2. Seed it with some runtime data (FIX target orders and bidder data)
  3. Define some other orders for it to match with, and set NBBO
  4. Run an auction and view the results

Things the simulator is great for:

  • Understanding how Expressive Bidding works
  • Prototyping new Expressive Bids
  • Developing a feel for bidder logic syntax
  • Testing assumptions about how OneChronos matching logic works*

Things the simulator isn't meant for:

  • Testing bidder logic you plan to submit for production (please reach out to the OneChronos team).
  • Modeling complex interactions between many bidders
  • Backtesting scenarios based on large datasets

1 - Create an Expressive Bid

We'll create and upload a portfolio bidder that buys and/or sells a mix of securities while enforcing a specific composition (share quantity ratio between securities).

Define Bidder Logic:

reason
type component = {symbol: string, side: side, weighting: int}
type ratios_data = {basket: list(component)}

/* example; provided over FIX */
let ratios_data_sample = {
  basket: [
    {symbol: "ASDF", side: Sell, weighting: 6},
    {symbol: "QWER", side: Sell, weighting: 3},
    {symbol: "ZXCV", side: Sell, weighting: 1}
  ]
};

/* For mapping `component` data onto `orders` arg; see Expressive Bidding Guide for more info */
let component_key = (c: component) => (c.side, c.symbol);

reason
let portfolio_bidder: bidder(_) = (arg, ~mkt) => {
    open Bid;
    let weighted_qtys = List.map(o => o.data.weighting * qty_order(o.order), arg.orders);
    Ok ([subject_to(all_eq(weighted_qtys),
        place_orders(arg.orders)
    )]);
}

Upload bidder to server:

Upload our bidder, defining the per_sym_data arguments for mapping bidder data (weightings) onto orders for ease of use. See expressive bidding guide for details.

reason
[@program]
/*  Upload using function name as string: "portfolio_bidder" */
let portfolio_bidder_id = upload_bidder(
    ~bidder_data_preprocessor="of_json_ratios_data",
    ~per_sym_data_preprocessor="of_json_component",
    ~per_sym_data_key="component_key",
    "portfolio_bidder"
);

2 - Create runtime bidder inputs: data and orders

In production, the instances of bidder data and target orders used as arguments to our portfolio_bidder would come over FIX. Here, we define them as simple records.

reason
/* Example target orders; must be a `list` of `Order.t` type (target orders) */
let portfolio_sample_orders: list(Order.t) = [
  {id: "o1", symbol: "ASDF", side: Sell, qty: 10000, price: 121},
  {id: "o2", symbol: "QWER", side: Sell, qty: 10000, price: 454},
  {id: "o3", symbol: "ZXCV", side: Sell, qty: 10000, price: 787},
];

set_orders(portfolio_bidder_id, portfolio_sample_orders);

/* Example bidder data; matches the `ratios_data` type above */
let portfolio_sample_data = {
  basket: [
    {symbol: "ASDF", side: Sell, weighting: 6},
    {symbol: "QWER", side: Sell, weighting: 3},
    {symbol: "ZXCV", side: Sell, weighting: 1}
  ]
};

set_data(
  portfolio_bidder_id,
  ~bidder_data=portfolio_sample_data,
  ~to_json_bidder_data=to_json_ratios_data,
  ~per_sym=portfolio_sample_data.basket,
  ~to_json_per_sym=to_json_component,
);

3 - Add Counterparties and Market Data

Define and upload a set of counterparty orders (standard limit orders) for our bidder to interact with. Set NBBO for each symbol.

reason
let contras: list(Order.t) = [
    {id: "contra_1", symbol: "ASDF", side: Buy, qty: 6000, price: 123},
    {id: "contra_2", symbol: "QWER", side: Buy, qty: 3000, price: 456},
    {id: "contra_3", symbol: "ZXCV", side: Buy, qty: 10000, price: 789},
];

List.map((order: Order.t) => upload_standard_order(order.id, order), contras);

let (mkt_data: list(mkt_symbol_snapshot)) = [
    {mkt_sym: "ASDF", mkt_prices: [{bid: 120, offer: 124, src: Composite, bid_qty: 1000, offer_qty: 1000}]},
    {mkt_sym: "QWER", mkt_prices: [{bid: 453, offer: 457, src: Composite, bid_qty: 1000, offer_qty: 1000}]},
    {mkt_sym: "ZXCV", mkt_prices: [{bid: 786, offer: 790, src: Composite, bid_qty: 1000, offer_qty: 1000}]},
];

update_mkt_data(mkt_data);

4 - Run the Auction

Call the run_auction() function with no arguments to see the results of an auction.

reason
compute_bids();

reason
run_auction();

*Important note: auctions in this simulator are computed using a deterministic optimization process due to resource constraints. Auctions in production include a mix of deterministic and non-deterministic solvers (all with the same optimization goals and objective function). Exact results may differ.

reason
clear_state();

Next Steps

For a deeper dive into how bidder logic works, take a look at the tutorials and the Developer Reference Guide.

If you're ready to get started writing your own bidder logic, look through the Runnable Templates to use as a starting point or for implementation ideas.