2f4f2ccbbe
research/ is a hard silo (README rules): read-only tape, no bot imports, own launchd (com.jaxperro.research-nightly 09:15, after daily ingest). - tape.py: proxy-resolution (the 742/742-validated method), niche + crypto strike/expiry/sprint parsers, tick loaders - sim.py: FAK execution replayer; hold_s=3 fitted on 29 real labeled live attempts (79% fill/miss classification), price noise 2-4c, measured OPTIMISM BIAS -2c/fill carried into every verdict threshold - requote.py: crater refill timing per niche (crypto 94% <4s, esports 83% <10s, sports needs ~25s, geo/politics minutes) -> params/requote_timing.json - study_flow.py + robustness: in-play surge momentum. Identity NULL result: 10 pooled controls +23.85/fill == informed +23.68 -> hypothesis revised at freeze, surge-EV primary, identity secondary (#16) - study_oracle.py: oracle digital fair value. 86% craters, winner's-curse inversion at big edges, nothing frozen (no cell at 30 fills) (#17) - forward.py + nightly.sh: re-scores frozen studies on last 3 tape days, appends forward_ledger.jsonl; verdicts ONLY from post-freeze rows Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
219 lines
9.3 KiB
Python
219 lines
9.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Study A — flow-state signal: trade the informed herd's lean, not any one
|
|
wallet's print.
|
|
|
|
Hypothesis (pre-registered, issue TBD): when the trailing net $-flow of the
|
|
tape-scored informed set crosses a threshold in one in-play sports/esports
|
|
market, the market's resolution probability exceeds its price by enough to
|
|
clear real execution (calibrated sim: lag, FAK crater, fees, ~2c optimism
|
|
bias) — because the herd's aggregate lean IS the event detector, arriving
|
|
before makers reprice.
|
|
|
|
Discipline:
|
|
* informed set as-of T uses ONLY tape < T (scoring + resolutions as-of T).
|
|
* FIT day and grid are fixed below; selection rule: highest after-fee EV
|
|
per trigger at p50 lag with >= MIN_FILLS fills. Params freeze into
|
|
params/study_flow.json; the holdout day is scored ONCE with frozen
|
|
params; forward days accrue via forward.py.
|
|
* Controls: 3 activity-matched shuffled sets through the identical
|
|
pipeline — the edge must vanish when the wallets are random.
|
|
|
|
Signal: per token (sports/esports niche only), rolling W-second sum of
|
|
signed informed flow (+buy$ / -sell$). Trigger when sum >= F with the
|
|
triggering print inside PRICE_BAND; COOLDOWN_S per token. Entry at the
|
|
triggering print's price through sim; hold to resolution; stake $100 flat.
|
|
"""
|
|
import json
|
|
import os
|
|
import time
|
|
|
|
import tape
|
|
import sim as simmod
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
PARAMS_F = os.path.join(HERE, "params", "study_flow.json")
|
|
|
|
# pre-registered exploration grid + universe (do not widen after the fact)
|
|
GRID = {"top_n": [50, 150], "window_s": [60, 300], "flow_usd": [300, 1000]}
|
|
PRICE_BAND = (0.10, 0.90)
|
|
NICHES = {"sports", "esports"}
|
|
COOLDOWN_S = 900
|
|
STAKE = 100.0
|
|
MIN_FILLS = 30
|
|
SET_MIN_Z, SET_MIN_BETS = 2.5, 6
|
|
|
|
|
|
def informed_set(db, before_ts, top_n):
|
|
"""Top-N wallets by improbability z using ONLY tape before before_ts."""
|
|
tape.build_resolved(db, t_end=before_ts)
|
|
rows = db.execute(f"""
|
|
WITH bets AS (
|
|
SELECT tr.wallet,
|
|
any_value(tk.payout) payout,
|
|
sum(CASE WHEN tr.side='BUY' THEN tr.size ELSE -tr.size END) net,
|
|
sum(CASE WHEN tr.side='BUY' THEN tr.size*tr.price END)
|
|
/ nullif(sum(CASE WHEN tr.side='BUY' THEN tr.size END),0) vwap
|
|
FROM trades tr JOIN res_tok tk ON tr.asset = tk.asset
|
|
WHERE tr.ts <= {before_ts}
|
|
GROUP BY tr.wallet, tr.asset
|
|
HAVING net >= 5 AND vwap BETWEEN 0.05 AND 0.95
|
|
)
|
|
SELECT wallet,
|
|
count(*) n,
|
|
sum(CASE WHEN payout=1.0 THEN 1 ELSE 0 END) wins,
|
|
sum(vwap) exp_w, sum(vwap*(1-vwap)) var_s,
|
|
sum(net*(payout - vwap)) pnl
|
|
FROM bets GROUP BY wallet
|
|
HAVING n >= {SET_MIN_BETS} AND var_s > 0 AND pnl > 0
|
|
""").fetchall()
|
|
scored = []
|
|
for w, n, wins, exp_w, var_s, pnl in rows:
|
|
z = (wins - exp_w) / (var_s ** 0.5)
|
|
if z >= SET_MIN_Z:
|
|
scored.append((z, w))
|
|
scored.sort(reverse=True)
|
|
return [w for _, w in scored[:top_n]]
|
|
|
|
|
|
def matched_random_set(db, before_ts, size, seed):
|
|
"""Activity-matched control: wallets with >= 20 trades before before_ts,
|
|
deterministic pseudo-shuffle by md5(wallet||seed) — no RNG state."""
|
|
rows = db.execute(f"""
|
|
SELECT wallet FROM trades WHERE ts <= {before_ts}
|
|
GROUP BY wallet HAVING count(*) >= 20
|
|
ORDER BY md5(wallet || '{seed}') LIMIT {size}""").fetchall()
|
|
return [w for (w,) in rows]
|
|
|
|
|
|
def signals(db, wallets, t_lo, t_hi, window_s, flow_usd):
|
|
"""Rolling-window triggers over the informed set's prints."""
|
|
if not wallets:
|
|
return []
|
|
rows = db.execute("""
|
|
SELECT tr.asset, tr.ts, tr.side, tr.price, tr.size, any_value(tr.title)
|
|
FROM trades tr
|
|
WHERE tr.ts > ? AND tr.ts <= ?
|
|
AND tr.wallet IN (SELECT unnest(?::varchar[]))
|
|
GROUP BY tr.asset, tr.ts, tr.side, tr.price, tr.size, tr.tx
|
|
ORDER BY tr.asset, tr.ts""", [t_lo, t_hi, wallets]).fetchall()
|
|
trig, cur, buf, last_trig = [], None, [], {}
|
|
for asset, ts, side, price, size, title in rows:
|
|
if asset != cur:
|
|
cur, buf = asset, []
|
|
if tape.niche(title) not in NICHES:
|
|
continue
|
|
usd = price * size * (1 if side == "BUY" else -1)
|
|
buf.append((ts, usd))
|
|
while buf and buf[0][0] < ts - window_s:
|
|
buf.pop(0)
|
|
flow = sum(u for _, u in buf)
|
|
if flow >= flow_usd and PRICE_BAND[0] <= price <= PRICE_BAND[1] \
|
|
and ts - last_trig.get(asset, 0) >= COOLDOWN_S:
|
|
last_trig[asset] = ts
|
|
trig.append({"asset": asset, "ts": ts, "p_ref": price,
|
|
"flow": round(flow), "title": title})
|
|
return trig
|
|
|
|
|
|
def score(db, triggers, lag_s, hold_s):
|
|
"""Sim each trigger; outcome from res_tok (resolved-by-tape-end only)."""
|
|
s = simmod.Sim(db, lag_s=lag_s, hold_s=hold_s)
|
|
res = dict(fills=0, misses=0, pending=0, pnl=0.0, wins=0, staked=0.0)
|
|
for t in triggers:
|
|
pay = db.execute("SELECT payout::DOUBLE FROM res_tok WHERE asset = ?",
|
|
[t["asset"]]).fetchone()
|
|
r = s.try_buy(t["asset"], t["ts"], t["p_ref"], stake_usd=STAKE)
|
|
if not r["filled"]:
|
|
res["misses"] += 1
|
|
continue
|
|
if pay is None: # fired, filled, not yet resolved
|
|
res["pending"] += 1
|
|
continue
|
|
res["fills"] += 1
|
|
res["staked"] += r["cost"]
|
|
pnl = r["shares"] * (pay[0] - r["price"]) - r["fee"]
|
|
res["pnl"] += pnl
|
|
res["wins"] += pay[0] == 1.0
|
|
if res["fills"]:
|
|
res["ev_per_fill"] = round(res["pnl"] / res["fills"], 2)
|
|
res["hit"] = round(res["wins"] / res["fills"], 3)
|
|
res["pnl"] = round(res["pnl"], 2)
|
|
return res
|
|
|
|
|
|
def run_cell(db, as_of, t_lo, t_hi, top_n, window_s, flow_usd, hold_s, wallets=None):
|
|
S = wallets if wallets is not None else informed_set(db, as_of, top_n)
|
|
tape.build_resolved(db) # scoring truth = full tape
|
|
trig = signals(db, S, t_lo, t_hi, window_s, flow_usd)
|
|
out = {"triggers": len(trig), "set_size": len(S)}
|
|
for lag, tag in ((simmod.LAG_P50, "p50"), (simmod.LAG_P90, "p90")):
|
|
out[tag] = score(db, trig, lag, hold_s)
|
|
return out, trig
|
|
|
|
|
|
def main():
|
|
db = tape.connect()
|
|
cal = json.load(open(os.path.join(HERE, "params", "sim_calibration.json")))
|
|
hold_s = cal["hold_s"]
|
|
lo, hi = db.execute("SELECT min(ts), max(ts) FROM trades").fetchone()
|
|
day = lambda d, h=0: time.mktime(time.strptime(f"2026-07-{d:02d}", "%Y-%m-%d")) \
|
|
- time.timezone + h * 3600
|
|
fit_lo, fit_hi = day(19), day(20) # fit day: Jul 19 UTC
|
|
hold_lo, hold_hi = day(20), hi # holdout: Jul 20 (partial)
|
|
|
|
print("== FIT (Jul 19, set as-of Jul 19 00:00 UTC) ==")
|
|
results = []
|
|
for tn in GRID["top_n"]:
|
|
for w in GRID["window_s"]:
|
|
for f in GRID["flow_usd"]:
|
|
r, _ = run_cell(db, fit_lo, fit_lo, fit_hi, tn, w, f, hold_s)
|
|
ev = r["p50"].get("ev_per_fill")
|
|
results.append(((tn, w, f), r))
|
|
print(f"top{tn:<4} W={w:<4} F=${f:<5} -> trig {r['triggers']:>4} "
|
|
f"fills {r['p50']['fills']:>3} miss {r['p50']['misses']:>3} "
|
|
f"EV/fill {ev if ev is not None else '—'} "
|
|
f"hit {r['p50'].get('hit', '—')}")
|
|
eligible = [(p, r) for p, r in results
|
|
if r["p50"]["fills"] >= MIN_FILLS and "ev_per_fill" in r["p50"]]
|
|
if not eligible:
|
|
print("\nNO cell reached MIN_FILLS — study inconclusive at this tape size.")
|
|
return
|
|
best_p, best_r = max(eligible, key=lambda pr: pr[1]["p50"]["ev_per_fill"])
|
|
tn, w, f = best_p
|
|
print(f"\nFROZEN: top_n={tn} window={w}s flow=${f} "
|
|
f"(fit EV/fill {best_r['p50']['ev_per_fill']}, hit {best_r['p50']['hit']})")
|
|
|
|
print("\n== CONTROLS (fit day, matched random sets) ==")
|
|
controls = []
|
|
for seed in (1, 2, 3):
|
|
S = matched_random_set(db, fit_lo, tn, seed)
|
|
r, _ = run_cell(db, fit_lo, fit_lo, fit_hi, tn, w, f, hold_s, wallets=S)
|
|
controls.append(r)
|
|
print(f"seed {seed}: trig {r['triggers']} fills {r['p50']['fills']} "
|
|
f"EV/fill {r['p50'].get('ev_per_fill', '—')} "
|
|
f"hit {r['p50'].get('hit', '—')}")
|
|
|
|
print("\n== HOLDOUT (Jul 20 partial, set as-of Jul 20 00:00 UTC) ==")
|
|
hr, htrig = run_cell(db, hold_lo, hold_lo, hold_hi, tn, w, f, hold_s)
|
|
print(f"trig {hr['triggers']} fills {hr['p50']['fills']} "
|
|
f"miss {hr['p50']['misses']} pending {hr['p50']['pending']} "
|
|
f"EV/fill {hr['p50'].get('ev_per_fill', '—')} "
|
|
f"hit {hr['p50'].get('hit', '—')} (p90 lag: EV "
|
|
f"{hr['p90'].get('ev_per_fill', '—')})")
|
|
|
|
json.dump({"frozen": {"top_n": tn, "window_s": w, "flow_usd": f,
|
|
"hold_s": hold_s, "price_band": PRICE_BAND,
|
|
"niches": sorted(NICHES), "cooldown_s": COOLDOWN_S,
|
|
"stake": STAKE,
|
|
"set_min_z": SET_MIN_Z, "set_min_bets": SET_MIN_BETS},
|
|
"fit": best_r, "fit_grid": [{"params": p, **r} for p, r in results],
|
|
"controls": controls, "holdout": hr,
|
|
"frozen_at": time.strftime("%Y-%m-%d %H:%M UTC", time.gmtime()),
|
|
"pending_triggers": htrig},
|
|
open(PARAMS_F, "w"), indent=1, default=float)
|
|
print(f"\nfroze {PARAMS_F}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|