vike
Scripting

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:

callrecords
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) and sma(30) are two instances; sma() and sma(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 and Indicator has 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 by Strategy::on_bar before it runs the on_bar hook. on_start and on_stop run their hooks without touching it.
  • intents — the output, a Vec<Intent> in push order.
  • overrides and params_seen — the sweepable knobs: overrides is injected before compile's one-time top-level run, and param() records each (name, default) into params_seen, first seen wins. Neither is touched by compile's intent/indicator clear, because a swept value must persist and discovery reads params_seen after 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.

On this page