Compare commits
19 Commits
26cd716590
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca8d7099b3 | ||
|
|
c5c9c7db83 | ||
|
|
a57a6835c5 | ||
|
|
89bdc6c552 | ||
|
|
e2cf6e2488 | ||
|
|
615f17a3bb | ||
|
|
87260f4b10 | ||
|
|
b6f49055ad | ||
|
|
ac101c663a | ||
|
|
f5b22deec3 | ||
|
|
b631c888a5 | ||
|
|
760b0a09ea | ||
|
|
16ad276146 | ||
|
|
e5820ebe4a | ||
|
|
cd6158b05c | ||
|
|
2446a2fde8 | ||
|
|
d3c919385f | ||
|
|
e797f8929d | ||
|
|
d46e8685d7 |
@@ -3,14 +3,16 @@ FROM python:3.12-slim AS base
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apt-get update && \
|
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/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY pyproject.toml ./
|
COPY pyproject.toml ./
|
||||||
|
|
||||||
RUN pip install --no-cache-dir . && \
|
RUN pip install --no-cache-dir . && \
|
||||||
pip install --no-cache-dir openbb-quantitative openbb-econometrics openbb-technical && \
|
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
|
apt-get autoremove -y
|
||||||
|
|
||||||
COPY *.py ./
|
COPY *.py ./
|
||||||
|
|||||||
275
README.md
275
README.md
@@ -1,6 +1,6 @@
|
|||||||
# OpenBB Investment Analysis API
|
# 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
|
## 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 |
|
| 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 |
|
| **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: Fed rate, CPI, GDP, unemployment, treasury yields | 120 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 |
|
| **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 |
|
| Provider | Data Provided |
|
||||||
|----------|-------------|--------------|
|
|----------|---------------|
|
||||||
| **FMP** | `OBB_FMP_API_KEY` | More granular financials, earnings transcripts (250 calls/day free) |
|
| **yfinance** | Quotes, fundamentals, financials, historical prices, news, ETF, index, crypto, forex, options, futures, analyst upgrades, price targets, dividends, management |
|
||||||
| **Intrinio** | `OBB_INTRINIO_API_KEY` | Institutional-grade fundamentals |
|
| **SEC** | Insider trading (Form 4), institutional holdings (13F), company filings, N-PORT disclosures, CIK mapping |
|
||||||
| **Tiingo** | `OBB_TIINGO_TOKEN` | Reliable historical price data |
|
| **stockgrid** | Short volume data |
|
||||||
| **Benzinga** | `OBB_BENZINGA_API_KEY` | Real-time news, analyst ratings |
|
| **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
|
### Configuration
|
||||||
|
|
||||||
@@ -40,9 +46,10 @@ export INVEST_API_ALPHAVANTAGE_API_KEY=your_alphavantage_key
|
|||||||
### 1. Create conda environment
|
### 1. Create conda environment
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
conda env create -f environment.yml
|
conda create -n openbb-invest-api python=3.12 -y
|
||||||
conda activate openbb-invest-api
|
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
|
### 2. Start the server
|
||||||
@@ -59,36 +66,39 @@ Server starts at `http://localhost:8000`. Visit `http://localhost:8000/docs` for
|
|||||||
# Health check
|
# Health check
|
||||||
curl http://localhost:8000/health
|
curl http://localhost:8000/health
|
||||||
|
|
||||||
# US stock quote
|
# Stock quote
|
||||||
curl http://localhost:8000/api/v1/stock/AAPL/quote
|
curl http://localhost:8000/api/v1/stock/AAPL/quote
|
||||||
|
|
||||||
# Swedish stock quote
|
# Technical indicators (14 individual + composite)
|
||||||
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
|
|
||||||
curl http://localhost:8000/api/v1/stock/AAPL/technical
|
curl http://localhost:8000/api/v1/stock/AAPL/technical
|
||||||
|
curl http://localhost:8000/api/v1/stock/AAPL/technical/ichimoku
|
||||||
|
|
||||||
# Quantitative risk metrics
|
# Relative Rotation Graph (multi-symbol)
|
||||||
curl http://localhost:8000/api/v1/stock/AAPL/performance
|
curl "http://localhost:8000/api/v1/technical/relative-rotation?symbols=AAPL,MSFT,GOOGL&benchmark=SPY"
|
||||||
curl http://localhost:8000/api/v1/stock/AAPL/capm
|
|
||||||
|
|
||||||
# SEC insider trading
|
# Quantitative analysis
|
||||||
curl http://localhost:8000/api/v1/stock/AAPL/sec-insider
|
curl http://localhost:8000/api/v1/stock/AAPL/sortino
|
||||||
|
curl http://localhost:8000/api/v1/stock/AAPL/rolling/skew?window=20
|
||||||
|
|
||||||
# ETF info
|
# Fixed income
|
||||||
curl http://localhost:8000/api/v1/etf/SPY/info
|
curl http://localhost:8000/api/v1/fixed-income/yield-curve
|
||||||
|
curl http://localhost:8000/api/v1/fixed-income/treasury-rates
|
||||||
|
|
||||||
# Crypto price history
|
# Macro economics
|
||||||
curl http://localhost:8000/api/v1/crypto/BTC-USD/historical?days=30
|
|
||||||
|
|
||||||
# Macro overview (requires FRED key)
|
|
||||||
curl http://localhost:8000/api/v1/macro/overview
|
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
|
# Portfolio analysis
|
||||||
curl -X POST http://localhost:8000/api/v1/portfolio/analyze \
|
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}]}'
|
-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
|
### 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}/historical?days=365` | Historical OHLCV data |
|
||||||
| GET | `/api/v1/stock/{symbol}/news` | Recent company news |
|
| 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}/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 |
|
| Method | Path | Description |
|
||||||
|--------|------|-------------|
|
|--------|------|-------------|
|
||||||
| GET | `/api/v1/stock/{symbol}/sentiment` | Aggregated: news sentiment + recommendations + upgrades |
|
| 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}/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}/insider-trades` | Insider transactions via Finnhub |
|
||||||
| GET | `/api/v1/stock/{symbol}/recommendations` | Monthly analyst buy/hold/sell counts |
|
| GET | `/api/v1/stock/{symbol}/recommendations` | Monthly analyst buy/hold/sell counts (Finnhub) |
|
||||||
| GET | `/api/v1/stock/{symbol}/upgrades` | Recent analyst upgrades and downgrades |
|
| 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 |
|
| 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 |
|
| Method | Path | Description |
|
||||||
|--------|------|-------------|
|
|--------|------|-------------|
|
||||||
| GET | `/api/v1/stock/{symbol}/performance?days=365` | Sharpe ratio, summary statistics, volatility |
|
| 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}/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}/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)
|
### 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/stock/{symbol}/institutional` | Institutional holders from SEC 13F filings |
|
||||||
| GET | `/api/v1/screener` | Stock screener |
|
| GET | `/api/v1/screener` | Stock screener |
|
||||||
|
|
||||||
### ETF Data (yfinance, no key needed)
|
### ETF Data (yfinance + SEC, no key needed)
|
||||||
|
|
||||||
| Method | Path | Description |
|
| Method | Path | Description |
|
||||||
|--------|------|-------------|
|
|--------|------|-------------|
|
||||||
| GET | `/api/v1/etf/{symbol}/info` | ETF profile, issuer, holdings |
|
| 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}/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 |
|
| 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 |
|
| Method | Path | Description |
|
||||||
|--------|------|-------------|
|
|--------|------|-------------|
|
||||||
| GET | `/api/v1/index/available` | List available indices |
|
| 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/{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)
|
### 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/{symbol}/historical?days=365` | Crypto price history (BTC-USD, ETH-USD) |
|
||||||
| GET | `/api/v1/crypto/search?query=` | Search cryptocurrencies |
|
| GET | `/api/v1/crypto/search?query=` | Search cryptocurrencies |
|
||||||
|
|
||||||
### Currency / Forex (yfinance, no key needed)
|
### Currency / Forex (yfinance + ECB, no key needed)
|
||||||
|
|
||||||
| Method | Path | Description |
|
| Method | Path | Description |
|
||||||
|--------|------|-------------|
|
|--------|------|-------------|
|
||||||
| GET | `/api/v1/currency/{symbol}/historical?days=365` | Forex price history (EURUSD, USDSEK) |
|
| 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)
|
### 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}/historical?days=365` | Futures price history |
|
||||||
| GET | `/api/v1/futures/{symbol}/curve` | Futures term structure/curve |
|
| GET | `/api/v1/futures/{symbol}/curve` | Futures term structure/curve |
|
||||||
|
|
||||||
### Macro Economics (FRED, free key)
|
### Regulators (CFTC/SEC, no key needed)
|
||||||
|
|
||||||
| Method | Path | Description |
|
| Method | Path | Description |
|
||||||
|--------|------|-------------|
|
|--------|------|-------------|
|
||||||
| GET | `/api/v1/macro/overview` | Key indicators: Fed rate, treasury yields, CPI, unemployment, GDP, VIX |
|
| GET | `/api/v1/regulators/cot?symbol=` | Commitment of Traders (futures positions) |
|
||||||
| GET | `/api/v1/macro/series/{series_id}?limit=30` | Any FRED time series by ID |
|
| 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)
|
### Portfolio Analysis (no key needed)
|
||||||
|
|
||||||
@@ -256,37 +353,50 @@ All settings are configurable via environment variables with the `INVEST_API_` p
|
|||||||
| `INVEST_API_LOG_LEVEL` | `info` | Logging level |
|
| `INVEST_API_LOG_LEVEL` | `info` | Logging level |
|
||||||
| `INVEST_API_DEBUG` | `false` | Enable debug mode (auto-reload) |
|
| `INVEST_API_DEBUG` | `false` | Enable debug mode (auto-reload) |
|
||||||
| `INVEST_API_FINNHUB_API_KEY` | _(empty)_ | Finnhub API key for analyst data |
|
| `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 |
|
| `INVEST_API_ALPHAVANTAGE_API_KEY` | _(empty)_ | Alpha Vantage API key for news sentiment |
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
openbb-invest-api/
|
openbb-invest-api/
|
||||||
├── main.py # FastAPI app entry point
|
├── main.py # FastAPI app entry point (lifespan, curl_cffi patch)
|
||||||
├── config.py # Settings (env-based)
|
├── config.py # Settings (env-based)
|
||||||
├── models.py # Pydantic request/response models
|
├── models.py # Pydantic request/response models
|
||||||
├── mappers.py # Dict-to-model mapping functions
|
├── mappers.py # Dict-to-model mapping functions
|
||||||
├── route_utils.py # Shared route utilities (validation, error handling)
|
├── route_utils.py # Shared route utilities (validation, error handling)
|
||||||
├── obb_utils.py # Shared OpenBB result conversion utilities
|
├── obb_utils.py # Shared OpenBB result conversion + fetch helpers
|
||||||
├── openbb_service.py # OpenBB SDK wrapper (async)
|
│
|
||||||
|
├── openbb_service.py # Equity data via OpenBB/yfinance (quote, profile, metrics, etc.)
|
||||||
├── finnhub_service.py # Finnhub REST client (insider, analyst data)
|
├── finnhub_service.py # Finnhub REST client (insider, analyst data)
|
||||||
├── alphavantage_service.py # Alpha Vantage REST client (news sentiment)
|
├── alphavantage_service.py # Alpha Vantage REST client (news sentiment)
|
||||||
├── quantitative_service.py # Risk metrics, CAPM, normality tests
|
├── technical_service.py # 14 technical indicators via openbb-technical
|
||||||
├── calendar_service.py # Calendar events, screening, ownership
|
├── quantitative_service.py # Risk metrics, CAPM, Sortino, Omega, rolling stats
|
||||||
├── market_service.py # ETF, index, crypto, currency, derivatives
|
|
||||||
├── macro_service.py # FRED macro data via OpenBB
|
├── macro_service.py # FRED macro data via OpenBB
|
||||||
├── technical_service.py # Technical indicators via openbb-technical
|
├── 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
|
├── analysis_service.py # Rule engine for portfolio analysis
|
||||||
|
│
|
||||||
├── routes.py # Core stock data + portfolio + discovery routes
|
├── routes.py # Core stock data + portfolio + discovery routes
|
||||||
├── routes_sentiment.py # Sentiment & analyst routes (Finnhub + Alpha Vantage)
|
├── routes_sentiment.py # Sentiment & analyst routes
|
||||||
|
├── routes_technical.py # Technical analysis routes (14 indicators)
|
||||||
├── routes_quantitative.py # Quantitative analysis routes
|
├── 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_calendar.py # Calendar, estimates, ownership routes
|
||||||
├── routes_market.py # ETF, index, crypto, currency, derivatives routes
|
├── routes_market.py # ETF, index, crypto, currency, derivatives routes
|
||||||
├── routes_macro.py # Macro economics routes (FRED)
|
│
|
||||||
├── routes_technical.py # Technical analysis routes
|
├── Dockerfile # Docker build (curl_cffi==0.7.4, safari TLS patch)
|
||||||
├── environment.yml # Conda environment
|
├── pyproject.toml # Project metadata + dependencies
|
||||||
├── pyproject.toml # Project metadata
|
|
||||||
└── tests/ # 102 tests
|
└── tests/ # 102 tests
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -315,11 +425,13 @@ Example OpenClaw workflow:
|
|||||||
2. OpenClaw calls `GET /api/v1/stock/AAPL/summary` for fundamental data
|
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
|
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
|
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)
|
5. OpenClaw calls `GET /api/v1/stock/AAPL/performance` for risk metrics (Sharpe, Sortino)
|
||||||
6. OpenClaw calls `GET /api/v1/stock/AAPL/sec-insider` for insider trading activity
|
6. OpenClaw calls `GET /api/v1/stock/AAPL/shorts/volume` for short selling activity
|
||||||
7. OpenClaw calls `GET /api/v1/macro/overview` for market context
|
7. OpenClaw calls `GET /api/v1/stock/AAPL/sec-insider` for insider trading activity
|
||||||
8. OpenClaw calls `POST /api/v1/portfolio/analyze` with user's holdings
|
8. OpenClaw calls `GET /api/v1/macro/overview` for market context
|
||||||
9. OpenClaw's LLM synthesizes all structured data into a personalized recommendation
|
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
|
## Kubernetes Deployment
|
||||||
|
|
||||||
@@ -333,7 +445,7 @@ Example OpenClaw workflow:
|
|||||||
### Architecture
|
### Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
git push → Gitea → Drone CI (kaniko) → Docker Registry → ArgoCD → K8s
|
git push -> Gitea -> Drone CI (kaniko) -> Docker Registry -> ArgoCD -> K8s
|
||||||
```
|
```
|
||||||
|
|
||||||
### Cluster Info
|
### Cluster Info
|
||||||
@@ -410,7 +522,7 @@ steps:
|
|||||||
--from-literal=INVEST_API_ALPHAVANTAGE_API_KEY=your_key
|
--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:
|
7. Verify:
|
||||||
```bash
|
```bash
|
||||||
@@ -431,10 +543,25 @@ docker run -p 8000:8000 invest-api
|
|||||||
|
|
||||||
| Source | Cost | Key Required | Data Provided |
|
| Source | Cost | Key Required | Data Provided |
|
||||||
|--------|------|-------------|---------------|
|
|--------|------|-------------|---------------|
|
||||||
| **yfinance** | Free | No | Quotes, fundamentals, financials, historical prices, news, discovery, ETF, index, crypto, forex, options, futures |
|
| **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 |
|
| **SEC** | Free | No | Insider trading (Form 4), institutional holdings (13F), company filings, N-PORT disclosures, CIK mapping, litigation releases |
|
||||||
| **Finnhub** | Free | Yes (free registration) | Insider trades, analyst recommendations, upgrades/downgrades |
|
| **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 |
|
| **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 |
|
| **FRED** | Free | Yes (free registration) | Fed rate, treasury yields, CPI, PCE, money supply, surveys, 800K+ economic series |
|
||||||
| **openbb-technical** | Free | No (local computation) | RSI, MACD, SMA, EMA, Bollinger Bands |
|
| **Federal Reserve** | Free | No | EFFR, SOFR, money measures, central bank holdings, primary dealer positions, FOMC documents |
|
||||||
| **openbb-quantitative** | Free | No (local computation) | Sharpe ratio, CAPM, normality tests, unit root tests, summary statistics |
|
| **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 []
|
||||||
29
k8s/base/drone-rbac.yaml
Normal file
29
k8s/base/drone-rbac.yaml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: drone-deploy
|
||||||
|
namespace: invest-api
|
||||||
|
rules:
|
||||||
|
- apiGroups: ["apps"]
|
||||||
|
resources: ["deployments"]
|
||||||
|
verbs: ["get", "list", "patch", "update"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: drone-deploy
|
||||||
|
namespace: invest-api
|
||||||
|
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
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
@@ -6,6 +6,8 @@ from typing import Any
|
|||||||
|
|
||||||
from openbb import obb
|
from openbb import obb
|
||||||
|
|
||||||
|
from obb_utils import to_list
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
PROVIDER = "fred"
|
PROVIDER = "fred"
|
||||||
@@ -23,32 +25,31 @@ SERIES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _to_dicts(result: Any) -> list[dict[str, Any]]:
|
async def get_series(
|
||||||
if result is None or result.results is None:
|
series_id: str, limit: int = 10, latest: bool = False,
|
||||||
return []
|
) -> list[dict[str, Any]]:
|
||||||
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]]:
|
|
||||||
"""Get a FRED time series by ID."""
|
"""Get a FRED time series by ID."""
|
||||||
try:
|
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(
|
result = await asyncio.to_thread(
|
||||||
obb.economy.fred_series,
|
obb.economy.fred_series,
|
||||||
symbol=series_id,
|
**kwargs,
|
||||||
limit=limit,
|
|
||||||
provider=PROVIDER,
|
|
||||||
)
|
)
|
||||||
items = _to_dicts(result)
|
items = to_list(result)
|
||||||
for item in items:
|
items = [
|
||||||
if "date" in item and not isinstance(item["date"], str):
|
{**item, "date": str(item["date"])}
|
||||||
item = {**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
|
return items
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning("Failed to fetch FRED series %s", series_id, exc_info=True)
|
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]:
|
async def get_macro_overview() -> dict[str, Any]:
|
||||||
"""Get a summary of key macro indicators."""
|
"""Get a summary of key macro indicators."""
|
||||||
tasks = {
|
tasks = {
|
||||||
name: get_series(series_id, limit=1)
|
name: get_series(series_id, limit=1, latest=True)
|
||||||
for name, series_id in SERIES.items()
|
for name, series_id in SERIES.items()
|
||||||
}
|
}
|
||||||
results = await asyncio.gather(*tasks.values(), return_exceptions=True)
|
results = await asyncio.gather(*tasks.values(), return_exceptions=True)
|
||||||
|
|
||||||
overview: dict[str, Any] = {}
|
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):
|
if isinstance(result, BaseException):
|
||||||
logger.warning("Failed to fetch %s: %s", name, result)
|
logger.warning("Failed to fetch %s: %s", name, result)
|
||||||
overview[name] = None
|
overview[name] = None
|
||||||
elif result and len(result) > 0:
|
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] = {
|
overview[name] = {
|
||||||
"value": entry.get("value"),
|
"value": value,
|
||||||
"date": str(entry.get("date", "")),
|
"date": str(entry.get("date", "")),
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
|
|||||||
58
main.py
58
main.py
@@ -1,27 +1,64 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
from config import settings
|
# Patch curl_cffi to use safari TLS fingerprint instead of chrome.
|
||||||
from routes import router
|
# curl_cffi's chrome impersonation triggers BoringSSL SSL_ERROR_SYSCALL on
|
||||||
from routes_sentiment import router as sentiment_router
|
# some networks; safari works reliably. This must happen before any import
|
||||||
from routes_macro import router as macro_router
|
# that creates a curl_cffi Session (yfinance, openbb).
|
||||||
from routes_technical import router as technical_router
|
import curl_cffi.requests as _cffi_requests
|
||||||
from routes_quantitative import router as quantitative_router
|
|
||||||
from routes_calendar import router as calendar_router
|
_orig_session_init = _cffi_requests.Session.__init__
|
||||||
from routes_market import router as market_router
|
|
||||||
|
|
||||||
|
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(
|
logging.basicConfig(
|
||||||
level=settings.log_level.upper(),
|
level=settings.log_level.upper(),
|
||||||
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
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(
|
app = FastAPI(
|
||||||
title="OpenBB Investment Analysis API",
|
title="OpenBB Investment Analysis API",
|
||||||
version="0.1.0",
|
version="0.1.0",
|
||||||
description="REST API for stock data and rule-based investment analysis, powered by OpenBB SDK.",
|
description="REST API for stock data and rule-based investment analysis, powered by OpenBB SDK.",
|
||||||
|
lifespan=lifespan,
|
||||||
)
|
)
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
@@ -39,6 +76,11 @@ app.include_router(technical_router)
|
|||||||
app.include_router(quantitative_router)
|
app.include_router(quantitative_router)
|
||||||
app.include_router(calendar_router)
|
app.include_router(calendar_router)
|
||||||
app.include_router(market_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])
|
@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:
|
except Exception:
|
||||||
logger.warning("Futures curve failed for %s", symbol, exc_info=True)
|
logger.warning("Futures curve failed for %s", symbol, exc_info=True)
|
||||||
return []
|
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."""
|
"""Shared OpenBB result conversion utilities."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from openbb import obb
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PROVIDER = "yfinance"
|
||||||
|
|
||||||
|
|
||||||
def to_list(result: Any) -> list[dict[str, Any]]:
|
def to_list(result: Any) -> list[dict[str, Any]]:
|
||||||
"""Convert OBBject result to list of dicts with serialized dates."""
|
"""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
|
return None
|
||||||
last = items[-1]
|
last = items[-1]
|
||||||
return last.model_dump() if hasattr(last, "model_dump") else None
|
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 asyncio
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
import yfinance as yf
|
||||||
from openbb import obb
|
from openbb import obb
|
||||||
|
|
||||||
|
from obb_utils import to_list, first_or_empty
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
PROVIDER = "yfinance"
|
PROVIDER = "yfinance"
|
||||||
|
|
||||||
|
|
||||||
def _to_dicts(result: Any) -> list[dict[str, Any]]:
|
async def get_quote(symbol: str) -> 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:
|
|
||||||
result = await asyncio.to_thread(
|
result = await asyncio.to_thread(
|
||||||
obb.equity.price.quote, symbol, provider=PROVIDER
|
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]:
|
async def get_historical(symbol: str, days: int = 365) -> list[dict[str, Any]]:
|
||||||
start = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
|
start = (datetime.now(tz=timezone.utc) - timedelta(days=days)).strftime("%Y-%m-%d")
|
||||||
result = await asyncio.to_thread(
|
result = await asyncio.to_thread(
|
||||||
obb.equity.price.historical,
|
obb.equity.price.historical,
|
||||||
symbol,
|
symbol,
|
||||||
start_date=start,
|
start_date=start,
|
||||||
provider=PROVIDER,
|
provider=PROVIDER,
|
||||||
)
|
)
|
||||||
items = _to_dicts(result)
|
items = to_list(result)
|
||||||
return [
|
return [
|
||||||
{**item, "date": str(item["date"])}
|
{**item, "date": str(item["date"])}
|
||||||
if "date" in item and not isinstance(item["date"], str)
|
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(
|
result = await asyncio.to_thread(
|
||||||
obb.equity.profile, symbol, provider=PROVIDER
|
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(
|
result = await asyncio.to_thread(
|
||||||
obb.equity.fundamental.metrics, symbol, provider=PROVIDER
|
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(
|
result = await asyncio.to_thread(
|
||||||
obb.equity.fundamental.income, symbol, provider=PROVIDER
|
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(
|
result = await asyncio.to_thread(
|
||||||
obb.equity.fundamental.balance, symbol, provider=PROVIDER
|
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(
|
result = await asyncio.to_thread(
|
||||||
obb.equity.fundamental.cash, symbol, provider=PROVIDER
|
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(
|
income, balance, cash_flow = await asyncio.gather(
|
||||||
get_income(symbol),
|
get_income(symbol),
|
||||||
get_balance(symbol),
|
get_balance(symbol),
|
||||||
@@ -104,26 +87,26 @@ async def get_financials(symbol: str) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
async def get_price_target(symbol: str) -> float | None:
|
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:
|
try:
|
||||||
result = await asyncio.to_thread(
|
return await asyncio.to_thread(_fetch)
|
||||||
obb.equity.estimates.price_target, symbol, provider=PROVIDER
|
|
||||||
)
|
|
||||||
items = _to_dicts(result)
|
|
||||||
if items:
|
|
||||||
return items[0].get("adj_price_target") or items[0].get("price_target")
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning("Failed to get price target for %s", symbol, exc_info=True)
|
logger.warning("Failed to get price target for %s", symbol, exc_info=True)
|
||||||
return None
|
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(
|
result = await asyncio.to_thread(
|
||||||
obb.news.company, symbol, provider=PROVIDER
|
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(
|
quote, profile, metrics, financials = await asyncio.gather(
|
||||||
get_quote(symbol),
|
get_quote(symbol),
|
||||||
get_profile(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(
|
result = await asyncio.to_thread(
|
||||||
obb.equity.discovery.gainers, provider=PROVIDER
|
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(
|
result = await asyncio.to_thread(
|
||||||
obb.equity.discovery.losers, provider=PROVIDER
|
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(
|
result = await asyncio.to_thread(
|
||||||
obb.equity.discovery.active, provider=PROVIDER
|
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(
|
result = await asyncio.to_thread(
|
||||||
obb.equity.discovery.undervalued_large_caps, provider=PROVIDER
|
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(
|
result = await asyncio.to_thread(
|
||||||
obb.equity.discovery.growth_tech, provider=PROVIDER
|
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]",
|
"openbb[yfinance]",
|
||||||
"pydantic-settings",
|
"pydantic-settings",
|
||||||
"httpx",
|
"httpx",
|
||||||
|
"curl_cffi==0.7.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from typing import Any
|
|||||||
|
|
||||||
from openbb import obb
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -122,3 +122,74 @@ async def get_unitroot_test(symbol: str, days: int = 365) -> dict[str, Any]:
|
|||||||
except Exception:
|
except Exception:
|
||||||
logger.warning("Unit root test failed for %s", symbol, exc_info=True)
|
logger.warning("Unit root test failed for %s", symbol, exc_info=True)
|
||||||
return {"symbol": symbol, "error": "Failed to compute unit root test"}
|
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."""
|
"""Get growth tech stocks."""
|
||||||
data = await openbb_service.get_growth()
|
data = await openbb_service.get_growth()
|
||||||
return ApiResponse(data=discover_items_from_list(data))
|
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)
|
symbol = validate_symbol(symbol)
|
||||||
data = await market_service.get_futures_curve(symbol)
|
data = await market_service.get_futures_curve(symbol)
|
||||||
return ApiResponse(data=data)
|
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)
|
symbol = validate_symbol(symbol)
|
||||||
data = await quantitative_service.get_unitroot_test(symbol, days=days)
|
data = await quantitative_service.get_unitroot_test(symbol, days=days)
|
||||||
return ApiResponse(data=data)
|
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
|
from route_utils import safe, validate_symbol
|
||||||
import alphavantage_service
|
import alphavantage_service
|
||||||
import finnhub_service
|
import finnhub_service
|
||||||
|
import openbb_service
|
||||||
|
|
||||||
import logging
|
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)
|
@router.get("/stock/{symbol}/upgrades", response_model=ApiResponse)
|
||||||
@safe
|
@safe
|
||||||
async def stock_upgrades(symbol: str = Path(..., min_length=1, max_length=20)):
|
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)
|
symbol = validate_symbol(symbol)
|
||||||
raw = await finnhub_service.get_upgrade_downgrade(symbol)
|
data = await openbb_service.get_upgrades_downgrades(symbol)
|
||||||
upgrades = [
|
return ApiResponse(data=data)
|
||||||
{
|
|
||||||
"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)
|
|
||||||
|
|||||||
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."""
|
"""Routes for technical analysis indicators."""
|
||||||
|
|
||||||
from fastapi import APIRouter, Path
|
from fastapi import APIRouter, Path, Query
|
||||||
|
|
||||||
from models import ApiResponse
|
from models import ApiResponse
|
||||||
from route_utils import safe, validate_symbol
|
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)
|
symbol = validate_symbol(symbol)
|
||||||
data = await technical_service.get_technical_indicators(symbol)
|
data = await technical_service.get_technical_indicators(symbol)
|
||||||
return ApiResponse(data=data)
|
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
|
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(
|
async def get_technical_indicators(
|
||||||
symbol: str, days: int = 400
|
symbol: str, days: int = 400
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Compute key technical indicators for a symbol."""
|
"""Compute key technical indicators for a symbol."""
|
||||||
from datetime import datetime, timedelta
|
hist = await fetch_historical(symbol, days)
|
||||||
|
if hist is None:
|
||||||
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:
|
|
||||||
return {"symbol": symbol, "error": "No historical data available"}
|
return {"symbol": symbol, "error": "No historical data available"}
|
||||||
|
|
||||||
result: dict[str, Any] = {"symbol": symbol}
|
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)")
|
signals.append("Death cross: SMA50 below SMA200 (bearish trend)")
|
||||||
|
|
||||||
return signals
|
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:
|
class MockModel:
|
||||||
@@ -14,34 +14,34 @@ class MockOBBject:
|
|||||||
self.results = results
|
self.results = results
|
||||||
|
|
||||||
|
|
||||||
class TestToDicts:
|
class TestToList:
|
||||||
def test_none_result(self):
|
def test_none_result(self):
|
||||||
assert _to_dicts(None) == []
|
assert to_list(None) == []
|
||||||
|
|
||||||
def test_none_results(self):
|
def test_none_results(self):
|
||||||
obj = MockOBBject(results=None)
|
obj = MockOBBject(results=None)
|
||||||
assert _to_dicts(obj) == []
|
assert to_list(obj) == []
|
||||||
|
|
||||||
def test_list_results(self):
|
def test_list_results(self):
|
||||||
obj = MockOBBject(results=[
|
obj = MockOBBject(results=[
|
||||||
MockModel({"a": 1}),
|
MockModel({"a": 1}),
|
||||||
MockModel({"b": 2}),
|
MockModel({"b": 2}),
|
||||||
])
|
])
|
||||||
result = _to_dicts(obj)
|
result = to_list(obj)
|
||||||
assert len(result) == 2
|
assert len(result) == 2
|
||||||
assert result[0] == {"a": 1}
|
assert result[0] == {"a": 1}
|
||||||
|
|
||||||
def test_single_result(self):
|
def test_single_result(self):
|
||||||
obj = MockOBBject(results=MockModel({"x": 42}))
|
obj = MockOBBject(results=MockModel({"x": 42}))
|
||||||
result = _to_dicts(obj)
|
result = to_list(obj)
|
||||||
assert result == [{"x": 42}]
|
assert result == [{"x": 42}]
|
||||||
|
|
||||||
|
|
||||||
class TestFirstOrEmpty:
|
class TestFirstOrEmpty:
|
||||||
def test_empty(self):
|
def test_empty(self):
|
||||||
assert _first_or_empty(None) == {}
|
assert first_or_empty(None) == {}
|
||||||
|
|
||||||
def test_with_data(self):
|
def test_with_data(self):
|
||||||
obj = MockOBBject(results=[MockModel({"price": 150.0})])
|
obj = MockOBBject(results=[MockModel({"price": 150.0})])
|
||||||
result = _first_or_empty(obj)
|
result = first_or_empty(obj)
|
||||||
assert result == {"price": 150.0}
|
assert result == {"price": 150.0}
|
||||||
|
|||||||
@@ -66,10 +66,12 @@ async def test_stock_recommendations(mock_recs, client):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@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):
|
async def test_stock_upgrades(mock_upgrades, client):
|
||||||
mock_upgrades.return_value = [
|
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")
|
resp = await client.get("/api/v1/stock/AAPL/upgrades")
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
|
|||||||
Reference in New Issue
Block a user