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.
Files changed (42) hide show
  1. {pmquant-0.3.0 → pmquant-0.4.1}/.github/workflows/canary.yml +2 -2
  2. {pmquant-0.3.0 → pmquant-0.4.1}/.github/workflows/publish.yml +2 -2
  3. {pmquant-0.3.0 → pmquant-0.4.1}/.github/workflows/test.yml +5 -4
  4. {pmquant-0.3.0 → pmquant-0.4.1}/.gitignore +1 -0
  5. {pmquant-0.3.0 → pmquant-0.4.1}/CHANGELOG.md +34 -0
  6. {pmquant-0.3.0 → pmquant-0.4.1}/PKG-INFO +13 -1
  7. {pmquant-0.3.0 → pmquant-0.4.1}/README.md +4 -0
  8. {pmquant-0.3.0 → pmquant-0.4.1}/bot-template/dash/bot_dash.py +4 -4
  9. {pmquant-0.3.0 → pmquant-0.4.1}/pyproject.toml +18 -2
  10. pmquant-0.4.1/server.json +61 -0
  11. {pmquant-0.3.0 → pmquant-0.4.1}/src/pmq/__init__.py +2 -2
  12. {pmquant-0.3.0 → pmquant-0.4.1}/src/pmq/data.py +70 -34
  13. {pmquant-0.3.0 → pmquant-0.4.1}/src/pmq/doctor.py +36 -15
  14. {pmquant-0.3.0 → pmquant-0.4.1}/src/pmq/executor.py +57 -31
  15. {pmquant-0.3.0 → pmquant-0.4.1}/src/pmq/mcp.py +20 -14
  16. pmquant-0.4.1/src/pmq/py.typed +0 -0
  17. {pmquant-0.3.0 → pmquant-0.4.1}/tests/test_data.py +82 -0
  18. pmquant-0.4.1/tests/test_doctor.py +160 -0
  19. {pmquant-0.3.0 → pmquant-0.4.1}/tests/test_executor.py +114 -0
  20. pmquant-0.4.1/tests/test_mcp.py +133 -0
  21. pmquant-0.3.0/tests/test_doctor.py +0 -26
  22. pmquant-0.3.0/tests/test_mcp.py +0 -38
  23. {pmquant-0.3.0 → pmquant-0.4.1}/AGENTS.md +0 -0
  24. {pmquant-0.3.0 → pmquant-0.4.1}/CLAUDE.md +0 -0
  25. {pmquant-0.3.0 → pmquant-0.4.1}/CONTRIBUTING.md +0 -0
  26. {pmquant-0.3.0 → pmquant-0.4.1}/LICENSE +0 -0
  27. {pmquant-0.3.0 → pmquant-0.4.1}/SECURITY.md +0 -0
  28. {pmquant-0.3.0 → pmquant-0.4.1}/bot-template/README.md +0 -0
  29. {pmquant-0.3.0 → pmquant-0.4.1}/bot-template/bot.py +0 -0
  30. {pmquant-0.3.0 → pmquant-0.4.1}/bot-template/dash/dash.html +0 -0
  31. {pmquant-0.3.0 → pmquant-0.4.1}/bot-template/pmq-bot.service +0 -0
  32. {pmquant-0.3.0 → pmquant-0.4.1}/bot-template/strategy.py +0 -0
  33. {pmquant-0.3.0 → pmquant-0.4.1}/docs/assets/pmq-doctor.svg +0 -0
  34. {pmquant-0.3.0 → pmquant-0.4.1}/docs/recipes.md +0 -0
  35. {pmquant-0.3.0 → pmquant-0.4.1}/docs/rounding-study.md +0 -0
  36. {pmquant-0.3.0 → pmquant-0.4.1}/docs/war-story.md +0 -0
  37. {pmquant-0.3.0 → pmquant-0.4.1}/examples/fak_buy_guarded.py +0 -0
  38. {pmquant-0.3.0 → pmquant-0.4.1}/examples/read_market.py +0 -0
  39. {pmquant-0.3.0 → pmquant-0.4.1}/llms.txt +0 -0
  40. {pmquant-0.3.0 → pmquant-0.4.1}/src/pmq/exceptions.py +0 -0
  41. {pmquant-0.3.0 → pmquant-0.4.1}/tests/test_canary_live.py +0 -0
  42. {pmquant-0.3.0 → pmquant-0.4.1}/tests/test_template_engine.py +0 -0
@@ -12,8 +12,8 @@ jobs:
12
12
  canary:
13
13
  runs-on: ubuntu-latest
14
14
  steps:
15
- - uses: actions/checkout@v4
16
- - uses: actions/setup-python@v5
15
+ - uses: actions/checkout@v5
16
+ - uses: actions/setup-python@v6
17
17
  with:
18
18
  python-version: "3.12"
19
19
  - run: pip install -e ".[dev]"
@@ -11,10 +11,10 @@ jobs:
11
11
  permissions:
12
12
  id-token: write
13
13
  steps:
14
- - uses: actions/checkout@v4
14
+ - uses: actions/checkout@v5
15
15
 
16
16
  - name: Set up Python
17
- uses: actions/setup-python@v5
17
+ uses: actions/setup-python@v6
18
18
  with:
19
19
  python-version: "3.12"
20
20
 
@@ -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@v4
13
- - uses: actions/setup-python@v5
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: pytest -q
18
+ - run: mypy
19
+ - run: pytest -q --cov=pmq --cov-fail-under=85
@@ -6,3 +6,4 @@ build/
6
6
  .venv/
7
7
  .pytest_cache/
8
8
  .env
9
+ .coverage
@@ -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.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
  [![PyPI](https://img.shields.io/pypi/v/pmquant)](https://pypi.org/project/pmquant/)
30
40
  [![tests](https://github.com/crp4222/pmq/actions/workflows/test.yml/badge.svg)](https://github.com/crp4222/pmq/actions/workflows/test.yml)
31
41
  [![canary](https://github.com/crp4222/pmq/actions/workflows/canary.yml/badge.svg)](https://github.com/crp4222/pmq/actions/workflows/canary.yml)
42
+ [![coverage gate](https://img.shields.io/badge/coverage-%E2%89%A585%25%20enforced%20in%20CI-blue)](.github/workflows/test.yml)
43
+ [![typed](https://img.shields.io/badge/types-mypy%20strict-blue)](pyproject.toml)
32
44
  [![license](https://img.shields.io/badge/license-MIT-green)](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
  [![PyPI](https://img.shields.io/pypi/v/pmquant)](https://pypi.org/project/pmquant/)
4
6
  [![tests](https://github.com/crp4222/pmq/actions/workflows/test.yml/badge.svg)](https://github.com/crp4222/pmq/actions/workflows/test.yml)
5
7
  [![canary](https://github.com/crp4222/pmq/actions/workflows/canary.yml/badge.svg)](https://github.com/crp4222/pmq/actions/workflows/canary.yml)
8
+ [![coverage gate](https://img.shields.io/badge/coverage-%E2%89%A585%25%20enforced%20in%20CI-blue)](.github/workflows/test.yml)
9
+ [![typed](https://img.shields.io/badge/types-mypy%20strict-blue)](pyproject.toml)
6
10
  [![license](https://img.shields.io/badge/license-MIT-green)](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 favbot journal;
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
- """favbot systemd state + last collateral line from its journal."""
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 = "favbot-dash"
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"favbot-dash listening on :{PORT}", flush=True)
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.3.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.3.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
- def fee(price, shares, rate=FEE_RATES["crypto"]):
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, logger=None):
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
- return data[0]
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
- return ev[0]["markets"][0]
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, outcome_a=None, outcome_b=None):
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.get("outcomes")
108
- token_ids = json.loads(m["clobTokenIds"]) if isinstance(m.get("clobTokenIds"), str) else m.get("clobTokenIds")
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
- "condition_id": m.get("conditionId"),
113
- "slug": m.get("slug"),
114
- "token_a": token_ids[a],
115
- "token_b": token_ids[b],
116
- "outcome_a": outcomes[a],
117
- "outcome_b": outcomes[b],
118
- "outcome_prices_raw": m.get("outcomePrices"),
119
- "idx_a": a,
120
- "end_ts": _end_ts(m),
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
- return http_get_json(f"{CLOB}/book?token_id={token_id}", logger=logger)
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
- def _f(k):
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, threshold=0.90):
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, limit=200):
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, logger=None):
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}",