vike
Tools

run_backtest

Ship a backtest profile to a vike-datahub server and get the report back, optionally with a Rhai script injected into it.

Runs one backtest on the remote vike-datahub server named by --addr and returns the BacktestReport JSON. Read-only in the MCP sense — a backtest mutates nothing — and annotated readOnlyHint: true in tools_spec().

Input

FieldTypeRequiredMeaning
profilestringyesA backtest profile TOML: [data], [strategy], optional [engine]
scriptstringnoRhai source, injected as [strategy.params].src

tool_run_backtest reads profile, and a missing one is a tool error before anything connects. With script present it rewrites the profile through crate::cmd::backtest::inject_script_src: the profile is parsed as a toml::Value (a malformed one is a clean error, not a panic), [strategy] and [strategy.params] are created if absent, and src is set — existing params, the numeric knobs a [sweep] varies, are preserved, and any pre-existing inline src is overwritten.

An injected script only runs if strategy.name is rhai

src is read by exactly one arm of vike_backtest::harness::registry's strategy_by_name — the "rhai" arm, which is deliberately absent from the STRATEGIES roster because it is not resolvable with default params. Inject a script under name = "buy_hold" and the script is carried along as an unread param while the built-in strategy runs.

A call

The transport is newline-delimited JSON-RPC 2.0 — one message per line, no embedded newlines — so each of these goes on the wire as a single line.

{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"run_backtest","arguments":{
  "profile": "[data]\nvenue = \"binance\"\nsymbols = [\"BTCUSDT\"]\nkind = \"bar\"\ninterval = \"1d\"\nfrom = \"2024-01-01T00\"\nto = \"2024-06-01T00\"\n\n[strategy]\nname = \"buy_hold\"\n"
}}}

The profile crosses the wire verbatim. The server's own BacktestProfile::from_toml_str is the only parser in the loop, so a profile-shape error — a bad range, an unknown strategy.name, a [data] table naming a series the store does not hold — arrives from the server as profile parse/validate failed: … and reaches the agent as an isError result. One parser, one error source. Run profiles documents the file itself.

What comes back

{ "report": … }, wrapping the BacktestReport exactly as the server serialized it. Its fields are fixed by vike_analytics::report::BacktestReport; the numbers are your run's.

FieldMeaning
namethe profile's free-form name, or null
final_equityclosing equity
total_returnfractional return from the first to the last equity-curve point
n_trades · win_ratetrade count, and the fraction with positive PnL
sharpeannualized Sharpe of per-bar returns; 252 for daily bars
max_drawdownlargest peak-to-trough drop, a positive fraction of the peak
profit_factorgross profit over gross loss — null when non-finite, so a run with no losing trades but some profit reads as "no meaningful ratio" rather than a number
funding_paidnet perp funding cashflow, received-positive; 0.0 for a spot run
per_symbol_pnlmulti-symbol event runs only; empty otherwise
zero_tradepresent only when the run closed no trades and equity never moved — a ranked list of probable causes, not an empty stats block

That last field is the one worth reading first on a disappointing run: ZeroTradeReport::analyze composes it from diagnostic counters the run already carried, so "why did nothing happen" has an answer instead of a table of zeros. The report and metrics define every number.

A keyed datahub refuses this tool at connect

Every Run* verb is control scope on the server, because it compiles client-supplied Rhai — vike_datahub::server's required_scope says so, and calls that a design decision rather than an oversight. The MCP server connects with DatahubClient::connect, which against a datahub advertising the auth feature fails immediately with an actionable message rather than connecting and having every verb refused a round trip later. Against a key-less datahub — the default, and the usual localhost or SSH-tunnelled shape — nothing changes.

An unreachable server is likewise a clean tool error, never a panic: tool_run_backtest maps the connect failure to cannot connect to datahub at …. The pin that drives that shape, remote_run_tools_without_a_reachable_datahub_are_clean_errors, points at the sibling run tools — run_sweep, run_walk_forward and list_series — rather than at this one.

The prompt

Backtest this script on BTCUSDT daily bars for the first half of 2024 and tell me whether it traded at all.

Pair it with validate_strategy before the run and run_walk_forward after it. The human twin of this tool is vike-cli backtest, which ships the same TOML over the same verb.

On this page