docs: def portfolio_performance(
      expected_returns, cov_matrix, weights, verbose=False, risk_free_rate=0.02
  ):
      """
      After optimising, calculate (and optionally print) the performance of the optimal
      portfolio. Currently calculates expected return, volatility, and the Sharpe ratio.
  
      :param expected_returns: expected returns for each asset. Set to None if
                               optimising for volatility only.
      :type expected_returns: pd.Series, list, np.ndarray
      :param cov_matrix: covariance of returns for each asset
      :type cov_matrix: pd.DataFrame or np.array
      :param weights: weights or assets
      :type weights: list, np.array or dict, optional
      :param verbose: whether performance should be printed, defaults to False
      :type verbose: bool, optional
      :param risk_free_rate: risk-free rate of borrowing/lending, defaults to 0.02
      :type risk_free_rate: float, optional
      :raises ValueError: if weights have not been calcualted yet
      :return: expected return, volatility, Sharpe ratio.
      :rtype: (float, float, float)
      """
      if isinstance(weights, dict):
          if isinstance(expected_returns, pd.Series):
              tickers = list(expected_returns.index)
          elif isinstance(cov_matrix, pd.DataFrame):
              tickers = list(cov_matrix.columns)
          else:
              tickers = list(range(len(expected_returns)))
          newweights = np.zeros(len(tickers))
          for i, k in enumerate(tickers):
              if k in weights:
                  newweights[i] = weights[k]
          if newweights.sum() == 0:
              raise ValueError("Weights add to zero, or ticker names don't match")
      elif weights is not None:
          newweights = np.asarray(weights)
      else:
          raise ValueError("Weights is None")
      sigma = np.sqrt(objective_functions.volatility(newweights, cov_matrix))
      mu = newweights.dot(expected_returns)
  
      sharpe = -objective_functions.negative_sharpe(
          newweights, expected_returns, cov_matrix, risk_free_rate = risk_free_rate
      )
      if verbose:
          print("Expected annual return: {:.1f}%".format(100 * mu))
          print("Annual volatility: {:.1f}%".format(100 * sigma))
          print("Sharpe Ratio: {:.2f}".format(sharpe))
      return mu, sigma, sharpe