MACD指标回测实战

macd
Photo Credits: Unsplash

本文重点概要

  • 文章难度:★★☆☆☆
  • MACD指标回测与视觉化买卖点
  • 阅读建议:本文针对市场上常用的MACD指标进行更贴近现实的回测,并以视觉化的方式观察买卖点。而本文主要是提供读者回测所需考虑的面向与架构,而非支持技术指标的实用性。若读者欲了解其他常用的技术指标,可阅读 技术分析简介与回测,进而发想出有利可图的交易策略!

前言

MACD的中文为平滑异同移动平均线,为一种判断股价中长期趋势的指标。当快线(DIF)由下而上穿越慢线(MACD)时,代表股价有上涨的动能存在;反之快线(DIF)向下跌破慢线(MACD)时,代表股价下跌的机率相对高。由于此类型的指标有明确的买卖讯号,所以非常适用利用回测来验证该策略表现。

而一个有效的回测,除了有明确的买卖点之外,买卖股票的成本也必须考量到买卖讯号产生的时机、手续费与证交税,甚至是最低消费20元的限制,计算报酬率时亦要考虑手中现金部位,才会最贴近实际采用该策略的表现!

编辑环境及模组需求

本文使用 Windows OS 并以 Jupyter Notebook 作为编辑器

#基本功能
import numpy as np
import pandas as pd
#绘图
import plotly.graph_objects as go
from plotly import subplots
#TEJ
import tejapi
tejapi.ApiConfig.api_key = "Your Key"
tejapi.ApiConfig.ignoretz = True

资料库使用

  • 免费资料库 : 资料库代码 ‘ TRAIL/TAPRCD ’,包含上市柜公司于2020年间的未调整股价

资料处理

Step 1. 股价资料捞取

stock_data = tejapi.get('TRAIL/TAPRCD',
            coid= '3481',
            mdate={'gte': '2020-01-01', 'lte': '2020-12-31'},
            opts={'columns': ['coid', 'mdate', 'open_d','close_d']},
            chinese_column_name=True,
            paginate=True)

本文以捞取以群创光电(3481)于2020年间股价做为示范,栏位选择开盘价与收盘价,前者计算买卖价格与成本,后者用于计算买卖讯号

Step 2. 讯号撰写

stock_data['12_ema'] = stock_data['收盘价(元)'].ewm(span = 12).mean()
stock_data['26_ema'] = stock_data['收盘价(元)'].ewm(span = 26).mean()
stock_data['dif'] = stock_data['12_ema']  - stock_data['26_ema']
stock_data['macd'] = stock_data['dif'].ewm(span = 9).mean()

接下来以收盘价计算MACD。首先利用 ewm().mean() 的方式计算12天与26天的指数移动平均线,两者之间的差即为快线DIF,再以此快线计算9日指数移动平均,则可以得到慢线MACD

stock_data['买卖股数'] = 0
#如果黄金交叉,隔天开盘买进
stock_data['买卖股数'] = np.where((stock_data['dif'].shift(1)>stock_data['macd'].shift(1)) & (stock_data['dif'].shift(2)<stock_data['macd'].shift(2)), 1000, stock_data['买卖股数'])
#如果死亡交叉,隔天开盘卖出
stock_data['买卖股数'] = np.where((stock_data['dif'].shift(1)<stock_data['macd'].shift(1)) & (stock_data['dif'].shift(2)>stock_data['macd'].shift(2)), -1000, stock_data['买卖股数'])

有了慢线与快线之后,即可建立买卖股数讯号。当两日前的DIF仍小于MACD线stock_data[‘dif’].shift(2)<stock_data[‘macd’].shift(2),但却于昨日DIF大于MACD stock_data[‘dif’].shift(1)>stock_data[‘macd’].shift(1),代表昨日出现黄金交叉买点,而因为这个讯号于昨日收盘才能确定,因此于今日开盘才购买1000股,若不符合买点条件,则维持原栏位内容stock_data[‘买卖股数’]。同理,出现死亡交叉时,亦是隔日开盘时才卖出1000股。

Step 3. 计算交易成本

stock_data['手续费'] = stock_data['开盘价(元)']* abs(stock_data['买卖股数'])*0.001425
stock_data['手续费'] = np.where((stock_data['手续费']>0)&(stock_data['手续费'] < 20), 20, stock_data['手续费'])
stock_data['证交税'] = np.where(stock_data['买卖股数']<0, stock_data['开盘价(元)']* abs(stock_data['买卖股数'])*0.003, 0)
stock_data['摩擦成本'] = (stock_data['手续费'] + stock_data['证交税']).apply(np.floor)

摩擦成本包含了手续费(0.1425%)与证交税(0.3%)。当买入股票时,所需负担的是手续费,但要注意的是如果券商没有提供相关优惠,则往往会有低消20元限制;而卖出时要负担的是手续费与证交税。

报酬率计算

stock_data['股票价值'] = stock_data['买卖股数'].cumsum() * stock_data['收盘价(元)']
stock_data['现金价值'] = 10000 - stock_data['摩擦成本'] + (stock_data['开盘价(元)']* -stock_data['买卖股数']).cumsum() 
stock_data['资产价值'] = stock_data['股票价值'] + stock_data['现金价值']

假设初始现金有10000元台币,资产价值为股票部位价值与现金部位价值的加总。股票价值随著股票持有数量、股价波动;现金价值则随著买卖操作变动,买入股价时减少,卖出时增加,并考虑摩擦成本。

stock_data['当日价值变动(%)'] = (stock_data['资产价值']/stock_data['资产价值'].shift(1) - 1)*100
stock_data['累计报酬(%)'] = (stock_data['资产价值']/10000 - 1)*100

接著即可利用资产价值的每日变化,计算每日报酬率;也能以初始现金计算累计报酬。

视觉化

Step 1. 买卖点观察(详见完整程式码)

可以看到MACD策略的确可以掌握部分波段,但在盘整时有反复买卖的风险,仍需特别注意。

Step 2. 策略表现(详见完整程式码)

可以看到采用MACD策略,资产价值每日的波动在8%上下以内,若波动为0代表目前手中仅有现金部位。最后的累计报酬约为63%,优于同时期0050约30%左右报酬。

结论

读者应该可以发现,当买卖点触发过于频繁时,则累积的摩擦成本不容小觑,导致表现结果可能还不如期初买入并持有。而再反复调整初始资金后也能发现资产配置的重要性,因为当手中现金部位过大时,会拉低整体的报酬率,但相对地每日价值波动幅度也较低。其实如果要贴近现实的回测,也必须考虑持有期间的除权息,但本文为了搭配当时股价显示买卖点,才选择采用未调整的股价计算报酬率;若操作方向为做空的话,更需要考虑保证金问题,而本文首笔交易为买入,故皆为一买一卖操作。若读者想回测更长的区间、使用调整后的股价,推荐到 TEJ E-Shop 挑选最适合的方案!

完整程式码

延伸阅读

相关连结

返回总览页