Skip to content

Commit

Permalink
feat(core): add StakePool.epochRewards and estimateStakePoolAPY util
Browse files Browse the repository at this point in the history
  • Loading branch information
mkazlauskas committed Feb 3, 2022
1 parent 5b22148 commit ff69031
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 1 deletion.
25 changes: 24 additions & 1 deletion packages/core/src/Cardano/types/StakePool/StakePool.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Lovelace, TransactionId } from '..';
import { Epoch, Lovelace, TransactionId } from '..';
import { PoolIdHex } from './primitives';
import { PoolParameters } from './PoolParameters';

Expand Down Expand Up @@ -46,6 +46,24 @@ export enum StakePoolStatus {
Retiring = 'retiring'
}

/**
* Stake pool performance per epoch, taken at epoch rollover
*/
export class StakePoolEpochRewards {
/**
* Epoch length in milliseconds
*/
epochLength: number;
epoch: Epoch;
activeStake: Lovelace;
totalRewards: Lovelace;
operatorFees: Lovelace;
/**
* (rewards-operatorFees)/activeStake, not annualized
*/
memberROI: Percent;
}

export interface StakePool extends PoolParameters {
/**
* Stake pool ID as a hex string
Expand All @@ -63,4 +81,9 @@ export interface StakePool extends PoolParameters {
* Transactions provisioning the stake pool
*/
transactions: StakePoolTransactions;
/**
* Stake pool rewards history per epoch.
* Sorted by epoch in ascending order.
*/
epochRewards: StakePoolEpochRewards[];
}
23 changes: 23 additions & 0 deletions packages/core/src/Cardano/util/estimateStakePoolAPY.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Cardano } from '../..';
import { sum } from 'lodash-es';

const MILLISECONDS_PER_DAY = 1000 * 60 * 60 * 24;

/**
* Estimates annualized percentage yield given past stake pool rewards.
* Assumes 365 day year, average historical yield "per time" and epoch length of last rewardsHistory data point.
*
* @param {Cardano.StakePoolEpochRewards[]} rewardsHistory sorted by epoch in ascending order
*/
export const estimateStakePoolAPY = (rewardsHistory: Cardano.StakePoolEpochRewards[]): Cardano.Percent | null => {
if (rewardsHistory.length === 0) return null;
const roisPerDay = rewardsHistory.map(
({ epochLength, memberROI }) => memberROI / (epochLength / MILLISECONDS_PER_DAY)
);
const epochLengthInDays = rewardsHistory[rewardsHistory.length - 1].epochLength / MILLISECONDS_PER_DAY;
const averageDailyROI = sum(roisPerDay) / roisPerDay.length;
const roiPerEpoch = averageDailyROI * epochLengthInDays;
const numEpochs = 365 / epochLengthInDays;
// Compound interest formula
return Math.pow(1 + roiPerEpoch, numEpochs) - 1;
};
1 change: 1 addition & 0 deletions packages/core/src/Cardano/util/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './coalesceValueQuantities';
export * from './computeMinUtxoValue';
export * from './computeImplicitCoin';
export * from './estimateStakePoolAPY';
export * from './primitives';
export * as metadatum from './metadatum';
39 changes: 39 additions & 0 deletions packages/core/test/Cardano/util/estimateStakePoolAPY.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Cardano } from '../../../src';

describe('estimateStakePoolAPY', () => {
const rewards = {
epochLength: 432_000_000,
memberROI: 0.000_68
} as Cardano.StakePoolEpochRewards;

it('provided no history => returns null', () => {
expect(Cardano.util.estimateStakePoolAPY([])).toBe(null);
});

it('provided a single history data point => returns compounded annualized ROI %', () => {
const apy = Cardano.util.estimateStakePoolAPY([rewards]);
const epochLengthInDays = rewards.epochLength / 1000 / 60 / 60 / 24;
expect(apy).toBeGreaterThan(
// % without compounding
(rewards.memberROI * 365) / epochLengthInDays
);
expect(apy).toBeLessThan(0.1);
});

// eslint-disable-next-line max-len
it('provided multiple history data points => returns compounded annualized ROI %, assuming weighted average rewards and epoch length of the last data point', () => {
const worseRewards = {
// computation should assume that this is the epoch length for the year
// since this is the last data point (as per epochNo)
...rewards,
epochLength: rewards.epochLength * 2,
// worse ROI than 'rewards': 2x the epoch length, but only 1.5x ROI
memberROI: rewards.memberROI * 1.5
};
const apy1 = Cardano.util.estimateStakePoolAPY([rewards])!;
const apy2 = Cardano.util.estimateStakePoolAPY([rewards, worseRewards])!;
const apy3 = Cardano.util.estimateStakePoolAPY([worseRewards])!;
expect(apy1).toBeGreaterThan(apy2);
expect(apy2).toBeGreaterThan(apy3);
});
});

0 comments on commit ff69031

Please sign in to comment.