vike
Research

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.

On this page