第 6 章 均价策略和指标----SPL量化编程课
本文介绍了股票量化交易中的均价策略及其实现方法。均价策略通过比较短期和长期移动平均线(MA)来判断买卖时机,当5日MA上穿10日MA时买入,下穿时卖出。文章详细讲解了MA的计算方法,即连续若干天收盘价的算术平均,并演示了如何将策略转化为代码实现,包括数据读取、指标计算、买卖信号判断等步骤。
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 纵向查看回测指标
更多推荐
所有评论(0)