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

..