Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/tdbe/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion crates/tdbe/src/types/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
6 changes: 5 additions & 1 deletion crates/tdbe/src/types/tick.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
/// <https://docs.thetadata.us/Articles/Getting-Started/v2-migration-guide.html#_5-parameter-mapping>.
#[must_use]
#[derive(Debug, Clone)]
pub struct OptionContract {
pub root: String,
pub symbol: String,
pub expiration: i32,
pub strike: f64,
pub right: i32,
Expand Down
6 changes: 3 additions & 3 deletions crates/thetadatadx/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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"] }
Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
11 changes: 7 additions & 4 deletions crates/thetadatadx/build_support/fpss_events/ffi_c.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://docs.thetadata.us/Articles/Getting-Started/v2-migration-guide.html#_5-parameter-mapping>.
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<T>. 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\
Expand Down
36 changes: 20 additions & 16 deletions crates/thetadatadx/build_support/fpss_events/ffi_rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>` for Rust->C interop.
///
/// Field names follow the v3 vendor surface (`symbol`, `expiration`)
/// per <https://docs.thetadata.us/Articles/Getting-Started/v2-migration-guide.html#_5-parameter-mapping>.
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<T>`\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\
Expand All @@ -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\
Expand Down Expand Up @@ -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<std::ffi::CString> = 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");
Expand Down Expand Up @@ -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");
}
Expand Down
20 changes: 11 additions & 9 deletions crates/thetadatadx/build_support/fpss_events/go_structs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
/// <https://docs.thetadata.us/Articles/Getting-Started/v2-migration-guide.html#_5-parameter-mapping>.
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"
}

Expand Down
18 changes: 10 additions & 8 deletions crates/thetadatadx/build_support/fpss_events/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Contract>` 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
/// <https://docs.thetadata.us/Articles/Getting-Started/v2-migration-guide.html#_5-parameter-mapping>.
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\
Expand All @@ -18,18 +20,18 @@ 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<i32>,\n\
#[pyo3(get)] pub expiration: Option<i32>,\n\
#[pyo3(get)] pub is_call: Option<bool>,\n\
#[pyo3(get)] pub strike: Option<i32>,\n\
}\n\
#[pymethods]\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\
Expand All @@ -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\
Expand Down
11 changes: 7 additions & 4 deletions crates/thetadatadx/build_support/fpss_events/typescript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://docs.thetadata.us/Articles/Getting-Started/v2-migration-guide.html#_5-parameter-mapping>.
#[must_use]
#[napi(object)]
#[derive(Clone)]
pub struct Contract {
pub root: String,
pub symbol: String,
pub sec_type: i32,
pub exp_date: Option<i32>,
pub expiration: Option<i32>,
pub is_call: Option<bool>,
pub strike: Option<i32>,
}
Expand Down Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading