OnTick() 與交易邏輯:MQL5 EA 的核心運作機制
📌 本文重點
深入理解 MQL5 的 OnTick() 函數、新K線檢測技巧、避免重複交易的方法,以及如何在 OnTick() 中實現完整的交易邏輯流程。
深入理解 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 撰寫,轉載請註明出處。