Initial commit with translated description
This commit is contained in:
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()
|
||||
Reference in New Issue
Block a user