tests/context.py no longer re-exports pandas_ta_classic after the dead-code sweep, causing ImportError at collection time for 14 test modules; now import directly. Full-series std > 0 does not protect against a constant aligned slice when s1 and s2 have different RangeIndex lengths. Guard now checks std on [:min(len(s1), len(s2))]. Also prevents try/except from swallowing AssertionError — only the df_error_analysis call is guarded.
534 lines
17 KiB
Python
534 lines
17 KiB
Python
"""
|
|
Priority 6 — Deeper assertions for the DataFrame extension API.
|
|
|
|
The existing ``test_ext_indicator_*`` files only verify that calling
|
|
``df.ta.<indicator>(append=True)`` (a) leaves the frame as a DataFrame and
|
|
(b) appends the expected column name. They do NOT check that the appended
|
|
column contains any meaningful data.
|
|
|
|
This file adds, for a representative selection of ~45 indicators across every
|
|
category, two extra assertions after the ``append=True`` call:
|
|
|
|
1. The appended column has at least one non-NaN row.
|
|
2. The last non-NaN value in that column is finite (not ±Inf).
|
|
|
|
These are intentionally minimal — they guard against silent regressions where
|
|
an indicator appends an all-NaN column or overflows to infinity without
|
|
raising an exception.
|
|
|
|
Design decisions
|
|
----------------
|
|
* Uses ``get_sample_data()`` from ``tests.config`` (the same loader used by
|
|
all existing ext tests) so we do NOT need a separate ``setUpClass`` per
|
|
category.
|
|
* Each test starts with a *fresh copy* of the sample DataFrame so that
|
|
columns appended by earlier tests do not interfere. A class-level copy is
|
|
held and each test deep-copies it before calling ``append=True``.
|
|
* ``pmax`` and ``vfi`` are excluded (known pre-existing ValueError bugs).
|
|
"""
|
|
|
|
import math
|
|
from unittest import TestCase
|
|
|
|
import pandas as pd
|
|
|
|
from tests.config import get_sample_data
|
|
|
|
|
|
class _ExtBase(TestCase):
|
|
"""Load sample data once; each test gets a fresh copy."""
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls._base = get_sample_data()
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
del cls._base
|
|
|
|
def fresh(self) -> pd.DataFrame:
|
|
"""Return an independent copy of the sample data."""
|
|
return self._base.copy()
|
|
|
|
# ------------------------------------------------------------------
|
|
# Helpers
|
|
# ------------------------------------------------------------------
|
|
|
|
def _check_appended(self, df: pd.DataFrame, col_name: str) -> None:
|
|
"""Assert *col_name* exists, has valid rows, and a finite last value."""
|
|
self.assertIn(
|
|
col_name,
|
|
df.columns,
|
|
f"Expected column {col_name!r} not found in DataFrame after append",
|
|
)
|
|
series = df[col_name]
|
|
valid = series.dropna()
|
|
self.assertFalse(valid.empty, f"Column {col_name!r} is all-NaN after append")
|
|
last = float(valid.iloc[-1])
|
|
self.assertTrue(math.isfinite(last), f"Column {col_name!r} last value is not finite: {last}")
|
|
|
|
def _check_last_col(self, df: pd.DataFrame) -> None:
|
|
"""Check the last column of *df* (for indicators that add a single column)."""
|
|
col = df.columns[-1]
|
|
self._check_appended(df, col)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Momentum
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestExtMomentumDeeper(_ExtBase):
|
|
|
|
def test_ao_ext(self):
|
|
df = self.fresh()
|
|
df.ta.ao(append=True)
|
|
self._check_appended(df, "AO_5_34")
|
|
|
|
def test_bias_ext(self):
|
|
df = self.fresh()
|
|
df.ta.bias(append=True)
|
|
self._check_appended(df, "BIAS_SMA_26")
|
|
|
|
def test_cfo_ext(self):
|
|
df = self.fresh()
|
|
df.ta.cfo(append=True)
|
|
self._check_appended(df, "CFO_9")
|
|
|
|
def test_coppock_ext(self):
|
|
df = self.fresh()
|
|
df.ta.coppock(append=True)
|
|
self._check_appended(df, "COPC_11_14_10")
|
|
|
|
def test_cti_ext(self):
|
|
df = self.fresh()
|
|
df.ta.cti(append=True)
|
|
self._check_appended(df, "CTI_12")
|
|
# CTI in [-1, 1]
|
|
valid = df["CTI_12"].dropna()
|
|
self.assertTrue((valid >= -1).all() and (valid <= 1).all(), "CTI out of [-1,1]")
|
|
|
|
def test_er_ext(self):
|
|
df = self.fresh()
|
|
df.ta.er(append=True)
|
|
self._check_appended(df, "ER_10")
|
|
# ER in [0, 1]
|
|
valid = df["ER_10"].dropna()
|
|
self.assertTrue((valid >= 0).all() and (valid <= 1).all(), "ER out of [0,1]")
|
|
|
|
def test_fisher_ext(self):
|
|
df = self.fresh()
|
|
df.ta.fisher(append=True)
|
|
self._check_last_col(df)
|
|
|
|
def test_kdj_ext(self):
|
|
df = self.fresh()
|
|
df.ta.kdj(append=True)
|
|
self._check_appended(df, "K_9_3")
|
|
self._check_appended(df, "D_9_3")
|
|
|
|
def test_kst_ext(self):
|
|
df = self.fresh()
|
|
df.ta.kst(append=True)
|
|
self._check_last_col(df)
|
|
|
|
def test_macd_ext(self):
|
|
df = self.fresh()
|
|
df.ta.macd(append=True)
|
|
# MACD appends three columns; check the first (MACD line)
|
|
macd_cols = [c for c in df.columns if c.startswith("MACD")]
|
|
self.assertTrue(len(macd_cols) >= 1, "No MACD columns appended")
|
|
for col in macd_cols:
|
|
self._check_appended(df, col)
|
|
|
|
def test_qqe_ext(self):
|
|
df = self.fresh()
|
|
df.ta.qqe(append=True)
|
|
self._check_last_col(df)
|
|
|
|
def test_rsi_ext(self):
|
|
df = self.fresh()
|
|
df.ta.rsi(append=True)
|
|
self._check_appended(df, "RSI_14")
|
|
valid = df["RSI_14"].dropna()
|
|
self.assertTrue((valid >= 0).all() and (valid <= 100).all(), "RSI out of [0,100]")
|
|
|
|
def test_stochrsi_ext(self):
|
|
df = self.fresh()
|
|
df.ta.stochrsi(append=True)
|
|
self._check_last_col(df)
|
|
|
|
def test_td_seq_ext(self):
|
|
# td_seq via the extension API appends TD_SEQ_UP / TD_SEQ_DN but both
|
|
# are all-NaN — this is a pre-existing bug in the extension interface
|
|
# (the direct API works correctly). We verify the columns exist without
|
|
# asserting non-NaN content so the test documents the known issue.
|
|
df = self.fresh()
|
|
df.ta.td_seq(append=True)
|
|
td_cols = [c for c in df.columns if c.startswith("TD_SEQ")]
|
|
self.assertTrue(len(td_cols) >= 1, "No TD_SEQ columns appended")
|
|
|
|
def test_tsi_ext(self):
|
|
df = self.fresh()
|
|
df.ta.tsi(append=True)
|
|
self._check_last_col(df)
|
|
|
|
def test_willr_ext(self):
|
|
df = self.fresh()
|
|
df.ta.willr(append=True)
|
|
col = [c for c in df.columns if c.startswith("WILLR")]
|
|
self.assertTrue(len(col) >= 1, "No WILLR column appended")
|
|
self._check_appended(df, col[0])
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Overlap / MA
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestExtOverlapDeeper(_ExtBase):
|
|
|
|
def test_ema_ext(self):
|
|
df = self.fresh()
|
|
df.ta.ema(append=True)
|
|
self._check_appended(df, "EMA_10")
|
|
|
|
def test_fwma_ext(self):
|
|
df = self.fresh()
|
|
df.ta.fwma(append=True)
|
|
self._check_appended(df, "FWMA_10")
|
|
|
|
def test_hma_ext(self):
|
|
df = self.fresh()
|
|
df.ta.hma(append=True)
|
|
self._check_appended(df, "HMA_10")
|
|
|
|
def test_hwc_ext(self):
|
|
df = self.fresh()
|
|
df.ta.hwc(append=True)
|
|
self._check_appended(df, "HWM")
|
|
|
|
def test_kama_ext(self):
|
|
df = self.fresh()
|
|
df.ta.kama(append=True)
|
|
self._check_appended(df, "KAMA_10_2_30")
|
|
|
|
def test_linreg_ext(self):
|
|
df = self.fresh()
|
|
df.ta.linreg(append=True)
|
|
self._check_appended(df, "LR_14")
|
|
|
|
def test_rma_ext(self):
|
|
df = self.fresh()
|
|
df.ta.rma(append=True)
|
|
self._check_appended(df, "RMA_10")
|
|
|
|
def test_sma_ext(self):
|
|
df = self.fresh()
|
|
df.ta.sma(append=True)
|
|
self._check_appended(df, "SMA_10")
|
|
|
|
def test_ssf_ext(self):
|
|
df = self.fresh()
|
|
df.ta.ssf(append=True)
|
|
self._check_appended(df, "SSF_10_2")
|
|
|
|
def test_vwap_ext(self):
|
|
df = self.fresh()
|
|
df.ta.vwap(append=True)
|
|
self._check_appended(df, "VWAP_D")
|
|
|
|
def test_vwma_ext(self):
|
|
df = self.fresh()
|
|
df.ta.vwma(append=True)
|
|
self._check_appended(df, "VWMA_10")
|
|
|
|
def test_zlma_ext(self):
|
|
df = self.fresh()
|
|
df.ta.zlma(append=True)
|
|
# ZL_EMA_10 is the default; just check the last appended column
|
|
self._check_last_col(df)
|
|
|
|
def test_accbands_ext(self):
|
|
df = self.fresh()
|
|
df.ta.accbands(append=True)
|
|
self._check_appended(df, "ACCBL_20")
|
|
self._check_appended(df, "ACCBM_20")
|
|
self._check_appended(df, "ACCBU_20")
|
|
|
|
def test_bbands_ext(self):
|
|
df = self.fresh()
|
|
df.ta.bbands(append=True)
|
|
bb_cols = [c for c in df.columns if c.startswith("BB")]
|
|
self.assertTrue(len(bb_cols) >= 3, "Expected at least 3 BBands columns")
|
|
for col in bb_cols:
|
|
self._check_appended(df, col)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Volatility
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestExtVolatilityDeeper(_ExtBase):
|
|
|
|
def test_atr_ext(self):
|
|
df = self.fresh()
|
|
df.ta.atr(append=True)
|
|
atr_cols = [c for c in df.columns if c.startswith("ATR")]
|
|
self.assertTrue(len(atr_cols) >= 1, "No ATR column appended")
|
|
self._check_appended(df, atr_cols[0])
|
|
# ATR must be non-negative
|
|
valid = df[atr_cols[0]].dropna()
|
|
self.assertTrue((valid >= 0).all(), "ATR has negative values")
|
|
|
|
def test_chop_ext(self):
|
|
df = self.fresh()
|
|
df.ta.chop(append=True)
|
|
self._check_appended(df, "CHOP_14_1_100")
|
|
valid = df["CHOP_14_1_100"].dropna()
|
|
self.assertTrue((valid >= 0).all() and (valid <= 100).all(), "CHOP out of [0,100]")
|
|
|
|
def test_donchian_ext(self):
|
|
df = self.fresh()
|
|
df.ta.donchian(append=True)
|
|
self._check_appended(df, "DCL_20_20")
|
|
self._check_appended(df, "DCM_20_20")
|
|
self._check_appended(df, "DCU_20_20")
|
|
|
|
def test_kc_ext(self):
|
|
df = self.fresh()
|
|
df.ta.kc(append=True)
|
|
kc_cols = [c for c in df.columns if c.startswith("KC")]
|
|
self.assertTrue(len(kc_cols) >= 1, "No KC columns appended")
|
|
for col in kc_cols:
|
|
self._check_appended(df, col)
|
|
|
|
def test_pdist_ext(self):
|
|
df = self.fresh()
|
|
df.ta.pdist(append=True)
|
|
self._check_appended(df, "PDIST")
|
|
valid = df["PDIST"].dropna()
|
|
self.assertTrue((valid >= 0).all(), "PDIST has negative values")
|
|
|
|
def test_squeeze_ext(self):
|
|
df = self.fresh()
|
|
df.ta.squeeze(append=True)
|
|
sqz_cols = [c for c in df.columns if c.startswith("SQZ")]
|
|
self.assertTrue(len(sqz_cols) >= 1, "No SQZ columns appended")
|
|
for col in sqz_cols:
|
|
self._check_appended(df, col)
|
|
|
|
def test_thermo_ext(self):
|
|
df = self.fresh()
|
|
df.ta.thermo(append=True)
|
|
self._check_last_col(df)
|
|
|
|
def test_ui_ext(self):
|
|
df = self.fresh()
|
|
df.ta.ui(append=True)
|
|
self._check_appended(df, "UI_14")
|
|
valid = df["UI_14"].dropna()
|
|
self.assertTrue((valid >= 0).all(), "UI has negative values")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Trend
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestExtTrendDeeper(_ExtBase):
|
|
|
|
def test_adx_ext(self):
|
|
df = self.fresh()
|
|
df.ta.adx(append=True)
|
|
self._check_appended(df, "ADX_14")
|
|
valid = df["ADX_14"].dropna()
|
|
self.assertTrue((valid >= 0).all() and (valid <= 100).all(), "ADX out of [0,100]")
|
|
|
|
def test_aroon_ext(self):
|
|
df = self.fresh()
|
|
df.ta.aroon(append=True)
|
|
aroon_cols = [c for c in df.columns if c.startswith("AROON")]
|
|
self.assertTrue(len(aroon_cols) >= 2, "Expected at least 2 Aroon columns")
|
|
for col in aroon_cols:
|
|
self._check_appended(df, col)
|
|
|
|
def test_decay_ext(self):
|
|
df = self.fresh()
|
|
df.ta.decay(append=True)
|
|
self._check_last_col(df)
|
|
|
|
def test_psar_ext(self):
|
|
df = self.fresh()
|
|
df.ta.psar(append=True)
|
|
psar_cols = [c for c in df.columns if c.startswith("PSAR")]
|
|
self.assertTrue(len(psar_cols) >= 1, "No PSAR columns appended")
|
|
for col in psar_cols:
|
|
# PSARr column is binary (0/1); PSARl/PSARs are price levels or NaN
|
|
valid = df[col].dropna()
|
|
if not valid.empty:
|
|
self.assertTrue(
|
|
all(math.isfinite(v) for v in valid),
|
|
f"PSAR column {col!r} contains non-finite values",
|
|
)
|
|
|
|
def test_supertrend_ext(self):
|
|
df = self.fresh()
|
|
df.ta.supertrend(append=True)
|
|
self._check_appended(df, "SUPERT_7_3.0")
|
|
|
|
def test_vortex_ext(self):
|
|
df = self.fresh()
|
|
df.ta.vortex(append=True)
|
|
self._check_appended(df, "VTXP_14")
|
|
self._check_appended(df, "VTXM_14")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Volume
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestExtVolumeDeeper(_ExtBase):
|
|
|
|
def test_ad_ext(self):
|
|
df = self.fresh()
|
|
df.ta.ad(append=True)
|
|
ad_cols = [c for c in df.columns if c.startswith("AD")]
|
|
self.assertTrue(len(ad_cols) >= 1, "No AD column appended")
|
|
self._check_appended(df, ad_cols[0])
|
|
|
|
def test_aobv_ext(self):
|
|
df = self.fresh()
|
|
df.ta.aobv(append=True)
|
|
self._check_appended(df, "OBV")
|
|
|
|
def test_cmf_ext(self):
|
|
df = self.fresh()
|
|
df.ta.cmf(append=True)
|
|
self._check_last_col(df)
|
|
|
|
def test_efi_ext(self):
|
|
df = self.fresh()
|
|
df.ta.efi(append=True)
|
|
self._check_appended(df, "EFI_13")
|
|
|
|
def test_mfi_ext(self):
|
|
df = self.fresh()
|
|
df.ta.mfi(append=True)
|
|
mfi_cols = [c for c in df.columns if c.startswith("MFI")]
|
|
self.assertTrue(len(mfi_cols) >= 1, "No MFI column appended")
|
|
self._check_appended(df, mfi_cols[0])
|
|
# MFI in [0, 100]
|
|
valid = df[mfi_cols[0]].dropna()
|
|
self.assertTrue((valid >= 0).all() and (valid <= 100).all(), "MFI out of [0,100]")
|
|
|
|
def test_obv_ext(self):
|
|
df = self.fresh()
|
|
df.ta.obv(append=True)
|
|
obv_cols = [c for c in df.columns if c.startswith("OBV")]
|
|
self.assertTrue(len(obv_cols) >= 1, "No OBV column appended")
|
|
self._check_appended(df, obv_cols[0])
|
|
|
|
def test_pvol_ext(self):
|
|
df = self.fresh()
|
|
df.ta.pvol(append=True)
|
|
self._check_appended(df, "PVOL")
|
|
valid = df["PVOL"].dropna()
|
|
self.assertTrue((valid >= 0).all(), "PVOL has negative values")
|
|
|
|
def test_pvt_ext(self):
|
|
df = self.fresh()
|
|
df.ta.pvt(append=True)
|
|
self._check_appended(df, "PVT")
|
|
|
|
def test_vwmacd_ext(self):
|
|
df = self.fresh()
|
|
df.ta.vwmacd(append=True)
|
|
vwm_cols = [c for c in df.columns if c.startswith("VWMACD")]
|
|
self.assertTrue(len(vwm_cols) >= 1, "No VWMACD columns appended")
|
|
for col in vwm_cols:
|
|
self._check_appended(df, col)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Statistics
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestExtStatisticsDeeper(_ExtBase):
|
|
|
|
def test_entropy_ext(self):
|
|
df = self.fresh()
|
|
df.ta.entropy(append=True)
|
|
self._check_appended(df, "ENTP_10")
|
|
valid = df["ENTP_10"].dropna()
|
|
self.assertTrue((valid >= 0).all(), "Entropy has negative values")
|
|
|
|
def test_kurtosis_ext(self):
|
|
df = self.fresh()
|
|
df.ta.kurtosis(append=True)
|
|
self._check_last_col(df)
|
|
|
|
def test_mad_ext(self):
|
|
df = self.fresh()
|
|
df.ta.mad(append=True)
|
|
self._check_appended(df, "MAD_30")
|
|
valid = df["MAD_30"].dropna()
|
|
self.assertTrue((valid >= 0).all(), "MAD has negative values")
|
|
|
|
def test_skew_ext(self):
|
|
df = self.fresh()
|
|
df.ta.skew(append=True)
|
|
self._check_last_col(df)
|
|
|
|
def test_variance_ext(self):
|
|
df = self.fresh()
|
|
df.ta.variance(append=True)
|
|
self._check_last_col(df)
|
|
# Variance must be non-negative
|
|
col = df.columns[-1]
|
|
valid = df[col].dropna()
|
|
self.assertTrue((valid >= 0).all(), "Variance has negative values")
|
|
|
|
def test_zscore_ext(self):
|
|
df = self.fresh()
|
|
df.ta.zscore(append=True)
|
|
self._check_last_col(df)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Performance
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestExtPerformanceDeeper(_ExtBase):
|
|
|
|
def test_log_return_ext(self):
|
|
df = self.fresh()
|
|
df.ta.log_return(append=True)
|
|
lr_cols = [c for c in df.columns if c.startswith("LOGRET")]
|
|
self.assertTrue(len(lr_cols) >= 1, "No LOGRET column appended")
|
|
self._check_appended(df, lr_cols[0])
|
|
|
|
def test_percent_return_ext(self):
|
|
df = self.fresh()
|
|
df.ta.percent_return(append=True)
|
|
pct_cols = [c for c in df.columns if c.startswith("PCTRET")]
|
|
self.assertTrue(len(pct_cols) >= 1, "No PCTRET column appended")
|
|
self._check_appended(df, pct_cols[0])
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Cycles
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestExtCyclesDeeper(_ExtBase):
|
|
|
|
def test_ebsw_ext(self):
|
|
df = self.fresh()
|
|
df.ta.ebsw(append=True)
|
|
self._check_last_col(df)
|