Conversation
Correctness:
- A rate at or below -100% returns {:error, :undefined} instead of raising
ArithmeticError or returning garbage everywhere (1 + rate)^t is computed:
xnpv/npv/mirr, TVM fv/pv/pmt/amortization_schedule, Bonds price + risk
metrics, Returns dpp/profitability_index/twr, effective_annual_rate, sln.
nominal_rate(-1.0, m) now returns -m so it stays the inverse of
effective_annual_rate at total loss.
- Bonds duration/modified_duration/convexity return :undefined when a negative
coupon zeroes the price, rather than a nonsensical value or a crash.
- ytm and profitability_index round once at full precision (ytm's periodic
rounding was amplified by freq); a regression test pins ytm at the default
precision.
- xirr(dates, []) returns :mismatched_lengths instead of crashing.
- Drop the broad rescue in normalize (dates parse via Date.from_erl/1); a
malformed amount raises as a caller bug instead of being mislabeled
:invalid_date.
Consistency and cleanup:
- Centralize discounting in one overflow-safe (1 + rate)^-t helper
(Shared.discount_factor), used by present_value and the bond/returns metrics,
which previously used a divide form that could overflow.
- Validate :precision as 0..15; accept integer :guess/:tolerance. Require an
integer Bonds freq; reject a fractional syd period.
- Bracket scan stops one step sooner when the root sits at the guess. Hoist the
solvers' safely/1 and rounding into Shared; dedupe the amortization row
converters; trim comments that restated code.
Testing:
- Run the solver fuzz properties against both Newton and Brent, and add a
dated, many-flow, random-sign XIRR property (only 2-flow dated series were
fuzzed before). A real Newton/Brent robustness bug would surface here.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AVibXT2w1dEdVtMHqffFKn
The only feature gating < 1.18 was Enum.sum_by/2 (Elixir 1.18), introduced in the 1.6.1 review pass; revert those six sites to Enum.reduce. The rest of the code is ~1.12 vintage. Widen the CI matrix to 1.15.8/25 and 1.18.4/27 (test only); format and Credo run on the newest version, whose formatter output is the reference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AVibXT2w1dEdVtMHqffFKn
CLAUDE.md and the 1.6.1 changelog reflect the lowered requirement and the widened CI matrix. The 1.0.0 changelog entry keeps ~> 1.18 as the historical record. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AVibXT2w1dEdVtMHqffFKn
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This release hardens the library against edge cases where rates at or below -100% would cause arithmetic errors or nonsensical results. It also fixes a double-rounding bug in bond yield calculations and improves validation of input parameters.
Key Changes
Rate validation (the main fix)
(1 + rate)^tnow validate that1 + rate > 0and return{:error, :undefined}instead of raisingArithmeticErroror returning garbage. This includes:CashFlow.xnpv,npv,mirrTVM.fv,pv,pmt,amortization_scheduleBonds.priceand risk metrics (duration,modified_duration,convexity)Returns.discounted_payback_period,profitability_index,twrRates.effective_annual_rateDepreciation.sln(also now rejects non-positivelife)Rates.nominal_rate(-1.0, m)now correctly returns-m(total loss), staying the exact inverse ofeffective_annual_rateBond yield rounding fix
Bonds.ytmnow solves the per-period rate at high precision (≥12 decimals) and rounds the annualized yield once, rather than rounding the period rate first and then multiplying byfreq. The old path letfreqamplify rounding error into the last reported digit (off by 1–3 units depending on frequency).Returns.profitability_indexsimilarly computes at full precision before the final roundInput validation improvements
:precisionis now validated as0..15(the rangeFloat.round/2accepts) instead of any non-negative integer:guessand:tolerancenow accept integers as well as floatsBondsfunctions now require an integerfreq(matching the documentedpos_integertype)Depreciation.sydrejects fractionalperiodvalues, matchingddb/dbxirr(dates, [])now correctly returns{:error, :mismatched_lengths}instead of crashing (empty amounts list is the two-list form, not options)Discounting refactor
discount_factor(rate, t)helper that computes(1 + rate)^-t(negative exponent). This avoids the divide form1 / (1 + rate)^twhich would overflow its denominator at high rates over long horizons. All bond and returns metrics now route through this helper.safely/1wrapper (which catchesArithmeticErrorand maps it to:diverged) toFinance.Sharedso both solvers use it consistentlyDate parsing robustness
CashFlow.xirrnow usesDate.from_erl/1instead of a broadrescue, so malformed amounts raise immediately (a caller error) rather than being mislabeled as{:error, :invalid_date}Test coverage
@solverslist to run fuzz properties and pathological corpus tests against both Newton and Brent solvers, ensuring both satisfy the same robustness contractsNotable Implementation Details
duration,modified_duration,convexity) now return{:error, :undefined}when a negative coupon drives the unit-face price to zero, rather than a nonsensical value or crashTVM.amortization_schedulevalidates that the principal is positive and the rate is above -100% before building the schedule, preventing undefined payment calculations1 + rate > 0(orhttps://claude.ai/code/session_01AVibXT2w1dEdVtMHqffFKn