決済ログをCSVに1行ずつ追記していく方法を、以前の記事で解説しました。
ただ、CSVやテキストのログには、ある種の限界があります。
「100件目の決済だけを確認したい」と思っても、1行ずつ順番に読み進めないと、
100件目がファイルの何バイト目にあるのか分からないのです。
行の長さが決済ごとに少しずつ違うため、位置を計算で求められません。
この記事では、決済履歴を「固定長のバイナリレコード」として記録することで、
読みたい記録の番号から直接そのバイト位置を計算し、
先頭から読まずにアクセスする方法を解説します。
・テキストログで「N番目の記録」に直接アクセスできない理由
・FileWriteStruct/FileReadStructの書式と構造体の制約
・固定長レコードを設計し、位置計算で直接読み出す方法
・実際に動くサンプルコード全体
なぜテキストログでは「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で動かすまでの手順です。
- MetaEditorで新規EA「StructTradeLogEA」を作成し、上のコードを貼り付けてコンパイルする
- エラー・警告が0件であることを確認する
- デモ口座のチャートに適用し、何度か決済を行う
- EAを一度外すか端末を再起動し、「エキスパート」タブに最後の記録のログが出ることを確認する
- 端末のデータフォルダ(ファイル→データフォルダを開く)の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件だけに高速にアクセスしたいなら本記事の固定長バイナリ、
という使い分けを覚えておいてください。
コメント