第 5 章 回测例程----SPL量化编程课
在上一章我们编写了第一个策略,并对其进行了回测,但这里有不少和策略无关的重复动作,每写一个策略都要把这些代码写一遍,实在是太麻烦了。可以这么办,把要回测过程的重复代码写成通用的例程,保存在脚本中,需要什么就直接调用。
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) ) ) | |
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月份以后的收益持续走高。
更多推荐
所有评论(0)