bbstrader 0.1.9__tar.gz → 0.1.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.
Potentially problematic release.
This version of bbstrader might be problematic. Click here for more details.
- bbstrader-0.1.92/MANIFEST.in +1 -0
- {bbstrader-0.1.9 → bbstrader-0.1.92}/PKG-INFO +12 -3
- {bbstrader-0.1.9 → bbstrader-0.1.92}/bbstrader/__ini__.py +4 -2
- {bbstrader-0.1.9 → bbstrader-0.1.92}/bbstrader/btengine/__init__.py +5 -5
- {bbstrader-0.1.9 → bbstrader-0.1.92}/bbstrader/btengine/backtest.py +60 -12
- {bbstrader-0.1.9 → bbstrader-0.1.92}/bbstrader/btengine/data.py +157 -57
- {bbstrader-0.1.9 → bbstrader-0.1.92}/bbstrader/btengine/event.py +14 -5
- bbstrader-0.1.92/bbstrader/btengine/execution.py +245 -0
- {bbstrader-0.1.9 → bbstrader-0.1.92}/bbstrader/btengine/performance.py +4 -7
- {bbstrader-0.1.9 → bbstrader-0.1.92}/bbstrader/btengine/portfolio.py +83 -36
- bbstrader-0.1.92/bbstrader/btengine/strategy.py +551 -0
- bbstrader-0.1.92/bbstrader/config.py +111 -0
- bbstrader-0.1.92/bbstrader/metatrader/__init__.py +6 -0
- {bbstrader-0.1.9 → bbstrader-0.1.92}/bbstrader/metatrader/account.py +348 -53
- bbstrader-0.1.92/bbstrader/metatrader/rates.py +510 -0
- {bbstrader-0.1.9 → bbstrader-0.1.92}/bbstrader/metatrader/risk.py +34 -23
- {bbstrader-0.1.9 → bbstrader-0.1.92}/bbstrader/metatrader/trade.py +328 -169
- {bbstrader-0.1.9 → bbstrader-0.1.92}/bbstrader/metatrader/utils.py +2 -53
- bbstrader-0.1.92/bbstrader/models/factors.py +0 -0
- bbstrader-0.1.92/bbstrader/models/ml.py +0 -0
- bbstrader-0.1.92/bbstrader/models/optimization.py +170 -0
- bbstrader-0.1.92/bbstrader/models/portfolios.py +202 -0
- {bbstrader-0.1.9 → bbstrader-0.1.92}/bbstrader/trading/__init__.py +1 -1
- bbstrader-0.1.92/bbstrader/trading/execution.py +539 -0
- bbstrader-0.1.92/bbstrader/trading/scripts.py +57 -0
- {bbstrader-0.1.9 → bbstrader-0.1.92}/bbstrader/trading/strategies.py +41 -65
- {bbstrader-0.1.9 → bbstrader-0.1.92}/bbstrader/tseries.py +274 -39
- {bbstrader-0.1.9 → bbstrader-0.1.92}/bbstrader.egg-info/PKG-INFO +12 -3
- {bbstrader-0.1.9 → bbstrader-0.1.92}/bbstrader.egg-info/SOURCES.txt +8 -0
- {bbstrader-0.1.9 → bbstrader-0.1.92}/bbstrader.egg-info/requires.txt +12 -2
- bbstrader-0.1.92/requirements.txt +25 -0
- {bbstrader-0.1.9 → bbstrader-0.1.92}/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.92}/LICENSE +0 -0
- {bbstrader-0.1.9 → bbstrader-0.1.92}/README.md +0 -0
- {bbstrader-0.1.9 → bbstrader-0.1.92}/bbstrader/models/__init__.py +0 -0
- {bbstrader-0.1.9 → bbstrader-0.1.92}/bbstrader/models/risk.py +0 -0
- {bbstrader-0.1.9 → bbstrader-0.1.92}/bbstrader.egg-info/dependency_links.txt +0 -0
- {bbstrader-0.1.9 → bbstrader-0.1.92}/bbstrader.egg-info/top_level.txt +0 -0
- {bbstrader-0.1.9 → bbstrader-0.1.92}/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.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/
|
|
@@ -34,12 +34,21 @@ 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
|
+
Requires-Dist: pyportfolioopt
|
|
50
|
+
Provides-Extra: mt5
|
|
51
|
+
Requires-Dist: MetaTrader5; extra == "mt5"
|
|
43
52
|
|
|
44
53
|
# Simplified Investment & Trading Toolkit
|
|
45
54
|

|
|
@@ -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],
|
|
@@ -107,8 +111,9 @@ class Backtest(object):
|
|
|
107
111
|
their class types.
|
|
108
112
|
"""
|
|
109
113
|
print(
|
|
110
|
-
f"\
|
|
111
|
-
f"
|
|
114
|
+
f"\n[======= STARTING BACKTEST =======]\n"
|
|
115
|
+
f"START DATE: {self.start_date} \n"
|
|
116
|
+
f"INITIAL CAPITAL: {self.initial_capital}\n"
|
|
112
117
|
)
|
|
113
118
|
self.data_handler: DataHandler = self.dh_cls(
|
|
114
119
|
self.events, self.symbol_list, **self.kwargs
|
|
@@ -123,7 +128,7 @@ class Backtest(object):
|
|
|
123
128
|
self.initial_capital, **self.kwargs
|
|
124
129
|
)
|
|
125
130
|
self.execution_handler: ExecutionHandler = self.eh_cls(
|
|
126
|
-
self.events, **self.kwargs)
|
|
131
|
+
self.events, self.data_handler, **self.kwargs)
|
|
127
132
|
|
|
128
133
|
def _run_backtest(self):
|
|
129
134
|
"""
|
|
@@ -132,11 +137,14 @@ class Backtest(object):
|
|
|
132
137
|
i = 0
|
|
133
138
|
while True:
|
|
134
139
|
i += 1
|
|
135
|
-
print(i)
|
|
136
140
|
# Update the market bars
|
|
137
141
|
if self.data_handler.continue_backtest == True:
|
|
138
142
|
self.data_handler.update_bars()
|
|
143
|
+
self.strategy.check_pending_orders()
|
|
139
144
|
else:
|
|
145
|
+
print("\n[======= BACKTEST COMPLETED =======]")
|
|
146
|
+
print(f"END DATE: {self.data_handler.get_latest_bar_datetime()}")
|
|
147
|
+
print(f"TOTAL BARS: {i} ")
|
|
140
148
|
break
|
|
141
149
|
|
|
142
150
|
# Handle the events
|
|
@@ -162,6 +170,11 @@ class Backtest(object):
|
|
|
162
170
|
elif event.type == 'FILL':
|
|
163
171
|
self.fills += 1
|
|
164
172
|
self.portfolio.update_fill(event)
|
|
173
|
+
self.strategy.update_trades_from_fill(event)
|
|
174
|
+
self.strategy.get_update_from_portfolio(
|
|
175
|
+
self.portfolio.current_positions,
|
|
176
|
+
self.portfolio.current_holdings
|
|
177
|
+
)
|
|
165
178
|
|
|
166
179
|
time.sleep(self.heartbeat)
|
|
167
180
|
|
|
@@ -189,7 +202,7 @@ class Backtest(object):
|
|
|
189
202
|
|
|
190
203
|
if self.show_equity:
|
|
191
204
|
print("\nCreating equity curve...")
|
|
192
|
-
print("\n[=======
|
|
205
|
+
print("\n[======= PORTFOLIO SUMMARY =======]")
|
|
193
206
|
print(
|
|
194
207
|
tabulate(
|
|
195
208
|
self.portfolio.equity_curve.tail(10),
|
|
@@ -201,11 +214,14 @@ class Backtest(object):
|
|
|
201
214
|
def simulate_trading(self):
|
|
202
215
|
"""
|
|
203
216
|
Simulates the backtest and outputs portfolio performance.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
pd.DataFrame: The portfilio values over time (capital, equity, returns etc.)
|
|
204
220
|
"""
|
|
205
221
|
self._run_backtest()
|
|
206
222
|
self._output_performance()
|
|
223
|
+
return self.portfolio.equity_curve
|
|
207
224
|
|
|
208
|
-
BacktestEngine = Backtest
|
|
209
225
|
|
|
210
226
|
def run_backtest(
|
|
211
227
|
symbol_list: List[str],
|
|
@@ -226,8 +242,8 @@ def run_backtest(
|
|
|
226
242
|
start_date (datetime): Start date of the backtest.
|
|
227
243
|
|
|
228
244
|
data_handler (DataHandler): An instance of the `DataHandler` class, responsible for managing
|
|
229
|
-
and processing market data. Available options include `
|
|
230
|
-
`
|
|
245
|
+
and processing market data. Available options include `CSVDataHandler`,
|
|
246
|
+
`MT5DataHandler`, and `YFDataHandler`. Ensure that the `DataHandler`
|
|
231
247
|
instance is initialized before passing it to the function.
|
|
232
248
|
|
|
233
249
|
strategy (Strategy): The trading strategy to be employed during the backtest.
|
|
@@ -253,6 +269,9 @@ def run_backtest(
|
|
|
253
269
|
|
|
254
270
|
**kwargs: Additional parameters passed to the `Backtest` instance, which may include strategy-specific,
|
|
255
271
|
data handler, portfolio, or execution handler options.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
pd.DataFrame: The portfolio values over time (capital, equities, returns etc.).
|
|
256
275
|
|
|
257
276
|
Notes:
|
|
258
277
|
This function generates three outputs:
|
|
@@ -263,7 +282,7 @@ def run_backtest(
|
|
|
263
282
|
Example:
|
|
264
283
|
>>> from bbstrader.trading.strategies import StockIndexSTBOTrading
|
|
265
284
|
>>> from bbstrader.metatrader.utils import config_logger
|
|
266
|
-
>>> from bbstrader.datahandlers import
|
|
285
|
+
>>> from bbstrader.datahandlers import MT5DataHandler
|
|
267
286
|
>>> from bbstrader.execution import MT5ExecutionHandler
|
|
268
287
|
>>> from datetime import datetime
|
|
269
288
|
>>>
|
|
@@ -281,7 +300,7 @@ def run_backtest(
|
|
|
281
300
|
>>> run_backtest(
|
|
282
301
|
... symbol_list=symbol_list,
|
|
283
302
|
... start_date=start,
|
|
284
|
-
... data_handler=
|
|
303
|
+
... data_handler=MT5DataHandler(),
|
|
285
304
|
... strategy=StockIndexSTBOTrading(),
|
|
286
305
|
... exc_handler=MT5ExecutionHandler(),
|
|
287
306
|
... initial_capital=100000.0,
|
|
@@ -290,11 +309,40 @@ def run_backtest(
|
|
|
290
309
|
... )
|
|
291
310
|
"""
|
|
292
311
|
if exc_handler is None:
|
|
293
|
-
execution_handler =
|
|
312
|
+
execution_handler = SimExecutionHandler
|
|
294
313
|
else:
|
|
295
314
|
execution_handler = exc_handler
|
|
296
315
|
engine = BacktestEngine(
|
|
297
316
|
symbol_list, initial_capital, heartbeat, start_date,
|
|
298
317
|
data_handler, execution_handler, strategy, **kwargs
|
|
299
318
|
)
|
|
300
|
-
engine.simulate_trading()
|
|
319
|
+
portfolio = engine.simulate_trading()
|
|
320
|
+
return portfolio
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class CerebroEngine:...
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class ZiplineEngine:...
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def run_backtest_with(engine: Literal["bbstrader", "cerebro", "zipline"], **kwargs):
|
|
330
|
+
"""
|
|
331
|
+
"""
|
|
332
|
+
if engine == "bbstrader":
|
|
333
|
+
return run_backtest(
|
|
334
|
+
symbol_list=kwargs.get("symbol_list"),
|
|
335
|
+
start_date=kwargs.get("start_date"),
|
|
336
|
+
data_handler=kwargs.get("data_handler"),
|
|
337
|
+
strategy=kwargs.get("strategy"),
|
|
338
|
+
exc_handler=kwargs.get("exc_handler"),
|
|
339
|
+
initial_capital=kwargs.get("initial_capital"),
|
|
340
|
+
heartbeat=kwargs.get("heartbeat", 0.0),
|
|
341
|
+
**kwargs
|
|
342
|
+
)
|
|
343
|
+
elif engine == "cerebro":
|
|
344
|
+
#TODO:
|
|
345
|
+
pass
|
|
346
|
+
elif engine == "zipline":
|
|
347
|
+
#TODO:
|
|
348
|
+
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.
|
|
@@ -204,20 +272,28 @@ class BaseCSVDataHandler(DataHandler):
|
|
|
204
272
|
print("Symbol not available in the historical data set.")
|
|
205
273
|
raise
|
|
206
274
|
else:
|
|
207
|
-
|
|
275
|
+
try:
|
|
276
|
+
return getattr(bars_list[-1][1], val_type)
|
|
277
|
+
except AttributeError:
|
|
278
|
+
print(f"Value type {val_type} not available in the historical data set.")
|
|
279
|
+
raise
|
|
208
280
|
|
|
209
|
-
def get_latest_bars_values(self, symbol: str, val_type: str, N=1):
|
|
281
|
+
def get_latest_bars_values(self, symbol: str, val_type: str, N=1) -> np.ndarray:
|
|
210
282
|
"""
|
|
211
283
|
Returns the last N bar values from the
|
|
212
284
|
latest_symbol list, or N-k if less available.
|
|
213
285
|
"""
|
|
214
286
|
try:
|
|
215
|
-
bars_list = self.get_latest_bars(symbol, N)
|
|
287
|
+
bars_list = self.get_latest_bars(symbol, N, df=False)
|
|
216
288
|
except KeyError:
|
|
217
289
|
print("That symbol is not available in the historical data set.")
|
|
218
290
|
raise
|
|
219
291
|
else:
|
|
220
|
-
|
|
292
|
+
try:
|
|
293
|
+
return np.array([getattr(b[1], val_type) for b in bars_list])
|
|
294
|
+
except AttributeError:
|
|
295
|
+
print(f"Value type {val_type} not available in the historical data set.")
|
|
296
|
+
raise
|
|
221
297
|
|
|
222
298
|
def update_bars(self):
|
|
223
299
|
"""
|
|
@@ -235,9 +311,9 @@ class BaseCSVDataHandler(DataHandler):
|
|
|
235
311
|
self.events.put(MarketEvent())
|
|
236
312
|
|
|
237
313
|
|
|
238
|
-
class
|
|
314
|
+
class CSVDataHandler(BaseCSVDataHandler):
|
|
239
315
|
"""
|
|
240
|
-
`
|
|
316
|
+
`CSVDataHandler` is designed to read CSV files for
|
|
241
317
|
each requested symbol from disk and provide an interface
|
|
242
318
|
to obtain the "latest" bar in a manner identical to a live
|
|
243
319
|
trading interface.
|
|
@@ -255,14 +331,19 @@ class HistoricCSVDataHandler(BaseCSVDataHandler):
|
|
|
255
331
|
|
|
256
332
|
Args:
|
|
257
333
|
events (Queue): The Event Queue.
|
|
258
|
-
csv_dir (str): Absolute directory path to the CSV files.
|
|
259
334
|
symbol_list (List[str]): A list of symbol strings.
|
|
335
|
+
csv_dir (str): Absolute directory path to the CSV files.
|
|
336
|
+
|
|
337
|
+
NOTE:
|
|
338
|
+
All csv fille can be strored in 'Home/.bbstrader/csv_data'
|
|
339
|
+
|
|
260
340
|
"""
|
|
261
341
|
csv_dir = kwargs.get("csv_dir")
|
|
342
|
+
csv_dir = csv_dir or BBSTRADER_DIR / 'csv_data'
|
|
262
343
|
super().__init__(events, symbol_list, csv_dir)
|
|
263
344
|
|
|
264
345
|
|
|
265
|
-
class
|
|
346
|
+
class MT5DataHandler(BaseCSVDataHandler):
|
|
266
347
|
"""
|
|
267
348
|
Downloads historical data from MetaTrader 5 (MT5) and provides
|
|
268
349
|
an interface for accessing this data bar-by-bar, simulating
|
|
@@ -284,27 +365,39 @@ class MT5HistoricDataHandler(BaseCSVDataHandler):
|
|
|
284
365
|
time_frame (str): MT5 time frame (e.g., 'D1' for daily).
|
|
285
366
|
mt5_start (datetime): Start date for historical data.
|
|
286
367
|
mt5_end (datetime): End date for historical data.
|
|
287
|
-
|
|
368
|
+
data_dir (str): Directory for storing data .
|
|
288
369
|
|
|
289
370
|
Note:
|
|
290
371
|
Requires a working connection to an MT5 terminal.
|
|
372
|
+
See `bbstrader.metatrader.rates.Rates` for other arguments.
|
|
373
|
+
See `bbstrader.btengine.data.BaseCSVDataHandler` for other arguments.
|
|
291
374
|
"""
|
|
292
375
|
self.tf = kwargs.get('time_frame', 'D1')
|
|
293
|
-
self.start = kwargs.get('mt5_start')
|
|
376
|
+
self.start = kwargs.get('mt5_start', datetime(2000, 1, 1))
|
|
294
377
|
self.end = kwargs.get('mt5_end', datetime.now())
|
|
295
|
-
self.
|
|
378
|
+
self.use_utc = kwargs.get('use_utc', False)
|
|
379
|
+
self.filer = kwargs.get('filter', False)
|
|
380
|
+
self.fill_na = kwargs.get('fill_na', False)
|
|
381
|
+
self.lower_cols = kwargs.get('lower_cols', True)
|
|
382
|
+
self.data_dir = kwargs.get('data_dir')
|
|
296
383
|
self.symbol_list = symbol_list
|
|
297
384
|
csv_dir = self._download_data(self.data_dir)
|
|
298
385
|
super().__init__(events, symbol_list, csv_dir)
|
|
299
386
|
|
|
300
387
|
def _download_data(self, cache_dir: str):
|
|
301
|
-
data_dir =
|
|
388
|
+
data_dir = cache_dir or BBSTRADER_DIR / 'mt5_data' / self.tf
|
|
302
389
|
data_dir.mkdir(parents=True, exist_ok=True)
|
|
303
390
|
for symbol in self.symbol_list:
|
|
304
391
|
try:
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
392
|
+
data = download_historical_data(
|
|
393
|
+
symbol=symbol,
|
|
394
|
+
time_frame=self.tf,
|
|
395
|
+
date_from=self.start,
|
|
396
|
+
date_to=self.end,
|
|
397
|
+
utc=self.use_utc,
|
|
398
|
+
filter=self.filer,
|
|
399
|
+
fill_na=self.fill_na,
|
|
400
|
+
lower_colnames=self.lower_cols
|
|
308
401
|
)
|
|
309
402
|
if data is None:
|
|
310
403
|
raise ValueError(f"No data found for {symbol}")
|
|
@@ -314,7 +407,7 @@ class MT5HistoricDataHandler(BaseCSVDataHandler):
|
|
|
314
407
|
return data_dir
|
|
315
408
|
|
|
316
409
|
|
|
317
|
-
class
|
|
410
|
+
class YFDataHandler(BaseCSVDataHandler):
|
|
318
411
|
"""
|
|
319
412
|
Downloads historical data from Yahoo Finance and provides
|
|
320
413
|
an interface for accessing this data bar-by-bar, simulating
|
|
@@ -333,40 +426,47 @@ class YFHistoricDataHandler(BaseCSVDataHandler):
|
|
|
333
426
|
symbol_list (list[str]): List of symbols to download data for.
|
|
334
427
|
yf_start (str): Start date for historical data (YYYY-MM-DD).
|
|
335
428
|
yf_end (str): End date for historical data (YYYY-MM-DD).
|
|
336
|
-
|
|
429
|
+
data_dir (str, optional): Directory for caching data .
|
|
430
|
+
|
|
431
|
+
Note:
|
|
432
|
+
See `bbstrader.btengine.data.BaseCSVDataHandler` for other arguments.
|
|
337
433
|
"""
|
|
338
434
|
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('
|
|
435
|
+
self.start_date = kwargs.get('yf_start', '2000-01-01')
|
|
436
|
+
self.end_date = kwargs.get('yf_end', datetime.now().strftime('%Y-%m-%d'))
|
|
437
|
+
self.cache_dir = kwargs.get('data_dir')
|
|
342
438
|
csv_dir = self._download_and_cache_data(self.cache_dir)
|
|
343
439
|
super().__init__(events, symbol_list, csv_dir)
|
|
344
440
|
|
|
345
441
|
def _download_and_cache_data(self, cache_dir: str):
|
|
346
442
|
"""Downloads and caches historical data as CSV files."""
|
|
443
|
+
cache_dir = cache_dir or BBSTRADER_DIR / 'yf_data' / 'daily'
|
|
347
444
|
os.makedirs(cache_dir, exist_ok=True)
|
|
348
445
|
for symbol in self.symbol_list:
|
|
349
446
|
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}")
|
|
447
|
+
try:
|
|
448
|
+
data = yf.download(
|
|
449
|
+
symbol, start=self.start_date, end=self.end_date, multi_level_index=False)
|
|
450
|
+
if data.empty:
|
|
451
|
+
raise ValueError(f"No data found for {symbol}")
|
|
452
|
+
data.to_csv(filepath) # Cache the data
|
|
453
|
+
except Exception as e:
|
|
454
|
+
raise ValueError(f"Error downloading {symbol}: {e}")
|
|
359
455
|
return cache_dir
|
|
360
456
|
|
|
361
457
|
|
|
362
458
|
# TODO # Get data from EODHD
|
|
363
459
|
# https://eodhd.com/
|
|
364
|
-
class
|
|
460
|
+
class EODHDataHandler(BaseCSVDataHandler):
|
|
365
461
|
...
|
|
366
462
|
|
|
367
463
|
# TODO # Get data from FMP using Financialtoolkit API
|
|
368
464
|
# https://github.com/bbalouki/FinanceToolkit
|
|
369
|
-
class
|
|
465
|
+
class FMPDataHandler(BaseCSVDataHandler):
|
|
466
|
+
...
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
class AlgoseekDataHandler(BaseCSVDataHandler):
|
|
370
470
|
...
|
|
371
471
|
|
|
372
472
|
|