Live

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.

POST /api/v1/backtest/run
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 }
}'
202 Accepted
{ "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.

FieldTypeRangeDefault
signalwhale, insider, allall
initial_capitalnumber> 0, ≤ 100,000,00010,000
period.from / period.toISO datetime or YYYY-MM-DDspan ≤ 1,095 daysall history
filters.min_score / max_scorenumber0–100none
filters.categorystring1–64 charsany
filters.min_odds / max_oddsnumber0–1none
filters.min_bet_valuenumber≥ 0none
stake_rule.kindflat, fractionalflat
stake_rule.sizenumber> 0, ≤ 1,000,000100
stake_rule.fractionnumber> 0, ≤ 10.05
stake_rule.max_fractionnumber> 0, ≤ 10.1
stake_rule.scale_by_scorebooleanfalse
fees.fee_bpsnumber0–2000200
fees.slippage_bpsnumber0–20000

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

TierRuns per monthConcurrent jobs
Free31
Terminaluncapped3
Terminal Prouncapped3

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.

GET /api/v1/backtest/{jobId}
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.

200 OK (trimmed)
{
"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_rate is 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 poor roi. Read roi and avg_return_per_trade together with it.
  • slippage_bps defaults 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

Live
Beta
···v0.1.0
CrowdIntel needs a newer browser

Your device is on iOS 15 or older. CrowdIntel uses features (color-mix, WebGL2, modern auth) that require iOS 16+ / Safari 16+.

iPhone 7 maxes out at iOS 15, so the site can't render here. Open CrowdIntel on a desktop browser or a newer phone.

crowdintel.xyz

◆ legacy browser fallback