保证金交易

到目前为止,我们的回测一直局限于纯现金交易。本文档将探讨 PyBroker v2 中的保证金交易功能。我们将使用 StrategyConfig 为购买力设置杠杆、计算借入资金的利息,并为空头仓位设置抵押品。

[1]:
import pybroker
from pybroker import Strategy, StrategyConfig, YFinance, sumv

pybroker.enable_data_source_cache("margin_trading")
[1]:
<pybroker.cache._L1Cache at 0x7fed5414fc80>

配置杠杆

保证金通过 leverage 配置项启用,它会为多头和空头仓位放大购买力。

[2]:
config = StrategyConfig(initial_cash=100_000, leverage=2.0)

默认值 1.0 表示仅使用现金买入,将 leverage 设置为 2.0 则允许持有价值最高达自身权益 2 倍的仓位,每个仓位中借入的那一半会作为保证金贷款进行跟踪。PyBroker 不模拟追缴保证金,订单只会在成交时受到可用购买力的限制。

接下来,我们编写一个简单的趋势跟踪策略,只要某个股票代码的收盘价高于其 50 日移动平均线,就持有该股票代码:

[3]:
sma_50 = pybroker.indicator("sma_50", lambda data: sumv(data.close, 50) / 50)


def trend_follow(ctx):
    sma = ctx.indicator("sma_50")[-1]
    if ctx.long_pos() is None and ctx.close[-1] > sma:
        ctx.buy_shares = ctx.calc_target_shares(0.25)
    elif ctx.long_pos() is not None and ctx.close[-1] < sma:
        ctx.sell_all_shares()

杠杆正是在仓位规模计算中发挥作用的地方。ctx.calc_target_shares 方法会按可部署资本的一定比例来确定订单规模,而这一资本等于你的总权益乘以 leverage

因此,在使用保证金交易时,对四只股票各设定 25% 的目标配置,最终部署的资金最多可达自身权益的约 2 倍:

[4]:
yfinance = YFinance()
strategy = Strategy(
    yfinance, start_date="1/1/2025", end_date="8/1/2026", config=config
)
strategy.add_execution(
    trend_follow, ["GS", "MS", "C", "USB"], indicators=sma_50
)
result_2x = strategy.backtest()
result_2x.portfolio.tail()
Backtesting: 2025-01-01 00:00:00 to 2026-08-01 00:00:00

Loading bar data...
[*********************100%***********************]  4 of 4 completed
Loaded bar data: 0:00:00

Computing indicators...
100% (4 of 4) |##########################| Elapsed Time: 0:00:00 Time:  0:00:00

Test split: 2025-01-02 00:00:00 to 2026-07-31 00:00:00
100% (395 of 395) |######################| Elapsed Time: 0:00:00 Time:  0:00:00

Finished backtest: 0:00:01
[4]:
cash equity notional margin margin_loan net_cash_balance market_value pnl unrealized_pnl fees
date
2026-07-27 30108.36 208216.89 311083.77 0.0 132975.24 -102866.88 208216.89 108216.89 0.0 0.0
2026-07-28 30108.36 207216.77 310083.65 0.0 132975.24 -102866.88 207216.77 107216.77 0.0 0.0
2026-07-29 135311.71 199685.90 115525.98 0.0 51151.79 84159.92 199685.90 99685.90 0.0 0.0
2026-07-30 135311.71 199833.02 115673.10 0.0 51151.79 84159.92 199833.02 99833.02 0.0 0.0
2026-07-31 135311.71 200035.31 115875.39 0.0 51151.79 84159.92 200035.31 100035.31 0.0 0.0

result.portfolio 会在每根 K 线记录保证金余额。开立杠杆仓位时,一部分现金(entry cost / leverage)会作为抵押品被占用。借入的剩余部分会在 margin_loan 中进行跟踪,而 net_cash_balance 等于 cash - margin_loan

margin 列则是独立的,它跟踪未平仓空头仓位的当前价值,因此在只做多的策略中始终为零。

为了实际观察这些保证金机制,我们对输出进行筛选,仅显示存在未偿还贷款的 K 线:

[5]:
levered = result_2x.portfolio[result_2x.portfolio["margin_loan"] > 0]
levered.head()
[5]:
cash equity notional margin margin_loan net_cash_balance market_value pnl unrealized_pnl fees
date
2025-05-02 74586.70 100122.40 50949.00 0.0 25413.30 49173.40 100122.40 122.40 0.0 0.0
2025-05-05 24856.76 99627.67 149914.16 0.0 75143.25 -50286.49 99627.67 -372.33 0.0 0.0
2025-05-06 24856.76 97635.42 147921.91 0.0 75143.25 -50286.49 97635.42 -2364.58 0.0 0.0
2025-05-07 24856.76 98739.16 149025.65 0.0 75143.25 -50286.49 98739.16 -1260.84 0.0 0.0
2025-05-08 24856.76 102121.35 152407.84 0.0 75143.25 -50286.49 102121.35 2121.35 0.0 0.0

在这些筛选出的 K 线中的第一根上,成交了一笔入场交易。现金减少了 $25,413,用于将入场成本的一半作为抵押品,而 margin_loan 列则记录了借入的剩余部分。抵押品与保证金贷款按入场价格固定不变,而 notional 列会在每根 K 线收盘时跟踪该仓位的当前市值。

与纯现金交易进行比较

为了观察杠杆对策略的影响,我们将 leverage 设置为纯现金的 1.0 并重新运行策略。我们还会使用 disable_logging 禁用 PyBroker 的日志记录,以保持后续运行的输出简洁:

[6]:
pybroker.disable_logging()


def run_backtest(
    config, exec_fn=trend_follow, symbols=("GS", "MS", "C", "USB")
):
    strategy = Strategy(
        yfinance, start_date="1/1/2025", end_date="8/1/2026", config=config
    )
    strategy.add_execution(exec_fn, symbols, indicators=sma_50)
    return strategy.backtest()


result_1x = run_backtest(StrategyConfig(initial_cash=100_000))
print(f"1x total return: {result_1x.metrics.total_return_pct:.2f}%")
print(f"2x total return: {result_2x.metrics.total_return_pct:.2f}%")
print(f"1x max drawdown: {result_1x.metrics.max_drawdown_pct:.2f}%")
print(f"2x max drawdown: {result_2x.metrics.max_drawdown_pct:.2f}%")
1x total return: 41.20%
2x total return: 86.46%
1x max drawdown: -10.82%
2x max drawdown: -19.91%

收取保证金利息

杠杆使回报率提高了一倍多(最大回撤也几乎翻倍),但借入资金并非没有成本。你可以使用 interest_rate 配置项来模拟这一融资成本:它会对投资组合的净现金余额按年利率计息,并以 interest_rate / bars_per_year 的比例按每根 K 线计提一次。要使用此功能,你还必须设置 bars_per_year

当净现金为负(即保证金贷款超过现金)时会收取利息,当净现金为正时则会计入利息收益。为了观察这些成本对投资组合的影响,我们将回测一个买入并持有的策略。由于两次运行持有相同的仓位,它们最终市值的任何差额都是所支付的利息:

[7]:
def buy_and_hold(ctx):
    if ctx.long_pos() is None:
        ctx.buy_shares = ctx.calc_target_shares(0.25)


config_interest = StrategyConfig(
    initial_cash=100_000,
    leverage=2.0,
    interest_rate=6.0,
    bars_per_year=252,
)
result_hold = run_backtest(config, buy_and_hold)
result_interest = run_backtest(config_interest, buy_and_hold)
print(
    "Final market value (no interest):",
    result_hold.portfolio["market_value"].iloc[-1],
)
print(
    "Final market value (6% interest):",
    result_interest.portfolio["market_value"].iloc[-1],
)
Final market value (no interest): 231518.52
Final market value (6% interest): 221770.63

观察投资组合的末尾部分,cash 会始终保持为零。与此同时,计提的利息会被加到 margin_loan 中,并随着每根 K 线不断增加:

[8]:
result_interest.portfolio[
    ["cash", "margin_loan", "net_cash_balance", "market_value"]
].tail()
[8]:
cash margin_loan net_cash_balance market_value
date
2026-07-27 0.0 109727.59 -109727.59 227059.35
2026-07-28 0.0 109753.72 -109753.72 225082.19
2026-07-29 0.0 109779.85 -109779.85 211670.48
2026-07-30 0.0 109805.99 -109805.99 222003.37
2026-07-31 0.0 109832.13 -109832.13 221770.63

使用保证金做空

卖空同样会使用保证金。空头需要相当于 entry cost / leverage 的抵押品,并与多头共用同一份购买力。

为了演示这一点,我们将做空那些价格低于其移动平均线的股票代码。我们还会启用 record_position_bars 来捕获每个仓位各自的余额:

[9]:
def trend_short(ctx):
    sma = ctx.indicator("sma_50")[-1]
    if ctx.short_pos() is None and ctx.close[-1] < sma:
        ctx.sell_shares = ctx.calc_target_shares(0.25)
    elif ctx.short_pos() is not None and ctx.close[-1] > sma:
        ctx.cover_all_shares()


config_short = StrategyConfig(
    initial_cash=100_000, leverage=2.0, record_position_bars=True
)
result_short = run_backtest(
    config_short, trend_short, ["HD", "LOW", "CMCSA", "KHC"]
)
shorted = result_short.portfolio[result_short.portfolio["margin"] > 0]
shorted[
    [
        "cash",
        "equity",
        "margin",
        "margin_loan",
        "net_cash_balance",
        "market_value",
    ]
].head()
[9]:
cash equity margin margin_loan net_cash_balance market_value
date
2025-03-18 25446.39 100000.00 148914.57 74553.61 -49107.22 100192.65
2025-03-19 50198.67 99686.68 99669.93 49488.01 710.66 98992.77
2025-03-20 25260.35 99686.68 149658.35 74426.33 -49165.98 98880.99
2025-03-21 25260.35 99686.68 148904.46 74426.33 -49165.98 99634.88
2025-03-24 25260.35 99686.68 151303.30 74426.33 -49165.98 97236.04

对于未平仓的空头仓位,margin 列会跟踪它们当前的名义敞口,在使用杠杆时,这一敞口可能超过你的总权益。

开立空头仓位时,会从你的现金中扣留相当于 entry cost / leverage 的抵押品,而 margin_loan 列则记录借入的剩余部分。由于抵押品与贷款在入场时即已固定,它们不会随 margin 列变化。

你的 net_cash_balance 等于剩余现金减去这笔保证金贷款,一旦贷款超过可用现金,其数值就会变为负数。请注意,equity 按固定的入场成本为空头仓位估值,而 market_value 则包含了它们的未实现盈亏。

由于我们启用了 record_position_barsresult.positions 会跟踪每个单独仓位的余额。这包括每个空头仓位在投资组合 margin 中所占的具体份额,以及它自身的未实现盈亏:

[10]:
result_short.positions[
    ["short_shares", "close", "margin", "unrealized_pnl"]
].head()
[10]:
short_shares close margin unrealized_pnl
symbol date
CMCSA 2025-03-18 1492 33.75 50353.25 -222.05
HD 2025-03-18 141 349.57 49289.37 164.97
LOW 2025-03-18 221 222.95 49271.95 249.73
HD 2025-03-19 141 353.42 49832.22 -377.88
LOW 2025-03-19 221 225.51 49837.71 -316.03