Trading
Positions and exits
A fill folds into a position. Positions are held per player, per contract, per side, and are readable and exitable over the API.
How a position is built
A filled order writes four things in one transaction: the fill, the order state, the fee row and the position. They are a single financial fact, and a partial application of them is the one outcome with no clean recovery, because a filled order with no position means a player was charged for something they do not hold.
A second buy on the same contract and side folds into the existing position at a weighted average rather than creating a second row. The average is recomputed in SQL against the current row, so two concurrent fills cannot overwrite each other.
Reading positions
/api/v1/positionsAPI keypositions:read. ?externalUserId= your own id for a player, ?status=open|closed|settled, ?limit= 1-200 (default 50), ?cursor=. An externalUserId that has never traded returns an empty page, not 404 unknown_user. Check the id before reading the emptiness as a fact./api/v1/positions/{id}API keypositionId. A settlement record and a settlement webhook both key on that id, so a partner processing one has an id and a single question: what was this? Paging a growing feed to answer it would be absurd.{
"positions": [
{
"positionId": "pos_...",
"externalUserId": "player-8421",
"contractId": "ctr_9f2b...",
"side": "YES",
"status": "open",
"quantity": 34.3056,
"averagePrice": 0.72,
"openedAt": "2026-08-21T04:43:40.282Z",
"updatedAt": "2026-08-21T04:43:40.282Z",
"operatorMoney": { "currency": "USD", "costBasis": "24.70", "realisedPnl": "0.00" }
}
],
"count": 1,
"nextCursor": null,
"hasMore": false
}| Field | Meaning |
|---|---|
externalUserId | Your own id for the player. Parity’s internal uuid is never published. |
contractId | The opaque ctr_… contract. Settlement follows this contract and no other. |
quantity | Contracts held. Each settles at $1.00 or $0.00. Size an exit with this. |
averagePrice | Weighted average entry price. |
status | open, closed or settled. |
operatorMoney.costBasis | Cash already spent on what is STILL held. |
operatorMoney.realisedPnl | Booked by exits and settlements. Negative on a loss. |
A position carries facts, not instructions
costBasis is cash already debited on the way in, not cash to debit now. realisedPnl is a fact about closed quantity, not a credit to post. The instructions live where they are generated: on the order (operatorMoney, operatorExitMoney) and on the settlement (settlementCredit, voidCredit).
A position that also carried an amount to move would give you two places to book the same dollar from, and the duplicate would only ever show up as a balance that will not reconcile.
There is no mark-to-market, deliberately
Valuing an open position against the current bid would make this response change between two identical calls because a price feed moved, and a number that moves on its own is not something a wallet can reconcile against. Want the exit value? Quote a sell, for a real, honourable price rather than an estimate.
Why the cursor sorts on openedAt
A keyset cursor needs an immutable sort key. updatedAt would be more useful for polling and is unusable here: a position sold down mid-page jumps to the front, and the row it displaced is never returned. openedAt never moves.
A cursor that cannot be decoded is refused; one that names a missing row is not
A cursor this feed cannot decode is 400 invalid_cursor, the same as on every other paged feed. A cursor that decodes cleanly and names a row that is no longer there returns the first page, because an empty page cannot be told apart from the end of the feed.
So treat an unexpectedly full first page after a resume as a signal rather than as a busy night, and never let a page of positions be the thing that decides a credit.
Closing a position, in full or in part
An exit is an ordinary quote-then-order, with action: "sell". There is no separate close endpoint: one execution path means one set of safety rules, one idempotency index and one money contract.
# 1. Price the exit. Sized in CONTRACTS, from the position's own quantity.
curl -s -X POST "$PARITY_BASE/api/v1/quotes" \
-H "Authorization: Bearer $PARITY_KEY" \
-H "X-Parity-User: $PARITY_USER" \
-H "content-type: application/json" \
-d '{"marketId":"ctr_9f2b...","side":"YES","action":"sell","contracts":17.15}'
# 2. Spend the quote, exactly like a buy.
curl -s -X POST "$PARITY_BASE/api/v1/orders" \
-H "Authorization: Bearer $PARITY_KEY" \
-H "X-Parity-User: $PARITY_USER" \
-H "content-type: application/json" \
-d '{"quoteId":"...","clientOrderId":"exit-9031"}'const exitQuote = await player.quoteSell({
contractId: held.contractId,
side: held.side,
contracts: held.quantity / 2,
});
const exit = await player.submitOrder({
quoteId: exitQuote.quoteId,
clientOrderId: `exit-${reservation.id}`,
});
await wallet.credit(exit.operatorExitMoney!.sellProceeds);An exit is sized in contracts, and it credits
A cash-sized sell is refused, and the refusal is right: inverting the fee rounding either oversells or strands a fraction, so “close this out” cannot be expressed in dollars at all. Pass the position's quantity.
The response carries operatorExitMoney.sellProceeds and operatorMoney: null. sellProceeds is already net of the fees reported beside it; subtracting them again is a double charge, and the null is there so a wallet applying the entry contract by habit cannot debit a player for selling their own position.
- A partial exit reduces
quantityand books intorealisedPnl. The remainder stays open at the sameaveragePrice, because an exit does not re-price what is left. - Selling more than is held is refused with
422 insufficient_position. Read the position first; do not infer the quantity from your own order history. - Fee attribution at settlement is pro-rated by the share of bought contracts still held, so a partial close does not over-refund a void.
Partial fills
partially_filled is a real state that a live book will produce. filledQuantity is what the player actually holds and may be less than the quote's contracts.
The money contract already answers it: operatorMoney.actualDebit is what the fill consumed and operatorMoney.releaseAmount is the rest of the authorization, to be handed back. An operator that debits the full stake on a partial fill is keeping money that is not theirs. See The money contract.

