feat: add 67 new endpoints across 10 feature groups
Prerequisite refactor: - Consolidate duplicate _to_dicts into shared obb_utils.to_list - Add fetch_historical and first_or_empty helpers to obb_utils Phase 1 - Local computation (no provider risk): - Group I: 12 technical indicators (ATR, ADX, Stoch, OBV, Ichimoku, Donchian, Aroon, CCI, Keltner, Fibonacci, A/D, Volatility Cones) - Group J: Sortino, Omega ratios + rolling stats (variance, stdev, mean, skew, kurtosis, quantile via generic endpoint) - Group H: ECB currency reference rates Phase 2 - FRED/Federal Reserve providers: - Group C: 10 fixed income endpoints (treasury rates, yield curve, auctions, TIPS, EFFR, SOFR, HQM, commercial paper, spot rates, spreads) - Group D: 11 economy endpoints (CPI, GDP, unemployment, PCE, money measures, CLI, HPI, FRED search, balance of payments, Fed holdings, FOMC documents) - Group E: 5 survey endpoints (Michigan, SLOOS, NFP, Empire State, BLS search) Phase 3 - SEC/stockgrid/FINRA providers: - Group B: 4 equity fundamental endpoints (management, dividends, SEC filings, company search) - Group A: 4 shorts/dark pool endpoints (short volume, FTD, short interest, OTC dark pool) - Group F: 3 index/ETF enhanced (S&P 500 multiples, index constituents, ETF N-PORT) Phase 4 - Regulators: - Group G: 5 regulatory endpoints (COT report, COT search, SEC litigation, institution search, CIK mapping) Security hardening: - Service-layer allowlists for all getattr dynamic dispatch - Regex validation on date, country, security_type, form_type params - Exception handling in fetch_historical - Callable guard on rolling stat dispatch Total: 32 existing + 67 new = 99 endpoints, all free providers.
This commit is contained in:
156
economy_service.py
Normal file
156
economy_service.py
Normal file
@@ -0,0 +1,156 @@
|
||||
"""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_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:
|
||||
kwargs["year"] = year
|
||||
result = await asyncio.to_thread(
|
||||
obb.economy.fomc_documents, **kwargs
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("FOMC documents failed", exc_info=True)
|
||||
return []
|
||||
29
findings.md
Normal file
29
findings.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Research Findings
|
||||
|
||||
## Architecture Analysis (2026-03-19)
|
||||
|
||||
### Current Codebase
|
||||
- 22 Python files, flat layout, all under 250 lines
|
||||
- Pattern: service file (async OpenBB wrapper) + route file (FastAPI router with @safe decorator)
|
||||
- Shared utils: obb_utils.py, route_utils.py, mappers.py, models.py
|
||||
|
||||
### Technical Debt
|
||||
1. Duplicated `_to_dicts` in openbb_service.py and macro_service.py (same as obb_utils.to_list)
|
||||
2. calendar_service.py has scope creep (ownership, screening mixed with calendar events)
|
||||
3. No shared `fetch_historical` helper (duplicated in technical_service.py and quantitative_service.py)
|
||||
|
||||
### Provider Availability (Verified)
|
||||
- **No API key needed:** yfinance, stockgrid, finra, multpl, cftc, government_us, sec, ecb, cboe
|
||||
- **Already configured:** fred, finnhub, alphavantage
|
||||
- **Not needed:** fmp (removed), intrinio, tiingo, benzinga
|
||||
|
||||
### Key Design Decisions
|
||||
- Keep flat file layout (avoid breaking all imports for ~40 files)
|
||||
- Domain-prefixed naming for new files
|
||||
- Generic technical indicator dispatcher pattern for 14 new indicators
|
||||
- Consolidate _to_dicts before adding new services
|
||||
|
||||
### OpenBB Features Discovered
|
||||
- 67 new endpoints across 10 groups (A-J)
|
||||
- 3 Small, 4 Medium, 3 Large complexity groups
|
||||
- All use free providers (no new API keys required)
|
||||
143
fixed_income_service.py
Normal file
143
fixed_income_service.py
Normal file
@@ -0,0 +1,143 @@
|
||||
"""Fixed income data: treasury rates, yield curve, auctions, corporate bonds."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from openbb import obb
|
||||
|
||||
from obb_utils import to_list
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def get_treasury_rates() -> list[dict[str, Any]]:
|
||||
"""Get full treasury yield curve rates (4W-30Y)."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.fixedincome.government.treasury_rates, provider="federal_reserve"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Treasury rates failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_yield_curve(date: str | None = None) -> list[dict[str, Any]]:
|
||||
"""Get yield curve with maturity/rate pairs."""
|
||||
try:
|
||||
kwargs: dict[str, Any] = {"provider": "federal_reserve"}
|
||||
if date:
|
||||
kwargs["date"] = date
|
||||
result = await asyncio.to_thread(
|
||||
obb.fixedincome.government.yield_curve, **kwargs
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Yield curve failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_treasury_auctions(security_type: str | None = None) -> list[dict[str, Any]]:
|
||||
"""Get treasury auction data (bid-to-cover, yields)."""
|
||||
try:
|
||||
kwargs: dict[str, Any] = {"provider": "government_us"}
|
||||
if security_type:
|
||||
kwargs["security_type"] = security_type
|
||||
result = await asyncio.to_thread(
|
||||
obb.fixedincome.government.treasury_auctions, **kwargs
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Treasury auctions failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_tips_yields() -> list[dict[str, Any]]:
|
||||
"""Get TIPS real yields by maturity."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.fixedincome.government.tips_yields, provider="fred"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("TIPS yields failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_effr() -> list[dict[str, Any]]:
|
||||
"""Get Effective Federal Funds Rate with percentiles."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.fixedincome.rate.effr, provider="federal_reserve"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("EFFR failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_sofr() -> list[dict[str, Any]]:
|
||||
"""Get SOFR rate with moving averages."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.fixedincome.rate.sofr, provider="federal_reserve"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("SOFR failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_hqm() -> list[dict[str, Any]]:
|
||||
"""Get High Quality Market corporate bond yields."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.fixedincome.corporate.hqm, provider="fred"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("HQM failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_commercial_paper() -> list[dict[str, Any]]:
|
||||
"""Get commercial paper rates by maturity and type."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.fixedincome.corporate.commercial_paper, provider="fred"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Commercial paper failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_spot_rates() -> list[dict[str, Any]]:
|
||||
"""Get corporate bond spot rates."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.fixedincome.corporate.spot_rates, provider="fred"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Spot rates failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
_VALID_SPREAD_SERIES = {"tcm", "tcm_effr", "treasury_effr"}
|
||||
|
||||
|
||||
async def get_spreads(series: str = "tcm") -> list[dict[str, Any]]:
|
||||
"""Get treasury/corporate spreads (tcm, tcm_effr, treasury_effr)."""
|
||||
if series not in _VALID_SPREAD_SERIES:
|
||||
return []
|
||||
try:
|
||||
fn = getattr(obb.fixedincome.spreads, series, None)
|
||||
if fn is None:
|
||||
return []
|
||||
result = await asyncio.to_thread(fn, provider="fred")
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Spreads %s failed", series, exc_info=True)
|
||||
return []
|
||||
@@ -6,6 +6,8 @@ from typing import Any
|
||||
|
||||
from openbb import obb
|
||||
|
||||
from obb_utils import to_list
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PROVIDER = "fred"
|
||||
@@ -23,19 +25,6 @@ SERIES = {
|
||||
}
|
||||
|
||||
|
||||
def _to_dicts(result: Any) -> list[dict[str, Any]]:
|
||||
if result is None or result.results is None:
|
||||
return []
|
||||
if isinstance(result.results, list):
|
||||
return [
|
||||
item.model_dump() if hasattr(item, "model_dump") else vars(item)
|
||||
for item in result.results
|
||||
]
|
||||
if hasattr(result.results, "model_dump"):
|
||||
return [result.results.model_dump()]
|
||||
return [vars(result.results)]
|
||||
|
||||
|
||||
async def get_series(
|
||||
series_id: str, limit: int = 10, latest: bool = False,
|
||||
) -> list[dict[str, Any]]:
|
||||
@@ -52,7 +41,7 @@ async def get_series(
|
||||
obb.economy.fred_series,
|
||||
**kwargs,
|
||||
)
|
||||
items = _to_dicts(result)
|
||||
items = to_list(result)
|
||||
items = [
|
||||
{**item, "date": str(item["date"])}
|
||||
if "date" in item and not isinstance(item["date"], str)
|
||||
|
||||
10
main.py
10
main.py
@@ -32,6 +32,11 @@ from routes_technical import router as technical_router
|
||||
from routes_quantitative import router as quantitative_router
|
||||
from routes_calendar import router as calendar_router
|
||||
from routes_market import router as market_router
|
||||
from routes_shorts import router as shorts_router
|
||||
from routes_fixed_income import router as fixed_income_router
|
||||
from routes_economy import router as economy_router
|
||||
from routes_surveys import router as surveys_router
|
||||
from routes_regulators import router as regulators_router
|
||||
|
||||
logging.basicConfig(
|
||||
level=settings.log_level.upper(),
|
||||
@@ -59,6 +64,11 @@ app.include_router(technical_router)
|
||||
app.include_router(quantitative_router)
|
||||
app.include_router(calendar_router)
|
||||
app.include_router(market_router)
|
||||
app.include_router(shorts_router)
|
||||
app.include_router(fixed_income_router)
|
||||
app.include_router(economy_router)
|
||||
app.include_router(surveys_router)
|
||||
app.include_router(regulators_router)
|
||||
|
||||
|
||||
@app.get("/health", response_model=dict[str, str])
|
||||
|
||||
@@ -161,3 +161,57 @@ async def get_futures_curve(symbol: str) -> list[dict[str, Any]]:
|
||||
except Exception:
|
||||
logger.warning("Futures curve failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
# --- Currency Reference Rates (Group H) ---
|
||||
|
||||
|
||||
async def get_currency_reference_rates() -> list[dict[str, Any]]:
|
||||
"""Get ECB reference exchange rates for major currencies."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.currency.reference_rates, provider="ecb"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Currency reference rates failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
# --- Index Enhanced (Group F) ---
|
||||
|
||||
|
||||
async def get_sp500_multiples(series_name: str = "pe_ratio") -> list[dict[str, Any]]:
|
||||
"""Get historical S&P 500 valuation multiples (PE, Shiller PE, P/B, etc.)."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.index.sp500_multiples, series_name=series_name, provider="multpl"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("SP500 multiples failed for %s", series_name, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_index_constituents(symbol: str) -> list[dict[str, Any]]:
|
||||
"""Get index member stocks with sector and price data."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.index.constituents, symbol, provider="cboe"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Index constituents failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_etf_nport(symbol: str) -> list[dict[str, Any]]:
|
||||
"""Get detailed ETF holdings from SEC N-PORT filings."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.etf.nport_disclosure, symbol, provider="sec"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("ETF N-PORT failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
|
||||
32
obb_utils.py
32
obb_utils.py
@@ -1,7 +1,16 @@
|
||||
"""Shared OpenBB result conversion utilities."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
|
||||
from openbb import obb
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PROVIDER = "yfinance"
|
||||
|
||||
|
||||
def to_list(result: Any) -> list[dict[str, Any]]:
|
||||
"""Convert OBBject result to list of dicts with serialized dates."""
|
||||
@@ -49,3 +58,26 @@ def safe_last(result: Any) -> dict[str, Any] | None:
|
||||
return None
|
||||
last = items[-1]
|
||||
return last.model_dump() if hasattr(last, "model_dump") else None
|
||||
|
||||
|
||||
def first_or_empty(result: Any) -> dict[str, Any]:
|
||||
"""Get first result as dict, or empty dict."""
|
||||
items = to_list(result)
|
||||
return items[0] if items else {}
|
||||
|
||||
|
||||
async def fetch_historical(
|
||||
symbol: str, days: int = 365, provider: str = PROVIDER,
|
||||
) -> Any | None:
|
||||
"""Fetch historical price data, returning the OBBject result or None."""
|
||||
start = (datetime.now(tz=timezone.utc) - timedelta(days=days)).strftime("%Y-%m-%d")
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.price.historical, symbol, start_date=start, provider=provider,
|
||||
)
|
||||
except Exception:
|
||||
logger.warning("Historical fetch failed for %s", symbol, exc_info=True)
|
||||
return None
|
||||
if result is None or result.results is None:
|
||||
return None
|
||||
return result
|
||||
|
||||
@@ -5,36 +5,18 @@ from typing import Any
|
||||
|
||||
from openbb import obb
|
||||
|
||||
from obb_utils import to_list, first_or_empty
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PROVIDER = "yfinance"
|
||||
|
||||
|
||||
def _to_dicts(result: Any) -> list[dict[str, Any]]:
|
||||
"""Convert OBBject results to list of dicts."""
|
||||
if result is None or result.results is None:
|
||||
return []
|
||||
if isinstance(result.results, list):
|
||||
return [
|
||||
item.model_dump() if hasattr(item, "model_dump") else vars(item)
|
||||
for item in result.results
|
||||
]
|
||||
if hasattr(result.results, "model_dump"):
|
||||
return [result.results.model_dump()]
|
||||
return [vars(result.results)]
|
||||
|
||||
|
||||
def _first_or_empty(result: Any) -> dict[str, Any]:
|
||||
"""Get first result as dict, or empty dict."""
|
||||
items = _to_dicts(result)
|
||||
return items[0] if items else {}
|
||||
|
||||
|
||||
async def get_quote(symbol: str) -> dict:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.price.quote, symbol, provider=PROVIDER
|
||||
)
|
||||
return _first_or_empty(result)
|
||||
return first_or_empty(result)
|
||||
|
||||
|
||||
async def get_historical(symbol: str, days: int = 365) -> list[dict]:
|
||||
@@ -45,7 +27,7 @@ async def get_historical(symbol: str, days: int = 365) -> list[dict]:
|
||||
start_date=start,
|
||||
provider=PROVIDER,
|
||||
)
|
||||
items = _to_dicts(result)
|
||||
items = to_list(result)
|
||||
return [
|
||||
{**item, "date": str(item["date"])}
|
||||
if "date" in item and not isinstance(item["date"], str)
|
||||
@@ -58,35 +40,35 @@ async def get_profile(symbol: str) -> dict:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.profile, symbol, provider=PROVIDER
|
||||
)
|
||||
return _first_or_empty(result)
|
||||
return first_or_empty(result)
|
||||
|
||||
|
||||
async def get_metrics(symbol: str) -> dict:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.fundamental.metrics, symbol, provider=PROVIDER
|
||||
)
|
||||
return _first_or_empty(result)
|
||||
return first_or_empty(result)
|
||||
|
||||
|
||||
async def get_income(symbol: str) -> list[dict]:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.fundamental.income, symbol, provider=PROVIDER
|
||||
)
|
||||
return _to_dicts(result)
|
||||
return to_list(result)
|
||||
|
||||
|
||||
async def get_balance(symbol: str) -> list[dict]:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.fundamental.balance, symbol, provider=PROVIDER
|
||||
)
|
||||
return _to_dicts(result)
|
||||
return to_list(result)
|
||||
|
||||
|
||||
async def get_cash_flow(symbol: str) -> list[dict]:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.fundamental.cash, symbol, provider=PROVIDER
|
||||
)
|
||||
return _to_dicts(result)
|
||||
return to_list(result)
|
||||
|
||||
|
||||
async def get_financials(symbol: str) -> dict:
|
||||
@@ -122,7 +104,7 @@ async def get_news(symbol: str) -> list[dict]:
|
||||
result = await asyncio.to_thread(
|
||||
obb.news.company, symbol, provider=PROVIDER
|
||||
)
|
||||
return _to_dicts(result)
|
||||
return to_list(result)
|
||||
|
||||
|
||||
async def get_summary(symbol: str) -> dict:
|
||||
@@ -144,35 +126,35 @@ async def get_gainers() -> list[dict]:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.discovery.gainers, provider=PROVIDER
|
||||
)
|
||||
return _to_dicts(result)
|
||||
return to_list(result)
|
||||
|
||||
|
||||
async def get_losers() -> list[dict]:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.discovery.losers, provider=PROVIDER
|
||||
)
|
||||
return _to_dicts(result)
|
||||
return to_list(result)
|
||||
|
||||
|
||||
async def get_active() -> list[dict]:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.discovery.active, provider=PROVIDER
|
||||
)
|
||||
return _to_dicts(result)
|
||||
return to_list(result)
|
||||
|
||||
|
||||
async def get_undervalued() -> list[dict]:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.discovery.undervalued_large_caps, provider=PROVIDER
|
||||
)
|
||||
return _to_dicts(result)
|
||||
return to_list(result)
|
||||
|
||||
|
||||
async def get_growth() -> list[dict]:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.discovery.growth_tech, provider=PROVIDER
|
||||
)
|
||||
return _to_dicts(result)
|
||||
return to_list(result)
|
||||
|
||||
|
||||
async def get_upgrades_downgrades(symbol: str, limit: int = 20) -> list[dict]:
|
||||
@@ -200,3 +182,37 @@ async def get_upgrades_downgrades(symbol: str, limit: int = 20) -> list[dict]:
|
||||
]
|
||||
|
||||
return await asyncio.to_thread(_fetch)
|
||||
|
||||
|
||||
# --- Equity Fundamentals Extended (Group B) ---
|
||||
|
||||
|
||||
async def get_management(symbol: str) -> list[dict]:
|
||||
"""Get executive team info (name, title, compensation)."""
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.fundamental.management, symbol, provider=PROVIDER
|
||||
)
|
||||
return to_list(result)
|
||||
|
||||
|
||||
async def get_dividends(symbol: str) -> list[dict]:
|
||||
"""Get historical dividend records."""
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.fundamental.dividends, symbol, provider=PROVIDER
|
||||
)
|
||||
return to_list(result)
|
||||
|
||||
|
||||
async def get_filings(symbol: str, form_type: str | None = None) -> list[dict]:
|
||||
"""Get SEC filings (10-K, 10-Q, 8-K, etc.)."""
|
||||
kwargs: dict[str, Any] = {"symbol": symbol, "provider": "sec"}
|
||||
if form_type:
|
||||
kwargs["type"] = form_type
|
||||
result = await asyncio.to_thread(obb.equity.fundamental.filings, **kwargs)
|
||||
return to_list(result)
|
||||
|
||||
|
||||
async def search_company(query: str) -> list[dict]:
|
||||
"""Search for companies by name."""
|
||||
result = await asyncio.to_thread(obb.equity.search, query, provider="sec")
|
||||
return to_list(result)
|
||||
|
||||
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
|
||||
@@ -7,7 +7,7 @@ from typing import Any
|
||||
|
||||
from openbb import obb
|
||||
|
||||
from obb_utils import extract_single, safe_last
|
||||
from obb_utils import extract_single, safe_last, fetch_historical, to_list
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -122,3 +122,74 @@ async def get_unitroot_test(symbol: str, days: int = 365) -> dict[str, Any]:
|
||||
except Exception:
|
||||
logger.warning("Unit root test failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute unit root test"}
|
||||
|
||||
|
||||
# --- Extended Quantitative (Phase 1, Group J) ---
|
||||
|
||||
|
||||
async def get_sortino(symbol: str, days: int = 365) -> dict[str, Any]:
|
||||
"""Sortino ratio -- risk-adjusted return penalizing only downside deviation."""
|
||||
fetch_days = max(days, PERF_DAYS)
|
||||
hist = await fetch_historical(symbol, fetch_days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.quantitative.performance.sortino_ratio,
|
||||
data=hist.results, target=TARGET,
|
||||
)
|
||||
return {"symbol": symbol, "period_days": days, "sortino": safe_last(result)}
|
||||
except Exception:
|
||||
logger.warning("Sortino failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute Sortino ratio"}
|
||||
|
||||
|
||||
async def get_omega(symbol: str, days: int = 365) -> dict[str, Any]:
|
||||
"""Omega ratio -- probability-weighted gain vs loss ratio."""
|
||||
fetch_days = max(days, PERF_DAYS)
|
||||
hist = await fetch_historical(symbol, fetch_days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.quantitative.performance.omega_ratio,
|
||||
data=hist.results, target=TARGET,
|
||||
)
|
||||
return {"symbol": symbol, "period_days": days, "omega": safe_last(result)}
|
||||
except Exception:
|
||||
logger.warning("Omega failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute Omega ratio"}
|
||||
|
||||
|
||||
async def get_rolling_stat(
|
||||
symbol: str, stat: str, days: int = 365, window: int = 30,
|
||||
) -> dict[str, Any]:
|
||||
"""Compute a rolling statistic (variance, stdev, mean, skew, kurtosis, quantile)."""
|
||||
valid_stats = {"variance", "stdev", "mean", "skew", "kurtosis", "quantile"}
|
||||
if stat not in valid_stats:
|
||||
return {"symbol": symbol, "error": f"Invalid stat: {stat}. Use: {', '.join(sorted(valid_stats))}"}
|
||||
|
||||
fetch_days = max(days, PERF_DAYS)
|
||||
hist = await fetch_historical(symbol, fetch_days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
fn = getattr(obb.quantitative.rolling, stat, None)
|
||||
if fn is None or not callable(fn):
|
||||
return {"symbol": symbol, "error": f"Stat '{stat}' not available"}
|
||||
result = await asyncio.to_thread(
|
||||
fn, data=hist.results, target=TARGET, window=window,
|
||||
)
|
||||
items = to_list(result)
|
||||
# Return last N items matching the requested window
|
||||
tail = items[-window:] if len(items) > window else items
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"stat": stat,
|
||||
"window": window,
|
||||
"period_days": days,
|
||||
"data": tail,
|
||||
}
|
||||
except Exception:
|
||||
logger.warning("Rolling %s failed for %s", stat, symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": f"Failed to compute rolling {stat}"}
|
||||
|
||||
71
regulators_service.py
Normal file
71
regulators_service.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""Regulatory data: CFTC COT reports, SEC litigation, institutional data."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from openbb import obb
|
||||
|
||||
from obb_utils import to_list
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def get_cot(symbol: str) -> list[dict[str, Any]]:
|
||||
"""Get Commitment of Traders report for a futures symbol."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.regulators.cftc.cot, symbol, provider="cftc"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("COT failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def cot_search(query: str) -> list[dict[str, Any]]:
|
||||
"""Search COT report symbols."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.regulators.cftc.cot_search, query, provider="cftc"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("COT search failed for %s", query, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_sec_litigation() -> list[dict[str, Any]]:
|
||||
"""Get SEC litigation releases."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.regulators.sec.rss_litigation, provider="sec"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("SEC litigation failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def search_institutions(query: str) -> list[dict[str, Any]]:
|
||||
"""Search for institutional investors filing with SEC."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.regulators.sec.institutions_search, query, provider="sec"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Institution search failed for %s", query, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_cik_map(symbol: str) -> list[dict[str, Any]]:
|
||||
"""Map ticker symbol to CIK number."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.regulators.sec.cik_map, symbol, provider="sec"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("CIK map failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
41
routes.py
41
routes.py
@@ -173,3 +173,44 @@ async def discover_growth():
|
||||
"""Get growth tech stocks."""
|
||||
data = await openbb_service.get_growth()
|
||||
return ApiResponse(data=discover_items_from_list(data))
|
||||
|
||||
|
||||
# --- Equity Fundamentals Extended (Group B) ---
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/management", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_management(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Get executive team: name, title, compensation."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await openbb_service.get_management(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/dividends", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_dividends(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Get historical dividend records."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await openbb_service.get_dividends(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/filings", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_filings(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
form_type: str = Query(default=None, max_length=20, pattern=r"^[A-Za-z0-9/-]+$"),
|
||||
):
|
||||
"""Get SEC filings (10-K, 10-Q, 8-K, etc.)."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await openbb_service.get_filings(symbol, form_type=form_type)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/search", response_model=ApiResponse)
|
||||
@safe
|
||||
async def company_search(query: str = Query(..., min_length=1, max_length=100)):
|
||||
"""Search for companies by name (SEC/NASDAQ)."""
|
||||
data = await openbb_service.search_company(query)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
107
routes_economy.py
Normal file
107
routes_economy.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""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(
|
||||
type: str = Query(default="real", pattern="^(nominal|real|forecast)$"),
|
||||
):
|
||||
"""GDP: nominal, real, or forecast."""
|
||||
data = await economy_service.get_gdp(gdp_type=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-search", response_model=ApiResponse)
|
||||
@safe
|
||||
async def economy_fred_search(query: str = Query(..., min_length=1, max_length=100)):
|
||||
"""Search FRED series by keyword (800K+ economic series)."""
|
||||
data = await economy_service.fred_search(query=query)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/economy/balance-of-payments", response_model=ApiResponse)
|
||||
@safe
|
||||
async def economy_bop():
|
||||
"""Balance of payments: current/capital/financial account."""
|
||||
data = await economy_service.get_balance_of_payments()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/economy/central-bank-holdings", response_model=ApiResponse)
|
||||
@safe
|
||||
async def economy_fed_holdings():
|
||||
"""Fed SOMA portfolio: holdings by security type."""
|
||||
data = await economy_service.get_central_bank_holdings()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/economy/fomc-documents", response_model=ApiResponse)
|
||||
@safe
|
||||
async def economy_fomc(year: int = Query(default=None, ge=2000, le=2099)):
|
||||
"""FOMC meeting documents: minutes, projections, press conferences."""
|
||||
data = await economy_service.get_fomc_documents(year=year)
|
||||
return ApiResponse(data=data)
|
||||
93
routes_fixed_income.py
Normal file
93
routes_fixed_income.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""Routes for fixed income data."""
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
from models import ApiResponse
|
||||
from route_utils import safe
|
||||
import fixed_income_service
|
||||
|
||||
router = APIRouter(prefix="/api/v1/fixed-income")
|
||||
|
||||
|
||||
@router.get("/treasury-rates", response_model=ApiResponse)
|
||||
@safe
|
||||
async def treasury_rates():
|
||||
"""Full treasury yield curve rates (4W-30Y)."""
|
||||
data = await fixed_income_service.get_treasury_rates()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/yield-curve", response_model=ApiResponse)
|
||||
@safe
|
||||
async def yield_curve(date: str = Query(default=None, max_length=10, pattern=r"^\d{4}-\d{2}-\d{2}$")):
|
||||
"""Yield curve with maturity/rate pairs."""
|
||||
data = await fixed_income_service.get_yield_curve(date=date)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/treasury-auctions", response_model=ApiResponse)
|
||||
@safe
|
||||
async def treasury_auctions(
|
||||
security_type: str = Query(default=None, max_length=30, pattern=r"^[a-zA-Z_ -]+$"),
|
||||
):
|
||||
"""Treasury auction data: bid-to-cover ratios, yields."""
|
||||
data = await fixed_income_service.get_treasury_auctions(security_type=security_type)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/tips-yields", response_model=ApiResponse)
|
||||
@safe
|
||||
async def tips_yields():
|
||||
"""TIPS real yields by maturity."""
|
||||
data = await fixed_income_service.get_tips_yields()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/effr", response_model=ApiResponse)
|
||||
@safe
|
||||
async def effr():
|
||||
"""Effective Federal Funds Rate with percentiles and volume."""
|
||||
data = await fixed_income_service.get_effr()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/sofr", response_model=ApiResponse)
|
||||
@safe
|
||||
async def sofr():
|
||||
"""SOFR rate with 30/90/180-day moving averages."""
|
||||
data = await fixed_income_service.get_sofr()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/hqm", response_model=ApiResponse)
|
||||
@safe
|
||||
async def hqm():
|
||||
"""High Quality Market corporate bond yields (AAA/AA/A)."""
|
||||
data = await fixed_income_service.get_hqm()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/commercial-paper", response_model=ApiResponse)
|
||||
@safe
|
||||
async def commercial_paper():
|
||||
"""Commercial paper rates by maturity and type."""
|
||||
data = await fixed_income_service.get_commercial_paper()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/spot-rates", response_model=ApiResponse)
|
||||
@safe
|
||||
async def spot_rates():
|
||||
"""Corporate bond spot rates and par yields."""
|
||||
data = await fixed_income_service.get_spot_rates()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/spreads", response_model=ApiResponse)
|
||||
@safe
|
||||
async def spreads(
|
||||
series: str = Query(default="tcm", pattern="^(tcm|tcm_effr|treasury_effr)$"),
|
||||
):
|
||||
"""Treasury/corporate spreads (tcm, tcm_effr, treasury_effr)."""
|
||||
data = await fixed_income_service.get_spreads(series=series)
|
||||
return ApiResponse(data=data)
|
||||
@@ -135,3 +135,45 @@ async def futures_curve(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await market_service.get_futures_curve(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
# --- Currency Reference Rates (Group H) ---
|
||||
|
||||
|
||||
@router.get("/currency/reference-rates", response_model=ApiResponse)
|
||||
@safe
|
||||
async def currency_reference_rates():
|
||||
"""Get ECB reference exchange rates for 28 major currencies."""
|
||||
data = await market_service.get_currency_reference_rates()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
# --- Index Enhanced (Group F) ---
|
||||
|
||||
|
||||
@router.get("/index/sp500-multiples", response_model=ApiResponse)
|
||||
@safe
|
||||
async def sp500_multiples(
|
||||
series: str = Query(default="pe_ratio", pattern="^[a-z_]+$"),
|
||||
):
|
||||
"""Historical S&P 500 valuation: pe_ratio, shiller_pe_ratio, dividend_yield, etc."""
|
||||
data = await market_service.get_sp500_multiples(series)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/index/{symbol}/constituents", response_model=ApiResponse)
|
||||
@safe
|
||||
async def index_constituents(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Get index member stocks with sector and price data."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await market_service.get_index_constituents(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/etf/{symbol}/nport", response_model=ApiResponse)
|
||||
@safe
|
||||
async def etf_nport(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Detailed ETF holdings from SEC N-PORT filings."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await market_service.get_etf_nport(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
@@ -52,3 +52,44 @@ async def stock_unitroot(
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await quantitative_service.get_unitroot_test(symbol, days=days)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
# --- Extended Quantitative (Group J) ---
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/sortino", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_sortino(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
days: int = Query(default=365, ge=30, le=3650),
|
||||
):
|
||||
"""Sortino ratio -- risk-adjusted return penalizing only downside deviation."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await quantitative_service.get_sortino(symbol, days=days)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/omega", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_omega(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
days: int = Query(default=365, ge=30, le=3650),
|
||||
):
|
||||
"""Omega ratio -- probability-weighted gain vs loss."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await quantitative_service.get_omega(symbol, days=days)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/rolling/{stat}", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_rolling(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
stat: str = Path(..., pattern="^(variance|stdev|mean|skew|kurtosis|quantile)$"),
|
||||
days: int = Query(default=365, ge=30, le=3650),
|
||||
window: int = Query(default=30, ge=5, le=252),
|
||||
):
|
||||
"""Rolling statistics: variance, stdev, mean, skew, kurtosis, quantile."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await quantitative_service.get_rolling_stat(symbol, stat=stat, days=days, window=window)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
51
routes_regulators.py
Normal file
51
routes_regulators.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""Routes for regulatory data (CFTC, SEC)."""
|
||||
|
||||
from fastapi import APIRouter, Path, Query
|
||||
|
||||
from models import ApiResponse
|
||||
from route_utils import safe, validate_symbol
|
||||
import regulators_service
|
||||
|
||||
router = APIRouter(prefix="/api/v1/regulators")
|
||||
|
||||
|
||||
@router.get("/cot", response_model=ApiResponse)
|
||||
@safe
|
||||
async def cot_report(symbol: str = Query(..., min_length=1, max_length=20)):
|
||||
"""Commitment of Traders: commercial/speculator positions for futures."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await regulators_service.get_cot(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/cot/search", response_model=ApiResponse)
|
||||
@safe
|
||||
async def cot_search(query: str = Query(..., min_length=1, max_length=100)):
|
||||
"""Search COT report symbols."""
|
||||
data = await regulators_service.cot_search(query)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/sec/litigation", response_model=ApiResponse)
|
||||
@safe
|
||||
async def sec_litigation():
|
||||
"""SEC litigation releases RSS feed."""
|
||||
data = await regulators_service.get_sec_litigation()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/sec/institutions", response_model=ApiResponse)
|
||||
@safe
|
||||
async def sec_institutions(query: str = Query(..., min_length=1, max_length=100)):
|
||||
"""Search institutional investors filing with SEC."""
|
||||
data = await regulators_service.search_institutions(query)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/sec/cik-map/{symbol}", response_model=ApiResponse)
|
||||
@safe
|
||||
async def sec_cik_map(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Map ticker symbol to SEC CIK number."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await regulators_service.get_cik_map(symbol)
|
||||
return ApiResponse(data=data)
|
||||
45
routes_shorts.py
Normal file
45
routes_shorts.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""Routes for equity shorts and dark pool data."""
|
||||
|
||||
from fastapi import APIRouter, Path
|
||||
|
||||
from models import ApiResponse
|
||||
from route_utils import safe, validate_symbol
|
||||
import shorts_service
|
||||
|
||||
router = APIRouter(prefix="/api/v1")
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/shorts/volume", response_model=ApiResponse)
|
||||
@safe
|
||||
async def short_volume(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Daily short volume and percent (stockgrid)."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await shorts_service.get_short_volume(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/shorts/ftd", response_model=ApiResponse)
|
||||
@safe
|
||||
async def fails_to_deliver(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Fails-to-deliver records from SEC."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await shorts_service.get_fails_to_deliver(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/shorts/interest", response_model=ApiResponse)
|
||||
@safe
|
||||
async def short_interest(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Short interest positions, days to cover (FINRA)."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await shorts_service.get_short_interest(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/darkpool/{symbol}/otc", response_model=ApiResponse)
|
||||
@safe
|
||||
async def darkpool_otc(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""OTC/dark pool aggregate trade volume (FINRA)."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await shorts_service.get_darkpool_otc(symbol)
|
||||
return ApiResponse(data=data)
|
||||
49
routes_surveys.py
Normal file
49
routes_surveys.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""Routes for economy surveys."""
|
||||
|
||||
from fastapi import APIRouter, Query
|
||||
|
||||
from models import ApiResponse
|
||||
from route_utils import safe
|
||||
import surveys_service
|
||||
|
||||
router = APIRouter(prefix="/api/v1/economy/surveys")
|
||||
|
||||
|
||||
@router.get("/michigan", response_model=ApiResponse)
|
||||
@safe
|
||||
async def survey_michigan():
|
||||
"""University of Michigan Consumer Sentiment + inflation expectations."""
|
||||
data = await surveys_service.get_michigan()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/sloos", response_model=ApiResponse)
|
||||
@safe
|
||||
async def survey_sloos():
|
||||
"""Senior Loan Officer Opinion Survey (lending standards, recession signal)."""
|
||||
data = await surveys_service.get_sloos()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/nonfarm-payrolls", response_model=ApiResponse)
|
||||
@safe
|
||||
async def survey_nfp():
|
||||
"""Detailed employment data: employees, hours, earnings by industry."""
|
||||
data = await surveys_service.get_nonfarm_payrolls()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/empire-state", response_model=ApiResponse)
|
||||
@safe
|
||||
async def survey_empire():
|
||||
"""Empire State Manufacturing Survey (NY manufacturing outlook)."""
|
||||
data = await surveys_service.get_empire_state()
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/bls-search", response_model=ApiResponse)
|
||||
@safe
|
||||
async def survey_bls_search(query: str = Query(..., min_length=1, max_length=100)):
|
||||
"""Search BLS data series (CPI components, wages, employment, etc.)."""
|
||||
data = await surveys_service.bls_search(query=query)
|
||||
return ApiResponse(data=data)
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Routes for technical analysis indicators."""
|
||||
|
||||
from fastapi import APIRouter, Path
|
||||
from fastapi import APIRouter, Path, Query
|
||||
|
||||
from models import ApiResponse
|
||||
from route_utils import safe, validate_symbol
|
||||
@@ -16,3 +16,140 @@ async def stock_technical(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await technical_service.get_technical_indicators(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
# --- Individual Technical Indicators (Group I) ---
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/technical/atr", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_atr(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
length: int = Query(default=14, ge=1, le=100),
|
||||
):
|
||||
"""Average True Range -- volatility for position sizing and stop-loss."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await technical_service.get_atr(symbol, length=length)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/technical/adx", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_adx(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
length: int = Query(default=14, ge=1, le=100),
|
||||
):
|
||||
"""Average Directional Index -- trend strength (>25 strong, <20 range-bound)."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await technical_service.get_adx(symbol, length=length)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/technical/stoch", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_stoch(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
fast_k: int = Query(default=14, ge=1, le=100),
|
||||
slow_d: int = Query(default=3, ge=1, le=100),
|
||||
slow_k: int = Query(default=3, ge=1, le=100),
|
||||
):
|
||||
"""Stochastic Oscillator -- overbought/oversold momentum signal."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await technical_service.get_stoch(symbol, fast_k=fast_k, slow_d=slow_d, slow_k=slow_k)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/technical/obv", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_obv(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""On-Balance Volume -- cumulative volume for divergence detection."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await technical_service.get_obv(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/technical/ichimoku", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_ichimoku(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Ichimoku Cloud -- comprehensive trend system with support/resistance."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await technical_service.get_ichimoku(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/technical/donchian", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_donchian(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
length: int = Query(default=20, ge=1, le=100),
|
||||
):
|
||||
"""Donchian Channels -- breakout detection system."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await technical_service.get_donchian(symbol, length=length)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/technical/aroon", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_aroon(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
length: int = Query(default=25, ge=1, le=100),
|
||||
):
|
||||
"""Aroon Indicator -- identifies trend direction and potential changes."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await technical_service.get_aroon(symbol, length=length)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/technical/cci", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_cci(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
length: int = Query(default=14, ge=1, le=100),
|
||||
):
|
||||
"""Commodity Channel Index -- cyclical trend identification."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await technical_service.get_cci(symbol, length=length)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/technical/kc", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_kc(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
length: int = Query(default=20, ge=1, le=100),
|
||||
):
|
||||
"""Keltner Channels -- ATR-based volatility bands."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await technical_service.get_kc(symbol, length=length)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/technical/fib", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_fib(
|
||||
symbol: str = Path(..., min_length=1, max_length=20),
|
||||
days: int = Query(default=120, ge=5, le=365),
|
||||
):
|
||||
"""Fibonacci Retracement -- key support/resistance levels."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await technical_service.get_fib(symbol, days=days)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/technical/ad", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_ad(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Accumulation/Distribution Line -- volume-based trend indicator."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await technical_service.get_ad(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/stock/{symbol}/technical/cones", response_model=ApiResponse)
|
||||
@safe
|
||||
async def stock_cones(symbol: str = Path(..., min_length=1, max_length=20)):
|
||||
"""Volatility Cones -- realized vol quantiles for options analysis."""
|
||||
symbol = validate_symbol(symbol)
|
||||
data = await technical_service.get_cones(symbol)
|
||||
return ApiResponse(data=data)
|
||||
|
||||
59
shorts_service.py
Normal file
59
shorts_service.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""Equity shorts and dark pool data (stockgrid, FINRA, SEC)."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from openbb import obb
|
||||
|
||||
from obb_utils import to_list
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def get_short_volume(symbol: str) -> list[dict[str, Any]]:
|
||||
"""Get daily short volume data (stockgrid)."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.shorts.short_volume, symbol, provider="stockgrid"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Short volume failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_fails_to_deliver(symbol: str) -> list[dict[str, Any]]:
|
||||
"""Get fails-to-deliver records (SEC)."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.shorts.fails_to_deliver, symbol, provider="sec"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("FTD failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_short_interest(symbol: str) -> list[dict[str, Any]]:
|
||||
"""Get short interest positions (FINRA)."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.shorts.short_interest, symbol, provider="finra"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Short interest failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_darkpool_otc(symbol: str) -> list[dict[str, Any]]:
|
||||
"""Get OTC/dark pool aggregate trade data (FINRA)."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.equity.darkpool.otc, symbol, provider="finra"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Dark pool OTC failed for %s", symbol, exc_info=True)
|
||||
return []
|
||||
71
surveys_service.py
Normal file
71
surveys_service.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""Economy surveys: Michigan, SLOOS, NFP, Empire State, BLS."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from openbb import obb
|
||||
|
||||
from obb_utils import to_list
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def get_michigan() -> list[dict[str, Any]]:
|
||||
"""Get University of Michigan Consumer Sentiment + inflation expectations."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.economy.survey.university_of_michigan, provider="fred"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Michigan survey failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_sloos() -> list[dict[str, Any]]:
|
||||
"""Get Senior Loan Officer Opinion Survey (recession predictor)."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.economy.survey.sloos, provider="fred"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("SLOOS failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_nonfarm_payrolls() -> list[dict[str, Any]]:
|
||||
"""Get detailed employment data (NFP)."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.economy.survey.nonfarm_payrolls, provider="fred"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("NFP failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def get_empire_state() -> list[dict[str, Any]]:
|
||||
"""Get Empire State Manufacturing Survey."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.economy.survey.manufacturing_outlook_ny, provider="fred"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("Empire State failed", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def bls_search(query: str) -> list[dict[str, Any]]:
|
||||
"""Search BLS data series."""
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.economy.survey.bls_search, query, provider="bls"
|
||||
)
|
||||
return to_list(result)
|
||||
except Exception:
|
||||
logger.warning("BLS search failed for %s", query, exc_info=True)
|
||||
return []
|
||||
216
task_plan.md
Normal file
216
task_plan.md
Normal file
@@ -0,0 +1,216 @@
|
||||
# OpenBB Feature Expansion Plan
|
||||
|
||||
> 67 new endpoints across 10 feature groups. All use free providers.
|
||||
|
||||
## Prerequisites (Do First)
|
||||
|
||||
### P0: Consolidate Shared Utilities
|
||||
- [ ] Replace duplicate `_to_dicts` in `openbb_service.py` and `macro_service.py` with `obb_utils.to_list`
|
||||
- [ ] Add `fetch_historical(symbol, days, provider)` helper to `obb_utils.py`
|
||||
- [ ] Add `serialize_dates(items)` helper to `obb_utils.py`
|
||||
- **Files:** `obb_utils.py`, `openbb_service.py`, `macro_service.py`, `technical_service.py`, `quantitative_service.py`
|
||||
- **Complexity:** S
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Local Computation (No Provider Risk)
|
||||
|
||||
### Group I: Technical Analysis Extended (14 endpoints)
|
||||
- [ ] Add generic indicator dispatcher to `technical_service.py`
|
||||
- [ ] Implement indicators: ATR, ADX, Stochastic, OBV, VWAP, Ichimoku, Donchian, Aroon, CCI, Keltner Channels, Fibonacci, A/D Line, Volatility Cones, Relative Rotation
|
||||
- [ ] Add individual endpoints to `routes_technical.py`
|
||||
- [ ] Add generic endpoint: `GET /api/v1/stock/{symbol}/technical/{indicator}`
|
||||
- **New endpoints:**
|
||||
- `GET /api/v1/stock/{symbol}/technical/atr` -- Average True Range (volatility, position sizing)
|
||||
- `GET /api/v1/stock/{symbol}/technical/adx` -- Average Directional Index (trend strength)
|
||||
- `GET /api/v1/stock/{symbol}/technical/stoch` -- Stochastic Oscillator (overbought/oversold)
|
||||
- `GET /api/v1/stock/{symbol}/technical/obv` -- On-Balance Volume (volume-price divergence)
|
||||
- `GET /api/v1/stock/{symbol}/technical/vwap` -- Volume Weighted Average Price
|
||||
- `GET /api/v1/stock/{symbol}/technical/ichimoku` -- Ichimoku Cloud (comprehensive trend)
|
||||
- `GET /api/v1/stock/{symbol}/technical/donchian` -- Donchian Channels (breakout detection)
|
||||
- `GET /api/v1/stock/{symbol}/technical/aroon` -- Aroon Indicator (trend changes)
|
||||
- `GET /api/v1/stock/{symbol}/technical/cci` -- Commodity Channel Index (cyclical trends)
|
||||
- `GET /api/v1/stock/{symbol}/technical/kc` -- Keltner Channels (volatility bands)
|
||||
- `GET /api/v1/stock/{symbol}/technical/fib` -- Fibonacci Retracement (support/resistance)
|
||||
- `GET /api/v1/stock/{symbol}/technical/ad` -- Accumulation/Distribution Line
|
||||
- `GET /api/v1/stock/{symbol}/technical/cones` -- Volatility Cones (implied vs realized vol)
|
||||
- `GET /api/v1/stock/{symbol}/technical/relative_rotation` -- RRG (sector rotation)
|
||||
- **Extend:** `technical_service.py` (+200 lines), `routes_technical.py` (+80 lines)
|
||||
- **Complexity:** L (high volume, low individual complexity)
|
||||
|
||||
### Group J: Quantitative Extended (8 endpoints)
|
||||
- [ ] Add Sortino ratio, Omega ratio
|
||||
- [ ] Add rolling statistics: variance, stdev, mean, skew, kurtosis, quantile
|
||||
- **New endpoints:**
|
||||
- `GET /api/v1/stock/{symbol}/sortino?days=365` -- Sortino ratio (downside risk only)
|
||||
- `GET /api/v1/stock/{symbol}/omega?days=365` -- Omega ratio (full distribution)
|
||||
- `GET /api/v1/stock/{symbol}/rolling/variance?days=365&window=30` -- Rolling variance
|
||||
- `GET /api/v1/stock/{symbol}/rolling/stdev?days=365&window=30` -- Rolling std deviation
|
||||
- `GET /api/v1/stock/{symbol}/rolling/mean?days=365&window=30` -- Rolling mean
|
||||
- `GET /api/v1/stock/{symbol}/rolling/skew?days=365&window=30` -- Rolling skewness
|
||||
- `GET /api/v1/stock/{symbol}/rolling/kurtosis?days=365&window=30` -- Rolling kurtosis
|
||||
- `GET /api/v1/stock/{symbol}/rolling/quantile?days=365&window=30&quantile=0.5` -- Rolling quantile
|
||||
- **Extend:** `quantitative_service.py` (+120 lines), `routes_quantitative.py` (+60 lines)
|
||||
- **Complexity:** M
|
||||
|
||||
### Group H: Currency Reference Rates (1 endpoint)
|
||||
- [ ] Add ECB reference rates to `market_service.py`
|
||||
- **New endpoint:**
|
||||
- `GET /api/v1/currency/reference-rates` -- ECB reference rates for 28 currencies
|
||||
- **Extend:** `market_service.py` (+15 lines), `routes_market.py` (+10 lines)
|
||||
- **Complexity:** S
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: FRED/Federal Reserve Providers
|
||||
|
||||
### Group C: Fixed Income (10 endpoints)
|
||||
- [ ] Create `fixed_income_service.py` -- treasury rates, yield curve, auctions, TIPS, EFFR, SOFR, HQM, commercial paper, spot rates, spreads
|
||||
- [ ] Create `routes_fixed_income.py`
|
||||
- [ ] Register router in `main.py`
|
||||
- **New endpoints:**
|
||||
- `GET /api/v1/fixed-income/treasury-rates` -- Full yield curve rates (4W-30Y)
|
||||
- `GET /api/v1/fixed-income/yield-curve?date=` -- Yield curve with maturity/rate pairs
|
||||
- `GET /api/v1/fixed-income/treasury-auctions?security_type=` -- Auction bid-to-cover, yields
|
||||
- `GET /api/v1/fixed-income/tips-yields` -- TIPS real yields by maturity
|
||||
- `GET /api/v1/fixed-income/effr` -- Effective Fed Funds Rate with percentiles
|
||||
- `GET /api/v1/fixed-income/sofr` -- SOFR rate with moving averages
|
||||
- `GET /api/v1/fixed-income/hqm` -- High Quality Market corporate bond yields
|
||||
- `GET /api/v1/fixed-income/commercial-paper` -- CP rates by maturity/type
|
||||
- `GET /api/v1/fixed-income/spot-rates` -- Corporate bond spot rates
|
||||
- `GET /api/v1/fixed-income/spreads?series=tcm` -- Treasury/corporate spreads
|
||||
- **New files:** `fixed_income_service.py` (~250 lines), `routes_fixed_income.py` (~180 lines)
|
||||
- **Complexity:** L
|
||||
|
||||
### Group D: Economy Expanded (13 endpoints)
|
||||
- [ ] Extend `macro_service.py` with structured FRED indicators (CPI, GDP, unemployment, PCE, money measures)
|
||||
- [ ] Create `economy_service.py` for non-series endpoints (fred_search, fred_regional, balance_of_payments, central_bank_holdings, primary_dealer_positioning, fomc_documents)
|
||||
- [ ] Extend `routes_macro.py` for FRED-based indicators
|
||||
- [ ] Create `routes_economy.py` for search/institutional data
|
||||
- [ ] Register new router in `main.py`
|
||||
- **New endpoints (extend routes_macro.py):**
|
||||
- `GET /api/v1/macro/cpi?country=united_states` -- Consumer Price Index (multi-country)
|
||||
- `GET /api/v1/macro/gdp?type=real` -- GDP nominal/real/forecast
|
||||
- `GET /api/v1/macro/unemployment?country=united_states` -- Unemployment rate (multi-country)
|
||||
- `GET /api/v1/macro/pce` -- Personal Consumption Expenditures (Fed preferred inflation)
|
||||
- `GET /api/v1/macro/money-measures` -- M1/M2 money supply
|
||||
- `GET /api/v1/macro/cli?country=united_states` -- Composite Leading Indicator
|
||||
- `GET /api/v1/macro/house-price-index?country=united_states` -- Housing price index
|
||||
- **New endpoints (new routes_economy.py):**
|
||||
- `GET /api/v1/economy/fred-search?query=` -- Search FRED series by keyword
|
||||
- `GET /api/v1/economy/fred-regional?series_id=®ion=` -- Regional economic data
|
||||
- `GET /api/v1/economy/balance-of-payments` -- Current/capital/financial account
|
||||
- `GET /api/v1/economy/central-bank-holdings` -- Fed SOMA portfolio
|
||||
- `GET /api/v1/economy/primary-dealer-positioning` -- Wall Street firm positions
|
||||
- `GET /api/v1/economy/fomc-documents?year=` -- FOMC meeting documents
|
||||
- **New files:** `economy_service.py` (~200 lines), `routes_economy.py` (~150 lines)
|
||||
- **Extend:** `macro_service.py` (+80 lines), `routes_macro.py` (+50 lines)
|
||||
- **Complexity:** L
|
||||
|
||||
### Group E: Economy Surveys (5 endpoints)
|
||||
- [ ] Create `surveys_service.py` -- Michigan, SLOOS, NFP, Empire State, BLS
|
||||
- [ ] Create `routes_surveys.py`
|
||||
- [ ] Register router in `main.py`
|
||||
- **New endpoints:**
|
||||
- `GET /api/v1/economy/surveys/michigan` -- Consumer Sentiment + inflation expectations
|
||||
- `GET /api/v1/economy/surveys/sloos` -- Senior Loan Officer survey (recession predictor)
|
||||
- `GET /api/v1/economy/surveys/nonfarm-payrolls` -- Detailed employment data
|
||||
- `GET /api/v1/economy/surveys/empire-state` -- NY manufacturing outlook
|
||||
- `GET /api/v1/economy/surveys/bls-search?query=` -- BLS data series search
|
||||
- **New files:** `surveys_service.py` (~130 lines), `routes_surveys.py` (~100 lines)
|
||||
- **Complexity:** M
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: SEC/Stockgrid/CFTC Providers
|
||||
|
||||
### Group B: Equity Fundamentals (4 endpoints)
|
||||
- [ ] Add management, dividends, filings, search to `openbb_service.py`
|
||||
- [ ] Add endpoints to `routes.py`
|
||||
- **New endpoints:**
|
||||
- `GET /api/v1/stock/{symbol}/management` -- Executive team, titles, compensation
|
||||
- `GET /api/v1/stock/{symbol}/dividends` -- Historical dividend records
|
||||
- `GET /api/v1/stock/{symbol}/filings?form_type=10-K` -- SEC filings (10-K, 10-Q, 8-K)
|
||||
- `GET /api/v1/search?query=` -- Company search by name (SEC/NASDAQ)
|
||||
- **Extend:** `openbb_service.py` (+60 lines), `routes.py` (+40 lines)
|
||||
- **Complexity:** S
|
||||
|
||||
### Group A: Equity Shorts & Dark Pool (4 endpoints)
|
||||
- [ ] Create `shorts_service.py` -- short volume, FTD, short interest, OTC dark pool
|
||||
- [ ] Create `routes_shorts.py`
|
||||
- [ ] Register router in `main.py`
|
||||
- **New endpoints:**
|
||||
- `GET /api/v1/stock/{symbol}/shorts/volume` -- Daily short volume & percent (stockgrid)
|
||||
- `GET /api/v1/stock/{symbol}/shorts/ftd` -- Fails-to-deliver records (SEC)
|
||||
- `GET /api/v1/stock/{symbol}/shorts/interest` -- Short interest, days to cover (FINRA)
|
||||
- `GET /api/v1/darkpool/{symbol}/otc` -- OTC/dark pool trade volume (FINRA)
|
||||
- **New files:** `shorts_service.py` (~120 lines), `routes_shorts.py` (~80 lines)
|
||||
- **Complexity:** M
|
||||
|
||||
### Group F: Index & ETF Enhanced (3 endpoints)
|
||||
- [ ] Add sp500_multiples, index_constituents, etf nport_disclosure to `market_service.py`
|
||||
- [ ] Add endpoints to `routes_market.py`
|
||||
- **New endpoints:**
|
||||
- `GET /api/v1/index/sp500-multiples?series=pe_ratio` -- Historical S&P 500 valuation (Shiller PE, P/B, P/S, dividend yield)
|
||||
- `GET /api/v1/index/{symbol}/constituents` -- Index member stocks with sector/price data
|
||||
- `GET /api/v1/etf/{symbol}/nport` -- Detailed ETF holdings from SEC N-PORT filings
|
||||
- **Extend:** `market_service.py` (+60 lines), `routes_market.py` (+50 lines)
|
||||
- **Complexity:** S
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Regulators
|
||||
|
||||
### Group G: Regulators (5 endpoints)
|
||||
- [ ] Create `regulators_service.py` -- COT, COT search, SEC litigation, institution search, CIK mapping
|
||||
- [ ] Create `routes_regulators.py`
|
||||
- [ ] Register router in `main.py`
|
||||
- **New endpoints:**
|
||||
- `GET /api/v1/regulators/cot?symbol=` -- Commitment of Traders report (commercial/speculator positions)
|
||||
- `GET /api/v1/regulators/cot/search?query=` -- Search COT report symbols
|
||||
- `GET /api/v1/regulators/sec/litigation` -- SEC litigation releases RSS feed
|
||||
- `GET /api/v1/regulators/sec/institutions?query=` -- Search institutional investors
|
||||
- `GET /api/v1/regulators/sec/cik-map?symbol=` -- Ticker to CIK mapping
|
||||
- **New files:** `regulators_service.py` (~150 lines), `routes_regulators.py` (~100 lines)
|
||||
- **Complexity:** M
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Phase | Groups | Endpoints | New Files | Complexity |
|
||||
|-------|--------|-----------|-----------|------------|
|
||||
| P0 Prereq | - | 0 | 0 | S |
|
||||
| Phase 1 | I, J, H | 23 | 0 | L+M+S |
|
||||
| Phase 2 | C, D, E | 28 | 6 | L+L+M |
|
||||
| Phase 3 | B, A, F | 11 | 2 | S+M+S |
|
||||
| Phase 4 | G | 5 | 2 | M |
|
||||
| **Total** | **10** | **67** | **10** | |
|
||||
|
||||
### File Impact
|
||||
|
||||
**New files (10):**
|
||||
- `shorts_service.py`, `routes_shorts.py`
|
||||
- `fixed_income_service.py`, `routes_fixed_income.py`
|
||||
- `economy_service.py`, `routes_economy.py`
|
||||
- `surveys_service.py`, `routes_surveys.py`
|
||||
- `regulators_service.py`, `routes_regulators.py`
|
||||
|
||||
**Extended files (12):**
|
||||
- `obb_utils.py` (shared helpers)
|
||||
- `openbb_service.py` (Group B fundamentals)
|
||||
- `routes.py` (Group B endpoints)
|
||||
- `macro_service.py` (Group D indicators)
|
||||
- `routes_macro.py` (Group D endpoints)
|
||||
- `market_service.py` (Groups F, H)
|
||||
- `routes_market.py` (Groups F, H)
|
||||
- `technical_service.py` (Group I indicators)
|
||||
- `routes_technical.py` (Group I endpoints)
|
||||
- `quantitative_service.py` (Group J metrics)
|
||||
- `routes_quantitative.py` (Group J endpoints)
|
||||
- `main.py` (register 5 new routers)
|
||||
|
||||
### Endpoint Count After Completion
|
||||
- Current: 32 endpoints
|
||||
- New: 67 endpoints
|
||||
- **Total: 99 endpoints**
|
||||
@@ -6,28 +6,17 @@ from typing import Any
|
||||
|
||||
from openbb import obb
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from obb_utils import fetch_historical, to_list
|
||||
|
||||
PROVIDER = "yfinance"
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def get_technical_indicators(
|
||||
symbol: str, days: int = 400
|
||||
) -> dict[str, Any]:
|
||||
"""Compute key technical indicators for a symbol."""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
start = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
|
||||
|
||||
# Fetch historical data first
|
||||
hist = await asyncio.to_thread(
|
||||
obb.equity.price.historical,
|
||||
symbol,
|
||||
start_date=start,
|
||||
provider=PROVIDER,
|
||||
)
|
||||
|
||||
if hist is None or hist.results is None:
|
||||
hist = await fetch_historical(symbol, days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data available"}
|
||||
|
||||
result: dict[str, Any] = {"symbol": symbol}
|
||||
@@ -144,3 +133,266 @@ def _interpret_signals(data: dict[str, Any]) -> list[str]:
|
||||
signals.append("Death cross: SMA50 below SMA200 (bearish trend)")
|
||||
|
||||
return signals
|
||||
|
||||
|
||||
# --- Individual Indicator Functions (Phase 1, Group I) ---
|
||||
|
||||
|
||||
async def get_atr(symbol: str, length: int = 14, days: int = 400) -> dict[str, Any]:
|
||||
"""Average True Range -- volatility measurement for position sizing."""
|
||||
hist = await fetch_historical(symbol, days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.technical.atr, data=hist.results, length=length
|
||||
)
|
||||
latest = _extract_latest(result)
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"length": length,
|
||||
"atr": latest.get(f"ATRr_{length}"),
|
||||
}
|
||||
except Exception:
|
||||
logger.warning("ATR failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute ATR"}
|
||||
|
||||
|
||||
async def get_adx(symbol: str, length: int = 14, days: int = 400) -> dict[str, Any]:
|
||||
"""Average Directional Index -- trend strength (>25 strong, <20 range-bound)."""
|
||||
hist = await fetch_historical(symbol, days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.technical.adx, data=hist.results, length=length
|
||||
)
|
||||
latest = _extract_latest(result)
|
||||
adx_val = latest.get(f"ADX_{length}")
|
||||
signal = "strong trend" if adx_val and adx_val > 25 else "range-bound"
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"length": length,
|
||||
"adx": adx_val,
|
||||
"dmp": latest.get(f"DMP_{length}"),
|
||||
"dmn": latest.get(f"DMN_{length}"),
|
||||
"signal": signal,
|
||||
}
|
||||
except Exception:
|
||||
logger.warning("ADX failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute ADX"}
|
||||
|
||||
|
||||
async def get_stoch(
|
||||
symbol: str, fast_k: int = 14, slow_d: int = 3, slow_k: int = 3, days: int = 400,
|
||||
) -> dict[str, Any]:
|
||||
"""Stochastic Oscillator -- overbought/oversold momentum signal."""
|
||||
hist = await fetch_historical(symbol, days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.technical.stoch, data=hist.results,
|
||||
fast_k=fast_k, slow_d=slow_d, slow_k=slow_k,
|
||||
)
|
||||
latest = _extract_latest(result)
|
||||
k_val = latest.get(f"STOCHk_{fast_k}_{slow_d}_{slow_k}")
|
||||
d_val = latest.get(f"STOCHd_{fast_k}_{slow_d}_{slow_k}")
|
||||
signal = "neutral"
|
||||
if k_val is not None:
|
||||
if k_val > 80:
|
||||
signal = "overbought"
|
||||
elif k_val < 20:
|
||||
signal = "oversold"
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"stoch_k": k_val,
|
||||
"stoch_d": d_val,
|
||||
"signal": signal,
|
||||
}
|
||||
except Exception:
|
||||
logger.warning("Stochastic failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute Stochastic"}
|
||||
|
||||
|
||||
async def get_obv(symbol: str, days: int = 400) -> dict[str, Any]:
|
||||
"""On-Balance Volume -- cumulative volume indicator for divergence detection."""
|
||||
hist = await fetch_historical(symbol, days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
result = await asyncio.to_thread(obb.technical.obv, data=hist.results)
|
||||
latest = _extract_latest(result)
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"obv": latest.get("OBV"),
|
||||
}
|
||||
except Exception:
|
||||
logger.warning("OBV failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute OBV"}
|
||||
|
||||
|
||||
async def get_ichimoku(symbol: str, days: int = 400) -> dict[str, Any]:
|
||||
"""Ichimoku Cloud -- comprehensive trend system."""
|
||||
hist = await fetch_historical(symbol, days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
result = await asyncio.to_thread(obb.technical.ichimoku, data=hist.results)
|
||||
latest = _extract_latest(result)
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"tenkan_sen": latest.get("ITS_9"),
|
||||
"kijun_sen": latest.get("IKS_26"),
|
||||
"senkou_span_a": latest.get("ISA_9"),
|
||||
"senkou_span_b": latest.get("ISB_26"),
|
||||
"chikou_span": latest.get("ICS_26"),
|
||||
}
|
||||
except Exception:
|
||||
logger.warning("Ichimoku failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute Ichimoku"}
|
||||
|
||||
|
||||
async def get_donchian(symbol: str, length: int = 20, days: int = 400) -> dict[str, Any]:
|
||||
"""Donchian Channels -- breakout detection system."""
|
||||
hist = await fetch_historical(symbol, days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.technical.donchian, data=hist.results, lower_length=length, upper_length=length,
|
||||
)
|
||||
latest = _extract_latest(result)
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"length": length,
|
||||
"upper": latest.get(f"DCU_{length}_{length}"),
|
||||
"middle": latest.get(f"DCM_{length}_{length}"),
|
||||
"lower": latest.get(f"DCL_{length}_{length}"),
|
||||
}
|
||||
except Exception:
|
||||
logger.warning("Donchian failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute Donchian"}
|
||||
|
||||
|
||||
async def get_aroon(symbol: str, length: int = 25, days: int = 400) -> dict[str, Any]:
|
||||
"""Aroon Indicator -- trend direction and strength."""
|
||||
hist = await fetch_historical(symbol, days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.technical.aroon, data=hist.results, length=length,
|
||||
)
|
||||
latest = _extract_latest(result)
|
||||
up = latest.get(f"AROONU_{length}")
|
||||
down = latest.get(f"AROOND_{length}")
|
||||
osc = latest.get(f"AROONOSC_{length}")
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"length": length,
|
||||
"aroon_up": up,
|
||||
"aroon_down": down,
|
||||
"aroon_oscillator": osc,
|
||||
}
|
||||
except Exception:
|
||||
logger.warning("Aroon failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute Aroon"}
|
||||
|
||||
|
||||
async def get_cci(symbol: str, length: int = 14, days: int = 400) -> dict[str, Any]:
|
||||
"""Commodity Channel Index -- cyclical trend identification."""
|
||||
hist = await fetch_historical(symbol, days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.technical.cci, data=hist.results, length=length,
|
||||
)
|
||||
latest = _extract_latest(result)
|
||||
cci_val = latest.get(f"CCI_{length}_{0.015}")
|
||||
signal = "neutral"
|
||||
if cci_val is not None:
|
||||
if cci_val > 100:
|
||||
signal = "overbought"
|
||||
elif cci_val < -100:
|
||||
signal = "oversold"
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"length": length,
|
||||
"cci": cci_val,
|
||||
"signal": signal,
|
||||
}
|
||||
except Exception:
|
||||
logger.warning("CCI failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute CCI"}
|
||||
|
||||
|
||||
async def get_kc(symbol: str, length: int = 20, days: int = 400) -> dict[str, Any]:
|
||||
"""Keltner Channels -- ATR-based volatility bands."""
|
||||
hist = await fetch_historical(symbol, days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
result = await asyncio.to_thread(
|
||||
obb.technical.kc, data=hist.results, length=length,
|
||||
)
|
||||
latest = _extract_latest(result)
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"length": length,
|
||||
"upper": latest.get(f"KCUe_{length}_2"),
|
||||
"middle": latest.get(f"KCBe_{length}_2"),
|
||||
"lower": latest.get(f"KCLe_{length}_2"),
|
||||
}
|
||||
except Exception:
|
||||
logger.warning("Keltner failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute Keltner Channels"}
|
||||
|
||||
|
||||
async def get_fib(symbol: str, days: int = 120) -> dict[str, Any]:
|
||||
"""Fibonacci Retracement levels from recent price range."""
|
||||
hist = await fetch_historical(symbol, days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
result = await asyncio.to_thread(obb.technical.fib, data=hist.results)
|
||||
latest = _extract_latest(result)
|
||||
return {"symbol": symbol, **latest}
|
||||
except Exception:
|
||||
logger.warning("Fibonacci failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute Fibonacci"}
|
||||
|
||||
|
||||
async def get_ad(symbol: str, days: int = 400) -> dict[str, Any]:
|
||||
"""Accumulation/Distribution Line -- volume-based trend indicator."""
|
||||
hist = await fetch_historical(symbol, days)
|
||||
if hist is None:
|
||||
return {"symbol": symbol, "error": "No historical data"}
|
||||
try:
|
||||
result = await asyncio.to_thread(obb.technical.ad, data=hist.results)
|
||||
latest = _extract_latest(result)
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"ad": latest.get("AD"),
|
||||
"ad_obv": latest.get("AD_OBV"),
|
||||
}
|
||||
except Exception:
|
||||
logger.warning("A/D failed for %s", symbol, exc_info=True)
|
||||
return {"symbol": symbol, "error": "Failed to compute A/D Line"}
|
||||
|
||||
|
||||
async def get_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"}
|
||||
|
||||
Reference in New Issue
Block a user