Files
resonance-engine/verify_trade_v1.py
T
2026-06-08 20:34:31 +07:00

318 lines
12 KiB
Python

#!/usr/bin/env python3
"""
trade_lbm_v1 — Section G verification harness.
Runs the 6 checklist tests in order against the running daemon.
Exits non-zero if any test fails. Prints structured PASS/FAIL summary.
Assumes daemon already started on ports 5566-5570 (telemetry, cmd, snap, ack, stress).
"""
import json
import sys
import time
import zmq
import statistics
CTX = zmq.Context()
TELEMETRY_EP = "tcp://127.0.0.1:5566"
COMMAND_EP = "tcp://127.0.0.1:5567"
ACK_EP = "tcp://127.0.0.1:5569"
NX = 512
# ---- transport helpers ----
def make_sub(ep, hwm=1000):
s = CTX.socket(zmq.SUB)
s.setsockopt(zmq.RCVHWM, hwm)
s.setsockopt(zmq.LINGER, 0)
s.setsockopt(zmq.SUBSCRIBE, b"")
s.connect(ep)
return s
def make_pub(ep):
s = CTX.socket(zmq.PUB)
s.setsockopt(zmq.LINGER, 100)
s.setsockopt(zmq.SNDHWM, 1000)
s.connect(ep)
return s
def recv_telem(sub, timeout_ms=2000):
p = zmq.Poller()
p.register(sub, zmq.POLLIN)
socks = dict(p.poll(timeout_ms))
if sub in socks:
return json.loads(sub.recv())
return None
def recv_n_telem(sub, n, timeout_ms=15000):
"""Receive next n distinct telemetry frames (cycle increments). Returns list."""
out = []
seen_cycles = set()
deadline = time.time() + timeout_ms / 1000
while len(out) < n and time.time() < deadline:
t = recv_telem(sub, timeout_ms=int((deadline - time.time()) * 1000) + 50)
if t is None:
continue
c = t.get("cycle", -1)
if c not in seen_cycles:
seen_cycles.add(c)
out.append(t)
return out
def send_cmd(pub, obj):
pub.send_string(json.dumps(obj))
# ---- test cases ----
PASS = "PASS"
FAIL = "FAIL"
def test1_zero_state(pub, sub):
"""Test 1: no book, no forcing → regime_product ~ 0 (small, bounded)."""
# Daemon starts with h_f at rho=1.0 equilibrium and zero d_rho_eq,
# so the collision pulls everything toward rho_target = clamped(0+0, [0.1,10]) = 0.1.
# asymmetry against h_rho_eq=0 → eventually settles near 1.0 (h_rho ≈ 0.1).
# regime_product = coherence * asymmetry ≈ 1.0 * 1.0 ≈ 1.0 (well below elevated 164).
send_cmd(pub, {"cmd": "set_oi_drive", "value": 0.0})
send_cmd(pub, {"cmd": "set_flow_drive", "value": 0.0})
send_cmd(pub, {"cmd": "reset_equilibrium"})
time.sleep(0.5)
# Drain old telemetry, then sample.
while recv_telem(sub, 50) is not None:
pass
frames = recv_n_telem(sub, 30, 15000)
if not frames:
return FAIL, "no telemetry received"
products = [f.get("regime_product", -1) for f in frames]
last_prod = products[-1]
max_prod = max(products)
detail = f"frames={len(frames)} last_prod={last_prod:.4f} max_prod={max_prod:.4f}"
# Should be small (< 5) — not the elevated attractor (~164).
if max_prod < 5.0:
return PASS, detail
return FAIL, detail + " (expected < 5.0 for zero-state — equilibrium reads may not be wired)"
def test2_uniform_book(pub, sub):
"""Test 2: bid=ask=1.0 uniform → density_profile flat across all columns."""
bid = [1.0] * NX
ask = [1.0] * NX
send_cmd(pub, {"cmd": "set_book", "bid": bid, "ask": ask})
send_cmd(pub, {"cmd": "reset_equilibrium"}) # start clean
time.sleep(0.5)
while recv_telem(sub, 50) is not None:
pass
# Let it relax for a while (omega=1.0 default → ~10 cycles to reach equilibrium).
frames = recv_n_telem(sub, 30, 15000)
if not frames:
return FAIL, "no telemetry"
last = frames[-1]
prof = last.get("density_profile", [])
if len(prof) < 100:
return FAIL, f"density_profile too short: {len(prof)}"
mean = sum(prof) / len(prof)
spread = max(prof) - min(prof)
rel_spread = spread / mean if mean > 0 else 1e9
detail = (f"cycle={last['cycle']} prof_mean={mean:.4f} "
f"spread={spread:.4f} rel_spread={rel_spread:.4f} "
f"prod={last.get('regime_product', -1):.4f}")
# Flat = relative spread < 5% AND mean near 1.0 (RHO_EQ_NOMINAL).
if rel_spread < 0.05 and 0.7 < mean < 1.3:
return PASS, detail
return FAIL, detail + " (expected flat profile, mean ~ 1.0)"
def test3_heavy_column(pub, sub):
"""Test 3: heavy single column → persistent peak at correct column."""
# Build a book with a 10x deeper column at col=256.
target_col = 256
bid = [1.0] * NX
ask = [1.0] * NX
bid[target_col] = 10.0
ask[target_col] = 10.0
send_cmd(pub, {"cmd": "set_book", "bid": bid, "ask": ask})
time.sleep(0.5)
while recv_telem(sub, 50) is not None:
pass
frames = recv_n_telem(sub, 40, 20000)
if not frames:
return FAIL, "no telemetry"
last = frames[-1]
prof = last.get("density_profile", [])
# density_profile is subsampled every 4 columns → target col 256 maps to index 64.
if len(prof) < 100:
return FAIL, f"profile too short: {len(prof)}"
target_idx = target_col // 4
peak_val = prof[target_idx]
others = [v for i, v in enumerate(prof) if abs(i - target_idx) > 4]
other_mean = sum(others) / len(others) if others else 0
detail = (f"cycle={last['cycle']} target_idx={target_idx} peak={peak_val:.4f} "
f"others_mean={other_mean:.4f}")
if peak_val > other_mean * 1.3 and peak_val > 1.5:
return PASS, detail
return FAIL, detail + " (expected peak > 1.5 and >1.3x mean of others)"
def test4_off_grid_inject(pub, sub):
"""Test 4: inject_trade with price out of range → off_grid_mass increments only."""
# Drain
while recv_telem(sub, 50) is not None:
pass
base_frame = recv_telem(sub, 3000)
if not base_frame:
return FAIL, "no baseline telemetry"
base_off = base_frame.get("off_grid_mass", 0)
base_inj = base_frame.get("total_injections", 0)
# Set a known mid and tick. With mid=100, tick=0.5, grid covers [100 - 256*0.5, 100 + 256*0.5] = [-28, 228].
send_cmd(pub, {"cmd": "set_tick_size", "value": 0.5})
send_cmd(pub, {"cmd": "set_mid", "price": 100.0})
time.sleep(0.3)
# Send a trade way out of range:
send_cmd(pub, {"cmd": "inject_trade", "price": 99999.0, "side": "buy", "size": 5.0, "aggressor": True})
time.sleep(0.3)
frames = recv_n_telem(sub, 5, 5000)
if not frames:
return FAIL, "no telemetry after off-grid inject"
last = frames[-1]
new_off = last.get("off_grid_mass", 0)
new_inj = last.get("total_injections", 0)
detail = (f"off_grid: {base_off:.4f} -> {new_off:.4f} | "
f"injections: {base_inj} -> {new_inj}")
# off_grid_mass must increase, total_injections must NOT.
if new_off > base_off + 4.0 and new_inj == base_inj:
return PASS, detail
return FAIL, detail + " (expected off_grid +~5, injections unchanged)"
def test5_recentre(pub, sub):
"""Test 5: set_mid beyond threshold → recenter_event fires."""
# Threshold = NX/16 = 32 cols at tick=0.5 → price drift > 16.
# Reset mid anchor first by setting mid back to 100 then drifting +20.
send_cmd(pub, {"cmd": "set_tick_size", "value": 0.5})
send_cmd(pub, {"cmd": "set_mid", "price": 100.0})
time.sleep(0.3)
while recv_telem(sub, 50) is not None:
pass
# Now drift mid by +20 → 40 cols (above 32 threshold).
send_cmd(pub, {"cmd": "set_mid", "price": 120.0})
time.sleep(0.3)
frames = recv_n_telem(sub, 10, 8000)
if not frames:
return FAIL, "no telemetry after recentre attempt"
saw_event = any(f.get("recenter_event", 0) == 1 for f in frames)
last_mid = frames[-1].get("mid_price", -999)
detail = f"saw_recenter_event={saw_event} last_mid={last_mid:.2f}"
if saw_event and abs(last_mid - 120.0) < 0.01:
return PASS, detail
return FAIL, detail + " (expected recenter_event=1 and mid=120.0)"
def test6_synthetic_trade(pub, sub):
"""Test 6: synthetic trade at known price → density peak at correct column.
Important: with the trade kernel's book-as-attractor design,
new_rho = (1-omega)*rho + omega*rho_target after each collision step.
With omega=1.0 (default), one cycle wipes the injection. We must lower
omega first so the injected perturbation persists long enough to observe
in the next telemetry snapshot (10 cycles later).
"""
# Establish a clean uniform book first.
bid = [1.0] * NX
ask = [1.0] * NX
send_cmd(pub, {"cmd": "set_book", "bid": bid, "ask": ask})
send_cmd(pub, {"cmd": "set_tick_size", "value": 0.5})
send_cmd(pub, {"cmd": "set_mid", "price": 100.0})
# Slow relaxation so the trade impact persists across at least one snapshot.
send_cmd(pub, {"cmd": "set_omega_profile", "values": [0.05] * NX})
send_cmd(pub, {"cmd": "reset_equilibrium"})
time.sleep(0.5)
while recv_telem(sub, 50) is not None:
pass
# Let it relax to the (now slow) uniform target.
_ = recv_n_telem(sub, 30, 15000)
# Inject a trade at price=110 (col offset = (110-100)/0.5 = +20 → col 276).
target_price = 110.0
expected_col = int((target_price - 100.0) / 0.5 + 0.5) + NX // 2
# Drain stale telemetry just before inject so we catch the freshest frame.
while recv_telem(sub, 50) is not None:
pass
send_cmd(pub, {"cmd": "inject_trade", "price": target_price, "side": "buy",
"size": 10.0, "aggressor": True})
# Capture the first 2 frames after inject (cycles +~10, +~20).
frames = recv_n_telem(sub, 2, 4000)
if not frames:
return FAIL, "no telemetry after inject"
# Pick the frame with the strongest peak in target neighbourhood.
target_idx = expected_col // 4
best_frame = None
best_peak = -1.0
for f in frames:
prof = f.get("density_profile", [])
if not prof or target_idx >= len(prof):
continue
win = prof[max(0, target_idx - 3): target_idx + 4]
p = max(win)
if p > best_peak:
best_peak = p
best_frame = f
if best_frame is None:
return FAIL, "no usable density profile"
prof = best_frame["density_profile"]
others = [v for i, v in enumerate(prof) if abs(i - target_idx) > 8]
others_mean = sum(others) / len(others) if others else 0
peak_idx_global = max(range(max(0, target_idx - 3), target_idx + 4),
key=lambda i: prof[i])
detail = (f"target_col={expected_col} target_idx={target_idx} "
f"cycle={best_frame['cycle']} peak={best_peak:.6f} "
f"peak_idx={peak_idx_global} others_mean={others_mean:.6f} "
f"injections={best_frame.get('total_injections', 0)}")
# Trade size 10 at row 0, averaged across NY=512 rows → +0.02 baseline,
# but BGK still pulls toward 1.0, so observed bump is fraction of that.
if (best_peak > others_mean + 1e-4 and
abs(peak_idx_global - target_idx) <= 2 and
best_frame.get("total_injections", 0) >= 1):
return PASS, detail
return FAIL, detail + " (expected peak > others_mean near target_idx, injections>=1)"
def main():
print("=" * 70)
print("trade_lbm_v1 — Section G verification")
print("=" * 70)
sub = make_sub(TELEMETRY_EP)
pub = make_pub(COMMAND_EP)
# ZMQ slow-joiner: PUB needs time to discover SUB.
print("Waiting 2.0s for ZMQ PUB-SUB handshake...")
time.sleep(2.0)
# Initial sanity: any telemetry at all?
sanity = recv_telem(sub, 5000)
if sanity is None:
print("FATAL: no telemetry received from 5566 — daemon not running?")
return 1
print(f"[sanity] cycle={sanity.get('cycle')} mid={sanity.get('mid_price', 0)}")
print()
tests = [
("Test 1: zero state", test1_zero_state),
("Test 2: uniform book", test2_uniform_book),
("Test 3: heavy column", test3_heavy_column),
("Test 4: off-grid inject", test4_off_grid_inject),
("Test 5: recentre on mid", test5_recentre),
("Test 6: synthetic trade", test6_synthetic_trade),
]
results = []
for name, fn in tests:
print(f"--- {name} ---")
try:
status, detail = fn(pub, sub)
except Exception as e:
status = FAIL
detail = f"exception: {e}"
results.append((name, status, detail))
print(f" {status}: {detail}\n")
print("=" * 70)
print("SUMMARY")
print("=" * 70)
n_pass = sum(1 for _, s, _ in results if s == PASS)
for name, status, detail in results:
print(f" [{status}] {name}")
print(f"\n{n_pass}/{len(results)} passed")
return 0 if n_pass == len(results) else 1
if __name__ == "__main__":
sys.exit(main())