Files
OpenBB/openbb_platform/extensions/quantitative/tests/test_views.py
Danglewood cd7d602b8f [V5] openbb-quantitative: Refactor for V5 (#7506)
* refactor quantitative for v5

* add factors endpoint and add a few charts

---------

Co-authored-by: deeleeramone <>
2026-05-29 10:46:25 +03:00

260 lines
9.3 KiB
Python

"""Tests for the chart view functions and the ``QuantitativeViews`` dispatcher."""
import pandas as pd
import pytest
from openbb_core.app.utils import df_to_basemodel
from openbb_quantitative import (
attribution as att_module,
factors as factors_module,
risk_decomposition as rd_module,
rolling,
)
from openbb_quantitative.quantitative_views import QuantitativeViews
from openbb_quantitative.views.attribution_bar import attribution_bar
from openbb_quantitative.views.factors_heatmap import factors_heatmap
from openbb_quantitative.views.risk_decomposition_bar import risk_decomposition_bar
from openbb_quantitative.views.rolling_factors_line import rolling_factors_line
# ---------------------------------------------------------------------------
# Helpers — run the matching endpoint once per session and reuse the rows.
# ---------------------------------------------------------------------------
@pytest.fixture(scope="module")
def factors_results(target_returns_data, factor_matrix_data):
out = factors_module.factors(
factors_module.FactorRegressionQueryParams(
data=target_returns_data,
factors_data=factor_matrix_data,
target="close",
risk_free_column="rf",
)
)
return out.results
@pytest.fixture(scope="module")
def rd_results(target_returns_data, factor_matrix_data):
out = rd_module.risk_decomposition(
rd_module.RiskDecompositionQueryParams(
data=target_returns_data,
factors_data=factor_matrix_data,
target="close",
risk_free_column="rf",
)
)
return out.results
@pytest.fixture(scope="module")
def attribution_results(target_returns_data, factor_matrix_data):
out = att_module.attribution(
att_module.ReturnAttributionQueryParams(
data=target_returns_data,
factors_data=factor_matrix_data,
target="close",
risk_free_column="rf",
)
)
return out.results
@pytest.fixture(scope="module")
def rolling_results(target_returns_data, factor_matrix_data):
out = rolling.factors(
rolling.RollingFactorsQueryParams(
data=target_returns_data,
factors_data=factor_matrix_data,
target="close",
window=252,
step=63,
risk_free_column="rf",
)
)
return out.results
# ---------------------------------------------------------------------------
# factors_heatmap
# ---------------------------------------------------------------------------
def test_factors_heatmap_via_obbject(factors_results):
fig, content = factors_heatmap(obbject_item=factors_results)
trace = content["data"][0]
assert trace["type"] == "heatmap"
assert set(trace["x"]) >= {"f1", "f2", "Constant"}
assert (
content["layout"]["title"]["text"]
== "Factor Regression Coefficients & P-Values"
)
def test_factors_heatmap_via_data_list(factors_results):
payload = [r.model_dump() for r in factors_results]
fig, content = factors_heatmap(data=df_to_basemodel(pd.DataFrame(payload)))
assert content["data"][0]["type"] == "heatmap"
def test_factors_heatmap_via_dataframe(factors_results):
df = pd.DataFrame([r.model_dump() for r in factors_results])
fig, content = factors_heatmap(
data=df, title="Custom Title", layout_kwargs={"height": 700}
)
assert content["layout"]["title"]["text"] == "Custom Title"
assert content["layout"]["height"] == 700
def test_factors_heatmap_missing_columns():
bad = pd.DataFrame({"period": ["Max"], "factor": ["f1"]})
with pytest.raises(ValueError, match="factors_heatmap requires"):
factors_heatmap(data=bad)
# ---------------------------------------------------------------------------
# risk_decomposition_bar
# ---------------------------------------------------------------------------
def test_risk_decomposition_bar_via_obbject(rd_results):
fig, content = risk_decomposition_bar(obbject_item=rd_results)
assert content["layout"]["barmode"] == "stack"
trace_names = [t["name"] for t in content["data"]]
assert trace_names[-1] == "Residual"
def test_risk_decomposition_bar_via_data_list(rd_results):
payload = df_to_basemodel(pd.DataFrame([r.model_dump() for r in rd_results]))
fig, content = risk_decomposition_bar(data=payload, layout_kwargs={"width": 800})
assert content["layout"]["width"] == 800
def test_risk_decomposition_bar_via_dataframe(rd_results):
df = pd.DataFrame([r.model_dump() for r in rd_results])
fig, content = risk_decomposition_bar(data=df, title="Risk Shares")
assert content["layout"]["title"]["text"] == "Risk Shares"
def test_risk_decomposition_bar_missing_columns():
bad = pd.DataFrame({"period": ["Max"], "factor": ["f1"]})
with pytest.raises(ValueError, match="risk_decomposition_bar requires"):
risk_decomposition_bar(data=bad)
# ---------------------------------------------------------------------------
# attribution_bar
# ---------------------------------------------------------------------------
def test_attribution_bar_via_obbject(attribution_results):
fig, content = attribution_bar(obbject_item=attribution_results)
trace_names = [t["name"] for t in content["data"]]
assert "Alpha" in trace_names
assert "Residual" in trace_names
# Alpha and Residual are last in the legend ordering.
assert trace_names[-2:] == ["Alpha", "Residual"]
def test_attribution_bar_via_data_list(attribution_results):
payload = df_to_basemodel(
pd.DataFrame([r.model_dump() for r in attribution_results])
)
fig, content = attribution_bar(data=payload)
assert content["layout"]["barmode"] == "relative"
def test_attribution_bar_via_dataframe(attribution_results):
df = pd.DataFrame([r.model_dump() for r in attribution_results])
fig, content = attribution_bar(
data=df, title="Return Attribution", layout_kwargs={"height": 650}
)
assert content["layout"]["title"]["text"] == "Return Attribution"
assert content["layout"]["height"] == 650
def test_attribution_bar_missing_columns():
bad = pd.DataFrame({"period": ["Max"], "factor": ["f1"]})
with pytest.raises(ValueError, match="attribution_bar requires"):
attribution_bar(data=bad)
# ---------------------------------------------------------------------------
# rolling_factors_line
# ---------------------------------------------------------------------------
def test_rolling_factors_line_via_obbject(rolling_results):
"""By default the intercept is dropped from the stack."""
fig, content = rolling_factors_line(obbject_item=rolling_results)
trace_names = [t["name"] for t in content["data"]]
assert "Intercept" not in trace_names
assert "f1" in trace_names
assert "f2" in trace_names
assert all(t.get("stackgroup") == "one" for t in content["data"])
# No separate legend — every band labeled inline via annotations.
assert content["layout"]["showlegend"] is False
labels = {a["text"] for a in content["layout"]["annotations"]}
assert any("f1" in lbl for lbl in labels)
def test_rolling_factors_line_include_intercept(rolling_results):
"""``include_intercept=True`` adds the intercept as another stacked band."""
fig, content = rolling_factors_line(
obbject_item=rolling_results, include_intercept=True
)
trace_names = [t["name"] for t in content["data"]]
assert "Intercept" in trace_names
def test_rolling_factors_line_via_data_list(rolling_results):
payload = df_to_basemodel(pd.DataFrame([r.model_dump() for r in rolling_results]))
fig, content = rolling_factors_line(data=payload)
assert all(t["type"] == "scatter" for t in content["data"])
def test_rolling_factors_line_via_dataframe(rolling_results):
df = pd.DataFrame([r.model_dump() for r in rolling_results])
fig, content = rolling_factors_line(
data=df, title="Rolling Betas", layout_kwargs={"width": 900}
)
assert content["layout"]["title"]["text"] == "Rolling Betas"
assert content["layout"]["width"] == 900
def test_rolling_factors_line_missing_columns():
bad = pd.DataFrame({"date": ["2020-01-01"], "factor": ["f1"]})
with pytest.raises(ValueError, match="rolling_factors_line requires"):
rolling_factors_line(data=bad)
# ---------------------------------------------------------------------------
# QuantitativeViews dispatcher
# ---------------------------------------------------------------------------
def test_views_quantitative_factors(factors_results):
fig, content = QuantitativeViews.quantitative_factors(obbject_item=factors_results)
assert content["data"][0]["type"] == "heatmap"
def test_views_quantitative_risk_decomposition(rd_results):
fig, content = QuantitativeViews.quantitative_risk_decomposition(
obbject_item=rd_results
)
assert content["layout"]["barmode"] == "stack"
def test_views_quantitative_attribution(attribution_results):
fig, content = QuantitativeViews.quantitative_attribution(
obbject_item=attribution_results
)
assert content["layout"]["barmode"] == "relative"
def test_views_quantitative_rolling_factors(rolling_results):
fig, content = QuantitativeViews.quantitative_rolling_factors(
obbject_item=rolling_results
)
assert all(t["type"] == "scatter" for t in content["data"])