一隻 EA 寫到能上線,檔案裡大半是跟策略無關的東西:自檢、算手數、下單重送、保本移動、事件檔、通知、日結。均線交叉 EA MaCrossMonitor.mq5 有 626 行,真正屬於「均線交叉」的只有建均線、讀均線、判斷交叉那幾段。要再寫一隻策略,就得整份複製,之後修一個 bug 要改好幾份。這篇把共用的部分抽成一個 .mqh,EA 檔只留訊號。
三個範例檔。EaKit.mqh 是共用框架,622 行;MaCrossKit.mq5 是同一個均線交叉,剩 77 行;ChannelKit.mq5 是第二隻策略:通道突破,81 行。兩隻 EA 都在策略測試器實跑,文中的日誌都是實際輸出。

先把範例跑起來
- EaKit.mqh 放到
MQL5\Include\OrzBlog\(OrzBlog 資料夾自己建);兩隻 EA 放MQL5\Experts\,在 MetaEditor 各按一次F7。 - 策略測試器
Ctrl+R:EURUSD,H1,2025.01.02 到 2025.01.31,模式「每次報價」,入金 10000,杠杆 1:100;EA 先選 MaCrossKit 跑一次,再換 ChannelKit 跑一次。
1. #include 怎麼找檔
#include <OrzBlog\EaKit.mqh> // 到 MQL5\Include\ 找
#include "EaKit.mqh" // 到這個 .mq5 的資料夾找角括號到 MQL5\Include\ 找,引號到「寫這行 #include 的檔」所在的資料夾找,兩種都可以帶子資料夾。兩邊不互相退讓:引號寫 "OrzBlog\EaKit.mqh",EA 旁邊沒有就報 not found,不會再去 Include 找;角括號也不會找 EA 旁邊的檔。多隻 EA 共用的放 Include,只給一隻 EA 用的放它旁邊。
編譯時 .mqh 的內容整段貼進來,一起編成 .ex5。所以 .ex5 不需要 .mqh 就能跑——我跑回測的那個終端,Include 底下根本沒有 OrzBlog。反過來,改了 .mqh 之後,用到它的每隻 EA 都要重新按 F7,新的 .ex5 才有改過的內容。
同一個檔被引入兩次很常見:MaCrossKit 引的 Trend.mqh 和 Oscilators.mqh 都各自引了 Indicator.mqh。編譯器只收一次,實測連兩條不同路徑指到同一個檔也一樣,所以 C 語言那套 #ifndef 守衛在 MQL5 不必寫。
2. 共用的參數跟著 .mqh 走
//=== 2. 共用的參數跟著 .mqh 走 ======================================
#include <Trade\Trade.mqh>
input group "共用:風險與出場"
input double i_RiskPct = 1.0; // 每筆風險(餘額的 %)
input double i_MaxLots = 1.0; // 手數上限
input double i_SlAtr = 2.0; // 停損 = ATR x 倍數
input double i_TpAtr = 3.0; // 停利倍數,0 不設
input double i_BeAtr = 1.0; // 保本門檻倍數,0 不用
input int i_BeLockPts = 10; // 保本時多鎖幾點
input double i_TrailAtr = 2.0; // 移動停損倍數,0 不用
input int i_Retry = 3; // 下單最多送幾次
input group "共用:通知"
input bool i_NotifyPush = true; // 推播到手機
input bool i_NotifyMail = false; // 寄電子郵件
input bool i_NotifyAlert = false; // 本機跳視窗
input string i_WebhookUrl = ""; // 空白=不送 WebRequest
input int i_StaleMin = 30; // 幾分鐘沒報價就警告,0 不用input 寫在 .mqh 裡也算數,每隻引入它的 EA 輸入頁都會有這些參數;input group 在輸入頁加一行分組標題。順序照原始碼由上往下:#include 寫在 EA 自己的 input 前面,共用的就排前面。測試器日誌開頭列的參數就是這個順序:
共用:風險與出場=
i_RiskPct=1.0
i_MaxLots=1.0
...
共用:通知=
i_NotifyPush=true
...
i_FastPeriod=10
i_SlowPeriod=30
i_MaMethod=1
i_AtrPeriod=14
i_Magic=20250801兩個限制。名稱會撞:EA 再宣告一次 i_RiskPct,編譯報 variable already defined(error 125)。預設值每隻 EA 都一樣:想讓每隻不同的,就別放進 .mqh。魔術碼就是這種——同一個商品上掛的每隻 EA 必須不同(同一隻掛兩個週期也算兩隻),所以它留在 EA 自己的 input,啟動時再交給框架。MaCrossKit 沿用 MaCrossMonitor 的 20250801,因為它是來取代舊版的,兩隻不會同時掛。
3. 用類別把狀態收起來
//=== 3. 用類別把狀態收起來 ==========================================
class CEaKit
{
private:
CTrade m_Trade;
long m_Magic;
datetime m_LastBarTime;
bool m_Ready; // 自檢過了沒
string m_LastBad; // 上次自檢的結果,變了才印
string m_LogName; // 事件檔檔名(Common\Files)
datetime m_DayStart; // 日結從幾點起算
bool m_StaleSaid; // 「報價停了」印過了沒
bool Preflight();
void AdoptPosition();
double CalcLots(double slDist, double &risk);
bool EnoughMargin(ENUM_ORDER_TYPE type, double lots,
double price);
bool SendMarket(bool isBuy, double lots, double sl, double tp);
bool OpenPosition(int dir, double atr);
void ManagePosition(ulong ticket, int type, double atr);
double PositionNet(long pos);
void DailySummary(datetime from, datetime to);
void CheckStale(datetime now);
bool InSession(datetime now, datetime &open);
public:
CEaKit() : m_Magic(0), m_LastBarTime(0), m_Ready(false),
m_LastBad("?"), m_DayStart(0),
m_StaleSaid(false) {}
bool Init(long magic); // OnInit 叫
void Deinit(); // OnDeinit 叫
bool Ready(); // 自檢+接手,過了才 true
bool IsNewBar();
void ForgetBar() { m_LastBarTime = 0; } // 這根下個 tick 重來
int FindMyPosition(ulong &ticket);
void ShowStatus();
void OnSignal(int dir, double atr); // 管倉+照訊號進出
void OnTrade(const MqlTradeTransaction &trans);
void OnTimer();
void Log(string msg);
void Notify(string msg);
};為什麼不直接把函數搬過去?原本的函數讀 g_Trade、g_LastBarTime、i_Magic 這些全域變數。搬進 .mqh 之後,這些名字要由 EA 提供:EA 沒宣告 i_Magic,編譯報 undeclared identifier(error 256),錯誤指在 .mqh 裡;EA 宣告了就過,寫在 #include 後面也過。.mqh 偷偷依賴 EA 裡的名字,只看 .mqh 本身看不出來。
類別把這些狀態收成自己的成員(m_ 開頭),放在 private:,類別外面碰不到;魔術碼由 Init(magic) 傳進來。EA 只宣告一個 CEaKit g_Kit,呼叫 public: 底下那幾個方法。public: 第一行 CEaKit() : m_Magic(0), … 是建構子,物件建立時自動執行,冒號後面是給成員初值。
方法的本體大多寫在類別外面,名字前加 CEaKit::,表示這是 CEaKit 的方法。例如 IsNewBar,原本讀寫全域的 g_LastBarTime,現在是成員 m_LastBarTime:
bool CEaKit::IsNewBar()
{
datetime t = iTime(_Symbol, _Period, 0);
if(t == m_LastBarTime)
return false;
m_LastBarTime = t;
return true;
}OnTick、OnTimer、OnTradeTransaction 這些事件函數留在 EA,再轉給框架。.mqh 裡如果直接寫了 OnTimer(),EA 想寫自己的就會報 function already defined and has body(error 165);框架的 OnTimer() 是類別的方法,名字一樣也不衝突。
4. 均線交叉:只剩訊號
//=== 4. 均線交叉:只剩訊號 ==========================================
#include <Indicators\Trend.mqh>
#include <Indicators\Oscilators.mqh>
#include <OrzBlog\EaKit.mqh>
input int i_FastPeriod = 10; // 快線期數
input int i_SlowPeriod = 30; // 慢線期數
input ENUM_MA_METHOD i_MaMethod = MODE_EMA; // 均線種類
input int i_AtrPeriod = 14; // ATR 期數
input long i_Magic = 20250801; // 魔術碼
CEaKit g_Kit;
CiMA g_Fast;
CiMA g_Slow;
CiATR g_Atr;
int OnInit()
{
if(!g_Fast.Create(_Symbol, _Period, i_FastPeriod, 0,
i_MaMethod, PRICE_CLOSE) ||
!g_Slow.Create(_Symbol, _Period, i_SlowPeriod, 0,
i_MaMethod, PRICE_CLOSE) ||
!g_Atr.Create(_Symbol, _Period, i_AtrPeriod))
{
Print("指標建立失敗,錯誤碼 ", GetLastError());
return INIT_FAILED;
}
return g_Kit.Init(i_Magic) ? INIT_SUCCEEDED : INIT_FAILED;
}
void OnDeinit(const int reason) { g_Kit.Deinit(); }
void OnTimer() { g_Kit.OnTimer(); }
void OnTradeTransaction(const MqlTradeTransaction &trans,
const MqlTradeRequest &request,
const MqlTradeResult &result)
{
g_Kit.OnTrade(trans);
}
void OnTick()
{
if(!g_Kit.Ready())
return;
g_Kit.ShowStatus();
if(!g_Kit.IsNewBar())
return;
g_Fast.Refresh();
g_Slow.Refresh();
g_Atr.Refresh();
double fast1 = g_Fast.Main(1), fast2 = g_Fast.Main(2);
double slow1 = g_Slow.Main(1), slow2 = g_Slow.Main(2);
double atr1 = g_Atr.Main(1);
if(fast2 == EMPTY_VALUE || slow2 == EMPTY_VALUE ||
atr1 == EMPTY_VALUE)
{
Print("指標資料還沒準備好,錯誤碼 ", GetLastError());
g_Kit.ForgetBar();
return;
}
int dir = -1; // -1 = 沒訊號
if(fast2 <= slow2 && fast1 > slow1) dir = POSITION_TYPE_BUY;
if(fast2 >= slow2 && fast1 < slow1) dir = POSITION_TYPE_SELL;
g_Kit.OnSignal(dir, atr1);
}OnTick 剩四件事:框架說可以動了沒、在圖上顯示持倉、是不是新 K 線、讀指標算訊號。訊號用 -1 表示沒有,POSITION_TYPE_BUY/POSITION_TYPE_SELL 表示方向,交給 OnSignal:它先管手上的倉(保本、移動停損),同方向不動,反方向先平再開。
抽完要證明行為沒變。同一天用同樣設定各跑一次 MaCrossMonitor 和 MaCrossKit:日誌裡帶時間的 376 行(EA 印的加上測試器自己的成交紀錄)只差事件檔檔名那一行,其餘逐字相同,包括 78 行開倉、保本、移動、平倉;報告的 28 筆成交也一樣,14 筆交易,總淨盈利 416.00。「同一天」要講究:策略測試器用的是券商現在的隔夜利息,隔天重跑,損益可能差幾分錢。
5. 第二隻 EA:通道突破
跟 MaCrossKit 一樣的樣板,用一行註解帶過:
//=== 5. 第二隻 EA:通道突破 =========================================
#include <Indicators\TimeSeries.mqh>
#include <Indicators\Oscilators.mqh>
#include <OrzBlog\EaKit.mqh>
input int i_Channel = 20; // 通道看幾根 K 線
input int i_AtrPeriod = 14; // ATR 期數
input long i_Magic = 20250802; // 魔術碼,別跟別隻 EA 撞
CEaKit g_Kit;
CiHigh g_High;
CiLow g_Low;
CiClose g_Close;
CiATR g_Atr;
int OnInit()
{
if(!g_High.Create(_Symbol, _Period) ||
!g_Low.Create(_Symbol, _Period) ||
!g_Close.Create(_Symbol, _Period) ||
!g_Atr.Create(_Symbol, _Period, i_AtrPeriod))
{
Print("指標建立失敗,錯誤碼 ", GetLastError());
return INIT_FAILED;
}
return g_Kit.Init(i_Magic) ? INIT_SUCCEEDED : INIT_FAILED;
}
// …OnDeinit、OnTimer、OnTradeTransaction 跟 MaCrossKit.mq5 一樣
void OnTick()
{
// …Ready、ShowStatus、IsNewBar 跟 MaCrossKit.mq5 一樣
g_High.Refresh();
g_Low.Refresh();
g_Close.Refresh();
g_Atr.Refresh();
int at; // MaxValue 順便回傳位置,不用
double top1 = g_High.MaxValue(2, i_Channel, at); // 前一根之前
double bot1 = g_Low.MinValue(2, i_Channel, at); // 的 N 根
double top2 = g_High.MaxValue(3, i_Channel, at);
double bot2 = g_Low.MinValue(3, i_Channel, at);
double close1 = g_Close.GetData(1);
double close2 = g_Close.GetData(2);
double atr1 = g_Atr.Main(1);
if(top2 == EMPTY_VALUE || bot2 == EMPTY_VALUE ||
close2 == EMPTY_VALUE || atr1 == EMPTY_VALUE)
{
Print("資料還沒準備好,錯誤碼 ", GetLastError());
g_Kit.ForgetBar();
return;
}
int dir = -1; // 剛收在通道外才算突破
if(close2 <= top2 && close1 > top1) dir = POSITION_TYPE_BUY;
if(close2 >= bot2 && close1 < bot1) dir = POSITION_TYPE_SELL;
g_Kit.OnSignal(dir, atr1);
}收盤剛站上前 20 根 K 線(不含剛收的那根)的最高價就做多,剛跌破最低價就做空。CiHigh/CiLow 的 MaxValue/MinValue 從第 2 根起往回找 i_Channel 根,第三個參數回傳找到的位置,這裡用不到。
保本、移動停損、事件檔、出場通知、日結,這隻 EA 一行都沒寫,全在框架裡。魔術碼 20250802,跟均線交叉不同,兩隻掛在同一個商品上才分得清誰的倉。同一段 2025 年 1 月:19 筆交易,總淨盈利 41.55。前三筆:
開空 #2 0.34 手 @1.03169 停損 294 點 風險 99.96
[通知] 出場 #2 停利 0.34 手 損益 147.90
開多 #4 0.35 手 @1.03042 停損 288 點 風險 100.80
[通知] 出場 #4 停利 0.35 手 損益 149.10
開多 #6 0.25 手 @1.04220 停損 401 點 風險 100.25
[通知] 出場 #6 停損 0.25 手 損益 -102.00本篇檔案:EaKit.mqh 放 MQL5\Include\OrzBlog\;MaCrossKit.mq5、ChannelKit.mq5 放 MQL5\Experts\。在 MetaEditor 打開 EA 按 F7 就能用。