Skip to content

Instantly share code, notes, and snippets.

@0xpatrickdev
Created June 21, 2023 17:26
Show Gist options
  • Save 0xpatrickdev/dbefd7b6829f067449810711d062a2d7 to your computer and use it in GitHub Desktop.
Save 0xpatrickdev/dbefd7b6829f067449810711d062a2d7 to your computer and use it in GitHub Desktop.
interest math agoric
// @ts-check
import {
makeRatio,
multiplyBy,
multiplyRatios
} from '@agoric/zoe/src/contractSupport/ratio.js';
import '@agoric/ertp/src/types-ambient.js';
export const MS_IN_A_YEAR = 31_557_600_000n;
const BPS_IN_DECIMAL = 10_000n;
/**
* @param {bigint} decimals
* @example
* // returns 10_000n
* scale(4n)
* @returns {bigint}
*/
export const scale = decimals => 10n ** decimals;
/**
* Simple interest - principal * rate (annual) * time (annual)
* @param {Amount<"nat">} principal
* @param {bigint} rateBps annual interest rate in basis points
* @param {bigint} timeMs time elapsed in ms
* @param {Brand} brand
* @returns {Amount<"nat">}
*/
export const calculateSimpleInterest = (principal, rateBps, timeMs, brand) => {
// convert bps to decimal ratio
const rate = makeRatio(rateBps, brand, BPS_IN_DECIMAL);
// convert msElapsed to yearly ratio
const time = makeRatio(timeMs, brand, MS_IN_A_YEAR);
// principal * rate * time
return multiplyBy(principal, multiplyRatios(rate, time));
};
/**
* Continuous interest - principal * e ^ (rate * time)
* @param {Amount<"nat">} principal
* @param {bigint} rateBps annual interest rate in basis points
* @param {bigint} timeMs time elapsed in ms
* @param {Brand} brand
* @param {bigint} decimals the number of precision decimals. default is 6n
* @returns {Amount<"nat">}
*/
export const calculateContinuousInterest = (
principal,
rateBps,
timeMs,
brand,
decimals = 6n
) => {
const time = Number(timeMs) / Number(MS_IN_A_YEAR);
const rate = Number(rateBps) / Number(BPS_IN_DECIMAL);
const cumulativeRate = Math.exp(time * rate) - 1;
const denominator = scale(decimals);
const scaledRate = BigInt(Math.round(cumulativeRate * Number(denominator)));
return multiplyBy(principal, makeRatio(scaledRate, brand, denominator));
};
/**
* Returns Euler's Constant, e, as a Ratio (≈2.7182)
*
* @param {Brand} brand the ERTP brand we should use for the ratio
* @param {bigint} decimals the number of precision decimals. default is 4n
* @returns {Ratio}
*/
export const makeEulerConstantRatio = (brand, decimals = 4n) => {
const denominator = scale(decimals);
const eulerConstant = BigInt(Math.round(Math.E * Number(denominator)));
return makeRatio(eulerConstant, brand, denominator);
};
/**
* @todo this does not work. It does a * b instead of a ** b. Can this be built
* in contractSupport/ratio.js?
* @param {Ratio} base
* @param {Ratio} exponent
* @returns {Ratio}
*/
const exponentiateRatios = (base, exponent) => multiplyRatios(base, exponent);
/**
* Continuous interest - principal * e ^ (rate * time)
* @todo doesn't work without support for exponents/logarithms in contractSupport/ratio.js
* @param {Amount<"nat">} principal
* @param {bigint} rateBps annual interest rate in basis points
* @param {bigint} timeMs time elapsed in ms
* @param {Brand} brand
* @param {bigint} decimals the number of precision decimals. default is 6n
* @returns {Amount<"nat">}
*/
export const calculateContinuousInterestWithRatios = (
principal,
rateBps,
timeMs,
brand,
decimals = 6n
) => {
// convert bps to decimal ratio
const rate = makeRatio(rateBps, brand, 10_000n);
// convert msElapsed to yearly ratio
const time = makeRatio(timeMs, brand, MS_IN_A_YEAR);
// 2.7182 as a ratio
const eulerConstantRatio = makeEulerConstantRatio(brand, decimals);
return multiplyBy(
principal,
exponentiateRatios(eulerConstantRatio, multiplyRatios(rate, time))
);
};
// @ts-check
import { test } from '../prepare-test-env-ava.js';
import { AmountMath, makeIssuerKit } from '@agoric/ertp';
import {
calculateSimpleInterest,
calculateContinuousInterest,
makeEulerConstantRatio,
scale,
MS_IN_A_YEAR
} from './interest.js';
const { brand } = makeIssuerKit('foo');
test('utils/interest scale', t => {
t.is(scale(1n), 10n);
t.is(scale(2n), 100n);
t.is(scale(3n), 1_000n);
t.is(scale(4n), 10_000n);
t.is(scale(9n), 1_000_000_000n);
t.is(Number(scale(3n)), 1000);
});
test('utils/interest calculateSimpleInterest', t => {
const principal = AmountMath.make(brand, 1_000n); // $1,000
const rateBps = 400n; // 4%
const timeMs = MS_IN_A_YEAR / 2n; // 6 months
const interest = calculateSimpleInterest(principal, rateBps, timeMs, brand);
t.is(interest.value, 20n);
t.is(
calculateSimpleInterest(
AmountMath.make(brand, 10_000n),
400n,
MS_IN_A_YEAR / 2n,
brand
).value,
200n
);
t.is(
calculateSimpleInterest(
AmountMath.make(brand, 500_000n),
50n,
MS_IN_A_YEAR / 4n,
brand
).value,
625n
);
});
test('utils/interest calculateContinuousInterest', t => {
const principal = AmountMath.make(brand, 1_000n); // $1,000
const rateBps = 400n; // 4%
const timeMs = MS_IN_A_YEAR / 2n; // 6 months
const interest = calculateContinuousInterest(
principal,
rateBps,
timeMs,
brand
);
t.is(interest.value, 20n);
t.is(
calculateContinuousInterest(
AmountMath.make(brand, 10_000n),
400n,
MS_IN_A_YEAR / 2n,
brand
).value,
202n
);
t.is(
calculateContinuousInterest(
AmountMath.make(brand, 500_000n),
50n,
MS_IN_A_YEAR / 4n,
brand
).value,
626n
);
t.is(
calculateContinuousInterest(
AmountMath.make(brand, 1_000_000n),
50n,
MS_IN_A_YEAR / 4n,
brand
).value,
1251n,
'default precision of 6'
);
t.is(
calculateContinuousInterest(
AmountMath.make(brand, 1_000_000n),
50n,
MS_IN_A_YEAR / 4n,
brand,
4n
).value,
1300n,
'precision of only 4 decimals'
);
t.is(
calculateContinuousInterest(
AmountMath.make(brand, 1_000_000n),
50n,
MS_IN_A_YEAR / 4n,
brand,
8n
).value,
1251n,
'custom precision of 8 decimals'
);
});
test('utils/interest makeEulerConstantRatio, base scale', t => {
const eulerConstantRatio = makeEulerConstantRatio(brand);
t.is(eulerConstantRatio.numerator.value, 27_183n);
t.is(eulerConstantRatio.denominator.value, 10_000n);
const eulerConstantRatioAsDecimal =
Number(eulerConstantRatio.numerator.value) /
Number(eulerConstantRatio.denominator.value);
t.is(eulerConstantRatioAsDecimal, 2.7183);
});
test('utils/interest makeEulerConstantRatio, custom scale', t => {
const eulerConstantRatio = makeEulerConstantRatio(brand, 6n);
t.is(eulerConstantRatio.numerator.value, 2_718_282n);
t.is(eulerConstantRatio.denominator.value, 1_000_000n);
const eulerConstantRatioAsDecimal =
Number(eulerConstantRatio.numerator.value) /
Number(eulerConstantRatio.denominator.value);
t.is(eulerConstantRatioAsDecimal, 2.718282);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment