Files
openbb-invest-api/tests/test_analysis_service.py
Yaojia Wang ad45cb429c 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
2026-03-09 00:20:10 +01:00

133 lines
3.6 KiB
Python

from models import ActionEnum, ConfidenceEnum
from analysis_service import (
compute_analysis,
_score_pnl,
_score_pe,
_score_revenue_growth,
_score_target_price,
)
class TestScorePnl:
def test_large_loss_suggests_buy(self):
score, reason = _score_pnl(-0.25)
assert score == 1
assert "averaging down" in reason
def test_large_profit_suggests_sell(self):
score, reason = _score_pnl(0.60)
assert score == -1
assert "taking profit" in reason
def test_moderate_suggests_hold(self):
score, reason = _score_pnl(0.10)
assert score == 0
class TestScorePe:
def test_low_pe(self):
score, _ = _score_pe(10.0)
assert score == 1
def test_high_pe(self):
score, _ = _score_pe(50.0)
assert score == -1
def test_normal_pe(self):
score, _ = _score_pe(20.0)
assert score == 0
def test_negative_pe(self):
score, _ = _score_pe(-5.0)
assert score == -1
def test_none_pe(self):
score, reason = _score_pe(None)
assert score == 0
assert reason is None
class TestScoreRevenueGrowth:
def test_strong_growth(self):
score, _ = _score_revenue_growth(0.20)
assert score == 1
def test_negative_growth(self):
score, _ = _score_revenue_growth(-0.05)
assert score == -1
def test_moderate_growth(self):
score, _ = _score_revenue_growth(0.05)
assert score == 0
def test_none(self):
score, reason = _score_revenue_growth(None)
assert score == 0
class TestScoreTargetPrice:
def test_big_upside(self):
score, _ = _score_target_price(100.0, 120.0)
assert score == 1
def test_big_downside(self):
score, _ = _score_target_price(100.0, 85.0)
assert score == -1
def test_near_target(self):
score, _ = _score_target_price(100.0, 105.0)
assert score == 0
def test_none_price(self):
score, _ = _score_target_price(None, 120.0)
assert score == 0
class TestComputeAnalysis:
def test_strong_buy_signals(self):
result = compute_analysis(
current_price=100.0,
buy_in_price=130.0, # loss > 20%
target_price=120.0, # upside > 15%
metrics={"pe_ratio": 10.0, "revenue_growth": 0.20},
)
assert result.action == ActionEnum.BUY_MORE
assert result.confidence == ConfidenceEnum.HIGH
def test_strong_sell_signals(self):
result = compute_analysis(
current_price=200.0,
buy_in_price=100.0, # profit > 50%
target_price=170.0, # downside
metrics={"pe_ratio": 50.0, "revenue_growth": -0.10},
)
assert result.action == ActionEnum.SELL
def test_mixed_signals_hold(self):
result = compute_analysis(
current_price=100.0,
buy_in_price=95.0,
target_price=105.0,
metrics={"pe_ratio": 20.0, "revenue_growth": 0.05},
)
assert result.action == ActionEnum.HOLD
def test_no_data(self):
result = compute_analysis(
current_price=None,
buy_in_price=100.0,
target_price=None,
metrics={},
)
assert result.action == ActionEnum.HOLD
assert result.confidence == ConfidenceEnum.LOW
def test_reasons_populated(self):
result = compute_analysis(
current_price=100.0,
buy_in_price=90.0,
target_price=110.0,
metrics={"pe_ratio": 25.0},
)
assert len(result.reasons) > 0