bbstrader 0.0.1__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 +17 -0
- bbstrader/btengine/__init__.py +50 -0
- bbstrader/btengine/backtest.py +900 -0
- bbstrader/btengine/data.py +374 -0
- bbstrader/btengine/event.py +201 -0
- bbstrader/btengine/execution.py +83 -0
- bbstrader/btengine/performance.py +309 -0
- bbstrader/btengine/portfolio.py +326 -0
- bbstrader/btengine/strategy.py +31 -0
- bbstrader/metatrader/__init__.py +6 -0
- bbstrader/metatrader/account.py +1038 -0
- bbstrader/metatrader/rates.py +226 -0
- bbstrader/metatrader/risk.py +626 -0
- bbstrader/metatrader/trade.py +1296 -0
- bbstrader/metatrader/utils.py +669 -0
- bbstrader/models/__init__.py +6 -0
- bbstrader/models/risk.py +349 -0
- bbstrader/strategies.py +681 -0
- bbstrader/trading/__init__.py +4 -0
- bbstrader/trading/execution.py +965 -0
- bbstrader/trading/run.py +131 -0
- bbstrader/trading/utils.py +153 -0
- bbstrader/tseries.py +592 -0
- bbstrader-0.0.1.dist-info/LICENSE +21 -0
- bbstrader-0.0.1.dist-info/METADATA +132 -0
- bbstrader-0.0.1.dist-info/RECORD +28 -0
- bbstrader-0.0.1.dist-info/WHEEL +5 -0
- bbstrader-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import os.path
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import yfinance as yf
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List
|
|
7
|
+
from queue import Queue
|
|
8
|
+
from abc import ABCMeta, abstractmethod
|
|
9
|
+
from bbstrader.metatrader.rates import Rates
|
|
10
|
+
from bbstrader.btengine.event import MarketEvent
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"DataHandler",
|
|
15
|
+
"BaseCSVDataHandler",
|
|
16
|
+
"HistoricCSVDataHandler",
|
|
17
|
+
"MT5HistoricDataHandler",
|
|
18
|
+
"YFHistoricDataHandler"
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DataHandler(metaclass=ABCMeta):
|
|
23
|
+
"""
|
|
24
|
+
One of the goals of an event-driven trading system is to minimise
|
|
25
|
+
duplication of code between the backtesting element and the live execution
|
|
26
|
+
element. Ideally it would be optimal to utilise the same signal generation
|
|
27
|
+
methodology and portfolio management components for both historical testing
|
|
28
|
+
and live trading. In order for this to work the Strategy object which generates
|
|
29
|
+
the Signals, and the `Portfolio` object which provides Orders based on them,
|
|
30
|
+
must utilise an identical interface to a market feed for both historic and live
|
|
31
|
+
running.
|
|
32
|
+
|
|
33
|
+
This motivates the concept of a class hierarchy based on a `DataHandler` object,
|
|
34
|
+
which givesall subclasses an interface for providing market data to the remaining
|
|
35
|
+
components within thesystem. In this way any subclass data handler can be "swapped out",
|
|
36
|
+
without affecting strategy or portfolio calculation.
|
|
37
|
+
|
|
38
|
+
Specific example subclasses could include `HistoricCSVDataHandler`,
|
|
39
|
+
`YFinanceDataHandler`, `FMPDataHandler`, `IBMarketFeedDataHandler` etc.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def get_latest_bar(self, symbol):
|
|
44
|
+
"""
|
|
45
|
+
Returns the last bar updated.
|
|
46
|
+
"""
|
|
47
|
+
raise NotImplementedError(
|
|
48
|
+
"Should implement get_latest_bar()"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def get_latest_bars(self, symbol, N=1):
|
|
53
|
+
"""
|
|
54
|
+
Returns the last N bars updated.
|
|
55
|
+
"""
|
|
56
|
+
raise NotImplementedError(
|
|
57
|
+
"Should implement get_latest_bars()"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def get_latest_bar_datetime(self, symbol):
|
|
62
|
+
"""
|
|
63
|
+
Returns a Python datetime object for the last bar.
|
|
64
|
+
"""
|
|
65
|
+
raise NotImplementedError(
|
|
66
|
+
"Should implement get_latest_bar_datetime()"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def get_latest_bar_value(self, symbol, val_type):
|
|
71
|
+
"""
|
|
72
|
+
Returns one of the Open, High, Low, Close, Adj Close, Volume or Returns
|
|
73
|
+
from the last bar.
|
|
74
|
+
"""
|
|
75
|
+
raise NotImplementedError(
|
|
76
|
+
"Should implement get_latest_bar_value()"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
@abstractmethod
|
|
80
|
+
def get_latest_bars_values(self, symbol, val_type, N=1):
|
|
81
|
+
"""
|
|
82
|
+
Returns the last N bar values from the
|
|
83
|
+
latest_symbol list, or N-k if less available.
|
|
84
|
+
"""
|
|
85
|
+
raise NotImplementedError(
|
|
86
|
+
"Should implement get_latest_bars_values()"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
@abstractmethod
|
|
90
|
+
def update_bars(self):
|
|
91
|
+
"""
|
|
92
|
+
Pushes the latest bars to the bars_queue for each symbol
|
|
93
|
+
in a tuple OHLCVI format: (datetime, Open, High, Low,
|
|
94
|
+
Close, Adj Close, Volume, Retruns).
|
|
95
|
+
"""
|
|
96
|
+
raise NotImplementedError(
|
|
97
|
+
"Should implement update_bars()"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class BaseCSVDataHandler(DataHandler):
|
|
102
|
+
"""
|
|
103
|
+
Base class for handling data loaded from CSV files.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
def __init__(self, events: Queue, symbol_list: List[str], csv_dir: str):
|
|
107
|
+
self.events = events
|
|
108
|
+
self.symbol_list = symbol_list
|
|
109
|
+
self.csv_dir = csv_dir
|
|
110
|
+
self.symbol_data = {}
|
|
111
|
+
self.latest_symbol_data = {}
|
|
112
|
+
self.continue_backtest = True
|
|
113
|
+
self._load_and_process_data()
|
|
114
|
+
|
|
115
|
+
def _load_and_process_data(self):
|
|
116
|
+
"""
|
|
117
|
+
Opens the CSV files from the data directory, converting
|
|
118
|
+
them into pandas DataFrames within a symbol dictionary.
|
|
119
|
+
"""
|
|
120
|
+
comb_index = None
|
|
121
|
+
for s in self.symbol_list:
|
|
122
|
+
# Load the CSV file with no header information,
|
|
123
|
+
# indexed on date
|
|
124
|
+
self.symbol_data[s] = pd.read_csv(
|
|
125
|
+
os.path.join(self.csv_dir, f'{s}.csv'),
|
|
126
|
+
header=0, index_col=0, parse_dates=True,
|
|
127
|
+
names=[
|
|
128
|
+
'Datetime', 'Open', 'High',
|
|
129
|
+
'Low', 'Close', 'Adj Close', 'Volume'
|
|
130
|
+
]
|
|
131
|
+
)
|
|
132
|
+
self.symbol_data[s].sort_index(inplace=True)
|
|
133
|
+
# Combine the index to pad forward values
|
|
134
|
+
if comb_index is None:
|
|
135
|
+
comb_index = self.symbol_data[s].index
|
|
136
|
+
else:
|
|
137
|
+
comb_index.union(self.symbol_data[s].index)
|
|
138
|
+
# Set the latest symbol_data to None
|
|
139
|
+
self.latest_symbol_data[s] = []
|
|
140
|
+
|
|
141
|
+
# Reindex the dataframes
|
|
142
|
+
for s in self.symbol_list:
|
|
143
|
+
self.symbol_data[s] = self.symbol_data[s].reindex(
|
|
144
|
+
index=comb_index, method='pad'
|
|
145
|
+
)
|
|
146
|
+
self.symbol_data[s]["Returns"] = self.symbol_data[s][
|
|
147
|
+
"Adj Close"
|
|
148
|
+
].pct_change().dropna()
|
|
149
|
+
self.symbol_data[s] = self.symbol_data[s].iterrows()
|
|
150
|
+
|
|
151
|
+
def _get_new_bar(self, symbol: str):
|
|
152
|
+
"""
|
|
153
|
+
Returns the latest bar from the data feed.
|
|
154
|
+
"""
|
|
155
|
+
for b in self.symbol_data[symbol]:
|
|
156
|
+
yield b
|
|
157
|
+
|
|
158
|
+
def get_latest_bar(self, symbol: str):
|
|
159
|
+
"""
|
|
160
|
+
Returns the last bar from the latest_symbol list.
|
|
161
|
+
"""
|
|
162
|
+
try:
|
|
163
|
+
bars_list = self.latest_symbol_data[symbol]
|
|
164
|
+
except KeyError:
|
|
165
|
+
print("Symbol not available in the historical data set.")
|
|
166
|
+
raise
|
|
167
|
+
else:
|
|
168
|
+
return bars_list[-1]
|
|
169
|
+
|
|
170
|
+
def get_latest_bars(self, symbol: str, N=1):
|
|
171
|
+
"""
|
|
172
|
+
Returns the last N bars from the latest_symbol list,
|
|
173
|
+
or N-k if less available.
|
|
174
|
+
"""
|
|
175
|
+
try:
|
|
176
|
+
bars_list = self.latest_symbol_data[symbol]
|
|
177
|
+
except KeyError:
|
|
178
|
+
print("Symbol not available in the historical data set.")
|
|
179
|
+
raise
|
|
180
|
+
else:
|
|
181
|
+
return bars_list[-N:]
|
|
182
|
+
|
|
183
|
+
def get_latest_bar_datetime(self, symbol: str):
|
|
184
|
+
"""
|
|
185
|
+
Returns a Python datetime object for the last bar.
|
|
186
|
+
"""
|
|
187
|
+
try:
|
|
188
|
+
bars_list = self.latest_symbol_data[symbol]
|
|
189
|
+
except KeyError:
|
|
190
|
+
print("Symbol not available in the historical data set.")
|
|
191
|
+
raise
|
|
192
|
+
else:
|
|
193
|
+
return bars_list[-1][0]
|
|
194
|
+
|
|
195
|
+
def get_latest_bar_value(self, symbol: str, val_type: str):
|
|
196
|
+
"""
|
|
197
|
+
Returns one of the Open, High, Low, Close, Volume or OI
|
|
198
|
+
values from the pandas Bar series object.
|
|
199
|
+
"""
|
|
200
|
+
try:
|
|
201
|
+
bars_list = self.latest_symbol_data[symbol]
|
|
202
|
+
except KeyError:
|
|
203
|
+
print("Symbol not available in the historical data set.")
|
|
204
|
+
raise
|
|
205
|
+
else:
|
|
206
|
+
return getattr(bars_list[-1][1], val_type)
|
|
207
|
+
|
|
208
|
+
def get_latest_bars_values(self, symbol: str, val_type: str, N=1):
|
|
209
|
+
"""
|
|
210
|
+
Returns the last N bar values from the
|
|
211
|
+
latest_symbol list, or N-k if less available.
|
|
212
|
+
"""
|
|
213
|
+
try:
|
|
214
|
+
bars_list = self.get_latest_bars(symbol, N)
|
|
215
|
+
except KeyError:
|
|
216
|
+
print("That symbol is not available in the historical data set.")
|
|
217
|
+
raise
|
|
218
|
+
else:
|
|
219
|
+
return np.array([getattr(b[1], val_type) for b in bars_list])
|
|
220
|
+
|
|
221
|
+
def update_bars(self):
|
|
222
|
+
"""
|
|
223
|
+
Pushes the latest bar to the latest_symbol_data structure
|
|
224
|
+
for all symbols in the symbol list.
|
|
225
|
+
"""
|
|
226
|
+
for s in self.symbol_list:
|
|
227
|
+
try:
|
|
228
|
+
bar = next(self._get_new_bar(s))
|
|
229
|
+
except StopIteration:
|
|
230
|
+
self.continue_backtest = False
|
|
231
|
+
else:
|
|
232
|
+
if bar is not None:
|
|
233
|
+
self.latest_symbol_data[s].append(bar)
|
|
234
|
+
self.events.put(MarketEvent())
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class HistoricCSVDataHandler(BaseCSVDataHandler):
|
|
238
|
+
"""
|
|
239
|
+
`HistoricCSVDataHandler` is designed to read CSV files for
|
|
240
|
+
each requested symbol from disk and provide an interface
|
|
241
|
+
to obtain the "latest" bar in a manner identical to a live
|
|
242
|
+
trading interface.
|
|
243
|
+
|
|
244
|
+
This class is useful when you have your own data or you want
|
|
245
|
+
to cutomize specific data in some form based on your `Strategy()` .
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
def __init__(self, events: Queue, symbol_list: List[str], **kwargs):
|
|
249
|
+
"""
|
|
250
|
+
Initialises the historic data handler by requesting
|
|
251
|
+
the location of the CSV files and a list of symbols.
|
|
252
|
+
It will be assumed that all files are of the form
|
|
253
|
+
`symbol.csv`, where `symbol` is a string in the list.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
events (Queue): The Event Queue.
|
|
257
|
+
csv_dir (str): Absolute directory path to the CSV files.
|
|
258
|
+
symbol_list (List[str]): A list of symbol strings.
|
|
259
|
+
"""
|
|
260
|
+
csv_dir = kwargs.get("csv_dir")
|
|
261
|
+
super().__init__(events, symbol_list, csv_dir)
|
|
262
|
+
|
|
263
|
+
MAX_BARS = 10_000_000
|
|
264
|
+
class MT5HistoricDataHandler(BaseCSVDataHandler):
|
|
265
|
+
"""
|
|
266
|
+
Downloads historical data from MetaTrader 5 (MT5) and provides
|
|
267
|
+
an interface for accessing this data bar-by-bar, simulating
|
|
268
|
+
a live market feed for backtesting.
|
|
269
|
+
|
|
270
|
+
Data is downloaded from MT5, saved as CSV files, and then loaded
|
|
271
|
+
using the functionality inherited from `BaseCSVDataHandler`.
|
|
272
|
+
|
|
273
|
+
This class is useful when you need to get data from specific broker
|
|
274
|
+
for different time frames.
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
def __init__(self, events: Queue, symbol_list: List[str], **kwargs):
|
|
278
|
+
"""
|
|
279
|
+
Args:
|
|
280
|
+
events (Queue): The Event Queue for passing market events.
|
|
281
|
+
symbol_list (List[str]): A list of symbol strings to download data for.
|
|
282
|
+
**kwargs: Keyword arguments for data retrieval:
|
|
283
|
+
time_frame (str): MT5 time frame (e.g., 'D1' for daily).
|
|
284
|
+
max_bars (int): Maximum number of bars to download per symbol.
|
|
285
|
+
start_pos (int | str, optional): Starting bar position (default: 0).
|
|
286
|
+
If it set to `str`, it must be in 'YYYY-MM-DD' format and
|
|
287
|
+
session_duration (int | float): Number of trading hours per day.
|
|
288
|
+
mt5_data (str): Directory for storing data (default: 'mt5_data').
|
|
289
|
+
|
|
290
|
+
Note:
|
|
291
|
+
Requires a working connection to an MT5 terminal.
|
|
292
|
+
"""
|
|
293
|
+
self.tf = kwargs.get('time_frame', 'D1')
|
|
294
|
+
self.start_pos = kwargs.get('start_pos', 0)
|
|
295
|
+
self.max_bars = kwargs.get('max_bars', MAX_BARS)
|
|
296
|
+
self.sd = kwargs.get('session_duration', 6.5)
|
|
297
|
+
self.data_dir = kwargs.get('mt5_data', 'mt5_data')
|
|
298
|
+
self.symbol_list = symbol_list
|
|
299
|
+
csv_dir = self._download_data(self.data_dir)
|
|
300
|
+
super().__init__(events, symbol_list, csv_dir)
|
|
301
|
+
|
|
302
|
+
def _download_data(self, cache_dir: str):
|
|
303
|
+
data_dir = Path() / cache_dir
|
|
304
|
+
data_dir.mkdir(parents=True, exist_ok=True)
|
|
305
|
+
for symbol in self.symbol_list:
|
|
306
|
+
try:
|
|
307
|
+
rate = Rates(symbol, self.tf, self.start_pos, self.max_bars, self.sd)
|
|
308
|
+
data = rate.get_rates_from_pos()
|
|
309
|
+
if data is None:
|
|
310
|
+
raise ValueError(f"No data found for {symbol}")
|
|
311
|
+
data.to_csv(data_dir / f'{symbol}.csv')
|
|
312
|
+
except Exception as e:
|
|
313
|
+
raise ValueError(f"Error downloading {symbol}: {e}")
|
|
314
|
+
return data_dir
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class YFHistoricDataHandler(BaseCSVDataHandler):
|
|
318
|
+
"""
|
|
319
|
+
Downloads historical data from Yahoo Finance and provides
|
|
320
|
+
an interface for accessing this data bar-by-bar, simulating
|
|
321
|
+
a live market feed for backtesting.
|
|
322
|
+
|
|
323
|
+
Data is fetched using the `yfinance` library and optionally cached
|
|
324
|
+
to disk to speed up subsequent runs.
|
|
325
|
+
|
|
326
|
+
This class is useful when working with historical daily prices.
|
|
327
|
+
"""
|
|
328
|
+
|
|
329
|
+
def __init__(self, events: Queue, symbol_list: List[str], **kwargs):
|
|
330
|
+
"""
|
|
331
|
+
Args:
|
|
332
|
+
events (Queue): The Event Queue for passing market events.
|
|
333
|
+
symbol_list (list[str]): List of symbols to download data for.
|
|
334
|
+
yf_start (str): Start date for historical data (YYYY-MM-DD).
|
|
335
|
+
yf_end (str): End date for historical data (YYYY-MM-DD).
|
|
336
|
+
cache_dir (str, optional): Directory for caching data (default: 'yf_cache').
|
|
337
|
+
"""
|
|
338
|
+
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('yf_cache', 'yf_cache')
|
|
342
|
+
csv_dir = self._download_and_cache_data(self.cache_dir)
|
|
343
|
+
super().__init__(events, symbol_list, csv_dir)
|
|
344
|
+
|
|
345
|
+
def _download_and_cache_data(self, cache_dir: str):
|
|
346
|
+
"""Downloads and caches historical data as CSV files."""
|
|
347
|
+
os.makedirs(cache_dir, exist_ok=True)
|
|
348
|
+
for symbol in self.symbol_list:
|
|
349
|
+
filepath = os.path.join(cache_dir, f"{symbol}.csv")
|
|
350
|
+
if not os.path.exists(filepath):
|
|
351
|
+
try:
|
|
352
|
+
data = yf.download(
|
|
353
|
+
symbol, start=self.start_date, end=self.end_date)
|
|
354
|
+
if data.empty:
|
|
355
|
+
raise ValueError(f"No data found for {symbol}")
|
|
356
|
+
data.to_csv(filepath) # Cache the data
|
|
357
|
+
except Exception as e:
|
|
358
|
+
raise ValueError(f"Error downloading {symbol}: {e}")
|
|
359
|
+
return cache_dir
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# TODO # Get data from FinancialModelingPrep ()
|
|
363
|
+
class FMPHistoricDataHandler(BaseCSVDataHandler):
|
|
364
|
+
...
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class BaseFMPDataHanler(object):
|
|
368
|
+
...
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class FMPFundamentalDataHandler(BaseFMPDataHanler):
|
|
372
|
+
...
|
|
373
|
+
|
|
374
|
+
# TODO Add other Handlers
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
"Event",
|
|
5
|
+
"MarketEvent",
|
|
6
|
+
"SignalEvent",
|
|
7
|
+
"OrderEvent",
|
|
8
|
+
"FillEvent"
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Event(object):
|
|
13
|
+
"""
|
|
14
|
+
Event is base class providing an interface for all subsequent
|
|
15
|
+
(inherited) events, that will trigger further events in the
|
|
16
|
+
trading infrastructure.
|
|
17
|
+
Since in many implementations the Event objects will likely develop greater
|
|
18
|
+
complexity, it is thus being "future-proofed" by creating a class hierarchy.
|
|
19
|
+
The Event class is simply a way to ensure that all events have a common interface
|
|
20
|
+
and can be handled in a consistent manner.
|
|
21
|
+
"""
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MarketEvent(Event):
|
|
26
|
+
"""
|
|
27
|
+
Market Events are triggered when the outer while loop of the backtesting
|
|
28
|
+
system begins a new `"heartbeat"`. It occurs when the `DataHandler` object
|
|
29
|
+
receives a new update of market data for any symbols which are currently
|
|
30
|
+
being tracked. It is used to `trigger the Strategy object` generating
|
|
31
|
+
new `trading signals`. The event object simply contains an identification
|
|
32
|
+
that it is a market event, with no other structure.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self):
|
|
36
|
+
"""
|
|
37
|
+
Initialises the MarketEvent.
|
|
38
|
+
"""
|
|
39
|
+
self.type = 'MARKET'
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SignalEvent(Event):
|
|
43
|
+
"""
|
|
44
|
+
The `Strategy object` utilises market data to create new `SignalEvents`.
|
|
45
|
+
The SignalEvent contains a `strategy ID`, a `ticker symbol`, a `timestamp`
|
|
46
|
+
for when it was generated, a `direction` (long or short) and a `"strength"`
|
|
47
|
+
indicator (this is useful for mean reversion strategies) and the `quantiy`
|
|
48
|
+
to buy or sell. The `SignalEvents` are utilised by the `Portfolio object`
|
|
49
|
+
as advice for how to trade.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self,
|
|
53
|
+
strategy_id: int,
|
|
54
|
+
symbol: str,
|
|
55
|
+
datetime: datetime,
|
|
56
|
+
signal_type: str,
|
|
57
|
+
quantity: int | float = 100,
|
|
58
|
+
strength: int | float = 1.0
|
|
59
|
+
):
|
|
60
|
+
"""
|
|
61
|
+
Initialises the SignalEvent.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
strategy_id (int): The unique identifier for the strategy that
|
|
65
|
+
generated the signal.
|
|
66
|
+
|
|
67
|
+
symbol (str): The ticker symbol, e.g. 'GOOG'.
|
|
68
|
+
datetime (datetime): The timestamp at which the signal was generated.
|
|
69
|
+
signal_type (str): 'LONG' or 'SHORT'.
|
|
70
|
+
quantity (int | float): An optional integer (or float) representing the order size.
|
|
71
|
+
strength (int | float): An adjustment factor "suggestion" used to scale
|
|
72
|
+
quantity at the portfolio level. Useful for pairs strategies.
|
|
73
|
+
"""
|
|
74
|
+
self.type = 'SIGNAL'
|
|
75
|
+
self.strategy_id = strategy_id
|
|
76
|
+
self.symbol = symbol
|
|
77
|
+
self.datetime = datetime
|
|
78
|
+
self.signal_type = signal_type
|
|
79
|
+
self.quantity = quantity
|
|
80
|
+
self.strength = strength
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class OrderEvent(Event):
|
|
84
|
+
"""
|
|
85
|
+
When a Portfolio object receives `SignalEvents` it assesses them
|
|
86
|
+
in the wider context of the portfolio, in terms of risk and position sizing.
|
|
87
|
+
This ultimately leads to `OrderEvents` that will be sent to an `ExecutionHandler`.
|
|
88
|
+
|
|
89
|
+
The `OrderEvents` is slightly more complex than a `SignalEvents` since
|
|
90
|
+
it contains a quantity field in addition to the aforementioned properties
|
|
91
|
+
of SignalEvent. The quantity is determined by the Portfolio constraints.
|
|
92
|
+
In addition the OrderEvent has a `print_order()` method, used to output the
|
|
93
|
+
information to the console if necessary.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(self,
|
|
97
|
+
symbol: str,
|
|
98
|
+
order_type: str,
|
|
99
|
+
quantity: int | float,
|
|
100
|
+
direction: str
|
|
101
|
+
):
|
|
102
|
+
"""
|
|
103
|
+
Initialises the order type, setting whether it is
|
|
104
|
+
a Market order ('MKT') or Limit order ('LMT'), has
|
|
105
|
+
a quantity (integral or float) and its direction ('BUY' or 'SELL').
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
symbol (str): The instrument to trade.
|
|
109
|
+
order_type (str): 'MKT' or 'LMT' for Market or Limit.
|
|
110
|
+
quantity (int | float): Non-negative number for quantity.
|
|
111
|
+
direction (str): 'BUY' or 'SELL' for long or short.
|
|
112
|
+
"""
|
|
113
|
+
self.type = 'ORDER'
|
|
114
|
+
self.symbol = symbol
|
|
115
|
+
self.order_type = order_type
|
|
116
|
+
self.quantity = quantity
|
|
117
|
+
self.direction = direction
|
|
118
|
+
|
|
119
|
+
def print_order(self):
|
|
120
|
+
"""
|
|
121
|
+
Outputs the values within the Order.
|
|
122
|
+
"""
|
|
123
|
+
print(
|
|
124
|
+
"Order: Symbol=%s, Type=%s, Quantity=%s, Direction=%s" %
|
|
125
|
+
(self.symbol, self.order_type, self.quantity, self.direction)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class FillEvent(Event):
|
|
130
|
+
"""
|
|
131
|
+
When an `ExecutionHandler` receives an `OrderEvent` it must transact the order.
|
|
132
|
+
Once an order has been transacted it generates a `FillEvent`, which describes
|
|
133
|
+
the cost of purchase or sale as well as the transaction costs, such as fees
|
|
134
|
+
or slippage.
|
|
135
|
+
|
|
136
|
+
The `FillEvent` is the Event with the greatest complexity.
|
|
137
|
+
It contains a `timestamp` for when an order was filled, the `symbol`
|
|
138
|
+
of the order and the `exchange` it was executed on, the `quantity`
|
|
139
|
+
of shares transacted, the `actual price of the purchase` and the `commission
|
|
140
|
+
incurred`.
|
|
141
|
+
|
|
142
|
+
The commission is calculated using the Interactive Brokers commissions.
|
|
143
|
+
For US API orders this commission is `1.30 USD` minimum per order, with a flat
|
|
144
|
+
rate of either 0.013 USD or 0.08 USD per share depending upon whether
|
|
145
|
+
the trade size is below or above `500 units` of stock.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def __init__(self,
|
|
149
|
+
timeindex: datetime,
|
|
150
|
+
symbol: str,
|
|
151
|
+
exchange: str,
|
|
152
|
+
quantity: int | float,
|
|
153
|
+
direction: str,
|
|
154
|
+
fill_cost: int | float,
|
|
155
|
+
commission: float | None = None
|
|
156
|
+
):
|
|
157
|
+
"""
|
|
158
|
+
Initialises the FillEvent object. Sets the symbol, exchange,
|
|
159
|
+
quantity, direction, cost of fill and an optional
|
|
160
|
+
commission.
|
|
161
|
+
|
|
162
|
+
If commission is not provided, the Fill object will
|
|
163
|
+
calculate it based on the trade size and Interactive
|
|
164
|
+
Brokers fees.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
timeindex (datetime): The bar-resolution when the order was filled.
|
|
168
|
+
symbol (str): The instrument which was filled.
|
|
169
|
+
exchange (str): The exchange where the order was filled.
|
|
170
|
+
quantity (int | float): The filled quantity.
|
|
171
|
+
direction (str): The direction of fill ('BUY' or 'SELL')
|
|
172
|
+
fill_cost (int | float): The holdings value in dollars.
|
|
173
|
+
commission (float | None): An optional commission sent from IB.
|
|
174
|
+
"""
|
|
175
|
+
self.type = 'FILL'
|
|
176
|
+
self.timeindex = timeindex
|
|
177
|
+
self.symbol = symbol
|
|
178
|
+
self.exchange = exchange
|
|
179
|
+
self.quantity = quantity
|
|
180
|
+
self.direction = direction
|
|
181
|
+
self.fill_cost = fill_cost
|
|
182
|
+
# Calculate commission
|
|
183
|
+
if commission is None:
|
|
184
|
+
self.commission = self.calculate_ib_commission()
|
|
185
|
+
else:
|
|
186
|
+
self.commission = commission
|
|
187
|
+
|
|
188
|
+
def calculate_ib_commission(self):
|
|
189
|
+
"""
|
|
190
|
+
Calculates the fees of trading based on an Interactive
|
|
191
|
+
Brokers fee structure for API, in USD.
|
|
192
|
+
This does not include exchange or ECN fees.
|
|
193
|
+
Based on "US API Directed Orders":
|
|
194
|
+
https://www.interactivebrokers.com/en/index.php?f=commission&p=stocks2
|
|
195
|
+
"""
|
|
196
|
+
full_cost = 1.3
|
|
197
|
+
if self.quantity <= 500:
|
|
198
|
+
full_cost = max(1.3, 0.013 * self.quantity)
|
|
199
|
+
else:
|
|
200
|
+
full_cost = max(1.3, 0.008 * self.quantity)
|
|
201
|
+
return full_cost
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from queue import Queue
|
|
3
|
+
from abc import ABCMeta, abstractmethod
|
|
4
|
+
from bbstrader.btengine.event import FillEvent, OrderEvent
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"ExecutionHandler",
|
|
8
|
+
"SimulatedExecutionHandler"
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ExecutionHandler(metaclass=ABCMeta):
|
|
13
|
+
"""
|
|
14
|
+
The ExecutionHandler abstract class handles the interaction
|
|
15
|
+
between a set of order objects generated by a Portfolio and
|
|
16
|
+
the ultimate set of Fill objects that actually occur in the
|
|
17
|
+
market.
|
|
18
|
+
|
|
19
|
+
The handlers can be used to subclass simulated brokerages
|
|
20
|
+
or live brokerages, with identical interfaces. This allows
|
|
21
|
+
strategies to be backtested in a very similar manner to the
|
|
22
|
+
live trading engine.
|
|
23
|
+
|
|
24
|
+
The ExecutionHandler described here is exceedingly simple,
|
|
25
|
+
since it fills all orders at the current market price.
|
|
26
|
+
This is highly unrealistic, but serves as a good baseline for improvement.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def execute_order(self, event: OrderEvent):
|
|
31
|
+
"""
|
|
32
|
+
Takes an Order event and executes it, producing
|
|
33
|
+
a Fill event that gets placed onto the Events queue.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
event (OrderEvent): Contains an Event object with order information.
|
|
37
|
+
"""
|
|
38
|
+
raise NotImplementedError(
|
|
39
|
+
"Should implement execute_order()"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SimulatedExecutionHandler(ExecutionHandler):
|
|
44
|
+
"""
|
|
45
|
+
The simulated execution handler simply converts all order
|
|
46
|
+
objects into their equivalent fill objects automatically
|
|
47
|
+
without latency, slippage or fill-ratio issues.
|
|
48
|
+
|
|
49
|
+
This allows a straightforward "first go" test of any strategy,
|
|
50
|
+
before implementation with a more sophisticated execution
|
|
51
|
+
handler.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, events: Queue):
|
|
55
|
+
"""
|
|
56
|
+
Initialises the handler, setting the event queues
|
|
57
|
+
up internally.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
events (Queue): The Queue of Event objects.
|
|
61
|
+
"""
|
|
62
|
+
self.events = events
|
|
63
|
+
|
|
64
|
+
def execute_order(self, event: OrderEvent):
|
|
65
|
+
"""
|
|
66
|
+
Simply converts Order objects into Fill objects naively,
|
|
67
|
+
i.e. without any latency, slippage or fill ratio problems.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
event (OrderEvent): Contains an Event object with order information.
|
|
71
|
+
"""
|
|
72
|
+
if event.type == 'ORDER':
|
|
73
|
+
fill_event = FillEvent(
|
|
74
|
+
datetime.datetime.now(), event.symbol,
|
|
75
|
+
'ARCA', event.quantity, event.direction, None
|
|
76
|
+
)
|
|
77
|
+
self.events.put(fill_event)
|
|
78
|
+
|
|
79
|
+
# TODO # Use in live execution
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class MT5ExecutionHandler(ExecutionHandler):
|
|
83
|
+
...
|