Pular para o conteúdo
TroiaQuants — início

SDK · JavaScript e TypeScript

Escreva seu expert.

Um expert é um robô que opera sozinho na sua conta de corretora. Você escreve uma classe em JavaScript; o app roda ao vivo e testa no histórico, com o mesmo arquivo.

Instalar

$ npm install @troiaquants/sdk
Versão
—
Roda em
Node 22+ e navegador
Dependências
Nenhuma
Licença
MIT
Nesta página Instalar

Instalar

Para rodar no app, não precisa instalar nada: o app já traz o SDK, e você só importa o arquivo do expert. Instale para escrever no seu computador, com o editor completando os nomes e mostrando os tipos, e para rodar experts pelo Node.

npm install @troiaquants/sdk

Pelo Node você precisa de uma chave de API, criada no app em Chaves de API. Toda chave lê; para enviar ordens, ela precisa da permissão de negociação, e para testar nos servidores e guardar experts e testes, da permissão de backtests.

Seu primeiro expert

Um expert é uma classe que estende Expert, exportada como padrão do arquivo. Este compra quando a média rápida cruza a lenta para cima e vende quando cruza para baixo:

import { Expert, input, MODE_EMA, PRICE_CLOSE, POSITION_TYPE_BUY } from '@troiaquants/sdk';

export default class MaCross extends Expert {
  static title = 'Cruzamento de médias';

  fast   = input.int(9,    { label: 'Média rápida', min: 2, max: 50 });
  slow   = input.int(21,   { label: 'Média lenta', min: 5, max: 200 });
  volume = input.double(1, { label: 'Volume' });

  async OnInit() {
    this.fastMa = await this.iMA(this._Symbol, this._Period, this.fast, 0, MODE_EMA, PRICE_CLOSE);
    this.slowMa = await this.iMA(this._Symbol, this._Period, this.slow, 0, MODE_EMA, PRICE_CLOSE);
  }

  // Um candle novo abriu: o que acabou de fechar está na posição 1
  async OnBar() {
    const [fastBefore, fastNow] = this.CopyBuffer(this.fastMa, 0, 1, 2);
    const [slowBefore, slowNow] = this.CopyBuffer(this.slowMa, 0, 1, 2);
    const up = fastBefore <= slowBefore && fastNow > slowNow;
    const down = fastBefore >= slowBefore && fastNow < slowNow;
    if (!up && !down) return;

    const position = this.positions_get({ symbol: this._Symbol }).find((p) => p.magic === this.magic);
    if (position && (position.type === POSITION_TYPE_BUY) === up) return;
    if (position) await this.trade.PositionClose(position.ticket);
    await (up ? this.trade.Buy(this.volume) : this.trade.Sell(this.volume));
  }
}
  1. 1

    Nome e parâmetros

    title é o nome no app. Cada input vira um campo do formulário de rodar.

  2. 2

    Preparar

    OnInit roda uma vez, antes de tudo. Crie os indicadores aqui.

  3. 3

    Reagir

    OnBar roda a cada candle novo. Escreva só os eventos que quiser.

Há três experts completos para começar — cruzamento de médias, rompimento da abertura e stop móvel. Estão no app, em Experts de exemplo, e na pasta examples do pacote.

Rodar no app

  1. 1

    Em Experts, toque em Importar expert e escolha o arquivo .js.

  2. 2

    Toque em Iniciar e escolha a conta, o ativo, o tempo gráfico e o número mágico — o número que marca as ordens deste expert, para ele reconhecer as posições dele. O mesmo expert pode rodar mais vezes, com outra conta ou outro ativo: Nova execução.

  3. 3

    Ajuste os parâmetros e, se quiser, os limites de risco (a perda máxima do dia e a queda máxima: atingido um, a plataforma zera e para), escolha onde ele roda — neste computador ou na nuvem, os nossos servidores — e confirme. O resultado, as posições e o diário aparecem na página do expert; um expert com vários ativos mostra cada um com a sua posição sobre o gráfico, e as operações do dia de cada ativo embaixo dele.

Isolado. O expert roda num lugar separado, no seu navegador ou nos nossos servidores: sem acesso à internet, à sua chave ou às suas outras contas. Ele só lê e envia ordens pela conta que você escolheu.

Neste computador, ele roda enquanto o app estiver aberto. Na nuvem, continua com o app fechado e o computador desligado, e a hora custa créditos. Um expert guardado pela API ou por um agente de IA roda só na nuvem.

Parâmetros

Cada parâmetro é um campo da classe com o seu valor padrão. O app monta o formulário a partir deles, e o teste no histórico usa os mesmos.

volume  = input.double(1, { label: 'Volume', min: 1, step: 1 });
period  = input.int(14, { label: 'Período', min: 2, max: 100 });
reverse = input.bool(false, { label: 'Inverter os sinais' });
note    = input.string('', { label: 'Comentário das ordens' });
method  = input.enum('ema', ['sma', 'ema'], { label: 'Tipo de média' });

label é o nome no formulário; min e max limitam os números, e step é o passo entre eles. Dentro das funções, this.period já é o número escolhido.

Ativos em várias contas

Um ativo também é um parâmetro. Quem liga o expert escolhe, para cada um, uma conta de qualquer corretora e um ativo dela. O parâmetro vai onde vai o nome de um ativo, e a ordem segue sozinha para a conta daquele ativo.

a = input.symbol({ label: 'WIN na XP' });
b = input.symbol({ label: 'WIN na ActivTrades' });

OnTick(symbol) {                       // por cada ativo dos parâmetros
  if (this.symbol_info_tick(this.a).last - this.symbol_info_tick(this.b).last > this.gap) {
    this.trade.Sell(1, this.a);        // na conta de a
    this.trade.Buy(1, this.b);         // na conta de b
  }
}

Um ativo da primeira conta tem o nome que a corretora dá (WINZ26); um de outra conta leva a conta depois de uma barra (WINZ26|ActivTradesCorp-Server). account_info(this.b) é a conta de b; posições, ordens e negócios trazem os de todas as contas. O número mágico é o mesmo em todas.

Eventos

As funções que o expert escreve e a plataforma chama. Todas podem ser async.

OnInit()
Uma vez, ao começar. Crie os indicadores e peça os dados de outros ativos. Devolver um número diferente de INIT_SUCCEEDED impede o expert de começar.
OnTick(symbol)
A cada cotação nova do ativo do expert e dos ativos dos parâmetros; symbol diz qual. A que chega enquanto a anterior do mesmo ativo ainda roda é pulada.
OnBar()
Quando abre um candle novo no tempo gráfico do expert.
OnTimer()
A cada intervalo marcado com EventSetTimer(segundos).
OnTrade()
Depois de qualquer mudança nas ordens e posições da conta.
OnTradeTransaction(…)
Cada passo de uma ordem: aceita, executada, alterada, cancelada. Recebe trans, request, result e { deal, order, position }.
OnBookEvent(symbol)
O livro de ofertas mudou, depois de market_book_add.
OnDeinit(reason)
Ao parar. reason diz por quê.
OnTester()
Só no teste no histórico, no fim, antes de OnDeinit. Devolve a medida do expert para o teste, pela qual a otimização pode ordenar os resultados.

Ler o mercado

Ler é imediato

O SDK mantém conta, posições, ordens, cotações e candles em dia pelo tempo real. Leituras não levam await.

Enviar espera

O que vai à corretora leva await: ordens, histórico e a criação de um indicador.

const { bid, ask } = this.symbol_info_tick(this._Symbol);
const { equity } = this.account_info();
const closed = this.iClose(this._Symbol, this._Period, 1);    // o último candle fechado
const hours = this.copy_rates_from_pos(this._Symbol, 'H1', 0, 50);
const mine = this.positions_get({ symbol: this._Symbol }).filter((p) => p.magic === this.magic);

_Symbol, _Period, _Point e _Digits são o ativo, o tempo gráfico, o tamanho do ponto e as casas decimais de onde o expert roda. Ativo vazio é o do expert; tempo gráfico 0 é o dele.

Para ler outro ativo, peça no OnInit: await this.symbol_select('PETR4'). Sem isso, a primeira leitura vem vazia e o ativo passa a ser acompanhado.

Indicadores

Crie no OnInit, com await, e leia com CopyBuffer(indicador, linha, início, quantos). Os valores vêm do mais antigo para o mais novo; o início 0 é o candle em formação, 1 o último fechado.

async OnInit() {
  this.rsi = await this.iRSI(this._Symbol, this._Period, 14, PRICE_CLOSE);
  this.bands = await this.iBands(this._Symbol, this._Period, 20, 0, 2, PRICE_CLOSE);
}

async OnBar() {
  const [rsi] = this.CopyBuffer(this.rsi, 0, 1, 1);
  const [upper] = this.CopyBuffer(this.bands, 1, 1, 1);   // linhas: 0 meio, 1 superior, 2 inferior
}
  • iMA
  • iRSI
  • iMACD
  • iBands
  • iATR
  • iStochastic
  • iADX
  • iCCI
  • iMomentum
  • iSAR
  • iStdDev
  • iOBV

Ordens

Os atalhos de this.trade marcam a ordem com o número mágico do expert e usam o ativo dele quando você não diz outro.

const { ask } = this.symbol_info_tick(this._Symbol);
const stop = ask - 200 * this._Point;
const target = ask + 400 * this._Point;

const result = await this.trade.Buy(this.volume, this._Symbol, 0, stop, target);
if (result.retcode !== TRADE_RETCODE_DONE) this.Print(`recusada: ${result.retcode} ${result.comment}`);
  • Buy
  • Sell
  • BuyLimit
  • SellLimit
  • BuyStop
  • SellStop
  • PositionClose
  • PositionClosePartial
  • PositionModify
  • OrderModify
  • OrderDelete

Para o pedido completo, order_send(request), com os campos da API. Uma ordem recusada não é erro: volta como resultado, e o retcode diz o motivo.

10009
TRADE_RETCODE_DONE
Executada
10008
TRADE_RETCODE_PLACED
Ordem pendente colocada
10016
TRADE_RETCODE_INVALID_STOPS
Stop ou alvo em preço inválido
10018
TRADE_RETCODE_MARKET_CLOSED
Mercado fechado
10019
TRADE_RETCODE_NO_MONEY
Sem dinheiro para a margem
10024
TRADE_RETCODE_TOO_MANY_REQUESTS
Ordens demais por segundo
10031
TRADE_RETCODE_CONNECTION
Conta sem conexão com a corretora

IA

O expert pode perguntar a um modelo de IA da plataforma. Ele monta os dados (o estado: um texto ou um objeto) e faz perguntas com tipo. O modelo responde todas de uma vez, cada uma com as probabilidades por trás. Hoje o modelo é o typesafe-ai/jev, rápido e feito para decidir.

async OnBar() {
  const candles = this.copy_rates_from_pos(this._Symbol, this._Period, 1, 10);
  const { answers, credits } = await this.ai_decide({
    model: 'typesafe-ai/jev',
    state: { symbol: this._Symbol, candles },
    questions: {
      side:  { type: 'choice', instructions: 'Para onde o preço vai?', criteria: { buy: 'alta', sell: 'queda', wait: 'sem direção' } },
      risk:  { type: 'score', criteria: ['baixo', 'médio', 'alto'] },
      trend: { type: 'boolean', instructions: 'O mercado está em tendência?' },
    },
  });
  // answers.side.choice → 'buy', answers.side.probabilities.buy → 0.71
  // answers.risk.score → 1.32 (de 0 a 2), answers.trend.probability → 0.83
  if (answers.side.choice === 'buy' && answers.side.probabilities.buy > 0.7) await this.trade.Buy(1);
}
'choice'
Escolhe uma das opções de criteria (nome e descrição de cada uma). Vem choice e a probabilidade de cada opção.
'score'
Dá uma nota numa escala de níveis, do mais baixo ao mais alto. Vem score, de 0 ao número de níveis − 1, com casas decimais.
'boolean'
Sim ou não. Vem probability, a chance de sim, de 0 a 1.
  • Cada chamada custa créditos, ao vivo e no teste no histórico, e quase sempre é uma fração de um crédito. A resposta traz credits (o que custou) e balance (o saldo depois).
  • Sem saldo, a chamada dá erro e o expert para, como qualquer erro. Pergunte uma vez por candle, não a cada cotação.
  • No teste, cada chamada espera a resposta do modelo: um teste com IA demora mais. O relatório soma as chamadas e os créditos em report.ai.
  • Uma pergunta que o modelo se recusa a responder vem como { type: 'refusal' }, e as outras valem.
  • No app, a aba IA de cada robô mostra o consumo dele no período (créditos, chamadas e tokens, por dia, por origem e por modelo), e o Perfil mostra o da conta inteira.

Fora do expert, pelo código, as mesmas funções que os agentes de IA usam pelo MCP:

tq.ai_models()
Os modelos: o que fazem, os tipos de pergunta e o preço em créditos por milhão de tokens.
tq.ai_decide(pedido)
O mesmo do expert, num script. Com { expert_id }, a chamada conta no consumo daquele expert.
tq.ai_usage(filtro)
O consumo de um período, de todos os experts ou de um: chamadas, tokens de entrada, em cache e de saída, e créditos, por modelo, por expert, por origem e por dia.

Regras

  • positions_get() traz todas as posições da conta. As do expert são as que têm o magic dele.
  • O expert só fala com o mundo por this: sem internet, sem tela e sem arquivos, com o relógio (TimeCurrent()) e o timer da plataforma. É o que deixa o mesmo arquivo rodar ao vivo e no teste.
  • Se uma função do expert der erro, ele para onde estava. As posições e ordens ficam como estão.
  • Print(…) escreve no diário do expert; ExpertRemove() o faz parar sozinho.

Testar no histórico

O mesmo arquivo roda sobre os candles da corretora, numa conta simulada. No app, na página do expert, toque em Testar no histórico: escolha o período, o depósito e os parâmetros e veja o lucro, o fator de lucro, a maior queda, a curva e os negócios.

Pelo código, no seu computador (de graça) ou nos servidores da plataforma (pelos créditos de um teste), com o mesmo pedido e a mesma resposta:

import { readFile } from 'node:fs/promises';

const request = {
  code: await readFile('cruzamento-de-medias.js', 'utf8'),   // ou expert_id: 12, um expert guardado
  login: 67312128,   // os candles e as regras do ativo vêm da corretora desta conta
  symbol: 'WINZ26',
  timeframe: 'M5',
  from: '2026-07-01',
  to: '2026-09-30',  // inclusive
  deposit: 10_000,
  model: 'ohlc_m1',
  inputs: { fast: 12, slow: 26 },
  // sem costs o teste sai só com o spread, e o relatório avisa em warnings
  costs: { commission: 1.2, fee_percent: 0, slippage: 1 },   // por contrato e negócio; % do valor; ticks
};

const test = await tq.backtest(request);                      // no seu computador
const other = await tq.backtest(request, { on: 'server' });   // nos servidores

console.log(test.status, test.result.profit, test.result.trades, test.result.profit_factor);
console.log(test.report.statistics[STAT_SHARPE_RATIO]);   // todos os números do teste, pela constante

Os candles são lidos da conta como os servidores leem, então os números saem iguais nos dois lugares. O arquivo só pode importar @troiaquants/sdk, como no servidor. Os dias são os do relógio do servidor da corretora.

A resposta é o teste como a conta guarda: status, result com os números principais, report com o relatório inteiro (negócios, ordens, curva de patrimônio e diário do expert), credits (o que custou) e seconds. Um teste que falha não lança erro: responde failed. ApiError é a API recusando o pedido, como uma conta que não é sua ou créditos que não bastam.

'done'
Terminou, com o relatório.
'failed'
Não terminou; o motivo vem em error (o expert deu erro, ou o período não tem candles). No servidor, não custa nada.
'cancelled'
Você desistiu pelo signal. No servidor, não custa nada.
const stop = new AbortController();
const test = await tq.backtest(request, {
  progress: (p) => console.log(Math.round(p * 100) + '%'),   // de 0 a 1
  signal: stop.signal,                                         // stop.abort() desiste do teste
});

model escolhe as cotações do teste:

'ohlc_m1'
Padrão. Os quatro preços de cada candle de 1 minuto viram cotações.
'open_prices'
Uma cotação por candle, na abertura. Mais rápido, para experts que só olham candles fechados.
'real_ticks'
As cotações que a plataforma gravou no servidor da conta, uma a uma, com o spread delas. Um minuto não gravado inteiro vem do candle de 1 minuto, e report.history_quality diz a porcentagem dos minutos que vieram das cotações gravadas.

O relatório diz o trecho que rodou de fato (report.tested_from e report.tested_to, em segundos do relógio da corretora) e avisa em report.warnings o que muda a leitura dos números: o histórico da corretora começa depois do dia pedido (o teste roda só onde há candles) ou só parte dos minutos tinha cotações gravadas.

Como numa corretora, a conta do teste tem margem: uma ordem que a margem livre não cobre volta com 10019. O stop out da corretora (o nível que a conta informa; sem ele, o patrimônio em zero) fecha tudo e encerra o teste ali, também em cada combinação de uma otimização, e report.stop_out diz quando. Um teste que, sem nada aberto, não paga mais nem o menor lote também termina ali. No histórico da B3, a margem por contrato vai em costs.margin.

Ainda não simulado: swap e horário de pregão. O teste tem ordens a mercado e pendentes, stop e alvo, e pode operar vários ativos e contas (abaixo).

Vários ativos e contas

No teste, o expert lê e opera outros ativos da mesma conta pelos parâmetros de ativo: as cotações de todos chegam numa fila só, na ordem do tempo. Nos servidores, e no app, um parâmetro de ativo também pode ser { login, symbol } de outra conta, de qualquer corretora (com server quando o número existe em duas): o teste opera em todas, cada conta simulada à parte, com o mesmo depósito e no relógio da sua corretora.

const test = await tq.backtest({
  ...request,
  inputs: { a: 'WINZ26', b: { login: 7120455, symbol: 'WINZ26' } },   // b noutra corretora
}, { on: 'server' });

test.report.accounts   // cada conta: depósito, saldo, lucro, maior queda e negócios

O relatório é o total: os negócios de todas as contas, com os ativos das outras nomeados com a conta depois de uma barra (WINZ26|Genial-Demo), e as curvas somadas. Contas de moedas diferentes não se somam: o relatório fica o da conta testada, com um aviso em warnings, e cada uma está em accounts. No motor do teste, direto, as outras contas vão em accounts: [{ name, offset, history }].

Arquivos do robô

No teste, o expert escreve as próprias planilhas: os sinais, os negócios como ele os vê. Cada FileWrite é uma linha, com os valores separados pelo delimitador.

import { FILE_CSV, FILE_WRITE } from '@troiaquants/sdk';

OnInit()   { this.file = this.FileOpen('sinais.csv', FILE_WRITE | FILE_CSV, ';'); }
OnTick()   { this.FileWrite(this.file, this.TimeCurrent(), this.symbol_info_tick().bid, this.sinal); }
OnDeinit() { this.FileClose(this.file); }

Os arquivos ficam com o relatório, em report.files (até 20, somando 2 milhões de caracteres), e tq.link(`/backtests/${id}/files/robot/sinais.csv`) dá um endereço que os abre por uma hora. Ao vivo eles não são guardados: FileOpen devolve INVALID_HANDLE; use Print.

Histórico da B3

Sem conta em corretora, um teste roda nos negócios da própria B3, que a plataforma guarda todo dia a partir dos arquivos públicos da bolsa, desde 15/09/2026: WIN, WDO, IND e DOL (cada contrato e a série contínua, WIN@N, como na XP) e as 20 ações mais negociadas. No app, escolha B3 · dados públicos da bolsa em Histórico de. Pelo código, troque login por source:

const test = await tq.backtest({ ...request, login: undefined, source: 'B3', symbol: 'WIN@N' });

const b3 = tq.exchange('B3');
await b3.symbols();                                              // o que há, com o primeiro e o último dia
await b3.copy_rates_range('WIN@N', 'M5', '2026-10-01', '2026-10-10');
await b3.copy_ticks_range('PETR4', '2026-10-06T10:00', '2026-10-06T11:00');
await b3.rolls('WIN@N', '2026-09-15', '2026-12-31');              // quando a série trocou de contrato
  • Os candles de 1 minuto saem dos negócios; os tempos maiores, deles. Com real_ticks, cada negócio é uma cotação: sem a melhor oferta no arquivo, a venda é o preço do negócio e a compra é ele mais o spread (o dos custos ou, sem ele, um tick).
  • A série contínua passa ao contrato seguinte no primeiro pregão depois de um em que ele negociou mais contratos que o atual, com os preços como foram negociados: o salto entre os dois aparece. O que estiver aberto na troca é fechado no último preço do contrato antigo, com os custos de um negócio (motivo DEAL_REASON_ROLLOVER), as ordens pendentes são canceladas e o relatório avisa. O expert pode reabrir no contrato novo.
  • As regras de cada ativo (valor do ponto, menor variação, lote) vêm de uma tabela da plataforma, em reais: o WIN anda de 5 em 5 pontos e cada 5 valem R$ 1 por contrato; o WDO anda de 0,5 em 0,5, que valem R$ 5; ação em lotes de 100.

Otimizar

O mesmo teste com várias combinações de parâmetros, para achar as melhores. No app, use Otimizar na página do expert: escolha o que varia, de quanto a quanto, e se testa todas as combinações ou faz a busca genética. Pelo código, é o pedido do teste com o que varia:

const run = await tq.optimize({
  ...request,
  ranges: [
    { name: 'fast', start: 5, step: 2, stop: 19 },    // 8 valores
    { name: 'slow', start: 20, step: 5, stop: 50 },   // 7 valores
  ],
  fixed: { volume: 1 },        // os outros parâmetros
  method: 'genetic',           // ou 'complete': todas as 56
  criterion: 'sharpe_ratio',
}, {
  cores: 4,                    // no seu computador: testes ao mesmo tempo
  progress: (o) => console.log(o.tested, o.best?.value),
});

console.log(run.status, run.tested, run.passes[0].inputs);   // as melhores, da melhor para a pior

Uma faixa vai de start a stop por step, as duas pontas incluídas, ou lista os valores: { name: 'mode', values: ['sma', 'ema'] }. criterion diz qual é a melhor:

'sharpe_ratio'
Padrão. O saldo final vezes o índice de Sharpe.
'balance'
O saldo final.
'profit_factor'
O saldo final vezes o fator de lucro.
'expected_payoff'
O saldo final vezes o retorno esperado por negócio.
'drawdown'
O saldo final vezes 100% menos a maior queda do patrimônio.
'recovery_factor'
O saldo final vezes o fator de recuperação.
'custom'
O que o OnTester do expert devolve (abaixo).

'complete' testa todas as combinações. 'genetic' (o padrão) testa uma geração por vez, de 64, a partir das melhores até ali, e para quando 5 gerações seguidas não melhoram as melhores (no máximo 50 gerações, 3.200 testes); nunca testa a mesma combinação duas vezes. max_tests põe um teto: ela para ao chegar nele, e o custo máximo sai dele. forward guarda o fim do período para conferir ('1/4', '1/3', '1/2' ou um dia): a busca roda só antes dele, e as forward_top melhores (10) rodam de novo nesse trecho; cada uma conferida traz forward com os números de lá. seed fixa o caminho da busca: o mesmo número faz as mesmas escolhas.

No seu computador, a otimização roda um teste por núcleo (todos menos um, ou os de cores), e os candles são lidos uma vez para todos. No servidor, cada combinação custa os créditos de um teste. Ela termina 'done', 'stopped' (pelo signal, com o que foi testado; no servidor, também quando o saldo acaba) ou 'failed' (o histórico não serve: 8 testes falharam e nenhum terminou). passes traz as melhores, cada uma com inputs, statistics e value, a posição pelo critério; top diz quantas (20, até 500). Uma combinação que o expert recusou no OnInit fica com error, no fim.

Robustez

Uma combinação boa pode ser um pico isolado. A robustez a testa de novo, nos servidores (cada teste cobrado), onde ela poderia facilmente ter estado: a vizinhança dela na grade das faixas, ou ela mesma com os custos sorteados entre duas pontas. É uma otimização de todas essas combinações, acompanhada como qualquer outra.

import { robustnessSummary } from '@troiaquants/sdk/backtest';

const plan = await tq.robustness_plan(optimization.id, { kind: 'neighbors' });          // quantos testes e o custo
const started = await tq.optimization_robustness(optimization.id, { kind: 'costs', runs: 50, costs: { slippage: [0, 3] } });
const ended = await tq.optimization_wait(started.id);
const passes = await tq.optimization_passes(ended.id, 500);
console.log(robustnessSummary(ended.settings.robustness, passes));   // quantas lucraram, a faixa de lucro

Janelas que avançam

O walk forward corta o período em rodadas: cada uma otimiza search_months meses e opera a melhor combinação, sem mexer, nos check_months seguintes; a rodada seguinte começa esse tanto depois. Mostra se reotimizar de tempos em tempos funciona para a estratégia, e se os ajustes escolhidos ficam parecidos de uma rodada para outra. No app, escolha Janelas que avançam na conferência da otimização.

import { walkForwardTotals } from '@troiaquants/sdk';

const walk = await tq.walkForward({
  ...request, ranges, fixed,
  from: '2025-01-01', to: '2026-09-30',
  search_months: 6,            // cada rodada otimiza 6 meses
  check_months: 1,             // e opera a melhor no mês seguinte
}, { on: 'server' });          // ou no seu computador, de graça

for (const r of walk.rounds) console.log(r.round, r.best?.inputs, r.best?.forward?.statistics);
const total = walkForwardTotals(walk.rounds);   // o que os trechos operados fizeram, somados
console.log(total.profit, total.profitable, 'de', total.rounds);

Nos servidores, cada rodada custa o que a otimização dela custa, mais um teste para operar a melhor; elas rodam uma depois da outra, e walk_forward_stop e walk_forward_resume param e continuam da rodada em que estavam.

Os números do teste

report.statistics traz os 42 números do teste, cada um pela sua constante: resultado, ganhos e perdas brutos, melhor e pior negócio, sequências de ganhos e de perdas, quedas do saldo e do patrimônio (em dinheiro e em porcentagem), retorno esperado, fator de lucro, fator de recuperação, índice de Sharpe (do ano) e as contagens de negócios. O patrimônio é medido a cada cotação. Retiradas, nível de margem e o critério complexo ficam em 0.

  • STAT_PROFIT
  • STAT_TRADES
  • STAT_PROFIT_TRADES
  • STAT_PROFIT_FACTOR
  • STAT_EXPECTED_PAYOFF
  • STAT_RECOVERY_FACTOR
  • STAT_SHARPE_RATIO
  • STAT_EQUITY_DD
  • STAT_EQUITY_DDREL_PERCENT
  • STAT_BALANCE_DD
  • STAT_MAX_CONLOSSES
  • STAT_CUSTOM_ONTESTER

O próprio expert pode dar a sua medida do teste: OnTester é chamado no fim, antes do OnDeinit, e o que ele devolve fica em STAT_CUSTOM_ONTESTER, pelo qual a otimização ordena com criterion: 'custom'. Lá, this.TesterStatistics(STAT_…) já tem os números do teste inteiro.

OnTester() {
  // Lucro por unidade de queda, só com 30 negócios ou mais
  if (this.TesterStatistics(STAT_TRADES) < 30) return 0;
  return this.TesterStatistics(STAT_PROFIT) / Math.max(1, this.TesterStatistics(STAT_EQUITY_DD));
}

No app, no servidor e em tq.backtest e tq.optimize, cada teste e cada combinação carregam o arquivo de novo: o que ele guarda fora da classe não passa de um para o outro.

No servidor e os créditos

Com on: 'server', o teste roda nos servidores da plataforma, num lugar isolado e sem internet, e o seu computador só espera a resposta. Cada teste no servidor custa os créditos de backtest_cost, reservados enquanto ele espera e debitados só quando termina bem; cada combinação de uma otimização custa o mesmo, quando termina. A chave de API precisa da permissão Backtests.

const { credits, reserved, available, backtest_cost } = await tq.credits();

Onde o resultado fica guardado depende de como o expert vai no pedido:

expert_id, no seu computador
Roda de graça e fica guardado na conta: aparece no app, na página do expert.
expert_id, no servidor
Roda nos servidores, pelos créditos, e fica guardado na conta.
code, no seu computador
Roda de graça e nada fica guardado: id vem null.
code, no servidor
O arquivo é guardado em Experts com o nome que a classe declara (ou name); o mesmo nome atualiza o que já existe. O teste fica guardado com ele.

Ao mesmo tempo, cada usuário tem até 50 testes (esperam na fila, dividida de forma justa entre os usuários) e 2 otimizações esperando ou rodando no servidor; um teste pode rodar até 120 minutos. Quando o saldo não paga a próxima combinação, a otimização para e pode continuar depois, sem testar de novo o que já testou.

Passo a passo, as mesmas funções que os agentes de IA usam pelo MCP:

backtest_run
Põe o teste de um expert guardado na fila e responde na hora, com status: 'queued'.
backtest_wait
Acompanha até ele terminar; backtest_cancel desiste, sem custo.
optimization_plan
Quantas combinações e testes uma otimização teria e o máximo que custaria, sem rodar.
optimization_run
Põe a otimização na fila; optimization_wait acompanha até o fim.
optimization_stop
Para de testar; o que foi testado fica. optimization_resume continua de onde parou.

Experts e testes guardados

Os experts que você importou no app, e todos os testes e otimizações, os do app e os do código. Toda chave lê; para guardar, mudar ou apagar, ela precisa da permissão Backtests.

const experts = await tq.experts_list();                  // sem os arquivos
const expert = await tq.expert_add({ code });              // nome e parâmetros lidos do arquivo
await tq.expert_update(expert.id, { code: newVersion });   // uma versão nova do arquivo

const tests = await tq.backtests_list({ expert_id: expert.id });       // o mais novo primeiro
const test = await tq.backtest_get(tests[0].id);                       // com o relatório
const csv = await tq.backtest_file(test.id, 'deals.csv');              // todos os negócios, em planilha
const { url } = await tq.link(`/backtests/${test.id}/files/equity.csv`); // abre sem a chave por uma hora
const best = await tq.optimization_passes(optimization.id, 10);        // as 10 melhores
const next = await tq.optimization_passes(optimization.id, 500, { offset: 500 });   // da 501ª à 1000ª
Experts
experts_list · expert_get · expert_add · expert_update · expert_remove
Ao vivo, nos servidores
runs_list · run_get · expert_start · run_update · run_start · run_stop · run_pause · run_resume · run_flatten · run_remove · run_journal · run_statistics · run_compare · notices_list · notices_read
Testes
backtests_list · backtest_get · backtest_file · link · backtests_portfolio · backtest_remove
Otimizações
optimizations_list · optimization_get · optimization_passes · optimization_remove
Créditos
credits

Apagar um expert apaga os testes dele. Ligado pelo código, um expert roda sempre nos servidores e continua com o computador desligado; a hora custa créditos (runs_list diz o preço). Ligar, parar, pausar e zerar pedem a permissão de negociação.

Ao vivo

O mesmo expert roda quantas vezes você quiser, cada execução com o seu número (id), a sua conta, o seu ativo e o seu número mágico.

const run = await tq.expert_start(expert.id, {
  login: 50389039, symbol: 'WIN@N', timeframe: 'M5',
  inputs: { b: { login: 7120455, symbol: 'WINZ26' } },       // um ativo noutra corretora
  limits: { daily_loss: 500, max_drawdown: 1500 },            // na moeda da conta
});
await tq.run_pause(run.id);                                  // sem abrir posição nova
const avisos = await tq.notices_list({ unread: true });
  • Limites de risco. daily_loss é o máximo de perda no dia (fechada e aberta, no dia do seu fuso) e max_drawdown, o máximo de queda do resultado desde o início. Atingido um, a plataforma zera a execução e a para. No app, os mesmos campos estão ao ligar.
  • Futuros da B3. O ativo pode ser a série WIN@N, WDO@N, IND@N ou DOL@N: a execução opera o contrato vigente e, no dia em que a série troca, fecha o que tem no velho e recomeça no novo. Num contrato fixo, ela recebe um aviso dias antes do vencimento.
  • Avisos. notices_list traz o que precisa de você: uma execução que falhou ou parou sozinha, uma ordem recusada, um limite atingido, uma troca de contrato, uma conta sem conexão com a corretora, e cada posição de um robô que fechou, com o resultado (kinds filtra os tipos). Uma queda que volta sozinha em até uns 2 minutos não vira aviso; a senha recusada vira na hora. Os mesmos avisos aparecem no app e chegam como notificação no celular e no computador e por e-mail, nos tipos que você escolhe no Perfil. notices_read(id) marca como vistos até aquele.

Testes juntos

Testes já feitos, juntos, como robôs rodando lado a lado: o resultado e a maior queda do conjunto, cada teste, e o quanto os resultados de cada dia andam juntos. Não roda teste novo nem custa créditos. No app, em Laboratório, Juntar testes.

const carteira = await tq.backtests_portfolio([441, 452, 460]);
carteira.whole.profit, carteira.whole.max_drawdown   // o conjunto
carteira.tests                                        // cada teste, com o expert e o período
carteira.correlation                                  // de -1 (sempre ao contrário) a 1 (sempre juntos)

Os testes precisam ser na mesma moeda. Testes de períodos que não se cruzam também entram, e warnings avisa que o conjunto soma períodos diferentes.

Monte Carlo

monteCarlo sorteia de novo, milhares de vezes, os negócios de um teste feito, sem rodar teste novo: a faixa de lucro e de quedas que a mesma estratégia poderia ter tido, e as chances de terminar no prejuízo e de cair uma parte do depósito. 'resample' (o padrão) sorteia com repetição, 'shuffle' muda só a ordem e 'skip' perde uma parte dos negócios ao acaso.

import { monteCarlo } from '@troiaquants/sdk/backtest';

const mc = monteCarlo(test.report, { method: 'resample', runs: 1000, ruin: 0.3 });
mc.profit.p5, mc.profit.p95         // o lucro de 5% a 95% das simulações
mc.max_drawdown.p95                 // a maior queda que 95% delas não passaram
mc.loss_probability                 // a parte que terminou no prejuízo
mc.ruin_probability                 // a parte que caiu 30% abaixo do depósito
mc.bands                            // o saldo ao longo dos negócios, para um gráfico

O motor do teste, direto

Para montar o seu próprio teste: backtest roda a classe do expert sobre um histórico, no seu processo, sem guardar nada. O histórico é o de uma conta ou um seu, com copy_rates_range e symbol_info.

import { backtest, optimize, backtestRunner, ParameterSpace, OPTIMIZATION_SHARPE_RATIO } from '@troiaquants/sdk/backtest';
import MaCross from './cruzamento-de-medias.js';

const options = {
  symbol: 'WINZ26', timeframe: 'M5', from: '2026-07-01', to: '2026-09-30', deposit: 10_000,
  history: tq.account(67312128),   // ou { copy_rates_range, symbol_info } seus
  spread: 1,                       // ticks inteiros; o do ativo quando falta
  commission: 0.5,                 // por lote, por negócio
  feePercent: 0.03,                // % do valor de cada negócio
  slippage: 1,                     // ticks inteiros a pior a mercado, nas stop e nos stop loss
};
const report = await backtest(MaCross, { ...options, inputs: { fast: 12, slow: 26 } });

const passes = await optimize({
  space: new ParameterSpace([{ name: 'fast', start: 5, step: 2, stop: 19 }], { slow: 26 }),
  method: 'genetic',
  criterion: OPTIMIZATION_SHARPE_RATIO,
  runners: [backtestRunner(MaCross, options), backtestRunner(MaCross, options)],
});

Cada runner roda um teste por vez: o motor dá a cada um a próxima combinação. known (as combinações já testadas) e seed continuam uma otimização interrompida sem repetir testes; maxTests para de dar testes ao chegar nele (contando os já testados); signal para e gate pausa. mostTests(space, method, { maxTests }) diz o máximo de testes antes de começar.

Rodar pelo Node

O expert roda no seu computador ou servidor, enquanto o processo estiver de pé. As ordens e posições aparecem na conta, no app.

import { TroiaQuants } from '@troiaquants/sdk';
import MaCross from './cruzamento-de-medias.js';

const tq = new TroiaQuants({ apiKey: process.env.TROIAQUANTS_API_KEY });

const running = await tq.account(67312128).attach(MaCross, {
  symbol: 'WINZ26',
  timeframe: 'M5',
  magic: 1001,
  inputs: { fast: 12, slow: 26 },   // os outros ficam com o padrão
});

running.on('print', (line) => console.log(line));
running.on('error', (err) => console.error(err));   // o expert deu erro e foi parado
// await running.stop();

A conta pelo código

Fora de um expert, o SDK é o cliente da API: as suas contas de corretora, cotações, candles e ordens, com os mesmos nomes de dentro do expert, só que cada leitura leva await.

import { TroiaQuants } from '@troiaquants/sdk';

const tq = new TroiaQuants({ apiKey: process.env.TROIAQUANTS_API_KEY });

const accounts = await tq.accounts_list();         // com o estado da conexão de cada uma
const account = tq.account(accounts[0].login);     // pelo número dela na corretora

const { balance, equity, currency } = await account.account_info();
const tick = await account.symbol_info_tick('WINZ26');
const candles = await account.copy_rates_from_pos('WINZ26', 'M5', 0, 500);
const positions = await account.positions_get({ symbol: 'WINZ26' });

Volumes em lotes, preços absolutos e horários na hora do servidor da corretora (segundos Unix). Tempos gráficos pelo nome ('M5', 'H1', 'D1'…) ou pela constante (TIMEFRAME_M5); datas em segundos Unix, texto sem fuso ('2026-09-29T10:00') ou Date. Se o mesmo número de conta existir em duas corretoras, diga qual: tq.account(login, { server }).

account_info
Saldo, patrimônio, lucro, margem, alavancagem e moeda.
symbols_get
Os ativos da corretora; group filtra pelo nome ('WIN*', '*,!EURUSD'). symbols_total conta.
symbol_info
As regras do ativo: casas decimais, tick, contrato e volumes.
symbol_info_tick
A última cotação.
symbol_select
Passa a acompanhar o ativo, ou deixa (false) quando ninguém mais o usa.
market_book_get
O livro de ofertas; market_book_add e market_book_release começam e param.
copy_rates_from_pos
Candles a partir do atual: (symbol, timeframe, start_pos, count), e 0 é o que está em formação.
copy_rates_from
Os count candles até uma data.
copy_rates_range
Os candles de um período.
copy_ticks_range
As cotações que a plataforma gravou de um período (abaixo).
positions_get
Posições abertas, de um ativo, de um grupo ou uma só (ticket); positions_total conta.
orders_get
Ordens pendentes; orders_total conta.
history_deals_get
Negócios de um período, ou de uma ordem (ticket) ou posição (position).
history_orders_get
Ordens encerradas.
order_send
Envia, altera ou cancela uma ordem (abaixo).

No cliente: accounts_list, brokers_list (as corretoras, os métodos e os servidores de cada uma) e key (o que a chave em uso pode fazer). Com a permissão de Contas, account_add, account_update e account_remove cadastram, ligam e desligam, e tiram contas da plataforma.

A plataforma deixa de acompanhar sozinha o ativo que ninguém usa há 10 minutos (sem posição, ordem, tempo real ou consulta); a próxima consulta volta a acompanhá-lo. Pelo navegador, a API só atende as páginas da própria TroiaQuants: fora do app, use o Node.

Ordens pelo código

Com a permissão de negociação na chave, o pedido completo, com os campos da API:

import { TRADE_ACTION_DEAL, ORDER_TYPE_BUY, TRADE_RETCODE_DONE } from '@troiaquants/sdk';

const result = await account.order_send({
  action: TRADE_ACTION_DEAL,
  symbol: 'WINZ26',
  type: ORDER_TYPE_BUY,
  volume: 1,
  sl: tick.ask - 200,
  tp: tick.ask + 400,
  magic: 1001,
});

if (result.retcode !== TRADE_RETCODE_DONE) console.log(result.retcode, result.comment);

Como no expert, uma ordem recusada não é erro: volta como resultado, e o retcode diz o motivo (os códigos estão em Ordens). Para fechar uma posição: action: TRADE_ACTION_DEAL, o tipo oposto, o mesmo volume e position com o número dela.

Tempo real

O que acontece na conta chega sozinho, sem perguntar de novo:

const stop = account.on('tick', 'WINZ26', (tick) => console.log(tick.bid, tick.ask));

account.on('candle', 'WINZ26', 'M5', (candle) => { /* o candle em formação, a cada mudança */ });
account.on('book', 'WINZ26', (book) => { /* o livro de ofertas */ });
account.on('transaction', ({ trans, deal, order, position }) => { /* cada mudança em ordens, negócios e posições */ });
account.on('trade', ({ account, positions, orders }) => { /* depois de qualquer mudança */ });
account.on('account', ({ account, positions, orders }) => { /* também a cada cotação que mexe no lucro */ });
account.on('connection', ({ connected, reason }) => { /* a ligação da conta com a corretora */ });

stop();       // para de ouvir
tq.close();   // encerra a conexão de tempo real

A conexão abre no primeiro on, volta sozinha quando cai e assina tudo de novo. Conta, posições e ordens de cada mensagem são do mesmo instante; seq numera as mensagens, e maior é mais nova.

Cotações gravadas

A plataforma grava, cotação a cotação, os ativos escolhidos de cada servidor de corretora, em arquivos de uma hora. Quem tem conta naquele servidor lê as cotações dele; são as mesmas que o teste usa com model: 'real_ticks'.

import { COPY_TICKS_ALL, COPY_TICKS_TRADE } from '@troiaquants/sdk';

const ticks = await account.copy_ticks_range('WINZ26', '2026-10-06T10:00', '2026-10-06T11:00', COPY_TICKS_ALL);
const deals = await account.copy_ticks_range('WINZ26', '2026-10-06T10:00', '2026-10-06T11:00', COPY_TICKS_TRADE);
const hours = await account.recorded_hours('WINZ26', '2026-10-01', '2026-10-07');   // as horas gravadas, com os buracos

Cada cotação traz time_msc (o horário em milissegundos), bid, ask, last, volume e flags, o que mudou nela. COPY_TICKS_INFO traz só as que mudaram compra ou venda; COPY_TICKS_TRADE, só os negócios.

  • TICK_FLAG_BID
  • TICK_FLAG_ASK
  • TICK_FLAG_LAST
  • TICK_FLAG_VOLUME
  • TICK_FLAG_BUY
  • TICK_FLAG_SELL

Rodar isolado

Para rodar o expert de outra pessoa sem entregar a sua chave, o SDK traz o que o app usa: o expert fica num lugar separado, sem internet, e só lê e envia ordens pela conta que você escolhe.

import { SandboxHost } from '@troiaquants/sdk/sandbox';

// Do lado de quem tem a conta: uma porta para o lugar separado (um Worker, um MessageChannel...)
const host = new SandboxHost(worker);
const { title, inputs } = await host.describe(code);   // nome e parâmetros, sem rodar
await host.start(code, account.terminal(), { symbol: 'WINZ26', timeframe: 'M5', magic: 1001 }, {
  print: (text) => console.log(text),
  error: (err) => console.error(err.message),
});
// ...
await host.stop();
// Do lado separado, dentro do worker: roda o que chegar, com esta mesma cópia do SDK
import { serveExpert } from '@troiaquants/sdk/sandbox';

serveExpert(self, async (code) => import(moduleUrl(code)));

host.backtest(code, opções, history, progress) faz o teste no histórico no lado separado, lendo os candles pelo lado de quem tem a conta; host.test(code, opções, history) roda uma combinação de otimização e devolve só os 42 números. Para não ter acesso à rede, o lado separado precisa ser isolado de verdade: no navegador, um worker dentro de um iframe com sandbox="allow-scripts" e uma política de segurança que proíba conexões.

Erros e limites

Fora do expert, uma chamada que falha lança ApiError, com status (o código HTTP) e code (o motivo em número, para decidir sem ler o texto).

import { ApiError } from '@troiaquants/sdk';

try {
  await account.account_info();
} catch (err) {
  if (err instanceof ApiError) console.log(err.status, err.code, err.message);
}
401
Chave ausente ou revogada
402
Créditos que não pagam o teste no servidor
403
A chave não tem a permissão
404
Conta, ativo, expert ou teste que não existe ou não é seu
429
Chamadas demais, ou testes demais esperando no servidor
503
Conta sem conexão com a corretora
0
A API não foi alcançada
-2
RES_E_INVALID_PARAMS
Parâmetro inválido ou faltando
-4
RES_E_NOT_FOUND
Conta ou ativo que não existe
-6
RES_E_AUTH_FAILED
Chave ausente, inválida ou revogada
-7
RES_E_UNSUPPORTED
A conta ou o ativo usa algo que a plataforma ainda não faz
-8
RES_E_AUTO_TRADING_DISABLED
A chave não tem a permissão de negociar
-10003
RES_E_INTERNAL_FAIL_CONNECT
Sem conexão com a corretora (ou com a API)
-10005
RES_E_INTERNAL_FAIL_TIMEOUT
A corretora não respondeu a tempo
-1
RES_E_FAIL
Qualquer outra falha, e o limite de chamadas

50 /s

Chamadas por chave, até 100 de uma vez. Acima disso, ApiError 429.

10 /s

Ordens por chave, até 20 de uma vez. Acima disso a ordem volta com 10024 e não é enviada.

30

Conexões de tempo real por usuário, cada uma com até 500 assinaturas.

5 · 2

Testes e otimizações de cada usuário esperando ou rodando no servidor ao mesmo tempo.

Tudo o que há em this

Onde está rodando
_Symbol · _Period · _Point · _Digits · magic
Conta e mercado · imediato
account_info · symbol_info · symbol_info_tick · positions_get · orders_get · copy_rates_from_pos · copy_rates_from · copy_rates_range · market_book_get
Um candle · imediato
iTime · iOpen · iHigh · iLow · iClose · iVolume · iBars
Ordens · await
order_send · trade.Buy · Sell · BuyLimit · SellLimit · BuyStop · SellStop · PositionClose · PositionClosePartial · PositionModify · OrderModify · OrderDelete
Indicadores · criar com await
iMA · iRSI · iMACD · iBands · iATR · iStochastic · iADX · iCCI · iMomentum · iSAR · iStdDev · iOBV · CopyBuffer
IA · await, por créditos
ai_decide
Outros ativos e histórico · await
symbol_select · market_book_add · market_book_release · history_deals_get · history_orders_get
Relógio, diário e outros
TimeCurrent · EventSetTimer · EventKillTimer · Print · Comment · Alert · NormalizeDouble · ExpertRemove · GlobalVariableSet · Get · Check · Del · TesterStatistics

Os campos de cada resposta e todas as rotas da API estão na referência da API; os tipos vêm com o pacote, para o editor completar os nomes.