页面账户模型 · 9 / 29
账户模型
每个 QuantCraft 策略都运行在内置的模拟交易账户之上。该账户跟踪你的现金、持有未平仓头寸、在你平仓时记录已实现盈亏,并在回测结束时计算业绩指标。
正常使用时你不需要自己构造它——回测引擎会根据 Run backtest 对话框的 Simulation 标签页(初始资金、佣金/滑点)创建它,并在 on_init() 运行之前将其赋给 quantcraft.process.runtime.account。可选的默认 SL/TP 以及基于风险的仓位计算,可在存在时通过配置/.qcs 键接入;该对话框目前不提供 SL/TP/风险输入。回调接收市场数据的方式见策略生命周期和 OHLCV 与 K 线数据;指标显示在测试结果中。
快速开始
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})这就是完整的模式:开一笔交易,保存头寸 id,稍后平仓,并(可选地)在结束时刷新指标。
你可以从实例读取的内容: account.cash、account.balance、account.realized_pnl、account.closed_trades 和 account.open_positions_payload()。没有 account.open_positions 属性——未平仓的腿来自快照(snap["open_positions"])或 open_positions_payload()。
前向运行会下真实订单。 在图表前向测试或批量运行中,
runtime.account是一个真实的券商账户——PaperAccount的子类,API 相同,但会向你已连接的券商账户(模拟或实盘)发送真实订单,包括股票的括号式 SL/TP 订单。请把那里的open_trade/close_trade当作真实交易。交易存在亏损风险。
PaperAccount 的作用
该账户模拟一个简单的现金交易账户:
- 跟踪初始资金、现金、余额和权益(equity)。
- 在同一账户中持有多头和空头头寸。
- 在你平仓时记录已实现盈亏,并跟踪未平仓头寸的未实现盈亏。
- 维护一份权益历史,每次价格更新都会追加。
- 可选地计算一整套业绩指标(夏普、索提诺、回撤、CAGR 等)。
它不模拟的内容:保证金、借券成本,或 SL/TP 的自动执行。存储的 SL/TP 值只是数字——检查价格并在触及止损或目标时调用 close_trade 是你的策略代码的责任。当在 Simulation 标签页配置了模型时,会应用佣金和滑点——见下文。
当运行启用了日历数据时,会模拟股息和拆股。在拆股生效日,股数乘以比例,成本基础除以该比例(权益曲线保持连续)。在除息日,多头头寸会将 qty × amount 计入现金,空头头寸则扣除。股息会进入 balance 和权益,但不会进入 realized_pnl——请从 account.refresh_metrics() 读取 total_dividend_income 和 total_splits_applied。参见日历数据。
佣金和滑点在 Run backtest 对话框的 Simulation 标签页中配置。启用后,成交会被自动调整;你不需要在 Python 中设置它们。Run 对话框默认采用按股佣金,即每股 $0.001、最低 $0,以及成交量占比滑点。(你自己构造的 PaperAccount 默认没有佣金,也没有滑点。)
配置账户
你在 Run backtest 的 Simulation 标签页中配置账户:
| 设置 | 作用 |
|---|---|
starting_balance | 账户起始的现金。 |
| 佣金/滑点模型 | 成交如何被调整——见下文。 |
可选的默认 SL/TP 和风险仓位计算(sl_pct / sl_dollars、tp_pct / tp_dollars、risk_pct_per_trade / risk_fixed_per_trade)在存在时仍可通过 .qcs/配置辅助函数应用,但 Run backtest 对话框目前不提供这些输入。设置后:
- 百分比字段既接受
2这样的整数百分比,也接受0.02这样的小数——两者含义相同。 - 对多头,止损位于入场价下方,目标位于入场价上方。空头则相反。
- 默认值适用于每一笔新交易,除非你在每次调用中覆盖。
仅限 notebook 或测试(IDE 回测流程之外),你可以手动构造账户:
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
)佣金与滑点(Simulation 标签页)
Run backtest 对话框的 Simulation 标签页用于配置成交如何按交易成本调整。这些设置会自动应用到 runtime.account——你不需要在 Python 中配置它们。要保存这些选项,请参见回测和运行预设(.qcs)。
佣金模型
| 模型 | 参数 | 行为 |
|---|---|---|
| 按股(默认) | 每股成本($)、每笔交易最低额($) | 最低额 > 0 时为 max(qty × cost, min) |
| 按笔 | 每笔订单固定成本($) | 无论数量多少,费用相同 |
| 零 | — | 无佣金——适合测试纯 alpha |
滑点模型
| 模型 | 参数 | 行为 |
|---|---|---|
| 成交量占比(默认) | 成交量上限(默认 0.025)、价格冲击(默认 0.1) | 冲击随 (qty / bar_volume)² 缩放;买入成交价更高,卖出成交价更低。仅在开仓时,volume_limit 还会将数量悄悄缩减为 volume_limit × bar_volume——请检查返回的快照以了解你实际得到的数量。(如果什么都不剩,open_trade 会抛出 qty after volume_limit cap is not positive。)平仓从不受限。K 线成交量为零/缺失 → 无冲击。 |
| 固定点差 | 每股总点差($) | 每个成交腿应用一半点差 |
| 零 | — | 按请求的确切价格成交 |
已平仓交易记录包含 commission、slippage_cost、raw_entry_price 和 raw_exit_price。存储的 entry_price / exit_price 是滑点后的成交价。account.refresh_metrics() 会提供 total_commission 和 total_slippage_cost。
头寸
一个头寸表示单一标的上的一笔未平仓交易。你不会直接构造头寸——它们由 open_trade(...) 创建,由 close_trade(...) 移除,后者也可以平掉头寸的一部分并保留其余部分。
开仓
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,
)该调用返回账户的一个快照(见下文)。新开的头寸出现在 snap["open_positions"] 中——如果你之后想按 id 平仓,请取走它的 id。存储的 entry_price 是滑点后的成交价;请求价格保存在 raw_entry_price 中。
当 qty × fill_price + commission(滑点后的成交价)超过现金时,多头开仓会被拒绝,并提示 insufficient cash for long。返回的快照按成交价对该标的估值。
按风险计算仓位
如果你希望账户根据你的风险预算(risk_pct_per_trade 或 risk_fixed_per_trade 之一)和止损距离来计算交易规模,请使用:
qty = account.position_size_for_risk(entry_price, stop_price)这要求恰好配置了一种风险模式,并且止损距离不为零。默认情况下,risk_pct_per_trade 使用现金的一个百分比;传入 equity_for_risk=... 则按权益来计算。
平仓
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完整签名为 close_trade(position_id, exit_price, *, qty=None)——qty 是仅限关键字参数。
open_trade / close_trade 接收一个请求价格,然后应用本次运行的滑点和佣金模型。存储的 exit_price 是成交价;请求价格保存在 raw_exit_price 中。一个常见的回测模拟模式是:在 on_tick 中请求该 K 线的合成收盘 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)在前向运行中,请在 on_tick 中使用来自每次券商报价的实时 price,而不是检查 tick_in_bar。
部分平仓(qty=)
传入 qty 只会平掉这么多单位,其余部分原地保持未平仓——相同的头寸 id、相同的 entry_price、相同的 entry_time——因此剩余头寸保留其成本基础和持仓时间:
pid = account.open_positions_payload()[0]["id"]
account.close_trade(pid, px, qty=4) # scale out 4 units, keep the rest部分平仓在 closed_trades 中的记录带有 partial: True 和 remaining_qty。
- 成本按实际成交的单位收取,该批次的开仓佣金按比例分摊,因此一连串部分平仓收取的费用,与一次全部平仓完全相同。
- 省略
qty——或传入等于或大于头寸规模的值——即为全部平仓。 - 剩余部分仍是一个正常的未平仓头寸:它在每一步都会被标记,并以相同的 id 出现在
snap["open_positions"]中,因此你之后可以再次减仓。
你也可以按标的批量平仓。在多标的运行中,symbol= 是必需的,以免你不小心同时平掉所有代码上的头寸:
account.close_all_longs(exit_price, symbol="AAPL")
account.close_all_shorts(exit_price, symbol="AAPL")其他辅助函数
| 方法 | 用途 |
|---|---|
open_positions_payload(prices=None) | 以字典形式返回未平仓的腿,每个包含按 prices 计算的 last_price 和 unrealized_pnl(默认:最后一次 tick)。 |
open_position_symbols() | 至少有一个未平仓腿的标的集合。 |
position_unrealized_pnl(pos, prices) | 单个 OpenPosition 的浮动盈亏。 |
apply_split(symbol, ratio, *, when=None) / apply_dividend(symbol, amount_per_share, *, estimated=False, when=None) | 公司行动——启用日历数据时由引擎调用;参见日历数据。 |
corporate_actions | 迄今已应用的拆股/股息记录。 |
set_bar_volume(symbol, volume) | 成交量占比滑点所用的 K 线成交量;引擎在每根 K 线设置它。 |
平仓价与指标:传入你打算瞄准的价格(例如来自
on_bar的float(bar["close"]));引擎仍会应用滑点/佣金。它不会把你的请求改写成 SMA 或其他指标。在回测中,on_tick里的tick_in_bar == 3是合成的收盘 tick(K 线 OHLC 模拟,而非真实 tick 数据)。在前向运行中,请在每次券商更新时使用on_tick的实时price来平仓。
现金、余额、权益与盈亏
以下是账户和每个快照上可用的核心资金跟踪字段:
| 字段 | 含义 |
|---|---|
starting_balance | 账户起始的现金。 |
cash / balance | 开仓/平仓后的已结算现金。多头开仓扣除 qty × fill_price + commission;空头开仓计入 qty × fill_price − commission。存储的 entry_price / exit_price 是滑点后的成交价。 |
equity | 按市值计价的总额:cash + Σ(long qty × mark) − Σ(short qty × mark)。 |
unrealized_pnl | 未平仓头寸按当前标记价计算的盈亏总和。 |
realized_pnl | 已平仓交易的累计盈亏。 |
标记价如何更新:
- 引擎在每个模拟价格步骤都会对账户调用
tick(prices, time=...),因此equity、unrealized_pnl和权益历史会自动保持最新。 - 如果某个标的的标记价缺失于
prices映射,该头寸将按其入场价估值——在你传入真实报价之前,该腿的未实现盈亏为 0。 close_trade会沿用之前的标记价,并将已平仓的标的标记为其平仓成交价,因此仍未平仓的头寸——包括部分平仓后的剩余部分——保持被标记。不带参数的refresh_metrics()会重用最近一次快照(open_trade/close_trade/tick)的标记价,仅当该映射为空时才回退到最后的权益/现金。
在
on_bar和on_tick内,你不需要自己调用account.tick(...)——引擎会替你驱动。它在每次on_tick之前执行 tick,但on_bar在该步骤的 tick 之前运行,因此在on_bar中,最新的标记价仍是上一根 K 线的收盘价。多余的tick会向equity_history追加一个额外的点。当仍可能有头寸未平仓时,建议在on_finish中向refresh_metrics({symbol: last_px})显式传入last_prices。
空头交易与现金
同一账户同时支持多头和空头:
- 空头开仓——所得款项增加现金(配置了佣金时需减去佣金)。
- 空头平仓(买入回补)——减少现金。
没有保证金模型,也没有借券成本,因此空头是纯粹的现金与标记价模拟(设置了模型时再加上佣金/滑点)。
快照
每次调用 open_trade、close_trade 和 tick 都会返回同一个快照字典,描述调用刚结束时账户的状态。常用键:
| 键 | 内容 |
|---|---|
starting_balance | 初始现金。 |
cash / balance | 当前现金。 |
equity | 当前按市值计价的权益。 |
unrealized_pnl | 按当前标记价计算的未平仓头寸盈亏。 |
realized_pnl | 累计已实现盈亏。 |
open_positions | 未平仓头寸字典列表:id、symbol、side、qty、entry_price、sl_price、tp_price、raw_entry_price、commission_at_open、entry_time(可能为 null)。未平仓头寸行上没有每条腿的盈亏字段——如需汇总的浮动盈亏,请读取快照中的 unrealized_pnl。 |
closed_trades | 已平仓交易列表:position_id、symbol、side、qty、入场/平仓价、已实现盈亏、commission(包含本次平仓分摊的开仓佣金)、slippage_cost(入场 + 平仓滑点)、raw_exit_price、可选的 raw_entry_price,以及 entry_time / exit_time;部分平仓会增加 partial 和 remaining_qty。也可通过实例上的 account.closed_trades 获取。 |
tick_prices | 该快照所用价格映射的副本。close_trade 沿用之前的标记价并将已平仓标的标记为其平仓成交价,因此未平仓的腿在平仓前后仍保持被标记。 |
没有 account.get_open_positions(symbol) 这个 API。请按大写的 symbol 过滤 snap["open_positions"](或 account.open_positions_payload()),或读取 account.closed_trades 获取已平仓历史。
这使得在策略中检查账户变得很容易:
snap = account.open_trade("AAPL", "long", 10, px)
print("equity now:", snap["equity"])
print("open positions:", len(snap["open_positions"]))业绩指标
当运行即将结束时,你可以让账户计算一整套指标:
def on_finish(bars, symbol=None, bars_by_symbol=None):
last_px = float(bars[-1]["close"])
metrics = account.refresh_metrics({"AAPL": last_px})该参数是一个 {symbol: last_price} 映射,用作最终标记价,以便正确估值未平仓头寸。使用传入 on_finish 的 bars 列表(最旧的在前)来获取最后的收盘价——这是完整历史的标准序列。
你还可以传入两个可选参数:
risk_free_rate=0——供夏普/索提诺类比率使用。calmar_annualized_return=None——覆盖卡玛比率使用的年化收益。
可用指标
refresh_metrics(...) 返回一个字典,其中包括:
| 分组 | 字段 |
|---|---|
| 收益 | total_pnl、total_return、cagr |
| 风险 | max_drawdown、volatility、risk_adjusted_return |
| 比率 | sharpe_ratio、sortino_ratio、calmar_ratio、profit_factor |
| 交易统计 | average_trade_expectancy、average_win、average_loss、total_commission、total_slippage_cost |
| 敞口 | current_exposure、average_exposure、max_exposure |
| 公司行动 | total_dividend_income、total_splits_applied |
注意:
- 基于交易的比率使用每笔交易收益,定义为
realized_pnl / starting_balance。 total_return和cagr使用当前权益与初始资金的对比。- 要得到有意义的 CAGR / Calmar,引擎需要日历信息——引擎在对账户 tick 时已经传入了时间戳,因此通常无需做任何特殊处理。
- 当分母为零且业绩良好时(例如没有亏损交易),某些比率可能返回
inf。
这些指标也会显示在编辑器的 Test Results 视图以及运行的结构化结果载荷中。
综合运用
策略内的典型用法:
- 在 Simulation 标签页配置初始资金和佣金/滑点(可选的 SL/TP/风险,在支持时通过
.qcs/代码设置)。 - 使用
account.open_trade(...)从on_bar(或为 K 线内成交从on_tick)开仓——如果之后会用到,请保存返回的id。 - 使用
account.close_trade(position_id, exit_price)平仓——或使用qty=减仓并让该批次的其余部分保持未平仓。 - 每当你想检查账户时,读取
snap["equity"]、snap["cash"]、snap["unrealized_pnl"]等。 - 在
on_finish中调用account.refresh_metrics({symbol: last_price}),为结果填充业绩指标。
这就是完整的账户模型——接口面很小,但足以为任何能用生命周期回调表达的策略模拟多头、空头、现金流、盈亏以及一整套指标。

