vike-cli backtest
Every flag, default, refusal and closed set of vike-cli backtest's 12 sub-verbs, rendered from the CLI's own exported surface.
Compute a strategy over history and judge what came back. One executor, six verbs that read and compare what it left behind, two that tell you what you can tune.
Every run is saved and addressable afterwards, so reporting, comparison and CI gating never re-run anything.
Start here
What each sub-verb is for
vike-cli backtest run compute a backtest — or a search, or a walk-forward
ls list what past runs left behind
show re-render ONE stored run, or export a document raw
path print one absolute path, so $(…) works
tag label a run, or give it a MARK a comparison can name
diff what differed in the INPUTS beside the OUTPUTS
gate judge a run against a mark → an EXIT CODE
params the tunable knobs a strategy declares (offline)
strategies the roster the compute SERVER can runrun is the only one that computes, and the only one that takes --addr — so it is the only one
that needs a server at all.
ls, show, path, tag, diff and gate read the run directory and nothing else, which is why
they work on runs made months ago, on another machine, by somebody else. params is local too, and
the CLI says so in its own refusal: reads the script or the strategy roster on this machine and
lists its knobs — no server, no store, no engine.
Why this verb exists
The heavy history never crosses the wire. You ship a profile — a small TOML document — to the machine that already holds the data, and get back a compact report. That is the whole trade: compute goes to the data, not the other way round.
It is also why --preset is read locally and merged into the profile before anything is sent.
A preset is a file on your disk; the server has no view of it, and giving it one would mean a
user_data/ directory on the far side with all the questions that raises.
What it looks like in use
# a complete backtest, no file anywhere
vike-cli backtest run --venue binance --symbol BTCUSDT --interval 1h \
--from 2025-01-01 --to 2026-01-01 \
--strategy momentum --param threshold=60 --param tp=900 \
--cash 100000 --fee 0.001 --slippage 0.0002
# the same command, saved so it can be committed and reviewed
vike-cli backtest run … --write-profile profiles/momentum_btc.toml
# a search — the profile's [paramscan] table is what makes it one
vike-cli backtest run --profile profiles/momentum_btc.toml \
--optimizer tpe --trials 400 --seed 7 --rank-by sharpe
# afterwards — no engine, no store, no socket
vike-cli backtest show <id> --metrics
vike-cli backtest diff <id> baseline/momentum
vike-cli backtest gate <id> --against baseline/momentum --fail-if 'sharpe:-5%'The grammar
A sub-verb is required. There is no bare vike-cli backtest — running it without one is an error, not a default action. 12 exist: run, ls, show, path, tag, diff, gate, params, strategies, templates, script-api, script-check.
A flag takes its value in either form — --flag value, --flag=value — and an = form splits on the FIRST = only, so a value may contain more. --help writes to stdout and exits 0, so it is pipeable and does not read as a failure in a script.
How much surface each sub-verb has
Flags each sub-verb accepts, of 54 that ship.
| sub-verb | flags accepted |
|---|---|
backtest run | 33 |
backtest ls | 7 |
backtest show | 9 |
backtest path | 2 |
backtest tag | 5 |
backtest diff | 6 |
backtest gate | 4 |
backtest params | 4 |
backtest strategies | 3 |
backtest templates | 2 |
backtest script-api | 1 |
backtest script-check | 2 |
The sub-verbs, one at a time
Each one: the line you type, commands to copy, then every flag it accepts. A flag accepted by several sub-verbs appears under each.
backtest run
vike-cli backtest run [--profile <run.toml>] [overrides]
Builds ONE profile TOML — from --profile if you name one, otherwise from an empty table — applies
the flags on the line as overrides, and computes it. The default route is remote: the profile
text is shipped to a compute daemon, so a line with no daemon up dies on the connect rung rather
than quietly running here. --local spawns the standalone engine on this machine instead, and the
two cannot be combined.
# A complete run with no file anywhere — every flag here is sugar for a profile key
vike-cli backtest run --venue binance --symbol BTCUSDT --interval 1h \
--from 2025-01-01 --to 2026-01-01 \
--strategy momentum --param threshold=60 --param tp=900 \
--cash 100000 --fee 0.001 --slippage 0.0002
# Shorter, while you are still deciding whether the idea works at all
vike-cli backtest run --venue binance --symbol BTCUSDT --interval 1d \
--from 2025-01-01 --to 2026-01-01 --strategy buy_hold --cash 10000
vike-cli backtest run --venue binance --symbol ETHUSDT --interval 4h \
--from 2025-06-01 --to 2026-01-01 --strategy momentum --cash 10000
# Several symbols — --symbol is the ONE flag that folds instead of last-wins
vike-cli backtest run --strategy buy_hold --symbol BTCUSDT,ETHUSDT \
--venue binance --interval 1h --kind bar \
--from 2026-01-01T00 --to 2026-02-01T00 --cash 10000
vike-cli backtest run --profile run.toml --symbol BTCUSDT,ETHUSDT --symbol SOLUSDT
# Ticks instead of bars
vike-cli backtest run --venue binance --symbol BTCUSDT --kind tick \
--from 2026-01-01 --to 2026-01-02 --strategy momentum --cash 10000
# Read it back before you spend a run — --show-effective PRINTS, then STOPS
vike-cli backtest run --profile profiles/momentum_btc.toml --show-effective
vike-cli backtest run --profile run.toml --fee 0.001 --slippage 0.0005 \
--set engine.fee_rate=0.001 --param size=1.5 --show-effective
vike-cli backtest run --venue binance --symbol BTCUSDT --strategy momentum \
--cash 10000 --show-effective
# Keep the command you liked — --write-profile writes, and then RUNS
vike-cli backtest run --profile profiles/momentum_btc.toml \
--write-profile profiles/momentum_btc.toml
vike-cli backtest run --venue binance --symbol BTCUSDT --interval 1h \
--from 2025-01-01 --to 2026-01-01 --strategy momentum --cash 100000 \
--write-profile profiles/first-try.toml
# Change one knob against a profile you trust
vike-cli backtest run --profile profiles/momentum_btc.toml --fee 0.002
vike-cli backtest run --profile profiles/momentum_btc.toml --cash 250000
vike-cli backtest run --profile profiles/momentum_btc.toml --param threshold=75
# Reach an engine knob that has no flag of its own
vike-cli backtest run --profile p.toml \
--set engine.cash_gate=true \
--set engine.maint_margin=0.005 \
--set engine.volume_limit=0.05
vike-cli backtest run --profile p.toml --set engine.sizer.kind=pct_equity \
--set engine.sizer.pct=0.02
vike-cli backtest run --profile p.toml --set name=btc-trend-1h-fees-on
# Layer a preset and a script — the preset merges FIRST, the script overwrites src
vike-cli backtest run --preset fast.toml --script s.rhai --write-profile out.toml
vike-cli backtest run --profile p.toml --script strategies/momentum.rhai
vike-cli backtest run --profile p.toml --preset presets/tight-stops.toml
# Search a [paramscan] grid — each method owns its own knobs
vike-cli backtest run --profile run.toml --optimizer grid --rank-by sharpe
vike-cli backtest run --profile run.toml --optimizer grid --rank-by max_dd
vike-cli backtest run --profile run.toml --optimizer tpe --trials 400 --seed 7
vike-cli backtest run --profile run.toml --optimizer tpe --trials 8 --seed 1
vike-cli backtest run --profile run.toml --optimizer euler --euler-depth 2
vike-cli backtest run --profile run.toml --optimizer euler --euler-depth 0
vike-cli backtest run --profile run.toml --optimizer genetic --seed 1
vike-cli backtest run --profile run.toml --optimizer tpe --trials 200 --seed 7 \
--rank-by multi
# Run the engine on THIS machine instead of shipping the profile
vike-cli backtest run --profile run.toml --local --store /data --engine /opt/backtest
vike-cli backtest run --profile p.toml --local --store ~/vike/store
vike-cli backtest run --profile run.toml --local --json
# Dial a specific daemon, or take the report JSON verbatim
vike-cli backtest run --profile run.toml --addr 127.0.0.1:7880
vike-cli backtest run --profile run.toml --addr 127.0.0.1:7881 --json
vike-cli backtest run --profile run.toml --json
# A preset plus a script, frozen to a profile you can commit
vike-cli backtest run --preset fast.toml --script s.rhai \
--write-profile out.toml--local and --addr are one choice, not two
Writing both is refused: --addr names a
remote daemon, so it cannot be combined with running the engine here. That choice then decides two
more flags — --store and --engine apply to --local ONLY, and typing either on the remote arm
is refused rather than ignored, because a remote run reads the server's store. And one shape has
no local answer at all: a profile declaring a [walkforward] table cannot run under --local,
because the standalone engine has one profile path that branches on [paramscan] and nothing else.
A search knob belongs to ONE method
--trials is tpe's, --euler-depth is euler's, --seed
is tpe's and genetic's. A knob typed under the wrong method is refused on both routes — including
under the grid you get by writing no --optimizer at all. These used to be discarded in silence,
which is the whole reason the refusal exists. genetic additionally REFUSES an absent --seed,
deliberately: a genetic search reports one sample of a distribution, and a seed nobody typed is a
constant the result silently depends on.
--param cannot be validated, on either side
[strategy.params] is an untyped TOML value,
so a typo'd knob name is a silent no-op — the run proceeds with the default you meant to change.
backtest params --strategy <name> is how you get the real list. --set is checked only on its
FIRST dotted segment (against name, data, engine, strategy, risk, paramscan, sweep, walkforward);
everything after it is the far side's unknown-field refusal.
Three values have no default anywhere, and --cash is one of them
Omit it and the far side
refuses by name, because a starting balance is a modelling input with no defensible default. --from
and --to are the other two. Meanwhile --fee and --slippage default to 0.0 and the
participation cap, cash gate and margin model are all off unless you turn them on with --set —
so a default run can spend money it does not have. --show-effective is how you check before you
believe a number.
| flag | sample | scope | what it does |
|---|---|---|---|
--addr host:port | 127.0.0.1:7880 | remote | The COMPUTE daemon to dial (vike-backend backtest --addr, NOT the datahub). Refused beside --local; unwritten, it falls through config.backtest_addr to 127.0.0.1:7880. |
--cash N | 10000 | any | A run-only sugar flag (SUGAR row engine.cash, Shape::Scalar) included here because it has THREE different answers across this plane. ACCEPTED on run. On params it is on PARAMS_REFUSED and gets the run-flag sentence. On every other reading sub-verb it is not a known token at all and falls into the generic unknown-option arm. |
--decide mode | sequential | any | WHEN a multi-symbol step is decided. simultaneous makes the run's answer a property of the instrument SET rather than of the order data.symbols was typed in, and IMPLIES engine.cash_gate. Sugar for --set engine.decide=…, reaching the same key through the same applier. |
--engine PATH | /opt/backtest | local | Name the standalone engine outright instead of searching for it. Local-arm only; never probed for existence. |
--euler-depth N | 2 | any | euler's successive-halving depth. Forwarded as a token; range-checked (0..=16) by the engine, never clamped silently. |
--fee RATE | 0.001 | any | Sugar for engine.fee_rate through parse_scalar; a non-numeric value becomes a TOML string and is refused far-side, not here. |
--from DATE | 2026-01-01T00 | any | Sugar for data.from. Shape::Str is REQUIRED not a preference: a bare epoch-ms stays a string, so --from 0 works while --set data.from=0 is a far-side type error. |
--interval 1h | 1h | any | Sugar for data.interval, plus the ONE grammar check inside the match body: the trimmed value must parse through vike_model::time::interval_ms. |
--json | — | any | Print a machine-readable document instead of the human rendering. On run that is the server's report JSON verbatim (and on --local it is forwarded to the spawned engine); each reading verb emits its own — gate's carries the verdict, one object per criterion, and available_keys when a criterion went unevaluated. ⚠ ACCEPTED on path and read nowhere: that verb prints one line so $(…) works and has no document form, so the flag is inert there rather than refused. On script-check the document is the VERDICT (ok, error, script, params) and the rung is beside it; on templates it is the starter roster, one starter, or the path a starter was saved to. |
--kind bar|tick | bar | any | Sugar for data.kind, plus the second in-match special case: the trimmed value goes through one_of against DATA_KINDS. It is the key implied_defaults always supplies. |
--local | — | any | Run on THIS machine by spawning the standalone backtest engine instead of shipping the profile to a compute daemon. |
--optimizer method | grid | any | The parameter-search METHOD (grid, euler, tpe, genetic). Spelling-checked locally against the protocol roster; meaning resolved by the engine or the compute server. |
--param k=v | size=1.5 | any | Sugar for --set strategy.params.<k>=<v>, value typed by parse_scalar. NO schema validation is possible on either side: StrategyCfg::params is an untyped toml::Value, so a typo is a silent no-op. |
--preset <p.toml> | fast.toml | any | A flat preset TOML merged into [strategy.params], applied BEFORE --script. Repeating it silently LAST-WINS. |
--profile <run.toml> | run.toml | any | Optional BASE profile file; flags override it. Repeating it silently LAST-WINS (plain Option assignment, no test pins it). |
--rank-by METRIC | sharpe | any | Which metric orders a parameter search's rows (sharpe, return, max_dd, equity, multi). A VALID value is IGNORED, not an error, on a profile with no grid. |
--script <s.rhai> | s.rhai | any | Injects the .rhai file's source into strategy.params.src (overwriting any existing src) and implies strategy.name=rhai. On the params sub-verb the same spelling names the script whose knobs are listed, and on script-check the script to COMPILE — three owners, one meaning: a .rhai file on this machine. |
--seed u64 | 1 | any | Reproducibility seed for the two stochastic searchers. tpe defaults it to 0; genetic REFUSES an absent one. |
--set key=value | engine.fee_rate=0.001 | any | Repeatable universal channel; value typed by parse_scalar. ONLY the FIRST segment is checked (against PROFILE_TOP_LEVEL_KEYS) — the rest is the far side's deny_unknown_fields, on exit rung 1. |
--show-effective | — | any | Prints the resolved profile with each override's origin as TOML COMMENTS and STOPS — exit 0, no dial, no engine, no store. Rejects an inline value. |
--slippage RATE | 0.0005 | any | Sugar for engine.slippage through parse_scalar; same typing rule as --fee and --cash. |
--store DIR | /data | local | The hist-store root the LOCAL engine reads. Local-arm only: a remote run reads the SERVER's store. |
--strategy NAME | buy_hold | any | Sugar for strategy.name, forwarded UNVALIDATED (the roster lives in vike-backtest and includes user strategies this crate cannot see). On the params sub-verb the same spelling names the roster entry whose knobs are listed. |
--symbol SYM[,SYM] | BTCUSDT,ETHUSDT | any | The ONE row that does not push per occurrence: every occurrence comma-splits and accumulates, and a SINGLE data.symbols array Override is pushed after the loop, in argv order. |
--to DATE | 2026-02-01T00 | any | Sugar for data.to; same Shape::Str string-preservation rule as --from. No CLI-side date validation of any kind. |
--trials N | 8 | any | tpe's trial budget. Forwarded as the token you typed; the engine or the server parses and refuses it. |
--venue NAME | binance | any | Sugar for data.venue, value trimmed and written as a TOML string. No roster check here — a venue name is forwarded unvalidated. |
--write-profile <out.toml> | out.toml | any | Writes the fully-resolved profile (after --preset and --script rewrites) and then RUNS. Refuses an existing path outright. Repeating it silently LAST-WINS. |
--explain-data | — | any | Print exactly what the run WILL read — every series, the resolved range, the store root and the rung that chose it, with row counts — and stop without computing. |
--require-coverage | — | any | Refuse a run whose window the store does not cover, naming the missing spans per series. The failure it ends: a window with a complete trade tape and no book runs to completion and REPORTS FILLS. |
--max-gap SPAN | 1d | any | How large an interior hole --require-coverage tolerates. A PER-SPAN tolerance, not a total: a shortfall that is the whole series ignores it under every value. |
--on-gap DISPOSITION | warn | any | What an armed coverage gate DOES about an uncovered window: refuse stops the run, warn logs the findings and runs unchanged, run leaves the verdict in the document only. |
--universe MODE | strict | any | What to do about a member whose tape does not span the window: declared takes the list verbatim, covered names the short members and runs anyway, strict refuses. NONE of the three drops a member. |
Which knob belongs to which search method — --euler-depth => euler, --trials => tpe, --seed => tpe, genetic.
Parses, then refuses: --list-params reach this sub-verb and are turned down by name — see Accepted nowhere yet for what each one is waiting on.
backtest ls
vike-cli backtest ls [--where EXPR] [--sort FIELD[:dir]] [--cols a,b,c] [--limit N] [--out FILE]
Lists runs already on disk. It takes no --addr and no selector, so there is no remote form and
nothing is recomputed. A bare ls prints four manifest fields and opens no report.json at all —
the moment you name a metric in --cols, --sort or --where, every run's report is read.
# Start here — the flag list, then the default table
vike-cli backtest ls --help # or -h: the USAGE block on stdout, exit 0
vike-cli backtest ls # the DEFAULT_COLS four; no report.json opened
# Choose the columns — a metric column opens every report.json
vike-cli backtest ls --cols run_id,kind,started_at,report # that default, spelled out
vike-cli backtest ls --cols run_id,kind,sharpe
vike-cli backtest ls --cols run_id,kind,sharpe,max_drawdown
vike-cli backtest ls --cols run_id,started_at,config.name
# Narrow, order, truncate — QUOTE the expression, > and < are the shell's
vike-cli backtest ls --where 'sharpe>1.2' --sort sharpe --limit 20
vike-cli backtest ls --where 'kind=backtest,sharpe>1' # comma is AND
vike-cli backtest ls --where 'kind=search' --sort sharpe:desc --limit 10
vike-cli backtest ls --where 'config.name~momentum' --sort started_at:desc
vike-cli backtest ls --where 'sharpe<0' --cols run_id,config.name,sharpe
# The worst runs, not the best — ascending is how you find what broke
vike-cli backtest ls --sort sharpe:asc --limit 10 --cols run_id,config.name,sharpe
vike-cli backtest ls --sort max_drawdown:desc --limit 10 \
--cols run_id,config.name,max_drawdown
# What ran today, and what it was called
vike-cli backtest ls --sort started_at:desc --limit 20 \
--cols run_id,kind,started_at,config.name
vike-cli backtest ls --where 'kind=walkforward' --sort started_at:desc --limit 5
# Write the rendered table to a file — an existing path is OVERWRITTEN
vike-cli backtest ls --sort sharpe:desc --limit 20 --out runs.txt
vike-cli backtest ls --where 'sharpe>1' --sort sharpe:desc --limit 50 --out shortlist.txt
# A leaderboard in one line: filter, order, pick the columns, cap the rows
vike-cli backtest ls --where 'kind=backtest,sharpe>1' --sort sharpe:desc \
--cols run_id,kind,sharpe --limit 20--where — the grammar, in full. One predicate in place of a wall of typed min/max flags, and
its field vocabulary is shared with --sort and --cols, so the three can never disagree about a
name:
EXPR := TERM (',' TERM)* # comma is AND. There is no OR and no grouping.
TERM := FIELD OP VALUE
OP := '>=' | '<=' | '!=' | '~' | '=' | '>' | '<' # two-character operators match FIRSTHow a comparison is decided — and what a missing field does
A comparison is numeric when both sides parse as a number, and a case-insensitive string
comparison otherwise; ~ is always a substring test. A missing field fails every operator except
!= — a null is not less than anything, and letting sharpe>1 admit a run that computed no
Sharpe would put an empty row at the top of your leaderboard. So a filter on a metric silently
narrows the table to runs that HAVE it, which is usually what you want and is worth knowing when a
count surprises you.
Quote the --where expression
> and < belong to the shell. Unquoted,
--where kind=backtest,sharpe>1 never reaches the CLI intact: the shell takes >1 as a redirection
and creates a file called 1, handing ls a term with its operator stripped off. It refuses rather
than filtering on something you did not write — but single-quote the whole expression and the
problem does not arise.
A misspelled --cols name is a column of dashes, not a refusal
The field vocabulary has an
open tail: the manifest fields resolve first, detail.<dotted.path> reaches into the manifest
detail, and any other name is read as a top-level key of report.json — which is a legal
request for a key that may simply be absent. So --cols cannot tell a typo from a missing key, and
renders -. The same typo in --sort is refused, because it has every row in hand and can see
that none carries the key. A quiet column of dashes is a typo; an empty table is an empty store.
--where has no OR and no grouping
The grammar is FIELD OP VALUE joined by commas, and the
comma means AND. Operators are >= <= != ~ = > <, with the two-character ones matched
first. A | anywhere is refused with the advice to run ls twice for a disjunction. --limit is
parsed at render time rather than in the flag loop, so a negative or non-integer value still lands
as a usage refusal rather than a silent zero.
--sort's typo check sees rows --where already removed
The "no run carries that field" check runs over the UNFILTERED scan, so a field only your excluded runs carry passes the check and then sorts every surviving row into the missing-key bucket at the end. An empty store is excused before that check runs, deliberately — otherwise a fresh project would exit 2 accusing you of a typo you had not made.
| flag | sample | scope | what it does |
|---|---|---|---|
--cols a,b,c | run_id,kind,sharpe | any | ls only. Split on ',', each element trimmed, EMPTY elements dropped — so --cols 'a,,b' is two columns, not three, and --cols , is refused as no column names. An unknown column name is NOT refused: it renders as a column of '-'. |
--help | — | any | -h or --help, matched inside the shared reading flag loop. Also FALLS THROUGH — no refusal array names it. It short-circuits through the Err channel with args::HELP_SENTINEL ("help requested"), which args::exit_for_parse_error turns back into the USAGE on stdout and exit 0. |
--json | — | any | Print a machine-readable document instead of the human rendering. On run that is the server's report JSON verbatim (and on --local it is forwarded to the spawned engine); each reading verb emits its own — gate's carries the verdict, one object per criterion, and available_keys when a criterion went unevaluated. ⚠ ACCEPTED on path and read nowhere: that verb prints one line so $(…) works and has no document form, so the flag is inert there rather than refused. On script-check the document is the VERDICT (ok, error, script, params) and the rung is beside it; on templates it is the starter roster, one starter, or the path a starter was saved to. |
--limit N | 20 | any | ls only. Parsed as usize at RENDER time (not in the flag loop), then runs.truncate(n). A negative or non-integer value is a usage refusal. |
--out FILE | runs.txt | any | A DOUBLE-OWNED FLAG. Owners: ls (writes the table or the --json document) and show (writes the rendered run, or the raw --export document). Refused on every other reading sub-verb by a NAMED rule, not by a roster: gate and diff are deliberately excluded because their single clean stream is what > already handles. The write itself is std::fs::write(file, format!("\{text}\\n")) — note the appended newline — and an existing path is OVERWRITTEN (no refusal). ⚠ That last property is why templates does NOT reuse this flag even though it is the one reading verb that creates a file: --write-strategy refuses an existing path, and one flag may not carry two clobber policies. |
--sort FIELD[:asc|:desc] | sharpe:desc | any | ls only. Direction defaults by TYPE of the surviving rows: numeric descends, text ascends; runs missing the key sort LAST in both directions. The typo check runs over the UNFILTERED scan, and an EMPTY store is excused before the fold (otherwise all(Missing) would be vacuously true and a fresh project would exit 2 accusing a typo). |
--where EXPR | kind=backtest,sharpe>1 | any | ls only. Grammar: EXPR := TERM ("," TERM)* ; TERM := FIELD OP VALUE ; OP := >= | <= | != | ~ | = | > | < (two-character operators tried FIRST). Comma is AND; there is no OR and no grouping. FIELD resolves through the same ls::field_of vocabulary --sort/--cols use. |
The columns a bare ls prints — run_id, kind, started_at, report.
The operators --where accepts — >=, <=, !=, ~, =, >, <.
The field names --cols, --sort and --where share — run_id, kind, produced_by, started_at, finished_at, git_sha, config.path, config.name, report, detail.<dotted.path>, <any other name> = a TOP-LEVEL key of report.json.
Parses, then refuses: --attribution, --breakdown, --drawdowns reach this sub-verb and are turned down by name — see Accepted nowhere yet for what each one is waiting on.
backtest show
vike-cli backtest show <selector> [--metrics|--trades|--config] [--export trades|equity] [--out FILE]
Re-renders one run that is already on disk: it opens the directory a selector names and prints it,
with no server, no engine and no store lookup in the path. The rule that catches people is that the
three section flags narrow — a bare show prints metrics, trades and config, and naming one of
them hides the other two.
# Name the run — five selector forms, all accepted here
vike-cli backtest show 1789213143-8821-01 # the full run id
vike-cli backtest show 1789213 # a unique id PREFIX is enough
vike-cli backtest show @last # the newest run, any kind
vike-cli backtest show @last:search # the newest run of ONE kind
vike-cli backtest show @baseline/momentum # a mark set by `backtest tag --as`
# Narrow to a section — naming one HIDES the others, it does not add one
vike-cli backtest show @last --metrics # report.json's metrics, alone
vike-cli backtest show @last --trades # the stored trade ledger, alone
vike-cli backtest show @last --config # profile path + resolved config.toml
vike-cli backtest show @last --metrics --config # two sections; trades is now hidden
# Audit what actually produced a number — was the cost model even on?
vike-cli backtest show @baseline/momentum --config
vike-cli backtest show 1789213 --config
vike-cli backtest show @last:search --config
# Export the raw stored document instead of a rendering
vike-cli backtest show @last --export trades # trades.json, pretty-printed
vike-cli backtest show @last --export equity # series.json: the equity curve
vike-cli backtest show @last --export=equity # same value, --flag=value form
# Send it to a file — with --export, the EXPORT is what lands there
vike-cli backtest show @last --out run.txt
vike-cli backtest show @last --metrics --out metrics.txt
vike-cli backtest show @last --export equity --out equity.json
vike-cli backtest show @last --export trades --out trades.json
vike-cli backtest show --help
# Write the rendering to a file instead of the terminal
vike-cli backtest show @last --out runs.txt
vike-cli backtest show @last --export equity --out runs.txt # the equity series, redirectedThe section flags narrow, and they do nothing under --export
show computes
all = not metrics and not trades and not config, so naming any one of them turns the other two
off. Under --export they shape nothing at all: the export replaces the rendering outright. Each
takes no value, so --metrics=1 is refused rather than read as truthy.
--export fills is refused by name, and that is a data fact rather than a missing feature
Fills are not stored at any size: a backtest computes them, consumes them into report.json's
scalars and drops them. Only the trade LEDGER and the equity curve survive, which is why trades
and equity are the whole set. A comma list such as trades,equity is refused too — one export is
one document and --out is one file, so run show twice.
Four renderers parse and then refuse, and two of them are missing DATA rather than a renderer
--html needs a tearsheet renderer nothing in this tree has built. --drawdowns needs only the
table — the equity curve it would read is already in series.json, and --export equity hands you
that curve today. --breakdown needs per-bar returns, and the stored curve is DECIMATED once a run
passes the sample cap, so a period breakdown computed off it would disagree with report.json's own
numbers. --attribution needs a per-source attribution the engine does not compute at all.
A run flag here is a bare unknown option, not a sentence
The sugar flags belong to
backtest run; on show they are not known tokens at all, so --cash gets unknown option '--cash' and exit 2 with nothing telling you where the flag actually lives. params is the one
reading sub-verb that holds a named roster of them and answers with the full sentence instead.
| flag | sample | scope | what it does |
|---|---|---|---|
--config | — | any | show only. Renders the run's RunConfig (the profile path as the operator spelled it, plus its name) and the resolved config.toml when the producer wrote one — saying so when it is absent. |
--export trades|equity | trades | any | show only. REPLACES the rendering entirely with the raw stored document (pretty-printed JSON), and composes with --out and with nothing else. The value is trimmed. A COMMA LIST is refused by name. fills is refused by NAME with its own sentence rather than by refusing the flag. |
--help | — | any | -h or --help, matched inside the shared reading flag loop. Also FALLS THROUGH — no refusal array names it. It short-circuits through the Err channel with args::HELP_SENTINEL ("help requested"), which args::exit_for_parse_error turns back into the USAGE on stdout and exit 0. |
--html | — | local | show only: this run's whole tearsheet as one standalone HTML page, to --out or stdout. ⚠ It was a member of the unbuilt_renderers roster until the vike-report edge landed, refused with a sentence blaming a missing renderer — vike_report::render_html was never missing and LiveTearsheet::from_report names show.rs by path as the caller it was built for; what was missing was the dependency. The metrics table carries report.json through value by value, but the charts, their captions and the monthly-returns table are FOLDED from series.json as stored, so on a decimated run the drawn low can read shallower than the max_drawdown printed above it. That asymmetry is why this ships where --breakdown stays refused: a picture of a sample is visibly one, while a recomputed period table would quietly disagree with report.json's own numbers. |
--json | — | any | Print a machine-readable document instead of the human rendering. On run that is the server's report JSON verbatim (and on --local it is forwarded to the spawned engine); each reading verb emits its own — gate's carries the verdict, one object per criterion, and available_keys when a criterion went unevaluated. ⚠ ACCEPTED on path and read nowhere: that verb prints one line so $(…) works and has no document form, so the flag is inert there rather than refused. On script-check the document is the VERDICT (ok, error, script, params) and the rung is beside it; on templates it is the starter roster, one starter, or the path a starter was saved to. |
--metrics | — | any | show only. Renders the report.json metrics section. Has no effect under --json (show_json takes only a.trades) and none under --export (the export replaces the rendering entirely). |
--metrics-list | — | any | show only, and the ONE flag on this verb that is not about a run: it prints the METRIC CATALOG (every id with where it is stored and what it measures, plus the declared-absent rows) and stops. Takes no selector and reads no runs directory. Composes with --out; refused beside --metrics/--trades/--config/--export and beside --json. |
--out FILE | runs.txt | any | A DOUBLE-OWNED FLAG. Owners: ls (writes the table or the --json document) and show (writes the rendered run, or the raw --export document). Refused on every other reading sub-verb by a NAMED rule, not by a roster: gate and diff are deliberately excluded because their single clean stream is what > already handles. The write itself is std::fs::write(file, format!("\{text}\\n")) — note the appended newline — and an existing path is OVERWRITTEN (no refusal). ⚠ That last property is why templates does NOT reuse this flag even though it is the one reading verb that creates a file: --write-strategy refuses an existing path, and one flag may not carry two clobber policies. |
--trades | — | any | THE OTHER DOUBLE-OWNED FLAG. Owners: show (renders trades.json as a section) and diff (adds a third diff section over both runs' trades.json). Deliberately kept OUT of both the show_only and diff_only rosters and refused on the remaining six by its own named rule with its own sentence. |
The ways to name a run — <run-id>, <unique-prefix>, @last, @last:<kind>, @<mark>.
The forms the REFUSAL lists when you name none — <run-id>, <unique-prefix>, @last, @last:<kind>.
⚠ HAND-WRITTEN, and it DISAGREES with FORMS. This four-name list is a literal inside a format! template in another crate-module; nothing derives it from FORMS and no test compares the two.
What --export can take — trades, equity.
⚠ fills is recognised and refused: --export fills is not available — fills are not stored at ANY size. A backtest computes them, consumes them into report.json's scalars and drops them; only the trade LEDGER and the equity curve survive. --export trades and --export equity both work today.
Parses, then refuses: --attribution, --breakdown, --drawdowns reach this sub-verb and are turned down by name — see Accepted nowhere yet for what each one is waiting on.
backtest path
vike-cli backtest path <selector> [FILE]
Prints ONE absolute line — the stored run's directory, or a file inside it — so it composes with
$(…), cd and jq with no parsing at all. It takes a selector positionally and an optional FILE
after it, and the rule people get wrong is that FILE is sugar over an open set: report and
manifest are spelled out, everything else is joined to the directory verbatim.
# One absolute line — the run's directory, or one file inside it
vike-cli backtest path @last # the newest run's directory
vike-cli backtest path @last report # sugar for report.json in that dir
vike-cli backtest path @last manifest.json # the other spelled-out name
vike-cli backtest path @last series.json # any other name joins VERBATIM
vike-cli backtest path @last trades.json
# Naming the run — the five selector forms this verb takes
vike-cli backtest path 1789213143-8821-01 # the full run id
vike-cli backtest path 1789213 # a unique short prefix
vike-cli backtest path @last:search # newest run of that kind
vike-cli backtest path @baseline/momentum report
# Which is the point of it — the line drops straight into another tool
jq .metrics.sharpe "$(vike-cli backtest path @last report)"
jq -r '.metrics | to_entries[] | "\(.key)\t\(.value)"' "$(vike-cli backtest path @last report)"
cd "$(vike-cli backtest path @last)"
ls -la "$(vike-cli backtest path @last)"
# Compare two runs with tools that know nothing about vike
diff <(jq -S .metrics "$(vike-cli backtest path @baseline/momentum report)") \
<(jq -S .metrics "$(vike-cli backtest path @last report)")
# Copy a run's artefacts somewhere else
cp "$(vike-cli backtest path @last report)" ./report-under-review.json
cp -r "$(vike-cli backtest path @baseline/momentum)" ./baseline-snapshot
# The only flag this verb has
vike-cli backtest path --helpFILE is sugar over an open set, not a vocabulary
report → report.json and manifest →
manifest.json are the only two spellings this verb knows; every other name is joined to the
directory exactly as typed. So path @last sereis.json prints a path happily — the typo surfaces
only when something tries to open it. The upside is that a run artefact added next month needs no
change here.
It prints a path; it does not promise the file exists
The run directory is resolved and the
name is appended, with no stat in the way. That is what makes it safe inside $(…) — a probe
would make the line's success depend on the artefact rather than on the run — but it means the
consumer is what reports a missing file, in its own words.
Every other flag of the plane is refused here, by name
path takes --help and nothing else,
so a pasted --cols or --metrics answers with the sub-verb that owns it rather than with
unknown option. There is no --out: the whole output is one line the shell can already redirect.
| flag | sample | scope | what it does |
|---|---|---|---|
--help | — | any | -h or --help, matched inside the shared reading flag loop. Also FALLS THROUGH — no refusal array names it. It short-circuits through the Err channel with args::HELP_SENTINEL ("help requested"), which args::exit_for_parse_error turns back into the USAGE on stdout and exit 0. |
--json | — | any | Print a machine-readable document instead of the human rendering. On run that is the server's report JSON verbatim (and on --local it is forwarded to the spawned engine); each reading verb emits its own — gate's carries the verdict, one object per criterion, and available_keys when a criterion went unevaluated. ⚠ ACCEPTED on path and read nowhere: that verb prints one line so $(…) works and has no document form, so the flag is inert there rather than refused. On script-check the document is the VERDICT (ok, error, script, params) and the rung is beside it; on templates it is the starter roster, one starter, or the path a starter was saved to. |
The ways to name a run — <run-id>, <unique-prefix>, @last, @last:<kind>, @<mark>.
File names spelled out — anything else joins verbatim — manifest, manifest.json, report, report.json.
Parses, then refuses: --attribution, --breakdown, --drawdowns reach this sub-verb and are turned down by name — see Accepted nowhere yet for what each one is waiting on.
backtest tag
vike-cli backtest tag <selector> [--add TAG] [--as NAME] [--note TEXT]
The one writing verb in this plane: it takes a stored run and puts labels, a note or a mark onto it. Nothing happens by default — with no flags it writes nothing — and the three flags write to two different places on disk, which is why a mark survives things a label does not.
# Label a run so you can find it again — --add is the ONE repeatable flag here
vike-cli backtest tag @last --add ci # one label
vike-cli backtest tag @last --add ci --add fee-fix # two labels, one write
vike-cli backtest tag 1789213 --add ci # a unique short prefix names the run
vike-cli backtest tag @last:search --add grid-sweep
# Give a run a MARK — the stable name a gate can be pointed at
vike-cli backtest tag @last --as baseline/momentum # gate --against @baseline/momentum
vike-cli backtest tag 1789213143-8821-01 --as prod # the mark outlives the run id
vike-cli backtest tag @last --as baseline/btc-1h
vike-cli backtest tag @last --as candidate
# Say why, and let the reason travel with the mark
vike-cli backtest tag @last --note 'fee model fix'
vike-cli backtest tag @last --as baseline/momentum --note 'pre-refactor reference'
vike-cli backtest tag @last --note 'cash_gate on for the first time'
# Move a baseline you have accepted — re-pointing is the whole operation
vike-cli backtest tag @last --as baseline/momentum --note 'accepted: fee model fix'
# All three in one write
vike-cli backtest tag @last:search --add ci --as baseline/momentum \
--note 'fee model fix'
vike-cli backtest tag @last --add release --as prod --note 'shipped 2026-02-01'
vike-cli backtest tag --help
# One command that marks a run AND records why
vike-cli backtest tag @last --as baseline/momentum --note 'fee model fix'Three writes, three homes — and none of them happen by default
--add pushes labels into the
run's own meta.json sidecar. --as writes a MARK into a file that is a sibling of the runs root,
not inside the run — which is exactly why a mark can outlive the run directory and why gate --against @baseline/<name> keeps working after the id is forgotten. --note is APPENDED, never
replacing what is there.
With --as, the note is deliberately written twice
The same text goes into the run's sidecar and into the mark. That is not a bug to tidy: the reason has to travel with the pointer, because the pointer is what a pipeline names six months later.
A mark name becomes a file path on every platform this ships to
So it is validated at parse
time rather than at write time. Keep to the baseline/momentum shape — a name with an empty
segment, leading or trailing whitespace, or a path escape is refused before anything is written.
--add is the only repeatable flag in the whole reading loop
Every other value-taking flag
here silently last-wins, so writing --as twice keeps the second and writing --note twice keeps
the second — while --add ci --add fee-fix genuinely means two labels.
| flag | sample | scope | what it does |
|---|---|---|---|
--add TAG | ci | any | tag only, and THE ONLY REPEATABLE FLAG IN THE READING FLAG LOOP — --add ci --add fee-fix is two labels, pushed onto ReadArgs::add. Writes into the run's own META_FILE (meta.json) sidecar via vike_model::runs::add_tags. |
--as NAME | baseline/momentum | any | tag only. Sets a MARK — the stable second operand gate --against takes — in a file that is a SIBLING of the runs root (vike_model::state_path::MARKS_SUBDIR). Validated at PARSE time by vike_model::runs::valid_mark_name, whose message is prefixed with --as . |
--help | — | any | -h or --help, matched inside the shared reading flag loop. Also FALLS THROUGH — no refusal array names it. It short-circuits through the Err channel with args::HELP_SENTINEL ("help requested"), which args::exit_for_parse_error turns back into the USAGE on stdout and exit 0. |
--json | — | any | Print a machine-readable document instead of the human rendering. On run that is the server's report JSON verbatim (and on --local it is forwarded to the spawned engine); each reading verb emits its own — gate's carries the verdict, one object per criterion, and available_keys when a criterion went unevaluated. ⚠ ACCEPTED on path and read nowhere: that verb prints one line so $(…) works and has no document form, so the flag is inert there rather than refused. On script-check the document is the VERDICT (ok, error, script, params) and the rung is beside it; on templates it is the starter roster, one starter, or the path a starter was saved to. |
--note TEXT | fee model fix | any | tag only. APPENDED, never replacing. ⚠ When --as is also given the SAME note is written TWICE on disk — into the run's sidecar and into the mark — deliberately, so the reason travels with the pointer. |
The ways to name a run — <run-id>, <unique-prefix>, @last, @last:<kind>, @<mark>.
Parses, then refuses: --attribution, --breakdown, --drawdowns reach this sub-verb and are turned down by name — see Accepted nowhere yet for what each one is waiting on.
backtest diff
vike-cli backtest diff <run-a> <run-b> [--all|--changed-only] [--trades] [--md]
Sets two stored runs side by side and reports the leaves that differ — inputs beside outputs, which is the question a metrics table cannot answer: a fee change and a signal change look identical in the numbers. Both selectors are required, and either can be any of the five forms.
# Name two stored runs — full id, unique prefix, @last, @last:<kind> or @<mark>
vike-cli backtest diff 1789213143-8821-01 1789213 # full id vs a unique prefix
vike-cli backtest diff @last @baseline/momentum # newest run vs a tagged mark
vike-cli backtest diff @last:search @last # @last:<kind> narrows by kind
vike-cli backtest diff @candidate @baseline/momentum # a mark on both sides
# Did the number move because the strategy changed, or because the inputs did?
vike-cli backtest diff @baseline/momentum @last --changed-only
vike-cli backtest diff @baseline/momentum @last --all
# Choose what the comparison holds
vike-cli backtest diff @last @baseline/momentum --all # unchanged leaves too
vike-cli backtest diff @last @baseline/momentum --trades # a third section
vike-cli backtest diff @last @baseline/momentum --trades --all
# Render it for a pull request — --md is this verb's answer to --json
vike-cli backtest diff @last @baseline/momentum --md
vike-cli backtest diff @last @baseline/momentum --md --trades --all
vike-cli backtest diff @last @baseline/momentum --md > diff.md
# In CI, beside the gate: the gate decides, this says what to look at
vike-cli backtest diff @last @baseline/momentum --md --changed-only
vike-cli backtest diff @last:search @baseline/momentum --md --changed-only
vike-cli backtest diff --help
# Narrow the comparison to what actually moved
vike-cli backtest diff @last @baseline/momentum --changed-only # only the leaves that differ--changed-only changes nothing — it is accepted and then ignored
The parser sets it and
nothing in run_diff reads it, because changed-only is already the default. It exists purely so an
explicit script is not refused for saying what the verb was going to do anyway. --all is the flag
that actually moves anything, by adding the unchanged leaves back.
There is no --json here, and --out is refused
This verb prints one clean stream, so the
shell's > already does the job — which is the stated reason --out is excluded from diff and
gate while ls and show own it. Use --md for a table a pull request can render.
--trades is shared with show, and means something different in each
On show it renders
the stored trade ledger as a section; here it adds a THIRD comparison section over both runs'
ledgers. It is deliberately kept out of both sub-verbs' exclusive rosters so that the other six can
refuse it by name — "it names the stored trade ledger, which show renders and diff compares" —
rather than answering unknown option.
| flag | sample | scope | what it does |
|---|---|---|---|
--all | — | any | diff only. Shows UNCHANGED leaves too — the negation of the default. Read by run_diff as show_all and passed to all three renderers (human/md/json). |
--changed-only | — | any | diff only, and a deliberate NO-OP: ReadArgs::changed_only is set by the parser and read by NOTHING in run_diff. It is accepted purely so an explicit script is not refused. |
--help | — | any | -h or --help, matched inside the shared reading flag loop. Also FALLS THROUGH — no refusal array names it. It short-circuits through the Err channel with args::HELP_SENTINEL ("help requested"), which args::exit_for_parse_error turns back into the USAGE on stdout and exit 0. |
--json | — | any | Print a machine-readable document instead of the human rendering. On run that is the server's report JSON verbatim (and on --local it is forwarded to the spawned engine); each reading verb emits its own — gate's carries the verdict, one object per criterion, and available_keys when a criterion went unevaluated. ⚠ ACCEPTED on path and read nowhere: that verb prints one line so $(…) works and has no document form, so the flag is inert there rather than refused. On script-check the document is the VERDICT (ok, error, script, params) and the rung is beside it; on templates it is the starter roster, one starter, or the path a starter was saved to. |
--md | — | any | diff only. Renders the same rows as a Markdown table. ⚠ --json WINS over it — run_diff tests a.json first, so --json --md emits JSON. |
--trades | — | any | THE OTHER DOUBLE-OWNED FLAG. Owners: show (renders trades.json as a section) and diff (adds a third diff section over both runs' trades.json). Deliberately kept OUT of both the show_only and diff_only rosters and refused on the remaining six by its own named rule with its own sentence. |
The ways to name a run — <run-id>, <unique-prefix>, @last, @last:<kind>, @<mark>.
Parses, then refuses: --attribution, --breakdown, --drawdowns reach this sub-verb and are turned down by name — see Accepted nowhere yet for what each one is waiting on.
backtest gate
vike-cli backtest gate <selector> --against <selector> --fail-if EXPR
Compares one stored run against a baseline and turns the comparison into an exit code. It needs
three things and refuses without any of them — a run selector, --against naming the baseline, and
--fail-if carrying the criteria — because every criterion is measured relative to that baseline
and this grammar has no absolute form.
# The shape: a stored run, a baseline, and the criteria that judge them
vike-cli backtest gate @last --against @baseline/momentum \
--fail-if 'sharpe:-5%' # exit 0 clean, exit 6 Breach
vike-cli backtest gate 1789213143-8821-01 --against @baseline/momentum \
--fail-if 'sharpe:-5%,max_dd:+10%' # ONE comma list — a 2nd --fail-if replaces
# Several criteria, one comma list — name each metric once
vike-cli backtest gate @last --against @baseline/momentum \
--fail-if 'sharpe:-5%,max_dd:+10%,return:-10%'
vike-cli backtest gate @last --against @baseline/momentum --fail-if 'equity:-2%'
vike-cli backtest gate @last --against @baseline/momentum --fail-if 'max_dd:+5%'
# Naming the two runs — any selector form works on either side
vike-cli backtest gate @last:search --against @baseline/momentum \
--fail-if 'sharpe:-5%' # @last:<kind> — the last run of that kind
vike-cli backtest gate 1789213 --against 1789213143-8821-01 \
--fail-if 'max_dd:+10%' # a unique prefix, judged against a full id
vike-cli backtest gate @baseline/candidate --against @baseline/momentum \
--fail-if 'sharpe:-5%' # a mark on both sides
# Either value form works — the split is on the FIRST '=' only
vike-cli backtest gate @last --against=@baseline/momentum --fail-if='sharpe:-5%'
vike-cli backtest gate --help # USAGE on stdout, exit 0Defend a result in CI. Set the baseline once by hand; everything after is the pipeline.
# once, on the run you want to defend
vike-cli backtest tag @last --as baseline/momentum --note 'pre-refactor reference'
# on every change
vike-cli backtest run --profile profiles/momentum_btc.toml --json
vike-cli backtest gate @last --against @baseline/momentum --fail-if 'sharpe:-5%'
# gate a search instead of a single run — @last:search needs no id
vike-cli backtest run --profile profiles/grid.toml --optimizer tpe --trials 200 --seed 7
vike-cli backtest gate @last:search --against @baseline/momentum \
--fail-if 'sharpe:-5%,max_dd:+10%'
# accepted the new number? re-point the mark — nothing is copied
vike-cli backtest tag @last --as baseline/momentum --note 'accepted: fee model fix'The whole CI step, branching on the CODE rather than on "non-zero". That is what makes the gate a gate: three of the codes below mean a BROKEN GATE rather than a failing strategy, and a step that fails the build on all of them teaches the team to ignore it.
# the run, with its report kept as an artifact — `--json` is `run`'s flag, not `gate`'s
set -u
vike-cli backtest run --profile profiles/momentum_btc.toml --json > run.json
# the verdict IS the exit code; the criteria it judged are on stdout
vike-cli backtest gate @last --against @baseline/momentum \
--fail-if 'sharpe:-5%,max_dd:+10%' | tee verdict.txt
rc=${PIPESTATUS[0]}
case "$rc" in
0) echo "within tolerance" ;;
6) echo "REGRESSION — the verdict names every criterion:"; cat verdict.txt; exit 1 ;;
7) echo "NOTHING WAS EVALUATED — an empty slice, or no coverage for the window."
echo "A DATA problem, not a strategy regression. Do not fail the build on it."; exit 0 ;;
3) echo "compute daemon unreachable — retry"; exit 75 ;; # EX_TEMPFAIL
*) echo "the gate itself failed (rc=$rc)"; exit 1 ;;
esac
# a reviewable comment, from the same two stored runs — `--md` is `diff`'s flag
vike-cli backtest diff @last @baseline/momentum --md >> "$GITHUB_STEP_SUMMARY"gate has no --json, so capture its stdout — and read the status past the pipe
--json is accepted on run alone. A pipeline expecting a machine-readable verdict FILE from
gate is written against a flag that does not exist, and will be refused by name. Pipe stdout
instead, and take ${PIPESTATUS[0]}: a plain $? after a pipe is tee's status, which is 0 even
on a breach — a gate that can only pass.
Three required inputs, all refused before any run is opened
Omit the baseline and the refusal
hands you the recipe rather than a complaint: make one with backtest tag <run> --as baseline/<name>, then point --against @baseline/<name> at it. Omit the criteria and you are told
the shape — a METRIC, a colon, a SIGN saying which direction is bad, the tolerance, and an optional
%. A blank or whitespace --against counts as absent, not as an empty baseline.
--fail-if — the grammar, in full. Get this wrong and the merge gate is decoration, so it is
worth the two minutes:
EXPR := CRITERION (',' CRITERION)*
CRITERION := METRIC ':' SIGN NUMBER ['%']
SIGN := '+' | '-'
--fail-if 'sharpe:-5%' # fail if Sharpe falls more than 5% below baseline
--fail-if 'sharpe:-5%,max_dd:+10%' # both — one flag, one comma list
--fail-if 'sharpe:-0.2' # no '%' — an ABSOLUTE amount, in the metric's own unitsThe sign says which direction is BAD and the number is the TOLERANCE, never a threshold. There is
no absolute-threshold form and no OR.
The sign is not optional, and it is not the direction of the change
- fails on a FALL and
+ fails on a RISE; the number is the MAGNITUDE. So sharpe:-5% means fail if Sharpe drops more
than 5%, and sharpe:--5% is refused outright — the tolerance cannot be negative, because the
direction is the sign's job. % makes the tolerance a share of the baseline's absolute value;
without it, it is an absolute amount. There is no absolute-threshold form at all: for "Sharpe must
exceed 1.2", pipe backtest show <run> --json through jq instead.
A metric is resolved against the report, not against a roster — so a typo is not an error
Three aliases resolve first, case-sensitively: max_dd → max_drawdown, return → total_return,
equity → final_equity. Every other name is kept verbatim and used as a key of report.json, which
means the metric set is open — any key the report has, or gains later, is accepted. The
consequence worth knowing: a misspelled metric does not fail the parse. When neither side carries a
finite number under that key the criterion comes back Unevaluated, which is not a pass you can
trust. Two criteria resolving to the same key are refused, alias pairs included
(max_dd:+5%,max_drawdown:+10%), because a silently-dropped criterion is a gate checking something
other than what it was handed.
Only exit 6 means "regression"
The ladder is 0 Ok, 1 Failed, 2 Usage, 3 Connect, 4 Refused, 5 Venue, 6 Breach, 7 Empty. A breach is 6; a run or baseline that will not resolve is 1 or 7; a
malformed flag is 2. A CI step branching on "non-zero" therefore reports a deleted mark, an empty
store and a typo'd expression as a failing strategy — branch on 6 specifically and treat the rest
as a broken gate, not as a result.
| flag | sample | scope | what it does |
|---|---|---|---|
--against <mark> | @baseline/momentum | any | gate only, and REQUIRED. A blank/whitespace value counts as absent. It is the baseline every criterion is relative to; a baseline that will not resolve is Exit::Failed/Exit::Empty, never Exit::Breach. |
--fail-if EXPR | sharpe:-5%,max_dd:+10% | any | gate only, and REQUIRED. Grammar: EXPR := CRITERION (',' CRITERION)* ; CRITERION := METRIC ':' SIGN NUMBER ['%'] ; SIGN := '+' | '-'. The sign says which direction is BAD and is REQUIRED; '%' makes the tolerance a share of |baseline|. RELATIVE only — there is no absolute form. Parsed TWICE: once by refuse_an_ungateable_line (before any directory is opened) and again in run_gate. |
--help | — | any | -h or --help, matched inside the shared reading flag loop. Also FALLS THROUGH — no refusal array names it. It short-circuits through the Err channel with args::HELP_SENTINEL ("help requested"), which args::exit_for_parse_error turns back into the USAGE on stdout and exit 0. |
--json | — | any | Print a machine-readable document instead of the human rendering. On run that is the server's report JSON verbatim (and on --local it is forwarded to the spawned engine); each reading verb emits its own — gate's carries the verdict, one object per criterion, and available_keys when a criterion went unevaluated. ⚠ ACCEPTED on path and read nowhere: that verb prints one line so $(…) works and has no document form, so the flag is inert there rather than refused. On script-check the document is the VERDICT (ok, error, script, params) and the rung is beside it; on templates it is the starter roster, one starter, or the path a starter was saved to. |
The ways to name a run — <run-id>, <unique-prefix>, @last, @last:<kind>, @<mark>.
⚠ OPEN — there is NO metric roster. parse_one resolves a metric against METRIC_ALIASES and otherwise keeps the spelling verbatim as the JSON key, so ANY key of report.json (and any key it gains later) is accepted. An unknown metric is NOT a parse error.
Aliases those metrics also answer to — max_dd -> max_drawdown, return -> total_return, equity -> final_equity.
Parses, then refuses: --attribution, --breakdown, --drawdowns reach this sub-verb and are turned down by name — see Accepted nowhere yet for what each one is waiting on.
backtest params
vike-cli backtest params (--strategy NAME | --script <s.rhai>)
Lists the tunable knobs a strategy declares, from exactly one source — a registry name or a .rhai
file. It is offline: it reads the script or the strategy roster on this machine and lists its
knobs, with no server, no store and no engine. Worth running before you write a profile, because a
typo'd knob name in [strategy.params] is a silent no-op rather than a refusal.
# A built-in strategy: name it, get its knobs
vike-cli backtest params --strategy buy_hold
vike-cli backtest params --strategy momentum
vike-cli backtest params --strategy spread_maker
vike-cli backtest params --strategy=buy_hold # --flag=value is the same flag
# A .rhai script: the knobs the file itself declares — it is read and COMPILED
vike-cli backtest params --script s.rhai
vike-cli backtest params --script strategies/momentum.rhai
# The usage text
vike-cli backtest params --helpOne source, named exactly once
--strategy and --script answer the same question from two
places, so naming both is the refusal — give one. Unlike on run, --strategy here is checked:
a name no roster carries is turned down with the roster printed, because this verb's whole product
is the knob list and there is no far side to decide it later.
[strategy.params] is untyped, which is why this verb matters
The table is an untyped TOML
value on both sides of the wire, so no schema validation is possible anywhere: a misspelled knob is
accepted, ignored, and the strategy runs with the default you meant to change. The names this prints
are the only ones --param actually reaches.
23 run flags are refused here by name, and two are not
--strategy and --script belong
to both verbs. Every other sugar flag — --profile, --preset, --set, --venue, --cash and
the rest — is refused with a sentence saying this verb reads the script or the roster and lists its
knobs, rather than with a bare unknown option. That named refusal is deliberate: these are the
flags somebody would most plausibly try here.
| flag | sample | scope | what it does |
|---|---|---|---|
--help | — | any | -h or --help, matched inside the shared reading flag loop. Also FALLS THROUGH — no refusal array names it. It short-circuits through the Err channel with args::HELP_SENTINEL ("help requested"), which args::exit_for_parse_error turns back into the USAGE on stdout and exit 0. |
--json | — | any | Print a machine-readable document instead of the human rendering. On run that is the server's report JSON verbatim (and on --local it is forwarded to the spawned engine); each reading verb emits its own — gate's carries the verdict, one object per criterion, and available_keys when a criterion went unevaluated. ⚠ ACCEPTED on path and read nowhere: that verb prints one line so $(…) works and has no document form, so the flag is inert there rather than refused. On script-check the document is the VERDICT (ok, error, script, params) and the rung is beside it; on templates it is the starter roster, one starter, or the path a starter was saved to. |
--script <s.rhai> | s.rhai | any | Injects the .rhai file's source into strategy.params.src (overwriting any existing src) and implies strategy.name=rhai. On the params sub-verb the same spelling names the script whose knobs are listed, and on script-check the script to COMPILE — three owners, one meaning: a .rhai file on this machine. |
--strategy NAME | buy_hold | any | Sugar for strategy.name, forwarded UNVALIDATED (the roster lives in vike-backtest and includes user strategies this crate cannot see). On the params sub-verb the same spelling names the roster entry whose knobs are listed. |
2 flags of this plane are refused here by name — each answers with a sentence naming the sub-verb that owns it, never a bare unknown option. They are listed in full under Refusals.
The strategies --strategy knows here — buy_hold, grid, dca_accumulate, spread_maker, gueant_maker, trailing_scalper, momentum, funding_carry, funding_capture, pairs_zscore, rotation_top_k, bracket_per_symbol, gated_weights, caps_sizers_mask, tick_pair_mse, cheap_catch_updown_fair_value, sport_copy_follower, rhai.
Parses, then refuses: --attribution, --breakdown, --drawdowns reach this sub-verb and are turned down by name — see Accepted nowhere yet for what each one is waiting on.
backtest strategies
vike-cli backtest strategies
Prints the roster of built-in strategies to stdout, one per line, and takes no flag but --help.
That is the whole surface — there is deliberately no filter, no format and no output flag, because
plain lines are what the shell already knows how to narrow.
# Print the roster — plain lines, so the shell does the rest
vike-cli backtest strategies # the built-in names, nothing else
vike-cli backtest strategies > strategies.txt # the shell redirects; there is no --out
vike-cli backtest strategies | grep -i maker
vike-cli backtest strategies | grep -i funding
vike-cli backtest strategies | wc -l # how many there are
# Confirm a name before you spell it in a profile
vike-cli backtest strategies | grep -x buy_hold
vike-cli backtest strategies | grep -x momentum
vike-cli backtest strategies | grep -x rhai # the name implied when --script is given
# The workflow this verb starts — read the knobs next with `params --strategy <name>`
vike-cli backtest strategies | grep -i momentum
vike-cli backtest strategies | grep -iE 'maker|carry'
# Read the usage instead of the roster
vike-cli backtest strategies --help # the USAGE, on stdout, exit 0
vike-cli backtest strategies --help | head -20 # stdout, so head and a pager both workConfirming a name here is worth the extra command
run will not do it for you:
--strategy is forwarded unvalidated — the roster lives in the engine and includes user
strategies this binary cannot see — so a bad name is decided far-side, after the profile has already
been shipped. On params the same spelling is checked. That asymmetry is why this verb exists.
The list is this binary's, not the engine's
It prints what is compiled in here; a compute
server can carry user strategies this roster has never heard of, and those names work on run
despite being absent from this output. An empty or missing name here is not proof the far side will
refuse it — and a name that is here is not proof a given server has it.
--help is the entire flag surface
Every other flag of the plane is refused by name, the three
unbuilt renderers included, so a pasted line from another sub-verb tells you where the flag actually
lives rather than answering unknown option. (It was four until --html shipped.)
| flag | sample | scope | what it does |
|---|---|---|---|
--addr host:port | 127.0.0.1:7880 | remote | The COMPUTE daemon to dial (vike-backend backtest --addr, NOT the datahub). Refused beside --local; unwritten, it falls through config.backtest_addr to 127.0.0.1:7880. |
--help | — | any | -h or --help, matched inside the shared reading flag loop. Also FALLS THROUGH — no refusal array names it. It short-circuits through the Err channel with args::HELP_SENTINEL ("help requested"), which args::exit_for_parse_error turns back into the USAGE on stdout and exit 0. |
--json | — | any | Print a machine-readable document instead of the human rendering. On run that is the server's report JSON verbatim (and on --local it is forwarded to the spawned engine); each reading verb emits its own — gate's carries the verdict, one object per criterion, and available_keys when a criterion went unevaluated. ⚠ ACCEPTED on path and read nowhere: that verb prints one line so $(…) works and has no document form, so the flag is inert there rather than refused. On script-check the document is the VERDICT (ok, error, script, params) and the rung is beside it; on templates it is the starter roster, one starter, or the path a starter was saved to. |
Parses, then refuses: --attribution, --breakdown, --drawdowns reach this sub-verb and are turned down by name — see Accepted nowhere yet for what each one is waiting on.
backtest templates
vike-cli backtest templates [ID] [--write-strategy <s.rhai>]
The starter Rhai strategies this binary ships — listed, printed, or saved to a new file. It is OFFLINE in the strongest sense available here: the answer comes from consts compiled into this binary, so there is no server, no store, no engine and no run directory. The positional is the selector and it is optional — absent lists the roster, present prints that one, and a second positional is a usage error rather than a filter.
# The roster — no argument, so it lists
vike-cli backtest templates
vike-cli backtest templates --json # the same roster, as a document
# One template, printed to stdout
vike-cli backtest templates sma-cross
vike-cli backtest templates sma-cross --json
# Start a strategy FROM one — the file is created, never overwritten
vike-cli backtest templates sma-cross --write-strategy strategies/my-cross.rhai
vike-cli backtest templates sma-cross --write-strategy s.rhai
# Confirm a selector before you save with it
vike-cli backtest templates | grep -i cross
vike-cli backtest templates | wc -l # how many ship
# The usage text
vike-cli backtest templates --help--write-strategy creates; it will not overwrite
This is the one member of the authoring family that writes a file, and it refuses an existing path rather than replacing it. That is deliberate: the argument is a path you are about to edit, and a silent overwrite of a strategy somebody has already changed is the one mistake this verb could make that no diff would show. Point it at a new name, or move the old file first.
Printing a template is not compiling it
The roster and the printed body are consts. Nothing is parsed on the way out, so a template that
prints is not thereby a template that compiles in your tree. script-check is the verb that answers
that, and it is the natural next command after a --write-strategy — as params is for reading
back the knobs the saved file declares.
Two neighbouring flags are refused here by name
--script belongs to script-check and --out to the reading verbs; typing either here is refused
with a message that names the sub-verb that owns it rather than unknown option. The selector is a
POSITIONAL, so there is no --template either.
| flag | sample | scope | what it does |
|---|---|---|---|
--json | — | any | Print a machine-readable document instead of the human rendering. On run that is the server's report JSON verbatim (and on --local it is forwarded to the spawned engine); each reading verb emits its own — gate's carries the verdict, one object per criterion, and available_keys when a criterion went unevaluated. ⚠ ACCEPTED on path and read nowhere: that verb prints one line so $(…) works and has no document form, so the flag is inert there rather than refused. On script-check the document is the VERDICT (ok, error, script, params) and the rung is beside it; on templates it is the starter roster, one starter, or the path a starter was saved to. |
--write-strategy <s.rhai> | s.rhai | any | templates only. Saves the named starter to a file that does NOT exist yet — a --write-profile-shaped rule, and the reason this is not --out: that flag overwrites, and the file this one writes is a strategy somebody then edits, so clobbering it from a flag typo is unrecoverable. It needs a starter id: given alone it is a usage refusal rather than a roster listing, because a listing plus exit 0 reads exactly like a write that happened. |
Parses, then refuses: --attribution, --breakdown, --drawdowns reach this sub-verb and are turned down by name — see Accepted nowhere yet for what each one is waiting on.
backtest script-api
vike-cli backtest script-api
Everything an authored Rhai strategy may call: the reads, the order verbs, the param() knob,
the indicator spellings, and the hooks to put them in. It takes no positional and no input file — it
describes the HOST, not a script — and it is OFFLINE: the answer is derived from the host's own
registration compiled into this binary, so it needs no server, no store and no engine.
# The whole callable surface
vike-cli backtest script-api
vike-cli backtest script-api --json # the same surface, as a document
# Settle a spelling before you type it into a strategy
vike-cli backtest script-api | grep -i rsi
vike-cli backtest script-api | grep -i atr
vike-cli backtest script-api | grep -iE 'buy|sell|close'
# Which hooks exist to put the calls in
vike-cli backtest script-api | grep -i hook
# Keep the surface beside the script you are writing
vike-cli backtest script-api --json > api.json
# The usage text
vike-cli backtest script-api --helpDerived from the registration, not from a list
The roster is read off the host's own registration rather than a hand-kept list, which is what makes this the place to settle a spelling. A name this output does not carry is a name the interpreter has not been given — and a misspelled indicator does not fail loudly at authoring time, it errors on every bar until the strategy switches itself off.
This describes the host, so it cannot see your script
There is deliberately no --script here. What a particular file DECLARES is params' answer and
whether it COMPILES is script-check's; this verb answers only what the host makes available to any
script at all. The three are separate for that reason rather than by accident.
| flag | sample | scope | what it does |
|---|---|---|---|
--json | — | any | Print a machine-readable document instead of the human rendering. On run that is the server's report JSON verbatim (and on --local it is forwarded to the spawned engine); each reading verb emits its own — gate's carries the verdict, one object per criterion, and available_keys when a criterion went unevaluated. ⚠ ACCEPTED on path and read nowhere: that verb prints one line so $(…) works and has no document form, so the flag is inert there rather than refused. On script-check the document is the VERDICT (ok, error, script, params) and the rung is beside it; on templates it is the starter roster, one starter, or the path a starter was saved to. |
Parses, then refuses: --attribution, --breakdown, --drawdowns reach this sub-verb and are turned down by name — see Accepted nowhere yet for what each one is waiting on.
backtest script-check
vike-cli backtest script-check --script <s.rhai>
Compiles one Rhai script offline and answers twice — the diagnostic on stdout and a verdict in the exit code. It is built for a save hook or a pre-commit, which is why the number matters as much as the text: no server, no store, no engine, and nothing written anywhere.
# Compile one file
vike-cli backtest script-check --script s.rhai
vike-cli backtest script-check --script strategies/my-cross.rhai
vike-cli backtest script-check --script=s.rhai # --flag=value is the same flag
# The document form, for a hook that reports rather than prints
vike-cli backtest script-check --script s.rhai --json
# In a save hook: the exit code is the verdict, the text is the reason
vike-cli backtest script-check --script s.rhai || echo 'blocked: it does not compile'
# The usage text
vike-cli backtest script-check --helpA script that compiles prints ok: <path> compiles (N param() knob(s)) and exits 0. One that
does not prints error: <path> does not compile, then the compiler's own diagnostic in full, and
exits 1 — the diagnostic is on stdout either way, so a hook can capture it without merging
streams.
Three outcomes, and only two are about your script
Exit 0 and exit 1 are verdicts: the file was read and compiled, or read and rejected. Exit
2 is not — it means the command line was wrong, and the case you will actually hit is omitting
--script, which is refused with a sentence naming the flag. A wrapper that treats every non-zero
as "the strategy is broken" reports a typo in its own invocation as a failing strategy.
An unreadable file is not a verdict either
A path that cannot be opened fails with cannot read script <path> rather than a compile
diagnostic, because nothing was compiled and there is nothing to diagnose. Check the path exists
first — a hook that reads a missing file as a broken script blocks a commit for a reason that is not
in the commit.
Compiling is not the same as declaring
This verb answers whether the interpreter accepts the file. What knobs it DECLARES is params'
answer, and the two disagree usefully: a script can compile cleanly and still declare nothing, which
is what a missing param() looks like from the outside. templates --write-strategy is where a
file worth checking usually comes from.
| flag | sample | scope | what it does |
|---|---|---|---|
--json | — | any | Print a machine-readable document instead of the human rendering. On run that is the server's report JSON verbatim (and on --local it is forwarded to the spawned engine); each reading verb emits its own — gate's carries the verdict, one object per criterion, and available_keys when a criterion went unevaluated. ⚠ ACCEPTED on path and read nowhere: that verb prints one line so $(…) works and has no document form, so the flag is inert there rather than refused. On script-check the document is the VERDICT (ok, error, script, params) and the rung is beside it; on templates it is the starter roster, one starter, or the path a starter was saved to. |
--script <s.rhai> | s.rhai | any | Injects the .rhai file's source into strategy.params.src (overwriting any existing src) and implies strategy.name=rhai. On the params sub-verb the same spelling names the script whose knobs are listed, and on script-check the script to COMPILE — three owners, one meaning: a .rhai file on this machine. |
Parses, then refuses: --attribution, --breakdown, --drawdowns reach this sub-verb and are turned down by name — see Accepted nowhere yet for what each one is waiting on.
Running it somewhere else
Reaching a daemon that is not on this machine
The compute daemon binds loopback only by default, and that is a refusal rather than a
preference: the control handshake is plaintext, so a non-loopback bind is refused outright unless
VIKE_DATAHUB_ALLOW_PUBLIC_BIND=1 is set on the daemon's own box, and even then it requires a node
key. Setting the variable on your side does nothing.
So the sanctioned way to reach a remote daemon is a tunnel, not a wider bind:
ssh -N -L 7880:127.0.0.1:7880 user@the-box # in another shell, or -f to background
vike-cli backtest run --profile p.toml # --addr now needs no change⚠ Set a file log level before any long run. The JSON file log defaults to trace and is not
affected by console log settings. One hour of a chatty batch job has measured 6.4 GB, and a long
range once wrote enough to nearly fill the disk of a box hosting live trading. Export
VIKE_LOG_FILE_LEVEL=warn — or off — for any long search or walk-forward.
The profile
You never have to write one by hand: build a command line you like, then --write-profile it.
Two optional tables change what kind of run it is — a non-empty [paramscan] makes it a search,
a [walkforward] table makes it a walk-forward, and a profile declaring both composes.
Every table, key, default, accepted value and refusal has its own generated page:
backtest --profile. It is rendered from the Rust
types that declare the schema — walk-forward's nine keys and its two window forms, the eight sizer
kinds and the knob each requires, and the sixty-odd refusals verbatim — so it cannot fall behind
them. This page stays the COMMAND surface; two properties of the file are worth carrying here,
because they change how you use the flags above.
--set <dotted key>=<value> reaches every key of every table, not just the ones with a flag of
their own, and the schema is closed — an undeclared field is a hard load error, so a typo is
refused, never a silent no-op. That is what makes the flag safe to paste from anywhere.
⚠ A default run is frictionless — know what is off. Cash admission (engine.cash_gate), margin
and liquidation (engine.maint_margin, engine.liq_buffer), and the volume-participation cap
(engine.volume_limit) are all off unless you turn them on. So a default run can spend money it
does not have and fill size the market could not absorb. Turn them on before you believe a number,
and use --show-effective to confirm what actually resolved.
The whole surface
Everything the exported surface carries, in full. Nothing above needs this section to be read — it is here so a question about one flag always has an answer.
Defaults, and who decides them
22 flags carry a default. Where it is decided differs — a FAR SIDE value cannot be shown before you run.
implied by the parser when the flag is absent
| flag | default | conditional on | why |
|---|---|---|---|
--addr | 127.0.0.1:7880 | — | resolve_addr(cli, configured) folds three rungs and falls through to vike_config::DEFAULT_BACKTEST_ADDR. The FLAG layer performs no host:port check at all (a blank value is treated as absent, and dialling anything else produces a connect-rung failure); the file and env layers do check, via vike_config's check_addr / check_env_addr. |
--cols | run_id,kind,started_at,report | — | ls::DEFAULT_COLS — manifest-only, so a bare ls opens no report.json at all. |
--engine | <project>/bin/backtest[EXE] → <exe_dir>/backtest[EXE] → <project>/bin/vike[EXE] with lead arg backtest → <exe_dir>/vike[EXE] with lead arg backtest → bare backtest on PATH | — | crate::cmd::engine's locate(explicit, project_root): an explicit path short-circuits the whole ladder and is deliberately NOT probed for existence (a named path that is not there must fail with the name the operator typed), and is never treated as a multicall. Otherwise the four-rung standalone-then-multicall search runs, with bare backtest on PATH as the last word. |
--kind | bar | — | no --kind and no profile said otherwise |
--strategy | rhai | --script | implied_defaults pushes strategy.name = "rhai" ONLY when a script was named. With no --script the CLI supplies nothing and StrategyCfg::name carries no serde default, so the far side decides. |
--on-gap | refuse | --require-coverage | An armed gate with no stated disposition refuses. The other way round — arming a gate that then runs anyway — is the shape that reads as checked while checking nothing. |
--universe | declared | — | Unwritten, the symbol list is taken verbatim — byte-identical to every profile written before this key existed. |
the profile schema — the Rust type its field deserialises into
| flag | default | conditional on | why |
|---|---|---|---|
--fee | 0.0 | — | EngineCfg::fee_rate is #[serde(default)] f64 in crates/vike-backtest/src/harness/profile.rs, so an absent value is 0.0. The CLI supplies nothing. |
--interval | 1d | — | DataCfg::interval is #[serde(default = "default_interval")] and crates/vike-backtest/src/harness/profile.rs's default_interval returns "1d". The CLI supplies nothing. |
--slippage | 0.0 | — | EngineCfg::slippage is #[serde(default)] f64 in crates/vike-backtest/src/harness/profile.rs. The CLI supplies nothing. |
--symbol | [] | — | DataCfg::symbols is #[serde(default)] Vec\<String> in crates/vike-backtest/src/harness/profile.rs; empty is the schema default and the CLI supplies nothing. |
--venue | None | — | DataCfg::venue is #[serde(default)] Option\<String> in crates/vike-backtest/src/harness/profile.rs, so an absent venue is None (the cross-venue [[data.series]] form is the alternative). The CLI supplies nothing. |
the server, not this CLI — the value is decided where the work runs
| flag | default | conditional on | why |
|---|---|---|---|
--cash | — | — | NO default is supplied by either side. implied_defaults's doc states it outright: a starting balance is a modelling input with no defensible default, so an omitted --cash is a FAR-SIDE missing-field refusal naming engine.cash. |
--decide | sequential | — | This crate substitutes NO default — an absent --decide writes no engine.decide key at all, and decide_mode resolves an absent key to DecideMode::Sequential, byte-identical to every profile written before the key existed. It is NOT an implied_defaults row for that reason: there is nothing the schema failed to write down. |
--euler-depth | 3 | — | CONFIRMED forwarded verbatim (same three sites as --trials). Default applied only under euler, by search_select's parse_euler_depth → EulerConfig::DEFAULT_MAX_DEPTH = 3; the hard cap is EulerConfig::MAX_DEPTH_CAP = 16 and 0 is legal (coarse-grid-only). |
--from | — | — | DataCfg::from carries NO serde default in crates/vike-backtest/src/harness/profile.rs, so an absent value is a far-side missing-field refusal. The CLI implies nothing. |
--optimizer | grid | — | This crate substitutes NO default — an absent --optimizer is forwarded as None on BOTH routes and the side that RUNS resolves it against vike_datahub_client::DEFAULT_SEARCH_METHOD ("grid"). DEFAULT_OPTIMIZER in vike-cli is #[cfg(test)]-gated and exists only to build sample argv in that file's own tests. |
--rank-by | sharpe | — | The CLI forwards None when unwritten; search_select's resolve_rank maps None to RankChoice::Metric(RankMetric::default()), and RankMetric's #[default] variant is Sharpe. Deliberately NOT part of Args::search_requested, so it never routes a run to the search verb by itself. |
--seed | 0 under tpe; NO default under genetic (absence is refused) | — | CONFIRMED forwarded verbatim (same three sites). search_select's parse_seed returns Option\<u64> and substitutes nothing; the tpe arm applies .unwrap_or(0) and the genetic arm calls require_seed, which refuses an absent seed outright. |
--store | — | — | Unwritten, no --store is pushed into the child argv at all and the engine resolves its own hist-store root (crates/vike-backtest/src/backtest_cli.rs's store_root, which also consults the caller-supplied vars). This side substitutes nothing and validates nothing about the path. |
--to | — | — | DataCfg::to carries NO serde default in crates/vike-backtest/src/harness/profile.rs. The CLI implies nothing. |
--trials | 64 | — | CONFIRMED forwarded verbatim: parse_run_args stores the raw String, execute_local pushes ("--trials", &args.trials) into the child argv unchanged, and Args::wire_search clones it into WireSearch.trials as a String. The default is applied only when tpe is the resolved method, by search_select's parse_trials → TpeConfig::DEFAULT_TRIALS = 64. |
Accepted nowhere yet
4 flags are declared and accepted by no sub-verb.
| flag | refusal reaches | what it will do |
|---|---|---|
--attribution | ls, show, path, tag, diff, gate, params, strategies, templates, script-api, script-check | Refused by name on any reading sub-verb: the engine computes no per-source attribution at all, so this is missing DATA rather than a missing renderer. |
--breakdown | ls, show, path, tag, diff, gate, params, strategies, templates, script-api, script-check | Refused by name on any reading sub-verb, and UNCONDITIONALLY: a period breakdown needs per-bar returns, and the reason it is refused — a curve DECIMATED past the sample cap — holds only for a large run, while this guard sees argv alone and no run. vike-cli report \<run> --breakdown day|month evaluates that condition per run, off the same series.json. |
--drawdowns | ls, show, path, tag, diff, gate, params, strategies, templates, script-api, script-check | Refused by name on any reading sub-verb, and what is missing is the WIRING rather than the renderer: vike_analytics::periods::drawdown_table_text is linked into this binary and nothing calls it, while the equity curve it reads is in the run record and --export equity hands it over today. |
--list-params | run | RETIRED. A knob discovery is now backtest params, not a mode flag on a run — decision 11 gives every action inside this plane exactly one spelling. Both parse levels answer this spelling with its replacement. |
Refusals
150 refusals across 50 flags, verbatim. A name in braces is substituted at runtime.
Given a value it will not take (130)
| flag | when | message |
|---|---|---|
--add | ls, show, path, diff, gate, params, strategies, templates, script-api, script-check | --add does not apply to backtest \{sub} — that flag WRITES a label, a note or a mark onto a stored run, which is what tag does |
--addr | the daemon is not reachable (exit 3, the CONNECT rung; \{addr} is the resolved address and \{e} the transport error) | cannot connect to the backtest daemon at {addr}: {e} (start it with vike-backend backtest --addr) |
--against | ls, show, path, tag, diff, params, strategies, templates, script-api, script-check | --against does not apply to backtest \{sub} — that flag names the BASELINE or the criteria a verdict is computed from, and gate is the verb whose product is that verdict |
--all | ls, show, path, tag, gate, params, strategies, templates, script-api, script-check | --all does not apply to backtest \{sub} — that flag shapes a two-run COMPARISON, which is what diff renders |
--all | (any reading sub-verb, with a value) | --all takes no value |
--as | tag, empty name | --as a mark name is required (for example baseline/momentum, or prod) |
--as | tag, leading/trailing whitespace | --as '{name}': a mark name may not begin or end with whitespace |
--as | tag, more than 3 '/'-separated parts | --as '{name}': a mark name may have at most 3 '/'-separated parts — a mark is a label, not a directory tree |
--as | tag, an empty part | --as '{name}': an empty part — a mark name may not start or end with '/' and may not contain '//' |
--as | tag, a part starting with '.' | --as '{name}': the part '{segment}' starts with '.' — that is a traversal ('..'), or a dot-entry every directory scan in this workspace skips, so the mark would be written and then be invisible |
--as | tag, a disallowed character | --as '{name}': the part '{segment}' contains '{bad}' — a mark name takes ASCII letters, digits, '.', '_', '-' and '/' only, because it becomes a file path on every platform this ships to |
--as | ls, show, path, diff, gate, params, strategies, templates, script-api, script-check | --as does not apply to backtest \{sub} — that flag WRITES a label, a note or a mark onto a stored run, which is what tag does |
--cash | ls, show, path, tag, diff, gate, strategies, templates, script-api, script-check | unknown option '--cash' |
--changed-only | ls, show, path, tag, gate, params, strategies, templates, script-api, script-check | --changed-only does not apply to backtest \{sub} — that flag shapes a two-run COMPARISON, which is what diff renders |
--changed-only | (any reading sub-verb, with a value) | --changed-only takes no value |
--cols | ls, when every element is empty after trimming | --cols was given no column names |
--cols | show, path, tag, diff, gate, params, strategies, templates, script-api, script-check | --cols does not apply to backtest \{sub} — that flag narrows, orders or shapes a LISTING, and ls is the listing |
--config | ls, path, tag, diff, gate, params, strategies, templates, script-api, script-check | --config does not apply to backtest \{sub} — that flag selects a SECTION of one stored run, which is what show renders |
--config | (any reading sub-verb, with a value) | --config takes no value |
--decide | an unknown spelling (the ENGINE or the compute server, after a spawn or a dial — this side forwards the value verbatim) | unknown engine.decide {other:?} (want sequential | simultaneous; absent = sequential, the per-symbol walk in the order data.symbols lists them) |
--decide | simultaneous on a tick profile (the far side, BacktestProfile::refusals) | engine.decide = "simultaneous" is bar-mode only: the cross-sectional … |
--engine | ⚠ typed WITHOUT --local, i.e. on the remote arm (CLI, exit 2). Same one-line template as --store. | --engine applies to --local only — a remote run reads the SERVER's store |
--engine | the named program cannot be spawned (CONNECT rung; \{} is the engine display form, \{e} the io::Error, MISSING_HINT a constant in crates/vike-cli/src/cmd/engine.rs that this area did not transcribe) | cannot run the backtest engine ({engine}): {e} {MISSING_HINT} |
--euler-depth | --euler-depth under any method but euler (from METHOD_KNOBS, both routes) | --euler-depth is a euler flag, but this run selected the "grid" optimizer. It was silently discarded before; it is refused now, because a knob that configures a search you did not select cannot do what it says |
--euler-depth | a value past the cap, under euler (\{d} is the parsed value, the second hole is EulerConfig::MAX_DEPTH_CAP) | --euler-depth 99 is past the cap of 16 — it was silently clamped before, which made the budget line report a depth you did not ask for |
--euler-depth | a non-integer value, under euler | invalid --euler-depth "abc" (expected an integer) |
--export | value fills (Usage, rung 2) — refused at EXECUTION time in export_document, not at parse time | --export fills is not available — fills are not stored at ANY size. A backtest computes them, consumes them into report.json's scalars and drops them; only the trade LEDGER and the equity curve survive. --export trades and --export equity both work today. |
--export | any value containing a comma, e.g. trades,equity (Usage, rung 2). Template: "--export '{value}': one value at a time. …" where {value} is the trimmed spec as typed | --export 'trades,equity': one value at a time. Each export is a single document and --out names a single file, so a list would either be truncated to its first value or written as two documents nothing can parse. Run show once per value. |
--export | any other unrecognised value (Usage, rung 2). Template: "--export '{other}' is not a value this verb knows — §6.2's set is trades, equity and fills, of which {list} are stored (fills are not: see --export fills)" where {other} is the trimmed value and {list} is EXPORTABLE.join(" and ") = "trades and equity" | --export 'nonsense' is not a value this verb knows — §6.2's set is trades, equity and fills, of which trades and equity are stored (fills are not: see --export fills) |
--export | --export trades on a run whose producer wrote no trades.json (Failed, rung 1). Template holes: {run_id} = the run's directory name, {file} = vike_model::runs::TRADES_FILE = "trades.json" | a-1-0 wrote no trades.json — this producer keeps no trade ledger (a study, or a run minted before the run artifact existed) |
--export | --export equity on a run whose producer wrote no series.json (Failed, rung 1). Template holes: {run_id} = the run's directory name, {file} = vike_model::runs::SERIES_FILE = "series.json" | a-1-0 wrote no series.json — this producer keeps no equity curve |
--export | any other RunReadError on either value (Failed, rung 1) | <the RunReadError's own Display, passed through verbatim by CliError::failed(other.to_string())> |
--export | used on any sub-verb other than show (Usage, rung 2), by refuse_foreign_read_flags's show_only roster. Template: "{flag} does not apply to backtest \{sub} — {why}" | --export does not apply to backtest ls — that flag selects a SECTION of one stored run, which is what show renders |
--fail-if | the whole expression is empty or whitespace | --fail-if takes at least one criterion, e.g. sharpe:-5%,max_dd:+10% — METRIC, a colon, a SIGN saying which direction is bad, the tolerance, and an optional % |
--fail-if | a term between two commas is empty | --fail-if: an empty criterion — terms are joined by a single comma, e.g. sharpe:-5%,max_dd:+10% |
--fail-if | the colon is missing (format! template; {term} is the trimmed term) | --fail-if \{term}: a criterion is METRIC:SIGN NUMBER[%], e.g. sharpe:-5% — the colon is missing |
--fail-if | the metric before the colon is empty ({term} = the trimmed term) | --fail-if \{term}: the metric before the colon is empty |
--fail-if | no leading + or - on the tolerance ({term} = the trimmed term, {metric} = the trimmed metric) | --fail-if \{term}: the tolerance needs a SIGN saying which direction is bad — - fails on a FALL (\{metric}:-5%), + fails on a RISE (\{metric}:+5%). Without one this gate would check a side of the number you did not choose. |
--fail-if | the magnitude does not parse as f64 ({term} = the trimmed term, {number} = the text after the sign with any trailing % stripped, NOT trimmed) | --fail-if \{term}: \{number} is not a number — the tolerance is the MAGNITUDE |
--fail-if | the magnitude parses but is not finite, e.g. sharpe:+inf ({term}, {number} as above) | --fail-if \{term}: \{number} is not a finite tolerance |
--fail-if | the magnitude is negative, e.g. sharpe:--5% ({term} = the trimmed term, {metric} = the trimmed metric) | --fail-if \{term}: the tolerance is negative — the DIRECTION is the sign's job and the number is the magnitude, so \{metric}:-5% is what \{term} was probably meant to be |
--fail-if | two criteria resolve to the SAME report key — including an alias pair such as max_dd:+5%,max_drawdown:+10% ({prev} and {c} are the two criteria re-rendered by Criterion's Display, {key} is the resolved JSON key) | \{prev} and \{c} both judge \{key} — name each metric once; a silently-dropped criterion is a gate checking something other than what it was given |
--help | --version / -V typed inside the backtest plane | unknown argument: --version |
--html | --html --json | --html and --json are two SERIALIZATIONS of the same run, and this verb writes one document: --html is the rendered tearsheet a human opens, --json is the machine shape (\{run_id, dir, manifest, report}). Run it twice, or take the JSON and render it elsewhere — every number in the HTML comes from the same report.json --json prints. |
--interval | vike_model::time::interval_ms returns None — fewer than 2 chars, a non-digit count, or an unknown trailing unit. NOTE: a ZERO count (0m) DOES parse and is NOT refused here. | --interval {v:?} is not a valid interval — a digit count and one unit of s|m|h|d (e.g. 30s, 5m, 1h, 1d) |
--json | an inline value (--json=1) | --json takes no value |
--kind | a value outside DATA_KINDS (exact, case-sensitive) | --kind must be bar|tick, got {value:?} |
--limit | ls, non-usize value | --limit '{n}' is not a whole number of rows |
--limit | show, path, tag, diff, gate, params, strategies, templates, script-api, script-check | --limit does not apply to backtest \{sub} — that flag narrows, orders or shapes a LISTING, and ls is the listing |
--local | --local on a profile that declares a [walkforward] table (CLI, exit 2). One real newline before Drop --local. | --local cannot run a walk-forward: this profile declares a [walkforward] table, and the standalone engine has ONE profile path — it branches on the [paramscan] grid and nothing else, so neither walk-forward driver is reachable from any binary and there is nothing local to spawn. Drop --local to run it on the compute daemon, or drop the [walkforward] table to backtest this profile here. |
--local | an inline value (--local=1) | --local takes no value |
--local | --local when the profile had to be STAGED and there is no project above the working directory (exit 1). \{} is filled by one of the two sentences below. | --local runs the engine on this machine from a profile FILE, and there is no project above the working directory to stage one in. {staging reason} Run inside your project (or set $VIKE_SETTINGS_DIR), pass an already-merged profile to --profile, or write one first with --write-profile <path> |
--md | ls, show, path, tag, gate, params, strategies, templates, script-api, script-check | --md does not apply to backtest \{sub} — that flag shapes a two-run COMPARISON, which is what diff renders |
--md | (any reading sub-verb, with a value) | --md takes no value |
--metrics | ls, path, tag, diff, gate, params, strategies, templates, script-api, script-check | --metrics does not apply to backtest \{sub} — that flag selects a SECTION of one stored run, which is what show renders |
--metrics | (any reading sub-verb, with a value) | --metrics takes no value |
--metrics-list | ls, path, tag, diff, gate, params, strategies, templates, script-api, script-check — a NAMED rule rather than a show_only roster row, because that roster's sentence ("selects a SECTION of one stored run") is FALSE of a listing | --metrics-list does not apply to backtest \{sub} — it prints the METRIC CATALOG (every id this tree can measure, with what each one means), which is show's answer to "what can I ask for" beside its --metrics. It names no run and reads no runs directory: backtest show --metrics-list |
--metrics-list | (any reading sub-verb, with a value) | --metrics-list takes no value |
--note | ls, show, path, diff, gate, params, strategies, templates, script-api, script-check | --note does not apply to backtest \{sub} — that flag WRITES a label, a note or a mark onto a stored run, which is what tag does |
--optimizer | a value outside the roster, in any case (CLI, before any dial or spawn; exit 2) — the ENGINE and the compute server answer with this same sentence, rendered from the same roster by the same function, so there is no longer a second wording to reach | invalid --optimizer "bogus" (expected grid|euler|tpe|genetic) |
--optimizer | typed on a profile that declares a [walkforward] table (CLI, exit 2) | --optimizer configures nothing on a walk-forward run: the wire verb it goes over carries the profile TOML and nothing else. A window's ranking is [walkforward].rank_by, in the profile, read by the one parser that runs it — and what each window searches is that same [walkforward] table's business, never a flag on this side. |
--optimizer | --optimizer genetic with no --seed (engine/server, via search_select::require_seed) | --optimizer genetic requires --seed <u64>. It is not defaulted, deliberately: a genetic search reports ONE sample of a distribution, and a seed nobody typed is a constant the result silently depends on — write --seed 7 (any u64) and the run is reproducible from your own shell history |
--optimizer | --local, on a profile with no [paramscan] table (the spawned engine, exit 2; \{asked} is the space-joined subset of --optimizer --euler-depth --trials --seed --keep-trials --resume that argv actually carried, \{profile_path:?} the Debug-quoted profile path) | backtest: {asked} names a parameter SEARCH, but {profile_path:?} has no [paramscan] table — there is nothing to search. Add one, or drop the search flags |
--optimizer | remote, on a profile with no [paramscan] table (compute server Response::Error; the literal contains a real newline before fast = …) | profile has no [paramscan] table — a parameter search needs a grid, e.g. [paramscan] fast = [5, 10, 15] |
--optimizer | any non-grid method against a daemon that does not advertise search_method — refused locally, NOTHING sent (\{:?} renders the daemon's advertised feature list) | backtest daemon does not advertise search_method (advertised: {features:?}) — nothing was sent. A search METHOD (--optimizer/--euler-depth/--trials/--seed) and --rank-by multi need a newer vike-backend backtest --addr; this verb is capability-negotiated, not version-gated. An older daemon would DROP the field, run the exhaustive grid and report success, which is the silent downgrade this refusal exists to prevent — upgrade the daemon, or drop the flag to search the grid on the server. |
--out | path, tag, diff, gate, params, strategies, script-api, script-check | --out names a FILE to write a rendered document to, and backtest \{sub} prints a line the shell can already redirect |
--out | templates — the same rule with a POINTER appended, because an operator typing it there is trying to save a starter and a bare > would clobber the strategy they are editing | --out names a FILE to write a rendered document to, and backtest templates prints a line the shell can already redirect — a starter is SAVED with --write-strategy, which refuses an existing path rather than overwriting one |
--out | ls or show, when the file cannot be written | cannot write {file}: {io_error} |
--param | the value carries no = | --param takes k=v, got {pair:?} (e.g. --param size=1.5) |
--param | the key is empty or whitespace (refused HERE so the message names --param rather than the --set grammar) | --param takes k=v with a non-empty key, got {pair:?} (e.g. --param size=1.5) |
--param | the key contains a . | --param {key}: [strategy.params] is a FLAT knob table — use --set strategy.params.{key}=… if you really mean a nested key |
--param | the value looks like a search axis — it starts with [, or it is three colon-separated parts that all parse as f64 | --param {key}={raw}: a RANGE declares a search axis, which this command cannot build yet — declare it as a [paramscan] table in a --profile, or pass a single value. (--set strategy.params.{key}={raw} sets it as a literal value if that is what you meant.) |
--preset | the preset wraps its knobs in a [params] table (exactly one key, and it is a table named params) — the message is prefixed preset \{preset_path}: by execute | it wraps its knobs in a [params] table, so the strategy would receive one parameter called 'params' that nothing reads and every knob would keep its default. A preset IS the params table: delete the [params] header and leave the keys at the top level |
--preset | the preset defines src | it defines 'src', which is the strategy's SOURCE rather than one of its knobs — that is what --script is for. Delete the 'src' key |
--preset | the preset is not valid TOML | not valid TOML: {e} |
--preset | the preset is TOML but not a table | a preset must be a table of parameters |
--preset | the file cannot be read | cannot read preset {preset_path}: {e} |
--profile | a trailing --profile with nothing after it | {flag} requires a value |
--profile | the next token starts with -- | {flag} requires a value, but the next argument is another flag ({found}) |
--profile | the file cannot be read (in execute, exit rung 1) | cannot read profile {path}: {e} |
--profile | the file is not valid TOML (in build_profile_toml, lifted onto the USAGE rung by CliError::usage) | profile is not valid TOML: {e} |
--rank-by | a value outside the roster, in any case (CLI, exit 2, before any dial or spawn) — ⚠ this row used to carry TWO messages, because the CLI refused with --rank-by must be …, got "bogus" while the engine and the server refused the same value with the sentence below; both sides render it from one roster through one function now, so the pair collapsed into this row | invalid --rank-by "bogus" (expected sharpe|return|max_dd|equity|multi) |
--rank-by | typed on a profile that declares a [walkforward] table (CLI, exit 2) — that wire verb carries the profile TOML and nothing else | --rank-by configures nothing on a walk-forward run: the wire verb it goes over carries the profile TOML and nothing else. A window's ranking is [walkforward].rank_by, in the profile, read by the one parser that runs it — and what each window searches is that same [walkforward] table's business, never a flag on this side. |
--rank-by | --rank-by multi against a daemon that does not advertise search_method — refused locally, nothing sent | backtest daemon does not advertise search_method (advertised: {features:?}) — nothing was sent. A search METHOD (--optimizer/--euler-depth/--trials/--seed) and --rank-by multi need a newer vike-backend backtest --addr; this verb is capability-negotiated, not version-gated. An older daemon would DROP the field, run the exhaustive grid and report success, which is the silent downgrade this refusal exists to prevent — upgrade the daemon, or drop the flag to search the grid on the server. |
--script | params, given together with --strategy | --script and --strategy name two different sources for the same question — give exactly one |
--script | params, unreadable file | cannot read script {path}: {io_error} |
--script | params, script that will not compile | rhai compile error: {err} |
--script | script-check, given a file that does not compile (exit rung 1; the diagnostic is the PRODUCT and goes to stdout, not through this refusal) | error: {path} does not compile\n{rhai compile error} |
--script | ls, show, path, tag, diff, gate, strategies, templates, script-api | --script does not apply to backtest \{sub} — it names a .rhai FILE, whose knobs params lists and which script-check compiles |
--seed | --seed under grid or euler (METHOD_KNOBS row owners are ["tpe", "genetic"], joined with " or ") | --seed is a tpe or genetic flag, but this run selected the "grid" optimizer. It was silently discarded before; it is refused now, because a knob that configures a search you did not select cannot do what it says |
--seed | a non-u64 value | invalid --seed "abc" (expected a u64) |
--seed | --optimizer genetic with no --seed | --optimizer genetic requires --seed <u64>. It is not defaulted, deliberately: a genetic search reports ONE sample of a distribution, and a seed nobody typed is a constant the result silently depends on — write --seed 7 (any u64) and the run is reproducible from your own shell history |
--set | the value carries no = (split_once, first = only — the VALUE may contain further =) | --set takes key=value, got {pair:?} (e.g. --set engine.fee_rate=0.001) |
--set | the first dotted segment is not in PROFILE_TOP_LEVEL_KEYS — including the EMPTY head produced by a leading dot or an empty key. The trailing hole is PROFILE_TOP_LEVEL_KEYS.join(", "). | --set {key}: a profile has no \{head} table — the top-level keys are {PROFILE_TOP_LEVEL_KEYS.join(", ")} |
--set | any segment is empty after the head check passed (e.g. --set name.=1) — raised by set_profile_key inside build_profile_toml, lifted onto the USAGE rung | --set key {dotted_key:?} has an empty segment — a key is spelled table.key (or table.sub.key), with no leading, trailing or doubled dot |
--set | the path runs THROUGH a plain value; \{} is segs[..=i].join("."), the blocking prefix | \{segs[..=i].join(".")} is a plain value in the profile, so \{dotted_key} cannot nest under it |
--set | the leaf names a whole table | \{dotted_key} names a whole table in the profile, not a single value — set its leaves instead (\{dotted_key}.\<key>) |
--set | the profile root is not a TOML table | profile root is not a TOML table |
--show-effective | an inline value (--show-effective=1) | {flag} takes no value |
--sort | ls, with a direction suffix that is not asc/desc | --sort direction '{other}' is not one of: asc | desc |
--sort | ls, when the store is non-empty and NO run carries the field | --sort '{key}': no run carries that field. Manifest fields are run_id, kind, produced_by, started_at, finished_at, git_sha, config.path, config.name, report and detail.<path>; any other name is read as a top-level key of report.json. |
--sort | show, path, tag, diff, gate, params, strategies, templates, script-api, script-check | --sort does not apply to backtest \{sub} — that flag narrows, orders or shapes a LISTING, and ls is the listing |
--store | ⚠ typed WITHOUT --local, i.e. on the remote arm (CLI, exit 2). One line, no continuations. | --store applies to --local only — a remote run reads the SERVER's store |
--strategy | params, given together with --script | --script and --strategy name two different sources for the same question — give exactly one |
--strategy | params, a name no roster carries | no built-in strategy named '{name}'. The roster is: {roster joined by ", " — PORTABLE_STRATEGIES, then SIMULATOR_ONLY names, then SCRIPT_ONLY names} |
--strategy | ls, show, path, tag, diff, gate, strategies, templates, script-api, script-check | --strategy does not apply to backtest \{sub} — that flag names the built-in strategy whose keys params lists |
--symbol | any empty element after trimming — a leading, trailing or doubled comma, or an empty value. The flag name in the message is HARD-CODED --symbol even though the arm is generic over Shape::StrList (it is the only StrList row). | --symbol takes SYM[,SYM…] with no empty element, got {raw:?} |
--trades | ls, path, tag, gate, params, strategies, templates, script-api, script-check | --trades does not apply to backtest \{sub} — it names the stored trade ledger, which show renders and diff compares |
--trades | (any reading sub-verb, with a value) | --trades takes no value |
--trials | ⚠ --trials under --optimizer euler (and under grid, i.e. under no --optimizer at all) — refused from METHOD_KNOBS by refuse_unowned_knob, on BOTH routes. \{method:?} is the Debug of the method TOKEN, so it renders quoted. | --trials is a tpe flag, but this run selected the "euler" optimizer. It was silently discarded before; it is refused now, because a knob that configures a search you did not select cannot do what it says |
--trials | --trials with no --optimizer written at all (name falls back to DEFAULT_SEARCH_METHOD) | --trials is a tpe flag, but this run selected the "grid" optimizer. It was silently discarded before; it is refused now, because a knob that configures a search you did not select cannot do what it says |
--trials | a value that is not a positive integer, under tpe (engine/server) | invalid --trials "abc" (expected a positive integer) |
--trials | against a daemon that does not advertise search_method — refused locally, nothing sent | backtest daemon does not advertise search_method (advertised: {features:?}) — nothing was sent. A search METHOD (--optimizer/--euler-depth/--trials/--seed) and --rank-by multi need a newer vike-backend backtest --addr; this verb is capability-negotiated, not version-gated. An older daemon would DROP the field, run the exhaustive grid and report success, which is the silent downgrade this refusal exists to prevent — upgrade the daemon, or drop the flag to search the grid on the server. |
--venue | a missing or flag-shaped value | {flag} requires a value |
--where | the expression contains a '|' anywhere ({expr} = the whole raw expression, untrimmed) | --where '{expr}': this grammar has no OR — terms are joined by a comma, which means AND (kind=backtest,sharpe>1). Run ls twice for a disjunction. |
--where | a term between two commas is empty ({expr} = the whole raw expression; OP_HELP is the literal '>= | <= | != | ~ | = | > | <') | --where '{expr}': an empty term — terms are joined by a single comma (FIELD OP VALUE, with OP one of: >= | <= | != | ~ | = | > | <) |
--where | a term contains none of the seven operators ({term} = the TRIMMED term, not the whole expression) | --where '{term}': no operator — a term is FIELD OP VALUE, with OP one of: >= | <= | != | ~ | = | > | < |
--where | the text before the operator is empty, e.g. '>1' ({term} = the trimmed term, {op} = the matched operator) | --where '{term}': no field before '{op}' — a term is FIELD OP VALUE, with OP one of: >= | <= | != | ~ | = | > | < |
--write-profile | the path already exists (USAGE rung, exit 2) | --write-profile {path} already exists, and this command will not overwrite a profile — delete it, or name a different path |
--write-profile | the write fails (exit rung 1) | cannot write the profile to {path}: {e} |
--write-strategy | the path already exists (USAGE rung, exit 2) | --write-strategy {path} already exists, and this command will not overwrite a strategy — delete it, or name a different path |
--write-strategy | given with no starter id (USAGE rung, exit 2; \{roster} is the derived id list) | --write-strategy {path} needs a starter to write — name one: vike-cli backtest templates \<id> --write-strategy \{path}. The ids are: {roster} |
--write-strategy | the write fails (exit rung 1) | cannot write the strategy to {path}: {io_error} |
--write-strategy | ls, show, path, tag, diff, gate, params, strategies, script-api, script-check | --write-strategy does not apply to backtest \{sub} — that flag SAVES a shipped starter to a new file, and templates is the verb that emits one |
--write-strategy | run — a DIFFERENT answer, because run has its own parser and this token is not in it | unknown argument: --write-strategy |
--explain-data | an inline value (--explain-data=1) | {flag} takes no value |
--require-coverage | an inline value (--require-coverage=1) | {flag} takes no value |
--max-gap | a blank value | --max-gap requires a duration, e.g. --max-gap 1d (or 4h, 900000) |
--on-gap | a value outside the roster | --on-gap {value} is not one of refuse | warn | run |
--universe | a value outside the roster | --universe {value} is not one of declared | covered | strict |
Combined with a flag it contradicts (7)
| flag | when | message |
|---|---|---|
--addr | --addr combined with --local (CLI, exit 2) | --addr names a remote backtest daemon, so it cannot be combined with --local drop one: --local runs the engine on this machine, --addr ships the profile to a server |
--decide | simultaneous beside an armed [risk] table or engine.leverage (the far side, BacktestProfile::refusals) — a DECLARED GAP rather than a preference: with a pre-trade gate armed the symbol judged first faces the whole step budget, and the fix is a step-boundary snapshot that is not built | engine.decide = "simultaneous" is not built for a run with a pre-trade risk gate: remove [risk] and engine.leverage, or keep engine.decide … |
--html | beside any other document or section flag on show | --html and {flag} ask for two different documents: --html renders this run's whole tearsheet as one HTML page, and {flag} selects or exports part of it. Which one wins would be a guess, so neither does — run backtest show \<run> --html --out sheet.html, then backtest show \<run> \{flag}. |
--local | --local combined with --addr (CLI, exit 2). The literal carries a real newline; the line continuations strip their own leading whitespace, so the second line begins at drop one:. | --addr names a remote backtest daemon, so it cannot be combined with --local drop one: --local runs the engine on this machine, --addr ships the profile to a server |
--metrics | show, beside --metrics-list | --metrics-list and --metrics ask for two different documents: the first lists what this tree CAN measure (the catalog, with no run involved at all), the second renders part of one stored run. Which of the two wins would be a guess, so neither does — run backtest show --metrics-list on its own, then backtest show \<run> --metrics. |
--metrics-list | show, beside --metrics | --trades | --config | --export (\{flag} is whichever was given) | --metrics-list and {flag} ask for two different documents: the first lists what this tree CAN measure (the catalog, with no run involved at all), the second renders part of one stored run. Which of the two wins would be a guess, so neither does — run backtest show --metrics-list on its own, then backtest show \<run> \{flag}. |
--metrics-list | show, beside --json | --metrics-list has no --json rendering: it is an aligned table for a human, and nothing in this tree serializes the metric catalog. For the machine-readable answer use the published surface asset — vike-cli surface --out DIR writes cli.json, whose rosters carry the metric ids. backtest show --metrics-list prints the table. |
Written without a value (10)
| flag | when | message |
|---|---|---|
--addr | written with no value | --addr requires a value |
--against | gate, absent or blank | backtest gate needs --against \<run|mark> — every criterion is RELATIVE to a baseline, so there is nothing to judge without one. Make a stable baseline with vike-cli backtest tag \<run> --as baseline/\<name>, then gate --against @baseline/\<name>. (For an ABSOLUTE threshold this grammar cannot spell, pipe vike-cli backtest show \<run> --json through jq.) |
--engine | written with no value | --engine requires a value |
--euler-depth | written with no value, on the engine's argv path | --euler-depth was written with no value (expected an integer). A trailing --euler-depth read as ABSENT before, which silently ran the default instead of refusing — write --euler-depth \<value> |
--optimizer | written with no value (trailing --optimizer, or --optimizer followed by another -- token) | --optimizer requires a value, but the next argument is another flag (--json) |
--out | (any reading sub-verb, no value) | --out requires a value |
--seed | written with no value, on the engine's argv path | --seed was written with no value (expected a u64). A trailing --seed read as ABSENT before, which silently ran the default instead of refusing — write --seed \<value> |
--store | written with no value | --store requires a value |
--trials | written with no value, on the engine's argv path (required_value) | --trials was written with no value (expected a positive integer). A trailing --trials read as ABSENT before, which silently ran the default instead of refusing — write --trials \<value> |
--write-strategy | written with no value | --write-strategy requires a value |
Used on a sub-verb that does not take it (3)
| flag | when | message |
|---|---|---|
--cash | params | --cash does not apply to params, which reads the script or the strategy roster on this machine and lists its knobs — no server, no store, no engine. It belongs to vike-cli backtest run |
--cash | run | (ACCEPTED — no refusal. It pushes an Override with key engine.cash, value parse_scalar(raw), origin Origin::Sugar("--cash").) |
--decide | params | --decide does not apply to params, which reads the script or the strategy roster on this machine and lists its knobs — no server, no store, no engine. It belongs to vike-cli backtest run |
Accepted values
Where a flag takes one of a closed set, this is the set.
| flag | accepts |
|---|---|
--kind | bar, tick |
--cols, --where | run_id, kind, produced_by, started_at, finished_at, git_sha, config.path, config.name, report, detail.<dotted.path>, <any other name> = a TOP-LEVEL key of report.json |
--optimizer | grid, euler, tpe, genetic |
--set | name, data, engine, strategy, risk, paramscan, sweep, walkforward |
--rank-by | sharpe, return, max_dd, equity |
--against | <run-id>, <unique-prefix>, @last, @last:<kind>, @<mark> |
--export | trades, equity |
--sort | asc, desc |
--on-gap | refuse, warn, run |
--universe | declared, covered, strict |
⚠ --rank-by — multi is recognised and refused: ⚠ NOT refused by today's engine-side --rank-by parser. Both routes resolve through crates/vike-backtest/src/harness/search_select.rs's resolve_rank, whose FIRST arm is Some(s) if s.eq_ignore_ascii_case("multi") — the engine binary reaches it via parse_search_flags and the compute server via run_paramscan_profile, so --rank-by multi is accepted on --local and on --addr alike (crates/vike-cli/tests/search_walkforward_cli.rs's every_search_knob_now_reaches_the_dial drives ["--rank-by", "multi"] to a CONNECT failure, exit 3, proving it was not a usage error). It IS refused by the FOUR-ARM parser crates/vike-backtest/src/harness/sweep.rs's RankMetric::from_str_ci, which returns None for it — and that parser is what a daemon predating the search_method capability uses, and what crates/vike-backtest/src/harness/profile.rs's WalkforwardCfg::rank_metric uses for the PROFILE key [walkforward].rank_by. Verbatim from that profile-key path: unknown walkforward.rank_by "multi" (want sharpe \| return \| max_dd \| equity; absent = sharpe). Against an old daemon the CLI never gets that far — crates/vike-datahub-client/src/client.rs's run_paramscan_profile pre-refuses rank_by == "multi" locally under FEATURE_SEARCH_METHOD, without sending.
⚠ --export — fills is recognised and refused: --export fills is not available — fills are not stored at ANY size. A backtest computes them, consumes them into report.json's scalars and drops them; only the trade LEDGER and the equity curve survive. --export trades and --export equity both work today.
⚠ --kind — HAND-WRITTEN COPY of the two spellings DataKind accepts, for the same reason as the roster above — this crate links no engine crate. Checked through the shared one_of helper, which is a SPELLING check and never a second implementation of what the value MEANS. The comparison is roster.contains(&value) on the already-trimmed string, so it is case-SENSITIVE.
⚠ --set — HAND-WRITTEN COPY, deliberately. crates/vike-cli/Cargo.toml states this crate links no vike-backtest and no engine crate, so BacktestProfile is not nameable here at all and nothing can derive it. It is the FIRST-SEGMENT roster only — a full key roster would be ~90 engine.* names against a schema that grew sixteen fields in one PR. base_dir is deliberately absent (it is #[serde(skip)], not a TOML key). BOTH paramscan and sweep are members: owner ruling R2 renamed the section and the loader keeps sweep as a PERMANENT serde alias, so an omitted alias row would make --set sweep.fast=… a CLI usage error against a key the engine accepts. Gated from the test tree (where the dev-dependency reaches) by crates/vike-cli/tests/backtest_flags_schema.rs's every_cli_top_level_key_is_one_the_profile_declares.
⚠ --on-gap — HAND-WRITTEN COPY of vike_backtest::data_plan::OnGap::NAMES, for the reason data_kinds and profile_top_level_keys above give: this crate links no engine crate, so that function is not nameable here and nothing can derive it. ⚠ This row claimed derived_from: crates/vike-backtest/src/harness/profile.rs's DataCfg::on_gap and that was a false claim of derivation — the field's own doc reserves Some for a path the members come FROM, and these are a literal. What IS gated is the direction this crate can reach: the_data_rosters_are_exactly_what_the_cli_validator_accepts holds these members equal to crates/vike-cli/src/cmd/data.rs's ON_GAP_VALUES, in order, by rendering the refusal that prints it — so the published roster and the validator an operator actually meets cannot differ. RESIDUAL, declared rather than assumed away: nothing compares either of them to the ENGINE, so a FOURTH disposition added to DataCfg::on_gap leaves --on-gap \<new> a local usage error against a value the profile loader accepts. The engine-side leg belongs in crates/vike-cli/tests/backtest_flags_schema.rs, where the vike-backtest dev-dependency already reaches and every_cli_top_level_key_is_one_the_profile_declares is the pattern.
⚠ --universe — HAND-WRITTEN COPY of vike_backtest::data_plan::UniverseMode::NAMES, on gap_dispositions' terms exactly — including the false derivation claim this row also carried, and the same declared RESIDUAL on the engine-side direction. Gated as far as this crate reaches by the_data_rosters_are_exactly_what_the_cli_validator_accepts.
A further 16 closed sets back the parser itself rather than any one flag.
Exit codes, and where this came from
The ladder
Every verb answers on this ladder, so a script branches on the number rather than on the text.
| code | name |
|---|---|
0 | Ok |
1 | Failed |
2 | Usage |
3 | Connect |
4 | Refused |
5 | Venue |
6 | Breach |
7 | Empty |
Read out of crates/vike-cli/src/exit.rs's Exit (#[repr(u8)]; the discriminants ARE the exit codes, converted by impl From\<Exit> for ExitCode)
Where each of these was read from
54 flags, each with the source it was rendered from — a path and the symbol inside it, never a line number.
| flag | read from |
|---|---|
--add | crates/vike-cli/src/cmd/backtest.rs's parse_read (the "--add" arm calls a.add.push, with the ⚠ comment naming it the only repeatable one); crates/vike-cli/src/cmd/runs/tag.rs's run_tag |
--addr | crates/vike-cli/src/cmd/backtest.rs's resolve_addr, Args::addr and execute; crates/vike-config/src/config.rs's DEFAULT_BACKTEST_ADDR, Config::backtest_addr, BACKTEST_ADDR_ENV and check_env_addr; crates/vike-cli/src/lib.rs's Resolved::backtest_addr |
--against | crates/vike-cli/src/cmd/runs/gate.rs's refuse_an_ungateable_line and run_gate |
--all | crates/vike-cli/src/cmd/runs/diff.rs's run_diff (let show_all = a.all;) |
--as | crates/vike-cli/src/cmd/runs/tag.rs's refuse_a_tag_that_writes_nothing (the valid_mark_name(name).map_err(|why| format!("--as \{why}")) call) and run_tag; crates/vike-model/src/runs.rs's valid_mark_name |
--cash | crates/vike-cli/src/cmd/backtest.rs's SUGAR (the Sugar \{ flag: "--cash", key: "engine.cash", shape: Shape::Scalar } row), PARAMS_REFUSED, refuse_a_run_flag_on_params, and parse_read's final other if other.starts_with("--") arm; implied_defaults's ⚠ paragraph |
--changed-only | crates/vike-cli/src/cmd/backtest.rs's ReadArgs::changed_only (its doc: "the DEFAULT, accepted so a script that says what it means is not refused"); crates/vike-cli/src/cmd/runs/diff.rs's run_diff never reads the field |
--cols | crates/vike-cli/src/cmd/runs/ls.rs's run_ls (the a.cols split) and DEFAULT_COLS |
--config | crates/vike-cli/src/cmd/runs/show.rs's show_text (the config || all branch) and that module's doc ⚠ --config says what is NOT recorded |
--decide | crates/vike-cli/src/cmd/backtest.rs's SUGAR (the engine.decide row) and parse_run_args (the Shape::Str arm, whose ⚠ argues why this value is not checked here); crates/vike-backtest/src/harness/profile.rs's EngineCfg::decide and decide_mode; crates/vike-backtest/src/engine/sim_broker.rs's DecideMode |
--engine | crates/vike-cli/src/cmd/backtest.rs's Args::engine, parse_run_args's remote-arm refusal loop and execute_local; crates/vike-cli/src/cmd/engine.rs's locate, ENGINE_BIN and cannot_spawn |
--euler-depth | crates/vike-cli/src/cmd/backtest.rs's Args::euler_depth and parse_run_args's "--euler-depth" arm; crates/vike-backtest/src/harness/search_select.rs's parse_euler_depth; crates/vike-backtest/src/search.rs's EulerConfig::DEFAULT_MAX_DEPTH and EulerConfig::MAX_DEPTH_CAP |
--export | crates/vike-cli/src/cmd/runs/show.rs's export_document and EXPORTABLE; run_show's match &a.export |
--fail-if | crates/vike-cli/src/cmd/runs/gate.rs's refuse_an_ungateable_line; crates/vike-cli/src/cmd/runs/failif.rs's parse_fail_if and that module's grammar block |
--fee | crates/vike-cli/src/cmd/backtest.rs's SUGAR (row --fee -> engine.fee_rate, Shape::Scalar); crates/vike-cli/src/cmd/backtest.rs's parse_scalar |
--from | crates/vike-cli/src/cmd/backtest.rs's SUGAR and its Shape::Str doc; pinned by crates/vike-cli/src/cmd/backtest.rs's a_bare_epoch_ms_date_stays_a_string_through_the_sugar_flag |
--help | crates/vike-cli/src/cmd/backtest.rs's parse_read (the "-h" | "--help" arm calls args::help_requested); crates/vike-cli/src/cmd/args.rs's HELP_SENTINEL and exit_for_parse_error |
--html | crates/vike-cli/src/cmd/runs/show.rs's html_document and refuse_a_second_document; the parser arm in crates/vike-cli/src/cmd/backtest.rs's parse_read. |
--interval | crates/vike-cli/src/cmd/backtest.rs's parse_run_args (the s.flag == "--interval" special case inside the Shape::Str arm); crates/vike-model/src/time.rs's interval_ms |
--json | crates/vike-cli/src/cmd/backtest.rs's Args::json, parse_run_args's "--json" arm, parse_read's "--json" arm, ReadArgs::json, execute, execute_local and execute_read; crates/vike-cli/src/cmd/runs/gate.rs's run_gate |
--kind | crates/vike-cli/src/cmd/backtest.rs's parse_run_args (the s.flag == "--kind" special case calls one_of with DATA_KINDS); crates/vike-cli/src/cmd/backtest.rs's implied_defaults |
--limit | crates/vike-cli/src/cmd/runs/ls.rs's run_ls (the a.limit parse + truncate) |
--local | crates/vike-cli/src/cmd/backtest.rs's Args::local, parse_run_args's "--local" arm, execute and execute_local |
--md | crates/vike-cli/src/cmd/runs/diff.rs's run_diff (the if a.json \{ … } else if a.md \{ … } ladder) |
--metrics | crates/vike-cli/src/cmd/runs/show.rs's show_text and run_show; crates/vike-cli/src/cmd/backtest.rs's refuse_foreign_read_flags (show_only) |
--metrics-list | crates/vike-cli/src/cmd/runs/show.rs's run_show (the a.metrics_list short-circuit), refuse_a_listing_beside_a_run_rendering and run_rendering_flags_given; crates/vike-cli/src/cmd/backtest.rs's parse_read (the "--metrics-list" arm) and ReadArgs::metrics_list; crates/vike-analytics/src/metric_catalog.rs's metric_list_text, METRICS and ABSENT |
--note | crates/vike-cli/src/cmd/runs/tag.rs's run_tag (passes a.note to both add_tags and write_mark) and that function's ⚠ doc |
--optimizer | crates/vike-cli/src/cmd/backtest.rs's parse_run_args (the "--optimizer" arm, which calls accept_value); crates/vike-datahub-client/src/flag_vocab.rs's accept_value and its --optimizer row, whose values IS the protocol const; crates/vike-cli/src/cmd/backtest.rs's DEFAULT_OPTIMIZER (cfg(test)); crates/vike-datahub-client/src/proto.rs's SEARCH_METHODS and DEFAULT_SEARCH_METHOD; crates/vike-backtest/src/harness/search_select.rs's resolve. ⚠ This row cited a local OPTIMIZERS const and one_of until the client adopted the shared vocabulary; that const was already = SEARCH_METHODS, so its MEMBERS never drifted — its MATCH RULE did, which is the field below. |
--out | crates/vike-cli/src/cmd/backtest.rs's refuse_foreign_read_flags (the !matches!(a.sub, ReadSub::Ls | ReadSub::Show) && a.out.is_some() rule); crates/vike-cli/src/cmd/runs/ls.rs's run_ls; crates/vike-cli/src/cmd/runs/show.rs's run_show |
--param | crates/vike-cli/src/cmd/backtest.rs's parse_run_args (the "--param" arm); crates/vike-cli/src/cmd/backtest.rs's looks_like_a_search_axis |
--preset | crates/vike-cli/src/cmd/backtest.rs's merge_preset_params; crates/vike-cli/src/cmd/backtest.rs's parse_run_args (the "--preset" arm) |
--profile | crates/vike-cli/src/cmd/backtest.rs's parse_run_args (the "--profile" arm assigns profile_path = Some(flags.value(&flag, inline)?)); crates/vike-cli/src/cmd/backtest.rs's execute reads it into base and hands it to build_profile_toml |
--rank-by | crates/vike-cli/src/cmd/backtest.rs's Args::rank_by, Args::search_requested and parse_run_args's "--rank-by" arm (which calls accept_value); crates/vike-datahub-client/src/flag_vocab.rs's RANK_METRICS and accept_value; crates/vike-backtest/src/harness/search_select.rs's resolve_rank; crates/vike-backtest/src/harness/sweep.rs's RankMetric |
--script | crates/vike-cli/src/cmd/backtest.rs's inject_script_src and its SRC_KEY; crates/vike-cli/src/cmd/backtest.rs's implied_defaults and the named --script rule in refuse_foreign_read_flags; crates/vike-cli/src/cmd/strategies.rs's run_script_check |
--seed | crates/vike-cli/src/cmd/backtest.rs's Args::seed and parse_run_args's "--seed" arm; crates/vike-backtest/src/harness/search_select.rs's parse_seed, require_seed and resolve |
--set | crates/vike-cli/src/cmd/backtest.rs's parse_run_args (the "--set" arm computes let head = key.split('.').next().unwrap_or("") and tests PROFILE_TOP_LEVEL_KEYS.contains(&head)); crates/vike-cli/src/cmd/backtest.rs's set_profile_key |
--show-effective | crates/vike-cli/src/cmd/backtest.rs's execute (if args.show_effective \{ print!(...); return Ok(()); }); crates/vike-cli/src/cmd/backtest.rs's render_effective |
--slippage | crates/vike-cli/src/cmd/backtest.rs's SUGAR (row --slippage -> engine.slippage, Shape::Scalar) |
--sort | crates/vike-cli/src/cmd/runs/ls.rs's sort_rows |
--store | crates/vike-cli/src/cmd/backtest.rs's Args::store, parse_run_args's remote-arm refusal loop and execute_local; crates/vike-backtest/src/backtest_cli.rs's store_root |
--strategy | crates/vike-cli/src/cmd/backtest.rs's SUGAR (row --strategy -> strategy.name); pinned by crates/vike-cli/src/cmd/backtest.rs's a_strategy_name_is_forwarded_unvalidated; the conditional default is crates/vike-cli/src/cmd/backtest.rs's implied_defaults |
--symbol | crates/vike-cli/src/cmd/backtest.rs's parse_run_args (the Shape::StrList arm accumulates into symbols; the post-loop block pushes one Override with Origin::Sugar("--symbol")); pinned by crates/vike-cli/src/cmd/backtest.rs's symbol_is_repeatable_and_comma_splitting_and_yields_one_override |
--to | crates/vike-cli/src/cmd/backtest.rs's SUGAR (row --to -> data.to, Shape::Str) |
--trades | crates/vike-cli/src/cmd/backtest.rs's refuse_foreign_read_flags (the !matches!(a.sub, ReadSub::Show | ReadSub::Diff) && a.trades rule, and the ⚠ comment above show_only saying why it is not in that array); crates/vike-cli/src/cmd/runs/show.rs's show_text; crates/vike-cli/src/cmd/runs/diff.rs's run_diff |
--trials | crates/vike-cli/src/cmd/backtest.rs's Args::trials and parse_run_args's "--trials" arm; crates/vike-cli/src/cmd/backtest.rs's execute_local; crates/vike-backtest/src/harness/search_select.rs's parse_trials and METHOD_KNOBS; crates/vike-backtest/src/harness/tpe.rs's DEFAULT_TRIALS |
--venue | crates/vike-cli/src/cmd/backtest.rs's SUGAR (row --venue -> data.venue, Shape::Str); crates/vike-backtest/src/harness/profile.rs's DataCfg |
--where | crates/vike-cli/src/cmd/backtest.rs's refuse_foreign_read_flags (ls_only); crates/vike-cli/src/cmd/runs/where_expr.rs's OPS and filter |
--write-profile | crates/vike-cli/src/cmd/backtest.rs's execute (the args.write_profile block sits BELOW both client-side rewrites and above the show_effective return) |
--write-strategy | crates/vike-cli/src/cmd/strategies.rs's run_templates; crates/vike-cli/src/cmd/backtest.rs's ReadArgs::write_strategy and the templates_only roster in refuse_foreign_read_flags |
--explain-data | crates/vike-backtest/src/data_plan.rs's plan_data and explain_lines; crates/vike-cli/src/cmd/data.rs's explain_data_override |
--require-coverage | crates/vike-backtest/src/data_plan.rs's enforce and coverage_verdict; crates/vike-data/src/coverage.rs's window_shortfall |
--max-gap | crates/vike-backtest/src/data_plan.rs's CoverageGate; crates/vike-cli/src/cmd/data.rs's max_gap_override |
--on-gap | crates/vike-backtest/src/data_plan.rs's CoverageVerdict; crates/vike-backtest/src/harness/profile.rs's DataCfg::on_gap |
--universe | crates/vike-backtest/src/data_plan.rs's universe_verdict and UniverseMode; crates/vike-backtest/src/harness/profile.rs's DataCfg::universe |
Related
Parameter search and walk-forward are not sub-verbs of backtest — both are backtest run with a different profile table — and moved off this page's bar entry for that reason. They cover the [paramscan] and [walkforward] tables named above in full.
indicators
Print the indicators a Rhai strategy can actually call — the bound set, never the registry — with their parameters, their defaults and your own files listed beside them.
backtest --profile
Every table, key, default, accepted value and refusal of the TOML file vike-cli backtest --profile takes, derived from the Rust types that declare the schema.