diff --git a/factor_attribution.py b/factor_attribution.py index 03d42f2..156fbd7 100644 --- a/factor_attribution.py +++ b/factor_attribution.py @@ -309,7 +309,10 @@ def run_factor_regression( fitted = x @ coefficients residuals = y.to_numpy() - fitted residual_series = pd.Series(residuals, index=regression_frame.index) - residual_vol_ann = float(residual_series.std(ddof=1) * np.sqrt(TRADING_DAYS_PER_YEAR)) + if len(residual_series) == 1: + residual_vol_ann = 0.0 + else: + residual_vol_ann = float(residual_series.std(ddof=1) * np.sqrt(TRADING_DAYS_PER_YEAR)) dof = n_obs - param_count if dof > 0: diff --git a/tests/test_factor_attribution.py b/tests/test_factor_attribution.py index 9b369db..8628639 100644 --- a/tests/test_factor_attribution.py +++ b/tests/test_factor_attribution.py @@ -501,6 +501,24 @@ class RegressionTests(unittest.TestCase): self.assertTrue(np.isnan(result["adj_r_squared"])) self.assertAlmostEqual(result["residual_vol_ann"], 0.0, places=12) + def test_run_factor_regression_single_observation_intercept_only_has_zero_residual_vol(self): + dates = pd.date_range("2024-01-01", periods=1, freq="B") + factors = pd.DataFrame(index=dates) + strategy = pd.Series([0.0015], index=dates) + + result = run_factor_regression(strategy, factors, factor_cols=[]) + + self.assertAlmostEqual(result["alpha_daily"], 0.0015, places=12) + self.assertEqual(result["betas"], {}) + self.assertEqual(result["t_stats"], {}) + self.assertEqual(result["p_values"], {}) + self.assertEqual(result["r_squared"], 0.0) + self.assertTrue(np.isnan(result["alpha_t_stat"])) + self.assertTrue(np.isnan(result["alpha_p_value"])) + self.assertTrue(np.isnan(result["adj_r_squared"])) + self.assertEqual(result["n_obs"], 1) + self.assertAlmostEqual(result["residual_vol_ann"], 0.0, places=12) + def test_run_factor_regression_rejects_rank_deficient_designs(self): dates = pd.date_range("2024-01-01", periods=6, freq="B") market = np.array([0.01, -0.02, 0.015, 0.005, -0.01, 0.02])