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
27 changes: 10 additions & 17 deletions kalshi/ws/models/communications.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Communications channel message models (RFQ and quote notifications)."""
from __future__ import annotations

from datetime import datetime

from pydantic import AliasChoices, BaseModel, Field

from kalshi.types import DollarDecimal, FixedPointCount
Expand All @@ -12,25 +14,17 @@ class RfqCreatedPayload(BaseModel):
Wire format per AsyncAPI spec: ``created_ts`` is an RFC3339 date-time
string; ``target_cost_dollars`` is a dollar string; ``contracts_fp`` is a
2-decimal fixed-point count string.

TODO(v0.15.1): ``created_ts`` is spec-aligned but lacks live capture.
The v0.14.0 ``user_orders`` capture showed demo emitting
``created_ts_ms`` as integer milliseconds instead of the spec's ISO
string. If Communications follows the same pattern, ``created_ts: str``
will reject frames that previously parsed as ``int``. Capture a live
frame on demo when the channel is active and confirm or adjust.
``extra="allow"`` provides a soft landing for unexpected extras.
"""

id: str
creator_id: str
market_ticker: str
created_ts: str
created_ts: datetime
event_ticker: str | None = None
contracts: str | None = Field(
contracts: FixedPointCount | None = Field(
default=None,
validation_alias=AliasChoices("contracts_fp", "contracts"),
) # _fp format
)
target_cost: DollarDecimal | None = Field(
default=None,
validation_alias=AliasChoices("target_cost_dollars", "target_cost"),
Expand All @@ -48,13 +42,12 @@ class RfqDeletedPayload(BaseModel):
"""RFQ deleted notification payload.

``deleted_ts`` is an RFC3339 date-time string per AsyncAPI spec.
Same ``created_ts_ms``-precedent caveat as :class:`RfqCreatedPayload`.
"""

id: str
creator_id: str
market_ticker: str
deleted_ts: str
deleted_ts: datetime

# v0.14+ backfill (#162). Same RFQ context as RfqCreatedPayload —
# surfaced again on delete for clients that subscribed mid-RFQ.
Expand Down Expand Up @@ -83,7 +76,7 @@ class QuoteCreatedPayload(BaseModel):
no_bid: DollarDecimal = Field(
validation_alias=AliasChoices("no_bid_dollars", "no_bid"),
)
created_ts: str
created_ts: datetime

# v0.14+ backfill (#162). Event linkage + RFQ offer/cost context echoed
# on the quote so subscribers don't need to look up the parent RFQ.
Expand Down Expand Up @@ -117,10 +110,10 @@ class QuoteAcceptedPayload(BaseModel):
validation_alias=AliasChoices("no_bid_dollars", "no_bid"),
)
accepted_side: str | None = None
contracts_accepted: str | None = Field(
contracts_accepted: FixedPointCount | None = Field(
default=None,
validation_alias=AliasChoices("contracts_accepted_fp", "contracts_accepted"),
) # _fp format
)

# v0.14+ backfill (#162). Mirrors QuoteCreatedPayload — same RFQ context
# echoed on accept.
Expand Down Expand Up @@ -150,7 +143,7 @@ class QuoteExecutedPayload(BaseModel):
order_id: str
client_order_id: str
market_ticker: str
executed_ts: str
executed_ts: datetime
model_config = {"extra": "allow"}


Expand Down
10 changes: 5 additions & 5 deletions kalshi/ws/models/fill.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from pydantic import AliasChoices, BaseModel, Field

from kalshi.models.orders import BookSideLiteral, SideLiteral
from kalshi.types import DollarDecimal
from kalshi.types import DollarDecimal, FixedPointCount


class FillPayload(BaseModel):
Expand All @@ -24,15 +24,15 @@ class FillPayload(BaseModel):
yes_price: DollarDecimal = Field(
validation_alias=AliasChoices("yes_price_dollars", "yes_price"),
)
count: str = Field(
count: FixedPointCount = Field(
validation_alias=AliasChoices("count_fp", "count"),
) # _fp format
)
fee_cost: DollarDecimal
action: str # buy/sell
ts: int
post_position: str = Field(
post_position: FixedPointCount = Field(
validation_alias=AliasChoices("post_position_fp", "post_position"),
) # _fp format
)
purchased_side: str
client_order_id: str | None = None
subaccount: int | None = None
Expand Down
10 changes: 5 additions & 5 deletions kalshi/ws/models/market_positions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from pydantic import AliasChoices, BaseModel, Field

from kalshi.types import DollarDecimal
from kalshi.types import DollarDecimal, FixedPointCount


class MarketPositionsPayload(BaseModel):
Expand All @@ -15,9 +15,9 @@ class MarketPositionsPayload(BaseModel):

user_id: str
market_ticker: str
position: str = Field(
position: FixedPointCount = Field(
validation_alias=AliasChoices("position_fp", "position"),
) # _fp format
)
position_cost: DollarDecimal = Field(
validation_alias=AliasChoices("position_cost_dollars", "position_cost"),
)
Expand All @@ -30,9 +30,9 @@ class MarketPositionsPayload(BaseModel):
position_fee_cost: DollarDecimal = Field(
validation_alias=AliasChoices("position_fee_cost_dollars", "position_fee_cost"),
)
volume: str = Field(
volume: FixedPointCount = Field(
validation_alias=AliasChoices("volume_fp", "volume"),
) # _fp format
)
subaccount: int | None = None
model_config = {"extra": "allow"}

Expand Down
4 changes: 2 additions & 2 deletions kalshi/ws/models/orderbook_delta.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ class OrderbookSnapshotPayload(BaseModel):

market_ticker: str
market_id: str
yes: list[tuple[str, str]] = Field(
yes: list[tuple[DollarDecimal, FixedPointCount]] = Field(
default=[],
validation_alias=AliasChoices("yes_dollars_fp", "yes"),
)
no: list[tuple[str, str]] = Field(
no: list[tuple[DollarDecimal, FixedPointCount]] = Field(
default=[],
validation_alias=AliasChoices("no_dollars_fp", "no"),
)
Expand Down
22 changes: 11 additions & 11 deletions kalshi/ws/models/ticker.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from pydantic import AliasChoices, BaseModel, Field

from kalshi.types import DollarDecimal
from kalshi.types import DollarDecimal, FixedPointCount


class TickerPayload(BaseModel):
Expand All @@ -30,23 +30,23 @@ class TickerPayload(BaseModel):
default=None,
validation_alias=AliasChoices("no_ask_dollars", "no_ask"),
)
volume: str = Field(
volume: FixedPointCount = Field(
validation_alias=AliasChoices("volume_fp", "volume"),
) # _fp format
open_interest: str = Field(
)
open_interest: FixedPointCount = Field(
validation_alias=AliasChoices("open_interest_fp", "open_interest"),
) # _fp format
)
dollar_volume: str
dollar_open_interest: str
yes_bid_size: str = Field(
yes_bid_size: FixedPointCount = Field(
validation_alias=AliasChoices("yes_bid_size_fp", "yes_bid_size"),
) # _fp format
yes_ask_size: str = Field(
)
yes_ask_size: FixedPointCount = Field(
validation_alias=AliasChoices("yes_ask_size_fp", "yes_ask_size"),
) # _fp format
last_trade_size: str = Field(
)
last_trade_size: FixedPointCount = Field(
validation_alias=AliasChoices("last_trade_size_fp", "last_trade_size"),
) # _fp format
)
ts: int
# v0.14+ backfill (#162). Spec promotes ts_ms (Unix ms) as the primary
# timestamp; ts (seconds) stays for compat. Do NOT auto-convert.
Expand Down
6 changes: 3 additions & 3 deletions kalshi/ws/models/trade.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from pydantic import AliasChoices, BaseModel, Field

from kalshi.models.orders import BookSideLiteral, SideLiteral
from kalshi.types import DollarDecimal
from kalshi.types import DollarDecimal, FixedPointCount


class TradePayload(BaseModel):
Expand All @@ -23,9 +23,9 @@ class TradePayload(BaseModel):
no_price: DollarDecimal = Field(
validation_alias=AliasChoices("no_price_dollars", "no_price"),
)
count: str = Field(
count: FixedPointCount = Field(
validation_alias=AliasChoices("count_fp", "count"),
) # _fp format
)
taker_side: str
ts: int
# v0.14+ backfill (#162). taker_outcome_side/taker_book_side are the
Expand Down
22 changes: 12 additions & 10 deletions kalshi/ws/models/user_orders.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
"""User orders channel message models."""
from __future__ import annotations

from datetime import datetime

from pydantic import AliasChoices, BaseModel, Field

from kalshi.models.orders import (
BookSideLiteral,
SelfTradePreventionTypeLiteral,
SideLiteral,
)
from kalshi.types import DollarDecimal
from kalshi.types import DollarDecimal, FixedPointCount


class UserOrdersPayload(BaseModel):
Expand All @@ -31,15 +33,15 @@ class UserOrdersPayload(BaseModel):
yes_price: DollarDecimal = Field(
validation_alias=AliasChoices("yes_price_dollars", "yes_price"),
)
fill_count: str = Field(
fill_count: FixedPointCount = Field(
validation_alias=AliasChoices("fill_count_fp", "fill_count"),
) # _fp format
remaining_count: str = Field(
)
remaining_count: FixedPointCount = Field(
validation_alias=AliasChoices("remaining_count_fp", "remaining_count"),
) # _fp format
initial_count: str = Field(
)
initial_count: FixedPointCount = Field(
validation_alias=AliasChoices("initial_count_fp", "initial_count"),
) # _fp format
)
taker_fill_cost: DollarDecimal = Field(
validation_alias=AliasChoices("taker_fill_cost_dollars", "taker_fill_cost"),
)
Expand All @@ -53,10 +55,10 @@ class UserOrdersPayload(BaseModel):
validation_alias=AliasChoices("maker_fees_dollars", "maker_fees"),
)
client_order_id: str
created_time: str
created_time: datetime
order_group_id: str | None = None
last_update_time: str | None = None
expiration_time: str | None = None
last_update_time: datetime | None = None
expiration_time: datetime | None = None
subaccount_number: int | None = None
# v0.14+ backfill (#162). outcome_side/book_side/self_trade_prevention_type
# mirror Order from #159. *_ts_ms (Unix ms) supersede the *_time RFC3339
Expand Down
4 changes: 2 additions & 2 deletions kalshi/ws/orderbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ def __init__(self) -> None:
def apply_snapshot(self, msg: OrderbookSnapshotMessage) -> Orderbook:
"""Initialize (or reset) a book from a full snapshot."""
ticker = msg.msg.market_ticker
yes_levels = {Decimal(p): Decimal(q) for p, q in msg.msg.yes}
no_levels = {Decimal(p): Decimal(q) for p, q in msg.msg.no}
yes_levels = dict(msg.msg.yes)
no_levels = dict(msg.msg.no)
state = _BookState(ticker=ticker, yes=yes_levels, no=no_levels)
self._books[ticker] = state
logger.debug(
Expand Down
21 changes: 15 additions & 6 deletions tests/test_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import importlib
import inspect
import typing
from datetime import datetime
from decimal import Decimal
from pathlib import Path
from types import UnionType
Expand Down Expand Up @@ -562,10 +563,12 @@ def _unwrap_annotation(ann: Any) -> Any:
def _sdk_type_kind(ann: Any) -> str:
"""Classify a Pydantic field annotation into a spec-aligned kind label.

Returns: ``'int'``, ``'str'``, ``'bool'``, ``'decimal'``, ``'list[<kind>]'``,
or ``'unknown'``. Decimals (DollarDecimal, FixedPointCount) count as
``'decimal'`` — a Decimal-typed field is considered compatible with a
spec-declared dollar string.
Returns: ``'int'``, ``'str'``, ``'bool'``, ``'decimal'``, ``'datetime'``,
``'list[<kind>]'``, or ``'unknown'``. Decimals (DollarDecimal,
FixedPointCount) count as ``'decimal'`` — a Decimal-typed field is
considered compatible with a spec-declared dollar string. ``datetime`` is
compatible with spec ``string`` + ``format=date-time`` (Pydantic
auto-coerces RFC3339 to tz-aware datetime).
"""
base = _unwrap_annotation(ann)
if base is int:
Expand All @@ -576,6 +579,8 @@ def _sdk_type_kind(ann: Any) -> str:
return "bool"
if base is Decimal:
return "decimal"
if base is datetime:
return "datetime"
origin = typing.get_origin(base)
if origin is list:
args = typing.get_args(base)
Expand Down Expand Up @@ -645,10 +650,14 @@ def _ws_field_type_violations(
# Rule 2: spec string with format=date-time (ISO timestamp) must be str
# on the SDK. An int-typed SDK field rejects the wire string
# "2026-04-19T18:43:37.662364Z".
if spec_type == "string" and spec_format == "date-time" and sdk_kind not in ("str",):
if (
spec_type == "string"
and spec_format == "date-time"
and sdk_kind not in ("str", "datetime")
):
problems.append(
f"{sdk_name!r}: spec '{spec_name}' is string (date-time), "
f"SDK typed as {sdk_kind}. Use str."
f"SDK typed as {sdk_kind}. Use str or datetime."
)

# Rule 3: spec array of strings (e.g. orderbook snapshot rows of
Expand Down
Loading
Loading