Skip to content
Merged
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
64 changes: 64 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,70 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [8.0.28] - 2026-05-06

### Breaking

- **`Contract`, `OptionContract`, `FlatFileRow`, and `IndexEntry` rename
`root` to `symbol` and `exp_date` to `expiration` to match the v3
vendor surface documented in the [v2 → v3 migration guide][v3-mig].
The wire codec is unchanged — `Contract::to_bytes` /
`Contract::from_bytes` still serialize the field as `root` per
`Contract.java` parity, and the FLATFILES decoder still resolves both
v2 (`root`) and v3 (`symbol`) response columns through the existing
`decode::HEADER_ALIASES`. Per-language renames:

- **Rust** (`thetadatadx::fpss::protocol::Contract`,
`tdbe::types::tick::OptionContract`,
`thetadatadx::flatfiles::FlatFileRow`):
- `Contract.root` → `Contract.symbol`
- `Contract.exp_date` → `Contract.expiration`
- `Contract::stock(root)` → `Contract::stock(symbol)`
- `Contract::index(root)` → `Contract::index(symbol)`
- `Contract::rate(root)` → `Contract::rate(symbol)`
- `Contract::option(root, exp_date, …)` →
`Contract::option(symbol, expiration, …)`
- `Contract::option_raw(root, exp_date, …)` →
`Contract::option_raw(symbol, expiration, …)`
- `OptionContract.root` → `OptionContract.symbol`
- `FlatFileRow.root` → `FlatFileRow.symbol`
- **Python** (`thetadatadx.Contract`, `thetadatadx.OptionContract`):
`contract.root` / `contract.exp_date` →
`contract.symbol` / `contract.expiration`;
`OptionContract(root=…)` constructor keyword → `symbol=…`.
- **TypeScript** (`Contract`, `OptionContract`):
`contract.root` / `contract.expDate` →
`contract.symbol` / `contract.expiration`.
- **Go** (`thetadatadx.Contract`, `thetadatadx.OptionContract`):
`c.Root` / `c.ExpDate` → `c.Symbol` / `c.Expiration`.
- **C++** (`OptionContract`, `TdxContract`, `TdxOptionContract`):
`c.root` / `c.exp_date` / `c.has_exp_date` →
`c.symbol` / `c.expiration` / `c.has_expiration`.
- **C ABI**: `TdxContract.root` → `TdxContract.symbol`,
`TdxContract.exp_date` → `TdxContract.expiration`,
`TdxContract.has_exp_date` → `TdxContract.has_expiration`,
`TdxOptionContract.root` → `TdxOptionContract.symbol`.
- **FLATFILES CSV / JSONL**: contract-prefix headers and JSON keys
change from `root,expiration,strike,right,…` to
`symbol,expiration,strike,right,…`. Stock blobs go from `root,…` to
`symbol,…`. The vendor's response columns are unchanged; only the
SDK's emitted file headers change.
- **REST / WebSocket / MCP outputs** in `tools/server` and
`tools/mcp` emit `"symbol"` / `"expiration"` keys on every contract
payload (option lists, FPSS event contracts, FLATFILES rows).

[v3-mig]: https://docs.thetadata.us/Articles/Getting-Started/v2-migration-guide.html#_5-parameter-mapping

### Changed

- Workspace 8.0.27 → 8.0.28, tdbe 0.12.7 → 0.12.8. The tdbe bump rides
the regenerated `OptionContract.symbol` field in
`crates/tdbe/src/types/tick_generated.rs`; every other change ships
as patch deltas off the existing v8 line per repo policy.
- `tools/cli` raw column header for `OptionContract` is `symbol`
instead of `root`, sourced from `tick_schema.toml::field` so future
schema renames flow through the CLI without a helper edit.

## [8.0.27] - 2026-05-06

### Changed
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.7"
version = "0.12.8"
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
2 changes: 1 addition & 1 deletion crates/tdbe/src/types/tick.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
//! ...). These read `flags::*` constants and don't fit the schema's
//! field-only model.
//! * `impl OptionContract` for `is_call` / `is_put` -- a non-`Copy` struct
//! so the macro doesn't apply.
//! (because of the `String` `symbol` field) so the macro doesn't apply.
//!
//! The structs themselves are generated at build-time from
//! `crates/thetadatadx/tick_schema.toml` by
Expand Down
4 changes: 2 additions & 2 deletions crates/tdbe/src/types/tick_generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,12 +275,12 @@ pub struct OpenInterestTick {

/// Option contract -- 4 fields. Contract specification.
///
/// Cannot be `Copy` because of the `String` root field.
/// Cannot be `Copy` because of the `String` symbol field.
#[must_use]
#[derive(Debug, Clone)]
#[repr(C)]
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.27"
version = "8.0.28"
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.7", path = "../tdbe" }
tdbe = { version = "0.12.8", 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.7", path = "../tdbe" }
tdbe = { version = "0.12.8", 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
8 changes: 4 additions & 4 deletions crates/thetadatadx/build_support/fpss_events/ffi_c.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ use super::schema::{sorted_data_events, Schema};
/// `#[repr(C)] TdxContract`. Layout must match field-for-field with
/// `render_contract_struct_rust` in `ffi_rust.rs`.
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
33 changes: 17 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,7 +12,7 @@ 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
Expand All @@ -21,20 +21,20 @@ 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 +44,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 +178,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 = 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 +226,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
18 changes: 9 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,19 @@ 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).
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
14 changes: 7 additions & 7 deletions crates/thetadatadx/build_support/fpss_events/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ 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
/// `event.contract.symbol`, `event.contract.strike`, etc. through the
/// normal pyo3 getter machinery.
fn render_contract_pyclass() -> &'static str {
"/// FPSS contract identifier. Surfaced on every decoded FPSS data\n\
Expand All @@ -18,18 +18,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 +41,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
12 changes: 6 additions & 6 deletions crates/thetadatadx/build_support/fpss_events/typescript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use super::common::{snake_case, snake_to_camel, ts_rust_field_type};
use super::schema::{load_schema, sorted_data_event_names, sorted_event_names, EventDef, Schema};

/// Emit the Contract napi struct. Same shape across every language —
/// `root` is always present (empty when not yet resolved), option fields
/// `symbol` is always present (empty when not yet resolved), option fields
/// are `Option<T>` on the Rust side / `?: T` on the TS side.
///
/// Uses a raw string literal (`r#""#`) so the field indentation is
Expand All @@ -22,9 +22,9 @@ fn render_contract_napi() -> &'static str {
#[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 +164,11 @@ pub(super) fn render_ts_fpss_event_classes(schema: &Schema) -> String {
name = column.name
)
.unwrap(),
// Contract is constructed explicitly — `root` clones, the
// option fields transfer by value.
// 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
Loading
Loading