均線、ATR、RSI 這些技術指標,MT5 不是直接給你一個數字:要先跟它「登記」一個指標拿到 handle,再用 CopyBuffer() 把數值抓進陣列。MT5 附的標準程式庫另外有 CiMA、CiATR 這些類別,把登記和抓值包成物件。這篇用兩條 EMA(比起每根等權的 SMA,近期價格權重較大、反應較快)加一個 ATR 把兩種寫法都走一遍,順便做出第一個交易訊號:均線交叉。EA 只印訊息、不下單。
範例是 EA 不是腳本,因為訊號要一根一根 K 線判斷,得靠 OnTick。兩個檔案:MaCrossSignal.mq5 直接用 handle,MaCrossSignalCi.mq5 用類別,訊號邏輯一樣。兩份都在我的 MT5 上編譯過、用策略測試器跑過 2025 年 1 月的 EURUSD H1,文中日誌是實際輸出,兩個檔案跑出來逐字相同。可以先下載 MaCrossSignal.mq5 跟著跑。
先把範例跑起來
- 把 MaCrossSignal.mq5 放到
MQL5\Experts\,在 MetaEditor 打開、按F7,0 errors。 - 掛到 EURUSD H1 圖表上。左上角會顯示上一根的快線、慢線、ATR;均線交叉的那根 K 線收完,「專家」頁會印一行。盤中要等到交叉可能要幾個小時。
- 不想等就用策略測試器:
Ctrl+R,EA 選 MaCrossSignal,EURUSD,H1,日期 2025.01.02 到 2025.01.31,模式「每次報價」,按「開始」,切到「日誌」頁。這隻 EA 一根 K 線只算一次,換別的模式訊號也一樣,只差印出的時間。
1. 參數與 handle
//=== 1. 參數與 handle ===============================================
input int i_FastPeriod = 10; // 快線期數
input int i_SlowPeriod = 30; // 慢線期數
input ENUM_MA_METHOD i_MaMethod = MODE_EMA; // 均線種類
input int i_AtrPeriod = 14; // ATR 期數
int g_FastHandle = INVALID_HANDLE; // 指標 handle 就是一個整數
int g_SlowHandle = INVALID_HANDLE;
int g_AtrHandle = INVALID_HANDLE;
datetime g_LastBarTime = 0; // 上次處理過的 K 線時間
int g_Signals = 0; // 訊號計數iMA()、iATR() 這類函數回傳的不是數值,是一個整數,叫 handle。可以把它想成掛號單:MT5 收到之後在背景幫你算這個指標,你之後拿號碼來要數值。跟 MT4 差最多的就是這裡,MT4 的 iMA(..., shift) 是直接回傳那一根的均線值。
handle 放在全域變數,初始值 INVALID_HANDLE(其實就是 -1),建立失敗時才分得出來。g_LastBarTime 記上次處理過的 K 線時間,第 4 段會用到。ENUM_MA_METHOD 是 MT5 內建的列舉,直接拿來當 input 就是 SMA、EMA 等四種均線的下拉選單。
2. OnInit:建立指標,一次就好
//=== 2. OnInit:建立指標,一次就好 ==================================
int OnInit()
{
g_FastHandle = iMA(_Symbol, _Period, i_FastPeriod, 0,
i_MaMethod, PRICE_CLOSE);
g_SlowHandle = iMA(_Symbol, _Period, i_SlowPeriod, 0,
i_MaMethod, PRICE_CLOSE);
g_AtrHandle = iATR(_Symbol, _Period, i_AtrPeriod);
if(g_FastHandle == INVALID_HANDLE ||
g_SlowHandle == INVALID_HANDLE ||
g_AtrHandle == INVALID_HANDLE)
{
Print("指標建立失敗,錯誤碼 ", GetLastError());
return INIT_FAILED;
}
Print("指標建立完成:", EnumToString(i_MaMethod), " ",
i_FastPeriod, "/", i_SlowPeriod, ",ATR ", i_AtrPeriod);
return INIT_SUCCEEDED;
}iMA() 六個參數:商品、週期、期數、位移(幾乎都填 0)、均線種類、用哪個價算(PRICE_CLOSE 收盤價)。iATR() 只要商品、週期、期數。建立失敗(通常是商品名稱打錯之類的參數問題)會回傳 INVALID_HANDLE,這裡直接 return INIT_FAILED 讓 EA 不要掛上去。
指標只在 OnInit 建一次。寫在 OnTick 裡,MT5 會認出是同一個指標、回同一個 handle,不會多算一份,但每個 tick 都白跑一次登記,是初學最常見的錯。
3. OnDeinit:釋放指標
//=== 3. OnDeinit:釋放指標 ==========================================
void OnDeinit(const int reason)
{
IndicatorRelease(g_FastHandle);
IndicatorRelease(g_SlowHandle);
IndicatorRelease(g_AtrHandle);
Comment("");
Print("EA 結束,共 ", g_Signals, " 個訊號");
}EA 被拿掉的時候用 IndicatorRelease() 把 handle 還給 MT5。不寫也不會怎樣,EA 卸載時 MT5 會自己收,但有借有還的習慣要養,之後在程式裡換參數重建指標時才不會越積越多。
4. 只在新 K 線算一次
//=== 4. 新 K 線判斷 =================================================
bool IsNewBar()
{
datetime t = iTime(_Symbol, _Period, 0); // 目前這根的開盤時間
if(t == g_LastBarTime)
return false;
g_LastBarTime = t;
return true;
}已收盤的 K 線,指標值不會再變,沒必要每個 tick 都算。iTime(_Symbol, _Period, 0) 是目前這根的開盤時間,跟上次記的一樣就是同一根,直接跳過;不一樣就是新的一根開始了,記下來、往下算。這個函數之後每隻 EA 都會有。
副作用是訊號會在「下一根的第一個 tick」才出現。日誌裡第一筆黃金交叉說的是 01.03(週五)23:00 那根,測試器印出這行的時間卻是 01.06(週一)00:05:週五收完之後要等週一第一筆報價進來,OnTick 才會被叫到。這個時間欄文末的日誌拿掉了,自己跑一次就看得到。
5. OnTick:讀指標值、判斷訊號
//=== 5. OnTick:讀指標值、判斷訊號 ==================================
void OnTick()
{
if(!IsNewBar())
return; // 一根 K 線只算一次
double fast[], slow[], atr[];
ArraySetAsSeries(fast, true); // [0] 進行中、[1] 上一根已收盤
ArraySetAsSeries(slow, true);
ArraySetAsSeries(atr, true);
if(CopyBuffer(g_FastHandle, 0, 0, 3, fast) < 3 ||
CopyBuffer(g_SlowHandle, 0, 0, 3, slow) < 3 ||
CopyBuffer(g_AtrHandle, 0, 0, 3, atr) < 3)
{
Print("指標資料還沒準備好,錯誤碼 ", GetLastError());
g_LastBarTime = 0; // 下一個 tick 再試這根
return;
}
// 訊號只看已收盤的 [1] 與 [2],不看還在跳的 [0]
bool goldenCross = fast[2] <= slow[2] && fast[1] > slow[1];
bool deadCross = fast[2] >= slow[2] && fast[1] < slow[1];
int atrPoints = (int)MathRound(atr[1] / _Point);
Comment("快線 ", DoubleToString(fast[1], _Digits),
" 慢線 ", DoubleToString(slow[1], _Digits),
"\nATR ", atrPoints, " 點");
if(goldenCross || deadCross)
{
g_Signals++;
Print(goldenCross ? "黃金交叉" : "死亡交叉",
" ", TimeToString(iTime(_Symbol, _Period, 1)),
" 快線 ", DoubleToString(fast[1], _Digits),
" 慢線 ", DoubleToString(slow[1], _Digits),
" ATR ", atrPoints, " 點");
}
}CopyBuffer(handle, 緩衝區編號, 起點, 幾根, 陣列)。緩衝區(buffer)是指標算好的一串值,一條線一個緩衝區,均線和 ATR 各只有一條,編號 0;起點 0 是目前這根,抓 3 根就是 [0]、[1]、[2]。回傳值是實際抓到幾根,剛掛上圖表、歷史還沒載完的時候會拿不齊,一定要檢查。拿不齊就把 g_LastBarTime 清成 0,下一個 tick 再試同一根,不然這根就被跳過了。
三個陣列都先 ArraySetAsSeries(..., true),[0] 才是最新那根、[1] 上一根、[2] 再前一根,跟 CopyClose 一樣。忘了設,順序就反過來,交叉判斷整個顛倒,而且不會報錯。
訊號只用 [1] 和 [2],不用 [0]。[0] 這根還在跑,均線值每個 tick 都在變,這一秒交叉、下一秒又沒了。用已收盤的 [1] 代價是慢一根,但訊號出來就不會消失,之後回測要準也靠這一條。
![EURUSD H1 的 K 線,2025.01.02 19:00 到 01.03(週五)23:00:藍色快線 EMA10 一直在橘色慢線 EMA30 下方,最後一根收盤才穿上去,紅圈標出黃金交叉,最右邊灰色虛線框是還在跑的 [0]。圖下列出 [2] 01.03 22:00 快線 1.02959 < 慢線 1.02965、[1] 23:00 快線 1.02981 > 慢線 1.02972,[0] 是 01.06 週一開盤那根。](https://orzalanluo.com/wp-content/uploads/mt5-03-ma-cross-bars.png)
黃金交叉的寫法是「前一根快線還在慢線下面(或相等)、這根收盤翻到上面」,兩個條件都要,只有翻過去的那一根會印,不會每根都印。死亡交叉反過來。日誌裡 01.16 05:00 那筆快線慢線都印 1.02929 卻算死亡交叉,因為 DoubleToString() 只顯示到第 5 位,實際差在第 6 位以後。均線值是 double,這種一線之隔很常見。
ATR(Average True Range)是最近 14 根的平均波動幅度,除以 _Point 換成點數比較好讀,日誌裡這幾筆落在 80 到 220 點之間。現在只是印出來,之後算停損距離會用它:停損跟著波動走,比固定點數合理。
6. 同一件事用 CiMA、CiATR 類別寫
標準程式庫把「建 handle、CopyBuffer」包進類別:均線是 Indicators\Trend.mqh 裡的 CiMA,ATR 是 Indicators\Oscilators.mqh 裡的 CiATR(檔名就只有一個 l,打成 Oscillators 會找不到檔)。MaCrossSignalCi.mq5 只改四個地方:宣告、OnInit、OnDeinit、OnTick 讀值,其餘跟上面一樣。
#include <Indicators\Trend.mqh> // CiMA
#include <Indicators\Oscilators.mqh> // CiATR(檔名就是一個 l)
CiMA g_Fast; // handle 包在物件裡
CiMA g_Slow;
CiATR g_Atr;#include 把標準程式庫裡寫好的檔案引進來,CiMA、CiATR 就寫在這兩個檔裡。指標物件宣告成全域變數,handle 藏在物件裡,不用自己拿 int 存。
//=== 2. OnInit:Create 建立指標 =====================================
int OnInit()
{
if(!g_Fast.Create(_Symbol, _Period, i_FastPeriod, 0,
i_MaMethod, PRICE_CLOSE) ||
!g_Slow.Create(_Symbol, _Period, i_SlowPeriod, 0,
i_MaMethod, PRICE_CLOSE) ||
!g_Atr.Create(_Symbol, _Period, i_AtrPeriod))
{
Print("指標建立失敗,錯誤碼 ", GetLastError());
return INIT_FAILED;
}
Print("指標建立完成:", EnumToString(i_MaMethod), " ",
i_FastPeriod, "/", i_SlowPeriod, ",ATR ", i_AtrPeriod);
return INIT_SUCCEEDED;
}g_Fast.Create(...) 是「物件.函數()」的寫法:g_Fast 是 CiMA 型態的變數,也就是物件,點後面是叫這個物件做的事。Create() 的參數跟 iMA()、iATR() 一模一樣,差別是回傳 bool:裡面就是呼叫 iMA() 把 handle 存起來,建不起來回 false。
OnDeinit 不用寫 IndicatorRelease():EA 卸載時 MT5 會自己把指標收回,跟第 3 節說的一樣。收的是 MT5,不是物件。
g_Fast.Refresh(); // 把最新的指標值抓進物件
g_Slow.Refresh();
g_Atr.Refresh();
double fast1 = g_Fast.Main(1), fast2 = g_Fast.Main(2);
double slow1 = g_Slow.Main(1), slow2 = g_Slow.Main(2);
double atr1 = g_Atr.Main(1); // Main(0) 進行中、Main(1) 上一根
if(fast2 == EMPTY_VALUE || slow2 == EMPTY_VALUE ||
atr1 == EMPTY_VALUE) // 還沒抓到就是 EMPTY_VALUE
{
Print("指標資料還沒準備好,錯誤碼 ", GetLastError());
g_LastBarTime = 0; // 下一個 tick 再試這根
return;
}
// 訊號只看已收盤的 1 與 2,不看還在跳的 0
bool goldenCross = fast2 <= slow2 && fast1 > slow1;
bool deadCross = fast2 >= slow2 && fast1 < slow1;Refresh() 把最新一段指標值抓進物件(底層就是 CopyBuffer),之後 Main(i) 讀第 i 根:0 是進行中那根、1 上一根,順序已經是最新在前,不用 ArraySetAsSeries。還沒抓到的位置回 EMPTY_VALUE,檢查它就等於檢查 CopyBuffer 的回傳筆數。
![兩種寫法的流程對照。上排 iMA() handle:iMA(...) 登記指標拿到 handle(一個號碼),每根新 K 線用 CopyBuffer(g_FastHandle, 0, 0, 3, fast) 把值抓進陣列,先 ArraySetAsSeries 再讀 fast[1]、fast[2];下排 CiMA 類別:g_Fast.Create(...) 登記指標、handle 包在物件裡,每根新 K 線 g_Fast.Refresh() 把值抓進物件,再讀 g_Fast.Main(1)、g_Fast.Main(2),不用 ArraySetAsSeries;中間一框寫著兩者背後是同一個指標、算出同一串值(緩衝區)。](https://orzalanluo.com/wp-content/uploads/mt5-03-handle-vs-class.png)
iMA、iATR | CiMA、CiATR | |
|---|---|---|
| 引入 | 不用 | include Trend.mqh 等 |
| 建立 | iMA(...),失敗回 INVALID_HANDLE | Create(...),失敗回 false |
| 釋放 | 要,第 3 節 | 不用,交給 MT5 |
| 讀值 | CopyBuffer() 抓進陣列 | Refresh() 再 Main(i) |
| 拿不到值 | CopyBuffer() 回傳的筆數不足 | Main(i) 回 EMPTY_VALUE |
| 多線指標 | 緩衝區編號 0、1、2 | Main()、Signal() 這類函數 |
| 一次抓幾根 | 自己指定 | 預設 1024 根,BufferResize() 可改 |
類別版少了 handle、陣列、釋放三件事,讀起來也比較像在講策略,之後的範例都用這種寫法。底下發生的事還是 handle 和 CopyBuffer,看別人的程式碼或查錯誤時要認得。
執行結果
2025 年 1 月,14 個訊號,兩個檔案輸出相同(測試器日誌的時間欄省略,只列訊息):
指標建立完成:MODE_EMA 10/30,ATR 14
黃金交叉 2025.01.03 23:00 快線 1.02981 慢線 1.02972 ATR 144 點
死亡交叉 2025.01.07 20:00 快線 1.03812 慢線 1.03816 ATR 195 點
黃金交叉 2025.01.14 02:00 快線 1.02346 慢線 1.02330 ATR 190 點
死亡交叉 2025.01.16 05:00 快線 1.02929 慢線 1.02929 ATR 142 點
黃金交叉 2025.01.16 19:00 快線 1.02915 慢線 1.02909 ATR 166 點
死亡交叉 2025.01.17 09:00 快線 1.02948 慢線 1.02950 ATR 80 點
黃金交叉 2025.01.17 10:00 快線 1.02952 慢線 1.02951 ATR 84 點
死亡交叉 2025.01.17 18:00 快線 1.02951 慢線 1.02955 ATR 152 點
黃金交叉 2025.01.20 06:00 快線 1.02915 慢線 1.02907 ATR 130 點
死亡交叉 2025.01.23 03:00 快線 1.04123 慢線 1.04125 ATR 106 點
黃金交叉 2025.01.23 19:00 快線 1.04129 慢線 1.04105 ATR 170 點
死亡交叉 2025.01.28 01:00 快線 1.04819 慢線 1.04823 ATR 173 點
黃金交叉 2025.01.30 17:00 快線 1.04220 慢線 1.04206 ATR 176 點
死亡交叉 2025.01.30 22:00 快線 1.04200 慢線 1.04203 ATR 217 點
EA 結束,共 14 個訊號光看這份就知道均線交叉的老問題:01.17 一天翻了三次,01.30 黃金交叉五小時後又死亡交叉。盤整時兩條均線黏在一起反覆穿越,所以交叉很少單獨拿來交易,通常會再加濾網。這篇只做訊號,先不管好不好賺。
換別的指標
換指標只是換函數或類別(類別名就是函數名前面加 C,iMA 對應 CiMA),讀法一樣。有好幾條線的指標,用 handle 要記緩衝區編號,用類別是不同名字的函數:
| 函數 | 緩衝區編號 | 類別的讀法 |
|---|---|---|
iMA、iATR、iRSI、iCCI | 0 | Main() |
iBands(布林通道) | 0 中軌、1 上軌、2 下軌 | Base()、Upper()、Lower() |
iMACD | 0 主線、1 訊號線 | Main()、Signal() |
iStochastic | 0 主線 %K、1 訊號線 %D | Main()、Signal() |
iADX | 0 ADX、1 +DI、2 -DI | Main()、Plus()、Minus() |
記不住就在 MetaEditor 把游標放在函數或類別名上按 F1,說明裡有列。類別全部收在 Indicators\Indicators.mqh,懶得分檔就 include 這一個。
用 handle 建的指標不會畫在圖表上(測試器的視覺化模式除外),類別版也一樣。想核對數值,就自己從導航拖一個同參數的 Moving Average 到圖上,開數據窗口(Ctrl+D)比對。
本篇檔案:MaCrossSignal.mq5、MaCrossSignalCi.mq5,放到 MQL5\Experts\ 底下,在 MetaEditor 打開按 F7 就能用。