<?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>冏掉的Alan羅</title>
	<atom:link href="https://orzalanluo.com/feed" rel="self" type="application/rss+xml" />
	<link>https://orzalanluo.com</link>
	<description>我什麼都不會，所以我在學習。</description>
	<lastBuildDate>Mon, 28 Sep 2026 05:55:52 +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 自訂指標：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 fetchpriority="high" 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>
		<item>
		<title>MT5 EA 上線前的檢查：交易權限、商品規格、接手持倉、下單失敗碼</title>
		<link>https://orzalanluo.com/archives/1892</link>
					<comments>https://orzalanluo.com/archives/1892#respond</comments>
		
		<dc:creator><![CDATA[alanluo]]></dc:creator>
		<pubDate>Sun, 20 Sep 2026 23:56:24 +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=1892</guid>

					<description><![CDATA[回測報告很漂亮，EA 掛到圖表上卻一張單都沒下——多半不是策略的問題。可能是演算法交易沒開，可能是商品加不進市 [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">回測報告很漂亮，EA 掛到圖表上卻一張單都沒下——多半不是策略的問題。可能是演算法交易沒開，可能是商品加不進市場報價，也可能單真的送出去了、被伺服器退回來，而 EA 沒有去看退回來的原因。這篇把掛上去之前該確認的事寫成程式碼：能不能交易、商品規格、EA 重新啟動之後怎麼接回手上的倉、下單失敗怎麼分類。</p>



<p class="wp-block-paragraph">三個範例檔。<a href="https://orzalanluo.com/wp-content/uploads/2026/09/PreflightCheck.mq5">PreflightCheck.mq5</a> 是腳本，在真的終端上跑一次，把權限、帳戶、要交易的商品規格全部印出來，只讀不下單。<a href="https://orzalanluo.com/wp-content/uploads/2026/09/MaCrossLive.mq5">MaCrossLive.mq5</a> 是在均線交叉 EA <a href="https://orzalanluo.com/wp-content/uploads/2026/09/MaCrossTrail.mq5">MaCrossTrail.mq5</a>（EMA 10／30 交叉進場、每筆賠餘額 1%、ATR 倍數停損停利、保本與移動停損）上面補自檢、接手持倉、下單失敗分類，魔術碼另外換了一個（20250801）。<a href="https://orzalanluo.com/wp-content/uploads/2026/09/RetcodeDemo.mq5">RetcodeDemo.mq5</a> 故意送三張會被拒的單，用來看伺服器回什麼。三個都在我的 MT5 上編譯過，腳本在連著伺服器的終端跑、兩隻 EA 在策略測試器跑，文中的日誌是實際輸出。</p>



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



<ol class="wp-block-list">
<li>PreflightCheck.mq5 放到 <code class="" data-line="">MQL5\Scripts\</code>，在 MetaEditor 按 <code class="" data-line="">F7</code>；回終端從「導航」把它拖到任何一張圖表上，輸出在「工具箱」的「專家」頁。腳本不跳參數視窗，要換商品清單就改 <code class="" data-line="">i_Symbols</code> 的預設值重編。</li>



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



<li>看「日誌」頁，開頭會多兩行「自檢通過」和「沒有要接手的持倉」，那就是這篇加的東西。</li>



<li>RetcodeDemo.mq5 也放 <code class="" data-line="">MQL5\Experts\</code>，EA 改選它、其餘設定不動再跑一次。它只送三張錯的單、印出伺服器回的碼，<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">而且只肯在策略測試器裡啟動</mark>。</li>
</ol>



<h2 class="wp-block-heading">1. 先問能不能交易</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 1. 自檢：連上了沒、四個交易開關、這個商品能不能下 ==============
bool Preflight()
{
   if(TerminalInfoInteger(TERMINAL_CONNECTED) == 0)
      return false;                    // 還沒連上，下一個 tick 再看

   string bad = &quot;&quot;;
   if(TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) == 0)
      bad += &quot;終端的演算法交易沒開；&quot;;
   if(MQLInfoInteger(MQL_TRADE_ALLOWED) == 0)
      bad += &quot;這張圖的 EA 沒允許演算法交易；&quot;;
   if(AccountInfoInteger(ACCOUNT_TRADE_ALLOWED) == 0)
      bad += &quot;帳戶不能交易；&quot;;
   if(AccountInfoInteger(ACCOUNT_TRADE_EXPERT) == 0)
      bad += &quot;帳戶不收 EA 的單；&quot;;
   if(SymbolInfoInteger(_Symbol, SYMBOL_TRADE_MODE)
      != SYMBOL_TRADE_MODE_FULL)
      bad += _Symbol + &quot; 現在不能完整交易；&quot;;
   if(SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) &lt;= 0 ||
      SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE_LOSS) &lt;= 0)
      bad += _Symbol + &quot; 規格讀不到；&quot;;

   if(bad != g_LastBad)                 // 狀況變了才印，免得洗版
   {
      if(bad == &quot;&quot;)
         Print(&quot;自檢通過：&quot;, _Symbol, &quot;  &quot;,
               AccountInfoInteger(ACCOUNT_MARGIN_MODE) ==
               ACCOUNT_MARGIN_MODE_RETAIL_HEDGING ? &quot;鎖倉&quot; : &quot;單邊&quot;,
               &quot;帳戶  餘額 &quot;, DoubleToString(
               AccountInfoDouble(ACCOUNT_BALANCE), 2));
      else
         Print(&quot;不能交易：&quot;, bad);
      g_LastBad = bad;
   }
   return bad == &quot;&quot;;
}</code></pre>



<p class="wp-block-paragraph">四個開關，少一個 EA 就下不了單，而且它們分屬不同的地方：</p>



<ul class="wp-block-list">
<li><code class="" data-line="">TERMINAL_TRADE_ALLOWED</code>：工具列那顆「演算法交易」按鈕（<code class="" data-line="">Ctrl+E</code>）。最常忘的一個，關著的時候終端會印「用戶端已禁用演算法交易」。</li>



<li><code class="" data-line="">MQL_TRADE_ALLOWED</code>：這支程式自己的開關，掛上去時「專家屬性」裡的「允許演算法交易」。同一個終端可以一隻 EA 開、一隻關。</li>



<li><code class="" data-line="">ACCOUNT_TRADE_ALLOWED</code>：帳戶端。用投資者密碼登入就是只讀，看得到跑得動，就是下不了單。</li>



<li><code class="" data-line="">ACCOUNT_TRADE_EXPERT</code>：伺服器允不允許 EA 下單，關著會印「伺服器端已禁用演算法交易」。</li>
</ul>



<p class="wp-block-paragraph">通過之後印一行，裡面那個「鎖倉」是帳戶的持倉模式。鎖倉帳戶同一個商品可以同時有多單和空單，單邊帳戶反向下單會跟原本的倉軋掉。先平倉再開新倉那種寫法兩種帳戶都能動；直接送反向單讓它自己軋掉的寫法，換到鎖倉帳戶就會變成兩個倉。</p>



<p class="wp-block-paragraph">檢查放在 <code class="" data-line="">OnTick</code> 不放 <code class="" data-line="">OnInit</code>，因為 <mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">OnInit 跑的時候終端可能還沒連上伺服器</mark>。這不是假設：我用啟動設定讓終端一開機就跑一支腳本，它讀到的持倉模式是 0，也就是「單邊」；同一個終端連上之後再讀，是「鎖倉」。<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">0 本身是個正常的值</mark>，讀錯了也看不出來。沒連上也不會有 tick，所以 EA 不必卡著等：每個 tick 檢查一次，過了才把 <code class="" data-line="">g_Ready</code> 打開、順手接手持倉（第 3 節）。</p>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">void OnTick()
{
   if(!g_Ready)
   {
      if(!Preflight())                 // 沒過就什麼都不做，下個 tick
         return;                       // 再檢查一次
      g_Ready = true;
      AdoptPosition();                 // 過了才看手上有沒有倉
   }
   ...</code></pre>



<p class="wp-block-paragraph">腳本沒有 tick 可以等，就自己等：</p>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">bool WaitConnected(int seconds)
{
   for(int i = 0; i &lt; seconds * 10 &amp;&amp; !IsStopped(); i++)
   {
      if(TerminalInfoInteger(TERMINAL_CONNECTED))
         return true;
      Sleep(100);
   }
   return false;
}</code></pre>



<p class="wp-block-paragraph">腳本在我的帳戶上印出來是這樣（帳號和餘額我遮掉了）：</p>



<pre class="wp-block-prismatic-blocks"><code class="" data-line="">終端的演算法交易  是   這支程式  是
帳戶可交易        是   帳戶收 EA 單  是
帳戶 ******** @Tickmill-Live  鎖倉
  杠杆 1:1000  結算貨幣 USD  餘額 ****</code></pre>



<h2 class="wp-block-heading">2. 商品規格用問的，不要寫死</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">   int    digits = (int)SymbolInfoInteger(sym, SYMBOL_DIGITS);
   double point  = SymbolInfoDouble(sym, SYMBOL_POINT);
   double vmin   = SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN);
   double vstep  = SymbolInfoDouble(sym, SYMBOL_VOLUME_STEP);
   double vmax   = SymbolInfoDouble(sym, SYMBOL_VOLUME_MAX);
   double contr  = SymbolInfoDouble(sym,
                                    SYMBOL_TRADE_CONTRACT_SIZE);
   double tickSz = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_SIZE);
   double tickVl = SymbolInfoDouble(sym,
                                    SYMBOL_TRADE_TICK_VALUE_LOSS);
   long   stops  = SymbolInfoInteger(sym, SYMBOL_TRADE_STOPS_LEVEL);
   long   freeze = SymbolInfoInteger(sym, SYMBOL_TRADE_FREEZE_LEVEL);
   long   fill   = SymbolInfoInteger(sym, SYMBOL_FILLING_MODE);
   long   trade  = SymbolInfoInteger(sym, SYMBOL_TRADE_MODE);
   long   exe    = SymbolInfoInteger(sym, SYMBOL_TRADE_EXEMODE);</code></pre>



<p class="wp-block-paragraph">前面還有一行 <code class="" data-line="">SymbolSelect(sym, true)</code>，加不進市場報價就是名字打錯了——券商的後綴五花八門，這一步先擋掉。接著每個數字都有用處：手數的最小／步進／最大決定你算出來的手數能不能送；<code class="" data-line="">SYMBOL_TRADE_STOPS_LEVEL</code> 是停損停利離現價的最小距離，<code class="" data-line="">SYMBOL_TRADE_FREEZE_LEVEL</code> 是「離現價多近就不准再碰這張單」（改單、平倉、刪掛單都不行）；<code class="" data-line="">SYMBOL_FILLING_MODE</code> 是這個商品收哪種成交方式，<code class="" data-line="">CTrade</code> 的 <code class="" data-line="">SetTypeFillingBySymbol()</code> 就是照它填的。</p>



<p class="wp-block-paragraph">同一個券商的六個商品，數字差成這樣：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>商品</th><th>最小 / 步進 / 最大手數</th><th>合約大小</th><th>tick size</th><th>tick value (USD)</th></tr></thead><tbody><tr><td>EURUSD</td><td>0.01 / 0.01 / 100.00</td><td>100000</td><td>0.00001</td><td>1.00</td></tr><tr><td>GBPUSD</td><td>0.01 / 0.01 / 100.00</td><td>100000</td><td>0.00001</td><td>1.00</td></tr><tr><td>USDJPY</td><td>0.01 / 0.01 / 100.00</td><td>100000</td><td>0.001</td><td>0.64</td></tr><tr><td>XAUUSD</td><td>0.01 / 0.01 / 100.00</td><td>100</td><td>0.01</td><td>1.00</td></tr><tr><td>US500</td><td>0.01 / 0.01 / 250.00</td><td>1</td><td>0.01</td><td>0.01</td></tr><tr><td>BRENT</td><td>1.00 / 1.00 / 10000.00</td><td>1</td><td>0.01</td><td>0.01</td></tr></tbody></table></figure>



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



<p class="wp-block-paragraph"><mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">BRENT 最小 1.00 手、步進也是 1.00 手</mark>：按風險算出來的手數只要不是整數就得捨去，算出不到 1 手就整筆做不了。把 0.01 當成天經地義寫死在程式裡，換到這種商品就一路被退。</p>



<p class="wp-block-paragraph">tick value 讀的是 <code class="" data-line="">SYMBOL_TRADE_TICK_VALUE_LOSS</code>：1 手、價格變動一個 tick size 會虧多少錢。報價貨幣跟帳戶貨幣一樣的商品，它就是 tick size 乘合約大小；<code class="" data-line="">USDJPY</code> 那個 0.64 是日圓換成美元來的，跟著匯率每天變，所以算手數時每次都要重新問。</p>



<p class="wp-block-paragraph">這個帳戶六個商品的停損距離下限和凍結距離都是 0，成交方式都是 IOC（能成交多少就成交多少，剩下的取消），別當成通例——換一家券商、換一個商品就可能不是。腳本印出來的規格行長這樣，這裡貼手數差最多的兩個：</p>



<pre class="wp-block-prismatic-blocks"><code class="" data-line="">規格 EURUSD  手數 0.01/0.01/100.00  合約 100000
  停損 0 點  凍結 0 點  tick 0.00001 值 1.00  成交 IOC  執行 市場
規格 BRENT  手數 1.00/1.00/10000.00  合約 1
  停損 0 點  凍結 0 點  tick 0.01 值 0.01  成交 IOC  執行 市場</code></pre>



<h2 class="wp-block-heading">3. EA 會重來一次，持倉不會</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 3. 接手持倉：EA 重來一次，倉還在伺服器那邊 =====================
void AdoptPosition()
{
   ulong ticket = 0;
   int   type   = FindMyPosition(ticket);
   if(type &lt; 0)
   {
      Print(&quot;沒有要接手的持倉&quot;);
      return;
   }
   Print(&quot;接手 #&quot;, ticket, &quot; &quot;,
         type == POSITION_TYPE_BUY ? &quot;多單&quot; : &quot;空單&quot;, &quot; &quot;,
         DoubleToString(PositionGetDouble(POSITION_VOLUME), 2),
         &quot; 手 @&quot;, DoubleToString(
         PositionGetDouble(POSITION_PRICE_OPEN), _Digits),
         &quot;  停損 &quot;, DoubleToString(
         PositionGetDouble(POSITION_SL), _Digits),
         &quot;  停利 &quot;, DoubleToString(
         PositionGetDouble(POSITION_TP), _Digits));
}</code></pre>



<p class="wp-block-paragraph">改參數、換週期，EA 只重跑 <code class="" data-line="">OnDeinit</code>、<code class="" data-line="">OnInit</code>，<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">全域變數留著舊值</mark>；移除再掛上、終端重開這類情況，才是從頭載入、回到初始值。持倉都還在伺服器上，但「這筆已經保本過了」「進場時的 ATR 是多少」這種記在變數裡的東西，重來之後不是不見就是舊的。所以 <code class="" data-line="">g_Ready</code> 這種旗標要在 <code class="" data-line="">OnInit</code> 裡自己設回 <code class="" data-line="">false</code>，重來才會重新自檢、重新查一次持倉。</p>



<p class="wp-block-paragraph">兩條路。一條是狀態全部從持倉讀回來：搬停損那段就是這樣寫的，每次都從 <code class="" data-line="">POSITION_SL</code> 和進場價現算，重來幾次都一樣。另一條是像上面這樣，自檢過了之後掃一次持倉，把 EA 要用的東西重建起來；這裡只做最小的一步——把手上的倉找出來印一行，至少日誌裡看得到它接手了什麼。</p>



<p class="wp-block-paragraph"><mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">這件事回測測不到</mark>：策略測試器不會重啟 EA，這行在測試器裡永遠印「沒有要接手的持倉」。要確認，拿模擬帳戶掛上去，在有持倉的時候打開「專家屬性」把魔術碼以外的任何一個參數改一下再按確定，這樣就會重跑一次 <code class="" data-line="">OnInit</code>，看日誌有沒有印出接手那行。</p>



<h2 class="wp-block-heading">4. 下單失敗要分類，不是重送就好</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 4. 下單失敗：先分類，再決定要不要重送 ==========================
#define FAIL_RETRY   1                  // 換個價再送一次就好
#define FAIL_UNKNOWN 2                  // 送成功了沒？先查持倉
#define FAIL_STOP    3                  // 自己送錯的，重送一樣錯

int ClassifyFail(uint code)
{
   switch(code)
   {
      case TRADE_RETCODE_REQUOTE:           // 10004 重新報價
      case TRADE_RETCODE_PRICE_CHANGED:     // 10020 價格變了
      case TRADE_RETCODE_PRICE_OFF:         // 10021 沒有報價
      case TRADE_RETCODE_TOO_MANY_REQUESTS: // 10024 請求太頻繁
         return FAIL_RETRY;
      case TRADE_RETCODE_TIMEOUT:           // 10012 請求逾時
      case TRADE_RETCODE_CONNECTION:        // 10031 沒有連線
         return FAIL_UNKNOWN;
   }
   return FAIL_STOP;                        // 手數、停損、錢不夠…
}

bool SendMarket(bool isBuy, double lots, double sl, double tp)
{
   for(int n = 1; n &lt;= i_Retry; n++)
   {
      bool ok = isBuy ? g_Trade.Buy(lots, _Symbol, 0, sl, tp)
                      : g_Trade.Sell(lots, _Symbol, 0, sl, tp);
      uint code = g_Trade.ResultRetcode();
      if(ok &amp;&amp; (code == TRADE_RETCODE_DONE ||        // 10009 全部成交
                code == TRADE_RETCODE_DONE_PARTIAL)) // 10010 部分成交
         return true;                    // 部分成交也是有倉，別當失敗
      Print(&quot;下單失敗 &quot;, code, &quot; &quot;,
            g_Trade.ResultRetcodeDescription(), &quot;（第 &quot;, n, &quot; 次）&quot;);
      int what = ClassifyFail(code);
      if(what == FAIL_STOP)
         return false;
      if(what == FAIL_UNKNOWN)
      {
         Sleep(1000);                   // 給伺服器一點時間回報
         ulong t = 0;
         if(FindMyPosition(t) &gt;= 0)
         {
            Print(&quot;  查到持倉 #&quot;, t, &quot;，這張其實送成功了&quot;);
            return true;
         }
         Print(&quot;  結果不明又查不到持倉，這根 K 線不再送&quot;);
         return false;
      }
      Sleep(200 * n);                   // 可重送：退一步再來
   }
   return false;
}</code></pre>



<p class="wp-block-paragraph"><code class="" data-line="">#define</code> 是替一個數字取名字的常數，編譯前會被換成那個數字，程式裡寫 <code class="" data-line="">FAIL_RETRY</code> 就等於寫 1。<code class="" data-line="">switch</code> 裡連續寫幾個 <code class="" data-line="">case</code>，代表這幾種情況做同一件事；<code class="" data-line="">return</code> 直接離開函數，所以不需要 <code class="" data-line="">break</code>。沒列到的碼走出 <code class="" data-line="">switch</code>，一律回 <code class="" data-line="">FAIL_STOP</code>。</p>



<p class="wp-block-paragraph">迴圈最多送 <code class="" data-line="">i_Retry</code> 次（預設 3），成功、或碰到不該再送的就直接出來。市價單成功的碼有兩個：10009 全部成交，10010 部分成交。IOC 這種成交方式本來就允許只成交一部分，這時候伺服器上<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">已經有倉了</mark>，要是只當成失敗回去，日誌會少一行開倉、狀態也對不上。平倉那條路一樣，部分平倉之後手上還有剩，別急著開反手單。</p>



<p class="wp-block-paragraph">失敗分三類，差別在「這張單到底送成功了沒」。價格變了、重新報價、沒有報價、請求太頻繁，這些是伺服器明確拒絕，沒有成交，退一步再送一次就好。手數不對、停損不對、錢不夠，是自己送錯的，重送一百次也一樣。</p>



<p class="wp-block-paragraph">麻煩的是逾時和沒有連線：<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">這兩個代表你不知道結果</mark>。請求可能已經到了伺服器並且成交，只是回覆沒傳回來，這時候盲目重送就會變成兩張單。所以先等一下去查持倉，查到就當它成功了。要留意查的是終端本地那份持倉清單，真的斷線時它也更新不了——查不到就別再送，等連上之後的 tick 再說。</p>



<p class="wp-block-paragraph">故意送錯的三張單，測試器回的碼：</p>



<pre class="wp-block-prismatic-blocks"><code class="" data-line="">最小 0.01 步進 0.01 最大 100.00 可用保證金 10000.00
手數 0.01333333 不是步進的倍數 -&gt; 10014 invalid volume
多單的停損掛在現價之上 -&gt; 10016 invalid stops
手數 100.00 保證金不夠 -&gt; 10019 not enough money</code></pre>



<p class="wp-block-paragraph">100 手需要 205128.00 的保證金而帳戶只有 10000.00——這個數字是測試器按商品的保證金規格算的，不是拿帳戶杠杆乘一乘就會得到。三張都沒有成交，餘額一毛沒動。</p>



<p class="wp-block-paragraph">常用的幾個碼，號碼是終端自己印出來的：</p>



<pre class="wp-block-prismatic-blocks"><code class="" data-line="">重新報價 10004  價格變了 10020  沒有報價 10021  請求太頻繁 10024
請求逾時 10012  沒有連線 10031  市場關閉 10018  禁止交易 10017
全部成交 10009  部分成交 10010  終端禁用 10027  伺服器禁用 10026</code></pre>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>回傳碼</th><th>意思</th><th>怎麼辦</th></tr></thead><tbody><tr><td>10009</td><td>全部成交</td><td>正常</td></tr><tr><td>10010</td><td>部分成交</td><td>有倉了，照成交量繼續</td></tr><tr><td>10004</td><td>重新報價</td><td>換個價再送</td></tr><tr><td>10020</td><td>價格變了</td><td>換個價再送</td></tr><tr><td>10021</td><td>沒有報價</td><td>換個價再送</td></tr><tr><td>10024</td><td>請求太頻繁</td><td>退一步再送</td></tr><tr><td>10012</td><td>請求逾時</td><td>結果未知，先查持倉</td></tr><tr><td>10031</td><td>沒有連線</td><td>結果未知，先查持倉</td></tr><tr><td>10014</td><td>手數無效</td><td>照規格修手數</td></tr><tr><td>10016</td><td>停損停利無效</td><td>照 StopsLevel 修距離</td></tr><tr><td>10019</td><td>錢不夠</td><td>縮手數或別做</td></tr><tr><td>10018</td><td>市場關閉</td><td>等開市</td></tr><tr><td>10017</td><td>禁止交易</td><td>查權限那四項</td></tr><tr><td>10027</td><td>用戶端禁用 EA</td><td>按 Ctrl+E 或查專家屬性</td></tr><tr><td>10026</td><td>伺服器端禁用 EA</td><td>問券商</td></tr></tbody></table></figure>



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



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



<p class="wp-block-paragraph">MaCrossLive 在 EURUSD 2025 年 1 月跑一趟，日誌開頭多了兩行：</p>



<pre class="wp-block-prismatic-blocks"><code class="" data-line="">EA 啟動：保本 1.0 ATR +10 點，移動停損 2.0 ATR，停利 3.0 ATR
自檢通過：EURUSD  鎖倉帳戶  餘額 10000.00
沒有要接手的持倉</code></pre>



<p class="wp-block-paragraph">後面 EA 自己印的 78 行開倉、保本、移動、平倉，跟同一套訊號但沒有這些檢查的 MaCrossTrail 逐字相同：14 筆交易、總淨盈利 416.21。這是應該的——自檢通過之後那條路徑跟原本一模一樣，檢查只在「不該下單的時候」才會改變行為。</p>



<h2 class="wp-block-heading">上線前的清單</h2>



<p class="wp-block-paragraph">把上面四件事排成順序，掛上去之前走一遍：</p>



<ul class="wp-block-list">
<li>跑一次 PreflightCheck，四個開關都要是「是」，要交易的商品規格印得出來。</li>



<li>手數、停損距離、合約大小全部從 <code class="" data-line="">SymbolInfo</code> 問，不要寫死在程式裡。</li>



<li>EA 重來一次之後，狀態要能從持倉重建。</li>



<li>下單失敗一定要看回傳碼，分成「換個價再送」「結果未知去查持倉」「別再送了」三類，成功別漏掉部分成交。</li>



<li>先用模擬帳戶掛一段時間：斷線、重啟、重新報價、請求逾時、真實滑點，這些回測裡都不會發生。</li>
</ul>



<p class="wp-block-paragraph">自檢是起飛前檢查，不是全程監看——<code class="" data-line="">g_Ready</code> 過了就不再檢查。想要盤中有人按掉「演算法交易」也能發現，就別把它閂死，讓它每根 K 線再跑一次。</p>



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

]]></content:encoded>
					
					<wfw:commentRss>https://orzalanluo.com/archives/1892/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>MQL5 保本與移動停損：PositionModify、ATR 距離、停損只往獲利方向搬</title>
		<link>https://orzalanluo.com/archives/1882</link>
					<comments>https://orzalanluo.com/archives/1882#respond</comments>
		
		<dc:creator><![CDATA[alanluo]]></dc:creator>
		<pubDate>Thu, 17 Sep 2026 23:26:07 +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=1882</guid>

					<description><![CDATA[開倉時設好的停損停利，之後就不動了：要嘛碰停損賠一筆固定的錢，要嘛碰停利賺一筆固定的錢。這篇加兩個常見的出場： [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">開倉時設好的停損停利，之後就不動了：要嘛碰停損賠一筆固定的錢，要嘛碰停利賺一筆固定的錢。這篇加兩個常見的出場：<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">保本</mark>是賺到一定程度就把停損搬到進場價附近，之後最差是平手；<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">移動停損</mark>是停損跟著價格走，賺到的鎖一部分。兩個做法的共同規則只有一句：停損只往獲利方向搬，永遠不往回搬。</p>



<p class="wp-block-paragraph">範例 <a href="https://orzalanluo.com/wp-content/uploads/2026/09/MaCrossTrail.mq5">MaCrossTrail.mq5</a> 是餘額 1% 風險那隻均線交叉 EA（<a href="https://orzalanluo.com/wp-content/uploads/2026/09/MaCrossRisk.mq5">MaCrossRisk.mq5</a>）加一個 <code class="" data-line="">ManagePosition()</code>，每根新 K 線先看持倉的停損要不要搬、再看訊號；開倉多了「停利倍數 0 就不設」的處理，魔術碼另外換了一個，其他沒動。整份在我的 MT5 上編譯過，策略測試器用 2025 年 1 月的 EURUSD H1 跑了四種設定，文中日誌和數字是實際輸出。</p>



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



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



<li>策略測試器 <code class="" data-line="">Ctrl+R</code>：EA 選 MaCrossTrail，EURUSD，H1，日期 2025.01.02 到 2025.01.31，模式「每次報價」，入金 10000，杠杆 1:100，按「開始」。</li>



<li>看「日誌」頁。每筆開倉之後多了「移動」「保本」兩種行，印停損從哪搬到哪；測試器自己也會對每次修改印一行 position modified。</li>
</ol>



<h2 class="wp-block-heading">1. 參數與交易物件</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 1. 參數與交易物件 ==============================================
#include &lt;Trade\Trade.mqh&gt;
#include &lt;Indicators\Trend.mqh&gt;
#include &lt;Indicators\Oscilators.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 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 long           i_Magic      = 20250701; // 魔術碼

CTrade   g_Trade;
CiMA     g_Fast;                          // 指標物件，handle 包在裡面
CiMA     g_Slow;
CiATR    g_Atr;
datetime g_LastBarTime = 0;</code></pre>



<p class="wp-block-paragraph">多三個參數。<code class="" data-line="">i_BeAtr</code>：賺到幾倍 ATR 就保本，保本價是進場價再往獲利方向多 <code class="" data-line="">i_BeLockPts</code> 點，把手續費蓋掉。<code class="" data-line="">i_TrailAtr</code>：移動停損跟現價保持幾倍 ATR 的距離。兩個都設 0 就是關掉，跑出來會跟 MaCrossRisk 一模一樣，文末的對照就是這樣跑的。<code class="" data-line="">i_TpAtr</code> 也多了一個意思：設 0 就不掛停利，讓移動停損自己決定什麼時候出。</p>



<p class="wp-block-paragraph">指標物件、算手數、查保證金、找持倉那幾段都沒改，檔案裡有，這篇不貼。</p>



<h2 class="wp-block-heading">2. 開倉：停利倍數 0 就不設</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">   bool   useTp   = (i_TpAtr &gt; 0);
   ...
   if(useTp &amp;&amp; tpDist &lt; minDist) tpDist = minDist;
   ...
   double tp = 0;                                 // 0 = 不設停利
   if(useTp)
      tp = NormalizeDouble(isBuy ? ask + tpDist : bid - tpDist,
                           _Digits);</code></pre>



<p class="wp-block-paragraph">CTrade 的 <code class="" data-line="">Buy()</code>／<code class="" data-line="">Sell()</code> 停利傳 0 就是不設。原本的寫法直接把 <code class="" data-line="">i_TpAtr</code> 設 0 拿不到這個效果：停利距離 0 會被「不能小於 StopsLevel」那行補成最小距離，掛出一個貼著現價的停利。所以補正那行和算 <code class="" data-line="">tp</code> 那幾行都看 <code class="" data-line="">useTp</code>。</p>



<h2 class="wp-block-heading">3. 保本與移動停損</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 3. 保本與移動停損：算出想要的停損價，比現在的好才改 ============
void ManagePosition(ulong ticket, int type, double atr)
{
   if(!PositionSelectByTicket(ticket))
      return;
   double open  = PositionGetDouble(POSITION_PRICE_OPEN);
   double sl    = PositionGetDouble(POSITION_SL);
   double tp    = PositionGetDouble(POSITION_TP);
   bool   isBuy = (type == POSITION_TYPE_BUY);
   double dir   = isBuy ? 1 : -1;                 // 多 +1、空 -1
   double price = SymbolInfoDouble(_Symbol, isBuy ? SYMBOL_BID
                                                  : SYMBOL_ASK);
   double gain  = dir * (price - open);           // 現在賺的價差

   // 兩個候選：保本價、移動停損價，留對自己最有利的
   double want = sl;                              // 目前的停損
   if(sl == 0)                                   // 沒停損：從最差起算
      want = isBuy ? 0 : DBL_MAX;
   string why = &quot;&quot;;
   if(i_BeAtr &gt; 0 &amp;&amp; gain &gt;= atr * i_BeAtr)
   {
      double be = open + dir * i_BeLockPts * _Point;
      if(dir * (be - want) &gt; 0) { want = be; why = &quot;保本&quot;; }
   }
   if(i_TrailAtr &gt; 0)
   {
      double tr = price - dir * atr * i_TrailAtr;
      if(dir * (tr - want) &gt; 0) { want = tr; why = &quot;移動&quot;; }
   }
   want = NormalizeDouble(want, _Digits);
   if(why == &quot;&quot;)
      return;                                     // 沒有比現在好
   if(sl != 0 &amp;&amp; dir * (want - sl) &lt; _Point * 0.5)
      return;                                     // 好不到 1 點，不改

   // 新停損離現價至少要 StopsLevel，太近伺服器會拒
   long stops = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   if(dir * (price - want) &lt; stops * _Point)
      return;

   if(!g_Trade.PositionModify(ticket, want, tp) ||
      g_Trade.ResultRetcode() != TRADE_RETCODE_DONE)
   {
      Print(&quot;改停損失敗 &quot;, g_Trade.ResultRetcode(), &quot; &quot;,
            g_Trade.ResultRetcodeDescription());
      return;
   }
   Print(why, &quot; #&quot;, ticket, &quot;  停損 &quot;, DoubleToString(sl, _Digits),
         &quot; -&gt; &quot;, DoubleToString(want, _Digits),
         &quot;  現價 &quot;, DoubleToString(price, _Digits));
}</code></pre>



<p class="wp-block-paragraph"><code class="" data-line="">dir</code> 是 +1（多）或 −1（空），乘上去之後多單空單共用同一套式子：<code class="" data-line="">dir * (a - b) &gt; 0</code> 就是「a 比 b 對自己更有利」，多單是 a 比 b 高、空單是 a 比 b 低。例如：</p>



<ul class="wp-block-list">
<li>多單 dir = +1：a = 1.0310、b = 1.0300，+1 × (1.0310 − 1.0300) = +0.0010 &gt; 0，較高的 a 有利。</li>



<li>空單 dir = −1：a = 1.0290、b = 1.0300，−1 × (1.0290 − 1.0300) = +0.0010 &gt; 0，較低的 a 有利。</li>
</ul>



<p class="wp-block-paragraph"><code class="" data-line="">gain</code> 是現在賺的價差，用平倉會用到的那邊的價，多單看 Bid、空單看 Ask。</p>



<p class="wp-block-paragraph">接著算兩個候選停損價：保本價（賺夠了才有）和移動停損價（現價往回退 ATR 倍數），<code class="" data-line="">want</code> 從現在的停損起算，候選價要比它更有利才換上去。沒停損時從最差起算：多單從 0，空單從 <code class="" data-line="">DBL_MAX</code>（double 能表示的最大值），任何候選價都比它有利。</p>



<p class="wp-block-paragraph">那兩個 if 就是<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">停損只往獲利方向搬</mark>：候選價比現在的停損差，就什麼都不做。差不到 1 點的也不改，省得每根 K 線都丟一筆修改給伺服器。</p>



<p class="wp-block-paragraph">改之前看 StopsLevel：新停損離現價要有那個距離，太近伺服器會拒。然後 <code class="" data-line="">PositionModify(ticket, sl, tp)</code> 改停損，<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">停利要把原值一起傳回去</mark>，傳 0 會把原本的停利拿掉。</p>



<p class="wp-block-paragraph">這個函數沒有記住任何東西：有沒有保本過、上次搬到哪，全部從持倉本身讀回來。EA 重啟、換台電腦接著跑，看到的持倉是什麼就從什麼開始。用一個「已經保本」的布林變數也做得到，但重啟就丟了。</p>



<h2 class="wp-block-heading">4. OnTick：先管持倉、再看訊號</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 4. OnTick：新 K 線先管持倉、再看訊號 ==========================
void OnTick()
{
   ShowStatus();
   if(!IsNewBar())
      return;

   g_Fast.Refresh();                // 抓最新指標值，再用 Main(i) 讀
   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_LastBarTime = 0;
      return;
   }

   ulong ticket = 0;
   int   have   = FindMyPosition(ticket);
   if(have &gt;= 0)
      ManagePosition(ticket, have, atr1);</code></pre>



<p class="wp-block-paragraph">一根 K 線只看一次：新 K 線的第一個 tick，用上一根收完的 ATR 和那時的現價算。想要每個 tick 都跟著搬，得把 <code class="" data-line="">Refresh()</code>、<code class="" data-line="">Main()</code> 那幾行和這個呼叫搬到 <code class="" data-line="">IsNewBar()</code> 前面，而且「差不到 1 點不改」要放寬，不然一秒鐘會丟好幾筆修改。</p>



<p class="wp-block-paragraph">移動停損的距離用當下的 ATR，波動縮小距離跟著縮，所以會出現價格沒漲、停損卻往上收的情況，下面的日誌有例子。要固定距離就在開倉時把 ATR 記下來。</p>



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



<p class="wp-block-paragraph">EURUSD 2025 年 1 月，預設參數（保本 1 ATR、移動停損 2 ATR、停利 3 ATR），14 筆，進場時間和價格跟 MaCrossRisk 逐筆相同，手數因餘額不同差一步。拿 #6 這筆看整個過程：</p>



<pre class="wp-block-prismatic-blocks"><code class="" data-line="">開多 #6  0.27 手 @1.02483  停損 380 點  風險 102.60
移動 #6  停損 1.02103 -&gt; 1.02123  現價 1.02490
移動 #6  停損 1.02123 -&gt; 1.02148  現價 1.02487
移動 #6  停損 1.02148 -&gt; 1.02169  現價 1.02504
移動 #6  停損 1.02169 -&gt; 1.02220  現價 1.02525
移動 #6  停損 1.02220 -&gt; 1.02233  現價 1.02543
移動 #6  停損 1.02233 -&gt; 1.02290  現價 1.02618
保本 #6  停損 1.02290 -&gt; 1.02493  現價 1.02680
移動 #6  停損 1.02493 -&gt; 1.02613  現價 1.02949
移動 #6  停損 1.02613 -&gt; 1.02635  現價 1.02992</code></pre>



<p class="wp-block-paragraph">開多之後停損跟著往上搬了 6 次，賺到 1 ATR 時跳到保本價 1.02493（進場 1.02483 加 10 點），之後再跟 2 次，最後碰停利出場。</p>



<p class="wp-block-paragraph">第 2、3 行就是上面說的情況：現價從 1.02490 掉到 1.02487，停損卻從 1.02123 搬到 1.02148，因為這根的 ATR 比上一根小。</p>



<figure class="wp-block-image size-full"><a href="https://orzalanluo.com/wp-content/uploads/mt5-07-trail-steps.png"><img decoding="async" width="1000" height="760" src="https://orzalanluo.com/wp-content/uploads/mt5-07-trail-steps.png" alt="EURUSD H1 2025.01.14 的 K 線圖：#6 多單 0.27 手 03:00 在 1.02483 開多，綠線是停利 1.03053，21:37 碰到出場。紅色階梯是停損：從開倉停損 1.02103 分 6 次往上搬，11:00～17:00 價格回檔時停住不動，17:00 搬到保本價 1.02493（進場＋10 點），之後再搬 2 次。" class="wp-image-1912" srcset="https://orzalanluo.com/wp-content/uploads/mt5-07-trail-steps.png 1000w, https://orzalanluo.com/wp-content/uploads/mt5-07-trail-steps-300x228.png 300w, https://orzalanluo.com/wp-content/uploads/mt5-07-trail-steps-644x489.png 644w" sizes="(max-width: 1000px) 100vw, 1000px" /></a><figcaption class="wp-element-caption">#6 多單的停損：每根新 K 線檢查一次，只往上搬，回檔時不往回搬</figcaption></figure>



<p class="wp-block-paragraph">整個月 EA 印了 62 行修改，測試器日誌也有 62 行 position modified，一一對應。修改不是成交，報告的交易總計還是 14。</p>



<p class="wp-block-paragraph">同一個月、同一套訊號，四種設定的 14 筆各怎麼結束。「獲利側」是停損已經搬過進場價才被打到，也就是保本或移動停損收掉的；「虧損側」是停損還在進場價另一邊；「反向／測試結束」是被反向訊號平掉、或測試結束時強制平倉：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>設定</th><th>停利</th><th>停損在獲利側</th><th>停損在虧損側</th><th>反向／測試結束</th></tr></thead><tbody><tr><td>固定停損停利（對照）</td><td>7</td><td>0</td><td>2</td><td>5</td></tr><tr><td>＋保本</td><td>5</td><td>3</td><td>2</td><td>4</td></tr><tr><td>＋保本＋移動停損</td><td>5</td><td>3</td><td>3</td><td>3</td></tr><tr><td>＋保本＋移動停損，不設停利</td><td>0</td><td>8</td><td>3</td><td>3</td></tr></tbody></table></figure>



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



<p class="wp-block-paragraph">報告數字：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>設定</th><th>總淨盈利 (USD)</th><th>盈利因子</th><th>最大權益數虧損</th></tr></thead><tbody><tr><td>固定停損停利（對照）</td><td>656.73</td><td>2.51</td><td>441.64 (4.19%)</td></tr><tr><td>＋保本</td><td>388.10</td><td>1.96</td><td>462.08 (4.38%)</td></tr><tr><td>＋保本＋移動停損</td><td>416.21</td><td>2.11</td><td>433.97 (4.11%)</td></tr><tr><td>＋保本＋移動停損，不設停利</td><td>197.60</td><td>1.53</td><td>435.19 (4.15%)</td></tr></tbody></table></figure>



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



<p class="wp-block-paragraph">三種改法這個月都輸給固定停損停利。先看只加保本那趟：8 筆保本過，3 筆最後在保本價出場，每筆賺正好 10 點乘手數，扣掉手續費還是正的。這 3 筆跟對照趟比：</p>



<ul class="wp-block-list">
<li>#18：對照趟到停利賺 148.20，這趟先回檔碰到保本價，只賺 3.90。</li>



<li>#20：對照趟到停利賺 152.64，這趟先回檔碰到保本價，只賺 4.70。</li>



<li>#8：對照趟被反向訊號平掉賠 28.08，這趟反而賺 3.60。</li>
</ul>



<p class="wp-block-paragraph">對照趟碰停損的 2 筆，保本一筆也沒救到，一樣賠。</p>



<p class="wp-block-paragraph">再加移動停損，舉兩筆對照趟賠錢的單：</p>



<ul class="wp-block-list">
<li>#14 少賠：開倉時印的風險是 102.48，停損先跟上來一段才被打到，賠 66.49。</li>



<li>#16 變糟：對照趟被反向訊號平掉賠 59.07，這趟停損跟上來之後在虧損側被打到，賠 66.99。</li>
</ul>



<p class="wp-block-paragraph">不設停利那趟最差。它跟保本＋移動停損那趟只差停利：有停利時到停利的 5 筆，拿掉停利後全部變成在獲利側被移動停損收掉，其餘 9 筆出場一樣。</p>



<p class="wp-block-paragraph">這 5 筆只有 #6 賺更多（153.90 變 173.42），其他 4 筆各少 40 到 78 塊。價格每次都到了原本停利的位置又回頭，要退 2 ATR 才出場，退掉的就是那段。1 月的 EURUSD 沒有那種一路走不回頭的趨勢，「讓獲利跑」跑不出去。</p>



<p class="wp-block-paragraph">這不是保本和移動停損沒用的結論，是 14 筆的一個月看不出來。它們改變的是每一筆怎麼結束，整體是好是壞看行情有沒有給趨勢。要不要用、距離幾倍 ATR，把同一段長一點的資料四種設定各跑一次比報告，不要憑直覺覺得保本一定比較安全。</p>



<p class="wp-block-paragraph">本篇檔案：<a href="https://orzalanluo.com/wp-content/uploads/2026/09/MaCrossTrail.mq5">MaCrossTrail.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/1882/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>MQL5 手數與風險控管：每筆固定風險、商品規格、保證金檢查</title>
		<link>https://orzalanluo.com/archives/1879</link>
					<comments>https://orzalanluo.com/archives/1879#respond</comments>
		
		<dc:creator><![CDATA[alanluo]]></dc:creator>
		<pubDate>Wed, 16 Sep 2026 13:20:28 +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=1879</guid>

					<description><![CDATA[固定 0.10 手有個問題：停損距離每筆不一樣，賠的錢也就每筆不一樣。這篇把手數改成由停損距離倒推，每筆最多賠 [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">固定 0.10 手有個問題：停損距離每筆不一樣，賠的錢也就每筆不一樣。這篇把手數改成由停損距離倒推，<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">每筆最多賠餘額的 1%</mark>，順便講算手數要用到的商品規格，還有下單前怎麼確認保證金（開倉時券商先扣住的押金）夠不夠。</p>



<p class="wp-block-paragraph">範例 <a href="https://orzalanluo.com/wp-content/uploads/2026/09/MaCrossRisk.mq5">MaCrossRisk.mq5</a> 是把固定 0.10 手的均線交叉 EA（<a href="https://orzalanluo.com/wp-content/uploads/2026/09/MaCrossTrade.mq5">MaCrossTrade.mq5</a>：EMA 10／30 交叉進場、ATR 倍數停損停利、反向訊號先平再開）只換掉手數這一塊，魔術碼另外換了一個，其他都沒動。整份在我的 MT5 上編譯過、策略測試器跑過 2025 年 1 月的 EURUSD H1 和另外幾種情境，文中日誌和數字是實際輸出。</p>



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



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



<li>策略測試器 <code class="" data-line="">Ctrl+R</code>：EA 選 MaCrossRisk，EURUSD，H1，日期 2025.01.02 到 2025.01.31，模式「每次報價」，入金 10000，杠杆 1:100（保證金是部位價值的 1%），按「開始」。</li>



<li>看「日誌」頁。開頭兩行「規格」是這個商品算手數要用的數字，之後每筆開倉都印手數、停損幾點、這筆最多賠多少。</li>
</ol>



<h2 class="wp-block-heading">固定手數，其實不知道自己在賠多少</h2>



<p class="wp-block-paragraph">同一個月、同一套訊號，固定 0.10 手的 MaCrossTrade 停損距離從 161 點到 434 點，一筆碰停損賠 16.1 到 43.4 塊，是 10000 塊帳戶的 0.16% 到 0.43%。差快三倍，而且波動大的時候停損拉遠、賠得更多，剛好是最不該加碼的時候。</p>



<p class="wp-block-paragraph">反過來想：先決定一筆最多賠多少，比如餘額的 1%，再用停損距離算手數。停損遠就開小、停損近就開大，每筆賠的錢一樣。</p>



<h2 class="wp-block-heading">算手數要用的商品規格</h2>



<p class="wp-block-paragraph">一手賠多少錢，看三個規格：<code class="" data-line="">SYMBOL_TRADE_TICK_SIZE</code> 是價格最小跳動一格，叫 tick size；<code class="" data-line="">SYMBOL_TRADE_TICK_VALUE_LOSS</code> 是一手跳一格賠多少，叫 tick value（通常是帳戶幣別，已經換算過）；<code class="" data-line="">SYMBOL_TRADE_CONTRACT_SIZE</code> 是一手的合約量。</p>



<p class="wp-block-paragraph">手數本身有最小、最大、步進三個限制，<code class="" data-line="">SYMBOL_VOLUME_MIN</code>、<code class="" data-line="">SYMBOL_VOLUME_MAX</code>、<code class="" data-line="">SYMBOL_VOLUME_STEP</code>，不照規格送單會被拒，錯誤碼 10014。範例 EA 啟動時把這些印出來，三個商品在我的 Tickmill 帳戶上：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>商品</th><th>合約</th><th>tick size</th><th>tick value (USD)</th><th>最小／步進 (手)</th></tr></thead><tbody><tr><td>EURUSD</td><td>100000</td><td>0.00001</td><td>1.0000</td><td>0.01／0.01</td></tr><tr><td>XAUUSD</td><td>100</td><td>0.01</td><td>1.0000</td><td>0.01／0.01</td></tr><tr><td>USDJPY</td><td>100000</td><td>0.001</td><td>0.6357</td><td>0.01／0.01</td></tr></tbody></table></figure>



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



<p class="wp-block-paragraph">EURUSD 一手 100000 歐元、跳 0.00001 賠 1 塊；黃金一手 100 盎司、跳 0.01 也是 1 塊；USDJPY 一手跳 0.001 是 100 日圓，換成美金 0.6357 塊，這個數跟著匯率走，所以每次算手數都要重新讀。</p>



<p class="wp-block-paragraph"><mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">tick size 不一定等於 _Point</mark>：有些商品一跳是好幾個 point，例如指數期貨報價到 0.01、一跳卻是 0.25，算錢要用 tick size，不能用 _Point。這裡三個商品剛好相等，別的商品、別家券商不一定。</p>



<p class="wp-block-paragraph">這些數字在 MT5 主視窗也看得到：「市場報價」（<code class="" data-line="">Ctrl+M</code>）對商品按右鍵、「規格」。</p>



<h2 class="wp-block-heading">1. 參數與交易物件</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 1. 參數與交易物件 ==============================================
#include &lt;Trade\Trade.mqh&gt;
#include &lt;Indicators\Trend.mqh&gt;
#include &lt;Indicators\Oscilators.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 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;      // 停利 = ATR x 倍數
input long           i_Magic      = 20250601; // 魔術碼

CTrade   g_Trade;
CiMA     g_Fast;                          // 指標物件，handle 包在裡面
CiMA     g_Slow;
CiATR    g_Atr;
datetime g_LastBarTime = 0;</code></pre>



<p class="wp-block-paragraph">原本固定手數的 <code class="" data-line="">i_Lots</code> 換成 <code class="" data-line="">i_RiskPct</code>（每筆賠餘額的百分之幾）和 <code class="" data-line="">i_MaxLots</code>（算出來再大也不超過這個）。上限是保險：餘額大、停損又很近的時候，算出來的手數可能大到不合理。</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(!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;
   }
   g_Trade.SetExpertMagicNumber(i_Magic);
   g_Trade.SetDeviationInPoints(20);
   g_Trade.SetTypeFillingBySymbol(_Symbol);

   // 算手數要用到的規格，先印出來看
   double minLot  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double maxLot  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   double stepLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
   double tickSz  = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
   double tickVal = SymbolInfoDouble(_Symbol,
                                     SYMBOL_TRADE_TICK_VALUE_LOSS);
   Print(&quot;規格 &quot;, _Symbol, &quot;  最小 &quot;, DoubleToString(minLot, 2),
         &quot; 手  最大 &quot;, DoubleToString(maxLot, 2),
         &quot; 手  步進 &quot;, DoubleToString(stepLot, 2), &quot; 手&quot;);
   Print(&quot;  合約 &quot;, DoubleToString(SymbolInfoDouble(_Symbol,
         SYMBOL_TRADE_CONTRACT_SIZE), 0),
         &quot;  tick &quot;, DoubleToString(tickSz, _Digits),
         &quot;  每 tick &quot;, DoubleToString(tickVal, 4), &quot; &quot;,
         AccountInfoString(ACCOUNT_CURRENCY));
   Print(&quot;EA 啟動：每筆風險 &quot;, DoubleToString(i_RiskPct, 1),
         &quot;% 餘額，上限 &quot;, DoubleToString(i_MaxLots, 2), &quot; 手&quot;);
   return INIT_SUCCEEDED;
}</code></pre>



<p class="wp-block-paragraph">指標物件和 CTrade 的三行設定跟 MaCrossTrade 一樣。多的是把上面那幾個規格讀出來印一遍，用 <code class="" data-line="">DoubleToString()</code> 指定小數位，tick size 用商品的 <code class="" data-line="">_Digits</code> 位才看得到 0.00001。實盤前看一眼這兩行，規格跟想的不一樣馬上知道。</p>



<h2 class="wp-block-heading">3. 依風險比例算手數</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 3. 依風險比例算手數 ============================================
// slDist 是停損距離（價格差）。回傳手數，算不出可用的手數就回 0；
// risk 帶回這筆手數真正會賠的錢（捨去到步進之後）
double CalcLots(double slDist, double &amp;risk)
{
   double balance   = AccountInfoDouble(ACCOUNT_BALANCE);
   double riskMoney = balance * i_RiskPct / 100.0;   // 這筆最多賠多少
   double tickSize  = SymbolInfoDouble(_Symbol,
                                       SYMBOL_TRADE_TICK_SIZE);
   double tickValue = SymbolInfoDouble(_Symbol,
                                       SYMBOL_TRADE_TICK_VALUE_LOSS);
   double minLot    = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
   double maxLot    = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
   double stepLot   = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
   risk = 0;
   if(tickSize &lt;= 0 || tickValue &lt;= 0 || stepLot &lt;= 0 || slDist &lt;= 0)
   {
      Print(&quot;規格讀不到或停損距離為 0，這筆不做&quot;);
      return 0;
   }

   double lossPerLot = slDist / tickSize * tickValue; // 1 手賠多少
   double lots = riskMoney / lossPerLot;
   long steps = (long)MathFloor(lots / stepLot + 1e-9); // 捨去到步進
   lots = NormalizeDouble(steps * stepLot, 8);          // 修浮點尾數
   if(lots &lt; minLot)
   {
      Print(&quot;風險 &quot;, DoubleToString(riskMoney, 2), &quot; 連最小手數 &quot;,
            DoubleToString(minLot, 2), &quot; 手都不夠，這筆不做&quot;);
      return 0;
   }
   lots = MathMin(lots, MathMin(maxLot, i_MaxLots));
   risk = lots * lossPerLot;
   return lots;
}</code></pre>



<p class="wp-block-paragraph">四步：餘額乘以風險比例是這筆最多賠的錢；停損距離除以 tick size 是停損有幾格，乘 tick value 是一手碰停損賠多少；兩者相除是手數；最後 <code class="" data-line="">MathFloor()</code> 無條件捨去到步進，<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">捨去不能四捨五入</mark>，進位就超過設定的風險了。</p>



<p class="wp-block-paragraph">拿下面執行結果的第一筆 EURUSD 開倉代入一次，那時還沒平過倉，餘額就是入金 10000：</p>



<ol class="wp-block-list">
<li>這筆最多賠：10000 × 1% ＝ 100 塊。</li>



<li>一手賠多少：停損 288 點是 0.00288，÷ tick size 0.00001 ＝ 288 格，× tick value 1.0000 ＝ 288 塊。</li>



<li>相除：100 ÷ 288 ＝ 0.347… 手。</li>



<li>捨去到步進 0.01：0.34 手。</li>



<li>實際風險：0.34 × 288 ＝ 97.92 塊，就是日誌印的「風險 97.92」。</li>
</ol>



<figure class="wp-block-image size-full"><a href="https://orzalanluo.com/wp-content/uploads/mt5-06-lot-formula.png"><img decoding="async" width="1000" height="800" src="https://orzalanluo.com/wp-content/uploads/mt5-06-lot-formula.png" alt="由上往下五個以箭頭相連的方框：① 這筆最多賠＝餘額 × 風險 %，10000 × 1% = 100；② 1 手賠多少＝停損距離 ÷ tick size × tick value，0.00288 ÷ 0.00001 × 1.0000 = 288；③ 手數 100 ÷ 288 = 0.3472…；④ 無條件捨去到步進 0.01 手得 0.34 手，紅字註明進位成 0.35 手會賠 100.80、超過 100；⑤ 實際風險 0.34 × 288 = 97.92，沒超過 100。" class="wp-image-1911" srcset="https://orzalanluo.com/wp-content/uploads/mt5-06-lot-formula.png 1000w, https://orzalanluo.com/wp-content/uploads/mt5-06-lot-formula-300x240.png 300w, https://orzalanluo.com/wp-content/uploads/mt5-06-lot-formula-644x515.png 644w" sizes="(max-width: 1000px) 100vw, 1000px" /></a><figcaption class="wp-element-caption">第一筆開倉代入公式；④ 進位成 0.35 手會賠 100.80，超過 100</figcaption></figure>



<p class="wp-block-paragraph">捨去之後如果連最小手數都不到，這筆就不做，回傳 0。硬開 0.01 手等於賠超過設定的比例，小帳戶會常遇到，下面有實跑的例子。算出來的手數再跟商品最大手數和 <code class="" data-line="">i_MaxLots</code> 取小；<code class="" data-line="">risk</code> 用參考帶回這筆真正會賠的錢，印出來對帳用，它不含手續費、隔夜利息和滑點。</p>



<p class="wp-block-paragraph">除法加的 1e-9 和 <code class="" data-line="">NormalizeDouble()</code> 都是在擋浮點數：0.29 除以 0.01 會算成 28.999…、29 乘 0.01 會變 0.29000000000000004，不修掉就可能少一步或被券商拒。</p>



<h2 class="wp-block-heading">4. 開倉前先問保證金</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 4. 開倉前先問保證金夠不夠 ======================================
bool EnoughMargin(ENUM_ORDER_TYPE type, double lots, double price)
{
   double need = 0;
   if(!OrderCalcMargin(type, _Symbol, lots, price, need))
   {
      Print(&quot;OrderCalcMargin 失敗，錯誤碼 &quot;, GetLastError());
      return false;
   }
   double freeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
   if(need &gt; freeMargin)
   {
      Print(&quot;保證金不夠：&quot;, DoubleToString(lots, 2), &quot; 手要 &quot;,
            DoubleToString(need, 2), &quot;，可用 &quot;,
            DoubleToString(freeMargin, 2), &quot;，這筆不做&quot;);
      return false;
   }
   return true;
}</code></pre>



<p class="wp-block-paragraph"><code class="" data-line="">OrderCalcMargin()</code> 問這張單要押多少保證金，<code class="" data-line="">ACCOUNT_MARGIN_FREE</code> 是帳戶現在還能押多少。不夠就不送，省得被伺服器退 10019（not enough money）。入金 10000、1:100 的回測碰不到這條；實盤同時跑好幾隻 EA、或券商調低杠杆時才會，文末有把杠杆改 1:1 逼它出現的例子。</p>



<h2 class="wp-block-heading">5. 開倉：先算手數、再查保證金、才下單</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 5. 開倉：先算手數、再查保證金、才下單 ==========================
bool OpenPosition(int dir, double atr)
{
   double ask     = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double bid     = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double slDist  = atr * i_SlAtr;
   double tpDist  = atr * i_TpAtr;
   long stops = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   double minDist = stops * _Point + (ask - bid);
   if(slDist &lt; minDist) slDist = minDist;
   if(tpDist &lt; minDist) tpDist = minDist;

   // 先把停損價修到商品的小數位，再用修過的價差算手數
   bool   isBuy = (dir == POSITION_TYPE_BUY);
   double entry = isBuy ? ask : bid;
   double sl = NormalizeDouble(isBuy ? ask - slDist : bid + slDist,
                               _Digits);
   double tp = NormalizeDouble(isBuy ? ask + tpDist : bid - tpDist,
                               _Digits);
   slDist = MathAbs(entry - sl);

   double risk = 0;
   double lots = CalcLots(slDist, risk);
   if(lots &lt;= 0)
      return false;
   if(!EnoughMargin(isBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL,
                    lots, entry))
      return false;

   bool ok = isBuy ? g_Trade.Buy(lots, _Symbol, 0, sl, tp)
                   : g_Trade.Sell(lots, _Symbol, 0, sl, tp);
   if(!ok || g_Trade.ResultRetcode() != TRADE_RETCODE_DONE)
   {
      Print(&quot;下單失敗 &quot;, g_Trade.ResultRetcode(), &quot; &quot;,
            g_Trade.ResultRetcodeDescription());
      return false;
   }
   Print(isBuy ? &quot;開多&quot; : &quot;開空&quot;, &quot; #&quot;, g_Trade.ResultOrder(),
         &quot;  &quot;, DoubleToString(lots, 2), &quot; 手 @&quot;,
         DoubleToString(g_Trade.ResultPrice(), _Digits),
         &quot;  停損 &quot;, DoubleToString(slDist / _Point, 0), &quot; 點&quot;,
         &quot;  風險 &quot;, DoubleToString(risk, 2));
   return true;
}</code></pre>



<p class="wp-block-paragraph">這裡的順序是：先把停損停利價用 <code class="" data-line="">NormalizeDouble()</code> 修到商品的小數位，再拿修過的價差去算手數。這樣印出來的「風險」就是停損真的被打到時會賠的數，不會差一個尾數。tick size 比 _Point 粗的商品，價位還要再對齊 tick size。OnTick、找持倉、平倉那幾段沒改，檔案裡有，這篇不貼。</p>



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



<p class="wp-block-paragraph">EURUSD 2025 年 1 月，14 筆，和固定 0.10 手的 MaCrossTrade 訊號、進場價、停損距離逐筆相同，只有手數不一樣。EA 印的行（日誌裡的「tick」指 tick size、「每 tick」指 tick value）：</p>



<pre class="wp-block-prismatic-blocks"><code class="" data-line="">規格 EURUSD  最小 0.01 手  最大 100.00 手  步進 0.01 手
  合約 100000  tick 0.00001  每 tick 1.0000 USD
EA 啟動：每筆風險 1.0% 餘額，上限 1.00 手
開多 #2  0.34 手 @1.03042  停損 288 點  風險 97.92
開空 #4  0.26 手 @1.03545  停損 389 點  風險 101.14
開多 #6  0.27 手 @1.02483  停損 380 點  風險 102.60
開空 #8  0.36 手 @1.02889  停損 284 點  風險 102.24
平倉 #8  浮動盈虧 -28.08
開多 #10  0.31 手 @1.02967  停損 331 點  風險 102.61
平倉 #10  浮動盈虧 -37.20
開空 #12  0.64 手 @1.02847  停損 161 點  風險 103.04
平倉 #12  浮動盈虧 -79.36
開多 #14  0.61 手 @1.02971  停損 168 點  風險 102.48
開空 #16  0.33 手 @1.02864  停損 304 點  風險 100.32
平倉 #16  浮動盈虧 -59.07
開多 #18  0.38 手 @1.03043  停損 260 點  風險 98.80
開空 #20  0.48 手 @1.04039  停損 212 點  風險 101.76
開多 #22  0.30 手 @1.04355  停損 340 點  風險 102.00
開空 #24  0.30 手 @1.04372  停損 347 點  風險 104.10
開多 #26  0.30 手 @1.04316  停損 352 點  風險 105.60
開空 #28  0.24 手 @1.04074  停損 434 點  風險 104.16</code></pre>



<p class="wp-block-paragraph">手數從 0.24 到 0.64 手，每筆風險都在 97.92 到 105.60 塊之間。不是剛好 100 有兩個原因：捨去到步進通常會少一點；餘額會變，賺了 1% 跟著變大、賠了跟著縮，最後一筆算出來是 104.16。</p>



<p class="wp-block-paragraph">碰停損的 2 筆可以對帳：#14 開 0.61 手印風險 102.48，報告成交表那筆盈利欄是 -102.48；#26 開 0.30 手印 105.60，盈利欄 -105.60。一分不差，手續費另外算，那兩筆各是 3.66 和 1.80。</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>項目</th><th>固定 0.10 手</th><th>餘額 1%</th></tr></thead><tbody><tr><td>交易總計</td><td>14</td><td>14</td></tr><tr><td>總淨盈利 (USD)</td><td>237.30</td><td>656.73</td></tr><tr><td>最大權益數虧損</td><td>103.56 (1.02%)</td><td>441.64 (4.19%)</td></tr></tbody></table></figure>



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



<p class="wp-block-paragraph">賺比較多不是策略變好了，是平均開了 0.37 手、三倍多的部位，回撤也跟著放大到四倍。風險比例不會讓一個策略從賠變賺，它做的是讓每一筆賠的錢可以預期，回撤跟著風險比例走而不是跟著行情。</p>



<p class="wp-block-paragraph">1% 是常見的起點，連賠 10 筆掉一成；要 2% 還是 0.5%，看自己受得了多深的回撤。</p>



<h2 class="wp-block-heading">換商品、換資金、換杠杆</h2>



<p class="wp-block-paragraph">黃金同一個月：手數只有 0.07 到 0.11 手，因為停損動輒 930 到 1353 點、一點 1 塊。手數每差一個步進 0.01 手，賠的錢就差 9 到 14 塊，捨去之後風險落在 92.24 到 104.24，比 EURUSD 離目標遠。一步的錢大的商品就是這樣，帳戶小的時候更明顯。</p>



<p class="wp-block-paragraph">入金 300 塊跑 EURUSD：1% 是 3 塊，0.01 手停損 161 點要賠 1.61，開得成；停損 304 點以上的訊號 0.01 手就超過 3 塊，14 個訊號被擋掉 8 個，日誌印「連最小手數 0.01 手都不夠，這筆不做」。要改成硬開最小手數也行，那就要接受這種帳戶單筆賠到 1.4% 的事實。</p>



<p class="wp-block-paragraph">杠杆改 1:1、入金 10000：第一筆 0.34 手要押 35034.28，正好是 0.34 × 100000 × 進場價 1.03042，整個部位的價值；可用只有 10000，整個月 14 個訊號全部印「保證金不夠」，一筆都沒開。沒有這個檢查的話一樣一筆都開不了，只是換成伺服器拒單：固定 0.10 手的 MaCrossTrade 在 1:1 跑同一個月，日誌是 14 行「下單失敗 10019 not enough money」。結果相同，差在自己先算過就知道差多少。</p>



<p class="wp-block-paragraph">本篇檔案：<a href="https://orzalanluo.com/wp-content/uploads/2026/09/MaCrossRisk.mq5">MaCrossRisk.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/1879/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>MT5 策略測試器：報價模式、報告數字、參數優化與前向測試</title>
		<link>https://orzalanluo.com/archives/1872</link>
					<comments>https://orzalanluo.com/archives/1872#respond</comments>
		
		<dc:creator><![CDATA[alanluo]]></dc:creator>
		<pubDate>Tue, 15 Sep 2026 23:21:22 +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=1872</guid>

					<description><![CDATA[這篇不寫新程式。把均線交叉的 MaCrossTrade 放進策略測試器，從頭到尾走一遍：設定怎麼填、四種報價模 [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">這篇不寫新程式。把均線交叉的 MaCrossTrade 放進策略測試器，從頭到尾走一遍：設定怎麼填、四種報價模式跑出來差在哪、報告該看哪幾個數字、參數優化怎麼設、前向測試怎麼抓過度擬合。文中的數字全是我這台 MT5 實跑出來的。</p>



<p class="wp-block-paragraph">範例用 <a href="https://orzalanluo.com/wp-content/uploads/2026/09/MaCrossTrade.mq5">MaCrossTrade.mq5</a>（EMA 10／30 交叉、ATR 倍數停損停利），放到 <code class="" data-line="">MQL5\Experts\</code>、在 MetaEditor 按 <code class="" data-line="">F7</code> 編譯好就能跟著做。</p>



<h2 class="wp-block-heading">設定頁怎麼填</h2>



<p class="wp-block-paragraph">「查看」選單的「策略測試」或 <code class="" data-line="">Ctrl+R</code> 打開測試器。「概述」頁是幾個捷徑，「設置」頁才是完整的設定，這次用的值（編號對應下圖）：</p>



<ul class="wp-block-list">
<li>①專家：MaCrossTrade；交易品種：EURUSD；週期：H1。</li>



<li>②日期：自定義週期，2025.01.02 到 2025.01.31。</li>



<li>③延遲：零延遲，理想執行。先不模擬網路延遲。</li>



<li>④模式：每次報價。下一節講差別。</li>



<li>⑤入金：10000 USD；杠杆：1:100，保證金只要部位價值的 1%。</li>



<li>⑥優化：已禁用。「轉寄」是 Forward 的怪翻譯，就是前向測試，先選否，優化那節再用。</li>
</ul>



<figure class="wp-block-image size-full"><a href="https://orzalanluo.com/wp-content/uploads/mt5-05-tester-settings.png"><img decoding="async" width="1000" height="265" src="https://orzalanluo.com/wp-content/uploads/mt5-05-tester-settings.png" alt="MT5 策略測試器的「設置」頁：專家 MaCrossTrade、交易品種 EURUSD、週期 H1，日期自定義週期 2025.01.02 到 2025.01.31，轉寄否，延遲零延遲、理想執行，模式每次報價，入金 10000 USD、杠杆 1:100，優化已禁用。紅框和編號 1 到 6 標出這幾欄，轉寄和優化都標 6。" class="wp-image-1906" srcset="https://orzalanluo.com/wp-content/uploads/mt5-05-tester-settings.png 1000w, https://orzalanluo.com/wp-content/uploads/mt5-05-tester-settings-300x80.png 300w, https://orzalanluo.com/wp-content/uploads/mt5-05-tester-settings-644x171.png 644w" sizes="(max-width: 1000px) 100vw, 1000px" /></a><figcaption class="wp-element-caption">單次測試的設置頁：①～⑥對應上面清單，轉寄和優化兩格都是⑥</figcaption></figure>



<p class="wp-block-paragraph">「輸入」頁列出 EA 的 <code class="" data-line="">input</code> 參數，值可以直接改，不用重新編譯。按「開始」，跑完會自動切到結果頁。「日誌」頁是測試器和 EA 的 Print 輸出，出錯先看這裡。</p>



<h2 class="wp-block-heading">四種報價模式，同一隻 EA 四種結果</h2>



<p class="wp-block-paragraph">除了真實報價那種模式，測試器手上只有 K 線，最細到 1 分鐘，中間的每一筆報價（tick）是模擬出來的，差別在每根 K 線裡模擬幾個價。同一個月、同一隻 EA、四種模式（名稱照「模式」下拉）：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>模式</th><th>報價數</th><th>交易筆數</th><th>總淨盈利 (USD)</th><th>跑的時間</th></tr></thead><tbody><tr><td>每個報價基於真實報價</td><td>2,445,557</td><td>14</td><td>+184.83</td><td>2.3 秒</td></tr><tr><td>每次報價</td><td>2,282,861</td><td>14</td><td>+237.23</td><td>2.2 秒</td></tr><tr><td>1分鐘OHLC</td><td>119,487</td><td>14</td><td>+237.03</td><td>0.9 秒</td></tr><tr><td>僅使用開價</td><td>1,985</td><td>13</td><td>+194.53</td><td>0.7 秒</td></tr></tbody></table></figure>



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



<p class="wp-block-paragraph">「每次報價」從 1 分鐘 K 線的開高低收把中間的走法畫出來；「1分鐘OHLC」一分鐘只給開高低收四個價；「僅使用開價」一根 H1 也只給四個價：開盤在整點，最高、最低、收盤擠在那根結束前的最後幾秒，日誌裡停損停利都在 59 分 57、58 秒觸發。「每個報價基於真實報價」用券商真的記錄下來的每一筆報價，含當時的點差。要券商有提供 tick 歷史才有意義：缺 tick 的分鐘會退回每次報價模式模擬，報告的「質量歷史」就不會是「100% 真實報價」；我的 Tickmill 帳戶 2025 年 1 月的 EURUSD 是。</p>



<p class="wp-block-paragraph">三種模擬模式的進場訊號完全一樣，訊號在 K 線收盤判斷，每種模式都有那個價；差在停損停利。1分鐘OHLC 跟每次報價幾乎一樣，差的 0.2 塊是 2 筆停利成交價各差 1 點：有一分鐘的最高最低就知道碰沒碰到，只有停損停利近到同一分鐘內兩邊都碰得到時才會猜錯，這隻 EA 兩邊都離進場價兩倍 ATR 以上。</p>



<p class="wp-block-paragraph">僅使用開價少一筆：第一個訊號的 tick 落在週一 00:00，市場還沒開，下單被拒「市場關閉」，其他模式週一第一筆 tick 是 00:05。停損停利只在一小時四個價上檢查，一小時內先碰哪邊只能猜。</p>



<p class="wp-block-paragraph">真實報價差最多，兩個地方。週一開盤第一筆多單買在 1.03102 而不是 1.03042，Bid 一樣、Ask 差 60 點：那一分鐘每次報價模式用的點差是 34 點，真實報價是 94 點；不過那筆最後只差 0.1 塊，停利跟著進場價一起挪了。1 月 23 日那筆空單，每次報價模式 18:12 碰到停利，真實報價模式那時價格沒碰到、18:45 打到停損，一來一回差 53 塊，兩種模式的缺口幾乎全在這一筆。<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">模擬出來的 tick 只是「這一分鐘內可能的一條路」</mark>，不是真的走過的路。</p>



<p class="wp-block-paragraph">怎麼選：像這種只在收盤判斷、停損停利離得遠的策略，開發和優化用 1分鐘OHLC，快得多（這裡 2.2 秒對 0.9 秒）、結果幾乎一樣，定案前用每次報價或真實報價再跑一次確認；盤中移動停損、掛單這類靠 tick 的策略一開始就用每次報價。僅使用開價只適合完全在收盤判斷、不掛停損停利的策略。</p>



<h2 class="wp-block-heading">報告要看哪幾個數字</h2>



<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>237.23 USD</td><td>毛利減毛損，這個月賺的錢</td></tr><tr><td>交易總計</td><td>14</td><td>開了幾筆</td></tr><tr><td>盈利交易(%在全部)</td><td>8 (57.14%)</td><td>賺錢的筆數與勝率</td></tr><tr><td>盈利因子</td><td>3.19</td><td>毛利 ÷ 毛損，1 以下是賠錢的系統</td></tr><tr><td>預期收益</td><td>16.94</td><td>平均每筆賺多少</td></tr><tr><td>最大權益數虧損</td><td>103.56 (1.02%)</td><td>權益從高點掉下來最深的一次（金額與百分比）</td></tr><tr><td>採收率</td><td>2.29</td><td>總淨盈利 ÷ 最大回撤，賺的錢是回撤的幾倍</td></tr><tr><td>夏普比率</td><td>4.80</td><td>報酬除以波動，越高越平穩</td></tr></tbody></table></figure>



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



<p class="wp-block-paragraph">報告裡的「本日餘額」是 Balance 的怪翻譯，「最大本日餘額虧損」就是餘額回撤，只算已平倉的；「權益數」是 Equity，含浮動盈虧。看回撤看權益數那個。「柱」是 K 線數，「報價」是 tick 數。</p>



<p class="wp-block-paragraph">一個月 14 筆，夏普比率 4.80 這種數字沒有意義，樣本太少。報告要拉長到幾年、幾百筆才值得認真看，下面的優化就用整年。</p>



<h2 class="wp-block-heading">參數優化</h2>



<p class="wp-block-paragraph">「輸入」頁每個參數後面有開始、步長、停止三格和一個勾。勾起來的參數會在範圍裡逐一嘗試，沒勾的固定用「值」那格。我勾了四個：快線 5 到 20 步長 5、慢線 20 到 60 步長 10、停損 1 到 3 倍、停利 2 到 4 倍，後兩個步長 1，4×5×3×3 = 180 組。</p>



<figure class="wp-block-image size-full"><a href="https://orzalanluo.com/wp-content/uploads/mt5-05-tester-inputs.png"><img decoding="async" width="1000" height="257" src="https://orzalanluo.com/wp-content/uploads/mt5-05-tester-inputs.png" alt="策略測試器的「輸入」頁：快線期數 5 到 20 步長 5、慢線期數 20 到 60 步長 10、停損倍數 1 到 3、停利倍數 2 到 4（步長都是 1），這四個參數打勾，均線種類、ATR 期數、手數、魔術碼沒勾。紅框標出開始／步長／停止三欄（1）、勾選欄（2）和右下角的總組數 180（3）。" class="wp-image-1907" srcset="https://orzalanluo.com/wp-content/uploads/mt5-05-tester-inputs.png 1000w, https://orzalanluo.com/wp-content/uploads/mt5-05-tester-inputs-300x77.png 300w, https://orzalanluo.com/wp-content/uploads/mt5-05-tester-inputs-644x166.png 644w" sizes="(max-width: 1000px) 100vw, 1000px" /></a><figcaption class="wp-element-caption">輸入頁：①開始／步長／停止 ②勾選要優化的參數 ③總組數 180</figcaption></figure>



<p class="wp-block-paragraph">手數和魔術碼不該優化，把勾拿掉；更乾淨的做法是在程式裡把 <code class="" data-line="">input</code> 改成 <code class="" data-line="">sinput</code>，輸入頁還看得到、值照改，只是開始、步長、停止那幾格和勾都不能用，優化結果也不會有這一欄（實測過：標了 sinput 的參數就算給了範圍，測試器也只跑其他參數的組合）：</p>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">sinput double i_Lots  = 0.10;     // 手數：不優化
sinput long   i_Magic = 20250401; // 魔術碼：不優化</code></pre>



<p class="wp-block-paragraph">「設置」頁的優化選「完整算法」，每一組都跑；組數多到跑不完時才用「基於快速遺傳算法」，它只往有希望的方向試。旁邊的標準是排名依據，用預設的「最大本日餘額」，也就是最終餘額最高。日期改成 2024 整年，轉寄那格下一節講；模式我還是用每次報價，求穩。按「開始」，180 組在我的 6 個代理並行跑了 8 分 45 秒，每一組平均 10 秒（加上下一節的前向 180 趟，全部 11 分 27 秒）；跑完看「優化結果」頁，可以點欄位標題排序。</p>



<figure class="wp-block-image size-full"><a href="https://orzalanluo.com/wp-content/uploads/mt5-05-tester-optresult.png"><img decoding="async" width="1000" height="280" src="https://orzalanluo.com/wp-content/uploads/mt5-05-tester-optresult.png" alt="「優化結果」頁依「結果」由高到低排序，第一列 20／30／1／3：結果 10010.91、盈利 10.91、157 筆交易，下面 9 列是快慢線同為 20、沒有交易的組合。這是重跑拍的，內文那趟這組盈利是 +12.23，右下角耗時也比內文長。" class="wp-image-1908" srcset="https://orzalanluo.com/wp-content/uploads/mt5-05-tester-optresult.png 1000w, https://orzalanluo.com/wp-content/uploads/mt5-05-tester-optresult-300x84.png 300w, https://orzalanluo.com/wp-content/uploads/mt5-05-tester-optresult-644x180.png 644w" sizes="(max-width: 1000px) 100vw, 1000px" /></a><figcaption class="wp-element-caption">依「結果」排序，20／30／1／3 唯一賺錢；重跑拍的，金額略有不同、耗時較長</figcaption></figure>



<p class="wp-block-paragraph">2024 年的結果：180 組只有 1 組賺錢，+12.23（快線 20、慢線 30、停損 1 倍、停利 3 倍，157 筆）；9 組快慢線同為 20，沒交易；其餘全賠，最差賠 1,277.77。均線交叉在 2024 年的 EURUSD H1 上，這 180 組裡沒有哪組救得了。這才是優化的用途：<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">不是找「最好的參數」，是看整片參數空間長什麼樣</mark>。</p>



<h2 class="wp-block-heading">前向測試：把優化出來的參數往後跑</h2>



<p class="wp-block-paragraph">「設置」頁的「轉寄」把測試期切成兩段：前段優化，後段（前向時期）拿優化出來最好的那批參數原封不動再跑一次：少於 256 組時全部都跑，這次 180 組全跑了，更多時只跑前 10%（遺傳算法是前 25%）。切法可以選 1/2、1/3、1/4 或自定義日期。我用自定義：2024 整年優化，2025 上半年前向。</p>



<figure class="wp-block-image size-full"><a href="https://orzalanluo.com/wp-content/uploads/mt5-05-tester-settings-opt.png"><img decoding="async" width="1000" height="266" src="https://orzalanluo.com/wp-content/uploads/mt5-05-tester-settings-opt.png" alt="優化加前向的「設置」頁：日期自定義週期 2024.01.01 到 2025.06.30，轉寄選自定義、2025.01.01，模式每次報價，優化選完整算法、標準最大本日餘額。紅框和編號 1 到 3 標出日期、轉寄、優化三列。" class="wp-image-1909" srcset="https://orzalanluo.com/wp-content/uploads/mt5-05-tester-settings-opt.png 1000w, https://orzalanluo.com/wp-content/uploads/mt5-05-tester-settings-opt-300x80.png 300w, https://orzalanluo.com/wp-content/uploads/mt5-05-tester-settings-opt-644x171.png 644w" sizes="(max-width: 1000px) 100vw, 1000px" /></a><figcaption class="wp-element-caption">①日期涵蓋兩段 ②轉寄：2025.01.01 起為前向 ③完整算法、最大本日餘額</figcaption></figure>



<p class="wp-block-paragraph">跑完多一個「向前結果」頁，每一組並排前段和後段的成績（下表參數欄是快線 / 慢線 / 停損倍數 / 停利倍數）：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>參數</th><th>2024 盈利（優化段）</th><th>2025 上半年盈利（前向段）</th><th>前向筆數</th></tr></thead><tbody><tr><td>20 / 30 / 1 / 3，2024 第 1 名</td><td>+12.23</td><td>-41.52</td><td>75</td></tr><tr><td>10 / 50 / 2 / 4</td><td>-793.81</td><td>+746.74</td><td>69</td></tr><tr><td>10 / 50 / 3 / 4</td><td>-891.83</td><td>+631.84</td><td>69</td></tr><tr><td>15 / 20 / 3 / 2</td><td>-851.19</td><td>+515.01</td><td>107</td></tr><tr><td>10 / 30 / 2 / 3，預設值</td><td>-1,090.91</td><td>+206.24</td><td>109</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-05-tester-forward.png"><img decoding="async" width="1000" height="315" src="https://orzalanluo.com/wp-content/uploads/mt5-05-tester-forward.png" alt="「向前結果」頁依「前臺」由高到低排序，第一列 10／50／2／4：前臺（前向段最終餘額）10746.08、後臺測試（優化段最終餘額）9203.42、盈利 746.08、69 筆交易。這是重跑拍的，內文表格是較早那趟的 +746.74 和 -793.81。" class="wp-image-1910" srcset="https://orzalanluo.com/wp-content/uploads/mt5-05-tester-forward.png 1000w, https://orzalanluo.com/wp-content/uploads/mt5-05-tester-forward-300x95.png 300w, https://orzalanluo.com/wp-content/uploads/mt5-05-tester-forward-644x203.png 644w" sizes="(max-width: 1000px) 100vw, 1000px" /></a><figcaption class="wp-element-caption">「前臺」是前向段、「後臺測試」是優化段的最終餘額；重跑拍的，金額略有不同</figcaption></figure>



<p class="wp-block-paragraph">2024 唯一賺錢的那組，2025 上半年賠 41.52；2025 上半年賺最多的那組 +746.74，2024 賠 793.81。兩段的排名沒有關係。2024 年 180 組只有 1 組賺，2025 上半年 96 組賺：賺不賺錢主要是那段行情決定的，不是參數。</p>



<p class="wp-block-paragraph">這叫過度擬合：在一段歷史裡挑出來的「最佳參數」，只是最貼合那段走勢的參數，不代表下一段還貼合。前向測試是最便宜的檢查：優化完的參數，往後那段沒跑過的資料還賺，才算數。判斷時看整片，一組參數附近的鄰居都賺，比單一組冠軍可信；前向冠軍一樣是回頭挑出來的，不能拿它當下一段的答案。</p>



<h2 class="wp-block-heading">回測還會在哪裡騙你</h2>



<ul class="wp-block-list">
<li>歷史資料。報告的「質量歷史」不是 100% 就有缺口。券商早期的資料常常只有用日線填出來的 1 分鐘 K，我這家 2019 年以前就是這樣，盤中策略的回測我從 2019 年起算。</li>



<li>隔夜利息和合約規格用的是跑回測那天券商給的，不是行情當時的；點差倒是歷史的，存在每根 1 分鐘 K 裡，真實報價模式更是每一筆 tick 的。同一份設定、同一個月，我 9 月 15 日跑總淨盈利 237.28，16 日重跑是 237.23，成交價、手續費、每筆盈利逐筆相同，差的 0.05 塊全在隔夜利息欄。券商一調隔夜利息，換一天重跑總淨盈利就跟著變，不是哪裡錯了。</li>



<li>樣本。一個月十幾筆什麼都證明不了；至少幾年、幾百筆，再加前向。</li>
</ul>



<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/1872/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>MQL5 下單與持倉管理：CTrade、魔術碼、停損停利</title>
		<link>https://orzalanluo.com/archives/1869</link>
					<comments>https://orzalanluo.com/archives/1869#respond</comments>
		
		<dc:creator><![CDATA[alanluo]]></dc:creator>
		<pubDate>Tue, 15 Sep 2026 15:00:27 +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=1869</guid>

					<description><![CDATA[這篇講 EA 怎麼下單：用 MT5 標準程式庫的 CTrade 類別開倉、帶停損停利、平倉，還有 EA 怎麼從 [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">這篇講 EA 怎麼下單：用 MT5 標準程式庫的 <code class="" data-line="">CTrade</code> 類別開倉、帶停損停利、平倉，還有 EA 怎麼從一堆持倉裡認出自己的那一筆。範例是均線交叉：黃金交叉做多、死亡交叉做空，反向訊號來就先平掉再開。</p>



<p class="wp-block-paragraph">整份程式在我的 MT5 上編譯過、用策略測試器跑過 2025 年 1 月的 EURUSD H1，文中日誌和數字是實際輸出。可以先<a href="https://orzalanluo.com/wp-content/uploads/2026/09/MaCrossTrade.mq5">下載 MaCrossTrade.mq5</a> 跟著跑。它會真的下單，請掛在模擬帳戶上。</p>



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



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



<li>策略測試器 <code class="" data-line="">Ctrl+R</code>：EA 選 MaCrossTrade，EURUSD，H1，日期 2025.01.02 到 2025.01.31，模式「每次報價」，入金 10000，按「開始」。這隻 EA 有停損停利，<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">最後驗證要用「每次報價」</mark>：「1分鐘OHLC」一分鐘只有開高低收四個價，「僅使用開價」一根圖表週期的 K 線只有少數幾個價（這裡一根 H1 四個），停損停利只在這些價位上檢查，碰到的時間和價格可能不一樣。像這隻只在收盤判斷、停損停利離得遠的，開發時可以先用 1分鐘OHLC 求快，定案前再用每次報價跑一次。</li>



<li>跑完看報告的「總淨盈利」「交易總計」，「日誌」頁有每一筆開倉、平倉的紀錄。</li>



<li>要掛在模擬帳戶的圖表上跑，拖上去時跳出的視窗裡「允許演算法交易」要勾，工具列的「演算法交易」（<code class="" data-line="">Ctrl+E</code>）也要開著，EA 才能下單。</li>
</ol>



<h2 class="wp-block-heading">1. 參數與交易物件</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 1. 參數與交易物件 ==============================================
#include &lt;Trade\Trade.mqh&gt;                   // 標準程式庫的交易類別
#include &lt;Indicators\Trend.mqh&gt;              // CiMA
#include &lt;Indicators\Oscilators.mqh&gt;         // CiATR

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 double         i_Lots       = 0.10;     // 手數
input double         i_SlAtr      = 2.0;      // 停損 = ATR x 倍數
input double         i_TpAtr      = 3.0;      // 停利 = ATR x 倍數
input long           i_Magic      = 20250401; // 魔術碼

CTrade   g_Trade;                         // 下單、平倉都透過它
CiMA     g_Fast;                          // 指標物件，handle 包在裡面
CiMA     g_Slow;
CiATR    g_Atr;
datetime g_LastBarTime = 0;</code></pre>



<p class="wp-block-paragraph"><code class="" data-line="">#include &lt;Trade\Trade.mqh&gt;</code> 把 MT5 附的 CTrade 類別拉進來。下單在 MQL5 底層是填一個 <code class="" data-line="">MqlTradeRequest</code> 結構、呼叫 <code class="" data-line="">OrderSend()</code>、再看回傳的結構，欄位十幾個；CTrade 把這些包起來，一行 <code class="" data-line="">Buy()</code> 就搞定。宣告成全域變數 <code class="" data-line="">g_Trade</code>，整隻 EA 共用一個，之後都用 <code class="" data-line="">g_Trade.Buy()</code> 這種「物件.函數()」的寫法叫它做事。</p>



<p class="wp-block-paragraph">停損停利（MT5 介面叫「止損」「止盈」，程式裡是 SL、TP）不用固定點數，用 ATR 的倍數：波動大，距離自動拉開。手數固定 0.10 手，先不管資金管理；EURUSD 1 手的合約大小是 100000 歐元，0.10 手就是 10000 歐元。</p>



<p class="wp-block-paragraph">做多是先買後賣，價格漲了賺；做空是先賣後買，跌了賺。開多買在 Ask、平多賣在 Bid，開空賣在 Bid、平空買在 Ask，所以多單的停損停利由 Bid 觸發，空單由 Ask 觸發。</p>



<figure class="wp-block-image size-full"><a href="https://orzalanluo.com/wp-content/uploads/mt5-04-ask-bid-sl-tp.png"><img decoding="async" width="1000" height="830" src="https://orzalanluo.com/wp-content/uploads/mt5-04-ask-bid-sl-tp.png" alt="示意圖，左欄多單、右欄空單，Ask 在上、Bid 在下，中間是點差。多單開倉買在 Ask、停利在上停損在下，平倉賣在 Bid、停損停利看 Bid 碰到；空單開倉賣在 Bid、停損在上停利在下，平倉買在 Ask、看 Ask 碰到。" class="wp-image-1905" srcset="https://orzalanluo.com/wp-content/uploads/mt5-04-ask-bid-sl-tp.png 1000w, https://orzalanluo.com/wp-content/uploads/mt5-04-ask-bid-sl-tp-300x249.png 300w, https://orzalanluo.com/wp-content/uploads/mt5-04-ask-bid-sl-tp-644x535.png 644w" sizes="(max-width: 1000px) 100vw, 1000px" /></a><figcaption class="wp-element-caption">停損停利看平倉用的價：多單平倉賣在 Bid、空單平倉買在 Ask</figcaption></figure>



<p class="wp-block-paragraph"><code class="" data-line="">i_Magic</code> 是魔術碼（magic number）：EA 送出的每張單都帶這個號碼，之後從一堆持倉裡認出自己的就靠它。同一個帳戶跑兩隻 EA、或同一隻 EA 掛在兩個週期，<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">魔術碼要不一樣</mark>，不然會把對方的單當成自己的平掉。手動下的單魔術碼是 0。</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(!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;
   }

   g_Trade.SetExpertMagicNumber(i_Magic);    // 之後每張單都帶這個號碼
   g_Trade.SetDeviationInPoints(20);         // 能接受的滑點（點）
   g_Trade.SetTypeFillingBySymbol(_Symbol);  // 成交方式照商品規格
   Print(&quot;EA 啟動：&quot;, DoubleToString(i_Lots, 2), &quot; 手，停損 &quot;,
         i_SlAtr, &quot; 倍 ATR，停利 &quot;, i_TpAtr, &quot; 倍 ATR，魔術碼 &quot;,
         i_Magic);
   return INIT_SUCCEEDED;
}</code></pre>



<p class="wp-block-paragraph">三個指標物件在這裡 <code class="" data-line="">Create()</code>，後面三行設定 <code class="" data-line="">g_Trade</code>。OnDeinit 和 IsNewBar 兩段檔案裡有，這篇不貼。</p>



<p class="wp-block-paragraph"><code class="" data-line="">SetExpertMagicNumber()</code> 之後每張單自動帶魔術碼。<code class="" data-line="">SetDeviationInPoints()</code> 是能接受的滑點（點），只有「即時執行」和更老的「執行請求」模式的商品用得到：報價差超過這個值，券商會回「重新報價」（requote）、不成交。Tickmill 的 EURUSD 是「市場執行」，券商直接照當下市價成交，這個值不起作用。<code class="" data-line="">SetTypeFillingBySymbol()</code> 依商品規格挑成交方式（FOK 或 IOC，Tickmill 是 IOC）。</p>



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



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 3. 找自己的持倉 ================================================
// 回傳 POSITION_TYPE_BUY 或 POSITION_TYPE_SELL，沒有就回 -1
// 找到的持倉會處於「已選取」狀態，ticket 由參數帶回
int FindMyPosition(ulong &amp;ticket)
{
   for(int i = PositionsTotal() - 1; i &gt;= 0; i--)
   {
      ulong t = PositionGetTicket(i);          // 選取第 i 個持倉
      if(t == 0)
         continue;
      if(PositionGetString(POSITION_SYMBOL) != _Symbol)
         continue;                             // 別的商品
      if(PositionGetInteger(POSITION_MAGIC) != i_Magic)
         continue;                             // 別的 EA 或手動單
      ticket = t;
      return (int)PositionGetInteger(POSITION_TYPE);
   }
   return -1;
}

void ShowStatus()
{
   ulong ticket = 0;
   int   have   = FindMyPosition(ticket);
   if(have &lt; 0)
   {
      Comment(&quot;沒有持倉&quot;);
      return;
   }
   double open   = PositionGetDouble(POSITION_PRICE_OPEN);
   double sl     = PositionGetDouble(POSITION_SL);
   double tp     = PositionGetDouble(POSITION_TP);
   double profit = PositionGetDouble(POSITION_PROFIT);
   Comment(have == POSITION_TYPE_BUY ? &quot;多單&quot; : &quot;空單&quot;, &quot; #&quot;, ticket,
           &quot;  開倉價 &quot;, DoubleToString(open, _Digits),
           &quot;\nSL &quot;, DoubleToString(sl, _Digits),
           &quot;  TP &quot;, DoubleToString(tp, _Digits),
           &quot;\n浮動盈虧 &quot;, DoubleToString(profit, 2));
}</code></pre>



<p class="wp-block-paragraph">從 MT4 過來最容易搞混的地方在這裡。MT4 一張單就是一筆部位，<code class="" data-line="">OrderSelect()</code> 掃過去就好。MT5 分三層：你送出的是訂單（order），成交後留下成交紀錄（deal），手上真正的部位叫持倉（position）。要知道現在手上有什麼，看持倉。</p>



<p class="wp-block-paragraph"><code class="" data-line="">PositionsTotal()</code> 是帳戶目前的持倉數，所有商品、所有 EA、手動單全算在內。<code class="" data-line="">PositionGetTicket(i)</code> 選取第 i 筆並回傳它的編號（ticket），選取之後 <code class="" data-line="">PositionGetString()</code>、<code class="" data-line="">PositionGetInteger()</code>、<code class="" data-line="">PositionGetDouble()</code> 讀的都是這一筆。商品跟魔術碼都對得上，才是自己的。這隻 EA 一次只留一筆，找到就回傳。</p>



<p class="wp-block-paragraph"><code class="" data-line="">ShowStatus()</code> 把那筆的開倉價、SL、TP、浮動盈虧顯示在圖表左上角，每個 tick 更新。策略測試器不是視覺化模式看不到，掛在圖表上才有。</p>



<h2 class="wp-block-heading">4. 開倉：市價單帶停損停利</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 4. 開倉：市價單帶停損停利 ======================================
bool OpenPosition(int dir, double atr)
{
   double ask     = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double bid     = SymbolInfoDouble(_Symbol, SYMBOL_BID);
   double slDist  = atr * i_SlAtr;
   double tpDist  = atr * i_TpAtr;
   long stops = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
   double minDist = stops * _Point + (ask - bid);  // 最小距離＋點差
   if(slDist &lt; minDist) slDist = minDist;
   if(tpDist &lt; minDist) tpDist = minDist;

   bool ok;
   if(dir == POSITION_TYPE_BUY)
      ok = g_Trade.Buy(i_Lots, _Symbol, 0,    // 0 = 用現在的 Ask
                       NormalizeDouble(ask - slDist, _Digits),
                       NormalizeDouble(ask + tpDist, _Digits));
   else
      ok = g_Trade.Sell(i_Lots, _Symbol, 0,   // 0 = 用現在的 Bid
                        NormalizeDouble(bid + slDist, _Digits),
                        NormalizeDouble(bid - tpDist, _Digits));

   if(!ok || g_Trade.ResultRetcode() != TRADE_RETCODE_DONE)
   {
      Print(&quot;下單失敗 &quot;, g_Trade.ResultRetcode(), &quot; &quot;,
            g_Trade.ResultRetcodeDescription());
      return false;
   }
   Print(dir == POSITION_TYPE_BUY ? &quot;開多&quot; : &quot;開空&quot;,
         &quot; #&quot;, g_Trade.ResultOrder(),
         &quot;  &quot;, DoubleToString(i_Lots, 2), &quot; 手 @&quot;,
         DoubleToString(g_Trade.ResultPrice(), _Digits),
         &quot;  SL &quot;, DoubleToString(g_Trade.RequestSL(), _Digits),
         &quot;  TP &quot;, DoubleToString(g_Trade.RequestTP(), _Digits));
   return true;
}</code></pre>



<p class="wp-block-paragraph">多單的停損放在 Ask 下面 2 倍 ATR、停利放上面 3 倍 ATR；空單用 Bid 算，方向反過來。算出來的價格用 <code class="" data-line="">NormalizeDouble()</code> 修到商品的小數位數，多幾位小數券商會拒。</p>



<p class="wp-block-paragraph"><code class="" data-line="">SYMBOL_TRADE_STOPS_LEVEL</code> 是券商規定停損停利離現價的最小距離（點），比它近會被拒，錯誤碼 10016。多單停損由 Bid 檢查、我們從 Ask 算，所以最小距離再加上點差。EURUSD 在我的 Tickmill 帳戶是 0，別的商品或券商不一定，所以還是擋一下。</p>



<p class="wp-block-paragraph"><code class="" data-line="">g_Trade.Buy(手數, 商品, 價格, 停損, 停利)</code>，價格填 0 就用當下的 Ask。<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">回傳 true 只代表請求送到伺服器</mark>，有沒有成交要看 <code class="" data-line="">ResultRetcode()</code>：10009（<code class="" data-line="">TRADE_RETCODE_DONE</code>）是全部成交，10010（<code class="" data-line="">TRADE_RETCODE_DONE_PARTIAL</code>）是只成交一部分，這時也已經有倉了。MaCrossTrade.mq5 只認 10009，遇到 10010 會印「下單失敗」，但成交的部分已是帶魔術碼的持倉，<code class="" data-line="">FindMyPosition()</code> 找得到：同方向訊號不再加碼，反向訊號照樣平掉。失敗時 <code class="" data-line="">ResultRetcodeDescription()</code> 會給一句英文說明，印出來就知道被拒的原因。</p>



<p class="wp-block-paragraph">成交後 <code class="" data-line="">ResultOrder()</code> 是訂單編號、<code class="" data-line="">ResultPrice()</code> 是實際成交價，<code class="" data-line="">RequestSL()</code>、<code class="" data-line="">RequestTP()</code> 是剛才送出去的值。在策略測試器裡 CTrade 自己還會多印一行 <code class="" data-line="">CTrade::OrderSend: market buy ...</code>；掛在圖表上預設只有失敗才印。</p>



<h2 class="wp-block-heading">5. OnTick：訊號來了，先平反向、再開新倉</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 5. OnTick：訊號來了，先平反向、再開新倉 ========================
void OnTick()
{
   ShowStatus();                    // 每個 tick 更新左上角
   if(!IsNewBar())
      return;

   g_Fast.Refresh();                // 抓最新指標值，再用 Main(i) 讀
   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_LastBarTime = 0;
      return;
   }
   bool goldenCross = fast2 &lt;= slow2 &amp;&amp; fast1 &gt; slow1;
   bool deadCross   = fast2 &gt;= slow2 &amp;&amp; fast1 &lt; slow1;
   if(!goldenCross &amp;&amp; !deadCross)
      return;
   int dir = goldenCross ? POSITION_TYPE_BUY : POSITION_TYPE_SELL;

   ulong ticket = 0;
   int   have   = FindMyPosition(ticket);
   if(have == dir)
      return;                       // 同方向已經有了，不加碼
   if(have &gt;= 0)                    // 反向的先平掉
   {
      double profit = PositionGetDouble(POSITION_PROFIT);
      if(!g_Trade.PositionClose(ticket) ||
         g_Trade.ResultRetcode() != TRADE_RETCODE_DONE)
      {
         Print(&quot;平倉失敗 &quot;, g_Trade.ResultRetcode(), &quot; &quot;,
               g_Trade.ResultRetcodeDescription());
         return;
      }
      Print(&quot;平倉 #&quot;, ticket, &quot;  浮動盈虧 &quot;,
            DoubleToString(profit, 2));
   }
   OpenPosition(dir, atr1);
}</code></pre>



<p class="wp-block-paragraph">先 <code class="" data-line="">Refresh()</code> 再用 <code class="" data-line="">Main(1)</code>、<code class="" data-line="">Main(2)</code> 讀已收盤的兩根，不看還在跳的 <code class="" data-line="">Main(0)</code>。有訊號才去查持倉：同方向已經有一筆就不加碼；有反向的就先 <code class="" data-line="">PositionClose(ticket)</code> 平掉再開新的。CTrade 會自己送一張反向市價單把它平掉，不用算方向，成交與否一樣看 <code class="" data-line="">ResultRetcode()</code>。</p>



<p class="wp-block-paragraph">平倉前先讀 <code class="" data-line="">POSITION_PROFIT</code>，那是平倉前一刻的浮動盈虧，單位是帳戶幣別，這裡是 USD。它就是工具箱「歷史」頁盈利欄那個數：測試器裡完全一樣，實盤平倉價可能差一點滑點；手續費、隔夜利息是另外兩欄。</p>



<p class="wp-block-paragraph">停損停利是券商伺服器在盯，價格碰到就平，EA 關掉、電腦關機也照樣觸發。所以 EA 只印開倉和反向平倉，停損停利觸發的紀錄是測試器自己印的。</p>



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



<p class="wp-block-paragraph">2025 年 1 月，14 筆交易。只列 EA 自己 Print() 的行，省略時間欄和 CTrade、測試器自己印的紀錄：</p>



<pre class="wp-block-prismatic-blocks"><code class="" data-line="">EA 啟動：0.10 手，停損 2.0 倍 ATR，停利 3.0 倍 ATR，魔術碼 20250401
開多 #2  0.10 手 @1.03042  SL 1.02754  TP 1.03473
開空 #4  0.10 手 @1.03545  SL 1.03934  TP 1.02961
開多 #6  0.10 手 @1.02483  SL 1.02103  TP 1.03053
開空 #8  0.10 手 @1.02889  SL 1.03173  TP 1.02463
平倉 #8  浮動盈虧 -7.80
開多 #10  0.10 手 @1.02967  SL 1.02636  TP 1.03464
平倉 #10  浮動盈虧 -12.00
開空 #12  0.10 手 @1.02847  SL 1.03008  TP 1.02606
平倉 #12  浮動盈虧 -12.40
開多 #14  0.10 手 @1.02971  SL 1.02803  TP 1.03222
開空 #16  0.10 手 @1.02864  SL 1.03168  TP 1.02408
平倉 #16  浮動盈虧 -17.90
開多 #18  0.10 手 @1.03043  SL 1.02783  TP 1.03432
開空 #20  0.10 手 @1.04039  SL 1.04251  TP 1.03721
開多 #22  0.10 手 @1.04355  SL 1.04015  TP 1.04865
開空 #24  0.10 手 @1.04372  SL 1.04719  TP 1.03852
開多 #26  0.10 手 @1.04316  SL 1.03964  TP 1.04844
開空 #28  0.10 手 @1.04074  SL 1.04508  TP 1.03422</code></pre>



<p class="wp-block-paragraph">持倉編號通常就是開倉那張訂單的編號，所以開空 #8 之後平倉也叫 #8；日誌裡跳過的奇數是平倉那些訂單的編號。開倉後沒有「平倉」行的，就是被停損停利收掉的，最後一筆 #28 是測試結束時強制平掉。</p>



<p class="wp-block-paragraph">14 筆的結局：7 筆停利、2 筆停損、4 筆被反向訊號平掉、最後 1 筆測試結束時強制平倉。報告的重點：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>項目</th><th>數值</th></tr></thead><tbody><tr><td>交易總計</td><td>14</td></tr><tr><td>盈利交易</td><td>8 (57.14%)</td></tr><tr><td>總淨盈利</td><td>237.30 USD</td></tr></tbody></table></figure>



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



<p class="wp-block-paragraph">一個月 14 筆什麼都證明不了，有賺運氣成分居多。看得出來的是停利 3 倍、停損 2 倍 ATR 的效果：勝率四成就打平（2 ÷ (2 + 3) ＝ 40%），這個月 57% 所以有賺。</p>



<p class="wp-block-paragraph">1 月 16 到 17 日盤整那段開的 5 筆全賠：4 筆被反向訊號平掉、賠 7.8 到 17.9 塊，1 筆碰停損。均線交叉在盤整時就是這樣，兩條線黏在一起反覆穿越。</p>



<h2 class="wp-block-heading">鎖倉帳戶與單邊帳戶</h2>



<p class="wp-block-paragraph">MT5 帳戶有兩種模式。鎖倉（hedging）可以同一個商品同時持有多單和空單，各自是一筆持倉；單邊（netting）一個商品只有一筆持倉，反向下單直接抵銷。我的 Tickmill 帳戶是鎖倉，開模擬帳戶時的「使用鎖倉交易」選項就是這個。這篇的寫法是先平反向再開新倉，兩種模式跑出來都一樣；如果省掉平倉直接反向下單，鎖倉帳戶會變成多空各一筆、單邊帳戶會變成沒部位，寫 EA 之前要知道自己在哪一種上面。</p>



<h2 class="wp-block-heading">常見的錯誤碼</h2>



<ul class="wp-block-list">
<li>10009 <code class="" data-line="">TRADE_RETCODE_DONE</code>：全部成交</li>



<li>10004 <code class="" data-line="">TRADE_RETCODE_REQUOTE</code>：重新報價（市場執行的商品不會遇到）</li>



<li>10014 <code class="" data-line="">TRADE_RETCODE_INVALID_VOLUME</code>：手數不合規格（低於最小手數、不是步進的倍數）</li>



<li>10016 <code class="" data-line="">TRADE_RETCODE_INVALID_STOPS</code>：停損停利價位不合法（太近、方向反了）</li>



<li>10018 <code class="" data-line="">TRADE_RETCODE_MARKET_CLOSED</code>：市場關閉</li>



<li>10019 <code class="" data-line="">TRADE_RETCODE_NO_MONEY</code>：保證金不夠</li>



<li>10027 <code class="" data-line="">TRADE_RETCODE_CLIENT_DISABLES_AT</code>：終端的「演算法交易」沒開</li>
</ul>



<p class="wp-block-paragraph">全部的代碼在 MetaEditor 按 <code class="" data-line="">F1</code> 搜 Trade Server Return Codes。</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/1869/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>MQL5 技術指標：iMA() handle 或 CiMA 類別，用均線交叉做第一個訊號</title>
		<link>https://orzalanluo.com/archives/1864</link>
					<comments>https://orzalanluo.com/archives/1864#respond</comments>
		
		<dc:creator><![CDATA[alanluo]]></dc:creator>
		<pubDate>Mon, 14 Sep 2026 23:05:55 +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=1864</guid>

					<description><![CDATA[均線、ATR、RSI 這些技術指標，MT5 不是直接給你一個數字：要先跟它「登記」一個指標拿到 handle， [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">均線、ATR、RSI 這些技術指標，MT5 不是直接給你一個數字：要先跟它「登記」一個指標拿到 handle，再用 <code class="" data-line="">CopyBuffer()</code> 把數值抓進陣列。MT5 附的標準程式庫另外有 <code class="" data-line="">CiMA</code>、<code class="" data-line="">CiATR</code> 這些類別，把登記和抓值包成物件。這篇用兩條 EMA（比起每根等權的 SMA，近期價格權重較大、反應較快）加一個 ATR 把兩種寫法都走一遍，順便做出第一個交易訊號：均線交叉。EA 只印訊息、不下單。</p>



<p class="wp-block-paragraph">範例是 EA 不是腳本，因為訊號要一根一根 K 線判斷，得靠 OnTick。兩個檔案：MaCrossSignal.mq5 直接用 handle，MaCrossSignalCi.mq5 用類別，訊號邏輯一樣。兩份都在我的 MT5 上編譯過、用策略測試器跑過 2025 年 1 月的 EURUSD H1，文中日誌是實際輸出，兩個檔案跑出來逐字相同。可以先<a href="https://orzalanluo.com/wp-content/uploads/2026/09/MaCrossSignal.mq5">下載 MaCrossSignal.mq5</a> 跟著跑。</p>



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



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



<li>掛到 EURUSD H1 圖表上。左上角會顯示上一根的快線、慢線、ATR；均線交叉的那根 K 線收完，「專家」頁會印一行。盤中要等到交叉可能要幾個小時。</li>



<li>不想等就用策略測試器：<code class="" data-line="">Ctrl+R</code>，EA 選 MaCrossSignal，EURUSD，H1，日期 2025.01.02 到 2025.01.31，模式「每次報價」，按「開始」，切到「日誌」頁。這隻 EA 一根 K 線只算一次，換別的模式訊號也一樣，只差印出的時間。</li>
</ol>



<h2 class="wp-block-heading">1. 參數與 handle</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 1. 參數與 handle ===============================================
input int            i_FastPeriod = 10;       // 快線期數
input int            i_SlowPeriod = 30;       // 慢線期數
input ENUM_MA_METHOD i_MaMethod   = MODE_EMA; // 均線種類
input int            i_AtrPeriod  = 14;       // ATR 期數

int      g_FastHandle  = INVALID_HANDLE;  // 指標 handle 就是一個整數
int      g_SlowHandle  = INVALID_HANDLE;
int      g_AtrHandle   = INVALID_HANDLE;
datetime g_LastBarTime = 0;               // 上次處理過的 K 線時間
int      g_Signals     = 0;               // 訊號計數</code></pre>



<p class="wp-block-paragraph"><code class="" data-line="">iMA()</code>、<code class="" data-line="">iATR()</code> 這類函數回傳的不是數值，是一個整數，叫 handle。可以把它想成掛號單：MT5 收到之後在背景幫你算這個指標，你之後拿號碼來要數值。跟 MT4 差最多的就是這裡，MT4 的 <code class="" data-line="">iMA(..., shift)</code> 是直接回傳那一根的均線值。</p>



<p class="wp-block-paragraph">handle 放在全域變數，初始值 <code class="" data-line="">INVALID_HANDLE</code>（其實就是 -1），建立失敗時才分得出來。<code class="" data-line="">g_LastBarTime</code> 記上次處理過的 K 線時間，第 4 段會用到。<code class="" data-line="">ENUM_MA_METHOD</code> 是 MT5 內建的列舉，直接拿來當 input 就是 SMA、EMA 等四種均線的下拉選單。</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()
{
   g_FastHandle = iMA(_Symbol, _Period, i_FastPeriod, 0,
                      i_MaMethod, PRICE_CLOSE);
   g_SlowHandle = iMA(_Symbol, _Period, i_SlowPeriod, 0,
                      i_MaMethod, PRICE_CLOSE);
   g_AtrHandle  = iATR(_Symbol, _Period, i_AtrPeriod);
   if(g_FastHandle == INVALID_HANDLE ||
      g_SlowHandle == INVALID_HANDLE ||
      g_AtrHandle  == INVALID_HANDLE)
   {
      Print(&quot;指標建立失敗，錯誤碼 &quot;, GetLastError());
      return INIT_FAILED;
   }
   Print(&quot;指標建立完成：&quot;, EnumToString(i_MaMethod), &quot; &quot;,
         i_FastPeriod, &quot;/&quot;, i_SlowPeriod, &quot;，ATR &quot;, i_AtrPeriod);
   return INIT_SUCCEEDED;
}</code></pre>



<p class="wp-block-paragraph"><code class="" data-line="">iMA()</code> 六個參數：商品、週期、期數、位移（幾乎都填 0）、均線種類、用哪個價算（<code class="" data-line="">PRICE_CLOSE</code> 收盤價）。<code class="" data-line="">iATR()</code> 只要商品、週期、期數。建立失敗（通常是商品名稱打錯之類的參數問題）會回傳 <code class="" data-line="">INVALID_HANDLE</code>，這裡直接 <code class="" data-line="">return INIT_FAILED</code> 讓 EA 不要掛上去。</p>



<p class="wp-block-paragraph"><mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">指標只在 OnInit 建一次</mark>。寫在 OnTick 裡，MT5 會認出是同一個指標、回同一個 handle，不會多算一份，但每個 tick 都白跑一次登記，是初學最常見的錯。</p>



<h2 class="wp-block-heading">3. OnDeinit：釋放指標</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 3. OnDeinit：釋放指標 ==========================================
void OnDeinit(const int reason)
{
   IndicatorRelease(g_FastHandle);
   IndicatorRelease(g_SlowHandle);
   IndicatorRelease(g_AtrHandle);
   Comment(&quot;&quot;);
   Print(&quot;EA 結束，共 &quot;, g_Signals, &quot; 個訊號&quot;);
}</code></pre>



<p class="wp-block-paragraph">EA 被拿掉的時候用 <code class="" data-line="">IndicatorRelease()</code> 把 handle 還給 MT5。不寫也不會怎樣，EA 卸載時 MT5 會自己收，但有借有還的習慣要養，之後在程式裡換參數重建指標時才不會越積越多。</p>



<h2 class="wp-block-heading">4. 只在新 K 線算一次</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 4. 新 K 線判斷 =================================================
bool IsNewBar()
{
   datetime t = iTime(_Symbol, _Period, 0);  // 目前這根的開盤時間
   if(t == g_LastBarTime)
      return false;
   g_LastBarTime = t;
   return true;
}</code></pre>



<p class="wp-block-paragraph">已收盤的 K 線，指標值不會再變，沒必要每個 tick 都算。<code class="" data-line="">iTime(_Symbol, _Period, 0)</code> 是目前這根的開盤時間，跟上次記的一樣就是同一根，直接跳過；不一樣就是新的一根開始了，記下來、往下算。這個函數之後每隻 EA 都會有。</p>



<p class="wp-block-paragraph">副作用是訊號會在「下一根的第一個 tick」才出現。日誌裡第一筆黃金交叉說的是 01.03（週五）23:00 那根，測試器印出這行的時間卻是 01.06（週一）00:05：週五收完之後要等週一第一筆報價進來，OnTick 才會被叫到。這個時間欄文末的日誌拿掉了，自己跑一次就看得到。</p>



<h2 class="wp-block-heading">5. OnTick：讀指標值、判斷訊號</h2>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 5. OnTick：讀指標值、判斷訊號 ==================================
void OnTick()
{
   if(!IsNewBar())
      return;                       // 一根 K 線只算一次

   double fast[], slow[], atr[];
   ArraySetAsSeries(fast, true);    // [0] 進行中、[1] 上一根已收盤
   ArraySetAsSeries(slow, true);
   ArraySetAsSeries(atr,  true);
   if(CopyBuffer(g_FastHandle, 0, 0, 3, fast) &lt; 3 ||
      CopyBuffer(g_SlowHandle, 0, 0, 3, slow) &lt; 3 ||
      CopyBuffer(g_AtrHandle,  0, 0, 3, atr)  &lt; 3)
   {
      Print(&quot;指標資料還沒準備好，錯誤碼 &quot;, GetLastError());
      g_LastBarTime = 0;            // 下一個 tick 再試這根
      return;
   }

   // 訊號只看已收盤的 [1] 與 [2]，不看還在跳的 [0]
   bool goldenCross = fast[2] &lt;= slow[2] &amp;&amp; fast[1] &gt; slow[1];
   bool deadCross   = fast[2] &gt;= slow[2] &amp;&amp; fast[1] &lt; slow[1];
   int  atrPoints   = (int)MathRound(atr[1] / _Point);

   Comment(&quot;快線 &quot;, DoubleToString(fast[1], _Digits),
           &quot;  慢線 &quot;, DoubleToString(slow[1], _Digits),
           &quot;\nATR &quot;, atrPoints, &quot; 點&quot;);

   if(goldenCross || deadCross)
   {
      g_Signals++;
      Print(goldenCross ? &quot;黃金交叉&quot; : &quot;死亡交叉&quot;,
            &quot;  &quot;, TimeToString(iTime(_Symbol, _Period, 1)),
            &quot;  快線 &quot;, DoubleToString(fast[1], _Digits),
            &quot;  慢線 &quot;, DoubleToString(slow[1], _Digits),
            &quot;  ATR &quot;, atrPoints, &quot; 點&quot;);
   }
}</code></pre>



<p class="wp-block-paragraph"><code class="" data-line="">CopyBuffer(handle, 緩衝區編號, 起點, 幾根, 陣列)</code>。緩衝區（buffer）是指標算好的一串值，一條線一個緩衝區，均線和 ATR 各只有一條，編號 0；起點 0 是目前這根，抓 3 根就是 [0]、[1]、[2]。回傳值是實際抓到幾根，剛掛上圖表、歷史還沒載完的時候會拿不齊，一定要檢查。拿不齊就把 <code class="" data-line="">g_LastBarTime</code> 清成 0，下一個 tick 再試同一根，不然這根就被跳過了。</p>



<p class="wp-block-paragraph">三個陣列都先 <code class="" data-line="">ArraySetAsSeries(..., true)</code>，[0] 才是最新那根、[1] 上一根、[2] 再前一根，跟 CopyClose 一樣。忘了設，順序就反過來，交叉判斷整個顛倒，而且不會報錯。</p>



<p class="wp-block-paragraph"><mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">訊號只用 [1] 和 [2]，不用 [0]</mark>。[0] 這根還在跑，均線值每個 tick 都在變，這一秒交叉、下一秒又沒了。用已收盤的 [1] 代價是慢一根，但訊號出來就不會消失，之後回測要準也靠這一條。</p>



<figure class="wp-block-image size-full"><a href="https://orzalanluo.com/wp-content/uploads/mt5-03-ma-cross-bars.png"><img decoding="async" width="1000" height="770" src="https://orzalanluo.com/wp-content/uploads/mt5-03-ma-cross-bars.png" alt="EURUSD H1 的 K 線，2025.01.02 19:00 到 01.03（週五）23:00：藍色快線 EMA10 一直在橘色慢線 EMA30 下方，最後一根收盤才穿上去，紅圈標出黃金交叉，最右邊灰色虛線框是還在跑的 [0]。圖下列出 [2] 01.03 22:00 快線 1.02959 &lt; 慢線 1.02965、[1] 23:00 快線 1.02981 &gt; 慢線 1.02972，[0] 是 01.06 週一開盤那根。" class="wp-image-1903" srcset="https://orzalanluo.com/wp-content/uploads/mt5-03-ma-cross-bars.png 1000w, https://orzalanluo.com/wp-content/uploads/mt5-03-ma-cross-bars-300x231.png 300w, https://orzalanluo.com/wp-content/uploads/mt5-03-ma-cross-bars-644x496.png 644w" sizes="(max-width: 1000px) 100vw, 1000px" /></a><figcaption class="wp-element-caption">第一筆黃金交叉：[2] 快線在慢線下，[1] 收盤翻上去，[0] 還沒收完</figcaption></figure>



<p class="wp-block-paragraph">黃金交叉的寫法是「前一根快線還在慢線下面（或相等）、這根收盤翻到上面」，兩個條件都要，只有翻過去的那一根會印，不會每根都印。死亡交叉反過來。日誌裡 01.16 05:00 那筆快線慢線都印 1.02929 卻算死亡交叉，因為 <code class="" data-line="">DoubleToString()</code> 只顯示到第 5 位，實際差在第 6 位以後。均線值是 double，這種一線之隔很常見。</p>



<p class="wp-block-paragraph">ATR（Average True Range）是最近 14 根的平均波動幅度，除以 <code class="" data-line="">_Point</code> 換成點數比較好讀，日誌裡這幾筆落在 80 到 220 點之間。現在只是印出來，之後算停損距離會用它：停損跟著波動走，比固定點數合理。</p>



<h2 class="wp-block-heading">6. 同一件事用 CiMA、CiATR 類別寫</h2>



<p class="wp-block-paragraph">標準程式庫把「建 handle、CopyBuffer」包進類別：均線是 <code class="" data-line="">Indicators\Trend.mqh</code> 裡的 <code class="" data-line="">CiMA</code>，ATR 是 <code class="" data-line="">Indicators\Oscilators.mqh</code> 裡的 <code class="" data-line="">CiATR</code>（檔名就只有一個 l，打成 Oscillators 會找不到檔）。MaCrossSignalCi.mq5 只改四個地方：宣告、OnInit、OnDeinit、OnTick 讀值，其餘跟上面一樣。</p>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">#include &lt;Indicators\Trend.mqh&gt;       // CiMA
#include &lt;Indicators\Oscilators.mqh&gt;  // CiATR（檔名就是一個 l）

CiMA     g_Fast;                          // handle 包在物件裡
CiMA     g_Slow;
CiATR    g_Atr;</code></pre>



<p class="wp-block-paragraph"><code class="" data-line="">#include</code> 把標準程式庫裡寫好的檔案引進來，<code class="" data-line="">CiMA</code>、<code class="" data-line="">CiATR</code> 就寫在這兩個檔裡。指標物件宣告成全域變數，handle 藏在物件裡，不用自己拿 int 存。</p>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">//=== 2. OnInit：Create 建立指標 =====================================
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;
   }
   Print(&quot;指標建立完成：&quot;, EnumToString(i_MaMethod), &quot; &quot;,
         i_FastPeriod, &quot;/&quot;, i_SlowPeriod, &quot;，ATR &quot;, i_AtrPeriod);
   return INIT_SUCCEEDED;
}</code></pre>



<p class="wp-block-paragraph"><code class="" data-line="">g_Fast.Create(...)</code> 是「物件.函數()」的寫法：<code class="" data-line="">g_Fast</code> 是 <code class="" data-line="">CiMA</code> 型態的變數，也就是物件，點後面是叫這個物件做的事。<code class="" data-line="">Create()</code> 的參數跟 <code class="" data-line="">iMA()</code>、<code class="" data-line="">iATR()</code> 一模一樣，差別是回傳 bool：裡面就是呼叫 <code class="" data-line="">iMA()</code> 把 handle 存起來，建不起來回 false。</p>



<p class="wp-block-paragraph">OnDeinit 不用寫 <code class="" data-line="">IndicatorRelease()</code>：EA 卸載時 MT5 會自己把指標收回，跟第 3 節說的一樣。收的是 MT5，不是物件。</p>



<pre class="wp-block-prismatic-blocks"><code class="language-cpp" data-line="">   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);   // Main(0) 進行中、Main(1) 上一根
   if(fast2 == EMPTY_VALUE || slow2 == EMPTY_VALUE ||
      atr1  == EMPTY_VALUE)        // 還沒抓到就是 EMPTY_VALUE
   {
      Print(&quot;指標資料還沒準備好，錯誤碼 &quot;, GetLastError());
      g_LastBarTime = 0;            // 下一個 tick 再試這根
      return;
   }

   // 訊號只看已收盤的 1 與 2，不看還在跳的 0
   bool goldenCross = fast2 &lt;= slow2 &amp;&amp; fast1 &gt; slow1;
   bool deadCross   = fast2 &gt;= slow2 &amp;&amp; fast1 &lt; slow1;</code></pre>



<p class="wp-block-paragraph"><code class="" data-line="">Refresh()</code> 把最新一段指標值抓進物件（底層就是 CopyBuffer），之後 <code class="" data-line="">Main(i)</code> 讀第 i 根：0 是進行中那根、1 上一根，順序已經是最新在前，不用 ArraySetAsSeries。還沒抓到的位置回 <code class="" data-line="">EMPTY_VALUE</code>，檢查它就等於檢查 CopyBuffer 的回傳筆數。</p>



<figure class="wp-block-image size-full"><a href="https://orzalanluo.com/wp-content/uploads/mt5-03-handle-vs-class.png"><img decoding="async" width="1000" height="750" src="https://orzalanluo.com/wp-content/uploads/mt5-03-handle-vs-class.png" alt="兩種寫法的流程對照。上排 iMA() handle：iMA(...) 登記指標拿到 handle（一個號碼），每根新 K 線用 CopyBuffer(g_FastHandle, 0, 0, 3, fast) 把值抓進陣列，先 ArraySetAsSeries 再讀 fast[1]、fast[2]；下排 CiMA 類別：g_Fast.Create(...) 登記指標、handle 包在物件裡，每根新 K 線 g_Fast.Refresh() 把值抓進物件，再讀 g_Fast.Main(1)、g_Fast.Main(2)，不用 ArraySetAsSeries；中間一框寫著兩者背後是同一個指標、算出同一串值（緩衝區）。" class="wp-image-1904" srcset="https://orzalanluo.com/wp-content/uploads/mt5-03-handle-vs-class.png 1000w, https://orzalanluo.com/wp-content/uploads/mt5-03-handle-vs-class-300x225.png 300w, https://orzalanluo.com/wp-content/uploads/mt5-03-handle-vs-class-644x483.png 644w" sizes="(max-width: 1000px) 100vw, 1000px" /></a><figcaption class="wp-element-caption">上排 handle、下排 CiMA 類別，背後是同一個指標、同一串值</figcaption></figure>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th></th><th><code class="" data-line="">iMA</code>、<code class="" data-line="">iATR</code></th><th><code class="" data-line="">CiMA</code>、<code class="" data-line="">CiATR</code></th></tr></thead><tbody><tr><td>引入</td><td>不用</td><td>include <code class="" data-line="">Trend.mqh</code> 等</td></tr><tr><td>建立</td><td><code class="" data-line="">iMA(...)</code>，失敗回 <code class="" data-line="">INVALID_HANDLE</code></td><td><code class="" data-line="">Create(...)</code>，失敗回 false</td></tr><tr><td>釋放</td><td>要，第 3 節</td><td>不用，交給 MT5</td></tr><tr><td>讀值</td><td><code class="" data-line="">CopyBuffer()</code> 抓進陣列</td><td><code class="" data-line="">Refresh()</code> 再 <code class="" data-line="">Main(i)</code></td></tr><tr><td>拿不到值</td><td><code class="" data-line="">CopyBuffer()</code> 回傳的筆數不足</td><td><code class="" data-line="">Main(i)</code> 回 <code class="" data-line="">EMPTY_VALUE</code></td></tr><tr><td>多線指標</td><td>緩衝區編號 0、1、2</td><td><code class="" data-line="">Main()</code>、<code class="" data-line="">Signal()</code> 這類函數</td></tr><tr><td>一次抓幾根</td><td>自己指定</td><td>預設 1024 根，<code class="" data-line="">BufferResize()</code> 可改</td></tr></tbody></table></figure>



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



<p class="wp-block-paragraph">類別版少了 handle、陣列、釋放三件事，讀起來也比較像在講策略，之後的範例都用這種寫法。底下發生的事還是 handle 和 CopyBuffer，看別人的程式碼或查錯誤時要認得。</p>



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



<p class="wp-block-paragraph">2025 年 1 月，14 個訊號，兩個檔案輸出相同（測試器日誌的時間欄省略，只列訊息）：</p>



<pre class="wp-block-prismatic-blocks"><code class="" data-line="">指標建立完成：MODE_EMA 10/30，ATR 14
黃金交叉  2025.01.03 23:00  快線 1.02981  慢線 1.02972  ATR 144 點
死亡交叉  2025.01.07 20:00  快線 1.03812  慢線 1.03816  ATR 195 點
黃金交叉  2025.01.14 02:00  快線 1.02346  慢線 1.02330  ATR 190 點
死亡交叉  2025.01.16 05:00  快線 1.02929  慢線 1.02929  ATR 142 點
黃金交叉  2025.01.16 19:00  快線 1.02915  慢線 1.02909  ATR 166 點
死亡交叉  2025.01.17 09:00  快線 1.02948  慢線 1.02950  ATR 80 點
黃金交叉  2025.01.17 10:00  快線 1.02952  慢線 1.02951  ATR 84 點
死亡交叉  2025.01.17 18:00  快線 1.02951  慢線 1.02955  ATR 152 點
黃金交叉  2025.01.20 06:00  快線 1.02915  慢線 1.02907  ATR 130 點
死亡交叉  2025.01.23 03:00  快線 1.04123  慢線 1.04125  ATR 106 點
黃金交叉  2025.01.23 19:00  快線 1.04129  慢線 1.04105  ATR 170 點
死亡交叉  2025.01.28 01:00  快線 1.04819  慢線 1.04823  ATR 173 點
黃金交叉  2025.01.30 17:00  快線 1.04220  慢線 1.04206  ATR 176 點
死亡交叉  2025.01.30 22:00  快線 1.04200  慢線 1.04203  ATR 217 點
EA 結束，共 14 個訊號</code></pre>



<p class="wp-block-paragraph">光看這份就知道均線交叉的老問題：01.17 一天翻了三次，01.30 黃金交叉五小時後又死亡交叉。盤整時兩條均線黏在一起反覆穿越，所以交叉很少單獨拿來交易，通常會再加濾網。這篇只做訊號，先不管好不好賺。</p>



<h2 class="wp-block-heading">換別的指標</h2>



<p class="wp-block-paragraph">換指標只是換函數或類別（類別名就是函數名前面加 C，<code class="" data-line="">iMA</code> 對應 <code class="" data-line="">CiMA</code>），讀法一樣。有好幾條線的指標，用 handle 要記緩衝區編號，用類別是不同名字的函數：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>函數</th><th>緩衝區編號</th><th>類別的讀法</th></tr></thead><tbody><tr><td><code class="" data-line="">iMA</code>、<code class="" data-line="">iATR</code>、<code class="" data-line="">iRSI</code>、<code class="" data-line="">iCCI</code></td><td>0</td><td><code class="" data-line="">Main()</code></td></tr><tr><td><code class="" data-line="">iBands</code>（布林通道）</td><td>0 中軌、1 上軌、2 下軌</td><td><code class="" data-line="">Base()</code>、<code class="" data-line="">Upper()</code>、<code class="" data-line="">Lower()</code></td></tr><tr><td><code class="" data-line="">iMACD</code></td><td>0 主線、1 訊號線</td><td><code class="" data-line="">Main()</code>、<code class="" data-line="">Signal()</code></td></tr><tr><td><code class="" data-line="">iStochastic</code></td><td>0 主線 %K、1 訊號線 %D</td><td><code class="" data-line="">Main()</code>、<code class="" data-line="">Signal()</code></td></tr><tr><td><code class="" data-line="">iADX</code></td><td>0 ADX、1 +DI、2 -DI</td><td><code class="" data-line="">Main()</code>、<code class="" data-line="">Plus()</code>、<code class="" data-line="">Minus()</code></td></tr></tbody></table></figure>



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



<p class="wp-block-paragraph">記不住就在 MetaEditor 把游標放在函數或類別名上按 <code class="" data-line="">F1</code>，說明裡有列。類別全部收在 <code class="" data-line="">Indicators\Indicators.mqh</code>，懶得分檔就 include 這一個。</p>



<p class="wp-block-paragraph">用 handle 建的指標不會畫在圖表上（測試器的視覺化模式除外），類別版也一樣。想核對數值，就自己從導航拖一個同參數的 Moving Average 到圖上，開數據窗口（<code class="" data-line="">Ctrl+D</code>）比對。</p>



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