Open the lab

Method

What the engine does, exactly, and where its numbers come from. This is the only page that explains the model's limits; the rest of the site points here.

Strategy DSL v1

A strategy is a JSON document with "dsl": 1, validated by the published JSON Schema. One token (universe.mint, optional pair_pool), a timeframe (1m, 5m, 15m or 1h: both the candle size and how often it's evaluated), a simulated budget_usd (default 100), an ordered list of up to 10 rules and a risk block.

Each rule is when (1–8 conditions joined by all or any) → then (one action), with optional once and cooldown. Conditions: price_change_pct, price, mcap_usd, liquidity_usd, volume_usd, drawdown_from_high_pct, position_open, position_pnl_pct, time_in_position, budget_left_usd. Actions: buy_usd, buy_pct_of_budget (of the starting budget), sell_pct_of_position, sell_all. Windows are multiples of the timeframe, up to 7 days. Risk: stop_loss_pct, take_profit_pct, trailing_stop_pct, max_position_usd, max_buys (per position).

Anything outside this set is unsupported. The compiler (Claude) must refuse and name the part it can't express: indicators, other assets, clock times, resting orders, shorting, leverage, sentiment, wallets. It never approximates silently. The plain-language rules you see are rendered from the DSL itself, so the text and the rules that run can't disagree.

Engine (dr-engine/1)

  1. Candles are walked in order. For candle i: a pending order fills at the open of i; then, on the close of i, the engine decides.
  2. Risk first: with a position open, if position P&L ≤ −stop loss, or the close is the trailing-stop distance below the highest close since entry, or P&L ≥ take profit, the decision is "sell everything". Stops are checked on closes, not intrabar lows, and fill at the next open: a gap down fills below the stop.
  3. Otherwise the rules in order: the first rule whose conditions hold and whose action can execute wins. A rule is skipped when it's used up (once), cooling down, a sell with no position, or a buy that the caps (max_buys, max_position_usd, cash left) clip below $1.
  4. The decision fills at the open of candle i+1. Conditions only read candles ≤ i; the test suite runs the engine on a series that throws if anything after i is read, and checks that changing future candles never changes past signals.
  5. Windows that reach before the first candle are false: the first part of a backtest is warm-up, as long as the longest window.
  6. Same DSL + same candles, snapshots and cost inputs give a byte-identical result. Every result carries dsl_hash, data_hash and result_hash (SHA-256 of its canonical JSON), and POST /api/v1/backtest with the same to and sol_usd reproduces it.

Stats: total PnL (equity marked at the last close, an open position not sold), buy-and-hold over the same window with the same entry costs, max drawdown on the candle-by-candle equity, win rate over closed positions, exposure (share of candles holding a position), number of fills. No projected or annualized returns, ever.

Cost model

Every simulated trade pays three things, all shown on each fill:

Liquidity at a past candle is estimated from the nearest DexScreener snapshot at or before it (the earliest one if there's none before): L(t) = L_snap × √(p(t) / p_snap), which is exact for a constant-product pool when nobody adds or removes liquidity in between. Market cap scales linearly with price from the snapshot. Snapshots are taken when a token is first used and every 5 minutes while a strategy on it runs, so paper trading reads real liquidity and older backtests read an estimate.

Data sources and refresh

Limits of the model

The paper record

"Start dry run" activates a version. The cron evaluates it with the same engine and cost model on each newly closed candle (every minute for 1m/5m strategies, every 5 minutes for 15m/1h), and every signal and simulated fill is appended to the record: strategy, version, sequence, candle time, rule, side, price, size, costs and running equity. Rows are never updated or deleted; the database refuses it. Editing a running strategy creates a new version with its own record and a fresh budget. The SOL/USD price used for network fees is the one at the time of the fill.

Merkle spec (DR1)

Test vectors

Three leaves and their root. Your implementation should reproduce these hashes exactly.

leaf 0

{"candle_t":1789999800,"dsl_hash":"9f2c2a1b4bd3c0d4ffb2b4d3a7c1e0f5b6a9d8c7e6f5a4b3c2d1e0f9a8b7c6d5","equity_usd":"100","fee_usd":"0","kind":"signal","network_usd":"0","pct":"0","price":"0.00214","reason":"rule","recorded_at":1790000160,"rule":1,"seq":1,"side":"buy","slippage_usd":"0","spec":"dr1","strategy_id":"s_vector2345","t":1790000100,"tokens":"0","usd":"25","version":1}

sha256 = d0cd4a9bf9ef9f9daf0c81867a5fdec10986eb0df44228f6fa449b0fb37c8827

leaf 1

{"candle_t":1790000100,"dsl_hash":"9f2c2a1b4bd3c0d4ffb2b4d3a7c1e0f5b6a9d8c7e6f5a4b3c2d1e0f9a8b7c6d5","equity_usd":"99.9124271488","fee_usd":"0.06245","kind":"fill","network_usd":"0.02","pct":"0","price":"0.00213","reason":"rule","recorded_at":1790000460,"rule":1,"seq":2,"side":"buy","slippage_usd":"0.0031228512","spec":"dr1","strategy_id":"s_vector2345","t":1790000100,"tokens":"11695.9230932","usd":"25","version":1}

sha256 = 331afa2510e2351e09b91260b6a12afffc65c12931c858e3f16ec5b27cc35487

leaf 2

{"candle_t":1790003400,"dsl_hash":"9f2c2a1b4bd3c0d4ffb2b4d3a7c1e0f5b6a9d8c7e6f5a4b3c2d1e0f9a8b7c6d5","equity_usd":"104.356","fee_usd":"0","kind":"signal","network_usd":"0","pct":"100","price":"0.00251","reason":"take_profit","recorded_at":1790003760,"rule":0,"seq":3,"side":"sell","slippage_usd":"0","spec":"dr1","strategy_id":"s_vector2345","t":1790003700,"tokens":"0","usd":"0","version":1}

sha256 = 71dd2eba28ab36f2c00b8b860a168eccb644ec17ca04e8ede3b8baf26c80dd34

root = fd5ef3e1c89d1da3d0cb6ff1f0fedc4671d185bc33fbb578fa74b8ccf5a5a6aa

proof(leaf 0) = [331afa2510e2351e09b91260b6a12afffc65c12931c858e3f16ec5b27cc35487, 077df11d124316ac8e95c4c77f5700a7a69812d58cbee051a6dfb4911edc0086]