Strategy · Kalshi · Market Making

Kalshi Market Making Strategies (Practical Python Guide)

Published May 20, 2026 · 18 min read · By WatchDog Bot Team

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

  1. What market making actually is
  2. Why Kalshi is unusually friendly to retail MMs
  3. The math of expected edge
  4. Inventory management — the real risk
  5. Adverse selection & how to avoid getting picked off
  6. A working Python implementation
  7. Pitfalls that kill retail market makers
  8. When (and how) to scale up

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:

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:

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:

  1. Your bid gets hit by a seller (probability roughly 1/3). You buy at 42¢, fair value is 44¢. Expected profit: +2¢.
  2. Your ask gets lifted by a buyer (probability roughly 1/3). You sell at 46¢, fair value is 44¢. Expected profit: +2¢.
  3. 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:

PositionFair valueYour bidYour 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:

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:

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:

When (and how) to scale up

If a single-market bot is consistently profitable over a few weeks of live (not paper) trading:

  1. 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.
  2. Increase position limits incrementally. Don't 10x overnight. Watch how P&L scales — usually slightly sub-linear due to widening adverse selection at scale.
  3. Add better fair-value signals. NOAA feeds for weather. ESPN scores for sports. News classifiers for political markets. Each one tightens your edge.
  4. 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 →

Related reading