285 lines
9.8 KiB
Python
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()
|