Record a live feed
One TOML profile, one daemon, and a tape of the markets you actually watched — plus the two watchdogs that tell you it is really recording.
vike-recorder accumulates a live tape into your own history store, so you can backtest on markets
you have actually watched without buying an archive. It is a separate binary on purpose
(crates/vike-recorder/src/lib.rs): recording drags the signer, WebSocket and TLS stack into
whatever links it, so keeping it out of vike-datahub means a panic in feed parsing cannot take
serving down. Not the desktop app either — a tape that exists only while a window is open has a hole
wherever you closed it — and not vike-tradehub, because a trading restart must not punch a hole in
the tape.
The profile
One TOML file names the store and what to record. Every tick re-resolves each subscription's current
symbols, publishes their family membership, and drives the venue feed to exactly that set; rows land
in your DataFusion+Parquet store through RecorderSink, one commit per family rather than one per
symbol (crates/vike-datahub/src/recorder.rs).
store = "market_data/hist"
[[subscribe]]
venue = "polymarket"
family = "btc-updown-5m"
backfill = "off"A rolling family is recorded as one grouped series, which is why a 5-minute rotation costs one
commit per flush instead of one per token. A subscription may name a family or symbols, never
both. A misspelled key is refused by name, with the line number and the keys that would have been
accepted — webhook for webhooks once left an operator believing a pager was armed.
The store path is two traps in one line
A relative store resolves against the process working directory — right for the daemon, whose
unit sets WorkingDirectory=<project>, and wrong for every command you run by hand: a hand-run
--once from a source checkout was measured writing its tape into the checkout while the project's
own market_data/ stayed empty. And the directory name hist is not a free choice — it is the name
every binary's own default resolves to (crates/vike-model/src/state_path.rs's HIST_SUBDIR, under
PROJECT_DATA_DIR), so a store called anything else is one the code cannot name: a reader run
without --store opens <project>/market_data/hist, finds nothing, and reports zero rows rather
than failing.
Which venues this build can record
crates/vike-recorder/src/venues/mod.rs is the only venue-aware code in the crate, one module per
recordable venue, each behind its own Cargo feature — enabling polymarket pulls an EIP-712 signer
into the binary, and a build recording only static-symbol venues should not link one. A
feature-absent build answers with a startup error naming the missing feature, never a silent
no-record.
Families are general, not a Polymarket shape: Polymarket asks what is live right now because its
token ids rotate every five minutes, while Binance resolves the same key as a glob (*USDT, BTC*,
*USDT.P) over an instrument list that does not change from tick to tick. What a Binance recording
contains is worth stating plainly — its declared capabilities are bars, trades and depth, with
quotes: false and book: false, so the tape is the trade tape plus a conflated depth series,
and the trade lane is the only path to that data at all
(crates/vike-recorder/src/venues/binance.rs).
Prove the profile before you daemonize
--once runs a single tick and exits: it proves the profile parses, the store opens, and each family
resolves to live symbols, without leaving a daemon running.
Read the exit code, not the log
--once exits 0 only if every feed ended that tick with a live subscription, and 4
(EXIT_DRY_RUN) otherwise, naming each feed that would record nothing. It exited 0 on a total
resolve failure until 2026-08-08.
Two watchdogs, because one is blind to the other
A feed that is subscribed and connected but receiving nothing is invisible from every other
vantage point: subscribe_* returned Ok, the socket is established, no error is raised, and the
loss counters stay at zero because there are no rows to lose. A production recorder ran 95 minutes
in exactly that state, writing nothing for binance while logging a clean startup and zero warnings;
it was noticed by accident, days later, by reading row counts by hand
(crates/vike-recorder/src/liveness.rs).
SilenceWatchwatches series — rows arriving on something already subscribed.--silent-secsdefaults to 300 s, chosen against the slowest thing a healthy recorder does: a Polymarket up/down family rotates every five minutes, so a shorter threshold cries wolf every cycle. On by default, deliberately — the failure is silent by nature, so an operator who has to know to switch it on is exactly the operator who will not.ResolveWatchwatches feeds — a venue that has never produced a symbol to subscribe at all. It exists because the first is structurally blind to it: a venue that resolved nothing subscribed nothing, so the series watch iterates an empty set and reports nothing at two seconds or at two days. A misconfigured Polymarket proxy recorded nothing for a whole run while every watchdog stayed green.
A silent series is always logged and always alerted. The third reaction, --exit-on-silence
(EXIT_SILENT = 3, distinct from a startup failure so a supervisor can tell them apart), is opt-in:
a venue can be legitimately quiet, and a quiet market must not kill a daemon recording five healthy
ones.
The [alerting] table is optional and its absence means defaults, not off — with no webhooks
the alert still fires and still reaches the log; naming a target escalates it to a pager.
series_prefix scopes it by a prefix on the {kind}/{venue}/{symbol} key, because a Polymarket
token id does not exist yet when the file is written. No credential goes in this file:
webhooks names targets, and the token behind a name comes from the credential store or the process
environment.
Maintenance, and the one memory knob
[maintenance] is likewise optional-with-defaults rather than optional-and-off. The recorder commits
once per buffer flush, so a busy series writes a part every few seconds: 23 parts in 150 s for one
family's book, about 57 KB each, roughly 13,000 files a day if nothing merges them. Compaction runs
in its own thread and takes the same per-series lock the appends do, so a racing flush serializes and
loses nothing.
max_merge_rows is the memory knob, and it is rows rather than bytes for a measured reason: a merge
is materialized whole (its output is timestamp-sorted, and a sort cannot stream), and on a live
Polymarket book series the parts were only 2.5× compressed, so a 64 MB budget peaked at 8.95 GB.
Size it against your MemoryMax. retention_days omitted means keep forever — a recorder exists to
accumulate tape, so deleting any of it is always an explicit choice — and 0 is rejected, because it
would prune the tape as it is being written.
Stopping without losing the tape
One stop flag, many triggers: a control word on stdin (quit/shutdown/stop), and SIGTERM/SIGINT
through vike_ops::stop's install_handlers, so systemctl stop, a bare kill and Ctrl-C all reach
the same teardown. EOF stops only on a TTY — under systemd stdin is /dev/null, which reads EOF
immediately. On Windows there is no signal, but install_handlers bridges the console control events
onto the same flag — Ctrl-C, Ctrl-Break, window close. A detached recorder has neither a console
nor a TTY stdin, and unlike vike-tradehub it watches for no SHUTDOWN file, so nothing short of
taskkill reaches it — and that loses the buffered tape.
Teardown order is load-bearing: unsubscribe every feed first, then shut the sink down. The other
order leaves feed threads pushing rows into a channel whose writer is gone, and those rows are counted
as dropped with no reporter left to announce them. The sequence is budgeted end to end —
FEED_STOP_BUDGET_SECS plus SHUTDOWN_DEADLINE_SECS, summing to TOTAL_STOP_BUDGET_SECS, which is
what the shipped unit's TimeoutStopSec= is checked against — and ordered flush first with the
maintenance join last, so what a hard cap abandons is a restartable compaction pass and never the
rows. Until that handler existed SIGTERM reached none of it, and the flush is where the tape was lost.
Once there is tape, replay it; the store's layout and its
per-kind contracts are in the history store and
concepts/data. A long run also wants
VIKE_LOG_FILE_LEVEL.
Stop, liquidate, restart
What a stop leaves at the venue, what the kill switch does and in which order, and what a restarted node actually recovers.
Replay a recorded session
Two different replays — a recorded market tape through the backtest engine, and a command journal through the real core against a state-hash fence.