UNPKG

@symmetry-hq/baskets-sdk

Version:

Software Development Kit for interacting with Symmetry Baskets Program

526 lines (495 loc) 17.4 kB
import { Program } from "@coral-xyz/anchor"; import { PublicKey } from "@solana/web3.js"; import { SortBy, Rule, CreateBasketParams, NUM_OF_DAYS_IN_DATABASE, NUM_OF_TOKENS_IN_ASSET_POOL, BPS_DIVIDER, EXPO_DIVIDER, WEIGHT_MULTIPLIER, COMBINED_TOKENS_IN_A_BASKET, } from "./config"; import { BasketsIDL } from "./basketsIDL"; export interface DataPoint { price: number, circulatingSupply: number, volume: number, } export interface BasketState { currentCompToken: number[], currentCompAmount: number[], currentCompWeight: number[], targetWeight: number[], numOfTokens: number, weightSum: number, basketWorth: number, lastRefilterTime: number, lastReweightTime: number, lastRebalanceTime: number, singleRuleAssets: number[][], ruleAssets: number[], ruleWeights: number[], numRuleTokens: number, } export interface TokenStats { stats: Stats[][], } export interface Stats { days: number, performance: number, volume: number, mcap: number, } export async function fetchDatabase( program: Program<BasketsIDL>, database: PublicKey, ) { let data: any = await program.account.database.fetch(database, "confirmed"); let processedData: DataPoint[][] = []; for(let i=0; i<NUM_OF_TOKENS_IN_ASSET_POOL; i++){ let index = data.data[i].index.toNumber(); let currentTokenData: DataPoint[] = []; while(true) { let dataPoint: DataPoint = { price: data.data[i].price[index].toNumber(), circulatingSupply: data.data[i].circulatingSupply[index].toNumber(), volume: data.data[i].volume[index].toNumber(), }; currentTokenData.push(dataPoint); index += 1; if(index == NUM_OF_DAYS_IN_DATABASE) index = 0; if(index == data.data[i].index.toNumber())break; } processedData.push(currentTokenData); } return processedData; } export function defaultBasketState() { let array: number[] = [1000000]; let token: number[] = [0]; let amount: number[] = [100000000]; for(let i=1; i<20; i++){ amount.push(0); array.push(0); token.push(0); } return { currentCompToken: token, currentCompAmount: amount, currentCompWeight: Object.assign([], array), targetWeight: Object.assign([], array), numOfTokens: 1, weightSum: 1000000, basketWorth: 100000000, lastRefilterTime: 0, lastReweightTime: 0, lastRebalanceTime: 0, singleRuleAssets: new Array(), ruleWeights: Object.assign([], array), ruleAssets: Object.assign([], token), numRuleTokens: 1, }; } export function calculateStats( data: DataPoint[], days: number, index: number, ) { let result = [0, 0, 0]; let startingDay = index - days + 1; let firstPrice = 0, lastPrice = 0; if(startingDay < 0) startingDay = 0; for(let i=startingDay; i<=index; i++){ if(data[i].price == 0)continue; if(firstPrice == 0){ firstPrice = data[i].price; } lastPrice = data[i].price; result[1] += data[i].volume; result[2] += data[i].circulatingSupply * data[i].price / 1000000; } result[0] = lastPrice * WEIGHT_MULTIPLIER / firstPrice; return result; } export function updateTokenStats( data: DataPoint[][], index: number, ) { let tokenStats: TokenStats = { stats: [], }; let daysArray = [1, 7, 30, 91, 182, 365]; for(let tokenId=0; tokenId<NUM_OF_TOKENS_IN_ASSET_POOL; tokenId++){ let statsForToken = []; for(let i=0; i<6; i++) { let result = calculateStats( data[tokenId], daysArray[i], index, ); statsForToken.push({ days: daysArray[i], performance: result[0], volume: result[1], mcap: result[2], }); } tokenStats.stats.push(statsForToken); } return tokenStats; } export function selectTokens( createBasketParams: CreateBasketParams, rule: Rule, tokenStats: TokenStats, ) { let ruleTokens = new Array(20); if(rule.filterBy == 0) { ruleTokens[0] = rule.fixedAsset; return ruleTokens; } let allTokens: { value: number, id: number, }[] = []; for(let i=0; i<NUM_OF_TOKENS_IN_ASSET_POOL; i++){ if(createBasketParams.assetPool.indexOf(i) == -1)continue; if(rule.excludeAssets.indexOf(i) != -1)continue; if(tokenStats.stats[i][0].volume == 0)continue; let tokenStatsInfo = tokenStats.stats[i][rule.filterDays]; let x; switch(rule.filterBy){ case 1: x = tokenStatsInfo.mcap; break; case 2: x = tokenStatsInfo.volume; break; default: x = tokenStatsInfo.performance; } allTokens.push({ value: x, id: i, }); } allTokens.sort((a, b) => 0 - (a.value > b.value ? -1 : 1)); for(let i=0; i<rule.numAssets; i++){ switch(rule.sortBy) { case SortBy.AscendingOrder: ruleTokens[i] = allTokens[i].id; break; default: ruleTokens[i] = allTokens[allTokens.length - i - 1].id; } } return ruleTokens; } export function refilter( createBasketParams: CreateBasketParams, tokenStats: TokenStats, basketState: BasketState, ) { basketState.singleRuleAssets = []; for(let i=0; i<createBasketParams.rules.length; i++){ let rule = createBasketParams.rules[i]; let ruleTokens = selectTokens(createBasketParams, rule, tokenStats); basketState.singleRuleAssets.push(ruleTokens); } } export function calculateWeight( rawWeight: number, expo: number, weightBy: number, ): number { let multiplier = 1; if(weightBy == 3)multiplier = WEIGHT_MULTIPLIER; return Math.floor(Math.pow(rawWeight/multiplier , expo / EXPO_DIVIDER) * multiplier); } export function generateWeights( tokenStats: TokenStats, rule: Rule, ruleAssets: any, ) { let ruleWeights = []; for(let i=0; i<rule.numAssets; i++){ let token = ruleAssets[i]; let tokenStatsInfo = tokenStats.stats[token][rule.weightDays]; let rawWeight = 1000; if(rule.filterBy != 0){ switch(rule.weightBy) { case 1: rawWeight = tokenStatsInfo.mcap;break; case 2: rawWeight = tokenStatsInfo.volume;break; default: rawWeight = tokenStatsInfo.performance; } } let assetWeight = calculateWeight( rawWeight, rule.weightExpo, rule.weightBy, ); ruleWeights.push({ token, assetWeight }); } return ruleWeights; } export function combineRules( basketState: BasketState, ruleAssets: any, createBasketParams: CreateBasketParams, ) { basketState.ruleAssets = new Array<number>(20).fill(0); basketState.ruleWeights = new Array<number>(20).fill(0); basketState.numRuleTokens = 1; for(let i=0; i<createBasketParams.rules.length; i++){ let sumOfWeights = 0; for(let j=0; j<ruleAssets[i].length; j++)sumOfWeights += ruleAssets[i][j].assetWeight; for(let j=0; j<ruleAssets[i].length; j++){ let token = ruleAssets[i][j].token; let optionIndex = basketState.ruleAssets.indexOf(token); let index = 0; switch(optionIndex) { case -1: basketState.ruleAssets[basketState.numRuleTokens] = token; basketState.numRuleTokens += 1; index = basketState.numRuleTokens -1; break; default: index = optionIndex; } let x = BigInt(WEIGHT_MULTIPLIER) * BigInt(ruleAssets[i][j].assetWeight) * BigInt(createBasketParams.rules[i].totalWeight) / BigInt(sumOfWeights); let weight = Math.floor(Number(x)); basketState.ruleWeights[index] += weight; } } } export function addTargetAssets( basketState: BasketState ) { let i = 0; while(i < basketState.numRuleTokens){ let optionIndex = basketState.currentCompToken.indexOf(basketState.ruleAssets[i]); if(optionIndex != -1){ basketState.targetWeight[optionIndex] = basketState.ruleWeights[i]; } else { if (basketState.numOfTokens < COMBINED_TOKENS_IN_A_BASKET) { basketState.currentCompAmount[basketState.numOfTokens] = 0; basketState.currentCompToken[basketState.numOfTokens] = basketState.ruleAssets[i]; basketState.targetWeight[basketState.numOfTokens] = basketState.ruleWeights[i]; basketState.numOfTokens += 1; } } i++; } } export function removeTargetAssets( basketState: BasketState ) { let i = 1; while(i < basketState.numOfTokens){ let optionIndex = basketState.ruleAssets.indexOf(basketState.currentCompToken[i]); if(optionIndex == -1){ if(basketState.currentCompAmount[i] == 0){ basketState.numOfTokens -= 1; let lastTokenIndex = basketState.numOfTokens; // we write lastToken data on this index basketState.currentCompAmount[i] = basketState.currentCompAmount[lastTokenIndex]; basketState.currentCompToken[i] = basketState.currentCompToken[lastTokenIndex]; basketState.targetWeight[i] = basketState.targetWeight[lastTokenIndex]; // after we moved data of lastToken on index i, we can make everything 0 on lastTokenIndex basketState.targetWeight[lastTokenIndex] = 0; basketState.currentCompToken[lastTokenIndex] = 0; basketState.currentCompAmount[lastTokenIndex] = 0; // since now we have the lastToken data on this index, we shouldn't // increase i in the while loop. because lastToken wasn't processed yet i = i - 1; } else { basketState.targetWeight[i] = 0; } } i++; } } export function updateTargetAssets( basketState: BasketState ) { removeTargetAssets(basketState); addTargetAssets(basketState); basketState.weightSum = 0; basketState.ruleWeights.forEach((element) => { basketState.weightSum += element; }); } export function reweight( createBasketParams: CreateBasketParams, tokenStats: TokenStats, basketState: BasketState, ) { basketState.ruleAssets = new Array(20); basketState.ruleWeights = new Array(20); basketState.weightSum = 0; let ruleAssets = []; for(let i=0; i<createBasketParams.rules.length; i++){ ruleAssets.push( generateWeights( tokenStats, createBasketParams.rules[i], basketState.singleRuleAssets[i] ) ); } combineRules( basketState, ruleAssets, createBasketParams, ); updateTargetAssets(basketState); } export function rebalance( createBasketParams: CreateBasketParams, basketState: BasketState, data: DataPoint[][], day: number, tokenList: any, ) { let sell = []; let buy = []; for(let i=0; i<basketState.numOfTokens; i++){ let currentPercentage = basketState.currentCompWeight[i]; let targetPercentage = Math.floor( basketState.targetWeight[i] * WEIGHT_MULTIPLIER / basketState.weightSum ); // checking rebalance threshold if(currentPercentage <= Math.floor( targetPercentage * (BPS_DIVIDER + createBasketParams.rebalanceThreshold) / BPS_DIVIDER ) && currentPercentage >= Math.floor( targetPercentage * (BPS_DIVIDER - createBasketParams.rebalanceThreshold) / BPS_DIVIDER ) )continue; // swapping if(currentPercentage > targetPercentage){ if(i == 0)continue; sell.push(i); } else { buy.push(i); } } for(let ii=0; ii<sell.length; ii++){ let i = sell[ii]; let currentPercentage = basketState.currentCompWeight[i]; let targetPercentage = Math.floor( basketState.targetWeight[i] * (WEIGHT_MULTIPLIER / basketState.weightSum) ); let amountToSell = Math.min(Math.floor( basketState.currentCompAmount[i] * (currentPercentage - targetPercentage) / currentPercentage ), basketState.currentCompAmount[i]); let toUsdcAmount = Math.floor( amountToSell / 1000 * data[basketState.currentCompToken[i]][day].price * 950 / Math.pow(10, tokenList.list[basketState.currentCompToken[i]].decimals) ); basketState.currentCompAmount[i] -= amountToSell; basketState.currentCompAmount[0] += toUsdcAmount; } for(let ii=0; ii<buy.length; ii++){ let i = buy[ii]; let currentPercentage = basketState.currentCompWeight[i]; let targetPercentage = Math.floor( basketState.targetWeight[i] * (WEIGHT_MULTIPLIER / basketState.weightSum) ); let amountToSpend = Math.min(Math.floor( basketState.basketWorth * ((targetPercentage - currentPercentage) / WEIGHT_MULTIPLIER) ), basketState.currentCompAmount[0]); let toTokenAmount = Math.floor( amountToSpend * 950 * Math.pow(10, tokenList[basketState.currentCompToken[i]].decimals) / data[basketState.currentCompToken[i]][day].price / 1000 ); basketState.currentCompAmount[i] += toTokenAmount; basketState.currentCompAmount[0] -= amountToSpend; } } export function updateBasketState( createBasketParams: CreateBasketParams, basketState: BasketState, tokenStats: TokenStats, database: DataPoint[][], day: number, tokenList: any, ) { if(basketState.lastRefilterTime + createBasketParams.refilterInterval <= day * 24 * 60 * 60){ refilter(createBasketParams, tokenStats, basketState); basketState.lastRefilterTime = day * 24 * 60 * 60; } if(basketState.lastReweightTime + createBasketParams.reweightInterval <= day * 24 * 60 * 60){ reweight(createBasketParams, tokenStats, basketState); basketState.lastReweightTime = day * 24 * 60 * 60; } if(basketState.lastRebalanceTime + createBasketParams.rebalanceInterval <= day * 24 * 60 * 60){ updateCurrentWeights(database, day, basketState, tokenList); rebalance(createBasketParams, basketState, database, day, tokenList); basketState.lastRebalanceTime = day * 24 * 60 * 60; } } export function updateCurrentWeights( data: DataPoint[][], day: number, basketState: BasketState, tokenList: any, ) { basketState.basketWorth = 0; for(let i=0; i<COMBINED_TOKENS_IN_A_BASKET; i++){ basketState.basketWorth += Math.floor(basketState.currentCompAmount[i] * data[basketState.currentCompToken[i]][day].price / Math.pow(10, tokenList[basketState.currentCompToken[i]].decimals)); } for(let i=0; i<COMBINED_TOKENS_IN_A_BASKET; i++){ let x = BigInt(WEIGHT_MULTIPLIER) * BigInt(basketState.currentCompAmount[i]) * BigInt(data[basketState.currentCompToken[i]][day].price) / BigInt(basketState.basketWorth) / BigInt(Math.pow(10, tokenList[basketState.currentCompToken[i]].decimals)); basketState.currentCompWeight[i] = Math.floor(Number(x)); } } export async function simulate( program: Program<BasketsIDL>, database: PublicKey, tokenList: PublicKey, createBasketParams: CreateBasketParams, simulationDays: number, ) { if(simulationDays > 365) simulationDays = 365; let data = await fetchDatabase(program, database); let tokenListData = await program.account.tokenList.fetch(tokenList, "confirmed"); let basketState: BasketState = await defaultBasketState(); let simulationData: { price: number, currentCompAmount: number[], currentCompToken: number[], }[] = []; for(let i = NUM_OF_DAYS_IN_DATABASE - simulationDays; i < NUM_OF_DAYS_IN_DATABASE; i++){ let tokenStats = await updateTokenStats(data, i); await updateCurrentWeights(data, i, basketState, tokenListData); await updateBasketState(createBasketParams, basketState, tokenStats, data, i, tokenListData); simulationData.push({ price: basketState.basketWorth, currentCompAmount: Object.assign([], basketState.currentCompAmount), currentCompToken: Object.assign([], basketState.currentCompToken), }); } return simulationData; }