Skip to content

feat(sheet): Excel-Bond-Funktionen mit Day-Count-Conventions - #374

Merged
SamTV12345 merged 1 commit into
mainfrom
feat/excel-bond-functions
Jul 28, 2026
Merged

feat(sheet): Excel-Bond-Funktionen mit Day-Count-Conventions#374
SamTV12345 merged 1 commit into
mainfrom
feat/excel-bond-functions

Conversation

@SamTV12345

Copy link
Copy Markdown
Member

Zweiter Batch der Excel-Lücke: Wertpapier-/Bond-Funktionen

Basis für alles ist geteilte Mechanik: die Day-Count-Conventions (basis 0–4) und der Coupon-Kalender, rückwärts von der Maturity abgeleitet, mit Monatsende-Clamping (eine 31.-August-Maturity driftet nicht über den Februar auf den 30.).

Gruppe Funktionen
Coupon-Termine COUPDAYBS, COUPDAYS, COUPDAYSNC, COUPNCD, COUPPCD, COUPNUM
Kupon-Anleihen PRICE, YIELD, DURATION, MDURATION
Stückzinsen ACCRINT, ACCRINTM
Diskontpapiere DISC, INTRATE, RECEIVED, PRICEDISC, YIELDDISC
Zinszahlung bei Fälligkeit PRICEMAT, YIELDMAT

Basis: 0 = US 30/360, 1 = actual/actual, 2 = actual/360, 3 = actual/365, 4 = europäisch 30/360.

Bewusste Abweichung von HyperFormula

Die Day-Counts werden für jede Basis gegen HyperFormulas eigenes YEARFRAC gegengeprüft — mit einer Ausnahme: HyperFormula wendet die US-30/360-Regeln in falscher Reihenfolge an (toBasisUS prüft "Enddatum ist der 31." bevor ein Februar-Ende-Startdatum auf den 30. normalisiert wird). Für 29.02.2008 → 31.08.2008 zählt es 181 Tage, Excel zählt 180. Die Bond-Mathematik folgt Excel; der Test hält beide Werte fest, damit die Abweichung dokumentiert bleibt und nicht unbemerkt kippt.

Zwei Bugs beim Testen gefunden und gefixt

  • DURATION nutzte zunächst einen Jahresbruchteil als Stub statt der Coupon-Fraktion DSC/E — 10.9216 statt der dokumentierten 10.9191453. Jetzt exakt.
  • Die Reihenfolge der 30/360-Regeln (siehe oben).

Tests

20 neue Cases mit Microsofts dokumentierten Beispielwerten (PRICE 94.63436, DURATION 10.9191453, ACCRINT 16.66667, RECEIVED 1014584.654, PRICEMAT 99.98449888 …), plus PRICE↔YIELD- und PRICEMAT↔YIELDMAT-Round-Trips und Validierung (Settlement ≥ Maturity, Frequenz ≠ 1/2/4). vitest run: 139 Tests grün.

Bewusst ausgelassen

  • ODDFPRICE / ODDLPRICE / ODDFYIELD / ODDLYIELD (unregelmäßige erste/letzte Periode)
  • AMORDEGRC / AMORLINC (französische Abschreibung)
  • XLSX-Präfixe brauchen diese Funktionen nicht — sie sind alle prä-2007 und damit ohne _xlfn. gespeichert.

🤖 Generated with Claude Code

Second batch of the Excel gap: 19 security/bond functions on top of shared
day-count machinery (basis 0-4) and a coupon schedule derived backwards from
maturity with end-of-month clamping.

COUPDAYBS, COUPDAYS, COUPDAYSNC, COUPNCD, COUPPCD, COUPNUM, PRICE, YIELD,
DURATION, MDURATION, ACCRINT, ACCRINTM, DISC, INTRATE, RECEIVED, PRICEDISC,
YIELDDISC, PRICEMAT, YIELDMAT.

Day counts cross-check against HyperFormula YEARFRAC for every basis. One
deliberate divergence: HyperFormula applies the US 30/360 rules in the wrong
order, so its YEARFRAC counts 181 days for 29-Feb to 31-Aug where Excel counts
180; the bond math follows Excel.

Tests assert Microsoft documented example values plus PRICE/YIELD and
PRICEMAT/YIELDMAT round trips.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jul 28, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jul 28, 2026

Copy link
Copy Markdown

PR Summary by Qodo

Add Excel-compatible bond functions and day-count conventions

✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Adds 19 Excel-compatible bond and security functions missing from HyperFormula.
• Shares five day-count bases and maturity-anchored, month-end-safe coupon schedules.
• Registers autocomplete names and validates Excel examples, edge cases, and inverse calculations.
Diagram

graph TD
  A["Spreadsheet Formula"] --> B["Formula Engine"] --> C["Plugin Registry"] --> D["Bond Functions"] --> E["Day Counts"] --> G["Security Result"]
  D --> F["Coupon Schedule"] --> G
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Add functions to ExcelExtrasPlugin
  • ➕ Avoids registering and translating a second HyperFormula plugin.
  • ➕ Keeps all Excel compatibility functions behind one class.
  • ➖ Further expands an already large, unrelated plugin.
  • ➖ Obscures the shared financial date and coupon domain.
  • ➖ Makes isolated testing and future bond-function expansion harder.
2. Use an external finance library
  • ➕ Could reduce custom valuation code.
  • ➕ May provide broader fixed-income instrument coverage.
  • ➖ Likely differs from Excel on serial dates, day-count edge cases, and error semantics.
  • ➖ Still requires a HyperFormula adapter and function metadata.
  • ➖ Adds dependency and maintenance risk for a narrowly defined compatibility feature.
3. Use Newton-based yield solving
  • ➕ Usually converges with fewer pricing evaluations.
  • ➕ Can support configurable precision and iteration limits.
  • ➖ Requires derivatives or finite-difference approximations.
  • ➖ Is more sensitive to initial guesses and irregular price curves.
  • ➖ Provides less predictable failure behavior than bracketed bisection.

Recommendation: Keep the dedicated BondFunctionsPlugin with shared internal day-count and coupon-schedule mechanics. It isolates a cohesive financial domain, preserves explicit Excel compatibility, and integrates cleanly with HyperFormula; bracketed bisection is also a reasonable robustness-first choice for YIELD despite its extra iterations.

Files changed (3) +629 / -3

Enhancement (2) +466 / -3
bondFunctions.tsImplement Excel-compatible bond and security functions +456/-0

Implement Excel-compatible bond and security functions

• Introduces a HyperFormula plugin implementing 19 coupon-date, bond valuation, duration, accrued-interest, discount-security, and maturity-interest functions. Shared helpers provide basis-aware day counts, maturity-anchored coupon schedules, validation, price calculations, and numerical yield solving.

ui/src/js/sheet/bondFunctions.ts

excelFunctions.tsRegister bond functions with HyperFormula and autocomplete +10/-3

Register bond functions with HyperFormula and autocomplete

• Extends the Excel-function registry with the bond plugin and its function names. Extracts translation registration into a shared helper while preserving idempotent global registration.

ui/src/js/sheet/excelFunctions.ts

Tests (1) +163 / -0
bondFunctions.test.tsValidate Excel bond functions and financial date conventions +163/-0

Validate Excel bond functions and financial date conventions

• Adds coverage for registration, all five day-count bases, month-end coupon schedules, documented Microsoft examples, inverse calculations, and invalid arguments. It explicitly records the intentional US 30/360 divergence from HyperFormula's YEARFRAC behavior.

ui/src/js/sheet/bondFunctions.test.ts

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. ACCRINT ignores coupon schedule 🐞 Bug ≡ Correctness
Description
With the default calc_method=TRUE, ACCRINT ignores first_interest and computes `YEARFRAC(issue,
settlement)` instead of accruing across quasi-coupon periods. Actual/actual calculations with
unequal or irregular coupon periods therefore return incorrect accrued interest.
Code

ui/src/js/sheet/bondFunctions.ts[R299-301]

+        const from =
+          !fromIssue && Math.trunc(settlement) > Math.trunc(first) ? this.coupons(settlement, first, f).pcd : Math.trunc(issue);
+        return par * rate * this.yearFrac(from, settlement, b);
Relevance

⭐⭐ Medium

PR 373 establishes Excel-function fidelity, but no historical evidence covers ACCRINT coupon
scheduling.

PR-#373

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The callback receives first, but when fromIssue is true—the metadata default—from is always
issue, and the result is a single yearFrac call. Elsewhere, basis-1 coupon calculations
explicitly derive pcd, ncd, and their actual period length, demonstrating the schedule
information that ACCRINT bypasses.

ui/src/js/sheet/bondFunctions.ts[289-301]
ui/src/js/sheet/bondFunctions.ts[118-140]
ui/src/js/sheet/bondFunctions.ts[442-445]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
ACCRINT's default path ignores the first-interest coupon schedule and applies one year fraction from issue to settlement. Implement schedule-aware accrual so each quasi-coupon segment uses its applicable coupon-period denominator, especially for basis 1 and irregular first periods.

## Issue Context
`first_interest` currently affects only the `calc_method=FALSE` branch. The existing coupon helpers already calculate schedule dates and actual coupon-period lengths.

## Fix Focus Areas
- ui/src/js/sheet/bondFunctions.ts[289-303]
- ui/src/js/sheet/bondFunctions.ts[108-140]
- ui/src/js/sheet/bondFunctions.test.ts[110-118]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Negative yields outside bracket 🐞 Bug ≡ Correctness
Description
YIELD searches only [-0.99, 10], although semiannual and quarterly pricing remains defined for
yields below -0.99. Valid high-premium bonds therefore return #NUM! instead of their negative
yield.
Code

ui/src/js/sheet/bondFunctions.ts[R231-233]

+        let lo = -0.99;
+        let hi = 10;
+        if (target(lo) * target(hi) > 0) return numErr('Yield could not be determined.');
Relevance

⭐⭐ Medium

PR 373 establishes Excel-function fidelity, but no historical evidence covers yield solver brackets.

PR-#373

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The solver rejects the calculation unless a root is bracketed between -0.99 and 10. However,
priceOf uses the base 1 + yld / frequency, so semiannual and quarterly calculations have a wider
negative domain, and YIELD's metadata does not otherwise restrict its computed result.

ui/src/js/sheet/bondFunctions.ts[149-166]
ui/src/js/sheet/bondFunctions.ts[220-239]
ui/src/js/sheet/bondFunctions.ts[430-433]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The YIELD solver's fixed lower bound excludes valid negative roots for frequencies 2 and 4. Use a frequency-aware domain and robust root bracketing while avoiding points where the pricing expression is undefined.

## Issue Context
For example, a zero-coupon semiannual bond with settlement on a coupon date, two remaining periods, redemption 100, and price 1600 has yield -1.5 under the implemented pricing equation. Both current endpoints produce prices below 1600, so the function returns `#NUM!` without searching the valid root.

## Fix Focus Areas
- ui/src/js/sheet/bondFunctions.ts[220-241]
- ui/src/js/sheet/bondFunctions.ts[149-168]
- ui/src/js/sheet/bondFunctions.test.ts[96-101]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

3. Coupon walk silently truncates 🐞 Bug ≡ Correctness
Description
After 3999 coupon steps, coupons returns without verifying that the computed previous coupon date
reached settlement. Long but accepted date ranges consequently produce non-bracketing coupon dates
and incorrect COUP*, PRICE, YIELD, and duration results.
Code

ui/src/js/sheet/bondFunctions.ts[R114-115]

+    while (num < 4000 && this.stepMonths(anchor, -step * num) > Math.trunc(settlement)) num++;
+    return { pcd: this.stepMonths(anchor, -step * num), ncd: this.stepMonths(anchor, -step * (num - 1)), num };
Relevance

⭐ Low

Comparable extreme-bound validation findings were rejected in PRs 326 and 328.

PR-#326
PR-#328

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The loop has a hard cap but the following return is unconditional, so the helper can return `pcd >
settlement and an undercounted num. Date arguments only have a nonnegative minimum and check`
validates ordering and frequency, leaving sufficiently long ranges accepted.

ui/src/js/sheet/bondFunctions.ts[108-116]
ui/src/js/sheet/bondFunctions.ts[142-146]
ui/src/js/sheet/bondFunctions.ts[412-425]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The coupon walk silently returns an invalid schedule when its iteration cap is reached before settlement. Either derive the coupon index directly or return `#NUM!` when the cap is exhausted without bracketing settlement.

## Issue Context
At quarterly frequency the cap covers about 1000 years. For settlement `DATE(1900,1,1)` and maturity `DATE(3001,1,1)`, the returned previous coupon remains around 2001, later than settlement, while the date metadata accepts both inputs.

## Fix Focus Areas
- ui/src/js/sheet/bondFunctions.ts[108-116]
- ui/src/js/sheet/bondFunctions.ts[412-425]
- ui/src/js/sheet/bondFunctions.test.ts[52-89]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment on lines +299 to +301
const from =
!fromIssue && Math.trunc(settlement) > Math.trunc(first) ? this.coupons(settlement, first, f).pcd : Math.trunc(issue);
return par * rate * this.yearFrac(from, settlement, b);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Accrint ignores coupon schedule 🐞 Bug ≡ Correctness

With the default calc_method=TRUE, ACCRINT ignores first_interest and computes `YEARFRAC(issue,
settlement)` instead of accruing across quasi-coupon periods. Actual/actual calculations with
unequal or irregular coupon periods therefore return incorrect accrued interest.
Agent Prompt
## Issue description
ACCRINT's default path ignores the first-interest coupon schedule and applies one year fraction from issue to settlement. Implement schedule-aware accrual so each quasi-coupon segment uses its applicable coupon-period denominator, especially for basis 1 and irregular first periods.

## Issue Context
`first_interest` currently affects only the `calc_method=FALSE` branch. The existing coupon helpers already calculate schedule dates and actual coupon-period lengths.

## Fix Focus Areas
- ui/src/js/sheet/bondFunctions.ts[289-303]
- ui/src/js/sheet/bondFunctions.ts[108-140]
- ui/src/js/sheet/bondFunctions.test.ts[110-118]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +231 to +233
let lo = -0.99;
let hi = 10;
if (target(lo) * target(hi) > 0) return numErr('Yield could not be determined.');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Negative yields outside bracket 🐞 Bug ≡ Correctness

YIELD searches only [-0.99, 10], although semiannual and quarterly pricing remains defined for
yields below -0.99. Valid high-premium bonds therefore return #NUM! instead of their negative
yield.
Agent Prompt
## Issue description
The YIELD solver's fixed lower bound excludes valid negative roots for frequencies 2 and 4. Use a frequency-aware domain and robust root bracketing while avoiding points where the pricing expression is undefined.

## Issue Context
For example, a zero-coupon semiannual bond with settlement on a coupon date, two remaining periods, redemption 100, and price 1600 has yield -1.5 under the implemented pricing equation. Both current endpoints produce prices below 1600, so the function returns `#NUM!` without searching the valid root.

## Fix Focus Areas
- ui/src/js/sheet/bondFunctions.ts[220-241]
- ui/src/js/sheet/bondFunctions.ts[149-168]
- ui/src/js/sheet/bondFunctions.test.ts[96-101]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@SamTV12345
SamTV12345 merged commit 3669529 into main Jul 28, 2026
11 checks passed
@SamTV12345
SamTV12345 deleted the feat/excel-bond-functions branch July 28, 2026 18:51
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