功能

手续费、滑点与杠杆

模拟佣金、滑点、杠杆、期货乘数和负价格

成本前表现出色的策略,成本后可能失效。VBT 可在回测中加入手续费与滑点、固定及按股佣金、杠杆和期货合约乘数,并在一次调用中测试多个成本水平,观察交易优势究竟有多少余量。

Test one strategy at four fee levels in one call
data = vbt.YFData.pull("SPY", start="2015-01-01", end="2025-01-01")
fast = data.close.rolling(10).mean()
slow = data.close.rolling(30).mean()

pf = vbt.PF.from_signals(
    data,
    fast.vbt.crossed_above(slow),
    fast.vbt.crossed_below(slow),
    fees=vbt.Param([0, 0.0005, 0.001, 0.002]),  
    init_cash=10_000,
)
metrics = ["total_return", "total_fees_paid", "sharpe_ratio", "total_trades"]
pf.stats(metrics, agg_func=None).round(2)
        Total Return [%]  Total Fees Paid  Sharpe Ratio  Total Trades
fees
0.0000             99.33             0.00          0.83            37
0.0005             92.09           476.16          0.78            37
0.0010             85.11           933.33          0.74            37
0.0020             71.91          1793.57          0.66            37
pf.value.vbt.plot().show()
2015 至 2024 年 SPY 均线交叉投资组合在 0、5、10、20 基点手续费下的净值 图表数据(JSON)

十年只有 37 笔交易,每笔 20 基点手续费仍使总收益率减少 27 个百分点。每日交易的策略损失会大得多,因此每次回测都应从一开始计入真实成本。

手续费与佣金

fees 是每笔订单金额的比例,fixed_fees 是每笔固定金额。按股佣金除以价格即可转化为比例:

Compare three commission models on a 100-share order
close = pd.Series([50.0], index=pd.date_range("2026-01-01", periods=1))
pf = vbt.PF.from_orders(
    close,
    size=100,
    fees=vbt.Param(
        [0.001, 0.0, 0.005 / 50],  
        keys=["0.1% of value", "$1 per order", "$0.005 per share"],
        level=0,
    ),
    fixed_fees=vbt.Param([0.0, 1.0, 0.0], level=0),
    init_cash=10_000,
)
pf.orders.fees.to_pd().iloc[0].droplevel("fixed_fees")
fees
0.1% of value       5.0
$1 per order        1.0
$0.005 per share    0.5
Name: 2026-01-01 00:00:00, dtype: float64

手续费与其他参数一样是数组,可按资产和 K 线变化。负手续费模拟返佣。若要区分挂单与吃单费率,可按信号类型填充费用数组,例如限价入场位置设置较低费率。若实际成交订单类型无法预先确定,例如止损,或费率依赖近期交易量等级,应在执行前由回调设置手续费。

滑点

slippage 按价格比例,将市价成交向不利方向移动:买入更高,卖出更低。信号模拟器中的限价单则使用限价成交规则。

Apply 10 basis points of slippage
index = pd.date_range("2026-01-01", periods=2)
pf = vbt.PF.from_signals(
    pd.Series([100.0, 110.0], index=index),
    pd.Series([True, False], index=index),
    pd.Series([False, True], index=index),
    slippage=vbt.Param([0, 0.001]),
)
pf.orders.readable[["Column", "Side", "Price"]]
   Column  Side   Price
0   0.000   Buy  100.00
1   0.000  Sell  110.00
2   0.001   Buy  100.10
3   0.001  Sell  109.89

模拟器决定订单可成交后,再应用滑点。100 美元的止损带 10 基点滑点,价格达到 100 美元时触发,以 99.90 美元成交。滑点可能使价格超出本根区间,price_area_vio_mode="cap" 可防止这种情况。

滑点可作为数组传入,为各资产和 K 线设置不同估计,也可在回调中随条件更新。例如,对流动性较低资产或高波动时期使用更大估计,再比较多种假设下的策略。若有买卖报价数据,执行价格示例展示如何按卖价买入、按买价卖出。

杠杆与借贷

杠杆决定持仓中有多少可以由借款支持,本身不会改变订单数量。因此,杠杆策略需要请求超过自身净值 100% 的仓位,并允许账户借入其余资金:

Hold 200% of equity in SPY, rebalanced daily
data = vbt.YFData.pull("SPY", start="2020-01-01", end="2025-01-01")
pf = vbt.PF.from_orders(
    data,
    size=2.0,
    size_type="targetpercent",
    leverage=vbt.Param([1, 2]),
    init_cash=10_000,
)
pd.DataFrame({
    "shares": pf.assets.iloc[0],
    "total return": pf.total_return,
    "max drawdown": pf.max_drawdown,
}).round(3)
          shares  total return  max drawdown
leverage
1         33.853         0.946        -0.337
2         67.707         1.206        -0.586

没有杠杆,账户只能用自身现金买入,200% 目标被限制为 100%。杠杆为 2 时,持有股数翻倍,并像杠杆 ETF 一样每日再平衡。总收益率从 95% 升至 121%,最大回撤从 34% 升至 59%。

数量类型决定订单请求多少借贷能力。设 leverage=3、现金 1,000 美元、每股 100 美元,size=1.0 配合 size_type="percent" 买入 30 股,即全部杠杆购买力;"valuepercent" 则买入 10 股,即净值的 100%。

默认 leverage_mode="lazy" 只在自身现金用尽后借款,"eager" 对每个订单应用杠杆,只使用部分可用现金。手续费从自有现金支付。账户逐根跟踪债务、锁定现金和自由现金。净值降至零时,引擎不会自动强平;订单停止成交,由您决定分析中如何处理该时点。

借贷成本与资金费用

融资成本取决于借款金额和持仓时长。回调可读取债务,将利息作为负 cash_earnings 扣除,即使当天没有交易。这里积极杠杆用 500 美元借款支持 1,000 美元持仓。示例每日费率 0.1%,入场后每日收取 0.50 美元:

Charge interest on borrowed cash each day
close = pd.Series([100.0, 100.0, 100.0], index=pd.date_range("2026-01-01", periods=3))

@njit
def financing_nb(ctx, cash_earnings, daily_rate):
    cash_earnings[ctx.i, ctx.col] = -ctx.last_debt[ctx.col] * daily_rate

pf = vbt.PF.from_signals(
    close,
    entries=[True, False, False],
    size=10,
    init_cash=1000,
    leverage=2,
    leverage_mode="eager",
    save_state=True,
    cash_earnings=np.zeros(3),
    adjust_func_nb=financing_nb,
    adjust_args=(vbt.Rep("cash_earnings"), 0.001),
)
pd.DataFrame({"debt": pf.debt, "financing": pf.cash_earnings, "value": pf.value})
             debt  financing   value
2026-01-01  500.0        0.0  1000.0
2026-01-02  500.0       -0.5   999.5
2026-01-03  500.0       -0.5   999.0

价格不变,因此净值变化完全来自融资。示例保留现金支付成本。负现金收益最多扣至自由现金,因此满仓账户无法支付它们。

永续合约资金费用方式相同:回调在每次资金结算时,将持仓价值乘以费率。按持仓方向设置支付正负号,并使用自己的费率序列和计划。VBT 将这些现金流计入投资组合净值与收益率。

期货与特殊品种

期货合约控制其报价的若干倍金额。multiplier 将该倍数应用于订单金额、现金流和利润,因此一份 E-mini 标普 500 合约每点对应 50 美元。价格也可为零或负数,价差和 2020 年 4 月原油期货都出现过这种情况。下方合约乘数与负价格使用真实数据展示两者。合约换月不会自动处理:可以对连续序列作向后调整(back-adjustment),或用自己的信号平掉到期合约、开立下一合约。

平仓时实现损益,未平仓头寸逐根按市值计价。交易所每日将未实现利润转入现金的逐日盯市保证金,不单独建模。多数策略研究中,最终净值曲线相同,但持仓期间现金余额会与期货账户结单不同。

多币种

现金与价格必须使用同一货币。单一货币对应以账户货币报价,必要时倒转货币对,并同时转换订单价格数组。海外股票等持仓,应在模拟前将各价格序列换算为账户货币,这会将汇率暴露纳入各资产净值。独立币种余额、兑换成本及杠杆外汇损益换算需要自定义记账。

合约乘数

✅ 所有模拟入口现在都接受乘数,按固定系数缩放每个订单,便于模拟期货及其他一份合约代表多个标的单位的衍生品。

SMA crossover on E-mini S&P 500 futures
data = vbt.YFData.pull("ES=F", start="2023", end="2024")  

fast_sma = data.run("talib_func:sma", timeperiod=10)  
slow_sma = data.run("talib_func:sma", timeperiod=30)
entries = fast_sma.vbt.crossed_above(slow_sma)
exits = fast_sma.vbt.crossed_below(slow_sma)

pf_stock = vbt.PF.from_signals(  
    data,
    entries=entries,
    exits=exits,
    size=1,
    init_cash=500_000,
)

pf_futures = vbt.PF.from_signals(  
    data,
    entries=entries,
    exits=exits,
    size=1,
    multiplier=50,
    init_cash=500_000,
)

print(pf_stock.total_profit)
627.25
print(pf_futures.total_profit)  
31362.5
print(pf_futures.trades.readable[["Avg Entry Price", "Avg Exit Price", "PnL", "Return"]])
   Avg Entry Price  Avg Exit Price      PnL    Return
0          4057.50         4138.00   4025.0  0.019840
1          4212.00         4480.75  13437.5  0.063806
2          4490.25         4378.75  -5575.0 -0.024832
3          4430.50         4820.00  19475.0  0.087913

负价格

✅ 模拟引擎此前在验证时拒绝负价格,无法模拟价格确实可能低于零的品种,例如著名的 2020 年 4 月 WTI 原油事件。现在整个流程已全面支持负价格。

Hold WTI crude oil through its negative close in April 2020
data = vbt.YFData.pull("CL=F", start="2020-02-01", end="2020-07-01")  

entries = pd.Series(False, index=data.index)  
exits = pd.Series(False, index=data.index)
entries["2020-03-03"] = True
exits["2020-05-04"] = True

pf = vbt.PF.from_signals(  
    data,
    entries=entries,
    exits=exits,
    size=1,
    multiplier=1000,
    init_cash=200_000,
)

print(data.close[data.close < 0])  
Date
2020-04-20 00:00:00-04:00   -37.630001
Name: Close, dtype: float64
print(pf.trades.readable[["Avg Entry Price", "Avg Exit Price", "PnL", "Return"]])
   Avg Entry Price  Avg Exit Price           PnL    Return
0            47.18       20.389999 -26790.000916 -0.567825
print(pf.value[["2020-03-03", "2020-03-09", "2020-04-20", "2020-05-04"]].round(2))  
Date
2020-03-03 00:00:00-05:00    200000.0
2020-03-09 00:00:00-04:00    183950.0
2020-04-20 00:00:00-04:00    115190.0
2020-05-04 00:00:00-04:00    173210.0
Name: value, dtype: float64

杠杆

✅ 杠杆现已成为投资组合模拟的一部分。支持两种模式:lazy(现金不足时才启用)和 eager(使用部分可用现金,同时启用杠杆)。可逐笔订单设置杠杆,还能自动确定满足订单要求的最佳杠杆值!🏋️

Explore how leverage affects the equity curve in a random portfolio
data = vbt.YFData.pull("BTC-USD", start="2020", end="2022")
pf = vbt.PF.from_random_signals(
    data,
    n=100,
    seed=42,
    leverage=vbt.Param([0.5, 1, 2, 3]),
)
pf.value.vbt.plot().show()
2020 至 2021 年 BTC-USD 随机投资组合在四种杠杆水平下的净值曲线 图表数据(JSON)

版权所有 © 2021–2026 Oleg Polakow。保留所有权利。

本站内容与文档供使用和评估 VectorBT PRO 以及教育用途。任何其他用途,包括开发或支持竞争产品或服务,均须事先取得书面同意。