上一篇把環境裝好、HelloWorld 也跑起來了。這篇把寫 EA 會用到的語法一次過一遍:變數、input 參數、判斷、迴圈、陣列、函數。以前 MT4 系列是拆成五篇慢慢講,這次合成一篇,範例也合成一支程式,看完直接有一份可以翻的小抄。
這篇的範例是腳本(Script)不是 EA。腳本拖到圖表上跑一次、印完就結束,拿來看語法最乾淨,不用等報價。腳本放 MQL5\Scripts\ 資料夾,進入點只有一個 OnStart(),EA 那三個函數(OnInit/OnTick/OnDeinit)它都沒有。語法兩邊完全一樣,之後搬到 EA 裡直接用。
整份程式已經在我的 MT5 上編譯過、跑過,文中的輸出都是實際的日誌,不是手打的。可以先下載 SyntaxBasics.mq5 跟著跑,再回來看說明。
先把範例跑起來
- 把 SyntaxBasics.mq5 放到
MQL5\Scripts\(上一篇說過,Ctrl+Shift+D打開資料資料夾就找得到),或是在 MetaEditor 用精靈選「腳本」建一個新檔,內容整份換掉。 - MetaEditor 按
F7編譯,看到 0 errors。 - 回到 MT5,開一張 EURUSD H1 圖表。導航(
Ctrl+N)展開「腳本」,把 SyntaxBasics 拖到圖表上。 - 會先跳出屬性視窗(上一篇掛 EA 時看過的那個),這次看「輸入參數」頁,裡面有六個參數,先都不動,按確定。
- 腳本跑完就自動從圖表消失了,輸出在工具箱(
Ctrl+T)的「專家」頁。
六個參數會出現在屬性視窗裡,是因為檔頭這一行:
//+------------------------------------------------------------------+
//| SyntaxBasics.mq5 |
//| 冏掉的Alan羅 orzalanluo.com |
//+------------------------------------------------------------------+
#property copyright "orzalanluo.com"
#property link "https://orzalanluo.com"
#property version "1.00"
#property description "MT5 教學 02:MQL5 語法基礎(腳本)"
#property script_show_inputs // 執行前先跳出參數視窗#property script_show_inputs 是腳本專用的。EA 掛上去一定會跳屬性視窗,腳本預設不會,要自己開。
1. 變數與型態
void OnStart()
{
//=== 1. 變數與型態 ===============================================
int bars = 10; // 整數
double price = 1.08765; // 浮點數
bool isOpen = true; // 布林值:true / false
string name = "EURUSD 測試"; // 字串,可以放中文
datetime now = TimeCurrent(); // 時間(1970 起算的秒數)
long big = 1234567890123; // 大整數
Print("bars=", bars, " price=", price, " isOpen=", isOpen);
Print("name=", name, " now=", TimeToString(now));
Print("big=", big);
int cut = (int)3.7; // 明確轉型:小數直接丟掉 → 3
double sum = bars + 0.5; // int 自動升成 double → 10.5
Print("cut=", cut, " sum=", sum);宣告的格式是 型態 名稱 = 初始值;,每一句結尾都要分號。名稱只能用英文、數字、底線,不能數字開頭,而且大小寫有分:bars 跟 Bars 是兩個不同的東西。// 後面到行尾是註解,/* */ 可以包好幾行。
常用的型態就這幾個:
| 型態 | 例子 | 什麼時候用 |
|---|---|---|
int | 300 | 整數,正負 21 億以內。點數、K 線數、計數器 |
long | 1234567890123 | 大整數。帳號、訂單編號(ticket)會超過 int |
double | 1.08765 | 有小數的數。價格、手數、指標值 |
bool | true / false | 是或否。開關類的參數 |
string | “EURUSD 測試” | 字串。MT5 的字串是 Unicode,中文直接放 |
datetime | TimeCurrent() | 時間。本質是 1970 年起算的秒數,可以直接加減 |
color | clrRed | 顏色。畫線、畫物件用 |
enum | ENUM_TIMEFRAMES | 列舉,一組有名字的整數常數。下面 input 那段會自己定一個 |
還有 uint、ulong、char、float 這些,初學用不到,知道有就好。
型態轉換要注意一件事:小數放進 int 會被直接截掉,3.7 變 3,不是四捨五入。範例裡的 (int)3.7 是明確告訴編譯器「我知道我在做什麼」,如果直接寫 int x = 3.7; 編譯會過,但會給一個警告,後面「故意寫錯」那段有實際訊息。反過來 int 放進 double 沒問題,bars + 0.5 會自動變成 10.5。
作用域跟 MT4 那時候講的一樣:寫在所有函數外面的是全域變數(範例裡的 g_Counter),整份程式都看得到、腳本跑完之前值都在;寫在函數裡面的是區域變數,離開那個大括號就沒了。全域變數我一律加 g_ 前綴,input 參數加 i_,看到名字就知道它是誰、能不能改,這個習慣會一直用到後面的 EA。
2. input 參數
下面這段在檔案裡寫在最上面,因為 input(還有它用到的 enum)一定要宣告在所有函數外面:
//=== 2. input 參數 ==================================================
enum ENUM_DIRECTION // 自訂列舉,做成下拉選單
{
DIR_BOTH = 0, // 多空都做
DIR_BUY = 1, // 只做多
DIR_SELL = 2 // 只做空
};
input int i_StopPoints = 300; // 停損(點)
input double i_RiskPercent = 1.0; // 每筆風險 %
input bool i_UseTrailing = true; // 啟用移動停損
input ENUM_DIRECTION i_Direction = DIR_BOTH; // 交易方向
input string i_Comment = "教學"; // 訂單註解
input bool i_ShowArrayError = false; // 故意讓陣列越界
int g_Counter = 0; // 全域變數:整份程式都看得到OnStart 裡只是把它們印出來:
//=== 2b. input 參數(宣告在最上面,這裡只是印出來)==============
Print("i_StopPoints=", i_StopPoints,
" i_RiskPercent=", i_RiskPercent,
" i_UseTrailing=", i_UseTrailing);
Print("i_Direction=", EnumToString(i_Direction),
" i_Comment=", i_Comment);
// i_StopPoints = 500; // 編譯錯誤:input 參數不能在程式裡改變數前面加 input,載入程式的時候就會出現在屬性視窗的「輸入參數」頁讓使用者改,不用改程式重新編譯。MT4 早期是寫 extern,MT5 統一用 input。
後面的註解會直接變成屬性視窗裡顯示的名稱,所以參數的註解要寫成使用者看得懂的中文,例如「停損(點)」,而不是寫給自己看的備忘。
自訂列舉 enum 拿來當 input,屬性視窗裡就是一個下拉選單,選項的文字一樣來自註解。比起讓使用者填 0、1、2 再自己記哪個是什麼,好用很多。列舉本質上是整數,所以可以 == 比較、也能丟進 switch。
input 參數在程式裡是常數,不能改值。範例裡那行註解掉的 i_StopPoints = 500; 拿掉註解去編譯,會直接報錯。真的需要在程式裡調整,就先複製到一個普通變數再改。
順帶一提,sinput 是 input 的變體,一樣會出現在視窗,但策略測試器優化參數的時候會跳過它。像 Magic number、訂單註解這種不需要優化的參數用 sinput,之後講回測會再遇到。
3. 預定義變數
//=== 3. 預定義變數 ===============================================
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
Print("_Symbol=", _Symbol, " _Period=", EnumToString(_Period),
" _Digits=", _Digits, " _Point=", _Point);
Print("Ask=", DoubleToString(ask, _Digits),
" Bid=", DoubleToString(bid, _Digits),
" 點差=", (int)MathRound((ask - bid) / _Point), " 點");
Print("停損 ", i_StopPoints, " 點 = 價格差 ",
DoubleToString(PointsToPrice(i_StopPoints), _Digits));底線開頭的這幾個是 MT5 幫你準備好的,不用宣告直接用:_Symbol 圖表商品、_Period 圖表週期、_Digits 報價小數位數、_Point 報價的最小單位,也就是一「點」(EURUSD 五位報價就是 0.00001)。
跟 MT4 差比較多的一個是 Bid、Ask。MT4 直接有 Bid、Ask 兩個變數,MT5 沒有,要用 SymbolInfoDouble(_Symbol, SYMBOL_ASK) 去問。寫起來長一點,但好處是可以問任何商品,不限目前圖表。
點數跟價格的換算是下單一定會碰到的:停損 300 點,價格差就是 300 * _Point = 0.00300。範例把它包成 PointsToPrice() 函數,後面第 7 段講。這裡的「點」是 MT5 的 point,五位報價的 1 點是 0.1 pip,跟一般講的 pip 差十倍,寫程式的時候要分清楚。
我這次是週末跑的,而且 MT5 還沒連上伺服器腳本就跑完了,所以日誌裡的 Ask、Bid 跟 TimeCurrent() 都是上次收到報價時留下的快取值:Ask 等於 Bid、點差 0 點、時間停在 9/10 下午。平日連線中跑會看到 1、2 點的點差。TimeCurrent() 是伺服器最後一筆報價的時間,不是你電腦的時間,電腦時間是 TimeLocal(),休市的時候兩個會差很多。
4. 判斷
//=== 4. 判斷 =====================================================
int a = 3, b = 5;
if(a > b)
Print("a 大於 b");
else if(a < b)
Print("a 小於 b");
else
Print("a 等於 b");
if(i_UseTrailing && i_StopPoints > 0) // && 兩個都要成立
Print("移動停損:開,停損 ", i_StopPoints, " 點");
if(i_Direction == DIR_BUY || i_Direction == DIR_BOTH) // || 其一
Print("允許做多");
if(i_Direction == DIR_SELL || i_Direction == DIR_BOTH)
Print("允許做空");
// double 不能直接用 == 比較
double x = 0.1 + 0.2;
Print("0.1+0.2 == 0.3 ? ", (x == 0.3));
Print("差距小於半點就當相等 ? ", (MathAbs(x - 0.3) < _Point / 2));
switch(i_Direction) // 同一個變數對多個值:用 switch
{
case DIR_BUY: Print("switch:只做多"); break;
case DIR_SELL: Print("switch:只做空"); break;
default: Print("switch:多空都做"); break;
}程式交易說穿了就是一連串判斷:達到條件就買、虧到某個點數就停損。if 後面括號裡的條件成立就執行下面那一句;要執行好幾句就用大括號包起來。else if 會依序試,前面有一個成立後面就不看了,else 是全部不成立才走。
比較運算子:>、<、>=、<=、==(等於,兩個等號)、!=(不等於)。邏輯運算子:&&(而且,兩邊都成立)、||(或者,其中一邊成立)、!(相反)。
等於判斷少打一個等號是 MT4 那篇就講過的老坑:if(y = 3) 會變成把 3 塞給 y,然後判斷 y 是不是非零,永遠成立。編譯會過,只給一個 warning,程式看起來正常跑但邏輯全錯。習慣把常數寫在左邊 if(3 == y) 也是一種防呆,少打一個等號會直接編譯錯誤。
MT5 這邊要多記一個:double 不能直接用 == 比。日誌裡 0.1+0.2 == 0.3 印出 false,因為浮點數在電腦裡不是精確值,0.1+0.2 其實是 0.30000000000000004。比價格的時候改成「差距小於半個點就當相等」:MathAbs(a - b) < _Point / 2。這條編譯器不會警告你,只能自己記得。
同一個變數要對好幾個值做不同事,用 switch 比一串 else if 好讀。每個 case 結尾要 break,不然會一路往下執行到下一個 case。
5. 迴圈
//=== 5. 迴圈 =====================================================
string s = "";
for(int i = 0; i < 10; i++)
{
if(i == 3) continue; // 跳過這一輪,直接下一個 i
if(i == 7) break; // 整個迴圈結束
s += IntegerToString(i) + " ";
}
Print("for:", s);
int n = 0;
while(n < 3) // 條件成立就一直跑
{
g_Counter += 10;
n++; // 忘了這行就是無窮迴圈
}
Print("while 跑了 ", n, " 次,g_Counter=", g_Counter);for 括號裡三段:起始、條件、每輪結束後做的事。int i = 0 宣告在括號裡的 i 只在迴圈裡有效,出了迴圈就不能用。範例印出 0 1 2 4 5 6:continue 是跳過這一輪剩下的程式碼直接進下一輪,所以 3 沒印;break 是整個迴圈結束,所以 7 以後都沒了。
while 只有一個條件,成立就一直跑。要小心那個 n++,忘了寫條件永遠成立,就是無窮迴圈,CPU 飆高、程式停不下來。腳本卡住可以在圖表上按右鍵把它移除,EA 卡住就比較麻煩了。
兩種迴圈可以互換,看哪個順手。之後掃描持倉、找出自己這隻 EA 的單,都是 for 迴圈。
6. 陣列
//=== 6. 陣列 =====================================================
int fixed[5] = {10, 20, 30, 40, 50}; // 固定大小,索引 0~4
Print("fixed 大小=", ArraySize(fixed),
" 第一個=", fixed[0], " 最後一個=", fixed[4]);
double closes[]; // 動態陣列,先不給大小
ArraySetAsSeries(closes, true); // [0] = 最新那根 K 線
int copied = CopyClose(_Symbol, _Period, 0, bars, closes);
if(copied != bars)
{
Print("CopyClose 只拿到 ", copied, " 根,錯誤碼 ",
GetLastError());
return;
}
for(int i = 0; i < copied; i++)
Print("closes[", i, "]=", DoubleToString(closes[i], _Digits),
" ", TimeToString(iTime(_Symbol, _Period, i)));
double lo, hi;
MinMax(closes, lo, hi);
Print("最近 ", copied, " 根收盤:最低 ",
DoubleToString(lo, _Digits),
" 最高 ", DoubleToString(hi, _Digits));
if(i_ShowArrayError)
{
int idx = ArraySize(fixed); // = 5,最後一格是 [4]
Print("故意讀 fixed[", idx, "],程式在這裡被終止");
Print(fixed[idx]); // array out of range
}
Print("腳本結束");
}陣列就是一排同型態的資料,名稱後面加中括號。int fixed[5] 是固定五格,索引從 0 開始,最後一格是 [4]。ArraySize() 回傳格數。
double closes[] 中括號空著是動態陣列,大小之後才決定,可以自己 ArrayResize(),或像範例這樣交給 CopyClose() 自動撐大。
這段是這篇跟 MT4 差最多的地方。MT4 直接有 Close[1]、High[i] 這些預定義陣列,MT5 沒有,要自己用 CopyClose(商品, 週期, 起點, 幾根, 陣列) 把收盤價抓進陣列;只要一根的話 iClose(_Symbol, _Period, 1) 比較省事。CopyClose 的回傳值是實際抓到幾根,一定要檢查,剛開圖表歷史資料還沒載完的時候會拿不齊。
然後是 ArraySetAsSeries(closes, true)。MT4 的 Close[] 天生是 [0] 最新、[1] 前一根;MT5 的陣列預設是反過來的,[0] 是最舊那根,要自己設成時間序列才會跟 MT4 一樣。我自己在多商品 EA 上就忘過一次,指標算的全是最舊那段資料,回測看起來很正常,找了很久。
日誌裡 closes[0] 是圖表最右邊那根(我跑的時候是週五收盤前最後一根 H1,K 線歷史是硬碟上已經下載好的,跟上面快取的報價不是同一個時間)。開盤中的時候 [0] 那根還沒收完,數值會一直變;真正「上一根收盤價」是 closes[1]。這件事下一篇講指標訊號的時候很重要。
最後一個參數 i_ShowArrayError 打開,程式會去讀 fixed[5]。這種越界編譯器抓不到,是執行的時候才炸,我實際跑的結果:
最近 10 根收盤:最低 1.15916 最高 1.16139
故意讀 fixed[5],程式在這裡被終止
array out of range in 'SyntaxBasics.mq5' (152,19)後面那行「腳本結束」沒有印出來,程式在越界那一行就被 MT5 終止了。陣列越界是執行期錯誤,整支程式直接停,EA 掛在圖表上遇到這個會被卸掉,等於停止交易。日誌會給行號跟欄號,照著找就行。
7. 函數
//=== 7. 函數 ========================================================
// 點數換算成價格差,例如 300 點 → 0.00300
double PointsToPrice(int points)
{
return points * _Point;
}
// 一次要傳回兩個值:用 & 傳參考,直接改呼叫端的變數
void MinMax(const double &arr[], double &lo, double &hi)
{
lo = arr[ArrayMinimum(arr)];
hi = arr[ArrayMaximum(arr)];
}一段程式要重複用,就包成函數。格式是 回傳型態 名稱(參數) { ... return 值; },不回傳東西就寫 void。函數要跟 OnStart 平行寫在外面,不能在 OnStart 裡面再定義一個函數。上一篇的 OnInit、OnTick 也是函數,只是名字是 MT5 規定的,時間到了 MT5 會自己呼叫。
MinMax() 示範兩件事。一是參數前面加 & 叫傳參考:函數裡改 lo、hi,呼叫端的變數會跟著變,這是一個函數要傳回好幾個值的標準做法。二是陣列參數一定要用參考 &arr[],MQL5 不允許整個陣列複製一份傳進去;不想讓函數改到它就加 const。
完整執行結果
六個參數都用預設值跑一次,「專家」頁的訊息欄是這樣(時間跟來源兩欄省略):
bars=10 price=1.08765 isOpen=true
name=EURUSD 測試 now=2026.09.10 16:04
big=1234567890123
cut=3 sum=10.5
i_StopPoints=300 i_RiskPercent=1.0 i_UseTrailing=true
i_Direction=DIR_BOTH i_Comment=教學
_Symbol=EURUSD _Period=PERIOD_H1 _Digits=5 _Point=0.00001
Ask=1.16059 Bid=1.16059 點差=0 點
停損 300 點 = 價格差 0.00300
a 小於 b
移動停損:開,停損 300 點
允許做多
允許做空
0.1+0.2 == 0.3 ? false
差距小於半點就當相等 ? true
switch:多空都做
for:0 1 2 4 5 6
while 跑了 3 次,g_Counter=30
fixed 大小=5 第一個=10 最後一個=50
closes[0]=1.15992 2026.09.11 23:00
closes[1]=1.15959 2026.09.11 22:00
closes[2]=1.15959 2026.09.11 21:00
closes[3]=1.15984 2026.09.11 20:00
closes[4]=1.15999 2026.09.11 19:00
closes[5]=1.16034 2026.09.11 18:00
closes[6]=1.16139 2026.09.11 17:00
closes[7]=1.16122 2026.09.11 16:00
closes[8]=1.16030 2026.09.11 15:00
closes[9]=1.15916 2026.09.11 14:00
最近 10 根收盤:最低 1.15916 最高 1.16139
腳本結束故意寫錯,看編譯器怎麼說
初學最花時間的不是寫,是看懂錯誤訊息。我另外準備了一支SyntaxMistakes.mq5,把這篇講到的坑一次犯完:
//+------------------------------------------------------------------+
//| SyntaxMistakes.mq5 |
//| 教學 02 附錄:故意寫錯,看 MetaEditor 會給什麼訊息 |
//+------------------------------------------------------------------+
#property script_show_inputs
input int i_StopPoints = 300; // 停損(點)
void OnStart()
{
int x = 3.7; // 小數放進 int
int y = 5;
if(y = 3) // 少打一個 =,變成賦值
Print("y=", y);
double d = 1.5;
if(d == 1.5) Print("d"); // double 用 == 比
i_StopPoints = 500; // 改 input 參數
int arr[3];
arr[3] = 1; // 越界(常數索引)
Print("x=", x);
}編譯結果 1 個 error、2 個 warning:
| 行 | 訊息 | 意思 |
|---|---|---|
| 10 | warning 44: truncation of constant value from ‘double(3.7)’ to ‘int(3)’ | 3.7 放進 int,小數被丟掉。編譯還是會過,但值已經不是你以為的 |
| 12 | warning 39: expression not boolean | if 裡面少打一個 =,變成賦值。編譯會過、邏輯全錯,最難抓的一種 |
| 16 | error 189: ‘i_StopPoints’ – constant cannot be modified | input 參數是常數,不能在程式裡改 |
注意沒被抓到的兩個:d == 1.5 那行完全沒警告,arr[3] = 1 越界也沒警告(要跑的時候才炸,上面第 6 段那樣)。編譯過只代表語法對,不代表邏輯對,warning 也一個都不要放著。
MT4 寫法對照
寫過 MT4 系列的人,差異主要就這張表,其他九成一樣:
| MT4 系列的寫法 | MT5 |
|---|---|
extern int X | input int X |
#property show_inputs | #property script_show_inputs |
start() | OnStart()(腳本)/OnTick()(EA) |
Bid、Ask | SymbolInfoDouble(_Symbol, SYMBOL_BID)、SYMBOL_ASK |
Point、Digits | _Point、_Digits |
Close[1]、High[i]、Time[i] | iClose()、iHigh()、iTime(),或 CopyClose() 一次抓一段 |
Close[] 這些預定義陣列 [0] 天生是最新那根 | 要自己 ArraySetAsSeries(arr, true) |
Bars | Bars(_Symbol, _Period) |
iMA(...) 直接回傳數值 | iMA(...) 回傳 handle,再 CopyBuffer()(下一篇) |
OrderSend() 十幾個參數 | CTrade 類別(第四篇) |
練習
- 把 i_Direction 改成「只做多」跑一次,看日誌哪幾行變了;再想想為什麼 switch 那行跟上面 if 那兩行是同一件事。
- 在第 6 段後面加一個 for 迴圈,把 10 根收盤價加總除以 10,印出平均。這就是最陽春的移動平均,下一篇會用 iMA 取代它。
- 把
ArraySetAsSeries(closes, true);那行註解掉再跑,對照上面預設跑的結果,看 closes[0] 的數值變成哪一根的收盤價。注意時間欄是用 iTime 另外算的,不會跟著變,所以價格跟時間會對不上,這正是忘了設時間序列的症狀。 - 把 i_ShowArrayError 打開跑一次,找到日誌裡的行號欄號,對回程式碼。
下一篇
語法會了,下一篇開始接技術指標:MT5 的指標是先拿一個 handle、再用 CopyBuffer 把數值抓進陣列,跟 MT4 直接 iMA() 拿到值不一樣,這也是 MT4 使用者搬過來最不習慣的地方。會用均線做第一個有判斷的訊號。
本篇兩個檔案:SyntaxBasics.mq5、SyntaxMistakes.mq5,放到 MQL5\Scripts\ 底下,在 MetaEditor 打開按 F7 就能用。