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")
146 chart.update_xaxes(visible=False, showticklabels=False)
147 chart.update_yaxes(side="right")
148 chart.write_image(output_filename, format="png", width=600, height=350)
149 print(f"Write {output_filename}...")
152 def periodic_render(self, key: str) -> bool:
153 """Write an up-to-date stock page."""
154 with file_writer.file_writer("stock_3_86400.html") as f:
155 f.write("<H1>Stock Quotes</H1><HR>")
156 f.write("<TABLE WIDTH=99%>")
159 for symbol in self.symbols:
160 ticker = self.get_financial_data(symbol)
162 logger.debug(f"Unknown symbol {symbol} -- ignored.")
164 name = self.get_ticker_name(ticker)
166 logger.debug(f"Bad name for {symbol} -- skipped.")
168 price = self.get_price(ticker)
170 logger.debug(f"No price information for {symbol} -- skipped.")
172 (percent_change, delta) = self.get_change_and_delta(ticker, price)
173 chart_filename = stock_quote_renderer.make_chart(symbol, ticker, "1y")
174 print(f"delta: {delta}, change: {percent_change}")
175 if percent_change < 0:
176 cell_color = "#b00000"
177 elif percent_change > 0:
178 cell_color = "#009000"
180 cell_color = "#a0a0a0"
181 if symbols_finished % 4 == 0:
182 if symbols_finished > 0:
185 symbols_finished += 1
186 if self.display_subs is not None and symbol in self.display_subs:
187 symbol = self.display_subs[symbol]
190 <TD WIDTH=20% HEIGHT=150 BGCOLOR="{cell_color}">
192 <DIV style="position:relative;
194 <!-- Symbol {symbol} -->
195 <DIV style="position:absolute;
198 -webkit-transform:rotate(-90deg);
200 font-family: helvetica, arial, sans-serif;
202 -webkit-text-stroke: 2px black;
206 <!-- Current price, Change today and percent change today, name -->
207 <DIV style="position:absolute;
211 font-family:helvetica, arial, sans-serif;
215 <I>({percent_change:.1f}%)</I><BR>
218 <IMG SRC="{chart_filename}" WIDTH=100%>
222 f.write("</TR></TABLE>")
227 # x = stock_quote_renderer(
229 # ["MSFT", "GOOG", "BTC-USD", "ABHYX", "GC=F", "VNQ"],
230 # {"BTC-USD": "BTC", "GC=F": "GOLD"},
232 # x.periodic_render(None)