POLOXUE's BLOG

POLOXUE's BLOG

08 Mar 2025

期货回测避坑-基于 tushare 计算期货复权价格

本周写点国内期货的内容,介绍如何通过 Python 计算期货主连合约的复权数据。之所以要处理这个数据,是因为它会影响策略(如 CTA 策略)的回测结,是个大坑。新手要理解下期货与股票的差异。

为便于后续使用,我将本文内容整合成了一个期货复权数据下载命令,如下是它的帮助信息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Usage: main.py [OPTIONS]

Options:
  --ts-code TEXT    期货合约代码(例如:LH.DCE 表示生猪期货)  [必填]
  --start-date TEXT 起始日期(格式:YYYYMMDD,默认为20220101)

  --adjust          复权方式(默认:forward): 
                    forward: 前复权(以当前价格为基准调整历史数据)
                    backward: 后复权(保持历史价格不变调整未来数据)
  --method          换月调整方法(默认:pre_close/pre_close):
                    pre_close/pre_close: 使用前收盘价复权
                    open/pre_close: 使用开盘价/前收盘价复权
                    pre_settle/pre_settle: 使用前结算价复权
                    open/open: 使用开盘价复权

  --no-download     是否禁用复权数据下载(默认下载)
  --plot            是否显示价格曲线图(默认不显示)
  --help            Show this message and exit.

支持了常见的主连合约的复权方法,先给大家效果,直观看看考虑复权和不复权价格数据的差异。

以生猪为例:

1
python main.py --ts-code LH.DCE --method pre_close/pre_close --plot

后复权价格图如下:

如果你想做期货的量化策略,特别是 CTA 策略,用主连价格回测,与实盘交易差异将会很大。

希望这能助新手避坑。

这个脚本还会直接将复权数据下载到本地,便于后续回测使用。

1
2
$ ls
LH.DCE_backward_pre_close-pre_close.csv

数据样例:

1
2
3
4
5
6
7
           ts_code  adj_open  adj_high  adj_low  adj_close
date
2021-01-08  LH.DCE   29500.0   30680.0  26385.0    26810.0
2021-01-11  LH.DCE   26225.0   26720.0  26030.0    26030.0
2021-01-12  LH.DCE   25760.0   26210.0  25205.0    25560.0
2021-01-13  LH.DCE   25830.0   26350.0  25715.0    25870.0
2021-01-14  LH.DCE   25805.0   26155.0  24860.0    24890.0

前复权也支持。

1
python main.py --ts-code LH.DCE --adjust forward --method open/pre_close --plot 

价格图如下:

如果没有 Tushare 数据,文中也提供了关于如何自定义切换规则的一些思路和代码。或者联系我拿 Tushare 权限,有 20% 的折扣。如果是临时需要,我可以免费提供一个品种的复权数据。

如果不想看完整文章,直接看文件代码地址:download_future_adj_data.py。这个代码是完整版,文章重点是介绍实现逻辑。

背景概述

最近希望合理分配资金、对冲风险,就计划测试些适用于国内市场的策略,如套利、CTA 策略,但 A 股不支持做空机制,于是转而选择了期货市场。

但问题是,期货不同于股票,不是一个品种一个可交易标的,而是由连续的交割合约组成,即每个合约都有到期日,而同一品种的不同月合约都是有价差的。

如下是玻璃2505和2509的合约的价格曲线:

查看交易软件上的主合约价格图表,如下图所示是生猪的主力合约的价格图:

你会发现,这其中存在一些明显的跳空。这些跳空基本就是主连合约切换合约时产生的。

如果是短线交易或者跨期套利,这个问题或许不是那么重要。但我还计划测试期货的 CTA 策略。如果用这个数据回测策略,和实盘将有会很大差异,因为那些跳空是不同合约的价差,是实际交易无法拿到的收益。

如何解决?

答案就是计算复权数据,通过复权计算买入持有的收益曲线,将换仓时的价差尽可能的考虑在内。

熟悉股票交易的人都知道,交易软件或数据平台会提供股票的复权价格(如分红、拆股后的调整数据),以消除公司行为导致的历史价格断裂,确保长期收益计算的准确性。

但期货不同,交易软件和数据平台通常不会提供这个数据。

为什么呢?

所谓的主连合约是基于某个规则拼接而成,复权并非强制需求。根据策略目标不同,可自行选择换月规则,如固定日期、持仓量切换等。

散户能接触到的数据源通常只提供主连合约的原始价格数据。听过付费平台(如 Wind)好像有定制的主连合约,也提供了相应的复权因子。

那我这个小散该怎么办?

当然是自己计算了。走独立自主的发展道路,还可以根据策略目标制定不同的换仓时间和复权计算的方法。


计算期货复权数据

正式进入主题,本文我将介绍如何基于 tushare 计算主连合约的复权数据。

核心有两部分:

  • 一是拿到主连合约与实际合约的映射;
  • 二是计算复权因子;

准备工作

下载安装将用到的几个 Python 库。

1
pip install tushare pandas matplotlib click tqdm

这里用到了一个不常用的库 - tqdm,它是用来展示数据下载进度的,特别当下载数量非常大时,这个小技巧能提升你的体验甚至效率。

简单示例:

1
2
3
4
5
import time
from tqdm import tqdm

for _ in tqdm(range(100)):
    time.sleep(0.1)

效果如下:

1
100%|██████████████████████████| 100/100 [00:01<00:00, 86.14it/s]

导入所有库,配置认证 tushare 的 token。

1
2
3
4
5
import tushare as ts
import pandas as pd
from tqdm import tqdm

pro = ts.pro_api("你的 Tushare Pro Token")

映射数据

映射数据从哪里拿?

我可以直接从数据提供商那里获取。如果能明确换仓规则,也可自己计算,即我知道如主连合约的底层合约何时从一个换到另一个。

标准映射数据

先说下如何拿到标准主连的映射关系,也就是如同花顺等交易软件上一样的主连合约规则,通过 tushare 的 fut_mapping 能直接拿到。我能接触到的数据源中,只在 tushare 看到了这个标准规则的映射关系。

如获取生猪的主连合约从2022年1月1日到现在与底层合约的映射,示例代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
mapping_data = pro.fut_mapping(
  ts_code="LH.DCE",
  start_date="20220101",
)
mapping_data.sort_values(
  by="trade_date",
  inplace=True,
  ignore_index=True,
)
print(mapping_data)

输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
    ts_code trade_date mapping_ts_code
0    LH.DCE   20220104      LH2203.DCE
1    LH.DCE   20220105      LH2203.DCE
2    LH.DCE   20220106      LH2203.DCE
3    LH.DCE   20220107      LH2203.DCE
4    LH.DCE   20220110      LH2203.DCE
..      ...        ...             ...
762  LH.DCE   20250303      LH2505.DCE
763  LH.DCE   20250304      LH2505.DCE
764  LH.DCE   20250305      LH2505.DCE
765  LH.DCE   20250306      LH2505.DCE
766  LH.DCE   20250307      LH2505.DCE

如上的 mappint_ts_code 主连合约当前的底层合约。

我没有去深究这个规则,最常见的连续合约就是主力连续,主力合约似乎是以持仓为判断标准编织,还有其他的连续合约,如按固定日期切换,复权,最小价差等。

假设,Tushare 还提供了一个生猪连续合约的映射关系,在 LH 后加上 L,即 LHL,它的代码规则是 主力合约代码+L。如果想要所有的合约信息,可通过 tushare 的 fut_basic 接口拿到合约的基础信息。

自定义换仓规则

如果无法拿到这个标准的映射数据,可自定义一个尽量接近于标准的换仓规则。

我定义了一个固定日期换仓规则:

每月 28 号换仓到下一个合约,要求交割日期不少于 40 天。我将其封装成了一个函数,其中用到了 tushare 的 fut_basic 合约列表接口、trade_cal 交易日历接口。

函数定义:

1
def fut_mapping(fut_code, exchange)

这是个简单例子,但在文章中介绍还是有点繁琐,就不展开了。请查看 custom_fut_mapping.py

调用示例:

1
2
mapping_data = fut_mapping("LH", exchange="DCE")
print(mapping_map)

输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
      trade_date ts_code mapping_ts_code
0       20210108     LHC      LH2109.DCE
1       20210111     LHC      LH2109.DCE
2       20210112     LHC      LH2109.DCE
3       20210113     LHC      LH2109.DCE
4       20210114     LHC      LH2109.DCE
...          ...     ...             ...
1001    20250303     LHC      LH2507.DCE
1002    20250304     LHC      LH2507.DCE
1003    20250305     LHC      LH2507.DCE
1004    20250306     LHC      LH2507.DCE
1005    20250307     LHC      LH2507.DCE

其中的 LHC 是我随意定义的表示这个规则的生猪合约代码。

不知道这个代码有没有参考价值。

如果你没有途径拿到标准的主连合约的映射关系,这个或许可参考,将 tushare 替换为你的数据源即可。你可以修改规则,如按最大持仓,价差最小等等,计算这些规则所需的数据基本都是公开可获取的。

查询换仓详情

如现在我想查看换仓详情,只要记录上个交易日的 mapping_ts_code,比较前后 mapping_ts_code 是否相同。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 之前的合约
mapping_data["pre_mapping_ts_code"] = mapping_data["mapping_ts_code"].shift(1)
# 当前合约和之前合约不同且之前合约不为空
mapping_data["rollover"] = (
  (mapping_data["mapping_ts_code"] != mapping_data["pre_mapping_ts_code"]) &
  mapping_data["pre_mapping_ts_code"].notna()
)
print(mapping_data[mapping_data["rollover"]][
  ["ts_code", "trade_date", "mapping_ts_code", "pre_mapping_ts_code]
])

输出:

1
2
3
4
5
6
7
8
    ts_code trade_date mapping_ts_code pre_mapping_ts_code
20   LH.DCE   20220208      LH2205.DCE           LH2203.DCE
68   LH.DCE   20220419      LH2209.DCE           LH2205.DCE
143  LH.DCE   20220808      LH2301.DCE           LH2209.DCE
..      ...        ...             ...
667  LH.DCE   20241010      LH2501.DCE           LH2411.DCE
714  LH.DCE   20241216      LH2503.DCE           LH2501.DCE
747  LH.DCE   20250210      LH2505.DCE           LH2503.DCE

从上面就能看出来,合约是哪天(trade_date)从哪个合约(pre_mapping_ts_code)切换到当前的合约(mapping_ts_code)。

计算复权数据

到此,已经拿到了最难获取的主连合约映射关系,开始计算复权数据。

这块的核心是计算复权因子。有了复权因子,接着只要将复权因子作用到原始价格上即可拿到复权价格数据。

复权方法

常见的复权因子公式有哪些?

  • open/pre_close,即当前合约的开盘价与上个合约收盘价的比值
  • pre_close/pre_close,即当前合约和上个合约收盘价的比值;
  • pre_settle/pre_settle,即当前合约与上个合约结算价的比值;
  • open/open,即当前合约和上个合约的开盘价的比值;

如上统一采用的是比值计算方法,只是计算所用的价格不同。这篇文章会介绍前三种方法的实现,第四种 open/open 在脚本也实现了,就不再文章里展开了。

接下来具体介绍这四个复权因子的计算方法。

open/pre_close

open/pre_close 是最简单的复权因子计算方法,因为在矩阵计算中,这最容易实现,不少文章就写了这种实现。对应到实际操作,即在合约切换收盘日平仓,次日在新合约重新开仓即可。

计算复权因子,要先拿到价格数据。

如果你用的 tushare 作为数据源,直接调用 fut_daily 就能拿到主连的日线数据,通过 open/pre_close 即可。不过为了与接下来其他计算方式的统一,我还是通过调取实际合约的价格,将它们与主连合约重新拼接得到主连的价格。

为了拿到实际合约的价格,我要知道每个合约在主连合约上出现的开始和结束时间。

1
2
3
4
def extract_date_range(group):
    return group["trade_date"].min(), group["trade_date"].max()
date_ranges = mapping_data.groupby("mapping_ts_code").apply(extract_date_range)
print(date_ranges)

输出:

1
2
3
4
5
6
7
8
9
mapping_ts_code
LH2109.DCE    (20210108, 20210818)
LH2201.DCE    (20210819, 20211210)
LH2203.DCE    (20211213, 20220207)
...           ...
LH2501.DCE    (20241010, 20241213)
LH2503.DCE    (20241216, 20250207)
LH2505.DCE    (20250210, 20250307)
dtype: object

现在基于 ts_codedate_rangefut_daily 获取接口数据。

1
2
3
4
5
6
7
all_ohlcvs = []
for ts_code, (start_date, end_date) in tqdm(date_ranges.items()):
    ohlcvs = pro.fut_daily(ts_code=ts_code, start_date=start_date, end_date=end_date)
    if not ohlcvs.empty:
        all_ohlcvs.append(ohlcvs)

ohlcv_data = pd.concat(all_ohlcvs, ignore_index=True)

ohlcv_data mapping_data 合并就能得到主连合约完整的数据了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
data = mapping_data.merge(
    ohlcv_data,
    left_on=["mapping_ts_code", "trade_date"],
    right_on=["ts_code", "trade_date"],
    how="left",
    suffixes=("", "_1"),
)

data.drop(columns=["ts_code_1"], inplace=True)
print(data[["ts_code", "trade_date", "open", "pre_close_before"]])

输出:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
    ts_code trade_date     open  pre_close
0    LH.DCE   20220104  14320.0    14450.0
1    LH.DCE   20220105  14165.0    14165.0
2    LH.DCE   20220106  14250.0    14245.0
3    LH.DCE   20220107  14150.0    14075.0
4    LH.DCE   20220110  13670.0    13915.0
..      ...        ...      ...        ...
762  LH.DCE   20250303  12950.0    12935.0
763  LH.DCE   20250304  13100.0    13125.0
764  LH.DCE   20250305  13160.0    13195.0
765  LH.DCE   20250306  13055.0    13070.0
766  LH.DCE   20250307  13100.0    13090.0

复权因子的计算是 open/pre_close 即可,如果是后复权的话,要取倒数,即 pre_close/open。

这里还有有个小问题,默认的 pre_close 是当前映射合约上个交易日的收盘价,不是上个映射合约的上个交易日的收盘价。

一行代码解决:

1
data["pre_close_before"] = data["close"].shift(1)

计算复权因子,以后复权为例。

1
2
3
4
5
# 换仓时的数据
rollover_data = data[data["rollover"]]
# 计算复权因子
data["rollover_factor"] = rollover_data["open"] / rollover_data["pre_close_before"]
data["adj_factor"] = (1/data["rollover_factor"]).fillna(1).cumprod()

后复权的 adj_factor 是复权因子的倒数,这里要处理下。

计算复权价格并绘制收盘价:

1
2
data["adj_close"] = data["close"] * data["adj_factor"]
data[["adj_close", "close"]].plot()

绘制价格图表:

1
2
3
4
data['trade_date'] = pd.datetime(data['trade_date'])
data.set_index("trade_date", inplace=True)
data.index.name = "date"
plt.show()

绘图如下:

复权价格和实际的价格差异还是很大。

pre_close/pre_close 和 pre_settle/pre_settle

pre_close/pre_close 和 pre_settle/pre_settle 的复权因子的计算也很简单了。因为这里是手动拼接的价格数据,当前合约和上个合约的价格上个交易日的价格都很容易拿到。

pre_close/pre_close:

1
2
data["rollover_factor"] = rollover_data["prev_close"] / rollover_data["pre_close_before"]
data["adj_factor"] = (1/data["rollover_factor"]).fillna(1).cumprod()

pre_settle/pre_settle:

1
2
3
data["pre_settle_before"] = data["settle"].shift(1)
data["rollover_factor"] = rollover_data["prev_settle"] / rollover_data["pre_settle_before"]
data["adj_factor"] = (1/data["rollover_factor"]).fillna(1).cumprod()

有了复权因子,就可以将价格数据与其相乘就能拿到复权价格了。

总结

这篇文章主要实现如何基于 tushare 计算期货的复权数据,包含了常见的复权计算方法,完整的脚本请查看地址:download_future_adj_data.py。如果没有 tushare 数据,文中也提供了实现的具体思路,即使没有合约映射数据,也可以自己制定规则实现。

希望本文对你有用。


最近有不少朋友希望我开群,如果有这个需求的,可以扫码加群:

补充:

如果想直接用 Tushare 的数据,可联系我,有 20% 的折扣。Tushare 一年 200 元的积分等级就包含了这篇文章中的所有数据 API(这个等级其实已经支持了 tushare.pro 上 60% 的数据接口)。如只是临时需要,可以免费提供一个品种的复权数据。

本文来源于 POLOXUE's BLOG,地址: 期货回测避坑-基于 tushare 计算期货复权价格