Skip to content

Commit

Permalink
more work to adapt
Browse files Browse the repository at this point in the history
Policy.execute to work with
market_data=None
  • Loading branch information
enzbus committed May 20, 2024
1 parent 61b125e commit cb12779
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 37 deletions.
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,14 @@ endif
env: ## create environment
$(PYTHON) -m venv $(VENV_OPTS) $(ENVDIR)
$(BINDIR)/python -m pip install --editable .[docs,dev,examples,test]

clean: ## clean environment
-rm -rf $(BUILDDIR)/*
-rm -rf $(PROJECT).egg*
-rm -rf $(ENVDIR)/*

update: clean env ## update environment

test: ## run tests w/ cov report
$(BINDIR)/python -m coverage run -m $(PROJECT).tests
$(BINDIR)/python -m coverage report
Expand Down
10 changes: 8 additions & 2 deletions cvxportfolio/costs.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,13 +450,16 @@ def values_in_time( # pylint: disable=arguments-differ
:meth:`Estimator.values_in_time`.
:type kwargs: dict
:raises SyntaxError: If the user forgets to specify ``periods_per_year``
when running without market data.
:returns: Trading periods per year.
:rtype: float
"""

if self.periods_per_year is None:
if past_returns is None:
raise ValueError(
raise SyntaxError(
"If not using Cvxportfolio's Market Data servers you"
+ " have to specify periods_per_year")
return periods_per_year_from_datetime_index(past_returns.index)
Expand Down Expand Up @@ -908,11 +911,14 @@ def values_in_time(
:param kwargs: Other unused arguments.
:type kwargs: dict
:raises SyntaxError: If the user tries to estimate sigma when
running without market data.
:returns: Estimated sigma
:rtype: np.array
"""
if past_returns is None:
raise ValueError(
raise SyntaxError(
"If not using Cvxportfolio's Market Data servers you"
+ " have to specify sigma")
return np.sqrt(
Expand Down
8 changes: 3 additions & 5 deletions cvxportfolio/data/market_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,8 @@
from ..errors import DataError
from ..utils import (hash_, make_numeric, periods_per_year_from_datetime_index,
resample_returns, set_pd_read_only)
from .symbol_data import BASE_LOCATION # pylint: disable=unused-import
from .symbol_data import (OLHCV, Fred, SymbolData, YahooFinance, _loader_csv,
_loader_pickle, _loader_sqlite, _storer_csv,
_storer_pickle, _storer_sqlite)
from . import symbol_data
from .symbol_data import BASE_LOCATION, OLHCV, Fred

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -707,7 +705,7 @@ def __init__(self,
if isinstance(datasource, type):
self.datasource = datasource
else: # try to load in current module
self.datasource = globals()[datasource]
self.datasource = getattr(symbol_data, datasource)
self._get_market_data(
universe, grace_period=grace_period,
storage_backend=storage_backend)
Expand Down
1 change: 0 additions & 1 deletion cvxportfolio/data/symbol_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
from io import StringIO
from pathlib import Path
from pickle import UnpicklingError
from urllib.error import URLError

import numpy as np
import pandas as pd
Expand Down
23 changes: 20 additions & 3 deletions cvxportfolio/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,21 @@
# limitations under the License.
"""This module defines some Exceptions thrown by Cvxportfolio objects."""

__all__ = ['DataError', 'MissingTimesError',
__all__ = ['DataError', 'UserDataError', 'MissingTimesError',
'NaNError', 'MissingAssetsError', 'ForecastError',
'PortfolioOptimizationError', 'Bankruptcy',
'ConvexSpecificationError', 'ConvexityError']
'PortfolioOptimizationError',
'ProgramInfeasible', 'ProgramUnbounded',
'Bankruptcy', 'ConvexSpecificationError', 'ConvexityError']


class DataError(ValueError):
"""Base class for exception related to data."""


class UserDataError(DataError, SyntaxError):
"""Exception for errors in data provided by the user."""


class MissingTimesError(DataError):
"""Cvxportfolio couldn't find data for a certain time."""

Expand All @@ -43,6 +48,18 @@ class PortfolioOptimizationError(Exception):
"""Errors with portfolio optimization problems."""


class NumericalSolverError(PortfolioOptimizationError):
"""Numerical solver failed to produce a solution."""


class ProgramInfeasible(PortfolioOptimizationError):
"""Optimization program is infeasible."""


class ProgramUnbounded(PortfolioOptimizationError):
"""Optimization program is unbounded."""


class Bankruptcy(Exception):
"""A backtest resulted in a bankruptcy."""

Expand Down
104 changes: 80 additions & 24 deletions cvxportfolio/policies.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
import pandas as pd

from .errors import (ConvexityError, ConvexSpecificationError, DataError,
MissingTimesError, PortfolioOptimizationError)
MissingTimesError, NumericalSolverError,
ProgramInfeasible, ProgramUnbounded, UserDataError)
from .estimator import DataEstimator, Estimator
from .forecast import HistoricalMeanVolume
from .returns import CashReturn
Expand Down Expand Up @@ -60,6 +61,12 @@ def execute(self, h, market_data, t=None):
Series of the number of shares to trade, if you pass a Market
Data server which provides open prices (or None).
.. versionadded:: 1.4.0
The option to pass ``market_data=None``, bypassing all Cvxportfolio
market data management.
:param h: Holdings vector, in dollars, including the cash account
(the last element).
:type h: pandas.Series
Expand All @@ -76,8 +83,8 @@ def execute(self, h, market_data, t=None):
argument to ``True``.
:type t: pandas.Timestamp or None
:raises cvxportfolio.errors.DataError: Holdings vector sum to a
negative value or don't match the market data server's universe.
:raises cvxportfolio.errors.UserDataError: Holdings vector sum to a
negative value, don't match the market data server's universe, ...
:returns: u, t, shares_traded
:rtype: pandas.Series, pandas.Timestamp, pandas.Series
Expand All @@ -91,24 +98,24 @@ def execute(self, h, market_data, t=None):
t = trading_calendar[-1]

if not t in trading_calendar:
raise ValueError(f'Provided time {t} must be in the '
raise UserDataError(f'Provided time {t} must be in the '
+ 'trading calendar implied by the market data server.')
else:
if t is None:
raise ValueError(
raise UserDataError(
"If market_data is None you must specify t.")
# TODO: should be possible to pass trading_calendar
trading_calendar = pd.DateTimeIndex([t])
trading_calendar = pd.DatetimeIndex([t])

if np.any(h.isnull()):
raise ValueError(
raise UserDataError(
f"Holdings provided to {self.__class__.__name__}.execute "
+ " have missing values!")

v = np.sum(h)

if v < 0.:
raise DataError(
raise UserDataError(
f"Holdings provided to {self.__class__.__name__}.execute "
+ " have negative sum.")

Expand All @@ -117,7 +124,7 @@ def execute(self, h, market_data, t=None):
market_data.serve(t)

if sorted(h.index) != sorted(past_returns.columns):
raise DataError(
raise UserDataError(
"Holdings provided don't match the universe"
" implied by the market data server.")

Expand Down Expand Up @@ -176,22 +183,42 @@ class AllCash(Policy):
:class:`MultiPeriodOptimization` policies.
"""

_universe = None

def initialize_estimator( # pylint: disable=arguments-differ
self, universe, **kwargs):
"""Save universe.
:param universe: Current universe.
:type universe: pd.Index
:param kwargs: Other unused arguments.
:type kwargs: dict
"""

self._universe = universe

def values_in_time( # pylint: disable=arguments-differ
self, past_returns, **kwargs):
self, **kwargs):
"""Return all cash weights.
:param past_returns: Past market returns (used to infer universe).
:type past_returns: pandas.DataFrame
:param kwargs: Unused arguments to :meth:`values_in_time`.
:type kwargs: dict
:returns: All cash weights.
:rtype: pandas.Series
"""
result = pd.Series(0., past_returns.columns)
result = pd.Series(0., self._universe)
result.iloc[-1] = 1.
return result

def finalize_estimator(self, **kwargs):
"""De-reference universe.
:param kwargs: Unused arguments.
:type kwargs: dict
"""
self._universe = None

class MarketBenchmark(Policy):
"""Allocation weighted by average market traded volumes.
Expand Down Expand Up @@ -223,13 +250,24 @@ def __init__(self, mean_volume_forecast=HistoricalMeanVolume):
self.mean_volume_forecast = DataEstimator(
mean_volume_forecast, data_includes_cash=False)

_universe = None

def initialize_estimator( # pylint: disable=arguments-differ
self, universe, **kwargs):
"""Save universe.
:param universe: Current universe.
:type universe: pd.Index
:param kwargs: Other unused arguments.
:type kwargs: dict
"""

self._universe = universe

def values_in_time( # pylint: disable=arguments-differ
self, past_returns, **kwargs):
self, **kwargs):
"""Return market benchmark weights.
:param past_returns: Past market returns (used to infer universe with
cash).
:type past_returns: pandas.DataFrame
:param kwargs: Unused arguments to :meth:`values_in_time`.
:type kwargs: dict
Expand All @@ -243,7 +281,15 @@ def values_in_time( # pylint: disable=arguments-differ
meanvolumes = self.mean_volume_forecast.current_value
result = np.zeros(len(meanvolumes) + 1)
result[:-1] = meanvolumes / sum(meanvolumes)
return pd.Series(result, index=past_returns.columns)
return pd.Series(result, index=self._universe)

def finalize_estimator(self, **kwargs):
"""De-reference universe.
:param kwargs: Unused arguments.
:type kwargs: dict
"""
self._universe = None

class RankAndLongShort(Policy):
"""Rank assets by signal; long highest and short lowest.
Expand Down Expand Up @@ -355,7 +401,7 @@ def values_in_time( # pylint: disable=arguments-differ

next_targets = self.targets.loc[self.targets.index >= t]
if not np.allclose(next_targets.sum(1), 1.):
raise ValueError(
raise UserDataError(
f"The target weights provided to {self.__class__.__name__} at"
+ f" time {t} do not sum to 1.")
if len(next_targets) == 0:
Expand Down Expand Up @@ -514,6 +560,13 @@ def initialize_estimator( # pylint: disable=arguments-differ
target_weights.iloc[-1] = 1. - target_weights.sum()
self.target_weights = DataEstimator(target_weights)

def finalize_estimator(self, **kwargs):
"""Clean up local variables.
:param kwargs: Unused arguments.
:type kwargs: dict
"""
self.target_weights = None

class PeriodicRebalance(FixedWeights):
"""Track a target weight vector rebalancing at given times.
Expand Down Expand Up @@ -885,20 +938,23 @@ def values_in_time_recursive( # pylint: disable=arguments-differ
if self._problem.status in ['optimal', 'optimal_inaccurate']:
logger.warning('Fallback solution with SCS worked!')
else: # pragma: no cover
raise PortfolioOptimizationError( # pragma: no cover
raise NumericalSolverError( # pragma: no cover
f"Numerical solver for policy {self.__class__.__name__}"
+ f" at time {t} failed; try changing it, relaxing some"
+ " constraints, or removing costs.") from exc

if self._problem.status in ["unbounded", "unbounded_inaccurate"]:
raise PortfolioOptimizationError(
raise ProgramUnbounded(
f"Policy {self.__class__.__name__} at time "
+ f"{t} resulted in an unbounded problem.")
+ f"{t} resulted in an unbounded optimization program. "
+ "You can fix this by adding constraints, like LeverageLimit.")

if self._problem.status in ["infeasible", 'infeasible_inaccurate']:
raise PortfolioOptimizationError(
raise ProgramInfeasible(
f"Policy {self.__class__.__name__} at time "
+ f"{t} resulted in an infeasible problem.")
+ f"{t} resulted in an infeasible problem. "
+ "You can fix this by replacing some constraints with "
+ "equivalent SoftConstraints in the objective.")

result = current_weights + pd.Series(
self._z_at_lags[0].value, current_weights.index)
Expand Down
4 changes: 4 additions & 0 deletions cvxportfolio/tests/test_policies.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,12 @@ def test_sell_all(self):
self.returns.columns)
policy = cvx.SellAll()
t = pd.Timestamp('2022-01-01')
policy.initialize_estimator_recursive(
universe=self.returns.columns,
trading_calendar=self.returns.index)
wplus = policy.values_in_time_recursive(
t=t, past_returns=self.returns)
policy.finalize_estimator_recursive()
allcash = np.zeros(len(start_portfolio))
allcash[-1] = 1
assert isinstance(wplus, pd.Series)
Expand Down

0 comments on commit cb12779

Please sign in to comment.