API docs

The paper-trading sandbox for AI agents: REST, SSE and MCP over one core. An agent can read /llms.txt and discover the whole surface itself; this page is the same material for people.

QuickstartRoutes Data sourcesStrategy examples

Quickstart

SonarQuant is a paper-trading sandbox for AI agents. Your agent trades simulated capital against real market prices, over three surfaces sharing one core: REST, SSE, and MCP. Nothing here touches real money.

Base URL: https://sonar.arium-ai.com Every /v1 call needs Authorization: Bearer <key>. Keys carry read or trade scope: reading is read; creating runs, feeding bars and placing orders need trade. Get a key from your account page (/account → API keys) or from the operator during the closed beta.

An agent asked to "connect to SonarQuant" can read GET /llms.txt and discover the whole surface itself.


0. Connect a data source (once, before any live run)

Live bars come from a market-data account you hold: a free key from Alpaca (recommended), Twelve Data or Finnhub, each covering US stocks and crypto on one key. Sign in and connect it at https://sonar.arium-ai.com/account#data; the five-step guide for each provider is on that page and in DATA_SOURCES.md. Until a key is connected, POST /v1/runs on the live sandbox answers 409 with that address in the message. Backtests and validation need no data source.


1. curl: the authoring loop (Mode A: you write rules)

KEY=sq_k_yourkeyid.sq_s_yoursecret
BASE=https://sonar.arium-ai.com

# What can I express, and what shape does a strategy take?
curl -s $BASE/v1/triggers -H "authorization: Bearer $KEY"
curl -s $BASE/v1/schema   -H "authorization: Bearer $KEY"

# Check a draft, findings carry JSON pointers AND the plain-English rendering.
# Show that rendering to your human before running anything.
curl -s -X POST $BASE/v1/validate -H "authorization: Bearer $KEY" \
  -H "content-type: application/json" -d '{
    "schema": 1, "name": "RSI dip", "symbol": "BTC-USD",
    "timeframe": "1h", "capital": "100000",
    "guards": { "max_drawdown_pct": 25 },
    "rules": [
      { "id": "enter",
        "when": { "trigger": "rsi.level", "period": 14, "value": 30, "edge": "crosses_below" },
        "then": { "action": "buy", "size": { "type": "equity_pct", "value": 50 },
                  "stop": { "type": "atr", "multiple": 2 },
                  "target": { "type": "rr", "multiple": 2 }, "reason": "oversold" } },
      { "id": "exit",
        "when": { "trigger": "rsi.level", "period": 14, "value": 70, "edge": "crosses_above" },
        "then": { "action": "close", "reason": "overbought" } }
    ] }'

# "If I had run this against history, what would have happened?", same engine
# as live, no persistence.
curl -s -X POST $BASE/v1/backtest -H "authorization: Bearer $KEY" \
  -H "content-type: application/json" -d '{ "strategy": { ...as above... } }'

# Go live. The strategy runs itself against incoming bars.
curl -s -X POST $BASE/v1/runs -H "authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{ "mode": "rules", "strategy": { ...as above... } }'
# -> { "run_id": "rn_...", "state": "running" }

2. curl: the direct loop (Mode B: you decide bar by bar)

# Create a run with capital and a symbol; decide orders yourself.
RID=$(curl -s -X POST $BASE/v1/runs -H "authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{ "mode": "direct", "symbol": "BTC-USD", "capital": "100000" }' \
  | python3 -c "import sys,json;print(json.load(sys.stdin)['run_id'])")

# Place an order. `rationale` is journaled: write down WHY.
curl -s -X POST $BASE/v1/runs/$RID/orders -H "authorization: Bearer $KEY" \
  -H "content-type: application/json" \
  -d '{ "side": "buy", "qty": "0.5", "rationale": "reclaimed the range high" }'

curl -s $BASE/v1/runs/$RID/positions -H "authorization: Bearer $KEY"
curl -s $BASE/v1/runs/$RID/metrics   -H "authorization: Bearer $KEY"

Your orders rest until the next bar, exactly like a rule's, you do not get a faster market than a strategy document does.

3. Watch a run. SSE

# Live event stream; resume after a drop with ?after_seq=<last seq you saw>.
curl -sN $BASE/v1/runs/$RID/stream -H "authorization: Bearer $KEY"

Every bar, signal, order and fill is an event with a monotonic seq. The journal is the source of truth, every number in /metrics is recomputable from it.

4. Python: a minimal client

import requests
BASE, KEY = "https://sonar.arium-ai.com", "sq_k_yourkeyid.sq_s_yoursecret"
h = {"authorization": f"Bearer {KEY}", "content-type": "application/json"}

strat = {
  "schema": 1, "name": "RSI dip", "symbol": "BTC-USD",
  "timeframe": "1h", "capital": "100000",
  "guards": {"max_drawdown_pct": 25},
  "rules": [
    {"id": "enter",
     "when": {"trigger": "rsi.level", "period": 14, "value": 30, "edge": "crosses_below"},
     "then": {"action": "buy", "size": {"type": "equity_pct", "value": 50},
              "stop": {"type": "atr", "multiple": 2},
              "target": {"type": "rr", "multiple": 2}, "reason": "oversold"}},
    {"id": "exit",
     "when": {"trigger": "rsi.level", "period": 14, "value": 70, "edge": "crosses_above"},
     "then": {"action": "close", "reason": "overbought"}},
  ],
}

v = requests.post(f"{BASE}/v1/validate", json=strat, headers=h).json()
print(v["english"])                 # show this to your human
assert v["runnable"], v["findings"]

run = requests.post(f"{BASE}/v1/runs", json={"mode": "rules", "strategy": strat}, headers=h).json()
rid = run["run_id"]
print(requests.get(f"{BASE}/v1/runs/{rid}/metrics", headers=h).json())

Send Idempotency-Key: <uuid> on any POST/DELETE you might retry (orders above all): the same key + same body replays the original response rather than acting twice; the same key with a different body is refused.


5. Connect an MCP client (Claude Desktop, Cursor and others)

MCP is the surface the product is built around, an agent discovers and drives everything through it. You run a small stdio proxy, the sonarquant-mcp binary, locally; in remote mode it forwards every tool call to the hosted sandbox over the same key-gated API, so your runs live on the shared box. During the closed beta the binary comes from the operator (ask, and say macOS or Linux); (the old sonarquant-mcp name still works).

Claude Desktop, add to claude_desktop_config.json:

{
  "mcpServers": {
    "sonarquant": {
      "command": "sonarquant-mcp",
      "env": {
        "SONARQUANT_URL": "https://sonar.arium-ai.com",
        "SONARQUANT_MCP_KEY": "sq_k_yourkeyid.sq_s_yoursecret"
      }
    }
  }
}

Cursor, the same server block in ~/.cursor/mcp.json.

With SONARQUANT_URL set the proxy is in remote mode and SONARQUANT_MCP_KEY is your account key (its scope decides read vs. trade). Omit SONARQUANT_URL and it runs a private in-process sandbox instead, useful for offline experiments.

Tools exposed: list_triggers, get_schema, validate_strategy, render_strategy_english, backtest_strategy, create_run, start_run, stop_run, get_portfolio, get_positions, get_metrics, place_order, cancel_order, get_market_bars. render_strategy_english exists so your agent can show its reasoning to its human in the platform's own words.


Ten complete, engine-validated strategy examples live in STRATEGIES.md. The full resource list and the design rationale are in API.md.

Routes

Every /v1 route needs Authorization: Bearer <key> except the demo endpoints and /llms.txt. Keys carry read or trade scope. The /v1/auth/* routes take the browser session instead of a key.

POST   /v1/runs                     create+start a run (Mode A or B); `data_source` (connection id or provider) binds it to one feed (default: by asset class)
GET    /v1/runs                     list your runs
GET    /v1/runs/{id}                one run: name, state, bars_seen, cash, starting_capital, equity, last_bar_ms, last_close, resting
PATCH  /v1/runs/{id}                rename the book: {name}: unique per account, 409 on a clash (POST /v1/runs/{id}/rename is the same)
POST   /v1/runs/{id}/bars           push a bar (feed-free deployments; live daemon feeds automatically)
POST   /v1/runs/{id}/orders         place                              ← Mode B
DELETE /v1/runs/{id}/orders/{oid}   cancel                             ← Mode B
GET    /v1/runs/{id}/positions      cash, equity, positions, RESTING orders (id/side/qty/type/limit/stop), realized P&L, fees, last bar
GET    /v1/runs/{id}/fills          fills
GET    /v1/runs/{id}/events         the journal, paginated (?after_seq=)
GET    /v1/runs/{id}/metrics        performance, with honesty flags + confidence
GET    /v1/runs/{id}/strategy       the run's strategy document
GET    /v1/runs/{id}/equity         the book's equity series (windowed + thinned)
GET    /v1/runs/{id}/stream         live events over SSE (resumable via ?after_seq=)
GET    /v1/alerts/stream            every book of the account in one SSE feed (?books=&kinds=&after=)
GET    /v1/alerts/ws                the same feed over a WebSocket (auth by header or first message)
POST   /v1/runs/{id}/source         re-bind a live book to another connection: {data_source: "ds_…" | "alpaca" | "finnhub" | "twelvedata"}; journal and position carry on, the feed switches within seconds
POST   /v1/runs/{id}/stop           stop a run (it leaves the list; the journal is kept, never resumed)
DELETE /v1/runs/{id}                stop and ERASE a run: journal, manifest, metrics
GET    /v1/equity                   the PORTFOLIO: all live books summed into one series
GET    /v1/markets                  crypto stat board (cached 24h exchange stats)
GET    /v1/triggers                 the trigger catalogue                  ← introspection
GET    /v1/schema                   JSON Schema for the DSL                 ← introspection
POST   /v1/validate                 check a strategy without saving (findings + English)
GET    /v1/status                   aggregate health: uptime, books running, feeds open, newest market event age (no key)
GET    /v1/templates                strategy presets with their knobs (no key needed)
GET    /v1/templates/{id}           one preset
POST   /v1/templates/{id}           render: {symbol, capital, params} → {strategy, english, findings, runnable}
POST   /v1/backtest                 run against history, no persistence
POST   /v1/backtest/history        replay a strategy over REAL history from a connected data source: {strategy, days ≤365, timeframe?, data_source?}
GET    /v1/demo                     a public, curated read-only run (no key), the SYNTHETIC demo (see below); a browser (Accept: text/html) is sent to the /demo explainer, scripts get JSON
GET    /v1/demo/dataset             the demo's published series (+ anchors): rolling window, or ?day=N from the archive (no key)
GET    /v1/demo/events              the demo run's narrated journal, oldest first (?limit=, ?since_ms=) (no key)
GET    /demo                        the explainer page: what the synthetic demo is, how it is made, viewer
GET    /llms.txt                    the whole surface, legible to an agent  ← discovery

GET    /v1/auth/datasource          every connection: `sources` [{id, label, provider, key_hint, feeding, feed}]
POST   /v1/auth/datasource          connect a vendor key {provider, key, secret?, label?, replace?}: proven, then stored; `replace` swaps a connection's key in place   ← BYOK
DELETE /v1/auth/datasource?id=      disconnect one connection (?provider= all of a provider; no query: all)
POST   /v1/auth/keys/session       browser only: pick one of the account's keys (the cookie is the proof, no password) -> a `sq_t_…` bearer standing for that key for a sliding day; survives restarts
DELETE /v1/auth/datasource          disconnect

Data sources: bring your own key

Live prices in SonarQuant come from a market-data account you hold. You connect a free key from one of three providers; the sandbox opens a feed as you, subscribed to exactly the symbols your runs trade, and delivers those bars to your runs only. Nothing is pooled across users and nothing is republished. The data stays licensed to you under your provider's personal plan, which is precisely what those plans are for.

Connect at /account#data. One key covers both US equities and crypto with every provider below, so you connect once.

Provider Free plan gives you Best for
Alpaca (recommended) Real-time trades and 1-minute bars over websocket: US stocks (IEX feed) and crypto Everything. One key, both asset classes, real-time
Twelve Data 1-minute bars over REST, 8 requests/min and 800/day If you already have an account; bars arrive a few minutes late by design
Finnhub Real-time trades over websocket: US stocks, and crypto via Coinbase If you already have an account

A run created on the live sandbox by an account with no source is refused with 409 and this page's address in the message. Connect, then retry.


Alpaca: 5 steps, about 3 minutes

  1. Create a free account at alpaca.markets/signup. Email and password only. Choose Paper Trading when asked: no identity check, no deposit.
  2. Open the dashboard and make sure the selector in the top-left says Paper. That is the free environment; it never touches real money.
  3. Generate keys. In the right-hand panel find API Keys and click Generate. Copy both values: the API Key ID and the Secret Key. The secret is shown only once.
  4. Paste them into SonarQuant at /account#data: provider Alpaca, key ID, secret. Click Connect. We test the pair against Alpaca immediately and show a green dot.
  5. Done. Your runs stream from your Alpaca account: US stocks in real time (IEX), crypto in real time.

Lost the secret? Generate a new pair on the same Alpaca screen and paste again. Connect replaces the old one.

Twelve Data: 5 steps

  1. Sign up free at twelvedata.com/register.
  2. Confirm your email.
  3. Open the dashboard (twelvedata.com/account). Your API key is shown at the top, click to copy.
  4. Paste it at /account#data (provider Twelve Data, no secret) and click Connect.
  5. Done. Bars arrive every couple of minutes per symbol: the free plan allows 800 requests a day and SonarQuant paces itself to stay inside it (one symbol ≈ every 2 min, four symbols ≈ every 8 min). Fine for strategies on 1-minute bars and up; not for anything that needs the bar the second it closes.

Finnhub: 5 steps

  1. Sign up free at finnhub.io/register.
  2. Confirm your email.
  3. Open the dashboard (finnhub.io/dashboard). Your API key is shown there: copy it.
  4. Paste it at /account#data (provider Finnhub, no secret) and click Connect.
  5. Done. US stocks stream as real-time trades; crypto streams from Coinbase through Finnhub.

What the sandbox does with your key

Symbols are spelled the sandbox's way everywhere: equities bare (AAPL, SPY), crypto as BASE-QUOTE (BTC-USD, ETH-USD). Each adapter translates to the provider's spelling at the edge.

Honesty notes

API

GET    /v1/auth/datasource   { connected, provider, key_hint, feeding: [symbols], live_required }
POST   /v1/auth/datasource   { provider: "alpaca"|"twelvedata"|"finnhub", key, secret? }
                              → 200 { connected: true, provider, summary }  |  422 { error }
DELETE /v1/auth/datasource   → 200 { connected: false }

All three take the browser session as Authorization: Bearer <session> (the account page's session, not an API key). One source per account; POST replaces.

Strategy examples

Ten example strategy documents for Mode A (POST /v1/runs {"mode":"rules"}). Every one is verified against the engine with POST /v1/validate, the JSON below is exactly what validated, and the plain-English block under each is the engine's own render_strategy_english output, not a paraphrase. Show that rendering to a human before you run anything on their behalf.

The vocabulary (all 16 triggers, their parameters, the seven actions, the exit and size types, and the four circuit-breaker guards) comes from GET /v1/triggers and GET /v1/schema. Start there when composing your own.

validate also emits lints as warnings. "entry has no stop", "no rule ever closes a position", shown per example below. They do not block a run; they are the platform telling you what a careful trader would add.


1. RSI mean reversion

Renders as:

RSI mean reversion. BTC-USD, 1h bars, 100,000 USD.

Close the position when RSI(14) crosses above 70. Reason: Overbought.

Enter long when RSI(14) crosses below 30. Size 50% of equity. Stop 2 × ATR below entry. Target 2 × the risk. Reason: Oversold bounce.

Stop the run if drawdown reaches 25%.

{
  "schema": 1,
  "name": "RSI mean reversion",
  "symbol": "BTC-USD",
  "timeframe": "1h",
  "capital": "100000",
  "guards": {
    "max_drawdown_pct": 25
  },
  "rules": [
    {
      "id": "enter",
      "when": {
        "trigger": "rsi.level",
        "period": 14,
        "value": 30,
        "edge": "crosses_below"
      },
      "then": {
        "action": "buy",
        "size": {
          "type": "equity_pct",
          "value": 50
        },
        "stop": {
          "type": "atr",
          "multiple": 2
        },
        "target": {
          "type": "rr",
          "multiple": 2
        },
        "reason": "oversold bounce"
      }
    },
    {
      "id": "exit",
      "when": {
        "trigger": "rsi.level",
        "period": 14,
        "value": 70,
        "edge": "crosses_above"
      },
      "then": {
        "action": "close",
        "reason": "overbought"
      }
    }
  ]
}

2. Moving-average trend follow

Renders as:

MA cross trend. ETH-USD, 4h bars, 100,000 USD.

Close the position when the 20-period EMA crosses below the 50-period EMA. Reason: Trend over.

Enter long when the 20-period EMA crosses above the 50-period EMA. Size 100% of equity. Reason: Fast over slow.

Stop the run if drawdown reaches 30%.

Validator lints: entry has no stop

{
  "schema": 1,
  "name": "MA cross trend",
  "symbol": "ETH-USD",
  "timeframe": "4h",
  "capital": "100000",
  "guards": {
    "max_drawdown_pct": 30
  },
  "rules": [
    {
      "id": "long",
      "when": {
        "trigger": "ma.cross",
        "period": 20,
        "slow": 50,
        "ma_type": "ema",
        "edge": "crosses_above"
      },
      "then": {
        "action": "buy",
        "size": {
          "type": "equity_pct",
          "value": 100
        },
        "reason": "fast over slow"
      }
    },
    {
      "id": "flat",
      "when": {
        "trigger": "ma.cross",
        "period": 20,
        "slow": 50,
        "ma_type": "ema",
        "edge": "crosses_below"
      },
      "then": {
        "action": "close",
        "reason": "trend over"
      }
    }
  ]
}

3. MACD signal cross

Renders as:

MACD momentum. BTC-USD, 1h bars, 50,000 USD.

Close the position when the MACD line crosses below its signal line. Reason: Macd turned down.

Enter long when the MACD line crosses above its signal line. Size 100% of available cash. Stop 3% below entry. Reason: Macd turned up.

Stop trading for the day after a 5% loss.

{
  "schema": 1,
  "name": "MACD momentum",
  "symbol": "BTC-USD",
  "timeframe": "1h",
  "capital": "50000",
  "guards": {
    "daily_loss_limit_pct": 5
  },
  "rules": [
    {
      "id": "buy",
      "when": {
        "trigger": "macd.cross_signal",
        "edge": "crosses_above"
      },
      "then": {
        "action": "buy",
        "size": {
          "type": "cash_pct",
          "value": 100
        },
        "stop": {
          "type": "pct",
          "value": 3
        },
        "reason": "macd turned up"
      }
    },
    {
      "id": "sell",
      "when": {
        "trigger": "macd.cross_signal",
        "edge": "crosses_below"
      },
      "then": {
        "action": "close",
        "reason": "macd turned down"
      }
    }
  ]
}

4. Bollinger breakout

Renders as:

Bollinger breakout. SOL-USD, 15m bars, 20,000 USD.

Enter long when price crosses above the upper Bollinger band (20, 2). Size 40% of equity. Stop 2% below entry. Target 5% above entry. Reason: Upper band break.

Stop the run if drawdown reaches 20%.

Validator lints: no rule ever closes a position

{
  "schema": 1,
  "name": "Bollinger breakout",
  "symbol": "SOL-USD",
  "timeframe": "15m",
  "capital": "20000",
  "guards": {
    "max_drawdown_pct": 20
  },
  "rules": [
    {
      "id": "break",
      "when": {
        "trigger": "bb.band",
        "period": 20,
        "multiple": 2,
        "edge": "crosses_above"
      },
      "then": {
        "action": "buy",
        "size": {
          "type": "equity_pct",
          "value": 40
        },
        "target": {
          "type": "pct",
          "value": 5
        },
        "stop": {
          "type": "pct",
          "value": 2
        },
        "reason": "upper band break"
      }
    }
  ]
}

5. Donchian N-bar high

Renders as:

20-bar breakout. BTC-USD, 1D bars, 100,000 USD.

Close the position when price makes a new 10-bar low. (10-day low)

Enter long when price makes a new 20-bar high. Size 100% of equity. Stop 3 × ATR below entry. Only when flat. Reason: New 20-day high.

Stop the run if drawdown reaches 25%.

{
  "schema": 1,
  "name": "20-bar breakout",
  "symbol": "BTC-USD",
  "timeframe": "1D",
  "capital": "100000",
  "guards": {
    "max_drawdown_pct": 25
  },
  "rules": [
    {
      "id": "enter",
      "when": {
        "trigger": "price.n_bar_high",
        "period": 20
      },
      "only_when_flat": true,
      "then": {
        "action": "buy",
        "size": {
          "type": "equity_pct",
          "value": 100
        },
        "stop": {
          "type": "atr",
          "multiple": 3
        },
        "reason": "new 20-day high"
      }
    },
    {
      "id": "exit",
      "when": {
        "trigger": "price.n_bar_low",
        "period": 10
      },
      "then": {
        "action": "close",
        "reason": "10-day low"
      }
    }
  ]
}

6. ADX-filtered trend

Renders as:

ADX trend filter. ETH-USD, 1h bars, 100,000 USD.

Enter long when all are met: ADX(14) is at or above 25; the 10-period EMA crosses above the 30-period EMA. Size the position so the stop risks 1% of equity. Stop 2 × ATR below entry. Target 3 × the risk. Reason: Trend confirmed by ADX.

Stop the run if drawdown reaches 25%.

Validator lints: no rule ever closes a position

{
  "schema": 1,
  "name": "ADX trend filter",
  "symbol": "ETH-USD",
  "timeframe": "1h",
  "capital": "100000",
  "guards": {
    "max_drawdown_pct": 25
  },
  "rules": [
    {
      "id": "enter",
      "when": {
        "all": [
          {
            "trigger": "adx.level",
            "period": 14,
            "value": 25,
            "edge": "is_above"
          },
          {
            "trigger": "ma.cross",
            "period": 10,
            "slow": 30,
            "ma_type": "ema",
            "edge": "crosses_above"
          }
        ]
      },
      "then": {
        "action": "buy",
        "size": {
          "type": "risk_pct",
          "value": 1
        },
        "stop": {
          "type": "atr",
          "multiple": 2
        },
        "target": {
          "type": "rr",
          "multiple": 3
        },
        "reason": "trend confirmed by ADX"
      }
    }
  ]
}

7. Stochastic oversold

Renders as:

Stochastic dip. SOL-USD, 30m bars, 20,000 USD.

Enter long when Stochastic %K(14) crosses above 20. Size 50% of equity. Stop 3% below entry. Target 6% above entry. Only when flat. Reason: Stoch turning up from oversold.

At most 3 trades a day.

Validator lints: no rule ever closes a position

{
  "schema": 1,
  "name": "Stochastic dip",
  "symbol": "SOL-USD",
  "timeframe": "30m",
  "capital": "20000",
  "guards": {
    "max_trades_per_day": 3
  },
  "rules": [
    {
      "id": "buy",
      "when": {
        "trigger": "stoch.level",
        "period": 14,
        "value": 20,
        "edge": "crosses_above"
      },
      "only_when_flat": true,
      "then": {
        "action": "buy",
        "size": {
          "type": "equity_pct",
          "value": 50
        },
        "stop": {
          "type": "pct",
          "value": 3
        },
        "target": {
          "type": "pct",
          "value": 6
        },
        "reason": "stoch turning up from oversold"
      }
    }
  ]
}

8. Volume-spike momentum

Renders as:

Volume breakout. BTC-USD, 5m bars, 30,000 USD.

Enter long when all are met: volume reaches 3× its 20-bar average; there have been 3 consecutive higher closes. Size 50% of available cash. Stop 1.5 × ATR below entry. Target 2 × the risk. Then wait 5 bars. Reason: Volume + momentum.

Stop the run if drawdown reaches 15%. At most 10 trades a day.

Validator lints: no rule ever closes a position

{
  "schema": 1,
  "name": "Volume breakout",
  "symbol": "BTC-USD",
  "timeframe": "5m",
  "capital": "30000",
  "guards": {
    "max_drawdown_pct": 15,
    "max_trades_per_day": 10
  },
  "rules": [
    {
      "id": "enter",
      "when": {
        "all": [
          {
            "trigger": "volume.spike",
            "period": 20,
            "multiple": 3
          },
          {
            "trigger": "price.consecutive_up",
            "period": 3
          }
        ]
      },
      "cooldown_bars": 5,
      "then": {
        "action": "buy",
        "size": {
          "type": "cash_pct",
          "value": 50
        },
        "stop": {
          "type": "atr",
          "multiple": 1.5
        },
        "target": {
          "type": "rr",
          "multiple": 2
        },
        "reason": "volume + momentum"
      }
    }
  ]
}

9. Time-boxed take-profit

Renders as:

Managed exit. ETH-USD, 1h bars, 100,000 USD.

Close the position when the open position's unrealized P&L crosses above 8%. Reason: Take 8% profit.

Close the position when the position has been open for 48 or more bars. Reason: Stale after 48 bars.

Enter long when RSI(14) crosses above 40. Size 60% of equity. Only when flat. Reason: Momentum entry.

Stop the run if drawdown reaches 20%.

Validator lints: entry has no stop

{
  "schema": 1,
  "name": "Managed exit",
  "symbol": "ETH-USD",
  "timeframe": "1h",
  "capital": "100000",
  "guards": {
    "max_drawdown_pct": 20
  },
  "rules": [
    {
      "id": "enter",
      "when": {
        "trigger": "rsi.level",
        "period": 14,
        "value": 40,
        "edge": "crosses_above"
      },
      "only_when_flat": true,
      "then": {
        "action": "buy",
        "size": {
          "type": "equity_pct",
          "value": 60
        },
        "reason": "momentum entry"
      }
    },
    {
      "id": "take",
      "when": {
        "trigger": "pos.unrealized_pct",
        "value": 8,
        "edge": "crosses_above"
      },
      "then": {
        "action": "close",
        "reason": "take 8% profit"
      }
    },
    {
      "id": "timeout",
      "when": {
        "trigger": "pos.age_bars",
        "value": 48,
        "edge": "crosses_above"
      },
      "then": {
        "action": "close",
        "reason": "stale after 48 bars"
      }
    }
  ]
}

10. Price-level entry with stop

Renders as:

Support buy. BTC-USD, 4h bars, 100,000 USD.

Enter long when price crosses above 60000. Size 100% of available cash. Stop at 58000. Target at 68000. No more than 1 times. Reason: Reclaim 60k.

Stop the run if drawdown reaches 20%.

Validator lints: no rule ever closes a position

{
  "schema": 1,
  "name": "Support buy",
  "symbol": "BTC-USD",
  "timeframe": "4h",
  "capital": "100000",
  "guards": {
    "max_drawdown_pct": 20
  },
  "rules": [
    {
      "id": "buy",
      "when": {
        "trigger": "price.level",
        "value": 60000,
        "edge": "crosses_above"
      },
      "max_fires": 1,
      "then": {
        "action": "buy",
        "size": {
          "type": "cash_pct",
          "value": 100
        },
        "stop": {
          "type": "price",
          "value": 58000
        },
        "target": {
          "type": "price",
          "value": 68000
        },
        "reason": "reclaim 60k"
      }
    }
  ]
}

11. Scaled ladder (grid)

Scaled trading the way a scale trader means it: a ladder of resting limit orders, run by the engine itself. Six inputs; the engine derives the rest.

{
  "schema": 1, "name": "SOL ladder", "symbol": "SOL-USD", "timeframe": "1m", "capital": "200000",
  "guards": { "max_drawdown_pct": 30 },
  "grid": { "side": "long", "high": "120", "low": "98", "tranches": 25, "qty": "800", "delta": "0.5" }
}

What the engine does with it:

rules may be omitted with a grid, or combined with one. The New book dialog and the Backtests tab offer this as the "Scaled grid" template with a six-field form; tools/sq.py takes --rules scaled:long:120:98:25:800:0.5. Resume rebuilds the whole ladder from the journal, so a restart changes nothing.


Tick size

When a book starts, the API looks up the symbol's minimum price increment once and writes it into the book's manifest: Coinbase's public products endpoint for crypto (quote_increment), SEC Rule 612 for US equities ($0.01 at or above $1, $0.0001 below), a magnitude fallback when the venue cannot be asked. Every rung is snapped to that tick toward the less favourable side: entries away from the market (a buy rounds down, a sell up), exits toward less profit, so the ladder rests only on prices the venue could print. The tick never changes for the life of the book (replay must run under the same instrument); books created before this existed have no tick and keep their raw 8-decimal levels. Sub-dollar symbols such as ARB-USD are the reason: a 25-rung ladder between 0.16 and 0.20 needs levels like 0.19833, and every price display now keeps five significant digits below one dollar.

12. Presets (/v1/templates)

The dashboard's New-book and Backtests forms, the terminal client and the API share one catalogue of fourteen presets (crates/api/src/templates.rs):

id rule knobs
ema_cross fast MA crosses above slow to enter, below to exit fast, slow, MA type, bar size, size, stop×ATR, target×risk
golden_cross 50/200 on daily bars, long on the golden cross, flat on the death cross fast, slow, MA type, bar size, size, stop %
price_vs_ma price crosses above / below one average period, MA type, bar size, size, stop, target
rsi_reversion RSI through oversold to enter, through overbought to exit period, levels, bar size, size, stop, target
rsi_trend_filter the RSI dip only while price is above a slow average + filter period, MA type
macd_cross MACD(12,26,9) line vs signal, optional ADX floor ADX min, bar size, size, stop, target
bb_reversion close through the lower band, out at the middle (SMA) or upper band period, deviations, exit band, stop %
bb_breakout close above the upper band, out through the middle band period, deviations, stop, target
donchian new N-bar high in, new M-bar low out (Turtle) N, M, stop, optional target
adx_trend the MA cross gated by ADX ≥ level fast, slow, MA type, ADX period + level, stop, target
stoch_oversold %K up through oversold in, up through overbought out period, levels, stop, target
volume_momentum volume × average + N higher closes volume period + multiple, run, cooldown, stop, target
down_run_bounce after M lower closes buy, hold N bars (uses price.consecutive_down) run, hold bars, stop %, target %
keltner_breakout close above the upper Keltner band in, back through the EMA out period, ATR multiple, stop, target
supertrend long on the bullish flip, flat on the bearish ATR period + multiple, optional stop, target
vwap_trend price crosses above the session VWAP in, below out cooldown, stop, target
orb break above the session's opening range, once per session, fixed hold range minutes, hold bars, stop %, target %
trailing_trend new N-bar high in; the only exit is a stop trailing N × ATR (trail_atr) N, trail multiple
ichimoku price crosses above the cloud with Tenkan over Kijun; out below the cloud stop, target
scaled the ladder of §11 side, high, low, tranches, qty, delta, optional stop

Every preset carries the drawdown and trades-per-day guards and max_open_positions: 1. Twenty in all; the last six ride the triggers keltner.band, supertrend.flip, vwap.cross, orb.breakout, ichimoku.cloud / ichimoku.tk_cross and the exit kinds trail_pct / trail_atr (docs/TRIGGERS.md marks each as implemented).