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 name
  • version โ€” incremented on every save
  • base_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 (lower priority fires first)
  • addon_rules โ€” {"enabled": false} or a full addon spec
  • conviction_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 entry
  • avg_entry_price โ€” average across all entries (for add-on positions)
  • total_qty โ€” total quantity held
  • total_bars_held โ€” bars since first entry
  • bars_since_last_entry โ€” bars since most recent entry (for add-on positions)
  • dip_pct โ€” current price relative to avg entry, as a percentage
  • num_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):

NameWhat it computesDefault period
close, open, high, low, volumeCurrent bar OHLCVn/a
prev_closePrevious bar closen/a
rsi14, rsi_NRSI14
sma20, sma_NSimple moving average20
ema20, ema_NExponential moving average20
wma_NWeighted moving average20
tema_NTriple exponential moving average20
atr14, atr_NAverage True Range14
bb_upper, bb_middle, bb_lowerBollinger Bands(20, 2)20, 2ฯƒ
macd_line, macd_signal, macd_histMACD(12, 26, 9)12, 26, 9
supertrendSuperTrend(10, 3)10, 3
supertrend_dirSuperTrend direction (up / down)n/a
stoch_k, stoch_dStochastic(14, 3)14, 3
stoch_rsi_k, stoch_rsi_dStochastic RSI14
adx14Average Directional Index14
aroon_up, aroon_downAroon25
cci20Commodity Channel Index20
williams_r14Williams %R14
obvOn-Balance Volumen/a
vwapVolume 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 match
  • ignore_min_hold โ€” if true, this rule fires even before config.min_hold days have passed (use for emergency stops)
  • condition โ€” a rule node that must be true for the exit to fire
  • exit_price โ€” usually {"type": "indicator", "name": "close"} but can be open, 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-ons
  • max_adds โ€” maximum number of add-on entries (so max_adds: 2 allows 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-ons
  • condition โ€” 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 has ignore_min_hold: true)
  • max_hold โ€” bars after which a forced exit fires (null for no max)
  • atr_stop_mult โ€” N ร— ATR(14) below entry as a stop GTT, set at fill time. 0 disables 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:

  1. For each open position (sorted by entry date):
    • Evaluate all exit rules in priority order
    • First match โ†’ fire exit (subject to min_hold and ignore_min_hold)
    • If no exit matched, evaluate addon rules
    • First addon match โ†’ fire addon (subject to min_days_between and max_adds)
  2. For each symbol in the universe (sorted by symbol):
    • Evaluate entry rules
    • If true, generate a pending entry signal
  3. Score all new entry signals by conviction
  4. 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_50 is 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:

  1. Visual rule builder (recommended) โ€” Strategies โ†’ New / Edit. Click-and-drop, no JSON.
  2. Strategy chat โ€” natural language description that gets converted to DSL.
  3. 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