---
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. Creating, changing or deleting a chart deployment (/deployments) or closing a chart starts or stops a live EA and needs the same confirmation. 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.

**Live EA deployments.** `POST /deployments`, `PATCH /deployments/<id>`, `DELETE /deployments/<id>` and `POST /charts/<id>/close` start, re-point, pause or stop an Expert Advisor running on a chart of a real account, and that EA trades on its own. Confirm the expert, set file, symbol, timeframe and account before each one, exactly like a trade. `PUT /webrequest` and `POST /webrequest/apply` restart the terminal when it runs on bare metal rather than in the Windows VM.

**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`, and the Chart Deployments reads `GET /experts`, `GET /sets*`, `GET /deployments*`, `GET /charts`, `GET /loader`, `GET /webrequest`) do not require per-action confirmation. Staging a new file with `POST /experts` or `POST /sets` changes nothing that runs until a deployment uses it. Overwriting an expert a deployment already uses (`?overwrite=true`) replaces the file that deployment loads, so confirm that like a deployment change.

**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.

On terminals with [Chart Deployments](#chart-deployments) enabled, MCP also has the chartctl tools: `upload_expert`, `list_experts`, `delete_expert`, `upload_set`, `list_sets`, `get_set`, `delete_set`, `create_deployment`, `list_deployments`, `get_deployment`, `update_deployment`, `delete_deployment`, `reconcile_deployments`, `list_charts`, `get_loader`, `screenshot_chart`, `close_chart`, `get_webrequest`, `set_webrequest`, `apply_webrequest`. The upload tools take the file as base64 (`upload_set` also takes plain text) and send the multipart form for you. `screenshot_chart` returns the PNG as image content you can look at. On terminals with the [file API](#files) enabled, `list_files`, `get_file`, `put_file` and `delete_file` read and write files in the terminal's install directory or the compile tree.

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/master/.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, each process mode (`live` or `backtest`), and whether Chart Deployments are enabled for each (`chartctl`); the mode 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, and 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).

### Chart Deployments

Deploy Expert Advisors to charts over HTTP. No RDP, no terminal restart. Stage `.ex5` + `.set` files, declare deployments, and a resident loader EA inside the terminal reconciles charts to match. The API holds desired state; the loader reports observed truth. A deployment only flips to `running` once the loader confirms the expert is live on a chart.

**Off by default.** The operator turns it on with `chartctl.enabled: true` in `config.yaml`, live-mode terminals only, and any terminal can opt out with `chartctl: false`. On a terminal without it these routes answer `404` and the per-terminal MCP endpoint has no chartctl tools; on the unified MCP endpoint `list_terminals` shows `chartctl` per terminal. Once enabled, the loader attaches itself on boot through `[StartUp] Expert=`, so there is nothing to attach by hand. If `GET /loader` says `alive: false`, nothing will deploy; tell the user instead of retrying.

Endpoint reference:

| Method | Endpoint | Description |
| ------ | -------- | ----------- |
| `POST` / `GET` / `DELETE` | `/experts` `/experts/<name>` | Stage, list, remove EA `.ex5` |
| `POST` / `GET` | `/sets` `/sets/<name>` | Stage, list, inspect `.set` (returns parsed inputs) |
| `POST` / `GET` | `/deployments` | Create or list deployments |
| `GET` / `PATCH` / `DELETE` | `/deployments/<id>` | Inspect, pause/resume, change set, delete |
| `POST` | `/deployments/reconcile` | Force immediate reconcile |
| `GET` | `/charts` | Live chart/EA inventory |
| `GET` | `/loader` | Loader EA status and version |
| `POST` | `/charts/<id>/screenshot` | Capture chart as PNG |
| `POST` | `/charts/<id>/close` | Close a chart by id |

```bash
# Stage artifacts. Re-uploading identical bytes is skipped; different bytes
# under the same .ex5 name need ?overwrite=true. The response's
# "navigator_refresh" must be "ok" before a deployment of a NEW expert can
# attach: on "failed" upload the same file again, on "unavailable" (bare metal)
# the terminal needs a restart first.
curl -H "Authorization: Bearer $MT5_API_TOKEN" \
  -F "expert=@HappyGoldScalp.ex5" "$MT5_API_URL/experts"
curl -H "Authorization: Bearer $MT5_API_TOKEN" \
  -F "set=@gold-m5.set" "$MT5_API_URL/sets"   # returns the parsed inputs

# Deploy (live EA: confirm first). 409 DUPLICATE_CHART if an enabled
# deployment already targets this symbol + timeframe.
curl -X POST "$MT5_API_URL/deployments" \
  -H "Authorization: Bearer $MT5_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"expert":"HappyGoldScalp.ex5","set":"gold-m5.set","symbol":"XAUUSD","timeframe":"M5"}'
# -> 202 {"id":"dep_a1b2c3","status":"pending",...}

# Poll until status is "running" (or "failed"; the error is in the payload)
curl -H "Authorization: Bearer $MT5_API_TOKEN" "$MT5_API_URL/deployments/dep_a1b2c3"

# Pause, switch the set file ("set": "" runs on the EA defaults), delete.
# Pausing or deleting closes the deployment's chart. A new set file does NOT
# reach an expert that is already running: it applies the next time the loader
# opens the chart. To apply it now: pause, wait until GET /charts no longer
# lists the deployment's chart (the deployment says "paused" right away, before
# the loader acts), then resume. Resuming is refused with 409 DUPLICATE_CHART
# while another enabled deployment runs on the same symbol and timeframe.
curl -X PATCH "$MT5_API_URL/deployments/dep_a1b2c3" \
  -H "Authorization: Bearer $MT5_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"enabled":false}'
curl -X DELETE "$MT5_API_URL/deployments/dep_a1b2c3" \
  -H "Authorization: Bearer $MT5_API_TOKEN"

# Look at the chart (PNG; width/height optional, default 1280x720)
curl -X POST -H "Authorization: Bearer $MT5_API_TOKEN" \
  "$MT5_API_URL/charts/133039100/screenshot?width=1280&height=720" -o chart.png
```

**WebRequest allowlist.** An EA that calls `WebRequest()` only reaches URLs on the terminal's allowlist. `GET /webrequest` returns the list. `PUT /webrequest` replaces it with `{"urls": [...]}` or edits it with `{"add": [...], "remove": [...]}` and applies it right away; only `http(s)` URLs are kept. Inside the Windows VM the API types the list into the terminal's Options dialog; on bare metal it rewrites `common.ini` and restarts the terminal. The VM terminal forgets the list when it restarts. The API re-applies it when its own process starts and after every terminal restart it performs itself (`POST /terminal/restart`, the health monitor), so call `POST /webrequest/apply` only after a restart from outside the API, or when an expert's `WebRequest()` is still refused. Add `?runas=1` to either call when MT5 runs elevated.

```bash
curl -X PUT "$MT5_API_URL/webrequest" \
  -H "Authorization: Bearer $MT5_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"add":["https://api.example.com"]}'
```

Full protocol: [`docs/chart-control-protocol.md`](https://github.com/psyb0t/mt5-httpapi/blob/master/docs/chart-control-protocol.md). Operator guide: [`docs/chart-deployments.md`](https://github.com/psyb0t/mt5-httpapi/blob/master/docs/chart-deployments.md).

### Files

**Off by default.** The operator turns it on with `files.enabled: true` in `config.yaml`; a terminal can opt out with `files: false`. Without it these routes answer `404`. `GET /files/<path>` lists a directory or downloads a file in the terminal's install directory (the folder with `terminal64.exe`), `PUT /files/<path>` uploads one (raw body or a multipart `file` field), `PUT /files/<dir>?extract` unpacks a zip into that directory, and `DELETE /files/<path>` (`?recursive` for a non-empty directory) removes it. `/compile/files/...` does the same on the MQL5 tree `POST /compile` builds against, which is where a shared `.mqh` library goes. The MCP tools are `list_files`, `get_file`, `put_file` and `delete_file`, with `tree` set to `terminal` or `compile`.

The broker credentials (`mt5start.ini`, `Config/accounts.dat`) are never served, and the terminal's executables and Chart Deployments' own files are read-only here. **Writing into `MQL5/Experts`, `MQL5/Libraries` or `MQL5/Include` changes what experts on that account run, and a DLL in `MQL5/Libraries` runs inside the terminal: do it only when the user asked for that exact change, and confirm the terminal first.** Reading files and logs needs no confirmation.

```bash
# Unpack a library into the compile tree, then compile against it
curl -X PUT -H "Authorization: Bearer $MT5_API_TOKEN" --data-binary @MyLib.zip \
  "$MT5_API_URL/compile/files/Include/MyLib?extract"
# source: #include <MyLib/Signals.mqh>
```

Operator guide: [`docs/files.md`](https://github.com/psyb0t/mt5-httpapi/blob/master/docs/files.md).

### 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. Responses below use snake_case field names. The old camelCase names
still show up alongside them in responses, and request bodies still accept
either one, but the camelCase names are deprecated and go away in v5.0.0.

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",
    "last_years": 5,
    "modelling": "open-prices",
    "latency_ms": 5,
    "expert_parameters": "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 .job_id)

# 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 `from_date`+`to_date`,
`last_years`, or `last_days`. Optional: `modelling` (`every-tick` `1m-ohlc`
`open-prices` `real-ticks`), `latency_ms`, `deposit` (10000), `currency`
(`USD`), `leverage` (100, written as `1:N`), `expert_parameters` (`.set`),
`optimization` (`0..3`), `optimization_criterion` (`0..7`), `forward_mode`
(`0..4`), `visual`, and `report_name` (`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 `top_passes`, for
optimization jobs, keeps 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 `job_id`, `status_url`, `report_url`, `log_url`, `poll_after_seconds`,
`queue_position`. 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/<job_id>` returns the job state. When `status: completed`, the
payload includes a `summary` parsed from the HTML (`net_profit`, `profit_factor`,
`recovery_factor`, `expected_payoff`, `sharpe_ratio`, `max_drawdown`,
`total_trades`, `profit_trades`, `loss_trades`, …). Jobs left running when the
API restarts are marked `failed` on the next startup.

`GET /backtest/<job_id>/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
   `from_date`/`to_date` and do not also send `last_years` or `last_days`.
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 `job_id` 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/<job_id>` using `poll_after_seconds` 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 `report_url` and `log_url` 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",
    "from_date": "2021-05-11",
    "to_date": "2026-05-11",
    "modelling": "open-prices",
    "latency_ms": 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 .job_id)

# 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
