robertmartin8 / PyPortfolioOpt

Financial portfolio optimisation in python, including classical efficient frontier, Black-Litterman, Hierarchical Risk Parity
https://pyportfolioopt.readthedocs.io/
MIT License
4.39k stars 940 forks source link

Add constraint to reduce option strategy cost #309

Closed linda390 closed 3 years ago

linda390 commented 3 years ago

What are you trying to do? I am interested in a married put option strategy for my portfolio. However, I would like to keep the option premium at a minimum for my portfolio. How can I add a constraint so that the existing efficient frontier portfolio selection is also taking into account the corresponding put option costs for each ticker?

For example- for my pre-selected strikes, I have downloaded the corresponding put option prices separately for each ticker in 'put_option_prices' pandra core series.

Would something like this work? ef.add_constraint(lambda w: w @ put_option_prices <= 0.5)

What data are you using? S&P 500 universe.

Example:

put_option_prices
Out[1]: 
AAPL    4.20
GE      0.31
MSFT    5.10
dtype: float64
robertmartin8 commented 3 years ago

@linda390 This depends on what exactly your objectives and constraints are.

"I would like to keep the option premium at a minimum for my portfolio"

For example, the above objective is satisfied by not buying any options.

If you have a constraint that no more than x% of the portfolio value is to be spent on options, then the snippet you've provided should work.

linda390 commented 3 years ago

Thanks. Given that I am interested in a married put option strategy, buying the corresponding options is a must.

My constraint is not that less than x% of the portfolio is to be spent on option. It is that whatever % of the portfolio is spent on options, it also factors in the option price to make that selection (i.e. not just based on the annual returns).

linda390 commented 3 years ago

I apologize for being a bother. Could you kindly assist me in what would be the best way to implement this?

Thank you so very much.

robertmartin8 commented 3 years ago

Thanks. Given that I am interested in a married put option strategy, buying the corresponding options is a must.

Indeed, I was just illustrating that minimising the option premium would result in that solution, unless you specified adequate constraints.

I'm happy to have a go at formulating your problem in PyPortfolioOpt, but you need to be very precise in specifying your objective and constraints (preferably with a simple example on a reduced universe of 3-4 stocks).

linda390 commented 3 years ago

Sure. Here are my my objectives and constraints:

ef = EfficientFrontier(averageAnnualReturn, covariance)

ef.add_sector_constraints(sector_mapper, sector_lower, sector_upper)
ef.add_constraint(lambda w: w @ mu_other >= 0.10) <[Feature Request # 159]:(https://github.com/robertmartin8/PyPortfolioOpt/issues/159)>

ef.efficient_risk(target_volatility=0.14);

ef.clean_weights()

I have downloaded the options prices per contract separately in 'put_option_prices' panda series. Example:

put_option_prices
Out[1]: 
AAPL    4.20
GE      0.31
MSFT    5.10
...

I would like to include an additional constraint so that when assigning weightage within the efficient frontier, I'm also taking into account the corresponding put option price for each ticker. The net weightage that is assigned should not only maximize the expected annual return, it should also attempt to minimize the cost that would be associated if I were to buy the corresponding number of put options for each ticker.

(For the sake of this example, you can assume that the weightage that is assigned is already either 100 or in multiples of 100, so there is no mismatch between the assigned allocations and the number of option contract underlying shares required.)

robertmartin8 commented 3 years ago

taking into account the corresponding put option price for each ticker.

This is the part that needs to be stated mathematically.

it should also attempt to minimize the cost that would be associated if I were to buy the corresponding number of put options

What is the "corresponding number of put options"? e.g if the AAPL portfolio weight is 0.50, how many options would you need to buy?

phschiele commented 3 years ago

@linda390 Interesting problem. If you don't mind, I have some questions about the general idea and assumptions of the optimization.

I don't have much experience with married put strategies, but it seems a married put is very similar to an ATM call (minus pricing, stock ownership, taxation, ... differences). Could it be modeled in that way?

The returns of such an option are (by definition) asymmetric. Do you think mean-variance is enough to accurately describe the return distribution? I think adding options to classical mean-variance optimization is quite tricky in general. Quite possibly, just using the expected returns of the underlying and later "adding" the puts will not result in an "ideal" portfolio. If you have any resources, it would be awesome if you could share them.

linda390 commented 3 years ago

@robertmartin8 -

x = total amount of funds to be allocated

x = A[Ticker1] + B[Ticker2] + C*[Ticker3] + ....

where A, B, C are the quantities assigned to Ticker1, Ticker2, Ticker3, respectively, and are greater than or equal to 0.

expected annual return of x = max(expected annual returns of x for given volatility) - min(ATicker1OptionPrice + BTicker2OptionPrice + C*Ticker3OptionPrice + ...)

Ticker1OptionPrice, Ticker2OptionPrice, Ticker3OptionPrice are picked up from 'put_option_prices' panda series, where each ticker is assigned a price per 100 contracts.

I apologize if this isn't clear enough in mathematical terms.

linda390 commented 3 years ago

it seems a married put is very similar to an ATM call (minus pricing, stock ownership, taxation, ... differences). Could it be modeled in that way?

Correct.

Do you think mean-variance is enough to accurately describe the return distribution?

I do.

I think adding options to classical mean-variance optimization is quite tricky in general.

I agree.

If you have any resources, it would be awesome if you could share them.

Unfortunately, don't have additional resources. This is a strategy I've come up with.

Thank you

robertmartin8 commented 3 years ago

@linda390

I apologise if I'm misunderstanding, but you should just be able to ef.add_constraint or ef.add_objective with a dot product between the weights and the option prices.

Inputs

> mu
AAPL    0.273617
AMZN    0.319276
MSFT    0.179663

> S
                AAPL       AMZN                  MSFT
AAPL    0.206056    0.046379    0.054299
AMZN    0.046379    0.202369    0.042293
MSFT    0.054299    0.042293    0.101783

I'm going to assume that you are providing the option prices per dollar allocation to a security. e.g below, for every $100 allocated to AAPL, you need to spend $41 on options:

> option_prices

AAPL    0.41
AMZN    0.26
MSFT    0.29
dtype: float64

Optimising option cost

We now define a function to calculate the total cost of options given a portfolio value (e.g $10000 allocation)

def option_cost(w, option_prices, portfolio_value, k=1):
    """  
    w - weights
    option_prices - pd.series of prices
    portfolio_value - float
    k - scaling parameter (float). Needed if calling this as an objective
    """
    return k* portfolio_value * (w @ option_prices)

First run an optimisation for a $10k portfolio and calculate option cost:

ef = EfficientFrontier(mu, S) 
ef.efficient_risk(0.35)
option_cost(ef.weights, option_prices, 10000)  # 3187

This allocation cost us $3187 in options. Let's say we only have a $3000 budget for options. We can add this as a constraint:

ef = EfficientFrontier(mu, S) 
ef.add_constraint(lambda w: option_cost(w, option_prices, 10000) <= 3000)
ef.efficient_risk(0.35)

option_cost(ef.weights, option_prices, 10000) # 3000

Alternatively, you can add this as an objective term (like you specified in your last message), though you will have to experiment with different scalings since option cost is measured in dollars while expected returns/risks are measured in percentages.

ef = EfficientFrontier(mu, S) 
ef.add_objective(option_cost, option_prices=option_prices, portfolio_value=10000, k=1/10000)
ef.efficient_risk(0.35)

option_cost(ef.weights, option_prices, 10000) # 2696

Hope this helps!

linda390 commented 3 years ago

@robertmartin8

This was very helpful, and it has gotten me very close!

(I'm actually providing the option prices per contract- for example if the strike for AAPL is 121, I'm providing a put option last price of 2.3, which would then be multiplied by 100 because each options contract must cover 100 shares. But I can work with your solution).

ef = EfficientFrontier(mu, S) 
ef.add_constraint(lambda w: option_cost(w, option_prices, 10000) <= 3000)
ef.efficient_risk(0.35)

option_cost(ef.weights, option_prices, 10000) # 3000

Apologies if I didn't understand this clearly- when using the constraint for the option_cost function, I can pass in the weights ('w') before I have set the target volatility I want?

robertmartin8 commented 3 years ago

@linda390

Yup, you add constraints before you call the optimisation method (e.g min_volatility, efficient_risk etc).

These are passed as anonymous (lambda) functions, so actually w is just a dummy variable representing the vector of weights (but it isn't actually the vector of weights).

In effect, you are telling the optimiser that "when you compute my weights, make sure they satisfy this function".

Two illustrations of this anonymous function behaviour:

ef = EfficientFrontier(mu, S)
# Can use a different variable name: it's just a placeholder
ef.add_constraint(lambda x: x[0] <= 0.1)  # 0th weight has to be less than 0.1
ef.min_volatility()

# Or you can define a function and pass the function
def my_constraint(weight_vector):
    return weight_vector[1] + weight_vector[2] <= 0.4

ef = EfficientFrontier(mu, S)
ef.add_constraint(my_constraint)
ef.efficient_risk(0.3)

Best, Robert

linda390 commented 3 years ago

@robertmartin8

Hmm. I'm not sure if this is working quite the way I want it to.

A couple of things might be causing issues:

1) It is impossible for me to have a put option for every single allocation in my portfolio (given constraints on a minimum of 100 underlying shares required for each options contract). Therefore, I can not know in advance how much of my portfolio will be spent on options and cannot specify an exact portfolio_value in option_cost. Put options can be considered as insurance for your portfolio to protect your downside risk, and I can only hope that I get at least 90% insurance coverage for my portfolio (with the residual 'uninsured' percentage being shares that did not meet minimum 100 shares criteria).

2) Because not all of my portfolio will be spent on options, I cannot specify the option_prices in option_cost for the ineligible tickers. This will cause a mismatch when applying the efficient frontier constraint because the size of the tickers in the efficient frontier and option_prices are not aligned. To get around this, I have put in an option_prices value of 0 for tickers which will not be spent on options, but not sure if that is the correct way to do it.

robertmartin8 commented 3 years ago

@linda390 It sounds like quite a complex problem, and I'm not sure I'm able to help much more beyond what I've suggested. Here are some misc comments:

def option_cost(w, option_prices, portfolio_value):
    return portfolio_value * (w[0] * option_prices[0] + w[9] * option_prices[1] * w[5] + option_prices[2])
linda390 commented 3 years ago

That's fair. I will keep chipping away. I may have separate related questions (on convex constraints, etc.) as I work through it that I may branch off into separate threads.

Thank you again for your excellent work on this module, and the time that you put in to answer my questions.