Lab notebook · chapter 4 of 7 · page 1 of 4

LLM agents

Scout the picker, what the API costs, and the veto that lost 112 points.

Scout (milestone 5)

The first agent. Sees price-derived features for the whole universe and its own book, returns names to hold, and emits Proposals judged by the same evaluate() as everything else. It can ask for anything; the gate decides.

Needs an API key (ANTHROPIC_API_KEY, or ant auth login) and pip install anthropic. Not yet run against the live API — everything below the network call is covered by 40 tests using a scripted client.

from labtrader.llm import AnthropicClient, CachingClient
from labtrader.scout import Scout
scout = Scout(CachingClient(AnthropicClient()), k=20, target_weight=0.045)

Billing and the spend guardrail

API usage bills to Anthropic Console — pay-as-you-go, separate from any Claude subscription (Max covers claude.ai, Desktop and Claude Code, not programmatic API calls). Set a spend limit in the Console.

In code, cap the calls. Composition order matters:

client = CachingClient(CallBudget(AnthropicClient(), max_calls=500))

The budget wraps the network client and the cache wraps the budget, so cache hits are free and uncounted and only paid calls are charged against it. Reversing them caps re-runs that cost nothing. A budget stop reaches Scout as a held book and a failures increment, not a crash.

The cache invalidation trap: the key covers system + user + schema, so one word changed in SYSTEM misses all ~462 entries. Prompt iteration costs ~$21 per iteration. Test prompt changes on a one-year window (~50 calls, ~$2) before running the full history.

Caching is not an optimisation. A nine-year weekly backtest is ~462 calls; without a cache each re-run costs that again and returns different answers, so the harness loses reproducibility — the one property it exists for. CachingClient keys on system + user + schema and writes atomically.

Estimated first full backtest: ~$21 at Opus 5 rates (~1,559 input tokens per call). Cached re-runs are free.

What it's built to test

The NBER result: LLM agents converge on concentrated, high-valuation, heavily-covered names and don't beat passive benchmarks. So Scout's prompt carries returns and volatility only — no market cap, no sector, no company names. A test asserts none of those leak into the rendered table. If Scout still drifts to mega-caps, that came from the model's priors, not the prompt.

mean_pairwise_overlap() in metrics.py is the measurement: run Scout several times and see whether independent instances converge on the same names. That is the herding experiment the earlier --seeds flag could not do, because it only perturbed a lookback.

Failure handling

A model outage or an unusable reply holds the book rather than flattening it, and increments scout.failures — a run with many failures is not a result. Hallucinated symbols are dropped before the gate so the rejection histogram stays about judgment rather than spelling. Out-of-range confidences are passed through deliberately: clamping would hide miscalibration, and the gate records it as BAD_CONFIDENCE.