vike
Scripting

Loading indicators

How user_data/indicators/*.rhai becomes a process-wide set every strategy can call — the load report, its five diagnostics, the once-per-process install, and which binaries do it.

crates/vike-script/src/load.rs loads <project>/user_data/indicators/*.rhai — the user's own indicators — into compiled RhaiIndicators ready to hand to RhaiStrategy::compile_with_indicators. Its header calls it the sibling of the strategy loader and deliberately much smaller: an indicator is one file with no presets, so there is no folder-versus-entry-file rule and no rust/ tier.

The layout is flat

vike_model::state_path::INDICATORS_SUBDIR is indicators, and its doc explains the asymmetry with strategies/: a strategy owns .toml presets that must travel with it, while an indicator's only knob is a top-level let inside its own file, so a folder per indicator would be an empty directory wrapping one file. The file stem is the name a strategy calls it by; the .rhai extension is matched case-insensitively, because a Windows editor that saved MyThing.RHAI wrote a real indicator.

Every rejection is reported

load_user_indicators(root) returns an IndicatorLoadReport rather than a Vec, and the header gives the reason: a user's indicator failing to load is indistinguishable, from inside their strategy, from a typo in the call — both are function-not-found — so the load side is the only place that can say which. An absent root is an empty, clean report, not a diagnostic; a root that exists and cannot be read is one. The report holds root, the loaded indicators sorted by name, and diagnostics in a deterministic order — load_user_indicators sorts them on their Debug rendering, which begins with the variant, so they arrive grouped by kind — each IndicatorDiagnostic carrying the file so the user's next action is to open it:

variantwhenmessage
CompileFailedthe script did not compile, or has no fn on_bar(bar)<path>: did not compile — <rhai error>
NameConflictthe stem would shadow a built-in or a host function (user_indicator_conflict)<path>: <reason>
InSubdirectorya .rhai inside a subdirectory of indicators/ignored — indicators/ is FLAT, one <name>.rhai per indicator. Move it up one level.
DuplicateNametwo files share a stem (ema.rhai + ema.RHAI, which coexist on Linux) — neither loads `<name>` is claimed by N files (…) — neither is loaded, because which one won would depend on directory order. Rename one.
Unreadablethe directory or a file could not be read<path>: could not be read — <error>

The conflict check runs before compiling: a file named sma.rhai is refused for its name whether or not its body is valid, since a compile error for it would send the author to fix the wrong thing. Order is sorted, not filesystem order — two indicators cannot interact, so order changes no value, but it changes the diagnostic list, and a report that reshuffles between machines is one nobody can diff. is_clean() is "nothing was rejected", distinct from "loaded nothing".

Installed once, per process

install_user_indicators in engine.rs installs already-compiled prototypes process-wide, so every strategy the process compiles can call them — including through the harness's run_* entry points and the Studio runners, none of which take an indicator argument. The set is process-wide for the same reason vike_indicators::registry() is: the built-ins are a static no run path passes around, and this makes the user's own indicators the same kind of thing. Only a binary may call it, on the vike_log::init precedent — the function performs no I/O, so the caller owns the directory resolution. A second call returns Err carrying the already-installed count rather than silently keeping or replacing the first set, because two callers means two places believe they own it.

load_and_install_user_indicators(user_data_dir) is the pair a composition root wires in one call: join INDICATORS_SUBDIR, load, install, and return every message as data — one indicator not loaded — … line per diagnostic. Data rather than output, because a root whose stdout is a protocol must print on stderr, and a root logging through tracing cannot emit anything until its subscriber exists. Nothing here is a reason to fail: one half-edited file must not block a command with nothing to do with it, and an empty return is the normal outcome, including for a project with no indicators/ directory at all.

Which binaries install the set

  • vike-cli — crates/vike-cli/src/lib.rs's install_user_indicators runs before any subcommand dispatches, so a script run by any verb can call them; rejections go to stderr, never stdout, because vike-cli mcp's stdout is a protocol.
  • the standalone backtest bin — crates/vike-backtest/src/backtest_cli.rs installs before the profile load, which is the first step that can compile a script, with diagnostics on stderr so --json's machine report stays parseable.
  • vike-datahub — the server installs the set from its project's directory. Its doc carries the hazard below.
  • vike-desktop — crates/vike-desktop/src/main.rs's install_user_indicators deliberately does not use the pair: it has two consumers of one load and needs the report, so it calls load_user_indicators once and hands chart_studies() to the chart's ƒx picker (unconditionally) and prototypes() to the strategy bindings (not in the desktop, which has no strategy runner). It runs after vike_log::init, so every rejected file is reported, and before any window, so the picker is complete the first time it opens. The directory is the sibling of the resolved settings directory, not a fresh walk, so a relocated project cannot get its credentials from one place and its indicators from another.

In each, VIKE_USER_DATA_DIR names the directory outright and beats the project walk, so the binaries cannot disagree about which library this box has.

On a remote datahub, the server's set wins and the client cannot see it

datahub_cli.rs's install_user_indicators doc calls this a real semantic change. Before it existed, a client script calling my_ind() failed on the server with an unknown-function error, loud and unambiguous. Now it binds to whatever <server project>/user_data/indicators/my_ind.rhai contains: vike-cli backtest ships the script and never the indicator files, and no hash or version is exchanged, so the same script on two servers with different indicator directories returns different numbers with nothing in the response saying so. Rejections land in the operator's log, not the client's error.

What a script may type

installed_user_indicators() returns the installed names, but a name is not necessarily a callable spelling: a multi-output file whose line 0 is not its namesake has no bare name, exactly as bollinger has none. An advertising surface joins it with installed_user_bare_call, installed_user_line_accessors and installed_user_indicator_params, which is what vike-cli indicators does — it prints the user set in its own block, one row per callable spelling with the line it returns, and --json carries them under their own key with no category. Two declared limits in that command's header: --category never matches a user indicator, because it has none, and --name matches the file's own name rather than a per-line accessor. See vike-cli indicators.

Registration order is what makes the name rules necessary: build_engine registers the user set after the built-ins, and register_fn replaces, so a file called stochastic_k.rhai would otherwise take over that accessor for every strategy. user_indicator_conflict refuses it, and register_user_indicators skips a refused name rather than binding it. One residual is declared rather than closed: a collision between two user files — a.rhai declaring a line b, beside a file a_b.rhai — is invisible to the per-file check, and the later registration wins.

Writing the file itself is Writing an indicator.

On this page