Developers

Strategy rules spec

The Custom rules JSON in full: every field, indicator and operator, the limits, and every error the check can return.

Overview

A Custom strategy is a small JSON object: a group of buy conditions, a group of sell conditions, and two optional exits. The Studio builds it for you; this page is for writing it by hand, from the Public API or the MCP server. The same check runs everywhere, so rules that pass here pass in the Studio too.

Every hour, for each market, the agent checks one group:

  • No position: the entry group. If it is met, the agent buys.
  • Holding a position: the exit group. If it is met, the agent sells. The stop loss, the take profit and the max holding time are checked as well, whatever the group says.
{
  "entry": { "join": "all" | "any", "conditions": [ condition, ... ] },
  "exit":  { "join": "all" | "any", "conditions": [ condition, ... ] },
  "takeProfitPct": number | null,
  "maxHoldHours": number | null
}

condition = { "left": indicator, "op": operator, "right": indicator | value }
indicator = { "kind": "sma", "hours": 48, "offsetPct": -2 }
value     = { "kind": "value", "value": 30 }

Full example

RSI dip buying with a trend filter. It passes the check as it is, so you can paste it into a validate call.

{
  "entry": {
    "join": "all",
    "conditions": [
      {
        "left": {
          "kind": "rsi",
          "hours": 14
        },
        "op": "below",
        "right": {
          "kind": "value",
          "value": 30
        }
      },
      {
        "left": {
          "kind": "price"
        },
        "op": "above",
        "right": {
          "kind": "sma",
          "hours": 72,
          "offsetPct": -5
        }
      }
    ]
  },
  "exit": {
    "join": "any",
    "conditions": [
      {
        "left": {
          "kind": "rsi",
          "hours": 14
        },
        "op": "crossesAbove",
        "right": {
          "kind": "value",
          "value": 55
        }
      }
    ]
  },
  "takeProfitPct": 6,
  "maxHoldHours": 72
}
  1. entry, join "all": both buy conditions must be true in the same hour.
  2. Buy condition 1: the 14 hour RSI is under 30, so the market has fallen hard lately.
  3. Buy condition 2: the price is still above its 72 hour average minus 5%, so it is a dip and not a crash.
  4. exit, join "any": one sell condition is enough.
  5. Sell condition: the 14 hour RSI crosses back above 55. True only in the hour it crosses.
  6. takeProfitPct 6: sell once the price is 6% above where it was bought.
  7. maxHoldHours 72: sell after 3 days whatever happens.
curl -X POST https://zolvaagent.app/api/public/v1/me/rules/validate \
  -H "Authorization: Bearer $ZOLVA_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "rules": {"entry":{"join":"all","conditions":[{"left":{"kind":"rsi","hours":14},"op":"below","right":{"kind":"value","value":30}},{"left":{"kind":"price"},"op":"above","right":{"kind":"sma","hours":72,"offsetPct":-5}}]},"exit":{"join":"any","conditions":[{"left":{"kind":"rsi","hours":14},"op":"crossesAbove","right":{"kind":"value","value":55}}]},"takeProfitPct":6,"maxHoldHours":72} }'

The answer is { "valid": true, "rules": {...}, "lookbackHours": 73, "errors": [] }, or valid: false with the list of errors below. lookbackHours is how much history the rules need before the first decision.

Fields

FieldTypeWhat it does
entrygroupWhen to buy. Checked every hour the agent holds nothing in that market.
exitgroupWhen to sell. Checked every hour the agent holds a position. May have no conditions if a take profit or max holding time is set.
join"all" or "any"On a group. all: every condition must be true. any: one is enough. Anything else counts as "all".
conditionsarrayOn a group: 0 to 3 conditions. The buy group needs at least one.
leftindicatorOn a condition: the indicator being tested. Its unit decides what the right side may be.
opabove, below, crossesAbove, crossesBelowOn a condition: the comparison. See the operators table.
rightindicator or valueOn a condition: what the left side is compared with. A price line when the left side is a price line, otherwise a value.
kindprice, sma, high, low, change, zscore, rsi, valueOn an indicator: which one. "value" marks a plain number on the right side.
hoursintegerOn an indicator: the period. Must be one of the periods in the indicators table; leave it out for price.
offsetPctnumber, −50 to 50On an indicator, optional: shifts the line by a percentage. −3 on a 48h average means 3% below it. 0 is the same as leaving it out.
valuenumberOn a value side: the number, in the left indicator's unit (percent for change, standard deviations for z score, 0 to 100 for RSI).
takeProfitPctnumber, 1 to 500, or nullSell once the price is this many percent above the entry price.
maxHoldHoursnumber, 1 to 720, or nullSell after holding a position this many hours, whatever the rules say.

Indicators

The kind of a side. Price lines (unit USD) can only be compared with other price lines; everything else is compared with a value.

kindWhat it measureshoursUnitCompare with
price
Price
The latest hourly close.none (leave hours out)USD price lineanother price line
sma
Average
The average close over the last N hours (simple moving average).12, 24, 48, 72USD price lineanother price line
high
Highest
The highest close of the N hours before this one. The current hour is left out, so price can cross above it.12, 24, 48, 72USD price lineanother price line
low
Lowest
The lowest close of the N hours before this one, current hour left out.12, 24, 48, 72USD price lineanother price line
change
Change
How much the price moved over the last N hours, in percent.1, 4, 12, 24, 48, 72percenta number
zscore
Z score
How far the price sits from its N hour average, in standard deviations. Around −2 or lower is an unusually deep dip.24, 48, 72, 96standard deviationsa number
rsi
RSI
Relative strength index over N hours: the share of the movement that was up. Below 30 is often read as oversold, above 70 as overbought.14, 240 to 100a number

Operators

opStudio labelTrue when
aboveaboveTrue every hour the left side is higher than the right side.
belowbelowTrue every hour the left side is lower than the right side.
crossesAbovecrosses aboveTrue only in the hour the left side moves from at or below the right side to above it.
crossesBelowcrosses belowTrue only in the hour the left side moves from at or above the right side to below it.

Agent settings next to the rules

Markets, venue, stop loss and position size are not part of the rules object. They are set when the agent is made (POST /me/agents or create_paper_agent) and in a backtest.

FieldTypeWhat it does
marketsarray of string, up to 42 characters (1 to 14)Market symbols (NVDA, ETH, BTC-PERP) or token addresses, from list_markets
venueone of: spot, perpsspot (default): stock and crypto tokens, long only. perps: Lighter perps, paper only
capitalnumber, 1,000 to 1,000,000Virtual dollars, 1000 to 1000000
maxPositionPctinteger, 10 to 100Most of the capital in one market, 10 to 100
stopLossPctinteger, 2 to 25Stop loss per position, 2 to 25

The stop loss always applies: a position that falls this far below its entry price is sold that hour. See Guardrails for every other limit.

Limits

LimitValue
Conditions per group (buy or sell)3
Longest look back168 hours (7 days)
Price line shift (offsetPct)±50%
Take profit1 to 500%
Max holding time1 to 720 hours (30 days)

Error messages

The check returns every problem at once, in plain words. These are all of them. Through the API they come back in errors (validate) or in the 400 invalid_input message (backtest, create).

MessageCauseFix
Use at most 3 buy rules.Use at most 3 sell rules.A group has more than 3 conditions.Drop the weakest condition, or split the idea into two agents.
Add at least one buy rule.entry.conditions is empty or missing.Add at least one buy condition.
Add a sell rule, a take profit or a max holding time, or the agent only sells on its stop loss.No exit conditions, no takeProfitPct and no maxHoldHours.Add a sell condition, a take profit or a max holding time.
Rules may look back at most 7 days.The rules need more than 168 hours of history.Use shorter periods.
A rule has an unknown indicator.A left or right side has an unknown kind, an hours value outside its periods, or an offsetPct outside the limit.Check kind, hours and offsetPct against the indicators table.
A rule has an unknown comparison.op is missing or not one of the four operators.Use above, below, crossesAbove or crossesBelow.
Compare prices with another price line, not a plain number.A price line (price, average, highest, lowest) is compared with a plain value.Compare it with another price line, and use offsetPct for a gap such as 2% below the average.
Every number in a rule needs a value.A value side has no number, or one that is not finite.Set value to a number.
Only price lines can be compared with each other.Two indicators that are not both price lines are compared, such as RSI with change.Compare change, z score and RSI with a value instead.
Take profit must be between 1 and 500.Max holding time must be between 1 and 720.takeProfitPct or maxHoldHours is outside its range or not a number.Use a number in range, or null to turn it off.

Log in or sign up

Email and Google accounts get a wallet automatically. Browser wallets like MetaMask and Rabby work too. Signing in is free and never moves funds.