mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-28 13:01:38 +00:00
* merge branch develop with main * bump desktop version * fix the merge * sync Cargo.lock to desktop 2.0.0 * fix cli conftest.py --------- Co-authored-by: deeleeramone <>
164 lines
5.5 KiB
Python
164 lines
5.5 KiB
Python
"""Shared deterministic real-data fixtures for the charting test suite."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import types
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
import pytest
|
|
from openbb_core.provider.abstract.data import Data
|
|
|
|
SYMBOLS = ("AAA", "BBB", "CCC", "DDD")
|
|
|
|
|
|
def _install_fake_pywry() -> None:
|
|
"""Install a minimal fake ``pywry`` so the GUI backend runs headlessly.
|
|
|
|
``pywry`` is an optional GUI dependency with no wheels for every supported
|
|
Python (e.g. 3.14), so CI cannot install it. It is the one true external
|
|
boundary — the window/websocket layer — so a faithful in-memory stand-in is
|
|
the correct test double. Installing it unconditionally keeps backend tests
|
|
deterministic across every environment.
|
|
"""
|
|
pywry = types.ModuleType("pywry")
|
|
pywry._OPENBB_FAKE = True # type: ignore[attr-defined]
|
|
|
|
class _Component:
|
|
def __init__(self, **kwargs):
|
|
self.__dict__.update(kwargs)
|
|
|
|
class Toolbar(_Component):
|
|
def __init__(self, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self.items = kwargs.get("items", [])
|
|
|
|
class _App:
|
|
def __init__(self, **kwargs):
|
|
self.__dict__.update(kwargs)
|
|
self.theme = kwargs.get("theme")
|
|
|
|
def show_plotly(self, **kwargs):
|
|
return None
|
|
|
|
def show_dataframe(self, **kwargs):
|
|
return None
|
|
|
|
def show(self, **kwargs):
|
|
return None
|
|
|
|
def emit(self, *args, **kwargs):
|
|
return None
|
|
|
|
def close(self, *args, **kwargs):
|
|
return None
|
|
|
|
class ThemeMode:
|
|
DARK = "dark"
|
|
LIGHT = "light"
|
|
|
|
pywry.PyWry = _App # type: ignore[attr-defined]
|
|
pywry.ThemeMode = ThemeMode # type: ignore[attr-defined]
|
|
pywry.Toolbar = Toolbar # type: ignore[attr-defined]
|
|
pywry.Button = _Component # type: ignore[attr-defined]
|
|
pywry.Div = _Component # type: ignore[attr-defined]
|
|
pywry.TextArea = _Component # type: ignore[attr-defined]
|
|
pywry.PlotlyConfig = _Component # type: ignore[attr-defined]
|
|
|
|
grid = types.ModuleType("pywry.grid")
|
|
|
|
class _GridData:
|
|
def __init__(self, frame):
|
|
self.columns = list(frame.columns)
|
|
self.index_columns = []
|
|
self.column_types = {col: "text" for col in frame.columns}
|
|
self.row_data = frame.to_dict(orient="records")
|
|
|
|
grid.normalize_data = _GridData # type: ignore[attr-defined]
|
|
grid.build_column_defs = lambda columns, index_columns, column_types: [ # type: ignore[attr-defined]
|
|
{"field": col} for col in columns
|
|
]
|
|
pywry.grid = grid # type: ignore[attr-defined]
|
|
|
|
sys.modules["pywry"] = pywry
|
|
sys.modules["pywry.grid"] = grid
|
|
|
|
|
|
_install_fake_pywry()
|
|
|
|
FIB_LEVELS = (0, 0.235, 0.382, 0.5, 0.618, 0.65, 1)
|
|
|
|
|
|
def _calculate_fib_levels(data, close_col, limit=120, start_date=None, end_date=None):
|
|
"""Return ``calculate_fib_levels`` outputs for the trailing ``limit`` closes."""
|
|
close = data.iloc[-limit:][close_col]
|
|
lvl_text = "left" if close.idxmin() < close.idxmax() else "right"
|
|
first, last = sorted((close.idxmin(), close.idxmax()))
|
|
min_pr, max_pr = close[first], close[last]
|
|
levels = [max_pr - (max_pr - min_pr) * level for level in FIB_LEVELS]
|
|
return pd.DataFrame({"Price": levels}), first, last, min_pr, max_pr, lvl_text
|
|
|
|
|
|
def _clenow_momentum(values, window=90):
|
|
"""Return ``clenow_momentum`` outputs for the trailing ``window`` values."""
|
|
return 0.0, 0.0, pd.Series(np.log(values.to_numpy()[-window:]))
|
|
|
|
|
|
@pytest.fixture
|
|
def technical_helpers(monkeypatch) -> types.ModuleType:
|
|
"""Stub ``openbb_technical.helpers`` with deterministic Fib and Clenow outputs."""
|
|
helpers = types.ModuleType("openbb_technical.helpers")
|
|
helpers.calculate_fib_levels = _calculate_fib_levels # type: ignore[attr-defined]
|
|
helpers.clenow_momentum = _clenow_momentum # type: ignore[attr-defined]
|
|
monkeypatch.setitem(sys.modules, "openbb_technical.helpers", helpers)
|
|
return helpers
|
|
|
|
|
|
def _make_ohlcv(
|
|
seed: int, periods: int = 200, start: str = "2023-01-01"
|
|
) -> pd.DataFrame:
|
|
"""Build a deterministic, valid OHLCV frame with a ``date`` index."""
|
|
rng = np.random.default_rng(seed)
|
|
idx = pd.date_range(start, periods=periods, freq="D")
|
|
close = 100 + np.cumsum(rng.normal(0, 1, periods))
|
|
open_ = close + rng.normal(0, 0.5, periods)
|
|
high = np.maximum(open_, close) + np.abs(rng.normal(0, 0.5, periods))
|
|
low = np.minimum(open_, close) - np.abs(rng.normal(0, 0.5, periods))
|
|
volume = rng.integers(1_000_000, 5_000_000, periods)
|
|
frame = pd.DataFrame(
|
|
{"open": open_, "high": high, "low": low, "close": close, "volume": volume},
|
|
index=idx,
|
|
)
|
|
frame.index.name = "date"
|
|
return frame
|
|
|
|
|
|
@pytest.fixture
|
|
def ohlcv_df() -> pd.DataFrame:
|
|
"""A single-symbol OHLCV DataFrame indexed by date (200 rows)."""
|
|
return _make_ohlcv(seed=42)
|
|
|
|
|
|
@pytest.fixture
|
|
def ohlcv_records(ohlcv_df) -> list[dict]:
|
|
"""The OHLCV frame as a list of ``date``-stamped record dicts."""
|
|
out = ohlcv_df.reset_index()
|
|
out["date"] = out["date"].dt.strftime("%Y-%m-%d")
|
|
return out.to_dict(orient="records")
|
|
|
|
|
|
@pytest.fixture
|
|
def ohlcv_data(ohlcv_records) -> list[Data]:
|
|
"""The OHLCV records as real ``Data`` models."""
|
|
return [Data.model_validate(row) for row in ohlcv_records]
|
|
|
|
|
|
@pytest.fixture
|
|
def multi_close_df() -> pd.DataFrame:
|
|
"""A multi-symbol close-price frame (wide), indexed by date."""
|
|
frames = {sym: _make_ohlcv(seed=i)["close"] for i, sym in enumerate(SYMBOLS)}
|
|
wide = pd.DataFrame(frames)
|
|
wide.index.name = "date"
|
|
return wide
|