feat(fix): FIX protocol foundation (codec + session engine + auth) — part of #402#422
Conversation
…part of #402 Hand-rolled, async-first, dependency-free FIX engine (FIXT.1.1 transport / FIX50SP2 application) shared by the prediction and perps (margin) products. Foundation layer only — order entry, drop copy, market data, and RFQ/settlement message flows land in later phases (sub-issues of #402). - codec: tag=value framing (BodyLength / 3-digit CheckSum / SOH), length- prefixed data fields read by byte count, incremental FixParser with false-"8=" resync and bounded buffering - typed Pydantic message framework (tag-metadata driven) + session/admin messages (Logon/Logout/Heartbeat/TestRequest/ResendRequest/SequenceReset/Reject) - FixSigner: RSA-PSS logon RawData signature reusing the existing cryptography key (adds an additive KalshiAuth.private_key property) - FixConfig: prod/demo x prediction/margin host+port tables, TLS + host guards, retransmission + UseDollars policy - async FixSession state machine: signed logon, heartbeat/test-request liveness, inbound sequence tracking with buffered gap-fill/resend, reset-on-logon policy, AWS full-jitter reconnect, graceful logout - FixClient (prediction, 6 sessions) + MarginFixClient (margin, KALSHI_PERPS_* creds, UseDollars on) over a shared core - 90 tests: codec round-trip/framing, message round-trip, logon signature, config resolution/guards, and the async session state machine driven by an in-memory mock acceptor Hardened via an internal multi-agent adversarial review and the multi-LLM review (codex/gemini/grok): codec resync + data-field adjacency + SOH-injection rejection + corrupt-frame resync; session ValidationError recovery, SequenceReset gap-validation/no-rewind, outbound-seq reserve-before-send, close() timeout + task ordering, logon lower-seq fatal, inbound-ResendRequest gating, strict Y/N booleans; config per-product heartbeat floor + TLS-to- gateway enforcement. Part of #402. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code Review — FIX Protocol FoundationThis is a substantial, well-prepared PR. The PR description is accurate: the multi-agent pre-review pass clearly improved the codec's security posture (SOH-injection rejection, data-field adjacency guards, BodyLength-bounded buffering). The architecture is a good fit for this codebase — it mirrors the WebSocket subsystem's structure and the existing dual-transport/async-first patterns. Findings are ordered HIGH → MEDIUM → LOW. HIGH1. In-place mutation of the caller's if isinstance(message, Logon):
message.raw_data = self._signer.sign_logon(...)The Suggested fix: construct a new if isinstance(message, Logon):
message = Logon.model_copy(message, update={"raw_data": self._signer.sign_logon(...)})Or freeze 2. @classmethod
def _fix_fields(cls) -> list[tuple[str, int, FixType]]:
out: list[tuple[str, int, FixType]] = []
for name, field in cls.model_fields.items():
...
return outThis rebuilds the list on every @classmethod
@functools.lru_cache(maxsize=None)
def _fix_fields(cls) -> list[tuple[str, int, FixType]]:
...MEDIUM3. Both use a loose type to avoid importing 4. Importing a private helper across module boundaries ( from kalshi.fix.messages.base import FixMessage, _format_utc_timestamp
5. @property
def effective_use_dollars(self) -> bool:
if self.product is FixProduct.MARGIN:
return True # use_dollars field value is silently ignored
...A caller who passes if self.product is FixProduct.MARGIN and self.use_dollars is False:
raise ValueError("use_dollars=False is not allowed for the margin product")6. Mid-session inbound if mt == MsgType.LOGON:
# Mid-session Logon is unexpected once ACTIVE; ignore.
returnA mid-session Logon from the acceptor is a protocol error (the FIX spec suggests closing the connection). The current behavior is too lenient — not even a LOW7. @pytest.fixture
async def acceptor() -> object:All tests access 8.
9. The same SummaryThe codec and session-layer logic is sound, the test coverage is solid for a foundation PR, and the security posture is good (SOH injection prevention, TLS enforcement, credential redaction in |
Resolves the automated code-review findings on PR #422: - _send signs onto a Logon COPY (model_copy) instead of mutating the caller's object — avoids stale signature bytes on reuse across reconnects - cache the per-class FIX field map (_fix_fields) — deterministic per class and on the hot path once order-entry/market-data application messages land - annotate ssl_context as ssl.SSLContext | None on FixSession and the client facades (was Any / object) - move the UTCTimestamp helpers to kalshi/fix/_timefmt.py so session.py no longer imports a module-private name from messages.base - FixConfig.__post_init__ rejects margin with use_dollars=False (previously silently ignored) - log a warning on an unexpected mid-session inbound Logon - type the acceptor test fixture (AsyncIterator[MockAcceptor]); add an on_state_change transition-coverage test - note SHA256 algorithm-descriptor reuse safety in fix/auth.py ruff + mypy --strict clean; 91 FIX tests pass. Part of #402. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for the thorough review — all nine findings are addressed in a648c08. HIGH
MEDIUM
LOW
|
Code Review — PR #422: FIX Protocol FoundationScope: 4 322 additions across OverviewA well-designed, async-first FIX engine that closely mirrors the existing WebSocket subsystem. The codec, session state machine, and test coverage are all at a high quality bar. The two adversarial review passes the PR description mentions are visible in the code — SOH injection, credential redaction, TLS enforcement, sequence-number invariants, and malformed-admin recovery are all handled correctly. Issues below are ordered by severity. Medium — Worth addressing before merge1. Gap-fill
if bool(sr.gap_fill_flag) and seq is not None and seq > self._in_seq:
if self._supports_retransmission:
await self._request_resend()
return # ← returns without buffering the SequenceResetWhen a In practice this path requires the server to send a gap-fill SequenceReset before the resend it covers, which is an unusual ordering, but it's a logical hole in the recovery path. The parallel in 2.
@classmethod
def from_env(cls, ...) -> FixClient: # should be -> Self
def from_env(cls, ...) -> MarginFixClient: # should be -> Self
Low — Suggestions3.
self._connected = asyncio.Event() # set once, never cleared
4.
# Does not call _send(): a gap-fill must use begin_seq_no as MsgSeqNum, not
# consume the next outbound slot — _send() always increments _out_seq.5.
6. Lines 107-156: Comments like Correctness notes (no action needed, just confirmed correct)
Test coverage90 tests is solid for a foundation layer. Missing cases worth adding in a follow-up:
VerdictArchitecture and security posture are excellent. The two blocking-ish items (gap-fill SequenceReset buffering, 🤖 Generated with Claude Code |
…turns - _handle_sequence_reset now BUFFERS an out-of-order gap-fill SequenceReset (seq ahead of expected, RT/PT) and _drain_pending applies its NewSeqNo jump once the preceding gap is recovered (extracted _apply_sequence_reset_forward). Previously the reset was dropped and _in_seq never advanced. - FixClient.from_env / MarginFixClient.from_env now return Self (subclass-correct, matching from_auth) - _send_gap_fill: comment explains WHY it bypasses _send (begin_seq_no as MsgSeqNum; must not consume an outbound seq) - document the KALSHI_FIX_ALLOW_UNKNOWN_HOST escape hatch in FixConfig's docstring - MockAcceptor clears _connected on disconnect so wait_connected() is valid after a reconnect - drop the misleading interleaved category comments in kalshi.fix.__all__ tests: - out-of-order gap-fill SequenceReset buffering on RT (pins the bug-1 fix) - new tests/fix/test_client.py: from_auth/from_env return the concrete subclass, prediction exposes all six sessions, margin lacks prediction-only helpers and rejects an RFQ session, product/config mismatch raises ruff + mypy --strict clean; 97 FIX tests pass. Part of #402. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks — second round addressed in 01ec0ad. Medium
Low
Also added the follow-up test cases you flagged: out-of-order gap-fill (#1), |
Code Review — FIX Foundation (#422)This is a substantial, well-prepared PR. The multi-LLM adversarial pre-review is evident: the hardest edge cases (SOH injection, false- One Medium Issue
Suggested fix — fail fast when the buffer exceeds a reasonable cap (e.g. 10 k entries): # in _handle_inbound, before self._pending[seq] = raw:
_PENDING_CAP = 10_000
if len(self._pending) >= _PENDING_CAP:
raise FixSequenceError(
f"pending message buffer cap {_PENDING_CAP} exceeded; closing session",
expected=self._in_seq,
received=seq,
)
self._pending[seq] = rawSame guard applies in Low Issues
Positive ObservationsCodec correctness:
Session state machine:
Security:
Tests:
Conventions:
SummaryThe only actionable change before merge is capping 🤖 Reviewed with Claude Code |
Summary
Foundation layer (PR-1) of the FIX protocol epic (#402): a hand-rolled, async-first, dependency-free FIX engine (FIXT.1.1 transport / FIX50SP2 application) shared by the prediction and perps (margin) products. One Kalshi FIX dictionary (v1.03) covers both; the margin product is a subset (no RFQ / settlement / post-trade) that always quotes fixed-point dollars.
This PR delivers the engine only. Order entry, drop copy, market data, and RFQ/settlement message flows follow as separate sub-issues of #402.
What's included
kalshi/fix/codec.py—BodyLength/3-digitCheckSum/SOH framing, length-prefixed data fields read by byte count, incrementalFixParserwith false-8=resync + bounded bufferingkalshi/fix/tags.py,enums.py(full dictionary v1.03 surface)kalshi/fix/messages/— tag-metadata-driven Pydantic framework + session/admin messageskalshi/fix/auth.py—FixSigner(RSA-PSS logonRawData) reusing the existingcryptographykey; adds an additiveKalshiAuth.private_keypropertykalshi/fix/config.py— prod/demo × prediction/margin host+port tables, TLS + host guards, retransmission +UseDollarspolicykalshi/fix/connection.py,session.py— signed logon, heartbeat/test-request liveness, sequence tracking with buffered gap-fill/resend, reset-on-logon, full-jitter reconnect, graceful logoutkalshi/fix/client.py(FixClient, prediction) +kalshi/perps/fix/(MarginFixClient, margin) over a shared basetests/fix/— 90 tests incl. an in-memory mock acceptor driving the full state machineDesign decisions
quickfix/simplefix): zero new deps, exactDollarDecimalmoney precision, async-native TLS, end-to-end typing — matches the existing hand-rolled WS subsystem.quickfix's threaded model,double-based pricing, and stunnel-only TLS conflict with this SDK's invariants.kalshi.fix, a margin facade inkalshi.perps.fix(forcesUseDollars, NR/RT/DC/MD only,KALSHI_PERPS_*creds).Review & hardening
Reviewed twice before this PR:
ValidationErrorrecovery (malformed admin → Reject, no wedge),SequenceResetgap-validation/no-rewind, outbound-seq reserve-before-send,close()timeout + task ordering, logon lower-seq → fatal, inbound-ResendRequestgating, strict Y/N booleans; per-product heartbeat floor; TLS-to-gateway enforcement.Verification
uv run ruff check .— cleanuv run mypy kalshi/(strict) — clean (147 files)uv run pytest tests/fix/— 90 passedOut of scope (follow-up sub-issues of #402)
Order Entry · Drop Copy · Market Data · Order Groups · RFQ/Quoting (prediction) · Market Settlement/Post-Trade (prediction). Repeating-group codec support and caller-supplied advanced logon options (
SkipPendingExecReports/ListenerSession) land with the phases that need them.Part of #402.
🤖 Generated with Claude Code