Backtest
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.
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.
holdoutscores the first two thirds of the window and then checks the result on the last third.holdsistruewhen the last third also made money.sweepruns the signal once for each value of one field, so you can see where a threshold stops paying. Each value counts as one request.breakdownsplits 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 -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
}'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())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
signalobjectThe conditions a whale buy must meet. Send this or
trader, not both. The API refuses unknown fields.Show 9 signal fields
venuestring[]Matches buys on these venues:
kalshi,polymarket,polymarket_usorgemini. You can pass up to 50. Leave it out to match every venue.categorystring[]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.minUsdnumberThe smallest trade to match, in USD.
maxUsdnumberThe largest trade to match, in USD. It must be at least
minUsd.priceMinnumberThe lowest price paid to match, from 0 to 1.
priceMaxnumberThe highest price paid to match, from 0 to 1. It must be at least
priceMin.entrystringdefaultallWhich buys to match.
openmatches new positions only,addmatches buys that add to a position, andallmatches both.traderstrader ref[]Matches only these traders' buys. Pass 1 to 25 trader refs.
keywordsstring[]Matches markets whose name contains any of these words. Pass 1 to 10 words, each up to 40 characters.
tradertrader refScores one trader's buys instead of a signal. Send this or
signal, not both.sizingobjectdefault{ "usd": 100 }How much each trade stakes. Send exactly one of the two forms below.
Show 3 sizing fields
usdnumberA fixed stake for each trade in USD, above 0 and up to 1,000,000.
percentnumberStakes this percentage of the source trade's dollars, above 0 and up to 10,000. Send it together with
maxUsd.maxUsdnumberThe most any one trade stakes when you use
percent, in USD, up to 1,000,000.
daysintegerdefault90How many days back from now to score, from 1 to 365.
holdoutbooleanSet to
trueto score the first two thirds of the window and check the result on the last third. It works on signals only.sweepobjectRuns the signal once for each value of one field. It works on signals only.
Show 2 sweep fields
fieldstringrequiredThe field to sweep:
priceMin,priceMax,minUsdormaxUsd.valuesnumber[]requiredThe values to try, from 2 to 20 of them. Prices run from 0 to 1.
breakdownstringHow to split the score:
category,monthorprice_band. It works on signals only.
Example response
{
"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
dataobjectThe score for your idea.
Show 16 data fields
daysintegerThe number of days scored.
sizingobjectThe sizing this result used:
{ usd }or{ percent, maxUsd }.tradesintegerThe number of trades the idea matched in the window, settled or not.
settledintegerThe number of matched trades that settled.
winsintegerThe number of settled trades that won.
lossesintegerThe number of settled trades that lost.
openintegerThe number of trades still open. Rivo counts them but does not score them.
winRatenumber or nullThe share of settled trades that won, from 0 to 1. It is
nulluntil a trade settles.stakednumberThe dollars staked on settled trades at the sizing you gave.
profitnumberThe profit in USD, with each trade held to settlement at the price paid.
returnnumber or nullThe profit divided by the dollars staked. It is
nullwhen nothing was staked.curveobject[]The profit for each week, oldest first.
Show 4 curve fields
weekstringThe Monday that starts the week, written as
YYYY-MM-DD.settledintegerThe number of trades that settled that week.
profitnumberThe profit that week, in USD.
cumulativenumberThe total profit up to the end of that week, in USD.
holdoutobjectThe holdout check, when you send
holdout: true.Show 4 holdout fields
splitAtstringThe time where Rivo split the window, in ISO 8601 UTC.
trainobjectThe score for the first two thirds of the window.
Show 9 train fields
tradesintegerThe number of trades the idea matched in the window, settled or not.
settledintegerThe number of matched trades that settled.
winsintegerThe number of settled trades that won.
lossesintegerThe number of settled trades that lost.
openintegerThe number of trades still open. Rivo counts them but does not score them.
winRatenumber or nullThe share of settled trades that won, from 0 to 1. It is
nulluntil a trade settles.stakednumberThe dollars staked on settled trades at the sizing you gave.
profitnumberThe profit in USD, with each trade held to settlement at the price paid.
returnnumber or nullThe profit divided by the dollars staked. It is
nullwhen nothing was staked.
testobjectThe score for the last third of the window.
Show 9 test fields
tradesintegerThe number of trades the idea matched in the window, settled or not.
settledintegerThe number of matched trades that settled.
winsintegerThe number of settled trades that won.
lossesintegerThe number of settled trades that lost.
openintegerThe number of trades still open. Rivo counts them but does not score them.
winRatenumber or nullThe share of settled trades that won, from 0 to 1. It is
nulluntil a trade settles.stakednumberThe dollars staked on settled trades at the sizing you gave.
profitnumberThe profit in USD, with each trade held to settlement at the price paid.
returnnumber or nullThe profit divided by the dollars staked. It is
nullwhen nothing was staked.
holdsbooleanThis is
truewhen the last third's return is above zero.
sweepobjectThe sweep results, when you send
sweep.Show 2 sweep fields
fieldstringThe field that was swept.
resultsobject[]One score for each value. Each row has
valuenext to the score fields.Show 10 results fields
valuenumberThe value Rivo tried.
tradesintegerThe number of trades the idea matched in the window, settled or not.
settledintegerThe number of matched trades that settled.
winsintegerThe number of settled trades that won.
lossesintegerThe number of settled trades that lost.
openintegerThe number of trades still open. Rivo counts them but does not score them.
winRatenumber or nullThe share of settled trades that won, from 0 to 1. It is
nulluntil a trade settles.stakednumberThe dollars staked on settled trades at the sizing you gave.
profitnumberThe profit in USD, with each trade held to settlement at the price paid.
returnnumber or nullThe profit divided by the dollars staked. It is
nullwhen nothing was staked.
breakdownobjectThe breakdown, when you send
breakdown.Show 2 breakdown fields
bystringHow the score is split:
category,monthorprice_band.rowsobject[]One score for each bucket. Each row has
bucketnext to the score fields.Show 10 rows fields
bucketstringThe bucket: the venue's own category name, a month written as
YYYY-MM, or a price band such as0.40-0.60.tradesintegerThe number of trades the idea matched in the window, settled or not.
settledintegerThe number of matched trades that settled.
winsintegerThe number of settled trades that won.
lossesintegerThe number of settled trades that lost.
openintegerThe number of trades still open. Rivo counts them but does not score them.
winRatenumber or nullThe share of settled trades that won, from 0 to 1. It is
nulluntil a trade settles.stakednumberThe dollars staked on settled trades at the sizing you gave.
profitnumberThe profit in USD, with each trade held to settlement at the price paid.
returnnumber or nullThe profit divided by the dollars staked. It is
nullwhen nothing was staked.
byCategoryobject[]The record by the venue's own category name. Only trader backtests return it.
Show 5 byCategory fields
categorystringThe venue's name for the category.
settledintegerThe number of settled trades.
winsintegerThe number of wins.
profitnumberThe profit in USD.
returnnumber or nullThe 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. |