Command line¶
The engine's binary is the command line. There is no separate arvo
executable, by decision: a second binary would be a second place for the
argument parsing, the discovery and the token handling to drift.
arvo-engine [<data-dir>] serve; the app data directory when none is given
arvo-engine [<data-dir>] shutdown
arvo-engine [<data-dir>] session list
arvo-engine [<data-dir>] session start <finding> <executor>
arvo-engine [<data-dir>] session stop|reconcile|resume <id>
arvo-engine [<data-dir>] session halt <id>|--all [reason...]
arvo-engine [<data-dir>] session explain <id> <time>
arvo-engine [<data-dir>] review [YYYY-MM-DD]
arvo-engine [<data-dir>] rank [--rule R] [--instrument I]
arvo-engine [<data-dir>] universes [refresh]
arvo-engine [<data-dir>] pine <file> [--interval 1day] [--keep]
arvo-engine [<data-dir>] option-quotes spreads <symbol>
arvo-engine [<data-dir>] option-quotes compact
arvo-engine [<data-dir>] views [--print]
arvo-engine [<data-dir>] evidence rewrite
arvo-engine help
Every verb talks to the engine serving <data-dir> — the one that wrote
engine.json there — or to the app data one when you give no directory. Give
the directory whenever you run more than one project, or the verb will reach a
different engine than you meant.
Reading what you have¶
rank — the leaderboard¶
Every comparable finding in one order, so two rules are compared the same way every time.
arvo-engine ~/arvo rank
arvo-engine ~/arvo rank --rule sma_cross
arvo-engine ~/arvo rank --instrument MSFT.YF
The order is not "highest return". It is: findings that survived under
conservative costs first, then those that were Supported only at stated
costs, then the rest; ties broken by drawdown, then by trade count. A rule that
looks better on paper and worse once costs are doubled ranks below its
neighbour, deliberately. Verdicts and advice →
universes — coverage, and filling it¶
arvo-engine ~/arvo universes # what the project asks for, and what is missing
arvo-engine ~/arvo universes refresh # fetch what is missing or behind
refresh fetches the whole reach, every time
Not an incremental top-up. A vendor write replaces a series file, so a short refetch would truncate a ten-year history to whatever window it asked for. Fetching the full reach is slower and cannot silently destroy history.
review — after the close¶
Printed, and written under reviews/ once the day is over. It covers every session's fills against
the price the rule decided at, what the gate refused and why, the stretches a
session was frozen or halted, and what a person did by hand.
session explain — the chain behind a position¶
For everything held at that instant: bar → signal → gate → order → fill → position, with the rule that fired, the value it fired on, and the regime. This is the one verb that needs no running engine — it reads the record on disk.
option-quotes — the recorded chains, read¶
The engine records option chains because no vendor serves a quote after the
day. spreads reads the whole recording for one underlying and prints the
half-spread by premium, beside what the cost model charges:
It changes nothing. The model's constant stays where it is until someone decides to move it, because moving it re-costs every option study.
compact rewrites every finished day of the recording as Parquet, about a
seventeenth of the size. The engine does this by itself each hour; the verb is
for a project whose engine is not running. A day's CSV is removed only after
the Parquet file has been read back and holds the same rows.
views — the project in DuckDB¶
Writes .arvo/views.sql, which names each of the project's stores as a DuckDB
view, and says how to open it. --print shows the file and writes nothing.
See Ask your project a question.
evidence rewrite — an older store, in today's form¶
A finding is stored as a compact record with its curves in a Parquet file beside it. A store written before that holds each finding as one indented JSON file with the curves inside, and is several times larger.
Stop the engine first; the verb refuses while one is serving the folder. Each finding's new form is read back and compared with what was there before its file is replaced, and a finding the build cannot read is named and left as it was. Nothing about a finding changes but its format number.
Running a session¶
arvo-engine ~/arvo session list
arvo-engine ~/arvo session start <finding> alpaca-paper
arvo-engine ~/arvo session stop twin_cross@alpaca-paper
A session's id is <finding>@<executor>, as session list prints it, with its
state, its counts, and its verdict against its finding — including the reason
when the live result is diverging from the backtest.
<executor> is one of:
alpaca-paper |
Alpaca's paper endpoint. Real API, real fills and latency, simulated money |
alpaca-live |
Real money |
robinhood-<last4> |
Real money, the account whose number ends in those four digits |
The last four digits are required and are not guessed: robinhood alone names
no account and is refused, as is robinhood-85 — too few digits to identify
one.
Paper is the only thing that measures fill cost
A backtest assumes a fill. Alpaca's paper endpoint gives you a real one, at a real spread, with real latency — so its divergence from the backtest is a measurement rather than an assumption. That is why five paper days gate a live executor.
When a session freezes¶
A session audits its book against the venue every poll. If they disagree it freezes: it stops taking entries and waits, rather than trading on a position it is not sure it holds.
arvo-engine ~/arvo session reconcile twin_cross@alpaca-paper # square the book against the broker
arvo-engine ~/arvo session resume twin_cross@alpaca-paper # once you are satisfied
halt — the kill switch¶
This arms the gate, flattens what can be flattened, and stays halted. A restart does not lift it and a new session cannot start under it; a person clears it deliberately. A limit a restart lifts is not a limit.
A halt reports what it could not do
If the venue refuses an exit, the halt says so and names the instrument rather than reporting success. Read the output — positions the venue would not close are still yours.
Importing a rule¶
arvo-engine ~/arvo pine strategy.pine --interval 1day
arvo-engine ~/arvo pine strategy.pine --keep # write it under rules/
Reads the subset of Pine v5 the rule language can express and refuses the rest
by name rather than translating it approximately. Without --keep it prints
the translation and writes nothing.
What it refuses, it lists: a strategy using a function Arvo cannot say is reported as unsupported with that function named, so you know what was dropped instead of getting a rule that quietly does something else. Importing from Pine →
Stopping the engine¶
Asks the engine to stop and waits until it has. On the way out the engine:
- asks every running session to stop, so each record ends with
stopped; - stops the plugins it started;
- removes its
engine.jsonand exits.
Positions are left as they are. Stopping is not flattening: session halt is
the kill switch. A session that does not end within fifteen seconds is named,
and the engine goes anyway.
The verb then says what became of each session:
the engine (pid 30312) has stopped
20260921T134031540-AAPL.AIEX@alpaca-paper: stopped, positions as they were
With no engine running it says so and exits 0, so it can sit in front of a build. Use it before rebuilding the engine: on Windows the binary cannot be replaced while it runs.
Ending the process with the operating system does none of this. No destructor runs and a session's record simply ends. The plugins still go: each is tied to the engine's life by the kernel (a Job Object on Windows, the parent-death signal on Linux), and one left behind by an older build is stopped by the next engine before it starts its own.
The sessions that were running come back when the next engine starts, through the ordinary start. A frozen or halted one waits for a person, as it did.
Exit codes¶
0 |
did what you asked |
| non-zero | did not, and said why on stderr |
A verb that cannot find an engine says so and names the file it looked in. A verb refused by the gate prints the gate's reason — a refusal is never silent, which is the same rule the engine follows internally.
The MCP server¶
Stdout is the protocol; diagnostics go to stderr. See With Claude Code.