bbstrader 0.2.93__py3-none-any.whl → 0.2.94__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/__ini__.py +20 -20
- bbstrader/__main__.py +50 -50
- bbstrader/btengine/__init__.py +54 -54
- bbstrader/btengine/scripts.py +157 -157
- bbstrader/compat.py +19 -19
- bbstrader/config.py +137 -137
- bbstrader/core/data.py +22 -22
- bbstrader/core/utils.py +146 -146
- bbstrader/metatrader/__init__.py +6 -6
- bbstrader/metatrader/account.py +1516 -1516
- bbstrader/metatrader/copier.py +750 -745
- bbstrader/metatrader/rates.py +584 -584
- bbstrader/metatrader/risk.py +749 -748
- bbstrader/metatrader/scripts.py +81 -81
- bbstrader/metatrader/trade.py +1836 -1836
- bbstrader/metatrader/utils.py +645 -645
- bbstrader/models/__init__.py +10 -10
- bbstrader/models/factors.py +312 -312
- bbstrader/models/ml.py +1272 -1272
- bbstrader/models/optimization.py +182 -182
- bbstrader/models/portfolio.py +223 -223
- bbstrader/models/risk.py +398 -398
- bbstrader/trading/__init__.py +11 -11
- bbstrader/trading/execution.py +846 -846
- bbstrader/trading/script.py +155 -155
- bbstrader/trading/scripts.py +69 -69
- bbstrader/trading/strategies.py +860 -860
- bbstrader/tseries.py +1842 -1842
- {bbstrader-0.2.93.dist-info → bbstrader-0.2.94.dist-info}/LICENSE +21 -21
- {bbstrader-0.2.93.dist-info → bbstrader-0.2.94.dist-info}/METADATA +188 -187
- bbstrader-0.2.94.dist-info/RECORD +44 -0
- bbstrader-0.2.93.dist-info/RECORD +0 -44
- {bbstrader-0.2.93.dist-info → bbstrader-0.2.94.dist-info}/WHEEL +0 -0
- {bbstrader-0.2.93.dist-info → bbstrader-0.2.94.dist-info}/entry_points.txt +0 -0
- {bbstrader-0.2.93.dist-info → bbstrader-0.2.94.dist-info}/top_level.txt +0 -0
bbstrader/config.py
CHANGED
|
@@ -1,137 +1,137 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
from typing import List
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
TERMINAL = "\\terminal64.exe"
|
|
7
|
-
BASE_FOLDER = "C:\\Program Files\\"
|
|
8
|
-
|
|
9
|
-
AMG_PATH = BASE_FOLDER + "Admirals Group MT5 Terminal" + TERMINAL
|
|
10
|
-
PGL_PATH = BASE_FOLDER + "Pepperstone MetaTrader 5" + TERMINAL
|
|
11
|
-
FTMO_PATH = BASE_FOLDER + "FTMO MetaTrader 5" + TERMINAL
|
|
12
|
-
JGM_PATH = BASE_FOLDER + "JustMarkets MetaTrader 5" + TERMINAL
|
|
13
|
-
|
|
14
|
-
BROKERS_PATHS = {
|
|
15
|
-
"AMG": AMG_PATH,
|
|
16
|
-
"FTMO": FTMO_PATH,
|
|
17
|
-
"PGL": PGL_PATH,
|
|
18
|
-
"JGM": JGM_PATH,
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def get_config_dir(name: str = ".bbstrader") -> Path:
|
|
23
|
-
"""
|
|
24
|
-
Get the path to the configuration directory.
|
|
25
|
-
|
|
26
|
-
Args:
|
|
27
|
-
name: The name of the configuration directory.
|
|
28
|
-
|
|
29
|
-
Returns:
|
|
30
|
-
The path to the configuration directory.
|
|
31
|
-
"""
|
|
32
|
-
home_dir = Path.home() / name
|
|
33
|
-
if not home_dir.exists():
|
|
34
|
-
home_dir.mkdir()
|
|
35
|
-
return home_dir
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
BBSTRADER_DIR = get_config_dir()
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class LogLevelFilter(logging.Filter):
|
|
42
|
-
def __init__(self, levels: List[int]):
|
|
43
|
-
"""
|
|
44
|
-
Initializes the filter with specific logging levels.
|
|
45
|
-
|
|
46
|
-
Args:
|
|
47
|
-
levels: A list of logging level values (integers) to include.
|
|
48
|
-
"""
|
|
49
|
-
super().__init__()
|
|
50
|
-
self.levels = levels
|
|
51
|
-
|
|
52
|
-
def filter(self, record: logging.LogRecord) -> bool:
|
|
53
|
-
"""
|
|
54
|
-
Filters log records based on their level.
|
|
55
|
-
|
|
56
|
-
Args:
|
|
57
|
-
record: The log record to check.
|
|
58
|
-
|
|
59
|
-
Returns:
|
|
60
|
-
True if the record's level is in the allowed levels, False otherwise.
|
|
61
|
-
"""
|
|
62
|
-
return record.levelno in self.levels
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
class CustomFormatter(logging.Formatter):
|
|
66
|
-
def formatTime(self, record, datefmt=None):
|
|
67
|
-
if hasattr(record, "custom_time"):
|
|
68
|
-
# Use the custom time if provided
|
|
69
|
-
record.created = record.custom_time.timestamp()
|
|
70
|
-
return super().formatTime(record, datefmt)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
class CustomLogger(logging.Logger):
|
|
74
|
-
def __init__(self, name, level=logging.NOTSET):
|
|
75
|
-
super().__init__(name, level)
|
|
76
|
-
|
|
77
|
-
def _log(
|
|
78
|
-
self,
|
|
79
|
-
level,
|
|
80
|
-
msg,
|
|
81
|
-
args,
|
|
82
|
-
exc_info=None,
|
|
83
|
-
extra=None,
|
|
84
|
-
stack_info=False,
|
|
85
|
-
stacklevel=1,
|
|
86
|
-
custom_time=None,
|
|
87
|
-
):
|
|
88
|
-
if extra is None:
|
|
89
|
-
extra = {}
|
|
90
|
-
# Add custom_time to the extra dictionary if provided
|
|
91
|
-
if custom_time:
|
|
92
|
-
extra["custom_time"] = custom_time
|
|
93
|
-
super()._log(level, msg, args, exc_info, extra, stack_info, stacklevel)
|
|
94
|
-
|
|
95
|
-
def info(self, msg, *args, custom_time=None, **kwargs):
|
|
96
|
-
self._log(logging.INFO, msg, args, custom_time=custom_time, **kwargs)
|
|
97
|
-
|
|
98
|
-
def debug(self, msg, *args, custom_time=None, **kwargs):
|
|
99
|
-
self._log(logging.DEBUG, msg, args, custom_time=custom_time, **kwargs)
|
|
100
|
-
|
|
101
|
-
def warning(self, msg, *args, custom_time=None, **kwargs):
|
|
102
|
-
self._log(logging.WARNING, msg, args, custom_time=custom_time, **kwargs)
|
|
103
|
-
|
|
104
|
-
def error(self, msg, *args, custom_time=None, **kwargs):
|
|
105
|
-
self._log(logging.ERROR, msg, args, custom_time=custom_time, **kwargs)
|
|
106
|
-
|
|
107
|
-
def critical(self, msg, *args, custom_time=None, **kwargs):
|
|
108
|
-
self._log(logging.CRITICAL, msg, args, custom_time=custom_time, **kwargs)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def config_logger(log_file: str, console_log=True):
|
|
112
|
-
# Use the CustomLogger
|
|
113
|
-
logging.setLoggerClass(CustomLogger)
|
|
114
|
-
logger = logging.getLogger(__name__)
|
|
115
|
-
logger.setLevel(logging.DEBUG)
|
|
116
|
-
|
|
117
|
-
# File handler
|
|
118
|
-
file_handler = logging.FileHandler(log_file)
|
|
119
|
-
file_handler.setLevel(logging.INFO)
|
|
120
|
-
|
|
121
|
-
# Custom formatter
|
|
122
|
-
formatter = CustomFormatter(
|
|
123
|
-
"%(asctime)s - %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
|
|
124
|
-
)
|
|
125
|
-
file_handler.setFormatter(formatter)
|
|
126
|
-
|
|
127
|
-
# Add the handler to the logger
|
|
128
|
-
logger.addHandler(file_handler)
|
|
129
|
-
|
|
130
|
-
if console_log:
|
|
131
|
-
# Handler for the console with a different level
|
|
132
|
-
console_handler = logging.StreamHandler()
|
|
133
|
-
console_handler.setLevel(logging.DEBUG)
|
|
134
|
-
console_handler.setFormatter(formatter)
|
|
135
|
-
logger.addHandler(console_handler)
|
|
136
|
-
|
|
137
|
-
return logger
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
TERMINAL = "\\terminal64.exe"
|
|
7
|
+
BASE_FOLDER = "C:\\Program Files\\"
|
|
8
|
+
|
|
9
|
+
AMG_PATH = BASE_FOLDER + "Admirals Group MT5 Terminal" + TERMINAL
|
|
10
|
+
PGL_PATH = BASE_FOLDER + "Pepperstone MetaTrader 5" + TERMINAL
|
|
11
|
+
FTMO_PATH = BASE_FOLDER + "FTMO MetaTrader 5" + TERMINAL
|
|
12
|
+
JGM_PATH = BASE_FOLDER + "JustMarkets MetaTrader 5" + TERMINAL
|
|
13
|
+
|
|
14
|
+
BROKERS_PATHS = {
|
|
15
|
+
"AMG": AMG_PATH,
|
|
16
|
+
"FTMO": FTMO_PATH,
|
|
17
|
+
"PGL": PGL_PATH,
|
|
18
|
+
"JGM": JGM_PATH,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_config_dir(name: str = ".bbstrader") -> Path:
|
|
23
|
+
"""
|
|
24
|
+
Get the path to the configuration directory.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
name: The name of the configuration directory.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
The path to the configuration directory.
|
|
31
|
+
"""
|
|
32
|
+
home_dir = Path.home() / name
|
|
33
|
+
if not home_dir.exists():
|
|
34
|
+
home_dir.mkdir()
|
|
35
|
+
return home_dir
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
BBSTRADER_DIR = get_config_dir()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class LogLevelFilter(logging.Filter):
|
|
42
|
+
def __init__(self, levels: List[int]):
|
|
43
|
+
"""
|
|
44
|
+
Initializes the filter with specific logging levels.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
levels: A list of logging level values (integers) to include.
|
|
48
|
+
"""
|
|
49
|
+
super().__init__()
|
|
50
|
+
self.levels = levels
|
|
51
|
+
|
|
52
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
53
|
+
"""
|
|
54
|
+
Filters log records based on their level.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
record: The log record to check.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
True if the record's level is in the allowed levels, False otherwise.
|
|
61
|
+
"""
|
|
62
|
+
return record.levelno in self.levels
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class CustomFormatter(logging.Formatter):
|
|
66
|
+
def formatTime(self, record, datefmt=None):
|
|
67
|
+
if hasattr(record, "custom_time"):
|
|
68
|
+
# Use the custom time if provided
|
|
69
|
+
record.created = record.custom_time.timestamp()
|
|
70
|
+
return super().formatTime(record, datefmt)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class CustomLogger(logging.Logger):
|
|
74
|
+
def __init__(self, name, level=logging.NOTSET):
|
|
75
|
+
super().__init__(name, level)
|
|
76
|
+
|
|
77
|
+
def _log(
|
|
78
|
+
self,
|
|
79
|
+
level,
|
|
80
|
+
msg,
|
|
81
|
+
args,
|
|
82
|
+
exc_info=None,
|
|
83
|
+
extra=None,
|
|
84
|
+
stack_info=False,
|
|
85
|
+
stacklevel=1,
|
|
86
|
+
custom_time=None,
|
|
87
|
+
):
|
|
88
|
+
if extra is None:
|
|
89
|
+
extra = {}
|
|
90
|
+
# Add custom_time to the extra dictionary if provided
|
|
91
|
+
if custom_time:
|
|
92
|
+
extra["custom_time"] = custom_time
|
|
93
|
+
super()._log(level, msg, args, exc_info, extra, stack_info, stacklevel)
|
|
94
|
+
|
|
95
|
+
def info(self, msg, *args, custom_time=None, **kwargs):
|
|
96
|
+
self._log(logging.INFO, msg, args, custom_time=custom_time, **kwargs)
|
|
97
|
+
|
|
98
|
+
def debug(self, msg, *args, custom_time=None, **kwargs):
|
|
99
|
+
self._log(logging.DEBUG, msg, args, custom_time=custom_time, **kwargs)
|
|
100
|
+
|
|
101
|
+
def warning(self, msg, *args, custom_time=None, **kwargs):
|
|
102
|
+
self._log(logging.WARNING, msg, args, custom_time=custom_time, **kwargs)
|
|
103
|
+
|
|
104
|
+
def error(self, msg, *args, custom_time=None, **kwargs):
|
|
105
|
+
self._log(logging.ERROR, msg, args, custom_time=custom_time, **kwargs)
|
|
106
|
+
|
|
107
|
+
def critical(self, msg, *args, custom_time=None, **kwargs):
|
|
108
|
+
self._log(logging.CRITICAL, msg, args, custom_time=custom_time, **kwargs)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def config_logger(log_file: str, console_log=True):
|
|
112
|
+
# Use the CustomLogger
|
|
113
|
+
logging.setLoggerClass(CustomLogger)
|
|
114
|
+
logger = logging.getLogger(__name__)
|
|
115
|
+
logger.setLevel(logging.DEBUG)
|
|
116
|
+
|
|
117
|
+
# File handler
|
|
118
|
+
file_handler = logging.FileHandler(log_file)
|
|
119
|
+
file_handler.setLevel(logging.INFO)
|
|
120
|
+
|
|
121
|
+
# Custom formatter
|
|
122
|
+
formatter = CustomFormatter(
|
|
123
|
+
"%(asctime)s - %(levelname)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
|
|
124
|
+
)
|
|
125
|
+
file_handler.setFormatter(formatter)
|
|
126
|
+
|
|
127
|
+
# Add the handler to the logger
|
|
128
|
+
logger.addHandler(file_handler)
|
|
129
|
+
|
|
130
|
+
if console_log:
|
|
131
|
+
# Handler for the console with a different level
|
|
132
|
+
console_handler = logging.StreamHandler()
|
|
133
|
+
console_handler.setLevel(logging.DEBUG)
|
|
134
|
+
console_handler.setFormatter(formatter)
|
|
135
|
+
logger.addHandler(console_handler)
|
|
136
|
+
|
|
137
|
+
return logger
|
bbstrader/core/data.py
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
from financetoolkit import Toolkit
|
|
2
|
-
|
|
3
|
-
__all__ = [
|
|
4
|
-
'FMP',
|
|
5
|
-
]
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class FMP(Toolkit):
|
|
9
|
-
"""
|
|
10
|
-
FMPData class for fetching data from Financial Modeling Prep API
|
|
11
|
-
using the Toolkit class from financetoolkit package.
|
|
12
|
-
|
|
13
|
-
See `financetoolkit` for more details.
|
|
14
|
-
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
|
-
def __init__(self, api_key: str = '', symbols: str | list = 'AAPL'):
|
|
18
|
-
super().__init__(tickers=symbols, api_key=api_key)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class DataBendo:
|
|
22
|
-
...
|
|
1
|
+
from financetoolkit import Toolkit
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
'FMP',
|
|
5
|
+
]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FMP(Toolkit):
|
|
9
|
+
"""
|
|
10
|
+
FMPData class for fetching data from Financial Modeling Prep API
|
|
11
|
+
using the Toolkit class from financetoolkit package.
|
|
12
|
+
|
|
13
|
+
See `financetoolkit` for more details.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, api_key: str = '', symbols: str | list = 'AAPL'):
|
|
18
|
+
super().__init__(tickers=symbols, api_key=api_key)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DataBendo:
|
|
22
|
+
...
|
bbstrader/core/utils.py
CHANGED
|
@@ -1,146 +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("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
|
|
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
|
-
)
|
|
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("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
|
|
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
|
+
)
|
bbstrader/metatrader/__init__.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
from bbstrader.metatrader.account import * # noqa: F403
|
|
3
|
-
from bbstrader.metatrader.rates import * # noqa: F403
|
|
4
|
-
from bbstrader.metatrader.risk import * # noqa: F403
|
|
5
|
-
from bbstrader.metatrader.trade import * # noqa: F403
|
|
6
|
-
from bbstrader.metatrader.utils import * # noqa: F403*
|
|
1
|
+
|
|
2
|
+
from bbstrader.metatrader.account import * # noqa: F403
|
|
3
|
+
from bbstrader.metatrader.rates import * # noqa: F403
|
|
4
|
+
from bbstrader.metatrader.risk import * # noqa: F403
|
|
5
|
+
from bbstrader.metatrader.trade import * # noqa: F403
|
|
6
|
+
from bbstrader.metatrader.utils import * # noqa: F403*
|
|
7
7
|
from bbstrader.metatrader.copier import * # noqa: F403
|