Skip to content

Commit

Permalink
working on GH issue #107 (fixed CashReturn), whitespaces
Browse files Browse the repository at this point in the history
  • Loading branch information
enzbus committed Sep 4, 2023
1 parent 211f0bc commit e3837a8
Show file tree
Hide file tree
Showing 10 changed files with 112 additions and 61 deletions.
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ env:
$(BINDIR)/python -m pip install -r requirements.txt

test:
$(BINDIR)/python -m unittest $(PROJECT)/tests/*.py
$(BINDIR)/coverage run -m unittest $(PROJECT)/tests/*.py

pytest:
$(BINDIR)/pytest $(PROJECT)/tests/*.py
Expand All @@ -36,6 +36,10 @@ cleanenv:
docs:
$(BINDIR)/sphinx-build -E docs $(BUILDDIR); open build/index.html

coverage: test
$(BINDIR)/coverage html
open htmlcov/index.html

pep8:
# use autopep8 to make innocuous fixes
$(BINDIR)/autopep8 -i $(PROJECT)/*.py $(PROJECT)/tests/*.py
Expand Down
20 changes: 12 additions & 8 deletions cvxportfolio/costs.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,9 @@ def _compile_to_cvxpy(self, w_plus, z, portfolio_value):
"""Iterate over constituent costs."""
self.expression = 0
for multiplier, cost in zip(self.multipliers, self.costs):
add = multiplier * \
cost._compile_to_cvxpy(w_plus, z, portfolio_value)
add = (multiplier.current_value
if hasattr(multiplier, 'current_value') else multiplier) * \
cost._compile_to_cvxpy(w_plus, z, portfolio_value)
if not add.is_dcp():
raise ConvexSpecificationError(cost * multiplier)
if not add.is_concave():
Expand All @@ -171,13 +172,16 @@ def __repr__(self):
"""Pretty-print."""
result = ''
for i, (mult, cost) in enumerate(zip(self.multipliers, self.costs)):
if mult == 0:
continue
if mult < 0:
result += ' - ' if i > 0 else '-'
if not isinstance(mult, HyperParameter):
if mult == 0:
continue
if mult < 0:
result += ' - ' if i > 0 else '-'
else:
result += ' + ' if i > 0 else ''
result += (str(abs(mult)) + ' * ' if abs(mult) != 1 else '')
else:
result += ' + ' if i > 0 else ''
result += (str(abs(mult)) + ' * ' if abs(mult) != 1 else '')
result += str(mult) + ' * '
result += cost.__repr__()
return result

Expand Down
29 changes: 20 additions & 9 deletions cvxportfolio/hyperparameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ def _collect_hyperparameters(self):
if hasattr(el, '_collect_hyperparameters'):
result += el._collect_hyperparameters()
return result

def __repr__(self):
result = ''
for le, ri in zip(self.left, self.right):
result += str(le) + ' * ' + str(ri)
return result


class RangeHyperParameter(HyperParameter):
Expand All @@ -100,29 +106,34 @@ class RangeHyperParameter(HyperParameter):
its subclasses for ones that you can use.
"""

def __init__(self, values_range, initial_value):
if not (initial_value in values_range):
def __init__(self, values_range, current_value):
if not (current_value in values_range):
raise SyntaxError('Initial value must be in the provided range')
self.values_range = values_range
self.current_value = initial_value
self.current_value = current_value

def __repr__(self):
return self.__class__.__name__ \
+ f'(values_range={self.values_range}'\
+ f', current_value={self.current_value})'


class GammaRisk(RangeHyperParameter):
"""Multiplier of a risk term."""

def __init__(self, values_range=GAMMA_RISK_RANGE, initial_value=1.):
super().__init__(values_range, initial_value)
def __init__(self, values_range=GAMMA_RISK_RANGE, current_value=1.):
super().__init__(values_range, current_value)


class GammaTrade(RangeHyperParameter):
"""Multiplier of a transaction cost term."""

def __init__(self, values_range=GAMMA_COST_RANGE, initial_value=1.):
super().__init__(values_range, initial_value)
def __init__(self, values_range=GAMMA_COST_RANGE, current_value=1.):
super().__init__(values_range, current_value)


class GammaHold(RangeHyperParameter):
"""Multiplier of a holding cost term."""

def __init__(self, values_range=GAMMA_COST_RANGE, initial_value=1.):
super().__init__(values_range, initial_value)
def __init__(self, values_range=GAMMA_COST_RANGE, current_value=1.):
super().__init__(values_range, current_value)
18 changes: 9 additions & 9 deletions cvxportfolio/policies.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,21 +345,21 @@ def __init__(self, objective, constraints=[], include_cash_return=True, planning
if not (hasattr(constraints, '__iter__') and len(constraints) and (hasattr(constraints[0], '__iter__') and len(objective) == len(constraints))):
raise SyntaxError(
'If you pass objective as a list, constraints should be a list of lists of the same length.')
self.planning_horizon = len(objective)
self._planning_horizon = len(objective)
self.objective = objective
self.constraints = constraints
else:
if not np.isscalar(planning_horizon):
raise SyntaxError(
'If `objective` and `constraints` are the same for all steps you must specify `planning_horizon`.')
self.planning_horizon = planning_horizon
self._planning_horizon = planning_horizon
self.objective = [copy.deepcopy(objective) for i in range(
planning_horizon)] if planning_horizon > 1 else [objective]
self.constraints = [copy.deepcopy(constraints) for i in range(
planning_horizon)] if planning_horizon > 1 else [constraints]

self.include_cash_return = include_cash_return
if self.include_cash_return:
self._include_cash_return = include_cash_return
if self._include_cash_return:
self.objective = [el + CashReturn() for el in self.objective]
self.terminal_constraint = terminal_constraint
self.benchmark = benchmark() if isinstance(benchmark, type) else benchmark
Expand Down Expand Up @@ -394,7 +394,7 @@ def compile_and_check_constraint(constr, i):
self.cvxpy_constraints = sum(self.cvxpy_constraints, [])
self.cvxpy_constraints += [cp.sum(z) == 0 for z in self.z_at_lags]
w = self.w_current
for i in range(self.planning_horizon):
for i in range(self._planning_horizon):
self.cvxpy_constraints.append(
self.w_plus_at_lags[i] == self.z_at_lags[i] + w)
self.cvxpy_constraints.append(
Expand Down Expand Up @@ -433,11 +433,11 @@ def _recursive_pre_evaluation(self, universe, backtest_times):
# self.portfolio_value = cp.Parameter(nonneg=True)
self.w_current = cp.Parameter(len(universe))
self.z_at_lags = [cp.Variable(len(universe))
for i in range(self.planning_horizon)]
for i in range(self._planning_horizon)]
self.w_plus_at_lags = [cp.Variable(
len(universe)) for i in range(self.planning_horizon)]
len(universe)) for i in range(self._planning_horizon)]
self.w_plus_minus_w_bm_at_lags = [cp.Variable(
len(universe)) for i in range(self.planning_horizon)]
len(universe)) for i in range(self._planning_horizon)]

# simulator will overwrite this with cached loaded from disk
self.cache = {}
Expand Down Expand Up @@ -499,7 +499,7 @@ def _collect_hyperparameters(self):
result += el._collect_hyperparameters()
for el in self.constraints:
for constr in el:
result += el._collect_hyperparameters()
result += constr._collect_hyperparameters()
return result


Expand Down
2 changes: 1 addition & 1 deletion cvxportfolio/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ def __repr__(self):
"Per-period absolute growth rate": self._print_growth_rate(self.growth_rates.mean()),
"Per-period excess growth rate": self._print_growth_rate(self.excess_growth_rates.mean()),
# stats
"Sharpe ratio (w/ excess returns)": self.sharpe_ratio,
"Sharpe ratio": self.sharpe_ratio,
"Worst drawdown (%)": self.drawdown.min() * 100,
"Average drawdown (%)": self.drawdown.mean() * 100,
"Per-period Turnover (%)": self.turnover.mean() * 100,
Expand Down
38 changes: 18 additions & 20 deletions cvxportfolio/returns.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,41 +42,39 @@ class BaseReturnsModel(BaseCost):
class CashReturn(BaseReturnsModel):
r"""Objective term representing cash return.
The forecast of cash return :math:`{\left(\hat{r}_t\right)}_n` is the observed value
from last period :math:`{\left({r}_{t-1}\right)}_n`.
By default, the forecast of cash return :math:`{\left(\hat{r}_t\right)}_n`
is the observed value from last period :math:`{\left({r}_{t-1}\right)}_n`.
This object is included automatically in :class:`SinglePeriodOptimization`
and :class:`MultiPeriodOptimization` policies. You can change
this behavior by setting their ``include_cash_return`` to False.
:param short_margin_requirement: fraction of value of a short positions
that is margined by portfolio cash
:type short_margin_requirement: float
this behavior by setting their ``include_cash_return`` to False. If you do
so, you may include this cost explicitely in the objective. You need
to do so (only) if you provide your own cash return forecast.
:param cash_returns: if you have your forecast for the cash return, you
should pass it here, either as a float (if constant) or as pd.Series
with datetime index (if it changes in time). If you leave the default,
None, the cash return forecast at time t is the observed cash return
at time t-1. (As is suggested in the book.)
:type cash_returns: float or pd.Series or None
"""

def __init__(self, cash_returns=None, short_margin_requirement=1.):
self.cash_returns = None if cash_returns is None else DataEstimator(cash_returns,
compile_parameter=True, non_negative=True)
self.short_margin_requirement = short_margin_requirement
def __init__(self, cash_returns=None):
self.cash_returns = None if cash_returns is None else DataEstimator(
cash_returns, compile_parameter=True)

def _pre_evaluation(self, universe, backtest_times):
self.cash_return_parameter = cp.Parameter(nonneg=True) if self.cash_returns is None \
self.cash_return_parameter = cp.Parameter() if self.cash_returns is None \
else self.cash_returns.parameter

# else DataEstimator(self.cash_returns, non_negative=True, compile_parameter=True)

def _values_in_time(self, t, past_returns, **kwargs):
"""Update cash return parameter as last cash return."""
if self.cash_returns is None:
self.cash_return_parameter.value = past_returns.iloc[-1, -1]

def _compile_to_cvxpy(self, w_plus, z, w_plus_minus_w_bm):
"""Apply cash return to "real" cash position (without shorts and margins)."""
realcash = (w_plus[-1] - (1 + self.short_margin_requirement)
* cp.sum(cp.neg(w_plus[:-1])))
result = realcash * self.cash_return_parameter
assert result.is_concave()
return result
"""Apply cash return to cash position."""
return w_plus[-1] * self.cash_return_parameter


class ReturnsForecast(BaseReturnsModel):
Expand Down
20 changes: 20 additions & 0 deletions cvxportfolio/simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,26 @@ def _concatenate_backtest_results(self, results):
@staticmethod
def _worker(policy, simulator, start_time, end_time, h):
return simulator._concatenated_backtests(policy, start_time, end_time, h)

def optimize_hyperparameters(self, policy, start_time=None, end_time=None,
initial_value=1E6, h=None, objective='sharpe_ratio'):
"""Optimize hyperparameters of a policy to maximize backtest objective.
EXPERIMENTAL: this method is currently being developed.
"""
hyperparameters = policy._collect_hyperparameters()
print(hyperparameters)

# def evaluate()
result_init = self.backtest(policy, start_time=start_time, end_time=end_time,
initial_value=1E6, h=h)

objective_init = getattr(result_init, objective)
print(result_init)

print(objective_init)



def backtest(self, policy, start_time=None, end_time=None, initial_value=1E6, h=None):
"""Backtest trading policy.
Expand Down
6 changes: 3 additions & 3 deletions cvxportfolio/tests/test_hyperparameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def setUpClass(cls):

def test_basic_HP(self):

gamma = GammaRisk(initial_value=1)
gamma = GammaRisk(current_value=1)

self.assertTrue((-gamma).current_value == -1)
self.assertTrue((gamma * .5).current_value == .5)
Expand All @@ -58,8 +58,8 @@ def test_basic_HP(self):
cvx.SinglePeriodOptimization(-GammaRisk() * cvx.FullCovariance())

def test_HP_algebra(self):
grisk = GammaRisk(initial_value=1)
gtrade = GammaRisk(initial_value=.5)
grisk = GammaRisk(current_value=1)
gtrade = GammaRisk(current_value=.5)

self.assertTrue((grisk + gtrade).current_value == 1.5)
self.assertTrue((grisk * gtrade).current_value == .5)
Expand Down
4 changes: 2 additions & 2 deletions cvxportfolio/tests/test_returns.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def test_cash_returns(self):
t=None, past_returns=self.returns.iloc[:123])
cr = self.returns.iloc[122, -1]
self.assertTrue(cvxpy_expression.value == cr * (
self.w_plus[-1].value + 2 * np.sum(np.minimum(self.w_plus[:-1].value, 0.))))
self.w_plus[-1].value ))

def test_cash_returns_provided(self):
"Test CashReturn object with provided cash returns."
Expand All @@ -66,7 +66,7 @@ def test_cash_returns_provided(self):
t=self.returns.index[123], past_returns=None)
cr = self.returns.iloc[123, -1]
self.assertTrue(cvxpy_expression.value == cr * (
self.w_plus[-1].value + 2 * np.sum(np.minimum(self.w_plus[:-1].value, 0.))))
self.w_plus[-1].value))

def test_returns_forecast(self):
"Test ReturnsForecast object with provided assets' returns."
Expand Down
30 changes: 22 additions & 8 deletions cvxportfolio/tests/test_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,7 @@ def test_spo_benchmark(self):
cvx.Benchmark(myunif)]]

results = sim.backtest_many(policies, start_time='2023-01-01',
parallel=False) # important for test coverage!!
parallel=False) # important for test coverage!!

# check myunif is the same as uniform
self.assertTrue(np.isclose(
Expand All @@ -635,15 +635,15 @@ def test_market_neutral(self):
"""Test SPO with market neutral constraint."""

sim = cvx.MarketSimulator(
['AAPL', 'MSFT', 'GE', 'ZM', 'META'], trading_frequency='monthly', base_location=self.datadir)
['AAPL', 'MSFT', 'GE', 'GOOG', 'META', 'GLD'], trading_frequency='monthly', base_location=self.datadir)

objective = cvx.ReturnsForecast() - 10 * cvx.FullCovariance()
objective = cvx.ReturnsForecast() - 2 * cvx.FullCovariance()

policies = [cvx.SinglePeriodOptimization(objective, co) for co in [
[], [cvx.MarketNeutral()], [cvx.DollarNeutral()]]]

results = sim.backtest_many(policies, start_time='2023-01-01',
parallel=False) # important for test coverage
parallel=False) # important for test coverage
print(results)

# check that market neutral sol is closer to
Expand All @@ -660,7 +660,8 @@ def test_timed_constraints(self):
"""Test some constraints that depend on time."""

sim = cvx.StockMarketSimulator(
['AAPL', 'MSFT', 'GE', 'ZM', 'META'], trading_frequency='monthly', base_location=self.datadir)
['AAPL', 'MSFT', 'GE', 'GLD', 'META'],
trading_frequency='monthly', base_location=self.datadir)

# cvx.NoTrade
objective = cvx.ReturnsForecast() - 10 * cvx.FullCovariance()
Expand Down Expand Up @@ -706,7 +707,7 @@ def test_eq_soft_constraints(self):
policies.append(cvx.SinglePeriodOptimization(
objective, [cvx.DollarNeutral()]))
results = sim.backtest_many(policies, start_time='2023-01-01',
parallel=False) # important for test coverage
parallel=False) # important for test coverage
print(results)
allcashpos = [((res.w.iloc[:, -1]-1)**2).mean() for res in results]
print(allcashpos)
Expand All @@ -728,7 +729,7 @@ def test_ineq_soft_constraints(self):
policies.append(cvx.SinglePeriodOptimization(
objective, [cvx.LongOnly(), cvx.MarketNeutral()]))
results = sim.backtest_many(policies, start_time='2023-01-01',
parallel=False) # important for test coverage
parallel=False) # important for test coverage
print(results)
allshorts = [np.minimum(res.w.iloc[:, :-1], 0.).sum().sum()
for res in results]
Expand All @@ -749,7 +750,7 @@ def test_cost_constraints(self):
for el in [0.01, .02, .05, .1]]

results = sim.backtest_many(policies, start_time='2023-01-01',
parallel=False) # important for test coverage
parallel=False) # important for test coverage
print(results)

self.assertTrue(results[0].volatility < results[1].volatility)
Expand All @@ -773,6 +774,19 @@ def test_dcp_convex_raises(self):
with self.assertRaises(ConvexityError):
sim.backtest(policy)

def test_hyperparameters_optimize(self):

objective = cvx.ReturnsForecast() - cvx.GammaRisk() * cvx.FullCovariance()
policy = cvx.SinglePeriodOptimization(
objective, [cvx.LongOnly(), cvx.LeverageLimit(1)])

simulator = cvx.StockMarketSimulator(
['AAPL', 'MSFT', 'GE', 'ZM', 'META'],
trading_frequency='monthly',
base_location=self.datadir)

simulator.optimize_hyperparameters(policy, start_time='2023-01-01')


if __name__ == '__main__':
unittest.main()

0 comments on commit e3837a8

Please sign in to comment.