5 from typing import Any, Dict, List, Optional, Tuple
7 import yfinance as yf # type: ignore
8 import yahooquery as yq # type: ignore
9 import plotly.graph_objects as go
12 import kiosk_constants
16 logger = logging.getLogger(__file__)
19 class stock_quote_renderer(renderer.abstaining_renderer):
20 """Render the stock prices page."""
24 name_to_timeout_dict: Dict[str, int],
26 display_subs: Dict[str, str] = None,
28 super().__init__(name_to_timeout_dict)
29 self.backend = "yahooquery"
30 self.symbols = symbols
31 self.display_subs = display_subs
32 self.info_cache: Dict[str, Any] = {}
34 def get_financial_data(self, symbol: str) -> Optional[Any]:
35 if symbol in self.info_cache:
36 return self.info_cache[symbol]
37 if self.backend == "yahooquery":
38 ticker = yq.Ticker(symbol)
39 if "Quote not found" in ticker.price[symbol]:
41 self.info_cache[symbol] = ticker
43 elif self.backend == "yfinance":
44 ticker = yf.Ticker(symbol)
45 if not ticker.fast_info["last_price"]:
47 self.info_cache[symbol] = ticker
50 raise Exception(f"Unknown backend: {self.backend}")
52 def get_ticker_name(self, ticker: Any) -> Optional[str]:
53 """Get friendly name of a ticker."""
54 if isinstance(ticker, yf.Ticker):
56 elif isinstance(ticker, yq.Ticker):
57 return ticker.symbols[0]
61 def prioritized_get_item_from_dict(
62 keys: List[str], dictionary: Dict[str, Any]
66 result = dictionary.get(key, None)
71 def get_price(self, ticker: Any) -> Optional[float]:
72 """Get most recent price of a ticker."""
73 if isinstance(ticker, yf.Ticker):
74 price = stock_quote_renderer.prioritized_get_item_from_dict(
75 ["last_price", "open", "previous_close"], ticker.fast_info
80 price = stock_quote_renderer.prioritized_get_item_from_dict(
81 ["bid", "ask", "lastMarket"],
87 elif isinstance(ticker, yq.Ticker):
88 price = stock_quote_renderer.prioritized_get_item_from_dict(
89 ["regularMarketPrice", "preMarketPrice", "regularMarketPreviousClose"],
90 ticker.price[ticker.symbols[0]],
96 def get_last_close(self, ticker: Any) -> Optional[float]:
97 if isinstance(ticker, yf.Ticker):
98 last_close = stock_quote_renderer.prioritized_get_item_from_dict(
99 ["previous_close", "open"], ticker.fast_info
104 last_close = stock_quote_renderer.prioritized_get_item_from_dict(
105 ["preMarketPrice"], ticker.info
109 elif isinstance(ticker, yq.Ticker):
110 last_close = stock_quote_renderer.prioritized_get_item_from_dict(
111 ["regularMarketPreviousClose", "regularMarketOpen"],
112 ticker.price[ticker.symbols[0]],
116 return self.get_price(ticker)
118 def get_change_and_delta(
119 self, ticker: Any, current_price: float
120 ) -> Tuple[float, float]:
121 """Given the current price, look up opening price and compute delta."""
122 last_price = self.get_last_close(ticker)
123 delta = current_price - last_price
124 return (delta / last_price * 100.0, delta)
127 def make_chart(symbol: str, ticker: Any, period: str) -> str:
128 base_filename = f"stock_chart_{symbol}.png"
129 output_filename = os.path.join(kiosk_constants.pages_dir, base_filename)
130 transparent = go.Layout(
131 paper_bgcolor="rgba(0,0,0,0)",
132 plot_bgcolor="rgba(0,0,0,0)",
133 xaxis_rangeslider_visible=False,
136 hist = ticker.history(period=period, interval="1wk")
137 if isinstance(ticker, yq.Ticker):
142 elif isinstance(ticker, yf.Ticker):
148 raise Exception("Bad Ticker type")
158 chart.update_xaxes(visible=False, showticklabels=False)
159 chart.update_yaxes(side="right")
160 chart.write_image(output_filename, format="png", width=600, height=350)
161 print(f"Write {output_filename}...")
164 def periodic_render(self, key: str) -> bool:
165 """Write an up-to-date stock page."""
166 with file_writer.file_writer("stock_3_86400.html") as f:
167 f.write("<H1>Stock Quotes</H1><HR>")
168 f.write("<TABLE WIDTH=99%>")
171 for symbol in self.symbols:
172 ticker = self.get_financial_data(symbol)
174 logger.debug(f"Unknown symbol {symbol} -- ignored.")
176 name = self.get_ticker_name(ticker)
178 logger.debug(f"Bad name for {symbol} -- skipped.")
180 price = self.get_price(ticker)
182 logger.debug(f"No price information for {symbol} -- skipped.")
184 (percent_change, delta) = self.get_change_and_delta(ticker, price)
185 chart_filename = stock_quote_renderer.make_chart(symbol, ticker, "1y")
186 print(f"delta: {delta}, change: {percent_change}")
187 if percent_change < 0:
188 cell_color = "#b00000"
189 elif percent_change > 0:
190 cell_color = "#009000"
192 cell_color = "#a0a0a0"
193 if symbols_finished % 4 == 0:
194 if symbols_finished > 0:
197 symbols_finished += 1
198 if self.display_subs is not None and symbol in self.display_subs:
199 symbol = self.display_subs[symbol]
202 <TD WIDTH=20% HEIGHT=150 BGCOLOR="{cell_color}">
204 <DIV style="position:relative;
206 <!-- Symbol {symbol} -->
207 <DIV style="position:absolute;
210 -webkit-transform:rotate(-90deg);
212 font-family: helvetica, arial, sans-serif;
214 -webkit-text-stroke: 2px black;
218 <!-- Current price, Change today and percent change today, name -->
219 <DIV style="position:absolute;
223 font-family:helvetica, arial, sans-serif;
227 <I>({percent_change:.1f}%)</I><BR>
230 <IMG SRC="{chart_filename}" WIDTH=100%>
234 f.write("</TR></TABLE>")
239 x = stock_quote_renderer(
241 ["MSFT", "GOOG", "BTC-USD", "ABHYX", "GC=F", "VNQ"],
242 {"BTC-USD": "BTC", "GC=F": "GOLD"},
244 x.periodic_render(None)