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:
66
skills/stock-lookup/SKILL.md
Normal file
66
skills/stock-lookup/SKILL.md
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
name: stock-lookup
|
||||
description: Look up stock prices and quotes
|
||||
---
|
||||
|
||||
# Stock Lookup Skill
|
||||
|
||||
Fetch real-time stock quotes and trends from Yahoo Finance.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
~/.claude/skills/stock-lookup/scripts/quote.py SYMBOL [SYMBOL...]
|
||||
~/.claude/skills/stock-lookup/scripts/quote.py SYMBOL --trend [RANGE]
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Single stock:
|
||||
```bash
|
||||
~/.claude/skills/stock-lookup/scripts/quote.py CRWV
|
||||
```
|
||||
|
||||
Multiple stocks:
|
||||
```bash
|
||||
~/.claude/skills/stock-lookup/scripts/quote.py AAPL MSFT GOOGL NVDA
|
||||
```
|
||||
|
||||
3-month trend (default):
|
||||
```bash
|
||||
~/.claude/skills/stock-lookup/scripts/quote.py CRWV --trend
|
||||
```
|
||||
|
||||
1-year trend:
|
||||
```bash
|
||||
~/.claude/skills/stock-lookup/scripts/quote.py NVDA --trend 1y
|
||||
```
|
||||
|
||||
JSON output:
|
||||
```bash
|
||||
~/.claude/skills/stock-lookup/scripts/quote.py --json CRWV
|
||||
~/.claude/skills/stock-lookup/scripts/quote.py --json --trend 6mo CRWV
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--trend [RANGE]` | Show trend with sparkline. Default: 3mo |
|
||||
| `--json` | Output as JSON |
|
||||
|
||||
## Trend Ranges
|
||||
|
||||
`1mo`, `3mo`, `6mo`, `1y`, `2y`, `5y`, `ytd`, `max`
|
||||
|
||||
## Output
|
||||
|
||||
**Quote mode:** Symbol, name, price, daily change, market state
|
||||
|
||||
**Trend mode:** Start/end prices, change, high/low, ASCII sparkline
|
||||
|
||||
## Notes
|
||||
|
||||
- Uses Yahoo Finance unofficial API (no key required)
|
||||
- Prices may be delayed 15-20 minutes for some exchanges
|
||||
- Works for stocks, ETFs, indices (^GSPC, ^DJI), crypto (BTC-USD)
|
||||
175
skills/stock-lookup/scripts/quote.py
Executable file
175
skills/stock-lookup/scripts/quote.py
Executable 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()
|
||||
Reference in New Issue
Block a user