Documentation

Backtesting API

Referência completa da Backtesting API — endpoints, schema de request, schema de response, métricas e códigos de erro.

#Visão geral

A Backtesting API expõe cinco endpoints. Submeta uma requisição de execução de estratégia — contra um único par de ativo, ou um batch inteiro de uma vez — faça polling do resultado usando o ID retornado, e — separadamente — pagine pela lista completa de trades.

URL base: https://backtest.emidlabs.com/api/public/v1

Toda requisição exige o header x-api-key com uma API key válida. Keys são geradas no Console.

#POST /backtest — Submeter

POST/backtest

Submete um novo backtest pra execução.

O corpo deve ser application/json. A estratégia é embutida como um objeto JSON aninhado dentro de strategySnapshotJson.

Corpo da requisição

CampoTipoObrigatórioDescrição
strategySnapshotJsonobjectSimObjeto de estratégia com configuration, inputs, conditions, score, decision, e (opcional) riskManagement.
assetPairstringSimPar de negociação. Ex: "BTC-USDC", "ETH-USDC".
initialDatestringSimData inicial em formato ISO: YYYY-MM-DD.
finalDatestringSimData final em formato ISO: YYYY-MM-DD.

Pares de ativo suportados

OrigemCoinbase

Por enquanto, só mercados cripto são suportados, com origem na Coinbase e cotados em USDC. Exemplos:

BTC-USDCETH-USDCSOL-USDCXRP-USDCADA-USDC

Veja a página de Referência de Dados pra lista completa de pares suportados e os ativos disponíveis no Console. Intervalos de data devem cair dentro dos dados históricos disponíveis pro ativo selecionado.

Essa é uma lista fixa e curada — só ativos que a EmidLabs já extraiu e armazenou candles históricos podem ser backtestados. O Live Execution não tem essa lista: ele se inscreve diretamente no feed ao vivo da Coinbase ou da Binance, então cobre tudo que qualquer uma das duas corretoras lista, não só o que está nessa lista.

Resposta do submit

CampoTipoDescrição
idstring (UUID)Identificador único do backtest.
statusstring"Running" imediatamente após a submissão.
assetPairstringO par de ativo usado.
initialDatestringData inicial do período do backtest.
finalDatestringData final do período do backtest.
createdAtUtcstringTimestamp de criação em UTC.
canViewResultbooleanSe o resultado está acessível (depende de créditos).

#POST /backtest/batch — Submeter Batch

POST/backtest/batch

Submete UMA estratégia contra VÁRIOS pares de ativo de uma vez.

Mesma ideia de POST /backtest, mas em vez de uma string assetPair, envie um array assetPairs — todo ativo recebe exatamente o mesmo strategySnapshotJson e intervalo de datas. Útil pra triar uma estratégia em vários mercados sem uma requisição por ativo.

Best-effort por item: um par de ativo desconhecido ou um intervalo de datas fora da cobertura desse ativo aparece como um error só naquele item — nunca falha o resto do batch.

Corpo da requisição

CampoTipoObrigatórioDescrição
strategySnapshotJsonobjectSimMesmo formato do endpoint de submissão única — aplicado a todo ativo no batch.
assetPairsarray de stringSimEx: ["BTC-USDC", "ETH-USDC", "SOL-USDC"]. Limitado a 200 por batch.
initialDatestringSimData inicial em formato ISO: YYYY-MM-DD.
finalDatestringSimData final em formato ISO: YYYY-MM-DD.

Resposta do submit batch

CampoTipoDescrição
batchIdstring (UUID)Passe isso pra GET /backtest/batch/:batchId/results pra buscar o resultado de todo ativo, paginado.
itemsarrayUma entrada por par de ativo requisitado — veja abaixo.
items[].assetPairstringO par de ativo desse item.
items[].idstring | nullO próprio id do backtest — null se esse par de ativo falhou na submissão.
items[].statusstring | null"Queued" em caso de sucesso, null em caso de falha.
items[].errorstring | nullDefinido só se esse item falhou na submissão. Os outros itens não são afetados.
Um item que falha na submissão nunca vira um registro de backtest de verdade — ele nunca aparece em GET /backtest/batch/:batchId/results também. Guarde essa resposta se você precisar do quadro completo de sucesso+falha depois.

#GET /backtest/:id — Buscar Resultados

GET/backtest/{id}

Busca o status e resultados de um backtest submetido.

result aqui é só métricas agregadas — nenhum detalhe trade-a-trade. Use GET /backtest/:id/trades abaixo pra isso.

Campos da resposta

CampoTipoDescrição
idstringUUID do backtest.
statusstring"Running", "Completed", ou "Failed".
strategySnapshotJsonstringA estratégia usada, serializada.
assetPairstringPar de ativo.
initialDate / finalDatestringIntervalo de datas.
createdAtUtcstringTimestamp de criação (UTC).
errorMessagestring | nullDetalhes do erro se status for "Failed".
canViewResultbooleanSe o resultado está acessível.
logsJsonstring | nullLogs de execução serializados como string JSON.
resultobject | nullMétricas agregadas de performance quando Completed. Sem detalhe trade-a-trade — veja GET /backtest/:id/trades.
recentTradeCountnumber | nullQuantos dos trades fechados mais recentes recentAvgPnlR/recentOutcomes se baseiam (até 5). Só definido quando Completed.
recentAvgPnlRnumber | nullpnlR médio dos recentTradeCount trades mais recentes — um sinal de recência, distinto do expectancyR de janela completa em result.
recentOutcomesarray de string | null"Win"/"Loss" por trade recente, cronológico (mais antigo primeiro — o último elemento é o trade mais recente). Deixa você distinguir uma sequência de perdas real de uma alternância com a mesma média.

#GET /backtest/batch/:batchId/results — Buscar Resultados do Batch

GET/backtest/batch/{batchId}/results

Busca o resultado de todo item de uma submissão em batch, paginado.

Todo backtest criado por POST /backtest/batch é marcado com o mesmo batchId — esse endpoint consulta por esse campo diretamente, então não tem nada pra lembrar além do id que a chamada de submit deu pra você.

Parâmetros de query

ParâmetroTipoPadrãoDescrição
pagenumber1Número da página, base 1.
pageSizenumber20Itens por página. Limitado ao intervalo 1–100.

Campos da resposta

CampoTipoDescrição
batchIdstringEcoa o batch requisitado.
totalCountnumberTotal de itens no batch, em todas as páginas.
completedCountnumberQuantos itens terminaram com sucesso.
failedCountnumberQuantos itens terminaram sem sucesso (Failed, Cancelled, ou Expired).
pendingCountnumberQuantos itens ainda estão Queued ou Running.
page / pageSize / totalPagesnumberCampos padrão de paginação.
itemsarrayUma entrada por backtest nessa página — mesmo formato de GET /backtest/:id (assetPair, id, status, result, recentTradeCount, recentAvgPnlR, recentOutcomes), então quem chama vê campos idênticos independente de ter buscado um ativo individualmente ou como parte de um batch.

totalCount/completedCount/failedCount/pendingCount voltam em toda página, não só na última — confira completedCount + failedCount === totalCount com uma chamada barata de pageSize=1 pra saber que o batch inteiro terminou, sem paginar por tudo.

#GET /backtest/:id/trades — Listar Trades

GET/backtest/{id}/trades

Pagina por todo trade de um backtest completo, independente do resultado principal.

Trades são armazenados um-por-documento no servidor, então esse endpoint continua rápido independente de quantos trades um backtest produziu — não precisa buscar o resultado completo antes.

Parâmetros de query

ParâmetroTipoPadrãoDescrição
pagenumber1Número da página, base 1.
pageSizenumber20Itens por página. Limitado ao intervalo 1–100.
sortBystring"number"Um de "number", "pnlR", "pnlPct", "entryTime", "exitTime".
sortDirectionstring"asc""asc" ou "desc". Valores não reconhecidos caem pro padrão em vez de dar erro.

Campos da resposta

CampoTipoDescrição
itemsarrayObjetos de trade pra página requisitada — veja "Campos de detalhe do trade" abaixo.
totalCountnumberNúmero total de trades em todas as páginas.
pagenumberO número da página retornado (depois de limitado).
pageSizenumberO tamanho de página retornado (depois de limitado).
totalPagesnumberceil(totalCount / pageSize).

#Confirmation Sources

Só conta um candidato de Entry/Exit como trade de verdade quando corroborado pelo próprio histórico de sinais de outra estratégia — ex: uma entrada de XRP só simulada como trade quando uma fonte de confirmação de BTC neutro concorda, ou quando a própria estratégia de tendência em timeframe superior do mesmo ativo concorda. O análogo ao vivo disso é o Confirmation Sources da Live Execution — mesma ideia, mesmo formato de requisição, resolvido aqui contra uma série histórica já totalmente conhecida em vez de um cache ao vivo.

Uma estratégia de confirmação (B) nunca vira um backtest de verdade — ela nunca abre posição, então nada da Referência de Métricas abaixo (pnlR, winRate, drawdown...) se aplica a ela. Ela ganha o próprio conceito, ConfirmationSource, com o próprio fluxo de dois passos abaixo.

Passo 1 — POST /confirmation-sources — Submeter

POST/confirmation-sources

Roda uma estratégia de corroboração contra um intervalo histórico, sem simular nenhum trade.

Mesmo custo de execução e mesmo formato de requisição que POST /backtest — caminha os candles e avalia a DSL exatamente do mesmo jeito — mas o resultado é um histórico bruto de sinais, não um desfecho de trade.

CampoTipoObrigatórioDescrição
strategySnapshotJsonobjectSimA estratégia de corroboração — mesmo formato do POST /backtest. Essa estratégia nunca é feita pra ser operada sozinha.
assetPairstringSimPar de negociação. Ex: "BTC-USDC".
initialDatestringSimData inicial em formato ISO: YYYY-MM-DD.
finalDatestringSimData final em formato ISO: YYYY-MM-DD.

Resposta do submit

CampoTipoDescrição
idstring (UUID)Identificador único da confirmation source — esse é o sourceId que você referencia no submit_backtest assim que estiver Completed.
statusstring"Queued" imediatamente após a submissão.

Passo 2 — GET /confirmation-sources/:id — Status

GET/confirmation-sources/{id}

Consulta o status — mirror de GET /backtest/:id, sem objeto de resultado.

CampoTipoDescrição
idstringUUID da confirmation source.
assetPairstringPar de ativo.
timeframestringResolvido a partir do próprio configuration.timeframe da estratégia.
initialDate / finalDatestringIntervalo de datas.
statusstring"Queued", "Running", "Completed", ou "Failed".
errorMessagestring | nullDetalhes do erro se status for "Failed".
createdAtUtcstringTimestamp de criação (UTC).
runtimeMsnumber | nullQuanto tempo o analyser levou pra rodar isso, em milissegundos. Null até Completed.
candlesProcessednumber | nullNúmero de candles processados. Null até Completed.
unitsConsumednumber | nullCusto em unidades de execução — mesma pool de cobrança do POST /backtest. Null até Completed.

Nunca tem campo result — uma confirmation source não tem desfecho de trade pra reportar. O payload de verdade dela é o histórico de sinais, buscado separadamente abaixo.

GET /confirmation-sources/:id/signals — Listar Sinais

GET/confirmation-sources/{id}/signals

Pagina pelo histórico bruto de sinais que uma confirmation source produziu, independente do status.

Mesmo raciocínio de GET /backtest/:id/trades — sinais são armazenados um-por-documento no servidor, então isso continua rápido independente de quantos candles bateram com as conditions da estratégia.

Parâmetros de query

ParâmetroTipoPadrãoDescrição
pagenumber1Número da página, base 1.
pageSizenumber20Itens por página. Limitado ao intervalo 1–100.

Campos da resposta

CampoTipoDescrição
itemsarrayObjetos de sinal pra página requisitada — veja abaixo.
items[].candleOpenTimenumberSegundos unix — OpenTime do candle que produziu esse sinal.
items[].typestring"Entry" ou "Exit".
items[].scorenumberSoma dos pesos de toda condition que avaliou true nesse candle.
items[].conditionsobjectToda condition nomeada da estratégia, mapeada pra se era true nesse candle.
items[].scoreBreakdownobjectPeso por condition que contribuiu pro score.
totalCountnumberNúmero total de sinais em todas as páginas.
page / pageSize / totalPagesnumberCampos padrão de paginação.

confirmationSources no POST /backtest

Assim que uma confirmation source reporta Completed, referencie-a numa submissão normal de POST /backtest como campo irmão de strategySnapshotJson/assetPair/initialDate/finalDate — não faz parte do próprio objeto de estratégia.

CampoTipoObrigatórioDescrição
confirmationSourcesarrayNãoOmitido/vazio = comportamento atual, inalterado. Quando definido, todo candidato de Entry/Exit precisa ser corroborado por todas as fontes listadas (E lógico) antes de ser simulado como trade.
confirmationSources[].sourceIdstring (UUID)Sim*O id de um submit_confirmation_source (Passo 1 acima), só da mesma conta. Diferente da versão desse mesmo mecanismo na Live Execution, um sourceId inválido (inexistente, ainda não Completed, ou de outra conta) falha a submissão imediatamente — isso é síncrono/batch, deixar passar geraria um resultado confuso de zero trades sem explicação.
confirmationSources[].signalTypestringNão"entry" (padrão) ou "exit" — qual tipo de sinal da fonte conta como confirmação.
confirmationSources[].validityWindow.countintegerNãoPadrão 1. Sempre em candles da própria timeframe da FONTE, não uma duração fixa — escala automaticamente com a estratégia que está confirmando.
Um candidato confirmado é simulado como trade exatamente como hoje — aparece em GET /backtest/:id/trades e afeta pnlR/winRate/drawdown normalmente. Um não confirmado é descartado antes da simulação de trade — nunca aparece em trades, nunca afeta nenhuma métrica. Veja confirmedSignalsCount/unconfirmedSignalsCount na Referência de Métricas abaixo pra medir o efeito diretamente — rode o mesmo backtest com e sem confirmationSources no mesmo intervalo e compare.

#Referência de Métricas

Quando status é Completed, o objeto result contém:

Métricas de lucratividade

CampoTipoDescrição
pnlRnumberLucro/prejuízo líquido em unidades de R. Soma de todos os PnLs dos trades.
grossProfitRnumberSoma de todos os trades vencedores em unidades de R.
grossLossRnumberSoma de todos os trades perdedores em unidades de R (negativo).
profitFactornumber | nullgrossProfitR / abs(grossLossR). >1 é lucrativo. null quando não há trades perdedores — a razão fica indefinida, não infinita.
expectancyRnumberR esperado médio por trade. (winRate × avgWinR) + (lossRate × avgLossR).
avgWinRnumberR médio nos trades vencedores.
avgLossRnumberR médio nos trades perdedores (-1.0 por padrão; varia se a estratégia sobrescreve riskManagement.stopLoss).
totalFeeRnumberR total subtraído em todos os trades por configuration.entryFeePct/exitFeePct (0 se nenhum foi definido). pnlR/expectancyR acima já são líquidos disso — totalFeeR é só quanto as taxas custaram, pra diagnóstico.

Estatísticas de trade

CampoTipoDescrição
tradesnumberNúmero total de trades executados.
winsnumberNúmero de trades vencedores.
lossesnumberNúmero de trades perdedores.
winRatenumberwins / trades. Intervalo: 0–1.
bothHitnumberTrades onde SL e TP foram atingidos no mesmo candle (resolvido como SL). Um bothHit alto em relação a trades significa que muitos desfechos foram decididos pela regra de precedência stop-vence-empate do engine em vez de dados reais de trajetória de preço intracandle — trate resultados com mais ceticismo quanto maior essa razão.

Métricas de drawdown

CampoTipoDescrição
maxDrawdownRnumberPior queda de pico-a-vale entre os trades fechados, em unidades de R. 0 se o equity nunca caiu abaixo da própria máxima histórica.
currentDrawdownRnumberQuão abaixo do próprio pico a curva de equity está no final da janela do backtest, em unidades de R. 0 se a janela termina numa nova máxima. Inclui o PnL não realizado de qualquer posição ainda aberta — veja unrealizedPnlRAtEnd.
openPositionsAtEndnumberNúmero de posições ainda abertas (nunca atingiram stop/take/sinal de saída) quando o intervalo de datas do backtest terminou. 0 no caso comum.
unrealizedPnlRAtEndnumberSoma do PnL não realizado, em unidades de R, de todas as posições ainda abertas no fim da janela — marcado a mercado contra o fechamento do último candle disponível. 0 quando openPositionsAtEnd é 0.

unrealizedPnlRAtEnd não inclui taxa de saída (nenhuma saída realmente aconteceu) e alimenta só currentDrawdownR/maxDrawdownR — nunca vaza pra pnlR, expectancyR, trades, ou qualquer outra métrica que descreve trades fechados e realizados.

Diagnósticos

CampoTipoDescrição
conditionsDistributionPctobjectIndexado por quantas conditions eram simultaneamente true (0, 1, 2...N), não pelo nome da condition — pra cada contagem, a fração de todos os candles onde exatamente esse número de conditions eram true ao mesmo tempo.
scoreDistributionPctobjectPra cada valor de score possível: fração de candles com esse score.
confirmedSignalsCountnumberSó tem significado quando a requisição declarou confirmationSources — quantos candidatos passaram pela confirmação e viraram um dos trades acima.
unconfirmedSignalsCountnumberSó tem significado quando a requisição declarou confirmationSources — quantos candidatos foram descartados antes da simulação de trade por não terem sido corroborados. confirmedSignalsCount + unconfirmedSignalsCount é igual ao total de candidatos que um backtest sem confirmationSources teria produzido.

Os campos conditionsDistributionPct e scoreDistributionPct são um diagnóstico de ajuste de estratégia, não uma métrica de performance — uma estratégia onde entradas raramente exigem várias conditions simultaneamente true é menos seletiva. Use isso pra diagnosticar e refinar a estrutura da sua estratégia. Veja Confirmation Sources acima pro que alimenta confirmedSignalsCount/unconfirmedSignalsCount.

Campos de detalhe do trade

Formato de cada item retornado por GET /backtest/:id/trades — o único lugar onde detalhe trade-a-trade está disponível.

CampoTipoDescrição
numbernumberNúmero sequencial do trade.
entryPricenumberPreço em que o trade foi aberto.
exitPricenumberPreço em que o trade foi fechado.
pnlRnumber+3.0 pra um ganho, -1.0 pra uma perda com o riskManagement padrão; outros valores se a estratégia configura seu próprio stopLoss/takeProfit. Já líquido de configuration.entryFeePct/exitFeePct quando definido.
pnlPctnumberGanho/perda percentual no trade. Já líquido de entryFeePct + exitFeePct quando definido.
feeRnumberUnidades de R subtraídas do pnlR desse trade por entryFeePct/exitFeePct (0 se nenhum foi definido). pnlR + feeR recupera o R bruto pré-taxa. Não diretamente comparável entre trades com riskDistance diferentes — veja o aviso abaixo.
stopPricenumberO preço de stop-loss contra o qual esse trade foi gerenciado.
takePricenumberO preço de take-profit contra o qual esse trade foi gerenciado.
riskDistancenumberDistância de preço entre entryPrice e stopPrice — a unidade em que pnlR/feeR são expressos.
holdingCandlesnumberContagem de candles independente de timeframe que a posição ficou aberta (índice do candle de saída menos índice do candle de entrada). Valor mínimo possível é 1, não 0 — uma posição aberta no candle i pode fechar no mais cedo no candle i+1.
exitReasonnumberO que fechou o trade: 0 = stop-loss, 1 = take-profit, 2 = sinal de decision.exit.
totalScoreAtEntrynumberScore total que disparou a entrada.
conditionsAtEntryobjectQuais conditions eram true na entrada.
scoreBreakdownAtEntryobjectScore contribuído por cada condition na entrada.
feeR escala inversamente com o próprio riskDistance de cada trade — um stop apertado transforma uma taxa pequena em vários R de custo, um stop largo torna a mesma taxa quase invisível em termos de R. Uma vez que entryFeePct/exitFeePct são definidos, 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.

#Códigos de erro

Toda resposta de erro tem o mesmo formato: { "error": "<código>", "message": "<texto>" }.

StatusCódigoDescrição
400invalid_payloadO corpo da requisição está malformado, faltando campos obrigatórios, ou o strategySnapshotJson falha na validação (intervalo de data ruim, ativo desconhecido, expressão de DSL inválida, etc.) — confira o campo message pros detalhes.
401api_key_missingO header x-api-key não foi enviado.
401api_key_invalidA API key fornecida é inválida, revogada ou inativa.
402insufficient_creditsCréditos insuficientes pra executar esse backtest.
429rate_limit_exceededMuitas requisições pra essa conta. Reduza o ritmo e tente de novo.
500execution_failedErro de execução no servidor. Confira o campo errorMessage.
Respostas 400 não distinguem hoje um corpo de requisição malformado de uma estratégia inválida, um ativo não suportado, ou uma data fora do intervalo — todos esses compartilham o único código invalid_payload hoje, com o motivo específico só em message. Não ramifique num código mais granular pra esses casos; leia o texto da mensagem em vez disso.

Quando um erro acontece no nível de execução (depois da submissão), o status do backtest é definido pra Failed e o campo errorMessage contém uma descrição do que deu errado.