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.
strategySnapshotJson field. Internally, the engine evaluates it against historical OHLCV data candle by candle.#Configuration
Defines the execution timeframe. The engine uses this to determine which candle interval to evaluate the strategy against.
"configuration": {
"timeframe": "1H",
"warmupBars": 21,
"maxOpenPositions": 1
}Supported timeframes
| Value | Description |
|---|---|
| 15M | 15-minute candles |
| 30M | 30-minute candles |
| 1H | 1-hour candles (most common) |
| 2H | 2-hour candles |
| 4H | 4-hour candles |
| 1D | Daily 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": {
"emaFast": "ema(close, 9)",
"emaSlow": "ema(close, 21)",
"rsiValue": "rsi(close, 14)",
"atrValue": "atr(14)",
"volSpike": "volumeSpike(1.5)"
}Available market data
closeopenhighlowvolumeRules
- 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": {
"trendUp": "emaFast > emaSlow",
"rsiHealthy": "rsiValue > 40 AND rsiValue < 65",
"breakout": "crossUp(close, emaSlow)",
"volConfirm": "volSpike"
}Supported operators
| Operator | Type | Example |
|---|---|---|
| > | Relational | "rsiValue > 40" |
| < | Relational | "rsiValue < 70" |
| >= | Relational | "score >= 50" |
| <= | Relational | "atrValue <= 200" |
| == | Equality | "timeframe == 1H" |
| != | Equality | "close != open" |
| AND | Logical | "condA AND condB" |
| OR | Logical | "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": {
"trendUp": 20,
"rsiHealthy": 30,
"breakout": 40,
"volConfirm": 10
}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": {
"entry": "score >= 50"
}The decision expression can reference score (the total for the candle) as well as any defined condition by name.
"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": {
"stopLoss": { "type": "percent", "percent": 1.0 },
"takeProfit": { "type": "riskReward", "multiple": 3.0 }
}stopLoss
| type | Parameters | Description |
|---|---|---|
| percent | percent (> 0) | Stop distance as a percentage of entry price. |
| atr | period (int > 0), multiplier (> 0) | Stop distance = ATR(period) at entry × multiplier. |
takeProfit
| type | Parameters | Description |
|---|---|---|
| riskReward | multiple (> 0) | Take-profit distance = stop-loss distance × multiple. |
| percent | percent (> 0) | Take-profit distance as a percentage of entry price, independent of the stop-loss. |
"riskManagement": {
"stopLoss": { "type": "atr", "period": 14, "multiplier": 1.5 },
"takeProfit": { "type": "percent", "percent": 2.0 }
}stopLoss / takeProfit — the other falls back to its own default independently.#Built-in Functions
Indicators
| Function | Description |
|---|---|
| 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
| Function | Description |
|---|---|
| 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
| Function | Description |
|---|---|
| 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
| Function | Description |
|---|---|
| volumeSma(period) | Simple moving average of volume over period candles. |
| volumeSpike(multiplier) | Boolean: true when current volume exceeds multiplier × volumeSma(20). |
Price action
| Function | Description |
|---|---|
| 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
| Function | Description |
|---|---|
| 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.
{
"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"
}
}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.
"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.
# 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".