summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanglewood <85772166+deeleeramone@users.noreply.github.com>2024-06-29 19:19:41 -0700
committerDanglewood <85772166+deeleeramone@users.noreply.github.com>2024-06-29 19:19:41 -0700
commit35d4997dca01f596ef8f88587d52d2683122b2d8 (patch)
tree54eb1f4d57a6593f1c9871598facfedff947f708
parentea8030b0b85affd9fc1cc8278b9563ab59cd36f6 (diff)
cboe vix with multiple dates allowed
-rw-r--r--openbb_platform/extensions/derivatives/integration/test_derivatives_api.py6
-rw-r--r--openbb_platform/extensions/derivatives/integration/test_derivatives_python.py4
-rw-r--r--openbb_platform/providers/cboe/openbb_cboe/models/futures_curve.py65
-rw-r--r--openbb_platform/providers/cboe/openbb_cboe/utils/vix.py327
4 files changed, 380 insertions, 22 deletions
diff --git a/openbb_platform/extensions/derivatives/integration/test_derivatives_api.py b/openbb_platform/extensions/derivatives/integration/test_derivatives_api.py
index 37042528326..8e52112ead0 100644
--- a/openbb_platform/extensions/derivatives/integration/test_derivatives_api.py
+++ b/openbb_platform/extensions/derivatives/integration/test_derivatives_api.py
@@ -131,13 +131,15 @@ def test_derivatives_futures_historical(params, headers):
(
{
"provider": "yfinance",
- "symbol": "VX",
+ "symbol": "ES",
+ "date": None,
}
),
(
{
"provider": "cboe",
- "symbol": "VX",
+ "symbol": "VX_EOD",
+ "date": None,
}
),
],
diff --git a/openbb_platform/extensions/derivatives/integration/test_derivatives_python.py b/openbb_platform/extensions/derivatives/integration/test_derivatives_python.py
index a3af4584c4b..0ba718e01fc 100644
--- a/openbb_platform/extensions/derivatives/integration/test_derivatives_python.py
+++ b/openbb_platform/extensions/derivatives/integration/test_derivatives_python.py
@@ -115,8 +115,8 @@ def test_derivatives_futures_historical(params, obb):
@parametrize(
"params",
[
- ({"provider": "yfinance", "symbol": "VX"}),
- ({"provider": "cboe", "symbol": "VX"}),
+ ({"provider": "yfinance", "symbol": "ES", "date": None}),
+ ({"provider": "cboe", "symbol": "VX", "date": None}),
],
)
@pytest.mark.integration
diff --git a/openbb_platform/providers/cboe/openbb_cboe/models/futures_curve.py b/openbb_platform/providers/cboe/openbb_cboe/models/futures_curve.py
index ce1a267f4df..3f5f703e438 100644
--- a/openbb_platform/providers/cboe/openbb_cboe/models/futures_curve.py
+++ b/openbb_platform/providers/cboe/openbb_cboe/models/futures_curve.py
@@ -1,17 +1,22 @@
"""CBOE Futures Curve Model."""
-# IMPORT STANDARD
-from typing import Any, Dict, List, Optional
+# pylint: disable=unused-argument
+
+from typing import Any, Dict, List, Literal, Optional
-from openbb_cboe.utils.helpers import get_settlement_prices
-from openbb_core.app.model.abstract.error import OpenBBError
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.futures_curve import (
FuturesCurveData,
FuturesCurveQueryParams,
)
+from openbb_core.provider.utils.descriptions import (
+ DATA_DESCRIPTIONS,
+ QUERY_DESCRIPTIONS,
+)
from openbb_core.provider.utils.errors import EmptyDataError
-from pydantic import Field
+from pydantic import Field, field_validator
+
+SymbolChoices = Literal["VX_AM", "VX_EOD"]
class CboeFuturesCurveQueryParams(FuturesCurveQueryParams):
@@ -20,11 +25,33 @@ class CboeFuturesCurveQueryParams(FuturesCurveQueryParams):
Source: https://www.cboe.com/
"""
+ __json_schema_extra__ = {"date": {"multiple_items_allowed": True}}
+
+ symbol: SymbolChoices = Field(
+ default="VX_EOD",
+ description=QUERY_DESCRIPTIONS.get("symbol", "")
+ + "Default is 'VX_EOD'. Entered dates return the data nearest to the entered date."
+ + "\n 'VX_AM' = Mid-Morning TWAP Levels"
+ + "\n 'VX_EOD' = 4PM Eastern Time Levels",
+ json_schema_extra={"choices": ["VX_AM", "VX_EOD"]},
+ )
+
+ @field_validator("symbol", mode="before", check_fields=False)
+ @classmethod
+ def validate_symbol(cls, v):
+ """Validate the symbol."""
+ if not v or v.lower() in ["vx", "vix", "^vix", "vix_index"]:
+ return "VX_EOD"
+ return v.upper()
+
class CboeFuturesCurveData(FuturesCurveData):
"""CBOE Futures Curve Data."""
- symbol: str = Field(description="The trading symbol for the tenor of future.")
+ symbol: Optional[str] = Field(
+ default=None,
+ description=DATA_DESCRIPTIONS.get("symbol", "")
+ )
class CboeFuturesCurveFetcher(
@@ -47,19 +74,21 @@ class CboeFuturesCurveFetcher(
**kwargs: Any,
) -> List[Dict]:
"""Return the raw data from the CBOE endpoint."""
- symbol = query.symbol.upper().split(",")[0]
- FUTURES = await get_settlement_prices(**kwargs)
- if len(FUTURES) == 0:
- raise EmptyDataError()
-
- if symbol not in FUTURES["product"].unique().tolist():
- raise OpenBBError(
- "The symbol, "
- f"{symbol}"
- ", is not valid. Chose from: "
- f"{FUTURES['product'].unique().tolist()}"
+ # pylint: disable=import-outside-toplevel
+ from openbb_cboe.utils.vix import get_vx_by_date, get_vx_current
+
+ symbol = "am" if query.symbol == "VX_AM" else "eod"
+ if query.date is not None:
+ data = await get_vx_by_date(
+ date=query.date, # type: ignore
+ vx_type=symbol,
+ use_cache=False,
)
- data = FUTURES[FUTURES["product"] == symbol][["expiration", "symbol", "price"]]
+ else:
+ data = await get_vx_current(vx_type=symbol, use_cache=False)
+
+ if data.empty:
+ raise EmptyDataError("The response was returned empty.")
return data.to_dict("records")
diff --git a/openbb_platform/providers/cboe/openbb_cboe/utils/vix.py b/openbb_platform/providers/cboe/openbb_cboe/utils/vix.py
new file mode 100644
index 00000000000..bbd3eb33432
--- /dev/null
+++ b/openbb_platform/providers/cboe/openbb_cboe/utils/vix.py
@@ -0,0 +1,327 @@
+"""VIX Utilities."""
+
+from typing import TYPE_CHECKING, Dict, List, Literal, Optional, Union
+
+if TYPE_CHECKING:
+ from pandas import DataFrame # pylint: disable=import-outside-toplevel
+
+
+VX_AM_SYMBOLS = [
+ "TWLV1",
+ "TWLV2",
+ "TWLV3",
+ "TWLV4",
+ "TWLV5",
+ "TWLV6",
+ "TWLV7",
+ "TWLV8",
+ "TWLV9",
+]
+
+VX_EOD_SYMBOL_TO_MONTH = {
+ "UZF": 1,
+ "UZG": 2,
+ "UZH": 3,
+ "UZJ": 4,
+ "UZK": 5,
+ "UZM": 6,
+ "UZN": 7,
+ "UZQ": 8,
+ "UZU": 9,
+ "UZV": 10,
+ "UZX": 11,
+ "UZZ": 12,
+}
+
+
+def get_front_month(date: Optional[str] = None):
+ """Get the front month based on the third Wednesday of the month."""
+ # pylint: disable=import-outside-toplevel
+ from datetime import datetime # noqa
+ from calendar import monthcalendar
+
+ today = datetime.now() if date is None else datetime.strptime(date, "%Y-%m-%d")
+ third_wednesday = [
+ week[2] for week in monthcalendar(today.year, today.month) if week[2] != 0
+ ][2]
+ if today.day > third_wednesday:
+ # If today is after the third Wednesday of the month, return the next month
+ return (today.month % 12) + 1
+ else:
+ # Otherwise, return the current month
+ return today.month
+
+
+def get_vx_symbols(date: Optional[str] = None) -> Dict:
+ """Get the VIX symbols based on relative position to the front month."""
+ # pylint: disable=import-outside-toplevel
+ from collections import deque
+
+ VIX_SYMBOLS = deque(
+ [
+ "UZF", # Jan
+ "UZG", # Feb
+ "UZH", # Mar
+ "UZJ", # Apr
+ "UZK", # May
+ "UZM", # Jun
+ "UZN", # Jul
+ "UZQ", # Aug
+ "UZU", # Sep
+ "UZV", # Oct
+ "UZX", # Nov
+ "UZZ", # Dec
+ ]
+ )
+ VIX_SYMBOLS.rotate(-(get_front_month(date) - 1))
+
+ return {f"VX{i+1}": symbol for i, symbol in enumerate(VIX_SYMBOLS)}
+
+
+def get_months(front_month):
+ """Translate the front month into forward expiration dates."""
+ # pylint: disable=import-outside-toplevel
+ from collections import deque
+
+ front_month = front_month % 12
+ MONTHS = deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
+ MONTHS.rotate(-front_month + 1)
+
+ return {f"VX{i+1}": month for i, month in enumerate(MONTHS)}
+
+
+def check_date(date):
+ """Check the date for weekdays."""
+ # pylint: disable=import-outside-toplevel
+ from datetime import timedelta
+
+ return (
+ date
+ if date.date().weekday() < 5
+ else date - timedelta(days=6 - date.date().weekday())
+ )
+
+
+async def get_vx_current(
+ vx_type: Literal["am", "eod"] = "eod", use_cache: bool = True
+) -> "DataFrame":
+ """Get the current quotes for VX Futures.
+
+ Parameters
+ ----------
+ vx_type : Literal["am", "eod"]
+ The type of VX futures to get. Default is "eod".
+ am: Mid-morning TWAP value
+ eod: End-of-day value
+ use_cache : bool
+ Whether to use the cache. Default is True. Cache is only used for symbol mapping.
+
+ Returns
+ -------
+ DataFrame
+ DataFrame with the current VX futures data.
+ """
+ # pylint: disable=import-outside-toplevel
+ from datetime import datetime # noqa
+ from openbb_core.app.model.abstract.error import OpenBBError
+ from openbb_cboe.models.equity_quote import CboeEquityQuoteFetcher
+ from pandas import DataFrame
+
+ if vx_type not in ["am", "eod"]:
+ raise OpenBBError("vx_type must be one of: 'am', 'eod'")
+
+ current_symbols = list(get_vx_symbols().values())[:9]
+ symbols = VX_AM_SYMBOLS if vx_type == "am" else current_symbols
+ current_months = [VX_EOD_SYMBOL_TO_MONTH.get(d) for d in current_symbols]
+ current_year = datetime.today().year
+ data = await CboeEquityQuoteFetcher.fetch_data(
+ {"symbol": ",".join(symbols), "use_cache": use_cache}, {}
+ )
+ df = DataFrame([d.model_dump() for d in data]) # type: ignore
+
+ if vx_type == "am":
+ df = df[["symbol", "last_price"]]
+ elif vx_type == "eod":
+ df = df.sort_values(by="last_timestamp", ascending=False)[
+ ["symbol", "last_price"]
+ ]
+ df = df.set_index("symbol")
+ df = df.filter(items=current_symbols, axis=0).reset_index()
+
+ expirations: List = []
+ for month in current_months:
+ new_year = month == 1
+ current_year = (
+ current_year + 1
+ if new_year and datetime.today().month != 1
+ else current_year
+ )
+ new_month = "0" + str(month) if month < 10 else str(month) # type: ignore
+ expirations.append(f"{current_year}-{new_month}")
+
+ df.symbol = expirations
+ df = df.rename(columns={"symbol": "expiration", "last_price": "price"})
+
+ return df
+
+
+async def get_vx_by_date(
+ date: Union[str, List[str]],
+ vx_type: Literal["am", "eod"] = "eod",
+ use_cache: bool = True,
+) -> "DataFrame":
+ """Get VX futures by date(s).
+
+ Parameters
+ ----------
+ date : str or List[str]
+ The date(s) to get VX futures for.
+ vx_type : Literal["am", "eod"]
+ The type of VX futures to get. Default is "eod".
+ am: Mid-morning TWAP value
+ eod: End-of-day value
+ use_cache : bool
+ Whether to use the cache. Default is True. Cache is only used for symbol mapping.
+
+ Returns
+ -------
+ DataFrame
+ Categorical DataFrame with VX futures data for the given date(s).
+ """
+ # pylint: disable=import-outside-toplevel
+ from datetime import datetime, timedelta # noqa
+ from openbb_core.app.model.abstract.error import OpenBBError
+ from openbb_core.provider.utils.errors import EmptyDataError
+ from openbb_cboe.models.equity_historical import CboeEquityHistoricalFetcher
+ from numpy import abs
+ from pandas import Categorical, DataFrame, DatetimeIndex, concat, isna, to_datetime
+
+ if vx_type not in ["am", "eod"]:
+ raise OpenBBError("'vx_type' must be one of: 'am', 'eod'")
+
+ df = DataFrame()
+ start_date = ""
+ end_date = ""
+ symbols = list(get_vx_symbols().values()) if vx_type == "eod" else VX_AM_SYMBOLS
+ dates = date.split(",") if isinstance(date, str) else date
+ dates = sorted([check_date(to_datetime(d)) for d in dates])
+ today = check_date(datetime.today()).strftime("%Y-%m-%d")
+
+ if len(dates) == 1:
+ new_date = check_date(to_datetime(dates[0]))
+ if new_date.strftime("%Y-%m-%d") == today:
+ df = await get_vx_current(vx_type=vx_type)
+ df["date"] = new_date.strftime("%Y-%m-%d")
+ return df
+
+ end_date = new_date.strftime("%Y-%m-%d")
+ start_date = (check_date(new_date - timedelta(days=1))).strftime("%Y-%m-%d")
+ else:
+ start_date = check_date(dates[0]).strftime("%Y-%m-%d")
+ end_date = check_date(dates[-1]).strftime("%Y-%m-%d")
+
+ # The data from the current date is not available in the historical data,
+ # so we need to get it separately, if required.
+ current_data = DataFrame()
+
+ if end_date == today:
+ current_data = await get_vx_current(vx_type=vx_type)
+ current_data["date"] = end_date
+ current_data["symbol"] = [
+ "VX1",
+ "VX2",
+ "VX3",
+ "VX4",
+ "VX5",
+ "VX6",
+ "VX7",
+ "VX8",
+ "VX9",
+ ]
+
+ data = await CboeEquityHistoricalFetcher.fetch_data(
+ {
+ "symbol": ",".join(symbols),
+ "start_date": start_date,
+ "end_date": end_date,
+ "use_cache": use_cache,
+ }
+ )
+ df = DataFrame([d.model_dump() for d in data]) # type: ignore
+ df = df.set_index("date").sort_index()
+
+ df.index = df.index.astype(str)
+ df.index = DatetimeIndex(df.index)
+ dates_list = DatetimeIndex(dates)
+ symbols = df.symbol.unique().tolist()
+ df = (
+ df.reset_index()
+ .pivot(columns="symbol", values="close", index="date") # type: ignore
+ .copy()
+ )
+ if vx_type == "am":
+ df = df.dropna(how="any")
+
+ nearest_dates = []
+ for date in dates_list:
+ nearest_date = df.index.asof(date)
+ if isna(nearest_date): # type: ignore
+ differences = abs(df.index - date)
+ min_diff_index = differences.argmin()
+ nearest_date = df.index[min_diff_index]
+ nearest_dates.append(nearest_date)
+ nearest_dates = DatetimeIndex(nearest_dates)
+
+ # Filter for only the nearest dates
+ df = df[df.index.isin(nearest_dates)]
+ df = df.fillna("N/A").replace("N/A", None)
+ output = DataFrame()
+ df.index = df.index.astype(str)
+ # For each date, we need to arrange VX1 - VX9 according to the relative front month.
+ for _date in df.index.tolist():
+ temp = df.filter(like=_date, axis=0).copy()
+ current_symbols = list(get_vx_symbols(date=_date).values())[:9]
+ current_symbols = VX_AM_SYMBOLS if vx_type == "am" else current_symbols
+ temp = temp.filter(items=current_symbols, axis=1)
+ current_month = get_front_month(_date)
+ current_months = get_months(current_month)
+ current_year = int(_date.split("-")[0])
+ expirations: List = []
+ for month in list(current_months.values())[:9]:
+ new_year = month == 1
+ current_year = (
+ current_year + 1 if new_year and current_month != 1 else current_year
+ )
+ new_month = "0" + str(month) if month < 10 else str(month) # type: ignore
+ expirations.append(f"{current_year}-{new_month}")
+ flattened = temp.reset_index().melt(
+ id_vars="date", var_name="expiration", value_name="price"
+ )
+ if vx_type == "eod":
+ vx_symbols = {v: k for k, v in get_vx_symbols(date=_date).items()}
+ elif vx_type == "am":
+ vx_symbols = {item: item.replace("TWLV", "VX") for item in VX_AM_SYMBOLS}
+ flattened["symbol"] = flattened.expiration.map(vx_symbols)
+ flattened.expiration = expirations
+ flattened = flattened.dropna(how="any", subset=["price"])
+
+ output = concat([output, flattened])
+
+ if not current_data.empty and current_data.date[0] not in output.date.unique():
+ output = concat([output, current_data])
+
+ if output.empty:
+ raise EmptyDataError()
+
+ output = output.sort_values("date")
+ dates = DatetimeIndex(dates)
+ if dates[-1] != nearest_dates[-1] and not current_data.empty:
+ output = output[output.date != nearest_dates[-1].strftime("%Y-%m-%d")] # type: ignore
+ output["symbol"] = Categorical(
+ output["symbol"],
+ categories=sorted(output.symbol.unique().tolist()),
+ ordered=True,
+ )
+ output = output.sort_values(by=["date", "symbol"]).reset_index(drop=True)
+
+ return output