"""regime_classifier_injector.py — 3-channel lattice regime classifier. DIRECTIVE FROM CLAUDE DESKTOP — Option B (2026-06-08) After 6h of 8-tension encoding confirming the predictive-feature path is dead, abandon "lattice as predictor". Use the lattice as a BINARY REGIME CLASSIFIER on top of trade_count (the strongest validated raw signal). INPUTS — three channels, injected per minute (no 4hr aggregation, no levels/vels): 1. trade_count z-score (rolling 500min one-sided) → CENTER (512, 512) 2. taker_buy_usd z-score (rolling 500min one-sided) → (400, 512) 3. taker_sell_usd z-score (rolling 500min one-sided) → (624, 512) Strength = z / 3 capped ±1.0. Sigma = 32. No exotic geometry: just three gaussian blobs along x, with buy at left and sell at right so an imbalance creates a directional asymmetry in the stress field. Why these three: - trade_count: validated +1.0 Spearman rank stability IS/OOS. - taker_buy + taker_sell SEPARATELY (not signed_flow): signed_flow shows zero predictive value; the components separately carry volume information the lattice can integrate. OUTPUTS: Same 9 telemetry channels as before (asymmetry, coherence, stress_xx, ...). Downstream analyzer uses arm A free-running quantiles to set p10/p90 thresholds for HIGH/LOW/NEUTRAL regime labels. SUCCESS CRITERION (downstream test, not in this script): |fwd_60| volatility ratio for (HIGH TENSION ∩ top-decile trade_count) minutes vs (top-decile trade_count alone). Current best is 2.42x. Beat that or the lattice is redundant. OUTPUT FILES: /mnt/d/Resonance_Engine/traj/regime_/ meta.json arm_A_no_inject.parquet arm_T_3ch.parquet progress.log """ from __future__ import annotations import argparse, glob, json, queue, threading, time, urllib.request, urllib.error from pathlib import Path import numpy as np import pandas as pd import zmq # ───────────────────────── config ───────────────────────── DATA_ROOT = "/mnt/d/PaperTrader/research/hl_data/minutes" COIN = "BTC" DEFAULT_DAYS_GLOB = "20260415*" # z-score rolling window (one-sided, past-only via shift(1)) Z_WIN_MIN = 500 Z_MIN_PERIODS = 100 # strength = z / Z_DENOM capped to STR_CAP Z_DENOM = 3.0 STR_CAP = 1.0 # Lattice spatial layout (1024×1024) CENTER = (512.0, 512.0) TAKER_BUY_XY = (400.0, 512.0) # left of center TAKER_SELL_XY = (624.0, 512.0) # right of center SIGMA = 32.0 # Timing PER_MINUTE_MS = 100 # 10× realtime WAIT_AFTER_INJECT_MS = 60 # only 3 injects, less settling time needed COOLDOWN_BETWEEN_ARMS_S = 1200 # 20 min # ZMQ TEL_ADDR = "tcp://127.0.0.1:5556" CMD_ADDR = "tcp://127.0.0.1:5557" CHANNELS = ["asymmetry", "coherence", "stress_xx", "stress_yy", "stress_xy", "vorticity_mean", "vel_mean", "vel_max", "vel_var"] # Fractonaut HTTP API (read-only observer, port 28822 — note: 28821 in directive # was wrong; actual code uses 28822) FRACTO_URL = "http://127.0.0.1:28822/ask" FRACTO_TIMEOUT_S = 180 # Ollama can take 30-60s for gemma3:4b responses FRACTO_QUEUE_MAX = 64 # cap pending queries — drop new ones if full RUN_ID = time.strftime("%Y%m%dT%H%M%S") OUT_DIR = Path(f"/mnt/d/Resonance_Engine/traj/regime_{RUN_ID}") PROGRESS = OUT_DIR / "progress.log" FRACTO_LOG = OUT_DIR / "fractonaut_observations.jsonl" def log(msg: str) -> None: line = f"[{time.strftime('%Y-%m-%dT%H:%M:%S')}] {msg}" print(line, flush=True) OUT_DIR.mkdir(parents=True, exist_ok=True) with PROGRESS.open("a") as f: f.write(line + "\n") # ───────────────────────── Fractonaut querier ───────────────────────── class FractoQuerier: """Fire-and-forget background querier. Submit() returns immediately; a worker thread serializes calls to /ask (Ollama only handles one request at a time anyway) and appends responses to fractonaut_observations.jsonl. """ def __init__(self, url: str = FRACTO_URL): self.url = url self.q: queue.Queue = queue.Queue(maxsize=FRACTO_QUEUE_MAX) self.dropped = 0 self.submitted = 0 self.responded = 0 self.errors = 0 self._stop = threading.Event() self._t = threading.Thread(target=self._worker, daemon=True) self._t.start() def submit(self, question: str, ctx: dict) -> None: """Non-blocking. Drops the query if the queue is full.""" item = { "submit_wall": time.time(), "question": question, "context": ctx, } try: self.q.put_nowait(item) self.submitted += 1 except queue.Full: self.dropped += 1 def _worker(self) -> None: while not self._stop.is_set(): try: item = self.q.get(timeout=1.0) except queue.Empty: continue self._call(item) self.q.task_done() def _call(self, item: dict) -> None: body = json.dumps({"question": item["question"]}).encode() req = urllib.request.Request( self.url, data=body, headers={"Content-Type": "application/json"}, method="POST", ) t0 = time.time() resp_text = None err = None try: with urllib.request.urlopen(req, timeout=FRACTO_TIMEOUT_S) as r: resp_json = json.loads(r.read()) resp_text = resp_json.get("response", "") self.responded += 1 except Exception as e: err = str(e) self.errors += 1 elapsed = time.time() - t0 entry = { "submit_wall": item["submit_wall"], "response_wall": time.time(), "elapsed_s": elapsed, "context": item["context"], "question": item["question"], "response": resp_text, "error": err, } OUT_DIR.mkdir(parents=True, exist_ok=True) with FRACTO_LOG.open("a") as f: f.write(json.dumps(entry) + "\n") def drain(self, max_wait_s: float = 600.0) -> None: """Block until queue is empty or timeout.""" deadline = time.time() + max_wait_s while time.time() < deadline: if self.q.empty(): # let in-flight call finish time.sleep(1) if self.q.empty(): return time.sleep(2) def stop(self) -> None: self._stop.set() self._t.join(timeout=5) def stats(self) -> str: return (f"fracto submitted={self.submitted} responded={self.responded} " f"dropped={self.dropped} errors={self.errors} pending={self.q.qsize()}") # ───────────────────────── ZMQ telemetry ───────────────────────── class LatestTel: def __init__(self, addr: str): self.ctx = zmq.Context.instance() self.sock = self.ctx.socket(zmq.SUB) self.sock.connect(addr) self.sock.setsockopt(zmq.SUBSCRIBE, b"") self.sock.setsockopt(zmq.RCVHWM, 2000) self.latest: dict | None = None self.latest_wall: float = 0.0 self.n_seen = 0 self._stop = threading.Event() self._t = threading.Thread(target=self._run, daemon=True) self._t.start() def _run(self): while not self._stop.is_set(): if self.sock.poll(100): try: raw = self.sock.recv_string(zmq.NOBLOCK) self.latest = json.loads(raw) self.latest_wall = time.time() self.n_seen += 1 except Exception: pass def snapshot(self) -> tuple[dict | None, float]: return self.latest, self.latest_wall def stop(self): self._stop.set() self._t.join(timeout=2) try: self.sock.close(0) except Exception: pass # ───────────────────────── data loading ───────────────────────── def load_btc(days_glob: str) -> pd.DataFrame: day_dirs = sorted(glob.glob(f"{DATA_ROOT}/{days_glob}")) if not day_dirs: raise RuntimeError(f"no day dirs matching {days_glob} in {DATA_ROOT}") log(f"loading {len(day_dirs)} day dirs ({day_dirs[0].split('/')[-1]} .. {day_dirs[-1].split('/')[-1]})") dfs = [] for d in day_dirs: for f in sorted(glob.glob(f"{d}/*.parquet")): dfs.append(pd.read_parquet(f)) df = pd.concat(dfs, ignore_index=True) df = df[df.coin == COIN].sort_values("minute").drop_duplicates("minute").reset_index(drop=True) log(f"loaded {len(df)} unique minutes of {COIN}") return df def compute_zscores(df: pd.DataFrame) -> pd.DataFrame: """Rolling 500-min one-sided z-scores for trade_count, taker_buy_usd, taker_sell_usd. Past-only (shift(1) before rolling).""" out = df.copy() for col in ["trade_count", "taker_buy_usd", "taker_sell_usd"]: s = out[col].astype(float).shift(1) mu = s.rolling(Z_WIN_MIN, min_periods=Z_MIN_PERIODS).mean() sd = s.rolling(Z_WIN_MIN, min_periods=Z_MIN_PERIODS).std() out[f"{col}_z"] = (out[col].astype(float) - mu) / sd.replace(0, np.nan) return out # ───────────────────────── injection ───────────────────────── def _send_inject(pub: zmq.Socket, x: float, y: float, strength: float, sigma: float = SIGMA) -> None: payload = {"cmd": "inject_density", "x": float(x), "y": float(y), "sigma": float(sigma), "strength": float(strength)} pub.send_string(json.dumps(payload)) def _z_to_strength(z: float) -> float | None: if not np.isfinite(z): return None s = float(np.clip(z / Z_DENOM, -STR_CAP, +STR_CAP)) if abs(s) < 1e-6: return None return s def encode_trade_count(pub: zmq.Socket, z: float) -> None: s = _z_to_strength(z) if s is None: return cx, cy = CENTER _send_inject(pub, cx, cy, s) def encode_taker_buy(pub: zmq.Socket, z: float) -> None: s = _z_to_strength(z) if s is None: return x, y = TAKER_BUY_XY _send_inject(pub, x, y, s) def encode_taker_sell(pub: zmq.Socket, z: float) -> None: s = _z_to_strength(z) if s is None: return x, y = TAKER_SELL_XY _send_inject(pub, x, y, s) # ───────────────────────── per-arm runner ───────────────────────── def classify_regime(asym: float, coh: float, asym_p10: float | None, asym_p90: float | None, coh_p10: float | None, coh_p90: float | None) -> str: """Return one of HIGH / LOW / NEUTRAL. Returns NEUTRAL if thresholds not yet known or telemetry invalid.""" if any(v is None or not np.isfinite(v) for v in (asym, coh, asym_p10, asym_p90, coh_p10, coh_p90)): return "NEUTRAL" if asym > asym_p90 or coh < coh_p10: return "HIGH" if asym < asym_p10 and coh > coh_p90: return "LOW" return "NEUTRAL" def snapshot_row(tel: LatestTel, minute: int, arm: str, row: pd.Series) -> dict: snap, snap_wall = tel.snapshot() rec = { "minute": int(minute), "arm": arm, "snap_wall": snap_wall, "snap_age_ms": (time.time() - snap_wall) * 1000 if snap_wall else None, # raw values for downstream analysis "trade_count": float(row.get("trade_count", np.nan)), "taker_buy_usd": float(row.get("taker_buy_usd", np.nan)), "taker_sell_usd": float(row.get("taker_sell_usd", np.nan)), "mid_price": float(row.get("mid_price", np.nan)), # z-scores actually injected "trade_count_z": row.get("trade_count_z"), "taker_buy_usd_z": row.get("taker_buy_usd_z"), "taker_sell_usd_z": row.get("taker_sell_usd_z"), } if snap is None: for ch in CHANNELS: rec[ch] = None else: for ch in CHANNELS: rec[ch] = snap.get(ch) return rec def run_arm(tel: LatestTel, pub: zmq.Socket | None, arm: str, df: pd.DataFrame, fracto: FractoQuerier | None = None, thresholds: dict | None = None) -> pd.DataFrame: """Run one arm. If `fracto` and `thresholds` are supplied, regime labels (HIGH/LOW/NEUTRAL) are computed each minute, transitions are detected, and a Fractonaut /ask query is fired (non-blocking) on every transition. """ log(f"\n=== arm {arm}: starting ({len(df)} minutes) ===") if arm == "A": log(" arm A: NO INJECTIONS — sampling state at cadence only") elif arm == "T": log(" arm T: 3 Z-SCORE INJECTIONS per minute (trade_count center, buy left, sell right)") if fracto is not None and thresholds is not None: log(f" fractonaut transitions ENABLED thresholds={thresholds}") records: list[dict] = [] t_arm_start = time.time() last_status = t_arm_start n_minutes = len(df) step_s = PER_MINUTE_MS / 1000.0 wait_inject_s = WAIT_AFTER_INJECT_MS / 1000.0 prev_regime = "NEUTRAL" n_transitions = 0 for i in range(n_minutes): t_tick = time.time() row = df.iloc[i] if arm == "T" and pub is not None: encode_trade_count(pub, row.get("trade_count_z")) encode_taker_buy (pub, row.get("taker_buy_usd_z")) encode_taker_sell (pub, row.get("taker_sell_usd_z")) time.sleep(wait_inject_s) else: time.sleep(wait_inject_s) rec = snapshot_row(tel, int(row.minute), arm, row) # Regime classification & transition detection (arm T only — arm A is the # threshold source, we don't ask Fractonaut about its own baseline) if arm == "T" and fracto is not None and thresholds is not None: asym = rec.get("asymmetry") coh = rec.get("coherence") label = classify_regime( asym, coh, thresholds.get("asym_p10"), thresholds.get("asym_p90"), thresholds.get("coh_p10"), thresholds.get("coh_p90"), ) rec["regime"] = label if label != prev_regime and label in ("HIGH", "LOW"): n_transitions += 1 tc_z = row.get("trade_count_z", float("nan")) buy_z = row.get("taker_buy_usd_z", float("nan")) sell_z = row.get("taker_sell_usd_z", float("nan")) question = ( f"Field just entered {label} TENSION state " f"(from {prev_regime}). " f"asymmetry={asym:.1f} coherence={coh:.4f} " f"trade_count_z={tc_z:.2f} buy_z={buy_z:.2f} sell_z={sell_z:.2f}. " f"Have you seen this configuration before? " f"What pattern does this resemble and what followed last time?" ) fracto.submit(question, { "event": "regime_transition", "minute": int(row.minute), "from": prev_regime, "to": label, "asymmetry": float(asym) if asym is not None else None, "coherence": float(coh) if coh is not None else None, "trade_count_z": float(tc_z) if np.isfinite(tc_z) else None, "taker_buy_usd_z": float(buy_z) if np.isfinite(buy_z) else None, "taker_sell_usd_z": float(sell_z) if np.isfinite(sell_z) else None, "stress_xx": rec.get("stress_xx"), "stress_yy": rec.get("stress_yy"), }) elif label != prev_regime and label == "NEUTRAL" and prev_regime in ("HIGH", "LOW"): n_transitions += 1 question = ( f"Field just exited {prev_regime} TENSION back to NEUTRAL. " f"asymmetry={asym:.1f} coherence={coh:.4f}. " f"How did the configuration evolve while in {prev_regime}? " f"Did anything notable change before the return to baseline?" ) fracto.submit(question, { "event": "regime_transition", "minute": int(row.minute), "from": prev_regime, "to": label, "asymmetry": float(asym) if asym is not None else None, "coherence": float(coh) if coh is not None else None, }) prev_regime = label records.append(rec) spent = time.time() - t_tick remaining = step_s - spent if remaining > 0: time.sleep(remaining) if time.time() - last_status > 60: elapsed = time.time() - t_arm_start pct = (i + 1) / n_minutes * 100 rate_per_s = (i + 1) / elapsed if elapsed > 0 else 0 eta_s = (n_minutes - i - 1) / rate_per_s if rate_per_s > 0 else 0 cur_field = tel.latest or {} asym = cur_field.get('asymmetry', float('nan')) extras = "" if fracto is not None and arm == "T": extras = f" trans={n_transitions} " + fracto.stats() log(f" arm {arm} {i+1}/{n_minutes} ({pct:.1f}%) " f"rate={rate_per_s:.1f}min/s ETA={eta_s/60:.1f}min " f"asym={asym:.3f} " f"latest_tel_age={(time.time()-tel.latest_wall)*1000 if tel.latest_wall else -1:.0f}ms " f"tel_seen={tel.n_seen}{extras}") last_status = time.time() log(f" arm {arm} COMPLETE records={len(records)} total={(time.time()-t_arm_start)/60:.1f}min") if fracto is not None and arm == "T": log(f" arm T transitions={n_transitions} {fracto.stats()}") return pd.DataFrame(records) # ───────────────────────── main ───────────────────────── def main(): ap = argparse.ArgumentParser() ap.add_argument("--days-glob", default=DEFAULT_DAYS_GLOB, help="day-dir glob under hl_data/minutes/") ap.add_argument("--max-minutes", type=int, default=None, help="cap minutes (shakedown)") ap.add_argument("--arms", default="A,T", help="comma-separated arms to run (default A,T)") ap.add_argument("--cooldown-s", type=int, default=COOLDOWN_BETWEEN_ARMS_S, help="seconds between arms (only after arm T)") args = ap.parse_args() log(f"=== regime_classifier_injector run_id={RUN_ID} ===") log(f"out_dir={OUT_DIR}") log(f"args: days_glob={args.days_glob} max_minutes={args.max_minutes} " f"arms={args.arms}") df = load_btc(args.days_glob) if args.max_minutes: df = df.head(args.max_minutes).reset_index(drop=True) log(f"capped to first {args.max_minutes} minutes") df = compute_zscores(df) log("z-scores computed (past-only 500min rolling):") for col in ["trade_count_z", "taker_buy_usd_z", "taker_sell_usd_z"]: s = df[col] finite = int(np.isfinite(s.astype(float)).sum()) log(f" {col:<22} n_finite={finite:>5}/{len(s)} " f"mean={s.mean():+.4f} std={s.std():+.4f} " f"min={s.min():+.4f} max={s.max():+.4f}") meta = { "run_id": RUN_ID, "coin": COIN, "data_root": DATA_ROOT, "days_glob": args.days_glob, "n_minutes": int(len(df)), "minute_range": [int(df.minute.min()), int(df.minute.max())], "z_win_min": Z_WIN_MIN, "z_denom": Z_DENOM, "str_cap": STR_CAP, "spatial": { "center": list(CENTER), "taker_buy_xy": list(TAKER_BUY_XY), "taker_sell_xy": list(TAKER_SELL_XY), "sigma": SIGMA, }, "timing": { "per_minute_ms": PER_MINUTE_MS, "wait_after_inject_ms": WAIT_AFTER_INJECT_MS, "cooldown_between_arms_s": COOLDOWN_BETWEEN_ARMS_S, }, "arms": args.arms.split(","), } (OUT_DIR / "meta.json").write_text(json.dumps(meta, indent=2)) tel = LatestTel(TEL_ADDR) ctx = zmq.Context.instance() pub = ctx.socket(zmq.PUB) pub.connect(CMD_ADDR) log(f"ZMQ connected: SUB {TEL_ADDR}, PUB {CMD_ADDR}") log("warming up SUB socket (3s) ...") time.sleep(3) log(f" initial tel_seen={tel.n_seen} latest_age_ms=" f"{(time.time()-tel.latest_wall)*1000 if tel.latest_wall else -1:.0f}") # Fractonaut querier (fire-and-forget). It is OK if the HTTP service is down # — the worker will just log errors; the run keeps going. fracto = FractoQuerier() log(f"FractoQuerier ready; HTTP target = {FRACTO_URL}") arm_A_telemetry: pd.DataFrame | None = None thresholds: dict | None = None try: arms_to_run = [a.strip().upper() for a in args.arms.split(",")] for ai, arm in enumerate(arms_to_run): if arm not in ("A", "T"): log(f"skipping unknown arm: {arm}") continue if ai > 0 and arms_to_run[ai - 1] == "T": log(f"--- cooldown {args.cooldown_s}s ---") time.sleep(args.cooldown_s) if arm == "T" and arm_A_telemetry is not None: # Derive thresholds from arm A telemetry distribution arm_A_clean = arm_A_telemetry.dropna(subset=["asymmetry", "coherence"]) thresholds = { "asym_p10": float(arm_A_clean["asymmetry"].quantile(0.10)), "asym_p90": float(arm_A_clean["asymmetry"].quantile(0.90)), "coh_p10": float(arm_A_clean["coherence"].quantile(0.10)), "coh_p90": float(arm_A_clean["coherence"].quantile(0.90)), } log(f"derived thresholds from arm A: {thresholds}") # START-OF-T summary query fracto.submit( ("Arm A free-running baseline complete. " "Arm T market injection beginning now. " "Three channels: trade_count at centre (512,512), " "taker_buy at x=400, taker_sell at x=624, each as " "rolling-500min z-score capped ±1. " "Summarise what you observed during arm A. " "What is the baseline field character?"), {"event": "arm_T_start", "thresholds": thresholds, "arm_A_rows": int(len(arm_A_clean))}, ) arm_df = run_arm( tel, pub if arm == "T" else None, arm, df, fracto=fracto if arm == "T" else None, thresholds=thresholds if arm == "T" else None, ) outf = OUT_DIR / f"arm_{arm}_{'3ch' if arm=='T' else 'no_inject'}.parquet" arm_df.to_parquet(outf) log(f" saved {outf.name} ({len(arm_df)} rows)") if arm == "A": arm_A_telemetry = arm_df elif arm == "T": # END-OF-T summary query fracto.submit( ("Arm T complete. Injection has been running for the full " "April dataset. What patterns did you observe during the " "injection period that were absent or different from the " "arm A baseline? What field configurations appeared " "repeatedly at HIGH TENSION moments?"), {"event": "arm_T_end", "arm_T_rows": int(len(arm_df))}, ) log(f"\n=== ALL ARMS COMPLETE ===") log(f"output: {OUT_DIR}") log(f"draining FractoQuerier (timeout 600s) ... {fracto.stats()}") fracto.drain(max_wait_s=600) log(f"fracto drain complete {fracto.stats()}") finally: fracto.stop() tel.stop() pub.close(0) ctx.term() if __name__ == "__main__": main()