Optimise some objective function. While an implicit requirement is that the function
        can be optimised via a quadratic optimiser, this is not enforced. Thus there is a
        decent chance of silent failure.

        :param objective_function: function which maps (weight, args) -> cost
        :type objective_function: function with signature (np.ndarray, args) -> float
        :return: asset weights that optimise the custom objective
        :rtype: dict

def custom_objective(self, objective_function, *args):
        """
        Optimise some objective function. While an implicit requirement is that the function
        can be optimised via a quadratic optimiser, this is not enforced. Thus there is a
        decent chance of silent failure.

        :param objective_function: function which maps (weight, args) -> cost
        :type objective_function: function with signature (np.ndarray, args) -> float
        :return: asset weights that optimise the custom objective
        :rtype: dict
        """
        result = sco.minimize(
            objective_function,
            x0=self.initial_guess,
            args=args,
            method="SLSQP",
            bounds=self.bounds,
            constraints=self.constraints,
        )
        self.weights = result["x"]
        return dict(zip(self.tickers, self.weights))