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:
98
tests/test_routes_congress.py
Normal file
98
tests/test_routes_congress.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""Tests for congress trading routes (TDD - RED phase first)."""
|
||||
|
||||
from unittest.mock import patch, AsyncMock
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient, ASGITransport
|
||||
|
||||
from main import app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def client():
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as c:
|
||||
yield c
|
||||
|
||||
|
||||
# --- GET /api/v1/regulators/congress/trades ---
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("routes_regulators.congress_service.get_congress_trades", new_callable=AsyncMock)
|
||||
async def test_congress_trades_happy_path(mock_fn, client):
|
||||
mock_fn.return_value = [
|
||||
{
|
||||
"representative": "Nancy Pelosi",
|
||||
"ticker": "NVDA",
|
||||
"transaction_date": "2024-01-15",
|
||||
"transaction_type": "Purchase",
|
||||
"amount": "$1,000,001-$5,000,000",
|
||||
}
|
||||
]
|
||||
resp = await client.get("/api/v1/regulators/congress/trades")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["success"] is True
|
||||
assert len(data["data"]) == 1
|
||||
assert data["data"][0]["representative"] == "Nancy Pelosi"
|
||||
mock_fn.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("routes_regulators.congress_service.get_congress_trades", new_callable=AsyncMock)
|
||||
async def test_congress_trades_empty(mock_fn, client):
|
||||
mock_fn.return_value = []
|
||||
resp = await client.get("/api/v1/regulators/congress/trades")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["data"] == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("routes_regulators.congress_service.get_congress_trades", new_callable=AsyncMock)
|
||||
async def test_congress_trades_service_error_returns_502(mock_fn, client):
|
||||
mock_fn.side_effect = RuntimeError("Data provider unavailable")
|
||||
resp = await client.get("/api/v1/regulators/congress/trades")
|
||||
assert resp.status_code == 502
|
||||
|
||||
|
||||
# --- GET /api/v1/regulators/congress/bills ---
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("routes_regulators.congress_service.search_congress_bills", new_callable=AsyncMock)
|
||||
async def test_congress_bills_happy_path(mock_fn, client):
|
||||
mock_fn.return_value = [
|
||||
{"title": "Infrastructure Investment and Jobs Act", "bill_id": "HR3684"},
|
||||
{"title": "Inflation Reduction Act", "bill_id": "HR5376"},
|
||||
]
|
||||
resp = await client.get("/api/v1/regulators/congress/bills?query=infrastructure")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
assert data["success"] is True
|
||||
assert len(data["data"]) == 2
|
||||
assert data["data"][0]["bill_id"] == "HR3684"
|
||||
mock_fn.assert_called_once_with("infrastructure")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_congress_bills_missing_query(client):
|
||||
resp = await client.get("/api/v1/regulators/congress/bills")
|
||||
assert resp.status_code == 422
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("routes_regulators.congress_service.search_congress_bills", new_callable=AsyncMock)
|
||||
async def test_congress_bills_empty(mock_fn, client):
|
||||
mock_fn.return_value = []
|
||||
resp = await client.get("/api/v1/regulators/congress/bills?query=nonexistent")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["data"] == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("routes_regulators.congress_service.search_congress_bills", new_callable=AsyncMock)
|
||||
async def test_congress_bills_service_error_returns_502(mock_fn, client):
|
||||
mock_fn.side_effect = RuntimeError("Congress API unavailable")
|
||||
resp = await client.get("/api/v1/regulators/congress/bills?query=tax")
|
||||
assert resp.status_code == 502
|
||||
Reference in New Issue
Block a user