Expressive Bidding

ETF Trading

Expressive Bidding templates for multi-security trades involving ETFs and indices.

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.

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.

Synthetic Create or Redeem

Objective: substitute N shares of an index ETF for N units of its components (redeem), or vice versa (create).

Synthetic ETF Create/Redeem is a special case of the more general goal of atomically executing trades in several securities at once. To 'create' an ETF share (or lot), we can simply enforce a constraint that for every share of the ETF bought, we simultaneously sell one unit of the component basket. A "unit" refers to the set of securities including each component of the index/ETF with the appropriate per-security weightings.

As with the Fixed Composition Basket example, we can rely on the all_eq() function to enforce the constraint that executions in all symbols happen in the correct proportion to each other. all_eq when used with the index weightings for each symbol will ensure that for each one share (lot) of the ETF, we trade the corresponding quantity of shares in each component.

Bidder Data:

reason
type security = {symbol: string, side: side, weighting: int}
type synth_create_redeem_data = {etf_and_components: list(security)}

/* example; provided over FIX. Synthetic "create" (buy ETF, sell components) */
let synth_create_sample = {
  etf_and_components: [
    {symbol: "ASDF", side: Sell, weighting: 6},
    {symbol: "QWER", side: Sell, weighting: 3},
    {symbol: "ZXCV", side: Sell, weighting: 5},
    {symbol: "ETFX", side: Buy, weighting: 1}
  ],
};

Bidder Logic:

reason
let synth_create_redeem: bidder(_) = (arg, ~mkt) => {
    open Bid;
    /* Compute list of index-weighted -fill- qtys */
    let weighted_qtys = List.map(o =>
        o.data.weighting * qty_order(o.order), arg.orders);

    /* Enforce equality across weighted fill qtys */
    Ok ([subject_to(all_eq(weighted_qtys),
        place_orders(arg.orders))]);
}

Synthetic Redeem

We can use the exact same bidder logic above, but with the sides inverted in the bidder data and target orders inputs (buy <-> sell):

reason
let synth_redeem_sample = {
  etf_and_components: [
    {symbol: "ASDF", side: Buy, weighting: 6},
    {symbol: "QWER", side: Buy, weighting: 3},
    {symbol: "ZXCV", side: Buy, weighting: 5},
    {symbol: "ETFX", side: Sell, weighting: 1}
  ],
};

Create / Redeem Validation Logic

While the above bidder logic accomplishes its goal when given the correct inputs, we can further specialize it by performing runtime validation that the input orders and data have the expected offsetting buy and sell properties. To do this, we can:

  1. Use bidder data to identify which security amongst the input arg.orders is the ETF
  2. Partition the arg.orders list into separate ETF and component lists using List.filter
  3. assert that the Components are all on the opposite side as the ETF using pattern matching and List.for_all

Bidder Data:

reason
type sec_type = ETF | Component;
type security = {symbol: string, side: side, weighting: int, sec_type: sec_type}
type synth_create_redeem_data = {etf_and_components: list(security)}

/* example; provided over FIX. Synthetic "create" (buy ETF, sell components) */
let validation_sample = {
  etf_and_components: [
    {symbol: "ASDF", side: Buy, weighting: 6, sec_type: Component},
    {symbol: "QWER", side: Buy, weighting: 3, sec_type: Component},
    {symbol: "ZXCV", side: Buy, weighting: 5, sec_type: Component},
    {symbol: "ETFX", side: Buy, weighting: 1, sec_type: ETF}
  ],
};

Bidder Logic

reason
let synth_create_redeem_validation: bidder(_) = (arg, ~mkt) => {
    let etf = List.filter(o => o.data.sec_type == ETF, arg.orders);
    let components = List.filter(o => o.data.sec_type == Component, arg.orders);

    /* Assert: 1 ETF order && all components on opposite side */
    let valid_orders = switch (etf) {
         | [etf] =>
              let opposite = fun | Buy => Sell | Sell => Buy;
              if(List.for_all(c => c.order.side == opposite(etf.order.side), components)) {
                  [etf] @ components;
              } else {[];};
         | _ => [];
    };

    /* Build bidder action off of validated orders */
    open Bid;
    let weighted_qtys = List.map(o => qty_order(o.order) / o.data.weighting, valid_orders);
    Ok ([subject_to(all_eq(weighted_qtys),
        place_orders(valid_orders)
    )]);
}

ETF or Components (Not Both)

Objective: trade either the ETF or its underlying components and minimize total cost (maximize price improvement).

To seek exposure to the securities in an index ETF, we can either buy that ETF directly OR we can buy each of the individual components according to the index weightings, but not both.

OneChronos auctions attempt to find matches that yield the highest gains from trade on both sides. We take advantage of this feature in this example by expressing mutually exclusive interest in both the ETF and the underlying basket of components. If counterparties are available in both the ETF and the component basket, then the auction will seek the execution that provides the highest aggregate price improvement (one way of determining lowest cost) as defined in OneChronos' Form ATS-N.

Bidder Data:

reason
type sec_type = ETF | Component;
type security = {symbol: string, side: side, weighting: int, sec_type: sec_type}
type etf_or_component_data = {etf_and_components: list(security)}

/* example; provided over FIX. Synthetic "create" (buy ETF, sell components) */
let validation_sample = {
  etf_and_components: [
    {symbol: "ASDF", side: Sell, weighting: 6, sec_type: Component},
    {symbol: "QWER", side: Sell, weighting: 3, sec_type: Component},
    {symbol: "ZXCV", side: Sell, weighting: 5, sec_type: Component},
    {symbol: "ETFX", side: Buy, weighting: 1, sec_type: ETF}
  ],
};

Bidder Logic:

reason
let etf_or_components: bidder(_) = (arg, ~mkt) => {
    let etf = List.filter(o => o.data.sec_type == ETF, arg.orders);
    let components = List.filter(o => o.data.sec_type == Component, arg.orders);

    open Bid;
    let etf_action = etf |> fun | [e] => place_orders([e]) | _ => place_orders([]);
    let weighted_components = List.map(o => qty_order(o.order) / o.data.weighting, arg.orders);
    Ok ([subject_to(all_eq(weighted_components),
        place_orders(components)),
    etf_action
    ]);
}

ETF or Components (Any Combination)

Objective: Trade either the ETF and/or its underlying components, minimizing total cost.

We can express flexibility (indifference) between trading in the index ETF and its underlying components by expressing maximum size in both while enforcing a total notional constraint across them.

Suppose we want to purchase up to 1,000,000 dollars of some mix of the ETF and its components. We would accept 1,000,000 dollars of just the ETF, 1,000,000 of just the components, and anywhere in between as long as the fills in the components remain correctly weighted. We can accomplish this using place_orders() including ETF and the basket of components, both with target order parameters reflecting the full size, subject to:

  • The basket components are only being filled in the appropriate unit proportions.
  • The total notional filled remaining below 1,000,000

As in the example above, the auction process will seek the solution with the highest aggregate price improvement, which represents the lowest cost given available liquidity.

Bidder Data:

reason
type sec_type = ETF | Component;
type security = {symbol: string, side: side, weighting: int, sec_type: sec_type}
type synth_create_redeem_data = {
    etf_and_components: list(security),
    max_notional: int
};

/* example; provided over FIX. Synthetic "create" (buy ETF, sell components) */
let validation_sample = {
    etf_and_components: [
        {symbol: "ASDF", side: Buy, weighting: 6, sec_type: Component},
        {symbol: "QWER", side: Buy, weighting: 3, sec_type: Component},
        {symbol: "ZXCV", side: Buy, weighting: 5, sec_type: Component},
        {symbol: "ETFX", side: Buy, weighting: 1, sec_type: ETF}
    ],
    max_notional: 1000000
};

Bidder Logic:

reason
let etf_or_components: bidder(_) = (arg, ~mkt) => {
    let orders = arg.orders;
    let max_notional = arg.bidder_data.max_notional;
    let etf = List.filter(o => o.data.sec_type == ETF, orders);
    let components = List.filter(o => o.data.sec_type == Component, orders);

    open Bid;
    let weighted_components = List.map(o =>
        qty_order(o.order) / o.data.weighting, orders);
    let within_max_notional = sum_map(o =>
        o.order.price * (qty(o)), etf @ components) <= const(max_notional);

    Ok ([subject_to(all_eq(weighted_components) && within_max_notional,
        place_orders(etf @ components))
    ]);
}

Next Steps

  1. 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.
  2. Modify to your needs and reach out to the OneChronos team at [email protected] for usage in production.