Expected Returns

Mean-variance optimisation requires knowledge of the expected returns. In practice, these are rather difficult to know with any certainty. Thus the best we can do is to come up with estimates, for example by extrapolating historical data, This is where the main flaw in efficient frontier lies – the optimisation procedure is sound, and provides strong mathematical guarantees, given the correct inputs. This is one of the reasons why I have emphasised modularity: users should be able to come up with their own superior models and feed them into the optimiser.

Caution

In my experience, supplying expected returns often does more harm than good. If predicting stock returns were as easy as calcualting the mean historical return, we’d all be rich! For most use-cases, I would suggest that you focus your efforts on choosing an appropriate risk model (see Risk Models)

The expected_returns module provides functions for estimating the expected returns of the assets, which is a required input in mean-variance optimisation.

By convention, the output of these methods are expected annual returns. It is assumed that daily prices are provided, though in reality the functions are agnostic to the time period (just change the frequency parameter). Asset prices must be given as a pandas dataframe, as per the format described in the User Guide.

All of the functions process the price data into percentage returns data, before calculating their respective estimates of expected returns.

Currently implemented:
  • mean historical return
  • exponentially weighted mean historical return

Additionally, we provide utility functions to convert from returns to prices and vice-versa.

pypfopt.expected_returns.mean_historical_return(prices, frequency=252)

Calculate annualised mean (daily) historical return from input (daily) asset prices.

Parameters:
  • prices (pd.DataFrame) – adjusted closing prices of the asset, each row is a date and each column is a ticker/id.
  • frequency (int, optional) – number of time periods in a year, defaults to 252 (the number of trading days in a year)
Returns:

annualised mean (daily) return for each asset

Return type:

pd.Series

This is probably the default textbook approach. It is intuitive and easily interpretable, however the estimates are unlikely to be accurate. This is a problem especially in the context of a quadratic optimiser, which will maximise the erroneous inputs, In some informal backtests, I’ve found that vanilla efficient frontier portfolios (using mean historical returns and sample covariance) actually do have a statistically significant outperformance over the S&P500 (in the order of 3-5%), though the same isn’t true for cryptoasset portfolios. At some stage, I may redo these backtests rigorously and add them to the repo (see the Roadmap and Changelog page for more).

pypfopt.expected_returns.ema_historical_return(prices, frequency=252, span=500)

Calculate the exponentially-weighted mean of (daily) historical returns, giving higher weight to more recent data.

Parameters:
  • prices (pd.DataFrame) – adjusted closing prices of the asset, each row is a date and each column is a ticker/id.
  • frequency (int, optional) – number of time periods in a year, defaults to 252 (the number of trading days in a year)
  • span (int, optional) – the time-span for the EMA, defaults to 500-day EMA.
Returns:

annualised exponentially-weighted mean (daily) return of each asset

Return type:

pd.Series

The exponential moving average is a simple improvement over the mean historical return; it gives more credence to recent returns and thus aims to increase the relevance of the estimates. This is parameterised by the span parameter, which gives users the ability to decide exactly how much more weight is given to recent data. Generally, I would err on the side of a higher span – in the limit, this tends towards the mean historical return. However, if you plan on rebalancing much more frequently, there is a case to be made for lowering the span in order to capture recent trends.

pypfopt.expected_returns.returns_from_prices(prices)

Calculate the returns given prices.

Parameters:prices (pd.DataFrame) – adjusted (daily) closing prices of the asset, each row is a date and each column is a ticker/id.
Returns:(daily) returns
Return type:pd.DataFrame
pypfopt.expected_returns.prices_from_returns(returns)

Calculate the pseudo-prices given returns. These are not true prices because the initial prices are all set to 1, but it behaves as intended when passed to any PyPortfolioOpt method.

Parameters:returns (pd.DataFrame) – (daily) percentage returns of the assets
Returns:(daily) pseudo-prices.
Return type:pd.DataFrame