Как решить систему уравнений и ограничений для оптимизации портфеля? - PullRequest
1 голос
/ 17 января 2020

У меня есть DataFrame следующим образом:

Name  Volatility    Return
a      0.0243        0.212
b      0.0321        0.431
c      0.0323        0.443
d      0.0391        0.2123
e      0.0433        0.3123

Я хотел бы иметь Volatility из 0.035 и максимальный Return для этой волатильности.

То есть, я хотел бы, чтобы в новом Df Name и процент этого актива, который будет в моем portfolio, который дает максимум Return для Volatility равняется 0.035 .

Поэтому мне нужно решить систему уравнений с несколькими условиями, чтобы получить наилучшее решение (Highest Return) для фиксированного результата (Volatility == 0.035).

Условия:

  • Каждый актив имеет вес от 0 до 1.
  • Сумма весов равна 1.
  • сумма весов, умноженная на волатильность каждого актива, равна «Желаемой волатильности».
  • Сумма весов, умноженная на доходность каждого актива, равна «Итоговому доходу». Это должно быть максимизировано.

1 Ответ

2 голосов
/ 17 января 2020

Вот подход, использующий Z3Py , с открытым исходным кодом SAT / SMT решатель. В решателе SAT / SMT вы можете написать свой код просто как список условий, и программа найдет оптимальное решение (или просто решение, которое удовлетворяет всем условиям, когда Z3 используется в качестве решателя).

Первоначально Решатели SAT работали только с чисто логическими выражениями, но современные решатели SAT / SMT также допускают использование фиксированных битов и неограниченных целых чисел, дробей, вещественных чисел и даже функций в качестве центральной переменной.

Чтобы записать данные уравнения в Z3, они буквально преобразуются в выражения Z3. Код ниже комментирует каждый из шагов.

import pandas as pd
from z3 import *

DesiredVolatility = 0.035
df = pd.DataFrame(columns=['Name', 'Volatility', 'Return'],
                  data=[['a', 0.0243, 0.212],
                        ['b', 0.0321, 0.431],
                        ['c', 0.0323, 0.443],
                        ['d', 0.0391, 0.2123],
                        ['e', 0.0433, 0.3123]])

# create a Z3 instance to optimize something
s = Optimize()
# the weight of each asset, as a Z3 variable
W = [Real(row.Name) for row in df.itertuples()]
# the total volatility
TotVol = Real('TotVol')
# the total return, to be maximized
TotReturn = Real('TotReturn')

# weights between 0 and 1, and sum to 1
s.add(And([And(w >= 0, w <= 1) for w in W]))
s.add(Sum([w for w in W]) == 1)
# the total return is calculated as the weighted sum of the asset returns
s.add(TotReturn == Sum([w * row.Return for w, row in zip(W, df.itertuples())]))
# the volatility is calculated as the weighted sum of the asset volatility
s.add(TotVol == Sum([w * row.Volatility for w, row in zip(W, df.itertuples())]))
# the volatility should be equal to the desired volatility
s.add(TotVol == DesiredVolatility)
# we're maximizing the total return
h1 = s.maximize(TotReturn)
# we ask Z3 to do its magick
res = s.check()
# we check the result, hoping for 'sat': all conditions satisfied, a maximum is found
if res == sat:
    s.upper(h1)
    m = s.model()
    #for w in W:
    #    print(f'asset {w}): {m[w]} = {m[w].numerator_as_long() / m[w] .denominator_as_long() : .6f}')
    # output the total return
    print(f'Total Return: {m[TotReturn]} = {m[TotReturn].numerator_as_long() / m[TotReturn] .denominator_as_long() :.6f}')
    # get the proportions out of the Z3 model
    proportions = [m[w].numerator_as_long() / m[w] .denominator_as_long() for w in W]
    # create a dataframe with the result
    df_result = pd.DataFrame({'Name': df.Name, 'Proportion': proportions})
    print(df_result)
else:
    print("No satisfiable solution found")

Результат:

Total Return: 452011/1100000 = 0.410919
  Name  Proportion
0    a    0.000000
1    b    0.000000
2    c    0.754545
3    d    0.000000
4    e    0.245455

Вы можете легко добавить дополнительные ограничения, например, «ни один актив не может иметь более 30% от общего количества». ":

# change 
s.add(And([And(w >= 0, w <= 1) for w in W]))`
# to
s.add(And([And(w >= 0, w <= 0.3) for w in W]))`

Что приведет к:

Total Return: 558101/1480000 = 0.377095
  Name  Proportion
0    a    0.082432
1    b    0.300000
2    c    0.300000
3    d    0.017568
4    e    0.300000
...