-from bs4 import BeautifulSoup
-from threading import Thread
-import datetime
-import file_writer
-import json
-import re
-import renderer
-import random
-import secrets
-import time
-import urllib.request, urllib.error, urllib.parse
-
-class stock_quote_renderer(renderer.debuggable_abstaining_renderer):
- # format exchange:symbol
- def __init__(self, name_to_timeout_dict, symbols):
- super(stock_quote_renderer, self).__init__(name_to_timeout_dict, False)
- self.symbols = symbols
- self.prefix = "https://www.alphavantage.co/query?"
- self.thread = None
-
- def debug_prefix(self):
- return "stock"
-
- def get_random_key(self):
- return random.choice(secrets.alphavantage_keys)
-
- def periodic_render(self, key):
- now = datetime.datetime.now()
- if (now.hour < (9 - 3) or
- now.hour >= (17 - 3) or
- datetime.datetime.today().weekday() > 4):
- self.debug_print("The stock market is closed so not re-rendering")
- return True
-
- if (self.thread is None or not self.thread.is_alive()):
- self.debug_print("Spinning up a background thread...")
- self.thread = Thread(target = self.thread_internal_render, args=())
- self.thread.start()
- return True
-
- def thread_internal_render(self):
- symbols_finished = 0
- f = file_writer.file_writer('stock_3_86400.html')
- f.write("<H1>Stock Quotes</H1><HR>")
- f.write("<TABLE WIDTH=99%>")
- for symbol in self.symbols:
-# print "---------- Working on %s\n" % symbol
-
- # https://www.alphavantage.co/query?function=TIME_SERIES_INTRADAY&symbol=MSFT&interval=5min&apikey=<key>
-
- # https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=MSFT&apikey=<key>
+#!/usr/bin/env python3
- attempts = 0
- cooked = ""
- while True:
- key = self.get_random_key()
- url = self.prefix + "function=GLOBAL_QUOTE&symbol=%s&apikey=%s" % (symbol, key)
- raw = urllib.request.urlopen(url).read()
- cooked = json.loads(raw)
- if 'Global Quote' not in cooked:
-# print "%s\n" % cooked
- print("Failure %d, sleep %d sec...\n" % (attempts + 1,
- 2 ** attempts))
- time.sleep(2 ** attempts)
- attempts += 1
- if attempts > 10: # we'll wait up to 512 seconds per symbol
- break
- else:
- break
+import logging
+from typing import Dict, List, Optional, Tuple
- # These fuckers...
- if 'Global Quote' not in cooked:
- print("Can't get data for symbol %s: %s\n" % (
- symbol, raw))
- continue
- cooked = cooked['Global Quote']
+import yfinance as yf # type: ignore
- # {
- # u'Global Quote':
- # {
- # u'01. symbol': u'MSFT',
- # u'02. open': u'151.2900',
- # u'03. high': u'151.8900',
- # u'04. low': u'150.7650',
- # u'05. price': u'151.1300',
- # u'06. volume': u'16443559',
- # u'07. latest trading day': u'2019-12-10',
- # u'08. previous close': u'151.3600',
- # u'09. change': u'-0.2300'
- # u'10. change percent': u'-0.1520%',
- # }
- # }
+import file_writer
+import renderer
- price = "?????"
- if '05. price' in cooked:
- price = cooked['05. price']
- price = price[:-2]
- percent_change = "?????"
- if '10. change percent' in cooked:
- percent_change = cooked['10. change percent']
- if not '-' in percent_change:
- percent_change = "+" + percent_change
+logger = logging.getLogger(__file__)
- change = "?????"
- cell_color = "#bbbbbb"
- if '09. change' in cooked:
- change = cooked['09. change']
- if "-" in change:
- cell_color = "#b00000"
- else:
- cell_color = "#009000"
- change = change[:-2]
- if symbols_finished % 4 == 0:
- if (symbols_finished > 0):
- f.write("</TR>")
- f.write("<TR>")
- symbols_finished += 1
+class stock_quote_renderer(renderer.abstaining_renderer):
+ """Render the stock prices page."""
- f.write("""
-<TD WIDTH=20%% HEIGHT=150 BGCOLOR="%s">
+ def __init__(
+ self,
+ name_to_timeout_dict: Dict[str, int],
+ symbols: List[str],
+ display_subs: Dict[str, str] = None,
+ ) -> None:
+ super().__init__(name_to_timeout_dict)
+ self.symbols = symbols
+ self.display_subs = display_subs
+
+ @staticmethod
+ def get_ticker_name(ticker: yf.ticker.Ticker) -> str:
+ """Get friendly name of a ticker."""
+ info = ticker.get_info()
+ if "shortName" in info:
+ return info["shortName"]
+ return ticker
+
+ @staticmethod
+ def get_price(ticker: yf.ticker.Ticker) -> Optional[float]:
+ """Get most recent price of a ticker."""
+ keys = [
+ "bid",
+ "ask",
+ "regularMarketPrice",
+ "lastMarket",
+ "open",
+ "previousClose",
+ ]
+ info = ticker.get_info()
+ 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])
+ return None
+
+ @staticmethod
+ def get_change_and_delta(
+ ticker: yf.ticker.Ticker, price: float
+ ) -> Tuple[float, float]:
+ """Given the current price, look up opening price and compute delta."""
+ keys = [
+ "previousClose",
+ "open",
+ ]
+ 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)
+
+ def periodic_render(self, key: str) -> bool:
+ """Write an up-to-date stock page."""
+ with file_writer.file_writer("stock_3_86400.html") as f:
+ f.write("<H1>Stock Quotes</H1><HR>")
+ f.write("<TABLE WIDTH=99%>")
+ symbols_finished = 0
+ for symbol in self.symbols:
+ ticker = yf.Ticker(symbol)
+ # print(ticker.get_info())
+ if ticker is None:
+ logger.debug(f"Unknown symbol {symbol} -- ignored.")
+ continue
+ name = stock_quote_renderer.get_ticker_name(ticker)
+ price = stock_quote_renderer.get_price(ticker)
+ if price is None:
+ 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"
+ if symbols_finished % 4 == 0:
+ if symbols_finished > 0:
+ f.write("</TR>")
+ f.write("<TR>")
+ symbols_finished += 1
+ if self.display_subs is not None and symbol in self.display_subs:
+ symbol = self.display_subs[symbol]
+ f.write(
+ f"""
+<TD WIDTH=20% HEIGHT=150 BGCOLOR="{cell_color}">
<!-- Container -->
<DIV style="position:relative;
height:150px;">
- <!-- Symbol -->
+ <!-- Symbol {symbol} -->
<DIV style="position:absolute;
bottom:50;
right:-20;
font-weight:900;
-webkit-text-stroke: 2px black;
color: #ddd">
- %s
+ {symbol}
</DIV>
- <!-- Current price, Change today and percent change today -->
+ <!-- Current price, Change today and percent change today, name -->
<DIV style="position:absolute;
left:10;
top:20;
font-size:23pt;
font-family: helvetica, arial, sans-serif;
- width:70%%">
- $%s<BR>
- <I>(%s)</I><BR>
- <B>$%s</B>
+ width:70%">
+ ${price:.2f}<BR>
+ <I>({percent_change:.1f}%)</I><BR>
+ <B>${delta:.2f}</B>
</DIV>
</DIV>
-</TD>""" % (cell_color,
- symbol,
- price,
- percent_change,
- change))
- f.write("</TR></TABLE>")
- f.close()
+</TD>"""
+ )
+ f.write("</TR></TABLE>")
return True
-#x = stock_quote_renderer({}, ["MSFT", "GOOG", "GOOGL", "OPTAX", "VNQ"])
-#x.periodic_render(None)
+# Test
+#x = stock_quote_renderer({}, ["MSFT", "GOOG", "BTC-USD", "OPTAX", "GC=F", "VNQ"], { "BTC-USD": "BTC", "GC=F": "GOLD" })
#x.periodic_render(None)