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@j(_TDS,[]) |
>env@j(_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) |
=if(I, [_TD.m(I)], Holds(K.code, K.tdate)) |
||
11 |
for B10 |
if !B11 || B11.id<=0 || B11.lnk || K.code!=B11.code |
next |
|
12 |
=abs(_TDS.pselect@bs(code-K.code) ) |
=ifn(P,K.close) |
||
13 |
=_TDS.insert@n(C12, _TD.insert@n(0, -B11.id, K.code, K.tdate, B11.share, D12, null, null) ) |
|||
14 |
if D12>K.high |
return 0 |
||
15 |
=B11.lnk=C13,C13.lnk=B11 |
=C13.run(amt=price*share,amt-=sfee(amt)) |
||
16 |
return B10.id |
|||
17 |
func Holds(C, D) |
=if(!C,_TDS, _TDS.select@b( code-C) ).select( id > 0 && !lnk && tdate<D) |
||
18 |
func Sells(K, U, L, ND) |
=Holds(if( ifr(K), K.code ), K.tdate) |
=K.tdate-ND |
|
19 |
=B18.select((U>=0 && K.close>=price*(1+U)) || (L>=0 && K.close<=price*(1-L)) || (ND>0 && tdate<=C18) ) |
|||
20 |
if ifr(K) |
>B19.run(Sell(K, id, null) ) |
||
21 |
else |
for B19.group(code) |
=K.select@b1(code-C21.code) |
|
22 |
>C21.run(Sell(D21, id, null) ) |
|||
23 |
func Check(KD,D) |
=ifn(D,_TD.max(tdate)) |
=create(股票 _ 数量, ${[D28:D41].concat@c()}) |
=[] |
24 |
for _TD.select(tdate<=B23).group(code) |
=B24.sort(tdate,-id) |
||
25 |
=KD.select@b(code-B24.code).select(tdate>=C24.tdate && tdate<=B23) |
|||
26 |
=C24.groups(tdate;sum(sign(id)*share):share, sum(amt*sign(id)):amt) |
|||
27 |
=join@1m(C25, tdate; C26, tdate).new(#1.tdate, cum(#2.share*#1.close-#2.amt,0):income) |
|||
28 |
=C24.sum(if( id<0 && lnk, amt-lnk.amt) ) |
现金收益 |
||
29 |
=C24.sum(if( id>0 && !lnk, share) )* C25.m(-1).close |
持仓价值 |
||
30 |
=C29-C24.sum(if(id>0 && !lnk, amt) ) |
持仓收益 |
||
31 |
=C24.max(iterate( ~~+sign(id)*amt, 0 )) |
占用资金 |
||
32 |
=(C28+C30)/C31 |
收益率 |
||
33 |
=sqrt(var@s( x=C27.(income/C31) ) ) |
波动率 |
||
34 |
=withdraw(x) |
回撤率 |
||
35 |
=C24.count(id>=0) |
买盘数 |
||
36 |
=C24.count(id>0) |
买入数 |
||
37 |
=C24.count(id<0) |
卖盘数 |
||
38 |
=C24.count(id<0 && lnk && amt>=lnk.amt) |
赢利数 |
||
39 |
=C24.count(id<0 && lnk && amt<lnk.amt) |
亏损数 |
||
40 |
=C24.sum(if(id>0, amt) ) |
买入资金 |
||
41 |
=C24.sum(if(id<0, amt) ) |
卖出资金 |
||
42 |
>C23.record(B24.code | [C28:C41]) |
>D23|=C27 |
||
43 |
=C23.record@in(long(C23.count()) | to(2,C23.fno()).(C23.field(~).sum()),1) |
|||
44 |
=D23.groups(tdate,sum(income):income).(income/B43.占用资金 ) |
|||
45 |
=B43.run( 收益率 =(现金收益 + 持仓收益)/ 占用资金, 波动率 =sqrt(var@s(B44)), 回撤率 =withdraw(B44)) |
|||
46 |
return C23 |
|||
47 |
func Trades(C) |
=_TD.select(amt && (!C || C==code || (ifa(C) && C.contain(code) ))) |
||
48 |
=B47.groups(code, tdate, sign(id):flag, price; int(sum(share)):share, sum(amt):amt ) |
|||
49 |
func Display(R) |
=create(项目, 值 ).record( R.fno().conj(R.fname(~) | [R.field(~)] ) ) |
||
50 |
=B49.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 或记录。I 可以为空,表示将该股票全部卖出。
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 纵向查看回测指标。