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

133 lines
8.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
## Recommended next directions
- **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
```bash
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.