# Architecture Patterns

A clean Polymarket bot uses four layers, in order: **Data → Strategy → Risk → Execution**. Each layer has a single responsibility. Each is independently testable. The code in `scripts/starter_*.py` follows this layout — when generating, keep it intact.

## Why four layers, not "just a script"

A single `while True: scan(); trade()` loop works for a demo and rots in production. The four-layer split is what separates "I have a bot" from "I have a bot I trust running overnight".

```
┌─────────────────────────────────────────────────────────────┐
│                       MAIN / ORCHESTRATOR                    │
│  - Wires layers, owns the event loop, handles signals/exit   │
└──────────┬───────────────┬────────────────┬─────────────────┘
           │               │                │
       ┌───▼────┐      ┌───▼────┐       ┌───▼────┐
       │ DATA   │ ───▶ │STRATEGY│ ────▶ │  RISK  │
       │ layer  │ sig  │ layer  │ order │ layer  │
       └────────┘      └────────┘ intent└───┬────┘
                                            │ approved
                                            │
                                       ┌────▼─────┐
                                       │EXECUTION │
                                       │  layer   │
                                       └────┬─────┘
                                            │
                                       ┌────▼─────┐
                                       │  CLOB /  │
                                       │  Polygon │
                                       └──────────┘
```

## Layer 1 — Data

**Responsibility:** ingest from Polymarket APIs and Polygon. Output normalized objects to the strategy.

**Includes:**
- Gamma API client (market discovery)
- Data API client (leaderboard, activity, positions)
- WebSocket subscriber (market or user channel)
- Local order-book reconstruction (snapshot + diff)
- Polygon RPC reads (balances, allowances)

**Outputs:** typed events. E.g. `BookEvent`, `PriceChange`, `TradeSeen`, `MarketDiscovered`. Use Pydantic models — they double as docs.

**Should not:** make trading decisions, hold strategy state, place orders.

## Layer 2 — Strategy

**Responsibility:** consume normalized data events, decide whether to act, emit `OrderIntent`.

**Includes:**
- Signal detection (arb threshold reached, copy-trading wallet acted, AI verdict, etc.)
- Sizing logic (proportional, Kelly, fixed, volatility-scaled)
- Per-strategy state (open orders, inventory, daily PnL)

**Outputs:** `OrderIntent` — `(token_id, side, size, price, order_type, reason)`.

**Should not:** know how to authenticate to Polymarket, do retries, format API requests.

The strategy is the part that's *unique to your bot*. Everything else should be reusable.

## Layer 3 — Risk

**Responsibility:** stand between strategy and execution. Reject intents that violate any hard rule. Track aggregate exposure and PnL. Trip the kill switch when needed.

**Pre-trade checks (run on every intent):**
1. `DRY_RUN` flag — if true, log the intent and stop.
2. `MAX_TRADE_SIZE_USD` — clamp size if over.
3. `MAX_DAILY_LOSS_USD` — kill switch if today's PnL is below the negative threshold.
4. `MIN_LIQUIDITY_USD` — fetch market's 24h volume; if below floor, reject.
5. `MIN_TIME_TO_RESOLUTION` — reject if market resolves too soon (default 4h for copy, 30min for MM, 15min for arb).
6. `MAX_PER_MARKET_NOTIONAL` — sum of own open orders + position; reject if exceeded.
7. `DEDUP_WINDOW` — for copy trading, reject if the same source signal was already mirrored in the last N seconds.

**State maintained:**
- Today's realized + unrealized PnL (resets at UTC midnight, or whatever the user wants)
- Inventory per market
- Recent signal hashes (for dedup)
- Kill-switch flag

**Persistence:** state must survive restarts. SQLite is the simplest. MongoDB is overkill but used in some open-source bots. JSON files work for very simple bots if you `fsync` on every write.

## Layer 4 — Execution

**Responsibility:** take a risk-approved order, sign it, submit it to CLOB, handle the result.

**Includes:**
- `py-clob-client` initialization with the right `signature_type`
- Order construction (`OrderArgs` / `MarketOrderArgs`)
- Submission (`create_and_post_order`)
- Three-phase retry (try at target, ±1.5%, max slippage)
- Result parsing
- Trade lifecycle tracking via the User WebSocket (MATCHED → MINED → CONFIRMED)
- Cancellation on shutdown

**Should not:** decide what to trade, change sizes, evaluate edge.

This layer is the most likely place to find a strategy-agnostic bug. The skill's `scripts/common/clob_helpers.py` ships a battle-tested version — use it.

## Cross-cutting: logging

Every layer logs structured JSONL to a single file. One event per line, one schema per `type` field.

```json
{"ts": 1714234567.123, "type": "signal_seen", "wallet": "0x…",
 "market": "0x…", "side": "BUY", "size": 50, "skipped": false}
{"ts": 1714234567.456, "type": "order_intent", "token_id": "…",
 "side": "BUY", "size": 50, "price": 0.55, "reason": "copy"}
{"ts": 1714234567.789, "type": "risk_reject", "reason": "max_per_market"}
{"ts": 1714234568.123, "type": "order_submitted", "order_id": "abc"}
{"ts": 1714234571.000, "type": "fill", "order_id": "abc",
 "filled_size": 50, "avg_price": 0.551}
```

Why JSONL specifically:
- One file, append-only, crash-safe
- Trivial to parse with `jq`, `pandas.read_json(lines=True)`, or any log search tool
- Trivially diffable, so you can compare runs
- Schema can evolve per-event without breaking older lines

Rotate the file daily (`logs/events-YYYY-MM-DD.jsonl`) so they don't grow forever.

## CLI > web dashboard for a single trader

Quoting Örvar Karlsson (Medium, "Building an Automated Polymarket Trading System with Claude Code", March 2026):

> I built a full web dashboard — FastAPI backend + Next.js frontend — with pages for markets, watchlist, leaderboard, monitoring, and more. Seven pages, dark theme, real-time data. Then I realized: everything works better from the terminal. The website was a nice exercise but added complexity without value for a single trader. **Lesson learned: don't build UI until you've validated the workflow. The CLI was the product.**

If the user is a single trader running this on their laptop:
- Use `rich.live.Live` + `rich.table.Table` for the dashboard
- Render in-place every 250ms
- Show: open orders, today's PnL, last 10 signals, current strategy state, kill-switch status

If the user is running multiple operators / a team / clients:
- Then a web dashboard is justified
- Use `FastAPI` + a single-file React UI (no build pipeline) for simplicity
- Auth must be on by default

The starter templates ship with the terminal dashboard. If the user asks for a web one, it's a follow-up.

## State management — where, what

| State | Where | Why |
|---|---|---|
| Today's PnL | SQLite | Survives restart, fast queries |
| Open orders | Polymarket (re-query on start) | Source of truth is the exchange |
| Inventory per market | SQLite + Polymarket reconciliation | Don't trust local-only |
| Recent signal hashes (dedup) | In-memory + SQLite | Fast lookup, persisted across restart |
| Bot config | `.env` + `config.py` | One source of truth |
| Decision/trade log | JSONL on disk | Append-only, diffable |
| Strategy-specific (e.g. quote ladder) | In-memory only | Rebuilt from data layer on start |

## Concurrency model

Use `asyncio` everywhere. The Polymarket data flow is naturally async (WebSockets, HTTP) and the SDK supports it. Don't mix threads and async — one cooperative event loop is all you need.

Layout:

```python
async def main():
    cfg = load_config()
    data = DataLayer(cfg)
    strat = Strategy(cfg)
    risk = RiskLayer(cfg)
    execu = ExecutionLayer(cfg)

    async def event_loop():
        async for event in data.stream():
            for intent in strat.on_event(event):
                approved = risk.check(intent)
                if approved:
                    await execu.submit(approved)

    async def dashboard_loop():
        await dashboard.run(strat, risk, execu)

    await asyncio.gather(event_loop(), dashboard_loop())
```

Graceful shutdown:

```python
import signal
stop = asyncio.Event()
loop = asyncio.get_running_loop()
for sig in (signal.SIGINT, signal.SIGTERM):
    loop.add_signal_handler(sig, stop.set)

# inside main:
await asyncio.wait_for(stop.wait(), timeout=None)
await execu.cancel_all_open()
log("shutdown_clean")
```

Always cancel open orders on shutdown — leaving them on the book overnight after a kill is how people lose money on stale prices.

## Testing — bare minimum

You don't need a full test suite, but you need:

1. **Unit tests for the strategy.** Pure functions ideally — given a sequence of synthetic events, the strategy emits expected intents.
2. **Risk-layer tests.** Each rule (max size, daily loss, dedup) has a passing and failing case.
3. **Smoke test for execution.** Submit a $1 GTC order at $0.01 (will never fill), confirm you get an order_id back, cancel it. Run on every deploy.

Don't test the data layer or the SDK — they're external. Mock them in strategy tests.

## Deployment shape

For most users, this is:
- A laptop or desktop on a stable internet connection
- `tmux` / `screen` so the bot survives terminal close
- A cron-restart on crash (`while true; do python main.py; sleep 5; done` works)
- Logs rotated daily

For more serious operations:
- A small VPS in `us-east-2` (close to Polymarket infra) — DigitalOcean / Linode / Hetzner $5–10/month
- `systemd` service with `Restart=always`
- A real log aggregator (Loki, Datadog free tier)
- Off-host backup of SQLite state daily

QuantVPS markets a low-latency VPS specifically for this. For arb strategies that need <100ms execution, a co-located VPS is meaningful; for everything else it's overkill.

## Don't overengineer

The skill's starter scripts are intentionally <500 lines each. A working copy-trading bot is 200 lines. A working AI-driven bot is 180 lines. Resist the urge to build:

- A web UI (CLI is fine)
- A microservice mesh (one Python process is fine)
- A trading engine abstraction layer (you have one strategy)
- A plugin system (you don't need it)
- An ORM (SQLite + raw SQL is fine)

Code that does one thing well, fast, ships. Code that's a framework, ships in 6 months and never makes a trade.
