Rust studies
The compiled study tier — a study as a folder under user_data compiled into the operator's own binary by a build script, the entry contract, the dependency table that is its whole API, and what a study is deliberately handed and denied.
vike-user-research is the compiled user-study host. crates/vike-user-research/src/lib.rs
states the mechanism in one sentence: build.rs scans
<workspace>/user_data/research/studies/rust/<name>/<name>.rs (VIKE_USER_DATA_DIR overrides
the root at build time) and generates the registry the crate re-exports — USER_STUDIES,
user_study_entry and run_user_study — copied from the strategy tier's vike-user-strategies
rather than reinvented. A study folder holds the entry file, whose stem must equal the folder name,
plus *.toml recipes the caller loads. The interpreted twin is
Rhai studies.
The entry contract
use vike_user_research::{StudyContext, StudyError, StudyOutcome};
pub fn run(ctx: &StudyContext, params: &toml::Value) -> Result<StudyOutcome, StudyError>crates/vike-user-research/src/contract.rs's StudyFn is that signature as a type, and the
generated registry coerces every entry to it (user_<name>::run as StudyFn), so a file whose
signature drifts fails the build naming the mismatch rather than failing at the call site of a
study somebody is waiting on. It is a function, not a trait — the design refused a Study trait as
wrong tool for one implementation; extract it from two — and non-generic on purpose, the one
place the contract diverges from the strategy tier's build<B: HftBroker>: a Rhai script cannot
carry a type parameter, and a study places no orders, so nothing about it is generic over a broker.
The load-bearing property of a separate crate is compilation: user entry files compile as modules
of this crate, a separate unit from vike-ml or vike-analytics, so they resolve only the
platform's public API. An internals refactor can never break a study, and a study can never grow a
load-bearing dependency on a pub(crate) detail. The contract types live here while there is one
study, and extern crate self as vike_user_research; lets a user file spell them as
vike_user_research::… regardless of where they eventually move.
What the scan accepts
crates/vike-user-research/src/codegen.rs is the pure scan, include!d by build.rs so the unit
tests exercise exactly the code the build runs. A folder holding .rs files but no <name>.rs is
an error — the operator's file is right there and nothing will ever compile it — while a folder
with no .rs at all is a recipes-only folder, skipped silently. The name must match
[a-z][a-z0-9_]*, because it becomes the registry name and the generated module name. A name
present in both tiers is a warning, since the interpreted tier is the one a binary install can
run and refusing to build the compiled twin would take away the resolution the operator still has.
There is no study.toml and no live flag: a study places no orders and its sanctioned surface
reaches no network, so a flag would gate nothing. build.rs fails the build outright on any scan
error, because an operator who dropped a malformed folder must hear about it at compile time, not
when a run reports "unknown study"; its scan root is a fixed hop from the crate's manifest
directory, deliberately not the runtime marker walk, because a build script runs in a source
checkout by construction.
What a study is handed
StudyContext is built by the caller — a binary, the only layer allowed to read the environment —
and never by a study. It carries the store, an optional learner, the run's window and a scratch
directory the caller owns and may delete afterwards. Its read verbs forward the store's read half
and nothing else: bars, quotes, trades, book_updates, depth (a store with no depth lane
refuses rather than answering empty), and the research series cohort and perp_metrics, each
named by stripping the store method's scan_/load_ prefix. The append half is unreachable by
construction: the crate takes vike-data feature-free, so the concrete store engine is not
compiled and a study holds no value it could append through.
Sanctioned surface, not a guarantee
contract.rs says it of itself: the read-only property belongs to the sanctioned surface. A user
file is ordinary Rust compiled into the operator's own binary, and nothing stops it opening a
socket. What the contract buys is that the cheapest path is the correct one, and that an
unsanctioned path has to be added to a manifest, in a diff. The Rhai tier is where the same rule is
actually enforced.
StudyOutcome is what comes back: metrics in emission order (a study puts its headline first),
duplicates refused, non-finite values accepted because a NaN Sharpe over a fold that never
traded is an observation; and text artifacts whose name becomes a file name in the run directory —
non-empty, at most MAX_ARTIFACT_NAME (64) bytes, [A-Za-z0-9._-] only, not starting with .,
so ../../secrets.env is refused by the charset rather than defended against by the writer.
StudyError has three variants split by who an operator has to talk to: Study is the author's,
Data carries vike_data::DataError itself (a From impl lets a body ? a store read through),
and NoLearner is the host's — the documented ceiling of a box with no LightGBM binary.
StudyLearner is vike_ml::Learner with its model type erased so it can cross a non-generic
boundary, and it erases the whole trait — fit, fit_captured, fit_identity — because a
two-method version would have silently removed two shipped capabilities: exporting the model a run
selected, and keying a cross-run fit cache. Models is the seam.
The dependency table is the API
Cargo.toml's [dependencies] is the user-study API surface — a user file resolves
use vike_ml::… against that manifest — and the header argues the absences, because nothing else
would record them. Present: vike-model; vike-indicators (the ColumnFeature study↔strategy
seam); vike-ml (the learner seam, pure-Rust inference, importance); vike-analytics (the signal
backtest, stats, the purged fold splitters); vike-data (the trait only); toml; and three that
arrived together for one named consumer — indexmap and rand_chacha as correctness, since an
insertion-ordered map and a portable RNG stream are bit-reproducibility machinery in this
workspace's convention, and rayon as speed. Absent on purpose: no HTTP client, because a
study reads the history store and the exploratory fetch is an ingest command — a throttled runtime
fetch does not fail a study, it becomes a number that flows into a feature and a Sharpe; no
vike-backfill, where the vendor clients live; no vike-backtest, illegal by layer and
wrong by kind, because a research Sharpe and an event-driven one must never quietly become one
number; no vike-report; no serde, because a study returns data and the caller
serialises it; no sha2/hex, because the fit cache's key is a method a study calls.
rayon sharpens the tier asymmetry: a Rhai study cannot express it, so a study that needs the
grid parallel is a Rust-tier study. That is not new — the interpreted tier can express no Learner
implementation, no generic and no #[cfg(test)] module either — and the tiers differ only in
reach, a property of compilation rather than a rule anyone chose.
CI and the empty state
No user_data/ — every CI checkout, every fresh clone — means an empty registry and an inert
crate: user_study_entry answers None for every name. The pipeline is still proven on every run
through the committed fixture tree, which drives a second generated registry end to end, including
a real fit through the shared learner double and a real read through the memory store, so the ML
half is proven on a box with no LightGBM binary. User code compiles only in a source checkout and
into the operator's own binary; user_data/ is gitignored. crates/vike-model/src/state_path.rs's
user_rust_studies_dir carries the operator consequence: the tier is meaningful only in a source
checkout, though it exists and lists on every install.
Running a study turns a registry name into a run, and
Authoring in Rust is the strategy tier this one mirrors.
Rhai studies
The interpreted study tier — the script contract, every host function a study may call, the operation ceiling, and the side channel that lets a typed store error survive the interpreter.
The run pipeline
The Studio's resolve → backtest → score pipeline, headless so CI tests it directly — the one strategy vocabulary it branches on, the data slice it loads, and the run, sweep, walk-forward and compare entry points over one store handle.