The analytics worked for six months on the first broker. Then a customer connected an account from a second broker, and every trade on GBPJPY showed up merged into one enormous position with a duration of three weeks and a net of roughly zero. Nothing was wrong with the deals. The code assumed netting, and the new account was hedging.
Netting vs hedging in MT5 is a per-account setting, chosen by the broker, that decides what a "position" even is. The same clicks produce different objects, different ids, and different close semantics in each mode, and most integration code silently bakes in one of them.
One sequence, two outcomes
Buy 1.00 lot of EURUSD. A minute later, sell 0.40 lots of EURUSD. That's the whole test.
On a netting account there is one position per symbol, always. The sell is applied against the existing long, and you are left holding 0.60 lots long. In the deal history:
| deal | type | volume | entry | position id | profit |
|---|---|---|---|---|---|
| 1 | buy | 1.00 | in | 5001 | 0 |
| 2 | sell | 0.40 | out | 5001 | realised on 0.40 |
Two deals, one position id, one open position of 0.60. The sell was a partial close, whether or not you meant it as one.
On a hedging account every entry opens its own position. The sell does not touch the long; it sits beside it.
| deal | type | volume | entry | position id | profit |
|---|---|---|---|---|---|
| 1 | buy | 1.00 | in | 5001 | 0 |
| 2 | sell | 0.40 | in | 5002 | 0 |
Two deals, two position ids, two open positions, each with its own floating P&L, its own swap, and its own stop-loss. You're long 1.00 and short 0.40 at the same time, and the account's net exposure of 0.60 long is something you have to compute, because nothing in the terminal shows it.
Retail forex and CFD accounts are usually hedging. Exchange-traded instruments and some institutional setups are netting. The broker decides at account creation and you cannot flip it.
What "close position" means in each mode
In hedging mode a position is addressed by ticket. Close 5002 and the 1.00
long is untouched. Partially close 5001 down to 0.70 and it keeps its id with a
smaller volume. There is also close by: closing 5001 against 5002 in one
operation, which produces out_by deals on both and saves one spread. Reversal
does not exist; you'd close the long and open a short as two separate positions.
In netting mode there are no tickets to choose between. "Close" means "send an
opposite deal for the full current volume". A partial close is an opposite
deal for part of it. And an opposite deal for more than the current volume is
a reversal: sell 1.60 against the 0.60 long and you're now 1.00 short, recorded
as a single deal with entry: "in_out".
That reversal deal is the one that bites. The MQL5 documentation says the
position identifier does not change on a netting reversal (the position ticket
does). So if you group deals by position id, one group can contain a long leg
and a short leg with a flip in the middle. If your metrics want those as two
trades, split the group at the in_out deal.
The orders, deals and positions post covers the base model these rules sit on.
The bug that only shows on the other broker
Here's the code that caused the GBPJPY incident, in spirit:
open_by_symbol = {}
for deal in deals:
if deal["entry"] == "in":
open_by_symbol[deal["symbol"]] = deal # assumes one position per symbol
elif deal["entry"] == "out":
trade = (open_by_symbol.pop(deal["symbol"]), deal)On a netting account it is fine, because one position per symbol is literally
the rule. On a hedging account with three overlapping GBPJPY longs, the dict
overwrites the first two entries, the first out pops the wrong entry, and you
get one trade with a phantom duration and two entries that never close.
The mirror-image bug exists too: code written on a hedging account that treats
every in deal as a new trade and every out as the end of one, keyed by
ticket. On netting, three in deals adding to the same position share one
position id, and the reconstruction produces three trades where there was one.
The fix is the same for both modes and it is boring: group by position id,
classify legs by entry, sum across the group. It does not need to know which
mode it's on, provided you handle out_by as an exit and decide what to do
with in_out.
Telling which mode an account is in
The terminal shows it: the account entry in the Navigator panel is labelled
Hedge or Netting, and it's in the account properties dialog. From MQL5 it's
AccountInfoInteger(ACCOUNT_MARGIN_MODE); from the Python MetaTrader5
package it's account_info().margin_mode, where retail netting is 0, exchange
is 1, and retail hedging is 2.
Over MetaKit's API the account object doesn't carry a margin mode field, so you can't read it directly. You can infer it from the data, and two of the three signals are conclusive:
- Two open positions on the same symbol in
/v1/accounts/{id}/positions: hedging, definitely. Netting can't produce that. - An
out_bydeal in the history: hedging, definitely. - An
in_outdeal in the history: netting, definitely. Hedging can't reverse.
def infer_margin_mode(deals: list[dict], positions: list[dict]) -> str:
entries = {d["entry"] for d in deals}
if "out_by" in entries:
return "hedging"
if "in_out" in entries:
return "netting"
symbols = [p["symbol"] for p in positions]
if len(symbols) != len(set(symbols)):
return "hedging"
return "unknown"unknown is a real answer. An account that has only ever opened one position
at a time looks identical in both modes, and that's fine, because such an
account also produces identical analytics in both modes. If you genuinely need
certainty, log into the terminal once and look. We'd rather say that than
invent a field.
What it does to a trade copier
Copying between accounts in the same mode is a ticket-to-ticket mapping. Across modes it's a translation problem, and the hard direction is hedging master to netting follower.
Take the sequence from the top. The master holds 5001 (buy 1.00) and 5002
(sell 0.40). The follower can only hold 0.60 long. Now the master closes 5002.
Nothing on the follower corresponds to it, so the copier has to work out that
"master closed a 0.40 short" means "follower buys 0.40" to get back to 1.00
long. If instead the master closes 5001, the follower must sell 1.00 against a
0.60 long: a reversal the master never made, leaving the follower 0.40 short
with an in_out deal and, on some brokers, a different swap regime than the
master's clean short.
Netting master to hedging follower is easier: a partial close on the master maps to a partial close on the follower's single mapped ticket, and a master reversal becomes a close plus an open. The follower's history looks tidier than the master's, which is a nice problem to have.
MetaKit's copier keys everything off the master's tickets rather than
symbols, and attributes copied P&L by follower ticket, so the follower's own
manual trades don't leak into copied_pnl. If your production pair is going to
be mixed-mode, run the same combination on demo first and watch the follower's
positions list, not just the copier's fidelity_pct, because the positions
list is where the translation shows. The copier post
explains how sizing and symbol mapping fit in.
How /performance handles both
/v1/accounts/{id}/performance reconstructs trades before computing anything,
using the mode-agnostic method above: deals are grouped by position id, in
deals are entries, out and out_by deals are exits, groups with no exit yet
are skipped as still open, entry and exit prices are volume-weighted across
partial fills, and net profit sums profit, commission, swap, and fee
across every deal in the group.
So the GBPJPY incident can't happen there: three overlapping hedged longs are
three groups with three position ids, and three netting adds to one position
are one group. The one deliberate simplification is that a netting reversal
stays in one group, so a position that flipped from long to short is reported
as a single trade with a volume-weighted entry. If you need those split, pull
/v1/accounts/{id}/deals and cut at in_out yourself. The full field list is
in the API reference, and the same content is
in llms.txt if you're building with an
agent.
Before shipping anything that reads MT5 history, connect one hedging demo and one netting demo, run the buy-1.00-sell-0.40 sequence on each, and diff the output. It takes ten minutes and it's the only test that catches this.