1"""Frozen policy library: a nearest-neighbour replay of *recorded* LLM decisions.23Every online LLM decision is logged with a feature vector of its observation; the library maps a new4observation to the recorded decision of the nearest neighbour in feature space (standardized Euclidean5distance) and rescales the quantity to the current account. This is NOT an adaptive agent: it is a6frozen, non-reasoning approximation used only (i) to scale populations to 1,000+ traders at zero API7cost and (ii) as the 'cached' arm of the cached-vs-online comparison. Runs using it are labelled8provider = 'library'."""9from __future__ import annotations1011import json12from pathlib import Path1314import numpy as np15import pandas as pd1617from ..agents.base import Observation1819FEATURES = ["mispricing", "momentum", "signal", "inv_ratio", "cash_ratio", "spread_bp", "has_open"]202122def features(obs: Observation, asset: int = 0) -> np.ndarray:23 b = obs.books[asset]24 tc = obs.tick_cents[asset]25 mid = max(b.mid, 1.0)26 h = obs.mid_history[asset]27 mom = (h[-1] / h[0] - 1.0) if len(h) > 1 and h[0] > 0 else 0.028 pv = obs.public_value[asset] / tc29 sig = obs.private_signals.get(asset, 0.0)30 pos_val = obs.positions.get(asset, 0) * mid * tc31 eq = max(obs.equity, 1.0)32 spread = ((b.best_ask - b.best_bid) / mid * 1e4) if (b.best_ask is not None and b.best_bid is not None) else 50.033 return np.array([(pv - mid) / mid, mom, sig / 10.0, pos_val / eq, obs.free_cash / eq, spread / 10.0, float(bool(obs.open_orders))])343536class DecisionLibrary:37 def __init__(self, X: np.ndarray, decisions: list[dict]) -> None:38 self.X = X39 self.mu = X.mean(axis=0)40 self.sd = X.std(axis=0) + 1e-941 self.Z = (X - self.mu) / self.sd42 self.decisions = decisions4344 @classmethod45 def from_runs(cls, run_dirs: list[str | Path]) -> "DecisionLibrary":46 X, D = [], []47 for rd in run_dirs:48 p = Path(rd) / "llm_decisions.parquet"49 if not p.exists():50 continue51 df = pd.read_parquet(p)52 df = df[df["valid"] & df["features"].notna()] if "features" in df else df.iloc[0:0]53 for _, r in df.iterrows():54 X.append(json.loads(r["features"]))55 D.append({"action": r["action"], "side": r["side"], "order_type": r["order_type"], "quantity": int(r["quantity"]),56 "rel_price": float(r["rel_price"]) if "rel_price" in r and pd.notna(r["rel_price"]) else 0.0,57 "rationale": "library replay"})58 if not X:59 raise ValueError("no recorded decisions with features in the given runs")60 return cls(np.array(X, dtype=float), D)6162 def save(self, path: str | Path) -> None:63 np.savez(path, X=self.X, decisions=json.dumps(self.decisions))6465 @classmethod66 def load(cls, path: str | Path) -> "DecisionLibrary":67 z = np.load(path, allow_pickle=False)68 return cls(z["X"], json.loads(str(z["decisions"])))6970 def lookup(self, f: np.ndarray, rng: np.random.Generator, k: int = 5) -> dict:71 z = (f - self.mu) / self.sd72 d = ((self.Z - z) ** 2).sum(axis=1)73 idx = np.argpartition(d, min(k, len(d) - 1))[:k]74 return dict(self.decisions[int(rng.choice(idx))])7576 def __len__(self) -> int:77 return len(self.decisions)787980class LibraryProvider:81 """Provider-shaped wrapper so the LLMPolicy pipeline (validation, logging) is reused unchanged."""82 name = "library"8384 def __init__(self, library_path: str, seed: int = 0, **_: object) -> None:85 self.lib = DecisionLibrary.load(library_path)86 self.rng = np.random.Generator(np.random.PCG64(seed))8788 def complete_obs(self, obs: Observation, mid_units: float) -> str:89 dec = self.lib.lookup(features(obs), self.rng)90 if dec["action"] == "place" and dec["order_type"] == "limit":91 dec["limit_price"] = round(mid_units * (1.0 + dec.get("rel_price", 0.0)), 2)92 else:93 dec["limit_price"] = None94 dec.setdefault("cancel_order_ids", [])95 dec.setdefault("confidence", None)96 return json.dumps(dec)97