Merge pull request #196 from worstface/main
Hotfix for inkycal stocks module
This commit is contained in:
		@@ -1,16 +1,23 @@
 | 
				
			|||||||
#!/usr/bin/python3
 | 
					#!/usr/bin/python3
 | 
				
			||||||
# -*- coding: utf-8 -*-
 | 
					# -*- 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.3: Added support for web-UI of Inkycal 2.0.0
 | 
				
			||||||
Version 0.2: Migration to Inkycal 2.0.0
 | 
					Version 0.2: Migration to Inkycal 2.0.0
 | 
				
			||||||
Version 0.1: Migration to Inkycal 2.0.0b
 | 
					Version 0.1: Migration to Inkycal 2.0.0b
 | 
				
			||||||
 | 
					
 | 
				
			||||||
by https://github.com/worstface
 | 
					by https://github.com/worstface
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import logging
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from inkycal.modules.template import inkycal_module
 | 
					from inkycal.modules.template import inkycal_module
 | 
				
			||||||
from inkycal.custom import *
 | 
					from inkycal.custom import write, internet_available
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from PIL import Image
 | 
				
			||||||
 | 
					
 | 
				
			||||||
try:
 | 
					try:
 | 
				
			||||||
  import yfinance as yf
 | 
					  import yfinance as yf
 | 
				
			||||||
@@ -18,8 +25,14 @@ except ImportError:
 | 
				
			|||||||
  print('yfinance is not installed! Please install with:')
 | 
					  print('yfinance is not installed! Please install with:')
 | 
				
			||||||
  print('pip3 install yfinance')
 | 
					  print('pip3 install yfinance')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
filename = os.path.basename(__file__).split('.py')[0]
 | 
					try:
 | 
				
			||||||
logger = logging.getLogger(filename)
 | 
					  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):
 | 
					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
 | 
					    # If tickers is a string from web-ui, convert to a list, else use
 | 
				
			||||||
    # tickers as-is i.e. for tests
 | 
					    # tickers as-is i.e. for tests
 | 
				
			||||||
    if config['tickers'] and isinstance(config['tickers'], str):
 | 
					    if config['tickers'] and isinstance(config['tickers'], str):
 | 
				
			||||||
      self.tickers = config['tickers'].split(',') #returns list
 | 
					      self.tickers = config['tickers'].replace(" ", "").split(',') #returns list
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
      self.tickers = config['tickers']
 | 
					      self.tickers = config['tickers']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # give an OK message
 | 
					    # give an OK message
 | 
				
			||||||
    print(f'{filename} loaded')
 | 
					    print(f'{__name__} loaded')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def generate_image(self):
 | 
					  def generate_image(self):
 | 
				
			||||||
    """Generate image for this module"""
 | 
					    """Generate image for this module"""
 | 
				
			||||||
@@ -60,12 +73,22 @@ class Stocks(inkycal_module):
 | 
				
			|||||||
    im_width = int(self.width - (2 * self.padding_left))
 | 
					    im_width = int(self.width - (2 * self.padding_left))
 | 
				
			||||||
    im_height = int(self.height - (2 * self.padding_top))
 | 
					    im_height = int(self.height - (2 * self.padding_top))
 | 
				
			||||||
    im_size = im_width, im_height
 | 
					    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)
 | 
					    # Create an image for black pixels and one for coloured pixels (required)
 | 
				
			||||||
    im_black = Image.new('RGB', size = im_size, color = 'white')
 | 
					    im_black = Image.new('RGB', size = im_size, color = 'white')
 | 
				
			||||||
    im_colour = 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
 | 
					    # Check if internet is available
 | 
				
			||||||
    if internet_available() == True:
 | 
					    if internet_available() == True:
 | 
				
			||||||
      logger.info('Connection test passed')
 | 
					      logger.info('Connection test passed')
 | 
				
			||||||
@@ -91,36 +114,136 @@ class Stocks(inkycal_module):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    parsed_tickers = []
 | 
					    parsed_tickers = []
 | 
				
			||||||
    parsed_tickers_colour = []
 | 
					    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}...')
 | 
					      logger.info(f'preparing data for {ticker}...')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      yfTicker = yf.Ticker(ticker)
 | 
					      yfTicker = yf.Ticker(ticker)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      try:
 | 
					      try:
 | 
				
			||||||
        stockInfo = yfTicker.info
 | 
					        stockInfo = yfTicker.info
 | 
				
			||||||
 | 
					      except Exception as exceptionMessage:
 | 
				
			||||||
 | 
					        logger.warning(f"Failed to get '{ticker}' ticker info: {exceptionMessage}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      try:
 | 
				
			||||||
        stockName = stockInfo['shortName']
 | 
					        stockName = stockInfo['shortName']
 | 
				
			||||||
      except Exception:
 | 
					      except Exception:
 | 
				
			||||||
        stockName = ticker
 | 
					        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.")
 | 
					                       "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])
 | 
					      previousQuote = (stockHistory.tail(2)['Close'].iloc[0])
 | 
				
			||||||
      currentQuote = (stockHistory.tail(1)['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
 | 
					      currentGain = currentQuote-previousQuote
 | 
				
			||||||
      currentGainPercentage = (1-currentQuote/previousQuote)*-100
 | 
					      currentGainPercentage = (1-currentQuote/previousQuote)*-100
 | 
				
			||||||
 | 
					      firstQuote = stockHistory.tail(stockHistoryLen)['Close'].iloc[0]
 | 
				
			||||||
 | 
					      logger.info(f'firstQuote {firstQuote} ...')
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
      tickerLine = '{}: {:.2f} {:+.2f} ({:+.2f}%)'.format(
 | 
					      def floatStr(precision, number):
 | 
				
			||||||
        stockName, currentQuote, currentGain, currentGainPercentage)
 | 
					        return "%0.*f" % (precision, number)
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
      logger.info(tickerLine)
 | 
					      def percentageStr(number):
 | 
				
			||||||
      parsed_tickers.append(tickerLine)
 | 
					        return '({:+.2f}%)'.format(number)
 | 
				
			||||||
      
 | 
					      
 | 
				
			||||||
 | 
					      def gainStr(precision, number):      
 | 
				
			||||||
 | 
					        return "%+.*f" % (precision, number)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      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(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:
 | 
					      if currentGain < 0:
 | 
				
			||||||
        parsed_tickers_colour.append(tickerLine)
 | 
					        parsed_tickers_colour.append(stockCurrentValueLine)
 | 
				
			||||||
      else:
 | 
					      else:
 | 
				
			||||||
        parsed_tickers_colour.append("")
 | 
					        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
 | 
					    # Write/Draw something on the black image
 | 
				
			||||||
    for _ in range(len(parsed_tickers)):
 | 
					    for _ in range(len(parsed_tickers)):
 | 
				
			||||||
@@ -142,4 +265,4 @@ class Stocks(inkycal_module):
 | 
				
			|||||||
    return im_black, im_colour
 | 
					    return im_black, im_colour
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if __name__ == '__main__':
 | 
					if __name__ == '__main__':
 | 
				
			||||||
  print(f'running {filename} in standalone/debug mode')
 | 
					  print('running module in standalone/debug mode')
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,4 +8,4 @@ arrow==0.17.0 	                # time operations
 | 
				
			|||||||
Flask==1.1.2                    # webserver
 | 
					Flask==1.1.2                    # webserver
 | 
				
			||||||
Flask-WTF==0.14.3               # webforms
 | 
					Flask-WTF==0.14.3               # webforms
 | 
				
			||||||
todoist-python==8.1.2           # todoist api
 | 
					todoist-python==8.1.2           # todoist api
 | 
				
			||||||
yfinance==0.1.55                # yahoo stocks
 | 
					yfinance>=0.1.62                # yahoo stocks
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								setup.py
									
									
									
									
									
								
							@@ -23,7 +23,7 @@ __install_requires__ = ['pyowm==3.1.1',                   # weather
 | 
				
			|||||||
                        'Flask==1.1.2',                   # webserver
 | 
					                        'Flask==1.1.2',                   # webserver
 | 
				
			||||||
                        'Flask-WTF==0.14.3',              # webforms
 | 
					                        'Flask-WTF==0.14.3',              # webforms
 | 
				
			||||||
                        'todoist-python==8.1.2',          # todoist api
 | 
					                        'todoist-python==8.1.2',          # todoist api
 | 
				
			||||||
                        'yfinance==0.1.55',               # yahoo stocks
 | 
					                        'yfinance>=0.1.62',               # yahoo stocks
 | 
				
			||||||
                        ]
 | 
					                        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__classifiers__ = [
 | 
					__classifiers__ = [
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user