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

@@ -360,10 +360,12 @@ class YFHistoricDataHandler(BaseCSVDataHandler):
360
360
 
361
361
 
362
362
  # TODO # Get data from EODHD
363
+ # https://eodhd.com/
363
364
  class EODHDHistoricDataHandler(BaseCSVDataHandler):
364
365
  ...
365
366
 
366
- # TODO # Get data from FinancialModelingPrep ()
367
+ # TODO # Get data from FMP using Financialtoolkit API
368
+ # https://github.com/bbalouki/FinanceToolkit
367
369
  class FMPHistoricDataHandler(BaseCSVDataHandler):
368
370
  ...
369
371
 
@@ -54,9 +54,10 @@ class SignalEvent(Event):
54
54
  strategy_id: int,
55
55
  symbol: str,
56
56
  datetime: datetime,
57
- signal_type: str,
57
+ signal_type: Literal['LONG', 'SHORT', 'EXIT'],
58
58
  quantity: int | float = 100,
59
- strength: int | float = 1.0
59
+ strength: int | float = 1.0,
60
+ price: int | float = None
60
61
  ):
61
62
  """
62
63
  Initialises the SignalEvent.
@@ -67,10 +68,11 @@ class SignalEvent(Event):
67
68
 
68
69
  symbol (str): The ticker symbol, e.g. 'GOOG'.
69
70
  datetime (datetime): The timestamp at which the signal was generated.
70
- signal_type (str): 'LONG' or 'SHORT'.
71
+ signal_type (str): 'LONG' or 'SHORT' or 'EXIT'.
71
72
  quantity (int | float): An optional integer (or float) representing the order size.
72
73
  strength (int | float): An adjustment factor "suggestion" used to scale
73
74
  quantity at the portfolio level. Useful for pairs strategies.
75
+ price (int | float): An optional price to be used when the signal is generated.
74
76
  """
75
77
  self.type = 'SIGNAL'
76
78
  self.strategy_id = strategy_id
@@ -79,6 +81,7 @@ class SignalEvent(Event):
79
81
  self.signal_type = signal_type
80
82
  self.quantity = quantity
81
83
  self.strength = strength
84
+ self.price = price
82
85
 
83
86
 
84
87
  class OrderEvent(Event):
@@ -96,9 +99,10 @@ class OrderEvent(Event):
96
99
 
97
100
  def __init__(self,
98
101
  symbol: str,
99
- order_type: str,
102
+ order_type: Literal['MKT', 'LMT'],
100
103
  quantity: int | float,
101
- direction: str
104
+ direction: Literal['BUY', 'SELL'],
105
+ price: int | float = None
102
106
  ):
103
107
  """
104
108
  Initialises the order type, setting whether it is
@@ -110,20 +114,22 @@ class OrderEvent(Event):
110
114
  order_type (str): 'MKT' or 'LMT' for Market or Limit.
111
115
  quantity (int | float): Non-negative number for quantity.
112
116
  direction (str): 'BUY' or 'SELL' for long or short.
117
+ price (int | float): The price at which to order.
113
118
  """
114
119
  self.type = 'ORDER'
115
120
  self.symbol = symbol
116
121
  self.order_type = order_type
117
122
  self.quantity = quantity
118
123
  self.direction = direction
124
+ self.price = price
119
125
 
120
126
  def print_order(self):
121
127
  """
122
128
  Outputs the values within the Order.
123
129
  """
124
130
  print(
125
- "Order: Symbol=%s, Type=%s, Quantity=%s, Direction=%s" %
126
- (self.symbol, self.order_type, self.quantity, self.direction)
131
+ "Order: Symbol=%s, Type=%s, Quantity=%s, Direction=%s, Price=%s" %
132
+ (self.symbol, self.order_type, self.quantity, self.direction, self.price)
127
133
  )
128
134
 
129
135
 
@@ -151,7 +157,7 @@ class FillEvent(Event):
151
157
  symbol: str,
152
158
  exchange: str,
153
159
  quantity: int | float,
154
- direction: Literal['LONG', 'SHORT', 'EXIT'],
160
+ direction: Literal['BUY', 'SELL'],
155
161
  fill_cost: int | float | None,
156
162
  commission: float | None = None
157
163
  ):
@@ -170,7 +176,7 @@ class FillEvent(Event):
170
176
  exchange (str): The exchange where the order was filled.
171
177
  quantity (int | float): The filled quantity.
172
178
  direction (str): The direction of fill `('LONG', 'SHORT', 'EXIT')`
173
- fill_cost (int | float): The holdings value in dollars.
179
+ fill_cost (int | float): Price of the shares when filled.
174
180
  commission (float | None): An optional commission sent from IB.
175
181
  """
176
182
  self.type = 'FILL'
@@ -1,11 +1,13 @@
1
- import datetime
1
+ from datetime import datetime
2
2
  from queue import Queue
3
3
  from abc import ABCMeta, abstractmethod
4
4
  from bbstrader.btengine.event import FillEvent, OrderEvent
5
+ from bbstrader.metatrader.account import Account
5
6
 
6
7
  __all__ = [
7
8
  "ExecutionHandler",
8
- "SimulatedExecutionHandler"
9
+ "SimulatedExecutionHandler",
10
+ "MT5ExecutionHandler"
9
11
  ]
10
12
 
11
13
 
@@ -71,13 +73,71 @@ class SimulatedExecutionHandler(ExecutionHandler):
71
73
  """
72
74
  if event.type == 'ORDER':
73
75
  fill_event = FillEvent(
74
- datetime.datetime.now(), event.symbol,
76
+ datetime.now(), event.symbol,
75
77
  'ARCA', event.quantity, event.direction, None
76
78
  )
77
79
  self.events.put(fill_event)
78
80
 
79
- # TODO # Use in live execution
80
-
81
81
 
82
82
  class MT5ExecutionHandler(ExecutionHandler):
83
- ...
83
+ def __init__(self, events: Queue, **kwargs):
84
+ """
85
+ """
86
+ self.events = events
87
+ self.account = Account()
88
+
89
+ def _estimate_total_fees(self, symbol, lot):
90
+ # TODO: Implement the calculation of fees based on the broker's fees
91
+ # https://www.metatrader5.com/en/terminal/help/trading/market_watch
92
+ # Calculate fees based on the broker's fees , swap and commission
93
+ return 0.0
94
+
95
+ def _calculate_lot(self, symbol, quantity, price):
96
+ FX = self.account.get_symbol_type(symbol) == 'FX'
97
+ COMD = self.account.get_symbol_type(symbol) == 'COMD'
98
+ FUT = self.account.get_symbol_type(symbol) == 'FUT'
99
+ CRYPTO = self.account.get_symbol_type(symbol) == 'CRYPTO'
100
+ symbol_info = self.account.get_symbol_info(symbol)
101
+ contract_size = symbol_info.trade_contract_size
102
+
103
+ lot = (quantity*price) / (contract_size * price)
104
+ if contract_size == 1:
105
+ lot = quantity
106
+ if COMD or FUT or CRYPTO and contract_size > 1:
107
+ lot = quantity / contract_size
108
+ if FX:
109
+ lot = (quantity*price / contract_size)
110
+ return self._check_lot(symbol, lot)
111
+
112
+ def _check_lot(self, symbol, lot):
113
+ symbol_info = self.account.get_symbol_info(symbol)
114
+ if lot < symbol_info.volume_min:
115
+ return symbol_info.volume_min
116
+ elif lot > symbol_info.volume_max:
117
+ return symbol_info.volume_max
118
+ return round(lot, 2)
119
+
120
+ def execute_order(self, event: OrderEvent):
121
+ """
122
+ Executes an Order event by converting it into a Fill event.
123
+
124
+ Args:
125
+ event (OrderEvent): Contains an Event object with order information.
126
+ """
127
+ if event.type == 'ORDER':
128
+ symbol = event.symbol
129
+ direction = event.direction
130
+ quantity = event.quantity
131
+ price = event.price
132
+ lot = self._calculate_lot(symbol, quantity, price)
133
+ fees = self._estimate_total_fees(symbol, lot)
134
+ fill_event = FillEvent(
135
+ timeindex=datetime.now(), symbol=symbol,
136
+ exchange='MT5', quantity=quantity, direction=direction,
137
+ fill_cost=None, commission=fees
138
+ )
139
+ self.events.put(fill_event)
140
+
141
+
142
+ class IBExecutionHandler(ExecutionHandler):
143
+ ...
@@ -6,9 +6,11 @@ import yfinance as yf
6
6
  from scipy.stats import mstats
7
7
  import matplotlib.pyplot as plt
8
8
  from matplotlib.ticker import MaxNLocator
9
+ import quantstats as qs
9
10
 
10
11
  import warnings
11
12
  warnings.filterwarnings("ignore")
13
+ warnings.simplefilter(action='ignore', category=FutureWarning)
12
14
  sns.set_theme()
13
15
 
14
16
  __all__ = [
@@ -17,7 +19,8 @@ __all__ = [
17
19
  "create_sharpe_ratio",
18
20
  "create_sortino_ratio",
19
21
  "plot_returns_and_dd",
20
- "plot_monthly_yearly_returns"
22
+ "plot_monthly_yearly_returns",
23
+ "show_qs_stats"
21
24
  ]
22
25
 
23
26
 
@@ -307,3 +310,33 @@ def plot_monthly_yearly_returns(df, title):
307
310
 
308
311
  # Show the plot
309
312
  plt.show()
313
+
314
+ def show_qs_stats(equity_curve, benchmark, strategy_name):
315
+ """
316
+ Generate the full quantstats report for the strategy.
317
+
318
+ Args:
319
+ equity_curve (pd.DataFrame):
320
+ The DataFrame containing the strategy returns and drawdowns.
321
+ benchmark (str):
322
+ The ticker symbol of the benchmark to compare the strategy to.
323
+ strategy_name (str): The name of the strategy.
324
+ """
325
+ # Load the returns data
326
+ returns = equity_curve.copy()
327
+
328
+ # Drop duplicate index entries
329
+ returns = returns[~returns.index.duplicated(keep='first')]
330
+
331
+ # Extend pandas functionality with quantstats
332
+ qs.extend_pandas()
333
+
334
+ # Generate the full report with a benchmark
335
+ file = strategy_name.replace(' ', '_')
336
+ qs.reports.full(
337
+ returns['Returns'], mode='full', benchmark=benchmark)
338
+ qs.reports.html(
339
+ returns['Returns'],
340
+ benchmark='SPY',
341
+ output=f"{file}_report.html",
342
+ title=strategy_name)
@@ -6,10 +6,10 @@ from bbstrader.btengine.event import (
6
6
  )
7
7
  from bbstrader.btengine.data import DataHandler
8
8
  from bbstrader.btengine.performance import (
9
- create_drawdowns, plot_performance,
10
- create_sharpe_ratio, create_sortino_ratio,
9
+ create_drawdowns, plot_performance, show_qs_stats,
11
10
  plot_returns_and_dd, plot_monthly_yearly_returns
12
11
  )
12
+ import quantstats as qs
13
13
 
14
14
 
15
15
  class Portfolio(object):
@@ -20,7 +20,7 @@ class Portfolio(object):
20
20
  sizing tools (such as the `Kelly Criterion`).
21
21
 
22
22
  The portfolio order management system is possibly the most complex component of
23
- an eventdriven backtester. Its role is to keep track of all current market positions
23
+ an event driven backtester. Its role is to keep track of all current market positions
24
24
  as well as the market value of the positions (known as the "holdings").
25
25
  This is simply an estimate of the liquidation value of the position and is derived in part
26
26
  from the data handling facility of the backtester.
@@ -95,7 +95,7 @@ class Portfolio(object):
95
95
  self.timeframe = kwargs.get("time_frame", "D1")
96
96
  self.trading_hours = kwargs.get("session_duration", 6.5)
97
97
  self.benchmark = kwargs.get('benchmark', 'SPY')
98
- self.strategy_name = kwargs.get('strategy_name', 'Strategy')
98
+ self.strategy_name = kwargs.get('strategy_name', '')
99
99
  if self.timeframe not in self._tf_mapping():
100
100
  raise ValueError(
101
101
  f"Timeframe not supported,"
@@ -227,10 +227,10 @@ class Portfolio(object):
227
227
  fill_dir = -1
228
228
 
229
229
  # Update holdings list with new quantities
230
- fill_cost = self.bars.get_latest_bar_value(
230
+ price = self.bars.get_latest_bar_value(
231
231
  fill.symbol, "Adj Close"
232
232
  )
233
- cost = fill_dir * fill_cost * fill.quantity
233
+ cost = fill_dir * price * fill.quantity
234
234
  self.current_holdings[fill.symbol] += cost
235
235
  self.current_holdings['Commission'] += fill.commission
236
236
  self.current_holdings['Cash'] -= (cost + fill.commission)
@@ -260,20 +260,21 @@ class Portfolio(object):
260
260
  direction = signal.signal_type
261
261
  quantity = signal.quantity
262
262
  strength = signal.strength
263
+ price = signal.price
263
264
  cur_quantity = self.current_positions[symbol]
264
265
 
265
266
  order_type = 'MKT'
266
267
  mkt_quantity = round(quantity * strength)
267
268
 
268
269
  if direction == 'LONG' and cur_quantity == 0:
269
- order = OrderEvent(symbol, order_type, mkt_quantity, 'BUY')
270
+ order = OrderEvent(symbol, order_type, mkt_quantity, 'BUY', price)
270
271
  if direction == 'SHORT' and cur_quantity == 0:
271
- order = OrderEvent(symbol, order_type, mkt_quantity, 'SELL')
272
+ order = OrderEvent(symbol, order_type, mkt_quantity, 'SELL', price)
272
273
 
273
274
  if direction == 'EXIT' and cur_quantity > 0:
274
- order = OrderEvent(symbol, order_type, abs(cur_quantity), 'SELL')
275
+ order = OrderEvent(symbol, order_type, abs(cur_quantity), 'SELL', price)
275
276
  if direction == 'EXIT' and cur_quantity < 0:
276
- order = OrderEvent(symbol, order_type, abs(cur_quantity), 'BUY')
277
+ order = OrderEvent(symbol, order_type, abs(cur_quantity), 'BUY', price)
277
278
 
278
279
  return order
279
280
 
@@ -305,9 +306,12 @@ class Portfolio(object):
305
306
  returns = self.equity_curve['Returns']
306
307
  pnl = self.equity_curve['Equity Curve']
307
308
 
308
- sharpe_ratio = create_sharpe_ratio(returns, periods=self.tf)
309
- sortino_ratio = create_sortino_ratio(returns, periods=self.tf)
310
- drawdown, max_dd, dd_duration = create_drawdowns(pnl)
309
+ sharpe_ratio = qs.stats.sharpe(returns, periods=self.tf)
310
+ sortino_ratio = qs.stats.sortino(returns, periods=self.tf)
311
+ drawdown, _, _ = create_drawdowns(pnl)
312
+ max_dd = qs.stats.max_drawdown(returns)
313
+ dd_details = qs.stats.drawdown_details(drawdown)
314
+ dd_duration = dd_details['days'].max()
311
315
  self.equity_curve['Drawdown'] = drawdown
312
316
 
313
317
  stats = [
@@ -317,10 +321,16 @@ class Portfolio(object):
317
321
  ("Max Drawdown", f"{max_dd * 100.0:.2f}%"),
318
322
  ("Drawdown Duration", f"{dd_duration}")
319
323
  ]
320
- self.equity_curve.to_csv('equity.csv')
324
+ now = datetime.now().strftime('%Y%m%d%H%M%S')
325
+ strategy_name = self.strategy_name.replace(' ', '_')
326
+ file = f"{strategy_name}_{now}_equity.csv"
327
+ self.equity_curve.to_csv(file)
321
328
  plot_performance(self.equity_curve, self.strategy_name)
322
329
  plot_returns_and_dd(self.equity_curve,
323
330
  self.benchmark, self.strategy_name)
331
+ qs.plots.monthly_heatmap(returns, savefig='monthly_heatmap.png')
324
332
  plot_monthly_yearly_returns(self.equity_curve, self.strategy_name)
333
+ show_qs_stats(self.equity_curve, self.benchmark, self.strategy_name)
325
334
 
326
335
  return stats
336
+
@@ -1,7 +1,6 @@
1
1
  from abc import ABCMeta, abstractmethod
2
2
  from typing import Dict, Union
3
3
 
4
-
5
4
  class Strategy(metaclass=ABCMeta):
6
5
  """
7
6
  A `Strategy()` object encapsulates all calculation on market data
@@ -30,4 +29,4 @@ class Strategy(metaclass=ABCMeta):
30
29
  """
31
30
  raise NotImplementedError(
32
31
  "Should implement calculate_signals()"
33
- )
32
+ )
@@ -782,7 +782,7 @@ class Account(object):
782
782
  else:
783
783
  positions = mt5.positions_get()
784
784
 
785
- if positions is None:
785
+ if positions is None or len(positions) == 0:
786
786
  return None
787
787
  if to_df:
788
788
  df = pd.DataFrame(list(positions), columns=positions[0]._asdict())
@@ -885,7 +885,7 @@ class Account(object):
885
885
  else:
886
886
  position_deals = mt5.history_deals_get(date_from, date_to)
887
887
 
888
- if position_deals is None:
888
+ if position_deals is None or len(position_deals) == 0:
889
889
  return None
890
890
 
891
891
  df = pd.DataFrame(list(position_deals),
@@ -961,7 +961,7 @@ class Account(object):
961
961
  else:
962
962
  orders = mt5.orders_get()
963
963
 
964
- if orders is None:
964
+ if orders is None or len(orders) == 0:
965
965
  return None
966
966
 
967
967
  if to_df:
@@ -1064,7 +1064,7 @@ class Account(object):
1064
1064
  else:
1065
1065
  history_orders = mt5.history_orders_get(date_from, date_to)
1066
1066
 
1067
- if history_orders is None:
1067
+ if history_orders is None or len(history_orders) == 0:
1068
1068
  return None
1069
1069
 
1070
1070
  df = pd.DataFrame(list(history_orders),
@@ -22,7 +22,12 @@ class Rates(object):
22
22
  and count of bars or by providing a specific date range.
23
23
 
24
24
  Notes:
25
- The `get_open, get_high, get_low, get_close, get_adj_close, get_returns,
25
+ 1. Befor using this class, ensure that the `Max bars in chart` in you terminal
26
+ is set to a value that is greater than the number of bars you want to retrieve
27
+ or just set it to Unlimited.
28
+ In your MT5 terminal, go to `Tools` -> `Options` -> `Charts` -> `Max bars in chart`.
29
+
30
+ 2. The `get_open, get_high, get_low, get_close, get_adj_close, get_returns,
26
31
  get_volume` properties return data in Broker's timezone.
27
32
 
28
33
  Example: