Compare commits
17 Commits
e797f8929d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca8d7099b3 | ||
|
|
c5c9c7db83 | ||
|
|
a57a6835c5 | ||
|
|
89bdc6c552 | ||
|
|
e2cf6e2488 | ||
|
|
615f17a3bb | ||
|
|
87260f4b10 | ||
|
|
b6f49055ad | ||
|
|
ac101c663a | ||
|
|
f5b22deec3 | ||
|
|
b631c888a5 | ||
|
|
760b0a09ea | ||
|
|
16ad276146 | ||
|
|
e5820ebe4a | ||
|
|
cd6158b05c | ||
|
|
2446a2fde8 | ||
|
|
d3c919385f |
@@ -3,14 +3,16 @@ FROM python:3.12-slim AS base
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends gcc g++ ca-certificates libssl-dev curl && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
gcc g++ libssl-dev \
|
||||
ca-certificates curl libnss3 libssl3 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY pyproject.toml ./
|
||||
|
||||
RUN pip install --no-cache-dir . && \
|
||||
pip install --no-cache-dir openbb-quantitative openbb-econometrics openbb-technical && \
|
||||
apt-get purge -y gcc g++ && \
|
||||
apt-get purge -y gcc g++ libssl-dev && \
|
||||
apt-get autoremove -y
|
||||
|
||||
COPY *.py ./
|
||||
|
||||
301
README.md
301
README.md
@@ -1,6 +1,6 @@
|
||||
# OpenBB Investment Analysis API
|
||||
|
||||
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.
|
||||
REST API wrapping OpenBB SDK with 99 endpoints covering stock data, sentiment analysis, technical indicators, quantitative risk metrics, fixed income, macro economics, shorts/dark pool, regulators, and rule-based investment analysis. 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
|
||||
|
||||
@@ -12,18 +12,24 @@ The core functionality uses **yfinance** (free, no API key). The API works witho
|
||||
|
||||
| Provider | Env Variable | How to Get | What It Unlocks | Free Limit |
|
||||
|----------|-------------|------------|-----------------|------------|
|
||||
| **Finnhub** | `INVEST_API_FINNHUB_API_KEY` | https://finnhub.io/register | Insider trades, analyst upgrades, recommendation trends | 60 calls/min |
|
||||
| **FRED** | `INVEST_API_FRED_API_KEY` | https://fred.stlouisfed.org/docs/api/api_key.html | Macro data: Fed rate, CPI, GDP, unemployment, treasury yields | 120 calls/min |
|
||||
| **Finnhub** | `INVEST_API_FINNHUB_API_KEY` | https://finnhub.io/register | Insider trades, recommendation trends, company news | 60 calls/min |
|
||||
| **FRED** | `INVEST_API_FRED_API_KEY` | https://fred.stlouisfed.org/docs/api/api_key.html | Macro data, fixed income, surveys, money supply | 120 calls/min |
|
||||
| **Alpha Vantage** | `INVEST_API_ALPHAVANTAGE_API_KEY` | https://www.alphavantage.co/support/#api-key | News sentiment scores (bullish/bearish per article per ticker) | 25 calls/day |
|
||||
|
||||
### Optional Paid Keys (for higher quality data)
|
||||
### Free Providers (no key needed)
|
||||
|
||||
| Provider | Env Variable | What It Adds |
|
||||
|----------|-------------|--------------|
|
||||
| **FMP** | `OBB_FMP_API_KEY` | More granular financials, earnings transcripts (250 calls/day free) |
|
||||
| **Intrinio** | `OBB_INTRINIO_API_KEY` | Institutional-grade fundamentals |
|
||||
| **Tiingo** | `OBB_TIINGO_TOKEN` | Reliable historical price data |
|
||||
| **Benzinga** | `OBB_BENZINGA_API_KEY` | Real-time news, analyst ratings |
|
||||
| Provider | Data Provided |
|
||||
|----------|---------------|
|
||||
| **yfinance** | Quotes, fundamentals, financials, historical prices, news, ETF, index, crypto, forex, options, futures, analyst upgrades, price targets, dividends, management |
|
||||
| **SEC** | Insider trading (Form 4), institutional holdings (13F), company filings, N-PORT disclosures, CIK mapping |
|
||||
| **stockgrid** | Short volume data |
|
||||
| **FINRA** | Short interest, dark pool OTC data |
|
||||
| **multpl** | S&P 500 historical valuation multiples |
|
||||
| **CFTC** | Commitment of Traders reports |
|
||||
| **ECB** | Currency reference rates |
|
||||
| **OECD** | GDP, unemployment, CPI, CLI, housing price index |
|
||||
| **openbb-technical** | 14 technical indicators (local computation) |
|
||||
| **openbb-quantitative** | Risk metrics, CAPM, normality tests (local computation) |
|
||||
|
||||
### Configuration
|
||||
|
||||
@@ -40,9 +46,10 @@ export INVEST_API_ALPHAVANTAGE_API_KEY=your_alphavantage_key
|
||||
### 1. Create conda environment
|
||||
|
||||
```bash
|
||||
conda env create -f environment.yml
|
||||
conda create -n openbb-invest-api python=3.12 -y
|
||||
conda activate openbb-invest-api
|
||||
pip install openbb-quantitative openbb-econometrics
|
||||
pip install -e .
|
||||
pip install openbb-quantitative openbb-econometrics openbb-technical
|
||||
```
|
||||
|
||||
### 2. Start the server
|
||||
@@ -59,36 +66,39 @@ Server starts at `http://localhost:8000`. Visit `http://localhost:8000/docs` for
|
||||
# Health check
|
||||
curl http://localhost:8000/health
|
||||
|
||||
# US stock quote
|
||||
# Stock quote
|
||||
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 (Finnhub + Alpha Vantage)
|
||||
curl http://localhost:8000/api/v1/stock/AAPL/sentiment
|
||||
|
||||
# News sentiment with per-article scores (Alpha Vantage)
|
||||
curl http://localhost:8000/api/v1/stock/AAPL/news-sentiment
|
||||
|
||||
# Technical indicators
|
||||
# Technical indicators (14 individual + composite)
|
||||
curl http://localhost:8000/api/v1/stock/AAPL/technical
|
||||
curl http://localhost:8000/api/v1/stock/AAPL/technical/ichimoku
|
||||
|
||||
# Quantitative risk metrics
|
||||
curl http://localhost:8000/api/v1/stock/AAPL/performance
|
||||
curl http://localhost:8000/api/v1/stock/AAPL/capm
|
||||
# Relative Rotation Graph (multi-symbol)
|
||||
curl "http://localhost:8000/api/v1/technical/relative-rotation?symbols=AAPL,MSFT,GOOGL&benchmark=SPY"
|
||||
|
||||
# SEC insider trading
|
||||
curl http://localhost:8000/api/v1/stock/AAPL/sec-insider
|
||||
# Quantitative analysis
|
||||
curl http://localhost:8000/api/v1/stock/AAPL/sortino
|
||||
curl http://localhost:8000/api/v1/stock/AAPL/rolling/skew?window=20
|
||||
|
||||
# ETF info
|
||||
curl http://localhost:8000/api/v1/etf/SPY/info
|
||||
# Fixed income
|
||||
curl http://localhost:8000/api/v1/fixed-income/yield-curve
|
||||
curl http://localhost:8000/api/v1/fixed-income/treasury-rates
|
||||
|
||||
# Crypto price history
|
||||
curl http://localhost:8000/api/v1/crypto/BTC-USD/historical?days=30
|
||||
|
||||
# Macro overview (requires FRED key)
|
||||
# Macro economics
|
||||
curl http://localhost:8000/api/v1/macro/overview
|
||||
curl http://localhost:8000/api/v1/macro/cpi
|
||||
curl http://localhost:8000/api/v1/macro/money-measures
|
||||
|
||||
# Economy surveys
|
||||
curl http://localhost:8000/api/v1/economy/surveys/michigan
|
||||
curl http://localhost:8000/api/v1/economy/surveys/sloos
|
||||
|
||||
# Shorts & dark pool
|
||||
curl http://localhost:8000/api/v1/stock/AAPL/shorts/volume
|
||||
curl http://localhost:8000/api/v1/darkpool/AAPL/otc
|
||||
|
||||
# Regulators
|
||||
curl "http://localhost:8000/api/v1/regulators/cot/search?query=gold"
|
||||
|
||||
# Portfolio analysis
|
||||
curl -X POST http://localhost:8000/api/v1/portfolio/analyze \
|
||||
@@ -96,7 +106,7 @@ curl -X POST http://localhost:8000/api/v1/portfolio/analyze \
|
||||
-d '{"holdings":[{"symbol":"AAPL","shares":100,"buy_in_price":150},{"symbol":"VOLV-B.ST","shares":50,"buy_in_price":250}]}'
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
## API Endpoints (99 total)
|
||||
|
||||
### Health
|
||||
|
||||
@@ -115,31 +125,111 @@ curl -X POST http://localhost:8000/api/v1/portfolio/analyze \
|
||||
| GET | `/api/v1/stock/{symbol}/historical?days=365` | Historical OHLCV data |
|
||||
| GET | `/api/v1/stock/{symbol}/news` | Recent company news |
|
||||
| GET | `/api/v1/stock/{symbol}/summary` | Aggregated: quote + profile + metrics + financials |
|
||||
| GET | `/api/v1/stock/{symbol}/management` | Executive team: name, title, compensation |
|
||||
| GET | `/api/v1/stock/{symbol}/dividends` | Historical dividend records |
|
||||
| GET | `/api/v1/stock/{symbol}/filings?form_type=10-K` | SEC filings (10-K, 10-Q, 8-K) |
|
||||
| GET | `/api/v1/search?query=` | Company search by name (SEC/NASDAQ) |
|
||||
|
||||
### Sentiment & Analyst Data (Finnhub + Alpha Vantage, free keys)
|
||||
### Sentiment & Analyst Data (Finnhub + Alpha Vantage + yfinance)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| 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 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 |
|
||||
| GET | `/api/v1/stock/{symbol}/recommendations` | Monthly analyst buy/hold/sell counts (Finnhub) |
|
||||
| GET | `/api/v1/stock/{symbol}/upgrades` | Analyst upgrades/downgrades with price targets (yfinance) |
|
||||
|
||||
### Technical Analysis (local computation, no key needed)
|
||||
### Technical Analysis (14 indicators, local computation, no key needed)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/stock/{symbol}/technical` | RSI, MACD, SMA, EMA, Bollinger Bands + signal interpretation |
|
||||
| GET | `/api/v1/stock/{symbol}/technical` | Composite: RSI, MACD, SMA, EMA, Bollinger Bands + signals |
|
||||
| GET | `/api/v1/stock/{symbol}/technical/atr` | Average True Range (volatility, position sizing) |
|
||||
| GET | `/api/v1/stock/{symbol}/technical/adx` | Average Directional Index (trend strength) |
|
||||
| GET | `/api/v1/stock/{symbol}/technical/stoch` | Stochastic Oscillator (overbought/oversold) |
|
||||
| GET | `/api/v1/stock/{symbol}/technical/obv` | On-Balance Volume (volume-price divergence) |
|
||||
| GET | `/api/v1/stock/{symbol}/technical/ichimoku` | Ichimoku Cloud (comprehensive trend system) |
|
||||
| GET | `/api/v1/stock/{symbol}/technical/donchian` | Donchian Channels (breakout detection) |
|
||||
| GET | `/api/v1/stock/{symbol}/technical/aroon` | Aroon Indicator (trend direction/changes) |
|
||||
| GET | `/api/v1/stock/{symbol}/technical/cci` | Commodity Channel Index (cyclical trends) |
|
||||
| GET | `/api/v1/stock/{symbol}/technical/kc` | Keltner Channels (ATR-based volatility bands) |
|
||||
| GET | `/api/v1/stock/{symbol}/technical/fib` | Fibonacci Retracement (support/resistance levels) |
|
||||
| GET | `/api/v1/stock/{symbol}/technical/ad` | Accumulation/Distribution Line |
|
||||
| GET | `/api/v1/stock/{symbol}/technical/cones` | Volatility Cones (realized vol quantiles) |
|
||||
| GET | `/api/v1/stock/{symbol}/technical/vwap` | Volume Weighted Average Price |
|
||||
| GET | `/api/v1/technical/relative-rotation?symbols=&benchmark=SPY` | Relative Rotation Graph (multi-symbol sector rotation) |
|
||||
|
||||
### Quantitative Analysis (openbb-quantitative, no key needed)
|
||||
### Quantitative Analysis (local computation, 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}/normality?days=365` | Normality tests: Jarque-Bera, Shapiro-Wilk, K-S |
|
||||
| GET | `/api/v1/stock/{symbol}/unitroot?days=365` | Unit root tests: ADF, KPSS for stationarity |
|
||||
| GET | `/api/v1/stock/{symbol}/sortino?days=365` | Sortino ratio (downside risk only) |
|
||||
| GET | `/api/v1/stock/{symbol}/omega?days=365` | Omega ratio (full distribution gain/loss) |
|
||||
| GET | `/api/v1/stock/{symbol}/rolling/{stat}?days=365&window=30` | Rolling stats: variance, stdev, mean, skew, kurtosis, quantile |
|
||||
|
||||
### Shorts & Dark Pool (stockgrid/FINRA/SEC, no key needed)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/stock/{symbol}/shorts/volume` | Daily short volume and percent (stockgrid) |
|
||||
| GET | `/api/v1/stock/{symbol}/shorts/ftd` | Fails-to-deliver records (SEC) |
|
||||
| GET | `/api/v1/stock/{symbol}/shorts/interest` | Short interest, days to cover (FINRA) |
|
||||
| GET | `/api/v1/darkpool/{symbol}/otc` | OTC/dark pool aggregate trade volume (FINRA) |
|
||||
|
||||
### Fixed Income (FRED/Federal Reserve, free key)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/fixed-income/treasury-rates` | Full yield curve rates (4W-30Y) |
|
||||
| GET | `/api/v1/fixed-income/yield-curve?date=` | Yield curve with maturity/rate pairs |
|
||||
| GET | `/api/v1/fixed-income/treasury-auctions` | Treasury auction bid-to-cover, yields |
|
||||
| GET | `/api/v1/fixed-income/tips-yields` | TIPS real yields by maturity |
|
||||
| GET | `/api/v1/fixed-income/effr` | Effective Federal Funds Rate with percentiles |
|
||||
| GET | `/api/v1/fixed-income/sofr` | SOFR rate with 30/90/180-day moving averages |
|
||||
| GET | `/api/v1/fixed-income/hqm` | High Quality Market corporate bond yields |
|
||||
| GET | `/api/v1/fixed-income/commercial-paper` | Commercial paper rates by maturity/type |
|
||||
| GET | `/api/v1/fixed-income/spot-rates` | Corporate bond spot rates and par yields |
|
||||
| GET | `/api/v1/fixed-income/spreads?series=tcm` | Treasury/corporate spreads |
|
||||
|
||||
### Macro Economics (FRED/OECD, free key)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/macro/overview` | Key indicators: Fed rate, treasury yields, CPI, unemployment, GDP, VIX |
|
||||
| GET | `/api/v1/macro/series/{series_id}?limit=30` | Any FRED time series by ID |
|
||||
| GET | `/api/v1/macro/cpi?country=united_states` | Consumer Price Index (multi-country) |
|
||||
| GET | `/api/v1/macro/gdp?gdp_type=real` | GDP: nominal, real, or forecast |
|
||||
| GET | `/api/v1/macro/unemployment?country=united_states` | Unemployment rate (multi-country) |
|
||||
| GET | `/api/v1/macro/pce` | Personal Consumption Expenditures (Fed preferred inflation) |
|
||||
| GET | `/api/v1/macro/money-measures` | M1/M2 money supply |
|
||||
| GET | `/api/v1/macro/cli?country=united_states` | Composite Leading Indicator (recession predictor) |
|
||||
| GET | `/api/v1/macro/house-price-index?country=united_states` | Housing price index (multi-country) |
|
||||
|
||||
### Economy Data (FRED/Federal Reserve)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/economy/fred-search?query=` | Search 800K+ FRED economic series |
|
||||
| GET | `/api/v1/economy/fred-regional?series_id=` | Regional economic data (by state/county/MSA) |
|
||||
| GET | `/api/v1/economy/balance-of-payments` | Current/capital/financial account balances |
|
||||
| GET | `/api/v1/economy/central-bank-holdings` | Fed SOMA portfolio holdings |
|
||||
| GET | `/api/v1/economy/primary-dealer-positioning` | Primary dealer net positions |
|
||||
| GET | `/api/v1/economy/fomc-documents?year=` | FOMC meeting documents |
|
||||
|
||||
### Economy Surveys (FRED/BLS)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/economy/surveys/michigan` | University of Michigan Consumer Sentiment |
|
||||
| GET | `/api/v1/economy/surveys/sloos` | Senior Loan Officer Survey (recession signal) |
|
||||
| GET | `/api/v1/economy/surveys/nonfarm-payrolls` | Detailed employment data |
|
||||
| GET | `/api/v1/economy/surveys/empire-state` | NY manufacturing outlook |
|
||||
| GET | `/api/v1/economy/surveys/bls-search?query=` | Search BLS data series |
|
||||
|
||||
### Calendar Events (no key needed)
|
||||
|
||||
@@ -160,20 +250,23 @@ curl -X POST http://localhost:8000/api/v1/portfolio/analyze \
|
||||
| GET | `/api/v1/stock/{symbol}/institutional` | Institutional holders from SEC 13F filings |
|
||||
| GET | `/api/v1/screener` | Stock screener |
|
||||
|
||||
### ETF Data (yfinance, no key needed)
|
||||
### ETF Data (yfinance + SEC, 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/{symbol}/nport` | Detailed ETF holdings from SEC N-PORT filings |
|
||||
| GET | `/api/v1/etf/search?query=` | Search ETFs by name |
|
||||
|
||||
### Index Data (yfinance, no key needed)
|
||||
### Index Data (yfinance + multpl + cboe, 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) |
|
||||
| GET | `/api/v1/index/sp500-multiples?series=pe_ratio` | Historical S&P 500 valuation (PE, Shiller PE, P/B, dividend yield) |
|
||||
| GET | `/api/v1/index/{symbol}/constituents` | Index member stocks with sector/price data |
|
||||
|
||||
### Crypto Data (yfinance, no key needed)
|
||||
|
||||
@@ -182,11 +275,12 @@ curl -X POST http://localhost:8000/api/v1/portfolio/analyze \
|
||||
| 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)
|
||||
### Currency / Forex (yfinance + ECB, no key needed)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/currency/{symbol}/historical?days=365` | Forex price history (EURUSD, USDSEK) |
|
||||
| GET | `/api/v1/currency/reference-rates` | ECB reference rates for 28 major currencies |
|
||||
|
||||
### Derivatives (yfinance, no key needed)
|
||||
|
||||
@@ -196,12 +290,15 @@ curl -X POST http://localhost:8000/api/v1/portfolio/analyze \
|
||||
| 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)
|
||||
### Regulators (CFTC/SEC, no key needed)
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/v1/macro/overview` | Key indicators: Fed rate, treasury yields, CPI, unemployment, GDP, VIX |
|
||||
| GET | `/api/v1/macro/series/{series_id}?limit=30` | Any FRED time series by ID |
|
||||
| GET | `/api/v1/regulators/cot?symbol=` | Commitment of Traders (futures positions) |
|
||||
| GET | `/api/v1/regulators/cot/search?query=` | Search COT report symbols |
|
||||
| GET | `/api/v1/regulators/sec/litigation` | SEC litigation releases |
|
||||
| GET | `/api/v1/regulators/sec/institutions?query=` | Search institutional investors |
|
||||
| GET | `/api/v1/regulators/sec/cik-map/{symbol}` | Ticker to SEC CIK mapping |
|
||||
|
||||
### Portfolio Analysis (no key needed)
|
||||
|
||||
@@ -256,38 +353,51 @@ All settings are configurable via environment variables with the `INVEST_API_` p
|
||||
| `INVEST_API_LOG_LEVEL` | `info` | Logging level |
|
||||
| `INVEST_API_DEBUG` | `false` | Enable debug mode (auto-reload) |
|
||||
| `INVEST_API_FINNHUB_API_KEY` | _(empty)_ | Finnhub API key for analyst data |
|
||||
| `INVEST_API_FRED_API_KEY` | _(empty)_ | FRED API key for macro data |
|
||||
| `INVEST_API_FRED_API_KEY` | _(empty)_ | FRED API key for macro/fixed income/surveys |
|
||||
| `INVEST_API_ALPHAVANTAGE_API_KEY` | _(empty)_ | Alpha Vantage API key for news sentiment |
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
openbb-invest-api/
|
||||
├── main.py # FastAPI app entry point
|
||||
├── config.py # Settings (env-based)
|
||||
├── models.py # Pydantic request/response models
|
||||
├── mappers.py # Dict-to-model mapping functions
|
||||
├── route_utils.py # Shared route utilities (validation, error handling)
|
||||
├── obb_utils.py # Shared OpenBB result conversion utilities
|
||||
├── 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
|
||||
├── main.py # FastAPI app entry point (lifespan, curl_cffi patch)
|
||||
├── config.py # Settings (env-based)
|
||||
├── models.py # Pydantic request/response models
|
||||
├── mappers.py # Dict-to-model mapping functions
|
||||
├── route_utils.py # Shared route utilities (validation, error handling)
|
||||
├── obb_utils.py # Shared OpenBB result conversion + fetch helpers
|
||||
│
|
||||
├── openbb_service.py # Equity data via OpenBB/yfinance (quote, profile, metrics, etc.)
|
||||
├── finnhub_service.py # Finnhub REST client (insider, analyst data)
|
||||
├── alphavantage_service.py # Alpha Vantage REST client (news sentiment)
|
||||
├── technical_service.py # 14 technical indicators via openbb-technical
|
||||
├── quantitative_service.py # Risk metrics, CAPM, Sortino, Omega, rolling stats
|
||||
├── macro_service.py # FRED macro data via OpenBB
|
||||
├── economy_service.py # Economy data: CPI, GDP, Fed holdings, FOMC docs
|
||||
├── surveys_service.py # Economy surveys: Michigan, SLOOS, NFP, BLS
|
||||
├── fixed_income_service.py # Fixed income: yield curve, treasury, SOFR, spreads
|
||||
├── shorts_service.py # Shorts & dark pool (stockgrid, FINRA, SEC)
|
||||
├── regulators_service.py # CFTC COT reports, SEC litigation, institutions
|
||||
├── market_service.py # ETF, index, crypto, currency, derivatives
|
||||
├── calendar_service.py # Calendar events, screening, ownership
|
||||
├── analysis_service.py # Rule engine for portfolio analysis
|
||||
│
|
||||
├── routes.py # Core stock data + portfolio + discovery routes
|
||||
├── routes_sentiment.py # Sentiment & analyst routes
|
||||
├── routes_technical.py # Technical analysis routes (14 indicators)
|
||||
├── routes_quantitative.py # Quantitative analysis routes
|
||||
├── routes_macro.py # Macro economics routes
|
||||
├── routes_economy.py # Economy data routes
|
||||
├── routes_surveys.py # Economy survey routes
|
||||
├── routes_fixed_income.py # Fixed income routes
|
||||
├── routes_shorts.py # Shorts & dark pool routes
|
||||
├── routes_regulators.py # Regulator data routes
|
||||
├── routes_calendar.py # Calendar, estimates, ownership routes
|
||||
├── routes_market.py # ETF, index, crypto, currency, derivatives routes
|
||||
│
|
||||
├── Dockerfile # Docker build (curl_cffi==0.7.4, safari TLS patch)
|
||||
├── pyproject.toml # Project metadata + dependencies
|
||||
└── tests/ # 102 tests
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
@@ -315,11 +425,13 @@ 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/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
|
||||
5. OpenClaw calls `GET /api/v1/stock/AAPL/performance` for risk metrics (Sharpe, Sortino)
|
||||
6. OpenClaw calls `GET /api/v1/stock/AAPL/shorts/volume` for short selling activity
|
||||
7. OpenClaw calls `GET /api/v1/stock/AAPL/sec-insider` for insider trading activity
|
||||
8. OpenClaw calls `GET /api/v1/macro/overview` for market context
|
||||
9. OpenClaw calls `GET /api/v1/fixed-income/yield-curve` for rate environment
|
||||
10. OpenClaw calls `POST /api/v1/portfolio/analyze` with user's holdings
|
||||
11. OpenClaw's LLM synthesizes all structured data into a personalized recommendation
|
||||
|
||||
## Kubernetes Deployment
|
||||
|
||||
@@ -333,7 +445,7 @@ Example OpenClaw workflow:
|
||||
### Architecture
|
||||
|
||||
```
|
||||
git push → Gitea → Drone CI (kaniko) → Docker Registry → ArgoCD → K8s
|
||||
git push -> Gitea -> Drone CI (kaniko) -> Docker Registry -> ArgoCD -> K8s
|
||||
```
|
||||
|
||||
### Cluster Info
|
||||
@@ -410,7 +522,7 @@ steps:
|
||||
--from-literal=INVEST_API_ALPHAVANTAGE_API_KEY=your_key
|
||||
```
|
||||
|
||||
6. Add DNS: `invest-api.k8s.home → 192.168.68.22`
|
||||
6. Add DNS: `invest-api.k8s.home -> 192.168.68.22`
|
||||
|
||||
7. Verify:
|
||||
```bash
|
||||
@@ -431,10 +543,25 @@ docker run -p 8000:8000 invest-api
|
||||
|
||||
| Source | Cost | Key Required | Data Provided |
|
||||
|--------|------|-------------|---------------|
|
||||
| **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 |
|
||||
| **yfinance** | Free | No | Quotes, fundamentals, financials, historical prices, news, discovery, ETF, index, crypto, forex, options, futures, analyst upgrades/downgrades, price targets, dividends, management |
|
||||
| **SEC** | Free | No | Insider trading (Form 4), institutional holdings (13F), company filings, N-PORT disclosures, CIK mapping, litigation releases |
|
||||
| **stockgrid** | Free | No | Daily short volume data |
|
||||
| **FINRA** | Free | No | Short interest, dark pool OTC trade data |
|
||||
| **CFTC** | Free | No | Commitment of Traders reports |
|
||||
| **multpl** | Free | No | S&P 500 historical valuation multiples (PE, Shiller PE, P/B, dividend yield) |
|
||||
| **ECB** | Free | No | Currency reference rates (28 currencies) |
|
||||
| **OECD** | Free | No | GDP, unemployment, CPI, Composite Leading Indicator, housing price index |
|
||||
| **Finnhub** | Free | Yes (free registration) | Insider trades, analyst recommendations, company news |
|
||||
| **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 |
|
||||
| **FRED** | Free | Yes (free registration) | Fed rate, treasury yields, CPI, PCE, money supply, surveys, 800K+ economic series |
|
||||
| **Federal Reserve** | Free | No | EFFR, SOFR, money measures, central bank holdings, primary dealer positions, FOMC documents |
|
||||
| **openbb-technical** | Free | No (local) | ATR, ADX, Stochastic, OBV, Ichimoku, Donchian, Aroon, CCI, Keltner, Fibonacci, A/D, VWAP, Volatility Cones, Relative Rotation |
|
||||
| **openbb-quantitative** | Free | No (local) | Sharpe, Sortino, Omega ratios, CAPM, normality tests, unit root tests, rolling statistics |
|
||||
|
||||
## Known Issues
|
||||
|
||||
### curl_cffi TLS Fingerprint
|
||||
|
||||
yfinance depends on `curl_cffi` for browser-impersonated HTTP requests. Versions 0.12+ have a BoringSSL bug
|
||||
that causes `SSL_ERROR_SYSCALL` on some networks (especially Linux). This project pins `curl_cffi==0.7.4`
|
||||
and patches the default TLS fingerprint from `chrome` to `safari` at startup (in `main.py`) to work around this.
|
||||
|
||||
410
SKILL.md
Normal file
410
SKILL.md
Normal file
@@ -0,0 +1,410 @@
|
||||
---
|
||||
name: invest-api
|
||||
description: "Investment analysis via OpenBB Invest API (99 endpoints). Use when: user asks about stocks, ETFs, crypto, portfolio analysis, technical indicators (14 types), quantitative risk metrics, fixed income/yield curve, macro economics, shorts/dark pool, regulators/COT, economy surveys, or investment advice. Covers US and Swedish markets. NOT for: real-time trading, order execution, or account management. No API key needed for most endpoints."
|
||||
homepage: https://invest-api.k8s.home/docs
|
||||
metadata: { "openclaw": { "emoji": "📈", "requires": { "bins": ["curl"] } } }
|
||||
---
|
||||
|
||||
# Investment Analysis Skill
|
||||
|
||||
Query stock data, run portfolio analysis, and get investment insights via the OpenBB Invest API. 99 endpoints across 14 data providers.
|
||||
|
||||
## When to Use
|
||||
|
||||
- "What's AAPL trading at?"
|
||||
- "Analyze my portfolio" / "Should I buy or sell?"
|
||||
- "Show me technical indicators for TSLA" / "What's the Ichimoku cloud for NVDA?"
|
||||
- "What are the top gainers today?"
|
||||
- "Any upcoming earnings this week?"
|
||||
- "What's the macro outlook?" / "What's the yield curve look like?"
|
||||
- "How risky is my VOLV-B.ST position?" / "What's the Sortino ratio?"
|
||||
- "Is anyone shorting AAPL?" / "Show dark pool activity"
|
||||
- "What are the latest FOMC documents?"
|
||||
- "Where are gold futures positioned?" (COT data)
|
||||
- "Compare AAPL, MSFT, GOOGL sector rotation vs SPY"
|
||||
|
||||
## When NOT to Use
|
||||
|
||||
- Placing trades or managing brokerage accounts
|
||||
- Real-time tick-by-tick data (this API has request latency)
|
||||
- Cryptocurrency on-chain analysis
|
||||
- Tax or accounting advice
|
||||
|
||||
## API Base URL
|
||||
|
||||
```
|
||||
BASE=https://invest-api.k8s.home
|
||||
```
|
||||
|
||||
All responses follow `{"success": true, "data": {...}, "error": null}`.
|
||||
|
||||
## Stock Data (no key needed)
|
||||
|
||||
```bash
|
||||
# Current quote
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/quote"
|
||||
|
||||
# Company profile (sector, industry, description)
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/profile"
|
||||
|
||||
# Key ratios (PE, PB, ROE, EPS, beta, dividend yield)
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/metrics"
|
||||
|
||||
# Financial statements (income, balance sheet, cash flow)
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/financials"
|
||||
|
||||
# Historical prices (OHLCV)
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/historical?days=365"
|
||||
|
||||
# Recent news
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/news"
|
||||
|
||||
# All-in-one summary (quote + profile + metrics + financials)
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/summary"
|
||||
|
||||
# Executive team and compensation
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/management"
|
||||
|
||||
# Historical dividend records
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/dividends"
|
||||
|
||||
# SEC filings (10-K, 10-Q, 8-K)
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/filings?form_type=10-K"
|
||||
|
||||
# Company search by name
|
||||
curl -sk "$BASE/api/v1/search?query=apple"
|
||||
```
|
||||
|
||||
## Swedish Stocks
|
||||
|
||||
Append `.ST` suffix for Stockholm exchange:
|
||||
|
||||
```bash
|
||||
curl -sk "$BASE/api/v1/stock/VOLV-B.ST/quote" # Volvo
|
||||
curl -sk "$BASE/api/v1/stock/ERIC-B.ST/quote" # Ericsson
|
||||
curl -sk "$BASE/api/v1/stock/HM-B.ST/quote" # H&M
|
||||
curl -sk "$BASE/api/v1/stock/SEB-A.ST/quote" # SEB
|
||||
```
|
||||
|
||||
## Portfolio Analysis
|
||||
|
||||
Provide holdings with buy-in cost. Returns BUY_MORE / HOLD / SELL recommendation per holding with confidence and reasons.
|
||||
|
||||
```bash
|
||||
curl -sk -X POST "$BASE/api/v1/portfolio/analyze" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"holdings": [
|
||||
{"symbol": "AAPL", "shares": 100, "buy_in_price": 150.0},
|
||||
{"symbol": "VOLV-B.ST", "shares": 50, "buy_in_price": 250.0},
|
||||
{"symbol": "MSFT", "shares": 30, "buy_in_price": 380.0}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
Max 50 holdings per request.
|
||||
|
||||
## Technical Analysis (14 indicators)
|
||||
|
||||
```bash
|
||||
# Composite: RSI, MACD, SMA, EMA, Bollinger Bands + signal interpretation
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/technical"
|
||||
|
||||
# Individual indicators
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/technical/atr" # Average True Range (volatility)
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/technical/adx" # Trend strength (>25 strong)
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/technical/stoch" # Stochastic (overbought/oversold)
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/technical/obv" # On-Balance Volume
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/technical/ichimoku" # Ichimoku Cloud
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/technical/donchian" # Donchian Channels (breakouts)
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/technical/aroon" # Aroon (trend direction)
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/technical/cci" # Commodity Channel Index
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/technical/kc" # Keltner Channels
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/technical/fib" # Fibonacci Retracement
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/technical/ad" # Accumulation/Distribution
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/technical/cones" # Volatility Cones
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/technical/vwap" # Volume Weighted Average Price
|
||||
|
||||
# Relative Rotation Graph (multi-symbol sector rotation)
|
||||
curl -sk "$BASE/api/v1/technical/relative-rotation?symbols=AAPL,MSFT,GOOGL&benchmark=SPY"
|
||||
# Returns quadrant per symbol: Leading / Weakening / Lagging / Improving
|
||||
```
|
||||
|
||||
## Quantitative Risk Metrics
|
||||
|
||||
```bash
|
||||
# Sharpe ratio, volatility, summary statistics
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/performance?days=365"
|
||||
|
||||
# CAPM: market risk, systematic risk, idiosyncratic risk
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/capm"
|
||||
|
||||
# Sortino ratio (downside risk only -- better than Sharpe for asymmetric returns)
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/sortino?days=365"
|
||||
|
||||
# Omega ratio (full distribution gain/loss)
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/omega?days=365"
|
||||
|
||||
# Normality tests (Jarque-Bera, Shapiro-Wilk)
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/normality?days=365"
|
||||
|
||||
# Unit root / stationarity tests (ADF, KPSS)
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/unitroot?days=365"
|
||||
|
||||
# Rolling statistics (variance, stdev, mean, skew, kurtosis, quantile)
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/rolling/skew?days=365&window=30"
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/rolling/variance?days=365&window=20"
|
||||
```
|
||||
|
||||
## Shorts & Dark Pool (no key needed)
|
||||
|
||||
```bash
|
||||
# Daily short volume and percent
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/shorts/volume"
|
||||
|
||||
# Fails-to-deliver records (SEC)
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/shorts/ftd"
|
||||
|
||||
# Short interest, days to cover (FINRA)
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/shorts/interest"
|
||||
|
||||
# Dark pool / OTC trade volume (FINRA)
|
||||
curl -sk "$BASE/api/v1/darkpool/AAPL/otc"
|
||||
```
|
||||
|
||||
## Sentiment & Analyst Data
|
||||
|
||||
```bash
|
||||
# Aggregated sentiment (news + recommendations + upgrades)
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/sentiment"
|
||||
|
||||
# Per-article news sentiment scores
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/news-sentiment?limit=30"
|
||||
|
||||
# Insider trades (SEC/Finnhub)
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/insider-trades"
|
||||
|
||||
# Analyst buy/hold/sell counts
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/recommendations"
|
||||
|
||||
# Recent upgrades/downgrades with price targets
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/upgrades"
|
||||
```
|
||||
|
||||
## Fixed Income (FRED/Federal Reserve)
|
||||
|
||||
```bash
|
||||
# Full yield curve rates (4W-30Y)
|
||||
curl -sk "$BASE/api/v1/fixed-income/treasury-rates"
|
||||
|
||||
# Yield curve with maturity/rate pairs (optional date)
|
||||
curl -sk "$BASE/api/v1/fixed-income/yield-curve"
|
||||
curl -sk "$BASE/api/v1/fixed-income/yield-curve?date=2025-01-15"
|
||||
|
||||
# Treasury auction data (bid-to-cover, yields)
|
||||
curl -sk "$BASE/api/v1/fixed-income/treasury-auctions"
|
||||
|
||||
# TIPS real yields
|
||||
curl -sk "$BASE/api/v1/fixed-income/tips-yields"
|
||||
|
||||
# Effective Federal Funds Rate (with percentiles)
|
||||
curl -sk "$BASE/api/v1/fixed-income/effr"
|
||||
|
||||
# SOFR rate (with moving averages)
|
||||
curl -sk "$BASE/api/v1/fixed-income/sofr"
|
||||
|
||||
# Corporate bond yields (AAA/AA/A)
|
||||
curl -sk "$BASE/api/v1/fixed-income/hqm"
|
||||
|
||||
# Commercial paper rates
|
||||
curl -sk "$BASE/api/v1/fixed-income/commercial-paper"
|
||||
|
||||
# Corporate bond spot rates
|
||||
curl -sk "$BASE/api/v1/fixed-income/spot-rates"
|
||||
|
||||
# Treasury/corporate spreads
|
||||
curl -sk "$BASE/api/v1/fixed-income/spreads?series=tcm"
|
||||
```
|
||||
|
||||
## Macro Economics
|
||||
|
||||
```bash
|
||||
# Key indicators: Fed rate, treasury yields, CPI, unemployment, GDP, VIX
|
||||
curl -sk "$BASE/api/v1/macro/overview"
|
||||
|
||||
# Any FRED time series by ID
|
||||
curl -sk "$BASE/api/v1/macro/series/DFF?limit=30" # Fed Funds Rate
|
||||
curl -sk "$BASE/api/v1/macro/series/CPIAUCSL?limit=12" # CPI
|
||||
|
||||
# Structured macro data (multi-country support)
|
||||
curl -sk "$BASE/api/v1/macro/cpi?country=united_states"
|
||||
curl -sk "$BASE/api/v1/macro/gdp?gdp_type=real"
|
||||
curl -sk "$BASE/api/v1/macro/unemployment?country=united_states"
|
||||
curl -sk "$BASE/api/v1/macro/pce" # Fed preferred inflation
|
||||
curl -sk "$BASE/api/v1/macro/money-measures" # M1/M2 money supply
|
||||
curl -sk "$BASE/api/v1/macro/cli?country=united_states" # Leading indicator
|
||||
curl -sk "$BASE/api/v1/macro/house-price-index?country=united_states"
|
||||
```
|
||||
|
||||
## Economy Data & Surveys
|
||||
|
||||
```bash
|
||||
# Search 800K+ FRED economic series
|
||||
curl -sk "$BASE/api/v1/economy/fred-search?query=inflation"
|
||||
|
||||
# Regional economic data (by state/county)
|
||||
curl -sk "$BASE/api/v1/economy/fred-regional?series_id=UNRATE"
|
||||
|
||||
# Fed balance sheet / SOMA portfolio
|
||||
curl -sk "$BASE/api/v1/economy/central-bank-holdings"
|
||||
|
||||
# Primary dealer positions (Wall Street firm positioning)
|
||||
curl -sk "$BASE/api/v1/economy/primary-dealer-positioning"
|
||||
|
||||
# Balance of payments
|
||||
curl -sk "$BASE/api/v1/economy/balance-of-payments"
|
||||
|
||||
# FOMC meeting documents
|
||||
curl -sk "$BASE/api/v1/economy/fomc-documents?year=2026"
|
||||
|
||||
# Consumer sentiment (recession predictor)
|
||||
curl -sk "$BASE/api/v1/economy/surveys/michigan"
|
||||
|
||||
# Senior Loan Officer Survey (strongest recession signal)
|
||||
curl -sk "$BASE/api/v1/economy/surveys/sloos"
|
||||
|
||||
# Detailed employment data
|
||||
curl -sk "$BASE/api/v1/economy/surveys/nonfarm-payrolls"
|
||||
|
||||
# NY manufacturing outlook
|
||||
curl -sk "$BASE/api/v1/economy/surveys/empire-state"
|
||||
|
||||
# BLS data search
|
||||
curl -sk "$BASE/api/v1/economy/surveys/bls-search?query=wages"
|
||||
```
|
||||
|
||||
## Market Data
|
||||
|
||||
```bash
|
||||
# ETF
|
||||
curl -sk "$BASE/api/v1/etf/SPY/info"
|
||||
curl -sk "$BASE/api/v1/etf/SPY/historical?days=365"
|
||||
curl -sk "$BASE/api/v1/etf/SPY/nport" # Detailed N-PORT holdings
|
||||
curl -sk "$BASE/api/v1/etf/search?query=technology"
|
||||
|
||||
# Index
|
||||
curl -sk "$BASE/api/v1/index/available"
|
||||
curl -sk "$BASE/api/v1/index/%5EGSPC/historical?days=365" # S&P 500
|
||||
curl -sk "$BASE/api/v1/index/sp500-multiples?series=pe_ratio" # Historical S&P 500 valuation
|
||||
curl -sk "$BASE/api/v1/index/%5EGSPC/constituents" # Index member stocks
|
||||
|
||||
# Crypto
|
||||
curl -sk "$BASE/api/v1/crypto/BTC-USD/historical?days=30"
|
||||
curl -sk "$BASE/api/v1/crypto/search?query=bitcoin"
|
||||
|
||||
# Forex
|
||||
curl -sk "$BASE/api/v1/currency/USDSEK/historical?days=365"
|
||||
curl -sk "$BASE/api/v1/currency/reference-rates" # ECB rates (28 currencies)
|
||||
|
||||
# Options
|
||||
curl -sk "$BASE/api/v1/options/AAPL/chains"
|
||||
|
||||
# Futures
|
||||
curl -sk "$BASE/api/v1/futures/CL=F/historical?days=365"
|
||||
curl -sk "$BASE/api/v1/futures/CL=F/curve"
|
||||
```
|
||||
|
||||
## Regulators (no key needed)
|
||||
|
||||
```bash
|
||||
# Commitment of Traders (futures positions)
|
||||
curl -sk "$BASE/api/v1/regulators/cot?symbol=GC=F"
|
||||
curl -sk "$BASE/api/v1/regulators/cot/search?query=gold"
|
||||
|
||||
# SEC litigation releases
|
||||
curl -sk "$BASE/api/v1/regulators/sec/litigation"
|
||||
|
||||
# Search institutional investors
|
||||
curl -sk "$BASE/api/v1/regulators/sec/institutions?query=berkshire"
|
||||
|
||||
# Ticker to CIK mapping
|
||||
curl -sk "$BASE/api/v1/regulators/sec/cik-map/AAPL"
|
||||
```
|
||||
|
||||
## Calendar Events
|
||||
|
||||
```bash
|
||||
curl -sk "$BASE/api/v1/calendar/earnings?start_date=2026-03-10&end_date=2026-03-17"
|
||||
curl -sk "$BASE/api/v1/calendar/dividends?start_date=2026-03-10&end_date=2026-03-17"
|
||||
curl -sk "$BASE/api/v1/calendar/ipo?start_date=2026-03-01&end_date=2026-03-31"
|
||||
curl -sk "$BASE/api/v1/calendar/splits?start_date=2026-03-01&end_date=2026-03-31"
|
||||
```
|
||||
|
||||
## Stock Discovery
|
||||
|
||||
```bash
|
||||
curl -sk "$BASE/api/v1/discover/gainers" # Top gainers
|
||||
curl -sk "$BASE/api/v1/discover/losers" # Top losers
|
||||
curl -sk "$BASE/api/v1/discover/active" # Most active
|
||||
curl -sk "$BASE/api/v1/discover/undervalued" # Undervalued large caps
|
||||
curl -sk "$BASE/api/v1/discover/growth" # Growth tech stocks
|
||||
```
|
||||
|
||||
## Estimates & Ownership
|
||||
|
||||
```bash
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/estimates" # Analyst consensus
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/share-statistics" # Float, short interest
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/sec-insider" # SEC Form 4
|
||||
curl -sk "$BASE/api/v1/stock/AAPL/institutional" # 13F institutional holders
|
||||
curl -sk "$BASE/api/v1/screener" # Stock screener
|
||||
```
|
||||
|
||||
## Recommended Workflows
|
||||
|
||||
### Comprehensive Stock Analysis
|
||||
1. `/stock/{symbol}/summary` -- fundamentals overview
|
||||
2. `/stock/{symbol}/technical` -- composite technical signals
|
||||
3. `/stock/{symbol}/technical/ichimoku` -- trend system
|
||||
4. `/stock/{symbol}/performance` -- Sharpe, volatility
|
||||
5. `/stock/{symbol}/sortino` -- downside risk
|
||||
6. `/stock/{symbol}/shorts/volume` -- short selling pressure
|
||||
7. `/stock/{symbol}/sentiment` -- market sentiment
|
||||
8. `/stock/{symbol}/sec-insider` -- insider activity
|
||||
9. `/stock/{symbol}/upgrades` -- analyst actions + price targets
|
||||
10. `/macro/overview` -- market context
|
||||
11. `/portfolio/analyze` -- rule-engine recommendation
|
||||
|
||||
### Macro & Fixed Income Analysis
|
||||
1. `/macro/overview` -- headline indicators
|
||||
2. `/fixed-income/yield-curve` -- rate environment
|
||||
3. `/fixed-income/effr` -- Fed funds rate detail
|
||||
4. `/macro/cpi` -- inflation
|
||||
5. `/macro/pce` -- Fed preferred inflation
|
||||
6. `/macro/money-measures` -- M1/M2 money supply
|
||||
7. `/economy/surveys/michigan` -- consumer confidence
|
||||
8. `/economy/surveys/sloos` -- lending conditions
|
||||
9. `/macro/cli` -- leading indicator (recession risk)
|
||||
|
||||
### Sector Rotation Analysis
|
||||
1. `/technical/relative-rotation?symbols=XLK,XLF,XLE,XLV&benchmark=SPY` -- RRG quadrants
|
||||
2. `/index/sp500-multiples?series=shiller_pe_ratio` -- market valuation context
|
||||
3. `/discover/gainers` + `/discover/losers` -- daily movers
|
||||
|
||||
### Short Squeeze Screening
|
||||
1. `/stock/{symbol}/shorts/volume` -- short volume %
|
||||
2. `/stock/{symbol}/shorts/interest` -- days to cover
|
||||
3. `/stock/{symbol}/shorts/ftd` -- fails to deliver
|
||||
4. `/darkpool/{symbol}/otc` -- dark pool activity
|
||||
5. `/stock/{symbol}/share-statistics` -- float, short % of float
|
||||
|
||||
Synthesize all data into a coherent recommendation. The API provides structured data only -- all reasoning and natural language analysis should be done by the LLM.
|
||||
|
||||
## Notes
|
||||
|
||||
- Prices are delayed (not real-time); yfinance data is typically 15-20 min delayed
|
||||
- Swedish stocks use `.ST` suffix (Stockholm exchange)
|
||||
- Portfolio analysis uses a rule engine (PE, revenue growth, P&L, analyst targets) returning BUY_MORE/HOLD/SELL
|
||||
- The `-k` flag is needed because the API uses a self-signed TLS certificate
|
||||
- Multi-country support for CPI, GDP, unemployment, CLI, HPI (use `country=united_states`, `country=japan`, etc.)
|
||||
- Health check: `curl -sk "$BASE/health"`
|
||||
185
economy_service.py
Normal file
185
economy_service.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""Economy data: FRED search, regional data, Fed holdings, FOMC documents."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from openbb import obb
|
||||
|
||||
from obb_utils import to_list
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def get_cpi(country: str = "united_states") -> list[dict[str, Any]]:
|
||||
"""Get Consumer Price Index data."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.economy.cpi, country=country, provider="fred"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("CPI failed for %s", country, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
_VALID_GDP_TYPES = {"nominal", "real", "forecast"}
|
||||
|
||||
|
||||
async def get_gdp(gdp_type: str = "real") -> list[dict[str, Any]]:
|
||||
"""Get GDP data (nominal, real, or forecast)."""
|
||||
if gdp_type not in _VALID_GDP_TYPES:
|
||||
return []
|
||||
try:
|
||||
fn = getattr(obb.economy.gdp, gdp_type, None)
|
||||
if fn is None:
|
||||
return []
|
||||
result = await asyncio.to_thread(fn, provider="oecd")
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("GDP %s failed", gdp_type, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_unemployment(country: str = "united_states") -> list[dict[str, Any]]:
|
||||
"""Get unemployment rate data."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.economy.unemployment, country=country, provider="oecd"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Unemployment failed for %s", country, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_composite_leading_indicator(
|
||||
country: str = "united_states",
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get Composite Leading Indicator (recession predictor)."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.economy.composite_leading_indicator, country=country, provider="oecd"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("CLI failed for %s", country, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_house_price_index(
|
||||
country: str = "united_states",
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get housing price index."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.economy.house_price_index, country=country, provider="oecd"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("HPI failed for %s", country, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_pce() -> list[dict[str, Any]]:
|
||||
"""Get Personal Consumption Expenditures (Fed preferred inflation)."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.economy.pce, provider="fred"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("PCE failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_money_measures() -> list[dict[str, Any]]:
|
||||
"""Get M1/M2 money supply data."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.economy.money_measures, provider="federal_reserve"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Money measures failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def fred_search(query: str) -> list[dict[str, Any]]:
|
||||
"""Search FRED series by keyword."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.economy.fred_search, query, provider="fred"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("FRED search failed for %s", query, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_balance_of_payments() -> list[dict[str, Any]]:
|
||||
"""Get balance of payments (current/capital/financial account)."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.economy.balance_of_payments, provider="fred"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Balance of payments failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_central_bank_holdings() -> list[dict[str, Any]]:
|
||||
"""Get Fed SOMA portfolio holdings."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.economy.central_bank_holdings, provider="federal_reserve"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Central bank holdings failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_fred_regional(
|
||||
series_id: str, region: str | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get geographically disaggregated FRED data (by state, county, MSA)."""
|
||||
try:
|
||||
kwargs: dict[str, Any] = {"symbol": series_id, "provider": "fred"}
|
||||
if region:
|
||||
kwargs["region_type"] = region
|
||||
result = await asyncio.to_thread(
|
||||
obb.economy.fred_regional, **kwargs
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("FRED regional failed for %s", series_id, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_primary_dealer_positioning() -> list[dict[str, Any]]:
|
||||
"""Get primary dealer net positions in treasuries, MBS, corporate bonds."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.economy.primary_dealer_positioning, provider="federal_reserve"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Primary dealer positioning failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_fomc_documents(year: int | None = None) -> list[dict[str, Any]]:
|
||||
"""Get FOMC meeting documents (minutes, projections, etc.)."""
|
||||
try:
|
||||
kwargs: dict[str, Any] = {"provider": "federal_reserve"}
|
||||
if year is not None:
|
||||
kwargs["year"] = year
|
||||
result = await asyncio.to_thread(
|
||||
obb.economy.fomc_documents, **kwargs
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("FOMC documents failed", exc_info=True)
|
||||
return []
|
||||
29
findings.md
Normal file
29
findings.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Research Findings
|
||||
|
||||
## Architecture Analysis (2026-03-19)
|
||||
|
||||
### Current Codebase
|
||||
- 22 Python files, flat layout, all under 250 lines
|
||||
- Pattern: service file (async OpenBB wrapper) + route file (FastAPI router with @safe decorator)
|
||||
- Shared utils: obb_utils.py, route_utils.py, mappers.py, models.py
|
||||
|
||||
### Technical Debt
|
||||
1. Duplicated `_to_dicts` in openbb_service.py and macro_service.py (same as obb_utils.to_list)
|
||||
2. calendar_service.py has scope creep (ownership, screening mixed with calendar events)
|
||||
3. No shared `fetch_historical` helper (duplicated in technical_service.py and quantitative_service.py)
|
||||
|
||||
### Provider Availability (Verified)
|
||||
- **No API key needed:** yfinance, stockgrid, finra, multpl, cftc, government_us, sec, ecb, cboe
|
||||
- **Already configured:** fred, finnhub, alphavantage
|
||||
- **Not needed:** fmp (removed), intrinio, tiingo, benzinga
|
||||
|
||||
### Key Design Decisions
|
||||
- Keep flat file layout (avoid breaking all imports for ~40 files)
|
||||
- Domain-prefixed naming for new files
|
||||
- Generic technical indicator dispatcher pattern for 14 new indicators
|
||||
- Consolidate _to_dicts before adding new services
|
||||
|
||||
### OpenBB Features Discovered
|
||||
- 67 new endpoints across 10 groups (A-J)
|
||||
- 3 Small, 4 Medium, 3 Large complexity groups
|
||||
- All use free providers (no new API keys required)
|
||||
143
fixed_income_service.py
Normal file
143
fixed_income_service.py
Normal file
@@ -0,0 +1,143 @@
|
||||
"""Fixed income data: treasury rates, yield curve, auctions, corporate bonds."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from openbb import obb
|
||||
|
||||
from obb_utils import to_list
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def get_treasury_rates() -> list[dict[str, Any]]:
|
||||
"""Get full treasury yield curve rates (4W-30Y)."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.fixedincome.government.treasury_rates, provider="federal_reserve"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Treasury rates failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_yield_curve(date: str | None = None) -> list[dict[str, Any]]:
|
||||
"""Get yield curve with maturity/rate pairs."""
|
||||
try:
|
||||
kwargs: dict[str, Any] = {"provider": "federal_reserve"}
|
||||
if date:
|
||||
kwargs["date"] = date
|
||||
result = await asyncio.to_thread(
|
||||
obb.fixedincome.government.yield_curve, **kwargs
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Yield curve failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_treasury_auctions(security_type: str | None = None) -> list[dict[str, Any]]:
|
||||
"""Get treasury auction data (bid-to-cover, yields)."""
|
||||
try:
|
||||
kwargs: dict[str, Any] = {"provider": "government_us"}
|
||||
if security_type:
|
||||
kwargs["security_type"] = security_type
|
||||
result = await asyncio.to_thread(
|
||||
obb.fixedincome.government.treasury_auctions, **kwargs
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Treasury auctions failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_tips_yields() -> list[dict[str, Any]]:
|
||||
"""Get TIPS real yields by maturity."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.fixedincome.government.tips_yields, provider="fred"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("TIPS yields failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_effr() -> list[dict[str, Any]]:
|
||||
"""Get Effective Federal Funds Rate with percentiles."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.fixedincome.rate.effr, provider="federal_reserve"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("EFFR failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_sofr() -> list[dict[str, Any]]:
|
||||
"""Get SOFR rate with moving averages."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.fixedincome.rate.sofr, provider="federal_reserve"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("SOFR failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_hqm() -> list[dict[str, Any]]:
|
||||
"""Get High Quality Market corporate bond yields."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.fixedincome.corporate.hqm, provider="fred"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("HQM failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_commercial_paper() -> list[dict[str, Any]]:
|
||||
"""Get commercial paper rates by maturity and type."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.fixedincome.corporate.commercial_paper, provider="fred"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Commercial paper failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_spot_rates() -> list[dict[str, Any]]:
|
||||
"""Get corporate bond spot rates."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.fixedincome.corporate.spot_rates, provider="fred"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Spot rates failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
_VALID_SPREAD_SERIES = {"tcm", "tcm_effr", "treasury_effr"}
|
||||
|
||||
|
||||
async def get_spreads(series: str = "tcm") -> list[dict[str, Any]]:
|
||||
"""Get treasury/corporate spreads (tcm, tcm_effr, treasury_effr)."""
|
||||
if series not in _VALID_SPREAD_SERIES:
|
||||
return []
|
||||
try:
|
||||
fn = getattr(obb.fixedincome.spreads, series, None)
|
||||
if fn is None:
|
||||
return []
|
||||
result = await asyncio.to_thread(fn, provider="fred")
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Spreads %s failed", series, exc_info=True)
|
||||
return []
|
||||
@@ -6,7 +6,7 @@ metadata:
|
||||
rules:
|
||||
- apiGroups: ["apps"]
|
||||
resources: ["deployments"]
|
||||
verbs: ["get", "patch"]
|
||||
verbs: ["get", "list", "patch", "update"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
@@ -17,6 +17,12 @@ subjects:
|
||||
- kind: ServiceAccount
|
||||
name: default
|
||||
namespace: drone
|
||||
- kind: ServiceAccount
|
||||
name: drone
|
||||
namespace: drone
|
||||
- kind: ServiceAccount
|
||||
name: drone-runner-drone-runner-kube
|
||||
namespace: drone
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: drone-deploy
|
||||
|
||||
@@ -9,5 +9,4 @@ resources:
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
- ingress.yaml
|
||||
- drone-rbac.yaml
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ from typing import Any
|
||||
|
||||
from openbb import obb
|
||||
|
||||
from obb_utils import to_list
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PROVIDER = "fred"
|
||||
@@ -23,32 +25,31 @@ SERIES = {
|
||||
}
|
||||
|
||||
|
||||
def _to_dicts(result: Any) -> list[dict[str, Any]]:
|
||||
if result is None or result.results is None:
|
||||
return []
|
||||
if isinstance(result.results, list):
|
||||
return [
|
||||
item.model_dump() if hasattr(item, "model_dump") else vars(item)
|
||||
for item in result.results
|
||||
]
|
||||
if hasattr(result.results, "model_dump"):
|
||||
return [result.results.model_dump()]
|
||||
return [vars(result.results)]
|
||||
|
||||
|
||||
async def get_series(series_id: str, limit: int = 10) -> list[dict[str, Any]]:
|
||||
async def get_series(
|
||||
series_id: str, limit: int = 10, latest: bool = False,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get a FRED time series by ID."""
|
||||
try:
|
||||
fetch_limit = limit if not latest else None
|
||||
kwargs: dict[str, Any] = {
|
||||
"symbol": series_id,
|
||||
"provider": PROVIDER,
|
||||
}
|
||||
if fetch_limit is not None:
|
||||
kwargs["limit"] = fetch_limit
|
||||
result = await asyncio.to_thread(
|
||||
obb.economy.fred_series,
|
||||
symbol=series_id,
|
||||
limit=limit,
|
||||
provider=PROVIDER,
|
||||
**kwargs,
|
||||
)
|
||||
items = _to_dicts(result)
|
||||
for item in items:
|
||||
if "date" in item and not isinstance(item["date"], str):
|
||||
item = {**item, "date": str(item["date"])}
|
||||
items = to_list(result)
|
||||
items = [
|
||||
{**item, "date": str(item["date"])}
|
||||
if "date" in item and not isinstance(item["date"], str)
|
||||
else item
|
||||
for item in items
|
||||
]
|
||||
if latest:
|
||||
items = items[-limit:]
|
||||
return items
|
||||
except Exception:
|
||||
logger.warning("Failed to fetch FRED series %s", series_id, exc_info=True)
|
||||
@@ -58,20 +59,22 @@ async def get_series(series_id: str, limit: int = 10) -> list[dict[str, Any]]:
|
||||
async def get_macro_overview() -> dict[str, Any]:
|
||||
"""Get a summary of key macro indicators."""
|
||||
tasks = {
|
||||
name: get_series(series_id, limit=1)
|
||||
name: get_series(series_id, limit=1, latest=True)
|
||||
for name, series_id in SERIES.items()
|
||||
}
|
||||
results = await asyncio.gather(*tasks.values(), return_exceptions=True)
|
||||
|
||||
overview: dict[str, Any] = {}
|
||||
for name, result in zip(tasks.keys(), results):
|
||||
for (name, series_id), result in zip(SERIES.items(), results):
|
||||
if isinstance(result, BaseException):
|
||||
logger.warning("Failed to fetch %s: %s", name, result)
|
||||
overview[name] = None
|
||||
elif result and len(result) > 0:
|
||||
entry = result[0]
|
||||
entry = result[-1]
|
||||
# FRED returns values keyed by series ID, not "value"
|
||||
value = entry.get(series_id) or entry.get("value")
|
||||
overview[name] = {
|
||||
"value": entry.get("value"),
|
||||
"value": value,
|
||||
"date": str(entry.get("date", "")),
|
||||
}
|
||||
else:
|
||||
|
||||
58
main.py
58
main.py
@@ -1,27 +1,64 @@
|
||||
import logging
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from config import settings
|
||||
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
|
||||
# Patch curl_cffi to use safari TLS fingerprint instead of chrome.
|
||||
# curl_cffi's chrome impersonation triggers BoringSSL SSL_ERROR_SYSCALL on
|
||||
# some networks; safari works reliably. This must happen before any import
|
||||
# that creates a curl_cffi Session (yfinance, openbb).
|
||||
import curl_cffi.requests as _cffi_requests
|
||||
|
||||
_orig_session_init = _cffi_requests.Session.__init__
|
||||
|
||||
|
||||
def _patched_session_init(self, *args, **kwargs):
|
||||
if kwargs.get("impersonate") == "chrome":
|
||||
kwargs["impersonate"] = "safari"
|
||||
_orig_session_init(self, *args, **kwargs)
|
||||
|
||||
|
||||
_cffi_requests.Session.__init__ = _patched_session_init
|
||||
|
||||
from openbb import obb # noqa: E402 - must be after curl_cffi patch
|
||||
|
||||
from config import settings # noqa: E402
|
||||
from routes import router # noqa: E402
|
||||
from routes_calendar import router as calendar_router # noqa: E402
|
||||
from routes_economy import router as economy_router # noqa: E402
|
||||
from routes_fixed_income import router as fixed_income_router # noqa: E402
|
||||
from routes_macro import router as macro_router # noqa: E402
|
||||
from routes_market import router as market_router # noqa: E402
|
||||
from routes_quantitative import router as quantitative_router # noqa: E402
|
||||
from routes_regulators import router as regulators_router # noqa: E402
|
||||
from routes_sentiment import router as sentiment_router # noqa: E402
|
||||
from routes_shorts import router as shorts_router # noqa: E402
|
||||
from routes_surveys import router as surveys_router # noqa: E402
|
||||
from routes_technical import router as technical_router # noqa: E402
|
||||
|
||||
logging.basicConfig(
|
||||
level=settings.log_level.upper(),
|
||||
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""Register provider credentials once at startup."""
|
||||
if settings.fred_api_key:
|
||||
obb.user.credentials.fred_api_key = settings.fred_api_key
|
||||
logger.info("FRED API key registered")
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="OpenBB Investment Analysis API",
|
||||
version="0.1.0",
|
||||
description="REST API for stock data and rule-based investment analysis, powered by OpenBB SDK.",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
@@ -39,6 +76,11 @@ app.include_router(technical_router)
|
||||
app.include_router(quantitative_router)
|
||||
app.include_router(calendar_router)
|
||||
app.include_router(market_router)
|
||||
app.include_router(shorts_router)
|
||||
app.include_router(fixed_income_router)
|
||||
app.include_router(economy_router)
|
||||
app.include_router(surveys_router)
|
||||
app.include_router(regulators_router)
|
||||
|
||||
|
||||
@app.get("/health", response_model=dict[str, str])
|
||||
|
||||
@@ -161,3 +161,57 @@ async def get_futures_curve(symbol: str) -> list[dict[str, Any]]:
|
||||
except Exception:
|
||||
logger.warning("Futures curve failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
# --- Currency Reference Rates (Group H) ---
|
||||
|
||||
|
||||
async def get_currency_reference_rates() -> list[dict[str, Any]]:
|
||||
"""Get ECB reference exchange rates for major currencies."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.currency.reference_rates, provider="ecb"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Currency reference rates failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
# --- Index Enhanced (Group F) ---
|
||||
|
||||
|
||||
async def get_sp500_multiples(series_name: str = "pe_ratio") -> list[dict[str, Any]]:
|
||||
"""Get historical S&P 500 valuation multiples (PE, Shiller PE, P/B, etc.)."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.index.sp500_multiples, series_name=series_name, provider="multpl"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("SP500 multiples failed for %s", series_name, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_index_constituents(symbol: str) -> list[dict[str, Any]]:
|
||||
"""Get index member stocks with sector and price data."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.index.constituents, symbol, provider="cboe"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Index constituents failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_etf_nport(symbol: str) -> list[dict[str, Any]]:
|
||||
"""Get detailed ETF holdings from SEC N-PORT filings."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.etf.nport_disclosure, symbol, provider="sec"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("ETF N-PORT failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
|
||||
32
obb_utils.py
32
obb_utils.py
@@ -1,7 +1,16 @@
|
||||
"""Shared OpenBB result conversion utilities."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
|
||||
from openbb import obb
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PROVIDER = "yfinance"
|
||||
|
||||
|
||||
def to_list(result: Any) -> list[dict[str, Any]]:
|
||||
"""Convert OBBject result to list of dicts with serialized dates."""
|
||||
@@ -49,3 +58,26 @@ def safe_last(result: Any) -> dict[str, Any] | None:
|
||||
return None
|
||||
last = items[-1]
|
||||
return last.model_dump() if hasattr(last, "model_dump") else None
|
||||
|
||||
|
||||
def first_or_empty(result: Any) -> dict[str, Any]:
|
||||
"""Get first result as dict, or empty dict."""
|
||||
items = to_list(result)
|
||||
return items[0] if items else {}
|
||||
|
||||
|
||||
async def fetch_historical(
|
||||
symbol: str, days: int = 365, provider: str = PROVIDER,
|
||||
) -> Any | None:
|
||||
"""Fetch historical price data, returning the OBBject result or None."""
|
||||
start = (datetime.now(tz=timezone.utc) - timedelta(days=days)).strftime("%Y-%m-%d")
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.price.historical, symbol, start_date=start, provider=provider,
|
||||
)
|
||||
except Exception:
|
||||
logger.warning("Historical fetch failed for %s", symbol, exc_info=True)
|
||||
return None
|
||||
if result is None or result.results is None:
|
||||
return None
|
||||
return result
|
||||
|
||||
@@ -1,51 +1,34 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
|
||||
import yfinance as yf
|
||||
from openbb import obb
|
||||
|
||||
from obb_utils import to_list, first_or_empty
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PROVIDER = "yfinance"
|
||||
|
||||
|
||||
def _to_dicts(result: Any) -> list[dict[str, Any]]:
|
||||
"""Convert OBBject results to list of dicts."""
|
||||
if result is None or result.results is None:
|
||||
return []
|
||||
if isinstance(result.results, list):
|
||||
return [
|
||||
item.model_dump() if hasattr(item, "model_dump") else vars(item)
|
||||
for item in result.results
|
||||
]
|
||||
if hasattr(result.results, "model_dump"):
|
||||
return [result.results.model_dump()]
|
||||
return [vars(result.results)]
|
||||
|
||||
|
||||
def _first_or_empty(result: Any) -> dict[str, Any]:
|
||||
"""Get first result as dict, or empty dict."""
|
||||
items = _to_dicts(result)
|
||||
return items[0] if items else {}
|
||||
|
||||
|
||||
async def get_quote(symbol: str) -> dict:
|
||||
async def get_quote(symbol: str) -> dict[str, Any]:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.price.quote, symbol, provider=PROVIDER
|
||||
)
|
||||
return _first_or_empty(result)
|
||||
return first_or_empty(result)
|
||||
|
||||
|
||||
async def get_historical(symbol: str, days: int = 365) -> list[dict]:
|
||||
start = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
|
||||
async def get_historical(symbol: str, days: int = 365) -> list[dict[str, Any]]:
|
||||
start = (datetime.now(tz=timezone.utc) - timedelta(days=days)).strftime("%Y-%m-%d")
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.price.historical,
|
||||
symbol,
|
||||
start_date=start,
|
||||
provider=PROVIDER,
|
||||
)
|
||||
items = _to_dicts(result)
|
||||
items = to_list(result)
|
||||
return [
|
||||
{**item, "date": str(item["date"])}
|
||||
if "date" in item and not isinstance(item["date"], str)
|
||||
@@ -54,42 +37,42 @@ async def get_historical(symbol: str, days: int = 365) -> list[dict]:
|
||||
]
|
||||
|
||||
|
||||
async def get_profile(symbol: str) -> dict:
|
||||
async def get_profile(symbol: str) -> dict[str, Any]:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.profile, symbol, provider=PROVIDER
|
||||
)
|
||||
return _first_or_empty(result)
|
||||
return first_or_empty(result)
|
||||
|
||||
|
||||
async def get_metrics(symbol: str) -> dict:
|
||||
async def get_metrics(symbol: str) -> dict[str, Any]:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.fundamental.metrics, symbol, provider=PROVIDER
|
||||
)
|
||||
return _first_or_empty(result)
|
||||
return first_or_empty(result)
|
||||
|
||||
|
||||
async def get_income(symbol: str) -> list[dict]:
|
||||
async def get_income(symbol: str) -> list[dict[str, Any]]:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.fundamental.income, symbol, provider=PROVIDER
|
||||
)
|
||||
return _to_dicts(result)
|
||||
return to_list(result)
|
||||
|
||||
|
||||
async def get_balance(symbol: str) -> list[dict]:
|
||||
async def get_balance(symbol: str) -> list[dict[str, Any]]:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.fundamental.balance, symbol, provider=PROVIDER
|
||||
)
|
||||
return _to_dicts(result)
|
||||
return to_list(result)
|
||||
|
||||
|
||||
async def get_cash_flow(symbol: str) -> list[dict]:
|
||||
async def get_cash_flow(symbol: str) -> list[dict[str, Any]]:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.fundamental.cash, symbol, provider=PROVIDER
|
||||
)
|
||||
return _to_dicts(result)
|
||||
return to_list(result)
|
||||
|
||||
|
||||
async def get_financials(symbol: str) -> dict:
|
||||
async def get_financials(symbol: str) -> dict[str, Any]:
|
||||
income, balance, cash_flow = await asyncio.gather(
|
||||
get_income(symbol),
|
||||
get_balance(symbol),
|
||||
@@ -104,26 +87,26 @@ async def get_financials(symbol: str) -> dict:
|
||||
|
||||
|
||||
async def get_price_target(symbol: str) -> float | None:
|
||||
"""Get consensus analyst price target via yfinance."""
|
||||
def _fetch() -> float | None:
|
||||
t = yf.Ticker(symbol)
|
||||
return t.info.get("targetMeanPrice")
|
||||
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.estimates.price_target, symbol, provider="fmp"
|
||||
)
|
||||
items = _to_dicts(result)
|
||||
if items:
|
||||
return items[0].get("adj_price_target") or items[0].get("price_target")
|
||||
return await asyncio.to_thread(_fetch)
|
||||
except Exception:
|
||||
logger.warning("Failed to get price target for %s", symbol, exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
async def get_news(symbol: str) -> list[dict]:
|
||||
async def get_news(symbol: str) -> list[dict[str, Any]]:
|
||||
result = await asyncio.to_thread(
|
||||
obb.news.company, symbol, provider=PROVIDER
|
||||
)
|
||||
return _to_dicts(result)
|
||||
return to_list(result)
|
||||
|
||||
|
||||
async def get_summary(symbol: str) -> dict:
|
||||
async def get_summary(symbol: str) -> dict[str, Any]:
|
||||
quote, profile, metrics, financials = await asyncio.gather(
|
||||
get_quote(symbol),
|
||||
get_profile(symbol),
|
||||
@@ -138,36 +121,119 @@ async def get_summary(symbol: str) -> dict:
|
||||
}
|
||||
|
||||
|
||||
async def get_gainers() -> list[dict]:
|
||||
async def get_gainers() -> list[dict[str, Any]]:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.discovery.gainers, provider=PROVIDER
|
||||
)
|
||||
return _to_dicts(result)
|
||||
return to_list(result)
|
||||
|
||||
|
||||
async def get_losers() -> list[dict]:
|
||||
async def get_losers() -> list[dict[str, Any]]:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.discovery.losers, provider=PROVIDER
|
||||
)
|
||||
return _to_dicts(result)
|
||||
return to_list(result)
|
||||
|
||||
|
||||
async def get_active() -> list[dict]:
|
||||
async def get_active() -> list[dict[str, Any]]:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.discovery.active, provider=PROVIDER
|
||||
)
|
||||
return _to_dicts(result)
|
||||
return to_list(result)
|
||||
|
||||
|
||||
async def get_undervalued() -> list[dict]:
|
||||
async def get_undervalued() -> list[dict[str, Any]]:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.discovery.undervalued_large_caps, provider=PROVIDER
|
||||
)
|
||||
return _to_dicts(result)
|
||||
return to_list(result)
|
||||
|
||||
|
||||
async def get_growth() -> list[dict]:
|
||||
async def get_growth() -> list[dict[str, Any]]:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.discovery.growth_tech, provider=PROVIDER
|
||||
)
|
||||
return _to_dicts(result)
|
||||
return to_list(result)
|
||||
|
||||
|
||||
async def get_upgrades_downgrades(
|
||||
symbol: str, limit: int = 20,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get analyst upgrades/downgrades via yfinance."""
|
||||
def _fetch() -> list[dict[str, Any]]:
|
||||
t = yf.Ticker(symbol)
|
||||
df = t.upgrades_downgrades
|
||||
if df is None or df.empty:
|
||||
return []
|
||||
df = df.head(limit).reset_index()
|
||||
return [
|
||||
{
|
||||
"date": str(row.get("GradeDate", "")),
|
||||
"company": row.get("Firm"),
|
||||
"action": row.get("Action"),
|
||||
"from_grade": row.get("FromGrade"),
|
||||
"to_grade": row.get("ToGrade"),
|
||||
"price_target_action": row.get("priceTargetAction"),
|
||||
"current_price_target": row.get("currentPriceTarget"),
|
||||
"prior_price_target": row.get("priorPriceTarget"),
|
||||
}
|
||||
for _, row in df.iterrows()
|
||||
]
|
||||
|
||||
return await asyncio.to_thread(_fetch)
|
||||
|
||||
|
||||
# --- Equity Fundamentals Extended (Group B) ---
|
||||
|
||||
|
||||
async def get_management(symbol: str) -> list[dict[str, Any]]:
|
||||
"""Get executive team info (name, title, compensation)."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.fundamental.management, symbol, provider=PROVIDER
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Management failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_dividends(symbol: str) -> list[dict[str, Any]]:
|
||||
"""Get historical dividend records."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.fundamental.dividends, symbol, provider=PROVIDER
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Dividends failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_filings(
|
||||
symbol: str, form_type: str | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get SEC filings (10-K, 10-Q, 8-K, etc.)."""
|
||||
try:
|
||||
kwargs: dict[str, Any] = {"symbol": symbol, "provider": "sec"}
|
||||
if form_type is not None:
|
||||
kwargs["type"] = form_type
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.fundamental.filings, **kwargs
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Filings failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def search_company(query: str) -> list[dict[str, Any]]:
|
||||
"""Search for companies by name."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.search, query, provider="sec"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Company search failed for %s", query, exc_info=True)
|
||||
return []
|
||||
|
||||
38
progress.md
Normal file
38
progress.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Progress Log
|
||||
|
||||
## Session 2026-03-19
|
||||
|
||||
### Completed
|
||||
- [x] Fixed Dockerfile SSL issue (libssl3 runtime dep)
|
||||
- [x] Fixed curl_cffi TLS error (pin 0.7.4, safari fingerprint patch)
|
||||
- [x] Registered FRED API key with OpenBB credentials
|
||||
- [x] Fixed macro_service to return latest data (not oldest)
|
||||
- [x] Switched upgrades endpoint from Finnhub to yfinance
|
||||
- [x] Switched price_target from FMP to yfinance
|
||||
- [x] Tested all 32 endpoints locally and on deployed environment
|
||||
- [x] Updated README
|
||||
- [x] Researched OpenBB features for expansion (67 new endpoints identified)
|
||||
- [x] Architecture analysis complete
|
||||
- [x] Implementation plan created (task_plan.md)
|
||||
|
||||
### Implementation Progress
|
||||
- [x] P0: Consolidated `_to_dicts` -> `obb_utils.to_list` in openbb_service.py and macro_service.py
|
||||
- [x] P0: Added `fetch_historical` and `first_or_empty` to obb_utils.py
|
||||
- [x] P0: Updated technical_service.py and quantitative_service.py to use shared helpers
|
||||
- [x] Phase 1 Group I: 12 new technical indicators (ATR, ADX, Stoch, OBV, Ichimoku, Donchian, Aroon, CCI, KC, Fib, A/D, Cones)
|
||||
- [x] Phase 1 Group J: Sortino, Omega, rolling stats (6 stats via generic endpoint)
|
||||
- [x] Phase 1 Group H: Currency reference rates (ECB)
|
||||
- [x] Phase 2 Group C: Fixed income (10 endpoints) - new service + routes
|
||||
- [x] Phase 2 Group D: Economy expanded (11 endpoints) - new service + routes
|
||||
- [x] Phase 2 Group E: Surveys (5 endpoints) - new service + routes
|
||||
- [x] Phase 3 Group B: Equity fundamentals (4 endpoints) - management, dividends, filings, search
|
||||
- [x] Phase 3 Group A: Shorts & dark pool (4 endpoints) - new service + routes
|
||||
- [x] Phase 3 Group F: Index/ETF enhanced (3 endpoints) - sp500 multiples, constituents, nport
|
||||
- [x] Phase 4 Group G: Regulators (5 endpoints) - COT, SEC litigation, institutions
|
||||
- [x] All 5 new routers registered in main.py
|
||||
- [x] App imports verified: 108 routes total
|
||||
|
||||
### Current State
|
||||
- 108 total routes (including OpenAPI/docs)
|
||||
- Code reviewer and security reviewer running in background
|
||||
- Pending: review feedback, testing, commit
|
||||
@@ -9,6 +9,7 @@ dependencies = [
|
||||
"openbb[yfinance]",
|
||||
"pydantic-settings",
|
||||
"httpx",
|
||||
"curl_cffi==0.7.4",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Any
|
||||
|
||||
from openbb import obb
|
||||
|
||||
from obb_utils import extract_single, safe_last
|
||||
from obb_utils import extract_single, safe_last, fetch_historical, to_list
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -122,3 +122,74 @@ async def get_unitroot_test(symbol: str, days: int = 365) -> dict[str, Any]:
|
||||
except Exception:
|
||||
logger.warning("Unit root test failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute unit root test"}
|
||||
|
||||
|
||||
# --- Extended Quantitative (Phase 1, Group J) ---
|
||||
|
||||
|
||||
async def get_sortino(symbol: str, days: int = 365) -> dict[str, Any]:
|
||||
"""Sortino ratio -- risk-adjusted return penalizing only downside deviation."""
|
||||
fetch_days = max(days, PERF_DAYS)
|
||||
hist = await fetch_historical(symbol, fetch_days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.quantitative.performance.sortino_ratio,
|
||||
data=hist.results, target=TARGET,
|
||||
)
|
||||
return {"symbol": symbol, "period_days": days, "sortino": safe_last(result)}
|
||||
except Exception:
|
||||
logger.warning("Sortino failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute Sortino ratio"}
|
||||
|
||||
|
||||
async def get_omega(symbol: str, days: int = 365) -> dict[str, Any]:
|
||||
"""Omega ratio -- probability-weighted gain vs loss ratio."""
|
||||
fetch_days = max(days, PERF_DAYS)
|
||||
hist = await fetch_historical(symbol, fetch_days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.quantitative.performance.omega_ratio,
|
||||
data=hist.results, target=TARGET,
|
||||
)
|
||||
return {"symbol": symbol, "period_days": days, "omega": safe_last(result)}
|
||||
except Exception:
|
||||
logger.warning("Omega failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute Omega ratio"}
|
||||
|
||||
|
||||
async def get_rolling_stat(
|
||||
symbol: str, stat: str, days: int = 365, window: int = 30,
|
||||
) -> dict[str, Any]:
|
||||
"""Compute a rolling statistic (variance, stdev, mean, skew, kurtosis, quantile)."""
|
||||
valid_stats = {"variance", "stdev", "mean", "skew", "kurtosis", "quantile"}
|
||||
if stat not in valid_stats:
|
||||
return {"symbol": symbol, "error": f"Invalid stat. Valid options: {', '.join(sorted(valid_stats))}"}
|
||||
|
||||
fetch_days = max(days, PERF_DAYS)
|
||||
hist = await fetch_historical(symbol, fetch_days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
fn = getattr(obb.quantitative.rolling, stat, None)
|
||||
if fn is None or not callable(fn):
|
||||
return {"symbol": symbol, "error": f"Stat '{stat}' not available"}
|
||||
result = await asyncio.to_thread(
|
||||
fn, data=hist.results, target=TARGET, window=window,
|
||||
)
|
||||
items = to_list(result)
|
||||
# Return last N items matching the requested window
|
||||
tail = items[-window:] if len(items) > window else items
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"stat": stat,
|
||||
"window": window,
|
||||
"period_days": days,
|
||||
"data": tail,
|
||||
}
|
||||
except Exception:
|
||||
logger.warning("Rolling %s failed for %s", stat, symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": f"Failed to compute rolling {stat}"}
|
||||
|
||||
71
regulators_service.py
Normal file
71
regulators_service.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""Regulatory data: CFTC COT reports, SEC litigation, institutional data."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from openbb import obb
|
||||
|
||||
from obb_utils import to_list
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def get_cot(symbol: str) -> list[dict[str, Any]]:
|
||||
"""Get Commitment of Traders report for a futures symbol."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.regulators.cftc.cot, symbol, provider="cftc"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("COT failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def cot_search(query: str) -> list[dict[str, Any]]:
|
||||
"""Search COT report symbols."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.regulators.cftc.cot_search, query, provider="cftc"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("COT search failed for %s", query, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_sec_litigation() -> list[dict[str, Any]]:
|
||||
"""Get SEC litigation releases."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.regulators.sec.rss_litigation, provider="sec"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("SEC litigation failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def search_institutions(query: str) -> list[dict[str, Any]]:
|
||||
"""Search for institutional investors filing with SEC."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.regulators.sec.institutions_search, query, provider="sec"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Institution search failed for %s", query, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_cik_map(symbol: str) -> list[dict[str, Any]]:
|
||||
"""Map ticker symbol to CIK number."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.regulators.sec.cik_map, symbol, provider="sec"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("CIK map failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
41
routes.py
41
routes.py
@@ -173,3 +173,44 @@ async def discover_growth():
|
||||
"""Get growth tech stocks."""
|
||||
data = await openbb_service.get_growth()
|
||||
return ApiResponse(data=discover_items_from_list(data))
|
||||
|
||||
|
||||
# --- Equity Fundamentals Extended (Group B) ---
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/management", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_management(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Get executive team: name, title, compensation."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await openbb_service.get_management(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/dividends", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_dividends(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Get historical dividend records."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await openbb_service.get_dividends(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/filings", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_filings(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
form_type: str = Query(default=None, max_length=20, pattern=r"^[A-Za-z0-9/-]+$"),
|
||||
):
|
||||
"""Get SEC filings (10-K, 10-Q, 8-K, etc.)."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await openbb_service.get_filings(symbol, form_type=form_type)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/search", response_model=ApiResponse)
|
||||
@safe
|
||||
async def company_search(query: str = Query(..., min_length=1, max_length=100)):
|
||||
"""Search for companies by name (SEC/NASDAQ)."""
|
||||
data = await openbb_service.search_company(query)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
126
routes_economy.py
Normal file
126
routes_economy.py
Normal file
@@ -0,0 +1,126 @@
|
||||
"""Routes for expanded economy data."""
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
from models import ApiResponse
|
||||
from route_utils import safe
|
||||
import economy_service
|
||||
|
||||
router = APIRouter(prefix="/api/v1")
|
||||
|
||||
|
||||
# --- Structured macro indicators (Group D) ---
|
||||
|
||||
|
||||
@router.get("/macro/cpi", response_model=ApiResponse)
|
||||
@safe
|
||||
async def macro_cpi(country: str = Query(default="united_states", max_length=50, pattern=r"^[a-z_]+$")):
|
||||
"""Consumer Price Index (multi-country)."""
|
||||
data = await economy_service.get_cpi(country=country)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/macro/gdp", response_model=ApiResponse)
|
||||
@safe
|
||||
async def macro_gdp(
|
||||
gdp_type: str = Query(default="real", pattern="^(nominal|real|forecast)$"),
|
||||
):
|
||||
"""GDP: nominal, real, or forecast."""
|
||||
data = await economy_service.get_gdp(gdp_type=gdp_type)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/macro/unemployment", response_model=ApiResponse)
|
||||
@safe
|
||||
async def macro_unemployment(
|
||||
country: str = Query(default="united_states", max_length=50, pattern=r"^[a-z_]+$"),
|
||||
):
|
||||
"""Unemployment rate (multi-country, with demographic breakdowns)."""
|
||||
data = await economy_service.get_unemployment(country=country)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/macro/pce", response_model=ApiResponse)
|
||||
@safe
|
||||
async def macro_pce():
|
||||
"""Personal Consumption Expenditures (Fed preferred inflation measure)."""
|
||||
data = await economy_service.get_pce()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/macro/money-measures", response_model=ApiResponse)
|
||||
@safe
|
||||
async def macro_money_measures():
|
||||
"""M1/M2 money supply, currency in circulation."""
|
||||
data = await economy_service.get_money_measures()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/macro/cli", response_model=ApiResponse)
|
||||
@safe
|
||||
async def macro_cli(country: str = Query(default="united_states", max_length=50, pattern=r"^[a-z_]+$")):
|
||||
"""Composite Leading Indicator (predicts recessions 6-9 months ahead)."""
|
||||
data = await economy_service.get_composite_leading_indicator(country=country)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/macro/house-price-index", response_model=ApiResponse)
|
||||
@safe
|
||||
async def macro_hpi(country: str = Query(default="united_states", max_length=50, pattern=r"^[a-z_]+$")):
|
||||
"""Housing price index (multi-country)."""
|
||||
data = await economy_service.get_house_price_index(country=country)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
# --- Economy data endpoints ---
|
||||
|
||||
|
||||
@router.get("/economy/fred-regional", response_model=ApiResponse)
|
||||
@safe
|
||||
async def economy_fred_regional(
|
||||
series_id: str = Query(..., min_length=1, max_length=30),
|
||||
region: str = Query(default=None, max_length=20, pattern=r"^[a-z_]+$"),
|
||||
):
|
||||
"""Regional FRED data by state, county, or MSA."""
|
||||
data = await economy_service.get_fred_regional(series_id=series_id, region=region)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/economy/primary-dealer-positioning", response_model=ApiResponse)
|
||||
@safe
|
||||
async def economy_primary_dealer():
|
||||
"""Primary dealer net positions: treasuries, MBS, corporate bonds."""
|
||||
data = await economy_service.get_primary_dealer_positioning()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/economy/fred-search", response_model=ApiResponse)
|
||||
@safe
|
||||
async def economy_fred_search(query: str = Query(..., min_length=1, max_length=100)):
|
||||
"""Search FRED series by keyword (800K+ economic series)."""
|
||||
data = await economy_service.fred_search(query=query)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/economy/balance-of-payments", response_model=ApiResponse)
|
||||
@safe
|
||||
async def economy_bop():
|
||||
"""Balance of payments: current/capital/financial account."""
|
||||
data = await economy_service.get_balance_of_payments()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/economy/central-bank-holdings", response_model=ApiResponse)
|
||||
@safe
|
||||
async def economy_fed_holdings():
|
||||
"""Fed SOMA portfolio: holdings by security type."""
|
||||
data = await economy_service.get_central_bank_holdings()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/economy/fomc-documents", response_model=ApiResponse)
|
||||
@safe
|
||||
async def economy_fomc(year: int = Query(default=None, ge=2000, le=2099)):
|
||||
"""FOMC meeting documents: minutes, projections, press conferences."""
|
||||
data = await economy_service.get_fomc_documents(year=year)
|
||||
return ApiResponse(data=data)
|
||||
93
routes_fixed_income.py
Normal file
93
routes_fixed_income.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""Routes for fixed income data."""
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
from models import ApiResponse
|
||||
from route_utils import safe
|
||||
import fixed_income_service
|
||||
|
||||
router = APIRouter(prefix="/api/v1/fixed-income")
|
||||
|
||||
|
||||
@router.get("/treasury-rates", response_model=ApiResponse)
|
||||
@safe
|
||||
async def treasury_rates():
|
||||
"""Full treasury yield curve rates (4W-30Y)."""
|
||||
data = await fixed_income_service.get_treasury_rates()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/yield-curve", response_model=ApiResponse)
|
||||
@safe
|
||||
async def yield_curve(date: str = Query(default=None, max_length=10, pattern=r"^\d{4}-\d{2}-\d{2}$")):
|
||||
"""Yield curve with maturity/rate pairs."""
|
||||
data = await fixed_income_service.get_yield_curve(date=date)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/treasury-auctions", response_model=ApiResponse)
|
||||
@safe
|
||||
async def treasury_auctions(
|
||||
security_type: str = Query(default=None, max_length=30, pattern=r"^[a-zA-Z_ -]+$"),
|
||||
):
|
||||
"""Treasury auction data: bid-to-cover ratios, yields."""
|
||||
data = await fixed_income_service.get_treasury_auctions(security_type=security_type)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/tips-yields", response_model=ApiResponse)
|
||||
@safe
|
||||
async def tips_yields():
|
||||
"""TIPS real yields by maturity."""
|
||||
data = await fixed_income_service.get_tips_yields()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/effr", response_model=ApiResponse)
|
||||
@safe
|
||||
async def effr():
|
||||
"""Effective Federal Funds Rate with percentiles and volume."""
|
||||
data = await fixed_income_service.get_effr()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/sofr", response_model=ApiResponse)
|
||||
@safe
|
||||
async def sofr():
|
||||
"""SOFR rate with 30/90/180-day moving averages."""
|
||||
data = await fixed_income_service.get_sofr()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/hqm", response_model=ApiResponse)
|
||||
@safe
|
||||
async def hqm():
|
||||
"""High Quality Market corporate bond yields (AAA/AA/A)."""
|
||||
data = await fixed_income_service.get_hqm()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/commercial-paper", response_model=ApiResponse)
|
||||
@safe
|
||||
async def commercial_paper():
|
||||
"""Commercial paper rates by maturity and type."""
|
||||
data = await fixed_income_service.get_commercial_paper()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/spot-rates", response_model=ApiResponse)
|
||||
@safe
|
||||
async def spot_rates():
|
||||
"""Corporate bond spot rates and par yields."""
|
||||
data = await fixed_income_service.get_spot_rates()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/spreads", response_model=ApiResponse)
|
||||
@safe
|
||||
async def spreads(
|
||||
series: str = Query(default="tcm", pattern="^(tcm|tcm_effr|treasury_effr)$"),
|
||||
):
|
||||
"""Treasury/corporate spreads (tcm, tcm_effr, treasury_effr)."""
|
||||
data = await fixed_income_service.get_spreads(series=series)
|
||||
return ApiResponse(data=data)
|
||||
@@ -135,3 +135,45 @@ async def futures_curve(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await market_service.get_futures_curve(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
# --- Currency Reference Rates (Group H) ---
|
||||
|
||||
|
||||
@router.get("/currency/reference-rates", response_model=ApiResponse)
|
||||
@safe
|
||||
async def currency_reference_rates():
|
||||
"""Get ECB reference exchange rates for 28 major currencies."""
|
||||
data = await market_service.get_currency_reference_rates()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
# --- Index Enhanced (Group F) ---
|
||||
|
||||
|
||||
@router.get("/index/sp500-multiples", response_model=ApiResponse)
|
||||
@safe
|
||||
async def sp500_multiples(
|
||||
series: str = Query(default="pe_ratio", pattern="^[a-z_]+$"),
|
||||
):
|
||||
"""Historical S&P 500 valuation: pe_ratio, shiller_pe_ratio, dividend_yield, etc."""
|
||||
data = await market_service.get_sp500_multiples(series)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/index/{symbol}/constituents", response_model=ApiResponse)
|
||||
@safe
|
||||
async def index_constituents(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Get index member stocks with sector and price data."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await market_service.get_index_constituents(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/etf/{symbol}/nport", response_model=ApiResponse)
|
||||
@safe
|
||||
async def etf_nport(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Detailed ETF holdings from SEC N-PORT filings."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await market_service.get_etf_nport(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
@@ -52,3 +52,44 @@ async def stock_unitroot(
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await quantitative_service.get_unitroot_test(symbol, days=days)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
# --- Extended Quantitative (Group J) ---
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/sortino", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_sortino(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
days: int = Query(default=365, ge=30, le=3650),
|
||||
):
|
||||
"""Sortino ratio -- risk-adjusted return penalizing only downside deviation."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await quantitative_service.get_sortino(symbol, days=days)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/omega", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_omega(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
days: int = Query(default=365, ge=30, le=3650),
|
||||
):
|
||||
"""Omega ratio -- probability-weighted gain vs loss."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await quantitative_service.get_omega(symbol, days=days)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/rolling/{stat}", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_rolling(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
stat: str = Path(..., pattern="^(variance|stdev|mean|skew|kurtosis|quantile)$"),
|
||||
days: int = Query(default=365, ge=30, le=3650),
|
||||
window: int = Query(default=30, ge=5, le=252),
|
||||
):
|
||||
"""Rolling statistics: variance, stdev, mean, skew, kurtosis, quantile."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await quantitative_service.get_rolling_stat(symbol, stat=stat, days=days, window=window)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
51
routes_regulators.py
Normal file
51
routes_regulators.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""Routes for regulatory data (CFTC, SEC)."""
|
||||
|
||||
from fastapi import APIRouter, Path, Query
|
||||
|
||||
from models import ApiResponse
|
||||
from route_utils import safe, validate_symbol
|
||||
import regulators_service
|
||||
|
||||
router = APIRouter(prefix="/api/v1/regulators")
|
||||
|
||||
|
||||
@router.get("/cot", response_model=ApiResponse)
|
||||
@safe
|
||||
async def cot_report(symbol: str = Query(..., min_length=1, max_length=20)):
|
||||
"""Commitment of Traders: commercial/speculator positions for futures."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await regulators_service.get_cot(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/cot/search", response_model=ApiResponse)
|
||||
@safe
|
||||
async def cot_search(query: str = Query(..., min_length=1, max_length=100)):
|
||||
"""Search COT report symbols."""
|
||||
data = await regulators_service.cot_search(query)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/sec/litigation", response_model=ApiResponse)
|
||||
@safe
|
||||
async def sec_litigation():
|
||||
"""SEC litigation releases RSS feed."""
|
||||
data = await regulators_service.get_sec_litigation()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/sec/institutions", response_model=ApiResponse)
|
||||
@safe
|
||||
async def sec_institutions(query: str = Query(..., min_length=1, max_length=100)):
|
||||
"""Search institutional investors filing with SEC."""
|
||||
data = await regulators_service.search_institutions(query)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/sec/cik-map/{symbol}", response_model=ApiResponse)
|
||||
@safe
|
||||
async def sec_cik_map(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Map ticker symbol to SEC CIK number."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await regulators_service.get_cik_map(symbol)
|
||||
return ApiResponse(data=data)
|
||||
@@ -8,6 +8,7 @@ from models import ApiResponse
|
||||
from route_utils import safe, validate_symbol
|
||||
import alphavantage_service
|
||||
import finnhub_service
|
||||
import openbb_service
|
||||
|
||||
import logging
|
||||
|
||||
@@ -96,17 +97,7 @@ async def stock_recommendations(symbol: str = Path(..., min_length=1, max_length
|
||||
@router.get("/stock/{symbol}/upgrades", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_upgrades(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Get recent analyst upgrades and downgrades."""
|
||||
"""Get recent analyst upgrades and downgrades (via yfinance)."""
|
||||
symbol = validate_symbol(symbol)
|
||||
raw = await finnhub_service.get_upgrade_downgrade(symbol)
|
||||
upgrades = [
|
||||
{
|
||||
"company": u.get("company"),
|
||||
"action": u.get("action"),
|
||||
"from_grade": u.get("fromGrade"),
|
||||
"to_grade": u.get("toGrade"),
|
||||
"date": u.get("gradeTime"),
|
||||
}
|
||||
for u in raw[:20]
|
||||
]
|
||||
return ApiResponse(data=upgrades)
|
||||
data = await openbb_service.get_upgrades_downgrades(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
45
routes_shorts.py
Normal file
45
routes_shorts.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""Routes for equity shorts and dark pool data."""
|
||||
|
||||
from fastapi import APIRouter, Path
|
||||
|
||||
from models import ApiResponse
|
||||
from route_utils import safe, validate_symbol
|
||||
import shorts_service
|
||||
|
||||
router = APIRouter(prefix="/api/v1")
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/shorts/volume", response_model=ApiResponse)
|
||||
@safe
|
||||
async def short_volume(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Daily short volume and percent (stockgrid)."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await shorts_service.get_short_volume(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/shorts/ftd", response_model=ApiResponse)
|
||||
@safe
|
||||
async def fails_to_deliver(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Fails-to-deliver records from SEC."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await shorts_service.get_fails_to_deliver(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/shorts/interest", response_model=ApiResponse)
|
||||
@safe
|
||||
async def short_interest(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Short interest positions, days to cover (FINRA)."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await shorts_service.get_short_interest(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/darkpool/{symbol}/otc", response_model=ApiResponse)
|
||||
@safe
|
||||
async def darkpool_otc(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""OTC/dark pool aggregate trade volume (FINRA)."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await shorts_service.get_darkpool_otc(symbol)
|
||||
return ApiResponse(data=data)
|
||||
49
routes_surveys.py
Normal file
49
routes_surveys.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""Routes for economy surveys."""
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
from models import ApiResponse
|
||||
from route_utils import safe
|
||||
import surveys_service
|
||||
|
||||
router = APIRouter(prefix="/api/v1/economy/surveys")
|
||||
|
||||
|
||||
@router.get("/michigan", response_model=ApiResponse)
|
||||
@safe
|
||||
async def survey_michigan():
|
||||
"""University of Michigan Consumer Sentiment + inflation expectations."""
|
||||
data = await surveys_service.get_michigan()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/sloos", response_model=ApiResponse)
|
||||
@safe
|
||||
async def survey_sloos():
|
||||
"""Senior Loan Officer Opinion Survey (lending standards, recession signal)."""
|
||||
data = await surveys_service.get_sloos()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/nonfarm-payrolls", response_model=ApiResponse)
|
||||
@safe
|
||||
async def survey_nfp():
|
||||
"""Detailed employment data: employees, hours, earnings by industry."""
|
||||
data = await surveys_service.get_nonfarm_payrolls()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/empire-state", response_model=ApiResponse)
|
||||
@safe
|
||||
async def survey_empire():
|
||||
"""Empire State Manufacturing Survey (NY manufacturing outlook)."""
|
||||
data = await surveys_service.get_empire_state()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/bls-search", response_model=ApiResponse)
|
||||
@safe
|
||||
async def survey_bls_search(query: str = Query(..., min_length=1, max_length=100)):
|
||||
"""Search BLS data series (CPI components, wages, employment, etc.)."""
|
||||
data = await surveys_service.bls_search(query=query)
|
||||
return ApiResponse(data=data)
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Routes for technical analysis indicators."""
|
||||
|
||||
from fastapi import APIRouter, Path
|
||||
from fastapi import APIRouter, Path, Query
|
||||
|
||||
from models import ApiResponse
|
||||
from route_utils import safe, validate_symbol
|
||||
@@ -16,3 +16,171 @@ async def stock_technical(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await technical_service.get_technical_indicators(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
# --- Individual Technical Indicators (Group I) ---
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/technical/atr", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_atr(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
length: int = Query(default=14, ge=1, le=100),
|
||||
):
|
||||
"""Average True Range -- volatility for position sizing and stop-loss."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await technical_service.get_atr(symbol, length=length)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/technical/adx", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_adx(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
length: int = Query(default=14, ge=1, le=100),
|
||||
):
|
||||
"""Average Directional Index -- trend strength (>25 strong, <20 range-bound)."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await technical_service.get_adx(symbol, length=length)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/technical/stoch", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_stoch(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
fast_k: int = Query(default=14, ge=1, le=100),
|
||||
slow_d: int = Query(default=3, ge=1, le=100),
|
||||
slow_k: int = Query(default=3, ge=1, le=100),
|
||||
):
|
||||
"""Stochastic Oscillator -- overbought/oversold momentum signal."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await technical_service.get_stoch(symbol, fast_k=fast_k, slow_d=slow_d, slow_k=slow_k)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/technical/obv", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_obv(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""On-Balance Volume -- cumulative volume for divergence detection."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await technical_service.get_obv(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/technical/ichimoku", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_ichimoku(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Ichimoku Cloud -- comprehensive trend system with support/resistance."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await technical_service.get_ichimoku(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/technical/donchian", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_donchian(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
length: int = Query(default=20, ge=1, le=100),
|
||||
):
|
||||
"""Donchian Channels -- breakout detection system."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await technical_service.get_donchian(symbol, length=length)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/technical/aroon", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_aroon(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
length: int = Query(default=25, ge=1, le=100),
|
||||
):
|
||||
"""Aroon Indicator -- identifies trend direction and potential changes."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await technical_service.get_aroon(symbol, length=length)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/technical/cci", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_cci(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
length: int = Query(default=14, ge=1, le=100),
|
||||
):
|
||||
"""Commodity Channel Index -- cyclical trend identification."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await technical_service.get_cci(symbol, length=length)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/technical/kc", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_kc(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
length: int = Query(default=20, ge=1, le=100),
|
||||
):
|
||||
"""Keltner Channels -- ATR-based volatility bands."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await technical_service.get_kc(symbol, length=length)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/technical/fib", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_fib(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
days: int = Query(default=120, ge=5, le=365),
|
||||
):
|
||||
"""Fibonacci Retracement -- key support/resistance levels."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await technical_service.get_fib(symbol, days=days)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/technical/ad", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_ad(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Accumulation/Distribution Line -- volume-based trend indicator."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await technical_service.get_ad(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/technical/cones", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_cones(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Volatility Cones -- realized vol quantiles for options analysis."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await technical_service.get_cones(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/technical/vwap", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_vwap(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Volume Weighted Average Price -- intraday fair value benchmark."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await technical_service.get_vwap(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/technical/relative-rotation", response_model=ApiResponse)
|
||||
@safe
|
||||
async def relative_rotation(
|
||||
symbols: str = Query(..., min_length=1, max_length=200, description="Comma-separated symbols, e.g. AAPL,MSFT,GOOGL"),
|
||||
benchmark: str = Query(default="SPY", min_length=1, max_length=20),
|
||||
study: str = Query(default="price", pattern="^(price|volume|volatility)$"),
|
||||
):
|
||||
"""Relative Rotation Graph -- compare multiple symbols vs benchmark.
|
||||
|
||||
Returns RS-Ratio and RS-Momentum for each symbol, indicating
|
||||
RRG quadrant: Leading, Weakening, Lagging, or Improving.
|
||||
"""
|
||||
symbol_list = [validate_symbol(s.strip()) for s in symbols.split(",") if s.strip()]
|
||||
if not symbol_list:
|
||||
return ApiResponse(data=[], error="No valid symbols provided")
|
||||
benchmark = validate_symbol(benchmark)
|
||||
data = await technical_service.get_relative_rotation(
|
||||
symbol_list, benchmark=benchmark, study=study,
|
||||
)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
59
shorts_service.py
Normal file
59
shorts_service.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""Equity shorts and dark pool data (stockgrid, FINRA, SEC)."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from openbb import obb
|
||||
|
||||
from obb_utils import to_list
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def get_short_volume(symbol: str) -> list[dict[str, Any]]:
|
||||
"""Get daily short volume data (stockgrid)."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.shorts.short_volume, symbol, provider="stockgrid"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Short volume failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_fails_to_deliver(symbol: str) -> list[dict[str, Any]]:
|
||||
"""Get fails-to-deliver records (SEC)."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.shorts.fails_to_deliver, symbol, provider="sec"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("FTD failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_short_interest(symbol: str) -> list[dict[str, Any]]:
|
||||
"""Get short interest positions (FINRA)."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.shorts.short_interest, symbol, provider="finra"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Short interest failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_darkpool_otc(symbol: str) -> list[dict[str, Any]]:
|
||||
"""Get OTC/dark pool aggregate trade data (FINRA)."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.darkpool.otc, symbol, provider="finra"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Dark pool OTC failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
71
surveys_service.py
Normal file
71
surveys_service.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""Economy surveys: Michigan, SLOOS, NFP, Empire State, BLS."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from openbb import obb
|
||||
|
||||
from obb_utils import to_list
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def get_michigan() -> list[dict[str, Any]]:
|
||||
"""Get University of Michigan Consumer Sentiment + inflation expectations."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.economy.survey.university_of_michigan, provider="fred"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Michigan survey failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_sloos() -> list[dict[str, Any]]:
|
||||
"""Get Senior Loan Officer Opinion Survey (recession predictor)."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.economy.survey.sloos, provider="fred"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("SLOOS failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_nonfarm_payrolls() -> list[dict[str, Any]]:
|
||||
"""Get detailed employment data (NFP)."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.economy.survey.nonfarm_payrolls, provider="fred"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("NFP failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_empire_state() -> list[dict[str, Any]]:
|
||||
"""Get Empire State Manufacturing Survey."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.economy.survey.manufacturing_outlook_ny, provider="fred"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Empire State failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def bls_search(query: str) -> list[dict[str, Any]]:
|
||||
"""Search BLS data series."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.economy.survey.bls_search, query, provider="bls"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("BLS search failed for %s", query, exc_info=True)
|
||||
return []
|
||||
216
task_plan.md
Normal file
216
task_plan.md
Normal file
@@ -0,0 +1,216 @@
|
||||
# OpenBB Feature Expansion Plan
|
||||
|
||||
> 67 new endpoints across 10 feature groups. All use free providers.
|
||||
|
||||
## Prerequisites (Do First)
|
||||
|
||||
### P0: Consolidate Shared Utilities
|
||||
- [ ] Replace duplicate `_to_dicts` in `openbb_service.py` and `macro_service.py` with `obb_utils.to_list`
|
||||
- [ ] Add `fetch_historical(symbol, days, provider)` helper to `obb_utils.py`
|
||||
- [ ] Add `serialize_dates(items)` helper to `obb_utils.py`
|
||||
- **Files:** `obb_utils.py`, `openbb_service.py`, `macro_service.py`, `technical_service.py`, `quantitative_service.py`
|
||||
- **Complexity:** S
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Local Computation (No Provider Risk)
|
||||
|
||||
### Group I: Technical Analysis Extended (14 endpoints)
|
||||
- [ ] Add generic indicator dispatcher to `technical_service.py`
|
||||
- [ ] Implement indicators: ATR, ADX, Stochastic, OBV, VWAP, Ichimoku, Donchian, Aroon, CCI, Keltner Channels, Fibonacci, A/D Line, Volatility Cones, Relative Rotation
|
||||
- [ ] Add individual endpoints to `routes_technical.py`
|
||||
- [ ] Add generic endpoint: `GET /api/v1/stock/{symbol}/technical/{indicator}`
|
||||
- **New endpoints:**
|
||||
- `GET /api/v1/stock/{symbol}/technical/atr` -- Average True Range (volatility, position sizing)
|
||||
- `GET /api/v1/stock/{symbol}/technical/adx` -- Average Directional Index (trend strength)
|
||||
- `GET /api/v1/stock/{symbol}/technical/stoch` -- Stochastic Oscillator (overbought/oversold)
|
||||
- `GET /api/v1/stock/{symbol}/technical/obv` -- On-Balance Volume (volume-price divergence)
|
||||
- `GET /api/v1/stock/{symbol}/technical/vwap` -- Volume Weighted Average Price
|
||||
- `GET /api/v1/stock/{symbol}/technical/ichimoku` -- Ichimoku Cloud (comprehensive trend)
|
||||
- `GET /api/v1/stock/{symbol}/technical/donchian` -- Donchian Channels (breakout detection)
|
||||
- `GET /api/v1/stock/{symbol}/technical/aroon` -- Aroon Indicator (trend changes)
|
||||
- `GET /api/v1/stock/{symbol}/technical/cci` -- Commodity Channel Index (cyclical trends)
|
||||
- `GET /api/v1/stock/{symbol}/technical/kc` -- Keltner Channels (volatility bands)
|
||||
- `GET /api/v1/stock/{symbol}/technical/fib` -- Fibonacci Retracement (support/resistance)
|
||||
- `GET /api/v1/stock/{symbol}/technical/ad` -- Accumulation/Distribution Line
|
||||
- `GET /api/v1/stock/{symbol}/technical/cones` -- Volatility Cones (implied vs realized vol)
|
||||
- `GET /api/v1/stock/{symbol}/technical/relative_rotation` -- RRG (sector rotation)
|
||||
- **Extend:** `technical_service.py` (+200 lines), `routes_technical.py` (+80 lines)
|
||||
- **Complexity:** L (high volume, low individual complexity)
|
||||
|
||||
### Group J: Quantitative Extended (8 endpoints)
|
||||
- [ ] Add Sortino ratio, Omega ratio
|
||||
- [ ] Add rolling statistics: variance, stdev, mean, skew, kurtosis, quantile
|
||||
- **New endpoints:**
|
||||
- `GET /api/v1/stock/{symbol}/sortino?days=365` -- Sortino ratio (downside risk only)
|
||||
- `GET /api/v1/stock/{symbol}/omega?days=365` -- Omega ratio (full distribution)
|
||||
- `GET /api/v1/stock/{symbol}/rolling/variance?days=365&window=30` -- Rolling variance
|
||||
- `GET /api/v1/stock/{symbol}/rolling/stdev?days=365&window=30` -- Rolling std deviation
|
||||
- `GET /api/v1/stock/{symbol}/rolling/mean?days=365&window=30` -- Rolling mean
|
||||
- `GET /api/v1/stock/{symbol}/rolling/skew?days=365&window=30` -- Rolling skewness
|
||||
- `GET /api/v1/stock/{symbol}/rolling/kurtosis?days=365&window=30` -- Rolling kurtosis
|
||||
- `GET /api/v1/stock/{symbol}/rolling/quantile?days=365&window=30&quantile=0.5` -- Rolling quantile
|
||||
- **Extend:** `quantitative_service.py` (+120 lines), `routes_quantitative.py` (+60 lines)
|
||||
- **Complexity:** M
|
||||
|
||||
### Group H: Currency Reference Rates (1 endpoint)
|
||||
- [ ] Add ECB reference rates to `market_service.py`
|
||||
- **New endpoint:**
|
||||
- `GET /api/v1/currency/reference-rates` -- ECB reference rates for 28 currencies
|
||||
- **Extend:** `market_service.py` (+15 lines), `routes_market.py` (+10 lines)
|
||||
- **Complexity:** S
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: FRED/Federal Reserve Providers
|
||||
|
||||
### Group C: Fixed Income (10 endpoints)
|
||||
- [ ] Create `fixed_income_service.py` -- treasury rates, yield curve, auctions, TIPS, EFFR, SOFR, HQM, commercial paper, spot rates, spreads
|
||||
- [ ] Create `routes_fixed_income.py`
|
||||
- [ ] Register router in `main.py`
|
||||
- **New endpoints:**
|
||||
- `GET /api/v1/fixed-income/treasury-rates` -- Full yield curve rates (4W-30Y)
|
||||
- `GET /api/v1/fixed-income/yield-curve?date=` -- Yield curve with maturity/rate pairs
|
||||
- `GET /api/v1/fixed-income/treasury-auctions?security_type=` -- Auction bid-to-cover, yields
|
||||
- `GET /api/v1/fixed-income/tips-yields` -- TIPS real yields by maturity
|
||||
- `GET /api/v1/fixed-income/effr` -- Effective Fed Funds Rate with percentiles
|
||||
- `GET /api/v1/fixed-income/sofr` -- SOFR rate with moving averages
|
||||
- `GET /api/v1/fixed-income/hqm` -- High Quality Market corporate bond yields
|
||||
- `GET /api/v1/fixed-income/commercial-paper` -- CP rates by maturity/type
|
||||
- `GET /api/v1/fixed-income/spot-rates` -- Corporate bond spot rates
|
||||
- `GET /api/v1/fixed-income/spreads?series=tcm` -- Treasury/corporate spreads
|
||||
- **New files:** `fixed_income_service.py` (~250 lines), `routes_fixed_income.py` (~180 lines)
|
||||
- **Complexity:** L
|
||||
|
||||
### Group D: Economy Expanded (13 endpoints)
|
||||
- [ ] Extend `macro_service.py` with structured FRED indicators (CPI, GDP, unemployment, PCE, money measures)
|
||||
- [ ] Create `economy_service.py` for non-series endpoints (fred_search, fred_regional, balance_of_payments, central_bank_holdings, primary_dealer_positioning, fomc_documents)
|
||||
- [ ] Extend `routes_macro.py` for FRED-based indicators
|
||||
- [ ] Create `routes_economy.py` for search/institutional data
|
||||
- [ ] Register new router in `main.py`
|
||||
- **New endpoints (extend routes_macro.py):**
|
||||
- `GET /api/v1/macro/cpi?country=united_states` -- Consumer Price Index (multi-country)
|
||||
- `GET /api/v1/macro/gdp?type=real` -- GDP nominal/real/forecast
|
||||
- `GET /api/v1/macro/unemployment?country=united_states` -- Unemployment rate (multi-country)
|
||||
- `GET /api/v1/macro/pce` -- Personal Consumption Expenditures (Fed preferred inflation)
|
||||
- `GET /api/v1/macro/money-measures` -- M1/M2 money supply
|
||||
- `GET /api/v1/macro/cli?country=united_states` -- Composite Leading Indicator
|
||||
- `GET /api/v1/macro/house-price-index?country=united_states` -- Housing price index
|
||||
- **New endpoints (new routes_economy.py):**
|
||||
- `GET /api/v1/economy/fred-search?query=` -- Search FRED series by keyword
|
||||
- `GET /api/v1/economy/fred-regional?series_id=®ion=` -- Regional economic data
|
||||
- `GET /api/v1/economy/balance-of-payments` -- Current/capital/financial account
|
||||
- `GET /api/v1/economy/central-bank-holdings` -- Fed SOMA portfolio
|
||||
- `GET /api/v1/economy/primary-dealer-positioning` -- Wall Street firm positions
|
||||
- `GET /api/v1/economy/fomc-documents?year=` -- FOMC meeting documents
|
||||
- **New files:** `economy_service.py` (~200 lines), `routes_economy.py` (~150 lines)
|
||||
- **Extend:** `macro_service.py` (+80 lines), `routes_macro.py` (+50 lines)
|
||||
- **Complexity:** L
|
||||
|
||||
### Group E: Economy Surveys (5 endpoints)
|
||||
- [ ] Create `surveys_service.py` -- Michigan, SLOOS, NFP, Empire State, BLS
|
||||
- [ ] Create `routes_surveys.py`
|
||||
- [ ] Register router in `main.py`
|
||||
- **New endpoints:**
|
||||
- `GET /api/v1/economy/surveys/michigan` -- Consumer Sentiment + inflation expectations
|
||||
- `GET /api/v1/economy/surveys/sloos` -- Senior Loan Officer survey (recession predictor)
|
||||
- `GET /api/v1/economy/surveys/nonfarm-payrolls` -- Detailed employment data
|
||||
- `GET /api/v1/economy/surveys/empire-state` -- NY manufacturing outlook
|
||||
- `GET /api/v1/economy/surveys/bls-search?query=` -- BLS data series search
|
||||
- **New files:** `surveys_service.py` (~130 lines), `routes_surveys.py` (~100 lines)
|
||||
- **Complexity:** M
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: SEC/Stockgrid/CFTC Providers
|
||||
|
||||
### Group B: Equity Fundamentals (4 endpoints)
|
||||
- [ ] Add management, dividends, filings, search to `openbb_service.py`
|
||||
- [ ] Add endpoints to `routes.py`
|
||||
- **New endpoints:**
|
||||
- `GET /api/v1/stock/{symbol}/management` -- Executive team, titles, compensation
|
||||
- `GET /api/v1/stock/{symbol}/dividends` -- Historical dividend records
|
||||
- `GET /api/v1/stock/{symbol}/filings?form_type=10-K` -- SEC filings (10-K, 10-Q, 8-K)
|
||||
- `GET /api/v1/search?query=` -- Company search by name (SEC/NASDAQ)
|
||||
- **Extend:** `openbb_service.py` (+60 lines), `routes.py` (+40 lines)
|
||||
- **Complexity:** S
|
||||
|
||||
### Group A: Equity Shorts & Dark Pool (4 endpoints)
|
||||
- [ ] Create `shorts_service.py` -- short volume, FTD, short interest, OTC dark pool
|
||||
- [ ] Create `routes_shorts.py`
|
||||
- [ ] Register router in `main.py`
|
||||
- **New endpoints:**
|
||||
- `GET /api/v1/stock/{symbol}/shorts/volume` -- Daily short volume & percent (stockgrid)
|
||||
- `GET /api/v1/stock/{symbol}/shorts/ftd` -- Fails-to-deliver records (SEC)
|
||||
- `GET /api/v1/stock/{symbol}/shorts/interest` -- Short interest, days to cover (FINRA)
|
||||
- `GET /api/v1/darkpool/{symbol}/otc` -- OTC/dark pool trade volume (FINRA)
|
||||
- **New files:** `shorts_service.py` (~120 lines), `routes_shorts.py` (~80 lines)
|
||||
- **Complexity:** M
|
||||
|
||||
### Group F: Index & ETF Enhanced (3 endpoints)
|
||||
- [ ] Add sp500_multiples, index_constituents, etf nport_disclosure to `market_service.py`
|
||||
- [ ] Add endpoints to `routes_market.py`
|
||||
- **New endpoints:**
|
||||
- `GET /api/v1/index/sp500-multiples?series=pe_ratio` -- Historical S&P 500 valuation (Shiller PE, P/B, P/S, dividend yield)
|
||||
- `GET /api/v1/index/{symbol}/constituents` -- Index member stocks with sector/price data
|
||||
- `GET /api/v1/etf/{symbol}/nport` -- Detailed ETF holdings from SEC N-PORT filings
|
||||
- **Extend:** `market_service.py` (+60 lines), `routes_market.py` (+50 lines)
|
||||
- **Complexity:** S
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Regulators
|
||||
|
||||
### Group G: Regulators (5 endpoints)
|
||||
- [ ] Create `regulators_service.py` -- COT, COT search, SEC litigation, institution search, CIK mapping
|
||||
- [ ] Create `routes_regulators.py`
|
||||
- [ ] Register router in `main.py`
|
||||
- **New endpoints:**
|
||||
- `GET /api/v1/regulators/cot?symbol=` -- Commitment of Traders report (commercial/speculator positions)
|
||||
- `GET /api/v1/regulators/cot/search?query=` -- Search COT report symbols
|
||||
- `GET /api/v1/regulators/sec/litigation` -- SEC litigation releases RSS feed
|
||||
- `GET /api/v1/regulators/sec/institutions?query=` -- Search institutional investors
|
||||
- `GET /api/v1/regulators/sec/cik-map?symbol=` -- Ticker to CIK mapping
|
||||
- **New files:** `regulators_service.py` (~150 lines), `routes_regulators.py` (~100 lines)
|
||||
- **Complexity:** M
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Phase | Groups | Endpoints | New Files | Complexity |
|
||||
|-------|--------|-----------|-----------|------------|
|
||||
| P0 Prereq | - | 0 | 0 | S |
|
||||
| Phase 1 | I, J, H | 23 | 0 | L+M+S |
|
||||
| Phase 2 | C, D, E | 28 | 6 | L+L+M |
|
||||
| Phase 3 | B, A, F | 11 | 2 | S+M+S |
|
||||
| Phase 4 | G | 5 | 2 | M |
|
||||
| **Total** | **10** | **67** | **10** | |
|
||||
|
||||
### File Impact
|
||||
|
||||
**New files (10):**
|
||||
- `shorts_service.py`, `routes_shorts.py`
|
||||
- `fixed_income_service.py`, `routes_fixed_income.py`
|
||||
- `economy_service.py`, `routes_economy.py`
|
||||
- `surveys_service.py`, `routes_surveys.py`
|
||||
- `regulators_service.py`, `routes_regulators.py`
|
||||
|
||||
**Extended files (12):**
|
||||
- `obb_utils.py` (shared helpers)
|
||||
- `openbb_service.py` (Group B fundamentals)
|
||||
- `routes.py` (Group B endpoints)
|
||||
- `macro_service.py` (Group D indicators)
|
||||
- `routes_macro.py` (Group D endpoints)
|
||||
- `market_service.py` (Groups F, H)
|
||||
- `routes_market.py` (Groups F, H)
|
||||
- `technical_service.py` (Group I indicators)
|
||||
- `routes_technical.py` (Group I endpoints)
|
||||
- `quantitative_service.py` (Group J metrics)
|
||||
- `routes_quantitative.py` (Group J endpoints)
|
||||
- `main.py` (register 5 new routers)
|
||||
|
||||
### Endpoint Count After Completion
|
||||
- Current: 32 endpoints
|
||||
- New: 67 endpoints
|
||||
- **Total: 99 endpoints**
|
||||
@@ -6,28 +6,17 @@ from typing import Any
|
||||
|
||||
from openbb import obb
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from obb_utils import fetch_historical, to_list
|
||||
|
||||
PROVIDER = "yfinance"
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def get_technical_indicators(
|
||||
symbol: str, days: int = 400
|
||||
) -> dict[str, Any]:
|
||||
"""Compute key technical indicators for a symbol."""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
start = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
|
||||
|
||||
# Fetch historical data first
|
||||
hist = await asyncio.to_thread(
|
||||
obb.equity.price.historical,
|
||||
symbol,
|
||||
start_date=start,
|
||||
provider=PROVIDER,
|
||||
)
|
||||
|
||||
if hist is None or hist.results is None:
|
||||
hist = await fetch_historical(symbol, days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data available"}
|
||||
|
||||
result: dict[str, Any] = {"symbol": symbol}
|
||||
@@ -144,3 +133,355 @@ def _interpret_signals(data: dict[str, Any]) -> list[str]:
|
||||
signals.append("Death cross: SMA50 below SMA200 (bearish trend)")
|
||||
|
||||
return signals
|
||||
|
||||
|
||||
# --- Individual Indicator Functions (Phase 1, Group I) ---
|
||||
|
||||
|
||||
async def get_atr(symbol: str, length: int = 14, days: int = 400) -> dict[str, Any]:
|
||||
"""Average True Range -- volatility measurement for position sizing."""
|
||||
hist = await fetch_historical(symbol, days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.technical.atr, data=hist.results, length=length
|
||||
)
|
||||
latest = _extract_latest(result)
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"length": length,
|
||||
"atr": latest.get(f"ATRr_{length}"),
|
||||
}
|
||||
except Exception:
|
||||
logger.warning("ATR failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute ATR"}
|
||||
|
||||
|
||||
async def get_adx(symbol: str, length: int = 14, days: int = 400) -> dict[str, Any]:
|
||||
"""Average Directional Index -- trend strength (>25 strong, <20 range-bound)."""
|
||||
hist = await fetch_historical(symbol, days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.technical.adx, data=hist.results, length=length
|
||||
)
|
||||
latest = _extract_latest(result)
|
||||
adx_val = latest.get(f"ADX_{length}")
|
||||
signal = "strong trend" if adx_val and adx_val > 25 else "range-bound"
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"length": length,
|
||||
"adx": adx_val,
|
||||
"dmp": latest.get(f"DMP_{length}"),
|
||||
"dmn": latest.get(f"DMN_{length}"),
|
||||
"signal": signal,
|
||||
}
|
||||
except Exception:
|
||||
logger.warning("ADX failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute ADX"}
|
||||
|
||||
|
||||
async def get_stoch(
|
||||
symbol: str, fast_k: int = 14, slow_d: int = 3, slow_k: int = 3, days: int = 400,
|
||||
) -> dict[str, Any]:
|
||||
"""Stochastic Oscillator -- overbought/oversold momentum signal."""
|
||||
hist = await fetch_historical(symbol, days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.technical.stoch, data=hist.results,
|
||||
fast_k=fast_k, slow_d=slow_d, slow_k=slow_k,
|
||||
)
|
||||
latest = _extract_latest(result)
|
||||
k_val = latest.get(f"STOCHk_{fast_k}_{slow_d}_{slow_k}")
|
||||
d_val = latest.get(f"STOCHd_{fast_k}_{slow_d}_{slow_k}")
|
||||
signal = "neutral"
|
||||
if k_val is not None:
|
||||
if k_val > 80:
|
||||
signal = "overbought"
|
||||
elif k_val < 20:
|
||||
signal = "oversold"
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"stoch_k": k_val,
|
||||
"stoch_d": d_val,
|
||||
"signal": signal,
|
||||
}
|
||||
except Exception:
|
||||
logger.warning("Stochastic failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute Stochastic"}
|
||||
|
||||
|
||||
async def get_obv(symbol: str, days: int = 400) -> dict[str, Any]:
|
||||
"""On-Balance Volume -- cumulative volume indicator for divergence detection."""
|
||||
hist = await fetch_historical(symbol, days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
result = await asyncio.to_thread(obb.technical.obv, data=hist.results)
|
||||
latest = _extract_latest(result)
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"obv": latest.get("OBV"),
|
||||
}
|
||||
except Exception:
|
||||
logger.warning("OBV failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute OBV"}
|
||||
|
||||
|
||||
async def get_ichimoku(symbol: str, days: int = 400) -> dict[str, Any]:
|
||||
"""Ichimoku Cloud -- comprehensive trend system."""
|
||||
hist = await fetch_historical(symbol, days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
result = await asyncio.to_thread(obb.technical.ichimoku, data=hist.results)
|
||||
latest = _extract_latest(result)
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"tenkan_sen": latest.get("ITS_9"),
|
||||
"kijun_sen": latest.get("IKS_26"),
|
||||
"senkou_span_a": latest.get("ISA_9"),
|
||||
"senkou_span_b": latest.get("ISB_26"),
|
||||
"chikou_span": latest.get("ICS_26"),
|
||||
}
|
||||
except Exception:
|
||||
logger.warning("Ichimoku failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute Ichimoku"}
|
||||
|
||||
|
||||
async def get_donchian(symbol: str, length: int = 20, days: int = 400) -> dict[str, Any]:
|
||||
"""Donchian Channels -- breakout detection system."""
|
||||
hist = await fetch_historical(symbol, days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.technical.donchian, data=hist.results, lower_length=length, upper_length=length,
|
||||
)
|
||||
latest = _extract_latest(result)
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"length": length,
|
||||
"upper": latest.get(f"DCU_{length}_{length}"),
|
||||
"middle": latest.get(f"DCM_{length}_{length}"),
|
||||
"lower": latest.get(f"DCL_{length}_{length}"),
|
||||
}
|
||||
except Exception:
|
||||
logger.warning("Donchian failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute Donchian"}
|
||||
|
||||
|
||||
async def get_aroon(symbol: str, length: int = 25, days: int = 400) -> dict[str, Any]:
|
||||
"""Aroon Indicator -- trend direction and strength."""
|
||||
hist = await fetch_historical(symbol, days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.technical.aroon, data=hist.results, length=length,
|
||||
)
|
||||
latest = _extract_latest(result)
|
||||
up = latest.get(f"AROONU_{length}")
|
||||
down = latest.get(f"AROOND_{length}")
|
||||
osc = latest.get(f"AROONOSC_{length}")
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"length": length,
|
||||
"aroon_up": up,
|
||||
"aroon_down": down,
|
||||
"aroon_oscillator": osc,
|
||||
}
|
||||
except Exception:
|
||||
logger.warning("Aroon failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute Aroon"}
|
||||
|
||||
|
||||
async def get_cci(symbol: str, length: int = 14, days: int = 400) -> dict[str, Any]:
|
||||
"""Commodity Channel Index -- cyclical trend identification."""
|
||||
hist = await fetch_historical(symbol, days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.technical.cci, data=hist.results, length=length,
|
||||
)
|
||||
latest = _extract_latest(result)
|
||||
cci_val = latest.get(f"CCI_{length}_{0.015}")
|
||||
signal = "neutral"
|
||||
if cci_val is not None:
|
||||
if cci_val > 100:
|
||||
signal = "overbought"
|
||||
elif cci_val < -100:
|
||||
signal = "oversold"
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"length": length,
|
||||
"cci": cci_val,
|
||||
"signal": signal,
|
||||
}
|
||||
except Exception:
|
||||
logger.warning("CCI failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute CCI"}
|
||||
|
||||
|
||||
async def get_kc(symbol: str, length: int = 20, days: int = 400) -> dict[str, Any]:
|
||||
"""Keltner Channels -- ATR-based volatility bands."""
|
||||
hist = await fetch_historical(symbol, days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.technical.kc, data=hist.results, length=length,
|
||||
)
|
||||
latest = _extract_latest(result)
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"length": length,
|
||||
"upper": latest.get(f"KCUe_{length}_2"),
|
||||
"middle": latest.get(f"KCBe_{length}_2"),
|
||||
"lower": latest.get(f"KCLe_{length}_2"),
|
||||
}
|
||||
except Exception:
|
||||
logger.warning("Keltner failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute Keltner Channels"}
|
||||
|
||||
|
||||
async def get_fib(symbol: str, days: int = 120) -> dict[str, Any]:
|
||||
"""Fibonacci Retracement levels from recent price range."""
|
||||
hist = await fetch_historical(symbol, days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
result = await asyncio.to_thread(obb.technical.fib, data=hist.results)
|
||||
latest = _extract_latest(result)
|
||||
return {"symbol": symbol, **latest}
|
||||
except Exception:
|
||||
logger.warning("Fibonacci failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute Fibonacci"}
|
||||
|
||||
|
||||
async def get_ad(symbol: str, days: int = 400) -> dict[str, Any]:
|
||||
"""Accumulation/Distribution Line -- volume-based trend indicator."""
|
||||
hist = await fetch_historical(symbol, days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
result = await asyncio.to_thread(obb.technical.ad, data=hist.results)
|
||||
latest = _extract_latest(result)
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"ad": latest.get("AD"),
|
||||
"ad_obv": latest.get("AD_OBV"),
|
||||
}
|
||||
except Exception:
|
||||
logger.warning("A/D failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute A/D Line"}
|
||||
|
||||
|
||||
async def get_vwap(symbol: str, days: int = 5) -> dict[str, Any]:
|
||||
"""Volume Weighted Average Price -- intraday fair value benchmark."""
|
||||
hist = await fetch_historical(symbol, days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
result = await asyncio.to_thread(obb.technical.vwap, data=hist.results)
|
||||
latest = _extract_latest(result)
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"vwap": latest.get("VWAP_D"),
|
||||
}
|
||||
except Exception:
|
||||
logger.warning("VWAP failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute VWAP"}
|
||||
|
||||
|
||||
async def get_relative_rotation(
|
||||
symbols: list[str],
|
||||
benchmark: str = "SPY",
|
||||
days: int = 365,
|
||||
study: str = "price",
|
||||
) -> dict[str, Any]:
|
||||
"""Relative Rotation -- strength ratio and momentum vs benchmark.
|
||||
|
||||
Requires multiple symbols compared against a single benchmark.
|
||||
Returns RS-Ratio and RS-Momentum for each symbol, indicating
|
||||
which RRG quadrant they occupy (Leading/Weakening/Lagging/Improving).
|
||||
"""
|
||||
from datetime import datetime, timedelta, timezone as tz
|
||||
|
||||
start = (datetime.now(tz=tz.utc) - timedelta(days=days)).strftime("%Y-%m-%d")
|
||||
all_symbols = ",".join(symbols + [benchmark])
|
||||
|
||||
try:
|
||||
hist = await asyncio.to_thread(
|
||||
obb.equity.price.historical,
|
||||
all_symbols,
|
||||
start_date=start,
|
||||
provider="yfinance",
|
||||
)
|
||||
if hist is None or hist.results is None:
|
||||
return {"symbols": symbols, "benchmark": benchmark, "error": "No historical data"}
|
||||
|
||||
result = await asyncio.to_thread(
|
||||
obb.technical.relative_rotation,
|
||||
data=hist.results,
|
||||
benchmark=benchmark,
|
||||
study=study,
|
||||
)
|
||||
items = to_list(result)
|
||||
|
||||
latest_by_symbol: dict[str, dict[str, Any]] = {}
|
||||
for item in items:
|
||||
sym = item.get("symbol")
|
||||
if sym and sym != benchmark:
|
||||
latest_by_symbol[sym] = item
|
||||
|
||||
entries = [
|
||||
{**item, "quadrant": _classify_rrg_quadrant(item)}
|
||||
for item in latest_by_symbol.values()
|
||||
]
|
||||
|
||||
return {
|
||||
"symbols": symbols,
|
||||
"benchmark": benchmark,
|
||||
"study": study,
|
||||
"data": entries,
|
||||
}
|
||||
except Exception:
|
||||
logger.warning("Relative rotation failed for %s", symbols, exc_info=True)
|
||||
return {"symbols": symbols, "error": "Failed to compute relative rotation"}
|
||||
|
||||
|
||||
def _classify_rrg_quadrant(item: dict[str, Any]) -> str | None:
|
||||
"""Classify RRG quadrant from RS-Ratio and RS-Momentum."""
|
||||
rs_ratio = item.get("rs_ratio")
|
||||
rs_momentum = item.get("rs_momentum")
|
||||
if rs_ratio is None or rs_momentum is None:
|
||||
return None
|
||||
if rs_ratio > 100 and rs_momentum > 100:
|
||||
return "Leading"
|
||||
if rs_ratio > 100:
|
||||
return "Weakening"
|
||||
if rs_momentum <= 100:
|
||||
return "Lagging"
|
||||
return "Improving"
|
||||
|
||||
|
||||
async def get_cones(symbol: str, days: int = 365) -> dict[str, Any]:
|
||||
"""Volatility Cones -- realized volatility quantiles for options analysis."""
|
||||
hist = await fetch_historical(symbol, days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.technical.cones, data=hist.results,
|
||||
)
|
||||
items = to_list(result)
|
||||
return {"symbol": symbol, "cones": items}
|
||||
except Exception:
|
||||
logger.warning("Volatility cones failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute volatility cones"}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from openbb_service import _to_dicts, _first_or_empty
|
||||
from obb_utils import to_list, first_or_empty
|
||||
|
||||
|
||||
class MockModel:
|
||||
@@ -14,34 +14,34 @@ class MockOBBject:
|
||||
self.results = results
|
||||
|
||||
|
||||
class TestToDicts:
|
||||
class TestToList:
|
||||
def test_none_result(self):
|
||||
assert _to_dicts(None) == []
|
||||
assert to_list(None) == []
|
||||
|
||||
def test_none_results(self):
|
||||
obj = MockOBBject(results=None)
|
||||
assert _to_dicts(obj) == []
|
||||
assert to_list(obj) == []
|
||||
|
||||
def test_list_results(self):
|
||||
obj = MockOBBject(results=[
|
||||
MockModel({"a": 1}),
|
||||
MockModel({"b": 2}),
|
||||
])
|
||||
result = _to_dicts(obj)
|
||||
result = to_list(obj)
|
||||
assert len(result) == 2
|
||||
assert result[0] == {"a": 1}
|
||||
|
||||
def test_single_result(self):
|
||||
obj = MockOBBject(results=MockModel({"x": 42}))
|
||||
result = _to_dicts(obj)
|
||||
result = to_list(obj)
|
||||
assert result == [{"x": 42}]
|
||||
|
||||
|
||||
class TestFirstOrEmpty:
|
||||
def test_empty(self):
|
||||
assert _first_or_empty(None) == {}
|
||||
assert first_or_empty(None) == {}
|
||||
|
||||
def test_with_data(self):
|
||||
obj = MockOBBject(results=[MockModel({"price": 150.0})])
|
||||
result = _first_or_empty(obj)
|
||||
result = first_or_empty(obj)
|
||||
assert result == {"price": 150.0}
|
||||
|
||||
@@ -66,10 +66,12 @@ async def test_stock_recommendations(mock_recs, client):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("routes_sentiment.finnhub_service.get_upgrade_downgrade", new_callable=AsyncMock)
|
||||
@patch("routes_sentiment.openbb_service.get_upgrades_downgrades", new_callable=AsyncMock)
|
||||
async def test_stock_upgrades(mock_upgrades, client):
|
||||
mock_upgrades.return_value = [
|
||||
{"company": "Morgan Stanley", "action": "upgrade", "fromGrade": "Hold", "toGrade": "Buy"}
|
||||
{"date": "2026-03-05", "company": "Morgan Stanley", "action": "upgrade",
|
||||
"from_grade": "Hold", "to_grade": "Buy", "price_target_action": "Raises",
|
||||
"current_price_target": 300.0, "prior_price_target": 250.0}
|
||||
]
|
||||
resp = await client.get("/api/v1/stock/AAPL/upgrades")
|
||||
assert resp.status_code == 200
|
||||
|
||||
Reference in New Issue
Block a user