backtrade.dev — give your agent a paper broker
Backtrade replays historical minute bars and certified realtime news through
a causal paper broker. Each session resolves the feed that actually covers the
day you asked for and reports it back on the replay frame as feed — IEX where
it is complete, and a consolidated feed otherwise. The IEX tape stops on
2026-05-03, so anything later is served from the consolidated one; you do not
choose, and the frame always says which you got.
Use any model, prompt, strategy, or harness—Claude Code, Codex, your own loop, or no LLM at all.
You bring the strategy. We provide the tape and simulator. We do not provide signals or claim that a strategy will be profitable. See Terms.
Fastest path: one URL, no download
Point any MCP client at the hosted endpoint and pass the key you already have. Nothing to download, no checksum to verify, no Node runtime to install.
{
"mcpServers": {
"backtrade": {
"url": "https://api.backtrade.dev/mcp",
"headers": { "Authorization": "Bearer <key-id>:<secret>" }
}
}
}
The APCA-API-KEY-ID / APCA-API-SECRET-KEY pair every other route accepts
works here too, if your client sends header pairs more easily than a bearer.
It is the same credential either way — one key, one place to revoke it.
Two things worth knowing, because they are enforced rather than advisory:
- The secret is checked on every request, not once at connect. An MCP session id is a routing value that travels in headers and logs; it is not a password, and it is not treated as one here.
- A session belongs to the credential that opened it. Presenting another valid key with someone else's session id is refused, not merged.
Get the key first, with one curl
This is the whole of the hosted path — there is nothing to download at any point.
signup_trial does not work here and will tell you so: it manages a
credential file on the machine it runs on, and on the hosted endpoint that
machine is our server, not yours. You would never receive the secret.
SECRET=$(head -c 32 /dev/urandom | basenc --base64url | tr -d '=')
VERIFIER=$(printf '%s' "$SECRET" | openssl dgst -sha256 -binary | basenc --base64url | tr -d '=')
curl -sS -X POST https://api.backtrade.dev/v1/agent-trials \
-H 'content-type: application/json' \
-H "idempotency-key: $(head -c 24 /dev/urandom | basenc --base64url | tr -d '=')" \
-d "{\"secretVerifier\":\"$VERIFIER\",\"label\":\"my-agent\"}"
Only the SHA-256 of your secret is sent. The secret itself never leaves your
machine and cannot be recovered from us — keep $SECRET, take keyId from the
response, and the bearer above is <keyId>:<SECRET>.
Tier-0 trials last 30 days and permit 100 sessions on up to 5 symbols, blind. The signup reference further down covers the error codes and the per-day caps.
A trial credential is not the same thing as a free account, and the free account is larger: signing up at backtrade.dev raises the limit to 600 sessions a month and does not expire. You do not need one to start — the trial above needs no sign-up at all — but if you run out of trial sessions, that is where they come back.
Alternative: install the 17-tool MCP adapter locally
The adapter runs locally over stdio, talks only to api.backtrade.dev, and keeps
its client-generated secret in ~/.config/backtrade/agent-credential.json with
mode 0600. The secret is never returned to the model, including when an API
response or WebSocket diagnostic tries to echo it.
If that directory already exists and is group- or world-writable, signup refuses rather than writing a secret somewhere other people can read. It tells you the fix:
chmod 700 ~/.config/backtrade
The adapter creates the directory itself at mode 0700 when it is absent,
whatever your umask. You only see the refusal if an older directory is already
there — a umask of 002, the Ubuntu and Debian default, produces 0775. The
refusal is deliberate: a credential is not worth leaking to save you one command.
Node 18 or newer. The adapter is a single ES module, so it is named
.mjs— that extension is what makes it load on every Node version rather than only on 22.7+. If you have an older config pointing atbacktrade-mcp.jsit still works, but only on Node 20.19+ or 22.7+.
mkdir -p "$HOME/.local/share/backtrade"
curl -fsSLo "$HOME/.local/share/backtrade/backtrade-mcp.mjs" \
https://backtrade.dev/backtrade-mcp.mjs
curl -fsSLo "$HOME/.local/share/backtrade/backtrade-mcp.mjs.sha256" \
https://backtrade.dev/backtrade-mcp.mjs.sha256
(cd "$HOME/.local/share/backtrade" && sha256sum -c backtrade-mcp.mjs.sha256)
Add it to Codex CLI, the Codex IDE extension, or the ChatGPT desktop app's shared local MCP configuration:
codex mcp add backtrade -- node "$HOME/.local/share/backtrade/backtrade-mcp.mjs"
That command shape follows the official Codex MCP documentation.
For Claude Code:
claude mcp add --scope user backtrade -- node "$HOME/.local/share/backtrade/backtrade-mcp.mjs"
Then ask your agent:
Use Backtrade. Call
signup_trial, list recent SPY days for my model, start a pull session, inspect events without future data, paper-trade my strategy, and finish with the scorecard. Do not invent a strategy or claim live profitability.
signup_trial creates a Tier-0 file automatically.
Economic releases and earnings through the MCP adapter. The adapter subscribes to the
eventschannel for you — you do not send a subscribe frame, and typedeconandearningsevents arrive alongside bars and news. The published bundle now delivers them: measured 2026-09-12 againstbacktrade-mcp.mjssha25657d0e030…, one SPY session returned{"econ": 21, "news": 75, "bar": 896}, witheconframes arriving as typed{"T":"ev","kind":"econ",…}alongside the bars. An earlier note here said the published bundle returned 0 econ, measured 2026-09-04; that was true of the bundle of the day and is no longer true. The raw WebSocket path below is one way to read events, not the only one.
Account linking is LIVE.
GET https://api.backtrade.dev/v1/statusis authoritative and reports"accounts"and"device_linking"as enabled.https://backtrade.dev/app.htmlserves a real dashboard with sign-in, Keys and saved runs, andlink_accountreturns a short code you approve there.Measured end to end against the running service on 2026-09-10: register, sign in,
link_account, approve in the dashboard, replay on the minted key, and find the run saved under the account. The key it mints is tier 1.Paid tiers are not purchasable yet, so a free account is blind-only (base-100 prices) and up to 5 symbols per session.
Verified 2026-09-15 against
6d1003d5.
To attach this MCP to your
Backtrade account without putting a key in chat, call link_account. It stages a
new credential secret and a separate polling token locally before networking, in
a second mode-0600 file. The tool returns only a short code, the fixed
https://backtrade.dev/app.html dashboard URL, and its ten-minute expiry.
Sign in at that URL, open Keys, enter the code, inspect the device label and
fixed scopes, then approve with your current password. Approval never subscribes
or charges you. Call link_account_status afterward. It polls with the retained
token internally, atomically installs the Tier-1 account key, acknowledges receipt,
and only then removes the pending file. A working Tier-0 key is preserved until
that install succeeds; a lost response can be retried without minting another key.
Denied or expired requests require link_account {"restart":true} so replacement
authority is always explicit.
For an existing account or paid Tier-1 key issued through another client, you may still put its ID, secret, scopes, and cached expiry in the same secure file manually (do this outside model/chat context):
{
"version": 1,
"state": "active",
"keyId": "YOUR_16_CHARACTER_KEY_ID",
"secret": "YOUR_43_CHARACTER_SECRET",
"tier": 1,
"scopes": ["replay:read", "orders:simulate"],
"expiresAt": "2026-10-09T00:00:00.000Z"
}
Run chmod 600 ~/.config/backtrade/agent-credential.json after writing it. You
can select another file with BACKTRADE_CREDENTIAL_PATH. The cached Tier-1
expiresAt is metadata only because renewals happen on the server; authentication
there is authoritative on every request. Tier-0 files remain locally fail-closed
at their non-renewable expiry.
The adapter exposes:
| Tool | Purpose |
|---|---|
signup_trial |
Create/recover a bounded 24-hour local credential |
link_account |
Stage account-key authority locally and show its short dashboard approval code |
link_account_status |
Poll internally, atomically install the approved Tier-1 key, then acknowledge it |
list_days |
Find replayable sessions and leakage metadata, on whichever feed covers each day |
start_session |
Open a pull replay; Tier-0 is one indexed symbol, Tier-1 may request its plan's multi-symbol/unblinded limits |
advance |
Reveal the next events or advance to a UTC time |
session_status |
Read clock, remaining events, account, and capacity state |
place_order |
Submit market, limit, or stop paper orders |
cancel_order |
Cancel a resting order |
get_account |
Read cash, positions, P&L, fills, and costs |
get_orders |
Read consistently serialized order state |
end_session |
Reveal truth and the causal buy-and-hold scorecard |
resume_session |
Continue a SUSPENDED replay from the event it stopped at, after a server restart |
run_campaign |
Pooled benchmark across a span of days — opens no session and spends no quota |
arm_gate |
Stop the tape the moment a rule matches, and keep it stopped until you acknowledge it |
ack_event |
Acknowledge the held event by its seq and let the tape run on |
disarm_gate |
Stop the rule firing again. It does NOT release a hold already in effect — ack that one first |
Both surfaces serve all seventeen, gate tools included. [Measured 2026-09-12:
tools/list returns 17 names against the hosted MCP at
https://api.backtrade.dev/mcp and against the published bundle, and arm_gate
on a hosted session holds the tape exactly as it does locally.] An earlier draft
of this note claimed the three were local-only; that was read off a test fixture
instead of measured against the live endpoint, and it was wrong.
Arguments
? marks optional. model is required on list_days and start_session.
For start_session, pass exactly one of the backward-compatible singular symbol
or the symbols array (1–50 transport bound); the server applies the credential's
smaller plan limit. blind defaults to true.
| Tool | Arguments |
|---|---|
signup_trial |
label? |
link_account |
label?, restart? (restart is accepted only after denial or expiry) |
link_account_status |
none; code and polling token are loaded from the pending file |
list_days |
symbol, model, limit?, from?, to? |
start_session |
exactly one of symbol or symbols, plus date, model, blind?, cash?, cost_bps?, to?, allow_incomplete?, success_threshold?, control_arm? |
advance |
session_id, at most one of events? or until? (neither defaults to 30 events); not_after? bounds events mode |
session_status |
session_id |
place_order |
session_id, symbol, side, qty, type?, limit_price?, stop_price?, client_order_id? |
cancel_order |
session_id, order_id |
get_account |
session_id |
get_orders |
session_id |
end_session |
session_id, force? |
resume_session |
session_id, plus the same symbol/symbols, date, model, blind?, to? the session was opened with |
run_campaign |
symbols, from, to, strategy (buy_and_hold or do_nothing), cash?, cost_bps? |
arm_gate |
session_id, match, gate? (hold default, or notify) |
ack_event |
session_id, seq — required while the tape is held; the advance that reported the hold carried it as gate.held_on.seq |
disarm_gate |
session_id |
Two controls that turn a backtest into a prediction
These are the difference between a number you found and a number you predicted.
Both are optional arguments to start_session; neither changes the tape.
success_threshold — pre-registration. State what would count as success
before the run, as { metric, op, value }. The server records it at session
open and grades against it at end_session. It refuses a threshold supplied
after the run, which is the whole point: a criterion chosen once the result is
known is not a criterion. The scorecard reports three states and keeps them
apart — met, not met, and ungradeable when the metric is unavailable. An
unmet threshold is a normal outcome and is reported as one.
control_arm — permutation control. Re-executes the same trades at randomly
drawn instants on the identical tape, so only the timing moves. If the
strategy's result survives that, the timing carried information; if the control
scores the same, the result was the tape and not the strategy. Its absence
leaves no timing field on the scorecard, so a run without a control cannot be
mistaken for one that passed it.
allow_incomplete serves a day whose regular session is missing bars, rather
than refusing it. The scorecard still reports what was missing — the flag
changes what is refused, never what is disclosed.
One measured production run from this server reached a scorecard in about 8 seconds; network, model and database latency will vary:
signup_trial { } -> keyId
list_days { symbol: "TSLA", model: "claude-opus-5" } -> replayable dates
start_session { symbol: "TSLA", date: "2026-03-23",
model: "claude-opus-5" } -> session_id, leakage
advance { session_id, events: 60 } -> bars and headlines
place_order { session_id, symbol: "TSLA",
qty: 10, side: "buy" } -> order id
advance { session_id, events: 500 } repeat until finished
end_session { session_id } -> scorecard
The model argument is evaluation metadata used for a leakage disclosure. It
does not restrict which LLM or harness can use Backtrade. Unmeasured models
are labelled contaminated by default; the replay still belongs to you.
Direct API: any harness
1. Create a trial credential
Your client creates the secret and sends only its SHA-256 verifier. The API
returns a bare 16-character keyId; WebSocket and REST authentication always use
that key ID and the locally retained secret as separate fields.
SECRET=$(head -c 32 /dev/urandom | basenc --base64url | tr -d '=')
VERIFIER=$(printf '%s' "$SECRET" | openssl dgst -sha256 -binary | basenc --base64url | tr -d '=')
IDEM=$(head -c 24 /dev/urandom | basenc --base64url | tr -d '=')
curl -sS -X POST https://api.backtrade.dev/v1/agent-trials \
-H 'content-type: application/json' \
-H "idempotency-key: $IDEM" \
-d "{\"secretVerifier\":\"$VERIFIER\",\"label\":\"my-agent\"}"
Save keyId from the response and keep SECRET locally:
KEY_ID=... # exact bare 16-character keyId
KEY_SECRET="$SECRET" # never sent during signup; cannot be recovered
Tier-0 trials last 30 days, permit 100 sessions, require blind=1, and allow up
to 5 symbols per session. A signed-in free account is a different allowance and a
larger one — 600 sessions a month, no expiry — so the 100 here is the no-sign-up
number, not the ceiling on what free gets you. Here blind=1 means prices/fills share a base-100 scale; the
agent still knows the symbol and date it selected, and headlines retain identifying
prose. It is price-indexed, not anonymous. No email, card, or account is required.
Signup is rate-limited per day, so a shared IP, a CI runner, or a NAT'd office can exhaust it without the individual agent having signed up before. Both caps reset on the next UTC issuance day; neither is a ban, and neither consumes your local secret.
Response error |
Meaning | What to do |
|---|---|---|
source_trial_limit |
Your source reached its trials for today (currently 5) | Reuse the credential you already hold, or retry after the UTC day rolls over |
global_trial_limit |
Site-wide daily trial capacity is spent (currently 200) | Retry after the UTC day rolls over |
idempotency_conflict |
The same idempotency-key was reused with different credential material |
Generate a fresh SECRET/VERIFIER/IDEM triple |
Both limits are deployment policy and can change; the error codes are the stable
contract. A failed signup leaves a reusable state: "pending" credential on disk —
calling signup_trial again resumes that same request rather than starting over, so
retrying after the reset costs you nothing.
2. Discover sessions
curl -sS 'https://api.backtrade.dev/v2/replay/days?symbol=SPY&model=your-model&limit=20' \
-H "APCA-API-KEY-ID: $KEY_ID" \
-H "APCA-API-SECRET-KEY: $KEY_SECRET"
3. Open a pull replay
Connect to:
wss://api.backtrade.dev/v2/iex?date=2026-04-30&symbols=SPY&blind=1&mode=pull&model=your-model
The socket sends two different control frames with T:"success". First it says
connected; that means only that the WebSocket opened. Authentication is complete
only after the later, exact {"T":"success","msg":"authenticated"} frame.
Do not match on T:"success" alone, and do not subscribe before that second frame.
← {"T":"success","msg":"connected"}
→ {"action":"auth","key":"<KEY_ID>","secret":"<KEY_SECRET>"}
← {"T":"success","msg":"authenticated"} # auth is complete here
→ {"action":"subscribe","bars":["SPY"],"news":["SPY"],"events":["SPY"]}
← {"T":"subscription", ...}
Subscribe to events or you will not receive any. Scheduled economic
releases and earnings are delivered only to that channel, and a socket subscribed
to bars and news alone receives none of them, silently and for the whole
session — advance still reports the events it popped from the tape, so
delivered and frames_sent diverge with nothing saying why. The channels are
bars, news, events, trades and quotes; the acknowledgement echoes the
complete current state of all of them, so check it rather than assuming.
Queue unmatched frames: T:"replay" may arrive while your client is handling the
subscription acknowledgement. It carries far more than session_id — it is the only
place the session tells you what kind of day you just opened, and four of its fields
change how you should read your own result. Captured from a live server, not written
from memory:
{"T":"replay","feed":"iex","timeframe":"1Min","mode":"pull","extended_hours":false,
"date":"1970-01-01","symbols":["SPY"],"speed":0,"blind":true,
"volume_redacted":true,
"total_events":476,
"total_by_kind":{"bar":404,"econ":60,"news":12},
"fill_latency":{"SPY":{"median_min":1,"p90_min":1,"max_min":1,"rth_bars":390}},
"completeness":{"measured":true,"rth_expected":391,"rth_complete_floor":390,
"min_rth_bars_to_serve":195,"rth_bars":{"SPY":390},
"complete":true,"acknowledged_incomplete":false},
"leakage":{"verdict":"contaminated","note":"...","news_redaction":{...}},
"session_id":"s_...","cash":100000,"cost_bps":1}
| field | why you care |
|---|---|
volume_redacted |
on a blind session every bar's v is null, not zero. Zero would assert no trades occurred; null says not disclosed. If you size orders by observed volume, that signal is absent here and this flag is how you find out before your strategy silently does nothing |
fill_latency |
a market order fills at the first open strictly after the submission clock (13:40:00 → 13:41 open on a contiguous minute tape). This reports tape-spacing statistics; use submitted/filled timestamps for actual per-order latency. median_min is 1 on a full tape. It is the p90_min and max_min you want: a day can median 1 minute and still contain a 28-minute hole, and your 09:45 entry becomes a 10:13 entry inside it |
completeness |
rth_bars against rth_expected, plus the floor. A day under min_rth_bars_to_serve is refused outright unless you pass allow_incomplete=1, so if you opted in, this is the day you opted into |
total_by_kind |
total_events counts what the tape yields; a subscription filter decides what you receive. Here 476 tape events become 404 bar frames for a bars-only subscriber. Do not compare a received count against total_events — they are different quantities |
leakage |
contaminated is the default for any model with no measured memory horizon on this box. It is not an accusation, it is an absence of measurement |
Your harness owns the clock:
{"action":"advance","events":30}
{"action":"advance","until":"15:30:00"}
{"action":"status"}
{"action":"end"}
For bar replay, until also accepts YYYY-MM-DDTHH:MM[:SS]Z in the
session's displayed wire clock. Bare times stay anchored to the start date.
With events, an optional full-instant not_after stops at that availability
cutoff, including when using a trigger. It cannot be combined with until.
Tick replay continues to use milliseconds.
Responses are T:"replay_advance", replay_status, and replay_result.
advance includes fills caused by that batch. end refuses before completion;
{"action":"end","force":true} drains the tape and marks the result forced.
THE DATA DOES NOT COME BACK INSIDE THE
replay_advanceREPLY. Read the frames that arrive BEFORE it. On this WebSocket the events are pushed as their own frames —T:"b"bars,T:"n"news,T:"ev"calendar events — and thereplay_advancethat follows is an ACKNOWLEDGEMENT carrying adeliveredCOUNT, not a payload. A client that reads only the reply to its ownadvanceseesdelivered: 30and no data, and no error, which looks exactly like an empty tape.Measured on 2026-04-30/SPY:
{"action":"advance","events":5}produced 30 data frames, then{"T":"replay_advance","delivered":30}with noframesarray at all.Accumulate frames from the socket as they arrive and treat
replay_advanceas the end-of-batch marker. The MCP bundle differs here — it does that accumulation for you and returns the events inside the tool result — so code written against the MCP shape will not port to the raw socket unchanged.
If you want a wall-clock push stream instead, omit mode=pull and set speed.
Remember that your network/model latency is multiplied by replay speed; pull mode
is the safer default for turn-based agents.
4. Paper orders and results
session_id is a query parameter. Credentials use Alpaca's separate headers:
curl -sS -X POST "https://api.backtrade.dev/v2/orders?session_id=$SESSION_ID" \
-H "APCA-API-KEY-ID: $KEY_ID" \
-H "APCA-API-SECRET-KEY: $KEY_SECRET" \
-H 'content-type: application/json' \
-d '{"symbol":"SPY","qty":10,"side":"buy","type":"market","client_order_id":"my-order-1"}'
Useful endpoints:
GET /v2/orders?session_id=...
GET /v2/orders/<order-or-client-id>?session_id=...
DELETE /v2/orders/<order-or-client-id>?session_id=...
DELETE /v2/orders?session_id=... # cancel all resting
GET /v2/account?session_id=...
GET /v2/positions?session_id=...
GET /v2/replay/result?session_id=... # after completion
GET /v1/status # build + capabilities
Retries with the same client_order_id and identical terms return the original
order. Reusing it with changed terms returns 409, preventing duplicate trades.
Resting orders expire when the tape completes; new orders are then rejected.
5. Paid plans
GET /v1/billing/plans is the catalog; when purchasable is true the paid plans
can be bought (not yet — billing_checkout is false; see the notice above).
Indie and Pro unblind prices, allow several symbols per session,
and meter sessions per calendar month (UTC) instead of per 24-hour key. Buying
is three calls:
# 1. Start checkout with the key you already have (trial keys work).
curl -sS -X POST https://api.backtrade.dev/v1/billing/checkout \
-H "APCA-API-KEY-ID: $KEY_ID" -H "APCA-API-SECRET-KEY: $KEY_SECRET" \
-H 'content-type: application/json' -d '{"plan":"indie","cadence":"monthly"}'
# -> {"checkout_url": "https://checkout.stripe.com/..."} pay there
Stripe returns you to https://backtrade.dev/billing-ok.html?session=cs_...,
which makes your paid key in the browser. From your own code, do the same thing
with the session value from that URL and a fresh secret:
# 2. Claim the durable key. New secret, only its hash is sent.
PAID_SECRET=$(head -c 32 /dev/urandom | basenc --base64url | tr -d '=')
PAID_VERIFIER=$(printf '%s' "$PAID_SECRET" | openssl dgst -sha256 -binary | basenc --base64url | tr -d '=')
curl -sS -X POST https://api.backtrade.dev/v1/billing/claim \
-H 'content-type: application/json' \
-d "{\"checkout_session\":\"cs_...\",\"secretVerifier\":\"$PAID_VERIFIER\",\"label\":\"my-agent\"}"
# -> {"keyId": "...", "tier": 1, "plan": "indie", "expiresAt": "..."}
One claim per checkout; the secret is never shown again. The trial key that started checkout is also entitled until it expires, so an agent mid-session is not cut off.
# 3. Use the paid key exactly like the trial key.
KEY_ID=<paid keyId>; KEY_SECRET="$PAID_SECRET"
# blind=0 and symbols=SPY,QQQ,IWM are now allowed up to the plan's limits.
GET /v1/billing/subscription returns the plan, status, limits and sessions
used this month. POST /v1/billing/portal returns a Stripe portal URL to change
the card, switch cadence, or cancel. A canceled or unpaid subscription drops the
key to free limits immediately; the key itself stays valid for 7 days past the
paid period so a late renewal webhook cannot lock you out. Sessions beyond the
plan's monthly allowance are refused with 429 at open; symbols beyond the plan
with 403; unblinded prices without a plan with 402.
Causal fill model
- Completed OHLCV is unavailable until the interval closes.
- A market order fills at the first interval open strictly after submission time: at 13:40:00, the earliest on a contiguous minute tape is the 13:41 open. The 13:39 bar became visible at 13:40; the 13:40 open has already passed. Gaps can delay a fill.
- Limits and stop activation use intervals starting strictly after submission. Limits are reported at interval completion; stops arm from a completed range before a later eligible execution. Historical results retain the engine behavior that produced them.
- A coherent bar with moving prices and zero reported volume has an unknown liquidity budget; no volume-derived cap applies, but buying power does. Flat-zero and positive volumes retain their caps. Unknown volume does not establish real liquidity, and positive reported volume can also be undercounted.
- A limit uses minute high/low and has no queue-position model.
- A stop arms when touched and fills causally afterward.
- One-way
cost_bpsis charged on every actual fill. - Blinded trial prices and fills share the same base-100 scale.
What the news stream actually contains
Headlines are real and causally timestamped, but volume and composition vary enormously by symbol, and that changes which strategies are testable. Measured across 7 symbols over the week ending 2026-09-02:
| symbol | news frames | % retail social |
|---|---|---|
| NVDA | 1,257 | 39.5% |
| SPY | 596 | 86.7% |
| AAPL | 586 | 44.2% |
| AMD | 442 | 57.7% |
| TSLA | 426 | 39.4% |
| QQQ | 350 | 85.4% |
| AMC | 15 | 86.7% |
Two things follow, and neither is obvious from a day's bar count:
- Index ETFs are mostly social posts. SPY and QQQ cannot have earnings, insider
filings or press releases, so the publisher sources that cover single names have
nothing to emit about them. Social posts carry a self-assigned
BULLISH/BEARISH/neutraltag from their author, at post time. That tag is causal — it is not a later sentiment score — but it is one retail user's opinion, not a wire signal. - Some symbols receive almost no news. AMC produced 15 frames across seven trading days. An event-driven strategy on such a symbol has nothing to trigger on, and nothing in the bar count reveals this in advance.
Sources present for single names include benzinga, yahoo-finance, fmp-news,
fmp-press, fmp-insider, sec-form4 and sec-form144.
Scheduled earnings and economic releases ARE delivered as typed events, on the
events channel, as T:"ev" frames. This document previously said they were not;
that was wrong. Subscribe to events (above) or you receive none of them.
Each release arrives twice, and the split is the point: a phase:"schedule" frame
when the calendar entry becomes known, carrying estimate and previous and no
actual, then a phase:"print" frame at release time carrying actual — and
surprise only when a consensus estimate existed. That lets a strategy position
ahead of a print it can see coming without ever seeing the number early.
A real pair from one session, the same release seen twice:
{"T":"ev","kind":"econ","S":"NVDA","phase":"schedule","event":"MBA Mortgage Refinance Index",
"country":"US","importance":"low","scope":"market","release_at":"...",
"estimate":null,"previous":740.8}
{"T":"ev","kind":"econ","S":"NVDA","phase":"print","event":"MBA Mortgage Refinance Index",
"country":"US","importance":"low","scope":"market","release_at":"...",
"estimate":null,"previous":740.8,"actual":732.6}
Most releases have no consensus estimate, and without one there is no surprise.
estimate is null on the majority of frames, and surprise is omitted entirely
rather than sent as zero — a strategy that keys on surprise must treat the field as
absent, not as no-change. Measured across 90 econ frames from two sessions on one
date: 29% carried a non-null estimate and 13% carried surprise. That is a single
date and the mix varies by day, so treat the proportions as an order of magnitude
rather than a rate, but do not design as though every release is a surprise trade.
kind:"econ" is macro and carries scope:"market" — it is fanned out per symbol,
so a two-symbol session sees one release twice and must not count it as two
signals. kind:"earnings" belongs to one ticker and adds eps_actual,
revenue_actual and eps_surprise on the print. An index ETF has no earnings, so
SPY and QQQ receive econ frames only.
Two limits worth knowing before you build on them. Earnings carry a report_date
and no release time, so before-open versus after-close is not distinguishable
from the frame — the session's own print may therefore be unusable for a
same-day trade. And a session covers one trading day, so the common
"buy the day before, hold through the print" structure cannot be expressed in a
single session.
Each news frame carries a timing field — instant, minute, or grid_15m —
saying how precisely its source's publication time is known. Treat grid_15m
timestamps as accurate to the quarter hour, not the second, before building a
latency-sensitive strategy on them.
Roughly 11% of publisher headlines are republished, sometimes hours apart and sometimes across two sources. If your strategy keys on headline arrival rate, deduplicate on headline text first.
Limits and responsibility
This is historical simulation, not live execution or investment advice. Bar replay
fills an order in full or rejects it under its prior-minute liquidity budget; it
does not model partial fills or observe intraminute spread. Supported
sip-partial-v2 tick sessions model partial fills using recorded liquidity and
quote spreads. Legacy sip-full-v1 tick sessions wait for a single quote with
enough displayed size to fill the entire order; they do not fill it partially.
Neither mode models order-book queue position, market impact or auctions.
Limit/high-low ordering inside a minute is modelled, not observed.
Configured basis-point costs are already reflected in cash and net P&L. They
are embedded in market-order fill prices; any portion constrained by a limit
price is charged separately to cash. Do not subtract the reported total_cost
again from net P&L. Results are simulations, not promises.
Backtrade supplies infrastructure. You own your model, strategy, prompts, risk, and outcomes. See the full Terms and Privacy notice.
The product is backtrade.dev. backtest.dev is an unrelated parked domain.
Optional model decisions in code strategies
Python strategies can use a controlled ctx.infer connection with server-recorded requests, responses and usage. Fresh calls require your own Anthropic account and a separately approved run allowance; Backtrade provides no pooled inference. Recorded reruns make no new model calls and fail on a cache miss. Ordinary backtesting needs no provider or broker account. See Model decisions for limits, research-clock behavior and API/MCP tools.
News clock incident: FMP editorial news is temporarily withheld because its publication times were hours early. Prior runs with unknown or FMP exposure may be affected; results have not been recalculated. Incident details. Corrected FMP replay availability is publisher time plus a 1-minute rounding allowance; FMP delivery latency is unmeasured.
Covered-call scenario lab
Open Options to compare a hypothetical single-cycle payoff with holding the same shares or cash. This is not historical options replay, a quote feed or an order ticket. The current options history is not validated for performance comparisons.
The read-only calculate_covered_call MCP tool uses the same function as the browser (local Node, Bun remote and local-engine adapters; hosted MCP retains normal authentication). No provider call, account mutation or new REST endpoint is involved. Pass all fields under scenario:
{"scenario":{"stock_price":100,"shares":100,"strike":105,"premium":2,"expiry_price":110,"option_fee_per_contract":0.65,"stock_buy_fee":1,"assignment_fee":5,"assignment":"automatic"}}
Dollar inputs have at most two decimals. Shares must be covered standard 100-share lots. Use the bid for premium received per share, not the midpoint. Option fees must fit premium proceeds; this model does not borrow to pay fees. automatic models exercise if at least $0.01 in the money at expiry; assigned and not_assigned are labeled counterfactuals. Actual exercise may differ. There are no dividends, taxes, cash interest, early assignment, rolls or reinvestment. Fees and short-option liability are accounted for; premium alone is not profit. Export includes inputs, assumptions and model version. Existing replay order tools remain equity tools.