---
name: Kalshi auto-trade rollout progress
description: Current phase of the Kalshi automated order execution plan and what's been built
type: project
---

Auto-trade rollout per plan at `C:\Users\lirse\.claude\plans\serene-soaring-kazoo.md`. Goal: close the manual-execution gap (see `project_manual_execution_gap.md`) by having `daily_signals.py` place real Kalshi orders automatically.

**Why:** User's strategy is +2.48σ over 304 Kalshi bets (p=0.013) — or +2.80σ (p=0.005) if EPL is excluded (see `project_kalshi_volume_edge_study.md`). EPL alone is -1.31σ over n=19, not conclusive. Hesitation on manual placement causes selection slippage on long-shot prices that hurt disproportionately given lumpy ~25% WR. Last sig test: 2026-05-26 on `bet_history.json` thru 2026-05-25.

**How to apply:** When user references auto-trade, Kalshi API, order placement, or the rollout phases, this is the source of truth for current state.

---

## Phase 1 — Auth & read-only — DONE 2026-05-25

- `E:/soccer/kalshi_trader.py` — signs RSA-PSS, exposes `get_balance()`, `get_positions()`, `get_open_orders()`, `get_fills()`. Read-only only.
- `E:/soccer/kalshi_auth_test.py` — one-shot Phase 1 verifier.
- `C:\Users\lirse\.kalshi\private_key.pem` — outside repo on purpose.
- `.env` has `KALSHI_ACCESS_KEY` + `KALSHI_PRIVATE_KEY_PATH`.
- Verified: $95.30 cash, 3 open positions (matches Kalshi UI).
- API base: `https://api.elections.kalshi.com/trade-api/v2` (NOT trading.kalshi.com — that subdomain doesn't resolve).
- **Security note:** old Phase 1 access key + private key were pasted into chat on 2026-05-25 and ROTATED on 2026-05-26. New access key `122b061a-aa70-4f51-be58-e952fa02ff8d`. New private key delivered via `E:\soccer\bot.txt` (since deleted). Old key backup at `C:\Users\lirse\.kalshi\private_key.pem.old` — user to confirm whether old key is revoked in Kalshi UI before deleting backup.

## Phase 2 — Order placement — DONE 2026-05-25

`place_limit_order(ticker, side, count, price_cents, client_order_id=None)` and `cancel_order(order_id)` live in `kalshi_trader.py`. Hard caps enforced BEFORE HTTP: MAX_PRICE_CENTS=99, MAX_CONTRACTS_PER_ORDER=5, MAX_DOLLARS_PER_ORDER=1.00. Validated by placing 1 KFU YES @ 92¢ which filled.

## Phase 3 — Auto-execute (continuous via live_monitor) — ENABLED 2026-05-27 (all leagues, NO daily cap)

Enabled on 2026-05-27 with `AUTO_TRADE_ENABLED=True`, `AUTO_TRADE_LEAGUES=None` (all leagues), `AUTO_TRADE_DAILY_CAP=None` (no daily cap). User explicitly requested all-leagues + no-daily-cap after seeing the +29% paper ROI across 22 leagues and the +2.80sigma significance excl-EPL.

### Sizing change 2026-05-28 — proper Kelly, $5/bet, fixed $1000 notional
Until 2026-05-28 the $1 per-bet cap dominated so every bet was effectively FLAT ~$1 (Kelly machinery was decorative). User noted this ("effectively just flat betting") and asked to raise the cap to **$5/game** and base Kelly off a **fixed $1000 notional** — NOT the $1421 theoretical `kalshi_bankroll.json` tracker (drifts) and NOT the $100 `kalshi_real_bankroll.json`. Rationale: keep per-bet sizing stable as the tracker moves. Changes made (5 files in lockstep — all must move together or the hard floors silently strangle it back to ~$1):
- `daily_signals.py`: `AUTO_TRADE_PER_BET_CAP` 1.00→**5.00**; new const `KALSHI_KELLY_BANKROLL=1000.0`; scan_fixtures Kelly line uses it instead of `kalshi_available`; `place_auto_order` contract clamp `min(count,5)`→`min(count, kt.MAX_CONTRACTS_PER_ORDER)`.
- `live_monitor.py`: new const `KALSHI_KELLY_BANKROLL=1000.0`; `_kelly_stake(use_kalshi=True)` uses it instead of loaded `kalshi_bankroll`. (This is the path that actually places orders during live tracking.)
- `kalshi_trader.py`: `MAX_DOLLARS_PER_ORDER` 1.00→**5.00**, `MAX_CONTRACTS_PER_ORDER` 5→**100**. (Contract cap of 100 only binds below ~5c ask, where it under-bets in the safe direction.)
Result: ¼-Kelly of $1000 binds on most bets (e.g. 22c/4.5% edge → $3.08); cap binds only on expensive/high-edge bets. Verified by simulation. Note `kalshi_available`/`kalshi_pending` in scan_fixtures (daily_signals ~L669-670) are now DEAD (left in place).

Kalshi-side hard floors: $5/order, 100 contracts/order, 99c/share (backstop above the soft cap).

### Per-bet cap lowered to $3 + idempotency fixed 2026-05-29
- `AUTO_TRADE_PER_BET_CAP` 5.00→**3.00** (user: "set the per game cap to $3 from now on"). Hard floor MAX_DOLLARS_PER_ORDER left at $5 as backstop.
- **Idempotency bug found & fixed:** On 2026-05-29 Nice/STE got double-bet — `client_order_id=auto-1545409-away` placed twice (4@22c filled at 00:00, then 24@20c at 04:00 after the cap raise + my live_monitor restart re-detected the signal). Kalshi's own client_order_id dedupe only covers *resting* orders, so once the first FILLED it accepted the duplicate. Fix: new `_already_placed(client_order_id)` in daily_signals.py scans `auto_trade_log.json`; `place_auto_order` now skips early if a fixture+side is already logged, regardless of fill status. Residual narrow race (live_monitor + daily_signals firing same fixture within ~1s before either writes the log) still falls back to Kalshi's resting-order dedupe. The stray 24@20c STE resting order from the bug may still need manual cancel.

Files:
- `E:/soccer/kalshi_kill.py` — `on`/`off`/`status` toggles `E:/soccer/AUTO_TRADE_KILL`
- `E:/soccer/auto_trade_log.json` — append-only log of every auto-placed order
- `E:/soccer/auto_trade_errors.log` — error log (also emailed via `send_email()`)
- `daily_signals.py` config block (after `SIGNAL_LOG`): `AUTO_TRADE_ENABLED=False`, `AUTO_TRADE_LEAGUES=['E0']`, `AUTO_TRADE_DAILY_CAP=5.00`, `AUTO_TRADE_PER_BET_CAP=1.00`
- `daily_signals.place_auto_order(signal)` — safety gates: enabled → kill-switch → league whitelist → has kalshi_stake/ask → daily cap → ticker resolve → re-fetch live ask → status=active → ask <0.95 → price drift <20% → idempotent `client_order_id=f"auto-{fixture_id}-{away|home}"`. Handles both "Away"/"Home" (daily_signals) and "AWAY YES"/"HOME YES" (live_monitor) bet labels. Skips NO sides.

**Primary trigger path: `live_monitor.py::_log_pending_bets()`** — runs 24/7, so auto-orders fire as soon as a CLV signal is detected (not waiting up to 8h for next daily_signals batch). The hook imports `place_auto_order` lazily so live_monitor doesn't pay the import cost when AUTO is off. Hook also stamps `auto_order_id`/`auto_placed_at`/`auto_count`/`auto_price_cents` onto the pending entry.

**Safety-net trigger: `daily_signals.add_to_pending()`** — same hook, still active. If live_monitor missed a signal (process down) daily_signals catches it. Kalshi-side idempotency on `client_order_id` prevents double-placement if both fire.

**Email "Auto" column** in `build_html`: backfills `auto_order_id`/`auto_count`/`auto_price_cents` onto each signal dict by joining `auto_trade_log.json` on `(fixture_id, away|home)`. Shows PLACED (green), SKIPPED (red) when eligible-but-blocked, `-` (grey) otherwise.

**Nightly reconciliation: `resolve_bets.py::reconcile_auto_orders()`** — pulls last 48h of Kalshi fills, matches by `order_id`, writes back `filled_count`/`filled_dollars` onto each log entry, flags UNFILLED (logged ≥1h ago, no fill) and ORPHAN (fill on a ticker we auto-traded but order_id missing from log). Orphan_fills filtered to only show fills on tickers we auto-traded — manual fills on other tickers are silent. Adds `[RECON-ALERT]` to subject if anything weird. Injects HTML table into nightly email.

Verified: all safety paths return None cleanly; live reconciler matched real KFU fill (1@$0.92).

**To enable:** edit `daily_signals.py` line ~56 set `AUTO_TRADE_ENABLED = True`. To panic-stop: `python kalshi_kill.py on`. Then restart `live_monitor.py` so it picks up the flipped flag (or just wait — the import happens fresh each `_log_pending_bets` call, so restart not strictly required).

**Still TODO:**
- 7-day soak test once user enables
- Optional: pass `expiration_ts=kickoff_ts` through `place_limit_order` (kalshi_trader currently ignores it)
- Optional: handle UTC vs ET cap window mismatch (currently $5/UTC day; user noted 8PM ET resets cap mid-evening)

## Phase 4 — Sub-ask limits + scale — NOT STARTED

## Real-money bankroll

`E:/soccer/kalshi_real_bankroll.json` ($100) is the two-track real bankroll for auto-trade sizing. `kalshi_bankroll.json` ($1421) remains the theoretical tracker. Don't conflate them.
