Initial commit

This commit is contained in:
Mohammad Aghdam
2025-10-05 07:52:39 +02:00
commit 90d2259345
29 changed files with 75192 additions and 0 deletions
+20
View File
@@ -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
+9
View File
@@ -0,0 +1,9 @@
.env
.ipynb_checkpoints/
data/
models/
logs/
*.log
__pycache__/
*.pyc
.DS_Store
+21
View File
@@ -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.
+274
View File
@@ -0,0 +1,274 @@
# DRLMT5Lab
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, logrich).
> ⚠️ Educational use only. Trading involves risk. You are responsible for your own deployment and keys.
---
## Highlights
- **StableBaselines3** agents (PPO primary; A2C, DQN optional in notebooks).
- Clean **live trading CLI** (`live_bot_ascii_market.py`) with multi-algorithm support:
- **DRYRUN** by default (no orders until `--live`).
- **Market guard**: weekend + stale tick check (configurable).
- FinRLstyle **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.
- Windowssafe 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 StableBaselines3 model
├─ adapters/
│ └─ broker.py # MT5 wrapper used by the CLI
│ └─ mt5.py
├─ features.py # add_indicators(...) etc.
├─ live_bot.py # ASCII logs + markethours 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
# TALib 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 DRYRUN)
```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 oppositeside positions if any
5) Place a market order (unless DRYRUN)
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 DRYRUN)
- `--dry-run` Force dryrun 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/staletick 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 **magicnumber scoping** and **ATRbased 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 UTF8:
- PowerShell: `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8`
- CMD: `chcp 65001`
---
## Safety checklist
- Start in **DRYRUN**. 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 youre 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 DRYRUN**
A: Thats by design. Add `--live` to actually send orders.
**Q: My BUY/SELL seem inverted**
A: Update `action_to_signals(...)` to match your models 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
![Equity Curve Comparison](notebooks/reports/equity_curve_comparison.png)
![Equity Curve Comparison](notebooks/reports/newplot.png)
### 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.
+2
View File
@@ -0,0 +1,2 @@
# lets notebooks `from adapters import broker`
from . import broker
+10
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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()
+211
View File
@@ -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
+349
View File
@@ -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
}
+1
View File
@@ -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

+1
View File
@@ -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

+14
View File
@@ -0,0 +1,14 @@
numpy
pandas
matplotlib
scikit-learn
ta
stable-baselines3
gymnasium
MetaTrader5
jupyter
nbformat
nbconvert
python-dotenv
QuantStats
talib
+7
View File
@@ -0,0 +1,7 @@
import datetime
def log_and_print(message, is_error=False):
"""Prints a message with a timestamp."""
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_level = "ERROR" if is_error else "INFO"
print(f"[{timestamp}] [{log_level}] {message}")