vike
Concepts

The dead-man's switches

Two of them, with opposite defaults because they read opposite evidence — the connection switch is on unless you turn it off and trips on a link the venue's own bridge reports dead; the silence switch is off unless you write the key and trips on a feed that has gone quiet, which is also what a closed market looks like.

A dead-man's switch is the automatic counterpart to the manual halt sentinel: when nobody is in front of the daemon, something cancels the resting orders anyway. Vike Trader ships two of them, and one sentence separates them — what each one accepts as evidence that the daemon has lost the market:

  • The connection dead-man reads the link state a venue's own bridge discloses. A feed reported Disconnected for longer than policy.toml's link_deadman_grace_ms cancels that venue's resting orders. The key absent means ARMED, at two minutes.
  • The silence dead-man reads ingest. No venue event, tick, bar, quote, trade or book update on any venue for longer than deadman_timeout_ms cancels every resting order. The key absent means OFF, with one warning at mount.

The defaults point in opposite directions because a market that closes is silence and is not a disconnect. The connection switch cannot see a close, so it may be armed by default; the silence switch cannot tell one from an outage, so it may not.

connection dead-mansilence dead-man
key in policy.tomllink_deadman_grace_msdeadman_timeout_ms
absentarmed at DEFAULT_LINK_DEADMAN_GRACE_MS — 120 000 msoff, warned about once
off is spelled= 0, the only off= 0, or left out
evidenceFeedStatus::Disconnected per (venue, symbol)nothing arrived, anywhere
a session closeinvisible — a close reads Stale, which never countstrips it
a trip cancelsthe venue whose link diedevery venue
where it armsa per-venue table and a disclosing lane in this mountwherever the key is written
authoritycrates/vike-core/src/runtime/link_deadman.rscrates/vike-core/src/runtime/deadman.rs

They are independent: either, both or neither may be on. They are separate CoreConfig fields, and they share exactly one setting — deadman_action, because what a trip does is the same question for both and a second key would be a second answer to it. Both are folded from policy.toml by crates/vike-tradehub/src/tradehub_cli.rs's live_mount_with and by nothing else: the daemon's own paper rehearsal arm constructs neither, and neither does vike-desktop or crates/vike-run/src/bin/ibkr_mount.rs.

The connection dead-man

The signal is the per-(venue, symbol) FeedStatus the bridges disclose. It reaches the core through two seams that the latch cannot tell apart: a feed that owns a LiveDataSink maps the data layer's StreamStatus 1:1 by crates/vike-core/src/core_sink.rs's stream_status, and a feed that owns no sink — the HFT tick pumps, which take a bare vike_exec::TickSender — pushes a FeedStatus onto the tick lane directly, through the one map in crates/vike-bridge-core/src/stream_health.rs's HealthEvent::feed_status. Both land on the same Ingest::StreamStatus arm. Disconnected opens a grace window for that key; Live for the same key closes it and re-arms the link; and Stale never counts, in either direction. That last line is the entire difference between this switch and the one below. Stale means "no fresh price exists", which is an accurate disclosure of a weekend on an FX venue and of a scheduled venue maintenance window — reacting to it would be the silence switch wearing a new name.

A Disconnected opens the window whether or not that link was ever seen Live, and that is a correction rather than a simplification: no bridge on this roster discloses a Live on a first successful connect — vike_bridge_core::stream_health's StreamHealth::recover returns nothing when no gap is open, so Live exists only to close one. A gate on a prior Live would have made the switch blind to the first socket death of a mount, which is the outage it exists for. The startup dial-up is absorbed by the grace instead.

Recovery is disclosed on the first frame back, never at connect, and that difference is what keeps a flapping venue disclosed down. A successful dial proves only that the venue accepted a TCP+TLS handshake; an inbound frame — data, or a server ping the socket answers — proves the wire carries traffic again. On a connect-time Live, a venue that accepts a connection and drops it in a loop (a rate-limit ban, a half-dead edge) would emit Live/Gap on every cycle and reset the grace window each time, so the switch could never trip on the outage it is most needed for. One Disconnected per outage, for the mirror-image reason: each feed owns one StreamHealth (crates/vike-bridge-core/src/stream_health.rs) across its whole lifetime, re-dials included, so a venue that flaps for ten minutes discloses one disconnect and one recovery rather than three hundred.

crates/vike-core/src/runtime/link_deadman.rs is a clock-free latch — two fields per link, both entry points taking an injected now_ms — evaluated at the drain-loop boundary on the timer wheel, on its own LinkDeadManSweep cadence of half the grace. An entry exists only for a link that has actually been disclosed down at least once, and a status change on an unarmed venue costs one set lookup and no allocation. The per-message fold has no branch for the feature at all.

The bounds are refused by name, never clamped

crates/vike-config/src/policy.rs accepts absent, 0, or MIN_LINK_DEADMAN_GRACE_MS (30 000 ms) through MAX_LINK_DEADMAN_GRACE_MS (3 600 000 ms). The floor is the measured worst ordinary reconnect across the lanes that report a dead link, which is the L2 depth driver: a bounded 10 s dial, a 10 s REST book seed and a 3 s backoff, 23 s end to end. It is the slowest of the three — the tick pumps a live vike-tradehub mount actually subscribes come in at 21 s and under — so anything under the floor makes the switch trip on the re-dial that follows an ordinary socket fault, which is the false-trip class the design exists to remove. Above the ceiling it is a switch that never fires. A value outside the range is rejected at load with the key and the file named, because a grace that was silently raised is a grace the operator believes they set and do not have.

Which venues actually arm

Arming needs two independent facts to be true, and the second one is a fact about your daemon rather than about the venue.

One: the venue's row. crates/vike-model/src/link_deadman.rs carries one named row per roster venue, each read from the emitter it cites, with three dispositions. Armed — the venue trades continuously and its bridge discloses a disconnect: binance, aster, bybit, okx — each from two emitters, both on a book socket: the DOM depth feed and the HFT quote/trade/book tick pump — and polymarket (from its primary book feed). SessionBounded — the bridge does disclose one, but the market has sessions and a close has not been observed to read as Stale rather than as a gap: oanda and ig, off until somebody watches a weekend through. Inert — the bridge hands a gap to the sink nowhere at all, so the switch could not fire even if it were armed: hyperliquid, deribit, alpaca, ibkr, ctrader, fxcm, dukascopy. Each unarmed row carries its reason verbatim, and the daemon prints it.

Two: whether this mount subscribes a lane that carries the disclosure. A row is a statement about the adapter, never a promise about a mount. crates/vike-tradehub/src/tradehub_cli.rs's mount_link_disclosure is the reading, one arm per venue plan, and for a CEX venue the answer is Discloses. That it is an answer at all is recent, and the history is the point: the daemon's VenuePlan::Cex arm subscribes the kline lane and the quote/trade/book tick pump and deliberately never subscribe_depth (crates/vike-tradehub/src/feeds.rs), and until 2026-09-06 neither of those two lanes disclosed anything — the kline lane rides vike_bridge_core::market_pump, which carries no link state at all, and the tick pump owns no LiveDataSink. The pump now discloses its own transport state onto the core tick lane it already writes ticks to: disclose_link in crates/bridges/binance/src/family/depth.rs's md_main, and its twins in crates/bridges/bybit/src/market_data.rs and crates/bridges/okx/src/market_data.rs.

Subscribing the DOM feed instead was the other way to close it, and it was rejected: that pump already carries the venue's @depth-class book stream on the same host, so subscribe_depth would open a second socket to the same stream, keep a second copy of the same book and add a periodic REST re-seed — for a signal the first socket already has, delivered into l2_snapshot, which is a no-op in every sink this daemon owns. The disclosure costs zero extra bytes on the wire this way, and the core's per-message fold is unchanged: nothing new rides the tick, quote, trade, bar or book path, only the occasional control event that was already there.

A default vike-tradehub daemon now arms four venues

Fold the two together. binance, bybit, okx and aster arm on a default live mount: the tick pump is the lane that mount subscribes, and it is the venue's book socket rather than a side channel. Polymarket is a fifth, behind its Cargo feature — its subscribe_book on every seated token feeds a MakerSink that forwards stream_status. Every session-bounded venue — the FX and equities mounts — stays off deliberately, because a market closing is not an outage: until a weekend has been watched through and seen to disclose Stale rather than a gap, the conservative row holds. Every Inert venue stays off because nothing in its bridge would reach the latch.

No row flipped direction. This was a change of reach, not of classification: those four were always Armed in the venue table, and the mount simply could not hear them — so link_deadman_config_from_policy returned None and built no latch at all on the binary that signs real orders, whatever policy.toml said. A daemon mounting only session-bounded or Inert venues is still that case, and it is the ordinary state of an FX-only daemon: no latch, no timer, no cadence contributed, and no automatic stop for a dead link. Nothing in policy.toml changes which venues arm — that is a property of the bridge and of the lanes this mount subscribes.

The mount says which it is rather than leaving it to be discovered: link_deadman_arming_report emits one line per mounted venue at startup, and it keeps apart the four ways a venue ends up unarmed — its market has sessions, its bridge discloses nothing, this daemon subscribes no lane that carries it, or the operator wrote 0. Read those lines; they describe your daemon and this page does not. The mount-lane case is checked before the armed line is printed, which is the whole of the fix: an earlier version logged LINK DEAD-MAN ARMED for binance on a daemon where nothing could ever reach the latch.

What an operator does about it

There is no line that arms the connection switch, because leaving the key out arms it. The lines that exist are the ones that change it:

# <project>/settings/policy.toml
link_deadman_grace_ms = 45000   # a tighter grace; legal range 30000..=3600000
link_deadman_grace_ms = 0       # OFF, for every venue — the only off
deadman_action = "cancel_all"   # a trip pulls the book and leaves the trading state alone

Neither key can be reached from the environment or the command line: Policy implements neither EnvOverride nor CliOverride and both traits are sealed, so an override is a compile error rather than a review comment. A stale service-unit line cannot disarm a kill switch with no diff to review.

What this file cannot do is arm a venue. The row is a property of the bridge and of the mount, not a preference: a venue whose feed hands the core no disconnect has nothing to observe, and a venue whose only emitter is a lane this daemon does not subscribe has nothing to observe here. Both are code changes, and the startup lines name which of the two you are looking at.

The silence dead-man, and the morning it was default-on

The older switch is crates/vike-core/src/runtime/deadman.rs: a four-field latch, deliberately clock-free, whose observe records a monotonic maximum over every liveness message — venue events and market data, never control or waker messages — so an out-of-order older frame cannot drag freshness backward. check returns the action exactly once when the newest observed timestamp has aged strictly past the threshold (now − newest > timeout; a lag equal to the timeout is still fresh) and the switch has not already tripped this outage; fresh data clears the latch and re-arms it. A core that has seen no data never trips, so pre-feed startup is silent rather than a nuisance.

It shipped armed for one morning, and a closed market is what reversed it

The key shipped with a compiled-in 60 s default — armed unless an operator wrote 0 — and the ruling was reversed the same day, once review established what the switch actually observes. It counts ingest and deliberately nothing else: not a control command, not the periodic waker, and not the one message that says a socket is alive. A transport heartbeat never reaches the core at all. So it cannot tell a dead socket from a quiet market or a closed one, and an armed default bought three halts nobody asked for. The session-close halt, deterministic: on FX over the weekend and on equities every evening it trips about a minute after the last tick, at every close — cancelling a GTC deliberately left over the weekend, halting every engine, writing the sentinel, and opening the next session halted until an operator deletes the file. The thin-market halt, probabilistic: a live mount following one illiquid instrument goes sixty seconds without a tick in a quiet minute. And the cap that could not be raised past the close: the maximum timeout is one day and an FX weekend is about 48 hours, so the only legal settings on an FX venue were 0 or a switch that trips every Friday. crates/vike-config/src/policy.rs's Policy::deadman_timeout_ms carries the record. The safety argument was right; the mechanism was the wrong one for it, and the connection switch above is the one it wanted.

So this key is now the optional extra for a 24/7 mount — a crypto perp venue that never closes and always ticks — that also wants a silence detector. Absent is off; and because the daemon cannot tell "the operator decided against it" from "the operator never heard of it", an absent key warns once at mount. deadman_timeout_ms = 0 is the only spelling that records the decision and silences that warning:

# <project>/settings/policy.toml
deadman_timeout_ms = 60000   # arm it — the recommendation for a venue that never closes
deadman_timeout_ms = 0       # a deliberate no, and the only thing that silences the warning

Nothing arms it silently: 60 000 ms is RECOMMENDED_DEADMAN_TIMEOUT_MS, the number the warning suggests, not one any binary applies. Accepted values are absent, 0, or 1 000 ms through one day, rejected by name rather than clamped for the same reason as the sibling's bounds. Raise it for a thin symbol; leave it off on anything that closes.

What a trip does

Both switches act at the drain-loop boundary on the timer wheel, never on the per-message fold, each on its own cadence of half its window — so a deadline is evaluated at least twice before it expires. crates/vike-core/src/runtime/watchdog.rs holds both acting halves, sweep_deadman and sweep_link_deadman. On trip each one:

  1. emits one tracing::warn! per outage or dead link — a fault transition, within the logging budget;
  2. cancels through the one order-write vocabulary, apply_intent with OrderIntent::MassCancel, so nothing bypasses mint, route or gate — and that verb also clears every armed conditional book, because a dead-man pulls all exposure. The cancel is tagged CancelIntent::RiskOff: a cancel issued because the market was lost must not be shed by a venue's own cancel budget;
  3. under DeadManAction::CancelAllAndHalt — the default deadman_action — halts every engine in-process and then writes the sentinel best-effort, a failed write logged rather than panicking the core, since the in-process gate has already stopped new orders;
  4. pushes a line into the recent-events ring and marks the snapshot dirty.

The scope is where they differ. The silence switch cancels with venue: None, symbol: None — everything. The connection switch scopes the cancel to the venue whose link died, across every engine of that exchange, so a two-account binance mount has both books pulled and a bybit socket death leaves the binance book resting. One cancel pass per venue rather than per link: two symbols of one venue expiring in the same sweep are one exchange fact.

HALT is the exception to that scoping and is deliberately process-wide, because the sentinel is one file by construction and a half-halted daemon is a state nobody asked for. It is the same file a manual touch HALT writes and every venue's submit boundary checks — the path is supplied by the binary through vike_bridge_core::halt::halt_path_from_env, because environment reads stay in binaries and vike-core takes no bridge dependency — so the automatic trip and an operator's hand reach one file. A recovered link re-arms its own latch for the next outage; Halted and the sentinel do not clear, which is an operator's decision.

It cancels; it does not flatten

A trip pulls resting orders and, at most, halts. It never sends a closing order, so a position open when the link died is still open after the trip. Getting out is the operator's market-exit, usable from a halted core precisely because a flatten leg is the position-covered reduce the halt admits — see halt. And because the halt is engaged, the strategy cannot re-open risk when data returns until the operator clears it.

What it costs replay

The boundary waker message is journaled only when a wall-clock sweep that mutates order state can ride it, and there are now three such sweeps: the stuck-order watchdog and both dead-man switches. That waker record is what makes replay_from return Unsupported for the session: a trip is a wall-clock effect the journal cannot re-derive. A core with none of the three on writes no waker record and replays normally; the rule is on the replay page.

In practice a live mount has already forfeited replay before either key is read — live_mount_with sets a 30 s submit-acknowledgement timeout, which arms the same record — so arming either switch forfeits nothing further. It is the paper and harness cores, which set no such timeout, where the distinction is live.

Both switches are among the mechanisms that reach Halted on their own — the fold panic's safe-state entry is another — which is why the halt policy has no mode that refuses a reducing order: a switch that trips and then traps the operator in the position it just exposed would be worse than no switch.

On this page