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

285 lines
9.8 KiB
Python

"""inject_v3_trajectory.py — sequence injection, trajectory capture
The unit of experiment is no longer a single pulse. It is a SEQUENCE: 60
consecutive minutes of one trade variable, fed to the lattice as 60 pulses
spaced 2s apart. We capture the full telemetry trajectory (all 8+ channels)
for the duration.
For each of the 6 independent BTC variables, we replay the SAME 60 minutes
of real BTC data, but encoded through that variable. So:
- signed_flow_usd run: 60 minutes' signed-flow values, normalized to ±0.3
- trade_count run: 60 minutes' trade-count values, normalized to ±0.3
- ...etc
The 60 minutes used are the same temporal window for every variable, so the
ONLY thing that differs between runs is which projection of that hour the
lattice sees.
After all 6 runs, the per-run telemetry parquets can be compared offline:
do different variables produce distinguishable trajectories on the same
underlying hour? If yes → the lattice is doing variable-specific
integration, not just amplitude-integration. If no → primitive bottleneck
confirmed at the structural level (not just the per-pulse level).
Output:
/mnt/d/Resonance_Engine/traj/<runid>/{var}.parquet per-variable telemetry trace
/mnt/d/Resonance_Engine/traj/<runid>/meta.json window, magnitudes, settings
Run inside WSL:
cd /mnt/d/Resonance_Engine
setsid nohup python3 -u inject_v3_trajectory.py > /tmp/inject_v3.log 2>&1 < /dev/null & disown
"""
from __future__ import annotations
import glob, json, threading, time
from collections import deque
from pathlib import Path
import numpy as np
import pandas as pd
import pyarrow.parquet as pq
import zmq
# -------- config --------
DATA_DAY_WSL = "/mnt/d/PaperTrader/research/hl_data/minutes/20260601"
COIN = "BTC"
# Same window for all variables. 60 consecutive minutes around the busiest
# part of the day. Use the picker logic from inject_v1 to find a high-activity
# anchor, then take ±30 min around it.
WINDOW_MINUTES = 60
PULSE_SPACING_S = 2.0 # 60 pulses * 2s = 120s per variable run
SETTLE_S = 30 # rest between variable runs
PRE_RECORD_S = 5 # capture some telemetry before first pulse
POST_RECORD_S = 30 # capture telemetry after last pulse
STR_CAP = 0.3
INJECT_X = 512.0
INJECT_Y = 512.0
INJECT_SIG = 32.0
TEL_ADDR = "tcp://127.0.0.1:5556"
CMD_ADDR = "tcp://127.0.0.1:5557"
VARS = [
"mid_price",
"signed_flow_usd",
"trade_count",
"large_print_cnt",
"wallet_entropy",
"vwap_drift",
]
RUN_ID = time.strftime("%Y%m%dT%H%M%S")
OUT_DIR = Path(f"/mnt/d/Resonance_Engine/traj/{RUN_ID}")
OUT_DIR.mkdir(parents=True, exist_ok=True)
# -------- telemetry subscriber --------
class TelSub:
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.history = []
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()
with self.lock:
self.history.append(msg)
except Exception as e:
print(f"[tel] {e}", flush=True)
time.sleep(0.1)
def reset(self):
with self.lock:
self.history = []
def harvest(self):
with self.lock:
return list(self.history)
def close(self):
self.stop = True
self.sub.close()
self.ctx.term()
# -------- data --------
def load_btc_day() -> pd.DataFrame:
files = sorted(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].sort_values("minute").reset_index(drop=True)
def pick_window(df: pd.DataFrame) -> pd.DataFrame:
"""Pick the WINDOW_MINUTES consecutive minutes with the highest combined
absolute z-score sum across the 6 vars. This guarantees the window has
something happening, so each variable's sequence has signal to inject."""
z = np.zeros(len(df))
for v in VARS:
s = df[v]
sd = float(s.std())
if sd == 0:
continue
z += ((s - float(s.mean())) / sd).abs().values
# Rolling sum of |z| across WINDOW_MINUTES
roll = pd.Series(z).rolling(WINDOW_MINUTES).sum()
end_idx = int(roll.idxmax())
start_idx = end_idx - WINDOW_MINUTES + 1
win = df.iloc[start_idx:end_idx + 1].reset_index(drop=True)
return win
def encode_strengths(window: pd.DataFrame, full_day: pd.DataFrame, var: str) -> np.ndarray:
"""Map this variable's window-values to per-pulse strengths in [-STR_CAP, +STR_CAP].
Normalize by the FULL DAY distribution so cross-variable runs are comparable."""
s_full = full_day[var]
mu = float(s_full.mean())
sd = float(s_full.std())
if sd == 0:
return np.zeros(len(window))
z = (window[var].values - mu) / sd
z_clip = np.clip(z, -3.0, 3.0)
return (z_clip / 3.0) * STR_CAP
# -------- main --------
def main():
print(f"[v3] run_id={RUN_ID}", flush=True)
print(f"[v3] out_dir={OUT_DIR}", flush=True)
df_day = load_btc_day()
window = pick_window(df_day)
print(f"[v3] window: minutes {int(window.minute.iloc[0])}..{int(window.minute.iloc[-1])} "
f"(n={len(window)})", flush=True)
# precompute strengths per variable
strengths_by_var = {v: encode_strengths(window, df_day, v) for v in VARS}
for v in VARS:
s = strengths_by_var[v]
print(f" {v:18s} min={s.min():+.4f} max={s.max():+.4f} "
f"mean={s.mean():+.4f} abs_mean={np.abs(s).mean():.4f}",
flush=True)
# write meta
meta = {
"run_id": RUN_ID,
"coin": COIN,
"vars": VARS,
"window_first_minute": int(window.minute.iloc[0]),
"window_last_minute": int(window.minute.iloc[-1]),
"n_pulses": int(len(window)),
"pulse_spacing_s": PULSE_SPACING_S,
"settle_s": SETTLE_S,
"pre_record_s": PRE_RECORD_S,
"post_record_s": POST_RECORD_S,
"inject_x": INJECT_X,
"inject_y": INJECT_Y,
"inject_sigma": INJECT_SIG,
"str_cap": STR_CAP,
"wall_iso_start": time.strftime("%Y-%m-%dT%H:%M:%S"),
}
(OUT_DIR / "meta.json").write_text(json.dumps(meta, indent=2))
# also dump the window itself so we know what raw values went in
window.to_parquet(OUT_DIR / "window.parquet")
# ZMQ setup
tel = TelSub(TEL_ADDR)
ctx = zmq.Context()
pub = ctx.socket(zmq.PUB)
pub.connect(CMD_ADDR)
time.sleep(0.7)
# wait for telemetry to actually start arriving
t0 = time.time()
while not tel.history and time.time() - t0 < 10:
time.sleep(0.2)
if not tel.history:
print("[v3] FATAL: no telemetry on 5556", flush=True)
return
# ---- per-variable sequences ----
for vi, var in enumerate(VARS):
strengths = strengths_by_var[var]
print(f"\n[v3] === run {vi+1}/{len(VARS)}: {var} ===", flush=True)
# capture window starts now
tel.reset()
run_start_wall = time.time()
run_start_cycle = (tel.harvest() or [{}])[-1].get("cycle", -1) if tel.history else -1
# pre-record period (no injects)
time.sleep(PRE_RECORD_S)
# build per-pulse record alongside
pulse_records = []
for i, strn in enumerate(strengths):
cmd = {
"cmd": "inject_density",
"x": INJECT_X,
"y": INJECT_Y,
"sigma": INJECT_SIG,
"strength": float(strn),
}
wall = time.time()
pub.send_string(json.dumps(cmd))
pulse_records.append({
"i": i,
"wall_ts": wall,
"wall_iso": time.strftime("%H:%M:%S", time.localtime(wall)),
"rel_t": wall - run_start_wall,
"minute": int(window.minute.iloc[i]),
"raw_value": float(window[var].iloc[i]),
"strength": float(strn),
})
if i % 10 == 0 or i == len(strengths) - 1:
print(f" pulse {i+1:3d}/{len(strengths)} rel_t={wall-run_start_wall:6.1f}s "
f"str={strn:+.4f}", flush=True)
# pace
target_next = run_start_wall + PRE_RECORD_S + (i + 1) * PULSE_SPACING_S
sleep_for = max(0.0, target_next - time.time())
time.sleep(sleep_for)
# post-record period
time.sleep(POST_RECORD_S)
# harvest telemetry trace
trace = tel.harvest()
if not trace:
print(f" no telemetry captured?!", flush=True)
continue
# turn into dataframe
df_trace = pd.DataFrame(trace)
df_trace["rel_t"] = df_trace["_recv_wall"] - run_start_wall
df_trace["var"] = var
df_trace.to_parquet(OUT_DIR / f"{var}.parquet")
# also write pulse log
pd.DataFrame(pulse_records).to_parquet(OUT_DIR / f"{var}_pulses.parquet")
print(f" trace saved: {len(df_trace)} samples over {df_trace.rel_t.max():.1f}s", flush=True)
# settle between runs
if vi < len(VARS) - 1:
print(f" settle {SETTLE_S}s...", flush=True)
time.sleep(SETTLE_S)
pub.close()
ctx.term()
tel.close()
meta["wall_iso_end"] = time.strftime("%Y-%m-%dT%H:%M:%S")
(OUT_DIR / "meta.json").write_text(json.dumps(meta, indent=2))
print(f"\n[v3] DONE. {OUT_DIR}", flush=True)
if __name__ == "__main__":
main()