Kalshi Market Making Strategies (Practical Python Guide)
Market making — quoting both sides of the order book and earning the spread — is one of the few strategies in finance where small traders can have an edge. On Kalshi, where spreads are often 5–15 cents wide and most participants are retail directional traders, the conditions are unusually favorable. This guide covers what the strategy is, when it works, the math, and a working Python implementation.
What we'll cover
01What market making actually is
A market maker simultaneously offers to buy (a bid) and sell (an ask). Anyone who wants to trade immediately pays the spread between them. The market maker collects that spread, over and over, on every trade they participate in.
Concrete example on Kalshi:
- Market: "Will NYC high temperature exceed 75°F today?" (binary YES/NO contract)
- Current orderbook: YES bid 38¢ / YES ask 50¢ (12¢ wide spread — very wide for a real market)
- You post YES bid at 42¢ and YES ask at 46¢ (4¢ wide — tighter than the market)
- A retail trader walks in wanting to sell YES — they hit your 42¢ bid. You now own 1 contract @ 42¢.
- 5 minutes later, another retail trader walks in wanting to buy YES — they hit your 46¢ ask. You're flat with 4¢ profit per contract.
That's the entire premise. You don't predict whether YES will resolve. You don't care about the weather. You profit from the flow of impatient traders.
02Why Kalshi is unusually friendly to retail MMs
Market making is a brutally competitive business on Binance or NYSE. The Citadel desks have colocated servers and microsecond execution. You cannot win a speed race against them.
Kalshi is different because:
- Low institutional participation. Big firms haven't fully moved into prediction markets at scale yet — the volumes are too small relative to their capital.
- Wide native spreads. Many Kalshi markets have 5–15¢ spreads — that's 5–15% of contract value. Compare to BTC/USDT where the spread is fractions of a basis point.
- Latency-tolerant. Most Kalshi markets move slowly. A 200ms round-trip from your laptop is not a competitive disadvantage on a market that ticks every few minutes.
- Bounded payoffs. Every Kalshi contract resolves at either 0¢ or 100¢. Your worst-case loss per contract is capped, which makes risk management mathematically clean.
- Predictable retail flow. When a market is in the news, a flood of one-sided directional orders comes in. Those orders cross spreads. That's your fuel.
Reality check: "Friendly" doesn't mean "easy." It means there's room for a careful retail trader to extract pennies. There are still ways to lose money fast — see section 7.
03The math of expected edge
Suppose you quote bid 42¢ / ask 46¢ on a market that's "fairly" priced at 44¢ (where 'fair' means the true probability of YES resolving).
Three things can happen on your next fill:
- Your bid gets hit by a seller (probability roughly 1/3). You buy at 42¢, fair value is 44¢. Expected profit: +2¢.
- Your ask gets lifted by a buyer (probability roughly 1/3). You sell at 46¢, fair value is 44¢. Expected profit: +2¢.
- Neither happens for some time, then the market moves and you're left holding old quotes (probability roughly 1/3). This is where you lose money — see adverse selection below.
If you make 100 round trips at 4¢ spread, gross edge is 4¢ per trade × 100 = $4 per contract notional. Subtract Kalshi fees (a few basis points), the cost of being wrong on directional moves, and your overhead — and you net somewhere south of that. But it scales linearly with volume.
04Inventory management — the real risk
The naive strategy ("quote both sides forever") fails because fills are not symmetric. You'll often get filled on one side three or four times in a row before any flow comes back the other way. Now you're holding 4 contracts at an average cost of 42¢ — and the market just moved to 35¢.
The fix is inventory-aware pricing: as your position grows in one direction, you skew your quotes to encourage flow that flattens you out.
A simple skew rule:
def quotes(fair_value, position, max_position=10, half_spread=2):
# fair_value: the price you believe is "correct" (e.g., midpoint of best bid/ask)
# position: your current inventory (signed; positive = long YES)
# max_position: hard limit; refuse to add inventory beyond this
# half_spread: cents on each side of fair (4-cent total spread)
# Skew quotes against your inventory
# If you're long, lower BOTH quotes (you'll sell more, buy less)
skew = (position / max_position) * half_spread
bid = fair_value - half_spread - skew
ask = fair_value + half_spread - skew
# Hard limits — never add to a maxed-out position
if position >= max_position:
bid = None # don't quote bid
if position <= -max_position:
ask = None # don't quote ask
return bid, ask
Example with this rule:
| Position | Fair value | Your bid | Your ask |
|---|---|---|---|
| 0 (flat) | 44¢ | 42¢ | 46¢ |
| +5 (long) | 44¢ | 41¢ | 45¢ |
| +10 (max long) | 44¢ | — | 44¢ |
| -5 (short) | 44¢ | 43¢ | 47¢ |
At max long, your bid disappears entirely and your ask is right at fair value, making it nearly certain to be lifted. The position naturally unwinds.
05Adverse selection — how to avoid getting picked off
"Adverse selection" is when the only people hitting your quotes are people who know something you don't. You quote 42/46 thinking fair is 44. A news event drops. Suddenly someone hits your ask at 46¢ because they know the real fair value is now 60¢. You sold at 46¢. The market is at 60¢. You just lost 14¢ per contract.
Three layered defenses:
1. Update your fair-value estimate fast
Your quotes are only as good as your fair-value estimate. Use the midpoint of the current orderbook as a starting point, but adjust based on:
- Recent trade prints (a series of YES trades at the ask suggests buyers are aggressive)
- Time-weighted average price over the last few minutes
- External signals — for weather markets, NOAA forecast feeds
2. Withdraw quotes during news events
If you can't update fast enough, get out of the way. Many MMs pull their quotes for the first 30 seconds after any major news headline touches the market's topic. Yes, you miss some easy fills. You also avoid the catastrophic ones.
3. Inventory limits double as adverse-selection protection
If your max position is 10 contracts, the worst single-event loss is bounded: 10 contracts × 100¢ per contract = $1,000 maximum. That's painful but survivable. With no limits, a single bad fill sequence can cost you your entire account.
06A working Python implementation
Putting it together. This bot:
- Watches one Kalshi market
- Re-quotes every 10 seconds based on current orderbook midpoint
- Skews quotes against current inventory
- Cancels and re-places quotes (rather than modifying) for simplicity
- Has a hard inventory limit
import time
import wd
from kalshi_client import KalshiClient # from /blog/how-to-build-a-kalshi-trading-bot-with-python
# ── Configuration ───────────────────────────────────────────
TICKER = "KXHIGHNY-26MAY20-T75"
HALF_SPREAD = 2 # cents
MAX_POSITION = 10 # contracts
QUOTE_SIZE = 1 # contracts per quote
RE_QUOTE_SECS = 10
MIN_SPREAD = 4 # don't quote if market is already tighter than this
# ── Setup ───────────────────────────────────────────────────
conn = wd.connection("Kalshi")
client = KalshiClient(conn.api_key, conn.api_secret_path)
position = 0
my_orders = [] # list of (order_id, side, price)
wd.log.info("MM bot starting on %s — half_spread=%d, max_pos=%d",
TICKER, HALF_SPREAD, MAX_POSITION)
def cancel_all():
for order_id, _, _ in my_orders:
try:
client.delete(f"/portfolio/orders/{order_id}")
except Exception as e:
wd.log.warning("cancel failed for %s: %s", order_id, e)
my_orders.clear()
def place_quote(side, price):
body = {
"ticker": TICKER, "action": "buy", "side": side,
"count": QUOTE_SIZE, "type": "limit",
f"{side}_price": price,
"client_order_id": f"mm-{side}-{int(time.time()*1000)}",
}
if wd.is_demo():
wd.log.info("[DEMO] would place %s @ %d", side, price)
return
resp = client.post("/portfolio/orders", body)
my_orders.append((resp["order"]["order_id"], side, price))
wd.log.info("Placed %s @ %d (order %s)", side, price, resp["order"]["order_id"])
def update_position():
"""Recalculate position from Kalshi's source of truth."""
global position
positions = client.get("/portfolio/positions").get("market_positions", [])
for p in positions:
if p["ticker"] == TICKER:
position = p.get("position", 0)
return
position = 0
while True:
try:
# 1. Cancel existing quotes
cancel_all()
# 2. Refresh position from Kalshi
update_position()
# 3. Compute fair value from current orderbook
market = client.get(f"/markets/{TICKER}")["market"]
yes_bid = market["yes_bid"]
yes_ask = market["yes_ask"]
if yes_bid == 0 or yes_ask == 0 or yes_ask - yes_bid < MIN_SPREAD:
wd.log.info("Spread too tight (%d/%d) — sitting out", yes_bid, yes_ask)
time.sleep(RE_QUOTE_SECS)
continue
fair = (yes_bid + yes_ask) / 2
# 4. Compute skewed quotes
skew = (position / MAX_POSITION) * HALF_SPREAD
bid = round(fair - HALF_SPREAD - skew)
ask = round(fair + HALF_SPREAD - skew)
# 5. Sanity check
bid = max(1, min(99, bid))
ask = max(1, min(99, ask))
wd.log.info("pos=%+d fair=%.1f → bid %d / ask %d",
position, fair, bid, ask)
# 6. Place quotes, respecting inventory caps
if position < MAX_POSITION:
place_quote("yes", bid) # buy YES at bid
if position > -MAX_POSITION:
place_quote("no", 100 - ask) # buying NO @ X = selling YES @ (100-X)
time.sleep(RE_QUOTE_SECS)
except Exception as e:
wd.log.error("MM loop failed: %s", e)
time.sleep(30)
Note on YES vs NO: Kalshi only lets you place buy orders, not sell. To "sell YES @ 46¢" you actually buy NO @ 54¢ (since YES + NO = 100¢ on any binary market). The code above handles this — your "ask" on YES becomes a buy NO order at 100 - ask.
07Pitfalls that kill retail market makers
From experience watching real users blow up — the fastest ways to lose money in this strategy:
- Quoting in markets that are about to resolve. Last 5 minutes before settlement, spreads naturally collapse and any inventory you're holding gets marked at near-binary values. Stop quoting at least 30 minutes before resolution time.
- No position limit. The first time you get hit on one side 8 times in a row, you'll learn why this matters.
- Re-quoting too fast. If you cancel/replace every 100ms, you'll pay Kalshi's API rate limits and your latency will hurt you. Every 5–30 seconds is the right zone for most retail markets.
- Trusting orderbook midpoint as "fair value". If the spread is 30¢ wide, the midpoint is nearly meaningless. Use volume-weighted average price over recent trades, or external signals.
- Ignoring overnight risk. If you're long 10 contracts at end of day and the resolution is tomorrow, you're holding directional exposure overnight. Either flatten or size for it.
- Running multiple bots on the same market. They'll trade with each other and eat fees. Make sure each market has exactly one MM bot.
When (and how) to scale up
If a single-market bot is consistently profitable over a few weeks of live (not paper) trading:
- Add more markets. The strategy is largely uncorrelated across markets — running 20 bots on 20 markets gives you much more steady aggregate P&L than 1 bot on 1 market.
- Increase position limits incrementally. Don't 10x overnight. Watch how P&L scales — usually slightly sub-linear due to widening adverse selection at scale.
- Add better fair-value signals. NOAA feeds for weather. ESPN scores for sports. News classifiers for political markets. Each one tightens your edge.
- Consider co-location. If you're running 50+ contracts of inventory regularly, the latency improvement of running on AWS us-east-1 (close to Kalshi) starts to matter.
Market making isn't about being smart. It's about being disciplined enough to do the same thing 10,000 times without breaking.
Run your market maker on WatchDog Bot
Built-in Kalshi connection, isolated venvs, real-time logs, AI-powered debugging. Free trial, no credit card.
Start Free Trial →
WatchDog Bot