* 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>
The securities-provider checkboxes used raw Tailwind utilities
(rounded border-primary text-primary focus:ring-primary) instead of the
design-system .checkbox component. In dark mode text-primary resolves to
white, so a checked box rendered a white check on a white fill and the
checkmark was invisible.
Switch to the theme-aware .checkbox checkbox--light classes used by every
other checkbox in the app (settings/preferences, transaction filters,
etc.), which render a dark check on a light fill in dark mode.
Adds Provider::TinkoffInvest, a token-based securities provider built on the
public T-Invest REST gateway (invest-public-api.tinkoff.ru/rest). It serves
prices for Russian instruments (shares, ETF/БПИФ, bonds) and, crucially, brand
logos via the T-Invest CDN — the authoritative logo source for MOEX
instruments, which ISS (MoexPublic) does not provide.
- Registry: register `tinkoff_invest` under the :securities concept; token via
ENV TINKOFF_INVEST_API_KEY or encrypted Setting.tinkoff_invest_api_key.
- Logos independent of the price provider: Security#import_brand_logo consults
T-Invest for a logo whenever a token is configured (after the price-provider
metadata fetch, so it never short-circuits website_url backfill). Gated on
token presence, not the securities checklist.
- display_logo_url: with no website domain, a stored provider logo (T-Invest)
now beats the ticker-only Brandfetch lettermark; when a domain exists,
Brandfetch still wins (unchanged).
- MoexPublic no longer reports moex.com as the issuer website — it's the
exchange, not the issuer, and would make Brandfetch render the exchange logo
for every instrument and shadow the real brand logo.
- Prices: GetCandles (daily, paged) + GetLastPrices; Quotation units+nano/1e9;
bonds priced as percent-of-par x nominal (missing nominal raises, not 0).
- Settings: encrypted token field (always shown) + provider checkbox + en locale.
- Tests for search/info/logo-url/prices/bond/incomplete-candle and display logic.
Co-authored-by: Claude <noreply@anthropic.com>
* feat(prices): add Moscow Exchange (MOEX ISS) securities + FX provider
Add Provider::MoexPublic, a keyless provider built on the free MOEX ISS API
(https://iss.moex.com/iss), modeled on Provider::BinancePublic.
Securities: shares, funds/ETF/БПИФ (e.g. LQDT), and bonds (OFZ + corporate).
Bonds are priced clean — LAST% × FACEVALUE / 100 in the instrument currency,
with per-row FACEVALUE for amortizing issues; NKD/accrued coupon excluded.
Exchange rates: also implements ExchangeRateConcept for RUB↔{USD,EUR,CNY} via
selt TOM instruments (USD000UTSTOM/EUR_RUB__TOM/CNYRUB_TOM); the selt quote is
X/RUB, inverted for RUB→X, nil for non-RUB-crossed pairs.
Details:
- Board/engine resolution via the ISS primary-board flag with a hardcoded
priority fallback (TQBR, TQTF, TQOB, TQCB, …).
- Instrument currency from CURRENCYID/FACEUNIT (handles USD/CNY eurobonds &
FX funds), normalizing legacy SUR/RUR → RUB; default RUB.
- Full history via from/till + start= pagination; current price fallback chain
LAST → MARKETPRICE → LCURRENTPRICE → LCLOSEPRICE → PREVPRICE → latest history
close.
- Bare SECID identity, exchange_operating_mic=MISX, country_code=nil (wildcard
like Binance); search accepts .ME/.MOEX/.MISX/.MCX aliases and ISIN.
- RateLimitable throttling, SslConfigurable, Faraday retry/timeouts; all public
methods wrapped in with_provider_response.
Wired into Provider::Registry for both :securities and :exchange_rates, the
hosting provider-selection UI, locales, and config/exchanges.yml (MISX).
Docker-tested (devcontainer, Ruby 3.4.9): 29 new tests green, full provider
suite + i18n green, rubocop clean; smoke-tested against live ISS (SBER price,
OFZ clean price, USD/RUB FX).
* fix(moex): address review — FX weekend lookback, dead branch, translated hints
- fetch_exchange_rate now fetches a 10-day lookback window (not just the exact
day) so a weekend/holiday request resolves to the prior trading day's close,
matching Yahoo's behavior (Codex P2).
- Remove dead identical if/else branches in history_row_price (CodeRabbit).
- Translate moex_public_hint into ca/fr/hu/vi/zh-CN instead of English copy
(CodeRabbit).
- Add a test covering the FX prior-trading-day lookback.
* fix(moex): guard ISS date parsing; doc TQTE in board priority
Address maintainer review (jjmata):
- parse_iss_date wraps Date.parse so a malformed ISS TRADEDATE skips just that
row (with a contextual log warning) instead of failing the whole history/FX
fetch. Used in history_row_price and fx_history.
- Add TQTE to the BOARD_PRIORITY doc comment (it was in the constant but missing
from the comment).
- Add a test covering the unparseable-date skip.
* feat(design-system): add info semantic color token
Mirrors success/warning/destructive: --color-info maps to blue-600 in
light mode, blue-500 in dark mode. Unblocks the DS::Alert info variant
from carrying a raw 'blue-600' literal in icon_color and lets surface
tokens use bg-info/N alpha modifiers like the rest of the system.
Refs #1715
* refactor(design-system): adopt semantic tokens and add body slot in DS::Alert
Replaces the bg-{blue,green,yellow,red}-50 / text-{...}-700 / border-{...}-200
palette block in DS::Alert with semantic alpha-modifier surfaces
(bg-{info,success,warning,destructive}/10 + matching /20 borders).
Drops the 'blue-600' literal that icon_color was returning for the
info variant; helpers#icon now accepts color: :info backed by the
new --color-info token.
Adds an optional title: kwarg and an opt-in block-content slot so
rich alerts (title + paragraph, lists, embedded actions) can render
without callers reaching for a hand-rolled flex layout. The existing
message: API stays backward-compatible — nothing in the codebase that
already calls DS::Alert.new(message: ..., variant: ...) needs to change.
Lookbook gains with_title and with_body_slot examples covering the
new shapes.
Refs #1715
* refactor(views): migrate api_keys, hostings, lunchflow alerts to DS::Alert
Cleans up nine bespoke alert blocks that hand-rolled the same
flex + icon + bordered-surface shape DS::Alert already provides:
- settings/api_keys/{new,created,created.turbo_stream}.html.erb — three
near-identical 'Security Warning' / 'Important Security Note' boxes
using the broken bg-warning-50 / text-warning-700 raw-palette pair.
- settings/hostings/{_alpha_vantage,_eodhd,_yahoo_finance,_twelve_data,_provider_selection}_settings.html.erb —
five amber-50 / amber-200 warning boxes covering rate-limit notes,
health-check failure messaging, and the env-configured override
banner. The twelve_data plan-restriction block keeps its bullet
list and pricing link inside the new DS::Alert body slot.
- lunchflow_items/{_api_error,_setup_required}.html.erb — two modal
alert headers whose flex+icon scaffolding now collapses onto
DS::Alert. The surrounding bg-surface 'Common issues' / 'Setup
steps' info cards stay as-is; this PR only touches the alert
shape itself.
No functional or behavioural changes. Locale keys preserved.
amber-* palette uses on the alerts disappear; remaining bg-amber-*
hits in the codebase live outside the alert pattern and stay for
follow-up sub-PRs of #1715.
Refs #1715
* Binance as securities provider
* Disable twelve data crypto results
* Add logo support and new currency pairs
* FIX importer fallback
* Add price clamping and optiimize retrieval
* Review
* Update adding-a-securities-provider.md
* day gap miss fix
* New fixes
* Brandfetch doesn't support crypto. add new CDN
* Update _investment_performance.html.erb