Exporting a statement once is straightforward. Keeping a trading database complete after restarts, delayed records and failed page requests is a different problem. An importer that saves its latest timestamp too early can skip deals permanently. One that never saves progress will repeatedly download years of history.
A dependable MT5 deal history sync uses fixed time windows, reads every page, deduplicates by account and deal identity, and commits records together with its checkpoint. This is a database ingestion design for a read-only application. If you only need a downloadable file, use the CSV export walkthrough.
Know what the endpoint guarantees
MetaKit exposes GET /v1/accounts/{id}/deals with from, to, page and limit. The current pagination envelope contains data, page, limit, total and total_pages. The default limit is 25 and the maximum is 100. Its handler reads terminal history for the requested interval and slices the resulting array for each request.
There is no snapshot token or documented stable cursor. Page two is another request, not a continuation of a frozen database view. Even with a fixed upper timestamp, a late history record or broker correction can change results between pages. That makes overlap and reconciliation necessary, rather than optional optimizations.
Native history_deals_get also retrieves deals, not one completed-trade summary per position. Keep the raw records. A later calculation can group them by positionId, with the cautions in orders, deals and positions.
Choose the interval before fetching page one
Maintain a checkpoint per account representing the last fully scanned upper bound. On the next run, start slightly before that checkpoint, then choose a new fixed upper bound. For example, scan from the checkpoint minus ten minutes to the current clock minus two minutes. Both durations are illustrative ingestion policies, not maximum broker delays.
Lagging the upper bound reduces activity near the boundary. Re-reading the overlap catches records that became visible after the previous scan. Neither guarantees discovery of a correction from a month ago. Schedule wider reconciliation scans separately, and document how far back they reach.
Use the time coordinate accepted by the endpoint consistently. MetaKit's current adapter formats terminal epoch values as UTC ISO strings without discovering a broker offset. Validate request boundaries and returned timestamps on the actual account. Do not apply an offset twice or use a local midnight without an explicit timezone.
For large histories, use small bounded windows and persist after each complete window. A quiet account might fit a day in one page; an active account might need a much shorter interval. Do not infer completeness from the size of the first page.
Read and validate the whole window
This JavaScript function accepts a getPage adapter that performs one authenticated GET and parses JSON. It intentionally fails the window if totals change, if a page has an unexpected size, or if duplicate ticket identities appear. It returns records only after the scan passes those checks.
async function collectWindow(getPage, accountId, from, to) {
const rows = new Map();
let expectedTotal;
let expectedPages;
const limit = 100;
for (let page = 1; ; page++) {
const body = await getPage(accountId, { from, to, page, limit });
if (!Array.isArray(body.data) || body.page !== page ||
body.limit !== limit || !Number.isSafeInteger(body.total) ||
body.total < 0 || !Number.isSafeInteger(body.total_pages) ||
body.total_pages !== Math.max(1, Math.ceil(body.total / limit))) {
throw new Error("Invalid pagination envelope");
}
expectedTotal ??= body.total;
expectedPages ??= body.total_pages;
if (body.total !== expectedTotal || body.total_pages !== expectedPages) {
throw new Error("History changed during scan; repeat the window");
}
const expectedSize = Math.min(limit, Math.max(0, expectedTotal - (page - 1) * limit));
if (body.data.length !== expectedSize) throw new Error("Incomplete page");
for (const deal of body.data) {
if (!Number.isSafeInteger(deal.ticket) || deal.ticket <= 0) {
throw new Error("Ticket cannot be represented safely by this client");
}
const key = String(deal.ticket);
if (rows.has(key)) throw new Error("Duplicate ticket within scan");
rows.set(key, deal);
}
if (page === expectedPages) break;
}
if (rows.size !== expectedTotal) throw new Error("Incomplete window");
return [...rows.values()];
}Construct the adapter URL using URL and searchParams, preserving the exact from and to for all pages. Stop on HTTP errors; do not turn a failed request into an empty successful page. A higher-level scheduler can retry the entire window using a bounded policy such as the read-client retry design.
Ticket safety deserves attention because native deal identifiers are 64-bit integers. This example rejects values outside JavaScript's safe-integer range. Converting an already-rounded JSON number to a string cannot restore its original digits. If an account can produce larger identifiers, use a lossless JSON parser that preserves numeric tokens, and adapt the validation and storage to decimal strings before importing.
The checks detect several forms of page drift, but are not a substitute for a server-side snapshot. Two different histories can have the same total. A later reconciliation pass remains part of the design.
Commit deals and progress together
For a single MetaKit user/environment, a small SQLite schema can store the account id and deal ticket as a composite primary key. If you combine users or providers, add that namespace to both tables and every key. Never use the ticket alone across unrelated accounts.
CREATE TABLE deals (
account_id INTEGER NOT NULL,
ticket TEXT NOT NULL,
payload TEXT NOT NULL,
PRIMARY KEY (account_id, ticket)
);
CREATE TABLE sync_state (
account_id INTEGER PRIMARY KEY,
scanned_through TEXT NOT NULL
);After network collection succeeds, begin a database transaction. Bind values through the driver's parameters and upsert each record. Update sync_state with the completed window's upper bound in the same transaction, then commit. On any error, roll back. SQLite documents explicit transaction commit and rollback; the driver integration must actually use those boundaries.
Upsert the entire raw payload so re-reading a corrected deal can replace its stored representation. Keep a separate audit history if your accounting requirements demand previous versions. Use a single writer per account, or an account lock that prevents an older worker from replacing a newer checkpoint.
A crash before commit leaves the old checkpoint, so the next run repeats the window. A crash after commit leaves both the records and progress. In either case an overlapping repeat changes no row count for deals already stored. This atomic boundary is what makes retries manageable.
Prove recovery before scheduling it
Use synthetic pages to test an empty interval, 101 deals over two pages, a failure on page two, changed totals, repeated tickets and unsafe identifiers. Then inject a database error after some upserts but before the checkpoint update. Confirm that rollback preserves the previous state and that a later successful replay inserts the complete batch exactly once per key.
Those pagination cases were exercised locally with mocks. A live broker synchronization was not performed. Before relying on the importer, compare a settled interval against the terminal statement, including balance operations and costs. Also verify the account identity whenever credentials are reconnected or an account configuration changes.
Track the last completed interval, row count, failure reason and age of the checkpoint. A job that runs successfully but scans the wrong account or date range is still an ingestion failure. Use the MetaKit documentation to confirm the current contract, and keep periodic reconciliation alongside event-driven updates.
Sources and verification
Verified 18 October 2026 against MetaKit's deal route, pagination helper and terminal serializer.
- MetaQuotes history_deals_get, native history retrieval.
- MetaQuotes deal properties, deal identity and position relationships.
- SQLite transactions, atomic persistence boundaries.
- MetaKit API reference, public endpoint and authentication.