Hey everyone!

Attached below is my code. It has worked well in the Quant book but when put into my algorithm and during backtesting, it keeps telling me the data coming from the ‘_black_litterman_’ function ‘ must be 1 dimensional’, when I'm assured it is. Can someone help me understand my problem and why it keeps failing during backtesting?

NOTE : The problematic line seems to be line 92 : starting ‘mu_bl = …’

 

import pandas as pd
import numpy as np
from scipy.optimize import minimize
from sklearn.linear_model import ElasticNet
from sklearn.model_selection import GridSearchCV
from numpy.linalg import inv
from AlgorithmImports import *
from datetime import datetime
from datetime import timedelta
from io import StringIO
import matplotlib as mpl
from matplotlib import cm
import matplotlib.pyplot as plt
from arch import arch_model

class ghostyfund(QCAlgorithm):

    def Initialize(self):

        self.SetStartDate(2020,1,1)  
        self.SetEndDate(2021,1,1)   
        self.SetCash(10000)            


        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
        self.UniverseSettings.Leverage = 3

        self._changes = None

    def implied_returns(self, delta, sigma, w):
        """
        Inputs:
        delta = Risk aversion coefficient (scalar) (excess return on the market / variance of the market)
        w = portfolio weights (N x 1) as Series
        sigma = N x N covariance matrix as df

        Returns a (N x 1) vector of implied expected returns by reverse engineering the portfolio weights
        """
        ir = delta * sigma.values.dot(w).squeeze()
        return ir

    def proportional_prior(self, sigma, tau, p, index):
        """
        Inputs:
        sigma = N x N covariance matrix as df
        tau = a scalar
        p = a K x N df linking Q & Assets

        Returns a (P x P) df, a matrix representing prior uncertainties
        """
        helit_omega = p.values.dot(tau * sigma.values).dot(p.values.T)
        return pd.DataFrame(np.diag(np.diag(helit_omega)), index=index, columns=index)


    def _black_litterman_(self, w_prior, sigma_prior, p, p_not_values, q, omega=None, delta=2.5, tau=0.2):
        """
        Computes the black litterman expected returns 
        W.prior must be an N x 1 vector of weights, series
        Sigma.prior is an N x N covariance matrix, a df
        P must be an K x N linking Q and the assets, a df
        Q must be a K x 1 vector of views, a series
        Omega must be a K x K matrix, a df

        if omega == None:
            its proportional to the variance of the prior

        Delta, tau are scalars
        """
        index = sigma_prior.index

        def as_column_vec(x):
            if (x.ndim == 2):
                return x
            else:
                return np.expand_dims(x, axis=1)

        if omega is None:
            omega = self.proportional_prior(sigma_prior, tau, p_not_values, index)

        # no of assets
        N = w_prior.shape[0]
        # no of views
        K = q.shape[0]
        # 1) Reverse engineer weights to get pi
        pi = self.implied_returns(delta, sigma_prior, w_prior)
        # 2) Adjust (scale) sigma by the uncertainty scale factor
        sigma_prior_scaled = sigma_prior.values * tau
        sigma_prior = sigma_prior.values
        # note; .dot == matrix multiplication
        mu_bl = pi + sigma_prior_scaled.dot(p.T).dot(inv(p.dot(sigma_prior_scaled).dot(p.T) + omega).dot(q - p.dot(pi)))
        # posterior estimate of uncertainty of mu_bl
        sigma_bl = sigma_prior + sigma_prior_scaled - sigma_prior_scaled.dot(p.T).dot(inv(p.dot(sigma_prior_scaled).dot(p.T) + omega)).dot(p).dot(sigma_prior_scaled)

        return (mu_bl, sigma_bl)



    def black_litty_implied_rets(self, tickers):
        q = pd.Series(index=tickers, data=[0.03]*int(len(tickers)))
        p = pd.DataFrame(columns=tickers, index=range(0, len(tickers)))
        w = pd.Series({}, index=tickers).fillna(float(1/len(tickers)))
        
        delta = 2.5
        tau = .02
        
        for i in range(0, len(p.columns)):
            p.iloc[i, i] = 1
        
        p = p.fillna(0)
        
        history = self.History(tickers, 60, Resolution.Daily)["close"]
        sigma_prior = history.pct_change().unstack(level=0).dropna().cov()
        index = sigma_prior.index
        
        pi = self.implied_returns(delta, sigma_prior, w)
        omega = self.proportional_prior(sigma_prior, tau, p, index)
        
        bl = self._black_litterman_(w_prior=w, sigma_prior=sigma_prior, p=p.values, p_not_values=p, q=q)
    
        df = pd.DataFrame(bl[0])
        df["Ticker"] = tickers
        df = df.pivot_table(columns="Ticker")
        exp_rets = df.sort_values(by=0, ascending=False, axis=1)
        #highest_exp_ret = exp_rets.iloc[0].index[0]

        return exp_rets

    def monte_carlo_gbm(self, n_years, n_scenarios, mu, sigma, steps_per_year, s_0):
        """
        Evolution of Geometric Brownian Motion trajectories, such as for Stock Prices through Monte Carlo
        :param n_years:  The number of years to generate data for
        :param n_paths: The number of scenarios/trajectories
        :param mu: Annualized Drift, e.g. Market Return
        :param sigma: Annualized Volatility
        :param steps_per_year: granularity of the simulation
        :param s_0: initial value
        :return: a numpy array of n_paths columns and n_years*steps_per_year rows
        """
        dt = 1/steps_per_year
        n_steps = int(n_years*steps_per_year) + 1
        rets_plus_1 = np.random.normal(loc=mu*dt+1, scale=sigma*np.sqrt(dt), size=(n_steps, n_scenarios))
        rets_plus_1[0] = 1
        prices = s_0*pd.DataFrame(rets_plus_1).cumprod()
        return prices


    def get_max_drawdown(self, array):
        drawdowns = []
        for i in range(len(array)):
            max_array = max(array[:i+1])
            drawdown = max_array - array[i]
            drawdowns.append(drawdown)

        return max(drawdowns)



    def CoarseSelectionFunction(self, coarse):
        sortedByDollarVolume = sorted(coarse, key=lambda x: x.DollarVolume, reverse=True)
        return [x.Symbol for x in sortedByDollarVolume]

    def FineSelectionFunction(self, fine):
        sortedByPeRatio = sorted(fine, key=lambda x: x.ValuationRatios.PERatio)
        tickers = [x.Symbol for x in sortedByPeRatio][:5]
        return tickers

            

    def OnData(self, data):
        if self._changes is None: 
            return

        tickers = [x.Symbol for x in self._changes.AddedSecurities]

        mu = self.black_litty_implied_rets(tickers)
        iteration = 0
        avg_drawdowns = []    

        for ticker in tickers:
            symbol = self.AddEquity(ticker).Symbol
            symbol_hist = self.History(symbol, 252, Resolution.Daily)["close"]
            returns = symbol_hist.pct_change().dropna().unstack(level=0)
            vol = returns.rolling(window=30, center=False).std().dropna()

            am = arch_model(vol, vol='Garch', p=1, o=0, q=1, dist='Normal')
            res = am.fit(disp='off')
            garch_implied_vol = 0.1 * np.sqrt(res.params['omega'] + res.params['alpha[1]'] * res.resid**2 + 
                                   res.conditional_volatility**2 * res.params['beta[1]'])

      
            mc = self.monte_carlo_gbm(n_years=1, n_scenarios=2500, 
            mu=mu.iloc[:, iteration], sigma=garch_implied_vol.max(), steps_per_year=252, s_0=100)
    
            drawdowns = []
            for i in range(0, len(mc.columns)):
                drawdown = self.get_max_drawdown(mc[i])
                drawdowns.append(drawdown)

            avg_drawd = np.mean(drawdowns)
            avg_drawdowns.append(avg_drawd)

            iteration += 1
            
        df = pd.DataFrame(avg_drawdowns)
        df["Ticker"] = tickers
        df = df.pivot_table(columns="Ticker")
        expected_drawdowns = df.sort_values(by=0, ascending=True, axis=1)

        ticker_symbol =  expected_drawdowns.columns[0]


        self.SetHoldings(ticker_symbol, 1)


        self._changes = None


    def OnSecuritiesChanged(self, changes):
        self._changes = changes

Author