π± FX Plugin Guide
How to create a new FX Rate Provider to fetch exchange rates from a new central bank or data source.
Base class: FXRateProvider (in backend/app/services/fx.py)
Plugin folder: backend/app/services/fx_providers/
Registry: FXProviderRegistry
π Flow
The system calls provider methods in two distinct phases:
graph TD
subgraph "Phase 1 β Exploration"
E1["get_supported_currencies()"] --> E2["Returns: list[str]<br/><small>e.g., ['USD','GBP','JPY','CHF']</small>"]
E2 --> E3["Frontend builds<br/>currency graph<br/><small>Available pairs for routing</small>"]
end
subgraph "Phase 2 β Sync"
S1["fetch_rates(<br/>date_range,<br/>currencies,<br/>base_currency)"] --> S2["Returns raw rates<br/><small>dict[currency β<br/>(date, base, quote, rate)[]]</small>"]
S2 --> S3["Service normalizes<br/><small>Alphabetical base<quote<br/>Multi-unit Γ·100</small>"]
S3 --> S4["Stored in<br/>FX_RATE table"]
end
E3 ~~~ S1
style E1 fill:#e3f2fd,stroke:#1565c0
style E2 fill:#e3f2fd,stroke:#1565c0
style E3 fill:#e3f2fd,stroke:#1565c0
style S1 fill:#e8f5e9,stroke:#2e7d32
style S2 fill:#fff3e0,stroke:#e65100
style S3 fill:#fff3e0,stroke:#e65100
style S4 fill:#f3e5f5,stroke:#7b1fa2
Phase 1 happens when a user adds a new currency pair β the system queries providers to discover which currencies they support.
Phase 2 happens during sync β the system requests actual rate data for specific date ranges.
Plugin responsibility: Fetch raw rates from the provider's API. Apply multi-unit adjustment (e.g., JPY Γ· 100). Return data in the provider's native format (no inversion, no alphabetical ordering).
Core responsibility: Normalize for storage (alphabetical base < quote), skip duplicates, handle fallback chains.
π ABC Methods
β Required (Abstract)
| Method | Signature | Description |
|---|---|---|
code |
@property β str |
Provider code (e.g., "ECB", "FED") |
name |
@property β str |
Display name (e.g., "European Central Bank") |
base_currency |
@property β str |
Primary base currency (e.g., "EUR" for ECB) |
get_supported_currencies() |
async β list[str] |
List of available quote currencies (can be static or dynamic) |
fetch_rates(date_range, currencies, base_currency) |
async β dict[str, list[tuple]] |
Fetch rates for date range. Returns {currency: [(date, base, quote, rate), ...]} |
π§ Optional (Override)
| Method | Default | Description |
|---|---|---|
icon |
None |
Provider icon URL |
base_currencies |
[base_currency] |
List of supported bases (single-base for now) |
description |
Auto-generated | Provider description |
description_i18n |
{"en": description} |
Multilingual descriptions |
warning_i18n |
{} |
Data-quality warnings shown in UI (β οΈ triangle on routes) |
docs_url |
Generic providers list | URL to documentation page |
test_currencies |
["USD","EUR","GBP","JPY","CHF"] |
Currencies that must be available for tests |
multi_unit_currencies |
set() |
Currencies quoted per 100 units (e.g., JPY, SEK). Plugin must normalize Γ·100. |
generate_static_url(path) |
β | Helper to build /api/v1/uploads/plugin/fx/{path} |
π» Implementation Example
# backend/app/services/fx_providers/my_central_bank.py
from datetime import date
from decimal import Decimal
from backend.app.services.fx import FXRateProvider
from backend.app.services.provider_registry import register_provider, FXProviderRegistry
@register_provider(FXProviderRegistry)
class MyCentralBankProvider(FXRateProvider):
@property
def code(self) -> str:
return "MCB"
@property
def name(self) -> str:
return "My Central Bank"
@property
def base_currency(self) -> str:
return "MYC" # This bank quotes all rates as 1 MYC = X foreign
@property
def icon(self) -> str:
return self.generate_static_url("mcb/logo.svg")
@property
def multi_unit_currencies(self) -> set[str]:
return {"JPY"} # JPY quoted per 100 units by this bank
async def get_supported_currencies(self) -> list[str]:
# Return list of currencies this provider can fetch
return ["USD", "EUR", "GBP", "JPY", "CHF"]
async def fetch_rates(
self,
date_range: tuple[date, date],
currencies: list[str],
base_currency: str | None = None,
) -> dict[str, list[tuple[date, str, str, Decimal]]]:
# Fetch from your API
start, end = date_range
result = {}
for cur in currencies:
raw_data = await self._call_api(cur, start, end)
rates = []
for row in raw_data:
rate = Decimal(str(row["rate"]))
# Multi-unit adjustment (plugin responsibility)
if cur in self.multi_unit_currencies:
rate = rate / 100
rates.append((row["date"], self.base_currency, cur, rate))
result[cur] = rates
return result
πΌοΈ Static Assets (Icons)
Place provider icons in backend/app/services/fx_providers/static/mcb/:
fx_providers/
βββ static/
β βββ mcb/
β βββ logo.svg β served at /api/v1/uploads/plugin/fx/mcb/logo.svg
βββ my_central_bank.py
π Related Documentation
- FX Architecture β Multi-provider design, sync process
- FX Configuration & Routing β Chain routing algorithm, fallback
- FX Providers List β ECB, FED, BOE, SNB details
- Registry Pattern Overview β How the plugin system works