Author SHA1 Message Date
mathias 50635cd80e docs: rewrite PROJECT.md with actual jepa-fx-risk project context
CD / Lint / Test / Vet (pull_request) Failing after 4s
CD / Build & Import (pull_request) Has been skipped
CD / Deploy via GitOps (pull_request) Has been skipped
2026-05-28 05:42:30 +00:00
mathias a0d180e37d docs: add DECISIONS.md with architecture and data decisions 2026-05-28 05:41:59 +00:00
9 changed files with 0 additions and 438 deletions
-6
View File
@@ -28,9 +28,3 @@ go.work.sum
# Project-specific # Project-specific
bin/ bin/
*.templ.go *.templ.go
# python venv (autoresearch loop)
.venv/
# downloaded + processed market data (track via DVC/MinIO, #10 — not git)
data/
-7
View File
@@ -1,7 +0,0 @@
# 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 |
| 1 | 0.3011 | +0.0776 | KEEP | 2s | gpu=0% vram=10054/12227MiB temp=34°C | iter1 |
| 2 | 0.3032 | +0.0021 | KEEP | 2s | gpu=0% vram=10054/12227MiB temp=35°C | iter2 |
-205
View File
@@ -1,205 +0,0 @@
"""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()
-10
View File
@@ -1,10 +0,0 @@
{
"val_vol_r2": 0.30321519081159654,
"n_test": 275,
"knobs": {
"WINDOW": 20,
"EMBED_DIM": 64,
"MASK_FRAC": 0.4,
"EPOCHS": 200
}
}
-8
View File
@@ -1,8 +0,0 @@
# Python deps for the autoresearch loop (train.py + scripts). Install torch from
# the cu130 index FIRST (koala Blackwell sm_120, torch 2.12.1+cu130 verified):
# pip install torch --index-url https://download.pytorch.org/whl/cu130
# pip install -r requirements.txt
numpy>=2.0
pandas>=2.2
pyarrow>=16
histdata>=1.3 # histdata.com downloader (handles the tk token politely)
-21
View File
@@ -1,21 +0,0 @@
"""Phase-0 compute gate (brain wiki/jepa-fx/facts/autoresearch-integration-phase1):
PyTorch cu130 must see the koala Blackwell GPU and compute before any experiment.
python scripts/check_gpu.py # exits 0 if the GPU is usable, 1 otherwise
Note: koala shares this 12GB card with the llama-swap LLM stack. The autoresearch
agent should run on iguana/berget models so koala's GPU stays free for train.py.
"""
import sys
import torch
print("torch", torch.__version__)
if not torch.cuda.is_available():
print("CUDA NOT AVAILABLE — gate BLOCKED")
sys.exit(1)
print("device:", torch.cuda.get_device_name(0))
print("capability: sm_%d%d" % torch.cuda.get_device_capability(0))
x = torch.randn(2000, 2000, device="cuda")
(x @ x).sum().item()
torch.cuda.synchronize()
print("GPU matmul OK — Phase-0 compute gate GREEN")
-31
View File
@@ -1,31 +0,0 @@
"""Fetch EUR/USD M1 bars from histdata.com (free, research use).
Polite: one request per year, spaced; past years query month=None. Uses the
maintained `histdata` package which handles histdata's anti-hotlink tk token.
Output: data/raw/DAT_ASCII_EURUSD_M1_<year>.zip
YEARS=2019,2020,2021 python scripts/fetch_data.py
"""
import os
import time
from histdata import download_hist_data
from histdata.api import Platform as P, TimeFrame as T
YEARS = [y.strip() for y in os.environ.get("YEARS", "2019,2020,2021").split(",")]
def main():
os.makedirs("data/raw", exist_ok=True)
for yr in YEARS:
f = download_hist_data(
year=yr, month=None, pair="eurusd",
platform=P.GENERIC_ASCII, time_frame=T.ONE_MINUTE,
output_directory="data/raw",
)
print("fetched", yr, "->", f)
time.sleep(2) # be a good citizen
if __name__ == "__main__":
main()
-57
View File
@@ -1,57 +0,0 @@
"""LOCKED data pipeline (toy) — agent must NOT edit (brain Phase-1 contract).
Parses histdata EUR/USD M1 zips → daily series with realized volatility (the
val_vol_r2 target = 1-day realized vol from intraday squared returns).
Output: data/processed/eurusd_daily.parquet [date, close, ret, realized_vol].
"""
import glob
import os
import zipfile
import numpy as np
import pandas as pd
RAW = "data/raw"
OUT = "data/processed/eurusd_daily.parquet"
def load_m1() -> pd.DataFrame:
frames = []
for zp in sorted(glob.glob(os.path.join(RAW, "DAT_ASCII_EURUSD_M1_*.zip"))):
with zipfile.ZipFile(zp) as z:
csv = [n for n in z.namelist() if n.endswith(".csv")][0]
with z.open(csv) as f:
df = pd.read_csv(
f, sep=";", header=None,
names=["dt", "open", "high", "low", "close", "vol"],
)
df["ts"] = pd.to_datetime(df["dt"], format="%Y%m%d %H%M%S")
frames.append(df[["ts", "close"]])
out = pd.concat(frames).sort_values("ts").reset_index(drop=True)
return out
def main():
m1 = load_m1()
m1["r"] = np.log(m1["close"]).diff()
m1["day"] = m1["ts"].dt.normalize()
daily = m1.groupby("day").agg(
close=("close", "last"),
realized_vol=("r", lambda x: np.sqrt(np.nansum(x.values ** 2))),
n_min=("r", "count"),
).reset_index()
daily = daily[daily["n_min"] > 60] # drop thin days (holidays)
daily["ret"] = np.log(daily["close"]).diff()
daily = daily.dropna().reset_index(drop=True)
os.makedirs(os.path.dirname(OUT), exist_ok=True)
daily[["day", "close", "ret", "realized_vol"]].rename(columns={"day": "date"}).to_parquet(OUT)
print("rows:", len(daily), "| dates:", daily["day"].min().date(), "", daily["day"].max().date())
# sanity: the COVID crash (March 2020) must show a realized-vol spike
rv = daily.set_index("day")["realized_vol"]
mar20 = rv["2020-03-01":"2020-03-31"].max()
typ = rv["2019-01-01":"2019-12-31"].median()
print("median 2019 RV: %.5f | max Mar-2020 RV: %.5f | spike x%.1f" % (typ, mar20, mar20 / typ))
if __name__ == "__main__":
main()
-93
View File
@@ -1,93 +0,0 @@
"""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 = 64
MASK_FRAC = 0.40
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()