Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
### Package Structure

- `mt5api/`: Main FastAPI package.
- `main.py`: App wiring, lifespan handling, middleware, CORS, and router registration.
- `main.py`: App wiring, lifespan handling, middleware, and router registration.
- `__main__.py`: CLI entry point for launching the API from environment-backed configuration.
- `config.py`: Environment-backed configuration normalization and validation.
- `auth.py`: Optional API key authentication helpers.
Expand Down Expand Up @@ -68,7 +68,7 @@
## Security & Configuration Tips

- Do not commit real MT5 credentials or API keys.
- Configure `MT5API_SECRET_KEY`, `MT5API_RATE_LIMIT`, `MT5API_CORS_ORIGINS`, `MT5API_ROUTER_PREFIX`, and `MT5API_LOG_LEVEL` through environment variables.
- Configure `MT5API_SECRET_KEY`, `MT5API_ROUTER_PREFIX`, and `MT5API_LOG_LEVEL` through environment variables.
- Keep runtime configuration in the environment instead of hardcoding deployment values.
- Authentication mode is fixed at process startup; cover both authenticated and unauthenticated behavior when changing auth-related code.

Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ MetaTrader 5 REST API

mt5api exposes MT5 market data, account info, trading history, and trading
operations over HTTP. It uses the [`pdmt5`](https://github.com/dceoy/pdmt5)
client internally and adds optional API-key auth, rate limiting, and
JSON/Parquet response formatting.
client internally and adds optional API-key auth and JSON/Parquet response
formatting.

The API server must run on Windows. The `MetaTrader5` Python package used by
`pdmt5` is supported only on Windows, so you must host `mt5api` on a Windows
Expand All @@ -22,7 +22,7 @@ graph TB

subgraph "Windows Host"
subgraph "FastAPI Application"
Middleware["Middleware Stack<br/>CORS · Logging · Error Handler · Rate Limiter"]
Middleware["Middleware Stack<br/>Logging · Error Handler"]
Routers["Routers<br/>health · symbols · market · account · history · calc · trading"]
Auth["API Key Security Dependency<br/>Security(api_key_header) · verify_api_key"]
Deps["FastAPI Dependencies<br/>MT5 Client Singleton · Format Negotiation"]
Expand All @@ -45,8 +45,8 @@ graph TB
- REST endpoints for symbols, market data, account info, orders, history,
calculations, and trading operations
- JSON and Apache Parquet responses (content negotiation)
- Optional API key authentication with per-minute rate limiting
- Structured JSON logging and configurable CORS
- Optional API key authentication
- Structured JSON logging
- OpenAPI/Swagger docs built into the API

## Requirements
Expand Down
2 changes: 0 additions & 2 deletions docs/api/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@ nssm install mt5api
# Optional: set MT5API_SECRET_KEY only when you want to require X-API-Key headers.
MT5API_SECRET_KEY=your-secret-api-key
MT5API_LOG_LEVEL=INFO
MT5API_RATE_LIMIT=100
MT5API_MAX_MARKET_BOOK_SUBSCRIPTIONS=100
MT5API_CORS_ORIGINS=*
MT5API_ROUTER_PREFIX=/api/v1
```

Expand Down
16 changes: 5 additions & 11 deletions docs/api/rest-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@ Set the optional API key and other limits via environment variables:
```powershell
$env:MT5API_SECRET_KEY = "your-secret-api-key" # Optional: omit to disable auth
$env:MT5API_LOG_LEVEL = "INFO"
$env:MT5API_RATE_LIMIT = "100"
$env:MT5API_MAX_MARKET_BOOK_SUBSCRIPTIONS = "100"
$env:MT5API_CORS_ORIGINS = "*"
$env:MT5API_ROUTER_PREFIX = "/api/v1" # Optional: omit for root-level routes
```

Expand Down Expand Up @@ -70,10 +68,7 @@ disabled and those endpoints are accessible without authorization.
curl -H "X-API-Key: your-secret-api-key" "http://windows-host:8000/symbols"
```

## Rate Limiting

Rate limiting uses `slowapi` with a default limit of `100/minute`. Set
`MT5API_RATE_LIMIT` to an integer for a different per-minute cap.
## Subscription Limits

Active market-book subscriptions are capped at `100` symbols by default. Set
`MT5API_MAX_MARKET_BOOK_SUBSCRIPTIONS` to a positive integer to adjust that
Expand Down Expand Up @@ -121,9 +116,9 @@ If `MT5API_ROUTER_PREFIX` is configured, prepend it to each API route below.
- `GET /rates/range` (`symbol`, `timeframe`, `date_from`, `date_to`, `format`)
- `GET /ticks/from` (`symbol`, `date_from`, `count`, `flags`, `format`)
- `GET /ticks/range` (`symbol`, `date_from`, `date_to`, `flags`, `format`)
- `GET /market-book/{symbol}` (`format`) — Market depth (DOM)
- `POST /market-book/{symbol}/subscribe` — Subscribe to DOM events
- `POST /market-book/{symbol}/unsubscribe` — Unsubscribe from DOM events
- `GET /market-book/{symbol}` (`format`) — Market depth (DOM) *(experimental)*
- `POST /market-book/{symbol}/subscribe` — Subscribe to DOM events *(experimental)*
- `POST /market-book/{symbol}/unsubscribe` — Unsubscribe from DOM events *(experimental)*

### Calculations

Expand Down Expand Up @@ -315,9 +310,8 @@ Errors follow RFC 7807 Problem Details:
Minimum security posture for deployments:

- Set `MT5API_SECRET_KEY` to enable API key authentication when needed
- Rate limiting enabled (`MT5API_RATE_LIMIT`)
- Configure rate limiting at the reverse proxy or API gateway level
- Run behind HTTPS in production
- Restrict CORS origins (`MT5API_CORS_ORIGINS`) for public deployments
- Restrict access to operational endpoints (`/order/check`,
`/symbols/{symbol}/select`, `/market-book/{symbol}/subscribe`,
`/market-book/{symbol}/unsubscribe`) to trusted clients only
6 changes: 3 additions & 3 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ trading operations.

mt5api exposes MT5 data and trading operations over HTTP using FastAPI. It
relies on the underlying MT5 client library for connectivity and adds optional
authentication, rate limiting, and response formatting suitable for analytics
authentication and response formatting suitable for analytics
workflows.

The API server must run on Windows because the `MetaTrader5` Python package is
Expand All @@ -19,8 +19,8 @@ logged in. API clients can connect from any operating system.
- REST endpoints for symbols, market data, account info, orders, history,
calculations, and trading operations
- JSON and Apache Parquet responses
- Optional API key authentication and rate limiting
- Structured JSON logging and configurable CORS
- Optional API key authentication
- Structured JSON logging
- OpenAPI/Swagger docs built in

## Requirements
Expand Down
22 changes: 0 additions & 22 deletions mt5api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,14 @@
import re

from .constants import (
DEFAULT_API_CORS_ORIGINS,
DEFAULT_API_HOST,
DEFAULT_API_LOG_LEVEL,
DEFAULT_API_RATE_LIMIT,
DEFAULT_API_ROUTER_PREFIX,
DEFAULT_MAX_MARKET_BOOK_SUBSCRIPTIONS,
ENV_MT5API_CORS_ORIGINS,
ENV_MT5API_HOST,
ENV_MT5API_LOG_LEVEL,
ENV_MT5API_MAX_MARKET_BOOK_SUBSCRIPTIONS,
ENV_MT5API_PORT,
ENV_MT5API_RATE_LIMIT,
ENV_MT5API_ROUTER_PREFIX,
ENV_MT5API_SECRET_KEY,
)
Expand Down Expand Up @@ -81,24 +77,6 @@ def get_configured_api_log_level() -> str:
return os.getenv(ENV_MT5API_LOG_LEVEL, DEFAULT_API_LOG_LEVEL)


def get_configured_api_rate_limit() -> str:
"""Get the configured API rate limit string.

Returns:
Raw per-minute rate-limit string from configuration.
"""
return os.getenv(ENV_MT5API_RATE_LIMIT, str(DEFAULT_API_RATE_LIMIT))


def get_configured_api_cors_origins() -> str:
"""Get the configured CORS origins string.

Returns:
Raw CORS origins configuration string.
"""
return os.getenv(ENV_MT5API_CORS_ORIGINS, DEFAULT_API_CORS_ORIGINS)


def get_configured_api_router_prefix() -> str:
"""Get the configured API router prefix.

Expand Down
4 changes: 0 additions & 4 deletions mt5api/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,13 @@
ENV_MT5API_HOST = "MT5API_HOST"
ENV_MT5API_PORT = "MT5API_PORT"
ENV_MT5API_LOG_LEVEL = "MT5API_LOG_LEVEL"
ENV_MT5API_RATE_LIMIT = "MT5API_RATE_LIMIT"
ENV_MT5API_CORS_ORIGINS = "MT5API_CORS_ORIGINS"
ENV_MT5API_ROUTER_PREFIX = "MT5API_ROUTER_PREFIX"
ENV_MT5API_MAX_MARKET_BOOK_SUBSCRIPTIONS = "MT5API_MAX_MARKET_BOOK_SUBSCRIPTIONS"
ENV_MT5API_SECRET_KEY = "MT5API_SECRET_KEY" # noqa: S105

DEFAULT_API_HOST = "0.0.0.0" # noqa: S104
DEFAULT_API_PORT = 8000
DEFAULT_API_LOG_LEVEL = "INFO"
DEFAULT_API_RATE_LIMIT = 100
DEFAULT_API_CORS_ORIGINS = "*"
DEFAULT_API_ROUTER_PREFIX = ""
DEFAULT_MAX_MARKET_BOOK_SUBSCRIPTIONS = 100
MAX_API_PORT = 65535
25 changes: 0 additions & 25 deletions mt5api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,9 @@

from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi
from starlette.middleware.cors import CORSMiddleware

from .auth import is_auth_enabled
from .config import (
get_configured_api_cors_origins,
get_configured_api_log_level,
get_configured_api_router_prefix,
get_configured_max_market_book_subscriptions,
Expand All @@ -28,7 +26,6 @@
API_REDOC_URL,
API_TITLE,
API_VERSION,
DEFAULT_API_CORS_ORIGINS,
MARKET_BOOK_CLEANUP_CLIENT_STATE_KEY,
MAX_MARKET_BOOK_SUBSCRIPTIONS_STATE_KEY,
)
Expand Down Expand Up @@ -70,19 +67,6 @@ def _configure_logging() -> None:
root_logger.addHandler(handler)


def _get_cors_origins() -> list[str]:
"""Get CORS origins from environment.

Returns:
List of allowed origins.
"""
raw_origins = get_configured_api_cors_origins()
if raw_origins.strip() == DEFAULT_API_CORS_ORIGINS:
return [DEFAULT_API_CORS_ORIGINS]

return [origin.strip() for origin in raw_origins.split(",") if origin.strip()]


def _strip_auth_from_openapi(openapi_schema: dict[str, Any]) -> None:
"""Remove API key requirements from OpenAPI when auth is disabled."""
openapi_schema.pop("security", None)
Expand Down Expand Up @@ -212,15 +196,6 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
)
app.openapi = _custom_openapi

# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=_get_cors_origins(),
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

# Add middleware
add_middleware(app)

Expand Down
55 changes: 1 addition & 54 deletions mt5api/middleware.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Error handling, logging, and rate limiting middleware."""
"""Error handling and logging middleware."""

from __future__ import annotations

Expand All @@ -10,13 +10,7 @@
from fastapi.responses import JSONResponse
from pdmt5.mt5 import Mt5RuntimeError
from pydantic import ValidationError
from slowapi import Limiter
from slowapi.errors import RateLimitExceeded
from slowapi.middleware import SlowAPIMiddleware
from slowapi.util import get_remote_address

from .config import get_configured_api_rate_limit
from .constants import DEFAULT_API_RATE_LIMIT
from .models import ErrorResponse

if TYPE_CHECKING:
Expand Down Expand Up @@ -168,58 +162,11 @@ async def logging_middleware(
return response


def _build_default_rate_limit() -> str:
"""Build the default rate limit string from environment config.

Returns:
Default rate limit string in slowapi format.
"""
raw_limit = get_configured_api_rate_limit()

try:
limit_value = max(1, int(raw_limit))
except ValueError:
limit_value = DEFAULT_API_RATE_LIMIT

return f"{limit_value}/minute"


def add_middleware(app: FastAPI) -> None:
"""Add middleware and error handlers to the FastAPI application.

Args:
app: FastAPI application instance.
"""
# Configure rate limiting
limiter = Limiter(
key_func=get_remote_address,
default_limits=[_build_default_rate_limit()],
)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
app.add_middleware(SlowAPIMiddleware)

# Add custom middleware
app.middleware("http")(error_handler_middleware)
app.middleware("http")(logging_middleware)


def _rate_limit_exceeded_handler(
request: Request, # noqa: ARG001
exc: Exception, # noqa: ARG001
) -> JSONResponse:
"""Handle rate limiting errors.

Returns:
JSON response describing the rate limit error.
"""
return JSONResponse(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
content={
"type": "/errors/rate-limit",
"title": "Rate Limit Exceeded",
"status": status.HTTP_429_TOO_MANY_REQUESTS,
"detail": "Too many requests. Please slow down.",
"instance": None,
},
)
4 changes: 2 additions & 2 deletions mt5api/routers/market.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,8 @@ async def get_ticks_range(
@router.get(
"/market-book/{symbol}",
response_model=DataResponse,
summary="Get market book",
description="Get market depth (DOM) for a symbol",
summary="Get market book (experimental)",
description="**Experimental.** Get market depth (DOM) for a symbol",
)
async def get_market_book(
mt5_client: Annotated[Mt5DataClient, Depends(get_mt5_client)],
Expand Down
8 changes: 4 additions & 4 deletions mt5api/routers/trading.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ async def post_symbol_select(
@router.post(
"/market-book/{symbol}/subscribe",
response_model=DataResponse,
summary="Subscribe to market depth",
description="Subscribe to Market Depth change events for a symbol",
summary="Subscribe to market depth (experimental)",
description="**Experimental.** Subscribe to Market Depth events for a symbol",
)
async def post_market_book_subscribe(
app_request: Request,
Expand Down Expand Up @@ -188,8 +188,8 @@ async def post_market_book_subscribe(
@router.post(
"/market-book/{symbol}/unsubscribe",
response_model=DataResponse,
summary="Unsubscribe from market depth",
description="Cancel Market Depth subscription for a symbol",
summary="Unsubscribe from market depth (experimental)",
description="**Experimental.** Cancel Market Depth subscription for a symbol",
)
async def post_market_book_unsubscribe(
app_request: Request,
Expand Down
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "mt5api"
version = "0.0.4"
version = "0.1.0"
description = "MetaTrader 5 REST API"
authors = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
maintainers = [{name = "dceoy", email = "dceoy@users.noreply.github.com"}]
Expand All @@ -17,7 +17,6 @@ dependencies = [
"python-multipart >= 0.0.9",
"python-jose[cryptography] >= 3.3.0",
"passlib[bcrypt] >= 1.7.4",
"slowapi >= 0.1.9",
"httpx >= 0.27.0",
]
classifiers = [
Expand Down
Loading