From 93ba24f7312ec69a378a265483eb229f6af411d9 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Sun, 13 Sep 2026 19:25:03 +0800 Subject: [PATCH] fix: normalize inverse-vol weights in float64 Co-Authored-By: Codex --- src/portfolio.py | 4 +++- tests/test_backtest_accounting.py | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/portfolio.py b/src/portfolio.py index ba4554a..261245f 100644 --- a/src/portfolio.py +++ b/src/portfolio.py @@ -28,7 +28,9 @@ def select_portfolio( return selected if weighting in {"inverse_vol", "inverse-vol", "inv_vol"} and "vol20" in selected.columns: - inverse_vol = 1.0 / selected["vol20"].clip(lower=0.05) + # Compute normalized weights in float64 so downcast research inputs + # cannot make a fully invested portfolio round above the strict guard. + inverse_vol = 1.0 / selected["vol20"].astype(np.float64).clip(lower=0.05) selected["target_weight"] = inverse_vol / inverse_vol.sum() else: selected["target_weight"] = 1.0 / len(selected) diff --git a/tests/test_backtest_accounting.py b/tests/test_backtest_accounting.py index a2c2791..5d8eadc 100644 --- a/tests/test_backtest_accounting.py +++ b/tests/test_backtest_accounting.py @@ -7,6 +7,8 @@ import pandas as pd from src.backtest import run_single_backtest +from src.portfolio import select_portfolio +from src.utils import clean_numeric_frame class BacktestAccountingTests(unittest.TestCase): @@ -121,6 +123,19 @@ def test_inverse_vol_skips_top_score_with_missing_volatility(self) -> None: self.assertEqual(result.trades["symbol"].tolist(), ["B"]) + def test_inverse_vol_float32_weights_remain_fully_invested(self) -> None: + panel = self.panel(("A", "B")) + panel["final_score"] = np.tile([2.0, 1.0], len(self.dates)) + panel["vol20"] = np.tile([0.13621539, 0.6013158], len(self.dates)) + panel = clean_numeric_frame(panel) + config = {"strategy": {**self.config["strategy"], "top_n": 2, "weighting": "inverse_vol"}} + + selected = select_portfolio(panel.xs(self.dates[0], level="date"), "final_score", 2, "inverse_vol") + self.assertEqual(selected["target_weight"].dtype, np.dtype("float64")) + self.assertLessEqual(float(selected["target_weight"].sum()), 1.0 + 1e-12) + result = run_single_backtest(panel, "final_score", config) + self.assertFalse(result.returns.empty) + def test_nonfinite_eligible_scores_are_not_cash(self) -> None: for bad_score in (np.inf, -np.inf): with self.subTest(bad_score=bad_score):