mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-27 20:42:26 +00:00
* refactor openbb-technical for v5 * forward refs * linting * no python 3.15 in test matrix * reame * ty * move all params to body --------- Co-authored-by: deeleeramone <>
453 lines
16 KiB
Python
453 lines
16 KiB
Python
"""End-to-end coverage of every openbb-technical endpoint against real CSV data.
|
|
|
|
Single-symbol endpoints run against one daily and one intraday SPY fixture
|
|
(``spy_daily_or_intraday``) so both date and datetime code paths are exercised
|
|
without a full 6-interval matrix explosion. Multi-symbol endpoints use the
|
|
``dow30_multi`` fixture loaded from ``multi.csv``. The catalog endpoint takes
|
|
no data and is exercised once per category.
|
|
|
|
Every test asserts the response is an ``OBBject`` whose ``results`` is a
|
|
non-empty list (or, for sparse signal endpoints, ``allow_empty=True``).
|
|
"""
|
|
|
|
import pytest
|
|
from openbb_core.app.model.obbject import OBBject
|
|
|
|
pytestmark = pytest.mark.integration
|
|
|
|
|
|
def _assert_obbject(result, *, allow_empty: bool = False) -> None:
|
|
"""Assert ``result`` is an OBBject with usable ``results``."""
|
|
assert isinstance(result, OBBject), f"expected OBBject, got {type(result).__name__}"
|
|
res = result.results
|
|
assert res is not None, "OBBject.results is None"
|
|
if not allow_empty and isinstance(res, list):
|
|
assert len(res) > 0, "OBBject.results is empty list"
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Overlays #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
class TestOverlays:
|
|
def test_sma(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.sma(data=spy_daily_or_intraday, length=20))
|
|
|
|
def test_ema(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.ema(data=spy_daily_or_intraday, length=20))
|
|
|
|
def test_hma(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.hma(data=spy_daily_or_intraday, length=20))
|
|
|
|
def test_wma(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.wma(data=spy_daily_or_intraday, length=20))
|
|
|
|
def test_zlma(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.zlma(data=spy_daily_or_intraday, length=20))
|
|
|
|
def test_tema(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.tema(data=spy_daily_or_intraday, length=10))
|
|
|
|
def test_dema(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.dema(data=spy_daily_or_intraday, length=10))
|
|
|
|
def test_kama(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.kama(data=spy_daily_or_intraday, length=10))
|
|
|
|
def test_frama(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.frama(data=spy_daily_or_intraday, length=16))
|
|
|
|
def test_vwma(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.vwma(data=spy_daily_or_intraday, length=20))
|
|
|
|
def test_bbands(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.bbands(data=spy_daily_or_intraday, length=20))
|
|
|
|
def test_donchian(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(
|
|
obb.technical.donchian(
|
|
data=spy_daily_or_intraday, lower_length=20, upper_length=20
|
|
)
|
|
)
|
|
|
|
def test_kc(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.kc(data=spy_daily_or_intraday, length=20))
|
|
|
|
def test_ichimoku(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.ichimoku(data=spy_daily_or_intraday))
|
|
|
|
def test_supertrend(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.supertrend(data=spy_daily_or_intraday, length=10))
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Oscillators #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
class TestOscillators:
|
|
def test_rsi(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.rsi(data=spy_daily_or_intraday, length=14))
|
|
|
|
def test_stoch(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(
|
|
obb.technical.stoch(
|
|
data=spy_daily_or_intraday,
|
|
fast_k_period=14,
|
|
slow_d_period=3,
|
|
slow_k_period=3,
|
|
)
|
|
)
|
|
|
|
def test_cci(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.cci(data=spy_daily_or_intraday, length=20))
|
|
|
|
def test_fisher(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.fisher(data=spy_daily_or_intraday, length=9))
|
|
|
|
def test_cg(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.cg(data=spy_daily_or_intraday, length=14))
|
|
|
|
def test_williams_r(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.williams_r(data=spy_daily_or_intraday, length=14))
|
|
|
|
def test_mfi(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.mfi(data=spy_daily_or_intraday, length=14))
|
|
|
|
def test_trix(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.trix(data=spy_daily_or_intraday, length=15))
|
|
|
|
def test_ultimate_oscillator(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(
|
|
obb.technical.ultimate_oscillator(
|
|
data=spy_daily_or_intraday, fast=7, medium=14, slow=28
|
|
)
|
|
)
|
|
|
|
def test_awesome_oscillator(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.awesome_oscillator(data=spy_daily_or_intraday))
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Trend #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
class TestTrend:
|
|
def test_macd(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(
|
|
obb.technical.macd(data=spy_daily_or_intraday, fast=12, slow=26, signal=9)
|
|
)
|
|
|
|
def test_adx(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.adx(data=spy_daily_or_intraday, length=14))
|
|
|
|
def test_di(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.di(data=spy_daily_or_intraday, length=14))
|
|
|
|
def test_aroon(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.aroon(data=spy_daily_or_intraday, length=14))
|
|
|
|
def test_choppiness(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.choppiness(data=spy_daily_or_intraday, length=14))
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Volume #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
class TestVolume:
|
|
def test_obv(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.obv(data=spy_daily_or_intraday))
|
|
|
|
def test_ad(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.ad(data=spy_daily_or_intraday))
|
|
|
|
def test_adosc(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(
|
|
obb.technical.adosc(data=spy_daily_or_intraday, fast=3, slow=10)
|
|
)
|
|
|
|
def test_vwap(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.vwap(data=spy_daily_or_intraday))
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Volatility #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
class TestVolatility:
|
|
def test_atr(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.atr(data=spy_daily_or_intraday, length=14))
|
|
|
|
def test_realized_volatility(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(
|
|
obb.technical.realized_volatility(
|
|
data=spy_daily_or_intraday, model="yang_zhang", window=30
|
|
)
|
|
)
|
|
|
|
def test_realized_volatility_compare(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(
|
|
obb.technical.realized_volatility_compare(
|
|
data=spy_daily_or_intraday, window=30
|
|
)
|
|
)
|
|
|
|
def test_cones(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.cones(data=spy_daily_or_intraday, model="std"))
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Structure #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
class TestStructure:
|
|
def test_fib(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.fib(data=spy_daily_or_intraday, period=120))
|
|
|
|
def test_demark(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.demark(data=spy_daily_or_intraday))
|
|
|
|
@pytest.mark.parametrize(
|
|
"method", ["classic", "fibonacci", "woodie", "camarilla", "demark"]
|
|
)
|
|
def test_pivot_points(self, obb, spy_1d, method):
|
|
_assert_obbject(obb.technical.pivot_points(data=spy_1d, method=method))
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Statistics #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
class TestStatistics:
|
|
def test_clenow(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.clenow(data=spy_daily_or_intraday, period=90))
|
|
|
|
def test_drawdown(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.drawdown(data=spy_daily_or_intraday))
|
|
|
|
def test_returns_stats(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(
|
|
obb.technical.returns_stats(data=spy_daily_or_intraday, frequency="daily")
|
|
)
|
|
|
|
def test_stationarity(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.stationarity(data=spy_daily_or_intraday))
|
|
|
|
def test_hurst(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(obb.technical.hurst(data=spy_daily_or_intraday))
|
|
|
|
def test_autocorrelation(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(
|
|
obb.technical.autocorrelation(data=spy_daily_or_intraday, max_lag=20)
|
|
)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Signals (/signals subrouter) #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
class TestSignals:
|
|
def test_crossovers(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(
|
|
obb.technical.signals.crossovers(
|
|
data=spy_daily_or_intraday,
|
|
fast_length=20,
|
|
slow_length=50,
|
|
mamode="sma",
|
|
),
|
|
allow_empty=True,
|
|
)
|
|
|
|
@pytest.mark.parametrize("indicator", ["rsi", "mfi", "stoch", "williams_r", "cci"])
|
|
def test_oscillator_signals(self, obb, spy_1d, indicator):
|
|
_assert_obbject(
|
|
obb.technical.signals.oscillator_signals(
|
|
data=spy_1d, indicator=indicator, length=14
|
|
)
|
|
)
|
|
|
|
@pytest.mark.parametrize("method", ["donchian", "bollinger"])
|
|
def test_breakouts(self, obb, spy_1d, method):
|
|
_assert_obbject(
|
|
obb.technical.signals.breakouts(data=spy_1d, method=method, length=20),
|
|
allow_empty=True,
|
|
)
|
|
|
|
@pytest.mark.parametrize("indicator", ["rsi", "macd", "stoch", "cci"])
|
|
def test_divergences(self, obb, spy_1d, indicator):
|
|
_assert_obbject(
|
|
obb.technical.signals.divergences(
|
|
data=spy_1d,
|
|
indicator=indicator,
|
|
indicator_length=14,
|
|
lookback=60,
|
|
),
|
|
allow_empty=True,
|
|
)
|
|
|
|
def test_candlestick_patterns(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(
|
|
obb.technical.signals.candlestick_patterns(data=spy_daily_or_intraday),
|
|
allow_empty=True,
|
|
)
|
|
|
|
def test_regime(self, obb, spy_daily_or_intraday):
|
|
_assert_obbject(
|
|
obb.technical.signals.regime(data=spy_daily_or_intraday, length=14)
|
|
)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Multi-symbol endpoints (/multi subrouter + relative_rotation) #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
class TestMultiSymbol:
|
|
def test_correlation_pearson(self, obb, dow30_multi):
|
|
_assert_obbject(
|
|
obb.technical.correlation(
|
|
data=dow30_multi,
|
|
pairs=[("AAPL", "MSFT")],
|
|
method="pearson",
|
|
)
|
|
)
|
|
|
|
@pytest.mark.parametrize("method", ["pearson", "spearman", "kendall"])
|
|
def test_correlation_methods(self, obb, dow30_multi, method):
|
|
_assert_obbject(
|
|
obb.technical.correlation(
|
|
data=dow30_multi,
|
|
pairs=[("AAPL", "MSFT"), ("AAPL", "NVDA")],
|
|
method=method,
|
|
)
|
|
)
|
|
|
|
def test_correlation_matrix(self, obb, dow30_multi):
|
|
_assert_obbject(obb.technical.correlation_matrix(data=dow30_multi))
|
|
|
|
def test_correlation_matrix_window(self, obb, dow30_multi):
|
|
_assert_obbject(obb.technical.correlation_matrix(data=dow30_multi, window=60))
|
|
|
|
def test_screen_oversold_rsi(self, obb, dow30_multi):
|
|
result = obb.technical.screen(
|
|
data=dow30_multi,
|
|
conditions=[
|
|
{
|
|
"indicator": "rsi",
|
|
"column": "close_RSI_14",
|
|
"operator": "lt",
|
|
"value": 100.0,
|
|
}
|
|
],
|
|
)
|
|
_assert_obbject(result, allow_empty=True)
|
|
|
|
def test_screen_between(self, obb, dow30_multi):
|
|
result = obb.technical.screen(
|
|
data=dow30_multi,
|
|
conditions=[
|
|
{
|
|
"indicator": "rsi",
|
|
"column": "close_RSI_14",
|
|
"operator": "between",
|
|
"value": [0.0, 100.0],
|
|
}
|
|
],
|
|
)
|
|
_assert_obbject(result, allow_empty=True)
|
|
|
|
def test_screen_and_combine(self, obb, dow30_multi):
|
|
result = obb.technical.screen(
|
|
data=dow30_multi,
|
|
conditions=[
|
|
{
|
|
"indicator": "rsi",
|
|
"column": "close_RSI_14",
|
|
"operator": "lt",
|
|
"value": 100.0,
|
|
},
|
|
{
|
|
"indicator": "sma",
|
|
"column": "close_SMA_20",
|
|
"operator": "gt",
|
|
"value": 0.0,
|
|
},
|
|
],
|
|
combine="and",
|
|
)
|
|
_assert_obbject(result, allow_empty=True)
|
|
|
|
def test_relative_rotation(self, obb, dow30_multi):
|
|
# relative_rotation requires daily data with >252 bars; multi.csv is daily
|
|
# with ~1600 bars per symbol so it satisfies the constraint.
|
|
_assert_obbject(
|
|
obb.technical.relative_rotation(
|
|
data=dow30_multi,
|
|
benchmark="AAPL",
|
|
)
|
|
)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Multi-indicator compose #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
class TestMultiCompose:
|
|
def test_multi_two_indicators(self, obb, spy_1d):
|
|
result = obb.technical.multi(
|
|
data=spy_1d,
|
|
indicators=[
|
|
{"indicator": "rsi", "params": {"length": 14}},
|
|
{"indicator": "sma", "params": {"length": 50}},
|
|
],
|
|
)
|
|
_assert_obbject(result)
|
|
|
|
def test_multi_intraday(self, obb, spy_30m):
|
|
result = obb.technical.multi(
|
|
data=spy_30m,
|
|
indicators=[
|
|
{"indicator": "ema", "params": {"length": 20}},
|
|
{"indicator": "macd", "params": {"fast": 12, "slow": 26, "signal": 9}},
|
|
],
|
|
)
|
|
_assert_obbject(result)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Catalog #
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
|
class TestCatalog:
|
|
def test_indicators_all(self, obb):
|
|
result = obb.technical.indicators(category="all")
|
|
_assert_obbject(result)
|
|
|
|
@pytest.mark.parametrize(
|
|
"category",
|
|
[
|
|
"overlay",
|
|
"oscillator",
|
|
"volatility",
|
|
"volume",
|
|
"trend",
|
|
"signal",
|
|
"structure",
|
|
"stats",
|
|
"multi",
|
|
],
|
|
)
|
|
def test_indicators_by_category(self, obb, category):
|
|
result = obb.technical.indicators(category=category)
|
|
_assert_obbject(result)
|