第 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的整数倍。
P:price 交易价格。P值为空时将昨日收盘价作为交易价格,因为,今天收盘价还不知道,所以一般用昨日的。如果只是想大概感受一下用今日收盘价的情况,可以在调用时修改。
如果交易价格低于当日最低价则买入失败,不记录信息
6. Sell(K, H, P=null):单支卖盘函数,计算卖日,卖出价格和卖出金额,返回交易信息记录。
返回结果示例:

参数:
K:当日K线数据
H:当前持仓记录
P:price 交易价格。P值为空时将昨日收盘价作为交易价格。
如果交易价格高于当日最高价则卖出失败,不记录信息。
7. SellOff(K, H, P=null):将持有股票全部卖出。计算卖出价格和卖出金额,返回交易信息记录(集)。
返回结果示例:

K:当日K线数据
H:当前持仓记录
P:price 交易价格。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手,并记录交易信息保存到买入列
如某次买入交易的买入值:

完成买卖处理后,要执行一下Loop(B12格),让回测程序把当天交易结束后的持仓和统计值计算好。
例如第一天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") |

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