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}_MAINNETis the exact string"1", read from the process environment or the store with the process winning. For the three CEX venuesmake_engineresolves it exactly once per mount (cex_mainnet_enabled) and threads aboolinto 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_permissionsbefore 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=1mounts its execution client andPOLY_RECONCILE=1independently 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:
| limit | what it does |
|---|---|
tick_size, lot_size | round the order onto the venue grid, round-half-to-even |
min_qty, min_notional | floors — on opening orders only; a pure reduce is exempt, so a position can always be closed |
max_notional_per_order | the 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_exposure | a cap on projected open notional — see the warning below |
max_orders_per_window, window_ms | a submission-rate throttle |
max_leverage | the operator's declared cap, recorded for audit; enforcement is im_requirement, derived as 1 / max_leverage so the two cannot disagree |
price_collar | the 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_fillable | an 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.
Order lifecycle
Every order folds through one state machine with one mutator, must reach exactly one terminal state, and may never silently vanish — and a shared harness proves that against every venue's real decoder.
The data model
The value types every crate shares — bars and ticks, the L2 book and its one delta-apply law, the per-symbol instrument grid, the fill primitive, the twenty-variant Event enum the core consumes — and the interning rule that decides which fields may be a pointer.