diff --git a/docs/resources/live-data.md b/docs/resources/live-data.md index 64bf024d..1bea8d3f 100644 --- a/docs/resources/live-data.md +++ b/docs/resources/live-data.md @@ -10,9 +10,9 @@ Public — no auth required. | Method | Endpoint | |---|---| -| `get(milestone_id, *, include_player_stats=None)` | `GET /live_data/{milestone_id}` | -| `batch(milestone_ids, *, include_player_stats=None)` | `GET /live_data` | -| `game_stats(milestone_id)` | `GET /live_data/{milestone_id}/game_stats` | +| `get(milestone_id, *, include_player_stats=None)` | `GET /live_data/milestone/{milestone_id}` | +| `batch(milestone_ids, *, include_player_stats=None)` | `GET /live_data/batch` | +| `game_stats(milestone_id)` | `GET /live_data/milestone/{milestone_id}/game_stats` | | `get_typed(live_data_type, milestone_id)` | `GET /live_data/{type}/milestone/{milestone_id}` (legacy) | ## Get one milestone's live data @@ -25,11 +25,11 @@ print(live.live_data_type, live.payload) ## Batch (up to 100 milestones) ```python -resp = client.live_data.batch( +entries = client.live_data.batch( milestone_ids=["ms_a", "ms_b", "ms_c"], include_player_stats=False, ) -for entry in resp.live_data: +for entry in entries: print(entry.milestone_id, entry.payload) ``` diff --git a/docs/resources/portfolio.md b/docs/resources/portfolio.md index f4102e7c..d1c9bd92 100644 --- a/docs/resources/portfolio.md +++ b/docs/resources/portfolio.md @@ -64,16 +64,29 @@ for ep in resp.event_positions: print(ep.event_ticker, ep.event_exposure) ``` -!!! warning "`positions()` does not return Page[T]" +!!! note "`positions()` does not return Page[T]" It returns `PositionsResponse` — two parallel lists (`market_positions` - and `event_positions`) plus its own `cursor` and `has_next`. There is no - `positions_all()` helper. Walk it manually: + and `event_positions`) plus its own `cursor` and `has_next`. Use + `positions_all()` (sync and async, shipped in v2.5.0) to auto-paginate + over `market_positions` as an iterator of `MarketPosition`: + + ```python + for mp in client.portfolio.positions_all(ticker="KXPRES-24-DJT"): + print(mp.ticker, mp.position, mp.realized_pnl) + ``` + + `event_positions` are intentionally *not* surfaced by `positions_all()`: + they are aggregate roll-ups over the same underlying markets, and page + boundaries cut the aggregate arbitrarily — concatenating across pages + would not recompute a meaningful event-level total. Callers that need + the event view should iterate `positions()` page-by-page: ```python cursor = None while True: resp = client.portfolio.positions(cursor=cursor) - ... + for ep in resp.event_positions: + ... if not resp.has_next: break cursor = resp.cursor diff --git a/docs/resources/structured-targets.md b/docs/resources/structured-targets.md index 3d3818dd..12f68e82 100644 --- a/docs/resources/structured-targets.md +++ b/docs/resources/structured-targets.md @@ -37,15 +37,23 @@ for t in page: ## Get one structured target ```python -t = client.structured_targets.get("st_abc") -if t is None: +from kalshi.errors import KalshiNotFoundError + +try: + t = client.structured_targets.get("st_abc") +except KalshiNotFoundError: print("not found") else: - print(t.name, t.target_type) + if t is None: + # rare: server returned 200 with {"structured_target": null} + print("soft-not-found") + else: + print(t.name, t.target_type) ``` -`get()` may return `None` for unknown IDs (the underlying endpoint returns a -404 that's mapped to `None` rather than `KalshiNotFoundError`). +A 404 from the server raises `KalshiNotFoundError`. `get()` returns `None` +only if the server responds 200 with `{"structured_target": null}` — a +server-side soft-not-found that the spec permits but is rare in practice. ## Reference diff --git a/kalshi/resources/live_data.py b/kalshi/resources/live_data.py index 9f852456..fc4ebf34 100644 --- a/kalshi/resources/live_data.py +++ b/kalshi/resources/live_data.py @@ -48,7 +48,15 @@ def get_typed( include_player_stats: bool | None = None, extra_headers: dict[str, str] | None = None, ) -> LiveData: - """Legacy ``/live_data/{type}/milestone/{milestone_id}`` shape. + """Legacy ``/live_data/{type}/milestone/{milestone_id}`` URL form. + + Despite the name, ``get_typed`` is **not** a more-strongly-typed + variant of :meth:`get`: both return the same :class:`LiveData` + model with the same annotations. The only difference is the URL + — ``get_typed`` hits the legacy path that embeds the milestone + ``{type}`` segment, while :meth:`get` hits the canonical + ``/live_data/milestone/{milestone_id}``. The ``_typed`` suffix + refers to the typed URL form, not to Python typing. ``milestone_type`` populates the ``{type}`` path segment. Named ``milestone_type`` (not ``type``) to avoid shadowing the Python @@ -134,7 +142,15 @@ async def get_typed( include_player_stats: bool | None = None, extra_headers: dict[str, str] | None = None, ) -> LiveData: - """Legacy ``/live_data/{type}/milestone/{milestone_id}`` shape. + """Legacy ``/live_data/{type}/milestone/{milestone_id}`` URL form. + + Despite the name, ``get_typed`` is **not** a more-strongly-typed + variant of :meth:`get`: both return the same :class:`LiveData` + model with the same annotations. The only difference is the URL + — ``get_typed`` hits the legacy path that embeds the milestone + ``{type}`` segment, while :meth:`get` hits the canonical + ``/live_data/milestone/{milestone_id}``. The ``_typed`` suffix + refers to the typed URL form, not to Python typing. ``milestone_type`` populates the ``{type}`` path segment. Named ``milestone_type`` (not ``type``) to avoid shadowing the Python