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.accountes una cuenta real de bróker: una subclase dePaperAccountcon 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. Trataopen_trade/close_tradeallí 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:
| Ajuste | Qué hace |
|---|---|
starting_balance | Efectivo con el que empieza la cuenta. |
| Modelos de comisión / slippage | Có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
2o fracciones como0.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
| Modelo | Parámetros | Comportamiento |
|---|---|---|
| Por acción (predeterminado) | Coste por acción ($), mínimo por operación ($) | max(qty × cost, min) cuando el mínimo > 0 |
| Por operación | Coste fijo por orden ($) | La misma tarifa sea cual sea la cantidad |
| Cero | — | Sin comisión; útil para probar alfa puro |
Modelos de slippage
| Modelo | Parámetros | Comportamiento |
|---|---|---|
| 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 fijo | Spread 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 restLa 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 restEl 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étodo | Propó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_actions | Registro 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"])desdeon_bar); el motor aplica igualmente slippage/comisión. No reescribe tu solicitud a una SMA u otro indicador. En backtest,tick_in_bar == 3enon_tickes un tick de cierre sintético (simulación OHLC de la barra, no datos reales de ticks). En las ejecuciones forward, cierra usando elpriceen vivo deon_ticken 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:
| Campo | Significado |
|---|---|
starting_balance | El efectivo con el que empezó la cuenta. |
cash / balance | Efectivo 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. |
equity | Total a valor de mercado: cash + Σ(long qty × mark) − Σ(short qty × mark). |
unrealized_pnl | Suma del P/L de las posiciones abiertas, valoradas a las marcas actuales. |
realized_pnl | Ganancia/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 queequity,unrealized_pnly 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_tradearrastra 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_baryon_tick, no necesitas llamar tú aaccount.tick(...): el motor lo hace por ti. Avanza antes de cadaon_tick, peroon_barse ejecuta antes de los ticks de ese paso, así que enon_barlas últimas marcas siguen siendo el cierre de la barra anterior. Untickredundante añade un punto extra aequity_history. Es preferible pasarlast_pricesexplícitos arefresh_metrics({symbol: last_px})enon_finishcuando 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:
| Clave | Qué contiene |
|---|---|
starting_balance | Efectivo inicial. |
cash / balance | Efectivo actual. |
equity | Capital actual a valor de mercado. |
unrealized_pnl | P/L de las posiciones abiertas a las marcas actuales. |
realized_pnl | P/L realizado acumulado. |
open_positions | Lista 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_trades | Lista 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_prices | Copia 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:
| Grupo | Campos |
|---|---|
| Rendimientos | total_pnl, total_return, cagr |
| Riesgo | max_drawdown, volatility, risk_adjusted_return |
| Ratios | sharpe_ratio, sortino_ratio, calmar_ratio, profit_factor |
| Estadísticas de operaciones | average_trade_expectancy, average_win, average_loss, total_commission, total_slippage_cost |
| Exposición | current_exposure, average_exposure, max_exposure |
| Acciones corporativas | total_dividend_income, total_splits_applied |
Notas:
- Los ratios basados en operaciones usan el rendimiento por operación, definido como
realized_pnl / starting_balance. total_returnycagrusan 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
infcuando 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:
- 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). - Abre operaciones desde
on_bar(o desdeon_tickpara ejecuciones intrabarra) conaccount.open_trade(...); guarda eliddevuelto si lo vas a necesitar. - Cierra operaciones con
account.close_trade(position_id, exit_price), o conqty=para reducir y dejar abierto el resto del lote. - Lee
snap["equity"],snap["cash"],snap["unrealized_pnl"], etc. siempre que quieras inspeccionar la cuenta. - En
on_finish, llama aaccount.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.

