System Architecture

Technical overview of how the Kalshi Unusual Market Activity Monitor is built and deployed

Tech Stack

LayerTechnology
Web FrameworkFastAPI (async Python)
ServerUvicorn
DatabasePostgreSQL via asyncpg + SQLAlchemy
SchedulerAPScheduler (in-process)
EmailResend API
TemplatesJinja2 + Pico CSS
HostingRailway (Hobby plan, auto-deploy from GitHub)

Data Pipeline

Data flows through five stages on a continuous loop:

Kalshi API ──▶ Ingestion ──▶ PostgreSQL ──▶ Scoring ──▶ Alerts ──▶ Email
                  │                            │           │
              Every 5 min                  Every 5 min   Every 5 min
              (markets,                    (6 signals     (if score
               trades,                     computed,      exceeds
               orderbooks)                 weighted,      threshold)
                                           labeled)

1. Ingestion Layer

The ingestion layer pulls data from the Kalshi public API and stores it in PostgreSQL.

Market Sync

Streams all open markets from Kalshi page-by-page via async generators (200 per batch) to stay under the 512MB RAM limit. Skips the Sports category. Maps each market to its event category. Marks markets as eligible or ineligible based on volume and open interest thresholds.

Trade Sync

For each eligible market, fetches recent trades since the last known timestamp. Stores individual trade records with price, size, side, and timestamp.

Orderbook Sync

Snapshots the current bid/ask/spread for eligible markets. Hardened against malformed API data with try/except guards around float conversions.

API Client

Async httpx client with cursor-based pagination, automatic rate limit handling (auto-retry on HTTP 429), and batched async generators to keep memory usage low.

2. Scoring Engine

The core intelligence. Computes a composite anomaly score (0–100) for each eligible market using six weighted signals. Each signal compares the market's current behavior to its own 7-day historical baseline.

SignalWeightWhat It Detects
Trade Size30%Individual trades that are abnormally large vs. the market's historical baseline
Price Impact25%Price moving more than expected given trade volume
Liquidity20%Spread widening or thinning books — someone draining liquidity
Clustering15%Bursts of trades concentrated in a short time window
Timing5%Activity during off-hours when informed traders tend to operate
Cross-Market5%Correlated unusual activity across related markets in the same event

A confirmation gate prevents false positives: at least one primary signal (Trade Size or Price Impact) must score 60+ and one secondary signal must score 40+ for a market to reach "High" or above. Markets failing the gate are capped at 54.99.

For full scoring details, see How It Works.

3. Alert System

Alert Generator

After each scoring cycle, checks if any market exceeds the user-configured score threshold (default: 70). Includes deduplication: won't re-alert the same market within 4 hours unless the score jumps by 15+ points (escalation detection).

Email Delivery

Sends via the Resend API using async httpx. Supports multiple comma-separated recipients. Two email types:

4. Database Schema

PostgreSQL with connection resilience: pool_pre_ping, 300s pool recycle, and 5-attempt startup retry with backoff.

TablePurpose
marketsAll tracked markets — ticker, title, category, price, volume, eligibility flags
tradesIndividual trade records — price, size, side, timestamp
orderbook_snapshotsPoint-in-time bid/ask/spread snapshots
alert_scoresEvery scoring cycle result per market — all 6 sub-scores + composite
baselinesRolling 7-day statistical baselines per market
alertsGenerated alerts with deduplication tracking
user_alert_statesWatch/dismiss states per market
user_settingsKey-value store for email, threshold, and recap preferences

5. Web Interface

Server-rendered HTML with Jinja2 templates and Pico CSS. HTMX provides interactive features (watch/dismiss buttons) without a JavaScript framework.

PageRoutePurpose
Dashboard/Live table of markets scored Medium+, sorted by score
Market Detail/market/{ticker}Deep dive: score breakdown bars, score history, recent trades, explanation text
Watched/watchedUser's personal watchlist
Alert History/alertsAll generated alerts with label filtering
How It Works/aboutFull scoring model documentation
Architecture/architectureThis page — system technical overview
Settings/settingsEmail config, threshold, daily recap toggle
Health Check/pingReturns alive status and database type

All timestamps display in 12-hour AM/PM Eastern time via a custom Jinja2 filter that converts UTC to US/Eastern with proper EST/EDT handling.

6. Background Scheduler

All background jobs run in-process via APScheduler. Each job is wrapped in an independent try/except so one failure doesn't cascade to the others.

JobIntervalWhat It Does
Market Sync5 minFetch and upsert all open markets from Kalshi
Data Ingestion5 minFetch trades and orderbook snapshots for eligible markets
Scoring Cycle5 minRun the 6-signal model on all eligible markets
Alert Generation5 minCheck scores, create alerts, send emails
Baseline Computation6 hoursRecompute 7-day rolling baselines per market
Data Retention24 hoursPurge trades and snapshots older than 14 days
Daily Recap EmailDaily 8pm ETSend recap of top alerts from the past 24 hours

7. Deployment

GitHub push ──▶ Railway auto-deploy ──▶ Docker build ──▶ Container start
                                                          ├── Uvicorn (web server)
                                                          ├── APScheduler (background jobs)
                                                          └── PostgreSQL (Railway service)

Single-container architecture — the web server and all background jobs run in one process. This keeps costs minimal ($5/month on Railway Hobby) but means a redeploy briefly pauses both the UI and data ingestion.

Memory Management

Railway's container has a 512MB RAM limit. To stay within this:

Resilience