The order vocabulary
The Intent verbs a script records and the ScriptCtx they land in — what the reads snapshot, what the verbs push, and how a bar's intents become broker calls.
A script never touches a broker. crates/vike-script/src/ctx.rs describes the arrangement in its
header: ScriptCtx is the shared, lock-guarded state the engine's host functions read and
mutate, owned by the strategy through SharedCtx = Arc<RwLock<ScriptCtx>> and never a borrowed
&mut B. Each Rhai closure registered onto the engine holds a cloned Arc, so it can push an
Intent without borrowing the strategy itself. The order verbs therefore record; the strategy
drains what was recorded into real orders once the hook returns.
The two intents
Intent is the whole order vocabulary, and it is pure data — one enum with two variants:
pub enum Intent {
Market { side: i32, qty: f64 },
Limit { side: i32, qty: f64, price: f64 },
}Its doc calls it one order-verb call recorded by a Rhai script during a bar's evaluation, with no
broker or strategy wiring of its own. Four host functions produce them, registered by engine.rs's
register_verbs:
| call | records |
|---|---|
buy(qty) | Market { side: 1, qty } |
sell(qty) | Market { side: -1, qty } |
market(side, qty) | Market { side, qty } — side is 1 or -1 |
limit(side, qty, price) | Limit { side, qty, price } |
side arrives from the script as a Rhai integer and is cast to the i32 the intent carries. There
is no cancel, modify or bracket verb in this vocabulary: the enum has these two variants, and the
seam is the single-symbol MVP that sym()'s doc describes.
What the reads see
The context's read fields are snapshotted before each hook evaluation — cur_bar, position,
price, equity, index, now — and register_reads exposes each as a zero-argument getter:
close(), open(), high(), low() and volume() read cur_bar; position(), price() and
equity() return f64; index() and now() return i64. run_hook in strategy.rs fills
them from the broker: equity(), index() and now() on every call, position() and price()
only when there is a symbol to ask about. The doc gives the reason: before the first bar arrives —
inside on_start — the symbol is empty, and a real SimBroker/LiveBroker panics on an unknown
symbol, which the strategy's own fail-safe does not catch. So a pre-trade on_start reads
position() and price() at their prior value, 0.0 on the very first call.
The symbol itself comes from the current bar's tag (Bar::symbol), resolved by sym(); this is
the single-symbol MVP, and Strategy::on_bar sets cur_bar fresh every bar.
The rest of the context
The remaining fields are the engine's, not the script's, but they explain behaviour a script author will observe:
indicators— the streaming indicator cache, fed once per bar on reference, keyed on the instance's real identity"<name>:<coerced params>", e.g."sma:[20.0]".sma(5)andsma(30)are two instances;sma()andsma(20)are one, because the registry default coerces to the same slice.user_indicators— the user-written ones, held concrete rather than boxed beside the built-ins, because a per-bar fault has to be readable back out andIndicatorhas no downcast. Keyed on"<name>:<call-site args>", the same identity-not-spelling rule, so every output line of one indicator shares one entry and one feed.fed_this_bar— the set of keys already advanced this bar, cleared byStrategy::on_barbefore it runs theon_barhook.on_startandon_stoprun their hooks without touching it.intents— the output, aVec<Intent>in push order.overridesandparams_seen— the sweepable knobs:overridesis injected before compile's one-time top-level run, andparam()records each(name, default)intoparams_seen, first seen wins. Neither is touched by compile's intent/indicator clear, because a swept value must persist and discovery readsparams_seenafter compile.
From intent to order
run_hook is where the vocabulary meets the broker. It snapshots the reads, clears intents,
calls the named zero-argument hook, and then takes the recorded intents and submits them in order:
Intent::Market { side, qty } becomes broker.submit_market(&sym, side, qty) and
Intent::Limit { side, qty, price } becomes broker.submit_limit(&sym, side, qty, price). If the
hook errors, the intents it recorded before the error are discarded and nothing is submitted that
bar — the fail-safe Writing a strategy describes.
A script holds no cross-bar state of its own
strategy.rs's header states the invariant: scripts hold no cross-bar mutable state — all state
is host-side, the broker's position() plus the indicator caches in ScriptCtx. A hook body's
let-declared locals live only for that one call. The idiomatic loop is therefore to read the
position, compute a target, and trade the difference, which is how the shipped sma_cross
example is written. Script-managed persistent state is named there as a follow-up, not a feature.
on_fill is a no-op in this tier: the strategy impl's own comment says a script observes its
resulting position via position(), not a dedicated fill callback. Fills reach the script the
next time a hook snapshots the broker.
The cheatsheet lists every read and verb beside the indicator forms; The engine is where they are registered.
The engine
The resource-limited rhai::Engine a strategy script runs in — what build_engine registers, what it refuses, why an unbound indicator is unbound, and where a script's print goes.
Writing a strategy
The compiled-script strategy — the three hooks, param() knobs, why the top level runs exactly once, the fail-safe, and the three ways a finished script is run.