pmquant 0.3.0__tar.gz → 0.4.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pmquant-0.3.0 → pmquant-0.4.1}/.github/workflows/canary.yml +2 -2
- {pmquant-0.3.0 → pmquant-0.4.1}/.github/workflows/publish.yml +2 -2
- {pmquant-0.3.0 → pmquant-0.4.1}/.github/workflows/test.yml +5 -4
- {pmquant-0.3.0 → pmquant-0.4.1}/.gitignore +1 -0
- {pmquant-0.3.0 → pmquant-0.4.1}/CHANGELOG.md +34 -0
- {pmquant-0.3.0 → pmquant-0.4.1}/PKG-INFO +13 -1
- {pmquant-0.3.0 → pmquant-0.4.1}/README.md +4 -0
- {pmquant-0.3.0 → pmquant-0.4.1}/bot-template/dash/bot_dash.py +4 -4
- {pmquant-0.3.0 → pmquant-0.4.1}/pyproject.toml +18 -2
- pmquant-0.4.1/server.json +61 -0
- {pmquant-0.3.0 → pmquant-0.4.1}/src/pmq/__init__.py +2 -2
- {pmquant-0.3.0 → pmquant-0.4.1}/src/pmq/data.py +70 -34
- {pmquant-0.3.0 → pmquant-0.4.1}/src/pmq/doctor.py +36 -15
- {pmquant-0.3.0 → pmquant-0.4.1}/src/pmq/executor.py +57 -31
- {pmquant-0.3.0 → pmquant-0.4.1}/src/pmq/mcp.py +20 -14
- pmquant-0.4.1/src/pmq/py.typed +0 -0
- {pmquant-0.3.0 → pmquant-0.4.1}/tests/test_data.py +82 -0
- pmquant-0.4.1/tests/test_doctor.py +160 -0
- {pmquant-0.3.0 → pmquant-0.4.1}/tests/test_executor.py +114 -0
- pmquant-0.4.1/tests/test_mcp.py +133 -0
- pmquant-0.3.0/tests/test_doctor.py +0 -26
- pmquant-0.3.0/tests/test_mcp.py +0 -38
- {pmquant-0.3.0 → pmquant-0.4.1}/AGENTS.md +0 -0
- {pmquant-0.3.0 → pmquant-0.4.1}/CLAUDE.md +0 -0
- {pmquant-0.3.0 → pmquant-0.4.1}/CONTRIBUTING.md +0 -0
- {pmquant-0.3.0 → pmquant-0.4.1}/LICENSE +0 -0
- {pmquant-0.3.0 → pmquant-0.4.1}/SECURITY.md +0 -0
- {pmquant-0.3.0 → pmquant-0.4.1}/bot-template/README.md +0 -0
- {pmquant-0.3.0 → pmquant-0.4.1}/bot-template/bot.py +0 -0
- {pmquant-0.3.0 → pmquant-0.4.1}/bot-template/dash/dash.html +0 -0
- {pmquant-0.3.0 → pmquant-0.4.1}/bot-template/pmq-bot.service +0 -0
- {pmquant-0.3.0 → pmquant-0.4.1}/bot-template/strategy.py +0 -0
- {pmquant-0.3.0 → pmquant-0.4.1}/docs/assets/pmq-doctor.svg +0 -0
- {pmquant-0.3.0 → pmquant-0.4.1}/docs/recipes.md +0 -0
- {pmquant-0.3.0 → pmquant-0.4.1}/docs/rounding-study.md +0 -0
- {pmquant-0.3.0 → pmquant-0.4.1}/docs/war-story.md +0 -0
- {pmquant-0.3.0 → pmquant-0.4.1}/examples/fak_buy_guarded.py +0 -0
- {pmquant-0.3.0 → pmquant-0.4.1}/examples/read_market.py +0 -0
- {pmquant-0.3.0 → pmquant-0.4.1}/llms.txt +0 -0
- {pmquant-0.3.0 → pmquant-0.4.1}/src/pmq/exceptions.py +0 -0
- {pmquant-0.3.0 → pmquant-0.4.1}/tests/test_canary_live.py +0 -0
- {pmquant-0.3.0 → pmquant-0.4.1}/tests/test_template_engine.py +0 -0
|
@@ -7,12 +7,13 @@ jobs:
|
|
|
7
7
|
runs-on: ubuntu-latest
|
|
8
8
|
strategy:
|
|
9
9
|
matrix:
|
|
10
|
-
python-version: ["3.10", "3.12"]
|
|
10
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
11
11
|
steps:
|
|
12
|
-
- uses: actions/checkout@
|
|
13
|
-
- uses: actions/setup-python@
|
|
12
|
+
- uses: actions/checkout@v5
|
|
13
|
+
- uses: actions/setup-python@v6
|
|
14
14
|
with:
|
|
15
15
|
python-version: ${{ matrix.python-version }}
|
|
16
16
|
- run: pip install -e ".[dev]"
|
|
17
17
|
- run: ruff check .
|
|
18
|
-
- run:
|
|
18
|
+
- run: mypy
|
|
19
|
+
- run: pytest -q --cov=pmq --cov-fail-under=85
|
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.1 (2026-07-03)
|
|
4
|
+
|
|
5
|
+
* Introspection guard now also verifies `OrderArgsV2` fields (including
|
|
6
|
+
`builder_code`, which the limit-order path depends on): a client that
|
|
7
|
+
dropped it is refused at startup instead of failing at call time.
|
|
8
|
+
pmq-doctor mirrors the same check.
|
|
9
|
+
* Distribution: `server.json` manifest for the MCP registry, and an
|
|
10
|
+
`mcp-name` ownership token in the README (visible to the registry's PyPI
|
|
11
|
+
verification, invisible when rendered).
|
|
12
|
+
|
|
13
|
+
## 0.4.0 (2026-07-03)
|
|
14
|
+
|
|
15
|
+
* Typing: the whole public API is annotated and ships a `py.typed` marker;
|
|
16
|
+
`mypy --strict` runs in CI. New exported types `ParsedMarket` and
|
|
17
|
+
`BookMeta` (TypedDicts) describe what `parse_market` and `book_meta`
|
|
18
|
+
return.
|
|
19
|
+
* Builder attribution: the code now rides explicitly inside EVERY order's
|
|
20
|
+
args (market and limit paths) in addition to the client-level
|
|
21
|
+
BuilderConfig, and tests pin all three layers: executor args, real client
|
|
22
|
+
construction, and the dependency's own config injection. Disclosure and
|
|
23
|
+
one-line opt-out unchanged.
|
|
24
|
+
* Tests: 97 (from 39). Executable table of the fail-closed fill contract
|
|
25
|
+
(one row per possible exchange outcome, both order paths), deep pmq-doctor
|
|
26
|
+
scenarios with mocked RPC and CLOB (sig_type advice, alternative sig_type
|
|
27
|
+
probing, market section), MCP tool coverage including the gated live
|
|
28
|
+
tools. Coverage gate at 85% in CI.
|
|
29
|
+
* pmq-doctor fixes: an RPC failure now fails the run (it used to leave the
|
|
30
|
+
verdict green), and a non-numeric POLY_SIG_TYPE is diagnosed instead of
|
|
31
|
+
crashing.
|
|
32
|
+
* CI: test matrix extended to Python 3.10 through 3.14, classifiers updated
|
|
33
|
+
accordingly.
|
|
34
|
+
* Template: the dash brands itself pmq-bot-dash, matching the shipped
|
|
35
|
+
pmq-bot.service unit name.
|
|
36
|
+
|
|
3
37
|
## 0.3.0 (2026-07-03)
|
|
4
38
|
|
|
5
39
|
* New: `pmq-doctor`, a read-only diagnosis command for Polymarket V2 setups.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pmquant
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Fail-closed execution and market-data layer for Polymarket CLOB V2: local signing, confirmed fills only, fee-correct math, working deposit-wallet (POLY_1271) support.
|
|
5
5
|
Project-URL: Homepage, https://github.com/crp4222/pmq
|
|
6
6
|
Project-URL: Issues, https://github.com/crp4222/pmq/issues
|
|
@@ -13,11 +13,19 @@ Classifier: Intended Audience :: Developers
|
|
|
13
13
|
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
14
14
|
Classifier: License :: OSI Approved :: MIT License
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
21
|
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
22
|
+
Classifier: Typing :: Typed
|
|
17
23
|
Requires-Python: >=3.10
|
|
18
24
|
Requires-Dist: py-clob-client-v2<2,>=1.0.2
|
|
19
25
|
Provides-Extra: dev
|
|
20
26
|
Requires-Dist: mcp>=1.2; extra == 'dev'
|
|
27
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-cov>=5; extra == 'dev'
|
|
21
29
|
Requires-Dist: pytest>=8; extra == 'dev'
|
|
22
30
|
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
23
31
|
Provides-Extra: mcp
|
|
@@ -26,9 +34,13 @@ Description-Content-Type: text/markdown
|
|
|
26
34
|
|
|
27
35
|
# pmq
|
|
28
36
|
|
|
37
|
+
<!-- mcp-name: io.github.crp4222/pmq -->
|
|
38
|
+
|
|
29
39
|
[](https://pypi.org/project/pmquant/)
|
|
30
40
|
[](https://github.com/crp4222/pmq/actions/workflows/test.yml)
|
|
31
41
|
[](https://github.com/crp4222/pmq/actions/workflows/canary.yml)
|
|
42
|
+
[](.github/workflows/test.yml)
|
|
43
|
+
[](pyproject.toml)
|
|
32
44
|
[](LICENSE)
|
|
33
45
|
|
|
34
46
|
Fail-closed execution and market data for **Polymarket CLOB V2**, in Python.
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# pmq
|
|
2
2
|
|
|
3
|
+
<!-- mcp-name: io.github.crp4222/pmq -->
|
|
4
|
+
|
|
3
5
|
[](https://pypi.org/project/pmquant/)
|
|
4
6
|
[](https://github.com/crp4222/pmq/actions/workflows/test.yml)
|
|
5
7
|
[](https://github.com/crp4222/pmq/actions/workflows/canary.yml)
|
|
8
|
+
[](.github/workflows/test.yml)
|
|
9
|
+
[](pyproject.toml)
|
|
6
10
|
[](LICENSE)
|
|
7
11
|
|
|
8
12
|
Fail-closed execution and market data for **Polymarket CLOB V2**, in Python.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
Design constraints:
|
|
5
5
|
- stdlib only (http.server), one process, a few MB of RSS: safe on the Pi
|
|
6
|
-
- READ ONLY: parses bot_runs CSVs, systemctl show and the
|
|
6
|
+
- READ ONLY: parses bot_runs CSVs, systemctl show and the bot's journal;
|
|
7
7
|
never reads live.env, never talks to any exchange API, no secrets
|
|
8
8
|
- the phone does the rendering; this process only ships small JSON + one
|
|
9
9
|
static HTML file (cached in memory, CSVs re-parsed only on mtime change)
|
|
@@ -67,7 +67,7 @@ def _run(cmd):
|
|
|
67
67
|
|
|
68
68
|
|
|
69
69
|
def service_state():
|
|
70
|
-
"""
|
|
70
|
+
"""bot systemd state + last collateral line from its journal."""
|
|
71
71
|
with _lock:
|
|
72
72
|
cached = _cache.get("svc")
|
|
73
73
|
if cached and time.time() - cached[0] < SVC_CACHE_S:
|
|
@@ -141,7 +141,7 @@ def api_payload():
|
|
|
141
141
|
|
|
142
142
|
|
|
143
143
|
class Handler(BaseHTTPRequestHandler):
|
|
144
|
-
server_version = "
|
|
144
|
+
server_version = "pmq-bot-dash"
|
|
145
145
|
|
|
146
146
|
def _send(self, code, ctype, body):
|
|
147
147
|
self.send_response(code)
|
|
@@ -172,5 +172,5 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
172
172
|
|
|
173
173
|
if __name__ == "__main__":
|
|
174
174
|
srv = ThreadingHTTPServer(("0.0.0.0", PORT), Handler)
|
|
175
|
-
print(f"
|
|
175
|
+
print(f"pmq-bot-dash listening on :{PORT}", flush=True)
|
|
176
176
|
srv.serve_forever()
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pmquant"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.1"
|
|
8
8
|
description = "Fail-closed execution and market-data layer for Polymarket CLOB V2: local signing, confirmed fills only, fee-correct math, working deposit-wallet (POLY_1271) support."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -22,7 +22,13 @@ classifiers = [
|
|
|
22
22
|
"Intended Audience :: Financial and Insurance Industry",
|
|
23
23
|
"License :: OSI Approved :: MIT License",
|
|
24
24
|
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Programming Language :: Python :: 3.13",
|
|
29
|
+
"Programming Language :: Python :: 3.14",
|
|
25
30
|
"Topic :: Office/Business :: Financial :: Investment",
|
|
31
|
+
"Typing :: Typed",
|
|
26
32
|
]
|
|
27
33
|
|
|
28
34
|
[project.urls]
|
|
@@ -31,7 +37,7 @@ Issues = "https://github.com/crp4222/pmq/issues"
|
|
|
31
37
|
|
|
32
38
|
[project.optional-dependencies]
|
|
33
39
|
mcp = ["mcp>=1.2"]
|
|
34
|
-
dev = ["pytest>=8", "mcp>=1.2", "ruff>=0.6"]
|
|
40
|
+
dev = ["pytest>=8", "pytest-cov>=5", "mcp>=1.2", "ruff>=0.6", "mypy>=1.10"]
|
|
35
41
|
|
|
36
42
|
[project.scripts]
|
|
37
43
|
pmq-mcp = "pmq.mcp:main"
|
|
@@ -40,6 +46,16 @@ pmq-doctor = "pmq.doctor:main"
|
|
|
40
46
|
[tool.hatch.build.targets.wheel]
|
|
41
47
|
packages = ["src/pmq"]
|
|
42
48
|
|
|
49
|
+
[tool.mypy]
|
|
50
|
+
python_version = "3.10"
|
|
51
|
+
strict = true
|
|
52
|
+
files = ["src/pmq"]
|
|
53
|
+
|
|
54
|
+
[[tool.mypy.overrides]]
|
|
55
|
+
module = ["py_clob_client_v2.*", "eth_account.*"]
|
|
56
|
+
ignore_missing_imports = true
|
|
57
|
+
follow_imports = "skip"
|
|
58
|
+
|
|
43
59
|
[tool.ruff]
|
|
44
60
|
line-length = 100
|
|
45
61
|
target-version = "py310"
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.crp4222/pmq",
|
|
4
|
+
"title": "pmq (Polymarket CLOB V2)",
|
|
5
|
+
"description": "Fail-closed Polymarket CLOB V2 data and execution. Read tools (markets, book, fees, account) need no credentials; trading tools are registered only when the operator sets PMQ_MCP_LIVE=1, with a per-order USD cap.",
|
|
6
|
+
"repository": {
|
|
7
|
+
"url": "https://github.com/crp4222/pmq",
|
|
8
|
+
"source": "github"
|
|
9
|
+
},
|
|
10
|
+
"version": "0.4.1",
|
|
11
|
+
"packages": [
|
|
12
|
+
{
|
|
13
|
+
"registryType": "pypi",
|
|
14
|
+
"registryBaseUrl": "https://pypi.org",
|
|
15
|
+
"identifier": "pmquant",
|
|
16
|
+
"version": "0.4.1",
|
|
17
|
+
"runtimeHint": "uvx",
|
|
18
|
+
"transport": {
|
|
19
|
+
"type": "stdio"
|
|
20
|
+
},
|
|
21
|
+
"environmentVariables": [
|
|
22
|
+
{
|
|
23
|
+
"name": "PMQ_MCP_LIVE",
|
|
24
|
+
"description": "Set to 1 to register the trading tools (fak_buy, fak_sell, cancel_and_reconcile). Unset or any other value keeps the server read-only.",
|
|
25
|
+
"isRequired": false,
|
|
26
|
+
"isSecret": false
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "PMQ_MCP_MAX_USD",
|
|
30
|
+
"description": "Per-order hard cap in USD enforced before any live buy (default 10). Only relevant when PMQ_MCP_LIVE=1.",
|
|
31
|
+
"isRequired": false,
|
|
32
|
+
"isSecret": false
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "POLY_PRIVATE_KEY",
|
|
36
|
+
"description": "EOA private key used only to sign orders locally when trading tools are enabled. Never printed or transmitted. Not needed for the read-only tools.",
|
|
37
|
+
"isRequired": false,
|
|
38
|
+
"isSecret": true
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "POLY_FUNDER",
|
|
42
|
+
"description": "Funder wallet address. Required with signature_type > 0 (deposit wallets). Only used when trading tools are enabled.",
|
|
43
|
+
"isRequired": false,
|
|
44
|
+
"isSecret": false
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "POLY_SIG_TYPE",
|
|
48
|
+
"description": "Polymarket signature type: 0 EOA, 1 POLY_PROXY, 2 POLY_GNOSIS_SAFE, 3 POLY_1271 (app deposit wallet). Run pmq-doctor if unsure.",
|
|
49
|
+
"isRequired": false,
|
|
50
|
+
"isSecret": false
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "POLY_BUILDER_CODE",
|
|
54
|
+
"description": "Optional bytes32 builder attribution code. Defaults to the maintainer's public zero-commission code (disclosed in the README); set your own or leave empty to opt out.",
|
|
55
|
+
"isRequired": false,
|
|
56
|
+
"isSecret": false
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
@@ -25,7 +25,7 @@ from .data import (
|
|
|
25
25
|
)
|
|
26
26
|
from .exceptions import IntrospectionMismatch, OrderUncertain, PmqError
|
|
27
27
|
|
|
28
|
-
__version__ = "0.
|
|
28
|
+
__version__ = "0.4.1"
|
|
29
29
|
__all__ = [
|
|
30
30
|
"FEE_RATES", "band_ask_depth_usd", "best_bid_ask", "book_inferred_winner",
|
|
31
31
|
"book_meta", "event_markets", "fee", "get_book", "get_market", "get_tape",
|
|
@@ -36,7 +36,7 @@ __all__ = [
|
|
|
36
36
|
]
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
def __getattr__(name):
|
|
39
|
+
def __getattr__(name: str) -> object:
|
|
40
40
|
# Lazy import: the data layer must stay usable without py-clob-client-v2.
|
|
41
41
|
if name in ("PolymarketExecutor", "Fill", "DEFAULT_BUILDER_CODE"):
|
|
42
42
|
from . import executor
|
|
@@ -19,10 +19,15 @@ Facts this module encodes (verified live, July 2026):
|
|
|
19
19
|
see :data:`FEE_RATES`. Makers always pay zero. The ``maker/taker_base_fee``
|
|
20
20
|
of 1000 bps seen in API responses is an on-chain CAP, never the charge.
|
|
21
21
|
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
22
24
|
import calendar
|
|
23
25
|
import json
|
|
24
26
|
import time
|
|
25
27
|
import urllib.request
|
|
28
|
+
from typing import Any, Callable, TypedDict
|
|
29
|
+
|
|
30
|
+
Logger = Callable[[str], None]
|
|
26
31
|
|
|
27
32
|
UA = {"User-Agent": "Mozilla/5.0"}
|
|
28
33
|
GAMMA = "https://gamma-api.polymarket.com"
|
|
@@ -31,7 +36,7 @@ DATA = "https://data-api.polymarket.com"
|
|
|
31
36
|
|
|
32
37
|
#: Official taker fee rates per market category (docs.polymarket.com/trading/fees,
|
|
33
38
|
#: fetched 2026-07-03). Fee in $ = rate * p * (1 - p) * shares. Makers pay 0.
|
|
34
|
-
FEE_RATES = {
|
|
39
|
+
FEE_RATES: dict[str, float] = {
|
|
35
40
|
"crypto": 0.07,
|
|
36
41
|
"sports": 0.03,
|
|
37
42
|
"finance": 0.04,
|
|
@@ -45,7 +50,28 @@ FEE_RATES = {
|
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
|
|
48
|
-
|
|
53
|
+
class ParsedMarket(TypedDict):
|
|
54
|
+
"""Normalized view of a Gamma market object, see :func:`parse_market`."""
|
|
55
|
+
condition_id: str | None
|
|
56
|
+
slug: str | None
|
|
57
|
+
token_a: str
|
|
58
|
+
token_b: str
|
|
59
|
+
outcome_a: str
|
|
60
|
+
outcome_b: str
|
|
61
|
+
outcome_prices_raw: Any
|
|
62
|
+
idx_a: int
|
|
63
|
+
end_ts: int | None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class BookMeta(TypedDict):
|
|
67
|
+
"""Exchange metadata riding on a book response, see :func:`book_meta`."""
|
|
68
|
+
min_order_size: float | None
|
|
69
|
+
tick_size: float | None
|
|
70
|
+
neg_risk: bool | None
|
|
71
|
+
last_trade_price: float | None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def fee(price: float, shares: float, rate: float = FEE_RATES["crypto"]) -> float:
|
|
49
75
|
"""Taker fee in $ under the current schedule. Makers pay zero.
|
|
50
76
|
|
|
51
77
|
The fee peaks at price 0.50 and vanishes toward 0 and 1: a taker fill at
|
|
@@ -54,7 +80,8 @@ def fee(price, shares, rate=FEE_RATES["crypto"]):
|
|
|
54
80
|
return rate * price * (1.0 - price) * shares
|
|
55
81
|
|
|
56
82
|
|
|
57
|
-
def http_get_json(url, retries=3, timeout=10,
|
|
83
|
+
def http_get_json(url: str, retries: int = 3, timeout: float = 10,
|
|
84
|
+
logger: Logger | None = None) -> Any:
|
|
58
85
|
"""GET a JSON document with linear backoff. Returns None on final failure."""
|
|
59
86
|
for i in range(retries):
|
|
60
87
|
try:
|
|
@@ -70,18 +97,20 @@ def http_get_json(url, retries=3, timeout=10, logger=None):
|
|
|
70
97
|
return None
|
|
71
98
|
|
|
72
99
|
|
|
73
|
-
def get_market(slug, logger=None):
|
|
100
|
+
def get_market(slug: str, logger: Logger | None = None) -> dict[str, Any] | None:
|
|
74
101
|
"""Gamma market object for a slug, falling back to /events for expired ones."""
|
|
75
102
|
data = http_get_json(f"{GAMMA}/markets?slug={slug}", logger=logger)
|
|
76
103
|
if data:
|
|
77
|
-
|
|
104
|
+
first: dict[str, Any] = data[0]
|
|
105
|
+
return first
|
|
78
106
|
ev = http_get_json(f"{GAMMA}/events?slug={slug}", logger=logger)
|
|
79
107
|
if ev and ev[0].get("markets"):
|
|
80
|
-
|
|
108
|
+
fallback: dict[str, Any] = ev[0]["markets"][0]
|
|
109
|
+
return fallback
|
|
81
110
|
return None
|
|
82
111
|
|
|
83
112
|
|
|
84
|
-
def _end_ts(m):
|
|
113
|
+
def _end_ts(m: dict[str, Any]) -> int | None:
|
|
85
114
|
"""Market close time as unix epoch, or None. Gamma sends UTC ISO 8601."""
|
|
86
115
|
raw = m.get("endDate") or m.get("endDateIso") or ""
|
|
87
116
|
for fmt in ("%Y-%m-%dT%H:%M:%SZ", "%Y-%m-%dT%H:%M:%S.%fZ"):
|
|
@@ -92,7 +121,8 @@ def _end_ts(m):
|
|
|
92
121
|
return None
|
|
93
122
|
|
|
94
123
|
|
|
95
|
-
def parse_market(m,
|
|
124
|
+
def parse_market(m: dict[str, Any] | None, outcome_a: str | None = None,
|
|
125
|
+
outcome_b: str | None = None) -> ParsedMarket | None:
|
|
96
126
|
"""Extract condition id and outcome token ids from a Gamma market object.
|
|
97
127
|
|
|
98
128
|
Works on ANY binary market: politics, sports, crypto, whatever the
|
|
@@ -104,26 +134,26 @@ def parse_market(m, outcome_a=None, outcome_b=None):
|
|
|
104
134
|
if not m:
|
|
105
135
|
return None
|
|
106
136
|
try:
|
|
107
|
-
outcomes = json.loads(m["outcomes"]) if isinstance(m.get("outcomes"), str) else m
|
|
108
|
-
token_ids = json.loads(m["clobTokenIds"]) if isinstance(m.get("clobTokenIds"), str) else m
|
|
137
|
+
outcomes: Any = json.loads(m["outcomes"]) if isinstance(m.get("outcomes"), str) else m["outcomes"]
|
|
138
|
+
token_ids: Any = json.loads(m["clobTokenIds"]) if isinstance(m.get("clobTokenIds"), str) else m["clobTokenIds"]
|
|
109
139
|
a = outcomes.index(outcome_a) if outcome_a else 0
|
|
110
140
|
b = outcomes.index(outcome_b) if outcome_b else (1 if a == 0 else 0)
|
|
111
|
-
return
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
141
|
+
return ParsedMarket(
|
|
142
|
+
condition_id=m.get("conditionId"),
|
|
143
|
+
slug=m.get("slug"),
|
|
144
|
+
token_a=token_ids[a],
|
|
145
|
+
token_b=token_ids[b],
|
|
146
|
+
outcome_a=outcomes[a],
|
|
147
|
+
outcome_b=outcomes[b],
|
|
148
|
+
outcome_prices_raw=m.get("outcomePrices"),
|
|
149
|
+
idx_a=int(a),
|
|
150
|
+
end_ts=_end_ts(m),
|
|
151
|
+
)
|
|
122
152
|
except Exception:
|
|
123
153
|
return None
|
|
124
154
|
|
|
125
155
|
|
|
126
|
-
def resolved_winner(pm):
|
|
156
|
+
def resolved_winner(pm: ParsedMarket | None) -> str | None:
|
|
127
157
|
"""Winning outcome name from settled Gamma outcomePrices; None if unsettled."""
|
|
128
158
|
if not pm:
|
|
129
159
|
return None
|
|
@@ -139,12 +169,14 @@ def resolved_winner(pm):
|
|
|
139
169
|
return None
|
|
140
170
|
|
|
141
171
|
|
|
142
|
-
def get_book(token_id, logger=None):
|
|
172
|
+
def get_book(token_id: str, logger: Logger | None = None) -> dict[str, Any] | None:
|
|
143
173
|
"""Real-time CLOB book. THE live data source; never the trade tape."""
|
|
144
|
-
|
|
174
|
+
book: dict[str, Any] | None = http_get_json(f"{CLOB}/book?token_id={token_id}", logger=logger)
|
|
175
|
+
return book
|
|
145
176
|
|
|
146
177
|
|
|
147
|
-
def best_bid_ask(book
|
|
178
|
+
def best_bid_ask(book: dict[str, Any] | None,
|
|
179
|
+
) -> tuple[float | None, float | None, float | None, float | None]:
|
|
148
180
|
"""(best_bid, bid_size, best_ask, ask_size), sizes summed at the level."""
|
|
149
181
|
if not book:
|
|
150
182
|
return None, None, None, None
|
|
@@ -157,12 +189,13 @@ def best_bid_ask(book):
|
|
|
157
189
|
return bb, bb_sz, ba, ba_sz
|
|
158
190
|
|
|
159
191
|
|
|
160
|
-
def book_meta(book):
|
|
192
|
+
def book_meta(book: dict[str, Any] | None) -> BookMeta:
|
|
161
193
|
"""Exchange metadata riding on the book response: per-market minimum
|
|
162
194
|
order size (shares), tick size, neg_risk flag, last trade price. Read
|
|
163
195
|
these from the live book instead of hardcoding exchange rules."""
|
|
164
196
|
b = book or {}
|
|
165
|
-
|
|
197
|
+
|
|
198
|
+
def _f(k: str) -> float | None:
|
|
166
199
|
try:
|
|
167
200
|
return float(b[k])
|
|
168
201
|
except (KeyError, TypeError, ValueError):
|
|
@@ -171,14 +204,15 @@ def book_meta(book):
|
|
|
171
204
|
"neg_risk": b.get("neg_risk"), "last_trade_price": _f("last_trade_price")}
|
|
172
205
|
|
|
173
206
|
|
|
174
|
-
def band_ask_depth_usd(book, lo, hi):
|
|
207
|
+
def band_ask_depth_usd(book: dict[str, Any] | None, lo: float, hi: float) -> float:
|
|
175
208
|
"""Total $ notional of asks resting within [lo, hi]."""
|
|
176
209
|
asks = (book or {}).get("asks") or []
|
|
177
210
|
return round(sum(float(a["price"]) * float(a["size"]) for a in asks
|
|
178
211
|
if lo <= float(a["price"]) <= hi), 2)
|
|
179
212
|
|
|
180
213
|
|
|
181
|
-
def book_inferred_winner(bid_a, bid_b,
|
|
214
|
+
def book_inferred_winner(bid_a: float | None, bid_b: float | None,
|
|
215
|
+
threshold: float = 0.90) -> str | None:
|
|
182
216
|
"""Winner from a last pre-close book snapshot; None if ambiguous.
|
|
183
217
|
|
|
184
218
|
Use when Gamma settlement lags: a side whose BID is pinned at or above
|
|
@@ -191,14 +225,14 @@ def book_inferred_winner(bid_a, bid_b, threshold=0.90):
|
|
|
191
225
|
return None
|
|
192
226
|
|
|
193
227
|
|
|
194
|
-
def event_markets(slug, logger=None):
|
|
228
|
+
def event_markets(slug: str, logger: Logger | None = None) -> list[ParsedMarket]:
|
|
195
229
|
"""All binary markets of one event (multi-outcome events like elections
|
|
196
230
|
or tournaments are one binary market per candidate). Returns a list of
|
|
197
231
|
:func:`parse_market` dicts; unparseable members are skipped."""
|
|
198
232
|
ev = http_get_json(f"{GAMMA}/events?slug={slug}", logger=logger)
|
|
199
233
|
if not ev:
|
|
200
234
|
return []
|
|
201
|
-
out = []
|
|
235
|
+
out: list[ParsedMarket] = []
|
|
202
236
|
for m in ev[0].get("markets") or []:
|
|
203
237
|
pm = parse_market(m)
|
|
204
238
|
if pm:
|
|
@@ -206,7 +240,8 @@ def event_markets(slug, logger=None):
|
|
|
206
240
|
return out
|
|
207
241
|
|
|
208
242
|
|
|
209
|
-
def positions(user_address, logger=None,
|
|
243
|
+
def positions(user_address: str, logger: Logger | None = None,
|
|
244
|
+
limit: int = 200) -> list[dict[str, Any]]:
|
|
210
245
|
"""Current holdings of a wallet per the data-api (public, ~1 min lag).
|
|
211
246
|
Answers "what do I hold?" after fills: list of dicts with asset,
|
|
212
247
|
conditionId, size, avgPrice, currentValue and friends."""
|
|
@@ -214,13 +249,14 @@ def positions(user_address, logger=None, limit=200):
|
|
|
214
249
|
logger=logger) or []
|
|
215
250
|
|
|
216
251
|
|
|
217
|
-
def get_tape(condition_id, since_ts, max_pages=4,
|
|
252
|
+
def get_tape(condition_id: str, since_ts: float, max_pages: int = 4,
|
|
253
|
+
logger: Logger | None = None) -> list[dict[str, Any]]:
|
|
218
254
|
"""Complete trade tape for a closed market (paginated, newest first).
|
|
219
255
|
|
|
220
256
|
OFFLINE USE ONLY: the indexer lags matching by 1 to 3 minutes; call at
|
|
221
257
|
least 5 minutes after close or you will score against missing fills.
|
|
222
258
|
"""
|
|
223
|
-
out = []
|
|
259
|
+
out: list[dict[str, Any]] = []
|
|
224
260
|
for page in range(max_pages):
|
|
225
261
|
batch = http_get_json(
|
|
226
262
|
f"{DATA}/trades?market={condition_id}&limit=500&offset={page*500}",
|