diff --git a/CHANGELOG.md b/CHANGELOG.md index ef13d5f5..28c3f190 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [8.0.25] - 2026-05-04 + +### Changed + +- **Renamed `Contract.root` → `Contract.symbol` and `Contract.exp_date` → + `Contract.expiration` to match the v3 vendor surface** (#467). The same + rename lands on every public type that carries those fields: + `tdbe::OptionContract.root` → `.symbol`, `FlatFileRow.root` → `.symbol`, + `FlatFileRow` keeps `expiration` (was already correct), and the + flat-file CSV / JSONL contract-prefix headers go from + `root,expiration,...` to `symbol,expiration,...`. Constructor and method + parameter names follow the field rename: `Contract::stock(symbol)`, + `Contract::option(symbol, expiration, ...)`, `Contract::option_raw(...)`. + Generated bindings (Python `Contract.symbol` / `Contract.expiration`, + TypeScript `contract.symbol` / `contract.expiration`, Go + `Contract.Symbol` / `Contract.Expiration`, C++ / C ABI + `TdxContract.symbol` / `TdxContract.has_expiration` / + `TdxContract.expiration`, `TdxOptionContract.symbol`) follow the same + rename. The wire format is unchanged; the rename is purely cosmetic on + the public API to align with the + [vendor v3 migration guide](https://docs.thetadata.us/Articles/Getting-Started/v2-migration-guide.html#_5-parameter-mapping). + Migration: + ```rust + // before (v8.0.24) + let c = Contract::stock("AAPL"); + let _ = c.root; + let opt = Contract::option("SPY", "20260417", "550", "C")?; + let _ = opt.exp_date; + + // after (v8.0.25) + let c = Contract::stock("AAPL"); + let _ = c.symbol; + let opt = Contract::option("SPY", "20260417", "550", "C")?; + let _ = opt.expiration; + ``` +- `tdbe` 0.12.5 → 0.12.6. + ## [8.0.24] - 2026-05-04 ### Added diff --git a/Cargo.lock b/Cargo.lock index 2740742b..ff14660a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3022,7 +3022,7 @@ dependencies = [ [[package]] name = "tdbe" -version = "0.12.5" +version = "0.12.6" dependencies = [ "criterion", "thiserror 2.0.18", @@ -3043,7 +3043,7 @@ dependencies = [ [[package]] name = "thetadatadx" -version = "8.0.24" +version = "8.0.25" dependencies = [ "arrow-array", "arrow-schema", @@ -3082,7 +3082,7 @@ dependencies = [ [[package]] name = "thetadatadx-cli" -version = "8.0.24" +version = "8.0.25" dependencies = [ "clap", "comfy-table", @@ -3095,7 +3095,7 @@ dependencies = [ [[package]] name = "thetadatadx-ffi" -version = "8.0.24" +version = "8.0.25" dependencies = [ "tdbe", "thetadatadx", diff --git a/crates/tdbe/Cargo.toml b/crates/tdbe/Cargo.toml index b463e6ed..b4aefde3 100644 --- a/crates/tdbe/Cargo.toml +++ b/crates/tdbe/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tdbe" -version = "0.12.5" +version = "0.12.6" edition.workspace = true rust-version.workspace = true authors.workspace = true diff --git a/crates/tdbe/src/types/enums.rs b/crates/tdbe/src/types/enums.rs index 1612e924..3ef8f437 100644 --- a/crates/tdbe/src/types/enums.rs +++ b/crates/tdbe/src/types/enums.rs @@ -4,7 +4,7 @@ /// The FPSS decoder uses it for the empty-contract placeholder that flows on /// data events arriving before their `ContractAssigned` frame — downstream /// consumers can pattern-match `sec_type == SecType::Unknown` instead of -/// relying on `contract.root.is_empty()`. `Unknown` has no wire-protocol +/// relying on `contract.symbol.is_empty()`. `Unknown` has no wire-protocol /// representation: [`SecType::from_code`] never returns it, and it is not /// serialized in subscribe payloads. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] diff --git a/crates/tdbe/src/types/tick.rs b/crates/tdbe/src/types/tick.rs index caad7319..1e38e83b 100644 --- a/crates/tdbe/src/types/tick.rs +++ b/crates/tdbe/src/types/tick.rs @@ -151,10 +151,14 @@ pub struct OpenInterestTick { } /// Option contract specification. +/// +/// `symbol` matches the v3 vendor surface (the field was named `root` +/// before the v3 rename). See the migration guide: +/// . #[must_use] #[derive(Debug, Clone)] pub struct OptionContract { - pub root: String, + pub symbol: String, pub expiration: i32, pub strike: f64, pub right: i32, diff --git a/crates/thetadatadx/Cargo.toml b/crates/thetadatadx/Cargo.toml index 72493079..22235ece 100644 --- a/crates/thetadatadx/Cargo.toml +++ b/crates/thetadatadx/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thetadatadx" -version = "8.0.24" +version = "8.0.25" edition.workspace = true rust-version.workspace = true authors.workspace = true @@ -40,7 +40,7 @@ frames = ["polars", "arrow"] live-tests = [] [dependencies] -tdbe = { version = "0.12.5", path = "../tdbe" } +tdbe = { version = "0.12.6", path = "../tdbe" } # gRPC + protobuf (tonic 0.14 extracted prost codec into tonic-prost) tonic = { version = "=0.14.5", features = ["tls-ring", "tls-native-roots", "channel", "transport"] } @@ -132,7 +132,7 @@ prost-build = "=0.14.3" regex = "1.12.3" toml = "1.1.2" serde = { version = "1.0.228", features = ["derive"] } -tdbe = { version = "0.12.5", path = "../tdbe" } +tdbe = { version = "0.12.6", path = "../tdbe" } [[bench]] name = "bench_decode" diff --git a/crates/thetadatadx/build_support/endpoints/render/templates/cpp/option_contracts_convert.cpp.tmpl b/crates/thetadatadx/build_support/endpoints/render/templates/cpp/option_contracts_convert.cpp.tmpl index edf1763a..000632be 100644 --- a/crates/thetadatadx/build_support/endpoints/render/templates/cpp/option_contracts_convert.cpp.tmpl +++ b/crates/thetadatadx/build_support/endpoints/render/templates/cpp/option_contracts_convert.cpp.tmpl @@ -9,7 +9,7 @@ result.reserve(arr.len); for (size_t i = 0; i < arr.len; ++i) { OptionContract c; - c.root = arr.data[i].root ? std::string(arr.data[i].root) : ""; + c.symbol = arr.data[i].symbol ? std::string(arr.data[i].symbol) : ""; c.expiration = arr.data[i].expiration; c.strike = arr.data[i].strike; c.right = arr.data[i].right; diff --git a/crates/thetadatadx/build_support/fpss_events/ffi_c.rs b/crates/thetadatadx/build_support/fpss_events/ffi_c.rs index 79995530..47135f3e 100644 --- a/crates/thetadatadx/build_support/fpss_events/ffi_c.rs +++ b/crates/thetadatadx/build_support/fpss_events/ffi_c.rs @@ -9,17 +9,20 @@ use super::schema::{sorted_data_events, Schema}; /// Emit the C `TdxContract` struct mirrored from the Rust /// `#[repr(C)] TdxContract`. Layout must match field-for-field with /// `render_contract_struct_rust` in `ffi_rust.rs`. +/// +/// Field names follow the v3 vendor surface (`symbol`, `expiration`) +/// per . fn render_contract_struct_c() -> &'static str { - "/* FPSS Contract shared across every data event. `root` is a\n\ + "/* FPSS Contract shared across every data event. `symbol` is a\n\ * NUL-terminated C string (may be null when not yet resolved);\n\ * optional option fields use a tagged-present bool because C has no\n\ * Option. Layout is byte-identical to Rust's #[repr(C)] TdxContract.\n\ */\n\ typedef struct {\n\ - const char *root;\n\ + const char *symbol;\n\ int32_t sec_type;\n\ - bool has_exp_date;\n\ - int32_t exp_date;\n\ + bool has_expiration;\n\ + int32_t expiration;\n\ bool has_is_call;\n\ bool is_call;\n\ bool has_strike;\n\ diff --git a/crates/thetadatadx/build_support/fpss_events/ffi_rust.rs b/crates/thetadatadx/build_support/fpss_events/ffi_rust.rs index 45c1f3d5..5ba9a8cf 100644 --- a/crates/thetadatadx/build_support/fpss_events/ffi_rust.rs +++ b/crates/thetadatadx/build_support/fpss_events/ffi_rust.rs @@ -12,29 +12,32 @@ use super::schema::{sorted_data_events, Schema}; /// event's `contract` field points at. Uses `#[repr(C)]` so the C header /// mirror gets byte-identical layout. /// -/// Strings (the `root` field) cross as C strings — the field is +/// Strings (the `symbol` field) cross as C strings — the field is /// `*const c_char` backed by a `CString` inside `FfiBufferedEvent` so the /// pointer stays valid for the lifetime of the buffered event. Optional /// fields use a tagged-optional pattern (`has_*: bool` + value) because /// `#[repr(C)]` rejects `Option` for Rust->C interop. +/// +/// Field names follow the v3 vendor surface (`symbol`, `expiration`) +/// per . fn render_contract_struct_rust() -> String { String::from( "/// FPSS `Contract` shared across every data event.\n\ /// \n\ -/// `root` is a NUL-terminated C string; may be null when the SDK has not\n\ +/// `symbol` is a NUL-terminated C string; may be null when the SDK has not\n\ /// yet resolved the server-assigned contract_id to a `ContractAssigned`\n\ -/// frame. Optional option fields (`exp_date`, `is_call`, `strike`) use a\n\ +/// frame. Optional option fields (`expiration`, `is_call`, `strike`) use a\n\ /// tagged-present bool because `#[repr(C)]` cannot express `Option`\n\ /// directly.\n\ #[repr(C)]\n\ pub struct TdxContract {\n\ - /// Ticker root (e.g. \"AAPL\"). Null until ContractAssigned arrives.\n\ - pub root: *const c_char,\n\ + /// Ticker symbol (e.g. \"AAPL\"). Null until ContractAssigned arrives.\n\ + pub symbol: *const c_char,\n\ /// Security type code — matches `tdbe::types::enums::SecType`.\n\ pub sec_type: i32,\n\ - /// Whether `exp_date` is meaningful (options only).\n\ - pub has_exp_date: bool,\n\ - pub exp_date: i32,\n\ + /// Whether `expiration` is meaningful (options only).\n\ + pub has_expiration: bool,\n\ + pub expiration: i32,\n\ /// Whether `is_call` is meaningful (options only).\n\ pub has_is_call: bool,\n\ pub is_call: bool,\n\ @@ -44,10 +47,10 @@ pub struct TdxContract {\n\ }\n\ \n\ pub(crate) const ZERO_CONTRACT_STRUCT: TdxContract = TdxContract {\n\ - root: ptr::null(),\n\ + symbol: ptr::null(),\n\ sec_type: 0,\n\ - has_exp_date: false,\n\ - exp_date: 0,\n\ + has_expiration: false,\n\ + expiration: 0,\n\ has_is_call: false,\n\ is_call: false,\n\ has_strike: false,\n\ @@ -178,14 +181,15 @@ pub(super) fn render_ffi_fpss_event_converter(schema: &Schema) -> String { // cursors / auxiliary fields without breaking the FFI conversion. out.push_str(" ..\n"); out.push_str(" }) => {\n"); - // Stage the CString backing the Contract.root pointer so it + // Stage the CString backing the Contract.symbol pointer so it // outlives the TdxFpssEvent inside the FfiBufferedEvent. The // backing-memory wrapper already has a `_detail_string` slot we - // repurpose here — only one owned CString per event, and Contract - // .root is mutually exclusive with Control.detail on Data variants. + // repurpose here — only one owned CString per event, and + // Contract.symbol is mutually exclusive with Control.detail on + // Data variants. if has_contract { out.push_str( - " let contract_root_cstring = if contract.root.is_empty() {\n None\n } else {\n std::ffi::CString::new(contract.root.as_str()).ok()\n };\n let contract_root_ptr = contract_root_cstring\n .as_ref()\n .map_or(ptr::null(), |cs| cs.as_ptr());\n let tdx_contract = TdxContract {\n root: contract_root_ptr,\n sec_type: contract.sec_type as i32,\n has_exp_date: contract.exp_date.is_some(),\n exp_date: contract.exp_date.unwrap_or(0),\n has_is_call: contract.is_call.is_some(),\n is_call: contract.is_call.unwrap_or(false),\n has_strike: contract.strike.is_some(),\n strike: contract.strike.unwrap_or(0),\n };\n", + " let contract_symbol_cstring: Option = if contract.symbol.is_empty() {\n None\n } else {\n std::ffi::CString::new(contract.symbol.as_str()).ok()\n };\n let contract_symbol_ptr = contract_symbol_cstring\n .as_ref()\n .map_or(ptr::null(), |cs| cs.as_ptr());\n let tdx_contract = TdxContract {\n symbol: contract_symbol_ptr,\n sec_type: contract.sec_type as i32,\n has_expiration: contract.expiration.is_some(),\n expiration: contract.expiration.unwrap_or(0),\n has_is_call: contract.is_call.is_some(),\n is_call: contract.is_call.unwrap_or(false),\n has_strike: contract.strike.is_some(),\n strike: contract.strike.unwrap_or(0),\n };\n", ); } out.push_str(" FfiBufferedEvent {\n"); @@ -225,7 +229,7 @@ pub(super) fn render_ffi_fpss_event_converter(schema: &Schema) -> String { out.push_str(" raw_data: ZERO_RAW,\n"); out.push_str(" },\n"); if has_contract { - out.push_str(" _detail_string: contract_root_cstring,\n"); + out.push_str(" _detail_string: contract_symbol_cstring,\n"); } else { out.push_str(" _detail_string: None,\n"); } diff --git a/crates/thetadatadx/build_support/fpss_events/go_structs.rs b/crates/thetadatadx/build_support/fpss_events/go_structs.rs index 2a982fb2..b7a77c33 100644 --- a/crates/thetadatadx/build_support/fpss_events/go_structs.rs +++ b/crates/thetadatadx/build_support/fpss_events/go_structs.rs @@ -82,19 +82,21 @@ pub(super) fn render_go_fpss_offset_checks(schema: &Schema) -> String { } /// Go Contract struct — optional fields use `*int32` / `*bool` pointers -/// so nil represents `None` (Java-style). `Root` is always present as a -/// string (empty when not yet resolved). +/// so nil represents `None` (Java-style). `Symbol` is always present as a +/// string (empty when not yet resolved). Field names follow the v3 vendor +/// surface; see +/// . fn render_contract_go() -> &'static str { - "// Contract identifies a subscribed instrument. Root is always present;\n\ -// option fields (ExpDate, IsCall, Strike) are non-nil only for options.\n\ + "// Contract identifies a subscribed instrument. Symbol is always present;\n\ +// option fields (Expiration, IsCall, Strike) are non-nil only for options.\n\ // The same Contract value is attached to every FPSS data event the SDK\n\ // emits for the matching contract_id.\n\ type Contract struct {\n\ -\tRoot string\n\ -\tSecType int32\n\ -\tExpDate *int32\n\ -\tIsCall *bool\n\ -\tStrike *int32\n\ +\tSymbol string\n\ +\tSecType int32\n\ +\tExpiration *int32\n\ +\tIsCall *bool\n\ +\tStrike *int32\n\ }\n\n" } diff --git a/crates/thetadatadx/build_support/fpss_events/python.rs b/crates/thetadatadx/build_support/fpss_events/python.rs index 69ec7cae..66802ba0 100644 --- a/crates/thetadatadx/build_support/fpss_events/python.rs +++ b/crates/thetadatadx/build_support/fpss_events/python.rs @@ -8,8 +8,10 @@ use super::schema::{sorted_event_names, ColumnDef, EventDef, Schema}; /// Emit the `Contract` pyclass + helper constructor. Every data event /// carries a `Py` field so Python code can read -/// `event.contract.root`, `event.contract.strike`, etc. through the -/// normal pyo3 getter machinery. +/// `event.contract.symbol`, `event.contract.strike`, etc. through the +/// normal pyo3 getter machinery. Field names follow the v3 vendor +/// surface; see +/// . fn render_contract_pyclass() -> &'static str { "/// FPSS contract identifier. Surfaced on every decoded FPSS data\n\ /// event as `event.contract`. Matches the shape of the Rust\n\ @@ -18,9 +20,9 @@ fn render_contract_pyclass() -> &'static str { #[pyclass(module = \"thetadatadx\", frozen, skip_from_py_object)]\n\ #[derive(Clone)]\n\ pub(crate) struct Contract {\n\ - #[pyo3(get)] pub root: String,\n\ + #[pyo3(get)] pub symbol: String,\n\ #[pyo3(get)] pub sec_type: i32,\n\ - #[pyo3(get)] pub exp_date: Option,\n\ + #[pyo3(get)] pub expiration: Option,\n\ #[pyo3(get)] pub is_call: Option,\n\ #[pyo3(get)] pub strike: Option,\n\ }\n\ @@ -28,8 +30,8 @@ pub(crate) struct Contract {\n\ impl Contract {\n\ fn __repr__(&self) -> String {\n\ format!(\n\ - \"Contract(root={:?}, sec_type={}, exp_date={:?}, is_call={:?}, strike={:?})\",\n\ - self.root, self.sec_type, self.exp_date, self.is_call, self.strike\n\ + \"Contract(symbol={:?}, sec_type={}, expiration={:?}, is_call={:?}, strike={:?})\",\n\ + self.symbol, self.sec_type, self.expiration, self.is_call, self.strike\n\ )\n\ }\n\ }\n\ @@ -41,9 +43,9 @@ impl Contract {\n\ /// exports if they need a symbolic reading.\n\ pub(crate) fn from_core(c: &fpss::protocol::Contract) -> Self {\n\ Self {\n\ - root: c.root.clone(),\n\ + symbol: c.symbol.clone(),\n\ sec_type: c.sec_type as i32,\n\ - exp_date: c.exp_date,\n\ + expiration: c.expiration,\n\ is_call: c.is_call,\n\ strike: c.strike,\n\ }\n\ diff --git a/crates/thetadatadx/build_support/fpss_events/typescript.rs b/crates/thetadatadx/build_support/fpss_events/typescript.rs index f7a78355..e620ef7a 100644 --- a/crates/thetadatadx/build_support/fpss_events/typescript.rs +++ b/crates/thetadatadx/build_support/fpss_events/typescript.rs @@ -18,13 +18,16 @@ use super::schema::{load_schema, sorted_data_event_names, sorted_event_names, Ev fn render_contract_napi() -> &'static str { r#"/// FPSS contract identifier. Surfaced on every decoded FPSS data /// event as `event.quote.contract` / `event.trade.contract` / etc. +/// +/// Field names follow the v3 vendor surface (`symbol`, `expiration`) +/// per . #[must_use] #[napi(object)] #[derive(Clone)] pub struct Contract { - pub root: String, + pub symbol: String, pub sec_type: i32, - pub exp_date: Option, + pub expiration: Option, pub is_call: Option, pub strike: Option, } @@ -164,11 +167,11 @@ pub(super) fn render_ts_fpss_event_classes(schema: &Schema) -> String { name = column.name ) .unwrap(), - // Contract is constructed explicitly — `root` clones, the + // Contract is constructed explicitly — `symbol` clones, the // option fields transfer by value. "Contract" => writeln!( out, - " {name}: Contract {{\n root: {name}.root.clone(),\n sec_type: {name}.sec_type as i32,\n exp_date: {name}.exp_date,\n is_call: {name}.is_call,\n strike: {name}.strike,\n }},", + " {name}: Contract {{\n symbol: {name}.symbol.clone(),\n sec_type: {name}.sec_type as i32,\n expiration: {name}.expiration,\n is_call: {name}.is_call,\n strike: {name}.strike,\n }},", name = column.name ) .unwrap(), diff --git a/crates/thetadatadx/build_support/sdk_surface/templates/go/next_event_body.go.tmpl b/crates/thetadatadx/build_support/sdk_surface/templates/go/next_event_body.go.tmpl index 8c116b93..251c723f 100644 --- a/crates/thetadatadx/build_support/sdk_surface/templates/go/next_event_body.go.tmpl +++ b/crates/thetadatadx/build_support/sdk_surface/templates/go/next_event_body.go.tmpl @@ -8,19 +8,19 @@ } // contractFromC converts a C TdxContract into the Go-idiomatic *Contract. - // Returns nil only when root is NULL, which indicates the SDK has not yet + // Returns nil only when symbol is NULL, which indicates the SDK has not yet // resolved the contract_id to a ContractAssigned frame. contractFromC := func(c C.TdxContract) *Contract { - if c.root == nil { + if c.symbol == nil { return nil } out := &Contract{ - Root: C.GoString(c.root), + Symbol: C.GoString(c.symbol), SecType: int32(c.sec_type), } - if bool(c.has_exp_date) { - v := int32(c.exp_date) - out.ExpDate = &v + if bool(c.has_expiration) { + v := int32(c.expiration) + out.Expiration = &v } if bool(c.has_is_call) { v := bool(c.is_call) diff --git a/crates/thetadatadx/build_support/ticks/go.rs b/crates/thetadatadx/build_support/ticks/go.rs index 3b310289..0590b309 100644 --- a/crates/thetadatadx/build_support/ticks/go.rs +++ b/crates/thetadatadx/build_support/ticks/go.rs @@ -294,9 +294,9 @@ fn render_go_tick_converter(type_name: &str, def: &TickTypeDef) -> String { writeln!(out, " result := make([]{public_type}, n)").unwrap(); out.push_str(" for i, t := range src {\n"); if type_name == "OptionContract" { - out.push_str(" root := \"\"\n"); - out.push_str(" if t.Root != 0 {\n"); - out.push_str(" root = C.GoString((*C.char)(unsafe.Pointer(t.Root)))\n"); + out.push_str(" symbol := \"\"\n"); + out.push_str(" if t.Symbol != 0 {\n"); + out.push_str(" symbol = C.GoString((*C.char)(unsafe.Pointer(t.Symbol)))\n"); out.push_str(" }\n"); } writeln!(out, " result[i] = {public_type}{{").unwrap(); @@ -438,7 +438,7 @@ fn go_public_field_name(type_name: &str, field: &str) -> &'static str { fn go_source_expr(type_name: &str, field: &str, kind: &str) -> String { let ffi_field = go_ffi_field_name(field); match (type_name, field, kind) { - ("OptionContract", "root", "String") => "root".into(), + ("OptionContract", "symbol", "String") => "symbol".into(), ("OptionContract", "right", "i32") => "RightStr(t.Right)".into(), (_, "right", "i32") => "RightStr(t.Right)".into(), (_, "is_open", "i32") => "t.IsOpen != 0".into(), diff --git a/crates/thetadatadx/src/decode.rs b/crates/thetadatadx/src/decode.rs index 30353a7b..0a18fee8 100644 --- a/crates/thetadatadx/src/decode.rs +++ b/crates/thetadatadx/src/decode.rs @@ -804,7 +804,7 @@ pub fn parse_option_contracts_v3( }; Ok(OptionContract { - root, + symbol: root, expiration, strike, right, diff --git a/crates/thetadatadx/src/flatfiles/decoded.rs b/crates/thetadatadx/src/flatfiles/decoded.rs index 1da5b97c..55e6de35 100644 --- a/crates/thetadatadx/src/flatfiles/decoded.rs +++ b/crates/thetadatadx/src/flatfiles/decoded.rs @@ -236,8 +236,8 @@ pub(crate) fn decode_to_memory(raw_path: &Path, sec: SecType) -> Result. #[derive(Debug, Clone, PartialEq)] pub struct FlatFileRow { - /// Underlying root symbol (e.g. `"SPY"`). - pub root: String, + /// Underlying ticker symbol (e.g. `"SPY"`). + pub symbol: String, /// Expiration in `YYYYMMDD`. `None` for stock blobs. pub expiration: Option, /// Strike in vendor units (1/1000 of a dollar). `None` for stocks. @@ -49,7 +53,7 @@ impl FlatFileRow { /// `Price.price_type`). `None` means the schema has no PRICE_TYPE /// column, so price-bearing values are emitted as raw integers. pub(crate) fn from_decoded( - root: &str, + symbol: &str, expiration: Option, strike: Option, right: Option, @@ -75,7 +79,7 @@ impl FlatFileRow { fields.push((dt.name().into_owned(), cell)); } Self { - root: root.to_string(), + symbol: symbol.to_string(), expiration, strike, right, diff --git a/crates/thetadatadx/src/flatfiles/index.rs b/crates/thetadatadx/src/flatfiles/index.rs index 54daf70e..5abf04b0 100644 --- a/crates/thetadatadx/src/flatfiles/index.rs +++ b/crates/thetadatadx/src/flatfiles/index.rs @@ -106,12 +106,19 @@ pub(crate) fn parse_header(blob: &[u8]) -> Result { } /// One INDEX entry's contract key + DATA-section pointer. +/// +/// Field names follow the v3 vendor surface (`symbol`, `expiration`) +/// per the migration guide: +/// . +/// The wire layout still names the same bytes `root` / `exp` and the +/// parser locals below preserve that naming for diff-ability against +/// the upstream binary protocol. #[derive(Debug, Clone)] pub(crate) struct IndexEntry { - /// UTF-8 root symbol (e.g. `"AAPL"`, `"SPY"`, `"ABBV"`). - pub(crate) root: String, + /// UTF-8 ticker symbol (e.g. `"AAPL"`, `"SPY"`, `"ABBV"`). + pub(crate) symbol: String, /// Option expiration (YYYYMMDD), or `None` for stock entries. - pub(crate) exp: Option, + pub(crate) expiration: Option, /// Strike price in tenths of a cent, or `None` for stocks. pub(crate) strike: Option, /// `'C'` or `'P'` for options, `None` for stocks. @@ -194,8 +201,8 @@ fn parse_one_entry(cur: &mut Cursor<&[u8]>, sec: SecType) -> Result. //! //! Both sinks must produce the **same logical rows**; only the on-disk //! encoding differs. This is what makes the byte-match test on `Csv` a @@ -94,9 +95,9 @@ fn append_csv_prefix(buf: &mut String, entry: &IndexEntry, sec: SecType) { use std::fmt::Write; match sec { SecType::Option | SecType::Index => { - buf.push_str(&entry.root); + buf.push_str(&entry.symbol); buf.push(','); - let _ = write!(buf, "{}", entry.exp.unwrap_or(0)); + let _ = write!(buf, "{}", entry.expiration.unwrap_or(0)); buf.push(','); let _ = write!(buf, "{}", entry.strike.unwrap_or(0)); buf.push(','); @@ -104,7 +105,7 @@ fn append_csv_prefix(buf: &mut String, entry: &IndexEntry, sec: SecType) { buf.push(','); } SecType::Stock => { - buf.push_str(&entry.root); + buf.push_str(&entry.symbol); buf.push(','); } } @@ -148,8 +149,10 @@ impl RowSink for CsvSink { fn write_header(&mut self) -> Result<(), Error> { self.line.clear(); match self.sec { - SecType::Option | SecType::Index => self.line.push_str("root,expiration,strike,right,"), - SecType::Stock => self.line.push_str("root,"), + SecType::Option | SecType::Index => { + self.line.push_str("symbol,expiration,strike,right,"); + } + SecType::Stock => self.line.push_str("symbol,"), } for (n, &i) in self.data_idx.iter().enumerate() { if n > 0 { @@ -237,12 +240,12 @@ impl RowSink for JsonlSink { match self.sec { SecType::Option | SecType::Index => { obj.insert( - "root".into(), - serde_json::Value::String(row.entry.root.clone()), + "symbol".into(), + serde_json::Value::String(row.entry.symbol.clone()), ); obj.insert( "expiration".into(), - serde_json::Value::Number(row.entry.exp.unwrap_or(0).into()), + serde_json::Value::Number(row.entry.expiration.unwrap_or(0).into()), ); obj.insert( "strike".into(), @@ -255,8 +258,8 @@ impl RowSink for JsonlSink { } SecType::Stock => { obj.insert( - "root".into(), - serde_json::Value::String(row.entry.root.clone()), + "symbol".into(), + serde_json::Value::String(row.entry.symbol.clone()), ); } } diff --git a/crates/thetadatadx/src/fpss/decode.rs b/crates/thetadatadx/src/fpss/decode.rs index 15a0814e..fa4d7b6e 100644 --- a/crates/thetadatadx/src/fpss/decode.rs +++ b/crates/thetadatadx/src/fpss/decode.rs @@ -27,17 +27,17 @@ use super::reconnect_delay; /// is shared across every "unknown" event so the hot path is always /// `Arc::clone` (refcount bump) with no allocation. /// -/// `root` is the empty string AND `sec_type` is [`SecType::Unknown`]. +/// `symbol` is the empty string AND `sec_type` is [`SecType::Unknown`]. /// Downstream code detecting the sentinel should pattern-match /// `contract.sec_type == SecType::Unknown` — this is the type-safe check -/// that survives future contract-root relaxations (e.g. unicode roots, -/// numeric-prefix tickers) where `root.is_empty()` alone could match a -/// real contract whose root is merely absent or transiently empty. +/// that survives future symbol relaxations (e.g. unicode tickers, +/// numeric-prefix tickers) where `symbol.is_empty()` alone could match a +/// real contract whose symbol is merely absent or transiently empty. static EMPTY_CONTRACT: std::sync::LazyLock> = std::sync::LazyLock::new(|| { Arc::new(Contract { - root: String::new(), + symbol: String::new(), sec_type: tdbe::types::enums::SecType::Unknown, - exp_date: None, + expiration: None, is_call: None, strike: None, }) @@ -124,7 +124,7 @@ pub(super) fn decode_frame( // Wrap the parsed contract in Arc once on insert. Every // subsequent data event refcount-clones this Arc, so the // only `Contract::clone` (and therefore the only - // `String::clone` of `contract.root`) happens here — + // `String::clone` of `contract.symbol`) happens here — // at most once per contract_id per session. let arc_contract: Arc = Arc::new(contract); // Insert into thread-local cache (zero-lock hot-path lookups). @@ -975,7 +975,7 @@ mod tests { match primary.expect("ContractAssigned must emit a primary event") { FpssEvent::Control(FpssControl::ContractAssigned { id, contract }) => { assert_eq!(id, 777); - assert_eq!(contract.root, "AAPL"); + assert_eq!(contract.symbol, "AAPL"); // The Arc inside the event, the Arc in the shared map, and // the Arc in the thread-local cache must all point at the // SAME Contract heap cell — a different pointer would mean @@ -1047,9 +1047,9 @@ mod tests { ); match primary.expect("Quote must emit a primary event") { FpssEvent::Data(FpssData::Quote { contract, .. }) => { - assert_eq!(contract.root, "AAPL"); + assert_eq!(contract.symbol, "AAPL"); // Arc::ptr_eq proves both events share the SAME heap - // allocation — `assert_eq!(contract.root, "AAPL")` alone + // allocation — `assert_eq!(contract.symbol, "AAPL")` alone // only checks that both events carry the same *value*, // which a regression to per-event Contract::clone would // still pass. Pointer equality pins down the exact @@ -1126,7 +1126,7 @@ mod tests { "missing contract_id must surface sec_type = Unknown" ); assert!( - contract.root.is_empty(), + contract.symbol.is_empty(), "empty-contract sentinel must also have an empty root" ); // Same Arc as EMPTY_CONTRACT — the hot path never @@ -1249,7 +1249,7 @@ mod tests { "post-Restart tick on known-but-cleared ID must surface Unknown" ); assert_ne!( - contract.root, "SEED", + contract.symbol, "SEED", "post-Restart decoder must NOT resurrect the pre-restart Contract" ); } diff --git a/crates/thetadatadx/src/fpss/events.rs b/crates/thetadatadx/src/fpss/events.rs index 687c6d49..d4e0c829 100644 --- a/crates/thetadatadx/src/fpss/events.rs +++ b/crates/thetadatadx/src/fpss/events.rs @@ -28,10 +28,10 @@ use super::protocol::Contract; /// placeholder. Detect it via /// `contract.sec_type == tdbe::types::enums::SecType::Unknown` — /// this is the canonical check documented in `fpss::decode`. The -/// secondary `contract.root.is_empty()` check is kept for +/// secondary `contract.symbol.is_empty()` check is kept for /// backwards-compatibility, but the `SecType::Unknown` match survives -/// future contract-root relaxations (e.g. unicode roots, numeric-prefix -/// tickers) where an empty root might coincidentally appear on a real +/// future symbol relaxations (e.g. unicode tickers, numeric-prefix +/// tickers) where an empty symbol might coincidentally appear on a real /// contract. #[derive(Debug, Clone)] #[non_exhaustive] @@ -314,7 +314,7 @@ mod tests { .. }) => { assert_eq!(*contract_id, 42); - assert_eq!(contract.root, "AAPL"); + assert_eq!(contract.symbol, "AAPL"); assert!((*price - 150.25).abs() < f64::EPSILON); } other => panic!("expected Data(Trade), got {other:?}"), diff --git a/crates/thetadatadx/src/fpss/mod.rs b/crates/thetadatadx/src/fpss/mod.rs index 2ee3d225..4539fe2d 100644 --- a/crates/thetadatadx/src/fpss/mod.rs +++ b/crates/thetadatadx/src/fpss/mod.rs @@ -34,11 +34,11 @@ //! // Push to your own queue for heavy processing. //! match event { //! FpssEvent::Data(FpssData::Quote { contract, bid, ask, .. }) => { -//! let _root = &contract.root; // symbol / option root +//! let _symbol = &contract.symbol; //! let _ = (bid, ask); // f64 prices //! } //! FpssEvent::Data(FpssData::Trade { contract, price, size, .. }) => { -//! let _root = &contract.root; +//! let _symbol = &contract.symbol; //! let _ = (price, size); //! } //! FpssEvent::Control(_) => { /* lifecycle */ } diff --git a/crates/thetadatadx/src/fpss/protocol.rs b/crates/thetadatadx/src/fpss/protocol.rs index 09154300..47292705 100644 --- a/crates/thetadatadx/src/fpss/protocol.rs +++ b/crates/thetadatadx/src/fpss/protocol.rs @@ -79,18 +79,26 @@ pub const READ_TIMEOUT_MS: u64 = 10_000; /// A contract identifier for FPSS subscriptions. /// /// Matches the wire format from `Contract.java`: -/// - Stock/Index/Rate: root ticker + security type -/// - Option: root ticker + security type + expiration + call/put + strike +/// - Stock/Index/Rate: ticker + security type +/// - Option: ticker + security type + expiration + call/put + strike +/// +/// The public field names (`symbol`, `expiration`) follow the v3 vendor +/// surface; the binary wire format still calls the same bytes `root` / +/// `exp_date` in `Contract.toBytes()` / `Contract.fromBytes()` and that +/// internal byte-level naming is preserved verbatim in the doc comments +/// and parser locals to keep this file diff-able against the decompiled +/// terminal source. See: +/// . /// /// Source: `Contract.java` — `toBytes()`, `fromBytes()`, constructor overloads. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Contract { - /// Root ticker symbol (ASCII, max ~6 chars in practice). - pub root: String, + /// Underlying ticker symbol (ASCII, max 16 chars on the wire). + pub symbol: String, /// Security type. pub sec_type: SecType, /// Expiration date as YYYYMMDD integer (options only). - pub exp_date: Option, + pub expiration: Option, /// True = call, false = put (options only). pub is_call: Option, /// Strike price in fixed-point (options only). The encoding matches @@ -102,33 +110,33 @@ impl Contract { /// Create a stock contract. /// /// Source: `Contract(String root)` constructor in `Contract.java` — defaults to STOCK. - pub fn stock(root: impl Into) -> Self { + pub fn stock(symbol: impl Into) -> Self { Self { - root: root.into(), + symbol: symbol.into(), sec_type: SecType::Stock, - exp_date: None, + expiration: None, is_call: None, strike: None, } } /// Create an index contract. - pub fn index(root: impl Into) -> Self { + pub fn index(symbol: impl Into) -> Self { Self { - root: root.into(), + symbol: symbol.into(), sec_type: SecType::Index, - exp_date: None, + expiration: None, is_call: None, strike: None, } } /// Create a rate contract. - pub fn rate(root: impl Into) -> Self { + pub fn rate(symbol: impl Into) -> Self { Self { - root: root.into(), + symbol: symbol.into(), sec_type: SecType::Rate, - exp_date: None, + expiration: None, is_call: None, strike: None, } @@ -137,8 +145,8 @@ impl Contract { /// Create an option contract. /// /// # Arguments - /// - `root`: Underlying ticker (e.g., `"AAPL"`) - /// - `exp_date`: Expiration as `"YYYYMMDD"` (e.g., `"20260320"`) + /// - `symbol`: Underlying ticker (e.g., `"AAPL"`) + /// - `expiration`: Expiration as `"YYYYMMDD"` (e.g., `"20260320"`) /// - `strike`: Strike price in dollars as string (e.g., `"550"`) /// - `right`: option right — accepts `"call"`/`"put"`/`"C"`/`"P"` /// (case-insensitive). FPSS per-contract subscriptions cannot carry @@ -146,19 +154,19 @@ impl Contract { /// /// # Errors /// - /// Returns [`Error::Config`] if `exp_date` is not a valid integer date, + /// Returns [`Error::Config`] if `expiration` is not a valid integer date, /// if `right` cannot be parsed to a single side, if `strike` is not a /// valid f64, or if `strike * 1000` would overflow `i32`. pub fn option( - root: impl Into, - exp_date: &str, + symbol: impl Into, + expiration: &str, strike: &str, right: &str, ) -> Result { - let exp: i32 = exp_date + let exp: i32 = expiration .replace('-', "") .parse() - .map_err(|e| Error::Config(format!("invalid expiration date {exp_date:?}: {e}")))?; + .map_err(|e| Error::Config(format!("invalid expiration date {expiration:?}: {e}")))?; let is_call = tdbe::right::parse_right_strict(right)? .as_is_call() .ok_or_else(|| { @@ -180,9 +188,9 @@ impl Contract { #[allow(clippy::cast_possible_truncation)] let strike_raw = strike_scaled as i32; Ok(Self { - root: root.into(), + symbol: symbol.into(), sec_type: SecType::Option, - exp_date: Some(exp), + expiration: Some(exp), is_call: Some(is_call), strike: Some(strike_raw), }) @@ -192,11 +200,16 @@ impl Contract { /// /// Prefer [`Contract::option`] for user-facing code. This constructor is for the /// drop-in REST/WS server which must match the Java terminal's contract format. - pub fn option_raw(root: impl Into, exp_date: i32, is_call: bool, strike: i32) -> Self { + pub fn option_raw( + symbol: impl Into, + expiration: i32, + is_call: bool, + strike: i32, + ) -> Self { Self { - root: root.into(), + symbol: symbol.into(), sec_type: SecType::Option, - exp_date: Some(exp_date), + expiration: Some(expiration), is_call: Some(is_call), strike: Some(strike), } @@ -207,7 +220,7 @@ impl Contract { /// /// Full-type subscriptions are not addressed by a real `Contract`, but the /// failure list keeps a homogeneous `(SubscriptionKind, Contract)` shape - /// so callers can iterate the list with one match arm. `root` is empty + /// so callers can iterate the list with one match arm. `symbol` is empty /// and option fields are `None`, which mirrors the lack of per-contract /// addressability for a full-type subscription. Operators see the /// original `SecType` via the per-failure `tracing::warn!` line emitted @@ -215,9 +228,9 @@ impl Contract { #[must_use] pub fn full_type_marker(sec_type: SecType) -> Self { Self { - root: String::new(), + symbol: String::new(), sec_type, - exp_date: None, + expiration: None, is_call: None, strike: None, } @@ -314,10 +327,13 @@ impl Contract { ))); } let bytes = input.as_bytes(); - // Root: bytes 0..6, right-padded with spaces. + // OCC-21 root field: bytes 0..6, right-padded with spaces. The + // wire/OCC-21 layout calls this region the "root"; we keep the + // local name matching the spec and assign it to the v3 + // `symbol` field on the Rust `Contract` struct below. let root_raw = &input[0..6]; - let root = root_raw.trim_end_matches(' ').to_string(); - Self::validate_root(input, &root)?; + let symbol = root_raw.trim_end_matches(' ').to_string(); + Self::validate_root(input, &symbol)?; // YYMMDD -> integer, then centuried to YYYYMMDD. let yymmdd_raw = &input[6..12]; @@ -336,7 +352,7 @@ impl Contract { // maps to 2099 rather than 1999. The FPSS live feed only // ships contracts with future expirations, so pre-2000 OCC // symbols cannot reach this parser over the wire. - let exp_date: i32 = 20_000_000 + yymmdd; + let expiration: i32 = 20_000_000 + yymmdd; // Right byte. let right_byte = bytes[12]; @@ -367,9 +383,9 @@ impl Contract { })?; Ok(Self { - root, + symbol, sec_type: SecType::Option, - exp_date: Some(exp_date), + expiration: Some(expiration), is_call: Some(is_call), strike: Some(strike), }) @@ -433,23 +449,27 @@ impl Contract { /// /// # Errors /// - /// Returns [`Error::Config`] if the root is empty or longer than 16 + /// Returns [`Error::Config`] if the symbol is empty or longer than 16 /// bytes. The 16-byte limit matches Java's `Contract.toBytes()`. pub fn validate(&self) -> Result<(), Error> { - let len = self.root.len(); + let len = self.symbol.len(); if len == 0 { - return Err(Error::Config("contract root is empty".into())); + return Err(Error::Config("contract symbol is empty".into())); } if len > 16 { return Err(Error::Config(format!( - "contract root too long: {len} bytes (max 16 to match Java Contract.toBytes())" + "contract symbol too long: {len} bytes (max 16 to match Java Contract.toBytes())" ))); } Ok(()) } fn encode_unchecked(&self) -> Vec { - let root_bytes = self.root.as_bytes(); + // Wire format keeps the legacy "root" naming for the byte-level + // length prefix and slice; the public Rust struct field is + // `symbol` per the v3 vendor surface, so we read from + // `self.symbol` and emit identical bytes. + let root_bytes = self.symbol.as_bytes(); let root_len = u8::try_from(root_bytes.len()).expect("validate() bounds root_len to <= 16"); let is_option = self.sec_type == SecType::Option; @@ -476,7 +496,7 @@ impl Contract { if is_option { // exp_date: i32 big-endian - buf.extend_from_slice(&self.exp_date.unwrap_or(0).to_be_bytes()); + buf.extend_from_slice(&self.expiration.unwrap_or(0).to_be_bytes()); // is_call: u8 (1 = call, 0 = put) buf.push(u8::from(self.is_call.unwrap_or(false))); // strike: i32 big-endian @@ -519,7 +539,10 @@ impl Contract { let root_start = 2; let root_end = root_start + root_len; - let root = std::str::from_utf8(&data[root_start..root_end]) + // Wire-format ASCII payload of the legacy "root" length-prefixed + // slice. Bound to `symbol` here so the struct construction below + // mirrors the v3 public field name. + let symbol = std::str::from_utf8(&data[root_start..root_end]) .map_err(|_| ContractParseError::InvalidUtf8)? .to_string(); @@ -534,7 +557,7 @@ impl Contract { return Err(ContractParseError::TooShort); } - let exp_date = i32::from_be_bytes([ + let expiration = i32::from_be_bytes([ data[opt_start], data[opt_start + 1], data[opt_start + 2], @@ -550,9 +573,9 @@ impl Contract { Ok(( Contract { - root, + symbol, sec_type, - exp_date: Some(exp_date), + expiration: Some(expiration), is_call: Some(is_call), strike: Some(strike), }, @@ -561,9 +584,9 @@ impl Contract { } else { Ok(( Contract { - root, + symbol, sec_type, - exp_date: None, + expiration: None, is_call: None, strike: None, }, @@ -585,14 +608,14 @@ impl std::fmt::Display for Contract { write!( f, "{} {} {} {} {}", - self.root, + self.symbol, self.sec_type.as_str(), - self.exp_date.unwrap_or(0), + self.expiration.unwrap_or(0), right, self.strike.unwrap_or(0), ) } - _ => write!(f, "{} {}", self.root, self.sec_type.as_str()), + _ => write!(f, "{} {}", self.symbol, self.sec_type.as_str()), } } } @@ -604,29 +627,31 @@ impl std::str::FromStr for Contract { /// /// Two formats are accepted: /// - /// 1. **Bare root** (stock contract). 1..=6 ASCII uppercase letters, + /// 1. **Bare symbol** (stock contract). 1..=16 ASCII uppercase letters, /// optionally including a single `.`: /// ``` /// # use std::str::FromStr; /// # use thetadatadx::fpss::protocol::Contract; /// let c = "AAPL".parse::().unwrap(); - /// assert_eq!(c.root, "AAPL"); + /// assert_eq!(c.symbol, "AAPL"); /// ``` /// 2. **OCC-21 option identifier**. 21 ASCII characters: /// `[root (6, space-padded)] [YYMMDD (6)] [C|P (1)] [strike (8, 1/1000$)]`. - /// Both 21-char (canonical) and 20-char (one space inside the - /// root-padding region) inputs are accepted. The 20-char form is - /// repaired by peeling off the fixed 15-char `YYMMDD+right+strike` - /// suffix and right-padding the leading root slice to 6 chars — - /// byte-for-byte equivalent to the 21-char form for the same - /// contract, matching the tolerant upstream feed that occasionally - /// ships the 20-char variant. + /// The OCC-21 layout still calls the leading 6-byte field `root`; + /// on the Rust struct it lands in the `symbol` field. Both 21-char + /// (canonical) and 20-char (one space inside the root-padding + /// region) inputs are accepted. The 20-char form is repaired by + /// peeling off the fixed 15-char `YYMMDD+right+strike` suffix and + /// right-padding the leading root slice to 6 chars — byte-for-byte + /// equivalent to the 21-char form for the same contract, matching + /// the tolerant upstream feed that occasionally ships the 20-char + /// variant. /// ``` /// # use std::str::FromStr; /// # use thetadatadx::fpss::protocol::Contract; /// let c = "SPY 260417C00550000".parse::().unwrap(); - /// assert_eq!(c.root, "SPY"); - /// assert_eq!(c.exp_date, Some(20_260_417)); + /// assert_eq!(c.symbol, "SPY"); + /// assert_eq!(c.expiration, Some(20_260_417)); /// assert_eq!(c.is_call, Some(true)); /// assert_eq!(c.strike, Some(550_000)); /// ``` @@ -635,7 +660,7 @@ impl std::str::FromStr for Contract { /// /// Returns [`Error::Config`] whose message names the specific failure /// (wrong length, non-ASCII, bad YYMMDD digits, right outside `C`/`P`, - /// bad strike digits, empty root, non-uppercase root character) and + /// bad strike digits, empty symbol, non-uppercase character) and /// includes the original input. fn from_str(input: &str) -> Result { // Strategy: the two formats are unambiguous by length after trim. @@ -1002,7 +1027,7 @@ mod tests { let (parsed, consumed) = Contract::from_bytes(&bytes).unwrap(); assert_eq!(consumed, 15); assert_eq!(parsed, c); - assert_eq!(parsed.exp_date, Some(20261218)); + assert_eq!(parsed.expiration, Some(20261218)); assert_eq!(parsed.is_call, Some(true)); assert_eq!(parsed.strike, Some(60000)); } @@ -1012,7 +1037,7 @@ mod tests { let c = Contract::index("SPX"); let bytes = c.to_bytes(); let (parsed, _) = Contract::from_bytes(&bytes).unwrap(); - assert_eq!(parsed.root, "SPX"); + assert_eq!(parsed.symbol, "SPX"); assert_eq!(parsed.sec_type, SecType::Index); } @@ -1270,9 +1295,9 @@ mod tests { fn from_str_bare_root_stock() { use std::str::FromStr; let c = Contract::from_str("AAPL").unwrap(); - assert_eq!(c.root, "AAPL"); + assert_eq!(c.symbol, "AAPL"); assert_eq!(c.sec_type, SecType::Stock); - assert!(c.exp_date.is_none()); + assert!(c.expiration.is_none()); assert!(c.is_call.is_none()); assert!(c.strike.is_none()); } @@ -1281,7 +1306,7 @@ mod tests { fn from_str_bare_root_short_ticker() { use std::str::FromStr; let c = Contract::from_str("A").unwrap(); - assert_eq!(c.root, "A"); + assert_eq!(c.symbol, "A"); assert_eq!(c.sec_type, SecType::Stock); } @@ -1290,7 +1315,7 @@ mod tests { use std::str::FromStr; // BRK.A style tickers must parse as stock roots. let c = Contract::from_str("BRK.A").unwrap(); - assert_eq!(c.root, "BRK.A"); + assert_eq!(c.symbol, "BRK.A"); assert_eq!(c.sec_type, SecType::Stock); } @@ -1298,7 +1323,7 @@ mod tests { fn from_str_bare_root_trims_surrounding_whitespace() { use std::str::FromStr; let c = Contract::from_str(" SPY ").unwrap(); - assert_eq!(c.root, "SPY"); + assert_eq!(c.symbol, "SPY"); } #[test] @@ -1306,9 +1331,9 @@ mod tests { use std::str::FromStr; // SPY (4 chars -> 6 chars padded) 26-04-17 Call 550.00. let c = Contract::from_str("SPY 260417C00550000").unwrap(); - assert_eq!(c.root, "SPY"); + assert_eq!(c.symbol, "SPY"); assert_eq!(c.sec_type, SecType::Option); - assert_eq!(c.exp_date, Some(20_260_417)); + assert_eq!(c.expiration, Some(20_260_417)); assert_eq!(c.is_call, Some(true)); assert_eq!(c.strike, Some(550_000)); } @@ -1318,9 +1343,9 @@ mod tests { use std::str::FromStr; // QQQ 26-06-20 Put 350.00. let c = Contract::from_str("QQQ 260620P00350000").unwrap(); - assert_eq!(c.root, "QQQ"); + assert_eq!(c.symbol, "QQQ"); assert_eq!(c.is_call, Some(false)); - assert_eq!(c.exp_date, Some(20_260_620)); + assert_eq!(c.expiration, Some(20_260_620)); assert_eq!(c.strike, Some(350_000)); } @@ -1329,8 +1354,8 @@ mod tests { use std::str::FromStr; // The exact example from the spec. let c = Contract::from_str("AAPL 260417C00550000").unwrap(); - assert_eq!(c.root, "AAPL"); - assert_eq!(c.exp_date, Some(20_260_417)); + assert_eq!(c.symbol, "AAPL"); + assert_eq!(c.expiration, Some(20_260_417)); assert_eq!(c.is_call, Some(true)); assert_eq!(c.strike, Some(550_000)); } @@ -1340,8 +1365,8 @@ mod tests { use std::str::FromStr; // Full six-char root: no spaces in the root field. let c = Contract::from_str("ABCDEF260417C00550000").unwrap(); - assert_eq!(c.root, "ABCDEF"); - assert_eq!(c.exp_date, Some(20_260_417)); + assert_eq!(c.symbol, "ABCDEF"); + assert_eq!(c.expiration, Some(20_260_417)); assert_eq!(c.strike, Some(550_000)); } @@ -1458,7 +1483,7 @@ mod tests { fn from_str_accepts_seven_char_root() { use std::str::FromStr; let c = Contract::from_str("ABCDEFG").expect("7-char root must parse"); - assert_eq!(c.root, "ABCDEFG"); + assert_eq!(c.symbol, "ABCDEFG"); assert_eq!(c.sec_type, SecType::Stock); } @@ -1469,7 +1494,7 @@ mod tests { let sixteen = "AAAAAAAAAAAAAAAA"; assert_eq!(sixteen.len(), 16); let c = Contract::from_str(sixteen).expect("16-char root must parse"); - assert_eq!(c.root, sixteen); + assert_eq!(c.symbol, sixteen); assert_eq!(c.sec_type, SecType::Stock); } @@ -1484,7 +1509,7 @@ mod tests { let root: String = "A".repeat(n); let parsed = Contract::from_str(&root) .unwrap_or_else(|_| panic!("from_str must accept {n}-char root")); - assert_eq!(parsed.root, root); + assert_eq!(parsed.symbol, root); let wire = parsed.to_bytes(); let (decoded, consumed) = Contract::from_bytes(&wire) .unwrap_or_else(|_| panic!("from_bytes must decode {n}-char root")); @@ -1534,8 +1559,8 @@ mod tests { // with a trailing space, which shifted the right-byte into a // digit slot and either errored or decoded a different contract. assert_eq!(c20, c21, "20-char and 21-char forms must parse identically"); - assert_eq!(c20.root, "SPY"); - assert_eq!(c20.exp_date, Some(20_260_417)); + assert_eq!(c20.symbol, "SPY"); + assert_eq!(c20.expiration, Some(20_260_417)); assert_eq!(c20.is_call, Some(true)); assert_eq!(c20.strike, Some(550_000)); } @@ -1548,7 +1573,7 @@ mod tests { let twenty = "T 260417C00150000"; assert_eq!(twenty.len(), 20); let c = Contract::from_str(twenty).expect("20-char OCC-21 with short root must repair"); - assert_eq!(c.root, "T"); + assert_eq!(c.symbol, "T"); assert_eq!(c.is_call, Some(true)); assert_eq!(c.strike, Some(150_000)); } @@ -1563,8 +1588,12 @@ mod tests { // the live FPSS feed ships only live contracts so a pre-2000 // OCC symbol cannot reach this parser over the wire. let c = Contract::from_str("AAPL 990101C00100000").expect("YY=99 must parse"); - assert_eq!(c.exp_date, Some(20_990_101), "YY=99 must map to 2099-01-01"); - assert_eq!(c.root, "AAPL"); + assert_eq!( + c.expiration, + Some(20_990_101), + "YY=99 must map to 2099-01-01" + ); + assert_eq!(c.symbol, "AAPL"); assert_eq!(c.is_call, Some(true)); assert_eq!(c.strike, Some(100_000)); } @@ -1593,6 +1622,6 @@ mod tests { // Industry-practice single-dot compound tickers MUST still // parse — keeping BRK.A / BRK.B / RDS.A reachable. let c = Contract::from_str("BRK.B").expect("single-dot root must parse"); - assert_eq!(c.root, "BRK.B"); + assert_eq!(c.symbol, "BRK.B"); } } diff --git a/crates/thetadatadx/src/frames_generated.rs b/crates/thetadatadx/src/frames_generated.rs index dab0478d..4e147bb0 100644 --- a/crates/thetadatadx/src/frames_generated.rs +++ b/crates/thetadatadx/src/frames_generated.rs @@ -847,24 +847,24 @@ impl crate::frames::TicksPolarsExt for [tdbe::types::tick::OpenInterestTick] { impl crate::frames::TicksArrowExt for [tdbe::types::tick::OptionContract] { fn to_arrow(&self) -> ::core::result::Result { let n = self.len(); - let mut col_root: Vec = Vec::with_capacity(n); + let mut col_symbol: Vec = Vec::with_capacity(n); 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); for t in self { - col_root.push(t.root.clone()); + col_symbol.push(t.symbol.clone()); col_expiration.push(t.expiration); col_strike.push(t.strike); col_right.push(if t.is_call() { "C".to_string() } else if t.is_put() { "P".to_string() } else { String::new() }); } let schema = Arc::new(ArrowSchema::new(vec![ - Field::new("root", DataType::Utf8, false), + Field::new("symbol", DataType::Utf8, false), Field::new("expiration", DataType::Int32, false), Field::new("strike", DataType::Float64, false), Field::new("right", DataType::Utf8, false), ])); let columns: Vec = vec![ - Arc::new(StringArray::from(col_root)) as ArrayRef, + Arc::new(StringArray::from(col_symbol)) as ArrayRef, Arc::new(Int32Array::from(col_expiration)) as ArrayRef, Arc::new(Float64Array::from(col_strike)) as ArrayRef, Arc::new(StringArray::from(col_right)) as ArrayRef, @@ -878,18 +878,18 @@ impl crate::frames::TicksArrowExt for [tdbe::types::tick::OptionContract] { impl crate::frames::TicksPolarsExt for [tdbe::types::tick::OptionContract] { fn to_polars(&self) -> PolarsResult { let n = self.len(); - let mut col_root: Vec = Vec::with_capacity(n); + let mut col_symbol: Vec = Vec::with_capacity(n); 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); for t in self { - col_root.push(t.root.clone()); + col_symbol.push(t.symbol.clone()); col_expiration.push(t.expiration); col_strike.push(t.strike); col_right.push(if t.is_call() { "C".to_string() } else if t.is_put() { "P".to_string() } else { String::new() }); } DataFrame::new(vec![ - Series::new(PlSmallStr::from_static("root"), col_root).into(), + Series::new(PlSmallStr::from_static("symbol"), col_symbol).into(), 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(), diff --git a/crates/thetadatadx/src/unified.rs b/crates/thetadatadx/src/unified.rs index 8120da58..306cfecb 100644 --- a/crates/thetadatadx/src/unified.rs +++ b/crates/thetadatadx/src/unified.rs @@ -783,7 +783,7 @@ mod tests { &per_contract, &full_type, |_kind, contract| { - if contract.root == "MSFT" { + if contract.symbol == "MSFT" { Err(Error::Fpss { kind: crate::error::FpssErrorKind::Disconnected, message: "injected: MSFT subscribe rejected".to_string(), @@ -844,9 +844,9 @@ mod tests { assert_eq!(*kind, SubscriptionKind::OpenInterest); assert_eq!(contract.sec_type, SecType::Option); assert!( - contract.root.is_empty(), - "full-type marker carries empty root, got {:?}", - contract.root + contract.symbol.is_empty(), + "full-type marker carries empty symbol, got {:?}", + contract.symbol ); } diff --git a/crates/thetadatadx/tests/flatfiles_synthetic_golden.rs b/crates/thetadatadx/tests/flatfiles_synthetic_golden.rs index 728bb508..5492d82f 100644 --- a/crates/thetadatadx/tests/flatfiles_synthetic_golden.rs +++ b/crates/thetadatadx/tests/flatfiles_synthetic_golden.rs @@ -121,7 +121,10 @@ fn synthetic_option_blob() -> Vec { /// /// Manual derivation: /// - header columns (price_type column suppressed by the writer): -/// `root,expiration,strike,right,ms_of_day,bid,date`. +/// `symbol,expiration,strike,right,ms_of_day,bid,date`. The leading +/// contract-key column was renamed from `root` to `symbol` to match +/// the v3 vendor surface; see the migration guide: +/// . /// - row 1: contract prefix `SPY,20240315,580000,C` plus /// `34200000,123.45,20240315`. Bid=12345 with price_type=8 → /// `12345 / 10^(10-8) = 12345 / 100 = 123.45`. @@ -129,7 +132,7 @@ fn synthetic_option_blob() -> Vec { /// 34200100; bid delta +5 → 12350 → 123.5 (Rust f64 Display drops /// the trailing zero); date carried forward → 20240315. const EXPECTED_CSV: &str = "\ -root,expiration,strike,right,ms_of_day,bid,date +symbol,expiration,strike,right,ms_of_day,bid,date SPY,20240315,580000,C,34200000,123.45,20240315 SPY,20240315,580000,C,34200100,123.5,20240315 "; diff --git a/crates/thetadatadx/tests/test_frames_arrow.rs b/crates/thetadatadx/tests/test_frames_arrow.rs index 880aa19d..9cd303fe 100644 --- a/crates/thetadatadx/tests/test_frames_arrow.rs +++ b/crates/thetadatadx/tests/test_frames_arrow.rs @@ -100,13 +100,13 @@ fn quote_tick_to_arrow_emits_midpoint() { fn option_contract_right_stringifies() { let ticks = vec![ tick::OptionContract { - root: "AAPL".into(), + symbol: "AAPL".into(), expiration: 20240119, strike: 195.0, right: 67, }, tick::OptionContract { - root: "AAPL".into(), + symbol: "AAPL".into(), expiration: 20240119, strike: 195.0, right: 80, @@ -115,7 +115,7 @@ fn option_contract_right_stringifies() { let batch = ticks.as_slice().to_arrow().unwrap(); assert_eq!(batch.num_rows(), 2); assert_eq!(dtype_of(&batch, "right"), DataType::Utf8); - assert_eq!(dtype_of(&batch, "root"), DataType::Utf8); + assert_eq!(dtype_of(&batch, "symbol"), DataType::Utf8); } #[test] diff --git a/crates/thetadatadx/tests/test_frames_polars.rs b/crates/thetadatadx/tests/test_frames_polars.rs index 0a95afd5..9b8df72f 100644 --- a/crates/thetadatadx/tests/test_frames_polars.rs +++ b/crates/thetadatadx/tests/test_frames_polars.rs @@ -101,13 +101,13 @@ fn quote_tick_to_polars_emits_midpoint() { fn option_contract_right_stringifies() { let ticks = vec![ tick::OptionContract { - root: "AAPL".into(), + symbol: "AAPL".into(), expiration: 20240119, strike: 195.0, right: 67, // 'C' }, tick::OptionContract { - root: "AAPL".into(), + symbol: "AAPL".into(), expiration: 20240119, strike: 195.0, right: 80, // 'P' @@ -116,7 +116,7 @@ fn option_contract_right_stringifies() { let df = ticks.as_slice().to_polars().unwrap(); assert_eq!(df.height(), 2); assert_eq!(dtype_of(&df, "right"), DataType::String); - assert_eq!(dtype_of(&df, "root"), DataType::String); + assert_eq!(dtype_of(&df, "symbol"), DataType::String); } #[test] diff --git a/crates/thetadatadx/tick_schema.toml b/crates/thetadatadx/tick_schema.toml index 83eb27c2..14e031f3 100644 --- a/crates/thetadatadx/tick_schema.toml +++ b/crates/thetadatadx/tick_schema.toml @@ -271,12 +271,12 @@ columns = [ ] [types.OptionContract] -doc = "Option contract -- 4 fields. Contract specification.\n\nCannot be `Copy` because of the `String` root field." +doc = "Option contract -- 4 fields. Contract specification.\n\nCannot be `Copy` because of the `String` symbol field. The vendor v3 surface renamed the underlying ticker field from `root` to `symbol`; see https://docs.thetadata.us/Articles/Getting-Started/v2-migration-guide.html#_5-parameter-mapping." copy = false parser = "parse_option_contracts" -required = ["root"] +required = ["symbol"] columns = [ - { name = "root", field = "root", type = "String" }, + { name = "symbol", field = "symbol", type = "String" }, { name = "expiration", field = "expiration", type = "i32" }, { name = "strike", field = "strike", type = "price" }, { name = "right", field = "right", type = "i32" }, diff --git a/docs-site/docs/changelog.md b/docs-site/docs/changelog.md index ef13d5f5..d4de4e58 100644 --- a/docs-site/docs/changelog.md +++ b/docs-site/docs/changelog.md @@ -7,6 +7,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [8.0.25] - 2026-05-04 + +### Changed + +- **Renamed `Contract.root` → `Contract.symbol` and `Contract.exp_date` → + `Contract.expiration` to match the v3 vendor surface** (#467). The same + rename lands on every public type that carries those fields: + `tdbe::OptionContract.root` → `.symbol`, `FlatFileRow.root` → `.symbol`, + and the flat-file CSV / JSONL contract-prefix headers go from + `root,expiration,...` to `symbol,expiration,...`. Constructor and method + parameter names follow the field rename: `Contract::stock(symbol)`, + `Contract::option(symbol, expiration, ...)`, `Contract::option_raw(...)`. + Generated bindings (Python `Contract.symbol` / `Contract.expiration`, + TypeScript `contract.symbol` / `contract.expiration`, Go + `Contract.Symbol` / `Contract.Expiration`, C++ / C ABI + `TdxContract.symbol` / `TdxContract.has_expiration` / + `TdxContract.expiration`, `TdxOptionContract.symbol`) follow the same + rename. The wire format is unchanged; the rename is purely cosmetic on + the public API to align with the + [vendor v3 migration guide](https://docs.thetadata.us/Articles/Getting-Started/v2-migration-guide.html#_5-parameter-mapping). + Migration: + ```rust + // before (v8.0.24) + let c = Contract::stock("AAPL"); + let _ = c.root; + let opt = Contract::option("SPY", "20260417", "550", "C")?; + let _ = opt.exp_date; + + // after (v8.0.25) + let c = Contract::stock("AAPL"); + let _ = c.symbol; + let opt = Contract::option("SPY", "20260417", "550", "C")?; + let _ = opt.expiration; + ``` +- `tdbe` 0.12.5 → 0.12.6. + ## [8.0.24] - 2026-05-04 ### Added diff --git a/docs-site/docs/getting-started/quickstart.md b/docs-site/docs/getting-started/quickstart.md index ef3443c5..606fa0ac 100644 --- a/docs-site/docs/getting-started/quickstart.md +++ b/docs-site/docs/getting-started/quickstart.md @@ -221,10 +221,10 @@ async fn main() -> Result<(), thetadatadx::Error> { tdx.start_streaming(|event: &FpssEvent| match event { FpssEvent::Data(FpssData::Quote { contract, bid, ask, .. }) => { - println!("Quote: {} {bid:.2}/{ask:.2}", contract.root); + println!("Quote: {} {bid:.2}/{ask:.2}", contract.symbol); } FpssEvent::Data(FpssData::Trade { contract, price, size, .. }) => { - println!("Trade: {} {price:.2} x {size}", contract.root); + println!("Trade: {} {price:.2} x {size}", contract.symbol); } _ => {} })?; diff --git a/docs-site/docs/getting-started/streaming.md b/docs-site/docs/getting-started/streaming.md index 7b311d44..4c6f4e3e 100644 --- a/docs-site/docs/getting-started/streaming.md +++ b/docs-site/docs/getting-started/streaming.md @@ -69,10 +69,10 @@ async fn main() -> Result<(), thetadatadx::Error> { tdx.start_streaming(|event: &FpssEvent| match event { FpssEvent::Data(FpssData::Quote { contract, bid, ask, .. }) => { - println!("Quote: {} {bid:.2}/{ask:.2}", contract.root); + println!("Quote: {} {bid:.2}/{ask:.2}", contract.symbol); } FpssEvent::Data(FpssData::Trade { contract, price, size, .. }) => { - println!("Trade: {} {price:.2} x {size}", contract.root); + println!("Trade: {} {price:.2} x {size}", contract.symbol); } _ => {} })?; diff --git a/docs-site/docs/streaming/connection.md b/docs-site/docs/streaming/connection.md index f50fdd44..d7e195a3 100644 --- a/docs-site/docs/streaming/connection.md +++ b/docs-site/docs/streaming/connection.md @@ -37,10 +37,10 @@ tdx.start_streaming(|event: &FpssEvent| { // Every data event carries an `Arc` — read the root ticker // directly, no contract-ID map lookup required. FpssEvent::Data(FpssData::Quote { contract, bid, ask, received_at_ns, .. }) => { - println!("Quote: {} bid={bid:.2} ask={ask:.2} rx={received_at_ns}ns", contract.root); + println!("Quote: {} bid={bid:.2} ask={ask:.2} rx={received_at_ns}ns", contract.symbol); } FpssEvent::Data(FpssData::Trade { contract, price, size, received_at_ns, .. }) => { - println!("Trade: {} price={price:.2} size={size} rx={received_at_ns}ns", contract.root); + println!("Trade: {} price={price:.2} size={size} rx={received_at_ns}ns", contract.symbol); } FpssEvent::Control(FpssControl::ContractAssigned { id, contract }) => { println!("Contract {id} = {contract}"); @@ -377,9 +377,9 @@ tdx.start_streaming(move |event: &FpssEvent| { FpssEvent::Control(FpssControl::ContractAssigned { id, contract }) => { contracts_clone.lock().unwrap().insert(*id, (**contract).clone()); } - // Preferred: read `contract.root` directly off the data event. + // Preferred: read `contract.symbol` directly off the data event. FpssEvent::Data(FpssData::Quote { contract, bid, ask, .. }) => { - println!("{}: bid={bid:.2} ask={ask:.2}", contract.root); + println!("{}: bid={bid:.2} ask={ask:.2}", contract.symbol); } _ => {} } diff --git a/docs-site/docs/streaming/events.md b/docs-site/docs/streaming/events.md index edb754d1..071c086f 100644 --- a/docs-site/docs/streaming/events.md +++ b/docs-site/docs/streaming/events.md @@ -12,31 +12,31 @@ description: Process data and control events from the FPSS streaming connection tdx.start_streaming(|event: &FpssEvent| { match event { // --- Data events --- - // Each data variant carries an `Arc`, so `contract.root` - // (plus `.exp_date` / `.strike` / `.is_call` on options) is readable + // Each data variant carries an `Arc`, so `contract.symbol` + // (plus `.expiration` / `.strike` / `.is_call` on options) is readable // inline — no contract-ID map lookup required. FpssEvent::Data(FpssData::Quote { contract, ms_of_day, bid, ask, bid_size, ask_size, received_at_ns, .. }) => { - println!("Quote: {} bid={bid:.2} ask={ask:.2} rx={received_at_ns}ns", contract.root); + println!("Quote: {} bid={bid:.2} ask={ask:.2} rx={received_at_ns}ns", contract.symbol); } FpssEvent::Data(FpssData::Trade { contract, price, size, sequence, received_at_ns, .. }) => { - println!("Trade: {} price={price:.2} size={size} seq={sequence}", contract.root); + println!("Trade: {} price={price:.2} size={size} seq={sequence}", contract.symbol); } FpssEvent::Data(FpssData::OpenInterest { contract, open_interest, received_at_ns, .. }) => { - println!("OI: {} oi={open_interest} rx={received_at_ns}ns", contract.root); + println!("OI: {} oi={open_interest} rx={received_at_ns}ns", contract.symbol); } FpssEvent::Data(FpssData::Ohlcvc { contract, open, high, low, close, volume, count, received_at_ns, .. }) => { // volume and count are i64 to avoid overflow on high-volume symbols - println!("OHLCVC: {} O={open:.2} H={high:.2} L={low:.2} C={close:.2} vol={volume} n={count}", contract.root); + println!("OHLCVC: {} O={open:.2} H={high:.2} L={low:.2} C={close:.2} vol={volume} n={count}", contract.symbol); } // --- Control events --- diff --git a/docs-site/docs/streaming/reconnection.md b/docs-site/docs/streaming/reconnection.md index 0c94970f..a4dabd22 100644 --- a/docs-site/docs/streaming/reconnection.md +++ b/docs-site/docs/streaming/reconnection.md @@ -203,7 +203,7 @@ async fn main() -> Result<(), thetadatadx::Error> { }) => { if let Some(c) = contracts_clone.lock().unwrap().get(contract_id) { println!("[QUOTE] {}: bid={bid:.2} ask={ask:.2} rx={received_at_ns}ns", - c.root); + c.symbol); } } FpssEvent::Data(FpssData::Trade { @@ -211,7 +211,7 @@ async fn main() -> Result<(), thetadatadx::Error> { }) => { if let Some(c) = contracts_clone.lock().unwrap().get(contract_id) { println!("[TRADE] {}: price={price:.2} size={size} rx={received_at_ns}ns", - c.root); + c.symbol); } } FpssEvent::Control(FpssControl::Disconnected { reason }) => { diff --git a/ffi/Cargo.toml b/ffi/Cargo.toml index 9e7e2b4b..3af82b38 100644 --- a/ffi/Cargo.toml +++ b/ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thetadatadx-ffi" -version = "8.0.24" +version = "8.0.25" edition.workspace = true rust-version.workspace = true authors.workspace = true @@ -31,7 +31,7 @@ testing-panic-boundary = [] [dependencies] thetadatadx = { path = "../crates/thetadatadx" } -tdbe = { version = "0.12.5", path = "../crates/tdbe" } +tdbe = { version = "0.12.6", path = "../crates/tdbe" } tokio = { version = "1.52.1", features = ["rt-multi-thread"] } # Used by the FPSS streaming callback silent-drop observability path # (see `tdx_fpss_dropped_events` / `tdx_unified_dropped_events`). Keep diff --git a/ffi/src/fpss_event_converter.rs b/ffi/src/fpss_event_converter.rs index bdeeb283..cc961dbc 100644 --- a/ffi/src/fpss_event_converter.rs +++ b/ffi/src/fpss_event_converter.rs @@ -20,19 +20,19 @@ pub(crate) fn fpss_event_to_ffi(event: &thetadatadx::fpss::FpssEvent) -> FfiBuff received_at_ns, .. }) => { - let contract_root_cstring = if contract.root.is_empty() { + let contract_symbol_cstring: Option = if contract.symbol.is_empty() { None } else { - std::ffi::CString::new(contract.root.as_str()).ok() + std::ffi::CString::new(contract.symbol.as_str()).ok() }; - let contract_root_ptr = contract_root_cstring + let contract_symbol_ptr = contract_symbol_cstring .as_ref() .map_or(ptr::null(), |cs| cs.as_ptr()); let tdx_contract = TdxContract { - root: contract_root_ptr, + symbol: contract_symbol_ptr, sec_type: contract.sec_type as i32, - has_exp_date: contract.exp_date.is_some(), - exp_date: contract.exp_date.unwrap_or(0), + has_expiration: contract.expiration.is_some(), + expiration: contract.expiration.unwrap_or(0), has_is_call: contract.is_call.is_some(), is_call: contract.is_call.unwrap_or(false), has_strike: contract.strike.is_some(), @@ -60,7 +60,7 @@ pub(crate) fn fpss_event_to_ffi(event: &thetadatadx::fpss::FpssEvent) -> FfiBuff control: ZERO_CONTROL, raw_data: ZERO_RAW, }, - _detail_string: contract_root_cstring, + _detail_string: contract_symbol_cstring, _raw_payload: None, } } @@ -74,19 +74,19 @@ pub(crate) fn fpss_event_to_ffi(event: &thetadatadx::fpss::FpssEvent) -> FfiBuff received_at_ns, .. }) => { - let contract_root_cstring = if contract.root.is_empty() { + let contract_symbol_cstring: Option = if contract.symbol.is_empty() { None } else { - std::ffi::CString::new(contract.root.as_str()).ok() + std::ffi::CString::new(contract.symbol.as_str()).ok() }; - let contract_root_ptr = contract_root_cstring + let contract_symbol_ptr = contract_symbol_cstring .as_ref() .map_or(ptr::null(), |cs| cs.as_ptr()); let tdx_contract = TdxContract { - root: contract_root_ptr, + symbol: contract_symbol_ptr, sec_type: contract.sec_type as i32, - has_exp_date: contract.exp_date.is_some(), - exp_date: contract.exp_date.unwrap_or(0), + has_expiration: contract.expiration.is_some(), + expiration: contract.expiration.unwrap_or(0), has_is_call: contract.is_call.is_some(), is_call: contract.is_call.unwrap_or(false), has_strike: contract.strike.is_some(), @@ -109,7 +109,7 @@ pub(crate) fn fpss_event_to_ffi(event: &thetadatadx::fpss::FpssEvent) -> FfiBuff control: ZERO_CONTROL, raw_data: ZERO_RAW, }, - _detail_string: contract_root_cstring, + _detail_string: contract_symbol_cstring, _raw_payload: None, } } @@ -130,19 +130,19 @@ pub(crate) fn fpss_event_to_ffi(event: &thetadatadx::fpss::FpssEvent) -> FfiBuff received_at_ns, .. }) => { - let contract_root_cstring = if contract.root.is_empty() { + let contract_symbol_cstring: Option = if contract.symbol.is_empty() { None } else { - std::ffi::CString::new(contract.root.as_str()).ok() + std::ffi::CString::new(contract.symbol.as_str()).ok() }; - let contract_root_ptr = contract_root_cstring + let contract_symbol_ptr = contract_symbol_cstring .as_ref() .map_or(ptr::null(), |cs| cs.as_ptr()); let tdx_contract = TdxContract { - root: contract_root_ptr, + symbol: contract_symbol_ptr, sec_type: contract.sec_type as i32, - has_exp_date: contract.exp_date.is_some(), - exp_date: contract.exp_date.unwrap_or(0), + has_expiration: contract.expiration.is_some(), + expiration: contract.expiration.unwrap_or(0), has_is_call: contract.is_call.is_some(), is_call: contract.is_call.unwrap_or(false), has_strike: contract.strike.is_some(), @@ -172,7 +172,7 @@ pub(crate) fn fpss_event_to_ffi(event: &thetadatadx::fpss::FpssEvent) -> FfiBuff control: ZERO_CONTROL, raw_data: ZERO_RAW, }, - _detail_string: contract_root_cstring, + _detail_string: contract_symbol_cstring, _raw_payload: None, } } @@ -198,19 +198,19 @@ pub(crate) fn fpss_event_to_ffi(event: &thetadatadx::fpss::FpssEvent) -> FfiBuff received_at_ns, .. }) => { - let contract_root_cstring = if contract.root.is_empty() { + let contract_symbol_cstring: Option = if contract.symbol.is_empty() { None } else { - std::ffi::CString::new(contract.root.as_str()).ok() + std::ffi::CString::new(contract.symbol.as_str()).ok() }; - let contract_root_ptr = contract_root_cstring + let contract_symbol_ptr = contract_symbol_cstring .as_ref() .map_or(ptr::null(), |cs| cs.as_ptr()); let tdx_contract = TdxContract { - root: contract_root_ptr, + symbol: contract_symbol_ptr, sec_type: contract.sec_type as i32, - has_exp_date: contract.exp_date.is_some(), - exp_date: contract.exp_date.unwrap_or(0), + has_expiration: contract.expiration.is_some(), + expiration: contract.expiration.unwrap_or(0), has_is_call: contract.is_call.is_some(), is_call: contract.is_call.unwrap_or(false), has_strike: contract.strike.is_some(), @@ -245,7 +245,7 @@ pub(crate) fn fpss_event_to_ffi(event: &thetadatadx::fpss::FpssEvent) -> FfiBuff control: ZERO_CONTROL, raw_data: ZERO_RAW, }, - _detail_string: contract_root_cstring, + _detail_string: contract_symbol_cstring, _raw_payload: None, } } diff --git a/ffi/src/fpss_event_structs.rs b/ffi/src/fpss_event_structs.rs index f2be025a..02129f0d 100644 --- a/ffi/src/fpss_event_structs.rs +++ b/ffi/src/fpss_event_structs.rs @@ -18,20 +18,20 @@ pub enum TdxFpssEventKind { /// FPSS `Contract` shared across every data event. /// -/// `root` is a NUL-terminated C string; may be null when the SDK has not +/// `symbol` is a NUL-terminated C string; may be null when the SDK has not /// yet resolved the server-assigned contract_id to a `ContractAssigned` -/// frame. Optional option fields (`exp_date`, `is_call`, `strike`) use a +/// frame. Optional option fields (`expiration`, `is_call`, `strike`) use a /// tagged-present bool because `#[repr(C)]` cannot express `Option` /// directly. #[repr(C)] pub struct TdxContract { -/// Ticker root (e.g. "AAPL"). Null until ContractAssigned arrives. -pub root: *const c_char, +/// Ticker symbol (e.g. "AAPL"). Null until ContractAssigned arrives. +pub symbol: *const c_char, /// Security type code — matches `tdbe::types::enums::SecType`. pub sec_type: i32, -/// Whether `exp_date` is meaningful (options only). -pub has_exp_date: bool, -pub exp_date: i32, +/// Whether `expiration` is meaningful (options only). +pub has_expiration: bool, +pub expiration: i32, /// Whether `is_call` is meaningful (options only). pub has_is_call: bool, pub is_call: bool, @@ -41,10 +41,10 @@ pub strike: i32, } pub(crate) const ZERO_CONTRACT_STRUCT: TdxContract = TdxContract { -root: ptr::null(), +symbol: ptr::null(), sec_type: 0, -has_exp_date: false, -exp_date: 0, +has_expiration: false, +expiration: 0, has_is_call: false, is_call: false, has_strike: false, diff --git a/ffi/src/streaming.rs b/ffi/src/streaming.rs index 738dab78..1a2cc1b6 100644 --- a/ffi/src/streaming.rs +++ b/ffi/src/streaming.rs @@ -1043,7 +1043,7 @@ pub unsafe extern "C" fn tdx_unified_reconnect(handle: *const TdxUnified) -> i32 target: "thetadatadx::ffi::reconnect", error = %e, kind = ?kind, - root = %contract.root, + symbol = %contract.symbol, "resubscribe failed after reconnect" ); } @@ -2354,7 +2354,7 @@ pub unsafe extern "C" fn tdx_fpss_reconnect(handle: *const TdxFpssHandle) -> i32 target: "thetadatadx::ffi::reconnect", error = %e, kind = ?kind, - root = %contract.root, + symbol = %contract.symbol, "resubscribe failed after reconnect" ); } diff --git a/ffi/src/types.rs b/ffi/src/types.rs index 7c3103fe..f35266c3 100644 --- a/ffi/src/types.rs +++ b/ffi/src/types.rs @@ -195,11 +195,13 @@ tick_array_free!(tdx_trade_quote_tick_array_free, TdxTradeQuoteTickArray); /// FFI-safe option contract descriptor. /// -/// The `root` field is a heap-allocated C string. Freed when the array is freed. +/// The `symbol` field is a heap-allocated C string. Freed when the array is freed. +/// Renamed from `root` to match the v3 vendor surface; see +/// . #[repr(C)] pub struct TdxOptionContract { /// Heap-allocated NUL-terminated C string. Freed with the array. - pub root: *const c_char, + pub symbol: *const c_char, pub expiration: i32, pub strike: f64, pub right: i32, @@ -227,7 +229,7 @@ impl TdxOptionContractArray { .into_iter() .map(|c| { Ok(TdxOptionContract { - root: CString::new(c.root)?.into_raw().cast_const(), + symbol: CString::new(c.symbol)?.into_raw().cast_const(), expiration: c.expiration, strike: c.strike, right: c.right, @@ -240,16 +242,16 @@ impl TdxOptionContractArray { } } -/// Free an option contract array, including all heap-allocated root strings. +/// Free an option contract array, including all heap-allocated symbol strings. #[no_mangle] pub unsafe extern "C" fn tdx_option_contract_array_free(arr: TdxOptionContractArray) { ffi_boundary!((), { if !arr.data.is_null() && arr.len > 0 { - // First free each root C string + // First free each symbol C string let slice = unsafe { std::slice::from_raw_parts(arr.data, arr.len) }; for contract in slice { - if !contract.root.is_null() { - drop(unsafe { CString::from_raw(contract.root.cast_mut()) }); + if !contract.symbol.is_null() { + drop(unsafe { CString::from_raw(contract.symbol.cast_mut()) }); } } // Then free the array itself diff --git a/sdks/cpp/CMakeLists.txt b/sdks/cpp/CMakeLists.txt index 2944ddff..f65bd97e 100644 --- a/sdks/cpp/CMakeLists.txt +++ b/sdks/cpp/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.16) # (`Cargo.toml`, `crates/*/Cargo.toml`, `tools/*/Cargo.toml`) and the # TypeScript `package.json` files. Keep all of them in lockstep when # cutting an 8.0.x release — see `CHANGELOG.md` for the canonical version. -project(thetadatadx-cpp VERSION 8.0.23 LANGUAGES CXX) +project(thetadatadx-cpp VERSION 8.0.25 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/sdks/cpp/include/fpss_event_structs.h.inc b/sdks/cpp/include/fpss_event_structs.h.inc index 4ede6fc0..7986d6fd 100644 --- a/sdks/cpp/include/fpss_event_structs.h.inc +++ b/sdks/cpp/include/fpss_event_structs.h.inc @@ -15,16 +15,16 @@ typedef enum { TDX_FPSS_RAW_DATA = 5, } TdxFpssEventKind; -/* FPSS Contract shared across every data event. `root` is a +/* FPSS Contract shared across every data event. `symbol` is a * NUL-terminated C string (may be null when not yet resolved); * optional option fields use a tagged-present bool because C has no * Option. Layout is byte-identical to Rust's #[repr(C)] TdxContract. */ typedef struct { -const char *root; +const char *symbol; int32_t sec_type; -bool has_exp_date; -int32_t exp_date; +bool has_expiration; +int32_t expiration; bool has_is_call; bool is_call; bool has_strike; diff --git a/sdks/cpp/include/thetadx.h b/sdks/cpp/include/thetadx.h index 28c32a11..0af231d3 100644 --- a/sdks/cpp/include/thetadx.h +++ b/sdks/cpp/include/thetadx.h @@ -285,10 +285,10 @@ typedef struct { const TdxCalendarDay* data; size_t len; } TdxCalendarDayArray; typedef struct { const TdxInterestRateTick* data; size_t len; } TdxInterestRateTickArray; typedef struct { const TdxTradeQuoteTick* data; size_t len; } TdxTradeQuoteTickArray; -/* ── OptionContract (has heap-allocated root string) ── */ +/* ── OptionContract (has heap-allocated symbol string) ── */ typedef struct { - const char* root; /* heap-allocated, freed with tdx_option_contract_array_free */ + const char* symbol; /* heap-allocated, freed with tdx_option_contract_array_free */ int32_t expiration; /* 4 bytes padding before f64 */ double strike; diff --git a/sdks/cpp/include/thetadx.hpp b/sdks/cpp/include/thetadx.hpp index 7dc6fefe..6a57d755 100644 --- a/sdks/cpp/include/thetadx.hpp +++ b/sdks/cpp/include/thetadx.hpp @@ -61,10 +61,10 @@ using TradeQuoteTick = TdxTradeQuoteTick; // Every data variant gained an embedded `TdxContract contract` field // immediately after `contract_id`. On LP64 (x86_64 / aarch64 Linux, // macOS), `TdxContract` is 32 bytes { -// const char *root offset 0, size 8 +// const char *symbol offset 0, size 8 // int32_t sec_type offset 8, size 4 -// bool has_exp_date offset 12, size 1 -// int32_t exp_date offset 16, size 4 (3 bytes pad after has_exp_date) +// bool has_expiration offset 12, size 1 +// int32_t expiration offset 16, size 4 (3 bytes pad after has_expiration) // bool has_is_call offset 20, size 1 // bool is_call offset 21, size 1 // bool has_strike offset 22, size 1 @@ -78,11 +78,12 @@ using TradeQuoteTick = TdxTradeQuoteTick; // Generated layout guards for the FPSS event C mirror structs. #include "fpss_layout_asserts.hpp.inc" -// OptionContract uses std::string for root to avoid use-after-free. +// OptionContract uses std::string for symbol to avoid use-after-free. // The C FFI TdxOptionContract uses a raw char* that is freed with the array, -// so we deep-copy the string during conversion. +// so we deep-copy the string during conversion. Field name follows the v3 +// vendor surface; see https://docs.thetadata.us/Articles/Getting-Started/v2-migration-guide.html#_5-parameter-mapping. struct OptionContract { - std::string root; + std::string symbol; int32_t expiration; double strike; int32_t right; diff --git a/sdks/cpp/src/historical.cpp.inc b/sdks/cpp/src/historical.cpp.inc index d4ec12dc..f9c7f134 100644 --- a/sdks/cpp/src/historical.cpp.inc +++ b/sdks/cpp/src/historical.cpp.inc @@ -247,7 +247,7 @@ std::vector Client::option_list_contracts(const std::string& req result.reserve(arr.len); for (size_t i = 0; i < arr.len; ++i) { OptionContract c; - c.root = arr.data[i].root ? std::string(arr.data[i].root) : ""; + c.symbol = arr.data[i].symbol ? std::string(arr.data[i].symbol) : ""; c.expiration = arr.data[i].expiration; c.strike = arr.data[i].strike; c.right = arr.data[i].right; diff --git a/sdks/go/fpss_event_structs.go b/sdks/go/fpss_event_structs.go index 91948c73..07efd6c0 100644 --- a/sdks/go/fpss_event_structs.go +++ b/sdks/go/fpss_event_structs.go @@ -40,16 +40,16 @@ const ( // Value 7 is reserved for future use. ) -// Contract identifies a subscribed instrument. Root is always present; -// option fields (ExpDate, IsCall, Strike) are non-nil only for options. +// Contract identifies a subscribed instrument. Symbol is always present; +// option fields (Expiration, IsCall, Strike) are non-nil only for options. // The same Contract value is attached to every FPSS data event the SDK // emits for the matching contract_id. type Contract struct { - Root string - SecType int32 - ExpDate *int32 - IsCall *bool - Strike *int32 + Symbol string + SecType int32 + Expiration *int32 + IsCall *bool + Strike *int32 } // FPSS OHLCVC bar. Mirrors `FpssData::Ohlcvc`. diff --git a/sdks/go/fpss_event_structs.h.inc b/sdks/go/fpss_event_structs.h.inc index 4ede6fc0..7986d6fd 100644 --- a/sdks/go/fpss_event_structs.h.inc +++ b/sdks/go/fpss_event_structs.h.inc @@ -15,16 +15,16 @@ typedef enum { TDX_FPSS_RAW_DATA = 5, } TdxFpssEventKind; -/* FPSS Contract shared across every data event. `root` is a +/* FPSS Contract shared across every data event. `symbol` is a * NUL-terminated C string (may be null when not yet resolved); * optional option fields use a tagged-present bool because C has no * Option. Layout is byte-identical to Rust's #[repr(C)] TdxContract. */ typedef struct { -const char *root; +const char *symbol; int32_t sec_type; -bool has_exp_date; -int32_t exp_date; +bool has_expiration; +int32_t expiration; bool has_is_call; bool is_call; bool has_strike; diff --git a/sdks/go/fpss_methods.go b/sdks/go/fpss_methods.go index 28d98671..c3948189 100644 --- a/sdks/go/fpss_methods.go +++ b/sdks/go/fpss_methods.go @@ -272,19 +272,19 @@ func (f *FpssClient) NextEvent(timeoutMs uint64) (*FpssEvent, error) { } // contractFromC converts a C TdxContract into the Go-idiomatic *Contract. - // Returns nil only when root is NULL, which indicates the SDK has not yet + // Returns nil only when symbol is NULL, which indicates the SDK has not yet // resolved the contract_id to a ContractAssigned frame. contractFromC := func(c C.TdxContract) *Contract { - if c.root == nil { + if c.symbol == nil { return nil } out := &Contract{ - Root: C.GoString(c.root), + Symbol: C.GoString(c.symbol), SecType: int32(c.sec_type), } - if bool(c.has_exp_date) { - v := int32(c.exp_date) - out.ExpDate = &v + if bool(c.has_expiration) { + v := int32(c.expiration) + out.Expiration = &v } if bool(c.has_is_call) { v := bool(c.is_call) diff --git a/sdks/go/tick_converters.go b/sdks/go/tick_converters.go index 22ca635c..0b798cd5 100644 --- a/sdks/go/tick_converters.go +++ b/sdks/go/tick_converters.go @@ -216,12 +216,12 @@ func convertOptionContracts(arr C.TdxOptionContractArray) []OptionContract { src := unsafe.Slice((*cOptionContract)(arr.data), n) result := make([]OptionContract, n) for i, t := range src { - root := "" - if t.Root != 0 { - root = C.GoString((*C.char)(unsafe.Pointer(t.Root))) + symbol := "" + if t.Symbol != 0 { + symbol = C.GoString((*C.char)(unsafe.Pointer(t.Symbol))) } result[i] = OptionContract{ - Root: root, + Symbol: symbol, Expiration: int(t.Expiration), Strike: t.Strike, Right: RightStr(t.Right), diff --git a/sdks/go/tick_ffi_mirrors.go b/sdks/go/tick_ffi_mirrors.go index 59156d8b..96bf0f84 100644 --- a/sdks/go/tick_ffi_mirrors.go +++ b/sdks/go/tick_ffi_mirrors.go @@ -270,9 +270,9 @@ type cTradeQuoteTick struct { } // cOptionContract mirrors TdxOptionContract from FFI -// Layout: root(8 ptr), exp(4), pad(4), strike(8), right(4), pad(4) = 32 +// Layout: symbol(8 ptr), expiration(4), pad(4), strike(8), right(4), pad(4) = 32 type cOptionContract struct { - Root uintptr // *const c_char + Symbol uintptr // *const c_char Expiration int32 _pad1 int32 Strike float64 diff --git a/sdks/go/tick_structs.go b/sdks/go/tick_structs.go index 26aa36ef..44799def 100644 --- a/sdks/go/tick_structs.go +++ b/sdks/go/tick_structs.go @@ -126,7 +126,7 @@ type OpenInterestTick struct { // OptionContract — Option contract -- 4 fields. Contract specification. type OptionContract struct { - Root string `json:"root"` + Symbol string `json:"symbol"` Expiration int `json:"expiration"` Strike float64 `json:"strike"` Right string `json:"right"` diff --git a/sdks/python/Cargo.lock b/sdks/python/Cargo.lock index 9948d0b0..251c283f 100644 --- a/sdks/python/Cargo.lock +++ b/sdks/python/Cargo.lock @@ -2310,7 +2310,7 @@ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "tdbe" -version = "0.12.5" +version = "0.12.6" dependencies = [ "thiserror 2.0.18", ] @@ -2330,7 +2330,7 @@ dependencies = [ [[package]] name = "thetadatadx" -version = "8.0.24" +version = "8.0.25" dependencies = [ "disruptor", "metrics", @@ -2364,7 +2364,7 @@ dependencies = [ [[package]] name = "thetadatadx-py" -version = "8.0.24" +version = "8.0.25" dependencies = [ "arrow", "arrow-array", diff --git a/sdks/python/Cargo.toml b/sdks/python/Cargo.toml index ace2e73f..3db9503b 100644 --- a/sdks/python/Cargo.toml +++ b/sdks/python/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thetadatadx-py" -version = "8.0.24" +version = "8.0.25" edition = "2021" description = "Python bindings for thetadatadx — native ThetaData SDK powered by Rust" license = "Apache-2.0" @@ -19,7 +19,7 @@ doc = false [dependencies] # The Rust SDK we're wrapping thetadatadx = { path = "../../crates/thetadatadx" } -tdbe = { version = "0.12.5", path = "../../crates/tdbe" } +tdbe = { version = "0.12.6", path = "../../crates/tdbe" } # Direct prost dep for decoding `thetadatadx::proto::ResponseData` bytes in # the `decode_response_bytes` hook. The main crate no longer re-exports diff --git a/sdks/python/src/fpss_event_classes.rs b/sdks/python/src/fpss_event_classes.rs index 6f4e39c1..8b88e841 100644 --- a/sdks/python/src/fpss_event_classes.rs +++ b/sdks/python/src/fpss_event_classes.rs @@ -10,9 +10,9 @@ #[pyclass(module = "thetadatadx", frozen, skip_from_py_object)] #[derive(Clone)] pub(crate) struct Contract { -#[pyo3(get)] pub root: String, +#[pyo3(get)] pub symbol: String, #[pyo3(get)] pub sec_type: i32, -#[pyo3(get)] pub exp_date: Option, +#[pyo3(get)] pub expiration: Option, #[pyo3(get)] pub is_call: Option, #[pyo3(get)] pub strike: Option, } @@ -20,8 +20,8 @@ pub(crate) struct Contract { impl Contract { fn __repr__(&self) -> String { format!( -"Contract(root={:?}, sec_type={}, exp_date={:?}, is_call={:?}, strike={:?})", -self.root, self.sec_type, self.exp_date, self.is_call, self.strike +"Contract(symbol={:?}, sec_type={}, expiration={:?}, is_call={:?}, strike={:?})", +self.symbol, self.sec_type, self.expiration, self.is_call, self.strike ) } } @@ -33,9 +33,9 @@ impl Contract { /// exports if they need a symbolic reading. pub(crate) fn from_core(c: &fpss::protocol::Contract) -> Self { Self { -root: c.root.clone(), +symbol: c.symbol.clone(), sec_type: c.sec_type as i32, -exp_date: c.exp_date, +expiration: c.expiration, is_call: c.is_call, strike: c.strike, } diff --git a/sdks/python/src/tick_arrow.rs b/sdks/python/src/tick_arrow.rs index fc6eea0e..754229de 100644 --- a/sdks/python/src/tick_arrow.rs +++ b/sdks/python/src/tick_arrow.rs @@ -120,7 +120,7 @@ pub(crate) fn arrow_schema_for_qualname(qualname: &str) -> Option> { Field::new("right", DataType::Utf8, false), ]))), "OptionContract" => Some(Arc::new(Schema::new(vec![ - Field::new("root", DataType::Utf8, false), + Field::new("symbol", DataType::Utf8, false), Field::new("expiration", DataType::Int32, false), Field::new("strike", DataType::Float64, false), Field::new("right", DataType::Utf8, false), @@ -634,18 +634,18 @@ pub(crate) mod slice_arrow { fn read_arrow_batch_from_option_contract_slice(ticks: &[tick::OptionContract]) -> PyResult { let schema = arrow_schema_for_qualname("OptionContract").expect("generated schema must be present for OptionContract"); let n = ticks.len(); - let mut col_root: Vec = Vec::with_capacity(n); + let mut col_symbol: Vec = Vec::with_capacity(n); 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); for t in ticks { - col_root.push(t.root.clone()); + col_symbol.push(t.symbol.clone()); col_expiration.push(t.expiration); col_strike.push(t.strike); col_right.push(if t.is_call() { "C".to_string() } else if t.is_put() { "P".to_string() } else { String::new() }); } let columns: Vec = vec![ - Arc::new(StringArray::from(col_root)) as ArrayRef, + Arc::new(StringArray::from(col_symbol)) as ArrayRef, Arc::new(Int32Array::from(col_expiration)) as ArrayRef, Arc::new(Float64Array::from(col_strike)) as ArrayRef, Arc::new(StringArray::from(col_right)) as ArrayRef, diff --git a/sdks/python/src/tick_classes.rs b/sdks/python/src/tick_classes.rs index 68d491a4..0737b90c 100644 --- a/sdks/python/src/tick_classes.rs +++ b/sdks/python/src/tick_classes.rs @@ -341,12 +341,12 @@ impl OpenInterestTick { /// Option contract. Contract specification. /// -/// Cannot be `Copy` because of the `String` root field. +/// Cannot be `Copy` because of the `String` symbol field. The vendor v3 surface renamed the underlying ticker field from `root` to `symbol`; see https://docs.thetadata.us/Articles/Getting-Started/v2-migration-guide.html#_5-parameter-mapping. #[must_use] #[pyclass(module = "thetadatadx", frozen, skip_from_py_object)] #[derive(Clone)] pub(crate) struct OptionContract { - #[pyo3(get)] pub root: String, + #[pyo3(get)] pub symbol: String, #[pyo3(get)] pub expiration: i32, #[pyo3(get)] pub strike: f64, #[pyo3(get)] pub right: String, @@ -354,17 +354,17 @@ pub(crate) struct OptionContract { #[pymethods] impl OptionContract { #[new] - #[pyo3(signature = (*, root = String::new(), expiration = 0i32, strike = 0.0f64, right = String::new()))] - fn new(root: String, expiration: i32, strike: f64, right: String) -> Self { + #[pyo3(signature = (*, symbol = String::new(), expiration = 0i32, strike = 0.0f64, right = String::new()))] + fn new(symbol: String, expiration: i32, strike: f64, right: String) -> Self { Self { - root, + symbol, expiration, strike, right, } } fn __repr__(&self) -> String { - format!("OptionContract(root={:?}, expiration={}, strike={}, right={:?})", self.root, self.expiration, self.strike, self.right) + format!("OptionContract(symbol={:?}, expiration={}, strike={}, right={:?})", self.symbol, self.expiration, self.strike, self.right) } } @@ -1949,7 +1949,7 @@ impl OptionContractList { let mut inner = Vec::with_capacity(ticks.len()); for t in &ticks { inner.push( tick::OptionContract { - root: t.root.clone(), + symbol: t.symbol.clone(), expiration: t.expiration, strike: t.strike, right: match t.right.as_str() { "C" => 67, "P" => 80, _ => 0 }, @@ -1981,7 +1981,7 @@ impl OptionContractList { } let t = &self.inner[resolved as usize]; Ok( OptionContract { - root: t.root.clone(), + symbol: t.symbol.clone(), expiration: t.expiration, strike: t.strike, right: (if t.is_call() { "C" } else if t.is_put() { "P" } else { "" }).to_string(), @@ -2000,7 +2000,7 @@ impl OptionContractList { let list = pyo3::types::PyList::empty(py); for t in &self.inner { let obj = OptionContract { - root: t.root.clone(), + symbol: t.symbol.clone(), expiration: t.expiration, strike: t.strike, right: (if t.is_call() { "C" } else if t.is_put() { "P" } else { "" }).to_string(), @@ -2057,7 +2057,7 @@ impl OptionContractListIter { let t = &self.inner[self.cursor]; self.cursor += 1; Some( OptionContract { - root: t.root.clone(), + symbol: t.symbol.clone(), expiration: t.expiration, strike: t.strike, right: (if t.is_call() { "C" } else if t.is_put() { "P" } else { "" }).to_string(), diff --git a/sdks/typescript/Cargo.lock b/sdks/typescript/Cargo.lock index 6e5c8790..c56d7d14 100644 --- a/sdks/typescript/Cargo.lock +++ b/sdks/typescript/Cargo.lock @@ -1934,7 +1934,7 @@ dependencies = [ [[package]] name = "tdbe" -version = "0.12.5" +version = "0.12.6" dependencies = [ "thiserror 2.0.18", ] @@ -1954,7 +1954,7 @@ dependencies = [ [[package]] name = "thetadatadx" -version = "8.0.24" +version = "8.0.25" dependencies = [ "disruptor", "metrics", @@ -1988,7 +1988,7 @@ dependencies = [ [[package]] name = "thetadatadx-napi" -version = "8.0.24" +version = "8.0.25" dependencies = [ "chrono", "napi", diff --git a/sdks/typescript/Cargo.toml b/sdks/typescript/Cargo.toml index 6fe42a8d..b8429555 100644 --- a/sdks/typescript/Cargo.toml +++ b/sdks/typescript/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thetadatadx-napi" -version = "8.0.24" +version = "8.0.25" edition = "2021" description = "TypeScript/Node.js bindings for thetadatadx — native ThetaData SDK powered by Rust" license = "Apache-2.0" @@ -13,7 +13,7 @@ crate-type = ["cdylib"] [dependencies] thetadatadx = { path = "../../crates/thetadatadx" } -tdbe = { version = "0.12.5", path = "../../crates/tdbe" } +tdbe = { version = "0.12.6", path = "../../crates/tdbe" } napi = { version = "3.8.5", features = ["async", "tokio_rt", "serde-json", "napi6", "chrono_date"] } napi-derive = "3.5.4" diff --git a/sdks/typescript/index.d.ts b/sdks/typescript/index.d.ts index 3628abcd..ab1e2b81 100644 --- a/sdks/typescript/index.d.ts +++ b/sdks/typescript/index.d.ts @@ -826,9 +826,9 @@ export interface CalendarDay { * event as `event.quote.contract` / `event.trade.contract` / etc. */ export interface Contract { - root: string + symbol: string secType: number - expDate?: number + expiration?: number isCall?: boolean strike?: number } @@ -1032,7 +1032,7 @@ export interface OpenInterestTick { /** Option contract. Contract specification. */ export interface OptionContract { - root: string + symbol: string expiration: number strike: number right: string diff --git a/sdks/typescript/npm/darwin-arm64/package.json b/sdks/typescript/npm/darwin-arm64/package.json index be9b8ecf..27a61e2c 100644 --- a/sdks/typescript/npm/darwin-arm64/package.json +++ b/sdks/typescript/npm/darwin-arm64/package.json @@ -1,6 +1,6 @@ { "name": "thetadatadx-darwin-arm64", - "version": "8.0.24", + "version": "8.0.25", "os": [ "darwin" ], diff --git a/sdks/typescript/npm/linux-x64-gnu/package.json b/sdks/typescript/npm/linux-x64-gnu/package.json index 2c64d1fd..9389d86c 100644 --- a/sdks/typescript/npm/linux-x64-gnu/package.json +++ b/sdks/typescript/npm/linux-x64-gnu/package.json @@ -1,6 +1,6 @@ { "name": "thetadatadx-linux-x64-gnu", - "version": "8.0.24", + "version": "8.0.25", "os": [ "linux" ], diff --git a/sdks/typescript/npm/win32-x64-msvc/package.json b/sdks/typescript/npm/win32-x64-msvc/package.json index 85225ac4..b8a5593a 100644 --- a/sdks/typescript/npm/win32-x64-msvc/package.json +++ b/sdks/typescript/npm/win32-x64-msvc/package.json @@ -1,6 +1,6 @@ { "name": "thetadatadx-win32-x64-msvc", - "version": "8.0.24", + "version": "8.0.25", "os": [ "win32" ], diff --git a/sdks/typescript/package-lock.json b/sdks/typescript/package-lock.json index ccb19664..c153a586 100644 --- a/sdks/typescript/package-lock.json +++ b/sdks/typescript/package-lock.json @@ -1,12 +1,12 @@ { "name": "thetadatadx", - "version": "8.0.24", + "version": "8.0.25", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "thetadatadx", - "version": "8.0.24", + "version": "8.0.25", "license": "Apache-2.0", "devDependencies": { "@napi-rs/cli": "^3.6.2" @@ -15,9 +15,9 @@ "node": ">= 20" }, "optionalDependencies": { - "thetadatadx-darwin-arm64": "8.0.24", - "thetadatadx-linux-x64-gnu": "8.0.24", - "thetadatadx-win32-x64-msvc": "8.0.24" + "thetadatadx-darwin-arm64": "8.0.25", + "thetadatadx-linux-x64-gnu": "8.0.25", + "thetadatadx-win32-x64-msvc": "8.0.25" } }, "node_modules/@emnapi/core": { diff --git a/sdks/typescript/package.json b/sdks/typescript/package.json index 28cbd0b0..8adeb7ad 100644 --- a/sdks/typescript/package.json +++ b/sdks/typescript/package.json @@ -1,6 +1,6 @@ { "name": "thetadatadx", - "version": "8.0.24", + "version": "8.0.25", "description": "Native ThetaData SDK for Node.js — powered by Rust via napi-rs", "license": "Apache-2.0", "repository": { @@ -30,9 +30,9 @@ "@napi-rs/cli": "^3.6.2" }, "optionalDependencies": { - "thetadatadx-linux-x64-gnu": "8.0.24", - "thetadatadx-darwin-arm64": "8.0.24", - "thetadatadx-win32-x64-msvc": "8.0.24" + "thetadatadx-linux-x64-gnu": "8.0.25", + "thetadatadx-darwin-arm64": "8.0.25", + "thetadatadx-win32-x64-msvc": "8.0.25" }, "engines": { "node": ">= 20" diff --git a/sdks/typescript/src/fpss_event_classes.rs b/sdks/typescript/src/fpss_event_classes.rs index e201f39a..01b80ac8 100644 --- a/sdks/typescript/src/fpss_event_classes.rs +++ b/sdks/typescript/src/fpss_event_classes.rs @@ -12,13 +12,16 @@ /// FPSS contract identifier. Surfaced on every decoded FPSS data /// event as `event.quote.contract` / `event.trade.contract` / etc. +/// +/// Field names follow the v3 vendor surface (`symbol`, `expiration`) +/// per . #[must_use] #[napi(object)] #[derive(Clone)] pub struct Contract { - pub root: String, + pub symbol: String, pub sec_type: i32, - pub exp_date: Option, + pub expiration: Option, pub is_call: Option, pub strike: Option, } @@ -173,9 +176,9 @@ pub(crate) fn buffered_event_to_typed(event: BufferedEvent) -> FpssEvent { out.ohlcvc = Some(Ohlcvc { contract_id, contract: Contract { - root: contract.root.clone(), + symbol: contract.symbol.clone(), sec_type: contract.sec_type as i32, - exp_date: contract.exp_date, + expiration: contract.expiration, is_call: contract.is_call, strike: contract.strike, }, @@ -202,9 +205,9 @@ pub(crate) fn buffered_event_to_typed(event: BufferedEvent) -> FpssEvent { out.open_interest = Some(OpenInterest { contract_id, contract: Contract { - root: contract.root.clone(), + symbol: contract.symbol.clone(), sec_type: contract.sec_type as i32, - exp_date: contract.exp_date, + expiration: contract.expiration, is_call: contract.is_call, strike: contract.strike, }, @@ -233,9 +236,9 @@ pub(crate) fn buffered_event_to_typed(event: BufferedEvent) -> FpssEvent { out.quote = Some(Quote { contract_id, contract: Contract { - root: contract.root.clone(), + symbol: contract.symbol.clone(), sec_type: contract.sec_type as i32, - exp_date: contract.exp_date, + expiration: contract.expiration, is_call: contract.is_call, strike: contract.strike, }, @@ -276,9 +279,9 @@ pub(crate) fn buffered_event_to_typed(event: BufferedEvent) -> FpssEvent { out.trade = Some(Trade { contract_id, contract: Contract { - root: contract.root.clone(), + symbol: contract.symbol.clone(), sec_type: contract.sec_type as i32, - exp_date: contract.exp_date, + expiration: contract.expiration, is_call: contract.is_call, strike: contract.strike, }, diff --git a/sdks/typescript/src/tick_classes.rs b/sdks/typescript/src/tick_classes.rs index 279078a5..04e4ae56 100644 --- a/sdks/typescript/src/tick_classes.rs +++ b/sdks/typescript/src/tick_classes.rs @@ -152,7 +152,7 @@ pub struct OpenInterestTick { #[napi(object)] #[derive(Clone)] pub struct OptionContract { - pub root: String, + pub symbol: String, pub expiration: i32, pub strike: f64, pub right: String, @@ -420,7 +420,7 @@ fn option_contracts_to_class_vec(ticks: &[tick::OptionContract]) -> Vec Vec sonic_rs::Value { let sec_type_str = format!("{:?}", c.sec_type).to_uppercase(); let mut obj = sonic_rs::Object::new(); - obj.insert("root", sonic_rs::Value::from(c.root.as_str())); + obj.insert("symbol", sonic_rs::Value::from(c.symbol.as_str())); obj.insert("sec_type", sonic_rs::Value::from(sec_type_str.as_str())); - if let Some(exp) = c.exp_date { + if let Some(exp) = c.expiration { obj.insert("expiration", sonic_rs::Value::from(exp)); } if let Some(strike) = c.strike { @@ -257,23 +257,23 @@ mod tests { // Peek under the lock — mirrors what the callback thread does. let peeked = lookup_event_contract(&event, &map); assert!(peeked.is_some(), "pre-peek must find the contract"); - assert_eq!(peeked.as_ref().unwrap().root, "AAPL"); + assert_eq!(peeked.as_ref().unwrap().symbol, "AAPL"); // Simulate a reconnect / market-close clearing the shared map // AFTER the callback has peeked but BEFORE the broadcast task // serializes. Under the pre-fix code, a subsequent re-lookup // would now return None and produce `{"id": 42}` with no - // root/strike/right. + // symbol/strike/right. map.lock().unwrap().clear(); // With the fix, the peeked snapshot still carries the full // contract via the Arc refcount — serialization must succeed - // with root = "AAPL". + // with symbol = "AAPL". let json = fpss_event_to_ws_json(&event, peeked.as_deref()) .expect("serialization must succeed with peeked contract"); assert!( - json.contains("\"root\":\"AAPL\""), - "serialized JSON must retain the peeked root after map clear: {json}" + json.contains("\"symbol\":\"AAPL\""), + "serialized JSON must retain the peeked symbol after map clear: {json}" ); }