Skip to content

feat(fx): real applied rate for FCY conversions (issue #253)#254

Merged
GeiserX merged 2 commits into
mainfrom
feat/fx-real-rate-conversions
Jun 20, 2026
Merged

feat(fx): real applied rate for FCY conversions (issue #253)#254
GeiserX merged 2 commits into
mainfrom
feat/fx-real-rate-conversions

Conversation

@GeiserX

@GeiserX GeiserX commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #253. When a broker reports the real EUR amount it actually applied to a EUR↔FCY conversion, DeclaRenta now values that conversion at the broker's effective rate (realEurAmount / |quantity|) instead of the official ECB mid-rate — so the broker's hidden FX spread is captured as a deductible cost (Art. 35.1.a "importe real" + 35.1.b "gastos inherentes"). ECB remains the default, the fallback, and the valuation for FCY that arrives with no euro price (stock-sale proceeds, dividends, interest).

This carves out the narrow "real conversion" case the maintainer already endorsed in #242 — it does not overturn the "always ECB, never fxRateToBase" rule: realEurAmount is a real cash amount from the statement, not a broker-derived rate.

Design

  • Trade.realEurAmount? — the real EUR FX principal (spread embedded, the separately-itemized fee EXCLUDED). The existing commission is still applied on top, so realEurAmount + commission = full real cost (no fee double-count).
  • One shared effectiveRate() helper in fx-fifo.ts, used by both addLot (acquire) and consumeLots (dispose). This is the load-bearing safety invariant: both legs of a tracked round-trip are always valued on the same basis — never a half-real/half-ECB mix, which is the only thing that could fabricate a phantom gain. Both legs real → realized FX gain = realEurReceived − realEurPaid exactly.
  • Byte-identical fallback: absent (or zero/non-finite) realEurAmounteffectiveRate returns ecbRate → prior behavior unchanged. Pinned by a regression test.
  • Lightyear populates it today (EUR-leg |Net Amt.|); every other broker falls back to ECB until its parser sets the field. Skipped under monodivisa (FX engine off).
  • No report.ts/CLI/web/profile changes: the field rides on the Trade, read in extractFxEvents, and merge.ts carries it automatically.

Robustness (from code review)

  • effectiveRate self-guards finite & strictly-positive (a stray 0/negative/NaN can never zero a conversion → ECB).
  • extractFxEvents parses realEurAmount defensively (garbage → ECB, no crash).
  • Lightyear: two EUR conversion legs sharing one timestamp are ambiguous → drop to ECB rather than mis-attach a principal (a correct mid-rate beats a wrong real principal).

Testing

  • npm run typecheck, npm run typecheck:tests, npm run lint — clean.
  • Full suite: 1729 passed / 1729 (90 files).
  • New: tests/engine/fx-fifo.test.ts (spread capture, symmetry, commission-on-top, byte-identical fallback, zero-amount self-guard), tests/parsers/lightyear-fx-realrate.test.ts (harvest, realEur + |fee| == |EUR Gross|, ambiguous-timestamp drop), tests/integration/fx-real-rate-e2e.test.ts (end-to-end through generateTaxReport).

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for broker-provided real exchange rates on FX conversions. When available, these rates are now used for valuation instead of ECB mid-rates, improving accuracy of gain/loss calculations.
  • Improvements

    • Enhanced the Lightyear CSV parser to extract real exchange rate data from statement records for CASH currency conversions.
    • Improved commission handling in FX conversions to ensure fees are applied exactly once without duplication.

…sue #253)

When a EUR↔FCY conversion carries the broker's real applied EUR amount, value
it at that effective rate instead of the official ECB mid-rate, so the broker's
hidden FX spread is captured as a deductible cost (Art. 35.1 LIRPF). ECB stays
the default and the fallback, and remains the valuation for FCY that arrives
with no euro price (stock proceeds, dividends, interest).

- types: add optional Trade.realEurAmount (real EUR FX principal, fee excluded;
  NOT fxRateToBase, which stays banned).
- engine (fx-fifo.ts): one shared effectiveRate() helper used by BOTH addLot
  and consumeLots, so acquire and dispose legs are always valued on the same
  basis — the symmetry invariant that prevents a phantom gain. Absent/zero/
  non-finite realEurAmount falls back to ECB (byte-identical to prior behavior).
  Commission still applied on top (no fee double-count).
- parser (lightyear.ts): harvest the conversion pair's EUR-leg |Net Amt.| and
  stamp it on the non-EUR CASH trade. Two EUR legs sharing a timestamp are
  ambiguous → drop to ECB rather than mis-attach a principal.

Pinned by tests/engine/fx-fifo.test.ts (spread capture, symmetry, no double-
count, byte-identical fallback, zero-amount self-guard), tests/parsers/
lightyear-fx-realrate.test.ts (parser harvest, reconciliation, ambiguity drop),
and tests/integration/fx-real-rate-e2e.test.ts (end-to-end).
@codecov

codecov Bot commented Jun 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.34%. Comparing base (232fc43) to head (3f40c92).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #254      +/-   ##
==========================================
+ Coverage   95.30%   95.34%   +0.04%     
==========================================
  Files          54       54              
  Lines        5192     5219      +27     
  Branches     1739     1755      +16     
==========================================
+ Hits         4948     4976      +28     
  Misses        210      210              
+ Partials       34       33       -1     
Files with missing lines Coverage Δ
src/engine/fx-fifo.ts 98.18% <100.00%> (+0.08%) ⬆️
src/parsers/lightyear.ts 99.30% <100.00%> (+0.06%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@GeiserX, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 52 minutes and 17 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ff04a9c6-0fd5-4634-aa9e-0518f344c6d9

📥 Commits

Reviewing files that changed from the base of the PR and between f218761 and 3f40c92.

📒 Files selected for processing (2)
  • src/engine/fx-fifo.ts
  • src/parsers/lightyear.ts
📝 Walkthrough

Walkthrough

Adds an optional realEurAmount field to the Trade interface and FxEvent, enabling brokers to supply the actual EUR principal used in a CASH EUR↔FCY conversion. A new effectiveRate() helper in FxFifoEngine selects realEurAmount/|quantity| over ECB mid-rate when the field is present and valid. The Lightyear parser is extended to derive and attach this value from EUR-leg net amounts. Tests cover engine valuation, commission handling, regression guards, and end-to-end pipeline behavior.

Changes

Real EUR Amount FX Effective-Rate Valuation

Layer / File(s) Summary
Trade interface and FxEvent contract
src/types/ibkr.ts, src/engine/fx-fifo.ts
Trade gains optional realEurAmount?: string with documentation; FxEvent gains optional realEurAmount?: Decimal as the parsed downstream counterpart.
extractFxEvents: parse and propagate realEurAmount
src/engine/fx-fifo.ts
Reads trade.realEurAmount, validates finite and strictly positive, and passes the Decimal value onto both acquire and dispose FxEvent legs.
effectiveRate helper and FxFifoEngine valuation updates
src/engine/fx-fifo.ts
New private effectiveRate() picks `realEurAmount/
Lightyear parser: EUR principal pre-scan and realEurAmount attachment
src/parsers/lightyear.ts
Pre-scan extended to track EUR-leg net/gross principal per timestamp and flag ambiguous multi-EUR-leg timestamps; realEurAmount conditionally spread into emitted FX trade objects.
FX FIFO engine unit tests
tests/engine/fx-fifo.test.ts
New suite validates real-rate disposal valuation, symmetric round-trip gain, commission non-double-counting, ECB equivalence, extraction propagation, and zero-value guard.
End-to-end and parser integration tests
tests/integration/fx-real-rate-e2e.test.ts, tests/parsers/lightyear-fx-realrate.test.ts
Covers spread capture, symmetric reconciliation, ECB regression guard, commission-once semantics, and full Lightyear CSV → generateTaxReport pipeline; plus parser unit tests for all realEurAmount set/unset scenarios.
CLAUDE.md documentation
CLAUDE.md
Documents effective-rate formula, commission handling, parser scope, and pinned test references for the new realEurAmount behavior.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • GeiserX/DeclaRenta#163: Both PRs modify src/parsers/lightyear.ts FX conversion parsing to change how EUR-side Net Amt. vs Gross Amount drives commission and net inputs — the foundation this PR's realEurAmount derivation builds directly on.
  • GeiserX/DeclaRenta#244: Both PRs modify FxFifoEngine.consumeLots() tracing/record output — #244 adds lotAcquireDate to the same dispose trace path that this PR extends with effectiveRate-based proceeds.
  • GeiserX/DeclaRenta#176: Both PRs modify FxFifoEngine.extractFxEvents#176 changes quantity/direction derivation for EUR.xxx trades, which this PR uses alongside realEurAmount to compute the effective rate.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(fx): real applied rate for FCY conversions (issue #253)' directly and specifically describes the main change: implementing support for real applied FX rates for foreign currency conversions, matching the PR's primary objective.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/fx-real-rate-conversions

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

- effectiveRate: reject negative realEurAmount (a real principal is positive)
  instead of absorbing it via abs(); 0/NaN/non-finite still fall back to ECB.
- lightyear: use the EUR-leg Net Amt. only as the real principal; drop the
  fee-inclusive Gross fallback that would double-count the fee when Net is
  empty (omit the field → ECB instead).
@GeiserX GeiserX merged commit 66d5f4c into main Jun 20, 2026
5 checks passed
@GeiserX GeiserX deleted the feat/fx-real-rate-conversions branch June 20, 2026 21:03
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.

Uso de cambio real aplicado en conversiones en vez de ratio ECB

1 participant