Reference / Chain frames

Chain frames

Every publication on a chain: channel is one binary frame in the CCH2 format. All values are little endian. See the Realtime API for how to connect. chain: frames carry quotes and aren't customer delivery; customer sockets receive the derived Arrow tables described under Frames.

FieldTypeNotes
magic4 bytesCCH2
kindu81 full frame, 2 delta frame
tieru81 100ms, 2 1s, 3 1m, 4 15s, 5 30s
root_lenu8Length of root
rootbytesThe option root
sequ64Counts per channel
ts_nsu64Frame time in nanoseconds since the Unix epoch
model_versionu16Version of the pricing model

Expiry

The expiry block follows the header in both frame kinds.

FieldTypeNotes
expiry_daysu16Days since 1970-01-01
forward_e4i32Implied forward, in 1/10,000 USD
spot_e4i32Implied spot, in 1/10,000 USD
gex, dex, vex, cexf32 eachExpiry totals
call_oi, put_oiu32 eachOpen interest totals

Spot comes from the options market through put-call parity, not from an equity feed.

Full frame

A u16 contract count follows the expiry block, and then each contract in this order.

BitFieldTypeNotes
strike_e3i32Strike, in 1/1,000 USD
rightu81 call, 2 put
0bid_e4i321/10,000 USD
1ask_e4i321/10,000 USD
2bid_szu32Contracts
3ask_szu32Contracts
4oiu32Open interest
5ivf32A fraction, so 0.2 is 20%, with no cap
6delta_e4i161/10,000
7gammaf32Per $1
8vannaf32Per vol point
9charmf32Per calendar day
10gexf32Per 1% move
11dexf32Dollars of delta notional
12vexf32Per vol point
13cexf32Per day

Missing values

A contract field with no value carries a marker. The decoder below returns each of these as null.

FieldTypeMissing value
bid_e4i320
ask_e4i320
delta_e4i16-32768
ivf32NaN
gammaf32NaN
vannaf32NaN
charmf32NaN
gexf32NaN
dexf32NaN
vexf32NaN
cexf32NaN

bid_sz, ask_sz and oi have no marker: 0 means zero.

Per-contract greeks and exposures are rounded to half precision, so don't expect more than about three significant digits.

Delta frame

Only the 100ms cadence sends delta frames. A u16 change count follows the expiry block, and then each change:

FieldTypeNotes
idxu16The contract's position in the last full frame
masku16Which fields follow, using the bit numbers above
ValuesvariesThe set fields, in bit order

Apply a delta only if its seq is exactly one more than the previous publication's. If it isn't, you have a gap, so treat the chain as unknown until the next full frame arrives. Full frames come every second at this cadence.

Exposure conventions

Exposures assume dealers are short calls and long puts, with sign of +1 for calls and -1 for puts. All are in dollars of delta notional.

FieldFormula
gexsign × gamma × oi × 100 × spot² × 0.01
dexdelta × oi × 100 × spot
vexsign × vanna × oi × 100 × spot
cexcharm × oi × 100 × spot

Decoder

A TypeScript decoder for both frame kinds. It returns missing values as null.

const FIELDS = ['bid', 'ask', 'bidSize', 'askSize', 'oi', 'iv', 'delta', 'gamma', 'vanna', 'charm', 'gex', 'dex', 'vex', 'cex'];

export function decode(bytes: Uint8Array) {
    const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
    let at = 0;
    const step = (size: number) => (at += size) - size;
    const u8 = () => view.getUint8(step(1));
    const u16 = () => view.getUint16(step(2), true);
    const u32 = () => view.getUint32(step(4), true);
    const i32 = () => view.getInt32(step(4), true);
    const f32 = () => view.getFloat32(step(4), true);
    const u64 = () => view.getBigUint64(step(8), true);
    const text = (size: number) => new TextDecoder().decode(bytes.subarray(step(size), at));
    const float = () => { const value = f32(); return Number.isNaN(value) ? null : value; };
    const price = () => { const value = i32(); return value === 0 ? null : value / 1e4; };

    const field = (index: number) => {
        if (index < 2) return price();
        if (index < 5) return u32();
        if (index === 6) { const value = view.getInt16(step(2), true); return value === -0x8000 ? null : value / 1e4; }
        return float();
    };

    if (text(4) !== 'CCH2') throw new Error('Unknown frame');
    const kind = u8();
    const tier = u8();
    const root = text(u8());
    const seq = u64();
    const timeNs = u64();
    const modelVersion = u16();
    const expiry = {
        date: new Date(u16() * 86_400_000).toISOString().slice(0, 10),
        forward: i32() / 1e4, spot: i32() / 1e4,
        gex: f32(), dex: f32(), vex: f32(), cex: f32(),
        callOi: u32(), putOi: u32(),
    };
    const header = { kind, tier, root, seq, timeNs, modelVersion, expiry };

    const count = u16();
    if (kind === 1) {
        const contracts = Array.from({ length: count }, () => {
            const contract: Record<string, unknown> = { strike: i32() / 1e3, right: u8() === 1 ? 'C' : 'P' };
            FIELDS.forEach((name, index) => { contract[name] = field(index); });
            return contract;
        });
        return { ...header, contracts };
    }
    const changes = Array.from({ length: count }, () => {
        const index = u16();
        const mask = u16();
        const values: Record<string, number | null> = {};
        FIELDS.forEach((name, bit) => { if (mask & (1 << bit)) values[name] = field(bit); });
        return { index, values };
    });
    return { ...header, changes };
}

© 2026

coreconvexity