OnurKerimoglu commited on
Commit
e11ad18
·
1 Parent(s): 8aa5811

technical_analysis: merge and plot forecasts

Browse files
Files changed (1) hide show
  1. src/technical_analysis.py +93 -30
src/technical_analysis.py CHANGED
@@ -1,6 +1,7 @@
1
  import datetime as dt
2
  import logging
3
 
 
4
  import matplotlib.pyplot as plt
5
  import matplotlib.dates as mdates
6
  from matplotlib.axes import Axes
@@ -11,12 +12,15 @@ from ta.momentum import RSIIndicator, StochasticOscillator
11
  from ta.trend import MACD
12
  import yfinance as yf
13
 
 
14
 
15
  class TechnicalAnalysis():
16
  def __init__(
17
  self,
18
  ticker:str,
19
  fetchperiodinweeks:int=12,
 
 
20
  plot_ta:bool=True,
21
  savefig:bool=False,
22
  debug=False):
@@ -28,6 +32,10 @@ class TechnicalAnalysis():
28
  stock ticker to analyze
29
  fetchperiodinweeks : int, optional, default: 8
30
  number of weeks to fetch historical data from YahooFinance
 
 
 
 
31
  plot_ta : bool, optional, default: True
32
  whether to generate plots of technical analysis metrics. Plot will be created under plots/{ticker}.png
33
  debug : bool, optional, default: False
@@ -44,6 +52,8 @@ class TechnicalAnalysis():
44
  # input arguments
45
  self.ticker = ticker
46
  self.fetchperiodinweeks = fetchperiodinweeks
 
 
47
  self.plot_ta = plot_ta
48
  self.savefig = savefig
49
  # done initializing
@@ -60,20 +70,23 @@ class TechnicalAnalysis():
60
  - plots the price and TA metrics.
61
  """
62
  # fetch data from yf
63
- self.df = self.fetch_data()
64
  # add the features based on technical analysis
65
- if self.df.shape[0] > 0:
66
- self.df = self.tech_analysis()
 
 
67
  # plot the results
68
  if self.plot_ta:
69
  os.makedirs('plots', exist_ok=True)
70
  fig = self.plot_stock_metrics(
71
- self.df,
72
  datasets={
73
- 'Volume': ['Volume'],
74
- 'Prices': ['Close', 'VWAP'], # 'High','Low',
75
  'Indices': ['RSI', 'StochOsc'],
76
- 'Trend': ['MACD', 'MACDsig', 'MACDdif']},
 
 
77
  savefig=self.savefig
78
  )
79
  else:
@@ -84,7 +97,7 @@ class TechnicalAnalysis():
84
  else:
85
  fig = None
86
 
87
- return self.df, fig
88
 
89
  def fetch_data(
90
  self
@@ -130,7 +143,8 @@ class TechnicalAnalysis():
130
  return df
131
 
132
  def tech_analysis(
133
- self
 
134
  ) -> pd.DataFrame:
135
  """
136
  Calculates technical analysis indicators for the fetched stock price data.
@@ -144,11 +158,13 @@ class TechnicalAnalysis():
144
  - Trend Indicators:
145
  - Moving Average Convergence Divergence (MACD)
146
  The calculated indicators are added to the DataFrame as new columns.
 
 
 
147
  Returns:
148
  pd.DataFrame
149
  The DataFrame with the calculated technical analysis indicators.
150
  """
151
- df = self.df
152
 
153
  # Price Indicators
154
  # Volume-Weighted Average Price (VWAP)
@@ -188,13 +204,41 @@ class TechnicalAnalysis():
188
 
189
  return df
190
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  def plot_stock_metrics(
192
  self,
193
  df,
194
- datasets={
195
- 'Volume': ['Volume'],
196
- 'Price': ['Close'] # 'High','Low'
197
- },
198
  savefig=False
199
  ) -> None:
200
  """
@@ -262,7 +306,7 @@ class TechnicalAnalysis():
262
  'High' columns is added.
263
  """
264
 
265
- print(f'plotting {colstoplot} in {dataset}')
266
  colorcycle = ['black', 'blue', 'red', 'green', 'orange']
267
  for i, col in enumerate(colstoplot):
268
  ax.plot(
@@ -271,6 +315,29 @@ class TechnicalAnalysis():
271
  color=colorcycle[i],
272
  label=col,
273
  linewidth=2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  # Format major ticks with year
275
  # Set major ticks (every Monday with labels)
276
  ax.xaxis.set_major_locator(mdates.WeekdayLocator(byweekday=mdates.MO))
@@ -278,22 +345,15 @@ class TechnicalAnalysis():
278
  ax.xaxis.set_major_formatter(mdates.DateFormatter('%m.%d'))
279
  # Set minor ticks (every day, but without labels)
280
  ax.xaxis.set_minor_locator(mdates.DayLocator())
281
-
282
  plt.setp(ax.get_xticklabels(), rotation=90, ha='center')
283
-
 
 
 
284
  ax.set_title(dataset)
285
  # ax.set_xlabel('Date')
286
- ax.set_ylabel(dataset)
287
  if len(colstoplot) > 1:
288
- ax.legend()
289
- if dataset in ['Index', 'Indices']:
290
- ax.set_ylim([0, 100])
291
- # Add a transparent shaded region between y=30 and y=70
292
- ax.fill_between(df.index, 30, 70, color='gray', alpha=0.3)
293
- if dataset in ['Price', 'Prices']:
294
- # Add a transparent shaded region between y=30 and y=70
295
- ax.fill_between(df.index, df['Low'], df['High'], color='gray', alpha=0.3)
296
- ax.grid(True, linestyle='--', alpha=0.7)
297
 
298
  def get_fetcherror_fig(
299
  self,
@@ -326,8 +386,11 @@ class TechnicalAnalysis():
326
  return fig
327
 
328
  if __name__ == '__main__':
329
- ticker = 'GOOG'
330
- df, fig = TechnicalAnalysis(ticker, plot_ta=True, savefig=True, debug=False).run()
331
- print(f'columns: {df.columns}')
 
 
 
332
 
333
 
 
1
  import datetime as dt
2
  import logging
3
 
4
+ import dotenv
5
  import matplotlib.pyplot as plt
6
  import matplotlib.dates as mdates
7
  from matplotlib.axes import Axes
 
12
  from ta.trend import MACD
13
  import yfinance as yf
14
 
15
+ from fetch_forecast import FetchForecast
16
 
17
  class TechnicalAnalysis():
18
  def __init__(
19
  self,
20
  ticker:str,
21
  fetchperiodinweeks:int=12,
22
+ df_past=None,
23
+ df_fcst=None,
24
  plot_ta:bool=True,
25
  savefig:bool=False,
26
  debug=False):
 
32
  stock ticker to analyze
33
  fetchperiodinweeks : int, optional, default: 8
34
  number of weeks to fetch historical data from YahooFinance
35
+ df_past: pd.DataFrame, optional, default: None
36
+ Closeing price of the ticker for the past few days
37
+ df_fcst: pd.DataFrame, optional, default: None
38
+ Forecasted closing price and relative returns nextf few days
39
  plot_ta : bool, optional, default: True
40
  whether to generate plots of technical analysis metrics. Plot will be created under plots/{ticker}.png
41
  debug : bool, optional, default: False
 
52
  # input arguments
53
  self.ticker = ticker
54
  self.fetchperiodinweeks = fetchperiodinweeks
55
+ self.df_past = df_past
56
+ self.df_fcst = df_fcst
57
  self.plot_ta = plot_ta
58
  self.savefig = savefig
59
  # done initializing
 
70
  - plots the price and TA metrics.
71
  """
72
  # fetch data from yf
73
+ df = self.fetch_data()
74
  # add the features based on technical analysis
75
+ if df.shape[0] > 0:
76
+ df = self.tech_analysis(df)
77
+ # Merge with forecast data
78
+ df_merged = self.merge_hist_with_forecast(df, self.df_past, self.df_fcst)
79
  # plot the results
80
  if self.plot_ta:
81
  os.makedirs('plots', exist_ok=True)
82
  fig = self.plot_stock_metrics(
83
+ df_merged,
84
  datasets={
85
+ 'Volume': ['Volume'],
 
86
  'Indices': ['RSI', 'StochOsc'],
87
+ 'Trend': ['MACD', 'MACDsig', 'MACDdif'],
88
+ 'Prices': ['Close', 'VWAP'] # 'High','Low',
89
+ },
90
  savefig=self.savefig
91
  )
92
  else:
 
97
  else:
98
  fig = None
99
 
100
+ return df_merged, fig
101
 
102
  def fetch_data(
103
  self
 
143
  return df
144
 
145
  def tech_analysis(
146
+ self,
147
+ df: pd.DataFrame
148
  ) -> pd.DataFrame:
149
  """
150
  Calculates technical analysis indicators for the fetched stock price data.
 
158
  - Trend Indicators:
159
  - Moving Average Convergence Divergence (MACD)
160
  The calculated indicators are added to the DataFrame as new columns.
161
+ Args:
162
+ df: pd.DataFrame
163
+ The DataFrame containing the fetched stock price data.
164
  Returns:
165
  pd.DataFrame
166
  The DataFrame with the calculated technical analysis indicators.
167
  """
 
168
 
169
  # Price Indicators
170
  # Volume-Weighted Average Price (VWAP)
 
204
 
205
  return df
206
 
207
+ def merge_hist_with_forecast(self, df_hist: pd.DataFrame, df_past: pd.DataFrame | None, df_fcst: pd.DataFrame | None) -> pd.DataFrame:
208
+ # make sure we are merging the right thing
209
+ """
210
+ Merge historical data with forecast data. If forecast data is available, merge it with historical data based on date.
211
+ If forecast data is not available, return the historical data as is.
212
+ Args:
213
+ df_hist: pd.DataFrame
214
+ Historical data
215
+ df_past: pd.DataFrame
216
+ Recent data used for comparison
217
+ df_fcst: pd.DataFrame or None
218
+ Forecast data
219
+ Returns:
220
+ df_merged: pd.DataFrame
221
+ Merged data
222
+ """
223
+ if df_fcst is not None:
224
+ # Make sure that the previous hist close price is matching to that of the past close price
225
+ assert df_hist.Close.iloc[-2] == df_past.Close.iloc[-2]
226
+ df_hist.reset_index(inplace=True)
227
+ # in case there are overlapping dates, make sure to remove them
228
+ df_fcst = df_fcst.loc[~df_fcst["Date"].isin(df_hist["Date"]), ["Date", "Close"]]
229
+ date_diff = df_fcst.Date.iloc[0] - df_hist.Date.iloc[-1]
230
+ if date_diff > pd.Timedelta('3 days'):
231
+ self.logger.warning(f'Date diff between the first forecast and the last hist is {date_diff}')
232
+ df_merged = pd.concat([df_hist, df_fcst], ignore_index=True)
233
+ df_merged.set_index("Date", inplace=True)
234
+ else:
235
+ df_merged = df
236
+ return df_merged
237
+
238
  def plot_stock_metrics(
239
  self,
240
  df,
241
+ datasets,
 
 
 
242
  savefig=False
243
  ) -> None:
244
  """
 
306
  'High' columns is added.
307
  """
308
 
309
+ self.logger.info(f'plotting {colstoplot} in {dataset}')
310
  colorcycle = ['black', 'blue', 'red', 'green', 'orange']
311
  for i, col in enumerate(colstoplot):
312
  ax.plot(
 
315
  color=colorcycle[i],
316
  label=col,
317
  linewidth=2)
318
+
319
+ if dataset in ['Index', 'Indices']:
320
+ ax.set_ylim([0, 100])
321
+ # Add a transparent shaded region between y=30 and y=70
322
+ ax.fill_between(df.index, 30, 70, color='gray', alpha=0.3, label='30-70 Range')
323
+ elif dataset in ['Price', 'Prices']:
324
+ # Add a transparent shaded region daily lows and highs
325
+ ax.fill_between(df.index, df['Low'], df['High'], color='gray', alpha=0.3, label='Price Range')
326
+ # extract the Price rows for which 'High's are NaN
327
+ nanind = df.High.isna()
328
+ df_fcst_close = df.loc[nanind, 'Close']
329
+ if df_fcst_close.shape[0] > 0:
330
+ ax.plot(
331
+ df_fcst_close.index,
332
+ df_fcst_close,
333
+ color='red',
334
+ marker='*',
335
+ label='Forecast')
336
+ # else:
337
+ # # plot a transparent line across the full df.index just to make sure that x-axis limits are identical for all panels
338
+ # ax.plot(df.index, df[col], color='gray', alpha=0.0)
339
+ # # ax.fill_between(df.index, 30, 70, color='gray', alpha=0.0)
340
+ ax.set_xlim([df.index.min()-pd.Timedelta(days=5), df.index.max()+pd.Timedelta(days=5)])
341
  # Format major ticks with year
342
  # Set major ticks (every Monday with labels)
343
  ax.xaxis.set_major_locator(mdates.WeekdayLocator(byweekday=mdates.MO))
 
345
  ax.xaxis.set_major_formatter(mdates.DateFormatter('%m.%d'))
346
  # Set minor ticks (every day, but without labels)
347
  ax.xaxis.set_minor_locator(mdates.DayLocator())
 
348
  plt.setp(ax.get_xticklabels(), rotation=90, ha='center')
349
+
350
+ ax.set_ylabel(dataset)
351
+
352
+ ax.grid(True, linestyle='--', alpha=0.7)
353
  ax.set_title(dataset)
354
  # ax.set_xlabel('Date')
 
355
  if len(colstoplot) > 1:
356
+ ax.legend(loc='upper left')
 
 
 
 
 
 
 
 
357
 
358
  def get_fetcherror_fig(
359
  self,
 
386
  return fig
387
 
388
  if __name__ == '__main__':
389
+ ticker = 'AAPL'
390
+ # fetch the forecasts
391
+ dotenv.load_dotenv(dotenv.find_dotenv())
392
+ df_past, df_fcst = FetchForecast(ticker).run()
393
+ df, fig = TechnicalAnalysis(ticker, df_past=df_past, df_fcst=df_fcst, plot_ta=True, savefig=True, debug=False).run()
394
+ # print(f'columns: {df.columns}')
395
 
396