初心者からエキスパートへ:Reporting EAで詳細な取引レポートをマスターする
内容
はじめに
今回のテーマは、MetaTrader 5における取引レポート配信の課題をどのように改善するかという点です。前回の記事では、システムが正しく機能するために必要となる全体的なワークフローと要件を整理し、取引レポートをPDF形式で生成し、ユーザーが設定した任意の頻度で配信できるツールとしてReporting EAを紹介しました。
本記事では、その基盤をさらに強化し、レポート内容の充実化と配信プロセスの効率化の両方を実現することを目指します。MQL5と強力なPythonライブラリを組み合わせることで、より深く、より表現力のあるレポートの自動生成が可能になります。その過程で、異なる言語間を連携させ、堅牢かつシームレスな取引ソリューションを構築するための要点も紹介します。
詳細なレポートがトレーダーの思考プロセスや意思決定に大きな影響を与えることを考えれば、このテーマに取り組む価値は十分にあります。初期バージョンのPDFレポートにはわずか4つのデータポイントしか含まれておらず、これは明らかな制約であり、より包括的な分析が必要であるという重要な示唆でもありました。今後は、前回の記事の2つ目のセクション(「MQL5における取引レポートの理解」)で触れたとおり、拡張された指標、可視化、グラフ、複数の分析コンポーネントをレポートへ統合していきます。
最終的な目標は、アナリストやトレーダーが自らの取引戦略やワークフローに合わせてカスタマイズできる、柔軟で情報量の多いレポートツールを提供することです。

図1:初期バージョンReporting EAが生成した取引レポートのスクリーンショット
上記の画像は、reports_processor.pyスクリプトの初期バージョンによって生成されたPDFレポートから抜粋したものです。対応するコードは以下のとおりです。本記事では、このコードを改善し、より深い分析と洞察を提供できるレポートを生成する方法に焦点を当てます。
また、Pythonがレポート生成プロセスにもたらす追加の可能性を探るとともに、MQL5とPythonの連携をより効率的かつ柔軟にするための方法についても紹介します。
def generate_pdf(report, output_path): pdf = FPDF() pdf.add_page() pdf.set_font("Arial", size=12) pdf.cell(0, 10, f"Report Date: {report['date']}", ln=True) pdf.cell(0, 10, f"Total Trades: {report['trade_count']}", ln=True) pdf.cell(0, 10, f"Net Profit: ${report['net_profit']:.2f}", ln=True) pdf.cell(0, 10, f"Top Symbol: {report['top_symbol']}", ln=True) pdf.output(output_path)
図1と図2に示したレポート機能を比較すると、依然として改善の余地が多くあることがわかります。私たちの狙いは既存のソリューションを再構築することではなく、取引レポート生成における柔軟でカスタマイズ可能な代替手段を提供することです。
以下のMetaTrader 5ターミナルのレポート画面は、現時点における標準機能を示しており、今後導入する拡張機能の基準点となるものです。

図2:MQL5取引レポートの機能
Reporting EAアップデートの概要
以下の図3(フローダイアグラム)は、今回のReporting EAアップデートで実装される処理ワークフローを示しています。EAは取引履歴を日付付きCSVファイルとしてエクスポートし、Pythonのレポートジェネレーターを起動します。レポートジェネレーターはCSVを処理して各種分析を計算し、チャートを生成し、最終レポートであるPDFに加えて、pdf_pathおよびemail_sentステータスを含むコンパクトなJSON結果を書き出します。EA側ではそのJSONをポーリングし、内容を読み取って解析し、出力パスやファイルサイズを検証します。検証が正常に完了した場合、通知を送信し、必要に応じてレポートをアーカイブまたは開くことで、自動化されたレポートサイクルを完結させます。

図3:Reporting EAのプロセスフローチャート
Reporting EAコードの更新
この段階では、高品質なレポート出力を実現するために、MQL5側の更新を実装します。EAを論理的なセクションに分割し、レポート機能の目標を直接支えるコードの仕組みを説明していきます。MQL5エキスパートアドバイザー(EA)は取引執行だけに限定されるものではなく、多くの定型的な取引タスクを自動化できます。レポート生成はその最良の例です。現時点では軽量で専用のReporting EAに焦点を当てていますが、今回構築するレポートモジュールは、将来的により高機能なEAへ統合することも可能です。特化したEAとして設計することで、大きな問題を管理しやすい単位に分解し、レポート機能を個別に構築・改善し、スモークテストやポイントを絞ったデバッグで検証してから、洗練されたコンポーネントとして上位システムへ組み込めるようになります。
1. ヘッダー、Windowsインポート、ユーザー入力
このセクションでは、EAのメタデータを宣言し、必要最小限のWindows API呼び出し(絶対パスのファイル属性チェックとcmd.exeの起動)をインポートし、ユーザーが設定できる入力項目を公開します。EAが起動時に正しく検証できるよう、PythonPath、ScriptPath、OutputDirといった環境パスは正確に設定しておく必要があります。ReportFormats、EnableEmail、TestOnInitの各入力によって、コードを変更することなく動作を柔軟に制御できます。
#property copyright "Clemence Benjamin" #property version "1.01" #property description "Exports trading history in CSV and generates reports via Python" #property strict #define SW_HIDE 0 #define INVALID_FILE_ATTRIBUTES 0xFFFFFFFF #import "kernel32.dll" uint GetFileAttributesW(string lpFileName); #import #import "shell32.dll" int ShellExecuteW(int hwnd, string lpOperation, string lpFile, string lpParameters, string lpDirectory, int nShowCmd); #import //--- User inputs input string PythonPath = "C:\\Users\\YOUR_COMPUTER_NAME\\AppData\\Local\\Programs\\Python\\Python312\\python.exe"; input string ScriptPath = "C:\\Users\\YOUR_COMPUTER_NAME\\PATH_TO\\reports_processor.py"; input string ReportFormats= "html,pdf"; input string OutputDir = "C:\\Users\\YOUR_COMPUTER_NAME\\PATH_TO_WHERE_YOUR_Reports_are_ saved"; input bool EnableEmail = true; input bool TestOnInit = true;
2. 初期化と環境検証(OnInit)
OnInit()はEAのウォームアップ処理をおこない、乱数の初期化、設定されたパスのクリーンアップ、ターミナル内のFilesディレクトリのログ記録、Python実行ファイルおよび出力ディレクトリの存在確認、そしてMQL5\Filesへの書き込み権限チェックをおこないます。TestOnInitが有効な場合にはテスト実行を即時におこない、レポート生成チェーン全体が正しく接続されているかをスモークテストとして検証できます。
int OnInit() { Print(">> Reporting EA initializing…"); MathSrand((uint)TimeLocal()); string cleanPythonPath = CleanPath(PythonPath); string cleanScriptPath = CleanPath(ScriptPath); string cleanOutputDir = CleanPath(OutputDir); Print("PythonPath set to: ", cleanPythonPath); Print("ScriptPath set to: ", cleanScriptPath); Print("OutputDir set to: ", cleanOutputDir); string filesDir = TerminalInfoString(TERMINAL_DATA_PATH) + "\\MQL5\\Files\\"; Print("Terminal Files directory: ", filesDir); if(GetFileAttributesW(cleanPythonPath) == INVALID_FILE_ATTRIBUTES) Print("!! Python executable not found at: ", cleanPythonPath); else Print("✔ Found Python at: ", cleanPythonPath); // Write-permission test (MQL5\\Files) int h = FileOpen("test_perm.txt", FILE_WRITE|FILE_TXT); if(h == INVALID_HANDLE) Print("!! Cannot write to MQL5\\Files directory! Error: ", GetLastError()); else { FileWrite(h, "OK"); FileFlush(h); FileClose(h); FileDelete("test_perm.txt"); Print("✔ Write permission confirmed."); } if(TestOnInit) { Print(">> Test mode: running initial export."); RunDailyExport(); lastRunTime = TimeCurrent(); } return(INIT_SUCCEEDED); }
3. パスユーティリティと基本ヘルパー
パスの正規化やファイル存在チェックを堅牢に保つことで、多くの実行時エラーを未然に防ぐことができます。CleanPath()はパスの整形処理を行い、FileExists()は絶対パスとローカルファイルの両方に対応できる汎用的な存在チェックヘルパーです。これらの関数はEA全体で再利用されるため、中央集約的に管理することが重要です。
string CleanPath(string path) { string cleaned = path; StringReplace(cleaned, "/", "\\"); while(StringFind(cleaned,"\\\\")>=0) StringReplace(cleaned, "\\\\", "\\"); StringTrimLeft(cleaned); StringTrimRight(cleaned); return cleaned; } bool FileExists(string path) { if(StringFind(path, "\\", 0) >= 0) { uint attrs = GetFileAttributesW(path); return(attrs != INVALID_FILE_ATTRIBUTES); } else { int fh = FileOpen(path, FILE_READ|FILE_TXT); if(fh == INVALID_HANDLE) return false; FileClose(fh); return true; } }
4. CSV作成とエクスポート処理
ExportHistoryToCSV()は、取引履歴の選択をおこない、ヘッダーと各取引の行を含むCSVファイルを書き出します。ファイル名の衝突を避けるために MakeUniqueFilename()を使用しており(繰り返し実行する場合に有用です)、選択されたファイル名はグローバル変数LastExportedCSVに返されます。CSV作成は決定論的であり、Pythonスクリプトが期待する列を含める必要があります。
string MakeUniqueFilename(string baseName) { int dot = StringFind(baseName, ".", 0); string name = dot >= 0 ? StringSubstr(baseName, 0, dot) : baseName; string ext = dot >= 0 ? StringSubstr(baseName, dot) : ""; string ts = TimeToString(TimeCurrent(), TIME_DATE | TIME_SECONDS); StringReplace(ts, ".", ""); StringReplace(ts, " ", "_"); StringReplace(ts, ":", ""); int suffix = MathAbs(MathRand()) % 9000 + 1000; return name + "_" + ts + "_" + IntegerToString(suffix) + ext; } bool ExportHistoryToCSV(string filename) { datetime end = TimeCurrent(); datetime start = end - 7776000; // 90 days if(!HistorySelect(start, end)) { Print("!! HistorySelect failed."); return false; } int total = HistoryDealsTotal(); if(total == 0) { Print("!! No trading history found."); return false; } int attempts = 3; string tryName = filename; int fh = INVALID_HANDLE; for(int a=0; a<attempts; a++) { fh = FileOpen(tryName, FILE_WRITE|FILE_CSV|FILE_ANSI, ","); if(fh != INVALID_HANDLE) break; tryName = MakeUniqueFilename(filename); Sleep(200); } if(fh == INVALID_HANDLE) { Print("!! FileOpen failed after retries."); return false; } FileWrite(fh, "Ticket,Time,Type,Symbol,Volume,Price,Profit,Commission,Swap,Balance,Equity"); // ... iterate deals and FileWrite rows ... FileFlush(fh); FileClose(fh); LastExportedCSV = tryName; return true; }
5. Pythonの呼び出しとオーケストレーション(RunDailyExport)
RunDailyExport()は処理全体のハブとして機能します。CSVパスを構築し、cmd.exe経由でPythonを起動し、stdout/stderrをpython_run.logへリダイレクトすることで、EAが後からログ内容を確認できるようにします。続いて、Pythonによって生成されるreport_result_YYYY-MM-DD.jsonを優先的にポーリングし、もし取得できなければPDFファイルの存在確認にフォールバックします。この設計によって、EAはJSONを中心とした構造的なハンドシェイクで成功可否を確実に判断できます。
void RunDailyExport() { string filesDir = TerminalInfoString(TERMINAL_DATA_PATH) + "\\MQL5\\Files\\"; string dateStr = TimeToString(TimeCurrent(), TIME_DATE); StringReplace(dateStr, ".", ""); string csvBase = "History_" + dateStr + ".csv"; LastExportedCSV = ""; if(!ExportHistoryToCSV(csvBase)) { Print("!! CSV export failed"); return; } string csvFull = filesDir + LastExportedCSV; string cleanOutputDir = CleanPath(OutputDir); string pyLogName = "python_run.log"; string pyLogFull = filesDir + pyLogName; string pythonCmd = StringFormat("\"%s\" \"%s\" \"%s\" --formats %s --outdir \"%s\" %s > \"%s\" 2>&1", CleanPath(PythonPath), CleanPath(ScriptPath), csvFull, "pdf", cleanOutputDir, EnableEmail ? "--email" : "", pyLogFull); string fullCmd = "/c " + pythonCmd; PrintFormat("→ Launching: cmd.exe %s", fullCmd); int result = ShellExecute(fullCmd); PrintFormat("← ShellExecute returned: %d", result); // Then poll for JSON or fallback to PDF; see next section for JSON handling... }
6. JSON処理とフォールバック読み取り戦略
人間が読めるログに脆弱に依存することを避けるため、EAは機械可読なJSON結果を優先的に参照します。ReadFileText()は絶対パスのJSONを直接読み取りを試みますが、MetaTrader5が絶対パスでのFileOpenを拒否する場合、CopyToFilesAndRead()はcmdコピーを使用してJSONをMQL5\Filesにコピーし、その後FileOpenで読み取ります。JsonExtractString/JsonExtractBoolは、単純なJSON構造(pdf_path、email_sent)に十分対応できる、小さく実用的な抽出関数です。
string ReadFileText(string fullpath) { if(StringFind(fullpath, "\\", 0) >= 0) { uint attrs = GetFileAttributesW(fullpath); if(attrs == INVALID_FILE_ATTRIBUTES) return(""); int fh = FileOpen(fullpath, FILE_READ|FILE_TXT); if(fh == INVALID_HANDLE) return(""); string txt = ""; while(!FileIsEnding(fh)) { txt += FileReadString(fh); if(!FileIsEnding(fh)) txt += "\n"; } FileClose(fh); return txt; } else { int fh = FileOpen(fullpath, FILE_READ|FILE_TXT); if(fh == INVALID_HANDLE) return(""); // ... read and return ... } } string CopyToFilesAndRead(string absPath, string filesName) { string filesDir = TerminalInfoString(TERMINAL_DATA_PATH) + "\\MQL5\\Files\\"; string destAbs = filesDir + filesName; string cmd = "/c copy /Y \"" + absPath + "\" \"" + destAbs + "\" > nul 2>&1"; int r = ShellExecute(cmd); // wait briefly for copy then read local file return ReadTerminalLogFile(filesName); } string JsonExtractString(string json, string key) { /* tiny extractor: locate "key": "value" */ } bool JsonExtractBool(string json, string key) { /* tiny extractor: locate "key": true/false */ }
7. 検証、通知、およびスムーズなフォールバック
JSONを解析した後、EAはFileExists()を使用してpdf_pathとhtml_pathの値を検証し、必要に応じてSendNotification()を通じてトレーダーに通知します。JSONの読み取りまたは検証が失敗した場合、EAはpython_run.logの追跡と、期待されるReport_YYYY-MM-DD.pdf(サイズ>0)のポーリングへとスムーズにフォールバックします。この多層的アプローチは、信頼性(JSON)と堅牢性(PDF/ログフォールバック)のバランスを取るものです。
if(StringLen(pdfp) > 0) { if(FileExists(pdfp)) { PrintFormat("✔ PDF exists at (from JSON): %s", pdfp); SendNotification("Report PDF created: " + pdfp); } else Print("!! JSON says PDF path but file not found: ", pdfp); } // fallback: if JSON unavailable -> poll expectedPdf in OutputDir and check size
8. サポートヘルパーと終了処理
GetFileSizeByOpen()(安全なファイルサイズチェック)、ReadTerminalLogFile()(MQL5\Filesからのファイル読み取り)、そしてShellExecute()ラッパーといったユーティリティルーチンは小さなものですが重要です。OnDeinit()はクリーンアップ時にメッセージを出力します。これらはコンパクトかつ十分に文書化された状態に保ってください。それによりデバッグが容易になり、重複も削減されます。
long GetFileSizeByOpen(string filename) { int fh = FileOpen(filename, FILE_READ|FILE_BIN); if(fh == INVALID_HANDLE) return -1; int pos = FileSeek(fh, 0, SEEK_END); FileClose(fh); return (pos < 0) ? -1 : (long)pos; } string ReadTerminalLogFile(string filename) { int fh = FileOpen(filename, FILE_READ | FILE_TXT); if(fh == INVALID_HANDLE) return(""); string content = ""; while(!FileIsEnding(fh)) { content += FileReadString(fh); if(!FileIsEnding(fh)) content += "\n"; } FileClose(fh); return content; } int ShellExecute(string command) { return ShellExecuteW(0, "open", "cmd.exe", command, NULL, SW_HIDE); }
reports_processor Pythonスクリプトの更新
EAはPythonのreports_processor.pyスクリプトと連携して動作します。そのため、EAを更新した今、Python側もEAが効率的に結果を取得し、利用できるように調整する必要があります。このセクションでは、スクリプトコードを詳細に解説し、CSVの取り込みから分析、PDFの生成、JSON形式での結果出力まで、各処理がどのようにEAと統合されるかを正確に示します。これにより、信頼性が高く自動化可能なレポートパイプラインを構築できます。
スクリプトの詳細に入る前に、まずPython環境を適切に設定して、システムがスクリプトの全機能をサポートできる状態にする必要があります。以下の手順では、インストールから始まるセットアッププロセスを順を追って説明します。この重要なステップを完了することで、環境が正しく構成されていることを確認でき、reports_processorスクリプトの開発中にスムーズにテストを実行できるようになります。
ステップバイステップガイド:Windows上でreports_processorスクリプト用にPythonを設定する
1. python.orgからPython 3.9以降をインストールし、pythonがPATHに含まれていることを確認します。
2. PowerShellを開き、venvを作成してアクティブ化します。
python -m venv venv .\venv\Scripts\Activate.ps1
3. pipをアップグレードします。
python -m pip install --upgrade pip
4. コアパッケージをインストールします(fpdfはデフォルトのPDFバックエンドです。必要に応じて他のパッケージも追加します。)
pip install pandas numpy jinja2 matplotlib fpdf yagmail
オプションのバックエンド
pip install pdfkit weasyprint
wkhtmltopdfを使用する場合は、Windows用インストーラーをダウンロードし、実行可能ファイルをPATHに追加するか、WKHTMLTOPDF_PATH環境変数を設定します。
5. メール用の環境変数を設定します(例:システムプロパティ→環境変数またはPowerShellで設定)
setx YAGMAIL_USERNAME "you@example.com" setx YAGMAIL_PASSWORD "your-email-password" setx WKHTMLTOPDF_PATH "C:\Program Files\wkhtmltopdf\bin\wkhtmltopdf.exe" # if using wkhtmltopdf
6. テストを実行します(仮想環境をアクティブにした状態で、プロジェクトフォルダから)。
python reports_generator.py "C:\path\to\MQL5\Files\History_20250821.csv" --formats pdf --outdir "C:\path\to\Reports" --email
7. C:\path\to\Reportsで出力を確認します。
- Report_YYYY-MM-DD.pdf(メインレポート)
- equity_curve_YYYY-MM-DD.png(チャート)
- report_result_YYYY-MM-DD.json(EAとのハンドシェイク)
Reports_processor.py
1. 起動、インポート、バックエンド検出
スクリプトは起動時に、Python標準ライブラリ(os、sys、logging、datetime、argparse、glob)とコアデータライブラリ(pandas、numpy)を読み込みます。続いて、オプションのツールチェーン(WeasyPrint、pdfkit (wkhtmltopdf)、FPDF、yagmail)をインポートし、それぞれのバックエンドの可用性を示すフラグを記録します。これらのフラグにより、スクリプトは一部のオプションパッケージが存在しない環境でも実行可能であり、インストール状況に応じて適切なPDF生成やメール送信のルートを実行時に選択できます。ログ記録はスクリプトの早い段階で設定されるため、EAがpython_run.logで取得できるタイムスタンプ付きの情報メッセージやエラーメッセージを出力できます。
#!/usr/bin/env python # reports_processor.py __version__ = "1.0.1" import sys import os import traceback import argparse import glob from datetime import datetime, timedelta import logging # core data libs import pandas as pd import numpy as np # templating & plotting from jinja2 import Environment, FileSystemLoader import matplotlib.pyplot as plt # optional libraries: import safely WEASY_AVAILABLE = False PDFKIT_AVAILABLE = False FPDF_AVAILABLE = False YAGMAIL_AVAILABLE = False try: from weasyprint import HTML # may fail if native libs aren't installed WEASY_AVAILABLE = True except Exception: WEASY_AVAILABLE = False try: import pdfkit # wrapper for wkhtmltopdf PDFKIT_AVAILABLE = True except Exception: PDFKIT_AVAILABLE = False try: from fpdf import FPDF # pure-python PDF writer (no native deps) FPDF_AVAILABLE = True except Exception: FPDF_AVAILABLE = False try: import yagmail YAGMAIL_AVAILABLE = True except Exception: YAGMAIL_AVAILABLE = False # Configure logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
2. 引数の解析とコマンド仕様
このスクリプトは小さなCLIを提供します。位置引数「csv_path」は、CSVファイルのパスまたは検索対象のフォルダーのいずれかを受け入れます。「--formats」は作成する出力(HTML、PDF、または両方)を制御します。「--outdir」は成果物の書き込み先を設定します。「--email」はレポート送信の有効/無効を切り替えます。「--pdf-backend」は使用するPDF生成バックエンドを選択します。この仕様はEAの呼び出しパターンと一致しており、EAはCSVパス、出力ディレクトリ、およびオプションのメールフラグを提供し、出力ディレクトリで生成されるJSON結果を監視します。
def parse_args(): """Parse command-line arguments.""" parser = argparse.ArgumentParser(description="Generate trading reports (HTML/PDF) from CSV.") parser.add_argument("csv_path", help="Path to the trades CSV file or a folder to search for History_*.csv") parser.add_argument("-f", "--formats", nargs="+", choices=["html", "pdf"], default=["pdf"], help="Report formats to generate (html, pdf, or both)") parser.add_argument("-o", "--outdir", default=".", help="Directory to write report files") parser.add_argument("--email", action="store_true", help="Send report via email") parser.add_argument("--pdf-backend", choices=["weasyprint","wkhtmltopdf","fpdf"], default="fpdf", help="PDF backend to use. 'weasyprint', 'wkhtmltopdf', or 'fpdf' (no native deps). Default: fpdf") return parser.parse_args()
3. CSV解決(入力ファイルの検索)
スクリプトにフォルダや柔軟なcsv_pathが指定された場合、処理対象の実際のCSVファイルを特定します。csv_pathがディレクトリの場合、History_*.csvファイルを検索し、一致するものがない場合は任意の.csvファイルにフォールバックして、最終更新日時が最新のファイルを選択します。完全なCSVパスが指定された場合は、そのまま使用されます。これにより、スクリプトは柔軟かつ呼び出しやすくなり、EA(特定のCSVを渡す場合)からの自動実行でも、手動実行でも容易に対応できます。
def resolve_csv_path(csv_path): """ Ensure csv_path points to an existing file. If not, try to locate the most recent History_*.csv in the same directory or the provided folder. Returns resolved path or raises FileNotFoundError. """ # If csv_path is a directory -> look there if os.path.isdir(csv_path): search_dir = csv_path else: # If path exists as file -> return if os.path.isfile(csv_path): return csv_path # otherwise, take parent directory to search search_dir = os.path.dirname(csv_path) or "." pattern = os.path.join(search_dir, "History_*.csv") candidates = glob.glob(pattern) if not candidates: # also allow any .csv if no History_*.csv found candidates = glob.glob(os.path.join(search_dir, "*.csv")) if not candidates: raise FileNotFoundError(f"No CSV files found in {search_dir} (tried {pattern})") # pick the newest file by modification time candidates.sort(key=os.path.getmtime, reverse=True) chosen = candidates[0] logging.info(f"Resolved CSV path: {chosen} (searched: {search_dir})") return chosen
4. 取引データの読み込みと正規化
ローダーはpandasを使用してCSVを読み込み、時間列を日付時刻に解析しようとします。必要な列(例:利益、銘柄、残高、エクイティ)が存在することを確認し、欠損している場合はデフォルト値を入力するか、可能な場合は値を計算します(例:残高やエクイティの累積合計)。正規化後、DataFrameは時間順にソートされ、一貫性のある時系列データセットが作成され、分析やプロットに使用可能になります。
def load_data(csv_path): """Load and validate CSV data, computing missing columns if necessary.""" try: if not os.path.isfile(csv_path): logging.error(f"CSV file not found: {csv_path}") raise FileNotFoundError(f"CSV file not found: {csv_path}") # try parsing Time; if fails, read without parse and try to coerce later try: df = pd.read_csv(csv_path, parse_dates=["Time"]) except Exception: df = pd.read_csv(csv_path) if "Time" in df.columns: df["Time"] = pd.to_datetime(df["Time"], errors="coerce") # Provide minimal defaults if columns missing if "Profit" not in df.columns: df["Profit"] = 0.0 if "Symbol" not in df.columns: df["Symbol"] = "N/A" required_cols = ["Time", "Symbol", "Profit", "Balance", "Equity"] for col in required_cols: if col not in df.columns: logging.warning(f"Missing column: {col}. Attempting to compute.") if col == "Balance": # create cumsum of profit + commission + swap if possible df["Commission"] = df.get("Commission", 0) df["Swap"] = df.get("Swap", 0) df["Balance"] = (df["Profit"] + df["Commission"] + df["Swap"]).cumsum() elif col == "Equity": df["Equity"] = df.get("Balance", df["Profit"].cumsum()) # ensure Time column exists and is sorted if "Time" in df.columns: df = df.sort_values(by="Time") return df except Exception as e: logging.error(f"Error loading CSV: {e}") raise
5. 分析と指標の計算
compute_statsステップでは、EAやユーザーが重要視する主要なレポート指標を算出します。具体的には、レポートの日付文字列、純利益、総取引数、集計利益に基づく最も成績の良い銘柄、エクイティカーブから計算された最大ドローダウン、そして単純なシャープ比の推定値です。これらの値は統計辞書にまとめられ、後でHTMLテンプレートやPDFページの両方に埋め込まれて、簡単な概要として提示されます。
def compute_stats(df): """Compute trading statistics from the DataFrame.""" try: top_symbol = None try: top_symbol = df.groupby("Symbol")["Profit"].sum().idxmax() except Exception: top_symbol = "N/A" eq = df.get("Equity") if eq is None: eq = df.get("Balance", df["Profit"].cumsum()) max_dd = float((eq.cummax() - eq).max()) if len(eq) > 0 else 0.0 sharpe = 0.0 if len(df) > 1 and "Profit" in df.columns: dif = df["Profit"].diff().dropna() if dif.std() != 0: sharpe = float((dif.mean() / dif.std()) * np.sqrt(252)) stats = { "date": datetime.now().strftime("%Y-%m-%d"), "net_profit": float(df["Profit"].sum()) if "Profit" in df.columns else 0.0, "trade_count": int(len(df)), "top_symbol": top_symbol, "max_drawdown": max_dd, "sharpe_ratio": sharpe } return stats except Exception as e: logging.error(f"Error computing stats: {e}") raise
6. チャート作成 - エクイティ/残高カーブ
スクリプトはmatplotlibを使用して、エクイティカーブのPNGを生成します。タイムスタンプが存在する場合は、時間足に対して残高とエクイティをプロットします。タイムスタンプがない場合は、インデックスに対して残高とエクイティをプロットします。生成されたPNGは「--outdir」に保存され、後でHTMLテンプレートで参照されるか、PDFに埋め込まれて、報告期間中の口座パフォーマンスを視覚的に示します。
# Generate equity curve plot curve_image = os.path.join(args.outdir, f"equity_curve_{stats['date']}.png") plt.figure() # handle possibility of NaT in Time if "Time" in df.columns and not df["Time"].isnull().all(): plt.plot(df["Time"], df["Balance"], label="Balance") plt.plot(df["Time"], df["Equity"], label="Equity") else: # fallback: plot index vs balance/equity plt.plot(df.index, df["Balance"], label="Balance") plt.plot(df.index, df["Equity"], label="Equity") plt.title("Balance & Equity Curve") plt.legend() plt.tight_layout() plt.savefig(curve_image) plt.close() logging.info(f"Equity curve saved: {curve_image}")
7. Jinja2を使ったHTMLレンダリング
HTML出力が要求された場合(または中間成果物として生成される場合)、スクリプトはJinja2を使用してreport_template.htmlをレンダリングします。テンプレートには、統計辞書、銘柄ごとの利益集計、そしてカーブ画像のファイル名が渡されます。レンダリングされたHTMLは出力フォルダに書き込まれ、ログにも記録されます。このHTMLは、最終成果物として利用することも、HTMLからPDFへの変換の入力として利用することも可能です。
def render_html(df, stats, outpath, curve_image): """Render the HTML report using a Jinja2 template.""" try: env = Environment(loader=FileSystemLoader(os.path.dirname(__file__))) tmpl_path = os.path.join(os.path.dirname(__file__), "report_template.html") if not os.path.isfile(tmpl_path): logging.error(f"Template not found: {tmpl_path}") raise FileNotFoundError(f"Missing template: {tmpl_path}") tmpl = env.get_template("report_template.html") html = tmpl.render(stats=stats, symbol_profits=df.groupby("Symbol")["Profit"].sum().to_dict(), curve_image=curve_image) os.makedirs(os.path.dirname(outpath), exist_ok=True) with open(outpath, "w", encoding="utf-8") as f: f.write(html) logging.info(f"HTML written: {outpath}") except Exception as e: logging.error(f"Error rendering HTML: {e}") raise
8. PDF生成 - 複数のバックエンド
このスクリプトは3つのPDF生成戦略をサポートしており、「--pdf-backend」引数と検出された可用性に基づいて1つを選択します。
- WeasyPrint:WeasyPrintが存在する場合、レンダリング済みのHTMLを直接PDFに変換します(HTML→PDF)。
- wkhtmltopdf (pdfkit):設定に応じて、wkhtmltopdfバイナリを使用してHTMLをPDFに変換します。
- FPDF:プログラムでPDFを構築し、HTML/CSSレンダリングに依存せずにグラフやデータテーブルを挿入する純粋なPython経路です。この方法では、タイトルページ、主要な統計ブロック、エクイティチャート、銘柄利益表、最近の取引サンプルを含む複数ページのA4 PDFを生成します。
def convert_pdf_weasy(html_path, pdf_path): if not WEASY_AVAILABLE: raise RuntimeError("WeasyPrint backend requested but weasyprint is not available.") os.makedirs(os.path.dirname(pdf_path), exist_ok=True) HTML(html_path).write_pdf(pdf_path) logging.info(f"PDF written: {pdf_path}") def convert_pdf_wkhtml(html_path, pdf_path, wk_path=None): if not PDFKIT_AVAILABLE: raise RuntimeError("wkhtmltopdf/pdfkit backend requested but pdfkit is not available.") config = None if wk_path and os.path.isfile(wk_path): config = pdfkit.configuration(wkhtmltopdf=wk_path) pdfkit.from_file(html_path, pdf_path, configuration=config) logging.info(f"PDF written: {pdf_path}") def convert_pdf_with_fpdf(df, stats, curve_image_path, pdf_path): if not FPDF_AVAILABLE: raise RuntimeError("FPDF backend requested but fpdf is not installed.") pdf = FPDF(orientation='P', unit='mm', format='A4') pdf.set_auto_page_break(auto=True, margin=15) pdf.add_page() pdf.set_font("Arial", "B", 16) pdf.cell(0, 8, f"Trading Report - {stats.get('date','')}", ln=True, align='C') # ... add stats, insert curve image and tables ... if curve_image_path and os.path.isfile(curve_image_path): try: pdf.image(curve_image_path, x=10, y=None, w=190) except Exception as e: logging.warning(f"Could not insert curve image into PDF: {e}") pdf.output(pdf_path) logging.info(f"PDF written: {pdf_path}")
各バックエンドは、最終的なReport_YYYY-MM-DD.pdfファイルを構成された出力ディレクトリに書き込み、ログを通じて完了を報告します。
9. メール送信
「--email」フラグが設定されている場合、スクリプトはメールヘルパールーチンを使用して、生成されたレポートファイルを利用可能なメールライブラリ経由で構成された受信者に送信します。ルーチンは送信の成否をログに記録し、その結果は後でJSONハンドシェイクで捕捉されるため、EAはその実行中にメールが送信されたかどうかを判断できます。
def send_email(files, stats): """Send the generated reports via email.""" try: if not YAGMAIL_AVAILABLE: logging.error("yagmail not available; cannot send email. Install yagmail or remove --email flag.") return user = os.getenv("Your YAGMAIL ACCOUNT") pw = os.getenv("Your Email password") if not user or not pw: logging.error("Email credentials not set in environment variables (YAGMAIL_USERNAME, YAGMAIL_PASSWORD).") return yag = yagmail.SMTP(user, pw) subject = f"Trading Report {stats['date']}" contents = [f"See attached report for {stats['date']}"] + files yag.send(to=user, subject=subject, contents=contents) logging.info("Email sent.") except Exception as e: logging.error(f"Error sending email: {e}")
10. 結果JSONライター - EAハンドシェイク
出力を生成した後、スクリプトはpdf_path、html_path、email_sent、timestamp、およびexit_codeを含むコンパクトで機械可読なJSONをコンパイルします。JSONはreport_result_YYYY-MM-DD.jsonとして「--outdir」にアトミックに(一時ファイル + 名前変更を介して)書き込まれます。書き込みがアトミックであるため、EAはPython側が完了したことを示す信頼性の高い信号としてJSONファイルの存在を安全にポーリングでき、その内容を読み取って正確な出力場所とメール送信の有無を確認できます。
def write_result_json(outdir, pdf_path=None, html_path=None, email_sent=False, exit_code=0): """Write a small JSON file with the report result so MQL5 can poll/verify.""" import json, tempfile try: result = { "pdf_path": pdf_path or "", "html_path": html_path or "", "email_sent": bool(email_sent), "timestamp": datetime.now().isoformat(), "exit_code": int(exit_code) } os.makedirs(outdir, exist_ok=True) fname = os.path.join(outdir, f"report_result_{datetime.now().strftime('%Y-%m-%d')}.json") # atomic write fd, tmp = tempfile.mkstemp(prefix="._report_result_", dir=outdir, text=True) with os.fdopen(fd, "w", encoding="utf-8") as f: json.dump(result, f, indent=2) os.replace(tmp, fname) logging.info(f"Result JSON written: {fname}") return fname except Exception as e: logging.error(f"Failed to write result JSON: {e}") return None
11. メインフロー
main()関数は、引数を解析し、CSVを解決し、データを読み込んで正規化し、統計を計算し、エクイティチャートを生成し、HTMLをレンダリングし(要求された場合)、選択されたバックエンドを介してPDFを生成し、オプションでメールを送信し、出力ディレクトリ内の古いレポートを消去し、結果のJSONを書き込み、成功または失敗のコードで終了するなど、すべてを一つにまとめます。シーケンスとログ記録により、スクリプトを起動したEAが実行の進行状況(python_run.log経由)を追跡し、最終的なreport_result_...jsonに反応できるようになります。
def main(): """Main function to generate reports.""" args = parse_args() try: # Ensure output directory exists os.makedirs(args.outdir, exist_ok=True) # Resolve csv_path in case a folder or variable-name was provided try: csv_path_resolved = resolve_csv_path(args.csv_path) except FileNotFoundError as e: logging.error(str(e)) return 1 # Load data and compute stats df = load_data(csv_path_resolved) stats = compute_stats(df) # Generate equity curve plot curve_image = os.path.join(args.outdir, f"equity_curve_{stats['date']}.png") # ... plotting code ... base = os.path.join(args.outdir, f"Report_{stats['date']}") files_out = [] # Generate HTML report (if requested or needed by PDF backend) html_path = base + ".html" if "html" in args.formats: render_html(df, stats, html_path, os.path.basename(curve_image)) files_out.append(html_path) # Decide PDF backend and generate PDF backend = args.pdf_backend.lower() if hasattr(args, "pdf_backend") else "fpdf" if "pdf" in args.formats: pdf_path = base + ".pdf" if backend == "weasyprint": convert_pdf_weasy(html_path, pdf_path) elif backend == "wkhtmltopdf": convert_pdf_wkhtml(html_path, pdf_path, wk_path=os.getenv("WKHTMLTOPDF_PATH")) elif backend == "fpdf": convert_pdf_with_fpdf(df, stats, curve_image, pdf_path) files_out.append(pdf_path) # Send email if enabled if args.email: send_email(files_out, stats) # write result JSON for MQL5 pdf_file = next((f for f in files_out if f.endswith(".pdf")), "") html_file = next((f for f in files_out if f.endswith(".html")), "") write_result_json(args.outdir, pdf_path=pdf_file, html_path=html_file, email_sent=args.email, exit_code=0) return 0 except Exception as e: logging.error(f"Main execution error: {e}") traceback.print_exc() try: write_result_json(args.outdir, exit_code=1) except: pass return 1 if __name__ == "__main__": sys.exit(main())
12. エラー処理と失敗時のJSONの保証
main()全体を通じて、例外は最上位レベルでキャッチされるため、処理されないエラーが発生した場合、スクリプトはトレースバックをログに記録し、失敗を示すexit_codeを含むreport_result_YYYY-MM-DD.jsonを書き込みます。これにより、EAは常に(成功または失敗)を検査する決定論的なJSONオブジェクトを受け取ることができ、EAの検証およびフォールバックロジックが簡素化されます。
except Exception as e: logging.error(f"Main execution error: {e}") traceback.print_exc() # ensure a JSON is still written so MQL5 knows it failed try: write_result_json(args.outdir, exit_code=1) except: pass return 1
13. 消去と保持ポリシー
実行が正常に完了すると、スクリプトは出力ディレクトリを点検し、構成可能な期間(現在のスクリプトでは30日間)を超えた古い「Report_」ファイルを消去します。これにより、出力フォルダーが整理され、レポートを頻繁に実行するシステムでディスク容量が無制限に増加するのを防ぎます。
# Clean up old reports (older than 30 days) cutoff = datetime.now() - timedelta(days=30) for f in os.listdir(args.outdir): if f.startswith("Report_") and f.endswith(tuple(args.formats)): full = os.path.join(args.outdir, f) if datetime.fromtimestamp(os.path.getmtime(full)) < cutoff: os.remove(full) logging.info(f"Deleted old file: {full}")
EAとPythonスクリプトが実行時に統合される方法
ランタイムのハンドシェイクはシンプルです。EAはCSVをエクスポートし、スクリプトを起動します(CSVパス、--outdir、および任意のフラグを渡します)。PythonスクリプトはCSVを処理し、成果物(PNG、PDF)を出力フォルダに書き込み、最後にreport_result_YYYY-MM-DD.jsonを出力します。EAはそのJSONをポーリングし、存在を確認したら内容を読み取り、報告されたパスやファイルサイズを検証し、通知、アーカイブ、メール確認などの事後処理をおこないます。python_run.logとJSONを組み合わせることで、両コンポーネント間に堅牢でマシンが扱える仕様が確立されます。
テスト
テストは、EAをMetaTrader 5プラットフォームに展開し、Pythonスクリプトをバックグラウンドで起動し、実行する形でおこないました。結果はターミナルの[エキスパート]タブのログで監視できるほか、指定した出力ディレクトリ内の生成ファイルを確認することでも追跡できます。テストでは、Report_2025-08-21という名前の出力レポートを確認しました。このファイルの内容は、以前のバージョンよりもはるかに充実しており、適切に構成された表、エクイティカーブ、複数の計算済み指標が含まれ、いずれも明瞭な形式で提示され、トレーダーにとって価値ある洞察を提供します。以下に、生成されたレポートファイルおよびPDFコンテンツの一部を示すスクリーンショットを掲載します。

図4:Windowsエクスプローラーで表示された出力ファイル

図5:生成されたレポートPDFのスクリーンショット

図6:レポートで作成されたエクイティカーブ
結論
MQL5とPythonの統合を通じて、以前のバージョンのReporting EAが提供していた基本的な出力をはるかに上回る、機能豊富な取引レポートの提供に成功しました。特に注目すべき機能は自動生成されるエクイティカーブで、時間の経過に伴う残高とエクイティの動向を視覚的に追跡し、トレーダーがパフォーマンスの変化を即座に把握できるようにします。このシステムは、EAを外部のPythonレポートパイプラインに連携させることの実現可能性を示すとともに、そのコラボレーションをさらに洗練し、拡張する方法も提示しています。
EAコードを継続的に改善し、Pandas、Matplotlib、FPDFなどの強力なPythonライブラリを活用することで、高度な統計計算、洞察に富んだ表の生成、そしてレポートへの明確な視覚化の埋め込みが可能になります。EAとPythonスクリプト間の軽量JSONハンドシェイクにより、出力の確実な確認が保証され、ワークフローはシームレスかつ透明になります。さらに、自動メール送信や柔軟なPDFバックエンドといったオプション機能は、このフレームワークがさまざまな環境に適応可能であることを示しています。
今回の開発により、長期的なレポートソリューションの堅固な基盤が構築されました。生成されたドキュメントはトレーディングパフォーマンス分析の包括的ツールとして活用でき、トレーダーや投資家がトレンドの把握、ドローダウンなどのリスク評価、戦略の明確な検証を行うのに役立ちます。最終的に、Reporting EAとreports_processor.pyスクリプトの組み合わせにより、言語間統合による自動化の強化、意思決定の改善、そして将来的なより高度な分析の実現が可能であることが示されました。
重要な学び
| 重要な学び | 説明 |
|---|---|
| 外部依存関係を起動時に検証する | OnInit中に外部ツールやフォルダ(Python実行ファイル、スクリプトパス、出力ディレクトリ)の場所を常に確認します。GetFileAttributesWによる早期検証により、実行時の不明瞭な失敗を防ぎます。 |
| 機械可読のハンドシェイク(JSON)を使用する | 自由形式ログを解析する代わりに、PythonからMQL5への正式な信号として、小さく構造化されたJSON結果ファイルを使用します。これにより検証が決定論的かつプログラム可能になります。 |
| 結果ファイルをアトミックに書き込む | JSONを一時ファイル名で書き込み、その後名前変更/置換してアトミックに保存します。これによりEAが完了をポーリングする際の途中読み取り競合を防げます。 |
| MQL5の絶対パス制限に対応する | MQL5のFileOpenは任意の絶対パスを読めないことが多いため、フォールバックを用意します。MQL5\Filesにコピーするか、WinAPI ReadFileアプローチで読み取ることでEAが一貫して出力にアクセス可能になります。 |
| 堅牢なファイルの存在とサイズチェックを使用する | 存在する=完全であると仮定せず、属性とファイルサイズ(open + seek で末尾確認)をチェックして、レポートやバイナリが完全に書き込まれたことを確認してから処理します。 |
| 衝突を避けるために一意のファイル名を使用する | CSVやレポートのファイル名にタイムスタンプやランダムサフィックスを含めることで、連続実行で結果を上書きせず、履歴を監査用に保持できます。 |
| Python用にCSVを一貫してエクスポートする | Pythonスクリプトが期待する列(ヘッダー、フォーマット、日時形式)でCSVを設計します。一貫性のあるエクスポート仕様により、解析の曖昧さやデータ加工エラーを減らします。 |
| ヘルパーユーティリティを一元管理する | パス正規化、ファイル存在チェック、JSON抽出、小規模な読み書きラッパーを1箇所にまとめ、EA内のすべての箇所で堅牢なロジックを再利用できるようにします(例:CleanPath、FileExists、ReadTerminalLogFile)。 |
| 適切なタイムアウトでポーリングを実装する | 外部プロセスを待つ際は、短いポーリング間隔と全体タイムアウトを組み合わせます。応答性とCPU使用量のバランスを取り、無限待機を防ぎます。 |
| フォールバックと多層検証を提供する | 多層チェックを設計します。JSONベースの確認を優先し、欠如時はpython_run.logをtail、さらに失敗時は期待されるPDFをポーリングします。複数の検証経路により堅牢性が向上します。 |
| 積極的にログに記録し、診断情報を明示する | 情報ログを丁寧に記録し、Pythonログの末尾を[エキスパート]タブのログに出力します。明確な診断情報は、運用中に実行が失敗した際のトラブルシューティングを加速します。 |
| 認証情報をハードコードしない | ソースコードに秘密情報を配置せず、メール認証情報は環境変数で管理し、必要な変数を文書化します。セキュリティと移植性が向上します。 |
| 複数のPDFバックエンドをサポートし、可用性を検出する | Pythonスクリプトをバックエンド非依存にし、WeasyPrint、wkhtmltopdf/pdfkit、純粋Python FPDFの中から実行時に最適な選択肢を検出して使用します。 |
| 仮想環境を利用し、設定を文書化する | 再現可能なWindows上の設定を文書化します(Pythonバージョン、virtualenv作成手順、pip install一覧、オプションバックエンドのネイティブ前提条件など)。 |
| エンドツーエンドフローのスモークテストを実施する | 簡単なエンドツーエンド「スモークテスト」を実施します。EAを展開しCSVをエクスポート、Pythonを起動、[エキスパート]タブのログと出力フォルダのPDF/JSONを確認します。詳細なQA前に迅速な確認が可能です。 |
| 決定論的なエラーレポートを保証する | 例外発生時は常にexit_codeと診断フィールドを含む結果JSONを書き込みます。これによりEAがエラーを検知し、フォールバックロジックを適用できるため、無限待機を回避できます。 |
添付ファイル
| ファイル名 | バージョン | 説明 |
|---|---|---|
| Reporting_EA.mq5 | 1.01 | 取引履歴を一意のCSVファイルにエクスポートし、Pythonレポートプロセッサを起動して出力を検証するEA。機能には、パス正規化、依存関係チェック、アトミックなファイル名生成、PythonとのJSON優先ハンドシェイク、絶対パス読み取り用のコピー・フォールバック、PDFポーリング・フォールバック、[エキスパート]への詳細ログ記録、およびオプションのプッシュ通知が含まれます。 |
| Reports_processor.py | 1.0.1 | EA CSVを取り込み、分析(純利益、ドローダウン、シャープレシオ、銘柄別集計)を計算し、チャートを作成、HTMLやPDFをレンダリングするPythonレポートエンジン(複数バックエンド対応:fpdf、wkhtmltopdf、WeasyPrint)。オプションでメール送信をおこない、EAハンドシェイク用にアトミックなreport_result_YYYY-MM-DD.jsonを書き込みます。保持期間に基づく消去機能と、失敗時にもJSONが保証される堅牢なエラー処理を備えています。 |
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/19006
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
MetaTraderとGoogleシートがPythonAnywhereで融合:安全なデータフローのガイド
取引システムの構築(第3回):現実的な利益目標のための最小リスクレベルの決定
MQL5での取引戦略の自動化(第29回):プライスアクションに基づくガートレーハーモニックパターンシステムの作成
MQL5でのデータベースの簡素化(第1回):データベースとSQL入門
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索