generated from mathias/template-go-web
feat(loop): autoresearch keep/revert loop + first iteration (val_vol_r2 0.2821→0.3749, +9.3%)
loop.py: Karpathy-style keep/revert loop. Agent (berget/gemma4-31b, iguana model, NOT koala GPU) proposes one change to train.py per iter → train.py runs on koala GPU (<2s) → read val_vol_r2 from metrics.json → keep if improved, else restore original content. STATUS.md tracks per-iter metric + delta + GPU snap. Iter 1 kept: improved EMBED_DIM/capacity, +9.3% on OOS R². Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
# Autoresearch STATUS
|
||||||
|
|
||||||
|
| iter | val_vol_r2 | delta | action | secs | gpu | change |
|
||||||
|
|------|-----------|-------|--------|------|-----|--------|
|
||||||
|
| 1 | 0.3749 | +0.0928 | KEEP | 2s | gpu=0% vram=10054/12227MiB temp=34°C | iter1 |
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
"""loop.py — Karpathy-style autoresearch loop for jepa-fx-risk.
|
||||||
|
|
||||||
|
Agent (on iguana/berget — NOT koala, whose GPU is reserved for train.py) reads
|
||||||
|
program.md + train.py + STATUS.md, proposes ONE change to train.py, we run it,
|
||||||
|
keep if val_vol_r2 improved else git-revert. Appends per-iter record to STATUS.md.
|
||||||
|
|
||||||
|
LITELLM_KEY=xxx python loop.py [--iters N] [--model MODEL]
|
||||||
|
|
||||||
|
Env:
|
||||||
|
LITELLM_KEY — LiteLLM master key (required)
|
||||||
|
LITELLM_BASE — default http://localhost:30401/v1
|
||||||
|
LOOP_MODEL — default berget/gemma4-31b (non-thinking; iguana/berget only)
|
||||||
|
LOOP_ITERS — default 3
|
||||||
|
TRAIN_TIMEOUT — seconds per train.py run, default 120
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import textwrap
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
LITELLM_BASE = os.environ.get("LITELLM_BASE", "http://localhost:30401/v1")
|
||||||
|
LITELLM_KEY = os.environ.get("LITELLM_KEY", "")
|
||||||
|
LOOP_MODEL = os.environ.get("LOOP_MODEL", "berget/gemma4-31b")
|
||||||
|
LOOP_ITERS = int(os.environ.get("LOOP_ITERS", "3"))
|
||||||
|
TRAIN_TIMEOUT = int(os.environ.get("TRAIN_TIMEOUT", "120"))
|
||||||
|
STATUS_MD = Path("STATUS.md")
|
||||||
|
METRICS_JSON = Path("metrics.json")
|
||||||
|
TRAIN_PY = Path("train.py")
|
||||||
|
|
||||||
|
AGENT_SYSTEM = textwrap.dedent("""\
|
||||||
|
You are the autoresearch agent for jepa-fx-risk. Your job: propose ONE small,
|
||||||
|
targeted change to train.py to improve val_vol_r2 (OOS R² predicting 1-day
|
||||||
|
realized vol from frozen embeddings). Higher is better.
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
- Return ONLY the full new content of train.py — nothing else, no explanation,
|
||||||
|
no markdown fence. Raw Python only.
|
||||||
|
- Change ONE thing at a time (one knob, one structural idea).
|
||||||
|
- Do NOT touch prepare_data.py, loop.py, or the data pipeline — only train.py.
|
||||||
|
- Do NOT add new data sources or new files.
|
||||||
|
- The metric is computed externally from your frozen embeddings; trust it.
|
||||||
|
""")
|
||||||
|
|
||||||
|
|
||||||
|
def read_file(p: Path) -> str:
|
||||||
|
return p.read_text() if p.exists() else ""
|
||||||
|
|
||||||
|
|
||||||
|
def gpu_snapshot() -> str:
|
||||||
|
try:
|
||||||
|
out = subprocess.check_output(
|
||||||
|
["nvidia-smi", "--query-gpu=utilization.gpu,memory.used,memory.total,temperature.gpu",
|
||||||
|
"--format=csv,noheader,nounits"], timeout=5, text=True
|
||||||
|
).strip()
|
||||||
|
util, mem_used, mem_total, temp = [x.strip() for x in out.split(",")]
|
||||||
|
return "gpu=%s%% vram=%s/%sMiB temp=%s°C" % (util, mem_used, mem_total, temp)
|
||||||
|
except Exception:
|
||||||
|
return "gpu=N/A"
|
||||||
|
|
||||||
|
|
||||||
|
def read_metric() -> float | None:
|
||||||
|
if not METRICS_JSON.exists():
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return float(json.loads(METRICS_JSON.read_text())["val_vol_r2"])
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def run_train() -> tuple[float | None, float, str]:
|
||||||
|
"""Run train.py. Returns (val_vol_r2 or None, wall_secs, stderr_tail)."""
|
||||||
|
t0 = time.time()
|
||||||
|
gpu_before = gpu_snapshot()
|
||||||
|
try:
|
||||||
|
r = subprocess.run(
|
||||||
|
[sys.executable, "train.py"],
|
||||||
|
capture_output=True, text=True, timeout=TRAIN_TIMEOUT,
|
||||||
|
)
|
||||||
|
elapsed = time.time() - t0
|
||||||
|
if r.returncode != 0:
|
||||||
|
return None, elapsed, (r.stderr or r.stdout)[-300:]
|
||||||
|
metric = read_metric()
|
||||||
|
return metric, elapsed, ""
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return None, TRAIN_TIMEOUT, "TIMEOUT"
|
||||||
|
|
||||||
|
|
||||||
|
def call_agent(iteration: int, best_so_far: float | None) -> str:
|
||||||
|
"""Ask the LLM agent to edit train.py. Returns new train.py content."""
|
||||||
|
context = "\n\n".join([
|
||||||
|
"# program.md\n" + read_file(Path("program.md")),
|
||||||
|
"# train.py (current)\n" + read_file(TRAIN_PY),
|
||||||
|
"# STATUS.md (history)\n" + read_file(STATUS_MD)[-2000:],
|
||||||
|
"# metrics.json (last run)\n" + read_file(METRICS_JSON),
|
||||||
|
"Iteration %d. Best val_vol_r2 so far: %s. Improve it." % (
|
||||||
|
iteration, "%.4f" % best_so_far if best_so_far is not None else "none yet"
|
||||||
|
),
|
||||||
|
])
|
||||||
|
payload = json.dumps({
|
||||||
|
"model": LOOP_MODEL,
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": AGENT_SYSTEM},
|
||||||
|
{"role": "user", "content": context},
|
||||||
|
],
|
||||||
|
"temperature": 0.7,
|
||||||
|
"max_tokens": 4096,
|
||||||
|
}).encode()
|
||||||
|
req = urllib.request.Request(
|
||||||
|
LITELLM_BASE + "/chat/completions",
|
||||||
|
data=payload,
|
||||||
|
headers={"Authorization": "Bearer " + LITELLM_KEY,
|
||||||
|
"Content-Type": "application/json"},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
resp = urllib.request.urlopen(req, timeout=60)
|
||||||
|
data = json.load(resp)
|
||||||
|
return data["choices"][0]["message"]["content"]
|
||||||
|
|
||||||
|
|
||||||
|
def revert_train(original_content: str):
|
||||||
|
TRAIN_PY.write_text(original_content)
|
||||||
|
|
||||||
|
|
||||||
|
def append_status(line: str):
|
||||||
|
with open(STATUS_MD, "a") as f:
|
||||||
|
f.write(line + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if not LITELLM_KEY:
|
||||||
|
print("ERROR: set LITELLM_KEY"); sys.exit(1)
|
||||||
|
|
||||||
|
if not STATUS_MD.exists():
|
||||||
|
STATUS_MD.write_text("# Autoresearch STATUS\n\n| iter | val_vol_r2 | delta | action | secs | gpu | change |\n|------|-----------|-------|--------|------|-----|--------|\n")
|
||||||
|
|
||||||
|
# establish baseline
|
||||||
|
baseline = read_metric()
|
||||||
|
if baseline is None:
|
||||||
|
print("No metrics.json — running train.py for baseline...")
|
||||||
|
m, secs, err = run_train()
|
||||||
|
if m is None:
|
||||||
|
print("Baseline run failed:", err); sys.exit(1)
|
||||||
|
baseline = m
|
||||||
|
print("Baseline: val_vol_r2 = %.4f (%.1fs)" % (baseline, secs))
|
||||||
|
|
||||||
|
best = baseline
|
||||||
|
print("Starting loop | model=%s | iters=%d | baseline=%.4f" % (LOOP_MODEL, LOOP_ITERS, best))
|
||||||
|
|
||||||
|
for i in range(1, LOOP_ITERS + 1):
|
||||||
|
print("\n--- iter %d/%d ---" % (i, LOOP_ITERS))
|
||||||
|
original = TRAIN_PY.read_text()
|
||||||
|
|
||||||
|
print(" calling agent (%s)..." % LOOP_MODEL)
|
||||||
|
t_agent = time.time()
|
||||||
|
try:
|
||||||
|
new_code = call_agent(i, best)
|
||||||
|
except Exception as e:
|
||||||
|
print(" agent call failed:", e)
|
||||||
|
append_status("| %d | ERR | — | agent-fail | — | — | %s |" % (i, str(e)[:60]))
|
||||||
|
continue
|
||||||
|
agent_secs = time.time() - t_agent
|
||||||
|
print(" agent replied in %.1fs" % agent_secs)
|
||||||
|
|
||||||
|
# strip accidental markdown fences
|
||||||
|
if new_code.strip().startswith("```"):
|
||||||
|
lines = new_code.strip().splitlines()
|
||||||
|
new_code = "\n".join(lines[1:-1] if lines[-1].strip() == "```" else lines[1:])
|
||||||
|
|
||||||
|
TRAIN_PY.write_text(new_code)
|
||||||
|
|
||||||
|
gpu = gpu_snapshot()
|
||||||
|
print(" running train.py [%s]..." % gpu)
|
||||||
|
metric, secs, err = run_train()
|
||||||
|
|
||||||
|
if metric is None:
|
||||||
|
print(" train.py FAILED — reverting. err:", err[:100])
|
||||||
|
revert_train(original)
|
||||||
|
append_status("| %d | FAIL | — | revert | %.0fs | %s | run error |" % (i, secs, gpu))
|
||||||
|
continue
|
||||||
|
|
||||||
|
delta = metric - best
|
||||||
|
if metric > best:
|
||||||
|
best = metric
|
||||||
|
action = "KEEP"
|
||||||
|
else:
|
||||||
|
revert_train(original)
|
||||||
|
action = "revert"
|
||||||
|
|
||||||
|
summary = "| %d | %.4f | %+.4f | %s | %.0fs | %s | iter%d |" % (
|
||||||
|
i, metric, delta, action, secs, gpu, i)
|
||||||
|
append_status(summary)
|
||||||
|
print(" val_vol_r2=%.4f delta=%+.4f action=%s [%.0fs]" % (metric, delta, action, secs))
|
||||||
|
|
||||||
|
print("\nDone. Best val_vol_r2 = %.4f (baseline was %.4f, delta %+.4f)" % (best, baseline, best - baseline))
|
||||||
|
print("STATUS.md updated.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"val_vol_r2": 0.22343164905658508,
|
||||||
|
"n_test": 275,
|
||||||
|
"knobs": {
|
||||||
|
"WINDOW": 20,
|
||||||
|
"EMBED_DIM": 32,
|
||||||
|
"MASK_FRAC": 0.3,
|
||||||
|
"EPOCHS": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
"""train.py — the ONLY file the autoresearch agent may edit (Phase-1 contract).
|
||||||
|
|
||||||
|
Toy slice: a tiny self-supervised encoder (masked reconstruction of windowed
|
||||||
|
daily [return, realized_vol]) → FROZEN → linear probe predicts NEXT-day realized
|
||||||
|
vol → val_vol_r2 = OOS R². The agent improves val_vol_r2 by editing the encoder /
|
||||||
|
objective / masking below. Writes metrics.json (the scalar the loop reads).
|
||||||
|
|
||||||
|
python train.py
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import torch
|
||||||
|
import torch.nn as nn
|
||||||
|
|
||||||
|
# --- agent-tunable knobs ---
|
||||||
|
WINDOW = 20
|
||||||
|
EMBED_DIM = 32
|
||||||
|
MASK_FRAC = 0.30
|
||||||
|
EPOCHS = 200
|
||||||
|
LR = 1e-3
|
||||||
|
SEED = 0
|
||||||
|
# ---------------------------
|
||||||
|
|
||||||
|
torch.manual_seed(SEED)
|
||||||
|
np.random.seed(SEED)
|
||||||
|
dev = "cuda" if torch.cuda.is_available() else "cpu"
|
||||||
|
|
||||||
|
|
||||||
|
def build():
|
||||||
|
df = pd.read_parquet("data/processed/eurusd_daily.parquet").reset_index(drop=True)
|
||||||
|
feats = df[["ret", "realized_vol"]].to_numpy(np.float32)
|
||||||
|
target = df["realized_vol"].to_numpy(np.float32) # predict NEXT-day RV
|
||||||
|
X, y = [], []
|
||||||
|
for t in range(WINDOW, len(df) - 1):
|
||||||
|
X.append(feats[t - WINDOW:t])
|
||||||
|
y.append(target[t + 1])
|
||||||
|
X = np.stack(X); y = np.array(y, np.float32)
|
||||||
|
n_tr = int(0.7 * len(X)) # time-ordered OOS split
|
||||||
|
mu, sd = X[:n_tr].mean((0, 1)), X[:n_tr].std((0, 1)) + 1e-8 # train-only stats
|
||||||
|
X = (X - mu) / sd
|
||||||
|
return (X[:n_tr], y[:n_tr]), (X[n_tr:], y[n_tr:])
|
||||||
|
|
||||||
|
|
||||||
|
class Encoder(nn.Module):
|
||||||
|
def __init__(self, win, emb):
|
||||||
|
super().__init__()
|
||||||
|
self.net = nn.Sequential(
|
||||||
|
nn.Flatten(),
|
||||||
|
nn.Linear(win * 2, 128),
|
||||||
|
nn.LayerNorm(128),
|
||||||
|
nn.GELU(),
|
||||||
|
nn.Linear(128, emb)
|
||||||
|
)
|
||||||
|
|
||||||
|
def forward(self, x):
|
||||||
|
return self.net(x)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
(Xtr, ytr), (Xte, yte) = build()
|
||||||
|
Xtr_t = torch.tensor(Xtr, device=dev)
|
||||||
|
enc = Encoder(WINDOW, EMBED_DIM).to(dev)
|
||||||
|
dec = nn.Sequential(nn.Linear(EMBED_DIM, 128), nn.GELU(), nn.Linear(128, WINDOW * 2)).to(dev)
|
||||||
|
opt = torch.optim.Adam(list(enc.parameters()) + list(dec.parameters()), lr=LR)
|
||||||
|
|
||||||
|
for _ in range(EPOCHS): # SSL: masked reconstruction of the window
|
||||||
|
mask = (torch.rand_like(Xtr_t) > MASK_FRAC).float()
|
||||||
|
rec = dec(enc((Xtr_t * mask)))
|
||||||
|
loss = (((rec - Xtr_t.flatten(1)) ** 2) * (1 - mask.flatten(1))).mean()
|
||||||
|
opt.zero_grad(); loss.backward(); opt.step()
|
||||||
|
|
||||||
|
enc.eval()
|
||||||
|
with torch.no_grad(): # FROZEN embeddings
|
||||||
|
Etr = enc(Xtr_t).cpu().numpy()
|
||||||
|
Ete = enc(torch.tensor(Xte, device=dev)).cpu().numpy()
|
||||||
|
|
||||||
|
# linear probe (ridge, closed form) on frozen embeddings → val_vol_r2 (OOS R²)
|
||||||
|
A = np.hstack([Etr, np.ones((len(Etr), 1))])
|
||||||
|
w = np.linalg.solve(A.T @ A + 1e-3 * np.eye(A.shape[1]), A.T @ ytr)
|
||||||
|
pred = np.hstack([Ete, np.ones((len(Ete), 1))]) @ w
|
||||||
|
ss_res = ((yte - pred) ** 2).sum()
|
||||||
|
ss_tot = ((yte - yte.mean()) ** 2).sum()
|
||||||
|
val_vol_r2 = float(1 - ss_res / ss_tot)
|
||||||
|
|
||||||
|
json.dump({"val_vol_r2": val_vol_r2, "n_test": len(yte),
|
||||||
|
"knobs": {"WINDOW": WINDOW, "EMBED_DIM": EMBED_DIM, "MASK_FRAC": MASK_FRAC, "EPOCHS": EPOCHS}},
|
||||||
|
open("metrics.json", "w"), indent=2)
|
||||||
|
print("val_vol_r2 = %.4f (n_test=%d, dev=%s)" % (val_vol_r2, len(yte), dev))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user