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