@symmetry-hq/baskets-sdk
Version:
Software Development Kit for interacting with Symmetry Baskets Program
526 lines (495 loc) • 17.4 kB
text/typescript
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;
}