bbstrader 0.1.9__tar.gz → 0.1.91__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.
Potentially problematic release.
This version of bbstrader might be problematic. Click here for more details.
- bbstrader-0.1.91/MANIFEST.in +1 -0
- {bbstrader-0.1.9 → bbstrader-0.1.91}/PKG-INFO +11 -3
- {bbstrader-0.1.9 → bbstrader-0.1.91}/bbstrader/__ini__.py +4 -2
- {bbstrader-0.1.9 → bbstrader-0.1.91}/bbstrader/btengine/__init__.py +5 -5
- {bbstrader-0.1.9 → bbstrader-0.1.91}/bbstrader/btengine/backtest.py +51 -10
- {bbstrader-0.1.9 → bbstrader-0.1.91}/bbstrader/btengine/data.py +147 -55
- {bbstrader-0.1.9 → bbstrader-0.1.91}/bbstrader/btengine/event.py +4 -1
- bbstrader-0.1.91/bbstrader/btengine/execution.py +245 -0
- {bbstrader-0.1.9 → bbstrader-0.1.91}/bbstrader/btengine/performance.py +4 -7
- {bbstrader-0.1.9 → bbstrader-0.1.91}/bbstrader/btengine/portfolio.py +34 -13
- bbstrader-0.1.91/bbstrader/btengine/strategy.py +492 -0
- bbstrader-0.1.91/bbstrader/config.py +111 -0
- bbstrader-0.1.91/bbstrader/metatrader/__init__.py +6 -0
- {bbstrader-0.1.9 → bbstrader-0.1.91}/bbstrader/metatrader/account.py +348 -53
- bbstrader-0.1.91/bbstrader/metatrader/rates.py +462 -0
- {bbstrader-0.1.9 → bbstrader-0.1.91}/bbstrader/metatrader/risk.py +34 -23
- {bbstrader-0.1.9 → bbstrader-0.1.91}/bbstrader/metatrader/trade.py +321 -165
- {bbstrader-0.1.9 → bbstrader-0.1.91}/bbstrader/metatrader/utils.py +2 -53
- bbstrader-0.1.91/bbstrader/models/factors.py +0 -0
- bbstrader-0.1.91/bbstrader/models/ml.py +0 -0
- bbstrader-0.1.91/bbstrader/models/optimization.py +0 -0
- {bbstrader-0.1.9 → bbstrader-0.1.91}/bbstrader/trading/__init__.py +1 -1
- bbstrader-0.1.91/bbstrader/trading/execution.py +537 -0
- bbstrader-0.1.91/bbstrader/trading/scripts.py +57 -0
- {bbstrader-0.1.9 → bbstrader-0.1.91}/bbstrader/trading/strategies.py +41 -65
- {bbstrader-0.1.9 → bbstrader-0.1.91}/bbstrader/tseries.py +274 -39
- {bbstrader-0.1.9 → bbstrader-0.1.91}/bbstrader.egg-info/PKG-INFO +11 -3
- {bbstrader-0.1.9 → bbstrader-0.1.91}/bbstrader.egg-info/SOURCES.txt +7 -0
- {bbstrader-0.1.9 → bbstrader-0.1.91}/bbstrader.egg-info/requires.txt +11 -2
- bbstrader-0.1.91/requirements.txt +24 -0
- {bbstrader-0.1.9 → bbstrader-0.1.91}/setup.py +28 -18
- bbstrader-0.1.9/bbstrader/btengine/execution.py +0 -143
- bbstrader-0.1.9/bbstrader/btengine/strategy.py +0 -32
- bbstrader-0.1.9/bbstrader/metatrader/__init__.py +0 -6
- bbstrader-0.1.9/bbstrader/metatrader/rates.py +0 -257
- bbstrader-0.1.9/bbstrader/trading/execution.py +0 -433
- {bbstrader-0.1.9 → bbstrader-0.1.91}/LICENSE +0 -0
- {bbstrader-0.1.9 → bbstrader-0.1.91}/README.md +0 -0
- {bbstrader-0.1.9 → bbstrader-0.1.91}/bbstrader/models/__init__.py +0 -0
- {bbstrader-0.1.9 → bbstrader-0.1.91}/bbstrader/models/risk.py +0 -0
- {bbstrader-0.1.9 → bbstrader-0.1.91}/bbstrader.egg-info/dependency_links.txt +0 -0
- {bbstrader-0.1.9 → bbstrader-0.1.91}/bbstrader.egg-info/top_level.txt +0 -0
- {bbstrader-0.1.9 → bbstrader-0.1.91}/setup.cfg +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
include requirements.txt
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: bbstrader
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.91
|
|
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/
|
|
@@ -34,12 +34,20 @@ Requires-Dist: seaborn
|
|
|
34
34
|
Requires-Dist: statsmodels
|
|
35
35
|
Requires-Dist: matplotlib
|
|
36
36
|
Requires-Dist: filterpy
|
|
37
|
+
Requires-Dist: pykalman
|
|
37
38
|
Requires-Dist: pytest
|
|
38
39
|
Requires-Dist: CurrencyConverter
|
|
39
40
|
Requires-Dist: tabulate
|
|
41
|
+
Requires-Dist: python-dotenv
|
|
40
42
|
Requires-Dist: ipython
|
|
41
|
-
Requires-Dist:
|
|
42
|
-
Requires-Dist:
|
|
43
|
+
Requires-Dist: QuantStats
|
|
44
|
+
Requires-Dist: exchange-calendars
|
|
45
|
+
Requires-Dist: tqdm
|
|
46
|
+
Requires-Dist: scikit-learn
|
|
47
|
+
Requires-Dist: notify-py
|
|
48
|
+
Requires-Dist: python-telegram-bot
|
|
49
|
+
Provides-Extra: mt5
|
|
50
|
+
Requires-Dist: MetaTrader5; extra == "mt5"
|
|
43
51
|
|
|
44
52
|
# Simplified Investment & Trading Toolkit
|
|
45
53
|

|
|
@@ -7,10 +7,12 @@ __author__ = "Bertin Balouki SIMYELI"
|
|
|
7
7
|
__copyright__ = "2023-2024 Bertin Balouki SIMYELI"
|
|
8
8
|
__email__ = "bbalouki@outlook.com"
|
|
9
9
|
__license__ = "MIT"
|
|
10
|
-
__version__ =
|
|
10
|
+
__version__ = '0.1.91'
|
|
11
|
+
|
|
11
12
|
|
|
12
13
|
from bbstrader import btengine
|
|
13
14
|
from bbstrader import metatrader
|
|
14
15
|
from bbstrader import models
|
|
15
16
|
from bbstrader import trading
|
|
16
|
-
from bbstrader import tseries
|
|
17
|
+
from bbstrader import tseries
|
|
18
|
+
from .config import config_logger
|
|
@@ -18,11 +18,11 @@ Features
|
|
|
18
18
|
Components
|
|
19
19
|
==========
|
|
20
20
|
|
|
21
|
-
- **
|
|
21
|
+
- **BacktestEgine**: Orchestrates the backtesting process, managing events and invoking components.
|
|
22
22
|
- **Event**: Abstract class for events, with implementations for market data, signals, fill and order events.
|
|
23
|
-
- **DataHandler**: Abstract class for market data handling, with an implementation for `
|
|
23
|
+
- **DataHandler**: Abstract class for market data handling, with an implementation for `CSVDataHandler`, `MT5DataHandler`, `YFDataHandler`. We will add another data handling in the future such as MacroEconomic Data, Fundamental Data, TICK Data and Real-time Data.
|
|
24
24
|
- **Portfolio**: Manages positions and calculates performance metrics, responding to market data and signals.
|
|
25
|
-
- **ExecutionHandler**: Abstract class for order execution, with a simulated execution handler provided with an implementation for `
|
|
25
|
+
- **ExecutionHandler**: Abstract class for order execution, with a simulated execution handler provided with an implementation for `SimExecutionHandler`.
|
|
26
26
|
- **Performance**: Utility functions for calculating performance metrics and visualizing strategy performance.
|
|
27
27
|
|
|
28
28
|
Examples
|
|
@@ -50,5 +50,5 @@ from bbstrader.btengine.event import *
|
|
|
50
50
|
from bbstrader.btengine.execution import *
|
|
51
51
|
from bbstrader.btengine.performance import *
|
|
52
52
|
from bbstrader.btengine.backtest import *
|
|
53
|
-
from bbstrader.btengine.strategy import
|
|
54
|
-
from bbstrader.btengine.portfolio import
|
|
53
|
+
from bbstrader.btengine.strategy import *
|
|
54
|
+
from bbstrader.btengine.portfolio import *
|
|
@@ -55,7 +55,11 @@ class Backtest(object):
|
|
|
55
55
|
the new positions.
|
|
56
56
|
|
|
57
57
|
"""
|
|
58
|
+
pass
|
|
58
59
|
|
|
60
|
+
|
|
61
|
+
class BacktestEngine(Backtest):
|
|
62
|
+
__doc__ = Backtest.__doc__
|
|
59
63
|
def __init__(
|
|
60
64
|
self,
|
|
61
65
|
symbol_list: List[str],
|
|
@@ -123,7 +127,7 @@ class Backtest(object):
|
|
|
123
127
|
self.initial_capital, **self.kwargs
|
|
124
128
|
)
|
|
125
129
|
self.execution_handler: ExecutionHandler = self.eh_cls(
|
|
126
|
-
self.events, **self.kwargs)
|
|
130
|
+
self.events, self.data_handler, **self.kwargs)
|
|
127
131
|
|
|
128
132
|
def _run_backtest(self):
|
|
129
133
|
"""
|
|
@@ -132,11 +136,13 @@ class Backtest(object):
|
|
|
132
136
|
i = 0
|
|
133
137
|
while True:
|
|
134
138
|
i += 1
|
|
135
|
-
print(i)
|
|
136
139
|
# Update the market bars
|
|
137
140
|
if self.data_handler.continue_backtest == True:
|
|
138
141
|
self.data_handler.update_bars()
|
|
142
|
+
self.strategy.check_pending_orders()
|
|
139
143
|
else:
|
|
144
|
+
print("\n[======= BACKTEST COMPLETE =======]\n")
|
|
145
|
+
print(f"Total bars: {i} ")
|
|
140
146
|
break
|
|
141
147
|
|
|
142
148
|
# Handle the events
|
|
@@ -189,7 +195,7 @@ class Backtest(object):
|
|
|
189
195
|
|
|
190
196
|
if self.show_equity:
|
|
191
197
|
print("\nCreating equity curve...")
|
|
192
|
-
print("\n[=======
|
|
198
|
+
print("\n[======= PORTFOLIO SUMMARY =======]")
|
|
193
199
|
print(
|
|
194
200
|
tabulate(
|
|
195
201
|
self.portfolio.equity_curve.tail(10),
|
|
@@ -201,11 +207,14 @@ class Backtest(object):
|
|
|
201
207
|
def simulate_trading(self):
|
|
202
208
|
"""
|
|
203
209
|
Simulates the backtest and outputs portfolio performance.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
pd.DataFrame: The portfilio values over time (capital, equity, returns etc.)
|
|
204
213
|
"""
|
|
205
214
|
self._run_backtest()
|
|
206
215
|
self._output_performance()
|
|
216
|
+
return self.portfolio.equity_curve
|
|
207
217
|
|
|
208
|
-
BacktestEngine = Backtest
|
|
209
218
|
|
|
210
219
|
def run_backtest(
|
|
211
220
|
symbol_list: List[str],
|
|
@@ -226,8 +235,8 @@ def run_backtest(
|
|
|
226
235
|
start_date (datetime): Start date of the backtest.
|
|
227
236
|
|
|
228
237
|
data_handler (DataHandler): An instance of the `DataHandler` class, responsible for managing
|
|
229
|
-
and processing market data. Available options include `
|
|
230
|
-
`
|
|
238
|
+
and processing market data. Available options include `CSVDataHandler`,
|
|
239
|
+
`MT5DataHandler`, and `YFDataHandler`. Ensure that the `DataHandler`
|
|
231
240
|
instance is initialized before passing it to the function.
|
|
232
241
|
|
|
233
242
|
strategy (Strategy): The trading strategy to be employed during the backtest.
|
|
@@ -253,6 +262,9 @@ def run_backtest(
|
|
|
253
262
|
|
|
254
263
|
**kwargs: Additional parameters passed to the `Backtest` instance, which may include strategy-specific,
|
|
255
264
|
data handler, portfolio, or execution handler options.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
pd.DataFrame: The portfolio values over time (capital, equities, returns etc.).
|
|
256
268
|
|
|
257
269
|
Notes:
|
|
258
270
|
This function generates three outputs:
|
|
@@ -263,7 +275,7 @@ def run_backtest(
|
|
|
263
275
|
Example:
|
|
264
276
|
>>> from bbstrader.trading.strategies import StockIndexSTBOTrading
|
|
265
277
|
>>> from bbstrader.metatrader.utils import config_logger
|
|
266
|
-
>>> from bbstrader.datahandlers import
|
|
278
|
+
>>> from bbstrader.datahandlers import MT5DataHandler
|
|
267
279
|
>>> from bbstrader.execution import MT5ExecutionHandler
|
|
268
280
|
>>> from datetime import datetime
|
|
269
281
|
>>>
|
|
@@ -281,7 +293,7 @@ def run_backtest(
|
|
|
281
293
|
>>> run_backtest(
|
|
282
294
|
... symbol_list=symbol_list,
|
|
283
295
|
... start_date=start,
|
|
284
|
-
... data_handler=
|
|
296
|
+
... data_handler=MT5DataHandler(),
|
|
285
297
|
... strategy=StockIndexSTBOTrading(),
|
|
286
298
|
... exc_handler=MT5ExecutionHandler(),
|
|
287
299
|
... initial_capital=100000.0,
|
|
@@ -290,11 +302,40 @@ def run_backtest(
|
|
|
290
302
|
... )
|
|
291
303
|
"""
|
|
292
304
|
if exc_handler is None:
|
|
293
|
-
execution_handler =
|
|
305
|
+
execution_handler = SimExecutionHandler
|
|
294
306
|
else:
|
|
295
307
|
execution_handler = exc_handler
|
|
296
308
|
engine = BacktestEngine(
|
|
297
309
|
symbol_list, initial_capital, heartbeat, start_date,
|
|
298
310
|
data_handler, execution_handler, strategy, **kwargs
|
|
299
311
|
)
|
|
300
|
-
engine.simulate_trading()
|
|
312
|
+
portfolio = engine.simulate_trading()
|
|
313
|
+
return portfolio
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class CerebroEngine:...
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class ZiplineEngine:...
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def run_backtest_with(engine: Literal["bbstrader", "cerebro", "zipline"], **kwargs):
|
|
323
|
+
"""
|
|
324
|
+
"""
|
|
325
|
+
if engine == "bbstrader":
|
|
326
|
+
return run_backtest(
|
|
327
|
+
symbol_list=kwargs.get("symbol_list"),
|
|
328
|
+
start_date=kwargs.get("start_date"),
|
|
329
|
+
data_handler=kwargs.get("data_handler"),
|
|
330
|
+
strategy=kwargs.get("strategy"),
|
|
331
|
+
exc_handler=kwargs.get("exc_handler"),
|
|
332
|
+
initial_capital=kwargs.get("initial_capital"),
|
|
333
|
+
heartbeat=kwargs.get("heartbeat", 0.0),
|
|
334
|
+
**kwargs
|
|
335
|
+
)
|
|
336
|
+
elif engine == "cerebro":
|
|
337
|
+
#TODO:
|
|
338
|
+
pass
|
|
339
|
+
elif engine == "zipline":
|
|
340
|
+
#TODO:
|
|
341
|
+
pass
|
|
@@ -3,20 +3,20 @@ import numpy as np
|
|
|
3
3
|
import pandas as pd
|
|
4
4
|
import yfinance as yf
|
|
5
5
|
from pathlib import Path
|
|
6
|
-
from typing import List
|
|
6
|
+
from typing import List, Dict
|
|
7
7
|
from queue import Queue
|
|
8
8
|
from abc import ABCMeta, abstractmethod
|
|
9
|
-
from bbstrader.metatrader.rates import
|
|
9
|
+
from bbstrader.metatrader.rates import download_historical_data
|
|
10
10
|
from bbstrader.btengine.event import MarketEvent
|
|
11
|
+
from bbstrader.config import BBSTRADER_DIR
|
|
11
12
|
from datetime import datetime
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
__all__ = [
|
|
15
16
|
"DataHandler",
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"YFHistoricDataHandler"
|
|
17
|
+
"CSVDataHandler",
|
|
18
|
+
"MT5DataHandler",
|
|
19
|
+
"YFDataHandler"
|
|
20
20
|
]
|
|
21
21
|
|
|
22
22
|
|
|
@@ -39,9 +39,21 @@ class DataHandler(metaclass=ABCMeta):
|
|
|
39
39
|
Specific example subclasses could include `HistoricCSVDataHandler`,
|
|
40
40
|
`YFinanceDataHandler`, `FMPDataHandler`, `IBMarketFeedDataHandler` etc.
|
|
41
41
|
"""
|
|
42
|
+
@property
|
|
43
|
+
def symbols(self) -> List[str]:
|
|
44
|
+
pass
|
|
45
|
+
@property
|
|
46
|
+
def data(self) -> Dict[str, pd.DataFrame]:
|
|
47
|
+
pass
|
|
48
|
+
@property
|
|
49
|
+
def labels(self) -> List[str]:
|
|
50
|
+
pass
|
|
51
|
+
@property
|
|
52
|
+
def index(self) -> str | List[str]:
|
|
53
|
+
pass
|
|
42
54
|
|
|
43
55
|
@abstractmethod
|
|
44
|
-
def get_latest_bar(self, symbol):
|
|
56
|
+
def get_latest_bar(self, symbol) -> pd.Series:
|
|
45
57
|
"""
|
|
46
58
|
Returns the last bar updated.
|
|
47
59
|
"""
|
|
@@ -50,7 +62,7 @@ class DataHandler(metaclass=ABCMeta):
|
|
|
50
62
|
)
|
|
51
63
|
|
|
52
64
|
@abstractmethod
|
|
53
|
-
def get_latest_bars(self, symbol, N=1):
|
|
65
|
+
def get_latest_bars(self, symbol, N=1, df=True) -> pd.DataFrame | List[pd.Series]:
|
|
54
66
|
"""
|
|
55
67
|
Returns the last N bars updated.
|
|
56
68
|
"""
|
|
@@ -59,7 +71,7 @@ class DataHandler(metaclass=ABCMeta):
|
|
|
59
71
|
)
|
|
60
72
|
|
|
61
73
|
@abstractmethod
|
|
62
|
-
def get_latest_bar_datetime(self, symbol):
|
|
74
|
+
def get_latest_bar_datetime(self, symbol) -> datetime | pd.Timestamp:
|
|
63
75
|
"""
|
|
64
76
|
Returns a Python datetime object for the last bar.
|
|
65
77
|
"""
|
|
@@ -68,7 +80,7 @@ class DataHandler(metaclass=ABCMeta):
|
|
|
68
80
|
)
|
|
69
81
|
|
|
70
82
|
@abstractmethod
|
|
71
|
-
def get_latest_bar_value(self, symbol, val_type):
|
|
83
|
+
def get_latest_bar_value(self, symbol, val_type) -> float:
|
|
72
84
|
"""
|
|
73
85
|
Returns one of the Open, High, Low, Close, Adj Close, Volume or Returns
|
|
74
86
|
from the last bar.
|
|
@@ -78,7 +90,7 @@ class DataHandler(metaclass=ABCMeta):
|
|
|
78
90
|
)
|
|
79
91
|
|
|
80
92
|
@abstractmethod
|
|
81
|
-
def get_latest_bars_values(self, symbol, val_type, N=1):
|
|
93
|
+
def get_latest_bars_values(self, symbol, val_type, N=1) -> np.ndarray:
|
|
82
94
|
"""
|
|
83
95
|
Returns the last N bar values from the
|
|
84
96
|
latest_symbol list, or N-k if less available.
|
|
@@ -102,40 +114,78 @@ class DataHandler(metaclass=ABCMeta):
|
|
|
102
114
|
class BaseCSVDataHandler(DataHandler):
|
|
103
115
|
"""
|
|
104
116
|
Base class for handling data loaded from CSV files.
|
|
117
|
+
|
|
105
118
|
"""
|
|
106
119
|
|
|
107
|
-
def __init__(self, events: Queue,
|
|
120
|
+
def __init__(self, events: Queue,
|
|
121
|
+
symbol_list: List[str],
|
|
122
|
+
csv_dir: str,
|
|
123
|
+
columns: List[str]=None,
|
|
124
|
+
index_col: str | int | List[str] | List[int] = 0):
|
|
125
|
+
|
|
126
|
+
"""
|
|
127
|
+
Initialises the data handler by requesting the location of the CSV files
|
|
128
|
+
and a list of symbols.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
events : The Event Queue.
|
|
132
|
+
symbol_list : A list of symbol strings.
|
|
133
|
+
csv_dir : Absolute directory path to the CSV files.
|
|
134
|
+
columns : List of column names to use for the data.
|
|
135
|
+
index_col : Column to use as the index.
|
|
136
|
+
"""
|
|
108
137
|
self.events = events
|
|
109
138
|
self.symbol_list = symbol_list
|
|
110
139
|
self.csv_dir = csv_dir
|
|
140
|
+
self.columns = columns
|
|
141
|
+
self.index_col = index_col
|
|
111
142
|
self.symbol_data = {}
|
|
112
143
|
self.latest_symbol_data = {}
|
|
113
144
|
self.continue_backtest = True
|
|
145
|
+
self._index = None
|
|
114
146
|
self._load_and_process_data()
|
|
115
147
|
|
|
148
|
+
@property
|
|
149
|
+
def symbols(self)-> List[str]:
|
|
150
|
+
return self.symbol_list
|
|
151
|
+
@property
|
|
152
|
+
def data(self)-> Dict[str, pd.DataFrame]:
|
|
153
|
+
return self.symbol_data
|
|
154
|
+
@property
|
|
155
|
+
def labels(self)-> List[str]:
|
|
156
|
+
return self.columns
|
|
157
|
+
@property
|
|
158
|
+
def index(self)-> str | List[str]:
|
|
159
|
+
return self._index
|
|
160
|
+
|
|
116
161
|
def _load_and_process_data(self):
|
|
117
162
|
"""
|
|
118
163
|
Opens the CSV files from the data directory, converting
|
|
119
164
|
them into pandas DataFrames within a symbol dictionary.
|
|
120
165
|
"""
|
|
166
|
+
default_names = pd.read_csv(
|
|
167
|
+
os.path.join(self.csv_dir, f'{self.symbol_list[0]}.csv')
|
|
168
|
+
).columns.to_list()
|
|
169
|
+
new_names = self.columns or default_names
|
|
170
|
+
new_names = [name.lower().replace(' ', '_') for name in new_names]
|
|
171
|
+
self.columns = new_names
|
|
172
|
+
assert 'adj_close' in new_names or 'close' in new_names, \
|
|
173
|
+
"Column names must contain 'Adj Close' and 'Close' or adj_close and close"
|
|
121
174
|
comb_index = None
|
|
122
175
|
for s in self.symbol_list:
|
|
123
176
|
# Load the CSV file with no header information,
|
|
124
177
|
# indexed on date
|
|
125
178
|
self.symbol_data[s] = pd.read_csv(
|
|
126
179
|
os.path.join(self.csv_dir, f'{s}.csv'),
|
|
127
|
-
header=0, index_col=
|
|
128
|
-
names=
|
|
129
|
-
'Datetime', 'Open', 'High',
|
|
130
|
-
'Low', 'Close', 'Adj Close', 'Volume'
|
|
131
|
-
]
|
|
180
|
+
header=0, index_col=self.index_col, parse_dates=True,
|
|
181
|
+
names=new_names
|
|
132
182
|
)
|
|
133
183
|
self.symbol_data[s].sort_index(inplace=True)
|
|
134
184
|
# Combine the index to pad forward values
|
|
135
185
|
if comb_index is None:
|
|
136
186
|
comb_index = self.symbol_data[s].index
|
|
137
|
-
|
|
138
|
-
comb_index
|
|
187
|
+
elif len(self.symbol_data[s].index) > len(comb_index):
|
|
188
|
+
comb_index = self.symbol_data[s].index
|
|
139
189
|
# Set the latest symbol_data to None
|
|
140
190
|
self.latest_symbol_data[s] = []
|
|
141
191
|
|
|
@@ -144,10 +194,12 @@ class BaseCSVDataHandler(DataHandler):
|
|
|
144
194
|
self.symbol_data[s] = self.symbol_data[s].reindex(
|
|
145
195
|
index=comb_index, method='pad'
|
|
146
196
|
)
|
|
147
|
-
self.symbol_data[s][
|
|
148
|
-
|
|
197
|
+
self.symbol_data[s]['returns'] = self.symbol_data[s][
|
|
198
|
+
'adj_close' if 'adj_close' in new_names else 'close'
|
|
149
199
|
].pct_change().dropna()
|
|
150
|
-
self.
|
|
200
|
+
self._index = self.symbol_data[s].index.name
|
|
201
|
+
if self.events is not None:
|
|
202
|
+
self.symbol_data[s] = self.symbol_data[s].iterrows()
|
|
151
203
|
|
|
152
204
|
def _get_new_bar(self, symbol: str):
|
|
153
205
|
"""
|
|
@@ -156,7 +208,7 @@ class BaseCSVDataHandler(DataHandler):
|
|
|
156
208
|
for b in self.symbol_data[symbol]:
|
|
157
209
|
yield b
|
|
158
210
|
|
|
159
|
-
def get_latest_bar(self, symbol: str):
|
|
211
|
+
def get_latest_bar(self, symbol: str) -> pd.Series:
|
|
160
212
|
"""
|
|
161
213
|
Returns the last bar from the latest_symbol list.
|
|
162
214
|
"""
|
|
@@ -168,7 +220,7 @@ class BaseCSVDataHandler(DataHandler):
|
|
|
168
220
|
else:
|
|
169
221
|
return bars_list[-1]
|
|
170
222
|
|
|
171
|
-
def get_latest_bars(self, symbol: str, N=1):
|
|
223
|
+
def get_latest_bars(self, symbol: str, N=1, df=True) -> pd.DataFrame | List[pd.Series]:
|
|
172
224
|
"""
|
|
173
225
|
Returns the last N bars from the latest_symbol list,
|
|
174
226
|
or N-k if less available.
|
|
@@ -179,9 +231,13 @@ class BaseCSVDataHandler(DataHandler):
|
|
|
179
231
|
print("Symbol not available in the historical data set.")
|
|
180
232
|
raise
|
|
181
233
|
else:
|
|
234
|
+
if df:
|
|
235
|
+
df = pd.DataFrame([bar[1] for bar in bars_list[-N:]])
|
|
236
|
+
df.index.name = self._index
|
|
237
|
+
return df
|
|
182
238
|
return bars_list[-N:]
|
|
183
239
|
|
|
184
|
-
def get_latest_bar_datetime(self, symbol: str):
|
|
240
|
+
def get_latest_bar_datetime(self, symbol: str) -> datetime | pd.Timestamp:
|
|
185
241
|
"""
|
|
186
242
|
Returns a Python datetime object for the last bar.
|
|
187
243
|
"""
|
|
@@ -193,7 +249,19 @@ class BaseCSVDataHandler(DataHandler):
|
|
|
193
249
|
else:
|
|
194
250
|
return bars_list[-1][0]
|
|
195
251
|
|
|
196
|
-
def
|
|
252
|
+
def get_latest_bars_datetime(self, symbol: str, N=1) -> List[datetime | pd.Timestamp]:
|
|
253
|
+
"""
|
|
254
|
+
Returns a list of Python datetime objects for the last N bars.
|
|
255
|
+
"""
|
|
256
|
+
try:
|
|
257
|
+
bars_list = self.get_latest_bars(symbol, N)
|
|
258
|
+
except KeyError:
|
|
259
|
+
print("Symbol not available in the historical data set.")
|
|
260
|
+
raise
|
|
261
|
+
else:
|
|
262
|
+
return [b[0] for b in bars_list]
|
|
263
|
+
|
|
264
|
+
def get_latest_bar_value(self, symbol: str, val_type: str) -> float:
|
|
197
265
|
"""
|
|
198
266
|
Returns one of the Open, High, Low, Close, Volume or OI
|
|
199
267
|
values from the pandas Bar series object.
|
|
@@ -206,13 +274,13 @@ class BaseCSVDataHandler(DataHandler):
|
|
|
206
274
|
else:
|
|
207
275
|
return getattr(bars_list[-1][1], val_type)
|
|
208
276
|
|
|
209
|
-
def get_latest_bars_values(self, symbol: str, val_type: str, N=1):
|
|
277
|
+
def get_latest_bars_values(self, symbol: str, val_type: str, N=1) -> np.ndarray:
|
|
210
278
|
"""
|
|
211
279
|
Returns the last N bar values from the
|
|
212
280
|
latest_symbol list, or N-k if less available.
|
|
213
281
|
"""
|
|
214
282
|
try:
|
|
215
|
-
bars_list = self.get_latest_bars(symbol, N)
|
|
283
|
+
bars_list = self.get_latest_bars(symbol, N, df=False)
|
|
216
284
|
except KeyError:
|
|
217
285
|
print("That symbol is not available in the historical data set.")
|
|
218
286
|
raise
|
|
@@ -235,9 +303,9 @@ class BaseCSVDataHandler(DataHandler):
|
|
|
235
303
|
self.events.put(MarketEvent())
|
|
236
304
|
|
|
237
305
|
|
|
238
|
-
class
|
|
306
|
+
class CSVDataHandler(BaseCSVDataHandler):
|
|
239
307
|
"""
|
|
240
|
-
`
|
|
308
|
+
`CSVDataHandler` is designed to read CSV files for
|
|
241
309
|
each requested symbol from disk and provide an interface
|
|
242
310
|
to obtain the "latest" bar in a manner identical to a live
|
|
243
311
|
trading interface.
|
|
@@ -255,14 +323,19 @@ class HistoricCSVDataHandler(BaseCSVDataHandler):
|
|
|
255
323
|
|
|
256
324
|
Args:
|
|
257
325
|
events (Queue): The Event Queue.
|
|
258
|
-
csv_dir (str): Absolute directory path to the CSV files.
|
|
259
326
|
symbol_list (List[str]): A list of symbol strings.
|
|
327
|
+
csv_dir (str): Absolute directory path to the CSV files.
|
|
328
|
+
|
|
329
|
+
NOTE:
|
|
330
|
+
All csv fille can be strored in 'Home/.bbstrader/csv_data'
|
|
331
|
+
|
|
260
332
|
"""
|
|
261
333
|
csv_dir = kwargs.get("csv_dir")
|
|
334
|
+
csv_dir = csv_dir or BBSTRADER_DIR / 'csv_data'
|
|
262
335
|
super().__init__(events, symbol_list, csv_dir)
|
|
263
336
|
|
|
264
337
|
|
|
265
|
-
class
|
|
338
|
+
class MT5DataHandler(BaseCSVDataHandler):
|
|
266
339
|
"""
|
|
267
340
|
Downloads historical data from MetaTrader 5 (MT5) and provides
|
|
268
341
|
an interface for accessing this data bar-by-bar, simulating
|
|
@@ -284,27 +357,39 @@ class MT5HistoricDataHandler(BaseCSVDataHandler):
|
|
|
284
357
|
time_frame (str): MT5 time frame (e.g., 'D1' for daily).
|
|
285
358
|
mt5_start (datetime): Start date for historical data.
|
|
286
359
|
mt5_end (datetime): End date for historical data.
|
|
287
|
-
|
|
360
|
+
data_dir (str): Directory for storing data .
|
|
288
361
|
|
|
289
362
|
Note:
|
|
290
363
|
Requires a working connection to an MT5 terminal.
|
|
364
|
+
See `bbstrader.metatrader.rates.Rates` for other arguments.
|
|
365
|
+
See `bbstrader.btengine.data.BaseCSVDataHandler` for other arguments.
|
|
291
366
|
"""
|
|
292
367
|
self.tf = kwargs.get('time_frame', 'D1')
|
|
293
|
-
self.start = kwargs.get('mt5_start')
|
|
368
|
+
self.start = kwargs.get('mt5_start', datetime(2000, 1, 1))
|
|
294
369
|
self.end = kwargs.get('mt5_end', datetime.now())
|
|
295
|
-
self.
|
|
370
|
+
self.use_utc = kwargs.get('use_utc', False)
|
|
371
|
+
self.filer = kwargs.get('filter', False)
|
|
372
|
+
self.fill_na = kwargs.get('fill_na', False)
|
|
373
|
+
self.lower_cols = kwargs.get('lower_cols', True)
|
|
374
|
+
self.data_dir = kwargs.get('data_dir')
|
|
296
375
|
self.symbol_list = symbol_list
|
|
297
376
|
csv_dir = self._download_data(self.data_dir)
|
|
298
377
|
super().__init__(events, symbol_list, csv_dir)
|
|
299
378
|
|
|
300
379
|
def _download_data(self, cache_dir: str):
|
|
301
|
-
data_dir =
|
|
380
|
+
data_dir = cache_dir or BBSTRADER_DIR / 'mt5_data' / self.tf
|
|
302
381
|
data_dir.mkdir(parents=True, exist_ok=True)
|
|
303
382
|
for symbol in self.symbol_list:
|
|
304
383
|
try:
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
384
|
+
data = download_historical_data(
|
|
385
|
+
symbol=symbol,
|
|
386
|
+
time_frame=self.tf,
|
|
387
|
+
date_from=self.start,
|
|
388
|
+
date_to=self.end,
|
|
389
|
+
utc=self.use_utc,
|
|
390
|
+
filter=self.filer,
|
|
391
|
+
fill_na=self.fill_na,
|
|
392
|
+
lower_colnames=self.lower_cols
|
|
308
393
|
)
|
|
309
394
|
if data is None:
|
|
310
395
|
raise ValueError(f"No data found for {symbol}")
|
|
@@ -314,7 +399,7 @@ class MT5HistoricDataHandler(BaseCSVDataHandler):
|
|
|
314
399
|
return data_dir
|
|
315
400
|
|
|
316
401
|
|
|
317
|
-
class
|
|
402
|
+
class YFDataHandler(BaseCSVDataHandler):
|
|
318
403
|
"""
|
|
319
404
|
Downloads historical data from Yahoo Finance and provides
|
|
320
405
|
an interface for accessing this data bar-by-bar, simulating
|
|
@@ -333,40 +418,47 @@ class YFHistoricDataHandler(BaseCSVDataHandler):
|
|
|
333
418
|
symbol_list (list[str]): List of symbols to download data for.
|
|
334
419
|
yf_start (str): Start date for historical data (YYYY-MM-DD).
|
|
335
420
|
yf_end (str): End date for historical data (YYYY-MM-DD).
|
|
336
|
-
|
|
421
|
+
data_dir (str, optional): Directory for caching data .
|
|
422
|
+
|
|
423
|
+
Note:
|
|
424
|
+
See `bbstrader.btengine.data.BaseCSVDataHandler` for other arguments.
|
|
337
425
|
"""
|
|
338
426
|
self.symbol_list = symbol_list
|
|
339
|
-
self.start_date = kwargs.get('yf_start')
|
|
340
|
-
self.end_date = kwargs.get('yf_end')
|
|
341
|
-
self.cache_dir = kwargs.get('
|
|
427
|
+
self.start_date = kwargs.get('yf_start', '2000-01-01')
|
|
428
|
+
self.end_date = kwargs.get('yf_end', datetime.now().strftime('%Y-%m-%d'))
|
|
429
|
+
self.cache_dir = kwargs.get('data_dir')
|
|
342
430
|
csv_dir = self._download_and_cache_data(self.cache_dir)
|
|
343
431
|
super().__init__(events, symbol_list, csv_dir)
|
|
344
432
|
|
|
345
433
|
def _download_and_cache_data(self, cache_dir: str):
|
|
346
434
|
"""Downloads and caches historical data as CSV files."""
|
|
435
|
+
cache_dir = cache_dir or BBSTRADER_DIR / 'yf_data' / 'daily'
|
|
347
436
|
os.makedirs(cache_dir, exist_ok=True)
|
|
348
437
|
for symbol in self.symbol_list:
|
|
349
438
|
filepath = os.path.join(cache_dir, f"{symbol}.csv")
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
raise ValueError(f"Error downloading {symbol}: {e}")
|
|
439
|
+
try:
|
|
440
|
+
data = yf.download(
|
|
441
|
+
symbol, start=self.start_date, end=self.end_date, multi_level_index=False)
|
|
442
|
+
if data.empty:
|
|
443
|
+
raise ValueError(f"No data found for {symbol}")
|
|
444
|
+
data.to_csv(filepath) # Cache the data
|
|
445
|
+
except Exception as e:
|
|
446
|
+
raise ValueError(f"Error downloading {symbol}: {e}")
|
|
359
447
|
return cache_dir
|
|
360
448
|
|
|
361
449
|
|
|
362
450
|
# TODO # Get data from EODHD
|
|
363
451
|
# https://eodhd.com/
|
|
364
|
-
class
|
|
452
|
+
class EODHDataHandler(BaseCSVDataHandler):
|
|
365
453
|
...
|
|
366
454
|
|
|
367
455
|
# TODO # Get data from FMP using Financialtoolkit API
|
|
368
456
|
# https://github.com/bbalouki/FinanceToolkit
|
|
369
|
-
class
|
|
457
|
+
class FMPDataHandler(BaseCSVDataHandler):
|
|
458
|
+
...
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
class AlgoseekDataHandler(BaseCSVDataHandler):
|
|
370
462
|
...
|
|
371
463
|
|
|
372
464
|
|
|
@@ -57,7 +57,8 @@ class SignalEvent(Event):
|
|
|
57
57
|
signal_type: Literal['LONG', 'SHORT', 'EXIT'],
|
|
58
58
|
quantity: int | float = 100,
|
|
59
59
|
strength: int | float = 1.0,
|
|
60
|
-
price: int | float = None
|
|
60
|
+
price: int | float = None,
|
|
61
|
+
stoplimit: int | float = None
|
|
61
62
|
):
|
|
62
63
|
"""
|
|
63
64
|
Initialises the SignalEvent.
|
|
@@ -73,6 +74,7 @@ class SignalEvent(Event):
|
|
|
73
74
|
strength (int | float): An adjustment factor "suggestion" used to scale
|
|
74
75
|
quantity at the portfolio level. Useful for pairs strategies.
|
|
75
76
|
price (int | float): An optional price to be used when the signal is generated.
|
|
77
|
+
stoplimit (int | float): An optional stop-limit price for the signal
|
|
76
78
|
"""
|
|
77
79
|
self.type = 'SIGNAL'
|
|
78
80
|
self.strategy_id = strategy_id
|
|
@@ -82,6 +84,7 @@ class SignalEvent(Event):
|
|
|
82
84
|
self.quantity = quantity
|
|
83
85
|
self.strength = strength
|
|
84
86
|
self.price = price
|
|
87
|
+
self.stoplimit = stoplimit
|
|
85
88
|
|
|
86
89
|
|
|
87
90
|
class OrderEvent(Event):
|