Expressive Bidding
Pairs
Note: these templates are meant to demonstrate bidder logic code in a simulated environment. Templates may require modification for use in production to meet your specific trading objectives and standards.
Weighted Long/Short Pair
Objective: Buy one security and sell another, ensuring that each is filled in the desired ratio (share quantity).
We use bidder data to express the desired ratio between the two securities and associate that data with target orders. For each security, we assign a weighting
by creating a data type (security
). The weighting will be used in bidder logic to enforce the desired share quantity ratio as a constraint. Using the symbol and side provided, the bidder logic can map the weighting data to the corresponding target order. Since this is a pairs trade we will need two instances of security
, which we can represent as a list as shown in pairs_data_sample
.
Bidder Data:
type security = {
symbol: string,
side: side,
price: int,
weighting: int,
};
type pairs_data = {pair: list(security)};
/* Bidder data example: two securities, in a 5:1 ratio */
let pairs_data_sample = {
pair: [
{symbol: "ASDF", side: Buy, price: 70, weighting: 1},
{symbol: "QWER", side: Sell, price: 20, weighting: 5},
],
};
Bidder Logic:
let weighted_qty_pairs: bidder(_) = (arg, ~mkt) => {
open Bid;
let (a, b) = (List.hd(arg.orders), List.hd(List.tl(arg.orders)));
let weighted_qty_a = a.data.weighting * qty(a);
let weighted_qty_b = b.data.weighting * qty(b);
Ok ([subject_to(weighted_qty_a - weighted_qty_b == const(0),
place_orders([a,b]))]);
};
Dollar Neutral Pair
Objective: Trade two securities such that the expected notional amount bought in one is equal to the expected notional amount sold in the other.
Target orders contain all the information needed to compute a strict dollar neutrality constraint. Alternatively, we can incorporate a tolerance range that offers a controlled relaxation of the dollar neutrality constraint using bidder data. This can increase the set of possible fills for our Expressive Bid and help find a balance between execution precision and fill rate.
Note that the dollar neutrality constraint is constructed using limit price, not execution price. Price improvement in either security may result in differences in absolute notional executed.
Bidder Logic:
let dollar_neutral_pairs: bidder(_) = (arg, ~mkt) => {
open Bid;
let (a, b) = (List.hd(arg.orders), List.hd(List.tl(arg.orders)));
let dollars_filled = (o) => o.order.price * qty(o);
Ok ([subject_to(dollars_filled(a) == dollars_filled(b),
place_orders([a, b]))
]);
}
A more idiomatic alternative to the example above uses pattern matching on the list of orders. This also ensures that the list is exactly two orders in length as the bidder logic is expecting, whereas the example above takes the first two orders regardless of how many are presented through arg
.
This example also demonstrates how to include a tolerance/error amount on dollar neutrality.
Bidder Data:
type dollar_neutral_data = {tolerance: int};
/* Example: dollar neutral within +/- $1,000 notional */
let my_data = {tolerance: 1000}
Bidder Logic:
let dollar_neutral_pairs_2: bidder(_) = (arg, ~mkt) => {
open Bid;
let my_tolerance = arg.bidder_data.tolerance;
let dollars_filled = (o) => o.order.price * qty(o);
switch(arg.orders) {
| [a, b] =>
let net_notional = dollars_filled(a) - dollars_filled(b);
let dollar_neutral = abs(net_notional) <= const(my_tolerance);
/* Place orders if and only if apprx dollar neutral */
Ok ([subject_to(dollar_neutral,
place_orders([a,b]))]);
| _ => Error ("expected exactly two target orders");
};
}
Pair Spread
Objective: Trade two securities subject to a constraint that the price spread between them is greater than some amount.
In this example, our bidder logic decides whether to execute the trade based on the spread between the midpoint price of the two symbols. We can access the midpoint prices measured by OneChronos at the moment of the auction using the mkt(symbol)
market data accessor and mid
helper function to extract the mid of NBBO. Note that we calculate spread as an absolute value assuming it is already known which security will be priced higher or lower. This could be verified at runtime by extending bidder data to include an indication of which symbol is expected to be priced higher.
Bidder Data:
type spread_data = {
spread: int
}
/* Example: min spread of 123 */
let my_data = {spread: 123}
Bidder Logic:
let pairs_spread: bidder(_) = (arg, ~mkt) => {
let mid = (o) => mkt(o.order.symbol) |> mid;
switch arg.orders {
| [a, b] =>
let spread = abs(mid(a) - mid(b));
open Bid;
Ok ([subject_to(const(spread) >= const(arg.bidder_data.spread),
place_notional(mid(a) * qty(a) + mid(b) * qty(b)))]);
| _ => Error ("expected exactly two target orders");
}
};
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.