Skip to content

Commit

Permalink
pylint fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
enzbus committed Sep 22, 2023
1 parent 76447a0 commit 93be8e5
Show file tree
Hide file tree
Showing 10 changed files with 93 additions and 79 deletions.
30 changes: 18 additions & 12 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ PYTHON = python
PROJECT = cvxportfolio
ENVDIR = env
BINDIR = $(ENVDIR)/bin
COVERAGE = 97 # target coverage score
LINT = 6.6 # target lint score


ifeq ($(OS), Windows_NT)
BINDIR=$(ENVDIR)/Scripts
endif

.PHONY: env test hardtest clean docs opendocs coverage fix hardfix release
.PHONY: env test lint clean docs opendocs coverage release fix

env:
$(PYTHON) -m venv $(ENVDIR)
Expand All @@ -17,19 +20,22 @@ env:

test:
$(BINDIR)/coverage run -m unittest $(PROJECT)/tests/*.py
$(BINDIR)/coverage report
$(BINDIR)/coverage report --fail-under $(COVERAGE)
$(BINDIR)/coverage xml
$(BINDIR)/diff-cover --compare-branch origin/master coverage.xml

hardtest:
$(BINDIR)/pytest --cov --cov-report=xml -W error $(PROJECT)/tests/*.py
$(BINDIR)/coverage report --fail-under 97
$(BINDIR)/ruff --line-length=79 --per-file-ignores='$(PROJECT)/__init__.py:F403' $(PROJECT)/*.py $(PROJECT)/tests/*.py
$(BINDIR)/isort --check-only $(PROJECT)/*.py $(PROJECT)/tests/*.py
$(BINDIR)/flake8 --per-file-ignores='$(PROJECT)/__init__.py:F401,F403' $(PROJECT)/*.py $(PROJECT)/tests/*.py
$(BINDIR)/docstr-coverage $(PROJECT)/*.py $(PROJECT)/tests/*.py
$(BINDIR)/bandit $(PROJECT)/*.py $(PROJECT)/tests/*.py
$(BINDIR)/pylint $(PROJECT)/*.py $(PROJECT)/tests/*.py
lint:
$(BINDIR)/pylint --fail-under $(LINT) $(PROJECT)/*.py $(PROJECT)/tests/*.py

# hardtest:
# $(BINDIR)/pytest --cov --cov-report=xml -W error $(PROJECT)/tests/*.py
# $(BINDIR)/coverage report --fail-under 97
# $(BINDIR)/ruff --line-length=79 --per-file-ignores='$(PROJECT)/__init__.py:F403' $(PROJECT)/*.py $(PROJECT)/tests/*.py
# $(BINDIR)/isort --check-only $(PROJECT)/*.py $(PROJECT)/tests/*.py
# $(BINDIR)/flake8 --per-file-ignores='$(PROJECT)/__init__.py:F401,F403' $(PROJECT)/*.py $(PROJECT)/tests/*.py
# $(BINDIR)/docstr-coverage $(PROJECT)/*.py $(PROJECT)/tests/*.py
# $(BINDIR)/bandit $(PROJECT)/*.py $(PROJECT)/tests/*.py
# $(BINDIR)/pylint $(PROJECT)/*.py $(PROJECT)/tests/*.py

clean:
-rm -rf $(BUILDDIR)/*
Expand Down Expand Up @@ -61,7 +67,7 @@ fix:
# THIS ONE DOES SAME AS RUFF, PLUS REMOVING PASS
# $(BINDIR)/autoflake --in-place $(PROJECT)/*.py $(PROJECT)/tests/*.py

release: cleanenv env test
release: cleanenv env lint test
$(BINDIR)/python bumpversion.py
git push
$(BINDIR)/python -m build
Expand Down
1 change: 0 additions & 1 deletion cvxportfolio/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@

class BaseBenchmark(PolicyEstimator):
"""Base class for cvxportfolio benchmark weights."""
pass


class Benchmark(BaseBenchmark, DataEstimator):
Expand Down
18 changes: 9 additions & 9 deletions cvxportfolio/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,22 +304,22 @@ def __init__(self, asset, periods):
self.periods = periods

def _pre_evaluation(self, universe, backtest_times):
self.index = (universe.get_loc if hasattr(
self._index = (universe.get_loc if hasattr(
universe, 'get_loc') else universe.index)(self.asset)
self.low = cp.Parameter()
self.high = cp.Parameter()
self._low = cp.Parameter()
self._high = cp.Parameter()

def _values_in_time(self, t, **kwargs):
if t in self.periods:
self.low.value = 0.
self.high.value = 0.
self._low.value = 0.
self._high.value = 0.
else:
self.low.value = -100.
self.high.value = +100.
self._low.value = -100.
self._high.value = +100.

def _compile_to_cvxpy(self, w_plus, z, w_plus_minus_w_bm):
return [z[self.index] >= self.low,
z[self.index] <= self.high]
return [z[self._index] >= self._low,
z[self._index] <= self._high]


class LeverageLimit(BaseWeightConstraint, InequalityConstraint):
Expand Down
16 changes: 8 additions & 8 deletions cvxportfolio/costs.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
from .errors import ConvexityError, ConvexSpecificationError
from .estimator import CvxpyExpressionEstimator, DataEstimator
from .hyperparameters import HyperParameter
from .utils import periods_per_year
from .utils import periods_per_year_from_datetime_index

__all__ = ["HoldingCost", "TransactionCost", "SoftConstraint",
"StocksTransactionCost", "StocksHoldingCost"]
Expand Down Expand Up @@ -215,9 +215,8 @@ def __init__(self, constraint):
def _compile_to_cvxpy(self, w_plus, z, w_plus_minus_w_bm):
"""Compile cost to cvxpy expression."""
try:
expr = (self.constraint._compile_constr_to_cvxpy(w_plus, z,
w_plus_minus_w_bm)
- self.constraint._rhs())
expr = (self.constraint._compile_constr_to_cvxpy(
w_plus, z, w_plus_minus_w_bm) - self.constraint._rhs())
except AttributeError:
raise SyntaxError(
f"{self.__class__.__name__} can only be used with"
Expand Down Expand Up @@ -353,8 +352,9 @@ def _values_in_time(self, t, past_returns, **kwargs):
if not ((self.short_fees is None)
and (self.long_fees is None)
and (self.dividends is None)):
ppy = periods_per_year(past_returns.index) if\
self.periods_per_year is None else self.periods_per_year
ppy = periods_per_year_from_datetime_index(
past_returns.index) if self.periods_per_year is None else \
self.periods_per_year

if self.short_fees is not None:
self._short_fees_parameter.value = np.ones(
Expand Down Expand Up @@ -507,7 +507,7 @@ def _values_in_time(self, t, current_portfolio_value, past_returns,

if (self.window_sigma_est is None) or\
(self.window_volume_est is None):
ppy = periods_per_year(past_returns.index)
ppy = periods_per_year_from_datetime_index(past_returns.index)
windowsigma = ppy if (
self.window_sigma_est is None) else self.window_sigma_est
windowvolume = ppy if (
Expand All @@ -527,7 +527,7 @@ def _simulate(self, t, u, current_and_past_returns,
current_and_past_volumes, current_prices, **kwargs):

if self.window_sigma_est is None:
windowsigma = periods_per_year(current_and_past_returns.index)
windowsigma = periods_per_year_from_datetime_index(current_and_past_returns.index)
else:
windowsigma = self.window_sigma_est

Expand Down
31 changes: 20 additions & 11 deletions cvxportfolio/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import pandas as pd

from .estimator import Estimator
from .utils import periods_per_year
from .utils import periods_per_year_from_datetime_index

__all__ = ['BacktestResult']

Expand Down Expand Up @@ -55,7 +55,7 @@ def cash_key(self):

@property
def PPY(self):
return periods_per_year(self.h.index)
return periods_per_year_from_datetime_index(self.h.index)

@property
def v(self):
Expand Down Expand Up @@ -222,21 +222,29 @@ def __repr__(self):
f"Final value ({self.cash_key})": f"{self.v.iloc[-1]:.3e}",
f"Profit ({self.cash_key})": f"{self.profit:.3e}",
' '*4: '',
"Absolute return (annualized)": f"{100 * self.mean_return:.1f}%",
"Absolute risk (annualized)": f"{100 * self.volatility:.1f}%",
"Excess return (annualized)": f"{self.excess_returns.mean() * 100 * self.PPY:.1f}%",
"Excess risk (annualized)": f"{self.excess_returns.std() * 100 * np.sqrt(self.PPY):.1f}%",
"Absolute return (annualized)":
f"{100 * self.mean_return:.1f}%",
"Absolute risk (annualized)":
f"{100 * self.volatility:.1f}%",
"Excess return (annualized)":
f"{self.excess_returns.mean() * 100 * self.PPY:.1f}%",
"Excess risk (annualized)":
f"{self.excess_returns.std() * 100 * np.sqrt(self.PPY):.1f}%",
' '*5: '',
"Avg. growth rate (absolute)": self._print_growth_rate(self.growth_rates.mean()),
"Avg. growth rate (excess)": self._print_growth_rate(self.excess_growth_rates.mean()),
"Avg. growth rate (absolute)":
self._print_growth_rate(self.growth_rates.mean()),
"Avg. growth rate (excess)":
self._print_growth_rate(self.excess_growth_rates.mean()),

})

if len(self.costs):
stats[' '*6] = ''
for cost in self.costs:
stats[f'Avg. {cost}'] = f"{(self.costs[cost]/self.v).mean()*1E4:.0f}bp"
stats[f'Max. {cost}'] = f"{(self.costs[cost]/self.v).max()*1E4:.0f}bp"
stats[f'Avg. {cost}'] = \
f"{(self.costs[cost]/self.v).mean()*1E4:.0f}bp"
stats[f'Max. {cost}'] = \
f"{(self.costs[cost]/self.v).max()*1E4:.0f}bp"

stats.update(collections.OrderedDict({
' '*7: '',
Expand All @@ -251,7 +259,8 @@ def __repr__(self):
' '*9: '',
"Avg. policy time": f"{self.policy_times.mean():.3f}s",
"Avg. simulator time": f"{self.simulator_times.mean():.3f}s",
"Total time": f"{self.simulator_times.sum() + self.policy_times.sum():.3f}s",
"Total time":
f"{self.simulator_times.sum() + self.policy_times.sum():.3f}s",
}))

content = pd.Series(stats).to_string()
Expand Down
9 changes: 3 additions & 6 deletions cvxportfolio/risks.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,9 @@
# limitations under the License.

import logging
import warnings

import cvxpy as cp
import numpy as np
import pandas as pd
import scipy.linalg

from .costs import BaseCost
from .estimator import DataEstimator
Expand All @@ -43,7 +40,7 @@ class BaseRiskModel(BaseCost):


class FullCovariance(BaseRiskModel):
"""Quadratic risk model with full covariance matrix.
r"""Quadratic risk model with full covariance matrix.
It represents the objective term:
Expand Down Expand Up @@ -152,7 +149,7 @@ def _compile_to_cvxpy(self, w_plus, z, w_plus_minus_w_bm):


class FactorModelCovariance(BaseRiskModel):
"""Factor model covariance, either user-provided or fitted from the data.
r"""Factor model covariance, either user-provided or fitted from the data.
It represents the objective term:
Expand All @@ -162,7 +159,7 @@ class FactorModelCovariance(BaseRiskModel):
where the factors exposure :math:`F` has as many rows as the number of assets and as many
columns as the number of factors,
the factors covariance matrix :math:`Sigma_{F}` is positive semi-definite,
the factors covariance matrix :math:`\Sigma_{F}` is positive semi-definite,
and the idyosyncratic variances vector :math:`d` is non-negative.
The advantage of this risk model over the standard :class:`FullCovariance` is mostly
Expand Down
18 changes: 11 additions & 7 deletions cvxportfolio/simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
from .errors import DataError
from .estimator import DataEstimator, Estimator
from .result import BacktestResult
from .utils import *
from .utils import (periods_per_year_from_datetime_index, repr_numpy_pandas,
resample_returns)

PPY = 252
__all__ = ['StockMarketSimulator', 'MarketSimulator']
Expand All @@ -55,15 +56,17 @@ def _hash_universe(universe):

def _load_cache(universe, trading_frequency, base_location):
"""Load cache from disk."""
folder = base_location /\
f'hash(universe)={_hash_universe(universe)},trading_frequency={trading_frequency}'
folder = base_location / (
f'hash(universe)={_hash_universe(universe)},'
+ f'trading_frequency={trading_frequency}')
if 'LOCK' in globals():
logging.debug(f'Acquiring cache lock from process {os.getpid()}')
LOCK.acquire()
try:
with open(folder/'cache.pkl', 'rb') as f:
logging.info(
f'Loading cache for universe = {universe} and trading_frequency = {trading_frequency}')
f'Loading cache for universe = {universe}'
f' and trading_frequency = {trading_frequency}')
return pickle.load(f)
except FileNotFoundError:
logging.info(f'Cache not found!')
Expand All @@ -76,8 +79,9 @@ def _load_cache(universe, trading_frequency, base_location):

def _store_cache(cache, universe, trading_frequency, base_location):
"""Store cache to disk."""
folder = base_location /\
f'hash(universe)={_hash_universe(universe)},trading_frequency={trading_frequency}'
folder = base_location / (
f'hash(universe)={_hash_universe(universe)},'
f'trading_frequency={trading_frequency}')
if 'LOCK' in globals():
logging.debug(f'Acquiring cache lock from process {os.getpid()}')
LOCK.acquire()
Expand Down Expand Up @@ -251,7 +255,7 @@ def _downsample(self, interval):
@property
def PPY(self):
"""Periods per year, assumes returns are about equally spaced."""
return periods_per_year(self.returns.index)
return periods_per_year_from_datetime_index(self.returns.index)

def _check_sizes(self):

Expand Down
32 changes: 17 additions & 15 deletions cvxportfolio/tests/test_hyperparameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,35 +11,34 @@
# 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.
"""Unit tests for the data and parameter estimator objects."""
"""Unit tests for the hyper-parameters interface."""

import unittest
from pathlib import Path

import cvxpy as cp
import numpy as np
import pandas as pd

import cvxportfolio as cvx
from cvxportfolio.hyperparameters import *
from cvxportfolio.hyperparameters import GammaRisk, GammaTrade


class TestHyperparameters(unittest.TestCase):
"""Test hyper-parameters interface."""

@classmethod
def setUpClass(cls):
"""Load the data and initialize cvxpy vars."""
# cls.sigma = pd.read_csv(Path(__file__).parent / "sigmas.csv", index_col=0, parse_dates=[0])
cls.returns = pd.read_csv(
Path(__file__).parent / "returns.csv", index_col=0, parse_dates=[0])
# cls.volumes = pd.read_csv(Path(__file__).parent / "volumes.csv", index_col=0, parse_dates=[0])
Path(__file__).parent / "returns.csv",
index_col=0, parse_dates=[0])
cls.w_plus = cp.Variable(cls.returns.shape[1])
cls.w_plus_minus_w_bm = cp.Variable(cls.returns.shape[1])
cls.z = cp.Variable(cls.returns.shape[1])
cls.N = cls.returns.shape[1]

def test_basic_HP(self):

def test_basic_hyper_parameters(self):
"""Test simple syntax and errors."""
gamma = GammaRisk(current_value=1)

self.assertTrue((-gamma).current_value == -1)
Expand All @@ -54,7 +53,8 @@ def test_basic_HP(self):

cvx.SinglePeriodOptimization(-GammaRisk() * cvx.FullCovariance())

def test_HP_algebra(self):
def test_hyper_parameters_algebra(self):
"""Test algebra of HPs objects."""
grisk = GammaRisk(current_value=1)
gtrade = GammaRisk(current_value=.5)

Expand All @@ -67,24 +67,26 @@ def test_HP_algebra(self):
self.assertTrue((grisk/2 + 2*gtrade).current_value == 1.5)
self.assertTrue((grisk/2 + 2 * (gtrade + gtrade/2)).current_value == 2)

def test_collect_HPs(self):
"""Collect hyperparameters."""
def test_collect_hyper_parameters(self):
"""Test collect hyperparameters."""

pol = cvx.SinglePeriodOptimization(GammaRisk() * cvx.FullCovariance())

res = pol._collect_hyperparameters()
print(res)
self.assertTrue(len(res) == 1)

pol = cvx.SinglePeriodOptimization(-GammaRisk() * cvx.FullCovariance()
- GammaTrade() * cvx.TransactionCost())
pol = cvx.SinglePeriodOptimization(
- GammaRisk() * cvx.FullCovariance()
- GammaTrade() * cvx.TransactionCost())

res = pol._collect_hyperparameters()
print(res)
self.assertTrue(len(res) == 2)

pol = cvx.SinglePeriodOptimization(-(GammaRisk() + .5 * GammaRisk())
* cvx.FullCovariance() - GammaTrade() * cvx.TransactionCost())
pol = cvx.SinglePeriodOptimization(
-(GammaRisk() + .5 * GammaRisk()) * cvx.FullCovariance()
- GammaTrade() * cvx.TransactionCost())

res = pol._collect_hyperparameters()
print(res)
Expand Down
Loading

0 comments on commit 93be8e5

Please sign in to comment.