fintic-tracker/stockTicker.py

361 lines
12 KiB
Python
Raw Normal View History

2021-04-26 18:51:21 +00:00
# Copyright (C) 2020 Daniel Richardson richardson.daniel@hotmail.co.uk
#
# This file is part of stockTicker project for justinodunn.
#
# stockTicker can not be copied and/or distributed without the express
# permission of Daniel Richardson
import sys, select
2021-04-26 18:51:21 +00:00
import os
import threading
from PIL import Image, ImageDraw, ImageFont
import time
import csv
import requests
2021-04-26 18:51:21 +00:00
2021-04-27 18:29:14 +00:00
from rgbmatrix import RGBMatrix, RGBMatrixOptions
import finnhub
2021-04-27 18:29:14 +00:00
def getInput(Block=False):
if Block or select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], []):
msg = sys.stdin.read(1)
#sys.stdin.flush()
else:
msg = ''
return msg
class StockTicker():
def __init__(self):
#Define global resources
self.symbols = []
self.ApiKey = "c24qddqad3ickpckgg80"
self.SandboxApiKey = "sandbox_c24qddqad3ickpckgg8g"
self.greenORred = (255, 255, 255)
#self.blank = Image.open('logos/blank.png')
self.blank = Image.new('RGB', (0, 32))
self.running = True
2021-04-27 18:29:14 +00:00
self.brightness = 1.0
self.delay = 0.02
2021-04-27 18:29:14 +00:00
# Configuration for the matrix
options = RGBMatrixOptions()
options.rows = 64
options.cols = 64
options.chain_length = 1
options.parallel = 1
options.hardware_mapping = 'adafruit-hat' # If you have an Adafruit HAT: 'adafruit-hat'
options.gpio_slowdown = 3
2021-04-27 18:29:14 +00:00
self.matrix = RGBMatrix(options = options)
self.finnhubClient = finnhub.Client(api_key=self.ApiKey)
2021-04-27 18:29:14 +00:00
def openImage(self, image_file):
image = Image.open(image_file)
# Make image fit our screen.
#image.thumbnail((self.matrix.width, self.matrix.height), Image.ANTIALIAS)
image = image.convert('RGB')
return image
def setImage(self, image, offset_x = 0, offset_y = 0, unsafe=False):
2021-04-27 18:29:14 +00:00
if (image.mode != "RGB"):
raise Exception("Currently, only RGB mode is supported for SetImage(). Please create images with mode 'RGB' or convert first with image = image.convert('RGB'). Pull requests to support more modes natively are also welcome :)")
if unsafe:
#In unsafe mode we directly acceshow to send commands to running python processs the underlying PIL image array
2021-04-27 18:29:14 +00:00
#in cython, which is considered unsafe pointer accecss,
#however it's super fast and seems to work fine
#https://groups.google.com/forum/#!topic/cython-users/Dc1ft5W6KM4
img_width, img_height = image.size
print('args', img_width, img_height, offset_x, offset_y, image)
2021-04-27 18:29:14 +00:00
self.matrix.SetPixelsPillow(offset_x, offset_y, img_width, img_height, image)
else:
# First implementation of a SetImage(). OPTIMIZE_ME: A more native
# implementation that directly reads the buffer and calls the underlying
# C functions can certainly be faster.
img_width, img_height = image.size
pixels = image.load()
for x in range(max(0, -offset_x), min(img_width, self.matrix.width - offset_x)):
for y in range(max(0, -offset_y), min(img_height, self.matrix.height - offset_y)):
(r, g, b) = pixels[x, y]
self.matrix.SetPixel(x + offset_x, y + offset_y, r*self.brightness, g*self.brightness, b*self.brightness)
def scrollImage(self, image_file, offset_x = 0, offset_y = 0):
2021-04-27 18:29:14 +00:00
image = self.openImage(image_file)
2021-04-27 18:59:25 +00:00
img_width, img_height = image.size
2021-04-27 18:29:14 +00:00
2021-04-27 18:59:25 +00:00
print(offset_x, offset_y)
while offset_x > -img_width:
offset_x -= 1
self.setImage(image, offset_x = offset_x, offset_y = offset_y)
2021-04-27 18:29:14 +00:00
time.sleep(delay)
2021-04-27 18:59:25 +00:00
def scrollImageTransition(self, image_files, offset_x = 0, offset_y = 0):
2021-04-27 18:59:25 +00:00
# use two image files and switch between them with a seemless transition
current_img = 1
#while True:
2021-04-27 18:59:25 +00:00
if current_img == 1:
image = self.openImage(image_files[0])
elif current_img == 2:
image = self.openImage(image_files[1])
img_width, img_height = image.size
2021-04-27 18:59:25 +00:00
while offset_x > -img_width:
offset_x -= 1
self.setImage(image, offset_x = offset_x, offset_y = offset_y)
time.sleep(self.delay)
try:
msg = getInput()
if msg == 'K':
self.resetMatrix()
break
elif msg == 's':
stock_ticker.delay = 0.04
elif msg == 'm':
stock_ticker.delay = 0.02
elif msg == 'f':
stock_ticker.delay = 0.01
elif msg in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']:
self.brightness = min(1.0, float(msg)/10 + 0.1)
except KeyboardInterrupt:
sys.stdout.flush()
pass
2021-04-27 18:59:25 +00:00
if current_img == 1:
current_img = 2
elif current_img == 2:
current_img = 1
offset_x = 0
2021-04-27 18:29:14 +00:00
#Get the logo from file that is the same as ticker name
def getLogo(self, Ticker):
try:
Logo = Image.open('logos/' + Ticker + '.png')
except:
print('Could not find logo for: ' + Ticker + ' using default')
Logo = Image.open('logos/default.png')
return Logo
#Using change between min and day price give appropriate arrow
#and set the overall change colour
def getArrow(self, CHANGE):
self.greenORred
if(CHANGE>0):
Arrow = Image.open('logos/up.png')
self.greenORred = (0, 255, 0)
return Arrow, CHANGE
Arrow = Image.open('logos/down.png')
self.greenORred = (255, 0, 0)
CHANGE = (CHANGE * -1)
2021-04-26 18:51:21 +00:00
return Arrow, CHANGE
def get_text_dimensions(self, text_string, font):
canvas = Image.new('RGB', (100,100))
draw = ImageDraw.Draw(canvas)
monospace = font
text = text_string
white = (255,255,255)
draw.text((10, 10), text, font=monospace, fill=white)
bbox = canvas.getbbox()
width = bbox[2]-bbox[0]
height = bbox[3]-bbox[1]
return width,height
#Draw Ticker, current and change onto one image
def textToImage(self, TICKER, CURRENT, CHANGE, ARROW):
2021-05-01 11:09:21 +00:00
font = ImageFont.load("./fonts/10x20.pil")
img = Image.new('RGB', (150, 32))
d = ImageDraw.Draw(img)
d.text((4, 0), TICKER, fill=(255, 255, 255), font=font)
d.text((4, 16), CURRENT, fill=self.greenORred, font=font)
text_width_current, text_height = self.get_text_dimensions(CURRENT, font)
img.paste(ARROW, ((text_width_current + 9),18))
d.text(((text_width_current+29), 16), CHANGE, fill=self.greenORred, font=font)
text_width_change, text_height = self.get_text_dimensions(CHANGE, font)
newWidth = (text_width_current+29) + (text_width_change)
img.crop((0,0,newWidth,32))
return img
#Stitch the logo & prices picture into one image
def stitchImage(self, image_list):
widths, heights = zip(*(i.size for i in image_list))
total_width = sum(widths)
max_height = max(heights)
new_im = Image.new('RGB', (total_width, max_height))
x_offset = 0
for im in image_list:
new_im.paste(im, (x_offset,0))
x_offset += im.size[0]
print('width:', im.size[0])
return new_im
#Get current prices and day prices for the ticker then format the response into an array
def getStockPrices(self, symbols):
apiCalledError = False
stock_info = []
try:
quotes = [self.finnhubClient.quote(symbol) for symbol in symbols]
current_prices = [quote['c'] for quote in quotes]
opening_prices = [quote['o'] for quote in quotes]
for i, symbol in enumerate(symbols):
stock_info.append([symbol, current_prices[i], opening_prices[i]])
except Exception as e:
print("Could not fetch data - API CALLS REACHED? - Will display old image")
print(e)
apiCalledError = True
return stock_info, apiCalledError
def resetMatrix(self):
for x in range(self.matrix.width):
for y in range(self.matrix.height):
self.matrix.SetPixel(x , y , 0,0,0)
#Connect all the pieces togeather creating 1 long final stock image
def getFullStockImage(self):
image_list = []
start = time.time()
stock_info, apiCalledError = self.getStockPrices(self.symbols)
if (apiCalledError == False):
for i in range(len(stock_info)):
change = float(stock_info[i][1])-float(stock_info[i][2]) #TEXT
ticker = stock_info[i][0] #TEXT
current = '%.2f' % float(stock_info[i][1]) #TEXT
logo = self.getLogo(stock_info[i][0])
arrow, change = self.getArrow(change)
change = '%.2f' % change
midFrame = self.textToImage(ticker, current, change, arrow) #IMAGE THE TEXT
stitchedStock = self.stitchImage([logo,midFrame])
image_list.append(self.blank)
image_list.append(stitchedStock)
print('elapsed time loop ended: ' + str((time.time()-start)))
if (apiCalledError == False):
finalDisplayImage = self.stitchImage(image_list)
finalDisplayImage.save('final.ppm')
#Send the final stitched image to the display for set amount of time
def displayMatrix(self):
#os.system("sudo ./demo -D1 final.ppm -t " + str(displayTime) +" -m "+ str(speedDisplay) +" --led-gpio-mapping=adafruit-hat --led-rows=32 --led-cols=256")
#os.system("sudo ./demo -D1 final.ppm -t " + str(self.displayTime) +" -m "+ str(self.speedDisplay) +" --led-gpio-mapping=adafruit-hat --led-rows=64 --led-cols=64 --led-slowdown-gpio=4 ")
self.scrollImageTransition(['final.ppm', 'final.ppm'], offset_x = 0, offset_y = 0)
#Retrieve symbols from the CSV file
def getSymbols(self):
2021-04-26 18:51:21 +00:00
self.symbols = []
CSV = csv.reader(open('csv/tickers.csv', 'r'))
for row in CSV:
print(row)
self.symbols.append(row[0])
print(self.symbols)
#Main run definition called by server
def runStockTicker(self, runtime, delay, speedtime):
self.getSymbols()
self.GetfullStockImage()
self.keySwapper += 1
self.running = True
2021-04-27 18:29:14 +00:00
while (True):
if (self.running == True):
#th = threading.Thread(target=self.displayMatrix)
#th.start()
self.displayMatrix()
time.sleep((int(runtime) - int(delay)))
self.getfullStockImage()
#th.join()
2021-04-26 18:51:21 +00:00
else:
break;
#Change running to false stopping refresh at next checkpoint
def stopStockTicker(self):
self.keySwapper = 0
self.running = False
print('MATRIX DISPLAY STOP CALLED')
if __name__ == '__main__':
#print(sys.stdin.readlines())
stock_ticker = StockTicker()
stock_ticker.getSymbols()
#stock_ticker.getFullStockImage()
#stock_ticker.displayMatrix()
while True:
msg = getInput()
if msg == 'S':
stock_ticker.getFullStockImage()
stock_ticker.displayMatrix()
elif msg == 's':
stock_ticker.delay = 0.04
elif msg == 'm':
stock_ticker.delay = 0.02
elif msg == 'f':
stock_ticker.delay = 0.01
elif msg in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']:
stock_ticker.brightness = min(1.0, float(msg)/10 + 0.1)