Market Scanner (Explanation)
The scanner automatically finds opportunities across all Kalshi markets instead of you manually browsing.
It applies filters and heuristics to surface markets worth investigating.
Scanner Types
1. Close Races
Markets priced between 40-60% - the "coin flip" zone where edge has the most value.
Why this matters:
- A 50/50 market has maximum uncertainty
- If you have real information, your edge is worth more here
- A 5% edge on a 50% market is worth more than 5% edge on a 95% market (Kelly criterion)
uv run kalshi scan opportunities --filter close-race --top 10
Filter logic:
mid_prob = (yes_bid_cents + yes_ask_cents) / 200.0
is_close_race = 0.40 <= mid_prob <= 0.60
2. High Volume
Markets with significant trading activity - these have liquidity and market attention.
Why this matters:
- Liquid markets have tighter spreads (cheaper to trade)
- High volume often means important events
- Price discovery is better in liquid markets
uv run kalshi scan opportunities --filter high-volume --top 10
Filter logic:
# Default threshold (not currently configurable via CLI): volume_24h >= 10,000
high_volume = [m for m in markets if m.volume_24h >= 10_000]
sorted_by_volume = sorted(high_volume, key=lambda m: m.volume_24h, reverse=True)
3. Wide Spread
Markets with large bid-ask spreads - potential opportunities for patient traders.
Why this matters:
- Wide spreads mean market makers are uncertain
- If you have conviction, you might get good fills
- Can also indicate illiquidity (trade carefully)
uv run kalshi scan opportunities --filter wide-spread --top 10
Filter logic:
spread = yes_ask - yes_bid
is_wide_spread = spread >= threshold # default: 5 cents (fixed in code today)
4. Expiring Soon
Markets closing within a time window - resolution is imminent.
Why this matters:
- Last chance to trade before settlement
- Late information advantages (you might know outcome before market)
- Prices should converge to 0 or 100
uv run kalshi scan opportunities --filter expiring-soon --top 10
Filter logic:
time_remaining = close_time - now
is_expiring_soon = time_remaining <= threshold # e.g., 24 hours
5. New Markets
Markets created recently — the “information arbitrage window” where early pricing can be sloppy.
uv run kalshi scan new-markets --hours 24 --limit 20
By default, unpriced markets are skipped. Use --include-unpriced to include:
- No quotes (0/0)
- Placeholder quotes (0/100)
uv run kalshi scan new-markets --hours 24 --include-unpriced
Category filtering uses the same aliases as other scanners (comma-separated):
uv run kalshi scan new-markets --category econ,politics,ai
6. Movers
Markets that moved significantly since a previous snapshot.
Why this matters:
- Large moves indicate new information
- Might be overreaction (mean reversion opportunity)
- Or might be underreaction (momentum opportunity)
uv run kalshi scan movers --period 1h --top 10
Requires: Price snapshots in database (run kalshi data snapshot periodically)
Filter logic:
# Compare current price to snapshot from N hours ago
move = current_price - historical_price
abs_move = abs(move)
percent_move = abs_move / historical_price
7. Arbitrage
Flags potential consistency / divergence opportunities across related markets.
Why this matters:
- Related markets often move together (or sum to ~100% when they represent two outcomes).
- If they diverge, it can signal mispricing or stale quotes.
uv run kalshi scan arbitrage --threshold 0.10 --top 10
This command does not place trades. It:
- Uses historical price snapshots (if available in your DB) to find correlated market pairs and flags:
divergence: positively correlated pairs whose midpoint probabilities differ by more than--threshold.inverse_sum: negatively correlated pairs whose midpoint probabilities no longer sum to ~100% within--threshold.- Always checks events with exactly two priced markets and flags
inverse_sumwhen their midpoint probabilities deviate from 100% by more than--threshold.
Use --tickers-limit to bound how many tickers are included in the historical correlation analysis.
Filter logic (simplified):
In the implementation, corr_type is derived from historical correlation analysis of the two markets' midpoint
probabilities (from your local price snapshot history). It is the string value of CorrelationType
(positive, negative, lead_lag, or none) from src/kalshi_research/analysis/correlation.py.
if corr_type == "positive" and abs(price_a - price_b) > threshold:
opportunity_type = "divergence"
if corr_type == "negative" and abs((price_a + price_b) - 1.0) > threshold:
opportunity_type = "inverse_sum"
if event_has_exactly_two_priced_markets and abs((p1 + p2) - 1.0) > threshold:
opportunity_type = "inverse_sum"
Caveats:
- Transaction costs eat into edge
- Timing risk (prices move while you execute)
- Capital lockup (money tied until settlement)
Output Format
Scanner commands print tables (exact columns depend on the command).
Opportunities
┌──────────────────┬──────────────────────┬─────────────┬────────┬─────────┐
│ Ticker │ Title │ Probability │ Spread │ Volume │
├──────────────────┼──────────────────────┼─────────────┼────────┼─────────┤
│ TRUMP-2024 │ ... │ 52.0% │ 2¢ │ 50,000 │
│ ... │ ... │ ... │ ... │ ... │
└──────────────────┴──────────────────────┴─────────────┴────────┴─────────┘
Arbitrage
┌────────────────────────────┬────────────┬─────────────────────────┬───────────┬────────────┐
│ Tickers │ Type │ Expected │ Divergence │ Confidence │
├────────────────────────────┼────────────┼─────────────────────────┼───────────┼────────────┤
│ AAA, BBB │ divergence │ Move together (r=0.72) │ 12.00% │ 0.72 │
│ ... │ ... │ ... │ ... │ ... │
└────────────────────────────┴────────────┴─────────────────────────┴───────────┴────────────┘
CLI Options
Add --full/-F to disable truncation in table output.
Opportunities
--profile tradeable # Preset filters: raw|tradeable|liquid|early
--early-hours 72 # Only used with --profile early (newness window)
--min-volume 1000 # Minimum 24h volume (close-race filter only; overrides --profile default)
--max-spread 10 # Maximum spread in cents (close-race filter only; overrides --profile default)
--max-pages 10 # Optional pagination safety limit (omit for full)
--top 10 # Number of results to show
--category ai # Filter by category (e.g. Politics, Economics, AI)
--no-sports # Exclude Sports markets
--event-prefix KXFED # Filter by event ticker prefix
--min-liquidity 50 # Minimum liquidity score (0-100; fetches orderbooks)
--show-liquidity # Show liquidity score column (fetches orderbooks)
--liquidity-depth 25 # Orderbook depth for liquidity scoring
--full # Show full tickers/titles without truncation
New Markets
--hours 24 # Hours to look back for new markets
--category econ,ai # Category filter (comma-separated; also supports --categories)
--include-unpriced # Include markets with no/placeholder quotes (0/0 or 0/100)
--limit 20 # Maximum results to show
--max-pages 10 # Optional pagination safety limit (omit for full)
--json # Output as JSON
--full # Show full tickers/titles without truncation
Arbitrage
--db data/kalshi.db # DB used for historical correlation analysis (optional)
--threshold 0.10 # Min divergence to flag (0-1)
--tickers-limit 50 # Correlation analysis cap (0 = analyze all tickers)
--top 10 # Number of results to show
--max-pages 10 # Optional pagination safety limit (omit for full)
--full # Show full tickers/relationships without truncation
Examples
# Close races (recommended default slop filters)
uv run kalshi scan opportunities --profile tradeable --filter close-race --top 10
# Close races with stronger execution constraints (fetches orderbooks)
uv run kalshi scan opportunities --profile liquid --filter close-race --top 10
# Close races in newly-created markets (tight spread + liquidity; fetches orderbooks)
uv run kalshi scan opportunities --profile early --early-hours 72 --top 10
# Fully manual (overrides all profile defaults)
uv run kalshi scan opportunities \
--filter close-race \
--profile raw \
--min-volume 1000 \
--max-spread 10 \
--min-liquidity 50 \
--full \
--top 10
# New markets (information arbitrage window)
uv run kalshi scan new-markets \
--hours 24 \
--include-unpriced \
--category econ,ai \
--limit 20
# Big movers in the last hour
uv run kalshi scan movers \
--period 1h \
--top 10 \
--db data/kalshi.db
# Arbitrage with at least 10% edge
uv run kalshi scan arbitrage \
--threshold 0.10 \
--db data/kalshi.db
Architecture
Kalshi API (public)
│
▼
MarketScanner
│
┌────┴────┐
▼ ▼
Filter Sort
│ │
└────┬────┘
▼
ScanResult[]
│
▼
CLI Table Output
Database Integration
Some scans require historical data:
- Movers: Needs price snapshots to compare against
- Arbitrage: Can use cached market data for speed
Build up your database:
# Initial sync
uv run kalshi data init
uv run kalshi data sync-markets
# Take snapshots periodically
uv run kalshi data snapshot
# Or run continuous collection
uv run kalshi data collect --interval 15
Key Code
- Scanner:
src/kalshi_research/analysis/scanner.py - CLI:
src/kalshi_research/cli/scan.py
Use Case: Daily Workflow
# Morning: What moved overnight?
uv run kalshi scan movers --period 24h --top 20
# Find close races worth researching
uv run kalshi scan opportunities --filter close-race --min-volume 5000 --top 10
# Check for arbitrage
uv run kalshi scan arbitrage --threshold 0.05 --top 10
# Take a snapshot for tomorrow's comparison
uv run kalshi data snapshot
See Also
- Alerts System - Get notified when conditions are met
- Usage: Scanning - CLI commands
- Data Pipeline - How snapshots work