From c1f5d3c96c50c1ca03377a7ed250ac4388abde3e Mon Sep 17 00:00:00 2001 From: preview Date: Sun, 12 Jul 2026 00:56:27 +0200 Subject: [PATCH 1/3] feat(quote)!: remove the derived QuoteTick.midpoint column MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- thetadatadx-cpp/include/thetadatadx.h | 6 +-- .../include/tick_layout_asserts.hpp.inc | 2 - thetadatadx-py/src/_generated/tick_arrow.rs | 8 ---- thetadatadx-py/src/_generated/tick_classes.rs | 11 +---- thetadatadx-py/tests/test_dataframe_arrow.py | 7 --- thetadatadx-rs/benches/bench_tick.rs | 27 +----------- thetadatadx-rs/build_support/ticks/parser.rs | 28 +++--------- thetadatadx-rs/build_support_bin/ticks/cpp.rs | 7 ++- .../build_support_bin/ticks/layout.rs | 16 +++---- .../build_support_bin/ticks/python_arrow.rs | 29 +----------- .../build_support_bin/ticks/python_classes.rs | 19 -------- .../build_support_bin/ticks/rust_frames.rs | 26 +++-------- .../build_support_bin/ticks/tdbe_structs.rs | 44 ++++++------------- .../build_support_bin/ticks/typescript.rs | 10 ----- thetadatadx-rs/src/columns.rs | 34 +++----------- thetadatadx-rs/src/frames/generated.rs | 20 --------- thetadatadx-rs/src/mdds/decode/tests.rs | 5 --- .../src/tdbe/types/generated/tick.rs | 4 +- .../types/generated/tick_layout_asserts.rs | 1 - .../tests/test_column_projection.rs | 28 ------------ thetadatadx-rs/tests/test_frames_arrow.rs | 25 ----------- thetadatadx-rs/tests/test_frames_polars.rs | 24 ---------- thetadatadx-rs/tick_schema.toml | 2 +- thetadatadx-ts/index.d.ts | 1 - thetadatadx-ts/src/_generated/tick_classes.rs | 3 -- tools/mcp/src/main.rs | 5 +-- tools/server/src/format.rs | 3 -- 27 files changed, 50 insertions(+), 345 deletions(-) diff --git a/thetadatadx-cpp/include/thetadatadx.h b/thetadatadx-cpp/include/thetadatadx.h index 69c7bae55..fd0feca41 100644 --- a/thetadatadx-cpp/include/thetadatadx.h +++ b/thetadatadx-cpp/include/thetadatadx.h @@ -603,7 +603,7 @@ THETADATADX_ALIGN64_BEGIN typedef struct { } ThetaDataDxIndexPriceAtTimeTick THETADATADX_ALIGN64_END; /* NBBO quote tick (*_history_quote): the bid/ask quote with sizes, - * exchanges, conditions, and a derived midpoint. */ + * exchanges, and conditions. */ THETADATADX_ALIGN64_BEGIN typedef struct { int32_t ms_of_day; int32_t bid_size; @@ -624,9 +624,7 @@ THETADATADX_ALIGN64_BEGIN typedef struct { * 'P' (80) for a put, 0 when contract identity is absent * (single-contract queries). Cast to char for display. */ uint32_t right; - /* 4 bytes padding before the double field */ - double midpoint; - uint8_t _tail_padding[40]; + uint8_t _tail_padding[52]; } ThetaDataDxQuoteTick THETADATADX_ALIGN64_END; /* Trade-with-quote tick (*_history_trade_quote): each trade print fused diff --git a/thetadatadx-cpp/include/tick_layout_asserts.hpp.inc b/thetadatadx-cpp/include/tick_layout_asserts.hpp.inc index 71c2561b4..614a3fdb8 100644 --- a/thetadatadx-cpp/include/tick_layout_asserts.hpp.inc +++ b/thetadatadx-cpp/include/tick_layout_asserts.hpp.inc @@ -462,8 +462,6 @@ static_assert(offsetof(QuoteTick, strike) == 64, "ThetaDataDxQuoteTick.strike offset drifted from Rust"); static_assert(offsetof(QuoteTick, right) == 72, "ThetaDataDxQuoteTick.right offset drifted from Rust"); -static_assert(offsetof(QuoteTick, midpoint) == 80, - "ThetaDataDxQuoteTick.midpoint offset drifted from Rust"); static_assert(sizeof(TradeGreeksAllTick) == 320 && alignof(TradeGreeksAllTick) == 64, "ThetaDataDxTradeGreeksAllTick layout drifted from Rust"); static_assert(offsetof(TradeGreeksAllTick, ms_of_day) == 0, diff --git a/thetadatadx-py/src/_generated/tick_arrow.rs b/thetadatadx-py/src/_generated/tick_arrow.rs index 9683294e0..2816a3b96 100644 --- a/thetadatadx-py/src/_generated/tick_arrow.rs +++ b/thetadatadx-py/src/_generated/tick_arrow.rs @@ -264,7 +264,6 @@ pub(crate) fn arrow_schema_for_qualname(qualname: &str) -> Option> { Field::new("ask", DataType::Float64, false), Field::new("ask_condition", DataType::Int32, false), Field::new("date", DataType::Int32, false), - Field::new("midpoint", DataType::Float64, false), Field::new("expiration", DataType::Int32, true), Field::new("strike", DataType::Float64, true), Field::new("right", DataType::Utf8, true), @@ -2310,7 +2309,6 @@ pub(crate) mod slice_arrow { let has_expiration = present.contains("expiration"); let has_strike = present.contains("strike"); let has_right = present.contains("right"); - let has_midpoint = present.contains("midpoint"); let mut col_ms_of_day: Vec = Vec::with_capacity(if has_ms_of_day { n } else { 0 }); let mut col_bid_size: Vec = Vec::with_capacity(if has_bid_size { n } else { 0 }); let mut col_bid_exchange: Vec = Vec::with_capacity(if has_bid_exchange { n } else { 0 }); @@ -2324,7 +2322,6 @@ pub(crate) mod slice_arrow { let mut col_expiration: Vec> = Vec::with_capacity(if has_expiration { n } else { 0 }); let mut col_strike: Vec> = Vec::with_capacity(if has_strike { n } else { 0 }); let mut col_right: Vec> = Vec::with_capacity(if has_right { n } else { 0 }); - let mut col_midpoint: Vec = Vec::with_capacity(if has_midpoint { n } else { 0 }); for t in ticks { if has_ms_of_day { col_ms_of_day.push(t.ms_of_day); } if has_bid_size { col_bid_size.push(t.bid_size); } @@ -2339,7 +2336,6 @@ pub(crate) mod slice_arrow { if has_expiration { col_expiration.push(t.has_contract_id().then_some(t.expiration)); } if has_strike { col_strike.push(t.has_contract_id().then_some(t.strike)); } if has_right { col_right.push(if t.right == '\0' { None } else { Some(t.right.to_string()) }); } - if has_midpoint { col_midpoint.push(t.midpoint); } } let mut fields: Vec = Vec::new(); let mut columns: Vec = Vec::new(); @@ -2402,10 +2398,6 @@ pub(crate) mod slice_arrow { fields.push(Field::new("right", DataType::Utf8, true)); columns.push(Arc::new(StringArray::from(col_right)) as ArrayRef); } - if has_midpoint { - fields.push(Field::new("midpoint", DataType::Float64, false)); - columns.push(Arc::new(Float64Array::from(col_midpoint)) as ArrayRef); - } 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())) } diff --git a/thetadatadx-py/src/_generated/tick_classes.rs b/thetadatadx-py/src/_generated/tick_classes.rs index c8f431da3..31476961c 100644 --- a/thetadatadx-py/src/_generated/tick_classes.rs +++ b/thetadatadx-py/src/_generated/tick_classes.rs @@ -991,7 +991,6 @@ pub(crate) struct QuoteTick { #[pyo3(get)] pub ask: f64, #[pyo3(get)] pub ask_condition: i32, #[pyo3(get)] pub date: i32, - #[pyo3(get)] pub midpoint: f64, #[pyo3(get)] pub expiration: Option, #[pyo3(get)] pub strike: Option, #[pyo3(get)] pub right: Option, @@ -999,8 +998,8 @@ pub(crate) struct QuoteTick { #[pymethods] impl QuoteTick { #[new] - #[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))] - 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, strike: Option, right: Option) -> Self { + #[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))] + 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, strike: Option, right: Option) -> Self { Self { ms_of_day, bid_size, @@ -1012,7 +1011,6 @@ impl QuoteTick { ask, ask_condition, date, - midpoint, expiration, strike, right, @@ -4940,7 +4938,6 @@ impl QuoteTickList { ask: t.ask, ask_condition: t.ask_condition, date: t.date, - midpoint: t.midpoint, expiration: t.expiration.unwrap_or(0), strike: t.strike.unwrap_or(0.0), 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 { ask: t.ask, ask_condition: t.ask_condition, date: t.date, - midpoint: t.midpoint, expiration: t.has_contract_id().then_some(t.expiration), strike: t.has_contract_id().then_some(t.strike), right: if t.right == '\0' { None } else { Some(t.right.to_string()) }, @@ -5035,7 +5031,6 @@ impl QuoteTickList { ask: t.ask, ask_condition: t.ask_condition, date: t.date, - midpoint: t.midpoint, expiration: t.has_contract_id().then_some(t.expiration), strike: t.has_contract_id().then_some(t.strike), right: if t.right == '\0' { None } else { Some(t.right.to_string()) }, @@ -5111,7 +5106,6 @@ impl QuoteTickListIter { ask: t.ask, ask_condition: t.ask_condition, date: t.date, - midpoint: t.midpoint, expiration: t.has_contract_id().then_some(t.expiration), strike: t.has_contract_id().then_some(t.strike), 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 ask: t.ask, ask_condition: t.ask_condition, date: t.date, - midpoint: t.midpoint, expiration: t.has_contract_id().then_some(t.expiration), strike: t.has_contract_id().then_some(t.strike), right: if t.right == '\0' { None } else { Some(t.right.to_string()) }, diff --git a/thetadatadx-py/tests/test_dataframe_arrow.py b/thetadatadx-py/tests/test_dataframe_arrow.py index 704d2cae0..d25f7cdac 100644 --- a/thetadatadx-py/tests/test_dataframe_arrow.py +++ b/thetadatadx-py/tests/test_dataframe_arrow.py @@ -261,13 +261,6 @@ def test_ohlc_tick_to_arrow_schema(): _assert_arrow_types_match(table, expected) -def test_quote_tick_to_arrow_has_midpoint(): - tick = thetadatadx.QuoteTick(ms_of_day=34_200_000, bid=99.95, ask=100.05, midpoint=100.0) - lst = thetadatadx.QuoteTickList([tick]) - table = lst.to_arrow() - schema = {f.name: str(f.type) for f in table.schema} - assert "midpoint" in schema - assert schema["midpoint"] == "double" def test_trade_tick_to_arrow_schema(): diff --git a/thetadatadx-rs/benches/bench_tick.rs b/thetadatadx-rs/benches/bench_tick.rs index e78570f91..5b3812f9b 100644 --- a/thetadatadx-rs/benches/bench_tick.rs +++ b/thetadatadx-rs/benches/bench_tick.rs @@ -1,7 +1,7 @@ use criterion::{criterion_group, criterion_main, Criterion}; use std::hint::black_box; -use thetadatadx::{OhlcTick, QuoteTick, TradeTick}; +use thetadatadx::{OhlcTick, TradeTick}; // ═══════════════════════════════════════════════════════════════════════════ // Tick operation benchmarks @@ -39,30 +39,6 @@ fn bench_trade_tick_price_access(c: &mut Criterion) { }); } -fn bench_quote_tick_midpoint(c: &mut Criterion) { - let tick = QuoteTick { - ms_of_day: 34_200_000, - bid_size: 50, - bid_exchange: 4, - bid: 150.20, - bid_condition: 1, - ask_size: 30, - ask_exchange: 4, - ask: 150.30, - ask_condition: 1, - midpoint: 150.25, - date: 20240315, - expiration: 0, - strike: 0.0, - right: '\0', - }; - c.bench_function("quote_tick_midpoint", |b| { - b.iter(|| { - black_box(black_box(&tick).midpoint); - }); - }); -} - fn bench_ohlc_tick_all_prices(c: &mut Criterion) { let tick = OhlcTick { ms_of_day: 34_200_000, @@ -93,7 +69,6 @@ fn bench_ohlc_tick_all_prices(c: &mut Criterion) { criterion_group!( tick_benches, bench_trade_tick_price_access, - bench_quote_tick_midpoint, bench_ohlc_tick_all_prices, ); diff --git a/thetadatadx-rs/build_support/ticks/parser.rs b/thetadatadx-rs/build_support/ticks/parser.rs index 1f40a15f1..290414158 100644 --- a/thetadatadx-rs/build_support/ticks/parser.rs +++ b/thetadatadx-rs/build_support/ticks/parser.rs @@ -166,9 +166,6 @@ fn generate_parser(out: &mut String, type_name: &str, def: &TickTypeDef) { out.push_str(" let _cid_right_idx = h.iter().position(|c| *c == \"right\");\n"); } - // Check if this is QuoteTick (needs midpoint computation). - let is_quote_tick = type_name == "QuoteTick"; - out.push('\n'); // 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) { out.push_str(" strike: 0.0,\n"); out.push_str(" right: '\\0',\n"); } - if is_quote_tick { - out.push_str(" midpoint: 0.0,\n"); - } out.push_str(" };\n"); out.push_str(" rows.len()\n"); out.push_str(" ];\n"); @@ -236,13 +230,6 @@ fn generate_parser(out: &mut String, type_name: &str, def: &TickTypeDef) { out.push_str(" }\n"); } - if is_quote_tick { - // QuoteTick gets midpoint computed from bid + ask. - out.push_str(" for tick in ticks.iter_mut() {\n"); - out.push_str(" tick.midpoint = (tick.bid + tick.ask) / 2.0;\n"); - out.push_str(" }\n"); - } - out.push_str(" row_base += rows.len();\n"); out.push_str(" }\n"); out.push_str(" Ok(ticks)\n"); @@ -259,10 +246,9 @@ fn generate_parser(out: &mut String, type_name: &str, def: &TickTypeDef) { /// resolves several schema names to the same wire spelling), and the parser /// fills every such field from that one column. `present_columns_from` counts /// each physical header once (first-claim, exact matches before aliases) but -/// exempts the two derived fields that are not their own physical column: -/// `date` (the `YYYYMMDD` split of a `Timestamp` header, present whenever it -/// resolves) and `QuoteTick.midpoint` (computed from `bid` + `ask`, present -/// whenever both inputs are). The EOD wire sends `created`: the exact match +/// exempts the derived `date` field that is not its own physical column: +/// the `YYYYMMDD` split of a `Timestamp` header, present whenever it +/// resolves. The EOD wire sends `created`: the exact match /// (`created` -> `created_ms_of_day`) claims the header, and `date` rides the /// same column via the exemption instead of being dropped. /// @@ -273,7 +259,6 @@ fn generate_parser(out: &mut String, type_name: &str, def: &TickTypeDef) { /// `is_open` and `status`, so its generated impl delegates to the matching /// calendar-specific presence table instead of the generic first-claim helper. fn generate_present_columns(out: &mut String, type_name: &str, def: &TickTypeDef) { - let derive_midpoint = type_name == "QuoteTick"; writeln!(out, "impl crate::columns::WireColumns for {type_name} {{").unwrap(); out.push_str(" fn present_columns(headers: &[&str]) -> crate::columns::ColumnPresence {\n"); if type_name == "CalendarDay" { @@ -292,7 +277,7 @@ fn generate_present_columns(out: &mut String, type_name: &str, def: &TickTypeDef out.push_str(" ];\n"); writeln!( out, - " crate::columns::present_columns_from(headers, COLS, {}, {derive_midpoint})", + " crate::columns::present_columns_from(headers, COLS, {})", def.contract_id, ) .unwrap(); @@ -300,7 +285,7 @@ fn generate_present_columns(out: &mut String, type_name: &str, def: &TickTypeDef out.push_str(" }\n\n"); // all_columns(): the full-schema column set (schema columns + the - // contract-id trio + the QuoteTick `midpoint` derived tail), matching + // contract-id trio), matching // the column order the full `to_arrow` / `to_polars` builders emit. out.push_str(" fn all_columns() -> crate::columns::ColumnPresence {\n"); 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 out.push_str(" \"strike\",\n"); out.push_str(" \"right\",\n"); } - if type_name == "QuoteTick" { - out.push_str(" \"midpoint\",\n"); - } out.push_str(" ])\n"); out.push_str(" }\n"); out.push_str("}\n\n"); diff --git a/thetadatadx-rs/build_support_bin/ticks/cpp.rs b/thetadatadx-rs/build_support_bin/ticks/cpp.rs index b1793e074..f8bc201ce 100644 --- a/thetadatadx-rs/build_support_bin/ticks/cpp.rs +++ b/thetadatadx-rs/build_support_bin/ticks/cpp.rs @@ -26,7 +26,7 @@ pub(super) fn render_cpp_tick_layout_asserts(schema: &Schema) -> String { continue; } let def = &schema.types[type_name]; - let (size, align) = tick_ffi_size_and_align(type_name, def); + let (size, align) = tick_ffi_size_and_align(def); // C++ wrapper exposes the schema type name verbatim under // `thetadatadx::cxx`; the C mirror is `ThetaDataDx`. The wrapper // alias is `using = ThetaDataDx;` so `sizeof(alias)` @@ -40,10 +40,9 @@ pub(super) fn render_cpp_tick_layout_asserts(schema: &Schema) -> String { .unwrap(); writeln!(out, " \"{c_name} layout drifted from Rust\");").unwrap(); // Per-field offset asserts. Same field set/order as the Rust - // offset table (column tail + contract_id triple + - // QuoteTick.midpoint). Field-offset drift can sneak past total- + // offset table (column tail + contract_id triple). Field-offset drift can sneak past total- // size asserts when two same-size fields swap order. - for (field, offset) in tick_ffi_offsets(type_name, def) { + for (field, offset) in tick_ffi_offsets(def) { let field = c_field_ident(&field); writeln!(out, "static_assert(offsetof({alias}, {field}) == {offset},").unwrap(); writeln!( diff --git a/thetadatadx-rs/build_support_bin/ticks/layout.rs b/thetadatadx-rs/build_support_bin/ticks/layout.rs index e7dc1b632..3a2cefa7f 100644 --- a/thetadatadx-rs/build_support_bin/ticks/layout.rs +++ b/thetadatadx-rs/build_support_bin/ticks/layout.rs @@ -9,12 +9,11 @@ use super::schema::TickTypeDef; /// `(field, byte_offset)` pairs for every column the parser fills, in -/// declaration order, including the `contract_id` triple and -/// `QuoteTick.midpoint` tail. -pub(super) fn tick_ffi_offsets(type_name: &str, def: &TickTypeDef) -> Vec<(String, usize)> { +/// declaration order, including the `contract_id` triple. +pub(super) fn tick_ffi_offsets(def: &TickTypeDef) -> Vec<(String, usize)> { let mut offsets = Vec::new(); let mut size = 0usize; - for (field_name, field_type) in tick_ffi_fields(type_name, def) { + for (field_name, field_type) in tick_ffi_fields(def) { let (field_size, field_align) = tick_ffi_field_layout(field_type); size = size.next_multiple_of(field_align); offsets.push((field_name.to_string(), size)); @@ -28,10 +27,10 @@ pub(super) fn tick_ffi_offsets(type_name: &str, def: &TickTypeDef) -> Vec<(Strin /// the max of the schema's `align` directive and every field's natural /// alignment, and size is rounded up to a multiple of that alignment to /// reproduce Rust's struct tail padding. -pub(super) fn tick_ffi_size_and_align(type_name: &str, def: &TickTypeDef) -> (usize, usize) { +pub(super) fn tick_ffi_size_and_align(def: &TickTypeDef) -> (usize, usize) { let mut size = 0usize; let mut struct_align = def.align.unwrap_or(1) as usize; - for (_, field_type) in tick_ffi_fields(type_name, def) { + for (_, field_type) in tick_ffi_fields(def) { let (field_size, field_align) = tick_ffi_field_layout(field_type); struct_align = struct_align.max(field_align); size = size.next_multiple_of(field_align) + field_size; @@ -39,7 +38,7 @@ pub(super) fn tick_ffi_size_and_align(type_name: &str, def: &TickTypeDef) -> (us (size.next_multiple_of(struct_align), struct_align) } -fn tick_ffi_fields<'a>(type_name: &'a str, def: &'a TickTypeDef) -> Vec<(&'a str, &'a str)> { +fn tick_ffi_fields(def: &TickTypeDef) -> Vec<(&str, &str)> { let mut fields = def .columns .iter() @@ -50,9 +49,6 @@ fn tick_ffi_fields<'a>(type_name: &'a str, def: &'a TickTypeDef) -> Vec<(&'a str fields.push(("strike", "price")); fields.push(("right", "right")); } - if type_name == "QuoteTick" { - fields.push(("midpoint", "price")); - } fields } diff --git a/thetadatadx-rs/build_support_bin/ticks/python_arrow.rs b/thetadatadx-rs/build_support_bin/ticks/python_arrow.rs index af78717bb..23256334b 100644 --- a/thetadatadx-rs/build_support_bin/ticks/python_arrow.rs +++ b/thetadatadx-rs/build_support_bin/ticks/python_arrow.rs @@ -215,7 +215,6 @@ pub(super) fn column_push_expr(column_type: &str, field: &str) -> String { fn render_python_slice_reader(type_name: &str, def: &TickTypeDef) -> String { let mut out = String::new(); let fn_name = python_slice_reader_fn_name(type_name); - let is_quote_tick = type_name == "QuoteTick"; let is_contract = def.contract_id; writeln!( @@ -245,9 +244,6 @@ fn render_python_slice_reader(type_name: &str, def: &TickTypeDef) -> String { .unwrap(); column_decls.push((column.field.clone(), column.r#type.clone())); } - if is_quote_tick { - out.push_str(" let mut col_midpoint: Vec = Vec::with_capacity(n);\n"); - } if is_contract { // Absent contract identity (single-contract queries) buffers // as Arrow nulls — the columnar mirror of the pyclass `None` @@ -266,9 +262,6 @@ fn render_python_slice_reader(type_name: &str, def: &TickTypeDef) -> String { ) .unwrap(); } - if is_quote_tick { - out.push_str(" col_midpoint.push(t.midpoint);\n"); - } if is_contract { out.push_str(" col_expiration.push(t.has_contract_id().then_some(t.expiration));\n"); out.push_str(" col_strike.push(t.has_contract_id().then_some(t.strike));\n"); @@ -289,9 +282,6 @@ fn render_python_slice_reader(type_name: &str, def: &TickTypeDef) -> String { ) .unwrap(); } - if is_quote_tick { - out.push_str(" Arc::new(Float64Array::from(col_midpoint)) as ArrayRef,\n"); - } if is_contract { out.push_str(" Arc::new(Int32Array::from(col_expiration)) as ArrayRef,\n"); out.push_str(" Arc::new(Float64Array::from(col_strike)) as ArrayRef,\n"); @@ -379,7 +369,6 @@ fn render_python_slice_public_helper(type_name: &str) -> String { /// One projectable Arrow column: schema field name, buffer element type, /// Arrow `DataType` expr, array constructor, push expression, nullability. -/// The contract-id trio and the `QuoteTick.midpoint` tail are modelled as /// synthetic columns so the projected reader shares one loop — the same /// column set (and order) the Rust `to_arrow_projected` builder emits. struct ProjCol { @@ -391,7 +380,7 @@ struct ProjCol { nullable: bool, } -fn projected_columns(type_name: &str, def: &TickTypeDef) -> Vec { +fn projected_columns(def: &TickTypeDef) -> Vec { let mut cols: Vec = def .columns .iter() @@ -430,16 +419,6 @@ fn projected_columns(type_name: &str, def: &TickTypeDef) -> Vec { nullable: true, }); } - if type_name == "QuoteTick" { - cols.push(ProjCol { - name: "midpoint".into(), - buf_ty: "f64", - data_type: "DataType::Float64", - ctor: "Float64Array", - push: "t.midpoint".into(), - nullable: false, - }); - } cols } @@ -456,7 +435,7 @@ fn render_python_slice_reader_projected(type_name: &str, def: &TickTypeDef) -> S ) .unwrap(); out.push_str(" let n = ticks.len();\n"); - let cols = projected_columns(type_name, def); + let cols = projected_columns(def); // SINGLE pass: presence resolved once (`has_*`), absent columns keep a // zero-capacity buffer never pushed to, so one `for t in ticks` loop // fills only the present columns rather than re-scanning the slice per @@ -622,7 +601,6 @@ fn render_python_arrow_schema_map(schema: &Schema) -> String { for type_name in sorted_type_names(schema) { let def = &schema.types[type_name]; let class = pyclass_name(type_name); - let is_quote_tick = type_name == "QuoteTick"; writeln!( out, " \"{class}\" => Some(Arc::new(Schema::new(vec![" @@ -643,9 +621,6 @@ fn render_python_arrow_schema_map(schema: &Schema) -> String { ) .unwrap(); } - if is_quote_tick { - out.push_str(" Field::new(\"midpoint\", DataType::Float64, false),\n"); - } if def.contract_id { // Nullable: contract identity is absent (Arrow null) on // single-contract queries — mirrors the pyclass `None`. diff --git a/thetadatadx-rs/build_support_bin/ticks/python_classes.rs b/thetadatadx-rs/build_support_bin/ticks/python_classes.rs index 9d7c60383..e08683698 100644 --- a/thetadatadx-rs/build_support_bin/ticks/python_classes.rs +++ b/thetadatadx-rs/build_support_bin/ticks/python_classes.rs @@ -353,7 +353,6 @@ fn slice_arrow_converter<'a>(schema: &'a Schema, type_name: &str) -> &'a str { /// Strip " -- N fields[...]." from the first doc line so the rendered /// comment reflects the actual field set once `contract_id` and -/// `QuoteTick` midpoint get appended to `def.columns`. Words stay stable /// even when a schema edit adds/removes a column, so we drop the numeric /// count instead of recomputing it per surface. pub(super) fn strip_field_count_from_doc(doc: &str) -> String { @@ -411,7 +410,6 @@ struct ReprField { fn render_python_tick_class_struct(type_name: &str, def: &TickTypeDef) -> String { let mut out = String::new(); let class = pyclass_name(type_name); - let is_quote_tick = type_name == "QuoteTick"; let doc_text = if def.doc.is_empty() { format!("Typed {type_name} record.") } else { @@ -461,9 +459,6 @@ fn render_python_tick_class_struct(type_name: &str, def: &TickTypeDef) -> String ) .unwrap(); } - if is_quote_tick { - out.push_str(" #[pyo3(get)] pub midpoint: f64,\n"); - } if def.contract_id { // Contract identity is populated on wildcard queries only — // absent identity is `None`, matching the streaming @@ -617,13 +612,6 @@ fn render_python_tick_class_new(type_name: &str, def: &TickTypeDef) -> String { default, }); } - if type_name == "QuoteTick" { - fields.push(CtorField { - name: "midpoint".to_string(), - rust_type: "f64", - default: "0.0f64", - }); - } if def.contract_id { fields.push(CtorField { name: "expiration".to_string(), @@ -727,9 +715,6 @@ fn pyclass_to_tick_expr( } } } - if type_name == "QuoteTick" { - writeln!(out, "{indent} midpoint: {source_expr}.midpoint,").unwrap(); - } if def.contract_id { // `None` is the absence convention on the Python surface; the // struct's documented fills (0 / 0.0 / '\0') are the reverse @@ -769,7 +754,6 @@ fn pyclass_from_tick_expr( indent: &str, ) { let class = pyclass_name(type_name); - let is_quote_tick = type_name == "QuoteTick"; writeln!(out, "{indent}{class} {{").unwrap(); for column in &def.columns { let field = &column.field; @@ -801,9 +785,6 @@ fn pyclass_from_tick_expr( } } } - if is_quote_tick { - writeln!(out, "{indent} midpoint: {source_expr}.midpoint,").unwrap(); - } if def.contract_id { // Absent contract identity (single-contract queries) is `None` // on the Python surface; the struct's documented fills (0 / diff --git a/thetadatadx-rs/build_support_bin/ticks/rust_frames.rs b/thetadatadx-rs/build_support_bin/ticks/rust_frames.rs index 3c38d6147..546e5ebfb 100644 --- a/thetadatadx-rs/build_support_bin/ticks/rust_frames.rs +++ b/thetadatadx-rs/build_support_bin/ticks/rust_frames.rs @@ -110,13 +110,11 @@ pub(super) fn tick_has_symbol_column(def: &TickTypeDef) -> bool { } /// The ordered emittable columns for a tick type: its schema columns, -/// then the contract-identity trio when `contract_id = true`, then the -/// `QuoteTick.midpoint` derived tail. Mirrors the column order the Python -/// slice_arrow emitter and the tick struct use, so all surfaces agree. -/// -/// (`IvTick.midpoint` is a real wire column and appears via the schema -/// columns, not this synthetic tail.) -fn emit_columns(type_name: &str, def: &TickTypeDef) -> Vec { +/// then the contract-identity trio when `contract_id = true`. Mirrors the +/// column order the Python slice_arrow emitter and the tick struct use, so +/// all surfaces agree. (`IvTick.midpoint` is a real wire column and appears +/// via the schema columns.) +fn emit_columns(def: &TickTypeDef) -> Vec { let mut cols: Vec = Vec::new(); for column in &def.columns { cols.push(EmitColumn { @@ -154,16 +152,6 @@ fn emit_columns(type_name: &str, def: &TickTypeDef) -> Vec { nullable: true, }); } - if type_name == "QuoteTick" { - cols.push(EmitColumn { - name: "midpoint".to_string(), - buf_ty: "f64".to_string(), - data_type: "DataType::Float64", - ctor: "Float64Array", - push: "t.midpoint".to_string(), - nullable: false, - }); - } cols } @@ -175,7 +163,7 @@ fn emit_columns(type_name: &str, def: &TickTypeDef) -> Vec { // contract identity → Arrow null). fn render_arrow_impl(type_name: &str, def: &TickTypeDef) -> String { - let cols = emit_columns(type_name, def); + let cols = emit_columns(def); let has_symbol_field = tick_has_symbol_column(def); let mut out = String::new(); @@ -348,7 +336,7 @@ fn render_arrow_impl(type_name: &str, def: &TickTypeDef) -> String { // matches the Arrow path 1:1. fn render_polars_impl(type_name: &str, def: &TickTypeDef) -> String { - let cols = emit_columns(type_name, def); + let cols = emit_columns(def); let has_symbol_field = tick_has_symbol_column(def); let mut out = String::new(); diff --git a/thetadatadx-rs/build_support_bin/ticks/tdbe_structs.rs b/thetadatadx-rs/build_support_bin/ticks/tdbe_structs.rs index 82f23f49f..6cda46ea6 100644 --- a/thetadatadx-rs/build_support_bin/ticks/tdbe_structs.rs +++ b/thetadatadx-rs/build_support_bin/ticks/tdbe_structs.rs @@ -9,16 +9,13 @@ //! this generated file so every `Vec` consumer in the workspace picks //! up new tick types automatically. //! -//! Layout decisions (alignment, copy-derive, contract-id field tail, -//! `QuoteTick.midpoint`) are taken straight from the schema: +//! Layout decisions (alignment, copy-derive, contract-id field tail) +//! are taken straight from the schema: //! //! * `align = N` -> `#[repr(C, align(N))]` (omitted for no align). //! * `copy = true/false` -> `#[derive(Clone, Copy)]` vs `#[derive(Clone)]`. //! * `contract_id = true` -> append `expiration: i32`, `strike: f64`, //! `right: i32` after the schema columns. -//! * `QuoteTick` -> append `midpoint: f64`. The parser computes it from -//! `(bid + ask) / 2.0`; the field exists on the struct so consumers -//! don't recompute. use std::fmt::Write as _; @@ -117,7 +114,7 @@ pub(super) fn timestamp_accessor_fields(def: &TickTypeDef) -> Vec<(String, Strin /// * total `size_of::()` (catches tail-padding drift), /// * `align_of::()` (catches `align(N)` drift), /// * `offset_of!(T, field)` for every column the parser fills, plus -/// the `contract_id` triple and `QuoteTick.midpoint` tail. This is the +/// the `contract_id` triple. This is the /// ABI that the C-mirror in `thetadatadx-cpp/include/thetadatadx.h` and the /// layout-assert `*.hpp.inc` all index into via `offsetof()`. /// @@ -152,7 +149,7 @@ pub(super) fn render_tdbe_layout_asserts(schema: &Schema) -> String { continue; } let def = &schema.types[type_name]; - let (size, align) = tick_ffi_size_and_align(type_name, def); + let (size, align) = tick_ffi_size_and_align(def); let snake = type_name.to_snake_case(); writeln!(out, " #[test]").unwrap(); @@ -163,7 +160,7 @@ pub(super) fn render_tdbe_layout_asserts(schema: &Schema) -> String { " assert_eq!(align_of::<{type_name}>(), {align});" ) .unwrap(); - for (field, offset) in tick_ffi_offsets(type_name, def) { + for (field, offset) in tick_ffi_offsets(def) { let field = rust_field_ident(&field); writeln!( out, @@ -184,9 +181,8 @@ fn render_one_struct(type_name: &str, def: &TickTypeDef) -> String { // Doc comment derived from the schema. Multi-line docs (e.g. // `GreeksTick`) preserve their per-line `///` shape. The field-count // phrase is recomputed from the actual field set the struct emits - // (schema columns plus the `contract_id` tail and `QuoteTick` - // midpoint), so the count is correct by construction rather than - // hand-maintained in the schema prose. + // (schema columns plus the `contract_id` tail), so the count is correct + // by construction rather than hand-maintained in the schema prose. let doc = if def.doc.is_empty() { format!("`{type_name}` tick.") } else { @@ -232,9 +228,8 @@ fn render_one_struct(type_name: &str, def: &TickTypeDef) -> String { } // Field order MUST match the legacy `tick.rs` layout for FFI ABI - // compatibility: `contract_id` triple comes BEFORE `QuoteTick.midpoint`. - // Reordering would silently shift offsets for already-compiled C / C++ - // consumers even when total `size_of` stays the same. + // compatibility. Reordering would silently shift offsets for + // already-compiled C / C++ consumers even when total `size_of` stays the same. if def.contract_id { out.push_str(" /// Contract expiration (`YYYYMMDD`). Populated on wildcard queries, 0 otherwise.\n"); out.push_str(" pub expiration: i32,\n"); @@ -244,28 +239,19 @@ fn render_one_struct(type_name: &str, def: &TickTypeDef) -> String { out.push_str(" pub right: char,\n"); } - if type_name == "QuoteTick" { - out.push_str(" /// Pre-computed midpoint: `(bid + ask) / 2.0`.\n"); - out.push_str(" pub midpoint: f64,\n"); - } - out.push_str("}\n"); out } /// Field count rendered in the `-- N fields` doc phrase: every schema /// column plus the `contract_id` tail (`expiration` / `strike` / `right`). -/// `QuoteTick`'s computed `midpoint` is excluded here because its doc -/// phrase already names it separately (`-- N fields + midpoint`), so -/// counting it in `N` would double-count. The total still tracks the field -/// list by construction — only the named suffix is kept out of `N`. fn struct_field_count(_type_name: &str, def: &TickTypeDef) -> usize { def.columns.len() + usize::from(def.contract_id) * 3 } -/// Rewrite the `-- N fields[ + midpoint]` phrase on the first doc line to -/// the supplied `count`, preserving any suffix wording (e.g. ` + midpoint`, -/// trailing summary sentence). When the first line carries no `-- N` +/// Rewrite the `-- N fields` phrase on the first doc line to the supplied +/// `count`, preserving any trailing summary sentence. When the first line +/// carries no `-- N` /// phrase the doc is returned unchanged — the count is informational, not /// mandatory, so types that never declared it stay as authored. fn with_field_count(doc: &str, count: usize) -> String { @@ -287,10 +273,8 @@ fn with_field_count(doc: &str, count: usize) -> String { /// Replace the numeric run immediately after ` -- ` on a single line with /// `count`, keeping the surrounding text. `"OHLC tick -- 9 fields. ..."` -/// with `count = 12` becomes `"OHLC tick -- 12 fields. ..."`; -/// `"Quote tick -- 10 fields + midpoint. ..."` with `count = 12` becomes -/// `"Quote tick -- 12 fields + midpoint. ..."`. Lines without a leading -/// digit after ` -- ` are returned unchanged. +/// with `count = 12` becomes `"OHLC tick -- 12 fields. ..."`. Lines +/// without a leading digit after ` -- ` are returned unchanged. fn rewrite_count_phrase(line: &str, count: usize) -> String { let Some(dash) = line.find(" -- ") else { return line.to_string(); diff --git a/thetadatadx-rs/build_support_bin/ticks/typescript.rs b/thetadatadx-rs/build_support_bin/ticks/typescript.rs index b09387015..17d25683c 100644 --- a/thetadatadx-rs/build_support_bin/ticks/typescript.rs +++ b/thetadatadx-rs/build_support_bin/ticks/typescript.rs @@ -314,9 +314,6 @@ fn render_ts_arrow_reconstruct_rows(type_name: &str, def: &TickTypeDef) -> Strin let expr = ts_arrow_reconstruct_expr(&column.r#type, &field); writeln!(out, " {field}: {expr},").unwrap(); } - if type_name == "QuoteTick" { - out.push_str(" midpoint: r.midpoint,\n"); - } if def.contract_id { // Inverse of the factory's `has_contract_id().then_some(..)`: the // absent JS `undefined`/`null` round-trips back to the struct's @@ -405,7 +402,6 @@ fn ts_arrow_reconstruct_is_fallible(def: &TickTypeDef) -> bool { fn render_ts_tick_class_struct(type_name: &str, def: &TickTypeDef) -> String { let mut out = String::new(); - let is_quote_tick = type_name == "QuoteTick"; // Strip " -- N fields[...]." from the first doc line — see // `strip_field_count_from_doc` for rationale. Shared with the Python // emitter so both surfaces drift together. @@ -430,9 +426,6 @@ fn render_ts_tick_class_struct(type_name: &str, def: &TickTypeDef) -> String { ) .unwrap(); } - if is_quote_tick { - out.push_str(" pub midpoint: f64,\n"); - } if def.contract_id { // Contract identity is populated on wildcard queries only — // absent identity is undefined/null on the JS surface, @@ -515,9 +508,6 @@ fn render_ts_tick_class_factory(schema: &Schema, type_name: &str, def: &TickType ) .unwrap(); } - if type_name == "QuoteTick" { - out.push_str(" midpoint: t.midpoint,\n"); - } if def.contract_id { // Absent contract identity (single-contract queries) crosses // to JS as undefined/null — the documented absence convention. diff --git a/thetadatadx-rs/src/columns.rs b/thetadatadx-rs/src/columns.rs index 680d95dcc..79fc5912c 100644 --- a/thetadatadx-rs/src/columns.rs +++ b/thetadatadx-rs/src/columns.rs @@ -167,14 +167,11 @@ impl ColumnPresence { /// claimed the shared `Timestamp` header, `date` is that column's derived /// `YYYYMMDD` sibling and rides it without a second claim. Either way it /// is present whenever it resolves. -/// * `midpoint` (`QuoteTick`) is computed at decode from `bid` + `ask` and -/// is never a wire header, so it is present exactly when both inputs are. #[must_use] pub fn present_columns_from( headers: &[&str], schema_columns: &[(&'static str, &'static str)], contract_id: bool, - derive_midpoint: bool, ) -> ColumnPresence { use crate::mdds::decode::headers::find_header; @@ -236,11 +233,6 @@ pub fn present_columns_from( } } } - // `midpoint` rides whenever both its inputs do. - if derive_midpoint && present.contains(&"bid") && present.contains(&"ask") { - present.push("midpoint"); - } - ColumnPresence::from_names(present) } @@ -469,7 +461,7 @@ mod tests { ("open", "open"), ("date", "date"), ]; - let p = present_columns_from(&["created", "open"], COLS, false, false); + let p = present_columns_from(&["created", "open"], COLS, false); assert!(p.contains("created_ms_of_day")); assert!(p.contains("open")); assert!( @@ -483,27 +475,11 @@ mod tests { #[test] fn present_date_standalone_header() { const COLS: &[(&str, &str)] = &[("ms_of_day", "ms_of_day"), ("date", "date")]; - let p = present_columns_from(&["timestamp", "date"], COLS, false, false); + let p = present_columns_from(&["timestamp", "date"], COLS, false); assert!(p.contains("ms_of_day")); assert!(p.contains("date")); } - /// `midpoint` is present exactly when both `bid` and `ask` are. - #[test] - fn present_midpoint_keys_on_bid_and_ask() { - const COLS: &[(&str, &str)] = &[ - ("ms_of_day", "ms_of_day"), - ("bid", "bid"), - ("ask", "ask"), - ("date", "date"), - ]; - let with = present_columns_from(&["ms_of_day", "bid", "ask", "date"], COLS, false, true); - assert!(with.contains("midpoint")); - - let without = present_columns_from(&["ms_of_day", "date"], COLS, false, true); - assert!(!without.contains("midpoint")); - } - /// Two-pass claiming: a column that names a header exactly claims it even /// when an alias-matching column is listed first, so ordering is not a /// latent invariant (`root` aliases to `symbol`; the exact `symbol` column @@ -511,7 +487,7 @@ mod tests { #[test] fn present_exact_match_beats_earlier_alias() { const COLS: &[(&str, &str)] = &[("root", "root"), ("symbol", "symbol")]; - let p = present_columns_from(&["symbol"], COLS, false, false); + let p = present_columns_from(&["symbol"], COLS, false); assert!( p.contains("symbol"), "exact `symbol` column claims the header" @@ -525,7 +501,7 @@ mod tests { #[test] fn present_date_is_primary_claimant_when_no_time_sibling() { const COLS: &[(&str, &str)] = &[("created", "date"), ("rate", "rate")]; - let p = present_columns_from(&["created", "rate"], COLS, false, false); + let p = present_columns_from(&["created", "rate"], COLS, false); assert!( p.contains("date"), "date claims the `created` header directly" @@ -544,7 +520,7 @@ mod tests { ]; // `ms_of_day` resolves via alias (pass 2), `rate`/`date` are exact — // yet the order must stay schema order, not claim order. - let p = present_columns_from(&["timestamp", "date", "rate"], COLS, false, false); + let p = present_columns_from(&["timestamp", "date", "rate"], COLS, false); let got: Vec<&str> = p.present_names().collect(); assert_eq!(got, ["ms_of_day", "date", "rate"]); } diff --git a/thetadatadx-rs/src/frames/generated.rs b/thetadatadx-rs/src/frames/generated.rs index 5672bad08..b62d754f0 100644 --- a/thetadatadx-rs/src/frames/generated.rs +++ b/thetadatadx-rs/src/frames/generated.rs @@ -5064,7 +5064,6 @@ impl crate::frames::TicksArrowExt for [crate::tdbe::types::tick::QuoteTick] { let mut col_expiration: Vec> = Vec::with_capacity(n); let mut col_strike: Vec> = Vec::with_capacity(n); let mut col_right: Vec> = Vec::with_capacity(n); - let mut col_midpoint: Vec = Vec::with_capacity(n); for t in self { col_ms_of_day.push(t.ms_of_day); col_bid_size.push(t.bid_size); @@ -5079,7 +5078,6 @@ impl crate::frames::TicksArrowExt for [crate::tdbe::types::tick::QuoteTick] { col_expiration.push(t.has_contract_id().then_some(t.expiration)); col_strike.push(t.has_contract_id().then_some(t.strike)); col_right.push(if t.right == '\0' { None } else { Some(t.right.to_string()) }); - col_midpoint.push(t.midpoint); } let schema = Arc::new(ArrowSchema::new(vec![ Field::new("ms_of_day", DataType::Int32, false), @@ -5095,7 +5093,6 @@ impl crate::frames::TicksArrowExt for [crate::tdbe::types::tick::QuoteTick] { Field::new("expiration", DataType::Int32, true), Field::new("strike", DataType::Float64, true), Field::new("right", DataType::Utf8, true), - Field::new("midpoint", DataType::Float64, false), ])); let columns: Vec = vec![ Arc::new(Int32Array::from(col_ms_of_day)) as ArrayRef, @@ -5111,7 +5108,6 @@ impl crate::frames::TicksArrowExt for [crate::tdbe::types::tick::QuoteTick] { Arc::new(Int32Array::from(col_expiration)) as ArrayRef, Arc::new(Float64Array::from(col_strike)) as ArrayRef, Arc::new(StringArray::from(col_right)) as ArrayRef, - Arc::new(Float64Array::from(col_midpoint)) as ArrayRef, ]; RecordBatch::try_new(schema, columns) } @@ -5132,7 +5128,6 @@ impl crate::frames::TicksArrowExt for [crate::tdbe::types::tick::QuoteTick] { let has_expiration = present.contains("expiration"); let has_strike = present.contains("strike"); let has_right = present.contains("right"); - let has_midpoint = present.contains("midpoint"); let mut col_ms_of_day: Vec = Vec::with_capacity(if has_ms_of_day { n } else { 0 }); let mut col_bid_size: Vec = Vec::with_capacity(if has_bid_size { n } else { 0 }); let mut col_bid_exchange: Vec = Vec::with_capacity(if has_bid_exchange { n } else { 0 }); @@ -5146,7 +5141,6 @@ impl crate::frames::TicksArrowExt for [crate::tdbe::types::tick::QuoteTick] { let mut col_expiration: Vec> = Vec::with_capacity(if has_expiration { n } else { 0 }); let mut col_strike: Vec> = Vec::with_capacity(if has_strike { n } else { 0 }); let mut col_right: Vec> = Vec::with_capacity(if has_right { n } else { 0 }); - let mut col_midpoint: Vec = Vec::with_capacity(if has_midpoint { n } else { 0 }); for t in self { if has_ms_of_day { col_ms_of_day.push(t.ms_of_day); } if has_bid_size { col_bid_size.push(t.bid_size); } @@ -5161,7 +5155,6 @@ impl crate::frames::TicksArrowExt for [crate::tdbe::types::tick::QuoteTick] { if has_expiration { col_expiration.push(t.has_contract_id().then_some(t.expiration)); } if has_strike { col_strike.push(t.has_contract_id().then_some(t.strike)); } if has_right { col_right.push(if t.right == '\0' { None } else { Some(t.right.to_string()) }); } - if has_midpoint { col_midpoint.push(t.midpoint); } } let mut fields: Vec = Vec::new(); let mut columns: Vec = Vec::new(); @@ -5224,10 +5217,6 @@ impl crate::frames::TicksArrowExt for [crate::tdbe::types::tick::QuoteTick] { fields.push(Field::new("right", DataType::Utf8, true)); columns.push(Arc::new(StringArray::from(col_right)) as ArrayRef); } - if has_midpoint { - fields.push(Field::new("midpoint", DataType::Float64, false)); - columns.push(Arc::new(Float64Array::from(col_midpoint)) as ArrayRef); - } RecordBatch::try_new_with_options(Arc::new(ArrowSchema::new(fields)), columns, &RecordBatchOptions::new().with_row_count(Some(n))) } } @@ -5251,7 +5240,6 @@ impl crate::frames::TicksPolarsExt for [crate::tdbe::types::tick::QuoteTick] { let mut col_expiration: Vec> = Vec::with_capacity(n); let mut col_strike: Vec> = Vec::with_capacity(n); let mut col_right: Vec> = Vec::with_capacity(n); - let mut col_midpoint: Vec = Vec::with_capacity(n); for t in self { col_ms_of_day.push(t.ms_of_day); col_bid_size.push(t.bid_size); @@ -5266,7 +5254,6 @@ impl crate::frames::TicksPolarsExt for [crate::tdbe::types::tick::QuoteTick] { col_expiration.push(t.has_contract_id().then_some(t.expiration)); col_strike.push(t.has_contract_id().then_some(t.strike)); col_right.push(if t.right == '\0' { None } else { Some(t.right.to_string()) }); - col_midpoint.push(t.midpoint); } DataFrame::new(n, vec![ Series::new(PlSmallStr::from_static("ms_of_day"), col_ms_of_day).into(), @@ -5282,7 +5269,6 @@ impl crate::frames::TicksPolarsExt for [crate::tdbe::types::tick::QuoteTick] { Series::new(PlSmallStr::from_static("expiration"), col_expiration).into(), Series::new(PlSmallStr::from_static("strike"), col_strike).into(), Series::new(PlSmallStr::from_static("right"), col_right).into(), - Series::new(PlSmallStr::from_static("midpoint"), col_midpoint).into(), ]) } @@ -5302,7 +5288,6 @@ impl crate::frames::TicksPolarsExt for [crate::tdbe::types::tick::QuoteTick] { let has_expiration = present.contains("expiration"); let has_strike = present.contains("strike"); let has_right = present.contains("right"); - let has_midpoint = present.contains("midpoint"); let mut col_ms_of_day: Vec = Vec::with_capacity(if has_ms_of_day { n } else { 0 }); let mut col_bid_size: Vec = Vec::with_capacity(if has_bid_size { n } else { 0 }); let mut col_bid_exchange: Vec = Vec::with_capacity(if has_bid_exchange { n } else { 0 }); @@ -5316,7 +5301,6 @@ impl crate::frames::TicksPolarsExt for [crate::tdbe::types::tick::QuoteTick] { let mut col_expiration: Vec> = Vec::with_capacity(if has_expiration { n } else { 0 }); let mut col_strike: Vec> = Vec::with_capacity(if has_strike { n } else { 0 }); let mut col_right: Vec> = Vec::with_capacity(if has_right { n } else { 0 }); - let mut col_midpoint: Vec = Vec::with_capacity(if has_midpoint { n } else { 0 }); for t in self { if has_ms_of_day { col_ms_of_day.push(t.ms_of_day); } if has_bid_size { col_bid_size.push(t.bid_size); } @@ -5331,7 +5315,6 @@ impl crate::frames::TicksPolarsExt for [crate::tdbe::types::tick::QuoteTick] { if has_expiration { col_expiration.push(t.has_contract_id().then_some(t.expiration)); } if has_strike { col_strike.push(t.has_contract_id().then_some(t.strike)); } if has_right { col_right.push(if t.right == '\0' { None } else { Some(t.right.to_string()) }); } - if has_midpoint { col_midpoint.push(t.midpoint); } } let mut series: Vec = Vec::new(); if let Some(syms) = present.symbols() { @@ -5378,9 +5361,6 @@ impl crate::frames::TicksPolarsExt for [crate::tdbe::types::tick::QuoteTick] { if has_right { series.push(Series::new(PlSmallStr::from_static("right"), col_right).into()); } - if has_midpoint { - series.push(Series::new(PlSmallStr::from_static("midpoint"), col_midpoint).into()); - } DataFrame::new(n, series) } } diff --git a/thetadatadx-rs/src/mdds/decode/tests.rs b/thetadatadx-rs/src/mdds/decode/tests.rs index 4327e9f13..4c4aa05f9 100644 --- a/thetadatadx-rs/src/mdds/decode/tests.rs +++ b/thetadatadx-rs/src/mdds/decode/tests.rs @@ -1343,11 +1343,6 @@ fn quote_tick_decodes_legacy_six_field_shape_with_zero_fill() { assert_eq!(t.bid_condition, 0); assert_eq!(t.ask_exchange, 0); assert_eq!(t.ask_condition, 0); - - // Midpoint is computed from bid + ask regardless of legacy / current - // layout — pin the value so a generator regression on the midpoint - // post-processing step would surface here. - assert!((t.midpoint - 1.50315).abs() < 1e-9); } /// The full 11-field shape must continue to decode all columns. A diff --git a/thetadatadx-rs/src/tdbe/types/generated/tick.rs b/thetadatadx-rs/src/tdbe/types/generated/tick.rs index 92b476d6a..3891e48f2 100644 --- a/thetadatadx-rs/src/tdbe/types/generated/tick.rs +++ b/thetadatadx-rs/src/tdbe/types/generated/tick.rs @@ -816,7 +816,7 @@ impl PriceTick { } } -/// Quote tick -- 13 fields + midpoint. NBBO quote data. +/// Quote tick -- 13 fields. NBBO quote data. /// /// Wire layout: the full shape is 11 columns (`ms_of_day`, /// `bid_size`, `bid_exchange`, `bid`, `bid_condition`, `ask_size`, @@ -858,8 +858,6 @@ pub struct QuoteTick { pub strike: f64, /// Contract right: `'C'` for a call, `'P'` for a put. `'\0'` on single-contract queries. pub right: char, - /// Pre-computed midpoint: `(bid + ask) / 2.0`. - pub midpoint: f64, } impl QuoteTick { diff --git a/thetadatadx-rs/src/tdbe/types/generated/tick_layout_asserts.rs b/thetadatadx-rs/src/tdbe/types/generated/tick_layout_asserts.rs index d8af97398..6adea26f9 100644 --- a/thetadatadx-rs/src/tdbe/types/generated/tick_layout_asserts.rs +++ b/thetadatadx-rs/src/tdbe/types/generated/tick_layout_asserts.rs @@ -316,7 +316,6 @@ mod layout_asserts { assert_eq!(offset_of!(QuoteTick, expiration), 56); assert_eq!(offset_of!(QuoteTick, strike), 64); assert_eq!(offset_of!(QuoteTick, right), 72); - assert_eq!(offset_of!(QuoteTick, midpoint), 80); } #[test] diff --git a/thetadatadx-rs/tests/test_column_projection.rs b/thetadatadx-rs/tests/test_column_projection.rs index 6946eefb4..86cd1196f 100644 --- a/thetadatadx-rs/tests/test_column_projection.rs +++ b/thetadatadx-rs/tests/test_column_projection.rs @@ -585,34 +585,6 @@ fn calendar_year_headers_project_all_v3_fields() { ); } -/// `QuoteTick.midpoint` is computed at decode from `bid` + `ask` and is never a -/// wire header, so the projected frame must key its presence on both inputs: -/// present when bid + ask are, absent otherwise. Regression guard — a decode -/// path that dropped midpoint left `ticks[0].midpoint` populated while the -/// frame omitted the column. -#[test] -fn quote_projects_midpoint_when_bid_and_ask_present() { - let with = projected_columns::(&[ - "ms_of_day", - "bid_size", - "bid", - "ask_size", - "ask", - "date", - ]); - assert!( - with.contains(&"midpoint".to_string()), - "midpoint must ride whenever bid + ask do; got {with:?}" - ); - - // A subset without bid/ask carries no midpoint. - let without = projected_columns::(&["ms_of_day", "date"]); - assert!( - !without.contains(&"midpoint".to_string()), - "midpoint must be absent when its inputs are; got {without:?}" - ); -} - // ── The full (hand-built) path is unchanged ─────────────────────────────── /// `to_arrow` (no presence) still emits the complete schema — the diff --git a/thetadatadx-rs/tests/test_frames_arrow.rs b/thetadatadx-rs/tests/test_frames_arrow.rs index d7bf9e01b..26984bfe9 100644 --- a/thetadatadx-rs/tests/test_frames_arrow.rs +++ b/thetadatadx-rs/tests/test_frames_arrow.rs @@ -72,31 +72,6 @@ fn ohlc_tick_to_arrow_schema_matches_python() { assert_eq!(dtype_of(&batch, "right"), DataType::Utf8); } -#[test] -fn quote_tick_to_arrow_emits_midpoint() { - let ticks = vec![tick::QuoteTick { - ms_of_day: 34200000, - bid_size: 10, - bid_exchange: 1, - bid: 99.99, - bid_condition: 0, - ask_size: 20, - ask_exchange: 2, - ask: 100.01, - ask_condition: 0, - date: 20240102, - midpoint: 100.0, - expiration: 0, - strike: 0.0, - right: '\0', - }]; - let batch = ticks.as_slice().to_arrow().unwrap(); - assert_eq!(batch.num_rows(), 1); - let cols = columns(&batch); - assert!(cols.contains(&"midpoint".to_string())); - assert_eq!(dtype_of(&batch, "midpoint"), DataType::Float64); -} - #[test] fn option_contract_right_stringifies() { let ticks = vec![ diff --git a/thetadatadx-rs/tests/test_frames_polars.rs b/thetadatadx-rs/tests/test_frames_polars.rs index 06e684269..c7825add8 100644 --- a/thetadatadx-rs/tests/test_frames_polars.rs +++ b/thetadatadx-rs/tests/test_frames_polars.rs @@ -74,30 +74,6 @@ fn ohlc_tick_to_polars_emits_contract_tail() { assert_eq!(dtype_of(&df, "right"), DataType::String); } -#[test] -fn quote_tick_to_polars_emits_midpoint() { - let ticks = vec![tick::QuoteTick { - ms_of_day: 34200000, - bid_size: 10, - bid_exchange: 1, - bid: 99.99, - bid_condition: 0, - ask_size: 20, - ask_exchange: 2, - ask: 100.01, - ask_condition: 0, - date: 20240102, - midpoint: 100.0, - expiration: 0, - strike: 0.0, - right: '\0', - }]; - let df = ticks.as_slice().to_polars().unwrap(); - assert_eq!(df.height(), 1); - assert!(columns(&df).contains(&"midpoint".to_string())); - assert_eq!(dtype_of(&df, "midpoint"), DataType::Float64); -} - #[test] fn option_contract_right_stringifies() { let ticks = vec![ diff --git a/thetadatadx-rs/tick_schema.toml b/thetadatadx-rs/tick_schema.toml index 2a2b1bb17..d97f7692a 100644 --- a/thetadatadx-rs/tick_schema.toml +++ b/thetadatadx-rs/tick_schema.toml @@ -113,7 +113,7 @@ ts_class_vec = "trade_ticks_to_class_vec" pyclass = "TradeTick" [types.QuoteTick] -doc = """Quote tick -- 10 fields + midpoint. NBBO quote data. +doc = """Quote tick -- 10 fields. NBBO quote data. Wire layout: the full shape is 11 columns (`ms_of_day`, `bid_size`, `bid_exchange`, `bid`, `bid_condition`, `ask_size`, diff --git a/thetadatadx-ts/index.d.ts b/thetadatadx-ts/index.d.ts index d494908ed..47146af68 100644 --- a/thetadatadx-ts/index.d.ts +++ b/thetadatadx-ts/index.d.ts @@ -6176,7 +6176,6 @@ export interface QuoteTick { ask: number askCondition: number date: number - midpoint: number expiration?: number strike?: number right?: string diff --git a/thetadatadx-ts/src/_generated/tick_classes.rs b/thetadatadx-ts/src/_generated/tick_classes.rs index 4bf527563..8dc112594 100644 --- a/thetadatadx-ts/src/_generated/tick_classes.rs +++ b/thetadatadx-ts/src/_generated/tick_classes.rs @@ -407,7 +407,6 @@ pub struct QuoteTick { pub ask: f64, pub ask_condition: i32, pub date: i32, - pub midpoint: f64, pub expiration: Option, pub strike: Option, pub right: Option, @@ -1091,7 +1090,6 @@ fn quote_ticks_to_class_vec(ticks: &[tick::QuoteTick]) -> Vec { ask: t.ask, ask_condition: t.ask_condition, date: t.date, - midpoint: t.midpoint, expiration: t.has_contract_id().then_some(t.expiration), strike: t.has_contract_id().then_some(t.strike), right: if t.right == '\0' { None } else { Some(t.right.to_string()) }, @@ -2701,7 +2699,6 @@ fn quote_tick_reconstruct_rows(rows: Vec) -> napi::Result 'C', Some("P") => 'P', None | Some("") => '\0', Some(other) => return Err(napi::Error::from_reason(format!("[InvalidParameterError] right must be \"C\" or \"P\", got {other:?}"))) }, diff --git a/tools/mcp/src/main.rs b/tools/mcp/src/main.rs index 7236747f8..4926743e4 100644 --- a/tools/mcp/src/main.rs +++ b/tools/mcp/src/main.rs @@ -692,7 +692,6 @@ fn serialize_quote_ticks(ticks: &[thetadatadx::QuoteTick]) -> Value { "ask_size": t.ask_size, "ask_exchange": t.ask_exchange, "ask_condition": t.ask_condition, - "midpoint": t.midpoint, }); insert_contract_id_fields(&mut row, t.expiration, t.strike, t.right); row @@ -2160,7 +2159,7 @@ mod tests { // the serialized JSON output. #[test] - fn serialize_quote_ticks_includes_condition_and_midpoint_fields() { + fn serialize_quote_ticks_includes_condition_fields() { let tick = QuoteTick { ms_of_day: 0, bid_size: 100, @@ -2175,14 +2174,12 @@ mod tests { expiration: 0, strike: 0.0, right: '\0', - midpoint: 150.5, }; let payload = serialize_quote_ticks(&[tick]); let row = payload["ticks"].as_array().unwrap().first().unwrap(); for key in [ "bid_condition", "ask_condition", - "midpoint", "bid_exchange", "ask_exchange", ] { diff --git a/tools/server/src/format.rs b/tools/server/src/format.rs index b79fd9d00..4493fdad0 100644 --- a/tools/server/src/format.rs +++ b/tools/server/src/format.rs @@ -1648,7 +1648,6 @@ fn representative_output(ep: &EndpointMeta) -> EndpointOutput { expiration: id_expiration, strike: id_strike, right: id_right, - midpoint: 0.0, }])) } ReturnType::TradeQuoteTicks => { @@ -2636,7 +2635,6 @@ mod tests { expiration: 20260417, strike: 150.0, right: 'C', - midpoint: 5.0, }; let r = quote_ticks_to_json(&[t]); let r = r.first().unwrap(); @@ -2876,7 +2874,6 @@ mod tests { expiration, strike, right, - midpoint: 5.0, } } From 3f2db580ce22347e484f828847d84ea6c867e0f3 Mon Sep 17 00:00:00 2001 From: preview Date: Sun, 12 Jul 2026 01:09:39 +0200 Subject: [PATCH 2/3] 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 --- thetadatadx-rs/tests/test_column_projection.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/thetadatadx-rs/tests/test_column_projection.rs b/thetadatadx-rs/tests/test_column_projection.rs index 86cd1196f..f42c645f3 100644 --- a/thetadatadx-rs/tests/test_column_projection.rs +++ b/thetadatadx-rs/tests/test_column_projection.rs @@ -277,9 +277,8 @@ fn option_contract_projects_exactly_one_symbol_column() { /// attributable to its underlying, rather than dropping the column silently. #[test] fn multi_symbol_snapshot_projects_per_row_symbol_column() { - // The quote columns a multi-symbol snapshot's wire carries (no `date`; - // `midpoint` rides on `bid`+`ask`), plus the per-row roots the seam reads - // off the varying `symbol` column. + // The quote columns a multi-symbol snapshot's wire carries (no `date`), + // plus the per-row roots the seam reads off the varying `symbol` column. let present = ColumnPresence::from_names([ "ms_of_day", "bid_size", @@ -290,7 +289,6 @@ fn multi_symbol_snapshot_projects_per_row_symbol_column() { "ask_exchange", "ask", "ask_condition", - "midpoint", ]) .with_symbols(["AAPL", "MSFT", "SPY"]); let quote = |bid: f64, ask: f64| QuoteTick { @@ -307,7 +305,6 @@ fn multi_symbol_snapshot_projects_per_row_symbol_column() { expiration: 0, strike: 0.0, right: '\0', - midpoint: (bid + ask) / 2.0, }; let ticks = vec![quote(1.0, 2.0), quote(3.0, 4.0), quote(5.0, 6.0)]; @@ -345,7 +342,7 @@ fn multi_symbol_snapshot_projects_per_row_symbol_column() { #[test] fn single_symbol_snapshot_still_broadcasts_constant() { let present = - ColumnPresence::from_names(["ms_of_day", "bid", "ask", "midpoint"]).with_symbol("AAPL"); + ColumnPresence::from_names(["ms_of_day", "bid", "ask"]).with_symbol("AAPL"); let quote = QuoteTick { ms_of_day: 1, bid_size: 0, @@ -360,7 +357,6 @@ fn single_symbol_snapshot_still_broadcasts_constant() { expiration: 0, strike: 0.0, right: '\0', - midpoint: 1.5, }; let ticks = vec![quote, quote]; let batch = ticks.as_slice().to_arrow_projected(&present).unwrap(); From 7dc4495aac6db3992042ed94cd7008c89bf41197 Mon Sep 17 00:00:00 2001 From: preview Date: Sun, 12 Jul 2026 01:10:57 +0200 Subject: [PATCH 3/3] style: rustfmt projection-test comment reflow Co-Authored-By: Claude Opus 4.8 --- thetadatadx-rs/tests/test_column_projection.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/thetadatadx-rs/tests/test_column_projection.rs b/thetadatadx-rs/tests/test_column_projection.rs index f42c645f3..2d17c7ec0 100644 --- a/thetadatadx-rs/tests/test_column_projection.rs +++ b/thetadatadx-rs/tests/test_column_projection.rs @@ -341,8 +341,7 @@ fn multi_symbol_snapshot_projects_per_row_symbol_column() { /// repeated on every row (no regression from the per-row addition). #[test] fn single_symbol_snapshot_still_broadcasts_constant() { - let present = - ColumnPresence::from_names(["ms_of_day", "bid", "ask"]).with_symbol("AAPL"); + let present = ColumnPresence::from_names(["ms_of_day", "bid", "ask"]).with_symbol("AAPL"); let quote = QuoteTick { ms_of_day: 1, bid_size: 0,