Expressive Bidding
Pairs (LIVE)
In this document:
- Simple examples of bidder logic for pairs trading
- Spread, dollar neutrality, quantity ratio constraints
Pairs trades involve trading two assets simultaneously. These trades often involve legging risk, as some are sensitive to price differences between the two assets, involve correlated assets, or are subject to dollar neutrality constraints. Using Expressive Bidding to enforce simultaneous execution in both assets can help avert that legging risk. Pairs trades are straightforward to implement using bidder logic, and very similar bidder logic can be used to achieve a variety of different trading goals:
- Buying one asset and selling another at a specific price difference (spread) between the two
- Using the proceeds from the sale of one asset to fund the purchase of another asset
- Hedging risk incurred from the purchase or sale of one asset with the purchase or sale of another asset
In this guide, we'll walk through a few different ways to implement these objectives.
The examples that follow are subsets of code showing possible implementations of constraints, not fully implemented bidder logic. See the Expressive Bidding Guide for an overview or the developer reference guide for implementation details.
Simple Pairs Constraints
We'll start with a simple pairs constraint centering around fill quantities. Consider a couple of informal ways to express a pairs constraint:
- Natural language: "Buy some of A but only if I also sell some of B"
- Predicate logic:
quantity(A) > 0 AND quantity(B) > 0
Here is what the notion of "trade both A and B simultaneously" could look like as a constraint in bidder logic code:
subject_to(qty(order_a) > 0 && qty(order_b) > 0,
place_orders([order_a, order_b])
); /* limit prices inferred from orders provided over FIX */
This form of the pairs constraint requires that the filled quantity represented by qty()
in both A and B must be greater than 0. This will always be enforced, but it permits fills in any ratio between "A" and "B" up to the quantity limits allowed by target orders. A more strict constraint would require not only being filled in both A and B but also being filled in a 2-to-1 ratio:
subject_to(1 * qty(order_a) == 2 * qty(order_b),
place_orders([order_a, order_b]))
Flexible Fill Ratio
This implementation is especially restrictive: it will only be filled if the ratio of fill quantities is exactly two shares of A
for every one share of B
. By partially relaxing this constraint, the order may have a better chance of being filled as it can interact with a wider variety of contra orders. We can use a delta
computation to require that "A" and "B" are filled within +/- 500 shares of the desired ratio:
let delta = abs(1 * qty(order_a) - 2 * qty(order_b));
subject_to(delta <= 500,
place_orders([order_a, order_b])
);
Dollar Neutrality
In the above, delta
represents the absolute difference between the number of shares filled in A and B. A similar delta
comparison can be used to enforce an approximate dollar neutrality constraint, where "A" and "B" are filled within an approximate net notional bound:
let px_A = 45; /* alternative: use prices from target orders */
let px_B = 55;
let delta = abs(px_A * qty(order_a) - px_B * qty(order_b));
subject_to(delta <= 1000,
place_notional(px_A * qty(order_a) + px_B * qty(order_b));
Adjusting for Price Improvement
Note that in the return, px_A
and px_B
represent limit price, not fill price. If the buy and sell orders are price improved below 45
and/or above 55
respectively, then the absolute net notional may fall outside the desired amount of 1000
. This can be taken into account by using the opposite NBB and NBO to incorporate the maximum possible price improvement in a given symbol, thereby setting an outer boundary on possible notional difference:
/* "B" can't be purchased below NBB; "A" can't be sold above NBO */
let upper_bound_A = (mkt(order_a.order.symbol) |> nbo) * qty(order_a); /* notional at NBO */
let lower_bound_B = (mkt(order_b.order.symbol) |> nbb) * qty(order_b); /* notional at NBB */
let delta = upper_bound_A - lower_bound_B;
Pairs Spread
NBBO computations can also be used to incorporate bid/ask spread into bidding decisions. For example, we can choose to submit a bid if and only if the midpoint of NBBO in A is at least $10 below the midpoint of NBBO in B:
/* Assumes "B" is the higher priced security; can be checked as addtl constraint */
let spread = mid(mkt(order_b.order.symbol)) - mid(mkt(order_a.order.symbol));
subject_to(spread >= 10,
place_notional(
mid(mkt(order_a.order.symbol)) * qty(order_a) +
mid(mkt(order_b.order.symbol)) * qty(order_b))
);
As in the previous example, NBBO for each symbol can be used to adjust for sensitivity to price improvement.
Next Steps
1: Check the pairs templates for runnable, full-featured examples of bidder logic.
2: Try this code in a simulated auction. Load the Auction Simulator 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.