diff --git a/inkycal/modules/inkycal_stocks.py b/inkycal/modules/inkycal_stocks.py index 872fdf8..2d623e9 100644 --- a/inkycal/modules/inkycal_stocks.py +++ b/inkycal/modules/inkycal_stocks.py @@ -1,16 +1,23 @@ #!/usr/bin/python3 # -*- coding: utf-8 -*- """ -Stocks Module for Inky-Calendar Project +Stocks Module for Inkycal Project +Version 0.5: Added improved precision by using new priceHint parameter of yfinance +Version 0.4: Added charts Version 0.3: Added support for web-UI of Inkycal 2.0.0 Version 0.2: Migration to Inkycal 2.0.0 Version 0.1: Migration to Inkycal 2.0.0b by https://github.com/worstface """ +import os +import logging + from inkycal.modules.template import inkycal_module -from inkycal.custom import * +from inkycal.custom import write, internet_available + +from PIL import Image try: import yfinance as yf @@ -18,8 +25,14 @@ except ImportError: print('yfinance is not installed! Please install with:') print('pip3 install yfinance') -filename = os.path.basename(__file__).split('.py')[0] -logger = logging.getLogger(filename) +try: + import matplotlib.pyplot as plt + import matplotlib.image as mpimg +except ImportError: + print('matplotlib is not installed! Please install with:') + print('pip3 install matplotlib') + +logger = logging.getLogger(__name__) class Stocks(inkycal_module): @@ -46,12 +59,12 @@ class Stocks(inkycal_module): # If tickers is a string from web-ui, convert to a list, else use # tickers as-is i.e. for tests if config['tickers'] and isinstance(config['tickers'], str): - self.tickers = config['tickers'].split(',') #returns list + self.tickers = config['tickers'].replace(" ", "").split(',') #returns list else: self.tickers = config['tickers'] # give an OK message - print(f'{filename} loaded') + print(f'{__name__} loaded') def generate_image(self): """Generate image for this module""" @@ -60,12 +73,22 @@ class Stocks(inkycal_module): im_width = int(self.width - (2 * self.padding_left)) im_height = int(self.height - (2 * self.padding_top)) im_size = im_width, im_height - logger.info(f'Image size: {im_size}') + logger.info(f'image size: {im_width} x {im_height} px') # Create an image for black pixels and one for coloured pixels (required) im_black = Image.new('RGB', size = im_size, color = 'white') im_colour = Image.new('RGB', size = im_size, color = 'white') + # Create tmp path + tmpPath = '/tmp/inkycal_stocks/' + + try: + os.mkdir(tmpPath) + except OSError: + print (f"Creation of tmp directory {tmpPath} failed") + else: + print (f"Successfully created tmp directory {tmpPath} ") + # Check if internet is available if internet_available() == True: logger.info('Connection test passed') @@ -91,36 +114,136 @@ class Stocks(inkycal_module): parsed_tickers = [] parsed_tickers_colour = [] + chartSpace = Image.new('RGBA', (im_width, im_height), "white") + chartSpace_colour = Image.new('RGBA', (im_width, im_height), "white") - for ticker in self.tickers: + tickerCount = range(len(self.tickers)) + + for _ in tickerCount: + ticker = self.tickers[_] logger.info(f'preparing data for {ticker}...') yfTicker = yf.Ticker(ticker) try: stockInfo = yfTicker.info + except Exception as exceptionMessage: + logger.warning(f"Failed to get '{ticker}' ticker info: {exceptionMessage}") + + try: stockName = stockInfo['shortName'] except Exception: stockName = ticker - logger.warning(f"Failed to get '{stockName}' ticker info! Using " + logger.warning(f"Failed to get '{stockName}' ticker name! Using " "the ticker symbol as name instead.") - stockHistory = yfTicker.history("2d") + try: + stockCurrency = stockInfo['currency'] + if stockCurrency == 'USD': + stockCurrency = '$' + elif stockCurrency == 'EUR': + stockCurrency = '€' + except Exception: + stockCurrency = '' + logger.warning(f"Failed to get ticker currency!") + + try: + precision = stockInfo['priceHint'] + except Exception: + precision = 2 + logger.warning(f"Failed to get '{stockName}' ticker price hint! Using " + "default precision of 2 instead.") + + stockHistory = yfTicker.history("30d") + stockHistoryLen = len(stockHistory) + logger.info(f'fetched {stockHistoryLen} datapoints ...') previousQuote = (stockHistory.tail(2)['Close'].iloc[0]) currentQuote = (stockHistory.tail(1)['Close'].iloc[0]) + currentHigh = (stockHistory.tail(1)['High'].iloc[0]) + currentLow = (stockHistory.tail(1)['Low'].iloc[0]) + currentOpen = (stockHistory.tail(1)['Open'].iloc[0]) currentGain = currentQuote-previousQuote currentGainPercentage = (1-currentQuote/previousQuote)*-100 + firstQuote = stockHistory.tail(stockHistoryLen)['Close'].iloc[0] + logger.info(f'firstQuote {firstQuote} ...') + + def floatStr(precision, number): + return "%0.*f" % (precision, number) + + def percentageStr(number): + return '({:+.2f}%)'.format(number) + + def gainStr(precision, number): + return "%+.*f" % (precision, number) - tickerLine = '{}: {:.2f} {:+.2f} ({:+.2f}%)'.format( - stockName, currentQuote, currentGain, currentGainPercentage) + stockNameLine = '{} ({})'.format(stockName, stockCurrency) + stockCurrentValueLine = '{} {} {}'.format( + floatStr(precision, currentQuote), gainStr(precision, currentGain), percentageStr(currentGainPercentage)) + stockDayValueLine = '1d OHL: {}/{}/{}'.format( + floatStr(precision, currentOpen), floatStr(precision, currentHigh), floatStr(precision, currentLow)) + maxQuote = max(stockHistory.High) + minQuote = min(stockHistory.Low) + logger.info(f'high {maxQuote} low {minQuote} ...') + stockMonthValueLine = '{}d OHL: {}/{}/{}'.format( + stockHistoryLen,floatStr(precision, firstQuote),floatStr(precision, maxQuote),floatStr(precision, minQuote)) - logger.info(tickerLine) - parsed_tickers.append(tickerLine) + logger.info(stockNameLine) + logger.info(stockCurrentValueLine) + logger.info(stockDayValueLine) + logger.info(stockMonthValueLine) + parsed_tickers.append(stockNameLine) + parsed_tickers.append(stockCurrentValueLine) + parsed_tickers.append(stockDayValueLine) + parsed_tickers.append(stockMonthValueLine) + parsed_tickers_colour.append("") if currentGain < 0: - parsed_tickers_colour.append(tickerLine) + parsed_tickers_colour.append(stockCurrentValueLine) else: parsed_tickers_colour.append("") + if currentOpen > currentQuote: + parsed_tickers_colour.append(stockDayValueLine) + else: + parsed_tickers_colour.append("") + if firstQuote > currentQuote: + parsed_tickers_colour.append(stockMonthValueLine) + else: + parsed_tickers_colour.append("") + + if (_ < len(tickerCount)): + parsed_tickers.append("") + parsed_tickers_colour.append("") + + logger.info(f'creating chart data...') + chartData = stockHistory.reset_index() + chartCloseData = chartData.loc[:,'Close'] + chartTimeData = chartData.loc[:,'Date'] + + logger.info(f'creating chart plot...') + fig, ax = plt.subplots() # Create a figure containing a single axes. + ax.plot(chartTimeData, chartCloseData, linewidth=8) # Plot some data on the axes. + ax.set_xticklabels([]) + ax.set_yticklabels([]) + chartPath = tmpPath+ticker+'.png' + logger.info(f'saving chart image to {chartPath}...') + plt.savefig(chartPath) + + logger.info(f'chartSpace is...{im_width} {im_height}') + logger.info(f'open chart ...{chartPath}') + chartImage = Image.open(chartPath) + chartImage.thumbnail((im_width/4,line_height*4), Image.BICUBIC) + + chartPasteX = im_width-(chartImage.width) + chartPasteY = line_height*5*_ + logger.info(f'pasting chart image with index {_} to...{chartPasteX} {chartPasteY}') + + if firstQuote > currentQuote: + chartSpace_colour.paste(chartImage, (chartPasteX, chartPasteY)) + else: + chartSpace.paste(chartImage, (chartPasteX, chartPasteY)) + + im_black.paste(chartSpace) + im_colour.paste(chartSpace_colour) # Write/Draw something on the black image for _ in range(len(parsed_tickers)): @@ -142,4 +265,4 @@ class Stocks(inkycal_module): return im_black, im_colour if __name__ == '__main__': - print(f'running {filename} in standalone/debug mode') + print('running module in standalone/debug mode') diff --git a/requirements.txt b/requirements.txt index 81ae15b..17db6a5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,4 +8,4 @@ arrow==0.17.0 # time operations Flask==1.1.2 # webserver Flask-WTF==0.14.3 # webforms todoist-python==8.1.2 # todoist api -yfinance==0.1.55 # yahoo stocks +yfinance>=0.1.62 # yahoo stocks diff --git a/setup.py b/setup.py index e9ff5b6..481e8f7 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ __install_requires__ = ['pyowm==3.1.1', # weather 'Flask==1.1.2', # webserver 'Flask-WTF==0.14.3', # webforms 'todoist-python==8.1.2', # todoist api - 'yfinance==0.1.55', # yahoo stocks + 'yfinance>=0.1.62', # yahoo stocks ] __classifiers__ = [