Skip to content

Move from float to Decimal for financial calculations#187

Draft
mbronk wants to merge 4 commits intoRustInFinance:mainfrom
mbronk:personal/mbronk/use_decimal_for_money_calculations
Draft

Move from float to Decimal for financial calculations#187
mbronk wants to merge 4 commits intoRustInFinance:mainfrom
mbronk:personal/mbronk/use_decimal_for_money_calculations

Conversation

@mbronk
Copy link
Copy Markdown
Contributor

@mbronk mbronk commented Apr 4, 2026

Important

Non-standalone!1 (therefore DRAFT, until #181 is dispositioned)

Summary

Replace all f32/f64 money 💰 types with rust_decimal::Decimal for precise financial arithmetic.
This eliminates floating-point representation errors in tax computations.

Motivation:

Starting at 131,072 PLN (2^17), f32's ULP reaches 0.015625, exceeding 0.01 PLN and making it impossible to represent individual grosz values correctly. Any realistic portfolio of ~$30k+ in US stock sales converted at ~4 PLN/USD crosses this threshold.

  • A new test (test_decimal_precision_vs_f32_sold_taxation) demonstrates a concrete 1-grosz error on a PIT-38-realistic scenario with just 3 trades.
  • This change will be required groundwork for yet another PR I plan to submit, which'll be updating cost basis calculation (i.e. including transaction fees as transaction cost, and fully capturing gross values).

Key changes:

  • Add rust_decimal 1.36.0 (macros, serde-with-float features)
  • Convert all money types (Currency, Transaction, SoldTransaction, TaxCalculationResult) and rounding functions to Decimal
  • Update parsers: PDF uses DecimalEntry; CSV parses &str→Decimal
    directly; XLSX converts calamine f64→Decimal (IEEE 754 unavoidable)
  • NBP rates: serde-with-float deserialization, cached rates as dec!()
  • ECB rates: Decimal division for inverse exchange rates
  • Round gross_sold/cost_sold to grosz in run_taxation output
  • Add rust_decimal.natvis for VS debugger visualization
  • Add proof test for f32 grosz error; update all existing tests

Footnotes

  1. Change itself is standalone, but can't commit time to revolving merge conflicts in all possible merge order permutations, therefore submitting as-is, the way I use these changes on my forked vsn.

mbronk added 4 commits April 4, 2026 16:30
Polish tax law requires different rounding methods depending on income type,
which was not previously implemented. This change brings the calculation into
compliance with Art. 63 Ordynacji Podatkowej (OP).

1. Per currency-converted "tax event" (i.e. stock sales, dividend...),
   convert each result to full "grosz" (0,01 precision) - before
   summing.
   Basis: Mathematical rounding rules (0,01zł is the lowest monetary
   value)

2. Interests and dividends are separated as they follow different
   rounding rules when calculating the lump-sum tax

   a) Interests aggregate (art. 30a ust. 1 pkt 3 PIT) as well as
      their resulting lump-sum tax, are rounded UP to the nearest
      full "grosz" — art. 63 §1a OP
   b) Dividends aggregate (art. 30a ust. 1 pkt 4 PIT) as well
      as their resulting lump-sum tax, are rounded to the nearest
      full ZLOTY (0,50zl -> 1zl)  — art. 63 §1 OP
   c) Foreign tax withholding: no rounding the standard FX rule
      to round to grosz (0,01) precision - rule #1 (above)
   d) Net/gross/cost stock proceeds are not subject to lump-sum
      tax calculations and reported in full on PIT-38 form, hence
      only standard FX rules (#1-above) applies and they are reported
      with "grosz" precision.

Signed-off-by: Mateusz Bronk <mbronk@users.noreply.github.com>
Per tax advisor consultation with Naczelna Izba Skarbowa and Art. 63
Ordynacja Podatkowa, rounding should only be applied to final tax base
and tax amount sums, not to individual transactions.

Change default behavior to carry full f32 precision through per-transaction
FX conversions. The previous per-transaction rounding to grosz is preserved
as an opt-in flag:
  - CLI: --round-per-transaction
  - GUI: Options menu toggle (MenuBar)

Also add default-run to Cargo.toml to resolve binary ambiguity.

Signed-off-by: Mateusz Bronk <mbronk@users.noreply.github.com>
Signed-off-by: Mateusz Bronk <mbronk@users.noreply.github.com>
Replace all f32/f64 money types with rust_decimal::Decimal for precise
financial arithmetic. This eliminates floating-point representation
errors in tax computations.

Motivation: starting at 131,072 PLN (2^17), f32's ULP reaches 0.015625,
exceeding 0.01 PLN and making it impossible to represent individual grosz
values correctly. Any realistic portfolio of ~$30k+ in US stock sales
converted at ~4 PLN/USD crosses this threshold.
A new test (test_decimal_precision_vs_f32_sold_taxation) demonstrates a
concrete 1-grosz error on a PIT-38-realistic scenario with just 3 trades.

Key changes:
- Add rust_decimal 1.36.0 (macros, serde-with-float features)
- Convert all money types (Currency, Transaction, SoldTransaction,
  TaxCalculationResult) and rounding functions to Decimal
- Update parsers: PDF uses DecimalEntry; CSV parses &str→Decimal
  directly; XLSX converts calamine f64→Decimal (IEEE 754 unavoidable)
- NBP rates: serde-with-float deserialization, cached rates as dec!()
- ECB rates: Decimal division for inverse exchange rates
- Round gross_sold/cost_sold to grosz in run_taxation output
- Add rust_decimal.natvis for VS debugger visualization
- Add proof test for f32 grosz error; update all existing tests

Signed-off-by: Mateusz Bronk <mbronk@users.noreply.github.com>
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