Backtest a rule
MCP tool: backtest
Scores the rule over the window and reports what a flat stake per matching trade would have returned on markets that already settled. Saves nothing, so it is safe to call repeatedly while exploring.
Set holdout to true to score the earlier two thirds of the window and validate on the later third. A rule tuned until it looks good on all the data is fitted to that data; holdout is the cheap way to tell a real effect from a fitted one. Use it before deploying anything.
Body parameters
criteriaobjectrequiredThe rule: which trades to include. The same shape a saved strategy uses, so a rule you test is a rule you can deploy without translation.
platformsstring[]Venues to include: "polymarket", "kalshi". Omit for both, which is usually right.
categoriesstring[]Market categories: "sports", "politics", "crypto", "finance", "culture". Omit for all.
sportsLeaguesstring[]Leagues, e.g. ["NBA", "NFL"]. Only applies to sports markets. Max 20.
minAmountUsdnumberMinimum trade size in USD. Typical values are 10000 to 50000.
maxAmountUsdnumberMaximum trade size in USD. Must be at least minAmountUsd when both are set.
priceMinnumberMinimum entry price, 0 to 1. 0.75 and up is heavy favourites.
priceMaxnumberMaximum entry price, 0 to 1. 0.20 and below is longshots. Must be at least priceMin when both are set.
eventTypesstring[]How the trade changed the trader's position: "open", "add", "flip", "trim", "close", "unknown". Entries only is ["open", "add", "flip"], which is the usual choice. Trims and closes are exits and never settle, so they cannot be scored.
directionsstring[]Trade direction: "buy_yes", "buy_no", "sell_yes", "sell_no". Only buys are ever scored; sells never resolve.
keywordsstring[]Case-insensitive substrings matched against the market name. Any one matching is enough. Max 10.
stakenumberdefault: 100Hypothetical stake per trade in USD, 1 to 1000000. Return percentage is stake-invariant.
sinceDaysintegerdefault: 90Lookback window in days, 1 to 365.
holdoutbooleandefault: falseSplit the window and validate out of sample. The response becomes { inSample, outOfSample, holds, splitAt }.
Response
A backtest result counts every trade the rule matched in the window. settled trades are scored as wins or losses; open trades are counted but not scored. winRate and returnPct are null until something settles.
With holdout: true, data is { inSample, outOfSample, holds, splitAt } where inSample and outOfSample are each this shape, holds is true when the rule stayed positive out of sample, and splitAt is the ISO timestamp of the split.
curl -X POST https://api.rivo.markets/v1/backtest \
-H "Authorization: Bearer rivo_live_..." \
-H "Content-Type: application/json" \
-d '{
"criteria": {
"priceMax": 0.2,
"minAmountUsd": 10000,
"eventTypes": [
"open",
"add",
"flip"
]
},
"stake": 100
}'import requests
r = requests.post(
"https://api.rivo.markets/v1/backtest",
headers={"Authorization": "Bearer rivo_live_..."},
json={
"criteria": {
"priceMax": 0.2,
"minAmountUsd": 10000,
"eventTypes": ["open", "add", "flip"],
},
"stake": 100,
},
)
print(r.json()["data"])const res = await fetch("https://api.rivo.markets/v1/backtest", {
method: "POST",
headers: {
Authorization: "Bearer rivo_live_...",
"Content-Type": "application/json",
},
body: JSON.stringify({
criteria: {
priceMax: 0.2,
minAmountUsd: 10000,
eventTypes: ["open", "add", "flip"],
},
stake: 100,
}),
});
const { data } = await res.json();{
"success": true,
"data": {
"matched": 489,
"settled": 449,
"wins": 105,
"losses": 344,
"open": 40,
"winRate": 0.234,
"stakeUsd": 100,
"stakedUsd": 44900,
"pnlUsd": 25910,
"returnPct": 57.7,
"window": { "sinceDays": 90, "stake": 100 }
}
}