PáginasCiclo de vida da estratégia · 8 / 29
Ciclo de vida da estratégia
Uma estratégia do QuantCraft é um arquivo Python comum que define um pequeno conjunto de funções de callback. O mecanismo de backtest chama essas funções em ordem fixa enquanto percorre os dados históricos. Você não as chama; quem faz isso é o mecanismo. Use o Test para acioná-las (executando código).
O ciclo de vida, do início ao fim:
on_init → on_bar → on_tick (×4 per bar) → account tick(close) → on_finish
└─────────── repeats for every bar ───────────┘No backtest, o on_tick dispara quatro vezes por barra sobre um OHLC sintético de cada barra (sem histórico real de ticks). Depois desses quatro ticks, o mecanismo faz uma última marcação a mercado da conta no fechamento — não um quinto on_tick. O on_timer só está disponível nas execuções forward (forward no gráfico, forward em massa / QuantCloud) quando timerIntervalMs está definido — a caixa Run backtest do IDE não tem controle de timer. Nas execuções forward, o on_tick roda a cada atualização de preço da corretora conectada; nas fronteiras intradiárias costumam disparar on_tick e depois on_bar — não presuma a ordem do backtest.
Esta página cobre cada callback, como as estratégias com vários símbolos se encaixam neles e o básico de que você precisa para escrever sua primeira estratégia.
O arquivo da estratégia
Uma estratégia mínima fica assim:
import quantcraft.process.runtime as bt # account, chart_indicators, qc_* handles
from quantcraft import Timeframe
import quantcraft.runtime as rt
def on_init(symbols=None):
pass
def on_bar(bar_index, bar, fundamentals=None, calendar=None, symbol=None):
close_px = float(bar["close"])
daily = rt.get_bar(Timeframe.DAILY, shift=1)
def on_tick(bar_index, tick_in_bar, price, bar, fundamentals=None, calendar=None, symbol=None):
if tick_in_bar == 3:
close_px = price
def on_finish(bars, symbol=None, bars_by_symbol=None):
passAlgumas regras valem para todos os callbacks:
- Devem ser funções de nível superior no seu arquivo (não aninhadas nem dentro de uma classe).
- O mecanismo de backtest exige
on_init,on_bar,on_tickeon_finishchamáveis (caso contrário a execução falha comUser script must define callable: …). Os botões Test, Compile e Debug Test do IDE aparecem quando a aba.pyativa defineon_init/on_bar/on_finish; a ausência deon_tickainda faz o backtest falhar ao iniciar. Oon_timeré opcional e só é usado em execuções forward com um intervalo de timer. - Adicione apenas os argumentos opcionais que você usa.
calendar,symbols(emon_init) e os argumentos deon_finishsão casados por nome;fundamentalsesymbolsão casados tentando as formas comuns em sequência, então mantenha-os na ordem documentada. A assinatura completa mais segura édef on_bar(bar_index, bar, fundamentals=None, calendar=None, symbol=None). Umcalendardeclarado sempre recebe o identificador do calendário ouNone— nunca o objeto de dados fundamentalistas. - Dentro dos callbacks você coloca ordens por meio de
accounte registra valores de indicadores por meio dechart_indicators(séries de indicadores) — ambos lidos dequantcraft.process.runtime.
Globais do runtime
O mecanismo expõe estes itens a partir de quantcraft.process.runtime antes de os callbacks rodarem:
| Nome | Finalidade |
|---|---|
account | Instância de PaperAccount configurada na aba Simulation (saldo + comissão / slippage). Nas execuções forward é uma conta real de corretora com a mesma API que coloca ordens reais — veja Modelo de conta |
chart_indicators | Coletor ChartIndicatorSeries mesclado ao resultado — colete os valores enquanto a execução percorre as barras e emita as séries em on_finish |
qc_fundamentals | Identificador QcFundamentals opcional quando dados fundamentalistas são carregados |
qc_fama_french | QcFamaFrench opcional quando fatores Fama-French globais são carregados; prefira fama_french.qc nos callbacks — veja Fatores Fama-French |
qc_calendar | BacktestCalendar | None — dados de calendário quando Include calendar data está ativado; cobre todos os símbolos negociados. Somente backtests — sempre None em execuções forward. Os callbacks podem receber o mesmo identificador nomeando um argumento calendar — veja Dados de calendário |
qc_ohlcv_by_tf | Buffers opcionais de intervalos secundários indexados pela string do intervalo (avançado; prefira get_bar) |
get_bar(tf, shift=1) | API recomendada para o intervalo principal e os adicionais — retorna um BarSnapshot ou None; importe com import quantcraft.runtime as rt |
Prefira o argumento bars de on_finish(bars, ...) para contas de série completa — não dependa do qc_ohlcv de nível de módulo nas estratégias do IDE (ele costuma ser None no momento da importação, é None em uma execução com alvo de screener e, no restante, abrange todo o intervalo carregado incluindo o aquecimento). Leia os preços do objeto bar nos callbacks. isinstance(bar, dict) é False — OhlcvBarView é um Mapping (o acesso no estilo dict ainda funciona).
Leia-os como atributos de módulo. Cada identificador é sustentado por um valor por sessão, então
bt.account/bt.chart_indicatorssempre se referem a esta execução.from quantcraft.process.runtime import accountvincula um instantâneo — tudo bem no arquivo raiz do algoritmo em um backtest, porque o mecanismo revincula esses nomes depois de carregá-lo (as execuções forward revinculam apenasaccount,qc_fundamentalseqc_fama_french, então usebt.chart_indicatorsert.qc_ohlcv*ali), mas errado em um módulo auxiliar separado, que permanece em cache emsys.modulese manteria o identificador da primeira sessão. Veja Identificadores de módulo vs nomes importados.
Intervalos de tempo secundários
O intervalo principal da execução está sempre disponível por rt.get_bar. Selecione tamanhos de barra extras no menu Additional timeframes da caixa Run backtest, em Symbol & data (Backtests), e leia-os de qualquer callback:
from quantcraft import Timeframe
import quantcraft.runtime as rt
def on_bar(bar_index, bar, fundamentals=None, calendar=None, symbol=None):
daily = rt.get_bar(Timeframe.DAILY, shift=1)
if daily and bar.close[0] > daily.close:
account.open_trade(...)A semântica completa de shift, a enumeração Timeframe e as ressalvas das execuções forward estão em Dados OHLCV e de barras.
Momento da execução das ordens
bar["close"]emon_baré o fechamento da barra de execução atual.account.open_trade/close_traderecebem um preço solicitado e então aplicam os modelos de slippage e comissão da execução. Osentry_price/exit_pricearmazenados são os preços de execução; os preços solicitados são mantidos comoraw_entry_price/raw_exit_price. Com o slippage por participação no volume padrão, as execuções costumam diferir do preço que você passa.- Para mirar o fechamento da barra a partir de
on_bar, passefloat(bar["close"])(ainda sujeito a slippage/comissão). - Para executar durante a simulação intrabarra, chame a partir de
on_tickquandotick_in_bar == 3—priceé o fechamento daquele símbolo para a linha.
on_init — preparar tudo
Roda logo depois que o mecanismo preparou a conta, carregou os dados de mercado e construiu o runtime — mas antes de qualquer barra ser processada.
Use-o para:
- Inicializar variáveis que a estratégia carrega entre barras (contadores, máquinas de estados, valores de parâmetros).
- Configurar indicadores ou buffers de aquecimento.
- Ler parâmetros de entrada para que conduzam o restante da execução.
def on_init(symbols=None):
global lookback, position_size
lookback = 20
position_size = 100Você não tem acesso a uma barra atual em on_init — neste ponto a simulação ainda não começou.
Parâmetro symbols (somente execuções com screener): Quando o backtest usa uma fonte de símbolos Screener, on_init é disparada de novo a cada rebalanceamento com symbols=<lista de tickers> para o novo universo. Isso permite que a estratégia reaja a um novo conjunto de tickers sem reiniciar a simulação.
- No início da execução,
on_initdispara duas vezes em sequência — uma comsymbols=None(a chamada de configuração padrão) e outra comsymbols=<tickers iniciais>. - Os novos disparos não redefinem o estado de nível de módulo; qualquer variável definida na primeira chamada continua lá.
- Estratégias que definem
def on_init():(sem parâmetro) funcionam exatamente como antes — o mecanismo casa os argumentos por nome, então o parâmetro ausente é simplesmente ignorado.
on_bar — uma vez por barra
Roda uma vez por barra de execução, antes dos quatro ticks intrabarra daquela barra. É aqui que a maioria das estratégias toma decisões.
def on_bar(bar_index, bar, fundamentals=None, calendar=None, symbol=None):
if bar.bars_back < 1:
return
if bar.close[0] > bar.close[1]:
account.open_trade(...)Parâmetros:
bar_index— índice, base zero, da barra de execução atual.bar— a visão da barra atual (OhlcvBarView). Comporta-se como um mapeamento somente leitura (bar["open"],bar["high"],bar["low"],bar["close"],bar["volume"],bar["t"]) e também expõe o histórico por deslocamentos:bar.close[0]é o fechamento da barra atual,bar.close[1]é o da barra anterior, e assim por diante.len(bar.close)ebar.bars_backdizem quanto histórico está disponível. Não é umdictsimples (isinstance(bar, dict)éFalse).fundamentals— snapshot de dados fundamentalistas para esta barra, ouNonese não estiver configurado.calendar— identificadorBacktestCalendarquando Include calendar data está ativado; caso contrárioNone(sempreNoneem execuções forward). Veja Dados de calendário para a API completa. Como os argumentos são casados por nome, omitir este parâmetro deixa o seu callback inalterado.symbol— string do ticker em maiúsculas. Útil em estratégias com vários símbolos; pode ser omitido se você carrega apenas um símbolo.
Padrões comuns:
- Decidir entradas/saídas no fechamento da barra: passe
bar.close[0](oufloat(bar["close"])) aaccount.open_trade/account.close_trade. - Calcular indicadores simples lendo deslocamentos como
bar.close[0],bar.close[1], … - Pular as primeiras barras que não têm histórico suficiente (
if bar.bars_back < N: return).
Dica:
on_barsempre roda antes de qualquer callback de tick da mesma barra.
on_tick — atualizações de preço intrabarra e ao vivo
on_tick tem a mesma assinatura no backtest e no forward, mas quando dispara e o que price significa dependem do modo de execução.
Backtest (Test)
Roda até quatro vezes por barra, depois de on_bar, para simular o movimento dentro da barra. O mecanismo percorre o OHLC de cada barra nesta ordem:
tick_in_bar | Significado |
|---|---|
0 | Abertura da barra |
1 | Máxima da barra |
2 | Mínima da barra |
3 | Fechamento da barra |
Não há dados reais de ticks no backtest. As execuções históricas não carregam o histórico de ticks da corretora. As quatro chamadas por barra usam preços sintéticos derivados apenas do OHLC de cada barra. Você pode aproximar execuções intrabarra na simulação, mas o comportamento do
on_tickno backtest não corresponderá aos ticks forward ao vivo. Para decisões no fechamento da barra,on_barbasta —on_tickcomtick_in_bar == 3equivale a agir sobre o fechamento da barra na simulação.
def on_tick(bar_index, tick_in_bar, price, bar, fundamentals=None, calendar=None, symbol=None):
if tick_in_bar == 3:
# Backtest simulation: closing tick fill
account.close_trade(position_id, price)Use o on_tick do backtest quando o momento dentro da barra importar na simulação — por exemplo, para executar no tick de fechamento, simular um stop intrabarra ou avaliar o toque na máxima ou na mínima da barra.
Execuções forward (forward no gráfico, forward em massa / QuantCloud)
Nas execuções forward, on_tick roda toda vez que a plataforma recebe um preço da sua corretora conectada enquanto o seu algoritmo está anexado. Não há um percurso OHLC fixo de quatro ticks — cada chamada reflete uma cotação ao vivo.
def on_tick(bar_index, tick_in_bar, price, bar, fundamentals=None, calendar=None, symbol=None):
# Forward run: react to each broker price update
if price <= stop_level:
account.close_trade(position_id, price)price— a última cotação da corretora para este símbolo.bar— a visão da barra atual para o histórico (mesmo formato do backtest).tick_in_bar— pode continuar presente por compatibilidade de assinatura; nas execuções forward, ramifique porprice, não por fases OHLC sintéticas.
Use o on_tick forward para a lógica intrabarra ao vivo — stops, saídas móveis ou qualquer reação às marcas entre fechamentos de barra.
Parâmetros (ambos os modos)
bar_index— igual ao deon_bar.tick_in_bar— no backtest,0,1,2ou3(passo OHLC sintético). Nas execuções forward, não dependa dele para lógica de fase da barra.price— passo OHLC sintético no backtest; cotação ao vivo da corretora nas execuções forward.bar— a mesma visão de barra deon_bar.fundamentals,calendar,symbol— mesmo significado deon_bar.
Se você só precisa de decisões no fechamento da barra no backtest, pode ignorar
on_tickpor completo e colocar a lógica emon_bar.
on_timer — callback periódico opcional (somente execuções forward)
Backtest do IDE: a caixa Run backtest não define um intervalo de timer. Mesmo que você defina
on_timerno arquivo da estratégia, ele não é chamado durante os backtests do IDE. Coloque a lógica periódica ou baseada em tempo emon_barouon_ticknos backtests.
As execuções forward (teste forward no gráfico, forward em massa / QuantCloud) podem definir timerIntervalMs na caixa de execução. Quando configurado, on_timer roda a um intervalo de tempo fixo entre ticks, somente quando duas condições são atendidas:
- Você configurou
timer_interval_ms/timerIntervalMspara a execução (um inteiro positivo em milissegundos). - O seu arquivo define uma função
on_timer.
def on_timer(bar_index, tick_in_bar, price, bar, fundamentals=None, calendar=None, symbol=None):
passA assinatura tem o mesmo formato de on_tick. Use on_timer para comportamentos baseados em tempo (por exemplo: atualizar um sinal lento a cada minuto, ou agendar uma verificação independente da contagem de barras). Se timer_interval_ms não estiver definido, on_timer nunca é chamado, mesmo que esteja definido.
Dados de calendário nos callbacks
Quando Include calendar data está ativado na aba Symbol & data, qualquer callback pode receber um identificador BacktestCalendar ao adicionar um parâmetro calendar:
def on_bar(bar_index, bar, calendar=None, symbol=None):
if calendar is None: # always guard — not every run enables calendar data
return
days = calendar.days_to_earnings(bar["t"], symbol)
if days is not None and days <= 2:
return # avoid trading into an earnings print
divs = calendar.dividends(bar["t"], symbol)
if divs and (divs[-1].amount or 0) > 0.5: # amount can be None
...O mesmo identificador está disponível em on_tick e on_timer — mas os dados de calendário são somente de backtest, então nas execuções forward calendar é sempre None. Todas as consultas são point-in-time — dividends(bar["t"], symbol) retorna apenas os dividendos que eram públicos naquela barra, nunca os futuros.
Métodos principais:
| Método | Retorna |
|---|---|
dividends(bar_time, symbol=None) | Dividendos públicos naquela barra (do mais antigo ao mais novo) |
splits(bar_time, symbol=None) | Desdobramentos anunciados até aquela barra (do mais antigo ao mais novo) |
next_earnings(bar_time, symbol=None) | {date, basis, estimated, …} ou None |
days_to_earnings(bar_time, symbol=None) | int ou None |
has(symbol=None) | Se este símbolo tem dados de calendário |
As listas vão do mais antigo ao mais novo — [-1] é o evento mais recente. Isso é o oposto dos dados fundamentalistas, em que o índice 0 é o período mais novo.
A global do runtime qc_calendar guarda o mesmo identificador, caso você prefira lê-lo de quantcraft.process.runtime em vez do argumento do callback.
Veja Dados de calendário para os esquemas dos campos de eventos, os detalhes da simulação de eventos corporativos e a solução de problemas.
on_finish — concluir
Roda uma única vez, depois da última barra. Use-o para calcular as saídas finais, montar indicadores de gráfico ou imprimir um resumo.
def on_finish(bars, symbol=None, bars_by_symbol=None):
closes = [b["close"] for b in bars]
bt.chart_indicators.add(...)Parâmetros:
bars— lista cronológica de dicionários OHLC do símbolo de relógio, da mais antiga para a mais nova — a janela executada, as mesmas linhas que o gráfico do Test Results desenha. Cada entrada temt,open,high,low,close,volumee, opcionalmente,fundamentals. É a série canônica de todo o histórico paraon_finish.symbol— ticker em maiúsculas do símbolo de relógio.- Em uma execução Screener não há símbolo de relógio:
barsé[]esymbolé"", então usebars_by_symbol. bars_by_symbol— para execuções com vários símbolos, um dict que mapeia cada ticker para a sua lista cronológica de barras (alinhada ao relógio de execução). Em execuções de um só símbolo você pode ignorá-lo.
Usos comuns:
- Entregar séries de indicadores a
chart_indicators.add(...)para que apareçam nos gráficos de resultados. Prefira os valores que você acumulou por símbolo enquanto a execução percorria as barras; recalcule a partir debarsapenas os indicadores que você não acompanhou por barra — veja Séries de indicadores. - Imprimir ou registrar um resumo final no Painel de saída.
- Atualizar as métricas do portfólio para a carga do resultado.
Todo
returnemon_finishé silencioso. A execução ainda tem sucesso e o Test Results ainda é renderizado — um retorno antecipado só significa que as suas séries ou métricas nunca foram anexadas, sem erro algum. Imprima emsys.stderrem cada guarda enquanto depura; a mensagem aparece no Painel de saída.
Noções de vários símbolos
As estratégias do QuantCraft podem rodar em um símbolo ou em vários ao mesmo tempo. O ciclo de vida continua o mesmo — mudam apenas um pouco as assinaturas dos callbacks e os dados que recebem.
Configurando vários símbolos
Na aba Symbol & data do Run backtest, selecione um ou mais tickers em maiúsculas. Todos os símbolos listados são carregados e simulados juntos.
O símbolo de relógio
Quando você roda vários símbolos, um deles dita o relógio — os carimbos de tempo das suas barras decidem quando os callbacks rodam. A caixa Run backtest não tem controle de símbolo principal. Ordem de resolução:
- Um
SYMBOL = "AAPL"de nível superior no seu arquivo de estratégia, se esse ticker estiver na lista carregada. - Caso contrário, o primeiro símbolo carregado.
Com uma fonte de símbolos Screener não existe símbolo de relógio — o conjunto de símbolos muda ao longo da execução. rt.get_bar sempre lê as barras do símbolo de relógio (e retorna None em uma execução Screener), então, para o histórico de outro ticker, use os deslocamentos bar.close[i] desse ticker.
O símbolo de relógio é também o que on_finish(bars, symbol=...) usa para os argumentos bars e symbol.
Callbacks por símbolo
Para cada barra de execução, o mecanismo percorre os símbolos na ordem da lista:
on_barroda uma vez por ticker carregado no horário dessa barra (antes de qualquer tick dela).- Somente backtest: para cada
tick_in_bar(0= abertura,1= máxima,2= mínima,3= fechamento),on_tickroda uma vez por ticker sobre OHLC sintético (sem dados reais de ticks). - Somente backtest: depois das quatro rodadas de ticks, um
tick(close_map)final da conta marca o patrimônio no fechamento — não um quintoon_tick. - Execuções forward:
on_tickroda a cada atualização de preço da corretora por símbolo, conforme as cotações chegam da sua corretora conectada (sem se limitar a quatro chamadas por barra). Nas fronteiras intradiárias costumam dispararon_ticke depoison_bar— não presuma a ordem do backtest. on_timer(somente em execuções forward comtimerIntervalMsconfigurado — não no backtest do IDE) segue o mesmo padrão por ticker entre os ticks.
Isso significa que um on_bar com vários símbolos é invocado, por exemplo, três vezes por barra se você tiver três símbolos carregados. Use o parâmetro symbol para saber para qual ticker é a chamada:
def on_bar(bar_index, bar, fundamentals=None, calendar=None, symbol=None):
if symbol == "AAPL":
...
elif symbol == "MSFT":
...A visão bar sempre pertence ao símbolo para o qual o callback foi invocado. O mesmo vale para price em on_tick.
Alinhamento "as-of". Para cada horário de barra de execução, cada ticker recebe a sua última barra naquele horário ou antes. Um ticker sem barra naquele horário exato (uma suspensão, um minuto ausente, um calendário de negociação diferente) recebe de novo a sua barra anterior. Se você conta barras ou sinais por ticker, ignore as repetições lembrando o último bar["t"] visto para aquele símbolo:
_last_t = {}
def on_bar(bar_index, bar, fundamentals=None, calendar=None, symbol=None):
if _last_t.get(symbol) == bar["t"]:
return # same bar as last step for this ticker
_last_t[symbol] = bar["t"]
...on_finish com vários símbolos
on_finish ainda roda uma vez, no fim. Para trabalhar com o histórico de todos os símbolos, use bars_by_symbol:
def on_finish(bars, symbol=None, bars_by_symbol=None):
if bars_by_symbol:
for ticker, ticker_bars in bars_by_symbol.items():
...Juntando tudo
Uma estratégia típica de um só símbolo:
- Define parâmetros de entrada ou outro estado em
on_init. - Lê o histórico de preços (OHLCV) por meio de
bar.close[0],bar.close[1], … emon_barpara tomar decisões de negociação e chamaaccount.open_trade/account.close_trade. - Opcionalmente usa
on_tick— no backtest, para execuções intrabarra sintéticas (por exemplo,tick_in_bar == 3para o tick de fechamento); nas execuções forward, para reagir aos preços ao vivo da corretora entre fechamentos de barra. - Opcionalmente usa
on_timernas execuções forward quandotimerIntervalMsestá configurado (indisponível no Run backtest do IDE). - Monta os indicadores de gráfico e finaliza os resultados em
on_finishusandobars.
Uma estratégia com vários símbolos usa os mesmos callbacks, apenas ramifica pelo argumento symbol dentro deles e lê os dados por ticker por meio da bar que recebe — e por meio de bars_by_symbol em on_finish.
Precisa de mais detalhes? Veja Modelo de conta, Dados fundamentalistas, Fatores Fama-French, Dados OHLCV e de barras, Séries de indicadores, ou a aba Documentation integrada na barra lateral direita (layout do espaço de trabalho).

