#!/usr/bin/env python3 """Fetch stock quotes from Yahoo Finance API.""" import argparse import json import urllib.request from datetime import datetime def fetch_chart(symbol: str, range_: str = "1d", interval: str = "1d") -> dict: """Fetch chart data for a symbol.""" url = f"https://query1.finance.yahoo.com/v8/finance/chart/{symbol.upper()}?interval={interval}&range={range_}" req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) try: with urllib.request.urlopen(req, timeout=10) as resp: return json.load(resp) except urllib.error.HTTPError as e: return {"error": f"HTTP {e.code}: Symbol '{symbol}' not found"} except urllib.error.URLError as e: return {"error": f"Network error: {e.reason}"} def get_quote(symbol: str) -> dict: """Fetch quote data for a symbol.""" data = fetch_chart(symbol) if "error" in data: return data try: result = data["chart"]["result"][0] meta = result["meta"] return { "symbol": meta["symbol"], "name": meta.get("shortName", meta.get("longName", "N/A")), "price": meta["regularMarketPrice"], "previous_close": meta.get("chartPreviousClose", meta.get("previousClose")), "currency": meta.get("currency", "USD"), "exchange": meta.get("exchangeName", "N/A"), "market_state": meta.get("marketState", "N/A"), } except (KeyError, IndexError, TypeError) as e: return {"error": f"Parse error: {e}"} def get_trend(symbol: str, range_: str = "3mo") -> dict: """Fetch trend data for a symbol over a time range.""" data = fetch_chart(symbol, range_=range_, interval="1d") if "error" in data: return data try: result = data["chart"]["result"][0] meta = result["meta"] timestamps = result["timestamp"] closes = result["indicators"]["quote"][0]["close"] # Filter valid data points valid_data = [(t, c) for t, c in zip(timestamps, closes) if c is not None] if not valid_data: return {"error": "No price data available"} first_ts, first_price = valid_data[0] last_ts, last_price = valid_data[-1] prices_only = [c for _, c in valid_data] high = max(prices_only) low = min(prices_only) change = last_price - first_price pct_change = (change / first_price) * 100 return { "symbol": meta["symbol"], "name": meta.get("shortName", meta.get("longName", "N/A")), "range": range_, "start_date": datetime.fromtimestamp(first_ts).strftime("%b %d"), "end_date": datetime.fromtimestamp(last_ts).strftime("%b %d"), "start_price": first_price, "end_price": last_price, "change": change, "pct_change": pct_change, "high": high, "low": low, "prices": prices_only, } except (KeyError, IndexError, TypeError) as e: return {"error": f"Parse error: {e}"} def format_quote(q: dict) -> str: """Format quote for display.""" if "error" in q: return f"Error: {q['error']}" price = q["price"] prev = q.get("previous_close") if prev: change = price - prev pct = (change / prev) * 100 direction = "+" if change >= 0 else "" change_str = f" ({direction}{change:.2f}, {direction}{pct:.2f}%)" else: change_str = "" market = f" [{q['market_state']}]" if q.get("market_state") else "" return f"{q['symbol']} ({q['name']}): ${price:.2f}{change_str}{market}" def format_trend(t: dict) -> str: """Format trend data for display.""" if "error" in t: return f"Error: {t['error']}" direction = "+" if t["change"] >= 0 else "" # Build sparkline prices = t["prices"] weekly = prices[::5] + [prices[-1]] # Sample every 5 trading days min_p, max_p = min(weekly), max(weekly) range_p = max_p - min_p if max_p > min_p else 1 bars = "▁▂▃▄▅▆▇█" sparkline = "" for p in weekly: idx = int((p - min_p) / range_p * 7.99) idx = min(7, max(0, idx)) sparkline += bars[idx] return f"""{t['symbol']} ({t['name']}) - {t['range']} Trend {'=' * 42} Start ({t['start_date']}): ${t['start_price']:.2f} Now ({t['end_date']}): ${t['end_price']:.2f} Change: {direction}{t['change']:.2f} ({direction}{t['pct_change']:.1f}%) High: ${t['high']:.2f} Low: ${t['low']:.2f} ${min_p:.0f} {sparkline} ${max_p:.0f}""" def main(): parser = argparse.ArgumentParser(description="Fetch stock quotes") parser.add_argument("symbols", nargs="+", help="Stock symbol(s) to look up") parser.add_argument("--json", action="store_true", help="Output as JSON") parser.add_argument( "--trend", nargs="?", const="3mo", metavar="RANGE", help="Show trend (default: 3mo). Ranges: 1mo, 3mo, 6mo, 1y, 2y, 5y, ytd, max", ) args = parser.parse_args() if args.trend: results = [get_trend(s, args.trend) for s in args.symbols] formatter = format_trend else: results = [get_quote(s) for s in args.symbols] formatter = format_quote if args.json: # Remove prices array from JSON output (too verbose) for r in results: if "prices" in r: del r["prices"] print(json.dumps(results, indent=2)) else: for i, r in enumerate(results): if i > 0: print() print(formatter(r)) if __name__ == "__main__": main()