Documentação1.0 (latest)
PáginasSéries de indicadores · 15 / 29

Séries de indicadores

Quando um backtest termina, a visão Resultados dos testes do IDE renderiza gráficos da sua execução. As séries de indicadores permitem anexar a esses gráficos séries temporais extras que você mesmo calculou — médias móveis, RSI, histogramas MACD, sinais personalizados — para que apareçam ao lado das velas OHLCV, em painéis próprios, ou por símbolo em execuções com vários símbolos.

O QuantCraft não oferece indicadores integrados. Não há API de SMA, MACD ou RSI na plataforma. Você implementa a matemática (ou usa sua própria biblioteca), monta uma lista de linhas {"time": ..., "value": ...} e a entrega ao coletor do runtime. No backtest do IDE, o mecanismo mantém uma ChartIndicatorSeries em runtime.chart_indicators e anexa chart.to_payload() ao resultado como chart_indicator_series. O módulo normaliza os tempos e armazena as séries — ele não calcula os valores dos indicadores por você.

As séries são sempre entregues a partir de on_finish. O melhor é coletar os valores enquanto a execução percorre as barras — veja Início rápido.

O IDE transforma cada entrada em uma sobreposição de linha ou histograma no painel de gráfico correspondente.


Como funciona

  1. Você calcula uma ou mais séries de indicadores (por exemplo, uma SMA a partir dos fechamentos, um RSI pela sua fórmula) como uma lista de linhas {"time": ..., "value": ...}.
  2. Você adiciona cada série ao coletor do runtime com opções de exibição (qual painel, linha ou histograma, nome/cor/símbolo opcionais).
  3. No fim do backtest, o mecanismo mescla essa coleção no resultado em chart_indicator_series.
  4. O gráfico do IDE em Test Results renderiza cada série com as opções que você escolheu.

Leia o coletor como um atributo de módulo, não como um nome importado:

import quantcraft.process.runtime as rt ci = rt.chart_indicators # assigned by the engine before on_init

chart_indicators é uma instância de ChartIndicatorSeries. Tudo o que você adicionar com .add(...) (com pontos que você montou) é anexado ao Test Results por meio do to_payload() do mecanismo. Não construa uma ChartIndicatorSeries() privada para o Test Results do IDE — use o coletor do runtime.

Por que rt.chart_indicators e não from ... import chart_indicators? Os identificadores do runtime são por sessão, então um from ... import vincula um instantâneo. No arquivo raiz do algoritmo isso não é problema — o mecanismo revincula o nome depois de carregar o seu script. Em um módulo auxiliar separado é: os módulos auxiliares ficam em cache em sys.modules, então o nome mantém o coletor da sessão vigente na primeira importação, o que está errado para a próxima execução no mesmo processo (forward / em massa). Veja Identificadores de módulo vs nomes importados.


Início rápido

O padrão recomendado é registrar o valor onde a estratégia o calcula — em on_bar / on_tick — e usar on_finish só para entregar as séries coletadas. Os valores plotados são então exatamente os valores com que a estratégia operou, no relógio de execução do próprio mecanismo, sem recálculo nem realinhamento.

import sys import quantcraft.process.runtime as rt PERIOD = 45 # symbol -> parallel lists, appended in lockstep so indices can never drift _bar_times: dict[str, list] = {} _sma: dict[str, list[float]] = {} def on_bar(bar_index, bar, fundamentals=None, symbol=None): sym = (symbol or "").upper() # `bar.close[i]` returns None past the start of the loaded series. closes = [bar.close[i] for i in range(PERIOD)] # One append per list, per bar — NaN while warming up, never a skip. value = float("nan") if None in closes else sum(closes) / PERIOD _bar_times.setdefault(sym, []).append(bar["t"]) # UTC bar time, verbatim _sma.setdefault(sym, []).append(value) ... # trade on `value` def on_finish(bars, symbol=None, bars_by_symbol=None): ci = rt.chart_indicators if ci is None: return for sym in sorted(_bar_times): times = _bar_times[sym] vals = _sma.get(sym, []) if len(times) != len(vals): # cheap guard against drift print(f"{sym}: {len(times)} times vs {len(vals)} values", file=sys.stderr) continue # NaN warmup rows and unparseable times are dropped by `add`. points = [{"time": t, "value": v} for t, v in zip(times, vals)] ci.add( points, pane="main", render="line", line_style="solid", id=f"sma_{PERIOD}_{sym}", name=f"SMA({PERIOD}) {sym}", color="#ff9800", symbol=sym, )

O gráfico de resultado então mostra a sua SMA(45) como uma sobreposição sólida sobre o gráfico de velas.

Derive id e name da mesma constante em que a estratégia opera (PERIOD acima), nunca de um número fixo — uma legenda desatualizada é indistinguível de um gráfico desatualizado.

Para as regras de caminhos de importação (quantcraft.* em todo lugar; os caminhos antigos ide.* ainda funcionam em scripts existentes), veja Referência.


Mantendo tempos e valores alinhados

add emparelha time e value por posição, então uma divergência desloca a linha inteira em silêncio — ela é renderizada, só que contra as velas erradas.

  • Acrescente o tempo e o valor na mesma instrução, a cada barra. Duas listas paralelas preenchidas em ritmos diferentes e depois combinadas com zip é o bug clássico: zip trunca para a lista mais curta, então uma lacuna de aquecimento de N barras emparelha os primeiros N tempos com valores que pertencem às barras N… — a linha cai N barras para a esquerda, sem erro algum.
  • Enquanto o indicador está aquecendo, acrescente float("nan") em vez de pular. add descarta valores não finitos por você (veja Montando séries), então as linhas de aquecimento somem do gráfico enquanto as duas listas mantêm o mesmo tamanho.
  • Passe o t da barra como está. coerce_chart_time aceita strings ISO, segundos/milissegundos epoch e datetime. Strings sem fuso horário — incluindo datas diárias do QuantCraft B2 como "2024-01-02" — são lidas como UTC. Um objeto datetime ingênuo, porém, é lido como hora local, o que desloca todos os pontos. As linhas de barras trazem um t em UTC; mantenha-o.
  • add já normaliza cada time e descarta valores não finitos, então pré-filtrar com coerce_chart_time / math.isnan no seu próprio laço é opcional.

Todo return em on_finish é silencioso. A execução ainda tem sucesso e o Test Results ainda é renderizado — só que sem as suas séries. Quando uma série não aparece, use print(..., file=sys.stderr) em cada guarda; a mensagem aparece no Painel de saída.


Alternativa — recalcular no fim

Para um indicador que você não acompanhou barra a barra, derive-o em on_finish a partir de bars — a janela executada do símbolo de relógio, da mais antiga para a mais nova, as mesmas linhas que o gráfico do Test Results desenha — ou de bars_by_symbol. Pegue os carimbos de tempo das mesmas linhas que você usou no cálculo, para que uma linha descartada ou filtrada não dessincronize o mapeamento:

import quantcraft.process.runtime as rt PERIOD = 20 def on_finish(bars, symbol=None, bars_by_symbol=None): ci = rt.chart_indicators if ci is None or not bars: return closes = [float(b["close"]) for b in bars] points = [ {"time": bars[i]["t"], "value": sum(closes[i - PERIOD + 1 : i + 1]) / PERIOD} for i in range(PERIOD - 1, len(bars)) ] if not points: return ci.add( points, pane="main", render="line", line_style="dotted", id=f"sma_{PERIOD}", name=f"SMA({PERIOD})", symbol=symbol, )

Não monte séries a partir de runtime.qc_ohlcv: ele é None em uma execução com alvo de screener e, onde está definido, abrange o intervalo completo carregado, incluindo o aquecimento, não a janela executada que o gráfico desenha.


Montando séries — a lista points

Toda série é uma lista de dicts que você montou depois de executar a sua própria lógica de indicador. Cada linha precisa de:

ChaveTipoObservações
timenúmero / datetime / string ISONormalizado para segundos Unix (int). Passe o carimbo de tempo da barra — bar["t"] em um callback, bars[i]["t"] em on_finish — como está.
valuenúmeroDeve ser finito — linhas NaN / inf são descartadas, e é assim que as linhas de aquecimento somem do gráfico.

Outras chaves do dict são ignoradas, e linhas com tempo inválido ou valor não finito são descartadas em silêncio — inclusive tempos numéricos pequenos demais para serem segundos epoch (abaixo de 100.000.000). Uma linha que não seja um dict é um erro: add lança TypeError: points[i] must be a dict with 'time' and 'value' keys.


Opções da série (por chamada de add)

Você as define uma vez por série, ao chamar ci.add(...):

ArgumentoObrigatórioValores
panesim"main" — sobreposição no gráfico de preços/velas. "separate" — painel próprio abaixo do gráfico (bom para RSI, MACD etc.).
rendersim"line" ou "histogram".
line_stylequando render="line""solid" ou "dotted". Obrigatório para linhas — omiti-lo lança ValueError: When render is "line", line_style must be "solid" or "dotted". Omita para histogramas (ignorado).
idnãoIdentificador estável da série; gerado automaticamente se você não fornecer. Útil para manter a mesma cor/ordem entre execuções.
namenãoNome exibido na legenda do gráfico. O padrão é id.
colornãoSugestão de cor (por exemplo "#22c55e").
symbolnãoTicker em maiúsculas (por exemplo "MSFT"). Em backtests com vários símbolos, fixa a série ao gráfico desse símbolo. Omita para o símbolo de relógio / principal. Uma execução Screener não tem símbolo de relógio, então sempre passe symbol ali.
pane_idnãoCom pane="separate", séries com o mesmo pane_id compartilham um subpainel (por exemplo RSI mais linhas de referência 30/70 pontilhadas). Omita para uma série por painel. Ignorado com pane="main".

Onde cada série aparece

Duas configurações juntas decidem onde a série é renderizada:

  • pane controla o posicionamento vertical:
    • "main" — fica sobre as velas do seu gráfico.
    • "separate" — ganha um painel próprio sob as velas.
  • symbol controla em qual gráfico, numa execução com vários símbolos:
    • omitido — anexa-se ao gráfico do símbolo principal / de relógio (igual ao padrão das execuções de um só símbolo).
    • "AAPL" (etc.) — anexa-se somente ao gráfico desse ticker.

Em backtests de um só símbolo você pode ignorar symbol por completo; tudo vai no único gráfico.


Linhas vs. histogramas

São escolhas de exibição para séries que você já calculou. Exemplos do que você pode plotar:

Sobreposições de linha (render="line")

Use para séries contínuas que se alinham aos fechamentos:

  • Médias móveis (SMA, EMA) — geralmente pane="main".
  • Bandas de Bollinger / Keltner — pane="main", várias séries.
  • RSI / Estocástico / ADX — pane="separate" para que a escala de preço não seja esmagada.

Escolha o line_style:

  • "solid" para indicadores principais.
  • "dotted" para séries secundárias / de referência (uma MA lenta ao lado de uma rápida, uma banda superior).
# rsi_points must come from your own RSI calculation rt.chart_indicators.add(rsi_points, pane="separate", render="line", line_style="solid", id="rsi_14", name="RSI(14)")

Histogramas (render="histogram")

Use para séries do estilo barra/coluna (valores positivos e negativos renderizados como barras):

  • Histograma MACD, scores do tipo volume, força do sinal — depois de calculá-los.
rt.chart_indicators.add( [{"time": t, "value": v} for t, v in zip(hist_times, hist_vals)], pane="separate", render="histogram", id="macd_hist", name="MACD Hist", )

line_style não é usado em histogramas — omita-o.


Backtests com vários símbolos

Em execuções com vários símbolos, cada ticker carregado tem seu próprio gráfico no Test Results. Use symbol="..." para enviar uma série a um gráfico específico e chame add uma vez por ticker.

Acumular a cada barra resolve isso de graça. on_bar já roda uma vez por ticker, então indexar os valores coletados por symbol — como no Início rápido — produz uma série por aba sem trabalho extra. on_finish roda apenas uma vez no fim de toda a execução, e o argumento bars é somente o do símbolo de relógio: se você montar indicadores só a partir de bars e passar symbol=symbol, todas as séries caem no gráfico do símbolo de relógio e as outras abas ficam sem séries de indicadores.

Correção para o caminho de recálculo: percorra bars_by_symbol, calcule os pontos a partir das barras de cada símbolo e passe esse ticker em cada add:

def on_finish(bars, symbol=None, bars_by_symbol=None): ci = rt.chart_indicators if ci is None: return sources = bars_by_symbol if bars_by_symbol else {(symbol or "").upper(): bars} for ticker, ticker_bars in sources.items(): sma_points = compute_sma(ticker_bars, window=20) # your function ci.add( sma_points, pane="main", render="line", line_style="solid", id=f"sma_20_{ticker.lower()}", name="SMA(20)", symbol=ticker, )

Séries sem symbol vão no gráfico de relógio / principal. Séries com symbol vão somente no gráfico desse ticker.

Dica: mantenha o mesmo name (por exemplo "SMA(20)") entre os símbolos — o id e o pane_id (quando usado) devem ser únicos por símbolo.


Agrupando séries em um subpainel (pane_id)

Com pane="separate", cada chamada de add(...) normalmente ganha seu próprio subpainel. Passe o mesmo pane_id para empilhar séries relacionadas — por exemplo, um RSI com linhas pontilhadas de sobrecompra/sobrevenda:

import quantcraft.process.runtime as rt RSI_PANE = "rsi" def on_finish(bars, symbol=None, bars_by_symbol=None): ci = rt.chart_indicators if ci is None or not bars: return bar_times = [b["t"] for b in bars] rsi_points = compute_rsi(bars) # your function ci.add( rsi_points, pane="separate", render="line", line_style="solid", id="rsi_14", name="RSI(14)", pane_id=RSI_PANE, ) ci.add( [{"time": t, "value": 70.0} for t in bar_times], pane="separate", render="line", line_style="dotted", id="rsi_ob", name="Overbought", pane_id=RSI_PANE, color="#94a3b8", ) ci.add( [{"time": t, "value": 30.0} for t in bar_times], pane="separate", render="line", line_style="dotted", id="rsi_os", name="Oversold", pane_id=RSI_PANE, color="#94a3b8", )

Em execuções com vários símbolos, use um pane_id por símbolo (por exemplo f"rsi_{ticker}") e um id único por ticker (por exemplo f"rsi_{ticker.lower()}").


Várias séries de uma vez

Você pode adicionar quantas séries quiser — chame add(...) uma vez por série, cada uma com pontos que você calculou:

ci.add(sma20_points, pane="main", render="line", line_style="solid", id="sma_20", name="SMA(20)") ci.add(sma50_points, pane="main", render="line", line_style="dotted", id="sma_50", name="SMA(50)") ci.add(rsi_points, pane="separate", render="line", line_style="solid", id="rsi_14", name="RSI(14)") ci.add(macd_hist, pane="separate", render="histogram", id="macd", name="MACD Hist")

Montando séries fora de um backtest

to_result_dict(key=DEFAULT_RESULT_KEY) e merge_into(result, key=...) são auxiliares para notebooks / executores personalizados que são donos do dict de resultado — eles retornam {key: to_payload()}, ou mesclam isso em result. DEFAULT_RESULT_KEY é "chart_indicator_series". Eles não são usados pelo caminho de backtest do IDE (o mecanismo lê to_payload() de runtime.chart_indicators).

Outros membros: ChartIndicatorSeries(max_entries=None) mantém apenas as N séries mais recentes quando max_entries é definido (as execuções em massa definem isso no coletor compartilhado); add(...) retorna o SeriesEntry armazenado; entries() lista os objetos SeriesEntry adicionados até agora.

from quantcraft.indicator_series import ChartIndicatorSeries # Points come from your own indicator logic, not from this module points = [ {"time": 1_700_000_000, "value": 150.0}, {"time": 1_700_008_640, "value": 151.2}, ] series = ChartIndicatorSeries() series.add( points, pane="main", render="line", line_style="dotted", id="sma_20", name="SMA(20)", ) test_result = {"success": True, "stdout": "..."} test_result = series.merge_into(test_result) # test_result["chart_indicator_series"] -> ready for the chart client

Dentro de um backtest normal do IDE, use apenas rt.chart_indicators e .add(...) — não construa um coletor separado nem faça merge_into em um dict local.


Formato da carga do resultado (para referência)

Cada entrada de chart_indicator_series acaba assim:

CampoDescrição
id, name, pane, render, pointsExatamente o que você passou a add(...).
line_style"solid" / "dotted" para linhas, null para histogramas.
colorPresente apenas se você passou color=.
symbolPresente apenas se você passou symbol= — ticker em maiúsculas para que o IDE o coloque no gráfico certo.
pane_idPresente apenas quando pane="separate" e você passou um pane_id= não vazio.

Cada linha de points é normalizada para apenas {"time": int, "value": float}.


Dicas e armadilhas

  • O QuantCraft plota; você calcula. Use pandas, numpy, TA-Lib ou seus próprios laços — depois passe os points resultantes a ci.add(...).
  • Calcule onde você opera, emita em on_finish. Registrar (hora da barra, valor) por símbolo enquanto a execução percorre as barras plota exatamente aquilo em que a estratégia agiu. Recalcule a partir dos bars de on_finish apenas os indicadores que você não acompanhou por barra.
  • Leia o coletor como rt.chart_indicators. from quantcraft.process.runtime import chart_indicators vincula um instantâneo — seguro no arquivo raiz do algoritmo, errado em um módulo auxiliar separado.
  • Acrescente NaN durante o aquecimento, não pule. Pular linhas em uma lista e não na outra desloca a linha inteira; add descarta as linhas não finitas por você.
  • Use os carimbos de tempo das barras em time, como estão. O t de uma barra de backtest costuma ser uma string ISO (por exemplo "2024-06-10T13:30:00+00:00", ou "2024-01-02" para barras diárias do B2) e está em UTC. Não presuma que int(b["t"]) funciona, e não o reconstrua como um datetime ingênuo — ele seria lido como hora local e deslocaria todos os pontos.
  • Os retornos de on_finish são silenciosos. Sem série e sem erro geralmente significa um return antecipado; imprima em sys.stderr em cada guarda.
  • Não a partir de qc_ohlcv. Ele é None em uma execução com alvo de screener e, fora isso, inclui barras de aquecimento que o gráfico nunca desenha.
  • ids estáveis ajudam no estilo. Reutilizar o mesmo id entre execuções permite ao gráfico manter cores e ordem estáveis, e derivar id / name da mesma constante que a estratégia usa mantém a legenda honesta.
  • Escolha o painel certo. Indicadores limitados (RSI 0–100) pertencem a "separate" — colocá-los em "main" distorcerá a escala de preço.
  • Histograma = sem line_style; linha = line_style obrigatório. Uma linha sem ele lança ValueError.
  • Execuções Screener: sempre passe symbol. Não há símbolo de relógio como alternativa.
14 min de leituraSugerir uma edição ↗
Séries de indicadores — Documentação (1.0 (latest)) | QuantCraft