bbstrader 0.2.91__py3-none-any.whl → 0.2.93__py3-none-any.whl
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.
Potentially problematic release.
This version of bbstrader might be problematic. Click here for more details.
- bbstrader/__main__.py +50 -0
- bbstrader/btengine/data.py +12 -9
- bbstrader/btengine/execution.py +13 -2
- bbstrader/btengine/performance.py +50 -1
- bbstrader/btengine/scripts.py +157 -0
- bbstrader/btengine/strategy.py +12 -2
- bbstrader/compat.py +2 -2
- bbstrader/config.py +2 -4
- bbstrader/core/utils.py +90 -1
- bbstrader/metatrader/__init__.py +2 -1
- bbstrader/metatrader/account.py +29 -39
- bbstrader/metatrader/copier.py +745 -0
- bbstrader/metatrader/rates.py +6 -3
- bbstrader/metatrader/risk.py +19 -8
- bbstrader/metatrader/scripts.py +81 -0
- bbstrader/metatrader/trade.py +178 -66
- bbstrader/metatrader/utils.py +5 -2
- bbstrader/models/ml.py +20 -12
- bbstrader/trading/execution.py +150 -33
- bbstrader/trading/script.py +155 -0
- bbstrader/trading/scripts.py +2 -0
- bbstrader/tseries.py +33 -7
- {bbstrader-0.2.91.dist-info → bbstrader-0.2.93.dist-info}/METADATA +6 -2
- bbstrader-0.2.93.dist-info/RECORD +44 -0
- {bbstrader-0.2.91.dist-info → bbstrader-0.2.93.dist-info}/WHEEL +1 -1
- bbstrader-0.2.93.dist-info/entry_points.txt +2 -0
- bbstrader-0.2.91.dist-info/RECORD +0 -38
- {bbstrader-0.2.91.dist-info → bbstrader-0.2.93.dist-info}/LICENSE +0 -0
- {bbstrader-0.2.91.dist-info → bbstrader-0.2.93.dist-info}/top_level.txt +0 -0
bbstrader/__main__.py
ADDED
|
@@ -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
|
+
python -m bbstrader --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()
|
bbstrader/btengine/data.py
CHANGED
|
@@ -175,9 +175,9 @@ class BaseCSVDataHandler(DataHandler):
|
|
|
175
175
|
new_names = self.columns or default_names
|
|
176
176
|
new_names = [name.strip().lower().replace(" ", "_") for name in new_names]
|
|
177
177
|
self.columns = new_names
|
|
178
|
-
assert (
|
|
179
|
-
"
|
|
180
|
-
)
|
|
178
|
+
assert "adj_close" in new_names or "close" in new_names, (
|
|
179
|
+
"Column names must contain 'Adj Close' and 'Close' or adj_close and close"
|
|
180
|
+
)
|
|
181
181
|
comb_index = None
|
|
182
182
|
for s in self.symbol_list:
|
|
183
183
|
# Load the CSV file with no header information,
|
|
@@ -207,7 +207,9 @@ class BaseCSVDataHandler(DataHandler):
|
|
|
207
207
|
self.columns.append("adj_close")
|
|
208
208
|
self.symbol_data[s]["adj_close"] = self.symbol_data[s]["close"]
|
|
209
209
|
self.symbol_data[s]["returns"] = (
|
|
210
|
-
self.symbol_data[s][
|
|
210
|
+
self.symbol_data[s][
|
|
211
|
+
"adj_close" if "adj_close" in new_names else "close"
|
|
212
|
+
]
|
|
211
213
|
.pct_change()
|
|
212
214
|
.dropna()
|
|
213
215
|
)
|
|
@@ -362,7 +364,7 @@ class CSVDataHandler(BaseCSVDataHandler):
|
|
|
362
364
|
|
|
363
365
|
"""
|
|
364
366
|
csv_dir = kwargs.get("csv_dir")
|
|
365
|
-
csv_dir = csv_dir or BBSTRADER_DIR / "csv_data"
|
|
367
|
+
csv_dir = csv_dir or BBSTRADER_DIR / "data" / "csv_data"
|
|
366
368
|
super().__init__(
|
|
367
369
|
events,
|
|
368
370
|
symbol_list,
|
|
@@ -422,7 +424,7 @@ class MT5DataHandler(BaseCSVDataHandler):
|
|
|
422
424
|
)
|
|
423
425
|
|
|
424
426
|
def _download_and_cache_data(self, cache_dir: str):
|
|
425
|
-
data_dir = cache_dir or BBSTRADER_DIR / "mt5" / self.tf
|
|
427
|
+
data_dir = cache_dir or BBSTRADER_DIR / "data" / "mt5" / self.tf
|
|
426
428
|
data_dir.mkdir(parents=True, exist_ok=True)
|
|
427
429
|
for symbol in self.symbol_list:
|
|
428
430
|
try:
|
|
@@ -486,7 +488,7 @@ class YFDataHandler(BaseCSVDataHandler):
|
|
|
486
488
|
|
|
487
489
|
def _download_and_cache_data(self, cache_dir: str):
|
|
488
490
|
"""Downloads and caches historical data as CSV files."""
|
|
489
|
-
cache_dir = cache_dir or BBSTRADER_DIR / "yfinance" / "daily"
|
|
491
|
+
cache_dir = cache_dir or BBSTRADER_DIR / "data" / "yfinance" / "daily"
|
|
490
492
|
os.makedirs(cache_dir, exist_ok=True)
|
|
491
493
|
for symbol in self.symbol_list:
|
|
492
494
|
filepath = os.path.join(cache_dir, f"{symbol}.csv")
|
|
@@ -496,6 +498,7 @@ class YFDataHandler(BaseCSVDataHandler):
|
|
|
496
498
|
start=self.start_date,
|
|
497
499
|
end=self.end_date,
|
|
498
500
|
multi_level_index=False,
|
|
501
|
+
auto_adjust=True,
|
|
499
502
|
progress=False,
|
|
500
503
|
)
|
|
501
504
|
if "Adj Close" not in data.columns:
|
|
@@ -597,7 +600,7 @@ class EODHDataHandler(BaseCSVDataHandler):
|
|
|
597
600
|
|
|
598
601
|
def _download_and_cache_data(self, cache_dir: str):
|
|
599
602
|
"""Downloads and caches historical data as CSV files."""
|
|
600
|
-
cache_dir = cache_dir or BBSTRADER_DIR / "eodhd" / self.period
|
|
603
|
+
cache_dir = cache_dir or BBSTRADER_DIR / "data" / "eodhd" / self.period
|
|
601
604
|
os.makedirs(cache_dir, exist_ok=True)
|
|
602
605
|
for symbol in self.symbol_list:
|
|
603
606
|
filepath = os.path.join(cache_dir, f"{symbol}.csv")
|
|
@@ -698,7 +701,7 @@ class FMPDataHandler(BaseCSVDataHandler):
|
|
|
698
701
|
|
|
699
702
|
def _download_and_cache_data(self, cache_dir: str):
|
|
700
703
|
"""Downloads and caches historical data as CSV files."""
|
|
701
|
-
cache_dir = cache_dir or BBSTRADER_DIR / "fmp" / self.period
|
|
704
|
+
cache_dir = cache_dir or BBSTRADER_DIR / "data" / "fmp" / self.period
|
|
702
705
|
os.makedirs(cache_dir, exist_ok=True)
|
|
703
706
|
for symbol in self.symbol_list:
|
|
704
707
|
filepath = os.path.join(cache_dir, f"{symbol}.csv")
|
bbstrader/btengine/execution.py
CHANGED
|
@@ -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}")
|
bbstrader/btengine/strategy.py
CHANGED
|
@@ -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:
|
bbstrader/compat.py
CHANGED
|
@@ -3,8 +3,8 @@ import sys
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
def setup_mock_metatrader():
|
|
6
|
-
"""Mock MetaTrader5 on Linux to prevent import errors."""
|
|
7
|
-
if platform.system()
|
|
6
|
+
"""Mock MetaTrader5 on Linux and MacOS to prevent import errors."""
|
|
7
|
+
if platform.system() != "Windows":
|
|
8
8
|
from unittest.mock import MagicMock
|
|
9
9
|
|
|
10
10
|
class Mock(MagicMock):
|
bbstrader/config.py
CHANGED
|
@@ -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
|
|
bbstrader/core/utils.py
CHANGED
|
@@ -1,11 +1,100 @@
|
|
|
1
|
-
|
|
1
|
+
import configparser
|
|
2
|
+
import importlib
|
|
3
|
+
import importlib.util
|
|
4
|
+
import os
|
|
2
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("bbstrader.cli", 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(interpolation=None)
|
|
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
|
|
3
91
|
|
|
4
92
|
|
|
5
93
|
class TradeAction(Enum):
|
|
6
94
|
"""
|
|
7
95
|
An enumeration class for trade actions.
|
|
8
96
|
"""
|
|
97
|
+
|
|
9
98
|
BUY = "LONG"
|
|
10
99
|
LONG = "LONG"
|
|
11
100
|
SELL = "SHORT"
|
bbstrader/metatrader/__init__.py
CHANGED
|
@@ -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
|