文档1.0 (latest)
页面账户模型 · 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 视图以及运行的结构化结果载荷中。


综合运用

策略内的典型用法:

  1. 在 Simulation 标签页配置初始资金和佣金/滑点(可选的 SL/TP/风险,在支持时通过 .qcs/代码设置)。
  2. 使用 account.open_trade(...) 从 on_bar(或为 K 线内成交从 on_tick)开仓——如果之后会用到,请保存返回的 id。
  3. 使用 account.close_trade(position_id, exit_price) 平仓——或使用 qty= 减仓并让该批次的其余部分保持未平仓。
  4. 每当你想检查账户时,读取 snap["equity"]、snap["cash"]、snap["unrealized_pnl"] 等。
  5. 在 on_finish 中调用 account.refresh_metrics({symbol: last_price}),为结果填充业绩指标。

这就是完整的账户模型——接口面很小,但足以为任何能用生命周期回调表达的策略模拟多头、空头、现金流、盈亏以及一整套指标。

阅读约 5 分钟建议修改 ↗
账户模型 — 文档 (1.0 (latest)) | QuantCraft