-
Notifications
You must be signed in to change notification settings - Fork 2.6k
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
Changes from all commits
7fd5698
b5442ed
cc8625c
05f2c96
57ce6d1
4e98eb4
0af4b79
c857873
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
---|---|---|
@@ -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, | ||
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 |
---|---|---|
@@ -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()}") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 We don't want to throw out the entire request because of one bad apple. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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] |
There was a problem hiding this comment.
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
?There was a problem hiding this comment.
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}'
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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:
The expectation is that the user can then use (eg)
index.historical
to get historical data for any given bond?