Documentation

Strategy System

How strategies are defined — the DSL structure, all available functions, operators, and a complete example.

Overview

A strategy is a JSON object with six top-level keys:configuration, inputs,conditions, score, decision, and riskManagement. Each serves a distinct role in the evaluation pipeline.

The strategy is passed to the API as a nested JSON object in thestrategySnapshotJson field. Internally, the engine evaluates it against historical OHLCV data candle by candle.
configurationinputsconditionsscoredecisionriskManagement

#Configuration

Defines the execution timeframe. The engine uses this to determine which candle interval to evaluate the strategy against.

configuration
"configuration": {
  "timeframe": "1H",
  "warmupBars": 21,
  "maxOpenPositions": 1
}

Supported timeframes

ValueDescription
15M15-minute candles
30M30-minute candles
1H1-hour candles (most common)
2H2-hour candles
4H4-hour candles
1DDaily candles

warmupBars

Optional, defaults to 0. Number of leading candles to exclude from trading while indicators stabilize. Set it to the largest lookback period used by any indicator in the strategy — e.g. if you use ema(close, 21)and rsi(close, 14), use warmupBars: 21. This is not derived automatically. Candles inside the warmup window still appear in the results, but entry and score are forced to false / 0 so no trade opens during that range.

maxOpenPositions

Optional, defaults to unlimited. Caps how many trades can be open at the same time — once the cap is reached, new entry signals are skipped until a position closes. Set it to 1 for single-position mode. See Multiple Positionsfor how this interacts with the engine's default (unlimited) behavior.

#Inputs

Inputs are named, reusable computed values. They are evaluated once per candle before conditions are checked. An input can reference market data, built-in functions, or other previously defined inputs.

inputs
"inputs": {
  "emaFast":   "ema(close, 9)",
  "emaSlow":   "ema(close, 21)",
  "rsiValue":  "rsi(close, 14)",
  "atrValue":  "atr(14)",
  "volSpike":  "volumeSpike(1.5)"
}

Available market data

closeopenhighlowvolume

Rules

  • Input names must be unique.
  • Inputs can reference other inputs defined above them (order matters).
  • Circular references are not allowed.
  • Input expressions must resolve to a numeric value.

#Conditions

Conditions are named boolean expressions. They are evaluated per candle using inputs, market data, and comparison operators. A condition must resolve to true orfalse.

conditions
"conditions": {
  "trendUp":    "emaFast > emaSlow",
  "rsiHealthy": "rsiValue > 40 AND rsiValue < 65",
  "breakout":   "crossUp(close, emaSlow)",
  "volConfirm": "volSpike"
}

Supported operators

OperatorTypeExample
>Relational"rsiValue > 40"
<Relational"rsiValue < 70"
>=Relational"score >= 50"
<=Relational"atrValue <= 200"
==Equality"timeframe == 1H"
!=Equality"close != open"
ANDLogical"condA AND condB"
ORLogical"condA OR condB"

Rules

  • All variables referenced in a condition must be defined in inputs.
  • Conditions must resolve to a boolean. A numeric value of 0 is false; non-zero is true.
  • No future data may be referenced. All expressions use only current or past candle data.
  • Randomness is not permitted — strategies must be fully deterministic.

#Score

The score block assigns a numeric weight to each condition. On every candle, the engine sums the scores of all conditions that evaluate to true. This total is the candle's score.

score
"score": {
  "trendUp":    20,
  "rsiHealthy": 30,
  "breakout":   40,
  "volConfirm": 10
}
💡
The score system is what makes EmidLabs strategies compositional. Rather than requiring all conditions to be true simultaneously, you define a threshold the combined score must reach. This mirrors how real conviction is built — multiple aligned signals, not a rigid checklist.

The score values are arbitrary integers. What matters is their relative weight. A condition with score 40 contributes twice as much as one with score 20.

#Decision

The decision block defines the entry rule. The engine evaluates this expression per candle after computing the total score. When it evaluates to true, a new trade is opened at the close of that candle.

decision
"decision": {
  "entry": "score >= 50"
}

The decision expression can reference score (the total for the candle) as well as any defined condition by name.

Combined decision example
"decision": {
  "entry": "score >= 50 AND breakout"
}

#Risk Management

Configures how each open trade's stop-loss and take-profit are calculated. This block is optional — if omitted, the engine defaults to a 1% stop-loss and a 1:3 risk-reward take-profit (see Execution Engine for full details on the default and how R-units are computed).

riskManagement
"riskManagement": {
  "stopLoss":   { "type": "percent", "percent": 1.0 },
  "takeProfit": { "type": "riskReward", "multiple": 3.0 }
}

stopLoss

typeParametersDescription
percentpercent (> 0)Stop distance as a percentage of entry price.
atrperiod (int > 0), multiplier (> 0)Stop distance = ATR(period) at entry × multiplier.

takeProfit

typeParametersDescription
riskRewardmultiple (> 0)Take-profit distance = stop-loss distance × multiple.
percentpercent (> 0)Take-profit distance as a percentage of entry price, independent of the stop-loss.
ATR stop-loss with fixed percent take-profit
"riskManagement": {
  "stopLoss":   { "type": "atr", "period": 14, "multiplier": 1.5 },
  "takeProfit": { "type": "percent", "percent": 2.0 }
}
💡
You can set only one of stopLoss / takeProfit — the other falls back to its own default independently.

#Built-in Functions

Indicators

FunctionDescription
ema(series, period)Exponential moving average of series over period candles.
sma(series, period)Simple moving average of series over period candles.
rsi(series, period)Relative Strength Index. Returns 0–100.
atr(period)Average True Range over period candles.
adx(period)Average Directional Index. Trend strength, 0–100 — does not indicate direction.
adxPlusDi(period)+DI. Directional indicator — compare against adxMinusDi to read trend direction.
adxMinusDi(period)-DI. Directional indicator — compare against adxPlusDi to read trend direction.
💡
adx, adxPlusDi, and adxMinusDi share one underlying calculation — calling any one of them for a given period computes all three at no extra cost. A common pattern is adx(14) > 25 AND adxPlusDi(14) > adxMinusDi(14)for "strong, confirmed uptrend."

Signals

FunctionDescription
crossUp(a, b)Returns true on the candle where a crosses above b.
crossDown(a, b)Returns true on the candle where a crosses below b.

Series operators

FunctionDescription
highest(series, n)Highest value of series over the last n candles.
lowest(series, n)Lowest value of series over the last n candles.
change(series)Difference between the current and previous candle value.

Volume

FunctionDescription
volumeSma(period)Simple moving average of volume over period candles.
volumeSpike(multiplier)Boolean: true when current volume exceeds multiplier × volumeSma(20).

Price action

FunctionDescription
body()Absolute difference between open and close.
range()Absolute difference between high and low.
upperWick()Size of the upper wick: high - max(open, close).
lowerWick()Size of the lower wick: min(open, close) - low.

Math

FunctionDescription
abs(x)Absolute value.
min(a, b)Minimum of two values.
max(a, b)Maximum of two values.

#Full Strategy Example

A trend-following strategy with EMA crossover, RSI confirmation, and volume spike entry.

strategy.json — Trend + RSI + Volume
{
  "configuration": {
    "timeframe": "1H",
    "warmupBars": 21
  },
  "inputs": {
    "emaFast":  "ema(close, 9)",
    "emaSlow":  "ema(close, 21)",
    "rsiValue": "rsi(close, 14)",
    "volRatio": "volumeSpike(1.3)"
  },
  "conditions": {
    "trendUp":    "emaFast > emaSlow",
    "rsiHealthy": "rsiValue > 40 AND rsiValue < 65",
    "volConfirm": "volRatio",
    "crossover":  "crossUp(emaFast, emaSlow)"
  },
  "score": {
    "trendUp":    20,
    "rsiHealthy": 25,
    "volConfirm": 15,
    "crossover":  40
  },
  "decision": {
    "entry": "score >= 60"
  }
}
💡
In this example, entry requires either a crossover (40pts) + any one other signal, or all three non-crossover conditions simultaneously (60pts). The crossover alone is not enough — which prevents false triggers in noisy markets.

Invalid Patterns

What to avoid

  • Referencing undefined inputs in conditions or the decision block.
  • Using conditions that do not resolve to a boolean (e.g., a numeric expression).
  • Referencing future data (lookahead bias).
  • Circular input references.
  • Using randomness or non-deterministic logic of any kind.
Invalid — undefined variable
"conditions": {
  "breakout": "close > resistance"  // ERROR: 'resistance' not defined in inputs
}

#Copy Spec for AI

Building strategies with an LLM? Copy the condensed reference below into your system or user prompt — it strips out the tutorial prose above and keeps only the schema, functions, operators, and rules the model needs to generate valid strategy JSON.

strategy-dsl-spec.md
# EmidLabs Strategy DSL — Reference for AI Strategy Generation

A strategy is a single JSON object with six top-level keys: configuration, inputs, conditions, score, decision, riskManagement.
Output must be valid JSON only — no markdown, no comments, no explanations.

## configuration
{
  "timeframe": "15M" | "30M" | "1H" | "2H" | "4H" | "1D",
  "warmupBars": <int, optional, default 0>,
  "maxOpenPositions": <int > 0, optional, default null = unlimited>
}
warmupBars is NOT derived automatically. Set it to the largest lookback period among the
indicators used in "inputs" (e.g. if the strategy uses ema(close, 21) and rsi(close, 14),
set warmupBars to 21). Candles inside the warmup window are excluded from trading — entry
is forced false and score to 0 — because their indicator values have not fully stabilized yet.
maxOpenPositions caps how many trades can be open at the same time. Omit it (or leave it null)
for unlimited concurrent positions (the default). Set it to 1 for single-position mode — a new
entry is skipped whenever a position is already open.

## inputs
Named, reusable computed values. Evaluated once per candle, before conditions.
Available market data: close, open, high, low, volume
Built-in functions:
  Indicators: ema(series, period), sma(series, period), rsi(series, period) [0-100], atr(period),
    adx(period) [0-100, trend strength], adxPlusDi(period), adxMinusDi(period) [+DI/-DI, trend direction —
    compare adxPlusDi vs adxMinusDi alongside adx]
  Signals: crossUp(a, b), crossDown(a, b)
  Series operators: highest(series, n), lowest(series, n), change(series)
  Volume: volumeSma(period), volumeSpike(multiplier) [boolean: true when volume > multiplier * volumeSma(20)]
  Price action: body(), range(), upperWick(), lowerWick()
  Math: abs(x), min(a, b), max(a, b)
Rules:
  - Names must be unique.
  - An input can only reference inputs defined above it (order matters).
  - Circular references are not allowed.
  - Must resolve to a numeric value.
Example: { "emaFast": "ema(close, 9)", "rsiValue": "rsi(close, 14)" }

## conditions
Named boolean expressions using inputs, market data, and operators.
Operators: >  <  >=  <=  ==  !=  AND  OR
Rules:
  - Every variable referenced must be defined in inputs.
  - Must resolve to a boolean (0 = false, non-zero = true).
  - No future data — only current or past candle data (no lookahead bias).
  - No randomness — strategies must be fully deterministic.
Example: { "trendUp": "emaFast > emaSlow", "rsiHealthy": "rsiValue > 40 AND rsiValue < 65" }

## score
Integer weight per condition. On each candle, the engine sums the scores of all conditions
that evaluate to true. Values are arbitrary — only their relative weight matters.
Example: { "trendUp": 20, "rsiHealthy": 30 }

## decision
{ "entry": "<expression>" }
Evaluated per candle after the total score is computed. May reference "score" (the candle's
total) and any condition by name. When true, a trade opens at that candle's close.
Example: { "entry": "score >= 50 AND breakout" }

## riskManagement
Optional. Configures how each open trade's stop-loss and take-profit are calculated. If omitted
entirely, the engine defaults to { stopLoss: percent 1.0, takeProfit: riskReward 3.0 } — the
same as older strategies that predate this section.

stopLoss (pick one type):
  { "type": "percent", "percent": <number > 0, e.g. 1.0 for 1%> }
  { "type": "atr", "period": <int > 0>, "multiplier": <number > 0> }

takeProfit (pick one type):
  { "type": "riskReward", "multiple": <number > 0> }  // take-profit distance = stopLoss distance * multiple
  { "type": "percent", "percent": <number > 0> }

Example: { "stopLoss": { "type": "atr", "period": 14, "multiplier": 1.5 },
           "takeProfit": { "type": "percent", "percent": 2.0 } }

## Execution model
  - Entry fills at the close of the candle where "decision.entry" turns true — no lookahead.
  - Risk per trade is governed by "riskManagement" (stop-loss and take-profit levels calculated
    at entry). If "riskManagement" is omitted, the default is a fixed 1% stop-loss and a 1:3
    risk-reward take-profit (win = +3.0R, loss = -1.0R). With a custom "riskManagement", win/loss
    R magnitudes can vary per trade (e.g. an ATR-based stop-loss).
  - Long-only. There is no short/sell side.
  - By default, multiple positions can be open at once — every candle where entry triggers opens
    a new, independent trade, regardless of other open trades. Set "configuration.maxOpenPositions"
    to cap concurrent trades (e.g. 1 for single-position mode).

## Invalid patterns — do not generate
  - Referencing an input or condition that isn't defined.
  - A condition that doesn't resolve to a boolean.
  - Referencing future candle data.
  - Circular input references.
  - Randomness or any non-deterministic logic.

## Full example
{
  "configuration": { "timeframe": "1H", "warmupBars": 21 },
  "inputs": {
    "emaFast": "ema(close, 9)",
    "emaSlow": "ema(close, 21)",
    "rsiValue": "rsi(close, 14)",
    "volSpike": "volumeSpike(1.3)"
  },
  "conditions": {
    "trendUp": "emaFast > emaSlow",
    "rsiHealthy": "rsiValue > 40 AND rsiValue < 65",
    "volConfirm": "volSpike",
    "crossover": "crossUp(emaFast, emaSlow)"
  },
  "score": { "trendUp": 20, "rsiHealthy": 25, "volConfirm": 15, "crossover": 40 },
  "decision": { "entry": "score >= 60" },
  "riskManagement": {
    "stopLoss": { "type": "percent", "percent": 1.0 },
    "takeProfit": { "type": "riskReward", "multiple": 3.0 }
  }
}

## Submitting to the API
POST https://backtest.emidlabs.com/api/public/v1/backtest
Nest the strategy object under "strategySnapshotJson", alongside "assetPair", "initialDate",
and "finalDate".