<?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>【辞書】MQLリファレンス アーカイブ - 自動売買を作ろう！</title>
	<atom:link href="https://mql-programing.com/archives/category/mql-reference/feed/" rel="self" type="application/rss+xml" />
	<link>https://mql-programing.com/archives/category/mql-reference/</link>
	<description>MQLプログラミング学習サイト</description>
	<lastBuildDate>Wed, 01 Apr 2026 02:47:38 +0000</lastBuildDate>
	<language>ja</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://mql-programing.com/main29/wp-content/uploads/2021/02/cropped-ブログアイコン-32x32.jpg</url>
	<title>【辞書】MQLリファレンス アーカイブ - 自動売買を作ろう！</title>
	<link>https://mql-programing.com/archives/category/mql-reference/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>【MQL4】iMACD関数の使い方を徹底解説！MACDの値を取得してEAに組み込もう【サンプルコード付き】</title>
		<link>https://mql-programing.com/archives/13019/%e3%80%90mql4%e3%80%91imacd%e9%96%a2%e6%95%b0%e3%81%ae%e4%bd%bf%e3%81%84%e6%96%b9%e3%82%92%e5%be%b9%e5%ba%95%e8%a7%a3%e8%aa%ac%ef%bc%81macd%e3%81%ae%e5%80%a4%e3%82%92%e5%8f%96%e5%be%97%e3%81%97/</link>
		
		<dc:creator><![CDATA[朝日奈りさ]]></dc:creator>
		<pubDate>Wed, 08 Apr 2026 01:00:00 +0000</pubDate>
				<category><![CDATA[関数]]></category>
		<category><![CDATA[【辞書】MQLリファレンス]]></category>
		<category><![CDATA[インジケーター関数]]></category>
		<category><![CDATA[MACD]]></category>
		<category><![CDATA[iMACD]]></category>
		<category><![CDATA[MQL4]]></category>
		<category><![CDATA[EA開発]]></category>
		<guid isPermaLink="false">https://mql-programing.com/?p=13019</guid>

					<description><![CDATA[<p>「MACDのクロスで自動売買したい！」──テクニカル分析の定番指標MACDをEAに組み込むなら、まず覚えるべきなのがiMACD関数です。 この記事では、MQL4のiMACD関数の書式・引数の意味から、ゴールデンクロス・デ [&#8230;]</p>
<p>投稿 <a href="https://mql-programing.com/archives/13019/%e3%80%90mql4%e3%80%91imacd%e9%96%a2%e6%95%b0%e3%81%ae%e4%bd%bf%e3%81%84%e6%96%b9%e3%82%92%e5%be%b9%e5%ba%95%e8%a7%a3%e8%aa%ac%ef%bc%81macd%e3%81%ae%e5%80%a4%e3%82%92%e5%8f%96%e5%be%97%e3%81%97/">【MQL4】iMACD関数の使い方を徹底解説！MACDの値を取得してEAに組み込もう【サンプルコード付き】</a> は <a href="https://mql-programing.com">自動売買を作ろう！</a> に最初に表示されました。</p>
]]></description>
										<content:encoded><![CDATA[<p>「MACDのクロスで自動売買したい！」──テクニカル分析の定番指標MACDをEAに組み込むなら、まず覚えるべきなのが<strong>iMACD関数</strong>です。</p>
<p>この記事では、MQL4の<strong>iMACD関数</strong>の書式・引数の意味から、ゴールデンクロス・デッドクロスの判定ロジック、そして実際に動くEAのサンプルコードまで、ステップバイステップで解説します。初心者の方でも、この記事を読み終える頃にはMACDを使ったEAの原型が作れるようになるはずです。</p>
<h2><span id="toc1">MACDのおさらい</span></h2>
<p>コードの話に入る前に、MACD（Moving Average Convergence Divergence）の基本をおさらいしておきましょう。MACDは以下の<strong>3つの要素</strong>で構成されます。</p>
<h3><span id="toc2">MACDの3つの構成要素</span></h3>
<ul>
<li><strong>メインライン（MACDライン）</strong>：短期EMA（通常12期間）と長期EMA（通常26期間）の差</li>
<li><strong>シグナルライン</strong>：メインラインのEMA（通常9期間）</li>
<li><strong>ヒストグラム</strong>：メインライン − シグナルラインの差をバーで表示したもの</li>
</ul>
<h3><span id="toc3">基本的な売買シグナル</span></h3>
<ul>
<li><strong>ゴールデンクロス（買いシグナル）</strong>：メインラインがシグナルラインを下から上に抜ける</li>
<li><strong>デッドクロス（売りシグナル）</strong>：メインラインがシグナルラインを上から下に抜ける</li>
</ul>
<p>このクロスをプログラムで検出するのが、MACD系EAの基本中の基本です。</p>
<h2><span id="toc4">iMACD関数の書式と引数</span></h2>
<p>MQL4でMACDの値を取得するには、<strong>iMACD関数</strong>を使います。書式は以下のとおりです。</p>
<pre><code class="language-mql4">double iMACD(
   string       symbol,           // 通貨ペア名
   int          timeframe,        // 時間足
   int          fast_ema_period,  // 短期EMAの期間
   int          slow_ema_period,  // 長期EMAの期間
   int          signal_period,    // シグナルラインの期間
   int          applied_price,    // 適用価格
   int          mode,             // 取得するライン
   int          shift             // シフト（何本前のバーか）
);</code></pre>
<h3><span id="toc5">各引数の詳細</span></h3>
<table>
<thead>
<tr>
<th>引数</th>
<th>説明</th>
<th>よく使う値</th>
</tr>
</thead>
<tbody>
<tr>
<td>symbol</td>
<td>通貨ペア名。現在のチャートならNULL</td>
<td>NULL, Symbol()</td>
</tr>
<tr>
<td>timeframe</td>
<td>時間足。0で現在のチャートの時間足</td>
<td>0, PERIOD_H1 など</td>
</tr>
<tr>
<td>fast_ema_period</td>
<td>短期EMAの期間</td>
<td>12</td>
</tr>
<tr>
<td>slow_ema_period</td>
<td>長期EMAの期間</td>
<td>26</td>
</tr>
<tr>
<td>signal_period</td>
<td>シグナルラインの期間</td>
<td>9</td>
</tr>
<tr>
<td>applied_price</td>
<td>計算に使う価格の種類</td>
<td>PRICE_CLOSE</td>
</tr>
<tr>
<td>mode</td>
<td>取得するライン（後述）</td>
<td>MODE_MAIN, MODE_SIGNAL</td>
</tr>
<tr>
<td>shift</td>
<td>何本前のバーの値を取得するか</td>
<td>0（現在）, 1（1本前）</td>
</tr>
</tbody>
</table>
<h3><span id="toc6">modeパラメータの値</span></h3>
<p><strong>mode</strong>には以下の2つの定数を指定できます。</p>
<ul>
<li><strong>MODE_MAIN（0）</strong>：MACDメインラインの値を返す</li>
<li><strong>MODE_SIGNAL（1）</strong>：シグナルラインの値を返す</li>
</ul>
<p>なお、ヒストグラムの値は直接取得する定数がありません。<strong>メインライン − シグナルライン</strong>を自分で計算して求めます。</p>
<h3><span id="toc7">適用価格（applied_price）の定数一覧</span></h3>
<table>
<thead>
<tr>
<th>定数名</th>
<th>値</th>
<th>説明</th>
</tr>
</thead>
<tbody>
<tr>
<td>PRICE_CLOSE</td>
<td>0</td>
<td>終値</td>
</tr>
<tr>
<td>PRICE_OPEN</td>
<td>1</td>
<td>始値</td>
</tr>
<tr>
<td>PRICE_HIGH</td>
<td>2</td>
<td>高値</td>
</tr>
<tr>
<td>PRICE_LOW</td>
<td>3</td>
<td>安値</td>
</tr>
<tr>
<td>PRICE_MEDIAN</td>
<td>4</td>
<td>中央値 (High+Low)/2</td>
</tr>
<tr>
<td>PRICE_TYPICAL</td>
<td>5</td>
<td>代表値 (High+Low+Close)/3</td>
</tr>
<tr>
<td>PRICE_WEIGHTED</td>
<td>6</td>
<td>加重終値 (High+Low+Close+Close)/4</td>
</tr>
</tbody>
</table>
<p>特別な理由がなければ、<strong>PRICE_CLOSE（終値）</strong>を使うのが一般的です。</p>
<h2><span id="toc8">基本的な使い方</span></h2>
<p>まずは最もシンプルな使い方を見てみましょう。現在のチャートでMACDのメインラインとシグナルラインの値を取得するコードです。</p>
<pre><code class="language-mql4">void OnTick()
{
   // 1本前の確定バーからMACDメインラインの値を取得
   double macdMain = iMACD(NULL, 0, 12, 26, 9, PRICE_CLOSE, MODE_MAIN, 1);
   
   // 1本前の確定バーからシグナルラインの値を取得
   double macdSignal = iMACD(NULL, 0, 12, 26, 9, PRICE_CLOSE, MODE_SIGNAL, 1);
   
   // ヒストグラムの値を計算
   double histogram = macdMain - macdSignal;
   
   // ログに出力して確認
   Print("MACD Main: ", macdMain, " Signal: ", macdSignal, " Histogram: ", histogram);
}</code></pre>
<p><strong>ポイント：</strong>shiftに<strong>1</strong>を指定しているのは、「1本前の確定したバー」の値を取得するためです。shift=0は現在形成中のバーなので値が変動します。売買判定にはshift=1以降の確定した値を使うのが基本です。</p>
<h2><span id="toc9">MACDクロス判定ロジック</span></h2>
<p>MACDのゴールデンクロス・デッドクロスを判定するには、<strong>1本前と2本前</strong>のバーの値を比較します。</p>
<pre><code class="language-mql4">// --- 1本前のバー（直近の確定バー）---
double macdMain1   = iMACD(NULL, 0, 12, 26, 9, PRICE_CLOSE, MODE_MAIN, 1);
double macdSignal1 = iMACD(NULL, 0, 12, 26, 9, PRICE_CLOSE, MODE_SIGNAL, 1);

// --- 2本前のバー ---
double macdMain2   = iMACD(NULL, 0, 12, 26, 9, PRICE_CLOSE, MODE_MAIN, 2);
double macdSignal2 = iMACD(NULL, 0, 12, 26, 9, PRICE_CLOSE, MODE_SIGNAL, 2);

// ゴールデンクロス判定：2本前ではメインがシグナル以下 → 1本前でメインがシグナルを超えた
bool goldenCross = (macdMain2 <= macdSignal2) &#038;&#038; (macdMain1 > macdSignal1);

// デッドクロス判定：2本前ではメインがシグナル以上 → 1本前でメインがシグナルを下回った
bool deadCross = (macdMain2 >= macdSignal2) && (macdMain1 < macdSignal1);</code></pre>
<p>この「2時点を比較してクロスを検出する」パターンは、移動平均線やRSIなど他のインジケーターでも応用できる重要なテクニックです。</p>
<h2><span id="toc10">実践！MACDクロスEAのサンプルコード</span></h2>
<p>ここからは、MACDのゴールデンクロスで買い、デッドクロスで売り（ドテン方式）のEAを作ってみましょう。実際にMT4で動作する完全なコードです。</p>
<pre><code class="language-mql4">//+------------------------------------------------------------------+
//|                                              MACD_Cross_EA.mq4   |
//|                        MACDクロスで売買するシンプルなEA            |
//+------------------------------------------------------------------+
#property strict

// --- 入力パラメータ ---
input int    FastEMA        = 12;          // 短期EMA期間
input int    SlowEMA        = 26;          // 長期EMA期間
input int    SignalPeriod    = 9;           // シグナル期間
input double LotSize        = 0.1;         // ロット数
input int    MagicNumber    = 20240101;    // マジックナンバー
input int    Slippage       = 3;           // スリッページ（ポイント）

//+------------------------------------------------------------------+
//| 新しいバーが形成されたか判定する関数                                |
//+------------------------------------------------------------------+
bool IsNewBar()
{
   static datetime lastBarTime = 0;
   datetime currentBarTime = iTime(NULL, 0, 0);
   
   if(lastBarTime != currentBarTime)
   {
      lastBarTime = currentBarTime;
      return true;
   }
   return false;
}

//+------------------------------------------------------------------+
//| 自分のEAが持つポジションの種類を返す関数                           |
//| 戻り値: 1=買いポジあり, -1=売りポジあり, 0=ポジなし                |
//+------------------------------------------------------------------+
int GetMyPosition()
{
   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
      {
         if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
         {
            if(OrderType() == OP_BUY)  return 1;
            if(OrderType() == OP_SELL) return -1;
         }
      }
   }
   return 0;
}

//+------------------------------------------------------------------+
//| 自分のEAが持つポジションを全決済する関数                           |
//+------------------------------------------------------------------+
void CloseMyPositions(int posType)
{
   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
      {
         if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
         {
            if(posType == 1 && OrderType() == OP_BUY)
               OrderClose(OrderTicket(), OrderLots(), Bid, Slippage, clrNone);
            if(posType == -1 && OrderType() == OP_SELL)
               OrderClose(OrderTicket(), OrderLots(), Ask, Slippage, clrNone);
         }
      }
   }
}

//+------------------------------------------------------------------+
//| メインのティック処理                                               |
//+------------------------------------------------------------------+
void OnTick()
{
   // 新しいバーが形成されたときだけ処理する
   if(!IsNewBar()) return;
   
   // --- MACDの値を取得 ---
   double macdMain1   = iMACD(NULL, 0, FastEMA, SlowEMA, SignalPeriod, PRICE_CLOSE, MODE_MAIN, 1);
   double macdSignal1 = iMACD(NULL, 0, FastEMA, SlowEMA, SignalPeriod, PRICE_CLOSE, MODE_SIGNAL, 1);
   double macdMain2   = iMACD(NULL, 0, FastEMA, SlowEMA, SignalPeriod, PRICE_CLOSE, MODE_MAIN, 2);
   double macdSignal2 = iMACD(NULL, 0, FastEMA, SlowEMA, SignalPeriod, PRICE_CLOSE, MODE_SIGNAL, 2);
   
   // --- クロス判定 ---
   bool goldenCross = (macdMain2 <= macdSignal2) &#038;&#038; (macdMain1 > macdSignal1);
   bool deadCross   = (macdMain2 >= macdSignal2) && (macdMain1 < macdSignal1);
   
   // --- 現在のポジション状態を取得 ---
   int myPos = GetMyPosition();
   
   // --- ゴールデンクロス → 買いエントリー ---
   if(goldenCross)
   {
      // 売りポジションがあれば先に決済
      if(myPos == -1) CloseMyPositions(-1);
      
      // 買いポジションがなければ新規注文
      if(GetMyPosition() != 1)
      {
         int ticket = OrderSend(Symbol(), OP_BUY, LotSize, Ask, Slippage, 0, 0,
                                "MACD GC Buy", MagicNumber, 0, clrBlue);
         if(ticket < 0)
            Print("Buy OrderSend Error: ", GetLastError());
         else
            Print("Buy Order Success! Ticket: ", ticket);
      }
   }
   
   // --- デッドクロス → 売りエントリー ---
   if(deadCross)
   {
      // 買いポジションがあれば先に決済
      if(myPos == 1) CloseMyPositions(1);
      
      // 売りポジションがなければ新規注文
      if(GetMyPosition() != -1)
      {
         int ticket = OrderSend(Symbol(), OP_SELL, LotSize, Bid, Slippage, 0, 0,
                                "MACD DC Sell", MagicNumber, 0, clrRed);
         if(ticket < 0)
            Print("Sell OrderSend Error: ", GetLastError());
         else
            Print("Sell Order Success! Ticket: ", ticket);
      }
   }
}</code></pre>
<h3><span id="toc11">サンプルEAのポイント解説</span></h3>
<ul>
<li><strong>IsNewBar関数</strong>：新しいバーが形成されたときだけ処理を実行します。これにより無駄な重複注文を防ぎます。</li>
<li><strong>MagicNumber</strong>：このEAが出した注文を識別するための番号です。他のEAや手動注文と区別できます。</li>
<li><strong>ドテン方式</strong>：ゴールデンクロスで買いに転換、デッドクロスで売りに転換する仕組みです。反対ポジションを先に決済してから新規注文を出します。</li>
<li><strong>GetMyPosition関数</strong>：自分のEAが持つポジションの状態を調べる関数です。ポジション管理はEA開発で必須のテクニックです。</li>
</ul>
<h2><span id="toc12">応用：ゼロラインフィルターでダマシを減らす</span></h2>
<p>MACDクロスだけだと「ダマシ」（偽のシグナル）が多くなりがちです。精度を上げるテクニックとして、<strong>ゼロラインフィルター</strong>があります。</p>
<p>考え方はシンプルです。</p>
<ul>
<li><strong>買いシグナル</strong>：ゴールデンクロス かつ MACDメインラインが<strong>ゼロより下</strong>で発生したとき（底からの反転を狙う）</li>
<li><strong>売りシグナル</strong>：デッドクロス かつ MACDメインラインが<strong>ゼロより上</strong>で発生したとき（天井からの反転を狙う）</li>
</ul>
<p>コードに組み込むなら、クロス判定の条件に1行追加するだけです。</p>
<pre><code class="language-mql4">// ゼロラインフィルター付きのゴールデンクロス判定
bool filteredGoldenCross = goldenCross && (macdMain1 < 0);

// ゼロラインフィルター付きのデッドクロス判定
bool filteredDeadCross = deadCross &#038;&#038; (macdMain1 > 0);</code></pre>
<p>たった1行の条件追加ですが、トレンドの初動を捉えやすくなり、レンジ相場でのダマシを軽減できます。バックテストで効果を確認してみてください。</p>
<h2><span id="toc13">注意点・よくあるつまずきポイント</span></h2>
<h3><span id="toc14">1. MT4とMT5でMACDの表示が異なる</span></h3>
<p>MT4のMACDインジケーターでは、<strong>メインラインがヒストグラム（バー）</strong>で表示され、シグナルラインが点線で表示されます。一方MT5では、メインラインもシグナルラインも<strong>ライン</strong>で表示され、別途ヒストグラムが描画されます。見た目は違いますが、iMACD関数で取得できる値の意味は同じです。</p>
<h3><span id="toc15">2. ヒストグラムは自分で計算する</span></h3>
<p>iMACD関数にはヒストグラムを直接取得するモードがありません。ヒストグラムが必要な場合は、以下のように計算してください。</p>
<pre><code class="language-mql4">double histogram = iMACD(NULL, 0, 12, 26, 9, PRICE_CLOSE, MODE_MAIN, 1)
                 - iMACD(NULL, 0, 12, 26, 9, PRICE_CLOSE, MODE_SIGNAL, 1);</code></pre>
<h3><span id="toc16">3. マルチタイムフレーム分析に使える</span></h3>
<p>iMACD関数の第2引数（timeframe）を変えれば、現在のチャートと違う時間足のMACDを取得できます。例えば、5分足チャート上で1時間足のMACDを確認してフィルターに使う、といった活用が可能です。</p>
<pre><code class="language-mql4">// 1時間足のMACDメインラインを取得
double macdH1 = iMACD(NULL, PERIOD_H1, 12, 26, 9, PRICE_CLOSE, MODE_MAIN, 1);</code></pre>
<h3><span id="toc17">4. 引数の順番に注意</span></h3>
<p>iMACD関数は引数が8つもあるため、順番を間違えやすいです。特に<strong>mode</strong>と<strong>shift</strong>を逆にするミスはよくあります。迷ったら公式リファレンスで確認する習慣をつけましょう。</p>
<h2><span id="toc18">まとめ</span></h2>
<p>この記事では、MQL4の<strong>iMACD関数</strong>について以下のポイントを解説しました。</p>
<ul>
<li>iMACD関数は<strong>8つの引数</strong>を持ち、MACDのメインラインとシグナルラインの値を取得できる</li>
<li>クロス判定は<strong>shift=1とshift=2</strong>の値を比較して行う</li>
<li>EA開発では<strong>新規バー判定</strong>・<strong>マジックナンバー管理</strong>・<strong>ポジション管理関数</strong>が重要</li>
<li><strong>ゼロラインフィルター</strong>を追加するだけでダマシを軽減できる</li>
<li>MT4とMT5でMACDの<strong>表示方法が異なる</strong>点に注意</li>
</ul>
<p>今回のサンプルEAはシンプルな構成ですが、ストップロスやテイクプロフィット、トレーリングストップなどを追加していくことで、より実践的なEAに育てていくことができます。まずはバックテストで動作を確認し、少しずつカスタマイズしてみてください！</p>
<p>投稿 <a href="https://mql-programing.com/archives/13019/%e3%80%90mql4%e3%80%91imacd%e9%96%a2%e6%95%b0%e3%81%ae%e4%bd%bf%e3%81%84%e6%96%b9%e3%82%92%e5%be%b9%e5%ba%95%e8%a7%a3%e8%aa%ac%ef%bc%81macd%e3%81%ae%e5%80%a4%e3%82%92%e5%8f%96%e5%be%97%e3%81%97/">【MQL4】iMACD関数の使い方を徹底解説！MACDの値を取得してEAに組み込もう【サンプルコード付き】</a> は <a href="https://mql-programing.com">自動売買を作ろう！</a> に最初に表示されました。</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>【MQL4】注文情報を取得する関数を徹底解説！OrderOpenPrice・OrderStopLoss・OrderTakeProfit・OrderCloseTime・OrderProfit の使い方</title>
		<link>https://mql-programing.com/archives/13011/%e3%80%90mql4%e3%80%91%e6%b3%a8%e6%96%87%e6%83%85%e5%a0%b1%e3%82%92%e5%8f%96%e5%be%97%e3%81%99%e3%82%8b%e9%96%a2%e6%95%b0%e3%82%92%e5%be%b9%e5%ba%95%e8%a7%a3%e8%aa%ac%ef%bc%81orderopenprice%e3%83%bbor/</link>
		
		<dc:creator><![CDATA[朝日奈りさ]]></dc:creator>
		<pubDate>Tue, 07 Apr 2026 01:00:00 +0000</pubDate>
				<category><![CDATA[関数]]></category>
		<category><![CDATA[【辞書】MQLリファレンス]]></category>
		<category><![CDATA[OrderOpenPrice]]></category>
		<category><![CDATA[MQL4]]></category>
		<category><![CDATA[EA開発]]></category>
		<category><![CDATA[注文情報取得]]></category>
		<category><![CDATA[OrderStopLoss]]></category>
		<guid isPermaLink="false">https://mql-programing.com/?p=13011</guid>

					<description><![CDATA[<p>EA（自動売買）を開発していると、「今持っているポジションのエントリー価格はいくらだろう？」「ストップロスはちゃんと設定されているかな？」といった場面に必ず出くわします。MQL4には、選択中の注文からさまざまな情報を取り [&#8230;]</p>
<p>投稿 <a href="https://mql-programing.com/archives/13011/%e3%80%90mql4%e3%80%91%e6%b3%a8%e6%96%87%e6%83%85%e5%a0%b1%e3%82%92%e5%8f%96%e5%be%97%e3%81%99%e3%82%8b%e9%96%a2%e6%95%b0%e3%82%92%e5%be%b9%e5%ba%95%e8%a7%a3%e8%aa%ac%ef%bc%81orderopenprice%e3%83%bbor/">【MQL4】注文情報を取得する関数を徹底解説！OrderOpenPrice・OrderStopLoss・OrderTakeProfit・OrderCloseTime・OrderProfit の使い方</a> は <a href="https://mql-programing.com">自動売買を作ろう！</a> に最初に表示されました。</p>
]]></description>
										<content:encoded><![CDATA[<p>EA（自動売買）を開発していると、「今持っているポジションのエントリー価格はいくらだろう？」「ストップロスはちゃんと設定されているかな？」といった場面に必ず出くわします。MQL4には、選択中の注文からさまざまな情報を取り出すための専用関数が用意されています。</p>
<p>この記事では、EA開発で頻繁に使う<strong>5つの注文情報取得関数</strong>――<code>OrderOpenPrice()</code>・<code>OrderStopLoss()</code>・<code>OrderTakeProfit()</code>・<code>OrderCloseTime()</code>・<code>OrderProfit()</code>を、サンプルコード付きで徹底解説します。</p>
<h2><span id="toc1">前提知識：OrderSelect() で注文を選択してから使う</span></h2>
<p>MQL4の注文情報取得関数には、共通のルールがあります。それは<strong>「必ず事前に <code>OrderSelect()</code> で注文を選択しておくこと」</strong>です。</p>
<p><code>OrderSelect()</code> を呼ばずにこれらの関数を使うと、意図しない値が返ってきたり、前回選択した注文の情報が返ってしまったりします。これはMQL4初心者が最もハマりやすいポイントの一つです。</p>
<pre><code class="language-mql4">// 基本パターン：注文を選択してから情報を取得する
if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
{
    double openPrice = OrderOpenPrice();   // エントリー価格
    double sl        = OrderStopLoss();    // 損切り価格
    double tp        = OrderTakeProfit();  // 利確価格
    double profit    = OrderProfit();      // 損益額
}
</code></pre>
<p>この前提を押さえた上で、各関数を詳しく見ていきましょう。</p>
<h2><span id="toc2">OrderOpenPrice() — エントリー価格を取得する</span></h2>
<h3><span id="toc3">基本仕様</span></h3>
<p><code>OrderOpenPrice()</code> は、選択中の注文の<strong>約定価格（エントリー価格）</strong>を <code>double</code> 型で返します。</p>
<pre><code class="language-mql4">double OrderOpenPrice();
</code></pre>
<p>成行注文の場合は実際に約定した価格、指値・逆指値注文（未約定の待機注文）の場合は設定した注文価格が返ります。</p>
<h3><span id="toc4">使用例</span></h3>
<pre><code class="language-mql4">if(OrderSelect(0, SELECT_BY_POS, MODE_TRADES))
{
    double entryPrice = OrderOpenPrice();
    Print("エントリー価格: ", entryPrice);
}
</code></pre>
<p>エントリー価格は、損益の計算やトレーリングストップのロジックなど、あらゆる場面で使う最も基本的な注文情報です。</p>
<h2><span id="toc5">OrderStopLoss() — 損切り価格を取得する</span></h2>
<h3><span id="toc6">基本仕様</span></h3>
<p><code>OrderStopLoss()</code> は、選択中の注文に設定されている<strong>ストップロス（損切り）価格</strong>を <code>double</code> 型で返します。</p>
<pre><code class="language-mql4">double OrderStopLoss();
</code></pre>
<p>ストップロスが設定されていない場合は <strong><code>0</code></strong> が返ります。この性質を利用して、「SLが未設定のポジションを検出する」といった処理が可能です（応用例で後述します）。</p>
<h2><span id="toc7">OrderTakeProfit() — 利確価格を取得する</span></h2>
<h3><span id="toc8">基本仕様</span></h3>
<p><code>OrderTakeProfit()</code> は、選択中の注文に設定されている<strong>テイクプロフィット（利益確定）価格</strong>を <code>double</code> 型で返します。</p>
<pre><code class="language-mql4">double OrderTakeProfit();
</code></pre>
<p><code>OrderStopLoss()</code> と同様に、テイクプロフィットが未設定の場合は <strong><code>0</code></strong> が返ります。</p>
<h2><span id="toc9">OrderCloseTime() — 決済時刻を取得する</span></h2>
<h3><span id="toc10">基本仕様</span></h3>
<p><code>OrderCloseTime()</code> は、選択中の注文の<strong>決済時刻</strong>を <code>datetime</code> 型で返します。</p>
<pre><code class="language-mql4">datetime OrderCloseTime();
</code></pre>
<p>ここで重要なのが、<strong>まだ決済されていないポジション（保有中のポジション）の場合は <code>0</code> が返る</strong>という点です。この性質を使えば、「この注文はまだ保有中か、それとも決済済みか」を判定できます。</p>
<h3><span id="toc11">使用例</span></h3>
<pre><code class="language-mql4">if(OrderSelect(i, SELECT_BY_POS, MODE_HISTORY))
{
    datetime closeTime = OrderCloseTime();
    if(closeTime > 0)
    {
        Print("決済時刻: ", TimeToString(closeTime, TIME_DATE | TIME_MINUTES));
    }
    else
    {
        Print("この注文はまだ未決済です");
    }
}
</code></pre>
<p>履歴からトレード結果を分析するEAや、日報を自動生成するスクリプトを作る際に活躍します。</p>
<h2><span id="toc12">OrderProfit() — 損益額を取得する</span></h2>
<h3><span id="toc13">基本仕様</span></h3>
<p><code>OrderProfit()</code> は、選択中の注文の<strong>損益額（口座通貨建て）</strong>を <code>double</code> 型で返します。</p>
<pre><code class="language-mql4">double OrderProfit();
</code></pre>
<p>保有中のポジションなら含み損益、決済済みの注文なら確定損益が返ります。</p>
<p><strong>重要な注意点：</strong><code>OrderProfit()</code> が返す値には<strong>スワップポイントと取引手数料は含まれません</strong>。スワップを含めた正確な損益を計算するには、<code>OrderSwap()</code> と <code>OrderCommission()</code> を加算する必要があります。</p>
<pre><code class="language-mql4">// スワップと手数料を含めた正確な損益
double totalProfit = OrderProfit() + OrderSwap() + OrderCommission();
</code></pre>
<h2><span id="toc14">実践サンプル：全ポジションの注文情報を一覧表示する</span></h2>
<p>ここまで学んだ5つの関数を組み合わせて、保有中の全ポジション情報をエキスパートログに出力する実践的な関数を作ってみましょう。</p>
<pre><code class="language-mql4">void ShowAllOrderInfo()
{
    int totalOrders = OrdersTotal();
    Print("===== 保有ポジション一覧（全 ", totalOrders, " 件）=====");

    for(int i = totalOrders - 1; i >= 0; i--)
    {
        if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
            continue;

        // 自分のEAの注文だけを対象にする
        if(OrderMagicNumber() != 12345)
            continue;

        // 待機注文を除外し、成行注文（ポジション）のみ対象にする
        if(OrderType() > OP_SELL)
            continue;

        string type = (OrderType() == OP_BUY) ? "BUY" : "SELL";
        double totalPL = OrderProfit() + OrderSwap() + OrderCommission();

        Print("チケット: ", OrderTicket(),
              " | 通貨: ", OrderSymbol(),
              " | ", type,
              " | ロット: ", OrderLots(),
              " | エントリー: ", DoubleToString(OrderOpenPrice(), Digits),
              " | SL: ", DoubleToString(OrderStopLoss(), Digits),
              " | TP: ", DoubleToString(OrderTakeProfit(), Digits),
              " | 損益: ", DoubleToString(totalPL, 2));
    }

    Print("==========================================");
}
</code></pre>
<p>ループを <strong><code>totalOrders - 1</code> から <code>0</code> に向かって逆順</strong>で回しているのがポイントです。これについては後述の注意点で詳しく説明します。</p>
<h2><span id="toc15">応用例：SL未設定ポジションに自動でSLを追加する</span></h2>
<p><code>OrderStopLoss()</code> が <code>0</code> を返す（＝SL未設定）ポジションを検出し、自動的にストップロスを設定する実用的なサンプルです。</p>
<pre><code class="language-mql4">void SetSLForUnprotectedPositions(int magicNumber, int slPips)
{
    for(int i = OrdersTotal() - 1; i >= 0; i--)
    {
        if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
            continue;

        if(OrderMagicNumber() != magicNumber)
            continue;

        // SLが既に設定されている場合はスキップ
        if(OrderStopLoss() != 0)
            continue;

        double newSL = 0;
        double point = MarketInfo(OrderSymbol(), MODE_POINT);
        int    digits = (int)MarketInfo(OrderSymbol(), MODE_DIGITS);

        // 3桁/5桁業者対応
        if(digits == 3 || digits == 5)
            point *= 10;

        if(OrderType() == OP_BUY)
        {
            newSL = OrderOpenPrice() - slPips * point;
        }
        else if(OrderType() == OP_SELL)
        {
            newSL = OrderOpenPrice() + slPips * point;
        }
        else
        {
            continue; // 待機注文はスキップ
        }

        newSL = NormalizeDouble(newSL, digits);

        bool result = OrderModify(OrderTicket(), OrderOpenPrice(), newSL, OrderTakeProfit(), 0, clrYellow);

        if(result)
        {
            Print("SL設定成功 チケット:", OrderTicket(), " SL:", DoubleToString(newSL, digits));
        }
        else
        {
            Print("SL設定失敗 チケット:", OrderTicket(), " エラー:", GetLastError());
        }
    }
}
</code></pre>
<p><code>OrderStopLoss() != 0</code> で「SLが設定済みかどうか」を判定し、未設定のポジションにのみ <code>OrderModify()</code> でSLを追加しています。リスク管理を自動化する際にとても便利なパターンです。</p>
<h2><span id="toc16">3つの重要な注意点</span></h2>
<h3><span id="toc17">1. OrderSelect() の呼び忘れに注意</span></h3>
<p>繰り返しになりますが、すべての注文情報取得関数は <code>OrderSelect()</code> で注文を選択した後に呼ぶ必要があります。選択せずに使うと、前回選択した注文の情報が返ったり、不正な値が返ったりして、バグの原因になります。</p>
<h3><span id="toc18">2. ループは逆順（降順）で回す</span></h3>
<p>注文一覧をループする際は、<code>for(int i = OrdersTotal() - 1; i >= 0; i--)</code> のように<strong>逆順で回す</strong>のがMQL4の定石です。</p>
<p>なぜなら、ループ中に注文が決済されるとインデックスがズレてしまうからです。昇順（0から）でループすると、途中の注文が消えた際にインデックスが詰まり、一部の注文がスキップされてしまいます。逆順で回せばこの問題を回避できます。</p>
<h3><span id="toc19">3. OrderProfit() にはスワップ・手数料が含まれない</span></h3>
<p><code>OrderProfit()</code> は純粋な為替差益のみを返します。スワップポイントや取引手数料は含まれません。正確なトータル損益を出すには、必ず以下のように計算してください。</p>
<pre><code class="language-mql4">double totalPL = OrderProfit() + OrderSwap() + OrderCommission();
</code></pre>
<p>特にスワップが大きい通貨ペアや、ECN口座のように手数料が発生する環境では、この差が無視できない金額になることがあります。</p>
<h2><span id="toc20">まとめ</span></h2>
<p>今回解説した5つの関数を一覧表で整理します。</p>
<table>
<thead>
<tr>
<th>関数名</th>
<th>戻り値の型</th>
<th>取得できる情報</th>
<th>備考</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>OrderOpenPrice()</code></td>
<td>double</td>
<td>エントリー価格（約定価格）</td>
<td>待機注文の場合は設定した注文価格</td>
</tr>
<tr>
<td><code>OrderStopLoss()</code></td>
<td>double</td>
<td>ストップロス価格</td>
<td>未設定なら0が返る</td>
</tr>
<tr>
<td><code>OrderTakeProfit()</code></td>
<td>double</td>
<td>テイクプロフィット価格</td>
<td>未設定なら0が返る</td>
</tr>
<tr>
<td><code>OrderCloseTime()</code></td>
<td>datetime</td>
<td>決済時刻</td>
<td>未決済なら0が返る</td>
</tr>
<tr>
<td><code>OrderProfit()</code></td>
<td>double</td>
<td>損益額（口座通貨建て）</td>
<td>スワップ・手数料は含まない</td>
</tr>
</tbody>
</table>
<p>いずれの関数も、<strong>事前に <code>OrderSelect()</code> で注文を選択すること</strong>が必須です。また、ループは逆順で回す、<code>OrderProfit()</code> にはスワップが含まれない、といったポイントを押さえておけば、安全で正確なEAを開発できます。</p>
<p>これらの関数をマスターすれば、ポジション管理・リスク管理・トレード分析など、EA開発の幅が大きく広がります。ぜひ実際のコードに組み込んで試してみてください！</p>
<p>投稿 <a href="https://mql-programing.com/archives/13011/%e3%80%90mql4%e3%80%91%e6%b3%a8%e6%96%87%e6%83%85%e5%a0%b1%e3%82%92%e5%8f%96%e5%be%97%e3%81%99%e3%82%8b%e9%96%a2%e6%95%b0%e3%82%92%e5%be%b9%e5%ba%95%e8%a7%a3%e8%aa%ac%ef%bc%81orderopenprice%e3%83%bbor/">【MQL4】注文情報を取得する関数を徹底解説！OrderOpenPrice・OrderStopLoss・OrderTakeProfit・OrderCloseTime・OrderProfit の使い方</a> は <a href="https://mql-programing.com">自動売買を作ろう！</a> に最初に表示されました。</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>【MQL4リファレンス】MqlTick構造体の使い方！SymbolInfoTick()でBid/Askをリアルタイム取得する方法</title>
		<link>https://mql-programing.com/archives/12857/%e3%80%90mql4%e3%83%aa%e3%83%95%e3%82%a1%e3%83%ac%e3%83%b3%e3%82%b9%e3%80%91mqltick%e6%a7%8b%e9%80%a0%e4%bd%93%e3%81%ae%e4%bd%bf%e3%81%84%e6%96%b9%ef%bc%81symbolinfotick%e3%81%a7bid-ask%e3%82%92/</link>
		
		<dc:creator><![CDATA[朝日奈りさ]]></dc:creator>
		<pubDate>Tue, 31 Mar 2026 02:45:15 +0000</pubDate>
				<category><![CDATA[構造体]]></category>
		<category><![CDATA[【辞書】MQLリファレンス]]></category>
		<category><![CDATA[MQL4]]></category>
		<category><![CDATA[MqlTick]]></category>
		<category><![CDATA[SymbolInfoTick]]></category>
		<category><![CDATA[スプレッドフィルター]]></category>
		<guid isPermaLink="false">https://mql-programing.com/?p=12857</guid>

					<description><![CDATA[<p>MqlTick構造体とは MqlTick構造体は、通貨ペアなどのシンボルにおける最新の価格情報（ティック情報）を格納するための構造体です。SymbolInfoTick()関数と組み合わせて使用することで、Bid価格・As [&#8230;]</p>
<p>投稿 <a href="https://mql-programing.com/archives/12857/%e3%80%90mql4%e3%83%aa%e3%83%95%e3%82%a1%e3%83%ac%e3%83%b3%e3%82%b9%e3%80%91mqltick%e6%a7%8b%e9%80%a0%e4%bd%93%e3%81%ae%e4%bd%bf%e3%81%84%e6%96%b9%ef%bc%81symbolinfotick%e3%81%a7bid-ask%e3%82%92/">【MQL4リファレンス】MqlTick構造体の使い方！SymbolInfoTick()でBid/Askをリアルタイム取得する方法</a> は <a href="https://mql-programing.com">自動売買を作ろう！</a> に最初に表示されました。</p>
]]></description>
										<content:encoded><![CDATA[<h2><span id="toc1">MqlTick構造体とは</span></h2>
<p>MqlTick構造体は、通貨ペアなどのシンボルにおける最新の価格情報（ティック情報）を格納するための構造体です。<strong>SymbolInfoTick()関数</strong>と組み合わせて使用することで、Bid価格・Ask価格・Last価格・出来高・タイムスタンプなどをリアルタイムに取得できます。</p>
<p>従来の定義済み変数（<code>Bid</code>、<code>Ask</code>）でも現在値は取得可能ですが、MqlTick構造体を使えば<strong>1回の関数呼び出しで複数の価格情報をまとめて取得</strong>できるため、効率的で安全なプログラミングが可能になります。</p>
<h2><span id="toc2">MqlTick構造体の定義</span></h2>
<pre><code class="language-mql4">struct MqlTick
{
   datetime time;          // 最後の価格更新の時刻
   double   bid;           // 現在のBid価格
   double   ask;           // 現在のAsk価格
   double   last;          // 最後の約定価格（Last）
   ulong    volume;        // 現在のLast価格の出来高
   long     time_msc;      // 最後の価格更新の時刻（ミリ秒単位）
   uint     flags;         // ティックフラグ
};
</code></pre>
<h3><span id="toc3">各メンバの説明</span></h3>
<table>
<thead>
<tr>
<th>メンバ名</th>
<th>型</th>
<th>説明</th>
</tr>
</thead>
<tbody>
<tr>
<td>time</td>
<td>datetime</td>
<td>最後の価格更新が行われた時刻（秒単位）</td>
</tr>
<tr>
<td>bid</td>
<td>double</td>
<td>現在のBid（売り値）価格</td>
</tr>
<tr>
<td>ask</td>
<td>double</td>
<td>現在のAsk（買い値）価格</td>
</tr>
<tr>
<td>last</td>
<td>double</td>
<td>最後の約定価格。FXでは通常0が返されます</td>
</tr>
<tr>
<td>volume</td>
<td>ulong</td>
<td>現在のLast価格に対応する出来高</td>
</tr>
<tr>
<td>time_msc</td>
<td>long</td>
<td>最後の価格更新の時刻（ミリ秒精度、1970年1月1日からの経過ミリ秒）</td>
</tr>
<tr>
<td>flags</td>
<td>uint</td>
<td>ティックフラグ（どの値が変更されたかを示すビットフラグ）</td>
</tr>
</tbody>
</table>
<h2><span id="toc4">SymbolInfoTick()関数の書式</span></h2>
<pre><code class="language-mql4">bool SymbolInfoTick(
   string   symbol,     // シンボル名
   MqlTick& tick        // ティック情報を受け取る構造体への参照
);
</code></pre>
<h3><span id="toc5">引数</span></h3>
<table>
<thead>
<tr>
<th>引数</th>
<th>型</th>
<th>説明</th>
</tr>
</thead>
<tbody>
<tr>
<td>symbol</td>
<td>string</td>
<td>価格情報を取得するシンボル名（例：&#8221;USDJPY&#8221;）</td>
</tr>
<tr>
<td>tick</td>
<td>MqlTick&#038;</td>
<td>ティック情報を格納する構造体の参照</td>
</tr>
</tbody>
</table>
<h3><span id="toc6">戻り値</span></h3>
<p><strong>bool型</strong> — 取得に成功した場合は<code>true</code>、失敗した場合は<code>false</code>を返します。失敗した場合は<code>GetLastError()</code>でエラーコードを確認できます。</p>
<h2><span id="toc7">プログラム例1：基本的なBid/Ask価格の取得</span></h2>
<p>最もシンプルな使い方として、現在のチャートシンボルのBid/Ask価格とスプレッドを取得・表示する例です。</p>
<pre><code class="language-mql4">//+------------------------------------------------------------------+
//| プログラム例1：基本的なBid/Ask価格の取得                          |
//+------------------------------------------------------------------+
void OnTick()
{
   // MqlTick構造体の変数を宣言
   MqlTick tick;

   // 現在のシンボルのティック情報を取得
   if(SymbolInfoTick(_Symbol, tick))
   {
      // Bid価格とAsk価格を出力
      Print("シンボル: ", _Symbol);
      Print("Bid: ", DoubleToString(tick.bid, _Digits));
      Print("Ask: ", DoubleToString(tick.ask, _Digits));

      // スプレッドを計算（ポイント単位）
      double spread = (tick.ask - tick.bid) / _Point;
      Print("スプレッド: ", DoubleToString(spread, 1), " points");

      // ティック受信時刻を出力
      Print("更新時刻: ", TimeToString(tick.time, TIME_DATE | TIME_SECONDS));
   }
   else
   {
      // 取得失敗時のエラー処理
      Print("ティック情報の取得に失敗しました。エラー: ", GetLastError());
   }
}
</code></pre>
<p>このコードでは<code>_Symbol</code>を使用して現在チャートに表示されている通貨ペアのティック情報を取得しています。<code>_Digits</code>は現在のシンボルの小数桁数を自動的に返すため、正しい桁数で価格を表示できます。</p>
<h2><span id="toc8">プログラム例2：複数通貨ペアの価格モニタリング</span></h2>
<p>SymbolInfoTick()は現在のチャート以外のシンボルの価格も取得できます。この例では複数の通貨ペアを同時に監視し、チャート上にダッシュボード風に表示します。</p>
<pre><code class="language-mql4">//+------------------------------------------------------------------+
//| プログラム例2：複数通貨ペアの価格モニタリング                      |
//+------------------------------------------------------------------+
// 監視する通貨ペアリスト
string g_symbols[] = {"USDJPY", "EURUSD", "GBPUSD", "AUDUSD"};

//+------------------------------------------------------------------+
//| 初期化処理                                                        |
//+------------------------------------------------------------------+
int OnInit()
{
   // チャートの左上にラベルを準備
   int totalSymbols = ArraySize(g_symbols);
   for(int i = 0; i < totalSymbols; i++)
   {
      // 各シンボルのラベルオブジェクトを作成
      string objName = "Label_" + g_symbols[i];
      ObjectCreate(0, objName, OBJ_LABEL, 0, 0, 0);
      ObjectSetInteger(0, objName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
      ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, 10);
      ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, 20 + i * 22);
      ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, 11);
      ObjectSetString(0, objName, OBJPROP_FONT, "Consolas");
   }

   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| ティック受信時の処理                                               |
//+------------------------------------------------------------------+
void OnTick()
{
   MqlTick tick;
   int totalSymbols = ArraySize(g_symbols);

   for(int i = 0; i < totalSymbols; i++)
   {
      string objName = "Label_" + g_symbols[i];

      // 各シンボルのティック情報を取得
      if(SymbolInfoTick(g_symbols[i], tick))
      {
         // 小数桁数をシンボルごとに取得
         int digits = (int)SymbolInfoInteger(g_symbols[i], SYMBOL_DIGITS);

         // スプレッドをポイント単位で計算
         double point = SymbolInfoDouble(g_symbols[i], SYMBOL_POINT);
         double spread = (tick.ask - tick.bid) / point;

         // 表示テキストを構築
         string text = StringFormat("%s  Bid: %s  Ask: %s  Spread: %.1f",
                                    g_symbols[i],
                                    DoubleToString(tick.bid, digits),
                                    DoubleToString(tick.ask, digits),
                                    spread);

         ObjectSetString(0, objName, OBJPROP_TEXT, text);
         ObjectSetInteger(0, objName, OBJPROP_COLOR, clrWhite);
      }
      else
      {
         // 取得失敗時はエラー表示
         ObjectSetString(0, objName, OBJPROP_TEXT, g_symbols[i] + "  データ取得失敗");
         ObjectSetInteger(0, objName, OBJPROP_COLOR, clrRed);
      }
   }
   // チャートを再描画
   ChartRedraw();
}

//+------------------------------------------------------------------+
//| 終了処理                                                          |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   // 作成したオブジェクトを削除
   int totalSymbols = ArraySize(g_symbols);
   for(int i = 0; i < totalSymbols; i++)
   {
      ObjectDelete(0, "Label_" + g_symbols[i]);
   }
}
</code></pre>
<p>他のシンボルの価格を取得する際は、対象シンボルが<strong>気配値表示（マーケットウォッチ）ウィンドウに追加されている</strong>必要があります。追加されていない場合、SymbolInfoTick()はfalseを返します。</p>
<h2><span id="toc9">プログラム例3：スプレッドの監視とエントリー条件の判定</span></h2>
<p>スプレッドが指定値以下に狭まったときだけエントリーを行う、実践的なスプレッドフィルター付きの注文ロジックの例です。</p>
<pre><code class="language-mql4">//+------------------------------------------------------------------+
//| プログラム例3：スプレッドフィルター付きエントリー                  |
//+------------------------------------------------------------------+
input double MaxSpreadPoints = 20.0;  // 許容最大スプレッド（ポイント単位）
input double LotSize         = 0.01;  // 取引ロットサイズ
input int    MagicNumber     = 12345; // マジックナンバー

//+------------------------------------------------------------------+
//| ティック受信時の処理                                               |
//+------------------------------------------------------------------+
void OnTick()
{
   MqlTick tick;

   // ティック情報を取得
   if(!SymbolInfoTick(_Symbol, tick))
   {
      Print("ティック情報取得失敗: ", GetLastError());
      return;
   }

   // 現在のスプレッドをポイント単位で計算
   double currentSpread = (tick.ask - tick.bid) / _Point;

   // スプレッドが許容範囲内かチェック
   if(currentSpread > MaxSpreadPoints)
   {
      // スプレッドが広すぎるのでエントリーしない
      Comment(StringFormat("スプレッド: %.1f pts（上限: %.1f pts）- エントリー待機中",
              currentSpread, MaxSpreadPoints));
      return;
   }

   // --- ここにエントリー条件を記述 ---
   // （例：移動平均線のゴールデンクロス判定など）
   bool buySignal  = CheckBuyCondition();
   bool sellSignal = CheckSellCondition();

   // 既存ポジションがなければエントリー
   if(OrdersTotal() == 0)
   {
      if(buySignal)
      {
         // MqlTickから取得したAsk価格で買い注文
         int ticket = OrderSend(_Symbol, OP_BUY, LotSize, tick.ask, 3,
                                0, 0, "MqlTick Buy", MagicNumber, 0, clrBlue);
         if(ticket > 0)
            Print("買い注文成功 Ask=", DoubleToString(tick.ask, _Digits),
                  " Spread=", DoubleToString(currentSpread, 1));
         else
            Print("買い注文失敗: ", GetLastError());
      }
      else if(sellSignal)
      {
         // MqlTickから取得したBid価格で売り注文
         int ticket = OrderSend(_Symbol, OP_SELL, LotSize, tick.bid, 3,
                                0, 0, "MqlTick Sell", MagicNumber, 0, clrRed);
         if(ticket > 0)
            Print("売り注文成功 Bid=", DoubleToString(tick.bid, _Digits),
                  " Spread=", DoubleToString(currentSpread, 1));
         else
            Print("売り注文失敗: ", GetLastError());
      }
   }

   Comment(StringFormat("Bid: %s | Ask: %s | Spread: %.1f pts | 状態: 監視中",
           DoubleToString(tick.bid, _Digits),
           DoubleToString(tick.ask, _Digits),
           currentSpread));
}

//+------------------------------------------------------------------+
//| 買い条件チェック（例：簡易的なMA判定）                             |
//+------------------------------------------------------------------+
bool CheckBuyCondition()
{
   double maFast = iMA(_Symbol, 0, 10, 0, MODE_EMA, PRICE_CLOSE, 1);
   double maSlow = iMA(_Symbol, 0, 25, 0, MODE_EMA, PRICE_CLOSE, 1);
   double maFastPrev = iMA(_Symbol, 0, 10, 0, MODE_EMA, PRICE_CLOSE, 2);
   double maSlowPrev = iMA(_Symbol, 0, 25, 0, MODE_EMA, PRICE_CLOSE, 2);

   // ゴールデンクロス判定
   return (maFastPrev < maSlowPrev &#038;&#038; maFast > maSlow);
}

//+------------------------------------------------------------------+
//| 売り条件チェック（例：簡易的なMA判定）                             |
//+------------------------------------------------------------------+
bool CheckSellCondition()
{
   double maFast = iMA(_Symbol, 0, 10, 0, MODE_EMA, PRICE_CLOSE, 1);
   double maSlow = iMA(_Symbol, 0, 25, 0, MODE_EMA, PRICE_CLOSE, 1);
   double maFastPrev = iMA(_Symbol, 0, 10, 0, MODE_EMA, PRICE_CLOSE, 2);
   double maSlowPrev = iMA(_Symbol, 0, 25, 0, MODE_EMA, PRICE_CLOSE, 2);

   // デッドクロス判定
   return (maFastPrev > maSlowPrev && maFast < maSlow);
}
</code></pre>
<p>スプレッドフィルターは実運用において非常に重要です。経済指標発表時やマーケットオープン直後はスプレッドが大きく広がるため、不利な価格でのエントリーを防止できます。</p>
<h2><span id="toc10">プログラム例4：ティック情報のCSVログ記録</span></h2>
<p>MqlTick構造体のミリ秒精度のタイムスタンプ（time_msc）を活用し、ティックデータをCSVファイルに記録する例です。ティック分析やバックテスト用のデータ作成に役立ちます。</p>
<pre><code class="language-mql4">//+------------------------------------------------------------------+
//| プログラム例4：ティックデータのCSVログ記録                         |
//+------------------------------------------------------------------+
input int MaxRecords = 10000;  // 最大記録件数（0で無制限）

int    g_fileHandle = INVALID_HANDLE;  // ファイルハンドル
int    g_recordCount = 0;              // 記録件数カウンター
double g_lastBid = 0;                  // 前回のBid価格
double g_lastAsk = 0;                  // 前回のAsk価格

//+------------------------------------------------------------------+
//| 初期化処理                                                        |
//+------------------------------------------------------------------+
int OnInit()
{
   // ファイル名に日付を含めて一意にする
   string fileName = _Symbol + "_ticks_" +
                     TimeToString(TimeCurrent(), TIME_DATE) + ".csv";

   // 日付部分のドットをハイフンに変換
   StringReplace(fileName, ".", "-");

   // CSVファイルを書き込みモードで開く
   g_fileHandle = FileOpen(fileName, FILE_WRITE | FILE_CSV, ',');

   if(g_fileHandle == INVALID_HANDLE)
   {
      Print("ファイルのオープンに失敗しました: ", GetLastError());
      return(INIT_FAILED);
   }

   // ヘッダー行を書き込み
   FileWrite(g_fileHandle,
             "Time",
             "TimeMsc",
             "Bid",
             "Ask",
             "Spread(pts)",
             "Last",
             "Volume");

   Print("ティックログ記録開始: ", fileName);
   g_recordCount = 0;

   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| ティック受信時の処理                                               |
//+------------------------------------------------------------------+
void OnTick()
{
   // 最大記録件数チェック
   if(MaxRecords > 0 && g_recordCount >= MaxRecords)
      return;

   // ファイルハンドルが有効か確認
   if(g_fileHandle == INVALID_HANDLE)
      return;

   MqlTick tick;

   // ティック情報を取得
   if(!SymbolInfoTick(_Symbol, tick))
      return;

   // 価格が変化した場合のみ記録（不要なデータを減らす）
   if(tick.bid == g_lastBid && tick.ask == g_lastAsk)
      return;

   // スプレッドを計算
   double spread = (tick.ask - tick.bid) / _Point;

   // time_mscからミリ秒部分を抽出
   // time_mscは1970年1月1日からのミリ秒数
   int milliseconds = (int)(tick.time_msc % 1000);

   // 時刻文字列を構築（秒 + ミリ秒）
   string timeStr = TimeToString(tick.time, TIME_DATE | TIME_SECONDS)
                    + "." + IntegerToString(milliseconds, 3, '0');

   // CSVにデータを書き込み
   FileWrite(g_fileHandle,
             timeStr,
             IntegerToString(tick.time_msc),
             DoubleToString(tick.bid, _Digits),
             DoubleToString(tick.ask, _Digits),
             DoubleToString(spread, 1),
             DoubleToString(tick.last, _Digits),
             IntegerToString(tick.volume));

   // 前回の価格を更新
   g_lastBid = tick.bid;
   g_lastAsk = tick.ask;

   // カウンターを更新
   g_recordCount++;

   // 1000件ごとにフラッシュして安全性を確保
   if(g_recordCount % 1000 == 0)
   {
      FileFlush(g_fileHandle);
      Print("記録件数: ", g_recordCount);
   }

   // チャートに状況を表示
   Comment(StringFormat("ティックログ記録中\n件数: %d / %s\nBid: %s  Ask: %s",
           g_recordCount,
           (MaxRecords > 0 ? IntegerToString(MaxRecords) : "無制限"),
           DoubleToString(tick.bid, _Digits),
           DoubleToString(tick.ask, _Digits)));
}

//+------------------------------------------------------------------+
//| 終了処理                                                          |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   // ファイルを閉じる
   if(g_fileHandle != INVALID_HANDLE)
   {
      FileClose(g_fileHandle);
      Print("ティックログ記録終了。総記録件数: ", g_recordCount);
   }
}
</code></pre>
<p>CSVファイルはMT4のデータフォルダ内の<code>MQL4\Files\</code>ディレクトリに保存されます。<code>time_msc</code>メンバを活用することで、通常の秒単位のタイムスタンプでは不可能な<strong>ミリ秒精度の時刻記録</strong>が可能になります。</p>
<h2><span id="toc11">使い方のポイント・注意事項</span></h2>
<h3><span id="toc12">1. Bid/Ask定義済み変数との違い</span></h3>
<p>MQL4では従来から<code>Bid</code>と<code>Ask</code>という定義済み変数が使えますが、これらは<strong>現在のチャートのシンボルの価格しか取得できません</strong>。SymbolInfoTick()を使えば任意のシンボルの価格を取得できるため、マルチ通貨ペアEAを開発する際に不可欠です。</p>
<pre><code class="language-mql4">// 従来の方法（現在のシンボルのみ）
double bid1 = Bid;
double ask1 = Ask;

// MqlTickを使う方法（任意のシンボルに対応）
MqlTick tick;
SymbolInfoTick("EURUSD", tick);
double bid2 = tick.bid;
double ask2 = tick.ask;
</code></pre>
<h3><span id="toc13">2. lastとvolumeについて</span></h3>
<p>FX（外国為替）取引では<code>last</code>（最終約定価格）と<code>volume</code>（出来高）は通常<strong>0</strong>が返されます。これらのメンバは主に株式や先物など、取引所取引型の金融商品で使用されます。</p>
<h3><span id="toc14">3. time_mscの活用</span></h3>
<p><code>time_msc</code>メンバはミリ秒単位のタイムスタンプを返します。高頻度トレーディング（HFT）やティック分析において、秒単位の<code>time</code>では精度が不足する場面で威力を発揮します。</p>
<h3><span id="toc15">4. エラーハンドリングを忘れずに</span></h3>
<p>SymbolInfoTick()は失敗する可能性があります（シンボルが無効、マーケットが閉じている場合など）。必ず戻り値をチェックし、<code>false</code>の場合は<code>GetLastError()</code>でエラーを確認しましょう。</p>
<h3><span id="toc16">5. マーケットウォッチへの追加</span></h3>
<p>他のシンボルの価格を取得する場合、そのシンボルが<strong>マーケットウォッチウィンドウ</strong>に表示されている必要があります。コードから追加する場合は以下のようにします。</p>
<pre><code class="language-mql4">// シンボルをマーケットウォッチに追加
SymbolSelect("GBPJPY", true);

// 追加後にティック情報を取得
MqlTick tick;
if(SymbolInfoTick("GBPJPY", tick))
{
   Print("GBPJPY Bid=", tick.bid, " Ask=", tick.ask);
}
</code></pre>
<h3><span id="toc17">6. OnTick()の外での使用</span></h3>
<p>SymbolInfoTick()は<code>OnTick()</code>以外の関数（<code>OnTimer()</code>、<code>OnChartEvent()</code>など）からも呼び出すことができます。タイマーを使った定期的な価格チェックなどに活用できます。</p>
<pre><code class="language-mql4">int OnInit()
{
   // 1秒ごとにタイマーイベントを発生させる
   EventSetTimer(1);
   return(INIT_SUCCEEDED);
}

void OnTimer()
{
   MqlTick tick;
   if(SymbolInfoTick(_Symbol, tick))
   {
      // タイマーからでも最新価格を取得可能
      Comment("Timer更新 Bid=", tick.bid, " Ask=", tick.ask);
   }
}

void OnDeinit(const int reason)
{
   EventKillTimer();
}
</code></pre>
<h2><span id="toc18">まとめ</span></h2>
<p>MqlTick構造体とSymbolInfoTick()関数は、MQL4プログラミングにおける<strong>価格情報取得の基本ツール</strong>です。主なメリットをまとめると以下の通りです。</p>
<ul>
<li><strong>一括取得</strong>：1回の呼び出しでBid/Ask/Last/Volume/タイムスタンプをまとめて取得</li>
<li><strong>マルチシンボル対応</strong>：任意のシンボルの価格を取得可能</li>
<li><strong>高精度タイムスタンプ</strong>：ミリ秒単位の時刻情報を取得可能</li>
<li><strong>安全性</strong>：エラーハンドリングにより信頼性の高いコードが書ける</li>
</ul>
<p>特にマルチ通貨ペアEAやスプレッド監視ツール、ティックデータの記録・分析など、本格的なトレーディングシステムの開発では欠かせない要素です。</p>
<p>投稿 <a href="https://mql-programing.com/archives/12857/%e3%80%90mql4%e3%83%aa%e3%83%95%e3%82%a1%e3%83%ac%e3%83%b3%e3%82%b9%e3%80%91mqltick%e6%a7%8b%e9%80%a0%e4%bd%93%e3%81%ae%e4%bd%bf%e3%81%84%e6%96%b9%ef%bc%81symbolinfotick%e3%81%a7bid-ask%e3%82%92/">【MQL4リファレンス】MqlTick構造体の使い方！SymbolInfoTick()でBid/Askをリアルタイム取得する方法</a> は <a href="https://mql-programing.com">自動売買を作ろう！</a> に最初に表示されました。</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>【MQL4リファレンス】MqlRates構造体の使い方！CopyRates()でローソク足データを配列取得する方法</title>
		<link>https://mql-programing.com/archives/12855/%e3%80%90mql4%e3%83%aa%e3%83%95%e3%82%a1%e3%83%ac%e3%83%b3%e3%82%b9%e3%80%91mqlrates%e6%a7%8b%e9%80%a0%e4%bd%93%e3%81%ae%e4%bd%bf%e3%81%84%e6%96%b9%ef%bc%81copyrates%e3%81%a7%e3%83%ad%e3%83%bc/</link>
		
		<dc:creator><![CDATA[朝日奈りさ]]></dc:creator>
		<pubDate>Tue, 31 Mar 2026 02:45:13 +0000</pubDate>
				<category><![CDATA[構造体]]></category>
		<category><![CDATA[【辞書】MQLリファレンス]]></category>
		<category><![CDATA[CopyRates]]></category>
		<category><![CDATA[ローソク足]]></category>
		<category><![CDATA[MQL4]]></category>
		<category><![CDATA[MqlRates]]></category>
		<guid isPermaLink="false">https://mql-programing.com/?p=12855</guid>

					<description><![CDATA[<p>MqlRates構造体とは MqlRates構造体は、MQL4でローソク足（バー）の価格データを格納するための構造体です。1本のローソク足に関する情報（始値・高値・安値・終値・出来高・時刻・スプレッド）をひとまとめに扱う [&#8230;]</p>
<p>投稿 <a href="https://mql-programing.com/archives/12855/%e3%80%90mql4%e3%83%aa%e3%83%95%e3%82%a1%e3%83%ac%e3%83%b3%e3%82%b9%e3%80%91mqlrates%e6%a7%8b%e9%80%a0%e4%bd%93%e3%81%ae%e4%bd%bf%e3%81%84%e6%96%b9%ef%bc%81copyrates%e3%81%a7%e3%83%ad%e3%83%bc/">【MQL4リファレンス】MqlRates構造体の使い方！CopyRates()でローソク足データを配列取得する方法</a> は <a href="https://mql-programing.com">自動売買を作ろう！</a> に最初に表示されました。</p>
]]></description>
										<content:encoded><![CDATA[<h2><span id="toc1">MqlRates構造体とは</span></h2>
<p>MqlRates構造体は、MQL4で<strong>ローソク足（バー）の価格データを格納するための構造体</strong>です。1本のローソク足に関する情報（始値・高値・安値・終値・出来高・時刻・スプレッド）をひとまとめに扱うことができます。</p>
<p>この構造体は、主に<strong>CopyRates()関数</strong>と組み合わせて使用し、過去のローソク足データを配列として一括取得する際に活用されます。</p>
<h2><span id="toc2">MqlRates構造体のメンバー</span></h2>
<p>MqlRates構造体は以下のメンバーで構成されています。</p>
<table>
<thead>
<tr>
<th>メンバー名</th>
<th>型</th>
<th>説明</th>
</tr>
</thead>
<tbody>
<tr>
<td>time</td>
<td>datetime</td>
<td>バーの開始時刻</td>
</tr>
<tr>
<td>open</td>
<td>double</td>
<td>始値</td>
</tr>
<tr>
<td>high</td>
<td>double</td>
<td>高値</td>
</tr>
<tr>
<td>low</td>
<td>double</td>
<td>安値</td>
</tr>
<tr>
<td>close</td>
<td>double</td>
<td>終値</td>
</tr>
<tr>
<td>tick_volume</td>
<td>long</td>
<td>ティックボリューム</td>
</tr>
<tr>
<td>spread</td>
<td>int</td>
<td>スプレッド</td>
</tr>
<tr>
<td>real_volume</td>
<td>long</td>
<td>出来高（取引所データが利用可能な場合）</td>
</tr>
</tbody>
</table>
<h3><span id="toc3">構造体の定義</span></h3>
<pre><code class="language-mql4">struct MqlRates
  {
   datetime time;         // バーの開始時刻
   double   open;         // 始値
   double   high;         // 高値
   double   low;          // 安値
   double   close;        // 終値
   long     tick_volume;  // ティックボリューム
   int      spread;       // スプレッド
   long     real_volume;  // 出来高
  };
</code></pre>
<h2><span id="toc4">CopyRates()関数の書式</span></h2>
<p>MqlRates構造体の配列にデータを格納するには、<strong>CopyRates()関数</strong>を使用します。CopyRates()には3つの呼び出し方法があります。</p>
<h3><span id="toc5">書式1：開始位置と本数を指定</span></h3>
<pre><code class="language-mql4">int CopyRates(
   string           symbol_name,   // 通貨ペア名
   ENUM_TIMEFRAMES  timeframe,     // 時間足
   int              start_pos,     // 開始位置（0が最新バー）
   int              count,         // 取得するバーの本数
   MqlRates         rates_array[]  // 格納先の配列
);
</code></pre>
<h3><span id="toc6">書式2：開始日時と本数を指定</span></h3>
<pre><code class="language-mql4">int CopyRates(
   string           symbol_name,   // 通貨ペア名
   ENUM_TIMEFRAMES  timeframe,     // 時間足
   datetime         start_time,    // 開始日時
   int              count,         // 取得するバーの本数
   MqlRates         rates_array[]  // 格納先の配列
);
</code></pre>
<h3><span id="toc7">書式3：開始日時と終了日時を指定</span></h3>
<pre><code class="language-mql4">int CopyRates(
   string           symbol_name,   // 通貨ペア名
   ENUM_TIMEFRAMES  timeframe,     // 時間足
   datetime         start_time,    // 開始日時
   datetime         stop_time,     // 終了日時
   MqlRates         rates_array[]  // 格納先の配列
);
</code></pre>
<h3><span id="toc8">引数の説明</span></h3>
<table>
<thead>
<tr>
<th>引数</th>
<th>説明</th>
</tr>
</thead>
<tbody>
<tr>
<td>symbol_name</td>
<td>通貨ペア名。NULL または Symbol() で現在のチャートの通貨ペアを指定</td>
</tr>
<tr>
<td>timeframe</td>
<td>時間足。PERIOD_CURRENT で現在の時間足を指定</td>
</tr>
<tr>
<td>start_pos</td>
<td>取得開始位置。0が最新のバー</td>
</tr>
<tr>
<td>count</td>
<td>取得するバーの本数</td>
</tr>
<tr>
<td>start_time</td>
<td>取得開始日時</td>
</tr>
<tr>
<td>stop_time</td>
<td>取得終了日時</td>
</tr>
<tr>
<td>rates_array[]</td>
<td>MqlRates型の配列（データ格納先）</td>
</tr>
</tbody>
</table>
<h3><span id="toc9">戻り値</span></h3>
<p>CopyRates()関数は、<strong>コピーに成功した要素数</strong>を返します。失敗した場合は<strong>-1</strong>を返します。エラー内容はGetLastError()で取得できます。</p>
<h2><span id="toc10">プログラム例1：直近のローソク足データを取得して表示する（基本）</span></h2>
<p>まずは最もシンプルな使い方から見ていきましょう。直近10本のローソク足データを取得し、エキスパートログに出力する例です。</p>
<pre><code class="language-mql4">//+------------------------------------------------------------------+
//| スクリプト：直近10本のローソク足データを表示                      |
//+------------------------------------------------------------------+
void OnStart()
  {
   // MqlRates型の配列を宣言
   MqlRates rates[];

   // 配列を時系列順に設定（最新のバーが[0]になる）
   ArraySetAsSeries(rates, true);

   // 現在の通貨ペア・時間足で直近10本のバーデータを取得
   int copied = CopyRates(Symbol(), PERIOD_CURRENT, 0, 10, rates);

   // 取得に失敗した場合のエラーチェック
   if(copied <= 0)
     {
      Print("CopyRates()でエラーが発生しました。エラーコード: ", GetLastError());
      return;
     }

   // 取得成功：バーデータを1本ずつ表示
   Print("===== 直近 ", copied, " 本のローソク足データ =====");

   for(int i = 0; i < copied; i++)
     {
      Print("バー[", i, "] ",
            "時刻=", TimeToString(rates[i].time, TIME_DATE | TIME_MINUTES), " | ",
            "始値=", DoubleToString(rates[i].open, Digits), " | ",
            "高値=", DoubleToString(rates[i].high, Digits), " | ",
            "安値=", DoubleToString(rates[i].low, Digits), " | ",
            "終値=", DoubleToString(rates[i].close, Digits), " | ",
            "出来高=", rates[i].tick_volume, " | ",
            "スプレッド=", rates[i].spread);
     }
  }
</code></pre>
<p>この例のポイントは、<strong>ArraySetAsSeries(rates, true)</strong>を呼び出している点です。これにより配列のインデックス[0]が最新のバーを指すようになり、通常のバー番号と同じ感覚でアクセスできます。</p>
<h2><span id="toc11">プログラム例2：ローソク足の実体とヒゲの長さを分析する</span></h2>
<p>MqlRates構造体の各メンバーを活用して、ローソク足の形状を分析するスクリプトです。陽線・陰線の判定や、実体・ヒゲの長さを計算します。</p>
<pre><code class="language-mql4">//+------------------------------------------------------------------+
//| スクリプト：ローソク足の形状分析                                  |
//+------------------------------------------------------------------+
void OnStart()
  {
   MqlRates rates[];
   ArraySetAsSeries(rates, true);

   // 直近20本のバーデータを取得
   int copied = CopyRates(Symbol(), PERIOD_CURRENT, 0, 20, rates);
   if(copied <= 0)
     {
      Print("データ取得に失敗しました。");
      return;
     }

   // 各バーのローソク足形状を分析
   for(int i = 0; i < copied; i++)
     {
      // --- ローソク足の各部分の長さを計算 ---

      // 実体の長さ（始値と終値の差の絶対値）
      double body = MathAbs(rates[i].close - rates[i].open);

      // バー全体の長さ（高値と安値の差）
      double total_range = rates[i].high - rates[i].low;

      // 上ヒゲの長さ
      double upper_shadow = rates[i].high - MathMax(rates[i].open, rates[i].close);

      // 下ヒゲの長さ
      double lower_shadow = MathMin(rates[i].open, rates[i].close) - rates[i].low;

      // 陽線か陰線かを判定
      string candle_type;
      if(rates[i].close > rates[i].open)
         candle_type = "陽線";
      else if(rates[i].close < rates[i].open)
         candle_type = "陰線";
      else
         candle_type = "同値";

      // ピンバー（上ヒゲまたは下ヒゲが実体の2倍以上）の判定
      string pattern = "";
      if(total_range > 0)
        {
         if(upper_shadow > body * 2.0 && upper_shadow > lower_shadow * 2.0)
            pattern = " [上ヒゲピンバー]";
         else if(lower_shadow > body * 2.0 && lower_shadow > upper_shadow * 2.0)
            pattern = " [下ヒゲピンバー]";
        }

      // 結果を出力
      Print("バー[", i, "] ",
            TimeToString(rates[i].time, TIME_DATE | TIME_MINUTES), " | ",
            candle_type, " | ",
            "実体=", DoubleToString(body / Point, 1), "pts | ",
            "上ヒゲ=", DoubleToString(upper_shadow / Point, 1), "pts | ",
            "下ヒゲ=", DoubleToString(lower_shadow / Point, 1), "pts",
            pattern);
     }
  }
</code></pre>
<p>ローソク足の分析では、<strong>open, high, low, close</strong>の4つの値を使って実体やヒゲの長さを算出します。ピンバーなどのパターン検出は、裁量トレードの判断支援やEAのエントリー条件に応用できます。</p>
<h2><span id="toc12">プログラム例3：異なる時間足のデータを比較するEA</span></h2>
<p>CopyRates()の強みは、<strong>現在のチャートとは異なる時間足のデータも取得できる</strong>点です。この例では、マルチタイムフレーム分析として日足と1時間足のデータを同時に取得し、トレンド判定に活用するEAを作成します。</p>
<pre><code class="language-mql4">//+------------------------------------------------------------------+
//| Expert：マルチタイムフレーム分析によるトレンド判定EA              |
//+------------------------------------------------------------------+
#property strict

//+------------------------------------------------------------------+
//| OnTick関数：ティックごとに実行される                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   // --- 日足データの取得（上位時間足のトレンド判定用） ---
   MqlRates daily_rates[];
   ArraySetAsSeries(daily_rates, true);

   int daily_copied = CopyRates(Symbol(), PERIOD_D1, 0, 5, daily_rates);
   if(daily_copied < 5)
     {
      Print("日足データの取得に失敗しました。");
      return;
     }

   // --- 1時間足データの取得（エントリータイミング用） ---
   MqlRates h1_rates[];
   ArraySetAsSeries(h1_rates, true);

   int h1_copied = CopyRates(Symbol(), PERIOD_H1, 0, 10, h1_rates);
   if(h1_copied < 10)
     {
      Print("1時間足データの取得に失敗しました。");
      return;
     }

   // --- 日足のトレンド判定 ---
   // 直近3本の日足の終値が連続して上昇しているか確認
   bool daily_uptrend = (daily_rates[1].close > daily_rates[2].close) &&
                        (daily_rates[2].close > daily_rates[3].close);

   // 直近3本の日足の終値が連続して下降しているか確認
   bool daily_downtrend = (daily_rates[1].close < daily_rates[2].close) &#038;&#038;
                          (daily_rates[2].close < daily_rates[3].close);

   // --- 1時間足のシグナル判定 ---
   // 直前の1時間足が陽線かどうか
   bool h1_bullish = (h1_rates[1].close > h1_rates[1].open);

   // 直前の1時間足が陰線かどうか
   bool h1_bearish = (h1_rates[1].close < h1_rates[1].open);

   // --- マルチタイムフレームの総合判定 ---
   string signal = "待機";

   if(daily_uptrend &#038;&#038; h1_bullish)
      signal = "買いシグナル（日足上昇トレンド＋1時間足陽線）";
   else if(daily_downtrend &#038;&#038; h1_bearish)
      signal = "売りシグナル（日足下降トレンド＋1時間足陰線）";

   // チャート左上にシグナルを表示
   Comment("=== マルチタイムフレーム分析 ===\n",
           "日足トレンド: ", daily_uptrend ? "上昇" : (daily_downtrend ? "下降" : "レンジ"), "\n",
           "日足[1] 始値=", DoubleToString(daily_rates[1].open, Digits),
           " 終値=", DoubleToString(daily_rates[1].close, Digits), "\n",
           "1時間足[1] 始値=", DoubleToString(h1_rates[1].open, Digits),
           " 終値=", DoubleToString(h1_rates[1].close, Digits), "\n",
           "判定: ", signal);
  }
</code></pre>
<p>CopyRates()の第2引数で時間足を指定することで、現在のチャートの時間足に関係なく任意の時間足のデータを取得できます。<strong>PERIOD_D1（日足）</strong>で全体のトレンドを確認し、<strong>PERIOD_H1（1時間足）</strong>でエントリータイミングを計るという実践的なマルチタイムフレーム分析を実装しています。</p>
<h2><span id="toc13">プログラム例4：日時指定でデータ取得し、期間内の最高値・最安値を求める</span></h2>
<p>CopyRates()の書式3（開始日時と終了日時を指定）を使って、特定期間のローソク足データを取得し、その期間内の最高値・最安値やボラティリティを計算する実用的なスクリプトです。</p>
<pre><code class="language-mql4">//+------------------------------------------------------------------+
//| スクリプト：特定期間の価格レンジとボラティリティ分析              |
//+------------------------------------------------------------------+
void OnStart()
  {
   // --- 分析する期間を設定 ---
   datetime start_time = D'2024.01.01 00:00'; // 開始日時
   datetime end_time   = D'2024.01.31 23:59'; // 終了日時

   // MqlRates配列を宣言（時系列設定はしない＝古い順）
   MqlRates rates[];

   // 指定期間のH1（1時間足）データを取得
   int copied = CopyRates(Symbol(), PERIOD_H1, start_time, end_time, rates);

   if(copied <= 0)
     {
      Print("指定期間のデータ取得に失敗しました。エラーコード: ", GetLastError());
      return;
     }

   Print("取得したバー数: ", copied);

   // --- 期間内の最高値・最安値を検索 ---
   double highest_price = rates[0].high;   // 最高値の初期値
   double lowest_price  = rates[0].low;    // 最安値の初期値
   datetime highest_time = rates[0].time;  // 最高値の時刻
   datetime lowest_time  = rates[0].time;  // 最安値の時刻
   long total_volume = 0;                  // 合計出来高
   double total_range = 0;                 // 各バーのレンジ合計

   for(int i = 0; i < copied; i++)
     {
      // 最高値の更新チェック
      if(rates[i].high > highest_price)
        {
         highest_price = rates[i].high;
         highest_time  = rates[i].time;
        }

      // 最安値の更新チェック
      if(rates[i].low < lowest_price)
        {
         lowest_price = rates[i].low;
         lowest_time  = rates[i].time;
        }

      // 出来高の合計
      total_volume += rates[i].tick_volume;

      // 各バーのレンジ（高値−安値）を合計
      total_range += (rates[i].high - rates[i].low);
     }

   // --- 平均ボラティリティの計算 ---
   double avg_range = total_range / copied;           // 平均レンジ（price）
   double avg_range_pts = avg_range / Point;           // 平均レンジ（ポイント）
   double avg_volume = (double)total_volume / copied;  // 平均出来高

   // --- 分析結果を出力 ---
   Print("===========================================");
   Print("  期間分析レポート: ", Symbol(), " H1");
   Print("===========================================");
   Print("分析期間: ", TimeToString(start_time, TIME_DATE), " ～ ",
         TimeToString(end_time, TIME_DATE));
   Print("バー数: ", copied);
   Print("-------------------------------------------");
   Print("期間最高値: ", DoubleToString(highest_price, Digits),
         " (", TimeToString(highest_time, TIME_DATE | TIME_MINUTES), ")");
   Print("期間最安値: ", DoubleToString(lowest_price, Digits),
         " (", TimeToString(lowest_time, TIME_DATE | TIME_MINUTES), ")");
   Print("期間レンジ: ", DoubleToString((highest_price - lowest_price) / Point, 1), " pts");
   Print("-------------------------------------------");
   Print("平均レンジ（1バーあたり）: ", DoubleToString(avg_range_pts, 1), " pts");
   Print("平均出来高（1バーあたり）: ", DoubleToString(avg_volume, 0));
   Print("合計出来高: ", total_volume);
   Print("===========================================");

   // --- 最初の5本と最後の5本のデータを確認表示 ---
   Print("--- 期間先頭のバー ---");
   int show_count = MathMin(5, copied);
   for(int i = 0; i < show_count; i++)
     {
      Print("  ", TimeToString(rates[i].time, TIME_DATE | TIME_MINUTES),
            " O=", DoubleToString(rates[i].open, Digits),
            " H=", DoubleToString(rates[i].high, Digits),
            " L=", DoubleToString(rates[i].low, Digits),
            " C=", DoubleToString(rates[i].close, Digits));
     }

   Print("--- 期間末尾のバー ---");
   for(int i = MathMax(0, copied - 5); i < copied; i++)
     {
      Print("  ", TimeToString(rates[i].time, TIME_DATE | TIME_MINUTES),
            " O=", DoubleToString(rates[i].open, Digits),
            " H=", DoubleToString(rates[i].high, Digits),
            " L=", DoubleToString(rates[i].low, Digits),
            " C=", DoubleToString(rates[i].close, Digits));
     }
  }
</code></pre>
<p>書式3では開始日時と終了日時を直接指定するため、<strong>特定の期間に限定した分析</strong>に非常に便利です。この例では期間内の最高値・最安値の検出に加え、平均ボラティリティや出来高の統計情報も算出しています。なお、日時指定で取得した場合は<strong>ArraySetAsSeries()を使わなければ古い順（時系列ではない順）</strong>で格納されます。</p>
<h2><span id="toc14">よくある使い方のポイント・注意事項</span></h2>
<h3><span id="toc15">1. ArraySetAsSeries()の設定を忘れない</span></h3>
<p>CopyRates()で取得した配列は、デフォルトでは<strong>古いデータから順番に格納</strong>されます。最新のバーを[0]としてアクセスしたい場合は、必ず<strong>CopyRates()の前に</strong>ArraySetAsSeries(rates, true)を呼び出してください。</p>
<pre><code class="language-mql4">// 正しい使い方
MqlRates rates[];
ArraySetAsSeries(rates, true);  // ← CopyRatesの前に設定
CopyRates(Symbol(), PERIOD_CURRENT, 0, 10, rates);
// rates[0]が最新バー、rates[9]が10本前のバー
</code></pre>
<h3><span id="toc16">2. 戻り値のエラーチェックは必須</span></h3>
<p>CopyRates()は、データが利用できない場合やサーバーとの接続が切れている場合に<strong>-1を返す</strong>ことがあります。戻り値を必ずチェックしてからデータにアクセスしてください。</p>
<pre><code class="language-mql4">int copied = CopyRates(Symbol(), PERIOD_CURRENT, 0, 100, rates);
if(copied <= 0)
  {
   Print("データ取得失敗: ", GetLastError());
   return; // データがない状態で処理を続行しない
  }
// copiedの値を上限として配列にアクセスする
for(int i = 0; i < copied; i++)
  {
   // rates[i] を安全に使用できる
  }
</code></pre>
<h3><span id="toc17">3. 他の通貨ペアのデータ取得時の注意</span></h3>
<p>現在のチャートとは異なる通貨ペアのデータを取得する場合、<strong>その通貨ペアのデータがダウンロード済みでないとエラーになる</strong>ことがあります。気配値表示ウィンドウに該当通貨ペアを追加し、過去のデータがダウンロードされていることを確認してください。</p>
<h3><span id="toc18">4. 配列サイズは自動調整される</span></h3>
<p>CopyRates()に渡す配列は<strong>動的配列</strong>である必要があります。関数が自動的に必要なサイズにリサイズしてくれるため、事前にArrayResize()でサイズを指定する必要はありません。ただし、<strong>静的配列（固定サイズの配列）を渡すとエラーになる</strong>場合があるので注意してください。</p>
<pre><code class="language-mql4">// OK：動的配列
MqlRates rates[];
CopyRates(Symbol(), PERIOD_CURRENT, 0, 100, rates);

// NG：静的配列は避ける
// MqlRates rates[100]; ← CopyRatesには動的配列を使うこと
</code></pre>
<h3><span id="toc19">5. iOpen()等の従来関数との使い分け</span></h3>
<p>MQL4にはiOpen()、iClose()、iHigh()、iLow()などの関数もありますが、<strong>複数のバーの情報をまとめて取得したい場合はCopyRates()の方が効率的</strong>です。1本のバー情報だけが必要な場合は従来の関数でも問題ありません。</p>
<pre><code class="language-mql4">// 1本だけ必要な場合 → 従来の関数が簡潔
double close_1 = iClose(Symbol(), PERIOD_CURRENT, 1);

// 複数本まとめて処理する場合 → CopyRates()が効率的
MqlRates rates[];
ArraySetAsSeries(rates, true);
CopyRates(Symbol(), PERIOD_CURRENT, 0, 100, rates);
// rates[0]～rates[99]まで一括でアクセス可能
</code></pre>
<h3><span id="toc20">6. インジケーターでの使用時はバッファとの混同に注意</span></h3>
<p>カスタムインジケーター内でCopyRates()を使う場合、インジケーターバッファの配列とMqlRates配列のインデックス方向が一致しているか注意してください。ArraySetAsSeries()の設定がバッファと同じ方向になっていないと、バー番号がずれてしまいます。</p>
<h2><span id="toc21">まとめ</span></h2>
<p>MqlRates構造体とCopyRates()関数は、MQL4でローソク足データを効率的に扱うための重要な機能です。以下のポイントを押さえておきましょう。</p>
<ul>
<li><strong>MqlRates構造体</strong>は1本のバーのOHLC、出来高、時刻、スプレッドを格納する</li>
<li><strong>CopyRates()</strong>は3つの書式があり、位置指定・日時指定・期間指定で柔軟にデータ取得できる</li>
<li><strong>ArraySetAsSeries()</strong>でインデックスの方向を制御できる</li>
<li>戻り値のエラーチェックを必ず行い、安全にデータにアクセスする</li>
<li>複数バーのデータをまとめて処理する場合に特に威力を発揮する</li>
</ul>
<p>投稿 <a href="https://mql-programing.com/archives/12855/%e3%80%90mql4%e3%83%aa%e3%83%95%e3%82%a1%e3%83%ac%e3%83%b3%e3%82%b9%e3%80%91mqlrates%e6%a7%8b%e9%80%a0%e4%bd%93%e3%81%ae%e4%bd%bf%e3%81%84%e6%96%b9%ef%bc%81copyrates%e3%81%a7%e3%83%ad%e3%83%bc/">【MQL4リファレンス】MqlRates構造体の使い方！CopyRates()でローソク足データを配列取得する方法</a> は <a href="https://mql-programing.com">自動売買を作ろう！</a> に最初に表示されました。</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>【MQL4リファレンス】MqlDateTime構造体の使い方！時間帯フィルターEAを作る方法</title>
		<link>https://mql-programing.com/archives/12853/%e3%80%90mql4%e3%83%aa%e3%83%95%e3%82%a1%e3%83%ac%e3%83%b3%e3%82%b9%e3%80%91mqldatetime%e6%a7%8b%e9%80%a0%e4%bd%93%e3%81%ae%e4%bd%bf%e3%81%84%e6%96%b9%ef%bc%81%e6%99%82%e9%96%93%e5%b8%af%e3%83%95/</link>
		
		<dc:creator><![CDATA[朝日奈りさ]]></dc:creator>
		<pubDate>Tue, 31 Mar 2026 02:45:10 +0000</pubDate>
				<category><![CDATA[構造体]]></category>
		<category><![CDATA[【辞書】MQLリファレンス]]></category>
		<category><![CDATA[自動売買]]></category>
		<category><![CDATA[MqlDateTime]]></category>
		<category><![CDATA[MQL4]]></category>
		<category><![CDATA[時間帯フィルター]]></category>
		<guid isPermaLink="false">https://mql-programing.com/?p=12853</guid>

					<description><![CDATA[<p>MqlDateTime構造体とは MqlDateTime構造体は、日時情報を年・月・日・時・分・秒・曜日・年内通算日といった個別の要素に分解して格納するための構造体です。MQL4では、時間は通常datetime型（197 [&#8230;]</p>
<p>投稿 <a href="https://mql-programing.com/archives/12853/%e3%80%90mql4%e3%83%aa%e3%83%95%e3%82%a1%e3%83%ac%e3%83%b3%e3%82%b9%e3%80%91mqldatetime%e6%a7%8b%e9%80%a0%e4%bd%93%e3%81%ae%e4%bd%bf%e3%81%84%e6%96%b9%ef%bc%81%e6%99%82%e9%96%93%e5%b8%af%e3%83%95/">【MQL4リファレンス】MqlDateTime構造体の使い方！時間帯フィルターEAを作る方法</a> は <a href="https://mql-programing.com">自動売買を作ろう！</a> に最初に表示されました。</p>
]]></description>
										<content:encoded><![CDATA[<h2><span id="toc1">MqlDateTime構造体とは</span></h2>
<p>MqlDateTime構造体は、日時情報を年・月・日・時・分・秒・曜日・年内通算日といった個別の要素に分解して格納するための構造体です。MQL4では、時間は通常<code>datetime</code>型（1970年1月1日からの経過秒数）で扱われますが、「何時台か？」「何曜日か？」といった判定を行うには、MqlDateTime構造体に変換するのが便利です。</p>
<p>特にEA（エキスパートアドバイザー）開発では、<strong>特定の時間帯だけエントリーする</strong>、<strong>金曜日は取引しない</strong>、<strong>月末にポジションを閉じる</strong>など、時間条件のフィルタリングに頻繁に使用されます。</p>
<h2><span id="toc2">MqlDateTime構造体の定義</span></h2>
<p>MqlDateTime構造体は以下のメンバーで構成されています。</p>
<pre><code class="language-mql4">struct MqlDateTime
{
   int year;         // 年（例: 2024）
   int mon;          // 月（1〜12）
   int day;          // 日（1〜31）
   int hour;         // 時（0〜23）
   int min;          // 分（0〜59）
   int sec;          // 秒（0〜59）
   int day_of_week;  // 曜日（0=日曜, 1=月曜, ..., 6=土曜）
   int day_of_year;  // 年内通算日（1月1日=0）
};
</code></pre>
<h3><span id="toc3">各メンバーの詳細</span></h3>
<table>
<thead>
<tr>
<th>メンバー名</th>
<th>型</th>
<th>説明</th>
<th>値の範囲</th>
</tr>
</thead>
<tbody>
<tr>
<td>year</td>
<td>int</td>
<td>西暦年</td>
<td>1970〜</td>
</tr>
<tr>
<td>mon</td>
<td>int</td>
<td>月</td>
<td>1〜12</td>
</tr>
<tr>
<td>day</td>
<td>int</td>
<td>日</td>
<td>1〜31</td>
</tr>
<tr>
<td>hour</td>
<td>int</td>
<td>時</td>
<td>0〜23</td>
</tr>
<tr>
<td>min</td>
<td>int</td>
<td>分</td>
<td>0〜59</td>
</tr>
<tr>
<td>sec</td>
<td>int</td>
<td>秒</td>
<td>0〜59</td>
</tr>
<tr>
<td>day_of_week</td>
<td>int</td>
<td>曜日（0=日, 1=月, 2=火, 3=水, 4=木, 5=金, 6=土）</td>
<td>0〜6</td>
</tr>
<tr>
<td>day_of_year</td>
<td>int</td>
<td>年の初日からの通算日数（1月1日=0）</td>
<td>0〜365</td>
</tr>
</tbody>
</table>
<h2><span id="toc4">datetime型との変換方法</span></h2>
<p><code>datetime</code>型とMqlDateTime構造体の相互変換には、以下の2つの関数を使用します。</p>
<h3><span id="toc5">TimeToStruct() ─ datetime → MqlDateTime</span></h3>
<pre><code class="language-mql4">// datetime型をMqlDateTime構造体に変換
bool TimeToStruct(
   datetime      dt,       // 変換元のdatetime値
   MqlDateTime&  dt_struct  // 変換先の構造体（参照渡し）
);
</code></pre>
<p>戻り値は<code>bool</code>型で、変換に成功すれば<code>true</code>、失敗すれば<code>false</code>を返します。</p>
<h3><span id="toc6">StructToTime() ─ MqlDateTime → datetime</span></h3>
<pre><code class="language-mql4">// MqlDateTime構造体をdatetime型に変換
datetime StructToTime(
   MqlDateTime&  dt_struct  // 変換元の構造体（参照渡し）
);
</code></pre>
<p>戻り値は変換された<code>datetime</code>型の値です。</p>
<h2><span id="toc7">プログラム例1：基本的な使い方 ─ 現在時刻の各要素を表示する</span></h2>
<p>まずは最もシンプルな使い方です。現在のサーバー時刻をMqlDateTime構造体に変換し、各メンバーの値をチャートに表示します。</p>
<pre><code class="language-mql4">//+------------------------------------------------------------------+
//| プログラム例1：現在時刻の分解表示                                    |
//+------------------------------------------------------------------+
#property indicator_chart_window

//+------------------------------------------------------------------+
//| 曜日番号を日本語文字列に変換する関数                                  |
//+------------------------------------------------------------------+
string DayOfWeekToString(int dow)
{
   string days[] = {"日曜日","月曜日","火曜日","水曜日","木曜日","金曜日","土曜日"};
   if(dow >= 0 && dow <= 6)
      return days[dow];
   return "不明";
}

//+------------------------------------------------------------------+
//| カスタムインジケーターの初期化関数                                    |
//+------------------------------------------------------------------+
int OnInit()
{
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| メインの計算関数（ティック毎に呼ばれる）                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &#038;time[],
                const double &#038;open[],
                const double &#038;high[],
                const double &#038;low[],
                const double &#038;close[],
                const long &#038;tick_volume[],
                const long &#038;volume[],
                const int &#038;spread[])
{
   // MqlDateTime構造体の変数を宣言
   MqlDateTime dt;
   
   // 現在のサーバー時刻を取得して構造体に変換
   datetime currentTime = TimeCurrent();
   TimeToStruct(currentTime, dt);
   
   // 各メンバーの値を文字列にまとめる
   string info = "";
   info += "━━━ 現在のサーバー時刻 ━━━\n";
   info += "datetime値: " + TimeToString(currentTime, TIME_DATE|TIME_SECONDS) + "\n";
   info += "年: " + IntegerToString(dt.year) + "\n";
   info += "月: " + IntegerToString(dt.mon) + "\n";
   info += "日: " + IntegerToString(dt.day) + "\n";
   info += "時: " + IntegerToString(dt.hour) + "\n";
   info += "分: " + IntegerToString(dt.min) + "\n";
   info += "秒: " + IntegerToString(dt.sec) + "\n";
   info += "曜日: " + DayOfWeekToString(dt.day_of_week) + " (" + IntegerToString(dt.day_of_week) + ")\n";
   info += "年内通算日: " + IntegerToString(dt.day_of_year) + "\n";
   
   // チャート左上に表示
   Comment(info);
   
   return(rates_total);
}

//+------------------------------------------------------------------+
//| インジケーター削除時にコメントをクリア                                |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   Comment("");
}
</code></pre>
<p>このプログラムをインジケーターとしてチャートに適用すると、サーバー時刻の各要素がリアルタイムで表示されます。<code>TimeToStruct()</code>に<code>datetime</code>値と構造体変数を渡すだけで、自動的に年・月・日・時・分・秒・曜日が分解されることが分かります。</p>
<h2><span id="toc8">プログラム例2：時間帯フィルターの実装 ─ 東京・ロンドン・NY時間を判定する</span></h2>
<p>実際のEA開発で最も多い用途が「特定の時間帯だけ取引する」というフィルター処理です。以下の例では、東京時間・ロンドン時間・ニューヨーク時間を判定する関数を作成します。</p>
<pre><code class="language-mql4">//+------------------------------------------------------------------+
//| プログラム例2：主要市場の時間帯判定                                   |
//| ※サーバー時刻がGMT+2（冬）/ GMT+3（夏）前提の例                     |
//+------------------------------------------------------------------+

// 取引を許可する時間帯の設定（サーバー時刻基準）
input int TokyoStartHour    = 2;   // 東京時間 開始（サーバー時刻）
input int TokyoEndHour      = 9;   // 東京時間 終了（サーバー時刻）
input int LondonStartHour   = 10;  // ロンドン時間 開始
input int LondonEndHour     = 18;  // ロンドン時間 終了
input int NewYorkStartHour  = 15;  // ニューヨーク時間 開始
input int NewYorkEndHour    = 23;  // ニューヨーク時間 終了

//+------------------------------------------------------------------+
//| 指定した時間帯内かどうかを判定する汎用関数                            |
//| startHour〜endHourの範囲内であればtrueを返す                        |
//+------------------------------------------------------------------+
bool IsWithinTimeRange(int startHour, int endHour)
{
   MqlDateTime dt;
   TimeToStruct(TimeCurrent(), dt);
   
   // 日付をまたがない通常のケース（例: 10時〜18時）
   if(startHour < endHour)
   {
      return (dt.hour >= startHour && dt.hour < endHour);
   }
   // 日付をまたぐケース（例: 22時〜6時）
   else if(startHour > endHour)
   {
      return (dt.hour >= startHour || dt.hour < endHour);
   }
   
   // startHour == endHour の場合は常にfalse
   return false;
}

//+------------------------------------------------------------------+
//| 現在が東京時間かどうか                                              |
//+------------------------------------------------------------------+
bool IsTokyoSession()
{
   return IsWithinTimeRange(TokyoStartHour, TokyoEndHour);
}

//+------------------------------------------------------------------+
//| 現在がロンドン時間かどうか                                           |
//+------------------------------------------------------------------+
bool IsLondonSession()
{
   return IsWithinTimeRange(LondonStartHour, LondonEndHour);
}

//+------------------------------------------------------------------+
//| 現在がニューヨーク時間かどうか                                       |
//+------------------------------------------------------------------+
bool IsNewYorkSession()
{
   return IsWithinTimeRange(NewYorkStartHour, NewYorkEndHour);
}

//+------------------------------------------------------------------+
//| EAのメインティック処理                                              |
//+------------------------------------------------------------------+
void OnTick()
{
   // 各セッションの状態を取得
   bool tokyo   = IsTokyoSession();
   bool london  = IsLondonSession();
   bool newyork = IsNewYorkSession();
   
   // セッション情報を表示
   string sessionInfo = "【セッション状態】\n";
   sessionInfo += "東京:      " + (tokyo   ? "●アクティブ" : "○非アクティブ") + "\n";
   sessionInfo += "ロンドン:   " + (london  ? "●アクティブ" : "○非アクティブ") + "\n";
   sessionInfo += "ニューヨーク: " + (newyork ? "●アクティブ" : "○非アクティブ") + "\n";
   Comment(sessionInfo);
   
   // 例：ロンドン時間のみエントリーを許可
   if(!london)
   {
      // ロンドン時間外なので何もしない
      return;
   }
   
   // ここにロンドン時間中の取引ロジックを記述
   // ...
}
</code></pre>
<p><code>IsWithinTimeRange()</code>関数は、日付をまたぐ時間帯（例：22時〜翌6時）にも対応しています。<code>MqlDateTime</code>の<code>hour</code>メンバーを利用して、サーバー時刻が指定範囲内かどうかを効率的に判定しています。</p>
<h2><span id="toc9">プログラム例3：曜日フィルターと月末決済の実装</span></h2>
<p>次に、<code>day_of_week</code>と<code>day</code>メンバーを活用した、曜日フィルターと月末自動決済のサンプルです。</p>
<pre><code class="language-mql4">//+------------------------------------------------------------------+
//| プログラム例3：曜日フィルターと月末自動決済                            |
//+------------------------------------------------------------------+

input bool TradeOnMonday    = true;   // 月曜日に取引するか
input bool TradeOnTuesday   = true;   // 火曜日に取引するか
input bool TradeOnWednesday = true;   // 水曜日に取引するか
input bool TradeOnThursday  = true;   // 木曜日に取引するか
input bool TradeOnFriday    = false;  // 金曜日に取引するか（デフォルトOFF）
input int  FridayCloseHour  = 20;    // 金曜日の強制決済時刻

//+------------------------------------------------------------------+
//| 今日が取引許可曜日かどうかを判定                                      |
//+------------------------------------------------------------------+
bool IsTradingDay()
{
   MqlDateTime dt;
   TimeToStruct(TimeCurrent(), dt);
   
   // day_of_week: 0=日, 1=月, 2=火, 3=水, 4=木, 5=金, 6=土
   switch(dt.day_of_week)
   {
      case 1: return TradeOnMonday;
      case 2: return TradeOnTuesday;
      case 3: return TradeOnWednesday;
      case 4: return TradeOnThursday;
      case 5: return TradeOnFriday;
      default: return false;  // 土日は常に取引しない
   }
}

//+------------------------------------------------------------------+
//| 金曜日の指定時刻以降かどうかを判定（週末リスク回避用）                  |
//+------------------------------------------------------------------+
bool IsFridayCloseTime()
{
   MqlDateTime dt;
   TimeToStruct(TimeCurrent(), dt);
   
   // 金曜日（day_of_week == 5）かつ指定時刻以降
   if(dt.day_of_week == 5 && dt.hour >= FridayCloseHour)
      return true;
   
   return false;
}

//+------------------------------------------------------------------+
//| 今月の最終営業日かどうかを判定                                       |
//+------------------------------------------------------------------+
bool IsLastBusinessDayOfMonth()
{
   MqlDateTime dt;
   TimeToStruct(TimeCurrent(), dt);
   
   // 各月の最終日を定義（配列インデックス0は未使用）
   int daysInMonth[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
   
   // うるう年の判定（2月の日数を修正）
   if(dt.mon == 2)
   {
      if((dt.year % 4 == 0 && dt.year % 100 != 0) || (dt.year % 400 == 0))
         daysInMonth[2] = 29;
   }
   
   int lastDay = daysInMonth[dt.mon];
   
   // 月末の最終営業日を逆算（土日を除く）
   // lastDayの曜日を計算するため、一時的にdatetimeを生成
   MqlDateTime tempDt;
   tempDt.year = dt.year;
   tempDt.mon  = dt.mon;
   tempDt.day  = lastDay;
   tempDt.hour = 12;
   tempDt.min  = 0;
   tempDt.sec  = 0;
   
   datetime lastDayTime = StructToTime(tempDt);
   TimeToStruct(lastDayTime, tempDt);  // 曜日を再計算
   
   // 最終日が土曜なら金曜（-1日）、日曜なら金曜（-2日）
   int lastBusinessDay = lastDay;
   if(tempDt.day_of_week == 6)       // 土曜
      lastBusinessDay = lastDay - 1;
   else if(tempDt.day_of_week == 0)  // 日曜
      lastBusinessDay = lastDay - 2;
   
   return (dt.day == lastBusinessDay);
}

//+------------------------------------------------------------------+
//| 全ポジションを決済する関数                                           |
//+------------------------------------------------------------------+
void CloseAllPositions()
{
   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
      {
         if(OrderSymbol() == Symbol() && OrderMagicNumber() == 12345)
         {
            if(OrderType() == OP_BUY)
               OrderClose(OrderTicket(), OrderLots(), Bid, 3, clrRed);
            else if(OrderType() == OP_SELL)
               OrderClose(OrderTicket(), OrderLots(), Ask, 3, clrBlue);
         }
      }
   }
}

//+------------------------------------------------------------------+
//| EAのメインティック処理                                              |
//+------------------------------------------------------------------+
void OnTick()
{
   // 金曜日の指定時刻を超えたら全ポジション決済
   if(IsFridayCloseTime())
   {
      CloseAllPositions();
      Comment("金曜日 " + IntegerToString(FridayCloseHour) + "時以降のため全決済済み");
      return;
   }
   
   // 月末最終営業日も全ポジション決済
   if(IsLastBusinessDayOfMonth())
   {
      CloseAllPositions();
      Comment("月末最終営業日のため全決済済み");
      return;
   }
   
   // 取引許可曜日でなければ新規エントリーしない
   if(!IsTradingDay())
   {
      Comment("本日は取引対象外の曜日です");
      return;
   }
   
   // ここに通常の取引ロジックを記述
   Comment("取引許可中");
   // ...
}
</code></pre>
<p>この例では3つの重要なテクニックを示しています。<code>day_of_week</code>を<code>switch</code>文で分岐させる曜日フィルター、<code>hour</code>と<code>day_of_week</code>を組み合わせた金曜決済、そして<code>StructToTime()</code>を使って任意の日付のdatetime値を生成し曜日を逆算する月末判定です。</p>
<h2><span id="toc10">プログラム例4：複合時間フィルターEA ─ 時間帯・曜日・重要イベント前を統合管理</span></h2>
<p>最後に、これまでの要素を統合した実践的な複合フィルターEAのテンプレートです。</p>
<pre><code class="language-mql4">//+------------------------------------------------------------------+
//| プログラム例4：複合時間フィルターEA                                   |
//| 時間帯・曜日・特定日を統合的にフィルタリングする                       |
//+------------------------------------------------------------------+
#property strict

// === 時間帯フィルター設定 ===
input string   _TimeFilter_     = "=== 時間帯フィルター ===";   // セパレータ
input int      TradeStartHour   = 10;    // エントリー開始時刻（時）
input int      TradeStartMin    = 30;    // エントリー開始時刻（分）
input int      TradeEndHour     = 20;    // エントリー終了時刻（時）
input int      TradeEndMin      = 0;     // エントリー終了時刻（分）

// === 曜日フィルター設定 ===
input string   _DayFilter_      = "=== 曜日フィルター ===";     // セパレータ
input bool     AllowMonday      = true;  // 月曜の取引を許可
input bool     AllowTuesday     = true;  // 火曜の取引を許可
input bool     AllowWednesday   = true;  // 水曜の取引を許可
input bool     AllowThursday    = true;  // 木曜の取引を許可
input bool     AllowFriday      = true;  // 金曜の取引を許可

// === 特殊ルール設定 ===
input string   _SpecialRules_   = "=== 特殊ルール ===";         // セパレータ
input bool     AvoidMonthEnd    = true;  // 月末（最終3日間）は取引しない
input bool     AvoidYearEnd     = true;  // 年末年始（12/25〜1/3）は取引しない
input int      MagicNumber      = 20240101; // マジックナンバー

//+------------------------------------------------------------------+
//| フィルター結果を格納する構造体                                       |
//+------------------------------------------------------------------+
struct FilterResult
{
   bool  isAllowed;     // 取引許可されているか
   string reason;       // 不許可の理由（許可の場合は空文字列）
};

//+------------------------------------------------------------------+
//| 時刻を「時×60＋分」の数値に変換する補助関数                           |
//+------------------------------------------------------------------+
int TimeToMinutes(int hour, int minute)
{
   return hour * 60 + minute;
}

//+------------------------------------------------------------------+
//| 時間帯フィルターのチェック                                           |
//+------------------------------------------------------------------+
bool CheckTimeFilter(MqlDateTime &dt, string &reason)
{
   int currentMinutes = TimeToMinutes(dt.hour, dt.min);
   int startMinutes   = TimeToMinutes(TradeStartHour, TradeStartMin);
   int endMinutes     = TimeToMinutes(TradeEndHour, TradeEndMin);
   
   // 日付をまたがないケース
   if(startMinutes < endMinutes)
   {
      if(currentMinutes < startMinutes || currentMinutes >= endMinutes)
      {
         reason = StringFormat("時間外（許可: %02d:%02d〜%02d:%02d）",
                               TradeStartHour, TradeStartMin,
                               TradeEndHour, TradeEndMin);
         return false;
      }
   }
   // 日付をまたぐケース
   else if(startMinutes > endMinutes)
   {
      if(currentMinutes < startMinutes &#038;&#038; currentMinutes >= endMinutes)
      {
         reason = StringFormat("時間外（許可: %02d:%02d〜%02d:%02d）",
                               TradeStartHour, TradeStartMin,
                               TradeEndHour, TradeEndMin);
         return false;
      }
   }
   
   return true;
}

//+------------------------------------------------------------------+
//| 曜日フィルターのチェック                                             |
//+------------------------------------------------------------------+
bool CheckDayFilter(MqlDateTime &dt, string &reason)
{
   // 曜日ごとの許可フラグを配列にまとめる
   bool dayAllowed[7];
   dayAllowed[0] = false;          // 日曜（常に不許可）
   dayAllowed[1] = AllowMonday;
   dayAllowed[2] = AllowTuesday;
   dayAllowed[3] = AllowWednesday;
   dayAllowed[4] = AllowThursday;
   dayAllowed[5] = AllowFriday;
   dayAllowed[6] = false;          // 土曜（常に不許可）
   
   string dayNames[] = {"日","月","火","水","木","金","土"};
   
   if(!dayAllowed[dt.day_of_week])
   {
      reason = dayNames[dt.day_of_week] + "曜日は取引不許可";
      return false;
   }
   
   return true;
}

//+------------------------------------------------------------------+
//| 特殊日フィルターのチェック（月末・年末年始）                           |
//+------------------------------------------------------------------+
bool CheckSpecialDateFilter(MqlDateTime &dt, string &reason)
{
   // 月末チェック（最終3日間）
   if(AvoidMonthEnd)
   {
      int daysInMonth[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
      
      // うるう年判定
      if(dt.mon == 2 && ((dt.year % 4 == 0 && dt.year % 100 != 0) || (dt.year % 400 == 0)))
         daysInMonth[2] = 29;
      
      if(dt.day > daysInMonth[dt.mon] - 3)
      {
         reason = "月末期間（最終3日間）のため取引停止";
         return false;
      }
   }
   
   // 年末年始チェック（12/25〜1/3）
   if(AvoidYearEnd)
   {
      if((dt.mon == 12 && dt.day >= 25) || (dt.mon == 1 && dt.day <= 3))
      {
         reason = "年末年始期間のため取引停止";
         return false;
      }
   }
   
   return true;
}

//+------------------------------------------------------------------+
//| 全フィルターを統合チェック                                           |
//+------------------------------------------------------------------+
FilterResult CheckAllFilters()
{
   FilterResult result;
   result.isAllowed = true;
   result.reason    = "";
   
   MqlDateTime dt;
   TimeToStruct(TimeCurrent(), dt);
   
   string reason = "";
   
   // 曜日チェック（最優先）
   if(!CheckDayFilter(dt, reason))
   {
      result.isAllowed = false;
      result.reason    = reason;
      return result;
   }
   
   // 特殊日チェック
   if(!CheckSpecialDateFilter(dt, reason))
   {
      result.isAllowed = false;
      result.reason    = reason;
      return result;
   }
   
   // 時間帯チェック
   if(!CheckTimeFilter(dt, reason))
   {
      result.isAllowed = false;
      result.reason    = reason;
      return result;
   }
   
   return result;
}

//+------------------------------------------------------------------+
//| EAの初期化関数                                                     |
//+------------------------------------------------------------------+
int OnInit()
{
   Print("複合時間フィルターEA 初期化完了");
   Print("取引時間帯: ", StringFormat("%02d:%02d〜%02d:%02d",
         TradeStartHour, TradeStartMin, TradeEndHour, TradeEndMin));
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| EAのメインティック処理                                              |
//+------------------------------------------------------------------+
void OnTick()
{
   // 全フィルターの統合チェック
   FilterResult filter = CheckAllFilters();
   
   // 現在時刻情報を取得して表示用に整形
   MqlDateTime dt;
   TimeToStruct(TimeCurrent(), dt);
   string dayNames[] = {"日","月","火","水","木","金","土"};
   
   string display = "";
   display += "━━━ 複合時間フィルターEA ━━━\n";
   display += StringFormat("サーバー時刻: %04d/%02d/%02d (%s) %02d:%02d:%02d\n",
                           dt.year, dt.mon, dt.day,
                           dayNames[dt.day_of_week],
                           dt.hour, dt.min, dt.sec);
   
   if(filter.isAllowed)
   {
      display += "状態: &#x2714; 取引許可中\n";
      
      // ここに取引ロジックを記述
      // 例: シグナル判定、エントリー処理など
   }
   else
   {
      display += "状態: &#x2716; 取引停止中\n";
      display += "理由: " + filter.reason + "\n";
   }
   
   Comment(display);
}

//+------------------------------------------------------------------+
//| EA削除時の処理                                                     |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   Comment("");
   Print("複合時間フィルターEA 削除完了");
}
</code></pre>
<p>このプログラム例4では、フィルター結果を専用の<code>FilterResult</code>構造体に格納することで、取引の可否だけでなく不許可の理由も管理できるようにしています。各フィルター関数は独立しているため、必要に応じて個別に有効・無効を切り替えたり、新しいフィルター条件を追加したりすることが容易です。</p>
<h2><span id="toc11">MqlDateTime構造体を使う際の注意点</span></h2>
<h3><span id="toc12">サーバー時刻とローカル時刻の違い</span></h3>
<p><code>TimeCurrent()</code>はブローカーのサーバー時刻を返します。日本時間とは異なる場合がほとんどです。時間帯フィルターを設定する際は、必ずサーバー時刻のタイムゾーンを確認してください。ローカル時刻が必要な場合は<code>TimeLocal()</code>を使用します。</p>
<h3><span id="toc13">day_of_yearの開始値</span></h3>
<p><code>day_of_year</code>は1月1日が<strong>0</strong>から始まります。1月2日は1、12月31日は364（うるう年は365）となります。直感的に「1月1日=1日目」と考えがちなので注意が必要です。</p>
<h3><span id="toc14">StructToTime()使用時の曜日自動計算</span></h3>
<p><code>StructToTime()</code>で<code>datetime</code>に変換する際、<code>day_of_week</code>と<code>day_of_year</code>の値は無視されます。逆に、<code>TimeToStruct()</code>で変換した場合は自動的に正しい値が設定されます。プログラム例3の月末判定のように、<code>StructToTime()</code>→<code>TimeToStruct()</code>と変換することで、任意の日付の曜日を取得できます。</p>
<h2><span id="toc15">まとめ</span></h2>
<p>MqlDateTime構造体は、<code>datetime</code>型の時刻データを人間が理解しやすい個別の要素に分解するための構造体です。<code>TimeToStruct()</code>と<code>StructToTime()</code>の2つの変換関数を使うことで、時間帯フィルター・曜日フィルター・月末判定など、EA開発で頻出する時間条件のロジックを効率的に実装できます。</p>
<p>投稿 <a href="https://mql-programing.com/archives/12853/%e3%80%90mql4%e3%83%aa%e3%83%95%e3%82%a1%e3%83%ac%e3%83%b3%e3%82%b9%e3%80%91mqldatetime%e6%a7%8b%e9%80%a0%e4%bd%93%e3%81%ae%e4%bd%bf%e3%81%84%e6%96%b9%ef%bc%81%e6%99%82%e9%96%93%e5%b8%af%e3%83%95/">【MQL4リファレンス】MqlDateTime構造体の使い方！時間帯フィルターEAを作る方法</a> は <a href="https://mql-programing.com">自動売買を作ろう！</a> に最初に表示されました。</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>【MQL4関数】OnCalculate関数とは？ロウソク足が動くたびに実行されるイベント関数</title>
		<link>https://mql-programing.com/archives/3471/oncalculate/</link>
		
		<dc:creator><![CDATA[朝日奈りさ]]></dc:creator>
		<pubDate>Mon, 23 May 2022 12:19:49 +0000</pubDate>
				<category><![CDATA[関数]]></category>
		<category><![CDATA[【中級編】MQLプログラムの読み方・書き方]]></category>
		<category><![CDATA[【辞書】MQLリファレンス]]></category>
		<category><![CDATA[その他]]></category>
		<category><![CDATA[MQL]]></category>
		<category><![CDATA[リファレンス]]></category>
		<category><![CDATA[自動売買]]></category>
		<category><![CDATA[イベント関数]]></category>
		<category><![CDATA[OnCalculate]]></category>
		<category><![CDATA[インジケータ]]></category>
		<guid isPermaLink="false">https://mql-programing.com/?p=3471</guid>

					<description><![CDATA[<p>OnCalculate関数とは？ OnCalculate()は、カスタムインジケータ専用のイベント関数で、チャートに新しい価格データ（ティック）が到着するたびに自動的に呼び出されます。ロウソク足の値動きが発生するたびに実 [&#8230;]</p>
<p>投稿 <a href="https://mql-programing.com/archives/3471/oncalculate/">【MQL4関数】OnCalculate関数とは？ロウソク足が動くたびに実行されるイベント関数</a> は <a href="https://mql-programing.com">自動売買を作ろう！</a> に最初に表示されました。</p>
]]></description>
										<content:encoded><![CDATA[<h2><span id="toc1">OnCalculate関数とは？</span></h2>
<p>OnCalculate()は、<strong>カスタムインジケータ専用のイベント関数</strong>で、チャートに新しい価格データ（ティック）が到着するたびに自動的に呼び出されます。ロウソク足の値動きが発生するたびに実行されるため、移動平均線やRSIなどのテクニカル指標の計算処理をこの関数の中に記述します。</p>
<p>EA（エキスパートアドバイザー）における <code>OnTick()</code> に相当する役割を持ちますが、OnCalculate()はインジケータバッファへの描画計算に特化した設計になっています。</p>
<h2><span id="toc2">OnCalculate関数の2つの書式</span></h2>
<p>OnCalculate()には<strong>2つの書式</strong>があります。1つのインジケータで使えるのはどちらか一方のみです。</p>
<h3><span id="toc3">書式1：短縮形（シンプル版）</span></h3>
<p>チャートの価格データ（始値・高値・安値・終値など）を直接受け取る形式です。最も一般的に使われます。</p>
<pre><code class="language-mql4">int OnCalculate(const int rates_total,      // 利用可能なバーの総数
                const int prev_calculated,   // 前回呼び出し時の計算済みバー数
                const datetime &time[],      // 各バーの時刻
                const double &open[],        // 始値
                const double &high[],        // 高値
                const double &low[],         // 安値
                const double &close[],       // 終値
                const long &tick_volume[],   // ティックボリューム
                const long &volume[],        // 実ボリューム
                const int &spread[])         // スプレッド
</code></pre>
<h3><span id="toc4">書式2：OnInit()でのSetIndexBuffer連携版</span></h3>
<p>他のインジケータの値を入力として使う場合に利用します。<code>#property indicator_separate_window</code>を使うインジケータなどで使われます。</p>
<pre><code class="language-mql4">int OnCalculate(const int rates_total,      // 利用可能なデータ数
                const int prev_calculated,   // 前回呼び出し時の計算済みデータ数
                const int begin,             // 意味のあるデータの開始位置
                const double &price[])       // 計算対象の価格配列
</code></pre>
<h2><span id="toc5">引数の詳細解説</span></h2>
<table>
<thead>
<tr>
<th>引数</th>
<th>型</th>
<th>説明</th>
</tr>
</thead>
<tbody>
<tr>
<td>rates_total</td>
<td>int</td>
<td>チャート上の全バー数。<code>Bars</code>と同等の値</td>
</tr>
<tr>
<td>prev_calculated</td>
<td>int</td>
<td>前回のOnCalculate()呼び出しで返した戻り値。初回は<strong>0</strong></td>
</tr>
<tr>
<td>time[]</td>
<td>datetime</td>
<td>各バーの開始時刻の配列</td>
</tr>
<tr>
<td>open[]</td>
<td>double</td>
<td>各バーの始値の配列</td>
</tr>
<tr>
<td>high[]</td>
<td>double</td>
<td>各バーの高値の配列</td>
</tr>
<tr>
<td>low[]</td>
<td>double</td>
<td>各バーの安値の配列</td>
</tr>
<tr>
<td>close[]</td>
<td>double</td>
<td>各バーの終値の配列</td>
</tr>
<tr>
<td>tick_volume[]</td>
<td>long</td>
<td>各バーのティックボリューム</td>
</tr>
<tr>
<td>volume[]</td>
<td>long</td>
<td>各バーの実ボリューム（対応サーバーのみ）</td>
</tr>
<tr>
<td>spread[]</td>
<td>int</td>
<td>各バーのスプレッド（ポイント単位）</td>
</tr>
<tr>
<td>begin</td>
<td>int</td>
<td>（書式2のみ）有効データの開始インデックス</td>
</tr>
<tr>
<td>price[]</td>
<td>double</td>
<td>（書式2のみ）計算に使う価格データ配列</td>
</tr>
</tbody>
</table>
<h2><span id="toc6">戻り値</span></h2>
<p>OnCalculate()は<strong>int型</strong>の値を返します。この戻り値は、<strong>次回呼び出し時に <code>prev_calculated</code> 引数として渡されます</strong>。</p>
<ul>
<li>通常は <code>rates_total</code> を返します（すべてのバーを計算済みという意味）</li>
<li><strong>0を返す</strong>と、次回の呼び出しで全バーを再計算します</li>
<li>初回呼び出し時は <code>prev_calculated</code> が0になるため、全バーの計算が行われます</li>
</ul>
<h2><span id="toc7">prev_calculatedの仕組み（重要）</span></h2>
<p><code>prev_calculated</code> は、OnCalculate()の<strong>パフォーマンス最適化の鍵</strong>となる引数です。毎回すべてのバーを計算し直すのではなく、新しいバーだけを計算することで処理を高速化します。</p>
<pre><code class="language-mql4">// prev_calculatedの動作イメージ
// 
// 1回目の呼び出し: prev_calculated = 0  → 全バーを計算
//   戻り値として rates_total（例：1000）を返す
//
// 2回目の呼び出し: prev_calculated = 1000
//   rates_total = 1000（新しいバーなし）→ 最後のバーだけ更新
//   rates_total = 1001（新しいバーあり）→ 新しいバーだけ計算
</code></pre>
<h2><span id="toc8">プログラム例1：最もシンプルな終値ラインインジケータ</span></h2>
<p>まずは最も基本的な例として、終値をそのままラインとして描画するインジケータを作成します。OnCalculate()の基本構造と<code>prev_calculated</code>の使い方を理解するのに最適です。</p>
<pre><code class="language-mql4">//+------------------------------------------------------------------+
//| 終値ラインインジケータ（OnCalculateの基本構造を学ぶ）             |
//+------------------------------------------------------------------+
#property indicator_chart_window        // メインチャートに表示
#property indicator_buffers 1           // バッファ数：1本
#property indicator_color1 clrDodgerBlue // ラインの色
#property indicator_width1 2            // ラインの太さ

// インジケータバッファ
double LineBuffer[];

//+------------------------------------------------------------------+
//| 初期化関数                                                        |
//+------------------------------------------------------------------+
int OnInit()
{
   // バッファをインジケータラインとして割り当て
   SetIndexBuffer(0, LineBuffer);
   SetIndexStyle(0, DRAW_LINE);
   SetIndexLabel(0, "終値ライン");
   
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| OnCalculate - ティックごとに呼ばれるメイン関数                    |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
{
   // 計算開始位置を決定
   // prev_calculated == 0 なら全バーを計算（初回 or 再計算）
   // それ以外なら、最後のバーから計算（効率化）
   int start;
   if(prev_calculated == 0)
      start = 0;                        // 初回：最初のバーから
   else
      start = prev_calculated - 1;      // 2回目以降：最後のバーから
   
   // 計算ループ
   for(int i = start; i < rates_total; i++)
   {
      LineBuffer[i] = close[i];         // 終値をそのままバッファに格納
   }
   
   // rates_totalを返すことで、次回のprev_calculatedに反映される
   return(rates_total);
}
</code></pre>
<p>このコードのポイントは、<code>prev_calculated</code>を活用した計算開始位置の決定です。初回はすべてのバーを計算し、2回目以降は最後のバーだけを更新するため、処理が非常に軽くなります。</p>
<h2><span id="toc9">プログラム例2：単純移動平均（SMA）インジケータ</span></h2>
<p>次に、実用的な単純移動平均線（SMA）を自作します。外部パラメータで期間を変更でき、<code>prev_calculated</code>による最適化も実装しています。</p>
<pre><code class="language-mql4">//+------------------------------------------------------------------+
//| 自作SMA（単純移動平均線）インジケータ                              |
//+------------------------------------------------------------------+
#property indicator_chart_window
#property indicator_buffers 1
#property indicator_color1 clrRed
#property indicator_width1 2

// 外部パラメータ（ユーザーが変更可能）
input int    MaPeriod = 20;             // 移動平均の期間
input int    MaShift  = 0;              // 表示をシフトするバー数

// インジケータバッファ
double MaBuffer[];

//+------------------------------------------------------------------+
//| 初期化関数                                                        |
//+------------------------------------------------------------------+
int OnInit()
{
   // パラメータチェック
   if(MaPeriod < 1)
   {
      Print("エラー: 期間は1以上を指定してください");
      return(INIT_PARAMETERS_INCORRECT);
   }
   
   SetIndexBuffer(0, MaBuffer);
   SetIndexStyle(0, DRAW_LINE);
   SetIndexShift(0, MaShift);           // ラインの水平シフト
   SetIndexLabel(0, "SMA(" + IntegerToString(MaPeriod) + ")");
   
   // 最初のMaPeriod-1本は描画しない（データ不足のため）
   SetIndexDrawBegin(0, MaPeriod);
   
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| OnCalculate - SMAの計算処理                                       |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &#038;time[],
                const double &#038;open[],
                const double &#038;high[],
                const double &#038;low[],
                const double &#038;close[],
                const long &#038;tick_volume[],
                const long &#038;volume[],
                const int &#038;spread[])
{
   // データが移動平均の期間に満たない場合は計算しない
   if(rates_total < MaPeriod)
      return(0);
   
   // 計算開始位置の決定
   int start;
   if(prev_calculated == 0)
      start = MaPeriod - 1;             // 初回：期間分のデータが揃った位置から
   else
      start = prev_calculated - 1;      // 2回目以降：最後のバーから
   
   // SMAの計算ループ
   for(int i = start; i < rates_total; i++)
   {
      double sum = 0.0;
      
      // 現在のバーからMaPeriod本分の終値を合計
      for(int j = 0; j < MaPeriod; j++)
      {
         sum += close[i - j];
      }
      
      // 平均値をバッファに格納
      MaBuffer[i] = sum / MaPeriod;
   }
   
   return(rates_total);
}
</code></pre>
<p><code>SetIndexDrawBegin()</code> を使って、データが不足する最初の数本のバーでは描画を行わないようにしている点に注目してください。</p>
<h2><span id="toc10">プログラム例3：高値・安値バンドインジケータ（複数バッファ）</span></h2>
<p>複数のインジケータバッファを使い、指定期間の高値ラインと安値ラインを同時に描画する例です。ドンチャンチャネルに似たバンド型インジケータを作成します。</p>
<pre><code class="language-mql4">//+------------------------------------------------------------------+
//| 高値・安値バンドインジケータ（ドンチャンチャネル風）               |
//+------------------------------------------------------------------+
#property indicator_chart_window
#property indicator_buffers 3           // バッファ3本（高値・安値・中間線）
#property indicator_color1 clrDeepSkyBlue   // 高値バンド
#property indicator_color2 clrOrangeRed     // 安値バンド
#property indicator_color3 clrGray          // 中間線
#property indicator_width1 2
#property indicator_width2 2
#property indicator_style3 STYLE_DOT    // 中間線は点線

// 外部パラメータ
input int BandPeriod = 20;              // バンドの期間

// インジケータバッファ
double HighBand[];                      // 高値バンド
double LowBand[];                       // 安値バンド
double MidLine[];                       // 中間線

//+------------------------------------------------------------------+
//| 初期化関数                                                        |
//+------------------------------------------------------------------+
int OnInit()
{
   if(BandPeriod < 2)
   {
      Print("エラー: 期間は2以上を指定してください");
      return(INIT_PARAMETERS_INCORRECT);
   }
   
   // バッファの割り当て
   SetIndexBuffer(0, HighBand);
   SetIndexBuffer(1, LowBand);
   SetIndexBuffer(2, MidLine);
   
   // 描画スタイルの設定
   SetIndexStyle(0, DRAW_LINE);
   SetIndexStyle(1, DRAW_LINE);
   SetIndexStyle(2, DRAW_LINE);
   
   // ラベル設定
   SetIndexLabel(0, "高値バンド(" + IntegerToString(BandPeriod) + ")");
   SetIndexLabel(1, "安値バンド(" + IntegerToString(BandPeriod) + ")");
   SetIndexLabel(2, "中間線");
   
   // 描画開始位置
   SetIndexDrawBegin(0, BandPeriod);
   SetIndexDrawBegin(1, BandPeriod);
   SetIndexDrawBegin(2, BandPeriod);
   
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| OnCalculate - 高値・安値バンドの計算                               |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &#038;time[],
                const double &#038;open[],
                const double &#038;high[],
                const double &#038;low[],
                const double &#038;close[],
                const long &#038;tick_volume[],
                const long &#038;volume[],
                const int &#038;spread[])
{
   // データ不足チェック
   if(rates_total < BandPeriod)
      return(0);
   
   // 計算開始位置
   int start;
   if(prev_calculated == 0)
      start = BandPeriod - 1;
   else
      start = prev_calculated - 1;
   
   // バンドの計算
   for(int i = start; i < rates_total; i++)
   {
      double highestHigh = high[i];     // 最高値の初期値
      double lowestLow   = low[i];      // 最安値の初期値
      
      // 過去BandPeriod本のバーから最高値・最安値を探す
      for(int j = 0; j < BandPeriod; j++)
      {
         if(high[i - j] > highestHigh)
            highestHigh = high[i - j];
         if(low[i - j] < lowestLow)
            lowestLow = low[i - j];
      }
      
      // バッファに格納
      HighBand[i] = highestHigh;        // 期間内の最高値
      LowBand[i]  = lowestLow;          // 期間内の最安値
      MidLine[i]   = (highestHigh + lowestLow) / 2.0;  // 中間値
   }
   
   return(rates_total);
}
</code></pre>
<p>3本のバッファを同時に使うことで、チャート上にバンド（帯域）を視覚的に表示できます。<code>SetIndexStyle()</code>で各ラインの線種を変えることで、見分けやすくなります。</p>
<h2><span id="toc11">プログラム例4：書式2を使った価格モメンタムインジケータ（サブウインドウ）</span></h2>
<p>書式2（price[]配列を使う形式）の例です。サブウインドウに表示するモメンタム系インジケータを作成します。ユーザーがチャート上で「適用価格」を選択でき、その値がprice[]として渡されます。</p>
<pre><code class="language-mql4">//+------------------------------------------------------------------+
//| モメンタムインジケータ（OnCalculate書式2を使用）                   |
//+------------------------------------------------------------------+
#property indicator_separate_window      // サブウインドウに表示
#property indicator_buffers 2
#property indicator_color1 clrLimeGreen  // モメンタムライン
#property indicator_color2 clrRed        // ゼロライン用（水平線）
#property indicator_width1 2
#property indicator_level1 0.0           // ゼロラインを自動描画
#property indicator_levelcolor clrSilver

// 外部パラメータ
input int MomentumPeriod = 14;          // モメンタム期間

// インジケータバッファ
double MomentumBuffer[];
double SignalBuffer[];                   // シグナル用（SMA平滑化）

//+------------------------------------------------------------------+
//| 初期化関数                                                        |
//+------------------------------------------------------------------+
int OnInit()
{
   if(MomentumPeriod < 1)
   {
      Print("エラー: 期間は1以上を指定してください");
      return(INIT_PARAMETERS_INCORRECT);
   }
   
   SetIndexBuffer(0, MomentumBuffer);
   SetIndexBuffer(1, SignalBuffer);
   
   SetIndexStyle(0, DRAW_LINE);
   SetIndexStyle(1, DRAW_LINE);
   
   SetIndexLabel(0, "Momentum(" + IntegerToString(MomentumPeriod) + ")");
   SetIndexLabel(1, "Signal(9)");
   
   SetIndexDrawBegin(0, MomentumPeriod);
   SetIndexDrawBegin(1, MomentumPeriod + 9);
   
   // インジケータの短縮名（サブウインドウのタイトル）
   IndicatorShortName("My Momentum(" + IntegerToString(MomentumPeriod) + ")");
   
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| OnCalculate - 書式2：price[]配列を使う形式                        |
//| ※ユーザーがインジケータの「適用価格」で選択した値がprice[]に入る   |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,             // 有効データの開始位置
                const double &#038;price[])       // 適用価格の配列
{
   // データ不足チェック
   if(rates_total < MomentumPeriod + 1)
      return(0);
   
   // ===== モメンタムの計算 =====
   int start;
   if(prev_calculated == 0)
      start = MomentumPeriod + begin;   // beginを考慮して開始位置を決定
   else
      start = prev_calculated - 1;
   
   // モメンタム = 現在価格 - N期間前の価格
   for(int i = start; i < rates_total; i++)
   {
      MomentumBuffer[i] = price[i] - price[i - MomentumPeriod];
   }
   
   // ===== シグナルライン（モメンタムの9期間SMA）=====
   int signalPeriod = 9;
   int signalStart;
   
   if(prev_calculated == 0)
      signalStart = MomentumPeriod + begin + signalPeriod - 1;
   else
      signalStart = prev_calculated - 1;
   
   // signalStartがデータ範囲内か確認
   if(signalStart < MomentumPeriod + begin + signalPeriod - 1)
      signalStart = MomentumPeriod + begin + signalPeriod - 1;
   
   for(int i = signalStart; i < rates_total; i++)
   {
      double sum = 0.0;
      for(int j = 0; j < signalPeriod; j++)
      {
         sum += MomentumBuffer[i - j];
      }
      SignalBuffer[i] = sum / signalPeriod;
   }
   
   return(rates_total);
}
</code></pre>
<p>書式2では、ユーザーがインジケータのプロパティで「適用価格」（終値、始値、高値、安値、中間値など）を選択でき、その値が<code>price[]</code>として自動的に渡されます。<code>begin</code>パラメータは、他のインジケータの出力を入力にチェーンする場合に、有効なデータの開始位置を示します。</p>
<h2><span id="toc12">よくある使い方のポイント・注意事項</span></h2>
<h3><span id="toc13">1. prev_calculatedの正しい活用</span></h3>
<p>OnCalculate()で最も重要なのは<code>prev_calculated</code>の活用です。毎回全バーを再計算すると、バー数が多い場合に処理が重くなります。</p>
<pre><code class="language-mql4">// ◎ 正しい使い方：差分計算
int start;
if(prev_calculated == 0)
   start = 0;              // 初回のみ全バー計算
else
   start = prev_calculated - 1;  // -1 は最後のバー（形成中）を再計算するため

// ✕ 悪い使い方：毎回全バー計算（非効率）
for(int i = 0; i < rates_total; i++)  // 毎回全部計算してしまう
</code></pre>
<h3><span id="toc14">2. 配列のインデックス方向に注意</span></h3>
<p>OnCalculate()の引数で渡される配列（close[], high[]など）は、<strong>デフォルトで時系列ではない</strong>点に注意が必要です。</p>
<pre><code class="language-mql4">// OnCalculate内の配列のインデックス
// close[0]                → 最も古いバーの終値
// close[rates_total - 1]  → 最新（現在形成中の）バーの終値
//
// ※通常のClose[0]が最新バーを指すのとは逆！
//
// もし時系列順にしたい場合は以下を使う（ただし通常は不要）
// ArraySetAsSeries(close, true);
// ※ただし const 配列なので変更できない場合がある
</code></pre>
<h3><span id="toc15">3. 戻り値は必ずrates_totalを返す</span></h3>
<pre><code class="language-mql4">// 正常な計算が完了した場合
return(rates_total);     // 次回のprev_calculatedにこの値が入る

// エラーや計算不能の場合
return(0);               // 次回は全バーを再計算する
</code></pre>
<h3><span id="toc16">4. OnCalculate()はEAやスクリプトでは使えない</span></h3>
<p>OnCalculate()は<strong>カスタムインジケータ（.mq4ファイルの#property indicator_～ がある場合）でのみ</strong>使用できます。EAではOnTick()、スクリプトではOnStart()を使用してください。</p>
<h3><span id="toc17">5. rates_totalの変化を検知して新しいバーを判定</span></h3>
<pre><code class="language-mql4">int OnCalculate(const int rates_total,
                const int prev_calculated,
                /* 省略 */)
{
   // 新しいバーが形成されたかどうかの判定
   if(rates_total > prev_calculated)
   {
      // 新しいバーが追加された（または初回呼び出し）
      Print("新しいバーが追加されました。バー総数: ", rates_total);
   }
   else
   {
      // 既存のバー（最新バー）のティック更新
      // 最新バーの終値だけが変わっている
   }
   
   return(rates_total);
}
</code></pre>
<h3><span id="toc18">6. EMPTY_VALUEで描画をスキップ</span></h3>
<p>特定の条件でラインを描画しない場合は、バッファに<code>EMPTY_VALUE</code>を設定します。</p>
<pre><code class="language-mql4">// 条件を満たさない場合は描画しない
if(close[i] < open[i])
   LineBuffer[i] = EMPTY_VALUE;  // この位置はラインが途切れる
else
   LineBuffer[i] = close[i];
</code></pre>
<h2><span id="toc19">まとめ</span></h2>
<ul>
<li>OnCalculate()は<strong>カスタムインジケータ専用</strong>のイベント関数で、ティックごとに自動実行される</li>
<li><strong>2つの書式</strong>があり、用途に応じて使い分ける（書式1がメイン、書式2は他インジケータの値を入力にする場合）</li>
<li><code>prev_calculated</code>を活用して<strong>差分計算</strong>を行い、パフォーマンスを最適化するのが基本</li>
<li>引数の配列は<strong>時系列ではない</strong>（インデックス0が最古、rates_total-1が最新）</li>
<li>戻り値として<code>rates_total</code>を返すことで、次回の<code>prev_calculated</code>に計算済みバー数を伝える</li>
<li>描画をスキップしたい場合は<code>EMPTY_VALUE</code>を使用する</li>
</ul>
<p>投稿 <a href="https://mql-programing.com/archives/3471/oncalculate/">【MQL4関数】OnCalculate関数とは？ロウソク足が動くたびに実行されるイベント関数</a> は <a href="https://mql-programing.com">自動売買を作ろう！</a> に最初に表示されました。</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>【MQL4関数】OnChartEvent関数とは？チャート操作を行なったときに実行されるイベント関数</title>
		<link>https://mql-programing.com/archives/3451/onchartevent/</link>
		
		<dc:creator><![CDATA[朝日奈りさ]]></dc:creator>
		<pubDate>Sun, 22 May 2022 08:32:00 +0000</pubDate>
				<category><![CDATA[関数]]></category>
		<category><![CDATA[【中級編】MQLプログラムの読み方・書き方]]></category>
		<category><![CDATA[【辞書】MQLリファレンス]]></category>
		<category><![CDATA[その他]]></category>
		<category><![CDATA[MQL]]></category>
		<category><![CDATA[リファレンス]]></category>
		<category><![CDATA[自動売買]]></category>
		<category><![CDATA[OnChartEvent]]></category>
		<category><![CDATA[イベント関数]]></category>
		<guid isPermaLink="false">https://mql-programing.com/?p=3451</guid>

					<description><![CDATA[<p>OnChartEvent関数とは？ OnChartEvent関数は、チャート上で発生するさまざまなイベント（マウスクリック、キーボード入力、オブジェクトの操作など）を検知して処理を実行するためのイベントハンドラ関数です。 [&#8230;]</p>
<p>投稿 <a href="https://mql-programing.com/archives/3451/onchartevent/">【MQL4関数】OnChartEvent関数とは？チャート操作を行なったときに実行されるイベント関数</a> は <a href="https://mql-programing.com">自動売買を作ろう！</a> に最初に表示されました。</p>
]]></description>
										<content:encoded><![CDATA[<h2><span id="toc1">OnChartEvent関数とは？</span></h2>
<p>OnChartEvent関数は、チャート上で発生するさまざまなイベント（マウスクリック、キーボード入力、オブジェクトの操作など）を検知して処理を実行するための<strong>イベントハンドラ関数</strong>です。エキスパートアドバイザー（EA）やインジケーターで使用でき、ユーザーのチャート操作に応じたインタラクティブな処理を実装できます。</p>
<p>この関数は、MQL4が自動的に呼び出す<strong>定義済みイベント関数</strong>の一つで、開発者が直接呼び出すものではありません。チャート上でイベントが発生するたびに、MT4のランタイムがこの関数を自動的に実行します。</p>
<h2><span id="toc2">基本的な書式</span></h2>
<pre><code class="language-mql4">void OnChartEvent(const int id,         // イベントID
                  const long &lparam,    // long型イベントパラメータ
                  const double &dparam,  // double型イベントパラメータ
                  const string &sparam)  // string型イベントパラメータ
</code></pre>
<h3><span id="toc3">引数の説明</span></h3>
<table>
<thead>
<tr>
<th>引数名</th>
<th>型</th>
<th>説明</th>
</tr>
</thead>
<tbody>
<tr>
<td>id</td>
<td>const int</td>
<td>イベントの種類を示すID。CHARTEVENT列挙値が入る</td>
</tr>
<tr>
<td>lparam</td>
<td>const long&amp;</td>
<td>イベントに応じたlong型パラメータ（キーコード、X座標など）</td>
</tr>
<tr>
<td>dparam</td>
<td>const double&amp;</td>
<td>イベントに応じたdouble型パラメータ（Y座標、価格など）</td>
</tr>
<tr>
<td>sparam</td>
<td>const string&amp;</td>
<td>イベントに応じたstring型パラメータ（オブジェクト名など）</td>
</tr>
</tbody>
</table>
<h3><span id="toc4">戻り値</span></h3>
<p>OnChartEvent関数の戻り値は<strong>void</strong>（なし）です。値を返す必要はありません。</p>
<h2><span id="toc5">主要なイベントID一覧</span></h2>
<p>id引数に渡されるイベントIDは、CHARTEVENT列挙型で定義されています。代表的なものを以下に示します。</p>
<table>
<thead>
<tr>
<th>イベントID</th>
<th>説明</th>
<th>lparam</th>
<th>dparam</th>
<th>sparam</th>
</tr>
</thead>
<tbody>
<tr>
<td>CHARTEVENT_KEYDOWN</td>
<td>キーが押された</td>
<td>キーコード</td>
<td>繰り返し回数</td>
<td>&#8211;</td>
</tr>
<tr>
<td>CHARTEVENT_MOUSE_MOVE</td>
<td>マウスが移動した</td>
<td>X座標</td>
<td>Y座標</td>
<td>マウスボタンフラグ</td>
</tr>
<tr>
<td>CHARTEVENT_OBJECT_CREATE</td>
<td>オブジェクトが作成された</td>
<td>&#8211;</td>
<td>&#8211;</td>
<td>オブジェクト名</td>
</tr>
<tr>
<td>CHARTEVENT_OBJECT_CHANGE</td>
<td>オブジェクトが変更された</td>
<td>&#8211;</td>
<td>&#8211;</td>
<td>オブジェクト名</td>
</tr>
<tr>
<td>CHARTEVENT_OBJECT_DELETE</td>
<td>オブジェクトが削除された</td>
<td>&#8211;</td>
<td>&#8211;</td>
<td>オブジェクト名</td>
</tr>
<tr>
<td>CHARTEVENT_CLICK</td>
<td>チャートがクリックされた</td>
<td>X座標</td>
<td>Y座標</td>
<td>&#8211;</td>
</tr>
<tr>
<td>CHARTEVENT_OBJECT_CLICK</td>
<td>オブジェクトがクリックされた</td>
<td>X座標</td>
<td>Y座標</td>
<td>オブジェクト名</td>
</tr>
<tr>
<td>CHARTEVENT_OBJECT_DRAG</td>
<td>オブジェクトがドラッグされた</td>
<td>&#8211;</td>
<td>&#8211;</td>
<td>オブジェクト名</td>
</tr>
<tr>
<td>CHARTEVENT_OBJECT_ENDEDIT</td>
<td>テキスト編集が完了した</td>
<td>&#8211;</td>
<td>&#8211;</td>
<td>オブジェクト名</td>
</tr>
<tr>
<td>CHARTEVENT_CHART_CHANGE</td>
<td>チャートのサイズ等が変更された</td>
<td>&#8211;</td>
<td>&#8211;</td>
<td>&#8211;</td>
</tr>
<tr>
<td>CHARTEVENT_CUSTOM</td>
<td>カスタムイベントの最初のID</td>
<td>カスタム値</td>
<td>カスタム値</td>
<td>カスタム値</td>
</tr>
<tr>
<td>CHARTEVENT_CUSTOM_LAST</td>
<td>カスタムイベントの最後のID</td>
<td>カスタム値</td>
<td>カスタム値</td>
<td>カスタム値</td>
</tr>
</tbody>
</table>
<h2><span id="toc6">プログラム例1：全イベントをログ出力する基本例</span></h2>
<p>まずは最も基本的な使い方として、チャート上で発生するすべてのイベントをエキスパートログに出力するサンプルです。どのようなイベントが発生しているかを確認する際に便利です。</p>
<pre><code class="language-mql4">//+------------------------------------------------------------------+
//| OnChartEvent基本サンプル：全イベントのログ出力                      |
//+------------------------------------------------------------------+
#property strict

//+------------------------------------------------------------------+
//| 初期化関数                                                        |
//+------------------------------------------------------------------+
int OnInit()
{
   // オブジェクト作成・削除イベントを有効にする
   ChartSetInteger(0, CHART_EVENT_OBJECT_CREATE, true);
   ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, true);
   
   Print("=== OnChartEventログ出力サンプル開始 ===");
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| ティック関数（必須だが今回は処理なし）                               |
//+------------------------------------------------------------------+
void OnTick()
{
   // 特に処理なし
}

//+------------------------------------------------------------------+
//| チャートイベント処理関数                                           |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
{
   // イベントIDに応じてイベント名を判定
   string eventName = "";
   
   switch(id)
   {
      case CHARTEVENT_KEYDOWN:
         eventName = "キーダウン";
         // lparamにキーコードが入る
         Print(eventName, " | キーコード=", lparam, 
               " | 文字='", ShortToString((ushort)lparam), "'");
         break;
         
      case CHARTEVENT_CLICK:
         eventName = "チャートクリック";
         // lparamにX座標、dparamにY座標が入る
         Print(eventName, " | X=", lparam, " | Y=", dparam);
         break;
         
      case CHARTEVENT_OBJECT_CLICK:
         eventName = "オブジェクトクリック";
         // sparamにオブジェクト名が入る
         Print(eventName, " | オブジェクト名=", sparam,
               " | X=", lparam, " | Y=", dparam);
         break;
         
      case CHARTEVENT_OBJECT_CREATE:
         eventName = "オブジェクト作成";
         Print(eventName, " | オブジェクト名=", sparam);
         break;
         
      case CHARTEVENT_OBJECT_DELETE:
         eventName = "オブジェクト削除";
         Print(eventName, " | オブジェクト名=", sparam);
         break;
         
      case CHARTEVENT_OBJECT_CHANGE:
         eventName = "オブジェクト変更";
         Print(eventName, " | オブジェクト名=", sparam);
         break;
         
      case CHARTEVENT_OBJECT_DRAG:
         eventName = "オブジェクトドラッグ";
         Print(eventName, " | オブジェクト名=", sparam);
         break;
         
      case CHARTEVENT_CHART_CHANGE:
         eventName = "チャート変更";
         Print(eventName, "（サイズ変更、スクロール等）");
         break;
         
      default:
         // カスタムイベントまたは未分類のイベント
         if(id >= CHARTEVENT_CUSTOM && id <= CHARTEVENT_CUSTOM_LAST)
         {
            Print("カスタムイベント | ID=", id - CHARTEVENT_CUSTOM,
                  " | lparam=", lparam,
                  " | dparam=", dparam,
                  " | sparam=", sparam);
         }
         else
         {
            Print("不明なイベント | ID=", id);
         }
         break;
   }
}
</code></pre>
<p>このサンプルを動かすと、チャート上で操作を行うたびにエキスパートタブにイベント情報が出力されます。<code>ChartSetInteger</code>でオブジェクトの作成・削除イベントを有効にしている点がポイントです。これを設定しないと、CHARTEVENT_OBJECT_CREATEとCHARTEVENT_OBJECT_DELETEイベントは発生しません。</p>
<h2><span id="toc7">プログラム例2：ボタンクリックで売買を行うパネルEA</span></h2>
<p>チャート上にBuy/Sellボタンを配置し、クリックで注文を出す実用的なサンプルです。GUIベースのトレードパネルの基本構造として応用できます。</p>
<pre><code class="language-mql4">//+------------------------------------------------------------------+
//| ボタンクリックで売買を行うパネルEA                                  |
//+------------------------------------------------------------------+
#property strict

// 入力パラメータ
input double LotSize    = 0.01;   // ロットサイズ
input int    Slippage   = 3;      // スリッページ
input int    StopLoss   = 50;     // ストップロス（ポイント）
input int    TakeProfit = 100;    // テイクプロフィット（ポイント）

// ボタン名の定数
#define BTN_BUY    "BtnBuy"
#define BTN_SELL   "BtnSell"
#define BTN_CLOSE  "BtnCloseAll"
#define LBL_INFO   "LblInfo"

//+------------------------------------------------------------------+
//| ボタンを作成する関数                                               |
//+------------------------------------------------------------------+
bool CreateButton(string name, string text, int x, int y, 
                  int width, int height, color bgColor, color textColor)
{
   // ボタンオブジェクトを作成
   if(!ObjectCreate(0, name, OBJ_BUTTON, 0, 0, 0))
   {
      Print("ボタン作成失敗: ", name);
      return false;
   }
   
   // ボタンのプロパティを設定
   ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
   ObjectSetInteger(0, name, OBJPROP_XSIZE, width);
   ObjectSetInteger(0, name, OBJPROP_YSIZE, height);
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   ObjectSetInteger(0, name, OBJPROP_COLOR, textColor);
   ObjectSetInteger(0, name, OBJPROP_BGCOLOR, bgColor);
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, 10);
   ObjectSetInteger(0, name, OBJPROP_BORDER_COLOR, clrBlack);
   
   return true;
}

//+------------------------------------------------------------------+
//| 情報ラベルを作成する関数                                           |
//+------------------------------------------------------------------+
void CreateInfoLabel()
{
   ObjectCreate(0, LBL_INFO, OBJ_LABEL, 0, 0, 0);
   ObjectSetInteger(0, LBL_INFO, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, LBL_INFO, OBJPROP_XDISTANCE, 20);
   ObjectSetInteger(0, LBL_INFO, OBJPROP_YDISTANCE, 150);
   ObjectSetInteger(0, LBL_INFO, OBJPROP_COLOR, clrWhite);
   ObjectSetInteger(0, LBL_INFO, OBJPROP_FONTSIZE, 9);
   ObjectSetString(0, LBL_INFO, OBJPROP_TEXT, "ボタンをクリックしてください");
}

//+------------------------------------------------------------------+
//| 情報ラベルを更新する関数                                           |
//+------------------------------------------------------------------+
void UpdateInfoLabel(string message)
{
   ObjectSetString(0, LBL_INFO, OBJPROP_TEXT, message);
   ChartRedraw(0);
}

//+------------------------------------------------------------------+
//| 初期化関数                                                        |
//+------------------------------------------------------------------+
int OnInit()
{
   // パネルのボタンを作成
   CreateButton(BTN_BUY,   "BUY",       20, 30, 120, 40, clrDodgerBlue,  clrWhite);
   CreateButton(BTN_SELL,  "SELL",       20, 75, 120, 40, clrCrimson,     clrWhite);
   CreateButton(BTN_CLOSE, "CLOSE ALL",  20, 120, 120, 25, clrDimGray,    clrWhite);
   
   // 情報表示ラベルを作成
   CreateInfoLabel();
   
   ChartRedraw(0);
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| 終了処理関数                                                      |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   // 作成したオブジェクトを全て削除
   ObjectDelete(0, BTN_BUY);
   ObjectDelete(0, BTN_SELL);
   ObjectDelete(0, BTN_CLOSE);
   ObjectDelete(0, LBL_INFO);
}

//+------------------------------------------------------------------+
//| ティック関数                                                      |
//+------------------------------------------------------------------+
void OnTick()
{
   // 特に処理なし
}

//+------------------------------------------------------------------+
//| 買い注文を送信する関数                                             |
//+------------------------------------------------------------------+
void ExecuteBuy()
{
   double sl = 0, tp = 0;
   double price = Ask;
   
   // ストップロス・テイクプロフィットを計算
   if(StopLoss > 0)
      sl = price - StopLoss * _Point;
   if(TakeProfit > 0)
      tp = price + TakeProfit * _Point;
   
   int ticket = OrderSend(Symbol(), OP_BUY, LotSize, price, Slippage, sl, tp,
                           "Panel Buy", 0, 0, clrBlue);
   
   if(ticket > 0)
      UpdateInfoLabel("BUY注文成功 Ticket#" + IntegerToString(ticket));
   else
      UpdateInfoLabel("BUY注文失敗 Error=" + IntegerToString(GetLastError()));
}

//+------------------------------------------------------------------+
//| 売り注文を送信する関数                                             |
//+------------------------------------------------------------------+
void ExecuteSell()
{
   double sl = 0, tp = 0;
   double price = Bid;
   
   if(StopLoss > 0)
      sl = price + StopLoss * _Point;
   if(TakeProfit > 0)
      tp = price - TakeProfit * _Point;
   
   int ticket = OrderSend(Symbol(), OP_SELL, LotSize, price, Slippage, sl, tp,
                           "Panel Sell", 0, 0, clrRed);
   
   if(ticket > 0)
      UpdateInfoLabel("SELL注文成功 Ticket#" + IntegerToString(ticket));
   else
      UpdateInfoLabel("SELL注文失敗 Error=" + IntegerToString(GetLastError()));
}

//+------------------------------------------------------------------+
//| 全ポジションを決済する関数                                         |
//+------------------------------------------------------------------+
void CloseAllPositions()
{
   int closedCount = 0;
   
   // 全注文を逆順にループして決済
   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
         continue;
      
      // 現在のシンボルのポジションのみ対象
      if(OrderSymbol() != Symbol())
         continue;
      
      bool result = false;
      
      if(OrderType() == OP_BUY)
         result = OrderClose(OrderTicket(), OrderLots(), Bid, Slippage, clrBlue);
      else if(OrderType() == OP_SELL)
         result = OrderClose(OrderTicket(), OrderLots(), Ask, Slippage, clrRed);
      
      if(result)
         closedCount++;
   }
   
   UpdateInfoLabel(IntegerToString(closedCount) + "件のポジションを決済しました");
}

//+------------------------------------------------------------------+
//| チャートイベント処理関数                                           |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
{
   // オブジェクトクリックイベントのみ処理
   if(id == CHARTEVENT_OBJECT_CLICK)
   {
      // クリックされたオブジェクト名で分岐
      if(sparam == BTN_BUY)
      {
         ExecuteBuy();
         // ボタンの押下状態を解除（元に戻す）
         ObjectSetInteger(0, BTN_BUY, OBJPROP_STATE, false);
      }
      else if(sparam == BTN_SELL)
      {
         ExecuteSell();
         ObjectSetInteger(0, BTN_SELL, OBJPROP_STATE, false);
      }
      else if(sparam == BTN_CLOSE)
      {
         CloseAllPositions();
         ObjectSetInteger(0, BTN_CLOSE, OBJPROP_STATE, false);
      }
      
      ChartRedraw(0);
   }
}
</code></pre>
<p>このサンプルのポイントは、ボタンクリック後に<code>ObjectSetInteger</code>で<code>OBJPROP_STATE</code>をfalseに戻している点です。これを行わないと、ボタンが押されたままの状態で表示され続けます。</p>
<h2><span id="toc8">プログラム例3：キーボードショートカットで機能を切り替えるインジケーター</span></h2>
<p>キーボード入力（CHARTEVENT_KEYDOWN）を使って、インジケーターの表示モードをキーボードショートカットで切り替えるサンプルです。</p>
<pre><code class="language-mql4">//+------------------------------------------------------------------+
//| キーボードショートカットで表示を切り替えるインジケーター              |
//+------------------------------------------------------------------+
#property strict
#property indicator_chart_window
#property indicator_buffers 2
#property indicator_color1 clrDodgerBlue
#property indicator_color2 clrOrangeRed
#property indicator_width1 2
#property indicator_width2 2

// インジケーターバッファ
double FastMABuffer[];   // 短期移動平均
double SlowMABuffer[];   // 長期移動平均

// 表示モード管理
int    DisplayMode = 0;     // 0=両方表示, 1=短期のみ, 2=長期のみ
string ModeName[]  = {"両方表示", "短期MAのみ", "長期MAのみ"};

// 移動平均のパラメータ
input int FastPeriod = 20;   // 短期MA期間
input int SlowPeriod = 50;   // 長期MA期間

// ステータス表示用ラベル名
#define STATUS_LABEL "MA_StatusLabel"

//+------------------------------------------------------------------+
//| ステータスラベルの作成・更新                                        |
//+------------------------------------------------------------------+
void UpdateStatusLabel()
{
   // ラベルがなければ作成
   if(ObjectFind(0, STATUS_LABEL) < 0)
   {
      ObjectCreate(0, STATUS_LABEL, OBJ_LABEL, 0, 0, 0);
      ObjectSetInteger(0, STATUS_LABEL, OBJPROP_CORNER, CORNER_RIGHT_UPPER);
      ObjectSetInteger(0, STATUS_LABEL, OBJPROP_XDISTANCE, 15);
      ObjectSetInteger(0, STATUS_LABEL, OBJPROP_YDISTANCE, 30);
      ObjectSetInteger(0, STATUS_LABEL, OBJPROP_COLOR, clrYellow);
      ObjectSetInteger(0, STATUS_LABEL, OBJPROP_FONTSIZE, 10);
      ObjectSetString(0, STATUS_LABEL, OBJPROP_FONT, "Arial Bold");
      ObjectSetInteger(0, STATUS_LABEL, OBJPROP_ANCHOR, ANCHOR_RIGHT_UPPER);
   }
   
   // 現在のモード名を表示
   string text = "MA表示: " + ModeName[DisplayMode] + " [M:切替 / H:ヘルプ]";
   ObjectSetString(0, STATUS_LABEL, OBJPROP_TEXT, text);
   ChartRedraw(0);
}

//+------------------------------------------------------------------+
//| 表示モードに応じてバッファの表示/非表示を切り替える                   |
//+------------------------------------------------------------------+
void ApplyDisplayMode()
{
   switch(DisplayMode)
   {
      case 0: // 両方表示
         SetIndexStyle(0, DRAW_LINE, STYLE_SOLID, 2, clrDodgerBlue);
         SetIndexStyle(1, DRAW_LINE, STYLE_SOLID, 2, clrOrangeRed);
         break;
         
      case 1: // 短期MAのみ
         SetIndexStyle(0, DRAW_LINE, STYLE_SOLID, 2, clrDodgerBlue);
         SetIndexStyle(1, DRAW_NONE);  // 長期MAを非表示
         break;
         
      case 2: // 長期MAのみ
         SetIndexStyle(0, DRAW_NONE);  // 短期MAを非表示
         SetIndexStyle(1, DRAW_LINE, STYLE_SOLID, 2, clrOrangeRed);
         break;
   }
   
   UpdateStatusLabel();
}

//+------------------------------------------------------------------+
//| 初期化関数                                                        |
//+------------------------------------------------------------------+
int OnInit()
{
   // バッファの設定
   SetIndexBuffer(0, FastMABuffer);
   SetIndexBuffer(1, SlowMABuffer);
   SetIndexLabel(0, "Fast MA(" + IntegerToString(FastPeriod) + ")");
   SetIndexLabel(1, "Slow MA(" + IntegerToString(SlowPeriod) + ")");
   
   // 初期表示モードを適用
   ApplyDisplayMode();
   
   Print("=== MA表示切替インジケーター ===");
   Print("Mキー: 表示モード切替");
   Print("Hキー: ヘルプ表示");
   
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| 終了処理関数                                                      |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   ObjectDelete(0, STATUS_LABEL);
}

//+------------------------------------------------------------------+
//| メイン計算関数                                                    |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &#038;time[],
                const double &#038;open[],
                const double &#038;high[],
                const double &#038;low[],
                const double &#038;close[],
                const long &#038;tick_volume[],
                const long &#038;volume[],
                const int &#038;spread[])
{
   // 計算開始位置を決定
   int limit = rates_total - prev_calculated;
   if(prev_calculated == 0)
      limit = rates_total - SlowPeriod - 1;
   
   // 移動平均を計算
   for(int i = limit; i >= 0; i--)
   {
      FastMABuffer[i] = iMA(NULL, 0, FastPeriod, 0, MODE_EMA, PRICE_CLOSE, i);
      SlowMABuffer[i] = iMA(NULL, 0, SlowPeriod, 0, MODE_EMA, PRICE_CLOSE, i);
   }
   
   return(rates_total);
}

//+------------------------------------------------------------------+
//| チャートイベント処理関数                                           |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
{
   // キーダウンイベントのみ処理
   if(id != CHARTEVENT_KEYDOWN)
      return;
   
   // lparamにキーコードが入る
   switch((int)lparam)
   {
      case 'M':  // Mキー：表示モードを切り替え
         DisplayMode = (DisplayMode + 1) % 3;  // 0→1→2→0とループ
         ApplyDisplayMode();
         Print("表示モードを変更: ", ModeName[DisplayMode]);
         break;
         
      case 'H':  // Hキー：ヘルプを表示
         Print("=== ヘルプ ===");
         Print("M: 表示モード切替（両方→短期のみ→長期のみ）");
         Print("H: このヘルプを表示");
         Print("現在のモード: ", ModeName[DisplayMode]);
         break;
   }
}
</code></pre>
<p>CHARTEVENT_KEYDOWNイベントでは、<code>lparam</code>に押されたキーの仮想キーコードが格納されます。英字キーの場合は大文字のASCIIコード（'A'〜'Z'）で判定できます。このサンプルではMキーで表示モードを順番に切り替え、Hキーでヘルプ情報を出力しています。</p>
<h2><span id="toc9">プログラム例4：マウス位置のクロスヘア情報を表示するインジケーター</span></h2>
<p>CHARTEVENT_MOUSE_MOVEを使って、マウスカーソルの位置に対応する価格と時刻をリアルタイムに表示するインジケーターです。マウスイベントの活用方法を理解できます。</p>
<pre><code class="language-mql4">//+------------------------------------------------------------------+
//| マウス位置のクロスヘア情報表示インジケーター                         |
//+------------------------------------------------------------------+
#property strict
#property indicator_chart_window
#property indicator_buffers 0

// オブジェクト名定数
#define LABEL_PRICE  "CrossHair_Price"
#define LABEL_TIME   "CrossHair_Time"
#define LABEL_BAR    "CrossHair_Bar"
#define LINE_H       "CrossHair_HLine"
#define LINE_V       "CrossHair_VLine"

// 表示ON/OFF
bool ShowCrossHair = true;

//+------------------------------------------------------------------+
//| ラベルを作成する関数                                               |
//+------------------------------------------------------------------+
void CreateLabel(string name, int x, int y, color clr)
{
   if(ObjectFind(0, name) < 0)
   {
      ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0);
      ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_LOWER);
      ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
      ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
      ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
      ObjectSetInteger(0, name, OBJPROP_FONTSIZE, 10);
      ObjectSetString(0, name, OBJPROP_FONT, "Consolas");
      ObjectSetInteger(0, name, OBJPROP_ANCHOR, ANCHOR_LEFT_LOWER);
   }
}

//+------------------------------------------------------------------+
//| 全オブジェクトを削除する関数                                       |
//+------------------------------------------------------------------+
void DeleteAllObjects()
{
   ObjectDelete(0, LABEL_PRICE);
   ObjectDelete(0, LABEL_TIME);
   ObjectDelete(0, LABEL_BAR);
   ObjectDelete(0, LINE_H);
   ObjectDelete(0, LINE_V);
}

//+------------------------------------------------------------------+
//| 初期化関数                                                        |
//+------------------------------------------------------------------+
int OnInit()
{
   // マウス移動イベントを有効にする
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);
   
   // 情報ラベルを作成
   CreateLabel(LABEL_PRICE, 20, 60, clrLime);
   CreateLabel(LABEL_TIME,  20, 40, clrAqua);
   CreateLabel(LABEL_BAR,   20, 20, clrYellow);
   
   Print("=== クロスヘア情報インジケーター ===");
   Print("Sキーで表示ON/OFF切替");
   
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| 終了処理関数                                                      |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   // マウス移動イベントを無効に戻す
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, false);
   DeleteAllObjects();
}

//+------------------------------------------------------------------+
//| メイン計算関数                                                    |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &#038;time[],
                const double &#038;open[],
                const double &#038;high[],
                const double &#038;low[],
                const double &#038;close[],
                const long &#038;tick_volume[],
                const long &#038;volume[],
                const int &#038;spread[])
{
   return(rates_total);
}

//+------------------------------------------------------------------+
//| チャートイベント処理関数                                           |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &#038;lparam,
                  const double &#038;dparam,
                  const string &#038;sparam)
{
   // キーダウンイベント：Sキーで表示切替
   if(id == CHARTEVENT_KEYDOWN)
   {
      if((int)lparam == 'S')
      {
         ShowCrossHair = !ShowCrossHair;
         if(!ShowCrossHair)
         {
            // 非表示時はラベルをクリア
            ObjectSetString(0, LABEL_PRICE, OBJPROP_TEXT, "");
            ObjectSetString(0, LABEL_TIME,  OBJPROP_TEXT, "");
            ObjectSetString(0, LABEL_BAR,   OBJPROP_TEXT, "");
            ObjectDelete(0, LINE_H);
            ObjectDelete(0, LINE_V);
            ChartRedraw(0);
         }
         Print("クロスヘア表示: ", ShowCrossHair ? "ON" : "OFF");
      }
      return;
   }
   
   // マウス移動イベント
   if(id == CHARTEVENT_MOUSE_MOVE &#038;&#038; ShowCrossHair)
   {
      // ピクセル座標を取得
      int mouseX = (int)lparam;
      int mouseY = (int)dparam;
      
      // ピクセル座標をチャートの時間・価格に変換
      int      subWindow = 0;
      datetime mouseTime = 0;
      double   mousePrice = 0;
      
      // ChartXYToTimePrice関数でピクセル座標を価格・時刻に変換
      if(ChartXYToTimePrice(0, mouseX, mouseY, subWindow, mouseTime, mousePrice))
      {
         // メインウィンドウ上のみ処理
         if(subWindow == 0)
         {
            // バーインデックスを取得
            int barIndex = iBarShift(NULL, 0, mouseTime);
            
            // ラベルに情報を表示
            ObjectSetString(0, LABEL_PRICE, OBJPROP_TEXT,
                          "価格: " + DoubleToString(mousePrice, _Digits));
            ObjectSetString(0, LABEL_TIME, OBJPROP_TEXT,
                          "時刻: " + TimeToString(mouseTime, TIME_DATE | TIME_MINUTES));
            ObjectSetString(0, LABEL_BAR, OBJPROP_TEXT,
                          "バー: " + IntegerToString(barIndex) +
                          " | O:" + DoubleToString(Open[barIndex], _Digits) +
                          " H:" + DoubleToString(High[barIndex], _Digits) +
                          " L:" + DoubleToString(Low[barIndex], _Digits) +
                          " C:" + DoubleToString(Close[barIndex], _Digits));
            
            // 水平線を描画
            if(ObjectFind(0, LINE_H) < 0)
            {
               ObjectCreate(0, LINE_H, OBJ_HLINE, 0, 0, mousePrice);
               ObjectSetInteger(0, LINE_H, OBJPROP_COLOR, clrGray);
               ObjectSetInteger(0, LINE_H, OBJPROP_STYLE, STYLE_DOT);
               ObjectSetInteger(0, LINE_H, OBJPROP_SELECTABLE, false);
            }
            else
            {
               ObjectSetDouble(0, LINE_H, OBJPROP_PRICE, mousePrice);
            }
            
            // 垂直線を描画
            if(ObjectFind(0, LINE_V) < 0)
            {
               ObjectCreate(0, LINE_V, OBJ_VLINE, 0, mouseTime, 0);
               ObjectSetInteger(0, LINE_V, OBJPROP_COLOR, clrGray);
               ObjectSetInteger(0, LINE_V, OBJPROP_STYLE, STYLE_DOT);
               ObjectSetInteger(0, LINE_V, OBJPROP_SELECTABLE, false);
            }
            else
            {
               ObjectSetInteger(0, LINE_V, OBJPROP_TIME, mouseTime);
            }
            
            ChartRedraw(0);
         }
      }
   }
}
</code></pre>
<p>マウス移動イベントを使用するには、<code>ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true)</code>で明示的に有効化する必要があります。<code>ChartXYToTimePrice</code>関数を使うことで、ピクセル座標をチャート上の時刻と価格に変換できます。このサンプルでは変換した情報をラベルに表示し、クロスヘア線も描画しています。</p>
<h2><span id="toc10">OnChartEvent関数を使う際の注意点</span></h2>
<h3><span id="toc11">イベントの有効化が必要な場合がある</span></h3>
<p>一部のイベントはデフォルトでは無効になっており、<code>ChartSetInteger</code>で明示的に有効化する必要があります。</p>
<table>
<thead>
<tr>
<th>イベント</th>
<th>有効化に必要な設定</th>
</tr>
</thead>
<tbody>
<tr>
<td>CHARTEVENT_MOUSE_MOVE</td>
<td><code>ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true)</code></td>
</tr>
<tr>
<td>CHARTEVENT_OBJECT_CREATE</td>
<td><code>ChartSetInteger(0, CHART_EVENT_OBJECT_CREATE, true)</code></td>
</tr>
<tr>
<td>CHARTEVENT_OBJECT_DELETE</td>
<td><code>ChartSetInteger(0, CHART_EVENT_OBJECT_DELETE, true)</code></td>
</tr>
</tbody>
</table>
<h3><span id="toc12">スクリプトでは使用できない</span></h3>
<p>OnChartEvent関数はEA（エキスパートアドバイザー）とインジケーターでのみ使用できます。スクリプトではイベントハンドラが呼び出されません。</p>
<h3><span id="toc13">重い処理を避ける</span></h3>
<p>特にCHARTEVENT_MOUSE_MOVEはマウスを動かすたびに高頻度で発生するため、イベントハンドラ内に重い処理を記述するとチャートの動作が遅くなります。必要最小限の処理にとどめるか、一定間隔でのみ処理を実行するようにしましょう。</p>
<h3><span id="toc14">ボタンの状態リセット</span></h3>
<p>OBJ_BUTTONオブジェクトをクリックした際は、処理後に<code>OBJPROP_STATE</code>をfalseに設定してボタンの押下状態をリセットする必要があります。これを忘れると、次回クリック時にイベントが正しく発生しない場合があります。</p>
<h3><span id="toc15">同一チャートでの制約</span></h3>
<p>一つのチャートに複数のEAやインジケーターがアタッチされている場合、それぞれのOnChartEvent関数が同じイベントを受け取ります。オブジェクト名の命名規則に注意し、自分が作成したオブジェクトのみを処理するようにフィルタリングすることが重要です。</p>
<p>投稿 <a href="https://mql-programing.com/archives/3451/onchartevent/">【MQL4関数】OnChartEvent関数とは？チャート操作を行なったときに実行されるイベント関数</a> は <a href="https://mql-programing.com">自動売買を作ろう！</a> に最初に表示されました。</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>【MQL4関数】OnTester関数とは？バックテスト終了時に実行されるイベント関数</title>
		<link>https://mql-programing.com/archives/3050/ontester/</link>
		
		<dc:creator><![CDATA[朝日奈りさ]]></dc:creator>
		<pubDate>Tue, 17 May 2022 08:02:17 +0000</pubDate>
				<category><![CDATA[関数]]></category>
		<category><![CDATA[【中級編】MQLプログラムの読み方・書き方]]></category>
		<category><![CDATA[【辞書】MQLリファレンス]]></category>
		<category><![CDATA[その他]]></category>
		<category><![CDATA[バックテスト]]></category>
		<category><![CDATA[シャープレシオ]]></category>
		<category><![CDATA[MQL]]></category>
		<category><![CDATA[リファレンス]]></category>
		<category><![CDATA[自動売買]]></category>
		<category><![CDATA[イベント関数]]></category>
		<category><![CDATA[OnTester]]></category>
		<guid isPermaLink="false">https://mql-programing.com/?p=3050</guid>

					<description><![CDATA[<p>OnTester関数の基本 OnTester()は、ストラテジーテスター（バックテスト）の終了時に自動的に呼び出されるイベント関数です。バックテスト完了後に独自の評価指標（カスタム最適化基準）を計算し、その結果を最適化プ [&#8230;]</p>
<p>投稿 <a href="https://mql-programing.com/archives/3050/ontester/">【MQL4関数】OnTester関数とは？バックテスト終了時に実行されるイベント関数</a> は <a href="https://mql-programing.com">自動売買を作ろう！</a> に最初に表示されました。</p>
]]></description>
										<content:encoded><![CDATA[<h2><span id="toc1">OnTester関数の基本</span></h2>
<p>OnTester()は、<strong>ストラテジーテスター（バックテスト）の終了時に自動的に呼び出されるイベント関数</strong>です。バックテスト完了後に独自の評価指標（カスタム最適化基準）を計算し、その結果を最適化プロセスに反映させることができます。</p>
<p>通常のバックテストでは、MetaTraderが用意するプロフィットファクターやシャープレシオなどの標準指標しか最適化基準に使えませんが、OnTester()を実装することで<strong>自分だけのオリジナル評価指標</strong>を最適化基準として使用できるようになります。</p>
<h2><span id="toc2">書式・引数・戻り値</span></h2>
<pre><code class="language-mql4">double OnTester(void);
</code></pre>
<h3><span id="toc3">引数</span></h3>
<p>引数はありません（void）。</p>
<h3><span id="toc4">戻り値</span></h3>
<table>
<thead>
<tr>
<th>型</th>
<th>説明</th>
</tr>
</thead>
<tbody>
<tr>
<td>double</td>
<td>カスタム最適化基準として使用される値。ストラテジーテスターの最適化時に「Custom max」を選択すると、この戻り値が最大化される方向で最適化が行われます。</td>
</tr>
</tbody>
</table>
<h3><span id="toc5">基本的な使い方</span></h3>
<p>OnTester()はEA（エキスパートアドバイザー）内に記述します。ストラテジーテスターでバックテストが完了した直後、OnDeinit()が呼ばれる前に実行されます。</p>
<pre><code class="language-mql4">// OnTester()の基本構造
double OnTester()
{
   // バックテスト結果を評価する処理
   double customCriterion = 0.0;
   
   // 何らかの計算を行う
   customCriterion = 計算結果;
   
   // 戻り値が「Custom max」最適化基準として使われる
   return(customCriterion);
}
</code></pre>
<h2><span id="toc6">呼び出しタイミング</span></h2>
<p>OnTester()が呼び出されるタイミングは以下の通りです。</p>
<ul>
<li><strong>バックテスト終了後</strong>、最後のティック処理が完了した直後</li>
<li><strong>OnDeinit()の前</strong>に呼ばれる</li>
<li><strong>ストラテジーテスター内でのみ</strong>動作する（ライブ取引では呼ばれない）</li>
<li>最適化実行時は<strong>各パス（パラメータの組み合わせ）ごと</strong>に呼ばれる</li>
</ul>
<p>呼び出し順序をまとめると以下のようになります。</p>
<pre><code class="language-mql4">// イベント関数の呼び出し順序（ストラテジーテスター）
// 1. OnInit()          ← テスト開始時
// 2. OnTick()          ← 各ティックごと（繰り返し）
// 3. OnTester()        ← テスト終了後
// 4. OnDeinit()        ← EA解放時
</code></pre>
<h2><span id="toc7">プログラム例1：基本的な損益ベースの評価指標</span></h2>
<p>最もシンプルな例として、<strong>総利益と総損失の比率から独自のスコアを算出</strong>するOnTester()を実装します。</p>
<pre><code class="language-mql4">//+------------------------------------------------------------------+
//| プログラム例1：基本的な損益評価指標                                  |
//| 総利益÷総損失（プロフィットファクター的な指標）を返す                    |
//+------------------------------------------------------------------+
#property strict

// EA本体のパラメータ（例）
input int    MAPeriod   = 20;     // 移動平均期間
input double LotSize    = 0.1;    // ロットサイズ
input int    TakeProfit = 100;    // 利確（ポイント）
input int    StopLoss   = 50;     // 損切り（ポイント）

//+------------------------------------------------------------------+
//| 初期化関数                                                         |
//+------------------------------------------------------------------+
int OnInit()
{
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| ティック処理（簡易的なMA戦略の例）                                   |
//+------------------------------------------------------------------+
void OnTick()
{
   // 既存ポジションがあれば何もしない
   if(OrdersTotal() > 0) return;
   
   double ma = iMA(NULL, 0, MAPeriod, 0, MODE_SMA, PRICE_CLOSE, 1);
   double prevClose = iClose(NULL, 0, 1);
   double prevClose2 = iClose(NULL, 0, 2);
   
   // 価格がMAを上抜けたら買い
   if(prevClose2 < ma &#038;&#038; prevClose > ma)
   {
      OrderSend(Symbol(), OP_BUY, LotSize, Ask, 3,
                Ask - StopLoss * Point, Ask + TakeProfit * Point,
                "MA Cross Buy", 0, 0, clrBlue);
   }
   // 価格がMAを下抜けたら売り
   else if(prevClose2 > ma && prevClose < ma)
   {
      OrderSend(Symbol(), OP_SELL, LotSize, Bid, 3,
                Bid + StopLoss * Point, Bid - TakeProfit * Point,
                "MA Cross Sell", 0, 0, clrRed);
   }
}

//+------------------------------------------------------------------+
//| バックテスト終了時に呼ばれるイベント関数                              |
//+------------------------------------------------------------------+
double OnTester()
{
   // TesterStatistics()でバックテスト結果の統計を取得
   double totalProfit = TesterStatistics(STAT_GROSS_PROFIT);  // 総利益
   double totalLoss   = TesterStatistics(STAT_GROSS_LOSS);    // 総損失（負の値）
   double totalTrades = TesterStatistics(STAT_TRADES);        // 総取引回数
   
   // 取引が一定回数以上ない場合は評価しない
   if(totalTrades < 10)
   {
      Print("取引回数が少なすぎます: ", totalTrades, " 回");
      return(0.0);
   }
   
   // 総損失が0の場合（全勝の場合）の処理
   if(totalLoss == 0.0)
   {
      Print("全勝！ 総利益: ", totalProfit);
      return(totalProfit);
   }
   
   // プロフィットファクター（総利益÷総損失の絶対値）を計算
   double profitFactor = totalProfit / MathAbs(totalLoss);
   
   Print("カスタム評価値（PF）: ", DoubleToStr(profitFactor, 2),
         " | 取引回数: ", totalTrades,
         " | 総利益: ", DoubleToStr(totalProfit, 2),
         " | 総損失: ", DoubleToStr(totalLoss, 2));
   
   // この値が「Custom max」最適化基準として使用される
   return(profitFactor);
}

//+------------------------------------------------------------------+
//| 終了処理                                                           |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   Print("EA終了 - 理由コード: ", reason);
}
</code></pre>
<p>この例では<code>TesterStatistics()</code>関数を使ってバックテスト結果の統計データを取得しています。取引回数が少なすぎる場合は0を返すことで、信頼性の低い結果を排除しています。</p>
<h2><span id="toc8">プログラム例2：リカバリーファクターによる評価</span></h2>
<p><strong>リカバリーファクター（純利益÷最大ドローダウン）</strong>は、リスクに対してどれだけ効率的に利益を上げているかを示す重要な指標です。この値をカスタム最適化基準として使います。</p>
<pre><code class="language-mql4">//+------------------------------------------------------------------+
//| プログラム例2：リカバリーファクターで最適化                            |
//| 純利益÷最大ドローダウンを評価指標として返す                           |
//+------------------------------------------------------------------+
#property strict

input int    FastMA = 10;   // 短期MA期間
input int    SlowMA = 30;   // 長期MA期間
input double Lots   = 0.1;  // ロットサイズ

//+------------------------------------------------------------------+
//| 初期化関数                                                         |
//+------------------------------------------------------------------+
int OnInit()
{
   // 短期MAが長期MAより大きくならないようにバリデーション
   if(FastMA >= SlowMA)
   {
      Print("FastMAはSlowMAより小さくしてください");
      return(INIT_PARAMETERS_INCORRECT);
   }
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| ティック処理（ゴールデンクロス・デッドクロス戦略）                     |
//+------------------------------------------------------------------+
void OnTick()
{
   if(OrdersTotal() > 0) return;
   
   double fastCurr = iMA(NULL, 0, FastMA, 0, MODE_EMA, PRICE_CLOSE, 1);
   double fastPrev = iMA(NULL, 0, FastMA, 0, MODE_EMA, PRICE_CLOSE, 2);
   double slowCurr = iMA(NULL, 0, SlowMA, 0, MODE_EMA, PRICE_CLOSE, 1);
   double slowPrev = iMA(NULL, 0, SlowMA, 0, MODE_EMA, PRICE_CLOSE, 2);
   
   // ゴールデンクロス→買い
   if(fastPrev <= slowPrev &#038;&#038; fastCurr > slowCurr)
   {
      OrderSend(Symbol(), OP_BUY, Lots, Ask, 3, 0, 0, "GC Buy", 12345, 0, clrBlue);
   }
   // デッドクロス→売り
   else if(fastPrev >= slowPrev && fastCurr < slowCurr)
   {
      OrderSend(Symbol(), OP_SELL, Lots, Bid, 3, 0, 0, "DC Sell", 12345, 0, clrRed);
   }
}

//+------------------------------------------------------------------+
//| バックテスト終了時：リカバリーファクターを計算して返す                  |
//+------------------------------------------------------------------+
double OnTester()
{
   // 各種統計値を取得
   double netProfit      = TesterStatistics(STAT_PROFIT);              // 純利益
   double maxDrawdown    = TesterStatistics(STAT_EQUITY_DD);           // 最大ドローダウン（金額）
   double totalTrades    = TesterStatistics(STAT_TRADES);              // 総取引回数
   double profitTrades   = TesterStatistics(STAT_PROFIT_TRADES);       // 勝ちトレード数
   double expectedPayoff = TesterStatistics(STAT_EXPECTED_PAYOFF);     // 期待利得
   
   // ログに詳細を出力
   Print("=== バックテスト結果サマリー ===");
   Print("純利益: ", DoubleToStr(netProfit, 2));
   Print("最大DD: ", DoubleToStr(maxDrawdown, 2));
   Print("取引数: ", (int)totalTrades);
   Print("勝率:   ", DoubleToStr(profitTrades / MathMax(totalTrades, 1) * 100, 1), "%");
   Print("期待利得: ", DoubleToStr(expectedPayoff, 2));
   
   // 最低取引回数のフィルター
   if(totalTrades < 30)
   {
      Print("取引回数不足（30回未満）→ 評価値0");
      return(0.0);
   }
   
   // 最大ドローダウンが0または利益がマイナスの場合
   if(maxDrawdown <= 0.0 || netProfit <= 0.0)
   {
      Print("利益なしまたはDD計算不可 → 評価値0");
      return(0.0);
   }
   
   // リカバリーファクター = 純利益 ÷ 最大ドローダウン
   double recoveryFactor = netProfit / maxDrawdown;
   
   Print("リカバリーファクター: ", DoubleToStr(recoveryFactor, 3));
   
   return(recoveryFactor);
}

//+------------------------------------------------------------------+
//| 終了処理                                                           |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
}
</code></pre>
<p>リカバリーファクターが高いほど、少ないリスク（ドローダウン）で大きな利益を上げていることを意味します。最適化時に「Custom max」を選択すれば、この値が最大化されるパラメータの組み合わせが探索されます。</p>
<h2><span id="toc9">プログラム例3：複合スコアによる多角的評価</span></h2>
<p>実際のトレーディングでは、単一の指標だけでなく<strong>複数の指標を組み合わせた総合スコア</strong>で評価するのが効果的です。この例では、プロフィットファクター・勝率・取引回数・ドローダウン率を組み合わせた複合スコアを計算します。</p>
<pre><code class="language-mql4">//+------------------------------------------------------------------+
//| プログラム例3：複合スコアによる多角的評価                              |
//| 複数の評価指標を重み付けして総合スコアを算出                           |
//+------------------------------------------------------------------+
#property strict

input int    RSIPeriod  = 14;    // RSI期間
input int    RSIBuyLv   = 30;    // RSI買いレベル
input int    RSISellLv  = 70;    // RSI売りレベル
input double Lots       = 0.1;   // ロットサイズ
input int    SL_Points  = 200;   // 損切り幅（ポイント）
input int    TP_Points  = 300;   // 利確幅（ポイント）

//+------------------------------------------------------------------+
//| 初期化                                                             |
//+------------------------------------------------------------------+
int OnInit()
{
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| ティック処理（RSI戦略の例）                                         |
//+------------------------------------------------------------------+
void OnTick()
{
   // 既にポジションがあれば何もしない
   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      if(OrderSelect(i, SELECT_BY_POS) && OrderSymbol() == Symbol())
         return;
   }
   
   double rsiCurr = iRSI(NULL, 0, RSIPeriod, PRICE_CLOSE, 1);
   double rsiPrev = iRSI(NULL, 0, RSIPeriod, PRICE_CLOSE, 2);
   
   // RSIが売られすぎ水準から回復→買い
   if(rsiPrev < RSIBuyLv &#038;&#038; rsiCurr >= RSIBuyLv)
   {
      double sl = Ask - SL_Points * Point;
      double tp = Ask + TP_Points * Point;
      OrderSend(Symbol(), OP_BUY, Lots, Ask, 3, sl, tp, "RSI Buy", 0, 0, clrBlue);
   }
   // RSIが買われすぎ水準から下落→売り
   else if(rsiPrev > RSISellLv && rsiCurr <= RSISellLv)
   {
      double sl = Bid + SL_Points * Point;
      double tp = Bid - TP_Points * Point;
      OrderSend(Symbol(), OP_SELL, Lots, Bid, 3, sl, tp, "RSI Sell", 0, 0, clrRed);
   }
}

//+------------------------------------------------------------------+
//| 複合スコアを計算する関数                                            |
//+------------------------------------------------------------------+
double CalculateCompositeScore()
{
   // ---- 統計データの取得 ----
   double netProfit     = TesterStatistics(STAT_PROFIT);
   double grossProfit   = TesterStatistics(STAT_GROSS_PROFIT);
   double grossLoss     = TesterStatistics(STAT_GROSS_LOSS);     // 負の値
   double totalTrades   = TesterStatistics(STAT_TRADES);
   double profitTrades  = TesterStatistics(STAT_PROFIT_TRADES);
   double maxDDPercent  = TesterStatistics(STAT_EQUITY_DDREL_PERCENT); // 最大DD（%）
   double maxDD         = TesterStatistics(STAT_EQUITY_DD);
   double sharpRatio    = TesterStatistics(STAT_SHARPE_RATIO);
   
   // ---- フィルター条件 ----
   // 取引回数が少なすぎる結果を除外
   if(totalTrades < 20)
   {
      Print("フィルター: 取引回数不足 (", (int)totalTrades, " < 20)");
      return(-1000.0);  // 大きな負の値で排除
   }
   
   // 純利益がマイナスの結果を除外
   if(netProfit <= 0)
   {
      Print("フィルター: 純利益がマイナス (", DoubleToStr(netProfit, 2), ")");
      return(-500.0);
   }
   
   // 最大ドローダウンが大きすぎる結果を除外（30%超）
   if(maxDDPercent > 30.0)
   {
      Print("フィルター: DD過大 (", DoubleToStr(maxDDPercent, 1), "% > 30%)");
      return(-100.0);
   }
   
   // ---- 各指標のスコア計算 ----
   
   // 1. プロフィットファクター（0〜10にスケーリング）
   double pf = 0.0;
   if(MathAbs(grossLoss) > 0)
      pf = grossProfit / MathAbs(grossLoss);
   double pfScore = MathMin(pf, 5.0) * 2.0;  // PF=5以上は全て10点
   
   // 2. 勝率スコア（0〜10にスケーリング）
   double winRate = profitTrades / totalTrades * 100.0;
   double winRateScore = MathMin(winRate, 100.0) / 10.0;  // 勝率100%で10点
   
   // 3. 取引回数スコア（多いほど信頼性が高い）
   // 100回以上で満点（10点）
   double tradeCountScore = MathMin(totalTrades / 100.0, 1.0) * 10.0;
   
   // 4. ドローダウン抑制スコア（DDが小さいほど高評価）
   // DD 0%→10点、DD 30%→0点
   double ddScore = MathMax(0.0, (30.0 - maxDDPercent) / 30.0 * 10.0);
   
   // ---- 重み付けによる総合スコア ----
   double weightPF       = 0.35;  // プロフィットファクター重視
   double weightWinRate  = 0.20;  // 勝率
   double weightTrades   = 0.15;  // 取引回数（信頼性）
   double weightDD       = 0.30;  // ドローダウン抑制
   
   double compositeScore = pfScore       * weightPF
                         + winRateScore  * weightWinRate
                         + tradeCountScore * weightTrades
                         + ddScore       * weightDD;
   
   // ---- 結果のログ出力 ----
   Print("=== 複合スコア詳細 ===");
   Print("  PFスコア:     ", DoubleToStr(pfScore, 2), " (PF=", DoubleToStr(pf, 2), ")");
   Print("  勝率スコア:   ", DoubleToStr(winRateScore, 2), " (勝率=", DoubleToStr(winRate, 1), "%)");
   Print("  取引数スコア: ", DoubleToStr(tradeCountScore, 2), " (取引数=", (int)totalTrades, ")");
   Print("  DDスコア:     ", DoubleToStr(ddScore, 2), " (DD=", DoubleToStr(maxDDPercent, 1), "%)");
   Print("  ★総合スコア:  ", DoubleToStr(compositeScore, 3));
   
   return(compositeScore);
}

//+------------------------------------------------------------------+
//| バックテスト終了時のイベント                                         |
//+------------------------------------------------------------------+
double OnTester()
{
   return(CalculateCompositeScore());
}

//+------------------------------------------------------------------+
//| 終了処理                                                           |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
}
</code></pre>
<p>この例のポイントは以下の通りです。</p>
<ul>
<li><strong>フィルター機能</strong>：取引回数が少ない、利益がマイナス、ドローダウンが大きすぎる結果を大きな負の値で排除</li>
<li><strong>スケーリング</strong>：各指標を0〜10のスコアに正規化してから合算</li>
<li><strong>重み付け</strong>：プロフィットファクターとドローダウンを重視した設計</li>
</ul>
<h2><span id="toc10">プログラム例4：取引履歴を直接分析してカスタム指標を算出</span></h2>
<p>TesterStatistics()だけでなく、<strong>決済済み注文の履歴を直接走査</strong>して、連続損失回数や最大連敗額など、より細かな分析を行う例です。</p>
<pre><code class="language-mql4">//+------------------------------------------------------------------+
//| プログラム例4：取引履歴を直接分析する高度な評価                        |
//| 連続損失・利益の安定性を評価指標に組み込む                             |
//+------------------------------------------------------------------+
#property strict

input int    Period1    = 12;
input int    Period2    = 26;
input double LotSize   = 0.1;

//+------------------------------------------------------------------+
//| 初期化                                                             |
//+------------------------------------------------------------------+
int OnInit()
{
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| ティック処理（省略：任意の売買ロジックを実装）                         |
//+------------------------------------------------------------------+
void OnTick()
{
   // ここには任意のトレードロジックを実装
   // （本例ではOnTester()の解説に焦点を当てるため省略）
}

//+------------------------------------------------------------------+
//| バックテスト終了時：取引履歴を直接分析                                |
//+------------------------------------------------------------------+
double OnTester()
{
   // 決済済み注文の履歴を分析
   int totalOrders = OrdersHistoryTotal();
   
   if(totalOrders == 0)
   {
      Print("取引履歴なし");
      return(0.0);
   }
   
   // ---- 分析用変数の初期化 ----
   double profits[];          // 各取引の損益を格納する配列
   int    tradeCount = 0;     // 有効な取引数
   int    maxConsecLoss = 0;  // 最大連敗数
   int    currentConsecLoss = 0;  // 現在の連敗カウント
   double maxConsecLossAmount = 0.0;  // 最大連敗時の損失額
   double currentConsecLossAmount = 0.0;
   double totalProfit = 0.0;  // 総利益
   
   // 配列サイズを事前確保
   ArrayResize(profits, totalOrders);
   
   // ---- 取引履歴をスキャンして損益データを収集 ----
   for(int i = 0; i < totalOrders; i++)
   {
      if(!OrderSelect(i, SELECT_BY_POS, MODE_HISTORY))
         continue;
      
      // 対象シンボルの決済注文のみ処理
      if(OrderSymbol() != Symbol())
         continue;
      
      // OP_BUYまたはOP_SELLのみ（入金・出金等を除外）
      int type = OrderType();
      if(type != OP_BUY &#038;&#038; type != OP_SELL)
         continue;
      
      // 損益（手数料・スワップ込み）
      double orderProfit = OrderProfit() + OrderCommission() + OrderSwap();
      profits[tradeCount] = orderProfit;
      totalProfit += orderProfit;
      tradeCount++;
      
      // 連敗トラッキング
      if(orderProfit < 0)
      {
         currentConsecLoss++;
         currentConsecLossAmount += MathAbs(orderProfit);
         
         if(currentConsecLoss > maxConsecLoss)
         {
            maxConsecLoss = currentConsecLoss;
            maxConsecLossAmount = currentConsecLossAmount;
         }
      }
      else
      {
         // 勝ちトレードで連敗リセット
         currentConsecLoss = 0;
         currentConsecLossAmount = 0.0;
      }
   }
   
   // 有効な取引がない場合
   if(tradeCount < 10)
   {
      Print("有効取引数不足: ", tradeCount);
      return(0.0);
   }
   
   // 配列サイズを実際の取引数にリサイズ
   ArrayResize(profits, tradeCount);
   
   // ---- 利益の安定性（標準偏差）を計算 ----
   double meanProfit = totalProfit / tradeCount;
   double sumSqDiff = 0.0;
   
   for(int j = 0; j < tradeCount; j++)
   {
      double diff = profits[j] - meanProfit;
      sumSqDiff += diff * diff;
   }
   
   double stdDev = MathSqrt(sumSqDiff / tradeCount);
   
   // ---- カスタムシャープレシオ的な指標 ----
   // 平均利益 ÷ 標準偏差（大きいほど安定して利益を出している）
   double customSharpe = 0.0;
   if(stdDev > 0)
      customSharpe = meanProfit / stdDev;
   
   // ---- 連敗ペナルティを適用 ----
   // 最大連敗が多いほどペナルティを与える
   double consecLossPenalty = 1.0;
   if(maxConsecLoss > 5)
      consecLossPenalty = 5.0 / maxConsecLoss;  // 5連敗超でペナルティ
   
   // ---- 最終スコアの計算 ----
   // カスタムシャープ × 連敗ペナルティ × √取引回数（信頼性補正）
   double finalScore = customSharpe * consecLossPenalty * MathSqrt(tradeCount);
   
   // ---- 詳細ログ出力 ----
   Print("=== 取引履歴分析結果 ===");
   Print("有効取引数: ", tradeCount);
   Print("総損益: ", DoubleToStr(totalProfit, 2));
   Print("平均損益: ", DoubleToStr(meanProfit, 2));
   Print("損益の標準偏差: ", DoubleToStr(stdDev, 2));
   Print("カスタムシャープ: ", DoubleToStr(customSharpe, 4));
   Print("最大連敗数: ", maxConsecLoss, " (損失額: ", DoubleToStr(maxConsecLossAmount, 2), ")");
   Print("連敗ペナルティ: ", DoubleToStr(consecLossPenalty, 2));
   Print("★最終スコア: ", DoubleToStr(finalScore, 4));
   
   return(finalScore);
}

//+------------------------------------------------------------------+
//| 終了処理                                                           |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
}
</code></pre>
<p>この例では、<code>OrdersHistoryTotal()</code>と<code>OrderSelect()</code>を使って決済済み注文を1件ずつ走査しています。TesterStatistics()では取得できない連敗数や損益の標準偏差といった情報を、取引履歴から直接計算している点がポイントです。</p>
<h2><span id="toc11">TesterStatistics()で取得できる主な統計定数</span></h2>
<p>OnTester()内で使用する<code>TesterStatistics()</code>関数で取得できる代表的な統計定数を一覧にまとめます。</p>
<table>
<thead>
<tr>
<th>定数名</th>
<th>説明</th>
</tr>
</thead>
<tbody>
<tr>
<td>STAT_PROFIT</td>
<td>純利益（総利益 - 総損失）</td>
</tr>
<tr>
<td>STAT_GROSS_PROFIT</td>
<td>総利益</td>
</tr>
<tr>
<td>STAT_GROSS_LOSS</td>
<td>総損失（負の値）</td>
</tr>
<tr>
<td>STAT_TRADES</td>
<td>総取引回数</td>
</tr>
<tr>
<td>STAT_PROFIT_TRADES</td>
<td>勝ちトレード数</td>
</tr>
<tr>
<td>STAT_LOSS_TRADES</td>
<td>負けトレード数</td>
</tr>
<tr>
<td>STAT_EXPECTED_PAYOFF</td>
<td>期待利得（1トレードあたりの平均損益）</td>
</tr>
<tr>
<td>STAT_EQUITY_DD</td>
<td>最大ドローダウン（金額）</td>
</tr>
<tr>
<td>STAT_EQUITY_DDREL_PERCENT</td>
<td>最大ドローダウン（%）</td>
</tr>
<tr>
<td>STAT_SHARPE_RATIO</td>
<td>シャープレシオ</td>
</tr>
<tr>
<td>STAT_PROFIT_FACTOR</td>
<td>プロフィットファクター</td>
</tr>
<tr>
<td>STAT_RECOVERY_FACTOR</td>
<td>リカバリーファクター</td>
</tr>
</tbody>
</table>
<h2><span id="toc12">最適化での使い方</span></h2>
<p>OnTester()の戻り値をカスタム最適化基準として使用するには、ストラテジーテスターの最適化設定で以下の手順を行います。</p>
<ol>
<li>ストラテジーテスターを開き、EAを選択する</li>
<li>「最適化」にチェックを入れる</li>
<li>最適化基準のドロップダウンから<strong>「Custom max」</strong>を選択する</li>
<li>最適化するパラメータの範囲を設定する</li>
<li>「スタート」を押して最適化を実行する</li>
</ol>
<p>「Custom max」を選択すると、OnTester()の戻り値が<strong>最大化</strong>される方向でパラメータの最適な組み合わせが探索されます。逆に最小化したい場合は、戻り値の符号を反転させる（負の値にする）ことで対応できます。</p>
<h2><span id="toc13">注意点・よくある間違い</span></h2>
<ul>
<li><strong>ライブ取引では呼ばれない</strong>：OnTester()はストラテジーテスター内でのみ動作します。ライブ取引や通常のチャートにEAをアタッチした場合は呼ばれません。</li>
<li><strong>戻り値の型はdouble</strong>：int型ではなくdouble型を返す必要があります。整数値を返しても問題はありませんが、関数の宣言はdoubleにしてください。</li>
<li><strong>TesterStatistics()はOnTester()内でのみ有効</strong>：この関数はバックテスト終了後にしか正確な値を返しません。OnTick()内で呼んでも意味のある値は取得できません。</li>
<li><strong>最適化時のパフォーマンス</strong>：OnTester()内で重い計算を行うと、最適化全体の速度に影響します。特に数千パスの最適化を行う場合は、計算量に注意してください。</li>
<li><strong>取引回数のフィルターは必須</strong>：取引回数が極端に少ない結果は統計的に信頼できません。必ず最低取引回数のチェックを入れてから評価値を計算しましょう。</li>
</ul>
<p>投稿 <a href="https://mql-programing.com/archives/3050/ontester/">【MQL4関数】OnTester関数とは？バックテスト終了時に実行されるイベント関数</a> は <a href="https://mql-programing.com">自動売買を作ろう！</a> に最初に表示されました。</p>
]]></content:encoded>
					
		
		
			</item>
		<item>
		<title>【MQL4関数】OnTimer関数とは？一定時間ごとに実行されるイベント関数</title>
		<link>https://mql-programing.com/archives/3030/ontimer/</link>
		
		<dc:creator><![CDATA[朝日奈りさ]]></dc:creator>
		<pubDate>Tue, 17 May 2022 05:09:20 +0000</pubDate>
				<category><![CDATA[関数]]></category>
		<category><![CDATA[【中級編】MQLプログラムの読み方・書き方]]></category>
		<category><![CDATA[【辞書】MQLリファレンス]]></category>
		<category><![CDATA[その他]]></category>
		<category><![CDATA[EventKillTimer]]></category>
		<category><![CDATA[MQL]]></category>
		<category><![CDATA[リファレンス]]></category>
		<category><![CDATA[自動売買]]></category>
		<category><![CDATA[イベント関数]]></category>
		<category><![CDATA[OnTimer]]></category>
		<category><![CDATA[EventSetTimer]]></category>
		<guid isPermaLink="false">https://mql-programing.com/?p=3030</guid>

					<description><![CDATA[<p>OnTimer関数の基本 OnTimer関数は、一定時間（タイマー間隔）が経過するたびに自動的に呼び出されるイベントハンドラ関数です。チャートのティック（価格変動）に依存せず、指定した時間間隔で定期的に処理を実行できるた [&#8230;]</p>
<p>投稿 <a href="https://mql-programing.com/archives/3030/ontimer/">【MQL4関数】OnTimer関数とは？一定時間ごとに実行されるイベント関数</a> は <a href="https://mql-programing.com">自動売買を作ろう！</a> に最初に表示されました。</p>
]]></description>
										<content:encoded><![CDATA[<h2><span id="toc1">OnTimer関数の基本</span></h2>
<p>OnTimer関数は、一定時間（タイマー間隔）が経過するたびに自動的に呼び出されるイベントハンドラ関数です。チャートのティック（価格変動）に依存せず、指定した時間間隔で定期的に処理を実行できるため、定期的なデータ取得、ログ出力、監視処理などに非常に便利です。</p>
<h3><span id="toc2">関数のシグネチャ</span></h3>
<pre><code class="language-mql4">void OnTimer(void);
</code></pre>
<p>OnTimer関数には引数も戻り値もありません。この関数を使うためには、事前に<code>EventSetTimer()</code>または<code>EventSetMillisecondTimer()</code>でタイマーを起動しておく必要があります。</p>
<h3><span id="toc3">タイマーの起動と停止</span></h3>
<table>
<thead>
<tr>
<th>関数名</th>
<th>説明</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>EventSetTimer(int seconds)</code></td>
<td>秒単位でタイマー間隔を設定（最小1秒）</td>
</tr>
<tr>
<td><code>EventSetMillisecondTimer(int milliseconds)</code></td>
<td>ミリ秒単位でタイマー間隔を設定（最小数十ms程度）</td>
</tr>
<tr>
<td><code>EventKillTimer()</code></td>
<td>タイマーを停止する</td>
</tr>
</tbody>
</table>
<h3><span id="toc4">基本的な使い方の流れ</span></h3>
<ol>
<li><code>OnInit()</code>内で<code>EventSetTimer()</code>を呼び出してタイマーを起動する</li>
<li>指定した間隔ごとに<code>OnTimer()</code>が自動的に呼ばれる</li>
<li><code>OnDeinit()</code>内で<code>EventKillTimer()</code>を呼び出してタイマーを停止する</li>
</ol>
<pre><code class="language-mql4">// OnTimer関数の最も基本的な構造
int OnInit()
{
   // 5秒ごとにOnTimerを呼び出すよう設定
   EventSetTimer(5);
   return(INIT_SUCCEEDED);
}

void OnDeinit(const int reason)
{
   // タイマーを停止（リソース解放）
   EventKillTimer();
}

void OnTimer()
{
   // 5秒ごとにここが実行される
   Print("タイマーイベント発生: ", TimeToString(TimeCurrent(), TIME_SECONDS));
}
</code></pre>
<h2><span id="toc5">プログラム例1：定期的にスプレッドを記録するインジケーター</span></h2>
<p>最もシンプルな例として、一定間隔でスプレッドをエキスパートログに記録するインジケーターです。ティックが発生しない閑散相場でも確実にスプレッド情報を取得できます。</p>
<pre><code class="language-mql4">//+------------------------------------------------------------------+
//| スプレッド定期記録インジケーター                                    |
//+------------------------------------------------------------------+
#property indicator_chart_window
#property strict

// 入力パラメータ：記録間隔（秒）
input int InpInterval = 10; // 記録間隔（秒）

//+------------------------------------------------------------------+
//| 初期化関数                                                        |
//+------------------------------------------------------------------+
int OnInit()
{
   // 指定秒数ごとにタイマーイベントを発生させる
   EventSetTimer(InpInterval);
   
   Print("スプレッド記録開始 - 間隔: ", InpInterval, "秒");
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| 終了処理関数                                                      |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   // タイマーを確実に停止する
   EventKillTimer();
   Print("スプレッド記録終了");
}

//+------------------------------------------------------------------+
//| タイマーイベントハンドラ                                           |
//+------------------------------------------------------------------+
void OnTimer()
{
   // 現在のスプレッドをポイント単位で取得
   int spreadPoints = (int)MarketInfo(Symbol(), MODE_SPREAD);
   
   // Bid/Askからスプレッドを実数値で計算
   double spreadValue = MarketInfo(Symbol(), MODE_ASK) - MarketInfo(Symbol(), MODE_BID);
   
   // ログに出力
   Print(Symbol(), " スプレッド: ", spreadPoints, " points (", 
         DoubleToString(spreadValue, (int)MarketInfo(Symbol(), MODE_DIGITS)), ")");
}

//+------------------------------------------------------------------+
//| OnCalculate（インジケーター必須関数）                              |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
{
   return(rates_total);
}
</code></pre>
<p>この例では<code>EventSetTimer(10)</code>により10秒ごとにスプレッドを記録しています。ティックベースの<code>OnCalculate</code>では価格変動がないとデータを取れませんが、タイマーを使えば一定間隔で確実にデータを収集できます。</p>
<h2><span id="toc6">プログラム例2：タイマーでチャート上に時計を表示するインジケーター</span></h2>
<p>チャート上にサーバー時間とローカル時間をリアルタイム表示する実用的なインジケーターです。1秒ごとにタイマーで更新するため、ティックがなくても時計が動き続けます。</p>
<pre><code class="language-mql4">//+------------------------------------------------------------------+
//| チャート時計インジケーター                                         |
//+------------------------------------------------------------------+
#property indicator_chart_window
#property strict

// 入力パラメータ
input color InpColorServer = clrWhite;  // サーバー時間の文字色
input color InpColorLocal  = clrYellow; // ローカル時間の文字色
input int   InpFontSize    = 12;        // フォントサイズ
input ENUM_BASE_CORNER InpCorner = CORNER_RIGHT_UPPER; // 表示コーナー

//+------------------------------------------------------------------+
//| 初期化                                                            |
//+------------------------------------------------------------------+
int OnInit()
{
   // 1秒ごとに更新
   EventSetTimer(1);
   
   // サーバー時間ラベルを作成
   CreateLabel("LabelServerTime", "Server: --:--:--", InpColorServer, 10, 20);
   // ローカル時間ラベルを作成
   CreateLabel("LabelLocalTime", "Local: --:--:--", InpColorLocal, 10, 40);
   
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| 終了処理                                                          |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   EventKillTimer();
   
   // ラベルオブジェクトを削除
   ObjectDelete(0, "LabelServerTime");
   ObjectDelete(0, "LabelLocalTime");
}

//+------------------------------------------------------------------+
//| タイマーイベント：1秒ごとに時計を更新                              |
//+------------------------------------------------------------------+
void OnTimer()
{
   // サーバー時間を取得して表示を更新
   string serverTimeStr = "Server: " + TimeToString(TimeCurrent(), TIME_SECONDS);
   ObjectSetString(0, "LabelServerTime", OBJPROP_TEXT, serverTimeStr);
   
   // ローカル時間を取得して表示を更新
   string localTimeStr = "Local: " + TimeToString(TimeLocal(), TIME_SECONDS);
   ObjectSetString(0, "LabelLocalTime", OBJPROP_TEXT, localTimeStr);
   
   // チャートを再描画して即座に反映
   ChartRedraw(0);
}

//+------------------------------------------------------------------+
//| ラベルオブジェクト作成用のヘルパー関数                              |
//+------------------------------------------------------------------+
void CreateLabel(string name, string text, color clr, int xDist, int yDist)
{
   // 既存のオブジェクトがあれば削除
   ObjectDelete(0, name);
   
   // ラベルオブジェクトを作成
   ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0);
   ObjectSetInteger(0, name, OBJPROP_CORNER, InpCorner);
   ObjectSetInteger(0, name, OBJPROP_XDISTANCE, xDist);
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, yDist);
   ObjectSetString(0, name, OBJPROP_TEXT, text);
   ObjectSetString(0, name, OBJPROP_FONT, "Arial Bold");
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, InpFontSize);
   ObjectSetInteger(0, name, OBJPROP_COLOR, clr);
}

//+------------------------------------------------------------------+
//| OnCalculate（インジケーター必須）                                  |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
{
   return(rates_total);
}
</code></pre>
<p>このインジケーターはタイマーの代表的な活用例です。<code>EventSetTimer(1)</code>で毎秒更新することで、秒単位の時計をチャート上に実現しています。<code>ChartRedraw()</code>を呼ぶことで、オブジェクトの変更が即座に画面に反映されます。</p>
<h2><span id="toc7">プログラム例3：一定時間ごとにトレーリングストップを実行するEA</span></h2>
<p>ティックベースではなくタイマーベースでトレーリングストップを管理するEAです。ティックが少ない通貨ペアや時間帯でも、確実にストップロスの調整が行われるメリットがあります。</p>
<pre><code class="language-mql4">//+------------------------------------------------------------------+
//| タイマー式トレーリングストップEA                                    |
//+------------------------------------------------------------------+
#property strict

// 入力パラメータ
input int    InpCheckInterval  = 3;     // チェック間隔（秒）
input double InpTrailDistance  = 30.0;  // トレーリング距離（pips）
input double InpTrailStep     = 10.0;  // トレーリングステップ（pips）
input int    InpMagicNumber   = 12345; // マジックナンバー

//+------------------------------------------------------------------+
//| 初期化                                                            |
//+------------------------------------------------------------------+
int OnInit()
{
   // 指定秒数ごとにトレーリングストップをチェック
   EventSetTimer(InpCheckInterval);
   
   Print("タイマー式トレーリングストップ起動 - チェック間隔: ", InpCheckInterval, "秒");
   Print("トレーリング距離: ", InpTrailDistance, " pips, ステップ: ", InpTrailStep, " pips");
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| 終了処理                                                          |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   EventKillTimer();
   Print("タイマー式トレーリングストップ停止");
}

//+------------------------------------------------------------------+
//| タイマーイベント：定期的にトレーリングストップを適用                 |
//+------------------------------------------------------------------+
void OnTimer()
{
   // 全オーダーをスキャンしてトレーリングストップを適用
   for(int i = OrdersTotal() - 1; i >= 0; i--)
   {
      // オーダーを選択
      if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
         continue;
      
      // 対象通貨ペアとマジックナンバーをフィルタ
      if(OrderSymbol() != Symbol())
         continue;
      if(OrderMagicNumber() != InpMagicNumber)
         continue;
      
      // トレーリングストップ処理を実行
      ApplyTrailingStop(OrderTicket(), OrderType(), OrderOpenPrice(), OrderStopLoss());
   }
}

//+------------------------------------------------------------------+
//| トレーリングストップの適用                                         |
//+------------------------------------------------------------------+
void ApplyTrailingStop(int ticket, int orderType, double openPrice, double currentSL)
{
   double point = MarketInfo(Symbol(), MODE_POINT);
   int digits   = (int)MarketInfo(Symbol(), MODE_DIGITS);
   
   // pipsをポイントに変換（3桁/5桁対応）
   double pipMultiplier = (digits == 3 || digits == 5) ? 10.0 : 1.0;
   double trailPoints   = InpTrailDistance * pipMultiplier * point;
   double stepPoints    = InpTrailStep * pipMultiplier * point;
   
   double newSL = 0;
   
   if(orderType == OP_BUY)
   {
      double bid = MarketInfo(Symbol(), MODE_BID);
      
      // 現在価格がトレーリング距離以上の利益に達しているか
      if(bid - openPrice >= trailPoints)
      {
         // 新しいストップロスを計算
         newSL = NormalizeDouble(bid - trailPoints, digits);
         
         // 現在のSLよりステップ分以上改善される場合のみ変更
         if(newSL > currentSL + stepPoints || currentSL == 0)
         {
            if(OrderModify(ticket, openPrice, newSL, OrderTakeProfit(), 0, clrGreen))
               Print("Buy #", ticket, " トレーリングSL更新: ", DoubleToString(newSL, digits));
            else
               Print("トレーリングSL更新失敗 #", ticket, " Error: ", GetLastError());
         }
      }
   }
   else if(orderType == OP_SELL)
   {
      double ask = MarketInfo(Symbol(), MODE_ASK);
      
      // 現在価格がトレーリング距離以上の利益に達しているか
      if(openPrice - ask >= trailPoints)
      {
         // 新しいストップロスを計算
         newSL = NormalizeDouble(ask + trailPoints, digits);
         
         // 現在のSLよりステップ分以上改善される場合のみ変更
         if(newSL < currentSL - stepPoints || currentSL == 0)
         {
            if(OrderModify(ticket, openPrice, newSL, OrderTakeProfit(), 0, clrRed))
               Print("Sell #", ticket, " トレーリングSL更新: ", DoubleToString(newSL, digits));
            else
               Print("トレーリングSL更新失敗 #", ticket, " Error: ", GetLastError());
         }
      }
   }
}

//+------------------------------------------------------------------+
//| OnTick（EA必須関数 - ここではエントリーロジック等を記述可能）       |
//+------------------------------------------------------------------+
void OnTick()
{
   // エントリーロジックはOnTickに記述
   // トレーリングストップはOnTimerで処理しているため、ここでは不要
}
</code></pre>
<p>OnTickでトレーリングストップを処理する一般的な方法と異なり、タイマー方式では価格変動がなくても定期的にチェックが行われます。特に流動性の低い通貨ペアや早朝の時間帯において、ストップロスの調整漏れを防ぐ効果があります。</p>
<h2><span id="toc8">プログラム例4：ミリ秒タイマーで高頻度監視を行うEA</span></h2>
<p><code>EventSetMillisecondTimer()</code>を使い、ミリ秒単位の高頻度で複数通貨ペアの価格を監視する例です。急変検知やアラート通知などの用途に適しています。</p>
<pre><code class="language-mql4">//+------------------------------------------------------------------+
//| 高頻度マルチ通貨監視EA                                             |
//+------------------------------------------------------------------+
#property strict

// 入力パラメータ
input int    InpTimerMs       = 500;    // タイマー間隔（ミリ秒）
input double InpAlertPips     = 5.0;    // アラート閾値（pips）
input bool   InpSendPush      = false;  // プッシュ通知を送信するか

// 監視対象通貨ペア
string WatchSymbols[] = {"EURUSD", "GBPUSD", "USDJPY", "AUDUSD"};

// 前回の価格を保存する配列
double PrevBid[];
// 最後にアラートを出した時刻（連続アラート防止用）
datetime LastAlertTime[];

//+------------------------------------------------------------------+
//| 初期化                                                            |
//+------------------------------------------------------------------+
int OnInit()
{
   int count = ArraySize(WatchSymbols);
   
   // 配列を初期化
   ArrayResize(PrevBid, count);
   ArrayResize(LastAlertTime, count);
   
   // 各通貨ペアの初期Bid価格を取得
   for(int i = 0; i < count; i++)
   {
      PrevBid[i] = MarketInfo(WatchSymbols[i], MODE_BID);
      LastAlertTime[i] = 0;
      
      // 通貨ペアがマーケットウォッチに存在するか確認
      if(PrevBid[i] == 0)
         Print("警告: ", WatchSymbols[i], " の価格を取得できません。マーケットウォッチに追加してください。");
   }
   
   // ミリ秒単位でタイマーを設定
   if(!EventSetMillisecondTimer(InpTimerMs))
   {
      Print("ミリ秒タイマーの設定に失敗しました");
      return(INIT_FAILED);
   }
   
   Print("マルチ通貨監視開始 - 間隔: ", InpTimerMs, "ms, 閾値: ", InpAlertPips, " pips");
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| 終了処理                                                          |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   EventKillTimer();
   Print("マルチ通貨監視終了");
}

//+------------------------------------------------------------------+
//| タイマーイベント：ミリ秒間隔で価格変動を監視                       |
//+------------------------------------------------------------------+
void OnTimer()
{
   int count = ArraySize(WatchSymbols);
   
   for(int i = 0; i < count; i++)
   {
      // 現在のBid価格を取得
      double currentBid = MarketInfo(WatchSymbols[i], MODE_BID);
      
      // 価格が取得できない場合はスキップ
      if(currentBid == 0)
         continue;
      
      // 前回からの変動幅を計算
      double change = currentBid - PrevBid[i];
      
      // pipsに変換
      int digits = (int)MarketInfo(WatchSymbols[i], MODE_DIGITS);
      double point = MarketInfo(WatchSymbols[i], MODE_POINT);
      double pipMultiplier = (digits == 3 || digits == 5) ? 10.0 : 1.0;
      double changePips = change / (point * pipMultiplier);
      
      // 変動幅が閾値を超えた場合にアラート
      if(MathAbs(changePips) >= InpAlertPips)
      {
         // 連続アラート防止（最低30秒間隔）
         if(TimeCurrent() - LastAlertTime[i] >= 30)
         {
            string direction = (changePips > 0) ? "上昇" : "下落";
            string msg = StringFormat("%s 急変検知！ %s %.1f pips (%.5f → %.5f)",
                                      WatchSymbols[i], direction, 
                                      MathAbs(changePips), PrevBid[i], currentBid);
            
            // ログとアラート出力
            Print(msg);
            Alert(msg);
            
            // プッシュ通知（設定が有効な場合）
            if(InpSendPush)
               SendNotification(msg);
            
            LastAlertTime[i] = TimeCurrent();
         }
      }
      
      // 現在の価格を次回比較用に保存
      PrevBid[i] = currentBid;
   }
}

//+------------------------------------------------------------------+
//| OnTick（EA必須関数）                                              |
//+------------------------------------------------------------------+
void OnTick()
{
   // メインのトレードロジックはここに記述可能
   // 監視処理はOnTimerで独立して動作する
}
</code></pre>
<p><code>EventSetMillisecondTimer(500)</code>により0.5秒ごとに複数通貨ペアの価格を監視し、急激な変動を検知してアラートを出します。連続アラート防止のために最低30秒の間隔を設けている点がポイントです。</p>
<h2><span id="toc9">OnTimer関数のポイントと注意事項</span></h2>
<h3><span id="toc10">タイマーの基本ルール</span></h3>
<ul>
<li><strong>1つのEA/インジケーターにつきタイマーは1つだけ</strong> — <code>EventSetTimer()</code>を複数回呼ぶと、最後の設定で上書きされます。複数の異なる間隔で処理を行いたい場合は、最小間隔でタイマーを設定し、OnTimer内でカウンタを使って分岐させます。</li>
<li><strong>OnDeinit()でEventKillTimer()を必ず呼ぶ</strong> — タイマーの停止を忘れると、EAやインジケーターを削除した後もリソースが解放されない可能性があります。</li>
<li><strong>タイマーの精度には限界がある</strong> — 特にミリ秒タイマーの場合、OSやMT4の処理負荷により正確な間隔にならないことがあります。</li>
</ul>
<h3><span id="toc11">複数間隔の処理をOnTimerで実現する方法</span></h3>
<pre><code class="language-mql4">// 1秒タイマーで複数の処理間隔を実現する例
int timerCount = 0; // タイマーカウンター

int OnInit()
{
   // 最小間隔（1秒）でタイマーを設定
   EventSetTimer(1);
   timerCount = 0;
   return(INIT_SUCCEEDED);
}

void OnTimer()
{
   timerCount++;
   
   // 毎秒実行する処理
   UpdateClockDisplay();
   
   // 5秒ごとに実行する処理
   if(timerCount % 5 == 0)
   {
      CheckSpread();
   }
   
   // 60秒（1分）ごとに実行する処理
   if(timerCount % 60 == 0)
   {
      LogAccountStatus();
   }
   
   // 300秒（5分）ごとに実行する処理
   if(timerCount % 300 == 0)
   {
      CheckMultiTimeframeSignals();
      timerCount = 0; // オーバーフロー防止のためリセット
   }
}
</code></pre>
<h3><span id="toc12">OnTimerとOnTickの使い分け</span></h3>
<table>
<thead>
<tr>
<th>項目</th>
<th>OnTimer</th>
<th>OnTick</th>
</tr>
</thead>
<tbody>
<tr>
<td>呼び出しタイミング</td>
<td>指定した時間間隔ごと</td>
<td>新しいティック（価格変動）が来るたび</td>
</tr>
<tr>
<td>閑散時の動作</td>
<td>価格変動がなくても実行される</td>
<td>価格変動がないと実行されない</td>
</tr>
<tr>
<td>適した用途</td>
<td>定期監視、UI更新、時刻ベース処理</td>
<td>価格ベースの売買判断</td>
</tr>
<tr>
<td>バックテスト</td>
<td>ストラテジーテスターでは動作しない</td>
<td>正常にテスト可能</td>
</tr>
</tbody>
</table>
<h3><span id="toc13">重要な注意事項</span></h3>
<ul>
<li><strong>ストラテジーテスターでは動作しない</strong> — OnTimerはバックテスト中に呼び出されません。タイマーに依存するロジックは、バックテストでは別の方法（OnTick内での時刻チェック等）で代替する必要があります。</li>
<li><strong>重い処理は避ける</strong> — OnTimer内で重い処理（大量のオーダー検索、複雑な計算など）を頻繁に実行すると、MT4全体のパフォーマンスに影響します。特にミリ秒タイマーを使う場合は処理を軽量に保ちましょう。</li>
<li><strong>MarketInfo()で最新価格を取得する</strong> — OnTimer内ではBid/Ask変数が更新されない場合があるため、<code>MarketInfo(Symbol(), MODE_BID)</code>等で明示的に取得するのが確実です。</li>
<li><strong>週末・市場クローズ中もタイマーは動作する</strong> — MT4が起動している限りタイマーイベントは発生します。市場が閉まっている間に不要な処理が走らないよう、必要に応じて曜日や時刻のチェックを入れましょう。</li>
</ul>
<p>投稿 <a href="https://mql-programing.com/archives/3030/ontimer/">【MQL4関数】OnTimer関数とは？一定時間ごとに実行されるイベント関数</a> は <a href="https://mql-programing.com">自動売買を作ろう！</a> に最初に表示されました。</p>
]]></content:encoded>
					
		
		
			</item>
	</channel>
</rss>
