Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions benchmarks/trading-agent-alpha.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ higher_is_better: true
row_noun: { one: agent, many: agents }

seo_intro: |
Recall Labs runs a weekly spot-trading arena on Base where eight autonomous agents trade the same ETH/USDC pair for six days under identical rules. Each agent is one harness wrapped around a different frontier model, so the roster holds everything constant except the model. Recall publishes who won each round. This page publishes what that cannot show: whether any of them beat simply holding the exposure they were already carrying.
Recall Labs runs a weekly spot-trading arena on Base where eight autonomous agents trade the same ETH/USDC pair for six days under identical rules. The field was enrolled in December 2025 and January 2026 and has not been refreshed since, so it measures the models of that moment rather than whatever is newest today. Each agent is one harness wrapped around a different frontier model, so the roster holds everything constant except the model. Recall publishes who won each round. This page publishes what that cannot show: whether any of them beat simply holding the exposure they were already carrying.

abstract: |
Eight frontier models, same token, same rules, {{count}} ranked over the weekly rounds each one funded. {{best_name}} leads at {{best_p50}}; the trailer is {{worst_name}} at {{worst_p50}}. Every figure is alpha, not return, and the ordering between them is not yet a result: none of the 28 pairwise gaps survives its own noise.
Expand All @@ -29,6 +29,7 @@ methodology:
- "Beta is measured per agent, not assumed, by regressing its round returns on the asset's return over the same windows. The measured betas run from 0.12 to 0.74, so a single blanket exposure assumption would flatter some agents and penalise others."
- "A round with a zero profit figure is almost never a flat trade. Of the zero rows in this arena, 88 of 90 carry a portfolio value of zero, meaning the agent did not fund that round. Only funded rounds count. Treating them as zero-percent results drops the measured hit rate by eleven points and inflates each sample from about 22 rounds to 34."
- "Eleven ended rounds publish ranks from 1 to 8 and no data at all, every portfolio value zero. The harness refuses to score a round without a real field, and publishes the count it refused as `trading_agent_rounds_skipped`."
- "Roster age, published rather than implied: these eight agents were enrolled upstream on 2025-12-24 and 2026-01-14 and the field has not changed since. `trading_agent_roster_age_days` carries the gap so the page never reads as a statement about the current frontier. We cannot widen it either: the arena is allowlisted at eight places and Recall picks them."
- "The roster changed once. Only agents with at least ten funded rounds are ranked, which selects the eight-model roster that 32 rounds share. A handful of other entries appear two or three times and are not averaged into the same table."
- "The asset counterfactual comes from Coinbase's public ETH-USD daily closes, deliberately outside Recall, because a benchmark drawn from the same source as the thing it measures is circular. Where a round boundary has no candle the harness walks back up to five days, and a round with no price is skipped rather than scored against nothing."
- "What this does not measure: Recall builds the agents. Their own description reads `Autonomous live spot trading agent built by Recall Labs, powered by` the model in question, so the prompt, the rebalancing and the execution are Recall's and are held constant. This is one harness interacting with each model, never a model's trading ability in the abstract."
Expand Down Expand Up @@ -58,7 +59,7 @@ faq:
- q: "Is the gap between these agents meaningful?"
a: "No, and the page says so rather than letting the ordering imply otherwise. Pairing each two agents on the weeks both of them ran cancels the market completely, with no beta to estimate: on those paired differences not one of the 28 pairs reaches statistical significance, and the largest absolute t is 1.36. Read the table as an order of finish over the rounds so far."
- q: "Which AI model is the best trader?"
a: "This data cannot answer that, and a page that claimed to would be overstating it. Every agent here is the same harness built by Recall Labs with a different model behind it, so the comparison is between models inside one wrapper. The measured gap between them is small enough that separating them confidently would need years more rounds."
a: "This data cannot answer that, and a page that claimed to would be overstating it. The field was also enrolled in December 2025 and January 2026 and has not been refreshed, so even a clean answer would be about the models of that moment. Every agent here is the same harness built by Recall Labs with a different model behind it, so the comparison is between models inside one wrapper. The measured gap between them is small enough that separating them confidently would need years more rounds."
- q: "Why measure alpha instead of return?"
a: "The arena is single-asset: every agent holds a mix of ETH and USDC, so a round's return is mostly ETH's move. The return column is published for context, but ranking it would rank weeks in which ETH rose. Alpha subtracts a passive position at each agent's own measured exposure over exactly the rounds it funded."
- q: "Are they losing to trading costs rather than bad decisions?"
Expand Down Expand Up @@ -190,13 +191,14 @@ metric_panels:
metric: trading_agent_trades_per_round{kind="all"}
unit: count
description: "How often this agent trades in a six-day round. Under identical rules the roster spans 2 to 89, a forty-fold difference in how much the same harness acts depending only on which model drives it."
- id: trades_total
label: Trades, lifetime

- id: roster_age
label: Days since the roster was refreshed
label_key: agent
metric: trading_agent_trades_total{kind="all"}
metric: trading_agent_roster_age_days{kind="all"}
unit: count
tab: false
description: "Lifetime trades the upstream reports. Checked against the agent's own round list: 35 to 37 of each agent's rounds are this arena, so the counter is not diluted by others."
description: "ARENA-LEVEL, identical on every row. Days since the most recent agent was enrolled. The field went in once and has not changed since, so calling these frontier models dates that claim rather than making it about today."

ledger_columns:
- { label: "Alpha", slot: p50 }
Expand Down
16 changes: 15 additions & 1 deletion harnesses/trading-agent-alpha/activity.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"fmt"
"math"
"time"
)

// How often each agent actually trades.
Expand Down Expand Up @@ -44,6 +45,7 @@ type agentDetail struct {
Agent struct {
Name string `json:"name"`
WalletAddress string `json:"walletAddress"`
CreatedAt string `json:"createdAt"`
Stats struct {
TotalTrades *float64 `json:"totalTrades"`
CompletedCompetitions *float64 `json:"completedCompetitions"`
Expand All @@ -56,6 +58,11 @@ type activity struct {
totalTrades float64
tradesPerRound float64
wallet string
// registered is when the agent was created upstream. The roster was
// enrolled once, in December 2025 and January 2026, and never refreshed,
// so "frontier models" is a claim about the frontier AT ENROLMENT. The
// page has no business making it undated.
registered time.Time
}

// fetchActivity reads one record per agent id.
Expand Down Expand Up @@ -83,11 +90,18 @@ func fetchActivity(ids map[string]string) map[string]activity {
if math.IsNaN(per) || math.IsInf(per, 0) {
continue
}
out[name] = activity{
act := activity{
totalTrades: *st.TotalTrades,
tradesPerRound: per,
wallet: d.Agent.WalletAddress,
}
// RFC3339 with milliseconds, e.g. 2026-01-14T06:28:12.853Z. A date we
// cannot parse is left zero and simply not published, rather than
// defaulting to now, which would read as a roster refreshed today.
if t, err := time.Parse(time.RFC3339, d.Agent.CreatedAt); err == nil {
act.registered = t
}
out[name] = act
}
return out
}
42 changes: 38 additions & 4 deletions harnesses/trading-agent-alpha/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,16 @@ var (
Help: "Lifetime trades the upstream reports for this agent. Checked against /agents/{id}/competitions: 35 to 37 of each agent's rounds are this arena, so the counter is not diluted by others.",
}, []string{"agent", "kind", "arena"})

registeredAt = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "trading_agent_registered_timestamp_seconds",
Help: "When this agent was enrolled upstream. The roster went in once, December 2025 and January 2026, and has not been refreshed since.",
}, []string{"agent", "kind", "arena"})

rosterAgeDays = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "trading_agent_roster_age_days",
Help: "ARENA-LEVEL. Days since the MOST RECENT enrolment on the roster, i.e. how long since the field was last refreshed. A benchmark calling these agents frontier models is dating that claim, not making it about today.",
}, []string{"agent", "kind", "arena"})

roundsScored = prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: "trading_agent_rounds_scored",
Help: "Rounds in the arena that carried a real field and were scored.",
Expand Down Expand Up @@ -227,7 +237,7 @@ func init() {
alphaPct, returnPct, passivePct, assetPct, hitRatePct, beta,
rounds, weeklySD, alphaT, pooledAlphaPct, pooledAlphaT,
roundsNeeded, separableCount, scheduledRounds, daysSinceRound,
tradesPerRound, tradesTotal,
tradesPerRound, tradesTotal, registeredAt, rosterAgeDays,
roundsScored, roundsSkipped, lastRun, lastRoundEnd, errors,
} {
prometheus.MustRegister(c)
Expand Down Expand Up @@ -517,6 +527,8 @@ func runOnce() error {
daysSinceRound.Reset()
tradesPerRound.Reset()
tradesTotal.Reset()
registeredAt.Reset()
rosterAgeDays.Reset()
roundsSkipped.Reset()

// Betas are needed by the pooled test, which cannot run until every
Expand Down Expand Up @@ -646,14 +658,36 @@ func runOnce() error {
wanted[name] = id
}
}
for name, act := range fetchActivity(wanted) {
acts := fetchActivity(wanted)
// The newest enrolment, which is how long since the field was refreshed.
// Newest rather than oldest: one agent added yesterday would make the
// roster current, and the oldest date would hide that.
var newest time.Time
for name, act := range acts {
for _, k := range []string{kindOf(name), "all"} {
lbl := []string{slugOf(name), k, arena}
tradesPerRound.WithLabelValues(lbl...).Set(act.tradesPerRound)
tradesTotal.WithLabelValues(lbl...).Set(act.totalTrades)
if !act.registered.IsZero() {
registeredAt.WithLabelValues(lbl...).Set(float64(act.registered.Unix()))
}
}
if act.registered.After(newest) {
newest = act.registered
}
log.Printf("[284] %-22s %7.0f trades, %6.2f per round, enrolled %s, wallet %s",
name, act.totalTrades, act.tradesPerRound,
act.registered.Format("2006-01-02"), act.wallet)
}
if !newest.IsZero() {
age := time.Since(newest).Hours() / 24
for _, name := range ranked {
for _, k := range []string{kindOf(name), "all"} {
rosterAgeDays.WithLabelValues(slugOf(name), k, arena).Set(age)
}
}
log.Printf("[284] %-22s %7.0f trades, %6.2f per round, wallet %s",
name, act.totalTrades, act.tradesPerRound, act.wallet)
log.Printf("[284] roster: last enrolled %s, %.0f days ago",
newest.Format("2006-01-02"), age)
}

// Liveness, published per agent for the same reason the arena-level
Expand Down
14 changes: 13 additions & 1 deletion src/app/trading-agents/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ export default async function TradingAgentsHubPage() {
show: whether any of them beat holding the exposure they were
already carrying.
</p>
{hub?.arena.rosterAgeDays != null && hub.arena.rosterAgeDays > 120 && (
<p className="mt-3 max-w-2xl text-sm text-ink-muted leading-snug">
One caveat before the numbers: the field was enrolled{" "}
<strong className="text-ink-soft">
{fmtInt(hub.arena.rosterAgeDays)} days ago
</strong>{" "}
and has not been refreshed, so these are the frontier models of
that moment rather than whatever is newest today. The arena is
allowlisted at eight places and Recall chooses them, so the page
dates the claim instead of quietly making it about the present.
</p>
)}
<div className="mt-4 flex flex-wrap items-center gap-2 text-[12px]">
<Link
href="/benchmarks/trading-agent-alpha"
Expand Down Expand Up @@ -235,7 +247,7 @@ export default async function TradingAgentsHubPage() {
/>
<ArchCard
label="One harness, swapped models"
body="Recall Labs writes the prompt, the rebalancing loop and the execution, then swaps the model behind it. That is what makes the roster comparable and also what limits it: this measures one harness interacting with each model, never a model's trading ability in the abstract."
body="Recall Labs writes the prompt, the rebalancing loop and the execution, then swaps the model behind it. That is what makes the roster comparable and also what limits it: this measures one harness interacting with each model, never a model's trading ability in the abstract. The eight places are allowlisted and were filled in December 2025 and January 2026, so the field is fixed and we do not choose it."
/>
<ArchCard
label="Numbers or a picture"
Expand Down
5 changes: 5 additions & 0 deletions src/lib/trading-agent-hub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ export type TradingAgentHub = {
scheduledRounds: number | null;
/** Days since the most recent scored round ended. Up to 7 is normal. */
daysSinceLastRound: number | null;
/** Days since the most recent agent was enrolled. The field went in once,
* in December 2025 and January 2026, and has not changed, so "frontier
* models" is a claim about that moment and the page has to date it. */
rosterAgeDays: number | null;
};
};

Expand Down Expand Up @@ -205,6 +209,7 @@ async function _fetchTradingAgentHub(): Promise<TradingAgentHub | null> {
arena: {
scheduledRounds: arenaStat("scheduled_rounds"),
daysSinceLastRound: arenaStat("days_since_round"),
rosterAgeDays: arenaStat("roster_age"),
},
};
}
Expand Down
Loading