Account Model
Every QuantCraft strategy runs against a built-in paper trading account. The account is what tracks your cash, holds your open positions, books realized profit and loss when you close trades, and computes performance metrics at the end of a backtest.
You don't construct it yourself in normal use — the Test (backtest) dialog sets the starting balance and risk options, and the engine creates the account and hands it to your strategy through ide.backtest.runtime.account. Callbacks receive market data as described in Strategy lifecycle and OHLCV and bar data; metrics show up in Test results.
Quick start
from ide.backtest.runtime import account
def on_init():
pass
def on_bar(bar_index, bar, fundamentals=None, symbol=None):
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 = account.refresh_metrics({"AAPL": last_px})That's the full pattern: open a trade, store the position id, close it later, and (optionally) refresh metrics at the end.
What PaperAccount does
The account simulates a simple cash trading account:
- Tracks starting balance, cash, balance, and equity.
- Holds long and short positions in the same account.
- Books realized P/L when you close a trade and tracks unrealized P/L for open positions.
- Maintains an equity history that is appended on every price update.
- Optionally computes a full set of performance metrics (Sharpe, Sortino, drawdown, CAGR, etc.).
What it does not model: margin, broker fees and commissions, dividends, borrow cost, or automatic SL/TP execution. Stored SL/TP values are just numbers — your strategy code is responsible for checking prices and calling close_trade when a stop or target is hit.
Configuring the account
You configure the account in the Test (backtest) dialog under the Account section. The most common settings are:
| Setting | What it does |
|---|---|
starting_balance | Cash the account starts with. |
sl_pct / sl_dollars | Default stop loss for new trades. Either a percent or a raw price distance — pick one. |
tp_pct / tp_dollars | Default take profit for new trades. Pick one. |
risk_pct_per_trade / risk_fixed_per_trade | Risk budget used by position_size_for_risk(...). Pick one. |
Notes:
- Percent fields accept either whole-percent values like
2or fractions like0.02— both mean the same thing. - For longs, the stop is below entry and the target is above entry. Shorts invert that.
- Defaults set here apply to every new trade unless you override them per call.
If you want to build an account manually (for example outside the IDE), you can also call the constructor directly:
account = PaperAccount(
100_000,
sl_pct=2,
tp_pct=4,
risk_pct_per_trade=1,
)Positions
A position represents one open trade in a single symbol. You don't construct positions directly — they are created by open_trade(...) and removed by close_trade(...).
Opening a trade
snap = account.open_trade(
symbol, # "AAPL"
side, # "long" or "short"
qty, # integer or float quantity
entry_price, # float, the fill price
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,
)The call returns a snapshot of the account (see below). The newly opened position appears in snap["open_positions"] — grab its id if you want to close it later by id.
Sizing by risk
If you want the account to size the trade based on your risk budget (one of risk_pct_per_trade or risk_fixed_per_trade) and a stop distance, use:
qty = account.position_size_for_risk(entry_price, stop_price)This requires that exactly one risk mode is configured and that the stop distance isn't zero. By default, risk_pct_per_trade uses a percent of cash; pass equity_for_risk=... to size against equity instead.
Closing a trade
account.close_trade(position_id, exit_price)The exit price is recorded exactly as you pass it, so you control whether a fill happens at bar close, intrabar, or at any other reference price. A common pattern: fill at the bar's closing tick from on_tick:
def on_tick(bar_index, tick_in_bar, price, bar, fundamentals=None, symbol=None):
if tick_in_bar == 3:
account.close_trade(pid, price)You can also close in bulk by symbol:
account.close_all_longs(exit_price, symbol="AAPL")
account.close_all_shorts(exit_price, symbol="AAPL")Cash, balance, equity, and PnL
These are the core money-tracking fields available on the account and on every snapshot:
| Field | Meaning |
|---|---|
starting_balance | The cash the account started with. |
cash / balance | Settled cash currently in the account (same value, two names). |
equity | Mark-to-market total account value: cash plus the value of open positions at current marks. |
unrealized_pnl | Sum of P/L on open positions, valued at the current marks. |
realized_pnl | Cumulative profit/loss from closed trades. |
How marks are updated:
- The engine calls
tick(prices, time=...)on the account on every simulated price step, soequity,unrealized_pnl, and the equity history stay current automatically. - If a symbol's mark price is missing for a tick, the position is valued at its entry price so equity does not jump before the first real quote.
Inside
on_barandon_tick, you do not need to callaccount.tick(...)yourself — the engine already does it for you.
Short trades and cash
Both longs and shorts are supported in the same account:
- Short open — proceeds increase cash.
- Short close (buy to cover) — reduces cash.
There is no margin model or borrow cost, so shorts are a pure cash-and-mark simulation.
Snapshots
Every call to open_trade, close_trade, and tick returns the same snapshot dictionary describing the state of the account right after the call. Useful keys:
| Key | What it contains |
|---|---|
starting_balance | Initial cash. |
cash / balance | Current cash. |
equity | Current mark-to-market equity. |
unrealized_pnl | P/L on open positions at current marks. |
realized_pnl | Cumulative realized P/L. |
open_positions | List of open positions, each with at least id, symbol, side, qty, entry_price, and current P/L. |
closed_trades | List of closed trades with entry/exit prices and realized P/L. |
tick_prices | The latest mark prices used for valuation. |
This makes it easy to inspect the account in your strategy:
snap = account.open_trade("AAPL", "long", 10, px)
print("equity now:", snap["equity"])
print("open positions:", len(snap["open_positions"]))Performance metrics
When the run is finishing, you can ask the account to compute a full set of metrics:
def on_finish(bars, symbol=None, bars_by_symbol=None):
last_px = float(bars[-1]["close"])
metrics = account.refresh_metrics({"AAPL": last_px})The argument is a {symbol: last_price} map used as the final mark so open positions are valued correctly. Use the bars list passed into on_finish (oldest first) to get the last close — this is the canonical full-history series.
You can also pass two optional arguments:
risk_free_rate=0— used by Sharpe/Sortino-style ratios.calmar_annualized_return=None— overrides the annualized return used by Calmar.
Available metrics
refresh_metrics(...) returns a dictionary that includes:
| Group | Fields |
|---|---|
| Returns | total_pnl, total_return, cagr |
| Risk | max_drawdown, volatility, risk_adjusted_return |
| Ratios | sharpe_ratio, sortino_ratio, calmar_ratio, profit_factor |
| Trade stats | average_trade_expectancy, average_win, average_loss |
| Exposure | current_exposure, average_exposure, max_exposure |
Notes:
- Trade-based ratios use per-trade return defined as
realized_pnl / starting_balance. total_returnandcagruse current equity vs. starting balance.- For meaningful CAGR / Calmar, the engine needs calendar information — the engine already passes timestamps when it ticks the account, so you generally don't need to do anything special.
- Some ratios may return
infwhen the denominator is zero and performance is favorable (for example, no losing trades).
The metrics are also surfaced in the Test Results view of the editor and in the structured result payload of the run.
Putting it together
Typical usage inside a strategy:
- Configure starting balance and (optional) default SL/TP and risk in the Test dialog.
- Open trades from
on_bar(or fromon_tickfor intrabar fills) usingaccount.open_trade(...)— store the returnedidif you'll need it. - Close trades with
account.close_trade(position_id, exit_price). - Read
snap["equity"],snap["cash"],snap["unrealized_pnl"], etc. whenever you want to inspect the account. - In
on_finish, callaccount.refresh_metrics({symbol: last_price})to populate performance metrics for the result.
That's the whole account model — small surface area, but enough to simulate longs, shorts, cash flows, P/L, and a complete metrics set for any strategy you can express with the lifecycle callbacks.