Backtests
Replay whale and insider signals over resolved markets with your own stake rule and fee assumptions, then poll the job for an equity curve and a trade list.
A backtest replays historical signals against markets that have already resolved, under stake and fee assumptions you choose. Two endpoints: one enqueues a job, one polls it. By the end of this page you will be able to run a replay, read the summary honestly, and know which assumptions the result depends on.
What a signal replay is
Every whale and insider alert we have ever recorded carries the timestamp it fired at, the score it fired with, the market's category, the odds at entry, and the size of the trade that triggered it. A backtest walks that history in order, takes every alert matching your filters, stakes it by your rule, and settles it against the market's resolved outcome net of the fees you specify.
What it answers: if I had mechanically taken every signal of this shape, with this stake rule, what would the equity curve have looked like?
What it does not answer: what you would actually have made. You would not have
been filled at the alert price, and the sizing you can support is not the sizing
in the sample. The assumptions block in every result names each gap explicitly
— read it before you quote a number from the summary.
POST /api/v1/backtest/run
Minimum tier free. Returns 202 with a job id — the run itself happens on a
queue.
curl -X POST "https://crowdintel.xyz/api/v1/backtest/run" \
-H "Authorization: Bearer $CROWDINTEL_KEY" \
-H "Content-Type: application/json" \
-d '{
"signal": "insider",
"initial_capital": 10000,
"period": { "from": "2026-01-01", "to": "2026-06-30" },
"filters": { "min_score": 75, "category": "politics", "max_odds": 0.6 },
"stake_rule": { "kind": "fractional", "fraction": 0.02, "max_fraction": 0.1 },
"fees": { "fee_bps": 200, "slippage_bps": 50 }
}'{ "job_id": 1234, "status": "queued", "poll": "/api/v1/backtest/1234" }Body
Every field is optional; an empty body runs all signals over all history with the engine defaults. Unknown keys are rejected rather than ignored, so a typo fails loudly instead of silently changing nothing.
| Field | Type | Range | Default |
|---|---|---|---|
signal | whale, insider, all | — | all |
initial_capital | number | > 0, ≤ 100,000,000 | 10,000 |
period.from / period.to | ISO datetime or YYYY-MM-DD | span ≤ 1,095 days | all history |
filters.min_score / max_score | number | 0–100 | none |
filters.category | string | 1–64 chars | any |
filters.min_odds / max_odds | number | 0–1 | none |
filters.min_bet_value | number | ≥ 0 | none |
stake_rule.kind | flat, fractional | — | flat |
stake_rule.size | number | > 0, ≤ 1,000,000 | 100 |
stake_rule.fraction | number | > 0, ≤ 1 | 0.05 |
stake_rule.max_fraction | number | > 0, ≤ 1 | 0.1 |
stake_rule.scale_by_score | boolean | — | false |
fees.fee_bps | number | 0–2000 | 200 |
fees.slippage_bps | number | 0–2000 | 0 |
size applies to a flat rule; fraction and max_fraction apply to a
fractional one, where each stake is a share of current equity capped at
max_fraction. scale_by_score scales the stake with the signal's score.
filters.max_odds is the useful one for anyone who has read the win-rate
argument: capping entry odds at 0.6 removes the near-certain favourites that
inflate a hit rate without adding expected value.
Limits
| Tier | Runs per month | Concurrent jobs |
|---|---|---|
| Free | 3 | 1 |
| Terminal | uncapped | 3 |
| Terminal Pro | uncapped | 3 |
Exhausting the monthly allowance returns 429 with code rate_limited. Having
too many jobs queued or running at once returns 429 as well — poll and drain
what you have before enqueuing more. If the shared queue is saturated the
endpoint returns 503 with code internal_error; retry shortly.
GET /api/v1/backtest/{jobId}
Minimum tier free. A job that does not exist, or belongs to another account,
returns 404 — jobs are private to the key that created them.
curl "https://crowdintel.xyz/api/v1/backtest/1234" \
-H "Authorization: Bearer $CROWDINTEL_KEY"status is one of queued, running, done or error. result is populated
only on done, error only on error.
{
"job_id": 1234,
"status": "done",
"created_at": "2026-08-16T09:00:00.000Z",
"started_at": "2026-08-16T09:00:04.000Z",
"finished_at": "2026-08-16T09:00:31.000Z",
"params": { "signal": "insider", "initial_capital": 10000 },
"error": null,
"result": {
"summary": {
"roi": 0.184,
"max_drawdown": 0.121,
"sharpe": 0.34,
"hit_rate": 0.512,
"total_trades": 418,
"wins": 214,
"losses": 204,
"initial_capital": 10000,
"final_equity": 11840.22,
"net_pnl": 1840.22,
"avg_return_per_trade": 0.0044
},
"equity_curve": [
{ "t": "2026-01-04T00:00:00.000Z", "equity": 10012.4, "pnl": 12.4 },
{ "t": "2026-01-05T00:00:00.000Z", "equity": 9987.1, "pnl": -25.3 }
],
"trades": [
{
"fired_at": "2026-01-04T14:22:08.000Z",
"resolved_at": "2026-02-11T00:00:00.000Z",
"source": "insider",
"score": 84,
"category": "politics",
"entry_odds": 0.41,
"stake": 200,
"outcome": "WIN",
"pnl": 287.8,
"return_pct": 1.439
}
],
"assumptions": {
"signal_source": "...",
"payout_model": "...",
"stake_sizing": "...",
"fee_model": "...",
"slippage_fill": "...",
"survivorship": "...",
"resolution_timing": "...",
"effective_params": { "signal": "insider", "initial_capital": 10000 }
}
}
}effective_params echoes back every parameter the engine actually used, defaults
filled in. Log it with the result — a run you cannot reproduce is not evidence.
Reading a result without fooling yourself
hit_rateis a win rate, and it lies for the same reason. A run that filters to entry odds above 0.9 will show a high hit rate and a poorroi. Readroiandavg_return_per_tradetogether with it.slippage_bpsdefaults to 0. That default assumes you were filled at the alert price, which you were not. Re-run with a slippage assumption you would defend out loud, and look at how much of the edge survives — that gap is the actual finding.- Alerts only exist for wallets the detection stack scored at the time. The sample is the alerts we recorded, not every trade that ever happened.
- A three-year cap on the period is enforced. Longer spans return
400.
Next
- Copy trade safely — what to do with a signal that survives the replay.
- API overview — auth, limits and error shapes.
