@symmetry-hq/baskets-sdk
Version:
Software Development Kit for interacting with Symmetry Baskets Program
391 lines (390 loc) • 16.9 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.fetchDatabase = fetchDatabase;
exports.defaultBasketState = defaultBasketState;
exports.calculateStats = calculateStats;
exports.updateTokenStats = updateTokenStats;
exports.selectTokens = selectTokens;
exports.refilter = refilter;
exports.calculateWeight = calculateWeight;
exports.generateWeights = generateWeights;
exports.combineRules = combineRules;
exports.addTargetAssets = addTargetAssets;
exports.removeTargetAssets = removeTargetAssets;
exports.updateTargetAssets = updateTargetAssets;
exports.reweight = reweight;
exports.rebalance = rebalance;
exports.updateBasketState = updateBasketState;
exports.updateCurrentWeights = updateCurrentWeights;
exports.simulate = simulate;
const config_1 = require("./config");
function fetchDatabase(program, database) {
return __awaiter(this, void 0, void 0, function* () {
let data = yield program.account.database.fetch(database, "confirmed");
let processedData = [];
for (let i = 0; i < config_1.NUM_OF_TOKENS_IN_ASSET_POOL; i++) {
let index = data.data[i].index.toNumber();
let currentTokenData = [];
while (true) {
let 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 == config_1.NUM_OF_DAYS_IN_DATABASE)
index = 0;
if (index == data.data[i].index.toNumber())
break;
}
processedData.push(currentTokenData);
}
return processedData;
});
}
function defaultBasketState() {
let array = [1000000];
let token = [0];
let amount = [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,
};
}
function calculateStats(data, days, index) {
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 * config_1.WEIGHT_MULTIPLIER / firstPrice;
return result;
}
function updateTokenStats(data, index) {
let tokenStats = {
stats: [],
};
let daysArray = [1, 7, 30, 91, 182, 365];
for (let tokenId = 0; tokenId < config_1.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;
}
function selectTokens(createBasketParams, rule, tokenStats) {
let ruleTokens = new Array(20);
if (rule.filterBy == 0) {
ruleTokens[0] = rule.fixedAsset;
return ruleTokens;
}
let allTokens = [];
for (let i = 0; i < config_1.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 config_1.SortBy.AscendingOrder:
ruleTokens[i] = allTokens[i].id;
break;
default:
ruleTokens[i] = allTokens[allTokens.length - i - 1].id;
}
}
return ruleTokens;
}
function refilter(createBasketParams, tokenStats, 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);
}
}
function calculateWeight(rawWeight, expo, weightBy) {
let multiplier = 1;
if (weightBy == 3)
multiplier = config_1.WEIGHT_MULTIPLIER;
return Math.floor(Math.pow(rawWeight / multiplier, expo / config_1.EXPO_DIVIDER) * multiplier);
}
function generateWeights(tokenStats, rule, ruleAssets) {
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;
}
function combineRules(basketState, ruleAssets, createBasketParams) {
basketState.ruleAssets = new Array(20).fill(0);
basketState.ruleWeights = new Array(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(config_1.WEIGHT_MULTIPLIER) * BigInt(ruleAssets[i][j].assetWeight)
* BigInt(createBasketParams.rules[i].totalWeight) / BigInt(sumOfWeights);
let weight = Math.floor(Number(x));
basketState.ruleWeights[index] += weight;
}
}
}
function addTargetAssets(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 < config_1.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++;
}
}
function removeTargetAssets(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++;
}
}
function updateTargetAssets(basketState) {
removeTargetAssets(basketState);
addTargetAssets(basketState);
basketState.weightSum = 0;
basketState.ruleWeights.forEach((element) => {
basketState.weightSum += element;
});
}
function reweight(createBasketParams, tokenStats, 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);
}
function rebalance(createBasketParams, basketState, data, day, tokenList) {
let sell = [];
let buy = [];
for (let i = 0; i < basketState.numOfTokens; i++) {
let currentPercentage = basketState.currentCompWeight[i];
let targetPercentage = Math.floor(basketState.targetWeight[i] * config_1.WEIGHT_MULTIPLIER / basketState.weightSum);
// checking rebalance threshold
if (currentPercentage <= Math.floor(targetPercentage *
(config_1.BPS_DIVIDER + createBasketParams.rebalanceThreshold) /
config_1.BPS_DIVIDER) &&
currentPercentage >= Math.floor(targetPercentage *
(config_1.BPS_DIVIDER - createBasketParams.rebalanceThreshold) /
config_1.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] * (config_1.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] * (config_1.WEIGHT_MULTIPLIER / basketState.weightSum));
let amountToSpend = Math.min(Math.floor(basketState.basketWorth *
((targetPercentage - currentPercentage) /
config_1.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;
}
}
function updateBasketState(createBasketParams, basketState, tokenStats, database, day, tokenList) {
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;
}
}
function updateCurrentWeights(data, day, basketState, tokenList) {
basketState.basketWorth = 0;
for (let i = 0; i < config_1.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 < config_1.COMBINED_TOKENS_IN_A_BASKET; i++) {
let x = BigInt(config_1.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));
}
}
function simulate(program, database, tokenList, createBasketParams, simulationDays) {
return __awaiter(this, void 0, void 0, function* () {
if (simulationDays > 365)
simulationDays = 365;
let data = yield fetchDatabase(program, database);
let tokenListData = yield program.account.tokenList.fetch(tokenList, "confirmed");
let basketState = yield defaultBasketState();
let simulationData = [];
for (let i = config_1.NUM_OF_DAYS_IN_DATABASE - simulationDays; i < config_1.NUM_OF_DAYS_IN_DATABASE; i++) {
let tokenStats = yield updateTokenStats(data, i);
yield updateCurrentWeights(data, i, basketState, tokenListData);
yield updateBasketState(createBasketParams, basketState, tokenStats, data, i, tokenListData);
simulationData.push({
price: basketState.basketWorth,
currentCompAmount: Object.assign([], basketState.currentCompAmount),
currentCompToken: Object.assign([], basketState.currentCompToken),
});
}
return simulationData;
});
}