Skip to content

Latest commit

 

History

History
503 lines (404 loc) · 33.4 KB

FinancialMath.md

File metadata and controls

503 lines (404 loc) · 33.4 KB

Lesson 1 - Part 2

Financial Math

JavaScript (JS) was designed as front-end language, providing the simpler data type to cope with the need of rendering, initially just the string type to express texts and number or bigint types to compute math. JS was not designed to be strongly typed language, but the language was designed to delegate at runtime the assessment of the type to represent any variable according its content. When the success of JS as 'browser language' made it popular as a more general purpose language, the need to pinpoint the data type of variable pushed the creation of TypeScript (TS) as a language derived by and compatible with JS. TS makes explicit the type definition of variables.

Number Type Limits

The JS number type implements the 64 bits Double Precision Floating Point representation as the double type common in C#, Java/Kotlin and Rust/Zig programming languages. This representation uses 8 bytes to approximate the value as follows

  • 1 bit to represent the value sign,
  • 11 bits to represent an exponent, one bit for the exponent sign and 210 = 0-1024 as exponent range value,
  • 52 bits to represent an integer - named significand multiplied for the above exponent and sign, 252 is roughly equal to 4500000000000000 (45 followed by fourteen zeros).

Any value can't be represented exactly as a product of the integers significand × 2exponent × sign is approximated with less precision for values very big or very small fraction, positive or negative doesn't matter.

Actually, IEEE-754 represents precisely values in the range ±2.23×10−308 to ±1.80×10308 in 16 decimal digits comprehensive of the integer and fractional parts.

It's important to anticipate that, due to the finite quantity of computer memory and the binary nature of electronic logic, computers can only represent values as polynomial expressions of powers of 2. Hence, any rational value not the result of a ratio between two values made of powers of two, like 1/3 returning "zero dot an infinite string of three" (also named 'repeating decimal three'), they can only be approximated. The quantity of memory to represent precisely 1/3 is infinite. It is worth to remember any irrational number, $\sqrt{2}$ and π are the most famous, can be only approximated in a computer architecture because writing an irrational value needs an infinite number of digits.

From what explained above, albeit the IEEE-754 allows to express a wide range of values, the 16 decimal digit precision is not sufficient for many purposes, like financial math. For example, approximating values to cents (two digits), the biggest value representable in any currency is 1014 hence at most 10 trillions (1012) of the currency units. Using more digits to represent fraction of cents to compute the compound interest, the remaining digits allow to express values in the range of billions, this is a too small range.

Cryptocurrency Economy Math

Cryptocurrencies like BTC, ETH or VeChain VET requires 18 decimal digits to express the fractional part of their nomination currency unit, plus the digits required to express the units, hence the IEEE-754 precision is too coarse for the purpose of cryptocurrency economy.

The need of 18 digits for the fractional part of a cryptocurrency is suggested by the idea to express cryptocurrency values as an integer value of its atomic smallest subunit named wei, to honor the computer engineer Wei Dai.

JS provides the bigint type, characterized to be a variable length type, as string is. The bigint type expands to include all the bits needed to express any integer value, bigint values are limited by available memory of the JS runtime. However, if bigint is ideal to express currency values, it doesn't work well for any mathematics operation involving the concept of ratio, as the division and root. Between integers, 1/3=0, not ideal when the same result computed between real values returns 'repeating decimal' .3.

FixedPointNumber Type

The VeChain SDK provides the FixedPointNumber class to provide an unrestricted representation of values, as bigint but supporting the concept of 'ratio' and rational numbers, allowing to define the precision of the approximation of irrational values.

The most important characteristic of the FixedPointNumber class is the algorithms it provides are purely based on bigint hence the code developed using the SDK runs smoothly on any JS runtime because bigint type is a base type of the JS language.

bignumber.js comparison

In JS there are other libraries addressing the same problem to represent values with adjustable precision. Among them, it is worth to name bignumber.js. The FixedPointNumber class was developed using bignumber.js as reference and benchmark: most of the methods provided by the FixedPointNumber class have the same names and signature of the same methods provided by bignumber.js. Being bignumber.js a popular and excellent library to deal with financial math in JS, FixedPointNumber class is designed to make trivial to adapt code developed for bignumber.js to run based on FixedPointNumber.

The reason the VeChain SDK provides its FixedPointNumber class instead of using bignumber.js is because the algorithms implemented in the class allows a true unrestricted precision, just limited by the available memory, those are simpler to verify manually, the source code links the abstract descriptions in math literature hence everyone competent can verify those are correctly implemented for each non-trivial algorithm like the division, the power exponentiation and square roots. All algorithms are strictly based on bigint.

The two libraries differ in the way they represent values.

The bignumber.js library encodes each value similarly as IEEE-754 number using significand, exponent and sign properties, with the difference the significand is an array of numbers (8 bytes per element) encoding each digit of the value to approximate. For example, the value -1.234567 × 104 is represented internally as

  • significand = [1,2,3,4,5,6,7]
  • exponent = 4
  • sign = 1 (to flag a negative value).

The mention and comparison with bignumber.js is due because FixedPointNumber class doesn't implement the whole functionalities of the bignumber.js library. For example FixedPointNumber class supports the square root operation, but it doesn't support yet the power to fractional exponent (n0.5/sup> = $\sqrt{n}$).

NOTE: VeChain uses and suggests to use bignumber.js for the functionalities not included in the FixedPointNumber class. The bignumber.js library is not part of the SDK and must be imported explicitly in any JS/TS project using it.

FixedPointNumber math

The FixedPointNumber class adopts a much simpler implementation inspired to the way the blockchain protocols, like Bitcoin, Ethereum and Thor represents cryptocurrencies values.

  • If the value is an integer, it's represented in a bigint.
  • If the value is a rational value, hence it has a fractional part between ±1 and 0, it is scaled up multiplying itself by 10 for the number of times equal to the number of digits required to write the fractional part.

For this reason the FixedPointNumber exposes two properties, both of bigint type:

  • scaledValue expresses the original value
    • if integer the integer itself,
    • if not integer the value multiplied by 10fractionalDigits;
  • fractionalDigit expresses the number of decimal digits required to write the fractional part of the value, it's 0 for integers.

For example 1.234567 is encoded as

  • scaledvalue = 1234567 (as 1.234567 × 106),
  • fractionalDigit = 6.

The expression 'fixed point number' means a FixedPointNumber objects uses fractionalDigit to fix its precision and pinpoint where the decimal separator, (the . dot character by default) is placed when the value is printed.

FixedPointNumber In Action

The FixedPointNumber class is part of the VeChain Data Model** hence, the .of(exp: bigint | number | string) method is the standard way to create a new object from the exp argument.

The following example shows the same value, represented by two different type expressions, creates two equivalent objects and how the .bi and .n properties behave.

// STEP 1: import FixedPointNumber and BigNumber to compare results among the two libraries.
import {FixedPointNumber} from '@vechain/sdk-core';
import {BigNumber} from 'bignumber.js';

// STEP 2: create two equivalent objects from different expression types and check they are equivalent.
let x = FixedPointNumber.of(123.456789);
let y = FixedPointNumber.of('123.456789');
console.log(`FPN value ${x} from number is ${x.isEqual(y) ? '' : 'not'}equal to ${y} from string.`);

// STEP 3: cast to a number
console.log(`Cast FPN value to number is ${x.n}.`);

// STEP 4: cast a rational value to a bigint type truncates the value to integer.
console.log(`Cast FPN value to bigint is ${x.bi}.`);

Division challenge

The arithmetic of the FixedPointNumber class is trivial for addition, subtraction and multiplication, the game becomes interesting when the division is involved. Recalling the well known ratio 1/3, the result of the division between bigint is zero and from above explanation, we know the object represents the internal operand as integers. How FixedPointNumber class solves the challenge to get a rational result from integer division?

The FixedPointNumber class uses the fractionalDigits property to scale up and down the scaledValue appropriately to a precision fine enough, by default when integers are involved in the division, logarithm and root operations internally, the class' algorithms scale up the value representation of 1020 imposing an internal minimal precision of 20 decimal digits, to be sure the result is accurate for the 18 digits the cryptocurrency math needs to represent any value in terms of wei atomic subunits, have two spare digits more to approximate the less significant wei digit.

We will see later the .dp(decimalPlaces: bigint | number) method allows to fix the minimal required precision. When two rational numbers are involved in arithmetic operations risking leading to a loss of precision, the internal math is scaled up to the double of the fractional digits of argument having the higher fractionalDigit value using the .dp method.

For the 1/3 ratio the division is made between 10000000000000000000000000000000000000000 / 30000000000000000000000000000000000000000, hence the integer is scaled up by 10default 20 fractional digit precision multiplied 2 times: the division will return something like 0.33333333333333333333???????????????????? where the question mark acts as a placeholder for digits should be 3, but it will diverge from 3 as the digit is closer to the less meaningful digit. As recalled many times, computers must approximate at some point. Then, the 40 digits internal precision is scaled down 10default 20 fractional digit preserving an accurate results for the wished 20 fractional digits' precision.

The elegance of FixedPointNumber implementation is the precision is fixed, but expand and shrink internally to assure at least the wished precision, consuming more memory only during the computation, returning to the operating system after the result is computed, in any case consuming less memory than bignumber.js implementation, the latter uses 8 bytes per each digit of precision.

The following example divide 1 by 3 comparing three results made by JS numbers, bignumber.js math and FixedPointNumber math with the default precision of 20 fractional digits.

// STEP 5: compute 1/3 comparing JS number, BigNumber and SDK FixedPointNumber math
x = FixedPointNumber.of(1);
y = FixedPointNumber.of(3);
let r = x.div(y);   // r for 'ratio'.
console.log(`${x}/${y} => JS = ${x.n / y.n};\tBigNumber = ${BigNumber(x.n).div(y.n)};\tSDK = ${r}`);

For sake of comprehension, the snipped above prints

1/3 => JS = 0.33333333333333326;        BigNumber = 0.33333333333333328889;     SDK = 0.33333333333333333333

As expected JS number type math is precise in 16 digits included the integer 0 and the following fifteen 3, then it diverges.

The bignumber.js math returns a 20 fractional digits ratio more precise than JS math but approximate for the last 4 digits. It's a good practice using bignumber.js to set a decimal precision, calling the same .dp(decimalPlaces: bigint | number) method in common with the FixedPointNumber class, to a number of digits greater than the minimal needed, then discard the last ones where precision diverge. For example, needing at least 20 decimal precision, set .dp(25) and discard the last five digits.

The SDK math returns the value closer to the theoretical real number ratio.

Let's see what happen when we increase the precision to 80 fractional digits.

// STEP 6: increase the precision to 80 decimal digits.
let fd = 80;            // Fractional Digits.
x = x.dp(80);                   // Force x to fd precision, .div function will adapt y automatically
r = x.div(y);                   // Ratio
let a = BigNumber(x.n).dp(fd);  // Force to fd precision.
let b = BigNumber(y.n).dp(fd);  // Force to fd precision
let q = a.div(b)                // q for 'quotient' synonymous of 'ratio'.
console.log(`${x}/${y} => BigNumber ${q};\t SDK = ${r}`);

The code prints

1/3 => BigNumber 0.33333333333333328889;         SDK = 0.33333333333333333333333333333333333333333333333333333333333333333333333333333333

because bignumber.js approximation algorithm converge to 0.33333333333333328889 followed by not meaningful zeros. The FixedPointNumber is more accurate regarding divisions.

Square root challenge

The next example computes the square root of few natural numbers, let's see how JS, bignumber.js and FixedPointNumber class behave.

// STEP 7: compute the squared root of the natural number from 0 to n.
fd = 20;
let n = 8
let rows = [];
for (let i = 0; i <= n; i++) {
    x = FixedPointNumber.of(i).dp(fd).sqrt();
    a = BigNumber(i).dp(fd).sqrt();
    rows.push({
        'JS number': Math.sqrt(i),
        'BigNumber': `${a}`,
        'SDK FixedPointNumber': `${x}`
    })
}
console.table(rows);

The code prints the table

┌─────────┬────────────────────┬──────────────────────────┬──────────────────────────┐
│ (index) │ JS number          │ BigNumber                │ SDK FixedPointNumber     │
├─────────┼────────────────────┼──────────────────────────┼──────────────────────────┤
│ 0       │ 0                  │ '0'                      │ '0'                      │
│ 1       │ 1                  │ '1'                      │ '1'                      │
│ 2       │ 1.4142135623730951 │ '1.4142135623730950488'  │ '1.4142135623730950488'  │
│ 3       │ 1.7320508075688772 │ '1.73205080756887729353' │ '1.73205080756887729352' │
│ 4       │ 2                  │ '2'                      │ '2'                      │
│ 5       │ 2.23606797749979   │ '2.23606797749978969641' │ '2.2360679774997896964'  │
│ 6       │ 2.449489742783178  │ '2.4494897427831780982'  │ '2.44948974278317809819' │
│ 7       │ 2.6457513110645907 │ '2.6457513110645905905'  │ '2.6457513110645905905'  │
│ 8       │ 2.8284271247461903 │ '2.8284271247461900976'  │ '2.8284271247461900976'  │
└─────────┴────────────────────┴──────────────────────────┴──────────────────────────┘

where the divergence between bignumber.js and the FixedPointNumber is limited to the last less significant digit.

Once more, is worth to repeat there is not an always ideal way to approximate real numbers to the binary representation computers provide, it's worth to repeat the previous experiment forcing the precision to 80 fractional digits, once more bignumber.js converges to a result having less digits, FixedPointNumber math keep computing until the 80 th digit, internally resolving the computation with 160 digits.

The code

// STEP 8: compute the squared root of the natural number from 0 to n.
fd = 80;
n = 8
rows = [];
for (let i = 0; i <= n; i++) {
    x = FixedPointNumber.of(i).dp(fd).sqrt();
    a = BigNumber(i).dp(fd).sqrt();
    rows.push({
        'JS number': Math.sqrt(i),
        'BigNumber': `${a}`,
        'SDK FixedPointNumber': `${x}`
    })
}
console.table(rows);

prints

┌─────────┬────────────────────┬──────────────────────────┬──────────────────────────────────────────────────────────────────────────────────────┐
│ (index) │ JS number          │ BigNumber                │ SDK FixedPointNumber                                                                 │
├─────────┼────────────────────┼──────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────┤
│ 0       │ 0                  │ '0'                      │ '0'                                                                                  │
│ 1       │ 1                  │ '1'                      │ '1'                                                                                  │
│ 2       │ 1.4142135623730951 │ '1.4142135623730950488'  │ '1.41421356237309504880168872420969807856967187537694807317667973799073247846210703' │
│ 3       │ 1.7320508075688772 │ '1.73205080756887729353' │ '1.73205080756887729352744634150587236694280525381038062805580697945193301690880003' │
│ 4       │ 2                  │ '2'                      │ '2'                                                                                  │
│ 5       │ 2.23606797749979   │ '2.23606797749978969641' │ '2.23606797749978969640917366873127623544061835961152572427089724541052092563780489' │
│ 6       │ 2.449489742783178  │ '2.4494897427831780982'  │ '2.44948974278317809819728407470589139196594748065667012843269256725096037745731502' │
│ 7       │ 2.6457513110645907 │ '2.6457513110645905905'  │ '2.64575131106459059050161575363926042571025918308245018036833445920106882323028362' │
│ 8       │ 2.8284271247461903 │ '2.8284271247461900976'  │ '2.82842712474619009760337744841939615713934375075389614635335947598146495692421407' │
└─────────┴────────────────────┴──────────────────────────┴──────────────────────────────────────────────────────────────────────────────────────┘

Note if you manually round the long SDK results to the digits printed in the bignumber.js result, you will get the bignumber.js results. The two libraries approximate the theoretical real number results consistently but FixedPointNumber works with higher precision when instructed to do because it was designed to respect at least the 20 digits of fractional precision cryptocurrency math requires.

Limit challenge

The FixedPointNumber class works as the JS number does to approximate the results at the limits. The following examples computes the most classic limits of the division, by zero, by infinity and between zero arguments.

// START-SNIPPET: FinancialMath_5
// STEP 9: compute the divisions by zero, infinity and between zeros.
r = x.div(FixedPointNumber.ZERO);
console.log(`${x}/0 = ${r}`);
r = x.div(FixedPointNumber.of(Infinity));
console.log(`${x}/Infinity = ${r}`);
r = x.div(FixedPointNumber.ZERO.div(FixedPointNumber.ZERO));
console.log(`0/0 = ${r}`);

Whatever is the last value of x, the code prints

2.82842712474619009760337744841939615713934375075389614635335947598146495692421407/0 = Infinity
2.82842712474619009760337744841939615713934375075389614635335947598146495692421407/Infinity = 0
0/0 = NaN

as expected.

Compound Interest challenge

The Compount Interest is the result of reinvesting or retaining interest that would otherwise be paid out from a capital, or of the accumulation of debts from a borrower.

From the referred literature the formula to compute the

  • A: accrued amount, from the
  • P := principal capital, at the
  • r := nominal annual interest rate, when accrued
  • n := times per
  • t := number of years in decimal form, hence 6 month = 0.5

is the following expression.

$$A=P(1+{r \over n})^{nt}$$

The following snipped shows how to implement the above formula with the FixedPointNumber class for the simpler case of 10,000 currency units invested at 15% of interest rate annually.

// STEP 10: compute the simple interest reat for 10,000 currency unit at 15% for one year
let P = 10000; // 10,000 $
let R = 0.15; // 15% interest rate
let N = 1; // interest accrued times per year
let T = 1; // 1 year of investment time
let jsA = interestWithNumberType(P, R, N, T);
console.log(
    `JS number            => ${P} at ${R} accrued ${N} per year for ${T} years = ${jsA} `
);
let bnA = interestWithBigNumberType(P, R, N, T);
console.log(
    `BigNumber            => ${P} at ${R} accrued ${N} per year for ${T} years = ${bnA} `
);
let fpA = interestWithFixedPointNumberType(P, R, N, T);
console.log(
    `SDK FixedPointNumber => ${P} at ${R} accrued ${N} per year for ${T} years = ${fpA} `
);

The above code prints

JS number            => 10000 at 0.15 accrued 1 per year for 1 years = 11500
BigNumber            => 10000 at 0.15 accrued 1 per year for 1 years = 11500
SDK FixedPointNumber => 10000 at 0.15 accrued 1 per year for 1 years = 11500

where the math is simple.

It becomes interesting to understand if we could have accrued in the capital the interests once per day, the code becomes

P = 10000; // 10,000 $
R = 0.15; // 15% interest rate
N = 365; // interest accrued times per day
T = 1; // 1 year of investment time
jsA = interestWithNumberType(P, R, N, T);
console.log(
    `JS number            => ${P} at ${R} accrued ${N} per year for ${T} years = ${jsA} `
);
bnA = interestWithBigNumberType(P, R, N, T);
console.log(
    `BigNumber            => ${P} at ${R} accrued ${N} per year for ${T} years = ${bnA} `
);
fpA = interestWithFixedPointNumberType(P, R, N, T);
console.log(
    `SDK FixedPointNumber => ${P} at ${R} accrued ${N} per year for ${T} years = ${fpA} `
);

that prints the following output.

JS number            => 10000 at 0.15 accrued 365 per year for 1 years = 11617.984431282737
BigNumber            => 10000 at 0.15 accrued 365 per year for 1 years = 11617.984431282297580741332627901777130315693092582736415183243388318237491778342908385720809654240040970598789674377765947470867911564401090655498297277650975577399724748298427632216300469085882205126498872953842419903091295485650124501686949728680155745247474335138232482179528909255366536402392875329941467045245858869644197239128719066296683081367462543205390000804087922075544539860855742520269818193809495203557007634259996219745309932882459518798524968113901703633929888998635141912068168058452472183416934462187935057393732956042640561423418567082114578032214061928405512072912635929995221365892079381592086022940837672235708964650239008816227116838590877049591322964899152767454901676441240192497489253888772433181792034083619461274791847315352488935494000067256440171197925364612917886966149610377974675067875969329030185742031433985309342124192478167223066139006719604689359394196296969241000167807707526911942624844129728153376077127099662124501925926262400807908100667459068640705163071829628876166416144650604075464967358748105805082716622225378985664759817456721566584649980686571966739424426900555857787691088510512370790518609850265247504195307410003670920350652938085871229040577564845370642100134126774852650664481607739716900248270176440329598644103824418655594316501156008480980716682782242614278880139844472436437042938201287303745900970102452173445939781488072924165863076960481844351048526193390029595200330434638261284649942836277464922095624061067458197847435464948760326222404388856873196780809435008547776084396323890431631972013758894777140309741935129390297015768713582643582348068171065563802650256565960065201047582581116691865279018481443083634133404456871734224588349932851240939902278852857772240874090103073366139194999398085913528002041787949791674995548809236904350253066698952181458954491521587600763302070101055709667346467178274981943340531021522745779841173173100876264702725095018516677824199733818142804282300961031747257156885200868283262162552850804621026592584042666050361511605692823276578862937221646249263387226746019107252022914312737706377194312459934216793950004099882178225338416417778346328972245140417878899256520398132742852152375062744008410226501238905946472866254396245283539337213331252101745238242758925097416813000735388071657272215630320845974338897771656555041763973550598236914984192269963986648377816228129380629038592368286598983134185222188166799690901539746460579787203029749770287168278013060365079064355083674125148509306763746692482514248137552297251646678328412875814259875291593503972546227382158958840236846540788238035029805960355494051129276122800357236219110138260911217725961189217381637291618712318040507760126364551859509154428340741101957780412277285523094815506659402101296679158236819278380864803586590895640024979637785001490636069382753310067262080662008374551411237242197309339195940406115487850125037507130818294504053083257596428408392560703205549675447339493801368201043030474759039019262194914203062392087296343396390456485525267865121108385382592954860634880962986192817267439352666847666157489902571135514525578918745486509804822731475416985234489977625440493130772022775991189276290027329665587185531530543206454536574667659589367011127224587509531212717181754469270666867381661716929585894830103402709693499856666380325736846063373961108804197128223849684167133844214462156301412887963361227739403071660047619772673714272672989566030756704912026202806904414756141050877173820467126821863214589542154214507220674770172074480518122963953827143784132131825710319355839177131961519962164573936360504510921943071620721478225732241696092470545416711449091231197409947570800111816988728323993927748087694375103322002551786532908225959918071812885153862720547036040176234585041356183336408463671774717112402126019277392052386242595710299970836973058795919985261217215532463448040471027097475516349207504695405799630225500316467665530686280496720382451548335866755027079968901476069244547605637114883551093053617935204887355249377970202741626315706124691277605917831773229234758966829205297909455982503322015748307649778207164102031977513233675014015393778284541528252684639556356440622270627351404365171320231057321728478194017365783885424667313087866411074216194345063051555381054064172603323211738852864410237985108247729834346660957553062456473858762927866393100596426722935498446318146888420749640452411934666892103469174673382975989047523845717274521847403271371069986619972541545134253842186788375129293688126572862115387719324965105989737112901937915991076889355410549661930187265953467401303864531898460980266601723832501816138179044919871086722233148280249031510561730844340942342552752797339969515184261652305893177119221594180785248835404646836951992084403389209903064614958108955775017427938729861471896432889143754999514215866581787875780518195492324090030209238689898749847097547332201970042332361179187882205382279489123276038554324542376299699431748417940616697963498398453267693487839206000992468170890787809074465833833196463153840816879434824909726841353267661992302221702086547490035887290185256786019556867916403856180613548209407427428077370808423244926090002052378929146269431360111728923038525642285848635701334548272961940575527578214830306877087295496043094370029968461721725931122906959895776924708699238003938445300122852634182483059734712134447129574528252224219402082590956703689919029030659233330544907648740772169481354518754802501356274798096471385614129581771624648999966708748460996049922163688691654080111831302649842118325696763454547261669119872910738319187020169833406880898955868935770271402602941664640045686162116748081874962518451448492409874925161119248814895310170073897053291015297428406688653811585699741373977572356288036024901500826634896249641517953519216217122274853273198117378197515066390088275104495779954424594351352378367504170370879233091766599335920101029085831254897829565278260692483037266835050591587628531733520004295815824586141378604375739933891004253354434850469067291902679358555199320950619712947450075798438078077536095433962891948700437561843125672228886857921426124413819322261040245713801881593557146574769577389858802213472120864318927179303450317314525085067389652207907199919225422117470334759242721216889842249001301391538464988190653091063509157979978283643327678457770510881671899296170914332781679190799498034101397504720140904971144170292408274384079420894326525665371636278322421273278426258321203221354484372505412649801255887345653780015097250209794039905314799983216469415302328198528313913445705434316109137813061851157444468570986979549798365340231124713689691546533719643932708191712713553794071778556027123472573813780229986257007757249006153795993810131914431558015307058287798711665470068967296921977705979849136618985820319515622500964455777206091165624522977628174471858620757943222409585380288039284091830621551632490845848868973849571519283631765833497270062040941078383405528284681354175222701039860699557935839377689676090845746980725178910930934668502448387038652955220237816656950225719626759719896877157902206341245898584211319540483952858211899601220699880254374817804694710554521371836044904856003205531183610678081768092129075647044919307768538250858692323989632797219472711805573349208751209872914132180689485824
SDK FixedPointNumber => 10000 at 0.15 accrued 365 per year for 1 years = 11617.9844312822975648

The output shows, if the approximation is acceptable, bignumber.js and FixedPointNumber class results are closer, but FixedPointNumber converge to a solution in the fraction digits, here 20 by default.

From above, it is evident how much the compound interest formula is convenient for the lender (usually the bank) and not for the borrower.

The two snippets above uses the following functions.

// START-SNIPPET: FinancialMath_Functions
// COMPOUND INTEREST FUNCTIONS FOR DIFFERENT DATA TYPES
function interestWithBigNumberType(
    P: number,
    r: number,
    n: number,
    t: number
): BigNumber {
    const _P = BigNumber(P);
    const _r = BigNumber(r);
    const _n = BigNumber(n);
    const _t = BigNumber(t);
    return BigNumber(1).plus(_r.div(n)).pow(_t.times(_n)).times(_P);
}

function interestWithFixedPointNumberType(
    P: number,
    r: number,
    n: number,
    t: number
): FixedPointNumber {
    const _P = FixedPointNumber.of(P);
    const _r = FixedPointNumber.of(r);
    const _n = FixedPointNumber.of(n);
    const _t = FixedPointNumber.of(t);
    return FixedPointNumber.ONE.plus(_r.div(_n))
        .pow(_t.times(_n))
        .times(_P);
}

function interestWithNumberType(
    P: number,
    r: number,
    n: number,
    t: number
): number {
    return (1 + r / n) ** (t * n) * P;
}

where you can play to adjust the precision in the interestWithFixedPointNumberType and interestWithBigNumberType functions, poking with the .dp method.

You can verify math using a compound interest calculator online.