olza-api-mt5 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- olza_api_mt5-0.1.0/PKG-INFO +95 -0
- olza_api_mt5-0.1.0/README.md +85 -0
- olza_api_mt5-0.1.0/app/__init__.py +1 -0
- olza_api_mt5-0.1.0/app/cli.py +14 -0
- olza_api_mt5-0.1.0/app/main.py +268 -0
- olza_api_mt5-0.1.0/app/services/__init__.py +1 -0
- olza_api_mt5-0.1.0/app/services/account_service.py +18 -0
- olza_api_mt5-0.1.0/app/services/bar_service.py +79 -0
- olza_api_mt5-0.1.0/app/services/symbol_service.py +43 -0
- olza_api_mt5-0.1.0/app/services/ticket_service.py +29 -0
- olza_api_mt5-0.1.0/app/services/trade_service.py +265 -0
- olza_api_mt5-0.1.0/olza_api_mt5.egg-info/PKG-INFO +95 -0
- olza_api_mt5-0.1.0/olza_api_mt5.egg-info/SOURCES.txt +17 -0
- olza_api_mt5-0.1.0/olza_api_mt5.egg-info/dependency_links.txt +1 -0
- olza_api_mt5-0.1.0/olza_api_mt5.egg-info/entry_points.txt +2 -0
- olza_api_mt5-0.1.0/olza_api_mt5.egg-info/requires.txt +3 -0
- olza_api_mt5-0.1.0/olza_api_mt5.egg-info/top_level.txt +1 -0
- olza_api_mt5-0.1.0/pyproject.toml +18 -0
- olza_api_mt5-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: olza-api-mt5
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: REST and WebSocket API project for MetaTrader 5
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: fastapi<1.0.0,>=0.116.0
|
|
8
|
+
Requires-Dist: MetaTrader5<6.0.0,>=5.0.0
|
|
9
|
+
Requires-Dist: uvicorn[standard]<1.0.0,>=0.35.0
|
|
10
|
+
|
|
11
|
+
# python-api-mt5
|
|
12
|
+
|
|
13
|
+
Starter project for a MetaTrader 5 API service with FastAPI.
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
1. Create a virtual environment:
|
|
18
|
+
- Windows PowerShell: `python -m venv .venv`
|
|
19
|
+
2. Activate it:
|
|
20
|
+
- `.\.venv\Scripts\Activate.ps1`
|
|
21
|
+
3. Install dependencies:
|
|
22
|
+
- `python -m pip install -U pip`
|
|
23
|
+
- `pip install -e .`
|
|
24
|
+
|
|
25
|
+
## MT5 startup config
|
|
26
|
+
|
|
27
|
+
The API initializes MetaTrader 5 when FastAPI starts and closes it when FastAPI stops.
|
|
28
|
+
It also checks MT5 connection every 5 seconds and tries to reconnect automatically if disconnected.
|
|
29
|
+
|
|
30
|
+
Set these environment variables in PowerShell before running:
|
|
31
|
+
|
|
32
|
+
```powershell
|
|
33
|
+
$env:MT5_PATH = "C:\Program Files\MetaTrader 5\terminal64.exe"
|
|
34
|
+
$env:MT5_LOGIN = "12345678"
|
|
35
|
+
$env:MT5_PASSWORD = "your-password"
|
|
36
|
+
$env:MT5_SERVER = "YourBroker-Server"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`MT5_PATH` is optional if MT5 is already discoverable, but setting it is recommended.
|
|
40
|
+
|
|
41
|
+
## Run
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
olza-api-mt5
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Optional flags:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
olza-api-mt5 --port 9000
|
|
51
|
+
olza-api-mt5 --host 0.0.0.0 --port 8000
|
|
52
|
+
olza-api-mt5 --reload
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Default port is `8000`.
|
|
56
|
+
|
|
57
|
+
API docs:
|
|
58
|
+
- Swagger UI: `http://127.0.0.1:8000/docs`
|
|
59
|
+
- ReDoc: `http://127.0.0.1:8000/redoc`
|
|
60
|
+
|
|
61
|
+
Health endpoint:
|
|
62
|
+
- `GET http://127.0.0.1:8000/health`
|
|
63
|
+
|
|
64
|
+
## Build and publish package
|
|
65
|
+
|
|
66
|
+
1. Build distribution files:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
python -m pip install --upgrade build twine
|
|
70
|
+
python -m build
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
2. Upload to PyPI:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
python -m twine upload dist/*
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
3. Install from PyPI and run:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pip install olza-api-mt5
|
|
83
|
+
olza-api-mt5 --port 8000
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Optional: standalone executable (no Python required on target machine)
|
|
87
|
+
|
|
88
|
+
If you want users to run it without installing Python, build an executable:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
python -m pip install pyinstaller
|
|
92
|
+
pyinstaller --onefile --name olza-api-mt5 app/cli.py
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The executable will be in `dist/olza-api-mt5.exe`.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# python-api-mt5
|
|
2
|
+
|
|
3
|
+
Starter project for a MetaTrader 5 API service with FastAPI.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
1. Create a virtual environment:
|
|
8
|
+
- Windows PowerShell: `python -m venv .venv`
|
|
9
|
+
2. Activate it:
|
|
10
|
+
- `.\.venv\Scripts\Activate.ps1`
|
|
11
|
+
3. Install dependencies:
|
|
12
|
+
- `python -m pip install -U pip`
|
|
13
|
+
- `pip install -e .`
|
|
14
|
+
|
|
15
|
+
## MT5 startup config
|
|
16
|
+
|
|
17
|
+
The API initializes MetaTrader 5 when FastAPI starts and closes it when FastAPI stops.
|
|
18
|
+
It also checks MT5 connection every 5 seconds and tries to reconnect automatically if disconnected.
|
|
19
|
+
|
|
20
|
+
Set these environment variables in PowerShell before running:
|
|
21
|
+
|
|
22
|
+
```powershell
|
|
23
|
+
$env:MT5_PATH = "C:\Program Files\MetaTrader 5\terminal64.exe"
|
|
24
|
+
$env:MT5_LOGIN = "12345678"
|
|
25
|
+
$env:MT5_PASSWORD = "your-password"
|
|
26
|
+
$env:MT5_SERVER = "YourBroker-Server"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
`MT5_PATH` is optional if MT5 is already discoverable, but setting it is recommended.
|
|
30
|
+
|
|
31
|
+
## Run
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
olza-api-mt5
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Optional flags:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
olza-api-mt5 --port 9000
|
|
41
|
+
olza-api-mt5 --host 0.0.0.0 --port 8000
|
|
42
|
+
olza-api-mt5 --reload
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Default port is `8000`.
|
|
46
|
+
|
|
47
|
+
API docs:
|
|
48
|
+
- Swagger UI: `http://127.0.0.1:8000/docs`
|
|
49
|
+
- ReDoc: `http://127.0.0.1:8000/redoc`
|
|
50
|
+
|
|
51
|
+
Health endpoint:
|
|
52
|
+
- `GET http://127.0.0.1:8000/health`
|
|
53
|
+
|
|
54
|
+
## Build and publish package
|
|
55
|
+
|
|
56
|
+
1. Build distribution files:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
python -m pip install --upgrade build twine
|
|
60
|
+
python -m build
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
2. Upload to PyPI:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
python -m twine upload dist/*
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
3. Install from PyPI and run:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install olza-api-mt5
|
|
73
|
+
olza-api-mt5 --port 8000
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Optional: standalone executable (no Python required on target machine)
|
|
77
|
+
|
|
78
|
+
If you want users to run it without installing Python, build an executable:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
python -m pip install pyinstaller
|
|
82
|
+
pyinstaller --onefile --name olza-api-mt5 app/cli.py
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The executable will be in `dist/olza-api-mt5.exe`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""OLZA MT5 API package."""
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
import uvicorn
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main() -> None:
|
|
8
|
+
parser = argparse.ArgumentParser(description="Run the OLZA MT5 API server.")
|
|
9
|
+
parser.add_argument("--host", default="127.0.0.1", help="Bind host. Default: 127.0.0.1")
|
|
10
|
+
parser.add_argument("--port", type=int, default=int(os.getenv("PORT", "8000")), help="Bind port. Default: 8000")
|
|
11
|
+
parser.add_argument("--reload", action="store_true", help="Enable auto-reload for development")
|
|
12
|
+
args = parser.parse_args()
|
|
13
|
+
|
|
14
|
+
uvicorn.run("app.main:app", host=args.host, port=args.port, reload=args.reload)
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import asyncio
|
|
3
|
+
from contextlib import asynccontextmanager
|
|
4
|
+
|
|
5
|
+
import MetaTrader5 as mt5
|
|
6
|
+
from fastapi import FastAPI, HTTPException
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from app.services.account_service import get_account_info
|
|
9
|
+
from app.services.bar_service import get_last_ohlc_bars
|
|
10
|
+
from app.services.symbol_service import get_symbol_details, search_symbols
|
|
11
|
+
from app.services.ticket_service import get_all_tickets
|
|
12
|
+
from app.services.trade_service import close_by_ticket, create_market_order, list_open_positions, list_pending_orders
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CreateOrderRequest(BaseModel):
|
|
16
|
+
symbol: str
|
|
17
|
+
side: str
|
|
18
|
+
volume: float
|
|
19
|
+
orderType: str = "market"
|
|
20
|
+
price: float | None = None
|
|
21
|
+
stopLimit: float | None = None
|
|
22
|
+
deviation: int = 20
|
|
23
|
+
sl: float | None = None
|
|
24
|
+
tp: float | None = None
|
|
25
|
+
magic: int = 0
|
|
26
|
+
comment: str = "api-order"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CloseOrderRequest(BaseModel):
|
|
30
|
+
ticket: int
|
|
31
|
+
volume: float | None = None
|
|
32
|
+
deviation: int = 20
|
|
33
|
+
comment: str = "api-close"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _initialize_mt5() -> tuple[bool, str]:
|
|
37
|
+
terminal_path = os.getenv("MT5_PATH")
|
|
38
|
+
login_raw = os.getenv("MT5_LOGIN")
|
|
39
|
+
password = os.getenv("MT5_PASSWORD")
|
|
40
|
+
server = os.getenv("MT5_SERVER")
|
|
41
|
+
|
|
42
|
+
init_kwargs: dict[str, object] = {}
|
|
43
|
+
if terminal_path:
|
|
44
|
+
init_kwargs["path"] = terminal_path
|
|
45
|
+
if login_raw:
|
|
46
|
+
try:
|
|
47
|
+
init_kwargs["login"] = int(login_raw)
|
|
48
|
+
except ValueError:
|
|
49
|
+
return False, "failed: MT5_LOGIN must be a number"
|
|
50
|
+
if password:
|
|
51
|
+
init_kwargs["password"] = password
|
|
52
|
+
if server:
|
|
53
|
+
init_kwargs["server"] = server
|
|
54
|
+
|
|
55
|
+
initialized = mt5.initialize(**init_kwargs)
|
|
56
|
+
if initialized:
|
|
57
|
+
return True, "connected"
|
|
58
|
+
|
|
59
|
+
last_error = mt5.last_error()
|
|
60
|
+
return False, f"failed: {last_error}"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _check_mt5_connection() -> tuple[bool, str]:
|
|
64
|
+
terminal_info = mt5.terminal_info()
|
|
65
|
+
if terminal_info is None:
|
|
66
|
+
return False, "disconnected"
|
|
67
|
+
return True, "connected"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def _mt5_monitor_loop(app_instance: FastAPI, interval_seconds: int = 5) -> None:
|
|
71
|
+
while True:
|
|
72
|
+
connected, message = _check_mt5_connection()
|
|
73
|
+
if not connected:
|
|
74
|
+
mt5_ok, mt5_message = _initialize_mt5()
|
|
75
|
+
app_instance.state.mt5_ok = mt5_ok
|
|
76
|
+
app_instance.state.mt5_message = mt5_message
|
|
77
|
+
else:
|
|
78
|
+
app_instance.state.mt5_ok = True
|
|
79
|
+
app_instance.state.mt5_message = message
|
|
80
|
+
|
|
81
|
+
await asyncio.sleep(interval_seconds)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@asynccontextmanager
|
|
85
|
+
async def lifespan(_: FastAPI):
|
|
86
|
+
mt5_ok, mt5_message = _initialize_mt5()
|
|
87
|
+
app.state.mt5_ok = mt5_ok
|
|
88
|
+
app.state.mt5_message = mt5_message
|
|
89
|
+
monitor_task = asyncio.create_task(_mt5_monitor_loop(app))
|
|
90
|
+
try:
|
|
91
|
+
yield
|
|
92
|
+
finally:
|
|
93
|
+
monitor_task.cancel()
|
|
94
|
+
try:
|
|
95
|
+
await monitor_task
|
|
96
|
+
except asyncio.CancelledError:
|
|
97
|
+
pass
|
|
98
|
+
mt5.shutdown()
|
|
99
|
+
|
|
100
|
+
app = FastAPI(
|
|
101
|
+
title="MT5 API",
|
|
102
|
+
version="0.1.0",
|
|
103
|
+
description="Starter FastAPI service for MetaTrader 5 integration.",
|
|
104
|
+
lifespan=lifespan,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@app.get("/health")
|
|
109
|
+
def health() -> dict[str, str]:
|
|
110
|
+
return {
|
|
111
|
+
"status": "ok",
|
|
112
|
+
"mt5": "connected" if app.state.mt5_ok else app.state.mt5_message,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@app.get("/account")
|
|
117
|
+
def account() -> dict:
|
|
118
|
+
if not app.state.mt5_ok:
|
|
119
|
+
raise HTTPException(status_code=503, detail=app.state.mt5_message)
|
|
120
|
+
|
|
121
|
+
ok, data = get_account_info()
|
|
122
|
+
if not ok:
|
|
123
|
+
raise HTTPException(status_code=500, detail=str(data))
|
|
124
|
+
|
|
125
|
+
return {"account": data}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@app.get("/tickets")
|
|
129
|
+
def tickets() -> dict:
|
|
130
|
+
if not app.state.mt5_ok:
|
|
131
|
+
raise HTTPException(status_code=503, detail=app.state.mt5_message)
|
|
132
|
+
|
|
133
|
+
ok, data = get_all_tickets()
|
|
134
|
+
if not ok:
|
|
135
|
+
raise HTTPException(status_code=500, detail=str(data))
|
|
136
|
+
|
|
137
|
+
return data
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@app.get("/symbols")
|
|
141
|
+
def symbols(query: str | None = None) -> dict:
|
|
142
|
+
if not app.state.mt5_ok:
|
|
143
|
+
raise HTTPException(status_code=503, detail=app.state.mt5_message)
|
|
144
|
+
|
|
145
|
+
ok, data = search_symbols(query=query)
|
|
146
|
+
if not ok:
|
|
147
|
+
raise HTTPException(status_code=500, detail=str(data))
|
|
148
|
+
|
|
149
|
+
return data
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@app.get("/symbols/{name}")
|
|
153
|
+
def symbol_details(name: str) -> dict:
|
|
154
|
+
if not app.state.mt5_ok:
|
|
155
|
+
raise HTTPException(status_code=503, detail=app.state.mt5_message)
|
|
156
|
+
|
|
157
|
+
ok, data = get_symbol_details(name=name)
|
|
158
|
+
if not ok:
|
|
159
|
+
raise HTTPException(status_code=404, detail=str(data))
|
|
160
|
+
|
|
161
|
+
return {"symbol": data}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@app.get("/bars/{symbol}")
|
|
165
|
+
def bars(symbol: str, timeframe: str = "M1", n: int = 100, include_volume: bool = False) -> dict:
|
|
166
|
+
if not app.state.mt5_ok:
|
|
167
|
+
raise HTTPException(status_code=503, detail=app.state.mt5_message)
|
|
168
|
+
|
|
169
|
+
if n <= 0:
|
|
170
|
+
raise HTTPException(status_code=400, detail="n must be greater than 0")
|
|
171
|
+
if n > 10_000:
|
|
172
|
+
raise HTTPException(status_code=400, detail="n must be less than or equal to 10000")
|
|
173
|
+
|
|
174
|
+
ok, data = get_last_ohlc_bars(
|
|
175
|
+
symbol=symbol,
|
|
176
|
+
timeframe=timeframe,
|
|
177
|
+
count=n,
|
|
178
|
+
include_volume=include_volume,
|
|
179
|
+
)
|
|
180
|
+
if not ok:
|
|
181
|
+
error_message = str(data)
|
|
182
|
+
if "unsupported timeframe" in error_message:
|
|
183
|
+
raise HTTPException(status_code=400, detail=error_message)
|
|
184
|
+
if "symbol not found" in error_message:
|
|
185
|
+
raise HTTPException(status_code=404, detail=error_message)
|
|
186
|
+
raise HTTPException(status_code=500, detail=error_message)
|
|
187
|
+
|
|
188
|
+
return data
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@app.post("/orders")
|
|
192
|
+
def create_order(payload: CreateOrderRequest) -> dict:
|
|
193
|
+
if not app.state.mt5_ok:
|
|
194
|
+
raise HTTPException(status_code=503, detail=app.state.mt5_message)
|
|
195
|
+
|
|
196
|
+
ok, data = create_market_order(
|
|
197
|
+
symbol=payload.symbol,
|
|
198
|
+
side=payload.side,
|
|
199
|
+
volume=payload.volume,
|
|
200
|
+
orderType=payload.orderType,
|
|
201
|
+
price=payload.price,
|
|
202
|
+
stopLimit=payload.stopLimit,
|
|
203
|
+
deviation=payload.deviation,
|
|
204
|
+
sl=payload.sl,
|
|
205
|
+
tp=payload.tp,
|
|
206
|
+
magic=payload.magic,
|
|
207
|
+
comment=payload.comment,
|
|
208
|
+
)
|
|
209
|
+
if not ok:
|
|
210
|
+
error_message = str(data)
|
|
211
|
+
if (
|
|
212
|
+
"side must be" in error_message
|
|
213
|
+
or "volume must be" in error_message
|
|
214
|
+
or "orderType must be" in error_message
|
|
215
|
+
or "required for" in error_message
|
|
216
|
+
):
|
|
217
|
+
raise HTTPException(status_code=400, detail=error_message)
|
|
218
|
+
if "symbol not found" in error_message:
|
|
219
|
+
raise HTTPException(status_code=404, detail=error_message)
|
|
220
|
+
raise HTTPException(status_code=500, detail=error_message)
|
|
221
|
+
|
|
222
|
+
return data
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@app.get("/positions/open")
|
|
226
|
+
def open_positions() -> dict:
|
|
227
|
+
if not app.state.mt5_ok:
|
|
228
|
+
raise HTTPException(status_code=503, detail=app.state.mt5_message)
|
|
229
|
+
|
|
230
|
+
ok, data = list_open_positions()
|
|
231
|
+
if not ok:
|
|
232
|
+
raise HTTPException(status_code=500, detail=str(data))
|
|
233
|
+
|
|
234
|
+
return data
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@app.get("/orders/pending")
|
|
238
|
+
def pending_orders() -> dict:
|
|
239
|
+
if not app.state.mt5_ok:
|
|
240
|
+
raise HTTPException(status_code=503, detail=app.state.mt5_message)
|
|
241
|
+
|
|
242
|
+
ok, data = list_pending_orders()
|
|
243
|
+
if not ok:
|
|
244
|
+
raise HTTPException(status_code=500, detail=str(data))
|
|
245
|
+
|
|
246
|
+
return data
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
@app.post("/orders/close")
|
|
250
|
+
def close_order(payload: CloseOrderRequest) -> dict:
|
|
251
|
+
if not app.state.mt5_ok:
|
|
252
|
+
raise HTTPException(status_code=503, detail=app.state.mt5_message)
|
|
253
|
+
|
|
254
|
+
ok, data = close_by_ticket(
|
|
255
|
+
ticket=payload.ticket,
|
|
256
|
+
volume=payload.volume,
|
|
257
|
+
deviation=payload.deviation,
|
|
258
|
+
comment=payload.comment,
|
|
259
|
+
)
|
|
260
|
+
if not ok:
|
|
261
|
+
error_message = str(data)
|
|
262
|
+
if "ticket must be" in error_message or "volume " in error_message:
|
|
263
|
+
raise HTTPException(status_code=400, detail=error_message)
|
|
264
|
+
if "ticket not found" in error_message:
|
|
265
|
+
raise HTTPException(status_code=404, detail=error_message)
|
|
266
|
+
raise HTTPException(status_code=500, detail=error_message)
|
|
267
|
+
|
|
268
|
+
return data
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Service layer for OLZA MT5 API."""
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import MetaTrader5 as mt5
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_account_info() -> tuple[bool, dict[str, Any] | str]:
|
|
7
|
+
"""
|
|
8
|
+
Fetch account information from the active MT5 session.
|
|
9
|
+
|
|
10
|
+
Returns:
|
|
11
|
+
(True, account_info_dict) on success
|
|
12
|
+
(False, error_message) on failure
|
|
13
|
+
"""
|
|
14
|
+
account_info = mt5.account_info()
|
|
15
|
+
if account_info is None:
|
|
16
|
+
return False, f"failed to fetch account info: {mt5.last_error()}"
|
|
17
|
+
|
|
18
|
+
return True, account_info._asdict()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import MetaTrader5 as mt5
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
_TIMEFRAME_MAP: dict[str, int] = {
|
|
8
|
+
"M1": mt5.TIMEFRAME_M1,
|
|
9
|
+
"M2": mt5.TIMEFRAME_M2,
|
|
10
|
+
"M3": mt5.TIMEFRAME_M3,
|
|
11
|
+
"M4": mt5.TIMEFRAME_M4,
|
|
12
|
+
"M5": mt5.TIMEFRAME_M5,
|
|
13
|
+
"M6": mt5.TIMEFRAME_M6,
|
|
14
|
+
"M10": mt5.TIMEFRAME_M10,
|
|
15
|
+
"M12": mt5.TIMEFRAME_M12,
|
|
16
|
+
"M15": mt5.TIMEFRAME_M15,
|
|
17
|
+
"M20": mt5.TIMEFRAME_M20,
|
|
18
|
+
"M30": mt5.TIMEFRAME_M30,
|
|
19
|
+
"H1": mt5.TIMEFRAME_H1,
|
|
20
|
+
"H2": mt5.TIMEFRAME_H2,
|
|
21
|
+
"H3": mt5.TIMEFRAME_H3,
|
|
22
|
+
"H4": mt5.TIMEFRAME_H4,
|
|
23
|
+
"H6": mt5.TIMEFRAME_H6,
|
|
24
|
+
"H8": mt5.TIMEFRAME_H8,
|
|
25
|
+
"H12": mt5.TIMEFRAME_H12,
|
|
26
|
+
"D1": mt5.TIMEFRAME_D1,
|
|
27
|
+
"W1": mt5.TIMEFRAME_W1,
|
|
28
|
+
"MN1": mt5.TIMEFRAME_MN1,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_last_ohlc_bars(
|
|
33
|
+
symbol: str,
|
|
34
|
+
timeframe: str,
|
|
35
|
+
count: int,
|
|
36
|
+
include_volume: bool = False,
|
|
37
|
+
) -> tuple[bool, dict[str, Any] | str]:
|
|
38
|
+
"""
|
|
39
|
+
Fetch the last N OHLC bars for a symbol and timeframe.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
(True, payload) on success
|
|
43
|
+
(False, error_message) on failure
|
|
44
|
+
"""
|
|
45
|
+
timeframe_value = _TIMEFRAME_MAP.get(timeframe.upper())
|
|
46
|
+
if timeframe_value is None:
|
|
47
|
+
supported = ", ".join(_TIMEFRAME_MAP.keys())
|
|
48
|
+
return False, f"unsupported timeframe '{timeframe}'. supported: {supported}"
|
|
49
|
+
|
|
50
|
+
symbol_info = mt5.symbol_info(symbol)
|
|
51
|
+
if symbol_info is None:
|
|
52
|
+
return False, f"symbol not found or unavailable: {symbol}"
|
|
53
|
+
|
|
54
|
+
if not symbol_info.visible and not mt5.symbol_select(symbol, True):
|
|
55
|
+
return False, f"failed to select symbol '{symbol}': {mt5.last_error()}"
|
|
56
|
+
|
|
57
|
+
rates = mt5.copy_rates_from_pos(symbol, timeframe_value, 0, count)
|
|
58
|
+
if rates is None:
|
|
59
|
+
return False, f"failed to fetch bars: {mt5.last_error()}"
|
|
60
|
+
|
|
61
|
+
bars: list[dict[str, Any]] = []
|
|
62
|
+
for rate in rates:
|
|
63
|
+
bar = {
|
|
64
|
+
"time": datetime.fromtimestamp(int(rate["time"]), tz=timezone.utc).isoformat(),
|
|
65
|
+
"open": float(rate["open"]),
|
|
66
|
+
"high": float(rate["high"]),
|
|
67
|
+
"low": float(rate["low"]),
|
|
68
|
+
"close": float(rate["close"]),
|
|
69
|
+
}
|
|
70
|
+
if include_volume:
|
|
71
|
+
bar["volume"] = int(rate["tick_volume"])
|
|
72
|
+
bars.append(bar)
|
|
73
|
+
|
|
74
|
+
return True, {
|
|
75
|
+
"symbol": symbol,
|
|
76
|
+
"timeframe": timeframe.upper(),
|
|
77
|
+
"count": len(bars),
|
|
78
|
+
"bars": bars,
|
|
79
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import MetaTrader5 as mt5
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def search_symbols(query: str | None = None) -> tuple[bool, dict[str, Any] | str]:
|
|
7
|
+
"""
|
|
8
|
+
Fetch all available symbols, optionally filtered by substring.
|
|
9
|
+
|
|
10
|
+
Returns:
|
|
11
|
+
(True, payload) on success
|
|
12
|
+
(False, error_message) on failure
|
|
13
|
+
"""
|
|
14
|
+
symbols = mt5.symbols_get()
|
|
15
|
+
if symbols is None:
|
|
16
|
+
return False, f"failed to fetch symbols: {mt5.last_error()}"
|
|
17
|
+
|
|
18
|
+
if query:
|
|
19
|
+
lowered_query = query.lower()
|
|
20
|
+
filtered_symbols = [symbol.name for symbol in symbols if lowered_query in symbol.name.lower()]
|
|
21
|
+
else:
|
|
22
|
+
filtered_symbols = [symbol.name for symbol in symbols]
|
|
23
|
+
|
|
24
|
+
return True, {
|
|
25
|
+
"query": query,
|
|
26
|
+
"count": len(filtered_symbols),
|
|
27
|
+
"symbols": filtered_symbols,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_symbol_details(name: str) -> tuple[bool, dict[str, Any] | str]:
|
|
32
|
+
"""
|
|
33
|
+
Fetch details for a single symbol by its exact name.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
(True, payload) on success
|
|
37
|
+
(False, error_message) on failure
|
|
38
|
+
"""
|
|
39
|
+
symbol_info = mt5.symbol_info(name)
|
|
40
|
+
if symbol_info is None:
|
|
41
|
+
return False, f"symbol not found or unavailable: {name}"
|
|
42
|
+
|
|
43
|
+
return True, symbol_info._asdict()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import MetaTrader5 as mt5
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_all_tickets() -> tuple[bool, dict[str, Any] | str]:
|
|
7
|
+
"""
|
|
8
|
+
Fetch currently available MT5 tickets from open positions and active orders.
|
|
9
|
+
|
|
10
|
+
Returns:
|
|
11
|
+
(True, payload) on success
|
|
12
|
+
(False, error_message) on failure
|
|
13
|
+
"""
|
|
14
|
+
positions = mt5.positions_get()
|
|
15
|
+
if positions is None:
|
|
16
|
+
return False, f"failed to fetch positions: {mt5.last_error()}"
|
|
17
|
+
|
|
18
|
+
orders = mt5.orders_get()
|
|
19
|
+
if orders is None:
|
|
20
|
+
return False, f"failed to fetch orders: {mt5.last_error()}"
|
|
21
|
+
|
|
22
|
+
position_tickets = [int(position.ticket) for position in positions]
|
|
23
|
+
order_tickets = [int(order.ticket) for order in orders]
|
|
24
|
+
|
|
25
|
+
return True, {
|
|
26
|
+
"position_tickets": position_tickets,
|
|
27
|
+
"order_tickets": order_tickets,
|
|
28
|
+
"total": len(position_tickets) + len(order_tickets),
|
|
29
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import MetaTrader5 as mt5
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _to_jsonable(value: Any) -> Any:
|
|
7
|
+
if hasattr(value, "_asdict"):
|
|
8
|
+
return {k: _to_jsonable(v) for k, v in value._asdict().items()}
|
|
9
|
+
if isinstance(value, dict):
|
|
10
|
+
return {k: _to_jsonable(v) for k, v in value.items()}
|
|
11
|
+
if isinstance(value, (list, tuple)):
|
|
12
|
+
return [_to_jsonable(item) for item in value]
|
|
13
|
+
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
14
|
+
return value
|
|
15
|
+
return str(value)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _ensure_symbol(symbol: str) -> tuple[bool, str | Any]:
|
|
19
|
+
info = mt5.symbol_info(symbol)
|
|
20
|
+
if info is None:
|
|
21
|
+
return False, f"symbol not found or unavailable: {symbol}"
|
|
22
|
+
|
|
23
|
+
if not info.visible and not mt5.symbol_select(symbol, True):
|
|
24
|
+
return False, f"failed to select symbol '{symbol}': {mt5.last_error()}"
|
|
25
|
+
|
|
26
|
+
return True, info
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _resolve_filling_mode(info: Any) -> int:
|
|
30
|
+
mode = int(info.filling_mode)
|
|
31
|
+
allowed = {mt5.ORDER_FILLING_FOK, mt5.ORDER_FILLING_IOC, mt5.ORDER_FILLING_RETURN}
|
|
32
|
+
if mode in allowed:
|
|
33
|
+
return mode
|
|
34
|
+
return mt5.ORDER_FILLING_RETURN
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _filling_candidates(info: Any) -> list[int]:
|
|
38
|
+
primary = _resolve_filling_mode(info)
|
|
39
|
+
fallback_order = [primary, mt5.ORDER_FILLING_RETURN, mt5.ORDER_FILLING_IOC, mt5.ORDER_FILLING_FOK]
|
|
40
|
+
candidates: list[int] = []
|
|
41
|
+
for filling in fallback_order:
|
|
42
|
+
if filling not in candidates:
|
|
43
|
+
candidates.append(filling)
|
|
44
|
+
return candidates
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _send_with_filling_fallback(request: dict[str, Any], info: Any) -> Any:
|
|
48
|
+
last_result = None
|
|
49
|
+
for filling in _filling_candidates(info):
|
|
50
|
+
req = dict(request)
|
|
51
|
+
req["type_filling"] = filling
|
|
52
|
+
result = mt5.order_send(req)
|
|
53
|
+
if result is None:
|
|
54
|
+
last_result = None
|
|
55
|
+
continue
|
|
56
|
+
last_result = result
|
|
57
|
+
if int(getattr(result, "retcode", -1)) != mt5.TRADE_RETCODE_INVALID_FILL:
|
|
58
|
+
return result
|
|
59
|
+
return last_result
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def create_market_order(
|
|
63
|
+
symbol: str,
|
|
64
|
+
side: str,
|
|
65
|
+
volume: float,
|
|
66
|
+
orderType: str = "market",
|
|
67
|
+
price: float | None = None,
|
|
68
|
+
stopLimit: float | None = None,
|
|
69
|
+
deviation: int = 20,
|
|
70
|
+
sl: float | None = None,
|
|
71
|
+
tp: float | None = None,
|
|
72
|
+
magic: int = 0,
|
|
73
|
+
comment: str = "api-order",
|
|
74
|
+
) -> tuple[bool, dict[str, Any] | str]:
|
|
75
|
+
normalized_side = side.lower()
|
|
76
|
+
if normalized_side not in {"buy", "sell"}:
|
|
77
|
+
return False, "side must be 'buy' or 'sell'"
|
|
78
|
+
if volume <= 0:
|
|
79
|
+
return False, "volume must be greater than 0"
|
|
80
|
+
|
|
81
|
+
orderTypeKey = orderType.replace("_", "").lower()
|
|
82
|
+
orderTypeMap = {
|
|
83
|
+
"market": "market",
|
|
84
|
+
"limit": "limit",
|
|
85
|
+
"stop": "stop",
|
|
86
|
+
"stoplimit": "stopLimit",
|
|
87
|
+
}
|
|
88
|
+
normalized_order_type = orderTypeMap.get(orderTypeKey)
|
|
89
|
+
supportedKinds = {"market", "limit", "stop", "stopLimit"}
|
|
90
|
+
if normalized_order_type not in supportedKinds:
|
|
91
|
+
return False, "orderType must be one of: market, limit, stop, stopLimit"
|
|
92
|
+
|
|
93
|
+
ok, info_or_error = _ensure_symbol(symbol)
|
|
94
|
+
if not ok:
|
|
95
|
+
return False, str(info_or_error)
|
|
96
|
+
symbol_info = info_or_error
|
|
97
|
+
|
|
98
|
+
tick = mt5.symbol_info_tick(symbol)
|
|
99
|
+
if tick is None:
|
|
100
|
+
return False, f"failed to fetch tick for symbol '{symbol}': {mt5.last_error()}"
|
|
101
|
+
|
|
102
|
+
is_buy = normalized_side == "buy"
|
|
103
|
+
|
|
104
|
+
if normalized_order_type == "market":
|
|
105
|
+
action = mt5.TRADE_ACTION_DEAL
|
|
106
|
+
orderTypeValue = mt5.ORDER_TYPE_BUY if is_buy else mt5.ORDER_TYPE_SELL
|
|
107
|
+
order_price = float(tick.ask if is_buy else tick.bid)
|
|
108
|
+
elif normalized_order_type == "limit":
|
|
109
|
+
if price is None:
|
|
110
|
+
return False, "price is required for limit orders"
|
|
111
|
+
action = mt5.TRADE_ACTION_PENDING
|
|
112
|
+
orderTypeValue = mt5.ORDER_TYPE_BUY_LIMIT if is_buy else mt5.ORDER_TYPE_SELL_LIMIT
|
|
113
|
+
order_price = float(price)
|
|
114
|
+
elif normalized_order_type == "stop":
|
|
115
|
+
if price is None:
|
|
116
|
+
return False, "price is required for stop orders"
|
|
117
|
+
action = mt5.TRADE_ACTION_PENDING
|
|
118
|
+
orderTypeValue = mt5.ORDER_TYPE_BUY_STOP if is_buy else mt5.ORDER_TYPE_SELL_STOP
|
|
119
|
+
order_price = float(price)
|
|
120
|
+
else:
|
|
121
|
+
if price is None:
|
|
122
|
+
return False, "price is required for stopLimit orders"
|
|
123
|
+
if stopLimit is None:
|
|
124
|
+
return False, "stopLimit is required for stopLimit orders"
|
|
125
|
+
action = mt5.TRADE_ACTION_PENDING
|
|
126
|
+
orderTypeValue = mt5.ORDER_TYPE_BUY_STOP_LIMIT if is_buy else mt5.ORDER_TYPE_SELL_STOP_LIMIT
|
|
127
|
+
order_price = float(price)
|
|
128
|
+
|
|
129
|
+
request: dict[str, Any] = {
|
|
130
|
+
"action": action,
|
|
131
|
+
"symbol": symbol,
|
|
132
|
+
"volume": float(volume),
|
|
133
|
+
"type": orderTypeValue,
|
|
134
|
+
"price": order_price,
|
|
135
|
+
"deviation": int(deviation),
|
|
136
|
+
"magic": int(magic),
|
|
137
|
+
"comment": comment,
|
|
138
|
+
"type_time": mt5.ORDER_TIME_GTC,
|
|
139
|
+
}
|
|
140
|
+
if normalized_order_type == "stopLimit":
|
|
141
|
+
request["stoplimit"] = float(stopLimit)
|
|
142
|
+
if sl is not None:
|
|
143
|
+
request["sl"] = float(sl)
|
|
144
|
+
if tp is not None:
|
|
145
|
+
request["tp"] = float(tp)
|
|
146
|
+
|
|
147
|
+
result = _send_with_filling_fallback(request, symbol_info)
|
|
148
|
+
if result is None:
|
|
149
|
+
return False, f"order_send failed: {mt5.last_error()}"
|
|
150
|
+
|
|
151
|
+
result_dict = _to_jsonable(result)
|
|
152
|
+
result_comment = str(getattr(result, "comment", ""))
|
|
153
|
+
return True, {
|
|
154
|
+
"retcode": result.retcode,
|
|
155
|
+
"success": result.retcode == mt5.TRADE_RETCODE_DONE,
|
|
156
|
+
"message": result_comment,
|
|
157
|
+
"hint": (
|
|
158
|
+
"Enable AutoTrading in the MT5 terminal and allow algorithmic trading."
|
|
159
|
+
if "AutoTrading disabled by client" in result_comment
|
|
160
|
+
else None
|
|
161
|
+
),
|
|
162
|
+
"result": result_dict,
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def list_open_positions() -> tuple[bool, dict[str, Any] | str]:
|
|
167
|
+
positions = mt5.positions_get()
|
|
168
|
+
if positions is None:
|
|
169
|
+
return False, f"failed to fetch positions: {mt5.last_error()}"
|
|
170
|
+
|
|
171
|
+
payload = [_to_jsonable(position) for position in positions]
|
|
172
|
+
return True, {
|
|
173
|
+
"count": len(payload),
|
|
174
|
+
"positions": payload,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def list_pending_orders() -> tuple[bool, dict[str, Any] | str]:
|
|
179
|
+
orders = mt5.orders_get()
|
|
180
|
+
if orders is None:
|
|
181
|
+
return False, f"failed to fetch pending orders: {mt5.last_error()}"
|
|
182
|
+
|
|
183
|
+
payload = [_to_jsonable(order) for order in orders]
|
|
184
|
+
return True, {
|
|
185
|
+
"count": len(payload),
|
|
186
|
+
"orders": payload,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def close_by_ticket(
|
|
191
|
+
ticket: int,
|
|
192
|
+
volume: float | None = None,
|
|
193
|
+
deviation: int = 20,
|
|
194
|
+
comment: str = "api-close",
|
|
195
|
+
) -> tuple[bool, dict[str, Any] | str]:
|
|
196
|
+
if ticket <= 0:
|
|
197
|
+
return False, "ticket must be greater than 0"
|
|
198
|
+
|
|
199
|
+
pending_orders = mt5.orders_get(ticket=ticket)
|
|
200
|
+
if pending_orders is not None and len(pending_orders) > 0:
|
|
201
|
+
remove_request = {
|
|
202
|
+
"action": mt5.TRADE_ACTION_REMOVE,
|
|
203
|
+
"order": int(ticket),
|
|
204
|
+
"comment": comment,
|
|
205
|
+
}
|
|
206
|
+
remove_result = mt5.order_send(remove_request)
|
|
207
|
+
if remove_result is None:
|
|
208
|
+
return False, f"failed to cancel order: {mt5.last_error()}"
|
|
209
|
+
return True, {
|
|
210
|
+
"operation": "cancel_pending_order",
|
|
211
|
+
"retcode": remove_result.retcode,
|
|
212
|
+
"success": remove_result.retcode == mt5.TRADE_RETCODE_DONE,
|
|
213
|
+
"result": _to_jsonable(remove_result),
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
positions = mt5.positions_get(ticket=ticket)
|
|
217
|
+
if positions is None:
|
|
218
|
+
return False, f"failed to fetch ticket {ticket}: {mt5.last_error()}"
|
|
219
|
+
if len(positions) == 0:
|
|
220
|
+
return False, f"ticket not found in open positions or pending orders: {ticket}"
|
|
221
|
+
|
|
222
|
+
position = positions[0]
|
|
223
|
+
close_volume = float(volume) if volume is not None else float(position.volume)
|
|
224
|
+
if close_volume <= 0:
|
|
225
|
+
return False, "volume must be greater than 0"
|
|
226
|
+
if close_volume > float(position.volume):
|
|
227
|
+
return False, f"volume exceeds position volume ({position.volume})"
|
|
228
|
+
|
|
229
|
+
symbol = str(position.symbol)
|
|
230
|
+
ok, info_or_error = _ensure_symbol(symbol)
|
|
231
|
+
if not ok:
|
|
232
|
+
return False, str(info_or_error)
|
|
233
|
+
symbol_info = info_or_error
|
|
234
|
+
|
|
235
|
+
tick = mt5.symbol_info_tick(symbol)
|
|
236
|
+
if tick is None:
|
|
237
|
+
return False, f"failed to fetch tick for symbol '{symbol}': {mt5.last_error()}"
|
|
238
|
+
|
|
239
|
+
is_buy_position = int(position.type) == mt5.POSITION_TYPE_BUY
|
|
240
|
+
close_type = mt5.ORDER_TYPE_SELL if is_buy_position else mt5.ORDER_TYPE_BUY
|
|
241
|
+
close_price = float(tick.bid if is_buy_position else tick.ask)
|
|
242
|
+
|
|
243
|
+
close_request: dict[str, Any] = {
|
|
244
|
+
"action": mt5.TRADE_ACTION_DEAL,
|
|
245
|
+
"position": int(position.ticket),
|
|
246
|
+
"symbol": symbol,
|
|
247
|
+
"volume": close_volume,
|
|
248
|
+
"type": close_type,
|
|
249
|
+
"price": close_price,
|
|
250
|
+
"deviation": int(deviation),
|
|
251
|
+
"magic": int(position.magic),
|
|
252
|
+
"comment": comment,
|
|
253
|
+
"type_time": mt5.ORDER_TIME_GTC,
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
close_result = _send_with_filling_fallback(close_request, symbol_info)
|
|
257
|
+
if close_result is None:
|
|
258
|
+
return False, f"failed to close position: {mt5.last_error()}"
|
|
259
|
+
|
|
260
|
+
return True, {
|
|
261
|
+
"operation": "close_position",
|
|
262
|
+
"retcode": close_result.retcode,
|
|
263
|
+
"success": close_result.retcode == mt5.TRADE_RETCODE_DONE,
|
|
264
|
+
"result": _to_jsonable(close_result),
|
|
265
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: olza-api-mt5
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: REST and WebSocket API project for MetaTrader 5
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: fastapi<1.0.0,>=0.116.0
|
|
8
|
+
Requires-Dist: MetaTrader5<6.0.0,>=5.0.0
|
|
9
|
+
Requires-Dist: uvicorn[standard]<1.0.0,>=0.35.0
|
|
10
|
+
|
|
11
|
+
# python-api-mt5
|
|
12
|
+
|
|
13
|
+
Starter project for a MetaTrader 5 API service with FastAPI.
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
1. Create a virtual environment:
|
|
18
|
+
- Windows PowerShell: `python -m venv .venv`
|
|
19
|
+
2. Activate it:
|
|
20
|
+
- `.\.venv\Scripts\Activate.ps1`
|
|
21
|
+
3. Install dependencies:
|
|
22
|
+
- `python -m pip install -U pip`
|
|
23
|
+
- `pip install -e .`
|
|
24
|
+
|
|
25
|
+
## MT5 startup config
|
|
26
|
+
|
|
27
|
+
The API initializes MetaTrader 5 when FastAPI starts and closes it when FastAPI stops.
|
|
28
|
+
It also checks MT5 connection every 5 seconds and tries to reconnect automatically if disconnected.
|
|
29
|
+
|
|
30
|
+
Set these environment variables in PowerShell before running:
|
|
31
|
+
|
|
32
|
+
```powershell
|
|
33
|
+
$env:MT5_PATH = "C:\Program Files\MetaTrader 5\terminal64.exe"
|
|
34
|
+
$env:MT5_LOGIN = "12345678"
|
|
35
|
+
$env:MT5_PASSWORD = "your-password"
|
|
36
|
+
$env:MT5_SERVER = "YourBroker-Server"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`MT5_PATH` is optional if MT5 is already discoverable, but setting it is recommended.
|
|
40
|
+
|
|
41
|
+
## Run
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
olza-api-mt5
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Optional flags:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
olza-api-mt5 --port 9000
|
|
51
|
+
olza-api-mt5 --host 0.0.0.0 --port 8000
|
|
52
|
+
olza-api-mt5 --reload
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Default port is `8000`.
|
|
56
|
+
|
|
57
|
+
API docs:
|
|
58
|
+
- Swagger UI: `http://127.0.0.1:8000/docs`
|
|
59
|
+
- ReDoc: `http://127.0.0.1:8000/redoc`
|
|
60
|
+
|
|
61
|
+
Health endpoint:
|
|
62
|
+
- `GET http://127.0.0.1:8000/health`
|
|
63
|
+
|
|
64
|
+
## Build and publish package
|
|
65
|
+
|
|
66
|
+
1. Build distribution files:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
python -m pip install --upgrade build twine
|
|
70
|
+
python -m build
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
2. Upload to PyPI:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
python -m twine upload dist/*
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
3. Install from PyPI and run:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pip install olza-api-mt5
|
|
83
|
+
olza-api-mt5 --port 8000
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Optional: standalone executable (no Python required on target machine)
|
|
87
|
+
|
|
88
|
+
If you want users to run it without installing Python, build an executable:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
python -m pip install pyinstaller
|
|
92
|
+
pyinstaller --onefile --name olza-api-mt5 app/cli.py
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The executable will be in `dist/olza-api-mt5.exe`.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
app/__init__.py
|
|
4
|
+
app/cli.py
|
|
5
|
+
app/main.py
|
|
6
|
+
app/services/__init__.py
|
|
7
|
+
app/services/account_service.py
|
|
8
|
+
app/services/bar_service.py
|
|
9
|
+
app/services/symbol_service.py
|
|
10
|
+
app/services/ticket_service.py
|
|
11
|
+
app/services/trade_service.py
|
|
12
|
+
olza_api_mt5.egg-info/PKG-INFO
|
|
13
|
+
olza_api_mt5.egg-info/SOURCES.txt
|
|
14
|
+
olza_api_mt5.egg-info/dependency_links.txt
|
|
15
|
+
olza_api_mt5.egg-info/entry_points.txt
|
|
16
|
+
olza_api_mt5.egg-info/requires.txt
|
|
17
|
+
olza_api_mt5.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
app
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "olza-api-mt5"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "REST and WebSocket API project for MetaTrader 5"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"fastapi>=0.116.0,<1.0.0",
|
|
9
|
+
"MetaTrader5>=5.0.0,<6.0.0",
|
|
10
|
+
"uvicorn[standard]>=0.35.0,<1.0.0",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[project.scripts]
|
|
14
|
+
olza-api-mt5 = "app.cli:main"
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["setuptools>=68", "wheel"]
|
|
18
|
+
build-backend = "setuptools.build_meta"
|