diff --git a/src/installments/split.ts b/src/installments/split.ts index 316c44e..cae1995 100644 --- a/src/installments/split.ts +++ b/src/installments/split.ts @@ -3,9 +3,11 @@ import expWad from "../fixed-point-math/expWad.js"; import lnWad from "../fixed-point-math/lnWad.js"; import fixedRate, { INTERVAL, type IRMParameters } from "../interest-rate-model/fixedRate.js"; import abs from "../vector/abs.js"; +import add from "../vector/add.js"; import fill from "../vector/fill.js"; import mean from "../vector/mean.js"; import mulDivUp from "../vector/mulDivUp.js"; +import powDiv from "../vector/powDiv.js"; import sub from "../vector/sub.js"; import sum from "../vector/sum.js"; @@ -23,6 +25,7 @@ export default function splitInstallments( power = (WAD * 60n) / 100n, scaleFactor = (WAD * 95n) / 100n, tolerance = WAD / 1_000_000_000n, + rateTolerance = 10_000n, maxIterations = 66_666n, } = {}, ) { @@ -59,7 +62,23 @@ export default function splitInstallments( error = mean(mulDivUp(abs(diffs), weight, WAD)); } while (error >= tolerance); - return { amounts, installments, rates }; + const maturityFactors = rates.map( + (_, index) => + (BigInt(firstMaturity + index * INTERVAL - timestamp) * WAD) / + BigInt(timestamp + maxPools * INTERVAL - (timestamp % INTERVAL)), + ); + let effectiveRate = rates[0]!; // eslint-disable-line @typescript-eslint/no-non-null-assertion + error = 0n; + do { + const aux = add(mulDivUp(maturityFactors, effectiveRate, WAD), WAD); + const f = sum(mulDivUp(installments, WAD, aux)) - totalAmount; + const fp = -sum(mulDivUp(mulDivUp(installments, maturityFactors, WAD), WAD, powDiv(aux, 2n, WAD))); + const rateDiff = (-f * WAD) / fp; + effectiveRate += rateDiff; + error = rateDiff < 0n ? -rateDiff : rateDiff; + } while (error >= rateTolerance); + + return { amounts, installments, rates, effectiveRate }; } function max(a: bigint, b: bigint) { diff --git a/src/vector/add.ts b/src/vector/add.ts new file mode 100644 index 0000000..064bad9 --- /dev/null +++ b/src/vector/add.ts @@ -0,0 +1,5 @@ +import map2 from "./map2.js"; + +export default function add(a: readonly bigint[], b: readonly bigint[] | bigint) { + return map2(a, b, (a_, b_) => a_ + b_); +} diff --git a/src/vector/max.ts b/src/vector/max.ts new file mode 100644 index 0000000..4aa48f1 --- /dev/null +++ b/src/vector/max.ts @@ -0,0 +1,3 @@ +export default function max(array: readonly bigint[]) { + return array.reduce((maxValue, value) => (value > maxValue ? value : maxValue)); // eslint-disable-line unicorn/no-array-reduce +} diff --git a/src/vector/min.ts b/src/vector/min.ts new file mode 100644 index 0000000..44b7133 --- /dev/null +++ b/src/vector/min.ts @@ -0,0 +1,3 @@ +export default function min(array: readonly bigint[]) { + return array.reduce((minValue, value) => (value < minValue ? value : minValue)); // eslint-disable-line unicorn/no-array-reduce +} diff --git a/src/vector/powDiv.ts b/src/vector/powDiv.ts new file mode 100644 index 0000000..e822cf4 --- /dev/null +++ b/src/vector/powDiv.ts @@ -0,0 +1,5 @@ +import map3 from "./map3.js"; + +export default function powDiv(a: readonly bigint[], b: readonly bigint[] | bigint, c: readonly bigint[] | bigint) { + return map3(a, b, c, (a_, b_, c_) => a_ ** b_ / c_); +} diff --git a/test/installments.test.ts b/test/installments.test.ts index 0b6ab38..747c28c 100644 --- a/test/installments.test.ts +++ b/test/installments.test.ts @@ -4,7 +4,9 @@ import { parseUnits } from "viem"; import WAD, { SQ_WAD } from "../src/fixed-point-math/WAD"; import { INTERVAL, type IRMParameters } from "../src/interest-rate-model/fixedRate"; +import max from "../src/vector/max"; import mean from "../src/vector/mean"; +import min from "../src/vector/min"; import mulDivDown from "../src/vector/mulDivDown"; import sum from "../src/vector/sum"; @@ -28,7 +30,7 @@ describe("installments", () => { totalAmount = (totalAmount * totalAssets * (WAD - uGlobal)) / SQ_WAD; if (sum(uFixed) > 0n) uFixed = mulDivDown(uFixed, uGlobal - uFloating, sum(uFixed)); - const { amounts, installments } = splitInstallments( + const { amounts, installments, rates, effectiveRate } = splitInstallments( totalAmount, totalAssets, firstMaturity, @@ -43,6 +45,8 @@ describe("installments", () => { expect(amounts).toHaveLength(uFixed.length); expect(sum(amounts)).toBeGreaterThanOrEqual(totalAmount); expect(sum(amounts) - totalAmount).toBeLessThan(totalAmount / 100_000n); + expect(effectiveRate).toBeGreaterThanOrEqual(min(rates)); + expect(effectiveRate).toBeLessThanOrEqual(max(rates)); const avg = mean(installments); for (const installment of installments) {