Strategy System
Como estratégias são definidas — a estrutura da DSL, todas as funções disponíveis, operadores e um exemplo completo.
#Visão geral
Uma estratégia é um objeto JSON com seis chaves de nível superior: configuration, inputs, conditions, score, decision e riskManagement. Cada uma tem um papel distinto no pipeline de avaliação.
strategySnapshotJson. Internamente, o engine avalia ela contra dados históricos OHLCV, candle por candle.Quer que uma estratégia exista como um ativo próprio, independente de qualquer backtest ou subscription ao vivo — salvar uma vez e reusar a mesma definição nos dois? Veja MCP Server (Strategy).
#Configuration
Define o timeframe de execução. O engine usa isso pra determinar qual intervalo de candle usar pra avaliar a estratégia.
"configuration": {
"timeframe": "1H",
"warmupBars": 21,
"maxOpenPositions": 1,
"direction": "long",
"entryFeePct": 0.1,
"exitFeePct": 0.1,
"timezone": "America/Sao_Paulo"
}Timeframes suportados
| Valor | Descrição |
|---|---|
| 5M | Candles de 5 minutos |
| 15M | Candles de 15 minutos |
| 30M | Candles de 30 minutos |
| 1H | Candles de 1 hora (mais comum) |
| 2H | Candles de 2 horas |
| 4H | Candles de 4 horas |
| 1D | Candles diários |
warmupBars
Opcional, padrão 0. Número de candles iniciais excluídos de trading enquanto os indicadores estabilizam. Ajuste pro maior período de lookback usado por qualquer indicador na estratégia — ex: se você usa ema(close, 21) e rsi(close, 14), use warmupBars: 21. Isso não é derivado automaticamente. Candles dentro da janela de warmup ainda aparecem nos resultados, mas entry e score são forçados pra false / 0 pra nenhum trade abrir nesse intervalo.
maxOpenPositions
Opcional, padrão ilimitado. Limita quantos trades podem estar abertos ao mesmo tempo — uma vez atingido o limite, novos sinais de entrada são ignorados até uma posição fechar. Ajuste pra 1 pro modo de posição única. Veja Multiple Positions pra como isso interage com o comportamento padrão (ilimitado) do engine.
direction
Opcional, padrão "long". Escolhe qual lado o backtest inteiro opera — "long" ou "short". Uma única estratégia opera uma direção, não as duas ao mesmo tempo. decision.entry/exit e riskManagement mantêm exatamente o mesmo significado dos dois jeitos — só o posicionamento de preço de stop-loss/take-profit e o sinal de ganho/perda se invertem pra "short". Veja Direction pra mecânica completa.
entryFeePct / exitFeePct
Opcionais, ambos com padrão 0 (sem fee, retrocompatível). Simulam uma taxa de corretagem por perna como percentual do preço — ex: 0.1 pra 0.1%, uma taxa taker típica na Binance VIP0. Quando definido, o pnlR/pnlPct de cada trade no resultado ficam líquidos dessa taxa, então expectancyR/winRate/profitFactor já refletem isso automaticamente — um trade que parece marginalmente lucrativo antes das taxas pode corretamente virar prejuízo. Corretoras reais podem cobrar taxas maker/taker diferentes por perna (ex: um take-profit passivo vs. um stop-loss acionado), então os dois campos são independentes — não são considerados iguais.
expectancyR só é uma comparação justa dentro da própria convenção de stop de uma estratégia. Prefira comparações baseadas em pnlPct entre estratégias ou timeframes com larguras de stop tipicamente diferentes — veja a Referência de Métricas.timezone
Opcional, padrão "America/Sao_Paulo". Um id de fuso IANA que localiza as funções hour()/minute()/dayOfWeek()/isWeekend() (veja Horário abaixo) — use pra restringir entradas/saídas por horário ou dia da semana, ex: operar só numa sessão líquida ou pular fins de semana.
#Inputs
Inputs são valores computados, nomeados e reutilizáveis. São avaliados uma vez por candle antes das conditions serem checadas. Um input pode referenciar dados de mercado, funções nativas, ou outros inputs definidos antes dele.
"inputs": {
"emaFast": "ema(close, 9)",
"emaSlow": "ema(close, 21)",
"rsiValue": "rsi(close, 14)",
"atrValue": "atr(14)",
"volAvg": "sma(volume, 20)",
"volSpike": "volume > volAvg * 1.5"
}Dados de mercado disponíveis
closeopenhighlowvolumeRegras
- Nomes de input devem ser únicos.
- Inputs podem referenciar outros inputs definidos acima deles (a ordem importa).
- Referências circulares não são permitidas.
- Expressões de input devem resolver pra um valor numérico.
#Conditions
Conditions são expressões booleanas nomeadas. São avaliadas por candle usando inputs, dados de mercado e operadores de comparação. Uma condition deve resolver pra true ou false.
"conditions": {
"trendUp": "emaFast > emaSlow",
"rsiHealthy": "rsiValue > 40 AND rsiValue < 65",
"breakout": "crossUp(close, emaSlow)",
"volConfirm": "volSpike"
}Operadores suportados
| Operador | Tipo | Exemplo |
|---|---|---|
| > | Relacional | "rsiValue > 40" |
| < | Relacional | "rsiValue < 70" |
| >= | Relacional | "score >= 50" |
| <= | Relacional | "atrValue <= 200" |
| == | Igualdade | "timeframe == 1H" |
| != | Igualdade | "close != open" |
| AND | Lógico | "condA AND condB" |
| OR | Lógico | "condA OR condB" |
Regras
- Toda variável referenciada numa condition deve estar definida em
inputs. - Conditions devem resolver pra um booleano. Um valor numérico 0 é false; não-zero é true.
- Nenhum dado futuro pode ser referenciado. Toda expressão usa só dados do candle atual ou passado.
- Aleatoriedade não é permitida — estratégias devem ser totalmente determinísticas.
#Score
O bloco score atribui um peso numérico a cada condition. A cada candle, o engine soma os scores de todas as conditions que avaliam pra true. Esse total é o score do candle.
"score": {
"trendUp": 20,
"rsiHealthy": 30,
"breakout": 40,
"volConfirm": 10
}O sistema de score é o que torna as estratégias EmidLabs composicionais. Em vez de exigir que todas as conditions sejam true simultaneamente, você define um limiar que o score combinado precisa atingir. Isso espelha como convicção real se constrói — vários sinais alinhados, não uma checklist rígida.
Os valores de score são inteiros arbitrários. O que importa é o peso relativo entre eles. Uma condition com score 40 contribui o dobro de uma com score 20.
#Decision
O bloco decision define a regra de entrada. O engine avalia essa expressão por candle depois de computar o score total. Quando ela avalia pra true, um novo trade abre no fechamento daquele candle.
"decision": {
"entry": "score >= 50"
}A expressão de decision pode referenciar score (o total do candle) assim como qualquer condition definida, pelo nome.
"decision": {
"entry": "score >= 50 AND breakout"
}exit (opcional)
Uma regra de saída opcional baseada em sinal, avaliada com as mesmas regras de entry. Quando avalia pra true num candle onde uma posição está aberta, essa posição fecha no fechamento do candle — independente de riskManagement. Se omitida, posições só fecham via stop-loss ou take-profit. Veja Execution Engine pra como exit interage com stop-loss/take-profit no mesmo candle.
"decision": {
"entry": "score >= 50 AND breakout",
"exit": "crossDown(emaFast, emaSlow)"
}#Risk Management
Configura como o stop-loss e take-profit de cada trade aberto são calculados. Esse bloco é opcional — se omitido, o engine usa o padrão de 1% de stop-loss e take-profit de 1:3 risco-retorno (veja Execution Engine pros detalhes completos do padrão e de como unidades de R são calculadas).
"riskManagement": {
"stopLoss": { "type": "percent", "percent": 1.0 },
"takeProfit": { "type": "riskReward", "multiple": 3.0 }
}stopLoss
| type | Parâmetros | Descrição |
|---|---|---|
| percent | percent (> 0) | Distância do stop como percentual do preço de entrada. |
| atr | period (int > 0), multiplier (> 0) | Distância do stop = ATR(period) na entrada × multiplier. |
takeProfit
| type | Parâmetros | Descrição |
|---|---|---|
| riskReward | multiple (> 0) | Distância do take-profit = distância do stop-loss × multiple. |
| percent | percent (> 0) | Distância do take-profit como percentual do preço de entrada, independente do stop-loss. |
"riskManagement": {
"stopLoss": { "type": "atr", "period": 14, "multiplier": 1.5 },
"takeProfit": { "type": "percent", "percent": 2.0 }
}Você pode definir só um dos dois, stopLoss ou takeProfit — o outro cai pro próprio padrão de forma independente.
#Funções Nativas
Indicadores
| Função | Descrição |
|---|---|
| ema(series, period) | Média móvel exponencial de series ao longo de period candles. |
| sma(series, period) | Média móvel simples de series ao longo de period candles. |
| rsi(series, period) | Índice de Força Relativa. Retorna 0–100. |
| atr(period) | Average True Range ao longo de period candles. |
| adx(period) | Average Directional Index. Força da tendência, 0–100 — não indica direção. |
| adxPlusDi(period) | +DI. Indicador direcional — compare com adxMinusDi pra ler a direção da tendência. |
| adxMinusDi(period) | -DI. Indicador direcional — compare com adxPlusDi pra ler a direção da tendência. |
adx, adxPlusDi e adxMinusDi compartilham um único cálculo subjacente — chamar qualquer um deles pra um dado period calcula os três sem custo extra. Um padrão comum é adx(14) > 25 AND adxPlusDi(14) > adxMinusDi(14)pra "tendência de alta forte e confirmada."
Sinais
| Função | Descrição |
|---|---|
| crossUp(a, b) | Retorna true no candle onde a cruza acima de b. |
| crossDown(a, b) | Retorna true no candle onde a cruza abaixo de b. |
Operadores de série
| Função | Descrição |
|---|---|
| highest(series, n) | Maior valor de series nos últimos n candles. |
| lowest(series, n) | Menor valor de series nos últimos n candles. |
| change(series) | Diferença entre o valor do candle atual e do anterior. |
| shift(series, n) | Valor de series n candles atrás. Só olha pro passado — retorna NaN até existir histórico suficiente. |
| any(boolSeries, n) | True se boolSeries foi true em qualquer um dos n candles antes do atual. |
| all(boolSeries, n) | True se boolSeries foi true em todos os n candles antes do atual. |
| count(boolSeries, n) | Quantos dos n candles antes do atual tiveram boolSeries true. |
Não existe volumeSma()/volumeSpike() — volume é uma série de dados de mercado igual a close/open/high/low, então use sma(volume, period) pra uma média móvel de volume, e volume > sma(volume, period) * multiplier pra uma condition de pico de volume.
Estrutura
| Função | Descrição |
|---|---|
| swingHigh(series, confirmBars) | True quando um topo de swing confirmado é reconhecido — confirmBars candles DEPOIS do pico real, nunca no próprio pico. |
| swingLow(series, confirmBars) | True quando um fundo de swing confirmado é reconhecido — confirmBars candles DEPOIS do vale real, nunca no próprio vale. |
O atraso de confirmação é intencional, não uma limitação: uma avaliação ao vivo genuinamente não consegue saber que um candle foi um pico/vale até confirmBars candles depois, então o sinal dispara nesse mesmo candle posterior no backtest também — é isso que mantém backtest e live consistentes, em vez do backtest enxergar estrutura silenciosamente antes do que o live jamais conseguiria.
Anatomia do candle
| Função | Descrição |
|---|---|
| body() | Diferença absoluta entre open e close. |
| range() | Diferença absoluta entre high e low. |
| upperWick() | Tamanho do pavio superior: high - max(open, close). |
| lowerWick() | Tamanho do pavio inferior: min(open, close) - low. |
| isBullish() | True quando close > open. Não é o oposto exato de isBearish() — ambos são false quando close é igual a open. |
| isBearish() | True quando close < open. Não é o oposto exato de isBullish() — ambos são false quando close é igual a open. |
Matemática
| Função | Descrição |
|---|---|
| abs(x) | Valor absoluto. |
| min(a, b) | Mínimo entre dois valores. |
| max(a, b) | Máximo entre dois valores. |
Horário
Localizado por configuration.timezone (um id IANA, ex. "America/Sao_Paulo", padrão quando omitido) — use pra restringir entradas/saídas a janelas de horário ou dia da semana, ex. "hour() >= 9 AND hour() < 13" ou "NOT isWeekend()".
| Função | Descrição |
|---|---|
| hour() | Hora local, 0-23. |
| minute() | Minuto local, 0-59. |
| dayOfWeek() | Dia da semana local, 0 (domingo) até 6 (sábado). |
| isWeekend() | True aos sábados ou domingos, horário local. |
Padrões de candlestick
As 28 funções abaixo não recebem argumentos, retornam true/false por candle, e leem direto do OHLC do candle fechado (open/high/low/close) — os mesmos dados de candle fechado que toda outra função lê, então se comportam de forma idêntica em backtest e live.
| Função | Descrição |
|---|---|
| hammer() | Corpo pequeno, pavio inferior longo (≥2× o corpo), pouco/nenhum pavio superior — formato de reversão de alta. |
| shootingStar() | Corpo pequeno, pavio superior longo (≥2× o corpo), pouco/nenhum pavio inferior — formato de reversão de baixa. |
| doji() | Corpo é ≤10% do range do candle — indecisão. |
| bullishEngulfing() | Candle de alta cujo corpo engolfa completamente o corpo do candle de baixa anterior. |
| bearishEngulfing() | Candle de baixa cujo corpo engolfa completamente o corpo do candle de alta anterior. |
| morningStar() | Candle de baixa longo, uma estrela de corpo pequeno, depois um candle de alta fechando de volta acima do ponto médio do primeiro candle. |
| eveningStar() | Candle de alta longo, uma estrela de corpo pequeno, depois um candle de baixa fechando de volta abaixo do ponto médio do primeiro candle. |
| bullishMarubozu() | Candle de alta com quase nenhum pavio — corpo é ≥95% do range. |
| bearishMarubozu() | Candle de baixa com quase nenhum pavio — corpo é ≥95% do range. |
| spinningTop() | Corpo pequeno com pavios dos dois lados pelo menos do tamanho do corpo — indecisão. |
| dragonflyDoji() | Doji com pavio inferior longo e praticamente nenhum pavio superior. |
| gravestoneDoji() | Doji com pavio superior longo e praticamente nenhum pavio inferior. |
| longLeggedDoji() | Doji com pavios longos dos dois lados. |
| piercingLine() | Candle de baixa seguido de um candle de alta abrindo abaixo do fechamento dele e fechando de volta acima do ponto médio do corpo dele (sem engolfar completamente). |
| darkCloudCover() | Candle de alta seguido de um candle de baixa abrindo acima do fechamento dele e fechando de volta abaixo do ponto médio do corpo dele (sem engolfar completamente). |
| bullishHarami() | Corpo de alta pequeno totalmente contido dentro do corpo de baixa anterior, maior. |
| bearishHarami() | Corpo de baixa pequeno totalmente contido dentro do corpo de alta anterior, maior. |
| haramiCross() | Formato de harami (qualquer direção) onde o candle contido é ele mesmo um doji. |
| tweezerTop() | Dois candles com máximas coincidentes, alta depois baixa — reversão de baixa. |
| tweezerBottom() | Dois candles com mínimas coincidentes, baixa depois alta — reversão de alta. |
| threeWhiteSoldiers() | Três candles de alta consecutivos, cada um abrindo dentro e fechando acima do anterior, pavios superiores pequenos. |
| threeBlackCrows() | Três candles de baixa consecutivos, cada um abrindo dentro e fechando abaixo do anterior, pavios inferiores pequenos. |
| threeInsideUp() | Harami de alta seguido de um terceiro candle fechando acima da abertura do primeiro candle. |
| threeInsideDown() | Harami de baixa seguido de um terceiro candle fechando abaixo da abertura do primeiro candle. |
| threeOutsideUp() | Engolfo de alta seguido de um terceiro candle fechando ainda mais alto. |
| threeOutsideDown() | Engolfo de baixa seguido de um terceiro candle fechando ainda mais baixo. |
| risingThreeMethods() | Candle de alta longo, três candles pequenos contidos dentro do seu range, depois um candle de alta fechando numa nova máxima — continuação de 5 candles. |
| fallingThreeMethods() | Candle de baixa longo, três candles pequenos contidos dentro do seu range, depois um candle de baixa fechando numa nova mínima — continuação de 5 candles. |
hammer() e shootingStar() só checam o formato do candle — eles não sabem se a tendência anterior era de alta ou baixa. Classicamente o mesmo formato é um Hammer depois de uma tendência de baixa mas um Hanging Man depois de uma tendência de alta (e Shooting Star vs. Inverted Hammer, respectivamente). Combine com um filtro de tendência/momentum (ex: rsi ou uma condition de média móvel) em vez de usar o formato sozinho como sinal de entrada.#Exemplo Completo de Estratégia
Uma estratégia trend-following com cruzamento de EMA, confirmação por RSI e entrada por pico de volume.
{
"configuration": {
"timeframe": "1H",
"warmupBars": 21
},
"inputs": {
"emaFast": "ema(close, 9)",
"emaSlow": "ema(close, 21)",
"rsiValue": "rsi(close, 14)",
"volAvg": "sma(volume, 20)",
"volRatio": "volume > volAvg * 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"
}
}Nesse exemplo, a entrada exige ou um crossover (40pts) + mais qualquer outro sinal, ou as três conditions que não são o crossover simultaneamente (60pts). O crossover sozinho não basta — o que evita disparos falsos em mercados ruidosos.
#Padrões Inválidos
O que evitar
- Referenciar inputs indefinidos em conditions ou no bloco decision.
- Usar conditions que não resolvem pra um booleano (ex: uma expressão numérica).
- Referenciar dados futuros (viés de lookahead).
- Referências circulares de input.
- Usar aleatoriedade ou lógica não-determinística de qualquer tipo.
"conditions": {
"breakout": "close > resistance" // ERRO: 'resistance' não está definida em inputs
}#Copiar Spec para IA
Construindo estratégias com um LLM? Copie a referência condensada abaixo pro seu prompt de sistema ou de usuário — ela remove a prosa tutorial acima e mantém só o schema, funções, operadores e regras que o modelo precisa pra gerar um JSON de estratégia válido.
# 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": "5M" | "15M" | "30M" | "1H" | "2H" | "4H" | "1D",
"warmupBars": <int, optional, default 0>,
"maxOpenPositions": <int > 0, optional, default null = unlimited>,
"direction": "long" | "short", optional, default "long",
"entryFeePct": <number 0-5, optional, default 0>,
"exitFeePct": <number 0-5, optional, default 0>,
"timezone": <IANA id string, optional, default "America/Sao_Paulo">
}
warmupBars should be set 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) — the backend
now raises an unset/too-low value up to that same floor automatically, but still set it explicitly
for best results, since the auto-floor is a conservative minimum, not a guarantee of a fully
converged indicator value. 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.
direction picks which side the whole backtest trades — every position opened by "decision.entry"
is a long (buy) or a short (sell) accordingly. A single strategy is one direction, not both at
once. "entry"/"exit"/"riskManagement" keep the exact same meaning either way — only the
stop-loss/take-profit price placement and win/loss sign are mirrored for "short" (stop above
entry, take below entry, profit when price falls).
entryFeePct/exitFeePct simulate a per-leg exchange fee as a percent of price (e.g. 0.1 = 0.1%,
a typical taker rate at Binance VIP0). Both default to 0 (no fee). When set, every trade's
pnlR/pnlPct in the result are net of this fee, so expectancyR/winRate/profitFactor account for it
automatically — a trade that looks marginally profitable before fees can correctly flip to a loss.
Real exchanges can charge different maker/taker rates per leg, so the two fields are independent,
not assumed equal. Caveat: a trade's fee cost in R-units scales inversely with that trade's own
stop distance, so once a fee is set, expectancyR is only a fair comparison within one strategy's
own stop convention — prefer pnlPct-based comparisons across strategies/timeframes with different
typical stop widths.
timezone is an IANA id (e.g. "America/Sao_Paulo") that localizes the hour()/minute()/dayOfWeek()/
isWeekend() functions above — use it for time-of-day or weekday gating conditions.
## 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)
Lag / window aggregation: shift(series, n) [look-back only, NaN before enough history],
any(boolSeries, n), all(boolSeries, n), count(boolSeries, n) [over the n candles before the current one]
Structure: swingHigh(series, confirmBars), swingLow(series, confirmBars) [confirmed N-bar swing point —
true confirmBars candles AFTER the actual peak/trough, never at the peak itself, so it can't repaint
between backtest and live]
Candle anatomy: body(), range(), upperWick(), lowerWick(), isBullish(), isBearish()
Math: abs(x), min(a, b), max(a, b)
Time: hour() [0-23], minute() [0-59], dayOfWeek() [0=Sunday..6=Saturday], isWeekend() [boolean] —
all localized to "configuration.timezone" (default "America/Sao_Paulo"), for time-of-day or
weekday gating, e.g. "hour() >= 9 and hour() < 13" or "not isWeekend()".
Volume: there is no volumeSma()/volumeSpike() — volume is a market-data series like close/open/high/low,
so use sma(volume, period) for a volume moving average, and volume > sma(volume, period) * multiplier
for a volume-spike condition.
Candlestick patterns (all no-arg, boolean, read only closed-candle OHLC — same in backtest and live):
Single-candle: hammer(), shootingStar(), doji(), bullishMarubozu(), bearishMarubozu(), spinningTop(),
dragonflyDoji(), gravestoneDoji(), longLeggedDoji()
Two-candle: bullishEngulfing(), bearishEngulfing(), piercingLine(), darkCloudCover(), bullishHarami(),
bearishHarami(), haramiCross(), tweezerTop(), tweezerBottom()
Three-plus-candle: morningStar(), eveningStar(), threeWhiteSoldiers(), threeBlackCrows(),
threeInsideUp(), threeInsideDown(), threeOutsideUp(), threeOutsideDown(), risingThreeMethods(),
fallingThreeMethods()
Caveat: hammer()/shootingStar() are shape-only and don't know the prior trend (the same shape is a
Hammer after a downtrend but a Hanging Man after an uptrend, and vice versa for shootingStar/Inverted
Hammer) — pair with a trend/momentum condition (e.g. rsi, ema) rather than using the shape alone.
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>", "exit": "<expression, optional>" }
"entry" is 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.
"exit" is optional and uses the same expression rules as "entry". When true on a candle where a
position is open, that position closes at that candle's close — independent of "riskManagement".
If omitted, positions only close via stop-loss/take-profit.
Example: { "entry": "score >= 50 AND breakout", "exit": "crossDown(emaFast, emaSlow)" }
## 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).
- A position closes on the first of three events: stop-loss hit, take-profit hit, or
"decision.exit" turning true, checked in that order each candle. If a candle's stop-loss (or
take-profit) and "decision.exit" would both trigger on the same candle, the stop-loss/
take-profit wins and "decision.exit" is not evaluated for that position that candle.
- "configuration.direction" picks long or short for the whole backtest (default long) — see
"configuration" above. There is no per-trade direction; one strategy trades one 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)",
"volAvg": "sma(volume, 20)",
"volSpike": "volume > volAvg * 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".