SPL 量化系列实践:回测例程

量化交易一个绕不开的步骤就是回测,任何策略都要在历史数据中验证后才可能用于模拟盘甚至是实盘进行交易,本文就来介绍 SPL 怎么完成回测。

话不多说,直接上干货。

假设现在已经有了某个策略生成的买卖交易数据,结果如下:

..

其中 ts_code 是股票代码、trade_date 是交易日期、shares 是交易股数、flag 是交易标志,1 表示买入,-1 表示卖出。

我们需要根据这些交易数据和日线行情数据完成回测,其中日线行情数据如下:

..

因为有些股票可能会分红送股等,需要复权,复权又分为前复权和后复权,因为是回测,所以我们使用后复权的方法来复权。复权的方法这里不过多介绍,感兴趣的同学可以阅读这篇文章《复权价格》。

常用的回测指标如下:

1. 累计收益率 (cumulative rate of return):最后一天的收益与投入资金的比值。

2. 年化收益率 (annualized rate of return):(累计收益率 +1)^(252/ 交易天数)-1。其中“^”是幂指数符号,252 是假定每年交易天数是固定的 252,交易天数是策略开始到结束的间隔天数。

3. 年化波动率 (annual volatility):日波动率 *(252 的平方根)。其中日波动率是日收益率的标准差,它可以用来衡量风险。

4. 夏普比率 (sharpe ratio):年化收益率 - 无风险收益率后与年化波动率的比值,用来衡量股票组合或者基金的收益风险比。

5. 最大回撤 (maximum drawdown):最高收益率与之后最低收益的差值与最高收益率 +1 的比值。

6. 投入现金 (cash invested):完成这些交易需要投入的现金综合(包括手续费)。

7. 总资产 (total assets):完成这些交易后现有的现金 + 股票价值。

8. 仓位占比 (stock holding ratio):现有股票价值与总资产的比值。

9. 盈利次数 (profit times)、亏损次数 (losses times):这些交易中盈利和亏损的次数,只要某支股票卖出后,收益增加了就算盈利 1 次,反之如果收益减少了,则亏损一次。

SPL 代码:


A

B

1

=trade_data


2

=end_date

/ 结束日期

3

=daily_dir="daily/"


4

=directory(daily_dir)


5

=A1.trade_date

/ 开始日期

6

=A1.id(ts_code)

/ 股票代码

7

=A4.align(A6.(filename@n(~)),filename@n(~))


8

=A7.(file(daily_dir/~).import@tc().select(trade_date>=A5&&trade_date<=A2))

/ 股票行情

9

=A8.(~.derive(if(#>1, close/ pre_close *factor[-1], close/pre_close):factor))

/ 增加复权因子

10

=A9.((hfq_fst=~(1),~.derive(round(factor/hfq_fst.factor*hfq_fst.close,2): hfq_close)))

/ 增加前复权和后复权收盘价

11

=A1.group(ts_code)


12

=A10.((idx=#,~.join(trade_date,A11(idx):trade_date,shares,flag)))

/ 增加买卖股数和买卖标志

13

=A12.((num=0,money=0,~.derive(shares*hfq_close:trade_amt,if(!shares&&#==1,0,if(shares,(num=num+shares*flag),num)):share_num,hfq_close*share_num:hold,round(if(flag==1,money=money-hold,if(flag==-1,money=money+hfq_close*shares,money)),2):cash,if(flag==1,max(trade_amt*(0.0003),5)+trade_amt*0.00001,if(flag==-1,max(trade_amt*(0.0003),5)+trade_amt*(0.00001+0.0005),0)):commission,round(hold+cash-commission,2):income)))

/ 增加持仓金额、现金数、手续费、收益

14

=A13.conj()


15

=A14.group(trade_date;round(~.sum(hold),2):holds,round(~.sum(income),2):income,round(~.sum(commission-cash),2):needs,(f=~.(flag),if(f.pos(1)>0,1,0)):buy,if(f.pos(-1)>0,-1,0):sell)

/ 按天统计持仓金额、收益、所需现金

16

=round(A15.max(needs),2)

/ 投入现金 (cash invested)

17

=A15.(income/A16)

/ 累计日收益率

18

=A17.m(-1)


19

=round(A18*100,2)/"%"

/ 累计收益率 (cumulative rate of return)

20

=power(A18+1,252/A15.len())-1


21

=round(A20*100,2)/"%"

/ 年化收益率 (annualized rate of return)

22

=pma=A17.pselect@a(~>~[-1]&&~>=~[1]),ma=A17(pma),mi=pma.((idx=~,A17.to(idx,).min())),round(((ma--mi)//(ma++1)).max()*100,2)/"%"

/ 最大回撤 (maximum drawdown)

23

=round(A15.m(-1).income+A16,2)

/ 总资产 (total assets)

24

=lst=A15.m(-1),round(lst.holds/A23*100,2)/"%"

/ 仓位占比 (stock holding ratio)

25

=A17.(if(#==1,0,~-~[-1]))

/ 每日收益率

26

=sqrt(var@s(A25))

/ 日波动率

27

=A26*sqrt(252)


28

=round(A27*100,2)/"%"

/ 年化波动率 (annual volatility)

29

=round((A20-0.03)/A27,2)

/ 夏普比率 (sharpe ratio)

30

=A13.(~.group@i(flag[-1]==-1))


31

=A30.(~.select(~.m(-1).flag==-1).align@a([true,false],~.m(-1).income>~.~.income))


32

=A31.sum(~(1).len())

/ 盈利次数 (profit times)

33

=A31.sum(~(2).len())

/ 亏损次数 (losses times)

34

=[A19,A21,A28,A29,A22,A16,A23,A24,A32,A33]


35

[累计收益率 (cumulative rate of return), 年化收益率 (annualized rate of return), 年化波动率 (annual volatility), 夏普比率 (sharpe ratio), 最大回撤 (maximum drawdown), 投入现金 (cash invested), 总资产 (total assets), 仓位占比 (stock holding ratio), 盈利次数 (profit times), 亏损次数 (losses times)]


36

=A35.new(~:indicatiors,A34(#):value)


关键代码解释:

A1:包含买卖信号和股数的数据 trade_data。因为这是回测方法,所以把这个数据作为参数,方便其他程序调用。本例中的 trade_data 如下:

..

A2:结束日期 end_date。交易的结束日期,方便后续取出日线行情数据,把它也作为参数。

A10:每支股票增加复权因子和复权收盘价。数据如下:

..

A12:行情数据按交易日期关联买卖数据。数据如下:

..

没有交易的日期关联后,股票数和买卖标志是 null。

A13:增加持仓金额、现金数、手续费、收益。

其中,持仓金额 hold:持股数 * 后复权收盘价,持仓数 = 仓内股数 + 交易股数 * 交易标志。

现金数:如果买入,现金数 = 现有现金 - 持仓金额;如果卖出,现金数 = 现有现金 + 后复权收盘价 * 卖出股数

手续费:

买入手续费:max(买入金额 *0.03%,5)+ 买入金额 *0.001%

卖出手续费:max(买入金额 *0.03%,5)+ 买入金额 *(0.001%+0.05%)

其中 0.03% 是券商的最高佣金比例,5 是最低佣金,即佣金不到 5 时,按 5 元收取,0.001% 是过户费,0.05% 是印花税(仅卖出时收取)。

收益:持仓金额 + 现金数 - 手续费

注意:这里假设开始的现金数是 0,所以现金数和收益有可能是负数。

部分数据如下:

..

A15:按天汇总持仓金额、收益、所需现金

将所有仓内股票按天统计。持仓金额和收益都是简单的求和,需要的现金 = 手续费 - 现金数(这样算是因为手续费是正的,现金可能是负的)后再求和。如果这天有买入动作,买入标志(buy)=1,没有则 buy=0;如果这天有卖出动作,卖出标志(sell)=-1,没有则 sell=0。结果如下:

..

A16:A15 中所需现金的最大值即为这些交易过程中需要投入的资金。

通常情况下,我们还希望看到交易以来的收益率走势图,下面介绍下收益率图所需的数据。

A15 中已经算出每天收益和买卖点,我们只要用每天收益 / 投入资金即可得到每天的收益率,同时把 trade_date 转换成日期格式。

代码如下:

A15.derive(round(income/A16,5):income_rate).run(trade_date=date(string(trade_date);"yyyyMMdd"))

计算后数据如下:

..

用这些数据即可画出收益率走势图,trade_date 作为横轴,income_rate 作为纵轴,buy 为 1 的点是买入点(用红色圆点表示),sell 为 -1 的点是卖出点(用绿色方点表示)。

画图代码比较繁琐,也不是本文的重点,如果想学习 SPL 画图知识可以学习《程序设计》的 12.1-12.4 画图师 部分。

收益率图如下:

..

回测指标见下表:

indicators

value

累计收益率 (cumulative rate of return)

-35.81%

年化收益率 (annualized rate of return)

-10.47%

年化波动率 (annual volatility)

18.19%

夏普比率 (sharpe ratio)

-0.74

最大回撤 (maximum drawdown)

46.88%

投入现金 (cash invested)

372725.4

总资产 (total assets)

239237.4

仓位占比 (stock holding ratio)

48.94%

盈利次数 (profit times)

4

亏损次数 (losses times)

10