Skip to content

Portfolio

Account balance, positions, settlements, and total resting order value. Auth required throughout.

Quick reference

Method Endpoint
balance(*, subaccount=None) GET /portfolio/balance
positions(*, ...) GET /portfolio/positions
settlements(...) / settlements_all(...) GET /portfolio/settlements
fills(...) / fills_all(...) GET /portfolio/fills
total_resting_order_value() GET /portfolio/summary/total_resting_order_value (FCM only)
deposits(*, limit, cursor) / deposits_all(*, limit, max_pages) GET /portfolio/deposits
withdrawals(*, limit, cursor) / withdrawals_all(*, limit, max_pages) GET /portfolio/withdrawals

balance(), positions() / positions_all(), settlements() / settlements_all(), and fills() / fills_all() all take an optional subaccount: int to scope the read to a specific subaccount (omit for the primary account).

Balance

bal = client.portfolio.balance()
print(bal.balance, bal.balance_dollars, bal.portfolio_value, bal.updated_ts)

Balance.balance and portfolio_value are integer cents. Balance.balance_dollars (required, new in v2.1.0) is the same amount as a DollarDecimal — use whichever shape your code expects without manually dividing by 100.

Balance.balance_breakdown (optional, also new in v2.1.0) splits the total across exchange shards:

if bal.balance_breakdown is not None:
    for shard in bal.balance_breakdown:
        # IndexedBalance.balance is DollarDecimal, not cents — same name
        # as Balance.balance but a different type. See note below.
        print(shard.exchange_index, shard.balance)

Type collision: Balance.balance vs. IndexedBalance.balance

Balance.balance is integer cents. IndexedBalance.balance (inside balance_breakdown) is DollarDecimal (dollars), matching Balance.balance_dollars units. Same field name, different types — be deliberate when iterating the breakdown.

Constructing Balance directly

v2.1.0 made balance_dollars required to match spec v3.18.0. Existing code that calls client.portfolio.balance() is unaffected. Code that builds Balance(...) directly (typically in tests/mocks) needs the field added — see Migration.

Positions

resp = client.portfolio.positions(
    limit=200,
    count_filter="position",        # only return rows with non-zero `position` (etc.)
    ticker="KXPRES-24-DJT",
    event_ticker="KXPRES-24",
)
for mp in resp.market_positions:
    print(mp.ticker, mp.position, mp.realized_pnl)
for ep in resp.event_positions:
    print(ep.event_ticker, ep.event_exposure)

positions() does not return Page[T]

It returns PositionsResponse — two parallel lists (market_positions 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:

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:

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

count_filter filters which fields the response includes a row for — filtering by "position" returns only markets where your position is non-zero.

Settlements

page = client.portfolio.settlements(
    ticker="KXPRES-24-DJT",
    event_ticker="KXPRES-24",
    min_ts=1_700_000_000,
    max_ts=1_800_000_000,
    limit=200,
)
for s in page:
    print(s.ticker, s.settled_at, s.market_result, s.revenue)

# Or:
for s in client.portfolio.settlements_all():
    ...

Standard Page[Settlement] pagination — see Pagination.

Fills

page = client.portfolio.fills(
    ticker="KXPRES-24-DJT",
    min_ts=1_700_000_000,
    max_ts=1_800_000_000,
    limit=200,
)
for f in page:
    print(f.trade_id, f.order_id, f.yes_price, f.count, f.is_taker)

# Or auto-paginate:
for f in client.portfolio.fills_all(ticker="KXPRES-24-DJT"):
    ...

Standard Page[Fill] pagination — see Pagination.

Moved from OrdersResource in v3.0.0 (issue #351)

client.orders.fills(...) / client.orders.fills_all(...) still work in v3.0.0 but emit a DeprecationWarning and will be removed in a future release. Update call sites to client.portfolio.fills(...) / client.portfolio.fills_all(...).

Total resting order value

total = client.portfolio.total_resting_order_value()
print(total.total_value)

FCM members only

Non-FCM accounts get a 403 (mapped to KalshiAuthError). Demo mirrors production behavior here.

Deposits and withdrawals

New in v2.1.0. Standard Page[T] pagination — see Pagination.

# Most recent deposits
page = client.portfolio.deposits(limit=50)
for d in page:
    print(d.id, d.status, d.type, d.amount_cents, d.created_ts)

# All deposits
for d in client.portfolio.deposits_all():
    ...

# Withdrawals follow the same shape
for w in client.portfolio.withdrawals_all():
    print(w.id, w.status, w.amount_cents, w.fee_cents)

Deposit and Withdrawal are structurally identical: id, status (PaymentStatusLiteral: "pending" / "applied" / "failed" / "returned"), type (PaymentTypeLiteral: "ach" / "wire" / "crypto" / "debit" / "apm"), amount_cents (int), fee_cents (int), created_ts (int Unix seconds), and finalized_ts: int | None which is None until the transfer settles.

Both *_all variants accept max_pages=N to bound iteration.

Position fields

MarketPosition and EventPosition use the standard _dollars / _fp wire-format aliases — on the wire you see total_traded_dollars, market_exposure_dollars, realized_pnl_dollars, fees_paid_dollars, position_fp. The SDK normalizes to short names returning Decimal. realized_pnl is signed.

Reference

kalshi.resources.portfolio.PortfolioResource

PortfolioResource(transport: SyncTransport)

Bases: SyncResource

Sync portfolio API.

positions_all

positions_all(
    *,
    limit: int | None = None,
    count_filter: str | None = None,
    ticker: str | None = None,
    event_ticker: str | None = None,
    subaccount: int | None = None,
    max_pages: int | None = None,
    extra_headers: dict[str, str] | None = None
) -> Iterator[MarketPosition]

Auto-paginate /portfolio/positions, yielding each MarketPosition.

Mirrors :meth:settlements_all. The endpoint response also carries event_positions (aggregate roll-ups over the same underlying markets); those are not surfaced here because page boundaries cut the aggregate arbitrarily and concatenating across pages would not recompute a meaningful event-level total. Callers that need the event view should iterate :meth:positions page-by-page.

fills

fills(
    *,
    ticker: str | None = None,
    order_id: str | None = None,
    min_ts: int | None = None,
    max_ts: int | None = None,
    limit: int | None = None,
    cursor: str | None = None,
    subaccount: int | None = None,
    extra_headers: dict[str, str] | None = None
) -> Page[Fill]

List trade fills (GET /portfolio/fills).

Moved from :class:OrdersResource in v3.0.0 (issue #351) to group with the rest of the /portfolio/* family (settlements, deposits, withdrawals).

fills_all

fills_all(
    *,
    ticker: str | None = None,
    order_id: str | None = None,
    min_ts: int | None = None,
    max_ts: int | None = None,
    limit: int | None = None,
    subaccount: int | None = None,
    max_pages: int | None = None,
    extra_headers: dict[str, str] | None = None
) -> Iterator[Fill]

Auto-paginate trade fills. Moved from :class:OrdersResource in v3.0.0.

total_resting_order_value

total_resting_order_value(
    *, extra_headers: dict[str, str] | None = None
) -> TotalRestingOrderValue

Total value of resting orders in cents. FCM-members only.

Non-FCM accounts receive 403; demo mirrors prod on this route per Path B audit (2026-04-18).

kalshi.resources.portfolio.AsyncPortfolioResource

AsyncPortfolioResource(transport: AsyncTransport)

Bases: AsyncResource

Async portfolio API.

positions_all

positions_all(
    *,
    limit: int | None = None,
    count_filter: str | None = None,
    ticker: str | None = None,
    event_ticker: str | None = None,
    subaccount: int | None = None,
    max_pages: int | None = None,
    extra_headers: dict[str, str] | None = None
) -> AsyncIterator[MarketPosition]

Async counterpart of :meth:PortfolioResource.positions_all. Use async for.

Mirrors :meth:settlements_all. The endpoint response also carries event_positions (aggregate roll-ups over the same underlying markets); those are not surfaced here because page boundaries cut the aggregate arbitrarily and concatenating across pages would not recompute a meaningful event-level total. Callers that need the event view should iterate :meth:positions page-by-page.

settlements_all

settlements_all(
    *,
    limit: int | None = None,
    ticker: str | None = None,
    event_ticker: str | None = None,
    min_ts: int | None = None,
    max_ts: int | None = None,
    subaccount: int | None = None,
    max_pages: int | None = None,
    extra_headers: dict[str, str] | None = None
) -> AsyncIterator[Settlement]

Returns an async iterator — use async for.

fills async

fills(
    *,
    ticker: str | None = None,
    order_id: str | None = None,
    min_ts: int | None = None,
    max_ts: int | None = None,
    limit: int | None = None,
    cursor: str | None = None,
    subaccount: int | None = None,
    extra_headers: dict[str, str] | None = None
) -> Page[Fill]

List trade fills (GET /portfolio/fills, async).

Moved from :class:AsyncOrdersResource in v3.0.0 (issue #351).

fills_all

fills_all(
    *,
    ticker: str | None = None,
    order_id: str | None = None,
    min_ts: int | None = None,
    max_ts: int | None = None,
    limit: int | None = None,
    subaccount: int | None = None,
    max_pages: int | None = None,
    extra_headers: dict[str, str] | None = None
) -> AsyncIterator[Fill]

Auto-paginate trade fills (async). Use async for.

Moved from :class:AsyncOrdersResource in v3.0.0 (issue #351).

total_resting_order_value async

total_resting_order_value(
    *, extra_headers: dict[str, str] | None = None
) -> TotalRestingOrderValue

Total value of resting orders in cents. FCM-members only.

Non-FCM accounts receive 403; demo mirrors prod on this route per Path B audit (2026-04-18).

deposits_all

deposits_all(
    *,
    limit: int | None = None,
    max_pages: int | None = None,
    extra_headers: dict[str, str] | None = None
) -> AsyncIterator[Deposit]

Returns an async iterator — use async for.

withdrawals_all

withdrawals_all(
    *,
    limit: int | None = None,
    max_pages: int | None = None,
    extra_headers: dict[str, str] | None = None
) -> AsyncIterator[Withdrawal]

Returns an async iterator — use async for.