Skip to content

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:

    kind Reads Notes
    SMA close unless input says otherwise simple average
    EMA same exponential, seeded on the simple average of the first period values
    ATR the bar's range period only
    RSI close unless input says otherwise 0–100; reports after period + 1 bars, since it is computed from changes
    MACD close unless input says otherwise fast, slow, signal, and line: macd, signal or histogram. One declaration is one series — declare it twice to compare a line against its signal
    MAX / MIN high / low unless input says otherwise rolling extreme

    A period is a number or the name of a parameter. For MACD, fast must be below slow — 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, or and ! combine. cross_above and cross_below are 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.