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
36 changes: 36 additions & 0 deletions kalshi/fix/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,32 +64,56 @@
KalshiFixError,
)
from kalshi.fix.messages import (
APP_MESSAGE_MODELS,
BusinessMessageReject,
CollateralAmountChange,
ExecutionReport,
FixGroup,
FixGroupMeta,
FixMessage,
Heartbeat,
Logon,
Logout,
MarketSettlementParty,
MiscFee,
MultivariateSelectedLeg,
NewOrderSingle,
OrderCancelReject,
OrderCancelReplaceRequest,
OrderCancelRequest,
OrderMassCancelReport,
OrderMassCancelRequest,
Party,
Reject,
ResendRequest,
SequenceReset,
TestRequest,
decode_app_message,
groupfield,
)
from kalshi.fix.session import FixSession, FixSessionState
from kalshi.fix.tags import Tag

# Sorted (ruff RUF022); grouping is by the imports above, not by ``__all__`` order.
__all__ = [
"APP_MESSAGE_MODELS",
"BEGIN_STRING_FIXT11",
"SOH",
"ApplVerID",
"BusinessMessageReject",
"CollateralAmountChange",
"EncryptMethod",
"ExecInst",
"ExecType",
"ExecutionReport",
"FixClient",
"FixCodecError",
"FixConfig",
"FixConnection",
"FixConnectionError",
"FixEnvironment",
"FixGroup",
"FixGroupMeta",
"FixLogonError",
"FixMessage",
"FixParser",
Expand All @@ -105,9 +129,19 @@
"KalshiFixError",
"Logon",
"Logout",
"MarketSettlementParty",
"MiscFee",
"MsgType",
"MultivariateSelectedLeg",
"NewOrderSingle",
"OrdStatus",
"OrdType",
"OrderCancelReject",
"OrderCancelReplaceRequest",
"OrderCancelRequest",
"OrderMassCancelReport",
"OrderMassCancelRequest",
"Party",
"RawMessage",
"Reject",
"ResendRequest",
Expand All @@ -119,5 +153,7 @@
"TestRequest",
"TimeInForce",
"decode",
"decode_app_message",
"encode",
"groupfield",
]
28 changes: 25 additions & 3 deletions kalshi/fix/messages/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""Typed FIX message models (FIX Dictionary v1.03).

Exposes the base framework (scalar + repeating-group fields), the session-layer
(admin) messages, and the shared repeating-group entry components. Application
messages (order entry, drop copy, market data, RFQ/settlement) are added in later
phases — see GH #402.
(admin) messages, the shared repeating-group entry components, and the
order-entry message flow. Market-data, drop-copy, and RFQ/settlement flows are
added in later phases — see GH #402.
"""

from __future__ import annotations
Expand All @@ -23,6 +23,18 @@
MultivariateSelectedLeg,
Party,
)
from kalshi.fix.messages.order_entry import (
APP_MESSAGE_MODELS,
BusinessMessageReject,
ExecutionReport,
NewOrderSingle,
OrderCancelReject,
OrderCancelReplaceRequest,
OrderCancelRequest,
OrderMassCancelReport,
OrderMassCancelRequest,
decode_app_message,
)
from kalshi.fix.messages.session import (
Heartbeat,
Logon,
Expand All @@ -34,7 +46,10 @@
)

__all__ = [
"APP_MESSAGE_MODELS",
"BusinessMessageReject",
"CollateralAmountChange",
"ExecutionReport",
"FixGroup",
"FixGroupMeta",
"FixMessage",
Expand All @@ -45,11 +60,18 @@
"MarketSettlementParty",
"MiscFee",
"MultivariateSelectedLeg",
"NewOrderSingle",
"OrderCancelReject",
"OrderCancelReplaceRequest",
"OrderCancelRequest",
"OrderMassCancelReport",
"OrderMassCancelRequest",
"Party",
"Reject",
"ResendRequest",
"SequenceReset",
"TestRequest",
"decode_app_message",
"fixfield",
"groupfield",
]
33 changes: 27 additions & 6 deletions kalshi/fix/messages/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

from __future__ import annotations

import logging
from dataclasses import dataclass
from decimal import Decimal
from enum import StrEnum
Expand All @@ -37,6 +38,8 @@
from kalshi.fix.enums import MsgType
from kalshi.fix.tags import DATA_LENGTH_FIELDS

logger = logging.getLogger("kalshi.fix")

# Reverse of DATA_LENGTH_FIELDS: data_tag -> length_tag. Used by to_body_fields
# to auto-emit the length field immediately before a data field.
_DATA_TO_LENGTH: dict[int, int] = {data: length for length, data in DATA_LENGTH_FIELDS.items()}
Expand Down Expand Up @@ -77,7 +80,7 @@ class FixGroupMeta:
"""

num_in_group_tag: int
entry_model: type[_FixFieldModel]
entry_model: type[FixGroup]


@dataclass(frozen=True)
Expand All @@ -91,7 +94,7 @@ class _ScalarSpec:
class _GroupSpec:
name: str
count_tag: int
entry_model: type[_FixFieldModel]
entry_model: type[FixGroup]


_FieldSpec = _ScalarSpec | _GroupSpec
Expand Down Expand Up @@ -173,8 +176,8 @@ def _parse_group(
pairs: list[tuple[int, str]],
start: int,
count: int,
entry_model: type[_FixFieldModel],
) -> tuple[list[_FixFieldModel], int]:
entry_model: type[FixGroup],
) -> tuple[list[FixGroup], int]:
"""Parse up to ``count`` group entries from ``pairs`` beginning at ``start``.

Each entry begins at the entry model's delimiter (first) tag and runs until
Expand All @@ -184,7 +187,7 @@ def _parse_group(
"""
delim = entry_model._first_tag()
member_tags = entry_model._member_tags()
entries: list[_FixFieldModel] = []
entries: list[FixGroup] = []
i = start
n = len(pairs)
while len(entries) < count and i < n and pairs[i][0] == delim:
Expand All @@ -194,6 +197,14 @@ def _parse_group(
entry_pairs.append(pairs[i])
i += 1
entries.append(entry_model._from_pairs(entry_pairs))
if len(entries) < count:
# Malformed FIX: NumInGroup promised more entries than were delivered.
logger.debug(
"FIX group %s: NumInGroup=%d but only %d entries parsed",
entry_model.__name__,
count,
len(entries),
)
return entries, i


Expand Down Expand Up @@ -258,7 +269,12 @@ def _member_tags(cls) -> frozenset[int]:

@classmethod
def _first_tag(cls) -> int:
"""The delimiter tag — the first declared field's tag (group entry start)."""
"""The delimiter tag — the first declared field's tag (group entry start).

Falls back to a leading group's ``NumInGroup`` tag, but no FIX group
places a nested group as an entry's first field, so in practice this is
always a scalar tag.
"""
for spec in cls._layout():
return spec.tag if isinstance(spec, _ScalarSpec) else spec.count_tag
raise ValueError(f"{cls.__name__} declares no FIX fields")
Expand Down Expand Up @@ -309,6 +325,11 @@ def _from_pairs(cls, pairs: list[tuple[int, str]]) -> Self:
try:
count = int(pairs[idx][1])
except ValueError:
logger.debug(
"FIX group %s: non-integer NumInGroup %r",
spec.entry_model.__name__,
pairs[idx][1],
)
continue
entries, _ = _parse_group(pairs, idx + 1, count, spec.entry_model)
kwargs[spec.name] = entries
Expand Down
Loading