---
slug: "mt5-httpapi"
source_type: "clawhub"
source_url: "https://clawhub.ai/skills/mt5-httpapi"
repo: ""
source_file: "description"
---
---
name: mt5-httpapi
description: HTTP client for a user-deployed mt5-httpapi MetaTrader 5 bridge. Use ONLY when the user has explicitly installed and configured mt5-httpapi AND provided MT5_API_URL. Read endpoints (account, symbols, rates, ticks, server-side technical-analysis enrichment via the wickworks sidecar, history, backtest report fetching) are safe to invoke. Trade-mutating endpoints on /orders and /positions require explicit per-action confirmation showing symbol, side, volume, and SL/TP; terminal shutdown/restart also require explicit confirmation naming the target terminal and operation. Never invoke mutations on inferred intent. Do not use this skill for generic market-data, charting, or trading questions where the user hasn't named mt5-httpapi.
compatibility: Requires curl and a user-deployed mt5-httpapi instance. MT5_API_URL env var must be set by the user. MT5_API_TOKEN is required whenever the server has auth configured; the agent must obtain it from MT5_API_TOKEN env var OR by asking the user — never by reading repository config files autonomously.
metadata:
  author: psyb0t
  homepage: https://github.com/psyb0t/mt5-httpapi
  openclaw:
    emoji: "📈"
    primaryEnv: MT5_API_URL
    requires:
      bins: [curl]
permissions:
  network: "outbound HTTP to the user-provided MT5_API_URL; requests can include brokerage account data and trade instructions, so use trusted self-hosted endpoints only."
  shell: "curl commands against MT5_API_URL; no other host commands are required for ordinary API use."
  filesystem: "no filesystem access for ordinary API use; the optional backtest runbook writes only user-requested local artifacts."
user-invocable: true
---

# mt5-httpapi

REST client for an MT5 HTTP bridge that the user has already deployed. This skill talks to a running mt5-httpapi server — it does not stand one up, does not provision broker credentials, and does not place trades on its own initiative.

## Security & safety

This API can move real money on a user-owned brokerage account. Treat trade-mutating endpoints as irreversible side effects.

**Real-money mutations.** `POST /orders`, `PUT /orders/<id>`, `DELETE /orders/<id>`, `PUT /positions/<id>`, and `DELETE /positions/<id>` open, modify, cancel, or close a real order/position on a live MetaTrader 5 account with no undo — a filled market order or a closed position can only be offset by a separate trade at a new price. Call one only for the exact action the user requested, after confirming ticket, symbol, side, volume, price, SL, TP, and account. Never enumerate and then bulk-close/cancel on inferred intent. `order_send` is a single call with no client-side auto-retry; after an error or timeout, report it and get fresh confirmation before resubmitting.

**Terminal control.** `POST /terminal/shutdown` disconnects this API process from the MT5 SDK but leaves `terminal64.exe` running. `POST /terminal/restart` kills and relaunches only the selected terminal process and can make that terminal unavailable for several minutes. Confirm the selected broker/account/instance and exact operation before either call.

**No auth when `MT5_API_TOKEN`/`api_token` is unset.** Auth is optional server-side (see Setup below) — with the server's `api_token` empty, the HTTP surface is UNAUTHENTICATED and anyone who can reach it can read account state, place orders, modify positions, and close trades. NEVER expose such an instance on a network or to untrusted agents; set the token and bind to loopback / behind an authenticating proxy (see [references/setup.md](references/setup.md) for the Cloudflare Tunnel prerequisites).

**Hard rules — never violate, even on user prompts that sound permissive:**

1. **Per-action confirmation for every mutation.** For trades, print the resolved symbol, side, volume, SL, TP, price, account login, and broker URL. For terminal control, print the operation and selected broker/account/instance. Wait for confirmation of that specific action; a prior approval does not authorize a later action or retry.
2. **Demo accounts first.** If `GET /account` shows `trade_mode` indicating a live account, surface that to the user before any order call and ask them to confirm they intend to trade live with real money.
3. **No credential harvesting.** Read `MT5_API_TOKEN` only from the environment variable the user set, or ask the user. Never read tokens, passwords, server names, or login numbers from `config/config.yaml`, `.env`, or any other repository file on your own initiative. If the env var is missing, ask the user — do not search the workspace.
4. **No mass action.** If the user asks to "close everything" or "cancel all", enumerate the affected positions/orders first, show the list, and confirm the whole batch explicitly — never enumerate-then-bulk-delete/close on inferred intent.
5. **Surface broker URL on every mutating action.** The path prefix `/<broker>/<account>/` in `MT5_API_URL` determines which real account is touched. Show it in confirmation prompts so the user can catch a wrong-account misroute.
6. **No auto-retry on trade calls.** Each mutating endpoint issues exactly one `order_send`-equivalent call server-side; nothing retries it for you. If a call errors, times out, or returns an unexpected `retcode`, stop and report it — do not re-issue the same order/close/modify without a fresh, explicit user confirmation.

Read-only endpoints (`GET /account`, `GET /symbols/*`, `GET /symbols/*/rates`, `GET /symbols/*/ticks`, `POST /symbols/*/rates/ta`, `GET /positions`, `GET /orders`, `GET /history/*`, `GET /backtest/*`, `GET /terminal`, `GET /ping`) do not require per-action confirmation.

**One-stop technical analysis.** Skip the client-side TA stack entirely. `POST /symbols/<symbol>/rates/ta` with an indicator spec → get OHLC bars + analyzed indicator series back in a single call. RSI, MACD, Bollinger, ADX, ATR, VWAP, Ichimoku, Order Blocks, Fair Value Gaps, BOS/CHoCH, swing structure, S/R levels, liquidity, session anchors, dozens more — all computed server-side by the [wickworks](https://github.com/psyb0t/docker-wickworks) sidecar that ships with this stack. Primitives only — wickworks returns raw indicator series and structural facts (e.g. "order block formed at this bar", "price closed past this swing"), never interpretive signals like divergences or crossover events. Build those in the consumer. See the [Technical Analysis](#technical-analysis) section.

For installation and setup, see [references/setup.md](references/setup.md).

## Setup

The user must have a running mt5-httpapi instance and must provide:

```bash
export MT5_API_URL=http://localhost:8888/<broker>/<account>
export MT5_API_TOKEN=<the-token-the-user-gives-you>
```

If `MT5_API_URL` is not set in the environment, ask the user — do not try to discover it from project files. Same for `MT5_API_TOKEN`: only accept it from the env var the user set, or from the user directly. Never read it from `config/config.yaml`, `.env`, or any other file in the workspace.

A single nginx sidecar (default `127.0.0.1:8888`) fronts every terminal. The path prefix `/<broker>/<account>/` (matching an entry in `config/config.yaml`'s `terminals` list) selects which terminal you talk to — set `MT5_API_URL` to the full base including that prefix. Override the host port with `API_HOST_PORT=...` at compose time.

**Verify:** `curl -H "Authorization: Bearer $MT5_API_TOKEN" $MT5_API_URL/ping` — should return `{"status": "ok", "mode": "live"}` for the normal API process. If not, the API isn't up yet (may still be initializing — it retries in the background).

Auth is optional server-side — if no token is configured on the server, all requests go through without a token. If a token is configured, all endpoints require `Authorization: Bearer <token>` and return `401` without it. From the agent's side, never assume the server is auth-disabled; always pass the token the user provided if there is one.

## MCP interface

Each terminal also speaks [Model Context Protocol](https://modelcontextprotocol.io) (streamable-HTTP) at `/mcp`, exposing the REST surface as **dedicated typed tools** whose names + params + descriptions are the agent's documentation. Families: market data (`list_symbols`, `get_symbol`, `get_tick`, `get_rates`, `get_ticks`, `get_rates_ta`), account/positions (`get_account`, `list_positions`, `get_position`, `modify_position`, `close_position`), orders (`list_orders`, `get_order`, `create_order`, `modify_order`, `cancel_order`), history/terminal (`get_history_orders`, `get_history_deals`, `get_terminal`, `terminal_control`), backtest (`get_backtest`), and `ping`. A generic `request` + `endpoints` catalog remain as a fallback for JSON-compatible routes without a dedicated tool; multipart submission at `POST /backtest` still uses REST directly. Every tool runs the exact same handler, auth, and MT5 locking as a real HTTP call.

Same bearer token as the REST API (`MT5_API_TOKEN`, empty = auth disabled). Connect either directly at `$MT5_API_URL/mcp/`, or via the [`@psyb0t/mt5-httpapi`](https://github.com/psyb0t/mt5-httpapi/tree/main/.agents/plugins/mt5-httpapi) OpenClaw plugin for stdio-only MCP clients. The order/position tools (`create_order`, `cancel_order`, `close_position`, …) are irreversible on a live account — see Security & safety above.

A per-terminal `/mcp` is bound to that one terminal, because an MCP session's tool catalog is fixed and has no per-call slot for naming an account. Pointing a client at the server ROOT instead (`http://<host>:8888/mcp/`, no broker/account prefix) gives the same tools with `broker` and `account` parameters, so one session reaches every terminal. Call `list_terminals` first to get the configured brokers/accounts and each process mode (`live` or `backtest`); this does not identify whether the brokerage account itself is live or demo, so check `GET /account` before trading. An unconfigured pair is refused with the valid list rather than routed to a plausible-looking wrong account. Both forms are available at once — the URL alone decides which one a client gets. **When acting through the unified endpoint, confirm the `broker`/`account` alongside the trade parameters: one wrong argument places a real order on a different real account.**

## How It Works

GET for reading, POST for creating, PUT for modifying, DELETE for closing/canceling. All bodies are JSON.

Application handler errors normally use this JSON shape; always inspect the
HTTP status and content type because framework-level failures can differ:

```json
{"error": "description of what went wrong"}
```

## Pre-Trade Checks (DO NOT SKIP)

Before placing any trade:

1. `GET /account` → `trade_allowed` must be `true`
2. `GET /symbols/SYMBOL` → `trade_mode` must be `4` (full trading)
3. `GET /symbols/SYMBOL` → check `trade_contract_size` — 1 lot of EURUSD = 100,000 EUR, not 1 EUR
4. `GET /terminal` → `connected` must be `true`

## API Reference

### Health

```bash
curl -H "Authorization: Bearer $MT5_API_TOKEN" $MT5_API_URL/ping
# {"status": "ok", "mode": "live"}

curl -H "Authorization: Bearer $MT5_API_TOKEN" $MT5_API_URL/error
# {"code": 1, "message": "Success"}
```

### Terminal

```bash
curl -H "Authorization: Bearer $MT5_API_TOKEN" $MT5_API_URL/terminal
curl -X POST -H "Authorization: Bearer $MT5_API_TOKEN" $MT5_API_URL/terminal/init
curl -X POST -H "Authorization: Bearer $MT5_API_TOKEN" $MT5_API_URL/terminal/shutdown
curl -X POST -H "Authorization: Bearer $MT5_API_TOKEN" $MT5_API_URL/terminal/restart
```

Key fields on `/terminal`: `connected`, `trade_allowed`, `build`, `company`, `broker_utc_offset_hours` (signed offset applied to all timestamps in/out — see Broker time below).

### Broker time vs real UTC

MT5 returns timestamps in the **broker server's wall-clock time** disguised as unix integers (RoboForex/FTMO = UTC+3, TeleTrade = UTC+2, etc.). The API normalizes this when `utc_offset` is set per terminal in `config/config.yaml`:

```yaml
terminals:
  - broker: roboforex
    account: main
    port: 6542
    utc_offset: "3h"
```

(`port` is container-internal — only nginx and the mt5 container talk to it.)

Forms accepted: `"3h"`, `"3h30m"`, `"-2h"`, `"90m"`, or a bare number (interpreted as hours).

When set, every outgoing time field (tick `time`, rate `time`, position/order/deal `time*` and `time_*_msc`) is real UTC unix, and every incoming `from`/`to` query param is interpreted as real UTC unix. If unset or `0`, raw broker timestamps pass through (legacy behavior). Check `GET /terminal` → `broker_utc_offset_hours` to confirm.

### Account

```bash
curl -H "Authorization: Bearer $MT5_API_TOKEN" $MT5_API_URL/account
```

```json
{
    "login": 12345678,
    "balance": 10000.0,
    "equity": 10000.0,
    "margin": 0.0,
    "margin_free": 10000.0,
    "margin_level": 0.0,
    "leverage": 500,
    "currency": "USD",
    "trade_allowed": true,
    "margin_so_call": 70.0,
    "margin_so_so": 20.0
}
```

### Symbols

```bash
curl -H "Authorization: Bearer $MT5_API_TOKEN" $MT5_API_URL/symbols
curl -H "Authorization: Bearer $MT5_API_TOKEN" "$MT5_API_URL/symbols?group=*USD*"
curl -H "Authorization: Bearer $MT5_API_TOKEN" $MT5_API_URL/symbols/EURUSD
curl -H "Authorization: Bearer $MT5_API_TOKEN" $MT5_API_URL/symbols/EURUSD/tick
curl -H "Authorization: Bearer $MT5_API_TOKEN" "$MT5_API_URL/symbols/EURUSD/rates?timeframe=H4&count=100"
curl -H "Authorization: Bearer $MT5_API_TOKEN" "$MT5_API_URL/symbols/EURUSD/rates?timeframe=H1&from=$(date +%s)&count=-100"
curl -H "Authorization: Bearer $MT5_API_TOKEN" "$MT5_API_URL/symbols/EURUSD/rates?timeframe=H1&from=$(date -d '1 day ago' +%s)&to=$(date +%s)"
curl -H "Authorization: Bearer $MT5_API_TOKEN" "$MT5_API_URL/symbols/EURUSD/ticks?count=100"
curl -H "Authorization: Bearer $MT5_API_TOKEN" "$MT5_API_URL/symbols/EURUSD/ticks?from=$(date -d '1 hour ago' +%s)&count=500"
curl -H "Authorization: Bearer $MT5_API_TOKEN" "$MT5_API_URL/symbols/EURUSD/ticks?from=$(date -d '1 hour ago' +%s)&to=$(date +%s)"
```

Timeframes: `M1` `M2` `M3` `M4` `M5` `M6` `M10` `M12` `M15` `M20` `M30` `H1` `H2` `H3` `H4` `H6` `H8` `H12` `D1` `W1` `MN1`

Rates/ticks query model — two modes, mutually exclusive:
- **Anchor + signed `count`:** `count=N` = N forward from `from`, `count=-N` = `\|N\|` ending at `from`, `count=0` = empty. Omit `from` to anchor at now.
- **Range (`from` + `to`):** all bars/ticks in the window, no count cap beyond `terminal_info().maxbars`. `to` requires `from` and rejects `count` (returns 400).

`from` and `to` accept three formats (all real UTC): unix seconds (`1700000000`), full datetime `YYYY_MM_DD_HH_MM_SS` (`2024_01_15_14_30_00`), or date-only `YYYY_MM_DD` (midnight UTC).

Capped at `terminal_info().maxbars` rows per request (default 100k — see `GET /terminal`). Symbols auto-select into MarketWatch on first access. Responses are gzipped if the client requests it (`curl --compressed`).

Tick `flags` param: `ALL` (default), `INFO` (bid/ask only — ~10× smaller), `TRADE` (trades only).

Key symbol fields: `bid`, `ask`, `digits`, `point`, `trade_contract_size`, `trade_tick_value`, `trade_tick_size`, `volume_min`, `volume_max`, `volume_step`, `spread`, `swap_long`, `swap_short`, `trade_stops_level`, `trade_mode`.

### Technical Analysis

`POST /symbols/<symbol>/rates/ta` — same query params as `/rates` (timeframe, count, from, to), JSON body carries the [wickworks](https://github.com/psyb0t/docker-wickworks) indicator spec. Response is `{symbol, timeframe, bars, ta}` — OHLC bars *and* analyzed indicator series in one round-trip. No client-side TA library needed.

Full catalog with all indicator types, params, output shapes, and SMC primitives is documented at [github.com/psyb0t/docker-wickworks](https://github.com/psyb0t/docker-wickworks#available-indicators).

```bash
# RSI + MACD + Bollinger Bands on the last 200 H1 bars; tail TA results to last 50.
curl -X POST -H "Authorization: Bearer $MT5_API_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "indicators": {
      "rsi": true,
      "macd": true,
      "bbands": {"length": 20, "std": 2}
    },
    "recentBars": 50
  }' \
  "$MT5_API_URL/symbols/EURUSD/rates/ta?timeframe=H1&count=200"
```

Response shape (keys under `ta` mirror the keys in your `indicators` object):

```json
{
  "symbol": "EURUSD",
  "timeframe": "H1",
  "bars": [ { "time": ..., "open": ..., "high": ..., "low": ..., "close": ..., "tick_volume": ... } ],
  "ta": {
    "rsi": [null, null, ..., 54.2, 56.1, 58.7],
    "macd": { "macd": [...], "signal": [...], "hist": [...] },
    "bbands": { "upper": [...], "middle": [...], "lower": [...] }
  }
}
```

**Indicator catalog** (request as `"name": true` for defaults, or `"name": {"length": 21, ...}` for tuning — params are flat on the object; add `"type": "<name>"` only when the output key differs from the indicator name, e.g. running two RSIs as `rsi14` + `rsi21`):

- **Moving averages**: `ema`, `sma`, `hma`, `wma`, `dema`, `tema`, `t3`, `kama`, `alma`, `linreg`, `jma`, `zlma`, `rma`, `fwma`, `swma`, `sinwma`, `trima`, `vwma`, `vwap` (session-anchored: `anchor` D/W/M + `sessionOffset`)
- **Momentum oscillators**: `rsi`, `mfi`, `willr`, `cci`, `roc`, `mom`, `uo`, `stoch`, `stochrsi`, `macd`, `tsi`, `trix`, `fisher`
- **Trend strength & cross-direction**: `adx`, `aroon`, `vortex`
- **Volatility**: `atr`, `natr`
- **Volume / money flow**: `obv`, `ad`, `cmf`, `adosc`, `kvo`
- **Bands & channels**: `bbands`, `kc`, `donchian`
- **Trailing trend signals**: `supertrend`, `psar`, `chandelierExit`, `ichimoku`
- **Compression**: `squeeze` (Bollinger inside Keltner — state machine `on`/`off`/`no` flags)
- **SMC primitives**: `orderBlocks`, `fvg` (alias `fvgs`), `bosChoch`, `swingLevels`, `srLevels`, `recentRange`, `liquidity`, `previousHighLow`, `sessions`, `retracements`
- **Analysis summaries** (last-bar snapshots, shared cached pass — free if you ask for any): `price`, `levels`, `momentum`, `volume`, `position`, `slope`

Wickworks is **primitives-only** as of v0.3.0 — no built-in divergence detection, no MA-cross events, no golden/death-cross tagging. Build those in the consumer over the raw series.

Each indicator declares its minimum bar requirement (e.g. `sma(200)` needs 200 bars). If you under-feed it, the server returns HTTP 502 wrapping a wickworks 400 with a per-indicator deficit list — so you see exactly which indicators need more bars, not just a generic "insufficient bars" error.

**Pre-flight tips:**
- Match `count` to your slowest indicator's lookback × 2 (e.g. `sma(200)` → fetch at least 400 bars for warmup + signal).
- `recentBars` is **inert** in wickworks v0.3.0 — accepted by the request schema but currently unused (reserved for future signal-tagged outputs). To get only the last N bars, set `count` accordingly on the rates query, or slice the response client-side.
- Combine with `/symbols/:symbol/rates` (raw OHLC) when you need TA for charting separate from trade-decision logic.

### Orders

> **Mutating endpoints — confirmation required.** Every `POST /orders`, `PUT /orders/<id>`, and `DELETE /orders/<id>` opens, modifies, or cancels a real order on the user's brokerage account. Before invoking any of them you MUST: (1) print the full resolved request (account login from `GET /account`, broker URL prefix, symbol, side, volume, price, SL, TP); (2) ask the user to confirm that specific action; (3) wait for an explicit yes. A prior confirmation does not carry over to a new action.

```bash
# Place market order — only after explicit per-action confirmation from the user.
curl -X POST -H "Authorization: Bearer $MT5_API_TOKEN" $MT5_API_URL/orders \
  -H 'Content-Type: application/json' \
  -d '{"symbol": "EURUSD", "type": "BUY", "volume": 0.1, "sl": 1.08, "tp": 1.10}'

# List pending orders
curl -H "Authorization: Bearer $MT5_API_TOKEN" $MT5_API_URL/orders
curl -H "Authorization: Bearer $MT5_API_TOKEN" "$MT5_API_URL/orders?symbol=EURUSD"
curl -H "Authorization: Bearer $MT5_API_TOKEN" $MT5_API_URL/orders/42094812

# Modify pending order
curl -X PUT -H "Authorization: Bearer $MT5_API_TOKEN" $MT5_API_URL/orders/42094812 \
  -H 'Content-Type: application/json' \
  -d '{"price": 1.09, "sl": 1.07, "tp": 1.11}'

# Cancel pending order
curl -X DELETE -H "Authorization: Bearer $MT5_API_TOKEN" $MT5_API_URL/orders/42094812
```

Order types: `BUY`, `SELL`, `BUY_LIMIT`, `SELL_LIMIT`, `BUY_STOP`, `SELL_STOP`, `BUY_STOP_LIMIT`, `SELL_STOP_LIMIT`

Fill policies: `FOK`, `IOC` (default), `RETURN`

Expiration: `GTC` (default), `DAY`, `SPECIFIED`, `SPECIFIED_DAY`

Required fields: `symbol`, `type`, `volume`. `price` auto-fills for market orders.

Trade result:

```json
{
    "retcode": 10009,
    "deal": 40536203,
    "order": 42094820,
    "volume": 0.1,
    "price": 1.0950,
    "comment": "Request executed"
}
```

`retcode` 10009 = success. Anything else = something went wrong.

### Positions

> **Mutating endpoints — confirmation required.** `PUT /positions/<id>` changes the SL/TP of a live position, and `DELETE /positions/<id>` closes it (full or partial). Both move real money. Per-action confirmation rule above applies — print symbol, ticket, current price, the change being made, and wait for explicit user yes.

```bash
curl -H "Authorization: Bearer $MT5_API_TOKEN" $MT5_API_URL/positions
curl -H "Authorization: Bearer $MT5_API_TOKEN" "$MT5_API_URL/positions?symbol=EURUSD"
curl -H "Authorization: Bearer $MT5_API_TOKEN" $MT5_API_URL/positions/42094820

# Update SL/TP
curl -X PUT -H "Authorization: Bearer $MT5_API_TOKEN" $MT5_API_URL/positions/42094820 \
  -H 'Content-Type: application/json' \
  -d '{"sl": 1.085, "tp": 1.105}'

# Close full position
curl -X DELETE -H "Authorization: Bearer $MT5_API_TOKEN" $MT5_API_URL/positions/42094820

# Partial close
curl -X DELETE -H "Authorization: Bearer $MT5_API_TOKEN" $MT5_API_URL/positions/42094820 \
  -H 'Content-Type: application/json' \
  -d '{"volume": 0.05}'
```

Key position fields: `ticket`, `type` (0=buy, 1=sell), `volume`, `price_open`, `price_current`, `sl`, `tp`, `profit`, `swap`.

### History

```bash
curl -H "Authorization: Bearer $MT5_API_TOKEN" "$MT5_API_URL/history/orders?from=$(date -d '1 day ago' +%s)&to=$(date +%s)"
curl -H "Authorization: Bearer $MT5_API_TOKEN" "$MT5_API_URL/history/deals?from=$(date -d '1 day ago' +%s)&to=$(date +%s)"
```

`from` and `to` are required, unix epoch seconds.

Deal fields: `type` (0=buy, 1=sell), `entry` (0=opening, 1=closing), `profit` (0 for entries, realized P&L for exits).

### Backtest

Run MT5 Strategy Tester via the API. Two-stage workflow: build the INI from a
JSON spec, then submit it together with the `.ex5` (and optional `.set`) for
async execution. Endpoints exist on every terminal but only run on a
`mode: backtest` terminal in `config.yaml` — MT5 is single-instance per
portable data dir, so a tester subprocess collides with a `mode: live`
terminal that already owns the directory and exits silently. The broker/account
in the URL determines which credentials are injected into the run's `[Common]`
section. Only one tester runs at a time per API process; extra submissions
queue.

The expert and set file can be uploaded inline OR referenced by name from a
host-managed pool mounted at `assets/experts/*.ex5` and `assets/sets/*.set`.

```bash
# 1. Build INI: NZDJPY M15, last 5 years, open prices only, 5 ms latency.
curl -sS -X POST -H "Authorization: Bearer $MT5_API_TOKEN" \
  -H "Content-Type: application/json" \
  $MT5_API_URL/backtest/build-ini \
  -d '{
    "symbol": "NZDJPY",
    "timeframe": "M15",
    "expert": "EA Studio NZDJPY M15 1615044595.ex5",
    "lastYears": 5,
    "modelling": "open-prices",
    "latencyMs": 5,
    "expertParameters": "ea studio nzdjpy m15 1615044595.set"
  }' > tester.ini

# 2. Submit. Use uploads OR host-managed asset names — here, both are host-managed.
JOB=$(curl -sS -X POST -H "Authorization: Bearer $MT5_API_TOKEN" \
  $MT5_API_URL/backtest \
  -F "ini=@tester.ini" \
  -F "expert_name=EA Studio NZDJPY M15 1615044595.ex5" \
  -F "set_name=ea studio nzdjpy m15 1615044595.set" \
  | jq -r .jobId)

# 3. Poll. Status is queued → running → completed (or failed).
curl -H "Authorization: Bearer $MT5_API_TOKEN" $MT5_API_URL/backtest/$JOB

# 4. Fetch the report HTML and the terminal log.
curl -H "Authorization: Bearer $MT5_API_TOKEN" $MT5_API_URL/backtest/$JOB/report -o report.htm
curl -H "Authorization: Bearer $MT5_API_TOKEN" $MT5_API_URL/backtest/$JOB/log    -o run.log
```

`POST /backtest/build-ini` JSON fields: `symbol`, `timeframe` (`M1`…`MN1`),
`expert` (must end `.ex5`), and exactly one of `fromDate`+`toDate`,
`lastYears`, or `lastDays`. Optional: `modelling` (`every-tick` `1m-ohlc`
`open-prices` `real-ticks`), `latencyMs`, `deposit` (10000), `currency`
(`USD`), `leverage` (100, written as `1:N`), `expertParameters` (`.set`),
`optimization` (`0..3`), `optimizationCriterion` (`0..7`), `forwardMode`
(`0..4`), `visual`, and `reportName` (`backtest-report.htm`, or
`optimization-report.xml` when optimization is enabled).

`POST /backtest/build-set` generates MT5-native `.set` parameter text from
structured JSON. Body: `comments` (array of strings) and `parameters` (array
of `{name, value, start?, step?, stop?, optimize?}`). `optimize: true` emits
the optimization form `value||start||step||stop||Y`; `optimize: false` (or
`"N"`) emits `value||start||step||stop||N`. Omitting all four range fields
emits plain `name=value`; providing only some range fields is rejected.
Response is `text/plain` `.set` content ready to save or upload as the `set`
file.

```bash
curl -sS -X POST -H "Authorization: Bearer $MT5_API_TOKEN" \
  -H "Content-Type: application/json" \
  $MT5_API_URL/backtest/build-set \
  -d '{
    "comments": ["saved on 2026.05.15 08:30:02"],
    "parameters": [
      {"name": "_Properties_", "value": "------"},
      {"name": "Take_Profit", "value": 92, "start": 80, "step": 4, "stop": 92, "optimize": true},
      {"name": "Stop_Loss", "value": 0, "start": 0, "step": 1, "stop": 10, "optimize": false}
    ]
  }' > myea.set
```

`POST /backtest` multipart fields: `ini` (required), one of `expert` or
`expert_name`, optional `set` or `set_name`. Optional `topPasses` — for
optimization jobs, keep the top `1..500` parsed XML passes in the status
payload (default `50`). Optional `timeout` overrides the configured Strategy
Tester deadline using duration strings such as `"30m"` or `"6h"`. Returns
`202` with `jobId`, `statusUrl`, `reportUrl`, `logUrl`, `pollAfterSeconds`,
`queuePosition`. The INI's `[Common]`
`Login`/`Password`/`Server` are always overwritten with the URL-selected
account's credentials. Path traversal in `*_name` is rejected.

`GET /backtest/<jobId>` returns the job state. When `status: completed`, the
payload includes a `summary` parsed from the HTML (`netProfit`, `profitFactor`,
`recoveryFactor`, `expectedPayoff`, `sharpeRatio`, `maxDrawdown`,
`totalTrades`, `profitTrades`, `lossTrades`, …). Jobs left running when the
API restarts are marked `failed` on the next startup.

`GET /backtest/<jobId>/tail?lines=N` returns live diagnostic JSON while a job
is queued, running, or finished. It includes captured process output plus the
latest terminal and Strategy Tester journal lines; `lines` defaults to `200`
and is clamped to `10..1000`. The MCP `get_backtest` tool exposes the same data
with `part: "tail"`.

### Real Backtest Runbook For Agents

When the user asks for a real backtest run, do not stop at a built INI or a
`202 Accepted` submit response. The task is only complete after one of these is
true:

- the job reaches `completed`, the report/log are downloaded, and the requested
  summary fields are returned
- a request fails and you report the exact endpoint, HTTP status, and response
  body
- the job reaches `failed` and you report the final status payload exactly

Before submitting a backtest that references host-managed files:

1. Verify the requested filenames exist on disk exactly as named under
   `assets/experts/` and `assets/sets/`.
2. Verify `GET $MT5_API_URL/ping` returns backtest mode on the target terminal.
   For a real tester run, expect `{"status":"ok","mode":"backtest"}`.
3. If the user specifies a concrete date window, prefer explicit UTC
   `fromDate`/`toDate` and do not also send `lastYears` or `lastDays`.
4. Auth handling: use `MT5_API_TOKEN` from the user-set environment variable.
   If it is missing and the server requires auth, ask the user to provide
   it — do not search the repository or read `config/config.yaml` to harvest
   credentials. Backtests are read-side from the user's perspective (the
   server orchestrates Strategy Tester locally and never opens live trades),
   so no per-action confirmation is required to submit, but still surface
   the broker URL prefix so the user can catch a wrong-account misroute.

Execution guidance:

- Prefer one shell script or one tightly scoped command sequence that performs
  verify -> ping -> build INI -> submit -> poll -> download artifacts. This
  reduces uncertainty from partially completed attempts.
- Persist the local artifacts in a dedicated output directory:
  `tester.ini`, `status.json`, `report.html` (or `.htm`), and `run.log`.
- Treat `queued` and `running` as normal intermediate states. Report the job ID
  and latest status while polling.
- If no `jobId` has been captured yet, there is no confirmed backtest in
  progress. Do not claim the server is still working without that evidence.
- Poll `GET /backtest/<jobId>` using `pollAfterSeconds` from the submit/status
  payload when available. If the user explicitly requests a cadence, follow it.
- On any non-2xx HTTP response, stop immediately and show:
  endpoint, HTTP status, and raw response body.
- On `status: failed`, stop immediately and show the full final status payload.

Completion guidance:

- Download both `reportUrl` and `logUrl` before declaring success.
- Return the requested summary fields directly from the final status payload's
  `summary` object.
- Include the local artifact paths so the user can inspect the exact report and
  terminal log.

Example agent-oriented flow:

```bash
# 0. Verify host-managed assets exactly as named.
test -f "assets/experts/EA.ex5"
test -f "assets/sets/EA.set"

# 1. Health check the target backtest terminal.
curl -sS -H "Authorization: Bearer $MT5_API_TOKEN" \
  "$MT5_API_URL/ping"

# 2. Build the INI from an explicit UTC window.
curl -sS -X POST -H "Authorization: Bearer $MT5_API_TOKEN" \
  -H "Content-Type: application/json" \
  "$MT5_API_URL/backtest/build-ini" \
  -d '{
    "symbol": "GBPCAD",
    "timeframe": "M15",
    "expert": "EA.ex5",
    "fromDate": "2021-05-11",
    "toDate": "2026-05-11",
    "modelling": "open-prices",
    "latencyMs": 5,
    "deposit": 1000,
    "currency": "USD"
  }' > tester.ini

# 3. Submit and capture the job ID.
JOB=$(curl -sS -X POST -H "Authorization: Bearer $MT5_API_TOKEN" \
  "$MT5_API_URL/backtest" \
  -F "ini=@tester.ini" \
  -F "expert_name=EA.ex5" \
  -F "set_name=EA.set" | jq -r .jobId)

# 4. Poll until completed or failed, then download artifacts.
curl -sS -H "Authorization: Bearer $MT5_API_TOKEN" \
  "$MT5_API_URL/backtest/$JOB"
curl -sS -H "Authorization: Bearer $MT5_API_TOKEN" \
  "$MT5_API_URL/backtest/$JOB/report" -o report.html
curl -sS -H "Authorization: Bearer $MT5_API_TOKEN" \
  "$MT5_API_URL/backtest/$JOB/log" -o run.log
```

## Position Sizing

```
risk_amount     = balance * risk_pct
sl_distance     = ATR * multiplier
ticks_in_sl     = sl_distance / trade_tick_size
risk_per_lot    = ticks_in_sl * trade_tick_value
volume          = risk_amount / risk_per_lot
```

Round down to nearest `volume_step`, clamp to `[volume_min, volume_max]`. Sanity check: `volume * trade_contract_size * price` should make sense relative to account balance.

## Tips

- Always check `retcode` — 10009 = good, anything else = bad
- Use `GET /error` to debug failed trades
- `deviation` on orders = max slippage in points (default 20, raise for volatile markets)
- `type_filling` matters — try `FOK`, `IOC`, `RETURN` if orders get rejected
- Candle `time` is the open time, not close time
- `trade_stops_level` = minimum SL/TP distance from current price in points
- Markets have hours — check `trade_mode` before placing orders
