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 纵向查看回测指标。

..