bbstrader 0.1.91__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.91/bbstrader.egg-info → bbstrader-0.1.92}/PKG-INFO +2 -1
- {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/btengine/backtest.py +11 -4
- {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/btengine/data.py +10 -2
- {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/btengine/event.py +10 -4
- {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/btengine/execution.py +6 -6
- {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/btengine/portfolio.py +51 -25
- {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/btengine/strategy.py +101 -42
- {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/metatrader/rates.py +50 -2
- {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/metatrader/trade.py +7 -4
- bbstrader-0.1.92/bbstrader/models/optimization.py +170 -0
- bbstrader-0.1.92/bbstrader/models/portfolios.py +202 -0
- {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/trading/execution.py +11 -9
- {bbstrader-0.1.91 → bbstrader-0.1.92/bbstrader.egg-info}/PKG-INFO +2 -1
- {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader.egg-info/SOURCES.txt +1 -0
- {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader.egg-info/requires.txt +1 -0
- {bbstrader-0.1.91 → bbstrader-0.1.92}/requirements.txt +2 -1
- {bbstrader-0.1.91 → bbstrader-0.1.92}/setup.py +1 -1
- bbstrader-0.1.91/bbstrader/models/optimization.py +0 -0
- {bbstrader-0.1.91 → bbstrader-0.1.92}/LICENSE +0 -0
- {bbstrader-0.1.91 → bbstrader-0.1.92}/MANIFEST.in +0 -0
- {bbstrader-0.1.91 → bbstrader-0.1.92}/README.md +0 -0
- {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/__ini__.py +0 -0
- {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/btengine/__init__.py +0 -0
- {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/btengine/performance.py +0 -0
- {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/config.py +0 -0
- {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/metatrader/__init__.py +0 -0
- {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/metatrader/account.py +0 -0
- {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/metatrader/risk.py +0 -0
- {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/metatrader/utils.py +0 -0
- {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/models/__init__.py +0 -0
- {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/models/factors.py +0 -0
- {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/models/ml.py +0 -0
- {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/models/risk.py +0 -0
- {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/trading/__init__.py +0 -0
- {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/trading/scripts.py +0 -0
- {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/trading/strategies.py +0 -0
- {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader/tseries.py +0 -0
- {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader.egg-info/dependency_links.txt +0 -0
- {bbstrader-0.1.91 → bbstrader-0.1.92}/bbstrader.egg-info/top_level.txt +0 -0
- {bbstrader-0.1.91 → bbstrader-0.1.92}/setup.cfg +0 -0
|
@@ -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/
|
|
@@ -46,6 +46,7 @@ Requires-Dist: tqdm
|
|
|
46
46
|
Requires-Dist: scikit-learn
|
|
47
47
|
Requires-Dist: notify-py
|
|
48
48
|
Requires-Dist: python-telegram-bot
|
|
49
|
+
Requires-Dist: pyportfolioopt
|
|
49
50
|
Provides-Extra: mt5
|
|
50
51
|
Requires-Dist: MetaTrader5; extra == "mt5"
|
|
51
52
|
|
|
@@ -111,8 +111,9 @@ class BacktestEngine(Backtest):
|
|
|
111
111
|
their class types.
|
|
112
112
|
"""
|
|
113
113
|
print(
|
|
114
|
-
f"\
|
|
115
|
-
f"
|
|
114
|
+
f"\n[======= STARTING BACKTEST =======]\n"
|
|
115
|
+
f"START DATE: {self.start_date} \n"
|
|
116
|
+
f"INITIAL CAPITAL: {self.initial_capital}\n"
|
|
116
117
|
)
|
|
117
118
|
self.data_handler: DataHandler = self.dh_cls(
|
|
118
119
|
self.events, self.symbol_list, **self.kwargs
|
|
@@ -141,8 +142,9 @@ class BacktestEngine(Backtest):
|
|
|
141
142
|
self.data_handler.update_bars()
|
|
142
143
|
self.strategy.check_pending_orders()
|
|
143
144
|
else:
|
|
144
|
-
print("\n[======= BACKTEST
|
|
145
|
-
print(f"
|
|
145
|
+
print("\n[======= BACKTEST COMPLETED =======]")
|
|
146
|
+
print(f"END DATE: {self.data_handler.get_latest_bar_datetime()}")
|
|
147
|
+
print(f"TOTAL BARS: {i} ")
|
|
146
148
|
break
|
|
147
149
|
|
|
148
150
|
# Handle the events
|
|
@@ -168,6 +170,11 @@ class BacktestEngine(Backtest):
|
|
|
168
170
|
elif event.type == 'FILL':
|
|
169
171
|
self.fills += 1
|
|
170
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
|
+
)
|
|
171
178
|
|
|
172
179
|
time.sleep(self.heartbeat)
|
|
173
180
|
|
|
@@ -272,7 +272,11 @@ class BaseCSVDataHandler(DataHandler):
|
|
|
272
272
|
print("Symbol not available in the historical data set.")
|
|
273
273
|
raise
|
|
274
274
|
else:
|
|
275
|
-
|
|
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
|
|
276
280
|
|
|
277
281
|
def get_latest_bars_values(self, symbol: str, val_type: str, N=1) -> np.ndarray:
|
|
278
282
|
"""
|
|
@@ -285,7 +289,11 @@ class BaseCSVDataHandler(DataHandler):
|
|
|
285
289
|
print("That symbol is not available in the historical data set.")
|
|
286
290
|
raise
|
|
287
291
|
else:
|
|
288
|
-
|
|
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
|
|
289
297
|
|
|
290
298
|
def update_bars(self):
|
|
291
299
|
"""
|
|
@@ -102,14 +102,15 @@ class OrderEvent(Event):
|
|
|
102
102
|
|
|
103
103
|
def __init__(self,
|
|
104
104
|
symbol: str,
|
|
105
|
-
order_type: Literal['MKT', 'LMT'],
|
|
105
|
+
order_type: Literal['MKT', 'LMT', 'STP', 'STPLMT'],
|
|
106
106
|
quantity: int | float,
|
|
107
107
|
direction: Literal['BUY', 'SELL'],
|
|
108
|
-
price: int | float = None
|
|
108
|
+
price: int | float = None,
|
|
109
|
+
signal: str = None
|
|
109
110
|
):
|
|
110
111
|
"""
|
|
111
112
|
Initialises the order type, setting whether it is
|
|
112
|
-
a Market order ('MKT') or Limit order ('LMT'),
|
|
113
|
+
a Market order ('MKT') or Limit order ('LMT'), or Stop order ('STP').
|
|
113
114
|
a quantity (integral or float) and its direction ('BUY' or 'SELL').
|
|
114
115
|
|
|
115
116
|
Args:
|
|
@@ -118,6 +119,7 @@ class OrderEvent(Event):
|
|
|
118
119
|
quantity (int | float): Non-negative number for quantity.
|
|
119
120
|
direction (str): 'BUY' or 'SELL' for long or short.
|
|
120
121
|
price (int | float): The price at which to order.
|
|
122
|
+
signal (str): The signal that generated the order.
|
|
121
123
|
"""
|
|
122
124
|
self.type = 'ORDER'
|
|
123
125
|
self.symbol = symbol
|
|
@@ -125,6 +127,7 @@ class OrderEvent(Event):
|
|
|
125
127
|
self.quantity = quantity
|
|
126
128
|
self.direction = direction
|
|
127
129
|
self.price = price
|
|
130
|
+
self.signal = signal
|
|
128
131
|
|
|
129
132
|
def print_order(self):
|
|
130
133
|
"""
|
|
@@ -162,7 +165,8 @@ class FillEvent(Event):
|
|
|
162
165
|
quantity: int | float,
|
|
163
166
|
direction: Literal['BUY', 'SELL'],
|
|
164
167
|
fill_cost: int | float | None,
|
|
165
|
-
commission: float | None = None
|
|
168
|
+
commission: float | None = None,
|
|
169
|
+
order: str = None
|
|
166
170
|
):
|
|
167
171
|
"""
|
|
168
172
|
Initialises the FillEvent object. Sets the symbol, exchange,
|
|
@@ -181,6 +185,7 @@ class FillEvent(Event):
|
|
|
181
185
|
direction (str): The direction of fill `('LONG', 'SHORT', 'EXIT')`
|
|
182
186
|
fill_cost (int | float): Price of the shares when filled.
|
|
183
187
|
commission (float | None): An optional commission sent from IB.
|
|
188
|
+
order (str): The order that this fill is related
|
|
184
189
|
"""
|
|
185
190
|
self.type = 'FILL'
|
|
186
191
|
self.timeindex = timeindex
|
|
@@ -194,6 +199,7 @@ class FillEvent(Event):
|
|
|
194
199
|
self.commission = self.calculate_ib_commission()
|
|
195
200
|
else:
|
|
196
201
|
self.commission = commission
|
|
202
|
+
self.order = order
|
|
197
203
|
|
|
198
204
|
def calculate_ib_commission(self):
|
|
199
205
|
"""
|
|
@@ -4,7 +4,6 @@ from abc import ABCMeta, abstractmethod
|
|
|
4
4
|
from bbstrader.btengine.event import FillEvent, OrderEvent
|
|
5
5
|
from bbstrader.btengine.data import DataHandler
|
|
6
6
|
from bbstrader.metatrader.account import Account
|
|
7
|
-
from bbstrader.config import config_logger
|
|
8
7
|
|
|
9
8
|
__all__ = [
|
|
10
9
|
"ExecutionHandler",
|
|
@@ -26,7 +25,8 @@ class ExecutionHandler(metaclass=ABCMeta):
|
|
|
26
25
|
|
|
27
26
|
The ExecutionHandler described here is exceedingly simple,
|
|
28
27
|
since it fills all orders at the current market price.
|
|
29
|
-
This is highly unrealistic,
|
|
28
|
+
This is highly unrealistic, for other markets thant ``CFDs``
|
|
29
|
+
but serves as a good baseline for improvement.
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
32
|
@abstractmethod
|
|
@@ -64,7 +64,7 @@ class SimExecutionHandler(ExecutionHandler):
|
|
|
64
64
|
"""
|
|
65
65
|
self.events = events
|
|
66
66
|
self.bardata = data
|
|
67
|
-
self.logger = kwargs.get("logger"
|
|
67
|
+
self.logger = kwargs.get("logger")
|
|
68
68
|
|
|
69
69
|
def execute_order(self, event: OrderEvent):
|
|
70
70
|
"""
|
|
@@ -78,7 +78,7 @@ class SimExecutionHandler(ExecutionHandler):
|
|
|
78
78
|
dtime = self.bardata.get_latest_bar_datetime(event.symbol)
|
|
79
79
|
fill_event = FillEvent(
|
|
80
80
|
dtime, event.symbol,
|
|
81
|
-
'ARCA', event.quantity, event.direction,
|
|
81
|
+
'ARCA', event.quantity, event.direction, order=event.signal
|
|
82
82
|
)
|
|
83
83
|
self.events.put(fill_event)
|
|
84
84
|
self.logger.info(
|
|
@@ -121,7 +121,7 @@ class MT5ExecutionHandler(ExecutionHandler):
|
|
|
121
121
|
"""
|
|
122
122
|
self.events = events
|
|
123
123
|
self.bardata = data
|
|
124
|
-
self.logger = kwargs.get("logger"
|
|
124
|
+
self.logger = kwargs.get("logger")
|
|
125
125
|
self.__account = Account()
|
|
126
126
|
|
|
127
127
|
def _calculate_lot(self, symbol, quantity, price):
|
|
@@ -232,7 +232,7 @@ class MT5ExecutionHandler(ExecutionHandler):
|
|
|
232
232
|
fill_event = FillEvent(
|
|
233
233
|
timeindex=dtime, symbol=symbol,
|
|
234
234
|
exchange='MT5', quantity=quantity, direction=direction,
|
|
235
|
-
fill_cost=None, commission=fees
|
|
235
|
+
fill_cost=None, commission=fees, order=event.signal
|
|
236
236
|
)
|
|
237
237
|
self.events.put(fill_event)
|
|
238
238
|
self.logger.info(
|
|
@@ -31,8 +31,6 @@ class Portfolio(object):
|
|
|
31
31
|
"""
|
|
32
32
|
This describes a `Portfolio()` object that keeps track of the positions
|
|
33
33
|
within a portfolio and generates orders of a fixed quantity of stock based on signals.
|
|
34
|
-
More sophisticated portfolio objects could include risk management and position
|
|
35
|
-
sizing tools (such as the `Kelly Criterion`).
|
|
36
34
|
|
|
37
35
|
The portfolio order management system is possibly the most complex component of
|
|
38
36
|
an event driven backtester. Its role is to keep track of all current market positions
|
|
@@ -57,10 +55,9 @@ class Portfolio(object):
|
|
|
57
55
|
value (defaulting to `100,000 USD`) and others parameter based on the `Strategy` requirement.
|
|
58
56
|
|
|
59
57
|
The `Portfolio` is designed to handle position sizing and current holdings,
|
|
60
|
-
but will carry out trading orders
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
management system (OMS) functions in an eventdriven fashion.
|
|
58
|
+
but will carry out trading orders by simply them to the brokerage with a predetermined
|
|
59
|
+
fixed quantity size, if the portfolio has enough cash to place the order.
|
|
60
|
+
|
|
64
61
|
|
|
65
62
|
The portfolio contains the `all_positions` and `current_positions` members.
|
|
66
63
|
The former stores a list of all previous positions recorded at the timestamp of a market data event.
|
|
@@ -108,7 +105,7 @@ class Portfolio(object):
|
|
|
108
105
|
self.initial_capital = initial_capital
|
|
109
106
|
|
|
110
107
|
self.timeframe = kwargs.get("time_frame", "D1")
|
|
111
|
-
self.trading_hours = kwargs.get("session_duration",
|
|
108
|
+
self.trading_hours = kwargs.get("session_duration", 23)
|
|
112
109
|
self.benchmark = kwargs.get('benchmark', 'SPY')
|
|
113
110
|
self.output_dir = kwargs.get('output_dir', None)
|
|
114
111
|
self.strategy_name = kwargs.get('strategy_name', '')
|
|
@@ -174,6 +171,23 @@ class Portfolio(object):
|
|
|
174
171
|
d['Commission'] = 0.0
|
|
175
172
|
d['Total'] = self.initial_capital
|
|
176
173
|
return d
|
|
174
|
+
|
|
175
|
+
def _get_price(self, symbol: str) -> float:
|
|
176
|
+
try:
|
|
177
|
+
price = self.bars.get_latest_bar_value(
|
|
178
|
+
symbol, "adj_close"
|
|
179
|
+
)
|
|
180
|
+
return price
|
|
181
|
+
except AttributeError:
|
|
182
|
+
try:
|
|
183
|
+
price = self.bars.get_latest_bar_value(
|
|
184
|
+
symbol, "close"
|
|
185
|
+
)
|
|
186
|
+
return price
|
|
187
|
+
except AttributeError:
|
|
188
|
+
raise AttributeError(
|
|
189
|
+
f"Bars object must have 'adj_close' or 'close' prices"
|
|
190
|
+
)
|
|
177
191
|
|
|
178
192
|
def update_timeindex(self, event: MarketEvent):
|
|
179
193
|
"""
|
|
@@ -203,8 +217,8 @@ class Portfolio(object):
|
|
|
203
217
|
dh['Total'] = self.current_holdings['Cash']
|
|
204
218
|
for s in self.symbol_list:
|
|
205
219
|
# Approximation to the real value
|
|
206
|
-
|
|
207
|
-
|
|
220
|
+
price = self._get_price(s)
|
|
221
|
+
market_value = self.current_positions[s] * price
|
|
208
222
|
dh[s] = market_value
|
|
209
223
|
dh['Total'] += market_value
|
|
210
224
|
|
|
@@ -245,9 +259,7 @@ class Portfolio(object):
|
|
|
245
259
|
fill_dir = -1
|
|
246
260
|
|
|
247
261
|
# Update holdings list with new quantities
|
|
248
|
-
price = self.
|
|
249
|
-
fill.symbol, "adj_close"
|
|
250
|
-
)
|
|
262
|
+
price = self._get_price(fill.symbol)
|
|
251
263
|
cost = fill_dir * price * fill.quantity
|
|
252
264
|
self.current_holdings[fill.symbol] += cost
|
|
253
265
|
self.current_holdings['Commission'] += fill.commission
|
|
@@ -263,14 +275,16 @@ class Portfolio(object):
|
|
|
263
275
|
self.update_positions_from_fill(event)
|
|
264
276
|
self.update_holdings_from_fill(event)
|
|
265
277
|
|
|
266
|
-
def
|
|
278
|
+
def generate_order(self, signal: SignalEvent):
|
|
267
279
|
"""
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
position sizing considerations.
|
|
280
|
+
Check if the portfolio has enough cash to place an order
|
|
281
|
+
and generate an OrderEvent, else return None.
|
|
271
282
|
|
|
272
283
|
Args:
|
|
273
284
|
signal (SignalEvent): The tuple containing Signal information.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
OrderEvent: The OrderEvent to be executed.
|
|
274
288
|
"""
|
|
275
289
|
order = None
|
|
276
290
|
|
|
@@ -278,21 +292,33 @@ class Portfolio(object):
|
|
|
278
292
|
direction = signal.signal_type
|
|
279
293
|
quantity = signal.quantity
|
|
280
294
|
strength = signal.strength
|
|
281
|
-
price = signal.price
|
|
295
|
+
price = signal.price or self._get_price(symbol)
|
|
282
296
|
cur_quantity = self.current_positions[symbol]
|
|
297
|
+
cash = self.current_holdings['Cash']
|
|
283
298
|
|
|
284
|
-
|
|
299
|
+
if direction in ['LONG', 'SHORT', 'EXIT']:
|
|
300
|
+
order_type = 'MKT'
|
|
301
|
+
else:
|
|
302
|
+
order_type = direction
|
|
285
303
|
mkt_quantity = round(quantity * strength)
|
|
304
|
+
cost = mkt_quantity * price
|
|
305
|
+
|
|
306
|
+
if cash >= cost:
|
|
307
|
+
new_quantity = mkt_quantity
|
|
308
|
+
elif cash < cost and cash > 0:
|
|
309
|
+
new_quantity = round(cash // price)
|
|
310
|
+
else:
|
|
311
|
+
new_quantity = 0
|
|
286
312
|
|
|
287
|
-
if
|
|
288
|
-
order = OrderEvent(symbol, order_type,
|
|
289
|
-
if
|
|
290
|
-
order = OrderEvent(symbol, order_type,
|
|
313
|
+
if new_quantity > 0 and direction == 'LONG':
|
|
314
|
+
order = OrderEvent(symbol, order_type, new_quantity, 'BUY', price, direction)
|
|
315
|
+
if new_quantity > 0 and direction == 'SHORT':
|
|
316
|
+
order = OrderEvent(symbol, order_type, new_quantity, 'SELL', price, direction)
|
|
291
317
|
|
|
292
318
|
if direction == 'EXIT' and cur_quantity > 0:
|
|
293
|
-
order = OrderEvent(symbol, order_type, abs(cur_quantity), 'SELL', price)
|
|
319
|
+
order = OrderEvent(symbol, order_type, abs(cur_quantity), 'SELL', price, direction)
|
|
294
320
|
if direction == 'EXIT' and cur_quantity < 0:
|
|
295
|
-
order = OrderEvent(symbol, order_type, abs(cur_quantity), 'BUY', price)
|
|
321
|
+
order = OrderEvent(symbol, order_type, abs(cur_quantity), 'BUY', price, direction)
|
|
296
322
|
|
|
297
323
|
return order
|
|
298
324
|
|
|
@@ -302,7 +328,7 @@ class Portfolio(object):
|
|
|
302
328
|
based on the portfolio logic.
|
|
303
329
|
"""
|
|
304
330
|
if event.type == 'SIGNAL':
|
|
305
|
-
order_event = self.
|
|
331
|
+
order_event = self.generate_order(event)
|
|
306
332
|
self.events.put(order_event)
|
|
307
333
|
|
|
308
334
|
def create_equity_curve_dataframe(self):
|
|
@@ -7,6 +7,7 @@ from queue import Queue
|
|
|
7
7
|
from datetime import datetime
|
|
8
8
|
from bbstrader.config import config_logger
|
|
9
9
|
from bbstrader.btengine.event import SignalEvent
|
|
10
|
+
from bbstrader.btengine.event import FillEvent
|
|
10
11
|
from bbstrader.btengine.data import DataHandler
|
|
11
12
|
from bbstrader.metatrader.account import Account
|
|
12
13
|
from bbstrader.metatrader.rates import Rates
|
|
@@ -38,6 +39,8 @@ class Strategy(metaclass=ABCMeta):
|
|
|
38
39
|
|
|
39
40
|
The strategy hierarchy is relatively simple as it consists of an abstract
|
|
40
41
|
base class with a single pure virtual method for generating `SignalEvent` objects.
|
|
42
|
+
Other methods are provided to check for pending orders, update trades from fills,
|
|
43
|
+
and get updates from the portfolio.
|
|
41
44
|
"""
|
|
42
45
|
|
|
43
46
|
@abstractmethod
|
|
@@ -45,8 +48,9 @@ class Strategy(metaclass=ABCMeta):
|
|
|
45
48
|
raise NotImplementedError(
|
|
46
49
|
"Should implement calculate_signals()"
|
|
47
50
|
)
|
|
48
|
-
|
|
49
|
-
def
|
|
51
|
+
def check_pending_orders(self, *args, **kwargs): ...
|
|
52
|
+
def get_update_from_portfolio(self, *args, **kwargs): ...
|
|
53
|
+
def update_trades_from_fill(self, *args, **kwargs): ...
|
|
50
54
|
|
|
51
55
|
|
|
52
56
|
class MT5Strategy(Strategy):
|
|
@@ -74,17 +78,71 @@ class MT5Strategy(Strategy):
|
|
|
74
78
|
self.symbols = symbol_list
|
|
75
79
|
self.mode = mode
|
|
76
80
|
self.volume = kwargs.get("volume")
|
|
77
|
-
self.
|
|
78
|
-
|
|
81
|
+
self.max_trades = kwargs.get("max_trades",
|
|
82
|
+
{symbol: 1 for symbol in self.symbols})
|
|
83
|
+
self.logger = kwargs.get("logger")
|
|
84
|
+
self._initialize_portfolio()
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def orders(self) -> Dict[str, Dict[str, List[SignalEvent]]]:
|
|
88
|
+
return self._orders
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def trades(self) -> Dict[str, Dict[str, int]]:
|
|
92
|
+
return self._trades
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def positions(self) -> Dict[str, Dict[str, int|float]]:
|
|
96
|
+
return self._positions
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def holdings(self) -> Dict[str, float]:
|
|
100
|
+
return self._holdings
|
|
79
101
|
|
|
80
|
-
def
|
|
81
|
-
self.positions: Dict[str, Dict[str, int]] = {}
|
|
82
|
-
self.orders: Dict[str, Dict[str, List[SignalEvent]]] = {}
|
|
102
|
+
def _initialize_portfolio(self):
|
|
83
103
|
positions = ['LONG', 'SHORT']
|
|
84
104
|
orders = ['BLMT', 'BSTP', 'BSTPLMT', 'SLMT', 'SSTP', 'SSTPLMT']
|
|
105
|
+
self._positions: Dict[str, Dict[str, int]] = {}
|
|
106
|
+
self._trades: Dict[str, Dict[str, int]] = {}
|
|
107
|
+
self._orders: Dict[str, Dict[str, List[SignalEvent]]] = {}
|
|
85
108
|
for symbol in self.symbols:
|
|
86
|
-
self.
|
|
87
|
-
self.
|
|
109
|
+
self._positions[symbol] = {}
|
|
110
|
+
self._orders[symbol] = {}
|
|
111
|
+
self._trades[symbol] = {}
|
|
112
|
+
for position in positions:
|
|
113
|
+
self._trades[symbol][position] = 0
|
|
114
|
+
self._positions[symbol][position] = 0
|
|
115
|
+
for order in orders:
|
|
116
|
+
self._orders[symbol][order] = []
|
|
117
|
+
self._holdings = {s: 0.0 for s in self.symbols}
|
|
118
|
+
|
|
119
|
+
def get_update_from_portfolio(self, positions, holdings):
|
|
120
|
+
"""
|
|
121
|
+
Update the positions and holdings for the strategy from the portfolio.
|
|
122
|
+
|
|
123
|
+
Positions are the number of shares of a security that are owned in long or short.
|
|
124
|
+
Holdings are the value (postions * price) of the security that are owned in long or short.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
positions : The positions for the symbols in the strategy.
|
|
128
|
+
holdings : The holdings for the symbols in the strategy.
|
|
129
|
+
"""
|
|
130
|
+
for symbol in self.symbols:
|
|
131
|
+
if symbol in positions:
|
|
132
|
+
if positions[symbol] > 0:
|
|
133
|
+
self._positions[symbol]['LONG'] = positions[symbol]
|
|
134
|
+
elif positions[symbol] < 0:
|
|
135
|
+
self._positions[symbol]['SHORT'] = positions[symbol]
|
|
136
|
+
if symbol in holdings:
|
|
137
|
+
self._holdings[symbol] = holdings[symbol]
|
|
138
|
+
|
|
139
|
+
def update_trades_from_fill(self, event: FillEvent):
|
|
140
|
+
"""
|
|
141
|
+
This method updates the trades for the strategy based on the fill event.
|
|
142
|
+
It is used to keep track of the number of trades executed for each order.
|
|
143
|
+
"""
|
|
144
|
+
if event.type == 'FILL':
|
|
145
|
+
self._trades[event.symbol][event.order] += 1
|
|
88
146
|
|
|
89
147
|
def calculate_signals(self, *args, **kwargs
|
|
90
148
|
) -> Dict[str, Union[str, dict, None]] | None:
|
|
@@ -115,7 +173,7 @@ class MT5Strategy(Strategy):
|
|
|
115
173
|
"""
|
|
116
174
|
pass
|
|
117
175
|
|
|
118
|
-
def get_quantity(self, symbol) -> int:
|
|
176
|
+
def get_quantity(self, symbol, volume=None) -> int:
|
|
119
177
|
"""
|
|
120
178
|
Calculate the quantity to buy or sell for a given symbol based on the dollar value provided.
|
|
121
179
|
The quantity calculated can be used to evalute a strategy's performance for each symbol
|
|
@@ -127,11 +185,15 @@ class MT5Strategy(Strategy):
|
|
|
127
185
|
Returns:
|
|
128
186
|
qty : The quantity to buy or sell for the symbol.
|
|
129
187
|
"""
|
|
130
|
-
if self.volume is None:
|
|
188
|
+
if self.volume is None and volume is None:
|
|
131
189
|
raise ValueError("Volume must be provided for the method.")
|
|
132
190
|
current_price = self.data.get_latest_bar_value(symbol, 'close')
|
|
133
|
-
|
|
134
|
-
|
|
191
|
+
try:
|
|
192
|
+
qty = math.ceil(self.volume or volume / current_price)
|
|
193
|
+
qty = max(qty, 1) / self.max_trades[symbol]
|
|
194
|
+
return max(math.ceil(qty), 1)
|
|
195
|
+
except Exception:
|
|
196
|
+
return 1
|
|
135
197
|
|
|
136
198
|
def get_quantities(self, quantities: Union[None, dict, int]) -> dict:
|
|
137
199
|
"""
|
|
@@ -157,7 +219,7 @@ class MT5Strategy(Strategy):
|
|
|
157
219
|
self.logger.info(
|
|
158
220
|
f"{signal} ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={quantity}, PRICE @{price}", custom_time=dtime)
|
|
159
221
|
|
|
160
|
-
def
|
|
222
|
+
def buy_mkt(self, id: int, symbol: str, price: float, quantity: int,
|
|
161
223
|
strength: float=1.0, dtime: datetime | pd.Timestamp=None):
|
|
162
224
|
"""
|
|
163
225
|
Open a long position
|
|
@@ -165,25 +227,22 @@ class MT5Strategy(Strategy):
|
|
|
165
227
|
See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
|
|
166
228
|
"""
|
|
167
229
|
self._send_order(id, symbol, 'LONG', strength, price, quantity, dtime)
|
|
168
|
-
self.positions[symbol]['LONG'] += quantity
|
|
169
230
|
|
|
170
|
-
def
|
|
231
|
+
def sell_mkt(self, id, symbol, price, quantity, strength=1.0, dtime=None):
|
|
171
232
|
"""
|
|
172
233
|
Open a short position
|
|
173
234
|
|
|
174
235
|
See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
|
|
175
236
|
"""
|
|
176
237
|
self._send_order(id, symbol, 'SHORT', strength, price, quantity, dtime)
|
|
177
|
-
self.positions[symbol]['SHORT'] += quantity
|
|
178
238
|
|
|
179
|
-
def
|
|
239
|
+
def close_positions(self, id, symbol, price, quantity, strength=1.0, dtime=None):
|
|
180
240
|
"""
|
|
181
|
-
Close a position
|
|
241
|
+
Close a position or exit all positions
|
|
182
242
|
|
|
183
243
|
See `bbstrader.btengine.event.SignalEvent` for more details on arguments.
|
|
184
244
|
"""
|
|
185
245
|
self._send_order(id, symbol, 'EXIT', strength, price, quantity, dtime)
|
|
186
|
-
self.positions[symbol]['LONG'] -= quantity
|
|
187
246
|
|
|
188
247
|
def buy_stop(self, id, symbol, price, quantity, strength=1.0, dtime=None):
|
|
189
248
|
"""
|
|
@@ -197,7 +256,7 @@ class MT5Strategy(Strategy):
|
|
|
197
256
|
"The buy_stop price must be greater than the current price.")
|
|
198
257
|
order = SignalEvent(id, symbol, dtime, 'LONG',
|
|
199
258
|
quantity=quantity, strength=strength, price=price)
|
|
200
|
-
self.
|
|
259
|
+
self._orders[symbol]['BSTP'].append(order)
|
|
201
260
|
|
|
202
261
|
def sell_stop(self, id, symbol, price, quantity, strength=1.0, dtime=None):
|
|
203
262
|
"""
|
|
@@ -211,7 +270,7 @@ class MT5Strategy(Strategy):
|
|
|
211
270
|
"The sell_stop price must be less than the current price.")
|
|
212
271
|
order = SignalEvent(id, symbol, dtime, 'SHORT',
|
|
213
272
|
quantity=quantity, strength=strength, price=price)
|
|
214
|
-
self.
|
|
273
|
+
self._orders[symbol]['SSTP'].append(order)
|
|
215
274
|
|
|
216
275
|
def buy_limit(self, id, symbol, price, quantity, strength=1.0, dtime=None):
|
|
217
276
|
"""
|
|
@@ -225,7 +284,7 @@ class MT5Strategy(Strategy):
|
|
|
225
284
|
"The buy_limit price must be less than the current price.")
|
|
226
285
|
order = SignalEvent(id, symbol, dtime, 'LONG',
|
|
227
286
|
quantity=quantity, strength=strength, price=price)
|
|
228
|
-
self.
|
|
287
|
+
self._orders[symbol]['BLMT'].append(order)
|
|
229
288
|
|
|
230
289
|
def sell_limit(self, id, symbol, price, quantity, strength=1.0, dtime=None):
|
|
231
290
|
"""
|
|
@@ -239,7 +298,7 @@ class MT5Strategy(Strategy):
|
|
|
239
298
|
"The sell_limit price must be greater than the current price.")
|
|
240
299
|
order = SignalEvent(id, symbol, dtime, 'SHORT',
|
|
241
300
|
quantity=quantity, strength=strength, price=price)
|
|
242
|
-
self.
|
|
301
|
+
self._orders[symbol]['SLMT'].append(order)
|
|
243
302
|
|
|
244
303
|
def buy_stop_limit(self, id: int, symbol: str, price: float, stoplimit: float,
|
|
245
304
|
quantity: int, strength: float=1.0, dtime: datetime | pd.Timestamp = None):
|
|
@@ -257,7 +316,7 @@ class MT5Strategy(Strategy):
|
|
|
257
316
|
f"The stop-limit price {stoplimit} must be greater than the price {price}.")
|
|
258
317
|
order = SignalEvent(id, symbol, dtime, 'LONG',
|
|
259
318
|
quantity=quantity, strength=strength, price=price, stoplimit=stoplimit)
|
|
260
|
-
self.
|
|
319
|
+
self._orders[symbol]['BSTPLMT'].append(order)
|
|
261
320
|
|
|
262
321
|
def sell_stop_limit(self, id, symbol, price, stoplimit, quantity, strength=1.0, dtime=None):
|
|
263
322
|
"""
|
|
@@ -274,7 +333,7 @@ class MT5Strategy(Strategy):
|
|
|
274
333
|
f"The stop-limit price {stoplimit} must be less than the price {price}.")
|
|
275
334
|
order = SignalEvent(id, symbol, dtime, 'SHORT',
|
|
276
335
|
quantity=quantity, strength=strength, price=price, stoplimit=stoplimit)
|
|
277
|
-
self.
|
|
336
|
+
self._orders[symbol]['SSTPLMT'].append(order)
|
|
278
337
|
|
|
279
338
|
def check_pending_orders(self):
|
|
280
339
|
"""
|
|
@@ -282,54 +341,54 @@ class MT5Strategy(Strategy):
|
|
|
282
341
|
"""
|
|
283
342
|
for symbol in self.symbols:
|
|
284
343
|
dtime = self.data.get_latest_bar_datetime(symbol)
|
|
285
|
-
for order in self.
|
|
344
|
+
for order in self._orders[symbol]['BLMT'].copy():
|
|
286
345
|
if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
|
|
287
|
-
self.
|
|
346
|
+
self.buy_mkt(order.strategy_id, symbol,
|
|
288
347
|
order.price, order.quantity, dtime)
|
|
289
348
|
self.logger.info(
|
|
290
349
|
f"BUY LIMIT ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
|
|
291
350
|
f"PRICE @ {order.price}", custom_time=dtime)
|
|
292
|
-
self.
|
|
293
|
-
for order in self.
|
|
351
|
+
self._orders[symbol]['BLMT'].remove(order)
|
|
352
|
+
for order in self._orders[symbol]['SLMT'].copy():
|
|
294
353
|
if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
|
|
295
|
-
self.
|
|
354
|
+
self.sell_mkt(order.strategy_id, symbol,
|
|
296
355
|
order.price, order.quantity, dtime)
|
|
297
356
|
self.logger.info(
|
|
298
357
|
f"SELL LIMIT ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
|
|
299
358
|
f"PRICE @ {order.price}", custom_time=dtime)
|
|
300
|
-
self.
|
|
301
|
-
for order in self.
|
|
359
|
+
self._orders[symbol]['SLMT'].remove(order)
|
|
360
|
+
for order in self._orders[symbol]['BSTP'].copy():
|
|
302
361
|
if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
|
|
303
|
-
self.
|
|
362
|
+
self.buy_mkt(order.strategy_id, symbol,
|
|
304
363
|
order.price, order.quantity, dtime)
|
|
305
364
|
self.logger.info(
|
|
306
365
|
f"BUY STOP ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
|
|
307
366
|
f"PRICE @ {order.price}", custom_time=dtime)
|
|
308
|
-
self.
|
|
309
|
-
for order in self.
|
|
367
|
+
self._orders[symbol]['BSTP'].remove(order)
|
|
368
|
+
for order in self._orders[symbol]['SSTP'].copy():
|
|
310
369
|
if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
|
|
311
|
-
self.
|
|
370
|
+
self.sell_mkt(order.strategy_id, symbol,
|
|
312
371
|
order.price, order.quantity, dtime)
|
|
313
372
|
self.logger.info(
|
|
314
373
|
f"SELL STOP ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
|
|
315
374
|
f"PRICE @ {order.price}", custom_time=dtime)
|
|
316
|
-
self.
|
|
317
|
-
for order in self.
|
|
375
|
+
self._orders[symbol]['SSTP'].remove(order)
|
|
376
|
+
for order in self._orders[symbol]['BSTPLMT'].copy():
|
|
318
377
|
if self.data.get_latest_bar_value(symbol, 'close') >= order.price:
|
|
319
378
|
self.buy_limit(order.strategy_id, symbol,
|
|
320
379
|
order.stoplimit, order.quantity, dtime)
|
|
321
380
|
self.logger.info(
|
|
322
381
|
f"BUY STOP LIMIT ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
|
|
323
382
|
f"PRICE @ {order.price}", custom_time=dtime)
|
|
324
|
-
self.
|
|
325
|
-
for order in self.
|
|
383
|
+
self._orders[symbol]['BSTPLMT'].remove(order)
|
|
384
|
+
for order in self._orders[symbol]['SSTPLMT'].copy():
|
|
326
385
|
if self.data.get_latest_bar_value(symbol, 'close') <= order.price:
|
|
327
386
|
self.sell_limit(order.strategy_id, symbol,
|
|
328
387
|
order.stoplimit, order.quantity, dtime)
|
|
329
388
|
self.logger.info(
|
|
330
389
|
f"SELL STOP LIMIT ORDER EXECUTED: SYMBOL={symbol}, QUANTITY={order.quantity}, "
|
|
331
390
|
f"PRICE @ {order.price}", custom_time=dtime)
|
|
332
|
-
self.
|
|
391
|
+
self._orders[symbol]['SSTPLMT'].remove(order)
|
|
333
392
|
|
|
334
393
|
def get_asset_values(self,
|
|
335
394
|
symbol_list: List[str],
|