第 7 章 MACD 背离策略----SPL量化编程课
本文详细阐述了MACD的计算方法(含EMA平滑系数公式)及指标函数封装,并展示了基于SPL语言的策略实现流程,包括数据加载、指标计算、买卖条件判断及多股票回测框架。此外,还介绍了平仓函数(SellAll/SellEach)用于止盈止损,支持按涨幅阈值或持有天数自动卖出。通过整合DVGMACD函数,策略开发效率显著提升,适用于中长期趋势投资。
7.1 相关概念
MACD(Moving Average Convergence and Divergence)是 Geral Appel 于 1979 年提出的,利用收盘价的短期(常用为 12 日)指数移动平均线(EMA)与长期(常用为 26 日)指数移动平均线 (EMA) 之间的聚合与分离状况,对买进、卖出时机作出研判的技术指标。
MACD从均线指标EMA衍化而来,对把握趋势性行情有着很好的应用效果,它的顶底背离是一种经过检验的“抄底逃顶”方法,是不少中长期投资者在实战中都会考虑的指标。
MACD计算方法:
1. 短线EMA:短线的指数移动平均,移动窗口通常取12;
EMA=q*当前价格+(1-q)* EMA[-1]
其中q是平滑系数,q=2/(移动周期+1)。
后续的EMA都是这样计算,只是移动周期取值不同。
2. 长线EMA:长线的指数移动平均,移动窗口通常取26;
3. 长短线的离差DIFF:短线EMA-长线EMA。
4. 离差平均值DEA:DIFF的指数移动平均,移动窗口通常取9;
5. MACD:2 *(DIFF - DEA)
下面介绍MACD背离买卖策略:
金叉:DIFF上穿DEA
死叉:DIFF下穿DEA
底背离:股价创新低但DIFF没有新低。把最近一次死叉到金叉的区间称为区间1,把再前一次死叉到金叉的区间称为区间2。区间1的最低股价小于区间2的最低股价,区间1的最低DIFF大于区间2的最低DIFF,此时的金叉作为买入信号。
如下图箭头处,股票价格创新低,dif指标背离式走高,出现底背离
顶背离:股价创新高但DIFF没有新高。把最近一次金叉到死叉的区间称为区间3,把再前一次金叉到死叉的区间称为区间4。区间3的最高股价大于区间4的最高股价,区间3的最高DIFF小于区间4的最高DIFF,此时的死叉作为卖出信号。
如下图箭头处,股票价格还在创新高,但 dif 指标背离式走低,出现顶背离。
7.2 相关指标编写
在编写策略之前,先来编写策略中用到的指标,写成函数形式添加到脚本indicator.splx中。
(1) EMA指标编写
EMA中文叫做指数移动平均,它是以指数式递减加权的移动平均,是一种趋势指标,要了均价趋势快慢的时候,用 EMA 更稳定。
EMA=q*当前价格+(1-q)* EMA[-1]
其中q是平滑系数,q=2/(移动周期+1)。
定义指标参数:
A | 序表,K线数据 |
ema | 指标返回的字段名 |
v | 要计算指数移动平均的字段,如收盘 |
n | 移动周期 |
编写指标函数,先计算平滑系数,再带入公式计算ema:
A | B | |
… | …… | …… |
12 | func EMA(A, $ema, $v, n) | =2/(n+1) |
13 | =A.run(if(#>1,B12*${v} + (1-B12)*${ema}[-1],${v}):${ema}) |
这里再复习一遍,参数前面加 $ 可以把字段名转化成字符串。代码中的${}是宏,使用宏可以将字符串作为表达式参与计算。
指标计算要用A.run()在原数据上进行。
(2) MACD指标编写
MACD指标返回三个值,DIF、EDA、MACD。
长短线的离差DIFF:短线EMA-长线EMA。
离差平均值DEA:DIFF的指数移动平均,移动周期通常取9;
MACD:2 *(DIFF - DEA)
定义指标参数:
A | 序表,K线数据 |
dif | 要返回的def字段名 |
dea | 要返回的dea字段名 |
macd | 要返回的macd字段名 |
v | 要计算指数移动平均的字段,如收盘 |
n1 | ema短线周期 |
n2 | ema长线周期 |
n3 | dea移动周期 |
编写函数代码:
A | B | C | |
… | …… | …… | …… |
15 | func MACD(A, $dif, $dea, $macd, $v, n1, n2, n3) | =A.derive@o(:macd1, :macd2) | |
16 | =EMA(A, macd1, ${v}, n1 ) | =EMA(A, macd2, ${v}, n2 ) | |
17 | =A.run(${dif} = macd1 - macd2 ) | =EMA(A, ${dea}, ${dif}, n3 ) | |
18 | =A.alter(; macd1, macd2) | =A.run(${macd}=2*(${dif}-${dea})) |
在MACD函数中引用了EMA函数,要注意中间变量的使用规则和命名,我们在6.2小节中讲过,这里不再赘述。
(3) 背离指标编写
底背离:股价创新低但DIFF没有新低。把最近一次死叉到金叉的区间称为区间1,把再前一次死叉到金叉的区间称为区间2。区间1的最低股价小于区间2的最低股价,区间1的最低DIFF大于区间2的最低DIFF,此时的金叉作为买入信号。
顶背离:股价创新高但DIFF没有新高。把最近一次金叉到死叉的区间称为区间3,把再前一次金叉到死叉的区间称为区间4。区间3的最高股价大于区间4的最高股价,区间3的最高DIFF小于区间4的最高DIFF,此时的死叉作为卖出信号。
背离指标的计算有点复杂,我们先在K线数据上计算出来,再封装为函数。
计算背离指标需要用到DIF和金叉死叉值,首先读取K线,算出要用到的值:
A | B | |
1 | >call("init.splx") | |
2 | 2023 | =date(A2,1,1) |
3 | =B2-100 | =date(A2,12,31) |
4 | 600690 | =Load@C(A4, A3,B3) |
5 | ||
6 | =B4.derive( :DIF, :DEA, :MACD, :GDX, :DVG, :昨日 ) | |
7 | =MACD( A6, DIF, DEA, MACD, 收盘, 12, 26, 9 ) | |
8 | =GDX(A6, GDX, DIF, DEA) |
A1:B4 读取K线数据。因为要计算指数平均,并且背离指标也要前两个区间值,所以要往前多读一部分数据。
A6 在K线上增加需要的指标值,DVG表示背离指标
A7 调用MACD函数,得到DIF值
A8 调用GDX函数,得到DIF和DEA两条线的金叉死叉值。
然后开始计算背离指标,按照背离指标的计算方法,先将数据划分为金叉到死叉,死叉到金叉……这样的区间,然后再比较相应区间内的最高(低)股价和最高(低)DIF值。
A | |
… | …… |
9 | =A6.group@i(GDX!=0) |
10 | =A9.(~|~[1](1) ) |
11 | =A10.run(x=~.GDX, ~.DVG = if ( #>3 && ~[-1].min( x*收盘 )<~[-3].min(x*收盘)&& [-1].min(x*DIF)>~[-3].min(x*DIF), x, 0 ) ) |
12 | =A6.run(DVG=ifn(DVG,0)) |
A9 group是分组函数,加选项@i时表示如果括号里表达式为true,则开始新的一组,也就是当GDX的值不为0时开始新的一组。分组后,GDX按照金叉、死叉、金叉……划分为了多个区间。
然后再将下一组的第1行并到当前组的后面就形成了金叉到死叉、死叉到金叉……这样的区间。
A10 中~和~[1]我们已经很熟悉了,表示当前组和下一组,~[1]后面又加了小括号(1)表示什么呢。在SPL里A(i)这样的语法表示取A的第i个成员。所以~[1](1)就表示下一组的第1行。用符号|将当前组和下一组的第1行合并,就实现了金叉到死叉、死叉到金叉……这样的区间。
区间划分好后,就可以找到相应的价格进行比较了。
A11 A.run()中的参数是两个以逗号分隔的表达式,第1个表达式定义了一个临时变量x,~.GDX表示取序表的第一个GDX值;同理第2个表达式中~.DVG表示当前组的第一个DVG值,等号后面语句虽长,但是并不难,就是到两个间隔的区间取出对应的最值价格进行比较,满足条件就返回此时的GDX值,反之返回0。
=A10.run( x=~.GDX , ~.DVG = if (#>3 && ~[-1].min( x*收盘 )<~[-3].min(x* 收盘) && [-1].min(x*DIF)>~[-3].min(x*DIF), x, 0 ) )
A11 执行完后,每一个金叉和死叉日的DVG值就有了。
A12 将其余DVG为null的设为0。ifn函数会返回参数中的第1个非空成员,这样DVG为空的就会返回0。
这样我们就完成了背离指标DVG的计算。
下一步将其封装为函数,添加到指标脚本中。
定义参数:
A | 序表,K线数据 |
dvg | 要返回的指标字段名 |
gdx | 要传入的gdx指标 |
dif | 要传入的dif指标 |
pr | 要参与计算的股价,如收盘 |
函数代码:
A | B | |
… | …… | …… |
20 | func DVG(A, $dvg, $gdx, $dif, $pr) | =A.group@i(${gdx}!=0 ).( ~|~[1](1) ) |
21 | =B20.run(x=~.${gdx}, ~1.${dvg} = if (#>3 && ~[-1].min(x*${pr} )<~[-3].min(x*${pr}) && [-1].min(x*${dif})>~[-3].min(x*${dif}), x, 0 ) ) | |
22 | =A.run(${dvg}=ifn(${dvg},0) ) |
7.3 MACD 背离策略编写
有了指标,策略就很容易写了。
当指标DVG为1时就买入1手,当DVG为-1时就全部卖出。
还是按照和均价策略相同的思路来写:
1. 登记脚本和读取数据
2. 计算策略需要的指标
3. 循环K线,进行买卖计算
4. 统计回测结果
策略代码:
A | B | |
1 | >call("init.splx") | |
2 | 2024 | =date(A2,1,1) |
3 | =B2-100 | =date(A2,12,31) |
4 | 600690 | =Load@C(A4, A3,B3) |
5 | ||
6 | =B4.derive(:DIF, :DEA, :MACD, :GDX, :DVG) | |
7 | =MACD( A6, DIF, DEA, MACD, 收盘, 12, 26, 9 ) | |
8 | =GDX(A6, GDX, DIF, DEA) | |
9 | =DVG( A6, DVG,GDX,DIF,收盘) | |
10 | ||
11 | =Begin(A6) | |
12 | =A11.select( 日期>=B2) | |
13 | for A12 | =H=昨日.持仓 |
14 | =卖出=if(昨日.DVG==-1,SellOff(~, H) ) | |
15 | =if(昨日.DVG==1 && H.len()<=0, 买入|=Buy(~,100)) | |
16 | =Loop() | |
17 | =Summary(A12) | =Display(A17) |
这段代码风格和前面一致,读者完全可以自己写出来。
我们在这里又用了缺省的昨日收盘价,这可能导致买卖不成功,也可以看看这种情况的回测结果。
另外,在B15格的加了H.len()<=0的条件,表示当前是该股票处于空仓状态才会买入,读者可以根据实际意愿决定是否加入这个条件。
类似地,这个背离指标DVG也可以整合成一个函数,我们给这个函数起名为DVGMACD。
它的输入参数为:
A | 序表,K线数据 |
dvg | 要返回的指标字段名 |
v | 要计算指数移动平均的字段,如收盘 |
n1 | ema短线周期 |
n2 | ema长线周期 |
n3 | dea移动周期 |
函数代码:
A | B | |
… | …… | …… |
24 | func DVGMACD(A, $dvg, $v, n1, n2, n3) | =A.derive@o(:dvgmacd_dif, :dvgmacd_dea, :dvgmacd_macd, :dvgmacd_gdx) |
25 | =MACD(A, dvgmacd_dif, dvgmacd_dea, dvgmacd_macd, ${v}, n1, n2, n3 ) | |
26 | =GDX(A, dvgmacd_gdx, dvgmacd_dif, dvgmacd_dea) | |
27 | =DVG(A, ${dvg}, dvgmacd_gdx, dvgmacd_dif, ${v} ) | |
28 | =A.alter(; dvgmacd_dif, dvgmacd_dea, dvgmacd_macd, dvgmacd_gdx) |
直接调用DVGMACD函数来编写策略:
A | B | |
1 | >call("init.splx") | |
2 | 2024 | =date(A2,1,1) |
3 | =B2-100 | =date(A2,12,31) |
4 | 600690 | =Load@C(A4, A3,B3) |
5 | ||
6 | =B4.derive(:DVG) | |
7 | =DVGMACD( A6, DVG, 收盘, 12, 26, 9 ) | |
8 | ||
9 | =Begin(A6) | |
10 | =A9.select( 日期>=B2) | |
11 | for A10 | =H=昨日.持仓 |
12 | =卖出=if(昨日.DVG==-1,SellOff(~, H) ) | |
13 | =if(昨日.DVG==1 && H.len()<=0, 买入|=Buy(~,100)) | |
14 | =Loop(null) | |
15 | =Summary(A10) | =Display(A15) |
完善的指标体系,可以有效提高策略开发效率。
7.4 多支股票和平仓处理
在单只股票策略的基础上加一层循环,改造成多支股票代码:
A | B | C | |
1 | >call("init.splx") | ||
2 | 2024 | =date(A2,1,1) | |
3 | =B2-100 | =date(A2,12,31) | |
4 | [1,600690] | =Load@C(A4, A3,B3) | |
5 | |||
6 | for B4.group(代码) | =A6.derive(:DVG) | |
7 | =DVGMACD( B6, DVG, 收盘, 12, 26, 9 ) | ||
8 | =Begin(B6) | ||
9 | =B8.select( 日期>=B2) | ||
10 | for B9 | =H=昨日.持仓 | |
11 | =卖出=if(昨日.DVG==-1,SellOff(~, H) ) | ||
12 | =if(昨日.DVG==1, 买入|=Buy(~,100)) | ||
13 | =Loop() | ||
14 | if !B9.pselect( 买入.len()>0) | next | |
15 | =@|Summary(B9) | =Yield(@|B9) | |
16 | =Total(B15,C15) | =Display(A16) |
这里多了一句B14,当股票没有任何买入信息时,就跳过B15和C15的汇总计算。
next语句用在循环结构中,表示跳出本次循环,执行下一次循环。如果B14的if条件为真,则跳过B15和C15,循环下一支股票。
顺便提一句,这里就没有写上一节说过的空仓条件。这样,即使该股票已有持仓,如果条件合适时仍然会继续买入。
在实际操作中,我们还可以加上平仓条件,用来止盈和止损。
在回测脚本中添加平仓函数:
A | B | C | |
… | …… | …… | …… |
43 | func SellAll(K, H, U=null, L=null, N=null) | =H.select( !卖日 ) | =if(N>0, workday@b( K.日期,-N,HOLIDAY),K.日期+N) |
44 | =B43.sum(股数*买价)/B43.sum(股数) | ||
45 | =(U && K.最高>(P=B44*(1+U))) || (P=null) || (L && K.昨日.收盘<B44*(1-L)) || ( N && C43>=B43.max(买日) ) | ||
46 | =if(B45, B43.select@0( Sell( K, ~, P) ) ) | ||
47 | func SellEach(K, H, U=null, L=null, N=null) | =H.select( !卖日 ) | =if(N>0, workday@b( K.日期,-N,HOLIDAY),K.日期+N) |
48 | =B47.select( (U && K.最高>(卖价=买价*(1+U))) || (卖价=null) || (L && K.昨日.收盘<买价*(1-L)) || ( N && C48>=买日) ) | ||
49 | =B48.select@0( Sell( K, ~, 卖价 ) ) |
1. SellAll(K, H, U=null, L=null, N=null):按条件批量卖出函数,当股票上
涨或下跌到一定程度或持天数大于N时,卖出。返回交易信息记录(集)。
参数:
K:当日K线数据。
U:up 上涨百分比。如果策略中不设上涨限制,则忽略U值即可,例如SellAll(K,H,,0.3,0) 表示下跌超过30%时卖出。
L:low 下跌百分比。同理如果策略中不设下跌限制,则忽略L值即可,例如SellAll( K,H,0.5) 表示上涨超过50%时卖出。
N:持有天数。当N>0时,按照交易日统计天数,当N<0时,按照日历日统计天数。
同理N值也可忽略。为了计算交易日数据,这里需要引入一个全程变量HOLIDAY记录所有非周末的休市日期。我们将提前整理好的非周末休市日期保存到shuholiday.csv中(该数据我们会定时更新,读者及时下载最新数据即可)。
需要说明的,上涨时的止盈处理和下跌时的止损处理并不对称,如果最高价超过期望值,那通常可以认为一定能卖得出来。但如果最低价跌过期望值,却不一定总能卖得出来,这里缺省会用昨日收盘价尝试卖出,如果失败,则会在下一天继续判断,直到成功为止。持有天数的处理也是类似,使用昨日收盘价不一定总能成功卖出,有可能会继续持有。可以修改这段代码来实现其它逻辑。
然后还要在 init.splx 脚本中将 holiday.csv 读入,并登记为全局变量 HOLIDAY:
A | |
… | …… |
6 | =env(HOLIDAY, file(DATAPATH/"holiday.csv").import@i() ) |
这样,C44 格的语句就可以正常运行了。
2. SellEach(K, H, U=null, L=null, N=null):按条件批量卖出函数,当股票上
涨或下跌到一定程度或持天数大于N时,卖出。返回交易信息记录(集)。
参数:
K:当日K线数据。
U:up 上涨百分比。如果策略中不设上涨限制,则忽略U值即可,例如SellAll(K,H,,0.3,0) 表示下跌超过30%时卖出。
L:low 下跌百分比。同理如果策略中不设下跌限制,则忽略L值即可,例如SellAll( K,H,0.5) 表示上涨超过50%时卖出。
N:持有天数。当N>0时,按照交易日统计天数,当N<0时,按照日历日统计天数。
SellAll和SellEach都用来平仓,不同的是平仓价格的计算不同。SellAll按照当前持仓股票均价和最后一次买入时间来计算是否达到平仓条件,而SellEach则将每次买入单独处理,分别按各自的买入价格和买入时间计算是否该平仓。
将多支股票的代码加上平仓处理,同时也将提取读数的天数改成按交易日计算:
A | B | C | |
1 | >call("init.splx") | ||
2 | 2024 | =date(A2,1,1) | |
3 | =workday(B2, -100, HOLIDAY) | =date(A2,12,31) | |
4 | [1,600690] | =Load@C(A4, A3,B3) | |
5 | |||
6 | for B4.group(代码) | =A6.derive(:DVG) | |
7 | =DVGMACD( B6, DVG, 收盘, 12, 26, 9 ) | ||
8 | =Begin(B6) | ||
9 | =B8.select( 日期>=B2) | ||
10 | for B9 | =H=昨日.持仓 | |
11 | =卖出=if(昨日.DVG==-1,SellOff(~, H) )| SellAll( ~, H, 0.2, 0.2, 60) | ||
12 | =if(昨日.DVG==1, 买入|=Buy(~,100)) | ||
13 | =Loop() | ||
14 | if !B9.pselect( 买入.len()>0) | next | |
15 | =@|Summary(B9) | =Yield(@|B9) | |
16 | =Total(B15,C15) | =Display(A16) |
A3 workday(t,k,h)函数可以计算出和日期t相距k个工作日的日期。这里的h是非周末的休市日期,workday在计算时会将这些日期剔除。因此A3语句就可以计算出2024年1月1日往前100个交易日的日期值。
C11中的SellAll函数为平仓函数,当股票上涨或下跌超过20%,又或持有交易日数大于60时,强制卖出。
更多推荐
所有评论(0)