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:
104
tests/test_routes_sentiment.py
Normal file
104
tests/test_routes_sentiment.py
Normal file
@@ -0,0 +1,104 @@
|
||||
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
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("routes_sentiment.alphavantage_service.get_news_sentiment", new_callable=AsyncMock)
|
||||
@patch("routes_sentiment.finnhub_service.get_sentiment_summary", new_callable=AsyncMock)
|
||||
async def test_stock_sentiment(mock_sentiment, mock_av, client):
|
||||
mock_sentiment.return_value = {
|
||||
"symbol": "AAPL",
|
||||
"news_sentiment": {"bullish_percent": 0.7, "bearish_percent": 0.3},
|
||||
"recent_news": [],
|
||||
"analyst_recommendations": [],
|
||||
"recent_upgrades_downgrades": [],
|
||||
}
|
||||
mock_av.return_value = {
|
||||
"configured": True,
|
||||
"symbol": "AAPL",
|
||||
"article_count": 1,
|
||||
"overall_sentiment": {"avg_score": 0.4, "label": "Bullish"},
|
||||
"articles": [],
|
||||
}
|
||||
resp = await client.get("/api/v1/stock/AAPL/sentiment")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()["data"]
|
||||
assert data["symbol"] == "AAPL"
|
||||
assert data["news_sentiment"]["bullish_percent"] == 0.7
|
||||
assert data["alpha_vantage_sentiment"]["overall_sentiment"]["label"] == "Bullish"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("routes_sentiment.finnhub_service.get_insider_transactions", new_callable=AsyncMock)
|
||||
async def test_stock_insider_trades(mock_insider, client):
|
||||
mock_insider.return_value = [
|
||||
{"name": "Tim Cook", "share": 100000, "change": -50000, "transactionCode": "S"}
|
||||
]
|
||||
resp = await client.get("/api/v1/stock/AAPL/insider-trades")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()["data"]
|
||||
assert len(data) == 1
|
||||
assert data[0]["name"] == "Tim Cook"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("routes_sentiment.finnhub_service.get_recommendation_trends", new_callable=AsyncMock)
|
||||
async def test_stock_recommendations(mock_recs, client):
|
||||
mock_recs.return_value = [
|
||||
{"period": "2026-01-01", "strongBuy": 10, "buy": 15, "hold": 5, "sell": 1, "strongSell": 0}
|
||||
]
|
||||
resp = await client.get("/api/v1/stock/AAPL/recommendations")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()["data"]
|
||||
assert len(data) == 1
|
||||
assert data[0]["strong_buy"] == 10
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("routes_sentiment.finnhub_service.get_upgrade_downgrade", new_callable=AsyncMock)
|
||||
async def test_stock_upgrades(mock_upgrades, client):
|
||||
mock_upgrades.return_value = [
|
||||
{"company": "Morgan Stanley", "action": "upgrade", "fromGrade": "Hold", "toGrade": "Buy"}
|
||||
]
|
||||
resp = await client.get("/api/v1/stock/AAPL/upgrades")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()["data"]
|
||||
assert len(data) == 1
|
||||
assert data[0]["action"] == "upgrade"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@patch("routes_sentiment.alphavantage_service.get_news_sentiment", new_callable=AsyncMock)
|
||||
async def test_stock_news_sentiment(mock_av, client):
|
||||
mock_av.return_value = {
|
||||
"configured": True,
|
||||
"symbol": "AAPL",
|
||||
"article_count": 2,
|
||||
"overall_sentiment": {"avg_score": 0.3, "label": "Somewhat-Bullish"},
|
||||
"articles": [
|
||||
{"title": "Apple up", "ticker_sentiment_score": 0.5},
|
||||
{"title": "Apple flat", "ticker_sentiment_score": 0.1},
|
||||
],
|
||||
}
|
||||
resp = await client.get("/api/v1/stock/AAPL/news-sentiment")
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()["data"]
|
||||
assert data["article_count"] == 2
|
||||
assert data["overall_sentiment"]["label"] == "Somewhat-Bullish"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invalid_symbol_sentiment(client):
|
||||
resp = await client.get("/api/v1/stock/AAPL;DROP/sentiment")
|
||||
assert resp.status_code == 400
|
||||
Reference in New Issue
Block a user