Skip to content

Migrating from v12 to v13

v13 is the typed-surface correction: every value a row or contract exposes now carries one type and one unit under one name, across Rust, Python, TypeScript, C, C++, the HTTP/WS server, and the columnar outputs. The public surface breaks once, here. This guide walks every change a downstream caller has to make.

TL;DR

SurfaceChangeMigration
strike (streaming + builders)Dollars everywhere; wire integer goneRead strike as dollars; delete / 1000 conversions and strike_dollars reads
right (tick rows)Logical character, never the ASCII integerCompare against 'C' / "C" instead of 67
EodTick.ms_of_day / .ms_of_day2Renamed created_ms_of_day / last_trade_ms_of_dayRename field reads; semantics now in the name
CalendarDay.is_open / .statusbool + vendor vocabularyReplace 0-3 integer checks with "open" / "early_close" / "full_close" / "weekend" (Rust: CalendarStatus)
Python Greeks lambdaSpelled lambda_Replace getattr(tick, "lambda") with tick.lambda_
Absent contract identityNone / undefined in Python / TSReplace == 0 / == "" absence checks with is None / == null
TypeScript optional paramsOne trailing options objectReplace positional undefined holes with { key: value }
FPSS parse-error eventClass renamed ParseError, kind parse_errorRename event handling; Error no longer names an SDK type
C TdxContract.strikedouble dollarsRecompile against the new header; delete / 1000.0

Strike is dollars — everywhere

This is the change most likely to bite silently, so check it first.

In v12 the name strike carried two different units: historical rows said dollars (550.0) while the streaming contract payload and the fluent builder said thousandths of a dollar (550000). Code that joined streaming contracts against historical rows by strike compared values 1000x apart.

In v13 strike is the price in dollars on every surface, and the wire's fixed-point integer is not reachable under that name anywhere.

python
# v12
option = Contract.option("SPY", expiration="20260618", strike="550", right="C")
option.strike            # 550000  (thousandths — the trap)
event.contract.strike_dollars  # 550.0

# v13
option = Contract.option("SPY", expiration="20260618", strike=550, right="C")
option.strike            # 550.0
event.contract.strike    # 550.0 — joins directly against row.strike

The fluent builders accept the strike as a number or string (550, 550.0, "550" are equivalent). Integers are dollars: code that previously passed the wire integer (strike=550000) now requests a $550,000 strike and must divide by 1000 before calling.

Per binding:

  • PythonContractRef.strike is Optional[float] dollars; strike_dollars is removed.
  • TypeScript — the streaming Contract.strike is number dollars; strikeDollars is removed. Contract.option(...) accepts number | string.
  • Rust — the codec struct field is renamed Contract.strike_thousandths (the wire encoding, unit in the name); read dollars via strike_dollars().
  • CTdxContract.strike is double dollars. Offset and struct size are unchanged (the field widens into former tail padding); recompile against the new header. The C++ tdx::strike_dollars(c) helper is now tdx::strike(c).
  • WS server — subscribe payloads take "strike" as a JSON number in dollars (550 or 550.5); the contract JSON in every outgoing frame emits dollars. Clients that sent thousandths must divide by 1000.
  • Flat files — decoded rows (FlatFileRow.strike, the Python/TS row dicts, the Arrow projection) carry dollars; the on-disk CSV/JSONL writers keep the vendor's file format.

Option right is the logical character

Historical tick rows exposed right as the wire's raw ASCII integer (67 / 80). Every surface now decodes it:

rust
// v12
if tick.right == 67 { ... }

// v13
if tick.right == 'C' { ... }      // or tick.is_call()
  • Rustright: char ('C' / 'P'; '\0' when contract identity is absent).
  • Python / TypeScript — one-character string, None / undefined when absent (see below).
  • Cuint32_t holding the Unicode scalar value (67 = 'C', 80 = 'P', 0 = absent); cast to char for display. Field offsets are unchanged.
  • CLI / server — render C / P.

EOD time columns carry their real names

The vendor's v3 EOD report has two time columns: when the report was generated, and when the day's last trade printed. v12 called them ms_of_day and ms_of_day2; the first was easy to misread as a trade time.

python
# v12                    # v13
tick.ms_of_day           tick.created_ms_of_day      # report creation, ~17:15 ET
tick.ms_of_day2          tick.last_trade_ms_of_day   # 0 when no trades printed

The rename applies to every binding, the Arrow / pandas / polars columns, and the CLI. The HTTP server emits the vendor's own created / last_trade JSON keys. On no-trade days last_trade_ms_of_day is 0 and open / high / low / close are 0.0 (documented zero-fill); the quote side still carries the closing NBBO.

Calendar days speak the vendor vocabulary

python
# v12
if day.is_open == 1: ...
if day.status == 2: ...   # magic number from decoder source

# v13
if day.is_open: ...
if day.status == "full_close": ...
  • Rustis_open: bool, status: CalendarStatus (exported enum: Open, EarlyClose, FullClose, Weekend, with as_str()).
  • Python / TypeScript / server JSON / Arrowis_open boolean, status string: "open", "early_close", "full_close", "weekend".
  • Cis_open is C99 bool; status stays int32_t with the TDX_CALENDAR_STATUS_* constants and the tdx_calendar_status_name() lookup.

Python: lambda_

The Greeks rows' lambda attribute was unreachable with attribute syntax (tick.lambda is a SyntaxError). It is now spelled lambda_ per the PEP 8 keyword convention — attribute, constructor kwarg, and repr alike:

python
# v12
leverage = getattr(tick, "lambda")

# v13
leverage = tick.lambda_

Columnar outputs keep the logical name: the Arrow field, pandas column, and dict key stay lambda.

Absent contract identity is None

On single-contract historical queries the server does not populate expiration / strike / right. v12 filled them with 0 / 0.0 / "" in Python and TypeScript while the streaming payload used None / undefined — two absence conventions for one concept.

v13 uses one: absent identity is None (Python), undefined/null (TypeScript), and an Arrow null in columnar output.

python
# v12
if tick.expiration == 0: ...

# v13
if tick.expiration is None: ...

The #[repr(C)] Rust and C rows keep their documented fills (0, 0.0, '\0') — has_contract_id() is the presence check there. Zero-fill remains, documented per field, where absence is unambiguous (sizes, volumes, the no-trade EOD price columns).

TypeScript: options objects

Optional endpoint parameters were positional; skipping one meant undefined holes, and a misplaced hole silently shifted every later argument. Each endpoint now takes its required parameters positionally plus one optional trailing options object whose keys are the camelCase parameter names; timeoutMs rides in the same object.

ts
// v12
client.stockHistoryTradeQuote('AAPL', '20260601', '20260605', undefined, undefined, true, 30000)

// v13
client.stockHistoryTradeQuote('AAPL', '20260601', '20260605', {
  exclusive: true,
  timeoutMs: 30000,
})

Calls that passed only required parameters are unchanged. Each endpoint exports its <Method>Options interface from index.d.ts.

FPSS parse-error event: ParseError

The protocol parse-error event class was named Error, colliding with Python's exception vocabulary and shadowing the JS global. It is ParseError in every binding, with kind tag parse_error:

ts
// v12                           // v13
case 'error':                    case 'parse_error':
  handle(event.error)              handle(event.parseError)

In Python, thetadatadx.Error no longer exists: the event class is ParseError and the exception hierarchy roots at ThetaDataError (catch that, or a specific leaf like NoDataFoundError). In C the event kind is TDX_FPSS_PARSE_ERROR and the payload struct is TdxFpssParseError; the TdxFpssEventKind discriminants renumber — recompile.

Columnar column names match row attributes

DataFrame and Arrow columns now use the public field name everywhere: OptionContract tables emit symbol (previously the wire spelling root) and InterestRateTick tables emit date (previously created). Update any df["root"] / df["created"] selections.

Server REST shape

REST responses align contract identity with the vendor's v3 docs: expiration is an ISO YYYY-MM-DD string (was a YYYYMMDD integer) and EOD rows use the created / last_trade keys. Time-of-day values remain Eastern-Time millisecond integers paired with date — the SDK's documented raw-time convention; pair them with date or convert with the new epoch accessors below.

New: epoch-instant accessors

Every row that carries date plus a milliseconds-of-day column gains a read-side epoch accessor (Unix epoch milliseconds, UTC, DST-aware) — the raw integer fields stay primary:

python
tick.timestamp_ms             # date + ms_of_day
eod.created_timestamp_ms      # date + created_ms_of_day
eod.last_trade_timestamp_ms   # date + last_trade_ms_of_day
greeks.underlying_timestamp_ms

Rust exposes the same names as methods returning Option<i64>; C exposes tdx_timestamp_ms(date, ms_of_day) (returns -1 for absent dates); C++ wraps it as tdx::timestamp_ms returning std::optional<int64_t>. List endpoints additionally return their values sorted ascending (numeric-aware for strike and date lists).

Released under the Apache-2.0 License.