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.
This commit is contained in:
OpenCode Test
2026-01-03 10:54:54 -08:00
parent ae958528a6
commit daa4de8832
13 changed files with 1590 additions and 0 deletions

View File

@@ -0,0 +1,175 @@
#!/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()