SciPy «Успешно» находит неправильное оптимальное решение и «Безуспешно» находит оптимальное решение (Построение портфолио) - PullRequest
0 голосов
/ 12 апреля 2020

Я строю торгового бота и пытаюсь внедрить оптимизатор для максимизации альфа-канала при соблюдении определенных ограничений.

Моя переменная - это вектор, содержащий веса ценных бумаг в портфеле. У меня также есть вектор, который содержит соответствующие альфа-баллы для каждой ценной бумаги. Моя целевая функция -сумма (вес * альфа) (чтобы минимизировать отрицательный альфа). У меня есть следующие ограничения:

  • Минимальный и максимальный вес акций в портфеле
  • Минимальная сделка (в процентах от общей стоимости портфеля - для торговли существует фиксированная стоимость, поэтому Я не хочу торговать, если это всего лишь пара изменений базовых пунктов)
  • Максимальный оборот (максимальная общая стоимость сделки в процентах от общей стоимости портфеля)
  • Сумма абсолютных весов необходимо добавить до 1
  • Сумма весов должна равняться 0 (для длинных / коротких) или 1 (только для длинных)

Я создал класс, который реализует ниже это используя scipy.optimize.minimize:

class Optimiser:

    def __init__(self, initial_portfolio, turnover, min_trade, max_wt, longshort=True):
        self.symbols = initial_portfolio.index.to_numpy()
        self.init_wt = initial_portfolio['weight'].to_numpy()
        self.alpha = initial_portfolio['alpha'].to_numpy()
        self.longshort = longshort
        self.turnover = turnover
        self.min_trade = self.init_wt.copy()
        self.set_min_trade(min_trade)
        self.max_wt = max_wt
        if self.longshort:
            self.wt_sum = 0
            self.abs_wt_sum = 1
        else:
            self.wt_sum = 1
            self.abs_wt_sum = 1

    def set_min_trade(self, min_trade):
        for i in range(len(self.init_wt)):
            if abs(self.init_wt[i]) > min_trade:
                self.min_trade[i] = 0.1

    def optimise(self):
        wt_bounds = self.get_stock_wt_bounds()
        constraints = self.get_constraints()
        result = minimize(
            fun=self.minimise_negative_alpha,
            x0=self.init_wt,
            bounds=wt_bounds,
            constraints=constraints,
            options={
                'disp': True,
            }
        )
        return result

    def minimise_negative_alpha(self, opt_wt):
        return -sum(opt_wt * self.alpha)

    def get_stock_wt_bounds(self):
        if self.longshort:
            return tuple((-self.max_wt, self.max_wt) for s in self.init_wt)
        else:
            return tuple((0, self.max_wt) for i in range(len(self.init_wt)))

    def get_constraints(self):
        min_trade = {'type': 'ineq', 'fun': self.min_trade_fn}
        turnover = {'type': 'ineq', 'fun': self.turnover_fn}
        wt_sum = {'type': 'eq', 'fun': self.wt_sum_fn}
        abs_wt_sum = {'type': 'eq', 'fun': self.abs_wt_sum_fn}
        return turnover, wt_sum, abs_wt_sum

    def min_trade_fn(self, opt_wt):
        return self.min_trade - abs(opt_wt - self.init_wt)

    def turnover_fn(self, opt_wt):
        return sum(abs(opt_wt - self.init_wt)) - self.turnover*2

    def wt_sum_fn(self, opt_wt):
        return sum(opt_wt)

    def abs_wt_sum_fn(self, opt_wt):
        return sum(abs(opt_wt)) - self.abs_wt_sum

Как вы можете видеть, я не использую ограничение min_trade, и я коснусь этого позже в этом вопросе.

Вот два примера Я перехожу к этому (эти примеры содержат только 4 акции и в правильной реализации я собираюсь передать массивы 50-100 ценных бумаг):

a)

def run_optimisation():
    initial_portfolio = pd.DataFrame({
        'symbol': ['AAPL', 'MSFT', 'GOOGL', 'TSLA'],
        'weight': [-0.3, -0.2, 0.45, 0.05],
        'alpha': [-0.2, -0.3, 0.25, 0],
    }).set_index('symbol')

    opt = Optimiser(initial_portfolio, turnover=0.3, min_trade=0.1, max_wt=0.4)
    result = opt.optimise()

b)

def run_optimisation():
    initial_portfolio = pd.DataFrame({
        'symbol': ['AAPL', 'MSFT', 'GOOGL', 'TSLA'],
        'weight': [-0.25, -0.25, 0.25, 0.25],
        'alpha': [-0.2, -0.3, 0.25, 0],
    }).set_index('symbol')

    opt = Optimiser(initial_portfolio, turnover=0.3, min_trade=0.1, max_wt=0.4)
    result = opt.optimise()

Результат, который я получаю ом а) для этого длинно-короткого примера: [-0,1, -0,4, 0,25, 0,25], что явно не оптимально [-0,1, -0,4, 0,4, 0,1].

Я получаю это сообщение :

Optimization terminated successfully.    (Exit mode 0)
            Current function value: -0.20249999999999585
            Iterations: 7
            Function evaluations: 42
            Gradient evaluations: 7

Это говорит о том, что оно успешно нашло минимум ... Это как будто он пытается максимизировать ограничение оборота. Это потому, что начальные веса не соответствуют ограничениям? Если так, как я могу изменить это так, как в идеале, я хотел бы передать его текущие веса портфеля как x0.

В б) Я получаю оптимальное решение [-0,1, -0,4, 0,4, 0,1 ] но я получаю False для result.success.

Я также получаю это сообщение:

Positive directional derivative for linesearch    (Exit mode 8)
            Current function value: -0.23999999999776675
            Iterations: 8
            Function evaluations: 34
            Gradient evaluations: 4

Я думаю, это сообщение может означать, что оно не может увеличить / уменьшить цель Много работает с изменениями, и поэтому он не знает, является ли он минимальным, пожалуйста, исправьте меня, если я ошибаюсь. Я пытался возиться с настройкой ftol, но безрезультатно, но я не совсем уверен, как настроить ее оптимально.

Есть ли способ изменить этот оптимизатор так, чтобы: а) он достиг оптимального решения и произвел исправить статус соответственно и б) может принять начальные веса, которые не соответствуют ограничениям? Надежда на будущее заключается также в том, чтобы учесть отраслевые и отраслевые ограничения, чтобы я не мог чрезмерно инвестировать в определенные области.

Кроме того, в качестве дополнительного вопроса (хотя и не так важного, как хотелось бы получить) работа для начала): Как я могу реализовать ограничение минимальной торговли? Мне бы хотелось, чтобы либо акция вообще не продавалась, либо она имеет торговую стоимость сверх этой суммы, либо она торгует на полную стоимость (до нулевого веса, если он меньше веса min_trade в портфеле).

Как вы видите, это очень длинный вопрос, но я был бы очень признателен за любую помощь, руководство или ответы, которые вы могли бы предоставить для этой проблемы! Пожалуйста, попросите каких-либо разъяснений или дополнительной информации, так как это заняло у меня много времени, чтобы собраться вместе, и я, вероятно, не объясняю что-то хорошо или что-то упускаю. Спасибо!

1 Ответ

1 голос
/ 12 апреля 2020

Исходя из вышеизложенных комментариев Саша, я хотел бы опубликовать правильную реализацию этой проблемы в cvxpy:

import cvxpy as cv


class Optimiser:

    def __init__(self, initial_portfolio, turnover, max_wt, longshort=True):
        self.symbols = initial_portfolio.index.to_numpy()
        self.init_wt = initial_portfolio['weight'].to_numpy()
        self.opt_wt = cv.Variable(self.init_wt.shape)
        self.alpha = initial_portfolio['alpha'].to_numpy()
        self.longshort = longshort
        self.turnover = turnover
        self.max_wt = max_wt
        if self.longshort:
            self.min_wt = -self.max_wt
            self.net_exposure = 0
            self.gross_exposure = 1
        else:
            self.min_wt = 0
            self.net_exposure = 1
            self.gross_exposure = 1

    def optimise(self):
        constraints = self.get_constraints()
        optimisation = cv.Problem(cv.Maximize(cv.sum(self.opt_wt*self.alpha)), constraints)
        optimisation.solve()
        if optimisation.status == 'optimal':
            print('Optimal solution found')
        else:
            print('Optimal solution not found')
        return optimisation.solution.primal_vars

    def get_constraints(self):
        min_wt = self.opt_wt >= self.min_wt
        max_wt = self.opt_wt <= self.max_wt
        turnover = cv.sum(cv.abs(self.opt_wt-self.init_wt)) <= self.turnover*2
        net_exposure = cv.sum(self.opt_wt) == self.net_exposure
        gross_exposure = cv.sum(cv.abs(self.opt_wt)) <= self.gross_exposure
        return [min_wt, max_wt, turnover, net_exposure, gross_exposure]

Большое спасибо Саша за помощь и руководство.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...