Skip to content

Latest commit

 

History

History
320 lines (255 loc) · 12 KB

portfolio.md

File metadata and controls

320 lines (255 loc) · 12 KB

Portfolio

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

Quick reference

Method Endpoint
balance(*, subaccount=None, exchange_index=None) GET /portfolio/balance
positions(*, ..., exchange_index=None) GET /portfolio/positions
settlements(...) / settlements_all(...) GET /portfolio/settlements
fills(...) / fills_all(..., exchange_index=None) 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
credit_history(*, limit, cursor, min_ts, max_ts) / credit_history_all(...) GET /portfolio/credit_history
exit_triggers(ticker, *, kind, subaccount) GET /portfolio/positions/{ticker}/exit_trigger
set_exit_trigger(ticker, *, request, subaccount) PUT /portfolio/positions/{ticker}/exit_trigger (never retried)
cancel_exit_trigger(ticker, *, kind, subaccount) DELETE /portfolio/positions/{ticker}/exit_trigger (never retried)
withdrawals(*, limit, cursor) / withdrawals_all(*, limit, max_pages) GET /portfolio/withdrawals
intra_exchange_transfers(...) / intra_exchange_transfers_all(...) GET /portfolio/intra_exchange_instance_transfers
get_intra_exchange_transfer(transfer_id) GET /portfolio/intra_exchange_instance_transfers/{transfer_id}
target_balance_allocation() GET /portfolio/target_balance_allocation
set_target_balance_allocation(*, allocations, resting_margin_reservation=None) POST /portfolio/target_balance_allocation

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() also takes an optional exchange_index: int (spec v3.24.0) to target a specific exchange shard (defaults to 0 server-side). positions() / positions_all() and fills() / fills_all() take an optional exchange_index filter as of OpenAPI 3.28.0 — omit it to return rows from every shard.

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)

!!! warning "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.

!!! note "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.)
    settlement_status="unsettled",  # "unsettled" (server default) | "settled" | "all"
    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)

!!! 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. 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
```

count_filter is a comma-separated OR list: "position" (non-zero net contracts, including closed markets awaiting determination) or "total_traded" (any contracts traded, including fully exited positions). Omit it for no count filter.

settlement_status (SettlementStatusLiteral: "unsettled", "settled", "all") filters settlement state, not trading status. Unsettled (the server default) includes closed markets awaiting determination or settlement; those rows may still have non-zero quantities and cost basis. "settled" is unarchived settled positions; "all" is both live states. Archived positions stay on GET /historical/positions.

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.

!!! note "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_resting_order_value)  # integer cents
for shard in total.resting_order_value_breakdown:
    # IndexedBalance.balance is DollarDecimal, not cents
    print(shard.exchange_index, shard.balance)

resting_order_value_breakdown is required as of OpenAPI 3.28.0. Direct constructors (tests/mocks) must pass it; an empty list is valid.

!!! warning "FCM members only" Non-FCM accounts get a 403 (mapped to KalshiAuthError). Demo mirrors production behavior here.

Credit history

credit_history returns Page[CreditHistory], newest first. min_ts is inclusive and max_ts is exclusive (credit creation time). limit is 1–1000. credit_type is the spec's type field. amount is a DollarDecimal (amount_dollars on the wire).

page = client.portfolio.credit_history(limit=50)
for credit in page:
    print(credit.credit_id, credit.credit_type, credit.amount, credit.status)

for credit in client.portfolio.credit_history_all():
    ...

Exit triggers

Take-profit / stop-loss brackets on an event-contract position. These routes are hidden in the published OpenAPI (x-excluded) until the feature launches. PUT and DELETE are never retried. Prices are YES prices (DollarDecimal). set_exit_trigger replaces both legs — omit a leg to clear it, and pass at least one of stop_loss_price or take_profit_price.

from decimal import Decimal
from kalshi import SetEventContractExitTriggerRequest

trigger = client.portfolio.set_exit_trigger(
    "HIGHNY-24JAN01-T60",
    request=SetEventContractExitTriggerRequest(stop_loss_price=Decimal("0.5600")),
)
print(trigger.id, trigger.status, trigger.stop_loss_price)

current = client.portfolio.exit_triggers("HIGHNY-24JAN01-T60", kind="bracket")
client.portfolio.cancel_exit_trigger("HIGHNY-24JAN01-T60", kind="bracket")

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.

Intra-exchange instance transfers

New in v10.0.0. History and detail for fund moves between the event_contract and margined exchange instances. Creating a transfer is still on the perps surface (PerpsClient.transfers.transfer_instance()); these GETs are on the core portfolio API. The perps create body accepts an optional client_transfer_id (UUID) for duplicate prevention; omit so the key is not sent.

page = client.portfolio.intra_exchange_transfers(limit=50)
for t in page:
    print(t.transfer_id, t.source, t.destination, t.amount, t.status)

for t in client.portfolio.intra_exchange_transfers_all():
    ...

t = client.portfolio.get_intra_exchange_transfer("xfer-...")
print(t.status, t.created_ts)

IntraExchangeInstanceTransfer.amount is a fixed-point dollar DollarDecimal (not the integer centicents used on the POST create body).

Target balance allocation

Per-shard sweepable-balance targets. POST is never retried.

from kalshi import TargetBalanceAllocationInput

current = client.portfolio.target_balance_allocation()
print(current.resting_margin_reservation)  # "max" or "sum"
client.portfolio.set_target_balance_allocation(
    allocations=[TargetBalanceAllocationInput(exchange_index=0, percent=100)]
)

Percents are integers 0–100. At most 101 allocation rows. Optional resting_margin_reservation is "max" or "sum" (server defaults to "sum") — collateral an automatic rebalance leaves behind for resting orders.

Fill, MarketPosition, and Settlement each carry a required exchange_index as of OpenAPI 3.29.0 (SDK v13.0.0). Live list callers are unaffected; constructors/fixtures must pass it.

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. Optional netting_enabled (bool | None) reports whether netting applies to the position's event (None when the server omits the key).

Reference

::: kalshi.resources.portfolio.PortfolioResource options: heading_level: 3

::: kalshi.resources.portfolio.AsyncPortfolioResource options: heading_level: 3