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:
| variant | when | message |
|---|---|---|
CompileFailed | the script did not compile, or has no fn on_bar(bar) | <path>: did not compile — <rhai error> |
NameConflict | the stem would shadow a built-in or a host function (user_indicator_conflict) | <path>: <reason> |
InSubdirectory | a .rhai inside a subdirectory of indicators/ | ignored — indicators/ is FLAT, one <name>.rhai per indicator. Move it up one level. |
DuplicateName | two 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. |
Unreadable | the 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'sinstall_user_indicatorsruns before any subcommand dispatches, so a script run by any verb can call them; rejections go to stderr, never stdout, becausevike-cli mcp's stdout is a protocol.- the standalone
backtestbin —crates/vike-backtest/src/backtest_cli.rsinstalls 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'sinstall_user_indicatorsdeliberately does not use the pair: it has two consumers of one load and needs the report, so it callsload_user_indicatorsonce and handschart_studies()to the chart's ƒx picker (unconditionally) andprototypes()to the strategy bindings (not in the desktop, which has no strategy runner). It runs aftervike_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.
Writing an indicator
A Rhai file under user_data/indicators is a real Indicator — the state-first contract, param() knobs, multi-output lines, chart placement, and what an indicator may never do.
User strategies
A user's strategies are files under user_data/strategies, not rows in a blob — the layout, the two trees, the scan that compiles every script, and its named diagnostics.