When to Use
Use this when a Python strategy sends market deals (TRADE_ACTION_DEAL) to a MetaTrader 5 terminal via the official MetaTrader5 package, and you need the order-submission path itself to be correct: building the MqlTradeRequest dictionary, validating volume and stop levels against the broker's own symbol metadata, choosing a filling mode the symbol actually permits, and turning the returned MqlTradeResult.retcode into a decision a bot can act on safely.
The core difficulty is that order_send() has no client-assigned order id. There is no MT5 equivalent of a clientOrderId, so a lost or ambiguous response cannot be resolved by resubmitting the same request — a resend is a genuinely new order. Everything in this skill is built around that constraint.
When NOT to Use
- You are not on Windows x86-64. The
MetaTrader5package on PyPI publisheswin_amd64wheels only, and it requires a running, logged-in terminal on the same host. Linux/macOS deployments need Wine or a broker-provided gateway — a decision that belongs inforex-broker-integration-oanda-mt5, not here. - You need pending orders or position management. This skill covers
TRADE_ACTION_DEALonly.TRADE_ACTION_PENDING(limit/stop entry),TRADE_ACTION_SLTP(modify stops),TRADE_ACTION_MODIFY,TRADE_ACTION_REMOVEandTRADE_ACTION_CLOSE_BYhave different required fields and different failure modes. - You are choosing pip/lot conventions, swap accounting, or terminal liveness monitoring. Those are
forex-broker-integration-oanda-mt5. - You want a generic idempotency layer. MT5 cannot provide one at the protocol level.
order-placement-idempotencycovers the general pattern; what applies here is reconciliation-before-retry, described below. - You are running a hedging-vs-netting-sensitive strategy without having checked the account mode. On a netting account an opposing deal reduces or reverses the existing position rather than opening a second one. This module submits the deal; it does not model the resulting position.
Prerequisites
- MT5 terminal installed, running and logged in on the same Windows host, with Algorithmic Trading enabled in the terminal — otherwise every deal returns
10027 TRADE_RETCODE_CLIENT_DISABLES_AT. MT5Config(login, password, server, path, max_slippage_points, magic_number, preferred_filling).magic_numbermust be positive and unique per strategy: it is the only tag available for reconciling an ambiguous submission.passwordis kept out of the dataclassrepr.- A terminal adapter satisfying
MT5TerminalAdapter(order_send,symbol_info). In production this wraps theMetaTrader5module; this repository's module never imports it, so the logic stays testable off-Windows. - Per-symbol trading conditions from
symbol_info()—volume_min,volume_max,volume_step,volume_limit,digits,point,trade_stops_level,filling_mode. None of these has a safe default; the engine refuses to trade a symbol it cannot read them for.
Workflow
-
Read the symbol's trading conditions before validating anything.
symbol_info()returnsNonefor a symbol the terminal does not know — including a correct symbol under the wrong broker suffix (EURUSD.pro,EURUSDm). TreatNoneas a hard stop (MT5_SYMBOL_UNAVAILABLE) and confirm the symbol is selected in Market Watch (symbol_select), not as a reason to guess defaults.- Never hard-code
0.01as the lot step. It is 0.001 on micro accounts and 1.0 on many index CFDs._validate_volumechecksvolume_min,volume_max,volume_stepandvolume_limitas the broker publishes them.
-
Reject an unrecognised side before serialising.
order_typemust be exactlyBUYorSELL(case-insensitive). Anything else —LONG,BUY_LIMIT, a typo — is rejected asMT5_INVALID_ORDER_TYPE. A side dispatcher that falls through to anelsebranch turns an unrecognised string into a live order in the opposite direction.
-
Validate stops on both side and distance.
- For a Buy, SL must be strictly below and TP strictly above the entry; for a Sell, the reverse. Both levels are checked — a mis-signed TP is as harmful as a mis-signed SL.
0.0(orNone) means "no level set" and is passed through untouched. - Distance is checked against
SYMBOL_TRADE_STOPS_LEVEL, in points, using the prices as they will actually be serialised. Atrade_stops_levelof 0 does not mean "any distance is allowed" — many brokers apply a floating, spread-derived level that the static property does not express. Passing this check makes10016unlikely, not impossible.
- For a Buy, SL must be strictly below and TP strictly above the entry; for a Sell, the reverse. Both levels are checked — a mis-signed TP is as harmful as a mis-signed SL.
-
Derive
type_fillingfrom the symbol, do not assume it.SYMBOL_FILLING_MODEis a bitmask (FOK=1,IOC=2,BOC=4) whileENUM_ORDER_TYPE_FILLINGis a plain enum (FOK=0,IOC=1). They are different numberings; feeding the mask straight intotype_fillingis the usual cause of10030/ "Unsupported filling mode".- Only FOK and IOC are candidates for a market deal — BOC applies to limit/stop-limit orders and RETURN is disabled under Market Execution. If the symbol permits neither, the deal is refused locally rather than sent to be rejected.
- Under Market Execution, MQL5 requires five fields:
action,symbol,volume,type,type_filling.
-
Submit exactly once, then classify the retcode — never retry inside the send path.
10009 DONE→ filled. Read the fill fromMqlTradeResult, not from your own request:result.volumeis "Deal volume, confirmed by broker" andresult.priceis the confirmed deal price.10010 DONE_PARTIAL→ a position is open. Report it as executed with the confirmed volume and both tickets (orderanddeal). Any follow-up must be sized from the shortfall; resending the original volume doubles the intended exposure.10008 PLACED→ accepted, not yet filled. Keep the ticket, claim no exposure.10004 / 10020 / 10021 / 10024→ transient and nothing filled. Safe to re-quote and resend under a bounded attempt cap.10011 / 10012 / 10028 / 10031, an adapter exception, ororder_send()returningNone→ outcome unknown.requires_reconciliation=True.- Anything else, including an unrecognised code, is terminal. An unknown server response is never a licence to resend a non-idempotent order.
-
Reconcile before any resend of an ambiguous submission.
- Query
history_deals_get(...)/positions_get(...)and filter on yourmagic. Deals carrymagic,order,position_id,volumeandprice, which is what makes magic-number reconciliation possible at all. - Do not use
commentas a substitute client order id: MT5 order comments are short and the trade server may truncate or overwrite them.
- Query
Full procedure: see
references/workflows.md. Standards reference and sources: seereferences/standards.md. Printable pre-flight checklist: seeassets/checklist.md.
Common Pitfalls
- Treating a non-
10009retcode as "nothing happened."10010 DONE_PARTIALmeans volume traded. Code that branches onretcode == 10009and resends everything else will double up on every partial fill. - Resending after a timeout or a
Noneresult.order_send()returningNone, raising, or answering10012/10031tells you the client lost the answer, not that the server rejected the order. MQL5 states plainly that "successful sending of a request does not entail that the requested trading operation will be executed successfully." Reconcile on the magic number first. - Calling
.retcode/.get()on the result without aNonecheck.MetaTrader5.order_send()returnsNonewhen the terminal cannot process the call, so the obvious accessor raisesAttributeErrorin precisely the failure case it exists to handle. - Hard-coding
0.01as the minimum lot and the lot step. Wrong for micro accounts (0.001) and for index/metal CFDs whose step can be 0.1 or 1.0. Readvolume_min/volume_stepfromsymbol_info(). - Validating the lot step with
round(v * 100) % 1 != 0.round()returns anint, soint % 1is always0and the check never fires —0.015lots sails through to a10014rejection at the server. - Passing
SYMBOL_FILLING_MODEstraight intotype_filling. The mask says FOK is bit1; the enum says FOK is0. The mismatch surfaces as10030with the broker comment "Unsupported filling mode", not as10013. - Validating only the stop loss. A Buy whose take profit sits below the entry is just as invalid, and passes silently if only SL is checked.
- Reading
trade_stops_level == 0as "no minimum distance." Brokers commonly apply a floating, spread-derived level that the static property reports as zero. - Comparing a stop distance against
trade_stops_levelin raw floats.1.08500 - 1.08480is0.00019999999999997797, so a stop placed exactly at a 20-point limit measures as 19.999… points and is falsely rejected. Compare with a sub-point tolerance. - Sending a stale price as the market price.
TRADE_ACTION_DEALexpects the current quote — Ask for a Buy, Bid for a Sell. A price from a closed bar produces requotes (10004) or10015, anddeviationonly widens the tolerance, it does not fix the reference. - Running with
magic = 0. Indistinguishable from a manually placed trade, which makes post-timeout reconciliation impossible to scope to the strategy. - A "simulation" default that returns
TRADE_RETCODE_DONE. An engine that fabricates a success when no terminal is attached is indistinguishable from a live fill to everything downstream. Require an adapter, or an explicit dry run that reportsis_executed=False. - Forgetting the terminal's Algorithmic Trading toggle. Every deal comes back
10027, with nothing wrong in the request.
Verification
- Run the unit suite:
python -m unittest discover -s skills/mt5-python-bridge-for-forex-bots/scripts— all tests must pass. - Construct
MT5PythonBridgeEnginewith neither an adapter nordry_run=Trueand confirm it raisesMT5BridgeErrorrather than producing a fabricated fill. - Submit
0.015lots against avolume_stepof0.01and confirmMT5_INVALID_VOLUMEwithorder_sendnever called; submit0.001against a micro-account spec (volume_step=0.001) and confirm it is accepted. - Submit a Buy with TP below entry and a Sell with TP above entry; confirm both are rejected as
MT5_INVALID_STOPSbefore submission. - Submit
order_type="LONG"and confirmMT5_INVALID_ORDER_TYPEwith an emptymql_trade_request— not a serialisedORDER_TYPE_SELL. - With
filling_mode = SYMBOL_FILLING_FOKonly, confirm the serialisedtype_fillingisORDER_FILLING_FOK(0); withSYMBOL_FILLING_BOConly, confirmMT5_INVALID_FILLINGand no submission. - Place a stop exactly
trade_stops_levelpoints away and confirm it is accepted; one point closer and confirm it is rejected. - Return
retcode=10010withvolumebelow the request and confirmis_executed=True,status="MT5_ORDER_PARTIALLY_FILLED", and that bothorder_idanddeal_idsurvive. - Make the adapter return
None, then raise, then answer10012; confirm all three yieldMT5_EXECUTION_AMBIGUOUSwithrequires_reconciliation=True, and thatorder_sendwas called exactly once in each case. - Return
retcode=10010with novolumefield and confirm the result isMT5_EXECUTION_AMBIGUOUS, not a fill of zero lots. - Pass a
symbol_specwhosesymboldiffers from the order's and confirmMT5_SYMBOL_MISMATCHwith nothing submitted. - Confirm
repr(MT5Config(...))does not contain the password.