backtest_screen
Run a screen back through every year we hold and compare it to the market. Pro only.
What it does
Rebuilds the screen once per year of stored history, holds the cohort for min_hold_years, and reports the compound annual growth rate of the equal-weighted strategy against the equal-weighted market over the same years. Criteria and markets take the same shape as screen_stocks. cohort_sizes is one entry per ENTRY year, and years carries one more entry than that -- the trailing year the last cohort is sold in -- so do not zip the two. weightings does that zip correctly, twice: equal and market_cap are the two weightings the website offers as a toggle, and each carries its own strategy_cagr and market_cagr, a yearly_returns array of one row per holding period (entry_year, exit_year, the strategy and market return for that year as ratios where 0.12 is +12%, their difference as excess, and the cohort_size that earned it), and a wealth_index of both curves compounded from 100 with one entry per entry in years -- the chart the website draws. Each block is self-consistent: chaining its yearly_returns reproduces ITS strategy_cagr, so never quote a year from one weighting against a CAGR from the other. The top-level strategy_cagr and market_cagr are the equal-weighted pair, unchanged. The cohort is the same companies under both weightings; only the weight each carries differs, and a company with no usable market capitalisation carries ZERO weight in market_cap while still counting in cohort_size -- market_cap_zero_weight is how many. The figures are a property of the rows we hold, not a forecast, and a small matched_symbols means the result describes a handful of names. membership says where each cohort was DRAWN FROM, which decides whether the figures mean anything. basis: "point-in-time" means each year uses the companies actually in the index that year, and membership.years carries, per year, how large the index was, how many of those companies we hold a scored row for (coverage), and how well documented that year's index changes are (confidence). basis: "current-constituents" means every cohort was drawn from TODAY's members, so companies that failed or were acquired are missing from every year and the result is flattered -- say so when reporting it. unpriceable_exits_excluded counts positions that left the index in a year we cannot price, which are dropped rather than booked as a total loss, because only a documented failure is worth -100%. survivorship says what corrects survivorship bias and what does not: there is no flat haircut any more, because the old one was subtracted from both legs and cancelled in the comparison, so it never corrected anything and only lowered both absolutes by an invented figure. Read survivorship.corrected_by with membership.basis. cohort_sizes is the per-year cohort, and thin_years, cash_years and smallest_cohort pick out the years the result actually rests on -- a year priced off two names compounds into the headline figure exactly as hard as a year of ninety, and matched_symbols is an aggregate that hides it.
backtest_screen needs Pro. A free key gets requires_pro back instead of data, on both transports. Everything else in the tool list runs on a free account.
Arguments, returns and a worked call
Everything below is generated from the same registry entry the MCP server and /openapi.json are generated from, so this page cannot name an argument that does not exist. The two examples are the same call, once over each transport.
backtest_screen
ProRebuilds the screen once per year of stored history, holds the cohort for `min_hold_years`, and reports the compound annual growth rate of the equal-weighted strategy against the equal-weighted market over the same years. Criteria and markets take the same shape as screen_stocks. `cohort_sizes` is one entry per ENTRY year, and `years` carries one more entry than that -- the trailing year the last cohort is sold in -- so do not zip the two. `weightings` does that zip correctly, twice: `equal` and `market_cap` are the two weightings the website offers as a toggle, and each carries its own `strategy_cagr` and `market_cagr`, a `yearly_returns` array of one row per holding period (`entry_year`, `exit_year`, the strategy and market return for that year as ratios where 0.12 is +12%, their difference as `excess`, and the `cohort_size` that earned it), and a `wealth_index` of both curves compounded from 100 with one entry per entry in `years` -- the chart the website draws. Each block is self-consistent: chaining its `yearly_returns` reproduces ITS `strategy_cagr`, so never quote a year from one weighting against a CAGR from the other. The top-level `strategy_cagr` and `market_cagr` are the equal-weighted pair, unchanged. The cohort is the same companies under both weightings; only the weight each carries differs, and a company with no usable market capitalisation carries ZERO weight in `market_cap` while still counting in `cohort_size` -- `market_cap_zero_weight` is how many. The figures are a property of the rows we hold, not a forecast, and a small `matched_symbols` means the result describes a handful of names. `membership` says where each cohort was DRAWN FROM, which decides whether the figures mean anything. `basis: "point-in-time"` means each year uses the companies actually in the index that year, and `membership.years` carries, per year, how large the index was, how many of those companies we hold a scored row for (`coverage`), and how well documented that year's index changes are (`confidence`). `basis: "current-constituents"` means every cohort was drawn from TODAY's members, so companies that failed or were acquired are missing from every year and the result is flattered -- say so when reporting it. `unpriceable_exits_excluded` counts positions that left the index in a year we cannot price, which are dropped rather than booked as a total loss, because only a documented failure is worth -100%. `survivorship` says what corrects survivorship bias and what does not: there is no flat haircut any more, because the old one was subtracted from both legs and cancelled in the comparison, so it never corrected anything and only lowered both absolutes by an invented figure. Read `survivorship.corrected_by` with `membership.basis`. `cohort_sizes` is the per-year cohort, and `thin_years`, `cash_years` and `smallest_cohort` pick out the years the result actually rests on -- a year priced off two names compounds into the headline figure exactly as hard as a year of ninety, and `matched_symbols` is an aggregate that hides it.
Arguments
criteria object[], required — Filters, each `{ "id": <criterion id>, "operator": <operator>, "value": <value> }`. A numeric criterion takes `>`, `<`, `>=` or `<=`; the word forms `More Than` and `Less Than` are accepted too, and case and surrounding spaces do not matter. `>=` and `<=` include the boundary value, `>` and `<` exclude it -- ask for the one you mean rather than nudging the number. A list criterion takes `in` or `not in` and a list of strings; a boolean criterion takes `True` or `False` and no `value`. Call explain_metric for the operators a given id accepts.
markets string[], optional — Which universes to screen. One or more of: sp500, ftse100, russell1000, ftseallshare, djia, nasdaq100. Defaults to sp500 and ftse100.
min_hold_years number, optional — Whole years to hold a name before it can be dropped, 1-10. 1 is a classic yearly rebalance. Defaults to 1.
Returns
strategy_cagr, market_cagr, years, min_hold_years, cohort_sizes, weightings, market_cap_zero_weight, membership, unpriceable_exits_excluded, survivorship, thin_years, cash_years, smallest_cohort, matched_symbols, universe_size, markets, note, query, url, url_note, screener_url, error, as_of, source, disclaimer
> curl -s https://crushthemarket.com/api/v1/backtest_screen \ -H "Authorization: Bearer $CTM_KEY" \ -H 'Content-Type: application/json' \ -d '{"criteria":[{"id":"totalScore","operator":">","value":70}],"markets":["sp500"]}'{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "backtest_screen", "arguments": { "criteria": [ { "id": "totalScore", "operator": ">", "value": 70 } ], "markets": [ "sp500" ] } }}Can answer
requires_pro — The caller is not on an active subscription.
bad_request — `criteria` is empty or malformed, a criterion id is not published, or an operator is not one the criterion accepts (`>=` needs a criterion that compares numerically, so a boolean or list criterion rejects it).
Calling backtest_screen
- 1.Register free — an email address, no card — then create a key under API Keys on your account page. Your email address has to be verified first, and the key is shown once.
- 2.Using a chat client that speaks MCP? The MCP guide is one paste. Anything else takes bearer auth, the error codes and the limits from the REST guide.
- 3.Make the call above with your own key in it.
Every successful response carries as_of, source, a quota block saying how much of the allowance is left, and the same disclaimer this page ends on.
More for agents
Data, not advice. Do your own diligence.