0. 说明
下面对这份 AH 溢价套利策略的关键要素做一个量化化说明:
1.策略逻辑(AHPremiumArbitrage)
- 思路:利用 A 股与 H 股在中国市场同一标的的价格差异(AH 溢价)进行套利。
- 轮动频率:按月度轮动——每月第一个交易日全部调仓一次。
- 选股规则:
- 卖出上月持仓。
- 从所有可交易标的中,分别挑出 AH 溢价最高的 5 支 H 股,以及溢价最低的 5 支 A 股。
- 将当日可用现金等分为 10 份,每份买入一只选中的标的。
2. 主要设置
- 初始资金:1 000 000 元
- 滑点:0.1%(由
cerebro.broker.set_slippage_perc(perc=0.001, …)
设置) - 调仓时机:策略的
next()
方法中,通过比较当前日期的(年, 月)
与上次执行的周期来决定是否调仓。 - 持仓跟踪:策略中维护
self.nav
与self.nav_dates
,实时记录策略净值与对应日期,便于后续绘图和绩效计算。
3.缺失值/停牌处理
.reindex(all_dates) # 补全所有业务日
df_feed[['open','high','low','close','premium']]\
.ffill() # 向前填充
.fillna(0) # 首次上市前或全部缺失时置零
df_feed['volume'] = df_feed['volume'].fillna(0) # 停牌日或停市时成交量为0
df_feed['listed'] = df_feed['close'].notna().astype(int) # 上市/停牌标记
- 策略执行时过滤:买入时只对
listed==1
且volume>0
的数据做选股,保证不会在未上市或停牌日下单。
4.交易成本
- A 股
- 佣金:0.01%(单边)
- 印花税:0.05%(仅卖方承担)
- 通过自定义
CommInfoA.getcommission()
方法实现。
- H 股
- 佣金:0.03%(单边)
- 印花税:0.05%(买卖双方都缴纳)
- 通过自定义
CommInfoH.getcommission()
方法实现。
- 滑点
- 全局设置
0.1%
,同时对开盘价、限价单、成交价都生效。
- 全局设置
5.绩效分析
- 分析器
SharpeRatio
(日频)TimeReturn
(年化回报)DrawDown
(最大回撤及最长回撤期)
- 净值曲线
- 在策略内
next()
中持续收集broker.getvalue()
,可用于绘制总资产随时间的走势曲线。
- 在策略内
- 年度回报和年末净值
- 从
TimeReturn
分析器读取各年度回报率,再结合self.nav
和self.nav_dates
获取各年最后一日的资产值。
- 从
通过以上设置,这个月度轮动的 AH 溢价套利策略能够:
- 自动规避停牌/未上市标的;
- 量化考量交易成本与滑点;
- 结合多种分析器对夏普率、年度收益、回撤等进行全方位绩效评估;
- 输出详细的买卖信号与交易闭合日志,便于事后复盘和审计。
1. 代码
import pandas as pd
import backtrader as bt
from datetime import datetime
import matplotlib.pyplot as plt
# ——————————— 0. 读取 AH 数据 ———————————
ahp = pd.read_feather(r"data/AH_premium.feather")
# ——————————— 1. 预处理日期 ———————————
ahp["trade_date"] = pd.to_datetime(
ahp["trade_date"].astype(str), format="%Y%m%d"
)
ahp.sort_values("trade_date", inplace=True)
earliest = ahp["trade_date"].min()
latest = ahp["trade_date"].max()
all_dates = pd.date_range(start=earliest, end=latest, freq="B")
# ——————————— 2. 自定义数据源 Feed ———————————
class CustomPandasData(bt.feeds.PandasData):
lines = ('listed', 'premium')
params = (
('listed', -1),
('premium', -1),
)
# ——————————— 3. 佣金与印花税设置 ———————————
class CommInfoA(bt.CommissionInfo):
params = dict(
commission=0.0001,
stamp=0.0005,
stocklike=True,
commtype=bt.CommissionInfo.COMM_PERC
)
def getcommission(self, size, price, pseudoexec=False):
cost = abs(size) * price * self.p.commission
if size < 0:
cost += abs(size) * price * self.p.stamp
return cost
class CommInfoH(bt.CommissionInfo):
params = dict(
commission=0.0003,
stamp=0.0005,
stocklike=True,
commtype=bt.CommissionInfo.COMM_PERC
)
def getcommission(self, size, price, pseudoexec=False):
cost = abs(size) * price * self.p.commission
cost += abs(size) * price * self.p.stamp
return cost
# ——————————— 4. 策略定义:AH 溢价套利 ———————————
class AHPremiumArbitrage(bt.Strategy):
def __init__(self):
self.last_period = (None, None)
self.nav = []
self.nav_dates = []
def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
return
if order.status == order.Completed:
action = 'BUY' if order.isbuy() else 'SELL'
print(f"{action} EXECUTED: {order.data._name}, Price: {order.executed.price:.2f}, Size: {order.executed.size}")
else:
print(f"Order {order.getordername()} on {order.data._name} Canceled/Margin/Rejected")
def notify_trade(self, trade):
if trade.isclosed:
print(f"TRADE CLOSED: {trade.data._name}, Gross PnL: {trade.pnl:.2f}, Net PnL: {trade.pnlcomm:.2f}")
def next(self):
# 将 datetime 换为纯 date 对象以便后续匹配
dt = self.datas[0].datetime.date(0)
self.nav_dates.append(dt)
self.nav.append(self.broker.getvalue())
period = (dt.year, dt.month)
if period != self.last_period:
# 卖出所有持仓
for d in self.datas:
pos = self.getposition(d).size
if pos:
print(f"SELL SIGNAL: {d._name} at Close {d.close[0]:.2f} on {dt}")
self.sell(data=d, size=pos, exectype=bt.Order.Market, coo=True)
# 可交易标的筛选
h_list, a_list = [], []
for d in self.datas:
if d.listed[0] and d.volume[0] > 0:
if d._name.endswith('_H'):
h_list.append((d.premium[0], d))
else:
a_list.append((d.premium[0], d))
# 选股
h_sel = sorted(h_list, key=lambda x: x[0], reverse=True)[:5]
a_sel = sorted(a_list, key=lambda x: x[0])[:5]
# 等额买入,滑点已全局设置 0.1%
cash = self.broker.getcash()
alloc = cash / 10
for _, d in h_sel + a_sel:
price = d.open[0]
size = int(alloc / price) if price > 0 else 0
if size:
print(f"BUY SIGNAL: {d._name} at Open {price:.2f} on {dt}")
self.buy(data=d, size=size, exectype=bt.Order.Market, coo=True)
self.last_period = period
def stop(self):
# 同样用 date 对象记录最终净值
dt = self.datas[0].datetime.date(0)
self.nav_dates.append(dt)
self.nav.append(self.broker.getvalue())
if __name__ == '__main__':
cerebro = bt.Cerebro(cheat_on_open=True)
cerebro.broker.setcash(1_000_000.0)
cerebro.broker.set_slippage_perc(perc=0.001, slip_open=True, slip_limit=True, slip_match=True)
# 添加分析器
cerebro.addanalyzer(bt.analyzers.SharpeRatio, timeframe=bt.TimeFrame.Days, _name='sharpe')
cerebro.addanalyzer(bt.analyzers.TimeReturn, timeframe=bt.TimeFrame.Years, compression=1, _name='yearlyreturn')
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
# 数据加载
def load_data(prefix, ts_col):
for code in ahp[ts_col].unique():
df = ahp[ahp[ts_col] == code].copy()
df.set_index('trade_date', inplace=True)
df_feed = df[[
f'open_adj_{prefix}', f'high_adj_{prefix}', f'low_adj_{prefix}',
f'close_adj_{prefix}', f'vol_{prefix}', 'AH_close_premium'
]].rename(columns={
f'open_adj_{prefix}':'open', f'high_adj_{prefix}':'high',
f'low_adj_{prefix}':'low', f'close_adj_{prefix}':'close',
f'vol_{prefix}':'volume', 'AH_close_premium':'premium'
}).reindex(all_dates)
df_feed['listed'] = df_feed['close'].notna().astype(int)
df_feed[['open','high','low','close','premium']] = df_feed[['open','high','low','close','premium']].ffill().fillna(0)
df_feed['volume'] = df_feed['volume'].fillna(0)
name = df.get('name', pd.Series([code])).iat[0]
datafeed = CustomPandasData(dataname=df_feed, name=f"{name}_{prefix}")
cerebro.adddata(datafeed)
# 佣金设置
cerebro.broker.addcommissioninfo(CommInfoA() if prefix=='A' else CommInfoH())
load_data('A', 'ts_code_A')
load_data('H', 'ts_code_H')
cerebro.addstrategy(AHPremiumArbitrage)
print(f"Starting Portfolio Value: {cerebro.broker.getvalue():.2f}")
strat = cerebro.run()[0]
print(f"Final Portfolio Value: {cerebro.broker.getvalue():.2f}")
# 输出绩效
sr = strat.analyzers.sharpe.get_analysis().get('sharperatio')
print(f"Sharpe Ratio: {sr:.2f}" if sr else "Sharpe Ratio: N/A")
yr = strat.analyzers.yearlyreturn.get_analysis()
nav = strat.nav
nav_dates = strat.nav_dates
print("Annual Returns and Year-End Value:")
for year_dt, ret in sorted(yr.items()):
try:
idx = nav_dates.index(year_dt)
end_val = nav[idx]
except ValueError:
end_val = float('nan')
print(f" {year_dt.year}: Return {ret*100:.2f}%, Year-End Value {end_val:.2f}")
dd = strat.analyzers.drawdown.get_analysis().get('max', {})
print(f"Max Drawdown: {dd.get('drawdown',0):.2f}%")
print(f"Longest DD Duration: {dd.get('len',0)} bars")
2. 运行结果
能够成功打印历史交易行为和各种策略评估值
...
SELL SIGNAL: 中国联通_A at Close 5.26 on 2025-05-01
SELL SIGNAL: 中国重汽_A at Close 16.59 on 2025-05-01
SELL SIGNAL: 招商银行_A at Close 40.74 on 2025-05-01
SELL SIGNAL: 药明康德_A at Close 57.92 on 2025-05-01
SELL SIGNAL: 美的集团_A at Close 70.23 on 2025-05-01
SELL SIGNAL: 上海电气_H at Close 2.55 on 2025-05-01
SELL SIGNAL: 浙江世宝_H at Close 3.29 on 2025-05-01
SELL SIGNAL: 中国稀土_H at Close 0.39 on 2025-05-01
SELL SIGNAL: 复旦张江_H at Close 2.43 on 2025-05-01
SELL SIGNAL: 弘业期货_H at Close 2.34 on 2025-05-01
SELL EXECUTED: 中国联通_A, Price: 5.27, Size: -46
SELL EXECUTED: 中国重汽_A, Price: 17.39, Size: -12
SELL EXECUTED: 招商银行_A, Price: 41.60, Size: -5
SELL EXECUTED: 药明康德_A, Price: 59.44, Size: -3
SELL EXECUTED: 美的集团_A, Price: 71.47, Size: -3
SELL EXECUTED: 上海电气_H, Price: 2.55, Size: -97
SELL EXECUTED: 浙江世宝_H, Price: 3.22, Size: -76
SELL EXECUTED: 中国稀土_H, Price: 0.39, Size: -652
SELL EXECUTED: 复旦张江_H, Price: 2.42, Size: -100
SELL EXECUTED: 弘业期货_H, Price: 2.34, Size: -97
TRADE CLOSED: 中国联通_A, Gross PnL: -10.05, Net PnL: -10.44
TRADE CLOSED: 中国重汽_A, Gross PnL: -25.31, Net PnL: -25.66
TRADE CLOSED: 招商银行_A, Gross PnL: -6.57, Net PnL: -6.91
TRADE CLOSED: 药明康德_A, Gross PnL: -22.03, Net PnL: -22.33
TRADE CLOSED: 美的集团_A, Gross PnL: -1.43, Net PnL: -1.78
TRADE CLOSED: 上海电气_H, Gross PnL: -6.32, Net PnL: -6.72
TRADE CLOSED: 浙江世宝_H, Gross PnL: -5.50, Net PnL: -5.90
TRADE CLOSED: 中国稀土_H, Gross PnL: -6.52, Net PnL: -6.93
TRADE CLOSED: 复旦张江_H, Gross PnL: -22.51, Net PnL: -22.91
TRADE CLOSED: 弘业期货_H, Gross PnL: -30.55, Net PnL: -30.94
Final Portfolio Value: 1532157.33
Sharpe Ratio: 0.01
Annual Returns and Year-End Value:
2012: Return 2.54%, Year-End Value 1025428.06
2013: Return -12.87%, Year-End Value 893414.49
2014: Return -1.19%, Year-End Value 882795.35
2015: Return 6.41%, Year-End Value 939403.78
2016: Return 21.46%, Year-End Value 1140994.00
2017: Return 2.04%, Year-End Value 1164264.34
2018: Return -18.56%, Year-End Value 948194.14
2019: Return 10.75%, Year-End Value 1050094.54
2020: Return 22.96%, Year-End Value 1291215.56
2021: Return 8.75%, Year-End Value 1404163.10
2022: Return 11.04%, Year-End Value 1559246.09
2023: Return -5.00%, Year-End Value 1481300.93
2024: Return 1.89%, Year-End Value 1509248.71
2025: Return 1.52%, Year-End Value 1532157.33
Max Drawdown: 26.00%
Longest DD Duration: 707 bars