feat: integrate quantitative, calendar, market data endpoints
Add 3 new service layers and route modules: - quantitative_service: Sharpe ratio, CAPM, normality tests, unit root tests - calendar_service: earnings/dividends/IPO/splits calendars, estimates, SEC ownership - market_service: ETF, index, crypto, forex, options, futures data Total endpoints: 50. All use free providers (yfinance, SEC). Update README with comprehensive endpoint documentation.
This commit is contained in:
114
README.md
114
README.md
@@ -1,6 +1,6 @@
|
||||
# OpenBB Investment Analysis API
|
||||
|
||||
REST API wrapping OpenBB SDK, providing stock data query, sentiment analysis, technical indicators, macro data, and rule-based investment analysis for US and Swedish markets. Designed to be called by OpenClaw (or any AI assistant) — the API returns structured data, all LLM reasoning happens on the caller side.
|
||||
REST API wrapping OpenBB SDK, providing stock data, sentiment analysis, technical indicators, quantitative risk metrics, macro data, market data (ETF/index/crypto/forex/options), and rule-based investment analysis for US and Swedish markets. Designed to be called by OpenClaw (or any AI assistant) -- the API returns structured data, all LLM reasoning happens on the caller side.
|
||||
|
||||
## API Keys
|
||||
|
||||
@@ -42,6 +42,7 @@ export INVEST_API_ALPHAVANTAGE_API_KEY=your_alphavantage_key
|
||||
```bash
|
||||
conda env create -f environment.yml
|
||||
conda activate openbb-invest-api
|
||||
pip install openbb-quantitative openbb-econometrics
|
||||
```
|
||||
|
||||
### 2. Start the server
|
||||
@@ -64,15 +65,28 @@ curl http://localhost:8000/api/v1/stock/AAPL/quote
|
||||
# Swedish stock quote
|
||||
curl http://localhost:8000/api/v1/stock/VOLV-B.ST/quote
|
||||
|
||||
# Sentiment analysis (requires Finnhub + Alpha Vantage keys)
|
||||
# Sentiment analysis (Finnhub + Alpha Vantage)
|
||||
curl http://localhost:8000/api/v1/stock/AAPL/sentiment
|
||||
|
||||
# News sentiment with per-article scores (requires Alpha Vantage key)
|
||||
# News sentiment with per-article scores (Alpha Vantage)
|
||||
curl http://localhost:8000/api/v1/stock/AAPL/news-sentiment
|
||||
|
||||
# Technical indicators
|
||||
curl http://localhost:8000/api/v1/stock/AAPL/technical
|
||||
|
||||
# Quantitative risk metrics
|
||||
curl http://localhost:8000/api/v1/stock/AAPL/performance
|
||||
curl http://localhost:8000/api/v1/stock/AAPL/capm
|
||||
|
||||
# SEC insider trading
|
||||
curl http://localhost:8000/api/v1/stock/AAPL/sec-insider
|
||||
|
||||
# ETF info
|
||||
curl http://localhost:8000/api/v1/etf/SPY/info
|
||||
|
||||
# Crypto price history
|
||||
curl http://localhost:8000/api/v1/crypto/BTC-USD/historical?days=30
|
||||
|
||||
# Macro overview (requires FRED key)
|
||||
curl http://localhost:8000/api/v1/macro/overview
|
||||
|
||||
@@ -108,7 +122,7 @@ curl -X POST http://localhost:8000/api/v1/portfolio/analyze \
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/stock/{symbol}/sentiment` | Aggregated: news sentiment + recommendations + upgrades |
|
||||
| GET | `/api/v1/stock/{symbol}/news-sentiment?limit=30` | News articles with per-ticker sentiment scores (Alpha Vantage) |
|
||||
| GET | `/api/v1/stock/{symbol}/insider-trades` | Insider transactions (CEO/CFO buys and sells) |
|
||||
| GET | `/api/v1/stock/{symbol}/insider-trades` | Insider transactions via Finnhub |
|
||||
| GET | `/api/v1/stock/{symbol}/recommendations` | Monthly analyst buy/hold/sell counts |
|
||||
| GET | `/api/v1/stock/{symbol}/upgrades` | Recent analyst upgrades and downgrades |
|
||||
|
||||
@@ -118,6 +132,70 @@ curl -X POST http://localhost:8000/api/v1/portfolio/analyze \
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/stock/{symbol}/technical` | RSI, MACD, SMA, EMA, Bollinger Bands + signal interpretation |
|
||||
|
||||
### Quantitative Analysis (openbb-quantitative, no key needed)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/stock/{symbol}/performance?days=365` | Sharpe ratio, summary statistics, volatility |
|
||||
| GET | `/api/v1/stock/{symbol}/capm` | CAPM: market risk, systematic risk, idiosyncratic risk |
|
||||
| GET | `/api/v1/stock/{symbol}/normality?days=365` | Normality tests: Jarque-Bera, Shapiro-Wilk, Kolmogorov-Smirnov |
|
||||
| GET | `/api/v1/stock/{symbol}/unitroot?days=365` | Unit root tests: ADF, KPSS for stationarity |
|
||||
|
||||
### Calendar Events (no key needed)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/calendar/earnings?start_date=&end_date=` | Upcoming earnings announcements |
|
||||
| GET | `/api/v1/calendar/dividends?start_date=&end_date=` | Upcoming dividend dates |
|
||||
| GET | `/api/v1/calendar/ipo?start_date=&end_date=` | Upcoming IPOs |
|
||||
| GET | `/api/v1/calendar/splits?start_date=&end_date=` | Upcoming stock splits |
|
||||
|
||||
### Estimates & Ownership (yfinance + SEC, no key needed)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/stock/{symbol}/estimates` | Analyst consensus estimates |
|
||||
| GET | `/api/v1/stock/{symbol}/share-statistics` | Float, shares outstanding, short interest |
|
||||
| GET | `/api/v1/stock/{symbol}/sec-insider` | Insider trading from SEC (Form 4) |
|
||||
| GET | `/api/v1/stock/{symbol}/institutional` | Institutional holders from SEC 13F filings |
|
||||
| GET | `/api/v1/screener` | Stock screener |
|
||||
|
||||
### ETF Data (yfinance, no key needed)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/etf/{symbol}/info` | ETF profile, issuer, holdings |
|
||||
| GET | `/api/v1/etf/{symbol}/historical?days=365` | ETF price history |
|
||||
| GET | `/api/v1/etf/search?query=` | Search ETFs by name |
|
||||
|
||||
### Index Data (yfinance, no key needed)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/index/available` | List available indices |
|
||||
| GET | `/api/v1/index/{symbol}/historical?days=365` | Index price history (^GSPC, ^DJI, ^IXIC) |
|
||||
|
||||
### Crypto Data (yfinance, no key needed)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/crypto/{symbol}/historical?days=365` | Crypto price history (BTC-USD, ETH-USD) |
|
||||
| GET | `/api/v1/crypto/search?query=` | Search cryptocurrencies |
|
||||
|
||||
### Currency / Forex (yfinance, no key needed)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/currency/{symbol}/historical?days=365` | Forex price history (EURUSD, USDSEK) |
|
||||
|
||||
### Derivatives (yfinance, no key needed)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/options/{symbol}/chains` | Options chain data |
|
||||
| GET | `/api/v1/futures/{symbol}/historical?days=365` | Futures price history |
|
||||
| GET | `/api/v1/futures/{symbol}/curve` | Futures term structure/curve |
|
||||
|
||||
### Macro Economics (FRED, free key)
|
||||
|
||||
| Method | Path | Description |
|
||||
@@ -192,26 +270,22 @@ openbb-invest-api/
|
||||
├── openbb_service.py # OpenBB SDK wrapper (async)
|
||||
├── finnhub_service.py # Finnhub REST client (insider, analyst data)
|
||||
├── alphavantage_service.py # Alpha Vantage REST client (news sentiment)
|
||||
├── quantitative_service.py # Risk metrics, CAPM, normality tests
|
||||
├── calendar_service.py # Calendar events, screening, ownership
|
||||
├── market_service.py # ETF, index, crypto, currency, derivatives
|
||||
├── macro_service.py # FRED macro data via OpenBB
|
||||
├── technical_service.py # Technical indicators via openbb-technical
|
||||
├── analysis_service.py # Rule engine for portfolio analysis
|
||||
├── routes.py # Core stock data + portfolio + discovery routes
|
||||
├── routes_sentiment.py # Sentiment & analyst routes (Finnhub + Alpha Vantage)
|
||||
├── routes_quantitative.py # Quantitative analysis routes
|
||||
├── routes_calendar.py # Calendar, estimates, ownership routes
|
||||
├── routes_market.py # ETF, index, crypto, currency, derivatives routes
|
||||
├── routes_macro.py # Macro economics routes (FRED)
|
||||
├── routes_technical.py # Technical analysis routes
|
||||
├── environment.yml # Conda environment
|
||||
├── pyproject.toml # Project metadata
|
||||
└── tests/ # 102 tests
|
||||
├── test_models.py
|
||||
├── test_mappers.py
|
||||
├── test_openbb_service.py
|
||||
├── test_finnhub_service.py
|
||||
├── test_analysis_service.py
|
||||
├── test_routes.py
|
||||
├── test_routes_sentiment.py
|
||||
├── test_alphavantage_service.py
|
||||
├── test_routes_macro.py
|
||||
└── test_routes_technical.py
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
@@ -239,16 +313,20 @@ Example OpenClaw workflow:
|
||||
2. OpenClaw calls `GET /api/v1/stock/AAPL/summary` for fundamental data
|
||||
3. OpenClaw calls `GET /api/v1/stock/AAPL/sentiment` for news/analyst sentiment
|
||||
4. OpenClaw calls `GET /api/v1/stock/AAPL/technical` for technical signals
|
||||
5. OpenClaw calls `GET /api/v1/macro/overview` for market context
|
||||
6. OpenClaw calls `POST /api/v1/portfolio/analyze` with user's holdings
|
||||
7. OpenClaw's LLM synthesizes all structured data into a personalized recommendation
|
||||
5. OpenClaw calls `GET /api/v1/stock/AAPL/performance` for risk metrics (Sharpe, volatility)
|
||||
6. OpenClaw calls `GET /api/v1/stock/AAPL/sec-insider` for insider trading activity
|
||||
7. OpenClaw calls `GET /api/v1/macro/overview` for market context
|
||||
8. OpenClaw calls `POST /api/v1/portfolio/analyze` with user's holdings
|
||||
9. OpenClaw's LLM synthesizes all structured data into a personalized recommendation
|
||||
|
||||
## Data Sources
|
||||
|
||||
| Source | Cost | Key Required | Data Provided |
|
||||
|--------|------|-------------|---------------|
|
||||
| **yfinance** | Free | No | Quotes, fundamentals, financials, historical prices, news, discovery |
|
||||
| **yfinance** | Free | No | Quotes, fundamentals, financials, historical prices, news, discovery, ETF, index, crypto, forex, options, futures |
|
||||
| **SEC** | Free | No | Insider trading (Form 4), institutional holdings (13F), company filings |
|
||||
| **Finnhub** | Free | Yes (free registration) | Insider trades, analyst recommendations, upgrades/downgrades |
|
||||
| **Alpha Vantage** | Free | Yes (free registration) | News sentiment scores (bullish/bearish per ticker per article), 25 req/day |
|
||||
| **FRED** | Free | Yes (free registration) | Fed rate, treasury yields, CPI, unemployment, GDP, VIX, 800K+ economic series |
|
||||
| **openbb-technical** | Free | No (local computation) | RSI, MACD, SMA, EMA, Bollinger Bands |
|
||||
| **openbb-quantitative** | Free | No (local computation) | Sharpe ratio, CAPM, normality tests, unit root tests, summary statistics |
|
||||
|
||||
159
calendar_service.py
Normal file
159
calendar_service.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""Calendar events (earnings, dividends, IPOs, splits), screening, and ownership."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from openbb import obb
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def get_earnings_calendar(
|
||||
start_date: str | None = None, end_date: str | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get upcoming earnings announcements."""
|
||||
try:
|
||||
kwargs: dict[str, Any] = {}
|
||||
if start_date:
|
||||
kwargs["start_date"] = start_date
|
||||
if end_date:
|
||||
kwargs["end_date"] = end_date
|
||||
result = await asyncio.to_thread(obb.equity.calendar.earnings, **kwargs)
|
||||
return _to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Earnings calendar failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_dividend_calendar(
|
||||
start_date: str | None = None, end_date: str | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get upcoming dividend dates."""
|
||||
try:
|
||||
kwargs: dict[str, Any] = {}
|
||||
if start_date:
|
||||
kwargs["start_date"] = start_date
|
||||
if end_date:
|
||||
kwargs["end_date"] = end_date
|
||||
result = await asyncio.to_thread(obb.equity.calendar.dividend, **kwargs)
|
||||
return _to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Dividend calendar failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_ipo_calendar(
|
||||
start_date: str | None = None, end_date: str | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get upcoming IPO dates."""
|
||||
try:
|
||||
kwargs: dict[str, Any] = {}
|
||||
if start_date:
|
||||
kwargs["start_date"] = start_date
|
||||
if end_date:
|
||||
kwargs["end_date"] = end_date
|
||||
result = await asyncio.to_thread(obb.equity.calendar.ipo, **kwargs)
|
||||
return _to_list(result)
|
||||
except Exception:
|
||||
logger.warning("IPO calendar failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_splits_calendar(
|
||||
start_date: str | None = None, end_date: str | None = None
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get upcoming stock split dates."""
|
||||
try:
|
||||
kwargs: dict[str, Any] = {}
|
||||
if start_date:
|
||||
kwargs["start_date"] = start_date
|
||||
if end_date:
|
||||
kwargs["end_date"] = end_date
|
||||
result = await asyncio.to_thread(obb.equity.calendar.splits, **kwargs)
|
||||
return _to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Splits calendar failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_analyst_estimates(symbol: str) -> dict[str, Any]:
|
||||
"""Get analyst consensus estimates for a symbol."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.estimates.consensus, symbol, provider="yfinance"
|
||||
)
|
||||
items = _to_list(result)
|
||||
return {"symbol": symbol, "estimates": items}
|
||||
except Exception:
|
||||
logger.warning("Analyst estimates failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "estimates": []}
|
||||
|
||||
|
||||
async def get_share_statistics(symbol: str) -> dict[str, Any]:
|
||||
"""Get share statistics (float, shares outstanding, etc.)."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.ownership.share_statistics, symbol, provider="yfinance"
|
||||
)
|
||||
items = _to_list(result)
|
||||
return items[0] if items else {}
|
||||
except Exception:
|
||||
logger.warning("Share statistics failed for %s", symbol, exc_info=True)
|
||||
return {}
|
||||
|
||||
|
||||
async def get_insider_trading(symbol: str) -> list[dict[str, Any]]:
|
||||
"""Get insider trading data from SEC (free)."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.ownership.insider_trading, symbol, provider="sec"
|
||||
)
|
||||
return _to_list(result)
|
||||
except Exception:
|
||||
logger.warning("SEC insider trading failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_institutional_holders(symbol: str) -> list[dict[str, Any]]:
|
||||
"""Get institutional holders from SEC 13F filings."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.ownership.form_13f, symbol, provider="sec"
|
||||
)
|
||||
return _to_list(result)
|
||||
except Exception:
|
||||
logger.warning("13F data failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def screen_stocks() -> list[dict[str, Any]]:
|
||||
"""Screen stocks using available screener."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.screener, provider="yfinance"
|
||||
)
|
||||
return _to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Stock screener failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
def _to_list(result: Any) -> list[dict[str, Any]]:
|
||||
"""Convert OBBject result to list of dicts."""
|
||||
if result is None or result.results is None:
|
||||
return []
|
||||
items = result.results
|
||||
if not isinstance(items, list):
|
||||
items = [items]
|
||||
out = []
|
||||
for item in items:
|
||||
if hasattr(item, "model_dump"):
|
||||
d = item.model_dump()
|
||||
else:
|
||||
d = vars(item) if vars(item) else {}
|
||||
for k, v in d.items():
|
||||
if hasattr(v, "isoformat"):
|
||||
d[k] = v.isoformat()
|
||||
out.append(d)
|
||||
return out
|
||||
6
main.py
6
main.py
@@ -9,6 +9,9 @@ from routes import router
|
||||
from routes_sentiment import router as sentiment_router
|
||||
from routes_macro import router as macro_router
|
||||
from routes_technical import router as technical_router
|
||||
from routes_quantitative import router as quantitative_router
|
||||
from routes_calendar import router as calendar_router
|
||||
from routes_market import router as market_router
|
||||
|
||||
logging.basicConfig(
|
||||
level=settings.log_level.upper(),
|
||||
@@ -33,6 +36,9 @@ app.include_router(router)
|
||||
app.include_router(sentiment_router)
|
||||
app.include_router(macro_router)
|
||||
app.include_router(technical_router)
|
||||
app.include_router(quantitative_router)
|
||||
app.include_router(calendar_router)
|
||||
app.include_router(market_router)
|
||||
|
||||
|
||||
@app.get("/health", response_model=dict[str, str])
|
||||
|
||||
181
market_service.py
Normal file
181
market_service.py
Normal file
@@ -0,0 +1,181 @@
|
||||
"""Market data: ETFs, indices, crypto, currencies, and derivatives."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from openbb import obb
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PROVIDER = "yfinance"
|
||||
|
||||
|
||||
# --- ETF ---
|
||||
|
||||
|
||||
async def get_etf_info(symbol: str) -> dict[str, Any]:
|
||||
"""Get ETF profile/info."""
|
||||
try:
|
||||
result = await asyncio.to_thread(obb.etf.info, symbol, provider=PROVIDER)
|
||||
items = _to_list(result)
|
||||
return items[0] if items else {}
|
||||
except Exception:
|
||||
logger.warning("ETF info failed for %s", symbol, exc_info=True)
|
||||
return {}
|
||||
|
||||
|
||||
async def get_etf_historical(symbol: str, days: int = 365) -> list[dict[str, Any]]:
|
||||
"""Get ETF price history."""
|
||||
start = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.etf.historical, symbol, start_date=start, provider=PROVIDER
|
||||
)
|
||||
return _to_list(result)
|
||||
except Exception:
|
||||
logger.warning("ETF historical failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def search_etf(query: str) -> list[dict[str, Any]]:
|
||||
"""Search for ETFs by name or keyword."""
|
||||
try:
|
||||
result = await asyncio.to_thread(obb.etf.search, query)
|
||||
return _to_list(result)
|
||||
except Exception:
|
||||
logger.warning("ETF search failed for %s", query, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
# --- Index ---
|
||||
|
||||
|
||||
async def get_available_indices() -> list[dict[str, Any]]:
|
||||
"""List available market indices."""
|
||||
try:
|
||||
result = await asyncio.to_thread(obb.index.available, provider=PROVIDER)
|
||||
return _to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Available indices failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_index_historical(symbol: str, days: int = 365) -> list[dict[str, Any]]:
|
||||
"""Get index price history."""
|
||||
start = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.index.price.historical, symbol, start_date=start, provider=PROVIDER
|
||||
)
|
||||
return _to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Index historical failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
# --- Crypto ---
|
||||
|
||||
|
||||
async def get_crypto_historical(symbol: str, days: int = 365) -> list[dict[str, Any]]:
|
||||
"""Get cryptocurrency price history."""
|
||||
start = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.crypto.price.historical, symbol, start_date=start, provider=PROVIDER
|
||||
)
|
||||
return _to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Crypto historical failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def search_crypto(query: str) -> list[dict[str, Any]]:
|
||||
"""Search for cryptocurrencies."""
|
||||
try:
|
||||
result = await asyncio.to_thread(obb.crypto.search, query)
|
||||
return _to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Crypto search failed for %s", query, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
# --- Currency ---
|
||||
|
||||
|
||||
async def get_currency_historical(
|
||||
symbol: str, days: int = 365
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get forex price history (e.g., EURUSD)."""
|
||||
start = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.currency.price.historical, symbol, start_date=start, provider=PROVIDER
|
||||
)
|
||||
return _to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Currency historical failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
# --- Derivatives ---
|
||||
|
||||
|
||||
async def get_options_chains(symbol: str) -> list[dict[str, Any]]:
|
||||
"""Get options chain data for a symbol."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.derivatives.options.chains, symbol, provider=PROVIDER
|
||||
)
|
||||
return _to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Options chains failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_futures_historical(
|
||||
symbol: str, days: int = 365
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get futures price history."""
|
||||
start = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.derivatives.futures.historical, symbol, start_date=start, provider=PROVIDER
|
||||
)
|
||||
return _to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Futures historical failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_futures_curve(symbol: str) -> list[dict[str, Any]]:
|
||||
"""Get futures term structure/curve."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.derivatives.futures.curve, symbol, provider=PROVIDER
|
||||
)
|
||||
return _to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Futures curve failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
def _to_list(result: Any) -> list[dict[str, Any]]:
|
||||
"""Convert OBBject result to list of dicts."""
|
||||
if result is None or result.results is None:
|
||||
return []
|
||||
items = result.results
|
||||
if not isinstance(items, list):
|
||||
items = [items]
|
||||
out = []
|
||||
for item in items:
|
||||
if hasattr(item, "model_dump"):
|
||||
d = item.model_dump()
|
||||
else:
|
||||
d = vars(item) if vars(item) else {}
|
||||
for k, v in d.items():
|
||||
if hasattr(v, "isoformat"):
|
||||
d[k] = v.isoformat()
|
||||
out.append(d)
|
||||
return out
|
||||
147
quantitative_service.py
Normal file
147
quantitative_service.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""Quantitative analysis: risk metrics, performance, CAPM, normality tests."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any
|
||||
|
||||
from openbb import obb
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PROVIDER = "yfinance"
|
||||
|
||||
# Need 252+ trading days for default window; 730 calendar days is safe
|
||||
PERF_DAYS = 730
|
||||
TARGET = "close"
|
||||
|
||||
|
||||
async def get_performance_metrics(symbol: str, days: int = 365) -> dict[str, Any]:
|
||||
"""Calculate Sharpe ratio, summary stats, and volatility for a symbol."""
|
||||
# Need at least 252 trading days for Sharpe window
|
||||
fetch_days = max(days, PERF_DAYS)
|
||||
start = (datetime.now() - timedelta(days=fetch_days)).strftime("%Y-%m-%d")
|
||||
|
||||
try:
|
||||
hist = await asyncio.to_thread(
|
||||
obb.equity.price.historical, symbol, start_date=start, provider=PROVIDER
|
||||
)
|
||||
if not hist or not hist.results:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
|
||||
sharpe_result, summary_result, stdev_result = await asyncio.gather(
|
||||
asyncio.to_thread(
|
||||
obb.quantitative.performance.sharpe_ratio,
|
||||
data=hist.results, target=TARGET,
|
||||
),
|
||||
asyncio.to_thread(
|
||||
obb.quantitative.summary, data=hist.results, target=TARGET
|
||||
),
|
||||
asyncio.to_thread(
|
||||
obb.quantitative.stats.stdev, data=hist.results, target=TARGET
|
||||
),
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
sharpe = _safe_last(sharpe_result) if not isinstance(sharpe_result, BaseException) else None
|
||||
summary = _extract_single(summary_result) if not isinstance(summary_result, BaseException) else {}
|
||||
stdev = _safe_last(stdev_result) if not isinstance(stdev_result, BaseException) else None
|
||||
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"period_days": days,
|
||||
"sharpe_ratio": sharpe,
|
||||
"summary": summary,
|
||||
"stdev": stdev,
|
||||
}
|
||||
except Exception:
|
||||
logger.warning("Performance metrics failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute performance metrics"}
|
||||
|
||||
|
||||
async def get_capm(symbol: str) -> dict[str, Any]:
|
||||
"""Calculate CAPM metrics: beta, alpha, systematic/idiosyncratic risk."""
|
||||
start = (datetime.now() - timedelta(days=PERF_DAYS)).strftime("%Y-%m-%d")
|
||||
|
||||
try:
|
||||
hist = await asyncio.to_thread(
|
||||
obb.equity.price.historical, symbol, start_date=start, provider=PROVIDER
|
||||
)
|
||||
if not hist or not hist.results:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
|
||||
capm = await asyncio.to_thread(
|
||||
obb.quantitative.capm, data=hist.results, target=TARGET
|
||||
)
|
||||
return {"symbol": symbol, **_extract_single(capm)}
|
||||
except Exception:
|
||||
logger.warning("CAPM failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute CAPM"}
|
||||
|
||||
|
||||
async def get_normality_test(symbol: str, days: int = 365) -> dict[str, Any]:
|
||||
"""Run normality tests (Jarque-Bera, Shapiro-Wilk, etc.) on returns."""
|
||||
fetch_days = max(days, PERF_DAYS)
|
||||
start = (datetime.now() - timedelta(days=fetch_days)).strftime("%Y-%m-%d")
|
||||
|
||||
try:
|
||||
hist = await asyncio.to_thread(
|
||||
obb.equity.price.historical, symbol, start_date=start, provider=PROVIDER
|
||||
)
|
||||
if not hist or not hist.results:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
|
||||
norm = await asyncio.to_thread(
|
||||
obb.quantitative.normality, data=hist.results, target=TARGET
|
||||
)
|
||||
return {"symbol": symbol, **_extract_single(norm)}
|
||||
except Exception:
|
||||
logger.warning("Normality test failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute normality tests"}
|
||||
|
||||
|
||||
async def get_unitroot_test(symbol: str, days: int = 365) -> dict[str, Any]:
|
||||
"""Run unit root tests (ADF, KPSS) for stationarity."""
|
||||
fetch_days = max(days, PERF_DAYS)
|
||||
start = (datetime.now() - timedelta(days=fetch_days)).strftime("%Y-%m-%d")
|
||||
|
||||
try:
|
||||
hist = await asyncio.to_thread(
|
||||
obb.equity.price.historical, symbol, start_date=start, provider=PROVIDER
|
||||
)
|
||||
if not hist or not hist.results:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
|
||||
ur = await asyncio.to_thread(
|
||||
obb.quantitative.unitroot_test, data=hist.results, target=TARGET
|
||||
)
|
||||
return {"symbol": symbol, **_extract_single(ur)}
|
||||
except Exception:
|
||||
logger.warning("Unit root test failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute unit root test"}
|
||||
|
||||
|
||||
def _extract_single(result: Any) -> dict[str, Any]:
|
||||
"""Extract data from an OBBject result (single model or list)."""
|
||||
if result is None:
|
||||
return {}
|
||||
items = getattr(result, "results", None)
|
||||
if items is None:
|
||||
return {}
|
||||
if hasattr(items, "model_dump"):
|
||||
return items.model_dump()
|
||||
if isinstance(items, list) and items:
|
||||
last = items[-1]
|
||||
return last.model_dump() if hasattr(last, "model_dump") else {}
|
||||
return {}
|
||||
|
||||
|
||||
def _safe_last(result: Any) -> dict[str, Any] | None:
|
||||
"""Get the last item from a list result, or None."""
|
||||
if result is None:
|
||||
return None
|
||||
items = getattr(result, "results", None)
|
||||
if items is None or not isinstance(items, list) or not items:
|
||||
return None
|
||||
last = items[-1]
|
||||
return last.model_dump() if hasattr(last, "model_dump") else None
|
||||
140
routes_calendar.py
Normal file
140
routes_calendar.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""Routes for calendar events, screening, ownership, and estimates."""
|
||||
|
||||
import functools
|
||||
import logging
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import ParamSpec, TypeVar
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Path, Query
|
||||
|
||||
from models import SYMBOL_PATTERN, ApiResponse
|
||||
import calendar_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/v1")
|
||||
|
||||
P = ParamSpec("P")
|
||||
R = TypeVar("R")
|
||||
|
||||
|
||||
def _validate_symbol(symbol: str) -> str:
|
||||
if not SYMBOL_PATTERN.match(symbol):
|
||||
raise HTTPException(status_code=400, detail="Invalid symbol format")
|
||||
return symbol.upper()
|
||||
|
||||
|
||||
def _safe(fn: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]:
|
||||
@functools.wraps(fn)
|
||||
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||
try:
|
||||
return await fn(*args, **kwargs)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception:
|
||||
logger.exception("Upstream data error")
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail="Data provider error. Check server logs.",
|
||||
)
|
||||
return wrapper # type: ignore[return-value]
|
||||
|
||||
|
||||
# --- Calendar Events ---
|
||||
|
||||
|
||||
@router.get("/calendar/earnings", response_model=ApiResponse)
|
||||
@_safe
|
||||
async def earnings_calendar(
|
||||
start_date: str | None = Query(default=None, description="YYYY-MM-DD"),
|
||||
end_date: str | None = Query(default=None, description="YYYY-MM-DD"),
|
||||
):
|
||||
"""Get upcoming earnings announcements."""
|
||||
data = await calendar_service.get_earnings_calendar(start_date, end_date)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/calendar/dividends", response_model=ApiResponse)
|
||||
@_safe
|
||||
async def dividend_calendar(
|
||||
start_date: str | None = Query(default=None, description="YYYY-MM-DD"),
|
||||
end_date: str | None = Query(default=None, description="YYYY-MM-DD"),
|
||||
):
|
||||
"""Get upcoming dividend dates."""
|
||||
data = await calendar_service.get_dividend_calendar(start_date, end_date)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/calendar/ipo", response_model=ApiResponse)
|
||||
@_safe
|
||||
async def ipo_calendar(
|
||||
start_date: str | None = Query(default=None, description="YYYY-MM-DD"),
|
||||
end_date: str | None = Query(default=None, description="YYYY-MM-DD"),
|
||||
):
|
||||
"""Get upcoming IPOs."""
|
||||
data = await calendar_service.get_ipo_calendar(start_date, end_date)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/calendar/splits", response_model=ApiResponse)
|
||||
@_safe
|
||||
async def splits_calendar(
|
||||
start_date: str | None = Query(default=None, description="YYYY-MM-DD"),
|
||||
end_date: str | None = Query(default=None, description="YYYY-MM-DD"),
|
||||
):
|
||||
"""Get upcoming stock splits."""
|
||||
data = await calendar_service.get_splits_calendar(start_date, end_date)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
# --- Analyst Estimates ---
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/estimates", response_model=ApiResponse)
|
||||
@_safe
|
||||
async def stock_estimates(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Get analyst consensus estimates."""
|
||||
symbol = _validate_symbol(symbol)
|
||||
data = await calendar_service.get_analyst_estimates(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/share-statistics", response_model=ApiResponse)
|
||||
@_safe
|
||||
async def stock_share_stats(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Get share statistics: float, outstanding, short interest."""
|
||||
symbol = _validate_symbol(symbol)
|
||||
data = await calendar_service.get_share_statistics(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
# --- Ownership (SEC, free) ---
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/sec-insider", response_model=ApiResponse)
|
||||
@_safe
|
||||
async def stock_sec_insider(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Get insider trading data from SEC (Form 4)."""
|
||||
symbol = _validate_symbol(symbol)
|
||||
data = await calendar_service.get_insider_trading(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/institutional", response_model=ApiResponse)
|
||||
@_safe
|
||||
async def stock_institutional(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Get institutional holders from SEC 13F filings."""
|
||||
symbol = _validate_symbol(symbol)
|
||||
data = await calendar_service.get_institutional_holders(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
# --- Screener ---
|
||||
|
||||
|
||||
@router.get("/screener", response_model=ApiResponse)
|
||||
@_safe
|
||||
async def stock_screener():
|
||||
"""Screen stocks using available filters."""
|
||||
data = await calendar_service.screen_stocks()
|
||||
return ApiResponse(data=data)
|
||||
166
routes_market.py
Normal file
166
routes_market.py
Normal file
@@ -0,0 +1,166 @@
|
||||
"""Routes for ETF, index, crypto, currency, and derivatives data."""
|
||||
|
||||
import functools
|
||||
import logging
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import ParamSpec, TypeVar
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Path, Query
|
||||
|
||||
from models import SYMBOL_PATTERN, ApiResponse
|
||||
import market_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/v1")
|
||||
|
||||
P = ParamSpec("P")
|
||||
R = TypeVar("R")
|
||||
|
||||
|
||||
def _validate_symbol(symbol: str) -> str:
|
||||
if not SYMBOL_PATTERN.match(symbol):
|
||||
raise HTTPException(status_code=400, detail="Invalid symbol format")
|
||||
return symbol.upper()
|
||||
|
||||
|
||||
def _safe(fn: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]:
|
||||
@functools.wraps(fn)
|
||||
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||
try:
|
||||
return await fn(*args, **kwargs)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception:
|
||||
logger.exception("Upstream data error")
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail="Data provider error. Check server logs.",
|
||||
)
|
||||
return wrapper # type: ignore[return-value]
|
||||
|
||||
|
||||
# --- ETF ---
|
||||
|
||||
|
||||
@router.get("/etf/{symbol}/info", response_model=ApiResponse)
|
||||
@_safe
|
||||
async def etf_info(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Get ETF profile and info."""
|
||||
symbol = _validate_symbol(symbol)
|
||||
data = await market_service.get_etf_info(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/etf/{symbol}/historical", response_model=ApiResponse)
|
||||
@_safe
|
||||
async def etf_historical(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
days: int = Query(default=365, ge=1, le=3650),
|
||||
):
|
||||
"""Get ETF price history."""
|
||||
symbol = _validate_symbol(symbol)
|
||||
data = await market_service.get_etf_historical(symbol, days=days)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/etf/search", response_model=ApiResponse)
|
||||
@_safe
|
||||
async def etf_search(query: str = Query(..., min_length=1, max_length=100)):
|
||||
"""Search for ETFs by name or keyword."""
|
||||
data = await market_service.search_etf(query)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
# --- Index ---
|
||||
|
||||
|
||||
@router.get("/index/available", response_model=ApiResponse)
|
||||
@_safe
|
||||
async def index_available():
|
||||
"""List available market indices."""
|
||||
data = await market_service.get_available_indices()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/index/{symbol}/historical", response_model=ApiResponse)
|
||||
@_safe
|
||||
async def index_historical(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
days: int = Query(default=365, ge=1, le=3650),
|
||||
):
|
||||
"""Get index price history (e.g., ^GSPC, ^DJI, ^IXIC)."""
|
||||
symbol = _validate_symbol(symbol)
|
||||
data = await market_service.get_index_historical(symbol, days=days)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
# --- Crypto ---
|
||||
|
||||
|
||||
@router.get("/crypto/{symbol}/historical", response_model=ApiResponse)
|
||||
@_safe
|
||||
async def crypto_historical(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
days: int = Query(default=365, ge=1, le=3650),
|
||||
):
|
||||
"""Get cryptocurrency price history (e.g., BTC-USD)."""
|
||||
symbol = _validate_symbol(symbol)
|
||||
data = await market_service.get_crypto_historical(symbol, days=days)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/crypto/search", response_model=ApiResponse)
|
||||
@_safe
|
||||
async def crypto_search(query: str = Query(..., min_length=1, max_length=100)):
|
||||
"""Search for cryptocurrencies."""
|
||||
data = await market_service.search_crypto(query)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
# --- Currency ---
|
||||
|
||||
|
||||
@router.get("/currency/{symbol}/historical", response_model=ApiResponse)
|
||||
@_safe
|
||||
async def currency_historical(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
days: int = Query(default=365, ge=1, le=3650),
|
||||
):
|
||||
"""Get forex price history (e.g., EURUSD, USDSEK)."""
|
||||
symbol = _validate_symbol(symbol)
|
||||
data = await market_service.get_currency_historical(symbol, days=days)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
# --- Derivatives ---
|
||||
|
||||
|
||||
@router.get("/options/{symbol}/chains", response_model=ApiResponse)
|
||||
@_safe
|
||||
async def options_chains(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Get options chain data."""
|
||||
symbol = _validate_symbol(symbol)
|
||||
data = await market_service.get_options_chains(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/futures/{symbol}/historical", response_model=ApiResponse)
|
||||
@_safe
|
||||
async def futures_historical(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
days: int = Query(default=365, ge=1, le=3650),
|
||||
):
|
||||
"""Get futures price history."""
|
||||
symbol = _validate_symbol(symbol)
|
||||
data = await market_service.get_futures_historical(symbol, days=days)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/futures/{symbol}/curve", response_model=ApiResponse)
|
||||
@_safe
|
||||
async def futures_curve(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Get futures term structure/curve."""
|
||||
symbol = _validate_symbol(symbol)
|
||||
data = await market_service.get_futures_curve(symbol)
|
||||
return ApiResponse(data=data)
|
||||
85
routes_quantitative.py
Normal file
85
routes_quantitative.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""Routes for quantitative analysis: risk metrics, CAPM, normality, unit root."""
|
||||
|
||||
import functools
|
||||
import logging
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import ParamSpec, TypeVar
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Path, Query
|
||||
|
||||
from models import SYMBOL_PATTERN, ApiResponse
|
||||
import quantitative_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/v1")
|
||||
|
||||
P = ParamSpec("P")
|
||||
R = TypeVar("R")
|
||||
|
||||
|
||||
def _validate_symbol(symbol: str) -> str:
|
||||
if not SYMBOL_PATTERN.match(symbol):
|
||||
raise HTTPException(status_code=400, detail="Invalid symbol format")
|
||||
return symbol.upper()
|
||||
|
||||
|
||||
def _safe(fn: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]:
|
||||
@functools.wraps(fn)
|
||||
async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
||||
try:
|
||||
return await fn(*args, **kwargs)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception:
|
||||
logger.exception("Upstream data error")
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail="Data provider error. Check server logs.",
|
||||
)
|
||||
return wrapper # type: ignore[return-value]
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/performance", response_model=ApiResponse)
|
||||
@_safe
|
||||
async def stock_performance(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
days: int = Query(default=365, ge=30, le=3650),
|
||||
):
|
||||
"""Performance metrics: Sharpe, Sortino, max drawdown, volatility."""
|
||||
symbol = _validate_symbol(symbol)
|
||||
data = await quantitative_service.get_performance_metrics(symbol, days=days)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/capm", response_model=ApiResponse)
|
||||
@_safe
|
||||
async def stock_capm(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""CAPM: beta, alpha, systematic and idiosyncratic risk."""
|
||||
symbol = _validate_symbol(symbol)
|
||||
data = await quantitative_service.get_capm(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/normality", response_model=ApiResponse)
|
||||
@_safe
|
||||
async def stock_normality(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
days: int = Query(default=365, ge=30, le=3650),
|
||||
):
|
||||
"""Normality tests: Jarque-Bera, Shapiro-Wilk on returns."""
|
||||
symbol = _validate_symbol(symbol)
|
||||
data = await quantitative_service.get_normality_test(symbol, days=days)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/unitroot", response_model=ApiResponse)
|
||||
@_safe
|
||||
async def stock_unitroot(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
days: int = Query(default=365, ge=30, le=3650),
|
||||
):
|
||||
"""Unit root tests: ADF, KPSS for stationarity."""
|
||||
symbol = _validate_symbol(symbol)
|
||||
data = await quantitative_service.get_unitroot_test(symbol, days=days)
|
||||
return ApiResponse(data=data)
|
||||
Reference in New Issue
Block a user