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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@

All notable changes to kalshi-sdk will be documented in this file.

## 22.9.0 — 2026-11-05

Reconciles nightly spec drift (#571) against OpenAPI 3.36.0 and the latest
perps spec. Additive only. No breaking changes.

### Added

- **`block_trade_proposals.cancel(block_trade_proposal_id)`** (sync and
async) — `DELETE /communications/block-trade-proposals/{id}`, 204, no
body. Only the proposer can cancel. POST/DELETE are never retried.
- **`block_trade_proposals.reject(block_trade_proposal_id)`** (sync and
async) — `POST /communications/block-trade-proposals/{id}/reject`, 204,
no request body. Either counterparty can reject; the proposer cancels
instead. (Closes #571).
- Optional **`MarginMarket.minimum_quantity`** (`FixedPointCount | None`).
Spec marks it required; the SDK keeps it optional (contract exclusion
`server_omits_despite_required`) so payloads omitting the key still
parse. Accepts `minimum_quantity_fp` and `minimum_quantity` aliases.

## 22.8.0 — 2026-11-04

Reconciles nightly spec drift (#569) against OpenAPI 3.36.0 and the latest
Expand Down
8 changes: 8 additions & 0 deletions docs/resources/communications.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ Auth required throughout.
| `block_trade_proposals.list(...)` / `block_trade_proposals.list_all(...)` | `GET /communications/block-trade-proposals` |
| `block_trade_proposals.create(...)` | `POST /communications/block-trade-proposals` |
| `block_trade_proposals.accept(block_trade_proposal_id, *, subtrader_id=None, subaccount=None)` | `POST /communications/block-trade-proposals/{block_trade_proposal_id}/accept` |
| `block_trade_proposals.cancel(block_trade_proposal_id)` | `DELETE /communications/block-trade-proposals/{block_trade_proposal_id}` |
| `block_trade_proposals.reject(block_trade_proposal_id)` | `POST /communications/block-trade-proposals/{block_trade_proposal_id}/reject` |
| `rfq_batches.list(...)` / `rfq_batches.list_all(...)` | `GET /communications/rfq_batches` |
| `rfq_batches.get(rfq_batch_id)` | `GET /communications/rfq_batches/{rfq_batch_id}` |
| `rfq_batches.create(...)` | `POST /communications/rfq_batches` |
Expand Down Expand Up @@ -211,6 +213,12 @@ for proposal in client.communications.block_trade_proposals.list_all(
# Accept (the counterparty side). The body is optional — pass subtrader_id
# or subaccount only if you trade through a sub-trader / subaccount.
client.communications.block_trade_proposals.accept(proposal_id)

# Cancel (proposer only). 204, no body.
client.communications.block_trade_proposals.cancel(proposal_id)

# Reject (either counterparty; the proposer cancels instead). 204, no body.
client.communications.block_trade_proposals.reject(proposal_id)
```

!!! info "Prices are centi-cents, counts are centicounts — plain integers"
Expand Down
2 changes: 1 addition & 1 deletion kalshi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,4 +503,4 @@
"Withdrawal",
]

__version__ = "22.8.0"
__version__ = "22.9.0"
7 changes: 7 additions & 0 deletions kalshi/perps/models/markets.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ class MarginMarket(BaseModel):
underlying_multiplier: str
tick_size: DollarDecimal
fractional_trading_enabled: bool
# Spec marks it required; the SDK keeps it optional so older payloads
# and existing constructors still validate (minor release). Minimum
# order quantity in contracts (FixedPointCount).
minimum_quantity: FixedPointCount | None = Field(
default=None,
validation_alias=AliasChoices("minimum_quantity_fp", "minimum_quantity"),
)
# Spec requires the key; value is null for markets that trade 24/7
# (``MarginMarketSchedule`` is ``nullable: true``). No default — missing
# key hard-fails so required-drift stays aligned.
Expand Down
62 changes: 62 additions & 0 deletions kalshi/resources/communications.py
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,37 @@ def accept(
extra_headers=extra_headers,
)

def cancel(
self, block_trade_proposal_id: str, *, extra_headers: dict[str, str] | None = None
) -> None:
"""Cancel a pending block-trade proposal.

``DELETE /communications/block-trade-proposals/{block_trade_proposal_id}``
— only the proposer can cancel. Returns 204 with no body.
"""
self._require_auth()
self._delete(
f"/communications/block-trade-proposals/"
f"{_seg(block_trade_proposal_id, name='block_trade_proposal_id')}",
extra_headers=extra_headers,
)

def reject(
self, block_trade_proposal_id: str, *, extra_headers: dict[str, str] | None = None
) -> None:
"""Reject a pending block-trade proposal.

``POST /communications/block-trade-proposals/{block_trade_proposal_id}/reject``
— either counterparty can reject; the proposer cancels instead.
Returns 204 with no request body.
"""
self._require_auth()
self._post_void(
f"/communications/block-trade-proposals/"
f"{_seg(block_trade_proposal_id, name='block_trade_proposal_id')}/reject",
extra_headers=extra_headers,
)


class RFQBatchQuotesResource(SyncResource):
"""Sync batch-quote sub-resource — ``client.communications.rfq_batches.quotes``.
Expand Down Expand Up @@ -2161,6 +2192,37 @@ async def accept(
extra_headers=extra_headers,
)

async def cancel(
self, block_trade_proposal_id: str, *, extra_headers: dict[str, str] | None = None
) -> None:
"""Cancel a pending block-trade proposal.

``DELETE /communications/block-trade-proposals/{block_trade_proposal_id}``
— only the proposer can cancel. Returns 204 with no body.
"""
self._require_auth()
await self._delete(
f"/communications/block-trade-proposals/"
f"{_seg(block_trade_proposal_id, name='block_trade_proposal_id')}",
extra_headers=extra_headers,
)

async def reject(
self, block_trade_proposal_id: str, *, extra_headers: dict[str, str] | None = None
) -> None:
"""Reject a pending block-trade proposal.

``POST /communications/block-trade-proposals/{block_trade_proposal_id}/reject``
— either counterparty can reject; the proposer cancels instead.
Returns 204 with no request body.
"""
self._require_auth()
await self._post_void(
f"/communications/block-trade-proposals/"
f"{_seg(block_trade_proposal_id, name='block_trade_proposal_id')}/reject",
extra_headers=extra_headers,
)


class AsyncRFQBatchQuotesResource(AsyncResource):
"""Async batch-quote sub-resource — ``client.communications.rfq_batches.quotes``."""
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "kalshi-sdk"
version = "22.8.0"
version = "22.9.0"
description = "A professional Python SDK for the Kalshi prediction markets and Perps (margin) APIs"
readme = "README.md"
license = { text = "MIT" }
Expand Down
67 changes: 65 additions & 2 deletions specs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2313,6 +2313,36 @@ paths:
'500':
$ref: '#/components/responses/InternalServerError'

/communications/block-trade-proposals/{block_trade_proposal_id}:
delete:
operationId: CancelBlockTradeProposal
summary: Cancel Block Trade Proposal
description: ' Endpoint for cancelling a pending block trade proposal. Only the proposer can cancel it.'
tags:
- communications
security:
- kalshiAccessKey: []
kalshiAccessSignature: []
kalshiAccessTimestamp: []
parameters:
- name: block_trade_proposal_id
in: path
required: true
description: Block trade proposal ID
schema:
type: string
responses:
'204':
description: Block trade proposal cancelled successfully
'400':
$ref: '#/components/responses/BadRequestError'
'401':
$ref: '#/components/responses/UnauthorizedError'
'404':
$ref: '#/components/responses/NotFoundError'
'500':
$ref: '#/components/responses/InternalServerError'

/communications/block-trade-proposals/{block_trade_proposal_id}/accept:
post:
operationId: AcceptBlockTradeProposal
Expand Down Expand Up @@ -2349,6 +2379,38 @@ paths:
'500':
$ref: '#/components/responses/InternalServerError'

/communications/block-trade-proposals/{block_trade_proposal_id}/reject:
post:
operationId: RejectBlockTradeProposal
summary: Reject Block Trade Proposal
description: ' Endpoint for rejecting a pending block trade proposal. Either counterparty can reject it, but not the proposer, which cancels it instead.'
tags:
- communications
security:
- kalshiAccessKey: []
kalshiAccessSignature: []
kalshiAccessTimestamp: []
parameters:
- name: block_trade_proposal_id
in: path
required: true
description: Block trade proposal ID
schema:
type: string
responses:
'204':
description: Block trade proposal rejected successfully
'400':
$ref: '#/components/responses/BadRequestError'
'401':
$ref: '#/components/responses/UnauthorizedError'
'404':
$ref: '#/components/responses/NotFoundError'
'409':
$ref: '#/components/responses/ConflictError'
'500':
$ref: '#/components/responses/InternalServerError'

/communications/rfqs:
get:
operationId: GetRFQs
Expand Down Expand Up @@ -7362,7 +7424,7 @@ components:
description: The event ticker symbol of the market that was settled.
market_result:
type: string
enum: ['yes', 'no', 'scalar']
enum: ['yes', 'no', 'scalar', '']
description: The outcome of the market settlement. 'yes' = market resolved to YES, 'no' = market resolved to NO, 'scalar' = scalar market settled at a specific value.
yes_count_fp:
$ref: '#/components/schemas/FixedPointCount'
Expand Down Expand Up @@ -8850,7 +8912,7 @@ components:
description: Expiration time of the proposal
status:
type: string
description: Current status of the proposal
description: 'Current status of the proposal: pending, executed, cancelled (by the proposer, or on expiry) or rejected (by a counterparty)'
created_ts:
type: string
format: date-time
Expand Down Expand Up @@ -11394,6 +11456,7 @@ components:
market_type:
type: string
enum: [binary, scalar]
x-enumNames: [MarketMarketTypeBinary, MarketMarketTypeScalar]
description: Identifies the type of market
title:
type: string
Expand Down
5 changes: 5 additions & 0 deletions specs/perps_openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3148,6 +3148,7 @@ components:
- underlying_multiplier
- tick_size
- fractional_trading_enabled
- minimum_quantity
- schedule
- exchange_index
- market_version
Expand Down Expand Up @@ -3186,6 +3187,10 @@ components:
fractional_trading_enabled:
type: boolean
description: Whether fractional trading is enabled for this market.
minimum_quantity:
$ref: '#/components/schemas/FixedPointCount'
description: Minimum order quantity in contracts.
example: "1.00"
leverage_estimate:
type: number
format: double
Expand Down
18 changes: 18 additions & 0 deletions tests/_contract_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,16 @@ class Exclusion:
path_template="/communications/block-trade-proposals/{block_trade_proposal_id}/accept",
request_body_schema="#/components/schemas/AcceptBlockTradeProposalRequest",
),
MethodEndpointEntry(
sdk_method="kalshi.resources.communications.BlockTradeProposalsResource.cancel",
http_method="DELETE",
path_template="/communications/block-trade-proposals/{block_trade_proposal_id}",
),
MethodEndpointEntry(
sdk_method="kalshi.resources.communications.BlockTradeProposalsResource.reject",
http_method="POST",
path_template="/communications/block-trade-proposals/{block_trade_proposal_id}/reject",
),
# RFQ batches
MethodEndpointEntry(
sdk_method="kalshi.resources.communications.RFQBatchesResource.list",
Expand Down Expand Up @@ -2104,6 +2114,14 @@ class Exclusion:
),
kind="server_omits_despite_required",
),
("kalshi.perps.models.markets.MarginMarket", "minimum_quantity"): Exclusion(
reason=(
"spec adds minimum_quantity as required; the SDK keeps it optional "
"(None when absent) so payloads/fixtures predating the field still "
"validate and the change ships as a minor release (22.9.0)."
),
kind="server_omits_despite_required",
),
("kalshi.perps.models.portfolio.MarginPosition", "subaccount_type"): Exclusion(
reason=(
"spec 3.35.0 adds subaccount_type as required; the SDK keeps it "
Expand Down
15 changes: 15 additions & 0 deletions tests/perps/test_markets.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,21 @@ def test_halt_sources_optional(self) -> None:
del payload["halt_sources"]
assert MarginMarket.model_validate(payload).halt_sources is None

def test_minimum_quantity_optional(self) -> None:
payload = _market_dict()
payload.pop("minimum_quantity", None)
payload.pop("minimum_quantity_fp", None)
assert MarginMarket.model_validate(payload).minimum_quantity is None

def test_minimum_quantity_parses(self) -> None:
m = MarginMarket.model_validate(_market_dict(minimum_quantity="1.00"))
assert m.minimum_quantity == Decimal("1.00")
assert isinstance(m.minimum_quantity, Decimal)

def test_minimum_quantity_fp_alias(self) -> None:
m = MarginMarket.model_validate(_market_dict(minimum_quantity_fp="2.50"))
assert m.minimum_quantity == Decimal("2.50")

def test_halt_sources_parses(self) -> None:
m = MarginMarket.model_validate(_market_dict(halt_sources=["corporate_action"]))
assert m.halt_sources == ["corporate_action"]
Expand Down
Loading
Loading