Documentación1.0 (latest)
PáginasModelo de cuenta · 9 / 29

Modelo de cuenta

Cada estrategia de QuantCraft se ejecuta sobre una cuenta de trading en papel integrada. La cuenta registra tu efectivo, mantiene tus posiciones abiertas, contabiliza la ganancia y la pérdida realizadas cuando cierras operaciones y calcula las métricas de rendimiento al final de un backtest.

En el uso normal no la construyes tú: el motor de backtest la crea a partir de la pestaña Simulation del cuadro Run backtest (saldo inicial, comisión / slippage) y la asigna a quantcraft.process.runtime.account antes de que se ejecute on_init(). Un SL/TP predeterminado opcional y el dimensionamiento basado en riesgo pueden conectarse mediante la configuración / claves .qcs cuando están presentes; el cuadro no expone actualmente entradas de SL/TP / riesgo. Los callbacks reciben los datos de mercado como se describe en Ciclo de vida de la estrategia y Datos OHLCV y de barras; las métricas aparecen en Resultados de pruebas.


Inicio rápido

import quantcraft.process.runtime as rt def on_init(): pass def on_bar(bar_index, bar, fundamentals=None, symbol=None): account = rt.account px = float(bar["close"]) snap = account.open_trade("AAPL", "long", 10, px) if snap["open_positions"]: pid = snap["open_positions"][0]["id"] account.close_trade(pid, px + 1.0) def on_finish(bars, symbol=None, bars_by_symbol=None): last_px = float(bars[-1]["close"]) metrics = rt.account.refresh_metrics({"AAPL": last_px})

Ese es el patrón completo: abrir una operación, guardar el id de la posición, cerrarla más tarde y (opcionalmente) actualizar las métricas al final.

Qué lees de la instancia: account.cash, account.balance, account.realized_pnl, account.closed_trades y account.open_positions_payload(). No existe el atributo account.open_positions: las posiciones abiertas salen del snapshot (snap["open_positions"]) o de open_positions_payload().

Las ejecuciones forward colocan órdenes reales. En una prueba forward sobre el gráfico o en una ejecución masiva, runtime.account es una cuenta real de bróker: una subclase de PaperAccount con la misma API que envía órdenes reales a tu cuenta de bróker conectada (papel o real), incluidas órdenes bracket de SL/TP para acciones. Trata open_trade / close_trade allí como operaciones reales. Operar conlleva riesgo de pérdida.


Qué hace PaperAccount

La cuenta simula una cuenta de trading al contado sencilla:

  • Registra el saldo inicial, el efectivo, el saldo y el capital (equity).
  • Mantiene posiciones largas y cortas en la misma cuenta.
  • Contabiliza el P/L realizado cuando cierras una operación y rastrea el P/L no realizado de las posiciones abiertas.
  • Mantiene un historial de capital al que se añade un punto en cada actualización de precio.
  • Opcionalmente calcula un conjunto completo de métricas de rendimiento (Sharpe, Sortino, drawdown, CAGR, etc.).

Lo que no modela: margen, coste de préstamo ni ejecución automática de SL/TP. Los valores SL/TP almacenados son solo números: tu código de estrategia es responsable de comprobar los precios y llamar a close_trade cuando se alcanza un stop o un objetivo. La comisión y el slippage se aplican cuando hay modelos configurados en la pestaña Simulation; ver más abajo.

Los dividendos y los splits se modelan cuando los datos de calendario están activados para la ejecución. En la fecha de efecto del split, el número de acciones se multiplica por la razón y la base de coste se divide por ella (la curva de capital se mantiene continua). En la fecha ex, qty × amount se abona al efectivo en las posiciones largas y se carga en las cortas. El dividendo fluye a balance y al capital pero no a realized_pnl: lee total_dividend_income y total_splits_applied en account.refresh_metrics(). Ver Datos de calendario.

La comisión y el slippage se configuran en la pestaña Simulation del cuadro Run backtest. Cuando están activados, las ejecuciones se ajustan automáticamente; no los defines en Python. El cuadro Run usa por defecto comisión por acción de 0,001 $ por acción con un mínimo de 0 $, y slippage de participación en el volumen. (Un PaperAccount que construyas tú mismo no tiene por defecto ninguna comisión ni ningún slippage.)


Configurar la cuenta

Configuras la cuenta en la pestaña Simulation de Run backtest:

AjusteQué hace
starting_balanceEfectivo con el que empieza la cuenta.
Modelos de comisión / slippageCómo se ajustan las ejecuciones; ver más abajo.

El SL/TP predeterminado opcional y el dimensionamiento por riesgo (sl_pct / sl_dollars, tp_pct / tp_dollars, risk_pct_per_trade / risk_fixed_per_trade) aún pueden aplicarse desde asistentes .qcs / de configuración cuando están presentes, pero el cuadro Run backtest no expone actualmente esas entradas. Cuando están definidos:

  • Los campos de porcentaje aceptan valores en porcentaje entero como 2 o fracciones como 0.02: ambos significan lo mismo.
  • En posiciones largas, el stop está por debajo de la entrada y el objetivo por encima. Las cortas lo invierten.
  • Los valores predeterminados se aplican a cada operación nueva, salvo que los sobrescribas en cada llamada.

Solo para notebooks o pruebas (fuera del flujo de backtest del IDE), puedes construir una cuenta manualmente:

account = PaperAccount( 100_000, sl_pct=2, tp_pct=4, risk_pct_per_trade=1, slippage_model=None, # optional; defaults to no slippage (IDE engine wires from Simulation tab) commission_model=None, # optional; defaults to zero commission (IDE engine wires from Simulation tab) equity_history_cap=None, # optional; keep only the newest N equity_history points )

Comisión y slippage (pestaña Simulation)

La pestaña Simulation del cuadro Run backtest configura cómo se ajustan las ejecuciones por los costes de trading. Los ajustes se aplican automáticamente a runtime.account: no los configuras en Python. Consulta Backtests y Preajustes de ejecución (.qcs) para guardar estas opciones.

Modelos de comisión

ModeloParámetrosComportamiento
Por acción (predeterminado)Coste por acción ($), mínimo por operación ($)max(qty × cost, min) cuando el mínimo > 0
Por operaciónCoste fijo por orden ($)La misma tarifa sea cual sea la cantidad
Cero—Sin comisión; útil para probar alfa puro

Modelos de slippage

ModeloParámetrosComportamiento
Participación en el volumen (predeterminado)Límite de volumen (predeterminado 0,025), impacto en el precio (predeterminado 0,1)El impacto escala con (qty / bar_volume)²; las compras se ejecutan más alto y las ventas más bajo. Solo en las aperturas, volume_limit también reduce en silencio la cantidad a volume_limit × bar_volume: revisa el snapshot devuelto para ver el tamaño que realmente obtuviste. (Si no queda nada, open_trade lanza qty after volume_limit cap is not positive.) Los cierres nunca se limitan. Volumen de barra cero o ausente → sin impacto.
Spread fijoSpread total por acción ($)Se aplica medio spread en cada tramo de ejecución
Cero—Ejecución al precio exacto solicitado

Los registros de operaciones cerradas incluyen commission, slippage_cost, raw_entry_price y raw_exit_price. Los entry_price / exit_price almacenados son los precios de ejecución tras el slippage. account.refresh_metrics() expone total_commission y total_slippage_cost.


Posiciones

Una posición representa una operación abierta en un único símbolo. No construyes las posiciones directamente: las crea open_trade(...) y las elimina close_trade(...), que también puede cerrar parte de una posición y dejar el resto en su lugar.

Abrir una operación

snap = account.open_trade( symbol, # "AAPL" side, # "long" or "short" qty, # integer or float quantity entry_price, # float, the requested price (slippage/commission applied) sl_price=None, # optional stop price (overrides default sl_pct/sl_dollars) tp_price=None, # optional target price (overrides default tp_pct/tp_dollars) position_id=None, )

La llamada devuelve un snapshot de la cuenta (ver más abajo). La posición recién abierta aparece en snap["open_positions"]: toma su id si quieres cerrarla más tarde por id. El entry_price almacenado es la ejecución tras el slippage; el precio solicitado se conserva como raw_entry_price.

Una apertura larga se rechaza con insufficient cash for long cuando qty × fill_price + commission (precio de ejecución tras el slippage) supera el efectivo. El snapshot devuelto valora ese símbolo al precio de ejecución.

Dimensionar por riesgo

Si quieres que la cuenta dimensione la operación según tu presupuesto de riesgo (uno de risk_pct_per_trade o risk_fixed_per_trade) y una distancia de stop, usa:

qty = account.position_size_for_risk(entry_price, stop_price)

Esto requiere que esté configurado exactamente un modo de riesgo y que la distancia al stop no sea cero. Por defecto, risk_pct_per_trade usa un porcentaje del efectivo; pasa equity_for_risk=... para dimensionar contra el capital.

Cerrar una operación

account.close_trade(position_id, exit_price) # close the whole position account.close_trade(position_id, exit_price, qty=4) # scale out 4 units, keep the rest

La firma completa es close_trade(position_id, exit_price, *, qty=None): qty es solo por palabra clave.

open_trade / close_trade toman un precio solicitado y luego aplican los modelos de slippage y comisión de la ejecución. El exit_price almacenado es la ejecución; el precio solicitado se conserva como raw_exit_price. Un patrón común de simulación de backtest: solicitar el tick sintético de cierre de la barra desde on_tick:

def on_tick(bar_index, tick_in_bar, price, bar, fundamentals=None, calendar=None, symbol=None): if tick_in_bar == 3: account.close_trade(pid, price)

En las ejecuciones forward, usa el price en vivo de cada cotización del bróker en on_tick en lugar de comprobar tick_in_bar.

Cierres parciales (qty=)

Pasar qty cierra solo esas unidades y deja el resto abierto en su lugar: mismo id de posición, mismo entry_price, mismo entry_time, de modo que la posición restante conserva su base de coste y su periodo de tenencia:

pid = account.open_positions_payload()[0]["id"] account.close_trade(pid, px, qty=4) # scale out 4 units, keep the rest

El registro de un cierre parcial en closed_trades lleva partial: True y remaining_qty.

  • Los costes se cobran sobre las unidades que realmente se operaron, y la comisión de apertura del lote se reparte a prorrata, así que una secuencia de cierres parciales cobra exactamente lo que cobraría un único cierre completo.
  • Omite qty —o pasa un valor igual o superior al tamaño de la posición— para un cierre completo.
  • El resto sigue siendo una posición abierta normal: se sigue valorando en cada paso y aparece en snap["open_positions"] con el mismo id, así que puedes reducirla de nuevo más tarde.

También puedes cerrar en bloque por símbolo. symbol= es obligatorio en ejecuciones con varios símbolos para no cerrar por accidente posiciones de todos los tickers a la vez:

account.close_all_longs(exit_price, symbol="AAPL") account.close_all_shorts(exit_price, symbol="AAPL")

Otros asistentes

MétodoPropósito
open_positions_payload(prices=None)Tramos abiertos como dicts, cada uno con last_price y unrealized_pnl a los prices (por defecto: el último tick).
open_position_symbols()Conjunto de símbolos con al menos un tramo abierto.
position_unrealized_pnl(pos, prices)P/L abierto de una OpenPosition.
apply_split(symbol, ratio, *, when=None) / apply_dividend(symbol, amount_per_share, *, estimated=False, when=None)Acciones corporativas: el motor las llama cuando los datos de calendario están activados; ver Datos de calendario.
corporate_actionsRegistro de los splits / dividendos aplicados hasta ahora.
set_bar_volume(symbol, volume)Volumen de barra que usa el slippage por participación en el volumen; el motor lo establece en cada barra.

Precio de salida frente a indicadores: Pasa el precio al que apuntas (p. ej. float(bar["close"]) desde on_bar); el motor aplica igualmente slippage/comisión. No reescribe tu solicitud a una SMA u otro indicador. En backtest, tick_in_bar == 3 en on_tick es un tick de cierre sintético (simulación OHLC de la barra, no datos reales de ticks). En las ejecuciones forward, cierra usando el price en vivo de on_tick en cada actualización del bróker.


Efectivo, saldo, capital y PnL

Estos son los campos centrales de seguimiento del dinero disponibles en la cuenta y en cada snapshot:

CampoSignificado
starting_balanceEl efectivo con el que empezó la cuenta.
cash / balanceEfectivo liquidado tras aperturas/cierres. Las aperturas largas cargan qty × fill_price + commission; las cortas abonan qty × fill_price − commission. Los entry_price / exit_price almacenados son precios de ejecución tras el slippage.
equityTotal a valor de mercado: cash + Σ(long qty × mark) − Σ(short qty × mark).
unrealized_pnlSuma del P/L de las posiciones abiertas, valoradas a las marcas actuales.
realized_pnlGanancia/pérdida acumulada de las operaciones cerradas.

Cómo se actualizan las marcas:

  • El motor llama a tick(prices, time=...) en la cuenta en cada paso de precio simulado, de modo que equity, unrealized_pnl y el historial de capital se mantienen al día automáticamente.
  • Si falta el precio de marca de un símbolo en el mapa prices, esa posición se valora a su precio de entrada: el P/L no realizado de ese tramo es 0 hasta que pases una cotización real.
  • close_trade arrastra las marcas anteriores y marca el símbolo cerrado a su ejecución de salida, de modo que las posiciones que quedan abiertas —incluido el resto de un cierre parcial— siguen valoradas. refresh_metrics() sin argumento reutiliza las marcas del snapshot más reciente (open_trade / close_trade / tick) y solo recurre al último capital / efectivo cuando ese mapa está vacío.

Dentro de on_bar y on_tick, no necesitas llamar tú a account.tick(...): el motor lo hace por ti. Avanza antes de cada on_tick, pero on_bar se ejecuta antes de los ticks de ese paso, así que en on_bar las últimas marcas siguen siendo el cierre de la barra anterior. Un tick redundante añade un punto extra a equity_history. Es preferible pasar last_prices explícitos a refresh_metrics({symbol: last_px}) en on_finish cuando algo pueda seguir abierto.

Operaciones cortas y efectivo

En la misma cuenta se admiten tanto posiciones largas como cortas:

  • Apertura corta: los ingresos aumentan el efectivo (menos la comisión cuando está configurada).
  • Cierre corto (comprar para cubrir): reduce el efectivo.

No hay modelo de margen ni coste de préstamo, así que las posiciones cortas son una simulación pura de efectivo y marcas (más comisión/slippage cuando hay modelos definidos).


Snapshots

Cada llamada a open_trade, close_trade y tick devuelve el mismo diccionario snapshot que describe el estado de la cuenta justo después de la llamada. Claves útiles:

ClaveQué contiene
starting_balanceEfectivo inicial.
cash / balanceEfectivo actual.
equityCapital actual a valor de mercado.
unrealized_pnlP/L de las posiciones abiertas a las marcas actuales.
realized_pnlP/L realizado acumulado.
open_positionsLista de dicts de posiciones abiertas: id, symbol, side, qty, entry_price, sl_price, tp_price, raw_entry_price, commission_at_open, entry_time (puede ser null). No hay campo de P/L por tramo en las filas de posiciones abiertas: lee unrealized_pnl del snapshot para el P/L flotante agregado.
closed_tradesLista de operaciones cerradas: position_id, symbol, side, qty, precios de entrada/salida, P/L realizado, commission (incluye la parte de la comisión de apertura correspondiente a este cierre), slippage_cost (slippage de entrada + salida), raw_exit_price, raw_entry_price opcional y entry_time / exit_time; los cierres parciales añaden partial y remaining_qty. También disponible como account.closed_trades en la instancia.
tick_pricesCopia del mapa de precios usado para ese snapshot. close_trade arrastra las marcas anteriores y marca el símbolo cerrado a su ejecución de salida, de modo que los tramos abiertos siguen valorados a través de un cierre.

No existe la API account.get_open_positions(symbol). Filtra snap["open_positions"] (o account.open_positions_payload()) por symbol en mayúsculas, o lee account.closed_trades para el historial cerrado.

Esto facilita inspeccionar la cuenta en tu estrategia:

snap = account.open_trade("AAPL", "long", 10, px) print("equity now:", snap["equity"]) print("open positions:", len(snap["open_positions"]))

Métricas de rendimiento

Cuando la ejecución está terminando, puedes pedirle a la cuenta que calcule un conjunto completo de métricas:

def on_finish(bars, symbol=None, bars_by_symbol=None): last_px = float(bars[-1]["close"]) metrics = account.refresh_metrics({"AAPL": last_px})

El argumento es un mapa {symbol: last_price} que se usa como marca final para que las posiciones abiertas se valoren correctamente. Usa la lista bars que recibe on_finish (la más antigua primero) para obtener el último cierre: es la serie canónica de todo el historial.

También puedes pasar dos argumentos opcionales:

  • risk_free_rate=0: lo usan los ratios tipo Sharpe/Sortino.
  • calmar_annualized_return=None: sustituye el rendimiento anualizado que usa Calmar.

Métricas disponibles

refresh_metrics(...) devuelve un diccionario que incluye:

GrupoCampos
Rendimientostotal_pnl, total_return, cagr
Riesgomax_drawdown, volatility, risk_adjusted_return
Ratiossharpe_ratio, sortino_ratio, calmar_ratio, profit_factor
Estadísticas de operacionesaverage_trade_expectancy, average_win, average_loss, total_commission, total_slippage_cost
Exposicióncurrent_exposure, average_exposure, max_exposure
Acciones corporativastotal_dividend_income, total_splits_applied

Notas:

  • Los ratios basados en operaciones usan el rendimiento por operación, definido como realized_pnl / starting_balance.
  • total_return y cagr usan el capital actual frente al saldo inicial.
  • Para un CAGR / Calmar significativo, el motor necesita información de calendario: ya pasa las marcas de tiempo cuando avanza la cuenta, así que en general no tienes que hacer nada especial.
  • Algunos ratios pueden devolver inf cuando el denominador es cero y el rendimiento es favorable (por ejemplo, ninguna operación perdedora).

Las métricas también se muestran en la vista Test Results del editor y en la carga estructurada del resultado de la ejecución.


Todo junto

Uso típico dentro de una estrategia:

  1. Configura el saldo inicial y la comisión / slippage en la pestaña Simulation (SL/TP / riesgo opcionales mediante .qcs / código cuando se admita).
  2. Abre operaciones desde on_bar (o desde on_tick para ejecuciones intrabarra) con account.open_trade(...); guarda el id devuelto si lo vas a necesitar.
  3. Cierra operaciones con account.close_trade(position_id, exit_price), o con qty= para reducir y dejar abierto el resto del lote.
  4. Lee snap["equity"], snap["cash"], snap["unrealized_pnl"], etc. siempre que quieras inspeccionar la cuenta.
  5. En on_finish, llama a account.refresh_metrics({symbol: last_price}) para rellenar las métricas de rendimiento del resultado.

Ese es todo el modelo de cuenta: una superficie pequeña, pero suficiente para simular posiciones largas y cortas, flujos de efectivo, P/L y un conjunto completo de métricas para cualquier estrategia que puedas expresar con los callbacks del ciclo de vida.

15 min de lecturaSugerir una edición ↗
Modelo de cuenta — Documentación (1.0 (latest)) | QuantCraft