405 lines
14 KiB
Python
405 lines
14 KiB
Python
"""inject_sn_battery.py — signal-to-noise battery for per-variable lattice response
|
||
|
||
Decision authority: Claude Desktop, 2026-06-06.
|
||
|
||
Methodology
|
||
-----------
|
||
1. Baseline (10 min, no injects):
|
||
subscribe to 5556 telemetry, record stress_xx/yy/xy each sample,
|
||
compute σ per channel = the lattice's intrinsic noise floor.
|
||
|
||
2. Per-variable battery:
|
||
for each independent trade variable (6 of them) at 5 magnitudes
|
||
sampled from the real BTC day distribution (p10, p25, p50, p75, p90),
|
||
do exactly one isolated pulse:
|
||
- record pre-pulse stress (mean of last ~30s window)
|
||
- send single inject_density at (512, 512) sigma=32, strength encoded
|
||
- monitor next ~5s for peak |Δstress_*|
|
||
- wait for full decay (~150s ≈ 15k cycles) before next pulse
|
||
Record peak Δ per axis. S/N = peak_Δ / σ_baseline.
|
||
|
||
Output
|
||
------
|
||
/mnt/d/Resonance_Engine/sn_battery_results.jsonl
|
||
one record per (variable, magnitude) trial:
|
||
{
|
||
"variable": "signed_flow_usd",
|
||
"percentile": 90,
|
||
"raw_value": ...,
|
||
"z": ...,
|
||
"strength": ...,
|
||
"pre_cycle": ..., "post_cycle": ...,
|
||
"pre": {"stress_xx": ..., "stress_yy": ..., "stress_xy": ...,
|
||
"asymmetry": ..., "coherence": ...},
|
||
"peak_delta_abs": {"stress_xx": ..., ...}, # max |during-pre|
|
||
"peak_delta_signed": {"stress_xx": ..., ...}, # signed delta at moment of max abs
|
||
}
|
||
|
||
followed by one summary record at end with sigma_baseline and S/N table.
|
||
|
||
LLM observer is NOT in this loop. Fractonaut watches passively, can be
|
||
queried afterward.
|
||
|
||
Run inside WSL (Windows pyzmq can't reach WSL loopback):
|
||
cd /mnt/d/Resonance_Engine
|
||
setsid nohup python3 -u inject_sn_battery.py > /tmp/sn_battery.log 2>&1 < /dev/null & disown
|
||
"""
|
||
from __future__ import annotations
|
||
import glob, json, math, sys, threading, time
|
||
from collections import deque
|
||
from pathlib import Path
|
||
|
||
import pyarrow.parquet as pq
|
||
import pandas as pd
|
||
import numpy as np
|
||
import zmq
|
||
|
||
# -------- config --------
|
||
DATA_DAY_WSL = "/mnt/d/PaperTrader/research/hl_data/minutes/20260601"
|
||
COIN = "BTC"
|
||
|
||
TEL_ADDR = "tcp://127.0.0.1:5556"
|
||
CMD_ADDR = "tcp://127.0.0.1:5557"
|
||
|
||
OUT_PATH = Path("/mnt/d/Resonance_Engine/sn_battery_results.jsonl")
|
||
|
||
INJECT_X = 512.0
|
||
INJECT_Y = 512.0
|
||
INJECT_SIG = 32.0
|
||
|
||
# Strength encoding: clip(z/3, ±1) * 0.3 — same regime as v1
|
||
STR_CAP = 0.3
|
||
|
||
# Independent trade variables (drop redundant buy/sell, signed_flow has both)
|
||
VARS = [
|
||
"mid_price",
|
||
"signed_flow_usd",
|
||
"trade_count",
|
||
"large_print_cnt",
|
||
"wallet_entropy",
|
||
"vwap_drift",
|
||
]
|
||
|
||
# Magnitudes sampled from real distribution
|
||
PERCENTILES = [10, 25, 50, 75, 90]
|
||
|
||
# Timing (wall-clock seconds; daemon runs ~100 cycle/s)
|
||
BASELINE_S = 600 # 10 min baseline
|
||
PEAK_WATCH_S = 5 # post-inject capture window for finding peak Δ
|
||
DECAY_S = 150 # full-decay wait before next pulse (~15k cycles)
|
||
|
||
# Channels we care about for S/N
|
||
STRESS_CHANNELS = ["stress_xx", "stress_yy", "stress_xy"]
|
||
EXTRA_CHANNELS = ["asymmetry", "coherence", "vel_mean", "vel_var", "vorticity_mean"]
|
||
ALL_CHANNELS = STRESS_CHANNELS + EXTRA_CHANNELS
|
||
|
||
|
||
# -------- telemetry subscriber thread --------
|
||
class TelemetrySub:
|
||
def __init__(self, addr):
|
||
self.ctx = zmq.Context()
|
||
self.sub = self.ctx.socket(zmq.SUB)
|
||
self.sub.setsockopt_string(zmq.SUBSCRIBE, "")
|
||
self.sub.connect(addr)
|
||
self.lock = threading.Lock()
|
||
self.latest = None
|
||
self.history = deque(maxlen=20000) # ~30 min at 10 samples/s
|
||
self.stop = False
|
||
self.thread = threading.Thread(target=self._run, daemon=True)
|
||
self.thread.start()
|
||
|
||
def _run(self):
|
||
while not self.stop:
|
||
try:
|
||
raw = self.sub.recv_string(flags=0)
|
||
msg = json.loads(raw)
|
||
msg["_recv_wall"] = time.time() # stamp arrival
|
||
with self.lock:
|
||
self.latest = msg
|
||
self.history.append(msg)
|
||
except Exception as e:
|
||
print(f"[tel] {e}", flush=True)
|
||
time.sleep(0.1)
|
||
|
||
def get_latest(self):
|
||
with self.lock:
|
||
return dict(self.latest) if self.latest else None
|
||
|
||
def get_history_since(self, t0):
|
||
with self.lock:
|
||
return [m for m in self.history if m.get("_recv_wall", 0) >= t0]
|
||
|
||
def snapshot_history(self):
|
||
with self.lock:
|
||
return list(self.history)
|
||
|
||
def close(self):
|
||
self.stop = True
|
||
self.sub.close()
|
||
self.ctx.term()
|
||
|
||
|
||
# -------- helpers --------
|
||
def load_btc_day():
|
||
files = glob.glob(str(Path(DATA_DAY_WSL) / "*.parquet"))
|
||
dfs = [pq.read_table(f).to_pandas() for f in files]
|
||
df = pd.concat(dfs).sort_values("minute").reset_index(drop=True)
|
||
return df[df.coin == COIN].reset_index(drop=True)
|
||
|
||
|
||
def percentile_strengths(df: pd.DataFrame):
|
||
"""For each VAR, for each percentile, pick the row at that percentile
|
||
of the variable and compute (raw, z, strength)."""
|
||
out = {}
|
||
for v in VARS:
|
||
s = df[v]
|
||
mu, sd = float(s.mean()), float(s.std())
|
||
out[v] = []
|
||
for p in PERCENTILES:
|
||
raw = float(np.percentile(s, p))
|
||
z = (raw - mu) / sd if sd > 0 else 0.0
|
||
z_clip = max(-3.0, min(3.0, z))
|
||
strength = (z_clip / 3.0) * STR_CAP
|
||
out[v].append({
|
||
"percentile": p,
|
||
"raw_value": raw,
|
||
"z": z,
|
||
"z_clip": z_clip,
|
||
"strength": strength,
|
||
})
|
||
return out
|
||
|
||
|
||
def send_pulse(pub, strength):
|
||
cmd = {
|
||
"cmd": "inject_density",
|
||
"x": INJECT_X,
|
||
"y": INJECT_Y,
|
||
"sigma": INJECT_SIG,
|
||
"strength": strength,
|
||
}
|
||
pub.send_string(json.dumps(cmd))
|
||
|
||
|
||
def channel_stats(samples, ch):
|
||
vals = [s[ch] for s in samples if ch in s]
|
||
if not vals:
|
||
return None
|
||
arr = np.array(vals, dtype=float)
|
||
return {
|
||
"n": len(arr),
|
||
"mean": float(arr.mean()),
|
||
"std": float(arr.std()),
|
||
"min": float(arr.min()),
|
||
"max": float(arr.max()),
|
||
}
|
||
|
||
|
||
def appendln(rec):
|
||
with OUT_PATH.open("a", encoding="utf-8") as fh:
|
||
fh.write(json.dumps(rec) + "\n")
|
||
|
||
|
||
# -------- main --------
|
||
def main():
|
||
OUT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||
# truncate
|
||
OUT_PATH.write_text("", encoding="utf-8")
|
||
|
||
print(f"[BAT] start {time.strftime('%Y-%m-%dT%H:%M:%S')}", flush=True)
|
||
appendln({"event": "battery_start", "wall_iso": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||
"params": {"baseline_s": BASELINE_S, "peak_watch_s": PEAK_WATCH_S,
|
||
"decay_s": DECAY_S, "vars": VARS, "percentiles": PERCENTILES,
|
||
"x": INJECT_X, "y": INJECT_Y, "sigma": INJECT_SIG,
|
||
"str_cap": STR_CAP}})
|
||
|
||
# ---- load data, compute strengths ----
|
||
print(f"[BAT] loading {COIN} parquet day", flush=True)
|
||
df = load_btc_day()
|
||
strengths = percentile_strengths(df)
|
||
for v in VARS:
|
||
for tr in strengths[v]:
|
||
print(f" {v:18s} p{tr['percentile']:>2} raw={tr['raw_value']:>+15.4f} "
|
||
f"z={tr['z']:>+6.2f} str={tr['strength']:>+7.4f}", flush=True)
|
||
appendln({"event": "trial_grid", "strengths": strengths})
|
||
|
||
# ---- start subscribers ----
|
||
tel = TelemetrySub(TEL_ADDR)
|
||
ctx_pub = zmq.Context()
|
||
pub = ctx_pub.socket(zmq.PUB)
|
||
pub.connect(CMD_ADDR)
|
||
time.sleep(0.7) # slow joiner
|
||
|
||
# ---- phase 1: baseline ----
|
||
print(f"[BAT] phase 1: {BASELINE_S}s baseline (no injects)", flush=True)
|
||
t0 = time.time()
|
||
# wait until telemetry actually arriving
|
||
while tel.get_latest() is None and time.time() - t0 < 30:
|
||
time.sleep(0.5)
|
||
if tel.get_latest() is None:
|
||
print("[BAT] FATAL: no telemetry on 5556 within 30s", flush=True)
|
||
sys.exit(1)
|
||
|
||
baseline_start_ts = time.time()
|
||
baseline_start_cycle = tel.get_latest().get("cycle", -1)
|
||
# let it run
|
||
end = baseline_start_ts + BASELINE_S
|
||
while time.time() < end:
|
||
time.sleep(5)
|
||
l = tel.get_latest()
|
||
if l:
|
||
print(f" [baseline t+{int(time.time()-baseline_start_ts):4d}s] "
|
||
f"cycle={l.get('cycle','?')} "
|
||
f"asym={l.get('asymmetry',0):.4f} "
|
||
f"sxx={l.get('stress_xx',0):+.5f} "
|
||
f"syy={l.get('stress_yy',0):+.5f} "
|
||
f"sxy={l.get('stress_xy',0):+.5f}", flush=True)
|
||
|
||
baseline_samples = tel.get_history_since(baseline_start_ts)
|
||
print(f"[BAT] baseline collected {len(baseline_samples)} samples", flush=True)
|
||
|
||
baseline_stats = {ch: channel_stats(baseline_samples, ch) for ch in ALL_CHANNELS}
|
||
appendln({"event": "baseline_stats",
|
||
"n_samples": len(baseline_samples),
|
||
"start_cycle": baseline_start_cycle,
|
||
"stats": baseline_stats})
|
||
print("[BAT] baseline σ:", flush=True)
|
||
for ch in STRESS_CHANNELS:
|
||
s = baseline_stats[ch]
|
||
if s:
|
||
print(f" {ch:14s} mean={s['mean']:+.6f} std={s['std']:.6f} "
|
||
f"range=[{s['min']:+.6f},{s['max']:+.6f}]", flush=True)
|
||
|
||
# ---- phase 2: per-variable pulses ----
|
||
# Build flat trial list, shuffle for fairness (avoid ordering bias)
|
||
trials = []
|
||
for v in VARS:
|
||
for tr in strengths[v]:
|
||
trials.append({"variable": v, **tr})
|
||
rng = np.random.default_rng(seed=42)
|
||
rng.shuffle(trials)
|
||
print(f"[BAT] phase 2: {len(trials)} trials, ~{(PEAK_WATCH_S+DECAY_S)*len(trials)/60:.1f} min", flush=True)
|
||
appendln({"event": "trial_order",
|
||
"order": [(t["variable"], t["percentile"]) for t in trials]})
|
||
|
||
for i, trial in enumerate(trials):
|
||
var = trial["variable"]
|
||
pct = trial["percentile"]
|
||
strn = trial["strength"]
|
||
# pre-pulse snapshot: last 5s before fire
|
||
pre_window_t = time.time() - 5.0
|
||
pre_samples = [s for s in tel.snapshot_history()
|
||
if s.get("_recv_wall", 0) >= pre_window_t]
|
||
pre_stats = {ch: channel_stats(pre_samples, ch) for ch in ALL_CHANNELS}
|
||
latest_pre = tel.get_latest() or {}
|
||
pre_cycle = latest_pre.get("cycle", -1)
|
||
pre_means = {ch: (pre_stats[ch]["mean"] if pre_stats[ch] else None) for ch in ALL_CHANNELS}
|
||
|
||
# fire
|
||
wall_fire = time.time()
|
||
send_pulse(pub, strn)
|
||
print(f"[BAT] trial {i+1:2d}/{len(trials)} {time.strftime('%H:%M:%S')} "
|
||
f"{var:18s} p{pct:>2} str={strn:+7.4f} pre_cycle={pre_cycle}",
|
||
flush=True)
|
||
|
||
# capture peak over next PEAK_WATCH_S seconds
|
||
peak_end = wall_fire + PEAK_WATCH_S
|
||
peak_abs = {ch: 0.0 for ch in ALL_CHANNELS}
|
||
peak_signed = {ch: 0.0 for ch in ALL_CHANNELS}
|
||
peak_at_cycle = {ch: None for ch in ALL_CHANNELS}
|
||
while time.time() < peak_end:
|
||
time.sleep(0.05)
|
||
l = tel.get_latest()
|
||
if not l:
|
||
continue
|
||
for ch in ALL_CHANNELS:
|
||
base = pre_means[ch]
|
||
cur = l.get(ch)
|
||
if base is None or cur is None:
|
||
continue
|
||
d = cur - base
|
||
if abs(d) > peak_abs[ch]:
|
||
peak_abs[ch] = abs(d)
|
||
peak_signed[ch] = d
|
||
peak_at_cycle[ch] = l.get("cycle")
|
||
|
||
post_cycle = (tel.get_latest() or {}).get("cycle", -1)
|
||
|
||
# write trial record
|
||
rec = {
|
||
"event": "trial",
|
||
"i": i,
|
||
"variable": var,
|
||
"percentile": pct,
|
||
"raw_value": trial["raw_value"],
|
||
"z": trial["z"],
|
||
"z_clip": trial["z_clip"],
|
||
"strength": strn,
|
||
"wall_iso": time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime(wall_fire)),
|
||
"pre_cycle": pre_cycle,
|
||
"post_cycle": post_cycle,
|
||
"pre_means": pre_means,
|
||
"peak_delta_abs": peak_abs,
|
||
"peak_delta_signed": peak_signed,
|
||
"peak_at_cycle": peak_at_cycle,
|
||
}
|
||
# immediate S/N preview using baseline σ
|
||
sn_preview = {}
|
||
for ch in STRESS_CHANNELS:
|
||
sigma = baseline_stats[ch]["std"] if baseline_stats[ch] else None
|
||
if sigma and sigma > 0:
|
||
sn_preview[ch] = peak_abs[ch] / sigma
|
||
rec["sn_preview_vs_baseline_sigma"] = sn_preview
|
||
appendln(rec)
|
||
print(f" peak |Δ| sxx={peak_abs['stress_xx']:.5f} "
|
||
f"syy={peak_abs['stress_yy']:.5f} "
|
||
f"sxy={peak_abs['stress_xy']:.5f} "
|
||
f"asym={peak_abs['asymmetry']:.4f} "
|
||
f"sn_xy={sn_preview.get('stress_xy',0):.1f}",
|
||
flush=True)
|
||
|
||
# wait full decay
|
||
time.sleep(DECAY_S)
|
||
|
||
# ---- phase 3: summary ----
|
||
print(f"[BAT] all trials done. Computing S/N table.", flush=True)
|
||
# Re-read jsonl, build table
|
||
trial_recs = []
|
||
with OUT_PATH.open() as fh:
|
||
for line in fh:
|
||
r = json.loads(line)
|
||
if r.get("event") == "trial":
|
||
trial_recs.append(r)
|
||
|
||
summary = {}
|
||
for r in trial_recs:
|
||
v = r["variable"]; p = r["percentile"]
|
||
key = f"{v}@p{p}"
|
||
sn = {}
|
||
for ch in STRESS_CHANNELS:
|
||
sigma = baseline_stats[ch]["std"] if baseline_stats[ch] else None
|
||
d = r["peak_delta_abs"].get(ch)
|
||
if sigma and sigma > 0 and d is not None:
|
||
sn[ch] = d / sigma
|
||
summary[key] = {
|
||
"strength": r["strength"],
|
||
"peak_delta_abs": {ch: r["peak_delta_abs"].get(ch) for ch in STRESS_CHANNELS},
|
||
"peak_delta_signed":{ch: r["peak_delta_signed"].get(ch) for ch in STRESS_CHANNELS},
|
||
"sn": sn,
|
||
}
|
||
|
||
appendln({"event": "summary",
|
||
"baseline_sigma": {ch: (baseline_stats[ch]["std"] if baseline_stats[ch] else None)
|
||
for ch in STRESS_CHANNELS},
|
||
"trials": summary})
|
||
|
||
print(f"[BAT] DONE. results: {OUT_PATH}", flush=True)
|
||
print(f"[BAT] {time.strftime('%Y-%m-%dT%H:%M:%S')}", flush=True)
|
||
pub.close()
|
||
ctx_pub.term()
|
||
tel.close()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|