Risk neutral options valuation (#906)

* Risk neutral hedge

* Changeable risk

* Added documentation

* Improved description

* Improved description

* Retry

Co-authored-by: Colin Delahunty <colindelahunty@Colins-MacBook-Air.local>
This commit is contained in:
Colin Delahunty
2021-11-04 15:26:22 -04:00
committed by GitHub
parent 905b3f555d
commit c75810aae8
11 changed files with 371 additions and 14 deletions

View File

@@ -10,14 +10,14 @@ def get_loss_at_strike(strike: float, chain: pd.DataFrame) -> float:
Parameters
----------
strike: Union[int,float]
strike : Union[int,float]
Value to calculate total loss at
chain: Dataframe:
chain : Dataframe:
Dataframe containing at least strike and openInterest
Returns
-------
loss: Union[float,int]
loss : Union[float,int]
Total loss
"""
@@ -38,7 +38,7 @@ def calculate_max_pain(chain: pd.DataFrame) -> int:
Parameters
----------
chain: DataFrame
chain : DataFrame
Dataframe to calculate value from
Returns
@@ -66,7 +66,7 @@ def convert(orig: str, to: str) -> float:
"""Convert a string to a specific type of number
Parameters
----------
orig: str
orig : str
String to convert
Returns
-------
@@ -82,6 +82,35 @@ def convert(orig: str, to: str) -> float:
raise ValueError("Invalid to format, please use '%' or ','.")
def rn_payoff(x: str, df: pd.DataFrame, put: bool, delta: int, rf: float) -> float:
"""The risk neutral payoff for a stock
Parameters
----------
x : str
Strike price
df : pd.DataFrame
Dataframe of stocks prices and probabilities
put : bool
Whether the asset is a put or a call
delta : int
Difference between today's date and expirations date in days
rf : float
The current risk-free rate
Returns
-------
number : float
Risk neutral value of option
"""
if put:
df["Gain"] = np.where(x > df["Price"], x - df["Price"], 0)
else:
df["Gain"] = np.where(x < df["Price"], df["Price"] - x, 0)
df["Vals"] = df["Chance"] * df["Gain"]
risk_free = (1 + rf) ** (delta / 365)
return sum(df["Vals"]) / risk_free
opt_chain_cols = {
"lastTradeDate": {"format": "date", "label": "Last Trade Date"},
"strike": {"format": "${x:.2f}", "label": "Strike"},

View File

@@ -10,6 +10,8 @@ from typing import List
import matplotlib.pyplot as plt
from colorama import Style
import pandas as pd
from tabulate import tabulate
from prompt_toolkit.completion import NestedCompleter
from gamestonk_terminal import feature_flags as gtff
@@ -75,6 +77,10 @@ class OptionsController:
"payoff",
"plot",
"parity",
"add",
"rmv",
"show",
"rnval",
]
CHOICES += CHOICES_COMMANDS
@@ -110,6 +116,7 @@ class OptionsController:
"cmd",
choices=self.CHOICES,
)
self.prices = pd.DataFrame(columns=["Price", "Chance"])
def print_help(self):
"""Print help."""
@@ -153,6 +160,13 @@ Current Expiry: {self.selected_date or None}
parity shows whether options are above or below expected price [Yfinance]
> payoff shows payoff diagram for a selection of options [Yfinance]
Options Pricing:
add add an expected price to the list
rmv remove an expected price from the list
show show the listed of expected prices
rnval risk neutral valuation for an option:
{Style.RESET_ALL if not colored else ''}"""
print(help_text)
@@ -1178,7 +1192,6 @@ Current Expiry: {self.selected_date or None}
prog="parity",
description="Shows whether options are over or under valued",
)
parser.add_argument(
"-p",
"--put",
@@ -1227,6 +1240,158 @@ Current Expiry: {self.selected_date or None}
)
print("")
@try_except
def call_add(self, other_args: List[str]):
"""Process add command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="add",
description="Adds a price to the list",
)
parser.add_argument(
"-p",
"--price",
type=float,
required="-h" not in other_args,
dest="price",
help="Projected price of the stock at the expiration date",
)
parser.add_argument(
"-c",
"--chance",
type=float,
required="-h" not in other_args,
dest="chance",
help="Chance that the stock is at a given projected price",
)
ns_parser = parse_known_args_and_warn(parser, other_args)
if not ns_parser:
return
if ns_parser.price in self.prices["Price"].to_list():
df = self.prices[(self.prices["Price"] != ns_parser.price)]
else:
df = self.prices
new = {"Price": ns_parser.price, "Chance": ns_parser.chance}
df = df.append(new, ignore_index=True)
self.prices = df.sort_values("Price")
@try_except
def call_rmv(self, other_args: List[str]):
"""Process rmv command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="rmv",
description="Removes a price from the list",
)
parser.add_argument(
"-p",
"--price",
type=float,
required="-h" not in other_args and "-a" not in other_args,
dest="price",
help="Price you want to remove from the list",
)
parser.add_argument(
"-a",
"--all",
action="store_true",
default=False,
dest="all",
help="Remove all prices from the list",
)
if other_args and "-" not in other_args[0]:
other_args.insert(0, "-p")
ns_parser = parse_known_args_and_warn(parser, other_args)
if not ns_parser:
return
if ns_parser.all:
self.prices = pd.DataFrame(columns=["Price", "Chance"])
return
self.prices = self.prices[(self.prices["Price"] != ns_parser.price)]
@try_except
def call_show(self, _):
"""Process show command"""
print(f"Estimated price(s) of {self.ticker} at {self.selected_date}")
if gtff.USE_TABULATE_DF:
print(
tabulate(
self.prices,
headers=self.prices.columns,
floatfmt=".2f",
showindex=False,
tablefmt="fancy_grid",
),
"\n",
)
else:
print(self.prices.to_string, "\n")
print("")
@try_except
def call_rnval(self, other_args: List[str]):
"""Process rnval command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="rnval",
description="The risk neutral value of the options",
)
parser.add_argument(
"-p",
"--put",
action="store_true",
default=False,
help="Show puts instead of calls",
)
parser.add_argument(
"-m",
"--min",
type=float,
default=None,
dest="mini",
help="Minimum strike price shown",
)
parser.add_argument(
"-M",
"--max",
type=float,
default=None,
dest="maxi",
help="Maximum strike price shown",
)
parser.add_argument(
"-r",
"--risk",
type=float,
default=None,
dest="risk",
help="The risk-free rate to use",
)
ns_parser = parse_known_args_and_warn(parser, other_args)
if not ns_parser:
return
if not self.ticker and not self.selected_date:
print("Ticker and expiration required. \n")
return
if sum(self.prices["Chance"]) != 1:
print("Total chances must equal one\n")
return
yfinance_view.risk_neutral_vals(
self.ticker,
self.selected_date,
ns_parser.put,
self.prices,
ns_parser.mini,
ns_parser.maxi,
ns_parser.risk,
)
def menu(ticker: str = ""):
"""Options Menu."""

View File

@@ -496,17 +496,17 @@ def show_parity(
Parameters
----------
ticker: str
ticker : str
Ticker to get expirations for
exp: str
exp : str
Expiration to use for options
put: bool
put : bool
Whether to use puts or calls
ask: bool
ask : bool
Whether to use ask or lastPrice
mini: float
mini : float
Minimum strike price to show
maxi: float
maxi : float
Maximum strike price to show
"""
@@ -567,8 +567,8 @@ def show_parity(
if maxi is None:
maxi = filtered.strike.quantile(0.75)
filtered = filtered.loc[filtered["strike"] > mini]
filtered = filtered.loc[filtered["strike"] < maxi]
filtered = filtered.loc[filtered["strike"] >= mini]
filtered = filtered.loc[filtered["strike"] <= maxi]
show = filtered[["strike", diff]].copy()
@@ -590,3 +590,75 @@ def show_parity(
print(show.to_string(index=False))
print("")
def risk_neutral_vals(
ticker: str,
exp: str,
put: bool,
df: pd.DataFrame,
mini: float,
maxi: float,
risk: float,
) -> None:
"""Prints current options prices and risk neutral values [Source: Yahoo Finance]
Parameters
----------
ticker : str
Ticker to get expirations for
exp : str
Expiration to use for options
put : bool
Whether to use puts or calls
df : pd.DataFrame
Estimates for stocks prices and probabilities
mini : float
Minimum strike price to show
maxi : float
Maximum strike price to show
risk : float
The risk-free rate for the asset
"""
if put:
chain = get_option_chain(ticker, exp).puts
else:
chain = get_option_chain(ticker, exp).calls
r_date = datetime.strptime(exp, "%Y-%m-%d").date()
delta = (r_date - date.today()).days
vals = []
if risk is None:
risk = get_rf()
for _, row in chain.iterrows():
vals.append(
[
row["strike"],
row["lastPrice"],
op_helpers.rn_payoff(row["strike"], df, put, delta, risk),
]
)
new_df = pd.DataFrame(vals, columns=["Strike", "Last Price", "Value"], dtype=float)
new_df["Difference"] = new_df["Last Price"] - new_df["Value"]
if mini is None:
mini = new_df.Strike.quantile(0.25)
if maxi is None:
maxi = new_df.Strike.quantile(0.75)
new_df = new_df[new_df["Strike"] >= mini]
new_df = new_df[new_df["Strike"] <= maxi]
if gtff.USE_TABULATE_DF:
print(
tabulate(
new_df,
headers=[x.title() for x in new_df.columns],
tablefmt="fancy_grid",
showindex=False,
floatfmt=".2f",
)
)
else:
print(new_df.to_string(index=False))
print("")

View File

@@ -0,0 +1,12 @@
Name,Type,Quantity,Date,Price,Fees,Premium,Side
cash,cash,1.0,2021/1/22,2000.0,,,deposit
cash,cash,1.0,2021/2/22,2000.0,,,deposit
cash,cash,1.0,2021/3/22,2000.0,,,deposit
cash,cash,1.0,2021/4/22,2000.0,,,deposit
cash,cash,1.0,2021/5/22,2000.0,,,deposit
cash,cash,1.0,2021/6/22,2000.0,,,deposit
cash,cash,1.0,2021/7/22,2000.0,,,deposit
cash,cash,1.0,2021/8/22,2000.0,,,deposit
cash,cash,1.0,2021/9/22,2000.0,,,deposit
cash,cash,1.0,2021/10/22,2000.0,,,deposit
cash,cash,1.0,2021/11/22,2000.0,,,deposit
1 Name Type Quantity Date Price Fees Premium Side
2 cash cash 1.0 2021/1/22 2000.0 deposit
3 cash cash 1.0 2021/2/22 2000.0 deposit
4 cash cash 1.0 2021/3/22 2000.0 deposit
5 cash cash 1.0 2021/4/22 2000.0 deposit
6 cash cash 1.0 2021/5/22 2000.0 deposit
7 cash cash 1.0 2021/6/22 2000.0 deposit
8 cash cash 1.0 2021/7/22 2000.0 deposit
9 cash cash 1.0 2021/8/22 2000.0 deposit
10 cash cash 1.0 2021/9/22 2000.0 deposit
11 cash cash 1.0 2021/10/22 2000.0 deposit
12 cash cash 1.0 2021/11/22 2000.0 deposit

View File

@@ -0,0 +1,5 @@
Name,Type,Quantity,Date,Price,Fees,Premium,Side
cash,cash,1.0,2021/3/24,2000.0,,,deposit
aapl,stock,10,2021/3/24,120.7,0,0,Buy
msft,stock,3,2021/3/26,232.8,0,0,Buy
aapl,stock,5,2021/7/2,139.8,0,0,Sell
1 Name Type Quantity Date Price Fees Premium Side
2 cash cash 1.0 2021/3/24 2000.0 deposit
3 aapl stock 10 2021/3/24 120.7 0 0 Buy
4 msft stock 3 2021/3/26 232.8 0 0 Buy
5 aapl stock 5 2021/7/2 139.8 0 0 Sell

View File

@@ -0,0 +1,4 @@
Name,Type,Quantity,Date,Price,Fees,Premium,Side
spy,stock,1.0,2019/10/21,200.0,,,Buy
cash,cash,1.0,2019/10/21,200.0,,,deposit
cash,cash,1.0,2021/6/21,200.0,,,withdrawal
1 Name Type Quantity Date Price Fees Premium Side
2 spy stock 1.0 2019/10/21 200.0 Buy
3 cash cash 1.0 2019/10/21 200.0 deposit
4 cash cash 1.0 2021/6/21 200.0 withdrawal

12
website/content/options/add/_index.md vendored Normal file
View File

@@ -0,0 +1,12 @@
```
usage: add [-p] [-c] [-h]
```
add a stock price and probability to the list
```
optional arguments:
-p PRICE, --PRICE the estimated finishing price of the asset
-c CHANCE, --CHANCE the chance of the asset ending at the given price
-h, --help show this help message (default: False)
```

12
website/content/options/rmv/_index.md vendored Normal file
View File

@@ -0,0 +1,12 @@
```
usage: rmv [-p] [-c] [-h]
```
remove a stock price from the list
```
optional arguments:
-p PRICE, --PRICE the estimated finishing price of the asset
-a, --all remove all items from the list of tickers
-h, --help show this help message (default: False)
```

28
website/content/options/rnval/_index.md vendored Normal file
View File

@@ -0,0 +1,28 @@
```
usage: rnval [-p] [-m MIN] [-M MAX] [-r RISK] [-h]
```
calculates the expcected value of a given option by multiplying
the payoff at each predicted stock price by the probability of
that stock price happening. This expected profit is divided by
one plus the risk free rate to determine the value of those
future dollars today. The expected payoff for each strike price
is then compared with the last traded price to determine if there
are differences in the market.
For example, if you assume the price of AAPL has an equal chance
of finishing at $100 or $200 for a given expiration, you could use
this to value an option at that expiration. To value a call with a
strike of $150, you would calculate the payoff for each stock price,
which would be $0 and $50. Then, we caclulate the weighted average
payoff of $25. We need to divide this amount by the risk-free rate,
assumed to be 0.02. So the value of this option expiring in one year
is 25/1.02, or $24.51.
```
optional arguments:
-p, --put flag to calculate put option (default: False)
-m MIN, --min MIN min price to look at (default: -1)
-M MAX, --max MAX max price to look at (default: -1)
-r RISK, --risk RISK use a custom risk-free amount (default: None)
-h, --help show this help message (default: False)
```

10
website/content/options/show/_index.md vendored Normal file
View File

@@ -0,0 +1,10 @@
```
usage: show [-h]
```
show the list of stocks prices and probabilities
```
optional arguments:
-h, --help show this help message (default: False)
```

View File

@@ -551,6 +551,14 @@ main:
ref: "/portfolio/portfolio_optimization"
- name: parity
ref: "/options/parity"
- name: add
ref: "/options/add"
- name: rmv
ref: "/options/rmv"
- name: show
ref: "/options/show"
- name: rnval
ref: "/options/rnval"
- name: payoff
ref: "/options/payoff"
sub: