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
- 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": ...}. - 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).
- No fim do backtest, o mecanismo mescla essa coleção no resultado em
chart_indicator_series. - 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_initchart_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_indicatorse nãofrom ... import chart_indicators? Os identificadores do runtime são por sessão, então umfrom ... importvincula 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 emsys.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:ziptrunca 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.adddescarta 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
tda barra como está.coerce_chart_timeaceita strings ISO, segundos/milissegundos epoch edatetime. Strings sem fuso horário — incluindo datas diárias do QuantCraft B2 como"2024-01-02"— são lidas como UTC. Um objetodatetimeingênuo, porém, é lido como hora local, o que desloca todos os pontos. As linhas de barras trazem umtem UTC; mantenha-o. addjá normaliza cadatimee descarta valores não finitos, então pré-filtrar comcoerce_chart_time/math.isnanno seu próprio laço é opcional.
Todo
returnemon_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, useprint(..., 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:
| Chave | Tipo | Observações |
|---|---|---|
time | número / datetime / string ISO | Normalizado para segundos Unix (int). Passe o carimbo de tempo da barra — bar["t"] em um callback, bars[i]["t"] em on_finish — como está. |
value | número | Deve 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(...):
| Argumento | Obrigatório | Valores |
|---|---|---|
pane | sim | "main" — sobreposição no gráfico de preços/velas. "separate" — painel próprio abaixo do gráfico (bom para RSI, MACD etc.). |
render | sim | "line" ou "histogram". |
line_style | quando 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). |
id | não | Identificador estável da série; gerado automaticamente se você não fornecer. Útil para manter a mesma cor/ordem entre execuções. |
name | não | Nome exibido na legenda do gráfico. O padrão é id. |
color | não | Sugestão de cor (por exemplo "#22c55e"). |
symbol | não | Ticker 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_id | não | Com 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:
panecontrola o posicionamento vertical:"main"— fica sobre as velas do seu gráfico."separate"— ganha um painel próprio sob as velas.
symbolcontrola 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 — oide opane_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 clientDentro 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:
| Campo | Descrição |
|---|---|
id, name, pane, render, points | Exatamente o que você passou a add(...). |
line_style | "solid" / "dotted" para linhas, null para histogramas. |
color | Presente apenas se você passou color=. |
symbol | Presente apenas se você passou symbol= — ticker em maiúsculas para que o IDE o coloque no gráfico certo. |
pane_id | Presente 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
pointsresultantes aci.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 dosbarsdeon_finishapenas os indicadores que você não acompanhou por barra. - Leia o coletor como
rt.chart_indicators.from quantcraft.process.runtime import chart_indicatorsvincula um instantâneo — seguro no arquivo raiz do algoritmo, errado em um módulo auxiliar separado. - Acrescente
NaNdurante o aquecimento, não pule. Pular linhas em uma lista e não na outra desloca a linha inteira;adddescarta as linhas não finitas por você. - Use os carimbos de tempo das barras em
time, como estão. Otde 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 queint(b["t"])funciona, e não o reconstrua como umdatetimeingênuo — ele seria lido como hora local e deslocaria todos os pontos. - Os retornos de
on_finishsão silenciosos. Sem série e sem erro geralmente significa umreturnantecipado; imprima emsys.stderrem cada guarda. - Não a partir de
qc_ohlcv. Ele éNoneem 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 mesmoidentre execuções permite ao gráfico manter cores e ordem estáveis, e derivarid/nameda 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_styleobrigatório. Uma linha sem ele lançaValueError. - Execuções Screener: sempre passe
symbol. Não há símbolo de relógio como alternativa.

