Skip to content

SDK drift: fetchOHLCV serializes start/end as UTC ISO-8601 with Z in TypeScript but as a bare datetime.isoformat() in Python #2095

Description

@realfishsam

Drift

Both SDKs accept a native date/time object for the start / end bounds of fetchOHLCV and stringify it before putting it on the wire, but they emit different formats:

  • TypeScript calls Date.prototype.toISOString(), which always normalizes to UTC and appends a Z designator.
  • Python calls datetime.isoformat(), which emits no timezone designator at all for a naive datetime (the common case), and a +HH:MM offset for an aware one.

The same wall-clock value therefore reaches the sidecar as two different instants depending on which SDK the caller used.

TypeScript SDK

sdks/typescript/pmxt/client.ts:1991-2003:

const paramsDict: any = { resolution: params.resolution };
if (params.start) {
    paramsDict.start = params.start.toISOString();   // <-- line 1993
}
if (params.end) {
    paramsDict.end = params.end.toISOString();       // <-- line 1996
}
if (params.limit) {
    paramsDict.limit = params.limit;
}

const args = [resolvedOutcomeId, paramsDict];
const query = { outcomeId: resolvedOutcomeId, ...paramsDict };

new Date(2025, 0, 1).toISOString()"2025-01-01T00:00:00.000Z" (always UTC, always Z, always milliseconds).

Python SDK

sdks/python/pmxt/client.py:2371-2387:

params_dict = {}
if resolution:
    params_dict["resolution"] = resolution
if start:
    params_dict["start"] = start.isoformat()     # <-- line 2375
if end:
    params_dict["end"] = end.isoformat()         # <-- line 2377
if limit:
    params_dict["limit"] = limit
...
args = [outcome_id, params_dict]
query = {"outcomeId": outcome_id, **params_dict}
  • datetime(2025, 1, 1).isoformat()"2025-01-01T00:00:00" — no Z, no offset, no milliseconds.
  • datetime(2025, 1, 1, tzinfo=timezone.utc).isoformat()"2025-01-01T00:00:00+00:00" — offset form, still not Z.

The start/end parameter is documented as start: Start datetime for historical data (client.py:2352), and every example in the repo passes a naive datetime, so the no-designator form is the realistic path.

Expected

TypeScript is right: the wire value should be an unambiguous, UTC-normalized ISO-8601 timestamp. Python should convert to UTC and emit a Z-suffixed string, e.g.

start.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")

(with a documented assumption — or an explicit timezone.utc attachment — for naive inputs), so both SDKs put the same instant on the wire for the same wall-clock input.

Impact

A caller in a non-UTC timezone gets a different candle range from each SDK for identical code. exchange.fetch_ohlcv(outcome, resolution="1h", start=datetime(2025, 1, 1)) in Python sends 2025-01-01T00:00:00, which the sidecar must guess at — typically interpreting it as UTC — whereas the TypeScript caller's new Date(2025, 0, 1) is converted from local time first and lands on a genuinely different instant (up to ±14 hours away). Neither side errors; the results just silently disagree, which is the worst failure mode for a historical-data API where users cross-check the two SDKs against each other. The aware-datetime case is milder but still emits a format (+00:00) that TypeScript never produces, so any server-side or downstream string comparison on the timestamp diverges too.


Found by automated SDK cross-language drift audit

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions