Skip to content

Lenient timestamp deserializer for Transaction + Order time fields#3

Open
dem23-h wants to merge 1 commit into
tigerfintech:mainfrom
dem23-h:permissive-timestamps-pr
Open

Lenient timestamp deserializer for Transaction + Order time fields#3
dem23-h wants to merge 1 commit into
tigerfintech:mainfrom
dem23-h:permissive-timestamps-pr

Conversation

@dem23-h
Copy link
Copy Markdown

@dem23-h dem23-h commented May 10, 2026

Problem

Tiger's paper /order_transactions endpoint has been observed returning transactedAt and time as "YYYY-MM-DD HH:MM:SS" naive datetime strings instead of epoch milliseconds. With the strict i64 typing in Transaction, decoding Vec<Transaction> fails on the first such row, breaking every call site that lists transactions:

decode items failed: invalid type: string "2026-05-08 22:57:14", expected i64

Reproduced on a fresh paper account against https://openapi-sandbox.tigerfintech.com after placing a few orders that filled. Once any transaction in the response carries the string-shaped timestamp, the entire response fails to deserialize — get_order_transactions returns Err(TigerError::Config("decode items failed: ...")) instead of the rows.

Fix

Adds model::serde_helpers::deserialize_lenient_timestamp which accepts:

  • JSON number (epoch ms) — pass through
  • numeric string ("1700000000000") — parse::<i64>
  • naive datetime ("YYYY-MM-DD HH:MM:SS") — interpreted as UTC, returned as epoch ms via NaiveDateTime::and_utc().timestamp_millis()
  • RFC 3339 ("2026-05-08T22:57:14Z" / with offset) — DateTime::parse_from_rfc3339
  • null / empty / unknown shape — fall back to 0 + tracing::warn!

Applied to:

  • Transaction.transacted_at and Transaction.time — the actual bug
  • Order.open_time, Order.update_time, Order.latest_time — defensive (Tiger may flip these to strings on a future release; same parser handles either shape)

TZ note

Tiger does not document the timezone of the naive datetime strings. The patch interprets them as UTC since (a) the rest of Tiger's i64 timestamp fields are already epoch-ms UTC, and (b) interpreting naive strings as UTC keeps relative ordering within a Vec<Transaction> consistent against the existing UTC-typed fields — important for downstream consumers that key off transacted_at for chronological replay. If Tiger documents a different convention later, the parser is one place to update.

Tests

  • 10 deserializer-level tests covering every accepted shape (integer, float-truncating-to-int, null, numeric string, naive datetime, RFC 3339 Z, RFC 3339 with offset, empty/whitespace string, unknown string, unexpected JSON shape).
  • 2 integration tests deserializing a Transaction with string timestamps and a Vec<Transaction> with mixed shapes (the exact pattern call_into_items::<Transaction> chokes on).
cargo test --lib
test result: ok. 245 passed; 0 failed; 0 ignored

Backward compatibility

Pure additive — the deserializer is permissive; any existing consumer that depended on i64 epoch-ms behaviour gets identical results. tracing::warn on unknown-shape strings only fires for values that would have previously panicked the decode entirely.

Why this matters downstream

In our trading engine, this bug fired on:

  • Boot-time reconciliation against the broker — events_boot_reconcile calls get_orders + get_order_transactions; the latter blew up on every restart against a paper account with prior fills.
  • Live frontend calls to list_transactions (HTTP 500 on the per-position transaction-history dropdown).

A permissive deserializer is the simplest fix that doesn't require us to fork the crate or duplicate the wire model. Happy to iterate on the patch shape (alternative TZ assumption, different fallback behaviour, narrower field selection) — let me know what you'd prefer.

Tiger's paper /order_transactions endpoint has been observed returning
`transactedAt` and `time` as "YYYY-MM-DD HH:MM:SS" naive datetime
strings instead of epoch milliseconds. The strict `i64` typing in
v0.3.0 makes `Vec<Transaction>` decode fail on the first such row,
breaking every call site that lists transactions:

  invalid type: string "2026-05-08 22:57:14", expected i64

This patch adds `model::serde_helpers::deserialize_lenient_timestamp`
which accepts:
- JSON number (epoch ms) — pass through
- numeric string ("1700000000000")
- naive datetime ("YYYY-MM-DD HH:MM:SS") — interpreted as UTC
- RFC 3339 ("2026-05-08T22:57:14Z" or with offset)
- null / empty string / unknown shape — fall back to 0 + tracing::warn

Applied to:
- Transaction.transacted_at, Transaction.time (the actual bug)
- Order.open_time, Order.update_time, Order.latest_time (defensive —
  Tiger may flip these to strings on a future release)

Naive strings without timezone are interpreted as UTC; Tiger does not
document the actual TZ for these strings, but UTC is consistent with
the rest of Tiger's epoch fields (also UTC) so relative ordering
within the events log is preserved.

+10 deserializer tests (every shape) + 2 integration tests covering
Vec<Transaction> with mixed shapes. Total fork tests: 241 pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant