简介:本资源是一份面向金融量化初学者与MATLAB入门用户的MACD技术指标实战演示代码包,聚焦于理解与复现经典趋势跟踪策略的核心逻辑。资源以简洁可运行的MATLAB脚本为核心,完整实现MACD三要素(DIF线、DEA信号线、MACD直方图)的计算、可视化及零轴交叉买卖信号标注,覆盖数据读取、EMA平滑计算、差值处理与多子图绘图等关键环节,助力用户建立从理论公式到工程落地的完整认知链条。压缩包为6KB的ZIP格式,内含主程序脚本及必要注释说明文件(具体文件数未提供),代码结构清晰、变量命名规范、关键步骤均附中文注解,便于逐行调试与策略扩展。目前已有1262人学习下载,适合用于课程实验、量化入门实训或个人交易系统原型开发的技术验证环节。 最近在整理一个MATLAB项目,标题写得很朴素,就叫“基本演示(MACD)”。但真正把它跑起来你会发现,这个脚本一点都不“基本”:MACD指标计算、金叉死叉信号识别、买卖点标注、资金曲线和回测统计,全在几十行代码里串成了一条完整的量化策略链路。对刚入门量化、又一直在用MATLAB做数据分析的人来说,这是很合适的练手样本。
这篇文章我就用这个项目标题作为主线,把基于MATLAB的MACD策略代码从原理到实现完整拆一遍,内容包括MACD三参数怎么算、金叉死叉信号怎么落到代码里、回测主循环怎么写、结果怎么看,以及我实际踩过的几个和数据对齐、信号偏移有关的坑。适合谁看?已经会一点MATLAB、想第一次尝试“策略+回测”的读者,以及写毕业论文需要做程序化交易演示的同学。如果你完全没接触过MACD,也没关系,下面会用最直白的话讲清楚。
1. 内容整体设计与思路拆解
1.1 这个演示到底在演示什么
“基本演示(MACD)”这个标题,拆开看有三个信息:
- 基本:意味着最小可用、不追求复杂,是一个能独立跑通的demo;
- 演示:说明它侧重在“看出来”和“复现”,要把买卖信号、收益曲线都展示出来;
- MACD:指技术指标“指数平滑异同移动平均线”,是这段代码的核心逻辑。
很多人拿到这类脚本,第一反应是“跑一下,看到图,结束”。但真正有价值的是把它拆成五个模块:数据读取、指标计算、信号生成、回测执行、结果展示。标题里“演示”二字,恰恰告诉我们这套代码设计的初衷是让人看清一个策略从数据到决策、再到结果的全过程,而不是写一个黑盒。
所以我整理代码时,刻意保持了模块化:每个函数只干一件事,主脚本负责串联。这样做的最大好处是,后续想换数据源、换指标参数、换交易规则,只需要动一个模块,其他部分几乎不用改。别小看这个习惯,很多回测脚本一开始写着写着就成了几百行的“大泥球”,问题出在哪都找不到,模块化是少走弯路的第一道保障。
1.2 为什么用MATLAB做MACD策略演示
我见过不少用Python做MACD策略的例子,为什么这个项目选MATLAB?主要有三个原因。
第一,MATLAB的矩阵运算和内置函数让指标计算特别顺手。EMA这类递推公式,用循环写虽然直观,但本质上就是在做数组的逐点更新,MATLAB对这类数值计算非常友好,而且plot系列绘图函数能很快画出价格、MACD指标和买卖点,这对“演示”的定位来说非常契合。
第二,很多高校的金融工程、数量经济学课程,默认工具就是MATLAB。学生手边已经装了MATLAB,有基础的数据处理能力,那么用一个MATLAB脚本来演示MACD策略,比让零基础的人先搭建Python环境要顺得多。我自己见过不少论文里的策略复现部分,代码本身不复杂,但用了MATLAB之后,从公式到代码的迁移成本很低,指标计算和公式符号几乎一一对应,这是很大的优势。
第三,策略本身不复杂。MACD只涉及三根线:DIF、DEA和柱状图,计算量极小,用MATLAB足够跑,完全不需要分布式计算或高频框架。它的短板也是明显的:数据获取、并发回测、社区生态都不如Python,但只要你把定位放在“策略研究”和“教学演示”上,MATLAB的优势还是能发挥出来。如果哪天真要上生产环境、跑几百上千个标的的组合回测,再迁移到Python也不迟,因为策略逻辑已经在MATLAB里验证过了。
1.3 MACD策略的三种常见信号
MACD策略本质上不是预测价格,而是用快慢两条EMA的收敛/发散来描述趋势状态。最常见的信号有三种:
- 金叉死叉:DIF上穿DEA是金叉,做多;DIF下穿DEA是死叉,做空或平仓。这是最常见、最容易代码化的信号。
- 零轴过滤:DIF在零轴上方运行时,说明快线在慢线之上、市场处于强势区,这个位置的金叉更可信;零轴下方则偏空头市场,可以过滤掉一部分假信号。
- 背离:价格创新低但DIF没有创新低,叫底背离,通常视为做多信号;反之叫顶背离。
这里要强调,本演示脚本实现的是第一种“金叉死叉”信号,原因很简单:它是所有MACD用法里最容易二值化、最方便回测的。背离虽然更有“技术含量”,但在程序里需要辅助判断局部极值,代码复杂度会高一个量级,放在演示项目里会喧宾夺主。先把金叉死叉跑通,再往背离、零轴过滤这些方向扩展,这个学习路径最顺。
2. 数据、指标与回测规则的准备工作
2.1 数据格式与预处理
任何策略代码的第一步都是数据。我习惯把行情数据统一整理成CSV文件,至少包含6列:date、open、high、low、close、volume。列名最好不要用中文,避免readtable读取时出现编码问题。
一个典型的数据文件长这样:
date,open,high,low,close,volume 2022-01-04,18.20,18.55,17.90,18.30,1023400 2022-01-05,18.28,18.42,17.75,17.96,984500 ...MATLAB读取用readtable就行:
data = readtable('your_data.csv'); date = data.date; open = data.open; high = data.high; low = data.low; close = data.close; volume = data.volume;这里有两个坑需要提醒。
第一个坑:日期格式。如果你把日期当作字符串读进来,后面画图的x轴以及时间统计都会很麻烦。建议在readtable之后,用datetime函数做一次转换:
date = datetime(data.date, 'InputFormat', 'yyyy-MM-dd');第二个坑:缺值和停牌。行情数据里偶尔会出现NaN,比如某天停牌、成交量为0。如果不处理,EMA计算会把NaN一路传播下去,导致后面所有指标全部变成NaN。我通常的做法是直接剔除全为NaN的行,或者用fillmissing做插值。演示项目里最简单可靠的是保留有效数据、剔除缺失行。
注意:不要让NaN进入指标计算,否则EMA会把缺失值一路传播下去,后面画出来的MACD图全是断崖,信号也会乱掉。
2.2 MACD指标的手算原理与MATLAB实现
MACD的完整计算分三步,理解了这三步,代码就是顺水推舟。
第一步:计算快线EMA。以最常见的12日EMA为例,公式是:
EMA(t) = alpha * close(t) + (1 - alpha) * EMA(t-1)
其中alpha = 2 / (12 + 1) = 0.1538。这个alpha的设计思路,是让最新数据占有固定权重,同时对距离当前越远的数据衰减更多。EMA本质上是一种带遗忘因子的移动平均,相比简单移动平均SMA,它对新信息的反应更快。
第二步:计算慢线EMA,常见的是26日EMA。快线减慢线,得到DIF:
DIF = EMA(close, 12) - EMA(close, 26)
DIF的含义就是短期趋势与长期趋势的差。DIF为正,说明短期均线在长期均线之上,市场处于多头氛围;DIF为负则是空头氛围。
第三步:对DIF再做一次EMA,得到DEA。常见参数是9,也就是:
DEA = EMA(DIF, 9)
最后,MACD柱状图通常是2倍DIF和DEA之差:
MACD_Bar = 2 * (DIF - DEA)
为什么乘2?这是国内行情软件的通用约定,只是为了让柱子在图上更明显,不影响信号本质。
有了公式,MATLAB实现很直接。我习惯先写一个独立的EMA函数:
function out = ema(data, period) % EMA递推计算 alpha = 2 / (period + 1); out = zeros(length(data), 1); out(1) = data(1); for i = 2:length(data) out(i) = alpha * data(i) + (1 - alpha) * out(i - 1); end end再写一个MACD计算函数:
function [dif, dea, macdBar] = calcMACD(close, shortPeriod, longPeriod, signalPeriod) emaShort = ema(close, shortPeriod); emaLong = ema(close, longPeriod); dif = emaShort - emaLong; dea = ema(dif, signalPeriod); macdBar = 2 * (dif - dea); end这里有一个和行情软件对不上的常见问题:EMA的第一个值怎么定。我这个函数是把EMA(1)直接设成data(1),也就是第一天的收盘价。大部分行情软件也是这么做的,但如果你用的数据在计算起点之前还有更长的行情,显示结果会略有偏差。演示项目里影响不大,但如果你要和Wind、通达信这类软件严格对齐,通常会把第一个有效EMA定在第longPeriod根K线附近,或者干脆跳过前N根数据再计算。原因在于EMA是递推公式,初值的选择会被后续所有计算继承,越靠前的数据偏差越明显,但过了足够长的迭代区间后,这个初值影响会逐渐衰减到可以忽略。
2.3 交易规则:金叉死叉怎么落成代码
指标算完之后,要把它转成交易动作。这步是整个演示的灵魂,也是代码里最容易出错的地方。
一个完整的交易系统至少需要三个规则:
- 开仓规则:DIF从下方向上穿越DEA(金叉),买入;
- 平仓规则:DIF从上方向下穿越DEA(死叉),卖出;
- 资金规则:全仓进出,手续费按万三计算。
在代码里判断金叉死叉,最常用的方式是看前一根K线的DIF和DEA关系,以及当前这根K线的DIF和DEA关系。用前一根判断的目的是避免未来函数——技术指标用的都是截至当前K线收盘的数据,“当前这根K线收盘时确认信号”是合理逻辑,但要在“当前这根K线最高价”判断信号就会偷看未来数据。
核心判断逻辑:
if position == 0 && dif(i-1) < dea(i-1) && dif(i) >= dea(i) % DIF上穿DEA,金叉,买入 elseif position == 1 && dif(i-1) > dea(i-1) && dif(i) <= dea(i) % DIF下穿DEA,死叉,卖出 end这里我特意用了position这个状态变量,而不是直接用信号数组。因为交易系统的持仓状态是连续的:已经持仓时,重复出现金叉不该重复买入;空仓时,重复出现死叉也不该重复卖出。用状态机维护持仓状态,才能保证回测逻辑和真实交易一致。
为什么演示版用全仓进出?因为这样资金曲线和买卖信号是一一对应的,看到收益曲线往下掉,马上就能想到是哪一次信号出了问题,方便排查问题。真到了实盘阶段,全仓肯定太过激进,需要引入仓位管理,但那是后话,演示阶段先把逻辑跑对是第一位的。
3. 实操过程:从零写出一套可运行的MACD回测
3.1 完整代码结构与参数区
我把整套演示代码分成四部分:参数区、函数区、主回测循环、统计绘图。参数区放在脚本最上方,方便随时调整:
clear; clc; close all; %% 参数区 fileName = 'demo_stock.csv'; % 行情数据文件 shortPeriod = 12; % 快线EMA周期 longPeriod = 26; % 慢线EMA周期 signalPeriod = 9; % DEA周期 initCash = 100000; % 初始资金 feeRate = 0.0003; % 单边手续费率这样的结构看起来简单,但重要性不亚于策略本身。我把参数集中管理后,调参数时只改一个地方,不会出现某个数字散落在循环里的尴尬局面。真正跑回测时,你一定会反复尝试不同的参数组合,如果参数散落在代码各处,改一个漏一个,结果自然对不上。我见过不少同学因为这类小问题折腾了一个晚上,最后发现是有一处参数没改过来。
3.2 指标计算函数
将2.2中的ema和calcMACD保存成独立m文件。注意,MATLAB函数名必须和文件名一致,否则会报错。
% 文件名: ema.m function out = ema(data, period) alpha = 2 / (period + 1); out = zeros(length(data), 1); out(1) = data(1); for i = 2:length(data) out(i) = alpha * data(i) + (1 - alpha) * out(i - 1); end end% 文件名: calcMACD.m function [dif, dea, macdBar] = calcMACD(close, shortPeriod, longPeriod, signalPeriod) emaShort = ema(close, shortPeriod); emaLong = ema(close, longPeriod); dif = emaShort - emaLong; dea = ema(dif, signalPeriod); macdBar = 2 * (dif - dea); end严格来说,纯循环的EMA在数据量很大的时候效率一般,但因为每次递推都必须依赖上一次结果,所以MATLAB里很难完全向量化。这里有个优化思路:你可以把整个EMA写成一个filter调用,或者用MATLAB的tsmovavg函数(需要Econometrics Toolbox),但循环写法最直观,也最容易让别人看懂,演示项目优先保证可读性。如果数据量真的到了几十万根K线,再考虑用filter或MEX加速也不迟。
3.3 信号生成与逐日回测主循环
主脚本里,读取数据、计算指标之后,就到了最核心的逐日循环:
%% 读取数据 data = readtable(fileName); date = datetime(data.date, 'InputFormat', 'yyyy-MM-dd'); close = data.close; %% 计算指标 [dif, dea, macdBar] = calcMACD(close, shortPeriod, longPeriod, signalPeriod); %% 生成信号 n = length(close); signal = zeros(n, 1); % 1=买入,-1=卖出,0=无操作 position = 0; % 当前是否持仓,0=空仓,1=持仓 for i = 2:n if isnan(dif(i-1)) || isnan(dea(i-1)) continue; end if position == 0 && dif(i-1) < dea(i-1) && dif(i) >= dea(i) signal(i) = 1; position = 1; elseif position == 1 && dif(i-1) > dea(i-1) && dif(i) <= dea(i) signal(i) = -1; position = 0; end end有几点要说明。
第一,为什么从i=2开始判断?因为信号判断要同时用到第i-1和第i根K线,第1根K线没有前一根可对比。实际运行MACD时,前面还有一部分指标值因EMA初值影响而不稳定,通常建议从longPeriod之后才开始产生信号。
第二,这里用第i根K线收盘价生成信号,又在同一天以第i根K线收盘价成交,严格来说属于“用收盘信号按当日收盘成交”,在日线级别对回测结果会有轻微乐观偏差。更严谨的做法是第i天收盘产生信号,第i+1天才真正下单。演示版可以接受,但实盘一定要注意。
第三,isnan判断非常重要。如果数据前面存在NaN,或者EMA初值导致某些点算出来是NaN,循环可能在还没进入稳定状态时就误判出信号。跳过非数值区是最稳妥的做法。
接下来是回测主循环。我用最朴素的逐日记账方式:
%% 回测主循环 cash = initCash; % 可用资金 holdShares = 0; % 持仓股数 equity = zeros(n, 1); % 每日总资产 for i = 1:n if signal(i) == 1 % 买入,全仓 if holdShares == 0 availShares = floor(cash / (close(i) * (1 + feeRate))); if availShares > 0 cost = availShares * close(i) * (1 + feeRate); cash = cash - cost; holdShares = availShares; end end elseif signal(i) == -1 % 卖出,清仓 if holdShares > 0 income = holdShares * close(i) * (1 - feeRate); cash = cash + income; holdShares = 0; end end equity(i) = cash + holdShares * close(i); end这里用了一个细节:买入时用floor取整股数,因为在A股交易单位是1手=100股,但为了演示简单,我直接按整数股买入。如果你用美股数据,可能需要考虑小数股;如果严格按照A股规则,应该改成floor(cash / (close * 100)) * 100。这个细节值得新手注意,不然回测和实盘对不上,真实下单时系统告诉你“买入数量不合法”,你才会发现模拟和现实差了整整一个数量单位。
买入成本、卖出收入都乘了手续费率。这里的手续费是单边万三,实际交易中还有印花税、滑点,后面可以逐步加上。别觉得万三不起眼,MACD这种中高频率的趋势策略一年可能来回几十次,手续费会实实在在吃掉一截利润。
3.4 回测统计与可视化输出
回测跑完之后,如果不把结果展示出来,“演示”的意义就少了一半。我写个统计函数和绘图代码。
统计指标至少包含:总收益率、年化收益率、最大回撤、交易次数、胜率。
%% 回测统计 totalReturn = equity(end) / initCash - 1; nYear = length(close) / 245; % 日线数据按每年245个交易日估算 annualReturn = (1 + totalReturn)^(1 / nYear) - 1; runningPeak = cummax(equity); drawdown = equity ./ runningPeak - 1; maxDrawdown = min(drawdown); % 统计每笔交易 tradeReturns = []; isHold = false; for i = 1:n if signal(i) == 1 && ~isHold buyPrice = close(i); isHold = true; elseif signal(i) == -1 && isHold sellPrice = close(i); tradeReturns = [tradeReturns; (sellPrice - buyPrice) / buyPrice]; isHold = false; end end winRate = sum(tradeReturns > 0) / length(tradeReturns);这里年化收益率用了一个简化估算:一年245个
本文还有配套的精品资源,点击获取