6.1 均价策略

均价策略,顾名思义就是使用平均价格线来判断股票未来走势。一般会使用两条均线做判断,一条长期均线(如10日MA),一条短期均线(如5日MA)。这种策略基于这样一种假设:股票价格的动量会朝着短期均线的方向移动。当短期均线穿过长期均线,并超过长期移动平均线时,动量将向上,此时股价可能会上涨。反之,如果短期均线的移动方向相反,股价则可能下跌。

这里反复提到均线概念,均线全称是移动平均线,英文Moving average,简称MA。

MA是分析股票很常用的一个指标,它以道·琼斯的“平均成本概念”为理论基础,采用统计学中“移动平均”的原理,将一段时期内的股票价格平均值连成曲线,用来显示股价的历史波动情况。

MA 计算方法就是求连续若干天收盘价的算术平均。
计算公式: MA = (C1+C2+C3+C4+C5+….+Cn)/n
C 为收盘价,n 为移动平均周期数
例如,5 日移动平均价格计算方法为: MA 5 = (前四天收盘价 + 前三天收盘价 + 前天收盘价 + 昨天收盘价 + 今天收盘价)/5

以时间的长短划分,移动平均线可分为短期、中期、长期几种,综合观察长、中、短期移动平均线,可以研判市场的多重倾向。长、中、短是相对的,可以自己确定。在国内股市中,常利用的移动平均线组合为 5 日、10 日、30 日、60 日、120 日、250 日线。

了解了MA的计算方法,均价策略的内容就更具体了。

如果短期MA从下往上突破长期MA,下单买入1手;如果短期MA从上往下跌破长期MA,下单全部卖出。这里MA周期分别取5和10。

下面来编写代码,首先读取股票数据,这里我们还是以600690股票为例,回测期为2024年。

这里读数时要注意,因为计算MA要用到前几天的股价数据,因此需要往前多读一些天的数据。

A B
1 >call("init.splx")
2 2024 =date(A2,1,1)
3 =B2-20 =date(A2,12,31)
4 600690 =Load@C(A4, A3,B3)

B2和B3是回测的起止日期

A3是从回测开始日期往前数20个日历日,应该足够包括10个交易日了,日期和整数可以直接相加减,A3结果返回日期

..

B4 读取日期从A3到B3的前复权数据。这里也可以看到从回测期往前多读了一部分数据。

..

数据读进来后,下一步计算策略需要用的指标MA。

我们用derive函数在源数据上衍生两个字段MA_5和MA_10,分别表示5日MA和10日MA:

A
……
6 =B4.derive(收盘[-4:0].avg():MA_5,收盘[-9:0].avg():MA_10)

这句代码中又一次出现了中括号的语法,不同的是中括号里的内容形式不同。[a:b]的形式表示取区间值,如,收盘[-4:0]表示取区间收盘价,从往前数第4天开始取,一直取到当前行(0表示当前行),即共计取到5天的收盘价,再用avg()函数求平均值就是5日MA;同理,收盘[-9:0].avg()就是计算10日均价。

在均价策略中需要比较MA_5和MA_10的大小,然后根据其大小的变化来进行下单,因此我们可以再衍生出一个信号flag,当MA_5大于MA_10时返回1,MA_5小于MA_10返回-1,相等则返回0。显然用我们之前学过的if()函数就可以实现,读者可以自己写一下。这里我们再教一个更简便的函数sign()来实现:

A
……
7 =A6.derive(sign(MA_5-MA_10):flag)

A7中的sign()函数可以判断参数表达式的正负,正数时返回1,负数返回-1,0则返回0。有了flag信号,就容易判读买卖时机了。

下一步,准备记录交易信息的表格并筛选出回测期内的数据:

A
……
8 =Begin(A7)
9 =A8.select( 日期>=B2 )

A8调用Begin函数,在K线上添加相关字段,用来记录交易信息。

..

A9 筛选出回测期内的数据。

然后就可以循环K线,进行买卖计算了,当flag信号由-1或0变为1时就买入,由0,1变为-1时就全部卖出。

A B
…… ……
11 for A9 =H=持仓[-1]
12 =if(flag[-1]==1 && flag[-2]!=1,买入|= Buy( ~, 100, 收盘 ) )
13 =卖出=if(flag[-1]==-1 && flag[-2]!=-1, SellOff( ~, B11, 收盘 ) )
14 =Loop( )

flag[-1]表示取第前一行的flag值,即昨日flag值;同理flag[-2]就表示取第前2行的flag值即前天flag值。

和固定价格策略不同的是在均线策略中我们使用收盘价来计算MA指标,要第2天才能挂单买卖,因此今天是否下单要看昨天和前天的flag值,即flag[-1]和flag[-2]。

为了确保回测时的买卖成功,在B12和B13格中使用了今日收盘价(缺省的昨日收盘价不一定能成功),这里重点是讲解回测框架的使用,就不再强调效准确性了。

最后再执行一下Loop函数,更新持仓和收益等信息。

买卖交易计算完成后,就可以调用相关函数查看各种结果了,例如:

A B
…… ……
15 =Summary(A9) =Display(A15)

A15 查看回测结果

B15 纵向查看回测结果

..

6.2 指标

在均价策略中我们用到了一个股票指标MA,事实上股票指标还有很多,比如还有MACD、KDJ、DMA等等。指标是量化分析的重要手段,它能够帮助投资者更加客观、科学地评估股票的价值和风险。很多策略都是根据各种指标来实现的,指标是策略开发最基础的模块。因此,对于一些使用频率较高的指标我们可以将其封装为函数,然后统一保存在一个脚本中,需要时直接调用。

例如MA就是一个最常用的股票指标,将其写成自定义函数:

A B
1 func MA(A, $ma,$v, n) =A.run(${v}[-n+1:0].avg():${ma})

自定义函数之前已经学习过了,以func语句开头,MA()是函数名,括号里面的内容是要传入的参数,func语句后面的代码块就是函数代码。

A1格里参数前面加$,表示将参数转为字符串。括号里的参数含义如下:

A 序表,如K线数据
ma 指标返回的字段名,如MA_10,$把字段名转化成字符串
v 要计算移动平均值的字段,如收盘价
n 移动平均周期

在开发策略时,不仅会经常用到指标,还会同时用到多个指标,并且指标之间也会相互引用。考虑到在主程序中调用的便利性和计算性能,我们需要建立一个统一指标编写规则。

(1)在编写指标函数时统一用A.run()在源数据上进行计算。这样我们要在主程序中把要用的指标字段先一次都derive出来,先后再调用对应的函数计算指标值就可以了,最后所有指标都会返回到一张表里。

(2)为了使用方便,每个指标函数的参数也都用统一的规则来定义,第一个参数总是K线,第二个参数是指标字段名,后面则是用来计算指标的源参数。

继续看MA函数的代码,B1格里的${}是宏,{}里的参数是字符串,使用宏可将字符串作为表达式参与计算。

如:

A
1 ="1"
2 =${A1}+3

A2 结果会返回4。

理解了宏概念,这个函数代码我们就很熟悉了,就是取前N-1天到当天的价格然后求平均值。代码写好后,将自定义函数保存为indicator.splx,以后所有的指标函数我们都保存在这个脚本中。

将指标脚本添加到init.splx中:

A
……
5 >call@f("indicator.splx")

调用指标脚本,计算MA:

A B
1 >call("init.splx")
2 2024 =date(A2,1,1)
3 =B2-20 =date(A2,12,31)
4 600690 =Load@C(A4, A3,B3)
5
6 =B4.derive(:MA_5)
7 =MA(A6, MA_5, 收盘, 5 )

A1 登记脚本

A2:B4 读取股票数据。注意要往前多读一部分数据。

A5 增加指标字段MA_5,值设为空。因为在MA()函数中,用到的A.run()在源数据上修改,因此要先增加字段。

A6 调用MA函数,计算收盘价的5日移动平均值。

..

下面我们用指标的形式来改写均价策略:

A B
1 >call("init.splx")
2 2024 =date(A2,1,1)
3 =B2-20 =date(A2,12,31)
4 600690 =Load@C(A4, A3,B3)
5
6 =B4.derive(:MA_5, :MA_10)
7 =MA(A6, MA_5, 收盘, 5 ) =MA(A6, MA_10, 收盘, 10 )
8 =A6.derive(sign(MA_5-MA_10):flag )
9 =Begin(A8)
10 =A9.select( 日期>=B2 )
11
12 for A10 =H=持仓[-1]
13 =if(flag[-1]==1 && flag[-2]!=1, 买入|=Buy( ~, 100, 收盘) )
14 =卖出=if(flag[-1]==-1 && flag[-2]!=-1, SellOff( ~, H, 收盘) )
15 =Loop()
16 =Summary(A10) =Display(A16)

A1 执行init.splx脚本。

其他代码都不变,只在加粗部分,由原来直接计算改为调用MA函数计算。

在这里直接调用MA函数,代码看起来并没有减少,但是代码结构变得规整。随着指标使用越来越多,这种方法会使策略开发更加清晰。

比如我们这里的交易信号的判断其实也是一种指标,当MA短线上穿长线时,称为金叉(Golden Cross),为买入信号,指标值为1;短线下穿长线时,称为死叉(Death Cross),为卖出信号,指标值为-1。我们可以将这个指标也写成函数,命名为GDX。函数GDX的功能是输入MA短线和MA长线值,输出金叉或死叉信号。

首先按照前面制定的规则来定义参数,第一个是K线,第二个是指标字段名,后面是源参数,MA短线和MA长线

A 序表,K线数据
gdx 返回的指标字段名
v1 MA_5短线字段名,如
v2 MA_10长线字段名,如

在计算一些复杂点的指标时,往往不能够一步算好,需要用到一个或多个中间变量。比如这里在判断金叉死叉时,需要先计算出短线-长线的差值,然后再根据差值的变化判断金叉死叉,显然这个差值就是一个中间变量,过程需要但是返回结果不需要。因此对于中间变量的编写有几点需要注意,首先中间变量要在原序表上生成,不能新建表,不然返回去的结果就不对了;其次中间变量使用完后需要删掉;另外中间变量的命名要有规律,不同指标的中间变量名字不能重复。这是因为指标之间可能会嵌套调用,如果它们的中间变量都使用了相同的名字,就会算错。为防止名字重复我们将中间变量名统一以函数名开头。

按照这几个注意点我们整理出编写GDX函数的思路:

(1) 在原序表上增加中间变量MA短线-MA长线,命名为gdx0

(2) 根据中间变量计算出金叉死叉信号gdx

(3) 删除中间变量

编写代码:

A B
…… ……
3 func GDX(A, $gdx, $v1, $v2) =A.derive@o(${v1}-${v2}:gdx0 )
4 =A.run(if(gdx0[-1]<=0 && gdx0>0:1, gdx0[-1]>=0 && gdx0<0:-1; 0 ):${gdx} )
5 =A.alter(; gdx0)

B3 derive加@o选项可以原表上添加变量

B4 当gdx0由负变正时为金叉,由正变负时为死叉。if()函数之前学过,讲的是if(a,b,c)这种形式,这里用到的是它的另一种形式if(x1:y1,…,xk:yk;y),表示如果x1为真返回y1,x2为真返回y2……xk为真返回yk,其余情况返回y。

B5 A.alter()用来修改序表中的字段,分号前面为要修改和新增的字段,分号后面是要删除的字段。这里只需要删除,因此分号前面为空。

将交易信号计算部分代码也改写为指标调用方式实现:

A B
1 >call("init.splx")
2 2024 =date(A2,1,1)
3 =B2-20 =date(A2,12,31)
4 600690 =Load@C(A4, A3,B3)
5
6 =B4.derive(:MA_5, :MA_10, :DMA)
7 =MA(A6, MA_5, 收盘, 5 ) =MA(A6, MA_10, 收盘, 10 )
8 =GDX(A6, DMA, MA_5, MA_10)
9 =Begin(A6)
10 =A9.select( 日期>=B2 )
11
12 for A10 =H=持仓[-1]
13 =if(DMA[-1]==1,买入|=Buy(~,100,收盘) )
14 =卖出=if(DMA[-1]==-1, SellOff( ~, H,收盘 ) )
15 =Loop()
16 =Summary(A10) =Display(A16)

可以看到有了GDX指标,策略计算部分代码更加清晰了。

进一步再优化下代码。前面我们讲了在均价策略中要通过昨天的指标信号(即字段名加[-1]的方式)来进行买卖计算。这里其实有一个问题,在A10单元格进行过滤操作后,A12 for语句的代码块在循环第1条数据时,持仓[-1]是取不到的。当[-1]值取不到时,SPL会默认按null处理,这样在开始的几条数据可能会计算不准确。

实际上在回测脚本的Begin()函数里,还有一个功能,就是会自动将昨天的数据提取到“昨日”列。这样只要用昨日.持仓来取数,就可以避免[-1]值取不到的问题了。

改写代码:

A B
1 >call("init.splx")
2 2024 =date(A2,1,1)
3 =B2-20 =date(A2,12,31)
4 600690 =Load@C(A4, A3,B3)
5
6 =B4.derive(:MA_5, :MA_10, :DMA)
7 =MA(A6, MA_5, 收盘, 5 ) =MA(A6, MA_10, 收盘, 10 )
8 =GDX(A6, DMA, MA_5, MA_10)
9 =Begin(A6)
10 =A9.select( 日期>=B2 )
11
12 for A10 =H=昨日.持仓
13 =if(昨日.DMA==1,买入|=Buy(~,100,收盘) )
14 =卖出=if(昨日.DMA==-1, SellOff( ~, H,收盘 ) )
15 =Loop()
16 =Summary(A10) =Display(A16)

B13和B14买卖计算时,用昨日. 持仓替代持仓 [-1],用昨日.DMA替代DMA[-1]。

修正了代码中的错误后,我们继续讲指标函数的编写。

再进一步GDX返回的这个DMA值也可以定义一个函数,一把返回,我们将这个函数命名为MAGDX。

首先还是先明确函数的功能,输入要计算的字段和短线、长线周期,输出金叉死叉信号。

定义输入参数:

A 序表,如K线数据
dma DMA返回值字段名,如
v 要计算移动平均值的字段,如收盘
n1 短线周期
n2 长线周期

编写MAGDX函数代码:

A B
…… ……
7 func MAGDX(A, $dma, $v, n1, n2) =A.derive@o(:magdx1, :magdx2)
8 =MA(A, magdx1, ${v}, n1 ), MA( A, magdx2, ${v}, n2 )
9 =GDX(A, ${dma}, magdx1, magdx2 )
10 =A.alter(;magdx1, magdx2)

这段代码没什么难度,根据我们前面制定的规则很容易写出来。

需要强调的是这段代码里发生了指标间的互相调用,并且MAGDX和GDX都有中间变量,如果它们起了相同的名字比如都起了x1,x2这样的名字,计算时就会出错。再次强调,中间变量命名要有规律,不能随便起,避免重复。

调用MAGDX函数改写策略:

A B
1 >call("init.splx")
2 2024 =date(A2,1,1)
3 =B2-20 =date(A2,12,31)
4 600690 =Load@C(A4, A3,B3)
5
6 =B4.derive(:DMA)
7 =MAGDX(A6, DMA, 收盘, 5, 10 )
8 =Begin(A6)
9 =A8.select( 日期>=B2)
10
11 for A9 =H=昨日.持仓
12 =if(昨日.DMA==1,买入|=Buy(~,100,收盘) )
13 =卖出=if(昨日.DMA==-1, SellOff( ~, H,收盘 ) )
14 =Loop()
15 =Summary(A9) =Display(A15)

可以看到有了指标体系,策略的描述要比不用任何指标要容易的多。

在本小节,我们重点讲了指标的编写方法,这非常重要读者要熟练掌握。有了完善的指标体系,策略开发会更加容易和高效。

最后,我们再来总结一下指标编写的几条规则:

(1) 指标参数统一定义,第一个参数总是K线,第二个参数是指标字段名,后面是用来计算指标的源参数。

(2) 在编写指标函数时统一用A.run()在原数据上进行计算。

(3) 中间变量名不能重复,统一以函数名开头

(4) 中间变量也在原表上衍生,用derive@o()

(5) 中间变量用完后要删除

6.3 均价策略 - 多支股票

掌握了指标体系的使用,策略写起来就容易多了,这一小节我们再拓展一下策略编写的能力。在前面的策略例子中,都是以单支股票为例来编写的,而在实际量化操作中都是多支股票一起操作,这里我们以均价策略为例,学习一下如何写多支股票。

多支股票的回测指标需要分别计算,最后汇总,因此要先在回测脚本中增加两个汇总的函数:

A B
…… ……
37 func Yield(Y) =Y.groups( 日期; conj(持仓):持仓,conj(买入):买入,conj(卖出):卖出,sum(现金):现金, sum(仓值):仓值,sum(收益):收益 )
38 func Total(S, Y=null) =S.fname().to(2,).(eval@s("sum(?):?", ~ )).concat@c()
39 =S.groups(; count(1):数量, ${B38} )
40

=B39.run(

if(Y,占用资金=-Y.min(现金)),

收益率=(现金收益+持仓收益)/占用资金,

年化收益率=if(Y,power(1+收益率,251/Y.len())-1),

日波动率=if(Y,var@sr( x=Y.(收益/占用资金))),

年化波动率=if(Y,日波动率*sqrt(251/Y.len())),

最大回撤率=if(Y,withdraw(x)),

夏普率=if(Y,(年化收益率-0.03)/年化波动率)

)

41 return B40(1)

11. Yield(Y):汇总回测期内多支股票的每日收益数据。

参数:

Y:序表,回测期内的多支股票的收益数据。

12. Total(S, Y):计算总的回测指标。返回记录。

参数:

S:每支股票的回测指标。

Y:每日总收益数据。

下面来编写多支股票的均价策略。

先登记脚本和读数:

A B
1 >call("init.splx")
2 2024 =date(A2,1,1)
3 =B2-20 =date(A2,12,31)
4 [1,600690] =Load@C(A4,A3,B3)

A4 输入多支股票代码。

B4 读取多支股票数据。

然后分组循环每支股票,分别计算每支股票,最后汇总。

A B C
…… …… ……
6 for B4.group(代码) =A6.derive(:DMA)
7 =MAGDX(B6, DMA, 收盘, 5, 10 )
8 =Begin(B6)
9 =B8.select( 日期>=B2)
10 for B9 =H=昨日.持仓
11 =if(昨日.DMA==1,买入|=Buy(~,100,收盘) )
12 =卖出=if(昨日.DMA==-1, SellOff( ~, H,收盘 ) )
13 =Loop()
14 =@|Summary(B9) =Yield(@|B9)
15 =Total(B14,C15) =Display(A15)

这段代码虽长,但是很好理解,就是在单支股票代码上加一层循环。

B14和C14中的@符号表示当前的单元格格值,如B14表示将当前B14值和Summary(B9)的结果合并,循环完成后,B14就计算出所有股票的回测结果。

..

C14的Yield函数用来汇总多票的收益结果,这里每次将上次汇总结果合并上新的股票的收益再重新汇总,最后就会得到所有股票的收益结果。

..

最后计算总的回测指标。

A15计算总回测指标

..

B15 纵向查看回测指标

..

Logo

专业量化交易与投资者大本营

更多推荐