Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] New endpoint economy.markets.historical #6107

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""Market Historical Price Standard Model."""

from datetime import (
date as dateType,
datetime,
)
from typing import Optional

from dateutil import parser
from pydantic import Field, PositiveFloat, field_validator

from openbb_core.provider.abstract.data import Data
from openbb_core.provider.abstract.query_params import QueryParams
from openbb_core.provider.utils.descriptions import (
DATA_DESCRIPTIONS,
QUERY_DESCRIPTIONS,
)


class MarketHistoricalQueryParams(QueryParams):
"""Market Historical Price Query."""

symbol: str = Field(description=QUERY_DESCRIPTIONS.get("symbol", ""))
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", ""),
)

@field_validator("symbol", mode="before", check_fields=False)
@classmethod
def upper_symbol(cls, v: str) -> str:
"""Convert symbol to uppercase."""
return v.upper()


class MarketHistoricalData(Data):
"""Market Historical Price Data."""

date: datetime = Field(description=DATA_DESCRIPTIONS.get("date", ""))
open: PositiveFloat = Field(description=DATA_DESCRIPTIONS.get("open", ""))
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: Optional[float] = Field(
description=DATA_DESCRIPTIONS.get("volume", ""), default=None
)

@field_validator("date", mode="before", check_fields=False)
@classmethod
def date_validate(cls, v): # pylint: disable=E0213
"""Return formatted datetime."""
return parser.isoparse(str(v))
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
from openbb_core.app.router import Router

from openbb_economy.gdp.gdp_router import router as gdp_router
from openbb_economy.markets.markets_router import router as markets_router

router = Router(prefix="")
router.include_router(gdp_router)
router.include_router(markets_router)

# pylint: disable=unused-argument

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Economy Markets Router."""

from openbb_core.app.model.command_context import CommandContext
from openbb_core.app.model.obbject import OBBject
from openbb_core.app.provider_interface import (
ExtraParams,
ProviderChoices,
StandardParams,
)
from openbb_core.app.query import Query
from openbb_core.app.router import Router

router = Router(prefix="/markets")

# pylint: disable=unused-argument


# @router.command(model="MarketCategoryQuotes")
# async def quote_category(
# cc: CommandContext,
# provider_choices: ProviderChoices,
# standard_params: StandardParams,
# extra_params: ExtraParams,
# ) -> OBBject:
# """Get quotes for a specific category of markets."""
# return await OBBject.from_query(Query(**locals()))


# @router.command(model="MarketSymbolQuotes")
# async def quote_symbol(
# cc: CommandContext,
# provider_choices: ProviderChoices,
# standard_params: StandardParams,
# extra_params: ExtraParams,
# ) -> OBBject:
# """Get quotes for a specific symbol."""
# return await OBBject.from_query(Query(**locals()))


@router.command(model="MarketHistorical")
async def historical(
cc: CommandContext,
provider_choices: ProviderChoices,
standard_params: StandardParams,
extra_params: ExtraParams,
) -> OBBject:
"""
Get historical price data for a symbol within a market category.

The market categories available are exchange rates, stock market indexes,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this functionality be split up into the existing router functions, like index.price.historical, crypto.price.historical, currency.price.historical?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with darren. This api is wacky, but we can get snapshots without a ticker (f'https://api.tradingeconomics.com/markets/index?c={api_key}' or for the specific symbol:

f'https://api.tradingeconomics.com/markets/historical/aapl:us,gac:com?c={api_key}'

So this should go into snapshot and historical for crypto/index/stock. This also probably warrants a new bond/government/snapshot for f'https://api.tradingeconomics.com/markets/bond?c={api_key}'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a very fair point. I don't know tbh, although it covers those assets, it's not limited to them - and TE doesn't seem to offer any options for filtering.
Thoughts @jmaslek @minhhoang1023 ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The page was outdated when I 1st commented! I agree with that approach @jmaslek. Will restructure accordingly.
So, iiuc:

  • TE Historical will serve crypto/index/equity historical.
  • TE Snapshot will serve economy/index snapshot. --> should we create a snapshot endpoint under economy for the bonds?

The expectation is that the user can then use (eg) index.historical to get historical data for any given bond?

share prices, commodity prices, government bonds and crypto currencies.
"""
return await OBBject.from_query(Query(**locals()))
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@

from openbb_core.provider.abstract.provider import Provider
from openbb_tradingeconomics.models.economic_calendar import TEEconomicCalendarFetcher
from openbb_tradingeconomics.models.market_historical import TEMarketHistoricalFetcher

tradingeconomics_provider = Provider(
name="tradingeconomics",
website="https://tradingeconomics.com/",
description="""Trading Economics""",
credentials=["api_key"],
fetcher_dict={"EconomicCalendar": TEEconomicCalendarFetcher},
fetcher_dict={
"EconomicCalendar": TEEconomicCalendarFetcher,
"MarketHistorical": TEMarketHistoricalFetcher,
},
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Trading Economics Economic Calendar Model."""

from datetime import datetime
from typing import Any, Dict, List, Optional, Union

from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.market_historical import (
MarketHistoricalData,
MarketHistoricalQueryParams,
)
from openbb_core.provider.utils.helpers import (
ClientResponse,
amake_requests,
get_querystring,
)
from pandas import to_datetime
from pydantic import field_validator


class TEMarketHistoricalQueryParams(MarketHistoricalQueryParams):
"""Trading Economics Market Historical Query.

Source: https://docs.tradingeconomics.com/markets/historical/
"""

__alias_dict__ = {"start_date": "d1", "end_date": "d2"}
__json_schema_extra__ = {"symbol": ["multiple_items_allowed"]}


class TEMarketHistorical(MarketHistoricalData):
"""Trading Economics Market Historical Data."""

__alias_dict__ = {
"symbol": "Symbol",
"date": "Date",
"open": "Open",
"high": "High",
"low": "Low",
"close": "Close",
}

@field_validator("date", mode="before")
@classmethod
def validate_date(cls, v: str) -> datetime:
"""Return formatted datetime."""
return to_datetime(v, utc=True, dayfirst=True)


class TEMarketHistoricalFetcher(
Fetcher[
TEMarketHistoricalQueryParams,
List[TEMarketHistorical],
]
):
"""Transform the query, extract and transform the data from the Trading Economics endpoints."""

@staticmethod
def transform_query(params: Dict[str, Any]) -> TEMarketHistoricalQueryParams:
"""Transform the query params."""
return TEMarketHistoricalQueryParams(**params)

@staticmethod
async def aextract_data(
query: TEMarketHistoricalQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> Union[dict, List[dict]]:
"""Return the raw data from the TE endpoint."""
api_key = credentials.get("tradingeconomics_api_key") if credentials else ""

symbols = query.symbol.split(",")

base_url = "https://api.tradingeconomics.com/markets/historical"
query_str = get_querystring(query.model_dump(), ["symbol"])
urls = []

for symbol in symbols:
urls.append(f"{base_url}/{symbol}?{query_str}&c={api_key}")

async def callback(response: ClientResponse, _: Any) -> Union[dict, List[dict]]:
"""Return the response."""
if response.status != 200:
raise RuntimeError(f"Error in TE request -> {await response.text()}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because we are allowing multiple symbols, the failure of one symbol should communicate via warnings, raising an EmptyDataError if all fail.

We don't want to throw out the entire request because of one bad apple.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed.

return await response.json()

return await amake_requests(urls, response_callback=callback, **kwargs)

# pylint: disable=unused-argument
@staticmethod
def transform_data(
query: TEMarketHistoricalQueryParams, data: List[Dict], **kwargs: Any
) -> List[TEMarketHistorical]:
"""Return the transformed data."""
return [TEMarketHistorical.model_validate(d) for d in data]