Strategy DSL
The Strategy DSL is the rule language behind every backtest and live trading model in TraderTape. It's a small, declarative JSON dialect that expresses entry conditions, exit rules, addon (averaging-down) rules, conviction scoring, and runtime config.
You'll mostly interact with the DSL via the visual rule builder on the Strategies page โ you don't need to write the JSON by hand. But understanding the underlying shape is useful when you want to compare strategies, debug edge cases, or use the API directly.
A complete strategy
{
"name": "V1 Momentum",
"version": 1,
"base_model": "v1_momentum",
"entry_rules": {
"type": "and",
"conditions": [
{
"type": "compare",
"left": {"type": "indicator", "name": "close"},
"op": ">",
"right": {"type": "indicator", "name": "sma20"}
},
{
"type": "compare",
"left": {"type": "indicator", "name": "rsi14"},
"op": "<=",
"right": {"type": "literal", "value": 70}
}
]
},
"exit_rules": [
{
"name": "rsi_overbought",
"priority": 0,
"ignore_min_hold": false,
"condition": {
"type": "compare",
"left": {"type": "indicator", "name": "rsi14"},
"op": ">",
"right": {"type": "literal", "value": 70}
},
"exit_price": {"type": "indicator", "name": "close"}
},
{
"name": "sma20_cross",
"priority": 1,
"ignore_min_hold": false,
"condition": {
"type": "and",
"conditions": [
{"type": "compare", "left": {"type": "indicator", "name": "close"}, "op": "<", "right": {"type": "indicator", "name": "sma20"}},
{"type": "compare", "left": {"type": "position", "field": "total_bars_held"}, "op": ">", "right": {"type": "literal", "value": 2}}
]
},
"exit_price": {"type": "indicator", "name": "close"}
}
],
"addon_rules": {"enabled": false},
"conviction_scoring": [...],
"config": {
"min_hold": 10,
"max_hold": null,
"atr_stop_mult": 0,
"exit_buffer_pct": 0
}
}
This is the actual DSL definition for V1 Momentum. Let's break it down.
Top-level keys
nameโ display nameversionโ incremented on every savebase_modelโ the parent built-in model (used as a hint for the backtest engine, optional)entry_rulesโ a single rule node (AND/OR/NOT/compare/near/always)exit_rulesโ an ordered list of exit rules (lowerpriorityfires first)addon_rulesโ{"enabled": false}or a full addon specconviction_scoringโ list of scoring dimensions (see Conviction Scoring)configโ runtime knobs like min/max hold, ATR stop multiplier, exit buffer
Rule nodes
Every condition in the DSL is a rule node. There are six node types:
and
Logical conjunction. All children must be true.
{
"type": "and",
"conditions": [
{"type": "compare", ...},
{"type": "compare", ...}
]
}
or
Logical disjunction. At least one child must be true.
{
"type": "or",
"conditions": [...]
}
not
Negation. Inverts the single child.
{
"type": "not",
"conditions": [{"type": "compare", ...}]
}
(Note: conditions is a list with exactly one element, for consistency with and / or.)
compare
The leaf. Compares two values.
{
"type": "compare",
"left": {"type": "indicator", "name": "rsi14"},
"op": "<=",
"right": {"type": "literal", "value": 45}
}
Operators: >, >=, <, <=, ==, !=.
left and right can be any value node (see below).
near
A specialized compare for "indicator A is within X% of indicator B". Common pattern for "price near SMA20".
{
"type": "near",
"left": {"type": "indicator", "name": "close"},
"right": {"type": "indicator", "name": "sma20"},
"tolerance_pct": 1.0,
"direction": "above"
}
direction is one of above (left within tolerance% above right), below (within tolerance% below), or either (default).
always
A trivial true. Useful as a placeholder or for "fire on every scan".
{"type": "always"}
Value nodes
A compare's left / right (and a near's left / right) can be any of:
literal
A constant.
{"type": "literal", "value": 70}
indicator
A computed indicator. Names match the indicator registry โ see the list below.
{"type": "indicator", "name": "rsi14"}
{"type": "indicator", "name": "sma_50"}
{"type": "indicator", "name": "bb_lower"}
Indicators with periods can use either the canonical name (sma20, rsi14) or a parameterized form (sma_20, rsi_14). Both work.
position
A field from the current open position (only valid in exit and addon rules).
{"type": "position", "field": "total_bars_held"}
{"type": "position", "field": "dip_pct"}
{"type": "position", "field": "avg_entry_price"}
Available fields:
entry_priceโ original entryavg_entry_priceโ average across all entries (for add-on positions)total_qtyโ total quantity heldtotal_bars_heldโ bars since first entrybars_since_last_entryโ bars since most recent entry (for add-on positions)dip_pctโ current price relative to avg entry, as a percentagenum_entriesโ total entry count (1 for non-addon, 2-3 for V3 add-ons)
config
A value from the strategy's config block (e.g. max_hold).
{"type": "config", "key": "max_hold"}
This lets exit rules reference config values without hardcoding them, so you can tune max_hold from the deploy modal without touching the rule tree.
Indicator registry
Built-in indicators (case-insensitive, parameterized periods supported):
| Name | What it computes | Default period |
|---|---|---|
close, open, high, low, volume | Current bar OHLCV | n/a |
prev_close | Previous bar close | n/a |
rsi14, rsi_N | RSI | 14 |
sma20, sma_N | Simple moving average | 20 |
ema20, ema_N | Exponential moving average | 20 |
wma_N | Weighted moving average | 20 |
tema_N | Triple exponential moving average | 20 |
atr14, atr_N | Average True Range | 14 |
bb_upper, bb_middle, bb_lower | Bollinger Bands(20, 2) | 20, 2ฯ |
macd_line, macd_signal, macd_hist | MACD(12, 26, 9) | 12, 26, 9 |
supertrend | SuperTrend(10, 3) | 10, 3 |
supertrend_dir | SuperTrend direction (up / down) | n/a |
stoch_k, stoch_d | Stochastic(14, 3) | 14, 3 |
stoch_rsi_k, stoch_rsi_d | Stochastic RSI | 14 |
adx14 | Average Directional Index | 14 |
aroon_up, aroon_down | Aroon | 25 |
cci20 | Commodity Channel Index | 20 |
williams_r14 | Williams %R | 14 |
obv | On-Balance Volume | n/a |
vwap | Volume Weighted Average Price (session) | n/a |
Plus three derived context fields that the strategy registry computes:
vol_regimeโvol_calm,vol_normal,vol_elevated,vol_spike(based on ATR percentile vs history)trend_alignmentโfully_aligned,partial,fully_against(based on close vs all MAs)sma20_dist_pctโ current close as a % distance from SMA20
These are particularly useful for entry filters and conviction scoring.
Exit rules in detail
Exit rules are a list, evaluated in priority order each scan day. The first one whose condition is true fires the exit. Each rule has:
{
"name": "rsi_overbought",
"priority": 0,
"ignore_min_hold": false,
"condition": {...rule node...},
"exit_price": {...value node...}
}
nameโ short label for the audit log (e.g.rsi_overbought)priorityโ lower numbers fire first when multiple rules matchignore_min_holdโ if true, this rule fires even beforeconfig.min_holddays have passed (use for emergency stops)conditionโ a rule node that must be true for the exit to fireexit_priceโ usually{"type": "indicator", "name": "close"}but can beopen,high,low, or any literal
Addon rules in detail
If your strategy supports averaging down:
{
"addon_rules": {
"enabled": true,
"max_adds": 2,
"min_dip_pct": 3.0,
"max_dip_pct": 10.0,
"min_days_between": 3,
"condition": {
"type": "and",
"conditions": [
{"type": "compare", "left": {"type": "indicator", "name": "rsi14"}, "op": "<=", "right": {"type": "literal", "value": 40}},
{"type": "compare", "left": {"type": "indicator", "name": "vol_regime"}, "op": "!=", "right": {"type": "literal", "value": "vol_spike"}}
]
}
}
}
enabledโ false to disable add-onsmax_addsโ maximum number of add-on entries (somax_adds: 2allows the original entry + 2 add-ons = 3 total)min_dip_pct/max_dip_pctโ the dip range from average entry where add-ons are allowed (3-10% means "the price has fallen 3-10% below avg_entry")min_days_betweenโ minimum bars between add-onsconditionโ an additional DSL condition that must also be true (typical: RSI even more oversold than entry, or vol regime not in spike)
When an add-on fires, the position's total_qty, total_invested, avg_entry_price, and last_entry_date are all updated. The min_hold clock restarts from last_entry_date, so a 15-day min hold with two add-ons effectively delays the first exit by 30 days.
Conviction scoring
Each entry signal gets a 0-100 conviction score based on how it stacks up across several dimensions. The full schema is covered in Conviction Scoring, but the basic shape is:
{
"conviction_scoring": [
{
"indicator": "rsi14",
"ranges": [
{"min": 60, "max": 65, "score": 40},
{"min": 0, "max": 35, "score": 35}
]
},
{
"indicator": "vol_regime",
"values": [
{"value": "vol_spike", "score": 18},
{"value": "vol_calm", "score": 5}
]
}
]
}
Numeric indicators use ranges (matched in order, first hit wins). Categorical indicators (like vol_regime) use values. The total score is the sum across all dimensions, normalized to 0-100.
Config
{
"config": {
"min_hold": 10,
"max_hold": null,
"atr_stop_mult": 0,
"exit_buffer_pct": 0
}
}
min_holdโ minimum bars before any exit can fire (unless an exit rule hasignore_min_hold: true)max_holdโ bars after which a forced exit fires (nullfor no max)atr_stop_multโ N ร ATR(14) below entry as a stop GTT, set at fill time.0disables stops.exit_buffer_pctโ small price improvement on exit GTTs (0.5% means "place the SELL limit 0.5% above the trigger")
Evaluation order
For every scan day, the DSL interpreter does:
- For each open position (sorted by entry date):
- Evaluate all exit rules in priority order
- First match โ fire exit (subject to
min_holdandignore_min_hold) - If no exit matched, evaluate addon rules
- First addon match โ fire addon (subject to
min_days_betweenandmax_adds)
- For each symbol in the universe (sorted by symbol):
- Evaluate entry rules
- If true, generate a pending entry signal
- Score all new entry signals by conviction
- Allocate capital to entry signals in conviction order until caps are hit
This ordering matters: exits run before entries on the same day, so capital frees up before new positions are evaluated. Sector caps are checked at allocation time, not at signal generation.
What the DSL can't do (yet)
- Custom indicators. New indicators require platform-side code changes. There's no UI to define a new indicator from scratch.
- Multi-symbol conditions. Each rule sees one symbol at a time. You can't say "buy A only if B is also above its SMA20".
- Regime detection. No global "are we in a bull market?" check. The closest you can do is filter by SMA50 > SMA200 on each symbol individually.
- Time-of-day rules. All rules evaluate on daily bars. Intraday timing isn't expressible.
- Lookback parameters at runtime.
sma_50is hardcoded. You can't say "buy when close > sma_N where N is determined by ATR". - Conditional sizing. Position size is fixed at the portfolio level. You can't size by conviction score (yet โ this is on the roadmap).
If you need any of these, you currently need to either pick a different design or extend the DSL interpreter in code.
Editing the DSL
Three ways:
- Visual rule builder (recommended) โ Strategies โ New / Edit. Click-and-drop, no JSON.
- Strategy chat โ natural language description that gets converted to DSL.
- API โ
POST /api/strategies/{id}with the full JSON. See API Overview.
The visual builder is the most pleasant. The API is useful for programmatic strategy generation.
Performance
A full DSL evaluation on 100 symbols ร 6 years of daily bars takes 2-8 seconds, depending on rule complexity. Indicator values and derived context fields are precomputed once per (symbol, day), so each rule evaluation reduces to dictionary lookups.
Next
- Built-in Models โ V0โV4 expressed in this DSL
- Conviction Scoring โ the scoring dimension format in detail
- Strategies Guide โ building strategies via the UI