Files
OpenBB/openbb_platform/extensions/technical/tests/test_technical_views.py
Danglewood d3d33a219b [V5] openbb-technical: Refactor for V5 & Add More Quick Win Endpoints (#7502)
* 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 <>
2026-05-29 10:48:51 +03:00

501 lines
16 KiB
Python

"""Tests for openbb_technical.technical_views."""
from __future__ import annotations
import sys
import types
from unittest.mock import MagicMock
import numpy as np
import pandas as pd
import pytest
from openbb_core.provider.abstract.data import Data
@pytest.fixture(autouse=True)
def stub_openbb_charting(monkeypatch):
"""Stub openbb_charting module tree so technical_views imports succeed."""
mod_names = [
"openbb_charting",
"openbb_charting.charts",
"openbb_charting.charts.relative_rotation",
"openbb_charting.core",
"openbb_charting.core.chart_style",
"openbb_charting.core.openbb_figure",
"openbb_charting.core.plotly_ta",
"openbb_charting.core.plotly_ta.ta_class",
"openbb_charting.core.to_chart",
"openbb_charting.styles",
"openbb_charting.styles.colors",
]
fake_modules = {name: types.ModuleType(name) for name in mod_names}
def _make_figure(*_args, **_kwargs):
figure = MagicMock(name="OpenBBFigure")
figure.create_subplots.return_value = figure
figure.show.return_value.to_plotly_json.return_value = {"plotly": "json"}
figure.to_plotly_json.return_value = {"plotly": "json"}
return figure
def _make_ta(*_args, **_kwargs):
ta = MagicMock(name="PlotlyTA")
ta.plot.return_value = _make_figure()
return ta
def _make_style(*_args, **_kwargs):
style = MagicMock(name="ChartStyle")
style.plotly_template = {"layout": {}}
style.plt_style = "light"
return style
fake_modules["openbb_charting.core.openbb_figure"].OpenBBFigure = _make_figure
fake_modules["openbb_charting.core.plotly_ta.ta_class"].PlotlyTA = _make_ta
fake_modules["openbb_charting.core.chart_style"].ChartStyle = _make_style
fake_modules["openbb_charting.core.to_chart"].to_chart = MagicMock(
return_value=(_make_figure(), MagicMock())
)
fake_modules["openbb_charting.styles.colors"].LARGE_CYCLER = [
"red",
"blue",
"green",
] * 50
rr_module = fake_modules["openbb_charting.charts.relative_rotation"]
rr_module.create_rrg_with_tails = MagicMock(return_value=MagicMock())
rr_module.create_rrg_without_tails = MagicMock(return_value=MagicMock())
fake_modules["openbb_charting.charts"].relative_rotation = rr_module
for name, mod in fake_modules.items():
monkeypatch.setitem(sys.modules, name, mod)
monkeypatch.delitem(sys.modules, "openbb_technical.technical_views", raising=False)
@pytest.fixture
def ta_accessor(monkeypatch):
"""Patch the pandas_ta DataFrame accessor used by _ta_ma."""
class _FakeTa:
def __init__(self, df):
self._df = df
def __getattr__(self, _name):
def _runner(length=None, offset=None):
values = np.arange(1, len(self._df) + 1, dtype="float64")
return pd.Series(values, index=self._df.index)
return _runner
monkeypatch.setattr(
pd.DataFrame,
"ta",
property(lambda self: _FakeTa(self)),
raising=False,
)
def _records_to_data(records):
return [Data(**row) for row in records]
def _rr_obbject(empty=False):
obb = MagicMock()
if empty:
obb.rs_ratios = []
obb.rs_momentum = []
else:
obb.rs_ratios = [
Data(date="2021-01-01", SPY=1.0, QQQ=1.1),
Data(date="2021-01-02", SPY=1.2, QQQ=1.3),
]
obb.rs_momentum = [
Data(date="2021-01-01", SPY=0.5, QQQ=0.6),
Data(date="2021-01-02", SPY=0.7, QQQ=0.8),
]
obb.benchmark = "SPY"
obb.study = "price"
return obb
class TestTechnicalSma:
def test_uses_dataframe_input(self, ohlcv_df, ta_accessor):
from openbb_technical.technical_views import TechnicalViews
fig, content = TechnicalViews.technical_sma(data=ohlcv_df.reset_index())
assert fig is not None
assert isinstance(content, dict)
def test_existing_ma_type_kwarg_preserved(self, ohlcv_df, ta_accessor):
from openbb_technical.technical_views import TechnicalViews
fig, _ = TechnicalViews.technical_sma(data=ohlcv_df, ma_type="ema")
assert fig is not None
class TestTechnicalEma:
def test_default_ma_type_set(self, ohlcv_df, ta_accessor):
from openbb_technical.technical_views import TechnicalViews
fig, _ = TechnicalViews.technical_ema(data=ohlcv_df)
assert fig is not None
class TestTechnicalHma:
def test_default_ma_type_set(self, ohlcv_df, ta_accessor):
from openbb_technical.technical_views import TechnicalViews
fig, _ = TechnicalViews.technical_hma(data=ohlcv_df)
assert fig is not None
class TestTechnicalWma:
def test_default_ma_type_set(self, ohlcv_df, ta_accessor):
from openbb_technical.technical_views import TechnicalViews
fig, _ = TechnicalViews.technical_wma(data=ohlcv_df)
assert fig is not None
class TestTechnicalZlma:
def test_default_ma_type_set(self, ohlcv_df, ta_accessor):
from openbb_technical.technical_views import TechnicalViews
fig, _ = TechnicalViews.technical_zlma(data=ohlcv_df)
assert fig is not None
class TestTaMaHelper:
def test_from_obbject_item(self, ohlcv_records, ta_accessor):
from openbb_technical.technical_views import _ta_ma
fig, content = _ta_ma(obbject_item=_records_to_data(ohlcv_records))
assert fig is not None
assert isinstance(content, dict)
def test_from_list_of_data(self, ohlcv_records, ta_accessor):
from openbb_technical.technical_views import _ta_ma
fig, _ = _ta_ma(data=_records_to_data(ohlcv_records))
assert fig is not None
def test_with_index_column_already_set(self, ohlcv_df, ta_accessor):
from openbb_technical.technical_views import _ta_ma
fig, _ = _ta_ma(data=ohlcv_df, index="date")
assert fig is not None
def test_multiple_ma_types_and_windows(self, ohlcv_df, ta_accessor):
from openbb_technical.technical_views import _ta_ma
fig, _ = _ta_ma(
data=ohlcv_df.reset_index(),
ma_type="sma,ema",
length=[10, 20],
offset=0,
title="Custom",
)
assert fig is not None
def test_target_fallback_to_close(self, ohlcv_df, ta_accessor):
from openbb_technical.technical_views import _ta_ma
fig, _ = _ta_ma(data=ohlcv_df.reset_index(), target="not_a_column")
assert fig is not None
def test_missing_target_and_close_raises(self, ta_accessor):
from openbb_technical.technical_views import _ta_ma
df = pd.DataFrame({"price": np.arange(1.0, 100.0)})
with pytest.raises(ValueError, match="not found"):
_ta_ma(data=df, target="missing")
def test_dropnan_branch(self, ohlcv_df, ta_accessor):
from openbb_technical.technical_views import _ta_ma
fig, _ = _ta_ma(data=ohlcv_df.reset_index(), dropnan=True)
assert fig is not None
def test_candles_branch(self, ohlcv_df, ta_accessor):
from openbb_technical.technical_views import _ta_ma
fig, _ = _ta_ma(
data=ohlcv_df.reset_index(),
candles=True,
volume=True,
)
assert fig is not None
def test_ma_type_none_defaults_to_sma(self, ohlcv_df, ta_accessor):
from openbb_technical.technical_views import _ta_ma
fig, _ = _ta_ma(data=ohlcv_df, ma_type=None, length=None, target=None)
assert fig is not None
def test_dark_theme_branch(self, ohlcv_df, ta_accessor, monkeypatch):
from openbb_technical import technical_views
def _dark_style(*_a, **_k):
style = MagicMock()
style.plotly_template = {"layout": {}}
style.plt_style = "dark"
return style
monkeypatch.setattr(
sys.modules["openbb_charting.core.chart_style"],
"ChartStyle",
_dark_style,
)
fig, _ = technical_views._ta_ma(data=ohlcv_df)
assert fig is not None
def test_scalar_window_int(self, ohlcv_df, ta_accessor):
from openbb_technical.technical_views import _ta_ma
fig, _ = _ta_ma(data=ohlcv_df, length=10)
assert fig is not None
class TestTechnicalAroon:
def test_with_dataframe(self, ohlcv_df):
from openbb_technical.technical_views import TechnicalViews
fig, content = TechnicalViews.technical_aroon(
data=ohlcv_df, symbol="SPY", volume=True
)
assert fig is not None
assert isinstance(content, dict)
def test_with_obbject_item(self, ohlcv_records):
from openbb_technical.technical_views import TechnicalViews
fig, _ = TechnicalViews.technical_aroon(
obbject_item=_records_to_data(ohlcv_records)
)
assert fig is not None
def test_date_column_reset(self, ohlcv_df):
from openbb_technical.technical_views import TechnicalViews
fig, _ = TechnicalViews.technical_aroon(data=ohlcv_df.reset_index())
assert fig is not None
def test_multi_symbol_raises(self, ohlcv_df):
from openbb_technical.technical_views import TechnicalViews
df = ohlcv_df.copy()
df["symbol"] = ["A", "B"] * (len(df) // 2) + ["A"]
with pytest.raises(ValueError, match="one symbol"):
TechnicalViews.technical_aroon(data=df)
class TestTechnicalMacd:
def test_with_dataframe(self, ohlcv_df):
from openbb_technical.technical_views import TechnicalViews
fig, content = TechnicalViews.technical_macd(
data=ohlcv_df, symbol="spy", volume=True
)
assert fig is not None
assert isinstance(content, dict)
def test_with_obbject_item(self, ohlcv_records):
from openbb_technical.technical_views import TechnicalViews
fig, _ = TechnicalViews.technical_macd(
obbject_item=_records_to_data(ohlcv_records)
)
assert fig is not None
def test_date_column_reset(self, ohlcv_df):
from openbb_technical.technical_views import TechnicalViews
fig, _ = TechnicalViews.technical_macd(data=ohlcv_df.reset_index())
assert fig is not None
def test_multi_symbol_raises(self, ohlcv_df):
from openbb_technical.technical_views import TechnicalViews
df = ohlcv_df.copy()
df["symbol"] = ["A", "B"] * (len(df) // 2) + ["A"]
with pytest.raises(ValueError, match="one symbol"):
TechnicalViews.technical_macd(data=df)
class TestTechnicalAdx:
def test_with_dataframe(self, ohlcv_df):
from openbb_technical.technical_views import TechnicalViews
fig, content = TechnicalViews.technical_adx(data=ohlcv_df, symbol="SPY")
assert fig is not None
assert isinstance(content, dict)
def test_with_obbject_item(self, ohlcv_records):
from openbb_technical.technical_views import TechnicalViews
fig, _ = TechnicalViews.technical_adx(
obbject_item=_records_to_data(ohlcv_records)
)
assert fig is not None
def test_date_column_reset(self, ohlcv_df):
from openbb_technical.technical_views import TechnicalViews
fig, _ = TechnicalViews.technical_adx(data=ohlcv_df.reset_index())
assert fig is not None
def test_multi_symbol_raises(self, ohlcv_df):
from openbb_technical.technical_views import TechnicalViews
df = ohlcv_df.copy()
df["symbol"] = ["A", "B"] * (len(df) // 2) + ["A"]
with pytest.raises(ValueError, match="one symbol"):
TechnicalViews.technical_adx(data=df)
class TestTechnicalRsi:
def test_with_dataframe(self, ohlcv_df):
from openbb_technical.technical_views import TechnicalViews
fig, content = TechnicalViews.technical_rsi(data=ohlcv_df, symbol="spy")
assert fig is not None
assert isinstance(content, dict)
def test_with_obbject_item(self, ohlcv_records):
from openbb_technical.technical_views import TechnicalViews
fig, _ = TechnicalViews.technical_rsi(
obbject_item=_records_to_data(ohlcv_records)
)
assert fig is not None
def test_date_column_reset(self, ohlcv_df):
from openbb_technical.technical_views import TechnicalViews
fig, _ = TechnicalViews.technical_rsi(data=ohlcv_df.reset_index())
assert fig is not None
def test_multi_symbol_raises(self, ohlcv_df):
from openbb_technical.technical_views import TechnicalViews
df = ohlcv_df.copy()
df["symbol"] = ["A", "B"] * (len(df) // 2) + ["A"]
with pytest.raises(ValueError, match="one symbol"):
TechnicalViews.technical_rsi(data=df)
class TestTechnicalCones:
def _cones_df(self):
return pd.DataFrame(
{
"window": [3, 10, 30, 60, 90],
"realized": [0.1, 0.2, 0.3, 0.25, 0.2],
"min": [0.05, 0.1, 0.15, 0.12, 0.1],
"median": [0.1, 0.2, 0.25, 0.22, 0.18],
"max": [0.2, 0.3, 0.4, 0.35, 0.3],
}
)
def test_with_dataframe(self):
from openbb_technical.technical_views import TechnicalViews
fig, content = TechnicalViews.technical_cones(
data=self._cones_df(), symbol="SPY", model="std", title="Cones"
)
assert fig is not None
assert isinstance(content, dict)
def test_with_obbject_item(self):
from openbb_technical.technical_views import TechnicalViews
records = self._cones_df().to_dict(orient="records")
items = [Data(**row) for row in records]
fig, _ = TechnicalViews.technical_cones(obbject_item=items)
assert fig is not None
def test_defaults_branch(self):
from openbb_technical.technical_views import TechnicalViews
fig, _ = TechnicalViews.technical_cones(data=self._cones_df())
assert fig is not None
def test_invalid_format_raises(self):
from openbb_technical.technical_views import TechnicalViews
df = pd.DataFrame({"window": [1, 2], "foo": [0.1, 0.2]})
with pytest.raises(ValueError, match="expected format"):
TechnicalViews.technical_cones(data=df)
def test_dark_theme_branch(self, monkeypatch):
from openbb_technical.technical_views import TechnicalViews
def _dark_style(*_a, **_k):
style = MagicMock()
style.plotly_template = {"layout": {}}
style.plt_style = "dark"
return style
monkeypatch.setattr(
sys.modules["openbb_charting.core.chart_style"],
"ChartStyle",
_dark_style,
)
fig, _ = TechnicalViews.technical_cones(data=self._cones_df())
assert fig is not None
class TestTechnicalRelativeRotation:
def test_with_tails_default(self):
from openbb_technical.technical_views import TechnicalViews
fig, content = TechnicalViews.technical_relative_rotation(
obbject_item=_rr_obbject(), title="RRG"
)
assert fig is not None
assert isinstance(content, dict)
def test_show_tails_false_with_date(self):
from openbb_technical.technical_views import TechnicalViews
fig, _ = TechnicalViews.technical_relative_rotation(
obbject_item=_rr_obbject(),
show_tails=False,
date="2021-01-02",
study="price",
)
assert fig is not None
def test_date_forces_no_tails(self):
from openbb_technical.technical_views import TechnicalViews
fig, _ = TechnicalViews.technical_relative_rotation(
obbject_item=_rr_obbject(),
date="2021-01-02",
tail_periods=8,
tail_interval="day",
)
assert fig is not None
def test_empty_data_raises(self):
from openbb_technical.technical_views import TechnicalViews
with pytest.raises(RuntimeError, match="No data"):
TechnicalViews.technical_relative_rotation(
obbject_item=_rr_obbject(empty=True)
)
def test_dark_theme_branch(self, monkeypatch):
from openbb_technical.technical_views import TechnicalViews
def _dark_style(*_a, **_k):
style = MagicMock()
style.plotly_template = {"layout": {}}
style.plt_style = "dark"
return style
monkeypatch.setattr(
sys.modules["openbb_charting.core.chart_style"],
"ChartStyle",
_dark_style,
)
fig, _ = TechnicalViews.technical_relative_rotation(obbject_item=_rr_obbject())
assert fig is not None