A stable, typed, encrypted snapshot of your MT5 deal history. Build a CLI, a dashboard, a notebook, or an AI agent on top β the snapshot is the contract.
ββββββββββββββββ writes ββββββββββββββββββ reads ββββββββββββββββ
β mt5-pnl- β βββββββββΊ β snapshot.json β ββββββββΊ β mt5-pnl-cli β
β exporter β β .gz.age β β mt5-pnl-ui β
β (this repo) β β (the contract) β β your tools β
ββββββββ¬ββββββββ ββββββββββββββββββ ββββββββββββββββ
β² MT5 deal history
ββββββββ΄ββββββββ
β Windows host β
ββββββββββββββββ
Runs on the Windows host where MT5 lives, reads deal history with a read-only investor password, writes one encrypted file. No daemon, no database, no third-party service.
- Why
- Install
- Prepare the MT5 host
- Quick start
- Commands
- Configuration
- How it works
- Schema
- Snapshot size
- Threat model
- Contributing
- Licence
- Self-hosted. No myfxbook, no fxblue, no third party holds your trading data. Runs on a Windows host you control.
- Stable contract. One typed, versioned snapshot β build whatever frontend suits you. Schema follows
major.minor; minor bumps add optional fields, major bumps are breaking. - Read-only credentials. Investor passwords can view balances and trade history but can never place or modify a trade.
- Encrypted at rest. Snapshot is gzipped then
age-encrypted under a passphrase from the OS keychain. Safe to sync via Dropbox, OneDrive, or Syncthing.
On a bare Windows host, install uv first:
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"Then install the exporter:
uv tool install "mt5-pnl-exporter[mt5]" # Windows host with MT5
uv tool install mt5-pnl-exporter # any OS β schema command onlyDo this once on the Windows host, before your first export.
The exporter needs its own MT5 terminal β not a terminal running your EAs. mt5.login() switches the connected terminal's active account, so pointing the exporter at an EA terminal would log your EA out and halt trading. A dedicated, idle terminal (no EA attached) runs alongside your EA terminals without touching them. The investor login and your EA's master login are independent, concurrent sessions.
Install a second MT5 to its own path β e.g. C:\Program Files\MT5 Exporter\ β separate from any EA terminal.
Launch the dedicated terminal once, manually, log in with any of your investor passwords, dismiss any first-run dialogs, then close it. This saves the server config and clears the open-account wizard. Skip this and export fails with (-10005, 'IPC timeout') β the wizard on a fresh install blocks the API.
terminal_pathβ full path to the dedicated terminal'sterminal64.exe(e.g.C:\Program Files\MT5 Exporter\terminal64.exe).loginβ the account number (the MT5 login).serverβ the broker server name, shown in MT5's login dialog (e.g.BrokerName-Live).
Once the MT5 host is prepared:
$ mt5-pnl-exporter set-investor-password 1234567
Password stored in keychain for login 1234567.
$ mt5-pnl-exporter set-encryption-passphrase
Encryption passphrase stored in keychain.
$ cp config.example.yaml config.yaml && chmod 600 config.yaml
# edit config.yaml β snapshot_path, terminal_path, accounts
$ mt5-pnl-exporter export
[INFO] [export] Trend EA (1234567): 12 closed deals, 0 open, 0 cash flows OK
[INFO] [export] Scalper EA (7654321): 8 closed deals, 1 open, 2 cash flows OK
[INFO] [export] wrote ~/snapshots/mt5.json.gz.age (2026-06-04 12:00)After decrypt + gunzip, the snapshot looks like:
{
"schema_version": "1.0",
"generated_at": "2026-06-04T12:00:00Z",
"accounts": [
{
"login": 1234567,
"label": "Trend EA",
"currency": "USD",
"balance": 10240.50,
"equity": 10198.20,
"last_success_at": "2026-06-04T12:00:00Z",
"last_error": null
}
],
"closed_deals": [
{
"account": 1234567,
"ticket": 9876543,
"time": 1748867482,
"symbol": "EURUSD",
"type": 0,
"entry": 1,
"volume": 0.10,
"price": 1.0834,
"profit": 12.40,
"swap": 0.0,
"commission": -0.70,
"fee": 0.0
}
],
"open_positions": [],
"cash_flows": []
}ClosedDeal and OpenPosition carry every field MT5 emits β raw type and entry integers, time as Unix seconds, etc. See schema/snapshot.schema.json for the full shape.
mt5-pnl-exporter exportβ fetch deals from MT5 once and writesnapshot.json.gz.ageatomically.mt5-pnl-exporter set-investor-password <login>β store an investor password in the OS keychain (keyring).mt5-pnl-exporter set-encryption-passphraseβ store the snapshot encryption passphrase in the OS keychain (entered twice).mt5-pnl-exporter schemaβ regenerateschema/snapshot.schema.jsonfrom the pydanticSnapshotmodel.
Copy config.example.yaml to config.yaml (gitignored) and fill in your values:
snapshot_path: ~/snapshots/mt5.json.gz.age
terminal_path: C:\Program Files\MT5 Exporter\terminal64.exe
accounts:
- label: Trend EA
login: 1234567
server: BrokerName-Live
- label: Scalper EA
login: 7654321
server: BrokerName-LiveOn Unix hosts, run chmod 600 config.yaml β export warns when the file is group- or world-readable. Investor passwords and the encryption passphrase go in the OS keychain via set-investor-password and set-encryption-passphrase, never in this file.
export looks for config.yaml in the current working directory, so run it from the directory that holds the file. To run from anywhere else, point it at the file explicitly with --config/-c:
mt5-pnl-exporter export --config C:\Users\you\mt5\config.yamldeals (live) βββΊ Snapshot βββΊ gzip βββΊ age encrypt βββΊ snapshot.json.gz.age
MT5 terminal pydantic (~10Γ smaller) passphrase (atomic .tmp swap)
models from keychain
export logs into each account with its investor password, reads closed deals, open positions, and balance-family deals via the MetaTrader5 Python API, then assembles them into a typed Snapshot. The full history is rebuilt each run β idempotent, so a missed run auto-backfills on the next run.
Gzip + age encryption is mandatory, not optional. The on-disk file is always snapshot.json.gz.age; readers must reverse the pipeline (age decrypt β gunzip β json.loads) to decrypt. Sync services (Dropbox, OneDrive, Syncthing) and backups only ever see ciphertext.
You can't open the file directly β double-clicking a .age file just fails. To decode it, use the age CLI (brew install age on macOS; see the age site for other platforms):
age -d snapshot.json.gz.age | gunzip # prompts for the passphrase, prints the JSONschema/snapshot.schema.json is generated from the pydantic models and committed. CI (tests/test_schema_file.py) fails if it drifts. The on-disk file is the schema's JSON gzipped then encrypted with age under a passphrase from the OS keychain β consumers must reverse the same pipeline to read it.
The snapshot carries one record per closed deal (ClosedDeal), open position (OpenPosition), and balance-family deal β deposit, withdrawal, credit, charge, correction, bonus, commission (CashFlow). Plus one AccountSnapshot per account with balance, equity, currency, and the last-success/last-error stamps. No pre-aggregation β consumers slice the raw records however they want.
Schema version stamping is major.minor (SCHEMA_VERSION = "1.0"). Readers accept the same major and any minor β€ their own; minor bumps add optional fields, major bumps are breaking. Consumers vendor schema/snapshot.schema.json from a specific release.
The snapshot stores one record per closed deal, so it grows with trading volume. Rough sizing: ~350 bytes per closed-deal record. Ten accounts with two years of 50-deals-per-day-per-account history (~250 trading days/year) is around 90 MB; busier setups (200 deals/day) reach ~350 MB. Each export gzips the JSON before encrypting, so the on-disk file is roughly an order of magnitude smaller β the 350 MB worst case lands at ~35 MB on disk, which is what sync services (Dropbox, Syncthing) see.
The OS user account on the Windows host that runs the exporter is the trust boundary. Anyone with that account's session can read the keychain, run export, and read decrypted snapshots. The same applies to a consumer machine: anyone with that account's session can decrypt the snapshot. The exporter does not defend against a compromised user session on either side.
- Snapshot contents at rest off the Windows host. Sync services (Dropbox, OneDrive, Syncthing), backups, and transit only ever see the gzipped, age-encrypted file. Mandatory encryption is what gets you this.
- Investor passwords and the encryption passphrase, on disk and in logs. Stored only in the OS keychain. The
redact_filterstrips any registered secret from log lines.
- A compromised user session on either host. With keychain access the snapshot decrypts to plaintext.
- Traffic-analysis metadata. File size, sync timing, and whether an export ran today are visible to anyone observing the transport. age hides contents, not existence.
- Passphrase loss. There is no recovery. The snapshot is reproducible, though β re-run
exportto rebuild it from the broker's history. - The broker side. MT5 deal history lives on the broker's server and is governed by their controls, not by anything in this tool.
The file is encrypted at rest, so transport choice is a workflow decision, not a security one. scp/rsync over SSH, a synced folder (Dropbox/Syncthing/OneDrive), or reading on the same machine are all viable. Pick whichever fits.
See CONTRIBUTING.md. For security reports, see SECURITY.md.
MIT β see LICENSE.