Files
claude-code/skills/stock-lookup/scripts/quote.py
OpenCode Test daa4de8832 Add morning-report and stock-lookup skills
Add comprehensive morning report skill with collectors for calendar, email, tasks,
infrastructure status, news, stocks, and weather. Add stock lookup skill for quote queries.
2026-01-03 10:54:54 -08:00

176 lines
5.6 KiB
Python
Executable File

#!/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()