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.