OnTick() 與交易邏輯:MQL5 EA 的核心運作機制

📌 本文重點
深入理解 MQL5 的 OnTick() 函數、新K線檢測技巧、避免重複交易的方法,以及如何在 OnTick() 中實現完整的交易邏輯流程。

前言:OnTick() 是什麼?

在 MetaTrader 5 中,每當經紀商伺服器推送新的報價更新(稱為「Tick」),MT5 就會自動呼叫你的 EA 中的 OnTick() 函數。這是 EA 的「心跳」——EA 的所有交易判斷、信號檢測、持倉管理都在這裡執行。理解 OnTick() 的執行機制是寫好 EA 的關鍵。

Tick 的頻率與特性

Tick 的到來是不規律的:外匯交易時段活躍時,每秒可能有數十個 Tick;市場清淡或週末時,可能數分鐘才有一個 Tick。這有重要含義:

  • OnTick() 在同一根K線內可能被呼叫數百次
  • 如果交易邏輯不加控制,可能在同一根K線多次下單
  • OnTick() 必須執行快速,避免阻塞報價接收

核心技巧:新K線檢測

//+------------------------------------------------------------------+
//| 基於新K線的交易邏輯框架                                          |
//+------------------------------------------------------------------+

// 全域變數:記錄最後一根K線的時間
datetime g_lastBarTime = 0;

void OnTick()
{
    // 步驟1:檢查是否出現新K線
    if (!IsNewBar()) return;  // 不是新K線,直接退出
    
    // 步驟2:到這裡代表新K線剛開始
    // 更新技術指標(使用已完成的K線數據)
    if (!UpdateIndicators()) return;
    
    // 步驟3:管理現有持倉(移動止損、追蹤停利等)
    ManageOpenPositions();
    
    // 步驟4:檢查新的交易信號
    if (!HasOpenPosition())
    {
        int signal = GetTradingSignal();
        if (signal == 1)  OpenBuyTrade();
        if (signal == -1) OpenSellTrade();
    }
}

//+------------------------------------------------------------------+
//| 新K線檢測函數                                                    |
//+------------------------------------------------------------------+
bool IsNewBar()
{
    datetime currentBarTime = iTime(_Symbol, _Period, 0);
    
    if (currentBarTime == g_lastBarTime)
        return false;  // 同一根K線,返回 false
    
    g_lastBarTime = currentBarTime;  // 更新最後K線時間
    return true;  // 新K線,返回 true
}

為什麼要用已完成的K線(index=1)?

在技術分析中,我們應該使用已完成的K線(iClose(_Symbol, _Period, 1)),而非當前未完成的K線(index=0)。當前K線的價格還在變動,如果根據未完成K線下單,訊號可能在K線結束時消失——這就是「重繪(Repainting)」問題。

//+------------------------------------------------------------------+
//| 正確取得技術指標數值(使用已完成K線)                            |
//+------------------------------------------------------------------+
int g_fastMAHandle = INVALID_HANDLE;
int g_slowMAHandle = INVALID_HANDLE;

bool UpdateIndicators()
{
    double fastMA[3], slowMA[3];
    ArraySetAsSeries(fastMA, true);
    ArraySetAsSeries(slowMA, true);
    
    // CopyBuffer 的第三個參數=0 表示從最新開始,=1 表示跳過當前K線
    // 我們取3個值:index[0]=當前(未完成), [1]=前一根(完成), [2]=前兩根
    if (CopyBuffer(g_fastMAHandle, 0, 0, 3, fastMA) < 3) return false;
    if (CopyBuffer(g_slowMAHandle, 0, 0, 3, slowMA) < 3) return false;
    
    return true;
}

int GetTradingSignal()
{
    double fastMA[3], slowMA[3];
    ArraySetAsSeries(fastMA, true);
    ArraySetAsSeries(slowMA, true);
    CopyBuffer(g_fastMAHandle, 0, 0, 3, fastMA);
    CopyBuffer(g_slowMAHandle, 0, 0, 3, slowMA);
    
    // 使用 index[1] (前一根已完成K線) 和 index[2] (前兩根) 檢測交叉
    // 金叉:快線由下向上穿越慢線
    if (fastMA[2] <= slowMA[2] && fastMA[1] > slowMA[1])
        return 1;   // 買入信號
    
    // 死叉:快線由上向下穿越慢線
    if (fastMA[2] >= slowMA[2] && fastMA[1] < slowMA[1])
        return -1;  // 賣出信號
    
    return 0; // 無信號
}

完整的 OnTick() 交易邏輯架構

//+------------------------------------------------------------------+
//| 完整 EA 框架示範                                                  |
//+------------------------------------------------------------------+
#property version "1.00"

input int    InpFastMA   = 10;     // 快速MA週期
input int    InpSlowMA   = 30;     // 慢速MA週期
input double InpLotSize  = 0.1;    // 交易手數
input int    InpMagic    = 88888;  // 魔術數字

datetime g_lastBarTime   = 0;
int      g_fastMAHandle  = INVALID_HANDLE;
int      g_slowMAHandle  = INVALID_HANDLE;

int OnInit()
{
    g_fastMAHandle = iMA(_Symbol, _Period, InpFastMA, 0, MODE_SMA, PRICE_CLOSE);
    g_slowMAHandle = iMA(_Symbol, _Period, InpSlowMA, 0, MODE_SMA, PRICE_CLOSE);
    
    if (g_fastMAHandle == INVALID_HANDLE || g_slowMAHandle == INVALID_HANDLE)
    {
        Print("指標初始化失敗!");
        return INIT_FAILED;
    }
    return INIT_SUCCEEDED;
}

void OnDeinit(const int reason)
{
    IndicatorRelease(g_fastMAHandle);
    IndicatorRelease(g_slowMAHandle);
}

void OnTick()
{
    // 只在新K線時執行邏輯(避免重複處理)
    datetime curBarTime = iTime(_Symbol, _Period, 0);
    if (curBarTime == g_lastBarTime) return;
    g_lastBarTime = curBarTime;
    
    // 等待足夠的K線數量
    if (BarsCalculated(g_fastMAHandle) < InpSlowMA + 2) return;
    
    // 取得MA數值(使用已完成K線)
    double fast[3], slow[3];
    ArraySetAsSeries(fast, true);
    ArraySetAsSeries(slow, true);
    if (CopyBuffer(g_fastMAHandle, 0, 0, 3, fast) < 3) return;
    if (CopyBuffer(g_slowMAHandle, 0, 0, 3, slow) < 3) return;
    
    // 計算持倉數量(屬於本 EA 的)
    int positions = CountMyPositions();
    
    // 金叉 + 無持倉 = 開多單
    if (fast[2] <= slow[2] && fast[1] > slow[1] && positions == 0)
    {
        double price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
        Print("金叉信號!開多單 @ ", DoubleToString(price, _Digits));
        OpenOrder(ORDER_TYPE_BUY, price, InpLotSize);
    }
    
    // 死叉 + 有多單 = 平倉
    if (fast[2] >= slow[2] && fast[1] < slow[1] && positions > 0)
    {
        Print("死叉信號!平倉");
        CloseAllMyPositions();
    }
}

int CountMyPositions()
{
    int count = 0;
    for (int i = PositionsTotal() - 1; i >= 0; i--)
    {
        if (PositionGetSymbol(i) == _Symbol &&
            PositionGetInteger(POSITION_MAGIC) == InpMagic)
            count++;
    }
    return count;
}

bool OpenOrder(ENUM_ORDER_TYPE type, double price, double lots)
{
    MqlTradeRequest req = {};
    MqlTradeResult  res = {};
    req.action   = TRADE_ACTION_DEAL;
    req.symbol   = _Symbol;
    req.volume   = lots;
    req.type     = type;
    req.price    = price;
    req.deviation = 10;
    req.magic    = InpMagic;
    req.comment  = "MA Cross";
    return OrderSend(req, res);
}

void CloseAllMyPositions()
{
    for (int i = PositionsTotal() - 1; i >= 0; i--)
    {
        if (PositionGetSymbol(i) != _Symbol) continue;
        if (PositionGetInteger(POSITION_MAGIC) != InpMagic) continue;
        
        ulong ticket = PositionGetInteger(POSITION_TICKET);
        MqlTradeRequest req = {};
        MqlTradeResult  res = {};
        req.action   = TRADE_ACTION_DEAL;
        req.symbol   = _Symbol;
        req.volume   = PositionGetDouble(POSITION_VOLUME);
        req.position = ticket;
        req.type     = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
                       ? ORDER_TYPE_SELL : ORDER_TYPE_BUY;
        req.price    = (req.type == ORDER_TYPE_SELL)
                       ? SymbolInfoDouble(_Symbol, SYMBOL_BID)
                       : SymbolInfoDouble(_Symbol, SYMBOL_ASK);
        req.deviation = 10;
        OrderSend(req, res);
    }
}

OnTick() 效能注意事項

  • 避免在 OnTick() 中進行耗時操作:如讀取大量歷史數據、複雜計算
  • 善用新K線檢測:不需要每個 Tick 都執行完整邏輯
  • 快取指標句柄:在 OnInit() 中建立,在 OnTick() 中使用,在 OnDeinit() 中釋放
  • 使用 BarsCalculated() 確認數據就緒:避免在數據不足時計算

本文由 James Lee 撰寫,轉載請註明出處。

Similar Posts

發佈留言

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