<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>MT5 教學 &#8211; 冏掉的Alan羅</title>
	<atom:link href="https://orzalanluo.com/archives/category/mt5/feed" rel="self" type="application/rss+xml" />
	<link>https://orzalanluo.com</link>
	<description>我什麼都不會，所以我在學習。</description>
	<lastBuildDate>Mon, 28 Sep 2026 23:18:44 +0000</lastBuildDate>
	<language>zh-TW</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.2</generator>
	<item>
		<title>MQL5 掛單：Buy Stop 與 Sell Stop、到期時間、改單刪單，一張成交就刪另一張</title>
		<link>https://orzalanluo.com/archives/1931</link>
					<comments>https://orzalanluo.com/archives/1931#respond</comments>
		
		<dc:creator><![CDATA[alanluo]]></dc:creator>
		<pubDate>Mon, 28 Sep 2026 23:18:44 +0000</pubDate>
				<category><![CDATA[MQL5 基礎]]></category>
		<category><![CDATA[MT5 教學]]></category>
		<category><![CDATA[MQL5]]></category>
		<category><![CDATA[MT5]]></category>
		<guid isPermaLink="false">https://orzalanluo.com/archives/1931</guid>

					<description><![CDATA[市價單是用現在的價格馬上成交；掛單是先把價格、停損停利交給券商，價格到了才成交。這篇把通道突破改成用掛單進場： [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">市價單是用現在的價格馬上成交；掛單是先把價格、停損停利交給券商，價格到了才成交。這篇把通道突破改成用掛單進場：每根 K 線在通道上緣（前 20 根的最高點）掛一張 Buy Stop、下緣（最低點）掛一張 Sell Stop，一張成交就刪掉另一張。</p>



<p class="wp-block-paragraph">範例 EA：<a href="https://orzalanluo.com/wp-content/uploads/2026/09/ChannelStop.mq5">ChannelStop.mq5</a>，送單、找單、改單、刪單都在這一個檔；參數、手數（每筆賠餘額 1% 倒推）這些在檔案開頭，文中不貼。文中的日誌都是策略測試器的實際輸出（EURUSD H1，2025 年 1 月，每次報價）。</p>



<h2 class="wp-block-heading">1. 四種掛單，各看哪個價</h2>



<p class="wp-block-paragraph">掛在現價上方的有兩種：Buy Stop 漲上來就買，追突破；Sell Limit 漲上來才賣，等反彈。現價下方也兩種：Sell Stop 跌下去就賣，追跌破；Buy Limit 跌下來才買，等回檔。另有 Buy Stop Limit、Sell Stop Limit（到價後改掛一張限價單），本文不用。</p>



<p class="wp-block-paragraph">Stop 這類，MT5 繁中介面叫「止損單」；Limit 這類叫「限價單」。為了不跟保護持倉的停損搞混，本文把 Stop 這類叫停止單。</p>



<figure class="wp-block-image size-full"><a href="https://orzalanluo.com/wp-content/uploads/mt5-13-pending-types.png"><img fetchpriority="high" decoding="async" width="1000" height="860" src="https://orzalanluo.com/wp-content/uploads/mt5-13-pending-types.png" alt="兩欄示意圖。左欄買單看 Ask：Buy Stop 在現價上方，漲上來就買；Buy Limit 在現價下方，跌下來才買。右欄賣單看 Bid：Sell Limit 在現價上方，漲上來才賣；Sell Stop 在現價下方，跌下去就賣。底下註明：價格一口氣跳過掛單價時，停止單成交得更差，限價單成交得更好。" class="wp-image-1930" srcset="https://orzalanluo.com/wp-content/uploads/mt5-13-pending-types.png 1000w, https://orzalanluo.com/wp-content/uploads/mt5-13-pending-types-300x258.png 300w, https://orzalanluo.com/wp-content/uploads/mt5-13-pending-types-644x554.png 644w" sizes="(max-width: 1000px) 100vw, 1000px" /></a><figcaption class="wp-element-caption">四種掛單：買單看 Ask、賣單看 Bid</figcaption></figure>



<p class="wp-block-paragraph">跟市價單一樣，<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">買單看 Ask、賣單看 Bid</mark>。MT5 圖表的 K 線是用 Bid 畫的，所以 K 線最高點還沒到掛單價，Buy Stop 就可能已經成交：</p>



<pre class="wp-block-prismatic-blocks"><code class="" data-line="">15:19:40  order [#193 buy stop 0.57 EURUSD at 1.03119] triggered
15:19:40  買入停止單成交 @1.03119  當時 Bid 1.03118</code></pre>



<p class="wp-block-paragraph">那根 H1 K 線（1 月 10 日 15:00）最高只到 1.03118，差 1 點沒碰到掛單價；Ask 比 Bid 高一個點差，先到了。換日時點差會放大，差得更多：1 月 14 日 00:02 那張 Buy Stop 掛 1.02469，成交那一刻 Bid 還在掛單價下方 80 點，只是點差撐開就買進了。</p>



<p class="wp-block-paragraph">價格一口氣跳過掛單價時，停止單用當時的價格成交，會比掛單價差；限價單則成交在掛單價或更好。另寫一隻測試 EA，在現價下方 30 點各掛一張 Buy Limit、Sell Stop；1 月 10 日 15:30:16 的一個 tick 同時越過兩張的價位：Buy Limit 掛 1.03009，成交在 1.03007，好 2 點；Sell Stop 掛 1.03008，成交在 1.03006，差 2 點。每次報價模式的 tick 是從 1 分鐘 K 線模擬的，跳幾點不代表實盤。</p>



<p class="wp-block-paragraph">掛單存在券商的伺服器上：EA 停了、電腦關了，價格到了照樣成交。</p>



<h2 class="wp-block-heading">2. 送出掛單：價格、停損停利、到期時間</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 2. 送出掛單：價格、停損停利、到期時間 ==========================
bool Done()                           // 伺服器收了這個請求
{
   uint code = g_Trade.ResultRetcode();
   return code == TRADE_RETCODE_DONE || code == TRADE_RETCODE_PLACED;
}

ENUM_ORDER_TYPE_TIME Life(datetime &amp;expiry)
{
   expiry = 0;
   long can = SymbolInfoInteger(_Symbol, SYMBOL_EXPIRATION_MODE);
   if(i_ExpireBars &lt;= 0 || (can &amp; SYMBOL_EXPIRATION_SPECIFIED) == 0)
      return ORDER_TIME_GTC;          // 一直有效，直到成交或刪掉
   expiry = iTime(_Symbol, _Period, 0)          // 這根的開盤時間
            + i_ExpireBars * PeriodSeconds();   // 再往後 N 根
   return ORDER_TIME_SPECIFIED;       // 到 expiry 就失效
}

bool SendStop(ENUM_ORDER_TYPE type, double price, double sl,
              double tp, double lots)
{
   datetime expiry;
   ENUM_ORDER_TYPE_TIME life = Life(expiry);
   bool ok = (type == ORDER_TYPE_BUY_STOP)
      ? g_Trade.BuyStop(lots, price, _Symbol, sl, tp, life, expiry)
      : g_Trade.SellStop(lots, price, _Symbol, sl, tp, life, expiry);
   if(!ok || !Done())
   {
      Print(&quot;掛單失敗 &quot;, g_Trade.ResultRetcode(), &quot; &quot;,
            g_Trade.ResultRetcodeDescription());
      return false;
   }
   return true;
}</code></pre>



<p class="wp-block-paragraph"><code class="" data-line="">CTrade</code> 的 <code class="" data-line="">BuyStop</code>、<code class="" data-line="">SellStop</code> 依序是手數、掛單價、商品、停損、停利、期限種類、到期時間。停損停利跟著掛單走，成交後就是這筆持倉的停損停利。回 true 只代表請求送出去了，<code class="" data-line="">Done()</code> 再看回傳碼：10009 是完成，10008 是單子已放上，都算收了。</p>



<p class="wp-block-paragraph">期限種類常用三種：<code class="" data-line="">ORDER_TIME_GTC</code> 一直有效，直到成交或刪掉；<code class="" data-line="">ORDER_TIME_DAY</code> 到當天結束；<code class="" data-line="">ORDER_TIME_SPECIFIED</code> 到指定的到期時間。</p>



<p class="wp-block-paragraph">券商收哪幾種，看 <code class="" data-line="">SYMBOL_EXPIRATION_MODE</code>。它是幾個旗標加起來的數，都是 <code class="" data-line="">SYMBOL_EXPIRATION_</code> 開頭：GTC 1、DAY 2、SPECIFIED 4、SPECIFIED_DAY 8（到指定那天結束）；實測 Tickmill 的 EURUSD 是 15＝1＋2＋4＋8，四種都收。<code class="" data-line="">can &amp; SYMBOL_EXPIRATION_SPECIFIED</code> 的 <code class="" data-line="">&amp;</code> 是位元運算，取出 4 那一位，結果 0 就是不收，跟參數 <code class="" data-line="">datetime &amp;expiry</code> 的 <code class="" data-line="">&amp;</code>（傳參考）是兩回事。<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">查的時候用 SYMBOL_EXPIRATION_*，送單才用 ORDER_TIME_*</mark>，兩組數值不同，不能混用。</p>



<p class="wp-block-paragraph">範例的 <code class="" data-line="">i_ExpireBars</code> 預設 2：到期時間設在兩根 K 線後。EA 正常時每根 K 線都會改單或刪掉重掛，到期時間跟著往後推；<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">EA 當掉或斷線，單子最多再掛兩根 K 線就自己消失</mark>（前提是券商收指定到期時間；不收的話 <code class="" data-line="">Life()</code> 退回 GTC，就沒有這層保護）。EA 不在的這段時間，沒人會在一張成交後刪另一張，兩張都可能成交，所以別掛太久。</p>



<p class="wp-block-paragraph">掛錯邊（Buy Stop 掛在 Ask 下方）會被拒，回 10015；到期時間在過去或離現在太近，回 10022。下面兩行是另一隻測試 EA 故意送錯的結果：</p>



<pre class="wp-block-prismatic-blocks"><code class="" data-line="">failed buy stop 0.1 EURUSD at 1.03595 [Invalid price]
failed buy stop 0.1 EURUSD at 1.06600 [Invalid expiration]</code></pre>



<p class="wp-block-paragraph">在測試器裡實測，到期時間設在 90 秒後被拒，120 秒後才收。</p>



<h2 class="wp-block-heading">3. 找自己的掛單</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 3. 找自己的掛單 ================================================
ulong FindMyOrder(ENUM_ORDER_TYPE type)
{
   for(int i = OrdersTotal() - 1; i &gt;= 0; i--)
   {
      ulong t = OrderGetTicket(i);    // 順便選取這張，下面才讀得到
      if(t == 0)
         continue;
      if(OrderGetString(ORDER_SYMBOL) != _Symbol ||
         OrderGetInteger(ORDER_MAGIC) != i_Magic)
         continue;
      if(OrderGetInteger(ORDER_TYPE) == type)
         return t;
   }
   return 0;
}

bool DeleteMyOrder(ENUM_ORDER_TYPE type, string why)
{
   ulong t = FindMyOrder(type);
   if(t == 0)
      return true;                    // 本來就沒有
   if(g_Trade.OrderDelete(t) &amp;&amp; Done())
   {
      Print(&quot;刪掉 #&quot;, t, &quot;：&quot;, why);
      return true;
   }
   Print(&quot;刪單失敗 &quot;, g_Trade.ResultRetcode(), &quot; &quot;,
         g_Trade.ResultRetcodeDescription());
   return false;
}</code></pre>



<p class="wp-block-paragraph">掛單還沒成交就不是持倉，<code class="" data-line="">PositionsTotal</code> 找不到，要用 <code class="" data-line="">OrdersTotal</code> 和 <code class="" data-line="">OrderGetTicket</code>。<code class="" data-line="">OrderGetTicket</code> 回傳訂單編號（ticket），同時選取這張單，之後 <code class="" data-line="">OrderGetString</code>、<code class="" data-line="">OrderGetInteger</code>、<code class="" data-line="">OrderGetDouble</code> 讀的都是它。跟找持倉一樣，用商品和魔術碼認出自己的單，再看類型。</p>



<p class="wp-block-paragraph">刪單是 <code class="" data-line="">OrderDelete(ticket)</code>，測試器日誌會印 <code class="" data-line="">order canceled</code>。<code class="" data-line="">DeleteMyOrder</code> 回傳有沒有刪成功（沒有要刪的也算成功），下一節會用到。</p>



<h2 class="wp-block-heading">4. 每根新 K 線：改價，或刪掉重掛</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 4. 每根新 K 線：改價，或刪掉重掛 ===============================
void PlaceOrMove(ENUM_ORDER_TYPE type, double price, double atr)
{
   bool   isBuy = (type == ORDER_TYPE_BUY_STOP);
   double now   = SymbolInfoDouble(_Symbol,
                                   isBuy ? SYMBOL_ASK : SYMBOL_BID);
   long   stops = SymbolInfoInteger(_Symbol,
                                   SYMBOL_TRADE_STOPS_LEVEL);
   // 停止單要掛在現價外面，而且至少隔 StopsLevel
   if((isBuy ? price - now : now - price) &lt;= stops * _Point)
   {
      DeleteMyOrder(type, &quot;現價已經過了通道線&quot;);
      return;
   }
   double dir = isBuy ? 1 : -1;
   double sl  = NormalizeDouble(price - dir * atr * i_SlAtr, _Digits);
   double tp  = NormalizeDouble(price + dir * atr * i_TpAtr, _Digits);
   double lots = CalcLots(MathAbs(price - sl));

   ulong t = FindMyOrder(type);
   if(t &gt; 0 &amp;&amp; MathAbs(OrderGetDouble(ORDER_VOLUME_CURRENT) - lots)
               &lt; 1e-8)
   {
      // 手數沒變：改價、停損停利、到期時間
      datetime expiry;
      ENUM_ORDER_TYPE_TIME life = Life(expiry);
      if(!g_Trade.OrderModify(t, price, sl, tp, life, expiry) ||
         !Done())
         Print(&quot;改單失敗 &quot;, g_Trade.ResultRetcode(), &quot; &quot;,
               g_Trade.ResultRetcodeDescription());
      return;
   }
   if(!DeleteMyOrder(type, &quot;手數要換&quot;))   // OrderModify 改不了手數
      return;                         // 舊的刪不掉，就別再掛一張
   if(lots &gt; 0)
      SendStop(type, price, sl, tp, lots);
}

void OnTick()
{
   datetime bar = iTime(_Symbol, _Period, 0);
   if(bar == g_LastBar)
      return;
   g_LastBar = bar;
   if(HavePosition())                 // 有倉就不掛，交給停損停利
   {
      DeleteMyOrder(ORDER_TYPE_BUY_STOP, &quot;已經有持倉&quot;);   // 平常早就
      DeleteMyOrder(ORDER_TYPE_SELL_STOP, &quot;已經有持倉&quot;);  // 刪掉了
      return;
   }

   g_High.Refresh();
   g_Low.Refresh();
   g_Atr.Refresh();
   int    at;                         // MaxValue 順便回傳位置，不用
   double top = g_High.MaxValue(1, i_Channel, at);  // 前 N 根的
   double bot = g_Low.MinValue(1, i_Channel, at);   // 最高、最低
   double atr = g_Atr.Main(1);
   if(top == EMPTY_VALUE || bot == EMPTY_VALUE || atr == EMPTY_VALUE)
   {
      g_LastBar = 0;                  // 資料還沒好，下個 tick 重來
      return;
   }
   PlaceOrMove(ORDER_TYPE_BUY_STOP, top, atr);
   PlaceOrMove(ORDER_TYPE_SELL_STOP, bot, atr);
}</code></pre>



<p class="wp-block-paragraph">通道線每根 K 線都可能移動；ATR 一變，停損距離就變，照每筆風險 1% 算出來的手數也跟著變。</p>



<p class="wp-block-paragraph"><code class="" data-line="">OrderModify</code> 能改掛單價、停損停利、到期時間，<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">改不了手數</mark>：<code class="" data-line="">CTrade</code> 的 <code class="" data-line="">OrderModify</code> 沒有手數參數。另寫測試 EA，把一張 0.1 手的掛單用 <code class="" data-line="">OrderSend</code> 直接送改單請求（<code class="" data-line="">TRADE_ACTION_MODIFY</code>）、手數寫 0.20：測試器裡回 10009 成功，價格改了，手數還是 0.1，不報錯。</p>



<p class="wp-block-paragraph">所以手數沒變就改單，變了就刪掉重掛；舊的刪不掉就先不掛，免得多出一張。這個月改單 124 次、刪掉重掛 318 次。</p>



<p class="wp-block-paragraph">停止單要掛在現價外面，還要隔商品的 StopsLevel 點（券商規定掛單價離現價至少幾點，Tickmill 是 0）。新 K 線一開盤就已經在通道外，這一邊就不掛，舊單也刪掉，不去追已經跑掉的價。</p>



<p class="wp-block-paragraph">有持倉就不掛新單，交給停損停利出場。</p>



<h2 class="wp-block-heading">5. 一張成交，就刪掉另一張</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 5. 一張成交，就刪掉另一張 ======================================
void OnTradeTransaction(const MqlTradeTransaction &amp;trans,
                        const MqlTradeRequest &amp;request,
                        const MqlTradeResult &amp;result)
{
   if(trans.type != TRADE_TRANSACTION_DEAL_ADD)
      return;                         // 只看「多了一筆成交」
   ulong deal = trans.deal;
   if(!HistoryDealSelect(deal) ||
      HistoryDealGetInteger(deal, DEAL_MAGIC) != i_Magic ||
      HistoryDealGetString(deal, DEAL_SYMBOL) != _Symbol ||
      HistoryDealGetInteger(deal, DEAL_ENTRY) != DEAL_ENTRY_IN)
      return;                         // 不是這隻 EA 的進場
   bool isBuy = (HistoryDealGetInteger(deal, DEAL_TYPE)
                 == DEAL_TYPE_BUY);
   Print(isBuy ? &quot;買入停止單&quot; : &quot;賣出停止單&quot;, &quot;成交 @&quot;,
         DoubleToString(HistoryDealGetDouble(deal, DEAL_PRICE),
                        _Digits), &quot;  當時 Bid &quot;, DoubleToString(
         SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits));
   DeleteMyOrder(isBuy ? ORDER_TYPE_SELL_STOP : ORDER_TYPE_BUY_STOP,
                 &quot;另一邊成交了&quot;);
}</code></pre>



<p class="wp-block-paragraph">MT5 的單子類型裡沒有「一張成交就取消另一張」（OCO）這種，要 EA 自己刪，而且要馬上刪：等到下一根 K 線才刪，價格掉頭碰到通道另一邊，另一張就可能也成交。</p>



<p class="wp-block-paragraph"><code class="" data-line="">OnTradeTransaction</code> 是交易有變化時 MT5 叫的函數。收到 <code class="" data-line="">TRADE_TRANSACTION_DEAL_ADD</code>（多了一筆成交），用 <code class="" data-line="">HistoryDealSelect</code> 選出這筆成交，確認是這隻 EA 的進場，就刪掉另一邊：</p>



<pre class="wp-block-prismatic-blocks"><code class="" data-line="">00:05:04  order [#3 sell stop 0.32 EURUSD at 1.03440] triggered
00:05:04  賣出停止單成交 @1.03433  當時 Bid 1.03433
00:05:04  order canceled [#2 buy stop 0.32 EURUSD at 1.04245]
00:05:04  刪掉 #2：另一邊成交了</code></pre>



<h2 class="wp-block-heading">6. 在測試器跑一個月</h2>



<p class="wp-block-paragraph">測試設定：EURUSD H1，2025.01.02 到 2025.01.31，每次報價，入金 10000，杠杆 1:100。整個月 34 張停止單成交：21 張正好在掛單價，13 張比掛單價差、最多差 10 點，沒有一張更好。</p>



<p class="wp-block-paragraph">過期的單，<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">測試器要等下一根 K 線開盤才刪</mark>。另一隻測試 EA 送一張 00:32 到期的單：測試週期選 H1，01:00 才刪；選 M15，00:45 就刪。「幾分鐘後失效」這種邏輯，測試器驗不準。</p>



<p class="wp-block-paragraph">週末也一樣：週五 23:00 改單，到期時間是週六 01:00，週一第一個 tick 才刪：</p>



<pre class="wp-block-prismatic-blocks"><code class="" data-line="">01.10 23:00:00  order modified [#206 buy stop 0.22 EURUSD at 1.03119]
01.10 23:00:00  order modified [#207 sell stop 0.22 EURUSD at 1.02109]
01.13 00:05:00  order expired [#206 buy stop 0.22 EURUSD at 1.03119]
01.13 00:05:00  order expired [#207 sell stop 0.22 EURUSD at 1.02109]</code></pre>



<p class="wp-block-paragraph">EA 在同一個 tick 重新掛上新的兩張。</p>



<p class="wp-block-paragraph">餘額 10000 跑到 9088.53：掛單只決定怎麼進場，這個通道突破在這個月是賠的。</p>



<p class="wp-block-paragraph">本篇檔案：<a href="https://orzalanluo.com/wp-content/uploads/2026/09/ChannelStop.mq5">ChannelStop.mq5</a> 放 <code class="" data-line="">MQL5\Experts\</code>，在 MetaEditor 打開按 <code class="" data-line="">F7</code>。</p>

]]></content:encoded>
					
					<wfw:commentRss>https://orzalanluo.com/archives/1931/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>MQL5 自訂指標：OnCalculate、緩衝區、prev_calculated，EA 用 CiCustom 讀</title>
		<link>https://orzalanluo.com/archives/1928</link>
					<comments>https://orzalanluo.com/archives/1928#respond</comments>
		
		<dc:creator><![CDATA[alanluo]]></dc:creator>
		<pubDate>Mon, 28 Sep 2026 05:55:52 +0000</pubDate>
				<category><![CDATA[MQL5 基礎]]></category>
		<category><![CDATA[MT5 教學]]></category>
		<category><![CDATA[MQL5]]></category>
		<category><![CDATA[MT5]]></category>
		<guid isPermaLink="false">https://orzalanluo.com/archives/1928</guid>

					<description><![CDATA[MT5 內建的均線、ATR 之外，想在圖上畫自己算的線，或讓好幾隻 EA 共用同一套計算，就寫自訂指標。這篇把 [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">MT5 內建的均線、ATR 之外，想在圖上畫自己算的線，或讓好幾隻 EA 共用同一套計算，就寫自訂指標。這篇把通道突破寫成指標：通道是前 20 根 K 線的最高點與最低點，收盤剛站上上軌、剛跌破下軌就畫一個箭頭；再寫一隻 EA 用 <code class="" data-line="">CiCustom</code> 讀箭頭下單。</p>



<p class="wp-block-paragraph">範例兩個檔：指標 <a href="https://orzalanluo.com/wp-content/uploads/2026/09/ChannelBreak.mq5">ChannelBreak.mq5</a>、EA <a href="https://orzalanluo.com/wp-content/uploads/2026/09/ChannelInd.mq5">ChannelInd.mq5</a>。EA 的下單、停損、移動停損都交給共用框架 <a href="https://orzalanluo.com/wp-content/uploads/2026/09/EaKit.mqh">EaKit.mqh</a>，自己只負責讀訊號。文中的日誌都是策略測試器的實際輸出。</p>



<h2 class="wp-block-heading">先把指標掛上圖表</h2>



<ol class="wp-block-list">
<li>ChannelBreak.mq5 放到 <code class="" data-line="">MQL5\Indicators\Blog\</code>（Blog 資料夾自己建），在 MetaEditor 打開按 <code class="" data-line="">F7</code>。</li>



<li>導航的「指標」底下會多一個 Blog 資料夾，把 ChannelBreak 拖到 EURUSD H1 圖表上，按確定。</li>
</ol>



<figure class="wp-block-image size-full"><a href="https://orzalanluo.com/wp-content/uploads/mt5-12-channel-chart.png"><img decoding="async" width="1000" height="660" src="https://orzalanluo.com/wp-content/uploads/mt5-12-channel-chart.png" alt="EURUSD H1 K 線圖，2025 年 1 月 13 日到 16 日。兩條藍線是通道上軌與下軌，呈階梯狀；收盤剛站上上軌的 K 線下方有綠色向上箭頭，收盤剛跌破下軌的 K 線上方有紅色向下箭頭。" class="wp-image-1927" srcset="https://orzalanluo.com/wp-content/uploads/mt5-12-channel-chart.png 1000w, https://orzalanluo.com/wp-content/uploads/mt5-12-channel-chart-300x198.png 300w, https://orzalanluo.com/wp-content/uploads/mt5-12-channel-chart-644x425.png 644w" sizes="(max-width: 1000px) 100vw, 1000px" /></a><figcaption class="wp-element-caption">①上軌 ②下軌 ③向上突破 ④向下突破（EURUSD H1）</figcaption></figure>



<h2 class="wp-block-heading">1. 檔頭的 #property：畫在哪、畫幾條</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 1. 檔頭的 #property：畫在哪、畫幾條 ============================
#property indicator_chart_window      // 畫在主圖（K 線上）
#property indicator_buffers 4         // 緩衝區（存數值的陣列）
#property indicator_plots   4         // 圖上畫幾樣
#property indicator_label1  &quot;上軌&quot;
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrDodgerBlue
#property indicator_label2  &quot;下軌&quot;
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrDodgerBlue
#property indicator_label3  &quot;向上突破&quot;
#property indicator_type3   DRAW_ARROW
#property indicator_color3  clrLime
#property indicator_width3  2
#property indicator_label4  &quot;向下突破&quot;
#property indicator_type4   DRAW_ARROW
#property indicator_color4  clrRed
#property indicator_width4  2

input int i_Channel = 20;             // 通道看幾根 K 線

double g_Upper[];
double g_Lower[];
double g_Buy[];
double g_Sell[];</code></pre>



<p class="wp-block-paragraph"><code class="" data-line="">indicator_chart_window</code> 表示畫在 K 線那個視窗；RSI 那種畫在下方另一個視窗的，改寫 <code class="" data-line="">indicator_separate_window</code>。</p>



<p class="wp-block-paragraph"><code class="" data-line="">indicator_buffers</code> 是存數值的陣列有幾個，<code class="" data-line="">indicator_plots</code> 是圖上畫幾樣；這裡都是 4：上軌、下軌、向上箭頭、向下箭頭。後面 <code class="" data-line="">label</code>、<code class="" data-line="">type</code>、<code class="" data-line="">color</code> 結尾的數字是第幾樣，<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">從 1 起算</mark>。<code class="" data-line="">DRAW_LINE</code> 把每根 K 線的值連成線，<code class="" data-line="">DRAW_ARROW</code> 在有值的 K 線畫一個符號。</p>



<p class="wp-block-paragraph">指標的 <code class="" data-line="">input</code> 跟 EA 一樣，掛上圖表時可以改。</p>



<h2 class="wp-block-heading">2. OnInit：把陣列接成緩衝區</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 2. OnInit：把陣列接成緩衝區 ====================================
int OnInit()
{
   if(i_Channel &lt; 1)
      return INIT_PARAMETERS_INCORRECT;
   SetIndexBuffer(0, g_Upper, INDICATOR_DATA);
   SetIndexBuffer(1, g_Lower, INDICATOR_DATA);
   SetIndexBuffer(2, g_Buy,   INDICATOR_DATA);
   SetIndexBuffer(3, g_Sell,  INDICATOR_DATA);
   PlotIndexSetInteger(2, PLOT_ARROW, 233);   // Wingdings 向上箭頭
   PlotIndexSetInteger(3, PLOT_ARROW, 234);   // 向下箭頭
   PlotIndexSetInteger(2, PLOT_ARROW_SHIFT, 15);  // 往下挪 15 像素
   PlotIndexSetInteger(3, PLOT_ARROW_SHIFT, -15); // 往上挪
   PlotIndexSetDouble(2, PLOT_EMPTY_VALUE, 0.0);  // 0＝這根沒箭頭
   PlotIndexSetDouble(3, PLOT_EMPTY_VALUE, 0.0);
   for(int k = 0; k &lt; 4; k++)                 // 前 N 根沒得算，不畫
      PlotIndexSetInteger(k, PLOT_DRAW_BEGIN, i_Channel);
   IndicatorSetString(INDICATOR_SHORTNAME,
                      &quot;ChannelBreak(&quot; + (string)i_Channel + &quot;)&quot;);
   return INIT_SUCCEEDED;
}</code></pre>



<p class="wp-block-paragraph"><code class="" data-line="">SetIndexBuffer</code> 把全域陣列接到第幾個緩衝區（從 0 起算，比 <code class="" data-line="">#property</code> 的編號少 1）。接上之後陣列大小由 MT5 管，K 線多一根陣列就多一格，不用 <code class="" data-line="">ArrayResize</code>。</p>



<p class="wp-block-paragraph"><code class="" data-line="">PLOT_ARROW</code> 是 Wingdings 字型的字碼，233 是向上箭頭、234 是向下；<code class="" data-line="">PLOT_ARROW_SHIFT</code> 把箭頭往下（正數）或往上（負數）挪幾個像素，免得壓在影線上。</p>



<p class="wp-block-paragraph"><code class="" data-line="">PLOT_EMPTY_VALUE</code> 是「這根不畫」的值。線用預設的 <code class="" data-line="">EMPTY_VALUE</code>，箭頭改成 0，原因在第 4 節。<code class="" data-line="">PLOT_DRAW_BEGIN</code> 讓前 20 根不畫，那幾根還湊不滿一個通道。</p>



<h2 class="wp-block-heading">3. OnCalculate：只算還沒算過的 K 線</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 3. OnCalculate：只算還沒算過的 K 線 ============================
int OnCalculate(const int rates_total, const int prev_calculated,
                const datetime &amp;time[], const double &amp;open[],
                const double &amp;high[], const double &amp;low[],
                const double &amp;close[], const long &amp;tick_volume[],
                const long &amp;volume[], const int &amp;spread[])
{
   if(rates_total &lt;= i_Channel)
      return 0;                       // K 線不夠，下次從頭再來
   int start = prev_calculated - 1;   // 上次最後一根要重算
   if(prev_calculated == 0)
   {
      for(int i = 0; i &lt; i_Channel; i++)
      {
         g_Upper[i] = EMPTY_VALUE;
         g_Lower[i] = EMPTY_VALUE;
         g_Buy[i]   = 0.0;
         g_Sell[i]  = 0.0;
      }
      start = i_Channel;
   }
   for(int i = start; i &lt; rates_total; i++)   // 索引 0 是最舊的
   {
      // 前面 N 根（不含這根）的最高、最低
      g_Upper[i] = high[ArrayMaximum(high, i - i_Channel, i_Channel)];
      g_Lower[i] = low[ArrayMinimum(low, i - i_Channel, i_Channel)];
      g_Buy[i]   = 0.0;
      g_Sell[i]  = 0.0;
      if(i == i_Channel)
         continue;                    // 前一根沒有通道，不比
      if(close[i - 1] &lt;= g_Upper[i - 1] &amp;&amp; close[i] &gt; g_Upper[i])
         g_Buy[i] = low[i];           // 剛收上上軌：箭頭畫在低點下
      if(close[i - 1] &gt;= g_Lower[i - 1] &amp;&amp; close[i] &lt; g_Lower[i])
         g_Sell[i] = high[i];
   }
   return rates_total;                // 下次的 prev_calculated
}</code></pre>



<p class="wp-block-paragraph">每來一個 tick，MT5 就叫一次 <code class="" data-line="">OnCalculate</code>，把整段 K 線的時間、開高低收陣列傳進來，<code class="" data-line="">rates_total</code> 是現在總共幾根。<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">陣列索引 0 是最舊的那根</mark>，<code class="" data-line="">rates_total - 1</code> 是最新、還沒收完的那根，跟 EA 裡 <code class="" data-line="">Main(1)</code> 從新往舊數正好相反。實測第一次呼叫時 <code class="" data-line="">time[0]</code> 是 2024.01.02 00:00，<code class="" data-line="">time[rates_total-1]</code> 是 2025.01.03 23:00。</p>



<p class="wp-block-paragraph"><code class="" data-line="">prev_calculated</code> 是<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">上一次 OnCalculate 回傳的值</mark>。讓 EA 每個 tick 讀一次指標，指標每次印出這兩個數：</p>



<pre class="wp-block-prismatic-blocks"><code class="" data-line="">00:00:00  rates_total=6284  prev_calculated=0
00:05:00  rates_total=6285  prev_calculated=6284
00:05:07  rates_total=6285  prev_calculated=6285
00:05:15  rates_total=6285  prev_calculated=6285
01:00:00  rates_total=6286  prev_calculated=6285
02:00:00  rates_total=6287  prev_calculated=6286</code></pre>



<p class="wp-block-paragraph">第一次是 0，整段 6,284 根全部算；同一根 K 線裡的 tick，<code class="" data-line="">prev_calculated</code> 等於 <code class="" data-line="">rates_total</code>；換新 K 線時少 1。所以從 <code class="" data-line="">prev_calculated - 1</code> 算起，兩種情況都對：同一根只重算最後一根，換根時把剛收完的那根用最終收盤價再算一次，再算新的一根。每次都從頭算也能跑，只是每個 tick 都重算幾千根。</p>



<p class="wp-block-paragraph">最後一根還沒收完，收盤價一直在變，箭頭可能出現又消失，所以迴圈裡每次重算都先把箭頭清成 0，EA 也只看收完的前一根。K 線不夠時回傳 0，下次 <code class="" data-line="">prev_calculated</code> 還是 0，會從頭再來。</p>



<h2 class="wp-block-heading">4. EA 用 CiCustom 讀自訂指標</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 4. EA 用 CiCustom 讀自訂指標 ===================================
#property tester_indicator &quot;Blog\\ChannelBreak.ex5&quot;
#include &lt;Indicators\Custom.mqh&gt;
#include &lt;Indicators\Oscilators.mqh&gt;
#include &lt;OrzBlog\EaKit.mqh&gt;

input int  i_Channel   = 20;          // 通道看幾根 K 線
input int  i_AtrPeriod = 14;          // ATR 期數
input long i_Magic     = 20250803;    // 魔術碼，別跟別隻 EA 撞

CEaKit   g_Kit;
CiCustom g_Chan;
CiATR    g_Atr;

int OnInit()
{
   MqlParam par[2];
   par[0].type          = TYPE_STRING;  // 第 1 個一定是指標路徑
   par[0].string_value  = &quot;Blog\\ChannelBreak&quot;;
   par[1].type          = TYPE_INT;     // 之後照指標 input 的順序
   par[1].integer_value = i_Channel;
   if(!g_Chan.NumBuffers(4) ||
      !g_Chan.Create(_Symbol, _Period, IND_CUSTOM, 2, par) ||
      !g_Atr.Create(_Symbol, _Period, i_AtrPeriod))
   {
      Print(&quot;指標建立失敗，錯誤碼 &quot;, 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 &amp;trans,
                        const MqlTradeRequest &amp;request,
                        const MqlTradeResult &amp;result)
{
   g_Kit.OnTrade(trans);
}

void OnTick()
{
   if(!g_Kit.Ready())
      return;
   g_Kit.ShowStatus();
   if(!g_Kit.IsNewBar())
      return;

   g_Chan.Refresh();
   g_Atr.Refresh();
   double buy  = g_Chan.GetData(2, 1);  // 緩衝區 2、前一根
   double sell = g_Chan.GetData(3, 1);
   double atr1 = g_Atr.Main(1);
   if(buy == EMPTY_VALUE || sell == EMPTY_VALUE ||   // 讀不到
      atr1 == EMPTY_VALUE)
   {
      Print(&quot;資料還沒準備好，錯誤碼 &quot;, GetLastError());
      g_Kit.ForgetBar();
      return;
   }

   int dir = -1;                        // 0＝沒箭頭
   if(buy != 0.0)  dir = POSITION_TYPE_BUY;
   if(sell != 0.0) dir = POSITION_TYPE_SELL;
   g_Kit.OnSignal(dir, atr1);
}</code></pre>



<p class="wp-block-paragraph"><code class="" data-line="">CiCustom</code> 建立時要一個 <code class="" data-line="">MqlParam</code> 陣列：<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">第 1 個一定是指標路徑</mark>（相對 <code class="" data-line="">MQL5\Indicators\</code>，不寫 .ex5），之後照指標 input 的順序給值。<code class="" data-line="">NumBuffers(4)</code> 要在 <code class="" data-line="">Create</code> 之前叫，告訴它指標有幾個緩衝區。<code class="" data-line="">GetData(2, 1)</code> 是第 2 號緩衝區（向上箭頭）的前一根，K 線跟 <code class="" data-line="">Main(1)</code> 一樣從新往舊數。</p>



<p class="wp-block-paragraph">不用類別也可以：<code class="" data-line="">iCustom(_Symbol, _Period, &quot;Blog\\ChannelBreak&quot;, 20)</code> 拿到 handle，指標的 input 直接接在後面當參數，再用 <code class="" data-line="">CopyBuffer</code> 讀。</p>



<p class="wp-block-paragraph"><code class="" data-line="">#property tester_indicator</code> 是給策略測試器看的，告訴它這隻 EA 會用到哪個指標。官方文件說：用 <code class="" data-line="">iCustom</code> 而且路徑是寫死的字串，測試器自己找得到；其他寫法（<code class="" data-line="">CiCustom</code> 就是）要寫這一行。實測在自己電腦上，拿掉這行也照跑，結果跟有寫一樣，因為指標檔就在本機的 <code class="" data-line="">Indicators</code> 資料夾；把指標檔也拿掉，就是下面這種 4802。這行是為測試交給其他電腦跑的情況寫的，照文件寫上就好。</p>



<p class="wp-block-paragraph">路徑打錯，測試器在 <code class="" data-line="">OnInit</code> 就停了，連 EA 自己的錯誤訊息都沒機會印：</p>



<pre class="wp-block-prismatic-blocks"><code class="" data-line="">custom indicator &#039;Blog\ChannelBrk&#039; cannot load [4802]
indicator create error in &#039;ChannelIndBad.mq5&#039; (1,1)
OnInit critical error
tester stopped because OnInit failed</code></pre>



<p class="wp-block-paragraph">箭頭的空值用 0，是因為 <code class="" data-line="">GetData</code> 讀不到資料時回傳 <code class="" data-line="">EMPTY_VALUE</code>。「沒箭頭」也用 <code class="" data-line="">EMPTY_VALUE</code> 的話，EA 分不出「沒訊號」和「讀不到」。實測把指標改成沒箭頭填 <code class="" data-line="">EMPTY_VALUE</code>、EA 不動，跑一天：<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">每個 tick 都當成讀不到</mark>，「資料還沒準備好」印了 127,296 次，一筆也沒做。</p>



<h2 class="wp-block-heading">5. 跟直接在 EA 裡算的版本比</h2>



<p class="wp-block-paragraph">同樣的通道突破，也可以在 EA 裡用 <code class="" data-line="">CiHigh</code>、<code class="" data-line="">CiLow</code> 直接算，例如 <a href="https://orzalanluo.com/wp-content/uploads/2026/09/ChannelKit.mq5">ChannelKit.mq5</a>。兩隻 EA 同一天、同設定跑（EURUSD H1，2025.01.02 到 2025.01.31，每次報價，入金 10000，杠杆 1:100）：都是 19 筆交易、最後餘額 10041.55，428 行的日誌除了指標那一行和測試耗時，逐行相同。</p>



<p class="wp-block-paragraph">算法搬進指標，結果沒變，多了兩個好處：訊號畫在圖上看得到，別隻 EA 也能讀同一個指標。</p>



<p class="wp-block-paragraph">本篇檔案：<a href="https://orzalanluo.com/wp-content/uploads/2026/09/ChannelBreak.mq5">ChannelBreak.mq5</a> 放 <code class="" data-line="">MQL5\Indicators\Blog\</code>；<a href="https://orzalanluo.com/wp-content/uploads/2026/09/ChannelInd.mq5">ChannelInd.mq5</a> 放 <code class="" data-line="">MQL5\Experts\</code>；<a href="https://orzalanluo.com/wp-content/uploads/2026/09/EaKit.mqh">EaKit.mqh</a> 放 <code class="" data-line="">MQL5\Include\OrzBlog\</code>。三個檔都在 MetaEditor 打開按 <code class="" data-line="">F7</code>，EA 會用到指標編出來的 ChannelBreak.ex5。</p>

]]></content:encoded>
					
					<wfw:commentRss>https://orzalanluo.com/archives/1928/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>MQL5 類別入門：struct、class、建構子，一個類別建出好幾個物件</title>
		<link>https://orzalanluo.com/archives/1925</link>
					<comments>https://orzalanluo.com/archives/1925#respond</comments>
		
		<dc:creator><![CDATA[alanluo]]></dc:creator>
		<pubDate>Wed, 23 Sep 2026 01:04:45 +0000</pubDate>
				<category><![CDATA[MQL5 基礎]]></category>
		<category><![CDATA[MT5 教學]]></category>
		<category><![CDATA[MQL5]]></category>
		<category><![CDATA[MT5]]></category>
		<guid isPermaLink="false">https://orzalanluo.com/archives/1925</guid>

					<description><![CDATA[MT5 標準程式庫的 CTrade、CiMA 都是現成的類別：CTrade g_Trade; 是用 CTrad [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">MT5 標準程式庫的 CTrade、CiMA 都是現成的類別：<code class="" data-line="">CTrade g_Trade;</code> 是用 CTrade 這個類別宣告一個物件（類別宣告出來的變數），<code class="" data-line="">g_Trade.Buy(...)</code> 是叫這個物件下單；<code class="" data-line="">CiMA g_Fast;</code> 也一樣，<code class="" data-line="">Create</code>、<code class="" data-line="">Refresh</code>、<code class="" data-line="">Main</code> 都是它的方法，方法就是寫在類別裡的函數。<code class="" data-line="">MqlDateTime</code> 的 <code class="" data-line="">dt.day_of_week</code>、交易事件的 <code class="" data-line="">trans.type</code> 則是結構的欄位。這篇講怎麼自己寫：struct、class、private／public、建構子與解構子。</p>



<p class="wp-block-paragraph">範例 <a href="https://orzalanluo.com/wp-content/uploads/2026/09/NewBarDemo.mq5">NewBarDemo.mq5</a> 是一隻只印不下單的 EA：自己寫一個判斷「新 K 線」的類別 <code class="" data-line="">CNewBar</code>，同一隻 EA 建兩個物件，一個看 H1、一個看 H4，各記各的；再用自訂結構 <code class="" data-line="">BarStat</code> 記每個週期觸發幾次，結束時印總結。文中的日誌都是策略測試器的實際輸出。</p>



<h2 class="wp-block-heading">先把範例跑起來</h2>



<ol class="wp-block-list">
<li>NewBarDemo.mq5 放到 <code class="" data-line="">MQL5\Experts\</code>，在 MetaEditor 打開按 <code class="" data-line="">F7</code>。</li>



<li>策略測試器 <code class="" data-line="">Ctrl+R</code>：EA 選 NewBarDemo，EURUSD，H1，2025.01.06 到 2025.01.11，模式「每次報價」，入金 10000，杠杆 1:100，按「開始」，跑完看「日誌」頁。</li>
</ol>



<h2 class="wp-block-heading">1. struct：把幾個值綁成一包</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 1. struct：把幾個值綁成一包 ====================================
struct BarStat
{
   int      count;                    // 觸發了幾次
   datetime last;                     // 最後一次是哪根 K 線
};</code></pre>



<p class="wp-block-paragraph"><code class="" data-line="">struct</code> 後面是型態名稱，大括號裡是欄位。這樣只是定義了一個新型態，跟 <code class="" data-line="">int</code>、<code class="" data-line="">datetime</code> 同一級；要用得宣告這個型態的變數，例如 <code class="" data-line="">BarStat g_StatH1 = {0, 0};</code>，大括號依序給每個欄位初值。之後用點號取欄位：<code class="" data-line="">g_StatH1.count++</code>、<code class="" data-line="">g_StatH1.last</code>，跟 <code class="" data-line="">dt.day_of_week</code> 同一個用法。</p>



<p class="wp-block-paragraph">欄位名稱照 <code class="" data-line="">MqlDateTime</code> 的習慣用小寫、不加前綴：這種只裝資料的結構，欄位就是給外面直接讀寫的。</p>



<h2 class="wp-block-heading">2. class：資料和方法放在一起</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 2. class：資料和方法放在一起 ===================================
class CNewBar
{
private:
   string          m_Symbol;          // 商品
   ENUM_TIMEFRAMES m_Period;          // 週期
   datetime        m_LastBar;         // 上次看到的 K 線開盤時間

public:
                   CNewBar(string symbol, ENUM_TIMEFRAMES period);
                  ~CNewBar();
   bool            IsNew();           // 本體寫在類別外面
   datetime        LastBar() { return m_LastBar; }
   string          Name()             // PERIOD_H1 取出 H1
   {
      return m_Symbol + &quot; &quot; + StringSubstr(EnumToString(m_Period), 7);
   }
};</code></pre>



<p class="wp-block-paragraph">類別也是自訂型態，通常把資料和處理資料的方法放在一起。<code class="" data-line="">CNewBar</code> 有三個成員變數：商品、週期、上次看到的 K 線開盤時間，名稱用 <code class="" data-line="">m_</code> 開頭，一看就知道是類別自己的。方法有 <code class="" data-line="">IsNew()</code>、<code class="" data-line="">LastBar()</code>、<code class="" data-line="">Name()</code>，另外兩行是建構子和解構子（第 4 節）。</p>



<p class="wp-block-paragraph"><code class="" data-line="">LastBar()</code>、<code class="" data-line="">Name()</code> 的本體直接寫在類別裡；<code class="" data-line="">IsNew()</code> 在類別裡只有一行宣告，本體寫在外面（第 3 節）。<code class="" data-line="">Name()</code> 裡的 <code class="" data-line="">m_Period</code> 是 <code class="" data-line="">PERIOD_H1</code> 時，<code class="" data-line="">EnumToString</code> 回傳字串 &#8220;PERIOD_H1&#8243;，<code class="" data-line="">StringSubstr</code> 去掉前面 7 個字元，剩 &#8220;H1&#8243;。</p>



<p class="wp-block-paragraph"><code class="" data-line="">private:</code> 底下的成員只有類別自己的方法能讀寫，<code class="" data-line="">public:</code> 底下的類別外面也能用。在類別外面（例如 <code class="" data-line="">OnTick</code> 裡）透過物件讀 <code class="" data-line="">m_LastBar</code>，編譯報 cannot access to private member（error 211）。要讓外面讀，就開一個 public 方法把值回傳出去，<code class="" data-line="">LastBar()</code> 就是：外面讀得到，改不了。</p>



<p class="wp-block-paragraph">struct 其實也能有方法、也能寫 <code class="" data-line="">private:</code>。本篇用得到的差別只有一個：沒寫 <code class="" data-line="">private:</code>／<code class="" data-line="">public:</code> 的時候，<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">struct 預設 public，class 預設 private</mark>。把範例的 <code class="" data-line="">struct BarStat</code> 改成 <code class="" data-line="">class BarStat</code>、其他不動，編譯先在兩個用 <code class="" data-line="">= {0, 0}</code> 給初值的地方報 cannot be initialized with initializer list（error 242），接著每個讀寫 <code class="" data-line="">count</code>、<code class="" data-line="">last</code> 的地方報 error 211；在大括號裡第一行補上 <code class="" data-line="">public:</code>，就又編得過。所以習慣上，只裝資料、給外面直接讀寫的用 struct；有狀態要保護的用 class。</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th></th><th>struct</th><th>class</th></tr></thead><tbody><tr><td>沒寫時預設</td><td>public</td><td>private</td></tr><tr><td>本篇</td><td><code class="" data-line="">BarStat</code></td><td><code class="" data-line="">CNewBar</code></td></tr><tr><td>現成的例子</td><td><code class="" data-line="">MqlDateTime</code></td><td><code class="" data-line="">CTrade</code>、<code class="" data-line="">CiMA</code></td></tr></tbody></table></figure>



<div style="height:24px" aria-hidden="true" class="wp-block-spacer"></div>



<h2 class="wp-block-heading">3. 方法寫在類別外面：類別名::</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 3. 方法寫在類別外面：類別名:: ==================================
bool CNewBar::IsNew()
{
   datetime t = iTime(m_Symbol, m_Period, 0);   // 這根的開盤時間
   if(t == 0 || t == m_LastBar)                 // 0＝資料還沒到
      return false;
   m_LastBar = t;
   return true;
}</code></pre>



<p class="wp-block-paragraph">本體寫在類別外面，名字前要加 <code class="" data-line="">CNewBar::</code>，意思是「CNewBar 的 IsNew」。有了這個前綴，本體裡才能直接用 <code class="" data-line="">m_Symbol</code>、<code class="" data-line="">m_LastBar</code> 這些成員，private 的也可以。</p>



<p class="wp-block-paragraph"><code class="" data-line="">iTime</code> 取這根 K 線的開盤時間，跟 <code class="" data-line="">m_LastBar</code> 一樣就是同一根；不一樣就是新的一根，記下來、回傳 true。取不到會回傳 0（例如資料還沒載好），當作沒有新 K 線。<code class="" data-line="">m_LastBar</code> 的初值是 0（建構子給的，第 4 節），所以 EA 一啟動，當下這根就會算一次。</p>



<p class="wp-block-paragraph"><mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">漏了 CNewBar::</mark>，外面那個就變成跟類別無關的普通函數，本體裡找不到 <code class="" data-line="">m_LastBar</code>，報 undeclared identifier（error 256）。本體剛好沒用到成員的話，錯誤改成指在類別裡那行宣告：function &#8216;CNewBar::IsNew&#8217; must have a body（error 111），類別宣告了 IsNew，卻沒有本體。</p>



<p class="wp-block-paragraph">哪些寫裡面、哪些寫外面，編譯器都接受。一兩行的寫在類別裡，長的寫外面，類別宣告就能一眼看完。</p>



<h2 class="wp-block-heading">4. 建構子與解構子</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 4. 建構子與解構子 ==============================================
CNewBar::CNewBar(string symbol, ENUM_TIMEFRAMES period)
   : m_Symbol(symbol), m_Period(period), m_LastBar(0)
{
   Print(&quot;建構 &quot;, Name());
}

CNewBar::~CNewBar()
{
   Print(&quot;解構 &quot;, Name());
}</code></pre>



<p class="wp-block-paragraph">建構子是名字跟類別一樣、不寫回傳型態的方法，物件建立時自動執行一次，負責給成員初值。參數後面冒號接的是初始化列：<code class="" data-line="">m_Symbol(symbol)</code> 把參數 symbol 放進 <code class="" data-line="">m_Symbol</code>，三個成員都給好了才進大括號，所以大括號裡的 <code class="" data-line="">Name()</code> 已經印得出「EURUSD H1」。建構子也能整個寫在類別裡，冒號和初始化列照樣接在參數後面。</p>



<p class="wp-block-paragraph">解構子是名字前面加 <code class="" data-line="">~</code> 的那個，物件要消失時自動執行，拿來收尾。這裡兩個都只印一行，看它們什麼時候跑：</p>



<pre class="wp-block-prismatic-blocks"><code class="" data-line="">建構 EURUSD H1
建構 EURUSD H4
OnInit
EURUSD H1 新 K 線 2025.01.06 00:00  第 1 次
...
OnDeinit
...
解構 EURUSD H4
解構 EURUSD H1</code></pre>



<p class="wp-block-paragraph"><mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">全域物件在 OnInit 之前就建好，OnDeinit 跑完才解構</mark>。建構照宣告順序（H1 先），解構順序相反（H4 先）。建構子裡已經印得出 EURUSD，表示這時 <code class="" data-line="">_Symbol</code> 已經有值；OnDeinit 裡兩個物件都還在，總結才印得出來。</p>



<p class="wp-block-paragraph">全域物件的建構、解構只發生在 EA 載入和卸載的時候。<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">換圖表的商品或週期不算卸載</mark>：我另外寫了一隻小 EA 掛在圖表上實測（不是測試器），讓它把自己的圖表從 EURUSD H1 換成 GBPUSD H4，結果只多跑了一次 OnDeinit、OnInit，全域物件沒有重建，裡面記的商品還是 EURUSD（官方文件也寫：換商品或週期時，全域變數不重設）。範例的 <code class="" data-line="">g_H1</code>、<code class="" data-line="">g_H4</code> 也一樣，圖表換了商品，它們看的還是建構時的商品；範例只用來看執行時機，沒處理這種情況。</p>



<h2 class="wp-block-heading">5. 一個類別，兩個物件</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 5. 一個類別，兩個物件 ==========================================
CNewBar g_H1(_Symbol, PERIOD_H1);
CNewBar g_H4(_Symbol, PERIOD_H4);
BarStat g_StatH1 = {0, 0};
BarStat g_StatH4 = {0, 0};

int OnInit()
{
   Print(&quot;OnInit&quot;);
   return INIT_SUCCEEDED;
}

void OnTick()
{
   if(g_H1.IsNew())
      Count(g_H1, g_StatH1);
   if(g_H4.IsNew())
      Count(g_H4, g_StatH4);
}

void Count(CNewBar &amp;bar, BarStat &amp;stat)   // 物件、結構要加 &amp;
{
   stat.count++;
   stat.last = bar.LastBar();
   Print(bar.Name(), &quot; 新 K 線 &quot;, TimeToString(stat.last),
         &quot;  第 &quot;, stat.count, &quot; 次&quot;);
}

void OnDeinit(const int reason)
{
   Print(&quot;OnDeinit&quot;);
   Summary(g_H1, g_StatH1);
   Summary(g_H4, g_StatH4);
}

void Summary(CNewBar &amp;bar, BarStat &amp;stat)
{
   Print(bar.Name(), &quot; 共 &quot;, stat.count, &quot; 次，最後一根 &quot;,
         TimeToString(stat.last));
}</code></pre>



<p class="wp-block-paragraph"><code class="" data-line="">CNewBar g_H1(_Symbol, PERIOD_H1);</code> 建一個物件，括號裡是傳給建構子的參數。建構子要兩個參數，寫成 <code class="" data-line="">CNewBar g_H1;</code> 會報 wrong parameters count, 0 passed, but 2 requires（error 199）。</p>



<p class="wp-block-paragraph">兩個物件用同一份程式碼，但<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">各有一份自己的成員</mark>：<code class="" data-line="">g_H1.IsNew()</code> 只動 g_H1 的 <code class="" data-line="">m_LastBar</code>，g_H4 的不受影響。<code class="" data-line="">OnTick</code> 每個 tick 兩個各問一次。</p>



<figure class="wp-block-image size-full"><a href="https://orzalanluo.com/wp-content/uploads/mt5-11-class-objects.png"><img decoding="async" width="1000" height="698" src="https://orzalanluo.com/wp-content/uploads/mt5-11-class-objects.png" alt="左邊是類別 CNewBar：private 成員 m_Symbol、m_Period、m_LastBar，public 方法 IsNew()、LastBar()、Name()。右邊兩個物件 g_H1、g_H4 各有一份成員：都是 EURUSD，週期分別是 PERIOD_H1、PERIOD_H4，m_LastBar 分別是 2025.01.10 23:00 與 20:00；g_H1.IsNew() 只改 g_H1 那格，改了 120 次，g_H4 的改了 30 次。" class="wp-image-1914" srcset="https://orzalanluo.com/wp-content/uploads/mt5-11-class-objects.png 1000w, https://orzalanluo.com/wp-content/uploads/mt5-11-class-objects-300x209.png 300w, https://orzalanluo.com/wp-content/uploads/mt5-11-class-objects-644x450.png 644w" sizes="(max-width: 1000px) 100vw, 1000px" /></a><figcaption class="wp-element-caption">一個類別建出兩個物件，成員各有一份（值是回測結束時的）</figcaption></figure>



<p class="wp-block-paragraph"><code class="" data-line="">Count()</code> 的兩個參數都加了 <code class="" data-line="">&amp;</code>（傳參考）：傳 <code class="" data-line="">g_StatH1</code> 進去，函數裡的 <code class="" data-line="">stat</code> 就是 <code class="" data-line="">g_StatH1</code> 本身，<code class="" data-line="">stat.count++</code> 改的就是它。物件、結構當參數只能傳參考，沒加 <code class="" data-line="">&amp;</code> 編譯報 objects are passed by reference only（error 293）。</p>



<p class="wp-block-paragraph">觸發的前幾行（只列 EA 印的，左邊是測試器時間）：</p>



<pre class="wp-block-prismatic-blocks"><code class="" data-line="">2025.01.06 00:05:00   EURUSD H1 新 K 線 2025.01.06 00:00  第 1 次
2025.01.06 00:05:00   EURUSD H4 新 K 線 2025.01.06 00:00  第 1 次
2025.01.06 01:00:00   EURUSD H1 新 K 線 2025.01.06 01:00  第 2 次
2025.01.06 02:00:00   EURUSD H1 新 K 線 2025.01.06 02:00  第 3 次
2025.01.06 03:00:00   EURUSD H1 新 K 線 2025.01.06 03:00  第 4 次
2025.01.06 04:00:00   EURUSD H1 新 K 線 2025.01.06 04:00  第 5 次
2025.01.06 04:00:00   EURUSD H4 新 K 線 2025.01.06 04:00  第 2 次</code></pre>



<p class="wp-block-paragraph">第一個 tick 在 00:05，印的是那根 K 線的開盤時間 00:00。之後 H1 每一根印一次；H4 只有開盤時間在 0、4、8、12、16、20 點的那幾根，跟 H1 同一個 tick、排在 H1 後面。結束時的總結：</p>



<pre class="wp-block-prismatic-blocks"><code class="" data-line="">OnDeinit
EURUSD H1 共 120 次，最後一根 2025.01.10 23:00
EURUSD H4 共 30 次，最後一根 2025.01.10 20:00</code></pre>



<p class="wp-block-paragraph">一週五天，H1 共 120 次，跟日誌裡測試器印的「120 bars generated」一樣；H4 30 次。</p>



<p class="wp-block-paragraph">本篇檔案：<a href="https://orzalanluo.com/wp-content/uploads/2026/09/NewBarDemo.mq5">NewBarDemo.mq5</a> 放 <code class="" data-line="">MQL5\Experts\</code>，在 MetaEditor 打開按 <code class="" data-line="">F7</code> 就能用。</p>

]]></content:encoded>
					
					<wfw:commentRss>https://orzalanluo.com/archives/1925/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>把 MQL5 重複的程式抽成 .mqh：#include、共用參數、類別框架，兩隻 EA 用同一套</title>
		<link>https://orzalanluo.com/archives/1898</link>
					<comments>https://orzalanluo.com/archives/1898#respond</comments>
		
		<dc:creator><![CDATA[alanluo]]></dc:creator>
		<pubDate>Tue, 22 Sep 2026 22:27:35 +0000</pubDate>
				<category><![CDATA[MQL5 基礎]]></category>
		<category><![CDATA[MT5 教學]]></category>
		<category><![CDATA[MQL5]]></category>
		<category><![CDATA[MT5]]></category>
		<guid isPermaLink="false">https://orzalanluo.com/archives/1898</guid>

					<description><![CDATA[一隻 EA 寫到能上線，檔案裡大半是跟策略無關的東西：自檢、算手數、下單重送、保本移動、事件檔、通知、日結。均 [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">一隻 EA 寫到能上線，檔案裡大半是跟策略無關的東西：自檢、算手數、下單重送、保本移動、事件檔、通知、日結。均線交叉 EA <a href="https://orzalanluo.com/wp-content/uploads/2026/09/MaCrossMonitor.mq5">MaCrossMonitor.mq5</a> 有 626 行，真正屬於「均線交叉」的只有建均線、讀均線、判斷交叉那幾段。要再寫一隻策略，就得整份複製，之後修一個 bug 要改好幾份。這篇把共用的部分抽成一個 <code class="" data-line="">.mqh</code>，EA 檔只留訊號。</p>



<p class="wp-block-paragraph">三個範例檔。<a href="https://orzalanluo.com/wp-content/uploads/2026/09/EaKit.mqh">EaKit.mqh</a> 是共用框架，622 行；<a href="https://orzalanluo.com/wp-content/uploads/2026/09/MaCrossKit.mq5">MaCrossKit.mq5</a> 是同一個均線交叉，剩 77 行；<a href="https://orzalanluo.com/wp-content/uploads/2026/09/ChannelKit.mq5">ChannelKit.mq5</a> 是第二隻策略：通道突破，81 行。兩隻 EA 都在策略測試器實跑，文中的日誌都是實際輸出。</p>



<figure class="wp-block-image size-full"><a href="https://orzalanluo.com/wp-content/uploads/mt5-10-kit-structure.png"><img decoding="async" width="1000" height="876" src="https://orzalanluo.com/wp-content/uploads/mt5-10-kit-structure.png" alt="結構圖：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 就能跑。" class="wp-image-1913" srcset="https://orzalanluo.com/wp-content/uploads/mt5-10-kit-structure.png 1000w, https://orzalanluo.com/wp-content/uploads/mt5-10-kit-structure-300x263.png 300w, https://orzalanluo.com/wp-content/uploads/mt5-10-kit-structure-644x564.png 644w" sizes="(max-width: 1000px) 100vw, 1000px" /></a><figcaption class="wp-element-caption">兩隻 EA 各留訊號與魔術碼，都引入同一個 EaKit.mqh</figcaption></figure>



<h2 class="wp-block-heading">先把範例跑起來</h2>



<ol class="wp-block-list">
<li>EaKit.mqh 放到 <code class="" data-line="">MQL5\Include\OrzBlog\</code>（OrzBlog 資料夾自己建）；兩隻 EA 放 <code class="" data-line="">MQL5\Experts\</code>，在 MetaEditor 各按一次 <code class="" data-line="">F7</code>。</li>



<li>策略測試器 <code class="" data-line="">Ctrl+R</code>：EURUSD，H1，2025.01.02 到 2025.01.31，模式「每次報價」，入金 10000，杠杆 1:100；EA 先選 MaCrossKit 跑一次，再換 ChannelKit 跑一次。</li>
</ol>



<h2 class="wp-block-heading">1. #include 怎麼找檔</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">#include &lt;OrzBlog\EaKit.mqh&gt;  // 到 MQL5\Include\ 找
#include &quot;EaKit.mqh&quot;           // 到這個 .mq5 的資料夾找</code></pre>



<p class="wp-block-paragraph">角括號到 <code class="" data-line="">MQL5\Include\</code> 找，引號到「寫這行 <code class="" data-line="">#include</code> 的檔」所在的資料夾找，兩種都可以帶子資料夾。<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">兩邊不互相退讓</mark>：引號寫 <code class="" data-line="">&quot;OrzBlog\EaKit.mqh&quot;</code>，EA 旁邊沒有就報 not found，不會再去 Include 找；角括號也不會找 EA 旁邊的檔。多隻 EA 共用的放 Include，只給一隻 EA 用的放它旁邊。</p>



<p class="wp-block-paragraph">編譯時 <code class="" data-line="">.mqh</code> 的內容整段貼進來，一起編成 <code class="" data-line="">.ex5</code>。所以 <mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">.ex5 不需要 .mqh 就能跑</mark>——我跑回測的那個終端，Include 底下根本沒有 OrzBlog。反過來，改了 <code class="" data-line="">.mqh</code> 之後，用到它的每隻 EA 都要重新按 <code class="" data-line="">F7</code>，新的 <code class="" data-line="">.ex5</code> 才有改過的內容。</p>



<p class="wp-block-paragraph">同一個檔被引入兩次很常見：MaCrossKit 引的 <code class="" data-line="">Trend.mqh</code> 和 <code class="" data-line="">Oscilators.mqh</code> 都各自引了 <code class="" data-line="">Indicator.mqh</code>。編譯器只收一次，實測連兩條不同路徑指到同一個檔也一樣，所以 C 語言那套 <code class="" data-line="">#ifndef</code> 守衛在 MQL5 不必寫。</p>



<h2 class="wp-block-heading">2. 共用的參數跟著 .mqh 走</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 2. 共用的參數跟著 .mqh 走 ======================================
#include &lt;Trade\Trade.mqh&gt;

input group &quot;共用：風險與出場&quot;
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 &quot;共用：通知&quot;
input bool   i_NotifyPush  = true;    // 推播到手機
input bool   i_NotifyMail  = false;   // 寄電子郵件
input bool   i_NotifyAlert = false;   // 本機跳視窗
input string i_WebhookUrl  = &quot;&quot;;      // 空白＝不送 WebRequest
input int    i_StaleMin    = 30;      // 幾分鐘沒報價就警告，0 不用</code></pre>



<p class="wp-block-paragraph"><code class="" data-line="">input</code> 寫在 <code class="" data-line="">.mqh</code> 裡也算數，每隻引入它的 EA 輸入頁都會有這些參數；<code class="" data-line="">input group</code> 在輸入頁加一行分組標題。順序照原始碼由上往下：<code class="" data-line="">#include</code> 寫在 EA 自己的 input 前面，共用的就排前面。測試器日誌開頭列的參數就是這個順序：</p>



<pre class="wp-block-prismatic-blocks"><code class="" data-line="">共用：風險與出場=
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</code></pre>



<p class="wp-block-paragraph">兩個限制。名稱會撞：EA 再宣告一次 <code class="" data-line="">i_RiskPct</code>，編譯報 variable already defined（error 125）。預設值每隻 EA 都一樣：想讓每隻不同的，就別放進 <code class="" data-line="">.mqh</code>。<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">魔術碼就是這種</mark>——同一個商品上掛的每隻 EA 必須不同（同一隻掛兩個週期也算兩隻），所以它留在 EA 自己的 input，啟動時再交給框架。MaCrossKit 沿用 MaCrossMonitor 的 20250801，因為它是來取代舊版的，兩隻不會同時掛。</p>



<h2 class="wp-block-heading">3. 用類別把狀態收起來</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 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 &amp;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 &amp;open);

public:
            CEaKit() : m_Magic(0), m_LastBarTime(0), m_Ready(false),
                       m_LastBad(&quot;?&quot;), 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 &amp;ticket);
   void     ShowStatus();
   void     OnSignal(int dir, double atr);     // 管倉＋照訊號進出
   void     OnTrade(const MqlTradeTransaction &amp;trans);
   void     OnTimer();
   void     Log(string msg);
   void     Notify(string msg);
};</code></pre>



<p class="wp-block-paragraph">為什麼不直接把函數搬過去？原本的函數讀 <code class="" data-line="">g_Trade</code>、<code class="" data-line="">g_LastBarTime</code>、<code class="" data-line="">i_Magic</code> 這些全域變數。搬進 <code class="" data-line="">.mqh</code> 之後，這些名字要由 EA 提供：EA 沒宣告 <code class="" data-line="">i_Magic</code>，編譯報 undeclared identifier（error 256），錯誤指在 <code class="" data-line="">.mqh</code> 裡；EA 宣告了就過，寫在 <code class="" data-line="">#include</code> 後面也過。<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">.mqh 偷偷依賴 EA 裡的名字</mark>，只看 <code class="" data-line="">.mqh</code> 本身看不出來。</p>



<p class="wp-block-paragraph">類別把這些狀態收成自己的成員（<code class="" data-line="">m_</code> 開頭），放在 <code class="" data-line="">private:</code>，類別外面碰不到；魔術碼由 <code class="" data-line="">Init(magic)</code> 傳進來。EA 只宣告一個 <code class="" data-line="">CEaKit g_Kit</code>，呼叫 <code class="" data-line="">public:</code> 底下那幾個方法。<code class="" data-line="">public:</code> 第一行 <code class="" data-line="">CEaKit() : m_Magic(0), …</code> 是建構子，物件建立時自動執行，冒號後面是給成員初值。</p>



<p class="wp-block-paragraph">方法的本體大多寫在類別外面，名字前加 <code class="" data-line="">CEaKit::</code>，表示這是 CEaKit 的方法。例如 <code class="" data-line="">IsNewBar</code>，原本讀寫全域的 <code class="" data-line="">g_LastBarTime</code>，現在是成員 <code class="" data-line="">m_LastBarTime</code>：</p>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">bool CEaKit::IsNewBar()
{
   datetime t = iTime(_Symbol, _Period, 0);
   if(t == m_LastBarTime)
      return false;
   m_LastBarTime = t;
   return true;
}</code></pre>



<p class="wp-block-paragraph"><code class="" data-line="">OnTick</code>、<code class="" data-line="">OnTimer</code>、<code class="" data-line="">OnTradeTransaction</code> 這些事件函數留在 EA，再轉給框架。<code class="" data-line="">.mqh</code> 裡如果直接寫了 <code class="" data-line="">OnTimer()</code>，EA 想寫自己的就會報 function already defined and has body（error 165）；框架的 <code class="" data-line="">OnTimer()</code> 是類別的方法，名字一樣也不衝突。</p>



<h2 class="wp-block-heading">4. 均線交叉：只剩訊號</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 4. 均線交叉：只剩訊號 ==========================================
#include &lt;Indicators\Trend.mqh&gt;
#include &lt;Indicators\Oscilators.mqh&gt;
#include &lt;OrzBlog\EaKit.mqh&gt;

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(&quot;指標建立失敗，錯誤碼 &quot;, 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 &amp;trans,
                        const MqlTradeRequest &amp;request,
                        const MqlTradeResult &amp;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(&quot;指標資料還沒準備好，錯誤碼 &quot;, GetLastError());
      g_Kit.ForgetBar();
      return;
   }

   int dir = -1;                                  // -1 = 沒訊號
   if(fast2 &lt;= slow2 &amp;&amp; fast1 &gt; slow1) dir = POSITION_TYPE_BUY;
   if(fast2 &gt;= slow2 &amp;&amp; fast1 &lt; slow1) dir = POSITION_TYPE_SELL;
   g_Kit.OnSignal(dir, atr1);
}</code></pre>



<p class="wp-block-paragraph"><code class="" data-line="">OnTick</code> 剩四件事：框架說可以動了沒、在圖上顯示持倉、是不是新 K 線、讀指標算訊號。訊號用 -1 表示沒有，<code class="" data-line="">POSITION_TYPE_BUY</code>／<code class="" data-line="">POSITION_TYPE_SELL</code> 表示方向，交給 <code class="" data-line="">OnSignal</code>：它先管手上的倉（保本、移動停損），同方向不動，反方向先平再開。</p>



<p class="wp-block-paragraph">抽完要證明行為沒變。同一天用同樣設定各跑一次 MaCrossMonitor 和 MaCrossKit：日誌裡帶時間的 376 行（EA 印的加上測試器自己的成交紀錄）只差事件檔檔名那一行，其餘逐字相同，包括 78 行開倉、保本、移動、平倉；報告的 28 筆成交也一樣，14 筆交易，總淨盈利 416.00。<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">「同一天」要講究</mark>：策略測試器用的是券商現在的隔夜利息，隔天重跑，損益可能差幾分錢。</p>



<h2 class="wp-block-heading">5. 第二隻 EA：通道突破</h2>



<p class="wp-block-paragraph">跟 MaCrossKit 一樣的樣板，用一行註解帶過：</p>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 5. 第二隻 EA：通道突破 =========================================
#include &lt;Indicators\TimeSeries.mqh&gt;
#include &lt;Indicators\Oscilators.mqh&gt;
#include &lt;OrzBlog\EaKit.mqh&gt;

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(&quot;指標建立失敗，錯誤碼 &quot;, 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(&quot;資料還沒準備好，錯誤碼 &quot;, GetLastError());
      g_Kit.ForgetBar();
      return;
   }

   int dir = -1;                        // 剛收在通道外才算突破
   if(close2 &lt;= top2 &amp;&amp; close1 &gt; top1) dir = POSITION_TYPE_BUY;
   if(close2 &gt;= bot2 &amp;&amp; close1 &lt; bot1) dir = POSITION_TYPE_SELL;
   g_Kit.OnSignal(dir, atr1);
}</code></pre>



<p class="wp-block-paragraph">收盤剛站上前 20 根 K 線（不含剛收的那根）的最高價就做多，剛跌破最低價就做空。<code class="" data-line="">CiHigh</code>／<code class="" data-line="">CiLow</code> 的 <code class="" data-line="">MaxValue</code>／<code class="" data-line="">MinValue</code> 從第 2 根起往回找 <code class="" data-line="">i_Channel</code> 根，第三個參數回傳找到的位置，這裡用不到。</p>



<p class="wp-block-paragraph">保本、移動停損、事件檔、出場通知、日結，這隻 EA 一行都沒寫，全在框架裡。魔術碼 20250802，跟均線交叉不同，兩隻掛在同一個商品上才分得清誰的倉。同一段 2025 年 1 月：19 筆交易，總淨盈利 41.55。前三筆：</p>



<pre class="wp-block-prismatic-blocks"><code class="" data-line="">開空 #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</code></pre>



<p class="wp-block-paragraph">本篇檔案：<a href="https://orzalanluo.com/wp-content/uploads/2026/09/EaKit.mqh">EaKit.mqh</a> 放 <code class="" data-line="">MQL5\Include\OrzBlog\</code>；<a href="https://orzalanluo.com/wp-content/uploads/2026/09/MaCrossKit.mq5">MaCrossKit.mq5</a>、<a href="https://orzalanluo.com/wp-content/uploads/2026/09/ChannelKit.mq5">ChannelKit.mq5</a> 放 <code class="" data-line="">MQL5\Experts\</code>。在 MetaEditor 打開 EA 按 <code class="" data-line="">F7</code> 就能用。</p>

]]></content:encoded>
					
					<wfw:commentRss>https://orzalanluo.com/archives/1898/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>MT5 EA 的日誌、通知與長期監測：事件檔、交易事件、推播與 WebRequest、定時器心跳</title>
		<link>https://orzalanluo.com/archives/1895</link>
					<comments>https://orzalanluo.com/archives/1895#respond</comments>
		
		<dc:creator><![CDATA[alanluo]]></dc:creator>
		<pubDate>Mon, 21 Sep 2026 23:09:37 +0000</pubDate>
				<category><![CDATA[MQL5 基礎]]></category>
		<category><![CDATA[MT5 教學]]></category>
		<category><![CDATA[MQL5]]></category>
		<category><![CDATA[MT5]]></category>
		<guid isPermaLink="false">https://orzalanluo.com/?p=1895</guid>

					<description><![CDATA[EA 掛上去之後，你不會一直盯著它。要知道三件事：它做了什麼、發生事情的時候有沒有人知道、隔一陣子還能不能確認 [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">EA 掛上去之後，你不會一直盯著它。要知道三件事：它做了什麼、發生事情的時候有沒有人知道、隔一陣子還能不能確認它活著。這篇用四個機制回答：自己留一份事件檔、用交易事件抓到伺服器平掉的倉、一個通知入口接 Alert／推播／郵件／Webhook、一個定時器每天結一次帳。</p>



<p class="wp-block-paragraph">兩個範例檔。<a href="https://orzalanluo.com/wp-content/uploads/2026/09/MaCrossMonitor.mq5">MaCrossMonitor.mq5</a> 是在均線交叉 EA <a href="https://orzalanluo.com/wp-content/uploads/2026/09/MaCrossLive.mq5">MaCrossLive.mq5</a>（EMA 10／30 交叉進場、每筆賠餘額 1%、ATR 倍數停損停利、保本與移動停損、上線自檢與接手持倉）上面加這四件事；策略邏輯一行沒動，魔術碼也沿用 20250801：這隻是拿來取代 MaCrossLive 的，兩隻別在同一個帳戶、同一個商品上同時跑，會把對方的倉當成自己的。<a href="https://orzalanluo.com/wp-content/uploads/2026/09/NotifyDemo.mq5">NotifyDemo.mq5</a> 是腳本，在連著伺服器的終端上把四個通知函數都叫一遍，印出回傳值和錯誤碼。EA 在策略測試器跑、腳本在真的終端跑，文中的日誌都是實際輸出。</p>



<h2 class="wp-block-heading">先把範例跑起來</h2>



<ol class="wp-block-list">
<li>MaCrossMonitor.mq5 放到 <code class="" data-line="">MQL5\Experts\</code>，在 MetaEditor 按 <code class="" data-line="">F7</code>；策略測試器 <code class="" data-line="">Ctrl+R</code>：EA 選 MaCrossMonitor，EURUSD，H1，2025.01.02 到 2025.01.31，模式「每次報價」，入金 10000，杠杆 1:100，按「開始」。</li>



<li>「日誌」頁會多出以 <code class="" data-line="">[通知]</code> 開頭的行。另外在 <code class="" data-line="">C:\Users\&lt;使用者&gt;\AppData\Roaming\MetaQuotes\Terminal\Common\Files\</code> 會多一個 <code class="" data-line="">MaCrossMonitor_EURUSD_tester.log</code>，用記事本打得開。</li>



<li>NotifyDemo.mq5 放到 <code class="" data-line="">MQL5\Scripts\</code>，按 <code class="" data-line="">F7</code>；回終端從「導航」拖到任何一張圖表上，輸出在「工具箱」的「專家」頁。它會真的送通知，第 3 節的設定開了哪幾個，就會收到哪幾個。</li>
</ol>



<h2 class="wp-block-heading">1. 日誌去了哪裡，自己再留一份</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 1. 日誌去了哪裡，自己再留一份 ==================================
void Log(string msg)
{
   Print(msg);                            // 專家頁 + MQL5\Logs
   int h = FileOpen(g_LogName, FILE_READ|FILE_WRITE|FILE_CSV|
                    FILE_ANSI|FILE_COMMON, &#039;\t&#039;, CP_UTF8);
   if(h == INVALID_HANDLE)
   {
      Print(&quot;事件檔打不開，錯誤碼 &quot;, GetLastError());
      return;
   }
   FileSeek(h, 0, SEEK_END);              // 接在檔尾
   string when = TimeToString(TimeTradeServer(),
                              TIME_DATE|TIME_SECONDS);
   FileWrite(h, when, _Symbol, msg);      // 三欄，Tab 隔開
   FileClose(h);
}</code></pre>



<p class="wp-block-paragraph"><code class="" data-line="">Print()</code> 的字出現在「工具箱」的「專家」頁，同時寫進 <code class="" data-line="">MQL5\Logs\</code> 底下以日期命名的檔，一天一個。終端自己的訊息——連線、登入、被伺服器拒的單——在「日誌」頁和 <code class="" data-line="">logs\</code> 資料夾，兩邊要分開看。策略測試器裡 EA 印的東西在測試器的「日誌」頁，檔案在 <code class="" data-line="">Tester\logs\</code>。</p>



<p class="wp-block-paragraph">這些檔有一個問題：<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">同一個終端所有 EA 的字混在一起</mark>。所以重要的事自己再留一份：一個商品一個檔，只寫事件——出場、日結、警告——不寫每個 tick、不寫每根 K 線。</p>



<p class="wp-block-paragraph"><code class="" data-line="">FileOpen</code> 的旗標是幾個選項用 <code class="" data-line="">|</code> 合在一起、一次傳進去。<code class="" data-line="">FILE_COMMON</code> 寫到所有終端共用的資料夾，路徑是 <code class="" data-line="">TerminalInfoString(TERMINAL_COMMONDATA_PATH)</code>，我這台是 <code class="" data-line="">C:\Users\&lt;使用者&gt;\AppData\Roaming\MetaQuotes\Terminal\Common</code>；<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">策略測試器也寫到同一個地方</mark>，不加的話會寫進測試器代理（真正跑回測的那個程式）自己的資料夾，要去 Tester 底下挖。</p>



<p class="wp-block-paragraph">其他旗標決定檔案長什麼樣。<code class="" data-line="">FILE_READ|FILE_WRITE</code> 一起給才會保留原內容，再 <code class="" data-line="">FileSeek</code> 到檔尾接著寫；<code class="" data-line="">FILE_CSV</code> 配 <code class="" data-line="">&#039;\t&#039;</code>，<code class="" data-line="">FileWrite</code> 的三個參數就用 Tab 隔開、自動換行；<code class="" data-line="">FILE_ANSI</code> 加 <code class="" data-line="">CP_UTF8</code> 存成 UTF-8，記事本打得開。</p>



<p class="wp-block-paragraph">每寫一行就開檔關檔，EA 被強制關掉也不會掉最後幾行。測試器每跑一趟都會往同一個檔接著寫，所以 <code class="" data-line="">OnInit</code> 在測試器裡另起檔名、先 <code class="" data-line="">FileDelete</code>；定時器也在這裡開、在 <code class="" data-line="">OnDeinit</code> 關，第 4 節用：</p>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">   // 事件檔：一個商品一個檔；測試器另起檔名，每趟從頭寫
   g_LogName = &quot;MaCrossMonitor_&quot; + _Symbol +
               (MQLInfoInteger(MQL_TESTER) ? &quot;_tester&quot; : &quot;&quot;) + &quot;.log&quot;;
   if(MQLInfoInteger(MQL_TESTER))
      FileDelete(g_LogName, FILE_COMMON);
   Print(&quot;Common=&quot;, TerminalInfoString(TERMINAL_COMMONDATA_PATH));
   Log(&quot;事件檔 Common\\Files\\&quot; + g_LogName);
   EventSetTimer(60);                     // 每分鐘進一次 OnTimer
   ...
}

void OnDeinit(const int reason)
{
   EventKillTimer();
   Comment(&quot;&quot;);                  // 指標物件不用釋放
}</code></pre>



<p class="wp-block-paragraph">跑完打開事件檔，每行三欄：時間、商品、訊息。節錄出場那幾行（Tab 這裡換成空格）：</p>



<pre class="wp-block-prismatic-blocks"><code class="" data-line="">2025.01.17 15:26:28  EURUSD  [通知] 出場 #14 停損 0.61 手  損益 -70.15
2025.01.20 06:03:04  EURUSD  [通知] 出場 #16 停損 0.33 手  損益 -67.90
2025.01.20 11:38:35  EURUSD  [通知] 出場 #18 停損 0.39 手  損益 1.56</code></pre>



<h2 class="wp-block-heading">2. 出場是伺服器做的，靠交易事件才知道</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 2. 出場是伺服器做的，靠交易事件才知道 ==========================
void OnTradeTransaction(const MqlTradeTransaction &amp;trans,
                        const MqlTradeRequest &amp;request,
                        const MqlTradeResult &amp;result)
{
   if(trans.type != TRADE_TRANSACTION_DEAL_ADD)
      return;                             // 只看「多了一筆成交」
   ulong deal = trans.deal;
   if(!HistoryDealSelect(deal))           // 先把這筆成交載進來
   {
      Print(&quot;成交 #&quot;, deal, &quot; 載不進來，錯誤碼 &quot;, GetLastError());
      return;
   }
   if(HistoryDealGetInteger(deal, DEAL_MAGIC) != i_Magic ||
      HistoryDealGetString(deal, DEAL_SYMBOL) != _Symbol)
      return;                             // 不是這隻 EA 的
   if(HistoryDealGetInteger(deal, DEAL_ENTRY) == DEAL_ENTRY_IN)
      return;                             // 進場那筆開倉時已經印過
   long   reason = HistoryDealGetInteger(deal, DEAL_REASON);
   string why    = &quot;其他&quot;;
   if(reason &lt;= DEAL_REASON_WEB)    why = &quot;手動&quot;;   // 桌面/手機/網頁
   if(reason == DEAL_REASON_SL)     why = &quot;停損&quot;;
   if(reason == DEAL_REASON_TP)     why = &quot;停利&quot;;
   if(reason == DEAL_REASON_EXPERT) why = &quot;EA 平倉&quot;;
   if(reason == DEAL_REASON_SO)     why = &quot;強制平倉&quot;;
   long pos = HistoryDealGetInteger(deal, DEAL_POSITION_ID);
   Notify(&quot;出場 #&quot; + IntegerToString(pos) + &quot; &quot; + why + &quot; &quot; +
          DoubleToString(HistoryDealGetDouble(deal, DEAL_VOLUME), 2) +
          &quot; 手  損益 &quot; + DoubleToString(PositionNet(pos), 2));
}

double PositionNet(long pos)              // 這筆部位所有成交的淨損益
{
   double net = 0;
   if(!HistorySelectByPosition(pos))
      return 0;
   for(int i = HistoryDealsTotal() - 1; i &gt;= 0; i--)
   {
      ulong d = HistoryDealGetTicket(i);
      net += HistoryDealGetDouble(d, DEAL_PROFIT) +
             HistoryDealGetDouble(d, DEAL_SWAP) +
             HistoryDealGetDouble(d, DEAL_COMMISSION);
   }
   return net;
}</code></pre>



<p class="wp-block-paragraph">停損停利是伺服器在盯，價格碰到就平，EA 只知道自己送出去的單。<code class="" data-line="">OnTradeTransaction</code> 每有一筆交易變動就被叫一次，這裡只看 <code class="" data-line="">TRADE_TRANSACTION_DEAL_ADD</code>——多了一筆成交。<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">先 HistoryDealSelect 把那筆成交載進來，才讀得到它的屬性</mark>。</p>



<p class="wp-block-paragraph">接著三個屬性：魔術碼和商品要對，別把別隻 EA 的倉算進來；<code class="" data-line="">DEAL_ENTRY</code> 分進場和出場，進場那筆開倉時已經印過；<code class="" data-line="">DEAL_REASON</code> 說是誰平的：伺服器的停損、停利、EA 自己送的、保證金不夠被強制平倉，桌面、手機、網頁手動平的是另外三個值，其餘一律印「其他」。</p>



<p class="wp-block-paragraph">損益不要只看那筆出場成交。手續費進場、出場各扣一次，隔夜利息掛在出場那筆。<code class="" data-line="">HistorySelectByPosition</code> 把同一個部位的所有成交撈出來加總，才會跟報告的每筆淨利一樣。</p>



<p class="wp-block-paragraph">2025 年 1 月這趟 13 個出場：5 筆停利、6 筆停損、2 筆 EA 平倉（反向訊號）。<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">「停損」不一定是賠錢</mark>——#8 保本之後才碰到停損，損益 +1.44。報告寫 14 筆，多的那筆是測試結束時強制平倉的，那一筆沒有事件。</p>



<pre class="wp-block-prismatic-blocks"><code class="" data-line="">2025.01.06 10:56:27   [通知] 出場 #2 停利 0.34 手  損益 144.84
2025.01.08 13:30:40   [通知] 出場 #4 停利 0.26 手  損益 151.12
2025.01.14 21:37:05   [通知] 出場 #6 停利 0.27 手  損益 152.28
2025.01.16 17:05:37   [通知] 出場 #8 停損 0.36 手  損益 1.44
2025.01.17 10:00:00   [通知] 出場 #10 EA 平倉 0.31 手  損益 -40.86</code></pre>



<h2 class="wp-block-heading">3. 通知：一個入口，四條路</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 3. 通知：一個入口，四條路 ======================================
void Notify(string msg)
{
   Log(&quot;[通知] &quot; + msg);                  // 先留紀錄，再往外送
   if(MQLInfoInteger(MQL_TESTER))
      return;                             // 測試器裡四個函數都不會動
   string text = _Symbol + &quot; &quot; + msg;
   if(i_NotifyAlert)
      Alert(text);
   ResetLastError();                      // 每條路各看自己的錯誤碼
   if(i_NotifyPush &amp;&amp; !SendNotification(text))
      Print(&quot;推播失敗，錯誤碼 &quot;, GetLastError());
   ResetLastError();
   if(i_NotifyMail &amp;&amp; !SendMail(&quot;EA &quot; + _Symbol, text))
      Print(&quot;寄信失敗，錯誤碼 &quot;, GetLastError());
   if(i_WebhookUrl == &quot;&quot;)
      return;
   char   body[], reply[];
   string replyHdr;
   int n = StringToCharArray(text, body, 0, WHOLE_ARRAY, CP_UTF8);
   ArrayResize(body, n - 1);              // 去掉結尾的 0
   ResetLastError();
   int code = WebRequest(&quot;POST&quot;, i_WebhookUrl,
                         &quot;Content-Type: text/plain; charset=utf-8&quot;,
                         5000, body, reply, replyHdr);
   if(code != 200)
      Print(&quot;Webhook 回 &quot;, code, &quot;，錯誤碼 &quot;, GetLastError());
}</code></pre>



<p class="wp-block-paragraph">除了 <code class="" data-line="">Alert()</code>，另外三個都要先在「工具 › 選項」設定；四個<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">在策略測試器裡全部不執行</mark>，所以 <code class="" data-line="">Notify()</code> 先寫進事件檔、再看 <code class="" data-line="">MQL_TESTER</code>，測試器裡看得到「這時候會通知」，又不會真的送。四條路各一個 input 開關，預設只開推播；<code class="" data-line="">i_WebhookUrl</code> 空白就不送。</p>



<ul class="wp-block-list">
<li><code class="" data-line="">Alert()</code>：不用設定。本機跳視窗加聲音，「專家」頁也留一行；只有坐在電腦前才看得到。</li>



<li><code class="" data-line="">SendNotification()</code>：「通知」頁勾「啟用推送通知」、填手機 App 的 MetaQuotes ID。一則 255 字，每秒 2 則、每分鐘 10 則。</li>



<li><code class="" data-line="">SendMail()</code>：「郵件」頁勾「啟用郵件通知」，填 SMTP 服務器、SMTP登錄名、SMTP 密碼、從、到。回 true 只是排進佇列。</li>



<li><code class="" data-line="">WebRequest()</code>：「EA交易」頁勾「允許WebRequest 用於所列的URL」，把網址加進那個清單（下面叫它白名單）。送出去會卡住 EA 直到回應或逾時，這裡設 5 秒；在交易事件裡卡太久，排隊中的事件會被新來的擠掉。</li>
</ul>



<p class="wp-block-paragraph">腳本在我這台跑的結果。推播和郵件沒設；白名單只加了 <code class="" data-line="">http://127.0.0.1</code>，本機 port 80 掛一支只會印出收到什麼、回 ok 的小伺服器；第二個網址 192.0.2.1 故意不列進白名單：</p>



<pre class="wp-block-prismatic-blocks"><code class="" data-line="">SendNotification false  錯誤碼 4517
SendMail false  錯誤碼 4510
WebRequest http://127.0.0.1/hook -&gt; 200  錯誤碼 0  回應 ok
WebRequest http://192.0.2.1/hook -&gt; -1  錯誤碼 4006  回應 
Alert: EA 測試通知 2026.09.22 06:53</code></pre>



<p class="wp-block-paragraph">4517 是推播沒設定，4510 是郵件送不出去（終端的「日誌」頁同時印一行 invalid settings）。<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">WebRequest 的網址沒列進白名單時回 -1、錯誤碼 4006</mark>——文件沒寫這種情況會回哪個號碼，這個號碼的官方名稱是 ERR_INVALID_ARRAY，跟情境對不上，別拿錯誤碼的名稱猜原因。列了的那個網址回 HTTP 200，小伺服器收到的是：</p>



<pre class="wp-block-prismatic-blocks"><code class="" data-line="">POST /hook text/plain; charset=utf-8 EA 測試通知 2026.09.22 06:53</code></pre>



<p class="wp-block-paragraph">送 POST 那幾行：文字用 <code class="" data-line="">StringToCharArray</code> 轉成 UTF-8 的位元組，回傳的長度含結尾的 0，要去掉一個。換成 Telegram 或 LINE 的 bot，就是網址、header 和 body 格式照它們的文件改，白名單也要加它們的網域。</p>



<h2 class="wp-block-heading">4. 定時器：每天結一次帳、報價停了要說</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 4. 定時器：每天結一次帳、報價停了要說 ==========================
void OnTimer()
{
   datetime now   = TimeTradeServer();    // 沒報價時 TimeCurrent 停住
   datetime today = now - now % 86400;    // 今天 00:00（伺服器時間）
   if(g_DayStart == 0)
      g_DayStart = today;
   if(today &gt; g_DayStart)                 // 過了午夜：結上一天的帳
   {
      DailySummary(g_DayStart, today);
      g_DayStart = today;
   }
   CheckStale(now);
}

void DailySummary(datetime from, datetime to)
{
   int    n   = 0;
   double net = 0;
   if(HistorySelect(from, to - 1))        // to 那一秒算下一天
      for(int i = HistoryDealsTotal() - 1; i &gt;= 0; i--)
      {
         ulong d = HistoryDealGetTicket(i);
         if(HistoryDealGetInteger(d, DEAL_MAGIC) != i_Magic ||
            HistoryDealGetString(d, DEAL_SYMBOL) != _Symbol)
            continue;
         net += HistoryDealGetDouble(d, DEAL_PROFIT) +
                HistoryDealGetDouble(d, DEAL_SWAP) +
                HistoryDealGetDouble(d, DEAL_COMMISSION);
         if(HistoryDealGetInteger(d, DEAL_ENTRY) != DEAL_ENTRY_IN)
            n++;                          // 只數出場
      }
   Notify(&quot;日結 &quot; + TimeToString(from, TIME_DATE) + &quot; &quot; +
          IntegerToString(n) + &quot; 筆 損益 &quot; + DoubleToString(net, 2) +
          &quot; 餘額 &quot; + DoubleToString(
          AccountInfoDouble(ACCOUNT_BALANCE), 2) + &quot; 權益 &quot; +
          DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY), 2));
}</code></pre>



<p class="wp-block-paragraph"><code class="" data-line="">EventSetTimer(60)</code> 之後 <code class="" data-line="">OnTimer</code> 每分鐘進來一次，<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">週末沒報價也照跑</mark>，測試器裡也一樣。所以現在幾點要拿 <code class="" data-line="">TimeTradeServer()</code>，它是終端自己算的、沒報價也會走；<code class="" data-line="">TimeCurrent()</code> 是最後一個報價的時間，斷線就停在那裡。文件說測試器裡兩者相等，實測（build 6182）週末只有前者在走。</p>



<p class="wp-block-paragraph"><code class="" data-line="">today</code> 那行：<code class="" data-line="">datetime</code> 是 1970 年起算的秒數，86400 是一天的秒數，<code class="" data-line="">now % 86400</code> 取餘數＝今天已經過了幾秒，<code class="" data-line="">now</code> 減掉它就是今天 00:00。日結：過了午夜就用 <code class="" data-line="">HistorySelect</code> 把前一天的成交撈出來，算出場筆數和淨損益，連餘額、權益一起送。</p>



<p class="wp-block-paragraph">損益是那天所有成交的合計，跟出場通知的整個部位損益是兩個口徑，所以 1 月 7 日開倉那天會看到「0 筆 損益 -0.78」，那是進場那筆的手續費。<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">這一則每天都會來，包括週末</mark>：它不只是報帳，是「EA 還活著」的心跳，哪天沒收到，就該去看終端了。EA 重啟跨過午夜的那天不會補結，狀態只記在記憶體裡。</p>



<pre class="wp-block-prismatic-blocks"><code class="" data-line="">[通知] 日結 2025.01.15 0 筆 損益 0.00 餘額 10448.24 權益 10448.24
[通知] 日結 2025.01.16 1 筆 損益 0.51 餘額 10448.75 權益 10463.94
[通知] 日結 2025.01.17 3 筆 損益 -194.27 餘額 10254.48 權益 10310.58
[通知] 日結 2025.01.18 0 筆 損益 0.00 餘額 10254.48 權益 10310.58
[通知] 日結 2025.01.19 0 筆 損益 0.00 餘額 10254.48 權益 10310.58</code></pre>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">void CheckStale(datetime now)
{
   datetime open;
   if(i_StaleMin &lt;= 0 || !InSession(now, open))
      return;                             // 休市時間不算
   datetime last = (datetime)SymbolInfoInteger(_Symbol, SYMBOL_TIME);
   if(last &lt; open)
      last = open;                        // 開盤之後才起算
   int idle = (int)(now - last) / 60;
   if(idle &gt;= i_StaleMin &amp;&amp; !g_StaleSaid)
   {
      g_StaleSaid = true;
      Notify(&quot;報價停了 &quot; + IntegerToString(idle) + &quot; 分鐘&quot;);
   }
   if(idle &lt; i_StaleMin &amp;&amp; g_StaleSaid)
   {
      g_StaleSaid = false;
      Log(&quot;報價恢復&quot;);
   }
}</code></pre>



<p class="wp-block-paragraph">報價停了：在交易時段內、離上一個報價滿 <code class="" data-line="">i_StaleMin</code> 分鐘才警告，恢復再印一行。看的是 <code class="" data-line="">SYMBOL_TIME</code>——這個商品自己的最後報價，不用 <code class="" data-line="">TimeCurrent()</code>，別的商品有報價它也會走。週一從開盤時間起算，不然會把整個週末算進去。</p>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">bool InSession(datetime now, datetime &amp;open) // 現在在交易時段內？
{
   MqlDateTime dt;
   TimeToStruct(now, dt);
   datetime day = now - now % 86400;
   datetime from, to;
   for(int i = 0; SymbolInfoSessionTrade(_Symbol,
       (ENUM_DAY_OF_WEEK)dt.day_of_week, i, from, to); i++)
   {
      from += day;                        // 時段是「幾點」，加上今天
      to   += day;
      if(now &gt;= from &amp;&amp; now &lt; to)
      {
         open = from;
         return true;
      }
   }
   return false;
}</code></pre>



<p class="wp-block-paragraph"><code class="" data-line="">SymbolInfoSessionTrade</code> 查這個商品今天的交易時段，拿到的是「幾點」，加上今天 00:00 才能跟現在比；EURUSD 週六、週日沒有時段，就不算。查的是交易時段，商品沒有時段資料就永遠不會叫。</p>



<p class="wp-block-paragraph">把門檻改成 1 分鐘、跑跨週末的 1 月 3 日到 7 日：週六、週日一次都沒叫；週一 00:02 開盤，00:05 才有第一個報價，00:03 警告、00:05 恢復。預設 30 分鐘。</p>



<pre class="wp-block-prismatic-blocks"><code class="" data-line="">2025.01.06 00:03:00   [通知] 報價停了 1 分鐘
2025.01.06 00:05:00   報價恢復</code></pre>



<h2 class="wp-block-heading">執行結果</h2>



<p class="wp-block-paragraph">MaCrossMonitor 跑 2025 年 1 月，EA 自己印的 78 行開倉、保本、移動、平倉跟 MaCrossLive 逐字相同：14 筆交易、總淨盈利 416.21。多出來的是 41 行 <code class="" data-line="">[通知]</code>：13 個出場、28 個日結。日誌開頭：</p>



<pre class="wp-block-prismatic-blocks"><code class="" data-line="">EA 啟動：保本 1.0 ATR +10 點，移動停損 2.0 ATR，停利 3.0 ATR
Common=C:\Users\&lt;使用者&gt;\AppData\Roaming\MetaQuotes\Terminal\Common
事件檔 Common\Files\MaCrossMonitor_EURUSD_tester.log
自檢通過：EURUSD  鎖倉帳戶  餘額 10000.00
沒有要接手的持倉</code></pre>



<h2 class="wp-block-heading">上線之後看什麼</h2>



<ul class="wp-block-list">
<li>每天等那一則日結。沒來，先看終端還開著沒、連線有沒有掉。</li>



<li>出場通知看原因：一路「停損」是行情比停損距離大，一路「EA 平倉」是訊號來回翻。</li>



<li>要回頭看一個月做了什麼，開事件檔，不用在專家頁裡翻。</li>



<li>「專家」頁還是要看：下單失敗、自檢沒過、改停損被拒，這些只在那裡。</li>
</ul>



<p class="wp-block-paragraph">本篇檔案：<a href="https://orzalanluo.com/wp-content/uploads/2026/09/MaCrossMonitor.mq5">MaCrossMonitor.mq5</a> 是 EA，放 <code class="" data-line="">MQL5\Experts\</code>；<a href="https://orzalanluo.com/wp-content/uploads/2026/09/NotifyDemo.mq5">NotifyDemo.mq5</a> 是腳本，放 <code class="" data-line="">MQL5\Scripts\</code>。在 MetaEditor 打開按 <code class="" data-line="">F7</code> 就能用。</p>

]]></content:encoded>
					
					<wfw:commentRss>https://orzalanluo.com/archives/1895/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
