2897edb7fb474b872ffa367096f50e19338bcb5f
[kiosk.git] / stock_renderer.py
1 #!/usr/bin/env python3
2
3 import logging
4 import os
5 from typing import Any, Dict, List, Optional, Tuple
6
7 import yfinance as yf  # type: ignore
8 import yahooquery as yq  # type: ignore
9 import plotly.graph_objects as go
10
11 import file_writer
12 import kiosk_constants
13 import renderer
14
15
16 logger = logging.getLogger(__file__)
17
18
19 class stock_quote_renderer(renderer.abstaining_renderer):
20     """Render the stock prices page."""
21
22     def __init__(
23         self,
24         name_to_timeout_dict: Dict[str, int],
25         symbols: List[str],
26         display_subs: Dict[str, str] = None,
27     ) -> 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] = {}
33
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]:
40                 return None
41             self.info_cache[symbol] = ticker
42             return ticker
43         elif self.backend == "yfinance":
44             ticker = yf.Ticker(symbol)
45             if not ticker.fast_info["last_price"]:
46                 return None
47             self.info_cache[symbol] = ticker
48             return ticker
49         else:
50             raise Exception(f"Unknown backend: {self.backend}")
51
52     def get_ticker_name(self, ticker: Any) -> Optional[str]:
53         """Get friendly name of a ticker."""
54         if isinstance(ticker, yf.Ticker):
55             return ticker.ticker
56         elif isinstance(ticker, yq.Ticker):
57             return ticker.symbols[0]
58         return None
59
60     @staticmethod
61     def prioritized_get_item_from_dict(
62         keys: List[str], dictionary: Dict[str, Any]
63     ) -> Optional[Any]:
64         result = None
65         for key in keys:
66             result = dictionary.get(key, None)
67             if result:
68                 return result
69         return None
70
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
76             )
77             if price:
78                 return price
79
80             price = stock_quote_renderer.prioritized_get_item_from_dict(
81                 ["bid", "ask", "lastMarket"],
82                 ticker.info,
83             )
84             if price:
85                 return price
86             return None
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]],
91             )
92             if price:
93                 return price
94         return None
95
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
100             )
101             if last_close:
102                 return last_close
103
104             last_close = stock_quote_renderer.prioritized_get_item_from_dict(
105                 ["preMarketPrice"], ticker.info
106             )
107             if last_close:
108                 return last_close
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]],
113             )
114             if last_close:
115                 return last_close
116         return self.get_price(ticker)
117
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)
125
126     @staticmethod
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,
134         )
135
136         hist = ticker.history(period=period, interval="1wk")
137         chart = go.Figure(
138             data=go.Candlestick(
139                 open=hist["Open"],
140                 high=hist["High"],
141                 low=hist["Low"],
142                 close=hist["Close"],
143             ),
144             layout=transparent,
145         )
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}...")
150         return base_filename
151
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%>")
157             symbols_finished = 0
158
159             for symbol in self.symbols:
160                 ticker = self.get_financial_data(symbol)
161                 if ticker is None:
162                     logger.debug(f"Unknown symbol {symbol} -- ignored.")
163                     continue
164                 name = self.get_ticker_name(ticker)
165                 if name is None:
166                     logger.debug(f"Bad name for {symbol} -- skipped.")
167                     continue
168                 price = self.get_price(ticker)
169                 if price is None:
170                     logger.debug(f"No price information for {symbol} -- skipped.")
171                     continue
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"
179                 else:
180                     cell_color = "#a0a0a0"
181                 if symbols_finished % 4 == 0:
182                     if symbols_finished > 0:
183                         f.write("</TR>")
184                         f.write("<TR>")
185                 symbols_finished += 1
186                 if self.display_subs is not None and symbol in self.display_subs:
187                     symbol = self.display_subs[symbol]
188                 f.write(
189                     f"""
190 <TD WIDTH=20% HEIGHT=150 BGCOLOR="{cell_color}">
191   <!-- Container -->
192   <DIV style="position:relative;
193               height:175px;">
194     <!-- Symbol {symbol} -->
195     <DIV style="position:absolute;
196                 bottom:50;
197                 right:-20;
198                 -webkit-transform:rotate(-90deg);
199                 font-size:28pt;
200                 font-family: helvetica, arial, sans-serif;
201                 font-weight:900;
202                 -webkit-text-stroke: 2px black;
203                 color: #ddd">
204       {symbol}
205     </DIV>
206     <!-- Current price, Change today and percent change today, name -->
207     <DIV style="position:absolute;
208                 left:10;
209                 top:20;
210                 font-size:24pt;
211                 font-family:helvetica, arial, sans-serif;
212                 font-weight:900;
213                 width:80%">
214             ${price:.2f}<BR>
215             <I>({percent_change:.1f}%)</I><BR>
216             <B>${delta:.2f}</B>
217     </DIV>
218     <IMG SRC="{chart_filename}" WIDTH=100%>
219   </DIV>
220 </TD>"""
221                 )
222             f.write("</TR></TABLE>")
223         return True
224
225
226 # Test
227 # x = stock_quote_renderer(
228 #    {},
229 #    ["MSFT", "GOOG", "BTC-USD", "ABHYX", "GC=F", "VNQ"],
230 #    {"BTC-USD": "BTC", "GC=F": "GOLD"},
231 # )
232 # x.periodic_render(None)