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,349 @@
1
+ import pickle
2
+ import numpy as np
3
+ import pandas as pd
4
+ import seaborn as sns
5
+ from datetime import datetime
6
+ from hmmlearn.hmm import GaussianHMM
7
+ from abc import ABCMeta, abstractmethod
8
+ from matplotlib import cm, pyplot as plt
9
+ from matplotlib.dates import YearLocator, MonthLocator
10
+ from typing import Optional
11
+ sns.set_theme()
12
+
13
+ __all__ = [
14
+ "RiskModel",
15
+ "HMMRiskManager"
16
+ ]
17
+
18
+
19
+ class RiskModel(metaclass=ABCMeta):
20
+ """
21
+ The RiskModel class serves as an abstract base for implementing
22
+ risk management strategies in financial markets. It is designed
23
+ to assist in the decision-making process regarding which trades
24
+ are permissible under current market conditions and how to allocate
25
+ assets effectively to optimize the risk-reward ratio.
26
+
27
+ Risk management is a critical component in trading and investment
28
+ strategies, aiming to minimize potential losses without significantly
29
+ reducing the potential for gains. This class encapsulates the core
30
+ principles of risk management by providing a structured approach to
31
+ evaluate market conditions and manage asset allocation.
32
+
33
+ Implementing classes are required to define two key methods:
34
+
35
+ - `which_trade_allowed`:
36
+ Determines the types of trades that are permissible based on the
37
+ analysis of current market conditions and the risk profile of the portfolio.
38
+ This method should analyze the provided `returns_val` parameter,
39
+ which could represent historical returns, volatility measures,
40
+ or other financial metrics, to decide on the suitability of executing
41
+ certain trades.
42
+
43
+ - `which_quantity_allowed`:
44
+ Defines how assets should be allocated across the portfolio to maintain
45
+ an optimal balance between risk and return. This involves determining
46
+ the quantity of each asset that can be held, considering factors such as
47
+ diversification, liquidity, and the asset's volatility. This method ensures
48
+ that the portfolio adheres to predefined risk tolerance levels and
49
+ investment objectives.
50
+
51
+ Note:
52
+ Implementing these methods requires a deep understanding of risk
53
+ management theories, market analysis, and portfolio management principles.
54
+ The implementation should be tailored to the specific needs of the
55
+ investment strategy and the risk tolerance of the investor or the fund
56
+ being managed.
57
+ """
58
+
59
+ @abstractmethod
60
+ def which_trade_allowed(self, returns_val):
61
+ """
62
+ Determines the types of trades permissible under current market conditions.
63
+
64
+ Parameters:
65
+ returns_val: A parameter representing financial metrics
66
+ such as historical returns or volatility, used to
67
+ assess market conditions.
68
+ """
69
+ raise NotImplementedError("Should implement which_trade_allowed()")
70
+
71
+ @abstractmethod
72
+ def which_quantity_allowed(self):
73
+ """
74
+ Defines the strategy for asset allocation within
75
+ the portfolio to optimize risk-reward ratio.
76
+ """
77
+ raise NotImplementedError("Should implement which_quantity_allowed()")
78
+
79
+
80
+ class HMMRiskManager(RiskModel):
81
+ """
82
+ This class represents a risk management model using Hidden Markov Models (HMM)
83
+ to identify and manage market risks. It inherits from a generic RiskModel class
84
+ and utilizes Gaussian HMM to model the financial market's hidden states. These states
85
+ are used to make decisions on permissible trading actions based on the identified market
86
+ trends, thus facilitating a risk-aware trading strategy.
87
+
88
+ To learn more about about the Hidden Markov model,
89
+ See https://en.wikipedia.org/wiki/Hidden_Markov_model
90
+
91
+ Exemple:
92
+ >>> # Assuming `data` is a DataFrame containing your market data
93
+ >>> risk_manager = HMMRiskManager(data=data, states=3, iterations=200, verbose=True)
94
+ >>> current_regime = risk_manager.get_current_regime(data['Returns'].values)
95
+ >>> print(f"Current Market Regime: {current_regime}")
96
+
97
+ """
98
+
99
+ def __init__(
100
+ self,
101
+ data: Optional[pd.DataFrame] = None,
102
+ states: int = 2,
103
+ iterations: int = 100,
104
+ end_date: Optional[datetime] = None,
105
+ csv_filepath: Optional[str] = None,
106
+ **kwargs):
107
+ """
108
+ Initializes the HMMRiskManager with market data and model parameters.
109
+
110
+ Args:
111
+ `data` : DataFrame containing market data.
112
+ If not provided, data must be loaded via csv_filepath.
113
+
114
+ `states` (int): The number of hidden states in the HMM.
115
+ `iterations` (int): The number of iterations for the HMM to converge.
116
+ `end_date` (datetime): The end date for the market data to be considered.
117
+
118
+ `csv_filepath` (str): Path to the CSV file containing
119
+ market data if data is not provided directly.
120
+
121
+ kwarg (dict): Additional arguments
122
+ - `model_filename` (str): Filename to save the trained HMM model.
123
+ - `verbose` (bool): If True, prints additional model information.
124
+
125
+ - `cov_variance` (str): Type of covariance to use in the HMM.
126
+ possibles values are "spherical", "tied", "diag", "full".
127
+ see https://hmmlearn.readthedocs.io/en/latest/api.html#gaussianhmm for more details.
128
+
129
+ """
130
+ self.data = data
131
+ self.states = states
132
+ self.iterations = iterations
133
+ self.end_date = end_date
134
+ self.csv_filepath = csv_filepath
135
+
136
+ self.model_filename = kwargs.get("model_filename")
137
+ self.verbose = kwargs.get("verbose", True)
138
+ self.cov_variance = kwargs.get("cov_variance", "diag")
139
+
140
+ self.df = self._get_data()
141
+ self.hmm_model = self._fit_model()
142
+ if self.verbose:
143
+ self.show_hidden_states()
144
+ trends = self.identify_market_trends()
145
+ self.bullish_state = trends['bullish']
146
+ self.bearish_state = trends['bearish']
147
+ self.allowed_regimes = [s for s in trends.values()]
148
+
149
+ def _get_data(self):
150
+ """
151
+ Retrieves market data for the model either
152
+ from a provided DataFrame or a CSV file.
153
+ """
154
+ if self.data is not None:
155
+ return self.data
156
+ elif self.csv_filepath is not None:
157
+ return self.read_csv_file(self.csv_filepath)
158
+ else:
159
+ raise ValueError("No data source provided.")
160
+
161
+ def _fit_model(self):
162
+ """
163
+ Fits the HMM model to the market data
164
+ and saves the model if a filename is provided.
165
+ """
166
+ df = self.df.copy()
167
+ data = self.obtain_prices_df(df, end=self.end_date)
168
+ returns = np.column_stack([data["Returns"]])
169
+
170
+ hmm_model = GaussianHMM(
171
+ n_components=self.states, covariance_type=self.cov_variance,
172
+ n_iter=self.iterations
173
+ ).fit(returns)
174
+
175
+ if self.verbose:
176
+ print(
177
+ f"Hidden Markov model (HMM) Score: {hmm_model.score(returns)}")
178
+ if self.model_filename is not None:
179
+ self.save_hmm_model(hmm_model, self.model_filename)
180
+
181
+ return hmm_model
182
+
183
+ def get_states(self):
184
+ """
185
+ Predicts the hidden states for the market data
186
+ and calculates the mean returns and volatility for each state.
187
+
188
+ Returns:
189
+ DataFrame containing mean returns
190
+ and volatility for each hidden state.
191
+ """
192
+ df = self.df.copy()
193
+ data = self.obtain_prices_df(df, end=self.end_date)
194
+ returns = np.column_stack([data["Returns"]])
195
+ states = self.hmm_model.predict(returns)
196
+ data['State'] = states
197
+ # Calculating mean and volatility for each state
198
+ state_stats = data.groupby(
199
+ 'State'
200
+ )['Returns'].agg(['mean', 'std']).rename(
201
+ columns={'mean': 'Mean Returns', 'std': 'Volatility'})
202
+ return state_stats
203
+
204
+ def identify_market_trends(self):
205
+ """
206
+ Identifies bullish and bearish market trends
207
+ based on the mean returns and volatility of each state.
208
+
209
+ Returns:
210
+ A dictionary with keys 'bullish' and 'bearish'
211
+ indicating the identified states.
212
+ """
213
+ df = self.get_states()
214
+ # Sort the df based on Mean Returns and then by lower Volatility
215
+ sorted_df = df.sort_values(by=['Mean Returns', 'Volatility'],
216
+ ascending=[False, True])
217
+
218
+ # The first row after sorting will be the bullish state
219
+ # (highest mean return, lower volatility preferred)
220
+ bullish_state = sorted_df.index[0]
221
+
222
+ # The last row will be the bearish state
223
+ # (as it has the lowest mean return)
224
+ bearish_state = sorted_df.index[-1]
225
+
226
+ return {"bullish": bullish_state, "bearish": bearish_state}
227
+
228
+ def get_current_regime(self, returns_val):
229
+ """
230
+ Determines the current market regime based on the latest returns.
231
+
232
+ Args:
233
+ returns_val : Array of recent return values
234
+ to predict the current state.
235
+
236
+ Returns:
237
+ The predicted current market state.
238
+ """
239
+ returns = returns_val[~(np.isnan(returns_val) | np.isinf(returns_val))]
240
+ features = np.array(returns).reshape(-1, 1)
241
+ current_regime = self.hmm_model.predict(features)[-1]
242
+ return current_regime
243
+
244
+ def which_trade_allowed(self, returns_val):
245
+ """
246
+ Decides whether a long or short trade
247
+ is allowed based on the current market regime.
248
+ This method override the which_trade_allowed() from
249
+ RiskModel class .
250
+
251
+ Args:
252
+ returns_val : Array of recent return values
253
+ to assess the permissible trade.
254
+
255
+ Returns:
256
+ A string indicating "LONG" or "SHORT"
257
+ if a trade is allowed, or None if no trade is permitted.
258
+ """
259
+ state = self.get_current_regime(returns_val)
260
+ if state in self.allowed_regimes:
261
+ trade = "LONG" if state == self.bullish_state else "SHORT"
262
+ return trade
263
+ else:
264
+ return None
265
+
266
+ def which_quantity_allowed(self):
267
+ ...
268
+
269
+ def save_hmm_model(self, hmm_model, filename):
270
+ """
271
+ Saves the trained HMM model to a pickle file.
272
+
273
+ Args:
274
+ hmm_model : The trained GaussianHMM model to be saved.
275
+ filename : The filename under which to save the model.
276
+ """
277
+ print("Pickling HMM model...")
278
+ pickle.dump(hmm_model, open(f"{filename}.pkl", "wb"))
279
+ print("...HMM model pickled.")
280
+
281
+ def read_csv_file(self, csv_filepath):
282
+ """
283
+ Reads market data from a CSV file.
284
+
285
+ Args:
286
+ csv_filepath : Path to the CSV file containing the market data.
287
+
288
+ Returns:
289
+ DataFrame containing the parsed market data.
290
+ """
291
+ df = pd.read_csv(csv_filepath, header=0,
292
+ names=["Date", "Open", "High", "Low",
293
+ "Close", "Adj Close", "Volume"],
294
+ index_col="Date", parse_dates=True)
295
+ return df
296
+
297
+ def obtain_prices_df(self, data_frame, end=None):
298
+ """
299
+ Processes the market data to calculate returns
300
+ and optionally filters data up to a specified end date.
301
+
302
+ Args:
303
+ data_frame : DataFrame containing the market data.
304
+ end : Optional datetime object specifying the end date for the data.
305
+
306
+ Returns:
307
+ DataFrame with returns calculated
308
+ and data filtered up to the end date if specified.
309
+ """
310
+ df = data_frame.copy()
311
+ df['Returns'] = df['Adj Close'].pct_change()
312
+ if end is not None:
313
+ df = df[:end.strftime('%Y-%m-%d')]
314
+ df.dropna(inplace=True)
315
+ return df
316
+
317
+ def show_hidden_states(self):
318
+ """
319
+ Visualizes the market data
320
+ and the predicted hidden states from the HMM model.
321
+ """
322
+ data = self.df.copy()
323
+ df = self.obtain_prices_df(data, end=self.end_date)
324
+ self.plot_hidden_states(self.hmm_model, df)
325
+
326
+ def plot_hidden_states(self, hmm_model, df):
327
+ """
328
+ Plots the adjusted closing prices masked
329
+ by the in-sample hidden states as a mechanism
330
+ to understand the market regimes.
331
+
332
+ Args:
333
+ hmm_model : The trained GaussianHMM model used for predicting states.
334
+ df : DataFrame containing the market data with calculated returns.
335
+ """
336
+ hidden_states = hmm_model.predict(df[['Returns']])
337
+ fig, axs = plt.subplots(hmm_model.n_components,
338
+ sharex=True, sharey=True)
339
+ colours = cm.rainbow(np.linspace(0, 1, hmm_model.n_components))
340
+
341
+ for i, (ax, colour) in enumerate(zip(axs, colours)):
342
+ mask = hidden_states == i
343
+ ax.plot_date(df.index[mask], df['Adj Close']
344
+ [mask], ".", linestyle='none', c=colour)
345
+ ax.set_title(f"Hidden State #{i}")
346
+ ax.xaxis.set_major_locator(YearLocator())
347
+ ax.xaxis.set_minor_locator(MonthLocator())
348
+ ax.grid(True)
349
+ plt.show()