This is the current running state of the kiosk sans the secrets.
[kiosk.git] / stock_renderer.py
index 40ced0fb94a85fab390ee086ca6cdae2a78bfdd9..60e70432ccbbbfee620119dc32b718d407222da8 100644 (file)
@@ -1,11 +1,14 @@
 #!/usr/bin/env python3
 
 import logging
-from typing import Dict, List, Optional, Tuple
+import os
+from typing import Any, Dict, List, Optional, Tuple
 
 import yfinance as yf  # type: ignore
+import plotly.graph_objects as go
 
 import file_writer
+import kiosk_constants
 import renderer
 
 
@@ -24,50 +27,117 @@ class stock_quote_renderer(renderer.abstaining_renderer):
         super().__init__(name_to_timeout_dict)
         self.symbols = symbols
         self.display_subs = display_subs
+        self.info_cache: Dict[yf.ticker.Ticker, Dict] = {}
 
-    @staticmethod
-    def get_ticker_name(ticker: yf.ticker.Ticker) -> str:
+    def cache_info(self, ticker: yf.ticker.Ticker) -> Dict:
+        if ticker in self.info_cache:
+            return self.info_cache[ticker]
+        i = ticker.get_info()
+        self.info_cache[ticker] = i
+        return i
+
+    def get_ticker_name(self, ticker: yf.Ticker) -> str:
         """Get friendly name of a ticker."""
-        info = ticker.get_info()
+        info = self.cache_info(ticker)
         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()
+    def get_item_from_dict(keys: List[str], dictionary: Dict[str, Any]) -> Optional[Any]:
+        result = None
         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])
+            result = dictionary.get(key, None)
+            if result:
+                return result
         return None
 
+    def get_price(self, ticker: yf.Ticker) -> Optional[float]:
+        """Get most recent price of a ticker."""
+
+        # First try fast_info
+        price = stock_quote_renderer.get_item_from_dict(
+            ['last_price',
+             'open',
+             'previous_close'],
+            ticker.fast_info)
+        if price:
+            return price
+
+        # Next try info
+        price = stock_quote_renderer.get_item_from_dict(
+            ['bid',
+             'ask',
+             'lastMarket'],
+            self.cache_info(ticker))
+        if price:
+            return price
+
+        # Finally, fall back on history
+        hist = ticker.history(period="1d").to_dict()['Close']
+        latest = None
+        latest_price = None
+        for k, v in hist.items():
+            if latest is None or k > latest:
+                price = hist[k]
+                if price is not None:
+                    latest = k
+                    latest_price = price
+        print(f"Price: fell back on latest close {latest_price} at {latest}")
+        return latest_price
+
     @staticmethod
+    def make_chart(symbol: str, ticker: yf.Ticker, period: str) -> str:
+        base_filename = f'stock_chart_{symbol}.png'
+        output_filename = os.path.join(kiosk_constants.pages_dir, base_filename)
+        transparent = go.Layout(
+            paper_bgcolor='rgba(0,0,0,0)',
+            plot_bgcolor='rgba(0,0,0,0)',
+            xaxis_rangeslider_visible=False,
+        )
+        hist = ticker.history(period=period, interval="1wk")
+        chart = go.Figure(
+            data=go.Candlestick(
+                open=hist['Open'],
+                high=hist['High'],
+                low=hist['Low'],
+                close=hist['Close'],
+            ),
+            layout=transparent,
+        )
+        chart.update_xaxes(visible=False, showticklabels=False)
+        chart.update_yaxes(side="right")
+        chart.write_image(output_filename, format="png", width=600, height=350)
+        print(f"Write {output_filename}...")
+        return base_filename
+
+    def get_last_close(
+            self,
+            ticker: yf.Ticker
+    ) -> float:
+        last_close = stock_quote_renderer.get_item_from_dict(
+            ['previous_close',
+             'open'],
+            ticker.fast_info)
+        if last_close:
+            return last_close
+
+        last_close = stock_quote_renderer.get_item_from_dict(
+            ['preMarketPrice'],
+            self.cache_info(ticker))
+        if last_close:
+            return last_close
+        return self.get_price(ticker)
+
     def get_change_and_delta(
-        ticker: yf.ticker.Ticker, price: float
+            self,
+            ticker: yf.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)
+        last_price = self.get_last_close(ticker)
+        delta = price - last_price
+        return (delta / last_price * 100.0, delta)
 
     def periodic_render(self, key: str) -> bool:
         """Write an up-to-date stock page."""
@@ -77,20 +147,28 @@ class stock_quote_renderer(renderer.abstaining_renderer):
             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)
+                name = self.get_ticker_name(ticker)
+                if name is None:
+                    logger.debug(f'Bad name for {symbol} -- skipped.')
+                    continue
+                price = self.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(
+                (percent_change, delta) = self.get_change_and_delta(
                     ticker, price
                 )
-                # print(f"delta: {delta}, change: {percent_change}")
-                cell_color = "#b00000" if percent_change < 0 else "#009000"
+                chart_filename = stock_quote_renderer.make_chart(symbol, ticker, "1y")
+                print(f"delta: {delta}, change: {percent_change}")
+                if percent_change < 0:
+                    cell_color = "#b00000"
+                elif percent_change > 0:
+                    cell_color = "#009000"
+                else:
+                    cell_color = "#a0a0a0"
                 if symbols_finished % 4 == 0:
                     if symbols_finished > 0:
                         f.write("</TR>")
@@ -103,7 +181,7 @@ class stock_quote_renderer(renderer.abstaining_renderer):
 <TD WIDTH=20% HEIGHT=150 BGCOLOR="{cell_color}">
   <!-- Container -->
   <DIV style="position:relative;
-              height:150px;">
+              height:175px;">
     <!-- Symbol {symbol} -->
     <DIV style="position:absolute;
                 bottom:50;
@@ -120,13 +198,15 @@ class stock_quote_renderer(renderer.abstaining_renderer):
     <DIV style="position:absolute;
                 left:10;
                 top:20;
-                font-size:23pt;
-                font-family: helvetica, arial, sans-serif;
-                width:70%">
+                font-size:24pt;
+                font-family:helvetica, arial, sans-serif;
+                font-weight:900;
+                width:80%">
             ${price:.2f}<BR>
             <I>({percent_change:.1f}%)</I><BR>
             <B>${delta:.2f}</B>
     </DIV>
+    <IMG SRC="{chart_filename}" WIDTH=100%>
   </DIV>
 </TD>"""
                 )
@@ -134,5 +214,5 @@ class stock_quote_renderer(renderer.abstaining_renderer):
         return True
 
 # Test
-#x = stock_quote_renderer({}, ["MSFT", "GOOG", "BTC-USD", "ABHYX", "GC=F", "VNQ"], { "BTC-USD": "BTC", "GC=F": "GOLD" })
-#x.periodic_render(None)
+x = stock_quote_renderer({}, ["MSFT", "GOOG", "BTC-USD", "ABHYX", "GC=F", "VNQ"], { "BTC-USD": "BTC", "GC=F": "GOLD" })
+x.periodic_render(None)