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
150 lines
3.5 KiB
Python
150 lines
3.5 KiB
Python
from datetime import datetime, timezone
|
|
|
|
from pydantic import ValidationError
|
|
|
|
from models import (
|
|
ActionEnum,
|
|
AnalysisResult,
|
|
ConfidenceEnum,
|
|
Holding,
|
|
HoldingAnalysis,
|
|
MetricsResponse,
|
|
PortfolioRequest,
|
|
PortfolioResponse,
|
|
QuoteResponse,
|
|
)
|
|
|
|
|
|
def test_holding_valid():
|
|
h = Holding(symbol="AAPL", shares=100, buy_in_price=150.0)
|
|
assert h.symbol == "AAPL"
|
|
assert h.shares == 100
|
|
assert h.buy_in_price == 150.0
|
|
|
|
|
|
def test_holding_swedish_symbol():
|
|
h = Holding(symbol="VOLV-B.ST", shares=50, buy_in_price=250.0)
|
|
assert h.symbol == "VOLV-B.ST"
|
|
|
|
|
|
def test_holding_symbol_uppercased():
|
|
h = Holding(symbol="aapl", shares=10, buy_in_price=150.0)
|
|
assert h.symbol == "AAPL"
|
|
|
|
|
|
def test_holding_invalid_symbol_format():
|
|
try:
|
|
Holding(symbol="AAPL;DROP TABLE", shares=10, buy_in_price=150.0)
|
|
assert False, "Should have raised"
|
|
except ValidationError:
|
|
pass
|
|
|
|
|
|
def test_holding_symbol_too_long():
|
|
try:
|
|
Holding(symbol="A" * 21, shares=10, buy_in_price=150.0)
|
|
assert False, "Should have raised"
|
|
except ValidationError:
|
|
pass
|
|
|
|
|
|
def test_holding_invalid_shares():
|
|
try:
|
|
Holding(symbol="AAPL", shares=0, buy_in_price=150.0)
|
|
assert False, "Should have raised"
|
|
except ValidationError:
|
|
pass
|
|
|
|
|
|
def test_holding_invalid_price():
|
|
try:
|
|
Holding(symbol="AAPL", shares=10, buy_in_price=-5.0)
|
|
assert False, "Should have raised"
|
|
except ValidationError:
|
|
pass
|
|
|
|
|
|
def test_portfolio_request_empty():
|
|
try:
|
|
PortfolioRequest(holdings=[])
|
|
assert False, "Should have raised"
|
|
except ValidationError:
|
|
pass
|
|
|
|
|
|
def test_portfolio_request_too_many():
|
|
holdings = [
|
|
Holding(symbol="AAPL", shares=1, buy_in_price=100)
|
|
for _ in range(51)
|
|
]
|
|
try:
|
|
PortfolioRequest(holdings=holdings)
|
|
assert False, "Should have raised"
|
|
except ValidationError:
|
|
pass
|
|
|
|
|
|
def test_portfolio_request_valid():
|
|
req = PortfolioRequest(
|
|
holdings=[Holding(symbol="AAPL", shares=10, buy_in_price=150)]
|
|
)
|
|
assert len(req.holdings) == 1
|
|
|
|
|
|
def test_quote_response_defaults():
|
|
q = QuoteResponse(symbol="AAPL")
|
|
assert q.price is None
|
|
assert q.change is None
|
|
|
|
|
|
def test_metrics_response():
|
|
m = MetricsResponse(symbol="AAPL", pe_ratio=25.0, roe=0.15)
|
|
assert m.pe_ratio == 25.0
|
|
assert m.pb_ratio is None
|
|
|
|
|
|
def test_analysis_result():
|
|
a = AnalysisResult(
|
|
action=ActionEnum.BUY_MORE,
|
|
confidence=ConfidenceEnum.HIGH,
|
|
reasons=["Low PE", "Strong growth"],
|
|
)
|
|
assert a.action == ActionEnum.BUY_MORE
|
|
assert len(a.reasons) == 2
|
|
|
|
|
|
def test_holding_analysis():
|
|
ha = HoldingAnalysis(
|
|
symbol="AAPL",
|
|
current_price=180.0,
|
|
buy_in_price=150.0,
|
|
shares=100,
|
|
pnl=3000.0,
|
|
pnl_percent=0.2,
|
|
analysis=AnalysisResult(
|
|
action=ActionEnum.HOLD,
|
|
confidence=ConfidenceEnum.MEDIUM,
|
|
reasons=["Within hold range"],
|
|
),
|
|
)
|
|
assert ha.pnl == 3000.0
|
|
|
|
|
|
def test_portfolio_response():
|
|
pr = PortfolioResponse(
|
|
holdings=[
|
|
HoldingAnalysis(
|
|
symbol="AAPL",
|
|
buy_in_price=150.0,
|
|
shares=100,
|
|
analysis=AnalysisResult(
|
|
action=ActionEnum.HOLD,
|
|
confidence=ConfidenceEnum.LOW,
|
|
reasons=["No data"],
|
|
),
|
|
)
|
|
],
|
|
analyzed_at=datetime.now(timezone.utc),
|
|
)
|
|
assert len(pr.holdings) == 1
|