name: mt5-trading description: Use when the user wants to place, modify, cancel, or close a real trade on their MetaTrader 5 broker - including market orders, limit / stop / stop-limit orders, adjusting stop-loss or take-profit, cancelling pending orders, or closing a position in full or part. Triggers on phrases like "buy 0.1 lots of EURUSD", "go short BTCUSD", "close my XAUUSD position", "move my stop to break-even", "cancel that pending order", "open a long". Always use this skill before any tool call that mutates broker state - it carries the consent flow, idempotency rules, and broker-specific failure modes that you must follow to avoid losing the user money.
Trading on MetaTrader 5 - mutating tools
This skill covers the four mutating tools the mt5-mcp server exposes. Every successful call here is a real broker action. Even on a demo account, the trade reaches the broker's MT5 server and is irreversible from the agent's side - the only way to undo a wrong order is another order. Treat the consent flow described below as the line that separates "I told the user what I'm about to do" from "I just did something the user didn't sign off on".
Tool catalogue
place_order(symbol, side, type, volume, price?, stop_limit_price?, sl?, tp?, deviation=10, comment?, idempotency_key?, approval_confirmed=false, approval_request_id?) - open a new market or pending order. side is "buy" or "sell". type is "market", "limit", "stop", or "stop_limit". volume is in lots, as a string (e.g. "0.10").
modify_order(ticket, sl?, tp?, price?, expiration?, idempotency_key?, approval_confirmed=false, approval_request_id?) - adjust a position's SL/TP (positions only), or a pending order's price / expiration / SL / TP. When the consent gate is armed (auto_approve_notional > 0), widening or removing an existing SL/TP on a position requires approval and tightening auto-approves; with the gate off (the default) every modify auto-executes. Pass expiration only for pending orders, as ISO 8601 UTC.
cancel_order(ticket, idempotency_key?) - remove a pending order. Never gates for approval (cancelling reduces exposure).
close_position(ticket, volume?, idempotency_key?, approval_confirmed=false, approval_request_id?) - close a position in full (omit volume) or part (pass a smaller string volume).
All mutating tools return a result envelope on success: {retcode, deal, order, volume, price, comment, request_id?, request_echo}. Surface deal and price to the user - that's the executed fill.
The two-call approval flow
For place_order, modify_order (when widening), and close_position, the policy engine compares the trade's notional value against policy.auto_approve_notional. This gate is opt-in and OFF by default: it defaults to 0, so out of the box every mutating call auto-executes with no approval step. The operator arms it by setting auto_approve_notional > 0. When the gate is armed, an order whose notional is at or above the threshold returns an ApprovalPreview envelope on the first call instead of executing:
{
"approval_required": true,
"preview": {
"request_id": "01HQX...",
"expires_at": "2026-04-28T19:55:00+00:00",
"summary": "BUY 0.10 EURUSD @ market (~1085 USD)",
"action": "place_order", "symbol": "EURUSD",
"notional": "1085.12", "estimated_margin": "10.85",
"reference_quote": {"symbol": "EURUSD", "bid": "1.08510", "ask": "1.08515", "time": "..."},
"request_echo": { ...all the fields the agent originally sent... }
}
}
When you receive this, do not retry automatically. Show the user a short, plain-English version of the preview - direction, size, symbol, approximate notional, estimated margin - and ask them to confirm. Only after they say yes do you make the second call:
- Same tool, same
symbol/side/type/volume/ticket/price(everything that was inrequest_echo). - Add
approval_confirmed=true. - Add
approval_request_id=<the request_id from the preview>.
If you change any of those fields between preview and confirm, the engine returns INVALID_APPROVAL ("request body changed since preview"). That's by design - it stops a bait-and-switch where the agent shows the user one trade and submits another.
If approval_confirmed=true but no preview was ever issued, or the request_id doesn't exist (e.g., the server restarted, or it expired after approval_ttl_seconds - default 300s), you also get INVALID_APPROVAL. The right response is to drop the confirmation flag and start the flow over: present a fresh preview to the user and ask again.
Idempotency
Always send an idempotency_key on retries, and always make it a UUIDv4 (or any unique string the user gives you). The engine remembers the result of the first call keyed by request hash; any subsequent call with the same key and the same request body returns the cached result instead of placing a duplicate trade. Two important rules:
- The hash excludes
approval_confirmedandapproval_request_id. A preview-then-confirm pair counts as one logical request - the second call returns the cached approval-required envelope unless the user has confirmed. - Same key, different body ->
IDEMPOTENCY_DIVERGED. That's an agent bug. Use a fresh key for distinct requests; reuse a key only when retrying the same trade.
For a fresh trade, omitting idempotency_key is fine - the engine generates one. Only set it explicitly when you're retrying after a network blip, ambiguous error, or a deliberate "make sure this is the same trade" flow.
Pre-flight limits
The server enforces a few configurable local limits before a trade reaches the broker (policy.preflight.*). These are UX guardrails - they protect the user from agent slip-ups, not from the broker. The broker enforces real margin and leverage; these checks just refuse obviously too-large requests early. The most common refusal:
EXCEEDS_LOCAL_LIMITwithdetails.limit_name="notional_per_request"- the trade's notional is above the configured per-request cap. Tell the user the cap; let them decide whether to split the trade or raise the cap (config edit, server reload).
Error taxonomy
You'll see most of these from mutating tools. Treat the requires_human flag as authoritative - when it's true, stop and surface the issue to the user; don't retry without their input.
AUTO_TRADING_DISABLED(mt5 retcode 10027) - the MT5 toolbar's "AlgoTrading" button is red. Only the human can fix this: tell them to click that button so it turns green, then ask if they want to retry.MARKET_CLOSED(10018) - the symbol's session is closed. Either wait, or pick another symbol.REQUOTE(10004) - price moved between submission and execution. Retryable; same idempotency key, same body, the engine will resubmit.INVALID_PRICE(10015) - the price you sent is invalid for the order type (e.g., a stop above market for a buy stop). Recheck and resubmit with a fresh price.INVALID_VOLUME(10014) - volume doesn't match the symbol's lot step / min / max. Useget_symbolsto look up the right step.INSUFFICIENT_MARGIN(10019) - not enough free margin. Surface the user's current margin fromget_account_infoso they can decide.REJECTED_BY_SERVER(10006) - the broker said no, often without specifics. Don't retry blindly; tell the user.EXCEEDS_LOCAL_LIMIT- see above.INVALID_APPROVAL- preview/confirm mismatch or expired preview. Re-do the consent flow.INVALID_TICKET- ticket doesn't exist (closed, cancelled, never existed). Useget_positions/get_ordersto find the right ticket.TERMINAL_NOT_CONNECTED- the operator's MT5 client isn't connected. Tell them to log in, then retry.INTERNAL_ERROR- unexpected exception. Don't retry; surface to the user.
Demo account framing
If the user's account is a demo, the money is fake but the execution is real. The order goes to a live broker server, fills against live or simulated liquidity, opens a real position object with a ticket, and gets reported in audit.jsonl. Do not treat demo as "tests don't matter" - the main reason the human is using a demo terminal is to verify your behaviour before they switch to live.
Worked example: place_order round-trip
User: "Buy 0.10 lots of EURUSD at market."
- Quote first so the user has context:
get_quote(symbol="EURUSD")-> bid 1.08510, ask 1.08515. - First call:
place_order(symbol="EURUSD", side="buy", type="market", volume="0.10") - If the server returns
approval_required: true(only when the operator has armed the gate - the notional 1.0851 × 0.10 × 100,000 ≈ 10,851 USD is at or above the configuredauto_approve_notionalthreshold), present this to the user verbatim:"About to BUY 0.10 EURUSD at market - roughly 10,851 USD notional, ~10.85 USD margin at your leverage. Confirm?" Wait for an explicit yes. Don't infer consent from the user's earlier "buy 0.10 lots" - that's the original request, not a confirmation of the preview.
- Second call (only after explicit yes):
place_order(symbol="EURUSD", side="buy", type="market", volume="0.10", approval_confirmed=true, approval_request_id="<the request_id from preview>") - On success, the result envelope carries
deal,order,price,volume. Tell the user: "Filled at 1.08516 - 0.10 lots EURUSD long. Position ticket 12345678." - If
approval_requiredis absent or false on the first call (the gate is off - the default - or the trade is below the armed threshold), the result is the fill directly - same final report.
Workflow rules
- Always quote before opening. A
get_quote(orget_market_hours) beforeplace_ordercatches stale-symbol and market-closed cases without a wasted approval round. - Always read the position before modifying or closing. A
get_positions(symbol=...)confirms the ticket exists and surfaces its current SL/TP - useful context for the user before changes. - Surface fills, not retcodes. The user wants to know they're in at 1.08516, not that retcode 10009 (TRADE_DONE) came back. Translate.
- Don't loop on errors. If a trade fails with anything that isn't explicitly
retryable=true, stop and tell the user. Especially never retry anINSUFFICIENT_MARGINorINVALID_VOLUME- those need human input. - Cancel ≠ close.
cancel_orderremoves a pending order that hasn't filled;close_positioncloses a filled position. The user often blurs these - confirm which they mean if their phrasing is ambiguous.
See also
mt5-market-dataskill - covers the read-only side. Use those tools to gather context (quotes, positions, account state) before mutating.docs/tools.md(consent flow) andCONTRIBUTING.md(policy-engine invariants) for the full architectural picture.audit.jsonl(operator-side, default~/.local/share/mt5-mcp/audit.jsonl) records every mutating call. Useful for the user to verify what was done.