policy.toml
The hard ceilings — leverage, per-order notional, the emulated-market slippage band, halt admission, the two dead-man switches and per-venue arming — in the one settings file no environment variable can reach.
crates/vike-config/src/policy.rs is the authority for this file. Policy answers what is this
deployment never allowed to exceed, which makes it the one settings type whose value must be
harder to change than the rest — so it carries fewer layers than the others, and that reduction is
the entire reason it exists as its own struct. It implements neither EnvOverride nor
CliOverride and has no apply_env, so policy.apply_env(&env) fails to compile.
settings/policy.example.toml ships every line commented out, so the file as shipped configures
nothing.
The keys
max_leverage — the maximum leverage any strategy on this machine may request, >= 1.0.
Defaults to DEFAULT_MAX_LEVERAGE, which is 1.0, meaning no leverage at all: a ceiling's default
must be the conservative end, so a deployment that ships no policy.toml gets the safest reading.
It was [risk] max_leverage in a run-profile TOML, where vike_exec::ProfileRisk::max_leverage
converts it to RiskLimits::im_requirement as 1.0 / max_leverage.
max_notional_per_order — the maximum notional of any single order, in quote currency; absent
means uncapped. Its field doc calls this the clearest case the whole taxonomy exists for. It
replaces two environment variables, VIKE_MAX_ORDER_NOTIONAL (read by vike-desktop and vike-cli)
and VIKE_TRADEHUB_MAX_ORDER_NOTIONAL, both of which are now refused at startup by
vike_config::refuse_removed_env rather than ignored — see the
environment reference. The example file records where it bites: the
ceiling is enforced at the node, and vike-cli trade's preview guardrail only advises, saying so on
the line.
market_slippage — the aggression band for an emulated market order, as a fraction
(0.002 = 0.2 %). Absent means each venue keeps its own historical literal, byte-identically. Only
venues with no native market order consult it, and hyperliquid is the only one on the roster; such
an adapter prices a market intent as an Ioc limit at mid * (1 ± band) and a tripped stop-market
at trigger * (1 ± band). The band is therefore the worst price the order is allowed to reach — not
a prediction and not a fee. Hyperliquid's compiled-in value was 0.05: 5 % on every market order and
every protective stop exit, which never binds on a deep BTC book and binds completely on a thin alt.
Accepted values lie in MIN_MARKET_SLIPPAGE ..= MAX_MARKET_SLIPPAGE, both imported from
vike_model::market_slippage rather than redefined here, and a value outside the range is rejected
rather than clamped.
halt_admit — how much evidence the halt file sentinel demands before letting a submit out:
"admit" (the default) or "verify", over vike_model::HaltAdmit. It is policy-class because it
governs what a kill switch lets through, and because its two values differ only under an engaged
halt — during an incident, the worst possible moment to discover that the environment overrode your
file.
verify is weaker than the word suggests
The field doc is explicit: only a live cTrader mount implements verify, and it refuses only
what a positive report from the venue's own position book proves opens risk. Every absence — never
fetched, fetch failed, socket dropped since, an answer that could not be read in full or was for
another account, or simply no position reported in that symbol — still admits, because halting
cannot trap you outranks halting is airtight. Every other venue degrades to "admit" and says so
once, at mount, naming the reason; a cTrader venue that fell back to paper says that too, because
the paper client holds no book either. crates/vike-exec/src/halt.rs is the single authority. There
is deliberately no third value — a "refuse" mode was the behaviour until 2026-08-06 and was
removed because it disarmed the panic button in exactly the situations that halt on their own.
See halt for what an engaged halt lets out.
link_deadman_grace_ms — how long a venue's market-data link may be reported Disconnected by
its own bridge before the daemon cancels that venue's resting orders and, per deadman_action,
engages the halt. It is the one key in this file whose absent state is armed, at
DEFAULT_LINK_DEADMAN_GRACE_MS — 120 000 ms — and 0 is the only off, which needs no warning
because writing it is a decision. Legal values are 0, or MIN_LINK_DEADMAN_GRACE_MS (30 000 ms)
through MAX_LINK_DEADMAN_GRACE_MS (3 600 000 ms); the floor is the slowest measured ordinary
reconnect among the lanes that report a dead link, and a value outside the range is rejected by
name rather than clamped. It may carry an armed default because it reads what the bridge discloses
instead of counting ticks: a market that merely closed is disclosed Stale, which never counts.
Where it arms is not settable here — that is a property of the venue's adapter and of the lanes the
mount subscribed. On a default live mount that is binance, bybit, okx and aster, plus polymarket
behind its feature; every FX and equities venue stays off deliberately, because a close is not an
outage. The daemon prints one line per mounted venue at startup saying which, and why not.
deadman_timeout_ms — the other switch, on the opposite convention: how long the whole core's
ingest may be silent before it cancels every resting order. Absent is off, with one warning
at mount naming the key; 0 is off as a recorded decision and is the only spelling that silences
that warning; a written value arms it, legal from 1 000 ms to one day. It shipped armed at 60 s for
a single morning and was made opt-in the same day, because silence is also what a closed market
looks like — it halted every FX and equities mount at every session close, and the one-day maximum
could not be raised past an FX weekend. Arm it on a 24/7 mount only.
deadman_action — what a trip does, shared by both switches because what does a trip do is
one question: "cancel_all_and_halt" (the default — pull the book and engage the halt, in-process
on every engine plus the sentinel file) or "cancel_all" (pull the book, leave the trading state
alone). An unknown spelling is refused at load naming the legal two, and the key is inert while the
switch it would act for is not constructed.
The dead-man's switches carries the mechanism, the per-venue
reading and the startup lines that say which of the two, if either, is armed on your box.
[venues] — per-venue arming
One key per roster venue, paper / demo / live, defaulting to paper everywhere.
crates/vike-config/src/venue_mode.rs states the property that makes this safe to introduce into a
deployment already trading real money: it is a ceiling, not a selector. The effective tier is
min(mode, whatever the existing mechanisms decide) — never max — so venues.bybit = "live" does
not put bybit live; it declines to stop bybit going live if its credentials, its {VENUE}_MAINNET
flag and its mount arm already say so. VenueMode derives Ord over variants declared in ascending
risk order and VenueMode::cap is the one spelling of the fold, so an author reaching for max has
to delete a method and write their own.
It is enforced. The fold sits at crates/vike-mount/src/lib.rs's make_engine_for_account —
make_engine_with_legs is a one-line delegation to it — above the credential read, so a paper
venue returns the paper client without loading a credential, fetching an instrument grid or opening
a socket. crates/vike-config/tests/policy_is_consumed.rs's venues row names
crates/vike-mount/src/arming.rs's account_ceiling, the per-account spelling of that fold, and
opens that file to check it is still there. Risk and arming
puts it in order against the other gates.
The first start on a box with credentials and no [venues] table
Every venue drops to paper. That is the correct ceiling and a silent one, so it is not silent:
vike_mount::venue_arming_migration warns once, names this file and this key, lists the venues
whose credentials would have armed, and prints the block that restores them. It warns rather than
refusing because a venue connection is a capability, and a daemon that will not start cannot flatten
a position either. VenuePolicy::is_declared is the fact it self-silences on — writing the table at
all, even with every venue at paper, is a stated decision and is not the same as leaving it out.
Every roster venue gets a row, including ones this build cannot mount. ibkr, polymarket and
fxcm sit behind Cargo features declared far above vike-config, which cannot see them; a ceiling
for an unmountable venue caps at paper anyway; and a policy.toml must stay portable across boxes
whose features differ, so refusing ibkr = "demo" on a build without the feature would hard-fail a
reviewed file for a line that is correct. An unknown venue id is refused by name at load with the
whole roster in the message, because [venues] keys are data to serde and deny_unknown_fields
cannot see them.
[accounts] — per-account ceilings
A venue you hold more than one account on addresses each by a label appended to that venue's
credential keys behind a double underscore. Policy's hand-written Serialize emits accounts as
a sixth field — a projection of VenuePolicy's per-account ceilings, not a second copy of them:
VenuePolicy is #[serde(transparent)] over its flat map and so cannot carry a second serialized
field, and a table nothing serializes gets no provenance row, no vike-cli config show cell and no
example line, which is the declared-but-undisclosed shape this crate exists to refuse. The default
account inherits its venue's [venues] line; a labelled account must be named here to be armed, and
the venue's own line still caps it. Accounts has the credential-key
grammar.
crates/vike-config/src/venue_accounts.rs carries the shared-book rule, with a correction worth
reading before assuming otherwise: two active accounts of one venue that resolve to the same
effective trading book are reported — loudly, by name — and both are mounted anyway. The symbol is
not part of the rule; long on account A and short on account B is an ordinary spread. The hazard
that is real is one book reached by two engines, and vike_mount::make_engine_accounts emits one
warning per pair naming the venue, both labels and the shared book.
Two tombstones, and the gate they left behind
max_total_exposure was declared here, validated by Policy::apply, accepted by PolicyPatch's
deny_unknown_fields, and consumed by no code path in the workspace — an operator who set it got
validation, no complaint, and no cap. rate.max_utilization was the second: it clamped a preference
that was itself read by nothing. Both now refuse their key by name, and
crates/vike-config/tests/policy_is_consumed.rs is the standing gate — every field of this struct
must name a file that genuinely reads it, or carry a written admission that nothing does, and a
claimed consumer inside vike-config itself is rejected as self-consumption. See the
settings gates.
Nothing in this file is hot-appliable. crates/vike-tradehub/src/hot_reload.rs's classify answers
restart-required for the policy file before its table is consulted, so no future row can make a
risk ceiling hot-appliable; a policy edit lands on disk and takes effect on the next boot.
Settings files
Four TOML files in one directory, split by who is allowed to change a value and from where, with the precedence order and the compiler-enforced exception that makes a ceiling a ceiling.
config.toml
Deployment settings — store roots, log and journal directories, and the three listen or dial addresses — each with its environment variable and whether a running daemon can apply it without a restart.