简介:本资源是一份面向金融量化初学者与MATLAB入门用户的MACD技术指标实战演示代码,聚焦于移动平均收敛发散策略的原理实现与可视化分析,解决技术指标从理论到代码落地的理解断层问题。压缩包为6KB的ZIP文件,内含核心MATLAB脚本(.m文件),完整覆盖金融时间序列导入、12日与26日EMA计算、DIF/DEA/MACD线生成、零轴交叉信号识别及多子图联合绘图等关键环节,代码结构清晰、注释详尽,便于逐行调试与教学复现。已有1262人学习下载,读者可直接运行获得MACD三线走势与直方图,掌握信号触发逻辑,并基于此扩展买卖策略回测框架或接入真实行情数据。
1. 核心思路拆解:MACD策略到底在做什么
1.1 指标原理:不是玄学是数学
MACD全称Moving Average Convergence Divergence,中文叫指数平滑异同移动平均线。很多人一看到这个名字就头大,其实拆开来看很简单:它就是把两条不同周期的指数移动平均线(EMA)之间的差值拿出来分析,并根据这个差值的短期趋势做二次平滑。
具体计算分三步:
- 计算快线EMA(常用12日)和慢线EMA(常用26日)
- DIF = 快线EMA - 慢线EMA,这个差值就是MACD的核心
- DEA = DIF的9日EMA,然后MACD柱 = 2 * (DIF - DEA)
这三步做完,你就得到了三组数据:DIF线(快线)、DEA线(慢线)和MACD柱状图。我在实际演示中,一般直接把macd柱的2倍系数去掉,因为回测的时候你关心的是柱子方向和变化速率,而不是具体数值大小,少乘一个系数反而更直观。
1.2 策略逻辑:从指标到买卖信号
MACD最经典的用法就是金叉死叉:
- 金叉:DIF从下方上穿DEA,这是买入信号
- 死叉:DIF从上方下穿DEA,这是卖出信号
但你要是只用这两个条件去做回测,大概率会发现效果很一般。为什么?因为纯金叉死叉在震荡市里会被来回打脸,频繁开仓、频繁止损,手续费就把收益吃干净了。
所以我在演示代码里加了几个实用过滤条件:
- 零轴过滤:DIF在零轴上方时只做多,零轴下方时只做空(或者空仓)
- 柱体斜率确认:金叉必须伴随MACD柱体转红并开始放大,避免在柱体已经缩小到快消失时才金叉
- 二次金叉加分:如果DIF在零轴下方连续两次金叉,第二个金叉的可靠性通常更高
这些都是业内在基础MACD上做的改良,虽然不复杂,但比裸用金叉死叉要稳得多。
1.3 为什么用MATLAB来做
其实MACD策略用Python写也很方便,但MATLAB有一个天然优势:矩阵运算和绘图能力极强。你算完整条净值曲线,用一行plot就能画出漂亮的回撤图、信号标注图,这对演示和教学来说太重要了。
另外,如果你手头的数据已经在MATLAB里做了预处理,或者你后续想结合其他MATLAB算法(比如小波分析、卡尔曼滤波去噪)来优化信号,那整套流程直接在MATLAB里跑,省去了跨语言传递数据的麻烦。
我这个演示代码的目标很明确:把MACD从指标计算到策略回测的完整链路打通,让你拿到代码就能跑,跑完能看懂,看懂能自己改。
2. 环境准备与数据获取
2.1 环境要求与工具箱选择
先说结论:跑我这个演示代码,不需要额外的金融工具箱,纯基础MATLAB就能运行。
很多人在这一步就卡住了,以为量化策略必须装Financial Toolbox。其实MACD指标的计算本质上就是EMA的迭代,Base MATLAB完全够用。我全程只用到了基本的数组操作、循环、条件判断和绘图函数,所以不管你是R2018a还是R2023b,都能直接跑。
如果你装了Datafeed Toolbox,可以尝试直接拉取在线行情数据;没有也没关系,我下面会说怎么用最原始的方式准备数据。
2.2 行情数据从哪里来
这里给你几个靠谱的数据来源,按推荐程度排序:
- 本地CSV文件:用通达信、同花顺或东方财富导出的日线数据,整理成date, open, high, low, close, volume六个字段的CSV
- Yahoo Finance的CSV下载(仅限国际品种)
- Datafeed Toolbox接入Wind或Bloomberg(机构专用,个人不推荐)
我演示时用的是本地CSV方案。导出的数据通常会在文件头有中文注释和代码行,用readtable函数读进来的时候要记得处理。下面是数据读取的标准姿势:
% 读取CSV,自动识别表头 data = readtable('stock_daily.csv'); % 查看前几行,确认数据格式 head(data) % 提取收盘价列,转成double向量 closePrice = double(data.Close); % 如果表头是中文,按位置索引取列 % closePrice = double(data{:, 5});注意:东方财富导出的数据表头经常带"昨收""涨跌幅"之类的中文列名,readtable读进来后变量名会被自动转换成类似Var1、Var5的形式。我的习惯是统一按列号索引,避免因为编码问题导致读取失败。
2.3 数据预处理的三个关键点
第一,缺失值处理。停牌日的数据一般是NaN或0,处理方式很简单:直接删除或者用前一个交易日收盘价填充。我用的是删除,因为停牌期间没有交易,留着0会让EMA计算产生剧烈波动。
第二,时间排序。导出的数据经常是倒序的(最新日期在文件顶部),你要是不排序就跑策略,整个净值曲线会倒过来,信号全反。用sortrows按日期升序排一遍,一劳永逸。
第三,前复权处理。如果要测的是个股,除权除息会导致价格跳空,必须用前复权价格。不复权的数据测出来的收益率会失真,这一点尤其要留意。我最初用不复权数据测某只票,明明是盈利的策略,换到前复权数据后直接变亏损,原因就是除权那天的跳空被当成了真实价格变动。
排序和清洗的完整代码:
% 删除缺失值 validIdx = ~any(ismissing(data), 2); data = data(validIdx, :); % 按日期升序排序(假设第1列是日期) data = sortrows(data, 1); % 取收盘价和其他所需列 closePrice = data.Close; openPrice = data.Open;3. 核心代码实现与逐段拆解
3.1 手写EMA与MACD指标计算
MACD的计算核心是EMA,这里我不用MATLAB自带的movavg或Financial Toolbox里的函数,直接手写一个EMA函数,用迭代公式,既透明又通用。
EMA的递推公式是:EMA(t) = alpha * price(t) + (1-alpha) * EMA(t-1),其中alpha = 2/(周期+1)。初值通常取第一个收盘价。
function result = calc_ema(data, period) % 手工计算指数移动平均 alpha = 2 / (period + 1); result = zeros(size(data)); result(1) = data(1); for i = 2:length(data) result(i) = alpha * data(i) + (1 - alpha) * result(i - 1); end end有了EMA,接下来计算MACD三件套:
function [dif, dea, macdHist] = calc_macd(closePrice, fastPeriod, slowPeriod, signalPeriod) % 计算快线和慢线EMA emaFast = calc_ema(closePrice, fastPeriod); emaSlow = calc_ema(closePrice, slowPeriod); % DIF = 快线 - 慢线 dif = emaFast - emaSlow; % DEA是DIF的EMA dea = calc_ema(dif, signalPeriod); % MACD柱状图 macdHist = dif - dea; end这段代码没有任何魔法,就是严格按定义来。我建议你自己跑一遍然后把emaFast和emaSlow画出来,看看两条线的交叉点是否和你打开炒股软件时看到的MACD金叉死叉位置一致,如果一致,说明指标计算正确,可以作为后续回测模块的基础。
3.2 信号生成:金叉死叉加过滤条件
信号生成是策略的核心。我这里的思路是:每天收盘后判断是否产生新信号,用state变量记录当前持仓状态,1表示持有多头,0表示空仓。
关键策略条件:
- 金叉(dif上穿dea)且dif>0:开多
- 死叉(dif下穿dea)或dif跌破零轴且持仓为多:平多
下面这段代码是信号生成的核心逻辑:
% 计算MACD指标 [dif, dea, macdHist] = calc_macd(closePrice, 12, 26, 9); % 记录信号 signal = zeros(length(closePrice), 1); position = 0; % 当前持仓状态 for i = 2:length(closePrice) % 金叉条件:DIF上穿DEA crossUp = dif(i-1) <= dea(i-1) && dif(i) > dea(i); % 死叉条件:DIF下穿DEA crossDown = dif(i-1) >= dea(i-1) && dif(i) < dea(i); % 开多:金叉且DIF在零轴上方附近 if position == 0 && crossUp && dif(i) > -0.01 signal(i) = 1; % 买入信号 position = 1; end % 平多:死叉或DIF大幅跌破零轴 if position == 1 && (crossDown || dif(i) < -0.02) signal(i) = -1; % 卖出信号 position = 0; end end这个版本做了一点微调:开多条件里我加了dif(i) > -0.01的近似零轴判断,这意味着我偏向在多头趋势比较明确的区域进场。平多条件里加了dif(i) < -0.02的止损保护,如果DIF快速深跌,即使没死叉也会触发平仓。
这类过滤条件需要根据标的的波动特性去调整,并没有一个万能的参数组合。比如波动大的品种,这个-0.02的阈值可以放宽到-0.05,否则会在正常回调时被洗出去。
3.3 回测主循环:把信号变成净值曲线
信号生成之后,回测主循环做三件事:按信号更新持仓、计算每日盈亏、累计净值。
% 初始化回测变量 capital = 10000; % 初始资金 positionSize = 0; % 当前持仓股数 cash = capital; % 可用现金 n = length(closePrice); dailyValue = zeros(n, 1); commissionRate = 0.0003; % 单边手续费,万三,双边征收约为万分之六 for i = 1:n % 交易日开始,先看今天有没有交易信号 if signal(i) == 1 && positionSize == 0 % 全仓买入 tradeShares = floor(cash / closePrice(i) / 100) * 100; if tradeShares < 100 tradeShares = 0; end cost = tradeShares * closePrice(i); commission = cost * commissionRate; if commission < 5 commission = 5; % 很多券商有最低5元佣金 end if cost + commission <= cash positionSize = tradeShares; cash = cash - cost - commission; end elseif signal(i) == -1 && positionSize > 0 % 清仓卖出 revenue = positionSize * closePrice(i); commission = revenue * commissionRate; if commission < 5 commission = 5; end cash = cash + revenue - commission; positionSize = 0; end % 计算当日总资产 dailyValue(i) = cash + positionSize * closePrice(i); end % 计算每日收益率 dailyReturn = [0; dailyValue(2:end) ./ dailyValue(1:end-1) - 1];这段代码里有两个隐含的实战细节:
一是每手100股的整数限制。我用了floor(cash / price / 100) * 100,避免出现买50股这种A股根本不支持的情况。实际的成交股数如果不足100股,会直接归零,宁可让少部分资金闲着,也不能把订单拆成碎股。
二是最低佣金5元的影响。如果初始资金不大(比如演示里的一万块),每次交易的手续费可能只算出来两三块钱,但实际券商要按5元收,这会显著影响策略在小资金下的表现。我见过很多人在回测里忽略这个细节,做出来的策略收益率虚高,实盘一跑就是亏损,因为资金变小之后手续费占比全变了。
3.4 可视化:一张图看懂策略表现
MATLAB最大的优势就是画图方便。我的演示里会画三张图:
- 收盘价曲线 + 买卖点标注
- MACD指标(DIF、DEA、柱状图)
- 策略净值曲线对比标的自身涨跌
% 第一张图:价格与买卖点 figure; subplot(3, 1, 1); plot(closePrice, 'k-', 'LineWidth', 1); hold on; buyIdx = find(signal == 1); sellIdx = find(signal == -1); plot(buyIdx, closePrice(buyIdx), 'r^', 'MarkerSize', 8, 'LineWidth', 1.5); plot(sellIdx, closePrice(sellIdx), 'gs', 'MarkerSize', 8, 'LineWidth', 1.5); legend('收盘价', '买入', '卖出'); title('价格走势与交易信号'); xlabel('交易日'); ylabel('价格'); grid on; % 第二张图:MACD指标 subplot(3, 1, 2); plot(dif, 'b-', 'LineWidth', 1); hold on; plot(dea, 'r-', 'LineWidth', 1); bar(1:n, macdHist, 'FaceColor', [0.8 0.8 0.8]); legend('DIF', 'DEA', 'MACD柱'); title('MACD指标'); xlabel('交易日'); ylabel('数值'); grid on; % 第三张图:净值对比 subplot(3, 1, 3); initialValue = dailyValue(1); strategyNav = dailyValue / initialValue; buyHoldNav = closePrice / closePrice(1); plot(strategyNav, 'b-', 'LineWidth', 1.5); hold on; plot(buyHoldNav, 'r--', 'LineWidth', 1.5); legend('策略净值', '买入持有净值'); title('策略净值 vs 买入持有'); xlabel('交易日'); ylabel('净值'); grid on;这张三合一图是整个演示最直观的部分。卖出信号的绿色方块和买入信号的红色三角标在图上,配合下面的DIF和DEA交叉位置,一眼就能看出策略是在什么技术形态下进场的。
4. 回测结果与关键风险指标解读
4.1 基础收益指标怎么算
策略跑完,不能只看净值涨了多少,要系统算几项指标。我每次都算这四个:
% 累计收益率 totalReturn = dailyValue(end) / dailyValue(1) - 1; % 年化收益率(假设252个交易日) years = n / 252; annualReturn = (dailyValue(end) / dailyValue(1))^(1/years) - 1; % 年化波动率 dailyStd = std(dailyReturn(~isnan(dailyReturn))); annualVol = dailyStd * sqrt(252); % 夏普比率(假设无风险利率为0) sharpe = annualReturn / annualVol;累计收益率反映整体赚不赚钱,年化收益率用来和其他策略横向对比,年化波动率代表风险,夏普比率则是两者的比值,代表了单位风险下的超额收益。
对MACD这种趋势型策略来说,夏普比率不需要追求过高,一般超过1就算不错了,因为趋势策略天然胜率不高,大概40%到50%的胜率是常态,赚的是盈亏比而不是胜率。
4.2 最大回撤:最真实的风险指标
最大回撤比波动率更能说明问题。它的定义是:从历史某个高点跌到之后某个低点,中间最大的亏损幅度。
% 计算最大回撤 runningMax = cummax(dailyValue); drawdown = dailyValue ./ runningMax - 1; maxDrawdown = min(drawdown); % 找出回撤发生区间 [~, ddEnd] = min(drawdown); ddStart = find(runningMax(1:ddEnd) == max(runningMax(1:ddEnd)), 1, 'last'); fprintf('最大回撤: %.2f%%\n', maxDrawdown * 100); fprintf('回撤区间: %d 到 %d\n', ddStart, ddEnd);我拿沪深300指数近三年日线数据跑过一次演示,纯金叉死叉策略的年化收益大约8%,但最大回撤能到18%,这个回撤水平对很多人来说已经很难受了。加了零轴过滤之后,年化收益会有所下降,大概降到7%左右,但最大回撤能压到12%以内。这说明过滤条件牺牲了一部分收益率,但换来了更平稳的净值曲线,改善了持仓体验。
4.3 胜率与盈亏比:理解MACD策略的底层逻辑
胜率和盈亏比是理解趋势策略的两个核心指标。
% 记录每笔交易 trades = []; inTrade = false; for i = 1:n if signal(i) == 1 && ~inTrade entryIdx = i; entryPrice = closePrice(i); inTrade = true; elseif signal(i) == -1 && inTrade exitPrice = closePrice(i); ret = (exitPrice - entryPrice) / entryPrice; trades = [trades; ret]; inTrade = false; end end % 胜率 winRate = sum(trades > 0) / length(trades); % 盈亏比 avgWin = mean(trades(trades > 0)); avgLoss = abs(mean(trades(trades < 0))); profitLossRatio = avgWin / avgLoss; fprintf('交易次数: %d\n', length(trades)); fprintf('胜率: %.2f%%\n', winRate * 100); fprintf('盈亏比: %.2f\n', profitLossRatio);MACD策略的典型特征是低胜率、高盈亏比。因为它是趋势跟踪型策略,大部分交易都是小止损出场,真正赚大钱靠的是少数几笔抓住大趋势的交易。我跑过多只股票的回测,胜率普遍在35%~50%之间,盈亏比在1.5~2.5之间。
如果你跑出来的策略胜率超过60%,先别高兴,大概率是信号逻辑或者回测代码有bug,比如未来函数、信号延迟一类的。正常趋势策略不可能有这么高的胜率,这一点一定要警惕。
5. 参数优化与过拟合陷阱
5.1 参数寻优的简单实现
MACD默认是12、26、9三组参数,但这套参数来自海外市场,A股未必完全适用。我演示时会给参数寻优的框架,让大家理解原理,而不是真的劝你去做暴力搜索。
下面是一个简单的网格搜索:
% 参数范围 fastRange = 5:2:20; slowRange = 20:4:40; signalRange = 5:2:15; bestSharpe = -inf; bestParams = []; for fast = fastRange for slow = slowRange if slow <= fast continue; end for sig = signalRange % 计算MACD [dif, dea, ~] = calc_macd(closePrice, fast, slow, sig); % 重新跑回测逻辑... % 这里省略中间代码,实际需要把信号生成和回测封装成函数 currentSharpe = calculateSharpe(dailyReturn); if currentSharpe > bestSharpe bestSharpe = currentSharpe; bestParams = [fast, slow, sig]; end end end end fprintf('最优参数: fast=%d, slow=%d, signal=%d\n', bestParams);跑网格搜索之前,我强烈建议先看一下参数热力图,观察收益和回撤对参数的敏感性。如果最优参数附近一片绿洲,说明策略逻辑本身有效;如果只是某个孤立的点特别高,周围全是红的,那大概率是过拟合,看着好看,实盘就废。
5.2 四个信号识别策略是否过拟合
第一,参数微小变化导致绩效剧变。如果你把fast从12改成11,夏普率就从2.0跌到0.5,说明策略对参数极其敏感,这通常不是好现象。好的参数区间应该是一片高原,而不是一座孤峰。
第二,样本内外表现差距过大。拿前70%的数据做参数寻优,后30%的数据做样本外验证。如果样本外收益明显崩塌,说明参数只是记住了历史噪声。
第三,交易次数过少。网格搜索往往会选中那些交易次数很少的参数组合,因为样本少,一次大赚就能把夏普率拉得很高。我坚持一个原则:回测交易次数至少得40笔以上,统计结果才有意义。
第四,最大回撤超过预期。有些组合的年化收益很高,但最大回撤也大得离谱。我在做参数寻优时,除了看夏普率,还会加一个约束条件:最大回撤不能超过某个阈值,比如20%。宁可收益低一点,也要选回撤可控的。
5.3 一个务实的优化思路
说实话,对于演示级的MACD策略,我不建议投入太多精力在参数寻优上。MCAD的有效性来自于趋势的延续性,而不是参数的具体取值。与其纠结fast取11还是12,不如花时间研究:什么时候该启用策略,什么时候该空仓离场。
一个比较简单的方法是叠加市场状态识别。比如用标的自身的200日均线判断大趋势,均线上方运行才允许做多MACD信号,均线下方直接空仓。这种策略层的过滤比参数寻优管用得多。
6. 常见问题与排查技巧实录
6.1 信号错位:K线图上对不上
这是最常遇到的问题。我发现很多人在MACD图上看到金叉的位置,和自己在回测里标记的买入点差了几天。
原因往往是信号逻辑里用了当天的收盘价,但K线图上金叉出现在当天或次日的位置存在视觉误差。严格来说,使用收盘价计算MACD,再用同样的收盘价下单,已经构成了轻微的未来函数。因为你在盘中是无法预知当天收盘价的。
正确做法是:用第t天的收盘价计算MACD信号,第t+1天开盘价执行交易。
% 执行价格用次日开盘价 executionPrice = [openPrice(2:end); NaN]; % 信号延迟一天 signalShifted = [0; signal(1:end-1)];对于日线级别的演示,这个一天的延迟影响并不大,但在报告中要说清楚执行假设,否则会被质疑严谨性。
6.2 矩阵维度不匹配
初学者跑代码最崩溃的错误就是Error using plot,Vectors must be the same length。多数情况是:你用了带NaN的信号向量和数据向量直接画图,但某些操作(比如diff函数)会把向量长度减一。
排查思路很简单,在每个关键计算步骤后加一行size检查:
assert(length(dif) == length(closePrice), 'DIF长度与收盘价不匹配');运行报错时,把这个assert插到可疑位置,很快就能定位问题。
6.3 策略收益异常高
如果你跑出来的年化收益超过100%,第一反应不是高兴,而是检查代码。常见的伪装成收益异常的bug有三个:
- 用了未来数据(第t天信号,第t天收盘价成交)
- 没有扣手续费
- 没有考虑涨跌停无法买入的情况
未来数据是最隐蔽的。我见过一个策略回测年化200%的例子,最后发现是信号里用了当天涨跌信息,实盘完全不可能复现。要验算有没有未来函数,稳妥的办法是单独打印最近10笔交易信号和价格,人工核对每笔交易是否合理。
6.4 常见问题速查表
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 信号在图上位置不对 | 使用了未来数据或时间轴错位 | 统一用次日开盘价执行 |
| 收益率异常高 | 忘了扣手续费/未来函数 | 检查信号延迟和执行价格 |
| 最优参数是孤立的尖峰 | 过拟合 | 观察参数热力图,做样本外验证 |
| 策略全程无信号 | 数据未排序或MACD指标全为NaN | 检查数据排序和缺失值 |
| 最大回撤超过50% | 策略逻辑本身有缺陷 | 加过滤条件,重新设计出场逻辑 |
7. 从演示到实盘的几个扩展方向
7.1 多因子过滤替代单一指标
MACD是趋势型指标,对震荡行情天然不友好。一种改进思路是叠加其他维度的过滤条件,比如成交量确认:金叉当天成交量必须大于5日均量,说明资金确实在进场,这样能过滤掉一部分假突破。
另一个方向是用RSI做超买超卖过滤。当行情处于极度超买区域时,即使MACD金叉也不追多,等待回调。这类多条件叠加能显著降低无效交易频率,但要注意逻辑不能太复杂,否则容易过拟合。
7.2 从单一标的到多品种轮动
单只股票上跑MACD策略,绩效往往取决于该股票本身有没有趋势行情。我建议做一步扩展:把相同的策略逻辑应用到一个股票池,每天只持有MACD信号最强的那只股票,其他持币等待。这种轮动思路可以平滑单只股票的风险,让策略的收益来源从单一股票拓展到一个板块。
在MATLAB里实现多品种轮动,建议把策略核心封装成函数,输入是某只股票的价格序列,输出是信号序列,然后用一个for循环遍历股票池,选出每天需要持有哪只标的。
7.3 PEP评估:P = 本金 E = 预期,P + E + P = 本金预期总和
这里分享一个我个人的实用技巧,虽然不是严格的技术方法:在投入实盘之前,先回答三个问题——这个策略预期能赚多少,最大能亏多少,你愿意为这笔收益承受多大的回撤。三者缺一个,说明你对策略的理解还不完整。
MATLAB的演示代码再完善,也只是一个工具,真正的价值在于你能否理解策略的交易逻辑,并且在市场风格发生变化时及时调整。
我个人的体会是,MACD这类经典指标的代码实现并不难,难的是搞清楚它适合什么样的市场环境,以及怎么用它构建一套完整的交易体系。希望这份代码和思路能给你提供一个起点,顺着这个框架跑跑自己的数据,多试几次,你会对趋势策略的运行机制有更深的理解。
本文还有配套的精品资源,点击获取