第 7 章 MACD 背离策略

7.1 相关概念

MACDMoving 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. 长短线的离差DIF:短线EMA-长线EMA

4. 离差平均值DEADIF的指数移动平均,移动窗口通常取9

5. MACD2 *(DIF - DEA)

下面介绍MACD背离买卖策略:

金叉:DIF上穿DEA

死叉:DIF下穿DEA

底背离:股价创新低但DIF没有新低。把最近一次死叉到金叉的区间称为区间1,把再前一次死叉到金叉的区间称为区间2。区间1的最低股价小于区间2的最低股价,区间1的最低DIF大于区间2的最低DIF,此时的金叉作为买入信号。

如下图箭头处,股票价格创新低,dif指标背离式走高,出现底背离

..

顶背离:股价创新高但DIF没有新高。把最近一次金叉到死叉的区间称为区间3,把再前一次金叉到死叉的区间称为区间4。区间3的最高股价大于区间4的最高股价,区间3的最高DIF小于区间4的最高DIF,此时的死叉作为卖出信号。

如下图箭头处,股票价格还在创新高,但 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指标返回三个值,DIFEDAMACD

长短线的离差DIF:短线EMA-长线EMA

离差平均值DEADIF的指数移动平均,移动周期通常取9

MACD2 *(DIF - DEA)

定义指标参数:

A

序表,K线数据

dif

要填入的dif字段名

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) 背离指标编写

底背离:股价创新低但DIF没有新低。把最近一次死叉到金叉的区间称为区间1,把再前一次死叉到金叉的区间称为区间2。区间1的最低股价小于区间2的最低股价,区间1的最低DIF大于区间2的最低DIF,此时的金叉作为买入信号。

顶背离:股价创新高但DIF没有新高。把最近一次金叉到死叉的区间称为区间3,把再前一次金叉到死叉的区间称为区间4。区间3的最高股价大于区间4的最高股价,区间3的最高DIF小于区间4的最高DIF,此时的死叉作为卖出信号。

背离指标的计算有点复杂,我们先在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函数,得到DIFDEA两条线的金叉死叉值。

..

然后开始计算背离指标,按照背离指标的计算方法,先将数据划分为金叉到死叉,死叉到金叉……这样的区间,然后再比较相应区间内的最高(低)股价和最高(低)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)表示什么呢。在SPLA(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 将其余DVGnull的设为0ifn函数会返回参数中的第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 背离策略编写

有了指标,策略就很容易写了。

当指标DVG1时就买入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,当股票没有任何买入信息时,就跳过B15C15的汇总计算。

next语句用在循环结构中,表示跳出本次循环,执行下一次循环。如果B14if条件为真,则跳过B15C15,循环下一支股票。

顺便提一句,这里就没有写上一节说过的空仓条件。这样,即使该股票已有持仓,如果条件合适时仍然会继续买入。

在实际操作中,我们还可以加上平仓条件,用来止盈和止损。

在回测脚本中添加平仓函数:


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, ~, 卖价 ) )

13. SellAll(K, H, U=null, L=null, N=null)按条件批量卖出函数,当股票上

涨或下跌到一定程度或持有天数大于N时,卖出。返回交易信息记录(集)。

参数:

K:当日K线数据

Uup 上涨百分比。如果策略中不设上涨限制,则忽略U值即可,例如SellAll(K,H,,0.3,0) 表示下跌超过30%时卖出。

Llow 下跌百分比。同理如果策略中不设下跌限制,则忽略L值即可,例如SellAll( K,H0.5) 表示上涨超过50%时卖出。

N:持有天数。当N>0时,按照交易日统计天数,当N<0时,按照日历日统计天数。

同理N值也可忽略。为了计算交易日数据,这里需要引入一个全程变量HOLIDAY记录所有非周末的休市日期。我们将提前整理好的非周末休市日期保存到holiday.csv中(该数据我们会定时更新,读者及时下载最新数据即可)。

需要说明的是,上涨时的止盈处理和下跌时的止损处理并不对称,如果最高价超过期望值,那通常可以认为一定能卖得出去。但如果最低价跌过期望值,却不一定总能卖得出去,这里缺省会用昨日收盘价尝试卖出,如果失败,则会在下一天继续判断,直到成功为止。持有天数的处理也是类似,使用昨日收盘价不一定总能成功卖出,有可能会继续持有。可以修改这段代码来实现其它逻辑。

然后还要在 init.splx 脚本中将 holiday.csv 读入,并登记为全局变量 HOLIDAY:


A

……

6

=env(HOLIDAY, file(DATAPATH/"holiday.csv").import@i() )

这样,C43 格的语句就可以正常运行了。

14. SellEach(K, H, U=null, L=null, N=null)按条件批量卖出函数,当股票上

涨或下跌到一定程度或持有天数大于N时,卖出。返回交易信息记录(集)。

参数:

K:当日K线数据。

Uup 上涨百分比。如果策略中不设上涨限制,则忽略U值即可,例如SellAll(K,H,,0.3,0) 表示下跌超过30%时卖出。

Llow 下跌百分比。同理如果策略中不设下跌限制,则忽略L值即可,例如SellAll( K,H0.5) 表示上涨超过50%时卖出。

N:持有天数。当N>0时,按照交易日统计天数,当N<0时,按照日历日统计天数。N可忽略。

SellAllSellEach都用来平仓,不同的是平仓价格的计算不同。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语句就可以计算出202411日往前100个交易日的日期值。

C11中的SellAll函数为平仓函数,当股票上涨或下跌超过20%,又或持有交易日数大于60时,强制卖出。