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
Nome e parâmetros
titleé o nome no app. Cadainputvira um campo do formulário de rodar. -
2
Preparar
OnInitroda uma vez, antes de tudo. Crie os indicadores aqui. -
3
Reagir
OnBarroda 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
Em Experts, toque em Importar expert e escolha o arquivo
.js. - 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
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_SUCCEEDEDimpede o expert de começar. - OnTick(symbol)
- A cada cotação nova do ativo do expert e dos ativos dos parâmetros;
symboldiz 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,resulte{ deal, order, position }. - OnBookEvent(symbol)
- O livro de ofertas mudou, depois de
market_book_add. - OnDeinit(reason)
- Ao parar.
reasondiz 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). Vemchoicee 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) ebalance(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 omagicdele.- 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_qualitydiz 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
OnTesterdo 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:
idvemnull. 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_canceldesiste, 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_waitacompanha até o fim. - optimization_stop
- Para de testar; o que foi testado fica.
optimization_resumecontinua 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) emax_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@NouDOL@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_listtraz 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 (kindsfiltra 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;
groupfiltra pelo nome ('WIN*','*,!EURUSD').symbols_totalconta. - 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_addemarket_book_releasecomeç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
countcandles 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_totalconta. - orders_get
- Ordens pendentes;
orders_totalconta. - 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.