Files
poly-maker/docs/scoping/01-current-state.md
T
Daniel Sapkota c7b686116e Getting there
2026-07-05 17:50:04 -04:00

155 lines
9.0 KiB
Markdown
Raw 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.
# 01 — Current State Audit (v1)
Honest inventory of what exists today, what's broken, and what dies in v2.
This is the baseline the rest of the scoping docs build on.
## What v1 is
A single-process Python market maker (~2,700 LOC) plus a separate market-scanner
script, glued together through a **Google Sheet** that acts as config store,
market database, and dashboard at the same time.
```
main.py entry point: REST polling thread + 2 websockets
trading.py the entire strategy (perform_trade, 470 lines)
update_markets.py scanner: crawls all markets, writes to Google Sheets hourly
update_stats.py writes account stats/earnings back to Google Sheets
poly_data/ client wrapper, global mutable state, WS handlers, book
poly_utils/ Google Sheets access (incl. a read-only CSV-export hack)
data_updater/ scanner internals (reward math, volatility fetch)
poly_stats/ earnings/positions summary -> Sheets
poly_merger/ Node.js subprocess to merge YES+NO via Gnosis Safe
positions/*.json ad-hoc risk-off state files written next to the code
```
### Data flow today
1. `update_markets.py` (separate process, ideally separate IP) crawls
`clob.polymarket.com/sampling-markets` page by page, then fetches **one order
book per market** via REST plus one `prices-history` call per market to
compute volatility — thousands of sequential REST calls per scan, ~hourly.
Results are pasted into the "All Markets" / "Volatility Markets" sheets.
2. A human picks rows into the "Selected Markets" sheet and tunes the
"Hyperparameters" sheet.
3. `main.py` re-reads the sheet **every 30 seconds** (network call to Google in
the hot path), polls positions/orders REST every 5s in a background *thread*,
and maintains order books from the market websocket in the asyncio loop.
4. Every `book`/`price_change`/user event spawns `perform_trade(market)` as a
new asyncio task, serialized per market by an `asyncio.Lock`.
## What's structurally wrong
### Concurrency model
- **threading + asyncio mixed**: a daemon thread mutates `global_state` (dicts of
positions/orders) while asyncio tasks read them. No coherent locking
(`global_state.lock` exists but is essentially unused).
- **Blocking I/O inside the event loop**: `py-clob-client` and `requests` are
synchronous. Every order placement/cancel blocks the loop — while it's
signing + POSTing, the bot is blind to every other market's book updates.
- **`perform_trade` holds the per-market lock through a `gc.collect()` and a
hard-coded `await asyncio.sleep(2)`** (trading.py:470). Every quote decision
on a market therefore takes ≥2s and events pile up behind the lock. This
alone caps reactivity at ~0.5 Hz per market in a game where queue position
and reaction to sweeps decide profitability.
- Event storm handling is "spawn a task per WS message" — no coalescing, no
debounce; the lock queue grows unboundedly under load.
### State management
- One module of **global mutable dicts** (`global_state.py`) shared across a
thread and dozens of tasks. Position tracking is a merge of three sources
(data-api REST, user WS fills, on-chain balance) reconciled with the
`performing` set + 15-second staleness hack — a hand-rolled, race-prone
answer to "did my fill land yet?".
- `set_order()` in `poly_data/data_utils.py:136` **overwrites both sides** of a
token's order record with a dict containing only the side that just updated —
the other side's `{price, size}` silently vanishes until the next 5s REST poll.
- Local order book is keyed by condition_id, stores only the YES token book,
derives the NO view by `1 - price`. Fine in principle, but book resync after
WS reconnect relies on the server re-sending a `book` snapshot; there's no
sequence-gap detection or hash check (the WS actually provides a book `hash`).
### Google Sheets as infrastructure
- Config store, market DB, ops dashboard, and inter-process message bus are all
one spreadsheet. It's rate-limited (Sheets API quota), slow (hundreds of ms
per read), needs service-account credentials, and pulls a network dependency
into the trading loop. The "read-only mode" fallback in
`poly_utils/google_utils.py` literally guesses gid numbers 04 against a CSV
export URL.
- The scanner and the bot communicate *through* the sheet, so market metadata
in the hot path can be an hour stale (`row['best_bid']` used as a sanity
reference in trading.py:356 comes from the sheet).
### Code quality
- Bare `except:` everywhere (~30 sites), `print()` as the only logging, no
tests at all, no types, dead code left as comments, `gc.collect()` sprinkled
as a superstition (7 call sites), `deets` reused as both the outcome list and
the book snapshot inside the same loop (trading.py:158/197).
- `data_updater/` duplicates `poly_utils` (two `google_utils.py`, two
`trading_utils.py` with different contents).
- Pinned to **Python 3.9** (.python-version = 3.9.18, EOL Oct 2025) and
`py-clob-client==0.28.0`. That client line is **dead**: Polymarket cut over
to CLOB V2 on 2026-04-28 (new order signing, new contracts) and archived
py-clob-client — **v1 of this bot cannot place an order at all anymore**
(see 03). This isn't a refactor of a working bot; it's a rebuild of a
non-functional one.
- A **Node.js subprocess** (`poly_merger/`) exists solely to call
`mergePositions` through a Gnosis Safe — a whole second runtime, package.json
and ethers v5 dependency for one contract call that web3.py can do.
### Strategy weaknesses (why it loses money — detailed fix in 04-strategy.md)
- **Quote logic is penny-jumping**: `bid = best_bid + tick`, `ask = best_ask -
tick`, with a couple of size-based exceptions. No fair-value estimate, no
microprice, no inventory skew — position size only gates *whether* to quote,
never *where*.
- **Adverse selection is handled after the fact**: the only defenses are an
hourly 3-hour volatility number from the spreadsheet and a stop-loss that
**crosses the spread** (sells at best bid — a taker exit, exactly what we
want to ban) then sleeps the market for hours via a JSON file on disk.
- **Rewards are a filter, not an objective**: the scanner computes
reward-per-100 carefully, but the live quoter only checks "is my bid inside
the incentive band" as a boolean. Placement within the band (where the
reward-vs-fill-risk tradeoff actually lives) is never optimized.
- Hard-coded magic everywhere: 0.10.9 price band, 250 share absolute cap,
0.005 reprice threshold, 15s staleness, 0.95/0.97/1.01 ratios, `max_size*2`
total-exposure allowance.
- Take-profit reprices only when 2% away from target; sell-side quote sits at
`avgPrice` floor (trading.py / trading_utils.py:136) — i.e. the bot refuses
to exit below cost, which turns losers into resolution lottery tickets.
## What's worth keeping (conceptually)
- **The two-token + merge insight**: buying YES and NO such that both fill and
merging the pair back to USDC is a maker-only exit that never crosses the
spread. v2 promotes this from an afterthought to a core mechanism.
- Book maintenance via `SortedDict` and the YES/NO mirror trick — cheap and
correct.
- The desired-vs-existing order diffing idea in `send_buy_order`/`send_sell_order`
(only cancel/replace on meaningful change) — right instinct, generalized into
a proper reconciler in v2.
- The scanner's reward-per-$100 estimator (`add_formula_params` implements the
official S(v,s) = ((vs)/v)² scoring) — the math moves into v2's selector.
- The `performing` concept (in-flight fills gate REST reconciliation) — becomes
a real state machine instead of dicts of sets.
## What dies in v2
| Component | Fate |
|---|---|
| Google Sheets (gspread, gspread-dataframe, google-auth, service accounts) | **Deleted.** Local files + SQLite (02/05) |
| `poly_merger/` Node.js + ethers + package.json | **Deleted.** Native Python Safe execution (03) |
| `update_stats.py`, `poly_stats/` (Sheets dashboards) | **Deleted.** Replaced by local status CLI/log (05) |
| `data_updater/` sampling-markets crawl + per-market book fetch | **Deleted.** Gamma API scanner, minutes → seconds (03/05) |
| `global_state.py` module-level dicts | **Deleted.** Typed state store owned by the engine (02) |
| Background REST polling thread | **Deleted.** Single asyncio loop; REST only for reconciliation (02) |
| `positions/*.json` risk files | **Deleted.** SQLite state (02) |
| Python 3.9 pin, py-clob-client 0.28 (archived, V1-only) | Upgraded: 3.12+, py-clob-client-v2 + own async HTTP (03) |
| `print()` logging, bare excepts, gc.collect() | Replaced: structured logging, real error policy (02/05) |
## Repo facts for reference
- uv is **already adopted** (uv.lock, pyproject.toml, hatchling build) — v2
keeps uv and finishes the job: bump `requires-python` to ≥3.12, refresh all
pins, add ruff + mypy + pytest as dev group, `src/` layout.
- Git history is shallow and healthy; no CI exists; MIT licensed; README
already warns the v1 strategy is unprofitable.