| backend/
|
| ├─ main.py
|
| ├─ engine.py
|
| ├─ mt5_adapter.py
|
| ├─ strategies.py
|
| ├─ backtest.py
|
| ├─ requirements.txt
|
| └─ .env.example
|
|
|
| from fastapi import FastAPI, BackgroundTasks
|
| from pydantic import BaseModel
|
| from dotenv import load_dotenv
|
| import os
|
|
|
| load_dotenv()
|
|
|
| from engine import Engine
|
|
|
| app = FastAPI(title="FX Bot - Demo (MT5 Adapter)")
|
| engine = Engine()
|
|
|
| class StartRequest(BaseModel):
|
| adapter: str = "mt5"
|
| demo: bool = True
|
|
|
| @app.post("/start")
|
| async def start(req: StartRequest, bg: BackgroundTasks):
|
| if engine.running:
|
| return {"ok": False, "msg": "Engine already running"}
|
| engine.select_adapter(req.adapter, demo=req.demo)
|
| bg.add_task(engine.run)
|
| return {"ok": True, "adapter": req.adapter}
|
|
|
| @app.post("/stop")
|
| def stop():
|
| engine.stop()
|
| return {"ok": True}
|
|
|
| @app.get("/status")
|
| def status():
|
| return engine.get_status()
|
|
|
| @app.post("/backtest")
|
| def backtest(payload: dict):
|
| return engine.backtest(payload)
|
| import asyncio
|
| from datetime import datetime
|
| from mt5_adapter import MT5Adapter
|
| from strategies import ema_atr_strategy
|
|
|
| class Engine:
|
| def _init_(self):
|
| self.adapter = None
|
| self.running = False
|
| self.equity = 10000.0
|
| self.open_positions = []
|
| self.logs = []
|
| self.adapter_name = None
|
| self._stop_requested = False
|
| self.max_concurrent = 2
|
| self.risk_per_trade = 0.005
|
|
|
| def select_adapter(self, name: str, demo: bool=True):
|
| name = name.lower()
|
| self.adapter_name = name
|
| if name == "mt5":
|
| self.adapter = MT5Adapter(demo=demo, engine=self)
|
| else:
|
| raise ValueError("Unknown adapter")
|
|
|
| async def _run_loop(self):
|
| self.running = True
|
| self._stop_requested = False
|
| self.logs.append(f"{datetime.utcnow().isoformat()}Z starting engine with {self.adapter_name}")
|
| await self.adapter.connect()
|
| try:
|
| while not self._stop_requested:
|
| df = await self.adapter.get_latest_candles("EURUSD", "M15")
|
| signal = ema_atr_strategy(df)
|
| if signal != 0:
|
| if len(self.open_positions) < self.max_concurrent:
|
| volume = 0.01
|
| order = await self.adapter.create_market_order("EURUSD", signal, volume=volume)
|
| self.open_positions.append(order)
|
| self.logs.append(f"{datetime.utcnow().isoformat()}Z order placed: {order}")
|
| await asyncio.sleep(5)
|
| finally:
|
| await self.adapter.disconnect()
|
| self.running = False
|
| self.logs.append(f"{datetime.utcnow().isoformat()}Z engine stopped")
|
|
|
| def run(self):
|
| loop = asyncio.new_event_loop()
|
| asyncio.set_event_loop(loop)
|
| loop.run_until_complete(self._run_loop())
|
| loop.close()
|
|
|
| def stop(self):
|
| self._stop_requested = True
|
|
|
| def get_status(self):
|
| return {
|
| "running": self.running,
|
| "mode": self.adapter_name,
|
| "equity": self.equity,
|
| "open_positions": len(self.open_positions),
|
| "logs": self.logs[-200:]
|
| }
|
|
|
| def backtest(self, payload):
|
| from backtest import run_backtest
|
| return run_backtest(payload)
|
| import asyncio
|
| from datetime import datetime
|
| from mt5_adapter import MT5Adapter
|
| from strategies import ema_atr_strategy
|
|
|
| class Engine:
|
| def _init_(self):
|
| self.adapter = None
|
| self.running = False
|
| self.equity = 10000.0
|
| self.open_positions = []
|
| self.logs = []
|
| self.adapter_name = None
|
| self._stop_requested = False
|
| self.max_concurrent = 2
|
| self.risk_per_trade = 0.005
|
|
|
| def select_adapter(self, name: str, demo: bool=True):
|
| name = name.lower()
|
| self.adapter_name = name
|
| if name == "mt5":
|
| self.adapter = MT5Adapter(demo=demo, engine=self)
|
| else:
|
| raise ValueError("Unknown adapter")
|
|
|
| async def _run_loop(self):
|
| self.running = True
|
| …
|
| import os, time, pandas as pd
|
| import MetaTrader5 as mt5
|
| from datetime import datetime
|
|
|
| class MT5Adapter:
|
| def _init_(self, demo=True, engine=None):
|
| self.demo = demo
|
| self.engine = engine
|
| self.login = int(os.getenv("MT5_LOGIN") or 0)
|
| self.password = os.getenv("MT5_PASSWORD")
|
| self.server = os.getenv("MT5_SERVER")
|
|
|
| async def connect(self):
|
| if not mt5.initialize():
|
| raise RuntimeError("MT5 initialize failed")
|
| if self.login:
|
| logged = mt5.login(self.login, password=self.password, server=self.server)
|
| if not logged:
|
| last = mt5.last_error()
|
| raise RuntimeError(f"MT5 login failed: {last}")
|
| time.sleep(1)
|
| return True
|
|
|
| async …
|
| import numpy as np
|
|
|
| def ema(series, n):
|
| return series.ewm(span=n, adjust=False).mean()
|
|
|
| def atr(df, n=14):
|
| h, l, c = df["high"], df["low"], df["close"]
|
| prev = c.shift(1)
|
| tr = np.maximum(h - l, np.maximum((h - prev).abs(), (l - prev).abs()))
|
| return tr.rolling(n).mean()
|
|
|
| def ema_atr_strategy(df):
|
| df = df.copy().dropna()
|
| if len(df) < 60:
|
| return 0
|
| df["ema_fast"] = ema(df["close"], 20)
|
| df["ema_slow"] = ema(df["close"], 50)
|
| df["atr"] = atr(df, 14)
|
| last = df.iloc[-1]
|
| prev = df.iloc[-2]
|
| if last["ema_fast"] > last["ema_slow"] and prev["ema_fast"] <= prev["ema_slow"]:
|
| return 1
|
| if last["ema_fast"] < last["ema_slow"] and prev["ema_fast"] >= prev["ema_slow"]:
|
| return -1
|
| return 0
|
| import pandas as pd
|
| import numpy as np
|
|
|
| def run_backtest(payload):
|
| start = payload.get("start", "2024-01-01")
|
| end = payload.get("end", "2024-02-01")
|
| freq = "15T"
|
| rng = pd.date_range(start=start, end=end, freq=freq)
|
| px = 1.0 + np.cumsum(np.random.normal(0, 0.0005, len(rng)))
|
| df = pd.DataFrame({"time": rng, "open": px, "high": px + 0.0005, "low": px - 0.0005, "close": px}).set_index("time")
|
| trades = []
|
| for i in range(5):
|
| trades.append({"time": str(rng[i]), "pnl": float(np.random.normal(5,20))})
|
| stats = {"trades": len(trades), "net_pnl": sum(t["pnl"] for t in trades), "win_rate": 0.5}
|
| return {"stats": stats, "trades": trades}
|
| fastapi
|
| uvicorn[standard]
|
| pydantic
|
| numpy
|
| pandas
|
| python-dotenv
|
| MetaTrader5
|
| requests
|
| websockets |