Initial commit
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
# MT5 credentials (fill locally; never commit .env)
|
||||||
|
MT5_LOGIN=1234567
|
||||||
|
MT5_PASSWORD=change_me
|
||||||
|
MT5_SERVER=YourBroker-Server
|
||||||
|
|
||||||
|
|
||||||
|
# Core
|
||||||
|
TRAINING_SYMBOL=EURUSD
|
||||||
|
TIMEFRAME=M15
|
||||||
|
SPLIT_RATIO=0.8
|
||||||
|
DRY_RUN=true
|
||||||
|
|
||||||
|
# Optional data range
|
||||||
|
DATA_START=2023-01-01
|
||||||
|
DATA_END=2025-01-01
|
||||||
|
|
||||||
|
# Trading meta
|
||||||
|
MAGIC_NUMBER=234002
|
||||||
|
DEVIATION=20
|
||||||
|
VOLUME=0.01
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
.env
|
||||||
|
.ipynb_checkpoints/
|
||||||
|
data/
|
||||||
|
models/
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to do so, subject to the following
|
||||||
|
conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
# DRL‑MT5‑Lab
|
||||||
|
|
||||||
|
Deep Reinforcement Learning (SB3) + MetaTrader 5 (MT5) for **backtesting** and **live trading**.
|
||||||
|
|
||||||
|
This repo keeps research and production concerns separate:
|
||||||
|
- **Notebooks** for data/feature engineering, training, optimization, backtesting and reports.
|
||||||
|
- **CLI** for **live inference only** (stable, safe, log‑rich).
|
||||||
|
|
||||||
|
> ⚠️ Educational use only. Trading involves risk. You are responsible for your own deployment and keys.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Highlights
|
||||||
|
|
||||||
|
- **Stable‑Baselines3** agents (PPO primary; A2C, DQN optional in notebooks).
|
||||||
|
- Clean **live trading CLI** (`live_bot_ascii_market.py`) with multi-algorithm support:
|
||||||
|
- **DRY‑RUN** by default (no orders until `--live`).
|
||||||
|
- **Market guard**: weekend + stale tick check (configurable).
|
||||||
|
- FinRL‑style **verbose logs** + rotating file logs.
|
||||||
|
- Feature snapshot (e.g., `close`, `ma_fast`, `ma_slow`, `rsi`).
|
||||||
|
- Backtest notebook with equity curve; optional QuantStats HTML report.
|
||||||
|
- Config via `.env` + CLI flags.
|
||||||
|
- Windows‑safe ASCII logs (no Unicode issues in CP1252 consoles).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Repo layout (typical)
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├─ notebooks/
|
||||||
|
│ ├─ 1_Data_Features.ipynb
|
||||||
|
│ ├─ 2_Train_Optimize.ipynb
|
||||||
|
│ ├─ 3_Backtest.ipynb
|
||||||
|
│ └─ 4_Live_Demo.ipynb
|
||||||
|
│ └─ models/
|
||||||
|
│ ├─ selected_features.json # list of columns used by the model
|
||||||
|
│ └─ ppo_{SYMBOL}_{TF}.zip # exported Stable‑Baselines3 model
|
||||||
|
├─ adapters/
|
||||||
|
│ └─ broker.py # MT5 wrapper used by the CLI
|
||||||
|
│ └─ mt5.py
|
||||||
|
├─ features.py # add_indicators(...) etc.
|
||||||
|
├─ live_bot.py # ASCII logs + market‑hours guard ← recommended
|
||||||
|
├─ utils.py # ASCII logs
|
||||||
|
├─ requirements.txt
|
||||||
|
├─ .env.example
|
||||||
|
└─ README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
> Names may vary slightly depending on your local structure. The CLI only needs: `adapters/broker.py`, `features.py`, `models/selected_features.json`, and a model zip.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
### 1) Environment
|
||||||
|
```bash
|
||||||
|
# create and activate your venv/conda, then:
|
||||||
|
pip install -r requirements.txt
|
||||||
|
# TA‑Lib on Windows: you may prefer a prebuilt 'ta-lib' wheel or conda-forge.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2) Create `.env`
|
||||||
|
Copy `.env.example` → `.env` and fill your MT5 credentials and defaults.
|
||||||
|
|
||||||
|
### 3) Train / export a model
|
||||||
|
Use the notebooks (recommended) to train and save:
|
||||||
|
- `models/ppo_{SYMBOL}_{TF}.zip`
|
||||||
|
- `models/selected_features.json` (list of feature column names)
|
||||||
|
|
||||||
|
> Alternatively point `--model`/`--features` at your own paths.
|
||||||
|
|
||||||
|
### 4) Run the live CLI (default DRY‑RUN)
|
||||||
|
```bash
|
||||||
|
# Windows PowerShell
|
||||||
|
python live_bot_ascii_market.py --symbol EURUSD --timeframe M15
|
||||||
|
|
||||||
|
# Place real orders (be careful)
|
||||||
|
python live_bot_ascii_market.py --symbol EURUSD --timeframe M15 --live
|
||||||
|
```
|
||||||
|
|
||||||
|
The bot will:
|
||||||
|
1) Pull latest bars via `adapters/broker.fetch_last_n(...)`
|
||||||
|
2) Build features with `features.add_indicators(...)`
|
||||||
|
3) Predict action with SB3
|
||||||
|
4) Close opposite‑side positions if any
|
||||||
|
5) Place a market order (unless DRY‑RUN)
|
||||||
|
6) Sleep to the next bar boundary
|
||||||
|
|
||||||
|
Logs are streamed to console and to `logs/live_bot.log` (rotating).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## CLI options
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python live_bot_ascii_market.py --help
|
||||||
|
```
|
||||||
|
|
||||||
|
Important flags:
|
||||||
|
|
||||||
|
- `--symbol EURUSD` Trading symbol
|
||||||
|
- `--timeframe M15` M1/M5/M15/M30/H1/H4/D1 etc.
|
||||||
|
- `--model PATH` Model zip (default: models/ppo_{SYMBOL}_{TF}.zip; also checks notebooks/models/)
|
||||||
|
- `--features PATH` JSON list of feature columns (default: models/selected_features.json)
|
||||||
|
- `--volume 0.01` Lot size
|
||||||
|
- `--live` Actually place orders (otherwise DRY‑RUN)
|
||||||
|
- `--dry-run` Force dry‑run even if `--live` was set
|
||||||
|
- `--order-comment DRL-Live` MT5 order comment (shows up in the terminal)
|
||||||
|
- `--log-file logs/live_bot.log`
|
||||||
|
- `--skip-market-check` Bypass weekend/stale‑tick guard
|
||||||
|
- env: `WEEKEND_TRADING=true` Allow weekend trading (e.g., crypto)
|
||||||
|
|
||||||
|
> **Action mapping:** This CLI maps **`0=BUY, 1=HOLD, 2=SELL`** by default (legacy-friendly).
|
||||||
|
> If your model uses **`0=BUY, 1=HOLD, 2=SELL`** (common in your legacy notebook), edit `action_to_signals(...)` in the file accordingly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## .env example
|
||||||
|
|
||||||
|
See **.env.example** in this repo. Key fields:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
# MT5 connection
|
||||||
|
MT5_LOGIN=12345678
|
||||||
|
MT5_PASSWORD=your_password
|
||||||
|
MT5_SERVER=YourBroker-Server
|
||||||
|
MT5_PATH=C:\Program Files\MetaTrader 5 erminal64.exe
|
||||||
|
|
||||||
|
# Defaults (used if CLI flags are not provided)
|
||||||
|
TRAINING_SYMBOL=EURUSD
|
||||||
|
TIMEFRAME=M15
|
||||||
|
VOLUME=0.01
|
||||||
|
ORDER_COMMENT=DRL-Live
|
||||||
|
|
||||||
|
# Optional Telegram (enable in code where noted)
|
||||||
|
TELEGRAM_BOT_TOKEN=
|
||||||
|
TELEGRAM_CHAT_ID=
|
||||||
|
|
||||||
|
# Live behavior
|
||||||
|
WEEKEND_TRADING=false
|
||||||
|
ALGO=auto
|
||||||
|
MAGIC=234002
|
||||||
|
USE_SLTP=false
|
||||||
|
SL_ATR=2.0
|
||||||
|
TP_ATR=3.0
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backtesting
|
||||||
|
|
||||||
|
- Use `notebooks/3_Backtest.ipynb` for a full workflow:
|
||||||
|
- Load test split
|
||||||
|
- Roll the policy
|
||||||
|
- Plot equity curve
|
||||||
|
- Compute rough Sharpe (rescaled by bar frequency)
|
||||||
|
- (Optional) `QuantStats` report
|
||||||
|
|
||||||
|
Minimal snippet you can adapt inside the notebook:
|
||||||
|
```python
|
||||||
|
from stable_baselines3 import PPO
|
||||||
|
import pandas as pd, numpy as np, matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
model = PPO.load("models/ppo_EURUSD_M15.zip")
|
||||||
|
# df_test & feature_cols prepared earlier…
|
||||||
|
obs, _ = env.reset()
|
||||||
|
equity = [1.0]
|
||||||
|
while True:
|
||||||
|
action, _ = model.predict(obs, deterministic=True)
|
||||||
|
obs, reward, terminated, truncated, info = env.step(int(action))
|
||||||
|
equity.append(equity[-1] * (1.0 + reward))
|
||||||
|
if terminated or truncated: break
|
||||||
|
|
||||||
|
equity = pd.Series(equity, index=df_test.index[:len(equity)])
|
||||||
|
equity.plot(title="Equity Curve")
|
||||||
|
plt.show()
|
||||||
|
```
|
||||||
|
|
||||||
|
> Tip: export static assets (equity PNG, HTML report) to `reports/` and link them in your model registry notes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Broker adapter
|
||||||
|
|
||||||
|
All live operations go through `adapters/broker.py`. It should implement:
|
||||||
|
|
||||||
|
- `open_session(login, password, server, path) -> bool`
|
||||||
|
- `close_session() -> None`
|
||||||
|
- `fetch_last_n(symbol, timeframe, n) -> pd.DataFrame[open,high,low,close,tick_volume,…]`
|
||||||
|
- `current_positions(symbol) -> pd.DataFrame[ticket,type,volume,profit,magic?,…]`
|
||||||
|
- `place_market_order(symbol, side, volume, comment, sl=None, tp=None, **kwargs) -> dict`
|
||||||
|
- (optional) `last_tick(symbol) -> dict` with `bid/ask/time`
|
||||||
|
|
||||||
|
> If you want **magic‑number scoping** and **ATR‑based SL/TP**, expose `magic` in positions and accept `sl/tp/deviation` in the order call, then wire the CLI through (tiny patch).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Logging & Windows notes
|
||||||
|
|
||||||
|
- Console + rotating file logs (`logs/live_bot.log`).
|
||||||
|
- On legacy Windows consoles you can also enable UTF‑8:
|
||||||
|
- PowerShell: `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8`
|
||||||
|
- CMD: `chcp 65001`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Safety checklist
|
||||||
|
|
||||||
|
- Start in **DRY‑RUN**. Inspect logs for a few bars.
|
||||||
|
- Confirm **action mapping** matches your trained model.
|
||||||
|
- Verify **volume**, **symbol**, **timeframe**, **order comment**.
|
||||||
|
- Test with a **demo** account first.
|
||||||
|
- Use **market guard** unless you’re sure about weekend/overnight behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FAQ
|
||||||
|
|
||||||
|
**Q: The bot says “market closed” on Saturday**
|
||||||
|
A: Expected for most FX pairs. Either wait for Monday or set `WEEKEND_TRADING=true` and `--skip-market-check` (only if your instrument trades on weekends).
|
||||||
|
|
||||||
|
**Q: No trades are placed in DRY‑RUN**
|
||||||
|
A: That’s by design. Add `--live` to actually send orders.
|
||||||
|
|
||||||
|
**Q: My BUY/SELL seem inverted**
|
||||||
|
A: Update `action_to_signals(...)` to match your model’s action semantics (see note above).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Backtesting Report
|
||||||
|
|
||||||
|
The `3_Backtest.ipynb` notebook evaluates all trained models and generates a comparison of their equity curves, along with detailed individual performance reports.
|
||||||
|
|
||||||
|
### Equity Curve Comparison
|
||||||
|
================ ppo_EURUSD_M15.zip | PPO ================
|
||||||
|
bars 9956.000000
|
||||||
|
Sharpe 0.896809
|
||||||
|
Sortino 1.245242
|
||||||
|
CAGR 0.065507
|
||||||
|
Calmar 1.755408
|
||||||
|
MaxDD -0.037317
|
||||||
|
WinRate 0.476195
|
||||||
|
ProfitFactor 1.018795
|
||||||
|
Exposure 0.995078
|
||||||
|
Trades 238.000000
|
||||||
|
AvgHoldBars 847.694255
|
||||||
|
dtype: float64
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
### Individual Model Reports
|
||||||
|
|
||||||
|
- **[View PPO Report](notebooks/reports/quantstats_report_ppo_EURUSD_M15.html)**
|
||||||
|
- **[View A2C Report](notebooks/reports/quantstats_report_a2c_EURUSD_M15.html)**
|
||||||
|
- **[View DQN Report](notebooks/reports/quantstats_report_dqn_EURUSD_M15.html)**
|
||||||
|
|
||||||
|
> Note: You may need to download the HTML files to view the interactive charts. GitHub provides a static preview.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT (or your choice).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
This repository is for educational and research purposes only.
|
||||||
|
Nothing herein constitutes financial, investment, or trading advice.
|
||||||
|
Trading involves substantial risk. You are solely responsible for any
|
||||||
|
use of the code and any resulting outcomes.
|
||||||
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# lets notebooks `from adapters import broker`
|
||||||
|
from . import broker
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from .mt5 import (
|
||||||
|
open_session, close_session,
|
||||||
|
fetch_ohlc, fetch_last_n,
|
||||||
|
place_market_order, close_position,
|
||||||
|
current_positions, timeframe_to_mt5,
|
||||||
|
)
|
||||||
|
__all__ = [
|
||||||
|
"open_session", "close_session", "fetch_ohlc", "fetch_last_n",
|
||||||
|
"place_market_order", "close_position", "current_positions", "timeframe_to_mt5",
|
||||||
|
]
|
||||||
+153
@@ -0,0 +1,153 @@
|
|||||||
|
"""
|
||||||
|
Minimal MetaTrader 5 adapter for this repo.
|
||||||
|
Keep credentials in environment variables or .env (never hardcode).
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
try:
|
||||||
|
import MetaTrader5 as mt5
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError("MetaTrader5 package is required. pip install MetaTrader5") from e
|
||||||
|
|
||||||
|
_TF_MAP = {
|
||||||
|
"M1": mt5.TIMEFRAME_M1, "M5": mt5.TIMEFRAME_M5, "M15": mt5.TIMEFRAME_M15,
|
||||||
|
"M30": mt5.TIMEFRAME_M30, "H1": mt5.TIMEFRAME_H1, "H4": mt5.TIMEFRAME_H4,
|
||||||
|
"D1": mt5.TIMEFRAME_D1, "W1": mt5.TIMEFRAME_W1, "MN1": mt5.TIMEFRAME_MN1,
|
||||||
|
}
|
||||||
|
|
||||||
|
def timeframe_to_mt5(tf: str):
|
||||||
|
return _TF_MAP.get(str(tf).upper(), mt5.TIMEFRAME_M15)
|
||||||
|
|
||||||
|
def _ensure_symbol(symbol: str) -> bool:
|
||||||
|
info = mt5.symbol_info(symbol)
|
||||||
|
if info is None:
|
||||||
|
return False
|
||||||
|
if not info.visible:
|
||||||
|
return mt5.symbol_select(symbol, True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def open_session(login: Optional[int]=None, password: Optional[str]=None, server: Optional[str]=None, path: Optional[str]=None) -> bool:
|
||||||
|
ok = mt5.initialize(path) if path else mt5.initialize()
|
||||||
|
if not ok:
|
||||||
|
print("MT5 initialize failed:", mt5.last_error())
|
||||||
|
return False
|
||||||
|
if login and password and server:
|
||||||
|
if not mt5.login(login=login, password=password, server=server):
|
||||||
|
print("MT5 login failed:", mt5.last_error())
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def close_session() -> None:
|
||||||
|
mt5.shutdown()
|
||||||
|
|
||||||
|
def fetch_ohlc(symbol: str, timeframe: str, start: str, end: str) -> pd.DataFrame:
|
||||||
|
if not _ensure_symbol(symbol):
|
||||||
|
raise ValueError(f"Symbol not available: {symbol}")
|
||||||
|
tf = timeframe_to_mt5(timeframe)
|
||||||
|
s = pd.to_datetime(start, utc=True)
|
||||||
|
e = pd.to_datetime(end, utc=True)
|
||||||
|
rates = mt5.copy_rates_range(symbol, tf, s.to_pydatetime(), e.to_pydatetime())
|
||||||
|
if rates is None:
|
||||||
|
err = mt5.last_error()
|
||||||
|
raise RuntimeError(f"copy_rates_range failed: {err}")
|
||||||
|
df = pd.DataFrame(rates)
|
||||||
|
if df.empty:
|
||||||
|
return df
|
||||||
|
df["time"] = pd.to_datetime(df["time"], unit="s", utc=True)
|
||||||
|
df = df.set_index("time")
|
||||||
|
if "tick_volume" in df.columns and "volume" not in df.columns:
|
||||||
|
df["volume"] = df["tick_volume"]
|
||||||
|
return df[["open","high","low","close","volume"]].sort_index()
|
||||||
|
|
||||||
|
def fetch_last_n(symbol: str, timeframe: str, n: int=500) -> pd.DataFrame:
|
||||||
|
if not _ensure_symbol(symbol):
|
||||||
|
raise ValueError(f"Symbol not available: {symbol}")
|
||||||
|
tf = timeframe_to_mt5(timeframe)
|
||||||
|
rates = mt5.copy_rates_from_pos(symbol, tf, 0, n)
|
||||||
|
if rates is None:
|
||||||
|
err = mt5.last_error()
|
||||||
|
raise RuntimeError(f"copy_rates_from_pos failed: {err}")
|
||||||
|
df = pd.DataFrame(rates)
|
||||||
|
if df.empty:
|
||||||
|
return df
|
||||||
|
df["time"] = pd.to_datetime(df["time"], unit="s", utc=True)
|
||||||
|
df = df.set_index("time")
|
||||||
|
if "tick_volume" in df.columns and "volume" not in df.columns:
|
||||||
|
df["volume"] = df["tick_volume"]
|
||||||
|
return df[["open","high","low","close","volume"]].sort_index()
|
||||||
|
|
||||||
|
def place_market_order(symbol: str, side: str, volume: float, sl: Optional[float]=None, tp: Optional[float]=None, comment: str="", deviation: int=20, magic: Optional[int]=None) -> Dict[str, Any]:
|
||||||
|
if not _ensure_symbol(symbol):
|
||||||
|
return {"ok": False, "msg": f"Symbol not available: {symbol}"}
|
||||||
|
tick = mt5.symbol_info_tick(symbol)
|
||||||
|
if tick is None:
|
||||||
|
return {"ok": False, "msg": "No tick data"}
|
||||||
|
order_type = mt5.ORDER_TYPE_BUY if side.lower()=="buy" else mt5.ORDER_TYPE_SELL
|
||||||
|
price = tick.ask if order_type==mt5.ORDER_TYPE_BUY else tick.bid
|
||||||
|
request = {
|
||||||
|
"action": mt5.TRADE_ACTION_DEAL,
|
||||||
|
"symbol": symbol,
|
||||||
|
"volume": float(volume),
|
||||||
|
"type": order_type,
|
||||||
|
"price": float(price),
|
||||||
|
"sl": float(sl) if sl else 0.0,
|
||||||
|
"tp": float(tp) if tp else 0.0,
|
||||||
|
"deviation": int(deviation),
|
||||||
|
"magic": int(magic) if magic is not None else int(os.getenv("MAGIC_NUMBER","234002")),
|
||||||
|
"comment": comment[:31],
|
||||||
|
"type_filling": mt5.ORDER_FILLING_FOK,
|
||||||
|
"type_time": mt5.ORDER_TIME_GTC,
|
||||||
|
}
|
||||||
|
result = mt5.order_send(request)
|
||||||
|
if result is None:
|
||||||
|
return {"ok": False, "msg": "order_send returned None", "error": mt5.last_error()}
|
||||||
|
return {"ok": result.retcode==mt5.TRADE_RETCODE_DONE, "retcode": result.retcode, "result": result._asdict()}
|
||||||
|
|
||||||
|
def close_position(position_id: int) -> Dict[str, Any]:
|
||||||
|
pos_list = mt5.positions_get(ticket=position_id)
|
||||||
|
if pos_list is None or len(pos_list)==0:
|
||||||
|
return {"ok": False, "msg": f"Position not found: {position_id}"}
|
||||||
|
pos = pos_list[0]
|
||||||
|
symbol = pos.symbol
|
||||||
|
volume = pos.volume
|
||||||
|
side = "buy" if pos.type==mt5.POSITION_TYPE_BUY else "sell"
|
||||||
|
|
||||||
|
tick = mt5.symbol_info_tick(symbol)
|
||||||
|
if tick is None:
|
||||||
|
return {"ok": False, "msg": "No tick data"}
|
||||||
|
order_type = mt5.ORDER_TYPE_SELL if side=="buy" else mt5.ORDER_TYPE_BUY
|
||||||
|
price = tick.bid if order_type==mt5.ORDER_TYPE_SELL else tick.ask
|
||||||
|
|
||||||
|
request = {
|
||||||
|
"action": mt5.TRADE_ACTION_DEAL,
|
||||||
|
"symbol": symbol,
|
||||||
|
"volume": float(volume),
|
||||||
|
"type": order_type,
|
||||||
|
"position": position_id,
|
||||||
|
"price": float(price),
|
||||||
|
"deviation": int(os.getenv("DEVIATION","20")),
|
||||||
|
"magic": int(os.getenv("MAGIC_NUMBER","234002")),
|
||||||
|
"comment": "close_position",
|
||||||
|
"type_filling": mt5.ORDER_FILLING_FOK,
|
||||||
|
"type_time": mt5.ORDER_TIME_GTC,
|
||||||
|
}
|
||||||
|
result = mt5.order_send(request)
|
||||||
|
if result is None:
|
||||||
|
return {"ok": False, "msg": "order_send returned None", "error": mt5.last_error()}
|
||||||
|
return {"ok": result.retcode==mt5.TRADE_RETCODE_DONE, "retcode": result.retcode, "result": result._asdict()}
|
||||||
|
|
||||||
|
def current_positions(symbol: str|None=None) -> pd.DataFrame:
|
||||||
|
positions = mt5.positions_get(symbol=symbol) if symbol else mt5.positions_get()
|
||||||
|
if positions is None:
|
||||||
|
err = mt5.last_error()
|
||||||
|
raise RuntimeError(f"positions_get failed: {err}")
|
||||||
|
if len(positions)==0:
|
||||||
|
return pd.DataFrame(columns=["ticket","symbol","type","volume","price_open","sl","tp","profit"])
|
||||||
|
df = pd.DataFrame([p._asdict() for p in positions])
|
||||||
|
df = df.rename(columns={
|
||||||
|
"ticket":"ticket","symbol":"symbol","type":"type","volume":"volume",
|
||||||
|
"price_open":"price_open","sl":"sl","tp":"tp","profit":"profit"
|
||||||
|
})
|
||||||
|
return df[["ticket","symbol","type","volume","price_open","sl","tp","profit"]]
|
||||||
+161
@@ -0,0 +1,161 @@
|
|||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
import ta
|
||||||
|
import talib
|
||||||
|
from sklearn.ensemble import RandomForestRegressor
|
||||||
|
from sklearn.feature_selection import SelectFromModel
|
||||||
|
from sklearn.preprocessing import LabelEncoder
|
||||||
|
from scipy.stats import ks_2samp
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
from utils import log_and_print
|
||||||
|
|
||||||
|
# Global variable to track drift history
|
||||||
|
drift_window = deque(maxlen=10)
|
||||||
|
|
||||||
|
def identify_market_structure(data):
|
||||||
|
"""Identifies swing highs/lows and market structure (HH, HL, LH, LL)."""
|
||||||
|
data['swing_high'] = (data['high'].shift(1) < data['high']) & (data['high'].shift(-1) < data['high'])
|
||||||
|
data['swing_low'] = (data['low'].shift(1) > data['low']) & (data['low'].shift(-1) > data['low'])
|
||||||
|
|
||||||
|
swing_highs = data[data['swing_high']]
|
||||||
|
swing_lows = data[data['swing_low']]
|
||||||
|
|
||||||
|
data['HH'] = (data['swing_high']) & (data['high'] > swing_highs['high'].shift(1))
|
||||||
|
data['HL'] = (data['swing_low']) & (data['low'] > swing_lows['low'].shift(1))
|
||||||
|
data['LH'] = (data['swing_high']) & (data['high'] < swing_highs['high'].shift(1))
|
||||||
|
data['LL'] = (data['swing_low']) & (data['low'] < swing_lows['low'].shift(1))
|
||||||
|
|
||||||
|
# Fill NaN values
|
||||||
|
for col in ['swing_high', 'swing_low', 'HH', 'HL', 'LH', 'LL']:
|
||||||
|
data[col] = data[col].fillna(False)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def identify_candlestick_patterns(data):
|
||||||
|
"""Identifies common candlestick patterns."""
|
||||||
|
patterns = {
|
||||||
|
'engulfing': talib.CDLENGULFING,
|
||||||
|
'doji': talib.CDLDOJI,
|
||||||
|
'hammer': talib.CDLHAMMER,
|
||||||
|
'shooting_star': talib.CDLSHOOTINGSTAR
|
||||||
|
}
|
||||||
|
for name, pattern_func in patterns.items():
|
||||||
|
data[name] = pattern_func(data['open'], data['high'], data['low'], data['close'])
|
||||||
|
return data
|
||||||
|
|
||||||
|
def identify_trends(data):
|
||||||
|
"""Identifies the current trend based on market structure."""
|
||||||
|
if 'HH' not in data.columns or 'HL' not in data.columns or 'LH' not in data.columns or 'LL' not in data.columns:
|
||||||
|
log_and_print("Warning: Market structure columns (HH, HL, etc.) are missing. Cannot identify trends.", is_error=True)
|
||||||
|
data['trend'] = 'sideways'
|
||||||
|
else:
|
||||||
|
data['trend'] = np.where(data['HH'] & data['HL'], 'uptrend',
|
||||||
|
np.where(data['LH'] & data['LL'], 'downtrend', 'sideways'))
|
||||||
|
return data
|
||||||
|
|
||||||
|
def detect_breakouts(data, window=20):
|
||||||
|
"""Detects breakouts from recent highs and lows."""
|
||||||
|
data['breakout'] = np.logical_or(
|
||||||
|
np.greater(data['close'], data['high'].rolling(window=window).max().shift(1)),
|
||||||
|
np.less(data['close'], data['low'].rolling(window=window).min().shift(1))
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def add_indicators(data):
|
||||||
|
"""Adds a set of technical indicators to the dataframe."""
|
||||||
|
# Add basic indicators
|
||||||
|
data['RSI'] = ta.momentum.rsi(data['close'], window=14)
|
||||||
|
data['MACD'] = ta.trend.macd(data['close'])
|
||||||
|
data['MACD_signal'] = ta.trend.macd_signal(data['close'])
|
||||||
|
data['Bollinger_high'] = ta.volatility.bollinger_hband(data['close'])
|
||||||
|
data['Bollinger_low'] = ta.volatility.bollinger_lband(data['close'])
|
||||||
|
data['MA20'] = data['close'].rolling(window=20).mean()
|
||||||
|
data['MA50'] = data['close'].rolling(window=50).mean()
|
||||||
|
data['MA200'] = data['close'].rolling(window=200).mean()
|
||||||
|
data['volatility_atr'] = ta.volatility.average_true_range(data['high'], data['low'], data['close'])
|
||||||
|
|
||||||
|
# Add advanced indicators
|
||||||
|
data['ichimoku_a'] = ta.trend.ichimoku_a(data['high'], data['low'])
|
||||||
|
data['ichimoku_b'] = ta.trend.ichimoku_b(data['high'], data['low'])
|
||||||
|
data['adx'] = ta.trend.adx(data['high'], data['low'], data['close'])
|
||||||
|
if 'tick_volume' in data.columns:
|
||||||
|
data['vwap'] = ta.volume.volume_weighted_average_price(data['high'], data['low'], data['close'], data['tick_volume'])
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def detect_market_regime(data):
|
||||||
|
"""Detects market regime (trending or ranging) based on ADX."""
|
||||||
|
try:
|
||||||
|
adx = data.get('adx', ta.trend.adx(data['high'], data['low'], data['close']))
|
||||||
|
data['regime'] = np.where(adx > 25, 'trending', 'ranging')
|
||||||
|
except Exception as e:
|
||||||
|
log_and_print(f"Error in detect_market_regime: {e}", is_error=True)
|
||||||
|
data['regime'] = 'ranging' # Default to 'ranging'
|
||||||
|
return data
|
||||||
|
|
||||||
|
def add_all_features(df):
|
||||||
|
"""A master function to add all features to the dataframe."""
|
||||||
|
df = add_indicators(df)
|
||||||
|
df = identify_market_structure(df)
|
||||||
|
df = identify_candlestick_patterns(df)
|
||||||
|
df = identify_trends(df)
|
||||||
|
df = detect_breakouts(df)
|
||||||
|
df = detect_market_regime(df)
|
||||||
|
df = df.fillna(0)
|
||||||
|
return df
|
||||||
|
|
||||||
|
def select_features(X, y, max_features=20):
|
||||||
|
"""Selects the best features using a RandomForestRegressor."""
|
||||||
|
estimator = RandomForestRegressor(n_estimators=100, random_state=42)
|
||||||
|
selector = SelectFromModel(estimator=estimator, threshold=-np.inf, max_features=max_features).fit(X, y)
|
||||||
|
selected_features = X.columns[selector.get_support()]
|
||||||
|
log_and_print(f"Selected Features: {list(selected_features)}")
|
||||||
|
return selected_features
|
||||||
|
|
||||||
|
def encode_categorical(df):
|
||||||
|
"""Encodes categorical columns like 'trend' and 'regime'."""
|
||||||
|
le = LabelEncoder()
|
||||||
|
for col in ['trend', 'regime']:
|
||||||
|
if col in df.columns:
|
||||||
|
df[col] = df[col].astype(str)
|
||||||
|
df[col] = le.fit_transform(df[col])
|
||||||
|
return df
|
||||||
|
|
||||||
|
def calculate_future_returns(df):
|
||||||
|
"""Calculates future returns for training the model."""
|
||||||
|
df["future_returns"] = df["close"].pct_change().shift(-1)
|
||||||
|
return df.dropna()
|
||||||
|
|
||||||
|
def ensure_same_columns(df1, df2):
|
||||||
|
"""Ensures two DataFrames have the same columns in the same order."""
|
||||||
|
missing_in_df1 = set(df2.columns) - set(df1.columns)
|
||||||
|
for c in missing_in_df1:
|
||||||
|
df1[c] = 0
|
||||||
|
missing_in_df2 = set(df1.columns) - set(df2.columns)
|
||||||
|
for c in missing_in_df2:
|
||||||
|
df2[c] = 0
|
||||||
|
return df1[df2.columns], df2
|
||||||
|
|
||||||
|
def detect_feature_drift(current_df, reference_df):
|
||||||
|
"""Detects drift between current data and a reference dataset."""
|
||||||
|
current_df, reference_df = ensure_same_columns(current_df.copy(), reference_df.copy())
|
||||||
|
|
||||||
|
drift_features = []
|
||||||
|
for column in current_df.columns:
|
||||||
|
if column not in reference_df.columns:
|
||||||
|
continue
|
||||||
|
stat, p_value = ks_2samp(current_df[column].dropna(), reference_df[column].dropna())
|
||||||
|
if p_value < 0.05:
|
||||||
|
drift_features.append(column)
|
||||||
|
|
||||||
|
drift_detected = 1 if drift_features else 0
|
||||||
|
drift_window.append(drift_detected)
|
||||||
|
|
||||||
|
drift_ratio = sum(drift_window) / len(drift_window) if drift_window else 0
|
||||||
|
log_and_print(f"Rolling drift ratio: {drift_ratio:.2f}")
|
||||||
|
|
||||||
|
if drift_ratio > 0.5:
|
||||||
|
log_and_print(f"Significant feature drift detected in: {drift_features}")
|
||||||
|
return drift_features
|
||||||
|
else:
|
||||||
|
return []
|
||||||
+295
@@ -0,0 +1,295 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Live trading runner (MT5) with verbose logs (ASCII-safe) + market-hours check.
|
||||||
|
"""
|
||||||
|
import os, sys, time, signal, argparse, logging
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Dict
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
try:
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
except Exception:
|
||||||
|
def load_dotenv(*args, **kwargs):
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
import MetaTrader5 as mt5
|
||||||
|
except Exception:
|
||||||
|
mt5 = None
|
||||||
|
|
||||||
|
from stable_baselines3 import PPO
|
||||||
|
import features
|
||||||
|
from adapters import broker
|
||||||
|
|
||||||
|
STOP = False
|
||||||
|
|
||||||
|
def handle_exit(signum, frame):
|
||||||
|
global STOP
|
||||||
|
STOP = True
|
||||||
|
logging.getLogger("live").info("Received signal %s, shutting down...", signum)
|
||||||
|
|
||||||
|
for sig in (signal.SIGINT, signal.SIGTERM):
|
||||||
|
try:
|
||||||
|
signal.signal(sig, handle_exit)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def setup_logging(log_path: Optional[Path] = None, level: int = logging.INFO) -> logging.Logger:
|
||||||
|
logger = logging.getLogger("live")
|
||||||
|
logger.setLevel(level)
|
||||||
|
fmt = logging.Formatter("%(asctime)s | %(levelname)s | %(message)s")
|
||||||
|
sh = logging.StreamHandler(sys.stdout); sh.setFormatter(fmt); logger.addHandler(sh)
|
||||||
|
if log_path:
|
||||||
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
fh = RotatingFileHandler(log_path, maxBytes=2000000, backupCount=3, encoding="utf-8")
|
||||||
|
fh.setFormatter(fmt); logger.addHandler(fh)
|
||||||
|
return logger
|
||||||
|
|
||||||
|
def minutes_per_bar(tf: str) -> int:
|
||||||
|
tf = str(tf).upper()
|
||||||
|
if tf.startswith("M"): return int(tf[1:])
|
||||||
|
if tf.startswith("H"): return int(tf[1:]) * 60
|
||||||
|
if tf in ("D1", "1D"): return 1440
|
||||||
|
return 15
|
||||||
|
|
||||||
|
def seconds_to_next_bar(tf: str) -> float:
|
||||||
|
m = minutes_per_bar(tf)
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
base = now.replace(second=0, microsecond=0)
|
||||||
|
mins = now.minute
|
||||||
|
next_min = ((mins // m) + 1) * m
|
||||||
|
next_bar = base + timedelta(minutes=(next_min - mins))
|
||||||
|
delta = (next_bar - now).total_seconds()
|
||||||
|
if delta < 5: delta += m * 60
|
||||||
|
return float(delta)
|
||||||
|
|
||||||
|
def scalar_action(action) -> int:
|
||||||
|
return int(np.asarray(action).reshape(-1)[0])
|
||||||
|
|
||||||
|
def action_to_signals(a: int) -> Dict[str, bool]:
|
||||||
|
return {"buy": a == 2, "sell": a == 0, "hold": a == 1}
|
||||||
|
|
||||||
|
def print_header(logger: logging.Logger, symbol: str, a: int, sig: Dict[str, bool]) -> None:
|
||||||
|
logger.info("Model action: %s", a)
|
||||||
|
logger.info("-" * 66)
|
||||||
|
logger.info("Date: %s, SYMBOL: %s, BUY SIGNAL: %s, SELL SIGNAL: %s, HOLD SIGNAL: %s",
|
||||||
|
datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S"), symbol, sig["buy"], sig["sell"], sig["hold"])
|
||||||
|
|
||||||
|
def positions_snapshot(logger: logging.Logger, symbol: str) -> None:
|
||||||
|
try:
|
||||||
|
pos = broker.current_positions(symbol)
|
||||||
|
if pos is None or len(pos) == 0:
|
||||||
|
logger.info("No open positions.")
|
||||||
|
return
|
||||||
|
n_long = int((pos["type"] == 0).sum())
|
||||||
|
n_short = int((pos["type"] == 1).sum())
|
||||||
|
vol_total = float(pos["volume"].sum())
|
||||||
|
flt_pnl = float(pos["profit"].sum())
|
||||||
|
logger.info("Positions snapshot | longs=%d shorts=%d total_vol=%.3f floatingPnL=%.2f",
|
||||||
|
n_long, n_short, vol_total, flt_pnl)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Snapshot failed: %s", e)
|
||||||
|
|
||||||
|
def has_same_side_position(symbol: str, want_buy: bool, want_sell: bool) -> bool:
|
||||||
|
try:
|
||||||
|
pos_now = broker.current_positions(symbol)
|
||||||
|
if pos_now is None or len(pos_now) == 0: return False
|
||||||
|
if want_buy and (pos_now["type"] == 0).any(): return True
|
||||||
|
if want_sell and (pos_now["type"] == 1).any(): return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return False
|
||||||
|
|
||||||
|
def close_opposite_positions_if_any(logger: logging.Logger, symbol: str, sig: Dict[str, bool], dry_run: bool) -> bool:
|
||||||
|
try:
|
||||||
|
pos = broker.current_positions(symbol)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Could not fetch positions: %s", e)
|
||||||
|
return False
|
||||||
|
if pos is None or len(pos) == 0:
|
||||||
|
return False
|
||||||
|
if sig["buy"]:
|
||||||
|
to_close = pos[pos["type"] == 1]
|
||||||
|
side_text = "sell"
|
||||||
|
elif sig["sell"]:
|
||||||
|
to_close = pos[pos["type"] == 0]
|
||||||
|
side_text = "buy"
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
if len(to_close) == 0:
|
||||||
|
return False
|
||||||
|
if dry_run:
|
||||||
|
tickets = ", ".join(map(lambda x: str(int(x)), to_close["ticket"]))
|
||||||
|
logger.info("DRY_RUN=True -> Would close %d %s position(s): %s", len(to_close), side_text, tickets)
|
||||||
|
return False
|
||||||
|
logger.info("Existing %s positions found. Attempting to close...", side_text)
|
||||||
|
any_closed = False
|
||||||
|
for _, row in to_close.iterrows():
|
||||||
|
ticket = int(row["ticket"])
|
||||||
|
res = broker.close_position(ticket)
|
||||||
|
if res.get("ok"):
|
||||||
|
logger.info("Successfully closed position %s for %s", ticket, symbol); any_closed = True
|
||||||
|
else:
|
||||||
|
logger.warning("Failed to close position %s: %s", ticket, res)
|
||||||
|
return any_closed
|
||||||
|
|
||||||
|
def place_signal_order(logger: logging.Logger, symbol: str, sig: Dict[str, bool], vol: float, dry_run: bool, comment: str):
|
||||||
|
if sig["hold"]:
|
||||||
|
logger.info("Hold signal detected. No actions taken."); return None
|
||||||
|
side = "buy" if sig["buy"] else "sell"
|
||||||
|
if has_same_side_position(symbol, sig["buy"], sig["sell"]):
|
||||||
|
logger.info("Same-side position already exists. Skipping new %s order.", side)
|
||||||
|
return None
|
||||||
|
if dry_run:
|
||||||
|
logger.info("DRY_RUN=True -> Skipping order. Would place %s %.3f on %s", side.upper(), vol, symbol)
|
||||||
|
return {"ok": True, "dry_run": True, "side": side, "volume": vol}
|
||||||
|
logger.info("%s positions closed (if any). Placing new %s order.", "Buy" if side=="buy" else "Sell", side)
|
||||||
|
res = broker.place_market_order(symbol, side=side, volume=vol, comment=comment)
|
||||||
|
detail = res.get("result", {}) if isinstance(res, dict) else {}
|
||||||
|
price = detail.get("price") if isinstance(detail, dict) else None
|
||||||
|
order = detail.get("order") if isinstance(detail, dict) else None
|
||||||
|
deal = detail.get("deal") if isinstance(detail, dict) else None
|
||||||
|
logger.info("Order result: ok=%s, retcode=%s, order=%s, deal=%s, price=%s",
|
||||||
|
res.get("ok"), res.get("retcode"), order, deal, price)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def print_bar_context(logger: logging.Logger, last_row: pd.Series):
|
||||||
|
fields = []
|
||||||
|
if "close" in last_row: fields.append("close=%.5f" % float(last_row["close"]))
|
||||||
|
if "ma_fast" in last_row: fields.append("ma_fast=%.5f" % float(last_row["ma_fast"]))
|
||||||
|
if "ma_slow" in last_row: fields.append("ma_slow=%.5f" % float(last_row["ma_slow"]))
|
||||||
|
if "rsi" in last_row: fields.append("rsi=%.1f" % float(last_row["rsi"]))
|
||||||
|
if fields:
|
||||||
|
logger.info("Context: " + " | ".join(fields))
|
||||||
|
|
||||||
|
def is_market_open(symbol: str, tf: str, logger: logging.Logger, allow_weekend: bool) -> bool:
|
||||||
|
try:
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
dow = now.weekday()
|
||||||
|
if not allow_weekend and dow in (5, 6):
|
||||||
|
logger.info("Market likely closed (weekend).")
|
||||||
|
return False
|
||||||
|
if mt5 is None:
|
||||||
|
return True
|
||||||
|
tick = mt5.symbol_info_tick(symbol)
|
||||||
|
if tick is None:
|
||||||
|
logger.info("No tick available for %s.", symbol)
|
||||||
|
return False
|
||||||
|
tick_ts = datetime.fromtimestamp(tick.time, tz=timezone.utc)
|
||||||
|
age_sec = (now - tick_ts).total_seconds()
|
||||||
|
bar_min = minutes_per_bar(tf)
|
||||||
|
thresh = max(bar_min * 120, 1800)
|
||||||
|
if age_sec > thresh:
|
||||||
|
logger.info("Market likely closed (last tick %.1f min ago).", age_sec / 60.0)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Market check failed: %s", e)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def main():
|
||||||
|
load_dotenv()
|
||||||
|
p = argparse.ArgumentParser(description="Live DRL trading bot (MT5)")
|
||||||
|
p.add_argument("--symbol", default=os.getenv("TRAINING_SYMBOL","EURUSD"))
|
||||||
|
p.add_argument("--timeframe", default=os.getenv("TIMEFRAME","M15"))
|
||||||
|
p.add_argument("--model", default=None)
|
||||||
|
p.add_argument("--features", default="notebooks/models/selected_features.json")
|
||||||
|
p.add_argument("--volume", type=float, default=float(os.getenv("VOLUME","0.01")))
|
||||||
|
p.add_argument("--live", action="store_true")
|
||||||
|
p.add_argument("--dry-run", action="store_true")
|
||||||
|
p.add_argument("--order-comment", default=os.getenv("ORDER_COMMENT","DRL-Live"))
|
||||||
|
p.add_argument("--log-file", default="logs/live_bot.log")
|
||||||
|
p.add_argument("--skip-market-check", action="store_true", help="Disable market closed guard")
|
||||||
|
args = p.parse_args()
|
||||||
|
|
||||||
|
symbol = args.symbol; tf = args.timeframe
|
||||||
|
dry_run = not args.live or args.dry_run
|
||||||
|
vol = max(args.volume, 0.0); comment = args.order_comment
|
||||||
|
allow_weekend = os.getenv("WEEKEND_TRADING","false").lower() in ("1","true","yes")
|
||||||
|
|
||||||
|
logger = setup_logging(Path(args.log_file))
|
||||||
|
logger.info("Connecting to MT5...")
|
||||||
|
ok = broker.open_session(
|
||||||
|
login=int(os.getenv("MT5_LOGIN","0")) or None,
|
||||||
|
password=os.getenv("MT5_PASSWORD"),
|
||||||
|
server=os.getenv("MT5_SERVER"),
|
||||||
|
path=os.getenv("MT5_PATH"),
|
||||||
|
)
|
||||||
|
logger.info("MT5 connected: %s", ok)
|
||||||
|
|
||||||
|
model_path = args.model or f"notebooks/models/ppo_{symbol}_{tf}.zip"
|
||||||
|
mp = Path(model_path)
|
||||||
|
if not mp.exists():
|
||||||
|
alt = Path("notebooks") / "notebooks/models" / Path(f"ppo_{symbol}_{tf}.zip")
|
||||||
|
if alt.exists():
|
||||||
|
mp = alt
|
||||||
|
if not mp.exists():
|
||||||
|
logger.error("Model not found: %s", model_path)
|
||||||
|
logger.error("Also checked notebooks/models/ppo_%s_%s.zip", symbol, tf)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
with open(args.features, "r", encoding="utf-8") as f:
|
||||||
|
feature_cols = json.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Failed to load features from %s: %s", args.features, e); sys.exit(2)
|
||||||
|
|
||||||
|
logger.info("Loading model: %s", mp)
|
||||||
|
model = PPO.load(mp.as_posix())
|
||||||
|
|
||||||
|
logger.info("Starting live loop | symbol=%s timeframe=%s dry_run=%s volume=%.3f", symbol, tf, dry_run, vol)
|
||||||
|
try:
|
||||||
|
while not STOP:
|
||||||
|
try:
|
||||||
|
open_ok = True if args.skip_market_check else is_market_open(symbol, tf, logger, allow_weekend)
|
||||||
|
if not open_ok:
|
||||||
|
secs = min(900, seconds_to_next_bar(tf))
|
||||||
|
logger.info("Sleeping %.0f seconds until next check (market closed).", secs)
|
||||||
|
time.sleep(secs)
|
||||||
|
continue
|
||||||
|
|
||||||
|
df = broker.fetch_last_n(symbol, tf, n=500)
|
||||||
|
if df is None or len(df) < 50:
|
||||||
|
logger.warning("Insufficient bars fetched; sleeping 5s"); time.sleep(5); continue
|
||||||
|
|
||||||
|
df_feat = features.add_indicators(df.copy())
|
||||||
|
last_row = df_feat.iloc[-1]
|
||||||
|
print_bar_context(logger, last_row)
|
||||||
|
|
||||||
|
obs = last_row[list(feature_cols)].astype(np.float32).values
|
||||||
|
if np.isnan(obs).any() or np.isinf(obs).any():
|
||||||
|
logger.warning("Obs has NaN/Inf; skipping this tick"); time.sleep(5); continue
|
||||||
|
|
||||||
|
action, _ = model.predict(obs, deterministic=True)
|
||||||
|
a = scalar_action(action); sig = action_to_signals(a)
|
||||||
|
print_header(logger, symbol, a, sig)
|
||||||
|
|
||||||
|
positions_snapshot(logger, symbol)
|
||||||
|
_ = close_opposite_positions_if_any(logger, symbol, sig, dry_run)
|
||||||
|
|
||||||
|
if sig["hold"]:
|
||||||
|
logger.info("Appropriate position already exists or HOLD signal. No new order.")
|
||||||
|
else:
|
||||||
|
_ = place_signal_order(logger, symbol, sig, vol, dry_run, comment)
|
||||||
|
|
||||||
|
secs = seconds_to_next_bar(tf)
|
||||||
|
logger.info("Waiting for new signals...")
|
||||||
|
logger.info("Calculated sleep time: %.3f seconds", secs)
|
||||||
|
time.sleep(secs)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Loop error: %s", e); time.sleep(5)
|
||||||
|
finally:
|
||||||
|
try: broker.close_session()
|
||||||
|
except Exception: pass
|
||||||
|
logger.info("Shutdown complete.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "53c28be9",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"# 1) Data — Collect & Save OHLC (MT5)\n",
|
||||||
|
"\n",
|
||||||
|
"This notebook fetches OHLCV data from MT5 via a tiny adapter and saves it to CSV.\n",
|
||||||
|
"- Keep your credentials in `.env` and **do not commit** `.env`.\n",
|
||||||
|
"- If MT5 is not available, you can skip this and drop in a CSV manually."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 1,
|
||||||
|
"id": "dcb5ac32",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"Connecting to MT5...\n",
|
||||||
|
"MT5 connected: True\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"import sys, os; sys.path.append(os.path.abspath('..')) # Path fix\n",
|
||||||
|
"\n",
|
||||||
|
"# Prereqs: `pip install -r requirements.txt` (locally)\n",
|
||||||
|
"import os\n",
|
||||||
|
"from pathlib import Path\n",
|
||||||
|
"import pandas as pd\n",
|
||||||
|
"from dotenv import load_dotenv\n",
|
||||||
|
"\n",
|
||||||
|
"from adapters import broker\n",
|
||||||
|
"\n",
|
||||||
|
"# Load settings from env\n",
|
||||||
|
"load_dotenv()\n",
|
||||||
|
"SYMBOL = os.getenv(\"TRAINING_SYMBOL\", \"EURUSD\")\n",
|
||||||
|
"TIMEFRAME = os.getenv(\"TIMEFRAME\", \"M15\")\n",
|
||||||
|
"START = os.getenv(\"DATA_START\", \"2023-01-01\")\n",
|
||||||
|
"END = os.getenv(\"DATA_END\", \"2025-01-01\")\n",
|
||||||
|
"\n",
|
||||||
|
"print(\"Connecting to MT5...\")\n",
|
||||||
|
"ok = broker.open_session(\n",
|
||||||
|
" login=int(os.getenv(\"MT5_LOGIN\", \"0\")) or None,\n",
|
||||||
|
" password=os.getenv(\"MT5_PASSWORD\"),\n",
|
||||||
|
" server=os.getenv(\"MT5_SERVER\")\n",
|
||||||
|
")\n",
|
||||||
|
"print(\"MT5 connected:\", ok)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 2,
|
||||||
|
"id": "3b690e97",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"Saved: G:\\My Drive\\Bots DRL\\DRL\\DRL-MT5-Lab\\notebooks\\data\\ohlc_EURUSD_M15.csv\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/html": [
|
||||||
|
"<div>\n",
|
||||||
|
"<style scoped>\n",
|
||||||
|
" .dataframe tbody tr th:only-of-type {\n",
|
||||||
|
" vertical-align: middle;\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" .dataframe tbody tr th {\n",
|
||||||
|
" vertical-align: top;\n",
|
||||||
|
" }\n",
|
||||||
|
"\n",
|
||||||
|
" .dataframe thead th {\n",
|
||||||
|
" text-align: right;\n",
|
||||||
|
" }\n",
|
||||||
|
"</style>\n",
|
||||||
|
"<table border=\"1\" class=\"dataframe\">\n",
|
||||||
|
" <thead>\n",
|
||||||
|
" <tr style=\"text-align: right;\">\n",
|
||||||
|
" <th></th>\n",
|
||||||
|
" <th>open</th>\n",
|
||||||
|
" <th>high</th>\n",
|
||||||
|
" <th>low</th>\n",
|
||||||
|
" <th>close</th>\n",
|
||||||
|
" <th>volume</th>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" <tr>\n",
|
||||||
|
" <th>time</th>\n",
|
||||||
|
" <th></th>\n",
|
||||||
|
" <th></th>\n",
|
||||||
|
" <th></th>\n",
|
||||||
|
" <th></th>\n",
|
||||||
|
" <th></th>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" </thead>\n",
|
||||||
|
" <tbody>\n",
|
||||||
|
" <tr>\n",
|
||||||
|
" <th>2024-12-31 20:45:00+00:00</th>\n",
|
||||||
|
" <td>1.03568</td>\n",
|
||||||
|
" <td>1.03568</td>\n",
|
||||||
|
" <td>1.03528</td>\n",
|
||||||
|
" <td>1.03531</td>\n",
|
||||||
|
" <td>529</td>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" <tr>\n",
|
||||||
|
" <th>2024-12-31 21:00:00+00:00</th>\n",
|
||||||
|
" <td>1.03528</td>\n",
|
||||||
|
" <td>1.03625</td>\n",
|
||||||
|
" <td>1.03518</td>\n",
|
||||||
|
" <td>1.03619</td>\n",
|
||||||
|
" <td>425</td>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" <tr>\n",
|
||||||
|
" <th>2024-12-31 21:15:00+00:00</th>\n",
|
||||||
|
" <td>1.03618</td>\n",
|
||||||
|
" <td>1.03647</td>\n",
|
||||||
|
" <td>1.03575</td>\n",
|
||||||
|
" <td>1.03590</td>\n",
|
||||||
|
" <td>465</td>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" <tr>\n",
|
||||||
|
" <th>2024-12-31 21:30:00+00:00</th>\n",
|
||||||
|
" <td>1.03590</td>\n",
|
||||||
|
" <td>1.03590</td>\n",
|
||||||
|
" <td>1.03543</td>\n",
|
||||||
|
" <td>1.03560</td>\n",
|
||||||
|
" <td>545</td>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" <tr>\n",
|
||||||
|
" <th>2024-12-31 21:45:00+00:00</th>\n",
|
||||||
|
" <td>1.03560</td>\n",
|
||||||
|
" <td>1.03571</td>\n",
|
||||||
|
" <td>1.03539</td>\n",
|
||||||
|
" <td>1.03542</td>\n",
|
||||||
|
" <td>306</td>\n",
|
||||||
|
" </tr>\n",
|
||||||
|
" </tbody>\n",
|
||||||
|
"</table>\n",
|
||||||
|
"</div>"
|
||||||
|
],
|
||||||
|
"text/plain": [
|
||||||
|
" open high low close volume\n",
|
||||||
|
"time \n",
|
||||||
|
"2024-12-31 20:45:00+00:00 1.03568 1.03568 1.03528 1.03531 529\n",
|
||||||
|
"2024-12-31 21:00:00+00:00 1.03528 1.03625 1.03518 1.03619 425\n",
|
||||||
|
"2024-12-31 21:15:00+00:00 1.03618 1.03647 1.03575 1.03590 465\n",
|
||||||
|
"2024-12-31 21:30:00+00:00 1.03590 1.03590 1.03543 1.03560 545\n",
|
||||||
|
"2024-12-31 21:45:00+00:00 1.03560 1.03571 1.03539 1.03542 306"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 2,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"\n",
|
||||||
|
"# Fetch OHLC and save\n",
|
||||||
|
"DATA_DIR = Path(\"data\"); DATA_DIR.mkdir(exist_ok=True)\n",
|
||||||
|
"df = broker.fetch_ohlc(SYMBOL, TIMEFRAME, START, END)\n",
|
||||||
|
"out_csv = DATA_DIR / f\"ohlc_{SYMBOL}_{TIMEFRAME}.csv\"\n",
|
||||||
|
"df.to_csv(out_csv)\n",
|
||||||
|
"print(\"Saved:\", out_csv.resolve())\n",
|
||||||
|
"df.tail()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "5e151d9a",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"\n",
|
||||||
|
"# Clean shutdown\n",
|
||||||
|
"broker.close_session()"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "drl",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.10.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,349 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "1efd5b49",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"# 4) Live Demo — Dry Run Loop (MT5)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "d3cefae7",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"This replaces the minimal print with a richer, FinRL-style trace:\n",
|
||||||
|
"- Model action + BUY/SELL/HOLD flags\n",
|
||||||
|
"- Existing positions check, close-opposite logic\n",
|
||||||
|
"- Place market order with result log\n",
|
||||||
|
"- Sleep until next bar boundary (derived from `TIMEFRAME`)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"id": "b638271d",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"#!/usr/bin/env python3\n",
|
||||||
|
"# -*- coding: utf-8 -*-\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"Live trading runner (MT5) with verbose logs (ASCII-safe) + market-hours check.\n",
|
||||||
|
"\"\"\"\n",
|
||||||
|
"import os, sys, time, signal, argparse, logging\n",
|
||||||
|
"from logging.handlers import RotatingFileHandler\n",
|
||||||
|
"from datetime import datetime, timedelta, timezone\n",
|
||||||
|
"from pathlib import Path\n",
|
||||||
|
"from typing import Optional, Dict\n",
|
||||||
|
"\n",
|
||||||
|
"import numpy as np\n",
|
||||||
|
"import pandas as pd\n",
|
||||||
|
"\n",
|
||||||
|
"try:\n",
|
||||||
|
" from dotenv import load_dotenv\n",
|
||||||
|
"except Exception:\n",
|
||||||
|
" def load_dotenv(*args, **kwargs):\n",
|
||||||
|
" return False\n",
|
||||||
|
"\n",
|
||||||
|
"try:\n",
|
||||||
|
" import MetaTrader5 as mt5\n",
|
||||||
|
"except Exception:\n",
|
||||||
|
" mt5 = None\n",
|
||||||
|
"\n",
|
||||||
|
"from stable_baselines3 import PPO\n",
|
||||||
|
"import features\n",
|
||||||
|
"from adapters import broker\n",
|
||||||
|
"\n",
|
||||||
|
"STOP = False\n",
|
||||||
|
"\n",
|
||||||
|
"def handle_exit(signum, frame):\n",
|
||||||
|
" global STOP\n",
|
||||||
|
" STOP = True\n",
|
||||||
|
" logging.getLogger(\"live\").info(\"Received signal %s, shutting down...\", signum)\n",
|
||||||
|
"\n",
|
||||||
|
"for sig in (signal.SIGINT, signal.SIGTERM):\n",
|
||||||
|
" try:\n",
|
||||||
|
" signal.signal(sig, handle_exit)\n",
|
||||||
|
" except Exception:\n",
|
||||||
|
" pass\n",
|
||||||
|
"\n",
|
||||||
|
"def setup_logging(log_path: Optional[Path] = None, level: int = logging.INFO) -> logging.Logger:\n",
|
||||||
|
" logger = logging.getLogger(\"live\")\n",
|
||||||
|
" logger.setLevel(level)\n",
|
||||||
|
" fmt = logging.Formatter(\"%(asctime)s | %(levelname)s | %(message)s\")\n",
|
||||||
|
" sh = logging.StreamHandler(sys.stdout); sh.setFormatter(fmt); logger.addHandler(sh)\n",
|
||||||
|
" if log_path:\n",
|
||||||
|
" log_path.parent.mkdir(parents=True, exist_ok=True)\n",
|
||||||
|
" fh = RotatingFileHandler(log_path, maxBytes=2000000, backupCount=3, encoding=\"utf-8\")\n",
|
||||||
|
" fh.setFormatter(fmt); logger.addHandler(fh)\n",
|
||||||
|
" return logger\n",
|
||||||
|
"\n",
|
||||||
|
"def minutes_per_bar(tf: str) -> int:\n",
|
||||||
|
" tf = str(tf).upper()\n",
|
||||||
|
" if tf.startswith(\"M\"): return int(tf[1:])\n",
|
||||||
|
" if tf.startswith(\"H\"): return int(tf[1:]) * 60\n",
|
||||||
|
" if tf in (\"D1\", \"1D\"): return 1440\n",
|
||||||
|
" return 15\n",
|
||||||
|
"\n",
|
||||||
|
"def seconds_to_next_bar(tf: str) -> float:\n",
|
||||||
|
" m = minutes_per_bar(tf)\n",
|
||||||
|
" now = datetime.now(timezone.utc)\n",
|
||||||
|
" base = now.replace(second=0, microsecond=0)\n",
|
||||||
|
" mins = now.minute\n",
|
||||||
|
" next_min = ((mins // m) + 1) * m\n",
|
||||||
|
" next_bar = base + timedelta(minutes=(next_min - mins))\n",
|
||||||
|
" delta = (next_bar - now).total_seconds()\n",
|
||||||
|
" if delta < 5: delta += m * 60\n",
|
||||||
|
" return float(delta)\n",
|
||||||
|
"\n",
|
||||||
|
"def scalar_action(action) -> int:\n",
|
||||||
|
" return int(np.asarray(action).reshape(-1)[0])\n",
|
||||||
|
"\n",
|
||||||
|
"def action_to_signals(a: int) -> Dict[str, bool]:\n",
|
||||||
|
" return {\"buy\": a == 2, \"sell\": a == 0, \"hold\": a == 1}\n",
|
||||||
|
"\n",
|
||||||
|
"def print_header(logger: logging.Logger, symbol: str, a: int, sig: Dict[str, bool]) -> None:\n",
|
||||||
|
" logger.info(\"Model action: %s\", a)\n",
|
||||||
|
" logger.info(\"-\" * 66)\n",
|
||||||
|
" logger.info(\"Date: %s, SYMBOL: %s, BUY SIGNAL: %s, SELL SIGNAL: %s, HOLD SIGNAL: %s\",\n",
|
||||||
|
" datetime.utcnow().strftime(\"%Y-%m-%d %H:%M:%S\"), symbol, sig[\"buy\"], sig[\"sell\"], sig[\"hold\"])\n",
|
||||||
|
"\n",
|
||||||
|
"def positions_snapshot(logger: logging.Logger, symbol: str) -> None:\n",
|
||||||
|
" try:\n",
|
||||||
|
" pos = broker.current_positions(symbol)\n",
|
||||||
|
" if pos is None or len(pos) == 0:\n",
|
||||||
|
" logger.info(\"No open positions.\")\n",
|
||||||
|
" return\n",
|
||||||
|
" n_long = int((pos[\"type\"] == 0).sum())\n",
|
||||||
|
" n_short = int((pos[\"type\"] == 1).sum())\n",
|
||||||
|
" vol_total = float(pos[\"volume\"].sum())\n",
|
||||||
|
" flt_pnl = float(pos[\"profit\"].sum())\n",
|
||||||
|
" logger.info(\"Positions snapshot | longs=%d shorts=%d total_vol=%.3f floatingPnL=%.2f\",\n",
|
||||||
|
" n_long, n_short, vol_total, flt_pnl)\n",
|
||||||
|
" except Exception as e:\n",
|
||||||
|
" logger.warning(\"Snapshot failed: %s\", e)\n",
|
||||||
|
"\n",
|
||||||
|
"def has_same_side_position(symbol: str, want_buy: bool, want_sell: bool) -> bool:\n",
|
||||||
|
" try:\n",
|
||||||
|
" pos_now = broker.current_positions(symbol)\n",
|
||||||
|
" if pos_now is None or len(pos_now) == 0: return False\n",
|
||||||
|
" if want_buy and (pos_now[\"type\"] == 0).any(): return True\n",
|
||||||
|
" if want_sell and (pos_now[\"type\"] == 1).any(): return True\n",
|
||||||
|
" except Exception:\n",
|
||||||
|
" return False\n",
|
||||||
|
" return False\n",
|
||||||
|
"\n",
|
||||||
|
"def close_opposite_positions_if_any(logger: logging.Logger, symbol: str, sig: Dict[str, bool], dry_run: bool) -> bool:\n",
|
||||||
|
" try:\n",
|
||||||
|
" pos = broker.current_positions(symbol)\n",
|
||||||
|
" except Exception as e:\n",
|
||||||
|
" logger.warning(\"Could not fetch positions: %s\", e)\n",
|
||||||
|
" return False\n",
|
||||||
|
" if pos is None or len(pos) == 0:\n",
|
||||||
|
" return False\n",
|
||||||
|
" if sig[\"buy\"]:\n",
|
||||||
|
" to_close = pos[pos[\"type\"] == 1]\n",
|
||||||
|
" side_text = \"sell\"\n",
|
||||||
|
" elif sig[\"sell\"]:\n",
|
||||||
|
" to_close = pos[pos[\"type\"] == 0]\n",
|
||||||
|
" side_text = \"buy\"\n",
|
||||||
|
" else:\n",
|
||||||
|
" return False\n",
|
||||||
|
" if len(to_close) == 0:\n",
|
||||||
|
" return False\n",
|
||||||
|
" if dry_run:\n",
|
||||||
|
" tickets = \", \".join(map(lambda x: str(int(x)), to_close[\"ticket\"]))\n",
|
||||||
|
" logger.info(\"DRY_RUN=True -> Would close %d %s position(s): %s\", len(to_close), side_text, tickets)\n",
|
||||||
|
" return False\n",
|
||||||
|
" logger.info(\"Existing %s positions found. Attempting to close...\", side_text)\n",
|
||||||
|
" any_closed = False\n",
|
||||||
|
" for _, row in to_close.iterrows():\n",
|
||||||
|
" ticket = int(row[\"ticket\"])\n",
|
||||||
|
" res = broker.close_position(ticket)\n",
|
||||||
|
" if res.get(\"ok\"):\n",
|
||||||
|
" logger.info(\"Successfully closed position %s for %s\", ticket, symbol); any_closed = True\n",
|
||||||
|
" else:\n",
|
||||||
|
" logger.warning(\"Failed to close position %s: %s\", ticket, res)\n",
|
||||||
|
" return any_closed\n",
|
||||||
|
"\n",
|
||||||
|
"def place_signal_order(logger: logging.Logger, symbol: str, sig: Dict[str, bool], vol: float, dry_run: bool, comment: str):\n",
|
||||||
|
" if sig[\"hold\"]:\n",
|
||||||
|
" logger.info(\"Hold signal detected. No actions taken.\"); return None\n",
|
||||||
|
" side = \"buy\" if sig[\"buy\"] else \"sell\"\n",
|
||||||
|
" if has_same_side_position(symbol, sig[\"buy\"], sig[\"sell\"]):\n",
|
||||||
|
" logger.info(\"Same-side position already exists. Skipping new %s order.\", side)\n",
|
||||||
|
" return None\n",
|
||||||
|
" if dry_run:\n",
|
||||||
|
" logger.info(\"DRY_RUN=True -> Skipping order. Would place %s %.3f on %s\", side.upper(), vol, symbol)\n",
|
||||||
|
" return {\"ok\": True, \"dry_run\": True, \"side\": side, \"volume\": vol}\n",
|
||||||
|
" logger.info(\"%s positions closed (if any). Placing new %s order.\", \"Buy\" if side==\"buy\" else \"Sell\", side)\n",
|
||||||
|
" res = broker.place_market_order(symbol, side=side, volume=vol, comment=comment)\n",
|
||||||
|
" detail = res.get(\"result\", {}) if isinstance(res, dict) else {}\n",
|
||||||
|
" price = detail.get(\"price\") if isinstance(detail, dict) else None\n",
|
||||||
|
" order = detail.get(\"order\") if isinstance(detail, dict) else None\n",
|
||||||
|
" deal = detail.get(\"deal\") if isinstance(detail, dict) else None\n",
|
||||||
|
" logger.info(\"Order result: ok=%s, retcode=%s, order=%s, deal=%s, price=%s\",\n",
|
||||||
|
" res.get(\"ok\"), res.get(\"retcode\"), order, deal, price)\n",
|
||||||
|
" return res\n",
|
||||||
|
"\n",
|
||||||
|
"def print_bar_context(logger: logging.Logger, last_row: pd.Series):\n",
|
||||||
|
" fields = []\n",
|
||||||
|
" if \"close\" in last_row: fields.append(\"close=%.5f\" % float(last_row[\"close\"]))\n",
|
||||||
|
" if \"ma_fast\" in last_row: fields.append(\"ma_fast=%.5f\" % float(last_row[\"ma_fast\"]))\n",
|
||||||
|
" if \"ma_slow\" in last_row: fields.append(\"ma_slow=%.5f\" % float(last_row[\"ma_slow\"]))\n",
|
||||||
|
" if \"rsi\" in last_row: fields.append(\"rsi=%.1f\" % float(last_row[\"rsi\"]))\n",
|
||||||
|
" if fields:\n",
|
||||||
|
" logger.info(\"Context: \" + \" | \".join(fields))\n",
|
||||||
|
"\n",
|
||||||
|
"def is_market_open(symbol: str, tf: str, logger: logging.Logger, allow_weekend: bool) -> bool:\n",
|
||||||
|
" try:\n",
|
||||||
|
" now = datetime.now(timezone.utc)\n",
|
||||||
|
" dow = now.weekday()\n",
|
||||||
|
" if not allow_weekend and dow in (5, 6):\n",
|
||||||
|
" logger.info(\"Market likely closed (weekend).\")\n",
|
||||||
|
" return False\n",
|
||||||
|
" if mt5 is None:\n",
|
||||||
|
" return True\n",
|
||||||
|
" tick = mt5.symbol_info_tick(symbol)\n",
|
||||||
|
" if tick is None:\n",
|
||||||
|
" logger.info(\"No tick available for %s.\", symbol)\n",
|
||||||
|
" return False\n",
|
||||||
|
" tick_ts = datetime.fromtimestamp(tick.time, tz=timezone.utc)\n",
|
||||||
|
" age_sec = (now - tick_ts).total_seconds()\n",
|
||||||
|
" bar_min = minutes_per_bar(tf)\n",
|
||||||
|
" thresh = max(bar_min * 120, 1800)\n",
|
||||||
|
" if age_sec > thresh:\n",
|
||||||
|
" logger.info(\"Market likely closed (last tick %.1f min ago).\", age_sec / 60.0)\n",
|
||||||
|
" return False\n",
|
||||||
|
" return True\n",
|
||||||
|
" except Exception as e:\n",
|
||||||
|
" logger.warning(\"Market check failed: %s\", e)\n",
|
||||||
|
" return True\n",
|
||||||
|
"\n",
|
||||||
|
"def main():\n",
|
||||||
|
" load_dotenv()\n",
|
||||||
|
" p = argparse.ArgumentParser(description=\"Live DRL trading bot (MT5)\")\n",
|
||||||
|
" p.add_argument(\"--symbol\", default=os.getenv(\"TRAINING_SYMBOL\",\"EURUSD\"))\n",
|
||||||
|
" p.add_argument(\"--timeframe\", default=os.getenv(\"TIMEFRAME\",\"M15\"))\n",
|
||||||
|
" p.add_argument(\"--model\", default=None)\n",
|
||||||
|
" p.add_argument(\"--features\", default=\"notebooks/models/selected_features.json\")\n",
|
||||||
|
" p.add_argument(\"--volume\", type=float, default=float(os.getenv(\"VOLUME\",\"0.01\")))\n",
|
||||||
|
" p.add_argument(\"--live\", action=\"store_true\")\n",
|
||||||
|
" p.add_argument(\"--dry-run\", action=\"store_true\")\n",
|
||||||
|
" p.add_argument(\"--order-comment\", default=os.getenv(\"ORDER_COMMENT\",\"DRL-Live\"))\n",
|
||||||
|
" p.add_argument(\"--log-file\", default=\"logs/live_bot.log\")\n",
|
||||||
|
" p.add_argument(\"--skip-market-check\", action=\"store_true\", help=\"Disable market closed guard\")\n",
|
||||||
|
" args = p.parse_args()\n",
|
||||||
|
"\n",
|
||||||
|
" symbol = args.symbol; tf = args.timeframe\n",
|
||||||
|
" dry_run = not args.live or args.dry_run\n",
|
||||||
|
" vol = max(args.volume, 0.0); comment = args.order_comment\n",
|
||||||
|
" allow_weekend = os.getenv(\"WEEKEND_TRADING\",\"false\").lower() in (\"1\",\"true\",\"yes\")\n",
|
||||||
|
"\n",
|
||||||
|
" logger = setup_logging(Path(args.log_file))\n",
|
||||||
|
" logger.info(\"Connecting to MT5...\")\n",
|
||||||
|
" ok = broker.open_session(\n",
|
||||||
|
" login=int(os.getenv(\"MT5_LOGIN\",\"0\")) or None,\n",
|
||||||
|
" password=os.getenv(\"MT5_PASSWORD\"),\n",
|
||||||
|
" server=os.getenv(\"MT5_SERVER\"),\n",
|
||||||
|
" path=os.getenv(\"MT5_PATH\"),\n",
|
||||||
|
" )\n",
|
||||||
|
" logger.info(\"MT5 connected: %s\", ok)\n",
|
||||||
|
"\n",
|
||||||
|
" model_path = args.model or f\"notebooks/models/ppo_{symbol}_{tf}.zip\"\n",
|
||||||
|
" mp = Path(model_path)\n",
|
||||||
|
" if not mp.exists():\n",
|
||||||
|
" alt = Path(\"notebooks\") / \"notebooks/models\" / Path(f\"ppo_{symbol}_{tf}.zip\")\n",
|
||||||
|
" if alt.exists():\n",
|
||||||
|
" mp = alt\n",
|
||||||
|
" if not mp.exists():\n",
|
||||||
|
" logger.error(\"Model not found: %s\", model_path)\n",
|
||||||
|
" logger.error(\"Also checked notebooks/models/ppo_%s_%s.zip\", symbol, tf)\n",
|
||||||
|
" sys.exit(2)\n",
|
||||||
|
"\n",
|
||||||
|
" try:\n",
|
||||||
|
" import json\n",
|
||||||
|
" with open(args.features, \"r\", encoding=\"utf-8\") as f:\n",
|
||||||
|
" feature_cols = json.load(f)\n",
|
||||||
|
" except Exception as e:\n",
|
||||||
|
" logger.error(\"Failed to load features from %s: %s\", args.features, e); sys.exit(2)\n",
|
||||||
|
"\n",
|
||||||
|
" logger.info(\"Loading model: %s\", mp)\n",
|
||||||
|
" model = PPO.load(mp.as_posix())\n",
|
||||||
|
"\n",
|
||||||
|
" logger.info(\"Starting live loop | symbol=%s timeframe=%s dry_run=%s volume=%.3f\", symbol, tf, dry_run, vol)\n",
|
||||||
|
" try:\n",
|
||||||
|
" while not STOP:\n",
|
||||||
|
" try:\n",
|
||||||
|
" open_ok = True if args.skip_market_check else is_market_open(symbol, tf, logger, allow_weekend)\n",
|
||||||
|
" if not open_ok:\n",
|
||||||
|
" secs = min(900, seconds_to_next_bar(tf))\n",
|
||||||
|
" logger.info(\"Sleeping %.0f seconds until next check (market closed).\", secs)\n",
|
||||||
|
" time.sleep(secs)\n",
|
||||||
|
" continue\n",
|
||||||
|
"\n",
|
||||||
|
" df = broker.fetch_last_n(symbol, tf, n=500)\n",
|
||||||
|
" if df is None or len(df) < 50:\n",
|
||||||
|
" logger.warning(\"Insufficient bars fetched; sleeping 5s\"); time.sleep(5); continue\n",
|
||||||
|
"\n",
|
||||||
|
" df_feat = features.add_indicators(df.copy())\n",
|
||||||
|
" last_row = df_feat.iloc[-1]\n",
|
||||||
|
" print_bar_context(logger, last_row)\n",
|
||||||
|
"\n",
|
||||||
|
" obs = last_row[list(feature_cols)].astype(np.float32).values\n",
|
||||||
|
" if np.isnan(obs).any() or np.isinf(obs).any():\n",
|
||||||
|
" logger.warning(\"Obs has NaN/Inf; skipping this tick\"); time.sleep(5); continue\n",
|
||||||
|
"\n",
|
||||||
|
" action, _ = model.predict(obs, deterministic=True)\n",
|
||||||
|
" a = scalar_action(action); sig = action_to_signals(a)\n",
|
||||||
|
" print_header(logger, symbol, a, sig)\n",
|
||||||
|
"\n",
|
||||||
|
" positions_snapshot(logger, symbol)\n",
|
||||||
|
" _ = close_opposite_positions_if_any(logger, symbol, sig, dry_run)\n",
|
||||||
|
"\n",
|
||||||
|
" if sig[\"hold\"]:\n",
|
||||||
|
" logger.info(\"Appropriate position already exists or HOLD signal. No new order.\")\n",
|
||||||
|
" else:\n",
|
||||||
|
" _ = place_signal_order(logger, symbol, sig, vol, dry_run, comment)\n",
|
||||||
|
"\n",
|
||||||
|
" secs = seconds_to_next_bar(tf)\n",
|
||||||
|
" logger.info(\"Waiting for new signals...\")\n",
|
||||||
|
" logger.info(\"Calculated sleep time: %.3f seconds\", secs)\n",
|
||||||
|
" time.sleep(secs)\n",
|
||||||
|
"\n",
|
||||||
|
" except Exception as e:\n",
|
||||||
|
" logger.exception(\"Loop error: %s\", e); time.sleep(5)\n",
|
||||||
|
" finally:\n",
|
||||||
|
" try: broker.close_session()\n",
|
||||||
|
" except Exception: pass\n",
|
||||||
|
" logger.info(\"Shutdown complete.\")\n",
|
||||||
|
"\n",
|
||||||
|
"if __name__ == \"__main__\":\n",
|
||||||
|
" main()\n"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "drl",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.10.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<h2>QuantStats Reports</h2><ul><li><a href="qs_ppo_EURUSD_M15.html">qs_ppo_EURUSD_M15.html</a></li><li><a href="qs_a2c_EURUSD_M15.html">qs_a2c_EURUSD_M15.html</a></li><li><a href="qs_dqn_EURUSD_M15.html">qs_dqn_EURUSD_M15.html</a></li></ul>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 194 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<h2>Interactive Model Dashboards</h2><ul><li><a href="plotly_ppo_EURUSD_M15.html">plotly_ppo_EURUSD_M15.html</a></li><li><a href="plotly_a2c_EURUSD_M15.html">plotly_a2c_EURUSD_M15.html</a></li><li><a href="plotly_dqn_EURUSD_M15.html">plotly_dqn_EURUSD_M15.html</a></li></ul>
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 155 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
@@ -0,0 +1,14 @@
|
|||||||
|
numpy
|
||||||
|
pandas
|
||||||
|
matplotlib
|
||||||
|
scikit-learn
|
||||||
|
ta
|
||||||
|
stable-baselines3
|
||||||
|
gymnasium
|
||||||
|
MetaTrader5
|
||||||
|
jupyter
|
||||||
|
nbformat
|
||||||
|
nbconvert
|
||||||
|
python-dotenv
|
||||||
|
QuantStats
|
||||||
|
talib
|
||||||
Reference in New Issue
Block a user