Expressive Bidding
Expressive Bidding Guide
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 amkt()
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.
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.
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:
- the
qty()
function - a placeholder for the future quantity filled in a given symbol; - input parameters Expressive Bid variables
aapl
andmsft
- these would come from the bidder logic argument (arg
); and - return value bidder action created by the
subject_to
andplace_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:
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 typeExpressive_order.t
(ororder
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. Eachorder
contains all relevant information from the target orders provided via FIX plus optional per-symbol bidder data. Anorder
is also the input accepted by theqty()
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_action
s. 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_action
s 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_action
s in the list is not taken into account, so constraints should be constructed such that multiple actions do not overlap. In OCaml/ReasonML Bidder_action
s are record types, and the best way to create them is using the subject_to
as follows:
[
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 ofqty()
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
and220
in our example) can only further constrain target order prices for the corresponding symbols.place_orders()
- accepts a list oforder
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/receiveX
dollars for each unit of the basket, up toN
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:
/* 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:
- Define a simple expressive bid
- Seed it with some runtime data (FIX target orders and bidder data)
- Define some other orders for it to match with, and set NBBO
- 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:
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);
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.
[@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.
/* 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.
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.
compute_bids();
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.
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.