A position opens at 09:00 and disappears at 12:00. Its lifetime is three hours. But if half its volume was added at 10:00 and most was closed at 11:00, “three hours per trade” says little about how long the exposure was actually held. A partial-close row is also not a complete trade simply because it contains realized profit.
This guide defines MT5 trade holding time for reporting, including scale-ins, partial exits and incomplete history. It develops an explicit FIFO attribution example rather than prescribing how long anyone should hold a position. The calculation belongs in your analytics application; it is not a dedicated MetaKit endpoint or a broker accounting statement.
Decide what the duration measures
Position lifetime runs from an identified opening to the final close of that episode. Closed-volume age measures how long each allocated portion was held before it exited. Open-position age measures time since an opening reference up to a stated observation cutoff. Those are different statistics and should have different column names.
MetaQuotes documents opening, exiting, reversal and close-by entry categories in its deal properties reference. A deal's entry category and volume determine how it changes exposure. Its profit field alone cannot tell you whether the position is now fully closed.
For scale-ins, there may be no unique entry time for every unit of an aggregated position. FIFO assigns exits to the oldest remaining entry volume. Another report might use proportional allocation. The choice changes the assigned ages. State the policy and apply it consistently; it does not establish the broker's economic or tax treatment.
Work through a two-entry position
Consider this fictional position in one unchanged direction, with volumes in the same contract units:
| Time | Event | Volume |
|---|---|---|
| 09:00 | Entry | 1.00 lot |
| 10:00 | Additional entry | 1.00 lot |
| 11:00 | Exit | 1.50 lots |
| 12:00 | Final exit | 0.50 lots |
The lifetime from first entry to final exit is 180 minutes. Under FIFO, the 11:00 exit consumes 1.00 lot aged 120 minutes and 0.50 lot aged 60 minutes. The final 0.50 lot exits aged 120 minutes. Total closed-volume time is 120 + 30 + 60 = 210 lot-minutes.
Divide by 2.00 closed lots to obtain 105 minutes of volume-weighted closed age. Neither 180 nor 105 is inherently wrong; they answer different questions. Averaging the timestamps of the two exit rows would produce a third measure that gives the 1.50-lot and 0.50-lot exits equal importance.
Normalize the deal chain before calculating
MetaKit's GET /v1/accounts/{id}/deals returns raw deal data including ticket, positionId, time, type, entry and volume. Preserve the raw records, page through the requested history and partition by account plus position identifier. Different accounts can reuse numeric identifiers. Exclude balance and credit operations from an exposure-duration calculation.
The current mapping labels reversal entry as in_out and close-by entry as out_by. Do not silently process either as an ordinary new entry. MetaQuotes notes in its position properties reference that a netting reversal can retain the position identifier. Grouping by that identifier and taking earliest-to-latest time can join opposite directions into one misleading holding period.
The strict example below handles only ordinary in and out rows for one reviewed, same-direction episode. Convert validated volumes into integer units of a chosen common volume quantum before calling it. The normalized timeMs and units fields are application fields, not renamed API parameters. Confirm timestamp semantics before comparing events from different brokers.
A strict FIFO calculation core
function fifoHoldingTime(events) {
if (!Array.isArray(events) || events.length === 0) {
throw new Error("Need a reviewed episode");
}
const lots = [], seen = new Set();
let previous = -1, first = null, last = null;
let remaining = 0, closed = 0, unitMs = 0;
for (const e of events) {
if (!e || typeof e.ticket !== "string" || !e.ticket ||
seen.has(e.ticket) || !["in", "out"].includes(e.kind) ||
!Number.isSafeInteger(e.timeMs) || e.timeMs <= previous ||
!Number.isSafeInteger(e.units) || e.units <= 0) {
throw new Error("Invalid, duplicate, ambiguous or unsupported deal");
}
seen.add(e.ticket);
previous = e.timeMs;
if (first === null && e.kind !== "in") throw new Error("Missing opening history");
if (first !== null && remaining === 0) throw new Error("Separate flat episodes");
first ??= e.timeMs;
last = e.timeMs;
if (e.kind === "in") {
lots.push({ timeMs: e.timeMs, units: e.units });
remaining += e.units;
} else {
if (e.units > remaining) throw new Error("Exit exceeds known exposure");
let needed = e.units;
while (needed > 0) {
const head = lots[0];
const used = Math.min(needed, head.units);
unitMs += used * (e.timeMs - head.timeMs);
head.units -= used;
needed -= used;
if (head.units === 0) lots.shift();
}
remaining -= e.units;
closed += e.units;
}
if (![remaining, closed, unitMs].every(Number.isSafeInteger)) {
throw new Error("Numeric range exceeded");
}
}
return {
remainingUnits: remaining,
closedUnits: closed,
closedWeightedMinutes: closed ? unitMs / closed / 60000 : null,
completedLifetimeMinutes: remaining === 0 ? (last - first) / 60000 : null,
};
}Use integer volumes [100, 100, 150, 50] for the worked example and chronological millisecond timestamps one hour apart. The output is zero remaining units, 200 closed units, 105 weighted minutes and 180 lifetime minutes. Tests should also include an exit without its opening, excess exit volume, duplicate tickets, equal timestamps and an unsupported reversal.
This helper deliberately rejects equal event times. MetaKit's current raw deal mapping uses second-resolution deal time rather than exposing native millisecond deal time. Two events in the same second can therefore need more evidence to establish order. A larger ticket number should not be invented as a time-order guarantee. Obtain a verified ordering source or mark the episode unresolved.
Make partial evidence visible in the report
A history window can begin after the first entry. If an exit is the first row, expanding the window or retrieving the missing chain is necessary. Returning zero minutes would turn missing evidence into an apparently valid short hold. The incremental history guide addresses durable collection and rechecking overlapping windows.
If exposure remains open, the function reports closed-volume age for the portion already exited and leaves completed lifetime null. Do not compare that partial number with completed episodes as though both were finished. Long-running open positions are absent from a closed-only sample, so show their count and current age separately alongside an explicit cutoff time.
Elapsed duration also includes nights, weekends and market closures. “Trading minutes” requires a verified symbol/session calendar and a different calculation. Do not remove every weekend by a global rule for instruments with different sessions. Report elapsed time by default and name any session-adjusted statistic precisely.
When aggregating many episodes, choose whether each episode or each closed unit receives equal weight. Include sample counts and the reporting interval; do not add hours across overlapping positions and call the sum account time in the market. For a practical presentation, add these distinct columns to your trading journal, and use the MetaKit documentation to confirm the source contract before building the adapter.
Sources and verification
Verified 21 October 2026. Field names and timestamp resolution were checked against the current deal mapping. The FIFO fixture and failure cases were exercised using synthetic data.
- MetaQuotes deal properties, entry categories and deal fields.
- MetaQuotes position properties, identifiers and netting reversals.
- MetaKit API documentation, raw deal access.