Skip to content

Commit

Permalink
feat: add new field capacityPerPool to capacity pool API (#139)
Browse files Browse the repository at this point in the history
  • Loading branch information
rackstar authored Nov 21, 2024
2 parents 5cc5933 + 92c29d4 commit bff74ff
Show file tree
Hide file tree
Showing 6 changed files with 831 additions and 150 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ best available combination of pools for the premium.
- **URL**: `/v2/capacity/{productId}`
- **Method**: `GET`
- **OpenAPI**: [v2/api/docs/#/Capacity/get_v2_capacity__productId_](https://api.nexusmutual.io/v2/api/docs/#/Capacity/get_v2_capacity__productId_)
- **Description**: Returns the current capacity for a specific product for a period of 30 days if no period query param is specified.
- **Description**: Returns the current capacity for a specific product for a period of 30 days if no period query param is specified. Additionally, if the query parameter `withPools=true` is provided, the response will include the `capacityPerPool` field with detailed capacity information per pool.

### Capacity Route for all products in a pool
- **URL**: `/v2/capacity/pools/{poolId}`
Expand Down
256 changes: 153 additions & 103 deletions src/lib/capacityEngine.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const { ethers, BigNumber } = require('ethers');
const { NXM_PER_ALLOCATION_UNIT, MAX_COVER_PERIOD } = require('./constants');
const { bnMax, bnMin, calculateTrancheId } = require('./helpers');
const { calculateBasePrice, calculatePremiumPerYear, calculateFixedPricePremiumPerYear } = require('./quoteEngine');
const { selectAsset, selectProduct, selectProductPools } = require('../store/selectors');
const { selectProduct, selectProductPools } = require('../store/selectors');

const { WeiPerEther, Zero } = ethers.constants;

Expand Down Expand Up @@ -31,78 +31,116 @@ function getUtilizationRate(capacityAvailableNXM, capacityUsedNXM) {
return capacityUsedNXM.mul(BASIS_POINTS).div(totalCapacity);
}

/**
* Calculates available capacity for a pool.
*
* @param {Array<BigNumber>} trancheCapacities - Array of capacity BigNumbers.
* @param {Array<BigNumber>} allocations - Array of allocation BigNumbers.
* @param {number} firstUsableTrancheIndex - Index of the first usable tranche.
* @returns {BigNumber} The available capacity as a BigNumber.
*/
function calculateAvailableCapacity(trancheCapacities, allocations, firstUsableTrancheIndex) {
const unused = trancheCapacities.reduce((available, capacity, index) => {
const allocationDifference = capacity.sub(allocations[index]);
const allocationToAdd =
index < firstUsableTrancheIndex
? bnMin(allocationDifference, Zero) // only carry over the negative
: allocationDifference;
return available.add(allocationToAdd);
}, Zero);
return bnMax(unused, Zero);
}

/**
* Calculates capacity and pricing data for a specific tranche of product pools.
*
* @param {Array<Object>} productPools - Array of product pool objects.
* @param {number} firstUsableTrancheIndex - Index of the first usable tranche.
* @param {boolean} useFixedPrice - Flag indicating whether to use fixed pricing.
* @param {BigNumber} now - Current timestamp in seconds.
* @returns {Object} An object containing capacity used, capacity available, minimum price, and total premium.
* @param {Object} assets - Object containing asset information.
* @param {Object} assetRates - Object containing asset rates.
* @returns {Object} An object containing aggregated data and capacity per pool.
*/
function calculateProductDataForTranche(productPools, firstUsableTrancheIndex, useFixedPrice, now) {
return productPools.reduce(
(accumulated, pool) => {
const { capacityUsedNXM, capacityAvailableNXM, minPrice, totalPremium } = accumulated;
const { allocations, trancheCapacities, targetPrice, bumpedPrice, bumpedPriceUpdateTime } = pool;

// calculating the capacity in allocation points
const used = allocations.reduce((total, allocation) => total.add(allocation), Zero);
const total = trancheCapacities.reduce((total, capacity) => total.add(capacity), Zero);

const unused = trancheCapacities.reduce((available, capacity, index) => {
const allocationDifference = capacity.sub(allocations[index]);
return index < firstUsableTrancheIndex
? available.add(bnMin(allocationDifference, Zero)) // only carry over the negative
: available.add(allocationDifference);
}, Zero);

const availableCapacity = bnMax(unused, Zero);

// convert to nxm
const totalInNXM = total.mul(NXM_PER_ALLOCATION_UNIT);
const usedInNxm = used.mul(NXM_PER_ALLOCATION_UNIT);
const availableInNXM = availableCapacity.mul(NXM_PER_ALLOCATION_UNIT);

if (availableCapacity.isZero()) {
// only add up the used capacity and return the same values for the rest
return {
capacityUsedNXM: usedInNxm.add(capacityUsedNXM),
capacityAvailableNXM,
minPrice,
totalPremium,
};
}
function calculateProductDataForTranche(productPools, firstUsableTrancheIndex, useFixedPrice, now, assets, assetRates) {
const aggregatedData = {
capacityUsedNXM: Zero,
capacityAvailableNXM: Zero,
minPrice: Zero,
totalPremium: Zero,
};

const capacityPerPool = productPools.map(pool => {
const { allocations, trancheCapacities, targetPrice, bumpedPrice, bumpedPriceUpdateTime, poolId } = pool;

const basePrice = useFixedPrice
? targetPrice
: calculateBasePrice(targetPrice, bumpedPrice, bumpedPriceUpdateTime, now);
// calculating the capacity in allocation points
const used = allocations.reduce((total, allocation) => total.add(allocation), Zero);
const total = trancheCapacities.reduce((total, capacity) => total.add(capacity), Zero);

// the minimum price depends on the surge
// so we buy the smallest possible unit of capacity
// and calculate the premium per year
const unitPremium = useFixedPrice
? calculateFixedPricePremiumPerYear(NXM_PER_ALLOCATION_UNIT, basePrice)
: calculatePremiumPerYear(NXM_PER_ALLOCATION_UNIT, basePrice, usedInNxm, totalInNXM);
const availableCapacity = calculateAvailableCapacity(trancheCapacities, allocations, firstUsableTrancheIndex);

const poolMinPrice = WeiPerEther.mul(unitPremium).div(NXM_PER_ALLOCATION_UNIT);
// convert to nxm
const totalInNXM = total.mul(NXM_PER_ALLOCATION_UNIT);
const usedInNXM = used.mul(NXM_PER_ALLOCATION_UNIT);
const availableInNXM = availableCapacity.mul(NXM_PER_ALLOCATION_UNIT);

// the maximum price a user would get can only be determined if the entire available
// capacity is bought because the routing will always pick the cheapest
// so we're summing up the premium for all pools and then calculate the average at the end
const poolPremium = useFixedPrice
? calculateFixedPricePremiumPerYear(availableInNXM, basePrice)
: calculatePremiumPerYear(availableInNXM, basePrice, usedInNxm, totalInNXM);
aggregatedData.capacityUsedNXM = aggregatedData.capacityUsedNXM.add(usedInNXM);
aggregatedData.capacityAvailableNXM = aggregatedData.capacityAvailableNXM.add(availableInNXM);

if (availableCapacity.isZero()) {
return {
capacityUsedNXM: usedInNxm.add(capacityUsedNXM),
capacityAvailableNXM: availableInNXM.add(capacityAvailableNXM),
minPrice: minPrice.eq(Zero) ? poolMinPrice : bnMin(minPrice, poolMinPrice),
totalPremium: totalPremium.add(poolPremium),
poolId,
availableCapacity: [],
allocatedNxm: usedInNXM.toString(),
minAnnualPrice: Zero,
maxAnnualPrice: Zero,
};
},
{ capacityUsedNXM: Zero, capacityAvailableNXM: Zero, minPrice: Zero, totalPremium: Zero },
);
}

const basePrice = useFixedPrice
? targetPrice
: calculateBasePrice(targetPrice, bumpedPrice, bumpedPriceUpdateTime, now);

// the minimum price depends on the surge
// so we buy the smallest possible unit of capacity
// and calculate the premium per year
const unitPremium = useFixedPrice
? calculateFixedPricePremiumPerYear(NXM_PER_ALLOCATION_UNIT, basePrice)
: calculatePremiumPerYear(NXM_PER_ALLOCATION_UNIT, basePrice, usedInNXM, totalInNXM);

const poolMinPrice = WeiPerEther.mul(unitPremium).div(NXM_PER_ALLOCATION_UNIT);

// the maximum price a user would get can only be determined if the entire available
// capacity is bought because the routing will always pick the cheapest
// so we're summing up the premium for all pools and then calculate the average at the end
const poolPremium = useFixedPrice
? calculateFixedPricePremiumPerYear(availableInNXM, basePrice)
: calculatePremiumPerYear(availableInNXM, basePrice, usedInNXM, totalInNXM);

const poolMaxPrice = availableInNXM.isZero() ? Zero : WeiPerEther.mul(poolPremium).div(availableInNXM);

if (aggregatedData.minPrice.isZero() || poolMinPrice.lt(aggregatedData.minPrice)) {
aggregatedData.minPrice = poolMinPrice;
}
aggregatedData.totalPremium = aggregatedData.totalPremium.add(poolPremium);

// The available capacity of a product for a particular pool
const availableCapacityInAssets = Object.keys(assets).map(assetId => ({
assetId: Number(assetId),
amount: availableInNXM.mul(assetRates[assetId]).div(WeiPerEther),
asset: assets[assetId],
}));

return {
poolId,
availableCapacity: availableCapacityInAssets,
allocatedNxm: usedInNXM,
minAnnualPrice: poolMinPrice,
maxAnnualPrice: poolMaxPrice,
};
});

return { aggregatedData, capacityPerPool };
}

/**
Expand Down Expand Up @@ -150,9 +188,10 @@ function calculateTrancheInfo(time, product, period) {
* @param {number|null} [options.poolId=null] - The ID of the pool to filter products by.
* @param {Array<number>} [options.productIds=[]] - Array of product IDs to process.
* @param {number} [options.period=30] - The coverage period in days.
* @param {boolean} [options.withPools=false] - Flag indicating whether to include capacityPerPool data field.
* @returns {Array<Object>} An array of capacity information objects for each product.
*/
function capacityEngine(store, { poolId = null, productIds = [], period = 30 } = {}) {
function capacityEngine(store, { poolId = null, productIds = [], period = 30, withPools = false } = {}) {
const { assets, assetRates, products } = store.getState();
const now = BigNumber.from(Date.now()).div(1000);
const capacities = [];
Expand All @@ -176,76 +215,87 @@ function capacityEngine(store, { poolId = null, productIds = [], period = 30 } =
}

const { firstUsableTrancheIndex, firstUsableTrancheForMaxPeriodIndex } = calculateTrancheInfo(now, product, period);

// Use productPools from poolId if available; otherwise, select all pools for productId
const productPools = selectProductPools(store, productId, poolId);

let aggregatedData = {};
let capacityPerPool = [];
let maxAnnualPrice = Zero;

if (product.useFixedPrice) {
// Fixed Price
const productData = calculateProductDataForTranche(productPools, firstUsableTrancheIndex, true, now);

const { capacityAvailableNXM, capacityUsedNXM, minPrice, totalPremium } = productData;

const maxAnnualPrice = capacityAvailableNXM.isZero()
? Zero
: WeiPerEther.mul(totalPremium).div(capacityAvailableNXM);

const capacityInAssets = Object.keys(assets).map(assetId => ({
assetId: Number(assetId),
amount: capacityAvailableNXM.mul(assetRates[assetId]).div(WeiPerEther),
asset: selectAsset(store, assetId),
}));

capacities.push({
productId: Number(productId),
availableCapacity: capacityInAssets,
usedCapacity: capacityUsedNXM,
utilizationRate: getUtilizationRate(capacityAvailableNXM, capacityUsedNXM),
minAnnualPrice: minPrice,
maxAnnualPrice,
});
({ aggregatedData, capacityPerPool } = calculateProductDataForTranche(
productPools,
firstUsableTrancheIndex,
true,
now,
assets,
assetRates,
));

const { capacityAvailableNXM, totalPremium } = aggregatedData;
maxAnnualPrice = capacityAvailableNXM.isZero() ? Zero : WeiPerEther.mul(totalPremium).div(capacityAvailableNXM);
} else {
// Non-fixed Price
let productData = {};
let maxAnnualPrice = BigNumber.from(0);

// use the first 6 tranches (over 1 year) for calculating the max annual price
for (let i = 0; i <= firstUsableTrancheForMaxPeriodIndex; i++) {
const productTrancheData = calculateProductDataForTranche(productPools, i, false, now);
const { aggregatedData: trancheData, capacityPerPool: trancheCapacityPerPool } = calculateProductDataForTranche(
productPools,
i,
false,
now,
assets,
assetRates,
);

if (i === firstUsableTrancheIndex) {
productData = productTrancheData;
aggregatedData = trancheData;
capacityPerPool = trancheCapacityPerPool;
}

const { capacityAvailableNXM, totalPremium } = productTrancheData;
const { capacityAvailableNXM, totalPremium } = trancheData;

const maxTrancheAnnualPrice = capacityAvailableNXM.isZero()
? Zero
: WeiPerEther.mul(totalPremium).div(capacityAvailableNXM);

maxAnnualPrice = bnMax(maxAnnualPrice, maxTrancheAnnualPrice);
}
}

const { capacityAvailableNXM, capacityUsedNXM, minPrice } = productData;
const capacityInAssets = Object.keys(assets).map(assetId => ({
assetId: Number(assetId),
amount: capacityAvailableNXM.mul(assetRates[assetId]).div(WeiPerEther),
asset: selectAsset(store, assetId),
}));

capacities.push({
productId: Number(productId),
availableCapacity: capacityInAssets,
usedCapacity: capacityUsedNXM,
utilizationRate: getUtilizationRate(capacityAvailableNXM, capacityUsedNXM),
minAnnualPrice: minPrice,
maxAnnualPrice,
});
const { capacityAvailableNXM, capacityUsedNXM, minPrice } = aggregatedData;
// The available capacity of a product across all pools
const capacityInAssets = Object.keys(assets).map(assetId => ({
assetId: Number(assetId),
amount: capacityAvailableNXM.mul(assetRates[assetId]).div(WeiPerEther),
asset: assets[assetId],
}));

const capacityData = {
productId: Number(productId),
availableCapacity: capacityInAssets,
usedCapacity: capacityUsedNXM,
utilizationRate: getUtilizationRate(capacityAvailableNXM, capacityUsedNXM),
minAnnualPrice: minPrice,
maxAnnualPrice,
};

if (withPools) {
capacityData.capacityPerPool = capacityPerPool;
}

capacities.push(capacityData);
}

return capacities;
}

module.exports = {
capacityEngine,
getUtilizationRate,
calculateAvailableCapacity,
calculateProductDataForTranche,
getProductsInPool,
calculateTrancheInfo,
capacityEngine,
};
Loading

0 comments on commit bff74ff

Please sign in to comment.