Skip to content

Commit e2db91d

Browse files
copyleftdevclaude
andauthored
docs: add examples/polymarket_anomalies.py — prediction-market shocks (#66)
Third worked example: pulls a Polymarket market's price history from the public Gamma + CLOB APIs (read-only, no key), enriches with per-step probability change, runs `anomalyx scan`, and maps findings back to UTC timestamps. Surfaces the information shocks — sharp probability jumps (point/mv) and sustained regime shifts in the odds (coll.cusum). The `timestamp` column is auto-classified a sequence (1.1.1) and skipped, so findings are about the odds, not the clock. Verified on a live market (MicroStrategy-sells-BTC, 648 hourly points): coll.cusum caught the 2026-05-20 regime shift (prob 0.345→0.133), mv.mahalanobis flagged the information-shock hours. Also fills in examples/README.md, which had only the stock example (the journal one from 1.1.1 was missing there). Docs/example only — outside the Cargo workspace, no build/gate/release impact. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ff93a4f commit e2db91d

3 files changed

Lines changed: 206 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
88

9+
### Examples
10+
11+
- **`examples/polymarket_anomalies.py`** — find information shocks in a Polymarket
12+
prediction market: pulls a market's price history from Polymarket's public APIs
13+
(read-only, no key), enriches with the per-step probability change, and scans —
14+
sharp probability jumps (`point` / `mv`) and sustained regime shifts in the odds
15+
(`coll.cusum`), each mapped back to its UTC timestamp. Also lists the journal
16+
example in `examples/README.md` (previously only in the changelog).
17+
918
## [1.1.1] - 2026-06-01
1019

1120
### Fixed

examples/README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,42 @@ On real NVDA history this surfaces, for example, the 2025‑01‑27 DeepSeek sel
3535
volatility, and the second‑half‑2025 price regime shift (`coll.cusum`) — and in
3636
`--baseline` mode, that NVDA's volume and volatility *distributions* differ
3737
sharply from a peer's.
38+
39+
## `journal_anomalies.py`
40+
41+
Finds anomalies in the systemd journal (Linux + systemd). Pipes
42+
`journalctl -o json` to anomalyx on **stdin** (so it content-sniffs as `journal`,
43+
not plain JSON) and maps each finding back to its **timestamp / unit / message**.
44+
45+
```sh
46+
python3 examples/journal_anomalies.py --lines 20000
47+
python3 examples/journal_anomalies.py --since "2 hours ago" --top 20
48+
49+
# Distributional drift between two windows (which units / priorities shifted):
50+
python3 examples/journal_anomalies.py --since "1 hour ago" \
51+
--baseline-since "3 hours ago" --baseline-until "1 hour ago"
52+
```
53+
54+
Single-window finds per-unit content anomalies (e.g. CPU‑usage spikes); the
55+
`--baseline-since` mode runs `dist.chi2` over `_SYSTEMD_UNIT` / `PRIORITY` to flag
56+
units that appeared or whose share changed. Column roles keep journald's many
57+
id / counter / timestamp fields out of the way automatically.
58+
59+
## `polymarket_anomalies.py`
60+
61+
Pulls a prediction market's price history from Polymarket's public APIs
62+
(read-only, no key), enriches it with the per‑step probability change, and finds
63+
the **information shocks** — sharp probability jumps (`point` / `mv`) and
64+
sustained regime shifts in the odds (`coll.cusum`).
65+
66+
```sh
67+
python3 examples/polymarket_anomalies.py # top market by volume
68+
python3 examples/polymarket_anomalies.py "bitcoin" # first match by question/slug
69+
python3 examples/polymarket_anomalies.py "fed" --top 15 # search first, then scan flags
70+
```
71+
72+
> Pass any search term **before** scan flags (the term is an optional positional).
73+
74+
Maps each finding back to its UTC timestamp; the `timestamp` column is
75+
auto-classified a `sequence` and skipped, so the findings are about the odds, not
76+
the clock.

examples/polymarket_anomalies.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
#!/usr/bin/env python3
2+
"""
3+
polymarket_anomalies.py — find information shocks in a Polymarket market.
4+
5+
A prediction market's implied probability is usually smooth; a sudden jump is an
6+
information shock (news, a debate, a resolution). This example pulls a market's
7+
price history from Polymarket's public APIs (Gamma for discovery, CLOB for the
8+
series), enriches it with the per-step probability change, runs `anomalyx scan`,
9+
and maps each finding back to its timestamp — another worked example of consuming
10+
the `tq1` contract on a real time series.
11+
12+
What anomalyx surfaces here:
13+
* point.modz on `prob_change` — the sharp probability jumps (the news days);
14+
* coll.cusum on `prob` — sustained regime shifts in the odds;
15+
* the `timestamp` column is auto-classified a sequence and skipped.
16+
17+
Usage:
18+
cargo install anomalyx # or set $ANOMALYX
19+
python3 examples/polymarket_anomalies.py # top market by volume
20+
python3 examples/polymarket_anomalies.py "bitcoin" # first match by question/slug
21+
python3 examples/polymarket_anomalies.py "fed" --top 15 --fidelity 60
22+
23+
Anything after the known flags passes through to `anomalyx scan`. Read-only,
24+
public data, no API key. Requires: python3 + the `anomalyx` binary (or $ANOMALYX).
25+
Exit code mirrors anomalyx: 0 clean, 1 anomalies found, 2 error.
26+
"""
27+
from __future__ import annotations
28+
29+
import argparse
30+
import csv
31+
import datetime as dt
32+
import json
33+
import os
34+
import shutil
35+
import subprocess
36+
import sys
37+
import tempfile
38+
import urllib.parse
39+
import urllib.request
40+
41+
GAMMA = "https://gamma-api.polymarket.com"
42+
CLOB = "https://clob.polymarket.com"
43+
44+
45+
def _get(url: str, timeout: int = 30) -> bytes:
46+
req = urllib.request.Request(url, headers={"User-Agent": "anomalyx-example/1.0"})
47+
return urllib.request.urlopen(req, timeout=timeout).read()
48+
49+
50+
def pick_market(search: str | None, candidates: int) -> tuple[str, str]:
51+
"""Return (question, clob_token_id) for the chosen market (YES outcome)."""
52+
url = (
53+
f"{GAMMA}/markets?closed=false&order=volumeNum&ascending=false"
54+
f"&limit={max(candidates, 1)}"
55+
)
56+
markets = json.loads(_get(url))
57+
needle = (search or "").lower()
58+
for m in markets:
59+
ids = m.get("clobTokenIds")
60+
if not ids:
61+
continue
62+
text = f"{m.get('question', '')} {m.get('slug', '')}".lower()
63+
if needle and needle not in text:
64+
continue
65+
return m.get("question") or m.get("slug") or "?", json.loads(ids)[0]
66+
sys.exit(f"no open market with price history matched {search!r}")
67+
68+
69+
def fetch_history(token: str, fidelity: int) -> list[tuple[int, float]]:
70+
url = f"{CLOB}/prices-history?market={urllib.parse.quote(token)}&interval=max&fidelity={fidelity}"
71+
pts = json.loads(_get(url)).get("history", [])
72+
if len(pts) < 10:
73+
sys.exit("not enough price history for that market")
74+
return [(int(p["t"]), float(p["p"])) for p in pts]
75+
76+
77+
def write_csv(points: list[tuple[int, float]], path: str) -> list[str]:
78+
"""Write timestamp/prob/prob_change; return the readable timestamps."""
79+
stamps = []
80+
with open(path, "w", newline="") as f:
81+
w = csv.writer(f)
82+
w.writerow(["timestamp", "prob", "prob_change"])
83+
prev = None
84+
for t, p in points:
85+
when = dt.datetime.fromtimestamp(t, dt.timezone.utc).strftime("%Y-%m-%d %H:%M")
86+
stamps.append(when)
87+
w.writerow([when, f"{p:.6f}", "" if prev is None else f"{p - prev:.6f}"])
88+
prev = p
89+
return stamps[1:] # the first row has an empty prob_change and is dropped on parse
90+
91+
92+
def anomalyx_scan(csv_path: str, extra: list[str]) -> dict:
93+
exe = os.environ.get("ANOMALYX", "anomalyx")
94+
if shutil.which(exe) is None and not os.path.exists(exe):
95+
sys.exit(f"`{exe}` not found — run `cargo install anomalyx` or set $ANOMALYX")
96+
proc = subprocess.run([exe, "scan", *extra, csv_path], capture_output=True, text=True)
97+
if proc.returncode == 2:
98+
sys.exit(f"anomalyx error: {proc.stderr.strip()}")
99+
return json.loads(proc.stdout)
100+
101+
102+
def describe_handle(handle: str, dates: list[str]) -> str:
103+
p = handle.split(":")
104+
if p[0] == "cell":
105+
return f"{dates[int(p[2])]} {p[1]}"
106+
if p[0] == "row":
107+
return f"{dates[int(p[1])]} (all columns)"
108+
if p[0] == "range":
109+
a, b = int(p[2]), min(int(p[3]), len(dates) - 1)
110+
return f"{p[1]} {dates[a]} -> {dates[b]}"
111+
if p[0] == "dist":
112+
return f"{p[1]} (distribution)"
113+
return handle
114+
115+
116+
def report(env: dict, dates: list[str]) -> None:
117+
dic = env["dict"]
118+
summ = env["summary"]
119+
print(
120+
f"format={env['format']} rows={env['rows_scanned']} exit={env['exit']} "
121+
f"detected={summ['total']} max_severity={summ.get('max_severity')}"
122+
)
123+
print("roles: " + ", ".join(f"{c['column']}={c['role']}" for c in env.get("roles", [])))
124+
if scope := env.get("scope"):
125+
print(f"scope: emitted {scope['emitted']} of {scope['detected']} (dropped {scope['dropped']})")
126+
print()
127+
for row in env["rows"]:
128+
print(f" [{dic[row[4]]:>8}] {dic[row[0]]:<15} {describe_handle(dic[row[2]], dates)}")
129+
print(f" {dic[row[6]]}")
130+
if not env["rows"]:
131+
print(" (no findings)")
132+
133+
134+
def main() -> None:
135+
ap = argparse.ArgumentParser(
136+
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
137+
)
138+
ap.add_argument("search", nargs="?", help="match a market by question/slug (else top volume)")
139+
ap.add_argument("--candidates", type=int, default=50, help="markets to consider when matching")
140+
ap.add_argument("--fidelity", type=int, default=60, help="price-history resolution in minutes")
141+
args, scan_args = ap.parse_known_args()
142+
143+
question, token = pick_market(args.search, args.candidates)
144+
points = fetch_history(token, args.fidelity)
145+
tmp = tempfile.mkdtemp(prefix="anomalyx-polymarket-")
146+
csv_path = os.path.join(tmp, "market.csv")
147+
dates = write_csv(points, csv_path)
148+
149+
span = f"{points[0][0]} .. {points[-1][0]}"
150+
print(f"# {question}")
151+
print(f"# {len(points)} points, prob {points[0][1]:.3f} -> {points[-1][1]:.3f}\n")
152+
env = anomalyx_scan(csv_path, scan_args)
153+
report(env, dates)
154+
sys.exit(0 if env["exit"] == 0 else 1)
155+
156+
157+
if __name__ == "__main__":
158+
main()

0 commit comments

Comments
 (0)