Merge branch 'develop' into feature/argparse_translator

This commit is contained in:
Henrique Joaquim
2024-02-20 15:37:28 +00:00
committed by GitHub
90 changed files with 256717 additions and 483 deletions

View File

@@ -5,7 +5,7 @@ from datetime import (
)
from typing import List, Optional, Union
from pydantic import Field, field_validator
from pydantic import Field
from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.query_params import QueryParams
@@ -67,15 +67,6 @@ class BondPricesQueryParams(QueryParams):
description="Minimum yield to maturity of the bond.",
)
@field_validator("isin", "currency", "lei", mode="before", check_fields=False)
@classmethod
def validate_upper_case(cls, v):
"""Convert the field to uppercase and convert a list to a query string."""
if isinstance(v, str):
return v.upper()
v = ",".join([symbol.upper() for symbol in list(v)])
return v if v else None
class BondPricesData(Data):
"""Bond Prices Data."""

View File

@@ -65,8 +65,7 @@ class BondReferenceQueryParams(QueryParams):
"""Convert the field to uppercase and convert a list to a query string."""
if isinstance(v, str):
return v.upper()
v = ",".join([symbol.upper() for symbol in list(v)])
return v if v else None
return ",".join([symbol.upper() for symbol in list(v)]) if v else None
class BondReferenceData(Data):

View File

@@ -1,20 +1,21 @@
"""Index Constituents Standard Model."""
from typing import List, Optional, Set, Union
from typing import Optional
from pydantic import Field, field_validator
from pydantic import Field
from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.query_params import QueryParams
from openbb_core.provider.utils.descriptions import DATA_DESCRIPTIONS
from openbb_core.provider.utils.descriptions import (
DATA_DESCRIPTIONS,
QUERY_DESCRIPTIONS,
)
class IndexConstituentsQueryParams(QueryParams):
"""Index Constituents Query."""
index: str = Field(
description="Index to fetch the constituents of.",
)
symbol: str = Field(description=QUERY_DESCRIPTIONS.get("symbol", ""))
class IndexConstituentsData(Data):
@@ -24,11 +25,3 @@ class IndexConstituentsData(Data):
name: Optional[str] = Field(
default=None, description="Name of the constituent company in the index."
)
@field_validator("symbol", mode="before", check_fields=False)
@classmethod
def upper_symbol(cls, v: Union[str, List[str], Set[str]]):
"""Convert symbol to uppercase."""
if isinstance(v, str):
return v.upper()
return ",".join([symbol.upper() for symbol in list(v)])

View File

@@ -12,8 +12,8 @@ from openbb_core.provider.utils.descriptions import DATA_DESCRIPTIONS
class IndexSnapshotsQueryParams(QueryParams):
"""Index Snapshots Query."""
region: Optional[str] = Field(
default=None, description="The region to return data for - i.e. 'us' or 'eu'."
region: str = Field(
default="us", description="The region of focus for the data - i.e., us, eu."
)

View File

@@ -37,7 +37,9 @@ class InsiderTradingQueryParams(QueryParams):
class InsiderTradingData(Data):
"""Insider Trading Data."""
symbol: str = Field(description=DATA_DESCRIPTIONS.get("symbol", ""))
symbol: Optional[str] = Field(
default=None, description=DATA_DESCRIPTIONS.get("symbol", "")
)
company_cik: Optional[Union[int, str]] = Field(
default=None, description="CIK number of the company."
)

View File

@@ -7,14 +7,11 @@ from pydantic import Field
from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.query_params import QueryParams
from openbb_core.provider.utils.descriptions import (
DATA_DESCRIPTIONS,
QUERY_DESCRIPTIONS,
)
from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
class USTreasuryPricesQueryParams(QueryParams):
"""US Treasury Prices Query."""
class TreasuryPricesQueryParams(QueryParams):
"""Treasury Prices Query."""
date: Optional[dateType] = Field(
description=QUERY_DESCRIPTIONS.get("date", "")
@@ -23,19 +20,33 @@ class USTreasuryPricesQueryParams(QueryParams):
)
class USTreasuryPricesData(Data):
"""US Treasury Prices Data."""
class TreasuryPricesData(Data):
"""Treasury Prices Data."""
date: dateType = Field(description=DATA_DESCRIPTIONS.get("date", ""))
cusip: str = Field(description="CUSIP of the security.")
security_type: str = Field(
description="The type of US Treasury security. Bill, Note, Bond, TIPS, FRN."
)
rate: Optional[float] = Field(
description="The annualized interest rate or coupon of the security.",
issuer_name: Optional[str] = Field(
default=None,
description="Name of the issuing entity.",
)
cusip: Optional[str] = Field(
default=None,
description="CUSIP of the security.",
)
isin: Optional[str] = Field(
default=None,
description="ISIN of the security.",
)
security_type: Optional[str] = Field(
default=None,
description="The type of Treasury security - i.e., Bill, Note, Bond, TIPS, FRN.",
)
issue_date: Optional[dateType] = Field(
default=None,
description="The original issue date of the security.",
)
maturity_date: Optional[dateType] = Field(
default=None,
description="The maturity date of the security.",
)
maturity_date: dateType = Field(description="The maturity date of the security.")
call_date: Optional[dateType] = Field(
description="The call date of the security.", default=None
)
@@ -48,3 +59,32 @@ class USTreasuryPricesData(Data):
eod_price: Optional[float] = Field(
description="The end-of-day price of the security.", default=None
)
last_traded_date: Optional[dateType] = Field(
description="The last trade date of the security.", default=None
)
total_trades: Optional[int] = Field(
default=None,
description="Total number of trades on the last traded date.",
)
last_price: Optional[float] = Field(
description="The last price of the security.", default=None
)
highest_price: Optional[float] = Field(
default=None,
description="The highest price for the bond on the last traded date.",
)
lowest_price: Optional[float] = Field(
default=None,
description="The lowest price for the bond on the last traded date.",
)
rate: Optional[float] = Field(
description="The annualized interest rate or coupon of the security.",
default=None,
)
ytm: Optional[float] = Field(
default=None,
description="Yield to maturity (YTM) is the rate of return anticipated on a bond"
+ " if it is held until the maturity date. It takes into account"
+ " the current market price, par value, coupon rate and time to maturity. It is assumed that all"
+ " coupons are reinvested at the same rate.",
)

View File

@@ -52,6 +52,7 @@ openbb-government-us = { path = "./providers/government_us", optional = true, de
openbb-nasdaq = { path = "./providers/nasdaq", optional = true, develop = true }
openbb-seeking-alpha = { path = "./providers/seeking_alpha", optional = true, develop = true }
openbb-stockgrid = { path = "./providers/stockgrid" , optional = true, develop = true }
openbb_tmx = { path = "./providers/tmx", optional = true, develop = true }
openbb-wsj = { path = "./providers/wsj", optional = true, develop = true }
openbb-charting = { path = "./obbject_extensions/charting", optional = true, develop = true }

View File

@@ -25,6 +25,14 @@ def headers():
[
({"provider": "intrinio", "symbol": "AAPL", "date": "2023-01-25"}),
({"provider": "cboe", "symbol": "AAPL", "use_cache": False}),
(
{
"provider": "tmx",
"symbol": "SHOP",
"date": "2022-12-28",
"use_cache": False,
}
),
],
)
@pytest.mark.integration

View File

@@ -22,6 +22,14 @@ def obb(pytestconfig):
[
({"provider": "intrinio", "symbol": "AAPL", "date": "2023-01-25"}),
({"provider": "cboe", "symbol": "AAPL", "use_cache": False}),
(
{
"provider": "tmx",
"symbol": "SHOP",
"date": "2022-12-28",
"use_cache": False,
}
),
],
)
@pytest.mark.integration

View File

@@ -139,6 +139,7 @@ def test_equity_calendar_splits(params, headers):
[
({"start_date": "2023-11-09", "end_date": "2023-11-10", "provider": "fmp"}),
({"start_date": "2023-11-09", "end_date": "2023-11-10", "provider": "nasdaq"}),
({"start_date": "2023-11-09", "end_date": "2023-11-10", "provider": "tmx"}),
],
)
@pytest.mark.integration
@@ -292,6 +293,7 @@ def test_equity_fundamental_historical_splits(params, headers):
"provider": "intrinio",
}
),
({"symbol": "RY", "provider": "tmx"}),
(
{
"symbol": "AAPL",
@@ -451,6 +453,13 @@ def test_equity_fundamental_income_growth(params, headers):
@parametrize(
"params",
[
(
{
"symbol": "RY",
"provider": "tmx",
"limit": None,
}
),
(
{
"provider": "fmp",
@@ -693,6 +702,7 @@ def test_equity_estimates_analyst_search(params, headers):
[
({"symbol": "AAPL", "provider": "fmp"}),
({"symbol": "AAPL,AMZN,RELIANCE.NS", "provider": "yfinance"}),
({"symbol": "TD:US", "provider": "tmx"}),
],
)
@pytest.mark.integration
@@ -799,6 +809,16 @@ def test_equity_fundamental_revenue_per_segment(params, headers):
"use_cache": False,
}
),
(
{
"provider": "tmx",
"symbol": "IBM:US",
"start_date": "2023-09-30",
"end_date": "2023-12-31",
"limit": None,
"form_type": None,
}
),
],
)
@pytest.mark.integration
@@ -1048,6 +1068,16 @@ def test_equity_compare_groups(params, headers):
"interval": "1M",
}
),
(
{
"provider": "tmx",
"symbol": "AAPL:US",
"start_date": "2023-01-01",
"end_date": "2023-12-31",
"interval": "1d",
"adjustment": "splits_only",
}
),
],
)
@pytest.mark.integration
@@ -1241,6 +1271,7 @@ def test_equity_fundamental_latest_attributes(params, headers):
({"query": "AAPl", "is_symbol": True, "provider": "cboe", "use_cache": False}),
({"query": "Apple", "provider": "sec", "use_cache": False, "is_fund": False}),
({"query": "", "provider": "nasdaq", "use_cache": False, "is_etf": True}),
({"query": "gold", "provider": "tmx", "use_cache": False}),
(
{
"query": "gold",
@@ -1308,6 +1339,7 @@ def test_equity_screener(params, headers):
({"symbol": "AAPL", "provider": "fmp"}),
({"symbol": "AAPL", "provider": "cboe", "use_cache": False}),
({"symbol": "AAPL", "provider": "yfinance"}),
({"symbol": "AAPL:US", "provider": "tmx"}),
],
)
@pytest.mark.integration
@@ -1328,6 +1360,7 @@ def test_equity_price_quote(params, headers):
({"symbol": "AAPL,MSFT", "provider": "intrinio"}),
({"symbol": "AAPL,MSFT", "provider": "finviz"}),
({"symbol": "AAPL,MSFT", "provider": "yfinance"}),
({"provider": "tmx", "symbol": "AAPL:US"}),
({"symbol": "AAPL,MSFT", "provider": "fmp"}),
],
)
@@ -1344,7 +1377,10 @@ def test_equity_profile(params, headers):
@parametrize(
"params",
[({"sort": "desc", "provider": "yfinance"})],
[
({"sort": "desc", "provider": "yfinance"}),
({"provider": "tmx", "category": "52w_high"}),
],
)
@pytest.mark.integration
def test_equity_discovery_gainers(params, headers):

View File

@@ -127,6 +127,7 @@ def test_equity_calendar_splits(params, obb):
[
({"start_date": "2023-11-09", "end_date": "2023-11-10", "provider": "fmp"}),
({"start_date": "2023-11-09", "end_date": "2023-11-10", "provider": "nasdaq"}),
({"start_date": "2023-11-09", "end_date": "2023-11-10", "provider": "tmx"}),
],
)
@pytest.mark.integration
@@ -269,6 +270,7 @@ def test_equity_fundamental_historical_splits(params, obb):
"provider": "intrinio",
}
),
({"symbol": "RY", "provider": "tmx"}),
(
{
"symbol": "AAPL",
@@ -419,8 +421,9 @@ def test_equity_fundamental_income_growth(params, obb):
[
(
{
"symbol": "AAPL",
"limit": 10,
"symbol": "RY",
"provider": "tmx",
"limit": None,
}
),
(
@@ -646,6 +649,7 @@ def test_equity_estimates_analyst_search(params, obb):
[
({"symbol": "AAPL", "provider": "fmp"}),
({"symbol": "AAPL,AMZN,RELIANCE.NS", "provider": "yfinance"}),
({"symbol": "TD:US", "provider": "tmx"}),
],
)
@pytest.mark.integration
@@ -756,6 +760,16 @@ def test_equity_fundamental_revenue_per_segment(params, obb):
"use_cache": False,
}
),
(
{
"provider": "tmx",
"symbol": "IBM:US",
"start_date": "2023-09-30",
"end_date": "2023-12-31",
"limit": None,
"form_type": None,
}
),
],
)
@pytest.mark.integration
@@ -993,6 +1007,16 @@ def test_equity_compare_groups(params, obb):
"interval": "1M",
}
),
(
{
"provider": "tmx",
"symbol": "AAPL:US",
"start_date": "2023-01-01",
"end_date": "2023-12-31",
"interval": "1d",
"adjustment": "splits_only",
}
),
],
)
@pytest.mark.integration
@@ -1176,6 +1200,7 @@ def test_equity_fundamental_latest_attributes(params, obb):
({"query": "AAPL", "is_symbol": True, "provider": "cboe", "use_cache": False}),
({"query": "Apple", "provider": "sec", "use_cache": False, "is_fund": False}),
({"query": "", "provider": "nasdaq", "use_cache": False, "is_etf": True}),
({"query": "gold", "provider": "tmx", "use_cache": False}),
(
{
"query": "gold",
@@ -1238,6 +1263,7 @@ def test_equity_screener(params, obb):
({"symbol": "AAPL", "provider": "fmp"}),
({"symbol": "AAPL", "provider": "cboe", "use_cache": False}),
({"symbol": "AAPL", "provider": "yfinance"}),
({"symbol": "AAPL:US", "provider": "tmx"}),
],
)
@pytest.mark.integration
@@ -1256,6 +1282,7 @@ def test_equity_price_quote(params, obb):
({"symbol": "AAPL,MSFT", "provider": "finviz"}),
({"symbol": "AAPL,MSFT", "provider": "yfinance"}),
({"symbol": "AAPL,MSFT", "provider": "fmp"}),
({"provider": "tmx", "symbol": "AAPL:US"}),
],
)
@pytest.mark.integration
@@ -1271,7 +1298,10 @@ def test_equity_profile(params, obb):
@parametrize(
"params",
[({"sort": "desc"})],
[
({"sort": "desc", "provider": "yfinance"}),
({"provider": "tmx", "category": "52w_high"}),
],
)
@pytest.mark.integration
def test_equity_discovery_gainers(params, obb):

View File

@@ -23,6 +23,15 @@ def headers():
"params",
[
({"query": "", "provider": "fmp"}),
(
{
"query": "vanguard",
"provider": "tmx",
"div_freq": "quarterly",
"sort_by": "return_1y",
"use_cache": False,
}
),
],
)
@pytest.mark.integration
@@ -72,7 +81,7 @@ def test_etf_historical(params, headers):
"params",
[
({"symbol": "IOO", "provider": "fmp"}),
({"symbol": "MISL", "provider": "fmp"}),
({"symbol": "XIU", "provider": "tmx", "use_cache": False}),
({"symbol": "QQQ", "provider": "yfinance"}),
],
)
@@ -91,7 +100,7 @@ def test_etf_info(params, headers):
"params",
[
({"symbol": "IOO", "provider": "fmp"}),
({"symbol": "MISL", "provider": "fmp"}),
({"symbol": "XIU", "provider": "tmx", "use_cache": False}),
],
)
@pytest.mark.integration
@@ -157,6 +166,13 @@ def test_etf_holdings_date(params, headers):
"use_cache": False,
}
),
(
{
"symbol": "XIU",
"provider": "tmx",
"use_cache": False,
}
),
],
)
@pytest.mark.integration
@@ -190,7 +206,10 @@ def test_etf_price_performance(params, headers):
@parametrize(
"params",
[({"symbol": "IOO"})],
[
({"symbol": "IOO", "provider": "fmp"}),
({"symbol": "XIU", "use_cache": False, "provider": "tmx"}),
],
)
@pytest.mark.integration
def test_etf_countries(params, headers):

View File

@@ -22,6 +22,15 @@ def obb(pytestconfig): # pylint: disable=inconsistent-return-statements
"params",
[
({"query": None, "provider": "fmp"}),
(
{
"query": "vanguard",
"provider": "tmx",
"div_freq": "quarterly",
"sort_by": "return_1y",
"use_cache": False,
}
),
],
)
@pytest.mark.integration
@@ -69,7 +78,7 @@ def test_etf_historical(params, obb):
"params",
[
({"symbol": "IOO", "provider": "fmp"}),
({"symbol": "MISL", "provider": "fmp"}),
({"symbol": "XIU", "provider": "tmx", "use_cache": False}),
({"symbol": "QQQ", "provider": "yfinance"}),
],
)
@@ -87,7 +96,7 @@ def test_etf_info(params, obb):
"params",
[
({"symbol": "IOO", "provider": "fmp"}),
({"symbol": "MISL", "provider": "fmp"}),
({"symbol": "XIU", "provider": "tmx", "use_cache": False}),
],
)
@pytest.mark.integration
@@ -151,6 +160,13 @@ def test_etf_holdings_date(params, obb):
"use_cache": False,
}
),
(
{
"symbol": "XIU",
"provider": "tmx",
"use_cache": False,
}
),
],
)
@pytest.mark.integration
@@ -182,7 +198,10 @@ def test_etf_price_performance(params, obb):
@parametrize(
"params",
[({"symbol": "IOO"})],
[
({"symbol": "IOO", "provider": "fmp"}),
({"symbol": "XIU", "provider": "tmx", "use_cache": False}),
],
)
@pytest.mark.integration
def test_etf_countries(params, obb):

View File

@@ -155,7 +155,6 @@ def test_fixedincome_rate_ameribor(params, headers):
@parametrize(
"params",
[
({"start_date": "2023-01-01", "end_date": "2023-06-06", "provider": "fred"}),
(
{
"parameter": "weekly",
@@ -515,12 +514,25 @@ def test_fixedincome_government_treasury_auctions(params, headers):
),
(
{
"date": "2023-11-16",
"date": "2023-12-28",
"cusip": None,
"security_type": "bill",
"provider": "government_us",
}
),
(
{
"date": None,
"provider": "tmx",
"govt_type": "federal",
"issue_date_min": None,
"issue_date_max": None,
"last_traded_min": None,
"maturity_date_min": None,
"maturity_date_max": None,
"use_cache": True,
}
),
],
)
@pytest.mark.integration
@@ -534,3 +546,39 @@ def test_fixedincome_government_treasury_prices(params, headers):
result = requests.get(url, headers=headers, timeout=30)
assert isinstance(result, requests.Response)
assert result.status_code == 200
@pytest.mark.parametrize(
"params",
[
(
{
"provider": "tmx",
"issuer_name": "federal",
"issue_date_min": None,
"issue_date_max": None,
"last_traded_min": None,
"coupon_min": 3,
"coupon_max": None,
"currency": None,
"issued_amount_min": None,
"issued_amount_max": None,
"maturity_date_min": None,
"maturity_date_max": None,
"isin": None,
"lei": None,
"country": None,
"use_cache": False,
}
)
],
)
@pytest.mark.integration
def test_fixedincome_corporate_bond_prices(params, headers):
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/fixedincome/corporate/bond_prices?{query_str}"
result = requests.get(url, headers=headers, timeout=30)
assert isinstance(result, requests.Response)
assert result.status_code == 200

View File

@@ -137,7 +137,6 @@ def test_fixedincome_rate_ameribor(params, obb):
@parametrize(
"params",
[
({"start_date": "2023-01-01", "end_date": "2023-06-06"}),
(
{
"parameter": "weekly",
@@ -477,12 +476,25 @@ def test_fixedincome_government_treasury_auctions(params, obb):
),
(
{
"date": "2023-11-16",
"date": "2023-12-28",
"cusip": None,
"security_type": "bill",
"provider": "government_us",
}
),
(
{
"date": None,
"provider": "tmx",
"govt_type": "federal",
"issue_date_min": None,
"issue_date_max": None,
"last_traded_min": None,
"maturity_date_min": None,
"maturity_date_max": None,
"use_cache": True,
}
),
],
)
@pytest.mark.integration
@@ -493,3 +505,38 @@ def test_fixedincome_government_treasury_prices(params, obb):
assert result
assert isinstance(result, OBBject)
assert len(result.results) > 0
@pytest.mark.parametrize(
"params",
[
(
{
"provider": "tmx",
"issuer_name": "federal",
"issue_date_min": None,
"issue_date_max": None,
"last_traded_min": None,
"coupon_min": 3,
"coupon_max": None,
"currency": None,
"issued_amount_min": None,
"issued_amount_max": None,
"maturity_date_min": None,
"maturity_date_max": None,
"isin": None,
"lei": None,
"country": None,
"use_cache": False,
}
)
],
)
@pytest.mark.integration
def test_fixedincome_corporate_bond_prices(params, obb):
params = {p: v for p, v in params.items() if v}
result = obb.fixedincome.corporate.bond_prices(**params)
assert result
assert isinstance(result, OBBject)
assert len(result.results) > 0

View File

@@ -127,3 +127,14 @@ async def commercial_paper(
and many find it to be a lower-cost alternative to bank loans.
"""
return await OBBject.from_query(Query(**locals()))
@router.command(model="BondPrices")
async def bond_prices(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""Corporate Bond Prices."""
return await OBBject.from_query(Query(**locals()))

View File

@@ -22,8 +22,9 @@ def headers():
@parametrize(
"params",
[
({"index": "dowjones", "provider": "fmp"}),
({"index": "BUKBUS", "provider": "cboe"}),
({"symbol": "dowjones", "provider": "fmp"}),
({"symbol": "^TX60", "provider": "tmx", "use_cache": False}),
({"symbol": "BUKBUS", "provider": "cboe"}),
],
)
@pytest.mark.integration
@@ -284,6 +285,7 @@ def test_index_market(params, headers):
({"provider": "cboe", "use_cache": False}),
({"provider": "fmp"}),
({"provider": "yfinance"}),
({"provider": "tmx", "use_cache": False}),
],
)
@pytest.mark.integration
@@ -316,7 +318,10 @@ def test_index_search(params, headers):
@parametrize(
"params",
[({"provider": "cboe", "region": "us"})],
[
({"provider": "cboe", "region": "us"}),
({"provider": "tmx", "region": "ca", "use_cache": False}),
],
)
@pytest.mark.integration
def test_index_snapshots(params, headers):
@@ -353,3 +358,20 @@ def test_index_sp500_multiples(params, headers):
result = requests.get(url, headers=headers, timeout=20)
assert isinstance(result, requests.Response)
assert result.status_code == 200
@parametrize(
"params",
[
({"provider": "tmx", "symbol": "^TX60", "use_cache": False}),
],
)
@pytest.mark.integration
def test_index_sectors(params, headers):
params = {p: v for p, v in params.items() if v}
query_str = get_querystring(params, [])
url = f"http://0.0.0.0:8000/api/v1/index/sectors?{query_str}"
result = requests.get(url, headers=headers, timeout=10)
assert isinstance(result, requests.Response)
assert result.status_code == 200

View File

@@ -21,8 +21,9 @@ def obb(pytestconfig): # pylint: disable=inconsistent-return-statements
@parametrize(
"params",
[
({"index": "dowjones", "provider": "fmp"}),
({"index": "BUKBUS", "provider": "cboe"}),
({"symbol": "dowjones", "provider": "fmp"}),
({"symbol": "BUKBUS", "provider": "cboe"}),
({"symbol": "^TX60", "provider": "tmx", "use_cache": False}),
],
)
@pytest.mark.integration
@@ -275,6 +276,7 @@ def test_index_market(params, obb):
({"provider": "cboe", "use_cache": False}),
({"provider": "fmp"}),
({"provider": "yfinance"}),
({"provider": "tmx", "use_cache": False}),
],
)
@pytest.mark.integration
@@ -288,7 +290,14 @@ def test_index_available(params, obb):
@parametrize(
"params",
[
({"query": "D", "is_symbol": True, "use_cache": False, "provider": "cboe"}),
(
{
"query": "D",
"is_symbol": True,
"provider": "cboe",
"use_cache": False,
}
),
],
)
@pytest.mark.integration
@@ -303,6 +312,7 @@ def test_index_search(params, obb):
"params",
[
({"region": "us", "provider": "cboe"}),
({"provider": "tmx", "region": "ca", "use_cache": False}),
],
)
@pytest.mark.integration
@@ -334,3 +344,17 @@ def test_index_sp500_multiples(params, obb):
assert result
assert isinstance(result, OBBject)
assert len(result.results) > 0
@parametrize(
"params",
[
({"symbol": "^TX60", "provider": "tmx"}),
],
)
@pytest.mark.integration
def test_index_sectors(params, obb):
result = obb.index.sectors(**params)
assert result
assert isinstance(result, OBBject)
assert len(result.results) > 0

View File

@@ -123,3 +123,20 @@ async def sp500_multiples(
) -> OBBject:
"""Historical S&P 500 multiples and Shiller PE ratios."""
return await OBBject.from_query(Query(**locals()))
@router.command(
model="IndexSectors",
exclude_auto_examples=True,
examples=[
'obb.index.sectors(symbol="^TX60", provider="tmx").to_df()',
],
)
async def sectors(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""Index Sectors. Sector weighting of an index."""
return await OBBject.from_query(Query(**locals()))

View File

@@ -175,6 +175,14 @@ def test_news_world(params, headers):
"end_date": None,
}
),
(
{
"provider": "tmx",
"symbol": "RBC",
"limit": 20,
"page": 1,
}
),
],
)
@pytest.mark.integration

View File

@@ -160,6 +160,14 @@ def test_news_world(params, obb):
"end_date": None,
}
),
(
{
"provider": "tmx",
"symbol": "RBC",
"limit": 20,
"page": 1,
}
),
],
)
@pytest.mark.integration

View File

@@ -136,6 +136,7 @@
"index_market": "/index/market",
"index_price": "/index/price",
"index_price_historical": "/index/price/historical",
"index_sectors": "/index/sectors",
"news": "/news",
"news_company": "/news/company",
"news_world": "/news/world",

888
openbb_platform/poetry.lock generated vendored

File diff suppressed because it is too large Load Diff

View File

@@ -25,7 +25,7 @@ class CboeIndexConstituentsQueryParams(IndexConstituentsQueryParams):
Gets the current price data for all constituents of the CBOE European Index.
"""
index: CONSTITUENTS_EU = Field(default="BUK100P")
symbol: CONSTITUENTS_EU = Field(default="BUK100P")
class CboeIndexConstituentsData(IndexConstituentsData):
@@ -109,7 +109,7 @@ class CboeIndexConstituentsFetcher(
url = (
"https://cdn.cboe.com/api/global/european_indices"
f"/constituent_quotes/{query.index}.json"
f"/constituent_quotes/{query.symbol}.json"
)
data = await amake_request(url)
return data.get("data")

View File

@@ -53,7 +53,7 @@ def test_cboe_index_historical_fetcher(credentials=test_credentials):
@pytest.mark.record_http
def test_cboe_index_constituents_fetcher(credentials=test_credentials):
params = {"symbol": "BUKBUS"}
params = {"symbol": "BUK100P"}
fetcher = CboeIndexConstituentsFetcher()
result = fetcher.test(params, credentials)

View File

@@ -24,7 +24,7 @@ class FMPIndexConstituentsQueryParams(IndexConstituentsQueryParams):
https://site.financialmodelingprep.com/developer/docs/list-of-nasdaq-companies-api/
"""
index: Literal["dowjones", "sp500", "nasdaq"] = Field(
symbol: Literal["dowjones", "sp500", "nasdaq"] = Field(
default="dowjones",
)
@@ -104,7 +104,7 @@ class FMPIndexConstituentsFetcher(
api_key = credentials.get("fmp_api_key") if credentials else ""
base_url = "https://financialmodelingprep.com/api/v3"
url = f"{base_url}/{query.index}_constituent/?apikey={api_key}"
url = f"{base_url}/{query.symbol}_constituent/?apikey={api_key}"
return await get_data_many(url, **kwargs)

View File

@@ -1,5 +1,6 @@
"""US Government Treasury Auctions Model."""
# pylint: disable=unused-argument
from typing import Any, Dict, List, Optional
import pandas as pd
@@ -69,7 +70,9 @@ class GovernmentUSTreasuryAuctionsFetcher(
if r.status_code != 200:
raise RuntimeError(r.status_code)
data = pd.DataFrame(r.json())
results = data.replace("", None).convert_dtypes().to_dict("records")
results = (
data.fillna("N/A").replace("", None).replace("N/A", None).to_dict("records")
)
return results

View File

@@ -1,5 +1,6 @@
"""US Government Treasury Prices"""
# pylint: disable=unused-argument
import asyncio
from datetime import datetime, timedelta
from io import StringIO
@@ -8,15 +9,15 @@ from typing import Any, Dict, List, Literal, Optional
import requests
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.treasury_prices import (
USTreasuryPricesData,
USTreasuryPricesQueryParams,
TreasuryPricesData,
TreasuryPricesQueryParams,
)
from openbb_government_us.utils.helpers import get_random_agent
from pandas import read_csv, to_datetime
from pydantic import Field
class GovernmentUSTreasuryPricesQueryParams(USTreasuryPricesQueryParams):
class GovernmentUSTreasuryPricesQueryParams(TreasuryPricesQueryParams):
"""US Government Treasury Prices Query."""
cusip: Optional[str] = Field(description="Filter by CUSIP.", default=None)
@@ -26,7 +27,7 @@ class GovernmentUSTreasuryPricesQueryParams(USTreasuryPricesQueryParams):
)
class GovernmentUSTreasuryPricesData(USTreasuryPricesData):
class GovernmentUSTreasuryPricesData(TreasuryPricesData):
"""US Government Treasury Prices Data."""
@@ -135,7 +136,7 @@ class GovernmentUSTreasuryPricesFetcher(
)
except Exception as e:
raise RuntimeError("No data was returned: " + str(e)) from e
raise RuntimeError(e) from e
if query.security_type is not None:
results = results[

52
openbb_platform/providers/tmx/README.md vendored Normal file
View File

@@ -0,0 +1,52 @@
# openbb-tmx: A Community Data Provider Extension
`openbb-tmx` is an unofficial, community, data provider extension for the OpenBB Platform.
Install with `pip install openbb-tmx`, or from the local directory, `pip install -e .`
## Command Coverage
- .derivatives.options.chains
- Historical EOD chains data available from 2009.
- .equity.calendar.earnings
- .equity.estimates.consensus
- .equity.discovery.gainers
- Includes a 'category' parameter for the type of 'best performer'.
- .equity.fundamental.dividends
- .equity.fundamental.filings
- .equity.ownership.insider_trading
- Does not use the Standard Model because the data returned are total shares traded over the previous 3,6,and 12 months.
- .equity.price.quote
- .equity.price.historical
- Daily, weekly, monthly, and intraday - with valid intervals being any X number of minutes.
- Weekly and monthly intervals are for the period beginning.
- Historical intraday data begins April 14, 2022.
- Split-adjusted, split and dividend adjusted, as well as unadjusted prices are available only for daily intervals. Other intervals are split-adjusted.
- .equity.search
- .equity.profile
- .etf.search
- .etf.info
- .etf.sectors
- .etf.countries
- .etf.holdings
- Top 10 holdings only.
- .fixedincome.corporate.bond_prices
- .fixedincome.government.treasury_prices
- .index.constituents
- Full constituents with weights and notional values.
- .index.snapshots
- Regions of: ["ca", "us"]
- .index.available
- Includes URLs to methedology and factsheet documents.
- .index.sectors
- .news.company
## Symbology
No exchange suffix is required to handle Canadian listings. The extension accepts `.TO` and `.TSX` as well as no suffix. Additionally, a composite ticker symbol can be entered. For example, `AAPL` trades as a Canadian Depositary Receipt, under the symbol `AAPL:AQN`, on the NEO Exchange. The US listing is also found as `AAPL:US`. Some US and select European data is available from some functions, like `quote` and `historical`.
Indices all begin with `^`.
## Caching
This extension uses `aiohttp-client-cache` with a SQL backend to cache symbol directories, ETF, and index data. It can be bypassed with the parameter, `use_cache=False`. ETF and index data is gathered from a single JSON file which is updated daily by the exchange. The HTTP request is cached for one day.

View File

@@ -0,0 +1,69 @@
"""TMX Provider Module."""
from openbb_core.provider.abstract.provider import Provider
from openbb_tmx.models.available_indices import TmxAvailableIndicesFetcher
from openbb_tmx.models.bond_prices import TmxBondPricesFetcher
from openbb_tmx.models.calendar_earnings import TmxCalendarEarningsFetcher
from openbb_tmx.models.company_filings import TmxCompanyFilingsFetcher
from openbb_tmx.models.company_news import TmxCompanyNewsFetcher
from openbb_tmx.models.equity_historical import TmxEquityHistoricalFetcher
from openbb_tmx.models.equity_profile import TmxEquityProfileFetcher
from openbb_tmx.models.equity_quote import TmxEquityQuoteFetcher
from openbb_tmx.models.equity_search import TmxEquitySearchFetcher
from openbb_tmx.models.etf_countries import TmxEtfCountriesFetcher
from openbb_tmx.models.etf_holdings import TmxEtfHoldingsFetcher
from openbb_tmx.models.etf_info import TmxEtfInfoFetcher
from openbb_tmx.models.etf_search import TmxEtfSearchFetcher
from openbb_tmx.models.etf_sectors import TmxEtfSectorsFetcher
from openbb_tmx.models.gainers import TmxGainersFetcher
from openbb_tmx.models.historical_dividends import TmxHistoricalDividendsFetcher
from openbb_tmx.models.index_constituents import TmxIndexConstituentsFetcher
from openbb_tmx.models.index_sectors import TmxIndexSectorsFetcher
from openbb_tmx.models.index_snapshots import TmxIndexSnapshotsFetcher
from openbb_tmx.models.insider_trading import TmxInsiderTradingFetcher
from openbb_tmx.models.options_chains import TmxOptionsChainsFetcher
from openbb_tmx.models.price_target_consensus import TmxPriceTargetConsensusFetcher
from openbb_tmx.models.treasury_prices import TmxTreasuryPricesFetcher
tmx_provider = Provider(
name="tmx",
website="https://www.tmx.com/",
description="""Unofficial TMX Data Provider Extension
TMX Group Companies
- Toronto Stock Exchange
- TSX Venture Exchange
- TSX Trust
- Montréal Exchange
- TSX Alpha Exchange
- Shorcan
- CDCC
- CDS
- TMX Datalinx
- Trayport
""",
fetcher_dict={
"AvailableIndices": TmxAvailableIndicesFetcher,
"BondPrices": TmxBondPricesFetcher,
"CalendarEarnings": TmxCalendarEarningsFetcher,
"CompanyFilings": TmxCompanyFilingsFetcher,
"CompanyNews": TmxCompanyNewsFetcher,
"EquityHistorical": TmxEquityHistoricalFetcher,
"EquityInfo": TmxEquityProfileFetcher,
"EquityQuote": TmxEquityQuoteFetcher,
"EquitySearch": TmxEquitySearchFetcher,
"EtfSearch": TmxEtfSearchFetcher,
"EtfHoldings": TmxEtfHoldingsFetcher,
"EtfSectors": TmxEtfSectorsFetcher,
"EtfCountries": TmxEtfCountriesFetcher,
"EtfInfo": TmxEtfInfoFetcher,
"EquityGainers": TmxGainersFetcher,
"HistoricalDividends": TmxHistoricalDividendsFetcher,
"IndexConstituents": TmxIndexConstituentsFetcher,
"IndexSectors": TmxIndexSectorsFetcher,
"IndexSnapshots": TmxIndexSnapshotsFetcher,
"InsiderTrading": TmxInsiderTradingFetcher,
"OptionsChains": TmxOptionsChainsFetcher,
"PriceTargetConsensus": TmxPriceTargetConsensusFetcher,
"TreasuryPrices": TmxTreasuryPricesFetcher,
},
)

View File

@@ -0,0 +1,131 @@
"""Available Indices fetcher for TMX"""
# pylint: disable=unused-argument
import re
from typing import Any, Dict, List, Optional
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.available_indices import (
AvailableIndicesData,
AvailableIndicesQueryParams,
)
from openbb_core.provider.utils.errors import EmptyDataError
from openbb_tmx.utils.helpers import get_data_from_url, tmx_indices_backend
from pydantic import Field
class TmxAvailableIndicesQueryParams(AvailableIndicesQueryParams):
"""TMX Available Indices Query Params."""
use_cache: bool = Field(
default=True,
description="Whether to use a cached request."
+ " Index data is from a single JSON file, updated each day after close."
+ " It is cached for one day. To bypass, set to False.",
)
class TmxAvailableIndicesData(AvailableIndicesData):
"""TMX Available Indices Data."""
symbol: str = Field(description="The ticker symbol of the index.")
class TmxAvailableIndicesFetcher(
Fetcher[
TmxAvailableIndicesQueryParams,
List[TmxAvailableIndicesData],
]
):
"""Transform the query, extract and transform the data from the TMX endpoints."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> TmxAvailableIndicesQueryParams:
"""Transform the query params."""
return TmxAvailableIndicesQueryParams(**params)
@staticmethod
async def aextract_data(
query: TmxAvailableIndicesQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> Dict:
"""Return the raw data from the TMX endpoint."""
url = "https://tmxinfoservices.com/files/indices/sptsx-indices.json"
data = await get_data_from_url(
url,
use_cache=query.use_cache,
backend=tmx_indices_backend,
)
return data
@staticmethod
def transform_data(
query: TmxAvailableIndicesQueryParams,
data: Dict,
**kwargs: Any,
) -> List[TmxAvailableIndicesData]:
"""Transform the data to the standard format."""
data = data.copy()
if data == {}:
raise EmptyDataError
# Extract the category for each index.
symbols = {}
for category, symbol_list in data["groups"].items():
for symbol in symbol_list:
if symbol not in symbols:
symbols[symbol] = category
else:
symbols[symbol].append(category)
category = {"category": symbols} # noqa: PLW2901
# Extract the data for each index and combine with the category.
new_data = []
for symbol in data["indices"]:
overview = data["indices"][symbol].get("overview_en", None)
if overview:
# Remove HTML tags from the overview
overview = re.sub("<.*?>", "", overview)
# Remove additional artifacts from the overview
overview = re.sub("\r|\n|amp;", "", overview)
new_data.append(
{
"symbol": symbol,
"name": data["indices"][symbol].get("name_en", None),
"currency": (
"USD"
if "(USD)" in data["indices"][symbol]["name_en"]
else "CAD"
),
"category": symbols[symbol],
"market_value": (
data["indices"][symbol]["quotedmarketvalue"].get("total", None)
if data["indices"][symbol].get("quotedmarketvalue")
else None
),
"num_constituents": data["indices"][symbol].get(
"nb_constituents", None
),
"overview": (
overview
if data["indices"][symbol].get("overview") != ""
else None
),
"methodology": (
data["indices"][symbol].get("methodology", None)
if data["indices"][symbol].get("methodology") != ""
else None
),
"factsheet": (
data["indices"][symbol].get("factsheet", None)
if data["indices"][symbol].get("factsheet") != ""
else None
),
}
)
return [TmxAvailableIndicesData.model_validate(d) for d in new_data]

View File

@@ -0,0 +1,189 @@
"""TMX Bond Prices Fetcher"""
# pylint: disable=unused-argument
from datetime import (
date as dateType,
datetime,
timedelta,
)
from typing import Any, Dict, List, Optional
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.bond_reference import (
BondReferenceData,
BondReferenceQueryParams,
)
from openbb_tmx.utils.helpers import get_all_bonds
from pandas import DataFrame
from pydantic import Field, field_validator
class TmxBondPricesQueryParams(BondReferenceQueryParams):
"""
TMX Bond Prices Query Params.
Data will be made available by 5:00 EST on T+1
Source: https://bondtradedata.iiroc.ca/#/
"""
__alias_dict__ = {
# "isin": "isins",
}
issue_date_min: Optional[dateType] = Field(
default=None,
description="Filter by the minimum original issue date.",
)
issue_date_max: Optional[dateType] = Field(
default=None,
description="Filter by the maximum original issue date.",
)
last_traded_min: Optional[dateType] = Field(
default=None,
description="Filter by the minimum last trade date.",
)
use_cache: bool = Field(
default=True,
description="All bond data is sourced from a single JSON file that is updated daily."
+ " The file is cached for one day to eliminate downloading more than once."
+ " Caching will significantly speed up subsequent queries. To bypass, set to False.",
)
class TmxBondPricesData(BondReferenceData):
"""TMX Bond Prices Data."""
__alias_dict__ = {
"coupon_rate": "couponRate",
}
ytm: Optional[float] = Field(
default=None,
description="Yield to maturity (YTM) is the rate of return anticipated on a bond"
+ " if it is held until the maturity date. It takes into account"
+ " the current market price, par value, coupon rate and time to maturity. It is assumed that all"
+ " coupons are reinvested at the same rate."
+ " Values are returned as a normalized percent.",
alias="lastYield",
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
price: Optional[float] = Field(
default=None,
description="The last price for the bond.",
alias="lastPrice",
)
highest_price: Optional[float] = Field(
default=None,
description="The highest price for the bond on the last traded date.",
alias="highestPrice",
)
lowest_price: Optional[float] = Field(
default=None,
description="The lowest price for the bond on the last traded date.",
alias="lowestPrice",
)
total_trades: Optional[int] = Field(
default=None,
description="Total number of trades on the last traded date.",
alias="totalTrades",
)
last_traded_date: Optional[dateType] = Field(
default=None,
description="Last traded date of the bond.",
alias="lastTradedDate",
)
maturity_date: Optional[dateType] = Field(
default=None,
description="Maturity date of the bond.",
alias="maturityDate",
)
issue_date: Optional[dateType] = Field(
default=None,
description="Issue date of the bond. This is the date when the bond first accrues interest.",
alias="originalIssueDate",
)
issuer_name: Optional[str] = Field(
default=None,
description="Name of the issuing entity.",
alias="issuer",
)
@field_validator(
"ytm",
"coupon_rate",
mode="before",
check_fields=False,
)
@classmethod
def normalize_percent(cls, v):
"""Return percents as normalized percentage points."""
return float(v) / 100 if v else None
class TmxBondPricesFetcher(
Fetcher[
TmxBondPricesQueryParams,
List[TmxBondPricesData],
]
):
"""Tmx Bond Reference Fetcher."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> TmxBondPricesQueryParams:
"""Transform query params."""
transformed_params = params.copy()
now = datetime.now()
if now.date().weekday() > 4:
now = now - timedelta(now.date().weekday() - 4)
if transformed_params.get("maturity_date_min") is None:
transformed_params["maturity_date_min"] = (
now - timedelta(days=1)
).strftime("%Y-%m-%d")
return TmxBondPricesQueryParams(**transformed_params)
@staticmethod
async def aextract_data(
query: TmxBondPricesQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> DataFrame:
"""Get the raw data containing all bond data."""
bonds = await get_all_bonds(use_cache=query.use_cache)
return bonds
@staticmethod
def transform_data(
query: TmxBondPricesQueryParams,
data: DataFrame,
**kwargs: Any,
) -> List[TmxBondPricesData]:
"""Transform data."""
bonds = data.copy()
results = []
data = data[data["bondType"] == "Corp"]
data = bonds.query(
"bondType == 'Corp'"
"& maturityDate >= @query.maturity_date_min.strftime('%Y-%m-%d')"
).sort_values(by=["maturityDate"])
data.issuer = data.loc[:, "issuer"].str.strip()
if query.maturity_date_max:
data = data.query(
"maturityDate <= @query.maturity_date_max.strftime('%Y-%m-%d')"
)
if query.last_traded_min:
data = data.query(
"lastTradedDate >= @query.last_traded_min.strftime('%Y-%m-%d')"
)
if query.coupon_min:
data = data.query("couponRate >= @query.coupon_min")
if query.coupon_max:
data = data.query("couponRate <= @query.coupon_max")
if query.issuer_name:
data = data.query("issuer.str.contains(@query.issuer_name, case=False)")
if len(data) > 0:
data = data.drop(columns=["bondType", "securityId", "secKey"])
data = data.fillna("N/A").replace("N/A", None)
results = data.to_dict("records")
return [TmxBondPricesData.model_validate(d) for d in results]

View File

@@ -0,0 +1,142 @@
"""TMX Earnings Calendar Model"""
# pylint: disable=unused-argument
import asyncio
import json
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.calendar_earnings import (
CalendarEarningsData,
CalendarEarningsQueryParams,
)
from openbb_tmx.utils import gql
from openbb_tmx.utils.helpers import get_data_from_gql, get_random_agent
from pandas import date_range
from pydantic import Field, field_validator
class TmxCalendarEarningsQueryParams(CalendarEarningsQueryParams):
"""TMX Calendar Earnings Query."""
class TmxCalendarEarningsData(CalendarEarningsData):
"""TMX Calendar Earnings Data."""
__alias_dict__ = {
"eps_actual": "actualEps",
"reporting_time": "announceTime",
"eps_consensus": "estimatedEps",
"eps_surprise": "epsSurpriseDollar",
"surprise_percent": "epsSurprisePercent",
}
name: str = Field(description="The company's name.", alias="companyName")
eps_consensus: Optional[float] = Field(
default=None, description="The consensus estimated EPS in dollars."
)
eps_actual: Optional[float] = Field(
default=None, description="The actual EPS in dollars."
)
eps_surprise: Optional[float] = Field(
default=None, description="The EPS surprise in dollars."
)
surprise_percent: Optional[float] = Field(
default=None,
description="The EPS surprise as a normalized percent.",
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
reporting_time: Optional[str] = Field(
default=None,
description="The time of the report - i.e., before or after market.",
)
@field_validator("surprise_percent", mode="before", check_fields=False)
@classmethod
def percent_validate(cls, v): # pylint: disable=E0213
"""Return the percent as a normalized value."""
return float(v) / 100 if v else None
class TmxCalendarEarningsFetcher(
Fetcher[TmxCalendarEarningsQueryParams, List[TmxCalendarEarningsData]]
):
"""Transform the query, extract and transform the data from the TMX endpoints."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> TmxCalendarEarningsQueryParams:
"""Transform the query."""
transformed_params = params.copy()
if transformed_params.get("start_date") is None:
transformed_params["start_date"] = (
datetime.now().date().strftime("%Y-%m-%d")
)
if transformed_params.get("end_date") is None:
transformed_params["end_date"] = (
(datetime.now() + timedelta(days=5)).date().strftime("%Y-%m-%d")
)
return TmxCalendarEarningsQueryParams(**transformed_params)
@staticmethod
async def aextract_data(
query: TmxCalendarEarningsQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[Dict]:
"""Return the raw data from the TMX endpoint."""
results = []
dates = []
user_agent = get_random_agent()
dates = date_range(query.start_date, end=query.end_date)
async def create_task(date, results):
"""Creates a task for a single date in the range."""
data = []
date = date.strftime("%Y-%m-%d")
payload = gql.get_earnings_date_payload.copy()
payload["variables"]["date"] = date
url = "https://app-money.tmx.com/graphql"
r = await get_data_from_gql(
method="POST",
url=url,
data=json.dumps(payload),
headers={
"Host": "app-money.tmx.com",
"Referer": "https://money.tmx.com/",
"locale": "en",
"Content-Type": "application/json",
"User-Agent": user_agent,
"Accept": "*/*",
},
timeout=3,
)
try:
if (
"data" in r
and r["data"].get("getEnhancedEarningsForDate") is not None
):
data = r["data"].get("getEnhancedEarningsForDate")
data = [{"report_date": date, **d} for d in data]
except Exception as e:
raise RuntimeError(e) from e
if len(data) > 0:
results.extend(data)
return results
tasks = [create_task(date, results) for date in dates if date.weekday() < 5]
await asyncio.gather(*tasks)
return sorted(results, key=lambda x: x["report_date"])
@staticmethod
def transform_data(
query: TmxCalendarEarningsQueryParams,
data: List[Dict],
**kwargs: Any,
) -> List[TmxCalendarEarningsData]:
"""Return the transformed data."""
results = [{k: (None if v == "N/A" else v) for k, v in d.items()} for d in data]
return [TmxCalendarEarningsData.model_validate(d) for d in results]

View File

@@ -0,0 +1,169 @@
"""TMX Company Filings Model"""
# pylint: disable=unused-argument
import asyncio
import json
from datetime import (
date as dateType,
datetime,
timedelta,
)
from typing import Any, Dict, List, Optional
from dateutil import rrule
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.company_filings import (
CompanyFilingsData,
CompanyFilingsQueryParams,
)
from openbb_tmx.utils import gql
from openbb_tmx.utils.helpers import get_data_from_gql, get_random_agent
from pydantic import Field
class TmxCompanyFilingsQueryParams(CompanyFilingsQueryParams):
"""TMX Company Filings Query Parameters."""
start_date: Optional[dateType] = Field(
description="The start date to fetch.",
default=None,
)
end_date: Optional[dateType] = Field(
description="The end date to fetch.",
default=None,
)
class TmxCompanyFilingsData(CompanyFilingsData):
"""TMX Sedar Filings Data."""
__alias_dict__ = {
"filing_date": "filingDate",
"report_type": "name",
"report_url": "urlToPdf",
}
description: str = Field(description="The description of the filing.")
size: str = Field(description="The file size of the PDF document.")
class TmxCompanyFilingsFetcher(
Fetcher[TmxCompanyFilingsQueryParams, List[TmxCompanyFilingsData]]
):
"""Transform the query, extract and transform the data from the TMX endpoints."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> TmxCompanyFilingsQueryParams:
"""Transform the query."""
transformed_params = params.copy()
if transformed_params.get("start_date") is None:
transformed_params["start_date"] = (
datetime.now() - timedelta(weeks=16)
).strftime("%Y-%m-%d")
if transformed_params.get("end_date") is None:
transformed_params["end_date"] = datetime.now().date().strftime("%Y-%m-%d")
transformed_params["symbol"] = (
params.get("symbol", "")
.upper()
.replace("-", ".")
.replace(".TO", "")
.replace(".TSX", "")
)
return TmxCompanyFilingsQueryParams(**transformed_params)
@staticmethod
async def aextract_data(
query: TmxCompanyFilingsQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[Dict]:
"""Return the raw data from the TMX endpoint."""
user_agent = get_random_agent()
results: List[Dict] = []
# Generate a list of dates from start_date to end_date with a frequency of 1 week
dates = list(
rrule.rrule(
rrule.WEEKLY, interval=1, dtstart=query.start_date, until=query.end_date
)
)
# Add end_date to the list if it's not there already
if dates[-1] != query.end_date:
dates.append(query.end_date) # type: ignore
# Create a list of 4-week chunks
chunks = [
(dates[i], dates[i + 1] - timedelta(days=1)) for i in range(len(dates) - 1)
]
# Adjust the end date of the last chunk to be the final end date
chunks[-1] = (chunks[-1][0], query.end_date) # type: ignore
async def create_task(start, end, results):
"""Create tasks from the chunked start/end dates."""
data = []
payload = gql.get_company_filings_payload
payload["variables"]["symbol"] = query.symbol
payload["variables"]["fromDate"] = start.strftime("%Y-%m-%d")
payload["variables"]["toDate"] = end.strftime("%Y-%m-%d")
payload["variables"]["limit"] = 1000
url = "https://app-money.tmx.com/graphql"
async def try_again():
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/{query.symbol}",
"locale": "en",
"Content-Type": "application/json",
"User-Agent": user_agent,
"Accept": "*/*",
},
timeout=10,
)
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/{query.symbol}",
"locale": "en",
"Content-Type": "application/json",
"User-Agent": user_agent,
"Accept": "*/*",
},
timeout=10,
)
except Exception:
data = await try_again()
if isinstance(data, str):
data = await try_again()
if data != [] and data["data"].get("filings") is not None:
results.extend(data["data"]["filings"])
return results
tasks = [create_task(chunk[0], chunk[1], results) for chunk in chunks]
await asyncio.gather(*tasks)
return sorted(results, key=lambda x: x["filingDate"], reverse=True)
@staticmethod
def transform_data(
query: TmxCompanyFilingsQueryParams,
data: List[Dict],
**kwargs: Any,
) -> List[TmxCompanyFilingsData]:
"""Return the transformed data."""
return [TmxCompanyFilingsData.model_validate(d) for d in data]

View File

@@ -0,0 +1,123 @@
"""TMX Stock News model."""
# pylint: disable=unused-argument
import asyncio
import json
from datetime import datetime
from typing import Any, Dict, List, Optional
import pytz
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.company_news import (
CompanyNewsData,
CompanyNewsQueryParams,
)
from openbb_tmx.utils import gql
from openbb_tmx.utils.helpers import get_data_from_gql, get_random_agent
from pydantic import Field, field_validator
class TmxCompanyNewsQueryParams(CompanyNewsQueryParams):
"""TMX Stock News query."""
__json_schema_extra__ = {"symbol": ["multiple_items_allowed"]}
page: Optional[int] = Field(
default=1, description="The page number to start from. Use with limit."
)
class TmxCompanyNewsData(CompanyNewsData):
"""TMX Stock News Data"""
__alias_dict__ = {
"date": "datetime",
"title": "headline",
}
source: Optional[str] = Field(description="Source of the news.", default=None)
@field_validator("date", mode="before", check_fields=False)
@classmethod
def date_validate(cls, v): # pylint: disable=E0213
"""Validate the datetime format."""
dt = datetime.strptime(v, "%Y-%m-%dT%H:%M:%S%z")
return dt.astimezone(pytz.timezone("America/New_York"))
class TmxCompanyNewsFetcher(
Fetcher[TmxCompanyNewsQueryParams, List[TmxCompanyNewsData]],
):
"""TMX Stock News Fetcher."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> TmxCompanyNewsQueryParams:
"""Transform the query."""
return TmxCompanyNewsQueryParams(**params)
@staticmethod
async def aextract_data(
query: TmxCompanyNewsQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[Dict]:
"""Return the raw data from the TMX endpoint."""
user_agent = get_random_agent()
symbols = query.symbol.split(",")
results = []
async def create_task(symbol, results):
"""Makes a POST request to the TMX GraphQL endpoint for a single symbol."""
symbol = (
symbol.upper().replace(".TO", "").replace(".TSX", "").replace("-", ".")
)
payload = gql.get_company_news_events_payload
payload["variables"]["symbol"] = symbol
payload["variables"]["page"] = query.page
payload["variables"]["limit"] = query.limit
payload["variables"]["locale"] = "en"
url = "https://app-money.tmx.com/graphql"
data = {}
response = 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,
)
data = response["data"] if response.get("data") else data
if data.get("news") is not None:
news = data["news"]
for i in range(len(news)): # pylint: disable=C0200
url = f"https://money.tmx.com/quote/{symbol.upper()}/news/{news[i]['newsid']}"
news[i]["url"] = url
# The newsid was used to create the URL, so we drop it.
news[i].pop("newsid", None)
# The summary is a duplicated headline, so we drop it.
news[i].pop("summary", None)
# Add the symbol to the data for multi-ticker support.
news[i]["symbols"] = symbol
results.extend(news)
return results
tasks = [create_task(symbol, results) for symbol in symbols]
await asyncio.gather(*tasks)
return sorted(results, key=lambda d: d["datetime"], reverse=True)
@staticmethod
def transform_data(
query: TmxCompanyNewsQueryParams, data: List[Dict], **kwargs: Any
) -> List[TmxCompanyNewsData]:
"""Return the transformed data."""
return [TmxCompanyNewsData.model_validate(d) for d in data]

View File

@@ -0,0 +1,239 @@
"""TMX Equity Historical Model."""
# pylint: disable=unused-argument
import asyncio
import warnings
from datetime import datetime
from typing import Any, Dict, List, Literal, Optional, Union
import pytz
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.equity_historical import (
EquityHistoricalData,
EquityHistoricalQueryParams,
)
from openbb_core.provider.utils.descriptions import (
QUERY_DESCRIPTIONS,
)
from openbb_core.provider.utils.errors import EmptyDataError
from openbb_tmx.utils.helpers import (
get_daily_price_history,
get_intraday_price_history,
get_weekly_or_monthly_price_history,
)
from pandas import DataFrame, to_datetime
from pydantic import Field, field_validator
_warn = warnings.warn
class TmxEquityHistoricalQueryParams(EquityHistoricalQueryParams):
"""
TMX Equity Historical Query Params.
Ticker symbols are assumed to be Canadian listings when no suffix is provided.
".TO" or ."TSX" are accepted but will automatically be removed.
US tickers are supported via their composite format: "AAPL:US"
Canadian Depositary Receipts (CDRs) are: "AAPL:AQL"
CDRs are the underlying asset for CAD-hedged assets.
source: https://money.tmx.com
"""
__json_schema_extra__ = {"symbol": ["multiple_items_allowed"]}
interval: Union[
Literal["1m", "2m", "5m", "15m", "30m", "60m", "1h", "1d", "1W", "1M"], str, int
] = Field(
description=QUERY_DESCRIPTIONS.get("interval", "")
+ " Or, any integer (entered as a string) representing the number of minutes."
+ " Default is daily data."
+ " There is no extended hours data, and intraday data is limited to after April 12 2022.",
default="day",
)
adjustment: Literal["splits_only", "splits_and_dividends", "unadjusted"] = Field(
description="The adjustment factor to apply. Only valid for daily data.",
default="splits_only",
)
@field_validator("interval", mode="after", check_fields=False)
@classmethod
def validate_interval(cls, v): # pylint: disable=R0911
"""Validate the interval to be valid for the TMX request."""
if v is None or v == "day":
return "day"
if v in ("1M", "1mo", "month"):
return "month"
if "m" in v:
return int(v.replace("m", ""))
if "h" in v:
return int(v.replace("h", "")) * 60
if v == "1d":
return "day"
if v in ("1W", "1w", "week"):
return "week"
if v.isnumeric():
return int(v)
raise ValueError(f"Invalid interval: {v}")
class TmxEquityHistoricalData(EquityHistoricalData):
"""TMX Equity Historical Data."""
__alias_dict__ = {
"date": "datetime",
"open": "openPrice",
"close": "closePrice",
"transactions_value": "tradeValue",
"transactions": "numberOfTrade",
"change_percent": "changePercent",
}
vwap: Optional[float] = Field(
description="Volume weighted average price for the day.", default=None
)
change: Optional[float] = Field(description="Change in price.", default=None)
change_percent: Optional[float] = Field(
description="Change in price, as a normalized percentage.",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
transactions: Optional[int] = Field(
description="Total number of transactions recorded.", default=None
)
transactions_value: Optional[float] = Field(
description="Nominal value of recorded transactions.", default=None
)
@field_validator("date", mode="before", check_fields=False)
@classmethod
def date_validate(cls, v): # pylint: disable=W0221
"""Validate the datetime format."""
try:
dt = datetime.strptime(v, "%Y-%m-%d %H:%M:%S%z")
return dt.astimezone(pytz.timezone("America/New_York"))
except ValueError:
return datetime.strptime(v, "%Y-%m-%d")
class TmxEquityHistoricalFetcher(
Fetcher[TmxEquityHistoricalQueryParams, List[TmxEquityHistoricalData]]
):
"""TMX Equity Historical Fetcher."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> TmxEquityHistoricalQueryParams:
"""Transform the query."""
adjustment = params.get("adjustment")
if (
adjustment is not None
and adjustment != "splits_only"
and params.get("interval") not in ["day", "1d"]
):
_warn("Adjustment parameter is only available for daily data.")
return TmxEquityHistoricalQueryParams(**params)
@staticmethod
async def aextract_data(
query: TmxEquityHistoricalQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[Dict]:
"""Return the raw data from the TMX endpoint."""
results: List[Dict] = []
symbols = query.symbol.split(",")
async def create_task(symbol, results):
"""Makes a POST request to the TMX GraphQL endpoint for a single ticker."""
data: List[Dict] = []
# A different request is used for each type of interval.
if query.interval == "day":
data = await get_daily_price_history(
symbol,
start_date=query.start_date,
end_date=query.end_date,
adjustment=query.adjustment,
)
if query.interval in ("week", "month"):
data = await get_weekly_or_monthly_price_history(
symbol,
start_date=query.start_date,
end_date=query.end_date,
interval=query.interval, # type: ignore
)
if isinstance(query.interval, int):
data = await get_intraday_price_history(
symbol,
interval=query.interval,
start_date=query.start_date,
end_date=query.end_date,
)
if data != []:
# Add the symbol to the data for multi-ticker support.
data = [{**d, "symbol": symbol} for d in data]
results.extend(data)
if data == []:
_warn(f"No data found for {symbol}.")
return results
tasks = [create_task(symbol, results) for symbol in symbols]
await asyncio.gather(*tasks)
return results
@staticmethod
def transform_data(
query: TmxEquityHistoricalQueryParams,
data: List[Dict],
**kwargs: Any,
) -> List[TmxEquityHistoricalData]:
"""Return the transformed data."""
results = DataFrame(data)
if results.empty or len(results) == 0:
raise EmptyDataError()
# Handle the date formatting differences.
results = results.rename(columns={"dateTime": "datetime"})
if query.interval != "day":
results["datetime"] = to_datetime(results["datetime"], utc=True)
if query.interval in ("week", "month"):
results["datetime"] = results["datetime"].dt.strftime("%Y-%m-%d")
else:
results["datetime"] = results["datetime"].dt.strftime(
"%Y-%m-%d %H:%M:%S%z"
)
if query.interval == "day":
results["datetime"] = to_datetime(results["datetime"]).dt.strftime(
"%Y-%m-%d"
)
symbols = query.symbol.split(",")
# If there are multiple symbols, sort the data by datetime and symbol.
if len(symbols) > 1:
results = results.set_index(["datetime", "symbol"]).sort_index()
results = results.reset_index()
# If there is only one symbol, drop the symbol column.
if len(symbols) == 1:
results = results.drop(columns=["symbol"])
# Normalizes the percent change values.
if "changePercent" in results.columns:
results["changePercent"] = results["changePercent"].astype(float) / 100
# For the week beginning 2011-09-12 replace the openPrice NaN with 0 because of 9/11.
if query.interval == "week":
results["open"] = results["open"].fillna(0)
# Convert any NaN values to None.
results = results.fillna(value="N/A").replace("N/A", None)
return [
TmxEquityHistoricalData.model_validate(d)
for d in results.to_dict("records")
]

View File

@@ -0,0 +1,164 @@
"""TMX Equity Profile fetcher"""
# pylint: disable=unused-argument
import asyncio
import json
from typing import Any, Dict, List, Optional
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.equity_info import (
EquityInfoData,
EquityInfoQueryParams,
)
from openbb_tmx.utils import gql
from openbb_tmx.utils.helpers import get_data_from_gql, get_random_agent
from pydantic import Field, model_validator
class TmxEquityProfileQueryParams(EquityInfoQueryParams):
"""TMX Equity Profile query params."""
__json_schema_extra__ = {"symbol": ["multiple_items_allowed"]}
class TmxEquityProfileData(EquityInfoData):
"""TMX Equity Profile Data."""
__alias_dict__ = {
"short_description": "shortDescription",
"long_description": "longDescription",
"company_url": "website",
"business_phone_no": "phoneNumber",
"business_address": "fullAddress",
"stock_exchange": "exchangeCode",
"industry_category": "industry",
"industry_group": "qmdescription",
}
email: Optional[str] = Field(description="The email of the company.", default=None)
issue_type: Optional[str] = Field(
description="The issuance type of the asset.", default=None, alias="issueType"
)
shares_outstanding: Optional[int] = Field(
description="The number of listed shares outstanding.",
default=None,
alias="shareOutStanding",
)
shares_escrow: Optional[int] = Field(
description="The number of shares held in escrow.",
default=None,
alias="sharesESCROW",
)
shares_total: Optional[int] = Field(
description="The total number of shares outstanding from all classes.",
default=None,
alias="totalSharesOutStanding",
)
dividend_frequency: Optional[str] = Field(
description="The dividend frequency.", default=None
)
@model_validator(mode="before")
@classmethod
def validate_empty_strings(cls, values) -> Dict:
"""Validate the query parameters."""
return {k: None if v == "" else v for k, v in values.items()}
class TmxEquityProfileFetcher(
Fetcher[
TmxEquityProfileQueryParams,
List[TmxEquityProfileData],
]
):
"""Transform the query, extract and transform the data from the TMX endpoints."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> TmxEquityProfileQueryParams:
"""Transform the query."""
return TmxEquityProfileQueryParams(**params)
@staticmethod
async def aextract_data(
query: TmxEquityProfileQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[Dict]:
"""Return the raw data from the TMX endpoint."""
symbols = query.symbol.split(",")
# The list where the results will be stored and appended to.
results = []
user_agent = get_random_agent()
url = "https://app-money.tmx.com/graphql"
async def create_task(symbol: str, results) -> None:
"""Makes a POST request to the TMX GraphQL endpoint for a single symbol."""
symbol = (
symbol.upper().replace("-", ".").replace(".TO", "").replace(".TSX", "")
)
payload = gql.stock_info_payload.copy()
payload["variables"]["symbol"] = symbol
data = {}
r = 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 r["data"].get("getQuoteBySymbol"):
data = r["data"]["getQuoteBySymbol"]
results.append(data)
tasks = [create_task(symbol, results) for symbol in symbols]
await asyncio.gather(*tasks)
return results
@staticmethod
def transform_data(
query: TmxEquityProfileQueryParams,
data: List[Dict],
**kwargs: Any,
) -> List[TmxEquityProfileData]:
"""Return the transformed data."""
# Get only the items associated with `equity.profile()`.
items_list = [
"shortDescription",
"longDescription",
"website",
"phoneNumber",
"fullAddress",
"sector",
"qmdescription",
"industry",
"exchangeCode",
"shareOutStanding",
"sharesESCROW",
"totalSharesOutStanding",
"email",
"issueType",
"name",
"symbol",
"dividendFrequency",
"employees",
]
data = [{k: v for k, v in d.items() if k in items_list} for d in data]
# Sort the data by the order of the symbols in the query.
symbols = query.symbol.split(",")
symbol_to_index = {symbol: index for index, symbol in enumerate(symbols)}
data = sorted(data, key=lambda d: symbol_to_index[d["symbol"]])
return [TmxEquityProfileData.model_validate(d) for d in data]

View File

@@ -0,0 +1,360 @@
"""TMX Equity Profile fetcher"""
# pylint: disable=unused-argument
import asyncio
import json
import warnings
from datetime import (
date as dateType,
datetime,
)
from typing import Any, Dict, List, Optional, Union
from numpy import nan
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.equity_quote import (
EquityQuoteData,
EquityQuoteQueryParams,
)
from openbb_core.provider.utils.descriptions import DATA_DESCRIPTIONS
from openbb_tmx.utils import gql
from openbb_tmx.utils.helpers import get_data_from_gql, get_random_agent
from pydantic import Field, field_validator
_warn = warnings.warn
class TmxEquityQuoteQueryParams(EquityQuoteQueryParams):
"""TMX Equity Profile query params."""
__json_schema_extra__ = {"symbol": ["multiple_items_allowed"]}
class TmxEquityQuoteData(EquityQuoteData):
"""TMX Equity Profile Data."""
__alias_dict__ = {
"last_price": "price",
"open": "openPrice",
"high": "dayHigh",
"low": "dayLow",
"change": "priceChange",
"change_percent": "percentChange",
"prev_close": "prevClose",
"stock_exchange": "exchangeCode",
"industry_category": "industry",
"industry_group": "qmdescription",
"exchange": "exchangeCode",
"security_type": "datatype",
}
name: Optional[str] = Field(default=None, description="The name of the asset.")
security_type: Optional[str] = Field(
description="The issuance type of the asset.", default=None
)
exchange: Optional[str] = Field(
default=None,
description="The listing exchange code.",
)
sector: Optional[str] = Field(default=None, description="The sector of the asset.")
industry_category: Optional[str] = Field(
default=None,
description="The industry category of the asset.",
)
industry_group: Optional[str] = Field(
default=None,
description="The industry group of the asset.",
)
last_price: Optional[float] = Field(
default=None, description="The last price of the asset."
)
open: Optional[float] = Field(
default=None,
description=DATA_DESCRIPTIONS.get("open", ""),
)
high: Optional[float] = Field(
default=None,
description=DATA_DESCRIPTIONS.get("high", ""),
)
low: Optional[float] = Field(
default=None,
description=DATA_DESCRIPTIONS.get("low", ""),
)
close: Optional[float] = Field(
default=None,
)
vwap: Optional[float] = Field(
default=None, description=DATA_DESCRIPTIONS.get("vwap", "")
)
volume: Optional[int] = Field(
default=None, description=DATA_DESCRIPTIONS.get("vwap", "")
)
prev_close: Optional[float] = Field(
default=None, description=DATA_DESCRIPTIONS.get("prev_close", "")
)
change: Optional[float] = Field(
default=None,
description="The change in price.",
)
change_percent: Optional[float] = Field(
default=None,
description="The change in price as a normalized percent.",
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
year_high: Optional[float] = Field(
description="Fifty-two week high.", default=None, alias="weeks52high"
)
year_low: Optional[float] = Field(
description="Fifty-two week low.", default=None, alias="weeks52low"
)
ma_21: Optional[float] = Field(
description="Twenty-one day moving average.",
default=None,
alias="day21MovingAvg",
)
ma_50: Optional[float] = Field(
description="Fifty day moving average.", default=None, alias="day50MovingAvg"
)
ma_200: Optional[float] = Field(
description="Two-hundred day moving average.",
default=None,
alias="day200MovingAvg",
)
volume_avg_10d: Optional[int] = Field(
description="Ten day average volume.", default=None, alias="averageVolume10D"
)
volume_avg_30d: Optional[int] = Field(
description="Thirty day average volume.", default=None, alias="averageVolume30D"
)
volume_avg_50d: Optional[int] = Field(
description="Fifty day average volume.", default=None, alias="averageVolume50D"
)
market_cap: Optional[int] = Field(
description="Market capitalization.", default=None, alias="MarketCap"
)
market_cap_all_classes: Optional[int] = Field(
description="Market capitalization of all share classes.",
default=None,
alias="MarketCapAllClasses",
)
div_amount: Optional[float] = Field(
description="The most recent dividend amount.",
default=None,
alias="dividendAmount",
)
div_currency: Optional[str] = Field(
description="The currency the dividend is paid in.",
default=None,
alias="dividendCurrency",
)
div_yield: Optional[float] = Field(
description="The dividend yield as a normalized percentage.",
default=None,
alias="dividendYield",
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
div_freq: Optional[str] = Field(
description="The frequency of dividend payments.",
default=None,
alias="dividendFrequency",
)
div_ex_date: Optional[dateType] = Field(
description="The ex-dividend date.", default=None, alias="exDividendDate"
)
div_pay_date: Optional[dateType] = Field(
description="The next dividend ayment date.",
default=None,
alias="dividendPayDate",
)
div_growth_3y: Optional[Union[float, str]] = Field(
description="The three year dividend growth as a normalized percentage.",
default=None,
alias="dividend3Years",
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
div_growth_5y: Optional[Union[float, str]] = Field(
description="The five year dividend growth as a normalized percentage.",
default=None,
alias="dividend5Years",
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
pe: Optional[Union[float, str]] = Field(
description="The price to earnings ratio.", default=None, alias="peRatio"
)
eps: Optional[Union[float, str]] = Field(
description="The earnings per share.", default=None
)
debt_to_equity: Optional[Union[float, str]] = Field(
description="The debt to equity ratio.", default=None, alias="totalDebtToEquity"
)
price_to_book: Optional[Union[float, str]] = Field(
description="The price to book ratio.", default=None, alias="priceToBook"
)
price_to_cf: Optional[Union[float, str]] = Field(
description="The price to cash flow ratio.",
default=None,
alias="priceToCashFlow",
)
return_on_equity: Optional[Union[float, str]] = Field(
description="The return on equity, as a normalized percentage.",
default=None,
alias="returnOnEquity",
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
return_on_assets: Optional[Union[float, str]] = Field(
description="The return on assets, as a normalized percentage.",
default=None,
alias="returnOnAssets",
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
beta: Optional[Union[float, str]] = Field(
description="The beta relative to the TSX Composite.", default=None
)
alpha: Optional[Union[float, str]] = Field(
description="The alpha relative to the TSX Composite.", default=None
)
shares_outstanding: Optional[int] = Field(
description="The number of listed shares outstanding.",
default=None,
alias="shareOutStanding",
)
shares_escrow: Optional[int] = Field(
description="The number of shares held in escrow.",
default=None,
alias="sharesESCROW",
)
shares_total: Optional[int] = Field(
description="The total number of shares outstanding from all classes.",
default=None,
alias="totalSharesOutStanding",
)
@field_validator(
"div_ex_date",
"div_pay_date",
mode="before",
check_fields=False,
)
@classmethod
def date_validate(cls, v): # pylint: disable=E0213
"""Return the datetime object from the date string"""
if v:
try:
return datetime.strptime(v, "%Y-%m-%d").date()
except ValueError:
return datetime.strptime(v, "%Y-%m-%d %H:%M:%S.%f").date()
return None
@field_validator(
"return_on_equity",
"return_on_assets",
"div_yield",
"div_growth_3y",
"div_growth_5y",
"change_percent",
mode="before",
check_fields=False,
)
@classmethod
def normalize_percent(cls, v):
"""Return percents as normalized percentage points."""
return round(float(v) / 100, 6) if v else None
class TmxEquityQuoteFetcher(
Fetcher[
TmxEquityQuoteQueryParams,
List[TmxEquityQuoteData],
]
):
"""Transform the query, extract and transform the data from the TMX endpoints."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> TmxEquityQuoteQueryParams:
"""Transform the query."""
return TmxEquityQuoteQueryParams(**params)
@staticmethod
async def aextract_data(
query: TmxEquityQuoteQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[Dict]:
"""Return the raw data from the TMX endpoint."""
symbols = query.symbol.split(",")
# The list where the results will be stored and appended to.
results = []
user_agent = get_random_agent()
url = "https://app-money.tmx.com/graphql"
async def create_task(symbol: str, results) -> None:
"""Makes a POST request to the TMX GraphQL endpoint for a single symbol."""
symbol = (
symbol.upper().replace("-", ".").replace(".TO", "").replace(".TSX", "")
)
payload = gql.stock_info_payload.copy()
payload["variables"]["symbol"] = symbol
data = {}
r = 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 r["data"].get("getQuoteBySymbol"):
data = r["data"]["getQuoteBySymbol"]
results.append(data)
else:
_warn(f"Could not get data for {symbol}.")
tasks = [create_task(symbol, results) for symbol in symbols]
await asyncio.gather(*tasks)
return results
@staticmethod
def transform_data(
query: TmxEquityQuoteQueryParams,
data: List[Dict],
**kwargs: Any,
) -> List[TmxEquityQuoteData]:
"""Return the transformed data."""
# Remove the items associated with `equity.profile()`.
items_list = [
"shortDescription",
"longDescription",
"website",
"phoneNumber",
"fullAddress",
"email",
"issueType",
"exchangeName",
"employees",
"exShortName",
]
data = [{k: v for k, v in d.items() if k not in items_list} for d in data]
# Replace all NaN values with None.
for d in data:
for k, v in d.items():
if v in (nan, 0, ""):
d[k] = None
# Sort the data by the order of the symbols in the query.
symbols = query.symbol.split(",")
symbol_to_index = {symbol: index for index, symbol in enumerate(symbols)}
data = sorted(data, key=lambda d: symbol_to_index[d["symbol"]])
return [TmxEquityQuoteData.model_validate(d) for d in data]

View File

@@ -0,0 +1,71 @@
"""TMX Equity Search fetcher."""
# pylint: disable=unused-argument
from typing import Any, Dict, List, Optional
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.equity_search import (
EquitySearchData,
EquitySearchQueryParams,
)
from openbb_tmx.utils.helpers import get_all_tmx_companies
from pandas import DataFrame
from pydantic import Field
class TmxEquitySearchQueryParams(EquitySearchQueryParams):
"""TMX Equity Search query.
Source: https://www.tmx.com/
"""
use_cache: bool = Field(
default=True,
description="Whether to use a cached request. The list of companies is cached for two days.",
)
class TmxEquitySearchData(EquitySearchData):
"""TMX Equity Search Data."""
class TmxEquitySearchFetcher(
Fetcher[
TmxEquitySearchQueryParams,
List[TmxEquitySearchData],
]
):
"""Transform the query, extract and transform the data from the TMX endpoints."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> TmxEquitySearchQueryParams:
"""Transform the query."""
return TmxEquitySearchQueryParams(**params)
@staticmethod
async def aextract_data(
query: TmxEquitySearchQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[Dict]:
"""Return the raw data from the TMX endpoint."""
companies = await get_all_tmx_companies(use_cache=query.use_cache)
results = DataFrame(index=companies, data=companies.values(), columns=["name"])
results = results.reset_index().rename(columns={"index": "symbol"})
if query:
results = results[
results["name"].str.contains(query.query, case=False)
| results["symbol"].str.contains(query.query, case=False)
]
return results.reset_index(drop=True).astype(str).to_dict("records")
@staticmethod
def transform_data(
query: TmxEquitySearchQueryParams, data: List[Dict], **kwargs: Any
) -> List[TmxEquitySearchData]:
"""Transform the data to the standard format."""
return [TmxEquitySearchData.model_validate(d) for d in data]

View File

@@ -0,0 +1,109 @@
"""TMX ETF Countries fetcher."""
# pylint: disable=unused-argument
import warnings
from typing import Any, Dict, List, Optional
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.etf_countries import (
EtfCountriesData,
EtfCountriesQueryParams,
)
from openbb_tmx.utils.helpers import get_all_etfs
from pandas import DataFrame
from pydantic import Field
_warn = warnings.warn
class TmxEtfCountriesQueryParams(EtfCountriesQueryParams):
"""TMX ETF Countries Query Params"""
__json_schema_extra__ = {"symbol": ["multiple_items_allowed"]}
use_cache: bool = Field(
default=True,
description="Whether to use a cached request. All ETF data comes from a single JSON file that is updated daily."
+ " To bypass, set to False. If True, the data will be cached for 4 hours.",
)
class TmxEtfCountriesData(EtfCountriesData):
"""TMX ETF Countries Data."""
class TmxEtfCountriesFetcher(
Fetcher[
TmxEtfCountriesQueryParams,
List[TmxEtfCountriesData],
]
):
"""Transform the query, extract and transform the data from the TMX endpoints."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> TmxEtfCountriesQueryParams:
"""Transform the query."""
return TmxEtfCountriesQueryParams(**params)
@staticmethod
async def aextract_data(
query: TmxEtfCountriesQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[Dict]:
"""Return the raw data from the TMX endpoint."""
symbols = (
query.symbol.split(",") if "," in query.symbol else [query.symbol.upper()]
)
_data = DataFrame(await get_all_etfs(use_cache=query.use_cache))
results = {}
for symbol in symbols:
data = {}
if ".TO" in symbol:
symbol = symbol.replace(".TO", "") # noqa
_target = _data[_data["symbol"] == symbol]["regions"]
target = DataFrame()
if len(_target) > 0:
target = DataFrame.from_records(_target.iloc[0]).rename(
columns={"name": "country", "percent": "weight"}
)
if not target.empty:
target = target.set_index("country")
for i in target.index:
data.update({i: target.loc[i]["weight"]})
if data:
results.update({symbol: data})
else:
_warn(f"No data found for {symbol}")
output = (
DataFrame(results)
.transpose()
.reset_index()
.rename(columns={"index": "symbol"})
).transpose()
output.columns = output.loc["symbol"].to_list()
output.drop("symbol", axis=0, inplace=True)
return (
output.reset_index().rename(columns={"index": "country"}).to_dict("records")
)
@staticmethod
def transform_data(
query: TmxEtfCountriesQueryParams, data: List[Dict], **kwargs: Any
) -> List[TmxEtfCountriesData]:
"""Return the transformed data."""
output = DataFrame(data)
for col in output.columns.to_list():
if col != "country":
output[col] = output[col].astype(float) / 100
output = output.fillna(value="N/A").replace("N/A", None)
output["country"] = (
output["country"].astype(str).str.lower().str.replace(" ", "_")
)
return [
TmxEtfCountriesData.model_validate(d) for d in output.to_dict("records")
]

View File

@@ -0,0 +1,142 @@
"""TMX ETF Holdings fetcher."""
# pylint: disable=unused-argument
from typing import Any, Dict, List, Optional, Union
import pandas as pd
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.etf_holdings import (
EtfHoldingsData,
EtfHoldingsQueryParams,
)
from openbb_tmx.utils.helpers import get_all_etfs
from pydantic import Field, field_validator
class TmxEtfHoldingsQueryParams(EtfHoldingsQueryParams):
"""TMX ETF Holdings query.
Source: https://www.tmx.com/
"""
use_cache: bool = Field(
default=True,
description="Whether to use a cached request. All ETF data comes from a single JSON file that is updated daily."
+ " To bypass, set to False. If True, the data will be cached for 4 hours.",
)
class TmxEtfHoldingsData(EtfHoldingsData):
"""TMX ETF Holdings Data."""
symbol: Optional[str] = Field(
description="The ticker symbol of the asset.", default=None
)
name: Optional[str] = Field(description="The name of the asset.", default=None)
weight: Optional[float] = Field(
description="The weight of the asset in the portfolio, as a normalized percentage.",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
shares: Optional[Union[int, str]] = Field(
description="The value of the assets under management.",
alias="number_of_shares",
default=None,
)
market_value: Optional[Union[float, str]] = Field(
description="The market value of the holding.", default=None
)
currency: Optional[str] = Field(description="The currency of the holding.")
share_percentage: Optional[float] = Field(
description="The share percentage of the holding, as a normalized percentage.",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
share_change: Optional[Union[float, str]] = Field(
description="The change in shares of the holding.", default=None
)
country: Optional[str] = Field(
description="The country of the holding.", default=None
)
exchange: Optional[str] = Field(
description="The exchange code of the holding.", default=None
)
type_id: Optional[str] = Field(
description="The holding type ID of the asset.", default=None
)
fund_id: Optional[str] = Field(
description="The fund ID of the asset.", default=None
)
@field_validator("share_percentage", "weight", mode="before", check_fields=False)
@classmethod
def normalize_percent(cls, v):
"""Return percents as normalized percentage points."""
return float(v) / 100 if v else None
class TmxEtfHoldingsFetcher(
Fetcher[
TmxEtfHoldingsQueryParams,
List[TmxEtfHoldingsData],
]
):
"""Transform the query, extract and transform the data from the TMX endpoints."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> TmxEtfHoldingsQueryParams:
"""Transform the query."""
params["symbol"] = (
params["symbol"].replace(".TO", "").replace(".TSX", "").replace("-", ".")
)
return TmxEtfHoldingsQueryParams(**params)
@staticmethod
async def aextract_data(
query: TmxEtfHoldingsQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[Dict]:
"""Return the raw data from the TMX endpoint."""
query.symbol = query.symbol.upper()
results = []
etf = pd.DataFrame()
etfs = pd.DataFrame(await get_all_etfs(use_cache=query.use_cache))
etf = etfs[etfs["symbol"] == query.symbol]
if len(etf) == 1:
top_holdings = pd.DataFrame(etf["holdings_top10"].iloc[0])
top_holdings = top_holdings.dropna(axis=1, how="all")
_columns = {
"numberofshares": "number_of_shares",
"symbol": "symbol",
"country": "country",
"fundid": "fund_id",
"excode": "exchange",
"securityname": "name",
"currency": "currency",
"marketvalue": "market_value",
"detailholdingtypeid": "type_id",
"weighting": "weight",
"sharepercentage": "share_percentage",
"sharechange": "share_change",
"shareChange": "share_change",
}
top_holdings.rename(columns=_columns, inplace=True)
results = (
top_holdings.fillna("N/A")
.replace("NA", None)
.replace("N/A", None)
.to_dict("records")
)
return results
@staticmethod
def transform_data(
query: TmxEtfHoldingsQueryParams,
data: List[Dict],
**kwargs: Any,
) -> List[TmxEtfHoldingsData]:
"""Transform the data to the standard format."""
return [TmxEtfHoldingsData.model_validate(d) for d in data]

View File

@@ -0,0 +1,233 @@
"""TMX ETF Info fetcher."""
# pylint: disable=unused-argument
from typing import Any, Dict, List, Optional
import pandas as pd
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.etf_info import (
EtfInfoData,
EtfInfoQueryParams,
)
from openbb_tmx.utils.helpers import get_all_etfs
from pydantic import Field, field_validator
class TmxEtfInfoQueryParams(EtfInfoQueryParams):
"""TMX ETF Info Query Params"""
use_cache: bool = Field(
default=True,
description="Whether to use a cached request. All ETF data comes from a single JSON file that is updated daily."
+ " To bypass, set to False. If True, the data will be cached for 4 hours.",
)
class TmxEtfInfoData(EtfInfoData):
"""TMX ETF Info Data."""
issuer: Optional[str] = Field(
description="The issuer of the ETF.", alias="fund_family", default=None
)
investment_style: Optional[str] = Field(
description="The investment style of the ETF.", default=None
)
esg: Optional[bool] = Field(
description="Whether the ETF qualifies as an ESG fund.", default=None
)
currency: Optional[str] = Field(description="The currency of the ETF.")
unit_price: Optional[float] = Field(
description="The unit price of the ETF.", default=None
)
close: Optional[float] = Field(description="The closing price of the ETF.")
prev_close: Optional[float] = Field(
description="The previous closing price of the ETF.", default=None
)
return_1m: Optional[float] = Field(
description="The one-month return of the ETF, as a normalized percent",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
return_3m: Optional[float] = Field(
description="The three-month return of the ETF, as a normalized percent.",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
return_6m: Optional[float] = Field(
description="The six-month return of the ETF, as a normalized percent.",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
return_ytd: Optional[float] = Field(
description="The year-to-date return of the ETF, as a normalized percent.",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
return_1y: Optional[float] = Field(
description="The one-year return of the ETF, as a normalized percent.",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
return_3y: Optional[float] = Field(
description="The three-year return of the ETF, as a normalized percent.",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
return_5y: Optional[float] = Field(
description="The five-year return of the ETF, as a normalized percent.",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
return_10y: Optional[float] = Field(
description="The ten-year return of the ETF, as a normalized percent.",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
return_from_inception: Optional[float] = Field(
description="The return from inception of the ETF, as a normalized percent.",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
avg_volume: Optional[int] = Field(
description="The average daily volume of the ETF.",
alias="volume_avg_daily",
default=None,
)
avg_volume_30d: Optional[int] = Field(
description="The 30-day average volume of the ETF.",
alias="volume_avg_30d",
default=None,
)
aum: Optional[float] = Field(description="The AUM of the ETF.", default=None)
pe_ratio: Optional[float] = Field(
description="The price-to-earnings ratio of the ETF.", default=None
)
pb_ratio: Optional[float] = Field(
description="The price-to-book ratio of the ETF.", default=None
)
management_fee: Optional[float] = Field(
description="The management fee of the ETF, as a normalized percent.",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
mer: Optional[float] = Field(
description="The management expense ratio of the ETF, as a normalized percent.",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
distribution_yield: Optional[float] = Field(
description="The distribution yield of the ETF, as a normalized percent.",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
dividend_frequency: Optional[str] = Field(
description="The dividend payment frequency of the ETF.", default=None
)
website: Optional[str] = Field(description="The website of the ETF.", default=None)
description: Optional[str] = Field(
description="The description of the ETF.",
alias="investment_objectives",
default=None,
)
@field_validator(
"distribution_yield",
"return_1m",
"return_3m",
"return_6m",
"return_ytd",
"return_1y",
"return_3y",
"return_5y",
"return_10y",
"return_from_inception",
"mer",
"management_fee",
mode="before",
check_fields=False,
)
@classmethod
def normalize_percent(cls, v):
"""Return percents as normalized percentage points."""
return float(v) / 100 if v else None
class TmxEtfInfoFetcher(
Fetcher[
TmxEtfInfoQueryParams,
List[TmxEtfInfoData],
]
):
"""Transform the query, extract and transform the data from the TMX endpoints."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> TmxEtfInfoQueryParams:
"""Transform the query."""
return TmxEtfInfoQueryParams(**params)
@staticmethod
async def aextract_data(
query: TmxEtfInfoQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[Dict]:
"""Return the raw data from the TMX endpoint."""
results = []
symbols = (
query.symbol.split(",") if "," in query.symbol else [query.symbol.upper()]
)
_data = pd.DataFrame(await get_all_etfs(use_cache=query.use_cache))
COLUMNS = [
"symbol",
"inception_date",
"name",
"fund_family",
"investment_style",
"esg",
"currency",
"unit_price",
"close",
"prev_close",
"return_1m",
"return_3m",
"return_6m",
"return_ytd",
"return_1y",
"return_3y",
"return_5y",
"return_from_inception",
"volume_avg_daily",
"volume_avg_30d",
"aum",
"pe_ratio",
"pb_ratio",
"management_fee",
"mer",
"distribution_yield",
"dividend_frequency",
"website",
"investment_objectives",
]
for symbol in symbols:
result = {}
target = pd.DataFrame()
symbol = ( # noqa: PLW2901
symbol.replace(".TO", "").replace(".TSX", "").replace("-", ".")
)
target = _data[_data["symbol"] == symbol][COLUMNS]
target = target.fillna("N/A").replace("N/A", None)
if len(target) > 0:
result = target.reset_index(drop=True).transpose().to_dict()[0]
results.append(result)
return results
@staticmethod
def transform_data(
query: TmxEtfInfoQueryParams,
data: List[Dict],
**kwargs: Any,
) -> List[TmxEtfInfoData]:
"""Return the transformed data."""
return [TmxEtfInfoData.model_validate(d) for d in data]

View File

@@ -0,0 +1,266 @@
"""TMX ETF Search fetcher."""
# pylint: disable=unused-argument
from typing import Any, Dict, List, Literal, Optional
import pandas as pd
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.etf_search import (
EtfSearchData,
EtfSearchQueryParams,
)
from openbb_tmx.utils.helpers import get_all_etfs
from pydantic import Field, field_validator
class TmxEtfSearchQueryParams(EtfSearchQueryParams):
"""TMX ETF Search query.
Source: https://www.tmx.com/
"""
div_freq: Optional[Literal["monthly", "annually", "quarterly"]] = Field(
description="The dividend payment frequency.", default=None
)
sort_by: Optional[
Literal[
"nav",
"return_1m",
"return_3m",
"return_6m",
"return_1y",
"return_3y",
"return_ytd",
"beta_1y",
"volume_avg_daily",
"management_fee",
"distribution_yield",
"pb_ratio",
"pe_ratio",
]
] = Field(description="The column to sort by.", default=None)
use_cache: bool = Field(
default=True,
description="Whether to use a cached request. All ETF data comes from a single JSON file that is updated daily."
+ " To bypass, set to False. If True, the data will be cached for 4 hours.",
)
class TmxEtfSearchData(EtfSearchData):
"""TMX ETF Search Data."""
short_name: Optional[str] = Field(
description="The short name of the ETF.", default=None
)
inception_date: Optional[str] = Field(
description="The inception date of the ETF.", default=None
)
issuer: Optional[str] = Field(
description="The issuer of the ETF.", alias="fund_family", default=None
)
investment_style: Optional[str] = Field(
description="The investment style of the ETF.", default=None
)
esg: Optional[bool] = Field(
description="Whether the ETF qualifies as an ESG fund.", default=None
)
currency: Optional[str] = Field(description="The currency of the ETF.")
unit_price: Optional[float] = Field(
description="The unit price of the ETF.", default=None
)
close: Optional[float] = Field(description="The closing price of the ETF.")
prev_close: Optional[float] = Field(
description="The previous closing price of the ETF.", default=None
)
return_1m: Optional[float] = Field(
description="The one-month return of the ETF, as a normalized percent.",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
return_3m: Optional[float] = Field(
description="The three-month return of the ETF, as a normalized percent.",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
return_6m: Optional[float] = Field(
description="The six-month return of the ETF, as a normalized percent.",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
return_ytd: Optional[float] = Field(
description="The year-to-date return of the ETF, as a normalized percent.",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
return_1y: Optional[float] = Field(
description="The one-year return of the ETF, as a normalized percent.",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
beta_1y: Optional[float] = Field(
description="The one-year beta of the ETF, as a normalized percent.",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
return_3y: Optional[float] = Field(
description="The three-year return of the ETF, as a normalized percent.",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
beta_3y: Optional[float] = Field(
description="The three-year beta of the ETF, as a normalized percent.",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
return_5y: Optional[float] = Field(
description="The five-year return of the ETF, as a normalized percent.",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
beta_5y: Optional[float] = Field(
description="The five-year beta of the ETF, as a normalized percent.",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
return_10y: Optional[float] = Field(
description="The ten-year return of the ETF, as a normalized percent.",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
beta_10y: Optional[float] = Field(
description="The ten-year beta of the ETF.", default=None
)
beta_15y: Optional[float] = Field(
description="The fifteen-year beta of the ETF.", default=None
)
return_from_inception: Optional[float] = Field(
description="The return from inception of the ETF, as a normalized percent.",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
avg_volume: Optional[int] = Field(
description="The average daily volume of the ETF.",
alias="volume_avg_daily",
default=None,
)
avg_volume_30d: Optional[int] = Field(
description="The 30-day average volume of the ETF.",
alias="volume_avg_30d",
default=None,
)
aum: Optional[float] = Field(description="The AUM of the ETF.", default=None)
pe_ratio: Optional[float] = Field(
description="The price-to-earnings ratio of the ETF.", default=None
)
pb_ratio: Optional[float] = Field(
description="The price-to-book ratio of the ETF.", default=None
)
management_fee: Optional[float] = Field(
description="The management fee of the ETF, as a normalized percent.",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
mer: Optional[float] = Field(
description="The management expense ratio of the ETF, as a normalized percent.",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
distribution_yield: Optional[float] = Field(
description="The distribution yield of the ETF, as a normalized percent.",
default=None,
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
dividend_frequency: Optional[str] = Field(
description="The dividend payment frequency of the ETF.", default=None
)
@field_validator(
"distribution_yield",
"return_1m",
"return_3m",
"return_6m",
"return_ytd",
"return_1y",
"return_3y",
"return_5y",
"return_10y",
"return_from_inception",
"mer",
"management_fee",
mode="before",
check_fields=False,
)
@classmethod
def normalize_percent(cls, v):
"""Return percents as normalized percentage points."""
if v:
return float(v) / 100
return None
class TmxEtfSearchFetcher(
Fetcher[
TmxEtfSearchQueryParams,
List[TmxEtfSearchData],
]
):
"""Transform the query, extract and transform the data from the TMX endpoints."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> TmxEtfSearchQueryParams:
"""Transform the query."""
return TmxEtfSearchQueryParams(**params)
@staticmethod
async def aextract_data(
query: TmxEtfSearchQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[Dict]:
"""Return the raw data from the TMX endpoint."""
etfs = pd.DataFrame(await get_all_etfs(use_cache=query.use_cache))
if query.query:
etfs = etfs[
etfs["name"].str.contains(query.query, case=False)
| etfs["short_name"].str.contains(query.query, case=False)
| etfs["investment_style"].str.contains(query.query, case=False)
| etfs["investment_objectives"].str.contains(query.query, case=False)
| etfs["symbol"].str.contains(query.query, case=False)
]
data = etfs.copy()
if query.div_freq:
data = data[data["dividend_frequency"] == query.div_freq.capitalize()]
if query.sort_by:
data = data.sort_values(by=query.sort_by, ascending=False)
data.drop(
columns=[
"sectors",
"regions",
"holdings_top10_summary",
"holdings_top10",
"additional_data",
"website",
"asset_class_id",
"investment_objectives",
],
inplace=True,
)
data = data.dropna(how="all")
return data.fillna("N/A").replace("N/A", None).to_dict("records")
@staticmethod
def transform_data(
query: TmxEtfSearchQueryParams,
data: List[Dict],
**kwargs: Any,
) -> List[TmxEtfSearchData]:
"""Transform the data to the standard format."""
return [TmxEtfSearchData.model_validate(d) for d in data]

View File

@@ -0,0 +1,85 @@
"""TMX ETF Sectors fetcher."""
# pylint: disable=unused-argument
import warnings
from typing import Any, Dict, List, Optional
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.etf_sectors import (
EtfSectorsData,
EtfSectorsQueryParams,
)
from openbb_tmx.utils.helpers import get_all_etfs
from pandas import DataFrame
from pydantic import Field
_warn = warnings.warn
class TmxEtfSectorsQueryParams(EtfSectorsQueryParams):
"""TMX ETF Sectors Query Params"""
use_cache: bool = Field(
default=True,
description="Whether to use a cached request. All ETF data comes from a single JSON file that is updated daily."
+ " To bypass, set to False. If True, the data will be cached for 4 hours.",
)
class TmxEtfSectorsData(EtfSectorsData):
"""TMX ETF Sectors Data."""
class TmxEtfSectorsFetcher(
Fetcher[
TmxEtfSectorsQueryParams,
List[TmxEtfSectorsData],
]
):
"""Transform the query, extract and transform the data from the TMX endpoints."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> TmxEtfSectorsQueryParams:
"""Transform the query."""
return TmxEtfSectorsQueryParams(**params)
@staticmethod
async def aextract_data(
query: TmxEtfSectorsQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[Dict]:
"""Return the raw data from the TMX endpoint."""
target = DataFrame()
_data = DataFrame(await get_all_etfs(use_cache=query.use_cache))
symbol = (
query.symbol.upper()
.replace("-", ".")
.replace(".TO", "")
.replace(".TSX", "")
)
_target = _data[_data["symbol"] == symbol]["sectors"]
if len(_target) > 0:
target = DataFrame.from_records(_target.iloc[0]).rename(
columns={"name": "sector", "percent": "weight"}
)
return target.to_dict(orient="records")
@staticmethod
def transform_data(
query: TmxEtfSectorsQueryParams,
data: List[Dict],
**kwargs: Any,
) -> List[TmxEtfSectorsData]:
"""Return the transformed data."""
target = DataFrame(data)
target["weight"] = target["weight"] / 100
target["sector"] = (
target["sector"].astype(str).str.lower().str.replace(" ", "_")
)
target = target.fillna(value="N/A").replace("N/A", None)
return [
TmxEtfSectorsData.model_validate(d)
for d in target.to_dict(orient="records")
]

View File

@@ -0,0 +1,145 @@
"""TMX Equity Gainers Model."""
# pylint: disable=unused-argument
import json
from typing import Any, Dict, List, Literal, Optional
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.equity_performance import (
EquityPerformanceData,
EquityPerformanceQueryParams,
)
from openbb_core.provider.utils.errors import EmptyDataError
from openbb_tmx.utils import gql
from openbb_tmx.utils.helpers import get_data_from_gql, get_random_agent
from pydantic import Field, field_validator, model_validator
STOCK_LISTS_DICT = {
"dividend": "TOP_DIVIDEND",
"energy": "TOP_ENERGY",
"healthcare": "TOP_HEALTHCARE",
"industrials": "TOP_INDUSTRIALS",
"price_performer": "TOP_PRICE_PERFORMER",
"rising_stars": "RISING_STARS",
"real_estate": "TOP_REAL_ESTATE",
"tech": "TOP_TECH",
"utilities": "TOP_UTILITIES",
"52w_high": "TOP_WEEK_52_HIGH",
"volume": "TOP_VOLUME",
}
STOCK_LISTS = Literal[
"dividend",
"energy",
"healthcare",
"industrials",
"price_performer",
"rising_stars",
"real_estate",
"tech",
"utilities",
"52w_high",
"volume",
]
class TmxGainersQueryParams(EquityPerformanceQueryParams):
"""TMX Gainers Query Params."""
category: STOCK_LISTS = Field(
default="price_performer",
description="The category of list to retrieve. Defaults to `price_performer`.",
)
class TmxGainersData(EquityPerformanceData):
"""TMX Gainers Data."""
__alias_dict__ = {
"name": "longName",
"change": "priceChange",
"percent_change": "percentChange",
"thirty_day_price_change": "30 Day Price Change",
"dividend_yield": "Dividend Yield",
"year_high": "52 Week High",
"avg_volume_10d": "10 Day Avg. Volume",
"ninety_day_price_change": "90 Day Price Change",
}
rank: int = Field(description="The rank of the stock in the list.")
@field_validator("percent_change", mode="after", check_fields=False)
@classmethod
def normalize_percent(cls, v):
"""Return percents as normalized percentage points."""
return float(v) / 100 if v else 0
@model_validator(mode="before")
@classmethod
def check_metric(cls, values):
for k, v in values.items():
if v is None or v == "-":
values[k] = 0
if k in ["Dividend Yield"]:
values[k] = float(v) / 100 if v else None
return values
class TmxGainersFetcher(
Fetcher[
TmxGainersQueryParams,
List[TmxGainersData],
]
):
"""TMX Gainers Fetcher."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> TmxGainersQueryParams:
"""Transform the query."""
return TmxGainersQueryParams(**params)
@staticmethod
async def aextract_data(
query: TmxGainersQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[TmxGainersData]:
"""Return the raw data from the TMX endpoint."""
user_agent = get_random_agent()
payload = gql.get_stock_list_payload.copy()
payload["variables"]["stockListId"] = STOCK_LISTS_DICT[query.category]
url = "https://app-money.tmx.com/graphql"
response = await get_data_from_gql(
method="POST",
url=url,
data=json.dumps(payload),
headers={
"authority": "app-money.tmx.com",
"referer": "https://money.tmx.com",
"locale": "en",
"Content-Type": "application/json",
"User-Agent": user_agent,
"Accept": "*/*",
},
timeout=5,
)
if "errors" in response:
raise EmptyDataError()
results = response["data"]["stockList"].get("listItems")
metric = response["data"]["stockList"].get("metricTitle")
for i in range(len(results)): # pylint: disable=C0200
if "metric" in results[i]:
results[i][metric] = results[i]["metric"]
del results[i]["metric"]
return results
@staticmethod
def transform_data(
query: TmxGainersQueryParams,
data: List[Dict],
**kwargs: Any,
) -> List[TmxGainersData]:
"""Transform the data to the model."""
return [TmxGainersData.model_validate(d) for d in data]

View File

@@ -0,0 +1,108 @@
"""TMX Stock Dividends Model"""
# pylint: disable=unused-argument
import json
from datetime import date as dateType
from typing import Any, Dict, List, Optional
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.historical_dividends import (
HistoricalDividendsData,
HistoricalDividendsQueryParams,
)
from openbb_tmx.utils import gql
from openbb_tmx.utils.helpers import get_data_from_gql, get_random_agent
from pydantic import Field
class TmxHistoricalDividendsQueryParams(HistoricalDividendsQueryParams):
"""TMX Historical Dividends Query Params"""
class TmxHistoricalDividendsData(HistoricalDividendsData):
"""TMX Historical Dividends Data"""
__alias_dict__ = {
"ex_dividend_date": "exDate",
"record_date": "recordDate",
"payment_date": "payableDate",
"declaration_date": "declarationDate",
}
currency: Optional[str] = Field(
default=None, description="The currency the dividend is paid in."
)
decalaration_date: Optional[dateType] = Field(
default=None, description="The date of the announcement."
)
record_date: Optional[dateType] = Field(
default=None,
description="The record date of ownership for rights to the dividend.",
)
payment_date: Optional[dateType] = Field(
default=None, description="The date the dividend is paid."
)
class TmxHistoricalDividendsFetcher(
Fetcher[TmxHistoricalDividendsQueryParams, List[TmxHistoricalDividendsData]]
):
"""TMX Historical Dividends Fetcher"""
@staticmethod
def transform_query(params: Dict[str, Any]) -> TmxHistoricalDividendsQueryParams:
"""Transform the query."""
return TmxHistoricalDividendsQueryParams(**params)
@staticmethod
async def aextract_data(
query: TmxHistoricalDividendsQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[Dict]:
"""Return the raw data from the TMX endpoint."""
user_agent = get_random_agent()
symbol = (
query.symbol.upper()
.replace("-", ".")
.replace(".TO", "")
.replace(".TSX", "")
)
data = []
payload = gql.historical_dividends_payload.copy()
payload["variables"]["symbol"] = symbol
payload["variables"]["batch"] = 500
payload["variables"]["page"] = 1
url = "https://app-money.tmx.com/graphql"
response = 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=5,
)
try:
if "data" in response and "dividends" in response["data"]:
data = response["data"].get("dividends")
data = sorted(data["dividends"], key=lambda d: d["exDate"])
except Exception as e:
raise RuntimeError(e) from e
return data
@staticmethod
def transform_data(
query: TmxHistoricalDividendsQueryParams,
data: List[Dict],
**kwargs: Any,
) -> List[TmxHistoricalDividendsData]:
"""Return the transformed data."""
return [TmxHistoricalDividendsData.model_validate(d) for d in data]

View File

@@ -0,0 +1,93 @@
"""TMX Index Constituents Model"""
# pylint: disable=unused-argument
from typing import Any, Dict, List, Optional
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.index_constituents import (
IndexConstituentsData,
IndexConstituentsQueryParams,
)
from openbb_core.provider.utils.errors import EmptyDataError
from openbb_tmx.utils.helpers import get_data_from_url, tmx_indices_backend
from pydantic import Field, field_validator
class TmxIndexConstituentsQueryParams(IndexConstituentsQueryParams):
"""TMX Index Constituents Query Params."""
use_cache: bool = Field(
default=True,
description="Whether to use a cached request."
+ " Index data is from a single JSON file, updated each day after close."
+ " It is cached for one day. To bypass, set to False.",
)
class TmxIndexConstituentsData(IndexConstituentsData):
"""TMX Index Constituents Data."""
__alias_dict__ = {
"market_value": "quotedmarketvalue",
}
market_value: Optional[float] = Field(
default=None,
description="The quoted market value of the asset.",
)
@field_validator("weight", mode="before", check_fields=False)
@classmethod
def normalize_percent(cls, v):
"""Return percents as normalized percentage points."""
return float(v) / 100 if v else None
class TmxIndexConstituentsFetcher(
Fetcher[
TmxIndexConstituentsQueryParams,
List[TmxIndexConstituentsData],
]
):
@staticmethod
def transform_query(params: Dict[str, Any]) -> TmxIndexConstituentsQueryParams:
"""Transform the query."""
return TmxIndexConstituentsQueryParams(**params)
@staticmethod
async def aextract_data(
query: TmxIndexConstituentsQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> Dict:
"""Return the raw data from the TMX endpoint."""
url = "https://tmxinfoservices.com/files/indices/sptsx-indices.json"
data = await get_data_from_url(
url,
use_cache=query.use_cache,
backend=tmx_indices_backend,
)
return data
@staticmethod
def transform_data(
query: TmxIndexConstituentsQueryParams, data: Dict, **kwargs
) -> List[TmxIndexConstituentsData]:
"""Return the transformed data."""
results = []
data = data.copy()
if data == {}:
raise EmptyDataError
if query.symbol not in data.get("indices"): # type: ignore
raise ValueError(f"Index {query.symbol} was not found. Check the symbol.")
index_data = data["indices"][query.symbol]
if (
index_data.get("nb_constituents") == 0
or index_data.get("constituents") is None
):
raise ValueError(f"No constituents found for index, {query.symbol}")
results = index_data["constituents"]
return [TmxIndexConstituentsData.model_validate(d) for d in results]

View File

@@ -0,0 +1,90 @@
"""TMX Index Sectors fetcher."""
# pylint: disable=unused-argument
from typing import Any, Dict, List, Optional
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.index_sectors import (
IndexSectorsData,
IndexSectorsQueryParams,
)
from openbb_core.provider.utils.errors import EmptyDataError
from openbb_tmx.utils.helpers import get_data_from_url, tmx_indices_backend
from pydantic import Field, field_validator
class TmxIndexSectorsQueryParams(IndexSectorsQueryParams):
"""TMX Index Sectors Query Params"""
use_cache: bool = Field(
default=True,
description="Whether to use a cached request. All Index data comes from a single JSON file that is updated daily."
+ " To bypass, set to False. If True, the data will be cached for 1 day.",
)
class TmxIndexSectorsData(IndexSectorsData):
"""TMX Index Sectors Data."""
@field_validator("weight", mode="after", check_fields=False)
@classmethod
def normalize_percent(cls, v):
"""Return percents as normalized percentage points."""
return float(v) / 100 if v else None
class TmxIndexSectorsFetcher(
Fetcher[
TmxIndexSectorsQueryParams,
List[TmxIndexSectorsData],
]
):
"""Transform the query, extract and transform the data from the TMX endpoints."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> TmxIndexSectorsQueryParams:
"""Transform the query."""
return TmxIndexSectorsQueryParams(**params)
@staticmethod
async def aextract_data(
query: TmxIndexSectorsQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> Dict:
"""Return the raw data from the TMX endpoint."""
url = "https://tmxinfoservices.com/files/indices/sptsx-indices.json"
data = await get_data_from_url(
url,
use_cache=query.use_cache,
backend=tmx_indices_backend,
)
return data
@staticmethod
def transform_data(
query: TmxIndexSectorsQueryParams, data: Dict, **kwargs: Any
) -> List[TmxIndexSectorsData]:
"""Return the transformed data."""
results = []
data = data.copy()
if data == {}:
raise EmptyDataError
if (
query.symbol in data["indices"]
and "sectors" in data["indices"][query.symbol]
):
temp = data["indices"][query.symbol].get("sectors")
results = [
{
"sector": d.get("name").lower().replace(" ", "_"),
"weight": d.get("weight"),
}
for d in temp
if temp is not None
]
return [TmxIndexSectorsData.model_validate(d) for d in results]

View File

@@ -0,0 +1,283 @@
"""TMX Index Snapshots Model"""
# pylint: disable=unused-argument
import json
from typing import Any, Dict, List, Literal, Optional
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.index_snapshots import (
IndexSnapshotsData,
IndexSnapshotsQueryParams,
)
from openbb_core.provider.utils.errors import EmptyDataError
from openbb_tmx.utils import gql
from openbb_tmx.utils.helpers import (
NASDAQ_GIDS,
get_data_from_gql,
get_data_from_url,
get_random_agent,
tmx_indices_backend,
)
from pydantic import Field, field_validator
class TmxIndexSnapshotsQueryParams(IndexSnapshotsQueryParams):
"""TMX Index Snapshots Query Params."""
region: Literal[None, "ca", "us"] = Field(default="ca")
use_cache: bool = Field(
default=True,
description="Whether to use a cached request."
+ " Index data is from a single JSON file, updated each day after close."
+ " It is cached for one day. To bypass, set to False.",
)
class TmxIndexSnapshotsData(IndexSnapshotsData):
"""TMX Index Snapshots Data."""
__alias_dict__ = {
"name": "longname",
"prev_close": "prevClose",
"change": "priceChange",
"change_percent": "previousday",
"year_high": "weeks52high",
"year_low": "weeks52low",
"return_mtd": "monthtodate",
"return_qtd": "quartertodate",
"return_ytd": "yeartodate",
"total_market_value": "total",
"constituent_average_market_value": "average",
"constituent_median_market_value": "median",
"constituent_top10_market_value": "sumtop10",
"constituent_largest_market_value": "largest",
"constituent_largest_weight": "largestweight",
"constituent_smallest_market_value": "smallest",
"constituent_smallest_weight": "smallestweight",
}
year_high: Optional[float] = Field(
default=None, description="The 52-week high of the index."
)
year_low: Optional[float] = Field(
default=None, description="The 52-week low of the index."
)
return_mtd: Optional[float] = Field(
default=None,
description="The month-to-date return of the index, as a normalized percent.",
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
return_qtd: Optional[float] = Field(
default=None,
description="The quarter-to-date return of the index, as a normalized percent.",
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
return_ytd: Optional[float] = Field(
default=None,
description="The year-to-date return of the index, as a normalized percent.",
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
total_market_value: Optional[float] = Field(
default=None,
description="The total quoted market value of the index.",
)
number_of_constituents: Optional[int] = Field(
default=None,
description="The number of constituents in the index.",
)
constituent_average_market_value: Optional[float] = Field(
default=None,
description="The average quoted market value of the index constituents.",
)
constituent_median_market_value: Optional[float] = Field(
default=None,
description="The median quoted market value of the index constituents.",
)
constituent_top10_market_value: Optional[float] = Field(
default=None,
description="The sum of the top 10 quoted market values of the index constituents.",
)
constituent_largest_market_value: Optional[float] = Field(
default=None,
description="The largest quoted market value of the index constituents.",
)
constituent_largest_weight: Optional[float] = Field(
default=None,
description="The largest weight of the index constituents, as a normalized percent.",
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
constituent_smallest_market_value: Optional[float] = Field(
default=None,
description="The smallest quoted market value of the index constituents.",
)
constituent_smallest_weight: Optional[float] = Field(
default=None,
description="The smallest weight of the index constituents, as a normalized percent.",
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
@field_validator(
"return_mtd",
"return_qtd",
"return_ytd",
"change_percent",
"constituent_largest_weight",
"constituent_smallest_weight",
mode="before",
check_fields=False,
)
@classmethod
def normalize_percent(cls, v):
"""Return percents as normalized percentage points."""
return round(float(v) / 100, 6) if v else None
@field_validator(
"year_high",
"year_low",
"price",
"prev_close",
"change",
mode="before",
check_fields=False,
)
@classmethod
def round_floating(cls, v):
"""Round floating values to two decimals."""
return round(float(v), 2) if v else None
class TmxIndexSnapshotsFetcher(
Fetcher[
TmxIndexSnapshotsQueryParams,
List[TmxIndexSnapshotsData],
]
):
@staticmethod
def transform_query(params: Dict[str, Any]) -> TmxIndexSnapshotsQueryParams:
"""Transform the query."""
return TmxIndexSnapshotsQueryParams(**params)
@staticmethod
async def aextract_data(
query: TmxIndexSnapshotsQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[Dict]:
"""Return the raw data from the TMX endpoint."""
url = "https://tmxinfoservices.com/files/indices/sptsx-indices.json"
user_agent = get_random_agent()
results = []
if query.region == "ca":
data = await get_data_from_url(
url,
use_cache=query.use_cache,
backend=tmx_indices_backend,
)
if not data:
raise EmptyDataError
symbols = []
for symbol in data["indices"]:
symbols.append(symbol)
new_data = {}
performance = data["indices"][symbol].get("performance", {})
market_value = data["indices"][symbol].get("quotedmarketvalue", {})
new_data.update(
{
"symbol": symbol,
"name": data["indices"][symbol].get("name_en", None),
"currency": (
"USD"
if "(USD)" in data["indices"][symbol]["name_en"]
else "CAD"
),
**performance,
**market_value,
}
)
results.append(new_data)
# Get current levels for each index.
payload = gql.get_quote_for_symbols_payload.copy()
payload["variables"]["symbols"] = symbols
url = "https://app-money.tmx.com/graphql"
response = await get_data_from_gql(
method="POST",
url=url,
data=json.dumps(payload),
headers={
"authority": "app-money.tmx.com",
"referer": "https://money.tmx.com/en/quote/^TSX",
"locale": "en",
"Content-Type": "application/json",
"User-Agent": user_agent,
"Accept": "*/*",
},
timeout=5,
)
if response.get("data") and response["data"].get("getQuoteForSymbols"):
quote_data = response["data"]["getQuoteForSymbols"]
for d in data:
if "longname" in d:
d.pop("longname")
if "percentChange" in d:
d.pop("percentChange")
merged_list = [
{
**d1,
**next(
(d2 for d2 in quote_data if d2["symbol"] == d1["symbol"]),
{},
),
}
for d1 in results
]
results = merged_list
if query.region == "us":
symbols = [f"{symbol}:US" for symbol in NASDAQ_GIDS]
payload = gql.get_quote_for_symbols_payload.copy()
payload["variables"]["symbols"] = symbols
url = "https://app-money.tmx.com/graphql"
response = await get_data_from_gql(
method="POST",
url=url,
data=json.dumps(payload),
headers={
"authority": "app-money.tmx.com",
"referer": "https://money.tmx.com/en/quote/^TSX",
"locale": "en",
"Content-Type": "application/json",
"User-Agent": user_agent,
"Accept": "*/*",
},
timeout=5,
)
if response.get("data") and response["data"].get("getQuoteForSymbols"):
results = response["data"]["getQuoteForSymbols"]
for item in results:
item["change_percent"] = item.pop("percentChange")
return results
@staticmethod
def transform_data(
query: TmxIndexSnapshotsQueryParams,
data: List[Dict],
**kwargs: Any,
) -> List[TmxIndexSnapshotsData]:
"""Return the transformed data."""
return [
TmxIndexSnapshotsData.model_validate(
{
"change_percent" if k == "percentChange" else k: (
None if v in ["", 0] else v
)
for k, v in d.items()
}
)
for d in data
if "price" in d and d["price"] is not None
]

View File

@@ -0,0 +1,168 @@
"""TMX Insider Trading Model"""
# pylint: disable=unused-argument
import json
from typing import Any, Dict, List, Optional
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.insider_trading import (
InsiderTradingData,
InsiderTradingQueryParams,
)
from openbb_core.provider.utils.errors import EmptyDataError
from openbb_core.provider.utils.helpers import to_snake_case
from openbb_tmx.utils import gql
from openbb_tmx.utils.helpers import get_data_from_gql, get_random_agent
from pydantic import Field, field_validator
class TmxInsiderTradingQueryParams(InsiderTradingQueryParams):
"""TMX Insider Trading Query Params"""
summary: bool = Field(
default=False,
description="Return a summary of the insider activity instead of the individuals.",
)
class TmxInsiderTradingData(InsiderTradingData):
"""TMX Insider Trading Data"""
period: str = Field(
description="The period of the activity. Bucketed by three, six, and twelve months."
)
owner_name: Optional[str] = Field(
default=None, description="The name of the insider."
)
acquisition_or_deposition: Optional[str] = Field(
default=None, description="Whether the insider bought or sold the shares."
)
number_of_trades: Optional[int] = Field(
default=None, description="The number of shares traded over the period."
)
securities_owned: Optional[int] = Field(
default=None, description="The number of shares held by the insider."
)
trade_value: Optional[float] = Field(
default=None, description="The value of the shares traded by the insider."
)
securities_transacted: Optional[int] = Field(
default=None,
description="The total number of shares traded by the insider over the period.",
)
securities_bought: Optional[int] = Field(
default=None,
description="The total number of shares bought by all insiders over the period.",
)
securities_sold: Optional[int] = Field(
default=None,
description="The total number of shares sold by all insiders over the period.",
)
net_activity: Optional[int] = Field(
default=None,
description="The total net activity by all insiders over the period.",
)
@field_validator("period", mode="before", check_fields=False)
@classmethod
def period_to_snake_case(cls, v):
return to_snake_case(v) if v else None
class TmxInsiderTradingFetcher(
Fetcher[
TmxInsiderTradingQueryParams,
List[TmxInsiderTradingData],
]
):
"""TMX Insider Trading Fetcher."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> TmxInsiderTradingQueryParams:
"""Transform the query."""
return TmxInsiderTradingQueryParams(**params)
@staticmethod
async def aextract_data(
query: TmxInsiderTradingQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[Dict]:
"""Return the raw data from the TMX endpoint."""
results = []
user_agent = get_random_agent()
symbol = (
query.symbol.upper()
.replace("-", ".")
.replace(".TO", "")
.replace(".TSX", "")
)
payload = gql.get_company_insiders_payload.copy()
payload["variables"]["symbol"] = symbol
url = "https://app-money.tmx.com/graphql"
response = 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=5,
)
if response.get("data") and response["data"].get(
"getCompanyInsidersActivities"
):
results = response["data"]["getCompanyInsidersActivities"]
if results == []:
raise EmptyDataError()
return results
@staticmethod
def transform_data(
query: TmxInsiderTradingQueryParams,
data: List[Dict],
**kwargs: Any,
) -> List[TmxInsiderTradingData]:
"""Transform the data."""
data = data.copy()
results = []
flattened_insiders = []
for activity in data["insiderActivities"]: # type: ignore
for transaction_type in ["buy", "sell"]:
for transaction in activity[transaction_type]:
new_transaction = {
"period": activity["periodkey"],
"acquisition_or_disposition": transaction_type,
"owner_name": transaction["name"],
"number_of_trades": transaction["trades"],
"securities_transacted": transaction["shares"],
"securities_owned": transaction["sharesHeld"],
"trade_value": transaction["tradeValue"],
}
flattened_insiders.append(new_transaction)
flattened_summary = []
for activity in data["activitySummary"]: # type: ignore
new_activity = {
"period": activity["periodkey"],
"securities_bought": activity["buyShares"],
"securities_sold": activity["soldShares"],
"net_activity": activity["netActivity"],
"securities_transacted": activity["totalShares"],
}
flattened_summary.append(new_activity)
if query.summary is False and len(flattened_insiders) > 0:
results = flattened_insiders
elif query.summary is True and len(flattened_summary) > 0:
results = flattened_summary
return [TmxInsiderTradingData.model_validate(d) for d in results]

View File

@@ -0,0 +1,105 @@
"""TMX Options Chains Model."""
# pylint: disable=unused-argument
from datetime import (
date as dateType,
datetime,
)
from typing import Any, Dict, List, Optional
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.options_chains import (
OptionsChainsData,
OptionsChainsQueryParams,
)
from openbb_core.provider.utils.descriptions import (
QUERY_DESCRIPTIONS,
)
from openbb_tmx.utils.helpers import download_eod_chains, get_current_options
from pydantic import Field, field_validator
class TmxOptionsChainsQueryParams(OptionsChainsQueryParams):
"""TMX Options Chains Query.
Source: https://www.Tmx.com/
"""
date: Optional[dateType] = Field(
description=QUERY_DESCRIPTIONS.get("date", ""),
default=None,
)
use_cache: bool = Field(
default=True,
description="Caching is used to validate the supplied ticker symbol, or if a historical EOD chain is requested."
+ " To bypass, set to False.",
)
class TmxOptionsChainsData(OptionsChainsData):
"""TMX Options Chains Data."""
transactions: Optional[int] = Field(
description="Number of transactions for the contract.", default=None
)
total_value: Optional[float] = Field(
description="Total value of the transactions.", default=None
)
settlement_price: Optional[float] = Field(
description="Settlement price on that date.", default=None
)
underlying_price: Optional[float] = Field(
description="Price of the underlying stock on that date.", default=None
)
dte: Optional[int] = Field(
description="Days to expiration for the option.", default=None
)
@field_validator("expiration", mode="before", check_fields=False)
@classmethod
def date_validate(cls, v): # pylint: disable=E0213
"""Return the datetime object from the date string"""
return datetime.strptime(v, "%Y-%m-%d")
class TmxOptionsChainsFetcher(
Fetcher[
TmxOptionsChainsQueryParams,
List[TmxOptionsChainsData],
]
):
"""Transform the query, extract and transform the data from the TMX endpoints."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> TmxOptionsChainsQueryParams:
"""Transform the query."""
return TmxOptionsChainsQueryParams(**params)
@staticmethod
async def aextract_data(
query: TmxOptionsChainsQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[Dict]:
"""Return the data."""
results = []
if query.date is not None:
chains = await download_eod_chains(
symbol=query.symbol, date=query.date, use_cache=query.use_cache
)
else:
chains = await get_current_options(query.symbol, use_cache=query.use_cache)
if not chains.empty:
results = chains.to_dict(orient="records")
return results
@staticmethod
def transform_data(
query: TmxOptionsChainsQueryParams,
data: dict,
**kwargs: Any,
) -> List[TmxOptionsChainsData]:
"""Transform the data and validate the model."""
return [TmxOptionsChainsData.model_validate(d) for d in data]

View File

@@ -0,0 +1,160 @@
"""TMX Stock Analysts Model"""
# pylint: disable=unused-argument
import asyncio
import json
from typing import Any, Dict, List, Optional
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.price_target_consensus import (
PriceTargetConsensusData,
PriceTargetConsensusQueryParams,
)
from openbb_tmx.utils import gql
from openbb_tmx.utils.helpers import get_data_from_gql, get_random_agent
from pydantic import Field, field_validator
class TmxPriceTargetConsensusQueryParams(PriceTargetConsensusQueryParams):
"""TMX Price Target Consensus Query."""
__json_schema_extra__ = {"symbol": ["multiple_items_allowed"]}
class TmxPriceTargetConsensusData(PriceTargetConsensusData):
"""TMX Price Target Consensus Data."""
__alias_dict__ = {
"target_consensus": "price_target",
"target_high": "price_target_high",
"target_low": "price_target_low",
"target_upside": "price_target_upside",
}
target_upside: Optional[float] = Field(
default=None,
description="Percent of upside, as a normalized percent.",
json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100},
)
total_analysts: Optional[int] = Field(
default=None, description="Total number of analyst."
)
buy_ratings: Optional[int] = Field(
default=None, description="Number of buy ratings."
)
sell_ratings: Optional[int] = Field(
default=None, description="Number of sell ratings."
)
hold_ratings: Optional[int] = Field(
default=None, description="Number of hold ratings."
)
consensus_action: Optional[str] = Field(
default=None, description="Consensus action."
)
@field_validator("target_upside", mode="before", check_fields=False)
@classmethod
def normalize_percent(cls, v):
"""Return percents as normalized percentage points."""
return float(v) / 100 if v else None
@field_validator(
"target_consensus",
"target_high",
"target_low",
mode="before",
check_fields=False,
)
@classmethod
def round_targets(cls, v):
"""Return rounded prices to two decimals."""
return round(float(v), 2) if v else None
class TmxPriceTargetConsensusFetcher(
Fetcher[TmxPriceTargetConsensusQueryParams, List[TmxPriceTargetConsensusData]]
):
"""Transform the query, extract and transform the data from the TMX endpoints."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> TmxPriceTargetConsensusQueryParams:
"""Transform the query."""
return TmxPriceTargetConsensusQueryParams(**params)
@staticmethod
async def aextract_data(
query: TmxPriceTargetConsensusQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[Dict]:
"""Return the raw data from the TMX endpoint."""
symbols = query.symbol.split(",")
results = []
async def create_task(symbol, results):
"""Create a task for each symbol provided."""
symbol = (
symbol.upper()
.replace("-", ".")
.replace(".TO", "")
.replace(".TSXV", "")
.replace(".TSX", "")
)
payload = gql.get_company_analysts_payload.copy()
payload["variables"]["symbol"] = symbol
payload["variables"]["datatype"] = "equity"
data = {}
url = "https://app-money.tmx.com/graphql"
response = 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": get_random_agent(),
"Accept": "*/*",
},
timeout=10,
)
r_data = (
response["data"].get("analysts", None) if response.get("data") else None
)
if r_data:
data.update(
{
"symbol": symbol,
"total_analysts": r_data["totalAnalysts"],
"consensus_action": r_data["consensusAnalysts"]["consensus"],
"buy_ratings": r_data["consensusAnalysts"]["buy"],
"sell_ratings": r_data["consensusAnalysts"]["sell"],
"hold_ratings": r_data["consensusAnalysts"]["hold"],
"price_target": r_data["priceTarget"]["priceTarget"],
"price_target_high": r_data["priceTarget"]["highPriceTarget"],
"price_target_low": r_data["priceTarget"]["lowPriceTarget"],
"price_target_upside": r_data["priceTarget"][
"priceTargetUpside"
],
}
)
results.append(data)
return results
tasks = [create_task(symbol, results) for symbol in symbols]
await asyncio.gather(*tasks)
return results
@staticmethod
def transform_data(
query: TmxPriceTargetConsensusQueryParams,
data: List[Dict],
**kwargs: Any,
) -> List[TmxPriceTargetConsensusData]:
"""Return the transformed data."""
return [TmxPriceTargetConsensusData.model_validate(d) for d in data]

View File

@@ -0,0 +1,156 @@
"""TMX Treasury Prices Fetcher"""
# pylint: disable=unused-argument
from datetime import (
date as dateType,
datetime,
timedelta,
)
from typing import Any, Dict, List, Literal, Optional
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.treasury_prices import (
TreasuryPricesData,
TreasuryPricesQueryParams,
)
from openbb_tmx.utils.helpers import get_all_bonds
from pandas import DataFrame
from pydantic import Field, field_validator
class TmxTreasuryPricesQueryParams(TreasuryPricesQueryParams):
"""
TMX Treasury Prices Query Params.
Data will be made available by 5:00 EST on T+1
Source: https://bondtradedata.iiroc.ca/#/
"""
govt_type: Literal["federal", "provincial", "municipal"] = Field(
default="federal",
description="The level of government issuer.",
)
issue_date_min: Optional[dateType] = Field(
default=None,
description="Filter by the minimum original issue date.",
)
issue_date_max: Optional[dateType] = Field(
default=None,
description="Filter by the maximum original issue date.",
)
last_traded_min: Optional[dateType] = Field(
default=None,
description="Filter by the minimum last trade date.",
)
maturity_date_min: Optional[dateType] = Field(
default=None,
description="Filter by the minimum maturity date.",
)
maturity_date_max: Optional[dateType] = Field(
default=None,
description="Filter by the maximum maturity date.",
)
use_cache: bool = Field(
default=True,
description="All bond data is sourced from a single JSON file that is updated daily."
+ " The file is cached for one day to eliminate downloading more than once."
+ " Caching will significantly speed up subsequent queries. To bypass, set to False.",
)
class TmxTreasuryPricesData(TreasuryPricesData):
"""TMX Treasury Prices Data."""
__alias_dict__ = {
"rate": "couponRate",
"ytm": "lastYield",
"last_price": "lastPrice",
"highest_price": "highestPrice",
"lowest_price": "lowestPrice",
"total_trades": "totalTrades",
"last_traded_date": "lastTradedDate",
"maturity_date": "maturityDate",
"issue_date": "originalIssueDate",
"issuer_name": "issuer",
}
@field_validator(
"ytm",
"rate",
mode="before",
check_fields=False,
)
@classmethod
def normalize_percent(cls, v):
"""Return percents as normalized percentage points."""
return float(v) / 100 if v else None
class TmxTreasuryPricesFetcher(
Fetcher[
TmxTreasuryPricesQueryParams,
List[TmxTreasuryPricesData],
]
):
"""Tmx Bond Reference Fetcher."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> TmxTreasuryPricesQueryParams:
"""Transform query params."""
transformed_params = params.copy()
now = datetime.now()
if now.date().weekday() > 4:
now = now - timedelta(now.date().weekday() - 4)
if "maturity_date_min" not in transformed_params:
transformed_params["maturity_date_min"] = (
now - timedelta(days=1)
).strftime("%Y-%m-%d")
return TmxTreasuryPricesQueryParams(**transformed_params)
@staticmethod
async def aextract_data(
query: TmxTreasuryPricesQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> DataFrame:
"""Get the raw data containing all bond data."""
bonds = await get_all_bonds(use_cache=query.use_cache)
return bonds
@staticmethod
def transform_data(
query: TmxTreasuryPricesQueryParams,
data: DataFrame,
**kwargs: Any,
) -> List[TmxTreasuryPricesData]:
"""Transform data."""
bonds = data.copy()
results = []
govt_type_dict = {
"provincial": "prov",
"federal": "government of canada",
"municipal": "municipal",
}
govt_type = govt_type_dict[query.govt_type] # noqa # pylint: disable=W0612
data = bonds.query(
"maturityDate >= @query.maturity_date_min.strftime('%Y-%m-%d')"
+ " & bondType == 'Govt'"
+ " & issuer.str.contains(@govt_type, case=False)"
).sort_values(by=["maturityDate"])
data.issuer = data.loc[:, "issuer"].str.strip()
if query.maturity_date_max:
data = data.query(
"maturityDate <= @query.maturity_date_max.strftime('%Y-%m-%d')"
)
if query.last_traded_min:
data = data.query(
"lastTradedDate >= @query.last_traded_min.strftime('%Y-%m-%d')"
)
if len(data) > 0:
data = data.drop(columns=["bondType", "securityId", "secKey"])
data = data.fillna("N/A").replace("N/A", None)
results = data.to_dict("records")
return [TmxTreasuryPricesData.model_validate(d) for d in results]

View File

View File

@@ -0,0 +1,513 @@
"""GraphQL query definitions."""
# pylint: disable=line-too-long
# ruff: noqa: E501
stock_info_query = """ query getQuoteBySymbol(
$symbol: String,
$locale: String
) {
getQuoteBySymbol(symbol: $symbol, locale: $locale) {
symbol
name
price
priceChange
percentChange
exchangeName
exShortName
exchangeCode
sector
industry
volume
openPrice
dayHigh
dayLow
MarketCap
MarketCapAllClasses
peRatio
prevClose
dividendFrequency
dividendYield
dividendAmount
dividendCurrency
beta
eps
exDividendDate
shortDescription
longDescription
website
email
phoneNumber
fullAddress
employees
shareOutStanding
totalDebtToEquity
totalSharesOutStanding
sharesESCROW
vwap
dividendPayDate
weeks52high
weeks52low
alpha
averageVolume10D
averageVolume30D
averageVolume50D
priceToBook
priceToCashFlow
returnOnEquity
returnOnAssets
day21MovingAvg
day50MovingAvg
day200MovingAvg
dividend3Years
dividend5Years
datatype
issueType
qmdescription
}
}
"""
stock_info_payload = {
"operationName": "getQuoteBySymbol",
"variables": {"locale": "en"},
"query": stock_info_query,
}
get_timeseries_query = """query getTimeSeriesData($symbol: String!, $freq: String, $interval: Int, $start: String, $end: String, $startDateTime: Int, $endDateTime: Int) {
getTimeSeriesData(
symbol: $symbol
freq: $freq
interval: $interval
start: $start
end: $end
startDateTime: $startDateTime
endDateTime: $endDateTime
) {
dateTime
open
high
low
close
volume
}
}"""
get_timeseries_payload = {
"operationName": "getTimeSeriesData",
"variables": {
"symbol": "BNS",
"freq": "day",
"interval": "",
"start": "2013-09-30",
"end": "2013-10-31",
"startDateTime": "",
"endDateTime": "",
},
"query": get_timeseries_query,
}
get_company_price_history_query = """query getCompanyPriceHistory($symbol: String!, $start: String, $end: String, $adjusted: Boolean, $adjustmentType: String, $unadjusted: Boolean, $limit: Int) {
getCompanyPriceHistory(
symbol: $symbol
start: $start
end: $end
adjusted: $adjusted
adjustmentType: $adjustmentType
unadjusted: $unadjusted
limit: $limit
) {
datetime
openPrice
closePrice
high
low
volume
tradeValue
numberOfTrade
change
changePercent
vwap
}
}"""
get_company_price_history_payload = {
"operationName": "getCompanyPriceHistory",
"variables": {
"adjusted": True,
"adjustmentType": "SO",
"end": "2023-10-28",
"start": "2023-10-01",
"symbol": "BNS",
"unadjusted": False,
},
"query": get_company_price_history_query,
}
get_company_most_recent_trades_query = """query getCompanyMostRecentTrades(
$symbol: String!
$limit: Int
) {
trades: getCompanyMostRecentTrades(
symbol: $symbol,
limit: $limit
) {
price
volume
datetime
sellerId
sellerName
buyerId
buyerName
exchangeCode
}
}
"""
get_company_most_recent_trades_payload = {
"operationName": "getCompanyMostRecentTrades",
"variables": {
"symbol": "BNS",
"limit": 51,
},
"query": get_company_most_recent_trades_query,
}
get_company_news_events_query = """query getNewsAndEvents(
$symbol: String!,
$page: Int!,
$limit: Int!,
$locale: String!
) {
news: getNewsForSymbol(
symbol: $symbol,
page: $page,
limit: $limit,
locale: $locale
) {
headline
datetime
source
newsid
summary
}
events: getUpComingEventsForSymbol(symbol: $symbol, locale: $locale) {
title
date
status
type
}
}
"""
get_company_news_events_payload = {
"operationName": "getNewsAndEvents",
"variables": {"symbol": "ART", "page": 1, "limit": 100, "locale": "en"},
"query": get_company_news_events_query,
}
get_company_filings_query = """query getCompanyFilings($symbol: String!, $fromDate: String, $toDate: String, $limit: Int) {
filings: getCompanyFilings(
symbol: $symbol
fromDate: $fromDate
toDate: $toDate
limit: $limit
) {
size
filingDate
description
name
urlToPdf
}
}"""
get_company_filings_payload = {
"operationName": "getCompanyFilings",
"variables": {
"symbol": "AC",
"fromDate": "2020-09-01",
"toDate": "2023-09-20",
"limit": 100,
},
"query": get_company_filings_query,
}
historical_dividends_query = """query getDividendsForSymbol(
$symbol: String!
$page: Int,
$batch: Int
) {
dividends: getDividendsForSymbol(
symbol: $symbol
page: $page
batch: $batch
) {
pageNumber
hasNextPage
dividends
{
exDate
amount
currency
payableDate
declarationDate
recordDate
}
}
}"""
historical_dividends_payload = {
"operationName": "getDividendsForSymbol",
"variables": {
"batch": 10,
"page": 1,
"symbol": "BNS",
},
"query": historical_dividends_query,
}
get_company_analysts_query = """query getCompanyAnalysts(
$symbol: String!
$dataType: String,
) {
analysts: getCompanyAnalysts(
datatype: $dataType,
symbol: $symbol
) {
totalAnalysts
priceTarget
{
highPriceTarget
lowPriceTarget
priceTarget
priceTargetUpside
}
consensusAnalysts
{
consensus
buy
sell
hold
}
}
}"""
get_company_analysts_payload = {
"operationName": "getCompanyAnalysts",
"variables": {
"symbol": "BNS",
"datatype": "equity",
},
"query": get_company_analysts_query,
}
get_earnings_date_query = """query getEnhancedEarningsForDate(
$date: String!
) {
getEnhancedEarningsForDate(
date: $date
) {
symbol
companyName
announceTime
estimatedEps
actualEps
epsSurprisePercent
epsSurpriseDollar
}
}"""
get_earnings_date_payload = {
"operationName": "getEnhancedEarningsForDate",
"variables": {
"date": "2023-10-04",
},
"query": get_earnings_date_query,
}
get_index_overview_query = """query getIndexBySymbol(
$symbol: String!,
$locale: String
) {
getIndexBySymbol(
symbol: $symbol,
locale: $locale
) {
name
intro
overview
}
}"""
get_index_overview_payload = {
"operationName": "getIndexBySymbol",
"variables": {
"symbol": "^TSX",
},
"query": get_index_overview_query,
}
get_index_constituents_query = """query getIndexConstituents(
$symbol: String!
) {
constituents: getIndexConstituents(
symbol: $symbol
) {
symbol
quotedMarketValue
longName
shortName
weight
exShortName
exchange
exLongName
}
keyData: getIndexKeyData(
symbol: $symbol
) {
adjMarketCap
avgConstituentMarketCap
numConstituents
top10HoldingsAdjMarketCap
ytdPriceReturn
prevDayPriceReturn
prevMonthPriceReturn
prevQuarterPriceReturn
percentWeightLargestConstituent
peRatio
pbRatio
priceToSales
divYield
pcfRatio
}
}"""
get_index_constituents_payload = {
"operationName": "getIndexConstituents",
"variables": {
"symbol": "^TSX",
},
"query": get_index_constituents_query,
}
get_stock_list_query = """query getStockListSymbolsWithQuote(
$stockListId: String!
$locale: String,
) {
stockList: getStockListSymbolsWithQuote(
stockListId: $stockListId,
locale: $locale
) {
stockListId
name
description
longDescription
metricTitle
listItems
{
symbol
longName
rank
metric
price
priceChange
percentChange
volume
}
totalPriceChange
totalPercentChange
createdAt
updatedAt
}
}"""
get_stock_list_payload = {
"operationName": "getStockListSymbolsWithQuote",
"variables": {"locale": "en", "stockListId": "TOP_VOLUME"},
"query": get_stock_list_query,
}
get_company_insiders_query = """query getCompanyInsidersActivities(
$symbol: String
) {
getCompanyInsidersActivities(
symbol: $symbol
) {
insiderActivities {
periodkey
buy {
name
trades
shares
sharesHeld
tradeValue
}
sell {
name
trades
shares
sharesHeld
tradeValue
}
}
activitySummary {
periodkey
buyShares
soldShares
netActivity
totalShares
}
}
}"""
get_company_insiders_payload = {
"operationName": "getCompanyInsidersActivities",
"variables": {
"symbol": "CNQ",
},
"query": get_company_insiders_query,
}
get_quote_for_symbols_query = """query getQuoteForSymbols($symbols: [String]) {
getQuoteForSymbols(symbols: $symbols) {
symbol
longname
price
prevClose
priceChange
percentChange
weeks52high
weeks52low
}
}"""
get_quote_for_symbols_payload = {
"operationName": "getQuoteForSymbols",
"variables": {
"symbols": [
"SRE:US",
"BWVTF:US",
"C.P.K:US",
"BAC.PY:US",
"BALTF:US",
"BACRP:US",
],
},
"query": get_quote_for_symbols_query,
}
get_index_price_history_query = """query getIndexPriceHistory($symbol: String!, $start: String, $end: String, $adjusted: Boolean, $adjustmentType: String, $unadjusted: Boolean, $limit: Int) {\n getIndexPriceHistory(\n symbol: $symbol\n start: $start\n end: $end\n adjusted: $adjusted\n adjustmentType: $adjustmentType\n unadjusted: $unadjusted\n limit: $limit\n ) {\n datetime\n openPrice\n closePrice\n high\n low\n volume\n change\n changePercent\n triv\n }\n}"}"""
get_index_price_history_payload = {
"operationName": "getIndexPriceHistory",
"variables": {
"symbol": "^TSX",
"start": "2023-12-01",
"end": "2023-12-31",
"adjusted": True,
"adjustmentType": "SO",
"unadjusted": False,
},
"query": get_index_price_history_query,
}

File diff suppressed because it is too large Load Diff

1549
openbb_platform/providers/tmx/poetry.lock generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
[tool.poetry]
name = "openbb-tmx"
version = "1.0.0"
description = "Unofficial TMX data provider extension for the OpenBB Platform - Public Canadian markets data for Python and Fast API."
authors = ["OpenBB <hello@openbb.co>"]
readme = "README.md"
packages = [{ include = "openbb_tmx" }]
[tool.poetry.dependencies]
python = "^3.8"
aiohttp-client-cache = "^0.10.0"
aiosqlite = "^0.19.0"
random-user-agent = "^1.0.1"
exchange-calendars = "^4.2.8"
openbb-core = "^1.1.2"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.plugins."openbb_provider_extension"]
tmx = "openbb_tmx:tmx_provider"

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,795 @@
interactions:
- request:
body: '{"operationName": "getCompanyFilings", "variables": {"symbol": "SHOP",
"fromDate": "2023-08-25", "toDate": "2023-08-31", "limit": 1000}, "query": "query
getCompanyFilings($symbol: String!, $fromDate: String, $toDate: String, $limit:
Int) {\n filings: getCompanyFilings(\n symbol: $symbol\n fromDate: $fromDate\n toDate:
$toDate\n limit: $limit\n ) {\nsize\nfilingDate\ndescription\nname\nurlToPdf\n}\n}"}'
headers:
Accept:
- '*/*'
Content-Type:
- application/json
authority:
- app-money.tmx.com
locale:
- en
referer:
- https://money.tmx.com/en/quote/SHOP
method: POST
uri: https://app-money.tmx.com/graphql
response:
body:
string: '{"data":{"filings":[]}}
'
headers:
Access-Control-Allow-Origin:
- '*'
Connection:
- keep-alive
Content-Length:
- '24'
Content-Security-Policy:
- frame-ancestors 'none'; default-src 'self'
Content-Type:
- application/json; charset=utf-8
Date:
- Wed, 27 Dec 2023 22:03:25 GMT
Etag:
- W/"18-YqxSvZ09xIbNEA8V1atJvD3G14s"
Strict-Transport-Security:
- max-age=15552000; includeSubDomains
Vary:
- Origin, Accept-Encoding
X-Content-Type-Options:
- nosniff
X-DNS-Prefetch-Control:
- 'off'
X-Download-Options:
- noopen
X-Frame-Options:
- SAMEORIGIN
X-XSS-Protection:
- 1; mode=block
status:
code: 200
message: OK
- request:
body: '{"operationName": "getCompanyFilings", "variables": {"symbol": "SHOP",
"fromDate": "2023-09-22", "toDate": "2023-09-28", "limit": 1000}, "query": "query
getCompanyFilings($symbol: String!, $fromDate: String, $toDate: String, $limit:
Int) {\n filings: getCompanyFilings(\n symbol: $symbol\n fromDate: $fromDate\n toDate:
$toDate\n limit: $limit\n ) {\nsize\nfilingDate\ndescription\nname\nurlToPdf\n}\n}"}'
headers:
Accept:
- '*/*'
Content-Type:
- application/json
authority:
- app-money.tmx.com
locale:
- en
referer:
- https://money.tmx.com/en/quote/SHOP
method: POST
uri: https://app-money.tmx.com/graphql
response:
body:
string: '{"data":{"filings":[]}}
'
headers:
Access-Control-Allow-Origin:
- '*'
Connection:
- keep-alive
Content-Length:
- '24'
Content-Security-Policy:
- frame-ancestors 'none'; default-src 'self'
Content-Type:
- application/json; charset=utf-8
Date:
- Wed, 27 Dec 2023 22:03:25 GMT
Etag:
- W/"18-YqxSvZ09xIbNEA8V1atJvD3G14s"
Strict-Transport-Security:
- max-age=15552000; includeSubDomains
Vary:
- Origin, Accept-Encoding
X-Content-Type-Options:
- nosniff
X-DNS-Prefetch-Control:
- 'off'
X-Download-Options:
- noopen
X-Frame-Options:
- SAMEORIGIN
X-XSS-Protection:
- 1; mode=block
status:
code: 200
message: OK
- request:
body: '{"operationName": "getCompanyFilings", "variables": {"symbol": "SHOP",
"fromDate": "2023-08-18", "toDate": "2023-08-24", "limit": 1000}, "query": "query
getCompanyFilings($symbol: String!, $fromDate: String, $toDate: String, $limit:
Int) {\n filings: getCompanyFilings(\n symbol: $symbol\n fromDate: $fromDate\n toDate:
$toDate\n limit: $limit\n ) {\nsize\nfilingDate\ndescription\nname\nurlToPdf\n}\n}"}'
headers:
Accept:
- '*/*'
Content-Type:
- application/json
authority:
- app-money.tmx.com
locale:
- en
referer:
- https://money.tmx.com/en/quote/SHOP
method: POST
uri: https://app-money.tmx.com/graphql
response:
body:
string: '{"data":{"filings":[]}}
'
headers:
Access-Control-Allow-Origin:
- '*'
Connection:
- keep-alive
Content-Length:
- '24'
Content-Security-Policy:
- frame-ancestors 'none'; default-src 'self'
Content-Type:
- application/json; charset=utf-8
Date:
- Wed, 27 Dec 2023 22:03:25 GMT
Etag:
- W/"18-YqxSvZ09xIbNEA8V1atJvD3G14s"
Strict-Transport-Security:
- max-age=15552000; includeSubDomains
Vary:
- Origin, Accept-Encoding
X-Content-Type-Options:
- nosniff
X-DNS-Prefetch-Control:
- 'off'
X-Download-Options:
- noopen
X-Frame-Options:
- SAMEORIGIN
X-XSS-Protection:
- 1; mode=block
status:
code: 200
message: OK
- request:
body: '{"operationName": "getCompanyFilings", "variables": {"symbol": "SHOP",
"fromDate": "2023-07-21", "toDate": "2023-07-27", "limit": 1000}, "query": "query
getCompanyFilings($symbol: String!, $fromDate: String, $toDate: String, $limit:
Int) {\n filings: getCompanyFilings(\n symbol: $symbol\n fromDate: $fromDate\n toDate:
$toDate\n limit: $limit\n ) {\nsize\nfilingDate\ndescription\nname\nurlToPdf\n}\n}"}'
headers:
Accept:
- '*/*'
Content-Type:
- application/json
authority:
- app-money.tmx.com
locale:
- en
referer:
- https://money.tmx.com/en/quote/SHOP
method: POST
uri: https://app-money.tmx.com/graphql
response:
body:
string: '{"data":{"filings":[]}}
'
headers:
Access-Control-Allow-Origin:
- '*'
Connection:
- keep-alive
Content-Length:
- '24'
Content-Security-Policy:
- frame-ancestors 'none'; default-src 'self'
Content-Type:
- application/json; charset=utf-8
Date:
- Wed, 27 Dec 2023 22:03:25 GMT
Etag:
- W/"18-YqxSvZ09xIbNEA8V1atJvD3G14s"
Strict-Transport-Security:
- max-age=15552000; includeSubDomains
Vary:
- Origin, Accept-Encoding
X-Content-Type-Options:
- nosniff
X-DNS-Prefetch-Control:
- 'off'
X-Download-Options:
- noopen
X-Frame-Options:
- SAMEORIGIN
X-XSS-Protection:
- 1; mode=block
status:
code: 200
message: OK
- request:
body: '{"operationName": "getCompanyFilings", "variables": {"symbol": "SHOP",
"fromDate": "2023-07-28", "toDate": "2023-08-03", "limit": 1000}, "query": "query
getCompanyFilings($symbol: String!, $fromDate: String, $toDate: String, $limit:
Int) {\n filings: getCompanyFilings(\n symbol: $symbol\n fromDate: $fromDate\n toDate:
$toDate\n limit: $limit\n ) {\nsize\nfilingDate\ndescription\nname\nurlToPdf\n}\n}"}'
headers:
Accept:
- '*/*'
Content-Type:
- application/json
authority:
- app-money.tmx.com
locale:
- en
referer:
- https://money.tmx.com/en/quote/SHOP
method: POST
uri: https://app-money.tmx.com/graphql
response:
body:
string: '{"data":{"filings":[{"size":"95.39 KB","filingDate":"2023-08-03","description":"Continuous
disclosure","name":"52-109F2 - Certification of interim filings - CFO (E)","urlToPdf":"http://www.investorx.ca/Doc/2308150646014439"},{"size":"97
KB","filingDate":"2023-08-03","description":"Continuous disclosure","name":"52-109F2
- Certification of interim filings - CEO (E)","urlToPdf":"http://www.investorx.ca/Doc/2308150645586460"},{"size":"472.49
KB","filingDate":"2023-08-03","description":"Continuous disclosure","name":"Interim
financial statements/report - English","urlToPdf":"http://www.investorx.ca/Doc/2308150645555689"},{"size":"400.83
KB","filingDate":"2023-08-03","description":"Continuous disclosure","name":"A
- English","urlToPdf":"http://www.investorx.ca/Doc/2308150645105523"},{"size":"308.45
KB","filingDate":"2023-08-02","description":"Continuous disclosure","name":"News
release - English","urlToPdf":"http://www.investorx.ca/Doc/2308150617461485"}]}}
'
headers:
Access-Control-Allow-Origin:
- '*'
Connection:
- keep-alive
Content-Length:
- '967'
Content-Security-Policy:
- frame-ancestors 'none'; default-src 'self'
Content-Type:
- application/json; charset=utf-8
Date:
- Wed, 27 Dec 2023 22:03:25 GMT
Etag:
- W/"3c7-ZPQpXSIQW2OoMN8sO4MUkGsZsw4"
Strict-Transport-Security:
- max-age=15552000; includeSubDomains
Vary:
- Origin, Accept-Encoding
X-Content-Type-Options:
- nosniff
X-DNS-Prefetch-Control:
- 'off'
X-Download-Options:
- noopen
X-Frame-Options:
- SAMEORIGIN
X-XSS-Protection:
- 1; mode=block
status:
code: 200
message: OK
- request:
body: '{"operationName": "getCompanyFilings", "variables": {"symbol": "SHOP",
"fromDate": "2023-07-07", "toDate": "2023-07-13", "limit": 1000}, "query": "query
getCompanyFilings($symbol: String!, $fromDate: String, $toDate: String, $limit:
Int) {\n filings: getCompanyFilings(\n symbol: $symbol\n fromDate: $fromDate\n toDate:
$toDate\n limit: $limit\n ) {\nsize\nfilingDate\ndescription\nname\nurlToPdf\n}\n}"}'
headers:
Accept:
- '*/*'
Content-Type:
- application/json
authority:
- app-money.tmx.com
locale:
- en
referer:
- https://money.tmx.com/en/quote/SHOP
method: POST
uri: https://app-money.tmx.com/graphql
response:
body:
string: '{"data":{"filings":[]}}
'
headers:
Access-Control-Allow-Origin:
- '*'
Connection:
- keep-alive
Content-Length:
- '24'
Content-Security-Policy:
- frame-ancestors 'none'; default-src 'self'
Content-Type:
- application/json; charset=utf-8
Date:
- Wed, 27 Dec 2023 22:03:25 GMT
Etag:
- W/"18-YqxSvZ09xIbNEA8V1atJvD3G14s"
Strict-Transport-Security:
- max-age=15552000; includeSubDomains
Vary:
- Origin, Accept-Encoding
X-Content-Type-Options:
- nosniff
X-DNS-Prefetch-Control:
- 'off'
X-Download-Options:
- noopen
X-Frame-Options:
- SAMEORIGIN
X-XSS-Protection:
- 1; mode=block
status:
code: 200
message: OK
- request:
body: '{"operationName": "getCompanyFilings", "variables": {"symbol": "SHOP",
"fromDate": "2023-09-08", "toDate": "2023-09-14", "limit": 1000}, "query": "query
getCompanyFilings($symbol: String!, $fromDate: String, $toDate: String, $limit:
Int) {\n filings: getCompanyFilings(\n symbol: $symbol\n fromDate: $fromDate\n toDate:
$toDate\n limit: $limit\n ) {\nsize\nfilingDate\ndescription\nname\nurlToPdf\n}\n}"}'
headers:
Accept:
- '*/*'
Content-Type:
- application/json
authority:
- app-money.tmx.com
locale:
- en
referer:
- https://money.tmx.com/en/quote/SHOP
method: POST
uri: https://app-money.tmx.com/graphql
response:
body:
string: '{"data":{"filings":[]}}
'
headers:
Access-Control-Allow-Origin:
- '*'
Connection:
- keep-alive
Content-Length:
- '24'
Content-Security-Policy:
- frame-ancestors 'none'; default-src 'self'
Content-Type:
- application/json; charset=utf-8
Date:
- Wed, 27 Dec 2023 22:03:25 GMT
Etag:
- W/"18-YqxSvZ09xIbNEA8V1atJvD3G14s"
Strict-Transport-Security:
- max-age=15552000; includeSubDomains
Vary:
- Origin, Accept-Encoding
X-Content-Type-Options:
- nosniff
X-DNS-Prefetch-Control:
- 'off'
X-Download-Options:
- noopen
X-Frame-Options:
- SAMEORIGIN
X-XSS-Protection:
- 1; mode=block
status:
code: 200
message: OK
- request:
body: '{"operationName": "getCompanyFilings", "variables": {"symbol": "SHOP",
"fromDate": "2023-06-30", "toDate": "2023-07-06", "limit": 1000}, "query": "query
getCompanyFilings($symbol: String!, $fromDate: String, $toDate: String, $limit:
Int) {\n filings: getCompanyFilings(\n symbol: $symbol\n fromDate: $fromDate\n toDate:
$toDate\n limit: $limit\n ) {\nsize\nfilingDate\ndescription\nname\nurlToPdf\n}\n}"}'
headers:
Accept:
- '*/*'
Content-Type:
- application/json
authority:
- app-money.tmx.com
locale:
- en
referer:
- https://money.tmx.com/en/quote/SHOP
method: POST
uri: https://app-money.tmx.com/graphql
response:
body:
string: '{"data":{"filings":[]}}
'
headers:
Access-Control-Allow-Origin:
- '*'
Connection:
- keep-alive
Content-Length:
- '24'
Content-Security-Policy:
- frame-ancestors 'none'; default-src 'self'
Content-Type:
- application/json; charset=utf-8
Date:
- Wed, 27 Dec 2023 22:03:25 GMT
Etag:
- W/"18-YqxSvZ09xIbNEA8V1atJvD3G14s"
Strict-Transport-Security:
- max-age=15552000; includeSubDomains
Vary:
- Origin, Accept-Encoding
X-Content-Type-Options:
- nosniff
X-DNS-Prefetch-Control:
- 'off'
X-Download-Options:
- noopen
X-Frame-Options:
- SAMEORIGIN
X-XSS-Protection:
- 1; mode=block
status:
code: 200
message: OK
- request:
body: '{"operationName": "getCompanyFilings", "variables": {"symbol": "SHOP",
"fromDate": "2023-09-01", "toDate": "2023-09-07", "limit": 1000}, "query": "query
getCompanyFilings($symbol: String!, $fromDate: String, $toDate: String, $limit:
Int) {\n filings: getCompanyFilings(\n symbol: $symbol\n fromDate: $fromDate\n toDate:
$toDate\n limit: $limit\n ) {\nsize\nfilingDate\ndescription\nname\nurlToPdf\n}\n}"}'
headers:
Accept:
- '*/*'
Content-Type:
- application/json
authority:
- app-money.tmx.com
locale:
- en
referer:
- https://money.tmx.com/en/quote/SHOP
method: POST
uri: https://app-money.tmx.com/graphql
response:
body:
string: '{"data":{"filings":[]}}
'
headers:
Access-Control-Allow-Origin:
- '*'
Connection:
- keep-alive
Content-Length:
- '24'
Content-Security-Policy:
- frame-ancestors 'none'; default-src 'self'
Content-Type:
- application/json; charset=utf-8
Date:
- Wed, 27 Dec 2023 22:03:25 GMT
Etag:
- W/"18-YqxSvZ09xIbNEA8V1atJvD3G14s"
Strict-Transport-Security:
- max-age=15552000; includeSubDomains
Vary:
- Origin, Accept-Encoding
X-Content-Type-Options:
- nosniff
X-DNS-Prefetch-Control:
- 'off'
X-Download-Options:
- noopen
X-Frame-Options:
- SAMEORIGIN
X-XSS-Protection:
- 1; mode=block
status:
code: 200
message: OK
- request:
body: '{"operationName": "getCompanyFilings", "variables": {"symbol": "SHOP",
"fromDate": "2023-09-29", "toDate": "2023-09-30", "limit": 1000}, "query": "query
getCompanyFilings($symbol: String!, $fromDate: String, $toDate: String, $limit:
Int) {\n filings: getCompanyFilings(\n symbol: $symbol\n fromDate: $fromDate\n toDate:
$toDate\n limit: $limit\n ) {\nsize\nfilingDate\ndescription\nname\nurlToPdf\n}\n}"}'
headers:
Accept:
- '*/*'
Content-Type:
- application/json
authority:
- app-money.tmx.com
locale:
- en
referer:
- https://money.tmx.com/en/quote/SHOP
method: POST
uri: https://app-money.tmx.com/graphql
response:
body:
string: '{"data":{"filings":[]}}
'
headers:
Access-Control-Allow-Origin:
- '*'
Connection:
- keep-alive
Content-Length:
- '24'
Content-Security-Policy:
- frame-ancestors 'none'; default-src 'self'
Content-Type:
- application/json; charset=utf-8
Date:
- Wed, 27 Dec 2023 22:03:25 GMT
Etag:
- W/"18-YqxSvZ09xIbNEA8V1atJvD3G14s"
Strict-Transport-Security:
- max-age=15552000; includeSubDomains
Vary:
- Origin, Accept-Encoding
X-Content-Type-Options:
- nosniff
X-DNS-Prefetch-Control:
- 'off'
X-Download-Options:
- noopen
X-Frame-Options:
- SAMEORIGIN
X-XSS-Protection:
- 1; mode=block
status:
code: 200
message: OK
- request:
body: '{"operationName": "getCompanyFilings", "variables": {"symbol": "SHOP",
"fromDate": "2023-08-11", "toDate": "2023-08-17", "limit": 1000}, "query": "query
getCompanyFilings($symbol: String!, $fromDate: String, $toDate: String, $limit:
Int) {\n filings: getCompanyFilings(\n symbol: $symbol\n fromDate: $fromDate\n toDate:
$toDate\n limit: $limit\n ) {\nsize\nfilingDate\ndescription\nname\nurlToPdf\n}\n}"}'
headers:
Accept:
- '*/*'
Content-Type:
- application/json
authority:
- app-money.tmx.com
locale:
- en
referer:
- https://money.tmx.com/en/quote/SHOP
method: POST
uri: https://app-money.tmx.com/graphql
response:
body:
string: '{"data":{"filings":[]}}
'
headers:
Access-Control-Allow-Origin:
- '*'
Connection:
- keep-alive
Content-Length:
- '24'
Content-Security-Policy:
- frame-ancestors 'none'; default-src 'self'
Content-Type:
- application/json; charset=utf-8
Date:
- Wed, 27 Dec 2023 22:03:25 GMT
Etag:
- W/"18-YqxSvZ09xIbNEA8V1atJvD3G14s"
Strict-Transport-Security:
- max-age=15552000; includeSubDomains
Vary:
- Origin, Accept-Encoding
X-Content-Type-Options:
- nosniff
X-DNS-Prefetch-Control:
- 'off'
X-Download-Options:
- noopen
X-Frame-Options:
- SAMEORIGIN
X-XSS-Protection:
- 1; mode=block
status:
code: 200
message: OK
- request:
body: '{"operationName": "getCompanyFilings", "variables": {"symbol": "SHOP",
"fromDate": "2023-07-14", "toDate": "2023-07-20", "limit": 1000}, "query": "query
getCompanyFilings($symbol: String!, $fromDate: String, $toDate: String, $limit:
Int) {\n filings: getCompanyFilings(\n symbol: $symbol\n fromDate: $fromDate\n toDate:
$toDate\n limit: $limit\n ) {\nsize\nfilingDate\ndescription\nname\nurlToPdf\n}\n}"}'
headers:
Accept:
- '*/*'
Content-Type:
- application/json
authority:
- app-money.tmx.com
locale:
- en
referer:
- https://money.tmx.com/en/quote/SHOP
method: POST
uri: https://app-money.tmx.com/graphql
response:
body:
string: '{"data":{"filings":[]}}
'
headers:
Access-Control-Allow-Origin:
- '*'
Connection:
- keep-alive
Content-Length:
- '24'
Content-Security-Policy:
- frame-ancestors 'none'; default-src 'self'
Content-Type:
- application/json; charset=utf-8
Date:
- Wed, 27 Dec 2023 22:03:25 GMT
Etag:
- W/"18-YqxSvZ09xIbNEA8V1atJvD3G14s"
Strict-Transport-Security:
- max-age=15552000; includeSubDomains
Vary:
- Origin, Accept-Encoding
X-Content-Type-Options:
- nosniff
X-DNS-Prefetch-Control:
- 'off'
X-Download-Options:
- noopen
X-Frame-Options:
- SAMEORIGIN
X-XSS-Protection:
- 1; mode=block
status:
code: 200
message: OK
- request:
body: '{"operationName": "getCompanyFilings", "variables": {"symbol": "SHOP",
"fromDate": "2023-09-15", "toDate": "2023-09-21", "limit": 1000}, "query": "query
getCompanyFilings($symbol: String!, $fromDate: String, $toDate: String, $limit:
Int) {\n filings: getCompanyFilings(\n symbol: $symbol\n fromDate: $fromDate\n toDate:
$toDate\n limit: $limit\n ) {\nsize\nfilingDate\ndescription\nname\nurlToPdf\n}\n}"}'
headers:
Accept:
- '*/*'
Content-Type:
- application/json
authority:
- app-money.tmx.com
locale:
- en
referer:
- https://money.tmx.com/en/quote/SHOP
method: POST
uri: https://app-money.tmx.com/graphql
response:
body:
string: '{"data":{"filings":[]}}
'
headers:
Access-Control-Allow-Origin:
- '*'
Connection:
- keep-alive
Content-Length:
- '24'
Content-Security-Policy:
- frame-ancestors 'none'; default-src 'self'
Content-Type:
- application/json; charset=utf-8
Date:
- Wed, 27 Dec 2023 22:03:25 GMT
Etag:
- W/"18-YqxSvZ09xIbNEA8V1atJvD3G14s"
Strict-Transport-Security:
- max-age=15552000; includeSubDomains
Vary:
- Origin, Accept-Encoding
X-Content-Type-Options:
- nosniff
X-DNS-Prefetch-Control:
- 'off'
X-Download-Options:
- noopen
X-Frame-Options:
- SAMEORIGIN
X-XSS-Protection:
- 1; mode=block
status:
code: 200
message: OK
- request:
body: '{"operationName": "getCompanyFilings", "variables": {"symbol": "SHOP",
"fromDate": "2023-08-04", "toDate": "2023-08-10", "limit": 1000}, "query": "query
getCompanyFilings($symbol: String!, $fromDate: String, $toDate: String, $limit:
Int) {\n filings: getCompanyFilings(\n symbol: $symbol\n fromDate: $fromDate\n toDate:
$toDate\n limit: $limit\n ) {\nsize\nfilingDate\ndescription\nname\nurlToPdf\n}\n}"}'
headers:
Accept:
- '*/*'
Content-Type:
- application/json
authority:
- app-money.tmx.com
locale:
- en
referer:
- https://money.tmx.com/en/quote/SHOP
method: POST
uri: https://app-money.tmx.com/graphql
response:
body:
string: '{"data":{"filings":[]}}
'
headers:
Access-Control-Allow-Origin:
- '*'
Connection:
- keep-alive
Content-Length:
- '24'
Content-Security-Policy:
- frame-ancestors 'none'; default-src 'self'
Content-Type:
- application/json; charset=utf-8
Date:
- Wed, 27 Dec 2023 22:03:25 GMT
Etag:
- W/"18-YqxSvZ09xIbNEA8V1atJvD3G14s"
Strict-Transport-Security:
- max-age=15552000; includeSubDomains
Vary:
- Origin, Accept-Encoding
X-Content-Type-Options:
- nosniff
X-DNS-Prefetch-Control:
- 'off'
X-Download-Options:
- noopen
X-Frame-Options:
- SAMEORIGIN
X-XSS-Protection:
- 1; mode=block
status:
code: 200
message: OK
version: 1

View File

@@ -0,0 +1,73 @@
interactions:
- request:
body: '{"operationName": "getNewsAndEvents", "variables": {"symbol": "SHOP", "page":
1, "limit": 5, "locale": "en"}, "query": "query getNewsAndEvents(\n $symbol:
String!,\n $page: Int!,\n $limit: Int!,\n $locale: String!\n) {\n news:
getNewsForSymbol(\n symbol: $symbol,\n page: $page,\n limit: $limit,\n locale:
$locale\n ) {\n headline\n datetime\n source\n newsid\n summary\n }\n events:
getUpComingEventsForSymbol(symbol: $symbol, locale: $locale) {\n title\n date\n status\n type\n }\n }\n"}'
headers:
Accept:
- '*/*'
Content-Type:
- application/json
authority:
- app-money.tmx.com
locale:
- en
referer:
- https://money.tmx.com/en/quote/SHOP
method: POST
uri: https://app-money.tmx.com/graphql
response:
body:
string: !!binary |
H4sIAAAAAAAAA62VbU/iQBDH39+nmOs9RJO22bYUWnyFPCi5E5DiUy6+WOkiG8su2W7Bxvjdb1bE
06jcqZc0LW1m/vPf38wuN1ZKNbXqN5Zgy9yq/7qxpoymGRfMqlvJVM75pIQDpsZTKnQOLcUXDIZs
LFXq7PPLKXyN3QB2eZZxKYAL2M3o+Ao6iqe0dJrlBVNwIAW+QEIzllu2qcg0n5kCPvEDx/McPxqR
qE4IXg4J8Y5huSzU2AT10NmEZwwWnMJhITU7YCmnGGI889SqV2rEj0lEopAEYRBFmFzMZlSVmN0V
minBtA3tBVPlcsoUc5ytB9GmVHMXHOjJBZsZt35kg/G1jR/XALpi7MJW7yxp2zBKTuuQ7PcH2zZQ
mCvXda1b+wm30ZSV0OIpdDU0LikXn+E5yl3F6NUTXPdcYck1cq243mOum+iFhl4QvJNejdTimh95
Vc8L4ijyPkYv/EPvL/heo3fMlGbXMKBKC6byFY61lJaQaCQ3M8Gwl8kLmsGIXqOT2TzjVOCiX4RE
qhtGzOgws6glVxtIhaHnk2rskbDi4xU8JvWj29uDfgcGw6Mk6TZs9O/ahosLpLpiAlt7P/u7bei1
T5KT7rCNjBASrNZrrygBcmokrcZh/bg9PN2Gre9frn3iNXeOXyC1htIQQha48hxGU44b87BAdqYZ
pmiHC6TCEdOQ5UWmXxki4o9Ibc2n8uYhCgOv4tWq1TBAPb/2ZIiGbMFEweBojuPxDahIYU/JPIeB
khOuzfeg+g3OGFVOH8fMMb92EAXuC4YzRvMpdDK5hIHMuTYH0EQq6KA5HIymFPl/Q2PUJjjimIBl
swxaCOrO8NF8LGdcXGKXFizXWL9Fy5dIEsf7IMm4FnmkEofVgAReGL9nO/bHWprd6PlvO8vkHclz
2zId06v/A811Zpw/EMDmCHzmyGXFdoax9yisuiiyDC1rqot8/abLuVFYJ9716plsyhc8ZYh6TktZ
/JPgOuUVRWwTnq05HgsPQ79R7j7y9vz29tNvghRykxkHAAA=
headers:
Access-Control-Allow-Origin:
- '*'
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Security-Policy:
- frame-ancestors 'none'; default-src 'self'
Content-Type:
- application/json; charset=utf-8
Date:
- Wed, 27 Dec 2023 22:03:26 GMT
Etag:
- W/"719-/eLbzQXgYik/dQCOz8PB4QsD1w4"
Strict-Transport-Security:
- max-age=15552000; includeSubDomains
Transfer-Encoding:
- chunked
Vary:
- Origin, Accept-Encoding
X-Content-Type-Options:
- nosniff
X-DNS-Prefetch-Control:
- 'off'
X-Download-Options:
- noopen
X-Frame-Options:
- SAMEORIGIN
X-XSS-Protection:
- 1; mode=block
status:
code: 200
message: OK
version: 1

View File

@@ -0,0 +1,147 @@
interactions:
- request:
body: '{"operationName": "getQuoteBySymbol", "variables": {"locale": "en", "symbol":
"RY"}, "query": " query getQuoteBySymbol(\n $symbol: String,\n $locale: String\n)
{\n getQuoteBySymbol(symbol: $symbol, locale: $locale) {\n symbol\n name\n price\n priceChange\n percentChange\n exchangeName\n exShortName\n exchangeCode\n sector\n industry\n volume\n openPrice\n dayHigh\n dayLow\n MarketCap\n MarketCapAllClasses\n peRatio\n prevClose\n dividendFrequency\n dividendYield\n dividendAmount\n dividendCurrency\n beta\n eps\n exDividendDate\n shortDescription\n longDescription\n website\n email\n phoneNumber\n fullAddress\n employees\n shareOutStanding\n totalDebtToEquity\n totalSharesOutStanding\n sharesESCROW\n vwap\n dividendPayDate\n weeks52high\n weeks52low\n alpha\n averageVolume10D\n averageVolume30D\n averageVolume50D\n priceToBook\n priceToCashFlow\n returnOnEquity\n returnOnAssets\n day21MovingAvg\n day50MovingAvg\n day200MovingAvg\n dividend3Years\n dividend5Years\n datatype\n issueType\n qmdescription\n }\n}\n"}'
headers:
Accept:
- '*/*'
Content-Type:
- application/json
authority:
- app-money.tmx.com
locale:
- en
referer:
- https://money.tmx.com/en/quote/RY
method: POST
uri: https://app-money.tmx.com/graphql
response:
body:
string: !!binary |
H4sIAAAAAAAAA61Uy27jNhTd9ysIbbqorBEp6uWdIydogc7k4XTaLGmJsYhIpEJS9giD/HsvaSVK
AnRXwwvp3Ad5j849P4OGWRasfwYHbm9HZfnFtJv6veocZuan4O4hCAPJeu6e1cQ6dMHkE1KPqGKS
NQyigxY1hHFCI5LMr1XL5AHAOEozgLiuubQLSHFRUhIG/EftsW/nA+6VVtIqtLOqfkKXczBwebtW
afuatvsnWGor1Syg4bVVGl6vhGSydrVCNqOxegLQXV3IA4BH1Y2uF8mygmIaBmrg8uZ1kCTK0xD4
mX4Xh/Y8WVZ64E91mhPC4CvTT9xWbACkKEhS4iLPc/IusOm6qmPGcONT8jLLcpLQ1DFyx6xQAOMo
96TxY9UpMx+fQftGHEXDZXOl+fPIZe0GuB2Ztlx3U7DEHwTvmmBNI5ws4KZXo7TQLEqKBa1GredO
1WYLPfbcSSCOCppSAhPywd0Uvk9Rwi93JG/n2i2zjmYSE7qK8YpQFMdr/49iR7z7Pltuai0GGEz+
h1yQMEhJ7gDbcmRPCnVMH7ixaA+JBgk5Z0bQtFPy8L/2RH9Yl80QMMK1EY+CN+jRS0VAX8P1ESRg
UK36gckphJ6PXINkEEjYKAk5TDYu3IOkXcn+LKkQnTjrbLvq4aAD70Htb91CuIAZtZNjCKV6UBq4
XCp9RzYIC+16Lx3zVhuh+/ac6u5dK+n2yJU3y1RwtrAtYk0jHEvQBcSsnbykn92R8le0i/xBCt40
NAJ1aAH9geUT3xvhP25r7WDWX76cTqdI7+sI5nSL1jPhrMCtegtEfxv7PXc79htGFGeozOmqSMoU
4o9j122aRnMDOgowuulYzdF30XV8BVsheFi9zb/jtQb56elXg7Z8AGE71kL0FSxAA5khuq1CVG38
DYZOTdytUVAUKcVecEzz69HuLIzllnqNaUySLIkx6NYqYHPL9/ZeXT6Pwk5O5bicAztXaz4XZynO
CMFza3O5q+6u/w7WEmYCwzj5PXceR7OYZMtS3bDpw26QFUk8q/zJpKQ9OwiF04s3sPMuEudRCW7B
uqH1SxjHSVqAFTGQJmjou7coHG+DdUpoipPsUyhxIRrHaZl+DqU+lGGaOG/zjnyvLpR6co6QY/yG
Vcy0V/46qyQCX4IvMmp5LV9JwzSK6QJvwMuscZfNM++HBH9VRyBwc3QckgyGoHMojd+HcBnRrIzP
LkriDzFCIhqnRbpwmjxwpuGcPHJ++oqmMwqeWfpGltlpcMzz83XB7I0Z+f0ZrHYAPPfNBwPZvlt8
ZyMmeHl5+eVfcjLaKQ0HAAA=
headers:
Access-Control-Allow-Origin:
- '*'
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Security-Policy:
- frame-ancestors 'none'; default-src 'self'
Content-Type:
- application/json; charset=utf-8
Date:
- Wed, 27 Dec 2023 22:03:23 GMT
Etag:
- W/"70d-fHugecv56LrRY9XFNFeamlQimkE"
Strict-Transport-Security:
- max-age=15552000; includeSubDomains
Transfer-Encoding:
- chunked
Vary:
- Origin, Accept-Encoding
X-Content-Type-Options:
- nosniff
X-DNS-Prefetch-Control:
- 'off'
X-Download-Options:
- noopen
X-Frame-Options:
- SAMEORIGIN
X-XSS-Protection:
- 1; mode=block
status:
code: 200
message: OK
- request:
body: '{"operationName": "getQuoteBySymbol", "variables": {"locale": "en", "symbol":
"NTR"}, "query": " query getQuoteBySymbol(\n $symbol: String,\n $locale: String\n)
{\n getQuoteBySymbol(symbol: $symbol, locale: $locale) {\n symbol\n name\n price\n priceChange\n percentChange\n exchangeName\n exShortName\n exchangeCode\n sector\n industry\n volume\n openPrice\n dayHigh\n dayLow\n MarketCap\n MarketCapAllClasses\n peRatio\n prevClose\n dividendFrequency\n dividendYield\n dividendAmount\n dividendCurrency\n beta\n eps\n exDividendDate\n shortDescription\n longDescription\n website\n email\n phoneNumber\n fullAddress\n employees\n shareOutStanding\n totalDebtToEquity\n totalSharesOutStanding\n sharesESCROW\n vwap\n dividendPayDate\n weeks52high\n weeks52low\n alpha\n averageVolume10D\n averageVolume30D\n averageVolume50D\n priceToBook\n priceToCashFlow\n returnOnEquity\n returnOnAssets\n day21MovingAvg\n day50MovingAvg\n day200MovingAvg\n dividend3Years\n dividend5Years\n datatype\n issueType\n qmdescription\n }\n}\n"}'
headers:
Accept:
- '*/*'
Content-Type:
- application/json
authority:
- app-money.tmx.com
locale:
- en
referer:
- https://money.tmx.com/en/quote/NTR
method: POST
uri: https://app-money.tmx.com/graphql
response:
body:
string: !!binary |
H4sIAAAAAAAAA61VS4/bNhC+91cQAooeaiui3vapjr1BgWZ3k5XTNkdampWIpUiFpOyqwf73DiX5
se21hg/mN5yPM988/N2rmGXe+rtXg/3cKwvvh2JoD0o4zMy/vIf9k7fwJGvBHXqrOUjy0VY+op3m
JcJZ4q/S+bRtmKwRC/wwQwh0CdJewShJM5ovPPirHLGHiXevtJJWkcKq8oXczUbP3Ssape35WvGn
d/XdquoKGiit0ni8ZxY0Z8IgyGXVG6sHhDc1RtcL22tHe1Sid4xZuMqDYOGpDuSnOZl4TKZiw6+8
bvCc+nE8nj+q08V8z/QL2C3rvHWUJWmS02QVZzf4RoitYMaAmW7EaUpzSp0kT8xy5a1p6OfurOG4
FcpMOqYrfIofeQWy+qDhWw+ydOF/7pnGvMTgXe1fOYgK2f08S67oplW9tE7rJLqi217rmepLsUOS
A7jaB/4qCsMIH4UOA8U6hiv85KFTeTf77lBR9AuDMFrScBnmJAjW49cPnPKuQDswpeYdJiZv2oQb
YhsgJ6VF9ZMhgukajCXPoC0X/G/QpNOq6kv8cRhIyTpWcju4zhJK1v8rJzn7z9aJxTYagLSMS1Jq
1RE5XbJGcqtVDXJBOmWZaRaEyYp0jTJdg3IwYRvV1w3h1kzuz6rsjQvufP/UgAa0nwOuhTowQQSw
CkNDDy6NZUJAdQmSnLhtiHbEYiBh8CMxDdPgkz36l6rtmBwcHTa3GjnPybNLc+MLGivLxfSGu/QF
c8FHCotxmwUxIASX9Y1gCI7Jlw20vERydwkqM+VsQB+506viGmcMA8M5fWa6JZiwVS36Oxkvahww
lJclei5b7AumCV7S6O64lMSngXSC2WelW+MqfYKD4WODNdZ2Zv3u3el08udC+Ji1m3iU2O0iLo/g
6Mwvb+1YFwkPfXsAtwF+piQKUrKKomWeBK5Fn3shNlWFYRjXyJQSukKlC4vVt+SOGbsoegyC0Ay3
ASmYeWFWKax+8duCbDdjCJ1QA8BIEE20Y3Uee4vSygo19dbxKk7iLIpxpVjsA7GDg92ru29IPrhp
S2e8cJ7mP65RSON05jV3xfbp8Q9vLTF43Fgnt2twGQXBiqbBzWh/YsN1QuNlQHFIR13hxSRhMy4x
SiM/Ci+gGDcZhpPj4mACW9oFFwTUrTZ2BM1q+H1ckTTYeeswSWnsdsIbU+RMNA7iVZT9y5SMpjAN
siSb/xf26r1SL4j6mOgF2+KofBijWWIk2Lm9lo/yrFfu9DqDG9ylFtWPfZqM6zik9+qI0m2O9biW
wyhN02lTJ8GtKfVpOrsEt4Y88gMs11XJ6CswPa7BlF7RZEaXgZ+PNDhIQ+f0hilO/J8xpof9BG4L
BL611Zvltbkd0KLvOsGxk15fX3/4BzRtXseGBwAA
headers:
Access-Control-Allow-Origin:
- '*'
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Security-Policy:
- frame-ancestors 'none'; default-src 'self'
Content-Type:
- application/json; charset=utf-8
Date:
- Wed, 27 Dec 2023 22:03:23 GMT
Etag:
- W/"786-J6jDjEdFePspLg4uw0/rmblQy0Q"
Strict-Transport-Security:
- max-age=15552000; includeSubDomains
Transfer-Encoding:
- chunked
Vary:
- Origin, Accept-Encoding
X-Content-Type-Options:
- nosniff
X-DNS-Prefetch-Control:
- 'off'
X-Download-Options:
- noopen
X-Frame-Options:
- SAMEORIGIN
X-XSS-Protection:
- 1; mode=block
status:
code: 200
message: OK
version: 1

View File

@@ -0,0 +1,75 @@
interactions:
- request:
body: '{"operationName": "getQuoteBySymbol", "variables": {"locale": "en", "symbol":
"SHOP"}, "query": " query getQuoteBySymbol(\n $symbol: String,\n $locale:
String\n) {\n getQuoteBySymbol(symbol: $symbol, locale: $locale) {\n symbol\n name\n price\n priceChange\n percentChange\n exchangeName\n exShortName\n exchangeCode\n sector\n industry\n volume\n openPrice\n dayHigh\n dayLow\n MarketCap\n MarketCapAllClasses\n peRatio\n prevClose\n dividendFrequency\n dividendYield\n dividendAmount\n dividendCurrency\n beta\n eps\n exDividendDate\n shortDescription\n longDescription\n website\n email\n phoneNumber\n fullAddress\n employees\n shareOutStanding\n totalDebtToEquity\n totalSharesOutStanding\n sharesESCROW\n vwap\n dividendPayDate\n weeks52high\n weeks52low\n alpha\n averageVolume10D\n averageVolume30D\n averageVolume50D\n priceToBook\n priceToCashFlow\n returnOnEquity\n returnOnAssets\n day21MovingAvg\n day50MovingAvg\n day200MovingAvg\n dividend3Years\n dividend5Years\n datatype\n issueType\n qmdescription\n }\n}\n"}'
headers:
Accept:
- '*/*'
Content-Type:
- application/json
authority:
- app-money.tmx.com
locale:
- en
referer:
- https://money.tmx.com/en/quote/SHOP
method: POST
uri: https://app-money.tmx.com/graphql
response:
body:
string: !!binary |
H4sIAAAAAAAAA7VVUW/bNhB+368gDAxdMVmVZEmW8zRXSdYCTZxFRrc+0tLZIkyRDElZVYv89x0l
23Ez9HF5cfjdHe/u492n75OKWjq5+j7Zgf2rlRbe90XfbCR3mDn+Nyk+rB4m3kTQBtyplopte/JR
lD7JOTWGLEnRbqSumKAWyGdpmdiRoqYaDMYpzUoMDIOZH2fHY15TsXOgn4YIgS5B2BcwyYI4Tr0J
fC0H7H5MvZZaCitJYWW5JzdH48T5YVXantyKfyYvsbmsXkADpZXaHaGsheRy1yPKRNUaq3vXndza
DgtH9CB56+6LgiyNktibSAXi4dyMh+T1H9iuHlvLogH4JDt3Dv0Fnu+o3oPNqUIoirNsMUvTZJFe
GJacDxQiUVfhLMoWSRgkiYtV8Egtk1jSQCEcci4NjHdnc8zFDqwCUd1qeGpBlFi8aDl/MXxhwKvX
4LKRrbCv0bzV+vKKDbipiPwgSechNgoKy5uGfhQG7s8hX6+Psdf45GORxj3BNZhSM4WVi4tZkdst
aEOoIDAtZdO49yaKU7uVuiE4EQ3VjPcE39Y0lHP0rEjDKsO+Adm0hglwHPmYhUux+3+TkHUNZMsw
pqaG2A7dYdfgfJorYtrNOTUxOCDuH0N+i2e/Yn6MMiXlJArCjOCTgWjh7ZgGi8FptJcxyfynMWMN
P0l2rIZgC7Iz5NT+KYVxDZYSR7q0l0zgHZQckAKwjqszM8YjTJS8rdzSWkyLEYqK/o0hHWwMs+AR
VfeGuSoNbg9ghJJq2qrzcc+k2eOvkSVDLwHImt5jj7e0hI2U+7feQMOyod+k8Mndf+nAnSO0qqZY
ptLSFW8IljdUdH5EW1NLtrRknFknNRftuevHPuBMyQPth3fzzkhRM6Ww0bGcE4qLiPdxN17HlnGs
amuVuXr3rus634yOPmZzytJQ5nTx4+MfPxpULQXct80GnMT8HpI0nJEoDqdRFmVo3+J2LasKKcN9
moRJSFZvcikEtllYDWC9PzXuZ0VuuZTaIytraUfx994j+XLIrLjsAYZwt4rD2iF1q9YWFlvC1pzW
BClqzTzFRbUSG7uGjV3Lm6eWWdzxwA/jo2HU6FfBWRwlWeakerja3BT54+rvkzYcukHOUPGiJAmc
10lFkO1RDEbHDmBvkqg+KmTqp9kZ5E4l49gPUAspVzV1VQUR6iwqHz2Apjv4PMhvGFyjJ7aTxcEr
08yZZukMq0hemZLRtIijeXL84qzle5xDrCT1g3h2BnNq6tuhnHnio68G22qxEieypjP0j17wJUqE
dWoYZX40fgOi8E4ekLvlwdEXIL1pHGWDKQkuTFnmJ8nREAWXlvnCj5PggsrZF6DajLJ6wpIjFnjD
V9v2yk0pjHXiN8yYFtYjmBcIPDXVDzK5VIrjDg9icv7KPT8///IvYC+baQYIAAA=
headers:
Access-Control-Allow-Origin:
- '*'
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Security-Policy:
- frame-ancestors 'none'; default-src 'self'
Content-Type:
- application/json; charset=utf-8
Date:
- Wed, 27 Dec 2023 22:03:26 GMT
Etag:
- W/"806-6HBMmFREzLBa0L/cA9O/rZwtqtA"
Strict-Transport-Security:
- max-age=15552000; includeSubDomains
Transfer-Encoding:
- chunked
Vary:
- Origin, Accept-Encoding
X-Content-Type-Options:
- nosniff
X-DNS-Prefetch-Control:
- 'off'
X-Download-Options:
- noopen
X-Frame-Options:
- SAMEORIGIN
X-XSS-Protection:
- 1; mode=block
status:
code: 200
message: OK
version: 1

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,109 @@
interactions:
- request:
body: '{"operationName": "getStockListSymbolsWithQuote", "variables": {"locale":
"en", "stockListId": "TOP_PRICE_PERFORMER"}, "query": "query getStockListSymbolsWithQuote(\n $stockListId:
String!\n $locale: String,\n) {\n stockList: getStockListSymbolsWithQuote(\n stockListId:
$stockListId,\n locale: $locale\n ) {\n stockListId\n name\n description\n longDescription\n metricTitle\n listItems\n {\n symbol\n longName\n rank\n metric\n price\n priceChange\n percentChange\n volume\n }\n totalPriceChange\n totalPercentChange\n createdAt\n updatedAt\n }\n}"}'
headers:
Accept:
- '*/*'
Content-Type:
- application/json
authority:
- app-money.tmx.com
locale:
- en
referer:
- https://money.tmx.com
method: POST
uri: https://app-money.tmx.com/graphql
response:
body:
string: !!binary |
H4sIAAAAAAAAA5WZ227jRhKG7/cpCF8sEmBM9PngO0mWD7uyxpbkmWQWi6BH6pG5I5EOSTlxgnn3
VMunriY9zmaAGKJOH6ur/vqr9OfByrXu4OjPg6atll8nRdOiB+erg6ODxfvLXy5n56PxL5fj2cn7
2cV4dvDuoHRbH56sbrPLulj67NLXX6p662t4cuWbZV3ctkVVwmvm4eOarL1xbXbj7nxW7drbx1ev
4LLPtq7+6tsc3rmpyvUxevdi/lPmylUGfz9kTfpRG1evfdNmt3uGtSvKJvvh8322qjbwVHbnNjv/
Y1aU+6+5dfBKTrKVu2/Cl219C29bFO0m3Ao8cezuH+9mdOPKtQ9AIQyt3zYHR/+ByNxvP1cbePFo
fv1IO30IxKgqm9bDlwbsbF59aX9ztc/Oy2X4ptqVXw+O6NNXwuuVyYUxkkl4dg9/cMSZFTnVj48f
CY6EylW45uulL9unqzQXXFMi3x3cVZtdIGBCKvrtXcR4Ov8ZM64r75r7bNKuXphYxMRZbhghRL8w
UWlzyhOkQ/h20UE6JLmxysRMwmjOEdN4coKZxoeT7KQoXbks3CYbVfVtVT/EcFJsi9avnkl5RMp0
HkeOEoiStQkmEw/XMCYDdskQpVCIcbQYJWfrSrcqXJktCjjS7zGKmJHnShBCI05mSMJIOnzkBUwa
xHU2PcZcQ9+6y7rKpq7d1RC9U9dAwt35uvHZxMNft4YCO3bF5j4beiiH8eLkGVXGqDTn2lobHbwx
uUpYec5YB5fnWlOu40Sk1AiJI/qvRRrRel39z7f7+oCIbrcQzFDlH1xduM8bn32o2qJcZ/MbqKLm
GVrF0JBvkJ5SRXlAZa5SbJpL2VM/XBrOTRRsYSVD1MOL90m8gSGrvmQXVdnW3m2esXSERW2uDRRR
jMWhYEwHi3XLmuRWSy4iLEoYYxqHc/hzmgjV/So7ravdbTb39R18S4OVx2BAwuGsTZSX4fxwibNc
074SBz4Th80YTfBZn5+ep9K42RSQlIDU+rrcV07I1j3vPgHmu89VvQIVaF87eBvfgMkJg2y1UYQ1
yWV68DJnpidfqRVS2+jgJaX4FhYn58ktwJWEHgs7ifFUbhTQxfpkKAQuwQNd78MzlBgWBZhTzRnm
myW6PqvugegpO/dq5V7Y4q5DRR7Q4kKnXOQsVfjHaKbJKaixgkWlrpQRVGCFvxomCn81TKLFMBGo
IuNRtIzKJU3zkfSVC1zllFIeIUFmK1wtH0dTDPQRbECQ8bL0y3CaSa3QuNFQnhMJAYuKhVrZzTWS
0642kpwoKqJatlRIDDcdYLbpU369dppxh6E0Jxr0IS4EQrrBg0t9hxlyNIKTFg7X4lqezl7phM+c
M+guv4FtAg2/deX9C2jcXygBWWZKiwhUqYd2koD2kXJOrYlQFSNU4N44ungF9HwLH1c8BfT9l32z
CV/wQqowaZSJij/UaJKJousq4KpWXIYif6GUVCpsLebjxFrMPfRrcKUjd1u0HVlBfYVETpH+Xz4C
28LR6ftUn9d+Wf1dJaZxL7G5VVhOpAZx7hyr4X0ONjgHGmEqrvGpzhdJ6c5bByq8TMJkEREEhxBk
DmVO0/5G+kw1XBSU6RhJK8uxvn08OU3kJJzfSe0aX4NB3H6GP6MqT1x23CAMBC1x2SAzaZ6B4PXV
AoPcV1GWUXhsEt8yvE6ybFhX1dcvhd+ssuGuKUrfNMjGjjYOrgyy8e/L/RftHdjbqcAouiswUEbq
OEtz3pWibiaAqQBp4LEUCY2DPrp8pbwv3bL4Uiyzf7uyAfc7Ktr7jilnDGFSC8FnOD94x/8E7e6r
ciKVZVGGWAYpglVzMZokLXBxnD0TT+BytvD1NjvxKx9s+7AC4xtbc8YxMEs6NoMGJDp2sitK4Hy5
0lFzFCpxktMP48TpTHd3vkDJ8TfyIG5KUafkvY2yRwtYzo01Kh4iGKeSJ2IwScUTBm4QrrNqA3jr
pI2zuAFp8OQ4iAJseleneJ8oaGG1iEuOSGmxpn66SAYcmB2y8a87ON2PvljftNnppvq870Iwm13A
3LZpsjO/CvNZW2WjwTHAr/zvOA0UugMVpgrCI6kFH5yayu/PkzKdy4ajn5LCOocpcuN+h4FxUy2/
ghwUZR+aRmiSk0TQbK+Fo70jhVREydjCIcartJgu3PKrL/8o/EtNnVYw7JZb+NiH8tpXVA+1QdSc
p0IA7SyV4ZCaPfkahlyrosqCj0LYk9Q6TaCCtq6OS+sFzCIwGs2OSkD+dfKU9JpNTiyJ4mg4SUro
ap5AzaB3uR1AtNkQ/r9KZzRUUZwgSgKDikbh4zlLdzChpfb1WcaFjoccyTVJWIfpGmZYtMsK0vFk
B6f7w+LG//iChlZruaWoSjrTzXfNkiIGm7ar2XjxWgoe+zu/qW6hjC/2O8wmm3mI2xjC2vqeFOQM
gxILhyZwM6JdK0C64g4XjQS/EuUgHhKv58lZ75rGb57EB58sR1Thc5WIqIR8MCOJV+8WBoUMgIkw
gtJcanywZ8PTZH1xVtXFH2EOGxbr7Ni1Lvtndubq1eMGtRNDgWg1wbsABsreKeIey27BFVKCdi2M
EBzE4SCZKyInNYBwtnDspVv7B+kBx/dspR5NyCsdk0t0Cwrm6NhD81x0jZPq22JBm7Q2GjEFPE68
09l1kr1nuzYzex2KMkAhIBEpEOO5TStI9E3jjOaaWsaiVskkI8Im5TQ4e62arufZJCz0wzgU2mdw
c8/nn/0AbfLwoWlGla8ROFdC86j6YQLrZEP/OMzBSMfqyRNBH05n41dzYeaLsgk6uvQ4DZCjfiMn
DLoTFloKxVmhunsa0S1C8NMEhqAoKzRL1vDD6au30jcXvAFuETi1ispY0wDcdK2W7rawsJSVNt7m
WEpgrEbog+NxAh+uLGYDLGmCYKiw3jYRFGfdn1rAoPRuvcI74zWOsRzPqYNxIhODdVmAMx27NRz7
RRi4uj8VxC1L5lbC9BtNrVr0zdE9G0M4bRhZTSRjwlhpWTLzJyvDkdvsjdN+B4tGVcEwF6WMRZ4e
5vvu70HkoWf1LOcEMVEzgBLDh/nT1RXmKh5yK5sO5seDqwxG+bcEQHDEq3gy/TOTy64rMX0HrTRn
8UoMmqBJNsPXPaPddT7Po7FuUXsHWnDfneuEQKhyX+HxFsB0FxW9P7UF3ecmFlpqkh325CKpkslu
C3kYbd1fsOR3saCfdg1oz48D0GRhTIrGeCGV0gjqIpk2wIvAm+F0+0yxUBiLcqpMNP6AMyY9y7me
NnlIc2jsNF7sGGmTVj8aJavYx7VY2BfuoJrdy6r4b4zEQifweNSQtju60b6VFAsWURBkUgyReOHw
KTVUr02fpzAp9xgqYRCsCIMmj7RS694FWjcFIC+sgn/xEhQ3/09pwQfUqWtW7td9sT82/cfRePHa
aCwsIqZhRa9wJelu0fdUUhj5qY5/DmJGEDwlf7o+eyO84FvOwPa3N2BcwLK+NdhLguj3/tXEa+cu
OukLNqFC4BU+ld/+++6grcDiX6LcouTxN7On/55e1RGW8DvGEiQMetWgPQg/szJ+SNkh0wvKjwiB
zgpHzz4B7u529fbLvn379o+/AIiEcMhoIgAA
headers:
Access-Control-Allow-Origin:
- '*'
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Security-Policy:
- frame-ancestors 'none'; default-src 'self'
Content-Type:
- application/json; charset=utf-8
Date:
- Wed, 27 Dec 2023 22:03:31 GMT
Etag:
- W/"2268-e3rt4kQv8I6aOB/LKVc+TM3z6Dk"
Strict-Transport-Security:
- max-age=15552000; includeSubDomains
Transfer-Encoding:
- chunked
Vary:
- Origin, Accept-Encoding
X-Content-Security-Policy:
- frame-ancestors 'none'; default-src 'self'
X-Content-Type-Options:
- nosniff
X-DNS-Prefetch-Control:
- 'off'
X-Download-Options:
- noopen
X-Frame-Options:
- SAMEORIGIN
X-WebKit-CSP:
- frame-ancestors 'none'; default-src 'self'
X-XSS-Protection:
- 1; mode=block
status:
code: 200
message: OK
version: 1

View File

@@ -0,0 +1,105 @@
interactions:
- request:
body: '{"operationName": "getDividendsForSymbol", "variables": {"batch": 500,
"page": 1, "symbol": "TD"}, "query": "query getDividendsForSymbol(\n $symbol:
String!\n $page: Int,\n $batch: Int\n) {\n dividends: getDividendsForSymbol(\n symbol:
$symbol\n page: $page\n batch: $batch\n ) {\n pageNumber\n hasNextPage\n dividends\n {\n exDate\n amount\n currency\n payableDate\n declarationDate\n recordDate\n }\n }\n}"}'
headers:
Accept:
- '*/*'
Content-Type:
- application/json
authority:
- app-money.tmx.com
locale:
- en
referer:
- https://money.tmx.com/en/quote/TD
method: POST
uri: https://app-money.tmx.com/graphql
response:
body:
string: !!binary |
H4sIAAAAAAAAA52cQW/cNhCF7/0Ze/YGQ1ISKd+K5hz0XvSwibdtAMcJHLtIEPi/V+JwVe/Om8ks
gVy6CvW+cp6kmSe1P3Z3h6fD7vbH7u7jvx/vjg93X9d/+HL4+/ju+dP74+Pu9uH5/v5m98/h67vj
t6fflwO7278O91+PN6+X/PFjd/z29vC0HNxFisOewp7m3c3u8Onz88PT7ja8oXiz+/D8+Hh8+PB9
+Vu//fp2Ofzl8P3w/v54sTKF5dDd8cP94fHw9PHzw/+H0z4sh2k5/Hj88Pnx7mJhoN3LzTnKsoL2
NL5CoTfz5EGpKy0UKvs4ABSWnAAK5eVPF8qy0kQZ93GEKMtCuCs09O7KsrJWQENZ/kSMMii7ErpR
LK/EfYjL38AoAaHEVrjXKGX2oETbK7F6BRWIJTNAkV7xopheidUrE0RZJQtCGbpRLK/E6pWEUQYF
JXSjWF4J1SvItlG5r4RWuNco2YUSbK+E6hVUIJaUuxJa4XpQTK+E6pUMUVbJGaEM3SiWV5bDEV9B
LAlRhFfcKJZXqHoF2ZYlZYGoFe56FLK9QtUrqEAsKXeFWuF6UEyvUPVKgSj4GUTdXiHbK1S9gncF
e4VEv7KgDE4Uwythrl5BtiV8X1lXUBcKr7RQVq/MEoUXQhTgFSeK5ZX18OIVcAXxQowydKMYXlkP
R2hbXohRhFem7ESxvFLW3lYpEPZKAV7xoRTbK2X1CmqzeSFEkV7xopheKdUroM3mhRhFeMWNYnml
1H4FNJS8EKNIrzhJLKtkbQzihYAkgzHIRZJtp+TqFLQnGU9B64p82WQ7SUyjZG0I4oVwT3gi6SGx
fJK1GYgXYpJwSTKOThTLKJM2A/FCgDIBo/hQJtspkzYDNUkxA60rhFO8KKZVJm0GapKig1t/F1Zx
o1hembS+tklCFOmV4ERpDxiIMmp9bZOUKGMtXOxAqSv1XRm1vrZJyrvKCOZlL4rplVHra5ukaCbX
34feXTG9MlavINuObUQHKOESZXA9Ckf7vjJUr6Cn8oizlXUF8Z3oWpTBvq8M1SuoQCyZAIr0ihfF
9MpQvYJu/IPilaEWrm9XTK8M2gzUJOGuCK8U1+PQzmxD0magJim9koBXiutua2e2IakzUFK8wpnt
+eOwuC5mO7MNSZ2BkuKVBLziRrG8ktQZKCleScAr2WVbO7MNnNmiWxxLDgIlAq/4UOzMNkR1BoqK
VzhATeconrcewc5sQ1RnoNjsCVDEM8iNYnklqjMQS6ICrYU735WpOFEsrwS1t43tlcIlSmiFux7F
zmzDOuRhlNDaaYmyFm44R3Hdbe3MNgS1tw2tnQYo4r7iRrG8ErR8v0nCXQliV1y3ODuzDaTl+01S
7gqB+4oPxc5sa2KDUUjxCme2sQvF9App+X6TRLsCvOJFsbxC1SvoFkf4vWGgbq/YmS3N6hxE2Cs0
93qF7MyWZm0OapLCKzT3eoXszJZmbQ5qkmhXOr1CdmZLszYzN0nxZKYZ9CtuFMsrRZuDmqTcldLt
FTuzpaLNQU1S7kpBM7PnBQzZmS0V1SsF97ZUgFfcKJZXitbbNkm4K8Iro6ehJDu0pazl+01StE6U
gVd8KHZqWzMuOHw0SbkrGfS2Y/KhmF7JWm/bJOUtLqN8xYtieSVXr4Anc5NEBZK97eBpKMnObYlz
W2TbrHhl2tcHVweKndvSpH3n1CQhynLOi13xvKwjO7elSYv4m6T0ylgRL1A8cxCN9q6MWprdJCHK
tO7WGYqPxNyUUWv4m6J8BI1rrxXGLhLr+uF8EjmFFUU+STVGvNiT5Bk9yM4n1xMHjMKSclOGZubr
Uex8kgbte54mCVGmy/okV4HsfJIGrd9vkqhAa+GmPhTLK4OWZTdJ8bKBUivcGYrrUrbzSarflEIU
lkQoXLjrUex8kpLaw7EkRKmFe40SXbd9O5+kpGXZTVL2cKkVrgvF8krSsmyWjOIlJtUYsXdXLK/E
6hWEwpJyV2ItXA+KnU8Sf1OKCsSSEGXqK5CdT1LNJwO6gqokLFC9uPpQLK/E6hWQxDVJ2a2EdpF3
oVheCatXAmoRqiTYldAu8utR7HyyfgiJb3FVEqNMfQWy88kazMIPRpokKtBauNSHYnklqP1+aB3S
JUqNES93xfNiiux8cj1xwFcQtRZJosh+34di55NE2hc9TRKiiH7fi2J6hdR+n5R+n9pFfobiSlfs
fJJI7W2ptUhnKGGeZ9DbulFUr/CJA98lzlA2SYQie9vw83SFz6h6hU+MettNEqKIb3rSzy/m00oT
BfW2m2RBKD29La9UvcKHUT65SUqUGiNe3/HzGS2v1G9Kg8icNskMUHh8vR7Fyif5xCjL3iQhytRX
ICuf5MMon9wkUYF4gL2y4+eVllcKnoM2yVmg5L45iM9oeYXzSfHCe5OUu5LBHOR4MvMZLa9kPAdt
khClZw7ilZZXOJ9EV1BGcxD/3jMH8UrLKxnPQSfJyy5u+X3qm4P4jJZXJpxlb5JyV6b24fiVTcJp
pYUCv+nZJEeAAuYgJ4rplZpPyk9GTpKwQGAO8qJYXuHvSvGugDlo+X3s6235jJZXxuoV8bJhk0Qo
oLd13eKs1JZPjLLsTRKiyN7WiWJ6ZcS97SYpbcshKvWhWF7h3BYXCPe2/Pnn3IdieUXJbU+SYFdq
iHqJ4iGxYls+Lwr4T4qYZBL18ZGYThlwwL8povLUULOLxDLKgIegTVG2cJygnu+Jb/KwQls+ccDl
ScoQxAlqD4oV2vKJlSEoKUMQJ6ihC8W0SlKHoIQCfv5dPIDcKJZXkjoEJWUI4gS1c1csr0Qc8G+S
clc4Qe1BsUJbPrEyBEVlCIogiPOimF6J6hAUlSEogiDOjWJ5JaqNbVSGIA5tO1EsrwS1sQ1KYxvA
K2YfihXa8omVxjaggJ9/F6+YvSimV5TQ9iQJGlv+9rP0oVheCfiDnpMkaOEIBHFuFMsrpHqFUMBf
f+8N4qzQlk+MPlY/SWKUqa9AVmjLh9EHPSdJWCAZxLlRLK8Q/lj9dCTGS5Qyt8J1oRheKTP+L3Y3
SXG3LTN4GeRCKXZoW2ZtCGLJGBCK+FjdFdoWO7QtszYEFfg/AuDfZRD384+/eOXA/9oaCh6CWDIm
gVIT1Bj6UNpEDFGK1ts2SXHjL6U3iCs1tNV3pWhTEEuCK6j0BnGlhrZVS0PBYxBLwgKBIM5VoBra
mii4t2XJKG5xJQOvuFGCgZLxBwmbpPRKRi+ZXSg1tK0VUFCU3pYl4a5wgnrlKzJemVlLQ8G9LUsu
2/ny58vLyy//Aem67ar/TgAA
headers:
Access-Control-Allow-Origin:
- '*'
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Security-Policy:
- frame-ancestors 'none'; default-src 'self'
Content-Type:
- application/json; charset=utf-8
Date:
- Wed, 27 Dec 2023 22:03:32 GMT
Etag:
- W/"4eff-tQtLOmUXB0Jex4vyhYj8l0ja/So"
Strict-Transport-Security:
- max-age=15552000; includeSubDomains
Transfer-Encoding:
- chunked
Vary:
- Origin, Accept-Encoding
X-Content-Security-Policy:
- frame-ancestors 'none'; default-src 'self'
X-Content-Type-Options:
- nosniff
X-DNS-Prefetch-Control:
- 'off'
X-Download-Options:
- noopen
X-Frame-Options:
- SAMEORIGIN
X-WebKit-CSP:
- frame-ancestors 'none'; default-src 'self'
X-XSS-Protection:
- 1; mode=block
status:
code: 200
message: OK
version: 1

View File

@@ -0,0 +1,70 @@
interactions:
- request:
body: '{"operationName": "getCompanyInsidersActivities", "variables": {"symbol":
"SHOP"}, "query": "query getCompanyInsidersActivities(\n $symbol: String\n)
{\n getCompanyInsidersActivities(\n symbol: $symbol\n ) {\n insiderActivities
{\n periodkey\n buy {\n name\n trades\n shares\n sharesHeld\n tradeValue\n }\n sell
{\n name\n trades\n shares\n sharesHeld\n tradeValue\n }\n }\n activitySummary
{\n periodkey\n buyShares\n soldShares\n netActivity\n totalShares\n }\n }\n}"}'
headers:
Accept:
- '*/*'
Content-Type:
- application/json
authority:
- app-money.tmx.com
locale:
- en
referer:
- https://money.tmx.com/en/quote/SHOP
method: POST
uri: https://app-money.tmx.com/graphql
response:
body:
string: !!binary |
H4sIAAAAAAAAA8WVy27bMBBF9/0KQWtB4PvhnZs4dQMEBewgm8ALxqIrNnoEEu1WNfzvpRKnolQD
cpui1cqkRvd65syQ+zBRVoWTffhZ24syf1JF87GoTaKrerq2Zmes0XX73rzs+pv3+/BJV6ZMHnUT
TkKbVlrflIVN6zAKH7Zu734VhbXOsufYQuXahc3LzSbXpra6ioJrvdm4YFuppFWELjxVVfsTcwhe
V3OdJeEEQQboMfZOZVsnhjggVB6iTlxX9nsrW9dmrTxl1CkLiERfGVICRF+ZCiYAj6mnfWWKR525
/22KKJirKtNNcGPWqdKZZ0Q7I8kpHRgBTCTrO3GAMccx89NYpqoolLO5LR+aIrhUO5N4HqzzQJRA
ifouGFFGSd8FcgYxcLVatTY+ttp8exs0r7QcSzoKjQIMAYkFlseHnwOQdC6QIQBHCUIgAKYyJuS3
GUKvD6Gkw0Y8RdEVF3AOYsFes/pPOO1Xne3eOIbcSx/JM+aQS4oAiOFZowixN+WICD6OUgLJMYwZ
PtEx5yFFvinDmIwjdXlKLHmHlP0bpJ3JdZkWtS2dy0WZZVoXp49K7r4bpAMhAn15QiB0B1pvGqbL
+SwKFp/ezxa3wYfZYrq4PO1AsBwUDEMi0LBgHAoC4h6dpcnLKLgyyRdzWprJIX/UsurpClfDGCHX
7K6R1csN1Cy3ea6qZvwaWh6d2hqVWeItC22P91nzvLalVdnP9yMH5d+S/XVg/1h5dTgc3v0A+pNt
u9MHAAA=
headers:
Access-Control-Allow-Origin:
- '*'
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Security-Policy:
- frame-ancestors 'none'; default-src 'self'
Content-Type:
- application/json; charset=utf-8
Date:
- Wed, 03 Jan 2024 20:08:44 GMT
Etag:
- W/"7d3-DY9oT4dVvL8jzGyeL78a7Ws2Ogo"
Strict-Transport-Security:
- max-age=15552000; includeSubDomains
Transfer-Encoding:
- chunked
Vary:
- Origin, Accept-Encoding
X-Content-Type-Options:
- nosniff
X-DNS-Prefetch-Control:
- 'off'
X-Download-Options:
- noopen
X-Frame-Options:
- SAMEORIGIN
X-XSS-Protection:
- 1; mode=block
status:
code: 200
message: OK
version: 1

View File

@@ -0,0 +1,57 @@
interactions:
- request:
body: '{"operationName": "getCompanyAnalysts", "variables": {"symbol": "SHOP",
"datatype": "equity"}, "query": "query getCompanyAnalysts(\n $symbol: String!\n $dataType:
String,\n) {\n analysts: getCompanyAnalysts(\n datatype: $dataType,\n symbol:
$symbol\n ) {\n totalAnalysts\n priceTarget\n {\n highPriceTarget\n lowPriceTarget\n priceTarget\n priceTargetUpside\n }\n consensusAnalysts\n {\n consensus\n buy\n sell\n hold\n }\n }\n}"}'
headers:
Accept:
- '*/*'
Content-Type:
- application/json
authority:
- app-money.tmx.com
locale:
- en
referer:
- https://money.tmx.com/en/quote/SHOP
method: POST
uri: https://app-money.tmx.com/graphql
response:
body:
string: '{"data":{"analysts":{"totalAnalysts":31,"priceTarget":{"highPriceTarget":131.9915,"lowPriceTarget":72.595325,"priceTarget":97.8320998,"priceTargetUpside":-3.96},"consensusAnalysts":{"consensus":"Buy","buy":10,"sell":2,"hold":19}}}}
'
headers:
Access-Control-Allow-Origin:
- '*'
Connection:
- keep-alive
Content-Length:
- '232'
Content-Security-Policy:
- frame-ancestors 'none'; default-src 'self'
Content-Type:
- application/json; charset=utf-8
Date:
- Wed, 27 Dec 2023 22:03:36 GMT
Etag:
- W/"e8-4l0k1dc8UP8MlOmcXd52N+O9B90"
Strict-Transport-Security:
- max-age=15552000; includeSubDomains
Vary:
- Origin, Accept-Encoding
X-Content-Type-Options:
- nosniff
X-DNS-Prefetch-Control:
- 'off'
X-Download-Options:
- noopen
X-Frame-Options:
- SAMEORIGIN
X-XSS-Protection:
- 1; mode=block
status:
code: 200
message: OK
version: 1

View File

@@ -0,0 +1,256 @@
"""TMX fetchers tests."""
from datetime import date
import pytest
from openbb_core.app.service.user_service import UserService
from openbb_tmx.models.available_indices import TmxAvailableIndicesFetcher
from openbb_tmx.models.bond_prices import TmxBondPricesFetcher
from openbb_tmx.models.calendar_earnings import TmxCalendarEarningsFetcher
from openbb_tmx.models.company_filings import TmxCompanyFilingsFetcher
from openbb_tmx.models.company_news import TmxCompanyNewsFetcher
from openbb_tmx.models.equity_historical import TmxEquityHistoricalFetcher
from openbb_tmx.models.equity_profile import TmxEquityProfileFetcher
from openbb_tmx.models.equity_quote import TmxEquityQuoteFetcher
from openbb_tmx.models.equity_search import TmxEquitySearchFetcher
from openbb_tmx.models.etf_countries import TmxEtfCountriesFetcher
from openbb_tmx.models.etf_holdings import TmxEtfHoldingsFetcher
from openbb_tmx.models.etf_info import TmxEtfInfoFetcher
from openbb_tmx.models.etf_search import TmxEtfSearchFetcher
from openbb_tmx.models.etf_sectors import TmxEtfSectorsFetcher
from openbb_tmx.models.gainers import TmxGainersFetcher
from openbb_tmx.models.historical_dividends import TmxHistoricalDividendsFetcher
from openbb_tmx.models.index_constituents import TmxIndexConstituentsFetcher
from openbb_tmx.models.index_sectors import TmxIndexSectorsFetcher
from openbb_tmx.models.index_snapshots import TmxIndexSnapshotsFetcher
from openbb_tmx.models.insider_trading import TmxInsiderTradingFetcher
from openbb_tmx.models.options_chains import TmxOptionsChainsFetcher
from openbb_tmx.models.price_target_consensus import TmxPriceTargetConsensusFetcher
from openbb_tmx.models.treasury_prices import TmxTreasuryPricesFetcher
test_credentials = UserService().default_user_settings.credentials.model_dump()
@pytest.fixture(scope="module")
def vcr_config():
return {
"filter_headers": [("User-Agent", None)],
"filter_query_parameters": [
None,
],
}
@pytest.mark.record_http
def test_tmx_equity_profile_fetcher(credentials=test_credentials):
params = {"symbol": "RY,NTR"}
fetcher = TmxEquityProfileFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_tmx_equity_search_fetcher(credentials=test_credentials):
params = {"query": "gold", "use_cache": False}
fetcher = TmxEquitySearchFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_tmx_available_indices_fetcher(credentials=test_credentials):
params = {"use_cache": False}
fetcher = TmxAvailableIndicesFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_tmx_calendar_earnings_fetcher(credentials=test_credentials):
params = {"start_date": date(2023, 1, 2), "end_date": date(2023, 1, 31)}
fetcher = TmxCalendarEarningsFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_tmx_company_filings_fetcher(credentials=test_credentials):
params = {
"symbol": "SHOP",
"start_date": date(2023, 6, 30),
"end_date": date(2023, 9, 30),
}
fetcher = TmxCompanyFilingsFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_tmx_company_news_fetcher(credentials=test_credentials):
params = {"symbol": "SHOP", "limit": 5}
fetcher = TmxCompanyNewsFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_tmx_equity_historical_fetcher(credentials=test_credentials):
params = {
"symbol": "SHOP",
"start_date": date(2022, 1, 1),
"end_date": date(2023, 1, 1),
}
fetcher = TmxEquityHistoricalFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_tmx_equity_quote_fetcher(credentials=test_credentials):
params = {"symbol": "SHOP"}
fetcher = TmxEquityQuoteFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_tmx_etf_countries_fetcher(credentials=test_credentials):
params = {"symbol": "HXX", "use_cache": False}
fetcher = TmxEtfCountriesFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_tmx_etf_holdings_fetcher(credentials=test_credentials):
params = {"symbol": "XIU", "use_cache": False}
fetcher = TmxEtfHoldingsFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_tmx_etf_info_fetcher(credentials=test_credentials):
params = {"symbol": "XIU", "use_cache": False}
fetcher = TmxEtfInfoFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_tmx_etf_search_fetcher(credentials=test_credentials):
params = {"query": "sector", "use_cache": False}
fetcher = TmxEtfSearchFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_tmx_etf_sectors_fetcher(credentials=test_credentials):
params = {"symbol": "XIU", "use_cache": False}
fetcher = TmxEtfSectorsFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_tmx_gainers_fetcher(credentials=test_credentials):
params = {}
fetcher = TmxGainersFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_tmx_historical_dividends_fetcher(credentials=test_credentials):
params = {"symbol": "TD"}
fetcher = TmxHistoricalDividendsFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_tmx_index_constituents_fetcher(credentials=test_credentials):
params = {"symbol": "^TX60", "use_cache": False}
fetcher = TmxIndexConstituentsFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_tmx_index_sectors_fetcher(credentials=test_credentials):
params = {"symbol": "^TSX", "use_cache": False}
fetcher = TmxIndexSectorsFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_tmx_index_snapshots_fetcher(credentials=test_credentials):
params = {"use_cache": False}
fetcher = TmxIndexSnapshotsFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_tmx_insider_trading_fetcher(credentials=test_credentials):
params = {"symbol": "SHOP", "summary": False}
fetcher = TmxInsiderTradingFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_tmx_options_chains_fetcher(credentials=test_credentials):
params = {"symbol": "SHOP", "use_cache": False}
fetcher = TmxOptionsChainsFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_tmx_price_target_consensus_fetcher(credentials=test_credentials):
params = {"symbol": "SHOP"}
fetcher = TmxPriceTargetConsensusFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_tmx_treasury_prices_fetcher(credentials=test_credentials):
params = {"govt_type": "federal", "use_cache": False}
fetcher = TmxTreasuryPricesFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_tmx_bond_prices_fetcher(credentials=test_credentials):
params = {"use_cache": False, "coupon_rate_min": 4}
fetcher = TmxBondPricesFetcher()
result = fetcher.test(params, credentials)
assert result is None

View File

@@ -76,7 +76,7 @@ class YFinanceBalanceSheetFetcher(
raise EmptyDataError()
data.index = [to_snake_case(i) for i in data.index]
data = data.reset_index().sort_index(ascending=False).set_index("index")
data = data.convert_dtypes().fillna(0).replace(0, None).to_dict()
data = data.fillna("N/A").replace("N/A", None).to_dict()
data = [{"period_ending": str(key), **value} for key, value in data.items()]
data = json.loads(json.dumps(data))

View File

@@ -75,7 +75,7 @@ class YFinanceCashFlowStatementFetcher(
data.index = [to_snake_case(i) for i in data.index]
data = data.reset_index().sort_index(ascending=False).set_index("index")
data = data.convert_dtypes().fillna(0).replace(0, None).to_dict()
data = data.fillna("N/A").replace("N/A", None).to_dict()
data = [{"period_ending": str(key), **value} for key, value in data.items()]
data = json.loads(json.dumps(data))

View File

@@ -75,7 +75,7 @@ class YFinanceIncomeStatementFetcher(
raise EmptyDataError()
data.index = [to_snake_case(i) for i in data.index]
data = data.reset_index().sort_index(ascending=False).set_index("index")
data = data.convert_dtypes().fillna(0).replace(0, None).to_dict()
data = data.fillna("N/A").replace("N/A", None).to_dict()
data = [{"period_ending": str(key), **value} for key, value in data.items()]
data = json.loads(json.dumps(data))

View File

@@ -45,6 +45,7 @@ openbb-government-us = { version = "^1.1.2", optional = true }
openbb-nasdaq = { version = "^1.1.3", optional = true }
openbb-seeking-alpha = { version = "^1.1.2", optional = true }
openbb-stockgrid = { version = "^1.1.2", optional = true }
openbb-tmx = { version = "^1.0.0", optional = true }
openbb-wsj = { version = "^1.1.2", optional = true }
openbb-charting = { version = "^2.0.0", optional = true }
@@ -67,6 +68,7 @@ quantitative = ["openbb-quantitative"]
seeking_alpha = ["openbb-seeking-alpha"]
stockgrid = ["openbb-stockgrid"]
technical = ["openbb-technical"]
tmx = ["openbb-tmx"]
wsj = ["openbb-wsj"]
@@ -85,6 +87,7 @@ all = [
"openbb-seeking-alpha",
"openbb-stockgrid",
"openbb-technical",
"openbb-tmx",
"openbb-wsj",
]