mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-28 13:01:38 +00:00
* refactor quantitative for v5 * add factors endpoint and add a few charts --------- Co-authored-by: deeleeramone <>
110 lines
3.9 KiB
Python
110 lines
3.9 KiB
Python
"""Tests for ``openbb_quantitative.factors``."""
|
|
|
|
from math import isfinite
|
|
|
|
import pandas as pd
|
|
import statsmodels.api as sm
|
|
from openbb_core.app.utils import df_to_basemodel
|
|
|
|
from openbb_quantitative import factors as factors_module
|
|
|
|
|
|
def _params(target_data, factor_data, **kwargs):
|
|
return factors_module.FactorRegressionQueryParams(
|
|
data=target_data, factors_data=factor_data, target="close", **kwargs
|
|
)
|
|
|
|
|
|
def test_factors_happy_path(target_returns_data, factor_matrix_data):
|
|
"""Returns one row per (period, regressor) with finite stats."""
|
|
out = factors_module.factors(_params(target_returns_data, factor_matrix_data))
|
|
results = out.results
|
|
assert isinstance(results, list)
|
|
assert results
|
|
|
|
n_regressors = 4 # const + f1 + f2 + rf
|
|
n_periods = len(set(r.period for r in results))
|
|
assert len(results) == n_periods * n_regressors
|
|
|
|
for row in results:
|
|
assert isinstance(row, factors_module.FactorRegressionData)
|
|
for field in ("coefficient", "p_value", "lower_ci", "upper_ci", "r_squared"):
|
|
value = getattr(row, field)
|
|
assert isinstance(value, float)
|
|
assert isfinite(value)
|
|
|
|
|
|
def test_factors_max_window_matches_statsmodels(
|
|
target_returns_df, factor_matrix_df, target_returns_data, factor_matrix_data
|
|
):
|
|
"""The Max-period coefficients match a direct statsmodels OLS fit."""
|
|
out = factors_module.factors(
|
|
_params(target_returns_data, factor_matrix_data, risk_free_column="rf")
|
|
)
|
|
df = pd.DataFrame([r.model_dump() for r in out.results])
|
|
max_rows = df[df.period == "Max"].set_index("factor")
|
|
|
|
aligned = factor_matrix_df.set_index("date").drop(columns=["rf"]).astype(float)
|
|
target = target_returns_df.set_index("date")["close"].astype(
|
|
float
|
|
) - factor_matrix_df.set_index("date")["rf"].astype(float)
|
|
x = sm.add_constant(aligned)
|
|
model = sm.OLS(target, x).fit()
|
|
|
|
for name in model.params.index:
|
|
assert max_rows.loc[name, "coefficient"] == pytest.approx( # type: ignore[name-defined]
|
|
float(model.params[name]), abs=1e-9
|
|
)
|
|
|
|
|
|
def test_factors_with_risk_free(target_returns_data, factor_matrix_data):
|
|
"""When ``risk_free_column`` is set the rf column drops out of the regressors."""
|
|
out = factors_module.factors(
|
|
_params(target_returns_data, factor_matrix_data, risk_free_column="rf")
|
|
)
|
|
factor_names = {r.factor for r in out.results}
|
|
assert "rf" not in factor_names
|
|
assert factor_names == {"const", "f1", "f2"}
|
|
|
|
|
|
def test_factors_specific_periods(target_returns_data, factor_matrix_data):
|
|
"""The ``periods`` parameter restricts which look-back windows are returned."""
|
|
out = factors_module.factors(
|
|
_params(target_returns_data, factor_matrix_data, periods=["Max"])
|
|
)
|
|
assert {r.period for r in out.results} == {"Max"}
|
|
|
|
|
|
def test_factors_skips_too_short_windows(factor_matrix_df, factor_matrix_data):
|
|
"""Periods whose slice contains fewer obs than regressors+1 are skipped."""
|
|
# Build a target whose factor overlap is two days of business activity.
|
|
short_dates = factor_matrix_df["date"].iloc[:2]
|
|
short_target = df_to_basemodel(
|
|
pd.DataFrame({"date": short_dates, "close": [0.01, -0.005]})
|
|
)
|
|
out = factors_module.factors(
|
|
_params(short_target, factor_matrix_data, periods=["1 Month", "Max"])
|
|
)
|
|
# 2 observations cannot fit a 4-regressor model -> both periods are skipped.
|
|
assert out.results == []
|
|
|
|
|
|
def test_factors_default_periods():
|
|
"""The default ``periods`` value enumerates the six named look-back windows."""
|
|
params = factors_module.FactorRegressionQueryParams(
|
|
data=[], factors_data=[], target="close"
|
|
)
|
|
assert params.periods == [
|
|
"1 Month",
|
|
"3 Month",
|
|
"YTD",
|
|
"1 Year",
|
|
"3 Year",
|
|
"Max",
|
|
]
|
|
|
|
|
|
# pytest is imported here (not at the top) so the local _params helper above
|
|
# can reference it without surfacing in autocompletion.
|
|
import pytest # noqa: E402
|