vike
Concepts

Risk and arming

The gates between an order and the wire, in the order they run — the per-venue arming ceiling read before any credential, the credential gate behind it, the pre-trade RiskGate on every order, and the halt — with paper the default under all of them.

Nothing in Vike Trader reaches a real venue by accident. Between an order and the wire there are four checks, and the order they run in is the point. The first two decide whether a venue is mounted live at all; the third runs on every order after that; the fourth is the operator's stop. This page is written from crates/vike-config/src/venue_mode.rs, crates/vike-mount/src/lib.rs, crates/vike-exec/src/risk.rs and crates/vike-exec/src/halt.rs, each the authority for its own gate.

Gate 1 — the arming ceiling, read before any credential

<project>/settings/policy.toml carries a [venues] table:

[venues]
binance = "demo"
bybit   = "live"

Each value is a vike_config::VenueMode — paper < demo < live, an ordering encoded in the type's own variant declaration — and it is a ceiling. vike_mount::make_engine, the one entry point that builds a venue's execution client, consults it first, above the credential read: a paper venue returns the paper client having loaded no credential, fetched no instrument grid and opened no socket.

The ceiling can only ever refuse. VenueMode::cap is min, never max, and it is a named method rather than a .min() at each call site for one reason: a.max(b) is one character from a.min(b) and reads just as plausible, while the two differ by "the file can only refuse" versus "the file can arm real money".

// A `live` ceiling over a demo-credentialled venue stays DEMO — the ceiling never promotes.
assert_eq!(VenueMode::Live.cap(VenueMode::Demo), VenueMode::Demo);
// A `paper` ceiling over a live-credentialled venue REFUSES it down to paper.
assert_eq!(VenueMode::Paper.cap(VenueMode::Live), VenueMode::Paper);

So bybit = "live" does not put bybit live. It declines to stop bybit going live if its credentials and its mainnet flag already say so. The measured defect it closes: a daemon whose run profile mounted one venue logged nine authenticated live sessions, none of them asked for, all a consequence of credential pairs sitting in one file.

A box with no [venues] table mounts all paper

The default is a filled map, one paper per roster venue — including venues this build cannot mount, because a policy.toml must be portable between a GUI box and a daemon. It is the one settings default in the workspace that changes behaviour, and it is deliberate: absence of the file must be the safest answer. Because "everything at paper" and "nobody wrote the table" produce the same map, VenuePolicy::is_declared records whether a file ever named a venue, and vike_mount::venue_arming_migration warns once at startup — naming the file, the key and the venues it refused — rather than refusing to start.

The ceiling is also the only refusal that exists for a venue with no other switch: Aster's mount tries its live keys first and has no mainnet flag to withhold, which is the measured hole this gate was built to close. A second account on a venue gets its own line under [accounts], and a labelled account with no line resolves to paper — the accounts page carries that grammar. Every key on this page is documented on the policy settings reference.

Gate 2 — absent credentials are the live gate

Only once the ceiling allows it does the older rule apply. A venue whose {VENUE}_DEMO_* keys are present in the credential store spawns a real, credential-gated execution client; otherwise it stays paper — vike_paper::PaperExecutionClient, a resting book behind the same seam. Three refinements sit on this gate, each at the venue that needs it:

  • Mainnet is an explicit flag. {VENUE}_MAINNET is the exact string "1", read from the process environment or the store with the process winning. For the three CEX venues make_engine resolves it exactly once per mount (cex_mainnet_enabled) and threads a bool into the grid pre-fetch, the exec spawn and the reconcile client, so no spawned thread re-reads global state and a mount can never sign mainnet credentials against demo hosts.
  • A withdraw-capable key is refused. The binance live arm consults vike_bridge_core::key_permissions before arming (arming::binance_withdraw_gate), so a key known to carry withdraw permission degrades that venue to paper instead of being discovered at the first order.
  • A venue with no testnet is gated twice. Polymarket's only tier is real money, so POLY_EXEC=1 mounts its execution client and POLY_RECONCILE=1 independently mounts its reconcile client; both are unset by default.

A live mount also refuses to start without an account-dependent risk budget: MountError::MissingRiskBudget names every missing [risk] key, because no universal safe default exists for max_notional_per_order or max_total_exposure. A paper mount never sees it. The startup preflight is the go/no-go probe that runs once after mounting.

Gate 3 — the RiskGate, on every order

vike_exec::RiskGate is one extensible stage every order crosses. It is pure: it owns no bus and publishes nothing; the engine acts on its verdict, which is what lets the same rules run identically in backtest, paper and live — a veto is an OrderDenied event, terminal and pre-venue, in every mode.

Its limits are built at mount from the venue's own fetched SymbolProperties (RiskLimits::from_properties — tick, lot and the two floors; a 0.0 field means unconstrained) and from the run profile's [risk] table:

limitwhat it does
tick_size, lot_sizeround the order onto the venue grid, round-half-to-even
min_qty, min_notionalfloors — on opening orders only; a pure reduce is exempt, so a position can always be closed
max_notional_per_orderthe per-order notional ceiling. It lives in policy.toml, and Policy implements neither the env nor the CLI override trait, so raising it from the environment is a compile error rather than a review comment
max_total_exposurea cap on projected open notional — see the warning below
max_orders_per_window, window_msa submission-rate throttle
max_leveragethe operator's declared cap, recorded for audit; enforcement is im_requirement, derived as 1 / max_leverage so the two cannot disagree
price_collarthe fat-finger axis: an opening order whose limit or trigger price sits further from the mark than max(pct × mark, abs_floor) is denied. Both halves are required — a percentage is useless on a cheap instrument, a floor on an expensive one
max_slippage_bps, require_fillablean opt-in walk-the-book veto; inert until a caller holds both the intent and a live book

Notional includes the contract multiplier, exactly as the backtest fill gate computes it, so live and backtest verdicts agree. A multi-leg combo crosses the gate as one atomic batch — all legs pass or the whole combo yields a single denial naming the failing leg — and consumes one throttle slot, because it is one venue order. A modify is judged against the projected order, the resting request with the new terms folded in, never against the delta; its veto is non-terminal and consumes no slot.

max_total_exposure scopes to ONE symbol at ONE venue, despite its name

It is not account-wide, cross-symbol or cross-venue. The gate holds no cross-symbol position book, and make_engine builds one engine — so one gate, so one copy of this cap — per venue. An operator trading N symbols is protected by this number N times over, once each, and never once in aggregate: a value sized for a whole book is an N× weaker cap than they believe they set. crates/vike-exec/tests/risk/risk_lane_completion.rs's max_total_exposure_is_scoped_to_one_venue_and_one_symbol pins the scope. The name is kept because it is also the operator-facing TOML key, the string in the live-mount refusal, and a serde key inside the journal's state hash.

Gate 4 — the halt, and why it is deliberately strict

A manual halt — and the two dead-man's switches that engage one automatically, one on a link the venue's bridge reports dead and one on a feed that has gone silent — must stop opening risk without trapping anyone in a position. Two mechanisms agree on that policy and differ in verification strength. The RiskGate under TradingState::Halted admits only a position-verified reduce (vike_model::is_covered_reduce: the order must oppose the position and be covered by it), because it holds the position book. The ExecutionClient boundary does not, so vike_exec::halt::halt_admits_submit takes the caller's reduce_only flag as the only evidence in existence. That is the default HaltAdmit::Admit mode, in which a venue's own position report is ignored outright; the tighter HaltAdmit::Verify mode still requires reduce_only and additionally refuses a submit the venue's book proves opens risk — and there every unknown admits (never fetched, fetch failed, an answer for another account), because the promise being kept is halting cannot trap you, not halting is airtight. The halt page carries the per-venue admit table.

On this page