def get_risk_contribution(cls, factors_rets: SimpleReturnsDataFrame,
                              weigths_of_assets: QFSeries,
                              portfolio_rets: SimpleReturnsSeries) -> QFSeries:
        """
        Calculates risk contribution of different factors to the portfolio. Risk is defined as volatility.
        Uses x-sigma-rho formula (MSCI Bara paper).

        Parameters
        ----------
        factors_rets
            dataframe consisted of returns for different assets
        weigths_of_assets
            series of weights of each asset. It's indexed with names of assets.
        portfolio_rets
            return of the whole portfolio

        Returns
        -------
        pandas.Series
            Series of risk contributions (one for each asset) to the portfolio. It's indexed with names of assets.
        """
        volatility_of_returns = factors_rets.std(axis=0)

        correlation_asset_portfolio = factors_rets.apply(
            lambda series: series.corr(portfolio_rets))

        risk_contribution = weigths_of_assets * volatility_of_returns * correlation_asset_portfolio

        normalized_risk_contribution_msci = risk_contribution / risk_contribution.sum(
        )
        return normalized_risk_contribution_msci
Example #2
0
    def setUp(self):
        self.dates = pd.date_range(start='2015-05-13', periods=5)
        self.column_names = ['a', 'b', 'c', 'd', 'e']

        self.prices_values = [[1., 1., 1., 1, 1.], [2., 2., 2., 2., 2.],
                              [3., 3., 3., 3., 3.], [4., 4., 4., 4., 4.],
                              [5., 5., 5., 5., 5.]]
        self.test_prices_df = PricesDataFrame(data=self.prices_values,
                                              index=self.dates,
                                              columns=self.column_names)

        self.log_returns_values = [
            [0.693147, 0.693147, 0.693147, 0.693147, 0.693147],
            [0.405465, 0.405465, 0.405465, 0.405465, 0.405465],
            [0.287682, 0.287682, 0.287682, 0.287682, 0.287682],
            [0.223144, 0.223144, 0.223144, 0.223144, 0.223144]
        ]
        self.test_log_returns_df = LogReturnsDataFrame(
            data=self.log_returns_values,
            index=self.dates[1:],
            columns=self.column_names)

        self.simple_returns_values = [
            [1.000000, 1.000000, 1.000000, 1.000000, 1.000000],
            [0.500000, 0.500000, 0.500000, 0.500000, 0.500000],
            [0.333333, 0.333333, 0.333333, 0.333333, 0.333333],
            [0.250000, 0.250000, 0.250000, 0.250000, 0.250000]
        ]
        self.test_simple_returns_df = SimpleReturnsDataFrame(
            data=self.simple_returns_values,
            index=self.dates[1:],
            columns=self.column_names)
Example #3
0
    def setUp(self):
        portfolio_rets = [0.01, 0.02, -0.03, 0.04, -0.05, 0.06]
        asset_1_rets = [0.011, 0.035, -0.028, 0.039, -0.044, 0.061]
        asset_2_rets = [0.02, 0.04, -0.06, 0.08, -0.1, 0.12]
        dates = pd.date_range(start='2015-02-01', periods=6)

        self.portfolio_tms = SimpleReturnsSeries(portfolio_rets, dates)
        returns_array = np.array([asset_1_rets, asset_2_rets]).T
        self.factors_df = SimpleReturnsDataFrame(data=returns_array, index=dates, columns=['a', 'b'])
Example #4
0
    def test_aggregate_by_year(self):
        dates = pd.DatetimeIndex(['2015-06-01', '2015-12-30', '2016-01-01', '2016-05-01'])
        test_dataframe = SimpleReturnsDataFrame(data=self.simple_returns_values, index=dates)

        expected_aggregated_rets = [[2.000000, 2.000000, 2.000000, 2.000000, 2.000000],
                                    [0.666666, 0.666666, 0.666666, 0.666666, 0.666666]]
        expected_dataframe = SimpleReturnsDataFrame(data=expected_aggregated_rets,
                                                    index=pd.DatetimeIndex(['2015-12-31', '2016-12-31']))

        actual_dataframe = test_dataframe.aggregate_by_year()

        assert_dataframes_equal(expected_dataframe, actual_dataframe)
def get_analysed_tms_and_regressors(dates_span: int = 1000, num_of_regressors: int = 7,
                                    start_date: datetime.datetime = str_to_date('2016-01-01'),
                                    mean_return: float = 0.001, std_of_returns: float = 0.02,
                                    a_coeff: float = -0.25, b_coeff: float = 1.25, intercept: float = 0.004)\
        -> Tuple[SimpleReturnsSeries, SimpleReturnsDataFrame]:
    """
    Creates a dataframe with simple returns of sample timeseries (regressors). Then creates a series which linearly
    depends on regressors 'a' and 'b'.
    """
    dates = pd.bdate_range(start=start_date, periods=dates_span)
    regressors_names = generate_sample_column_names(
        num_of_columns=num_of_regressors)
    np.random.seed(
        5
    )  # init random number generator with a fixed number, so that results are always the same

    regressors_data = np.random.normal(mean_return, std_of_returns,
                                       (dates_span, num_of_regressors))
    regressors_df = SimpleReturnsDataFrame(data=regressors_data,
                                           index=dates,
                                           columns=regressors_names)

    analyzed_data = a_coeff * regressors_data[:, 0] + b_coeff * regressors_data[:, 1] + \
        np.random.normal(0, 0.02, dates_span) + intercept

    analysed_tms = SimpleReturnsSeries(data=analyzed_data,
                                       index=dates,
                                       name='Fund')

    return analysed_tms, regressors_df
def convert_dataframe_frequency(
        dataframe: QFDataFrame,
        frequency: Frequency) -> SimpleReturnsDataFrame:
    """
    Converts each column in the dataframe to the specified frequency.
    ValueError is raised when a column has a lower frequency than the one we are converting to.
    """
    # Verify that all columns in the dataframe have a lower frequency.
    data_frequencies = dataframe.get_frequency()
    for column, col_frequency in data_frequencies.items():
        if col_frequency < frequency:
            raise ValueError(
                "Column '{}' cannot be converted to '{}' frequency because its frequency is '{}'."
                .format(column, frequency, col_frequency))

    if frequency == Frequency.DAILY:
        return dataframe.to_simple_returns()

    filled_df = dataframe.to_prices().fillna(method="ffill")
    new_columns = {}
    for column in filled_df:
        new_columns[column] = get_aggregate_returns(filled_df[column],
                                                    frequency)

    return SimpleReturnsDataFrame(new_columns)
Example #7
0
    def setUp(self):
        dates_span = 100
        regressor_names = ['a', 'b', 'c']

        dates = pd.date_range(start='2015-01-01', periods=dates_span, freq='D')

        fund_returns_tms = SimpleReturnsSeries(
            data=[i / 100 for i in range(1, dates_span + 1)], index=dates)
        deviation = 0.005
        fit_returns_tms = SimpleReturnsSeries(data=(fund_returns_tms.values +
                                                    deviation),
                                              index=dates)

        regressors_returns_df = SimpleReturnsDataFrame(data=np.array([
            fund_returns_tms, fund_returns_tms + deviation,
            fund_returns_tms - deviation
        ]).T,
                                                       index=dates,
                                                       columns=regressor_names)
        coefficients = QFSeries(index=regressor_names, data=[1.0, 1.0, 1.0])

        self.fund_returns_tms = fund_returns_tms
        self.fit_returns_tms = fit_returns_tms
        self.regressors_returns_df = regressors_returns_df
        self.coefficients = coefficients

        self.alpha = 0.005
Example #8
0
def generate_random_paths(sample_len: int,
                          sample_size: int,
                          mean: float,
                          std: float,
                          leverage: float = 1.0):
    """ Generates random paths.

    Parameters
    ------------
    sample_len: int
        length of each path of data, equivalent to time
    sample_size: int
        Number of paths simulated
    mean: float
        mean simle return
    std: float
        standard deviation of returns
    leverage: float
        leverage used in the simulated investment process

    Returns
    -----------
    SimpleReturnsDataFrame
        indexed by steps with paths as columns
    """
    mean = mean * leverage
    std = std * leverage

    time = np.arange(1, 1 + sample_len)

    returns_vector = np.random.normal(loc=mean,
                                      scale=std,
                                      size=(sample_len * sample_size, 1))
    returns = np.reshape(returns_vector, (sample_len, sample_size))
    return SimpleReturnsDataFrame(data=returns, index=time)
Example #9
0
    def make_scenarios(self, trade_rets: Sequence[float], scenarios_length: int = 100, num_of_scenarios: int = 10000) \
            -> SimpleReturnsDataFrame:
        """
        Utility function to generate different trades scenarios, where each scenario is a series of returns for a given
        investment strategy.
        The scenarios of a given length are created by randomly choosing (with replacement) returns from the original
        sequence of a Trade's returns. The result is the SimpleReturnsDataFrame which is indexed by the Trade's
        ordinal number and has a scenario in each column.

        Parameters
        ----------
        trade_rets: Sequence[float]
            sequence of floats which represent the returns on Trades performed by some investment strategy
        scenarios_length: int
            number of Trades which should simulated for each scenario
        num_of_scenarios: int
            number of scenarios which should be generated

        Returns
        -------
        SimpleReturnsDataFrame
            data frame of size scenarios_length (rows) by num_of_scenarios (columns). It contains float numbers.
        """
        values = np.random.choice(trade_rets,
                                  scenarios_length * num_of_scenarios)
        values = np.reshape(values, (scenarios_length, num_of_scenarios))

        return SimpleReturnsDataFrame(values)
Example #10
0
    def _calculate_box(
            asset_returns_df: SimpleReturnsDataFrame) -> SimpleReturnsSeries:
        portfolio = EqualRiskContributionPortfolio(asset_returns_df.cov())
        weights = portfolio.get_weights()
        portfolio_rets, _ = Portfolio.constant_weights(asset_returns_df,
                                                       weights)

        return portfolio_rets
Example #11
0
    def drifting_weights(cls, assets_rets_df: SimpleReturnsDataFrame, weights: pd.Series) \
            -> Tuple[SimpleReturnsSeries, QFDataFrame]:
        """
        Calculates the time series of portfolio returns (given the initial weights of portfolio's assets).
        Weights of assets change over time because there is no rebalancing.

        The method also calculates the allocation matrix which shows what portfolio consists of on each date.

        Parameters
        ----------
        assets_rets_df
            simple returns of assets which create the portfolio
        weights
            weights of assets which create the portfolio

        Returns
        -------
        portfolio_rets_tms
            timeseries of portfolio's returns
        allocation_df
            dataframe indexed with dates and showing allocations in time (one column per asset)
        """
        assert len(weights) == assets_rets_df.num_of_columns

        weights_sum = weights.sum()
        if abs(weights_sum - 1.0) > cls.EPSILON:
            cls.logger().warning(
                "Sum of all weights is not equal to 1.0: sum(weights) = {:f}".
                format(weights_sum))

        # create a data frame with cumulative returns with a row of zeroes at the beginning
        assets_prices_df = assets_rets_df.to_prices(
            initial_prices=weights.values)
        portfolio_total_value_tms = cast_series(assets_prices_df.sum(axis=1),
                                                PricesSeries)
        portfolio_rets = portfolio_total_value_tms.to_simple_returns()
        portfolio_rets *= weights_sum  # scale returns so that they correspond to the level of investment

        # to get an allocation matrix one needs to divide each row of assets' prices by the cumulative
        # portfolio return at that time
        portfolio_total_values = portfolio_total_value_tms.values.reshape(
            (-1, 1))  # make it a vertical vector
        normalizing_factor = np.tile(portfolio_total_values,
                                     (1, assets_prices_df.num_of_columns))
        allocation_matrix = assets_prices_df.values / normalizing_factor

        # to keep the correct level of investment values in allocation matrix need to be multiplied by the sum
        # of weights
        allocation_matrix *= weights_sum
        allocation_matrix = allocation_matrix[:-1, :]

        allocation_df = QFDataFrame(index=assets_rets_df.index.copy(),
                                    columns=assets_rets_df.columns.copy(),
                                    data=allocation_matrix)

        return portfolio_rets, allocation_df
Example #12
0
    def _create_test_dataframe(cls):
        values = [[np.nan, 0.0, 0.0, 0.0, 0.0], [1.0, np.nan, 1.0, 1.0, 1.0],
                  [2.0, np.nan, np.nan, 2.0, 2.0],
                  [3.0, 3.0, 3.0, np.nan, 3.0], [4.0, 4.0, 4.0, 4.0, 4.0],
                  [5.0, 5.0, 5.0, 5.0, 5.0]]

        index = pd.date_range(start='2015-01-01', periods=6)
        columns = ['a', 'b', 'c', 'd', 'e']
        dataframe = SimpleReturnsDataFrame(data=values,
                                           index=index,
                                           columns=columns)

        return dataframe
Example #13
0
    def test_proxy_using_values(self):
        expected_values = [[0.0, 0.0, 0.0, 0.0], [1.0, 1.0, 1.0, 1.0],
                           [2.0, 0.0, 2.0, 2.0], [3.0, 3.0, 0.0, 3.0],
                           [4.0, 4.0, 4.0, 4.0], [5.0, 5.0, 5.0, 5.0]]
        expected_columns = ['a', 'c', 'd', 'e']
        expected_dates = self.test_dataframe.index.copy()
        expected_dataframe = SimpleReturnsDataFrame(data=expected_values,
                                                    columns=expected_columns,
                                                    index=expected_dates)
        self.data_cleaner.threshold = 0.2

        actual_dataframe = self.data_cleaner.proxy_using_value(proxy_value=0.0)

        assert_dataframes_equal(expected_dataframe, actual_dataframe)
Example #14
0
    def test_proxy_using_regression(self):
        expected_values = [[np.nan, 0.0, 0.0, 0.0], [1.0, 1.0, 1.0, 1.0],
                           [2.0, 2.0, 2.0, 2.0], [3.0, 3.0, 3.0, 3.0],
                           [4.0, 4.0, 4.0, 4.0], [5.0, 5.0, 5.0, 5.0]]
        expected_columns = ['a', 'c', 'd', 'e']
        expected_dates = self.test_dataframe.index.copy()
        expected_dataframe = SimpleReturnsDataFrame(data=expected_values,
                                                    columns=expected_columns,
                                                    index=expected_dates)
        self.data_cleaner.threshold = 0.2

        actual_dataframe = self.data_cleaner.proxy_using_regression(
            benchmark_tms=self.test_benchmark,
            columns_type=SimpleReturnsSeries)

        assert_dataframes_equal(expected_dataframe, actual_dataframe)
Example #15
0
    def form_different_is_and_oos_sets(
            self, multiple_returns_timeseries: QFDataFrame) -> Tuple:
        """
        Splits slices into two groups of equal sizes for all possible combinations.

        Returns an list of tuples. 1st element of the tuple contains the In-Sample set and the 2nd one contains the
        Out-Of-Sample set (both in form of QFDataFrames). Each tuple contains one of possible combinations
        of slices forming IS and OOS sets. E.g. if there are 4 slices: A,B,C,D then one of possible
        combinations is IS: A,B and OOS: C,D. The given example will be one of rows of the result list.
        A and B (C and D) will be concatenated (so that there will be one timeseries AB),
        and so will be one CD timeseries.
        """
        # Drop all rows not aligned to num_of_slices. E.g if the df has 233 rows and the num_of_slices is 50,
        # then last 33 rows of the original matrix will be dropped
        rows_to_keep = (multiple_returns_timeseries.num_of_rows //
                        self.num_of_slices) * self.num_of_slices
        aligned_df = multiple_returns_timeseries.iloc[:rows_to_keep]
        if aligned_df.empty:
            raise ValueError("Too few rows in the data frame.")

        size_of_slices = aligned_df.num_of_rows // self.num_of_slices
        df_slices = [
            SimpleReturnsDataFrame(aligned_df.iloc[i:i + size_of_slices, :])
            for i in range(0, len(aligned_df), size_of_slices)
        ]

        # The index order of the original data frame should be preserved in both IS and OOS dfs
        new_index = aligned_df.index[:len(aligned_df) // 2]
        is_dfs = [
            pd.concat(slices) for slices in itertools.combinations(
                df_slices, self.num_of_slices // 2)
        ]
        oos_dfs = [
            aligned_df.loc[aligned_df.index.difference(df.index)]
            for df in is_dfs
        ]

        # Adjust the indices at the end, after the oos_dfs and is_dfs are already computed
        for df in itertools.chain(is_dfs, oos_dfs):
            df.index = new_index

        return is_dfs, oos_dfs
    def get_risk_contribution_optimised(
            cls, assets_rets: SimpleReturnsDataFrame,
            weights_of_assets: QFSeries) -> QFSeries:
        """
        Calculates risk contribution of each asset of the portfolio.

        Parameters
        ----------
        assets_rets
            returns of assets building the portfolio (each assets in a separate column)
        weights_of_assets
            Series of weights (one for each asset).  It's indexed with names of assets.

        Returns
        -------
        Series of risk contributions (one for each asset). It's indexed with names of assets
        """

        assets_covariance = assets_rets.cov()
        risk_contribution = cls._get_normalized_risk_contribution(
            assets_covariance, weights_of_assets)

        return risk_contribution
Example #17
0
class TestDataFrames(TestCase):
    def setUp(self):
        self.dates = pd.date_range(start='2015-05-13', periods=5)
        self.column_names = ['a', 'b', 'c', 'd', 'e']

        self.prices_values = [[1, 1, 1, 1, 1], [2, 2, 2, 2,
                                                2], [3, 3, 3, 3, 3],
                              [4, 4, 4, 4, 4], [5, 5, 5, 5, 5]]
        self.test_prices_df = PricesDataFrame(data=self.prices_values,
                                              index=self.dates,
                                              columns=self.column_names)

        self.log_returns_values = [
            [0.693147, 0.693147, 0.693147, 0.693147, 0.693147],
            [0.405465, 0.405465, 0.405465, 0.405465, 0.405465],
            [0.287682, 0.287682, 0.287682, 0.287682, 0.287682],
            [0.223144, 0.223144, 0.223144, 0.223144, 0.223144]
        ]
        self.test_log_returns_df = LogReturnsDataFrame(
            data=self.log_returns_values,
            index=self.dates[1:],
            columns=self.column_names)

        self.simple_returns_values = [
            [1.000000, 1.000000, 1.000000, 1.000000, 1.000000],
            [0.500000, 0.500000, 0.500000, 0.500000, 0.500000],
            [0.333333, 0.333333, 0.333333, 0.333333, 0.333333],
            [0.250000, 0.250000, 0.250000, 0.250000, 0.250000]
        ]
        self.test_simple_returns_df = SimpleReturnsDataFrame(
            data=self.simple_returns_values,
            index=self.dates[1:],
            columns=self.column_names)

    def test_num_of_columns(self):
        self.assertEqual(self.test_prices_df.num_of_columns, 5)

    def test_prices_to_log_returns(self):
        expected_dataframe = self.test_log_returns_df

        actual_dataframe = self.test_prices_df.to_log_returns()
        assert_dataframes_equal(expected_dataframe, actual_dataframe)

    def test_simple_to_log_returns(self):
        expected_dataframe = self.test_log_returns_df

        actual_dataframe = self.test_simple_returns_df.to_log_returns()
        assert_dataframes_equal(expected_dataframe, actual_dataframe)

    def test_log_to_log_returns(self):
        expected_dataframe = self.test_log_returns_df

        actual_dataframe = self.test_log_returns_df.to_log_returns()
        assert_dataframes_equal(expected_dataframe, actual_dataframe)

    def test_log_to_simple_returns(self):
        expected_dataframe = self.test_simple_returns_df

        actual_dataframe = self.test_log_returns_df.to_simple_returns()
        assert_dataframes_equal(expected_dataframe, actual_dataframe)

    def test_simple_to_simple_returns(self):
        expected_dataframe = self.test_simple_returns_df

        actual_dataframe = self.test_simple_returns_df.to_simple_returns()
        assert_dataframes_equal(expected_dataframe, actual_dataframe)

    def test_prices_to_simple_returns(self):
        expected_dataframe = self.test_simple_returns_df

        actual_dataframe = self.test_prices_df.to_simple_returns()
        assert_dataframes_equal(expected_dataframe, actual_dataframe)

    def test_log_returns_to_prices(self):
        expected_dataframe = self.test_prices_df

        actual_dataframe = self.test_log_returns_df.to_prices()
        assert_dataframes_equal(expected_dataframe, actual_dataframe)

    def test_simple_returns_to_prices(self):
        expected_dataframe = self.test_prices_df

        actual_dataframe = self.test_simple_returns_df.to_prices()
        assert_dataframes_equal(expected_dataframe, actual_dataframe)

    def test_prices_to_prices(self):
        expected_dataframe = self.test_prices_df

        actual_dataframe = self.test_prices_df.to_prices()
        assert_dataframes_equal(expected_dataframe, actual_dataframe)

    def test_min_max_normalized(self):
        normalized_prices = [[0.00, 0.00, 0.00, 0.00, 0.00],
                             [0.25, 0.25, 0.25, 0.25, 0.25],
                             [0.50, 0.50, 0.50, 0.50, 0.50],
                             [0.75, 0.75, 0.75, 0.75, 0.75],
                             [1.00, 1.00, 1.00, 1.00, 1.00]]
        expected_dataframe = PricesDataFrame(data=normalized_prices,
                                             index=self.dates,
                                             columns=self.column_names)

        actual_dataframe = self.test_prices_df.min_max_normalized()

        assert_dataframes_equal(expected_dataframe, actual_dataframe)

    def test_exponential_average(self):
        smoothed_values = [[1.000000, 1.000000, 1.000000, 1.000000, 1.000000],
                           [1.940000, 1.940000, 1.940000, 1.940000, 1.940000],
                           [2.936400, 2.936400, 2.936400, 2.936400, 2.936400],
                           [3.936184, 3.936184, 3.936184, 3.936184, 3.936184],
                           [4.936171, 4.936171, 4.936171, 4.936171, 4.936171]]
        expected_dataframe = PricesDataFrame(data=smoothed_values,
                                             index=self.dates,
                                             columns=self.column_names)

        actual_dataframe = self.test_prices_df.exponential_average()

        assert_dataframes_equal(expected_dataframe, actual_dataframe)

    def test_aggregate_by_year(self):
        dates = pd.DatetimeIndex(
            ['2015-06-01', '2015-12-30', '2016-01-01', '2016-05-01'])
        test_dataframe = SimpleReturnsDataFrame(
            data=self.simple_returns_values, index=dates)

        expected_aggregated_rets = [[
            2.000000, 2.000000, 2.000000, 2.000000, 2.000000
        ], [0.666666, 0.666666, 0.666666, 0.666666, 0.666666]]
        expected_dataframe = SimpleReturnsDataFrame(
            data=expected_aggregated_rets,
            index=pd.DatetimeIndex(['2015-12-31', '2016-12-31']))

        actual_dataframe = test_dataframe.aggregate_by_year()

        assert_dataframes_equal(expected_dataframe, actual_dataframe)

    def test_rolling_time_window(self):
        actual_result = self.test_prices_df.rolling_time_window(
            window_length=2, step=1, func=lambda x: x.mean())
        expected_values = [[1.5, 1.5, 1.5, 1.5,
                            1.5], [2.5, 2.5, 2.5, 2.5, 2.5],
                           [3.5, 3.5, 3.5, 3.5, 3.5],
                           [4.5, 4.5, 4.5, 4.5, 4.5]]
        expected_index = self.test_prices_df.index[-4:].copy(deep=True)
        expected_columns = ['a', 'b', 'c', 'd', 'e']
        expected_result = QFDataFrame(expected_values, expected_index,
                                      expected_columns)
        assert_dataframes_equal(expected_result,
                                actual_result,
                                absolute_tolerance=1e-20)

        actual_result = self.test_prices_df.rolling_time_window(
            window_length=2, step=1, func=lambda x: x.mean().mean())
        expected_values = [1.5, 2.5, 3.5, 4.5]
        expected_index = self.test_prices_df.index[-4:].copy(deep=True)
        expected_result = QFSeries(expected_values, expected_index)
        assert_series_equal(expected_result,
                            actual_result,
                            absolute_tolerance=1e-20)

    def test_total_cumulative_return(self):
        actual_result = self.test_prices_df.total_cumulative_return()
        expected_result = pd.Series(index=self.test_prices_df.columns,
                                    data=[4.0, 4.0, 4.0, 4.0, 4.0])
        assert_series_equal(expected_result, actual_result)
Example #18
0
class TestRiskContributionAnalysis(TestCase):
    def setUp(self):
        portfolio_rets = [0.01, 0.02, -0.03, 0.04, -0.05, 0.06]
        asset_1_rets = [0.011, 0.035, -0.028, 0.039, -0.044, 0.061]
        asset_2_rets = [0.02, 0.04, -0.06, 0.08, -0.1, 0.12]
        dates = pd.date_range(start='2015-02-01', periods=6)

        self.portfolio_tms = SimpleReturnsSeries(portfolio_rets, dates)
        returns_array = np.array([asset_1_rets, asset_2_rets]).T
        self.factors_df = SimpleReturnsDataFrame(data=returns_array, index=dates, columns=['a', 'b'])

    def test_get_risk_contribution(self):
        weights = pd.Series([0.5, 0.5], index=self.factors_df.columns)
        actual_result = RiskContributionAnalysis.get_risk_contribution(factors_rets=self.factors_df,
                                                                       weigths_of_assets=weights,
                                                                       portfolio_rets=self.portfolio_tms)
        expected_result = pd.Series([0.32739478440485410, 0.672605215595146], index=self.factors_df.columns)
        assert_series_equal(expected_result, actual_result)

        weights = pd.Series([0.25, 0.75], index=self.factors_df.columns)
        actual_result = RiskContributionAnalysis.get_risk_contribution(factors_rets=self.factors_df,
                                                                       weigths_of_assets=weights,
                                                                       portfolio_rets=self.portfolio_tms)
        expected_result = pd.Series([0.139601453264340, 0.860398546735660], index=self.factors_df.columns)
        assert_series_equal(expected_result, actual_result)

    def test_get_risk_contribution_optimised(self):
        weights = pd.Series([0.5, 0.5], index=self.factors_df.columns)
        actual_result = RiskContributionAnalysis.get_risk_contribution_optimised(assets_rets=self.factors_df,
                                                                                 weights_of_assets=weights)
        expected_result = pd.Series([0.328845104104390, 0.671154895895610], index=self.factors_df.columns)
        assert_series_equal(expected_result, actual_result)

        weights = pd.Series([0.25, 0.75], index=self.factors_df.columns)
        actual_result = RiskContributionAnalysis.get_risk_contribution_optimised(assets_rets=self.factors_df,
                                                                                 weights_of_assets=weights)
        expected_result = pd.Series([0.139939367445589, 0.860060632554411], index=self.factors_df.columns)
        assert_series_equal(expected_result, actual_result)

    def test_get_distance_to_equal_risk_contrib(self):
        factors_covariance = self.factors_df.cov()
        weights = pd.Series([0.5, 0.5], index=self.factors_df.columns)
        actual_result = RiskContributionAnalysis.get_distance_to_equal_risk_contrib(factors_covariance, weights)
        expected_result = 0.342309791791
        self.assertAlmostEqual(actual_result, expected_result, places=10)

        weights = pd.Series([0.25, 0.75], index=self.factors_df.columns)
        actual_result = RiskContributionAnalysis.get_distance_to_equal_risk_contrib(factors_covariance, weights)
        expected_result = 0.72012126510882146
        self.assertAlmostEqual(actual_result, expected_result, places=10)

    def test_is_equal_risk_contribution(self):
        asset_a_tms = self.factors_df.loc[:, 'a']
        factors_df = pd.concat((asset_a_tms, asset_a_tms), axis=1)
        factors_df = cast_dataframe(factors_df, SimpleReturnsDataFrame)
        factors_df.columns = ['a', 'b']
        factors_covariance = factors_df.cov()

        weights = pd.Series([0.25, 0.75], index=self.factors_df.columns)
        actual_result = RiskContributionAnalysis.is_equal_risk_contribution(factors_covariance, weights)
        self.assertFalse(actual_result)

        weights = pd.Series([0.5, 0.5], index=self.factors_df.columns)
        actual_result = RiskContributionAnalysis.is_equal_risk_contribution(factors_covariance, weights)
        self.assertTrue(actual_result)
Example #19
0
class TestDataFrames(TestCase):
    def setUp(self):
        self.dates = pd.date_range(start='2015-05-13', periods=5)
        self.column_names = ['a', 'b', 'c', 'd', 'e']

        self.prices_values = [[1., 1., 1., 1, 1.], [2., 2., 2., 2., 2.],
                              [3., 3., 3., 3., 3.], [4., 4., 4., 4., 4.],
                              [5., 5., 5., 5., 5.]]
        self.test_prices_df = PricesDataFrame(data=self.prices_values,
                                              index=self.dates,
                                              columns=self.column_names)

        self.log_returns_values = [
            [0.693147, 0.693147, 0.693147, 0.693147, 0.693147],
            [0.405465, 0.405465, 0.405465, 0.405465, 0.405465],
            [0.287682, 0.287682, 0.287682, 0.287682, 0.287682],
            [0.223144, 0.223144, 0.223144, 0.223144, 0.223144]
        ]
        self.test_log_returns_df = LogReturnsDataFrame(
            data=self.log_returns_values,
            index=self.dates[1:],
            columns=self.column_names)

        self.simple_returns_values = [
            [1.000000, 1.000000, 1.000000, 1.000000, 1.000000],
            [0.500000, 0.500000, 0.500000, 0.500000, 0.500000],
            [0.333333, 0.333333, 0.333333, 0.333333, 0.333333],
            [0.250000, 0.250000, 0.250000, 0.250000, 0.250000]
        ]
        self.test_simple_returns_df = SimpleReturnsDataFrame(
            data=self.simple_returns_values,
            index=self.dates[1:],
            columns=self.column_names)

    def test_num_of_columns(self):
        self.assertEqual(self.test_prices_df.num_of_columns, 5)

    def test_prices_dataframe_dtypes(self):
        dtypes = self.test_prices_df.dtypes
        self.assertEqual({dtype("float64")}, set(dtypes))

    def test_log_returns_dataframe_dtypes(self):
        dtypes = self.test_log_returns_df.dtypes
        self.assertEqual({dtype("float64")}, set(dtypes))

    def test_simple_returns_dataframe_dtypes(self):
        dtypes = self.test_simple_returns_df.dtypes
        self.assertEqual({dtype("float64")}, set(dtypes))

    def test_prices_to_log_returns(self):
        expected_dataframe = self.test_log_returns_df

        actual_dataframe = self.test_prices_df.to_log_returns()
        assert_dataframes_equal(expected_dataframe, actual_dataframe)

    def test_simple_to_log_returns(self):
        expected_dataframe = self.test_log_returns_df

        actual_dataframe = self.test_simple_returns_df.to_log_returns()
        assert_dataframes_equal(expected_dataframe, actual_dataframe)
        self.assertEqual({dtype("float64")}, set(actual_dataframe.dtypes))

    def test_log_to_log_returns(self):
        expected_dataframe = self.test_log_returns_df

        actual_dataframe = self.test_log_returns_df.to_log_returns()
        assert_dataframes_equal(expected_dataframe, actual_dataframe)
        self.assertEqual({dtype("float64")}, set(actual_dataframe.dtypes))

    def test_log_to_simple_returns(self):
        expected_dataframe = self.test_simple_returns_df

        actual_dataframe = self.test_log_returns_df.to_simple_returns()
        assert_dataframes_equal(expected_dataframe, actual_dataframe)
        self.assertEqual({dtype("float64")}, set(actual_dataframe.dtypes))

    def test_simple_to_simple_returns(self):
        expected_dataframe = self.test_simple_returns_df

        actual_dataframe = self.test_simple_returns_df.to_simple_returns()
        assert_dataframes_equal(expected_dataframe, actual_dataframe)
        self.assertEqual({dtype("float64")}, set(actual_dataframe.dtypes))

    def test_prices_to_simple_returns(self):
        expected_dataframe = self.test_simple_returns_df

        actual_dataframe = self.test_prices_df.to_simple_returns()
        assert_dataframes_equal(expected_dataframe, actual_dataframe)
        self.assertEqual({dtype("float64")}, set(actual_dataframe.dtypes))

    def test_log_returns_to_prices(self):
        expected_dataframe = self.test_prices_df

        actual_dataframe = self.test_log_returns_df.to_prices()
        assert_dataframes_equal(expected_dataframe, actual_dataframe)
        self.assertEqual({dtype("float64")}, set(actual_dataframe.dtypes))

    def test_simple_returns_to_prices(self):
        expected_dataframe = self.test_prices_df

        actual_dataframe = self.test_simple_returns_df.to_prices()
        assert_dataframes_equal(expected_dataframe, actual_dataframe)
        self.assertEqual({dtype("float64")}, set(actual_dataframe.dtypes))

    def test_prices_to_prices(self):
        expected_dataframe = self.test_prices_df

        actual_dataframe = self.test_prices_df.to_prices()
        assert_dataframes_equal(expected_dataframe, actual_dataframe)
        self.assertEqual({dtype("float64")}, set(actual_dataframe.dtypes))

    def test_min_max_normalized(self):
        normalized_prices = [[0.00, 0.00, 0.00, 0.00, 0.00],
                             [0.25, 0.25, 0.25, 0.25, 0.25],
                             [0.50, 0.50, 0.50, 0.50, 0.50],
                             [0.75, 0.75, 0.75, 0.75, 0.75],
                             [1.00, 1.00, 1.00, 1.00, 1.00]]
        expected_dataframe = PricesDataFrame(data=normalized_prices,
                                             index=self.dates,
                                             columns=self.column_names)

        actual_dataframe = self.test_prices_df.min_max_normalized()

        assert_dataframes_equal(expected_dataframe, actual_dataframe)
        self.assertEqual({dtype("float64")}, set(actual_dataframe.dtypes))

    def test_exponential_average(self):
        smoothed_values = [[1.000000, 1.000000, 1.000000, 1.000000, 1.000000],
                           [1.940000, 1.940000, 1.940000, 1.940000, 1.940000],
                           [2.936400, 2.936400, 2.936400, 2.936400, 2.936400],
                           [3.936184, 3.936184, 3.936184, 3.936184, 3.936184],
                           [4.936171, 4.936171, 4.936171, 4.936171, 4.936171]]
        expected_dataframe = PricesDataFrame(data=smoothed_values,
                                             index=self.dates,
                                             columns=self.column_names)

        actual_dataframe = self.test_prices_df.exponential_average()

        assert_dataframes_equal(expected_dataframe, actual_dataframe)
        self.assertEqual({dtype("float64")}, set(actual_dataframe.dtypes))

    def test_aggregate_by_year(self):
        dates = pd.DatetimeIndex(
            ['2015-06-01', '2015-12-30', '2016-01-01', '2016-05-01'])
        test_dataframe = SimpleReturnsDataFrame(
            data=self.simple_returns_values, index=dates)

        expected_aggregated_rets = [[
            2.000000, 2.000000, 2.000000, 2.000000, 2.000000
        ], [0.666666, 0.666666, 0.666666, 0.666666, 0.666666]]
        expected_dataframe = SimpleReturnsDataFrame(
            data=expected_aggregated_rets,
            index=pd.DatetimeIndex(['2015-12-31', '2016-12-31']))

        actual_dataframe = test_dataframe.aggregate_by_year()

        assert_dataframes_equal(expected_dataframe, actual_dataframe)
        self.assertEqual({dtype("float64")}, set(actual_dataframe.dtypes))

    def test_rolling_time_window(self):
        actual_result = self.test_prices_df.rolling_time_window(
            window_length=2, step=1, func=lambda x: x.mean())
        expected_values = [[1.5, 1.5, 1.5, 1.5,
                            1.5], [2.5, 2.5, 2.5, 2.5, 2.5],
                           [3.5, 3.5, 3.5, 3.5, 3.5],
                           [4.5, 4.5, 4.5, 4.5, 4.5]]
        expected_index = self.test_prices_df.index[-4:].copy(deep=True)
        expected_columns = ['a', 'b', 'c', 'd', 'e']
        expected_result = QFDataFrame(expected_values, expected_index,
                                      expected_columns)
        assert_dataframes_equal(expected_result,
                                actual_result,
                                absolute_tolerance=1e-20)

        actual_result = self.test_prices_df.rolling_time_window(
            window_length=2, step=1, func=lambda x: x.mean().mean())
        expected_values = [1.5, 2.5, 3.5, 4.5]
        expected_index = self.test_prices_df.index[-4:].copy(deep=True)
        expected_result = QFSeries(expected_values, expected_index)
        assert_series_equal(expected_result,
                            actual_result,
                            absolute_tolerance=1e-20)
        self.assertEqual(dtype("float64"), actual_result.dtypes)

    def test_total_cumulative_return(self):
        actual_result = self.test_prices_df.total_cumulative_return()
        expected_result = PricesSeries(index=self.test_prices_df.columns,
                                       data=[4.0, 4.0, 4.0, 4.0, 4.0])
        assert_series_equal(expected_result, actual_result)
        self.assertEqual(dtype("float64"), actual_result.dtypes)

    def test_stats_functions(self):
        qf_df = QFDataFrame(data=self.prices_values,
                            index=self.dates,
                            columns=self.column_names)
        max_qf_df = qf_df.max()
        expected_max = QFSeries([5, 5, 5, 5, 5],
                                index=['a', 'b', 'c', 'd', 'e'])

        self.assertEqual(type(max_qf_df), QFSeries)
        assert_series_equal(max_qf_df, expected_max)
        self.assertEqual(dtype("float64"), max_qf_df.dtypes)

    def test_squeeze(self):
        qf_df = QFDataFrame(data=self.prices_values,
                            index=self.dates,
                            columns=self.column_names)
        self.assertEqual(type(qf_df[['a']]), QFDataFrame)
        self.assertEqual({dtype("float64")}, set(qf_df[['a']].dtypes))

        self.assertEqual(type(qf_df[['a']].squeeze()), QFSeries)
        self.assertEqual(dtype("float64"), qf_df[['a']].squeeze().dtypes)

        self.assertEqual(type(qf_df[['a', 'b']].squeeze()), QFDataFrame)
        self.assertEqual({dtype("float64")},
                         set(qf_df[['a', 'b']].squeeze().dtypes))

        self.assertEqual(type(qf_df.iloc[[0]]), QFDataFrame)
        self.assertEqual({dtype("float64")}, set(qf_df.iloc[[0]].dtypes))

        self.assertEqual(type(qf_df.iloc[[0]].squeeze()), QFSeries)
        self.assertEqual("float64", qf_df.iloc[[0]].squeeze().dtypes)

    def test_concat(self):
        full_df = QFDataFrame(data=self.prices_values,
                              index=self.dates,
                              columns=self.column_names)

        # Concatenate along index (axis = 0)
        number_of_rows = len(full_df)
        half_df = full_df.iloc[:number_of_rows // 2]
        second_half_df = full_df.iloc[number_of_rows // 2:]
        concatenated_df = pd.concat([half_df, second_half_df])

        self.assertEqual(type(concatenated_df), QFDataFrame)
        self.assertEqual({dtype("float64")}, set(concatenated_df.dtypes))
        assert_dataframes_equal(concatenated_df, full_df)

        # Concatenate along columns (axis = 1)
        number_of_columns = full_df.num_of_columns
        half_df = full_df.loc[:, full_df.columns[:number_of_columns // 2]]
        second_half_df = full_df.loc[:,
                                     full_df.columns[number_of_columns // 2:]]
        concatenated_df = pd.concat([half_df, second_half_df], axis=1)

        self.assertEqual(type(concatenated_df), QFDataFrame)
        self.assertEqual({dtype("float64")}, set(concatenated_df.dtypes))
        assert_dataframes_equal(concatenated_df, full_df)

    def test_concat_series(self):
        index = [1, 2, 3]
        series_1 = QFSeries(data=[17., 15., 16.], index=index)
        series_2 = QFSeries(data=[18., 19., 20.], index=index)

        df = pd.concat([series_1, series_2], axis=1)
        self.assertEqual(type(df), QFDataFrame)
        self.assertEqual({s.dtypes
                          for s in [series_1, series_2]}, set(df.dtypes))

        series = pd.concat([series_1, series_2], axis=0)
        self.assertEqual(type(series), QFSeries)
        self.assertEqual("float64", series.dtypes)
        self.assertEqual({s.dtypes
                          for s in [series_1, series_2]}, {series.dtypes})