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.
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.
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.
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" }
# 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.
# 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.
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.
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.
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
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.
/account#data: provider Alpaca, key ID, secret.
Click Connect. We test the pair against Alpaca
immediately and show a green dot.Lost the secret? Generate a new pair on the same Alpaca screen and paste again. Connect replaces the old one.
/account#data (provider
Twelve Data, no secret) and click
Connect./account#data (provider
Finnhub, no secret) and click Connect.ds_…) and a name you choose. Replace a connection's key
in place (books bound to it keep their binding) or disconnect it by id.
Two Alpaca keys are two vendor sockets, which is exactly what Alpaca's
one-socket-per-key rule needs.data_source on /v1/backtest/history overrides
this when you know what you want.data_source when you create it: a connection id, or a
provider name when you hold one key of that provider (several → a 409
listing the ids), or leave it out and the sandbox picks by asset class:
crypto → Finnhub (Coinbase's real tape. Alpaca's own crypto venue is
thin), US equities → Alpaca (IEX prints, and free history for
backtests), Twelve Data as the polled fallback. Naming a source you have
not connected is refused (409). The binding is recorded in the book and
survives restarts.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.
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.
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.
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"
}
}
]
}
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"
}
}
]
}
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"
}
}
]
}
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"
}
}
]
}
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"
}
}
]
}
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"
}
}
]
}
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"
}
}
]
}
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"
}
}
]
}
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"
}
}
]
}
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"
}
}
]
}
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:
"stop" is given: the ladder keeps
resting if price leaves the range, like a grid on a venue. With an
explicit "stop", a close beyond it cancels every rung,
closes the position and halts."side": "short" mirrors everything: sell limits at the
levels, covers at level − delta.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.
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.
/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).