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:
@@ -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}"}
|
||||
|
||||
Reference in New Issue
Block a user