Documentación1.0 (latest)
PáginasCiclo de vida de la estrategia · 8 / 29

Ciclo de vida de la estrategia

Una estrategia de QuantCraft es un archivo Python normal que define un pequeño conjunto de funciones de callback. El motor de backtest llama a esas funciones en un orden fijo mientras recorre los datos históricos. No las llamas tú: lo hace el motor. Usa Test para ejecutarlas (ejecutar código).

El ciclo de vida, de principio a fin:

on_init → on_bar → on_tick (×4 per bar) → account tick(close) → on_finish └─────────── repeats for every bar ───────────┘

En backtest, on_tick se dispara cuatro veces por barra sobre un OHLC sintético de cada barra (sin historial real de ticks). Tras esos cuatro ticks, el motor hace una última valoración a mercado de la cuenta al cierre; no es un quinto on_tick. on_timer solo está disponible en las ejecuciones forward (forward sobre el gráfico, forward masivo / QuantCloud) cuando se define timerIntervalMs; el cuadro Run backtest del IDE no tiene control de temporizador. En las ejecuciones forward, on_tick se ejecuta con cada actualización de precio del bróker conectado; en los límites intradía suelen dispararse on_tick y luego on_bar: no supongas el orden del backtest.

Esta página cubre cada callback, cómo se integran las estrategias con varios símbolos y los fundamentos necesarios para escribir tu primera estrategia.


El archivo de la estrategia

Una estrategia mínima tiene este aspecto:

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): pass

Algunas reglas se aplican a todos los callbacks:

  • Deben ser funciones de nivel superior en tu archivo (no anidadas ni dentro de una clase).
  • El motor de backtest exige on_init, on_bar, on_tick y on_finish invocables (de lo contrario la ejecución falla con User script must define callable: …). Los botones Test, Compile y Debug Test del IDE aparecen cuando la pestaña .py activa define on_init / on_bar / on_finish; la falta de on_tick sigue haciendo fallar el backtest al iniciarse. on_timer es opcional y solo se usa en ejecuciones forward con un intervalo de temporizador.
  • Añade solo los argumentos opcionales que uses. calendar, symbols (en on_init) y los argumentos de on_finish se emparejan por nombre; fundamentals y symbol se emparejan probando por turno las formas habituales, así que mantenlos en el orden documentado. La firma completa más segura es def on_bar(bar_index, bar, fundamentals=None, calendar=None, symbol=None). Un calendar declarado siempre recibe el manejador del calendario o None, nunca el objeto de datos fundamentales.
  • Dentro de los callbacks colocas órdenes mediante account y registras valores de indicadores mediante chart_indicators (series de indicadores); ambos se leen de quantcraft.process.runtime.

Globales del runtime

El motor expone estos elementos desde quantcraft.process.runtime antes de que se ejecuten los callbacks:

NombrePropósito
accountInstancia de PaperAccount configurada desde la pestaña Simulation (saldo + comisión / slippage). En las ejecuciones forward es una cuenta de bróker real con la misma API que coloca órdenes reales; consulta Modelo de cuenta
chart_indicatorsRecolector ChartIndicatorSeries que se fusiona en el resultado: recopila los valores mientras la ejecución recorre las barras y emite las series en on_finish
qc_fundamentalsManejador QcFundamentals opcional cuando se cargan datos fundamentales
qc_fama_frenchQcFamaFrench opcional cuando se cargan los factores Fama-French globales; prefiere fama_french.qc en los callbacks; consulta Factores Fama-French
qc_calendarBacktestCalendar | None: datos de calendario cuando Include calendar data está activado; cubre todos los símbolos operados. Solo backtests: siempre None en ejecuciones forward. Los callbacks pueden recibir el mismo manejador nombrando un argumento calendar; consulta Datos de calendario
qc_ohlcv_by_tfBúferes opcionales de marcos temporales secundarios indexados por cadena de marco (avanzado; prefiere get_bar)
get_bar(tf, shift=1)API recomendada para el marco principal y los adicionales: devuelve un BarSnapshot o None; se importa con import quantcraft.runtime as rt

Prefiere el argumento bars de on_finish(bars, ...) para cálculos de serie completa; no te apoyes en el qc_ohlcv a nivel de módulo en las estrategias del IDE (a menudo es None en el momento de la importación, es None en una ejecución con objetivo de screener y, en el resto de casos, abarca todo el rango cargado incluido el calentamiento). Lee los precios del objeto bar en los callbacks. isinstance(bar, dict) es False: OhlcvBarView es un Mapping (el acceso tipo dict sigue funcionando).

Léelos como atributos de módulo. Cada manejador se apoya en un valor por sesión, así que bt.account / bt.chart_indicators siempre se refieren a esta ejecución. from quantcraft.process.runtime import account enlaza una instantánea: está bien en el archivo raíz del algoritmo en un backtest, porque el motor vuelve a enlazar esos nombres tras cargarlo (las ejecuciones forward solo vuelven a enlazar account, qc_fundamentals y qc_fama_french, así que usa allí bt.chart_indicators y rt.qc_ohlcv*), pero es incorrecto en un módulo auxiliar dividido, que permanece en caché en sys.modules y conservaría el manejador de la primera sesión. Consulta Manejadores de módulo frente a nombres importados.

Marcos temporales secundarios

El marco temporal principal de la ejecución siempre está disponible mediante rt.get_bar. Selecciona tamaños de barra adicionales en el desplegable Additional timeframes del cuadro Run backtest, en Symbol & data (Backtests), y luego léelos desde cualquier 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(...)

La semántica completa de shift, la enumeración Timeframe y las salvedades de las ejecuciones forward están en Datos OHLCV y de barras.

Momento de ejecución de las órdenes

  • bar["close"] en on_bar es el cierre de la barra de ejecución actual.
  • account.open_trade / close_trade toman un precio solicitado y luego aplican los modelos de slippage y comisión de la ejecución. Los entry_price / exit_price almacenados son los precios de ejecución; los precios solicitados se conservan como raw_entry_price / raw_exit_price. Con el slippage por participación en el volumen predeterminado, las ejecuciones suelen diferir del precio que pasas.
  • Para apuntar al cierre de la barra desde on_bar, pasa float(bar["close"]) (sigue sujeto a slippage/comisión).
  • Para ejecutar durante la simulación intrabarra, llama desde on_tick cuando tick_in_bar == 3: price es el cierre de ese símbolo para la fila.

on_init — preparar todo

Se ejecuta justo después de que el motor ha preparado la cuenta, cargado los datos de mercado y construido el runtime, pero antes de procesar ninguna barra.

Úsala para:

  • Inicializar las variables que tu estrategia arrastra entre barras (contadores, máquinas de estados, valores de parámetros).
  • Configurar indicadores o búferes de calentamiento.
  • Leer parámetros de entrada para que gobiernen el resto de la ejecución.
def on_init(symbols=None): global lookback, position_size lookback = 20 position_size = 100

En on_init no tienes acceso a una barra actual: en este punto la simulación aún no ha empezado.

Parámetro symbols (solo ejecuciones con screener): Cuando el backtest usa una fuente de símbolos Screener, on_init se vuelve a disparar en cada reequilibrio con symbols=<lista de tickers> para el nuevo universo. Esto permite que tu estrategia reaccione a un nuevo conjunto de tickers sin reiniciar la simulación.

  • Al empezar la ejecución, on_init se dispara dos veces seguidas: una con symbols=None (la llamada de configuración estándar) y otra con symbols=<tickers iniciales>.
  • Los nuevos disparos no restablecen el estado a nivel de módulo; las variables que definiste en la primera llamada siguen ahí.
  • Las estrategias que definen def on_init(): (sin parámetro) funcionan exactamente como antes: el motor empareja los argumentos por nombre, así que el parámetro ausente simplemente se ignora.

on_bar — una vez por barra

Se ejecuta una vez por barra de ejecución, antes de los cuatro ticks intrabarra de esa barra. Aquí es donde la mayoría de las estrategias toman decisiones.

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 cero, de la barra de ejecución actual.
  • bar: la vista de la barra actual (OhlcvBarView). Se comporta como un mapeo de solo lectura (bar["open"], bar["high"], bar["low"], bar["close"], bar["volume"], bar["t"]) y también expone el historial mediante desplazamientos: bar.close[0] es el cierre de la barra actual, bar.close[1] el de la barra anterior, y así sucesivamente. len(bar.close) y bar.bars_back indican cuánto historial hay disponible. No es un dict simple (isinstance(bar, dict) es False).
  • fundamentals: instantánea de datos fundamentales para esta barra, o None si no está configurado.
  • calendar: manejador BacktestCalendar cuando Include calendar data está activado; si no, None (siempre None en ejecuciones forward). Consulta Datos de calendario para la API completa. Como los argumentos se emparejan por nombre, omitir este parámetro deja tu callback sin cambios.
  • symbol: cadena del ticker en mayúsculas. Útil en estrategias con varios símbolos; puedes omitirlo si solo cargas un símbolo.

Patrones habituales:

  • Decidir entradas/salidas al cierre de la barra: pasa bar.close[0] (o float(bar["close"])) a account.open_trade / account.close_trade.
  • Calcular indicadores sencillos leyendo desplazamientos como bar.close[0], bar.close[1], …
  • Saltar las primeras barras que no tienen suficiente historial (if bar.bars_back < N: return).

Consejo: on_bar siempre se ejecuta antes de cualquier callback de tick de la misma barra.


on_tick — actualizaciones de precio intrabarra y en vivo

on_tick tiene la misma firma en backtest y en forward, pero cuándo se dispara y qué significa price dependen del modo de ejecución.

Backtest (Test)

Se ejecuta hasta cuatro veces por barra, después de on_bar, para simular el movimiento dentro de la barra. El motor recorre el OHLC de cada barra en este orden:

tick_in_barSignificado
0Apertura de la barra
1Máximo de la barra
2Mínimo de la barra
3Cierre de la barra

No hay datos reales de ticks en backtest. Las ejecuciones históricas no cargan el historial de ticks del bróker. Las cuatro llamadas por barra usan precios sintéticos derivados únicamente del OHLC de cada barra. Puedes aproximar ejecuciones intrabarra en simulación, pero el comportamiento de on_tick en backtest no coincidirá con los ticks forward en vivo. Para decisiones al cierre de la barra basta con on_bar: on_tick con tick_in_bar == 3 equivale a actuar sobre el cierre de la barra en simulación.

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)

Usa on_tick en backtest cuando el momento dentro de la barra importe en simulación; por ejemplo, para ejecutar en el tick de cierre, simular un stop intrabarra o evaluar el toque del máximo o el mínimo de la barra.

Ejecuciones forward (forward sobre el gráfico, forward masivo / QuantCloud)

En las ejecuciones forward, on_tick se ejecuta cada vez que la plataforma recibe un precio de tu bróker conectado mientras tu algoritmo está adjunto. No hay un recorrido OHLC fijo de cuatro ticks: cada llamada refleja una cotización en 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: la última cotización del bróker para este símbolo.
  • bar: la vista de la barra actual para el historial (misma forma que en backtest).
  • tick_in_bar: puede seguir presente por compatibilidad de firma; en las ejecuciones forward, ramifica según price, no según fases OHLC sintéticas.

Usa on_tick en forward para la lógica intrabarra en vivo: stops, salidas dinámicas o cualquier reacción a las marcas entre cierres de barra.

Parámetros (ambos modos)

  • bar_index: igual que en on_bar.
  • tick_in_bar: en backtest, 0, 1, 2 o 3 (paso OHLC sintético). En las ejecuciones forward, no cuentes con él para lógica de fase de barra.
  • price: paso OHLC sintético en backtest; cotización en vivo del bróker en ejecuciones forward.
  • bar: la misma vista de barra que en on_bar.
  • fundamentals, calendar, symbol: mismo significado que en on_bar.

Si solo necesitas decisiones al cierre de la barra en backtest, puedes ignorar on_tick por completo y poner tu lógica en on_bar.


on_timer — callback periódico opcional (solo ejecuciones forward)

Backtest del IDE: el cuadro Run backtest no define un intervalo de temporizador. Aunque definas on_timer en tu archivo de estrategia, no se llama durante los backtests del IDE. Pon la lógica periódica o basada en tiempo en on_bar o on_tick para los backtests.

Las ejecuciones forward (prueba forward sobre el gráfico, forward masivo / QuantCloud) pueden definir timerIntervalMs en su cuadro de ejecución. Cuando está configurado, on_timer se ejecuta a un intervalo de tiempo fijo entre ticks, solo si se cumplen dos condiciones:

  1. Has configurado timer_interval_ms / timerIntervalMs para la ejecución (un entero positivo en milisegundos).
  2. Tu archivo define una función on_timer.
def on_timer(bar_index, tick_in_bar, price, bar, fundamentals=None, calendar=None, symbol=None): pass

La firma tiene la misma forma que on_tick. Usa on_timer para comportamientos basados en tiempo (por ejemplo: refrescar una señal lenta cada minuto, o programar una comprobación independiente del número de barras). Si timer_interval_ms no está definido, on_timer nunca se llama aunque esté definida.


Datos de calendario en los callbacks

Cuando Include calendar data está activado en la pestaña Symbol & data, cualquier callback puede recibir un manejador BacktestCalendar añadiendo un 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 ...

El mismo manejador está disponible en on_tick y on_timer, pero los datos de calendario son solo de backtest, así que en las ejecuciones forward calendar es siempre None. Todas las consultas son point-in-time: dividends(bar["t"], symbol) devuelve solo los dividendos que eran públicos en esa barra, nunca los futuros.

Métodos principales:

MétodoDevuelve
dividends(bar_time, symbol=None)Dividendos públicos en esa barra (de más antiguo a más reciente)
splits(bar_time, symbol=None)Splits anunciados hasta esa barra (de más antiguo a más reciente)
next_earnings(bar_time, symbol=None){date, basis, estimated, …} o None
days_to_earnings(bar_time, symbol=None)int o None
has(symbol=None)Si este símbolo tiene datos de calendario

Las listas van de la más antigua a la más reciente: [-1] es el evento más reciente. Es lo opuesto de los datos fundamentales, donde el índice 0 es el periodo más reciente.

La global del runtime qc_calendar contiene el mismo manejador si prefieres leerlo de quantcraft.process.runtime en lugar de usar el argumento del callback.

Consulta Datos de calendario para los esquemas de campos de eventos, los detalles de simulación de acciones corporativas y la resolución de problemas.


on_finish — cerrar

Se ejecuta una sola vez, después de la última barra. Úsala para calcular las salidas finales, construir indicadores de gráfico o imprimir un resumen.

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 diccionarios OHLC del símbolo de reloj, la más antigua primero: la ventana ejecutada, las mismas filas que dibuja el gráfico de Test Results. Cada entrada tiene t, open, high, low, close, volume y, opcionalmente, fundamentals. Es la serie canónica de todo el historial para on_finish.
  • symbol: ticker en mayúsculas del símbolo de reloj.
  • En una ejecución Screener no hay símbolo de reloj: bars es [] y symbol es "", así que usa bars_by_symbol.
  • bars_by_symbol: para ejecuciones con varios símbolos, un dict que asigna a cada ticker su lista cronológica de barras (alineada con el reloj de ejecución). En ejecuciones de un solo símbolo puedes ignorarlo.

Usos habituales:

  • Entregar series de indicadores a chart_indicators.add(...) para que aparezcan en los gráficos de resultados. Prefiere los valores que acumulaste por símbolo mientras la ejecución recorría las barras; recalcula a partir de bars solo los indicadores que no seguiste por barra; consulta Series de indicadores.
  • Imprimir o registrar un resumen final en el Panel de salida.
  • Actualizar las métricas de la cartera para la carga del resultado.

Cada return en on_finish es silencioso. La ejecución sigue teniendo éxito y Test Results se sigue mostrando; un retorno anticipado solo significa que tus series o métricas nunca se adjuntaron, sin ningún error. Imprime en sys.stderr en cada protección mientras depuras; el mensaje aparece en el Panel de salida.


Fundamentos con varios símbolos

Las estrategias de QuantCraft pueden ejecutarse sobre un símbolo o varios a la vez. El ciclo de vida es el mismo; solo cambian ligeramente las firmas de los callbacks y los datos que reciben.

Configurar varios símbolos

En la pestaña Symbol & data de Run backtest, selecciona uno o más tickers en mayúsculas. Todos los símbolos listados se cargan y se simulan juntos.

El símbolo de reloj

Cuando ejecutas varios símbolos, uno de ellos marca el reloj: sus marcas de tiempo de barra deciden cuándo se ejecutan los callbacks. El cuadro Run backtest no tiene control de símbolo principal. Orden de resolución:

  1. Un SYMBOL = "AAPL" de nivel superior en tu archivo de estrategia, si ese ticker está en la lista cargada.
  2. Si no, el primer símbolo cargado.

Con una fuente de símbolos Screener no hay símbolo de reloj: el conjunto de símbolos cambia a lo largo de la ejecución. rt.get_bar siempre lee las barras del símbolo de reloj (y devuelve None en una ejecución Screener), así que para el historial de otro ticker usa los desplazamientos bar.close[i] de ese ticker.

El símbolo de reloj es también el que on_finish(bars, symbol=...) usa para sus argumentos bars y symbol.

Callbacks por símbolo

Para cada barra de ejecución, el motor recorre los símbolos en el orden de la lista:

  1. on_bar se ejecuta una vez por ticker cargado a la hora de esa barra (antes de cualquier tick de esa barra).
  2. Solo backtest: para cada tick_in_bar (0 = apertura, 1 = máximo, 2 = mínimo, 3 = cierre), on_tick se ejecuta una vez por ticker sobre OHLC sintético (sin datos reales de ticks).
  3. Solo backtest: tras las cuatro rondas de ticks, un tick(close_map) final de la cuenta valora el capital al cierre; no es un quinto on_tick.
  4. Ejecuciones forward: on_tick se ejecuta con cada actualización de precio del bróker por símbolo a medida que llegan las cotizaciones de tu bróker conectado (sin limitarse a cuatro llamadas por barra). En los límites intradía suelen dispararse on_tick y luego on_bar: no supongas el orden del backtest.
  5. on_timer (solo en ejecuciones forward con timerIntervalMs configurado; no en el backtest del IDE) sigue el mismo patrón por ticker entre ticks.

Eso significa que un on_bar con varios símbolos se invoca, por ejemplo, tres veces por barra si tienes tres símbolos cargados. Usa el parámetro symbol para saber para qué ticker es la llamada:

def on_bar(bar_index, bar, fundamentals=None, calendar=None, symbol=None): if symbol == "AAPL": ... elif symbol == "MSFT": ...

La vista bar siempre pertenece al símbolo para el que se invocó el callback. Lo mismo vale para price en on_tick.

Alineación "as-of". Para cada hora de barra de ejecución, cada ticker recibe su última barra en esa hora o antes. Un ticker sin barra exactamente a esa hora (una parada de cotización, un minuto ausente, un calendario de negociación distinto) recibe de nuevo su barra anterior. Si cuentas barras o señales por ticker, omite las repeticiones recordando el último bar["t"] que viste para ese 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 con varios símbolos

on_finish sigue ejecutándose una vez al final. Para trabajar con el historial de todos los símbolos, usa 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(): ...

Todo junto

Una estrategia típica de un solo símbolo:

  1. Define parámetros de entrada u otro estado en on_init.
  2. Lee el historial de precios (OHLCV) mediante bar.close[0], bar.close[1], … en on_bar para tomar decisiones de trading, y llama a account.open_trade / account.close_trade.
  3. Opcionalmente usa on_tick: en backtest, para ejecuciones intrabarra sintéticas (p. ej. tick_in_bar == 3 para el tick de cierre); en ejecuciones forward, para reaccionar a los precios en vivo del bróker entre cierres de barra.
  4. Opcionalmente usa on_timer en ejecuciones forward cuando timerIntervalMs está configurado (no disponible en Run backtest del IDE).
  5. Construye los indicadores de gráfico y finaliza los resultados en on_finish usando bars.

Una estrategia con varios símbolos usa los mismos callbacks, solo que ramifica según el argumento symbol dentro de ellos y lee los datos por ticker mediante la bar que recibe, y mediante bars_by_symbol en on_finish.

¿Necesitas más detalle? Consulta Modelo de cuenta, Datos fundamentales, Factores Fama-French, Datos OHLCV y de barras, Series de indicadores, o la pestaña Documentation integrada en la barra lateral derecha (disposición del espacio de trabajo).

18 min de lecturaSugerir una edición ↗
Ciclo de vida de la estrategia — Documentación (1.0 (latest)) | QuantCraft