Expressive Bidding

Price-Quantity Curves


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.

In this document:

  • Simple examples of bidder logic for price-quantity curves (AKA indifference curves, utility curves)
  • Constraints for managing information leakage, market impact, and price improvement

Standard limit orders permit one price limit, and that same price limit applies regardless of fill size. However, implicit costs like market impact and cost-of-liquidity mean the price per share for 100 shares is rarely the same as for a large block. Price-quantity curves or "indifference curves" address this reality by allowing you to express different constraints and pricing preferences for different fill quantities. Some of the common scenarios where this is useful:

  • Price improvement "size-up": a willingness to accept higher fill quantities if filled at a more favorable price.
  • Market impact control: bidding more aggressively for larger fill quantities while seeking liquidity
  • Information leakage control: a desire to either trade a small quantity of a larger order (child order) to avoid leaking information, or a large quantity (e.g., the majority of a parent order), but not in between.

These curves can be expressed as a combination of if/else statements, where each if condition represents a pricing tier (quantity range). The rest of this tutorial presents a few ways to construct indifference curves using bidder logic.

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.

Price-Quantity Curve Bidding Constraints

Suppose we have a parent order to buy 100,000 shares of a stock. Depending on the range of fill quantities, we want to express different price preferences.

For instance, we might say that we want one of these three outcomes:

  • Buy up to 1,000 shares at some price_x, or
  • Buy between 1,000 and 10,000 shares at some price_y, or
  • Buy between 10,000 and 100,000 shares at some price_z

Setting price preferences across multiple tiers of quantity ranges enables us to express various strategies that are traditionally more difficult to accomplish with a standard limit order book. By specifying a quantity tier, we can effectively price information leakage such that some leakage is allowed as long as a large fill can is achieved. Alternatively, we could preemptively lock in a preferred price for specific quantities in anticipation of market price changes and manage execution costs.

In bidder logic, each tier/preference above directly translates into a constraint that looks like:

reason
subject_to(lower_bound <= qty(order) && qty(order) <= upper_bound,
  place_notional(preferred_price * qty(order))
)

The quantity bounds (lower_bound, upper_bound) and preferred_price depend on each statement and will be defined in the later examples according to specific trading objectives.

We can stitch together multiple constraints, one for each tier listed above:

reason
[
  subject_to(qty(order) <= 1000,
    place_notional(price_x * qty(order))),
  subject_to(qty(order) > 1000 && qty(order) <= 10000,
    place_notional(price_y * qty(order))),
  subject_to(qty(order) > 10000 && qty(order) <= 100000,
    place_notional(price_z * qty(order)))
]

Here we define multiple bidder_actions using the subject_to function. Bidder actions are mutually exclusive, so a fill will only take place in at most one of the three tiers, not all simultaneously. Starting from the first tier, if qty(order) <= 1000 is satisfied, then only the first action place_notional(price_x * qty(order)) is taken. No other branches will be acted on, ensuring no overfill (e.g., a fill of 1000 + 10,000 + 100,000 shares is infeasible). Note on code construction: using a List.map combinator may be more appropriate in some cases - see the developer reference guide for details.

The constraint now has three distinct quantity tiers - let's see how to use this framework for a few different trading objectives.

Price Improvement

Consider a scenario where we are opportunistically willing to trade larger quantities if given some price improvement. For example, we are comfortable paying $20 per share up to 1,000 shares, but at a lower price of $19, we'd accept up to 5,000 shares. We can express this by setting a lower price preference for the higher fill quantity tier. In the following code, the bidder only transacts above 1,000 shares if the price per share can be reduced (improved) by $1.

reason
[
  subject_to(qty(order) <= 1000,
    place_notional(20 * qty(order))),
  subject_to(qty(order) >= 5000,
    place_notional(19 * qty(order)))
]

Market Impact Control

Consider a different scenario where we are anticipating some market impact and may be willing to pay more in order to execute a full parent order as opposed to multiple smaller child orders. In doing so, we can specify acceptable levels of market impact pre-trade and control the total implicit cost to execute the full order.

reason
[
  subject_to(qty(order) <= 1000,
    place_notional(20 * qty(order))),
  subject_to(qty(order) >= 100000,
    place_notional(22 * qty(order)))
]

Notice that the structure of the code is nearly the same: all we changed from the previous example is the positive/negative directionality of the curve, while our bidder logic framework has remained the same. We can achieve two very different trading objects with the same bidder just by changing the price. See the list operations section for tips on how to specify these prices as variables for runtime flexibility.

Information Leakage Control

Similar to the goal of reducing marking impact, we can control information leakage by expressing willingness to pay slightly more to get a majority of the parent order filled in one transaction. Let's say we want a bidder that will trade only if the fill quantity is < 1% or > 90% of the total order size. For any fill quantities in between, we do not want to transact.

Much of the code from the previous example can be reused. The only difference is that instead of using fixed quantity tiers (hard coding the curve), we can define each tier as a function of the quantity from the target order referencing. Recall that target orders are submitted via FIX and can be accessed inside the bidder logic via the input parameter orders (see the input parameters section of Expressive Bidding Guide).

From the orders list, we can find the corresponding order for symbol "A" and assign the target order size to the target_qty variable. Then we define the fill quantity tiers as percentages of the target_qty with appropriate comparison operators:

reason
let order = List.hd(orders);        /* expects 1 order in the `orders` parameter */
let target_qty = order.order.qty;   /* "qty" refers to the size of the order */
[
  subject_to(qty(order) < target_qty / 100,
    place_notional(20 * qty(order))),
  subject_to(qty(order) > target_qty * (9/10),
    place_notional(21 * qty(order)))
]

By referencing the target orders (orders), we no longer need to hard code quantity values or symbols and have templated a reusable bidder that can build a price-quantity curve off of any other FIX order. To take it one step further, we can make the code even more flexible by incorporating the bidder_data parameter -- the user-defined input data -- to assign percentages dynamically (e.g., 1%, 90%) and preferred prices (e.g., $20, $21). See how to incorporate user-defined data in the input parameters section of the Expressive Bidding Guide.

Next Steps

1: Check the Price-Quantity Indifference Curves 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.