bbstrader 0.2.91__tar.gz → 0.2.92__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bbstrader-0.2.91/bbstrader.egg-info → bbstrader-0.2.92}/PKG-INFO +6 -2
- {bbstrader-0.2.91 → bbstrader-0.2.92}/README.md +3 -1
- bbstrader-0.2.92/bbstrader/__main__.py +50 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/btengine/data.py +1 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/btengine/execution.py +13 -2
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/btengine/performance.py +50 -1
- bbstrader-0.2.92/bbstrader/btengine/scripts.py +157 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/btengine/strategy.py +12 -2
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/compat.py +1 -1
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/config.py +2 -4
- bbstrader-0.2.92/bbstrader/core/utils.py +146 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/metatrader/__init__.py +2 -1
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/metatrader/account.py +27 -37
- bbstrader-0.2.92/bbstrader/metatrader/copier.py +735 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/metatrader/rates.py +6 -3
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/metatrader/risk.py +19 -8
- bbstrader-0.2.92/bbstrader/metatrader/scripts.py +81 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/metatrader/trade.py +165 -63
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/metatrader/utils.py +5 -2
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/models/ml.py +1 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/trading/execution.py +145 -32
- bbstrader-0.2.92/bbstrader/trading/script.py +155 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/trading/scripts.py +2 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/tseries.py +33 -7
- {bbstrader-0.2.91 → bbstrader-0.2.92/bbstrader.egg-info}/PKG-INFO +6 -2
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader.egg-info/SOURCES.txt +6 -0
- bbstrader-0.2.92/bbstrader.egg-info/entry_points.txt +2 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader.egg-info/requires.txt +2 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/requirements.txt +3 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/setup.py +6 -1
- bbstrader-0.2.91/bbstrader/core/utils.py +0 -57
- {bbstrader-0.2.91 → bbstrader-0.2.92}/LICENSE +0 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/MANIFEST.in +0 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/__ini__.py +0 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/btengine/__init__.py +0 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/btengine/backtest.py +0 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/btengine/event.py +0 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/btengine/portfolio.py +0 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/core/__init__.py +0 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/core/data.py +0 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/ibkr/__init__.py +0 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/ibkr/utils.py +0 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/models/__init__.py +0 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/models/factors.py +0 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/models/optimization.py +0 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/models/portfolio.py +0 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/models/risk.py +0 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/trading/__init__.py +0 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader/trading/strategies.py +0 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader.egg-info/dependency_links.txt +0 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/bbstrader.egg-info/top_level.txt +0 -0
- {bbstrader-0.2.91 → bbstrader-0.2.92}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: bbstrader
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.92
|
|
4
4
|
Summary: Simplified Investment & Trading Toolkit
|
|
5
5
|
Home-page: https://github.com/bbalouki/bbstrader
|
|
6
6
|
Download-URL: https://pypi.org/project/bbstrader/
|
|
@@ -55,6 +55,8 @@ Requires-Dist: financetoolkit
|
|
|
55
55
|
Requires-Dist: tables
|
|
56
56
|
Requires-Dist: lightgbm
|
|
57
57
|
Requires-Dist: alphalens-reloaded
|
|
58
|
+
Requires-Dist: pyfiglet
|
|
59
|
+
Requires-Dist: colorama
|
|
58
60
|
Provides-Extra: mt5
|
|
59
61
|
Requires-Dist: MetaTrader5; extra == "mt5"
|
|
60
62
|
Dynamic: author
|
|
@@ -104,6 +106,7 @@ It leverages statistical models and algorithms to perform tasks such as cointegr
|
|
|
104
106
|
- **Comprehensive Backtesting**: Assess the performance of trading strategies with historical market data to optimize parameters and strategies for live trading environments.
|
|
105
107
|
- **Integrated Risk Management**: Leverage advanced risk management techniques to adapt to changing market conditions and maintain control over risk exposure.
|
|
106
108
|
- **Automated Trading**: Execute trades automatically on the MT5 platform, with support for managing orders, positions, and risk in real-time.
|
|
109
|
+
- **Trade Copier**: Copy trades from one account to another or multiple accounts.
|
|
107
110
|
- **Flexible Framework**: Customize existing strategies or develop new ones with the flexible, modular architecture designed to accommodate traders' evolving needs.
|
|
108
111
|
- **Advanced Time Series Analysis**: Conduct in-depth analysis of financial time series data to identify patterns, trends, and relationships that can inform trading strategies.
|
|
109
112
|
You can read the full documentation [here](https://bbstrader.readthedocs.io/en/latest/index.html)
|
|
@@ -118,7 +121,8 @@ This Module currenlty support three brokers, [Admirals Group AS](https://cabinet
|
|
|
118
121
|
|
|
119
122
|
Then, you can install `bbstrader` using pip:
|
|
120
123
|
```bash
|
|
121
|
-
pip install bbstrader
|
|
124
|
+
pip install bbstrader # Mac and Linux
|
|
125
|
+
pip install bbstrader[MT5] # Windows
|
|
122
126
|
```
|
|
123
127
|
|
|
124
128
|
## Examples
|
|
@@ -30,6 +30,7 @@ It leverages statistical models and algorithms to perform tasks such as cointegr
|
|
|
30
30
|
- **Comprehensive Backtesting**: Assess the performance of trading strategies with historical market data to optimize parameters and strategies for live trading environments.
|
|
31
31
|
- **Integrated Risk Management**: Leverage advanced risk management techniques to adapt to changing market conditions and maintain control over risk exposure.
|
|
32
32
|
- **Automated Trading**: Execute trades automatically on the MT5 platform, with support for managing orders, positions, and risk in real-time.
|
|
33
|
+
- **Trade Copier**: Copy trades from one account to another or multiple accounts.
|
|
33
34
|
- **Flexible Framework**: Customize existing strategies or develop new ones with the flexible, modular architecture designed to accommodate traders' evolving needs.
|
|
34
35
|
- **Advanced Time Series Analysis**: Conduct in-depth analysis of financial time series data to identify patterns, trends, and relationships that can inform trading strategies.
|
|
35
36
|
You can read the full documentation [here](https://bbstrader.readthedocs.io/en/latest/index.html)
|
|
@@ -44,7 +45,8 @@ This Module currenlty support three brokers, [Admirals Group AS](https://cabinet
|
|
|
44
45
|
|
|
45
46
|
Then, you can install `bbstrader` using pip:
|
|
46
47
|
```bash
|
|
47
|
-
pip install bbstrader
|
|
48
|
+
pip install bbstrader # Mac and Linux
|
|
49
|
+
pip install bbstrader[MT5] # Windows
|
|
48
50
|
```
|
|
49
51
|
|
|
50
52
|
## Examples
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
import pyfiglet
|
|
5
|
+
from colorama import Fore
|
|
6
|
+
|
|
7
|
+
from bbstrader.btengine.scripts import backtest
|
|
8
|
+
from bbstrader.metatrader.scripts import copy_trades
|
|
9
|
+
from bbstrader.trading.script import execute_strategy
|
|
10
|
+
|
|
11
|
+
DESCRIPTION = "BBSTRADER"
|
|
12
|
+
USAGE_TEXT = """
|
|
13
|
+
Usage:
|
|
14
|
+
python -m bbstrader --run <module> [options]
|
|
15
|
+
|
|
16
|
+
Modules:
|
|
17
|
+
copier: Copy trades from one MetaTrader account to another or multiple accounts
|
|
18
|
+
backtest: Backtest a strategy, see bbstrader.btengine.backtest.run_backtest
|
|
19
|
+
execution: Execute a strategy, see bbstrader.trading.execution.MT5ExecutionEngine
|
|
20
|
+
|
|
21
|
+
run <module> --help for more information on the module
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
FONT = pyfiglet.figlet_format("BBSTRADER", font="big")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main():
|
|
28
|
+
print(Fore.BLUE + FONT)
|
|
29
|
+
parser = argparse.ArgumentParser(
|
|
30
|
+
prog="BBSTRADER",
|
|
31
|
+
usage=USAGE_TEXT,
|
|
32
|
+
description=DESCRIPTION,
|
|
33
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
34
|
+
add_help=False,
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument("--run", type=str, nargs="?", default=None, help="Run a module")
|
|
37
|
+
args, unknown = parser.parse_known_args()
|
|
38
|
+
if ("-h" in sys.argv or "--help" in sys.argv) and args.run is None:
|
|
39
|
+
print(USAGE_TEXT)
|
|
40
|
+
sys.exit(0)
|
|
41
|
+
if args.run == "copier":
|
|
42
|
+
copy_trades(unknown)
|
|
43
|
+
elif args.run == "backtest":
|
|
44
|
+
backtest(unknown)
|
|
45
|
+
elif args.run == "execution":
|
|
46
|
+
execute_strategy(unknown)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if __name__ == "__main__":
|
|
50
|
+
main()
|
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
from abc import ABCMeta, abstractmethod
|
|
2
2
|
from queue import Queue
|
|
3
3
|
|
|
4
|
+
from loguru import logger
|
|
5
|
+
|
|
4
6
|
from bbstrader.btengine.data import DataHandler
|
|
5
7
|
from bbstrader.btengine.event import FillEvent, OrderEvent
|
|
8
|
+
from bbstrader.config import BBSTRADER_DIR
|
|
6
9
|
from bbstrader.metatrader.account import Account
|
|
7
10
|
|
|
8
11
|
__all__ = ["ExecutionHandler", "SimExecutionHandler", "MT5ExecutionHandler"]
|
|
9
12
|
|
|
10
13
|
|
|
14
|
+
logger.add(
|
|
15
|
+
f"{BBSTRADER_DIR}/logs/execution.log",
|
|
16
|
+
enqueue=True,
|
|
17
|
+
level="INFO",
|
|
18
|
+
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name} | {message}",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
11
22
|
class ExecutionHandler(metaclass=ABCMeta):
|
|
12
23
|
"""
|
|
13
24
|
The ExecutionHandler abstract class handles the interaction
|
|
@@ -59,7 +70,7 @@ class SimExecutionHandler(ExecutionHandler):
|
|
|
59
70
|
"""
|
|
60
71
|
self.events = events
|
|
61
72
|
self.bardata = data
|
|
62
|
-
self.logger = kwargs.get("logger")
|
|
73
|
+
self.logger = kwargs.get("logger") or logger
|
|
63
74
|
|
|
64
75
|
def execute_order(self, event: OrderEvent):
|
|
65
76
|
"""
|
|
@@ -123,7 +134,7 @@ class MT5ExecutionHandler(ExecutionHandler):
|
|
|
123
134
|
"""
|
|
124
135
|
self.events = events
|
|
125
136
|
self.bardata = data
|
|
126
|
-
self.logger = kwargs.get("logger")
|
|
137
|
+
self.logger = kwargs.get("logger") or logger
|
|
127
138
|
self.__account = Account(**kwargs)
|
|
128
139
|
|
|
129
140
|
def _calculate_lot(self, symbol, quantity, price):
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from typing import Dict, List
|
|
1
2
|
import warnings
|
|
2
3
|
|
|
3
4
|
import matplotlib.pyplot as plt
|
|
@@ -19,8 +20,56 @@ __all__ = [
|
|
|
19
20
|
"plot_returns_and_dd",
|
|
20
21
|
"plot_monthly_yearly_returns",
|
|
21
22
|
"show_qs_stats",
|
|
23
|
+
"get_asset_performances",
|
|
24
|
+
"get_perfbased_weights",
|
|
22
25
|
]
|
|
23
26
|
|
|
27
|
+
def get_asset_performances(
|
|
28
|
+
portfolio: pd.DataFrame, assets: List[str], plot=True, strategy=""
|
|
29
|
+
) -> pd.Series:
|
|
30
|
+
"""
|
|
31
|
+
Calculate the performance of the assets in the portfolio.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
portfolio (pd.DataFrame): The portfolio DataFrame.
|
|
35
|
+
assets (List[str]): The list of assets to calculate the performance for.
|
|
36
|
+
plot (bool): Whether to plot the performance of the assets.
|
|
37
|
+
strategy (str): The name of the strategy.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
pd.Series: The performance of the assets.
|
|
41
|
+
"""
|
|
42
|
+
asset_prices = portfolio[assets]
|
|
43
|
+
asset_prices = asset_prices.abs()
|
|
44
|
+
asset_prices.replace(0, np.nan, inplace=True)
|
|
45
|
+
asset_prices.ffill(inplace=True)
|
|
46
|
+
asset_returns = asset_prices.pct_change()
|
|
47
|
+
asset_returns.replace([np.inf, -np.inf], np.nan, inplace=True)
|
|
48
|
+
asset_returns.fillna(0, inplace=True)
|
|
49
|
+
asset_cum_returns = (1.0 + asset_returns).cumprod()
|
|
50
|
+
if plot:
|
|
51
|
+
asset_cum_returns.plot(figsize=(12, 6), title=f"{strategy} Strategy Assets Performance")
|
|
52
|
+
plt.show()
|
|
53
|
+
return asset_cum_returns.iloc[-1] - 1
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_perfbased_weights(performances) -> Dict[str, float]:
|
|
57
|
+
"""
|
|
58
|
+
Calculate the weights of the assets based on their performances.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
performances (pd.Series): The performances of the assets.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Dict[str, float]: The weights of the assets.
|
|
65
|
+
"""
|
|
66
|
+
weights = (
|
|
67
|
+
performances.to_frame()
|
|
68
|
+
.assign(weight=performances.values / performances.sum())
|
|
69
|
+
.weight.to_dict()
|
|
70
|
+
)
|
|
71
|
+
return weights
|
|
72
|
+
|
|
24
73
|
|
|
25
74
|
def create_sharpe_ratio(returns, periods=252) -> float:
|
|
26
75
|
"""
|
|
@@ -173,7 +222,7 @@ def plot_returns_and_dd(df: pd.DataFrame, benchmark: str, title):
|
|
|
173
222
|
# To avoid errors, we use the try-except block
|
|
174
223
|
# in case the benchmark is not available
|
|
175
224
|
try:
|
|
176
|
-
bm = yf.download(benchmark, start=first_date, end=last_date)
|
|
225
|
+
bm = yf.download(benchmark, start=first_date, end=last_date, auto_adjust=True)
|
|
177
226
|
bm["log_return"] = np.log(bm["Close"] / bm["Close"].shift(1))
|
|
178
227
|
# Use exponential to get cumulative returns
|
|
179
228
|
bm_returns = np.exp(np.cumsum(bm["log_return"].fillna(0)))
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from bbstrader.btengine.backtest import run_backtest
|
|
8
|
+
from bbstrader.btengine.data import (
|
|
9
|
+
CSVDataHandler,
|
|
10
|
+
DataHandler,
|
|
11
|
+
EODHDataHandler,
|
|
12
|
+
FMPDataHandler,
|
|
13
|
+
MT5DataHandler,
|
|
14
|
+
YFDataHandler,
|
|
15
|
+
)
|
|
16
|
+
from bbstrader.btengine.execution import (
|
|
17
|
+
ExecutionHandler,
|
|
18
|
+
MT5ExecutionHandler,
|
|
19
|
+
SimExecutionHandler,
|
|
20
|
+
)
|
|
21
|
+
from bbstrader.core.utils import load_class, load_module
|
|
22
|
+
|
|
23
|
+
BACKTEST_PATH = os.path.expanduser("~/.bbstrader/backtest/backtest.py")
|
|
24
|
+
CONFIG_PATH = os.path.expanduser("~/.bbstrader/backtest/backtest.json")
|
|
25
|
+
|
|
26
|
+
DATA_HANDLER_MAP = {
|
|
27
|
+
"csv": CSVDataHandler,
|
|
28
|
+
"mt5": MT5DataHandler,
|
|
29
|
+
"yf": YFDataHandler,
|
|
30
|
+
"eodh": EODHDataHandler,
|
|
31
|
+
"fmp": FMPDataHandler,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
EXECUTION_HANDLER_MAP = {
|
|
35
|
+
"sim": SimExecutionHandler,
|
|
36
|
+
"mt5": MT5ExecutionHandler,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_exc_handler(module, handler_name):
|
|
41
|
+
return load_class(module, handler_name, ExecutionHandler)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def load_data_handler(module, handler_name):
|
|
45
|
+
return load_class(module, handler_name, DataHandler)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load_strategy(module, strategy_name):
|
|
49
|
+
from bbstrader.btengine.strategy import MT5Strategy, Strategy
|
|
50
|
+
|
|
51
|
+
return load_class(module, strategy_name, (Strategy, MT5Strategy))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def load_config(config_path, strategy_name):
|
|
55
|
+
if not os.path.exists(config_path):
|
|
56
|
+
raise FileNotFoundError(
|
|
57
|
+
f"Configuration file {config_path} not found. Please create it."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
with open(config_path, "r") as f:
|
|
61
|
+
config = json.load(f)
|
|
62
|
+
try:
|
|
63
|
+
config = config[strategy_name]
|
|
64
|
+
except KeyError:
|
|
65
|
+
raise ValueError(
|
|
66
|
+
f"Strategy {strategy_name} not found in the configuration file."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
required_fields = ["symbol_list", "start_date", "data_handler", "execution_handler"]
|
|
70
|
+
for field in required_fields:
|
|
71
|
+
if not config.get(field):
|
|
72
|
+
raise ValueError(f"{field} is required in the configuration file.")
|
|
73
|
+
|
|
74
|
+
config["start_date"] = datetime.strptime(config["start_date"], "%Y-%m-%d")
|
|
75
|
+
|
|
76
|
+
if config.get("execution_handler") not in EXECUTION_HANDLER_MAP:
|
|
77
|
+
try:
|
|
78
|
+
backtest_module = load_module(BACKTEST_PATH)
|
|
79
|
+
exc_handler_class = load_exc_handler(
|
|
80
|
+
backtest_module, config["execution_handler"]
|
|
81
|
+
)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
raise ValueError(f"Invalid execution handler: {e}")
|
|
84
|
+
else:
|
|
85
|
+
exc_handler_class = EXECUTION_HANDLER_MAP[config["execution_handler"]]
|
|
86
|
+
|
|
87
|
+
if config.get("data_handler") not in DATA_HANDLER_MAP:
|
|
88
|
+
try:
|
|
89
|
+
backtest_module = load_module(BACKTEST_PATH)
|
|
90
|
+
data_handler_class = load_data_handler(
|
|
91
|
+
backtest_module, config["data_handler"]
|
|
92
|
+
)
|
|
93
|
+
except Exception as e:
|
|
94
|
+
raise ValueError(f"Invalid data handler: {e}")
|
|
95
|
+
else:
|
|
96
|
+
data_handler_class = DATA_HANDLER_MAP[config["data_handler"]]
|
|
97
|
+
|
|
98
|
+
config["execution_handler"] = exc_handler_class
|
|
99
|
+
config["data_handler"] = data_handler_class
|
|
100
|
+
|
|
101
|
+
return config
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def backtest(unknown):
|
|
105
|
+
HELP_MSG = """
|
|
106
|
+
Usage:
|
|
107
|
+
python -m bbstrader --run backtest [options]
|
|
108
|
+
|
|
109
|
+
Options:
|
|
110
|
+
-s, --strategy: Strategy class name to run
|
|
111
|
+
-c, --config: Configuration file path (default: ~/.bbstrader/backtest/backtest.json)
|
|
112
|
+
-p, --path: Path to the backtest file (default: ~/.bbstrader/backtest/backtest.py)
|
|
113
|
+
|
|
114
|
+
Note:
|
|
115
|
+
The configuration file must contain all the required parameters
|
|
116
|
+
for the data handler and execution handler and strategy.
|
|
117
|
+
See bbstrader.btengine.BacktestEngine for more details on the parameters.
|
|
118
|
+
"""
|
|
119
|
+
if "-h" in unknown or "--help" in unknown:
|
|
120
|
+
print(HELP_MSG)
|
|
121
|
+
sys.exit(0)
|
|
122
|
+
|
|
123
|
+
parser = argparse.ArgumentParser(description="Backtesting Engine CLI")
|
|
124
|
+
parser.add_argument(
|
|
125
|
+
"-s", "--strategy", type=str, required=True, help="Strategy class name to run"
|
|
126
|
+
)
|
|
127
|
+
parser.add_argument(
|
|
128
|
+
"-c", "--config", type=str, default=CONFIG_PATH, help="Configuration file path"
|
|
129
|
+
)
|
|
130
|
+
parser.add_argument(
|
|
131
|
+
"-p",
|
|
132
|
+
"--path",
|
|
133
|
+
type=str,
|
|
134
|
+
default=BACKTEST_PATH,
|
|
135
|
+
help="Path to the backtest file",
|
|
136
|
+
)
|
|
137
|
+
args = parser.parse_args(unknown)
|
|
138
|
+
config = load_config(args.config, args.strategy)
|
|
139
|
+
strategy_module = load_module(args.path)
|
|
140
|
+
strategy_class = load_strategy(strategy_module, args.strategy)
|
|
141
|
+
|
|
142
|
+
symbol_list = config.pop("symbol_list")
|
|
143
|
+
start_date = config.pop("start_date")
|
|
144
|
+
data_handler = config.pop("data_handler")
|
|
145
|
+
execution_handler = config.pop("execution_handler")
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
run_backtest(
|
|
149
|
+
symbol_list,
|
|
150
|
+
start_date,
|
|
151
|
+
data_handler,
|
|
152
|
+
strategy_class,
|
|
153
|
+
exc_handler=execution_handler,
|
|
154
|
+
**config,
|
|
155
|
+
)
|
|
156
|
+
except Exception as e:
|
|
157
|
+
print(f"Error: {e}")
|
|
@@ -7,9 +7,12 @@ from typing import Dict, List, Literal, Union
|
|
|
7
7
|
import numpy as np
|
|
8
8
|
import pandas as pd
|
|
9
9
|
import pytz
|
|
10
|
+
from loguru import logger
|
|
10
11
|
|
|
11
12
|
from bbstrader.btengine.data import DataHandler
|
|
12
13
|
from bbstrader.btengine.event import FillEvent, SignalEvent
|
|
14
|
+
from bbstrader.config import BBSTRADER_DIR
|
|
15
|
+
from bbstrader.core.utils import TradeSignal
|
|
13
16
|
from bbstrader.metatrader.account import (
|
|
14
17
|
Account,
|
|
15
18
|
AdmiralMarktsGroup,
|
|
@@ -17,10 +20,16 @@ from bbstrader.metatrader.account import (
|
|
|
17
20
|
)
|
|
18
21
|
from bbstrader.metatrader.rates import Rates
|
|
19
22
|
from bbstrader.models.optimization import optimized_weights
|
|
20
|
-
from bbstrader.core.utils import TradeSignal
|
|
21
23
|
|
|
22
24
|
__all__ = ["Strategy", "MT5Strategy"]
|
|
23
25
|
|
|
26
|
+
logger.add(
|
|
27
|
+
f"{BBSTRADER_DIR}/logs/strategy.log",
|
|
28
|
+
enqueue=True,
|
|
29
|
+
level="INFO",
|
|
30
|
+
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {name} | {message}",
|
|
31
|
+
)
|
|
32
|
+
|
|
24
33
|
|
|
25
34
|
class Strategy(metaclass=ABCMeta):
|
|
26
35
|
"""
|
|
@@ -91,10 +100,11 @@ class MT5Strategy(Strategy):
|
|
|
91
100
|
self.risk_budget = self._check_risk_budget(**kwargs)
|
|
92
101
|
self.max_trades = kwargs.get("max_trades", {s: 1 for s in self.symbols})
|
|
93
102
|
self.tf = kwargs.get("time_frame", "D1")
|
|
94
|
-
self.logger = kwargs.get("logger")
|
|
103
|
+
self.logger = kwargs.get("logger") or logger
|
|
95
104
|
if self.mode == "backtest":
|
|
96
105
|
self._initialize_portfolio()
|
|
97
106
|
self.kwargs = kwargs
|
|
107
|
+
self.periodes = 0
|
|
98
108
|
|
|
99
109
|
@property
|
|
100
110
|
def cash(self) -> float:
|
|
@@ -7,17 +7,15 @@ TERMINAL = "\\terminal64.exe"
|
|
|
7
7
|
BASE_FOLDER = "C:\\Program Files\\"
|
|
8
8
|
|
|
9
9
|
AMG_PATH = BASE_FOLDER + "Admirals Group MT5 Terminal" + TERMINAL
|
|
10
|
-
XCB_PATH = BASE_FOLDER + "4xCube MT5 Terminal" + TERMINAL
|
|
11
|
-
TML_PATH = BASE_FOLDER + "Trinota Markets MetaTrader 5 Terminal" + TERMINAL
|
|
12
10
|
PGL_PATH = BASE_FOLDER + "Pepperstone MetaTrader 5" + TERMINAL
|
|
13
11
|
FTMO_PATH = BASE_FOLDER + "FTMO MetaTrader 5" + TERMINAL
|
|
12
|
+
JGM_PATH = BASE_FOLDER + "JustMarkets MetaTrader 5" + TERMINAL
|
|
14
13
|
|
|
15
14
|
BROKERS_PATHS = {
|
|
16
15
|
"AMG": AMG_PATH,
|
|
17
16
|
"FTMO": FTMO_PATH,
|
|
18
|
-
"XCB": XCB_PATH,
|
|
19
|
-
"TML": TML_PATH,
|
|
20
17
|
"PGL": PGL_PATH,
|
|
18
|
+
"JGM": JGM_PATH,
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import configparser
|
|
2
|
+
import importlib
|
|
3
|
+
import importlib.util
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Any, Dict, List
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_module(file_path):
|
|
11
|
+
"""Load a module from a file path.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
file_path: Path to the file to load.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
The loaded module.
|
|
18
|
+
"""
|
|
19
|
+
if not os.path.exists(file_path):
|
|
20
|
+
raise FileNotFoundError(
|
|
21
|
+
f"Strategy file {file_path} not found. Please create it."
|
|
22
|
+
)
|
|
23
|
+
spec = importlib.util.spec_from_file_location("strategies", file_path)
|
|
24
|
+
module = importlib.util.module_from_spec(spec)
|
|
25
|
+
spec.loader.exec_module(module)
|
|
26
|
+
return module
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def load_class(module, class_name, base_class):
|
|
30
|
+
"""Load a class from a module.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
module: The module to load the class from.
|
|
34
|
+
class_name: The name of the class to load.
|
|
35
|
+
base_class: The base class that the class must inherit from.
|
|
36
|
+
"""
|
|
37
|
+
if not hasattr(module, class_name):
|
|
38
|
+
raise AttributeError(f"{class_name} not found in {module}")
|
|
39
|
+
class_ = getattr(module, class_name)
|
|
40
|
+
if not issubclass(class_, base_class):
|
|
41
|
+
raise TypeError(f"{class_name} must inherit from {base_class}.")
|
|
42
|
+
return class_
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def auto_convert(value):
|
|
46
|
+
"""Convert string values to appropriate data types"""
|
|
47
|
+
if value.lower() in {"true", "false"}: # Boolean
|
|
48
|
+
return value.lower() == "true"
|
|
49
|
+
elif value.lower() in {"none", "null"}: # None
|
|
50
|
+
return None
|
|
51
|
+
elif value.isdigit():
|
|
52
|
+
return int(value)
|
|
53
|
+
try:
|
|
54
|
+
return float(value)
|
|
55
|
+
except ValueError:
|
|
56
|
+
return value
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def dict_from_ini(file_path, sections: str | List[str] = None) -> Dict[str, Any]:
|
|
60
|
+
"""Reads an INI file and converts it to a dictionary with proper data types.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
file_path: Path to the INI file to read.
|
|
64
|
+
sections: Optional list of sections to read from the INI file.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
A dictionary containing the INI file contents with proper data types.
|
|
68
|
+
"""
|
|
69
|
+
config = configparser.ConfigParser()
|
|
70
|
+
config.read(file_path)
|
|
71
|
+
|
|
72
|
+
ini_dict = {}
|
|
73
|
+
for section in config.sections():
|
|
74
|
+
ini_dict[section] = {
|
|
75
|
+
key: auto_convert(value) for key, value in config.items(section)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if isinstance(sections, str):
|
|
79
|
+
try:
|
|
80
|
+
return ini_dict[sections]
|
|
81
|
+
except KeyError:
|
|
82
|
+
raise KeyError(f"{sections} not found in the {file_path} file")
|
|
83
|
+
if isinstance(sections, list):
|
|
84
|
+
sect_dict = {}
|
|
85
|
+
for section in sections:
|
|
86
|
+
try:
|
|
87
|
+
sect_dict[section] = ini_dict[section]
|
|
88
|
+
except KeyError:
|
|
89
|
+
raise KeyError(f"{section} not found in the {file_path} file")
|
|
90
|
+
return ini_dict
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class TradeAction(Enum):
|
|
94
|
+
"""
|
|
95
|
+
An enumeration class for trade actions.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
BUY = "LONG"
|
|
99
|
+
LONG = "LONG"
|
|
100
|
+
SELL = "SHORT"
|
|
101
|
+
EXIT = "EXIT"
|
|
102
|
+
BMKT = "BMKT"
|
|
103
|
+
SMKT = "SMKT"
|
|
104
|
+
BLMT = "BLMT"
|
|
105
|
+
SLMT = "SLMT"
|
|
106
|
+
BSTP = "BSTP"
|
|
107
|
+
SSTP = "SSTP"
|
|
108
|
+
SHORT = "SHORT"
|
|
109
|
+
BSTPLMT = "BSTPLMT"
|
|
110
|
+
SSTPLMT = "SSTPLMT"
|
|
111
|
+
EXIT_LONG = "EXIT_LONG"
|
|
112
|
+
EXIT_SHORT = "EXIT_SHORT"
|
|
113
|
+
EXIT_STOP = "EXIT_STOP"
|
|
114
|
+
EXIT_LIMIT = "EXIT_LIMIT"
|
|
115
|
+
EXIT_LONG_STOP = "EXIT_LONG_STOP"
|
|
116
|
+
EXIT_LONG_LIMIT = "EXIT_LONG_LIMIT"
|
|
117
|
+
EXIT_SHORT_STOP = "EXIT_SHORT_STOP"
|
|
118
|
+
EXIT_SHORT_LIMIT = "EXIT_SHORT_LIMIT"
|
|
119
|
+
EXIT_LONG_STOP_LIMIT = "EXIT_LONG_STOP_LIMIT"
|
|
120
|
+
EXIT_SHORT_STOP_LIMIT = "EXIT_SHORT_STOP_LIMIT"
|
|
121
|
+
EXIT_PROFITABLES = "EXIT_PROFITABLES"
|
|
122
|
+
EXIT_LOSINGS = "EXIT_LOSINGS"
|
|
123
|
+
EXIT_ALL_POSITIONS = "EXIT_ALL_POSITIONS"
|
|
124
|
+
EXIT_ALL_ORDERS = "EXIT_ALL_ORDERS"
|
|
125
|
+
|
|
126
|
+
def __str__(self):
|
|
127
|
+
return self.value
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass()
|
|
131
|
+
class TradeSignal:
|
|
132
|
+
"""
|
|
133
|
+
A dataclass for storing trading signal.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
id: int
|
|
137
|
+
symbol: str
|
|
138
|
+
action: TradeAction
|
|
139
|
+
price: float = None
|
|
140
|
+
stoplimit: float = None
|
|
141
|
+
|
|
142
|
+
def __repr__(self):
|
|
143
|
+
return (
|
|
144
|
+
f"TradeSignal(id={self.id}, symbol='{self.symbol}', "
|
|
145
|
+
f"action='{self.action.value}', price={self.price}, stoplimit={self.stoplimit})"
|
|
146
|
+
)
|
|
@@ -3,4 +3,5 @@ from bbstrader.metatrader.account import * # noqa: F403
|
|
|
3
3
|
from bbstrader.metatrader.rates import * # noqa: F403
|
|
4
4
|
from bbstrader.metatrader.risk import * # noqa: F403
|
|
5
5
|
from bbstrader.metatrader.trade import * # noqa: F403
|
|
6
|
-
from bbstrader.metatrader.utils import * # noqa: F403
|
|
6
|
+
from bbstrader.metatrader.utils import * # noqa: F403*
|
|
7
|
+
from bbstrader.metatrader.copier import * # noqa: F403
|