Metadata-Version: 2.4
Name: open-api-mt5
Version: 0.7.1
Summary: REST and WebSocket API project for MetaTrader 5
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: fastapi<1.0.0,>=0.116.0
Requires-Dist: MetaTrader5<6.0.0,>=5.0.0
Requires-Dist: uvicorn[standard]<1.0.0,>=0.35.0
Provides-Extra: test
Requires-Dist: httpx<1.0.0,>=0.28.0; extra == "test"
Requires-Dist: pytest<9.0.0,>=8.4.0; extra == "test"
Requires-Dist: pytest-asyncio<2.0.0,>=1.0.0; extra == "test"
Requires-Dist: pytest-cov<7.0.0,>=6.2.0; extra == "test"

# open-api-mt5

MetaTrader 5 API service with FastAPI.

## Important limitation

This API controls a **single MT5 terminal/session instance** per running service process.

- A single API instance can be connected to only one account at a time.
- `/account/connect` switches that single active session.
- If you run multiple API services, use separate MT5 terminal instances/data folders for reliable isolation.

## Setup

1. Create a virtual environment:
   - Windows PowerShell: `python -m venv .venv`
2. Activate it:
   - `.\.venv\Scripts\Activate.ps1`
3. Install dependencies:
   - `python -m pip install -U pip`
   - `pip install -e .`

## MT5 startup config

The API initializes MetaTrader 5 when FastAPI starts and closes it when FastAPI stops.
It also checks MT5 connection every 5 seconds and tries to reconnect automatically if disconnected.

You can configure startup with a `server_config.json` file in the project directory:

```json
{
  "path": "C:\\Program Files\\MetaTrader 5\\terminal64.exe",
  "username": "12345678",
  "password": "your-password",
  "server": "YourBroker-Server",
  "port": 8000,
  "apiKey": "optional-api-key"
}
```

`port` and `apiKey` are optional. If the JSON file does not exist, the app uses the environment variables and command-line parameters by default.

You can also set these environment variables in PowerShell before running:

```powershell
$env:MT5_PATH = "C:\Program Files\MetaTrader 5\terminal64.exe"
$env:MT5_LOGIN = "12345678"
$env:MT5_PASSWORD = "your-password"
$env:MT5_SERVER = "YourBroker-Server"
```

`MT5_PATH` is optional if MT5 is already discoverable, but setting it is recommended.
Mode, `apiKey`, `apiSecret`, and `registryUrl` are startup settings and are not stored by `/account/connect`.

## Run

```bash
open-api-mt5
```

Optional flags:

```bash
open-api-mt5 --port 9000
open-api-mt5 --host 0.0.0.0 --port 8000
open-api-mt5 --mode standalone
open-api-mt5 --mode secure --api-key your-api-key
open-api-mt5 --mode secure-client --api-key your-api-key --api-secret your-api-secret
open-api-mt5 --registry-url https://example.com/registry
open-api-mt5 --reload
open-api-mt5 --config C:\path\to\server_config.json
```

Default port is `8000`.

Run unit tests with coverage:

```bash
pip install -e ".[test]"
pytest
```

The calendar, bars, and trade services require 100% statement coverage.

Run only the mocked news-close integration scenarios:

```bash
pytest -m integration --no-cov
```

API docs:
- Swagger UI: `http://127.0.0.1:8000/docs`
- WebSocket docs in Swagger:
  - `GET /ws/positions/open/docs`

Health endpoint:
- `GET http://127.0.0.1:8000/health`

Bars endpoints:
- `GET http://127.0.0.1:8000/bars/{symbol}?timeframe=M1&n=100`
- `GET http://127.0.0.1:8000/bars/{symbol}/range?timeframe=M1&fromDate=2026-03-20T08:00:00Z&toDate=2026-03-20T12:00:00Z`
- `GET http://127.0.0.1:8000/quotes/{symbol}`
- `GET http://127.0.0.1:8000/ticks/{symbol}?count=100`
- `GET http://127.0.0.1:8000/market-depth/{symbol}`
- `GET http://127.0.0.1:8000/exposure`
- `GET http://127.0.0.1:8000/exposure/{symbol}`

Account connection endpoints:
- `POST http://127.0.0.1:8000/account/connect`
  - Body: `username`, `password`, `server`, optional `path`
  - Example body:
    ```json
    {
      "username": "12345678",
      "password": "your-password",
      "server": "YourBroker-Server",
      "path": "C:\\Program Files\\MetaTrader 5\\terminal64.exe"
    }
    ```
- `POST http://127.0.0.1:8000/account/disconnect`
- If `--registry-url` is set at startup, the API sends a `POST` call to that URL with JSON body fields `address`, `port`, `apiKey`, and `accountId`

Security modes:
- Set the mode when starting the app with `--mode standalone`, `--mode secure`, or `--mode secure-client`
- Set the headers credentials when starting the app with `--api-key` and, for `secure-client`, `--api-secret`
- Set the registry target when starting the app with `--registry-url`
- `standalone`: default mode, no `X-apiKey` or `X-apiSecret` header checks
- `secure`: every HTTP endpoint and the open positions WebSocket require `X-apiKey` to match the locally stored `apiKey`
- `secure-client`: every HTTP endpoint and the open positions WebSocket require both `X-apiKey` and `X-apiSecret` to match the locally stored values

Server info endpoint:
- `GET http://127.0.0.1:8000/api/server/info`
- Returns JSON with the locally stored `apiKey` and the detected machine `ipAddress`

Trade history endpoint:
- `GET http://127.0.0.1:8000/trades/history`
- Optional query params: `fromDate`, `toDate` (ISO datetime, UTC recommended)
- If omitted, it returns the last 7 days by default

Modify an open position's stop loss / take profit:
- `POST http://127.0.0.1:8000/positions/modify`
  - Body: `ticket`, optional `sl`, optional `tp`, optional `comment`
  - At least one of `sl` or `tp` is required. If one is omitted, its current MT5 value is kept.
  - Example body:
    ```json
    {
      "ticket": 123456789,
      "sl": 1.0825,
      "tp": 1.095
    }
    ```

Open position details:
- `GET http://127.0.0.1:8000/positions/{ticket}/details`
- Returns `entryPrice`, `stopLossPrice`, `takeProfitPrice`, `volume`, `contractSize`, `stopLossValue`, and `takeProfitValue`
- Value formula:
  - Buy stop loss: `(entryPrice - stopLossPrice) * volume * contractSize`
  - Buy take profit: `(takeProfitPrice - entryPrice) * volume * contractSize`
  - Sell stop loss: `(stopLossPrice - entryPrice) * volume * contractSize`
  - Sell take profit: `(entryPrice - takeProfitPrice) * volume * contractSize`

Adjust an open position's stop loss / take profit by money values:
- `POST http://127.0.0.1:8000/positions/adjust-by-money`
  - Body: `ticket`, optional `stopLossValueInMoney`, optional `takeProfitValueInMoney`, optional `comment`
  - Defaults: `stopLossValueInMoney = 10`, `takeProfitValueInMoney = 30`
  - The API converts money values to SL/TP price distances using the position entry price, volume, and symbol contract size.
  - If the exact value cannot be represented by the symbol price step, the API uses the nearest lower value.
  - Example body:
    ```json
    {
      "ticket": 123456789,
      "stopLossValueInMoney": 10,
      "takeProfitValueInMoney": 30
    }
    ```
  - Response includes `stopLossPrice`, `takeProfitPrice`, `stopLossValueInMoney`, and `takeProfitValueInMoney` after rounding.

Close orders before economic news:
- `POST /orders` accepts optional `closeOnNews`, for example `M15_High`, `M10_Medium`, or `M5_Low`
- New orders with `closeOnNews` are rejected with HTTP 409 when a matching event is already inside the configured close window
- `GET /orders/news-close/jobs` lists the current tagged pending orders and open positions watched by the news-close checker
- The policy is stored in a durable SQLite file at `.db/news_close_jobs.sqlite3` under the folder where the API process was started
- Restart the API from the same folder to reuse the existing news-close jobs; set `NEWS_CLOSE_DB_PATH` only if you need a custom database path
- `High` closes only for high-impact events; `Medium` closes for medium/high; `Low` closes for low/medium/high
- Events are matched against the symbol's base, profit, or margin currency
- Tagged open positions are closed and tagged pending orders are cancelled when an event enters the configured time window
- Modifying SL/TP values, including money-based adjustments, keeps the stored news policy unchanged
- The check runs once at startup and every 60 seconds; orders without `closeOnNews` are unchanged
- Fair Economy events include an explicit timezone and are compared in UTC. If you test with timezone-less event timestamps, the API also considers the local computer timezone; set `LOCAL_TIMEZONE` (for example `Europe/Paris`) only if you need to override the OS timezone.

Calendar events endpoint:
- `GET http://127.0.0.1:8000/calendar/events`
- The default source is the Fair Economy current-week feed, cached in `calendar_cache.json`
- The cache is loaded at startup, retried once on startup failure, refreshed every Monday at 00:00 UTC, and loaded lazily when missing or stale
- Optional query params: `source` (`fairEconomy` or `mt5`), `fromDate`, `toDate` (ISO datetime), `country`/`currency` (example: `USD`), and `impact` (`Low`, `Medium`, `High`, or `Holiday`)
- Fair Economy defaults to the current Monday-through-Sunday UTC week
- Use `source=mt5` for the previous MT5 calendar behavior (default range: last 7 days to next 7 days)
- Optional environment variables: `FAIR_ECONOMY_CALENDAR_URL` and `FAIR_ECONOMY_CALENDAR_CACHE_PATH`

## WebSocket streams

Open positions stream:
- `ws://127.0.0.1:8000/ws/positions/open`
- Optional query param: `intervalSeconds` (poll interval, bounded to 0.2..60)
- Events:
  - `subscribed`
  - `positionsSnapshot`
  - `error`
- `positionsSnapshot` includes:
  - `positions[].pnl` (position PnL, sourced from MT5 `profit`)
  - `totalPnl` (sum of all open positions PnL)

Example JavaScript client:

```javascript
const ws = new WebSocket("ws://127.0.0.1:8000/ws/positions/open?intervalSeconds=1");
ws.onmessage = (event) => {
  const payload = JSON.parse(event.data);
  console.log(payload.event, payload);
};
```

## Build and publish package

Build distribution files locally:

```bash
python -m pip install --upgrade build
python -m build
```

Install from the local build to smoke-test it:

```bash
python -m pip install --force-reinstall dist/*.whl
open-api-mt5 --help
```

Install from PyPI and run:

```bash
pip install open-api-mt5
open-api-mt5 --port 8000
```

## Release to PyPI

The repository includes `.github/workflows/publish.yml`, which runs tests, builds the package, and publishes to PyPI with a PyPI API token. It only runs from release branches named like `release/0.5`.

One-time PyPI token setup:

1. Create or open your PyPI account.
2. Create a PyPI API token. If the project already exists on PyPI, prefer a project-scoped token for `open-api-mt5`; otherwise create an account-scoped token for the first upload.
3. In GitHub, open repository `Settings` -> `Secrets and variables` -> `Actions`.
4. Add a repository secret named `PYPI_API_TOKEN` with the token value from PyPI. The workflow uses PyPI username `__token__` and this secret as the password.
5. Optional: create a GitHub environment named `pypi` and add required reviewers if you want manual approval before publishing.

Prepare a release:

1. Update `version` in `pyproject.toml`.
2. Run tests:
   ```bash
   python -m pip install -e ".[test]"
   python -m pytest
   ```
3. Build locally:
   ```bash
   python -m pip install --upgrade build
   python -m build
   ```
4. Commit the version change:
   ```bash
   git add pyproject.toml
   git commit -m "Release 0.6.1"
   ```
5. Create and push a release branch:
   ```bash
   git checkout -b release/0.6.1
   git push origin release/0.6.1
   ```
6. The `Publish to PyPI` workflow runs on pushes to `release/**`. You can also run it manually from GitHub Actions, but select a `release/**` branch such as `release/0.6.1`.

PyPI rejects reused versions, so every release must have a new `pyproject.toml` version.

Manual upload with a token, if needed:

```bash
python -m pip install --upgrade twine
python -m twine upload dist/* -u __token__ -p "<your-pypi-token>"
```

## Optional: standalone executable (no Python required on target machine)

If you want users to run it without installing Python, build an executable:

```bash
python -m pip install pyinstaller
pyinstaller --onefile --name open-api-mt5 app/cli.py
```

The executable will be in `dist/open-api-mt5.exe`.
