SPL 量化 回测
回测是一种评估交易策略的通用方法。它通过计算策略在历史数据上的表现来评估交易策略的可行性。如果回测结果良好,交易者和分析师可能会有信心在未来继续使用该策略。
1. 回测脚本
首先要编写回测脚本,将回测脚本保存为 backtest.splx。
脚本代码如下:
A | B | C | D | |
1 | func bfee@m(amt) | =max(amt*0.0003,5)+amt*0.00001 | ||
2 | func sfee@m(amt) | =bfee(amt)+amt*0.0005 | ||
3 | func withdraw(R) | =pma=R.pselect@a(~>~[-1]&&~>=~[1]),ma=R(pma),mi=pma.((idx=~,R.to(idx,).min())),((ma--mi)//(ma++1)).max() | ||
4 | ||||
5 | func Begin() | >env(_TDS,[]) | >env(_TD, create( id, code, tdate, share, price, amt, lnk) ) | |
6 | func Buy(K, S, P) | =abs(_TD.pselect@bs( code-K.code) ) | =ifn(P,K.close) | |
7 | =_TDS.insert@n(B6, _TD.insert@n( 0, 0, K.code, K.tdate, S, C6, null, null) ) | |||
8 | if C6<K.low | return 0 | ||
9 | =B7.run(amt=price*share, amt += bfee(amt) ) | return B7.id=_TD.len() | ||
10 | func Sell(K, I, P) | =_TD.m(I) | if !B10 || B10.id<=0 || B10.lnk || K.code!=B10.code | return null |
11 | =abs(_TDS.pselect@bs(code-K.code) ) | =ifn(P,K.close) | ||
12 | =_TDS.insert@n(B11, _TD.insert@n(0, -B10.id, K.code, K.tdate, B10.share, C11, null, null) ) | |||
13 | if C11>K.high | return 0 | ||
14 | =B10.lnk=B12,B12.lnk=B10 | =B12.run(amt=price*share,amt-=sfee(amt)) | return B10.id | |
15 | func Holds(C, D) | =if(!C,_TDS, _TDS.select@b( code-C) ).select( id > 0 && !lnk && tdate<D) | ||
16 | func Sells(K, U, L, D) | =Holds(if( ifr(K), K.code ), K.tdate) | =K.tdate-D | |
17 | =B16.select((U>=0 && K.close>=price*(1+U)) || (L>=0 && K.close<=price*(1-L)) || (D>0 && tdate<=C16) ) | |||
18 | if ifr(K) | >B17.run(Sell(K, id, null) ) | ||
19 | else | for B17.group(code) | =K.select@b1(code-C19.code) | |
20 | >C19.run(Sell(D19, id, null) ) | |||
21 | func Check(KD,D) | =ifn(D,_TD.max(tdate)) | =create(股票 _ 数量, ${[D26:D39].concat@c()}) | =[] |
22 | for _TD.select(tdate<=B21).group(code) | =B22.sort(tdate,-id) | ||
23 | =KD.select@b(code-B22.code).select(tdate>=C22.tdate && tdate<=B21) | |||
24 | =C22.groups(tdate;sum(sign(id)*share):share, sum(amt*sign(id)):amt) | |||
25 | =join@1m(C23, tdate; C24, tdate).new(#1.tdate, cum(#2.share*#1.close-#2.amt,0):income) | |||
26 | =C22.sum(if( id<0 && lnk, amt-lnk.amt) ) | 现金收益 | ||
27 | =C22.sum(if( id>0 && !lnk, share) )* C23.m(-1).close | 持仓价值 | ||
28 | =C27-C22.sum(if(id>0 && !lnk, amt) ) | 持仓收益 | ||
29 | =C22.max(iterate( ~~+sign(id)*amt, 0 )) | 占用资金 | ||
30 | =(C26+C28)/C29 | 收益率 | ||
31 | =sqrt(var@s( x=C25.(income/C29) ) ) | 波动率 | ||
32 | =withdraw(x) | 回撤率 | ||
33 | =C22.count(id>=0) | 买盘数 | ||
34 | =C22.count(id>0) | 买入数 | ||
35 | =C22.count(id<0) | 卖盘数 | ||
36 | =C22.count(id<0 && lnk && amt>=lnk.amt) | 赢利数 | ||
37 | =C22.count(id<0 && lnk && amt<lnk.amt) | 亏损数 | ||
38 | =C22.sum(if(id>0, amt) ) | 买入资金 | ||
39 | =C22.sum(if(id<0, amt) ) | 卖出资金 | ||
40 | >C21.record(B22.code | [C26:C39]) | >D21|=C25 | ||
41 | =C21.record@in(long(C21.count()) | to(2,C21.fno()).(C21.field(~).sum()),1) | |||
42 | =D21.groups(tdate,sum(income):income).(income/B41.占用资金 ) | |||
43 | =B41.run( 收益率 =(现金收益 + 持仓收益)/ 占用资金, 波动率 =sqrt(var@s(B42)), 回撤率 =withdraw(B42)) | |||
44 | return C21 | |||
45 | func Trades(C) | =_TD.select(amt && (!C || C==code || (ifa(C) && C.contain(code) ))) | ||
46 | =B45.groups(code, tdate, sign(id):flag, price; int(sum(share)):share, sum(amt):amt ) | |||
47 | func Display(R) | =create(项目, 值 ).record( R.fno().conj(R.fname(~) | R.field(~) ) ) | ||
48 | =B47.run( 值 =string(值, if( typeof@x( 值)=="float", if(right( 项目,1)!="率","#0.00", "#0.00%"),"") ) ) |
2. 脚本函数解释
1. bfee(amt):计算买入手续费。
买入手续费 =max(交易金额 *0.03%,5)+ 交易金额 *0.001%
参数:amt 为交易金额
2. sfee(amt):计算卖出手续费。
卖出手续费 = bfee(amt)+amt*0.05%
参数:amt 为交易金额
3. withdraw(R):计算回撤率。
4. Begin():初始化交易数据。
5. Buy(K, S, P):单支买盘函数,计算买盘的买入资金。返回买盘序号(用于卖盘)
参数:
K:当日 K 线数据
S:share 交易股票数量
P:price 交易价格。P 值为空时将当日收盘价作为交易价格。
如果交易价格低于当日最低价则买入失败,记录买盘序号返回 0
6. Sell(K, I, P):单支卖盘函数,计算卖盘的卖出资金。返回对应的买盘序号。
参数:
K:当日 K 线数据
I:买盘序号 id 或记录
P:price 交易价格。P 值为空时将当日收盘价作为交易价格。
如果交易价格高于当日最高价则卖出失败,记录买盘序号返回 0
7. Holds(C,D):返回股票 C 当前持股数据。
参数:
C:code 股票代码,C 为空时,返回所有股票的持股数据。
D:tdate 交易日期
8. Sells(K, U, L, D):按条件批量卖出函数,当股票上涨或下跌到一定程度或持有天数大于 D 时,卖出。
参数:
K:当日 K 线数据,如果卖出涉及多支股票时将把当日所有股票 K 线形成集合作为参数。
U:up 上涨百分比
L:low 下跌百分比
D:持股天数
9. Check(KD,D):计算各种回测指标。
参数:
KD:所有股票的 K 线数据集合,
D:最后的统计日期
10. Trades(C):返回股票交易成功的买卖数据。
参数:
C:股票代码。可以为单值如 600000;也可以为股票代码列表,如 [600000,600015];也可以为空:返回所有股票
11. Display(R):纵向查看 Check() 中的计算结果。
参数:
R:记录。Check 函数中返回的某条记录。
3. 回测指标
股票 _ 数量:总共持有多少支股票。例如,买入股票 [600000,600015],则持有的股票 _ 数量就 2。
现金收益:指买卖股票所获得的价差收益,只计算已卖出的股票。例如,8 块买入,买入 100 股,买入手续费 5 元;15 元卖出,卖出手续费 5 元,那么现金收益就是 15*100-5-8*100-5=690。
持仓价值:当前持有的股票价值。例如持有 A 股票 100 股,当前股价 10 元,那么 A 股票的持仓价值就是 1000。
持仓收益:当前的持仓价值 - 持仓成本。
占用资金:在给定时间段内完成所有交易需要投入的现金综合(包括手续费)。比如某支股票一周内的买卖资金如下表。不难算出,要完成这些交易,需要投入 800 元,那么该股票的占用资金就是 800 元。
买入资金 | 500 |
卖出资金 | 700 |
买入资金 | 1000 |
卖出资金 | 900 |
收益率:指收益总额与投资额的比例。收益总额包括现金收益和持仓收益。投资额就是该股票的占用资金。
波动率:指股票价格在一定时间内的变动幅度,它反映了市场的不确定性和风险。波动率越高,金融资产价格的波动越剧烈,资产收益率的不确定性就越强;波动率越低,金融资产价格的波动越平缓,资产收益率的确定性就越强。波动率等于每日收益率的年化标准差。
回撤率:描述策略可能出现的最糟糕的情况,指在某一段时期内股票从最高点到最低点的百分比幅度。比如,某股票价格在一个月内的最高值为 20 元,其最低的价格为 10 元,则该股票在这一个月内的回撤率=(20-10)/20×100%=50%。一般来说回撤率越大,股票反弹的机会越大,投资风险也越高。
买盘数:指希望购买某支股票的订单数。
买入数:指成功购买某支股票的订单数。买盘数 >= 买入数
卖盘数:指希望售出某支股票的订单数。
赢利数:策略在给定时间段内交易次数中盈利的次数。
亏损数:策略在给定时间段内交易次数中亏损的次数。
买入资金:给定时间段内某支股票的总买入资金。
卖出资金:给定时间段内某支股票的总买入资金。
比如某支股票一周内的买卖资金如下表,那么一周内该股票的买入资金就是 1500 元,卖出资金是 1600 元。
买入资金 | 500 |
卖出资金 | 700 |
买入资金 | 1000 |
卖出资金 | 900 |
4. 回测举例
我们以 N 日均线策略为例,进行回测。
N 日均线策略内容为,当股价高于 N 日均价时买入,低于 N 日均价时卖出。
代码示例:
A | B | |
1 | >call@f("backtest.splx") | 2024 |
2 | =date(B1,1,1) | =date(B1,12,31) |
3 | =call("adjustprice.splx", "", call("loadkday.splx", null, A2,B2) ) | |
4 | ||
5 | >Begin() | 5 |
6 | for A3.group(code) | =A6.derive(if(#>B5+1, sign(close[-1]-close[-B5:-1].avg()), 0):flag ) |
7 | =B6.group@o1(flag) | |
8 | =B7.run(if( flag==1:Buy(~,100,null), flag==-1:Sells(~, 0, 0, 0) ) ) | |
9 | ||
10 | =Trades(null) | =Check(A3,B2) |
11 | =Display(B10(1)) |
A1 登记回测脚本中的函数。
A2 B2 回测起始和截止日期。
A3 读取前复权数据。
A5 初始化交易数据。
B5 均线策略的移动周期。
A6:B8 循环每支股票,进行回测计算。
B6 N 日均线策略,生成交易信号。1 表示买入,-1 卖出。
B7 按照一买一卖分组。
B8 对每次买卖进行回测计算。
A10 返回所有成功买卖的交易数据。
B10 返回回测指标。
A11 纵向查看回测指标。