auto: hourly snapshot 2026-06-08 13:34
This commit is contained in:
@@ -0,0 +1,608 @@
|
||||
"""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_<RUN_ID>/
|
||||
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()
|
||||
Reference in New Issue
Block a user