feat: OpenBB Investment Analysis API

REST API wrapping OpenBB SDK for stock data, sentiment analysis,
technical indicators, macro data, and rule-based portfolio analysis.

- Stock data via yfinance (quote, profile, metrics, financials, historical, news)
- News sentiment via Alpha Vantage (per-article, per-ticker scores)
- Analyst data via Finnhub (recommendations, insider trades, upgrades)
- Macro data via FRED (Fed rate, CPI, GDP, unemployment, treasury yields)
- Technical indicators via openbb-technical (RSI, MACD, SMA, EMA, Bollinger)
- Rule-based portfolio analysis engine (BUY_MORE/HOLD/SELL)
- Stock discovery (gainers, losers, active, undervalued, growth)
- 102 tests, all passing
This commit is contained in:
Yaojia Wang
2026-03-09 00:20:10 +01:00
commit ad45cb429c
30 changed files with 3107 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
from unittest.mock import patch, AsyncMock, MagicMock
import pytest
import finnhub_service
@pytest.mark.asyncio
@patch("finnhub_service.settings")
async def test_not_configured_returns_empty(mock_settings):
mock_settings.finnhub_api_key = ""
result = await finnhub_service.get_news_sentiment("AAPL")
assert result == {}
@pytest.mark.asyncio
@patch("finnhub_service.settings")
async def test_not_configured_returns_message(mock_settings):
mock_settings.finnhub_api_key = ""
result = await finnhub_service.get_sentiment_summary("AAPL")
assert result["configured"] is False
@pytest.mark.asyncio
@patch("finnhub_service.settings")
async def test_insider_not_configured(mock_settings):
mock_settings.finnhub_api_key = ""
result = await finnhub_service.get_insider_transactions("AAPL")
assert result == []
@pytest.mark.asyncio
@patch("finnhub_service.settings")
async def test_recommendations_not_configured(mock_settings):
mock_settings.finnhub_api_key = ""
result = await finnhub_service.get_recommendation_trends("AAPL")
assert result == []
@pytest.mark.asyncio
@patch("finnhub_service.settings")
async def test_upgrade_downgrade_not_configured(mock_settings):
mock_settings.finnhub_api_key = ""
result = await finnhub_service.get_upgrade_downgrade("AAPL")
assert result == []
@pytest.mark.asyncio
@patch("finnhub_service.settings")
@patch("finnhub_service.httpx.AsyncClient")
async def test_news_sentiment_with_key(mock_client_cls, mock_settings):
mock_settings.finnhub_api_key = "test_key"
mock_resp = MagicMock()
mock_resp.json.return_value = {
"buzz": {"articlesInLastWeek": 10},
"sentiment": {"bullishPercent": 0.7, "bearishPercent": 0.3},
"companyNewsScore": 0.65,
"symbol": "AAPL",
}
mock_resp.raise_for_status = MagicMock()
mock_client = AsyncMock()
mock_client.get.return_value = mock_resp
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=False)
mock_client_cls.return_value = mock_client
result = await finnhub_service.get_news_sentiment("AAPL")
assert result["symbol"] == "AAPL"
assert result["sentiment"]["bullishPercent"] == 0.7
@pytest.mark.asyncio
@patch("finnhub_service.settings")
@patch("finnhub_service.httpx.AsyncClient")
async def test_insider_transactions_with_key(mock_client_cls, mock_settings):
mock_settings.finnhub_api_key = "test_key"
mock_resp = MagicMock()
mock_resp.json.return_value = {
"data": [
{"name": "Tim Cook", "share": 100000, "change": -50000, "transactionCode": "S"}
]
}
mock_resp.raise_for_status = MagicMock()
mock_client = AsyncMock()
mock_client.get.return_value = mock_resp
mock_client.__aenter__ = AsyncMock(return_value=mock_client)
mock_client.__aexit__ = AsyncMock(return_value=False)
mock_client_cls.return_value = mock_client
result = await finnhub_service.get_insider_transactions("AAPL")
assert len(result) == 1
assert result[0]["name"] == "Tim Cook"