polymarket-cli 0.2.0__py3-none-any.whl

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.
polymarket_cli/cli.py ADDED
@@ -0,0 +1,482 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from typing import Any
5
+
6
+ from .api import HISTORY_INTERVALS, PolymarketClient
7
+ from .formatting import (
8
+ coerce_float,
9
+ format_timestamp,
10
+ market_token_rows,
11
+ parse_datetime,
12
+ parse_duration_to_seconds,
13
+ summarize_market,
14
+ to_pretty_json,
15
+ to_unix_seconds,
16
+ )
17
+
18
+ MARKET_SORT_CHOICES = ["volume24hr", "volume", "liquidity", "startDate", "endDate", "competitive", "closedTime"]
19
+ HISTORY_FORMAT_CHOICES = ["points", "ohlc", "summary"]
20
+
21
+
22
+ def _positive_int(value: str) -> int:
23
+ parsed = int(value)
24
+ if parsed <= 0:
25
+ raise argparse.ArgumentTypeError("value must be > 0")
26
+ return parsed
27
+
28
+
29
+ def _non_negative_float(value: str) -> float:
30
+ parsed = float(value)
31
+ if parsed < 0:
32
+ raise argparse.ArgumentTypeError("value must be >= 0")
33
+ return parsed
34
+
35
+
36
+ def _history_window(value: str) -> str:
37
+ if parse_duration_to_seconds(value) is None:
38
+ raise argparse.ArgumentTypeError(
39
+ "window must be a positive minute count like 60, or a duration like 15m, 1h, 1d, or 1w"
40
+ )
41
+ return value
42
+
43
+
44
+ def _add_market_selector(parser: argparse.ArgumentParser, *, required: bool = True) -> None:
45
+ group = parser.add_mutually_exclusive_group(required=required)
46
+ group.add_argument("--slug", help="Polymarket market slug")
47
+ group.add_argument("--id", help="Polymarket market id")
48
+
49
+
50
+ def _add_market_filter_arguments(parser: argparse.ArgumentParser, *, allow_query: bool) -> None:
51
+ if allow_query:
52
+ parser.add_argument("query", nargs="?", default=None, help="Optional free-text market query")
53
+ parser.add_argument("--limit", type=_positive_int, default=10, help="Maximum markets to return")
54
+ parser.add_argument("--offset", type=int, default=0, help="Gamma pagination offset")
55
+ parser.add_argument(
56
+ "--sort",
57
+ choices=MARKET_SORT_CHOICES,
58
+ default="volume24hr",
59
+ help="Market sort field. Server-side when supported, otherwise normalized client-side.",
60
+ )
61
+ parser.add_argument("--ascending", action="store_true", help="Return lower values / earlier dates first")
62
+ status = parser.add_mutually_exclusive_group()
63
+ status.add_argument("--active-only", action="store_true", help="Only active, non-closed markets (default)")
64
+ status.add_argument("--closed-only", action="store_true", help="Only closed markets")
65
+ status.add_argument("--all", action="store_true", help="Do not apply the default active-only market filter")
66
+ parser.add_argument(
67
+ "--archived",
68
+ action="store_true",
69
+ help="Filter for archived markets only. Archived filtering is server-side.",
70
+ )
71
+ parser.add_argument(
72
+ "--tag-id",
73
+ action="append",
74
+ default=[],
75
+ help="Gamma tag id filter. Repeat to require multiple tags.",
76
+ )
77
+ parser.add_argument(
78
+ "--exclude-tag-id",
79
+ action="append",
80
+ default=[],
81
+ help="Exclude markets with these tag ids. Repeatable.",
82
+ )
83
+ parser.add_argument(
84
+ "--related-tags",
85
+ action="store_true",
86
+ help="Request related-tag expansion alongside --tag-id where supported by Gamma.",
87
+ )
88
+ parser.add_argument("--start-after", help="Client-side filter on market startDate/startDateIso")
89
+ parser.add_argument("--start-before", help="Client-side filter on market startDate/startDateIso")
90
+ parser.add_argument("--end-after", help="Client-side filter on market endDate/endDateIso")
91
+ parser.add_argument("--end-before", help="Client-side filter on market endDate/endDateIso")
92
+ parser.add_argument("--min-liquidity", type=_non_negative_float, help="Client-side minimum liquidity")
93
+ parser.add_argument("--max-liquidity", type=_non_negative_float, help="Client-side maximum liquidity")
94
+ parser.add_argument("--min-volume24hr", type=_non_negative_float, help="Client-side minimum 24h volume")
95
+ parser.add_argument("--max-volume24hr", type=_non_negative_float, help="Client-side maximum 24h volume")
96
+ parser.add_argument(
97
+ "--hydrate",
98
+ action="store_true",
99
+ help="Hydrate matching markets to full market detail before rendering/filtering locally.",
100
+ )
101
+ parser.add_argument(
102
+ "--with-odds",
103
+ action="store_true",
104
+ help="Include current token odds in output. Implies market hydration for search/list.",
105
+ )
106
+ parser.add_argument(
107
+ "--with-market",
108
+ action="store_true",
109
+ help="Include the resolved raw market payload in JSON output. Implies market hydration for search/list.",
110
+ )
111
+ parser.add_argument("--json", action="store_true", help="Emit JSON instead of tabular text")
112
+
113
+
114
+ def _resolve_market(client: PolymarketClient, args: argparse.Namespace) -> dict[str, Any]:
115
+ return client.get_market(slug=args.slug, market_id=args.id)
116
+
117
+
118
+ def _resolve_condition_id(client: PolymarketClient, args: argparse.Namespace) -> str:
119
+ if getattr(args, "condition_id", None):
120
+ return args.condition_id
121
+ if not (getattr(args, "slug", None) or getattr(args, "id", None)):
122
+ raise SystemExit("Provide --condition-id or a market selector (--slug/--id)")
123
+ market = _resolve_market(client, args)
124
+ condition_id = market.get("conditionId")
125
+ if not condition_id:
126
+ raise SystemExit("No condition id found on selected market")
127
+ return str(condition_id)
128
+
129
+
130
+ def _resolve_token_id(client: PolymarketClient, args: argparse.Namespace) -> str:
131
+ if getattr(args, "token_id", None):
132
+ return args.token_id
133
+ if not (getattr(args, "slug", None) or getattr(args, "id", None)):
134
+ raise SystemExit("Provide --token-id or a market selector (--slug/--id)")
135
+ market = _resolve_market(client, args)
136
+ tokens = market_token_rows(market)
137
+ if not tokens:
138
+ raise SystemExit("No token ids found on selected market")
139
+ if getattr(args, "outcome", None):
140
+ for row in tokens:
141
+ if str(row.get("outcome", "")).lower() == args.outcome.lower():
142
+ return str(row["token_id"])
143
+ raise SystemExit(f"Outcome not found: {args.outcome}")
144
+ return str(tokens[0]["token_id"])
145
+
146
+
147
+ def _resolve_market_filters(args: argparse.Namespace) -> dict[str, Any]:
148
+ if args.closed_only:
149
+ active = None
150
+ closed = True
151
+ elif args.all:
152
+ active = None
153
+ closed = None
154
+ else:
155
+ active = True
156
+ closed = False
157
+ if args.archived:
158
+ active = None if active is True else active
159
+ closed = True if closed is False else closed
160
+ archived = True
161
+ else:
162
+ archived = False if not args.all and not args.closed_only else None
163
+ return {
164
+ "limit": args.limit,
165
+ "offset": args.offset,
166
+ "active": active,
167
+ "closed": closed,
168
+ "archived": archived,
169
+ "order": args.sort,
170
+ "ascending": args.ascending,
171
+ "tag_ids": args.tag_id or None,
172
+ "exclude_tag_ids": args.exclude_tag_id or None,
173
+ "related_tags": args.related_tags,
174
+ "start_after": args.start_after,
175
+ "start_before": args.start_before,
176
+ "end_after": args.end_after,
177
+ "end_before": args.end_before,
178
+ "min_liquidity": args.min_liquidity,
179
+ "max_liquidity": args.max_liquidity,
180
+ "min_volume24hr": args.min_volume24hr,
181
+ "max_volume24hr": args.max_volume24hr,
182
+ "hydrate": bool(args.hydrate or args.with_odds or args.with_market),
183
+ }
184
+
185
+
186
+ def build_parser() -> argparse.ArgumentParser:
187
+ parser = argparse.ArgumentParser(
188
+ prog="polymarket-cli",
189
+ description="Read-only Polymarket Gamma + CLOB CLI for market discovery, price snapshots, trades, and history",
190
+ )
191
+ sub = parser.add_subparsers(dest="command", required=True)
192
+
193
+ p_search = sub.add_parser("search", help="Search markets with public Gamma filters")
194
+ _add_market_filter_arguments(p_search, allow_query=True)
195
+
196
+ p_list = sub.add_parser("list", help="List markets with public Gamma filters")
197
+ _add_market_filter_arguments(p_list, allow_query=False)
198
+
199
+ p_market = sub.add_parser("market", help="Fetch market details by slug or id")
200
+ _add_market_selector(p_market, required=True)
201
+ p_market.add_argument("--json", action="store_true")
202
+
203
+ for name, help_text in [("book", "Fetch order book"), ("midpoint", "Fetch midpoint"), ("price", "Fetch last trade price")]:
204
+ p = sub.add_parser(name, help=help_text)
205
+ p.add_argument("--token-id")
206
+ p.add_argument("--outcome", help="Outcome name to resolve token id when using a market selector")
207
+ _add_market_selector(p, required=False)
208
+ p.add_argument("--json", action="store_true")
209
+
210
+ p_history = sub.add_parser("history", help="Fetch public CLOB price history for a token")
211
+ p_history.add_argument("--token-id")
212
+ p_history.add_argument("--outcome", help="Outcome name to resolve token id when using a market selector")
213
+ _add_market_selector(p_history, required=False)
214
+ p_history.add_argument("--interval", choices=sorted(HISTORY_INTERVALS), default="1d")
215
+ p_history.add_argument("--fidelity", type=_positive_int, default=60, help="Sampling fidelity in minutes")
216
+ p_history.add_argument("--start", help="Unix seconds or ISO-8601 timestamp passed to startTs")
217
+ p_history.add_argument("--end", help="Unix seconds or ISO-8601 timestamp passed to endTs")
218
+ p_history.add_argument(
219
+ "--window",
220
+ type=_history_window,
221
+ help="Local aggregation window for --format ohlc/summary. Accepts bare minutes (60) or durations (15m, 1h, 1d, 1w).",
222
+ )
223
+ p_history.add_argument("--format", choices=HISTORY_FORMAT_CHOICES, default="points")
224
+ p_history.add_argument("--json", action="store_true")
225
+
226
+ p_trades = sub.add_parser("trades", help="Fetch recent public trades")
227
+ p_trades.add_argument("--condition-id")
228
+ _add_market_selector(p_trades, required=False)
229
+ p_trades.add_argument("--limit", type=_positive_int, default=20)
230
+ p_trades.add_argument("--json", action="store_true")
231
+
232
+ return parser
233
+
234
+
235
+ def _render_market_table(markets: list[dict[str, Any]], *, with_odds: bool = False) -> str:
236
+ lines = []
237
+ for market in markets:
238
+ status = []
239
+ if market.get("active"):
240
+ status.append("active")
241
+ if market.get("closed"):
242
+ status.append("closed")
243
+ if market.get("archived"):
244
+ status.append("archived")
245
+ odds = ""
246
+ if with_odds:
247
+ odds = " | ".join(
248
+ f"{row.get('outcome')}={row.get('price')}"
249
+ for row in market.get("odds", [])
250
+ if row.get("outcome") is not None and row.get("price") is not None
251
+ )
252
+ lines.append(
253
+ "\t".join(
254
+ [
255
+ str(market.get("id") or ""),
256
+ str(market.get("slug") or ""),
257
+ str(market.get("question") or ""),
258
+ ",".join(status) or "open",
259
+ str(market.get("endDate") or ""),
260
+ str(market.get("liquidity") or ""),
261
+ str(market.get("volume24hr") or ""),
262
+ odds,
263
+ ]
264
+ )
265
+ )
266
+ return "\n".join(lines)
267
+
268
+
269
+ def _normalize_history_points(payload: dict[str, Any]) -> list[dict[str, Any]]:
270
+ history = payload.get("history") or []
271
+ points = []
272
+ for row in history:
273
+ if not isinstance(row, dict):
274
+ continue
275
+ timestamp = row.get("t") or row.get("timestamp") or row.get("time")
276
+ price = row.get("p") or row.get("price")
277
+ dt = parse_datetime(timestamp)
278
+ px = coerce_float(price)
279
+ if dt is None or px is None:
280
+ continue
281
+ points.append({"timestamp": int(dt.timestamp()), "price": px})
282
+ return sorted(points, key=lambda item: item["timestamp"])
283
+
284
+
285
+ def _aggregate_history(points: list[dict[str, Any]], window: str | None) -> list[dict[str, Any]]:
286
+ if not points:
287
+ return []
288
+ bucket_size = parse_duration_to_seconds(window) if window else None
289
+ if not bucket_size:
290
+ return [
291
+ {
292
+ "windowStart": row["timestamp"],
293
+ "windowEnd": row["timestamp"],
294
+ "open": row["price"],
295
+ "high": row["price"],
296
+ "low": row["price"],
297
+ "close": row["price"],
298
+ "average": row["price"],
299
+ "count": 1,
300
+ }
301
+ for row in points
302
+ ]
303
+
304
+ buckets: dict[int, list[dict[str, Any]]] = {}
305
+ for row in points:
306
+ bucket_start = row["timestamp"] - (row["timestamp"] % bucket_size)
307
+ buckets.setdefault(bucket_start, []).append(row)
308
+
309
+ aggregated = []
310
+ for bucket_start in sorted(buckets):
311
+ rows = buckets[bucket_start]
312
+ prices = [row["price"] for row in rows]
313
+ aggregated.append(
314
+ {
315
+ "windowStart": bucket_start,
316
+ "windowEnd": bucket_start + bucket_size,
317
+ "open": rows[0]["price"],
318
+ "high": max(prices),
319
+ "low": min(prices),
320
+ "close": rows[-1]["price"],
321
+ "average": sum(prices) / len(prices),
322
+ "count": len(rows),
323
+ }
324
+ )
325
+ return aggregated
326
+
327
+
328
+ def _render_history_points(points: list[dict[str, Any]]) -> str:
329
+ return "\n".join(f"{format_timestamp(row['timestamp'])}\t{row['price']}" for row in points)
330
+
331
+
332
+ def _render_history_ohlc(rows: list[dict[str, Any]]) -> str:
333
+ return "\n".join(
334
+ "\t".join(
335
+ [
336
+ format_timestamp(row["windowStart"]),
337
+ format_timestamp(row["windowEnd"]),
338
+ str(row["open"]),
339
+ str(row["high"]),
340
+ str(row["low"]),
341
+ str(row["close"]),
342
+ str(row["average"]),
343
+ str(row["count"]),
344
+ ]
345
+ )
346
+ for row in rows
347
+ )
348
+
349
+
350
+ def _history_summary(points: list[dict[str, Any]], rows: list[dict[str, Any]]) -> dict[str, Any]:
351
+ if not points:
352
+ return {"points": 0, "windows": len(rows)}
353
+ prices = [row["price"] for row in points]
354
+ return {
355
+ "points": len(points),
356
+ "windows": len(rows),
357
+ "firstTimestamp": format_timestamp(points[0]["timestamp"]),
358
+ "lastTimestamp": format_timestamp(points[-1]["timestamp"]),
359
+ "firstPrice": points[0]["price"],
360
+ "lastPrice": points[-1]["price"],
361
+ "high": max(prices),
362
+ "low": min(prices),
363
+ "change": points[-1]["price"] - points[0]["price"],
364
+ }
365
+
366
+
367
+ def main(argv: list[str] | None = None) -> int:
368
+ parser = build_parser()
369
+ args = parser.parse_args(argv)
370
+ client = PolymarketClient()
371
+
372
+ if args.command in {"search", "list"}:
373
+ filters = _resolve_market_filters(args)
374
+ if args.command == "search" and args.query:
375
+ markets = client.search_markets(args.query, **filters)
376
+ else:
377
+ markets = client.list_markets(search=getattr(args, "query", None), **filters)
378
+ payload = []
379
+ for market in markets:
380
+ summary = summarize_market(market)
381
+ if args.with_market:
382
+ summary["market"] = market
383
+ payload.append(summary)
384
+ print(to_pretty_json(payload) if args.json else _render_market_table(payload, with_odds=args.with_odds))
385
+ return 0
386
+
387
+ if args.command == "market":
388
+ market = summarize_market(_resolve_market(client, args))
389
+ if args.json:
390
+ print(to_pretty_json(market))
391
+ else:
392
+ lines = [
393
+ market["question"],
394
+ f"slug: {market['slug']}",
395
+ f"id: {market['id']}",
396
+ f"conditionId: {market['conditionId']}",
397
+ f"startDate: {market.get('startDate')}",
398
+ f"endDate: {market.get('endDate')}",
399
+ f"liquidity: {market.get('liquidity')}",
400
+ f"volume24hr: {market.get('volume24hr')}",
401
+ ]
402
+ lines.extend(f"- {row['outcome']}: {row['token_id']} @ {row['price']}" for row in market["tokens"])
403
+ print("\n".join(lines))
404
+ return 0
405
+
406
+ if args.command == "book":
407
+ token_id = _resolve_token_id(client, args)
408
+ payload = client.get_book(token_id)
409
+ print(
410
+ to_pretty_json(payload)
411
+ if args.json
412
+ else (
413
+ f"token_id: {token_id}\n"
414
+ f"bids: {len(payload.get('bids', []))}\n"
415
+ f"asks: {len(payload.get('asks', []))}\n"
416
+ f"best_bid: {payload.get('bids', [{}])[0].get('price') if payload.get('bids') else None}\n"
417
+ f"best_ask: {payload.get('asks', [{}])[0].get('price') if payload.get('asks') else None}"
418
+ )
419
+ )
420
+ return 0
421
+
422
+ if args.command == "midpoint":
423
+ token_id = _resolve_token_id(client, args)
424
+ payload = client.get_midpoint(token_id)
425
+ print(to_pretty_json(payload) if args.json else payload.get("mid", ""))
426
+ return 0
427
+
428
+ if args.command == "price":
429
+ token_id = _resolve_token_id(client, args)
430
+ payload = client.get_last_trade_price(token_id)
431
+ print(to_pretty_json(payload) if args.json else payload.get("price", ""))
432
+ return 0
433
+
434
+ if args.command == "history":
435
+ token_id = _resolve_token_id(client, args)
436
+ start_ts = to_unix_seconds(args.start)
437
+ end_ts = to_unix_seconds(args.end)
438
+ payload = client.get_price_history(
439
+ token_id,
440
+ interval=args.interval,
441
+ fidelity=args.fidelity,
442
+ start_ts=start_ts,
443
+ end_ts=end_ts,
444
+ )
445
+ points = _normalize_history_points(payload)
446
+ windows = _aggregate_history(points, args.window)
447
+ if args.json:
448
+ rendered: Any
449
+ if args.format == "points":
450
+ rendered = {"token_id": token_id, "interval": args.interval, "fidelity": args.fidelity, "history": points}
451
+ elif args.format == "ohlc":
452
+ rendered = {"token_id": token_id, "interval": args.interval, "fidelity": args.fidelity, "window": args.window, "ohlc": windows}
453
+ else:
454
+ rendered = {"token_id": token_id, "interval": args.interval, "fidelity": args.fidelity, "window": args.window, "summary": _history_summary(points, windows)}
455
+ print(to_pretty_json(rendered))
456
+ elif args.format == "points":
457
+ print(_render_history_points(points))
458
+ elif args.format == "ohlc":
459
+ print(_render_history_ohlc(windows))
460
+ else:
461
+ print(to_pretty_json(_history_summary(points, windows)))
462
+ return 0
463
+
464
+ if args.command == "trades":
465
+ condition_id = _resolve_condition_id(client, args)
466
+ payload = client.get_trades(condition_id=condition_id, limit=args.limit)
467
+ print(
468
+ to_pretty_json(payload)
469
+ if args.json
470
+ else "\n".join(
471
+ f"{row.get('timestamp')}\t{row.get('side')}\t{row.get('price')}\t{row.get('size')}\t{row.get('title') or row.get('slug')}"
472
+ for row in payload
473
+ )
474
+ )
475
+ return 0
476
+
477
+ parser.error("unknown command")
478
+ return 2
479
+
480
+
481
+ if __name__ == "__main__": # pragma: no cover
482
+ raise SystemExit(main())
@@ -0,0 +1,157 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from datetime import UTC, datetime
5
+ from typing import Any
6
+
7
+
8
+ def parse_json_array(raw: str | None) -> list[Any]:
9
+ if not raw:
10
+ return []
11
+ try:
12
+ value = json.loads(raw)
13
+ except json.JSONDecodeError:
14
+ return []
15
+ return value if isinstance(value, list) else []
16
+
17
+
18
+ def market_token_rows(market: dict[str, Any]) -> list[dict[str, Any]]:
19
+ outcomes = market.get("outcomes")
20
+ outcome_prices = market.get("outcomePrices")
21
+ token_ids = market.get("clobTokenIds")
22
+
23
+ if isinstance(outcomes, str):
24
+ outcomes = parse_json_array(outcomes)
25
+ if isinstance(outcome_prices, str):
26
+ outcome_prices = parse_json_array(outcome_prices)
27
+ if isinstance(token_ids, str):
28
+ token_ids = parse_json_array(token_ids)
29
+
30
+ rows = []
31
+ for idx, token_id in enumerate(token_ids or []):
32
+ price = outcome_prices[idx] if idx < len(outcome_prices or []) else None
33
+ rows.append(
34
+ {
35
+ "index": idx,
36
+ "outcome": outcomes[idx] if idx < len(outcomes or []) else None,
37
+ "price": coerce_float(price),
38
+ "token_id": token_id,
39
+ }
40
+ )
41
+ return rows
42
+
43
+
44
+ def parse_datetime(value: Any) -> datetime | None:
45
+ if value in (None, ""):
46
+ return None
47
+ if isinstance(value, (int, float)):
48
+ return datetime.fromtimestamp(float(value), tz=UTC)
49
+ if isinstance(value, str):
50
+ text = value.strip()
51
+ if not text:
52
+ return None
53
+ if text.isdigit():
54
+ return datetime.fromtimestamp(int(text), tz=UTC)
55
+ if text.endswith("Z"):
56
+ text = f"{text[:-1]}+00:00"
57
+ try:
58
+ parsed = datetime.fromisoformat(text)
59
+ except ValueError:
60
+ return None
61
+ if parsed.tzinfo is None:
62
+ return parsed.replace(tzinfo=UTC)
63
+ return parsed.astimezone(UTC)
64
+ return None
65
+
66
+
67
+ def format_timestamp(value: Any) -> str:
68
+ parsed = parse_datetime(value)
69
+ if not parsed:
70
+ return ""
71
+ return parsed.astimezone(UTC).isoformat().replace("+00:00", "Z")
72
+
73
+
74
+ def to_unix_seconds(value: Any) -> int | None:
75
+ parsed = parse_datetime(value)
76
+ if not parsed:
77
+ return None
78
+ return int(parsed.timestamp())
79
+
80
+
81
+ def parse_duration_to_seconds(value: str | None) -> int | None:
82
+ if not value:
83
+ return None
84
+ text = value.strip().lower()
85
+ if not text:
86
+ return None
87
+ if text.isdigit():
88
+ amount = int(text)
89
+ return amount * 60 if amount > 0 else None
90
+ units = {"m": 60, "h": 3600, "d": 86400, "w": 604800}
91
+ unit = text[-1]
92
+ if unit not in units:
93
+ return None
94
+ try:
95
+ amount = int(text[:-1])
96
+ except ValueError:
97
+ return None
98
+ if amount <= 0:
99
+ return None
100
+ return amount * units[unit]
101
+
102
+
103
+ def coerce_float(value: Any) -> float | None:
104
+ if value in (None, ""):
105
+ return None
106
+ try:
107
+ return float(value)
108
+ except (TypeError, ValueError):
109
+ return None
110
+
111
+
112
+ def summarize_market(market: dict[str, Any]) -> dict[str, Any]:
113
+ rows = market_token_rows(market)
114
+ liquidity = coerce_float(market.get("liquidityNum") or market.get("liquidity"))
115
+ volume24hr = coerce_float(market.get("volume24hrClob") or market.get("volume24hr"))
116
+ summary = {
117
+ "id": market.get("id"),
118
+ "slug": market.get("slug"),
119
+ "question": market.get("question"),
120
+ "active": market.get("active"),
121
+ "closed": market.get("closed"),
122
+ "archived": market.get("archived"),
123
+ "acceptingOrders": market.get("acceptingOrders"),
124
+ "conditionId": market.get("conditionId"),
125
+ "endDate": market.get("endDate") or market.get("endDateIso"),
126
+ "liquidity": liquidity,
127
+ "volume24hr": volume24hr,
128
+ "startDate": market.get("startDate") or market.get("startDateIso"),
129
+ "resolved": bool(market.get("id") and market.get("conditionId")),
130
+ "odds": rows,
131
+ "tokens": rows,
132
+ }
133
+ ranking = market.get("_ranking")
134
+ if isinstance(ranking, dict):
135
+ summary.update(
136
+ {
137
+ "rankField": ranking.get("rankField"),
138
+ "rankValue": ranking.get("rankValue"),
139
+ "rankingResolved": ranking.get("rankingResolved"),
140
+ "rankingSource": ranking.get("rankingSource"),
141
+ "rankingFallbackUsed": ranking.get("rankingFallbackUsed"),
142
+ }
143
+ )
144
+ ranking_context = market.get("_rankingContext")
145
+ if isinstance(ranking_context, dict):
146
+ summary.update(
147
+ {
148
+ "rankingDegraded": ranking_context.get("rankingDegraded"),
149
+ "rankingIncompleteCount": ranking_context.get("rankingIncompleteCount"),
150
+ "rankingDegradedReason": ranking_context.get("rankingDegradedReason"),
151
+ }
152
+ )
153
+ return summary
154
+
155
+
156
+ def to_pretty_json(data: Any) -> str:
157
+ return json.dumps(data, indent=2, sort_keys=False)