Merge pull request #5554 from OpenBB-finance/feature/tradingeconomics

Trading Economics Economic Calendar
This commit is contained in:
James Maslek
2023-10-12 16:35:39 -04:00
committed by GitHub
15 changed files with 4095 additions and 0 deletions

View File

@@ -22,6 +22,8 @@ openbb-fmp = { path = "./providers/fmp", develop = true }
openbb-fred = { path = "./providers/fred", develop = true }
openbb-intrinio = { path = "./providers/intrinio", develop = true }
openbb-polygon = { path = "./providers/polygon", develop = true }
openbb-tradingeconomics = { path = "./providers/tradingeconomics", develop = true }
openbb-oecd = { path = "./providers/oecd", develop = true }
openbb-crypto = { path = "./extensions/crypto", develop = true }
openbb-economy = { path = "./extensions/economy", develop = true }

View File

@@ -467,3 +467,18 @@ def test_economy_gdpforecast(params, headers):
result = requests.get(url, headers=headers, timeout=10)
assert isinstance(result, requests.Response)
assert result.status_code == 200
@pytest.mark.parametrize(
"params",
[({"start_date": "2023-01-01", "end_date": "2023-06-06", "country": "Portugal"})],
)
@pytest.mark.integration
def test_economy_econcal(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/economy/econcal?{query_str}"
result = requests.get(url, headers=headers, timeout=10)
assert isinstance(result, requests.Response)
assert result.status_code == 200

View File

@@ -195,3 +195,14 @@ def gdpforecast(
) -> OBBject[BaseModel]:
"""GDP Data."""
return OBBject(results=Query(**locals()).execute())
@router.command(model="EconCal")
def econcal(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject[BaseModel]:
"""GDP Data."""
return OBBject(results=Query(**locals()).execute())

View File

@@ -0,0 +1,88 @@
"""Economic Calendar Standard Model"""
from datetime import (
date as dateType,
datetime,
)
from typing import List, Literal, Optional, Union
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 QUERY_DESCRIPTIONS
class EconomicCalendarQueryParams(QueryParams):
"""Economic calendar Query."""
start_date: Optional[dateType] = Field(
default=None,
description=QUERY_DESCRIPTIONS.get("start_date", ""),
)
end_date: Optional[dateType] = Field(
default=None,
description=QUERY_DESCRIPTIONS.get("end_date", ""),
)
importance: Optional[Literal[1, 2, 3]] = Field(
default=None,
description="Importance of the event. 1-Low, 2-Medium, 3-High",
)
group: Optional[
Literal[
"interest rate",
"inflation",
"bonds",
"consumer",
"gdp",
"government",
"housing",
"labour",
"markets",
"money",
"prices",
"trade",
"business",
]
] = Field(default=None, description="Grouping of events")
# TODO: Probably want to figure out the list we can use.
country: Optional[Union[str, List[str]]] = Field(
default=None,
description="Country of the event",
)
class EconomicCalendarData(Data):
"""Economic calendar Data."""
date: Optional[datetime] = Field(
default=None, description="Date and time of event."
)
country: Optional[str] = Field(default=None, description="Country of event.")
category: Optional[str] = Field(default=None, description="Category of event.")
event: Optional[str] = Field(default=None, description="Event name.")
reference: Optional[str] = Field(
default=None,
description="Abbreviated period for which released data refers to.",
)
source: Optional[str] = Field(default=None, description="Source of the data.")
sourceurl: Optional[str] = Field(default=None, description="Source URL.")
actual: Optional[str] = Field(default=None, description="Latest released value.")
previous: Optional[str] = Field(
default=None,
description="Value for the previous period after the revision (if revision is applicable).",
)
consensus: Optional[str] = Field(
default=None,
description="Average forecast among a representative group of economists.",
)
forecast: Optional[str] = Field(
default=None, description="Trading Economics projections"
)
url: Optional[str] = Field(default=None, description="Trading Economics URL")
importance: Optional[Literal[0, 1, 2, 3]] = Field(
default=None, description="Importance of the event. 1-Low, 2-Medium, 3-High"
)
currency: Optional[str] = Field(default=None, description="Currency of the data.")
unit: Optional[str] = Field(default=None, description="Unit of the data.")

View File

@@ -0,0 +1,15 @@
# OpenBB Trading Economics Provider
This extension integrates the [Trading Economics](https://docs.tradingeconomics.com/) data provider into the OpenBB SDK.
## Installation
To install the extension:
```bash
pip install openbb-tradingeconomics
```
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,11 @@
"""Trading Economics provider module."""
from openbb_provider.abstract.provider import Provider
from openbb_tradingeconomics.models.economic_calendar import TEEarningsCalendarFetcher
tradingeconomics_provider = Provider(
name="tradingeconomics",
website="https://tradingeconomics.com/",
description="""Trading Economics""",
required_credentials=["api_key"],
fetcher_dict={"EconCal": TEEarningsCalendarFetcher},
)

View File

@@ -0,0 +1,106 @@
"""Trading Economics Earnings Calendar fetcher."""
from datetime import datetime
from typing import Any, Dict, List, Optional, Union
from dateutil import parser
from openbb_provider.abstract.fetcher import Fetcher
from openbb_provider.standard_models.economic_calendar import (
EconomicCalendarData,
EconomicCalendarQueryParams,
)
from openbb_provider.utils.helpers import make_request
from openbb_tradingeconomics.utils import countries, url_generator
from pydantic import field_validator
class TEEarningsCalendarQueryParams(EconomicCalendarQueryParams):
"""FMP Economic Calendar Query.
Source: https://docs.tradingeconomics.com/economic_calendar/
"""
@field_validator("country", mode="before")
@classmethod
def validate_country(
cls, country: Optional[Union[str, List[str]]]
) -> Optional[List[str]]:
if country is None:
return None
if isinstance(country, str):
country = [country]
for c in country:
if c not in countries.country_list:
raise ValueError(f"{c} is not a valid country")
return country
class TEEarningsCalendarData(EconomicCalendarData):
"""TE Earnings Calendar Data."""
__alias_dict__ = {
"date": "Date",
"country": "Country",
"category": "Category",
"event": "Event",
"reference": "Reference",
"source": "Source",
"sourceurl": "SourceURL",
"actual": "Actual",
"consensus": "Forecast",
"forecast": "TEForecast",
"url": "URL",
"importance": "Importance",
"currency": "Currency",
"unit": "Unit",
"ticker": "Ticker",
"symbol": "Symbol",
"previous": "Previous",
"revised": "Revised",
}
@field_validator("date", mode="before")
@classmethod
def validate_date(cls, input_date: str) -> datetime:
return parser.parse(input_date)
class TEEarningsCalendarFetcher(
Fetcher[
TEEarningsCalendarQueryParams,
List[TEEarningsCalendarData],
]
):
"""Transform the query, extract and transform the data from the TE endpoints."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> TEEarningsCalendarQueryParams:
"""Transform the query params."""
return TEEarningsCalendarQueryParams(**params)
@staticmethod
def extract_data(
query: TEEarningsCalendarQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> List[Dict]:
"""Return the raw data from the TE endpoint."""
api_key = credentials.get("tradingeconomics_api_key") if credentials else ""
url = url_generator.generate_url(query)
if not url:
raise RuntimeError(
"No url generated. Check combination of input parameters."
)
url = f"{url}{api_key}"
response = make_request(url, **kwargs)
if response.status_code != 200:
raise RuntimeError(f"Error in TE request -> {response.text}")
return response.json()
@staticmethod
def transform_data(data: List[Dict]) -> List[TEEarningsCalendarData]:
"""Return the transformed data."""
return [TEEarningsCalendarData.model_validate(d) for d in data]

View File

@@ -0,0 +1,228 @@
country_dict = {
"G20": [
"United States",
"Euro Area",
"China",
"Japan",
"Germany",
"United Kingdom",
"France",
"India",
"Italy",
"Brazil",
"Canada",
"South Korea",
"Russia",
"Spain",
],
"Australia": [
"Fiji",
"Kiribati",
"New Caledonia",
"New Zealand",
"Papua New Guinea",
"Samoa",
"Solomon Islands",
"Tonga",
"Vanuatu",
],
"America": [
"Antigua and Barbuda",
"Argentina",
"Aruba",
"Bahamas",
"Barbados",
"Belize",
"Bermuda",
"Bolivia",
"Brazil",
"Canada",
"Cayman Islands",
"Chile",
"Colombia",
"Costa Rica",
"Cuba",
"Dominica",
"Dominican Republic",
"Ecuador",
"El Salvador",
"Grenada",
"Guatemala",
"Guyana",
"Haiti",
"Honduras",
"Jamaica",
"Mexico",
"Nicaragua",
"Panama",
"Paraguay",
"Peru",
"Puerto Rico",
"Suriname",
"Trinidad and Tobago",
"United States",
"Uruguay",
"Venezuela",
],
"Europe": [
"Albania",
"Andorra",
"Austria",
"Belarus",
"Belgium",
"Bosnia and Herzegovina",
"Bulgaria",
"Croatia",
"Cyprus",
"Czech Republic",
"Denmark",
"Estonia",
"Euro area",
"Faroe Islands",
"Finland",
"France",
"Germany",
"Greece",
"Hungary",
"Iceland",
"Ireland",
"Isle of Man",
"Italy",
"Kosovo",
"Latvia",
"Liechtenstein",
"Lithuania",
"Luxembourg",
"Malta",
"Moldova",
"Monaco",
"Montenegro",
"Netherlands",
"North Macedonia",
"Norway",
"Poland",
"Portugal",
"Romania",
"Russia",
"Serbia",
"Slovakia",
"Slovenia",
"Spain",
"Sweden",
"Switzerland",
"Turkey",
"Ukraine",
"United Kingdom",
],
"Africa": [
"Algeria",
"Angola",
"Benin",
"Botswana",
"Burkina Faso",
"Burundi",
"Cameroon",
"Cape Verde",
"Central African Republic",
"Chad",
"Comoros",
"Congo",
"Djibouti",
"Egypt",
"Equatorial Guinea",
"Eritrea",
"Ethiopia",
"Gabon",
"Gambia",
"Ghana",
"Guinea",
"Guinea Bissau",
"Ivory Coast",
"Kenya",
"Lesotho",
"Liberia",
"Libya",
"Madagascar",
"Malawi",
"Mali",
"Mauritania",
"Mauritius",
"Morocco",
"Mozambique",
"Namibia",
"Niger",
"Nigeria",
"Republic of the Congo",
"Rwanda",
"Sao Tome and Principe",
"Senegal",
"Seychelles",
"Sierra Leone",
"Somalia",
"South Africa",
"South Sudan",
"Sudan",
"Swaziland",
"Tanzania",
"Togo",
"Tunisia",
"Uganda",
"Zambia",
"Zimbabwe",
],
"Asia": [
"Afghanistan",
"Armenia",
"Azerbaijan",
"Bahrain",
"Bangladesh",
"Bhutan",
"Brunei",
"Cambodia",
"China",
"East Timor",
"Georgia",
"Hong Kong",
"India",
"Indonesia",
"Iran",
"Iraq",
"Israel",
"Japan",
"Jordan",
"Kazakhstan",
"Kuwait",
"Kyrgyzstan",
"Laos",
"Lebanon",
"Macao",
"Malaysia",
"Maldives",
"Mongolia",
"Myanmar",
"Nepal",
"North Korea",
"Oman",
"Palestine",
"Pakistan",
"Philippines",
"Qatar",
"Saudi Arabia",
"Singapore",
"South Korea",
"Sri Lanka",
"Syria",
"Taiwan",
"Tajikistan",
"Thailand",
"Turkmenistan",
"United Arab Emirates",
"Uzbekistan",
"Vietnam",
"Yemen",
],
}
country_list = list(
set([item.lower() for sublist in country_dict.values() for item in sublist])
)

View File

@@ -0,0 +1,78 @@
"""Helper to generate urls for Trading Economics API."""
from datetime import date
from typing import Dict, List
from urllib.parse import quote, urlencode
def check_args(query_args: Dict, to_include: List[str]):
available_args = ["country", "start_date", "end_date", "importance", "group"]
# Check if all fields in to_include are present in query_args
# and elements in available_args that are not in to_include are not present in query_args
return all(field in query_args for field in to_include) and all(
field not in query_args for field in available_args if field not in to_include
)
def generate_url(in_query):
"""
Generate the url for trading economimcs. There is not a single api endpoint to hit so these are
generated based on the combinations. There are also some combinations that return no data so that will return ""
"""
# Converting the input query to a dict of params that are not None
query = {k: v for k, v in in_query.dict().items() if v is not None}
# Nothing -- just a snapshot
if not query:
return "https://api.tradingeconomics.com/calendar?c="
# Both start and end date are required
if "start_date" in query and "end_date" not in query:
query["end_date"] = date.today().strftime("%Y-%m-%d")
if "end_date" in query and "start_date" not in query:
query["start_date"] = query["end_date"]
# Handle the formatting for the api
if "country" in query:
country = quote(",".join(query["country"]).replace("_", " "))
if "group" in query:
group = quote(query["group"])
base_url = "https://api.tradingeconomics.com/calendar"
url = ""
# Construct URL based on query parameters
# Country Only
if check_args(query, ["country"]):
url = f"{base_url}/country/{country}?c="
# Country + Date
elif check_args(query, ["country", "start_date", "end_date"]):
url = (
f'{base_url}/country/{country}/{query["start_date"]}/{query["end_date"]}?c='
)
# Country + Importance
elif check_args(query, ["country", "importance"]):
url = f"{base_url}/country/{country}?{urlencode(query)}&c="
# Country + Group
elif check_args(query, ["country", "group"]):
url = f"{base_url}/country/{country}/group/{group}?c="
# Country + Group + Date
elif check_args(query, ["country", "group", "start_date", "end_date"]):
url = f'{base_url}/country/{country}/group/{group}/{query["start_date"]}/{query["end_date"]}?c='
# By date only
elif check_args(query, ["start_date", "end_date"]):
url = f'{base_url}/country/All/{query["start_date"]}/{query["end_date"]}?c='
# By importance only
elif check_args(query, ["importance"]):
url = f"{base_url}?{urlencode(query)}&c="
# By importance and date
elif check_args(query, ["importance", "start_date", "end_date"]):
url = f'{base_url}/country/All/{query["start_date"]}/{query["end_date"]}?{urlencode(query)}&c='
# Group Only
elif check_args(query, ["group"]):
url = f'{base_url}/group/{query["group"]}?c='
# Group + Date
elif check_args(query, ["group", "start_date", "end_date"]):
url = f'{base_url}/group/{query["group"]}/{query["start_date"]}/{query["end_date"]}?c='
return url if url else ""

1529
openbb_platform/providers/tradingeconomics/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-tradingeconomics"
version = "0.1.0a2"
description = "Trading Economics extension for OpenBB"
authors = ["OpenBB Team <hello@openbb.co>"]
readme = "README.md"
packages = [{ include = "openbb_tradingeconomics" }]
[tool.poetry.dependencies]
python = "^3.8"
openbb-core = "^0.1.0a2"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.plugins."openbb_provider_extension"]
tradingeconomics = "openbb_tradingeconomics:tradingeconomics_provider"

View File

@@ -0,0 +1,29 @@
from datetime import date
import pytest
from openbb_core.app.service.user_service import UserService
from openbb_tradingeconomics.models.economic_calendar import TEEarningsCalendarFetcher
test_credentials = UserService().default_user_settings.credentials.dict()
@pytest.fixture(scope="module")
def vcr_config():
return {
"filter_headers": [("User-Agent", None)],
"filter_query_parameters": [
("c", "mock_api_key"),
],
}
@pytest.mark.record_http
def test_te_earnings_calendar_fetcher(credentials=test_credentials):
params = {
"start_date": date(2023, 1, 1),
"end_date": date(2023, 6, 6),
}
fetcher = TEEarningsCalendarFetcher()
result = fetcher.test(params, credentials)
assert result is None

View File

@@ -15,6 +15,7 @@ openbb-fmp = "^0.1.0a2"
openbb-fred = "^0.1.0a2"
openbb-intrinio = "^0.1.0a2"
openbb-polygon = "^0.1.0a2"
openbb-tradingeconomics = "^0.1.0a2"
openbb-crypto = "^0.1.0a2"
openbb-economy = "^0.1.0a2"