X-Git-Url: https://wannabe.guru.org/gitweb/?a=blobdiff_plain;f=stock_renderer.py;h=60e70432ccbbbfee620119dc32b718d407222da8;hb=6cd5b068127501d2b48e8ac67b7432bffc5fce53;hp=fda43bd99b2f08b7380ea023287eb5758a3f7c44;hpb=e4dca16bbd329afdb587e8488767d88e17777254;p=kiosk.git diff --git a/stock_renderer.py b/stock_renderer.py index fda43bd..60e7043 100644 --- a/stock_renderer.py +++ b/stock_renderer.py @@ -1,13 +1,21 @@ #!/usr/bin/env python3 -from typing import Dict, List, Optional, Tuple +import logging +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 -class stock_quote_renderer(renderer.debuggable_abstaining_renderer): +logger = logging.getLogger(__file__) + + +class stock_quote_renderer(renderer.abstaining_renderer): """Render the stock prices page.""" def __init__( @@ -16,54 +24,120 @@ class stock_quote_renderer(renderer.debuggable_abstaining_renderer): symbols: List[str], display_subs: Dict[str, str] = None, ) -> None: - super(stock_quote_renderer, self).__init__(name_to_timeout_dict, False) + super().__init__(name_to_timeout_dict) self.symbols = symbols self.display_subs = display_subs + self.info_cache: Dict[yf.ticker.Ticker, Dict] = {} - def debug_prefix(self) -> str: - return "stock" + 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 - @staticmethod - def get_ticker_name(ticker: yf.ticker.Ticker) -> str: + def get_ticker_name(self, ticker: yf.Ticker) -> str: """Get friendly name of a ticker.""" - info = ticker.get_info() - return info["shortName"] + 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 = [ - "open", - "previousClose", - ] - 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.""" @@ -73,21 +147,28 @@ class stock_quote_renderer(renderer.debuggable_abstaining_renderer): symbols_finished = 0 for symbol in self.symbols: ticker = yf.Ticker(symbol) - print(type(ticker)) - # print(ticker.get_info()) if ticker is None: - self.debug_print(f"Unknown symbol {symbol} -- ignored.") + logger.debug(f"Unknown symbol {symbol} -- ignored.") + continue + name = self.get_ticker_name(ticker) + if name is None: + logger.debug(f'Bad name for {symbol} -- skipped.') continue - name = stock_quote_renderer.get_ticker_name(ticker) - price = stock_quote_renderer.get_price(ticker) + price = self.get_price(ticker) if price is None: - self.debug_print(f"No price information for {symbol} -- skipped.") + 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("") @@ -100,7 +181,7 @@ class stock_quote_renderer(renderer.debuggable_abstaining_renderer):
+ height:175px;">
+ font-size:24pt; + font-family:helvetica, arial, sans-serif; + font-weight:900; + width:80%"> ${price:.2f}
({percent_change:.1f}%)
${delta:.2f}
+
""" ) @@ -131,5 +214,5 @@ class stock_quote_renderer(renderer.debuggable_abstaining_renderer): return True # Test -#x = stock_quote_renderer({}, ["MSFT", "GOOG", "BTC-USD", "OPTAX", "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)