Add trading economics economic calendar

This commit is contained in:
James Maslek
2023-10-12 12:10:45 -04:00
parent 2ba872e18b
commit c8a24374d9
14 changed files with 2263 additions and 19 deletions

View File

@@ -379,15 +379,25 @@ def test_economy_fred_index(params, headers):
assert isinstance(result, requests.Response)
assert result.status_code == 200
@pytest.mark.parametrize(
"params",
[({'units': 'usd', 'start_date': '2023-01-01', 'end_date': '2023-06-06'}),
({'country': 'united_states', 'provider': 'oecd', 'units': 'usd', 'start_date': '2023-01-01', 'end_date': '2023-06-06'})],
[
({"units": "usd", "start_date": "2023-01-01", "end_date": "2023-06-06"}),
(
{
"country": "united_states",
"provider": "oecd",
"units": "usd",
"start_date": "2023-01-01",
"end_date": "2023-06-06",
}
),
],
)
@pytest.mark.integration
def test_economy_gdpnom(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/gdpnom?{query_str}"
@@ -395,15 +405,25 @@ def test_economy_gdpnom(params, headers):
assert isinstance(result, requests.Response)
assert result.status_code == 200
@pytest.mark.parametrize(
"params",
[({'units': 'yoy', 'start_date': '2023-01-01', 'end_date': '2023-06-06'}),
({'country': 'united_states', 'provider': 'oecd', 'units': 'yoy', 'start_date': '2023-01-01', 'end_date': '2023-06-06'})],
[
({"units": "yoy", "start_date": "2023-01-01", "end_date": "2023-06-06"}),
(
{
"country": "united_states",
"provider": "oecd",
"units": "yoy",
"start_date": "2023-01-01",
"end_date": "2023-06-06",
}
),
],
)
@pytest.mark.integration
def test_economy_gdpreal(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/gdpreal?{query_str}"
@@ -411,18 +431,51 @@ def test_economy_gdpreal(params, headers):
assert isinstance(result, requests.Response)
assert result.status_code == 200
@pytest.mark.parametrize(
"params",
[({'period': 'annual', 'start_date': '2023-01-01', 'end_date': '2023-06-06', 'type': 'real'}),
({'country': 'united_states', 'provider': 'oecd', 'period': 'annual', 'start_date': '2023-01-01', 'end_date': '2023-06-06', 'type': 'real'})],
[
(
{
"period": "annual",
"start_date": "2023-01-01",
"end_date": "2023-06-06",
"type": "real",
}
),
(
{
"country": "united_states",
"provider": "oecd",
"period": "annual",
"start_date": "2023-01-01",
"end_date": "2023-06-06",
"type": "real",
}
),
],
)
@pytest.mark.integration
def test_economy_gdpforecast(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/gdpforecast?{query_str}"
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

@@ -366,9 +366,16 @@ def test_economy_fred_index(params):
@pytest.mark.parametrize(
"params",
[
({'units': 'usd', 'start_date': '2023-01-01', 'end_date': '2023-06-06'}),
({'country': 'united_states', 'provider': 'oecd', 'units': 'usd', 'start_date': '2023-01-01', 'end_date': '2023-06-06'}),
({"units": "usd", "start_date": "2023-01-01", "end_date": "2023-06-06"}),
(
{
"country": "united_states",
"provider": "oecd",
"units": "usd",
"start_date": "2023-01-01",
"end_date": "2023-06-06",
}
),
],
)
@pytest.mark.integration
@@ -384,9 +391,16 @@ def test_economy_gdpnom(params, obb):
@pytest.mark.parametrize(
"params",
[
({'units': 'yoy', 'start_date': '2023-01-01', 'end_date': '2023-06-06'}),
({'country': 'united_states', 'provider': 'oecd', 'units': 'yoy', 'start_date': '2023-01-01', 'end_date': '2023-06-06'}),
({"units": "yoy", "start_date": "2023-01-01", "end_date": "2023-06-06"}),
(
{
"country": "united_states",
"provider": "oecd",
"units": "yoy",
"start_date": "2023-01-01",
"end_date": "2023-06-06",
}
),
],
)
@pytest.mark.integration
@@ -402,9 +416,24 @@ def test_economy_gdpreal(params, obb):
@pytest.mark.parametrize(
"params",
[
({'period': 'annual', 'start_date': '2023-01-01', 'end_date': '2023-06-06', 'type': 'real'}),
({'country': 'united_states', 'provider': 'oecd', 'period': 'annual', 'start_date': '2023-01-01', 'end_date': '2023-06-06', 'type': 'real'}),
(
{
"period": "annual",
"start_date": "2023-01-01",
"end_date": "2023-06-06",
"type": "real",
}
),
(
{
"country": "united_states",
"provider": "oecd",
"period": "annual",
"start_date": "2023-01-01",
"end_date": "2023-06-06",
"type": "real",
}
),
],
)
@pytest.mark.integration

View File

@@ -192,3 +192,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

@@ -1,9 +1,10 @@
import datetime
import pytest
from openbb_core.app.service.user_service import UserService
from openbb_oecd.models.gdpforecast import OECDGDPForecastFetcher
from openbb_oecd.models.gdpnom import OECDGDPNomFetcher
from openbb_oecd.models.gdpreal import OECDGDPRealFetcher
from openbb_oecd.models.gdpforecast import OECDGDPForecastFetcher
test_credentials = UserService().default_user_settings.credentials.dict()

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,105 @@
"""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."
)
response = make_request(url.replace("api_key", api_key), **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,127 @@
"""Helper to generate urls for Trading Economics API."""
from datetime import date
from urllib.parse import quote
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 cominations. 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=api_key"
# 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"])
# We can have the possible combinations:
# Country only
if (
"country" in query
and "start_date" not in query
and "importance" not in query
and "group" not in query
and "end_date" not in query
):
return f"https://api.tradingeconomics.com/calendar/country/{country}?c=api_key"
# Country + Date
if (
"country" in query
and "start_date" in query
and "end_date" in query
and "importance" not in query
and "group" not in query
):
return f'https://api.tradingeconomics.com/calendar/country/{country}/{query["start_date"]}/{query["end_date"]}?c=api_key'
# Country + Importance
if (
"country" in query
and "importance" in query
and "start_date" not in query
and "end_date" not in query
and "group" not in query
):
return f'https://api.tradingeconomics.com/calendar/country/{country}?c=api_key&importance{query["importance"]}'
# Country + Group
if (
"country" in query
and "group" in query
and "start_date" not in query
and "end_date" not in query
and "importance" not in query
):
return f"https://api.tradingeconomics.com/calendar/country/{country}/group/{group}?c=api_key"
# Country + group + date
if (
"country" in query
and "group" in query
and "start_date" in query
and "end_date" in query
and "importance" not in query
):
return f'https://api.tradingeconomics.com/calendar/country/{country}&group={group}/{query["start_date"]}/{query["end_date"]}?c=api_key'
# By date only
if (
"start_date" in query
and "end_date" in query
and "country" not in query
and "importance" not in query
and "group" not in query
):
return f'https://api.tradingeconomics.com/calendar/country/All/{query["start_date"]}/{query["end_date"]}?c=api_key'
# By importance only
if (
"importance" in query
and "country" not in query
and "group" not in query
and "start_date" not in query
and "end_date" not in query
):
return f'https://api.tradingeconomics.com/calendar?c=api_key&importance={query["importance"]}'
# By importance and date
if (
"importance" in query
and "start_date" in query
and "end_date" in query
and "country" not in query
and "group" not in query
):
return f'https://api.tradingeconomics.com/calendar/country/All/{query["start_date"]}/{query["end_date"]}?c=api_key&importance={query["importance"]}'
# Group Only
if (
"group" in query
and "country" not in query
and "start_date" not in query
and "end_date" not in query
and "importance" not in query
):
return f'https://api.tradingeconomics.com/calendar/group/{query["group"]}?c=api_key'
# Group + Date
if (
"group" in query
and "start_date" in query
and "end_date" in query
and "country" not in query
and "importance" not in query
):
return f'https://api.tradingeconomics.com/calendar/group/{query["group"]}/{query["start_date"]}/{query["end_date"]}?c=api_key'
return ""

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 datetime
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": [
("token", "MOCK_TOKEN"),
],
}
@pytest.mark.record_http
def test_te_earnings_calendar_fetcher(credentials=test_credentials):
params = {
"start_date": datetime.date(2023, 1, 1),
"end_date": datetime.date(2023, 6, 6),
}
fetcher = TEEarningsCalendarFetcher()
result = fetcher.test(params, credentials)
assert result is None