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
| Surface | Change | Migration |
|---|---|---|
strike (streaming + builders) | Dollars everywhere; wire integer gone | Read strike as dollars; delete / 1000 conversions and strike_dollars reads |
right (tick rows) | Logical character, never the ASCII integer | Compare against 'C' / "C" instead of 67 |
EodTick.ms_of_day / .ms_of_day2 | Renamed created_ms_of_day / last_trade_ms_of_day | Rename field reads; semantics now in the name |
CalendarDay.is_open / .status | bool + vendor vocabulary | Replace 0-3 integer checks with "open" / "early_close" / "full_close" / "weekend" (Rust: CalendarStatus) |
Python Greeks lambda | Spelled lambda_ | Replace getattr(tick, "lambda") with tick.lambda_ |
| Absent contract identity | None / undefined in Python / TS | Replace == 0 / == "" absence checks with is None / == null |
| TypeScript optional params | One trailing options object | Replace positional undefined holes with { key: value } |
| FPSS parse-error event | Class renamed ParseError, kind parse_error | Rename event handling; Error no longer names an SDK type |
C TdxContract.strike | double dollars | Recompile 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.
# 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.strikeThe 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:
- Python —
ContractRef.strikeisOptional[float]dollars;strike_dollarsis removed. - TypeScript — the streaming
Contract.strikeisnumberdollars;strikeDollarsis removed.Contract.option(...)acceptsnumber | string. - Rust — the codec struct field is renamed
Contract.strike_thousandths(the wire encoding, unit in the name); read dollars viastrike_dollars(). - C —
TdxContract.strikeisdoubledollars. 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 nowtdx::strike(c). - WS server — subscribe payloads take
"strike"as a JSON number in dollars (550or550.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:
// v12
if tick.right == 67 { ... }
// v13
if tick.right == 'C' { ... } // or tick.is_call()- Rust —
right: char('C'/'P';'\0'when contract identity is absent). - Python / TypeScript — one-character string,
None/undefinedwhen absent (see below). - C —
uint32_tholding the Unicode scalar value (67 ='C', 80 ='P', 0 = absent); cast tocharfor 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.
# 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 printedThe 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
# 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": ...- Rust —
is_open: bool,status: CalendarStatus(exported enum:Open,EarlyClose,FullClose,Weekend, withas_str()). - Python / TypeScript / server JSON / Arrow —
is_openboolean,statusstring:"open","early_close","full_close","weekend". - C —
is_openis C99bool;statusstaysint32_twith theTDX_CALENDAR_STATUS_*constants and thetdx_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:
# 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.
# 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.
// 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:
// 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:
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_msRust 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).