RFC: Multi-Currency Commodity Prices¶
Status: Draft
Date: 2026-03-18
Owner: Paisa Core Team
Summary¶
Add first-class multi-currency commodity pricing across:
- Journal ingestion (ledger, hledger, beancount).
- External provider sync.
- Valuation and analytics.
- Price export back to ledger-family formats.
The current system stores one value per commodity and effectively treats it as quoted in default currency. This RFC introduces explicit price pairs and quote currency support while preserving backward compatibility.
Goals¶
- Ingest and persist commodity prices as base and quote currency pairs.
- Support valuation in default currency and arbitrary report currency.
- Preserve round-trip fidelity: DB -> export file -> sync.
- Keep existing users functional without immediate config changes.
Non-Goals¶
- Real-time streaming FX pricing.
- Intraday tick-level pricing.
- Automated provider arbitration beyond deterministic precedence rules.
Current Behavior and Gaps¶
- Journal price parsing filters to default-currency targets for most directives.
- Provider prices are stored without explicit quote currency.
- Price lookup is keyed by commodity only, not pair.
- Currency helper behavior assumes a single default currency.
This leads to loss of non-default quote information and prevents native multi-currency valuation.
Proposed Data Model¶
Add quote currency and source metadata to the price model.
Proposed table shape:
- id
- date
- base_commodity
- quote_commodity
- rate
- source_type (journal | provider)
- source_provider (nullable)
- source_ref (nullable, provider code/ticker/scheme)
- created_at
- updated_at
Proposed uniqueness:
- unique(date, base_commodity, quote_commodity, source_type, source_provider, source_ref)
Recommended indexes:
- (base_commodity, quote_commodity, date)
- (quote_commodity, base_commodity, date)
- (source_type, source_provider)
Backward compatibility strategy:
- Existing rows without quote info are migrated with quote_commodity = default currency at migration time.
Ingestion Design¶
Journal¶
- Parse and keep all price directives (not only default-currency targets).
- Store directed pair base -> quote with rate.
- If parser receives the inverse pattern (for example default currency as commodity with foreign target), normalize into canonical base -> quote direction when deterministic.
- Preserve file date semantics and timezone behavior as today.
Providers¶
- Extend provider contract to return explicit quote_commodity.
- For providers already converting to default currency (for example some stock providers), either:
- keep conversion behavior but set quote_commodity explicitly, or
- store native quote + FX chain as separate pairs if available.
- For providers that return NAV in known local currency, set quote_commodity explicitly.
Rate Resolution and Valuation¶
Introduce a pair-aware resolver:
- GetRate(base, quote, date) -> rate record.
- Resolution order:
- direct pair on or before date,
- inverse pair on or before date,
- optional one-hop cross via configured anchor currencies.
- Deterministic precedence when multiple sources exist on same date:
- journal overrides provider by default,
- tie-break by latest updated_at.
Valuation API behavior:
- Existing valuation endpoints continue to use default currency unless explicitly requested.
- New optional query parameter report_currency enables alternate report currency.
Export Design¶
Add export that writes DB prices to selected dialect format.
Inputs¶
- dialect: ledger | hledger | beancount
- from_date (optional)
- to_date (optional)
- base_commodities (optional list)
- quote_commodities (optional list)
- source filter (optional)
Output format¶
- ledger: P YYYY/MM/DD HH:MM:SS BASE RATE QUOTE
- hledger: P YYYY-MM-DD BASE RATE QUOTE
- beancount: YYYY-MM-DD price BASE RATE QUOTE
Notes:
- Keep deterministic sort order by date, base, quote, source.
- Optionally emit source comments where dialect supports comments.
API and CLI Proposal¶
API¶
- GET /api/price
- add optional filters: base, quote, from, to, report_currency, source
- GET /api/price/export
- returns text output in requested dialect
- POST /api/price/export
- supports larger filter payloads and future options
CLI¶
- paisa prices export --dialect ledger --from 2020-01-01 --to 2026-12-31 --base NIFTY --quote INR
- paisa prices export --dialect beancount --source journal > prices.beancount
Migration Plan¶
Phase 1: Schema and dual-write¶
- Add new columns/table and migration.
- Read old model, write both old and new on sync.
- Add validation logs for pair completeness.
Phase 2: Pair-aware reads¶
- Switch service price resolution to pair-aware engine.
- Keep old read path behind fallback flag.
Phase 3: API and UI¶
- Add pair filters and report currency selector.
- Update price pages/charts for base/quote awareness.
Phase 4: Export and cleanup¶
- Ship dialect export endpoint and CLI command.
- Remove old single-commodity assumptions after compatibility window.
Config Additions (Optional)¶
- price_anchor_currencies: [USD, EUR]
- price_source_precedence:
- journal
- provider
- allow_cross_rate: true
Defaults should preserve current behavior for users who do not opt in.
Testing Plan¶
- Parser tests:
- ledger/hledger/beancount directives with mixed quote currencies.
- Migration tests:
- old DB upgrades and valuation parity in default currency mode.
- Resolver tests:
- direct, inverse, and cross-rate path resolution.
- precedence tie-breaking across journal and provider rows.
- Export tests:
- deterministic output snapshots for all dialects.
- Regression tests:
- existing snapshots remain valid in default settings.
Rollout and Risk Management¶
- Feature flag: enable_multi_currency_prices.
- Compatibility mode defaulted on for one release cycle.
- Telemetry/log counters for missing pairs and failed conversions.
- Clear fallback path to old resolver if severe issues are detected.
Primary risks:
- Ambiguous provider quote currency for some feeds.
- Performance regression from pair-aware lookups.
- Behavioral changes in historical valuation due to richer data.
Mitigations:
- Require quote currency from providers at ingestion boundary.
- Add indexed query plan checks and cache pair trees.
- Keep source precedence explicit and documented.
Definition of Done¶
- Multi-currency journal prices are persisted without dropping non-default quotes.
- Provider prices include explicit quote currency.
- Valuation works for default currency and requested report currency.
- Export works for ledger, hledger, and beancount formats.
- Existing users can upgrade without manual data edits.
- Regression suite passes with compatibility mode enabled and disabled.
Decisions Locked¶
- Source precedence: journal overrides provider.
- Cross-rate resolution: one hop only.
- Export default: full history.
GitHub Issue Action Plan¶
The items below are ready to be created as GitHub issues labeled enhancement.
EPIC 1: Pair-Aware Price Schema and Migration¶
Suggested title: enhancement: add base/quote price schema with backward-compatible migration
Scope:
- Add pair-aware schema for prices with base commodity, quote commodity, rate, and source metadata.
- Add indexes and uniqueness constraints for deterministic lookups.
- Add migration from existing rows to
quote_commodity = default_currency. - Keep compatibility mode for existing single-currency behavior.
Acceptance criteria:
- Existing databases migrate successfully without data loss.
- Existing valuations in default currency remain unchanged in compatibility mode.
- New pair-aware rows can be written and queried by date/base/quote.
Dependencies:
- None.
Estimate:
- 3-5 engineering days.
EPIC 2: Journal Ingestion for Full Price Pairs¶
Suggested title: enhancement: ingest full multi-currency journal price directives
Scope:
- Update ledger/hledger/beancount price parsers to retain non-default quote pairs.
- Normalize parsed prices into canonical base -> quote direction.
- Persist journal prices with source metadata (
source_type = journal). - Add parser fixtures for mixed quote currencies.
Acceptance criteria:
- Non-default quote prices are no longer dropped during sync.
- Same input journal produces deterministic base/quote output rows.
- Existing parser behavior for default-currency-only journals remains stable.
Dependencies:
- EPIC 1.
Estimate:
- 3-4 engineering days.
EPIC 3: Provider Contract Upgrade with Explicit Quote Currency¶
Suggested title: enhancement: require explicit quote currency in price providers
Scope:
- Extend provider contract to return quote commodity explicitly.
- Update all built-in providers to populate quote commodity.
- Persist provider prices with source metadata (
source_type = provider). - Preserve current conversion behavior where already implemented.
Acceptance criteria:
- Every provider row includes explicit quote commodity.
- Provider sync succeeds across all current provider implementations.
- Missing quote metadata fails fast with actionable errors.
Dependencies:
- EPIC 1.
Estimate:
- 3-5 engineering days.
EPIC 4: Pair-Aware Resolver with One-Hop Cross Rate¶
Suggested title: enhancement: implement pair-aware rate resolver with one-hop cross rates
Scope:
- Add
GetRate(base, quote, date)style resolver. - Resolve in this order: direct pair, inverse pair, one-hop cross via anchors.
- Enforce source precedence rule: journal overrides provider.
- Add caching/index-aware lookup path for performance.
Acceptance criteria:
- Resolver returns deterministic rates for direct, inverse, and one-hop cases.
- When journal and provider both exist, journal value is selected.
- Performance is within acceptable bounds for current dataset sizes.
Dependencies:
- EPIC 1.
- EPIC 2.
- EPIC 3.
Estimate:
- 4-6 engineering days.
EPIC 5: API Enhancements for Pair Filtering and Report Currency¶
Suggested title: enhancement: extend price APIs for base/quote filters and report currency
Scope:
- Extend
/api/pricewith base/quote/date/source/report-currency filters. - Keep old API usage backward compatible where possible.
- Add consistent error envelopes for invalid combinations.
- Update API docs with request/response examples.
Acceptance criteria:
- API supports pair-aware queries and returns deterministic ordering.
- Existing clients continue to work in compatibility mode.
- Invalid requests return standardized API errors.
Dependencies:
- EPIC 4.
Estimate:
- 2-3 engineering days.
EPIC 6: Full-History Export for Ledger, Hledger, and Beancount¶
Suggested title: enhancement: export full price history from DB to ledger-family formats
Scope:
- Add export endpoint and CLI command for
ledger,hledger,beancount. - Export full history by default.
- Support optional filters (date range, base, quote, source).
- Ensure deterministic sort order for reproducible output.
Acceptance criteria:
- Export output validates in selected target dialect.
- Default export includes full available history.
- Repeated export on unchanged data yields byte-for-byte stable output.
Dependencies:
- EPIC 2.
- EPIC 3.
- EPIC 4.
Estimate:
- 3-4 engineering days.
EPIC 7: UI and UX for Multi-Currency Prices¶
Suggested title: enhancement: add UI support for base/quote prices and report currency
Scope:
- Add base/quote selectors in relevant price screens.
- Add report-currency selection for valuation views.
- Show source metadata (journal/provider) where relevant.
- Preserve existing UX defaults for current users.
Acceptance criteria:
- Users can view and filter by base/quote pairs.
- Users can switch report currency without breaking existing dashboards.
- Default views remain compatible with old workflows.
Dependencies:
- EPIC 5.
Estimate:
- 4-6 engineering days.
EPIC 8: Testing, Rollout, and Compatibility Hardening¶
Suggested title: enhancement: complete test coverage and rollout plan for multi-currency pricing
Scope:
- Add parser, migration, resolver, API, and export regression tests.
- Add feature flag rollout controls and fallback path.
- Add release notes and migration guidance.
- Add observability counters for missing pairs and conversion failures.
Acceptance criteria:
- Regression suite passes with compatibility mode on and off.
- Upgrade path is documented and validated on fixture datasets.
- Rollback and fallback procedures are documented and tested.
Dependencies:
- EPIC 1-7.
Estimate:
- 3-5 engineering days.
Recommended Issue Labels and Milestone¶
Labels:
enhancementbackendapifrontendmigrationtesting
Milestone suggestion:
Multi-Currency Prices v1