# Backtest

> Scores a signal or one trader on settled history at a sizing you choose. You can add a holdout check, a sweep or a breakdown.

Source: https://rivo.markets/docs/reference/backtest

`POST /v2/backtest`

Scope: `read`

This route scores an idea on settled whale trades. It works out what you would have made by copying every matching trade and holding each one to settlement at the price paid. It returns scores, never individual trades.

Send exactly one of `signal` or `trader`. A signal is a set of conditions that whale buys must meet. A trader scores one trader's own buys.

> **Note:** A signal scores buys only. It has no `kind` because live signal bots match on category, not on kind of market. That way a backtest scores exactly what a bot built from the same signal would run.

- `holdout` scores the first two thirds of the window and then checks the result on the last third. `holds` is `true` when the last third also made money.
- `sweep` runs the signal once for each value of one field, so you can see where a threshold stops paying. Each value counts as one request.
- `breakdown` splits the score by category, by month or by price band.

Holdout, sweep and breakdown work on signals only. Settled history changes only as markets settle, so Rivo answers a repeated request from its cache for 10 minutes.

## Example request

curl:

```bash
curl -X POST "https://api.rivo.markets/v2/backtest" \
  -H "Authorization: Bearer rivo_live_..." \
  -H "Content-Type: application/json" \
  -d '{
  "signal": {
    "venue": [
      "kalshi"
    ],
    "category": [
      "sports"
    ],
    "priceMin": 0.3,
    "priceMax": 0.7,
    "minUsd": 5000
  },
  "sizing": {
    "usd": 100
  },
  "days": 90,
  "holdout": true
}'
```

Python:

```python
import requests

url = "https://api.rivo.markets/v2/backtest"
headers = {
    "Authorization": "Bearer rivo_live_...",
}
body = {
    "signal": {
        "venue": [
            "kalshi",
        ],
        "category": [
            "sports",
        ],
        "priceMin": 0.3,
        "priceMax": 0.7,
        "minUsd": 5000,
    },
    "sizing": {
        "usd": 100,
    },
    "days": 90,
    "holdout": True,
}

res = requests.post(url, headers=headers, json=body)
res.raise_for_status()
print(res.json())
```

TypeScript:

```typescript
const res = await fetch("https://api.rivo.markets/v2/backtest", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RIVO_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "signal": {
      "venue": [
        "kalshi"
      ],
      "category": [
        "sports"
      ],
      "priceMin": 0.3,
      "priceMax": 0.7,
      "minUsd": 5000
    },
    "sizing": {
      "usd": 100
    },
    "days": 90,
    "holdout": true
  }),
});
if (!res.ok) throw new Error(`Rivo API ${res.status}`);
const { data } = await res.json();
```

## Body

- `signal` (object, optional): The conditions a whale buy must meet. Send this or `trader`, not both. The API refuses unknown fields.
  - `venue` (string[], optional): Matches buys on these venues: `kalshi`, `polymarket`, `polymarket_us` or `gemini`. You can pass up to 50. Leave it out to match every venue.
  - `category` (string[], optional): Matches buys in these categories: `sports`, `politics`, `crypto`, `finance`, `culture`. You can pass up to 5. It matches the market's stored category, as saved signal bots do.
  - `minUsd` (number, optional): The smallest trade to match, in USD.
  - `maxUsd` (number, optional): The largest trade to match, in USD. It must be at least `minUsd`.
  - `priceMin` (number, optional): The lowest price paid to match, from 0 to 1.
  - `priceMax` (number, optional): The highest price paid to match, from 0 to 1. It must be at least `priceMin`.
  - `entry` (string, optional, default `all`): Which buys to match. `open` matches new positions only, `add` matches buys that add to a position, and `all` matches both.
  - `traders` (trader ref[], optional): Matches only these traders' buys. Pass 1 to 25 trader refs.
  - `keywords` (string[], optional): Matches markets whose name contains any of these words. Pass 1 to 10 words, each up to 40 characters.
- `trader` (trader ref, optional): Scores one trader's buys instead of a signal. Send this or `signal`, not both.
- `sizing` (object, optional, default `{ "usd": 100 }`): How much each trade stakes. Send exactly one of the two forms below.
  - `usd` (number, optional): A fixed stake for each trade in USD, above 0 and up to 1,000,000.
  - `percent` (number, optional): Stakes this percentage of the source trade's dollars, above 0 and up to 10,000. Send it together with `maxUsd`.
  - `maxUsd` (number, optional): The most any one trade stakes when you use `percent`, in USD, up to 1,000,000.
- `days` (integer, optional, default `90`): How many days back from now to score, from 1 to 365.
- `holdout` (boolean, optional): Set to `true` to score the first two thirds of the window and check the result on the last third. It works on signals only.
- `sweep` (object, optional): Runs the signal once for each value of one field. It works on signals only.
  - `field` (string, required): The field to sweep: `priceMin`, `priceMax`, `minUsd` or `maxUsd`.
  - `values` (number[], required): The values to try, from 2 to 20 of them. Prices run from 0 to 1.
- `breakdown` (string, optional): How to split the score: `category`, `month` or `price_band`. It works on signals only.

## Example response

```json
{
  "data": {
    "days": 90,
    "sizing": { "usd": 100 },
    "trades": 1711,
    "settled": 1662,
    "wins": 924,
    "losses": 738,
    "open": 48,
    "winRate": 0.556,
    "staked": 166200,
    "profit": 4164.71,
    "return": 0.0251,
    "curve": [
      { "week": "2026-07-13", "settled": 15, "profit": 1014.77, "cumulative": 1014.77 },
      { "week": "2026-07-20", "settled": 0, "profit": 0, "cumulative": 1014.77 },
      { "week": "2026-07-27", "settled": 7, "profit": 338.89, "cumulative": 1353.66 }
    ],
    "holdout": {
      "splitAt": "2026-09-15T02:28:05.331Z",
      "train": {
        "trades": 733,
        "settled": 717,
        "wins": 425,
        "losses": 292,
        "open": 16,
        "winRate": 0.5927,
        "staked": 71700,
        "profit": 4527.89,
        "return": 0.0632
      },
      "test": {
        "trades": 978,
        "settled": 945,
        "wins": 499,
        "losses": 446,
        "open": 32,
        "winRate": 0.528,
        "staked": 94500,
        "profit": -363.18,
        "return": -0.0038
      },
      "holds": false
    }
  }
}
```

## Response fields

- `data` (object): The score for your idea.
  - `days` (integer): The number of days scored.
  - `sizing` (object): The sizing this result used: `{ usd }` or `{ percent, maxUsd }`.
  - `trades` (integer): The number of trades the idea matched in the window, settled or not.
  - `settled` (integer): The number of matched trades that settled.
  - `wins` (integer): The number of settled trades that won.
  - `losses` (integer): The number of settled trades that lost.
  - `open` (integer): The number of trades still open. Rivo counts them but does not score them.
  - `winRate` (number or null): The share of settled trades that won, from 0 to 1. It is `null` until a trade settles.
  - `staked` (number): The dollars staked on settled trades at the sizing you gave.
  - `profit` (number): The profit in USD, with each trade held to settlement at the price paid.
  - `return` (number or null): The profit divided by the dollars staked. It is `null` when nothing was staked.
  - `curve` (object[]): The profit for each week, oldest first.
    - `week` (string): The Monday that starts the week, written as `YYYY-MM-DD`.
    - `settled` (integer): The number of trades that settled that week.
    - `profit` (number): The profit that week, in USD.
    - `cumulative` (number): The total profit up to the end of that week, in USD.
  - `holdout` (object): The holdout check, when you send `holdout: true`.
    - `splitAt` (string): The time where Rivo split the window, in ISO 8601 UTC.
    - `train` (object): The score for the first two thirds of the window.
      - `trades` (integer): The number of trades the idea matched in the window, settled or not.
      - `settled` (integer): The number of matched trades that settled.
      - `wins` (integer): The number of settled trades that won.
      - `losses` (integer): The number of settled trades that lost.
      - `open` (integer): The number of trades still open. Rivo counts them but does not score them.
      - `winRate` (number or null): The share of settled trades that won, from 0 to 1. It is `null` until a trade settles.
      - `staked` (number): The dollars staked on settled trades at the sizing you gave.
      - `profit` (number): The profit in USD, with each trade held to settlement at the price paid.
      - `return` (number or null): The profit divided by the dollars staked. It is `null` when nothing was staked.
    - `test` (object): The score for the last third of the window.
      - `trades` (integer): The number of trades the idea matched in the window, settled or not.
      - `settled` (integer): The number of matched trades that settled.
      - `wins` (integer): The number of settled trades that won.
      - `losses` (integer): The number of settled trades that lost.
      - `open` (integer): The number of trades still open. Rivo counts them but does not score them.
      - `winRate` (number or null): The share of settled trades that won, from 0 to 1. It is `null` until a trade settles.
      - `staked` (number): The dollars staked on settled trades at the sizing you gave.
      - `profit` (number): The profit in USD, with each trade held to settlement at the price paid.
      - `return` (number or null): The profit divided by the dollars staked. It is `null` when nothing was staked.
    - `holds` (boolean): This is `true` when the last third's return is above zero.
  - `sweep` (object): The sweep results, when you send `sweep`.
    - `field` (string): The field that was swept.
    - `results` (object[]): One score for each value. Each row has `value` next to the score fields.
      - `value` (number): The value Rivo tried.
      - `trades` (integer): The number of trades the idea matched in the window, settled or not.
      - `settled` (integer): The number of matched trades that settled.
      - `wins` (integer): The number of settled trades that won.
      - `losses` (integer): The number of settled trades that lost.
      - `open` (integer): The number of trades still open. Rivo counts them but does not score them.
      - `winRate` (number or null): The share of settled trades that won, from 0 to 1. It is `null` until a trade settles.
      - `staked` (number): The dollars staked on settled trades at the sizing you gave.
      - `profit` (number): The profit in USD, with each trade held to settlement at the price paid.
      - `return` (number or null): The profit divided by the dollars staked. It is `null` when nothing was staked.
  - `breakdown` (object): The breakdown, when you send `breakdown`.
    - `by` (string): How the score is split: `category`, `month` or `price_band`.
    - `rows` (object[]): One score for each bucket. Each row has `bucket` next to the score fields.
      - `bucket` (string): The bucket: the venue's own category name, a month written as `YYYY-MM`, or a price band such as `0.40-0.60`.
      - `trades` (integer): The number of trades the idea matched in the window, settled or not.
      - `settled` (integer): The number of matched trades that settled.
      - `wins` (integer): The number of settled trades that won.
      - `losses` (integer): The number of settled trades that lost.
      - `open` (integer): The number of trades still open. Rivo counts them but does not score them.
      - `winRate` (number or null): The share of settled trades that won, from 0 to 1. It is `null` until a trade settles.
      - `staked` (number): The dollars staked on settled trades at the sizing you gave.
      - `profit` (number): The profit in USD, with each trade held to settlement at the price paid.
      - `return` (number or null): The profit divided by the dollars staked. It is `null` when nothing was staked.
  - `byCategory` (object[]): The record by the venue's own category name. Only trader backtests return it.
    - `category` (string): The venue's name for the category.
    - `settled` (integer): The number of settled trades.
    - `wins` (integer): The number of wins.
    - `profit` (number): The profit in USD.
    - `return` (number or null): The profit divided by the dollars staked.

A signal scored here matches what a signal bot built from the same signal would record. Rivo scores signals on whale trades only. If a signal names `traders`, it counts those traders' buys of any size.

## Errors

| Code | When |
| --- | --- |
| `VALIDATION` | You sent both `signal` and `trader`, or neither. You sent holdout, sweep or breakdown with `trader`. A sweep price is above 1, a minimum is above its maximum, or a field is unknown. |
| `QUOTA_EXCEEDED` | The sweep's values would take the key past its per-minute limit. |
