* feat(exchange-rates): add Frankfurter as an exchange-rate provider
Frankfurter (frankfurter.dev) is a free, keyless FX rates API backed by
ECB daily reference rates, with no published rate limit and no auth
flow to maintain (unlike Yahoo Finance's reverse-engineered cookie/
crumb auth or TwelveData's fast-exhausting free tier).
Follows the Provider::MoexPublic template: Faraday client with retry
middleware, SslConfigurable for self-hosted CA support, a light
RateLimitable throttle, and a FRANKFURTER_URL env escape hatch for
self-hosters. Registered as exchange-rates-only (no security/stock
data) and added to the hosting settings dropdown.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* refactor(provider): switch Frankfurter to the v2 API
v1 is explicitly marked "frozen" on Frankfurter's own root endpoint; v2
is "current" and covers 201 currencies across 84 central banks vs v1's
~30 ECB-only. Confirmed via the v2 OpenAPI spec and live requests:
- Single-date lookups now use GET /rate/{base}/{quote}?date=..., which
carries weekends/holidays forward server-side (a Saturday returns a
real rate directly), so the provider no longer needs its own
lookback-window logic.
- Range lookups now use GET /rates?base=..."es=...&from=...&to=...,
a flat array of { date, base, quote, rate } records (v2's shape)
instead of v1's { "rates": { date => currencies } } hash.
- Every calendar day in a range is present (v2 gapfills itself), rather
than v1's omit-non-trading-days behavior.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(provider): sanitize currency codes before URL path interpolation
from/to were only upcased before being interpolated directly into the
URL path in fetch_exchange_rate (GET /rate/{from}/{to}). Low risk since
currency codes come from validated internal sources, but adds cheap
defense-in-depth: strip anything that isn't A-Z, matching the ISO 4217
format real currency codes always take.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>