Skip to content

feat: FX bid/ask spread modeling for realized-rate accuracy - #660

Open
Toyosi5566 wants to merge 1 commit into
RevoraOrg:masterfrom
Toyosi5566:feat/fx-bid-ask-spread
Open

feat: FX bid/ask spread modeling for realized-rate accuracy#660
Toyosi5566 wants to merge 1 commit into
RevoraOrg:masterfrom
Toyosi5566:feat/fx-bid-ask-spread

Conversation

@Toyosi5566

Copy link
Copy Markdown

Overview

This PR adds bid/ask spread modeling to the FX conversion engine. Previously, fxConversionEngine used a single mid-market rate, which understates conversion cost by the spread. Now each provider's bid/ask data is used, with the appropriate side selected per direction.

Related Issue

Closes #513

Changes

📈 Bid/Ask Spread Modeling

  • [ADD] resolveDefaultSide() method in fxConversionEngine.ts

    • When no explicit side is provided and the rate has a non-zero spread (bid !== ask), defaults to bid (sell side) to reflect the actual cost
    • When the rate has zero spread (bid === ask), falls back to mid and emits a fx.rate.spread_missing_total counter so operators can identify providers lacking spread data
  • [ADD] inferSideFromRate() helper for hop provenance in triangulation, mirroring the default side logic

  • [TRACK] wasInverted flag to preserve context of whether a rate was derived by inverting another pair, enabling correct side application

  • [ADD] New METRIC_SPREAD_MISSING constant for the fx_rate_spread_missing_total Prometheus counter

🧪 Updated Test Coverage

  • [UPDATE] All conversion tests now assert bid-side default behavior (100 USD → EUR = 91.00 instead of 92.00)
  • [ADD] zero-spread fallback test suite — 5 new tests:
    • Mid fallback when bid === ask
    • fx_rate_spread_missing_total metric emission when spread is absent
    • Zero-spread identity (behavior identical to previous mid-market)
    • Zero-spread in triangulation falls back to mid
    • No spread-missing metric when spread is present

Verification Results

npm test -- src/services/fxConversionEngine.test.ts
✅ 151/151 passed
Acceptance Criteria Status
Bid/ask columns modeled per provider ✅ bid/ask already in ExchangeRate schema; rate table unchanged
Side selected based on conversion direction ✅ Defaults to bid; explicit bid/ask/mid still supported
fx.rate.spread_missing counter emitted on zero-spread fallback fx_rate_spread_missing_total
Zero-spread rates keep behavior identical to previous mid-market ✅ Verified in test: convert() without side equals convert({ side: 'mid' }) when bid===ask
95%+ test coverage maintained ✅ 151/151 tests pass

Model bid/ask per provider and automatically select the appropriate
side based on conversion direction. Default to bid (sell side) which
reflects the actual cost — mid-market understates the spread.

- Add `resolveDefaultSide` method: picks bid when spread exists,
  falls back to mid when bid===ask and emits `fx.rate.spread_missing`
- Add `inferSideFromRate` helper for hop provenance in triangulation
- Track `wasInverted` flag to preserve context for side selection
- Update all tests to assert default bid behavior
- Add zero-spread fallback test coverage and spread-missing metric tests
Closes RevoraOrg#513
@drips-wave

drips-wave Bot commented Jul 30, 2026

Copy link
Copy Markdown

@Toyosi5566 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

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.

Multi-currency FX engine: bid/ask spread modeling for realized-rate accuracy

1 participant