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.

@@ -0,0 +1,309 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ import seaborn as sns
4
+ import yfinance as yf
5
+
6
+ from scipy.stats import mstats
7
+ import matplotlib.pyplot as plt
8
+ from matplotlib.ticker import MaxNLocator
9
+
10
+ import warnings
11
+ warnings.filterwarnings("ignore")
12
+ sns.set_theme()
13
+
14
+ __all__ = [
15
+ "create_drawdowns",
16
+ "plot_performance",
17
+ "create_sharpe_ratio",
18
+ "create_sortino_ratio",
19
+ "plot_returns_and_dd",
20
+ "plot_monthly_yearly_returns"
21
+ ]
22
+
23
+
24
+ def create_sharpe_ratio(returns, periods=252):
25
+ """
26
+ Create the Sharpe ratio for the strategy, based on a
27
+ benchmark of zero (i.e. no risk-free rate information).
28
+
29
+ Args:
30
+ returns : A pandas Series representing period percentage returns.
31
+ periods (int): Daily (252), Hourly (252*6.5), Minutely(252*6.5*60) etc.
32
+
33
+ Returns:
34
+ S (float): Sharpe ratio
35
+ """
36
+ if np.std(returns) != 0:
37
+ return np.sqrt(periods) * (np.mean(returns)) / np.std(returns)
38
+ else:
39
+ return 0.0
40
+
41
+ # Define a function to calculate the Sortino Ratio
42
+
43
+
44
+ def create_sortino_ratio(returns, periods=252):
45
+ """
46
+ Create the Sortino ratio for the strategy, based on a
47
+ benchmark of zero (i.e. no risk-free rate information).
48
+
49
+ Args:
50
+ returns : A pandas Series representing period percentage returns.
51
+ periods (int): Daily (252), Hourly (252*6.5), Minutely(252*6.5*60) etc.
52
+
53
+ Returns:
54
+ S (float): Sortino ratio
55
+ """
56
+ # Calculate the annualized return
57
+ annualized_return = np.power(1 + np.mean(returns), periods) - 1
58
+ # Calculate the downside deviation
59
+ downside_returns = returns.copy()
60
+ downside_returns[returns > 0] = 0
61
+ annualized_downside_std = np.std(downside_returns) * np.sqrt(periods)
62
+ if annualized_downside_std != 0:
63
+ return annualized_return / annualized_downside_std
64
+ else:
65
+ return 0.0
66
+
67
+
68
+ def create_drawdowns(pnl):
69
+ """
70
+ Calculate the largest peak-to-trough drawdown of the PnL curve
71
+ as well as the duration of the drawdown. Requires that the
72
+ pnl_returns is a pandas Series.
73
+
74
+ Args:
75
+ pnl : A pandas Series representing period percentage returns.
76
+
77
+ Returns:
78
+ (tuple): drawdown, duration - high-water mark, duration.
79
+ """
80
+ # Calculate the cumulative returns curve
81
+ # and set up the High Water Mark
82
+ hwm = pd.Series(index=pnl.index)
83
+ hwm.iloc[0] = 0
84
+
85
+ # Create the drawdown and duration series
86
+ idx = pnl.index
87
+ drawdown = pd.Series(index=idx)
88
+ duration = pd.Series(index=idx)
89
+
90
+ # Loop over the index range
91
+ for t in range(1, len(idx)):
92
+ hwm.iloc[t] = max(hwm.iloc[t-1], pnl.iloc[t])
93
+ drawdown.iloc[t] = (hwm.iloc[t] - pnl.iloc[t])
94
+ duration.iloc[t] = (0 if drawdown.iloc[t] ==
95
+ 0 else duration.iloc[t-1]+1)
96
+
97
+ return drawdown, drawdown.max(), duration.max()
98
+
99
+
100
+ def plot_performance(df, title):
101
+ """
102
+ Plot the performance of the strategy:
103
+ - (Portfolio value, %)
104
+ - (Period returns, %)
105
+ - (Drawdowns, %)
106
+
107
+ Args:
108
+ df (pd.DataFrame):
109
+ The DataFrame containing the strategy returns and drawdowns.
110
+ title (str): The title of the plot.
111
+
112
+ Note:
113
+ The DataFrame should contain the following columns
114
+ - Datetime: The timestamp of the data
115
+ - Equity Curve: The portfolio value
116
+ - Returns: The period returns
117
+ - Drawdown: The drawdowns
118
+ - Total : The total returns
119
+ """
120
+ data = df.copy()
121
+ data = data.sort_values(by='Datetime')
122
+ # Plot three charts: Equity curve,
123
+ # period returns, drawdowns
124
+ fig = plt.figure(figsize=(14, 8))
125
+ fig.suptitle(f'{title} Strategy Performance', fontsize=16)
126
+
127
+ # Set the outer colour to white
128
+ sns.set_theme()
129
+
130
+ # Plot the equity curve
131
+ ax1 = fig.add_subplot(311, ylabel='Portfolio value, %')
132
+ data['Equity Curve'].plot(ax=ax1, color="blue", lw=2.)
133
+ ax1.set_xlabel('')
134
+ plt.grid(True)
135
+
136
+ # Plot the returns
137
+ ax2 = fig.add_subplot(312, ylabel='Period returns, %')
138
+ data['Returns'].plot(ax=ax2, color="black", lw=2.)
139
+ ax2.set_xlabel('')
140
+ plt.grid(True)
141
+
142
+ # Plot Drawdown
143
+ ax3 = fig.add_subplot(313, ylabel='Drawdowns, %')
144
+ data['Drawdown'].plot(ax=ax3, color="red", lw=2.)
145
+ ax3.set_xlabel('')
146
+ plt.grid(True)
147
+
148
+ # Plot the figure
149
+ plt.tight_layout()
150
+ plt.show()
151
+
152
+
153
+ def plot_returns_and_dd(df, benchmark: str, title):
154
+ """
155
+ Plot the returns and drawdowns of the strategy
156
+ compared to a benchmark.
157
+
158
+ Args:
159
+ df (pd.DataFrame):
160
+ The DataFrame containing the strategy returns and drawdowns.
161
+ benchmark (str):
162
+ The ticker symbol of the benchmark to compare the strategy to.
163
+ title (str): The title of the plot.
164
+
165
+ Note:
166
+ The DataFrame should contain the following columns:
167
+ - Datetime : The timestamp of the data
168
+ - Equity Curve : The portfolio value
169
+ - Returns : The period returns
170
+ - Drawdown : The drawdowns
171
+ - Total : The total returns
172
+ """
173
+ # Ensure data is sorted by Datetime
174
+ data = df.copy()
175
+ data.reset_index(inplace=True)
176
+ data = data.sort_values(by='Datetime')
177
+ data.sort_values(by='Datetime', inplace=True)
178
+
179
+ # Get the first and last Datetime values
180
+ first_date = data['Datetime'].iloc[0]
181
+ last_date = data['Datetime'].iloc[-1]
182
+
183
+ # Download benchmark data from Yahoo Finance
184
+ # To avoid errors, we use the try-except block
185
+ # in case the benchmark is not available
186
+ try:
187
+ bm = yf.download(benchmark, start=first_date, end=last_date)
188
+ bm['log_return'] = np.log(bm['Close'] / bm['Close'].shift(1))
189
+ # Use exponential to get cumulative returns
190
+ bm_returns = np.exp(np.cumsum(bm['log_return'].fillna(0)))
191
+
192
+ # Normalize bm series to start at 1.0
193
+ bm_returns_normalized = bm_returns / bm_returns.iloc[0]
194
+ except Exception:
195
+ bm = None
196
+
197
+ # Create figure and plot space
198
+ fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(
199
+ 14, 8), gridspec_kw={'height_ratios': [3, 1]})
200
+
201
+ # Plot the Equity Curve for the strategy
202
+ ax1.plot(data['Datetime'], data['Equity Curve'],
203
+ label='Backtest', color='green', lw=2.5)
204
+ # Check benchmarck an Plot the Returns for the benchmark
205
+ if bm is not None:
206
+ ax1.plot(bm.index, bm_returns_normalized,
207
+ label='benchmark', color='gray', lw=2.5)
208
+ ax1.set_title(f'{title} Strategy vs. Benchmark ({benchmark})')
209
+ else:
210
+ ax1.set_title(f'{title} Strategy Returns')
211
+ ax1.set_xlabel('Date')
212
+ ax1.set_ylabel('Cumulative Returns')
213
+ ax1.grid(True)
214
+ ax1.legend(loc='upper left')
215
+
216
+ # Plot the Drawdown
217
+ ax2.fill_between(data['Datetime'], data['Drawdown'],
218
+ 0, color='red', step="pre", alpha=0.5)
219
+ ax2.plot(data['Datetime'], data['Drawdown'], color='red',
220
+ alpha=0.6, lw=2.5) # Overlay the line
221
+ ax2.set_title('Drawdown (%)')
222
+ ax2.set_xlabel('Date')
223
+ ax2.set_ylabel('Drawdown')
224
+ ax2.grid(True)
225
+
226
+ # Display the plot
227
+ plt.tight_layout()
228
+ plt.show()
229
+
230
+
231
+ def plot_monthly_yearly_returns(df, title):
232
+ """
233
+ Plot the monthly and yearly returns of the strategy.
234
+
235
+ Args:
236
+ df (pd.DataFrame):
237
+ The DataFrame containing the strategy returns and drawdowns.
238
+ title (str): The title of the plot.
239
+
240
+ Note:
241
+ The DataFrame should contain the following columns:
242
+ - Datetime : The timestamp of the data
243
+ - Equity Curve : The portfolio value
244
+ - Returns : The period returns
245
+ - Drawdown : The drawdowns
246
+ - Total : The total returns
247
+ """
248
+ equity_df = df.copy()
249
+ equity_df.reset_index(inplace=True)
250
+ equity_df['Datetime'] = pd.to_datetime(equity_df['Datetime'])
251
+ equity_df.set_index('Datetime', inplace=True)
252
+
253
+ # Calculate daily returns
254
+ equity_df['Daily Returns'] = equity_df['Total'].pct_change()
255
+
256
+ # Group by year and month to get monthly returns
257
+ monthly_returns = equity_df['Daily Returns'].groupby(
258
+ [equity_df.index.year, equity_df.index.month]
259
+ ).apply(lambda x: (1 + x).prod() - 1)
260
+
261
+ # Prepare monthly returns DataFrame
262
+ monthly_returns_df = monthly_returns.unstack(level=-1) * 100
263
+ monthly_returns_df.columns = monthly_returns_df.columns.map(
264
+ lambda x: pd.to_datetime(x, format='%m').strftime('%b'))
265
+
266
+ # Calculate and prepare yearly returns DataFrame
267
+ yearly_returns_df = equity_df['Total'].resample(
268
+ 'YE').last().pct_change().to_frame(name='Yearly Returns') * 100
269
+
270
+ # Set the aesthetics for the plots
271
+ sns.set_theme(style="darkgrid")
272
+
273
+ # Initialize the matplotlib figure,
274
+ # adjust the height_ratios to give more space to the yearly returns
275
+ f, (ax1, ax2) = plt.subplots(2, 1, figsize=(
276
+ 12, 8), gridspec_kw={'height_ratios': [2, 1]})
277
+ f.suptitle(f'{title} Strategy Monthly and Yearly Returns')
278
+ # Find the min and max values in the data to set the color scale range.
279
+ vmin = monthly_returns_df.min().min()
280
+ vmax = monthly_returns_df.max().max()
281
+ # Define the color palette for the heatmap
282
+ cmap = sns.diverging_palette(10, 133, sep=3, n=256, center="light")
283
+
284
+ # Create the heatmap with the larger legend
285
+ sns.heatmap(monthly_returns_df, annot=True, fmt=".1f",
286
+ linewidths=.5, ax=ax1, cbar_kws={"shrink": .8},
287
+ cmap=cmap, center=0, vmin=vmin, vmax=vmax)
288
+
289
+ # Rotate the year labels on the y-axis to vertical
290
+ ax1.set_yticklabels(ax1.get_yticklabels(), rotation=0)
291
+ ax1.set_ylabel('')
292
+ ax1.set_xlabel('')
293
+
294
+ # Create the bar plot
295
+ yearly_returns_df.plot(kind='bar', ax=ax2, legend=None, color='skyblue')
296
+
297
+ # Set plot titles and labels
298
+ ax1.set_title('Monthly Returns (%)')
299
+ ax2.set_title('Yearly Returns (%)')
300
+
301
+ # Rotate the x labels for the yearly returns bar plot
302
+ ax2.set_xticklabels(yearly_returns_df.index.strftime('%Y'), rotation=45)
303
+ ax2.set_xlabel('')
304
+
305
+ # Adjust layout spacing
306
+ plt.tight_layout()
307
+
308
+ # Show the plot
309
+ plt.show()
@@ -0,0 +1,326 @@
1
+ import pandas as pd
2
+ from queue import Queue
3
+ from datetime import datetime
4
+ from bbstrader.btengine.event import (
5
+ OrderEvent, FillEvent, MarketEvent, SignalEvent
6
+ )
7
+ from bbstrader.btengine.data import DataHandler
8
+ from bbstrader.btengine.performance import (
9
+ create_drawdowns, plot_performance,
10
+ create_sharpe_ratio, create_sortino_ratio,
11
+ plot_returns_and_dd, plot_monthly_yearly_returns
12
+ )
13
+
14
+
15
+ class Portfolio(object):
16
+ """
17
+ This describes a `Portfolio()` object that keeps track of the positions
18
+ within a portfolio and generates orders of a fixed quantity of stock based on signals.
19
+ More sophisticated portfolio objects could include risk management and position
20
+ sizing tools (such as the `Kelly Criterion`).
21
+
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
24
+ as well as the market value of the positions (known as the "holdings").
25
+ This is simply an estimate of the liquidation value of the position and is derived in part
26
+ from the data handling facility of the backtester.
27
+
28
+ In addition to the positions and holdings management the portfolio must also be aware of
29
+ risk factors and position sizing techniques in order to optimise orders that are sent
30
+ to a brokerage or other form of market access.
31
+
32
+ Unfortunately, Portfolio and `Order Management Systems (OMS)` can become rather complex!
33
+ So let's keep the `Portfolio` object relatively straightforward anf improve it foward.
34
+
35
+ Continuing in the vein of the Event class hierarchy a Portfolio object must be able
36
+ to handle `SignalEvent` objects, generate `OrderEvent` objects and interpret `FillEvent`
37
+ objects to update positions. Thus it is no surprise that the Portfolio objects are often
38
+ the largest component of event-driven systems, in terms of lines of code (LOC).
39
+
40
+ The initialisation of the Portfolio object requires access to the bars `DataHandler`,
41
+ the `Event Queue`, a `start datetime stamp` and an `initial capital`
42
+ value (defaulting to `100,000 USD`) and others parameter based on the `Strategy` requirement.
43
+
44
+ The `Portfolio` is designed to handle position sizing and current holdings,
45
+ but will carry out trading orders in a "dumb" manner by simply sending them directly
46
+ to the brokerage with a predetermined fixed quantity size, irrespective of cash held.
47
+ These are all unrealistic assumptions, but they help to outline how a portfolio order
48
+ management system (OMS) functions in an eventdriven fashion.
49
+
50
+ The portfolio contains the `all_positions` and `current_positions` members.
51
+ The former stores a list of all previous positions recorded at the timestamp of a market data event.
52
+ A position is simply the quantity of the asset held. Negative positions mean the asset has been shorted.
53
+
54
+ The latter current_positions dictionary stores contains the current positions
55
+ for the last market bar update, for each symbol.
56
+
57
+ In addition to the positions data the portfolio stores `holdings`,
58
+ which describe the current market value of the positions held. "Current market value"
59
+ in this instance means the closing price obtained from the current market bar,
60
+ which is clearly an approximation, but is reasonable enough for the time being.
61
+ `all_holdings` stores the historical list of all symbol holdings, while current_holdings
62
+ stores the most up to date dictionary of all symbol holdings values.
63
+ """
64
+
65
+ def __init__(self,
66
+ bars: DataHandler,
67
+ events: Queue,
68
+ start_date: datetime,
69
+ initial_capital=100000.0,
70
+ **kwargs
71
+ ):
72
+ """
73
+ Initialises the portfolio with bars and an event queue.
74
+ Also includes a starting datetime index and initial capital
75
+ (USD unless otherwise stated).
76
+
77
+ Args:
78
+ bars (DataHandler): The DataHandler object with current market data.
79
+ events (Queue): The Event Queue object.
80
+ start_date (datetime): The start date (bar) of the portfolio.
81
+ initial_capital (float): The starting capital in USD.
82
+
83
+ kwargs (dict): Additional arguments
84
+ - `time_frame`: The time frame of the bars.
85
+ - `trading_hours`: The number of trading hours in a day.
86
+ - `benchmark`: The benchmark symbol to compare the portfolio.
87
+ - `strategy_name`: The name of the strategy (the name must not include 'Strategy' in it).
88
+ """
89
+ self.bars = bars
90
+ self.events = events
91
+ self.symbol_list = self.bars.symbol_list
92
+ self.start_date = start_date
93
+ self.initial_capital = initial_capital
94
+
95
+ self.timeframe = kwargs.get("time_frame", "D1")
96
+ self.trading_hours = kwargs.get("session_duration", 6.5)
97
+ self.benchmark = kwargs.get('benchmark', 'SPY')
98
+ self.strategy_name = kwargs.get('strategy_name', 'Strategy')
99
+ if self.timeframe not in self._tf_mapping():
100
+ raise ValueError(
101
+ f"Timeframe not supported,"
102
+ f"please choose one of the following: "
103
+ f"{', '.join(list(self._tf_mapping().keys()))}"
104
+ )
105
+ else:
106
+ self.tf = self._tf_mapping()[self.timeframe]
107
+
108
+ self.all_positions = self.construct_all_positions()
109
+ self.current_positions = dict((k, v) for k, v in
110
+ [(s, 0) for s in self.symbol_list])
111
+ self.all_holdings = self.construct_all_holdings()
112
+ self.current_holdings = self.construct_current_holdings()
113
+
114
+ def _tf_mapping(self):
115
+ """
116
+ Returns a dictionary mapping the time frames
117
+ to the number of bars in a year.
118
+ """
119
+ th = self.trading_hours
120
+ time_frame_mapping = {}
121
+ for minutes in [1, 2, 3, 4, 5, 6, 10, 12, 15, 20,
122
+ 30, 60, 120, 180, 240, 360, 480, 720]:
123
+ key = f"{minutes//60}h" if minutes >= 60 else f"{minutes}m"
124
+ time_frame_mapping[key] = int(252 * (60 / minutes) * th)
125
+ time_frame_mapping['D1'] = 252
126
+ return time_frame_mapping
127
+
128
+ def construct_all_positions(self):
129
+ """
130
+ Constructs the positions list using the start_date
131
+ to determine when the time index will begin.
132
+ """
133
+ d = dict((k, v) for k, v in [(s, 0) for s in self.symbol_list])
134
+ d['Datetime'] = self.start_date
135
+ return [d]
136
+
137
+ def construct_all_holdings(self):
138
+ """
139
+ Constructs the holdings list using the start_date
140
+ to determine when the time index will begin.
141
+ """
142
+ d = dict((k, v) for k, v in [(s, 0.0) for s in self.symbol_list])
143
+ d['Datetime'] = self.start_date
144
+ d['Cash'] = self.initial_capital
145
+ d['Commission'] = 0.0
146
+ d['Total'] = self.initial_capital
147
+ return [d]
148
+
149
+ def construct_current_holdings(self):
150
+ """
151
+ This constructs the dictionary which will hold the instantaneous
152
+ value of the portfolio across all symbols.
153
+ """
154
+ d = dict((k, v) for k, v in [(s, 0.0) for s in self.symbol_list])
155
+ d['Cash'] = self.initial_capital
156
+ d['Commission'] = 0.0
157
+ d['Total'] = self.initial_capital
158
+ return d
159
+
160
+ def update_timeindex(self, event: MarketEvent):
161
+ """
162
+ Adds a new record to the positions matrix for the current
163
+ market data bar. This reflects the PREVIOUS bar, i.e. all
164
+ current market data at this stage is known (OHLCV).
165
+ Makes use of a MarketEvent from the events queue.
166
+ """
167
+ latest_datetime = self.bars.get_latest_bar_datetime(
168
+ self.symbol_list[0]
169
+ )
170
+ # Update positions
171
+ # ================
172
+ dp = dict((k, v) for k, v in [(s, 0) for s in self.symbol_list])
173
+ dp['Datetime'] = latest_datetime
174
+ for s in self.symbol_list:
175
+ dp[s] = self.current_positions[s]
176
+ # Append the current positions
177
+ self.all_positions.append(dp)
178
+
179
+ # Update holdings
180
+ # ===============
181
+ dh = dict((k, v) for k, v in [(s, 0) for s in self.symbol_list])
182
+ dh['Datetime'] = latest_datetime
183
+ dh['Cash'] = self.current_holdings['Cash']
184
+ dh['Commission'] = self.current_holdings['Commission']
185
+ dh['Total'] = self.current_holdings['Cash']
186
+ for s in self.symbol_list:
187
+ # Approximation to the real value
188
+ market_value = self.current_positions[s] * \
189
+ self.bars.get_latest_bar_value(s, "Adj Close")
190
+ dh[s] = market_value
191
+ dh['Total'] += market_value
192
+
193
+ # Append the current holdings
194
+ self.all_holdings.append(dh)
195
+
196
+ def update_positions_from_fill(self, fill: FillEvent):
197
+ """
198
+ Takes a Fill object and updates the position matrix to
199
+ reflect the new position.
200
+
201
+ Args:
202
+ fill (FillEvent): The Fill object to update the positions with.
203
+ """
204
+ # Check whether the fill is a buy or sell
205
+ fill_dir = 0
206
+ if fill.direction == 'BUY':
207
+ fill_dir = 1
208
+ if fill.direction == 'SELL':
209
+ fill_dir = -1
210
+
211
+ # Update positions list with new quantities
212
+ self.current_positions[fill.symbol] += fill_dir*fill.quantity
213
+
214
+ def update_holdings_from_fill(self, fill: FillEvent):
215
+ """
216
+ Takes a Fill object and updates the holdings matrix to
217
+ reflect the holdings value.
218
+
219
+ Args:
220
+ fill (FillEvent): The Fill object to update the holdings with.
221
+ """
222
+ # Check whether the fill is a buy or sell
223
+ fill_dir = 0
224
+ if fill.direction == 'BUY':
225
+ fill_dir = 1
226
+ if fill.direction == 'SELL':
227
+ fill_dir = -1
228
+
229
+ # Update holdings list with new quantities
230
+ fill_cost = self.bars.get_latest_bar_value(
231
+ fill.symbol, "Adj Close"
232
+ )
233
+ cost = fill_dir * fill_cost * fill.quantity
234
+ self.current_holdings[fill.symbol] += cost
235
+ self.current_holdings['Commission'] += fill.commission
236
+ self.current_holdings['Cash'] -= (cost + fill.commission)
237
+ self.current_holdings['Total'] -= (cost + fill.commission)
238
+
239
+ def update_fill(self, event: FillEvent):
240
+ """
241
+ Updates the portfolio current positions and holdings
242
+ from a FillEvent.
243
+ """
244
+ if event.type == 'FILL':
245
+ self.update_positions_from_fill(event)
246
+ self.update_holdings_from_fill(event)
247
+
248
+ def generate_naive_order(self, signal: SignalEvent):
249
+ """
250
+ Simply files an Order object as a constant quantity
251
+ sizing of the signal object, without risk management or
252
+ position sizing considerations.
253
+
254
+ Args:
255
+ signal (SignalEvent): The tuple containing Signal information.
256
+ """
257
+ order = None
258
+
259
+ symbol = signal.symbol
260
+ direction = signal.signal_type
261
+ quantity = signal.quantity
262
+ strength = signal.strength
263
+ cur_quantity = self.current_positions[symbol]
264
+
265
+ order_type = 'MKT'
266
+ mkt_quantity = round(quantity * strength)
267
+
268
+ if direction == 'LONG' and cur_quantity == 0:
269
+ order = OrderEvent(symbol, order_type, mkt_quantity, 'BUY')
270
+ if direction == 'SHORT' and cur_quantity == 0:
271
+ order = OrderEvent(symbol, order_type, mkt_quantity, 'SELL')
272
+
273
+ if direction == 'EXIT' and cur_quantity > 0:
274
+ order = OrderEvent(symbol, order_type, abs(cur_quantity), 'SELL')
275
+ if direction == 'EXIT' and cur_quantity < 0:
276
+ order = OrderEvent(symbol, order_type, abs(cur_quantity), 'BUY')
277
+
278
+ return order
279
+
280
+ def update_signal(self, event: SignalEvent):
281
+ """
282
+ Acts on a SignalEvent to generate new orders
283
+ based on the portfolio logic.
284
+ """
285
+ if event.type == 'SIGNAL':
286
+ order_event = self.generate_naive_order(event)
287
+ self.events.put(order_event)
288
+
289
+ def create_equity_curve_dataframe(self):
290
+ """
291
+ Creates a pandas DataFrame from the all_holdings
292
+ list of dictionaries.
293
+ """
294
+ curve = pd.DataFrame(self.all_holdings)
295
+ curve.set_index('Datetime', inplace=True)
296
+ curve['Returns'] = curve['Total'].pct_change(fill_method=None)
297
+ curve['Equity Curve'] = (1.0+curve['Returns']).cumprod()
298
+ self.equity_curve = curve
299
+
300
+ def output_summary_stats(self):
301
+ """
302
+ Creates a list of summary statistics for the portfolio.
303
+ """
304
+ total_return = self.equity_curve['Equity Curve'].iloc[-1]
305
+ returns = self.equity_curve['Returns']
306
+ pnl = self.equity_curve['Equity Curve']
307
+
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)
311
+ self.equity_curve['Drawdown'] = drawdown
312
+
313
+ stats = [
314
+ ("Total Return", f"{(total_return-1.0) * 100.0:.2f}%"),
315
+ ("Sharpe Ratio", f"{sharpe_ratio:.2f}"),
316
+ ("Sortino Ratio", f"{sortino_ratio:.2f}"),
317
+ ("Max Drawdown", f"{max_dd * 100.0:.2f}%"),
318
+ ("Drawdown Duration", f"{dd_duration}")
319
+ ]
320
+ self.equity_curve.to_csv('equity.csv')
321
+ plot_performance(self.equity_curve, self.strategy_name)
322
+ plot_returns_and_dd(self.equity_curve,
323
+ self.benchmark, self.strategy_name)
324
+ plot_monthly_yearly_returns(self.equity_curve, self.strategy_name)
325
+
326
+ return stats
@@ -0,0 +1,31 @@
1
+ from abc import ABCMeta, abstractmethod
2
+
3
+
4
+ class Strategy(metaclass=ABCMeta):
5
+ """
6
+ A `Strategy()` object encapsulates all calculation on market data
7
+ that generate advisory signals to a `Portfolio` object. Thus all of
8
+ the "strategy logic" resides within this class. We opted to separate
9
+ out the `Strategy` and `Portfolio` objects for this backtester,
10
+ since we believe this is more amenable to the situation of multiple
11
+ strategies feeding "ideas" to a larger `Portfolio`, which then can handle
12
+ its own risk (such as sector allocation, leverage). In higher frequency trading,
13
+ the strategy and portfolio concepts will be tightly coupled and extremely
14
+ hardware dependent.
15
+
16
+ At this stage in the event-driven backtester development there is no concept of
17
+ an indicator or filter, such as those found in technical trading. These are also
18
+ good candidates for creating a class hierarchy.
19
+
20
+ The strategy hierarchy is relatively simple as it consists of an abstract
21
+ base class with a single pure virtual method for generating `SignalEvent` objects.
22
+ """
23
+
24
+ @abstractmethod
25
+ def calculate_signals(self):
26
+ """
27
+ Provides the mechanisms to calculate the list of signals.
28
+ """
29
+ raise NotImplementedError(
30
+ "Should implement calculate_signals()"
31
+ )
@@ -0,0 +1,6 @@
1
+
2
+ from bbstrader.metatrader.account import Account
3
+ from bbstrader.metatrader.rates import Rates
4
+ from bbstrader.metatrader.risk import RiskManagement
5
+ from bbstrader.metatrader.trade import Trade, create_trade_instance
6
+ from bbstrader.metatrader.utils import *