<?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>alanluo &#8211; 冏掉的Alan羅</title>
	<atom:link href="https://orzalanluo.com/archives/author/alanluo/feed" rel="self" type="application/rss+xml" />
	<link>https://orzalanluo.com</link>
	<description>我什麼都不會，所以我在學習。</description>
	<lastBuildDate>Sat, 24 Oct 2026 03:15:29 +0000</lastBuildDate>
	<language>zh-TW</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.1.3</generator>
	<item>
		<title>MT5 策略驗證：訓練、樣本外、鎖定三段，把優化冠軍拿去考沒看過的年份</title>
		<link>https://orzalanluo.com/archives/1935</link>
					<comments>https://orzalanluo.com/archives/1935#respond</comments>
		
		<dc:creator><![CDATA[alanluo]]></dc:creator>
		<pubDate>Sat, 24 Oct 2026 03:15:29 +0000</pubDate>
				<category><![CDATA[MT5 教學]]></category>
		<category><![CDATA[策略驗證]]></category>
		<category><![CDATA[MQL5]]></category>
		<category><![CDATA[MT5]]></category>
		<guid isPermaLink="false">https://orzalanluo.com/?p=1935</guid>

					<description><![CDATA[策略測試器的優化，會依你選的標準（例如最後餘額）從一堆參數裡排出第一名。問題是它排名時已經看過答案：同一段資料 [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">策略測試器的優化，會依你選的標準（例如最後餘額）從一堆參數裡排出第一名。問題是它排名時已經看過答案：同一段資料拿來挑參數又拿來打分數，成績容易偏好看。這篇把歷史切成三段，用一段挑參數，另外兩段考試，看冠軍在沒看過的年份還站不站得住。</p>



<p class="wp-block-paragraph">範例沿用均線交叉下單的 <a href="https://orzalanluo.com/wp-content/uploads/2026/09/MaCrossTrade.mq5">MaCrossTrade.mq5</a>（快慢 EMA 交叉進場、ATR 倍數停損停利、固定 0.10 手），程式一行沒改。商品 EURUSD、H1，報價模式「1分鐘OHLC」。先講結論：<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">訓練段的冠軍，在樣本外和鎖定段兩段都賠錢</mark>。</p>



<h2 class="wp-block-heading">1. 為什麼要切三段</h2>



<ul class="wp-block-list">
<li><strong>訓練段</strong>：拿來優化、挑參數。</li>



<li><strong>樣本外</strong>：冠軍參數凍結後第一次考試。</li>



<li><strong>鎖定段</strong>：最後一關，只考一次。樣本外考完，人總會想回頭調參數；鎖定段完全不碰，留到最後才用，調過的版本也只能在這裡考一次。</li>
</ul>



<p class="wp-block-paragraph">MT5 測試器設置頁的前向測試只切一刀（一段優化、一段前向），前向結果就列在優化結果旁邊，看完很容易回頭改參數。三段切法多留一段完全不碰的資料，這是兩者最大的差別。</p>



<p class="wp-block-paragraph">測試器也沒有自動「滾動」的功能（每隔一段重新優化、往前推）。這篇只做一次切分，嚴格說是單次前推，不是滾動式的 Walk-Forward。</p>



<h2 class="wp-block-heading">2. 開跑前先把規則寫死</h2>



<p class="wp-block-paragraph">挑參數的方法如果是看完結果才決定的，三段切得再漂亮也沒用。所以開跑前先寫一份登記，存進版本控制，之後不改：</p>



<ul class="wp-block-list">
<li>網格：快線 5～20、慢線 20～60、停損 1～3 倍 ATR、停利 2～4 倍 ATR，共 180 組，完整算法、優化標準「最大本日餘額」。</li>



<li>選冠軍：交易筆數至少 100、獲利因子大於 1 的組合裡，淨利最高的那組。</li>



<li>通過條件：樣本外和鎖定段的獲利因子都要大於 1.2、各至少 30 筆，而且樣本外的獲利因子不能低於訓練段的 6 成。</li>
</ul>



<p class="wp-block-paragraph">三段的日期這樣切：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>段</th><th>期間</th><th>用途</th></tr></thead><tbody><tr><td>訓練</td><td>2019-01～2021-12</td><td>挑參數</td></tr><tr><td>樣本外</td><td>2022-01～2023-12</td><td>第一次考試</td></tr><tr><td>跳過</td><td>2024-01～2025-06</td><td>已經看過</td></tr><tr><td>鎖定</td><td>2025-07～2026-09</td><td>只考一次</td></tr></tbody></table></figure>



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



<figure class="wp-block-image size-full"><a href="https://orzalanluo.com/wp-content/uploads/mt5-14-wf-segments.png"><img fetchpriority="high" decoding="async" width="1000" height="576" src="https://orzalanluo.com/wp-content/uploads/mt5-14-wf-segments.png" alt="時間軸：訓練 2019.01～2021.12、樣本外 2022.01～2023.12、跳過 2024.01～2025.06、鎖定 2025.07～2026.09，冠軍獲利因子依序 1.12、0.90、不測、0.83" class="wp-image-1934" srcset="https://orzalanluo.com/wp-content/uploads/mt5-14-wf-segments.png 1000w, https://orzalanluo.com/wp-content/uploads/mt5-14-wf-segments-300x173.png 300w, https://orzalanluo.com/wp-content/uploads/mt5-14-wf-segments-644x371.png 644w" sizes="(max-width: 1000px) 100vw, 1000px" /></a><figcaption class="wp-element-caption">色塊長度按月數；下方是冠軍在各段的獲利因子</figcaption></figure>



<p class="wp-block-paragraph">訓練段從 2019 年開始，是因為我這家券商 2018 年的歷史資料不夠細。同一隻 EA 拿 2018 年整年來跑，報告只有 258 根 H1 K 線、1032 個報價（每根 4 個），「質量歷史」0%；樣本外兩年則有 12431 根。一年的 H1 只剩兩百多根，幾乎是一天一根，H1 策略用這種資料回測沒有參考價值。</p>



<p class="wp-block-paragraph">跳過的那一年半，是之前用同一隻 EA、同一組網格優化過的期間。看過的資料不能再當作沒看過，所以鎖定段改用之後的 15 個月。另外，測試器的結束日期是當天 0 點，不含那一天：樣本外實際測到 2023-12-30，鎖定段測到 2026-09-29。</p>



<h2 class="wp-block-heading">3. 訓練段：180 組選出冠軍</h2>



<p class="wp-block-paragraph">訓練段 180 組裡，快線等於慢線的 9 組一筆都不會交易；賺錢的有 41 組。照登記的規則，冠軍是快線 20、慢線 40、停損 3 倍 ATR、停利 4 倍 ATR：三年 353 筆、淨利 +723.33、獲利因子 1.12。這組剛好也是 180 組裡淨利最高的，合格條件沒有排掉更好的組合。</p>



<p class="wp-block-paragraph">執行時出了一個小差錯：登記要求冠軍先存進版本控制再考樣本外，結果那次存檔的指令失敗了，樣本外先跑了。冠軍是程式照規則從訓練結果選出來的，選出的時間早於樣本外開跑，參數沒有因此改動；補存之後，後面每一步都先確認存檔成功才往下做。</p>



<h2 class="wp-block-heading">4. 考試：樣本外與鎖定段</h2>



<p class="wp-block-paragraph">冠軍參數凍結後，各段都從入金 10,000、空倉重新開始，各跑一趟：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>段</th><th>筆數</th><th>淨利／獲利因子</th></tr></thead><tbody><tr><td>訓練</td><td>353</td><td>+723.33／1.12</td></tr><tr><td>樣本外</td><td>296</td><td>−667.43／0.90</td></tr><tr><td>鎖定</td><td>155</td><td>−468.63／0.83</td></tr></tbody></table></figure>



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



<p class="wp-block-paragraph">兩段考試都賠錢，獲利因子 0.90 和 0.83 都在 1 以下，離 1.2 的門檻很遠：<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">判定不通過</mark>。筆數（296、155 筆）和「質量歷史」（99%、100%）都達到登記的門檻。</p>



<p class="wp-block-paragraph">淨利已經扣掉點差、手續費和隔夜利息。把樣本外的成交表逐筆加起來：手續費 −177.60、隔夜利息 −39.23，扣這兩項之前的成交損益是 −450.60；鎖定段是 −93.00、−10.93、−364.70。點差已經算在成交價裡，拆不出來；但不計手續費和隔夜利息，兩段也一樣賠錢。</p>



<p class="wp-block-paragraph">樣本外已經不過，照理可以停在這裡；鎖定段還是照登記跑了，判定用的單趟只跑這一次，結果一樣是賠錢。</p>



<h2 class="wp-block-heading">5. 換個角度看：冠軍其實排在哪</h2>



<p class="wp-block-paragraph">判定寫下之後，才拿其他資料做對照，這些都不影響結論：</p>



<ul class="wp-block-list">
<li><strong>樣本外整個網格</strong>：同樣 180 組在 2022～2023 年只有 8 組賺錢（另有 9 組沒交易），訓練冠軍排第 78 名。訓練段賺錢的 41 組裡，到樣本外還賺錢的剩 6 組；樣本外至少 30 筆的組合，獲利因子最高 1.13，沒有一組過 1.2。</li>



<li><strong>樣本外最賺的那組</strong>（快線 20、慢線 50、停損 3、停利 3）在訓練段排第 23 名，照登記的規則事前挑不到它。</li>



<li><strong>全段一起優化</strong>：把 2019 到 2026 年 9 月全部丟進去優化，180 組只有 3 組賺錢，第一名獲利因子也只有 1.02（898 筆、+323.35），就是上面樣本外最賺的那組。訓練冠軍放在全段是 −597.30。</li>



<li><strong>換報價模式</strong>：冠軍改用「每次報價」重跑兩段，筆數相同，獲利因子 0.90、0.83，跟「1分鐘OHLC」幾乎一樣。這兩種模式都是從 1 分鐘資料模擬報價，真實報價模式沒有測。</li>
</ul>



<p class="wp-block-paragraph">全段第一名就是樣本外最賺的那組。全段本來就包含樣本外那兩年，在其中一段特別好的組合，加總起來也容易排在前面。拿全段優化的第一名當成「這個策略最好的設定」，等於用考試答案回頭挑參數。</p>



<h2 class="wp-block-heading">6. 這個結果能說明什麼</h2>



<p class="wp-block-paragraph">照登記選出的冠軍沒有通過。整個網格看，訓練段賺錢的 41 組到樣本外只剩 6 組還賺錢，也沒有一組達到 1.2 的門檻：這個均線交叉策略在 EURUSD H1 上，這組網格裡沒有參數達到事先定的通過門檻。</p>



<p class="wp-block-paragraph">結論的範圍只到這裡：只測了一個商品、一個週期、一種切法。不代表均線交叉在其他商品或週期上一定沒用；反過來，就算某組通過了三段，也還要檢查是不是靠一兩筆大單撐起來、換個商品還成不成立，這些檢查本文沒做。</p>



<p class="wp-block-paragraph">三段驗證最有用的地方，是把「挑參數」和「打分數」分開。只要在動手前把規則寫死、鎖定段只碰一次，不通過的結果至少是可信的，不會被事後的調整蓋過去。</p>



<p class="wp-block-paragraph">本篇檔案：<a href="https://orzalanluo.com/wp-content/uploads/2026/09/MaCrossTrade.mq5">MaCrossTrade.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/1935/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<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 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>
	</channel>
</rss>
