從零開始建立你的第一個 MQL5 專家顧問 (EA):完整實戰教學
本文帶你從零完成一個完整的 MQL5 Expert Advisor(EA):從基本骨架、移動平均線交叉策略、到動態風險管理。包含可直接複製的完整程式碼與詳細中文解說,適合 MQL5 初學者到中級開發者。
前置需求:已安裝 MT5 與 MetaEditor,了解基本 MQL5 語法。
從零開始建立你的第一個 MQL5 專家顧問 (EA)
> 「程式交易不是魔法,而是將你的交易邏輯轉化為可執行的程式碼。」 — 交易程式設計師的日常
前言:為什麼要學習 MQL5?
還記得我第一次接觸程式交易的時候,看著那些複雜的 EA(專家顧問)程式碼,心裡想著:「這也太難了吧!」但經過多年的實戰經驗,我發現只要掌握正確的學習路徑,任何人都能學會建立自己的交易機器人。
MQL5 是 MetaTrader 5 平台的專屬程式語言,它專為金融市場的程式化交易而設計。與其他程式語言相比,MQL5 有幾個獨特優勢:
1. 內建交易函數庫:直接呼叫下單、平倉、管理持倉的函數
2. 即時市場數據:輕鬆取得報價、K線數據、技術指標
3. 策略回測功能:內建強大的歷史數據測試工具
4. 圖表整合:直接在交易圖表上顯示自訂指標和信號
今天,我們就從最基礎的「Hello World」開始,一步步建立一個完整的 EA。
第一步:建立 EA 的基本骨架
每個 MQL5 EA 都有固定的結構,就像人體有骨架一樣。讓我們先來看看最基本的 EA 長什麼樣子:
//+------------------------------------------------------------------+
//| MyFirstEA.mq5 |
//| Copyright 2026, James Lee |
//| https://mq5.fincosoft.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, James Lee"
#property link "https://mq5.fincosoft.com"
#property version "1.00"
#property strict
// 輸入參數區 - 這些參數可以在策略測試器中調整
input double LotSize = 0.1; // 交易手數
input int StopLoss = 50; // 止損點數
input int TakeProfit = 100; // 止盈點數
input int MagicNumber = 123456; // EA 識別碼
// 全域變數區
datetime LastBarTime = 0; // 記錄最後一根K線的時間
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
// 當 EA 被載入到圖表時執行一次
Print("=== EA 初始化開始 ===");
Print("EA 名稱: ", MQL5InfoString(MQL5_PROGRAM_NAME));
Print("版本: ", MQL5InfoString(MQL5_PROGRAM_VERSION));
// 檢查交易權限
if(!IsTradeAllowed())
{
Alert("警告: 當前圖表不允許交易!");
return INIT_FAILED;
}
// 取得帳戶資訊
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
Print("帳戶餘額: $", DoubleToString(balance, 2));
Print("帳戶淨值: $", DoubleToString(equity, 2));
Print("=== EA 初始化完成 ===");
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| Expert deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
// 當 EA 從圖表移除時執行
string reasonText;
switch(reason)
{
case REASON_PROGRAM: reasonText = "手動移除"; break;
case REASON_REMOVE: reasonText = "圖表關閉"; break;
case REASON_RECOMPILE: reasonText = "重新編譯"; break;
case REASON_CHARTCHANGE: reasonText = "圖表週期改變"; break;
case REASON_CHARTCLOSE: reasonText = "圖表關閉"; break;
case REASON_PARAMETERS: reasonText = "參數改變"; break;
case REASON_ACCOUNT: reasonText = "帳戶改變"; break;
default: reasonText = "未知原因: " + IntegerToString(reason);
}
Print("EA 已停止,原因: ", reasonText);
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
// 每次有新報價時執行
// 這是 EA 的心跳,所有交易邏輯都在這裡判斷
// 檢查是否為新K線(避免在同一根K線重複交易)
datetime currentBarTime = iTime(_Symbol, _Period, 0);
if(currentBarTime == LastBarTime)
return; // 還是同一根K線,不執行交易邏輯
// 更新最後K線時間
LastBarTime = currentBarTime;
// 執行交易邏輯
CheckTradingSignals();
}
//+------------------------------------------------------------------+
//| 交易信號檢查函數 |
//+------------------------------------------------------------------+
void CheckTradingSignals()
{
// 這裡放置你的交易策略邏輯
// 目前先留空,我們會在後面章節填充
Print("新K線出現,時間: ", TimeToString(LastBarTime));
}
程式碼解說:三大核心函數
1. OnInit() – 初始化函數
– 只在 EA 啟動時執行一次
– 適合做初始化設定、檢查交易權限、載入設定檔
– 必須返回 INIT_SUCCEEDED 或 INIT_FAILED
2. OnDeinit() – 清理函數
– 在 EA 停止時執行
– 適合釋放資源、儲存設定、記錄日誌
– 可以根據停止原因做不同的清理工作
3. OnTick() – 心跳函數
– 每次有新報價時都會執行
– 這是 EA 的核心,所有交易邏輯都在這裡
– 通常會檢查是否為新K線,避免重複交易
第二步:加入簡單的交易策略
現在讓我們為 EA 加入一個實際的交易策略。我們從最經典的「移動平均線交叉策略」開始:
//+------------------------------------------------------------------+
//| 移動平均線交叉策略 |
//+------------------------------------------------------------------+
input int FastMAPeriod = 10; // 快速移動平均線週期
input int SlowMAPeriod = 30; // 慢速移動平均線週期
input ENUM_MA_METHOD MAType = MODE_SMA; // 移動平均線類型
// 指標句柄
int FastMAHandle = INVALID_HANDLE;
int SlowMAHandle = INVALID_HANDLE;
//+------------------------------------------------------------------+
//| 初始化指標 |
//+------------------------------------------------------------------+
bool InitializeIndicators()
{
// 建立快速移動平均線指標
FastMAHandle = iMA(_Symbol, _Period, FastMAPeriod, 0, MAType, PRICE_CLOSE);
// 建立慢速移動平均線指標
SlowMAHandle = iMA(_Symbol, _Period, SlowMAPeriod, 0, MAType, PRICE_CLOSE);
// 檢查指標是否建立成功
if(FastMAHandle == INVALID_HANDLE || SlowMAHandle == INVALID_HANDLE)
{
Print("錯誤: 無法建立移動平均線指標!");
return false;
}
Print("移動平均線指標初始化成功");
Print("快速MA週期: ", FastMAPeriod, ", 慢速MA週期: ", SlowMAPeriod);
return true;
}
//+------------------------------------------------------------------+
//| 更新 OnInit 函數 |
//+------------------------------------------------------------------+
int OnInit()
{
Print("=== EA 初始化開始 ===");
// 檢查交易權限
if(!IsTradeAllowed())
{
Alert("警告: 當前圖表不允許交易!");
return INIT_FAILED;
}
// 初始化指標
if(!InitializeIndicators())
return INIT_FAILED;
Print("=== EA 初始化完成 ===");
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| 取得移動平均線數值 |
//+------------------------------------------------------------------+
bool GetMAValues(double &fastMA[], double &slowMA[])
{
// 取得快速MA的最新3個值
if(CopyBuffer(FastMAHandle, 0, 0, 3, fastMA) < 3)
{
Print("錯誤: 無法取得快速MA數值");
return false;
}
// 取得慢速MA的最新3個值
if(CopyBuffer(SlowMAHandle, 0, 0, 3, slowMA) < 3)
{
Print("錯誤: 無法取得慢速MA數值");
return false;
}
// 將數組設置為時間序列(最新數據在索引0)
ArraySetAsSeries(fastMA, true);
ArraySetAsSeries(slowMA, true);
return true;
}
//+------------------------------------------------------------------+
//| 檢查移動平均線交叉信號 |
//+------------------------------------------------------------------+
ENUM_SIGNAL CheckMACrossSignal()
{
double fastMA[3], slowMA[3];
// 取得MA數值
if(!GetMAValues(fastMA, slowMA))
return SIGNAL_NONE;
// 檢查金叉(快速MA向上穿越慢速MA)
if(fastMA[1] <= slowMA[1] && fastMA[0] > slowMA[0])
{
Print("發現金叉信號!");
Print("快速MA: ", DoubleToString(fastMA[0], 5),
" > 慢速MA: ", DoubleToString(slowMA[0], 5));
return SIGNAL_BUY;
}
// 檢查死叉(快速MA向下穿越慢速MA)
if(fastMA[1] >= slowMA[1] && fastMA[0] < slowMA[0])
{
Print("發現死叉信號!");
Print("快速MA: ", DoubleToString(fastMA[0], 5),
" < 慢速MA: ", DoubleToString(slowMA[0], 5));
return SIGNAL_SELL;
}
return SIGNAL_NONE;
}
//+------------------------------------------------------------------+
//| 信號枚舉 |
//+------------------------------------------------------------------+
enum ENUM_SIGNAL
{
SIGNAL_NONE, // 無信號
SIGNAL_BUY, // 買入信號
SIGNAL_SELL // 賣出信號
};
//+------------------------------------------------------------------+
//| 更新交易信號檢查函數 |
//+------------------------------------------------------------------+
void CheckTradingSignals()
{
// 檢查是否有持倉
if(HasOpenPosition())
{
Print("已有持倉,跳過新信號檢查");
return;
}
// 檢查移動平均線交叉信號
ENUM_SIGNAL signal = CheckMACrossSignal();
switch(signal)
{
case SIGNAL_BUY:
OpenBuyPosition();
break;
case SIGNAL_SELL:
OpenSellPosition();
break;
case SIGNAL_NONE:
// 無信號,不做任何事
break;
}
}
第三步:實現下單功能
有了交易信號,接下來我們需要實現實際的下單功能:
//+------------------------------------------------------------------+
//| 檢查是否有持倉 |
//+------------------------------------------------------------------+
bool HasOpenPosition()
{
// 檢查當前品種是否有持倉
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(PositionGetSymbol(i) == _Symbol &&
PositionGetInteger(POSITION_MAGIC) == MagicNumber)
{
return true;
}
}
return false;
}
//+------------------------------------------------------------------+
//| 開立買單 |
//+------------------------------------------------------------------+
bool OpenBuyPosition()
{
MqlTradeRequest request;
MqlTradeResult result;
// 初始化請求結構
ZeroMemory(request);
ZeroMemory(result);
// 設定交易請求
request.action = TRADE_ACTION_DEAL;
request.symbol = _Symbol;
request.volume = LotSize;
request.type = ORDER_TYPE_BUY;
request.price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
request.sl = CalculateStopLoss(ORDER_TYPE_BUY, request.price);
request.tp = CalculateTakeProfit(ORDER_TYPE_BUY, request.price);
request.deviation = 10; // 允許10點的滑點
request.magic = MagicNumber;
request.comment = "MA Cross Buy Signal";
// 發送交易請求
if(!OrderSend(request, result))
{
Print("買單失敗! 錯誤碼: ", GetLastError());
Print("錯誤描述: ", ErrorDescription(GetLastError()));
return false;
}
Print("買單成功!");
Print("訂單編號: ", result.order);
Print("成交價格: ", DoubleToString(result.price, Digits()));
Print("手數: ", DoubleToString(result.volume, 2));
return true;
}
//+------------------------------------------------------------------+
//| 開立賣單 |
//+------------------------------------------------------------------+
bool OpenSellPosition()
{
MqlTradeRequest request;
MqlTradeResult result;
// 初始化請求結構
ZeroMemory(request);
ZeroMemory(result);
// 設定交易請求
request.action = TRADE_ACTION_DEAL;
request.symbol = _Symbol;
request.volume = LotSize;
request.type = ORDER_TYPE_SELL;
request.price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
request.sl = CalculateStopLoss(ORDER_TYPE_SELL, request.price);
request.tp = CalculateTakeProfit(ORDER_TYPE_SELL, request.price);
request.deviation = 10; // 允許10點的滑點
request.magic = MagicNumber;
request.comment = "MA Cross Sell Signal";
// 發送交易請求
if(!OrderSend(request, result))
{
Print("賣單失敗! 錯誤碼: ", GetLastError());
Print("錯誤描述: ", ErrorDescription(GetLastError()));
return false;
}
Print("賣單成功!");
Print("訂單編號: ", result.order);
Print("成交價格: ", DoubleToString(result.price, Digits()));
Print("手數: ", DoubleToString(result.volume, 2));
return true;
}
//+------------------------------------------------------------------+
//| 計算止損價格 |
//+------------------------------------------------------------------+
double CalculateStopLoss(ENUM_ORDER_TYPE orderType, double entryPrice)
{
if(StopLoss <= 0)
return 0; // 不設定止損
double stopLossPrice;
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
if(orderType == ORDER_TYPE_BUY)
{
stopLossPrice = entryPrice - StopLoss * point;
}
else // ORDER_TYPE_SELL
{
stopLossPrice = entryPrice + StopLoss * point;
}
// 檢查止損價格是否有效
if(!CheckStopLossTakeProfit(orderType, entryPrice, stopLossPrice, 0))
{
Print("警告: 止損價格無效,將不使用止損");
return 0;
}
return stopLossPrice;
}
//+------------------------------------------------------------------+
//| 計算止盈價格 |
//+------------------------------------------------------------------+
double CalculateTakeProfit(ENUM_ORDER_TYPE orderType, double entryPrice)
{
if(TakeProfit <= 0)
return 0; // 不設定止盈
double takeProfitPrice;
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
if(orderType == ORDER_TYPE_BUY)
{
takeProfitPrice = entryPrice + TakeProfit * point;
}
else // ORDER_TYPE_SELL
{
takeProfitPrice = entryPrice - TakeProfit * point;
}
// 檢查止盈價格是否有效
if(!CheckStopLossTakeProfit(orderType, entryPrice, 0, takeProfitPrice))
{
Print("警告: 止盈價格無效,將不使用止盈");
return 0;
}
return takeProfitPrice;
}
//+------------------------------------------------------------------+
//| 檢查止損止盈價格有效性 |
//+------------------------------------------------------------------+
bool CheckStopLossTakeProfit(ENUM_ORDER_TYPE orderType,
double entryPrice,
double stopLoss,
double takeProfit)
{
// 取得品種的最小止損止盈距離
double minStopDistance = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) *
SymbolInfoDouble(_Symbol, SYMBOL_POINT);
if(stopLoss > 0)
{
double stopDistance = MathAbs(entryPrice - stopLoss);
if(stopDistance < minStopDistance)
{
Print("錯誤: 止損距離太小 (", DoubleToString(stopDistance, 5),
") < 最小要求 (", DoubleToString(minStopDistance, 5), ")");
return false;
}
}
if(takeProfit > 0)
{
double tpDistance = MathAbs(entryPrice - takeProfit);
if(tpDistance < minStopDistance)
{
Print("錯誤: 止盈距離太小 (", DoubleToString(tpDistance, 5),
") < 最小要求 (", DoubleToString(minStopDistance, 5), ")");
return false;
}
}
return true;
}
//+------------------------------------------------------------------+
//| 錯誤代碼轉文字函數 |
//+------------------------------------------------------------------+
string ErrorDescription(int errorCode)
{
switch(errorCode)
{
case 0: return "成功";
case 1: return "沒有錯誤,但結果未知";
case 2: return "通用錯誤";
case 3: return "無效參數";
case 4: return "伺服器忙線";
case 5: return "舊版本";
case 6: return "無連線";
case 7: return "未足夠權限";
case 8: return "請求太頻繁";
case 9: return "被拒絕或禁止操作";
case 64: return "帳戶被禁用";
case 65: return "無效帳戶";
case 128: return "交易超時";
case 129: return "無效價格";
case 130: return "無效止損或止盈";
case 131: return "無效手數";
case 132: return "交易量不足";
case 133: return "市場關閉";
case 134: return "保證金不足";
case 135: return "市場改變";
case 136: return "無報價";
case 137: return "經紀商忙線";
case 138: return "重新報價";
case 139: return "訂單被鎖定";
case 140: return "只允許買單或賣單";
case 141: return "請求太多";
case 145: return "價格改變";
case 146: return "交易環境忙線";
case 147: return "交易環境改變";
case 148: return "太多訂單";
case 149: return "掛單和止損止盈同時存在";
case 150: return "交易規則禁止";
case 10004: return "請求處理中";
case 10006: return "請求失敗";
case 10007: return "請求取消";
case 10010: return "部分完成";
case 10011: return "請求錯誤";
case 10012: return "請求超時";
case 10013: return "請求無效";
case 10014: return "無效請求";
case 10015: return "無請求頻率設定";
case 10016: return "無足夠數據";
default: return "未知錯誤: " + IntegerToString(errorCode);
}
}
第四步:加入風險管理
一個好的 EA 必須有完善的風險管理機制:
//+------------------------------------------------------------------+
//| 風險管理參數 |
//+------------------------------------------------------------------+
input double MaxRiskPercent = 2.0; // 最大風險百分比
input double MaxLotsPerTrade = 1.0; // 每筆交易最大手數
input bool UseDynamicLotSize = true; // 使用動態手數計算
//+------------------------------------------------------------------+
//| 計算動態手數 |
//+------------------------------------------------------------------+
double CalculateDynamicLotSize()
{
if(!UseDynamicLotSize)
return LotSize;
// 根據帳戶餘額和風險百分比計算手數
double accountBalance = AccountInfoDouble(ACCOUNT_BALANCE);
double riskAmount = accountBalance * (MaxRiskPercent / 100.0);
// 計算每點價值
double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
double pointValue = tickValue * (tickSize / SymbolInfoDouble(_Symbol, SYMBOL_POINT));
if(pointValue <= 0)
{
Print("警告: 無法計算點值,使用固定手數");
return LotSize;
}
// 計算手數 = 風險金額 / (止損點數 × 每點價值)
double calculatedLots = riskAmount / (StopLoss * pointValue);
// 調整到符合交易規則
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
// 確保不小於最小手數
calculatedLots = MathMax(calculatedLots, minLot);
// 確保不大於最大手數
calculatedLots = MathMin(calculatedLots, MaxLotsPerTrade);
calculatedLots = MathMin(calculatedLots, maxLot);
// 調整到符合手數步進
calculatedLots = MathRound(calculatedLots / lotStep) * lotStep;
Print("動態手數計算:");
Print("帳戶餘額: $", DoubleToString(accountBalance, 2));
Print("風險金額: $", DoubleToString(riskAmount, 2));
Print("計算手數: ", DoubleToString(calculatedLots, 2));
return calculatedLots;
}
//+------------------------------------------------------------------+
//| 更新開倉函數 |
//+------------------------------------------------------------------+
bool OpenBuyPosition()
{
// 計算實際交易手數
double actualLotSize = CalculateDynamicLotSize();
// 檢查手數是否有效
if(!CheckLotSize(actualLotSize))
return false;
MqlTradeRequest request;
MqlTradeResult result;
ZeroMemory(request);
ZeroMemory(result);
request.action = TRADE_ACTION_DEAL;
request.symbol = _Symbol;
request.volume = actualLotSize;
request.type = ORDER_TYPE_BUY;
request.price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
request.sl = CalculateStopLoss(ORDER_TYPE_BUY, request.price);
request.tp = CalculateTakeProfit(ORDER_TYPE_BUY, request.price);
request.deviation = 10;
request.magic = MagicNumber;
request.comment = "MA Cross Buy - Dynamic Lot";
if(!OrderSend(request, result))
{
Print("買單失敗! 錯誤碼: ", GetLastError());
Print("錯誤描述: ", ErrorDescription(GetLastError()));
return false;
}
Print("買單成功! 手數: ", DoubleToString(actualLotSize, 2));
return true;
}
//+------------------------------------------------------------------+
//| 檢查手數有效性 |
//+------------------------------------------------------------------+
bool CheckLotSize(double lotSize)
{
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
// 檢查最小手數
if(lotSize < minLot)
{
Print("錯誤: 手數太小 (", DoubleToString(lotSize, 2),
") < 最小值 (", DoubleToString(minLot, 2), ")");
return false;
}
// 檢查最大手數
if(lotSize > maxLot)
{
Print("錯誤: 手數太大 (", DoubleToString(lotSize, 2),
") > 最大值 (", DoubleToString(maxLot, 2), ")");
return false;
}
// 檢查手數步進
double remainder = MathMod(lotSize, lotStep);
if(MathAbs(remainder) > 0.00001)
{
Print("錯誤: 手數不符合步進規則");
Print("手數: ", DoubleToString(lotSize, 2),
", 步進: ", DoubleToString(lotStep, 2));
return false;
}
return true;
}
第五步:完整的 EA 程式碼
以下是完整的 EA 程式碼,你可以直接複製到 MetaEditor 中編譯:
//+------------------------------------------------------------------+
//| MyFirstCompleteEA.mq5 |
//| Copyright 2026, James Lee |
//| https://mq5.fincosoft.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, James Lee"
#property link "https://mq5.fincosoft.com"
#property version "1.00"
#property strict
// 輸入參數
input double LotSize = 0.1; // 固定交易手數
input int StopLoss = 50; // 止損點數
input int TakeProfit = 100; // 止盈點數
input int MagicNumber = 123456; // EA 識別碼
input int FastMAPeriod = 10; // 快速移動平均線週期
input int SlowMAPeriod = 30; // 慢速移動平均線週期
input ENUM_MA_METHOD MAType = MODE_SMA; // 移動平均線類型
input double MaxRiskPercent = 2.0; // 最大風險百分比
input double MaxLotsPerTrade = 1.0; // 每筆交易最大手數
input bool UseDynamicLotSize = true; // 使用動態手數計算
// 全域變數
datetime LastBarTime = 0;
int FastMAHandle = INVALID_HANDLE;
int SlowMAHandle = INVALID_HANDLE;
// 信號枚舉
enum ENUM_SIGNAL
{
SIGNAL_NONE,
SIGNAL_BUY,
SIGNAL_SELL
};
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
Print("=== MyFirstCompleteEA 初始化開始 ===");
// 檢查交易權限
if(!IsTradeAllowed())
{
Alert("警告: 當前圖表不允許交易!");
return INIT_FAILED;
}
// 初始化指標
if(!InitializeIndicators())
return INIT_FAILED;
// 顯示帳戶資訊
DisplayAccountInfo();
Print("=== EA 初始化完成 ===");
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| Expert deinitialization function |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
// 釋放指標句柄
if(FastMAHandle != INVALID_HANDLE)
IndicatorRelease(FastMAHandle);
if(SlowMAHandle != INVALID_HANDLE)
IndicatorRelease(SlowMAHandle);
Print("EA 已停止,指標資源已釋放");
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
// 檢查是否為新K線
datetime currentBarTime = iTime(_Symbol, _Period, 0);
if(currentBarTime == LastBarTime)
return;
LastBarTime = currentBarTime;
// 執行交易邏輯
CheckTradingSignals();
}
//+------------------------------------------------------------------+
//| 初始化指標 |
//+------------------------------------------------------------------+
bool InitializeIndicators()
{
FastMAHandle = iMA(_Symbol, _Period, FastMAPeriod, 0, MAType, PRICE_CLOSE);
SlowMAHandle = iMA(_Symbol, _Period, SlowMAPeriod, 0, MAType, PRICE_CLOSE);
if(FastMAHandle == INVALID_HANDLE || SlowMAHandle == INVALID_HANDLE)
{
Print("錯誤: 無法建立移動平均線指標!");
return false;
}
Print("移動平均線指標初始化成功");
return true;
}
//+------------------------------------------------------------------+
//| 顯示帳戶資訊 |
//+------------------------------------------------------------------+
void DisplayAccountInfo()
{
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
double margin = AccountInfoDouble(ACCOUNT_MARGIN);
double freeMargin = AccountInfoDouble(ACCOUNT_FREEMARGIN);
Print("=== 帳戶資訊 ===");
Print("帳戶餘額: $", DoubleToString(balance, 2));
Print("帳戶淨值: $", DoubleToString(equity, 2));
Print("已用保證金: $", DoubleToString(margin, 2));
Print("可用保證金: $", DoubleToString(freeMargin, 2));
Print("槓桿: 1:", AccountInfoInteger(ACCOUNT_LEVERAGE));
}
//+------------------------------------------------------------------+
//| 檢查交易信號 |
//+------------------------------------------------------------------+
void CheckTradingSignals()
{
// 檢查是否有持倉
if(HasOpenPosition())
{
Print("已有持倉,跳過新信號檢查");
return;
}
// 檢查移動平均線交叉信號
ENUM_SIGNAL signal = CheckMACrossSignal();
switch(signal)
{
case SIGNAL_BUY:
OpenBuyPosition();
break;
case SIGNAL_SELL:
OpenSellPosition();
break;
}
}
//+------------------------------------------------------------------+
//| 檢查移動平均線交叉信號 |
//+------------------------------------------------------------------+
ENUM_SIGNAL CheckMACrossSignal()
{
double fastMA[3], slowMA[3];
if(!GetMAValues(fastMA, slowMA))
return SIGNAL_NONE;
// 金叉信號
if(fastMA[1] <= slowMA[1] && fastMA[0] > slowMA[0])
{
Print("發現金叉信號!");
return SIGNAL_BUY;
}
// 死叉信號
if(fastMA[1] >= slowMA[1] && fastMA[0] < slowMA[0])
{
Print("發現死叉信號!");
return SIGNAL_SELL;
}
return SIGNAL_NONE;
}
//+------------------------------------------------------------------+
//| 取得移動平均線數值 |
//+------------------------------------------------------------------+
bool GetMAValues(double &fastMA[], double &slowMA[])
{
if(CopyBuffer(FastMAHandle, 0, 0, 3, fastMA) < 3)
return false;
if(CopyBuffer(SlowMAHandle, 0, 0, 3, slowMA) < 3)
return false;
ArraySetAsSeries(fastMA, true);
ArraySetAsSeries(slowMA, true);
return true;
}
//+------------------------------------------------------------------+
//| 檢查是否有持倉 |
//+------------------------------------------------------------------+
bool HasOpenPosition()
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
if(PositionGetSymbol(i) == _Symbol &&
PositionGetInteger(POSITION_MAGIC) == MagicNumber)
{
return true;
}
}
return false;
}
//+------------------------------------------------------------------+
//| 計算動態手數 |
//+------------------------------------------------------------------+
double CalculateDynamicLotSize()
{
if(!UseDynamicLotSize)
return LotSize;
double accountBalance = AccountInfoDouble(ACCOUNT_BALANCE);
double riskAmount = accountBalance * (MaxRiskPercent / 100.0);
double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
double pointValue = tickValue * (tickSize / SymbolInfoDouble(_Symbol, SYMBOL_POINT));
if(pointValue <= 0)
return LotSize;
double calculatedLots = riskAmount / (StopLoss * pointValue);
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
calculatedLots = MathMax(calculatedLots, minLot);
calculatedLots = MathMin(calculatedLots, MaxLotsPerTrade);
calculatedLots = MathMin(calculatedLots, maxLot);
calculatedLots = MathRound(calculatedLots / lotStep) * lotStep;
return calculatedLots;
}
//+------------------------------------------------------------------+
//| 開立買單 |
//+------------------------------------------------------------------+
bool OpenBuyPosition()
{
double actualLotSize = CalculateDynamicLotSize();
if(!CheckLotSize(actualLotSize))
return false;
MqlTradeRequest request;
MqlTradeResult result;
ZeroMemory(request);
ZeroMemory(result);
request.action = TRADE_ACTION_DEAL;
request.symbol = _Symbol;
request.volume = actualLotSize;
request.type = ORDER_TYPE_BUY;
request.price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
request.sl = CalculateStopLoss(ORDER_TYPE_BUY, request.price);
request.tp = CalculateTakeProfit(ORDER_TYPE_BUY, request.price);
request.deviation = 10;
request.magic = MagicNumber;
request.comment = "MA Cross Buy";
if(!OrderSend(request, result))
{
Print("買單失敗! 錯誤: ", ErrorDescription(GetLastError()));
return false;
}
Print("買單成功! 手數: ", DoubleToString(actualLotSize, 2));
return true;
}
//+------------------------------------------------------------------+
//| 開立賣單 |
//+------------------------------------------------------------------+
bool OpenSellPosition()
{
double actualLotSize = CalculateDynamicLotSize();
if(!CheckLotSize(actualLotSize))
return false;
MqlTradeRequest request;
MqlTradeResult result;
ZeroMemory(request);
ZeroMemory(result);
request.action = TRADE_ACTION_DEAL;
request.symbol = _Symbol;
request.volume = actualLotSize;
request.type = ORDER_TYPE_SELL;
request.price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
request.sl = CalculateStopLoss(ORDER_TYPE_SELL, request.price);
request.tp = CalculateTakeProfit(ORDER_TYPE_SELL, request.price);
request.deviation = 10;
request.magic = MagicNumber;
request.comment = "MA Cross Sell";
if(!OrderSend(request, result))
{
Print("賣單失敗! 錯誤: ", ErrorDescription(GetLastError()));
return false;
}
Print("賣單成功! 手數: ", DoubleToString(actualLotSize, 2));
return true;
}
//+------------------------------------------------------------------+
//| 計算止損價格 |
//+------------------------------------------------------------------+
double CalculateStopLoss(ENUM_ORDER_TYPE orderType, double entryPrice)
{
if(StopLoss <= 0)
return 0;
double stopLossPrice;
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
if(orderType == ORDER_TYPE_BUY)
stopLossPrice = entryPrice - StopLoss * point;
else
stopLossPrice = entryPrice + StopLoss * point;
return NormalizeDouble(stopLossPrice, Digits());
}
//+------------------------------------------------------------------+
//| 計算止盈價格 |
//+------------------------------------------------------------------+
double CalculateTakeProfit(ENUM_ORDER_TYPE orderType, double entryPrice)
{
if(TakeProfit <= 0)
return 0;
double takeProfitPrice;
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
if(orderType == ORDER_TYPE_BUY)
takeProfitPrice = entryPrice + TakeProfit * point;
else
takeProfitPrice = entryPrice - TakeProfit * point;
return NormalizeDouble(takeProfitPrice, Digits());
}
//+------------------------------------------------------------------+
//| 檢查手數有效性 |
//+------------------------------------------------------------------+
bool CheckLotSize(double lotSize)
{
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
if(lotSize < minLot)
{
Print("錯誤: 手數太小");
return false;
}
if(lotSize > maxLot)
{
Print("錯誤: 手數太大");
return false;
}
double remainder = MathMod(lotSize, lotStep);
if(MathAbs(remainder) > 0.00001)
{
Print("錯誤: 手數不符合步進規則");
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| 錯誤描述函數 |
//+------------------------------------------------------------------+
string ErrorDescription(int errorCode)
{
switch(errorCode)
{
case 0: return "成功";
case 130: return "無效止損或止盈";
case 131: return "無效手數";
case 134: return "保證金不足";
case 138: return "重新報價";
default: return "錯誤碼: " + IntegerToString(errorCode);
}
}
//+------------------------------------------------------------------+
實戰演練:測試你的第一個 EA
步驟 1:編譯程式碼
1. 打開 MetaEditor(F4)
2. 新建 Expert Advisor
3. 複製上面的完整程式碼
4. 點擊「編譯」(F7)
步驟 2:載入到圖表
1. 在 MT5 中打開 EURUSD H1 圖表
2. 從「導航器」拖曳 MyFirstCompleteEA 到圖表
3. 設定參數(建議先用模擬帳戶測試)
步驟 3:策略測試
1. 打開策略測試器(Ctrl+R)
2. 選擇你的 EA
3. 設定測試時間範圍(建議至少1年)
4. 使用「每個即時價」模式
5. 點擊「開始」
常見問題與解決方案
Q1:編譯時出現錯誤怎麼辦?
- 錯誤:'某某函數' 未定義 → 檢查函數名稱拼寫
- 錯誤:未結束的字串 → 檢查所有字串都有正確的引號
- 錯誤:未預期的 '}' → 使用編輯器的括號匹配功能檢查
Q2:EA 沒有交易怎麼辦?
1. 檢查日誌(Ctrl+T)是否有錯誤訊息
2. 確認圖表有即時報價
3. 檢查 IsTradeAllowed() 是否返回 true
4. 確認移動平均線有正確計算
Q3:如何優化這個 EA?
1. 加入更多確認指標(RSI、MACD)
2. 實作追蹤止損功能
3. 加入時間過濾(避開重要新聞)
4. 加入資金管理規則(馬丁格爾、反馬丁格爾)
下一步學習建議
恭喜你完成了第一個 MQL5 EA!但這只是開始,接下來你可以:
1. 學習技術指標整合:加入 RSI、MACD、布林帶等指標
2. 實作進出場策略:學習更多交易策略(突破、回調、趨勢跟隨)
3. 風險管理進階:實作動態止損、部位大小調整
4. 回測與優化:學習使用策略測試器進行參數優化
5. 實盤部署:從模擬帳戶過渡到真實交易
結語
建立第一個 EA 就像學騎腳踏車一樣,一開始可能會搖搖晃晃,但一旦掌握了基本技巧,後面就會越來越順手。記住幾個關鍵原則:
1. 從簡單開始:不要一開始就想建立完美的交易系統
2. 充分測試:在模擬帳戶中至少測試3個月
3. 風險第一:永遠把風險管理放在第一位
4. 持續學習:市場在變,你的交易系統也需要不斷進化
如果你在學習過程中遇到任何問題,歡迎在下方留言,我會盡力幫你解答。
---
系列文章預告:
1. ✅ 從零開始建立你的第一個 MQL5 專家顧問(本篇)
2. 🔜 MQL5 技術指標深度解析與實戰應用
3. 🔜 進階交易策略:多時間框架分析實作
4. 🔜 風險管理與資金分配策略
5. 🔜 EA 回測與參數優化完全指南
---
*本文由 James Lee 撰寫,轉載請註明出處。本文內容僅供教育目的,不構成投資建議。交易有風險,投資需謹慎。*