feat: add portfolio optimization and congress tracking (TDD)
Portfolio optimization (3 endpoints): - POST /portfolio/optimize - HRP optimal weights via scipy clustering - POST /portfolio/correlation - pairwise correlation matrix - POST /portfolio/risk-parity - inverse-volatility risk parity weights Congress tracking (2 endpoints): - GET /regulators/congress/trades - congress member stock trades - GET /regulators/congress/bills?query= - search congress bills Implementation: - portfolio_service.py: HRP with scipy fallback to inverse-vol - congress_service.py: multi-provider fallback pattern - 51 new tests (14 portfolio unit, 20 portfolio route, 12 congress unit, 7 congress route) - All 312 tests passing
This commit is contained in:
68
congress_service.py
Normal file
68
congress_service.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""Congress trading data: member trades and bill search."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from openbb import obb
|
||||
|
||||
from obb_utils import to_list
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def _try_obb_call(fn, *args, **kwargs) -> list[dict[str, Any]] | None:
|
||||
"""Attempt a single OBB call and return to_list result, or None on failure."""
|
||||
try:
|
||||
result = await asyncio.to_thread(fn, *args, **kwargs)
|
||||
return to_list(result)
|
||||
except Exception as exc:
|
||||
logger.debug("OBB call failed: %s", exc)
|
||||
return None
|
||||
|
||||
|
||||
def _get_congress_fn():
|
||||
"""Resolve the congress trading OBB function safely."""
|
||||
try:
|
||||
return obb.regulators.government_us.congress_trading
|
||||
except AttributeError:
|
||||
logger.debug("obb.regulators.government_us.congress_trading not available")
|
||||
return None
|
||||
|
||||
|
||||
async def get_congress_trades() -> list[dict[str, Any]]:
|
||||
"""Get recent US congress member stock trades.
|
||||
|
||||
Returns an empty list if the data provider is unavailable.
|
||||
"""
|
||||
fn = _get_congress_fn()
|
||||
if fn is None:
|
||||
return []
|
||||
|
||||
providers = ["quiverquant", "fmp"]
|
||||
for provider in providers:
|
||||
data = await _try_obb_call(fn, provider=provider)
|
||||
if data is not None:
|
||||
return data
|
||||
|
||||
logger.warning("All congress trades providers failed")
|
||||
return []
|
||||
|
||||
|
||||
async def search_congress_bills(query: str) -> list[dict[str, Any]]:
|
||||
"""Search US congress bills by keyword.
|
||||
|
||||
Returns an empty list if the data provider is unavailable.
|
||||
"""
|
||||
fn = _get_congress_fn()
|
||||
if fn is None:
|
||||
return []
|
||||
|
||||
providers = ["quiverquant", "fmp"]
|
||||
for provider in providers:
|
||||
data = await _try_obb_call(fn, query, provider=provider)
|
||||
if data is not None:
|
||||
return data
|
||||
|
||||
logger.warning("All congress bills providers failed for query: %s", query)
|
||||
return []
|
||||
Reference in New Issue
Block a user