All you fuckers.
[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(__name__)
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         if isinstance(ticker, yq.Ticker):
138             _open = "open"
139             _high = "high"
140             _low = "low"
141             _close = "adjclose"
142         elif isinstance(ticker, yf.Ticker):
143             _open = "Open"
144             _high = "High"
145             _low = "Low"
146             _close = "Close"
147         else:
148             raise Exception("Bad Ticker type")
149         chart = go.Figure(
150             data=go.Candlestick(
151                 open=hist[_open],
152                 high=hist[_high],
153                 low=hist[_low],
154                 close=hist[_close],
155             ),
156             layout=transparent,
157         )
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}...")
162         return base_filename
163
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%>")
169             symbols_finished = 0
170
171             for symbol in self.symbols:
172                 ticker = self.get_financial_data(symbol)
173                 if ticker is None:
174                     logger.debug(f"Unknown symbol {symbol} -- ignored.")
175                     continue
176                 name = self.get_ticker_name(ticker)
177                 if name is None:
178                     logger.debug(f"Bad name for {symbol} -- skipped.")
179                     continue
180                 price = self.get_price(ticker)
181                 if price is None:
182                     logger.debug(f"No price information for {symbol} -- skipped.")
183                     continue
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"
191                 else:
192                     cell_color = "#a0a0a0"
193                 if symbols_finished % 4 == 0:
194                     if symbols_finished > 0:
195                         f.write("</TR>")
196                         f.write("<TR>")
197                 symbols_finished += 1
198                 if self.display_subs is not None and symbol in self.display_subs:
199                     symbol = self.display_subs[symbol]
200                 f.write(
201                     f"""
202 <TD WIDTH=20% HEIGHT=150 BGCOLOR="{cell_color}">
203   <!-- Container -->
204   <DIV style="position:relative;
205               height:175px;">
206     <!-- Symbol {symbol} -->
207     <DIV style="position:absolute;
208                 bottom:50;
209                 right:-20;
210                 -webkit-transform:rotate(-90deg);
211                 font-size:28pt;
212                 font-family: helvetica, arial, sans-serif;
213                 font-weight:900;
214                 -webkit-text-stroke: 2px black;
215                 color: #ddd">
216       {symbol}
217     </DIV>
218     <!-- Current price, Change today and percent change today, name -->
219     <DIV style="position:absolute;
220                 left:10;
221                 top:20;
222                 font-size:24pt;
223                 font-family:helvetica, arial, sans-serif;
224                 font-weight:900;
225                 width:80%">
226             ${price:.2f}<BR>
227             <I>({percent_change:.1f}%)</I><BR>
228             <B>${delta:.2f}</B>
229     </DIV>
230     <IMG SRC="{chart_filename}" WIDTH=100%>
231   </DIV>
232 </TD>"""
233                 )
234             f.write("</TR></TABLE>")
235         return True
236
237
238 # Test
239 # x = stock_quote_renderer(
240 #    {},
241 #    ["MSFT", "GOOG", "BTC-USD", "ABHYX", "GC=F", "VNQ"],
242 #    {"BTC-USD": "BTC", "GC=F": "GOLD"},
243 # )
244 # x.periodic_render(None)