Skip to content

Commit

Permalink
Other various fixes, addressed #115.
Browse files Browse the repository at this point in the history
  • Loading branch information
enzbus committed Nov 27, 2023
1 parent aafc11c commit 7300027
Show file tree
Hide file tree
Showing 8 changed files with 81 additions and 43 deletions.
14 changes: 9 additions & 5 deletions cvxportfolio/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,9 @@ def update(self, grace_period):
+ f" of {self.symbol} changed last value!")
self._print_difference(current, updated)
except KeyError:
logging.error(f"{self.__class__.__name__} update"
+ f" of {self.symbol} could not be checked for append-only"
+ " edits. Was there a DST change?")
logging.error("%s update of %s could not be checked for"
+ " append-only edits. Was there a DST change?",
self.__class__.__name__, self.symbol)
self._store(updated)

def _download(self, symbol, current, grace_period, **kwargs):
Expand Down Expand Up @@ -790,8 +790,8 @@ def partial_universe_signature(self, partial_universe):
available at some time for trading.
This is used in cvxportfolio.cache to sign back-test caches that
are saved on disk. See its implementation below for details. If
not redefined it returns None which disables on-disk caching.
are saved on disk. If not redefined it returns None which disables
on-disk caching.
:param partial_universe: A subset of the full universe.
:type partial_universe: pandas.Index
Expand All @@ -810,6 +810,10 @@ class MarketDataInMemory(MarketData):
def __init__(
self, trading_frequency, base_location, cash_key, min_history):
"""This must be called by the derived classes."""
if (self.returns.index[-1] - self.returns.index[0]) < min_history:
raise DataError(
"The provided returns have less history "
+ f"than the min_history {min_history}")
if trading_frequency:
self._downsample(trading_frequency)
self.trading_frequency = trading_frequency
Expand Down
2 changes: 1 addition & 1 deletion cvxportfolio/policies.py
Original file line number Diff line number Diff line change
Expand Up @@ -891,4 +891,4 @@ class MultiPeriodOpt(MultiPeriodOptimization):
As it was defined originally in :paper:`section 6.1 <section.6.1>` of the
paper.
"""
"""
3 changes: 1 addition & 2 deletions cvxportfolio/returns.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
from .costs import Cost
from .estimator import DataEstimator # , ParameterEstimator
from .forecast import HistoricalMeanError, HistoricalMeanReturn
from .risks import BaseRiskModel

__all__ = [
"ReturnsForecast",
Expand Down Expand Up @@ -197,7 +196,7 @@ def compile_to_cvxpy(self, w_plus, z, w_plus_minus_w_bm):
return w_plus[:-1].T @ self._r_hat_parameter


class ReturnsForecastError(BaseRiskModel):
class ReturnsForecastError(Cost):
r"""Simple return forecast error risk with values provided by the user.
It represents the objective term:
Expand Down
51 changes: 27 additions & 24 deletions cvxportfolio/risks.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""This module implements risk models, which are objective terms that are
used to penalize allocations which might incur in losses."""

import logging

Expand All @@ -37,11 +39,7 @@
]


class BaseRiskModel(Cost):
pass


class FullCovariance(BaseRiskModel):
class FullCovariance(Cost):
r"""Quadratic risk model with full covariance matrix.
It represents the objective term:
Expand Down Expand Up @@ -70,6 +68,7 @@ def __init__(self, Sigma=HistoricalFactorizedCovariance):
and Sigma.FACTORIZED

self.Sigma = DataEstimator(Sigma)
self._sigma_sqrt = None

def initialize_estimator(self, universe, trading_calendar):
"""Initialize risk model with universe and trading times.
Expand All @@ -79,7 +78,7 @@ def initialize_estimator(self, universe, trading_calendar):
:param trading_calendar: Future (including current) trading calendar.
:type trading_calendar: pandas.DatetimeIndex
"""
self.Sigma_sqrt = cp.Parameter((len(universe)-1, len(universe)-1))
self._sigma_sqrt = cp.Parameter((len(universe)-1, len(universe)-1))

def values_in_time(self, **kwargs):
"""Update parameters of risk model.
Expand All @@ -88,9 +87,9 @@ def values_in_time(self, **kwargs):
:type kwargs: dict
"""
if self._alreadyfactorized:
self.Sigma_sqrt.value = self.Sigma.current_value
self._sigma_sqrt.value = self.Sigma.current_value
else:
self.Sigma_sqrt.value = project_on_psd_cone_and_factorize(
self._sigma_sqrt.value = project_on_psd_cone_and_factorize(
self.Sigma.current_value)

def compile_to_cvxpy(self, w_plus, z, w_plus_minus_w_bm):
Expand All @@ -108,11 +107,11 @@ def compile_to_cvxpy(self, w_plus, z, w_plus_minus_w_bm):
:rtype: cvxpy.expression
"""
self.cvxpy_expression = cp.sum_squares(
self.Sigma_sqrt.T @ w_plus_minus_w_bm[:-1])
self._sigma_sqrt.T @ w_plus_minus_w_bm[:-1])
return self.cvxpy_expression


class RiskForecastError(BaseRiskModel):
class RiskForecastError(Cost):
"""Risk forecast error.
Implements the model defined in :paper:`chapter 4, page 32 <section.4.3>`
Expand Down Expand Up @@ -166,10 +165,11 @@ def compile_to_cvxpy(self, w_plus, z, w_plus_minus_w_bm):
:returns: Cvxpy expression representing the risk model.
:rtype: cvxpy.expression
"""
return cp.square(cp.abs(w_plus_minus_w_bm[:-1]).T @ self.sigmas_parameter)
return cp.square(
cp.abs(w_plus_minus_w_bm[:-1]).T @ self.sigmas_parameter)


class DiagonalCovariance(BaseRiskModel):
class DiagonalCovariance(Cost):
"""Diagonal covariance matrix, user-provided or fit from data.
:param sigma_squares: per-stock variances, indexed by time if
Expand Down Expand Up @@ -221,7 +221,7 @@ def compile_to_cvxpy(self, w_plus, z, w_plus_minus_w_bm):
self.sigmas_parameter))


class FactorModelCovariance(BaseRiskModel):
class FactorModelCovariance(Cost):
r"""Factor model covariance, either user-provided or fitted from the data.
It represents the objective term:
Expand Down Expand Up @@ -312,6 +312,7 @@ def __init__(self, F=None, d=None, Sigma_F=None, num_factors=1,
self.num_factors = num_factors
else:
self._fit = False
self.idyosync_sqrt_parameter = None

def initialize_estimator(self, universe, trading_calendar):
"""Initialize risk model with universe and trading times.
Expand All @@ -324,14 +325,15 @@ def initialize_estimator(self, universe, trading_calendar):
self.idyosync_sqrt_parameter = cp.Parameter(len(universe)-1)
if self._fit:
effective_num_factors = min(self.num_factors, len(universe)-1)
self.F_parameter = cp.Parameter(
self.factor_exposures_parameter = cp.Parameter(
(effective_num_factors, len(universe)-1))
else:
if self.Sigma_F is None:
self.F_parameter = self.F.parameter
self.factor_exposures_parameter = self.F.parameter
else:
# we could refactor the code here so we don't create duplicate parameters
self.F_parameter = cp.Parameter(self.F.parameter.shape)
self.factor_exposures_parameter = cp.Parameter(
self.F.parameter.shape)

def values_in_time(self, **kwargs):
"""Update internal parameters.
Expand All @@ -341,20 +343,21 @@ def values_in_time(self, **kwargs):
"""
if self._fit:
if hasattr(self, 'F_and_d_Forecaster'):
self.F_parameter.value, d = \
self.factor_exposures_parameter.value, d = \
self.F_and_d_Forecaster.current_value
else:
Sigmasqrt = self.Sigma.current_value \
sigma_sqrt = self.Sigma.current_value \
if self._alreadyfactorized \
else project_on_psd_cone_and_factorize(
self.Sigma.current_value)
# numpy eigendecomposition has largest eigenvalues last
self.F_parameter.value = Sigmasqrt[:, -self.num_factors:].T
d = np.sum(Sigmasqrt[:, :-self.num_factors]**2, axis=1)
self.factor_exposures_parameter.value = sigma_sqrt[
:, -self.num_factors:].T
d = np.sum(sigma_sqrt[:, :-self.num_factors]**2, axis=1)
else:
d = self.d.current_value
if not (self.Sigma_F is None):
self.F_parameter.value = (
if not self.Sigma_F is None:
self.factor_exposures_parameter.value = (
self.F.parameter.value.T @ np.linalg.cholesky(
self.Sigma_F.current_value)).T

Expand All @@ -378,14 +381,14 @@ def compile_to_cvxpy(self, w_plus, z, w_plus_minus_w_bm):
self.idyosync_sqrt_parameter, w_plus_minus_w_bm[:-1]))
assert self.expression.is_dcp(dpp=True)

self.expression += cp.sum_squares(self.F_parameter @
self.expression += cp.sum_squares(self.factor_exposures_parameter @
w_plus_minus_w_bm[:-1])
assert self.expression.is_dcp(dpp=True)

return self.expression


class WorstCaseRisk(BaseRiskModel):
class WorstCaseRisk(Cost):
"""Select the most restrictive risk model for each value of the allocation.
vector.
Expand Down
8 changes: 8 additions & 0 deletions cvxportfolio/simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ class MarketSimulator:
``'annual'``. By default (None) don't down-sample.
:type trading_frequency: str or None
:param min_history: Minimum amount of time for which an asset must have
returns that are not ``nan`` before it is included in a back-test.
Default one year.
:type min_history: pandas.Timedelta
:param market_data: An instance of a :class:`cvxportfolio.data.MarketData`
derived class. If provided, all previous arguments are ignored.
:type market_data: :class:`cvxportfolio.data.MarketData` instance or None
Expand All @@ -113,6 +118,7 @@ def __init__(self, universe=(), returns=None, volumes=None,
datasource='YahooFinance',
cash_key="USDOLLAR",
base_location=BASE_LOCATION,
min_history=pd.Timedelta('365.24d'),
trading_frequency=None):
"""Initialize the Simulator and download data if necessary."""
self.base_location = Path(base_location)
Expand All @@ -133,12 +139,14 @@ def __init__(self, universe=(), returns=None, volumes=None,
volumes=volumes, prices=prices,
cash_key=cash_key,
base_location=base_location,
min_history=min_history,
trading_frequency=trading_frequency)
else:
self.market_data = DownloadedMarketData(
universe=universe,
cash_key=cash_key,
base_location=base_location,
min_history=min_history,
trading_frequency=trading_frequency,
datasource=datasource)

Expand Down
19 changes: 14 additions & 5 deletions cvxportfolio/tests/test_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,8 @@ def test_user_provided_market_data(self):

_ = UserProvidedMarketData(
returns=used_returns, volumes=used_volumes, prices=used_prices,
cash_key='USDOLLAR', base_location=self.datadir)
cash_key='USDOLLAR', base_location=self.datadir,
min_history=pd.Timedelta('0d'))

without_prices = UserProvidedMarketData(
returns=used_returns, volumes=used_prices, cash_key='USDOLLAR',
Expand All @@ -459,27 +460,35 @@ def test_user_provided_market_data(self):

with self.assertRaises(SyntaxError):
UserProvidedMarketData(returns=self.returns, volumes=self.volumes,
prices=self.prices.iloc[:, :-1], cash_key='cash')
prices=self.prices.iloc[:, :-1], cash_key='cash',
min_history=pd.Timedelta('0d'))

with self.assertRaises(DataError):
UserProvidedMarketData(returns=self.returns.iloc[:10],
cash_key='cash', min_history=pd.Timedelta('50d'))

with self.assertRaises(SyntaxError):
UserProvidedMarketData(
returns=self.returns,
volumes=self.volumes.iloc[:, :-3],
prices=self.prices, cash_key='cash')
prices=self.prices, cash_key='cash',
min_history=pd.Timedelta('0d'))

with self.assertRaises(SyntaxError):
used_prices = pd.DataFrame(
self.prices, index=self.prices.index,
columns=self.prices.columns[::-1])
UserProvidedMarketData(returns=self.returns, volumes=self.volumes,
prices=used_prices, cash_key='cash')
prices=used_prices, cash_key='cash',
min_history=pd.Timedelta('0d'))

with self.assertRaises(SyntaxError):
used_volumes = pd.DataFrame(
self.volumes, index=self.volumes.index,
columns=self.volumes.columns[::-1])
UserProvidedMarketData(returns=self.returns, volumes=used_volumes,
prices=self.prices, cash_key='cash')
prices=self.prices, cash_key='cash',
min_history=pd.Timedelta('0d'))

def test_market_data_full(self):
"""Test serve method of DownloadedMarketData."""
Expand Down
19 changes: 14 additions & 5 deletions cvxportfolio/tests/test_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,18 +93,23 @@ def test_simulator_raises(self):
with self.assertRaises(SyntaxError):
MarketSimulator(returns=pd.DataFrame(
[[0.]], columns=['USDOLLAR'], index=[pd.Timestamp.today()]),
volumes=pd.DataFrame([[0.]]))
volumes=pd.DataFrame([[0.]]),
min_history=pd.Timedelta('0d'))

# not raises
_ = MarketSimulator(returns=pd.DataFrame([[0., 0.]],
columns=['A', 'USDOLLAR']), volumes=pd.DataFrame(
[[0.]], columns=['A']), round_trades=False)
_ = MarketSimulator(
returns=pd.DataFrame([[0., 0.]], columns=['A', 'USDOLLAR'],
index=[pd.Timestamp('2020-01-01'), pd.Timestamp('2021-01-01')]),
volumes=pd.DataFrame(
[[0.]], columns=['A']), round_trades=False,
min_history = pd.Timedelta('0d'))

with self.assertRaises(SyntaxError):
MarketSimulator(returns=pd.DataFrame(
[[0., 0.]], index=[pd.Timestamp.today()],
columns=['X', 'USDOLLAR']),
volumes=pd.DataFrame([[0.]]))
volumes=pd.DataFrame([[0.]]),
min_history = pd.Timedelta('0d'))

def test_backtest_with_ipos_and_delistings(self):
"""Test back-test with assets that both enter and exit."""
Expand Down Expand Up @@ -475,6 +480,10 @@ def test_multiple_backtest(self):
result = sim.run_multiple_backtest([pol, pol1], pd.Timestamp(
'2023-01-01'), pd.Timestamp('2023-04-20'), h=['hello'])

with self.assertRaises(SyntaxError):
result = sim.run_multiple_backtest(pol, pd.Timestamp(
'2023-01-01'), pd.Timestamp('2023-04-20'), h=['hello'])

result = sim.backtest(pol1, pd.Timestamp(
'2023-01-01'), pd.Timestamp('2023-04-20'))

Expand Down
8 changes: 7 additions & 1 deletion docs/data.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,10 @@ Base classes (for using other data sources)

.. automethod:: serve

.. automethod:: trading_calendar
.. automethod:: trading_calendar

.. autoproperty:: periods_per_year

.. autoproperty:: full_universe

.. automethod:: partial_universe_signature

0 comments on commit 7300027

Please sign in to comment.