Rules and rulesets¶
You have a trading idea. These are the two things you write to test it.
A rule says when to enter and when to exit. A ruleset says which parameter values to try for that rule.
Rule "buy when the fast average crosses above the slow one"
Ruleset "try fast = 5, 10, 20 against slow = 30, 50, 100"
Arvo ships nine rules compiled in, listed below. You can also write your own
as a JSON file under rules/ — an indicator list and conditions over it — or
import one from Pine. Either way it is then a rule like any other,
and goes through the same evaluation.
On the word strategy
Elsewhere these are all called strategies. Arvo says rule for the logic and
ruleset for the parameter search, because the distinction matters here: a
ruleset is a search, and how much you searched decides the bar the winner
has to clear. strategy still appears in code, the protocol and stored
findings. See the glossary.
The rules Arvo ships¶
| Rule | Interval | Premise |
|---|---|---|
sma_cross — Moving-average crossover |
1 day | The control. Not a good idea, a rule nobody disputes |
volatility_breakout |
1 day | A thrust measured in ATRs, so it means the same on any price |
momentum_breakout |
1 day | Buy a new channel high, leave on a trailing channel low |
cross_sectional_momentum |
1 day | Hold the few that rose most, and nothing else — ranks the set rather than judging each name on its own |
opening_range |
5 minutes | The session's first bars set a range; a close above it enters, with a target in range widths |
vwap_reversion |
5 minutes | Buy a close more than a deviation below VWAP; leave at VWAP |
put_spread |
1 day | Sell a put near a delta, buy one a width below; sized by the cash the worst case needs |
zero_dte_breakout, zero_dte_put_spread |
5 minutes | Same-day options on the instrument's chain |
A rule is defined at an interval; a ruleset cannot move it. A one-minute
sma_cross is refused with the reason.
The option rules and what they can be studied on¶
No vendor serves an option quote after the fact, so a chain that was not
recorded on the day is gone. What the engine records is therefore the whole
of what the option rules can ever be studied on, and the list is a file in
the project, option-quotes/underlyings.json:
{
"reason": "The most liquid US option markets: tight spreads and daily or weekly expiries.",
"underlyings": [
{ "symbol": "SPY", "why": "the deepest option book there is; daily expiries" },
{ "symbol": "QQQ", "why": "the same for the Nasdaq 100; daily expiries" }
]
}
Chosen for liquidity and said out loud, the way a universe is, never for what the underlyings returned. Every fifteen minutes of the regular session the engine records each one's chain; an underlying that fails is named and the rest are still recorded, because today's chain cannot be fetched tomorrow. A file that cannot be used leaves SPY recording and says why, since recording nothing would be worse.
Recording happens on trading days only, and a chain that has not changed since the last snapshot is not written again. While a day is being recorded its file is a CSV. Once the day is over the engine rewrites it as Parquet, about a seventeenth of the size, and removes the CSV after reading the Parquet file back and finding the same rows in it.
arvo-engine <dir> option-quotes spreads SPY reads the whole recording and
prints what an option cost to cross, by premium. See
the command line.
A ruleset¶
A JSON file in the project's rulesets/ folder:
{
"name": "fast_cross",
"label": "Faster crossover",
"premise": "MSFT swings on a 20-30 day base, faster than the shipped 50-200 grid.",
"interval": { "step": 1, "unit": "day" },
"kind": {
"kind": "grid",
"rule": "sma_cross",
"fixed": { "trade_size": 10.0 },
"axes": { "fast": [5.0, 10.0], "slow": [20.0, 30.0] }
}
}
axes is the search: every combination is one trial, and the deflation
counts them. fixed pins a parameter. A ruleset that searches nothing is
refused — one axis with two values is the smallest search.
The Rulesets tab lists the project's, with each one's problem when it has one (a rule Arvo does not have, an interval the rule is not defined at, a name that is one of Arvo's own). Edit the file in the editor and the list re-reads it; the Agent panel and the MCP server can write one for you. How to write a ruleset →
Rules as data¶
A rule can also be a file, rules/<name>.json in the project: named
indicators by their TA-Lib names, and entry and exit
conditions in JSON Logic, with defaults for every
number a ruleset's grid may vary. The engine evaluates it exactly as it
evaluates a compiled rule, and a data twin of sma_cross books the same
trades, to the cent, with the same journal lines.
{
"name": "twin_cross",
"label": "Moving-average crossover, as data",
"premise": "The control, written down instead of compiled.",
"interval": { "step": 1, "unit": "day" },
"params": { "fast": 10, "slow": 30 },
"indicators": {
"fast": { "kind": "SMA", "period": "fast" },
"slow": { "kind": "SMA", "period": "slow" }
},
"entry": { "cross_above": [ { "var": "fast" }, { "var": "slow" } ] },
"exit": { "cross_below": [ { "var": "fast" }, { "var": "slow" } ] }
}
-
Indicators, each a name you then read in a condition:
kindReads Notes SMAcloseunlessinputsays otherwisesimple average EMAsame exponential, seeded on the simple average of the first periodvaluesATRthe bar's range periodonlyRSIcloseunlessinputsays otherwise0–100; reports after period + 1bars, since it is computed from changesMACDcloseunlessinputsays otherwisefast,slow,signal, andline:macd,signalorhistogram. One declaration is one series — declare it twice to compare a line against its signalMAX/MINhigh/lowunlessinputsays otherwiserolling extreme A
periodis a number or the name of a parameter. ForMACD,fastmust be belowslow— a fast average that is not faster makes the line zero or inverted, and a cross on it is noise, so it is refused with that reason. - Conditions: JSON Logic, one operator per object.{"var": name}reads an indicator, a bar field (open,high,low,close,volume) or a parameter; a bare number is a literal.>,<,and,orand!combine.cross_aboveandcross_beloware Arvo's two stateful operators: they fire on the bar the relation changes, never before both sides are known, and forget after a stop so the next entry needs a fresh crossing. - Exit may be absent: the rule then leaves on its stop alone.
A rule file is offered under its own name beside Arvo's, with its defaults
as fixed parameters. The agent writes one with write_rule and reads what
the project holds with list_rules; the same two are
Research.WriteRule and Research.ListRuleFiles in the contract. A file
that cannot run is listed with its reason rather than dropped, and a rule
that would shadow one of Arvo's own names is refused. A ruleset names it in rule and varies its parameters
as it would sma_cross's. A finding on a data rule carries the definition
inside its experiment, so it replays on a machine that never saw the file,
and is stamped with the rule's content hash. A file whose defaults cannot
run is left out of the picker with its reason.