A research stack for event-contract markets — Kalshi, Polymarket — built so that a strategy cannot cheat.
Strategies receive normalized events and return typed decisions. They do not call venue clients, read storage, place orders, or know what time it is. The runner owns all of that.
That constraint is the design. A strategy that can reach the network can look ahead; a strategy that returns values cannot. It also means the same strategy object runs unchanged across backtest, replay, paper, and eventually live.
python3 explore.pyA numbered menu: the typed seam between Python and Rust, the strategy specs, and a synthetic end-to-end run that takes a strategy from raw events to a risk-gated decision with no network and no credentials.
raw venue payload Kalshi REST/WS, Polymarket, weather, macro
│
▼
normalized event one typed shape, whatever the venue
│
▼
Strategy.on_event(event, ctx) returns values; touches nothing
│
▼
strategy decision ─▶ intent ─▶ risk gate ─▶ paper executor · bus · OMS
614 Python tests, passing from a bare checkout · 38 strategy specs · 18 frozen parity cases that both the Python and Rust implementations must reproduce exactly · 11 Rust crates for the hot path.
There is no live order path, deliberately. The point is to normalize, replay, and estimate fills honestly before anything can send an order.
Fill estimation is pessimistic by construction: marketable orders walk the captured opposite book; passive orders join a configurable fraction of visible same-price depth and only fill when later captured trades actually consume that queue. Repeated REST trade polls are deduped so fill volume isn't multiplied by the polling rate.
This is a polyglot workspace. Python code lives under python/, the Rust
hot-path and live-spine crates under rust/ (runner, runtime-hot, risk,
gateway, oms, live-runner, kalshi, parity, model-runtime, bus,
contracts), and the cross-language file-format contracts
that both consume under contracts/. The Python/Rust seam is files on
disk (TOML specs, JSON schemas, Parquet parity cases, ONNX models), not
in-process imports — so either side can be rewritten without touching the
other, and a mismatch surfaces as a schema-validation failure rather than a
silent divergence.
Use python/requirements.txt as the runtime dependency source of truth.
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install --upgrade pip
python3 -m pip install -r python/requirements.txtFor tests, linting, and type checking, install the development requirements.
python/requirements-dev.txt includes python/requirements.txt.
python3 -m pip install -r python/requirements-dev.txtOptional editable package install for the CLI:
python3 -m pip install -e ./pythonFrom the repo root, the Makefile delegates into python/ for you:
make qualityOr run the underlying commands directly:
cd python
python3 -m compileall -q src tests
PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python3 -m pytestThe PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 flag keeps unrelated globally installed
pytest plugins from affecting this repository.
The interesting modules first: domain (the types everything else agrees on),
strategy (the plug-in contract), runner (lifecycle, provenance, risk,
dispatch), normalization (contract matching and cross-venue rejection),
replay (deterministic event-time), and execution (paper fills and queue
modelling). The full inventory:
All 25 packages under python/src/eventcontracts/
domain: venue-neutral dataclasses and closed sum types.strategy: the researcher-facing strategy protocol, lifecycle states, read-only context contract, and registry.plugins/strategies: concrete strategy plugins; currently includesexample_threshold.runner: reference runner. In-memory ports for tests live undertesting/.testing: in-memory ports and other test doubles. Not for production imports.adapters/venues: Kalshi and Polymarket adapter boundaries.ingestion: capture job boundaries.storage: raw envelope and normalized storage boundaries.normalization: contract matching and cross-venue rejection logic.replay: deterministic event-time replay boundary.execution: paper execution, fill simulation, and queue modeling boundary.risk: pre-trade policy, limits, and compliance boundary.features: offline/online feature schema, builder, and feature-store contracts.models: Python-first training, model-family-agnostic ONNX export (sklearn/XGBoost/LightGBM/HuggingFace), calibrated evaluation, export-parity, inference, and model-registry contracts.artifacts: immutable strategy/model bundle writer, loader, validator, and promotion contracts.bus: typed IPC topic, message, codec, publisher, and subscriber contracts.gateway: live-shaped venue command, idempotency, priority scheduling, dry-run gateway, and reconciliation contracts.oms: order ticket, transition, state-machine, and reconciliation contracts.ledger: ledger, cash keeper, position keeper, and settlement accounting contracts.allocation: sleeve registry and capital allocation contracts.observability: structured logging, metrics, tracing, alerts, and health-check contracts.external: point-in-time weather, crypto, macro, and other provider contracts.contracts: dependency-free validation helpers for the cross-language schema files in top-levelcontracts/.markets: subscription-based market detection and in-memory reference catalog for paper/dry-run mode.
Other top-level directories:
python/tests: pytest suite.rust/: Rust workspace with compileable low-latency scaffold crates:contracts,feature-builder,runner,gateway,bus,allocator, andparity.contracts/: cross-language file-format contracts (JSON schemas, TOML examples, parity cases).configs/: non-secret config examples for venues, storage, and research programs.docs/: architecture, contracts, roadmap, research programs, and development notes.notebooks/: researcher exploration surface.
raw venue/external payload
-> normalized event
-> Strategy.on_event(event, context)
-> strategy decision
-> intent envelope
-> risk gate
-> paper executor, bus, OMS, or gateway
Strategies do not call venue clients, storage, the bus, or execution APIs directly. They return values. The runner owns lifecycle, provenance, state restore/save, risk evaluation, and dispatch.
For predictive edges that do not depend on sub-second latency, start with Kalshi REST polling. This captures repeated order-book snapshots and deduped recent trades, then normalizes them into the same replay path used by backtests.
eventcontracts capture \
--venue kalshi \
--transport rest \
--patterns "KXHIGHNY-*" \
--max-polls 120 \
--poll-interval-seconds 30 \
--trades-limit 100 \
--normalize \
--out data/edge-tests/kalshi-highny-rest
eventcontracts inspect-data \
--data data/edge-tests/kalshi-highny-rest \
--source kalshi-rest
eventcontracts backtest \
--strategy configs/strategies/weather-temperature-arbitrage.toml \
--sleeve configs/sleeves/weather-kalshi-paper-a.toml \
--data data/edge-tests/kalshi-highny-rest \
--latency-ms 250 \
--queue-fraction 1.0 \
--starting-equity 10000 \
--out artifacts/reports/weather-rest-edge.jsonFill realism in this mode is deliberately conservative: marketable orders walk the captured opposite book, while passive orders join a configurable fraction of visible same-price depth and only fill when later captured trades consume that queue. Repeated REST trade polls are deduped so fill volume is not artificially multiplied.
Every strategy implements:
on_init(ctx): one-time setup.on_event(event, ctx): receive a normalized event and return decisions.on_shutdown(ctx): clean exit hook.snapshot()/restore(state): optional opaque state persistence.
Strategies are registered by name:
from eventcontracts.strategy.registry import register
@register("my_strategy")
def factory(spec):
return MyStrategy(spec)The runner resolves a StrategySpec through the registry, feeds it
NormalizedEvent values, wraps returned StrategyDecision values into
IntentEnvelope objects, and passes them through a RiskGate before emitting.
See docs/strategy-runner-contract.md for the full contract and src/eventcontracts/plugins/strategies/example_threshold.py for the smallest working example.
The domain layer now includes:
- Market data:
Market,Quote,Trade,OrderBook,OrderBookLevel. - Identity:
StrategyId,SleeveId,RunId, order/fill/event/model IDs. - Orders and fills:
Order,OrderReject,Fill, side/type/status enums. - State:
Position,CashBalance,Exposure,LedgerEntry. - Lifecycle:
MarketLifecycleEvent,SettlementEvent. - Features:
FeatureVector,Signal,Prediction. - Execution priority:
ExecutionPriority,LatencyTier. - Specs:
StrategySpec,SleeveSpec,RiskProfile,ModelRef. - Inbound event variants: quote, trade, book, lifecycle, settlement, external, timer, own fill, own order update, own order reject.
- Strategy decision variants: place, cancel, replace, alert, no action.
Order-affecting decisions can carry latency hints. A crypto lead-lag strategy
can emit ExecutionPriority(tier=LatencyTier.FAST, max_delay_ms=100), while a
slower weather or macro prediction edge can use the default STANDARD or
RELAXED priority. The future gateway should use this for scheduling and
rate-limit allocation, but never to bypass risk checks.
- Create a new module in
src/eventcontracts/plugins/strategies/. - Subclass
StrategyBaseor implement theStrategyprotocol. - Match only on
NormalizedEventvariants your strategy understands. - Return
StrategyDecisionvalues such asPlaceOrderorNoAction. - Register a factory with
@register("strategy_name"). - Add a smoke test that builds a
StrategySpec,SleeveSpec, in-memory event source, andStrategyRunner.
- docs/architecture.md: layer boundaries and data flow.
- docs/dataflow-map.md: current partial implementation functions and the data types handed between layers.
- docs/strategy-runner-contract.md: concrete data types and plug-in contract.
- docs/artifact-contract.md: planned model and strategy artifact bundle format.
- docs/ml-model-pipeline.md: model-family-agnostic ONNX/HuggingFace training, export, parity, and serving pipeline with reference results.
- docs/development.md: local setup, verification, and contribution conventions.
- docs/implementation-roadmap.md: gap matrix, MVP sequence, and hardening plan.
- docs/deep-research-report.md: research assessment and market-structure rationale behind the roadmap.
- docs/production-readiness-assessment.md: acquirer-lens readiness + scalability audit, severity-ranked findings, and the ordered path (P0–P7) to live.
- docs/live-deployment-remaining-roadmap.md: the 12 non-negotiable live invariants, deployment stages, and workstreams.
- docs/quant-mind-integration-assessment.md: decision memo on using external research-extraction tooling at the front of the edge funnel.
A gated live-ordering path now exists: the Rust rust/crates/live-runner with
--live-submit routes intents through risk + the Kalshi gateway/venue client to
place real orders. It is off by default and guarded (explicit KALSHI_ENV,
mandatory --max-live-orders, startup reconciliation, a confirmation prompt, a
file kill switch, and a toxicity breaker). Everything else runs paper/backtest.
The safe path each strategy must clear before real capital:
- Real venue capture.
- Raw event persistence.
- Deterministic replay.
- Strategy/runner parity in backtest and paper.
- Paper execution with fees, slippage, latency, queue, pauses, and settlement.
- Risk, compliance, reconciliation, credentials, and gateway isolation.
- Cross-language parity: any model/feature/risk change must agree Python↔Rust.
Read docs/runbooks/kalshi-live-runner.md before enabling real orders. Note the
two risk gates are not fully identical (e.g. max_daily_loss latches in
Python but not in Rust) — the Rust gate is authoritative for live.