bbstrader 0.1.91__py3-none-any.whl → 0.1.93__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.

@@ -85,6 +85,8 @@ class BacktestEngine(Backtest):
85
85
  strategy (Strategy): Generates signals based on market data.
86
86
  kwargs : Additional parameters based on the `ExecutionHandler`,
87
87
  the `DataHandler`, the `Strategy` used and the `Portfolio`.
88
+ - show_equity (bool): Show the equity curve of the portfolio.
89
+ - stats_file (str): File to save the summary stats.
88
90
  """
89
91
  self.symbol_list = symbol_list
90
92
  self.initial_capital = initial_capital
@@ -104,6 +106,7 @@ class BacktestEngine(Backtest):
104
106
 
105
107
  self._generate_trading_instances()
106
108
  self.show_equity = kwargs.get("show_equity", False)
109
+ self.stats_file = kwargs.get("stats_file", None)
107
110
 
108
111
  def _generate_trading_instances(self):
109
112
  """
@@ -111,8 +114,9 @@ class BacktestEngine(Backtest):
111
114
  their class types.
112
115
  """
113
116
  print(
114
- f"\nStarting Backtest on {self.symbol_list} "
115
- f"with ${self.initial_capital} Initial Capital\n"
117
+ f"\n[======= STARTING BACKTEST =======]\n"
118
+ f"START DATE: {self.start_date} \n"
119
+ f"INITIAL CAPITAL: {self.initial_capital}\n"
116
120
  )
117
121
  self.data_handler: DataHandler = self.dh_cls(
118
122
  self.events, self.symbol_list, **self.kwargs
@@ -136,13 +140,21 @@ class BacktestEngine(Backtest):
136
140
  i = 0
137
141
  while True:
138
142
  i += 1
139
- # Update the market bars
143
+ value = self.portfolio.all_holdings[-1]['Total']
140
144
  if self.data_handler.continue_backtest == True:
145
+ # Update the market bars
141
146
  self.data_handler.update_bars()
142
147
  self.strategy.check_pending_orders()
148
+ self.strategy.get_update_from_portfolio(
149
+ self.portfolio.current_positions,
150
+ self.portfolio.current_holdings
151
+ )
152
+ self.strategy.cash = value
143
153
  else:
144
- print("\n[======= BACKTEST COMPLETE =======]\n")
145
- print(f"Total bars: {i} ")
154
+ print("\n[======= BACKTEST COMPLETED =======]")
155
+ print(f"END DATE: {self.data_handler.get_latest_bar_datetime(self.symbol_list[0])}")
156
+ print(f"TOTAL BARS: {i} ")
157
+ print(f"PORFOLIO VALUE: {round(value, 2)}")
146
158
  break
147
159
 
148
160
  # Handle the events
@@ -168,6 +180,7 @@ class BacktestEngine(Backtest):
168
180
  elif event.type == 'FILL':
169
181
  self.fills += 1
170
182
  self.portfolio.update_fill(event)
183
+ self.strategy.update_trades_from_fill(event)
171
184
 
172
185
  time.sleep(self.heartbeat)
173
186
 
@@ -185,13 +198,13 @@ class BacktestEngine(Backtest):
185
198
  stat2['Orders'] = self.orders
186
199
  stat2['Fills'] = self.fills
187
200
  stats.extend(stat2.items())
188
- print(
189
- tabulate(
190
- stats,
191
- headers=["Metric", "Value"],
192
- tablefmt="outline"),
193
- "\n"
194
- )
201
+ tab_stats = tabulate(stats, headers=["Metric", "Value"], tablefmt="outline")
202
+ print(tab_stats, "\n")
203
+ if self.stats_file:
204
+ with open(self.stats_file, 'a') as f:
205
+ f.write("\n[======= Summary Stats =======]\n")
206
+ f.write(tab_stats)
207
+ f.write("\n")
195
208
 
196
209
  if self.show_equity:
197
210
  print("\nCreating equity curve...")
@@ -329,7 +342,7 @@ def run_backtest_with(engine: Literal["bbstrader", "cerebro", "zipline"], **kwar
329
342
  data_handler=kwargs.get("data_handler"),
330
343
  strategy=kwargs.get("strategy"),
331
344
  exc_handler=kwargs.get("exc_handler"),
332
- initial_capital=kwargs.get("initial_capital"),
345
+ initial_capital=kwargs.get("initial_capital", 100000.0),
333
346
  heartbeat=kwargs.get("heartbeat", 0.0),
334
347
  **kwargs
335
348
  )
@@ -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
- return getattr(bars_list[-1][1], val_type)
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
- return np.array([getattr(b[1], val_type) for b in bars_list])
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'), has
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, but serves as a good baseline for improvement.
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", config_logger("execution.log"))
67
+ self.logger = kwargs.get("logger")
68
68
 
69
69
  def execute_order(self, event: OrderEvent):
70
70
  """
@@ -77,8 +77,9 @@ class SimExecutionHandler(ExecutionHandler):
77
77
  if event.type == 'ORDER':
78
78
  dtime = self.bardata.get_latest_bar_datetime(event.symbol)
79
79
  fill_event = FillEvent(
80
- dtime, event.symbol,
81
- 'ARCA', event.quantity, event.direction, None
80
+ timeindex=dtime, symbol=event.symbol,
81
+ exchange='ARCA', quantity=event.quantity, direction=event.direction,
82
+ fill_cost=None, commission=None, order=event.signal
82
83
  )
83
84
  self.events.put(fill_event)
84
85
  self.logger.info(
@@ -121,7 +122,7 @@ class MT5ExecutionHandler(ExecutionHandler):
121
122
  """
122
123
  self.events = events
123
124
  self.bardata = data
124
- self.logger = kwargs.get("logger", config_logger("execution.log"))
125
+ self.logger = kwargs.get("logger")
125
126
  self.__account = Account()
126
127
 
127
128
  def _calculate_lot(self, symbol, quantity, price):
@@ -232,7 +233,7 @@ class MT5ExecutionHandler(ExecutionHandler):
232
233
  fill_event = FillEvent(
233
234
  timeindex=dtime, symbol=symbol,
234
235
  exchange='MT5', quantity=quantity, direction=direction,
235
- fill_cost=None, commission=fees
236
+ fill_cost=None, commission=fees, order=event.signal
236
237
  )
237
238
  self.events.put(fill_event)
238
239
  self.logger.info(
@@ -6,6 +6,8 @@ from scipy.stats import mstats
6
6
  import matplotlib.pyplot as plt
7
7
  from matplotlib.ticker import MaxNLocator
8
8
  import quantstats as qs
9
+ import warnings
10
+ warnings.filterwarnings("ignore")
9
11
 
10
12
  sns.set_theme()
11
13
 
@@ -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 in a "dumb" manner by simply sending them directly
61
- to the brokerage with a predetermined fixed quantity size, irrespective of cash held.
62
- These are all unrealistic assumptions, but they help to outline how a portfolio order
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.
@@ -96,19 +93,23 @@ class Portfolio(object):
96
93
  initial_capital (float): The starting capital in USD.
97
94
 
98
95
  kwargs (dict): Additional arguments
96
+ - `leverage`: The leverage to apply to the portfolio.
99
97
  - `time_frame`: The time frame of the bars.
100
- - `trading_hours`: The number of trading hours in a day.
98
+ - `session_duration`: The number of trading hours in a day.
101
99
  - `benchmark`: The benchmark symbol to compare the portfolio.
102
- - `strategy_name`: The name of the strategy (the name must not include 'Strategy' in it).
100
+ - `output_dir`: The directory to save the backtest results.
101
+ - `strategy_name`: The name of the strategy (the name must not include 'Strategy' in it).
102
+ - `print_stats`: Whether to print the backtest statistics.
103
103
  """
104
104
  self.bars = bars
105
105
  self.events = events
106
106
  self.symbol_list = self.bars.symbol_list
107
107
  self.start_date = start_date
108
108
  self.initial_capital = initial_capital
109
+ self._leverage = kwargs.get('leverage', 1)
109
110
 
110
111
  self.timeframe = kwargs.get("time_frame", "D1")
111
- self.trading_hours = kwargs.get("session_duration", 6.5)
112
+ self.trading_hours = kwargs.get("session_duration", 23)
112
113
  self.benchmark = kwargs.get('benchmark', 'SPY')
113
114
  self.output_dir = kwargs.get('output_dir', None)
114
115
  self.strategy_name = kwargs.get('strategy_name', '')
@@ -174,6 +175,23 @@ class Portfolio(object):
174
175
  d['Commission'] = 0.0
175
176
  d['Total'] = self.initial_capital
176
177
  return d
178
+
179
+ def _get_price(self, symbol: str) -> float:
180
+ try:
181
+ price = self.bars.get_latest_bar_value(
182
+ symbol, "adj_close"
183
+ )
184
+ return price
185
+ except AttributeError:
186
+ try:
187
+ price = self.bars.get_latest_bar_value(
188
+ symbol, "close"
189
+ )
190
+ return price
191
+ except AttributeError:
192
+ raise AttributeError(
193
+ f"Bars object must have 'adj_close' or 'close' prices"
194
+ )
177
195
 
178
196
  def update_timeindex(self, event: MarketEvent):
179
197
  """
@@ -203,8 +221,8 @@ class Portfolio(object):
203
221
  dh['Total'] = self.current_holdings['Cash']
204
222
  for s in self.symbol_list:
205
223
  # Approximation to the real value
206
- market_value = self.current_positions[s] * \
207
- self.bars.get_latest_bar_value(s, "adj_close")
224
+ price = self._get_price(s)
225
+ market_value = self.current_positions[s] * price
208
226
  dh[s] = market_value
209
227
  dh['Total'] += market_value
210
228
 
@@ -245,9 +263,7 @@ class Portfolio(object):
245
263
  fill_dir = -1
246
264
 
247
265
  # Update holdings list with new quantities
248
- price = self.bars.get_latest_bar_value(
249
- fill.symbol, "adj_close"
250
- )
266
+ price = self._get_price(fill.symbol)
251
267
  cost = fill_dir * price * fill.quantity
252
268
  self.current_holdings[fill.symbol] += cost
253
269
  self.current_holdings['Commission'] += fill.commission
@@ -263,14 +279,15 @@ class Portfolio(object):
263
279
  self.update_positions_from_fill(event)
264
280
  self.update_holdings_from_fill(event)
265
281
 
266
- def generate_naive_order(self, signal: SignalEvent):
282
+ def generate_order(self, signal: SignalEvent):
267
283
  """
268
- Simply files an Order object as a constant quantity
269
- sizing of the signal object, without risk management or
270
- position sizing considerations.
284
+ Turns a SignalEvent into an OrderEvent.
271
285
 
272
286
  Args:
273
287
  signal (SignalEvent): The tuple containing Signal information.
288
+
289
+ Returns:
290
+ OrderEvent: The OrderEvent to be executed.
274
291
  """
275
292
  order = None
276
293
 
@@ -278,31 +295,36 @@ class Portfolio(object):
278
295
  direction = signal.signal_type
279
296
  quantity = signal.quantity
280
297
  strength = signal.strength
281
- price = signal.price
298
+ price = signal.price or self._get_price(symbol)
282
299
  cur_quantity = self.current_positions[symbol]
300
+ mkt_quantity = round(quantity * strength, 2)
301
+ new_quantity = mkt_quantity * self._leverage
283
302
 
284
- order_type = 'MKT'
285
- mkt_quantity = round(quantity * strength)
303
+ if direction in ['LONG', 'SHORT', 'EXIT']:
304
+ order_type = 'MKT'
305
+ else:
306
+ order_type = direction
286
307
 
287
- if direction == 'LONG' and cur_quantity == 0:
288
- order = OrderEvent(symbol, order_type, mkt_quantity, 'BUY', price)
289
- if direction == 'SHORT' and cur_quantity == 0:
290
- order = OrderEvent(symbol, order_type, mkt_quantity, 'SELL', price)
308
+ if direction == 'LONG' and new_quantity > 0:
309
+ order = OrderEvent(symbol, order_type, new_quantity, 'BUY', price, direction)
310
+ if direction == 'SHORT' and new_quantity > 0:
311
+ order = OrderEvent(symbol, order_type, new_quantity, 'SELL', price, direction)
291
312
 
292
313
  if direction == 'EXIT' and cur_quantity > 0:
293
- order = OrderEvent(symbol, order_type, abs(cur_quantity), 'SELL', price)
314
+ order = OrderEvent(symbol, order_type, abs(cur_quantity), 'SELL', price, direction)
294
315
  if direction == 'EXIT' and cur_quantity < 0:
295
- order = OrderEvent(symbol, order_type, abs(cur_quantity), 'BUY', price)
316
+ order = OrderEvent(symbol, order_type, abs(cur_quantity), 'BUY', price, direction)
296
317
 
297
318
  return order
298
319
 
320
+
299
321
  def update_signal(self, event: SignalEvent):
300
322
  """
301
323
  Acts on a SignalEvent to generate new orders
302
324
  based on the portfolio logic.
303
325
  """
304
326
  if event.type == 'SIGNAL':
305
- order_event = self.generate_naive_order(event)
327
+ order_event = self.generate_order(event)
306
328
  self.events.put(order_event)
307
329
 
308
330
  def create_equity_curve_dataframe(self):