| """TMX Helpers Module.""" |
|
|
| |
|
|
| from datetime import ( |
| date as dateType, |
| datetime, |
| time, |
| timedelta, |
| ) |
| from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Union |
|
|
| from openbb_core.app.model.abstract.error import OpenBBError |
| from openbb_tmx.utils import gql |
|
|
| if TYPE_CHECKING: |
| from aiohttp_client_cache import SQLiteBackend |
| from pandas import DataFrame |
|
|
| |
| COLUMNS_DICT = { |
| "symbol": "symbol", |
| "shortname": "short_name", |
| "longname": "name", |
| "fundfamily": "fund_family", |
| "regions": "regions", |
| "sectors": "sectors", |
| "currency": "currency", |
| "inceptiondate": "inception_date", |
| "unitprice": "unit_price", |
| "prevClose": "prev_close", |
| "close": "close", |
| "esg": "esg", |
| "investmentstyle": "investment_style", |
| "avgdailyvolume": "volume_avg_daily", |
| "totalreturn1month": "return_1m", |
| "totalreturn3month": "return_3m", |
| "totalreturn1year": "return_1y", |
| "totalreturn3year": "return_3y", |
| "totalreturn5year": "return_5y", |
| "totalreturnytd": "return_ytd", |
| "totalreturnsinceinception": "return_from_inception", |
| "distributionyeld": "distribution_yield", |
| "dividendfrequency": "dividend_frequency", |
| "pricetoearnings": "pe_ratio", |
| "pricetobook": "pb_ratio", |
| "assetclass": "asset_class_id", |
| "prospectobjective": "investment_objectives", |
| "beta1y": "beta_1y", |
| "beta2y": "beta_2y", |
| "beta3y": "beta_3y", |
| "beta4y": "beta_4y", |
| "beta5y": "beta_5y", |
| "beta6y": "beta_6y", |
| "beta7y": "beta_7y", |
| "beta8y": "beta_8y", |
| "beta9y": "beta_9y", |
| "beta10y": "beta_10y", |
| "beta11y": "beta_11y", |
| "beta12y": "beta_12y", |
| "beta13y": "beta_13y", |
| "beta14y": "beta_14y", |
| "beta15y": "beta_15y", |
| "beta16y": "beta_16y", |
| "beta17y": "beta_17y", |
| "beta18y": "beta_18y", |
| "beta19y": "beta_19y", |
| "beta20y": "beta_20y", |
| "avgvol30days": "volume_avg_30d", |
| "aum": "aum", |
| "top10holdings": "holdings_top10", |
| "top10holdingsummary": "holdings_top10_summary", |
| "totalreturn6month": "return_6m", |
| "totalreturn10year": "return_10y", |
| "managementfee": "management_fee", |
| "altData": "additional_data", |
| } |
|
|
| |
|
|
| NASDAQ_GIDS = { |
| "^ADRAI": "BLDRS Asia 50 ADR Index Fund", |
| "^ADRDI": "BLDRS Developed Markets 100 ADR Index Fund", |
| "^ADREI": "BLDRS Emerging Markets 50 ADR Index Fund", |
| "^ASRN": "AlphaSector Rotation Index", |
| "^ASRX": "AlphaSector Rotation Total Return Index", |
| "^AVSPY": "NASDAQ OMX Alpha AAPL vs. SPY Index", |
| "^BIXR": "BetterInvesting 100 Total Return Index", |
| "^BIXX": "BetterInvesting 100 Index", |
| "^BKX": "KBW Bank Index", |
| "^BSCBK": "NASDAQ BulletShares USD Corporate Bond 2020 Index", |
| "^BSCBL": "NASDAQ BulletShares USD Corporate Bond 2021 Index", |
| "^BSCBM": "NASDAQ BulletShares USD Corporate Bond 2022 Index", |
| "^BSCBN": "NASDAQ BulletShares USD Corporate Bond 2023 Index", |
| "^BSCBO": "NASDAQ BulletShares USD Corporate Bond 2024 Index", |
| "^BSCBP": "NASDAQ BulletShares USD Corporate Bond 2025 Index", |
| "^BSJKK": "NASDAQ BulletShares USD High Yield Corporate Bond", |
| "^BSJKL": "NASDAQ BulletShares USD High Yield Corporate Bond", |
| "^BSJKM": "NASDAQ BulletShares USD High Yield Corporate Bond", |
| "^BSJKN": "NASDAQ BulletShares USD High Yield Corporate Bond", |
| "^BXN": "CBOE NASDAQ-100 BuyWrite Index", |
| "^CELS": "NASDAQ Clean Edge Green Energy Index", |
| "^CEXX": "NASDAQ Clean Edge Green Energy Total Return Index", |
| "^CHXN": "NASDAQ China Index", |
| "^CIX100": "Cryptoindex.com", |
| "^CND": "NASDAQ Canada", |
| "^COMPX": "NASDAQ Composite", |
| "^CVXLF": "NASDAQ OMX Alpha C vs. XLF Index", |
| "^DFX": "PHLX Defense Sector", |
| "^DIVQ": "NASDAQ Dividend Achievers Index", |
| "^DOT": "TheStreet.com Internet Sector", |
| "^DTEC": "NASDAQ Dallas Regional Chamber Index", |
| "^DVQT": "NASDAQ Dividend Achievers Total Return Index", |
| "^DWAFIR": "Dorsey Wright Fixed Income Allocation Index", |
| "^DWANQFF": "Dorsey Wright Focus Five Index", |
| "^EMCLOUD": "BVP Nasdaq Emerging Cloud Index", |
| "^EPX": "SIG Oil Exploration & Production Index", |
| "^ABAQ": "ABA Community Bank NASDAQ Index", |
| "^EVSPY": "NASDAQ OMX Alpha EEM vs. SPY Index", |
| "^GESPY": "NASDAQ OMX Alpha GE vs. SPY Index", |
| "^GOOSY": "NASDAQ OMX Alpha GOOG vs. SPY Index", |
| "^GVSPY": "NASDAQ OMX Alpha GLD vs. SPY Index", |
| "^HAUL": "Wilder NASDAQ OMX Global Energy Efficient Transport Index", |
| "^HGX": "PHLX Housing Sector", |
| "^IBMSY": "NASDAQ OMX Alpha IBM vs. SPY Index", |
| "^ILTI": "NASDAQ OMX AeA Illinois Tech Index", |
| "^INTSY": "NASDAQ OMX Alpha INTC vs. SPY Index", |
| "^ISRQ": "NASDAQ Israel Index", |
| "^ISRX": "NASDAQ Israel Total Return", |
| "^IVSPY": "NASDAQ OMX Alpha IBM vs. SPY Index", |
| "^IXBK": "NASDAQ Bank", |
| "^IXCO": "NASDAQ Computer", |
| "^IXF": "NASDAQ Financial", |
| "^IXFN": "NASDAQ Other Finance", |
| "^IXHC": "NASDAQ Health Care Index", |
| "^IXID": "NASDAQ Industrial", |
| "^IXIS": "NASDAQ Insurance", |
| "^IXTC": "NASDAQ Telecommunications", |
| "^IXTR": "NASDAQ Transportation", |
| "^JVSPY": "NASDAQ OMX Alpha INTC vs. SPY Index", |
| "^KRX": "KBW Regional Banking Index", |
| "^LVSPY": "NASDAQ OMX Alpha GE vs. SPY Index", |
| "^MFX": "KBW Mortgage Finance Index", |
| "^MRKSY": "NASDAQ OMX Alpha MRK vs. SPY Index", |
| "^MSH": "Morgan Stanley Technology index", |
| "^MXZ": "PHLX Medical Device Sector", |
| "^NBI": "NASDAQ Biotechnology", |
| "^NBIE": "NASDAQ Biotechnology Equal Weighted Index", |
| "^NBIJR": "Nasdaq Junior Biotechnology Index", |
| "^NCI": "Nasdaq Crypto Index", |
| "^NDX": "NASDAQ 100 Index", |
| "^NDXE": "The NASDAQ-100 Equal Weighted Index", |
| "^NDXT": "NASDAQ-100 Technology Sector Index", |
| "^NDXX": "NASDAQ-100 Ex-Tech Sector Index", |
| "^NEUX": "NASDAQ OMX Europe Index", |
| "^NGX": "Nasdaq Next Generation 100 Index", |
| "^NQ7HANDLTL": "Nasdaq 7HANDL Index", |
| "^NQCICLER": "NASDAQ Commodity Crude Oil Index ER", |
| "^NQCIGCER": "NASDAQ Commodity Gold Index ER", |
| "^NQCIHGER": "NASDAQ Commodity HG Copper Index ER", |
| "^NQCINGER": "NASDAQ Commodity Natural Gas Index ER", |
| "^NQCISIER": "NASDAQ Commodity Silver Index ER", |
| "^NQCYBRT": "Nasdaq CTA Cybersecurity Index", |
| "^NQGM": "NASDAQ Global Market Composite", |
| "^NQGS": "NASDAQ Global Select Market Composite", |
| "^NQH2O": "Nasdaq Veles California Water Index", |
| "^NQMGUSL": "Nasdaq US Mega Cap Select Leaders Index", |
| "^NQVWLCCT": "Nasdaq Victory US 500 Large Vol Wt L/C TR", |
| "^NQVWLCT": "Nasdaq Victory US 500 Large Vol Wt TR", |
| "^NQVWLDCT": "Nasdaq Victory US 100 Large High Div Vol Wt L/C TR", |
| "^NQX": "NASDAQ-100 Reduced Value Index", |
| "^NVSPY": "NASDAQ OMX Alpha MRK vs. SPY Index", |
| "^NXTQ": "NASDAQ Q-50", |
| "^OMXB10": "OMX Baltic 10", |
| "^OMXC20": "OMX Copenhagen 20", |
| "^OMXH25": "OMX Helsinki 25", |
| "^OMXN40": "OMX Nordic 40", |
| "^OMXS30": "OMX Stockholm 30 Index", |
| "^ONEQI": "Fidelity Nasdaq Composite Index Tracking Stock", |
| "^OSX": "PHLX Oil Service Sector", |
| "^PRFEI": "PowerShares FTSE RAFI Energy Sector Portfolio", |
| "^PRFFI": "PowerShares FTSE RAFI Financials Sector Portfolio", |
| "^PRFGI": "PowerShares FTSE RAFI Consumer Goods Sector Portfolio", |
| "^PRFHI": "PowerShares FTSE RAFI Health Care Sector Portfolio", |
| "^PRFMI": "PowerShares FTSE RAFI Basic Materials Sector Portfolio", |
| "^PRFNI": "PowerShares FTSE RAFI Industrials Sector Portfolio", |
| "^PRFQI": "PowerShares FTSE RAFI Telecom & Tech Sector Portfolio", |
| "^PRFSI": "PowerShares FTSE RAFI Consumer Goods Sector Portfolio", |
| "^PRFUI": "PowerShares FTSE RAFI Utilities Sector Portfolio", |
| "^PRFZI": "PowerShares FTSE RAFI US 1500 Small-Mid Portfolio", |
| "^QAGR": "NASDAQ OMX Global Agriculture Index", |
| "^QCLNI": "First Trust NASDAQ Clean Edge U.S. Liquid Series", |
| "^QCOL": "NASDAQ OMX Global Coal Index", |
| "^QGLD": "NASDAQ OMX Global Gold & Precious Metals Index", |
| "^QGRI": "NASDAQ OMX Government Relief Index", |
| "^QIRL": "NASDAQ OMX Ireland Index", |
| "^QIV": "NASDAQ 100 After Hours Indicator", |
| "^QMEA": "NASDAQ OMX Middle East North Africa Index", |
| "^QMI": "NASDAQ 100 Pre Market Indicator", |
| "^QNET": "NASDAQ Internet Index", |
| "^QOMX": "NASDAQ OMX 100 Index", |
| "^QQEWI": "First Trust NASDAQ 100 Equal Weighted Index Fund", |
| "^NOCO": "NASDAQ OMX Carbon Excess Return Index", |
| "^QQXTI": "First Trust NASDAQ 100 Ex-Technology Sector", |
| "^QSTL": "NASDAQ OMX Global Steel Index", |
| "^QTECI": "First Trust NASDAQ 100 Technology Sector", |
| "^QWND": "NASDAQ OMX Clean Edge Global Wind Energy Index", |
| "^RCMP": "NASDAQ Capital Market Composite Index", |
| "^RXS": "PHLX Drug Sector", |
| "^SHX": "PHLX Marine Shipping Sector", |
| "^SOX": "PHLX Semiconductor Sector", |
| "^SRVRSCPR": "Kelly Data Center and Tech Infrastructure Index", |
| "^SVO": "SIG Energy MLP Index", |
| "^TRAN": "Dow Transportation", |
| "^TVSPY": "NASDAQ OMX Alpha TLT vs. SPY Index", |
| "^UTY": "PHLX Utility Sector", |
| "^UVSPY": "NASDAQ OMX Alpha GOOG vs. SPY Index", |
| "^VOLNDX": "Volatility NASDAQ - 100", |
| "^VOLQ": "Nasdaq-100 Volatility Index", |
| "^WMTSY": "NASDAQ OMX Alpha WMT vs. SPY Index", |
| "^WVSPY": "NASDAQ OMX Alpha WMT vs. SPY Index", |
| "^XAU": "PHLX Gold/Silver Sector", |
| "^XCM": "PHLX Chemicals Sector", |
| "^XEX": "PHLX Europe Sector", |
| "^XND": "Nasdaq-100 Micro Index", |
| } |
|
|
|
|
| def get_random_agent() -> str: |
| """Get a random user agent.""" |
| |
| from random_user_agent.user_agent import UserAgent |
|
|
| user_agent_rotator = UserAgent(limit=100) |
| user_agent = user_agent_rotator.get_random_user_agent() |
| return user_agent |
|
|
|
|
| def get_companies_backend(): |
| """Get the SQLiteBackend for the TMX companies.""" |
| |
| from aiohttp_client_cache import SQLiteBackend |
| from openbb_core.app.utils import get_user_cache_directory |
|
|
| |
| tmx_companies_backend = SQLiteBackend( |
| f"{get_user_cache_directory()}/http/tmx_companies", |
| expire_after=timedelta(days=2), |
| ) |
|
|
| return tmx_companies_backend |
|
|
|
|
| def get_indices_backend(): |
| """Get the SQLiteBackend for the TMX indices.""" |
| |
| from aiohttp_client_cache import SQLiteBackend |
| from openbb_core.app.utils import get_user_cache_directory |
|
|
| |
| tmx_indices_backend = SQLiteBackend( |
| f"{get_user_cache_directory()}/http/tmx_indices", expire_after=timedelta(days=1) |
| ) |
|
|
| return tmx_indices_backend |
|
|
|
|
| async def response_callback(response, _: Any): |
| """Use callback for HTTP Client Response.""" |
| content_type = response.headers.get("Content-Type", "") |
| if "application/json" in content_type: |
| return await response.json() |
| if "text" in content_type: |
| return await response.text() |
| return await response.read() |
|
|
|
|
| async def get_data_from_url( |
| url: str, |
| use_cache: bool = True, |
| backend: Optional["SQLiteBackend"] = None, |
| **kwargs: Any, |
| ) -> Any: |
| """Make an asynchronous HTTP request to a static file.""" |
| |
| from aiohttp_client_cache.session import CachedSession |
| from openbb_core.provider.utils.helpers import amake_request |
|
|
| data: Any = None |
| if use_cache is True: |
| async with CachedSession(cache=backend) as cached_session: |
| try: |
| response = await cached_session.get(url, **kwargs) |
| data = await response_callback(response, None) |
| finally: |
| await cached_session.close() |
| else: |
| data = await amake_request(url, response_callback=response_callback, timeout=20) |
|
|
| return data |
|
|
|
|
| async def get_data_from_gql(url: str, headers, data, **kwargs: Any) -> Any: |
| """Make an asynchronous GraphQL request.""" |
| |
| from openbb_core.provider.utils.helpers import amake_request |
|
|
| response = await amake_request( |
| url=url, |
| method="POST", |
| response_callback=response_callback, |
| headers=headers, |
| data=data, |
| timeout=30, |
| ) |
|
|
| return response |
|
|
|
|
| def replace_values_in_list_of_dicts(data): |
| """Replace "NA" and "-" with None in a list of dictionaries.""" |
| for d in data: |
| for k, v in d.items(): |
| if isinstance(v, dict): |
| replace_values_in_list_of_dicts([v]) |
| elif isinstance(v, list): |
| for i in range(len(v)): |
| if isinstance(v[i], dict): |
| replace_values_in_list_of_dicts( |
| [v[i]] |
| ) |
| elif v[i] in ("NA", "-"): |
| v[i] = None |
| elif v in ("NA", "-"): |
| d[k] = None |
| return data |
|
|
|
|
| def check_weekday(date) -> str: |
| """Check if the input date is a weekday, and if not, returns the next weekday. |
| |
| Parameters |
| ---------- |
| date: str |
| The date to check in YYYY-MM-DD format. |
| |
| Returns |
| ------- |
| str |
| Date in YYYY-MM-DD format. If the date is a weekend, returns the date of the next weekday. |
| """ |
| |
| from pandas import to_datetime |
| from pandas.tseries.holiday import next_workday |
|
|
| if to_datetime(date).weekday() > 4: |
| return next_workday(to_datetime(date)).strftime("%Y-%m-%d") |
| return date |
|
|
|
|
| async def get_all_etfs(use_cache: bool = True) -> List[Dict]: |
| """Get a summary of the TMX ETF universe. |
| |
| Returns |
| ------- |
| Dict |
| Dictionary with all TMX-listed ETFs. |
| """ |
| |
| from aiohttp_client_cache import SQLiteBackend |
| from openbb_core.app.utils import get_user_cache_directory |
| from pandas import DataFrame |
|
|
| |
| tmx_etfs_backend = SQLiteBackend( |
| f"{get_user_cache_directory()}/http/tmx_etfs", expire_after=timedelta(hours=4) |
| ) |
|
|
| url = "https://dgr53wu9i7rmp.cloudfront.net/etfs/etfs.json" |
|
|
| response = await get_data_from_url( |
| url, use_cache=use_cache, backend=tmx_etfs_backend |
| ) |
|
|
| if not response or response is None: |
| raise OpenBBError("There was a problem with the request. Could not get ETFs.") |
|
|
| response = replace_values_in_list_of_dicts(response) |
|
|
| etfs = DataFrame(response).rename(columns=COLUMNS_DICT) |
|
|
| etfs = etfs.drop( |
| columns=[ |
| "beta_2y", |
| "beta_4y", |
| "beta_6y", |
| "beta_7y", |
| "beta_8y", |
| "beta_9y", |
| "beta_11y", |
| "beta_12y", |
| "beta_13y", |
| "beta_14y", |
| "beta_16y", |
| "beta_17y", |
| "beta_18y", |
| "beta_19y", |
| ] |
| ) |
|
|
| for i in etfs.index: |
| etfs.loc[i, "fund_family"] = etfs.loc[i, "additional_data"].get("fundfamilyen", None) |
| etfs.loc[i, "website"] = etfs.loc[i, "additional_data"].get("websitefactsheeten", None) |
| etfs.loc[i, "mer"] = etfs.loc[i, "additional_data"].get("mer", None) |
| etfs = etfs.fillna("N/A").replace("N/A", None) |
|
|
| return etfs.to_dict(orient="records") |
|
|
|
|
| async def get_tmx_tickers( |
| exchange: Literal["tsx", "tsxv"] = "tsx", use_cache: bool = True |
| ) -> Dict: |
| """Get a dictionary of either TSX or TSX-V symbols and names.""" |
| |
| from pandas import DataFrame |
|
|
| tsx_json_url = "https://www.tsx.com/json/company-directory/search" |
| url = f"{tsx_json_url}/{exchange}/*" |
| response = await get_data_from_url( |
| url, use_cache=use_cache, backend=get_companies_backend() |
| ) |
| data = ( |
| DataFrame.from_records(response["results"])[["symbol", "name"]] |
| .set_index("symbol") |
| .sort_index() |
| ) |
| results = data.to_dict()["name"] |
| return results |
|
|
|
|
| async def get_all_tmx_companies(use_cache: bool = True) -> Dict: |
| """Merge TSX and TSX-V listings into a single dictionary.""" |
| all_tmx = {} |
| tsx_tickers = await get_tmx_tickers(use_cache=use_cache) |
| tsxv_tickers = await get_tmx_tickers("tsxv", use_cache=use_cache) |
| all_tmx.update(tsxv_tickers) |
| all_tmx.update(tsx_tickers) |
| return all_tmx |
|
|
|
|
| async def get_all_options_tickers(use_cache: bool = True) -> "DataFrame": |
| """Return a DataFrame with all valid ticker symbols.""" |
| |
| from io import StringIO |
| from pandas import concat, read_html |
| from openbb_core.provider.utils.helpers import to_snake_case |
|
|
| url = "https://www.m-x.ca/en/trading/data/options-list" |
|
|
| r = await get_data_from_url( |
| url, use_cache=use_cache, backend=get_companies_backend() |
| ) |
|
|
| if r is None or r == []: |
| raise OpenBBError("Error with the request") |
|
|
| options_listings = read_html(StringIO(r)) |
| listings = concat(options_listings) |
| listings = listings.set_index("Option Symbol").drop_duplicates().sort_index() |
| symbols = listings[:-1] |
| symbols = symbols.fillna(value="") |
| symbols["Underlying Symbol"] = ( |
| symbols["Underlying Symbol"].str.replace(" u", ".UN").str.replace("––", "") |
| ) |
| symbols = symbols.reset_index() |
| symbols.columns = [ |
| to_snake_case(col).replace("name_of_", "") for col in symbols.columns |
| ] |
|
|
| return symbols.set_index("option_symbol") |
|
|
|
|
| async def get_current_options(symbol: str, use_cache: bool = True) -> "DataFrame": |
| """Get the current quotes for the complete options chain.""" |
| |
| from io import StringIO |
| from pandas import DataFrame, DatetimeIndex, concat, read_html, to_datetime |
| from openbb_core.provider.utils.helpers import to_snake_case |
|
|
| SYMBOLS = await get_all_options_tickers(use_cache=use_cache) |
| data = DataFrame() |
| symbol = symbol.upper() |
|
|
| |
| symbol = symbol.upper().replace("-", ".").replace(".TO", "").replace(".TSX", "") |
| |
| if len(SYMBOLS[SYMBOLS["underlying_symbol"].str.contains(symbol)]) == 1: |
| symbol = SYMBOLS[SYMBOLS["underlying_symbol"] == symbol].index.values[0] |
| |
| if symbol not in SYMBOLS.index and not SYMBOLS.empty: |
| raise OpenBBError( |
| f"The symbol, {symbol}, is not a valid listing or does not trade options." |
| ) |
|
|
| QUOTES_URL = f"https://www.m-x.ca/en/trading/data/quotes?symbol={symbol}" |
|
|
| cols = [ |
| "expiration", |
| "strike", |
| "bid", |
| "ask", |
| "lastTradePrice", |
| "change", |
| "openInterest", |
| "volume", |
| "optionType", |
| ] |
|
|
| r = await get_data_from_url(QUOTES_URL, use_cache=False) |
| data = read_html(StringIO(r))[0] |
| data = data.iloc[:-1] |
|
|
| expirations = ( |
| data["Unnamed: 0_level_0"]["Expiry date"].astype(str).rename("expiration") |
| ) |
|
|
| expirations = expirations.str.strip("(Weekly)") |
|
|
| strikes = ( |
| data["Unnamed: 7_level_0"] |
| .dropna() |
| .sort_values("Strike") |
| .rename(columns={"Strike": "strike"}) |
| ) |
|
|
| calls = concat([expirations, strikes, data["Calls"]], axis=1) |
| calls["expiration"] = DatetimeIndex(calls["expiration"]).astype(str) |
| calls["optionType"] = "call" |
| calls.columns = cols |
| calls = calls.set_index(["expiration", "strike", "optionType"]) |
|
|
| puts = concat([expirations, strikes, data["Puts"]], axis=1) |
| puts["expiration"] = DatetimeIndex(puts["expiration"]).astype(str) |
| puts["optionType"] = "put" |
| puts.columns = cols |
| puts = puts.set_index(["expiration", "strike", "optionType"]) |
|
|
| chains = concat([calls, puts]) |
| chains["openInterest"] = chains["openInterest"].astype("int64") |
| chains["volume"] = chains["volume"].astype("int64") |
| chains["change"] = chains["change"].astype(float) |
| chains["lastTradePrice"] = chains["lastTradePrice"].astype(float) |
| chains["bid"] = chains["bid"].astype(float) |
| chains["ask"] = chains["ask"].astype(float) |
| chains = chains.sort_index() |
| chains = chains.reset_index() |
| now = datetime.now() |
| temp = DatetimeIndex(chains.expiration) |
| temp_ = (temp - now).days + 1 |
| chains["dte"] = temp_ |
|
|
| |
| _strikes = chains["strike"] |
| strikes = [] |
| for _strike in _strikes: |
| _strike = str(_strike).split(".") |
| front = "0" * (5 - len(_strike[0])) |
| back = "0" * (3 - len(_strike[1])) |
| strike = f"{front}{_strike[0]}{_strike[1]}{back}" |
| strikes.append(str(strike)) |
|
|
| chains["strikes"] = strikes |
| chains["contract_symbol"] = ( |
| symbol |
| + " " * (6 - len(symbol)) |
| + to_datetime(chains["expiration"]).dt.strftime("%y%m%d") |
| + (chains["optionType"].replace("call", "C").replace("put", "P")) |
| + chains["strikes"] |
| ) |
| chains.drop(columns=["strikes"], inplace=True) |
|
|
| chains.columns = [to_snake_case(c) for c in chains.columns.to_list()] |
|
|
| return chains |
|
|
|
|
| async def download_eod_chains( |
| symbol: str, date: Optional[dateType] = None, use_cache: bool = False |
| ) -> "DataFrame": |
| """Download EOD chains data for a given symbol and date.""" |
| |
| from io import StringIO |
| import exchange_calendars as xcals |
| from pandas import DatetimeIndex, Timedelta, read_csv, to_datetime |
| from openbb_core.provider.utils.helpers import to_snake_case |
|
|
| symbol = symbol.upper() |
| SYMBOLS = await get_all_options_tickers(use_cache=False) |
| |
| symbol = symbol.upper().replace("-", ".").replace(".TO", "").replace(".TSX", "") |
|
|
| |
| if len(SYMBOLS[SYMBOLS["underlying_symbol"].str.contains(symbol)]) == 1: |
| symbol = SYMBOLS[SYMBOLS["underlying_symbol"] == symbol].index.values[0] |
| |
| if symbol not in SYMBOLS.index and not SYMBOLS.empty: |
| raise OpenBBError( |
| f"The symbol, {symbol}, is not a valid listing or does not trade options." |
| ) |
|
|
| BASE_URL = "https://www.m-x.ca/en/trading/data/historical?symbol=" |
|
|
| cal = xcals.get_calendar("XTSE") |
|
|
| if date is None: |
| EOD_URL = BASE_URL + f"{symbol}" "&dnld=1#quotes" |
| else: |
| date = check_weekday(date) |
| if cal.is_session(date) is False: |
| date = (to_datetime(date) + timedelta(days=1)).strftime("%Y-%m-%d") |
| date = check_weekday(date) |
| if cal.is_session(date=date) is False: |
| date = (to_datetime(date) + timedelta(days=1)).strftime("%Y-%m-%d") |
|
|
| EOD_URL = ( |
| BASE_URL + f"{symbol}" "&from=" f"{date}" "&to=" f"{date}" "&dnld=1#quotes" |
| ) |
|
|
| r = await get_data_from_url(EOD_URL, use_cache=use_cache) |
|
|
| if r is None: |
| raise OpenBBError("Error with the request, no data was returned.") |
|
|
| data = read_csv(StringIO(r)) |
| if data.empty: |
| raise OpenBBError( |
| f"No data found for, {symbol}, on, {date}." |
| "The symbol may not have been listed, or traded options, before that date." |
| ) |
|
|
| data["contractSymbol"] = data["Symbol"] |
|
|
| data["optionType"] = data["Call/Put"].replace(0, "call").replace(1, "put") |
|
|
| data = data.drop( |
| columns=[ |
| "Symbol", |
| "Class Symbol", |
| "Root Symbol", |
| "Underlying Symbol", |
| "Ins. Type", |
| "Call/Put", |
| ] |
| ) |
|
|
| cols = [ |
| "eod_date", |
| "strike", |
| "expiration", |
| "closeBid", |
| "closeAsk", |
| "closeBidSize", |
| "closeAskSize", |
| "lastTradePrice", |
| "volume", |
| "prevClose", |
| "change", |
| "open", |
| "high", |
| "low", |
| "totalValue", |
| "transactions", |
| "settlementPrice", |
| "openInterest", |
| "impliedVolatility", |
| "contractSymbol", |
| "optionType", |
| ] |
|
|
| data.columns = cols |
| data["underlying_symbol"] = symbol + ":CA" |
| data["expiration"] = to_datetime(data["expiration"], format="%Y-%m-%d") |
| data["eod_date"] = to_datetime(data["eod_date"], format="%Y-%m-%d") |
| data["impliedVolatility"] = 0.01 * data["impliedVolatility"] |
|
|
| date_ = data["eod_date"] |
| temp = DatetimeIndex(data.expiration) |
| temp_ = temp - date_ |
| data["dte"] = [Timedelta(_temp_).days for _temp_ in temp_] |
| data = data.set_index(["expiration", "strike", "optionType"]).sort_index() |
| data["eod_date"] = data["eod_date"].astype(str) |
| underlying_price = data.iloc[-1]["lastTradePrice"] |
| data["underlyingPrice"] = underlying_price |
| data = data.reset_index() |
| data = data[data["strike"] != 0] |
| data["expiration"] = to_datetime(data["expiration"]).dt.strftime("%Y-%m-%d") |
|
|
| data.columns = [to_snake_case(c) for c in data.columns.to_list()] |
|
|
| return data |
|
|
|
|
| async def get_company_filings( |
| symbol: str, |
| start_date: Optional[str] = (datetime.now() - timedelta(days=30)).strftime( |
| "%Y-%m-%d" |
| ), |
| end_date: Optional[str] = datetime.now().date().strftime("%Y-%m-%d"), |
| limit: int = 50, |
| ) -> List[Dict]: |
| """Get company filings.""" |
| |
| import json |
|
|
| user_agent = get_random_agent() |
| results: List[Dict] = [] |
| symbol = symbol.upper().replace("-", ".").replace(".TO", "").replace(".TSX", "") |
|
|
| payload = gql.get_company_filings_payload |
| payload["variables"]["symbol"] = symbol |
| payload["variables"]["fromDate"] = start_date |
| payload["variables"]["toDate"] = end_date |
| payload["variables"]["limit"] = limit |
| url = "https://app-money.tmx.com/graphql" |
| try: |
| r = await get_data_from_gql( |
| url=url, |
| data=json.dumps(payload), |
| headers={ |
| "Accept": "*/*", |
| "Accept-Encoding": "gzip, deflate, br", |
| "Accept-Language": "en-CA,en-US;q=0.7,en;q=0.3", |
| "Connection": "keep-alive", |
| "Content-Type": "application/json", |
| "Host": "app-money.tmx.com", |
| "Origin": "https://money.tmx.com", |
| "Referer": "https://money.tmx.com/", |
| "locale": "en", |
| "Sec-Fetch-Dest": "empty", |
| "Sec-Fetch-Mode": "cors", |
| "Sec-Fetch-Site": "same-site", |
| "TE": "trailers", |
| "User-Agent": user_agent, |
| }, |
| ) |
| except Exception as _e: |
| raise OpenBBError(_e) from _e |
| if r["data"]["filings"] is None: |
| results = [] |
| results = r.get("data").get("filings") |
|
|
| return results |
|
|
|
|
| async def get_daily_price_history( |
| symbol: str, |
| start_date: Optional[Union[str, dateType]] = None, |
| end_date: Optional[Union[str, dateType]] = None, |
| adjustment: Literal[ |
| "splits_only", "unadjusted", "splits_and_dividends" |
| ] = "splits_only", |
| ): |
| """Get historical price data.""" |
| |
| import json |
| import asyncio |
| from dateutil import rrule |
|
|
| start_date = ( |
| datetime.strptime(start_date, "%Y-%m-%d") |
| if isinstance(start_date, str) |
| else start_date |
| ) |
| end_date = ( |
| datetime.strptime(end_date, "%Y-%m-%d") |
| if isinstance(end_date, str) |
| else end_date |
| ) |
| user_agent = get_random_agent() |
| results: List[Dict] = [] |
| symbol = symbol.upper().replace("-", ".").replace(".TO", "").replace(".TSX", "") |
| start_date = ( |
| (datetime.now() - timedelta(weeks=52)).date() |
| if start_date is None |
| else start_date |
| ) |
| end_date = datetime.now() if end_date is None else end_date |
|
|
| |
| dates = list( |
| rrule.rrule(rrule.WEEKLY, interval=4, dtstart=start_date, until=end_date) |
| ) |
|
|
| |
| if dates[-1] != end_date: |
| dates.append(end_date) |
|
|
| |
| chunks = [ |
| (dates[i], dates[i + 1] - timedelta(days=1)) for i in range(len(dates) - 1) |
| ] |
|
|
| |
| chunks[-1] = (chunks[-1][0], end_date) |
|
|
| async def create_task(start, end, results): |
| """Create a task from a start and end date chunk.""" |
| payload = gql.get_company_price_history_payload.copy() |
| payload["variables"]["adjusted"] = ( |
| False if adjustment == "unadjusted" else True |
| ) |
| payload["variables"]["adjustmentType"] = ( |
| "SO" if adjustment == "splits_only" else None |
| ) |
| payload["variables"]["end"] = end.strftime("%Y-%m-%d") |
| payload["variables"]["start"] = start.strftime("%Y-%m-%d") |
| payload["variables"]["symbol"] = symbol |
| payload["variables"]["unadjusted"] = ( |
| True if adjustment == "unadjusted" else False |
| ) |
| if payload["variables"]["adjustmentType"] is None: |
| payload["variables"].pop("adjustmentType") |
| url = "https://app-money.tmx.com/graphql" |
|
|
| async def try_again(): |
| """Try again if it fails.""" |
| return await get_data_from_gql( |
| method="POST", |
| url=url, |
| data=json.dumps(payload), |
| headers={ |
| "authority": "app-money.tmx.com", |
| "referer": f"https://money.tmx.com/en/quote/{symbol}", |
| "locale": "en", |
| "Content-Type": "application/json", |
| "User-Agent": user_agent, |
| "Accept": "*/*", |
| }, |
| timeout=3, |
| ) |
|
|
| try: |
| data = await get_data_from_gql( |
| method="POST", |
| url=url, |
| data=json.dumps(payload), |
| headers={ |
| "authority": "app-money.tmx.com", |
| "referer": f"https://money.tmx.com/en/quote/{symbol}", |
| "locale": "en", |
| "Content-Type": "application/json", |
| "User-Agent": user_agent, |
| "Accept": "*/*", |
| }, |
| timeout=3, |
| ) |
| except Exception: |
| data = await try_again() |
|
|
| if isinstance(data, str): |
| data = await try_again() |
|
|
| if data.get("data") and data["data"].get("getCompanyPriceHistory"): |
| results.extend(data["data"].get("getCompanyPriceHistory")) |
|
|
| return results |
|
|
| tasks = [create_task(chunk[0], chunk[1], results) for chunk in chunks] |
|
|
| await asyncio.gather(*tasks) |
|
|
| results = [d for d in results if d["openPrice"] is not None] |
|
|
| return sorted(results, key=lambda x: x["datetime"], reverse=False) |
|
|
|
|
| async def get_weekly_or_monthly_price_history( |
| symbol: str, |
| start_date: Optional[Union[str, dateType]] = None, |
| end_date: Optional[Union[str, dateType]] = None, |
| interval: Literal["month", "week"] = "month", |
| ): |
| """Get historical price data.""" |
| |
| import json |
|
|
| if start_date: |
| start_date = ( |
| datetime.strptime(start_date, "%Y-%m-%d") |
| if isinstance(start_date, str) |
| else start_date |
| ) |
| if end_date: |
| end_date = ( |
| datetime.strptime(end_date, "%Y-%m-%d") |
| if isinstance(end_date, str) |
| else end_date |
| ) |
| user_agent = get_random_agent() |
| results: List[Dict] = [] |
| symbol = symbol.upper().replace("-", ".").replace(".TO", "").replace(".TSX", "") |
| start_date = ( |
| (datetime.now() - timedelta(weeks=52 * 100)).date() |
| if start_date is None |
| else start_date |
| ) |
| end_date = datetime.now() if end_date is None else end_date |
|
|
| payload = gql.get_timeseries_payload.copy() |
| if "interval" in payload["variables"]: |
| payload["variables"].pop("interval") |
| if "startDateTime" in payload["variables"]: |
| payload["variables"].pop("startDateTime") |
| if "endDateTime" in payload["variables"]: |
| payload["variables"].pop("endDateTime") |
| payload["variables"]["symbol"] = symbol |
| payload["variables"]["freq"] = interval |
| payload["variables"]["end"] = ( |
| end_date.strftime("%Y-%m-%d") if isinstance(end_date, dateType) else end_date |
| ) |
| payload["variables"]["start"] = ( |
| start_date.strftime("%Y-%m-%d") |
| if isinstance(start_date, dateType) |
| else start_date |
| ) |
| url = "https://app-money.tmx.com/graphql" |
| data = await get_data_from_gql( |
| method="POST", |
| url=url, |
| data=json.dumps(payload), |
| headers={ |
| "authority": "app-money.tmx.com", |
| "referer": f"https://money.tmx.com/en/quote/{symbol}", |
| "locale": "en", |
| "Content-Type": "application/json", |
| "User-Agent": user_agent, |
| "Accept": "*/*", |
| }, |
| timeout=3, |
| ) |
|
|
| async def try_again(): |
| """Try again if the request fails.""" |
| return await get_data_from_gql( |
| method="POST", |
| url=url, |
| data=json.dumps(payload), |
| headers={ |
| "authority": "app-money.tmx.com", |
| "referer": f"https://money.tmx.com/en/quote/{symbol}", |
| "locale": "en", |
| "Content-Type": "application/json", |
| "User-Agent": user_agent, |
| "Accept": "*/*", |
| }, |
| timeout=3, |
| ) |
|
|
| if isinstance(data, str): |
| data = await try_again() |
|
|
| if data.get("data") and data["data"].get("getTimeSeriesData"): |
| results = data["data"].get("getTimeSeriesData") |
| results = sorted(results, key=lambda x: x["dateTime"], reverse=False) |
| return results |
|
|
|
|
| async def get_intraday_price_history( |
| symbol: str, |
| start_date: Optional[Union[str, dateType]] = None, |
| end_date: Optional[Union[str, dateType]] = None, |
| interval: Optional[int] = 1, |
| ): |
| """Get historical price data.""" |
| |
| import json |
| import asyncio |
| import pytz |
| from dateutil import rrule |
|
|
| if start_date: |
| start_date = ( |
| datetime.strptime(start_date, "%Y-%m-%d") |
| if isinstance(start_date, str) |
| else start_date |
| ) |
| if end_date: |
| end_date = ( |
| datetime.strptime(end_date, "%Y-%m-%d") |
| if isinstance(end_date, str) |
| else end_date |
| ) |
| user_agent = get_random_agent() |
| results: List[Dict] = [] |
| symbol = symbol.upper().replace("-", ".").replace(".TO", "").replace(".TSX", "") |
| start_date = ( |
| (datetime.now() - timedelta(weeks=4)).date() |
| if start_date is None |
| else start_date |
| ) |
| end_date = datetime.now().date() if end_date is None else end_date |
| |
| date_check = datetime(2022, 4, 12).date() |
| start_date = max(start_date, date_check) |
| if end_date < date_check: |
| end_date = datetime.now().date() |
| |
| dates = list( |
| rrule.rrule(rrule.WEEKLY, interval=4, dtstart=start_date, until=end_date) |
| ) |
|
|
| if dates[-1] != end_date: |
| dates.append(end_date) |
|
|
| |
| chunks = [ |
| (dates[i], dates[i + 1] - timedelta(days=1)) for i in range(len(dates) - 1) |
| ] |
|
|
| |
| chunks[-1] = (chunks[-1][0], end_date) |
|
|
| async def create_task(start, end, results): |
| """Create a task from a start and end date chunk.""" |
| |
| start_obj = datetime.combine(start, time(9, 30)) |
| end_obj = datetime.combine(end, time(16, 0)) |
|
|
| |
| est = pytz.timezone("US/Eastern") |
| start_obj_est = est.localize(start_obj) |
| end_obj_est = est.localize(end_obj) |
|
|
| |
| start_time = int(start_obj_est.timestamp()) |
| end_time = int(end_obj_est.timestamp()) |
|
|
| payload = gql.get_timeseries_payload.copy() |
| payload["variables"]["interval"] = None |
| if payload["variables"].get("start"): |
| payload["variables"].pop("start") |
| payload["variables"]["startDateTime"] = int(start_time) |
| if payload["variables"].get("end"): |
| payload["variables"].pop("end") |
| payload["variables"]["endDateTime"] = int(end_time) |
| payload["variables"]["interval"] = interval |
| payload["variables"]["symbol"] = symbol |
| if payload["variables"].get("freq"): |
| payload["variables"].pop("freq") |
| url = "https://app-money.tmx.com/graphql" |
| data = await get_data_from_gql( |
| method="POST", |
| url=url, |
| data=json.dumps(payload), |
| headers={ |
| "authority": "app-money.tmx.com", |
| "referer": f"https://money.tmx.com/en/quote/{symbol}", |
| "locale": "en", |
| "Content-Type": "application/json", |
| "User-Agent": user_agent, |
| "Accept": "*/*", |
| }, |
| timeout=3, |
| ) |
|
|
| async def try_again(): |
| """Try again if the request fails.""" |
| return await get_data_from_gql( |
| method="POST", |
| url=url, |
| data=json.dumps(payload), |
| headers={ |
| "authority": "app-money.tmx.com", |
| "referer": f"https://money.tmx.com/en/quote/{symbol}", |
| "locale": "en", |
| "Content-Type": "application/json", |
| "User-Agent": user_agent, |
| "Accept": "*/*", |
| }, |
| timeout=3, |
| ) |
|
|
| if isinstance(data, str): |
| data = await try_again() |
|
|
| if data.get("data") and data["data"].get("getTimeSeriesData"): |
| result = data["data"].get("getTimeSeriesData") |
| results.extend(result) |
|
|
| return results |
|
|
| tasks = [create_task(chunk[0], chunk[1], results) for chunk in chunks] |
|
|
| await asyncio.gather(*tasks) |
|
|
| if len(results) > 0 and "dateTime" in results[0]: |
| results = sorted(results, key=lambda x: x["dateTime"], reverse=False) |
|
|
| return results |
|
|
|
|
| async def get_all_bonds(use_cache: bool = True) -> "DataFrame": |
| """Get all bonds reference data published by CIRO. |
| |
| The complete list is approximately 70-100K securities. |
| """ |
| |
| from aiohttp_client_cache import SQLiteBackend |
| from openbb_core.app.utils import get_user_cache_directory |
| from pandas import DataFrame |
|
|
| tmx_bonds_backend = SQLiteBackend( |
| f"{get_user_cache_directory()}/http/tmx_bonds", expire_after=timedelta(days=1) |
| ) |
|
|
| url = "https://bondtradedata.iiroc.ca/debtip/designatedbonds/list" |
| response = await get_data_from_url( |
| url, use_cache=use_cache, timeout=30, backend=tmx_bonds_backend |
| ) |
|
|
| |
| |
| bonds_data = ( |
| DataFrame.from_records(response) |
| .replace("N/A", None) |
| .sort_values(by=["lastTradedDate", "totalTrades"], ascending=False) |
| ) |
|
|
| bonds_data["issuer"] = ( |
| bonds_data["issuer"].fillna("-").replace("-", None).astype(str) |
| ) |
|
|
| int_columns = ["totalTrades", "secKey"] |
| for column in int_columns: |
| bonds_data[column] = bonds_data[column].astype(int) |
|
|
| float_columns = [ |
| "lastPrice", |
| "lowestPrice", |
| "highestPrice", |
| "lastYield", |
| "couponRate", |
| ] |
| for column in float_columns: |
| bonds_data[column] = bonds_data[column].astype(float) |
|
|
| return bonds_data |
|
|