Fix cameras, improve weather, delegate health renderer to a helper,
[kiosk.git] / stock_renderer.py
index f8491e6a9eb73b8ba06ea35ffd1eb96f5ca0b5c2..a8a1571c10336c11d9e9480e9e8b058c9371baaf 100644 (file)
-from bs4 import BeautifulSoup
-from threading import Thread
-import datetime
-import file_writer
-import json
-import re
-import renderer
-import random
-import secrets
-import time
-import urllib.request, urllib.error, urllib.parse
-
-class stock_quote_renderer(renderer.debuggable_abstaining_renderer):
-    # format exchange:symbol
-    def __init__(self, name_to_timeout_dict, symbols):
-        super(stock_quote_renderer, self).__init__(name_to_timeout_dict, False)
-        self.symbols = symbols
-        self.prefix = "https://www.alphavantage.co/query?"
-        self.thread = None
-
-    def debug_prefix(self):
-        return "stock"
-
-    def get_random_key(self):
-        return random.choice(secrets.alphavantage_keys)
-
-    def periodic_render(self, key):
-        now = datetime.datetime.now()
-        if (now.hour < (9 - 3) or
-            now.hour >= (17 - 3) or
-            datetime.datetime.today().weekday() > 4):
-            self.debug_print("The stock market is closed so not re-rendering")
-            return True
-
-        if (self.thread is None or not self.thread.is_alive()):
-            self.debug_print("Spinning up a background thread...")
-            self.thread = Thread(target = self.thread_internal_render, args=())
-            self.thread.start()
-        return True
-
-    def thread_internal_render(self):
-        symbols_finished = 0
-        f = file_writer.file_writer('stock_3_86400.html')
-        f.write("<H1>Stock Quotes</H1><HR>")
-        f.write("<TABLE WIDTH=99%>")
-        for symbol in self.symbols:
-#            print "---------- Working on %s\n" % symbol
-
-            # https://www.alphavantage.co/query?function=TIME_SERIES_INTRADAY&symbol=MSFT&interval=5min&apikey=<key>
-
-            # https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=MSFT&apikey=<key>
+#!/usr/bin/env python3
 
-            attempts = 0
-            cooked = ""
-            while True:
-                key = self.get_random_key()
-                url = self.prefix + "function=GLOBAL_QUOTE&symbol=%s&apikey=%s" % (symbol, key)
-                raw = urllib.request.urlopen(url).read()
-                cooked = json.loads(raw)
-                if 'Global Quote' not in cooked:
-#                    print "%s\n" % cooked
-                    print("Failure %d, sleep %d sec...\n" % (attempts + 1,
-                                                             2 ** attempts))
-                    time.sleep(2 ** attempts)
-                    attempts += 1
-                    if attempts > 10: # we'll wait up to 512 seconds per symbol
-                        break
-                else:
-                    break
+import logging
+from typing import Dict, List, Optional, Tuple
 
-            # These fuckers...
-            if 'Global Quote' not in cooked:
-                print("Can't get data for symbol %s: %s\n" % (
-                    symbol, raw))
-                continue
-            cooked = cooked['Global Quote']
+import yfinance as yf  # type: ignore
 
-            # {
-            #   u'Global Quote':
-            #     {
-            #       u'01. symbol': u'MSFT',
-            #       u'02. open': u'151.2900',
-            #       u'03. high': u'151.8900',
-            #       u'04. low': u'150.7650',
-            #       u'05. price': u'151.1300',
-            #       u'06. volume': u'16443559',
-            #       u'07. latest trading day': u'2019-12-10',
-            #       u'08. previous close': u'151.3600',
-            #       u'09. change': u'-0.2300'
-            #       u'10. change percent': u'-0.1520%',
-            #     }
-            # }
+import file_writer
+import renderer
 
-            price = "?????"
-            if '05. price' in cooked:
-                price = cooked['05. price']
-                price = price[:-2]
 
-            percent_change = "?????"
-            if '10. change percent' in cooked:
-                percent_change = cooked['10. change percent']
-                if not '-' in percent_change:
-                    percent_change = "+" + percent_change
+logger = logging.getLogger(__file__)
 
-            change = "?????"
-            cell_color = "#bbbbbb"
-            if '09. change' in cooked:
-                change = cooked['09. change']
-                if "-" in change:
-                    cell_color = "#b00000"
-                else:
-                    cell_color = "#009000"
-                change = change[:-2]
 
-            if symbols_finished % 4 == 0:
-                if (symbols_finished > 0):
-                    f.write("</TR>")
-                f.write("<TR>")
-            symbols_finished += 1
+class stock_quote_renderer(renderer.abstaining_renderer):
+    """Render the stock prices page."""
 
-            f.write("""
-<TD WIDTH=20%% HEIGHT=150 BGCOLOR="%s">
+    def __init__(
+            self,
+            name_to_timeout_dict: Dict[str, int],
+            symbols: List[str],
+            display_subs: Dict[str, str] = None,
+    ) -> None:
+        super().__init__(name_to_timeout_dict)
+        self.symbols = symbols
+        self.display_subs = display_subs
+
+    @staticmethod
+    def get_ticker_name(ticker: yf.ticker.Ticker) -> str:
+        """Get friendly name of a ticker."""
+        info = ticker.get_info()
+        if "shortName" in info:
+            return info["shortName"]
+        return ticker
+
+    @staticmethod
+    def get_price(ticker: yf.ticker.Ticker) -> Optional[float]:
+        """Get most recent price of a ticker."""
+        keys = [
+            "bid",
+            "ask",
+            "regularMarketPrice",
+            "lastMarket",
+            "open",
+            "previousClose",
+        ]
+        info = ticker.get_info()
+        for key in keys:
+            if key in info and info[key] is not None and info[key] != 0.0:
+                print(f"Price: picked {key}, ${info[key]}.")
+                return float(info[key])
+        return None
+
+    @staticmethod
+    def get_change_and_delta(
+        ticker: yf.ticker.Ticker, price: float
+    ) -> Tuple[float, float]:
+        """Given the current price, look up opening price and compute delta."""
+        keys = [
+            "previousClose",
+            "open",
+        ]
+        info = ticker.get_info()
+        for key in keys:
+            if key in info and info[key] is not None:
+                print(f"Change: picked {key}, ${info[key]}.")
+                old_price = float(info[key])
+                delta = price - old_price
+                return (delta / old_price * 100.0, delta)
+        return (0.0, 0.0)
+
+    def periodic_render(self, key: str) -> bool:
+        """Write an up-to-date stock page."""
+        with file_writer.file_writer("stock_3_86400.html") as f:
+            f.write("<H1>Stock Quotes</H1><HR>")
+            f.write("<TABLE WIDTH=99%>")
+            symbols_finished = 0
+            for symbol in self.symbols:
+                ticker = yf.Ticker(symbol)
+                # print(ticker.get_info())
+                if ticker is None:
+                    logger.debug(f"Unknown symbol {symbol} -- ignored.")
+                    continue
+                name = stock_quote_renderer.get_ticker_name(ticker)
+                price = stock_quote_renderer.get_price(ticker)
+                if price is None:
+                    logger.debug(f"No price information for {symbol} -- skipped.")
+                    continue
+                (percent_change, delta) = stock_quote_renderer.get_change_and_delta(
+                    ticker, price
+                )
+                # print(f"delta: {delta}, change: {percent_change}")
+                cell_color = "#b00000" if percent_change < 0 else "#009000"
+                if symbols_finished % 4 == 0:
+                    if symbols_finished > 0:
+                        f.write("</TR>")
+                        f.write("<TR>")
+                symbols_finished += 1
+                if self.display_subs is not None and symbol in self.display_subs:
+                    symbol = self.display_subs[symbol]
+                f.write(
+                    f"""
+<TD WIDTH=20% HEIGHT=150 BGCOLOR="{cell_color}">
   <!-- Container -->
   <DIV style="position:relative;
               height:150px;">
-    <!-- Symbol -->
+    <!-- Symbol {symbol} -->
     <DIV style="position:absolute;
                 bottom:50;
                 right:-20;
@@ -133,29 +114,25 @@ class stock_quote_renderer(renderer.debuggable_abstaining_renderer):
                 font-weight:900;
                 -webkit-text-stroke: 2px black;
                 color: #ddd">
-      %s
+      {symbol}
     </DIV>
-    <!-- Current price, Change today and percent change today -->
+    <!-- Current price, Change today and percent change today, name -->
     <DIV style="position:absolute;
                 left:10;
                 top:20;
                 font-size:23pt;
                 font-family: helvetica, arial, sans-serif;
-                width:70%%">
-            $%s<BR>
-            <I>(%s)</I><BR>
-            <B>$%s</B>
+                width:70%">
+            ${price:.2f}<BR>
+            <I>({percent_change:.1f}%)</I><BR>
+            <B>${delta:.2f}</B>
     </DIV>
   </DIV>
-</TD>""" % (cell_color,
-            symbol,
-            price,
-            percent_change,
-            change))
-        f.write("</TR></TABLE>")
-        f.close()
+</TD>"""
+                )
+            f.write("</TR></TABLE>")
         return True
 
-#x = stock_quote_renderer({}, ["MSFT", "GOOG", "GOOGL", "OPTAX", "VNQ"])
-#x.periodic_render(None)
+# Test
+#x = stock_quote_renderer({}, ["MSFT", "GOOG", "BTC-USD", "OPTAX", "GC=F", "VNQ"], { "BTC-USD": "BTC", "GC=F": "GOLD" })
 #x.periodic_render(None)