X-Git-Url: https://wannabe.guru.org/gitweb/?a=blobdiff_plain;f=stock_renderer.py;h=41a56ba650a29a4134d89745b05df5f653e8bf6c;hb=138f8de1ece8b876693d7b8dfc6a139e76aa58b9;hp=2ff6895cbd30d69bb1151c7e99e5d03471fd6aed;hpb=c06bfef53f70551e7920bc4facce27f47b89e2ba;p=kiosk.git diff --git a/stock_renderer.py b/stock_renderer.py index 2ff6895..41a56ba 100644 --- a/stock_renderer.py +++ b/stock_renderer.py @@ -1,65 +1,135 @@ #!/usr/bin/env python3 -from typing import Dict, List, Tuple -import yfinance as yf +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__( - self, name_to_timeout_dict: Dict[str, int], symbols: List[str] + self, + name_to_timeout_dict: Dict[str, int], + 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) -> 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.""" @@ -68,35 +138,41 @@ class stock_quote_renderer(renderer.debuggable_abstaining_renderer): f.write("") symbols_finished = 0 for symbol in self.symbols: - # print(f"--- Symbol: {symbol} ---") 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 = 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: - 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( - ticker, price - ) - # print(f"delta: {delta}, change: {percent_change}") - cell_color = "#b00000" if percent_change < 0 else "#009000" + (percent_change, delta) = self.get_change_and_delta(ticker, price) + 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("") f.write("") symbols_finished += 1 + if self.display_subs is not None and symbol in self.display_subs: + symbol = self.display_subs[symbol] f.write( f""" """ ) @@ -127,5 +205,5 @@ class stock_quote_renderer(renderer.debuggable_abstaining_renderer): # Test -# x = stock_quote_renderer({}, ["MSFT", "GOOG", "GBTC", "OPTAX", "VNQ"]) +# x = stock_quote_renderer({}, ["MSFT", "GOOG", "BTC-USD", "ABHYX", "GC=F", "VNQ"], { "BTC-USD": "BTC", "GC=F": "GOLD" }) # x.periodic_render(None)
- + height:175px;"> +
+ font-size:24pt; + font-family:helvetica, arial, sans-serif; + font-weight:900; + width:80%"> ${price:.2f}
({percent_change:.1f}%)
${delta:.2f}
+