把 MQL5 重複的程式抽成 .mqh:#include、共用參數、類別框架,兩隻 EA 用同一套

一隻 EA 寫到能上線,檔案裡大半是跟策略無關的東西:自檢、算手數、下單重送、保本移動、事件檔、通知、日結。均線交叉 EA MaCrossMonitor.mq5 有 626 行,真正屬於「均線交叉」的只有建均線、讀均線、判斷交叉那幾段。要再寫一隻策略,就得整份複製,之後修一個 bug 要改好幾份。這篇把共用的部分抽成一個 .mqh,EA 檔只留訊號。

三個範例檔。EaKit.mqh 是共用框架,622 行;MaCrossKit.mq5 是同一個均線交叉,剩 77 行;ChannelKit.mq5 是第二隻策略:通道突破,81 行。兩隻 EA 都在策略測試器實跑,文中的日誌都是實際輸出。

結構圖:MaCrossKit.mq5(均線交叉,魔術碼 20250801)與 ChannelKit.mq5(通道突破,魔術碼 20250802)都用 #include 引入 MQL5\Include\OrzBlog\ 的 EaKit.mqh,OnTick、OnTimer、OnTradeTransaction 分別轉給 g_Kit 的 OnSignal、OnTimer、OnTrade。EaKit.mqh 有共用 input 13 個(風險與出場、通知)和類別 CEaKit(自檢、手數、下單、保本移動、事件檔、通知、日結);底下註明編譯時 .mqh 被整段貼進 EA、各自編成 .ex5,所以 .ex5 不需要 .mqh 就能跑。
兩隻 EA 各留訊號與魔術碼,都引入同一個 EaKit.mqh

先把範例跑起來

  1. EaKit.mqh 放到 MQL5\Include\OrzBlog\(OrzBlog 資料夾自己建);兩隻 EA 放 MQL5\Experts\,在 MetaEditor 各按一次 F7
  2. 策略測試器 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.mqhOscilators.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_Tradeg_LastBarTimei_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;
}

OnTickOnTimerOnTradeTransaction 這些事件函數留在 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_BUYPOSITION_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 線(不含剛收的那根)的最高價就做多,剛跌破最低價就做空。CiHighCiLowMaxValueMinValue 從第 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.mqhMQL5\Include\OrzBlog\MaCrossKit.mq5ChannelKit.mq5MQL5\Experts\。在 MetaEditor 打開 EA 按 F7 就能用。

發佈留言

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

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料