Skip to content

Commit f3de3ac

Browse files
userFRMclaude
andauthored
feat(quote)!: remove the derived QuoteTick.midpoint column (#1201)
* feat(quote)!: remove the derived QuoteTick.midpoint column The engine-vs-terminal audit flagged QuoteTick.midpoint as a client-side derivation: the parser computed (bid + ask) / 2 and surfaced it as a column the wire never sends. The terminal's gRPC bridge is pure pass-through and emits no midpoint for quotes — this is the same class as the already-removed derive_ohlcvc and local greeks calc. The SDK's contract is to deliver exactly what the terminal delivers, so the derived column is removed. Removed the midpoint synthesis from every tick codegen surface (parser, tdbe struct, rust/python arrow frames, python/typescript classes, cpp layout asserts) and the columns presence helper's derive_midpoint path, then regenerated. The QuoteTick struct, its Arrow/Polars column, the per-binding class fields, the C ABI struct (tail padding grows to keep the align(64) size), the MCP/server JSON serialisers, and the benches all drop it. IvTick.midpoint and MarketValueTick's integer midpoint are real server-sent values and are unchanged. Breaking: the quote history / snapshot Arrow schema and the QuoteTick binding types no longer carry a midpoint column; compute (bid + ask) / 2 caller-side if needed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test: drop residual QuoteTick.midpoint references in projection tests test_column_projection built two QuoteTick values with a midpoint field and asserted midpoint in the projected column set. QuoteTick no longer carries that column, so `cargo check --all-targets --features arrow,polars` failed with E0560. Remove the field from both constructions and drop midpoint from the expected column lists; the projection assertions now match the wire schema. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * style: rustfmt projection-test comment reflow Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: preview <noreply@anthropic.com>
1 parent c225c09 commit f3de3ac

27 files changed

Lines changed: 53 additions & 353 deletions

File tree

thetadatadx-cpp/include/thetadatadx.h

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -603,7 +603,7 @@ THETADATADX_ALIGN64_BEGIN typedef struct {
603603
} ThetaDataDxIndexPriceAtTimeTick THETADATADX_ALIGN64_END;
604604

605605
/* NBBO quote tick (*_history_quote): the bid/ask quote with sizes,
606-
* exchanges, conditions, and a derived midpoint. */
606+
* exchanges, and conditions. */
607607
THETADATADX_ALIGN64_BEGIN typedef struct {
608608
int32_t ms_of_day;
609609
int32_t bid_size;
@@ -624,9 +624,7 @@ THETADATADX_ALIGN64_BEGIN typedef struct {
624624
* 'P' (80) for a put, 0 when contract identity is absent
625625
* (single-contract queries). Cast to char for display. */
626626
uint32_t right;
627-
/* 4 bytes padding before the double field */
628-
double midpoint;
629-
uint8_t _tail_padding[40];
627+
uint8_t _tail_padding[52];
630628
} ThetaDataDxQuoteTick THETADATADX_ALIGN64_END;
631629

632630
/* Trade-with-quote tick (*_history_trade_quote): each trade print fused

thetadatadx-cpp/include/tick_layout_asserts.hpp.inc

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -462,8 +462,6 @@ static_assert(offsetof(QuoteTick, strike) == 64,
462462
"ThetaDataDxQuoteTick.strike offset drifted from Rust");
463463
static_assert(offsetof(QuoteTick, right) == 72,
464464
"ThetaDataDxQuoteTick.right offset drifted from Rust");
465-
static_assert(offsetof(QuoteTick, midpoint) == 80,
466-
"ThetaDataDxQuoteTick.midpoint offset drifted from Rust");
467465
static_assert(sizeof(TradeGreeksAllTick) == 320 && alignof(TradeGreeksAllTick) == 64,
468466
"ThetaDataDxTradeGreeksAllTick layout drifted from Rust");
469467
static_assert(offsetof(TradeGreeksAllTick, ms_of_day) == 0,

thetadatadx-py/src/_generated/tick_arrow.rs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,6 @@ pub(crate) fn arrow_schema_for_qualname(qualname: &str) -> Option<Arc<Schema>> {
264264
Field::new("ask", DataType::Float64, false),
265265
Field::new("ask_condition", DataType::Int32, false),
266266
Field::new("date", DataType::Int32, false),
267-
Field::new("midpoint", DataType::Float64, false),
268267
Field::new("expiration", DataType::Int32, true),
269268
Field::new("strike", DataType::Float64, true),
270269
Field::new("right", DataType::Utf8, true),
@@ -2310,7 +2309,6 @@ pub(crate) mod slice_arrow {
23102309
let has_expiration = present.contains("expiration");
23112310
let has_strike = present.contains("strike");
23122311
let has_right = present.contains("right");
2313-
let has_midpoint = present.contains("midpoint");
23142312
let mut col_ms_of_day: Vec<i32> = Vec::with_capacity(if has_ms_of_day { n } else { 0 });
23152313
let mut col_bid_size: Vec<i32> = Vec::with_capacity(if has_bid_size { n } else { 0 });
23162314
let mut col_bid_exchange: Vec<i32> = Vec::with_capacity(if has_bid_exchange { n } else { 0 });
@@ -2324,7 +2322,6 @@ pub(crate) mod slice_arrow {
23242322
let mut col_expiration: Vec<Option<i32>> = Vec::with_capacity(if has_expiration { n } else { 0 });
23252323
let mut col_strike: Vec<Option<f64>> = Vec::with_capacity(if has_strike { n } else { 0 });
23262324
let mut col_right: Vec<Option<String>> = Vec::with_capacity(if has_right { n } else { 0 });
2327-
let mut col_midpoint: Vec<f64> = Vec::with_capacity(if has_midpoint { n } else { 0 });
23282325
for t in ticks {
23292326
if has_ms_of_day { col_ms_of_day.push(t.ms_of_day); }
23302327
if has_bid_size { col_bid_size.push(t.bid_size); }
@@ -2339,7 +2336,6 @@ pub(crate) mod slice_arrow {
23392336
if has_expiration { col_expiration.push(t.has_contract_id().then_some(t.expiration)); }
23402337
if has_strike { col_strike.push(t.has_contract_id().then_some(t.strike)); }
23412338
if has_right { col_right.push(if t.right == '\0' { None } else { Some(t.right.to_string()) }); }
2342-
if has_midpoint { col_midpoint.push(t.midpoint); }
23432339
}
23442340
let mut fields: Vec<Field> = Vec::new();
23452341
let mut columns: Vec<ArrayRef> = Vec::new();
@@ -2402,10 +2398,6 @@ pub(crate) mod slice_arrow {
24022398
fields.push(Field::new("right", DataType::Utf8, true));
24032399
columns.push(Arc::new(StringArray::from(col_right)) as ArrayRef);
24042400
}
2405-
if has_midpoint {
2406-
fields.push(Field::new("midpoint", DataType::Float64, false));
2407-
columns.push(Arc::new(Float64Array::from(col_midpoint)) as ArrayRef);
2408-
}
24092401
RecordBatch::try_new_with_options(Arc::new(Schema::new(fields)), columns, &RecordBatchOptions::new().with_row_count(Some(n))).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
24102402
}
24112403

thetadatadx-py/src/_generated/tick_classes.rs

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -991,16 +991,15 @@ pub(crate) struct QuoteTick {
991991
#[pyo3(get)] pub ask: f64,
992992
#[pyo3(get)] pub ask_condition: i32,
993993
#[pyo3(get)] pub date: i32,
994-
#[pyo3(get)] pub midpoint: f64,
995994
#[pyo3(get)] pub expiration: Option<i32>,
996995
#[pyo3(get)] pub strike: Option<f64>,
997996
#[pyo3(get)] pub right: Option<String>,
998997
}
999998
#[pymethods]
1000999
impl QuoteTick {
10011000
#[new]
1002-
#[pyo3(signature = (*, ms_of_day = 0i32, bid_size = 0i32, bid_exchange = 0i32, bid = 0.0f64, bid_condition = 0i32, ask_size = 0i32, ask_exchange = 0i32, ask = 0.0f64, ask_condition = 0i32, date = 0i32, midpoint = 0.0f64, expiration = None, strike = None, right = None))]
1003-
fn new(ms_of_day: i32, bid_size: i32, bid_exchange: i32, bid: f64, bid_condition: i32, ask_size: i32, ask_exchange: i32, ask: f64, ask_condition: i32, date: i32, midpoint: f64, expiration: Option<i32>, strike: Option<f64>, right: Option<String>) -> Self {
1001+
#[pyo3(signature = (*, ms_of_day = 0i32, bid_size = 0i32, bid_exchange = 0i32, bid = 0.0f64, bid_condition = 0i32, ask_size = 0i32, ask_exchange = 0i32, ask = 0.0f64, ask_condition = 0i32, date = 0i32, expiration = None, strike = None, right = None))]
1002+
fn new(ms_of_day: i32, bid_size: i32, bid_exchange: i32, bid: f64, bid_condition: i32, ask_size: i32, ask_exchange: i32, ask: f64, ask_condition: i32, date: i32, expiration: Option<i32>, strike: Option<f64>, right: Option<String>) -> Self {
10041003
Self {
10051004
ms_of_day,
10061005
bid_size,
@@ -1012,7 +1011,6 @@ impl QuoteTick {
10121011
ask,
10131012
ask_condition,
10141013
date,
1015-
midpoint,
10161014
expiration,
10171015
strike,
10181016
right,
@@ -4940,7 +4938,6 @@ impl QuoteTickList {
49404938
ask: t.ask,
49414939
ask_condition: t.ask_condition,
49424940
date: t.date,
4943-
midpoint: t.midpoint,
49444941
expiration: t.expiration.unwrap_or(0),
49454942
strike: t.strike.unwrap_or(0.0),
49464943
right: match t.right.as_deref() { Some("C") => 'C', Some("P") => 'P', None | Some("") => '\0', Some(other) => return Err(pyo3::exceptions::PyValueError::new_err(format!("right must be \"C\" or \"P\", got {other:?}"))) },
@@ -5005,7 +5002,6 @@ impl QuoteTickList {
50055002
ask: t.ask,
50065003
ask_condition: t.ask_condition,
50075004
date: t.date,
5008-
midpoint: t.midpoint,
50095005
expiration: t.has_contract_id().then_some(t.expiration),
50105006
strike: t.has_contract_id().then_some(t.strike),
50115007
right: if t.right == '\0' { None } else { Some(t.right.to_string()) },
@@ -5035,7 +5031,6 @@ impl QuoteTickList {
50355031
ask: t.ask,
50365032
ask_condition: t.ask_condition,
50375033
date: t.date,
5038-
midpoint: t.midpoint,
50395034
expiration: t.has_contract_id().then_some(t.expiration),
50405035
strike: t.has_contract_id().then_some(t.strike),
50415036
right: if t.right == '\0' { None } else { Some(t.right.to_string()) },
@@ -5111,7 +5106,6 @@ impl QuoteTickListIter {
51115106
ask: t.ask,
51125107
ask_condition: t.ask_condition,
51135108
date: t.date,
5114-
midpoint: t.midpoint,
51155109
expiration: t.has_contract_id().then_some(t.expiration),
51165110
strike: t.has_contract_id().then_some(t.strike),
51175111
right: if t.right == '\0' { None } else { Some(t.right.to_string()) },
@@ -7485,7 +7479,6 @@ pub(crate) fn quote_ticks_vec_to_pylist(py: Python<'_>, ticks: thetadatadx::Tick
74857479
ask: t.ask,
74867480
ask_condition: t.ask_condition,
74877481
date: t.date,
7488-
midpoint: t.midpoint,
74897482
expiration: t.has_contract_id().then_some(t.expiration),
74907483
strike: t.has_contract_id().then_some(t.strike),
74917484
right: if t.right == '\0' { None } else { Some(t.right.to_string()) },

thetadatadx-py/tests/test_dataframe_arrow.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -261,13 +261,6 @@ def test_ohlc_tick_to_arrow_schema():
261261
_assert_arrow_types_match(table, expected)
262262

263263

264-
def test_quote_tick_to_arrow_has_midpoint():
265-
tick = thetadatadx.QuoteTick(ms_of_day=34_200_000, bid=99.95, ask=100.05, midpoint=100.0)
266-
lst = thetadatadx.QuoteTickList([tick])
267-
table = lst.to_arrow()
268-
schema = {f.name: str(f.type) for f in table.schema}
269-
assert "midpoint" in schema
270-
assert schema["midpoint"] == "double"
271264

272265

273266
def test_trade_tick_to_arrow_schema():

thetadatadx-rs/benches/bench_tick.rs

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use criterion::{criterion_group, criterion_main, Criterion};
22
use std::hint::black_box;
33

4-
use thetadatadx::{OhlcTick, QuoteTick, TradeTick};
4+
use thetadatadx::{OhlcTick, TradeTick};
55

66
// ═══════════════════════════════════════════════════════════════════════════
77
// Tick operation benchmarks
@@ -39,30 +39,6 @@ fn bench_trade_tick_price_access(c: &mut Criterion) {
3939
});
4040
}
4141

42-
fn bench_quote_tick_midpoint(c: &mut Criterion) {
43-
let tick = QuoteTick {
44-
ms_of_day: 34_200_000,
45-
bid_size: 50,
46-
bid_exchange: 4,
47-
bid: 150.20,
48-
bid_condition: 1,
49-
ask_size: 30,
50-
ask_exchange: 4,
51-
ask: 150.30,
52-
ask_condition: 1,
53-
midpoint: 150.25,
54-
date: 20240315,
55-
expiration: 0,
56-
strike: 0.0,
57-
right: '\0',
58-
};
59-
c.bench_function("quote_tick_midpoint", |b| {
60-
b.iter(|| {
61-
black_box(black_box(&tick).midpoint);
62-
});
63-
});
64-
}
65-
6642
fn bench_ohlc_tick_all_prices(c: &mut Criterion) {
6743
let tick = OhlcTick {
6844
ms_of_day: 34_200_000,
@@ -93,7 +69,6 @@ fn bench_ohlc_tick_all_prices(c: &mut Criterion) {
9369
criterion_group!(
9470
tick_benches,
9571
bench_trade_tick_price_access,
96-
bench_quote_tick_midpoint,
9772
bench_ohlc_tick_all_prices,
9873
);
9974

thetadatadx-rs/build_support/ticks/parser.rs

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -166,9 +166,6 @@ fn generate_parser(out: &mut String, type_name: &str, def: &TickTypeDef) {
166166
out.push_str(" let _cid_right_idx = h.iter().position(|c| *c == \"right\");\n");
167167
}
168168

169-
// Check if this is QuoteTick (needs midpoint computation).
170-
let is_quote_tick = type_name == "QuoteTick";
171-
172169
out.push('\n');
173170

174171
// Phase 2: seed the output, then bulk-extract column by column in
@@ -187,9 +184,6 @@ fn generate_parser(out: &mut String, type_name: &str, def: &TickTypeDef) {
187184
out.push_str(" strike: 0.0,\n");
188185
out.push_str(" right: '\\0',\n");
189186
}
190-
if is_quote_tick {
191-
out.push_str(" midpoint: 0.0,\n");
192-
}
193187
out.push_str(" };\n");
194188
out.push_str(" rows.len()\n");
195189
out.push_str(" ];\n");
@@ -236,13 +230,6 @@ fn generate_parser(out: &mut String, type_name: &str, def: &TickTypeDef) {
236230
out.push_str(" }\n");
237231
}
238232

239-
if is_quote_tick {
240-
// QuoteTick gets midpoint computed from bid + ask.
241-
out.push_str(" for tick in ticks.iter_mut() {\n");
242-
out.push_str(" tick.midpoint = (tick.bid + tick.ask) / 2.0;\n");
243-
out.push_str(" }\n");
244-
}
245-
246233
out.push_str(" row_base += rows.len();\n");
247234
out.push_str(" }\n");
248235
out.push_str(" Ok(ticks)\n");
@@ -259,10 +246,9 @@ fn generate_parser(out: &mut String, type_name: &str, def: &TickTypeDef) {
259246
/// resolves several schema names to the same wire spelling), and the parser
260247
/// fills every such field from that one column. `present_columns_from` counts
261248
/// each physical header once (first-claim, exact matches before aliases) but
262-
/// exempts the two derived fields that are not their own physical column:
263-
/// `date` (the `YYYYMMDD` split of a `Timestamp` header, present whenever it
264-
/// resolves) and `QuoteTick.midpoint` (computed from `bid` + `ask`, present
265-
/// whenever both inputs are). The EOD wire sends `created`: the exact match
249+
/// exempts the derived `date` field that is not its own physical column:
250+
/// the `YYYYMMDD` split of a `Timestamp` header, present whenever it
251+
/// resolves. The EOD wire sends `created`: the exact match
266252
/// (`created` -> `created_ms_of_day`) claims the header, and `date` rides the
267253
/// same column via the exemption instead of being dropped.
268254
///
@@ -273,7 +259,6 @@ fn generate_parser(out: &mut String, type_name: &str, def: &TickTypeDef) {
273259
/// `is_open` and `status`, so its generated impl delegates to the matching
274260
/// calendar-specific presence table instead of the generic first-claim helper.
275261
fn generate_present_columns(out: &mut String, type_name: &str, def: &TickTypeDef) {
276-
let derive_midpoint = type_name == "QuoteTick";
277262
writeln!(out, "impl crate::columns::WireColumns for {type_name} {{").unwrap();
278263
out.push_str(" fn present_columns(headers: &[&str]) -> crate::columns::ColumnPresence {\n");
279264
if type_name == "CalendarDay" {
@@ -292,15 +277,15 @@ fn generate_present_columns(out: &mut String, type_name: &str, def: &TickTypeDef
292277
out.push_str(" ];\n");
293278
writeln!(
294279
out,
295-
" crate::columns::present_columns_from(headers, COLS, {}, {derive_midpoint})",
280+
" crate::columns::present_columns_from(headers, COLS, {})",
296281
def.contract_id,
297282
)
298283
.unwrap();
299284
}
300285
out.push_str(" }\n\n");
301286

302287
// all_columns(): the full-schema column set (schema columns + the
303-
// contract-id trio + the QuoteTick `midpoint` derived tail), matching
288+
// contract-id trio), matching
304289
// the column order the full `to_arrow` / `to_polars` builders emit.
305290
out.push_str(" fn all_columns() -> crate::columns::ColumnPresence {\n");
306291
out.push_str(" crate::columns::ColumnPresence::from_names([\n");
@@ -312,9 +297,6 @@ fn generate_present_columns(out: &mut String, type_name: &str, def: &TickTypeDef
312297
out.push_str(" \"strike\",\n");
313298
out.push_str(" \"right\",\n");
314299
}
315-
if type_name == "QuoteTick" {
316-
out.push_str(" \"midpoint\",\n");
317-
}
318300
out.push_str(" ])\n");
319301
out.push_str(" }\n");
320302
out.push_str("}\n\n");

thetadatadx-rs/build_support_bin/ticks/cpp.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ pub(super) fn render_cpp_tick_layout_asserts(schema: &Schema) -> String {
2626
continue;
2727
}
2828
let def = &schema.types[type_name];
29-
let (size, align) = tick_ffi_size_and_align(type_name, def);
29+
let (size, align) = tick_ffi_size_and_align(def);
3030
// C++ wrapper exposes the schema type name verbatim under
3131
// `thetadatadx::cxx`; the C mirror is `ThetaDataDx<TypeName>`. The wrapper
3232
// alias is `using <TypeName> = ThetaDataDx<TypeName>;` so `sizeof(alias)`
@@ -40,10 +40,9 @@ pub(super) fn render_cpp_tick_layout_asserts(schema: &Schema) -> String {
4040
.unwrap();
4141
writeln!(out, " \"{c_name} layout drifted from Rust\");").unwrap();
4242
// Per-field offset asserts. Same field set/order as the Rust
43-
// offset table (column tail + contract_id triple +
44-
// QuoteTick.midpoint). Field-offset drift can sneak past total-
43+
// offset table (column tail + contract_id triple). Field-offset drift can sneak past total-
4544
// size asserts when two same-size fields swap order.
46-
for (field, offset) in tick_ffi_offsets(type_name, def) {
45+
for (field, offset) in tick_ffi_offsets(def) {
4746
let field = c_field_ident(&field);
4847
writeln!(out, "static_assert(offsetof({alias}, {field}) == {offset},").unwrap();
4948
writeln!(

thetadatadx-rs/build_support_bin/ticks/layout.rs

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,11 @@
99
use super::schema::TickTypeDef;
1010

1111
/// `(field, byte_offset)` pairs for every column the parser fills, in
12-
/// declaration order, including the `contract_id` triple and
13-
/// `QuoteTick.midpoint` tail.
14-
pub(super) fn tick_ffi_offsets(type_name: &str, def: &TickTypeDef) -> Vec<(String, usize)> {
12+
/// declaration order, including the `contract_id` triple.
13+
pub(super) fn tick_ffi_offsets(def: &TickTypeDef) -> Vec<(String, usize)> {
1514
let mut offsets = Vec::new();
1615
let mut size = 0usize;
17-
for (field_name, field_type) in tick_ffi_fields(type_name, def) {
16+
for (field_name, field_type) in tick_ffi_fields(def) {
1817
let (field_size, field_align) = tick_ffi_field_layout(field_type);
1918
size = size.next_multiple_of(field_align);
2019
offsets.push((field_name.to_string(), size));
@@ -28,18 +27,18 @@ pub(super) fn tick_ffi_offsets(type_name: &str, def: &TickTypeDef) -> Vec<(Strin
2827
/// the max of the schema's `align` directive and every field's natural
2928
/// alignment, and size is rounded up to a multiple of that alignment to
3029
/// reproduce Rust's struct tail padding.
31-
pub(super) fn tick_ffi_size_and_align(type_name: &str, def: &TickTypeDef) -> (usize, usize) {
30+
pub(super) fn tick_ffi_size_and_align(def: &TickTypeDef) -> (usize, usize) {
3231
let mut size = 0usize;
3332
let mut struct_align = def.align.unwrap_or(1) as usize;
34-
for (_, field_type) in tick_ffi_fields(type_name, def) {
33+
for (_, field_type) in tick_ffi_fields(def) {
3534
let (field_size, field_align) = tick_ffi_field_layout(field_type);
3635
struct_align = struct_align.max(field_align);
3736
size = size.next_multiple_of(field_align) + field_size;
3837
}
3938
(size.next_multiple_of(struct_align), struct_align)
4039
}
4140

42-
fn tick_ffi_fields<'a>(type_name: &'a str, def: &'a TickTypeDef) -> Vec<(&'a str, &'a str)> {
41+
fn tick_ffi_fields(def: &TickTypeDef) -> Vec<(&str, &str)> {
4342
let mut fields = def
4443
.columns
4544
.iter()
@@ -50,9 +49,6 @@ fn tick_ffi_fields<'a>(type_name: &'a str, def: &'a TickTypeDef) -> Vec<(&'a str
5049
fields.push(("strike", "price"));
5150
fields.push(("right", "right"));
5251
}
53-
if type_name == "QuoteTick" {
54-
fields.push(("midpoint", "price"));
55-
}
5652
fields
5753
}
5854

0 commit comments

Comments
 (0)