vike

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.

On this page