從零開始建立你的第一個 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_SUCCEEDEDINIT_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 撰寫,轉載請註明出處。本文內容僅供教育目的,不構成投資建議。交易有風險,投資需謹慎。*

Similar Posts

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *