---
slug: "mt5-httpapi-x-10"
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

This skill drives an mt5-httpapi server the user already runs. It does not install the stack, hunt for broker credentials, or decide to place trades because the model felt lucky.

## Security and safety — real money, no cowboy shit

This API moves real money on a real brokerage account. Mutating endpoints are irreversible, so precision beats enthusiasm every fucking time.

**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 local pandas circus. `POST /symbols/<symbol>/rates/ta` returns OHLC bars and the indicators you asked for in one call. [wickworks](https://github.com/psyb0t/docker-wickworks) does the math server-side and returns primitives, not magical buy/sell bullshit. Build interpretations in the consumer. See [Technical Analysis](#technical-analysis).

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 the fucker 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 this shit

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 — finish the fucking job

When the user asks for a real backtest, do not declare victory after building an
INI or getting `202 Accepted`. That only means the job entered the building. The
task is 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
