The trade REPL
What driving a live node from a terminal is actually like — the two connections, the preview before every write, what the local guardrail is and is not, and why each command reports its own outcome.
vike-cli trade --node <host:port> attaches a terminal to a running
vike-tradehub node. You type short verbs — submit,
cancel, flatten, orders, positions, state — and they become authenticated commands on the
node's wire. This page is about the experience: what the prompt does, what the confirmation gate is
for, and where the authority actually sits. The trade reference is
the verb-by-verb grammar.
Two connections, opened separately
The REPL opens two connections, and either can be missing. Reads go over an OBSERVE handle
subscribed to the node's pushed snapshot stream; writes go over a CONTROL handle. Each is gated on
its own HMAC key, and the node verifies each scope against its own key — a control key cannot stand
in for an observe one.
Startup says which halves came up, and where each key came from:
connected: OBSERVE (read) to 10.0.0.5:9200 [key from the credential store]
connected: CONTROL (write) to 10.0.0.5:9200 [key from the process env]
type `help` for commands, `quit` (or Ctrl-D) to exit
vike>The keys are resolved once by the dispatcher, process environment first, then
<project>/settings/secrets.env — the same store the daemon on the other end loads them from.
crates/vike-cli/src/cmd/nodekeys.rs argues that order: the fix is strictly additive, so an
operator who exports the keys today keeps exactly the behaviour they have, and an export is
ephemeral, narrow and typed on purpose while the store is the box's standing configuration. Before
that resolver existed this command read std::env::var and nothing else, so a box configured
exactly as the daemon documents — both keys in the store, vike-cli secrets list confirming them —
answered nothing to do and exited.
Values are trimmed on the way in because the server trims on the way out. A client that did not
would sign with "secret\n" against a server verifying "secret", and an HMAC mismatch surfaces as
an opaque auth denial rather than as your key file has a trailing newline.
Writes disabled is three different problems
With no control key you get observe-only. But a perfectly good key in the store can still leave
writes off, because the node refused the control scope — its tradehub_control flag is off —
and that refusal happened once, at connect time, before it scrolled away. The three cases are
distinguished at connect and carried forward, so a later write names the true reason: no key
resolved, the node refused the scope, or the connection could not be opened at all. Every message
still ends in "nothing was sent", the one fact all three share.
Every write previews first
Typing a write verb does not send it. The REPL resolves the command, prints it, prints a guardrail line, and asks:
vike> buy binance BTCUSDT 0.1 @64000 --reason rebalancing the hedge
preview: SUBMIT buy binance BTCUSDT qty=0.1 limit @ 64000 coid=a1b2c3d41
guardrail: qty=0.1 notional=6400 max_qty=- max_notional=250 → OVER LIMIT (the node will reject)
reason (recorded in the node's audit trail): rebalancing the hedge
confirm? [y/N]Only an explicit y or yes sends anything; Ctrl-C, Ctrl-D, a blank line and anything else are all
a no, which is the safe default for an order write. --yes skips the prompt — and note that the
preview still prints even then, so the human watching a scripted session sees exactly what went.
Two details in that preview are worth knowing about.
The client order id was minted by this side. A node refuses a remote submit whose
client_order_id is empty — a remote peer whose id the runtime minted has no stable handle to
cancel or deduplicate by — so every client of that protocol must pre-mint one. The generator is
session-scoped: an eight-hex prefix from OS entropy per process, then a counter, so two REPLs open
side by side, or one restarted while an order still rests at the venue, cannot collide. The id is
filled in before the preview renders, so the id you read is the id sent and the id you type back
at cancel. submit … --coid <id> pins one explicitly.
The numbers are rendered, not dumped. 0.4 * 3 is exactly 1.2000000000000002 in binary
floating point, and f64::to_string is obliged to print all of it. The preview instead prints the
shortest fixed-point rendering that still round-trips within a display tolerance, because a reader
who has learned to skip past seventeen digits of arithmetic noise is a reader who will skip past a
real discrepancy in the same position. The comparison itself is done on the raw values —
crates/vike-cli/src/cmd/verbs.rs's guardrail_check never reads the string.
What the guardrail is, and what it is not
It is advisory. guardrail_check compares the order against two local caps: this box's
max_notional_per_order from policy.toml, and
VIKE_MAX_ORDER_QTY, a local typo-catcher with no policy field and no enforcing counterpart
anywhere. An OVER LIMIT verdict does not stop the send — it warns you that the node will refuse
it.
The enforcing gate is on the node: its server-side control limits and the core risk gate. The client preview is UX, the node gate is truth. See the control boundary and risk and arming.
A market order cannot be sized client-side
The default order type has no price, so there is no notional to compute and the notional half of the check is skipped. The quantity half still runs. This is the branch most likely to be taken and the one where a client-side estimate is weakest — another reason the node's own gate is the one that counts.
The notional cap deliberately does not come from the environment. It used to be
VIKE_MAX_ORDER_NOTIONAL, and that variable was removed rather than deprecated: a ceiling any
exported variable can raise is not a ceiling. Setting it now refuses startup, with a message
naming policy.toml, the key, and a paste-ready line carrying your own value.
Each command reports its own outcome
After sending, the REPL waits up to two seconds for the node's verdict on that command and prints one of four things: accepted (with the coid), rejected with the node's reason, an explicit not confirmed — the connection dropped before the node answered, or sent, but the node has not answered within 2s. It is a deadline rather than a sleep: the answer returns the instant the reply lands, which is sub-millisecond on loopback.
Read an outcome from its own ticket, never a shared side-channel
This used to poll a handle-wide error field that latches. After one refusal every later write in
the session printed that same refusal, including commands the node had accepted and executed. An
operator who fat-fingered an order, got a correct refusal, then typed market-exit was told the
panic button had been rejected when it had run. The outcome is now awaited on the ticket the send
returned, which is that command's identity and no other's.
The small comforts
--reason <text to end of line> may end any write line. It is split off before the verb grammar
sees it, shown in the preview, sent beside the command, and recorded in the node's audit trail — so
an incident review reads why, not just what. It never becomes part of the order, and the node
sanitizes it before recording.
Ctrl-C abandons the current line and keeps the session; Ctrl-D quits. Line history persists under
the settings directory's state/ sub-directory, best-effort — with no project above the working
directory it is simply not persisted, which is a quiet degrade rather than an error.
help prints the verb reference, and it is the one place in the binary where --help does not
short-circuit through the usage-error channel: the REPL's help is a reference to an interactive
grammar rather than a usage line, so it is recorded and rendered instead.
The two procedures this REPL exists for are stop, liquidate, restart and triage a stuck order. The read-only question — what is that daemon running? — has its own verb, on The daemon and the app.
A first session
One end-to-end pass at the command line — scaffold the project, prove which settings and store are being read, list what a strategy may call, and get a backtest report back from a data service.
The daemon and the app
Three clients of one trading node — the desktop app in observe mode, the CLI's REPL and read verb, and the MCP server — what each is best at, and what strategy-status actually reads.