| | import requests |
| | from fastapi import FastAPI, HTTPException, status |
| | from pydantic import BaseModel |
| | from typing import List, Dict, Optional, Any |
| |
|
| |
|
| | from data_ingestion.api_loader import get_daily_adjusted_prices, DataIngestionError |
| | import logging |
| |
|
| |
|
| | logging.basicConfig( |
| | level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" |
| | ) |
| | logger = logging.getLogger(__name__) |
| |
|
| | app = FastAPI(title="API Agent") |
| |
|
| |
|
| | class MarketDataRequest(BaseModel): |
| | tickers: List[str] |
| | start_date: Optional[str] = None |
| | end_date: Optional[str] = None |
| | data_type: Optional[str] = "adjClose" |
| |
|
| |
|
| | @app.post("/get_market_data") |
| | def get_market_data(request: MarketDataRequest): |
| | """ |
| | Fetches daily adjusted market data by calling the data_ingestion layer (FMP). |
| | Returns adjusted close prices per ticker per date. |
| | """ |
| | result: Dict[str, Dict[str, float]] = {} |
| | errors: Dict[str, str] = {} |
| | warnings: Dict[str, str] = {} |
| |
|
| | key = ( |
| | request.data_type |
| | if request.data_type in ["open", "high", "low", "close", "adjClose", "volume"] |
| | else "adjClose" |
| | ) |
| |
|
| | for ticker in request.tickers: |
| | try: |
| | raw = get_daily_adjusted_prices(ticker) |
| |
|
| | time_series: Dict[str, Any] = {} |
| | if isinstance(raw, dict): |
| | time_series = raw |
| | elif isinstance(raw, list): |
| | logger.warning( |
| | f"Loader returned list for {ticker}; filtering dict entries." |
| | ) |
| | for rec in raw: |
| | if isinstance(rec, dict) and "date" in rec: |
| | date_val = rec["date"] |
| | time_series[date_val] = rec |
| | else: |
| | logger.warning( |
| | f"Skipping non-dict or missing-date entry for {ticker}: {rec}" |
| | ) |
| | else: |
| | raise DataIngestionError( |
| | f"Unexpected format from loader for {ticker}: {type(raw)}" |
| | ) |
| |
|
| | ticker_prices: Dict[str, float] = {} |
| | for date_str, values in time_series.items(): |
| | if not isinstance(values, dict): |
| | warnings.setdefault(ticker, "") |
| | warnings[ticker] += f" Non-dict for {date_str}; skipped." |
| | continue |
| | if key not in values: |
| | warnings.setdefault(ticker, "") |
| | warnings[ticker] += f" Missing '{key}' on {date_str}." |
| | continue |
| | try: |
| | ticker_prices[date_str] = float(values[key]) |
| | except (TypeError, ValueError): |
| | warnings.setdefault(ticker, "") |
| | warnings[ticker] += f" Invalid '{key}' value on {date_str}." |
| |
|
| | if ticker_prices: |
| | result[ticker] = ticker_prices |
| | logger.info(f"Fetched {len(ticker_prices)} points for {ticker}.") |
| | else: |
| | warnings.setdefault(ticker, "") |
| | warnings[ticker] += " No valid data points found." |
| |
|
| | except (requests.RequestException, DataIngestionError) as err: |
| | errors[ticker] = str(err) |
| | logger.error(f"Error fetching {ticker}: {err}") |
| | except Exception as err: |
| | errors[ticker] = f"Unexpected error for {ticker}: {err}" |
| | logger.error(errors[ticker]) |
| |
|
| | if not result and errors: |
| | raise HTTPException( |
| | status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| | detail="Failed to fetch market data for all tickers.", |
| | ) |
| |
|
| | return {"result": result, "errors": errors, "warnings": warnings} |
| |
|