Add tiingo provider (#5740)

* first commit

* things

* Add news endpoints

* CryptoHistorical

* Add currency and trailing dividend yield

* things working

* deps

* fix pre commit things

* tests?

* black on the tests

* pratty told me to add it

* integration tests

* added helpers

* crypto revamped

* currency revamped

* trailing dividend yield revamped

* equity historical revamped

* company news overhauled

* global news overhauled

* remove `tags` param from news models

* crypto correct `exchanges` description

* james smh

* re-recorded fetcher tests

* fixed integration tests

* econometrics smh

* james smh again

---------

Co-authored-by: Pratyush Shukla <ps4534@nyu.edu>
This commit is contained in:
James Maslek
2023-11-16 04:34:29 -05:00
committed by GitHub
parent 3ce696feeb
commit 2b058c664c
34 changed files with 2887 additions and 1 deletions

View File

@@ -25,6 +25,7 @@ openbb-intrinio = { path = "./providers/intrinio", develop = true }
openbb-oecd = { path = "./providers/oecd", develop = true }
openbb-polygon = { path = "./providers/polygon", develop = true }
openbb-sec = { path = "./providers/sec", develop = true }
openbb-tiingo = { path = "./providers/tiingo", develop = true }
openbb-tradingeconomics = { path = "./providers/tradingeconomics", develop = true }
openbb-crypto = { path = "./extensions/crypto", develop = true }

View File

@@ -113,6 +113,26 @@ def test_crypto_search(params, headers):
"end_date": "2023-06-06",
}
),
(
{
"provider": "tiingo",
"interval": "1day",
"exchanges": ["POLONIEX", "GDAX"],
"symbol": "BTCUSD",
"start_date": "2023-01-01",
"end_date": "2023-06-06",
}
),
(
{
"provider": "tiingo",
"interval": "1hour",
"exchanges": ["POLONIEX", "GDAX"],
"symbol": "BTCUSD",
"start_date": "2023-01-01",
"end_date": "2023-06-06",
}
),
],
)
@pytest.mark.integration

View File

@@ -98,6 +98,26 @@ def test_crypto_search(params, obb):
"end_date": "2023-06-06",
}
),
(
{
"provider": "tiingo",
"interval": "1day",
"exchanges": ["POLONIEX", "GDAX"],
"symbol": "BTCUSD",
"start_date": "2023-01-01",
"end_date": "2023-06-06",
}
),
(
{
"provider": "tiingo",
"interval": "1hour",
"exchanges": ["POLONIEX", "GDAX"],
"symbol": "BTCUSD",
"start_date": "2023-01-01",
"end_date": "2023-06-06",
}
),
],
)
@pytest.mark.integration

View File

@@ -123,6 +123,24 @@ def test_currency_search(params, headers):
"end_date": "2023-06-06",
}
),
(
{
"interval": "1hour",
"provider": "tiingo",
"symbol": "EURUSD",
"start_date": "2023-05-21",
"end_date": "2023-06-06",
}
),
(
{
"interval": "1day",
"provider": "tiingo",
"symbol": "EURUSD",
"start_date": "2023-05-21",
"end_date": "2023-06-06",
}
),
],
)
@pytest.mark.integration

View File

@@ -119,6 +119,24 @@ def test_currency_search(params, obb):
"end_date": "2023-06-06",
}
),
(
{
"interval": "1hour",
"provider": "tiingo",
"symbol": "EURUSD",
"start_date": "2023-05-21",
"end_date": "2023-06-06",
}
),
(
{
"interval": "1day",
"provider": "tiingo",
"symbol": "EURUSD",
"start_date": "2023-05-21",
"end_date": "2023-06-06",
}
),
],
)
@pytest.mark.integration

View File

@@ -872,6 +872,24 @@ def test_equity_options_unusual(params, headers):
"interval": "1d",
}
),
(
{
"provider": "tiingo",
"symbol": "AAPL",
"start_date": "2023-01-01",
"end_date": "2023-06-06",
"interval": "1d",
}
),
(
{
"provider": "tiingo",
"symbol": "AAPL",
"start_date": "2023-01-01",
"end_date": "2023-06-06",
"interval": "1M",
}
),
],
)
@pytest.mark.integration
@@ -1334,3 +1352,18 @@ def test_equity_market_snapshots(params, headers):
result = requests.get(url, headers=headers, timeout=10)
assert isinstance(result, requests.Response)
assert result.status_code == 200
@pytest.mark.parametrize(
"params",
[({"provider": "tiingo", "symbol": "AAPL"})],
)
@pytest.mark.integration
def test_equity_fundamental_trailing_dividend_yield(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/equity/fundamental/trailing_dividend_yield?{query_str}"
result = requests.get(url, headers=headers, timeout=10)
assert isinstance(result, requests.Response)
assert result.status_code == 200

View File

@@ -807,6 +807,24 @@ def test_equity_options_unusual(params, obb):
"interval": "1d",
}
),
(
{
"provider": "tiingo",
"symbol": "AAPL",
"start_date": "2023-01-01",
"end_date": "2023-06-06",
"interval": "1d",
}
),
(
{
"provider": "tiingo",
"symbol": "AAPL",
"start_date": "2023-01-01",
"end_date": "2023-06-06",
"interval": "1M",
}
),
],
)
@pytest.mark.integration
@@ -1228,3 +1246,17 @@ def test_equity_market_snapshots(params, obb):
assert result
assert isinstance(result, OBBject)
assert len(result.results) > 0
@pytest.mark.parametrize(
"params",
[({"provider": "tiingo", "symbol": "AAPL"})],
)
@pytest.mark.integration
def test_equity_fundamental_trailing_dividend_yield(params, obb):
params = {p: v for p, v in params.items() if v}
result = obb.equity.fundamental.trailing_dividend_yield(**params)
assert result
assert isinstance(result, OBBject)
assert len(result.results) > 0

View File

@@ -255,3 +255,14 @@ def transcript(
) -> OBBject[BaseModel]:
"""Earnings Call Transcript. Earnings call transcript for a given company."""
return OBBject(results=Query(**locals()).execute())
@router.command(model="TrailingDividendYield")
def trailing_dividend_yield(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject[BaseModel]:
"""Trailing 1yr dividend yield."""
return OBBject(results=Query(**locals()).execute())

View File

@@ -56,6 +56,13 @@ def headers():
"term": "MSFT",
}
),
(
{
"provider": "tiingo",
"limit": 30,
"source": "bloomberg.com",
}
),
],
)
@pytest.mark.integration
@@ -131,6 +138,14 @@ def test_news_world(params, headers):
"symbols": "AAPL,MSFT",
}
),
(
{
"provider": "tiingo",
"symbols": "AAPL,MSFT",
"limit": 20,
"source": "bloomberg.com",
}
),
],
)
@pytest.mark.integration

View File

@@ -62,6 +62,13 @@ def obb(pytestconfig): # pylint: disable=inconsistent-return-statements
"term": "MSFT",
}
),
(
{
"provider": "tiingo",
"limit": 30,
"source": "bloomberg.com",
}
),
],
)
@pytest.mark.integration
@@ -133,6 +140,14 @@ def test_news_world(params, obb):
"symbols": "AAPL,MSFT",
}
),
(
{
"provider": "tiingo",
"symbols": "AAPL,MSFT",
"limit": 20,
"source": "bloomberg.com",
}
),
],
)
@pytest.mark.integration

View File

@@ -49,7 +49,9 @@ class CurrencyHistoricalData(Data):
high: PositiveFloat = Field(description=DATA_DESCRIPTIONS.get("high", ""))
low: PositiveFloat = Field(description=DATA_DESCRIPTIONS.get("low", ""))
close: PositiveFloat = Field(description=DATA_DESCRIPTIONS.get("close", ""))
volume: float = Field(description=DATA_DESCRIPTIONS.get("volume", ""))
volume: Optional[float] = Field(
description=DATA_DESCRIPTIONS.get("volume", ""), default=None
)
vwap: Optional[PositiveFloat] = Field(
description=DATA_DESCRIPTIONS.get("vwap", ""), default=None
)

View File

@@ -0,0 +1,23 @@
"""Trailing Dividend Yield Data Model."""
from datetime import date as dateType
from pydantic import Field
from openbb_provider.abstract.data import Data
from openbb_provider.abstract.query_params import QueryParams
from openbb_provider.utils.descriptions import DATA_DESCRIPTIONS, QUERY_DESCRIPTIONS
class TrailingDivYieldQueryParams(QueryParams):
"""Trailing Dividend Yield Query."""
symbol: str = Field(default=None, description=QUERY_DESCRIPTIONS.get("symbol", ""))
class TrailingDivYieldData(Data):
"""Return Trailing Dividend Yield Data."""
date: dateType = Field(description=DATA_DESCRIPTIONS.get("date", ""))
trailing_dividend_yield: float = Field(description="Trailing dividend yield.")

View File

@@ -10,4 +10,5 @@ test_credentials: Dict[str, Tuple[str, str]] = {
"nasdaq": ("x-api-token", "MOCK_API_KEY"),
"fred": ("api_key", "MOCK_API_KEY"),
"intrinio": ("api_key", "MOCK_API_KEY"),
"tiingo": ("token", "MOCK_TOKEN"),
}

View File

@@ -0,0 +1,15 @@
# OpenBB Tiingo Provider
This extension integrates the [Tiingo](https://www.tiingo.com/) data provider into the OpenBB Platform.
## Installation
To install the extension:
```bash
pip install openbb-tiingo
```
For development please check [Contribution Guidelines](https://github.com/OpenBB-finance/OpenBBTerminal/blob/feature/openbb-sdk-v4/openbb_platform/CONTRIBUTING.md).
Documentation available [here](https://docs.openbb.co/sdk).

View File

@@ -0,0 +1,26 @@
"""Tiingo provider module."""
from openbb_provider.abstract.provider import Provider
from openbb_tiingo.models.company_news import TiingoCompanyNewsFetcher
from openbb_tiingo.models.crypto_historical import TiingoCryptoHistoricalFetcher
from openbb_tiingo.models.currency_historical import TiingoCurrencyHistoricalFetcher
from openbb_tiingo.models.equity_historical import TiingoEquityHistoricalFetcher
from openbb_tiingo.models.global_news import TiingoGlobalNewsFetcher
from openbb_tiingo.models.trailing_dividend_yield import (
TiingoTrailingDivYieldFetcher,
)
tiingo_provider = Provider(
name="tiingo",
website="https://tiingo.com/",
description="""""",
required_credentials=["token"],
fetcher_dict={
"EquityHistorical": TiingoEquityHistoricalFetcher,
"CompanyNews": TiingoCompanyNewsFetcher,
"GlobalNews": TiingoGlobalNewsFetcher,
"CryptoHistorical": TiingoCryptoHistoricalFetcher,
"CurrencyHistorical": TiingoCurrencyHistoricalFetcher,
"TrailingDividendYield": TiingoTrailingDivYieldFetcher,
},
)

View File

@@ -0,0 +1,98 @@
"""Tiingo Company News."""
from datetime import datetime
from typing import Any, Dict, List, Optional
from dateutil import parser
from openbb_provider.abstract.fetcher import Fetcher
from openbb_provider.standard_models.company_news import (
CompanyNewsData,
CompanyNewsQueryParams,
)
from openbb_provider.utils.helpers import get_querystring
from openbb_tiingo.utils.helpers import get_data_many
from pydantic import Field, field_validator
class TiingoCompanyNewsQueryParams(CompanyNewsQueryParams):
"""tiingo Company News query.
Source: https://www.tiingo.com/documentation/news
"""
__alias_dict__ = {"symbols": "tickers"}
source: Optional[str] = Field(
default=None, description="A comma-separated list of the domains requested."
)
class TiingoCompanyNewsData(CompanyNewsData):
"""Tiingo Company News data."""
__alias_dict__ = {
"date": "publishedDate",
"text": "description",
}
symbols: str = Field(
description="Ticker tagged in the fetched news.", alias="tickers"
)
article_id: int = Field(description="Unique ID of the news article.", alias="id")
site: str = Field(description="Name of the news source.", alias="source")
tags: str = Field(description="Tags associated with the news article.")
crawl_date: datetime = Field(description="Date the news article was crawled.")
@field_validator("tags", "symbols", mode="before")
@classmethod
def list_to_string(cls, v):
"""Convert list to string."""
return ",".join(v)
@field_validator("crawl_date", mode="before")
@classmethod
def validate_date(cls, v):
"""Validate the date."""
return parser.parse(v)
@field_validator("symbols", mode="after")
@classmethod
def symbols_validate(cls, v: str):
"""Convert symbols to upper case."""
return v.upper()
class TiingoCompanyNewsFetcher(
Fetcher[
TiingoCompanyNewsQueryParams,
List[TiingoCompanyNewsData],
]
):
"""Transform the query, extract and transform the data from the tiingo endpoints."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> TiingoCompanyNewsQueryParams:
"""Transform the query params."""
return TiingoCompanyNewsQueryParams(**params)
@staticmethod
def extract_data(
query: TiingoCompanyNewsQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[Dict]:
"""Return the raw data from the tiingo endpoint."""
api_key = credentials.get("tiingo_token") if credentials else ""
base_url = "https://api.tiingo.com/tiingo/news"
query_str = get_querystring(query.model_dump(by_alias=True), [])
url = f"{base_url}?{query_str}&token={api_key}"
return get_data_many(url)
@staticmethod
def transform_data(
query: TiingoCompanyNewsQueryParams, data: List[Dict], **kwargs: Any
) -> List[TiingoCompanyNewsData]:
"""Return the transformed data."""
return [TiingoCompanyNewsData.model_validate(d) for d in data]

View File

@@ -0,0 +1,107 @@
"""Tiingo Crypto end of day fetcher."""
from datetime import datetime
from typing import Any, Dict, List, Literal, Optional
from dateutil.relativedelta import relativedelta
from openbb_provider.abstract.fetcher import Fetcher
from openbb_provider.standard_models.crypto_historical import (
CryptoHistoricalData,
CryptoHistoricalQueryParams,
)
from openbb_provider.utils.helpers import get_querystring
from openbb_tiingo.utils.helpers import get_data_one
from pydantic import Field
class TiingoCryptoHistoricalQueryParams(CryptoHistoricalQueryParams):
"""Tiingo Crypto end of day Query.
Source: https://www.tiingo.com/documentation/end-of-day
"""
__alias_dict__ = {
"symbol": "tickers",
"start_date": "startDate",
"end_date": "endDate",
}
interval: Literal[
"1min", "5min", "15min", "30min", "1hour", "4hour", "1day"
] = Field(default="1day", description="Data granularity.", alias="resampleFreq")
exchanges: Optional[List[str]] = Field(
default=None,
description=(
"To limit the query to a subset of exchanges e.g. ['POLONIEX', 'GDAX']"
),
)
# pylint: disable=protected-access
class TiingoCryptoHistoricalData(CryptoHistoricalData):
"""Tiingo Crypto end of day Data."""
transactions: Optional[int] = Field(
default=None, description="Number of trades.", alias="tradesDone"
)
volume_notional: Optional[float] = Field(
default=None,
description=(
"The last size done for the asset on the specific date in the "
"quote currency. The volume of the asset on the specific date in "
"the quote currency."
),
alias="volumeNotional",
)
class TiingoCryptoHistoricalFetcher(
Fetcher[
TiingoCryptoHistoricalQueryParams,
List[TiingoCryptoHistoricalData],
]
):
"""Transform the query, extract and transform the data from the Tiingo endpoints."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> TiingoCryptoHistoricalQueryParams:
"""Transform the query params."""
transformed_params = params
now = datetime.now().date()
if params.get("start_date") is None:
transformed_params["start_date"] = now - relativedelta(years=1)
if params.get("end_date") is None:
transformed_params["end_date"] = now
return TiingoCryptoHistoricalQueryParams(**transformed_params)
# pylint: disable=protected-access
@staticmethod
def extract_data(
query: TiingoCryptoHistoricalQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Optional[Any],
) -> List[Dict]:
"""Return the raw data from the Tiingo endpoint."""
api_key = credentials.get("tiingo_token") if credentials else ""
base_url = "https://api.tiingo.com/tiingo/crypto/prices"
query_str = get_querystring(query.model_dump(by_alias=True), [])
url = f"{base_url}?{query_str}&token={api_key}"
data = get_data_one(url).get("priceData", [])
return data
# pylint: disable=unused-argument
@staticmethod
def transform_data(
query: TiingoCryptoHistoricalQueryParams,
data: List[Dict],
**kwargs: Any,
) -> List[TiingoCryptoHistoricalData]:
"""Return the transformed data."""
return [TiingoCryptoHistoricalData.model_validate(d) for d in data]

View File

@@ -0,0 +1,85 @@
"""Tiingo Currency end of day fetcher."""
from datetime import datetime
from typing import Any, Dict, List, Literal, Optional
from dateutil.relativedelta import relativedelta
from openbb_provider.abstract.fetcher import Fetcher
from openbb_provider.standard_models.currency_historical import (
CurrencyHistoricalData,
CurrencyHistoricalQueryParams,
)
from openbb_provider.utils.helpers import get_querystring
from openbb_tiingo.utils.helpers import get_data_many
from pydantic import Field
class TiingoCurrencyHistoricalQueryParams(CurrencyHistoricalQueryParams):
"""Tiingo Currency end of day Query.
Source: https://www.tiingo.com/documentation/end-of-day
"""
__alias_dict__ = {
"symbol": "tickers",
"start_date": "startDate",
"end_date": "endDate",
}
interval: Literal[
"1min", "5min", "15min", "30min", "1hour", "4hour", "1day"
] = Field(default="1day", description="Data granularity.", alias="resampleFreq")
class TiingoCurrencyHistoricalData(CurrencyHistoricalData):
"""Tiingo Currency end of day Data."""
class TiingoCurrencyHistoricalFetcher(
Fetcher[
TiingoCurrencyHistoricalQueryParams,
List[TiingoCurrencyHistoricalData],
]
):
"""Transform the query, extract and transform the data from the Tiingo endpoints."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> TiingoCurrencyHistoricalQueryParams:
"""Transform the query params."""
transformed_params = params
now = datetime.now().date()
if params.get("start_date") is None:
transformed_params["start_date"] = now - relativedelta(years=1)
if params.get("end_date") is None:
transformed_params["end_date"] = now
return TiingoCurrencyHistoricalQueryParams(**transformed_params)
# pylint: disable=protected-access
@staticmethod
def extract_data(
query: TiingoCurrencyHistoricalQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[Dict]:
"""Return the raw data from the Tiingo endpoint."""
api_key = credentials.get("tiingo_token") if credentials else ""
base_url = "https://api.tiingo.com/tiingo/fx/prices"
query_str = get_querystring(query.model_dump(by_alias=True), [])
url = f"{base_url}?{query_str}&token={api_key}"
data = get_data_many(url)
return data
# pylint: disable=unused-argument
@staticmethod
def transform_data(
query: TiingoCurrencyHistoricalQueryParams,
data: List[Dict],
**kwargs: Any,
) -> List[TiingoCurrencyHistoricalData]:
"""Return the transformed data."""
return [TiingoCurrencyHistoricalData.model_validate(d) for d in data]

View File

@@ -0,0 +1,142 @@
"""Tiingo Equity end of day fetcher."""
from datetime import datetime
from typing import Any, Dict, List, Literal, Optional
from dateutil.relativedelta import relativedelta
from openbb_provider.abstract.fetcher import Fetcher
from openbb_provider.standard_models.equity_historical import (
EquityHistoricalData,
EquityHistoricalQueryParams,
)
from openbb_provider.utils.descriptions import QUERY_DESCRIPTIONS
from openbb_provider.utils.helpers import get_querystring
from openbb_tiingo.utils.helpers import get_data_many
from pydantic import Field, PrivateAttr, model_validator
class TiingoEquityHistoricalQueryParams(EquityHistoricalQueryParams):
"""Tiingo Equity end of day Query.
Source: https://www.tiingo.com/documentation/end-of-day
"""
__alias_dict__ = {
"start_date": "startDate",
"end_date": "endDate",
}
interval: Literal["1d", "1W", "1M", "1Y"] = Field(
default="1d", description=QUERY_DESCRIPTIONS.get("interval", "")
)
_frequency: Literal[
"daily", "weekly", "monthly", "quarterly", "yearly"
] = PrivateAttr(default=None)
# pylint: disable=protected-access
@model_validator(mode="after") # type: ignore[arg-type]
@classmethod
def set_time_params(cls, values: "TiingoEquityHistoricalQueryParams"):
"""Set the default start & end date and time params for Tiingo API."""
frequency_dict = {
"1d": "daily",
"1W": "weekly",
"1M": "monthly",
"1Q": "quarterly",
"1Y": "yearly",
}
values._frequency = frequency_dict[values.interval] # type: ignore[assignment]
return values
class TiingoEquityHistoricalData(EquityHistoricalData):
"""Tiingo Equity end of day Data."""
adj_open: Optional[float] = Field(
default=None,
description="Adjusted open price during the period.",
alias="adjOpen",
)
adj_high: Optional[float] = Field(
default=None,
description="Adjusted high price during the period.",
alias="adjHigh",
)
adj_low: Optional[float] = Field(
default=None,
description="Adjusted low price during the period.",
alias="adjLow",
)
adj_close: Optional[float] = Field(
default=None,
description="Adjusted closing price during the period.",
alias="adjClose",
)
adj_volume: Optional[float] = Field(
default=None,
description="Adjusted volume during the period.",
alias="adjVolume",
)
split_ratio: Optional[float] = Field(
default=None,
description="Ratio of the equity split, if a equity split occurred.",
alias="splitFactor",
)
dividend: Optional[float] = Field(
default=None,
description="Dividend amount, if a dividend was paid.",
alias="divCash",
)
class TiingoEquityHistoricalFetcher(
Fetcher[
TiingoEquityHistoricalQueryParams,
List[TiingoEquityHistoricalData],
]
):
"""Transform the query, extract and transform the data from the Tiingo endpoints."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> TiingoEquityHistoricalQueryParams:
"""Transform the query params."""
transformed_params = params
now = datetime.now().date()
if params.get("start_date") is None:
transformed_params["start_date"] = now - relativedelta(years=1)
if params.get("end_date") is None:
transformed_params["end_date"] = now
return TiingoEquityHistoricalQueryParams(**transformed_params)
# pylint: disable=protected-access
@staticmethod
def extract_data(
query: TiingoEquityHistoricalQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[Dict]:
"""Return the raw data from the Tiingo endpoint."""
api_key = credentials.get("tiingo_token") if credentials else ""
base_url = "https://api.tiingo.com/tiingo/daily"
query_str = get_querystring(
query.model_dump(by_alias=True), ["symbol", "interval"]
)
url = f"{base_url}/{query.symbol}/prices?{query_str}&resampleFreq={query._frequency}&token={api_key}"
return get_data_many(url)
# pylint: disable=unused-argument
@staticmethod
def transform_data(
query: TiingoEquityHistoricalQueryParams,
data: List[Dict],
**kwargs: Any,
) -> List[TiingoEquityHistoricalData]:
"""Return the transformed data."""
return [TiingoEquityHistoricalData.model_validate(d) for d in data]

View File

@@ -0,0 +1,96 @@
"""Tiingo Global News."""
from datetime import datetime
from typing import Any, Dict, List, Optional
from dateutil import parser
from openbb_provider.abstract.fetcher import Fetcher
from openbb_provider.standard_models.global_news import (
GlobalNewsData,
GlobalNewsQueryParams,
)
from openbb_provider.utils.helpers import get_querystring
from openbb_tiingo.utils.helpers import get_data_many
from pydantic import Field, field_validator
class TiingoGlobalNewsQueryParams(GlobalNewsQueryParams):
"""Tiingo Global News query.
Source: https://www.tiingo.com/documentation/news
"""
source: Optional[str] = Field(
default=None, description="A comma-separated list of the domains requested."
)
class TiingoGlobalNewsData(GlobalNewsData):
"""Tiingo Global News data."""
__alias_dict__ = {
"date": "publishedDate",
"text": "description",
}
symbols: str = Field(
description="Ticker tagged in the fetched news.", alias="tickers"
)
article_id: int = Field(description="Unique ID of the news article.", alias="id")
site: str = Field(description="Name of the news source.", alias="source")
tags: str = Field(description="Tags associated with the news article.")
crawl_date: datetime = Field(description="Date the news article was crawled.")
@field_validator("tags", "symbols", mode="before")
@classmethod
def list_to_string(cls, v):
"""Convert list to string."""
return ",".join(v)
@field_validator("crawl_date", mode="before")
@classmethod
def validate_date(cls, v):
"""Validate the date."""
return parser.parse(v)
@field_validator("symbols", mode="after")
@classmethod
def symbols_validate(cls, v: str):
"""Convert symbols to upper case."""
return v.upper()
class TiingoGlobalNewsFetcher(
Fetcher[
TiingoGlobalNewsQueryParams,
List[TiingoGlobalNewsData],
]
):
"""Transform the query, extract and transform the data from the tiingo endpoints."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> TiingoGlobalNewsQueryParams:
"""Transform the query params."""
return TiingoGlobalNewsQueryParams(**params)
@staticmethod
def extract_data(
query: TiingoGlobalNewsQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[Dict]:
"""Return the raw data from the tiingo endpoint."""
api_key = credentials.get("tiingo_token") if credentials else ""
base_url = "https://api.tiingo.com/tiingo/news"
query_str = get_querystring(query.model_dump(by_alias=True), [])
url = f"{base_url}?{query_str}&token={api_key}"
return get_data_many(url)
@staticmethod
def transform_data(
query: TiingoGlobalNewsQueryParams, data: List[Dict], **kwargs: Any
) -> List[TiingoGlobalNewsData]:
"""Return the transformed data."""
return [TiingoGlobalNewsData.model_validate(d) for d in data]

View File

@@ -0,0 +1,63 @@
"""Tiingo Trailing Dividend Yield fetcher."""
from typing import Any, Dict, List, Optional
from openbb_provider.abstract.fetcher import Fetcher
from openbb_provider.standard_models.trailing_dividend_yield import (
TrailingDivYieldData,
TrailingDivYieldQueryParams,
)
from openbb_tiingo.utils.helpers import get_data_many
class TiingoTrailingDivYieldQueryParams(TrailingDivYieldQueryParams):
"""Tiingo Trailing Dividend Yield Query.
Source: https://www.tiingo.com/documentation/end-of-day
"""
class TiingoTrailingDivYieldData(TrailingDivYieldData):
"""Tiingo Trailing Dividend Yield Data."""
__alias_dict__ = {"trailing_dividend_yield": "trailingDiv1Y"}
class TiingoTrailingDivYieldFetcher(
Fetcher[
TiingoTrailingDivYieldQueryParams,
List[TiingoTrailingDivYieldData],
]
):
"""Transform the query, extract and transform the data from the Tiingo endpoints."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> TiingoTrailingDivYieldQueryParams:
"""Transform the query params."""
transformed_params = params
return TiingoTrailingDivYieldQueryParams(**transformed_params)
# pylint: disable=protected-access
@staticmethod
def extract_data(
query: TiingoTrailingDivYieldQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[Dict]:
"""Return the raw data from the Tiingo endpoint."""
api_key = credentials.get("tiingo_token") if credentials else ""
url = (
f"https://api.tiingo.com/tiingo/corporate-actions/{query.symbol}/distribution-yield?"
f"token={api_key}"
)
return get_data_many(url)
# pylint: disable=unused-argument
@staticmethod
def transform_data(
query: TiingoTrailingDivYieldQueryParams,
data: List[Dict],
**kwargs: Any,
) -> List[TiingoTrailingDivYieldData]:
"""Return the transformed data."""
return [TiingoTrailingDivYieldData.model_validate(d) for d in data]

View File

@@ -0,0 +1 @@
"""Tiingo helpers."""

View File

@@ -0,0 +1,108 @@
"""Tiingo Helpers Module."""
import json
from io import StringIO
from typing import Any, List, Optional, TypeVar, Union
import requests
from openbb_provider import helpers
from openbb_provider.utils.errors import EmptyDataError
from pydantic import BaseModel
from requests.exceptions import SSLError
T = TypeVar("T", bound=BaseModel)
class BasicResponse:
"""Basic Response class."""
def __init__(self, response: StringIO):
"""Initialize the BasicResponse class."""
# Find a way to get the status code
self.status_code = 200
response.seek(0)
self.text = response.read()
def json(self) -> dict:
"""Return the response as a dictionary."""
return json.loads(self.text)
def request(url: str) -> BasicResponse:
"""
Request function for PyScript. Pass in Method and make sure to await.
Parameters:
-----------
url: str
URL to make request to
Return:
-------
response: BasicRequest
BasicRequest object with status_code and text attributes
"""
# pylint: disable=import-outside-toplevel
from pyodide.http import open_url # type: ignore
response = open_url(url)
return BasicResponse(response)
def get_data(url: str, **kwargs: Any) -> Union[list, dict]:
"""Get data from Tiingo endpoint."""
try:
r: Union[requests.Response, BasicResponse] = helpers.make_request(url, **kwargs)
except SSLError:
r = request(url)
if r.status_code == 404:
raise RuntimeError("Tiingo endpoint doesn't exist")
data = r.json()
if r.status_code != 200:
error = data.get("detail")
raise RuntimeError(f"Error in Tiingo request -> {error}")
if len(data) == 0:
raise EmptyDataError()
return data
def get_data_many(
url: str, sub_dict: Optional[str] = None, **kwargs: Any
) -> List[dict]:
"""Get data from Tiingo endpoint and convert to list of schemas.
Parameters:
-----------
url: str
The URL to get the data from.
sub_dict: Optional[str]
The sub-dictionary to use.
Returns:
--------
List[dict]
Dictionary of data.
"""
data = get_data(url, **kwargs)
if sub_dict and isinstance(data, dict):
data = data.get(sub_dict, [])
if isinstance(data, dict):
raise ValueError("Expected list of dicts, got dict")
return data
def get_data_one(url: str, **kwargs: Any) -> dict:
"""Get data from Tiingo endpoint and convert to schema."""
data = get_data(url, **kwargs)
if isinstance(data, list):
if len(data) == 0:
raise ValueError("Expected dict, got empty list")
try:
data = {i: data[i] for i in range(len(data))} if len(data) > 1 else data[0]
except TypeError as e:
raise ValueError("Expected dict, got list of dicts") from e
return data

1400
openbb_platform/providers/tiingo/poetry.lock generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
[tool.poetry]
name = "openbb-tiingo"
version = "1.0.0b0"
description = "Tiingo extension for OpenBB"
authors = ["OpenBB Team <hello@openbb.co>"]
readme = "README.md"
packages = [{ include = "openbb_tiingo" }]
[tool.poetry.dependencies]
python = "^3.8"
openbb-core = "^1.0.0b0"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.plugins."openbb_provider_extension"]
tiingo = "openbb_tiingo:tiingo_provider"

View File

@@ -0,0 +1,152 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate, br
Connection:
- keep-alive
method: GET
uri: https://api.tiingo.com/tiingo/news?limit=20&tickers=AAPL%2CMSFT&token=MOCK_TOKEN
response:
body:
string: "[{\"id\":59591345,\"publishedDate\":\"2023-11-16T08:31:31Z\",\"title\":\"I
walked 5,500 steps with the Google Pixel Watch 2 and Fitbit Charge 6 \u2014
here\u2019s which was more accurate\",\"url\":\"https://www.tomsguide.com/features/i-walked-5500-steps-with-the-google-pixel-watch-2-and-fitbit-charge-6-heres-which-was-more-accurate\",\"description\":\"Here\u2019s
which came out on top\",\"source\":\"tomsguide.com\",\"tags\":[\"Stock\",\"Technology\"],\"crawlDate\":\"2023-11-16T08:34:32.287630Z\",\"tickers\":[\"aapl\",\"goog\",\"googl\"]},{\"id\":59591139,\"publishedDate\":\"2023-11-16T08:22:28.911292Z\",\"title\":\"Sprinklr
and infinit.cx Partner to Deliver AI-Powered Contact Center and Customer Experience
Solutions\",\"url\":\"http://www.businesswire.com/news/home/20231116087060/en/Sprinklr-and-infinit.cx-Partner-to-Deliver-AI-Powered-Contact-Center-and-Customer-Experience-Solutions\",\"description\":\"Sprinklr
(NYSE: CXM), the unified customer experience management (Unified-CXM) platform
for modern enterprises, today announced a partnership with Con\",\"source\":\"businesswire.com\",\"tags\":[\"Stock\",\"Technology\",\"Unknown
Sector\"],\"crawlDate\":\"2023-11-16T08:22:47.067660Z\",\"tickers\":[\"cxm\",\"dtegy\",\"msft\"]},{\"id\":59591138,\"publishedDate\":\"2023-11-16T08:22:28.901883Z\",\"title\":\"Sprinklr
und infinit.cx kooperieren bei KI-gesttztem Contact Center und Customer Experience-Lsungen\",\"url\":\"http://www.businesswire.com/news/home/20231116060376/de/\",\"description\":\"Sprinklr
(NYSE: CXM), die Plattform f\xFCr einheitliches Customer Experience Management
(Unified-CXM) f\xFCr moderne Unternehmen, gab heute eine Partnerscha\",\"source\":\"businesswire.com\",\"tags\":[\"Stock\",\"Technology\",\"Unknown
Sector\"],\"crawlDate\":\"2023-11-16T08:22:45.583804Z\",\"tickers\":[\"cxm\",\"dtegy\",\"msft\"]},{\"id\":59591365,\"publishedDate\":\"2023-11-16T08:19:00Z\",\"title\":\"Data
Center Networking Market Report Published: A Comprehensive Report on Market
Size, Growth Drivers, and Key Players\",\"url\":\"https://finance.yahoo.com/news/data-center-networking-market-report-081900779.html\",\"description\":\"The
global data center networking market is expected to touch US$ 48.3 billion
by 2030, with an annual growth rate of more than 11.5%Wilmington, Delaware,
Nov. 16, 2023 (GLOBE NEWSWIRE) -- Global Data Center Networking Market is
valued at US$ 22.6 billion in 2023 and is expected to grow at a significant
CAGR of over 11.5% over the forecast period of 2023-2030, according to the
published market report by RationalStat Market Definition, Market Scope, and
Report Overview The advent of digital techn\",\"source\":\"finance.yahoo.com\",\"tags\":[\"Cagr\",\"Communication
Services\",\"Data Center\",\"Forecast Period\",\"Market Intelligence\",\"Market
Share\",\"Market Sizing\",\"Networking Market\",\"Stock\",\"Technology\",\"Unknown
Sector\"],\"crawlDate\":\"2023-11-16T08:36:32.393928Z\",\"tickers\":[\"anet\",\"cagr\",\"csco\",\"dell\",\"dvmt\",\"fjtsy\",\"intc\",\"iqnt\",\"jnpr\",\"msft\",\"orcl\",\"vmw\"]},{\"id\":59590759,\"publishedDate\":\"2023-11-16T08:04:55Z\",\"title\":\"J
Arnold Wealth Management Co Bolsters Portfolio with First Trust Consumer Staples
AlphaDEX ...\",\"url\":\"https://www.gurufocus.com/search?s=j-arnold-wealth-management-co-bolsters-portfolio-with-first-trust-consumer-staples-alphadex-fund-fxg-shares\",\"description\":\"Search
for Stocks, ETFs, Indices, Insiders, Investing Gurus and Futures on Gurufocus.com.\",\"source\":\"gurufocus.com\",\"tags\":[\"ETF\",\"Healthcare\",\"Investing
Guru Lookup\",\"Stock\",\"Stock Insider Lookup\",\"Stock Symbol Lookup\",\"Symbol
Lookup\",\"Technology\",\"Ticker Lookup\",\"Unknown Sector\"],\"crawlDate\":\"2023-11-16T08:05:24.260582Z\",\"tickers\":[\"aapl\",\"fxg\",\"fxg\",\"ihi\",\"isrg\",\"voo\"]},{\"id\":59591215,\"publishedDate\":\"2023-11-16T08:04:55Z\",\"title\":\"J
Arnold Wealth Management Co Bolsters Portfolio with First Trust Consumer Staples
AlphaDEX ...\",\"url\":\"https://finance.yahoo.com/news/j-arnold-wealth-management-co-080455684.html\",\"description\":\"J
Arnold Wealth Management Co (Trades, Portfolio), a recognized investment firm,
has recently expanded its portfolio with a significant addition of shares
in the First Trust Consumer Staples AlphaDEX Fund (FXG). On October 31, 2023,
the firm acquired 101,9722 shares of FXG, marking a notable trade action that
reflects a strategic investment move. The transaction, which had a trade
impact of 0.22% on the firm's portfolio, was executed at a trade price of
$58.12.\",\"source\":\"finance.yahoo.com\",\"tags\":[\"Consumer Staples\",\"Healthcare\",\"Portfolio\",\"Stock\",\"Strategic
Investment\",\"Technology\",\"Trade Impact\",\"Trade Price\",\"Wealth Management\"],\"crawlDate\":\"2023-11-16T08:25:50.754245Z\",\"tickers\":[\"aapl\",\"isrg\"]},{\"id\":59590789,\"publishedDate\":\"2023-11-16T08:01:49Z\",\"title\":\"Black
Friday Apple deals \u2014 7 best sales to shop right now\",\"url\":\"https://www.tomsguide.com/deals/black-friday-apple-deals-7-best-sales-to-shop-right-now\",\"description\":\"Save
on iPhone 15, iPads, MacBooks and more\",\"source\":\"tomsguide.com\",\"tags\":[\"Stock\",\"Technology\"],\"crawlDate\":\"2023-11-16T08:06:23.382809Z\",\"tickers\":[\"aapl\"]},{\"id\":59590788,\"publishedDate\":\"2023-11-16T08:01:24Z\",\"title\":\"How
to change keyboard shortcuts in Windows 11\",\"url\":\"https://www.tomsguide.com/how-to/how-to-change-keyboard-shortcuts-in-windows-11\",\"description\":\"Change
keyboard shortcuts in Windows if the default ones don't work for you\",\"source\":\"tomsguide.com\",\"tags\":[\"Stock\",\"Technology\"],\"crawlDate\":\"2023-11-16T08:06:22.603414Z\",\"tickers\":[\"msft\"]},{\"id\":59590685,\"publishedDate\":\"2023-11-16T08:00:00Z\",\"title\":\"Sprinklr
and infinit.cx Partner to Deliver AI-Powered Contact Center and Customer Experience
Solutions\",\"url\":\"https://www.businesswire.com/news/home/20231116087060/en/Sprinklr-and-infinit.cx-Partner-to-Deliver-AI-Powered-Contact-Center-and-Customer-Experience-Solutions/?feedref=JjAwJuNHiystnCoBq_hl-d0q77hRjGWvCunvqSsMC5PkKuEqWGdtYcKRejd3o8qMqnKfqP6Z7nZVMLG-WqUNUuIFBOuQU34x9_qUrqnb9IFt3G9xeKEyIFKgj2nxZILuWb2_mZzaa2HVGkrPJ34F1Q%3D%3D\",\"description\":\"Sprinklr
(NYSE: CXM), the unified customer experience management (Unified-CXM) platform
for modern enterprises, today announced a partnership with Con\",\"source\":\"businesswire.com\",\"tags\":[\"Stock\",\"Technology\",\"Unknown
Sector\"],\"crawlDate\":\"2023-11-16T08:03:15.847532Z\",\"tickers\":[\"cxm\",\"dtegy\",\"msft\"]},{\"id\":59590686,\"publishedDate\":\"2023-11-16T08:00:00Z\",\"title\":\"Sprinklr
und infinit.cx kooperieren bei KI-gesttztem Contact Center und Customer Experience-Lsungen\",\"url\":\"https://www.businesswire.com/news/home/20231116060376/de/?feedref=JjAwJuNHiystnCoBq_hl-d0q77hRjGWvCunvqSsMC5PkKuEqWGdtYcKRejd3o8qMqnKfqP6Z7nZVMLG-WqUNUuIFBOuQU34x9_qUrqnb9IFt3G9xeKEyIFKgj2nxZILuWb2_mZzaa2HVGkrPJ34F1Q%3D%3D\",\"description\":\"Sprinklr
(NYSE: CXM), die Plattform f\xFCr einheitliches Customer Experience Management
(Unified-CXM) f\xFCr moderne Unternehmen, gab heute eine Partnerscha\",\"source\":\"businesswire.com\",\"tags\":[\"Stock\",\"Technology\",\"Unknown
Sector\"],\"crawlDate\":\"2023-11-16T08:03:19.469883Z\",\"tickers\":[\"cxm\",\"dtegy\",\"msft\"]},{\"id\":59590811,\"publishedDate\":\"2023-11-16T08:00:00Z\",\"title\":\"Uberall
Announces Enhanced Integration Experience with Apple Business Connect\",\"url\":\"http://www.hawaiinewsnow.com/prnewswire/2023/11/16/uberall-announces-enhanced-integration-experience-with-apple-business-connect/\",\"description\":\"Uberall
Announces Enhanced Integration Experience with Apple Business Connect\\nPublished:
Nov. 15, 2023 at 10:00 PM HST\\n|\\nUpdated: 7 minutes ago\\nNew capability
will empower multi-location businesses to manage their presence in the Apple
ecosystem dire\",\"source\":\"hawaiinewsnow.com\",\"tags\":[\"CryptoCurrency\",\"Stock\",\"Technology\"],\"crawlDate\":\"2023-11-16T08:08:30.008038Z\",\"tickers\":[\"aapl\"]},{\"id\":59590969,\"publishedDate\":\"2023-11-16T08:00:00Z\",\"title\":\"Virtual
Reality Headsets Market projected to surpass USD 61.6 Billion by 2030, growing
at a CAGR of 29.6% during the forecast period of 2023-2030 - professed by
MarketDigits in its recent study.\",\"url\":\"https://finance.yahoo.com/news/virtual-reality-headsets-market-projected-080000399.html\",\"description\":\"The
Global Virtual Reality Headsets Market was valued USD 7.8 Billion in 2022
and projected to reach USD 61.6 Billion by 2030, growing at a CAGR of 29.6%
during the forecast period of 2023-2030.Richmond, Nov. 16, 2023 (GLOBE NEWSWIRE)
-- According to a research report \\\"Virtual Reality Headsets Market, by
End-device (Low-end Device, Mid-range Device, High-end Device), Product Type
(Standalone, Smartphone-enabled, Standalone PC-connected), Application (Gaming,
Healthcare, Media & Entertainment, Ma\",\"source\":\"finance.yahoo.com\",\"tags\":[\"Application\",\"Asia
Pacific\",\"Entertainment\",\"Media & Entertainment\",\"North America\",\"Stock\",\"Technology\",\"Virtual
Reality\",\"Virtual Reality Headsets\"],\"crawlDate\":\"2023-11-16T08:14:30.202071Z\",\"tickers\":[\"msft\"]},{\"id\":59590984,\"publishedDate\":\"2023-11-16T08:00:00Z\",\"title\":\"interop.io
and comitFS Form Strategic Partnership to Revolutionize Trader-Client Conversations\",\"url\":\"https://finance.yahoo.com/news/interop-io-comitfs-form-strategic-080000985.html\",\"description\":\"interop.io,
the leading interoperability provider for the finance industry, and comitFS,
the market leader in optimizing high touch voice conversations, today announced
their strategic partnership, marking a significant leap in the automation
of voice communication within capital markets.\",\"source\":\"finance.yahoo.com\",\"tags\":[\"Stock\",\"Strategic
Partnership\",\"Technology\",\"Voice Conversations\",\"Workflow Automation\"],\"crawlDate\":\"2023-11-16T08:14:52.193412Z\",\"tickers\":[\"csco\",\"msft\"]},{\"id\":59590994,\"publishedDate\":\"2023-11-16T08:00:00Z\",\"title\":\"interop.io
and comitFS Form Strategic Partnership to Revolutionize Trader-Client Conversations\",\"url\":\"https://finance.yahoo.com/news/interop-io-comitfs-form-strategic-080000483.html\",\"description\":\"interop.io,
the leading interoperability provider for the finance industry, and comitFS,
the market leader in optimizing high touch voice conversations, today announced
their strategic partnership, marking a significant leap in the automation
of voice communication within capital markets.\",\"source\":\"finance.yahoo.com\",\"tags\":[\"Stock\",\"Strategic
Partnership\",\"Technology\",\"Voice Conversations\",\"Workflow Automation\"],\"crawlDate\":\"2023-11-16T08:15:08.220216Z\",\"tickers\":[\"csco\",\"msft\"]},{\"id\":59590995,\"publishedDate\":\"2023-11-16T08:00:00Z\",\"title\":\"Sprinklr
und infinit.cx kooperieren bei KI-gesttztem Contact Center und Customer Experience-Lsungen\",\"url\":\"https://www.businesswire.com/news/home/20231116060376/de/?feedref=JjAwJuNHiystnCoBq_hl-fqXyBvmekC3Xq1G1wQ7hjMFbZM1ntlKeK-8bUzJqFSVSfe41V1BKA-b6v2ZVrSSNs-dUYKiDvY7XniNSqv88QcY1Ge_vf9QIMZ4alzIf_0HlyE0C4hZhHVlTvKnn0d0vQ%3D%3D\",\"description\":\"Sprinklr
(NYSE: CXM), die Plattform f\xFCr einheitliches Customer Experience Management
(Unified-CXM) f\xFCr moderne Unternehmen, gab heute eine Partnerscha\",\"source\":\"businesswire.com\",\"tags\":[\"Stock\",\"Technology\",\"Unknown
Sector\"],\"crawlDate\":\"2023-11-16T08:15:24.206362Z\",\"tickers\":[\"cxm\",\"dtegy\",\"msft\"]},{\"id\":59590997,\"publishedDate\":\"2023-11-16T08:00:00Z\",\"title\":\"Sprinklr
and infinit.cx Partner to Deliver AI-Powered Contact Center and Customer Experience
Solutions\",\"url\":\"https://www.businesswire.com/news/home/20231116087060/en/Sprinklr-and-infinit.cx-Partner-to-Deliver-AI-Powered-Contact-Center-and-Customer-Experience-Solutions/?feedref=JjAwJuNHiystnCoBq_hl-fqXyBvmekC3Xq1G1wQ7hjMFbZM1ntlKeK-8bUzJqFSVSfe41V1BKA-b6v2ZVrSSNs-dUYKiDvY7XniNSqv88QcY1Ge_vf9QIMZ4alzIf_0HlyE0C4hZhHVlTvKnn0d0vQ%3D%3D\",\"description\":\"Sprinklr
(NYSE: CXM), the unified customer experience management (Unified-CXM) platform
for modern enterprises, today announced a partnership with Con\",\"source\":\"businesswire.com\",\"tags\":[\"Stock\",\"Technology\",\"Unknown
Sector\"],\"crawlDate\":\"2023-11-16T08:15:25.737765Z\",\"tickers\":[\"cxm\",\"dtegy\",\"msft\"]},{\"id\":59591209,\"publishedDate\":\"2023-11-16T08:00:00Z\",\"title\":\"Sprinklr
and infinit.cx Partner to Deliver AI-Powered Contact Center and Customer Experience
Solutions\",\"url\":\"https://finance.yahoo.com/news/sprinklr-infinit-cx-partner-deliver-080000313.html\",\"description\":\"NEW
YORK, November 16, 2023--Sprinklr (NYSE: CXM), the unified customer experience
management (Unified-CXM) platform for modern enterprises, today announced
a partnership with Contact Center as a Service (CCaaS) and customer experience
specialists infinit.cx to help enterprises optimize Sprinklr Service and facilitate
a seamless, transformational migration to cloud-based customer service across
30+ digital channels. Through this partnership, enterprises can future-proof
their customer service, m\",\"source\":\"finance.yahoo.com\",\"tags\":[\"Customer
Experience Management\",\"Customer Service\",\"Sprinklr\",\"Stock\",\"Technology\",\"Technology
Enterprises\",\"Unknown Sector\"],\"crawlDate\":\"2023-11-16T08:25:40.107409Z\",\"tickers\":[\"cxm\",\"dtegy\",\"msft\"]},{\"id\":59590684,\"publishedDate\":\"2023-11-16T07:59:36Z\",\"title\":\"iPhone
15 Wireless Charging Is Now Broken In GM Cars After Issues With BMW\",\"url\":\"https://www.benzinga.com/news/23/11/35820909/iphone-15-wireless-charging-is-now-broken-in-gm-cars-after-issues-with-bmw\",\"description\":\"After
BMW, now GM car owners are reporting iPhone 15 wireless charging issues, while
some users have been told by Apple that it's a GM issue.\",\"source\":\"benzinga.com\",\"tags\":[\"Aapl\",\"Apple\",\"Appleverse\",\"Benzinga\",\"Bmwyy\",\"Consumer
Cyclical\",\"Consumer Tech\",\"General Motors\",\"Gm\",\"Iphone\",\"Iphone
15\",\"Mobility\",\"News\",\"Stock\",\"Tech\",\"Technology\",\"Tiingo Top\",\"Unknown
Sector\"],\"crawlDate\":\"2023-11-16T08:02:55.629506Z\",\"tickers\":[\"aapl\",\"bmwyy\",\"gm\",\"ne-ws\"]},{\"id\":59590693,\"publishedDate\":\"2023-11-16T07:47:00Z\",\"title\":\"Diginius
is thrilled to announce the official launch of the Shopify app, Diginius Shopping,
engineered to streamline Microsoft Shopping campaigns for Shopify store users\",\"url\":\"https://finance.yahoo.com/news/diginius-thrilled-announce-official-launch-074700630.html\",\"description\":\"Diginius
is thrilled to announce the official launch of the Shopify app, Diginius Shopping.
This innovative app aims to streamline Microsoft Shopping management for Shopify
store owners advertising on Bing.\",\"source\":\"finance.yahoo.com\",\"tags\":[\"Diginius
Shopping\",\"Microsoft\",\"Microsoft Advertising\",\"Shopify\",\"Shopping
App\",\"Stock\",\"Technology\"],\"crawlDate\":\"2023-11-16T08:03:26.584670Z\",\"tickers\":[\"msft\"]},{\"id\":59589990,\"publishedDate\":\"2023-11-16T07:34:05.673695Z\",\"title\":\"TikTok
joins Meta in appealing against EU gatekeeper status\",\"url\":\"http://www.streetinsider.com/Reuters/TikTok+joins+Meta+in+appealing+against+EU+gatekeeper+status/22421627.html\",\"description\":\"\\nBy
Supantha Mukherjee\\n STOCKHOLM (Reuters) - TikTok on Thursday
joined Meta in appealing against the \\\"gatekeeper\\\" status under the Digital
Markets Act (DMA), an EU law that brings in tougher rules for tech...\\n\",\"source\":\"streetinsider.com\",\"tags\":[\"Stock\",\"Technology\"],\"crawlDate\":\"2023-11-16T07:34:06.714809Z\",\"tickers\":[\"goog\",\"googl\",\"msft\"]}]"
headers:
allow:
- GET, HEAD, OPTIONS
content-length:
- '14859'
content-type:
- application/json
date:
- Thu, 16 Nov 2023 08:39:36 GMT
server:
- nginx/1.18.0 (Ubuntu)
vary:
- Cookie, Origin
x-frame-options:
- SAMEORIGIN
status:
code: 200
message: OK
version: 1

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,138 @@
interactions:
- request:
body: null
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate, br
Connection:
- keep-alive
method: GET
uri: https://api.tiingo.com/tiingo/news?limit=20&token=MOCK_TOKEN
response:
body:
string: "[{\"id\":59591461,\"publishedDate\":\"2023-11-16T08:38:56Z\",\"title\":\"How
to navigate higher education finances for a transformative journey? MintGenie
explains\",\"url\":\"https://www.livemint.com/money/personal-finance/how-to-navigate-higher-education-finances-for-a-transformative-journey-mintgenie-explains-11699517017204.html\",\"description\":\"Navigating
higher education finances requires careful planning and a personal approach.
Prioritize, stay informed, budget effectively, seek scholarships, explore
investments, and manage loans wisely to establish a robust foundation for
a successful educational journey.\",\"source\":\"livemint.com\",\"tags\":[\"Budgeting\",\"Cost
Of Education\",\"Educational Goals\",\"Expert Speak\",\"Financial Goals\",\"Financial
Planning\",\"Financial Resources\",\"Higher Education\",\"Inflation\",\"Investing\",\"Loans\",\"Mutual
Funds\",\"Personal Finance\",\"Scholarships\",\"Stocks\",\"Tax Benefits\"],\"crawlDate\":\"2023-11-16T08:39:15.354554Z\",\"tickers\":[]},{\"id\":59591464,\"publishedDate\":\"2023-11-16T08:38:22Z\",\"title\":\"Broome
financial counsellor Sen Amaratunga recognised for service to community\",\"url\":\"https://thewest.com.au/news/broome-advertiser/broome-financial-counsellor-sen-amaratunga-recognised-for-service-to-community--c-12574080\",\"description\":\"A
Broome financial counsellor has been honoured with an award for his dedication
to servicing the community.\_\",\"source\":\"thewest.com.au\",\"tags\":[\"Broome-Advertiser\",\"Kimberley\"],\"crawlDate\":\"2023-11-16T08:39:32.545512Z\",\"tickers\":[]},{\"id\":59591466,\"publishedDate\":\"2023-11-16T08:38:00Z\",\"title\":\"NetEase
Announces Third Quarter 2023 Unaudited Financial Results\",\"url\":\"https://www.prnewswire.com/news-releases/netease-announces-third-quarter-2023-unaudited-financial-results-301990338.html\",\"description\":\"/PRNewswire/
-- NetEase, Inc. (NASDAQ: NTES and HKEX: 9999, \\\"NetEase\\\" or the \\\"Company\\\"),
a leading internet and game services provider, today announced its...\",\"source\":\"prnewswire.com\",\"tags\":[\"Banking
& Financial Services\",\"Computer & Electronics\",\"Computer Software\",\"Earnings\",\"Earnings
Forecasts & Projections\",\"Electronic Gaming\",\"Entertainment\",\"Internet
Technology\",\"Mobile Entertainment\",\"Multimedia & Internet\",\"Telecommunications
Industry\",\"Web Site\"],\"crawlDate\":\"2023-11-16T08:39:47.836668Z\",\"tickers\":[]},{\"id\":59591465,\"publishedDate\":\"2023-11-16T08:37:46Z\",\"title\":\"UK's
Cameron makes first official visit to Ukraine\",\"url\":\"https://thewest.com.au/news/conflict/uks-cameron-makes-first-official-visit-to-ukraine-c-12578128\",\"description\":\"David
Cameron has met President Volodymyr Zelenskiy to underscore Britain's support
for Ukraine on his first official trip as foreign secretary.\",\"source\":\"thewest.com.au\",\"tags\":[\"Conflict\",\"Ukraine\"],\"crawlDate\":\"2023-11-16T08:39:33.544746Z\",\"tickers\":[]},{\"id\":59591382,\"publishedDate\":\"2023-11-16T08:37:25.324087Z\",\"title\":\"Retail,
institutional investors to be subject to same rules for short selling for
fairness\",\"url\":\"https://koreatimes.co.kr/www/nation/2023/11/488_363360.html?utm_source=fl\",\"description\":\"The
ruling People Power Party and financial authorities have announced plans to
apply the same conditions for the local short selling system for both retail
and institutional investors. \",\"source\":\"koreatimes.co.kr\",\"tags\":[\"Fsc\",\"Fss\",\"Korea\",\"Korea
Times\",\"Korean In English\",\"Koreatimes\",\"Short Selling\",\"South Korea\",\"The
Korea Times\",\"Tiingo Top\"],\"crawlDate\":\"2023-11-16T08:37:26.483241Z\",\"tickers\":[]},{\"id\":59591420,\"publishedDate\":\"2023-11-16T08:37:24Z\",\"title\":\"Bill
Director buys stock worth $1M (NYSE:BILL)\",\"url\":\"https://seekingalpha.com/news/4037421-bill-director-buys-stock-worth-1m\",\"description\":\"Bill
Holdings director David Hornik purchased $1M worth of shares at $56.49 each.
The stock price is up 1.9% to $60.75 pre-market.\",\"source\":\"seekingalpha.com\",\"tags\":[\"Stock\",\"Unknown
Sector\"],\"crawlDate\":\"2023-11-16T08:38:21.175265Z\",\"tickers\":[\"bill\"]},{\"id\":59591429,\"publishedDate\":\"2023-11-16T08:37:11.873000Z\",\"title\":\"New
Delhi smog grows more intense as farm fires rage\",\"url\":\"https://www.reuters.com/world/india/new-delhi-smog-grows-more-intense-farm-fires-rage-2023-11-16/\",\"description\":\"Toxic
smog gripping New Delhi grew more intense on Thursday as farm fires raged
in nearby fields despite a court-ordered ban, making India's capital the world's
most polluted city again.\",\"source\":\"reuters.com\",\"tags\":[\"2023:Ml_Kbn32B0Iw\",\"2023:Ml_Kbn32B0Iw:1\",\"2023:Ml_L1N3Ch0Kv\",\"2023:Ml_L1N3Ch0Kv:1327319944\",\"2023:Ml_M1L1N3Ch0Kv\",\"2023:Ml_M1L1N3Ch0Kv:1327319944\",\"Guid:Tag:Reuters.Com\",\"India\",\"Usn:Kbn32B0Iw\",\"Usn:L1N3Ch0Kv\",\"Usn:M1L1N3Ch0Kv\",\"Vguid:Tag:Reuters.Com\"],\"crawlDate\":\"2023-11-16T08:38:28.349029Z\",\"tickers\":[]},{\"id\":59591435,\"publishedDate\":\"2023-11-16T08:37:09Z\",\"title\":\"Fans
to miss Coldplay after flight cancellation\",\"url\":\"https://www.smh.com.au/national/fans-to-miss-coldplay-after-flight-cancellation-20231116-p5eko8.html?ref=rss&utm_medium=rss&utm_source=rss_feed\",\"description\":\"Coldplay
and Robbie Williams fans are devastated to miss their concerts after flights
were cancelled from Adelaide Airport.\",\"source\":\"smh.com.au\",\"tags\":[\"National\"],\"crawlDate\":\"2023-11-16T08:38:31.298558Z\",\"tickers\":[]},{\"id\":59591380,\"publishedDate\":\"2023-11-16T08:37:05Z\",\"title\":\"Bitcoin\u2019s
Price is Pumping in the Past 24 Hours: Here\u2019s Why\",\"url\":\"https://cryptopotato.com/bitcoins-price-is-pumping-in-the-past-24-hours-heres-why/\",\"description\":\"BTC
rose to almost $38K following the SEC's latest delay on crypto ETF applications.\",\"source\":\"cryptopotato.com\",\"tags\":[\"Bitcoin\",\"Bitcoin
(Btc) Price\",\"Crypto Bits\",\"Crypto News\",\"CryptoCurrency\",\"ETF\",\"Financial
Services\",\"Real Estate\",\"Stock\",\"Unknown Sector\"],\"crawlDate\":\"2023-11-16T08:37:20.616538Z\",\"tickers\":[\"bcf\",\"bgio\",\"blk\",\"ihit\",\"ivr\",\"ivz\"]},{\"id\":59591430,\"publishedDate\":\"2023-11-16T08:37:00Z\",\"title\":\"ATP
Finals LIVE: Novak Djokovic given stark warning as Nick Kyrgios makes X-rated
error\",\"url\":\"https://www.express.co.uk/sport/tennis/1835605/ATP-Finals-LIVE-Novak-Djokovic-Nick-Kyrgios-tennis-scores-results-highlights\",\"description\":\"Express
Sport is on hand to provide the latest news from the ATP Finals.\",\"source\":\"express.co.uk\",\"tags\":[\"Andrey
Rublev\",\"Atp Finals\",\"Atp Finals Highlights\",\"Atp Finals Results\",\"Atp
Finals Scores\",\"Nick Kyrgios\",\"Novak Djokovic\",\"Rafael Nadal\",\"Tennis\",\"Tennis
Scores\"],\"crawlDate\":\"2023-11-16T08:38:28.605087Z\",\"tickers\":[]},{\"id\":59591426,\"publishedDate\":\"2023-11-16T08:36:48Z\",\"title\":\"Royal
Mail blames strikes and slump in letter sending as losses deepen\",\"url\":\"https://www.telegraph.co.uk/business/2023/11/16/royal-mail-blames-strikes-slump-letter-sending-losses/\",\"description\":\"Postal
company suffers \xA3319m hit as boss reiterates calls to scrap Saturday letter
deliveries\",\"source\":\"telegraph.co.uk\",\"tags\":[\"Business\",\"Ofcom\",\"Royal
Mail\",\"Royal Mail Strikes\",\"Standard\",\"Stock\",\"Tiingo Top\",\"Unknown
Sector\"],\"crawlDate\":\"2023-11-16T08:38:27.571781Z\",\"tickers\":[\"roymf\"]},{\"id\":59591434,\"publishedDate\":\"2023-11-16T08:36:05Z\",\"title\":\"Car
rams barricade near Israel embassy in Tokyo, driver detained\",\"url\":\"https://nypost.com/2023/11/16/news/car-rams-barricade-near-israel-embassy-in-tokyo-man-detained/\",\"description\":\"A
man crashed a car into a barricade near the entrance of the Israeli embassy
in Tokyo on Thursday, injuring a police officer.\",\"source\":\"nypost.com\",\"tags\":[\"Hamas\",\"Israel\",\"Israel
War 2023\",\"Tokyo\"],\"crawlDate\":\"2023-11-16T08:38:31.314583Z\",\"tickers\":[]},{\"id\":59591371,\"publishedDate\":\"2023-11-16T08:36:03Z\",\"title\":\"Monet
in Paris: The French Impressionists Alive multi-sensory experience arrives
at Crown Perth\",\"url\":\"https://thewest.com.au/genwest/arts/monet-in-paris-the-french-impressionists-alive-multi-sensory-experience-arrives-at-crown-perth-c-12566259\",\"description\":\"All
the charm of Parisian neighbourhood Montmartre has been transported to Perth
with multi-sensory experience, Monet in Paris \u2013 The French Impressionists
Alive.\",\"source\":\"thewest.com.au\",\"tags\":[\"Arts\"],\"crawlDate\":\"2023-11-16T08:36:41.491701Z\",\"tickers\":[]},{\"id\":59591356,\"publishedDate\":\"2023-11-16T08:35:54Z\",\"title\":\"Synagogue
in Armenia set alight in latest attack on Jewish diaspora\",\"url\":\"https://www.jpost.com/diaspora/antisemitism/article-773485\",\"description\":\"A
similar attack occurred in October by ASALA, which targeted the World Jewish
Center in Yerevan.\",\"source\":\"jpost.com\",\"tags\":[\"Antisemitism\",\"Armenia\",\"Attack\",\"Azerbaijan\",\"Diaspora
Jews\"],\"crawlDate\":\"2023-11-16T08:36:14.626883Z\",\"tickers\":[]},{\"id\":59591377,\"publishedDate\":\"2023-11-16T08:35:54Z\",\"title\":\"Nationwide
protests add to Panama woes\",\"url\":\"https://splash247.com/nationwide-protests-add-to-panama-woes/\",\"description\":\"It\u2019s
not just drought in Panama that is giving supply chain planners pause for
reflection, the Central American nation is also reeling from the largest protests
in the western hemisphere, action that has affected rail, roads and ports
over the past month. Protests that began over a mining contract with a Canadian
company have rocked \u2026\",\"source\":\"splash247.com\",\"tags\":[\"Central
America\",\"Panama\"],\"crawlDate\":\"2023-11-16T08:37:15.350030Z\",\"tickers\":[]},{\"id\":59591358,\"publishedDate\":\"2023-11-16T08:35:03Z\",\"title\":\"Police
investigate after woman\u2019s body found\",\"url\":\"https://www.smh.com.au/national/police-investigate-after-woman-s-body-found-20231116-p5eko7.html?ref=rss&utm_medium=rss&utm_source=rss_feed\",\"description\":\"Detectives
are investigating after a woman's body was found in a block of units in Adelaide\u2019s
east.\",\"source\":\"smh.com.au\",\"tags\":[\"National\"],\"crawlDate\":\"2023-11-16T08:36:19.777723Z\",\"tickers\":[]},{\"id\":59591440,\"publishedDate\":\"2023-11-16T08:35:02.359000Z\",\"title\":\"Macquarie
invests $190 mln in green aviation fuel supplier SkyNRG\",\"url\":\"https://www.reuters.com/sustainability/climate-energy/macquarie-invests-190-mln-green-aviation-fuel-supplier-skynrg-2023-11-16/\",\"description\":\"Australia's
Macquarie Group Ltd said on Thursday its asset management arm will make an
initial investment of up to 175 million euros (about $190 million) in Netherlands-based
sustainable aviation fuel (SAF) supplier SkyNRG.\",\"source\":\"reuters.com\",\"tags\":[\"2023:Ml_Kbn32B0In\",\"2023:Ml_Kbn32B0In:1\",\"2023:Ml_Kbn32B0Io\",\"2023:Ml_Kbn32B0Io:1\",\"2023:Ml_L4N3Ch1Um\",\"2023:Ml_L4N3Ch1Va\",\"2023:Ml_L4N3Ch1Va:722089037\",\"Australia\",\"Guid:Tag:Reuters.Com\",\"Netherlands\",\"Netherlands
Antilles\",\"Stock\",\"Unknown Sector\",\"Usn:Kbn32B0In\",\"Usn:Kbn32B0Io\",\"Usn:L4N3Ch1Um\",\"Usn:L4N3Ch1Va\",\"Vguid:Tag:Reuters.Com\"],\"crawlDate\":\"2023-11-16T08:38:39.691757Z\",\"tickers\":[\"mqg\"]},{\"id\":59591353,\"publishedDate\":\"2023-11-16T08:35:00Z\",\"title\":\"US-Cannabis
Market is to grow by USD 46.90 billion between 2022 - 2027 | Aurora Cannabis
Inc., Canopy Growth Corp., Charlottes Web Holdings Inc. and more among the
key companies\",\"url\":\"https://www.prnewswire.com/news-releases/us-cannabis-market-is-to-grow-by-usd-46-90-billion-between-2022---2027--aurora-cannabis-inc-canopy-growth-corp-charlottes-web-holdings-inc-and-more-among-the-key-companies-301989972.html\",\"description\":\"/PRNewswire/
-- The US-Cannabis Market is projected to witness a surge at a Compound Annual
Growth Rate (CAGR) of 24.03% with USD 46.90 b illion from 2022 to...\",\"source\":\"prnewswire.com\",\"tags\":[\"Cannabis\",\"CryptoCurrency\",\"ETF\",\"FX\",\"Health
Care & Hospitals\",\"Healthcare\",\"Market Research Reports\",\"Stock\",\"Unknown
Sector\"],\"crawlDate\":\"2023-11-16T08:35:37.725202Z\",\"tickers\":[\"acb\",\"twmjf\",\"usd\"]},{\"id\":59591354,\"publishedDate\":\"2023-11-16T08:35:00Z\",\"title\":\"North
America - The Maintenance, Repair, and Overhaul (MRO) Distribution Market
size to grow at a CAGR of 2.43% from 2022 to 2027| A.R. Thomson Group, Applied
Industrial Technologies Inc., AWC Inc. and more among the key companies in
the market -Technavio\",\"url\":\"https://www.prnewswire.com/news-releases/north-america---the-maintenance-repair-and-overhaul-mro-distribution-market-size-to-grow-at-a-cagr-of-2-43-from-2022-to-2027-ar-thomson-group-applied-industrial-technologies-inc-awc-inc-and-more-among-the-key-companies-301989958.html\",\"description\":\"/PRNewswire/
-- The MRO distribution market in North America is estimated to grow at a
CAGR of 2.43% between 2022 and 2027 and the size of the market is...\",\"source\":\"prnewswire.com\",\"tags\":[\"Energy\",\"Industrials\",\"Market
Research Reports\",\"North America\",\"Oil & Energy\",\"Stock\",\"Unknown
Sector\"],\"crawlDate\":\"2023-11-16T08:35:38.520380Z\",\"tickers\":[\"ait\",\"awc\",\"cagr\",\"mro\"]},{\"id\":59591355,\"publishedDate\":\"2023-11-16T08:35:00Z\",\"title\":\"Smartphone
Screen Protector Market is driven by the growing awareness of the importance
of device protection - Technavio\",\"url\":\"https://www.prnewswire.com/news-releases/smartphone-screen-protector-market-is-driven-by-the-growing-awareness-of-the-importance-of-device-protection---technavio-301990019.html\",\"description\":\"/PRNewswire/
-- In a recent analysis conducted by Technavio, titled \\\"Smartphone Screen
Protector Market Analysis - APAC, North America, Europe, South America,...\",\"source\":\"prnewswire.com\",\"tags\":[\"Computer
& Electronics\",\"Consumer Electronics\",\"Market Research Reports\",\"Mobile
Entertainment\",\"North America\",\"South America\"],\"crawlDate\":\"2023-11-16T08:35:39.717934Z\",\"tickers\":[]}]"
headers:
allow:
- GET, HEAD, OPTIONS
content-length:
- '13244'
content-type:
- application/json
date:
- Thu, 16 Nov 2023 08:40:09 GMT
server:
- nginx/1.18.0 (Ubuntu)
vary:
- Cookie, Origin
x-frame-options:
- SAMEORIGIN
status:
code: 200
message: OK
version: 1

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,90 @@
from datetime import date
import pytest
from openbb_core.app.service.user_service import UserService
from openbb_tiingo.models.company_news import TiingoCompanyNewsFetcher
from openbb_tiingo.models.crypto_historical import TiingoCryptoHistoricalFetcher
from openbb_tiingo.models.currency_historical import TiingoCurrencyHistoricalFetcher
from openbb_tiingo.models.equity_historical import TiingoEquityHistoricalFetcher
from openbb_tiingo.models.global_news import TiingoGlobalNewsFetcher
from openbb_tiingo.models.trailing_dividend_yield import TiingoTrailingDivYieldFetcher
test_credentials = UserService().default_user_settings.credentials.model_dump(
mode="json"
)
@pytest.fixture(scope="module")
def vcr_config():
return {
"filter_headers": [("User-Agent", None)],
"filter_query_parameters": [
("token", "MOCK_TOKEN"),
],
}
@pytest.mark.record_http
def test_tiingo_equity_historical_fetcher(credentials=test_credentials):
params = {
"symbol": "AAPL",
"start_date": date(2023, 1, 1),
"end_date": date(2023, 6, 6),
}
fetcher = TiingoEquityHistoricalFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_tiingo_company_news_fetcher(credentials=test_credentials):
params = {"symbols": "AAPL,MSFT"}
fetcher = TiingoCompanyNewsFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_tiingo_global_news_fetcher(credentials=test_credentials):
params = {}
fetcher = TiingoGlobalNewsFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_tiingo_crypto_historical_fetcher(credentials=test_credentials):
params = {
"symbol": "BTCUSD",
"start_date": date(2023, 1, 1),
"end_date": date(2023, 6, 6),
}
fetcher = TiingoCryptoHistoricalFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_tiingo_currency_historical_fetcher(credentials=test_credentials):
params = {
"symbol": "EURUSD",
"start_date": date(2023, 1, 1),
"end_date": date(2023, 6, 6),
}
fetcher = TiingoCurrencyHistoricalFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_tiingo_trailing_div_yield_fetcher(credentials=test_credentials):
params = {"symbol": "SCHD"}
fetcher = TiingoTrailingDivYieldFetcher()
result = fetcher.test(params, credentials)
assert result is None

View File

@@ -17,6 +17,7 @@ openbb-intrinio = "^1.0.0b0"
openbb-oecd = "^1.0.0b0"
openbb-polygon = "^1.0.0b0"
openbb-sec = "^1.0.0b0"
openbb-tiingo = "^1.0.0b0"
openbb-tradingeconomics = "^1.0.0b0"
openbb-crypto = "^1.0.0b0"
@@ -80,6 +81,7 @@ all = [
"openbb-technical",
"openbb-wsj",
"openbb-yfinance",
"openbb-tiingo"
]
[tool.poetry.group.dev.dependencies]