Live trading and risk

Every real order goes through one in-process guard on your machine, with hard limits, a kill switch, reconciliation and an audit log. Kalshi from the command line; Polymarket US from the library.

On this page

Every order the runtime sends to an exchange goes through one in-process gate, the live guard (langbot.live.LiveGuard). A strategy cannot reach the exchange around it. The guard is not tied to one venue: Kalshi plugs in as KalshiVenue, Polymarket US as PolymarketUSVenue.

Terminal
langbot live run my_strategy.py                          # Kalshi demo (the default)
langbot live run my_strategy.py --dry-run                # check and log every order, send nothing
langbot live run my_strategy.py --env prod --yes-real-money
langbot live halt [--cancel-orders] [--reason "..."]     # stop new orders now
langbot live resume                                      # clear the halt
langbot live status                                      # halt state + latest audit lines
langbot live limits [--strategy Name] [--env prod]       # the limits in force

langbot run --mode live still works and goes through the same guard; langbot live run is the recommended entry.

Environments

Kalshi demoKalshi prod
selected bydefault, --env demo--env prod --yes-real-money
RESTexternal-api.demo.kalshi.coexternal-api.kalshi.com
moneyplay moneyreal money
empty series_allowevery seriesnothing

A Kalshi client built for prod refuses to send or amend an order unless it was created with allow_prod_orders=True (which only --yes-real-money does). The kalshi.env value in config.toml never switches langbot live run to prod. Polymarket US has no demo, so its adapter always counts as prod.

Hard limits (~/.config/langbot/live.toml)

Checked before every send, in this order. If the file is missing, the defaults below apply: the smallest limits, never "no limit". An unknown key is an error, so a typo cannot drop a limit. [strategy.<Name>] (the strategy's class name) can tighten [account] but never loosen it.

KeyDefaultRejects an order when
series_allow / series_denydemo ["*"], prod [] / []the series (ticker before the first -) is not allowed, or is denied
min_price / max_price0.02 / 0.98the price is outside the band
max_contracts_per_order5the order is larger
max_order_usd5.00contracts × risk per contract is larger (risk is price for a buy of YES, 1 − price for a sell)
(fresh book)[halt] stale_book_s = 10there is no book for the market from the last 10 s
max_price_through_cents3a bid is more than 3¢ above the best ask, or an ask more than 3¢ below the best bid
max_daily_loss_usd10.00today's realised + marked loss has reached the limit (this also halts)
max_position_contracts10position + resting orders on that side + this order would exceed it (per market)
max_position_usd10.00the same in dollars at risk
max_total_exposure_usd25.00dollars at risk over all markets (positions + resting orders + this order) would exceed it
max_open_orders5that many orders are already resting
max_orders_per_minute10that many orders were sent in the last 60 s

An order that only shrinks a position toward zero skips the position and exposure checks. Fills an ack reports before the fill message arrives count as exposure at $1 a contract until they are confirmed. The daily loss is the change in equity (cash + positions at the book mid) since 00:00 UTC or the session start. Fees are included. A position held from earlier takes its first mark as the baseline, so old losses do not count as today's.

TOML
[account]
max_contracts_per_order = 5
max_order_usd = 5.0
max_position_contracts = 10
max_position_usd = 10.0
max_total_exposure_usd = 25.0
max_open_orders = 5
max_orders_per_minute = 10
max_daily_loss_usd = 10.0
max_price_through_cents = 3
series_allow = ["KXFEDMENTION"]

[strategy.MentionSniper]
max_order_usd = 2.0

[halt]
stale_book_s = 10
reconcile_interval_s = 15
max_consecutive_api_errors = 3
max_clock_skew_s = 3.0
cash_tolerance_usd = 0.05
cancel_on_halt = true

Kill switch and automatic halts

A halt is a file, ~/.config/langbot/live/HALT. While it exists, no live process on the machine sends an order. Every order checks it, and running processes poll it every 0.25 s. langbot live halt writes it, and --cancel-orders also makes running processes cancel their resting orders. The guard writes the same file when it halts itself, so a halt survives a restart. Only langbot live resume clears it. A running process that sees the file go re-reads positions, orders and cash from the exchange before it trades again.

The guard halts on its own, and by default cancels resting orders, when:

  • the daily loss limit is reached;
  • reconciliation finds a mismatch with the exchange that is still there one second later;
  • 3 API errors in a row happen;
  • a book goes stale: a market where we have resting orders has had no update for 10 s and a REST refresh fails;
  • the clock is skewed: the local clock is more than 3 s off the exchange's Date header (Kalshi);
  • an order's state is unknown: a send timed out and the order could not be looked up.

Reconciliation

At start, and on every resume, the guard takes Kalshi's GET /portfolio/positions (position_fp, market_exposure_dollars), GET /portfolio/orders?status=resting and GET /portfolio/balance (balance_dollars) as the truth. Every 15 s it compares what it believes (positions, resting order ids and remaining counts, cash) with the same three calls. A mismatch is logged with both sides, re-checked once after 1 s to absorb a fill message still in flight, and then halts. Trading the same account by hand, or from a second process, will halt the guard. Use a separate account or subaccount for the bot.

Idempotent sends

Each order gets a client order id before its first send, and every retry reuses it unchanged. On a timeout, a dropped connection or a 5xx, the guard waits 1 s and looks the order up by that id. It resends only when the exchange confirms the order is not there. If the lookup itself fails, it halts instead of guessing. A 409 (duplicate id) is looked up and never resent. A 4xx means the order was not placed. Polymarket US has no server-side client id, so an ambiguous submit there always halts.

Audit log

~/.config/langbot/live/audit-YYYY-MM-DD.jsonl: append-only, one fsynced JSON line per intent, limit rejection, send, ack, uncertain send, lookup, venue rejection, fill, cancel, reconciliation, halt and resume, each with a millisecond timestamp and a session id. Keys that look like credentials are masked before they are written.

  1. langbot live run strategy.py --dry-run on demo, then on demo for real, for at least one full event. Read the audit log.
  2. Backtest over at least 10 archived events. Look at max drawdown and no-trade rate, not just P&L.
  3. --env prod --dry-run for one event: the same checks against real books, nothing sent.
  4. --env prod --yes-real-money with max_order_usd = 1, max_daily_loss_usd = 5 and an allowlist of one series.
  5. Raise limits only after a week of live fills behaves like the backtest.

Polymarket US

langbot live run trades on Kalshi. Polymarket US is available from the library: PolymarketUSClient for orders and PolymarketUSVenue, which plugs into the same live guard.

US residents trade Polymarket on Polymarket US: a CFTC-regulated exchange (QCX LLC, doing business as Polymarket US) with USD accounts and identity checks. It is a separate venue from the international Polymarket, which does not accept new orders from the US. The SDK's PolymarketUSClient connects to Polymarket US only, and you trade with your own account and API key.

Account and key

  1. Install the Polymarket US app, create an account and finish identity verification (legal name, address and SSN, and sometimes an ID and a selfie). Eligibility, including whether your state is supported, is checked during this step.
  2. Sign in at polymarket.us/developer with the same sign-in method you used in the app, and create an API key. You get a Key ID and a Secret Key. The secret is shown only once.
  3. If you plan to send more than about 20 requests a second, email Polymarket US support with your use case to ask for higher limits.

Configure

VariableMeaning
POLYMARKET_KEY_IDyour Key ID
POLYMARKET_SECRET_KEYyour Secret Key (base64)
POLYMARKET_US_ALLOW_ORDERS=1turns on order entry; leave it unset to read only

Keep these values in your own environment or secret store, and never commit them. You don't need a key for order books and market data.

Python
from langbot.polymarket_trading import PolymarketUSClient

async with PolymarketUSClient(ticker_map={"pm_516710": "your-us-market-slug"}) as pm:
    book = await pm.get_orderbook("pm_516710")  # public, no key needed
    balance = await pm.get_balance()  # {"balance": cents, "buying_power_dollars": ...}
    order = await pm.create_order(
        "pm_516710", "bid", 10, 0.42, post_only=True, client_order_id="my-run-001"
    )
    await pm.cancel_all("pm_516710")

Name the market

Polymarket US markets have their own slugs, for example paccc-usho-midterms-2026-11-03-dem. Langbot's pm_<id> tickers refer to markets on the international venue, and Polymarket doesn't publish a mapping between the two. You can pass a US slug directly, or map a pm_ ticker to the US market you mean with ticker_map. Before you map them, check that the two markets have the same rules. An unmapped pm_ ticker raises an error instead of being guessed.

How it maps to the Kalshi client

PolymarketUSClient uses the same method names as KalshiClient, so the strategy runtime and risk rails can drive either one.

KalshiClientPolymarketUSClientPolymarket US endpoint
get_markets, get_marketsameGET /v1/markets, GET /v1/market/slug/{slug} (public)
get_orderbook → BooksameGET /v1/markets/{slug}/book (public)
get_balance (cents)same, plus balance_dollars, buying_power_dollarsGET /v1/account/balances
get_positionssame keys (ticker, position_fp, market_exposure_dollars, realized_pnl_dollars)GET /v1/portfolio/positions
get_orders(status="resting")open orders onlyGET /v1/orders/open
create_order(ticker, side, count, price, tif, client_order_id, post_only)same, plus optional intent=POST /v1/orders
amend_ordersame, then re-reads the orderPOST /v1/order/{id}/modify
cancel_ordersamePOST /v1/order/{id}/cancel
cancel_allsamePOST /v1/orders/open/cancel

There are some differences to know about:

  • Sides. Each market trades a single YES instrument, and every price is a YES price. A bid buys YES. An ask sells YES that you hold. If you hold none, an ask buys NO at 1 − price, which is the same position. If you hold less YES than the size of your ask, the order is refused, so split it or pass intent= explicitly.
  • client_order_id. Polymarket US has no idempotency key. The client tracks ids locally: if you reuse an id that already placed an order, you get that order back and nothing new is sent. An order whose outcome is unknown, such as after a timeout or a 5xx error, raises PolymarketUSAmbiguousOrder and is not retried. Check get_orders() first, then call pm.registry.resolve(...) or pm.registry.forget(...). Pass ClientOrderIdRegistry(path) to keep the ids across restarts.
  • Time in force. Supported values are good_till_canceled, fill_or_kill and immediate_or_cancel. post_only=True becomes the exchange's maker-only flag.
  • Fees. Polymarket US publishes its own fee schedule; it is the authority.
  • Order flag. Orders are flagged as placed by an automated system. Pass manual=True only for orders a person enters by hand.

Things that will hurt you

  • Venue order delays. Kalshi may delay orders on live-event markets (Rule 13.1(i)). Polymarket runs taker delays of 50 ms to 3 s. A strategy that only works with sub-second fills is fragile. Prefer resting orders and breadth.
  • Settlement during a session. A settled market leaves the positions list and moves cash, so reconciliation halts. Resume after you check it.
  • Rules nuance. "Any company representative" includes the operator, plurals usually count, and a phrase said by an analyst in Q&A usually does not. get_market_rules summarises this, but read the actual rules text before trading.
  • ASR confidence. Homophones happen ("Blackwell" vs "black well"). Gate on confidence ≥ 0.9 and, where it matters, on speaker.name.
  • Clock drift. Keep NTP on. The guard halts past 3 s of skew, and Kalshi rejects badly skewed signatures.

Nothing here is financial advice. You can lose money, and in live-event markets you can lose it in under a second.