Files
2026-07-06 15:28:07 -04:00

8.4 KiB
Raw Permalink Blame History

Operator's field guide

Practical notes for running this bot live — where to look, what breaks, and what to build next. Written from a real supervised session (Newsom 2028 + Romania PM), so the failure modes below are ones that actually bit us, not hypotheticals.

Running & watching

  • Run exactly ONE engine. uv run polymaker run. If you background it, verify with pgrep -f "polymaker run" and grep -c engine_started <logfile> — two engines on the same wallet race each other and double-order. (The process tree is zsh→uv→python, so ~3 procs but only one engine_started line.)
  • Capture the log to a file (… > live.log 2>&1). Everything meaningful is a structured line: requote … regime=… fv=… place=… cancel=… tox=… flowz=…, fill …, meta_refreshed, market_ws_dropped, market_halted_by_meta.
  • Watch the stream, don't poll snapshots. Follow the log and react the instant something fires — a fill, regime=EVENT/HALTED/REDUCE_ONLY, tox=0.1+, or any Traceback/quoter_error/divergence. Polling every N minutes misses the fill + quick move that happens between checks. (See session/watch.py / monitor.py from the session for a working pattern.)
  • Liveness ≠ quiet. A silent log can mean "healthy and resting" OR "hung." Do a real health probe periodically: open orders on the exchange, positions on-chain, and that each order is inside the reward band.

Where it goes wrong (ranked by how much it cost us)

  1. Adverse selection on thin/gapped books — the big one. On a market with a sparse book (e.g. a 14¢ air-pocket below the touch), someone can shove the price, fill your resting bid, and leave you holding a directional bag. We rested $75 / 159-share orders on Romania; a seller gapped the market 0.478→0.442 and filled our whole bid → an oversized $5 long we never wanted. → On thin/manipulable markets, rest the minimum reward-qualifying size (the market's rewardsMinSize), not larger. A fill should be small and disposable. Big size only belongs on deep books you can offload into.
  2. Orders that don't actually score. To earn rewards an order must be rewardsMinSize shares AND within rewardsMaxSpread of the midpoint. Below-min orders earn zero — easy to miss. rewardsMinSize also changes (we saw 50→100 live); a stale catalog value silently mis-sizes you. Let the engine refresh metadata from Gamma at startup, and rescan periodically.
  3. False regime signals on quiet markets. Two we hit and fixed:
    • False HALT: staleness measured "time since last book update," so a quiet market halted itself into zero rewards. Gate on the WS connection liveness instead (it pings every 5s).
    • False TRENDING: on a market trading ~1×/hour, microprice jitter spikes the short/long vol ratio → TRENDING → size halved → half the reward, for a trend that doesn't exist. Raise trend_vol_ratio on thin markets.
  4. Churn. Reprice/resize thresholds too tight → cancel/replace every few seconds → you lose queue position and get sampled out of rewards. Make it sticky: raise reprice_ticks, resize_frac, and the trend thresholds. Resting > reacting for a reward farmer.
  5. Fine-tick illusion. On 0.001-tick markets (prices like 19.3¢) per-share spread is fractions of a cent — profit is rewards + rebates, not spread capture. A "+$4 exit" is noise; don't let it set your strategy.
  6. Stale reads. The positions API (data-api) lags; during a fast move it showed +$1 while the real book was $5. Trust the live book / on-chain, not the position endpoint mid-move.

Getting out (exits & closing) — learned the hard way

  • You can't cleanly exit a large position on a thin market. This is the flip side of the min-size rule: small fills unwind easily, big ones don't. To close 159 Romania YES we either market-dumped through the gap (VWAP craters ~0.42, then 0.33) or rested a limit near mid that didn't fill as the market drifted away from us. We ended up eating ~2 ticks of slippage to get flat. If you can't exit a size without moving the book, you never should have been that size.
  • Taker fees hit on market-order exits. Maker fills (resting) pay zero, but closing with a market/marketable order is a taker — the Newsom close reported a gross 126.97 but only 122.86 landed (~$4.1 fee); Romania ~$1.5. Budget for it.
  • Don't dump into the gap. On a gapped book, a plain market/FAK sell fills straight through the air-pocket. Floor it: sell only into the near bids and stop before the gap, even if it leaves a small tail to work off.
  • Separate the bot's trades from your own. Our cash showed $70, which looked alarming — but the bot only lost ~$15.50; the rest was manual World Cup sports bets on the same wallet. When tallying bot PnL, filter to the exact tokens the bot traded (it already scopes untracked positions out of its own state/exposure — do the same in your accounting). Session result: Newsom $5.27, Romania $10.24 (one bad adverse fill), total ≈ $15.51 — over-sizing a thin market cost ~$10 of that, which min-size would have made ~$3.

Economics (set expectations)

  • Liquidity rewards = a fixed daily pool split by your Qmin share. Diminishing returns — past ~a third of the pool you're fighting yourself. Sweet spot is a small-to-mid size on a market with a real pool and light competition.
  • Maker rebates = 25% (most markets) or 20% (high-fee) of taker fees, uncapped and volume-driven. Modest on quiet markets, dominant on busy ones — but you only earn them on orders that fill, so they come coupled with inventory/adverse-selection risk.
  • Taker fee = rate × p(1-p) per share. rate is 0.04 in the fee schedule and the client library treats it as 4% (≈23% of notional). Verify against the UI — if it actually shows 0.4%, every rebate estimate is 10× too high.
  • Backtest against historical L2 order-book data — the highest-leverage next step. Every parameter we tuned this session (churn thresholds, trend_vol_ratio, event sensitivity, min-size, exit urgency) was fit by intuition on live money. Instead: record the market WS feed (book snapshots + deltas + trade prints) to a dataset, then replay it through the pure quoting/regime core (strategy/quoting.py and strategy/regime.py are already I/O-free and deterministic — designed for exactly this) to simulate fills, markouts, rewards, and PnL. Then sweep/optimize params per market archetype (deep-liquid vs thin-gappy) offline. Model the two things that actually decide profit: fill probability (are we at the touch when a taker crosses?) and adverse selection (where's the price 3060s after a fill?). This turns the live losses above into a one-time data-collection cost.
  • Confirm the fee rate (4% vs 0.4%) from a real taker fill / the UI — it 10×'s all rebate math.
  • Per-fill markout logging + a reward-band watchdog alert (fires if a resting order drifts outside the band, i.e. stops scoring). We were half-blind to toxicity until we added tox/flowz to the requote line — go further.
  • Refine rebate estimates with each shortlisted market's actual /trades volume (the scanner uses Gamma's 24h figure, which overstates CLOB flow).
  • Wire the alerts webhook (Alerter) before any unattended run.
  • Market selection: rank by reward pool + rebate pool, prefer deep books and light competition; treat thin gapped books as min-size-only.
  • Exit tuning per market; consider re-enabling the merge path for hedged YES+NO pairs (currently gated off for deposit wallets).

Quick reference

uv run polymaker scan          # discover + rank markets -> markets.csv, state.db
uv run polymaker doctor        # preflight: wallet, clock, WS, balances
uv run polymaker moneydoctor   # live buy/sell/limit self-test (spends a little)
uv run polymaker run           # start the maker (ONE instance)
uv run polymaker cancel-all    # pull every resting order

Config lives in config/*.toml: config.toml (wallet/engine/risk), strategy.toml (named profiles), markets.toml (trade list). A heartbeat dead-man switch cancels all orders within ~10s if the engine dies — but that is a safety net, not a reason to leave it unwatched on a thin market.