Documentación1.0 (latest)
PáginasSeries de indicadores · 15 / 29

Series de indicadores

Cuando termina un backtest, la vista Resultados de pruebas del IDE muestra gráficos de tu ejecución. Las series de indicadores te permiten añadir a esos gráficos series temporales adicionales que has calculado tú mismo —medias móviles, RSI, histogramas MACD, señales personalizadas— para que aparezcan junto a las velas OHLCV, en sus propios paneles, o por símbolo en ejecuciones con varios símbolos.

QuantCraft no ofrece indicadores integrados. No hay API de SMA, MACD ni RSI en la plataforma. Tú implementas la matemática (o usas tu propia biblioteca), construyes una lista de filas {"time": ..., "value": ...} y se la entregas al recolector del runtime. En el backtest del IDE, el motor posee una ChartIndicatorSeries en runtime.chart_indicators y adjunta chart.to_payload() al resultado como chart_indicator_series. El módulo normaliza los tiempos y almacena las series; no calcula los valores de los indicadores por ti.

Las series siempre se entregan desde on_finish. Lo mejor es recopilar los valores mientras la ejecución recorre las barras; consulta Inicio rápido.

El IDE convierte cada entrada en una superposición de línea o de histograma en el panel del gráfico correspondiente.


Cómo funciona

  1. Calculas una o más series de indicadores (p. ej. una SMA a partir de los cierres, un RSI con tu fórmula) como una lista de filas {"time": ..., "value": ...}.
  2. Añades cada serie al recolector del runtime con opciones de visualización (qué panel, línea o histograma, nombre/color/símbolo opcionales).
  3. Al final del backtest, el motor fusiona esa colección en el resultado bajo chart_indicator_series.
  4. El gráfico del IDE en Test Results muestra cada serie con las opciones que elegiste.

Lee el recolector como un atributo de módulo, no como un nombre importado:

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

chart_indicators es una instancia de ChartIndicatorSeries. Todo lo que añadas con .add(...) (con puntos que tú construiste) se adjunta a Test Results mediante el to_payload() del motor. No construyas una ChartIndicatorSeries() privada para los Test Results del IDE: usa el recolector del runtime.

¿Por qué rt.chart_indicators y no from ... import chart_indicators? Los manejadores del runtime son por sesión, así que un from ... import enlaza una instantánea. En el archivo raíz del algoritmo no importa: el motor vuelve a enlazar el nombre tras cargar tu script. En un módulo auxiliar dividido sí importa: los módulos auxiliares permanecen en caché en sys.modules, de modo que el nombre conserva el recolector de la sesión que estaba activa en la primera importación, lo que es incorrecto para la siguiente ejecución en el mismo proceso (forward / masiva). Consulta Manejadores de módulo frente a nombres importados.


Inicio rápido

El patrón recomendado es registrar el valor donde la estrategia lo calcula —en on_bar / on_tick— y usar on_finish solo para entregar las series recopiladas. Los valores trazados son entonces exactamente aquellos con los que operó la estrategia, en el reloj de ejecución propio del motor, sin recalcular ni realinear.

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, )

El gráfico de resultados muestra entonces tu SMA(45) como una superposición continua sobre el gráfico de velas.

Deriva id y name de la misma constante con la que opera la estrategia (PERIOD arriba), nunca de un número fijo: una leyenda obsoleta es indistinguible de un trazado obsoleto.

Para las reglas de rutas de importación (quantcraft.* en todas partes; las rutas antiguas ide.* siguen resolviéndose para scripts existentes), consulta Referencia.


Mantener alineados tiempos y valores

add empareja time y value por posición, así que un desajuste desplaza toda la línea en silencio: se dibuja, pero frente a las velas equivocadas.

  • Añade el tiempo y el valor en la misma instrucción, en cada barra. Dos listas paralelas rellenadas a ritmos distintos y luego unidas con zip es el error clásico: zip trunca a la lista más corta, así que un hueco de calentamiento de N barras empareja los primeros N tiempos con valores que pertenecen a las barras N…: la línea queda N barras a la izquierda, sin ningún error.
  • Mientras el indicador se calienta, añade float("nan") en lugar de saltar. add descarta los valores no finitos por ti (consulta Construir series), así que las filas de calentamiento desaparecen del trazado mientras las dos listas siguen teniendo la misma longitud.
  • Pasa el t de la barra tal cual. coerce_chart_time acepta cadenas ISO, segundos/milisegundos epoch y datetime. Las cadenas sin zona horaria —incluidas las fechas diarias de QuantCraft B2 como "2024-01-02"— se leen como UTC. Un objeto datetime ingenuo, en cambio, se lee como hora local, lo que desplaza cada punto. Las filas de barras llevan un t en UTC; consérvalo.
  • add ya normaliza cada time y descarta los valores no finitos, así que prefiltrar con coerce_chart_time / math.isnan en tu propio bucle es opcional.

Cada return en on_finish es silencioso. La ejecución sigue teniendo éxito y Test Results se sigue mostrando, solo que sin tus series. Cuando una serie no aparece, haz print(..., file=sys.stderr) en cada protección; el mensaje aparece en el Panel de salida.


Alternativa: recalcular al final

Para un indicador que no seguiste barra a barra, derívalo en on_finish a partir de bars —la ventana ejecutada del símbolo de reloj, la más antigua primero, las mismas filas que dibuja el gráfico de Test Results— o de bars_by_symbol. Toma las marcas de tiempo de las mismas filas que alimentaste al cálculo, para que una fila descartada o filtrada no pueda desincronizar la correspondencia:

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, )

No construyas series a partir de runtime.qc_ohlcv: es None en una ejecución con objetivo de screener y, donde sí está definido, abarca el rango completo cargado incluido el calentamiento, no la ventana ejecutada que dibuja el gráfico.


Construir series: la lista points

Cada serie es una lista de dicts que construiste tras ejecutar tu propia lógica de indicador. Cada fila necesita:

ClaveTipoNotas
timenúmero / datetime / cadena ISOSe normaliza a segundos Unix (int). Pasa la marca de tiempo de la barra —bar["t"] en un callback, bars[i]["t"] en on_finish— tal cual.
valuenúmeroDebe ser finito: las filas NaN / inf se descartan, que es como desaparecen del trazado las filas de calentamiento.

Las demás claves del dict se ignoran, y las filas con tiempo inválido o valor no finito se descartan en silencio, incluidos los tiempos numéricos demasiado pequeños para ser segundos epoch (por debajo de 100.000.000). Una fila que no sea un dict es un error: add lanza TypeError: points[i] must be a dict with 'time' and 'value' keys.


Opciones de serie (por llamada a add)

Las defines una vez por serie, al llamar a ci.add(...):

ArgumentoObligatorioValores
panesí"main": superposición en el gráfico de precios/velas. "separate": su propio panel bajo el gráfico (bueno para RSI, MACD, etc.).
rendersí"line" o "histogram".
line_stylecon render="line""solid" o "dotted". Obligatorio para líneas: omitirlo lanza ValueError: When render is "line", line_style must be "solid" or "dotted". Omítelo en histogramas (se ignora).
idnoIdentificador estable de la serie; se genera automáticamente si no das uno. Útil para conservar el mismo color/orden entre ejecuciones.
namenoNombre mostrado en la leyenda del gráfico. Por defecto es id.
colornoIndicación de color (p. ej. "#22c55e").
symbolnoTicker en mayúsculas (p. ej. "MSFT"). En backtests con varios símbolos, fija la serie al gráfico de ese símbolo. Omítelo para el símbolo de reloj / principal. Una ejecución Screener no tiene símbolo de reloj, así que pasa siempre symbol allí.
pane_idnoCon pane="separate", las series con el mismo pane_id comparten un subpanel (p. ej. RSI más líneas de referencia 30/70 punteadas). Omítelo para una serie por panel. Se ignora con pane="main".

Dónde aparece cada serie

Dos ajustes deciden juntos dónde se dibuja la serie:

  • pane controla la ubicación vertical:
    • "main": se sitúa sobre las velas de su gráfico.
    • "separate": obtiene su propio panel bajo las velas.
  • symbol controla en qué gráfico de una ejecución con varios símbolos:
    • omitido: se adjunta al gráfico del símbolo principal / de reloj (coincide con el valor por defecto en ejecuciones de un solo símbolo).
    • "AAPL" (etc.): se adjunta solo al gráfico de ese ticker.

En backtests de un solo símbolo puedes ignorar symbol por completo; todo va en el único gráfico.


Líneas frente a histogramas

Son opciones de visualización para series que ya calculaste. Ejemplos de lo que podrías trazar:

Superposiciones de línea (render="line")

Úsalas para series continuas que se alinean con los cierres:

  • Medias móviles (SMA, EMA): normalmente pane="main".
  • Bandas de Bollinger / Keltner: pane="main", varias series.
  • RSI / Estocástico / ADX: pane="separate" para que no se comprima la escala de precios.

Elige line_style:

  • "solid" para indicadores principales.
  • "dotted" para series secundarias / de referencia (una MA lenta junto a una rápida, una 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")

Úsalos para series de estilo barra/columna (valores positivos y negativos dibujados como barras):

  • Histograma MACD, puntuaciones tipo volumen, fuerza de la señal, una vez calculados.
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 no se usa en histogramas: omítelo.


Backtests con varios símbolos

En ejecuciones con varios símbolos, cada ticker cargado tiene su propio gráfico en Test Results. Usa symbol="..." para enviar una serie a un gráfico concreto y llama a add una vez por ticker.

Acumular por barra lo resuelve gratis. on_bar ya se ejecuta una vez por ticker, así que indexar los valores recopilados por symbol —como en Inicio rápido— produce una serie por pestaña sin trabajo extra. on_finish se ejecuta una sola vez al final de toda la ejecución, y su argumento bars es solo el del símbolo de reloj: si construyes indicadores solo con bars y pasas symbol=symbol, todas las series caen en el gráfico del símbolo de reloj y las demás pestañas no tienen series de indicadores.

Solución para la vía de recálculo: recorre bars_by_symbol, calcula los puntos a partir de las barras de cada símbolo y pasa ese ticker en 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, )

Las series sin symbol van en el gráfico de reloj / principal. Las series con symbol van solo en el gráfico de ese ticker.

Consejo: conserva el mismo name (p. ej. "SMA(20)") en todos los símbolos; el id y el pane_id (cuando se use) deben ser únicos por símbolo.


Agrupar series en un subpanel (pane_id)

Con pane="separate", cada llamada a add(...) obtiene normalmente su propio subpanel. Pasa el mismo pane_id para apilar series relacionadas, por ejemplo un RSI con líneas punteadas de sobrecompra/sobreventa:

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", )

En ejecuciones con varios símbolos, usa un pane_id por símbolo (p. ej. f"rsi_{ticker}") y un id único por ticker (p. ej. f"rsi_{ticker.lower()}").


Varias series a la vez

Puedes añadir tantas series como quieras: llama a add(...) una vez por serie, cada una con puntos que tú calculaste:

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")

Construir series fuera de un backtest

to_result_dict(key=DEFAULT_RESULT_KEY) y merge_into(result, key=...) son ayudantes para notebooks / ejecutores personalizados que poseen el dict de resultado: devuelven {key: to_payload()}, o fusionan eso en result. DEFAULT_RESULT_KEY es "chart_indicator_series". No los usa la ruta de backtest del IDE (el motor lee to_payload() de runtime.chart_indicators).

Otros miembros: ChartIndicatorSeries(max_entries=None) conserva solo las N series más recientes cuando se define max_entries (las ejecuciones masivas lo definen en el recolector compartido); add(...) devuelve el SeriesEntry almacenado; entries() lista los objetos SeriesEntry añadidos hasta ahora.

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 un backtest normal del IDE, usa solo rt.chart_indicators y .add(...): no construyas un recolector aparte ni hagas merge_into sobre un dict local.


Forma de la carga útil del resultado (como referencia)

Cada entrada de chart_indicator_series termina así:

CampoDescripción
id, name, pane, render, pointsExactamente lo que pasaste a add(...).
line_style"solid" / "dotted" para líneas, null para histogramas.
colorPresente solo si pasaste color=.
symbolPresente solo si pasaste symbol=: ticker en mayúsculas para que el IDE pueda situarlo en el gráfico correcto.
pane_idPresente solo cuando pane="separate" y pasaste un pane_id= no vacío.

Cada fila de points se normaliza a solo {"time": int, "value": float}.


Consejos y trampas

  • QuantCraft traza; tú calculas. Usa pandas, numpy, TA-Lib o tus propios bucles, y luego pasa los points resultantes a ci.add(...).
  • Calcula donde operas, emite en on_finish. Registrar (hora de barra, valor) por símbolo mientras la ejecución recorre las barras traza exactamente aquello sobre lo que actuó la estrategia. Recalcula a partir de los bars de on_finish solo los indicadores que no seguiste por barra.
  • Lee el recolector como rt.chart_indicators. from quantcraft.process.runtime import chart_indicators enlaza una instantánea: seguro en el archivo raíz del algoritmo, incorrecto en un módulo auxiliar dividido.
  • Añade NaN durante el calentamiento, no saltes. Saltar filas en una lista y no en la otra desplaza toda la línea; add descarta las filas no finitas por ti.
  • Usa las marcas de tiempo de las barras para time, tal cual. El t de una barra de backtest suele ser una cadena ISO (p. ej. "2024-06-10T13:30:00+00:00", o "2024-01-02" para barras diarias B2) y está en UTC. No supongas que int(b["t"]) funciona, y no lo reconstruyas como un datetime ingenuo: se leería como hora local y desplazaría cada punto.
  • Los return de on_finish son silenciosos. Sin series y sin error suele significar un return temprano; imprime en sys.stderr en cada protección.
  • No desde qc_ohlcv. Es None en una ejecución con objetivo de screener y, si no, incluye barras de calentamiento que el gráfico nunca dibuja.
  • Los id estables ayudan al estilo. Reutilizar el mismo id entre ejecuciones permite al gráfico mantener estables colores y orden, y derivar id / name de la misma constante que usa la estrategia mantiene honesta la leyenda.
  • Elige el panel correcto. Los indicadores acotados (RSI 0–100) pertenecen a "separate": ponerlos en "main" deformará la escala de precios.
  • Histograma = sin line_style; línea = line_style obligatorio. Una línea sin él lanza ValueError.
  • Ejecuciones Screener: pasa siempre symbol. No hay símbolo de reloj al que recurrir.
14 min de lecturaSugerir una edición ↗
Series de indicadores — Documentación (1.0 (latest)) | QuantCraft