Initial commit with translated description
This commit is contained in:
82
SKILL.md
Normal file
82
SKILL.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
name: finance
|
||||||
|
description: "追踪股票、ETF、指数、加密货币(如可用)和外汇货币对,具备缓存和提供商回退功能。"
|
||||||
|
metadata: {"clawdbot":{"config":{"requiredEnv":["TWELVEDATA_API_KEY","ALPHAVANTAGE_API_KEY"],"stateDirs":[".cache/finance"],"example":"# Optional (only if you add a paid provider later)\n# export TWELVEDATA_API_KEY=\"...\"\n# export ALPHAVANTAGE_API_KEY=\"...\"\n"}}}
|
||||||
|
---
|
||||||
|
|
||||||
|
# Market Tracker Skill
|
||||||
|
|
||||||
|
This skill helps you fetch **latest quotes** and **historical series** for:
|
||||||
|
- Stocks / ETFs / Indices (e.g., AAPL, MSFT, ^GSPC, VOO)
|
||||||
|
- FX pairs (e.g., USD/ZAR, EURUSD, GBP-JPY)
|
||||||
|
- Crypto tickers supported by the chosen provider (best-effort)
|
||||||
|
|
||||||
|
It is optimized for:
|
||||||
|
- fast “what’s the price now?” queries
|
||||||
|
- lightweight tracking with a local watchlist
|
||||||
|
- caching to avoid rate-limits
|
||||||
|
|
||||||
|
## When to use
|
||||||
|
Use this skill when the user asks:
|
||||||
|
- “What’s the latest price of ___?”
|
||||||
|
- “Track ___ and ___ and show me daily changes.”
|
||||||
|
- “Give me a 30-day series for ___.”
|
||||||
|
- “Convert USD to ZAR (or track USD/ZAR).”
|
||||||
|
- “Maintain a watchlist and summarize performance.”
|
||||||
|
|
||||||
|
## Provider strategy (important)
|
||||||
|
- **Stocks/ETFs/indices** default: Yahoo Finance via `yfinance` (no key, broad coverage), but it is unofficial and can rate-limit.
|
||||||
|
- **FX** default: ExchangeRate-API Open Access endpoint (no key, daily update).
|
||||||
|
- If the user needs high-frequency or many symbols, recommend adding a paid provider later.
|
||||||
|
|
||||||
|
See `providers.md` for details and symbol formats.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Quick start (how you run it)
|
||||||
|
These scripts are intended to be run from a terminal. The agent should:
|
||||||
|
1) ensure dependencies installed
|
||||||
|
2) run the scripts
|
||||||
|
3) summarize results cleanly
|
||||||
|
|
||||||
|
Install:
|
||||||
|
- `python -m venv .venv && source .venv/bin/activate` (or Windows equivalent)
|
||||||
|
- `pip install -r requirements.txt`
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### 1) Latest quote (stock/ETF/index)
|
||||||
|
Examples:
|
||||||
|
- `python scripts/market_quote.py AAPL`
|
||||||
|
- `python scripts/market_quote.py ^GSPC`
|
||||||
|
- `python scripts/market_quote.py VOO`
|
||||||
|
|
||||||
|
### 2) Latest FX rate
|
||||||
|
Examples:
|
||||||
|
- `python scripts/market_quote.py USD/ZAR`
|
||||||
|
- `python scripts/market_quote.py EURUSD`
|
||||||
|
- `python scripts/market_quote.py GBP-JPY`
|
||||||
|
|
||||||
|
### 3) Historical series (CSV to stdout)
|
||||||
|
Examples:
|
||||||
|
- `python scripts/market_series.py AAPL --days 30`
|
||||||
|
- `python scripts/market_series.py USD/ZAR --days 30`
|
||||||
|
|
||||||
|
### 4) Watchlist summary (local file)
|
||||||
|
- Add tickers: `python scripts/market_watchlist.py add AAPL MSFT USD/ZAR`
|
||||||
|
- Remove: `python scripts/market_watchlist.py remove MSFT`
|
||||||
|
- Show summary: `python scripts/market_watchlist.py summary`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Output expectations (what you should return to the user)
|
||||||
|
- For quotes: price, change %, timestamp/source, and any caveats (like “FX updates daily”).
|
||||||
|
- For series: confirm date range, number of points, and show a small preview (first/last few rows).
|
||||||
|
- If rate-limited: explain what happened and retry with backoff OR advise to reduce frequency.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Safety / correctness
|
||||||
|
- Never claim “real-time” unless the provider is truly real-time. FX open access updates daily.
|
||||||
|
- Always cache responses and throttle repeated calls.
|
||||||
|
- If Yahoo blocks requests, propose a paid provider or increase cache TTL.
|
||||||
6
_meta.json
Normal file
6
_meta.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"ownerId": "kn78eqgx8n6vbvb1eqtce4bmx980axpn",
|
||||||
|
"slug": "finance",
|
||||||
|
"version": "1.1.2",
|
||||||
|
"publishedAt": 1769983031560
|
||||||
|
}
|
||||||
39
providers.md
Normal file
39
providers.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Providers & symbol formats
|
||||||
|
|
||||||
|
## Stocks / ETFs / Indices (default: Yahoo via yfinance)
|
||||||
|
Pros:
|
||||||
|
- Very broad symbol coverage, no API key.
|
||||||
|
Cons:
|
||||||
|
- Unofficial access patterns can be rate-limited or break.
|
||||||
|
|
||||||
|
Common examples:
|
||||||
|
- AAPL, MSFT, TSLA
|
||||||
|
- Indices: ^GSPC (S&P 500), ^DJI, ^IXIC
|
||||||
|
- ETFs: VOO, SPY, QQQ
|
||||||
|
|
||||||
|
## FX (default: ExchangeRate-API Open Access)
|
||||||
|
Endpoint: https://open.er-api.com/v6/latest/<BASE>
|
||||||
|
- No API key
|
||||||
|
- Updates once per day
|
||||||
|
- Rate-limited
|
||||||
|
- Attribution required by their terms
|
||||||
|
|
||||||
|
Symbol formats accepted by this skill:
|
||||||
|
- USD/ZAR
|
||||||
|
- USDZAR
|
||||||
|
- GBP-JPY
|
||||||
|
- EURUSD
|
||||||
|
|
||||||
|
We normalize to BASE/QUOTE and fetch BASE->all then pick QUOTE.
|
||||||
|
|
||||||
|
## Why not exchangerate.host by default?
|
||||||
|
It now requires an API key for most useful endpoints and has limited free quotas, so it’s not a great no-key default.
|
||||||
|
|
||||||
|
## Paid providers (optional future upgrade)
|
||||||
|
If you need many symbols or frequent polling:
|
||||||
|
- Twelve Data
|
||||||
|
- Alpha Vantage
|
||||||
|
- Polygon
|
||||||
|
- Finnhub
|
||||||
|
|
||||||
|
This skill includes environment variable placeholders, but does not implement these providers yet.
|
||||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
yfinance>=0.2.40
|
||||||
|
pandas>=2.0.0
|
||||||
|
requests>=2.31.0
|
||||||
207
scripts/market_quote.py
Normal file
207
scripts/market_quote.py
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
market_quote.py
|
||||||
|
|
||||||
|
Fetch the latest quote for:
|
||||||
|
- Stocks/ETFs/indices (via yfinance)
|
||||||
|
- FX pairs (via ExchangeRate-API open access)
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/market_quote.py AAPL
|
||||||
|
python scripts/market_quote.py ^GSPC
|
||||||
|
python scripts/market_quote.py USD/ZAR
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Dict, Optional, Tuple
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
CACHE_DIR = os.path.join(".cache", "market-tracker")
|
||||||
|
os.makedirs(CACHE_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
# Conservative defaults to reduce rate-limit pain.
|
||||||
|
DEFAULT_TTL_SECONDS_STOCKS = 60
|
||||||
|
DEFAULT_TTL_SECONDS_FX = 12 * 60 * 60 # open FX endpoint updates daily
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Quote:
|
||||||
|
symbol: str
|
||||||
|
kind: str # "stock" | "fx"
|
||||||
|
price: float
|
||||||
|
currency: Optional[str]
|
||||||
|
asof_unix: int
|
||||||
|
source: str
|
||||||
|
extra: Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
def _cache_path(key: str) -> str:
|
||||||
|
safe = re.sub(r"[^A-Za-z0-9_.-]+", "_", key)
|
||||||
|
return os.path.join(CACHE_DIR, f"{safe}.json")
|
||||||
|
|
||||||
|
|
||||||
|
def _cache_get(key: str, ttl: int) -> Optional[Dict[str, Any]]:
|
||||||
|
path = _cache_path(key)
|
||||||
|
if not os.path.exists(path):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
with open(path, "r", encoding="utf-8") as f:
|
||||||
|
payload = json.load(f)
|
||||||
|
if int(time.time()) - int(payload.get("_cached_at", 0)) <= ttl:
|
||||||
|
return payload
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _cache_set(key: str, payload: Dict[str, Any]) -> None:
|
||||||
|
payload["_cached_at"] = int(time.time())
|
||||||
|
with open(_cache_path(key), "w", encoding="utf-8") as f:
|
||||||
|
json.dump(payload, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_fx_pair(s: str) -> Optional[Tuple[str, str]]:
|
||||||
|
s = s.strip().upper()
|
||||||
|
s = s.replace(" ", "")
|
||||||
|
s = s.replace("-", "/")
|
||||||
|
if "/" in s:
|
||||||
|
parts = s.split("/")
|
||||||
|
if len(parts) == 2 and len(parts[0]) == 3 and len(parts[1]) == 3:
|
||||||
|
return parts[0], parts[1]
|
||||||
|
return None
|
||||||
|
# e.g. EURUSD
|
||||||
|
if len(s) == 6 and s.isalpha():
|
||||||
|
return s[:3], s[3:]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_fx(base: str, quote: str) -> Quote:
|
||||||
|
cache_key = f"fx_{base}_{quote}"
|
||||||
|
cached = _cache_get(cache_key, DEFAULT_TTL_SECONDS_FX)
|
||||||
|
if cached:
|
||||||
|
return Quote(**cached)
|
||||||
|
|
||||||
|
url = f"https://open.er-api.com/v6/latest/{base}"
|
||||||
|
r = requests.get(url, timeout=20)
|
||||||
|
r.raise_for_status()
|
||||||
|
data = r.json()
|
||||||
|
|
||||||
|
if data.get("result") != "success":
|
||||||
|
raise RuntimeError(f"FX provider error: {data}")
|
||||||
|
|
||||||
|
rates = data.get("rates") or {}
|
||||||
|
if quote not in rates:
|
||||||
|
raise RuntimeError(f"FX pair not supported by provider: {base}/{quote}")
|
||||||
|
|
||||||
|
q = Quote(
|
||||||
|
symbol=f"{base}/{quote}",
|
||||||
|
kind="fx",
|
||||||
|
price=float(rates[quote]),
|
||||||
|
currency=quote,
|
||||||
|
asof_unix=int(data.get("time_last_update_unix") or time.time()),
|
||||||
|
source="ExchangeRate-API Open Access (open.er-api.com)",
|
||||||
|
extra={
|
||||||
|
"base": base,
|
||||||
|
"quote": quote,
|
||||||
|
"time_last_update_utc": data.get("time_last_update_utc"),
|
||||||
|
"time_next_update_utc": data.get("time_next_update_utc"),
|
||||||
|
"attribution_required": True,
|
||||||
|
"provider": data.get("provider"),
|
||||||
|
"documentation": data.get("documentation"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
_cache_set(cache_key, q.__dict__)
|
||||||
|
return q
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_stock(symbol: str) -> Quote:
|
||||||
|
cache_key = f"stk_{symbol}"
|
||||||
|
cached = _cache_get(cache_key, DEFAULT_TTL_SECONDS_STOCKS)
|
||||||
|
if cached:
|
||||||
|
return Quote(**cached)
|
||||||
|
|
||||||
|
# Import inside to keep startup light if user only does FX.
|
||||||
|
import yfinance as yf
|
||||||
|
|
||||||
|
t = yf.Ticker(symbol)
|
||||||
|
# Fast path: try fast_info first (often less fragile than scraping-heavy calls)
|
||||||
|
price = None
|
||||||
|
currency = None
|
||||||
|
extra: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
fi = getattr(t, "fast_info", None)
|
||||||
|
if fi:
|
||||||
|
price = fi.get("lastPrice") or fi.get("last_price")
|
||||||
|
currency = fi.get("currency")
|
||||||
|
extra["exchange"] = fi.get("exchange")
|
||||||
|
extra["timezone"] = fi.get("timezone")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallback: use 1d history
|
||||||
|
if price is None:
|
||||||
|
try:
|
||||||
|
hist = t.history(period="1d", interval="1m")
|
||||||
|
if hist is not None and len(hist) > 0:
|
||||||
|
price = float(hist["Close"].iloc[-1])
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(f"Yahoo/yfinance fetch failed for {symbol}: {e}")
|
||||||
|
|
||||||
|
if price is None:
|
||||||
|
raise RuntimeError(f"Could not resolve price for symbol: {symbol}")
|
||||||
|
|
||||||
|
# Try to enrich
|
||||||
|
try:
|
||||||
|
info = t.get_info() # can be heavier / more likely to rate-limit
|
||||||
|
if isinstance(info, dict):
|
||||||
|
currency = currency or info.get("currency")
|
||||||
|
extra["shortName"] = info.get("shortName") or info.get("longName")
|
||||||
|
extra["marketState"] = info.get("marketState")
|
||||||
|
except Exception:
|
||||||
|
# Non-fatal: still return price
|
||||||
|
pass
|
||||||
|
|
||||||
|
q = Quote(
|
||||||
|
symbol=symbol,
|
||||||
|
kind="stock",
|
||||||
|
price=float(price),
|
||||||
|
currency=currency,
|
||||||
|
asof_unix=int(time.time()),
|
||||||
|
source="Yahoo Finance via yfinance (unofficial; best-effort)",
|
||||||
|
extra=extra,
|
||||||
|
)
|
||||||
|
|
||||||
|
_cache_set(cache_key, q.__dict__)
|
||||||
|
return q
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("symbol", help="Ticker (AAPL, ^GSPC) or FX pair (USD/ZAR, EURUSD)")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
s = args.symbol.strip()
|
||||||
|
|
||||||
|
fx = _parse_fx_pair(s)
|
||||||
|
if fx:
|
||||||
|
base, quote = fx
|
||||||
|
q = _fetch_fx(base, quote)
|
||||||
|
else:
|
||||||
|
q = _fetch_stock(s)
|
||||||
|
|
||||||
|
# Print as JSON so the calling agent can format nicely.
|
||||||
|
print(json.dumps(q.__dict__, ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
86
scripts/market_series.py
Normal file
86
scripts/market_series.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
market_series.py
|
||||||
|
|
||||||
|
Fetch historical series and print CSV to stdout.
|
||||||
|
|
||||||
|
Stocks/ETFs/indices: yfinance history (daily)
|
||||||
|
FX: derived from ExchangeRate-API open endpoint (daily snapshots not provided here),
|
||||||
|
so we return a simple "latest only" line unless you add a historical FX provider.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/market_series.py AAPL --days 30
|
||||||
|
python scripts/market_series.py ^GSPC --days 120
|
||||||
|
python scripts/market_series.py USD/ZAR --days 30
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_fx_pair(s: str) -> Optional[Tuple[str, str]]:
|
||||||
|
s = s.strip().upper()
|
||||||
|
s = s.replace(" ", "")
|
||||||
|
s = s.replace("-", "/")
|
||||||
|
if "/" in s:
|
||||||
|
parts = s.split("/")
|
||||||
|
if len(parts) == 2 and len(parts[0]) == 3 and len(parts[1]) == 3:
|
||||||
|
return parts[0], parts[1]
|
||||||
|
return None
|
||||||
|
if len(s) == 6 and s.isalpha():
|
||||||
|
return s[:3], s[3:]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _stock_series(symbol: str, days: int) -> pd.DataFrame:
|
||||||
|
import yfinance as yf
|
||||||
|
|
||||||
|
end = datetime.utcnow()
|
||||||
|
start = end - timedelta(days=days)
|
||||||
|
|
||||||
|
df = yf.download(symbol, start=start.date().isoformat(), end=end.date().isoformat(), progress=False)
|
||||||
|
if df is None or len(df) == 0:
|
||||||
|
raise RuntimeError(f"No data returned for symbol: {symbol}")
|
||||||
|
|
||||||
|
df = df.reset_index()
|
||||||
|
# Normalize column names (handle MultiIndex tuples from yfinance)
|
||||||
|
cols = {}
|
||||||
|
for c in df.columns:
|
||||||
|
if isinstance(c, tuple):
|
||||||
|
# Flatten tuple to string (e.g., ('Close', 'AAPL') -> 'close')
|
||||||
|
name = "_".join(str(x) for x in c if x).lower().replace(" ", "_")
|
||||||
|
else:
|
||||||
|
name = str(c).lower().replace(" ", "_")
|
||||||
|
cols[c] = name
|
||||||
|
df = df.rename(columns=cols)
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("symbol", help="Ticker or FX pair")
|
||||||
|
ap.add_argument("--days", type=int, default=30)
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
fx = _parse_fx_pair(args.symbol)
|
||||||
|
if fx:
|
||||||
|
# Open access endpoint does not provide true historical in this mode.
|
||||||
|
# We intentionally fail loudly so the agent explains the limitation.
|
||||||
|
raise SystemExit(
|
||||||
|
"FX historical series is not supported with the no-key open endpoint. "
|
||||||
|
"Use latest quotes, or add a historical FX provider (paid or other source)."
|
||||||
|
)
|
||||||
|
|
||||||
|
df = _stock_series(args.symbol, args.days)
|
||||||
|
df.to_csv(sys.stdout, index=False)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
124
scripts/market_watchlist.py
Normal file
124
scripts/market_watchlist.py
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
market_watchlist.py
|
||||||
|
|
||||||
|
Maintain a local watchlist and summarize current quotes.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/market_watchlist.py add AAPL MSFT USD/ZAR
|
||||||
|
python scripts/market_watchlist.py remove MSFT
|
||||||
|
python scripts/market_watchlist.py list
|
||||||
|
python scripts/market_watchlist.py summary
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
WATCHLIST_PATH = os.path.join(".cache", "market-tracker", "watchlist.json")
|
||||||
|
os.makedirs(os.path.dirname(WATCHLIST_PATH), exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def load_watchlist() -> List[str]:
|
||||||
|
if not os.path.exists(WATCHLIST_PATH):
|
||||||
|
return []
|
||||||
|
with open(WATCHLIST_PATH, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
items = data.get("items") or []
|
||||||
|
# de-dupe, preserve order
|
||||||
|
seen = set()
|
||||||
|
out = []
|
||||||
|
for x in items:
|
||||||
|
x = str(x).strip()
|
||||||
|
if x and x not in seen:
|
||||||
|
out.append(x)
|
||||||
|
seen.add(x)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def save_watchlist(items: List[str]) -> None:
|
||||||
|
with open(WATCHLIST_PATH, "w", encoding="utf-8") as f:
|
||||||
|
json.dump({"items": items}, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_add(symbols: List[str]) -> None:
|
||||||
|
items = load_watchlist()
|
||||||
|
for s in symbols:
|
||||||
|
s = s.strip()
|
||||||
|
if s and s not in items:
|
||||||
|
items.append(s)
|
||||||
|
save_watchlist(items)
|
||||||
|
print(json.dumps({"ok": True, "items": items}, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_remove(symbols: List[str]) -> None:
|
||||||
|
items = load_watchlist()
|
||||||
|
rm = set(s.strip() for s in symbols if s.strip())
|
||||||
|
items = [x for x in items if x not in rm]
|
||||||
|
save_watchlist(items)
|
||||||
|
print(json.dumps({"ok": True, "items": items}, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_list() -> None:
|
||||||
|
print(json.dumps({"items": load_watchlist()}, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_summary() -> None:
|
||||||
|
items = load_watchlist()
|
||||||
|
if not items:
|
||||||
|
print(json.dumps({"items": [], "summary": []}, indent=2))
|
||||||
|
return
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for sym in items:
|
||||||
|
# Call market_quote.py to keep logic centralized
|
||||||
|
p = subprocess.run(
|
||||||
|
[sys.executable, os.path.join(os.path.dirname(__file__), "market_quote.py"), sym],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
if p.returncode != 0:
|
||||||
|
results.append({"symbol": sym, "error": p.stderr.strip() or p.stdout.strip()})
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
results.append(json.loads(p.stdout))
|
||||||
|
except Exception:
|
||||||
|
results.append({"symbol": sym, "error": "Invalid JSON from market_quote.py", "raw": p.stdout[:500]})
|
||||||
|
|
||||||
|
print(json.dumps({"items": items, "summary": results}, ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||||
|
|
||||||
|
sp_add = sub.add_parser("add")
|
||||||
|
sp_add.add_argument("symbols", nargs="+")
|
||||||
|
|
||||||
|
sp_rm = sub.add_parser("remove")
|
||||||
|
sp_rm.add_argument("symbols", nargs="+")
|
||||||
|
|
||||||
|
sub.add_parser("list")
|
||||||
|
sub.add_parser("summary")
|
||||||
|
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
if args.cmd == "add":
|
||||||
|
cmd_add(args.symbols)
|
||||||
|
elif args.cmd == "remove":
|
||||||
|
cmd_remove(args.symbols)
|
||||||
|
elif args.cmd == "list":
|
||||||
|
cmd_list()
|
||||||
|
elif args.cmd == "summary":
|
||||||
|
cmd_summary()
|
||||||
|
else:
|
||||||
|
raise SystemExit("Unknown command")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user