> ## Documentation Index
> Fetch the complete documentation index at: https://polyquantlab.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Auto-trade logical arbs

> A 30-line Python loop that polls /v1/arb/live, filters to math-guaranteed setups, and submits Polymarket orders.

This guide shows the smallest useful bot you can build on top of the
PolyQuantLab signal stream. Reads `/v1/arb/live` every few seconds,
filters to the two tiers we audit publicly:

* **`tier == "logical"`** — math-guaranteed (yes\_ask + no\_ask \< \$1)
* **`tier == "endgame"`** — last 0-120 s, > 2σ past strike, dual-oracle confirmed

…and submits the corresponding Polymarket order via the
[`py-clob-client`](https://github.com/Polymarket/py-clob-client) (Polymarket's official SDK).

<Note>
  Backtest the loop against the public audit before betting capital.
  The [/audit](https://polyquantlab.com/audit) page shows the realised
  PnL of every signal tier — model EV vs reality, unedited.
</Note>

## Prerequisites

```bash theme={null}
pip install httpx py-clob-client python-dotenv
```

`.env`:

```dotenv theme={null}
POLYQUANTLAB_API_KEY=pql_live_xxx       # from /dashboard/api-keys
POLYMARKET_PRIVATE_KEY=0xabc...         # your Polygon wallet private key
POLYMARKET_PROXY_ADDRESS=0xdef...       # your Polymarket proxy address
```

## The loop

```python title="bot.py" theme={null}
"""
Minimal auto-trader for PolyQuantLab logical + endgame signals.

Run with:  python bot.py
Stop with: Ctrl+C
"""

import os
import time
import logging
import httpx
from dotenv import load_dotenv
from py_clob_client.client import ClobClient
from py_clob_client.clob_types import OrderArgs, OrderType

load_dotenv()
logging.basicConfig(level=logging.INFO, format="%(asctime)s  %(message)s")

PQL_API_KEY = os.environ["POLYQUANTLAB_API_KEY"]
PM_KEY      = os.environ["POLYMARKET_PRIVATE_KEY"]
PM_PROXY    = os.environ["POLYMARKET_PROXY_ADDRESS"]

# How much of your bankroll to put on each signal. Start tiny.
POSITION_SIZE_USDC = 5

# Don't trade signals where the order book is so thin that fill price
# beats our expected_pnl_per_share into negative territory.
MIN_NET_EV_PER_SHARE = 0.005   # $0.005/share = 0.5¢ after fees

# Avoid double-trading the same market.
seen_markets: set[str] = set()

pql = httpx.Client(
    base_url="https://api.polyquantlab.com",
    headers={"Authorization": f"Bearer {PQL_API_KEY}"},
    timeout=10.0,
)

pm = ClobClient(
    host="https://clob.polymarket.com",
    key=PM_KEY,
    chain_id=137,                # Polygon mainnet
    signature_type=1,            # email/magic-link proxy wallets
    funder=PM_PROXY,
)
pm.set_api_creds(pm.create_or_derive_api_creds())


def poll_and_trade() -> None:
    r = pql.get("/v1/arb/live", params={"min_edge_pp": 0, "limit": 200})
    r.raise_for_status()
    opps = r.json()["opportunities"]

    actionable = [
        o for o in opps
        if o["tier"] in ("logical", "endgame")
        and o["expected_pnl_per_share"] >= MIN_NET_EV_PER_SHARE
        and o["market_id"] not in seen_markets
    ]

    for o in actionable:
        size_shares = round(POSITION_SIZE_USDC / o["fill_price"], 2)
        side  = "BUY"
        # Use the YES token for BUY_YES / BUY_BOTH, NO token for BUY_NO.
        # Polymarket SDK takes a token_id; resolve from market_id.
        # (Markets list endpoint returns yes_token_id / no_token_id.)
        market = pql.get(f"/v1/markets/{o['market_id']}").json()
        token_id = (
            market["no_token_id"]
            if o["direction"] == "BUY_NO"
            else market["yes_token_id"]
        )

        order = OrderArgs(
            token_id=token_id,
            price=o["fill_price"],
            size=size_shares,
            side=side,
        )
        signed = pm.create_order(order)
        resp = pm.post_order(signed, OrderType.FOK)   # fill-or-kill

        logging.info(
            "submitted %s  tier=%s  size=%.2f @ $%.3f  edge=$%.4f/sh  resp=%s",
            o["market_id"][:8] + "…",
            o["tier"],
            size_shares,
            o["fill_price"],
            o["expected_pnl_per_share"],
            resp.get("status"),
        )
        seen_markets.add(o["market_id"])

        # For BUY_BOTH (logical) also buy the opposite leg.
        if o["direction"] == "BUY_BOTH":
            opp_token = market["no_token_id"] if token_id == market["yes_token_id"] else market["yes_token_id"]
            opp_price = o["no_ask"] if token_id == market["yes_token_id"] else o["yes_ask"]
            opp_size  = round(POSITION_SIZE_USDC / opp_price, 2)
            order2 = OrderArgs(
                token_id=opp_token, price=opp_price, size=opp_size, side=side
            )
            pm.post_order(pm.create_order(order2), OrderType.FOK)
            logging.info(
                "  +opposite leg  size=%.2f @ $%.3f (BUY_BOTH closing)",
                opp_size, opp_price,
            )


def main() -> None:
    logging.info("bot started — POLL every 3 s")
    while True:
        try:
            poll_and_trade()
        except httpx.HTTPError as e:
            logging.warning("PQL fetch failed: %s", e)
        except Exception as e:
            logging.exception("unexpected: %s", e)
        time.sleep(3)


if __name__ == "__main__":
    main()
```

## What this gets you

* \~3-second loop polling all live signals
* Skip signals whose net EV is \< \$0.005/share (fees eat them)
* Skip markets already traded this session
* For `BUY_BOTH` (logical), automatically place the opposite leg too — that's what locks the math guarantee

## What it doesn't handle

This is the **minimum viable** loop. Production-grade additions:

| Gap                                 | Fix                                                                                                                      |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Stop-loss / unwind on adverse fills | Subscribe `/v1/snapshots` WS and check positions every snapshot                                                          |
| Polymarket gas reservations         | Track `MATIC` balance; pause if \< 0.5                                                                                   |
| Bankroll management                 | Cap concurrent open positions; size by Kelly fraction                                                                    |
| Idempotency on retry                | Persist `seen_markets` to disk / Redis                                                                                   |
| Risk gates                          | Sanity-check `expected_pnl_per_share / fill_price < 0.4` (avoid suspicious 10x edges that are usually wrong oracle data) |

## Reality check

Even on the math-guaranteed `logical` tier, you're racing HFT. Expect
30-60% **submission success** in the first week as you tune latency.
The [audit page](https://polyquantlab.com/audit) shows our detector's
record — your execution will be a few % lower than that.

The same `/v1/arb/live` endpoint also returns `tier="stable"` and
`tier="stale"` rows. We surface them because the dashboard reads the
same JSON shape, but **don't trade them** — they're public-audit-confirmed
as \~\$0 EV after fees. The honest tier label is the most useful field
on the whole response.
