From 0a3e53bb3a878aa5c64ae1c2e46560ee2283c784 Mon Sep 17 00:00:00 2001 From: Divasco Date: Thu, 11 Jul 2024 17:40:26 -0300 Subject: [PATCH] Documented all --- garpar/core/__init__.py | 2 + garpar/core/covcorr_acc.py | 172 ++++++++++++++++- garpar/core/ereturns_acc.py | 59 +++++- garpar/core/plot_acc.py | 64 ++++++- garpar/core/risk_acc.py | 2 +- garpar/core/utilities_acc.py | 2 +- garpar/datasets/base.py | 210 ++++++++++++++++++++ garpar/datasets/data/__init__.py | 6 +- garpar/datasets/multisector.py | 111 +++++++++++ garpar/datasets/risso.py | 317 +++++++++++++++++++++++++++++++ garpar/utils/entropy.py | 38 ++++ garpar/utils/mabc.py | 51 ++++- 12 files changed, 1023 insertions(+), 11 deletions(-) diff --git a/garpar/core/__init__.py b/garpar/core/__init__.py index a1352ca..c932d28 100644 --- a/garpar/core/__init__.py +++ b/garpar/core/__init__.py @@ -4,4 +4,6 @@ # License: MIT # Full Text: https://github.com/quatrope/garpar/blob/master/LICENSE +"""Core utilities.""" + from .portfolio import GARPAR_METADATA_KEY, Portfolio diff --git a/garpar/core/covcorr_acc.py b/garpar/core/covcorr_acc.py index 9b510bb..8b46425 100644 --- a/garpar/core/covcorr_acc.py +++ b/garpar/core/covcorr_acc.py @@ -4,6 +4,8 @@ # License: MIT # Full Text: https://github.com/quatrope/garpar/blob/master/LICENSE +"""Covariance Accessor.""" + import attr from pypfopt import risk_models @@ -11,38 +13,123 @@ from ..utils import accabc # ============================================================================= -# +# COVARIANCE ACCESSOR # ============================================================================= @attr.s(frozen=True, cmp=False, slots=True, repr=False) class CovarianceAccessor(accabc.AccessorABC): + """Accessor class for calculating various covariance matrices. + + Attributes + ---------- + _default_kind : str + Default kind of covariance matrix. + _pf : Portfolio + Portfolio object containing prices data. + + Methods + ------- + sample_cov(**kwargs) + Compute the sample covariance matrix. + exp_cov(**kwargs) + Compute the exponentially-weighted covariance matrix. + semi_cov(**kwargs) + Compute the semi-covariance matrix. + ledoit_wolf_cov(shrinkage_target="constant_variance", **kwargs) + Compute the Ledoit-Wolf covariance matrix with optional shrinkage target. + oracle_approximating_cov(**kwargs) + Compute the Oracle-approximating covariance matrix. + """ + _default_kind = "sample_cov" _pf = attr.ib() def sample_cov(self, **kwargs): + """Compute the sample covariance matrix. + + Parameters + ---------- + **kwargs + Additional keyword arguments passed to `risk_models.sample_cov`. + + Returns + ------- + pandas.DataFrame + Sample covariance matrix. + """ return risk_models.sample_cov( prices=self._pf._prices_df, returns_data=False, **kwargs ) def exp_cov(self, **kwargs): + """Compute the exponentially-weighted covariance matrix. + + Parameters + ---------- + **kwargs + Additional keyword arguments passed to `risk_models.exp_cov`. + + Returns + ------- + pandas.DataFrame + Exponentially-weighted covariance matrix. + """ return risk_models.exp_cov( prices=self._pf._prices_df, returns_data=False, **kwargs ) def semi_cov(self, **kwargs): + """Compute the semi-covariance matrix. + + Parameters + ---------- + **kwargs + Additional keyword arguments passed to `risk_models.semicovariance`. + + Returns + ------- + pandas.DataFrame + Semi-covariance matrix. + """ return risk_models.semicovariance( prices=self._pf._prices_df, returns_data=False, **kwargs ) def ledoit_wolf_cov(self, shrinkage_target="constant_variance", **kwargs): + """Compute the Ledoit-Wolf covariance matrix with optional shrinkage target. + + Parameters + ---------- + shrinkage_target : str, optional + Shrinkage target for Ledoit-Wolf covariance estimation, default is "constant_variance". + **kwargs + Additional keyword arguments passed to `risk_models.CovarianceShrinkage.ledoit_wolf`. + + Returns + ------- + pandas.DataFrame + Ledoit-Wolf covariance matrix. + """ covshrink = risk_models.CovarianceShrinkage( prices=self._pf._prices_df, returns_data=False, **kwargs ) return covshrink.ledoit_wolf(shrinkage_target=shrinkage_target) def oracle_approximating_cov(self, **kwargs): + """Compute the Oracle-approximating covariance matrix. + + Parameters + ---------- + **kwargs + Additional keyword arguments passed to `risk_models.CovarianceShrinkage.oracle_approximating`. + + Returns + ------- + pandas.DataFrame + Oracle-approximating covariance matrix. + """ covshrink = risk_models.CovarianceShrinkage( prices=self._pf._prices_df, returns_data=False, **kwargs ) @@ -51,26 +138,109 @@ def oracle_approximating_cov(self, **kwargs): @attr.s(frozen=True, cmp=False, slots=True, repr=False) class CorrelationAccessor(accabc.AccessorABC): + """Accessor class for calculating various correlation matrices. + + Attributes + ---------- + _default_kind : str + Default kind of correlation matrix. + _pf : Portfolio + Portfolio object containing prices data. + + Methods + ------- + sample_corr(**kwargs) + Compute the sample correlation matrix. + exp_corr(**kwargs) + Compute the exponentially-weighted correlation matrix. + semi_corr(**kwargs) + Compute the semi-correlation matrix. + ledoit_wolf_corr(**kwargs) + Compute the Ledoit-Wolf correlation matrix. + oracle_approximating_corr(**kwargs) + Compute the Oracle-approximating correlation matrix. + """ + _default_kind = "sample_corr" _pf = attr.ib() def sample_corr(self, **kwargs): + """Compute the sample correlation matrix. + + Parameters + ---------- + **kwargs + Additional keyword arguments passed to `self._pf.covariance.sample_cov`. + + Returns + ------- + pandas.DataFrame + Sample correlation matrix. + """ cov = self._pf.covariance.sample_cov(**kwargs) return risk_models.cov_to_corr(cov) def exp_corr(self, **kwargs): + """Compute the exponentially-weighted correlation matrix. + + Parameters + ---------- + **kwargs + Additional keyword arguments passed to `self._pf.covariance.exp_cov`. + + Returns + ------- + pandas.DataFrame + Exponentially-weighted correlation matrix. + """ cov = self._pf.covariance.exp_cov(**kwargs) return risk_models.cov_to_corr(cov) def semi_corr(self, **kwargs): + """Compute the semi-correlation matrix. + + Parameters + ---------- + **kwargs + Additional keyword arguments passed to `self._pf.covariance.semi_cov`. + + Returns + ------- + pandas.DataFrame + Semi-correlation matrix. + """ cov = self._pf.covariance.semi_cov(**kwargs) return risk_models.cov_to_corr(cov) def ledoit_wolf_corr(self, **kwargs): + """Compute the Ledoit-Wolf correlation matrix. + + Parameters + ---------- + **kwargs + Additional keyword arguments passed to `self._pf.covariance.ledoit_wolf_cov`. + + Returns + ------- + pandas.DataFrame + Ledoit-Wolf correlation matrix. + """ cov = self._pf.covariance.ledoit_wolf_cov(**kwargs) return risk_models.cov_to_corr(cov) def oracle_approximating_corr(self, **kwargs): + """Compute the Oracle-approximating correlation matrix. + + Parameters + ---------- + **kwargs + Additional keyword arguments passed to `self._pf.covariance.oracle_approximating_cov`. + + Returns + ------- + pandas.DataFrame + Oracle-approximating correlation matrix. + """ cov = self._pf.covariance.oracle_approximating_cov(**kwargs) return risk_models.cov_to_corr(cov) diff --git a/garpar/core/ereturns_acc.py b/garpar/core/ereturns_acc.py index 8e22f84..3d59fad 100644 --- a/garpar/core/ereturns_acc.py +++ b/garpar/core/ereturns_acc.py @@ -4,6 +4,8 @@ # License: MIT # Full Text: https://github.com/quatrope/garpar/blob/master/LICENSE +"""Expected Returns Accessor.""" + import attr from pypfopt import expected_returns @@ -11,17 +13,48 @@ from ..utils import accabc # ============================================================================= -# +# EXPECTED RETURNS # ============================================================================= @attr.s(frozen=True, cmp=False, slots=True, repr=False) class ExpectedReturnsAccessor(accabc.AccessorABC): + """Accessor class for computing expected returns of a portfolio. + + Attributes + ---------- + _default_kind : str + Default method for computing expected returns. + _pf : Portfolio + Portfolio object containing prices data. + + Methods + ------- + capm(**kwargs) + Compute expected returns using the CAPM method. + mah(**kwargs) + Compute expected returns using the mean historical method. + emah(**kwargs) + Compute expected returns using the exponential moving average historical method. + """ + _default_kind = "capm" _pf = attr.ib() def capm(self, **kwargs): + """Compute expected returns using the CAPM (Capital Asset Pricing Model) method. + + Parameters + ---------- + **kwargs + Additional keyword arguments passed to expected_returns.capm_return. + + Returns + ------- + pandas.Series + Series containing computed expected returns with name "CAPM". + """ returns = expected_returns.capm_return( prices=self._pf._prices_df, returns_data=False, **kwargs ) @@ -29,6 +62,18 @@ def capm(self, **kwargs): return returns def mah(self, **kwargs): + """Compute expected returns using the mean historical method. + + Parameters + ---------- + **kwargs + Additional keyword arguments passed to expected_returns.mean_historical_return. + + Returns + ------- + pandas.Series + Series containing computed expected returns with name "MAH". + """ returns = expected_returns.mean_historical_return( prices=self._pf._prices_df, returns_data=False, **kwargs ) @@ -36,6 +81,18 @@ def mah(self, **kwargs): return returns def emah(self, **kwargs): + """Compute expected returns using the exponential moving average historical method. + + Parameters + ---------- + **kwargs + Additional keyword arguments passed to expected_returns.ema_historical_return. + + Returns + ------- + pandas.Series + Series containing computed expected returns with name "EMAH". + """ returns = expected_returns.ema_historical_return( prices=self._pf._prices_df, returns_data=False, **kwargs ) diff --git a/garpar/core/plot_acc.py b/garpar/core/plot_acc.py index b859053..a8720a8 100644 --- a/garpar/core/plot_acc.py +++ b/garpar/core/plot_acc.py @@ -4,6 +4,7 @@ # License: MIT # Full Text: https://github.com/quatrope/garpar/blob/master/LICENSE +"""Plot Accessor.""" # ============================================================================= # IMPORTS @@ -23,7 +24,38 @@ @attr.s(frozen=True, cmp=False, slots=True, repr=False) class PortfolioPlotter(accabc.AccessorABC): - """Make plots of Portfolio.""" + """Accessor class for plotting portfolio data. + + Attributes + ---------- + _default_kind : str + Default kind of plot. + _pf : Portfolio + Portfolio object containing data to plot. + + Methods + ------- + line(returns=False, **kwargs) + Plot data as a line plot. + heatmap(returns=False, **kwargs) + Plot data as a heatmap. + wheatmap(**kwargs) + Plot weights as a heatmap. + hist(returns=False, **kwargs) + Plot data as a histogram. + whist(**kwargs) + Plot weights as a histogram. + box(returns=False, **kwargs) + Plot data as a box plot. + wbox(**kwargs) + Plot weights as a box plot. + kde(returns=False, **kwargs) + Plot data as a kernel density estimate plot. + wkde(**kwargs) + Plot weights as a kernel density estimate plot. + ogive(returns=False, **kwargs) + Plot data as an ogive (empirical cumulative distribution function). + """ _default_kind = "line" @@ -32,11 +64,13 @@ class PortfolioPlotter(accabc.AccessorABC): # INTERNAL ================================================================ def _ddf(self, returns): + """Retrieve returns for plotting.""" if returns: return self._pf.as_returns(), "Returns" return self._pf._prices_df, "Price" def _wdf(self): + """Retrieve weights data for plotting.""" # proxy to access the dataframe with the weights return self._pf.weights.to_frame(), "Weights" @@ -46,6 +80,20 @@ def _edf(self): # PLOTS =================================================================== def line(self, returns=False, **kwargs): + """Plot data as a line plot. + + Parameters + ---------- + returns : bool, optional + Whether to plot returns data (default is False). + **kwargs + Additional keyword arguments passed to seaborn.lineplot. + + Returns + ------- + matplotlib.axes.Axes + The matplotlib Axes object containing the plot. + """ data, title = self._ddf(returns=returns) ax = sns.lineplot(data=data, **kwargs) ax.set_title(title) @@ -254,6 +302,20 @@ def wkde(self, **kwargs): return ax def ogive(self, returns=False, **kwargs): + """Plot data as an ogive (empirical cumulative distribution function). + + Parameters + ---------- + returns : bool, optional + Whether to plot returns data (default is False). + **kwargs + Additional keyword arguments passed to seaborn.ecdfplot. + + Returns + ------- + matplotlib.axes.Axes + The matplotlib Axes object containing the plot. + """ data, title = self._ddf(returns=returns) ax = sns.ecdfplot(data=data, **kwargs) ax.set_title(title) diff --git a/garpar/core/risk_acc.py b/garpar/core/risk_acc.py index 62f0c40..ba0e3b3 100644 --- a/garpar/core/risk_acc.py +++ b/garpar/core/risk_acc.py @@ -16,7 +16,7 @@ from ..utils import accabc # ============================================================================= -# +# RISK ACCESSOR # ============================================================================= diff --git a/garpar/core/utilities_acc.py b/garpar/core/utilities_acc.py index 195a369..9e0de28 100644 --- a/garpar/core/utilities_acc.py +++ b/garpar/core/utilities_acc.py @@ -14,7 +14,7 @@ from ..utils import accabc # ============================================================================= -# +# UTILITIES ACCESSOR # ============================================================================= diff --git a/garpar/datasets/base.py b/garpar/datasets/base.py index ba1993c..d8ac2f1 100644 --- a/garpar/datasets/base.py +++ b/garpar/datasets/base.py @@ -4,6 +4,8 @@ # License: MIT # Full Text: https://github.com/quatrope/garpar/blob/master/LICENSE +"""Base Portfolio Maker.""" + # ============================================================================= # IMPORTS # ============================================================================= @@ -25,6 +27,23 @@ class PortfolioMakerABC(mabc.ModelABC): + """ + Abstract base class for defining a portfolio maker. + + Attributes + ---------- + _MKPORT_SIGNATURE : set of str + Expected signature for the make_portfolio method. + + Methods + ------- + __init_subclass__() + Checks the signature of make_portfolio method against _MKPORT_SIGNATURE. + + make_portfolio(*, window_size=5, days=365, stocks=10, price=100, weights=None) + Abstract method to create a portfolio. + """ + _MKPORT_SIGNATURE = { "self", "window_size", @@ -35,6 +54,13 @@ class PortfolioMakerABC(mabc.ModelABC): } def __init_subclass__(cls): + """Ensure that the make_portfolio method in subclasses conforms to _MKPORT_SIGNATURE. + + Raises + ------ + TypeError + If make_portfolio method signature does not match _MKPORT_SIGNATURE. + """ mpsig = inspect.signature(cls.make_portfolio) missing_args = cls._MKPORT_SIGNATURE.difference(mpsig.parameters) if missing_args: @@ -53,6 +79,26 @@ def make_portfolio( price=100, weights=None, ): + """Abstract method to create a portfolio. + + Parameters + ---------- + window_size : int, optional + Window size for portfolio creation (default is 5). + days : int, optional + Number of days for portfolio evaluation (default is 365). + stocks : int, optional + Number of stocks in the portfolio (default is 10). + price : float, optional + Initial price for stocks (default is 100). + weights : array-like or None, optional + Initial weights of stocks (default is None). + + Raises + ------ + NotImplementedError + If the method is not implemented in a subclass. + """ raise NotImplementedError() @@ -62,6 +108,43 @@ def make_portfolio( class RandomEntropyPortfolioMakerABC(PortfolioMakerABC): + """Abstract base class for creating random entropy-based portfolios. + + Attributes + ---------- + entropy : float, default=0.5 + Entropy parameter for portfolio creation. + random_state : numpy.random.Generator, default=None + Random number generator. If None, uses numpy's default generator. + n_jobs : int or None, default=None + Number of jobs to run in parallel. If None, 1 job is run. + verbose : int, default=0 + Verbosity level. + + Methods + ------- + get_window_loss_probability(window_size, entropy) + Abstract method to calculate the loss probability for a given window size and entropy. + make_stock_price(price, loss, random) + Abstract method to calculate the stock price based on initial price, loss, and random generator. + _coerce_price(stocks, prices) + Coerces the initial prices into an array of float values. + _make_stocks_seeds(stocks) + Generate seeds for random number generation for each stock. + _make_loss_sequence(days, loss_probability, random) + Generate a sequence of losses based on the given loss probability. + _make_stock(days, loss_probability, stock_idx, initial_price, random) + Generate a DataFrame for a single stock with random prices based on loss sequence. + make_portfolio(*, window_size=5, days=365, stocks=10, price=100, weights=None) + Create a portfolio of stocks with random prices and specified parameters. + + Notes + ----- + This class extends PortfolioMakerABC and provides methods to generate random + portfolios based on entropy and loss probabilities. + """ + + # HYPERS ================================================================ entropy = mabc.hparam(default=0.5) random_state = mabc.hparam( default=None, converter=np.random.default_rng, repr=False @@ -73,15 +156,74 @@ class RandomEntropyPortfolioMakerABC(PortfolioMakerABC): @mabc.abstractmethod def get_window_loss_probability(self, window_size, entropy): + """Abstract method to calculate the loss probability for a given window size and entropy. + + Parameters + ---------- + window_size : int + Window size for portfolio creation. + entropy : float + Entropy parameter for portfolio creation. + + Returns + ------- + float + Loss probability for the given window size and entropy. + + Raises + ------ + NotImplementedError + If the method is not implemented in a subclass. + """ raise NotImplementedError() @mabc.abstractmethod def make_stock_price(self, price, loss, random): + """Abstract method to calculate the stock price based on initial price, loss, and random generator. + + Parameters + ---------- + price : float + Initial price of the stock. + loss : bool + Whether there is a loss on the current day. + random : numpy.random.Generator + Random number generator. + + Returns + ------- + float + Updated stock price based on loss and randomness. + + Raises + ------ + NotImplementedError + If the method is not implemented in a subclass. + """ raise NotImplementedError() # INTERNAL ================================================================ def _coerce_price(self, stocks, prices): + """Coerces the initial prices into an array of float values. + + Parameters + ---------- + stocks : int + Number of stocks. + prices : int, float, or array-like + Initial prices of stocks. + + Returns + ------- + numpy.ndarray + Array of initial prices. + + Raises + ------ + ValueError + If the number of prices does not match the number of stocks. + """ if isinstance(prices, (int, float)): prices = np.full(stocks, prices, dtype=float) elif len(prices) != stocks: @@ -89,6 +231,18 @@ def _coerce_price(self, stocks, prices): return np.asarray(prices, dtype=float) def _make_stocks_seeds(self, stocks): + """Generate seeds for random number generation for each stock. + + Parameters + ---------- + stocks : int + Number of stocks. + + Returns + ------- + numpy.ndarray + Array of seeds for random number generation. + """ iinfo = np.iinfo(int) seeds = self.random_state.integers( low=0, @@ -101,6 +255,22 @@ def _make_stocks_seeds(self, stocks): return seeds def _make_loss_sequence(self, days, loss_probability, random): + """Generate a sequence of losses based on the given loss probability. + + Parameters + ---------- + days : int + Number of days. + loss_probability : float + Probability of loss on each day. + random : numpy.random.Generator + Random number generator. + + Returns + ------- + numpy.ndarray + Boolean array indicating loss (True) or no loss (False) on each day. + """ win_probability = 1.0 - loss_probability # primero seleccionamos con las probabilidades adecuadas si en cada @@ -129,6 +299,26 @@ def _make_stock( initial_price, random, ): + """Generate a DataFrame for a single stock with random prices based on loss sequence. + + Parameters + ---------- + days : int + Number of days. + loss_probability : float + Probability of loss on each day. + stock_idx : int + Index of the stock. + initial_price : float + Initial price of the stock. + random : numpy.random.Generator + Random number generator. + + Returns + ------- + pandas.DataFrame + DataFrame containing the stock prices for each day. + """ # determinamos que dia se pierde y que dia se gana loss_sequence = self._make_loss_sequence( days, loss_probability, random @@ -162,6 +352,26 @@ def make_portfolio( price=100, weights=None, ): + """Create a portfolio of stocks with random prices and specified parameters. + + Parameters + ---------- + window_size : int, optional + Window size for portfolio creation (default is 5). + days : int, optional + Number of days for portfolio evaluation (default is 365). + stocks : int, optional + Number of stocks in the portfolio (default is 10). + price : int, float, or array-like, optional + Initial price or prices of stocks (default is 100). + weights : array-like or None, optional + Initial weights of stocks (default is None). + + Returns + ------- + Portfolio + Portfolio object representing the generated portfolio. + """ if window_size <= 0: raise ValueError("'window_size' must be > 0") if days < window_size: diff --git a/garpar/datasets/data/__init__.py b/garpar/datasets/data/__init__.py index ae9b2ee..7a04237 100644 --- a/garpar/datasets/data/__init__.py +++ b/garpar/datasets/data/__init__.py @@ -4,6 +4,8 @@ # License: MIT # Full Text: https://github.com/quatrope/garpar/blob/master/LICENSE +"""MERVAL dataset.""" + import os import pathlib @@ -15,9 +17,7 @@ def load_MERVAL(imputation="ffill", first=None, last=None): - """ - Argentine stock market prices (MERVAL). Unlisted shares were eliminated. - """ + """Argentine stock market prices (MERVAL). Unlisted shares were eliminated.""" df = pd.read_csv(DATA_PATH / "merval.csv", index_col="Days") df.index = pd.to_datetime(df.index) df.sort_index(inplace=True) diff --git a/garpar/datasets/multisector.py b/garpar/datasets/multisector.py index c4b03e1..8917881 100644 --- a/garpar/datasets/multisector.py +++ b/garpar/datasets/multisector.py @@ -4,6 +4,8 @@ # License: MIT # Full Text: https://github.com/quatrope/garpar/blob/master/LICENSE +"""Multisector.""" + import numpy as np import pandas as pd @@ -14,10 +16,42 @@ class MultiSector(PortfolioMakerABC): + """Portfolio maker for creating a multi-sector portfolio. + + Attributes + ---------- + makers : tuple + Tuple of (maker_name, PortfolioMakerABC) pairs representing different sector makers. + + Methods + ------- + make_portfolio(*, window_size=5, days=365, stocks=10, price=100, weights=None) + Creates a multi-sector portfolio based on specified parameters. + + Notes + ----- + This class extends PortfolioMakerABC and allows for creating a portfolio with + multiple sectors, each handled by a different PortfolioMakerABC instance. + """ + makers = mabc.hparam(converter=lambda v: tuple(dict(v).items())) @makers.validator def _makers_validator(self, attribute, value): + """Validate 'makers' attribute. + + Parameters + ---------- + attribute : str + Name of the attribute being validated. + value : tuple + Tuple of (maker_name, maker) pairs. + + Raises + ------ + ValueError + If there are fewer than 2 makers provided or any maker is not an instance of PortfolioMakerABC. + """ if len(value) < 2: raise ValueError("You must provide at least 2 makers") for maker_name, maker in value: @@ -27,6 +61,27 @@ def _makers_validator(self, attribute, value): raise TypeError(msg) def _coerce_price(self, stocks, prices, makers_len): + """Coerces initial prices into arrays split by the number of makers. + + Parameters + ---------- + stocks : int + Number of stocks. + prices : int, float, or array-like + Initial prices of stocks. + makers_len : int + Number of sector makers. + + Returns + ------- + numpy.ndarray + Array of initial prices split by the number of makers. + + Raises + ------ + ValueError + If the number of prices does not match the number of stocks. + """ if isinstance(prices, (int, float)): prices = np.full(stocks, prices, dtype=float) elif len(prices) != stocks: @@ -37,6 +92,26 @@ def _coerce_price(self, stocks, prices, makers_len): def make_portfolio( self, *, window_size=5, days=365, stocks=10, price=100, weights=None ): + """Create a multi-sector portfolio based on specified parameters. + + Parameters + ---------- + window_size : int, optional + Window size for portfolio creation (default is 5). + days : int, optional + Number of days for portfolio evaluation (default is 365). + stocks : int, optional + Number of stocks in the portfolio (default is 10). + price : int, float, or array-like, optional + Initial price or prices of stocks (default is 100). + weights : array-like or None, optional + Initial weights of stocks (default is None). + + Returns + ------- + Portfolio + Portfolio object representing the generated multi-sector portfolio. + """ makers_len = len(self.makers) if stocks < makers_len: raise ValueError(f"stocks must be >= {makers_len}") @@ -84,6 +159,42 @@ def make_portfolio( def make_multisector(*makers, **kwargs): + """Create a multi-sector portfolio using specified sector makers. + + Parameters + ---------- + *makers : variable-length arguments + Instances of PortfolioMakerABC representing sector makers. + **kwargs : keyword arguments + Additional parameters passed to MultiSector.make_portfolio. + + Returns + ------- + Portfolio + Multi-sector portfolio object generated by MultiSector.make_portfolio. + + Notes + ----- + This function creates a multi-sector portfolio by initializing a MultiSector + object with unique names for each sector maker and then calling make_portfolio + with specified parameters. + + Example + ------- + Example usage: + + >>> from mymodule import make_multisector, CustomSectorMaker1, CustomSectorMaker2 + + >>> port = make_multisector( + >>> CustomSectorMaker1(), + >>> CustomSectorMaker2(), + >>> window_size=7, + >>> days=250, + >>> stocks=15, + >>> price=200, + >>> weights=[0.2, 0.3, 0.5] + >>> ) + """ names = [type(maker).__name__.lower() for maker in makers] named_makers = unique_names(names=names, elements=makers) diff --git a/garpar/datasets/risso.py b/garpar/datasets/risso.py index e00bc80..46e9bc5 100644 --- a/garpar/datasets/risso.py +++ b/garpar/datasets/risso.py @@ -4,6 +4,8 @@ # License: MIT # Full Text: https://github.com/quatrope/garpar/blob/master/LICENSE +"""Risso Portfolio Maker.""" + # ============================================================================= # IMPORTS # ============================================================================= @@ -24,6 +26,35 @@ def argnearest(arr, v): + """Find the index of the element in the array `arr` that is nearest to the value `v`. + + Parameters + ---------- + arr : array_like + Input array. + v : scalar + Value to which the elements of `arr` will be compared. + + Returns + ------- + idx : int + Index of the element in `arr` that is closest to `v`. + + Notes + ----- + If there are multiple elements at the same distance from `v`, the index of the first + occurrence is returned. + + Examples + -------- + >>> arr = np.array([1, 3, 5, 7, 9]) + >>> argnearest(arr, 4) + 1 + >>> argnearest(arr, 6) + 2 + >>> argnearest(arr, 8) + 4 + """ diff = np.abs(np.subtract(arr, v)) idx = np.argmin(diff) return idx @@ -35,7 +66,47 @@ def argnearest(arr, v): class RissoABC(RandomEntropyPortfolioMakerABC): + """Implementation of a portfolio maker based on entropy calculation by Risso. + + This class extends RandomEntropyPortfolioMakerABC and implements methods + for calculating candidate entropies and selecting loss probabilities based + on a given window size and target entropy. + + Attributes + ---------- + entropy : float + Target entropy value for portfolio optimization. + random_state : numpy.random.Generator + Random number generator instance for reproducibility. + n_jobs : int, optional + Number of parallel jobs to run. Default is None. + verbose : int, optional + Verbosity level. Default is 0. + + Methods + ------- + candidate_entropy(window_size) + Calculate candidate entropies and corresponding loss probabilities. + + get_window_loss_probability(window_size, entropy) + Get the loss probability that corresponds to the nearest candidate + entropy value to the target entropy. + """ + def candidate_entropy(self, window_size): + """Calculate candidate entropies and corresponding loss probabilities. + + Parameters + ---------- + window_size : int + Size of the sliding window for entropy calculation. + + Returns + ------- + tuple + Tuple containing the calculated modified entropy values and + corresponding loss probabilities. + """ loss_probability = np.linspace(0.0, 1.0, num=window_size + 1) # Se corrigen probabilidades porque el cálculo de la entropía trabaja @@ -52,6 +123,21 @@ def candidate_entropy(self, window_size): return modificated_entropy, loss_probability def get_window_loss_probability(self, window_size, entropy): + """Get the loss probability that corresponds to the nearest candidate entropy value to the target entropy. + + Parameters + ---------- + window_size : int + Size of the sliding window for entropy calculation. + entropy : float + Target entropy value for portfolio optimization. + + Returns + ------- + float + Loss probability that corresponds to the nearest candidate entropy + value to the target entropy. + """ h_candidates, loss_probabilities = self.candidate_entropy(window_size) idx = argnearest(h_candidates, entropy) loss_probability = loss_probabilities[idx] @@ -63,10 +149,49 @@ def get_window_loss_probability(self, window_size, entropy): # NORMAL # ============================================================================= class RissoUniform(RissoABC): + """Implementation of a portfolio maker using a uniform distribution for price changes. + + This class extends RissoABC and overrides the method make_stock_price to simulate + stock price changes based on a uniform distribution within specified bounds. + + Attributes + ---------- + low : float, optional + Lower bound of the uniform distribution for daily returns. Default is 1.0. + high : float, optional + Upper bound of the uniform distribution for daily returns. Default is 5.0. + + Methods + ------- + make_stock_price(price, loss, random) + Calculate the new stock price based on the current price, loss flag, and a random + number generator following a uniform distribution. + + Notes + ----- + Inherits from RissoABC and utilizes its methods for entropy-based portfolio optimization. + """ + low = mabc.hparam(default=1, converter=float) high = mabc.hparam(default=5, converter=float) def make_stock_price(self, price, loss, random): + """Calculate the new stock price based on the current price, loss flag, and a random number generator following a uniform distribution. + + Parameters + ---------- + price : float + Current price of the stock. + loss : bool + Flag indicating if it's a loss day (True) or a gain day (False). + random : numpy.random.Generator + Random number generator instance. + + Returns + ------- + float + New price of the stock after simulating the daily return. + """ if price == 0.0: return 0.0 sign = -1 if loss else 1 @@ -85,6 +210,35 @@ def make_risso_uniform( verbose=0, **kwargs, ): + """Create a portfolio using RissoUniform portfolio maker. + + Parameters + ---------- + low : float, optional + Lower bound of the uniform distribution for daily returns. Default is 1.0. + high : float, optional + Upper bound of the uniform distribution for daily returns. Default is 5.0. + entropy : float, optional + Entropy parameter controlling the randomness in portfolio creation. Default is 0.5. + random_state : {None, int, numpy.random.Generator}, optional + Seed or Generator for the random number generator. Default is None. + n_jobs : int, optional + Number of parallel jobs to run. Default is None. + verbose : int, optional + Verbosity level. Default is 0. + **kwargs + Additional keyword arguments passed to the make_portfolio method of RissoUniform. + + Returns + ------- + Portfolio + Generated portfolio instance. + + Notes + ----- + This function initializes a RissoUniform portfolio maker with specified parameters, + generates a portfolio using those parameters, and returns the resulting portfolio object. + """ maker = RissoUniform( low=low, high=high, @@ -101,10 +255,56 @@ def make_risso_uniform( # NORMAL # ============================================================================= class RissoNormal(RissoABC): + """Portfolio maker implementing a stochastic model with normal distribution for daily returns. + + Parameters + ---------- + mu : float, optional + Mean of the normal distribution for daily returns. Default is 0.0. + sigma : float, optional + Standard deviation of the normal distribution for daily returns. Default is 0.2. + entropy : float, optional + Entropy parameter controlling the randomness in portfolio creation. Default is 0.5. + random_state : {None, int, numpy.random.Generator}, optional + Seed or Generator for the random number generator. Default is None. + n_jobs : int, optional + Number of parallel jobs to run. Default is None. + verbose : int, optional + Verbosity level. Default is 0. + + Notes + ----- + This class extends RissoABC and implements a portfolio maker using a normal distribution + model for daily returns. The make_stock_price method generates stock prices based on + the normal distribution parameters (mu, sigma). + """ + mu = mabc.hparam(default=0, converter=float) sigma = mabc.hparam(default=0.2, converter=float) def make_stock_price(self, price, loss, random): + """Generate a new stock price based on current price, daily return direction, and normal distribution parameters. + + Parameters + ---------- + price : float + Current price of the stock. + loss : bool + Flag indicating if it's a loss day (True) or gain day (False). + random : numpy.random.Generator + Random number generator instance. + + Returns + ------- + float + New price of the stock after daily price change. + + Notes + ----- + This method calculates a new stock price based on the current price, + the direction of daily return (loss or gain), and the parameters of + the normal distribution (mu, sigma). + """ if price == 0.0: return 0.0 sign = -1 if loss else 1 @@ -123,6 +323,35 @@ def make_risso_normal( verbose=0, **kwargs, ): + """Create a portfolio using RissoNormal portfolio maker. + + Parameters + ---------- + mu : float, optional + Mean of the normal distribution for daily returns. Default is 0.0. + sigma : float, optional + Standard deviation of the normal distribution for daily returns. Default is 0.2. + entropy : float, optional + Entropy parameter controlling the randomness in portfolio creation. Default is 0.5. + random_state : {None, int, numpy.random.Generator}, optional + Seed or Generator for the random number generator. Default is None. + n_jobs : int, optional + Number of parallel jobs to run. Default is None. + verbose : int, optional + Verbosity level. Default is 0. + **kwargs + Additional keyword arguments passed to the make_portfolio method of RissoNormal. + + Returns + ------- + Portfolio + Generated portfolio instance. + + Notes + ----- + This function initializes a RissoNormal portfolio maker with specified parameters, + generates a portfolio using those parameters, and returns the resulting portfolio object. + """ maker = RissoNormal( mu=mu, sigma=sigma, @@ -180,6 +409,34 @@ def get_value(self, sign, refresher, random): class RissoLevyStable(RissoABC): + """Portfolio maker implementing a stochastic model with Levy stable distribution for daily returns. + + Parameters + ---------- + alpha : float, optional + Shape parameter of the Levy stable distribution. Default is 1.6411. + beta : float, optional + Scale parameter of the Levy stable distribution. Default is -0.0126. + mu : float, optional + Location parameter (mean) of the Levy stable distribution. Default is 0.0005. + sigma : float, optional + Scale parameter (spread) of the Levy stable distribution. Default is 0.005. + entropy : float, optional + Entropy parameter controlling the randomness in portfolio creation. Default is 0.5. + random_state : {None, int, numpy.random.Generator}, optional + Seed or Generator for the random number generator. Default is None. + n_jobs : int, optional + Number of parallel jobs to run. Default is None. + verbose : int, optional + Verbosity level. Default is 0. + + Notes + ----- + This class extends RissoABC and implements a portfolio maker using Levy stable + distribution model for daily returns. The make_stock_price method generates stock + prices based on the Levy stable distribution parameters (alpha, beta, mu, sigma). + """ + alpha = mabc.hparam(default=1.6411, converter=float) # shape beta = mabc.hparam(default=-0.0126, converter=float) # scale mu = mabc.hparam(default=0.0005, converter=float) # loc @@ -191,11 +448,40 @@ class RissoLevyStable(RissoABC): @levy_stable_.default def _levy_stable_default(self): + """Initialize the Levy stable distribution object. + + Returns + ------- + scipy.stats._continuous_distns.levy_stable_gen + Levy stable distribution object initialized with the specified parameters. + """ return scipy.stats.levy_stable( alpha=self.alpha, beta=self.beta, loc=self.mu, scale=self.sigma ) def make_stock_price(self, price, loss, random): + """Generate a new stock price based on current price, daily return direction, and Levy stable distribution parameters. + + Parameters + ---------- + price : float + Current price of the stock. + loss : bool + Flag indicating if it's a loss day (True) or gain day (False). + random : numpy.random.Generator + Random number generator instance. + + Returns + ------- + float + New price of the stock after daily price change. + + Notes + ----- + This method calculates a new stock price based on the current price, + the direction of daily return (loss or gain), and the parameters of + the Levy stable distribution (alpha, beta, mu, sigma). + """ if price == 0.0: return 0.0 sign = -1 if loss else 1 @@ -217,6 +503,37 @@ def make_risso_levy_stable( verbose=0, **kwargs, ): + """Create a portfolio using the RissoLevyStable portfolio maker. + + Parameters + ---------- + alpha : float, optional + Shape parameter of the Levy stable distribution. Default is 1.6411. + beta : float, optional + Scale parameter of the Levy stable distribution. Default is -0.0126. + mu : float, optional + Location parameter (mean) of the Levy stable distribution. Default is 0.0005. + sigma : float, optional + Scale parameter (spread) of the Levy stable distribution. Default is 0.005. + entropy : float, optional + Entropy parameter controlling the randomness in portfolio creation. Default is 0.5. + random_state : {None, int, numpy.random.Generator}, optional + Seed or Generator for the random number generator. Default is None. + n_jobs : int, optional + Number of parallel jobs to run. Default is None. + verbose : int, optional + Verbosity level. Default is 0. + + Returns + ------- + Portfolio + Portfolio object representing the created portfolio. + + Notes + ----- + This function initializes a RissoLevyStable portfolio maker with the provided + parameters and creates a portfolio using the make_portfolio method. + """ maker = RissoLevyStable( alpha=alpha, beta=beta, diff --git a/garpar/utils/entropy.py b/garpar/utils/entropy.py index 3829905..16d0663 100644 --- a/garpar/utils/entropy.py +++ b/garpar/utils/entropy.py @@ -4,12 +4,30 @@ # License: MIT # Full Text: https://github.com/quatrope/garpar/blob/master/LICENSE +"""Entropy.""" + from scipy import stats import warnings def shannon(prices, window_size=None, **kwargs): + """Calculate the Shannon entropy of the given prices. + + Parameters + ---------- + prices : array_like + Prices data to calculate entropy. + window_size : int, optional + Ignored parameter for Shannon entropy calculation. + **kwargs + Additional keyword arguments to pass to stats.entropy. + + Returns + ------- + array_like + The Shannon entropy of the prices along axis 0. + """ if window_size is not None: warnings.warn( f"'window_size={window_size}' is ignored in shannon entropy" @@ -18,5 +36,25 @@ def shannon(prices, window_size=None, **kwargs): def risso(prices, window_size, **kwargs): + """Calculate the Risso entropy of the given prices. + + Parameters + ---------- + prices : array_like + Description of prices parameter. + window_size : int + Description of window_size parameter. + **kwargs + Additional keyword arguments. + + Raises + ------ + ValueError + If 'window_size' is not valid. + + Returns + ------- + None + """ if not window_size or window_size < 0: raise ValueError(f"'window_size' must be >= 0") diff --git a/garpar/utils/mabc.py b/garpar/utils/mabc.py index d902fdd..2029273 100644 --- a/garpar/utils/mabc.py +++ b/garpar/utils/mabc.py @@ -4,6 +4,8 @@ # License: MIT # Full Text: https://github.com/quatrope/garpar/blob/master/LICENSE +"""Metadata utilities.""" + import attr from abc import ABCMeta, abstractmethod # noqa @@ -73,12 +75,31 @@ def mproperty(**kwargs): @attr.s(repr=False) class ModelABC(metaclass=ABCMeta): + """ + Base class for all model classes. + + This class provides a base for all model classes in the project. It is + designed to be used with the `attrs` library, and it ensures that all + inherited classes are decorated with `attr.s()` and have a frozen + configuration. + + Parameters + ---------- + None + + Attributes + ---------- + __model_cls_config__ : dict + Class configuration for `attr.s()`. + """ + __model_cls_config__ = {"repr": False, "frozen": True} def __init_subclass__(cls): - """Initiate of subclasses. + """ + Initiate of subclasses. - It ensures that every inherited class is decorated by ``attr.s()`` and + It ensures that every inherited class is decorated by `attr.s()` and assigns as class configuration the parameters defined in the class variable `__portfolio_maker_cls_config__`. @@ -90,6 +111,16 @@ def __init_subclass__(cls): class Decomposer(PortfolioMakerABC): pass + Parameters + ---------- + cls : type + The class being initialized. + + Returns + ------- + type + The decorated class. + """ model_config = getattr(cls, MODEL_CONFIG) acls = attr.s(maybe_cls=cls, **model_config) @@ -97,7 +128,21 @@ class Decomposer(PortfolioMakerABC): return acls def __repr__(self): - """x.__repr__() <==> repr(x).""" + """ + x.__repr__() <==> repr(x). + + Returns a string representation of the object. + + Parameters + ---------- + None + + Returns + ------- + str + String representation of the object. + + """ clsname = type(self).__name__ selfd = attr.asdict(