決済ログをCSVに1行ずつ追記していく方法を、以前の記事で解説しました。

ただ、CSVやテキストのログには、ある種の限界があります。

「100件目の決済だけを確認したい」と思っても、1行ずつ順番に読み進めないと、

100件目がファイルの何バイト目にあるのか分からないのです。

行の長さが決済ごとに少しずつ違うため、位置を計算で求められません。

この記事では、決済履歴を「固定長のバイナリレコード」として記録することで、

読みたい記録の番号から直接そのバイト位置を計算し、

先頭から読まずにアクセスする方法を解説します。

この記事で分かること
・テキストログで「N番目の記録」に直接アクセスできない理由
・FileWriteStruct/FileReadStructの書式と構造体の制約
・固定長レコードを設計し、位置計算で直接読み出す方法
・実際に動くサンプルコード全体
  1. なぜテキストログでは「N番目の記録」に直接アクセスできないのか
    1. 既刊「前回の続きから読む」記事との違い
    2. 固定長バイナリレコードなら位置計算だけで直接アクセスできる
  2. FileWriteStruct/FileReadStructの書式と構造体の制約
    1. 関数シグネチャ(引数・戻り値)
    2. 構造体に入れてはいけないもの
  3. 記録用の構造体を設計する
    1. このEAが記録する4つの項目
    2. 構造体のサイズがそのままレコード1件分の長さになる
  4. 書き込み: OnTradeTransactionで決済ごとに1レコード追記する
    1. FILE_BINフラグでファイルを開く
    2. 末尾に移動してから書き込む
  5. 読み出し: 直接N番目のレコードにアクセスする
    1. レコード番号からバイト位置を計算する
    2. 先頭から読まずに1件だけ取り出す
  6. 実際に動くコード全体
  7. MetaEditorでのコンパイル・実行手順
  8. よくあるエラーと注意点
    1. 構造体にstring型を入れるとコンパイルエラーになる
    2. 構造体の定義を変更すると過去のファイルが読めなくなる
  9. 既刊のファイル操作記事との使い分け
  10. よくある質問
    1. 銘柄名などの文字列も構造体に含めたいのですが?
    2. MQL4でも同じ関数が使えますか?
    3. 書き込み中に電源が落ちたらファイルは壊れませんか?
  11. 【MQL5入門】FileWriteStruct/FileReadStructで決済履歴を固定長レコードのバイナリファイルに記録し直接読み出す方法:まとめ
    1. 関連記事

なぜテキストログでは「N番目の記録」に直接アクセスできないのか

まず、既刊のテキストログ記事とこの記事がどう違うのかを整理します。

既刊「前回の続きから読む」記事との違い

既刊のFileSeek/FileTellの記事は、前回読み終えたバイト位置を保存し、

そこから続きの行だけを読む設計でした。

これは「最後に読んだところから先」という連続的なアクセスには向いていますが、

「いきなり100件目だけを読みたい」という用途には使えません。

テキストログは1行の長さが決済ごとにバラバラなため、

100件目が何バイト目から始まるかを事前に計算できないからです。

固定長バイナリレコードなら位置計算だけで直接アクセスできる

補足
・全てのレコードを同じサイズ(バイト数)で記録すれば
・N番目のレコードの開始位置は「レコードサイズ×N」で計算できる
・1件も読まずに、目的の位置へ一気にジャンプできる

この記事では、決済1件ごとの情報を固定サイズの構造体にまとめ、

`FileWriteStruct`でバイナリファイルへそのまま書き込みます。

読み出すときは`FileSeek`で計算した位置まで移動し、

`FileReadStruct`で1件だけを取り出します。

FileWriteStruct/FileReadStructの書式と構造体の制約

この記事の中心になる2つの関数の仕様を確認します。

関数シグネチャ(引数・戻り値)

uint FileWriteStruct(
   int         file_handle,     //FileOpen()が返したハンドル
   const void &struct_object,  //書き込む構造体
   int         size = -1        //書き込むバイト数(省略時は構造体全体)
   );

uint FileReadStruct(
   int         file_handle,     //FileOpen()が返したハンドル
   const void &struct_object,  //読み込み先の構造体
   int         size = -1        //読み込むバイト数(省略時は構造体全体)
   );

公式リファレンス(FileWriteStruct)によると、

戻り値は「実際に書き込まれた(読み込まれた)バイト数」です。

`size`を省略すると、構造体のサイズがそのまま使われます。

構造体に入れてはいけないもの

注意
・string型(文字列)は使えない
・動的配列は使えない
・仮想関数を持つクラス(継承構造)は使えない

これらはサイズが可変、またはポインタを内部に含むため、「そのままバイト列として書き出す」

方式のFileWriteStructでは扱えません。

固定サイズの数値型(int/double/datetime/long等)と、

固定サイズの配列だけで構成する必要があります。

記録用の構造体を設計する

実際に書き込む構造体の内容を決めます。

このEAが記録する4つの項目

struct TradeRecord
  {
   datetime close_time; //決済時刻
   double   profit;     //決済損益
   double   lots;       //ロット数
   int      deal_type;  //DEAL_TYPE_BUY または DEAL_TYPE_SELL
  };

銘柄名(symbol)は文字列のため構造体に入れられません。

この記事のサンプルは「1つのチャート=1つの銘柄」を前提にし、

銘柄情報はファイル名の方に持たせます

(`TradeLog_EURUSD.bin`のように)。

補足
・複数銘柄をまとめて1ファイルに記録したい場合は
・固定サイズのuchar配列(char symbol[12]等)と
・StringToCharArrayを組み合わせる方法もある
・詳しくはこの記事末尾のよくある質問で紹介する

構造体のサイズがそのままレコード1件分の長さになる

この`TradeRecord`は、

datetime(8バイト)+double×2(16バイト)+int(4バイト)で、

合計28バイトになります

(環境によりパディングが入る可能性はありますが、

`sizeof(TradeRecord)`で常に正確なサイズを取得できます)。

このサイズが変わらない限り、何件目の記録でも「サイズ × 件数」で位置を計算できます。

書き込み: OnTradeTransactionで決済ごとに1レコード追記する

決済が発生した瞬間に、構造体を1件ぶん末尾へ追記します。

FILE_BINフラグでファイルを開く

int handle = FileOpen("TradeLog_" + _Symbol + ".bin",
                       FILE_READ | FILE_WRITE | FILE_BIN);

公式リファレンス(FileOpen)によると、`FILE_BIN`は「バイナリ読み書きモード(文字列への変換なし)」

を意味します。

`FileSeek`と`FileWriteStruct`を組み合わせる場合、

このフラグが必須です。

末尾に移動してから書き込む

FileSeek(handle, 0, SEEK_END);
FileWriteStruct(handle, rec);

書き込む前に`FileSeek(handle, 0, SEEK_END)`で、

ファイルポインタを末尾へ移動させ、

既存の記録を上書きしないようにします。

読み出し: 直接N番目のレコードにアクセスする

ここが、この記事の核心部分です。

レコード番号からバイト位置を計算する

long targetPos = (long)index * sizeof(TradeRecord);
FileSeek(handle, targetPos, SEEK_SET);

`index`は0件目から数えた記録の番号です。

`sizeof(TradeRecord)`を掛けるだけで、

その記録が始まるバイト位置が求まります。

補足(具体例)
・TradeRecordが28バイトのとき
・「101件目」(index=100)を読みたい場合
・100×28=2800バイト目がその記録の開始位置になる
・何件あっても、この掛け算1回で位置が分かる

先頭から読まずに1件だけ取り出す

TradeRecord rec;
FileReadStruct(handle, rec);
Print("決済時刻=", TimeToString(rec.close_time), " 損益=", rec.profit);

直前のレコード数が何千件あっても、この処理は「目的の位置へ移動して1件読むだけ」

で完了します。

要点
・何件記録されているかはFileSize(handle)で分かる
・(FileSize(handle)をsizeof(TradeRecord)で割るだけ)
・indexが0件目〜(総件数-1)の範囲内か
・読み出し前に必ず確認する

実際に動くコード全体

決済ごとの記録・指定した番号の読み出しの両方を含む、

実際にMetaEditorでコンパイルできるEAです。

//+------------------------------------------------------------------+
//|                                   StructTradeLogEA.mq5             |
//|                     Copyright 2026, FX-EA System Project Creator  |
//|                        https://creator.fx-ea-system-project.com/  |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, FX-EA System Project Creator"
#property link      "https://creator.fx-ea-system-project.com/"
#property version   "1.00"

struct TradeRecord
  {
   datetime close_time;
   double   profit;
   double   lots;
   int      deal_type;
  };

string g_fileName;

int OnInit()
  {
   g_fileName = "TradeLog_" + _Symbol + ".bin";
   return(INIT_SUCCEEDED);
  }

//決済のたびに1レコード追記する
void OnTradeTransaction(const MqlTradeTransaction &trans,
                         const MqlTradeRequest &request,
                         const MqlTradeResult &result)
  {
   if(trans.type != TRADE_TRANSACTION_DEAL_ADD)
      return;
   if(!HistoryDealSelect(trans.deal))
      return;
   if(HistoryDealGetInteger(trans.deal, DEAL_ENTRY) != DEAL_ENTRY_OUT)
      return;

   TradeRecord rec;
   rec.close_time = (datetime)HistoryDealGetInteger(trans.deal, DEAL_TIME);
   rec.profit     = HistoryDealGetDouble(trans.deal, DEAL_PROFIT);
   rec.lots       = HistoryDealGetDouble(trans.deal, DEAL_VOLUME);
   rec.deal_type  = (int)HistoryDealGetInteger(trans.deal, DEAL_TYPE);

   int handle = FileOpen(g_fileName, FILE_READ | FILE_WRITE | FILE_BIN);
   if(handle == INVALID_HANDLE)
     {
      Print("FileOpenに失敗しました。error=", GetLastError());
      return;
     }

   FileSeek(handle, 0, SEEK_END);
   FileWriteStruct(handle, rec);
   FileClose(handle);
  }

//指定した番号(0件目から)の記録だけを読み出す
bool ReadRecordByIndex(int index, TradeRecord &outRec)
  {
   int handle = FileOpen(g_fileName, FILE_READ | FILE_BIN);
   if(handle == INVALID_HANDLE)
     {
      Print("FileOpenに失敗しました。error=", GetLastError());
      return(false);
     }

   long totalRecords = (long)FileSize(handle) / sizeof(TradeRecord);
   if(index < 0 || index >= totalRecords)
     {
      Print("indexが範囲外です。総件数=", totalRecords);
      FileClose(handle);
      return(false);
     }

   long targetPos = (long)index * sizeof(TradeRecord);
   FileSeek(handle, targetPos, SEEK_SET);
   FileReadStruct(handle, outRec);
   FileClose(handle);
   return(true);
  }

//動作確認用: 最後に記録された1件をログへ出力する
void OnDeinit(const int reason)
  {
   int handle = FileOpen(g_fileName, FILE_READ | FILE_BIN);
   if(handle == INVALID_HANDLE)
      return;
   long totalRecords = (long)FileSize(handle) / sizeof(TradeRecord);
   FileClose(handle);

   if(totalRecords > 0)
     {
      TradeRecord last;
      if(ReadRecordByIndex((int)totalRecords - 1, last))
         Print("最後の記録: 時刻=", TimeToString(last.close_time),
               " 損益=", last.profit, " ロット=", last.lots);
     }
  }

MetaEditorでのコンパイル・実行手順

実際にMT5で動かすまでの手順です。

  1. MetaEditorで新規EA「StructTradeLogEA」を作成し、上のコードを貼り付けてコンパイルする
  2. エラー・警告が0件であることを確認する
  3. デモ口座のチャートに適用し、何度か決済を行う
  4. EAを一度外すか端末を再起動し、「エキスパート」タブに最後の記録のログが出ることを確認する
  5. 端末のデータフォルダ(ファイル→データフォルダを開く)のMQL5/Filesに`TradeLog_<銘柄名>.bin`が作成されていることを確認する

よくあるエラーと注意点

実装でつまずきやすいポイントをまとめます。

構造体にstring型を入れるとコンパイルエラーになる

注意
・構造体のメンバーにstring型を1つでも含めると
・FileWriteStructの呼び出しでコンパイルエラーになる
・銘柄名などの文字情報は、この記事のようにファイル名側で表現するか
・固定サイズのuchar配列で代用する(よくある質問を参照)

構造体の定義を変更すると過去のファイルが読めなくなる

一度`TradeRecord`にメンバーを追加・削除すると、

`sizeof`が変わり、過去に記録したファイルの区切り位置とズレてしまいます。

運用を始めた後に項目を増やしたい場合は、

新しいファイル名(別ファイル)で記録し直すことをおすすめします。

既刊のファイル操作記事との使い分け

似た目的の記事が複数あるため、使い分けを整理します。

記事 形式 得意なこと 苦手なこと
本記事 固定長バイナリ N番目への直接アクセス Excelでそのまま開けない
CSVログ(既刊) テキスト(可変長) Excelで目視確認 N番目への直接アクセス
FileSeek/FileTell(既刊) テキスト(可変長) 前回の続きだけ読む 任意の番号への直接アクセス
SQLite(既刊) データベース 条件集計・絞り込み 設定が本記事より重い

よくある質問

FileWriteStruct/FileReadStructについて、

よくある疑問に回答します。

銘柄名などの文字列も構造体に含めたいのですが?

string型は使えませんが、

固定サイズのuchar配列(例: `uchar symbol[12];`)と、

`StringToCharArray`/`CharArrayToString`を、

組み合わせる方法もあります。

ただし、配列のサイズより長い文字列を渡すとあふれる可能性があるため、

`StringToCharArray`の`count`引数で、

書き込む文字数を明示的に制限することをおすすめします

(小さなテストコードで動作を確認してから本番に使うことをおすすめします)。

MQL4でも同じ関数が使えますか?

FileWriteStruct/FileReadStructという関数名自体は、

MQL4にも存在しますが、

本記事のHistoryDealGetInteger等の履歴取得APIは、

MQL5独自の方式です。

MQL4版で同じ仕組みを作る場合は、

OrderSelect(MODE_HISTORY)で決済履歴を取得する、

既刊記事群の設計に置き換える必要があります。

書き込み中に電源が落ちたらファイルは壊れませんか?

理論上、1レコードの書き込み途中で異常終了すると、

そのレコードだけが不完全なバイト列として残る可能性があります。

重要な記録を失いたくない場合は、この記事のバイナリ方式に加えて、

既刊CSVログやSQLiteへの記録も並行して行う二重化も検討してください。

【MQL5入門】FileWriteStruct/FileReadStructで決済履歴を固定長レコードのバイナリファイルに記録し直接読み出す方法:まとめ

テキストログは1行の長さがバラバラなため、

「N番目の記録」に直接アクセスすることができません。

決済履歴を固定長の構造体として記録し、

FileWriteStruct/FileReadStructで読み書きすれば、

「レコードサイズ×件数」という単純な計算だけで、

読みたい記録へ直接ジャンプできます。

目視確認したいならCSV、集計したいならSQLite、

特定の1件だけに高速にアクセスしたいなら本記事の固定長バイナリ、

という使い分けを覚えておいてください。

関連記事