summaryrefslogtreecommitdiffstats
path: root/website/content/platform/user_guides/add_endpoint_to_existing_provider.mdx
diff options
context:
space:
mode:
Diffstat (limited to 'website/content/platform/user_guides/add_endpoint_to_existing_provider.mdx')
-rw-r--r--website/content/platform/user_guides/add_endpoint_to_existing_provider.mdx804
1 files changed, 804 insertions, 0 deletions
diff --git a/website/content/platform/user_guides/add_endpoint_to_existing_provider.mdx b/website/content/platform/user_guides/add_endpoint_to_existing_provider.mdx
new file mode 100644
index 00000000000..84fb3ab80ab
--- /dev/null
+++ b/website/content/platform/user_guides/add_endpoint_to_existing_provider.mdx
@@ -0,0 +1,804 @@
+---
+title: Add Command To An Existing Provider
+sidebar_position: 11
+description: This guide outlines the process for adding a new endpoint to an existing data provider, that does not yet have a standard model.
+keywords:
+ - OpenBB Platform
+ - Open source
+ - Python interface
+ - REST API
+ - contribution
+ - contributing
+ - documentation
+ - code
+ - provider
+ - new endpoint
+ - fmp
+ - OpenBB extensions
+ - OpenBB provider
+ - standard model
+ - data model
+ - currency
+ - snapshot
+ - router
+ - how to
+---
+
+import HeadTitle from "@site/src/components/General/HeadTitle.tsx";
+
+<HeadTitle title="Add Command To An Existing Provider - How-To | OpenBB Platform Docs" />
+
+This page will walk through adding a new router endpoint to an existing data provider, and how to go about creating a new standard model.
+
+To demonstrate, we will be extending the `openbb-currency` router. The objective is to add a snapshot of currencies relative to a base currency.
+The process will be very similar to adding a data provider to an existing endpoint - described on [this page](add_data_to_existing_endpoint) -
+only here, we need to create a new router function and standard model.
+
+It's about the same amount of work, but effort should be placed in consideration of others inheriting from this model in the future.
+
+At a high level, the workflow is going to look something like:
+
+- With clear objectives, define the requirements for inputs and outputs of this function.
+- Create a standard model that will be suitable for any provider to inherit from.
+- Catalogue parameters and returned fields from the specific source of data, then build the models and fetcher.
+- Create a new router endpoint in the `openbb-currency` module.
+- Rebuild the Python interface and static assets.
+- Create unit tests.
+- Create integration tests.
+- Submit a pull request.
+
+:::note
+
+Before getting started, get a few housekeeping items in order:
+
+- Clone the GitHub repo and navigate into the project's folder.
+ - If you have already done this, update your local branch:
+ - `git fetch`
+ - `git pull origin develop`
+ - `git fetch`
+ - `git pull origin develop`
+- Install the OpenBB Platform in "editable" mode.
+ - `cd openbb_platform`
+ - `python dev_install.py -e`
+- Rebuild the Python interface and static assets.
+ - `import openbb`
+ - `openbb.build()`
+- Create a new local branch (pick a relevant name and use dashes for multiple words), always beginning with `feature/`.
+ - `git checkout -b feature/currency-snapshots`
+
+:::
+
+## Let's Get Started
+
+Currencies, as an asset class, have different data properties than securities.
+For this exercise, we're really only concerned about the differences within the market data we are working with.
+Things to keep in mind are:
+
+- Market trading hours are relative to three major centers: Hong Kong, London, New York.
+- Between the active global trading sessions, FX markets are 24/5.
+- The data returned from a source could be time-indexed to any of the three market centers, localized as UTC, or make you guess.
+- OHLC time series data will not always have volume.
+- Not all sources will provide bid/ask, and/or, lot sizes.
+- Perspective for the data is a relative relationship, there are always two "symbols".
+ - Similar to index benchmarking, but with a layer of interest rate expectations.
+- Gold and silver are typically included as, XAU and XAG, respectively.
+
+## How To Build A Standard Model
+
+The essence of a standard model is to be a shared resource with common ground between all sources.
+It should not be so specific that it is relevant only to one provider, and it needs to have defining characteristics that warrant its existence.
+
+Mandatory fields and parameters should be minimal, and names need to be consistent with similar ones across the OpenBB Platform.
+
+### Requirements
+
+Our objective in this exercise has similar endpoints in the Equity and Index modules, `obb.equity.market_snapshots()` and `obb.index.snapshots()`;
+however, there are differences between currency data and stocks.
+
+The normal parameter for most asset classes, "symbol", fits our requirement; but, it is not the correct description. Instead, we want to name it, "base".
+We need data providers to have an option to "allow" querying multiple base symbols.
+
+We want to view the universe relative to a base currency, but we also want the option for comparative analysis between multiple bases.
+
+In the data model, we'll need to split the typical "symbol" field into two: "base" and "currency".
+
+It's quite likely that a large portion of users will not desire the entire universe, but maybe 20-30 of them.
+It would be a good idea to have a parameter that filters for a list of desired currencies.
+
+For this purpose, we want to express the view as an "[indirect quote](https://www.investopedia.com/terms/i/indirectquote.asp)" from the perspective of the "base currency".
+How many units of "currency" X are received by selling one unit of the "base".
+Compared against the USD, EUR should be less than 1, AUD should be greater than 1, and gold is a large decimal.
+
+We can easily apply an inverse that allows users to decide for themselves which perspective they want to view
+the exchange rate from. This is something that will need to be applied at the provider level, and it should be a requirement.
+
+We will add a parameter, "quote_type", with choices ["indirect", "direct"].
+
+There is one major monkey wrench in all of this. Is it, EUR/USD or USD/EUR? Do all providers return the same conventions?
+It's a known-unknown, and we can't assume blindly that all follow the norm - or are even consistent with themselves.
+We'll need to check a variety of response data from each source to find out.
+
+The output needs to be usable as a conversion table, and this will likely need to be manually enforced.
+
+:::important
+
+The rule must be clearly communicated and each provider's output should be verified for compliance, else coerced to be.
+
+:::
+
+### Create File
+
+We're going to map this new endpoint in the interface to, `obb.currency.snapshots()`. We'll name the model accordingly, `CurrencySnapshots`, and create a file, `currency_snapshots.py`. The file should be created here:
+
+```console
+~/OpenBBTerminal/openbb_platform/core/openbb_core/provider/standard_models/
+```
+
+The first line of the file should be a docstring, the second line should be empty, and the import statements follow.
+
+### Import Statements
+
+The code block below are the typical imports in a standard model file, modify to suit the specific requirements.
+
+:::tip
+
+Constrained types can be imported from the Pydantic library, i.e. `PositiveInt`, `NonNegativeFloat`, etc.
+
+:::
+
+```python
+"""Currency Snapshots Standard Model."""
+
+from typing import Literal, Optional
+
+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
+```
+
+### QueryParams
+
+Don't try to add every possible parameter unless it is certain that the majority of providers will have this available from their API.
+The same applies to `Literal` types, set as a generic `str` or `int` type and redefine it within the provider model as a `Literal["choice1", "choice2"]`.
+We don't want a standard model parameter to provide invalid choices for individual providers.
+
+Our `CurrencySnapshotsQueryParams` model is going to be very similar to `MarketSnapshotsQueryParams`, with the only difference being the field name "base".
+
+:::important
+
+If the field will only sometimes accept a list of values, DO NOT define it in the standard model as a Union - `Union[str, List[str]]`.
+Instead, define it for the single value, `str`, and then add the property below to the provider's QueryParams model.
+
+```python
+__json_schema_extra__ = {"base": ["multiple_items_allowed"]}
+```
+
+:::
+
+The code block below is a continuation of the section above.
+
+```python
+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
+```
+
+It would be nice to have a list of valid choices, but each source may not have data for all currencies. Or, we could miss choices by only consulting one provider.
+This can be a consideration for the data provider models to handle, and country codes for currencies are widely known ISO three-letter abbreviations.
+
+### Data
+
+Like `QueryParams`, we don't want to attempt to define every potential future field. We want a core foundation for others to build on.
+We will define three fields as mandatory, "base_currency", "counter_currency", and "last_rate". This is enough to communicate our
+We will define three fields as mandatory, "base_currency", "counter_currency", and "last_rate". This is enough to communicate our
+data parsing requirements for this endpoint:
+
+- Split the six-letter symbol as two symbols.
+- If the provider only returns `{"symbol": "price"}`, it will need to coerced accordingly within the `transform_data` static method of the `Fetcher` class.
+
+```python
+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."
+ + " By default, 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,
+ )
+```
+
+Combine the three code blocks above to make a complete standard model file, and then we have completed the first two tasks.
+
+- [x] With clear objectives, define the requirements for inputs and outputs of this function.
+- [x] Create a standard model that will be suitable for any provider to inherit from.
+
+## Build Provider Models
+
+We're going to start with one provider, [FMP](https://site.financialmodelingprep.com/developer/docs#exchange-prices-quote), and this section will look a lot like the process outlined [here](add_data_to_existing_endpoint).
+
+Sample output data from the source is pasted below, and we can see that there are some fields which don't have anything to do with currencies. Those will be dropped.
+
+```json
+[
+ {
+ "symbol": "AEDAUD",
+ "name": "AED/AUD",
+ "price": 0.40401,
+ "changesPercentage": 0.3901,
+ "change": 0.0016,
+ "dayLow": 0.40211,
+ "dayHigh": 0.40535,
+ "yearHigh": 0.440948,
+ "yearLow": 0.356628,
+ "marketCap": null,
+ "priceAvg50": 0.39494148,
+ "priceAvg200": 0.40097216,
+ "volume": 0,
+ "avgVolume": 0,
+ "exchange": "FOREX",
+ "open": 0.40223,
+ "previousClose": 0.40244,
+ "eps": null,
+ "pe": null,
+ "earningsAnnouncement": null,
+ "sharesOutstanding": null,
+ "timestamp": 1677792573
+ }
+]
+```
+
+### Create File For Provider
+
+We need to create a new file in the FMP provider extension. This will have the same name as our standard model.
+
+```console
+~/OpenBBTerminal/openbb_platform/providers/fmp/openbb_fmp/models/currency_snapshots.py
+```
+
+The first line in the file will always be a docstring, with the import statements beginning below an empty line.
+
+```python
+"""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
+```
+
+### Provider QueryParams
+
+Here, we won't need to define any new parameters. All that's added is a URL to the endpoint's documentation,
+and then the `__json_schema_extra__` dictionary which will allow multiple base symbols to be accepted by this provider.
+
+```python
+class FMPCurrencySnapshotsQueryParams(CurrencySnapshotsQueryParams):
+ """
+ FMP Currency Snapshots Query.
+
+ Source: https://site.financialmodelingprep.com/developer/docs#exchange-prices-quote
+ """
+
+ __json_schema_extra__ = {"base": ["multiple_items_allowed"]}
+```
+
+### Provider Data
+
+We'll then need to map the fields in the sample output data to the corresponding ones in the standard model, and then define the remaining.
+
+A validator is setup to convert the percentage to a normalized value (1% -> 0.01).
+
+```python
+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
+```
+
+### Build The Fetcher
+
+The Fetcher class will always have the same general construction, in this instance we will use the `transform_data` stage to parse and filter the returned data before validating the model on output.
+
+```python
+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")
+ ]
+```
+
+The last four code blocks combined are the entire contents of the new provider model file.
+
+Next, open `~/OpenBBTerminal/openbb_platform/providers/fmp/openbb_fmp/__init__.py`, import the new model, and map it in the Provider class.
+
+Step 3 is now done.
+
+- [x] Catalogue parameters and returned fields from the specific source of data, then build the models and fetcher.
+
+## Create Router Endpoint
+
+To use our new function, we need to create a router command. The currency router is located here:
+
+```python
+~/OpenBBTerminal/openbb_platform/extensions/currency/openbb_currency/currency_router.py
+```
+
+It's as simple as copying and pasting the function above and modifying details to suit.
+The examples will be included in the docstring of the endpoint.
+
+```python
+@router.command(
+ model="CurrencySnapshots",
+ examples=[
+ APIEx(parameters={}),
+ APIEx(
+ description="Get exchange rates from USD and XAU to EUR, JPY, and GBP using 'fmp' as provider.",
+ parameters={
+ "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()))
+```
+
+Save the file, start a new Python session in a Terminal window, rebuild the app, and it's ready to use!
+
+```console
+import openbb
+
+openbb.build()
+
+exit()
+```
+
+Steps 4 and 5 are done!
+
+- [x] Create a new router endpoint in the `openbb-currency` module.
+- [x] Rebuild the Python interface and static assets.
+
+```python
+from openbb import obb
+
+obb.currency.snapshots(base="xau,xag", counter_currencies=["usd", "gbp", "eur", "hkd"],quote_type="indirect").to_df()
+```
+
+| base_currency | counter_currency | last_rate | open | high | low | volume | prev_close | change | change_percent | ma50 | ma200 | year_high | year_low | last_rate_timestamp |
+| :------------ | :--------------- | --------: | ------: | ------: | ------: | -----: | ---------: | -----: | -------------: | ------: | ------: | --------: | -------: | :------------------ |
+| XAU | USD | 2092.76 | 2083.17 | 2092.8 | 2079.4 | 2246 | 2083 | 9.76 | 0.0046855 | 2030.83 | 1976.63 | 2084.35 | 1813.82 | 2024-03-04 06:16:12 |
+| XAU | GBP | 1645.45 | 1644.1 | 1645.6 | 1640 | 643 | 1644 | 1.45 | 0.000881995 | 1603.92 | 1573.46 | 1652.15 | 1482.2 | 2024-03-04 05:45:11 |
+| XAU | EUR | 1924 | 1921.5 | 1924 | 1917.15 | 1517 | 1921 | 3 | 0.0015617 | 1874.69 | 1826.4 | 1921.6 | 1719.35 | 2024-03-04 05:51:11 |
+| XAU | HKD | 16341.8 | 16310 | 16341.9 | 16276.4 | 1665 | 16307 | 34.75 | 0.002131 | 15891.1 | 15452.8 | 16306.3 | 14238 | 2024-03-04 05:57:11 |
+| XAG | USD | 23.299 | 23.1091 | 23.3062 | 23.0172 | 2074 | 23 | 0.299 | 0.013 | 22.7862 | 23.4349 | 26.035 | 20.005 | 2024-03-04 05:56:41 |
+| XAG | GBP | 18.26 | 18.21 | 18.26 | 18.14 | 413 | 18 | 0.26 | 0.0144444 | 17.9988 | 18.5021 | 20.67 | 16.81 | 2024-03-04 05:24:10 |
+| XAG | EUR | 21.36 | 21.32 | 21.37 | 21.2087 | 1079 | 21 | 0.36 | 0.0171429 | 21.0393 | 21.4906 | 23.64 | 18.97 | 2024-03-04 05:30:10 |
+| XAG | HKD | 181.237 | 180.881 | 181.399 | 180.124 | 1596 | 180 | 1.237 | 0.0068722 | 178.342 | 181.815 | 204.411 | 157.209 | 2024-03-04 05:30:10 |
+
+## Write Tests
+
+We'll need to create a unit test for the FMP provider, and then integration tests for the Python interface and Fast API. It's as simple as creating a new router function was, copying and pasting.
+
+### Unit Test
+
+This is located in the `openbb-fmp` extension:
+
+```console
+~/OpenBBTerminal/openbb_platform/providers/fmp/tests/test_fmp_fetchers.py
+```
+
+- Import the new fetcher with the rest of the imports (keep them alphabetically sorted).
+- Copy and paste the last test function in the file.
+
+```python
+@pytest.mark.record_http
+def test_fmp_currency_snapshots_fetcher(credentials=test_credentials):
+ params = {
+ "base": "XAU",
+ "quote_type": "indirect",
+ "counter_currencies": "USD,EUR,GBP,JPY,HKD,AUD,CAD,CHF,SEK,NZD,SGD",
+ }
+
+ fetcher = FMPCurrencySnapshotsFetcher()
+ result = fetcher.test(params, credentials)
+ assert result is None
+```
+
+- Navigate to the path above and enter: `pytest test_fmp_fetchers.py --record http --record-no-overwrite`
+
+This will generate a new file:
+
+```console
+~/OpenBBTerminal/openbb_platform/providers/fmp/tests/record/test_fmp_currency_snapshots_fetcher.yaml
+```
+
+Check the file for any obvious errors, like a bad HTTP request status code.
+
+### Integration Tests
+
+The Python interface and Fast API each require a new integration test. Again, emulate an existing test and make sure to declare all parameters available to each provider.
+
+#### API
+
+Open the file below, and go to the last test in the file.
+
+```console
+~/OpenBBTerminal/openbb_platform/extensions/currency/integration/test_currency_api.py
+```
+
+We can copy this one:
+
+```python
+@parametrize(
+ "params",
+ [({"provider": "ecb"})],
+)
+@pytest.mark.integration
+def test_currency_reference_rates(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/reference_rates?{query_str}"
+ result = requests.get(url, headers=headers, timeout=10)
+ assert isinstance(result, requests.Response)
+ assert result.status_code == 200
+```
+
+Converting it for our new endpoint:
+
+```python
+@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
+```
+
+#### Python
+
+The `@parameterize` section can be copied directly to the Python integration test.
+
+```console
+~/OpenBBTerminal/openbb_platform/extensions/currency/integration/test_currency_python.py
+```
+
+```python
+@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
+```
+
+Now run `pytest` for both of these files.
+
+Step 6 & 7 are done.
+
+- [x] Add unit test.
+- [x] Add integration tests.
+
+## Submit A Pull Request
+
+We're already on the correct branch, `feature/currency-snapshots`, but it may be out-of-sync with the `develop` branch. Let's update it just to be sure.
+
+```console
+git fetch
+git pull origin develop
+```
+
+### Linters
+
+Before opening a pull request, run the linters over all files that were touched.
+
+- black
+- ruff
+- mypy
+- pylint
+
+Fix all items, and valid fixes for `pylint` can be disabling on that line. It won't always know what is contextually correct.
+
+Here are all the files we touched in this process:
+
+- `/OpenBBTerminal/openbb_platform/core/openbb_core/provider/standard_models/currency_snapshots.py`
+- `/OpenBBTerminal/openbb_platform/providers/fmp/openbb_fmp/models/currency_snapshots.py`
+- `/OpenBBTerminal/openbb_platform/providers/fmp/tests/test_fmp_fetchers.py`
+- `/OpenBBTerminal/openbb_platform/providers/fmp/tests/record/test_fmp_currency_snapshots_fetcher.yaml`
+- `/OpenBBTerminal/openbb_platform/extensions/currency/openbb_currency/currency_router.py`
+- `/OpenBBTerminal/openbb_platform/extensions/currency/integration/test_currency_api.py`
+- `/OpenBBTerminal/openbb_platform/extensions/currency/integration/test_currency_python.py`