summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanglewood <85772166+deeleeramone@users.noreply.github.com>2024-03-05 05:14:08 -0800
committerGitHub <noreply@github.com>2024-03-05 13:14:08 +0000
commite7896a839eeb5b8eba809aae5197a05867aae415 (patch)
treee233792e84dd6182591ece0016e99fe7e85e35f0
parentc06410746fd5f7928bf4e2f4928952c86220018a (diff)
[Feature] Add Currency Snapshots With Data From FMP (#6162)
* add obb.currency.snapshots() endpoint and create new standard model * add standard model * static assets * add couple of comments * codespell --------- Co-authored-by: Igor Radovanovic <74266147+IgorWounds@users.noreply.github.com>
-rw-r--r--openbb_platform/core/openbb_core/provider/standard_models/currency_snapshots.py82
-rw-r--r--openbb_platform/extensions/currency/integration/test_currency_api.py24
-rw-r--r--openbb_platform/extensions/currency/integration/test_currency_python.py21
-rw-r--r--openbb_platform/extensions/currency/openbb_currency/currency_router.py19
-rw-r--r--openbb_platform/openbb/assets/module_map.json1
-rw-r--r--openbb_platform/openbb/package/currency.py122
-rw-r--r--openbb_platform/providers/fmp/openbb_fmp/__init__.py2
-rw-r--r--openbb_platform/providers/fmp/openbb_fmp/models/currency_snapshots.py158
-rw-r--r--openbb_platform/providers/fmp/tests/record/http/test_fmp_fetchers/test_fmp_currency_snapshots_fetcher.yaml2153
-rw-r--r--openbb_platform/providers/fmp/tests/test_fmp_fetchers.py14
10 files changed, 2595 insertions, 1 deletions
diff --git a/openbb_platform/core/openbb_core/provider/standard_models/currency_snapshots.py b/openbb_platform/core/openbb_core/provider/standard_models/currency_snapshots.py
new file mode 100644
index 00000000000..e0201ad6bbd
--- /dev/null
+++ b/openbb_platform/core/openbb_core/provider/standard_models/currency_snapshots.py
@@ -0,0 +1,82 @@
+"""Currency Snapshots Standard Model."""
+
+from typing import List, Literal, Optional, Union
+
+from pydantic import Field, 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
+
+
+class CurrencySnapshotsQueryParams(QueryParams):
+ """Currency Snapshots Query Params."""
+
+ base: str = Field(description="The base currency symbol.", default="usd")
+ quote_type: Literal["direct", "indirect"] = Field(
+ description="Whether the quote is direct or indirect."
+ + " Selecting 'direct' will return the exchange rate"
+ + " as the amount of domestic currency required to buy one unit"
+ + " of the foreign currency."
+ + " Selecting 'indirect' (default) will return the exchange rate"
+ + " as the amount of foreign currency required to buy one unit"
+ + " of the domestic currency.",
+ default="indirect",
+ )
+ counter_currencies: Optional[Union[str, List[str]]] = Field(
+ description="An optional list of counter currency symbols to filter for."
+ + " None returns all.",
+ default=None,
+ )
+
+ @field_validator("base", mode="before", check_fields=False)
+ @classmethod
+ def to_upper(cls, v):
+ """Convert the base currency to uppercase."""
+ return v.upper()
+
+ @field_validator("counter_currencies", mode="before", check_fields=False)
+ @classmethod
+ def convert_string(cls, v):
+ """Convert the counter currencies to an upper case string list."""
+ if v is not None:
+ return ",".join(v).upper() if isinstance(v, list) else v.upper()
+ return None
+
+
+class CurrencySnapshotsData(Data):
+ """Currency Snapshots Data."""
+
+ base_currency: str = Field(description="The base, or domestic, currency.")
+ counter_currency: str = Field(description="The counter, or foreign, currency.")
+ last_rate: float = Field(
+ description="The exchange rate, relative to the base currency."
+ + " Rates are expressed as the amount of foreign currency"
+ + " received from selling one unit of the base currency,"
+ + " or the quantity of foreign currency required to purchase"
+ + " one unit of the domestic currency."
+ + " To inverse the perspective, set the 'quote_type' parameter as 'direct'.",
+ )
+ open: Optional[float] = Field(
+ description=DATA_DESCRIPTIONS.get("open", ""),
+ default=None,
+ )
+ high: Optional[float] = Field(
+ description=DATA_DESCRIPTIONS.get("high", ""),
+ default=None,
+ )
+ low: Optional[float] = Field(
+ description=DATA_DESCRIPTIONS.get("low", ""),
+ default=None,
+ )
+ close: Optional[float] = Field(
+ description=DATA_DESCRIPTIONS.get("close", ""),
+ default=None,
+ )
+ volume: Optional[int] = Field(
+ description=DATA_DESCRIPTIONS.get("volume", ""), default=None
+ )
+ prev_close: Optional[float] = Field(
+ description=DATA_DESCRIPTIONS.get("prev_close", ""),
+ default=None,
+ )
diff --git a/openbb_platform/extensions/currency/integration/test_currency_api.py b/openbb_platform/extensions/currency/integration/test_currency_api.py
index d5dea2303af..9cdfee0063e 100644
--- a/openbb_platform/extensions/currency/integration/test_currency_api.py
+++ b/openbb_platform/extensions/currency/integration/test_currency_api.py
@@ -165,3 +165,27 @@ def test_currency_reference_rates(params, headers):
result = requests.get(url, headers=headers, timeout=10)
assert isinstance(result, requests.Response)
assert result.status_code == 200
+
+
+@parametrize(
+ "params",
+ [
+ (
+ {
+ "provider": "fmp",
+ "base": "USD,XAU",
+ "counter_currencies": "EUR,JPY,GBP",
+ "quote_type": "indirect",
+ }
+ ),
+ ],
+)
+@pytest.mark.integration
+def test_currency_snapshots(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/currency/snapshots?{query_str}"
+ result = requests.get(url, headers=headers, timeout=10)
+ assert isinstance(result, requests.Response)
+ assert result.status_code == 200
diff --git a/openbb_platform/extensions/currency/integration/test_currency_python.py b/openbb_platform/extensions/currency/integration/test_currency_python.py
index 329f017eb2c..a2be2302101 100644
--- a/openbb_platform/extensions/currency/integration/test_currency_python.py
+++ b/openbb_platform/extensions/currency/integration/test_currency_python.py
@@ -154,3 +154,24 @@ def test_currency_reference_rates(params, obb):
assert result
assert isinstance(result, OBBject)
assert len(result.model_dump()["results"].items()) > 0
+
+
+@parametrize(
+ "params",
+ [
+ (
+ {
+ "provider": "fmp",
+ "base": "USD,XAU",
+ "counter_currencies": "EUR,JPY,GBP",
+ "quote_type": "indirect",
+ }
+ ),
+ ],
+)
+@pytest.mark.integration
+def test_currency_snapshots(params, obb):
+ result = obb.currency.snapshots(**params)
+ assert result
+ assert isinstance(result, OBBject)
+ assert len(result.results) > 0
diff --git a/openbb_platform/extensions/currency/openbb_currency/currency_router.py b/openbb_platform/extensions/currency/openbb_currency/currency_router.py
index e7ef5d46f7c..40a82e870c5 100644
--- a/openbb_platform/extensions/currency/openbb_currency/currency_router.py
+++ b/openbb_platform/extensions/currency/openbb_currency/currency_router.py
@@ -66,3 +66,22 @@ async def reference_rates(
impacting global trade, loans, and investments.
"""
return await OBBject.from_query(Query(**locals()))
+
+
+@router.command(
+ model="CurrencySnapshots",
+ exclude_auto_examples=True,
+ examples=[
+ "obb.currency.snapshots(",
+ 'provider="fmp", base="USD,XAU", counter_currencies="EUR,JPY,GBP", quote_type="indirect"',
+ ")",
+ ],
+)
+async def snapshots(
+ cc: CommandContext,
+ provider_choices: ProviderChoices,
+ standard_params: StandardParams,
+ extra_params: ExtraParams,
+) -> OBBject:
+ """Snapshots of currency exchange rates from an indirect or direct perspective of a base currency."""
+ return await OBBject.from_query(Query(**locals()))
diff --git a/openbb_platform/openbb/assets/module_map.json b/openbb_platform/openbb/assets/module_map.json
index 6b9c08e3199..d3ee8837e71 100644
--- a/openbb_platform/openbb/assets/module_map.json
+++ b/openbb_platform/openbb/assets/module_map.json
@@ -8,6 +8,7 @@
"currency_price": "/currency/price",
"currency_price_historical": "/currency/price/historical",
"currency_search": "/currency/search",
+ "currency_snapshots": "/currency/snapshots",
"derivatives": "/derivatives",
"derivatives_futures": "/derivatives/futures",
"derivatives_futures_curve": "/derivatives/futures/curve",
diff --git a/openbb_platform/openbb/package/currency.py b/openbb_platform/openbb/package/currency.py
index c93faa2ebbb..c30b0e2a54e 100644
--- a/openbb_platform/openbb/package/currency.py
+++ b/openbb_platform/openbb/package/currency.py
@@ -1,17 +1,20 @@
### THIS FILE IS AUTO-GENERATED. DO NOT EDIT. ###
-from typing import Literal, Optional
+from typing import List, Literal, Optional, Union
+from openbb_core.app.model.custom_parameter import OpenBBCustomParameter
from openbb_core.app.model.obbject import OBBject
from openbb_core.app.static.container import Container
from openbb_core.app.static.utils.decorators import exception_handler, validate
from openbb_core.app.static.utils.filters import filter_inputs
+from typing_extensions import Annotated
class ROUTER_currency(Container):
"""/currency
/price
search
+ snapshots
"""
def __repr__(self) -> str:
@@ -136,3 +139,120 @@ class ROUTER_currency(Container):
extra_params=kwargs,
)
)
+
+ @exception_handler
+ @validate
+ def snapshots(
+ self,
+ base: Annotated[
+ Union[str, List[str]],
+ OpenBBCustomParameter(
+ description="The base currency symbol. Multiple items allowed for provider(s): fmp."
+ ),
+ ] = "usd",
+ quote_type: Annotated[
+ Literal["direct", "indirect"],
+ OpenBBCustomParameter(
+ description="Whether the quote is direct or indirect. Selecting 'direct' will return the exchange rate as the amount of domestic currency required to buy one unit of the foreign currency. Selecting 'indirect' (default) will return the exchange rate as the amount of foreign currency required to buy one unit of the domestic currency."
+ ),
+ ] = "indirect",
+ counter_currencies: Annotated[
+ Union[str, List[str], None],
+ OpenBBCustomParameter(
+ description="An optional list of counter currency symbols to filter for. None returns all."
+ ),
+ ] = None,
+ provider: Optional[Literal["fmp"]] = None,
+ **kwargs
+ ) -> OBBject:
+ """Snapshots of currency exchange rates from an indirect or direct perspective of a base currency.
+
+ Parameters
+ ----------
+ base : Union[str, List[str]]
+ The base currency symbol. Multiple items allowed for provider(s): fmp.
+ quote_type : Literal['direct', 'indirect']
+ Whether the quote is direct or indirect. Selecting 'direct' will return the exchange rate as the amount of domestic currency required to buy one unit of the foreign currency. Selecting 'indirect' (default) will return the exchange rate as the amount of foreign currency required to buy one unit of the domestic currency.
+ counter_currencies : Union[str, List[str], None]
+ An optional list of counter currency symbols to filter for. None returns all.
+ provider : Optional[Literal['fmp']]
+ The provider to use for the query, by default None.
+ If None, the provider specified in defaults is selected or 'fmp' if there is
+ no default.
+
+ Returns
+ -------
+ OBBject
+ results : List[CurrencySnapshots]
+ Serializable results.
+ provider : Optional[Literal['fmp']]
+ Provider name.
+ warnings : Optional[List[Warning_]]
+ List of warnings.
+ chart : Optional[Chart]
+ Chart object.
+ extra : Dict[str, Any]
+ Extra info.
+
+ CurrencySnapshots
+ -----------------
+ base_currency : str
+ The base, or domestic, currency.
+ counter_currency : str
+ The counter, or foreign, currency.
+ last_rate : float
+ The exchange rate, relative to the base currency. Rates are expressed as the amount of foreign currency received from selling one unit of the base currency, or the quantity of foreign currency required to purchase one unit of the domestic currency. To inverse the perspective, set the 'quote_type' parameter as 'direct'.
+ open : Optional[float]
+ The open price.
+ high : Optional[float]
+ The high price.
+ low : Optional[float]
+ The low price.
+ close : Optional[float]
+ The close price.
+ volume : Optional[int]
+ The trading volume.
+ prev_close : Optional[float]
+ The previous close price.
+ change : Optional[float]
+ The change in the price from the previous close. (provider: fmp)
+ change_percent : Optional[float]
+ The change in the price from the previous close, as a normalized percent. (provider: fmp)
+ ma50 : Optional[float]
+ The 50-day moving average. (provider: fmp)
+ ma200 : Optional[float]
+ The 200-day moving average. (provider: fmp)
+ year_high : Optional[float]
+ The 52-week high. (provider: fmp)
+ year_low : Optional[float]
+ The 52-week low. (provider: fmp)
+ last_rate_timestamp : Optional[datetime]
+ The timestamp of the last rate. (provider: fmp)
+
+ Example
+ -------
+ >>> from openbb import obb
+ >>> obb.currency.snapshots(
+ >>> provider="fmp", base="USD,XAU", counter_currencies="EUR,JPY,GBP", quote_type="indirect"
+ >>> )
+ """ # noqa: E501
+
+ return self._run(
+ "/currency/snapshots",
+ **filter_inputs(
+ provider_choices={
+ "provider": self._get_provider(
+ provider,
+ "/currency/snapshots",
+ ("fmp",),
+ )
+ },
+ standard_params={
+ "base": base,
+ "quote_type": quote_type,
+ "counter_currencies": counter_currencies,
+ },
+ extra_params=kwargs,
+ extra_info={"base": {"multiple_items_allowed": ["fmp"]}},
+ )
+ )
diff --git a/openbb_platform/providers/fmp/openbb_fmp/__init__.py b/openbb_platform/providers/fmp/openbb_fmp/__init__.py
index c3e2843540f..9c83df5a19e 100644
--- a/openbb_platform/providers/fmp/openbb_fmp/__init__.py
+++ b/openbb_platform/providers/fmp/openbb_fmp/__init__.py
@@ -17,6 +17,7 @@ from openbb_fmp.models.crypto_historical import FMPCryptoHistoricalFetcher
from openbb_fmp.models.crypto_search import FMPCryptoSearchFetcher
from openbb_fmp.models.currency_historical import FMPCurrencyHistoricalFetcher
from openbb_fmp.models.currency_pairs import FMPCurrencyPairsFetcher
+from openbb_fmp.models.currency_snapshots import FMPCurrencySnapshotsFetcher
from openbb_fmp.models.discovery_filings import FMPDiscoveryFilingsFetcher
from openbb_fmp.models.earnings_call_transcript import FMPEarningsCallTranscriptFetcher
from openbb_fmp.models.economic_calendar import FMPEconomicCalendarFetcher
@@ -86,6 +87,7 @@ fmp_provider = Provider(
"CryptoSearch": FMPCryptoSearchFetcher,
"CurrencyHistorical": FMPCurrencyHistoricalFetcher,
"CurrencyPairs": FMPCurrencyPairsFetcher,
+ "CurrencySnapshots": FMPCurrencySnapshotsFetcher,
"DiscoveryFilings": FMPDiscoveryFilingsFetcher,
"EarningsCallTranscript": FMPEarningsCallTranscriptFetcher,
"EconomicCalendar": FMPEconomicCalendarFetcher,
diff --git a/openbb_platform/providers/fmp/openbb_fmp/models/currency_snapshots.py b/openbb_platform/providers/fmp/openbb_fmp/models/currency_snapshots.py
new file mode 100644
index 00000000000..97ea456eb4b
--- /dev/null
+++ b/openbb_platform/providers/fmp/openbb_fmp/models/currency_snapshots.py
@@ -0,0 +1,158 @@
+"""FMP Currency Snapshots Model."""
+
+# pylint: disable=unused-argument
+
+from datetime import datetime
+from typing import Any, Dict, List, Optional
+
+from openbb_core.provider.abstract.fetcher import Fetcher
+from openbb_core.provider.standard_models.currency_snapshots import (
+ CurrencySnapshotsData,
+ CurrencySnapshotsQueryParams,
+)
+from openbb_core.provider.utils.errors import EmptyDataError
+from openbb_core.provider.utils.helpers import amake_request
+from pandas import DataFrame, concat
+from pydantic import Field, field_validator
+
+
+class FMPCurrencySnapshotsQueryParams(CurrencySnapshotsQueryParams):
+ """FMP Currency Snapshots Query.
+
+ Source: https://site.financialmodelingprep.com/developer/docs#exchange-prices-quote
+ """
+
+ __json_schema_extra__ = {"base": ["multiple_items_allowed"]}
+
+
+class FMPCurrencySnapshotsData(CurrencySnapshotsData):
+ """FMP Currency Snapshots Data."""
+
+ __alias_dict__ = {
+ "last_rate": "price",
+ "high": "dayHigh",
+ "low": "dayLow",
+ "ma50": "priceAvg50",
+ "ma200": "priceAvg200",
+ "year_high": "yearHigh",
+ "year_low": "yearLow",
+ "prev_close": "previousClose",
+ "change_percent": "changesPercentage",
+ "last_rate_timestamp": "timestamp",
+ }
+
+ change: Optional[float] = Field(
+ description="The change in the price from the previous close.", default=None
+ )
+ change_percent: Optional[float] = Field(
+ description="The change in the price from the previous close, as a normalized percent.",
+ default=None,
+ json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
+ )
+ ma50: Optional[float] = Field(
+ description="The 50-day moving average.", default=None
+ )
+ ma200: Optional[float] = Field(
+ description="The 200-day moving average.", default=None
+ )
+ year_high: Optional[float] = Field(description="The 52-week high.", default=None)
+ year_low: Optional[float] = Field(description="The 52-week low.", default=None)
+ last_rate_timestamp: Optional[datetime] = Field(
+ description="The timestamp of the last rate.", default=None
+ )
+
+ @field_validator("change_percent", mode="before", check_fields=False)
+ @classmethod
+ def normalize_percent(cls, v):
+ """Normalize the percent."""
+ return v / 100 if v is not None else None
+
+
+class FMPCurrencySnapshotsFetcher(
+ Fetcher[FMPCurrencySnapshotsQueryParams, List[FMPCurrencySnapshotsData]]
+):
+ """FMP Currency Snapshots Fetcher."""
+
+ @staticmethod
+ def transform_query(params: Dict[str, Any]) -> FMPCurrencySnapshotsQueryParams:
+ """Transform the query parameters."""
+ return FMPCurrencySnapshotsQueryParams(**params)
+
+ @staticmethod
+ async def aextract_data(
+ query: FMPCurrencySnapshotsQueryParams,
+ credentials: Optional[Dict[str, str]],
+ **kwargs: Any,
+ ) -> List[Dict]:
+ """Extract the data from the FMP endpoint."""
+
+ api_key = credentials.get("fmp_api_key") if credentials else ""
+
+ url = f"https://financialmodelingprep.com/api/v3/quotes/forex?apikey={api_key}"
+
+ return await amake_request(url, **kwargs) # type: ignore
+
+ @staticmethod
+ def transform_data(
+ query: FMPCurrencySnapshotsQueryParams,
+ data: List[Dict],
+ **kwargs: Any,
+ ) -> List[FMPCurrencySnapshotsData]:
+ """Filter by the query parameters and validate the model."""
+
+ if not data:
+ raise EmptyDataError("No data was returned from the FMP endpoint.")
+
+ # Drop all the zombie columns FMP returns.
+ df = (
+ DataFrame(data)
+ .dropna(how="all", axis=1)
+ .drop(columns=["exchange", "avgVolume"])
+ )
+
+ new_df = DataFrame()
+
+ # Filter for the base currencies requested and the quote_type.
+ for symbol in query.base.split(","):
+ temp = (
+ df.query("`symbol`.str.startswith(@symbol)")
+ if query.quote_type == "indirect"
+ else df.query("`symbol`.str.endswith(@symbol)")
+ ).rename(columns={"symbol": "base_currency", "name": "counter_currency"})
+ temp["base_currency"] = symbol
+ temp["counter_currency"] = (
+ [d.split("/")[1] for d in temp["counter_currency"]]
+ if query.quote_type == "indirect"
+ else [d.split("/")[0] for d in temp["counter_currency"]]
+ )
+ # Filter for the counter currencies, if requested.
+ if query.counter_currencies is not None:
+ counter_currencies = ( # noqa: F841 # pylint: disable=unused-variable
+ query.counter_currencies
+ if isinstance(query.counter_currencies, list)
+ else query.counter_currencies.split(",")
+ )
+ temp = (
+ temp.query("`counter_currency`.isin(@counter_currencies)")
+ .set_index("counter_currency")
+ # Sets the counter currencies in the order they were requested.
+ .filter(items=counter_currencies, axis=0)
+ .reset_index()
+ )
+ # If there are no records, don't concatenate.
+ if len(temp) > 0:
+ # Convert the Unix timestamp to a datetime.
+ temp.timestamp = temp.timestamp.apply(
+ lambda x: datetime.fromtimestamp(x)
+ )
+ new_df = concat([new_df, temp])
+ if len(new_df) == 0:
+ raise EmptyDataError(
+ "No data was found using the applied filters. Check the parameters."
+ )
+ # Fill and replace any NaN values with NoneType.
+ new_df = new_df.fillna("N/A").replace("N/A", None)
+ return [
+ FMPCurrencySnapshotsData.model_validate(d)
+ for d in new_df.reset_index(drop=True).to_dict(orient="records")
+ ]
diff --git a/openbb_platform/providers/fmp/tests/record/http/test_fmp_fetchers/test_fmp_currency_snapshots_fetcher.yaml b/openbb_platform/providers/fmp/tests/record/http/test_fmp_fetchers/test_fmp_currency_snapshots_fetcher.yaml
new file mode 100644
index 00000000000..ec419dc6dab
--- /dev/null
+++ b/openbb_platform/providers/fmp/tests/record/http/test_fmp_fetchers/test_fmp_currency_snapshots_fetcher.yaml
@@ -0,0 +1,2153 @@
+interactions:
+- request:
+ body: null
+ headers:
+ Accept:
+ - application/json
+ Accept-Encoding:
+ - gzip, deflate
+ Connection:
+ - keep-alive
+ method: GET
+ uri: https://financialmodelingprep.com/api/v3/quotes/forex?apikey=MOCK_API_KEY
+ response:
+ body:
+ string: !!binary |
+ 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
+ R/