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

@@ -57,7 +57,8 @@ class SignalEvent(Event):
57
57
  signal_type: Literal['LONG', 'SHORT', 'EXIT'],
58
58
  quantity: int | float = 100,
59
59
  strength: int | float = 1.0,
60
- price: int | float = None
60
+ price: int | float = None,
61
+ stoplimit: int | float = None
61
62
  ):
62
63
  """
63
64
  Initialises the SignalEvent.
@@ -73,6 +74,7 @@ class SignalEvent(Event):
73
74
  strength (int | float): An adjustment factor "suggestion" used to scale
74
75
  quantity at the portfolio level. Useful for pairs strategies.
75
76
  price (int | float): An optional price to be used when the signal is generated.
77
+ stoplimit (int | float): An optional stop-limit price for the signal
76
78
  """
77
79
  self.type = 'SIGNAL'
78
80
  self.strategy_id = strategy_id
@@ -82,6 +84,7 @@ class SignalEvent(Event):
82
84
  self.quantity = quantity
83
85
  self.strength = strength
84
86
  self.price = price
87
+ self.stoplimit = stoplimit
85
88
 
86
89
 
87
90
  class OrderEvent(Event):
@@ -99,14 +102,15 @@ class OrderEvent(Event):
99
102
 
100
103
  def __init__(self,
101
104
  symbol: str,
102
- order_type: Literal['MKT', 'LMT'],
105
+ order_type: Literal['MKT', 'LMT', 'STP', 'STPLMT'],
103
106
  quantity: int | float,
104
107
  direction: Literal['BUY', 'SELL'],
105
- price: int | float = None
108
+ price: int | float = None,
109
+ signal: str = None
106
110
  ):
107
111
  """
108
112
  Initialises the order type, setting whether it is
109
- a Market order ('MKT') or Limit order ('LMT'), has
113
+ a Market order ('MKT') or Limit order ('LMT'), or Stop order ('STP').
110
114
  a quantity (integral or float) and its direction ('BUY' or 'SELL').
111
115
 
112
116
  Args:
@@ -115,6 +119,7 @@ class OrderEvent(Event):
115
119
  quantity (int | float): Non-negative number for quantity.
116
120
  direction (str): 'BUY' or 'SELL' for long or short.
117
121
  price (int | float): The price at which to order.
122
+ signal (str): The signal that generated the order.
118
123
  """
119
124
  self.type = 'ORDER'
120
125
  self.symbol = symbol
@@ -122,6 +127,7 @@ class OrderEvent(Event):
122
127
  self.quantity = quantity
123
128
  self.direction = direction
124
129
  self.price = price
130
+ self.signal = signal
125
131
 
126
132
  def print_order(self):
127
133
  """
@@ -159,7 +165,8 @@ class FillEvent(Event):
159
165
  quantity: int | float,
160
166
  direction: Literal['BUY', 'SELL'],
161
167
  fill_cost: int | float | None,
162
- commission: float | None = None
168
+ commission: float | None = None,
169
+ order: str = None
163
170
  ):
164
171
  """
165
172
  Initialises the FillEvent object. Sets the symbol, exchange,
@@ -178,6 +185,7 @@ class FillEvent(Event):
178
185
  direction (str): The direction of fill `('LONG', 'SHORT', 'EXIT')`
179
186
  fill_cost (int | float): Price of the shares when filled.
180
187
  commission (float | None): An optional commission sent from IB.
188
+ order (str): The order that this fill is related
181
189
  """
182
190
  self.type = 'FILL'
183
191
  self.timeindex = timeindex
@@ -191,6 +199,7 @@ class FillEvent(Event):
191
199
  self.commission = self.calculate_ib_commission()
192
200
  else:
193
201
  self.commission = commission
202
+ self.order = order
194
203
 
195
204
  def calculate_ib_commission(self):
196
205
  """
@@ -2,15 +2,15 @@ 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.btengine.data import DataHandler
5
6
  from bbstrader.metatrader.account import Account
6
7
 
7
8
  __all__ = [
8
9
  "ExecutionHandler",
9
- "SimulatedExecutionHandler",
10
+ "SimExecutionHandler",
10
11
  "MT5ExecutionHandler"
11
12
  ]
12
13
 
13
-
14
14
  class ExecutionHandler(metaclass=ABCMeta):
15
15
  """
16
16
  The ExecutionHandler abstract class handles the interaction
@@ -25,7 +25,8 @@ class ExecutionHandler(metaclass=ABCMeta):
25
25
 
26
26
  The ExecutionHandler described here is exceedingly simple,
27
27
  since it fills all orders at the current market price.
28
- 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.
29
30
  """
30
31
 
31
32
  @abstractmethod
@@ -42,7 +43,7 @@ class ExecutionHandler(metaclass=ABCMeta):
42
43
  )
43
44
 
44
45
 
45
- class SimulatedExecutionHandler(ExecutionHandler):
46
+ class SimExecutionHandler(ExecutionHandler):
46
47
  """
47
48
  The simulated execution handler simply converts all order
48
49
  objects into their equivalent fill objects automatically
@@ -53,7 +54,7 @@ class SimulatedExecutionHandler(ExecutionHandler):
53
54
  handler.
54
55
  """
55
56
 
56
- def __init__(self, events: Queue, **kwargs):
57
+ def __init__(self, events: Queue, data: DataHandler, **kwargs):
57
58
  """
58
59
  Initialises the handler, setting the event queues
59
60
  up internally.
@@ -62,6 +63,8 @@ class SimulatedExecutionHandler(ExecutionHandler):
62
63
  events (Queue): The Queue of Event objects.
63
64
  """
64
65
  self.events = events
66
+ self.bardata = data
67
+ self.logger = kwargs.get("logger")
65
68
 
66
69
  def execute_order(self, event: OrderEvent):
67
70
  """
@@ -72,51 +75,145 @@ class SimulatedExecutionHandler(ExecutionHandler):
72
75
  event (OrderEvent): Contains an Event object with order information.
73
76
  """
74
77
  if event.type == 'ORDER':
78
+ dtime = self.bardata.get_latest_bar_datetime(event.symbol)
75
79
  fill_event = FillEvent(
76
- datetime.now(), event.symbol,
77
- 'ARCA', event.quantity, event.direction, None
80
+ dtime, event.symbol,
81
+ 'ARCA', event.quantity, event.direction, order=event.signal
78
82
  )
79
83
  self.events.put(fill_event)
84
+ self.logger.info(
85
+ f"{event.direction} ORDER FILLED: SYMBOL={event.symbol}, "
86
+ f"QUANTITY={event.quantity}, PRICE @{event.price} EXCHANGE={fill_event.exchange}",
87
+ custom_time=fill_event.timeindex
88
+ )
80
89
 
81
90
 
82
91
  class MT5ExecutionHandler(ExecutionHandler):
83
- def __init__(self, events: Queue, **kwargs):
92
+ """
93
+ The main role of `MT5ExecutionHandler` class is to estimate the execution fees
94
+ for different asset classes on the MT5 terminal.
95
+
96
+ Generally we have four types of fees when we execute trades using the MT5 terminal
97
+ (commissions, swap, spread and other fees). But most of these fees depend on the specifications
98
+ of each instrument and the duration of the transaction for the swap for example.
99
+
100
+ Calculating the exact fees for each instrument would be a bit complex because our Backtest engine
101
+ and the Portfolio class do not take into account the duration of each trade to apply the appropriate
102
+ rate for the swap for example. So we have to use only the model of calculating the commissions
103
+ for each asset class and each instrument.
104
+
105
+ The second thing that must be taken into account on MT5 is the type of account offered by the broker.
106
+ Brokers have different account categories each with its specifications for each asset class and each instrument.
107
+ Again considering all these conditions would make our class very complex. So we took the `Raw Spread`
108
+ account fee calculation model from [Just Market](https://one.justmarkets.link/a/tufvj0xugm/registration/trader)
109
+ for indicies, forex, commodities and crypto. We used the [Admiral Market](https://cabinet.a-partnership.com/visit/?bta=35537&brand=admiralmarkets)
110
+ account fee calculation model from `Trade.MT5` account type for stocks and ETFs.
111
+
112
+ NOTE:
113
+ This class only works with `bbstrader.metatrader.data.MT5DataHandler` class.
114
+ """
115
+ def __init__(self, events: Queue, data: DataHandler, **kwargs):
84
116
  """
117
+ Initialises the handler, setting the event queues up internally.
118
+
119
+ Args:
120
+ events (Queue): The Queue of Event objects.
85
121
  """
86
122
  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
123
+ self.bardata = data
124
+ self.logger = kwargs.get("logger")
125
+ self.__account = Account()
94
126
 
95
127
  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)
128
+ symbol_type = self.__account.get_symbol_type(symbol)
129
+ symbol_info = self.__account.get_symbol_info(symbol)
101
130
  contract_size = symbol_info.trade_contract_size
102
131
 
103
132
  lot = (quantity*price) / (contract_size * price)
104
133
  if contract_size == 1:
105
134
  lot = quantity
106
- if COMD or FUT or CRYPTO and contract_size > 1:
135
+ if symbol_type in [
136
+ 'COMD', 'FUT', 'CRYPTO'] and contract_size > 1:
107
137
  lot = quantity / contract_size
108
- if FX:
138
+ if symbol_type == 'FX':
109
139
  lot = (quantity*price / contract_size)
110
140
  return self._check_lot(symbol, lot)
111
141
 
112
142
  def _check_lot(self, symbol, lot):
113
- symbol_info = self.account.get_symbol_info(symbol)
143
+ symbol_info = self.__account.get_symbol_info(symbol)
114
144
  if lot < symbol_info.volume_min:
115
145
  return symbol_info.volume_min
116
146
  elif lot > symbol_info.volume_max:
117
147
  return symbol_info.volume_max
118
148
  return round(lot, 2)
119
149
 
150
+ def _estimate_total_fees(self, symbol, lot, qty, price):
151
+ symbol_type = self.__account.get_symbol_type(symbol)
152
+ if symbol_type in ['STK', 'ETF']:
153
+ return self._estimate_stock_commission(symbol, qty, price)
154
+ elif symbol_type == 'FX':
155
+ return self._estimate_forex_commission(lot)
156
+ elif symbol_type == 'COMD':
157
+ return self._estimate_commodity_commission(lot)
158
+ elif symbol_type == 'IDX':
159
+ return self._estimate_index_commission(lot)
160
+ elif symbol_type == 'FUT':
161
+ return self._estimate_futures_commission()
162
+ elif symbol_type == 'CRYPTO':
163
+ return self._estimate_crypto_commission()
164
+ else:
165
+ return 0.0
166
+
167
+ def _estimate_stock_commission(self, symbol, qty, price):
168
+ # https://admiralmarkets.com/start-trading/contract-specifications?regulator=jsc
169
+ min_com = 1.0
170
+ min_aud = 8.0
171
+ min_dkk = 30.0
172
+ min_nok = min_sek = 10.0
173
+ us_com = 0.02 # per chare
174
+ ger_fr_uk_cm = 0.001 # percent
175
+ eu_asia_cm = 0.0015 # percent
176
+ if (
177
+ symbol in self.__account.get_stocks_from_country('USA')
178
+ or self.__account.get_symbol_type(symbol) == 'ETF'
179
+ and self.__account.get_currency_rates(symbol)['mc'] == 'USD'
180
+ ):
181
+ return max(min_com, qty * us_com)
182
+ elif (
183
+ symbol in self.__account.get_stocks_from_country('GBR')
184
+ or symbol in self.__account.get_stocks_from_country('FRA')
185
+ or symbol in self.__account.get_stocks_from_country('DEU')
186
+ or self.__account.get_symbol_type(symbol) == 'ETF'
187
+ and self.__account.get_currency_rates(symbol)['mc'] in ['GBP', 'EUR']
188
+ ):
189
+ return max(min_com, qty*price*ger_fr_uk_cm)
190
+ else:
191
+ if self.__account.get_currency_rates(symbol)['mc'] == 'AUD':
192
+ return max(min_aud, qty*price*eu_asia_cm)
193
+ elif self.__account.get_currency_rates(symbol)['mc'] == 'DKK':
194
+ return max(min_dkk, qty*price*eu_asia_cm)
195
+ elif self.__account.get_currency_rates(symbol)['mc'] == 'NOK':
196
+ return max(min_nok, qty*price*eu_asia_cm)
197
+ elif self.__account.get_currency_rates(symbol)['mc'] == 'SEK':
198
+ return max(min_sek, qty*price*eu_asia_cm)
199
+ else:
200
+ return max(min_com, qty*price*eu_asia_cm)
201
+
202
+ def _estimate_forex_commission(self, lot):
203
+ return 3.0 * lot
204
+
205
+ def _estimate_commodity_commission(self, lot):
206
+ return 3.0 * lot
207
+
208
+ def _estimate_index_commission(self, lot):
209
+ return 0.25 * lot
210
+
211
+ def _estimate_futures_commission(self):
212
+ return 0.0
213
+
214
+ def _estimate_crypto_commission(self):
215
+ return 0.0
216
+
120
217
  def execute_order(self, event: OrderEvent):
121
218
  """
122
219
  Executes an Order event by converting it into a Fill event.
@@ -130,13 +227,18 @@ class MT5ExecutionHandler(ExecutionHandler):
130
227
  quantity = event.quantity
131
228
  price = event.price
132
229
  lot = self._calculate_lot(symbol, quantity, price)
133
- fees = self._estimate_total_fees(symbol, lot)
230
+ fees = self._estimate_total_fees(symbol, lot, quantity, price)
231
+ dtime = self.bardata.get_latest_bar_datetime(symbol)
134
232
  fill_event = FillEvent(
135
- timeindex=datetime.now(), symbol=symbol,
233
+ timeindex=dtime, symbol=symbol,
136
234
  exchange='MT5', quantity=quantity, direction=direction,
137
- fill_cost=None, commission=fees
235
+ fill_cost=None, commission=fees, order=event.signal
138
236
  )
139
237
  self.events.put(fill_event)
238
+ self.logger.info(
239
+ f"{direction} ORDER FILLED: SYMBOL={symbol}, QUANTITY={quantity}, "
240
+ f"PRICE @{price} EXCHANGE={fill_event.exchange}", custom_time=fill_event.timeindex
241
+ )
140
242
 
141
243
 
142
244
  class IBExecutionHandler(ExecutionHandler):
@@ -2,15 +2,11 @@ import numpy as np
2
2
  import pandas as pd
3
3
  import seaborn as sns
4
4
  import yfinance as yf
5
-
6
5
  from scipy.stats import mstats
7
6
  import matplotlib.pyplot as plt
8
7
  from matplotlib.ticker import MaxNLocator
9
8
  import quantstats as qs
10
9
 
11
- import warnings
12
- warnings.filterwarnings("ignore")
13
- warnings.simplefilter(action='ignore', category=FutureWarning)
14
10
  sns.set_theme()
15
11
 
16
12
  __all__ = [
@@ -53,6 +49,7 @@ def create_sortino_ratio(returns, periods=252) -> float:
53
49
  """
54
50
  return qs.stats.sortino(returns, periods=periods)
55
51
 
52
+
56
53
  def create_drawdowns(pnl):
57
54
  """
58
55
  Calculate the largest peak-to-trough drawdown of the PnL curve
@@ -138,7 +135,7 @@ def plot_performance(df, title):
138
135
  plt.show()
139
136
 
140
137
 
141
- def plot_returns_and_dd(df, benchmark: str, title):
138
+ def plot_returns_and_dd(df: pd.DataFrame, benchmark: str, title):
142
139
  """
143
140
  Plot the returns and drawdowns of the strategy
144
141
  compared to a benchmark.
@@ -216,7 +213,7 @@ def plot_returns_and_dd(df, benchmark: str, title):
216
213
  plt.show()
217
214
 
218
215
 
219
- def plot_monthly_yearly_returns(df, title):
216
+ def plot_monthly_yearly_returns(df:pd.DataFrame, title):
220
217
  """
221
218
  Plot the monthly and yearly returns of the strategy.
222
219
 
@@ -253,7 +250,7 @@ def plot_monthly_yearly_returns(df, title):
253
250
 
254
251
  # Calculate and prepare yearly returns DataFrame
255
252
  yearly_returns_df = equity_df['Total'].resample(
256
- 'YE').last().pct_change().to_frame(name='Yearly Returns') * 100
253
+ 'A').last().pct_change().to_frame(name='Yearly Returns') * 100
257
254
 
258
255
  # Set the aesthetics for the plots
259
256
  sns.set_theme(style="darkgrid")
@@ -3,23 +3,34 @@ from queue import Queue
3
3
  from pathlib import Path
4
4
  from datetime import datetime
5
5
  from bbstrader.btengine.event import (
6
- OrderEvent, FillEvent, MarketEvent, SignalEvent
6
+ OrderEvent,
7
+ FillEvent,
8
+ MarketEvent,
9
+ SignalEvent
7
10
  )
8
11
  from bbstrader.btengine.data import DataHandler
9
12
  from bbstrader.btengine.performance import (
10
- create_drawdowns, create_sharpe_ratio, create_sortino_ratio,
11
- plot_performance, show_qs_stats,plot_returns_and_dd,
13
+ create_drawdowns,
14
+ create_sharpe_ratio,
15
+ create_sortino_ratio,
16
+ plot_performance,
17
+ show_qs_stats,
18
+ plot_returns_and_dd,
12
19
  plot_monthly_yearly_returns
13
20
  )
21
+ from bbstrader.config import BBSTRADER_DIR
14
22
  import quantstats as qs
15
23
 
16
24
 
25
+ __all__ = [
26
+ 'Portfolio',
27
+ ]
28
+
29
+
17
30
  class Portfolio(object):
18
31
  """
19
32
  This describes a `Portfolio()` object that keeps track of the positions
20
33
  within a portfolio and generates orders of a fixed quantity of stock based on signals.
21
- More sophisticated portfolio objects could include risk management and position
22
- sizing tools (such as the `Kelly Criterion`).
23
34
 
24
35
  The portfolio order management system is possibly the most complex component of
25
36
  an event driven backtester. Its role is to keep track of all current market positions
@@ -44,10 +55,9 @@ class Portfolio(object):
44
55
  value (defaulting to `100,000 USD`) and others parameter based on the `Strategy` requirement.
45
56
 
46
57
  The `Portfolio` is designed to handle position sizing and current holdings,
47
- but will carry out trading orders in a "dumb" manner by simply sending them directly
48
- to the brokerage with a predetermined fixed quantity size, irrespective of cash held.
49
- These are all unrealistic assumptions, but they help to outline how a portfolio order
50
- 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
+
51
61
 
52
62
  The portfolio contains the `all_positions` and `current_positions` members.
53
63
  The former stores a list of all previous positions recorded at the timestamp of a market data event.
@@ -95,9 +105,11 @@ class Portfolio(object):
95
105
  self.initial_capital = initial_capital
96
106
 
97
107
  self.timeframe = kwargs.get("time_frame", "D1")
98
- self.trading_hours = kwargs.get("session_duration", 6.5)
108
+ self.trading_hours = kwargs.get("session_duration", 23)
99
109
  self.benchmark = kwargs.get('benchmark', 'SPY')
110
+ self.output_dir = kwargs.get('output_dir', None)
100
111
  self.strategy_name = kwargs.get('strategy_name', '')
112
+ self.print_stats = kwargs.get('print_stats', True)
101
113
  if self.timeframe not in self._tf_mapping():
102
114
  raise ValueError(
103
115
  f"Timeframe not supported,"
@@ -112,6 +124,7 @@ class Portfolio(object):
112
124
  [(s, 0) for s in self.symbol_list])
113
125
  self.all_holdings = self.construct_all_holdings()
114
126
  self.current_holdings = self.construct_current_holdings()
127
+ self.equity_curve = None
115
128
 
116
129
  def _tf_mapping(self):
117
130
  """
@@ -158,6 +171,23 @@ class Portfolio(object):
158
171
  d['Commission'] = 0.0
159
172
  d['Total'] = self.initial_capital
160
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
+ )
161
191
 
162
192
  def update_timeindex(self, event: MarketEvent):
163
193
  """
@@ -187,8 +217,8 @@ class Portfolio(object):
187
217
  dh['Total'] = self.current_holdings['Cash']
188
218
  for s in self.symbol_list:
189
219
  # Approximation to the real value
190
- market_value = self.current_positions[s] * \
191
- self.bars.get_latest_bar_value(s, "Adj Close")
220
+ price = self._get_price(s)
221
+ market_value = self.current_positions[s] * price
192
222
  dh[s] = market_value
193
223
  dh['Total'] += market_value
194
224
 
@@ -229,9 +259,7 @@ class Portfolio(object):
229
259
  fill_dir = -1
230
260
 
231
261
  # Update holdings list with new quantities
232
- price = self.bars.get_latest_bar_value(
233
- fill.symbol, "Adj Close"
234
- )
262
+ price = self._get_price(fill.symbol)
235
263
  cost = fill_dir * price * fill.quantity
236
264
  self.current_holdings[fill.symbol] += cost
237
265
  self.current_holdings['Commission'] += fill.commission
@@ -247,14 +275,16 @@ class Portfolio(object):
247
275
  self.update_positions_from_fill(event)
248
276
  self.update_holdings_from_fill(event)
249
277
 
250
- def generate_naive_order(self, signal: SignalEvent):
278
+ def generate_order(self, signal: SignalEvent):
251
279
  """
252
- Simply files an Order object as a constant quantity
253
- sizing of the signal object, without risk management or
254
- position sizing considerations.
280
+ Check if the portfolio has enough cash to place an order
281
+ and generate an OrderEvent, else return None.
255
282
 
256
283
  Args:
257
284
  signal (SignalEvent): The tuple containing Signal information.
285
+
286
+ Returns:
287
+ OrderEvent: The OrderEvent to be executed.
258
288
  """
259
289
  order = None
260
290
 
@@ -262,21 +292,33 @@ class Portfolio(object):
262
292
  direction = signal.signal_type
263
293
  quantity = signal.quantity
264
294
  strength = signal.strength
265
- price = signal.price
295
+ price = signal.price or self._get_price(symbol)
266
296
  cur_quantity = self.current_positions[symbol]
297
+ cash = self.current_holdings['Cash']
267
298
 
268
- order_type = 'MKT'
299
+ if direction in ['LONG', 'SHORT', 'EXIT']:
300
+ order_type = 'MKT'
301
+ else:
302
+ order_type = direction
269
303
  mkt_quantity = round(quantity * strength)
304
+ cost = mkt_quantity * price
270
305
 
271
- if direction == 'LONG' and cur_quantity == 0:
272
- order = OrderEvent(symbol, order_type, mkt_quantity, 'BUY', price)
273
- if direction == 'SHORT' and cur_quantity == 0:
274
- order = OrderEvent(symbol, order_type, mkt_quantity, 'SELL', price)
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
312
+
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)
275
317
 
276
318
  if direction == 'EXIT' and cur_quantity > 0:
277
- order = OrderEvent(symbol, order_type, abs(cur_quantity), 'SELL', price)
319
+ order = OrderEvent(symbol, order_type, abs(cur_quantity), 'SELL', price, direction)
278
320
  if direction == 'EXIT' and cur_quantity < 0:
279
- order = OrderEvent(symbol, order_type, abs(cur_quantity), 'BUY', price)
321
+ order = OrderEvent(symbol, order_type, abs(cur_quantity), 'BUY', price, direction)
280
322
 
281
323
  return order
282
324
 
@@ -286,7 +328,7 @@ class Portfolio(object):
286
328
  based on the portfolio logic.
287
329
  """
288
330
  if event.type == 'SIGNAL':
289
- order_event = self.generate_naive_order(event)
331
+ order_event = self.generate_order(event)
290
332
  self.events.put(order_event)
291
333
 
292
334
  def create_equity_curve_dataframe(self):
@@ -295,6 +337,7 @@ class Portfolio(object):
295
337
  list of dictionaries.
296
338
  """
297
339
  curve = pd.DataFrame(self.all_holdings)
340
+ curve['Datetime'] = pd.to_datetime(curve['Datetime'], utc=True)
298
341
  curve.set_index('Datetime', inplace=True)
299
342
  curve['Returns'] = curve['Total'].pct_change(fill_method=None)
300
343
  curve['Equity Curve'] = (1.0+curve['Returns']).cumprod()
@@ -325,20 +368,24 @@ class Portfolio(object):
325
368
  ]
326
369
  now = datetime.now().strftime('%Y%m%d%H%M%S')
327
370
  strategy_name = self.strategy_name.replace(' ', '_')
328
- results_dir = Path('.backtests') / strategy_name
371
+ if self.output_dir:
372
+ results_dir = Path(self.output_dir) / strategy_name
373
+ else:
374
+ results_dir = Path('.backtests') / strategy_name
329
375
  results_dir.mkdir(parents=True, exist_ok=True)
330
376
 
331
- csv_file = f"{strategy_name}_{now}_equity.csv"
377
+ csv_file = f"{strategy_name}_{now}_equities.csv"
332
378
  png_file = f'{strategy_name}_{now}_returns_heatmap.png'
333
379
  html_file = f"{strategy_name}_{now}_report.html"
334
380
  self.equity_curve.to_csv(results_dir / csv_file)
335
381
 
336
- plot_performance(self.equity_curve, self.strategy_name)
337
- plot_returns_and_dd(self.equity_curve, self.benchmark, self.strategy_name)
338
- qs.plots.monthly_heatmap(returns, savefig=f"{results_dir}/{png_file}")
339
- plot_monthly_yearly_returns(self.equity_curve, self.strategy_name)
340
- show_qs_stats(returns, self.benchmark, self.strategy_name,
341
- save_dir=f"{results_dir}/{html_file}")
382
+ if self.print_stats:
383
+ plot_performance(self.equity_curve, self.strategy_name)
384
+ plot_returns_and_dd(self.equity_curve, self.benchmark, self.strategy_name)
385
+ qs.plots.monthly_heatmap(returns, savefig=f"{results_dir}/{png_file}")
386
+ plot_monthly_yearly_returns(self.equity_curve, self.strategy_name)
387
+ show_qs_stats(returns, self.benchmark, self.strategy_name,
388
+ save_dir=f"{results_dir}/{html_file}")
342
389
 
343
390
  return stats
344
391