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:
149
tests/test_models.py
Normal file
149
tests/test_models.py
Normal file
@@ -0,0 +1,149 @@
|
||||
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
|
||||
Reference in New Issue
Block a user