Files
resonance-engine/validate_trade_v1.py
T
2026-06-08 21:34:30 +07:00

443 lines
20 KiB
Python

"""trade_lbm_v1 — §10 validation gate: Event A (breakout) + Event B (consolidation).
Replays per-minute BTC aggregate data from HL April parquets into trade_lbm_v1
via set_book + inject_trade + set_mid, queries Fractonaut at boundary/event
points, and verifies the §10 pass criteria.
Owner: RESONANCE.
"""
import json
import os
import sys
import time
import urllib.request
from pathlib import Path
import numpy as np
import pandas as pd
import zmq
# ──────────────────────── config ────────────────────────
DAEMON_TELEM = "tcp://127.0.0.1:5566"
DAEMON_CMD = "tcp://127.0.0.1:5567"
DAEMON_ACK = "tcp://127.0.0.1:5569"
FRACT_URL = "http://127.0.0.1:28822/ask"
FRACT_TO_S = 240
PARQUET = "/mnt/d/Resonance_Engine/_tools_btc_april.parquet"
RUN_ROOT = "/mnt/d/Resonance_Engine/traj"
NX = 512
TICK_USD = 1.0
SNAPSHOT_INTERVAL = 200 # ~5 snapshots/sec
MINUTE_DT = 0.5 # wall seconds per replayed minute
WARMUP_MIN = 30 # minutes ignored before consolidation can "count"
RELAX_BOOT_S = 4.0 # let field absorb book before replay starts
BOOK_DECAY_L = 30 # exponential book width in columns
OMEGA_REPLAY = 0.3 # lets injections leave a multi-cycle trace, book still dominates
# ──────────────────────── ZMQ helpers ────────────────────────
def make_sub(ctx, ep, topic=b""):
s = ctx.socket(zmq.SUB)
s.setsockopt(zmq.SUBSCRIBE, topic)
s.setsockopt(zmq.RCVHWM, 4096)
s.connect(ep)
return s
def make_pub(ctx, ep):
s = ctx.socket(zmq.PUB)
s.setsockopt(zmq.SNDHWM, 1024)
s.connect(ep)
return s
def send_cmd(pub, obj):
pub.send_string(json.dumps(obj))
def drain_telem(sub, max_ms=200):
"""Drain any pending telemetry frames; return list of decoded dicts."""
out = []
poller = zmq.Poller(); poller.register(sub, zmq.POLLIN)
deadline = time.time() + max_ms / 1000.0
while time.time() < deadline:
socks = dict(poller.poll(timeout=20))
if sub in socks and socks[sub] == zmq.POLLIN:
try:
raw = sub.recv_string(zmq.NOBLOCK)
try: out.append(json.loads(raw))
except Exception: pass
except zmq.Again:
pass
else:
break
return out
# ──────────────────────── Fractonaut helper ────────────────────────
def fractonaut_ask(question):
body = json.dumps({"question": question}).encode()
req = urllib.request.Request(
FRACT_URL, data=body, headers={"Content-Type": "application/json"})
t0 = time.time()
try:
with urllib.request.urlopen(req, timeout=FRACT_TO_S) as r:
data = json.loads(r.read())
return {"ok": True, "response": data.get("response", ""),
"turn": data.get("turn"), "model": data.get("model"),
"elapsed_s": round(time.time() - t0, 2)}
except Exception as e:
return {"ok": False, "error": str(e),
"elapsed_s": round(time.time() - t0, 2)}
# ──────────────────────── book synthesis ────────────────────────
def synthesize_book(taker_buy_usd: float, taker_sell_usd: float):
"""Build NX bid+ask arrays from per-minute taker totals.
Exponentially-decaying depth around mid; bids below, asks above."""
bid = np.zeros(NX, dtype=np.float64)
ask = np.zeros(NX, dtype=np.float64)
half = NX // 2
# offsets 1..half on each side
offs = np.arange(1, half + 1)
decay = np.exp(-offs / BOOK_DECAY_L)
decay /= decay.sum()
bid_vals = max(taker_buy_usd, 0.0) * decay
ask_vals = max(taker_sell_usd, 0.0) * decay
# bids at half-1..0 (closer to mid is index half-1)
bid[half-1::-1] = bid_vals
ask[half:half+len(decay)] = ask_vals
# normalise so mean total ~ 1.0 to keep rho_eq in clamp range
total = bid + ask
m = total.mean()
if m > 1e-12:
bid = bid / m
ask = ask / m
return bid.tolist(), ask.tolist()
# ──────────────────────── data loader ────────────────────────
def load_btc(start_min, end_min):
df = pd.read_parquet(PARQUET)
df = df[(df["minute"] >= start_min) & (df["minute"] <= end_min)]
return df.sort_values("minute").reset_index(drop=True)
# ──────────────────────── one event replay ────────────────────────
def run_event(run_id: str, label: str, start_min: int, end_min: int,
opening_q: str, closing_q: str,
tag_event_filter, log_dir: Path):
"""Replay minutes [start_min, end_min] and record events + Fractonaut answers."""
print(f"\n══════ {label} (minutes {start_min}{end_min}, {end_min-start_min+1} mins) ══════")
df = load_btc(start_min, end_min)
if len(df) == 0:
print(f" no data for window"); return None
print(f" loaded {len(df)} BTC minutes mid: ${df['mid_price'].iloc[0]:,.2f} → ${df['mid_price'].iloc[-1]:,.2f}")
ctx = zmq.Context.instance()
sub = make_sub(ctx, DAEMON_TELEM)
ack = make_sub(ctx, DAEMON_ACK)
pub = make_pub(ctx, DAEMON_CMD)
time.sleep(2.0) # PUB-SUB slow-joiner
# ─── reset + boot ───
send_cmd(pub, {"cmd": "reset_equilibrium"}); time.sleep(0.2)
send_cmd(pub, {"cmd": "set_tick_size", "value": TICK_USD}); time.sleep(0.05)
send_cmd(pub, {"cmd": "set_snapshot_interval", "interval": SNAPSHOT_INTERVAL}); time.sleep(0.05)
send_cmd(pub, {"cmd": "set_omega_profile", "values": [OMEGA_REPLAY] * NX}); time.sleep(0.05)
# anchor mid + first book
first = df.iloc[0]
send_cmd(pub, {"cmd": "set_mid", "price": float(first["mid_price"])}); time.sleep(0.1)
bid, ask = synthesize_book(float(first["taker_buy_usd"]), float(first["taker_sell_usd"]))
send_cmd(pub, {"cmd": "set_book", "bid": bid, "ask": ask}); time.sleep(0.1)
print(f" booted; relaxing {RELAX_BOOT_S}s before replay…")
time.sleep(RELAX_BOOT_S)
drain_telem(sub, 200)
fract_log = log_dir / "fractonaut_validation.jsonl"
events_log = log_dir / f"events_{label}.jsonl"
frames_log = log_dir / f"frames_{label}.jsonl"
fract_log.parent.mkdir(parents=True, exist_ok=True)
def write_fract(entry):
with open(fract_log, "a") as f:
f.write(json.dumps(entry) + "\n")
def write_event(entry):
with open(events_log, "a") as f:
f.write(json.dumps(entry) + "\n")
def write_frame(entry):
with open(frames_log, "a") as f:
f.write(json.dumps(entry) + "\n")
# ─── opening question (give Fractonaut current field state) ───
drain_telem(sub, 100)
frames = drain_telem(sub, 1000)
snap = frames[-1] if frames else {}
cycle = snap.get("cycle", 0)
coh = snap.get("coherence", 0.0)
asy = snap.get("asymmetry", 0.0)
rprod = snap.get("regime_product", 0.0)
open_full = (f"{opening_q}\n\n"
f"[trade_lbm_v1 field snapshot] cycle={cycle} "
f"coherence={coh:.4f} asymmetry={asy:.4f} "
f"regime_product={rprod:.4f} "
f"book mid=${float(first['mid_price']):,.2f} "
f"taker_buy=${float(first['taker_buy_usd']):,.0f} "
f"taker_sell=${float(first['taker_sell_usd']):,.0f}")
print(f" Fractonaut OPENING …", flush=True)
fa = fractonaut_ask(open_full)
print(f" fractonaut: {fa.get('elapsed_s')}s ok={fa['ok']}")
write_fract({"event_label": label, "phase": "open", "ts": time.time(),
"question": open_full, "reply": fa})
# ─── replay loop ───
seen_event_keys = set() # dedupe events
detected_events = []
minute_count = 0
t_start = time.time()
first_post_warmup_event = None
last_recentre = 0
total_recentres = 0
sum_asymmetry = 0.0
sum_regime = 0.0
n_samples = 0
asked_for_kind = set() # at most one Fractonaut query per event-kind per window
armed = False # send arm_consolidation once warmup elapses
for idx, row in df.iterrows():
mid = float(row["mid_price"])
tbuy = float(row["taker_buy_usd"])
tsel = float(row["taker_sell_usd"])
sflow = float(row["signed_flow_usd"])
tcnt = int(row["trade_count"])
bid, ask = synthesize_book(tbuy, tsel)
# Arm the consolidation detector once warmup elapses. Per §10:
# detector must not fire in the first WARMUP_MIN minutes.
if not armed and minute_count >= WARMUP_MIN:
send_cmd(pub, {"cmd": "arm_consolidation"})
armed = True
print(f" [t={minute_count:4d} min] arm_consolidation sent")
# update mid (may trigger recentre), update book, inject net flow as one trade
send_cmd(pub, {"cmd": "set_mid", "price": mid})
send_cmd(pub, {"cmd": "set_book", "bid": bid, "ask": ask})
# inject: split into buy and sell halves at mid (we lose price granularity)
# scale: 1 unit of "size" maps to a rho perturbation of size/NY=size/512.
# Use USD/MID/100 → ~hundreds of size units per minute.
if tbuy > 0:
send_cmd(pub, {"cmd": "inject_trade", "price": mid + TICK_USD,
"size": tbuy / mid / 100.0, "side": "buy", "aggressor": True})
if tsel > 0:
send_cmd(pub, {"cmd": "inject_trade", "price": mid - TICK_USD,
"size": tsel / mid / 100.0, "side": "sell", "aggressor": True})
time.sleep(MINUTE_DT)
frames = drain_telem(sub, 50)
for fr in frames:
sum_asymmetry += abs(fr.get("asymmetry", 0.0))
sum_regime += abs(fr.get("regime_product", 0.0))
n_samples += 1
if fr.get("recenter_event"):
total_recentres += 1
for ev in (fr.get("events") or []):
key = (ev.get("kind"), ev.get("col"), round(ev.get("price", 0), 2))
if key in seen_event_keys:
continue
seen_event_keys.add(key)
entry = {"label": label, "minute_idx": minute_count,
"minute_unix": int(row["minute"]) * 60,
"cycle": fr.get("cycle"), "mid": mid,
"event": ev,
"field_asy": fr.get("asymmetry"),
"field_coh": fr.get("coherence"),
"regime_product": fr.get("regime_product")}
detected_events.append(entry)
write_event(entry)
if tag_event_filter(ev) and minute_count >= WARMUP_MIN and \
first_post_warmup_event is None:
first_post_warmup_event = entry
print(f" [t={minute_count:4d} min] event {ev.get('kind')} "
f"col={ev.get('col')} price=${ev.get('price', 0):,.2f}")
# Mid-replay Fractonaut query: one per event-kind per window.
ev_kind = ev.get("kind")
if ev_kind not in asked_for_kind and minute_count >= WARMUP_MIN:
asked_for_kind.add(ev_kind)
# Compose context from this telemetry frame
dens = fr.get("density_profile") or []
div = fr.get("divergence_profile") or []
vort = fr.get("vorticity_profile") or []
def stats(a):
if not a: return "n/a"
arr = np.array(a)
return f"min={arr.min():.4f} max={arr.max():.4f} mean={arr.mean():.4f} std={arr.std():.4f}"
mid_q = (
f"Event detector fired: {ev_kind} at col={ev.get('col')} "
f"price=${ev.get('price', 0):,.2f} magnitude={ev.get('mag', 0):.3f} "
f"persisted_rows={ev.get('rows', 0)}.\n"
f"[trade_lbm_v1 field snapshot at this moment] "
f"cycle={fr.get('cycle')} coherence={fr.get('coherence'):.4f} "
f"asymmetry={fr.get('asymmetry'):.4f} "
f"regime_product={fr.get('regime_product'):.4f} "
f"mid=${mid:,.2f}\n"
f"density_profile (every 4th col, {len(dens)} pts): {stats(dens)}\n"
f"divergence_profile (every 4th col, {len(div)} pts): {stats(div)}\n"
f"vorticity_profile (every 4th col, {len(vort)} pts): {stats(vort)}\n\n"
f"Does the field configuration support this reading? "
f"What do you observe in the density and vorticity profiles?"
)
print(f" Fractonaut MID-{ev_kind}", flush=True)
fa_mid = fractonaut_ask(mid_q)
print(f" fractonaut: {fa_mid.get('elapsed_s')}s ok={fa_mid['ok']}")
write_fract({"event_label": label, "phase": f"event_{ev_kind}",
"ts": time.time(), "minute_idx": minute_count,
"event": ev, "question": mid_q, "reply": fa_mid})
# periodic frame log (every 10 min)
if minute_count % 10 == 0 and frames:
last = frames[-1]
write_frame({
"label": label, "minute_idx": minute_count,
"minute_unix": int(row["minute"]) * 60,
"mid": mid, "cycle": last.get("cycle"),
"coherence": last.get("coherence"),
"asymmetry": last.get("asymmetry"),
"regime_product": last.get("regime_product"),
"vel_mean": last.get("vel_mean"),
"vorticity_mean": last.get("vorticity_mean"),
"recenter": last.get("recenter_event"),
"n_events_pending": len(last.get("events") or []),
})
minute_count += 1
# ─── closing question ───
drain_telem(sub, 200)
frames = drain_telem(sub, 1000)
snap = frames[-1] if frames else {}
last_mid = float(df["mid_price"].iloc[-1])
end_summary = (f"{closing_q}\n\n"
f"[replay summary] minutes={minute_count} "
f"end_mid=${last_mid:,.2f} "
f"recentres={total_recentres} events_detected={len(detected_events)} "
f"mean_|asymmetry|={sum_asymmetry/max(n_samples,1):.4f} "
f"mean_|regime_product|={sum_regime/max(n_samples,1):.4f}")
print(f" Fractonaut CLOSING …", flush=True)
fa = fractonaut_ask(end_summary)
print(f" fractonaut: {fa.get('elapsed_s')}s ok={fa['ok']}")
write_fract({"event_label": label, "phase": "close", "ts": time.time(),
"question": end_summary, "reply": fa})
elapsed = time.time() - t_start
print(f" replay done in {elapsed:.1f}s events={len(detected_events)} "
f"recentres={total_recentres}")
return {
"label": label,
"minutes": minute_count,
"n_events": len(detected_events),
"first_post_warmup_event": first_post_warmup_event,
"events": detected_events,
"recentres": total_recentres,
"elapsed_s": round(elapsed, 1),
"mean_asy": sum_asymmetry / max(n_samples, 1),
"mean_regime_product": sum_regime / max(n_samples, 1),
}
# ──────────────────────── entry ────────────────────────
def main():
run_id = time.strftime("%Y%m%d_%H%M%S")
log_dir = Path(f"{RUN_ROOT}/validation_{run_id}")
log_dir.mkdir(parents=True, exist_ok=True)
print(f"RUN_ID={run_id} log_dir={log_dir}")
# Event B: 2026-04-03 18:29 UTC → 2026-04-04 06:29 UTC (12h, 0.305% excursion)
b = run_event(
run_id, "EVENT_B_consolidation",
start_min=29587349, end_min=29587349 + 720,
opening_q=("Replay starting. Describe the field state as the book loads "
"for the first time. This is a 12-hour consolidation window "
"(BTC moves <0.5% total). What do you expect to see in the "
"density and vorticity profiles?"),
closing_q=("Replay complete for this consolidation window. What "
"structural features persisted across the full 12 hours? "
"What appeared and dissolved? Did the field hold or break?"),
tag_event_filter=lambda ev: ev.get("kind") == "consolidation",
log_dir=log_dir)
# Event A: 2026-04-07 22:29 → 23:29 UTC (+3.19% in 60 min); 3h window centred = 21:00→24:00
a_centre_start = 29593260 # 2026-04-07 21:00 UTC
a = run_event(
run_id, "EVENT_A_breakout",
start_min=a_centre_start, end_min=a_centre_start + 180,
opening_q=("Replay starting. Describe the field state as the book loads "
"for the first time. This is a 3-hour breakout window — BTC "
"moves +3.19% in ~60 min starting roughly 89 minutes into the "
"window. What pre-breakout structure do you expect to detect?"),
closing_q=("Replay complete for this breakout window. What structural "
"features persisted across the 3 hours? What appeared and "
"dissolved? Did the field signal the breakout in advance, "
"during, or after?"),
tag_event_filter=lambda ev: ev.get("kind") == "breakout",
log_dir=log_dir)
# ─── verdicts ───
print("\n" + "=" * 70)
print("PASS / FAIL")
print("=" * 70)
# Event B: consolidation event must fire at least once AND not in first 30 min
b_pass = False
if b is not None:
consol_events = [e for e in b["events"] if e["event"]["kind"] == "consolidation"]
early = [e for e in consol_events if e["minute_idx"] < WARMUP_MIN]
late = [e for e in consol_events if e["minute_idx"] >= WARMUP_MIN]
b_pass = (len(late) >= 1) and (len(early) == 0)
print(f" EVENT B: consolidation events: early(<30min)={len(early)} "
f"late(>=30min)={len(late)}{'PASS' if b_pass else 'FAIL'}")
# Event A: breakout event fires within 30 min of the move; col matches direction.
# Move starts at minute 89, ends at minute 149. "Within 30 min of the actual
# move" = between minute 89 and 119. Direction = UP, so col > NX/2 expected.
a_pass = False
if a is not None:
bo = [e for e in a["events"] if e["event"]["kind"] == "breakout"]
in_window = [e for e in bo if 89 <= e["minute_idx"] <= 119]
right_side = [e for e in in_window if e["event"].get("col", 0) > NX // 2]
a_pass = len(right_side) >= 1
print(f" EVENT A: breakout events: total={len(bo)} in [89,119]={len(in_window)} "
f"on UP side={len(right_side)}{'PASS' if a_pass else 'FAIL'}")
print(f"\nOVERALL: {'PASS' if (b_pass and a_pass) else 'FAIL'}\n")
summary = {
"run_id": run_id, "ts": time.time(),
"log_dir": str(log_dir),
"event_B": {**(b or {}), "pass": b_pass, "events": None,
"first_post_warmup_event": (b or {}).get("first_post_warmup_event")},
"event_A": {**(a or {}), "pass": a_pass, "events": None,
"first_post_warmup_event": (a or {}).get("first_post_warmup_event")},
}
with open(log_dir / "summary.json", "w") as f:
json.dump(summary, f, indent=2, default=str)
print(f"summary written: {log_dir / 'summary.json'}")
print(f"fractonaut log: {log_dir / 'fractonaut_validation.jsonl'}")
if __name__ == "__main__":
main()