Skip to content

feat(market-data): add Moscow Exchange (MOEX ISS) provider - #1800

Open
shkarinn wants to merge 2 commits into
wealthfolio:mainfrom
shkarinn:feat/moex-iss-provider
Open

shkarinn wants to merge 2 commits into
wealthfolio:mainfrom
shkarinn:feat/moex-iss-provider

Conversation

@shkarinn

Copy link
Copy Markdown

Description

Adds a Moscow Exchange (MOEX) market data provider backed by the public
ISS API (no API key).

Problem

Yahoo no longer serves MOEX listings, and no other built-in provider covers
them. Worse, a bare MOEX ticker resolves to whatever other venue shares it, so
Russian shares get valued with a foreign security's prices. Real cases from one
broker import:

MOEX security Resolved to Effect
AFLT (Aeroflot) Amundi Floating Rate ETF on XPAR / XSWX 260 phantom units worth ~$36k, EUR FX warnings
CHMF (Severstal) CHMFX (CI U.S. Stock Selection Fund) wrong valuation
HYDR (RusHydro) Global X Hydrogen ETF wrong valuation
MAIL (VK GDR) 600107 on XSHG ("Mailyard") wrong valuation

What this adds

  • MoexProvider (crates/market-data/src/provider/moex/), equities only,
    coverage restricted to MIC MISX (strict: rejects mic = None).
    • Latest: last trade → current official price → previous close
      (stamped with its own date before the first trade of the day).
    • History: lists the security's boards, pages each main T+ board
      (TQBR, TQTF, TQIF, currency ETF/fund boards) 100 rows at a time and
      merges one bar per trading date. This follows board moves (e.g. TMOS
      moved TQTF → TQBR in 2026), skips odd-lot (SMAL) and settlement
      (SPEQ) boards, prefers the board in the hinted currency, and falls back
      to LEGALCLOSEPRICE on days without trades. Delisted securities keep
      their history (e.g. MAIL, YNDX).
    • Search (traded main-board listings only) and profile (name, ISIN, type).
    • Numbers are parsed from the JSON literal (no f64 round trip); SUR → RUB;
      SECIDs are validated before they reach the URL path.
  • MISX (Moscow Exchange, RUB, Europe/Moscow) in exchanges.json, plus
    RUB → MISX currency priority. No Yahoo suffix: Yahoo has no MOEX data, so
    a Yahoo lookup for MISX stays an untrusted fallback checked by check_profile.
  • Resolver: MOEX addresses a security by its bare SECID, marked trusted.
  • inferred_provider_config prefers MOEX for new MISX equities (same
    pattern as Börse Frankfurt for Xetra ISINs), so Yahoo (priority 1) is not
    tried first.
  • Core wiring: DATA_SOURCE_MOEX, ProviderId::MOEX, client construction,
    e2e "disabled" list, provider settings info.

Why a migration

The quote client only constructs providers that have a row in
market_data_providers (MarketDataClient::new iterates enabled rows), so the
provider cannot be used without one. The migration inserts MOEX
(INSERT OR IGNORE, priority 14, enabled, no logo; the settings page already
handles a missing logo). It does not touch existing assets.

Out of scope

Bonds (ISS prices them in % of face on separate markets) and dividends.

Validation

Run in rust:1.98.1 (toolchain from rust-toolchain.toml):

  • cargo fmt --all -- --check — clean
  • cargo test --locked -p wealthfolio-market-data — 308 passed (16 new: parsing
    fixtures trimmed from real ISS responses, coverage, resolver)
  • cargo test --locked -p wealthfolio-core — 2074 passed (incl. new
    test_moex_equity_prefers_moex)
  • cargo test --locked -p wealthfolio-storage-sqlite — all passed (migration
    applies); portable_permissions needs an unprivileged runner, as the test
    itself states, and passes when run as a normal user
  • cargo clippy --locked -p wealthfolio-market-data -p wealthfolio-core --all-targets --all-features -- -D warnings — clean
  • cargo check --locked -p wealthfolio-server — ok
  • Live smoke test (opt-in, #[ignore]):
    cargo test -p wealthfolio-market-data --test moex_live -- --ignored — passes
    against ISS: search, profile ISIN, latest quote, multi-page history (AFLT
    2020-02-26 close 108.22 matches ISS), delisted GDR history, unknown SECID error.

Not run: wealthfolio-app (Tauri) compilation and frontend checks — no
frontend or Tauri code changed.

Checklist

By submitting this PR, I agree to the
CLA.

🤖 Generated with Claude Code

@shkarinn

Copy link
Copy Markdown
Author

Rebased onto current main after #1828 and resolved the conflicts in exchange_suffixes.rs and rules_resolver.rs. I also moved the MOEX migration after 2026-09-27-000001_exchange_registry_iso_mics. market-data, core and storage-sqlite tests pass locally. The PR is ready for review whenever you have time.

shkarinn and others added 2 commits September 30, 2026 22:17
Yahoo no longer serves MOEX listings, and a bare MOEX ticker resolves to
whatever other venue shares it (AFLT -> an Amundi ETF on XPAR, CHMF ->
CHMFX), so Russian shares end up valued with a foreign security's prices.

Add a no-key MOEX provider on the public ISS API:
- latest price (last trade, then official current price, then previous close)
- end-of-day history, paged per main T+ board and merged per trading date,
  so board moves (TQTF -> TQBR) and odd-lot/settlement boards are handled
- search and profile (name, ISIN, type)

Register MISX (Moscow Exchange, RUB) in the exchange registry, restrict the
provider to MISX, resolve its bare SECID as trusted, and prefer MOEX for new
MISX equities. The migration adds the provider row because the quote client
only builds providers listed in market_data_providers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@shkarinn
shkarinn force-pushed the feat/moex-iss-provider branch from 9b25cbf to 9bc62eb Compare September 30, 2026 15:23

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant