diff --git a/cvxportfolio/result.py b/cvxportfolio/result.py index be4623e77..8fb37e5e5 100644 --- a/cvxportfolio/result.py +++ b/cvxportfolio/result.py @@ -24,6 +24,7 @@ from __future__ import annotations, print_function import collections +import logging from typing import Dict import matplotlib.pyplot as plt @@ -63,8 +64,13 @@ def __init__(self, universe, trading_calendar, costs): self._current_universe = pd.Index(universe) self._indexer = np.arange(len(universe), dtype=int) + @property + def _current_full_universe(self): + """Helper property used by _change_universe.""" + return self._h.columns + def _change_universe(self, new_universe): - """Change current universe (columns of dataframes) during backtest.""" + """Change current universe (columns of dataframes) during back-test.""" # print('new universe') # print(new_universe) @@ -83,9 +89,12 @@ def _change_universe(self, new_universe): joined = new_universe # otherwise we lose the ordering :( - else: #TODO: write testcase for this + else: + logging.info( + f"{self.__class__.__name__} joining new universe with old") joined = pd.Index( - sorted(set(self._current_universe[:-1] + # need to join with full, not current! + sorted(set(self._current_full_universe[:-1] ).union(new_universe[:-1]))) joined = joined.append(new_universe[-1:]) diff --git a/cvxportfolio/tests/test_simulator.py b/cvxportfolio/tests/test_simulator.py index a2ef2a1c6..5462acc98 100644 --- a/cvxportfolio/tests/test_simulator.py +++ b/cvxportfolio/tests/test_simulator.py @@ -59,6 +59,38 @@ def test_simulator_raises(self): columns=['X', 'USDOLLAR']), volumes=pd.DataFrame([[0.]])) + def test_backtest_with_difficult_universe_changes(self): + """Test back-test with assets that both enter and exit at same time.""" + rets = pd.DataFrame(self.returns.iloc[:, -10:], copy=True) + volumes = pd.DataFrame(self.volumes.iloc[:, -9:], copy=True) + prices = pd.DataFrame(self.prices.iloc[:, -9:], copy=True) + rets.iloc[15:25, 1:3] = np.nan + rets.iloc[9:17, 3:5] = np.nan + rets.iloc[8:15, 5:7] = np.nan + rets.iloc[17:29, 7:8] = np.nan + print(rets.iloc[10:20]) + + modified_market_data = cvx.UserProvidedMarketData( + returns=rets, volumes=volumes, prices=prices, + cash_key='cash', + min_history=pd.Timedelta('0d')) + + simulator = cvx.StockMarketSimulator(market_data=modified_market_data) + + policy = cvx.SinglePeriodOptimization( + cvx.ReturnsForecast() - 10 * cvx.FullCovariance(), + [cvx.LongOnly(), cvx.LeverageLimit(1)]) + + bt_result = simulator.backtest(policy, start_time = rets.index[10], + end_time = rets.index[20]) + + print(bt_result.w) + + self.assertTrue(set(bt_result.w.columns) == set(rets.columns)) + self.assertTrue( + np.all(bt_result.w.iloc[:-1].isnull() == rets.iloc[ + 10:20].isnull())) + def test_prepare_data(self): simulator = MarketSimulator(['ZM', 'META'], base_location=self.datadir) self.assertTrue(simulator.market_data.returns.shape[1] == 3)