From f06345c4f3465618f1ab4f1db9c8bbb222473ba4 Mon Sep 17 00:00:00 2001 From: Jeff West Date: Tue, 3 Nov 2026 09:04:51 -0600 Subject: [PATCH 1/4] chore(spec): sync upstream specs for #567 --- specs/asyncapi.yaml | 4 +++- specs/openapi.yaml | 4 +++- specs/perps_asyncapi.yaml | 5 ++++- specs/perps_openapi.yaml | 15 ++++++++++++++- specs/perps_scm_openapi.yaml | 17 ++++++++++++++++- 5 files changed, 40 insertions(+), 5 deletions(-) diff --git a/specs/asyncapi.yaml b/specs/asyncapi.yaml index 198fbfb..9a23505 100644 --- a/specs/asyncapi.yaml +++ b/specs/asyncapi.yaml @@ -2157,7 +2157,7 @@ components: name: "MVE One" title: "Market 1" yes_sub_title: "YES 1" - no_sub_title: "NO 1" + no_sub_title: "YES 1" rules_primary: "Rule 1" rules_secondary: "Rule 2" can_close_early: true @@ -3978,6 +3978,8 @@ components: type: string no_sub_title: type: string + deprecated: true + description: Deprecated. Use `yes_sub_title` instead. Equal to `yes_sub_title` on every market created since September 2024. rules_primary: type: string rules_secondary: diff --git a/specs/openapi.yaml b/specs/openapi.yaml index 56f4bba..432c33b 100644 --- a/specs/openapi.yaml +++ b/specs/openapi.yaml @@ -11398,7 +11398,9 @@ components: description: Shortened title for the yes side of this market no_sub_title: type: string - description: Shortened title for the no side of this market + deprecated: true + description: | + Deprecated. Use `yes_sub_title` instead. Equal to `yes_sub_title` on every market created since September 2024. created_time: type: string format: date-time diff --git a/specs/perps_asyncapi.yaml b/specs/perps_asyncapi.yaml index c1995a2..aa2a05f 100644 --- a/specs/perps_asyncapi.yaml +++ b/specs/perps_asyncapi.yaml @@ -1346,7 +1346,7 @@ components: $ref: '#/components/schemas/sequenceNumber' msg: type: object - required: ["trade_id", "market_ticker", "price", "count", "taker_side", "ts_ms"] + required: ["trade_id", "market_ticker", "price", "count", "taker_side", "is_block_trade", "ts_ms"] properties: trade_id: type: string @@ -1363,6 +1363,9 @@ components: x-rust-serialize-with: ws::dto::serde_money::serialize_amount_fixed_point_count taker_side: $ref: '#/components/schemas/bookSide' + is_block_trade: + type: boolean + description: True if the trade was matched off book as a block trade ts_ms: type: integer format: int64 diff --git a/specs/perps_openapi.yaml b/specs/perps_openapi.yaml index cd57d23..37f7c35 100644 --- a/specs/perps_openapi.yaml +++ b/specs/perps_openapi.yaml @@ -953,7 +953,7 @@ paths: get: operationId: GetMarginTrades summary: Get Trades - description: 'Endpoint for retrieving public margin trades across all markets or filtered by market ticker. Returns a paginated response. Use the cursor value from the previous response to get the next page.' + description: 'Endpoint for retrieving public margin trades across all markets or filtered by market ticker. Block trades are included in the response by default and identified by the `is_block_trade` field; use the `is_block_trade` query parameter to filter by block / non-block. Returns a paginated response. Use the cursor value from the previous response to get the next page.' tags: - market parameters: @@ -995,6 +995,7 @@ paths: type: integer format: int64 description: Filter trades before this Unix timestamp + - $ref: '#/components/parameters/IsBlockTradeQuery' responses: '200': description: Trades retrieved successfully @@ -3184,6 +3185,7 @@ components: $ref: '#/components/schemas/MarginMarketStatus' fractional_trading_enabled: type: boolean + description: Whether fractional trading is enabled for this market. leverage_estimate: type: number format: double @@ -3564,6 +3566,7 @@ components: - price - created_time - taker_side + - is_block_trade properties: trade_id: type: string @@ -3584,6 +3587,9 @@ components: taker_side: $ref: '#/components/schemas/BookSide' description: Side of the taker in this trade + is_block_trade: + type: boolean + description: True if this trade was matched off-book as a block trade (e.g. via RFQ / negotiated block proposal); false for trades that filled on the standard order book. BookSide: type: string enum: ['bid', 'ask'] @@ -4213,6 +4219,13 @@ components: schema: type: string x-go-type-skip-optional-pointer: true + IsBlockTradeQuery: + name: is_block_trade + in: query + description: | + Filter trades by whether they are block trades. Omit to return all trades. Set to `true` to return only block trades. Set to `false` to return only non-block trades. + schema: + type: boolean LimitQuery: name: limit in: query diff --git a/specs/perps_scm_openapi.yaml b/specs/perps_scm_openapi.yaml index 141c4de..cecab03 100644 --- a/specs/perps_scm_openapi.yaml +++ b/specs/perps_scm_openapi.yaml @@ -730,24 +730,39 @@ components: type: string enum: - trade_audit + - trade_audit_fp - position_transfer_audit + - position_transfer_audit_fp - position_snapshot + - position_snapshot_fp - market_price_snapshot - funding_periods + - funding_periods_fp - settlement_periods + - settlement_periods_fp - corporate_action_payments + - corporate_action_payments_fp - maintenance_margin - maintenance_margin_aggregate - cash_balance_daily + - cash_balance_daily_fp - cash_activity_daily + - cash_activity_daily_fp - daily_financial_summary + - daily_financial_summary_fp - interest_on_collateral_monthly + - interest_on_collateral_monthly_fp - daily_statement description: >- Report file type. Every type is a CSV except `daily_statement`, the end-of-day PDF statement that lays out the same date's position, trade, settlement, funding, and cash files with an - account summary. + account summary. The `_fp` variants are companion CSV files with + the same columns as the original report, selected monetary amounts + formatted to 6 decimal places, and contract quantities to 2 decimal + places. Prices, fees, and margin requirements retain their existing + precision. Companion filenames append `_fp` before `.csv`, after + the date, timestamp, or `intraday` suffix. url: { type: string, description: Presigned download URL (omitted from logs). } date: type: string From b364750a04b4dc0e0d9c9c946e79120890928de1 Mon Sep 17 00:00:00 2001 From: Jeff West Date: Tue, 3 Nov 2026 09:15:12 -0600 Subject: [PATCH 2/4] wip(#567): is_block_trade param and fields --- kalshi/perps/klear/models/margin.py | 10 +++++++++ kalshi/perps/models/portfolio.py | 4 ++++ kalshi/perps/resources/portfolio.py | 32 ++++++++++++++++++++++++----- kalshi/perps/ws/models/trade.py | 11 +++++++--- 4 files changed, 49 insertions(+), 8 deletions(-) diff --git a/kalshi/perps/klear/models/margin.py b/kalshi/perps/klear/models/margin.py index 9143243..8cf225c 100644 --- a/kalshi/perps/klear/models/margin.py +++ b/kalshi/perps/klear/models/margin.py @@ -75,12 +75,22 @@ def _require_positive_withdrawal(value: Decimal) -> Decimal: # the ``PaymentStatusLiteral`` style in kalshi/models/portfolio.py). MarginReportTypeLiteral = Literal[ "trade_audit", + "trade_audit_fp", + "position_transfer_audit_fp", "position_snapshot", + "position_snapshot_fp", "market_price_snapshot", "funding_periods", + "funding_periods_fp", "settlement_periods", + "settlement_periods_fp", + "corporate_action_payments_fp", "maintenance_margin", "maintenance_margin_aggregate", + "cash_balance_daily_fp", + "cash_activity_daily_fp", + "daily_financial_summary_fp", + "interest_on_collateral_monthly_fp", ] """Spec ``MarginReport.report_type`` — the kind of margin report.""" diff --git a/kalshi/perps/models/portfolio.py b/kalshi/perps/models/portfolio.py index d4bcf6a..a27fb91 100644 --- a/kalshi/perps/models/portfolio.py +++ b/kalshi/perps/models/portfolio.py @@ -128,6 +128,10 @@ class MarginTrade(BaseModel): price: DollarDecimal created_time: AwareDatetime taker_side: BookSideLiteral + # Spec marks is_block_trade required. Kept optional (None when the + # key is absent) so fixtures and payloads from before the field + # still validate. See PERPS_EXCLUSIONS. + is_block_trade: bool | None = None model_config = {"extra": "allow"} diff --git a/kalshi/perps/resources/portfolio.py b/kalshi/perps/resources/portfolio.py index e28dc8a..9cc22ab 100644 --- a/kalshi/perps/resources/portfolio.py +++ b/kalshi/perps/resources/portfolio.py @@ -38,6 +38,7 @@ from kalshi.resources._base import ( AsyncResource, SyncResource, + _bool_param, _params, _seg, _validate_limit, @@ -71,11 +72,15 @@ def _margin_trades_params( max_ts: int | None, limit: int | None, cursor: str | None, + is_block_trade: bool | None, ) -> dict[str, Any]: """Build query params for ``GET /margin/trades``. ``ticker`` is optional — omit (``None``) to retrieve trades across all - margin markets. ``_params`` drops ``None`` so the query key is not sent. + margin markets. ``is_block_trade`` is optional — omit for all trades, + ``True`` for block trades only, ``False`` for non-block only (serialized + as ``"true"`` / ``"false"``; ``None`` is dropped). ``_params`` drops + ``None`` so omitted keys are not sent. """ limit = _validate_limit(limit, hi=1000) return _params( @@ -84,6 +89,7 @@ def _margin_trades_params( max_ts=max_ts, limit=limit, cursor=cursor, + is_block_trade=_bool_param(is_block_trade), ) @@ -161,12 +167,15 @@ def trades( max_ts: int | None = None, limit: int | None = None, cursor: str | None = None, + is_block_trade: bool | None = None, extra_headers: dict[str, str] | None = None, ) -> Page[MarginTrade]: """Public margin trades (no auth required). ``ticker`` filters to one market. Omit to retrieve trades across all - margin markets. + margin markets. ``is_block_trade`` filters by block / non-block: + omit for all trades, ``True`` for block trades only, ``False`` for + non-block only. """ params = _margin_trades_params( ticker=ticker, @@ -174,6 +183,7 @@ def trades( max_ts=max_ts, limit=limit, cursor=cursor, + is_block_trade=is_block_trade, ) return self._list( "/margin/trades", MarginTrade, "trades", params=params, extra_headers=extra_headers @@ -186,13 +196,16 @@ def trades_all( min_ts: int | None = None, max_ts: int | None = None, limit: int | None = None, + is_block_trade: bool | None = None, max_pages: int | None = None, extra_headers: dict[str, str] | None = None, ) -> Iterator[MarginTrade]: """Auto-paginate public margin trades (no auth required). ``ticker`` filters to one market. Omit to retrieve trades across all - margin markets. + margin markets. ``is_block_trade`` filters by block / non-block: + omit for all trades, ``True`` for block trades only, ``False`` for + non-block only. """ _validate_max_pages(max_pages) params = _margin_trades_params( @@ -201,6 +214,7 @@ def trades_all( max_ts=max_ts, limit=limit, cursor=None, + is_block_trade=is_block_trade, ) return self._list_all( "/margin/trades", @@ -438,12 +452,15 @@ async def trades( max_ts: int | None = None, limit: int | None = None, cursor: str | None = None, + is_block_trade: bool | None = None, extra_headers: dict[str, str] | None = None, ) -> Page[MarginTrade]: """Public margin trades (no auth required). ``ticker`` filters to one market. Omit to retrieve trades across all - margin markets. + margin markets. ``is_block_trade`` filters by block / non-block: + omit for all trades, ``True`` for block trades only, ``False`` for + non-block only. """ params = _margin_trades_params( ticker=ticker, @@ -451,6 +468,7 @@ async def trades( max_ts=max_ts, limit=limit, cursor=cursor, + is_block_trade=is_block_trade, ) return await self._list( "/margin/trades", MarginTrade, "trades", params=params, extra_headers=extra_headers @@ -463,13 +481,16 @@ def trades_all( min_ts: int | None = None, max_ts: int | None = None, limit: int | None = None, + is_block_trade: bool | None = None, max_pages: int | None = None, extra_headers: dict[str, str] | None = None, ) -> AsyncIterator[MarginTrade]: """Returns an async iterator over public margin trades — use ``async for`` (no auth). ``ticker`` filters to one market. Omit to retrieve trades across all - margin markets. + margin markets. ``is_block_trade`` filters by block / non-block: + omit for all trades, ``True`` for block trades only, ``False`` for + non-block only. """ _validate_max_pages(max_pages) params = _margin_trades_params( @@ -478,6 +499,7 @@ def trades_all( max_ts=max_ts, limit=limit, cursor=None, + is_block_trade=is_block_trade, ) return self._list_all( "/margin/trades", diff --git a/kalshi/perps/ws/models/trade.py b/kalshi/perps/ws/models/trade.py index 6525df3..f2eb1e2 100644 --- a/kalshi/perps/ws/models/trade.py +++ b/kalshi/perps/ws/models/trade.py @@ -17,9 +17,10 @@ class MarginTradePayload(BaseModel): """Payload for ``trade`` messages (``marginTradePayload.msg``). - All fields required per spec. ``trade_id`` is a UUID string; ``price`` is a - dollar-decimal string; ``count`` is a fixed-point count string; - ``taker_side`` is ``bid``/``ask``; ``ts_ms`` is epoch milliseconds. + ``trade_id`` is a UUID string; ``price`` is a dollar-decimal string; + ``count`` is a fixed-point count string; ``taker_side`` is ``bid``/``ask``; + ``ts_ms`` is epoch milliseconds. Spec marks ``is_block_trade`` required; + the SDK keeps it optional so payloads omitting the key still parse. """ trade_id: str @@ -28,6 +29,10 @@ class MarginTradePayload(BaseModel): count: FixedPointCount taker_side: PerpsBookSide ts_ms: int + # Spec marks is_block_trade required. Kept optional (None when the + # key is absent) so fixtures and payloads from before the field + # still validate. See PERPS_EXCLUSIONS. + is_block_trade: bool | None = None model_config = {"extra": "allow", "populate_by_name": True} From 0b19ce229551f31e54bacebcc0792edf26154c0b Mon Sep 17 00:00:00 2001 From: Jeff West Date: Tue, 3 Nov 2026 09:25:24 -0600 Subject: [PATCH 3/4] wip(#567): tests, docs, version --- CHANGELOG.md | 22 ++++++ docs/perps.md | 6 +- kalshi/__init__.py | 2 +- pyproject.toml | 2 +- tests/_contract_support.py | 16 +++++ tests/perps/klear/test_margin.py | 10 +++ tests/perps/test_portfolio.py | 96 +++++++++++++++++++++++++- tests/perps/ws/test_perps_ws_models.py | 39 +++++++---- 8 files changed, 176 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 213b058..3e9905b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,28 @@ All notable changes to kalshi-sdk will be documented in this file. +## 22.7.0 — 2026-11-03 + +Reconciles nightly spec drift (#567) against the latest perps OpenAPI / +AsyncAPI and Klear (SCM) specs. Additive only. No breaking changes. + +### Added + +- **`is_block_trade`** query param on perps `portfolio.trades` / + `trades_all` (sync and async). Omit for all trades; `True` filters to + block trades; `False` filters to non-block (`"false"` is sent, not + dropped). (Closes #567). +- Optional **`is_block_trade`** (`bool | None`) on `MarginTrade` and + `MarginTradePayload`. The spec marks it required; the SDK keeps it + optional (contract exclusion `server_omits_despite_required`) so + payloads omitting the key still parse. +- Klear `MarginReport.report_type` gains the companion `*_fp` variants + (`trade_audit_fp`, `position_transfer_audit_fp`, `position_snapshot_fp`, + `funding_periods_fp`, `settlement_periods_fp`, + `corporate_action_payments_fp`, `cash_balance_daily_fp`, + `cash_activity_daily_fp`, `daily_financial_summary_fp`, + `interest_on_collateral_monthly_fp`). + ## 22.6.0 — 2026-10-31 Reconciles nightly spec drift (#563) against the latest Klear (SCM) spec diff --git a/docs/perps.md b/docs/perps.md index 0db482d..a5b044c 100644 --- a/docs/perps.md +++ b/docs/perps.md @@ -107,7 +107,11 @@ key is not sent. rows across all subaccounts (subaccount-restricted API keys still only see their locked subaccount). Pass `subaccount=0` for the primary. `portfolio.trades()` / `trades_all()` take an optional `ticker`; omit it -to retrieve public trades across all margin markets. +to retrieve public trades across all margin markets. Optional +`is_block_trade` filters by block / non-block: omit for all trades, +`True` for block trades only, `False` for non-block only (`"false"` is +sent, not dropped). Each `MarginTrade` also carries optional +`is_block_trade` (`bool | None`; `None` when the server omits the key). !!! warning "Deprecated in v7.2.0 — `list_fcm` / `list_all_fcm`" Kalshi removed `GET /margin/fcm/orders` from the perps OpenAPI. The SDK diff --git a/kalshi/__init__.py b/kalshi/__init__.py index 92ecadc..80e15d1 100644 --- a/kalshi/__init__.py +++ b/kalshi/__init__.py @@ -503,4 +503,4 @@ "Withdrawal", ] -__version__ = "22.6.0" +__version__ = "22.7.0" diff --git a/pyproject.toml b/pyproject.toml index ffa46a6..496cb40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kalshi-sdk" -version = "22.6.0" +version = "22.7.0" description = "A professional Python SDK for the Kalshi prediction markets and Perps (margin) APIs" readme = "README.md" license = { text = "MIT" } diff --git a/tests/_contract_support.py b/tests/_contract_support.py index e167607..5a14f31 100644 --- a/tests/_contract_support.py +++ b/tests/_contract_support.py @@ -2104,6 +2104,22 @@ class Exclusion: ), kind="server_omits_despite_required", ), + ("kalshi.perps.models.portfolio.MarginTrade", "is_block_trade"): Exclusion( + reason=( + "spec marks is_block_trade required; the SDK keeps it optional " + "(None when absent) so payloads and fixtures predating the field " + "still validate and the change ships as a minor release (22.7.0)." + ), + kind="server_omits_despite_required", + ), + ("kalshi.perps.ws.models.trade.MarginTradePayload", "is_block_trade"): Exclusion( + reason=( + "AsyncAPI marks is_block_trade required; the SDK keeps it optional " + "(None when absent) so payloads and fixtures predating the field " + "still validate and the change ships as a minor release (22.7.0)." + ), + kind="server_omits_despite_required", + ), ( "kalshi.perps.models.margin_account.MarginSubaccountBalance", "subaccount_type", diff --git a/tests/perps/klear/test_margin.py b/tests/perps/klear/test_margin.py index 03d2f86..824f43e 100644 --- a/tests/perps/klear/test_margin.py +++ b/tests/perps/klear/test_margin.py @@ -56,12 +56,22 @@ ALL_REPORT_TYPES = [ "trade_audit", + "trade_audit_fp", + "position_transfer_audit_fp", "position_snapshot", + "position_snapshot_fp", "market_price_snapshot", "funding_periods", + "funding_periods_fp", "settlement_periods", + "settlement_periods_fp", + "corporate_action_payments_fp", "maintenance_margin", "maintenance_margin_aggregate", + "cash_balance_daily_fp", + "cash_activity_daily_fp", + "daily_financial_summary_fp", + "interest_on_collateral_monthly_fp", ] diff --git a/tests/perps/test_portfolio.py b/tests/perps/test_portfolio.py index 6603e75..2e4a4e7 100644 --- a/tests/perps/test_portfolio.py +++ b/tests/perps/test_portfolio.py @@ -433,7 +433,7 @@ def test_optional_params_dropped(self, perps_client: PerpsClient) -> None: perps_client.portfolio.trades(ticker="BTC-PERP") params = route.calls.last.request.url.params assert params["ticker"] == "BTC-PERP" - for key in ("min_ts", "max_ts", "limit", "cursor"): + for key in ("min_ts", "max_ts", "limit", "cursor", "is_block_trade"): assert key not in params def test_limit_out_of_range_raises(self, perps_client: PerpsClient) -> None: @@ -509,6 +509,100 @@ async def test_async_trades_all_paginates( assert len(items) == 2 await async_perps_client.close() + @respx.mock + def test_is_block_trade_filter_serializes(self, perps_client: PerpsClient) -> None: + """``False`` must be sent explicitly (not dropped) so callers can + request only non-block trades.""" + route = respx.get(f"{BASE}/margin/trades").mock( + return_value=httpx.Response(200, json={"trades": [_trade()], "cursor": ""}) + ) + perps_client.portfolio.trades(is_block_trade=True) + assert route.calls[0].request.url.params["is_block_trade"] == "true" + perps_client.portfolio.trades(is_block_trade=False) + assert route.calls[1].request.url.params["is_block_trade"] == "false" + perps_client.portfolio.trades() + assert "is_block_trade" not in route.calls[2].request.url.params + + @respx.mock + def test_trades_all_is_block_trade_filter_serializes( + self, perps_client: PerpsClient + ) -> None: + route = respx.get(f"{BASE}/margin/trades").mock( + return_value=httpx.Response(200, json={"trades": [_trade()], "cursor": ""}) + ) + list(perps_client.portfolio.trades_all(is_block_trade=True)) + assert route.calls[0].request.url.params["is_block_trade"] == "true" + list(perps_client.portfolio.trades_all(is_block_trade=False)) + assert route.calls[1].request.url.params["is_block_trade"] == "false" + list(perps_client.portfolio.trades_all()) + assert "is_block_trade" not in route.calls[2].request.url.params + + @respx.mock + async def test_async_is_block_trade_filter_serializes( + self, async_perps_client: AsyncPerpsClient + ) -> None: + route = respx.get(f"{BASE}/margin/trades").mock( + return_value=httpx.Response(200, json={"trades": [_trade()], "cursor": ""}) + ) + await async_perps_client.portfolio.trades(is_block_trade=True) + assert route.calls[0].request.url.params["is_block_trade"] == "true" + await async_perps_client.portfolio.trades(is_block_trade=False) + assert route.calls[1].request.url.params["is_block_trade"] == "false" + await async_perps_client.portfolio.trades() + assert "is_block_trade" not in route.calls[2].request.url.params + await async_perps_client.close() + + @respx.mock + async def test_async_trades_all_is_block_trade_filter_serializes( + self, async_perps_client: AsyncPerpsClient + ) -> None: + route = respx.get(f"{BASE}/margin/trades").mock( + return_value=httpx.Response(200, json={"trades": [_trade()], "cursor": ""}) + ) + items = [ + t async for t in async_perps_client.portfolio.trades_all(is_block_trade=True) + ] + assert len(items) == 1 + assert route.calls[0].request.url.params["is_block_trade"] == "true" + items = [ + t async for t in async_perps_client.portfolio.trades_all(is_block_trade=False) + ] + assert len(items) == 1 + assert route.calls[1].request.url.params["is_block_trade"] == "false" + items = [t async for t in async_perps_client.portfolio.trades_all()] + assert len(items) == 1 + assert "is_block_trade" not in route.calls[2].request.url.params + await async_perps_client.close() + + @respx.mock + def test_is_block_trade_server_4xx_raises(self, perps_client: PerpsClient) -> None: + respx.get(f"{BASE}/margin/trades").mock( + return_value=httpx.Response(400, json={"error": {"code": "bad_request"}}) + ) + with pytest.raises(KalshiValidationError): + perps_client.portfolio.trades(is_block_trade=True) + + @respx.mock + async def test_async_is_block_trade_server_4xx_raises( + self, async_perps_client: AsyncPerpsClient + ) -> None: + respx.get(f"{BASE}/margin/trades").mock( + return_value=httpx.Response(400, json={"error": {"code": "bad_request"}}) + ) + with pytest.raises(KalshiValidationError): + await async_perps_client.portfolio.trades(is_block_trade=False) + await async_perps_client.close() + + def test_parses_is_block_trade(self) -> None: + payload = _trade() + payload["is_block_trade"] = True + assert MarginTrade.model_validate(payload).is_block_trade is True + payload["is_block_trade"] = False + assert MarginTrade.model_validate(payload).is_block_trade is False + + def test_parses_without_is_block_trade(self) -> None: + assert MarginTrade.model_validate(_trade()).is_block_trade is None + def _exit_trigger(**overrides: object) -> dict[str, object]: base: dict[str, object] = { diff --git a/tests/perps/ws/test_perps_ws_models.py b/tests/perps/ws/test_perps_ws_models.py index 4e54811..001b716 100644 --- a/tests/perps/ws/test_perps_ws_models.py +++ b/tests/perps/ws/test_perps_ws_models.py @@ -420,26 +420,39 @@ def test_funding_rate_no_float_drift(self) -> None: class TestTrade: - def test_happy(self) -> None: - frame = { - "type": "trade", - "sid": 3, - "msg": { - "trade_id": "11111111-1111-1111-1111-111111111111", - "market_ticker": "BTC-PERP", - "price": "100.0000", - "count": "5.00", - "taker_side": "ask", - "ts_ms": 1700000000000, - }, + def _frame(self, **msg_overrides: Any) -> dict[str, Any]: + msg: dict[str, Any] = { + "trade_id": "11111111-1111-1111-1111-111111111111", + "market_ticker": "BTC-PERP", + "price": "100.0000", + "count": "5.00", + "taker_side": "ask", + "ts_ms": 1700000000000, } - msg = MarginTradeMessage.model_validate(frame) + msg.update(msg_overrides) + return {"type": "trade", "sid": 3, "msg": msg} + + def test_happy(self) -> None: + msg = MarginTradeMessage.model_validate(self._frame()) assert msg.msg.taker_side == "ask" assert msg.msg.count == Decimal("5.00") assert isinstance(msg.msg.count, Decimal) assert msg.msg.ts_ms == 1700000000000 assert isinstance(msg.msg.ts_ms, int) assert msg.seq is None + assert msg.msg.is_block_trade is None + + def test_parses_is_block_trade(self) -> None: + msg = MarginTradeMessage.model_validate(self._frame(is_block_trade=True)) + assert msg.msg.is_block_trade is True + payload = MarginTradePayload.model_validate( + self._frame(is_block_trade=False)["msg"] + ) + assert payload.is_block_trade is False + + def test_parses_without_is_block_trade(self) -> None: + payload = MarginTradePayload.model_validate(self._frame()["msg"]) + assert payload.is_block_trade is None def test_rejects_equities_yes_taker_side(self) -> None: frame = { From d40fe8a18b084adfbb6be11bcee5642a3516a69e Mon Sep 17 00:00:00 2001 From: Jeff West Date: Tue, 3 Nov 2026 09:48:46 -0600 Subject: [PATCH 4/4] wip(#567): full report_type enum --- CHANGELOG.md | 5 ++++- kalshi/perps/klear/models/margin.py | 7 +++++++ tests/perps/klear/test_margin.py | 7 +++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e9905b..55a70e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,10 @@ AsyncAPI and Klear (SCM) specs. Additive only. No breaking changes. `MarginTradePayload`. The spec marks it required; the SDK keeps it optional (contract exclusion `server_omits_despite_required`) so payloads omitting the key still parse. -- Klear `MarginReport.report_type` gains the companion `*_fp` variants +- Klear `MarginReport.report_type` now accepts every spec report type: the + previously missing `position_transfer_audit`, `corporate_action_payments`, + `cash_balance_daily`, `cash_activity_daily`, `daily_financial_summary`, + `interest_on_collateral_monthly`, `daily_statement`, and the companion `*_fp` variants (`trade_audit_fp`, `position_transfer_audit_fp`, `position_snapshot_fp`, `funding_periods_fp`, `settlement_periods_fp`, `corporate_action_payments_fp`, `cash_balance_daily_fp`, diff --git a/kalshi/perps/klear/models/margin.py b/kalshi/perps/klear/models/margin.py index 8cf225c..16063a4 100644 --- a/kalshi/perps/klear/models/margin.py +++ b/kalshi/perps/klear/models/margin.py @@ -76,6 +76,7 @@ def _require_positive_withdrawal(value: Decimal) -> Decimal: MarginReportTypeLiteral = Literal[ "trade_audit", "trade_audit_fp", + "position_transfer_audit", "position_transfer_audit_fp", "position_snapshot", "position_snapshot_fp", @@ -84,13 +85,19 @@ def _require_positive_withdrawal(value: Decimal) -> Decimal: "funding_periods_fp", "settlement_periods", "settlement_periods_fp", + "corporate_action_payments", "corporate_action_payments_fp", "maintenance_margin", "maintenance_margin_aggregate", + "cash_balance_daily", "cash_balance_daily_fp", + "cash_activity_daily", "cash_activity_daily_fp", + "daily_financial_summary", "daily_financial_summary_fp", + "interest_on_collateral_monthly", "interest_on_collateral_monthly_fp", + "daily_statement", ] """Spec ``MarginReport.report_type`` — the kind of margin report.""" diff --git a/tests/perps/klear/test_margin.py b/tests/perps/klear/test_margin.py index 824f43e..3820eeb 100644 --- a/tests/perps/klear/test_margin.py +++ b/tests/perps/klear/test_margin.py @@ -57,6 +57,7 @@ ALL_REPORT_TYPES = [ "trade_audit", "trade_audit_fp", + "position_transfer_audit", "position_transfer_audit_fp", "position_snapshot", "position_snapshot_fp", @@ -65,13 +66,19 @@ "funding_periods_fp", "settlement_periods", "settlement_periods_fp", + "corporate_action_payments", "corporate_action_payments_fp", "maintenance_margin", "maintenance_margin_aggregate", + "cash_balance_daily", "cash_balance_daily_fp", + "cash_activity_daily", "cash_activity_daily_fp", + "daily_financial_summary", "daily_financial_summary_fp", + "interest_on_collateral_monthly", "interest_on_collateral_monthly_fp", + "daily_statement", ]