第 5 章 回测例程

在上一章我们编写了第一个策略,并对其进行了回测,但这里有不少和策略无关的重复动作,每写一个策略都要把这些代码写一遍,实在是太麻烦了。可以这么办,把要回测过程的重复代码写成通用的例程,保存在脚本中,需要什么就直接调用。

5.1 回测例程

我们将需要回测的功能,编写为一个个函数,统一保存在脚本backtest.splx中。

脚本代码如下:


A

B

C

1

func bfee(x)

=max(x*0.0003,5)+x*0.00001


2

func sfee(x)

=bfee(x)+x*0.0005


3

func withdraw(R)

=R.max(if(~>~[-1] && ~>=~[1],(~-~[0:].min())/(~+1)))

4




5

func Begin(K)

=K.derive( :持仓, :买入, :卖出, :现金, :仓值, :收益, :昨日 ).run( 昨日=~[-1])

6

func Buy(K, S, P=null)

if (P=ifn(P,K.昨日.收盘))<K.最低

return null

7


=if(S<0,S=int(-S/P))

=S=S\100*100

8


if S<=0

return null

9


=new( K.代码:代码, S:股数, K.日期:买日, P:买价, (a=P*S, a += bfee(a) ):买额, null:卖日, null:卖价, null:卖额 )

10

func Sell(K, H, P=null)

if H.卖日 || (P=ifn(P,K.昨日.收盘))>K.最高

return null

11


=H.run( 卖日=K.日期, 卖价=P, 卖额=(a=P*股数,a-=sfee(a)))

12

func SellOff(K, H, P=null)

=H.select@0( !卖日 && Sell(K, ~, P) )


13

func Loop@m()

=(持仓=昨日.持仓.select(!卖日)|买入,现金=昨日.现金+卖出.sum(卖额)-买入.sum(买额),仓值=持仓.sum(股数)*收盘,收益=现金+仓值)

14




15

func Summary(K)

=K(1).field("代码")

代码

16


=-K.min(现金)

占用资金

17


=K.sum( 卖出.sum( 卖额-买额 ) )

现金收益

18


=K.m(-1).仓值

持仓价值

19


=B19-K.m(-1).持仓.sum( 买额 )

持仓收益

20


=(B18+B20)/B17

收益率

21


=K.sum( 买入.count() )

买盘数

22


=K.sum( 卖出.count( 卖额>买额 ) )

赢利数

23


=K.sum( 卖出.count( 卖额<买额 ) )

亏损数

24


=K.sum( 买入.sum(买额) )

买入资金

25


=K.sum( 卖出.sum( 卖额) )

卖出资金

26


=var@sr( x=K.(收益/B17) )

日波动率

27


=withdraw(x)

最大回撤率

28


=power(1+B21,251/K.len())-1

年化收益率

29


=B27*sqrt(251/K.len())

年化波动率

30


=(B29-0.03)/B30

夏普率

31


=[B16:B31]

=[C16:C31]

32


return new(${B32.($[B32(]/#/"):"/C32(#)).concat@c()})

33

func Display(R)

=R.fname().conj( ~ | [ R.field(~) ] )


34


=create(项目,).record( B34)


35


=B35.run( =string(, if( typeof@x()=="float", if(right(项目,1)!="","#0.00","#0.00%"),"") ) )

36



此脚本代码较长,且有一定难度,初学者首先要重点掌握脚本中每个函数的功能和用法,学会调用即可。

脚本写好后,在init.splx中登记。


A

……

4

>call@f("backtest.splx")

call@f()函数调用脚本的同时登记脚本中的自定义函数,然后可以在主脚本中使用。

5.2 例程函数解释

1. bfee(x):计算买入手续费。

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

参数:x为交易金额

2. sfee(x):计算卖出手续费。

卖出手续费= bfee(x)+x*0.05%

参数:x为交易金额

3. withdraw(R):基于波动率计算最大回撤率,细节可以不用管,了解这个概念的业务意义就行了(后面有解释)。

4. Begin(K):初始化交易数据。在K线数据上增加衍生字段,用来记录交易信息和收益信息。持仓:持仓记录(集),买入:购买记录(集),卖出:卖出记录(集),现金:现金数,仓值:持仓价值,收益:每日收益,昨日:昨天的交易数据。

参数:

K:序表,某支股票的K线数据

5. Buy(K, S, P=null):单支买盘函数,返回每次买入的交易信息记录,包括代码,股数,买日,买价,买额,卖日,卖价,卖额。其中买入金额和卖出金额包含手续费。卖日,卖价和卖额由Sell()函数计算。

返回结果示例:

..

卖日为空表示股票还未卖出。

参数:

K:当日K线数据。这里参数使用当日K线数据,是因为判断是否成功交易以及获取日期都需用到K线里的信息。并且回测时外层循环是一天一天按K线来的,所以这里设计成直接用K线记录,传参时会更方便。

S:买入股票数量或买入钱数。S>0时表示买入数量,如100表示买入100股;S<0时表示买入钱数,如-5000表示买入5000元股票。买入数量一般是100的整数倍。

Pprice 交易价格。P值为空时将昨日收盘价作为交易价格,因为,今天收盘价还不知道,所以一般用昨日的。如果只是想大概感受一下用今日收盘价的情况,可以在调用时修改。

如果交易价格低于当日最低价则买入失败,不记录信息

6. Sell(K, H, P=null):单支卖盘函数,计算卖日,卖出价格和卖出金额,返回交易信息记录。

返回结果示例:

..

参数:

K:当日K线数据

H:当前持仓记录

Pprice 交易价格。P值为空时将昨日收盘价作为交易价格。

如果交易价格高于当日最高价则卖出失败,不记录信息。

7. SellOff(K, H, P=null):将持有股票全部卖出。计算卖出价格和卖出金额,返回交易信息记录(集)。

返回结果示例:

..

K:当日K线数据

H:当前持仓记录

Pprice 交易价格。P值为空时将昨日收盘价作为交易价格。

如果交易价格高于当日最高价则卖出失败,不记录信息。

我们初期研究的策略大都是发现可卖信号时就卖光,所以基本上都是调用这个SellOff函数,而很少调用那个单笔卖出的函数Sell

8. Loop@m():计算当前持仓情况,现金数,持仓价值,收益,在每天买卖交易完成后调用。@m的意思是会把这句代码直接抄进主程序执行,从而可以使用主程序的上下文,这些细节可以先不用理解,当普通函数使用就行了,调用时不必写@m

9. Summary(K):计算各种回测指标。返回记录。

参数:

K:单只股票回测期内的全部K线数据。

10.Display(R)将某条记录转换成纵向方式方便查看。如可以用来查看Summary()返回的各种指标。

参数:

R:记录。如Summary函数中返回的记录。

5.3 回测指标

回测脚本会返回如下回测指标。

占用资金:在给定时间段内完成所有交易需要投入的现金总和(包括手续费)。比如某个策略一周内的买卖资金如下表。不难算出,要完成这些交易,需要投入800元,那么该策略的占用资金就是800元。

买入资金

500

卖出资金

700

买入资金

1000

卖出资金

900

现金收益:指买卖股票所获得的价差收益,只计算已卖出的股票。例如,8块买入,买入100股,买入手续费5元;15元卖出,卖出手续费5元,那么现金收益就是15*100-5-8*100-5=690元。

持仓价值:当前持有的股票价值。例如持有A股票100股,当前股价10元,那么A股票的持仓价值就是1000

持仓收益:当前的持仓价值-持仓成本。

收益率:指收益总额与投资额的比例。收益总额包括现金收益和持仓收益。投资额就是该股票的占用资金。

买盘数:指成功购买的订单数。

赢利数:策略在给定时间段内卖出次数中盈利的次数。

亏损数:策略在给定时间段内卖出次数中亏损的次数。

买入资金:给定时间段内策略的总买入资金。

卖出资金:给定时间段内策略的总卖出资金。

比如某个策略一周内的买卖资金如下表,那么一周内该策略的买入资金就是1500元,卖出资金是1600元。

买入资金

500

卖出资金

700

买入资金

1000

卖出资金

900

日波动率:指策略收益率在一定时间内的变动幅度,它反映了市场的不确定性和风险。波动率越高,收益率的波动越剧烈,资产收益率的不确定性就越强;波动率越低,收益率的波动越平缓,资产收益率的确定性就越强。波动率等于每日收益率的标准差。

最大回撤率:描述策略在回测期可能出现的最大亏损幅度,反映了策略的风险承受能力。最大回撤率越小,说明策略的稳定性越高,风险越低。最大回撤率等于最高收益率与之后最低收益的差值与最高收益率 +1的比值。

年化收益率:(收益率 +1)^(251/交易天数)-1。其中“^”是幂指数符号,251 是假定每年交易天数是固定的251,有时也用252,交易天数是策略开始到结束的间隔交易天数。

年化波动率:日波动率 *(251/交易天数的平方根)

夏普率:年化收益率 - 无风险收益率后与年化波动率的比值,用来衡量策略的收益风险比。

5.4 回测举例

我们还是以第4章中编写的固定价格买卖策略为例,将回测部分改为直接调用回测脚本计算来实现。

首先,准备K线数据和买卖价格参数。注意读取K线时要加@C选项,中文显示。


A

B

C

1

>call("init.splx")



2

2024



3

=date(A2,1,1)

=date(A2,12,31)


4

600690

=Load@C(A4,A3,B3)


5




6

25

30

100

准备记录交易数据的序表,调用backtest.splx中的Begin()函数即可:


A

……

7

=Begin(B4)

A7 Begin()函数会在K线数据后面添加衍生字段,用来记录交易信息。

..

然后循环K线数据,调用买卖函数计算,当最高价高于30全部卖出,最低价低于25就买入:


A

B

……

……

9

for A7

=H=持仓[-1]

10


=卖出=if(最高>=B6, SellOff(~, H, B6) )

11


=if(最低<A6, 买入|=Buy(~, C6, A6) )

12


=Loop( )

A9 循环A7

B9 取出昨日持仓数据,赋值给变量H。前面我们讲过在循环函数中~[-1]表示取前一个成员值,这个规则在循环序表时同样适用,序表中的前一个成员就是上一行记录。因为在序表里更常用的是取某个字段的相邻值,所以SPL约定用字段名加中括号的方式来获取相邻行的字段值,所以持仓[-1]表示取上一行记录中的持仓值,本质上持仓[-1]就相当于~[-1].持仓。这里的持仓[-1]就是昨天交易结束后的持仓数据,用于在今日卖出。

B10 当最高价高于30元时,执行SellOff()函数,将持仓股票全部卖出,并将交易信息记录保存到卖出列。

如某次卖出交易的卖出值:

..

B11 当最低价低于25元时,执行Buy()函数,买入1手,并记录交易信息保存到买入列

如某次买入交易的买入值:

..

完成买卖处理后,要执行一下LoopB12格),让回测程序把当天交易结束后的持仓和统计值计算好。

例如第一天Loop执行前,已经买入了股票,但是持仓、现金等值还是空的没有更新。

..

执行Loop后,可以看到持仓、现金等值发生了变化,完成了更新。

..

整个循环执行完以后,再看A7:

..

其中保存了所有的交易信息及每日收益数据。比如第2天上图中可以看到有买入,双击买入字段,就可以看到该笔订单的买卖信息。

..

双击持仓字段就可以看到买入后的持仓信息:

..

再比如拖动A7的滚动条,第79天有卖出交易,卖出多笔订单,卖出后持仓值为空。

..

A7中可以清楚的查看每一天的买卖情况和持仓数据。

根据A7的结果,调用相关函数统计回测结果:


A

B

……


13

=Summary(A7)

=Display(A13)

A13 返回各种回测指标

..

B13 纵向查看A13中的指标。

..

这里返回的收益率比上一章略低一点,上一章为30.62%。这是因为两者卖出算法略有不同,上一章的代码中是多次购买股票集合到一起卖,而这里是按买入订单一笔一笔去卖,手续费会有所不同。但这里本来也就是粗略估算,有点差距也不用细究。

我们还可以调用绘图脚本,观察A7返回的每日收益情况。

……

15

=Draw(A7,"日期","收益",,"600690.html")

..

可以看到该策略在20244月份以后的收益持续走高。