Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions docs/resources/live-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
```

Expand Down
21 changes: 17 additions & 4 deletions docs/resources/portfolio.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 13 additions & 5 deletions docs/resources/structured-targets.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
20 changes: 18 additions & 2 deletions kalshi/resources/live_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading