Initial commit
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Mohammad Aghdam
|
||||
|
||||
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 whom the Software is
|
||||
furnished 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,226 @@
|
||||
# AlphaFlow ML & DL Trading Bot Project
|
||||
|
||||
A comprehensive **machine learning and deep learning trading framework** that covers the entire workflow:
|
||||
|
||||
1. **Data loading** from MetaTrader 5
|
||||
2. **Feature engineering** (technical indicators, custom features, labeling)
|
||||
3. **Model training** (RandomForest, XGBoost, LightGBM, deep learning models, etc.)
|
||||
4. **Hyperparameter tuning** (RandomizedSearchCV, GridSearchCV or Optuna)
|
||||
5. **Time-based / walk-forward cross-validation**
|
||||
6. **Backtesting** (VectorBT or simple custom code)
|
||||
7. **Live trading** integration with MetaTrader 5
|
||||
|
||||
### Supported Labeling Strategies:
|
||||
- **Regression** on next-bar returns
|
||||
- **Multi-bar classification**
|
||||
- **Double-barrier labeling** (López de Prado style)
|
||||
- **Regime detection** (simple up/down/sideways approach)
|
||||
|
||||
This project provides a flexible **template** for you to **create and add your own** custom labeling functions or feature engineering steps, allowing you to experiment with new ideas and strategies.
|
||||
|
||||
## Table of Contents
|
||||
1. [Features](#features)
|
||||
2. [Repository Structure](#repository-structure)
|
||||
3. [Setup & Installation](#setup--installation)
|
||||
4. [Usage](#usage)
|
||||
- Backtesting Notebooks
|
||||
- Live Trading Scripts
|
||||
5. [Key Modules](#key-modules)
|
||||
6. [Extending the Project](#extending-the-project)
|
||||
7. [Disclaimer](#disclaimer)
|
||||
8. [License](#license)
|
||||
|
||||
## Features
|
||||
- **MetaTrader 5** data retrieval (`data_loader.py`)
|
||||
- **TA** library for feature engineering (`ta.add_all_ta_features`)
|
||||
- Multiple **labeling methods**: next-bar, multi-bar, double-barrier, regime detection, etc.
|
||||
- **Time-based** or **walk-forward** cross-validation to avoid data leakage
|
||||
- **RandomizedSearchCV** or **GridSearchCV** for hyperparameter tuning
|
||||
- **VectorBT** or custom backtesting scripts for performance evaluation
|
||||
- **Live trading** scripts with real-time MetaTrader 5 order sending
|
||||
|
||||
## Repository Structure
|
||||
```bash
|
||||
# ML Bot Trading Repository Structure
|
||||
|
||||
ml_bot_trading/
|
||||
├── data/
|
||||
│ ├── data_loader.py # MetaTrader 5 data retrieval
|
||||
│
|
||||
├── features/
|
||||
│ ├── feature_engineering.py # Technical indicators, custom features
|
||||
│ ├── labeling.py # Labeling methods: next-bar, multi-bar, double-barrier, regime detection
|
||||
│
|
||||
├── models/
|
||||
│ ├── model_training.py # Model selection, hyperparam tuning
|
||||
│ ├── saved_models/ # Folder for .pkl pipelines (best_rf_pipeline.pkl, etc.)
|
||||
│
|
||||
├── backtests/
|
||||
│ ├── simple_backtest.py # Simple Pythonic backtest logic
|
||||
│ ├── vectorbt_backtest.py # VectorBT-based backtesting template
|
||||
│
|
||||
├── live_trading/
|
||||
│ ├── regression_returns.py # Live trading script for regression returns
|
||||
│ ├── multi_bar.py # Live trading script for multi-bar classification
|
||||
│ ├── double_barrier.py # Live trading script for double-barrier labeling
|
||||
│ ├── regime_detection.py # Live trading script for regime detection
|
||||
│
|
||||
├── notebooks/
|
||||
│ ├── dl_notebooks/
|
||||
│ │ ├── 00_eda_visualization.ipynb
|
||||
│ │ ├── 01_backtests_regression_returns_dl.ipynb
|
||||
│ │ ├── 01_live_trading_regression_returns_dl.ipynb
|
||||
│ │ ├── 02_time_series_arima_sarima_var_lstmprice.ipynb
|
||||
│ │
|
||||
│ ├── eda_notebooks/
|
||||
│ │ ├── 00_eda_visualization.ipynb
|
||||
│ │
|
||||
│ ├── ml_notebooks/
|
||||
│ │ ├── 01_backtests_regression_returns.ipynb
|
||||
│ │ ├── 01_live_trading_regression_returns.ipynb
|
||||
│ │ ├── 02_backtests_multi_bar_classification.ipynb
|
||||
│ │ ├── 02_live_trading_multi_bar_classification.ipynb
|
||||
│ │ ├── 03_backtests_double_barrier_labeling.ipynb
|
||||
│ │ ├── 03_live_trading_double_barrier_labeling.ipynb
|
||||
│ │ ├── 04_backtests_regime_detection.ipynb
|
||||
│ │ ├── 04_live_trading_regime_detection.ipynb
|
||||
│
|
||||
├── requirements.txt
|
||||
├── README.md
|
||||
```
|
||||
|
||||
## Setup & Installation
|
||||
|
||||
### 1. Clone this repository:
|
||||
```bash
|
||||
git clone https://github.com/maghdam/AlphaFlow-Trading-Bot.git
|
||||
cd ml_bot_trading
|
||||
```
|
||||
|
||||
### 2. Create and activate a Python environment (conda or venv):
|
||||
```bash
|
||||
conda create -n ml_trading python>=3.9
|
||||
conda activate ml_trading
|
||||
```
|
||||
|
||||
### 3. Install dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
- Make sure you have **MetaTrader5** installed [IC Markets MT5](https://www.icmarkets.com/global/en/forex-trading-platform-metatrader/metatrader-5).
|
||||
|
||||
### 4. (Optional) Install Jupyter Notebook:
|
||||
```bash
|
||||
pip install jupyter
|
||||
```
|
||||
|
||||
## Usage
|
||||
### Backtesting Notebooks
|
||||
1. Navigate to `ml_notebooks/` or `dl_notebooks/`, pick a relevant file (e.g., `02_backtests_multi_bar_classification.ipynb`), and run it:
|
||||
```bash
|
||||
jupyter notebook
|
||||
```
|
||||
2. Inside the notebook, you can see how we do:
|
||||
- Feature engineering
|
||||
- Labeling
|
||||
- Walk-forward splits
|
||||
- Train & tune
|
||||
- VectorBT or custom backtesting
|
||||
|
||||
### Live Trading Scripts
|
||||
1. Navigate to `ml_notebooks/` or `dl_notebooks/`, pick a relevant live trading file (e.g., 2_live_trading_multi_bar_classification.ipynb), or go to `live_trading/` folder and pick the script for your labeling approach:
|
||||
- `regression_returns.py`
|
||||
- `multi_bar.py`
|
||||
- `double_barrier.py`
|
||||
- `regime_detection.py`
|
||||
2. Adjust **MetaTrader 5 credentials** (login, server, password) in the script.
|
||||
3. Run from terminal:
|
||||
```bash
|
||||
python live_trading/multi_bar.py.py
|
||||
```
|
||||
4. The script will:
|
||||
- Load the pipeline (e.g., `best_rf_mb_pipeline.pkl`)
|
||||
- Fetch new bars from MetaTrader 5
|
||||
- Predict SHIFTED classes `[0, 1, 2]` => SHIFT back to `[-1, 0, +1]`
|
||||
- Place orders if signals = ±1
|
||||
|
||||
## Key Modules
|
||||
- **`data/data_loader.py`**: Connects to MetaTrader 5, fetches bars with `copy_rates_from_pos`.
|
||||
- **`features/feature_engineering.py`**: Uses the **TA** library and additional custom features (spreads, autocorrelation, etc.).
|
||||
- **`features/labeling.py`**:
|
||||
- `calculate_future_return(...)`
|
||||
- `create_labels_multi_bar(...)`
|
||||
- `create_labels_double_barrier(...)`
|
||||
- `create_labels_regime_detection(...)`
|
||||
- **`models/model_training.py`**:
|
||||
- `select_features_rf_reg(...)`
|
||||
- Time-based splits, random/grid search for hyperparams.
|
||||
- **`backtests/`**:
|
||||
- `simple_backtest.py` or `vectorbt_backtest.py`
|
||||
- **`live_trading/`**:
|
||||
- Each script loads a pipeline (`.pkl`), connects to MT5, and places trades based on predictions.
|
||||
|
||||
## Extending the Project
|
||||
- **Add your own label**: Create a new function in `features/labeling.py` (e.g. `create_labels_custom(...)` that returns a new column with `[-1, 0, +1]` (or your custom classes)).
|
||||
- **Add your own features**: Implement them in `features/feature_engineering.py` or create a new file.
|
||||
- **Train a new model**: Adapt `models/model_training.py` or your notebooks to handle new classifiers/regressors.
|
||||
- **Explore new backtest approaches**: Either integrate with `vectorbt` in a notebook or write a custom `.py` in `backtests/`.
|
||||
|
||||
## Disclaimer
|
||||
We share this code for **learning and development/research purposes only**. Nothing herein constitutes financial advice or a recommendation to trade real money. **Trading involves substantial risk.** Always do your own due diligence, consult professionals, and only risk capital you can afford to lose.
|
||||
|
||||
## License
|
||||
This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
|
||||
## Backtest Results - US30 - H4
|
||||
|
||||
|
||||
```
|
||||
Loaded best classification model from 'best_rf_mb_pipeline.pkl'
|
||||
|
||||
Out-of-Sample Accuracy: 0.5439
|
||||
|
||||
Running Full Backtest on the Last 5000 Bars...
|
||||
```
|
||||
|
||||
```
|
||||
Full Backtest Results:
|
||||
Accuracy=0.54, Return=0.30%, Sharpe=1.21
|
||||
Start 2021-12-01 16:00:00
|
||||
End 2025-02-28 00:00:00
|
||||
Period 832 days 12:00:00
|
||||
Start Value 10000.0
|
||||
End Value 12971.402323
|
||||
Total Return [%] 29.714023
|
||||
Benchmark Return [%] 24.515943
|
||||
Max Gross Exposure [%] 100.0
|
||||
Total Fees Paid 236.291366
|
||||
Max Drawdown [%] 13.645737
|
||||
Max Drawdown Duration 295 days 04:00:00
|
||||
Total Trades 56
|
||||
Total Closed Trades 56
|
||||
Total Open Trades 0
|
||||
Open Trade PnL 0.0
|
||||
Win Rate [%] 60.714286
|
||||
Best Trade [%] 13.156291
|
||||
Worst Trade [%] -3.463323
|
||||
Avg Winning Trade [%] 1.517335
|
||||
Avg Losing Trade [%] -1.094661
|
||||
Avg Winning Trade Duration 7 days 03:03:31.764705882
|
||||
Avg Losing Trade Duration 1 days 00:00:00
|
||||
Profit Factor 2.150809
|
||||
Expectancy 53.060756
|
||||
Sharpe Ratio 1.20547
|
||||
Calmar Ratio 0.885441
|
||||
Omega Ratio 1.156419
|
||||
Sortino Ratio 1.793852
|
||||
dtype: object
|
||||
|
||||
```
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,86 @@
|
||||
# simple_backtest.py
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
# backtests/simple_backtest.py
|
||||
|
||||
|
||||
def simulate_trading(signals, df, cost=0.0002):
|
||||
"""
|
||||
A simple backtest function that simulates trading based on signals (+1/-1/0).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
signals : array-like of int
|
||||
Sequence of +1, -1, or 0 indicating long, short, or flat.
|
||||
df : pd.DataFrame
|
||||
Must contain at least a 'close' column with the same length as 'signals'.
|
||||
cost : float
|
||||
Transaction cost fraction per position change (e.g. 0.0002 = 0.02%).
|
||||
|
||||
Returns
|
||||
-------
|
||||
daily_returns : np.array
|
||||
The sequence of returns from the strategy for each bar.
|
||||
total_return : float
|
||||
The total percentage return (e.g., 10.0 = +10%).
|
||||
"""
|
||||
if len(signals) != len(df):
|
||||
raise ValueError("Length of signals must match length of df.")
|
||||
if 'close' not in df.columns:
|
||||
raise ValueError("df must contain a 'close' column.")
|
||||
|
||||
# 1) Calculate price returns bar to bar
|
||||
df['price_return'] = df['close'].pct_change().fillna(0)
|
||||
|
||||
# 2) Strategy returns = signals * price_return
|
||||
# But we must subtract cost each time we change position.
|
||||
# If signals[i] != signals[i-1], we pay cost.
|
||||
daily_returns = np.zeros(len(signals))
|
||||
|
||||
prev_signal = 0
|
||||
for i in range(len(signals)):
|
||||
# Base return from price movement
|
||||
daily_returns[i] = signals[i] * df['price_return'].iloc[i]
|
||||
|
||||
# Check if position changed from previous bar
|
||||
if i > 0 and signals[i] != prev_signal:
|
||||
# Subtract cost
|
||||
daily_returns[i] -= cost
|
||||
prev_signal = signals[i]
|
||||
|
||||
# 3) Compute total return in percent
|
||||
cumulative_return = (1 + daily_returns).prod() - 1
|
||||
total_return = cumulative_return * 100.0
|
||||
|
||||
return daily_returns, total_return
|
||||
|
||||
|
||||
def calculate_sharpe_ratio(returns, risk_free=0.0):
|
||||
"""
|
||||
Calculates a simple Sharpe ratio for a series of returns.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
returns : list or np.array
|
||||
A sequence of returns per bar/day.
|
||||
risk_free : float, optional
|
||||
Risk-free rate per bar/day, default is 0.0 (no risk-free rate).
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The Sharpe ratio = (mean(returns - risk_free)) / std(returns).
|
||||
If std is zero, returns np.nan.
|
||||
"""
|
||||
returns = np.array(returns)
|
||||
excess_returns = returns - risk_free
|
||||
avg_excess = np.mean(excess_returns)
|
||||
std_excess = np.std(excess_returns)
|
||||
|
||||
if std_excess == 0:
|
||||
return np.nan
|
||||
|
||||
sharpe = avg_excess / std_excess
|
||||
return sharpe
|
||||
@@ -0,0 +1,80 @@
|
||||
# vectorbt_backtest.py
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import vectorbt as vbt
|
||||
|
||||
def run_vectorbt_backtest(
|
||||
model,
|
||||
X,
|
||||
selected_features,
|
||||
data,
|
||||
scaler,
|
||||
init_cash=10000,
|
||||
freq='4H',
|
||||
threshold=0.0
|
||||
):
|
||||
"""
|
||||
Runs a vectorbt backtest for a given pre-trained model.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
model : fitted scikit-learn model
|
||||
Already fitted model (e.g. RandomForestRegressor).
|
||||
X : pd.DataFrame
|
||||
The full feature DataFrame (or the portion you want to backtest).
|
||||
selected_features : list
|
||||
List of feature names used by the model.
|
||||
data : pd.DataFrame
|
||||
Original DataFrame containing at least a 'close' column.
|
||||
scaler : fitted scaler
|
||||
The StandardScaler (or other) used to scale features.
|
||||
init_cash : float
|
||||
Starting capital for the backtest.
|
||||
freq : str
|
||||
Frequency for vectorbt (e.g. '4H', '1D').
|
||||
threshold : float
|
||||
Minimum absolute predicted return to place a trade (optional).
|
||||
|
||||
Returns
|
||||
-------
|
||||
pf : vbt.Portfolio
|
||||
The resulting vectorbt portfolio object.
|
||||
"""
|
||||
# 1) Subset X to the selected features
|
||||
X_sel = X[selected_features]
|
||||
|
||||
# 2) Scale
|
||||
X_scaled = scaler.transform(X_sel)
|
||||
|
||||
# 3) Generate predictions
|
||||
preds = model.predict(X_scaled)
|
||||
|
||||
# 4) Convert predictions to signals
|
||||
# Optionally use threshold to reduce whipsaws
|
||||
if threshold > 0.0:
|
||||
signals = np.where(preds > threshold, 1, np.where(preds < -threshold, -1, 0))
|
||||
else:
|
||||
signals = np.sign(preds)
|
||||
|
||||
# 5) Align signals with close prices
|
||||
close_prices = data.loc[X_sel.index, "close"]
|
||||
# If signals is shorter or the same length
|
||||
if len(signals) < len(close_prices):
|
||||
# Pad signals with 0 if needed
|
||||
signals = np.append(signals, [0]*(len(close_prices)-len(signals)))
|
||||
|
||||
signals_s = pd.Series(signals, index=close_prices.index)
|
||||
# Align if any missing indexes
|
||||
close_prices, signals_s = close_prices.align(signals_s, join="inner", axis=0)
|
||||
|
||||
# 6) Run vectorbt Portfolio
|
||||
pf = vbt.Portfolio.from_signals(
|
||||
close_prices,
|
||||
entries=signals_s > 0,
|
||||
exits=signals_s < 0,
|
||||
init_cash=init_cash,
|
||||
freq=freq
|
||||
)
|
||||
|
||||
return pf
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,31 @@
|
||||
# data_loader.py
|
||||
|
||||
import MetaTrader5 as mt5
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from datetime import datetime
|
||||
|
||||
def get_data_mt5(symbol: str, n_bars: int, timeframe, start_pos=None) -> pd.DataFrame:
|
||||
"""
|
||||
Fetch historical data from MetaTrader 5.
|
||||
|
||||
- `symbol`: Trading instrument (e.g., "BTCUSD").
|
||||
- `n_bars`: Number of bars to retrieve.
|
||||
- `timeframe`: MT5 timeframe (e.g., mt5.TIMEFRAME_H1).
|
||||
- `start_pos`: Offset from the most recent bar (default `None` for live trading).
|
||||
|
||||
If `start_pos` is `None`, fetches the latest `n_bars` (useful for live trading).
|
||||
If `start_pos` is given, fetches `n_bars` from that historical position (useful for backtesting).
|
||||
"""
|
||||
if start_pos is None:
|
||||
rates = mt5.copy_rates_from_pos(symbol, timeframe, 0, n_bars) # Latest n_bars for live trading
|
||||
else:
|
||||
rates = mt5.copy_rates_from_pos(symbol, timeframe, start_pos, n_bars) # Historical data for backtesting
|
||||
|
||||
if rates is None:
|
||||
raise ValueError(f"Could not retrieve data for {symbol}")
|
||||
|
||||
df = pd.DataFrame(rates)
|
||||
df['time'] = pd.to_datetime(df['time'], unit='s')
|
||||
df.set_index('time', inplace=True)
|
||||
return df
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,381 @@
|
||||
# feature_engineering.py
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import math
|
||||
import ta
|
||||
from statsmodels.tsa.stattools import adfuller
|
||||
from scipy.fftpack import fft
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# 1) TA-LIB FEATURES (add_all_ta_features)
|
||||
# --------------------------------------------------------------------
|
||||
def add_all_ta_features(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""
|
||||
Adds a wide range of technical analysis indicators to the DataFrame
|
||||
using the 'ta' library. Modifies the DataFrame in place.
|
||||
"""
|
||||
df = ta.add_all_ta_features(
|
||||
df, open="open", high="high", low="low", close="close", volume="tick_volume", fillna=True
|
||||
)
|
||||
return df
|
||||
|
||||
|
||||
|
||||
def create_custom_feature(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""
|
||||
Example custom feature. For instance, a rolling mean of the close price.
|
||||
"""
|
||||
df["rolling_mean_10"] = df["close"].rolling(window=10).mean()
|
||||
return df
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# 2) MISCELLANEOUS FEATURES
|
||||
# --------------------------------------------------------------------
|
||||
def spread(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""
|
||||
Calculates the spread between 'high' and 'low' columns.
|
||||
"""
|
||||
df_copy = df.copy()
|
||||
df_copy["spread"] = df_copy["high"] - df_copy["low"]
|
||||
return df_copy
|
||||
|
||||
def auto_corr_multi(df: pd.DataFrame, col: str, n: int = 50, lags: list = [1, 3, 5, 10]) -> pd.DataFrame:
|
||||
"""
|
||||
Computes rolling autocorrelation for multiple lags.
|
||||
"""
|
||||
df_copy = df.copy()
|
||||
for lag in lags:
|
||||
df_copy[f"autocorr_{lag}"] = (
|
||||
df_copy[col]
|
||||
.rolling(window=n, min_periods=n)
|
||||
.apply(lambda x: x.autocorr(lag=lag), raw=False)
|
||||
)
|
||||
return df_copy
|
||||
|
||||
|
||||
def candle_information(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""
|
||||
Adds candle-specific features:
|
||||
- candle_way
|
||||
- fill
|
||||
- amplitude
|
||||
"""
|
||||
df_copy = df.copy()
|
||||
df_copy["candle_way"] = 0
|
||||
df_copy.loc[df_copy["close"] > df_copy["open"], "candle_way"] = 1
|
||||
|
||||
df_copy["fill"] = (
|
||||
np.abs(df_copy["close"] - df_copy["open"])
|
||||
/ (df_copy["high"] - df_copy["low"] + 1e-5)
|
||||
)
|
||||
df_copy["amplitude"] = (
|
||||
np.abs(df_copy["close"] - df_copy["open"])
|
||||
/ (df_copy["open"] + 1e-5)
|
||||
)
|
||||
return df_copy
|
||||
|
||||
def log_transform(df: pd.DataFrame, col: str, n: int) -> pd.DataFrame:
|
||||
"""
|
||||
Log-transform a column + compute % change over 'n' bars.
|
||||
"""
|
||||
df_copy = df.copy()
|
||||
df_copy[f"log_{col}"] = np.log(df_copy[col])
|
||||
df_copy[f"ret_log_{n}"] = df_copy[f"log_{col}"].pct_change(periods=n)
|
||||
return df_copy
|
||||
|
||||
def mathematical_derivatives(df: pd.DataFrame, col: str) -> pd.DataFrame:
|
||||
"""
|
||||
Adds 'velocity' and 'acceleration' for a given column.
|
||||
"""
|
||||
df_copy = df.copy()
|
||||
df_copy["velocity"] = df_copy[col].diff()
|
||||
df_copy["acceleration"] = df_copy["velocity"].diff()
|
||||
return df_copy
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# 3) VOLATILITY ESTIMATORS
|
||||
# --------------------------------------------------------------------
|
||||
def parkinson_estimator(window: pd.DataFrame) -> float:
|
||||
n = len(window)
|
||||
if n < 1:
|
||||
return np.nan
|
||||
sum_sq = np.sum(np.log(window['high'] / window['low']) ** 2)
|
||||
return math.sqrt(sum_sq / (4 * math.log(2) * n))
|
||||
|
||||
def moving_parkinson_estimator(df: pd.DataFrame, window_size: int = 30) -> pd.DataFrame:
|
||||
df_copy = df.copy()
|
||||
rolling_vol = pd.Series(dtype="float64", index=df_copy.index)
|
||||
for i in range(window_size, len(df_copy)):
|
||||
w = df_copy.iloc[i - window_size : i]
|
||||
rolling_vol.iloc[i] = parkinson_estimator(w)
|
||||
df_copy["rolling_volatility_parkinson"] = rolling_vol
|
||||
return df_copy
|
||||
|
||||
def yang_zhang_estimator(window: pd.DataFrame) -> float:
|
||||
n = len(window)
|
||||
if n < 1:
|
||||
return np.nan
|
||||
term1 = np.log(window['high'] / window['low']) ** 2
|
||||
term2 = np.log(window['close'] / window['open']) ** 2
|
||||
return math.sqrt(np.mean(term1 + term2))
|
||||
|
||||
def moving_yang_zhang_estimator(df: pd.DataFrame, window_size: int = 30) -> pd.DataFrame:
|
||||
df_copy = df.copy()
|
||||
rolling_vol = pd.Series(dtype="float64", index=df_copy.index)
|
||||
for i in range(window_size, len(df_copy)):
|
||||
w = df_copy.iloc[i - window_size : i]
|
||||
rolling_vol.iloc[i] = yang_zhang_estimator(w)
|
||||
df_copy["rolling_volatility_yang_zhang"] = rolling_vol
|
||||
return df_copy
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# 4) MARKET REGIME / DC EVENTS
|
||||
# --------------------------------------------------------------------
|
||||
def dc_event(P: float, Pext: float, threshold: float) -> int:
|
||||
dc = 0
|
||||
var = (P - Pext) / Pext
|
||||
if var >= threshold:
|
||||
dc = 1
|
||||
elif var <= -threshold:
|
||||
dc = -1
|
||||
return dc
|
||||
|
||||
def calculate_dc(df: pd.DataFrame, threshold: float = 0.01) -> tuple:
|
||||
df_copy = df.copy()
|
||||
prices = df_copy['close'].values
|
||||
dc_events_up, dc_events_down = [], []
|
||||
Pext = prices[0]
|
||||
direction = 0
|
||||
for i in range(1, len(prices)):
|
||||
P = prices[i]
|
||||
dc_flag = dc_event(P, Pext, threshold)
|
||||
if dc_flag == 1:
|
||||
dc_events_up.append(i)
|
||||
direction = 1
|
||||
Pext = P
|
||||
elif dc_flag == -1:
|
||||
dc_events_down.append(i)
|
||||
direction = -1
|
||||
Pext = P
|
||||
else:
|
||||
if direction == 1 and P > Pext:
|
||||
Pext = P
|
||||
elif direction == -1 and P < Pext:
|
||||
Pext = P
|
||||
return dc_events_up, dc_events_down
|
||||
|
||||
def calculate_trend(dc_events_up: list, dc_events_down: list, df: pd.DataFrame):
|
||||
trend_events_down = []
|
||||
trend_events_up = []
|
||||
trend_events_down.extend(sorted(dc_events_down))
|
||||
trend_events_up.extend(sorted(dc_events_up))
|
||||
return trend_events_down, trend_events_up
|
||||
|
||||
def market_regime_dc(df: pd.DataFrame, threshold: float = 0.01) -> pd.DataFrame:
|
||||
df_copy = df.copy()
|
||||
dc_up, dc_down = calculate_dc(df_copy, threshold=threshold)
|
||||
t_down, t_up = calculate_trend(dc_up, dc_down, df_copy)
|
||||
df_copy['market_regime'] = np.nan
|
||||
df_copy.loc[t_up, 'market_regime'] = 1
|
||||
df_copy.loc[t_down, 'market_regime'] = 0
|
||||
df_copy['market_regime'] = df_copy['market_regime'].ffill().bfill()
|
||||
return df_copy
|
||||
|
||||
def kama_market_regime(df: pd.DataFrame, col: str = 'close', n1: int = 10, n2: int = 30) -> pd.DataFrame:
|
||||
df_copy = df.copy()
|
||||
short_kama = df_copy[col].ewm(span=n1, adjust=False).mean()
|
||||
long_kama = df_copy[col].ewm(span=n2, adjust=False).mean()
|
||||
df_copy['kama_diff'] = short_kama - long_kama
|
||||
df_copy['kama_trend'] = (df_copy['kama_diff'] >= 0).astype(int)
|
||||
return df_copy
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# 5) GAP & DISPLACEMENT
|
||||
# --------------------------------------------------------------------
|
||||
def gap_detection(df: pd.DataFrame, lookback: int = 1) -> pd.DataFrame:
|
||||
df_copy = df.copy()
|
||||
df_copy['Bullish_gap_inf'] = np.nan
|
||||
df_copy['Bullish_gap_sup'] = np.nan
|
||||
df_copy['Bullish_gap_size'] = np.nan
|
||||
df_copy['Bearish_gap_inf'] = np.nan
|
||||
df_copy['Bearish_gap_sup'] = np.nan
|
||||
df_copy['Bearish_gap_size'] = np.nan
|
||||
for i in range(lookback, len(df_copy)):
|
||||
prev_high = df_copy['high'].iloc[i - lookback]
|
||||
prev_low = df_copy['low'].iloc[i - lookback]
|
||||
curr_high = df_copy['high'].iloc[i]
|
||||
curr_low = df_copy['low'].iloc[i]
|
||||
if curr_low > prev_high:
|
||||
df_copy.at[df_copy.index[i], 'Bullish_gap_inf'] = prev_high
|
||||
df_copy.at[df_copy.index[i], 'Bullish_gap_sup'] = curr_low
|
||||
df_copy.at[df_copy.index[i], 'Bullish_gap_size'] = curr_low - prev_high
|
||||
if curr_high < prev_low:
|
||||
df_copy.at[df_copy.index[i], 'Bearish_gap_inf'] = curr_high
|
||||
df_copy.at[df_copy.index[i], 'Bearish_gap_sup'] = prev_low
|
||||
df_copy.at[df_copy.index[i], 'Bearish_gap_size'] = prev_low - curr_high
|
||||
return df_copy
|
||||
|
||||
def displacement_detection(
|
||||
df: pd.DataFrame,
|
||||
type_range: str = 'standard',
|
||||
strenght: float = 3.0,
|
||||
period: int = 20
|
||||
) -> pd.DataFrame:
|
||||
df_copy = df.copy()
|
||||
if type_range == 'standard':
|
||||
df_copy['candle_range'] = np.abs(df_copy['close'] - df_copy['open'])
|
||||
elif type_range == 'extrem':
|
||||
df_copy['candle_range'] = np.abs(df_copy['high'] - df_copy['low'])
|
||||
else:
|
||||
raise ValueError("Invalid 'type_range'. Use 'standard' or 'extrem'.")
|
||||
|
||||
df_copy['Variation'] = np.abs(df_copy['close'] / df_copy['open'] - 1)
|
||||
df_copy['STD'] = df_copy['candle_range'].rolling(period).std()
|
||||
df_copy['displacement'] = 0
|
||||
mask = df_copy['candle_range'] > strenght * df_copy['STD']
|
||||
df_copy.loc[mask, 'displacement'] = 1
|
||||
df_copy['red_displacement'] = (
|
||||
df_copy['displacement'] & df_copy['displacement'].shift(1).fillna(0)
|
||||
).astype(int)
|
||||
return df_copy
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# 6) ROLLING ADF (Stationarity)
|
||||
# --------------------------------------------------------------------
|
||||
def rolling_adf_with_flag(df: pd.DataFrame, col: str = 'close', window_size: int = 50, p_value_threshold=0.05) -> pd.DataFrame:
|
||||
"""
|
||||
Computes rolling ADF test and adds a stationarity flag (1=stationary, 0=non-stationary).
|
||||
"""
|
||||
df_copy = df.copy()
|
||||
adf_stat = pd.Series(dtype="float64", index=df_copy.index)
|
||||
adf_pval = pd.Series(dtype="float64", index=df_copy.index)
|
||||
stationarity_flag = pd.Series(dtype="int", index=df_copy.index)
|
||||
|
||||
for i in range(window_size, len(df_copy)):
|
||||
slice_data = df_copy[col].iloc[i - window_size : i].values
|
||||
try:
|
||||
result = adfuller(slice_data, autolag='AIC')
|
||||
adf_stat.iloc[i] = result[0]
|
||||
adf_pval.iloc[i] = result[1]
|
||||
stationarity_flag.iloc[i] = 1 if result[1] < p_value_threshold else 0
|
||||
except:
|
||||
adf_stat.iloc[i] = np.nan
|
||||
adf_pval.iloc[i] = np.nan
|
||||
stationarity_flag.iloc[i] = np.nan
|
||||
|
||||
df_copy['rolling_adf_stat'] = adf_stat
|
||||
df_copy['rolling_adf_pval'] = adf_pval
|
||||
df_copy['stationary_flag'] = stationarity_flag # 1 = stationary, 0 = non-stationary
|
||||
|
||||
return df_copy
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# 7) DOUBLE-BARRIER LABEL
|
||||
# --------------------------------------------------------------------
|
||||
def set_double_barrier_label(
|
||||
df: pd.DataFrame,
|
||||
up: float = 0.005,
|
||||
down: float = 0.005,
|
||||
horizon: int = 50
|
||||
) -> pd.DataFrame:
|
||||
df_copy = df.copy()
|
||||
closes = df_copy["close"].values
|
||||
labels = np.full(len(closes), np.nan)
|
||||
|
||||
for i in range(len(closes)):
|
||||
current_price = closes[i]
|
||||
upper_barrier = current_price * (1 + up)
|
||||
lower_barrier = current_price * (1 - down)
|
||||
end = min(i + horizon, len(closes))
|
||||
for forward_i in range(i + 1, end):
|
||||
if closes[forward_i] >= upper_barrier:
|
||||
labels[i] = 1
|
||||
break
|
||||
elif closes[forward_i] <= lower_barrier:
|
||||
labels[i] = 0
|
||||
break
|
||||
df_copy["barrier_label"] = labels
|
||||
df_copy.dropna(subset=["barrier_label"], inplace=True)
|
||||
return df_copy
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# 8) FUTURE MARKET REGIME (Directional-Change Example)
|
||||
# --------------------------------------------------------------------
|
||||
def future_DC_market_regime(df: pd.DataFrame, threshold: float = 0.03, horizon: int = 10) -> pd.DataFrame:
|
||||
df_copy = df.copy()
|
||||
df_copy['future_return'] = df_copy['close'].shift(-horizon) / df_copy['close'] - 1.0
|
||||
df_copy['future_market_regime'] = np.nan
|
||||
df_copy.loc[df_copy['future_return'] >= threshold, 'future_market_regime'] = 1
|
||||
df_copy.loc[df_copy['future_return'] <= -threshold, 'future_market_regime'] = 0
|
||||
df_copy.dropna(subset=['future_market_regime'], inplace=True)
|
||||
return df_copy
|
||||
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# 9) Introduce Fourier & Wavelet Features for Cyclical Pattern Recognition
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
def add_fourier_features(df: pd.DataFrame, col: str = "close", n_components: int = 5) -> pd.DataFrame:
|
||||
"""
|
||||
Extracts the top 'n_components' Fourier coefficients from price data.
|
||||
"""
|
||||
fft_vals = np.abs(fft(df[col].values))
|
||||
for i in range(1, n_components + 1):
|
||||
df[f'fft_comp_{i}'] = fft_vals[i]
|
||||
return df
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# 10) Optimize ADF Test for Model Selection
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
def apply_differencing_if_needed(df: pd.DataFrame, col: str = "close", threshold: float = 0.05) -> pd.DataFrame:
|
||||
"""
|
||||
If ADF p-value > threshold (non-stationary), apply first differencing.
|
||||
"""
|
||||
if df['rolling_adf_pval'].iloc[-1] > threshold: # Check last rolling p-value
|
||||
df[f"{col}_diff"] = df[col] - df[col].shift(1) # First differencing
|
||||
return df.dropna()
|
||||
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# 11) Normalize Feature Distributions (Scaling)
|
||||
# --------------------------------------------------------------------
|
||||
def scale_features(df: pd.DataFrame, cols_to_scale: list) -> pd.DataFrame:
|
||||
scaler = StandardScaler()
|
||||
df[cols_to_scale] = scaler.fit_transform(df[cols_to_scale])
|
||||
return df
|
||||
|
||||
|
||||
|
||||
# 12) SINGLE PIPELINE EXAMPLE
|
||||
# --------------------------------------------------------------------
|
||||
def create_features(df: pd.DataFrame, col: str = "close", window_size: int = 30) -> pd.DataFrame:
|
||||
"""
|
||||
Optimized pipeline integrating TA, autocorrelation, stationarity, Fourier transform, and normalization.
|
||||
"""
|
||||
df = add_all_ta_features(df) # Adds TA indicators
|
||||
df = spread(df) # Adds 'spread'
|
||||
df = auto_corr_multi(df, col='close') # Multi-lag autocorrelation
|
||||
df = rolling_adf_with_flag(df) # ADF with stationarity flag
|
||||
|
||||
df = log_transform(df, col, 5) # Log transform
|
||||
df = moving_yang_zhang_estimator(df, window_size)
|
||||
df = moving_parkinson_estimator(df, window_size)
|
||||
|
||||
df = add_fourier_features(df, col="close") # Fourier Transform for cyclic detection
|
||||
df = apply_differencing_if_needed(df, col="close") # Ensure stationarity
|
||||
|
||||
# Normalize all numeric features
|
||||
df = scale_features(df, df.select_dtypes(include=[np.number]).columns.tolist())
|
||||
|
||||
return df
|
||||
@@ -0,0 +1,145 @@
|
||||
import pandas as pd
|
||||
import numpy as np # <-- Make sure this is present
|
||||
|
||||
|
||||
def calculate_future_returns(df: pd.DataFrame, horizon: int = 1) -> pd.DataFrame:
|
||||
"""
|
||||
Calculates future returns for a given horizon. By default, horizon=1
|
||||
means next-bar returns. The function appends a new column 'future_returns'.
|
||||
"""
|
||||
df["future_returns"] = df["close"].pct_change(periods=horizon).shift(-horizon)
|
||||
return df.dropna(subset=["future_returns"])
|
||||
|
||||
def create_labels_multi_bar(df, horizon=5, threshold=0.005):
|
||||
"""
|
||||
Creates classification labels for a multi-bar horizon.
|
||||
+1 if future return >= +threshold
|
||||
-1 if future return <= -threshold
|
||||
0 otherwise (could keep as neutral or drop).
|
||||
|
||||
df must have a 'close' column.
|
||||
Returns a new DataFrame with:
|
||||
- 'future_return_h' (the h-bar future return)
|
||||
- 'multi_bar_label' (the classification label)
|
||||
"""
|
||||
df_copy = df.copy()
|
||||
|
||||
# 1) Compute the horizon-based future returns
|
||||
df_copy["future_return_h"] = df_copy["close"].pct_change(periods=horizon).shift(-horizon)
|
||||
|
||||
# 2) Create classification labels
|
||||
df_copy["multi_bar_label"] = 0
|
||||
df_copy.loc[df_copy["future_return_h"] >= threshold, "multi_bar_label"] = 1
|
||||
df_copy.loc[df_copy["future_return_h"] <= -threshold, "multi_bar_label"] = -1
|
||||
|
||||
# 3) Drop rows where future_return_h is NaN (the last 'horizon' bars)
|
||||
df_copy.dropna(subset=["future_return_h"], inplace=True)
|
||||
|
||||
# If you prefer a pure up/down classification, do:
|
||||
# df_copy = df_copy[df_copy["multi_bar_label"] != 0]
|
||||
|
||||
return df_copy
|
||||
|
||||
|
||||
def create_labels_double_barrier(df, up=0.005, down=0.005, horizon=20):
|
||||
"""
|
||||
Double-barrier labeling:
|
||||
- For each index i, define:
|
||||
upper_barrier = close_i * (1 + up)
|
||||
lower_barrier = close_i * (1 - down)
|
||||
- Look ahead up to 'horizon' bars to see which barrier is touched first.
|
||||
- Label = +1 if upper barrier touched first,
|
||||
-1 if lower barrier touched first,
|
||||
0 if neither is touched within horizon.
|
||||
df must have a 'close' column.
|
||||
Returns a new DataFrame with a 'barrier_label' in {-1, 0, +1}.
|
||||
"""
|
||||
df_copy = df.copy()
|
||||
closes = df_copy["close"].values
|
||||
|
||||
labels = np.full(len(closes), np.nan)
|
||||
|
||||
for i in range(len(closes)):
|
||||
current_price = closes[i]
|
||||
upper_barrier = current_price * (1 + up)
|
||||
lower_barrier = current_price * (1 - down)
|
||||
|
||||
# Look ahead up to horizon bars (or until dataset ends)
|
||||
end = min(i + horizon, len(closes))
|
||||
for fwd_i in range(i+1, end):
|
||||
if closes[fwd_i] >= upper_barrier:
|
||||
labels[i] = 1
|
||||
break
|
||||
elif closes[fwd_i] <= lower_barrier:
|
||||
labels[i] = -1
|
||||
break
|
||||
# if we exit loop without setting label => neither barrier hit => 0
|
||||
if np.isnan(labels[i]):
|
||||
labels[i] = 0
|
||||
|
||||
df_copy["barrier_label"] = labels
|
||||
return df_copy
|
||||
|
||||
|
||||
|
||||
def create_labels_double_barrier(df, up=0.005, down=0.005, horizon=20):
|
||||
"""
|
||||
Double-barrier labeling:
|
||||
+1 if upper barrier is touched first,
|
||||
-1 if lower barrier is touched first,
|
||||
0 if neither is touched within horizon.
|
||||
df must have a 'close' column.
|
||||
Returns a new DataFrame with a 'barrier_label' column in {-1, 0, +1}.
|
||||
"""
|
||||
df_copy = df.copy()
|
||||
closes = df_copy["close"].values
|
||||
labels = np.full(len(closes), np.nan)
|
||||
|
||||
for i in range(len(closes)):
|
||||
current_price = closes[i]
|
||||
upper_barrier = current_price * (1 + up)
|
||||
lower_barrier = current_price * (1 - down)
|
||||
|
||||
end = min(i + horizon, len(closes))
|
||||
for fwd_i in range(i+1, end):
|
||||
if closes[fwd_i] >= upper_barrier:
|
||||
labels[i] = 1
|
||||
break
|
||||
elif closes[fwd_i] <= lower_barrier:
|
||||
labels[i] = -1
|
||||
break
|
||||
if np.isnan(labels[i]):
|
||||
labels[i] = 0
|
||||
|
||||
df_copy["barrier_label"] = labels
|
||||
return df_copy
|
||||
|
||||
|
||||
|
||||
def create_labels_regime_detection(df, short_window=20, long_window=50):
|
||||
"""
|
||||
Simple regime detection:
|
||||
+1 if short MA > long MA (up)
|
||||
-1 if short MA < long MA (down)
|
||||
0 otherwise (sideways)
|
||||
df must have 'close' column.
|
||||
Returns a new DataFrame with 'regime_label' in {-1, 0, +1}.
|
||||
"""
|
||||
df_copy = df.copy()
|
||||
|
||||
# 1) Compute short and long MAs
|
||||
df_copy["ma_short"] = df_copy["close"].rolling(short_window).mean()
|
||||
df_copy["ma_long"] = df_copy["close"].rolling(long_window).mean()
|
||||
|
||||
# 2) Label each bar
|
||||
df_copy["regime_label"] = 0
|
||||
up_mask = df_copy["ma_short"] > df_copy["ma_long"]
|
||||
down_mask = df_copy["ma_short"] < df_copy["ma_long"]
|
||||
|
||||
df_copy.loc[up_mask, "regime_label"] = 1
|
||||
df_copy.loc[down_mask, "regime_label"] = -1
|
||||
|
||||
# 3) Drop rows where MAs are NaN (the first 'long_window' bars)
|
||||
df_copy.dropna(subset=["ma_short", "ma_long"], inplace=True)
|
||||
|
||||
return df_copy
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 122 KiB |
@@ -0,0 +1,410 @@
|
||||
# LIVE TRADING CODE FOR DOUBLE-BARRIER CLASSIFICATION
|
||||
|
||||
import sys
|
||||
import os
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1) SET PROJECT ROOT AND UPDATE PATH/WORKING DIRECTORY
|
||||
# ---------------------------------------------------------------------------
|
||||
project_root = Path.cwd().parent.parent # Adjust if your notebook is in notebooks/time_series
|
||||
sys.path.append(str(project_root))
|
||||
os.chdir(str(project_root))
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
import warnings
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
import MetaTrader5 as mt5
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import ta
|
||||
from datetime import datetime, timedelta
|
||||
import time
|
||||
import logging
|
||||
import joblib
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(
|
||||
filename='models/saved_models/trading_app_db.log',
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s %(levelname)s:%(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
|
||||
def log_and_print(message, is_error=False):
|
||||
"""
|
||||
Logs and prints a message.
|
||||
If is_error=True, logs at the ERROR level; otherwise logs at INFO level.
|
||||
"""
|
||||
if is_error:
|
||||
logging.error(message)
|
||||
else:
|
||||
logging.info(message)
|
||||
print(message)
|
||||
|
||||
# Update the login credentials and server information accordingly
|
||||
name = 66677507
|
||||
key = 'ST746$nG38'
|
||||
serv = 'ICMarketsSC-Demo'
|
||||
|
||||
# Global variables
|
||||
SYMBOL = "EURUSD"
|
||||
LOT_SIZE = 0.01
|
||||
TIMEFRAME = mt5.TIMEFRAME_D1
|
||||
N_BARS = 50000
|
||||
MAGIC_NUMBER = 234003
|
||||
SLEEP_TIME = 86400 # e.g. 24 hours
|
||||
COMMENT_ML = "DoubleBarrier-ML"
|
||||
|
||||
class TradingApp:
|
||||
def __init__(self, symbol, lot_size, magic_number):
|
||||
self.symbol = symbol
|
||||
self.lot_size = lot_size
|
||||
self.magic_number = magic_number
|
||||
self.pipeline = None # We'll store the loaded classification pipeline here
|
||||
self.last_retrain_time = None
|
||||
|
||||
def get_data(self, symbol, n, timeframe):
|
||||
"""
|
||||
Fetch the last 'n' bars from MetaTrader 5 for the given timeframe.
|
||||
"""
|
||||
rates = mt5.copy_rates_from_pos(symbol, timeframe, 0, n)
|
||||
rates_frame = pd.DataFrame(rates)
|
||||
rates_frame['time'] = pd.to_datetime(rates_frame['time'], unit='s')
|
||||
rates_frame.set_index('time', inplace=True)
|
||||
return rates_frame
|
||||
|
||||
def add_all_ta_features(self, df):
|
||||
"""
|
||||
Add technical analysis features to the DataFrame using 'ta' library.
|
||||
"""
|
||||
df = ta.add_all_ta_features(
|
||||
df, open="open", high="high", low="low", close="close", volume="tick_volume", fillna=True
|
||||
)
|
||||
return df
|
||||
|
||||
def load_pipeline(self, pipeline_path):
|
||||
"""
|
||||
Loads a pre-trained classification pipeline (e.g., final_production_pipeline.pkl)
|
||||
that was trained on SHIFTED double-barrier labels in {0,1,2}.
|
||||
"""
|
||||
self.pipeline = joblib.load(pipeline_path)
|
||||
logging.info(f"Loaded pipeline from {pipeline_path}")
|
||||
log_and_print(f"Loaded pipeline from {pipeline_path}")
|
||||
|
||||
def ml_signal_generation(self, symbol, n_bars, timeframe):
|
||||
"""
|
||||
Generate buy/sell signals using the loaded classification pipeline.
|
||||
The pipeline outputs SHIFTED labels in {0,1,2} => SHIFT them back to {-1,0,+1}.
|
||||
We'll interpret +1 => buy, -1 => sell, 0 => no trade.
|
||||
|
||||
Double-Barrier labeling was used offline to train this pipeline,
|
||||
so we just replicate the same feature engineering steps and let the model predict.
|
||||
"""
|
||||
if self.pipeline is None:
|
||||
logging.error("No pipeline loaded. Call load_pipeline(...) first.")
|
||||
return False, False, True, True
|
||||
|
||||
# 1) Fetch new data
|
||||
df = self.get_data(symbol, n_bars, timeframe)
|
||||
# 2) Add TA features
|
||||
df = self.add_all_ta_features(df)
|
||||
df.fillna(method='ffill', inplace=True)
|
||||
|
||||
# 3) Prepare the features
|
||||
X_new = df
|
||||
|
||||
# 4) Predict SHIFTED classes
|
||||
preds_shifted = self.pipeline.predict(X_new)
|
||||
# SHIFT them back: 0->-1, 1->0, 2->+1
|
||||
preds = preds_shifted - 1
|
||||
|
||||
# Get the latest predicted class
|
||||
latest_pred = preds[-1]
|
||||
# If latest_pred == +1 => buy signal
|
||||
# If latest_pred == -1 => sell signal
|
||||
# If 0 => no trade
|
||||
buy_signal = (latest_pred == 1)
|
||||
sell_signal = (latest_pred == -1)
|
||||
|
||||
return buy_signal, sell_signal, not buy_signal, not sell_signal
|
||||
|
||||
def orders(self, symbol, lot, is_buy=True, id_position=None, sl=None, tp=None):
|
||||
"""
|
||||
Send an order (buy/sell) to MetaTrader 5.
|
||||
"""
|
||||
symbol_info = mt5.symbol_info(symbol)
|
||||
if symbol_info is None:
|
||||
log_and_print(f"Symbol {symbol} not found, can't place order.", is_error=True)
|
||||
return "Symbol not found"
|
||||
|
||||
# Make sure symbol is visible
|
||||
if not symbol_info.visible:
|
||||
if not mt5.symbol_select(symbol, True):
|
||||
log_and_print(f"Failed to select symbol {symbol}", is_error=True)
|
||||
return "Symbol not visible or could not be selected."
|
||||
|
||||
tick_info = mt5.symbol_info_tick(symbol)
|
||||
if tick_info is None:
|
||||
log_and_print(f"Could not get tick info for {symbol}.", is_error=True)
|
||||
return "Tick info unavailable"
|
||||
|
||||
# Check for valid bid/ask
|
||||
if tick_info.bid <= 0 or tick_info.ask <= 0:
|
||||
log_and_print(
|
||||
f"Zero or invalid bid/ask for {symbol}: bid={tick_info.bid}, ask={tick_info.ask}",
|
||||
is_error=True
|
||||
)
|
||||
return "Invalid prices"
|
||||
|
||||
# LOT SIZE VALIDATION
|
||||
lot = max(lot, symbol_info.volume_min)
|
||||
step = symbol_info.volume_step
|
||||
if step > 0:
|
||||
remainder = lot % step
|
||||
if remainder != 0:
|
||||
lot = lot - remainder + step
|
||||
if lot > symbol_info.volume_max:
|
||||
lot = symbol_info.volume_max
|
||||
|
||||
log_and_print(
|
||||
f"Adjusted lot size to {lot} (min={symbol_info.volume_min}, "
|
||||
f"step={symbol_info.volume_step}, max={symbol_info.volume_max})"
|
||||
)
|
||||
|
||||
# Force ORDER_FILLING_IOC
|
||||
filling_mode = mt5.ORDER_FILLING_IOC
|
||||
|
||||
order_type = mt5.ORDER_TYPE_BUY if is_buy else mt5.ORDER_TYPE_SELL
|
||||
deviation = 20
|
||||
|
||||
request = {
|
||||
"action": mt5.TRADE_ACTION_DEAL,
|
||||
"symbol": symbol,
|
||||
"volume": lot,
|
||||
"type": order_type,
|
||||
"deviation": deviation,
|
||||
"magic": self.magic_number,
|
||||
"comment": COMMENT_ML,
|
||||
"type_time": mt5.ORDER_TIME_GTC,
|
||||
"type_filling": filling_mode,
|
||||
}
|
||||
|
||||
if sl is not None:
|
||||
request["sl"] = sl
|
||||
if tp is not None:
|
||||
request["tp"] = tp
|
||||
if id_position is not None:
|
||||
request["position"] = id_position
|
||||
|
||||
log_and_print(f"Sending order request: {request}")
|
||||
result = mt5.order_send(request)
|
||||
|
||||
order_type_str = "BUY" if is_buy else "SELL"
|
||||
if result is None or result.retcode != mt5.TRADE_RETCODE_DONE:
|
||||
error_message = f"Order failed for {symbol}"
|
||||
if result:
|
||||
error_message += f", retcode={result.retcode}, comment={result.comment}"
|
||||
additional_info = (
|
||||
f"Date/Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
|
||||
f"Order Type: {order_type_str}\n"
|
||||
f"Lot Size: {lot}\n"
|
||||
f"SL: {sl if sl else 'None'}\n"
|
||||
f"TP: {tp if tp else 'None'}\n"
|
||||
f"Comment: {COMMENT_ML}\n"
|
||||
f"Request: {request}\n"
|
||||
f"Result: {result}"
|
||||
)
|
||||
# If you need notifications, you could log or handle them differently here.
|
||||
log_and_print(f"Order failed details: {additional_info}", is_error=True)
|
||||
else:
|
||||
success_message = f"Order successful for {symbol}, comment={result.comment}"
|
||||
additional_info = (
|
||||
f"Date/Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
|
||||
f"Order Type: {order_type_str}\n"
|
||||
f"Lot Size: {lot}\n"
|
||||
f"SL: {sl if sl else 'None'}\n"
|
||||
f"TP: {tp if tp else 'None'}\n"
|
||||
f"Comment: {COMMENT_ML}"
|
||||
)
|
||||
# If you need notifications, you could log or handle them differently here.
|
||||
log_and_print(success_message)
|
||||
|
||||
def get_positions_by_magic(self, symbol, magic_number):
|
||||
"""
|
||||
Retrieve positions for a specific symbol and magic number.
|
||||
"""
|
||||
all_positions = mt5.positions_get(symbol=symbol)
|
||||
if not all_positions:
|
||||
log_and_print("No positions found.", is_error=False)
|
||||
return []
|
||||
return [pos for pos in all_positions if pos.magic == magic_number]
|
||||
|
||||
def run_strategy(self, symbol, lot, buy_signal, sell_signal):
|
||||
"""
|
||||
Decide whether to open a buy or sell order based on signals,
|
||||
close opposite positions if needed, etc.
|
||||
"""
|
||||
log_and_print("------------------------------------------------------------------")
|
||||
log_and_print(
|
||||
f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}, "
|
||||
f"SYMBOL: {symbol}, BUY SIGNAL: {buy_signal}, SELL SIGNAL: {sell_signal}"
|
||||
)
|
||||
|
||||
positions = self.get_positions_by_magic(symbol, self.magic_number)
|
||||
has_buy = any(pos.type == mt5.POSITION_TYPE_BUY for pos in positions)
|
||||
has_sell = any(pos.type == mt5.POSITION_TYPE_SELL for pos in positions)
|
||||
|
||||
if buy_signal and not has_buy:
|
||||
if has_sell:
|
||||
log_and_print("Existing sell positions found. Attempting to close...")
|
||||
if self.close_position(symbol, is_buy=True):
|
||||
log_and_print("Sell positions closed. Placing new buy order.")
|
||||
self.orders(symbol, lot, is_buy=True)
|
||||
else:
|
||||
log_and_print("Failed to close sell positions.")
|
||||
else:
|
||||
self.orders(symbol, lot, is_buy=True)
|
||||
elif sell_signal and not has_sell:
|
||||
if has_buy:
|
||||
log_and_print("Existing buy positions found. Attempting to close...")
|
||||
if self.close_position(symbol, is_buy=False):
|
||||
log_and_print("Buy positions closed. Placing new sell order.")
|
||||
self.orders(symbol, lot, is_buy=False)
|
||||
else:
|
||||
log_and_print("Failed to close buy positions.")
|
||||
else:
|
||||
self.orders(symbol, lot, is_buy=False)
|
||||
else:
|
||||
log_and_print("Appropriate position already exists or no signal to act on.")
|
||||
|
||||
def close_position(self, symbol, is_buy):
|
||||
"""
|
||||
Close all positions of the opposite type for the given symbol & magic.
|
||||
"""
|
||||
positions = mt5.positions_get(symbol=symbol)
|
||||
if not positions:
|
||||
log_and_print(f"No positions to close for symbol: {symbol}")
|
||||
return False
|
||||
|
||||
initial_balance = mt5.account_info().balance
|
||||
closed_any = False
|
||||
|
||||
for position in positions:
|
||||
if position.magic == self.magic_number:
|
||||
# if is_buy==True => we want to close SELL positions
|
||||
# if is_buy==False => we want to close BUY positions
|
||||
if ((is_buy and position.type == mt5.POSITION_TYPE_SELL) or
|
||||
(not is_buy and position.type == mt5.POSITION_TYPE_BUY)):
|
||||
close_request = {
|
||||
"action": mt5.TRADE_ACTION_DEAL,
|
||||
"symbol": symbol,
|
||||
"volume": position.volume,
|
||||
"type": mt5.ORDER_TYPE_BUY if position.type == mt5.POSITION_TYPE_SELL else mt5.ORDER_TYPE_SELL,
|
||||
"position": position.ticket,
|
||||
"deviation": 20,
|
||||
"magic": self.magic_number,
|
||||
"comment": COMMENT_ML,
|
||||
"type_time": mt5.ORDER_TIME_GTC,
|
||||
"type_filling": mt5.ORDER_FILLING_RETURN,
|
||||
}
|
||||
result = mt5.order_send(close_request)
|
||||
if result.retcode != mt5.TRADE_RETCODE_DONE:
|
||||
error_message = (
|
||||
f"Failed to close position {position.ticket} for {symbol}: {result.retcode}"
|
||||
)
|
||||
log_and_print(error_message, is_error=True)
|
||||
# If you need notifications, you could log or handle them differently here.
|
||||
else:
|
||||
log_and_print(f"Successfully closed position {position.ticket} for {symbol}")
|
||||
closed_any = True
|
||||
|
||||
if closed_any:
|
||||
final_balance = mt5.account_info().balance
|
||||
profit = final_balance - initial_balance
|
||||
success_message = f"Closed positions successfully, Profit: {profit}"
|
||||
log_and_print(success_message)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def check_and_execute_trades(self):
|
||||
"""
|
||||
Called in the main loop: generate signals, run strategy, etc.
|
||||
"""
|
||||
mt5.symbol_select(self.symbol, True)
|
||||
buy, sell, _, _ = self.ml_signal_generation(self.symbol, N_BARS, TIMEFRAME)
|
||||
self.run_strategy(self.symbol, self.lot_size, buy, sell)
|
||||
mt5.symbol_select(self.symbol, False)
|
||||
log_and_print("Waiting for new signals...")
|
||||
|
||||
def is_market_open():
|
||||
"""
|
||||
Check if the current time is within the typical Forex trading session, adjusted for CET/CEST.
|
||||
Market closes at Friday 10:00 PM CET and opens at Sunday 11:00 PM CET.
|
||||
"""
|
||||
current_time_utc = datetime.utcnow()
|
||||
current_time_cet = (
|
||||
current_time_utc + timedelta(hours=2)
|
||||
if time.localtime().tm_isdst
|
||||
else current_time_utc + timedelta(hours=1)
|
||||
)
|
||||
|
||||
# Market closes Friday after 10 PM CET
|
||||
if current_time_cet.weekday() == 4 and current_time_cet.hour >= 22:
|
||||
return False
|
||||
# Market opens Sunday after 11 PM CET
|
||||
elif current_time_cet.weekday() == 6 and current_time_cet.hour < 23:
|
||||
return False
|
||||
# Closed all day Saturday
|
||||
elif current_time_cet.weekday() == 5:
|
||||
return False
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
if not mt5.initialize(login=name, server=serv, password=key):
|
||||
log_and_print("Failed to initialize MetaTrader 5", is_error=True)
|
||||
exit()
|
||||
|
||||
app = TradingApp(symbol=SYMBOL, lot_size=LOT_SIZE, magic_number=MAGIC_NUMBER)
|
||||
|
||||
# 1) Load the classification pipeline
|
||||
# Make sure this pipeline is a classification model expecting SHIFTED double-barrier labels in {0,1,2}
|
||||
pipeline_path = "models/saved_models/best_rf_db_pipeline.pkl"
|
||||
app.load_pipeline(pipeline_path)
|
||||
|
||||
while True:
|
||||
log_and_print("Checking market status...")
|
||||
if is_market_open():
|
||||
log_and_print("Market is open. Executing trades...")
|
||||
|
||||
# 2) Generate signals using the loaded pipeline
|
||||
# This pipeline is classification-based => SHIFTED labels {0,1,2}
|
||||
# ml_signal_generation() SHIFTs them back to [-1,0,+1]
|
||||
buy_signal, sell_signal, _, _ = app.ml_signal_generation(
|
||||
symbol=app.symbol,
|
||||
n_bars=N_BARS,
|
||||
timeframe=TIMEFRAME
|
||||
)
|
||||
|
||||
# 3) Run strategy
|
||||
app.run_strategy(app.symbol, app.lot_size, buy_signal, sell_signal)
|
||||
else:
|
||||
log_and_print("Market is closed. No actions performed.")
|
||||
|
||||
time.sleep(SLEEP_TIME)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
log_and_print("Shutdown signal received.")
|
||||
# If you need a notification here, handle it (e.g., log, email, etc.).
|
||||
except Exception as e:
|
||||
error_message = f"An error occurred: {e}"
|
||||
log_and_print(error_message, is_error=True)
|
||||
# If you need a notification here, handle it (e.g., log, email, etc.).
|
||||
finally:
|
||||
mt5.shutdown()
|
||||
log_and_print("MetaTrader 5 shutdown completed.")
|
||||
# If you need a notification here, handle it (e.g., log, email, etc.).
|
||||
@@ -0,0 +1,419 @@
|
||||
# LIVE TRADING CODE FOR MULTI-BAR CLASSIFICATION
|
||||
|
||||
import sys
|
||||
import os
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1) SET PROJECT ROOT AND UPDATE PATH/WORKING DIRECTORY
|
||||
# ---------------------------------------------------------------------------
|
||||
project_root = Path.cwd().parent.parent # Adjust if your notebook is in notebooks/time_series
|
||||
sys.path.append(str(project_root))
|
||||
os.chdir(str(project_root))
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
import warnings
|
||||
warnings.filterwarnings("ignore")
|
||||
import MetaTrader5 as mt5
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import ta
|
||||
from datetime import datetime, timedelta
|
||||
import time
|
||||
import logging
|
||||
import joblib
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(
|
||||
filename='models/saved_models/trading_app1.log',
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s %(levelname)s:%(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
|
||||
def log_and_print(message, is_error=False):
|
||||
"""
|
||||
Logs and prints a message.
|
||||
If is_error=True, logs at the ERROR level; otherwise logs at INFO level.
|
||||
"""
|
||||
if is_error:
|
||||
logging.error(message)
|
||||
else:
|
||||
logging.info(message)
|
||||
print(message)
|
||||
|
||||
# Update the login credentials and server information accordingly
|
||||
name = 66677507
|
||||
key = 'ST746$nG38'
|
||||
serv = 'ICMarketsSC-Demo'
|
||||
|
||||
# Global variables
|
||||
SYMBOL = "EURUSD"
|
||||
LOT_SIZE = 0.01
|
||||
TIMEFRAME = mt5.TIMEFRAME_D1
|
||||
N_BARS = 50000
|
||||
MAGIC_NUMBER = 234003
|
||||
SLEEP_TIME = 86400 # 24 hours in seconds
|
||||
COMMENT_ML = "RFFV-D"
|
||||
|
||||
# If you still need feature selection, you can keep this helper function:
|
||||
def select_features_rf_reg(X, y, estimator, max_features=20):
|
||||
"""
|
||||
Example helper function for feature selection using RandomForest.
|
||||
"""
|
||||
from sklearn.feature_selection import SelectFromModel
|
||||
selector = SelectFromModel(estimator=estimator, threshold=-np.inf, max_features=max_features).fit(X, y)
|
||||
X_transformed = selector.transform(X)
|
||||
selected_features_mask = selector.get_support()
|
||||
return X_transformed, selected_features_mask
|
||||
|
||||
class TradingApp:
|
||||
def __init__(self, symbol, lot_size, magic_number):
|
||||
self.symbol = symbol
|
||||
self.lot_size = lot_size
|
||||
self.magic_number = magic_number
|
||||
self.pipeline = None # We'll store the loaded classification pipeline here
|
||||
self.last_retrain_time = None
|
||||
|
||||
def get_data(self, symbol, n, timeframe):
|
||||
"""
|
||||
Fetch 'n' bars of historical data for the given symbol and timeframe.
|
||||
"""
|
||||
rates = mt5.copy_rates_from_pos(symbol, timeframe, 0, n)
|
||||
rates_frame = pd.DataFrame(rates)
|
||||
rates_frame['time'] = pd.to_datetime(rates_frame['time'], unit='s')
|
||||
rates_frame.set_index('time', inplace=True)
|
||||
return rates_frame
|
||||
|
||||
def add_all_ta_features(self, df):
|
||||
"""
|
||||
Add technical analysis features to the DataFrame using the 'ta' library.
|
||||
"""
|
||||
df = ta.add_all_ta_features(
|
||||
df, open="open", high="high", low="low", close="close", volume="tick_volume", fillna=True
|
||||
)
|
||||
return df
|
||||
|
||||
def load_pipeline(self, pipeline_path):
|
||||
"""
|
||||
Loads a pre-trained classification pipeline (e.g., 'best_rf_pipeline.pkl').
|
||||
This pipeline is expected to produce SHIFTED labels [0,1,2].
|
||||
"""
|
||||
self.pipeline = joblib.load(pipeline_path)
|
||||
logging.info(f"Loaded pipeline from {pipeline_path}")
|
||||
log_and_print(f"Loaded pipeline from {pipeline_path}")
|
||||
|
||||
def ml_signal_generation(self, symbol, n_bars, timeframe):
|
||||
"""
|
||||
Generate buy/sell signals using the loaded classification pipeline.
|
||||
The pipeline outputs SHIFTED labels in {0,1,2} => we SHIFT them back to {-1,0,+1}.
|
||||
We'll interpret +1 => buy, -1 => sell, 0 => no trade.
|
||||
"""
|
||||
if self.pipeline is None:
|
||||
logging.error("No pipeline loaded. Call load_pipeline(...) first.")
|
||||
return False, False, True, True
|
||||
|
||||
# 1) Fetch new data
|
||||
df = self.get_data(symbol, n_bars, timeframe)
|
||||
|
||||
# 2) Add TA features
|
||||
df = self.add_all_ta_features(df)
|
||||
df.fillna(method='ffill', inplace=True)
|
||||
|
||||
# 3) Prepare the features
|
||||
X_new = df # The pipeline must handle columns in the correct order.
|
||||
|
||||
# 4) Predict SHIFTED classes
|
||||
preds_shifted = self.pipeline.predict(X_new)
|
||||
# SHIFT them back: 0->-1, 1->0, 2->+1
|
||||
preds = preds_shifted - 1
|
||||
|
||||
# Get the latest predicted class
|
||||
latest_pred = preds[-1]
|
||||
# If latest_pred == +1 => buy signal
|
||||
# If latest_pred == -1 => sell signal
|
||||
# If 0 => do nothing
|
||||
buy_signal = (latest_pred == 1)
|
||||
sell_signal = (latest_pred == -1)
|
||||
|
||||
return buy_signal, sell_signal, not buy_signal, not sell_signal
|
||||
|
||||
def orders(self, symbol, lot, is_buy=True, id_position=None, sl=None, tp=None):
|
||||
"""
|
||||
Place an order (BUY or SELL) for the specified symbol and lot size.
|
||||
"""
|
||||
symbol_info = mt5.symbol_info(symbol)
|
||||
if symbol_info is None:
|
||||
log_and_print(f"Symbol {symbol} not found, can't place order.", is_error=True)
|
||||
return "Symbol not found"
|
||||
|
||||
# Make sure symbol is visible
|
||||
if not symbol_info.visible:
|
||||
if not mt5.symbol_select(symbol, True):
|
||||
log_and_print(f"Failed to select symbol {symbol}", is_error=True)
|
||||
return "Symbol not visible or could not be selected."
|
||||
|
||||
tick_info = mt5.symbol_info_tick(symbol)
|
||||
if tick_info is None:
|
||||
log_and_print(f"Could not get tick info for {symbol}.", is_error=True)
|
||||
return "Tick info unavailable"
|
||||
|
||||
# Check for valid bid/ask
|
||||
if tick_info.bid <= 0 or tick_info.ask <= 0:
|
||||
log_and_print(
|
||||
f"Zero or invalid bid/ask for {symbol}: bid={tick_info.bid}, ask={tick_info.ask}",
|
||||
is_error=True
|
||||
)
|
||||
return "Invalid prices"
|
||||
|
||||
# LOT SIZE VALIDATION
|
||||
lot = max(lot, symbol_info.volume_min)
|
||||
step = symbol_info.volume_step
|
||||
if step > 0:
|
||||
remainder = lot % step
|
||||
if remainder != 0:
|
||||
lot = lot - remainder + step
|
||||
if lot > symbol_info.volume_max:
|
||||
lot = symbol_info.volume_max
|
||||
|
||||
log_and_print(
|
||||
f"Adjusted lot size to {lot} (min={symbol_info.volume_min}, "
|
||||
f"step={symbol_info.volume_step}, max={symbol_info.volume_max})"
|
||||
)
|
||||
|
||||
# Force ORDER_FILLING_IOC
|
||||
filling_mode = 1 # ORDER_FILLING_IOC
|
||||
|
||||
order_type = mt5.ORDER_TYPE_BUY if is_buy else mt5.ORDER_TYPE_SELL
|
||||
order_price = tick_info.ask if is_buy else tick_info.bid
|
||||
deviation = 20
|
||||
|
||||
request = {
|
||||
"action": mt5.TRADE_ACTION_DEAL,
|
||||
"symbol": symbol,
|
||||
"volume": lot,
|
||||
"type": order_type,
|
||||
"deviation": deviation,
|
||||
"magic": self.magic_number,
|
||||
"comment": COMMENT_ML,
|
||||
"type_time": mt5.ORDER_TIME_GTC,
|
||||
"type_filling": filling_mode,
|
||||
}
|
||||
|
||||
if sl is not None:
|
||||
request["sl"] = sl
|
||||
if tp is not None:
|
||||
request["tp"] = tp
|
||||
if id_position is not None:
|
||||
request["position"] = id_position
|
||||
|
||||
log_and_print(f"Sending order request: {request}")
|
||||
result = mt5.order_send(request)
|
||||
|
||||
order_type_str = "BUY" if is_buy else "SELL"
|
||||
if result is None or result.retcode != mt5.TRADE_RETCODE_DONE:
|
||||
error_message = f"Order failed for {symbol}"
|
||||
if result:
|
||||
error_message += f", retcode={result.retcode}, comment={result.comment}"
|
||||
additional_info = (
|
||||
f"Date/Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
|
||||
f"Order Type: {order_type_str}\n"
|
||||
f"Lot Size: {lot}\n"
|
||||
f"SL: {sl if sl else 'None'}\n"
|
||||
f"TP: {tp if tp else 'None'}\n"
|
||||
f"Comment: {COMMENT_ML}\n"
|
||||
f"Request: {request}\n"
|
||||
f"Result: {result}"
|
||||
)
|
||||
# If you want notifications, you could log or handle them differently here.
|
||||
log_and_print(f"Order failed details: {additional_info}", is_error=True)
|
||||
else:
|
||||
success_message = f"Order successful for {symbol}, comment={result.comment}"
|
||||
additional_info = (
|
||||
f"Date/Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
|
||||
f"Order Type: {order_type_str}\n"
|
||||
f"Lot Size: {lot}\n"
|
||||
f"SL: {sl if sl else 'None'}\n"
|
||||
f"TP: {tp if tp else 'None'}\n"
|
||||
f"Comment: {COMMENT_ML}"
|
||||
)
|
||||
# If you want notifications, you could log or handle them differently here.
|
||||
log_and_print(success_message)
|
||||
|
||||
def get_positions_by_magic(self, symbol, magic_number):
|
||||
"""
|
||||
Retrieve positions for a specific symbol and magic number.
|
||||
"""
|
||||
all_positions = mt5.positions_get(symbol=symbol)
|
||||
if not all_positions:
|
||||
log_and_print("No positions found.", is_error=False)
|
||||
return []
|
||||
return [pos for pos in all_positions if pos.magic == magic_number]
|
||||
|
||||
def run_strategy(self, symbol, lot, buy_signal, sell_signal):
|
||||
"""
|
||||
Run the trading strategy logic based on buy/sell signals.
|
||||
"""
|
||||
log_and_print("------------------------------------------------------------------")
|
||||
log_and_print(
|
||||
f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}, "
|
||||
f"SYMBOL: {symbol}, BUY SIGNAL: {buy_signal}, SELL SIGNAL: {sell_signal}"
|
||||
)
|
||||
|
||||
positions = self.get_positions_by_magic(symbol, self.magic_number)
|
||||
has_buy = any(pos.type == mt5.POSITION_TYPE_BUY for pos in positions)
|
||||
has_sell = any(pos.type == mt5.POSITION_TYPE_SELL for pos in positions)
|
||||
|
||||
if buy_signal and not has_buy:
|
||||
if has_sell:
|
||||
log_and_print("Existing sell positions found. Attempting to close...")
|
||||
if self.close_position(symbol, is_buy=True):
|
||||
log_and_print("Sell positions closed. Placing new buy order.")
|
||||
self.orders(symbol, lot, is_buy=True)
|
||||
else:
|
||||
log_and_print("Failed to close sell positions.")
|
||||
else:
|
||||
self.orders(symbol, lot, is_buy=True)
|
||||
elif sell_signal and not has_sell:
|
||||
if has_buy:
|
||||
log_and_print("Existing buy positions found. Attempting to close...")
|
||||
if self.close_position(symbol, is_buy=False):
|
||||
log_and_print("Buy positions closed. Placing new sell order.")
|
||||
self.orders(symbol, lot, is_buy=False)
|
||||
else:
|
||||
log_and_print("Failed to close buy positions.")
|
||||
else:
|
||||
self.orders(symbol, lot, is_buy=False)
|
||||
else:
|
||||
log_and_print("Appropriate position already exists or no signal to act on.")
|
||||
|
||||
def close_position(self, symbol, is_buy):
|
||||
"""
|
||||
Closes positions of the opposite type (BUY/SELL) for this app's magic number.
|
||||
"""
|
||||
positions = mt5.positions_get(symbol=symbol)
|
||||
if not positions:
|
||||
log_and_print(f"No positions to close for symbol: {symbol}")
|
||||
return False
|
||||
|
||||
initial_balance = mt5.account_info().balance
|
||||
closed_any = False
|
||||
|
||||
for position in positions:
|
||||
# Close positions of the opposite type with the same magic number
|
||||
if position.magic == self.magic_number and (
|
||||
(is_buy and position.type == mt5.POSITION_TYPE_SELL) or
|
||||
(not is_buy and position.type == mt5.POSITION_TYPE_BUY)
|
||||
):
|
||||
close_request = {
|
||||
"action": mt5.TRADE_ACTION_DEAL,
|
||||
"symbol": symbol,
|
||||
"volume": position.volume,
|
||||
"type": mt5.ORDER_TYPE_BUY if position.type == mt5.POSITION_TYPE_SELL else mt5.ORDER_TYPE_SELL,
|
||||
"position": position.ticket,
|
||||
"deviation": 20,
|
||||
"magic": self.magic_number,
|
||||
"comment": COMMENT_ML,
|
||||
"type_time": mt5.ORDER_TIME_GTC,
|
||||
"type_filling": mt5.ORDER_FILLING_RETURN,
|
||||
}
|
||||
result = mt5.order_send(close_request)
|
||||
if result.retcode != mt5.TRADE_RETCODE_DONE:
|
||||
error_message = f"Failed to close position {position.ticket} for {symbol}: {result.retcode}"
|
||||
log_and_print(error_message, is_error=True)
|
||||
# If you want notifications, you could log or handle them differently here.
|
||||
else:
|
||||
log_and_print(f"Successfully closed position {position.ticket} for {symbol}")
|
||||
closed_any = True
|
||||
|
||||
if closed_any:
|
||||
final_balance = mt5.account_info().balance
|
||||
profit = final_balance - initial_balance
|
||||
success_message = f"Closed positions successfully, Profit: {profit}"
|
||||
log_and_print(success_message)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def check_and_execute_trades(self):
|
||||
"""
|
||||
Convenience method to perform the entire flow:
|
||||
generate signals, run strategy, and deselect symbol.
|
||||
"""
|
||||
mt5.symbol_select(self.symbol, True)
|
||||
buy, sell, _, _ = self.ml_signal_generation(self.symbol, N_BARS, TIMEFRAME)
|
||||
self.run_strategy(self.symbol, self.lot_size, buy, sell)
|
||||
mt5.symbol_select(self.symbol, False)
|
||||
log_and_print("Waiting for new signals...")
|
||||
|
||||
def is_market_open():
|
||||
"""
|
||||
Check if the current time is within the typical Forex trading session, adjusted for CET/CEST.
|
||||
Market closes at Friday 10:00 PM CET and opens at Sunday 11:00 PM CET.
|
||||
It is closed all day Saturday.
|
||||
"""
|
||||
current_time_utc = datetime.utcnow()
|
||||
# Adjust for Central European Time (UTC+1) or Central European Summer Time (UTC+2)
|
||||
current_time_cet = (
|
||||
current_time_utc + timedelta(hours=2)
|
||||
if time.localtime().tm_isdst
|
||||
else current_time_utc + timedelta(hours=1)
|
||||
)
|
||||
|
||||
# Friday after 10 PM CET
|
||||
if current_time_cet.weekday() == 4 and current_time_cet.hour >= 22:
|
||||
return False
|
||||
# Sunday before 11 PM CET
|
||||
elif current_time_cet.weekday() == 6 and current_time_cet.hour < 23:
|
||||
return False
|
||||
# All day Saturday
|
||||
elif current_time_cet.weekday() == 5:
|
||||
return False
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
if not mt5.initialize(login=name, server=serv, password=key):
|
||||
log_and_print("Failed to initialize MetaTrader 5", is_error=True)
|
||||
exit()
|
||||
|
||||
app = TradingApp(symbol=SYMBOL, lot_size=LOT_SIZE, magic_number=MAGIC_NUMBER)
|
||||
|
||||
# 1) Load the classification pipeline
|
||||
pipeline_path = "models/saved_models/best_rf_mb_pipeline.pkl"
|
||||
app.load_pipeline(pipeline_path)
|
||||
|
||||
while True:
|
||||
log_and_print("Checking market status...")
|
||||
if is_market_open():
|
||||
log_and_print("Market is open. Executing trades...")
|
||||
|
||||
# 2) Generate signals using the loaded pipeline
|
||||
# This pipeline is classification-based => SHIFTED labels [0,1,2]
|
||||
# ml_signal_generation() SHIFTs them back to [-1,0,+1] for signals
|
||||
buy_signal, sell_signal, _, _ = app.ml_signal_generation(
|
||||
symbol=app.symbol,
|
||||
n_bars=N_BARS,
|
||||
timeframe=TIMEFRAME
|
||||
)
|
||||
|
||||
# 3) Run strategy
|
||||
app.run_strategy(app.symbol, app.lot_size, buy_signal, sell_signal)
|
||||
else:
|
||||
log_and_print("Market is closed. No actions performed.")
|
||||
|
||||
time.sleep(SLEEP_TIME)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
log_and_print("Shutdown signal received.")
|
||||
# If you need a notification here, handle it (e.g., log, email, etc.).
|
||||
except Exception as e:
|
||||
error_message = f"An error occurred: {e}"
|
||||
log_and_print(error_message, is_error=True)
|
||||
# If you need a notification here, handle it (e.g., log, email, etc.).
|
||||
finally:
|
||||
mt5.shutdown()
|
||||
log_and_print("MetaTrader 5 shutdown completed.")
|
||||
# If you need a notification here, handle it (e.g., log, email, etc.).
|
||||
@@ -0,0 +1,407 @@
|
||||
# LIVE TRADING CODE FOR REGIME DETECTION CLASSIFICATION
|
||||
import sys
|
||||
import os
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1) SET PROJECT ROOT AND UPDATE PATH/WORKING DIRECTORY
|
||||
# ---------------------------------------------------------------------------
|
||||
project_root = Path.cwd().parent.parent # Adjust if your notebook is in notebooks/time_series
|
||||
sys.path.append(str(project_root))
|
||||
os.chdir(str(project_root))
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
import warnings
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
import MetaTrader5 as mt5
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import ta
|
||||
from datetime import datetime, timedelta
|
||||
import time
|
||||
import logging
|
||||
import joblib
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(
|
||||
filename='models/saved_models/trading_app.log',
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s %(levelname)s:%(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
|
||||
def log_and_print(message, is_error=False):
|
||||
"""
|
||||
Logs and prints a message.
|
||||
If is_error=True, logs at the ERROR level; otherwise logs at INFO level.
|
||||
"""
|
||||
if is_error:
|
||||
logging.error(message)
|
||||
else:
|
||||
logging.info(message)
|
||||
print(message)
|
||||
|
||||
# Update the login credentials and server information accordingly
|
||||
name = 66677507
|
||||
key = 'ST746$nG38'
|
||||
serv = 'ICMarketsSC-Demo'
|
||||
|
||||
# Global variables
|
||||
SYMBOL = "EURUSD"
|
||||
LOT_SIZE = 0.01
|
||||
TIMEFRAME = mt5.TIMEFRAME_D1
|
||||
N_BARS = 50000
|
||||
MAGIC_NUMBER = 234003
|
||||
SLEEP_TIME = 86400 # 24 hours in seconds
|
||||
COMMENT_ML = "Regime-Detection"
|
||||
|
||||
class TradingApp:
|
||||
def __init__(self, symbol, lot_size, magic_number):
|
||||
self.symbol = symbol
|
||||
self.lot_size = lot_size
|
||||
self.magic_number = magic_number
|
||||
self.pipeline = None # We'll store the loaded classification pipeline here
|
||||
self.last_retrain_time = None
|
||||
|
||||
def get_data(self, symbol, n, timeframe):
|
||||
"""
|
||||
Fetch the last 'n' bars from MetaTrader 5 for the given timeframe.
|
||||
"""
|
||||
rates = mt5.copy_rates_from_pos(symbol, timeframe, 0, n)
|
||||
rates_frame = pd.DataFrame(rates)
|
||||
rates_frame['time'] = pd.to_datetime(rates_frame['time'], unit='s')
|
||||
rates_frame.set_index('time', inplace=True)
|
||||
return rates_frame
|
||||
|
||||
def add_all_ta_features(self, df):
|
||||
"""
|
||||
Add technical analysis features to the DataFrame using the 'ta' library.
|
||||
"""
|
||||
df = ta.add_all_ta_features(
|
||||
df, open="open", high="high", low="low", close="close", volume="tick_volume", fillna=True
|
||||
)
|
||||
return df
|
||||
|
||||
def load_pipeline(self, pipeline_path):
|
||||
"""
|
||||
Loads a pre-trained classification pipeline (e.g., final_production_pipeline.pkl).
|
||||
This pipeline is expected to produce SHIFTED labels [0,1,2].
|
||||
"""
|
||||
self.pipeline = joblib.load(pipeline_path)
|
||||
logging.info(f"Loaded pipeline from {pipeline_path}")
|
||||
log_and_print(f"Loaded pipeline from {pipeline_path}")
|
||||
|
||||
def ml_signal_generation(self, symbol, n_bars, timeframe):
|
||||
"""
|
||||
Generate buy/sell signals using the loaded classification pipeline.
|
||||
The pipeline outputs SHIFTED labels in {0,1,2} => SHIFT them back to {-1,0,+1}.
|
||||
We'll interpret +1 => buy, -1 => sell, 0 => no trade.
|
||||
"""
|
||||
if self.pipeline is None:
|
||||
logging.error("No pipeline loaded. Call load_pipeline(...) first.")
|
||||
return False, False, True, True
|
||||
|
||||
# 1) Fetch new data
|
||||
df = self.get_data(symbol, n_bars, timeframe)
|
||||
# 2) Add TA features
|
||||
df = self.add_all_ta_features(df)
|
||||
df.fillna(method='ffill', inplace=True)
|
||||
|
||||
# 3) Prepare the features (the pipeline must handle columns in correct order)
|
||||
X_new = df
|
||||
|
||||
# 4) Predict SHIFTED classes
|
||||
preds_shifted = self.pipeline.predict(X_new)
|
||||
# SHIFT them back: 0->-1, 1->0, 2->+1
|
||||
preds = preds_shifted - 1
|
||||
|
||||
# Get the latest predicted class
|
||||
latest_pred = preds[-1]
|
||||
# If latest_pred == +1 => buy signal
|
||||
# If latest_pred == -1 => sell signal
|
||||
# If 0 => no trade
|
||||
buy_signal = (latest_pred == 1)
|
||||
sell_signal = (latest_pred == -1)
|
||||
|
||||
return buy_signal, sell_signal, not buy_signal, not sell_signal
|
||||
|
||||
def orders(self, symbol, lot, is_buy=True, id_position=None, sl=None, tp=None):
|
||||
"""
|
||||
Send an order (buy/sell) to MetaTrader 5.
|
||||
"""
|
||||
symbol_info = mt5.symbol_info(symbol)
|
||||
if symbol_info is None:
|
||||
log_and_print(f"Symbol {symbol} not found, can't place order.", is_error=True)
|
||||
return "Symbol not found"
|
||||
|
||||
# Make sure symbol is visible
|
||||
if not symbol_info.visible:
|
||||
if not mt5.symbol_select(symbol, True):
|
||||
log_and_print(f"Failed to select symbol {symbol}", is_error=True)
|
||||
return "Symbol not visible or could not be selected."
|
||||
|
||||
tick_info = mt5.symbol_info_tick(symbol)
|
||||
if tick_info is None:
|
||||
log_and_print(f"Could not get tick info for {symbol}.", is_error=True)
|
||||
return "Tick info unavailable"
|
||||
|
||||
# Check for valid bid/ask
|
||||
if tick_info.bid <= 0 or tick_info.ask <= 0:
|
||||
log_and_print(
|
||||
f"Zero or invalid bid/ask for {symbol}: bid={tick_info.bid}, ask={tick_info.ask}",
|
||||
is_error=True
|
||||
)
|
||||
return "Invalid prices"
|
||||
|
||||
# LOT SIZE VALIDATION
|
||||
lot = max(lot, symbol_info.volume_min)
|
||||
step = symbol_info.volume_step
|
||||
if step > 0:
|
||||
remainder = lot % step
|
||||
if remainder != 0:
|
||||
lot = lot - remainder + step
|
||||
if lot > symbol_info.volume_max:
|
||||
lot = symbol_info.volume_max
|
||||
|
||||
log_and_print(
|
||||
f"Adjusted lot size to {lot} (min={symbol_info.volume_min}, "
|
||||
f"step={symbol_info.volume_step}, max={symbol_info.volume_max})"
|
||||
)
|
||||
|
||||
# Force ORDER_FILLING_IOC
|
||||
filling_mode = mt5.ORDER_FILLING_IOC
|
||||
|
||||
order_type = mt5.ORDER_TYPE_BUY if is_buy else mt5.ORDER_TYPE_SELL
|
||||
deviation = 20
|
||||
order_price = tick_info.ask if is_buy else tick_info.bid
|
||||
|
||||
request = {
|
||||
"action": mt5.TRADE_ACTION_DEAL,
|
||||
"symbol": symbol,
|
||||
"volume": lot,
|
||||
"type": order_type,
|
||||
"deviation": deviation,
|
||||
"magic": self.magic_number,
|
||||
"comment": COMMENT_ML,
|
||||
"type_time": mt5.ORDER_TIME_GTC,
|
||||
"type_filling": filling_mode,
|
||||
}
|
||||
|
||||
if sl is not None:
|
||||
request["sl"] = sl
|
||||
if tp is not None:
|
||||
request["tp"] = tp
|
||||
if id_position is not None:
|
||||
request["position"] = id_position
|
||||
|
||||
log_and_print(f"Sending order request: {request}")
|
||||
result = mt5.order_send(request)
|
||||
|
||||
order_type_str = "BUY" if is_buy else "SELL"
|
||||
if result is None or result.retcode != mt5.TRADE_RETCODE_DONE:
|
||||
error_message = f"Order failed for {symbol}"
|
||||
if result:
|
||||
error_message += f", retcode={result.retcode}, comment={result.comment}"
|
||||
additional_info = (
|
||||
f"Date/Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
|
||||
f"Order Type: {order_type_str}\n"
|
||||
f"Lot Size: {lot}\n"
|
||||
f"SL: {sl if sl else 'None'}\n"
|
||||
f"TP: {tp if tp else 'None'}\n"
|
||||
f"Comment: {COMMENT_ML}\n"
|
||||
f"Request: {request}\n"
|
||||
f"Result: {result}"
|
||||
)
|
||||
# If you need notifications, you could log or handle them differently here.
|
||||
log_and_print(f"Order failed details: {additional_info}", is_error=True)
|
||||
else:
|
||||
success_message = f"Order successful for {symbol}, comment={result.comment}"
|
||||
additional_info = (
|
||||
f"Date/Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
|
||||
f"Order Type: {order_type_str}\n"
|
||||
f"Lot Size: {lot}\n"
|
||||
f"SL: {sl if sl else 'None'}\n"
|
||||
f"TP: {tp if tp else 'None'}\n"
|
||||
f"Comment: {COMMENT_ML}"
|
||||
)
|
||||
# If you need notifications, you could log or handle them differently here.
|
||||
log_and_print(success_message)
|
||||
|
||||
def get_positions_by_magic(self, symbol, magic_number):
|
||||
"""
|
||||
Retrieve positions for a specific symbol and magic number.
|
||||
"""
|
||||
all_positions = mt5.positions_get(symbol=symbol)
|
||||
if not all_positions:
|
||||
log_and_print("No positions found.", is_error=False)
|
||||
return []
|
||||
return [pos for pos in all_positions if pos.magic == magic_number]
|
||||
|
||||
def run_strategy(self, symbol, lot, buy_signal, sell_signal):
|
||||
"""
|
||||
Decide whether to open a buy or sell order based on signals,
|
||||
close opposite positions if needed, etc.
|
||||
"""
|
||||
log_and_print("------------------------------------------------------------------")
|
||||
log_and_print(
|
||||
f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}, "
|
||||
f"SYMBOL: {symbol}, BUY SIGNAL: {buy_signal}, SELL SIGNAL: {sell_signal}"
|
||||
)
|
||||
|
||||
positions = self.get_positions_by_magic(symbol, self.magic_number)
|
||||
has_buy = any(pos.type == mt5.POSITION_TYPE_BUY for pos in positions)
|
||||
has_sell = any(pos.type == mt5.POSITION_TYPE_SELL for pos in positions)
|
||||
|
||||
if buy_signal and not has_buy:
|
||||
if has_sell:
|
||||
log_and_print("Existing sell positions found. Attempting to close...")
|
||||
if self.close_position(symbol, is_buy=True):
|
||||
log_and_print("Sell positions closed. Placing new buy order.")
|
||||
self.orders(symbol, lot, is_buy=True)
|
||||
else:
|
||||
log_and_print("Failed to close sell positions.")
|
||||
else:
|
||||
self.orders(symbol, lot, is_buy=True)
|
||||
elif sell_signal and not has_sell:
|
||||
if has_buy:
|
||||
log_and_print("Existing buy positions found. Attempting to close...")
|
||||
if self.close_position(symbol, is_buy=False):
|
||||
log_and_print("Buy positions closed. Placing new sell order.")
|
||||
self.orders(symbol, lot, is_buy=False)
|
||||
else:
|
||||
log_and_print("Failed to close buy positions.")
|
||||
else:
|
||||
self.orders(symbol, lot, is_buy=False)
|
||||
else:
|
||||
log_and_print("Appropriate position already exists or no signal to act on.")
|
||||
|
||||
def close_position(self, symbol, is_buy):
|
||||
"""
|
||||
Close all positions of the opposite type for the given symbol & magic.
|
||||
"""
|
||||
positions = mt5.positions_get(symbol=symbol)
|
||||
if not positions:
|
||||
log_and_print(f"No positions to close for symbol: {symbol}")
|
||||
return False
|
||||
|
||||
initial_balance = mt5.account_info().balance
|
||||
closed_any = False
|
||||
|
||||
for position in positions:
|
||||
if position.magic == self.magic_number:
|
||||
# if is_buy==True => we want to close SELL positions
|
||||
# if is_buy==False => we want to close BUY positions
|
||||
if ((is_buy and position.type == mt5.POSITION_TYPE_SELL) or
|
||||
(not is_buy and position.type == mt5.POSITION_TYPE_BUY)):
|
||||
close_request = {
|
||||
"action": mt5.TRADE_ACTION_DEAL,
|
||||
"symbol": symbol,
|
||||
"volume": position.volume,
|
||||
"type": mt5.ORDER_TYPE_BUY if position.type == mt5.POSITION_TYPE_SELL else mt5.ORDER_TYPE_SELL,
|
||||
"position": position.ticket,
|
||||
"deviation": 20,
|
||||
"magic": self.magic_number,
|
||||
"comment": COMMENT_ML,
|
||||
"type_time": mt5.ORDER_TIME_GTC,
|
||||
"type_filling": mt5.ORDER_FILLING_RETURN,
|
||||
}
|
||||
result = mt5.order_send(close_request)
|
||||
if result.retcode != mt5.TRADE_RETCODE_DONE:
|
||||
error_message = (
|
||||
f"Failed to close position {position.ticket} for {symbol}: {result.retcode}"
|
||||
)
|
||||
log_and_print(error_message, is_error=True)
|
||||
# If you need notifications, you could log or handle them differently here.
|
||||
else:
|
||||
log_and_print(f"Successfully closed position {position.ticket} for {symbol}")
|
||||
closed_any = True
|
||||
|
||||
if closed_any:
|
||||
final_balance = mt5.account_info().balance
|
||||
profit = final_balance - initial_balance
|
||||
success_message = f"Closed positions successfully, Profit: {profit}"
|
||||
log_and_print(success_message)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def check_and_execute_trades(self):
|
||||
"""
|
||||
Called in the main loop: generate signals, run strategy, etc.
|
||||
"""
|
||||
mt5.symbol_select(self.symbol, True)
|
||||
buy, sell, _, _ = self.ml_signal_generation(self.symbol, N_BARS, TIMEFRAME)
|
||||
self.run_strategy(self.symbol, self.lot_size, buy, sell)
|
||||
mt5.symbol_select(self.symbol, False)
|
||||
log_and_print("Waiting for new signals...")
|
||||
|
||||
def is_market_open():
|
||||
"""
|
||||
Check if the current time is within the typical Forex trading session, adjusted for CET/CEST.
|
||||
Market closes at Friday 10:00 PM CET and opens at Sunday 11:00 PM CET.
|
||||
"""
|
||||
current_time_utc = datetime.utcnow()
|
||||
current_time_cet = (
|
||||
current_time_utc + timedelta(hours=2)
|
||||
if time.localtime().tm_isdst
|
||||
else current_time_utc + timedelta(hours=1)
|
||||
)
|
||||
|
||||
# Market closes Friday after 10 PM CET
|
||||
if current_time_cet.weekday() == 4 and current_time_cet.hour >= 22:
|
||||
return False
|
||||
# Market opens Sunday after 11 PM CET
|
||||
elif current_time_cet.weekday() == 6 and current_time_cet.hour < 23:
|
||||
return False
|
||||
# Closed all day Saturday
|
||||
elif current_time_cet.weekday() == 5:
|
||||
return False
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
if not mt5.initialize(login=name, server=serv, password=key):
|
||||
log_and_print("Failed to initialize MetaTrader 5", is_error=True)
|
||||
exit()
|
||||
|
||||
app = TradingApp(symbol=SYMBOL, lot_size=LOT_SIZE, magic_number=MAGIC_NUMBER)
|
||||
|
||||
# 1) Load the classification pipeline
|
||||
# Make sure this pipeline is a classification model expecting SHIFTED labels [0,1,2]
|
||||
pipeline_path = "models/saved_models/best_rf_rd_pipeline.pkl"
|
||||
app.load_pipeline(pipeline_path)
|
||||
|
||||
while True:
|
||||
log_and_print("Checking market status...")
|
||||
if is_market_open():
|
||||
log_and_print("Market is open. Executing trades...")
|
||||
|
||||
# 2) Generate signals using the loaded pipeline
|
||||
# This pipeline is classification-based => SHIFTED labels [0,1,2]
|
||||
# ml_signal_generation() SHIFTs them back to [-1,0,+1] for signals
|
||||
buy_signal, sell_signal, _, _ = app.ml_signal_generation(
|
||||
symbol=app.symbol,
|
||||
n_bars=N_BARS,
|
||||
timeframe=TIMEFRAME
|
||||
)
|
||||
|
||||
# 3) Run strategy
|
||||
app.run_strategy(app.symbol, app.lot_size, buy_signal, sell_signal)
|
||||
else:
|
||||
log_and_print("Market is closed. No actions performed.")
|
||||
|
||||
time.sleep(SLEEP_TIME)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
log_and_print("Shutdown signal received.")
|
||||
# If you need a notification here, handle it (e.g., log, email, etc.).
|
||||
except Exception as e:
|
||||
error_message = f"An error occurred: {e}"
|
||||
log_and_print(error_message, is_error=True)
|
||||
# If you need a notification here, handle it (e.g., log, email, etc.).
|
||||
finally:
|
||||
mt5.shutdown()
|
||||
log_and_print("MetaTrader 5 shutdown completed.")
|
||||
# If you need a notification here, handle it (e.g., log, email, etc.).
|
||||
@@ -0,0 +1,416 @@
|
||||
import sys
|
||||
import os
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1) SET PROJECT ROOT AND UPDATE PATH/WORKING DIRECTORY
|
||||
# ---------------------------------------------------------------------------
|
||||
project_root = Path.cwd().parent.parent # Adjust if your notebook is in notebooks/time_series
|
||||
sys.path.append(str(project_root))
|
||||
os.chdir(str(project_root))
|
||||
warnings.filterwarnings("ignore")
|
||||
import warnings
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
import MetaTrader5 as mt5
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import ta
|
||||
from datetime import datetime, timedelta
|
||||
from sklearn.model_selection import train_test_split
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from sklearn.ensemble import RandomForestRegressor
|
||||
from sklearn.feature_selection import SelectFromModel
|
||||
import time
|
||||
import logging
|
||||
import joblib
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(
|
||||
filename='models/saved_models/trading_app.log',
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s %(levelname)s:%(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
|
||||
def log_and_print(message, is_error=False):
|
||||
"""
|
||||
Logs and prints a message.
|
||||
If is_error=True, logs at the ERROR level; otherwise logs at INFO level.
|
||||
"""
|
||||
if is_error:
|
||||
logging.error(message)
|
||||
else:
|
||||
logging.info(message)
|
||||
print(message)
|
||||
|
||||
# Update the login credentials and server information accordingly
|
||||
name = 52868686
|
||||
key = 'kkk7s$zzz6'
|
||||
serv = 'ICMarketsSC-Demo'
|
||||
|
||||
# Global variables
|
||||
SYMBOL = "EURUSD"
|
||||
LOT_SIZE = 0.01
|
||||
TIMEFRAME = mt5.TIMEFRAME_D1
|
||||
N_BARS = 50000
|
||||
MAGIC_NUMBER = 234003
|
||||
SLEEP_TIME = 86400 # 4 hours in seconds
|
||||
COMMENT_ML = "regression return"
|
||||
|
||||
def select_features_rf_reg(X, y, estimator, max_features=20):
|
||||
"""
|
||||
Use a RandomForest (or similar) to select top 'max_features' features.
|
||||
"""
|
||||
selector = SelectFromModel(estimator=estimator, threshold=-np.inf, max_features=max_features).fit(X, y)
|
||||
X_transformed = selector.transform(X)
|
||||
selected_features_mask = selector.get_support()
|
||||
return X_transformed, selected_features_mask
|
||||
|
||||
class TradingApp:
|
||||
def __init__(self, symbol, lot_size, magic_number):
|
||||
self.symbol = symbol
|
||||
self.lot_size = lot_size
|
||||
self.magic_number = magic_number
|
||||
self.pipeline = None # We'll store the loaded pipeline here
|
||||
self.last_retrain_time = None
|
||||
|
||||
def get_data(self, symbol, n, timeframe):
|
||||
"""
|
||||
Fetch 'n' bars of historical data for the given symbol and timeframe.
|
||||
"""
|
||||
rates = mt5.copy_rates_from_pos(symbol, timeframe, 0, n)
|
||||
rates_frame = pd.DataFrame(rates)
|
||||
rates_frame['time'] = pd.to_datetime(rates_frame['time'], unit='s')
|
||||
rates_frame.set_index('time', inplace=True)
|
||||
return rates_frame
|
||||
|
||||
def add_all_ta_features(self, df):
|
||||
"""
|
||||
Add technical analysis features to the DataFrame using the 'ta' library.
|
||||
"""
|
||||
df = ta.add_all_ta_features(
|
||||
df,
|
||||
open="open",
|
||||
high="high",
|
||||
low="low",
|
||||
close="close",
|
||||
volume="tick_volume",
|
||||
fillna=True
|
||||
)
|
||||
return df
|
||||
|
||||
def load_pipeline(self, pipeline_path):
|
||||
"""
|
||||
Load a pre-trained pipeline (scaler + model + possibly feature selection)
|
||||
from disk, e.g. 'best_rf_pipeline.pkl'.
|
||||
"""
|
||||
self.pipeline = joblib.load(pipeline_path)
|
||||
logging.info(f"Loaded pipeline from {pipeline_path}")
|
||||
|
||||
def ml_signal_generation(self, symbol, n_bars, timeframe):
|
||||
"""
|
||||
Generate buy/sell signals using the loaded pipeline.
|
||||
Make sure the pipeline expects the same features as we create below.
|
||||
"""
|
||||
if self.pipeline is None:
|
||||
logging.error("No pipeline loaded. Call load_pipeline(...) first.")
|
||||
return False, False, True, True
|
||||
|
||||
# 1) Fetch new data
|
||||
df = self.get_data(symbol, n_bars, timeframe)
|
||||
# 2) Add TA features (if your pipeline doesn't handle feature eng, do it here)
|
||||
df = self.add_all_ta_features(df)
|
||||
df.fillna(method='ffill', inplace=True)
|
||||
|
||||
# 3) Prepare the features (the pipeline will do scaling/selection if included)
|
||||
X_new = df # If your pipeline expects specific columns, subset accordingly.
|
||||
|
||||
# 4) Predict with the pipeline
|
||||
predictions = self.pipeline.predict(X_new)
|
||||
latest_pred = predictions[-1] # Get the most recent bar's prediction
|
||||
|
||||
buy_signal = latest_pred > 0
|
||||
sell_signal = latest_pred < 0
|
||||
|
||||
return buy_signal, sell_signal, not buy_signal, not sell_signal
|
||||
|
||||
def calculate_future_returns(self, df):
|
||||
"""
|
||||
(Optional) Example function to calculate future returns for labeling.
|
||||
"""
|
||||
df["future_returns"] = df["close"].pct_change().shift(-1)
|
||||
return df.dropna()
|
||||
|
||||
def orders(self, symbol, lot, is_buy=True, id_position=None, sl=None, tp=None):
|
||||
"""
|
||||
Place an order (BUY or SELL) for the specified symbol and lot size.
|
||||
"""
|
||||
symbol_info = mt5.symbol_info(symbol)
|
||||
if symbol_info is None:
|
||||
log_and_print(f"Symbol {symbol} not found, can't place order.", is_error=True)
|
||||
return "Symbol not found"
|
||||
|
||||
# Make sure symbol is selected/visible
|
||||
if not symbol_info.visible:
|
||||
if not mt5.symbol_select(symbol, True):
|
||||
log_and_print(f"Failed to select symbol {symbol}", is_error=True)
|
||||
return "Symbol not visible or could not be selected."
|
||||
|
||||
tick_info = mt5.symbol_info_tick(symbol)
|
||||
if tick_info is None:
|
||||
log_and_print(f"Could not get tick info for {symbol}.", is_error=True)
|
||||
return "Tick info unavailable"
|
||||
|
||||
# Check for valid bid/ask
|
||||
if tick_info.bid <= 0 or tick_info.ask <= 0:
|
||||
log_and_print(
|
||||
f"Zero or invalid bid/ask for {symbol}: bid={tick_info.bid}, ask={tick_info.ask}",
|
||||
is_error=True
|
||||
)
|
||||
return "Invalid prices"
|
||||
|
||||
# ----------- LOT SIZE VALIDATION -----------
|
||||
lot = max(lot, symbol_info.volume_min)
|
||||
step = symbol_info.volume_step
|
||||
if step > 0:
|
||||
remainder = lot % step
|
||||
if remainder != 0:
|
||||
lot = lot - remainder + step
|
||||
if lot > symbol_info.volume_max:
|
||||
lot = symbol_info.volume_max
|
||||
|
||||
log_and_print(
|
||||
f"Adjusted lot size to {lot} (min={symbol_info.volume_min}, "
|
||||
f"step={symbol_info.volume_step}, max={symbol_info.volume_max})"
|
||||
)
|
||||
|
||||
# ----------- FORCE ORDER_FILLING_IOC -----------
|
||||
filling_mode = 1 # ORDER_FILLING_IOC
|
||||
|
||||
order_type = mt5.ORDER_TYPE_BUY if is_buy else mt5.ORDER_TYPE_SELL
|
||||
deviation = 20
|
||||
|
||||
request = {
|
||||
"action": mt5.TRADE_ACTION_DEAL,
|
||||
"symbol": symbol,
|
||||
"volume": lot,
|
||||
"type": order_type,
|
||||
"deviation": deviation,
|
||||
"magic": self.magic_number,
|
||||
"comment": COMMENT_ML,
|
||||
"type_time": mt5.ORDER_TIME_GTC,
|
||||
"type_filling": filling_mode,
|
||||
}
|
||||
|
||||
if sl is not None:
|
||||
request["sl"] = sl
|
||||
if tp is not None:
|
||||
request["tp"] = tp
|
||||
if id_position is not None:
|
||||
request["position"] = id_position
|
||||
|
||||
log_and_print(f"Sending order request: {request}")
|
||||
result = mt5.order_send(request)
|
||||
|
||||
order_type_str = "BUY" if is_buy else "SELL"
|
||||
if result is None or result.retcode != mt5.TRADE_RETCODE_DONE:
|
||||
error_message = f"Order failed for {symbol}"
|
||||
if result:
|
||||
error_message += f", retcode={result.retcode}, comment={result.comment}"
|
||||
additional_info = (
|
||||
f"Date/Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
|
||||
f"Order Type: {order_type_str}\n"
|
||||
f"Lot Size: {lot}\n"
|
||||
f"SL: {sl if sl else 'None'}\n"
|
||||
f"TP: {tp if tp else 'None'}\n"
|
||||
f"Comment: {COMMENT_ML}\n"
|
||||
f"Request: {request}\n"
|
||||
f"Result: {result}"
|
||||
)
|
||||
log_and_print(f"Order failed details: {additional_info}", is_error=True)
|
||||
else:
|
||||
success_message = f"Order successful for {symbol}, comment={result.comment}"
|
||||
additional_info = (
|
||||
f"Date/Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
|
||||
f"Order Type: {order_type_str}\n"
|
||||
f"Lot Size: {lot}\n"
|
||||
f"SL: {sl if sl else 'None'}\n"
|
||||
f"TP: {tp if tp else 'None'}\n"
|
||||
f"Comment: {COMMENT_ML}"
|
||||
)
|
||||
log_and_print(success_message)
|
||||
|
||||
def get_positions_by_magic(self, symbol, magic_number):
|
||||
"""
|
||||
Retrieve open positions for the specified symbol and magic number.
|
||||
"""
|
||||
all_positions = mt5.positions_get(symbol=symbol)
|
||||
if not all_positions:
|
||||
log_and_print("No positions found.", is_error=False)
|
||||
return []
|
||||
return [pos for pos in all_positions if pos.magic == magic_number]
|
||||
|
||||
def run_strategy(self, symbol, lot, buy_signal, sell_signal):
|
||||
"""
|
||||
Based on buy/sell signals, decide whether to open or close positions.
|
||||
"""
|
||||
log_and_print("------------------------------------------------------------------")
|
||||
log_and_print(
|
||||
f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}, "
|
||||
f"SYMBOL: {symbol}, BUY SIGNAL: {buy_signal}, SELL SIGNAL: {sell_signal}"
|
||||
)
|
||||
|
||||
# Retrieve positions based on the magic number to manage trades specific to this instance
|
||||
positions = self.get_positions_by_magic(symbol, self.magic_number)
|
||||
has_buy = any(pos.type == mt5.POSITION_TYPE_BUY for pos in positions)
|
||||
has_sell = any(pos.type == mt5.POSITION_TYPE_SELL for pos in positions)
|
||||
|
||||
# Decision making based on current signals and existing positions
|
||||
if buy_signal and not has_buy:
|
||||
if has_sell:
|
||||
log_and_print("Existing sell positions found. Attempting to close...")
|
||||
if self.close_position(symbol, is_buy=True):
|
||||
log_and_print("Sell positions closed. Placing new buy order.")
|
||||
self.orders(symbol, lot, is_buy=True)
|
||||
else:
|
||||
log_and_print("Failed to close sell positions.")
|
||||
else:
|
||||
self.orders(symbol, lot, is_buy=True)
|
||||
elif sell_signal and not has_sell:
|
||||
if has_buy:
|
||||
log_and_print("Existing buy positions found. Attempting to close...")
|
||||
if self.close_position(symbol, is_buy=False):
|
||||
log_and_print("Buy positions closed. Placing new sell order.")
|
||||
self.orders(symbol, lot, is_buy=False)
|
||||
else:
|
||||
log_and_print("Failed to close buy positions.")
|
||||
else:
|
||||
self.orders(symbol, lot, is_buy=False)
|
||||
else:
|
||||
log_and_print("Appropriate position already exists or no signal to act on.")
|
||||
|
||||
def close_position(self, symbol, is_buy):
|
||||
"""
|
||||
Closes positions of the opposite type (BUY/SELL) for this app's magic number.
|
||||
"""
|
||||
positions = mt5.positions_get(symbol=symbol)
|
||||
if not positions:
|
||||
log_and_print(f"No positions to close for symbol: {symbol}")
|
||||
return False
|
||||
|
||||
initial_balance = mt5.account_info().balance
|
||||
closed_any = False
|
||||
|
||||
for position in positions:
|
||||
# Close positions of the opposite type with the same magic number
|
||||
if position.magic == self.magic_number and (
|
||||
(is_buy and position.type == mt5.POSITION_TYPE_SELL) or
|
||||
(not is_buy and position.type == mt5.POSITION_TYPE_BUY)
|
||||
):
|
||||
close_request = {
|
||||
"action": mt5.TRADE_ACTION_DEAL,
|
||||
"symbol": symbol,
|
||||
"volume": position.volume,
|
||||
"type": mt5.ORDER_TYPE_BUY if position.type == mt5.POSITION_TYPE_SELL else mt5.ORDER_TYPE_SELL,
|
||||
"position": position.ticket,
|
||||
"deviation": 20,
|
||||
"magic": self.magic_number,
|
||||
"comment": COMMENT_ML,
|
||||
"type_time": mt5.ORDER_TIME_GTC,
|
||||
"type_filling": mt5.ORDER_FILLING_RETURN,
|
||||
}
|
||||
result = mt5.order_send(close_request)
|
||||
if result.retcode != mt5.TRADE_RETCODE_DONE:
|
||||
error_message = (
|
||||
f"Failed to close position {position.ticket} for {symbol}: {result.retcode}"
|
||||
)
|
||||
log_and_print(error_message, is_error=True)
|
||||
else:
|
||||
log_and_print(f"Successfully closed position {position.ticket} for {symbol}")
|
||||
closed_any = True
|
||||
|
||||
if closed_any:
|
||||
final_balance = mt5.account_info().balance
|
||||
profit = final_balance - initial_balance
|
||||
success_message = f"Closed positions successfully, Profit: {profit}"
|
||||
log_and_print(success_message)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def check_and_execute_trades(self):
|
||||
"""
|
||||
Convenience method to perform the entire flow:
|
||||
generate signals, run strategy, and deselect symbol.
|
||||
"""
|
||||
mt5.symbol_select(self.symbol, True)
|
||||
buy, sell, _, _ = self.ml_signal_generation(self.symbol, N_BARS, TIMEFRAME)
|
||||
self.run_strategy(self.symbol, self.lot_size, buy, sell)
|
||||
mt5.symbol_select(self.symbol, False)
|
||||
log_and_print("Waiting for new signals...")
|
||||
|
||||
def is_market_open():
|
||||
"""
|
||||
Check if the current time is within typical Forex trading session hours (CET/CEST).
|
||||
- Closes: Friday 10:00 PM CET
|
||||
- Opens: Sunday 11:00 PM CET
|
||||
- Closed all day Saturday
|
||||
"""
|
||||
current_time_utc = datetime.utcnow()
|
||||
# Adjust for CET (UTC+1) or CEST (UTC+2)
|
||||
current_time_cet = current_time_utc + timedelta(hours=2) if time.localtime().tm_isdst else current_time_utc + timedelta(hours=1)
|
||||
|
||||
# Friday after 10 PM CET
|
||||
if current_time_cet.weekday() == 4 and current_time_cet.hour >= 22:
|
||||
return False
|
||||
# Sunday before 11 PM CET
|
||||
elif current_time_cet.weekday() == 6 and current_time_cet.hour < 23:
|
||||
return False
|
||||
# All day Saturday
|
||||
elif current_time_cet.weekday() == 5:
|
||||
return False
|
||||
return True
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
if not mt5.initialize(login=name, server=serv, password=key):
|
||||
log_and_print("Failed to initialize MetaTrader 5", is_error=True)
|
||||
exit()
|
||||
|
||||
app = TradingApp(symbol=SYMBOL, lot_size=LOT_SIZE, magic_number=MAGIC_NUMBER)
|
||||
|
||||
# Load a previously trained pipeline (scaler + model, etc.)
|
||||
pipeline_path = "models/saved_models/best_rf_pipeline.pkl"
|
||||
app.load_pipeline(pipeline_path)
|
||||
log_and_print(f"Loaded final pipeline for {app.symbol}")
|
||||
|
||||
while True:
|
||||
log_and_print("Checking market status...")
|
||||
if is_market_open():
|
||||
log_and_print("Market is open. Executing trades...")
|
||||
|
||||
# Generate signals using the loaded pipeline
|
||||
buy_signal, sell_signal, _, _ = app.ml_signal_generation(
|
||||
symbol=app.symbol,
|
||||
n_bars=N_BARS,
|
||||
timeframe=TIMEFRAME
|
||||
)
|
||||
|
||||
# Run strategy
|
||||
app.run_strategy(app.symbol, app.lot_size, buy_signal, sell_signal)
|
||||
else:
|
||||
log_and_print("Market is closed. No actions performed.")
|
||||
|
||||
# Sleep for the configured interval (e.g., 4 hours)
|
||||
time.sleep(SLEEP_TIME)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
log_and_print("Shutdown signal received.")
|
||||
except Exception as e:
|
||||
error_message = f"An error occurred: {e}"
|
||||
log_and_print(error_message, is_error=True)
|
||||
finally:
|
||||
mt5.shutdown()
|
||||
log_and_print("MetaTrader 5 shutdown completed.")
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user