MQL5 程式設計常見錯誤與最佳實踐:從踩坑到精通

📌 本文重點
本文揭露 MQL5 程式交易開發中最常踩的坑,包括:指標句柄管理錯誤、浮點數比較陷阱、OnTick 效能問題、Magic Number 管理、錯誤碼處理等,並提供具體的最佳實踐解決方案與可直接使用的程式碼模板。

MQL5 程式設計常見錯誤與最佳實踐:從踩坑到精通

> 「每個優秀的程式設計師都曾經寫過糟糕的程式碼,差別在於他們從錯誤中學習的速度。」 — 程式設計師的成長之路

前言:為什麼錯誤是學習的最佳機會?

我還記得剛開始學習 MQL5 的時候,寫的第一個 EA 在模擬帳戶運行了一週後,MT5 突然變得超級慢,最後直接當機。檢查日誌才發現,我忘記釋放指標句柄,導致記憶體洩漏。

這種「踩坑」經驗雖然痛苦,但卻是成長最快的方式。今天,我想分享這些年來我遇到(或看到別人遇到)的常見錯誤,以及如何避免它們。

錯誤 1:指標句柄的記憶體洩漏(最常見的殺手)

問題描述

每次呼叫 iMA()iRSI()iMACD() 等指標函數時,MT5 都會在後台建立一個指標句柄。如果你在 OnTick() 中不斷建立新的句柄而不釋放,記憶體就會像漏水的水桶一樣慢慢耗盡。

錯誤示範

// ❌ 錯誤寫法:每次 OnTick 都建立新句柄
void OnTick()
{
    // 每次有新報價就建立新的 MA 指標
    int maHandle = iMA(_Symbol, _Period, 20, 0, MODE_SMA, PRICE_CLOSE);
    
    double maValue[];
    CopyBuffer(maHandle, 0, 0, 1, maValue);
    
    // 問題:maHandle 沒有被釋放!
    // 運行一天後,可能建立了數千個句柄...
}

解決方案:正確的指標生命週期管理

// ✅ 正確寫法:全域句柄 + 生命週期管理
int g_maHandle = INVALID_HANDLE;
int g_rsiHandle = INVALID_HANDLE;

//+------------------------------------------------------------------+
//| EA 初始化時建立指標句柄                                          |
//+------------------------------------------------------------------+
int OnInit()
{
    Print("=== 初始化指標句柄 ===");
    
    // 建立移動平均線指標
    g_maHandle = iMA(_Symbol, _Period, 20, 0, MODE_SMA, PRICE_CLOSE);
    if(g_maHandle == INVALID_HANDLE)
    {
        Print("錯誤: 無法建立 MA 指標");
        return INIT_FAILED;
    }
    
    // 建立 RSI 指標
    g_rsiHandle = iRSI(_Symbol, _Period, 14, PRICE_CLOSE);
    if(g_rsiHandle == INVALID_HANDLE)
    {
        Print("錯誤: 無法建立 RSI 指標");
        IndicatorRelease(g_maHandle);  // 釋放已建立的指標
        return INIT_FAILED;
    }
    
    Print("指標句柄建立成功:");
    Print("MA Handle: ", g_maHandle);
    Print("RSI Handle: ", g_rsiHandle);
    
    return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
//| EA 結束時釋放指標句柄                                            |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
    Print("=== 釋放指標資源 ===");
    
    // 釋放 MA 指標
    if(g_maHandle != INVALID_HANDLE)
    {
        if(IndicatorRelease(g_maHandle))
            Print("MA 指標已釋放");
        else
            Print("警告: MA 指標釋放失敗");
        
        g_maHandle = INVALID_HANDLE;
    }
    
    // 釋放 RSI 指標
    if(g_rsiHandle != INVALID_HANDLE)
    {
        if(IndicatorRelease(g_rsiHandle))
            Print("RSI 指標已釋放");
        else
            Print("警告: RSI 指標釋放失敗");
        
        g_rsiHandle = INVALID_HANDLE;
    }
}

//+------------------------------------------------------------------+
//| 使用指標的範例                                                   |
//+------------------------------------------------------------------+
double GetMAValue(int shift = 0)
{
    if(g_maHandle == INVALID_HANDLE)
    {
        Print("錯誤: MA 指標句柄無效");
        return EMPTY_VALUE;
    }
    
    double values[1];
    if(CopyBuffer(g_maHandle, 0, shift, 1, values) <= 0)
    {
        Print("錯誤: 無法取得 MA 數值");
        return EMPTY_VALUE;
    }
    
    return values[0];
}

進階技巧:指標管理器類別

對於複雜的 EA,建議建立一個指標管理器:

//+------------------------------------------------------------------+
//| 指標管理器類別                                                   |
//+------------------------------------------------------------------+
class IndicatorManager
{
private:
    struct IndicatorHandle
    {
        int handle;
        string name;
        datetime created;
    };
    
    IndicatorHandle handles[];
    int count;
    
public:
    IndicatorManager() : count(0) {}
    
    ~IndicatorManager()
    {
        ReleaseAll();
    }
    
    // 建立指標
    int CreateMA(string symbol, ENUM_TIMEFRAMES timeframe, 
                 int period, int shift, ENUM_MA_METHOD method, 
                 ENUM_APPLIED_PRICE price)
    {
        int handle = iMA(symbol, timeframe, period, shift, method, price);
        if(handle != INVALID_HANDLE)
        {
            AddHandle(handle, "MA_" + IntegerToString(period));
        }
        return handle;
    }
    
    // 釋放所有指標
    void ReleaseAll()
    {
        for(int i = 0; i < count; i++)
        {
            if(handles[i].handle != INVALID_HANDLE)
            {
                IndicatorRelease(handles[i].handle);
                Print("釋放指標: ", handles[i].name);
            }
        }
        ArrayResize(handles, 0);
        count = 0;
    }
    
private:
    void AddHandle(int handle, string name)
    {
        ArrayResize(handles, count + 1);
        handles[count].handle = handle;
        handles[count].name = name;
        handles[count].created = TimeCurrent();
        count++;
    }
};

// 使用範例
IndicatorManager g_indicatorManager;

int OnInit()
{
    // 透過管理器建立指標
    int maHandle = g_indicatorManager.CreateMA(_Symbol, _Period, 20, 0, MODE_SMA, PRICE_CLOSE);
    // ... 不需要手動釋放,管理器會在解構時自動處理
    return INIT_SUCCEEDED;
}

錯誤 2:陣列操作的地雷區

問題 2.1:陣列索引越界

這是最容易導致程式崩潰的錯誤之一。

// ❌ 危險的陣列操作
double prices[10];
for(int i = 0; i <= 10; i++)  // 錯誤:i=10 時會越界!
{
    prices[i] = iClose(_Symbol, _Period, i);
}

// ✅ 安全的陣列操作
double safePrices[];
ArrayResize(safePrices, 10);  // 先設定大小

// 方法 1:使用 ArraySize()
for(int i = 0; i < ArraySize(safePrices); i++)
{
    safePrices[i] = iClose(_Symbol, _Period, i);
}

// 方法 2:使用 CopyClose() 自動處理
if(CopyClose(_Symbol, _Period, 0, 10, safePrices) > 0)
{
    // 複製成功,safePrices 已經自動調整大小
    ArraySetAsSeries(safePrices, true);  // 設定為時間序列
}

問題 2.2:未初始化陣列

// ❌ 未初始化就使用
double data[];
Print(data[0]);  // 錯誤:陣列大小為0!

// ✅ 正確的初始化
double properData[];
ArrayResize(properData, 10);
ArrayInitialize(properData, 0.0);  // 初始化為0

// 或者使用 Copy 函數自動初始化
double autoData[];
if(CopyRates(_Symbol, _Period, 0, 10, autoData) > 0)
{
    // autoData 已經被正確初始化並填充數據
}

錯誤 3:浮點數比較的陷阱

問題描述

在金融計算中,浮點數的精度問題可能導致嚴重的交易錯誤。

// ❌ 錯誤的浮點數比較
double price1 = 1.23456;
double price2 = 1.23457;

if(price1 == price2)  // 幾乎永遠不會成立!
{
    Print("價格相等");  // 這行永遠不會執行
}

// ✅ 正確的浮點數比較
bool CompareDoubles(double value1, double value2, int digits = 5)
{
    return NormalizeDouble(value1 - value2, digits) == 0.0;
}

// 或者使用內建的 CompareDoubles
#include 

if(MathAbs(price1 - price2) < 0.00001)  // 允許微小誤差
{
    Print("價格在誤差範圍內相等");
}

// 交易中的實際應用
bool CanOpenOrder(double currentPrice, double lastOrderPrice)
{
    // 檢查價格是否變動超過最小點數
    double minTick = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
    return MathAbs(currentPrice - lastOrderPrice) >= minTick;
}

錯誤 4:交易函數的錯誤處理不完整

常見問題

很多開發者只檢查 OrderSend() 的返回值,卻忽略了詳細的錯誤資訊。

// ❌ 不完整的錯誤處理
bool OpenOrder()
{
    MqlTradeRequest request = {0};
    MqlTradeResult result = {0};
    
    // 設定請求參數...
    
    if(!OrderSend(request, result))
    {
        Print("下單失敗");  // 太籠統了!
        return false;
    }
    
    return true;
}

// ✅ 完整的錯誤處理
bool OpenOrderWithDetailedError()
{
    MqlTradeRequest request;
    MqlTradeResult result;
    
    ZeroMemory(request);
    ZeroMemory(result);
    
    // 設定交易請求
    request.action    = TRADE_ACTION_DEAL;
    request.symbol    = _Symbol;
    request.volume    = 0.1;
    request.type      = ORDER_TYPE_BUY;
    request.price     = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
    request.deviation = 5;
    request.magic     = 123456;
    
    // 發送訂單
    bool success = OrderSend(request, result);
    
    if(!success)
    {
        int errorCode = GetLastError();
        Print("=== 下單失敗詳細資訊 ===");
        Print("錯誤碼: ", errorCode);
        Print("錯誤描述: ", GetTradeErrorDescription(errorCode));
        Print("返回碼: ", result.retcode);
        Print("返回描述: ", GetRetcodeDescription(result.retcode));
        
        // 根據錯誤類型採取不同行動
        HandleOrderError(errorCode, result.retcode);
        
        return false;
    }
    
    Print("=== 下單成功 ===");
    Print("訂單編號: ", result.order);
    Print("成交價格: ", DoubleToString(result.price, Digits()));
    Print("成交量: ", DoubleToString(result.volume, 2));
    Print("手續費: ", DoubleToString(result.commission, 2));
    
    return true;
}

//+------------------------------------------------------------------+
//| 詳細的錯誤描述函數                                               |
//+------------------------------------------------------------------+
string GetTradeErrorDescription(int errorCode)
{
    switch(errorCode)
    {
        case 0:   return "成功";
        case 1:   return "沒有錯誤,但結果未知";
        case 2:   return "通用錯誤";
        case 3:   return "無效參數";
        case 4:   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 145: return "價格改變";
        case 146: return "交易環境忙線";
        case 10004: return "請求處理中";
        case 10006: return "請求失敗";
        case 10007: return "請求取消";
        default:  return "未知錯誤: " + IntegerToString(errorCode);
    }
}

string GetRetcodeDescription(long retcode)
{
    switch(retcode)
    {
        case 10004: return "TRADE_RETCODE_REQUOTE";
        case 10006: return "TRADE_RETCODE_REJECT";
        case 10007: return "TRADE_RETCODE_CANCEL";
        case 10008: return "TRADE_RETCODE_PLACED";
        case 10009: return "TRADE_RETCODE_DONE";
        case 10010: return "TRADE_RETCODE_DONE_PARTIAL";
        case 10011: return "TRADE_RETCODE_ERROR";
        case 10012: return "TRADE_RETCODE_TIMEOUT";
        case 10013: return "TRADE_RETCODE_INVALID";
        case 10014: return "TRADE_RETCODE_INVALID_VOLUME";
        case 10015: return "TRADE_RETCODE_INVALID_PRICE";
        case 10016: return "TRADE_RETCODE_INVALID_STOPS";
        case 10017: return "TRADE_RETCODE_TRADE_DISABLED";
        case 10018: return "TRADE_RETCODE_MARKET_CLOSED";
        case 10019: return "TRADE_RETCODE_NO_MONEY";
        case 10020: return "TRADE_RETCODE_PRICE_CHANGED";
        case 10021: return "TRADE_RETCODE_PRICE_OFF";
        case 10022: return "TRADE_RETCODE_INVALID_EXPIRATION";
        case 10023: return "TRADE_RETCODE_ORDER_CHANGED";
        case 10024: return "TRADE_RETCODE_TOO_MANY_REQUESTS";
        case 10025: return "TRADE_RETCODE_NO_CHANGES";
        case 10026: return "TRADE_RETCODE_SERVER_DISABLES_AT";
        case 10027: return "TRADE_RETCODE_CLIENT_DISABLES_AT";
        case 10028: return "TRADE_RETCODE_LOCKED";
        case 10029: return "TRADE_RETCODE_FROZEN";
        case 10030: return "TRADE_RETCODE_INVALID_FILL";
        case 10031: return "TRADE_RETCODE_CONNECTION";
        case 10032: return "TRADE_RETCODE_ONLY_REAL";
        case 10033: return "TRADE_RETCODE_LIMIT_ORDERS";
        case 10034: return "TRADE_RETCODE_LIMIT_VOLUME";
        case 10035: return "TRADE_RETCODE_INVALID_ORDER";
        case 10036: return "TRADE_RETCODE_POSITION_CLOSED";
        case 10038: return "TRADE_RETCODE_INVALID_CLOSE_VOLUME";
        case 10039: return "TRADE_RETCODE_CLOSE_ORDER_EXIST";
        case 10040: return "TRADE_RETCODE_LIMIT_POSITIONS";
        case 10041: return "TRADE_RETCODE_REJECT_CANCEL";
        case 10042: return "TRADE_RETCODE_LONG_ONLY";
        case 10043: return "TRADE_RETCODE_SHORT_ONLY";
        case 10044: return "TRADE_RETCODE_CLOSE_ONLY";
        case 10045: return "TRADE_RETCODE_FIFO_CLOSE";
        default:   return "未知返回碼: " + IntegerToString(retcode);
    }
}

//+------------------------------------------------------------------+
//| 錯誤處理策略                                                     |
//+------------------------------------------------------------------+
void HandleOrderError(int errorCode, long retcode)
{
    // 重新報價 - 可以重試
    if(retcode == 10004 || errorCode == 138)
    {
        Print("收到重新報價,等待後重試...");
        Sleep(100);  // 等待100毫秒
        // 可以在此處加入重試邏輯
    }
    
    // 保證金不足 - 需要調整手數
    else if(errorCode == 134 || retcode == 10019)
    {
        Print("保證金不足,建議:");
        Print("1. 減少交易手數");
        Print("2. 檢查是否有其他持倉");
        Print("3. 等待帳戶入金");
    }
    
    // 無效手數 - 檢查手數規則
    else if(errorCode == 131 || retcode == 10014)
    {
        Print("無效手數,請檢查:");
        Print("1. 最小手數: ", SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN));
        Print("2. 最大手數: ", SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX));
        Print("3. 手數步進: ", SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP));
    }
    
    // 無效止損止盈 - 檢查距離
    else if(errorCode == 130 || retcode == 10016)
    {
        Print("無效止損止盈,請檢查:");
        Print("最小止損距離: ", 
              SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL), "點");
    }
}

錯誤 5:時間處理的常見陷阱

問題 5.1:時區混淆

// ❌ 混淆伺服器時間和本地時間
datetime serverTime = TimeCurrent();      // 伺服器時間
datetime localTime = TimeLocal();         // 本地電腦時間

// 重要:交易相關的時間應該使用伺服器時間!
if(TimeHour(serverTime) >= 9 && TimeHour(serverTime) < 17)
{
    // 交易時間判斷
}

// ✅ 正確的時間處理
bool IsTradingHours()
{
    datetime current = TimeCurrent();
    int hour = TimeHour(current);
    int dayOfWeek = TimeDayOfWeek(current);
    
    // 週一至週五,9:00-17:00(伺服器時間)
    if(dayOfWeek >= 1 && dayOfWeek <= 5)
    {
        return (hour >= 9 && hour < 17);
    }
    
    return false;
}

問題 5.2:新K線檢測不準確

// ❌ 不可靠的新K線檢測
datetime lastBar = 0;

void OnTick()
{
    datetime currentBar = iTime(_Symbol, _Period, 0);
    
    if(currentBar > lastBar)  // 可能漏掉某些情況
    {
        lastBar = currentBar;
        // 執行新K線邏輯
    }
}

// ✅ 可靠的新K線檢測
datetime g_lastBarTime = 0;
int g_lastBarIndex = -1;

bool IsNewBar()
{
    datetime currentTime;
    int currentIndex;
    
    // 方法1:使用時間和索引雙重檢查
    if(CopyTime(_Symbol, _Period, 0, 1, currentTime) > 0)
    {
        if(currentTime != g_lastBarTime)
        {
            g_lastBarTime = currentTime;
            return true;
        }
    }
    
    // 方法2:使用成交量確認(更可靠)
    long currentVolume[];
    if(CopyTickVolume(_Symbol, _Period, 0, 1, currentVolume) > 0)
    {
        if(currentVolume[0] == 1)  // 新K線的第一個tick
        {
            return true;
        }
    }
    
    return false;
}

錯誤 6:忽略交易規則檢查

問題描述

每個經紀商、每個交易品種都有不同的交易規則,忽略這些規則會導致下單失敗。

// ✅ 完整的交易規則檢查函數
bool CheckTradingRules(double volume, double price, 
                       double stopLoss, double takeProfit)
{
    // 1. 檢查交易時間
    if(!IsTradeAllowed())
    {
        Print("錯誤: 當前不允許交易");
        return false;
    }
    
    // 2. 檢查市場狀態
    if(!SymbolInfoInteger(_Symbol, SYMBOL_TRADE_MODE) == SYMBOL_TRADE_MODE_FULL)
    {
        Print("錯誤: 交易品種不可用");
        return false;
    }
    
    // 3. 檢查手數
    double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
    double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
    double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
    
    if(volume < minLot || volume > maxLot)
    {
        Print("錯誤: 手數超出範圍");
        return false;
    }
    
    // 檢查手數步進
    double remainder = MathMod(volume, lotStep);
    if(MathAbs(remainder) > 0.00001)
    {
        Print("錯誤: 手數不符合步進規則");
        return false;
    }
    
    // 4. 檢查止損止盈距離
    if(stopLoss > 0 || takeProfit > 0)
    {
        int stopLevel = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
        double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
        double minDistance = stopLevel * point;
        
        if(stopLoss > 0)
        {
            double slDistance = MathAbs(price - stopLoss);
            if(slDistance < minDistance)
            {
                Print("錯誤: 止損距離太小");
                return false;
            }
        }
        
        if(takeProfit > 0)
        {
            double tpDistance = MathAbs(price - takeProfit);
            if(tpDistance < minDistance)
            {
                Print("錯誤: 止盈距離太小");
                return false;
            }
        }
    }
    
    // 5. 檢查保證金
    double marginRequired;
    if(OrderCalcMargin(ORDER_TYPE_BUY, _Symbol, volume, price, marginRequired))
    {
        double freeMargin = AccountInfoDouble(ACCOUNT_FREEMARGIN);
        if(marginRequired > freeMargin)
        {
            Print("錯誤: 保證金不足");
            Print("需要: $", DoubleToString(marginRequired, 2));
            Print("可用: $", DoubleToString(freeMargin, 2));
            return false;
        }
    }
    
    return true;
}

錯誤 7:日誌記錄不完整

問題描述

當 EA 出現問題時,如果沒有完整的日誌,很難找出問題所在。

// ✅ 完整的日誌記錄系統
enum LOG_LEVEL
{
    LOG_LEVEL_DEBUG,    // 除錯資訊
    LOG_LEVEL_INFO,     // 一般資訊
    LOG_LEVEL_WARNING,  // 警告
    LOG_LEVEL_ERROR,    // 錯誤
    LOG_LEVEL_CRITICAL  // 嚴重錯誤
};

input LOG_LEVEL LogLevel = LOG_LEVEL_INFO;  // 預設記錄等級

void Log(LOG_LEVEL level, string message)
{
    // 根據設定決定是否記錄
    if(level < LogLevel)
        return;
    
    string prefix;
    switch(level)
    {
        case LOG_LEVEL_DEBUG:    prefix = "[DEBUG] "; break;
        case LOG_LEVEL_INFO:     prefix = "[INFO] "; break;
        case LOG_LEVEL_WARNING:  prefix = "[WARNING] "; break;
        case LOG_LEVEL_ERROR:    prefix = "[ERROR] "; break;
        case LOG_LEVEL_CRITICAL: prefix = "[CRITICAL] "; break;
    }
    
    string timestamp = TimeToString(TimeCurrent(), TIME_DATE|TIME_SECONDS);
    string logMessage = timestamp + " " + prefix + message;
    
    Print(logMessage);
    
    // 嚴重錯誤時發送通知
    if(level >= LOG_LEVEL_ERROR)
    {
        SendNotification(logMessage);
        
        // 可以寫入檔案備份
        WriteToLogFile(logMessage);
    }
}

// 使用範例
void OnTick()
{
    Log(LOG_LEVEL_DEBUG, "OnTick 開始執行");
    
    try
    {
        // 交易邏輯...
        Log(LOG_LEVEL_INFO, "檢查交易信號");
        
        if(someErrorCondition)
        {
            Log(LOG_LEVEL_ERROR, "發現交易條件錯誤");
        }
    }
    catch(string errorMsg)
    {
        Log(LOG_LEVEL_CRITICAL, "發生異常: " + errorMsg);
    }
    
    Log(LOG_LEVEL_DEBUG, "OnTick 執行完成");
}

錯誤 8:忽略效能優化

問題 8.1:過度頻繁的指標計算

// ❌ 效能不佳的寫法
void OnTick()
{
    // 每次tick都重新計算所有指標
    double ma1 = iMA(_Symbol, _Period, 10, 0, MODE_SMA, PRICE_CLOSE, 0);
    double ma2 = iMA(_Symbol, _Period, 20, 0, MODE_SMA, PRICE_CLOSE, 0);
    double ma3 = iMA(_Symbol, _Period, 30, 0, MODE_SMA, PRICE_CLOSE, 0);
    double rsi = iRSI(_Symbol, _Period, 14, PRICE_CLOSE, 0);
    double macd = iMACD(_Symbol, _Period, 12, 26, 9, PRICE_CLOSE, 0);
    
    // ... 每次tick都這樣做會很慢
}

// ✅ 效能優化的寫法
class EfficientEA
{
private:
    int ma10Handle, ma20Handle, ma30Handle, rsiHandle, macdHandle;
    datetime lastCalcTime;
    
public:
    bool Initialize()
    {
        // 初始化時建立一次指標句柄
        ma10Handle = iMA(_Symbol, _Period, 10, 0, MODE_SMA, PRICE_CLOSE);
        ma20Handle = iMA(_Symbol, _Period, 20, 0, MODE_SMA, PRICE_CLOSE);
        ma30Handle = iMA(_Symbol, _Period, 30, 0, MODE_SMA, PRICE_CLOSE);
        rsiHandle = iRSI(_Symbol, _Period, 14, PRICE_CLOSE);
        macdHandle = iMACD(_Symbol, _Period, 12, 26, 9, PRICE_CLOSE);
        
        lastCalcTime = 0;
        return true;
    }
    
    void OnTickEfficient()
    {
        // 只在需要時計算(例如每5秒或新K線)
        datetime currentTime = TimeCurrent();
        if(currentTime - lastCalcTime < 5)  // 5秒內不重複計算
            return;
        
        lastCalcTime = currentTime;
        
        // 批次取得指標數值
        double ma10 = GetIndicatorValue(ma10Handle, 0);
        double ma20 = GetIndicatorValue(ma20Handle, 0);
        double ma30 = GetIndicatorValue(ma30Handle, 0);
        double rsi = GetIndicatorValue(rsiHandle, 0);
        double macd = GetIndicatorValue(macdHandle, 0);
        
        // 使用快取的數值進行交易決策
    }
    
private:
    double GetIndicatorValue(int handle, int shift)
    {
        double value[1];
        if(CopyBuffer(handle, 0, shift, 1, value) > 0)
            return value[0];
        return EMPTY_VALUE;
    }
};

最佳實踐總結

1. 記憶體管理黃金法則

- 指標句柄:在 OnInit 建立,在 OnDeinit 釋放
- 陣列操作:總是檢查大小,使用安全的複製函數
- 物件生命週期:使用類別封裝資源管理

2. 錯誤處理最佳實踐

- 詳細日誌:記錄足夠的除錯資訊
- 分級處理:根據錯誤嚴重程度採取不同行動
- 重試機制:對於可恢復錯誤(如重新報價)實作重試

3. 交易安全準則

- 規則檢查:下單前檢查所有交易規則
- 風險控制:實作動態手數計算和風險限制
- 時間管理:正確處理時區和交易時間

4. 效能優化技巧

- 避免重複計算:快取指標數值
- 批次操作:一次取得多個數據點
- 適當頻率:根據策略需求調整計算頻率

實用除錯工具

1. 內建除錯功能

// 開啟詳細日誌
#property show_inputs
#property show_confirm

// 使用 DebugBreak() 暫停執行
if(someCondition)
{
    DebugBreak();  // 在此處暫停,檢查變數值
}

// 檢查記憶體使用
Print("記憶體使用: ", MQL5InfoInteger(MQL5_MEMORY_USED), " bytes");

2. 自訂除錯面板

// 在圖表上顯示除錯資訊
void DrawDebugInfo()
{
    string debugText = "";
    debugText += "時間: " + TimeToString(TimeCurrent()) + "\n";
    debugText += "價格: " + DoubleToString(SymbolInfoDouble(_Symbol, SYMBOL_BID), 5) + "\n";
    debugText += "持倉: " + IntegerToString(PositionsTotal()) + "\n";
    debugText += "錯誤: " + IntegerToString(GetLastError()) + "\n";
    
    ObjectCreate(0, "DebugPanel", OBJ_LABEL, 0, 0, 0);
    ObjectSetString(0, "DebugPanel", OBJPROP_TEXT, debugText);
    ObjectSetInteger(0, "DebugPanel", OBJPROP_CORNER, CORNER_RIGHT_UPPER);
    ObjectSetInteger(0, "DebugPanel", OBJPROP_XDISTANCE, 10);
    ObjectSetInteger(0, "DebugPanel", OBJPROP_YDISTANCE, 10);
    ObjectSetInteger(0, "DebugPanel", OBJPROP_FONTSIZE, 8);
}

結語:從錯誤中成長

學習程式設計就像學習任何技能一樣,犯錯是不可避免的過程。關鍵在於:

1. 擁抱錯誤:每個錯誤都是一次學習機會
2. 系統化除錯:建立標準的除錯流程
3. 持續改進:從每次錯誤中提取經驗教訓
4. 分享經驗:幫助其他人避免同樣的錯誤

記住,即使是經驗豐富的開發者也會犯錯。差別在於他們有更好的工具和習慣來快速發現和解決問題。

希望這篇文章能幫助你避開一些常見的陷阱,寫出更穩定、更可靠的 MQL5 程式!

---

下一步學習建議:
1. ✅ MQL5 程式設計常見錯誤與最佳實踐(本篇)
2. 🔜 深入理解 MQL5 記憶體管理與效能優化
3. 🔜 高級錯誤處理與異常恢復機制
4. 🔜 實戰:建立生產級別的 EA 框架

---

*本文分享的經驗來自多年的實戰開發,每個錯誤背後都有一個故事。如果你有特別的「踩坑」經驗想分享,歡迎在下方留言交流!*

Similar Posts

發佈留言

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