均線、ATR、RSI 這些技術指標,MT5 不是直接給你一個數字:要先跟它「登記」一個指標拿到 handle,再用 CopyBuffer() 把數值抓進陣列。這篇用兩條 EMA 加一個 ATR 走完這條路,順便做出第一個交易訊號:均線交叉。EA 只印訊息、不下單。
範例是 EA 不是腳本,因為訊號要一根一根 K 線判斷,得靠 OnTick。整份程式在我的 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, 緩衝區編號, 起點, 幾根, 陣列)。均線跟 ATR 只有一個緩衝區,編號 0;起點 0 是目前這根,抓 3 根就是 [0]、[1]、[2]。回傳值是實際抓到幾根,剛掛上圖表、歷史還沒載完的時候會拿不齊,一定要檢查。拿不齊就把 g_LastBarTime 清成 0,下一個 tick 再試同一根,不然這根就被跳過了。
三個陣列都先 ArraySetAsSeries(..., true),[0] 才是最新那根、[1] 上一根、[2] 再前一根,跟 CopyClose 一樣。忘了設,順序就反過來,交叉判斷整個顛倒,而且不會報錯。
訊號只用 [1] 和 [2],不用 [0]。[0] 這根還在跑,均線值每個 tick 都在變,這一秒交叉、下一秒又沒了。用已收盤的 [1] 代價是慢一根,但訊號出來就不會消失,之後回測要準也靠這一條。
黃金交叉的寫法是「前一根快線還在慢線下面(或相等)、這根收盤翻到上面」,兩個條件都要,只有翻過去的那一根會印,不會每根都印。死亡交叉反過來。日誌裡 01.16 05:00 那筆快線慢線都印 1.02929 卻算死亡交叉,因為 DoubleToString() 只顯示到第 5 位,實際差在第 6 位以後。均線值是 double,這種一線之隔很常見。
ATR(Average True Range)是最近 14 根的平均波動幅度,除以 _Point 換成點數比較好讀,日誌裡這幾筆落在 80 到 220 點之間。現在只是印出來,之後算停損距離會用它:停損跟著波動走,比固定點數合理。
執行結果
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 黃金交叉五小時後又死亡交叉。盤整時兩條均線黏在一起反覆穿越,所以交叉很少單獨拿來交易,通常會再加濾網。這篇只做訊號,先不管好不好賺。
換別的指標
換指標只是換函數跟緩衝區編號,讀法一樣。有好幾條線的指標,每條線是一個緩衝區:
| 函數 | 緩衝區編號 |
|---|---|
iMA、iATR、iRSI、iCCI | 0 |
iBands(布林通道) | 0 中軌、1 上軌、2 下軌 |
iMACD | 0 主線、1 訊號線 |
iStochastic | 0 主線 %K、1 訊號線 %D |
iADX | 0 ADX、1 +DI、2 -DI |
記不住就在 MetaEditor 把游標放在函數名上按 F1,說明裡有列。
用 handle 建的指標不會畫在圖表上(測試器的視覺化模式除外)。想核對數值,就自己從導航拖一個同參數的 Moving Average 到圖上,開數據窗口(Ctrl+D)比對。
本篇檔案:MaCrossSignal.mq5,放到 MQL5\Experts\ 底下,在 MetaEditor 打開按 F7 就能用。