#!/usr/bin/env python3 """Replicate a row of the Stockonomy error series with this one file and the public API; no account, no key, no help from us. python3 reproduce_series.py --base https:// [--row 4] [--limit 40] It downloads the series and the exact claims that row ran (checking their sha256 against the row, which the anchor's timestamp covers), asks the public API to re-run each claim (figures, segments and growth; only claims already in the committed file can be asked for), and prints how many verdicts equal the ones recorded. The API is rate limited (about 60 a minute), so a full row takes a few minutes; `--limit` runs the first N claims. Standard library only. """ import argparse import hashlib import json import sys import time import urllib.error import urllib.request def fetch(url: str) -> bytes: for _ in range(4): try: with urllib.request.urlopen(urllib.request.Request(url, headers={"User-Agent": "series-reproduction"}), timeout=60) as r: return r.read() except urllib.error.HTTPError as e: if e.code != 429: raise time.sleep(30) # the per-minute allowance; wait and try again raise SystemExit(f"still rate limited: {url}") def reproduce(base: str, row: int | None, limit: int | None, get=fetch, pause: float = 1.2) -> dict: series = json.loads(get(f"{base}/check/record/series"))["series"] r = series[(row or series[-1]["version"]) - 1] body = get(f"{base}/check/record/series/claims/{r['version']}") if hashlib.sha256(body).hexdigest() != r.get("claims_sha256"): raise SystemExit("the claims file is not the one the row records (sha256 differs): stop") claims = json.loads(body) out = {"row": r["version"], "run_on": r["on"], "claims": len(claims), "checked": 0, "same_verdict": 0, "different": []} for i, c in list(enumerate(claims))[:limit]: now = json.loads(get(f"{base}/check/record/series/claims/{r['version']}/{i}")).get("verdict") out["checked"] += 1 if now == c["ours"]: out["same_verdict"] += 1 else: out["different"].append({"ticker": c["ticker"], "claim": c["claim"], "recorded": c["ours"], "now": now}) time.sleep(pause) return out def main(argv=None) -> int: ap = argparse.ArgumentParser() ap.add_argument("--base", required=True) ap.add_argument("--row", type=int) ap.add_argument("--limit", type=int) a = ap.parse_args(argv) res = reproduce(a.base.rstrip("/"), a.row, a.limit) print(json.dumps(res, indent=1)) return 0 if not res["different"] else 1 if __name__ == "__main__": sys.exit(main())