Autonomous multi-strategy trading system for Bitcoin binary prediction markets.
Kalshibot exploits structural pricing inefficiencies between Binance spot BTC, Polymarket prediction contracts, and Kalshi's 15-minute rolling Bitcoin markets. It combines a Geometric Brownian Motion probability model, cross-exchange arbitrage detection, and Kelly Criterion position sizing to execute high-frequency, edge-positive trades with full real-time observability.
- Abstract
- System Architecture
- Market Microstructure
- Trading Strategies
- Probability Model
- Position Sizing
- Risk Management
- Execution Engine
- Data Infrastructure
- Real-Time Dashboard
- Performance Metrics
- Technical Stack
- Getting Started
- Configuration Reference
- Disclaimer
Kalshibot is an automated trading agent that targets Kalshi's KXBTC15M series -- 15-minute binary contracts on whether Bitcoin's price will be higher or lower at settlement. The system ingests real-time price data from Binance (via WebSocket), fair-value signals from Polymarket (via REST polling), and on-chain oracle prices from RedStone, then generates trading signals through three independent strategy modules: (1) directional trading based on a GBM-derived implied probability model, (2) cross-exchange arbitrage when Polymarket fair value diverges from Kalshi contract pricing, and (3) dual-side guaranteed-profit trades when the combined cost of YES + NO contracts falls below $1.00. All positions are sized using fractional Kelly Criterion and managed through adaptive scan-rate execution, automated take-profit triggers, and real-time portfolio monitoring via a web-based Mission Control dashboard.
KALSHIBOT SYSTEM ARCHITECTURE
============================================================================
DATA LAYER ENGINE INTERFACE
──────────────────────────────────────────────────────────────────────────────
┌─────────────────┐ ┌──────────────────────┐ ┌──────────────────┐
│ Binance WebSocket│ │ │ │ Mission Control │
│ btcusdt@bookTick │────>│ Strategy Engine │ │ (Dashboard) │
│ ~200ms latency │ │ │ │ │
│ + REST fallback │ │ ┌────────────────┐ │ │ P&L Chart │
└─────────────────┘ │ │ Normal CDF │ │ │ Bot Intent │
│ │ Implied Prob │ │ │ Active Markets │
┌─────────────────┐ │ │ Kelly Sizing │ │ │ Open Positions │
│ Polymarket CLOB │ │ └────────────────┘ │ │ Trade Log │
│ Gamma + Price API│────>│ │ │ 14 Statistics │
│ 4s poll cycle │ │ 3 Signal Generators │ │ 4 Connection │
│ Slug-based disc. │ │ ┌────┐ ┌────┐ ┌───┐ │ │ Status Dots │
└─────────────────┘ │ │DIR │ │ARB │ │DUL│ │ └────────┬─────────┘
│ └────┘ └────┘ └───┘ │ │
┌─────────────────┐ │ │ Socket.io (real-time)
│ Kalshi REST API │ │ Adaptive Scan Rate │ │
│ RSA-PSS Auth │<───>│ 1s-3s based on vol │──────────────┘
│ Order Execution │ │ │
└─────────────────┘ │ Take-Profit Monitor │
│ Settlement Handler │
┌─────────────────┐ │ │
│ RedStone Oracle │ │ State Persistence │
│ Multi-gateway │────>│ (JSON, atomic write) │
│ 3s poll cycle │ └──────────────────────┘
└─────────────────┘
Kalshi's KXBTC15M series consists of rolling 15-minute binary contracts on Bitcoin price direction. Each contract settles to $1.00 if BTC's price is higher at the end of the 15-minute window than at the start, and $0.00 otherwise. New contracts are continuously created, providing a steady stream of tradable instruments.
Binary prediction markets exhibit a structural latency asymmetry:
- Binance spot BTC updates via WebSocket approximately every 200ms
- Kalshi contract prices reprice with a 3-7 second lag relative to spot movements
- Polymarket equivalent contracts serve as an independent fair-value benchmark
This latency window creates a systematic edge: when BTC moves on Binance, the probability of the contract's outcome changes immediately, but Kalshi's order book takes several seconds to reflect the new equilibrium. During this window, contracts are mispriced relative to their true implied probability.
- Kalshi's order book is thinner than centralized spot exchanges, resulting in slower price discovery
- Market makers on Kalshi do not have the same low-latency infrastructure as spot exchanges
- The 15-minute contract duration creates continuous new opportunities across overlapping windows
- Polymarket operates as a fully independent venue with its own price formation dynamics
Kalshibot employs three independent signal generators, each targeting a different market inefficiency. Signals are generated in parallel, sorted by edge magnitude, and executed sequentially.
Exploits the latency between Binance spot price movements and Kalshi contract repricing.
Signal Logic:
modelEdge_YES = (P(UP) - Kalshi_YES_ask) * 100
modelEdge_NO = (P(DOWN) - Kalshi_NO_ask) * 100
IF modelEdge > MIN_DIVERGENCE (default 8%):
Generate BUY signal on the mispriced side
Where P(UP) is the model-implied probability derived from the GBM framework (see Probability Model).
Entry Conditions:
- Binance BTC price is live (updated within 5 seconds)
- Model-implied probability diverges from Kalshi contract price by >=
MIN_DIVERGENCE - Time since market open <
TRADING_WINDOW(default 10 minutes) - Time until settlement > 30 seconds
- Sufficient available balance
Exploits pricing divergence between Polymarket fair value and Kalshi ask prices.
Signal Logic:
polyFair_UP = (Polymarket_UP_bid + Polymarket_UP_ask) / 2
polyFair_DOWN = (Polymarket_DOWN_bid + Polymarket_DOWN_ask) / 2
polyEdge_YES = (polyFair_UP - Kalshi_YES_ask) * 100
polyEdge_NO = (polyFair_DOWN - Kalshi_NO_ask) * 100
IF polyEdge > MIN_EDGE (default 5%):
BUY the underpriced side on Kalshi
Rationale: Polymarket's higher liquidity and broader participant base provide a more efficient price discovery mechanism. When Polymarket's mid-price for a direction exceeds Kalshi's ask, the Kalshi contract is undervalued relative to cross-market consensus.
Captures guaranteed profit when Kalshi's order book misprices the combined cost of YES + NO contracts below $1.00.
Signal Logic:
combined = Kalshi_YES_ask + Kalshi_NO_ask
IF combined < $0.98:
BUY both YES and NO simultaneously
Guaranteed profit = ($1.00 - combined) per contract pair
Mechanics:
- One side always settles at $1.00, the other at $0.00
- Combined cost < $1.00 guarantees net positive return regardless of outcome
- Position is split equally: half YES, half NO
- Threshold set at $0.98 to ensure minimum 2% guaranteed return after fees
The directional strategy derives implied probabilities from a Geometric Brownian Motion (GBM) model for BTC price evolution over the remaining contract duration.
Core Formula:
P(UP) = Phi(Z)
where:
Z = move / (sigma * sqrt(T_remaining))
move = (S_current - S_open) / S_open
sigma = realized_volatility(window = contract_duration)
T_remaining = time_remaining / total_duration
Phi(x) = standard normal cumulative distribution function
S_current: Current BTC spot price from BinanceS_open: BTC price recorded at market open (captured on first discovery)sigma: Annualized realized volatility scaled to the contract's time horizonT_remaining: Fraction of total contract duration remaining (0 to 1)
The cumulative normal distribution is computed using the Abramowitz & Stegun (1964) rational approximation:
Phi(x) = 0.5 * (1 + sign(x) * Y)
where:
Y = 1 - P(t) * exp(-x^2 / 2) / sqrt(2 * pi)
P(t) = ((((a5*t + a4)*t + a3)*t + a2)*t + a1) * t
t = 1 / (1 + p * |x|)
Constants:
p = 0.3275911
a1 = 0.254829592
a2 = -0.284496736
a3 = 1.421413741
a4 = -1.453152027
a5 = 1.061405429
Bounded at [-8, 8] standard deviations to prevent numerical instability. Probabilities are clamped to [0.01, 0.99] to avoid extreme Kelly sizing.
Volatility is estimated from the trailing window of 1-second Binance price samples using the standard log-return method. The window length matches the contract's total duration (900 seconds for 15-minute contracts), ensuring the volatility estimate reflects the relevant time horizon:
r_i = ln(P_i / P_{i-1}) for each consecutive price sample
mean = (1/N) * sum(r_i)
variance = (1/N) * sum((r_i - mean)^2)
std_per_sample = sqrt(variance)
avg_interval = (t_last - t_first) / (N - 1)
samples_in_window = window_seconds * 1000 / avg_interval
volatility = std_per_sample * sqrt(samples_in_window)
Default fallback: 0.15% per 15-minute period when fewer than 10 samples are available. The price history buffer maintains 600 samples (~10 minutes at 1 sample/second).
| Condition | Behavior |
|---|---|
| No Binance price data | Return neutral probability (50/50) |
| Near-zero remaining sigma (< 0.00001) | Price momentum determines outcome (99/1) |
| Fewer than 10 price samples | Use default volatility (0.15%) |
Position sizes are determined using the Kelly Criterion, which maximizes the long-term geometric growth rate of capital:
b = (1 / (1 - p)) - 1 Odds ratio (binary payout)
q = 1 - p Loss probability
f* = (b * p - q) / b Full Kelly fraction
position_fraction = f* * KELLY_FRACTION Fractional Kelly (default 25%)
position_fraction = min(position_fraction, 0.25) Absolute cap
position_dollars = position_fraction * available_balance
position_dollars = min(position_dollars, MAX_POSITION_SIZE)
contracts = floor(position_dollars / contract_ask_price)
Full Kelly sizing is theoretically optimal but assumes perfect probability estimates. In practice:
- Model uncertainty: The GBM model is an approximation; true probabilities are unknown
- Variance reduction: Quarter-Kelly reduces variance by 75% while retaining 50% of the growth rate
- Drawdown protection: Maximum single-trade risk is capped at 25% of available balance
- Ruin prevention: Returns 0 if probability is extreme (< 1% or > 99%)
| Parameter | Default | Description |
|---|---|---|
MAX_POSITION_SIZE |
$25 | Maximum dollar amount per individual trade |
MAX_POSITIONS_PER_CONTRACT |
2 | Maximum concurrent positions on the same contract |
| Kelly fraction cap | 25% | Never risk more than 25% of available balance per trade |
| Balance validation | Every trade | Order rejected if cost exceeds available balance |
| Parameter | Default | Description |
|---|---|---|
MAX_TOTAL_OPEN_POSITIONS |
10 | Maximum concurrent open positions across all contracts |
| Exposure limit | 50% | Total position costs must not exceed 50% of total balance |
| Drawdown circuit breaker | 10% | Reduce position sizes by 50% if session loss exceeds 10% |
| Trading pause | 20% | Pause all trading for 15 minutes if session loss exceeds 20% |
| Scenario | Response |
|---|---|
| Binance WebSocket disconnects | Pause directional trading; continue arbitrage strategies |
| Binance + RedStone both fail | Full trading halt |
| Kalshi API returns errors | Stop new trades; manage existing positions to settlement |
| 5 consecutive execution failures | Emergency stop |
| Balance drops below $5 | Emergency stop |
| API authentication failure (401) | Immediate shutdown |
- Trading window: Only enter new positions within the first 10 minutes of each 15-minute contract
- Close-proximity guard: No new entries within 30 seconds of settlement
- Take-profit minimum: Sell orders require >= 30 seconds remaining to ensure fill execution
The scan interval dynamically adjusts based on realized market volatility, measured every 15 seconds:
vol = getRecentVolatility(300s window)
if vol < 0.1%: scanInterval = 3000ms (calm market)
if 0.1% <= vol <= 0.3%: scanInterval = 2000ms (normal)
if vol > 0.3%: scanInterval = 1000ms (volatile - max speed)
This reduces unnecessary API calls during quiet periods while maximizing signal capture during high-volatility windows when edges appear and decay rapidly.
Each scan cycle executes the following pipeline:
- Parallel market refresh: Fetch all active Kalshi market prices simultaneously via
Promise.allSettled() - Cache update: Write refreshed data to shared market cache (3-second TTL) for take-profit monitor
- Unrealized P&L: Calculate mark-to-market value of all open positions (no additional API calls)
- Signal generation: Run all three strategy modules against refreshed market data
- Signal ranking: Sort signals by edge magnitude (highest first)
- Execution: Execute signals sequentially with 100ms delay between same-contract trades
Runs independently every 3 seconds, reading from the shared market cache to avoid redundant API calls:
For each open position:
currentBid = market bid for position's side (YES or NO)
profitPct = (currentBid - entryPrice) / entryPrice * 100
maxGain = 1.00 - entryPrice
gainFraction = (currentBid - entryPrice) / maxGain
IF profitPct > 15% OR gainFraction > 50%:
SELL at current bid
Positions that are not exited via take-profit are held to contract settlement:
- Schedule settlement check at
closeTime + 60 seconds(buffer for Kalshi processing) - Poll order fill status -- unfilled orders are removed from tracking
- Poll market result (
yesorno) - If not yet settled, retry every 30 seconds
- Calculate P&L:
payout - (taker_fill_cost + taker_fees) / 100 - Update statistics, persist state, refresh balance
| Property | Value |
|---|---|
| Protocol | WebSocket (wss://) |
| Stream | btcusdt@bookTicker |
| Update frequency | ~200ms (tick-level) |
| Data | Best bid/ask prices |
| Failover | Binance.US -> Binance Global -> REST polling (1s) |
| Reconnection | Exponential backoff (1s initial, 30s max) |
| Price history | 600 samples (~10 min), used for volatility calculation |
| Property | Value |
|---|---|
| Protocol | HTTP REST |
| APIs | Gamma API (event discovery) + CLOB API (pricing) |
| Poll interval | 4 seconds |
| Slug pattern | btc-updown-15m-{slot_start_unix} |
| Concurrency | 3 parallel fetches per cycle |
| Cache TTL | 4 seconds |
| Deduplication | Multiple Kalshi markets mapping to same Polymarket slug are fetched once |
| Property | Value |
|---|---|
| Authentication | RSA-PSS SHA-256 signature (timestamp_ms + METHOD + path) |
| Market discovery | GET /trade-api/v2/markets?series_ticker=KXBTC15M&status=open |
| Market pricing | GET /trade-api/v2/markets/{ticker} |
| Order placement | POST /trade-api/v2/portfolio/orders (limit orders only) |
| Balance | GET /trade-api/v2/portfolio/balance |
| Price format | Cents (0-100), converted to decimals internally |
| Property | Value |
|---|---|
| Protocol | HTTP REST |
| Poll interval | 3 seconds |
| Endpoints | 2 gateway endpoints + 1 fallback API |
| Aggregation | Median of all available signer packages |
| Role | Price validation and fallback source |
The Mission Control dashboard is served via Express and communicates with the engine through Socket.io event forwarding. All state changes emit named events that are broadcast to connected clients in real time.
| Panel | Content |
|---|---|
| Connection Status | 4 indicator dots: BIN (Binance), POLY (Polymarket), KAL (Kalshi), RED (RedStone) |
| Key Metrics | BTC/USD price, Total P&L, ROI, Account Balance |
| Bot Intent | Current status, pending action, model probability, edge, spot move, volatility, time remaining |
| P&L Chart | Cumulative profit/loss over time (Chart.js, persisted to localStorage) |
| Open Positions | Ticker, side, contracts, entry price, cost, edge, strategy type |
| Active Markets | All tracked contracts with bid/ask, combined spread, countdown |
| Trade Log | Chronological trade and settlement history (last 30 entries) |
| Statistics | 14 tracked metrics (see Performance Metrics) |
BotState (EventEmitter)
│
├── price:binance → BTC price updates
├── price:redstone → Oracle price updates
├── balance → Account balance changes
├── markets → Active market list refresh
├── intent → Bot status and reasoning
├── model → Probability model outputs
├── trade → Trade execution events
├── position:open → New position added
├── position:close → Position settled/exited
├── stats → Statistics updates
├── connection:kalshi → Connection status change
├── connection:polymarket → Connection status change
└── connection:binance → Connection status change
│
└──→ Socket.io broadcast to all connected dashboard clients
The system tracks 14 statistics in real time, updated on every trade execution and scan cycle:
| Metric | Description |
|---|---|
| Total Trades | Cumulative settled trades |
| Win Rate | Wins / (Wins + Losses) as percentage |
| Avg Edge | Mean edge at entry across all trades |
| Best Trade | Largest single-trade P&L |
| Worst Trade | Largest single-trade loss |
| Volume Traded | Cumulative dollar volume |
| Trades/Hour | Trade frequency |
| Open Positions | Current concurrent position count |
| Profit Factor | Gross Wins / Gross Losses |
| Avg Win | Mean P&L of winning trades |
| Avg Loss | Mean P&L of losing trades |
| Streak | Current consecutive win (+) or loss (-) streak |
| Unrealized P&L | Mark-to-market value of open positions (updated every scan cycle) |
| Strategy Breakdown | Per-strategy win/loss record (DIR, POLY, DUAL) |
- Format: JSON file (
data/state.json) - Write strategy: Atomic (write to
.tmp, then rename) - Debounce: 5-second buffer to prevent write thrashing
- Contents: Stats, closed positions (last 100), trade log (last 500), P&L history (last 500)
- Recovery: On startup, loads persisted state and resumes accumulation
- Client-side: P&L chart data persisted to
localStoragefor dashboard refresh survival
| Component | Technology |
|---|---|
| Runtime | Node.js |
| HTTP Client | Axios |
| WebSocket | ws |
| Web Server | Express |
| Real-Time Communication | Socket.io |
| Charting | Chart.js 4.4.7 |
| Configuration | dotenv |
| Authentication | RSA-PSS (Node.js crypto) |
| Persistence | JSON file (atomic writes) |
| Database | None (stateless between restarts except persisted JSON) |
Dependencies (5 total, zero native modules):
axios ^1.6.0
dotenv ^16.3.1
express ^4.21.0
socket.io ^4.7.5
ws ^8.18.0
- Node.js 16+
- Kalshi API credentials with trading permissions (Kalshi API Settings)
- RSA private key (PEM format) for Kalshi API authentication
git clone https://github.com/your-repo/kalshibot.git
cd kalshibot
npm installcp .env.example .envEdit .env with your credentials and desired parameters (see Configuration Reference).
npm startThe Mission Control dashboard will be available at http://localhost:3333.
- Dashboard: Open
http://localhost:3333in any browser - Console: All trade executions, settlements, and engine events are logged with timestamps
- State: Persisted automatically to
data/state.json
| Variable | Type | Default | Description |
|---|---|---|---|
KALSHI_API_KEY |
string | required | Kalshi API public key |
KALSHI_PRIVATE_KEY_PATH |
path | ./kalshi_private_key.pem |
Path to RSA private key for API signing |
SERIES_TICKER |
string | KXBTC15M |
Kalshi market series to trade |
SLOT_DURATION |
number | 900 |
Contract duration in seconds (15 min) |
MIN_EDGE |
number | 5.0 |
Minimum edge (%) for Polymarket arbitrage signals |
MIN_DIVERGENCE |
number | 8.0 |
Minimum divergence (%) for directional signals |
MAX_POSITION_SIZE |
number | 25 |
Maximum dollars per individual trade |
MAX_TOTAL_OPEN_POSITIONS |
number | 10 |
Maximum concurrent open positions |
MAX_POSITIONS_PER_CONTRACT |
number | 2 |
Maximum positions per contract ticker |
TRADING_WINDOW |
number | 10 |
Only trade within first N minutes of contract |
USE_KELLY_SIZING |
boolean | true |
Enable Kelly Criterion position sizing |
KELLY_FRACTION |
number | 0.25 |
Fraction of full Kelly to use (0.25 = quarter-Kelly) |
PORT |
number | 3333 |
Dashboard server port |
This software is provided for educational and research purposes only. Trading binary contracts involves substantial risk of loss and is not suitable for all investors. Past performance does not guarantee future results.
- This system trades with real capital on regulated exchanges
- No guarantee of profitability is expressed or implied
- Users are solely responsible for their own trading decisions and capital allocation
- Always start with small position sizes and monitor system behavior closely
- Ensure compliance with all applicable regulations in your jurisdiction
Built with Node.js. No external databases. Five dependencies. One objective.