Files
mt-data-mcp/docs/TRADING_SAFETY.md

23 KiB

Trading safety runbook

Audience: User

If you only skim one trading doc, make it this one. The trade_* tools send real requests to the MT5 account currently logged into the terminal. This runbook covers previewing orders, validation, account guardrails, and broker quirks for trade_place, trade_modify, and trade_close.

These tools default to preview mode. dry_run defaults to true. A request reaches MT5 only when you explicitly pass --dry-run false in the CLI or dry_run=false through Python/MCP. The Web API Tools path additionally requires "confirm": true only when arguments set "dry_run": false (an omitted dry_run is still a preview and does not need confirm). Use a demo account until you trust your setup — mtdata has no separate paper-trading mode. trade_idea_compose is stricter: it is preview-only and has no live-send flag.

Dense terms: Dry-run · Trade guardrails · Slippage · Lot size · TP/SL

Related: Trade ideas · Risk analytics · Env vars (guardrails) · CLI · Sample trade · Glossary


Golden rules

  1. Preview first — --dry-run true until the request looks right.
  2. Demo while learning — no simulated mode except an MT5 demo.
  3. Enable guardrails on any account that can place orders (Account guardrails).
  4. Exact tickets for modify/close; treat --close-all as nuclear.
  5. Protective levels — market and pending orders require SL and TP by default (--require-sl-tp). If a filled market order cannot attach those levels, mtdata always tries to close the unprotected position; that fail-safe is not a CLI flag.
  6. Confirm the account — compact trade_account_info does not include the raw login. Use account_context_id (a hash of login + server) to tell two logins on the same broker apart. Full detail still includes login when you need the exact number. Compact trade_risk_analyze follows the same rule.

MT5 tickets and magic numbers are unsigned 64-bit identifiers. Ticket inputs accept 1..18446744073709551615; magic accepts 0..18446744073709551615, where zero is a real manual/untagged strategy scope, not an omitted filter. Decimal input is parsed without floating-point conversion. When an identifier exceeds JavaScript's exact integer range, JSON responses also include a sibling such as ticket_exact or magic_exact as a canonical decimal string and set identifier_encoding=decimal_string_in_exact_fields.


Preview with --dry-run

A dry run routes and validates the request without sending it to MT5. Successful trade_place, trade_modify, and trade_close previews expose dry_run=true, preview_ok=true, and would_send_order=false. Modify and close previews also retain actionability=preview_only and a preview_scope_summary that states no request was sent, including in compact TOON output. The trade_place preview returns these additional markers you can assert on:

{
  "dry_run": true,
  "no_action": true,
  "would_send_order": false,
  "dry_run_simulated": true,
  "preview_ok": true,
  "validation_passed": true,
  "validation_scope": "local_preview_plus_estimates",
  "preview_checks_performed": [ /* checks actually completed */ ],
  "checks_not_performed": [ /* for example, margin_estimate when unavailable */ ],
  "broker_validation_not_performed": [ /* broker acceptance/enforcement, margin reservation, fillability, SL/TP attachment */ ],
  "guardrails_preview": { /* which guardrails would apply */ }
}

Eligible previews return success=true and preview_ok=true. A preview that is not eligible for live submission because of local safety failures returns success=false, error_code=preview_blocked, and preview_ok=false while retaining the preview body and its actionable blockers. This includes missing required SL/TP, closed-market or stale-quote checks, and other local safety failures. Ticketless bulk trade_close dry runs are different: when selection and local validation succeed they return success=true and preview_ok=true with authorization_status=required, required_confirmation, and validation.live_submission_eligible=false. Confirmation is enforced only for --dry-run false. The CLI prints blocked safety previews and exits 1; an eligible preview, including an unconfirmed bulk dry run, exits 0. Compact output always retains these gate fields and the broker-validation limitations. Its guardrails_preview summary retains enabled, blocked, ignored_for_demo, would_block_live, live_projection, and checks_not_performed; standard/full detail includes the complete guardrail diagnostics. When demo enforcement is disabled, blocked remains the decision for the demo account while live_projection.blocked and would_block_live show what the same request would do on a live account. A projected live block makes the dry-run preview ineligible (preview_ok=false).

Trade previews and live submissions report requested_comment and applied_comment for the outbound order tag. Live broker status text from MqlTradeResult.comment is broker_message; comment keeps the applied tag in both dry-run and live responses. MT5-bound comments use a conservative ASCII subset and are capped at 31 characters for place and cancel requests, or 24 characters for position-close requests. trade_modify cannot retag an existing ticket. When a supplied comment changes, the preview includes structured sanitization or truncation metadata and a warning. For target=all_exposure, comment_previews shows the separate close and pending-cancel forms because their limits differ.

What a dry run does check: required fields, order-type validity, market-vs-pending routing, an indicative margin estimate when MT5 exposes one, and a guardrails preview. For pending orders, margin_required_when_filled is calculated with the corresponding active BUY/SELL action at the requested entry price; margin_estimate_basis records that assumption.

What a dry run cannot check (only a live send confirms these): final broker acceptance, live price-distance/stops rules, the final margin reservation and funds decision, fillability, and SL/TP attachment after a market fill. Treat a clean preview as necessary, not sufficient.


trade_place

Requires symbol, volume, and order_type.

Flag Default Notes
symbol — Broker symbol
--volume — Lots (validated against broker min/max/step)
--order-type — See Order types
--price — Entry for pending orders; omit for market orders
--stop-loss — Stop-loss price
--take-profit — Take-profit price
--deviation 20 Max slippage in points (market orders)
--require-sl-tp true Require both SL and TP on market and pending orders
--expiration — Future pending-order expiry (dateparser or positive UTC epoch seconds); literal GTC means no expiry
--magic MTDATA_ORDER_MAGIC Strategy identifier stamped on the order
--comment — Free-text order comment
--idempotency-key — Durable dedupe shared across processes/restarts (24-hour default retention)
--dry-run true Set false explicitly for live execution
--detail compact Use full for execution diagnostics

If required SL/TP protection cannot be attached after a market fill, mtdata always attempts to close the unprotected position. This fail-safe is not optional.

Idempotency outcomes are stored in MTDATA_TRADE_IDEMPOTENCY_DB. If a process stops after reserving a key but before recording the broker outcome, retries for that key fail closed. Reconcile the order or modification in MT5 before removing the unresolved database row; never clear it merely to make a retry proceed.

# Preview a market buy with protective levels
mtdata-cli trade_place EURUSD --volume 0.10 --order-type BUY --stop-loss 1.0850 --take-profit 1.0950 --dry-run true

# Go live (only on the intended account)
mtdata-cli trade_place EURUSD --volume 0.10 --order-type BUY --stop-loss 1.0850 --take-profit 1.0950 --dry-run false

For a synchronously acquired send-path tick, mtdata tolerates a broker clock up to 30 seconds ahead of the workstation clock by evaluating freshness on that bounded broker reference. The preview discloses clock_reconciled and local_clock_lag_seconds. Larger leads, stale ticks, and invalid two-sided quotes remain hard blockers.

Order types

order_type accepts these canonical strings (case-insensitive; - or space becomes _):

BUY, SELL, BUY_LIMIT, BUY_STOP, BUY_STOP_LIMIT, SELL_LIMIT, SELL_STOP, SELL_STOP_LIMIT

MT5 numeric constants and ORDER_TYPE_* names are rejected as input — they only appear when reading existing orders/positions. Market orders use BUY/SELL (no --price). Every pending order requires --price. For a stop-limit order, --price is the stop trigger and --stop-limit-price is the limit leg activated after the trigger. A buy stop-limit's limit price must be at or below its trigger; a sell stop-limit's limit price must be at or above it.

# Trigger above the ask, then activate a buy limit at or below that trigger
mtdata-cli trade_place EURUSD --volume 0.10 --order-type BUY_STOP_LIMIT --price 1.1050 --stop-limit-price 1.1045 --stop-loss 1.1000 --take-profit 1.1150 --dry-run true

trade_modify

Modifies an existing order/position by ticket.

At least one of price, stop_limit_price, stop_loss, take_profit, clear_stop_loss, clear_take_profit, or expiration must be supplied. An explicit value that already matches the live object is a successful idempotent no-change request; omitting every modification field is an error. MT5 does not support changing an existing ticket's comment; set the comment at place or close time. Passing 0 for --stop-loss or --take-profit is rejected; use --clear-stop-loss true or --clear-take-profit true to remove protection.

Flag Default Notes
ticket — Required
--price — New pending-order price
--stop-limit-price — New limit leg for an existing stop-limit order
--stop-loss — New stop-loss price. Zero is rejected
--take-profit — New take-profit price. Zero is rejected
--clear-stop-loss false Explicitly remove the stop-loss
--clear-take-profit false Explicitly remove the take-profit
--expiration — New future pending-order expiry, or literal GTC
--idempotency-key — Durable dedupe shared across processes/restarts
--dry-run true Preview by default; set false explicitly for a live modification
mtdata-cli trade_get_open --json
mtdata-cli trade_modify --ticket 123456789 --stop-loss 1.0860 --take-profit 1.0980 --dry-run true

Guardrails apply to trade_modify only for pending-order changes and SL changes that increase risk; risk-reducing changes stay allowed. Risk-increasing modifications still apply symbol allowlist, blocklist, and volume-map rules. Pending orders with no stop-loss still apply the kill switch and symbol rules on price or expiration changes.


trade_close

trade_close acts on one explicit object class. It closes positions by default, cancels pending orders only with --target pending, and flattens both classes with --target all_exposure. There is no automatic position-to-order fallback.

Flag Default Notes
--ticket — Act on one ticket in the selected target class
--target positions positions, pending, or all_exposure (bulk scopes only)
--volume — Partial-close size (validated against broker step)
--symbol — Restrict closes to a symbol
--side — Restrict open positions to BUY/LONG or SELL/SHORT; pending orders are unaffected
--magic — Restrict closes to a magic number
--close-all false Select the whole account when ticket, symbol, side, and magic are omitted
--confirm-close-all false Required for any ticketless live bulk operation
--pnl-filter all Close all matches, only winners (profit), or only losers (loss)
--close-priority — loss_first, profit_first, or largest_first; largest uses broker tick economics to compare approximate account-currency exposure, with lot size only as a fallback
--deviation 20 Max slippage in points
--dry-run true Preview by default; set false explicitly for a live close
# Preview a partial close of one ticket
mtdata-cli trade_close --ticket 123456789 --volume 0.05 --dry-run true

# Cancel one pending order; default target=positions would not cancel it
mtdata-cli trade_close --ticket 987654321 --target pending --dry-run false

# Close all positions account-wide
mtdata-cli trade_close --close-all --confirm-close-all true --dry-run false

# Close positions and cancel pending orders for one strategy
mtdata-cli trade_close --magic 3001 --target all_exposure --confirm-close-all true --dry-run false

# Preview closing only long EURUSD positions in a hedged book
mtdata-cli trade_close --symbol EURUSD --side BUY --dry-run true

For all_exposure, the response keeps closed_positions and cancelled_pending_orders as separate result legs and reports partial failures; one failed leg does not prevent the other from being attempted. There is no separate "confirm" token for trade_place/trade_modify; the extra --confirm-close-all gate applies to every ticketless live bulk close. Dry-run bulk previews can enumerate matching exposure without the flag. They remain successful previews (success=true, preview_ok=true) and report authorization_status=required, required_confirmation="--confirm-close-all true", and validation.live_submission_eligible=false. Add the confirmation only when you intend --dry-run false, or include it on a preview if you want to verify that the same request is locally eligible to go live.


Account guardrails

Guardrails are optional pre-trade controls that reject risky orders before they reach MT5. They are evaluated when MTDATA_TRADE_GUARDRAILS_ENABLED=1 or whenever any individual guardrail variable is set. They apply on demo accounts by default; set MTDATA_TRADE_GUARDRAILS_IGNORE_ON_DEMO=true only when you intentionally want demo to skip those caps.

Guardrails span several layers:

Layer Rejects when… Key variables
Kill switch Trading is disabled MTDATA_TRADING_ENABLED=0
Symbol rules Symbol is blocked or not allowlisted MTDATA_TRADE_ALLOWED_SYMBOLS, MTDATA_TRADE_BLOCKED_SYMBOLS
Volume caps Order volume exceeds a global or per-symbol cap MTDATA_TRADE_MAX_VOLUME, MTDATA_TRADE_MAX_VOLUME_BY_SYMBOL
Safety policy Missing SL, excessive deviation, or non-reducing order MTDATA_TRADE_SAFETY_REQUIRE_STOP_LOSS, MTDATA_TRADE_SAFETY_MAX_DEVIATION, MTDATA_TRADE_SAFETY_REDUCE_ONLY
Account risk Margin too low, floating loss or exposure too high MTDATA_TRADE_MIN_MARGIN_LEVEL_PCT, MTDATA_TRADE_MAX_FLOATING_LOSS, MTDATA_TRADE_MAX_TOTAL_EXPOSURE_LOTS
Wallet risk Post-trade risk exceeds a % of equity/balance/free margin MTDATA_TRADE_MAX_RISK_PCT_OF_EQUITY, MTDATA_TRADE_MAX_RISK_PCT_OF_BALANCE, MTDATA_TRADE_MAX_RISK_PCT_OF_FREE_MARGIN

Note: A per-symbol volume map (MTDATA_TRADE_MAX_VOLUME_BY_SYMBOL) also acts as an allowlist — a symbol missing from the map is rejected. Exposure and wallet-risk caps include both open positions and pending orders. Wallet-risk caps fail closed when any position or pending order lacks a quantifiable stop-loss or valid broker tick metadata.

Reduce-only checks the current open positions before allowing an opposite-side order no larger than the net position. On hedging accounts, trade_place cannot guarantee a reduction, so use trade_close with a position ticket instead.

See ENV_VARS.md § Trade Guardrails for every variable, defaults, formats, and a ready-to-copy .env block. A dry run returns a guardrails_preview so you can confirm which rules would fire before going live. On a demo account where enforcement is ignored, inspect would_block_live and live_projection; these evaluate the configured rules without changing demo enforcement.

Live market and pending placements are serialized within one mtdata process so the portfolio snapshot, guardrail decision, and broker submission are atomic against concurrent tool calls. Separate mtdata processes connected to the same account do not share that lock; use a single live-trade executor per MT5 account when exposure or wallet-risk caps must be enforced across clients.


Pre-trade validation & broker behavior

Even with guardrails off, mtdata validates each order against broker constraints before submission:

  • Volume — must be numeric, positive, finite, within the symbol's volume_min/volume_max, and aligned to volume_step (misaligned sizes are rejected with an aligned suggestion).
  • Pending price side — buy_limit must sit below ask, buy_stop above ask, sell_limit above bid, sell_stop below bid.
  • Stops distance — SL/TP and pending prices must respect the broker's minimum stops/freeze level.
  • Symbol readiness — the symbol must be selectable and have live bid/ask.
  • Filling mode — mtdata resolves a broker-compatible filling mode for market fills and closes.
  • Margin — a market-order preview estimates required margin.

Because these depend on live broker state, they are only fully enforced on a real send — another reason to keep position sizes small when first going live.


Live-trade checklist

  1. Confirm the account: mtdata-cli trade_account_info --json (verify it's the intended demo/live account).
  2. Snapshot context: mtdata-cli trade_session_context EURUSD --json.
  3. Configure guardrails in .env (allowlist, volume caps, risk %). Restart mtdata.
  4. Preview: run the order with --dry-run true; inspect guardrails_preview, preview_checks_performed, checks_not_performed, and broker_validation_not_performed. A margin estimate appears under performed checks only when MT5 returned a finite estimate.
  5. Go live with a small size and --dry-run false.
  6. Verify: mtdata-cli trade_get_open --json, then manage with trade_modify / trade_close.

Account and journal (read-only)

Look at the account without placing an order. None of these send trade_place / trade_modify / trade_close.

Question Tool
Which account is logged in? Balance, equity, margin? trade_account_info
What is open right now? trade_get_open
What is waiting as a pending order? trade_get_pending
What filled recently? trade_history
How did closed trades perform? trade_journal_analyze
Session + quote + exposure in one bundle trade_session_context

trade_account_info compact output keeps the two distinct gates without their derivable aliases. execution_ready is terminal/account enablement only (execution_ready_scope=account_and_terminal_enablement); it does not include margin policy. The canonical new_exposure_allowed combines broker permission, non-critical margin, and strict execution readiness. Use it before adding risk. Full detail retains the component aliases and basis diagnostics. trade_session_context still adds symbol/session checks on top of that account gate. Its execution_preconditions_allow_open flag is that scoped execution gate; it does not imply portfolio-risk approval (trade_ready.portfolio_risk_assessed stays false until a risk tool is run). Compact output reports duplicated gates inside trade_ready only; full detail retains their root aliases and account type booleans. Compact execution-quality and journal results keep metrics, coverage gaps, low-sample warnings, and other exceptions, but omit static unit legends and repeated sample prose.

mtdata-cli trade_account_info --json
mtdata-cli trade_get_open --json
mtdata-cli trade_get_pending --json
mtdata-cli trade_history --history-kind deals --minutes-back 10080 --json
mtdata-cli trade_journal_analyze --minutes-back 10080 --json
mtdata-cli trade_journal_analyze --magic 3001 --minutes-back 10080 --json
mtdata-cli trade_session_context EURUSD --json

trade_history and trade_journal_analyze default to the last 7 days when you omit a window. trade_history returns 20 rows by default (max 500 per page). If more rows exist, pass its opaque pagination.next_cursor back as --cursor with the same filters and time controls. Cursor pages retain the first page's exact UTC bounds and expire after one hour, preventing a moving relative window from skipping records. On accounts shared by multiple strategies, pass --magic to either command so history pagination and journal metrics are scoped to one MT5 strategy identifier. For deal history, mtdata attributes every position leg to the earliest opening deal available in the requested window. A manual exit with deal magic 0 therefore stays with the strategy that opened the position. Each row keeps the broker's raw deal_magic, the resolved attributed_magic, and attribution_method. If the opening deal falls outside the window, attribution falls back to that row's own deal magic; expand the window when you need complete strategy reconciliation.

History vs journal: history is the raw deal/order tape. The journal summarizes exit deals (wins, losses, averages) for review. It matches entry fills by position ticket and allocates their commission and fees by closed volume. Check entry_cost_coverage: an entry outside the requested history window leaves that exit on the explicitly reported exit-deal-only PnL basis. History deal rows preserve MT5's profit, commission, swap, and fee components and also report net_pnl as their sum. profit_basis makes clear that the broker profit value excludes those separately reported cost fields.

Do not paste journal averages into Kelly sizing. trade_journal_analyze reports profit and loss in account currency per exit, including matched entry costs where entry_cost_coverage permits. Kelly needs a win rate and average win/loss that are normalized to a consistent stake (for example R-multiples). Build those inputs on purpose; see TRADING_RISK.md.

Dense terms: Balance / equity / free margin · Margin · Magic number


See Also