Files
OpenBB/openbb_terminal/stocks/options/nasdaq_model.py
Colin Delahunty 141dd68e68 Removed prints (#5367)
* Removed prints

* FIxed typing

* FIxed typing

* Fixed typing

* Fixed typing

* Fixed typing

* Fixed typing

* Fixed typing

* Fix

* Reverted stuff
2023-08-23 18:40:31 +00:00

500 lines
15 KiB
Python

# mypy: disable-error-code=attr-defined
"""Nasdaq Model"""
__docformat__ = "numpy"
import logging
from datetime import datetime, timedelta
from typing import List
import numpy as np
import pandas as pd
import pandas_datareader
from openbb_terminal.decorators import log_start_end
from openbb_terminal.helper_funcs import request
from openbb_terminal.rich_config import console
from openbb_terminal.stocks.options.op_helpers import Options, PydanticOptions
logger = logging.getLogger(__name__)
# pylint: disable=unsupported-assignment-operation
call_cols = [
"c_Last",
"c_Bid",
"c_Ask",
"c_Volume",
"c_Openinterest",
"strike",
"expiration",
]
put_cols = [
"p_Last",
"p_Bid",
"p_Ask",
"p_Volume",
"p_Openinterest",
"strike",
"expiration",
]
cols = ["lastPrice", "bid", "ask", "volume", "openInterest", "strike", "expiration"]
sorted_chain_columns = [
"optionType",
"expiration",
"strike",
"lastPrice",
"bid",
"ask",
"openInterest",
"volume",
]
def get_dte_from_expiration(date: str) -> float:
"""
Converts a date to total days until the option would expire.
This assumes that the date is in the form %B %d, %Y such as January 11, 2023
This calculates time from 'now' to 4 PM the date of expiration
This is particularly a helper for nasdaq results.
Parameters
----------
date: str
Date in format %B %d, %Y
Returns
-------
float
Days to expiration as a decimal
"""
# Get the date as a datetime and add 16 hours (4PM)
expiration_time = datetime.strptime(date, "%B %d, %Y") + timedelta(hours=16)
# Find total seconds from now
time_to_now = (expiration_time - datetime.now()).total_seconds()
# Convert to days
time_to_now /= 60 * 60 * 24
return time_to_now
@log_start_end(log=logger)
def get_full_option_chain(symbol: str) -> pd.DataFrame:
"""Get the full option chain for symbol over all expirations
Parameters
----------
symbol: str
Symbol to get options for. Can be a stock, etf or index.
Returns
-------
pd.DataFrame
Dataframe of option chain
"""
# Nasdaq requires an asset code, so instead of making user supply one, just loop through all
for asset in ["stocks", "index", "etf"]:
url = (
f"https://api.nasdaq.com/api/quote/{symbol}/option-chain?assetclass={asset}&"
"fromdate=2010-09-09&todate=2030-09-09&excode=oprac&callput=callput&money=all&type=all"
)
# I have had issues with nasdaq requests, and this user agent seems to work in US and EU
response_json = request(
url,
headers={
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
" AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6 Safari/605.1.15"
},
).json()
if response_json["status"]["rCode"] == 200:
return process_response(response_json)
console.print(f"[red]{symbol} Option Chain not found.[/red]\n")
return pd.DataFrame()
def process_response(response_json):
df = pd.DataFrame(response_json["data"]["table"]["rows"]).drop(
columns=["c_colour", "p_colour", "drillDownURL"]
)
df["expirygroup"] = df["expirygroup"].replace("", np.nan).fillna(method="ffill")
df["expiration"] = pd.to_datetime(
df["expirygroup"], format="%B %d, %Y"
).dt.strftime("%Y-%m-%d")
# Make numeric
columns_w_types = {
"c_Last": float,
"c_Change": float,
"c_Bid": float,
"c_Ask": float,
"c_Volume": int,
"c_Openinterest": int,
"strike": float,
"p_Last": float,
"p_Change": float,
"p_Bid": float,
"p_Ask": float,
"p_Volume": int,
"p_Openinterest": int,
}
for key, _ in columns_w_types.items():
df[key] = df[key].replace(",", "", regex=True)
df = df.fillna(np.nan).dropna(axis=0).replace("--", 0).astype(columns_w_types)
df["DTE"] = df["expirygroup"].apply(lambda t: get_dte_from_expiration(t))
df = df[df.DTE > 0]
df = df.drop(columns=["DTE"])
# Process into consistent format
calls = df[call_cols].copy()
puts = df[put_cols].copy()
calls.columns = cols
puts.columns = cols
calls["optionType"] = "call"
puts["optionType"] = "put"
chain = (
pd.concat([calls, puts], axis=0)
.sort_values(by=["expiration", "strike"])
.reset_index(drop=True)
)
return chain[sorted_chain_columns]
@log_start_end(log=logger)
def option_expirations(symbol: str) -> List[str]:
"""Get available expirations
Parameters
----------
symbol : str
Ticker symbol to get expirations for
Returns
-------
List[str]
List of expiration dates
"""
df = get_full_option_chain(symbol)
if df.empty:
return []
# get everything that is not an empty string
return [exp for exp in list(df.expiration.unique()) if exp]
@log_start_end(log=logger)
def get_last_price(symbol: str) -> float:
"""Get the last price from nasdaq
Parameters
----------
symbol: str
Symbol to get quote for
Returns
-------
float
Last price
"""
for asset in ["stocks", "index", "etf"]:
url = f"https://api.nasdaq.com/api/quote/{symbol}/info?assetclass={asset}"
response_json = request(
url,
headers={
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
" AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6 Safari/605.1.15"
},
).json()
if response_json["status"]["rCode"] == 200:
return float(
response_json["data"]["primaryData"]["lastSalePrice"]
.strip("$")
.replace(",", "")
)
console.print(f"[red]Last price for {symbol} not found[/red]\n")
return np.nan
@log_start_end(log=logger)
def get_underlying_price(symbol: str) -> pd.Series:
"""Get the last price from nasdaq
Parameters
----------
symbol: str
Symbol to get quote for
Returns
-------
float
Last price
"""
df = pd.Series(dtype=object)
for asset in ["stocks", "index", "etf"]:
url = f"https://api.nasdaq.com/api/quote/{symbol}/info?assetclass={asset}"
response_json = request(
url,
headers={
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
" AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6 Safari/605.1.15"
},
).json()
if response_json["status"]["rCode"] == 200:
data = response_json["data"]
data = pd.Series(data)
df["companyName"] = data["companyName"]
df["price"] = float(
data["primaryData"]["lastSalePrice"].strip("$").replace(",", "")
)
df["change"] = float(data["primaryData"]["netChange"])
df["changePercent"] = float(
data["primaryData"]["percentageChange"].replace("%", "")
)
df["volume"] = data["primaryData"]["volume"].replace(",", "")
df["date"] = pd.to_datetime(
data["primaryData"]["lastTradeTimestamp"]
.replace("ET - PRE-MARKET", "")
.replace(" - AFTER HOURS", "")
.replace("ET", ""),
yearfirst=True,
).strftime("%Y-%m-%d")
return df.rename(symbol)
console.print(f"[red]Last price for {symbol} not found[/red]\n")
return pd.Series()
# Ugh this doesn't get the full chain
# TODO: apply CRR binomial tree to backtrack IV for greeks
@log_start_end(log=logger)
def get_option_greeks(symbol: str, expiration: str) -> pd.DataFrame:
"""Get option greeks from nasdaq
Parameters
----------
symbol: str
Symbol to get
expiration: str
Option expiration
Returns
-------
pd.DataFrame
Dataframe with option greeks
"""
for asset in ["stocks", "index", "etf"]:
url_greeks = f"https://api.nasdaq.com/api/quote/{symbol}/option-chain/greeks?assetclass={asset}&date={expiration}"
response_json = request(
url_greeks,
headers={
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
" AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6 Safari/605.1.15"
},
).json()
if response_json["status"]["rCode"] == 200:
greeks = pd.DataFrame(response_json["data"]["table"]["rows"])
greeks = greeks.drop(columns="url")
return greeks
console.print(f"[red]Greeks not found for {symbol} on {expiration}[/red].")
return pd.DataFrame()
def get_available_greeks(OptionsChains, expiration: str = "") -> pd.DataFrame:
"""Get available greeks for a specific expiration.
This function will return data for strike prices near the money only.
Parameters
----------
expiration: str
The expiration date to return the data. Default is the first available date. (YYYY-MM-DD)
Returns
-------
pd.DataFrame
Dataframe with option greeks and strike prices.
Examples
--------
Near-the-Money Greeks for the closest expiration date.
>>> greeks = self.get_available_greeks()
Get the last expiration date.
>>> greeks = self.get_available_greeks(self.expirations[-1])
"""
if expiration == "":
expiration = OptionsChains.expirations[0]
if expiration not in OptionsChains.expirations:
console.print(
f"{expiration}",
" is not a valid expiration. Choose from, ",
OptionsChains.expirations,
sep="",
)
return pd.DataFrame()
greeks = get_option_greeks(OptionsChains.symbol, expiration)
return greeks
@log_start_end(log=logger)
def load_options(symbol: str, pydantic: bool = False) -> Options:
"""OptionsChains data object for Nasdaq.
Parameters
----------
symbol: str
The ticker symbol to load.
pydantic: bool
Whether to return the object as a Pydantic Model or a subscriptable Pandas Object. Default is False.
Returns
-------
object: Options
chains: dict
The complete options chain for the ticker. Returns as a Pandas DataFrame if pydantic is False.
expirations: list[str]
List of unique expiration dates. (YYYY-MM-DD)
strikes: list[float]
List of unique strike prices.
last_price: float
The last price of the underlying asset.
underlying_name: str
The name of the underlying asset.
underlying_price: dict
The price and recent performance of the underlying asset. Returns as a Pandas Series if pydantic is False.
hasIV: bool
Returns implied volatility.
hasGreeks: bool
Returns greeks data.
symbol: str
The symbol entered by the user.
source: str
The source of the data, "Nasdaq".
SYMBOLS: dict
The Nasdaq symbol directory. Returns as a Pandas DataFrame if pydantic is False.
Examples
--------
Get current options chains for AAPL.
>>> from openbb_terminal.stocks.options.nasdaq_model import load_options
>>> data = load_options("AAPL")
>>> chains = data.chains
Return the object as a Pydantic Model.
>>> from openbb_terminal.stocks.options.nasdaq_model import load_options
>>> data = load_options("AAPL", pydantic=True)
"""
symbol = symbol.upper()
OptionsChains = Options()
OptionsChains.source = "Nasdaq"
OptionsChains.SYMBOLS = pandas_datareader.nasdaq_trader.get_nasdaq_symbols()
OptionsChains.symbol = symbol
if (
OptionsChains.symbol not in OptionsChains.SYMBOLS.index
and OptionsChains.symbol != "NDX"
):
console.print(OptionsChains.symbol, "was not found in the Nasdaq directory")
return OptionsChains
OptionsChains.symbol = symbol
try:
OptionsChains.chains = get_full_option_chain(OptionsChains.symbol)
now = datetime.now()
temp = pd.DatetimeIndex(OptionsChains.chains.expiration)
temp_ = (temp - now).days + 1
OptionsChains.chains["dte"] = temp_
except Exception:
return OptionsChains
if not OptionsChains.chains.empty:
OptionsChains.expirations = option_expirations(OptionsChains.symbol)
OptionsChains.strikes = (
pd.Series(OptionsChains.chains["strike"]).sort_values().unique().tolist()
)
OptionsChains.underlying_price = get_underlying_price(OptionsChains.symbol)
OptionsChains.last_price = OptionsChains.underlying_price["price"]
OptionsChains.underlying_name = OptionsChains.underlying_price["companyName"]
OptionsChains.hasIV = "impliedVolatility" in OptionsChains.chains.columns
OptionsChains.hasGreeks = "gamma" in OptionsChains.chains.columns
def get_greeks(expiration: str = ""):
"""Get available greeks for a specific expiration.
This function will return data for strike prices near the money only.
Parameters
----------
expiration: str
The expiration date to return the data. Default is the first available date. (YYYY-MM-DD)
Returns
-------
pd.DataFrame
Dataframe with option greeks and strike prices.
Examples
--------
Near-the-Money Greeks for the closest expiration date.
>>> greeks = self.get_available_greeks()
Get the last expiration date.
>>> greeks = self.get_available_greeks(self.expirations[-1])
"""
if expiration == "":
expiration = OptionsChains.expirations[0]
if expiration not in OptionsChains.expirations:
console.print(
f"{expiration}",
" is not a valid expiration. Choose from, ",
OptionsChains.expirations,
sep="",
)
return pd.DataFrame()
return get_option_greeks(OptionsChains.symbol, expiration)
if not OptionsChains.chains.empty:
if OptionsChains.last_price is None:
OptionsChains.last_price = 0
console.print("No last price for " + OptionsChains.symbol)
if not pydantic:
setattr(OptionsChains, "get_available_greeks", get_greeks)
return OptionsChains
OptionsChainsPydantic = PydanticOptions(
chains=OptionsChains.chains.to_dict(),
expirations=OptionsChains.expirations,
strikes=OptionsChains.strikes,
last_price=OptionsChains.last_price,
underlying_name=OptionsChains.underlying_name,
underlying_price=OptionsChains.underlying_price.to_dict(),
hasIV=OptionsChains.hasIV,
hasGreeks=OptionsChains.hasGreeks,
symbol=OptionsChains.symbol,
source=OptionsChains.source,
SYMBOLS=OptionsChains.SYMBOLS.to_dict(),
)
setattr(OptionsChainsPydantic, "get_available_greeks", get_greeks)
return OptionsChainsPydantic
return Options()