UNPKG

@d8x/perpetuals-sdk

Version:

Node TypeScript SDK for D8X Perpetual Futures

991 lines 103 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const ethers_1 = require("ethers"); const constants_1 = require("./constants"); const contracts_1 = require("./contracts"); const d8XMath_1 = require("./d8XMath"); const perpetualDataHandler_1 = __importDefault(require("./perpetualDataHandler")); const utils_1 = require("./utils"); /** * Functions to access market data (e.g., information on open orders, information on products that can be traded). * This class requires no private key and is blockchain read-only. * No gas required for the queries here. * @extends PerpetualDataHandler */ class MarketData extends perpetualDataHandler_1.default { /** * Constructor * @param {NodeSDKConfig} config Configuration object, see * PerpetualDataHandler.readSDKConfig. * @example * import { MarketData, PerpetualDataHandler } from '@d8x/perpetuals-sdk'; * async function main() { * console.log(MarketData); * // load configuration for Polygon zkEVM (testnet) * const config = PerpetualDataHandler.readSDKConfig("cardona"); * // MarketData (read only, no authentication needed) * let mktData = new MarketData(config); * // Create a proxy instance to access the blockchain * await mktData.createProxyInstance(); * } * main(); * */ constructor(config) { super(config); } /** * Initialize the marketData-Class with this function * to create instance of D8X perpetual contract and gather information * about perpetual currencies * @param providerOrMarketData optional provider or existing market data instance */ async createProxyInstance(providerOrMarketData, overrides) { await this.priceFeedGetter.init(); if (providerOrMarketData == undefined || !("createProxyInstance" in providerOrMarketData)) { this.provider = providerOrMarketData ?? new ethers_1.JsonRpcProvider(this.nodeURL); await this.initContractsAndData(this.provider, overrides); } else { const mktData = providerOrMarketData; this.nodeURL = mktData.config.nodeURL; this.provider = new ethers_1.JsonRpcProvider(mktData.config.nodeURL, mktData.network, { staticNetwork: true }); this.proxyContract = contracts_1.IPerpetualManager__factory.connect(mktData.getProxyAddress(), this.provider); this.multicall = contracts_1.Multicall3__factory.connect(this.config.multicall ?? constants_1.MULTICALL_ADDRESS, this.provider); ({ nestedPerpetualIDs: this.nestedPerpetualIDs, poolStaticInfos: this.poolStaticInfos, symbolToTokenAddrMap: this.symbolToTokenAddrMap, symbolToPerpStaticInfo: this.symbolToPerpStaticInfo, perpetualIdToSymbol: this.perpetualIdToSymbol, } = mktData.getAllMappings()); this.priceFeedGetter.setTriangulations(mktData.getTriangulations()); this.signerOrProvider = this.provider; } } /** * Get the proxy address * @returns {string} Address of the perpetual proxy contract */ getProxyAddress() { if (this.proxyContract == null) { throw Error("no proxy contract initialized. Use createProxyInstance()."); } return this.proxyContract.target.toString(); } /** * Get the pre-computed triangulations * @returns Triangulations */ getTriangulations() { return this.priceFeedGetter.getTriangulations(); } /** * Convert the smart contract output of an order into a convenient format of type "Order" * @param {SmartContractOrder} smOrder SmartContractOrder, as obtained e.g., by PerpetualLimitOrderCreated event * @returns {Order} more convenient format of order, type "Order" */ smartContractOrderToOrder(smOrder) { return perpetualDataHandler_1.default.fromSmartContractOrder(smOrder, this.symbolToPerpStaticInfo); } /** * Get contract instance. Useful for event listening. * @example * import { MarketData, PerpetualDataHandler } from '@d8x/perpetuals-sdk'; * async function main() { * console.log(MarketData); * // setup * const config = PerpetualDataHandler.readSDKConfig("cardona"); * let mktData = new MarketData(config); * await mktData.createProxyInstance(); * // Get contract instance * let proxy = await mktData.getReadOnlyProxyInstance(); * console.log(proxy); * } * main(); * * @returns read-only proxy instance */ getReadOnlyProxyInstance() { if (this.proxyContract == null) { throw Error("no proxy contract initialized. Use createProxyInstance()."); } return this.proxyContract; } /** * Information about the products traded in the exchange. * @example * import { MarketData, PerpetualDataHandler } from '@d8x/perpetuals-sdk'; * async function main() { * console.log(MarketData); * // setup * const config = PerpetualDataHandler.readSDKConfig("cardona"); * let mktData = new MarketData(config); * await mktData.createProxyInstance(); * // Get exchange info * let info = await mktData.exchangeInfo(); * console.log(info); * } * main(); * * @returns {ExchangeInfo} Array of static data for all the pools and perpetuals in the system. */ async exchangeInfo(overrides) { if (this.proxyContract == null || this.multicall == null) { throw Error("no proxy contract initialized. Use createProxyInstance()."); } let rpcURL; if (overrides) { ({ rpcURL, ...overrides } = overrides); } await this.refreshSymbols(); const provider = new ethers_1.JsonRpcProvider(rpcURL ?? this.nodeURL, this.network, { staticNetwork: true }); return await MarketData._exchangeInfo(new ethers_1.Contract(this.proxyAddr, this.config.proxyABI, provider), contracts_1.Multicall3__factory.connect(this.config.multicall ?? constants_1.MULTICALL_ADDRESS, provider), this.poolStaticInfos, this.symbolToPerpStaticInfo, this.perpetualIdToSymbol, this.nestedPerpetualIDs, this.symbolList, this.priceFeedGetter, this.oraclefactoryAddr, // not undefined if proxy contract was initialized overrides); } /** * All open orders for a trader-address and a symbol. * @param {string} traderAddr Address of the trader for which we get the open orders. * @param {string} symbol Symbol of the form ETH-USD-MATIC or a pool symbol, or undefined. * If a poolSymbol is provided, the response includes orders in all perpetuals of the given pool. * If no symbol is provided, the response includes orders from all perpetuals in all pools. * @example * import { MarketData, PerpetualDataHandler } from '@d8x/perpetuals-sdk'; * async function main() { * console.log(MarketData); * // setup * const config = PerpetualDataHandler.readSDKConfig("cardona"); * let mktData = new MarketData(config); * await mktData.createProxyInstance(); * // Get all open orders for a trader/symbol * let opOrder = await mktData.openOrders("0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B", * "ETH-USD-MATIC"); * console.log(opOrder); * } * main(); * * @returns For each perpetual an array of open orders and corresponding order-ids. */ async openOrders(traderAddr, symbol, overrides) { // open orders requested only for given symbol let resArray = []; let symbols; await this.refreshSymbols(); if (symbol) { symbols = symbol.split("-").length == 1 ? this.getPerpetualSymbolsInPool(symbol) : [symbol]; } else { symbols = this.poolStaticInfos.reduce((syms, pool) => syms.concat(this.getPerpetualSymbolsInPool(pool.poolMarginSymbol)), new Array()); } let rpcURL; if (overrides) { ({ rpcURL, ...overrides } = overrides); } const provider = new ethers_1.JsonRpcProvider(rpcURL ?? this.nodeURL, this.network, { staticNetwork: true }); if (symbols.length < 1) { throw new Error(`No perpetuals found for symbol ${symbol}`); } else if (symbols.length < 2) { let res = await this._openOrdersOfPerpetual(traderAddr, symbols[0], provider, overrides); resArray.push(res); } else { resArray = await this._openOrdersOfPerpetuals(traderAddr, symbols, provider, overrides); } return resArray; } /** * All open orders for a trader-address and a given perpetual symbol. * @param {string} traderAddr Address of the trader for which we get the open orders. * @param {string} symbol perpetual-symbol of the form ETH-USD-MATIC * @returns open orders and order ids * @ignore */ async _openOrdersOfPerpetual(traderAddr, symbol, provider, overrides) { // open orders requested only for given symbol const orderBookContract = this.getOrderBookContract(symbol, provider); const orders = await MarketData.openOrdersOnOrderBook(traderAddr, orderBookContract, this.symbolToPerpStaticInfo, overrides); const digests = await MarketData.orderIdsOfTrader(traderAddr, orderBookContract, overrides); return { orders: orders, orderIds: digests }; } /** * All open orders for a trader-address and a given perpetual symbol. * @param {string} traderAddr Address of the trader for which we get the open orders. * @param {string} symbol perpetual-symbol of the form ETH-USD-MATIC * @returns open orders and order ids * @ignore */ async _openOrdersOfPerpetuals(traderAddr, symbols, provider, overrides) { // filter by perpetuals with valid order book symbols = symbols.filter((symbol) => this.symbolToPerpStaticInfo.get(symbol)?.limitOrderBookAddr !== constants_1.ZERO_ADDRESS); // open orders requested only for given symbol const orderBookContracts = symbols.map((symbol) => this.getOrderBookContract(symbol, provider), this); const multicall = contracts_1.Multicall3__factory.connect(this.config.multicall ?? constants_1.MULTICALL_ADDRESS, provider); const { orders, digests } = await MarketData._openOrdersOnOrderBooks(traderAddr, orderBookContracts, multicall, this.symbolToPerpStaticInfo, overrides); return symbols.map((_symbol, i) => ({ orders: orders[i], orderIds: digests[i] })); } /** * Information about the position open by a given trader in a given perpetual contract, or * for all perpetuals in a pool * @param {string} traderAddr Address of the trader for which we get the position risk. * @param {string} symbol Symbol of the form ETH-USD-MATIC, * or pool symbol ("MATIC") to get all positions in a given pool, * or no symbol to get all positions in all pools. * @example * import { MarketData, PerpetualDataHandler } from '@d8x/perpetuals-sdk'; * async function main() { * console.log(MarketData); * // setup * const config = PerpetualDataHandler.readSDKConfig("cardona"); * let mktData = new MarketData(config); * await mktData.createProxyInstance(); * // Get position risk info * let posRisk = await mktData.positionRisk("0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B", * "ETH-USD-MATIC"); * console.log(posRisk); * } * main(); * * @returns {Array<MarginAccount>} Array of position risks of trader. */ async positionRisk(traderAddr, symbol, overrides) { if (this.proxyContract == null) { throw Error("no proxy contract initialized. Use createProxyInstance()."); } await this.refreshSymbols(); let resArray = []; let symbols; if (symbol) { symbols = symbol.split("-").length == 1 ? this.getPerpetualSymbolsInPool(symbol) : [symbol]; } else { symbols = this.poolStaticInfos.reduce((syms, pool) => syms.concat(this.getPerpetualSymbolsInPool(pool.poolMarginSymbol)), new Array()); } let rpcURL; if (overrides) { ({ rpcURL, ...overrides } = overrides); } const provider = new ethers_1.JsonRpcProvider(rpcURL ?? this.nodeURL, this.network, { staticNetwork: true }); if (symbols.length < 1) { throw new Error(`No perpetuals found for symbol ${symbol}`); } else if (symbols.length < 2) { let res = await this._positionRiskForTraderInPerpetual(traderAddr, symbols[0], provider, overrides); resArray.push(res); } else { resArray = await this._positionRiskForTraderInPerpetuals(traderAddr, symbols, provider, overrides); } return resArray; } /** * Information about the position open by a given trader in a given perpetual contract. * @param {string} traderAddr Address of the trader for which we get the position risk. * @param {string} symbol perpetual symbol of the form ETH-USD-MATIC * @returns MarginAccount struct for the trader * @ignore */ async _positionRiskForTraderInPerpetual(traderAddr, symbol, provider, overrides) { let obj = await this.priceFeedGetter.fetchPricesForPerpetual(symbol); const isPred = this.isPredictionMarket(symbol); let mgnAcct = await perpetualDataHandler_1.default.getMarginAccount(traderAddr, symbol, this.symbolToPerpStaticInfo, new ethers_1.Contract(this.proxyAddr, this.config.proxyABI, provider), obj, isPred, overrides); return mgnAcct; } /** * Information about the position open by a given trader in a given perpetual contract. * @param {string} traderAddr Address of the trader for which we get the position risk. * @param {string} symbol perpetual symbol of the form ETH-USD-MATIC * @returns MarginAccount struct for the trader * @ignore */ async _positionRiskForTraderInPerpetuals(traderAddr, symbols, provider, overrides) { const MAX_SYMBOLS_PER_CALL = 10; const pxInfo = new Array(); const symbolsToFetch = []; for (let i = 0; i < symbols.length; i++) { try { let obj = await this.priceFeedGetter.fetchPricesForPerpetual(symbols[i]); pxInfo.push(obj); symbolsToFetch.push(symbols[i]); } catch (e) { console.log(`skipping symbol ${symbols[i]})`); } } let mgnAcct = []; let callSymbols = symbolsToFetch.slice(0, MAX_SYMBOLS_PER_CALL); let _px = pxInfo.slice(0, MAX_SYMBOLS_PER_CALL); while (callSymbols.length > 0) { const isPred = callSymbols.map((_sym) => this.isPredictionMarket(_sym)); let acc = await perpetualDataHandler_1.default.getMarginAccounts(Array(callSymbols.length).fill(traderAddr), callSymbols, this.symbolToPerpStaticInfo, contracts_1.Multicall3__factory.connect(this.config.multicall ?? constants_1.MULTICALL_ADDRESS, provider), new ethers_1.Contract(this.proxyAddr, this.config.proxyABI, provider), _px, isPred, overrides); mgnAcct = mgnAcct.concat(acc); callSymbols = symbolsToFetch.slice(mgnAcct.length, mgnAcct.length + MAX_SYMBOLS_PER_CALL); _px = pxInfo.slice(mgnAcct.length, mgnAcct.length + MAX_SYMBOLS_PER_CALL); } return mgnAcct; } async dataForPositionRiskOnTrade(symbol, traderAddr, tradeAmountBC, indexPriceInfo, signedPositionNotionalBaseCCY, overrides) { if (this.proxyContract == null || this.multicall == null) { throw Error("no proxy contract initialized. Use createProxyInstance()."); } const isPredMkt = this.isPredictionMarket(symbol); // create all calls const perpId = perpetualDataHandler_1.default.symbolToPerpetualId(symbol, this.symbolToPerpStaticInfo); const [fS2, fS3, fEma] = [indexPriceInfo.s2, indexPriceInfo.s3 ?? 0, indexPriceInfo.ema].map((x) => (0, d8XMath_1.floatToABK64x64)(x)); const proxyCalls = [ // 0: traderState { target: this.proxyContract.target, allowFailure: true, callData: this.proxyContract.interface.encodeFunctionData("getTraderState", [perpId, traderAddr, [fEma, fS3]]), }, // 1: perpetual price { target: this.proxyContract.target, allowFailure: true, callData: this.proxyContract.interface.encodeFunctionData("queryPerpetualPrice", [ perpId, (0, d8XMath_1.floatToABK64x64)(tradeAmountBC), [fS2, fS3], 10n * (indexPriceInfo.conf & ((1n << 16n) - 1n)), indexPriceInfo.predMktCLOBParams, ]), }, // 2: max long pos { target: this.proxyContract.target, allowFailure: false, callData: this.proxyContract.interface.encodeFunctionData("getMaxSignedOpenTradeSizeForPos", [ perpId, (0, d8XMath_1.floatToABK64x64)(signedPositionNotionalBaseCCY), true, ]), }, // 3: max short pos { target: this.proxyContract.target, allowFailure: false, callData: this.proxyContract.interface.encodeFunctionData("getMaxSignedOpenTradeSizeForPos", [ perpId, (0, d8XMath_1.floatToABK64x64)(signedPositionNotionalBaseCCY), false, ]), }, ]; // multicall const encodedResults = await this.multicall.aggregate3.staticCall(proxyCalls, (overrides || {})); // positionRisk to apply this trade on: if not given, defaults to the current trader's position let traderState; if (encodedResults[0].success) { traderState = this.proxyContract.interface.decodeFunctionResult("getTraderState", encodedResults[0].returnData)[0]; } else { traderState = await this.proxyContract.getTraderState(perpId, traderAddr, [fEma, fS3]); } const account = MarketData.buildMarginAccountFromState(symbol, traderState, this.symbolToPerpStaticInfo, indexPriceInfo, isPredMkt); // amm price for this trade amount let ammPrice; { let fPrice; if (encodedResults[1].success) { fPrice = this.proxyContract.interface.decodeFunctionResult("queryPerpetualPrice", encodedResults[1].returnData)[0]; } else { fPrice = await this.proxyContract.queryPerpetualPrice(perpId, (0, d8XMath_1.floatToABK64x64)(tradeAmountBC), [(0, d8XMath_1.floatToABK64x64)(indexPriceInfo.s2), (0, d8XMath_1.floatToABK64x64)(indexPriceInfo.s3 ?? 0)], indexPriceInfo.conf, indexPriceInfo.predMktCLOBParams); } ammPrice = (0, d8XMath_1.ABK64x64ToFloat)(fPrice); } // max buy const fMaxLong = this.proxyContract.interface.decodeFunctionResult("getMaxSignedOpenTradeSizeForPos", encodedResults[2].returnData)[0]; const maxLongTrade = (0, d8XMath_1.ABK64x64ToFloat)(fMaxLong); // max sell const fMaxShort = this.proxyContract.interface.decodeFunctionResult("getMaxSignedOpenTradeSizeForPos", encodedResults[3].returnData)[0]; const maxShortTrade = (0, d8XMath_1.ABK64x64ToFloat)(fMaxShort); return { account: account, ammPrice: ammPrice, maxShortTrade: maxShortTrade, maxLongTrade: maxLongTrade }; } /** * Estimates what the position risk will be if a given order is executed. * @param traderAddr Address of trader * @param order Order to be submitted * @param signedPositionNotionalBaseCCY signed position notional of current position (before trade) * @param tradingFeeTbps trading fee in tenth of basis points (exchange fee and broker fee) * @param indexPriceInfo Index prices and market status (open/closed). Defaults to current market status if not given. * @returns Position risk after trade, including order cost and maximal trade sizes for position * @example * import { MarketData, PerpetualDataHandler } from '@d8x/perpetuals-sdk'; * async function main() { * console.log(MarketData); * // setup * const config = PerpetualDataHandler.readSDKConfig("cardona"); * const mktData = new MarketData(config); * await mktData.createProxyInstance(); * const order: Order = { * symbol: "MATIC-USD-MATIC", * side: "BUY", * type: "MARKET", * quantity: 100, * leverage: 2, * executionTimestamp: Date.now()/1000, * }; * // Get position risk conditional on this order being executed * const posRisk = await mktData.positionRiskOnTrade("0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B", order, 0, 60); * console.log(posRisk); * } * main(); */ async positionRiskOnTrade(traderAddr, order, signedPositionNotionalBaseCCY, tradingFeeTbps, indexPriceInfo, overrides) { if (this.proxyContract == null || this.multicall == null) { throw Error("no proxy contract initialized. Use createProxyInstance()."); } await this.refreshSymbols(); const isPredMkt = this.isPredictionMarket(order.symbol); // fetch prices if (indexPriceInfo == undefined) { indexPriceInfo = await this.priceFeedGetter.fetchPricesForPerpetual(order.symbol); } // signed trade amount let tradeAmountBC = Math.abs(order.quantity) * (order.side == constants_1.BUY_SIDE ? 1 : -1); const symbol = order.symbol; let obj = await this.dataForPositionRiskOnTrade(symbol, traderAddr, tradeAmountBC, indexPriceInfo, signedPositionNotionalBaseCCY, overrides); const account = obj.account; const maxLongTrade = obj.maxLongTrade; const maxShortTrade = obj.maxShortTrade; const ammPrice = obj.ammPrice; let Sm = account.markPrice; let S2 = indexPriceInfo.s2; let S3 = account.collToQuoteConversion; // price for this order = amm price if no limit given, else conservatively adjusted let tradePrice; if (order.limitPrice == undefined) { tradePrice = ammPrice; } else { if (order.type == constants_1.ORDER_TYPE_MARKET) { if (order.side == constants_1.BUY_SIDE) { // limit price > amm price --> likely not binding, use avg, less conservative // limit price < amm price --> likely fails due to slippage, use limit price to get actual max cost tradePrice = 0.5 * (order.limitPrice + Math.min(order.limitPrice, ammPrice)); } else { tradePrice = 0.5 * (order.limitPrice + Math.max(order.limitPrice, ammPrice)); } } else { // limit orders either get executed now (at ammPrice) or later (at limit price) if ((order.side == constants_1.BUY_SIDE && order.limitPrice > ammPrice) || (order.side == constants_1.SELL_SIDE && order.limitPrice < ammPrice)) { // can be executed now at ammPrice tradePrice = ammPrice; } else { // will execute in the future at limitPrice -> assume prices converge proportionally const slippage = ammPrice / S2; Sm = (Sm / S2) * (order.limitPrice / slippage); S2 = order.limitPrice / slippage; tradePrice = order.limitPrice; if (this.getPerpetualStaticInfo(order.symbol).collateralCurrencyType == constants_1.COLLATERAL_CURRENCY_BASE) { S3 = S2; } } } } // Current state: let lotSizeBC = MarketData._getLotSize(order.symbol, this.symbolToPerpStaticInfo); // Too small, no change to account if (Math.abs(order.quantity) < lotSizeBC) { return { newPositionRisk: account, orderCost: 0, maxLongTrade: maxLongTrade, maxShortTrade: maxShortTrade, ammPrice: obj.ammPrice, }; } // cash in margin account: upon trading, unpaid funding will be realized let currentMarginCashCC = account.collateralCC + account.unrealizedFundingCollateralCCY; // signed position, still correct if side is closed (==0) let currentPositionBC = (account.side == constants_1.BUY_SIDE ? 1 : -1) * account.positionNotionalBaseCCY; // signed locked-in value let currentLockedInQC = account.entryPrice * currentPositionBC; // New trader state: // signed position let newPositionBC = currentPositionBC + tradeAmountBC; if (Math.abs(newPositionBC) < 10 * lotSizeBC) { // fully closed tradeAmountBC = -currentPositionBC; newPositionBC = 0; } let newSide = newPositionBC > 0 ? constants_1.BUY_SIDE : newPositionBC < 0 ? constants_1.SELL_SIDE : constants_1.CLOSED_SIDE; let tradingFeeCC = isPredMkt ? (Math.abs(tradeAmountBC) * tradingFeeTbps * 1e-5) / S3 : (Math.abs(tradeAmountBC) * tradingFeeTbps * 1e-5 * S2) / S3; let referralFeeCC = this.symbolToPerpStaticInfo.get(account.symbol).referralRebate; // Trade type: let isClose = newPositionBC == 0 || newPositionBC * tradeAmountBC < 0; let isOpen = newPositionBC != 0 && (currentPositionBC == 0 || tradeAmountBC * currentPositionBC > 0); // regular open, no flip let isFlip = Math.abs(newPositionBC) > Math.abs(currentPositionBC) && !isOpen; // flip position sign, not fully closed let keepPositionLvgOnClose = (order.keepPositionLvg ?? false) && !isOpen; // Contract: _doMarginCollateralActions // No collateral actions if // 1) leverage is not set or // 2) fully closed after trade or // 3) is a partial closing, it doesn't flip, and keep lvg flag is not set let traderDepositCC; let targetLvg; if (order.leverage == undefined || newPositionBC == 0 || (!isOpen && !isFlip && !keepPositionLvgOnClose)) { traderDepositCC = 0; targetLvg = 0; } else { // 1) opening and flipping trades need to specify a leverage: default to max if not given // 2) for others it's ignored, set target to 0 let initialMarginRate = this.symbolToPerpStaticInfo.get(account.symbol).initialMarginRate; targetLvg = isFlip || isOpen ? order.leverage ?? 1 / initialMarginRate : 0; let b0 = currentMarginCashCC + (currentPositionBC * Sm - currentLockedInQC) / S3; let pos0 = currentPositionBC; if (isOpen) { b0 -= (initialMarginRate * Math.abs(currentPositionBC) * Sm) / S3; b0 = b0 < 0 ? b0 : 0; pos0 = 0; } traderDepositCC = (0, d8XMath_1.getDepositAmountForLvgTrade)(pos0, b0, tradeAmountBC, targetLvg, tradePrice, S3, Sm, isPredMkt ? initialMarginRate : undefined); // fees are paid from wallet in this case traderDepositCC += tradingFeeCC + referralFeeCC; } // Contract: _executeTrade let deltaCashCC = 0; // (-tradeAmountBC * (tradePrice - S2)) / S3; let deltaLockedQC = tradeAmountBC * tradePrice; // tradeAmountBC * S2; if (isClose) { let pnl = account.entryPrice * tradeAmountBC - deltaLockedQC; deltaLockedQC += pnl; deltaCashCC += pnl / S3; } // funding and fees deltaCashCC = deltaCashCC - tradingFeeCC - referralFeeCC; // New cash, locked-in, entry price & leverage after trade let newLockedInValueQC = currentLockedInQC + deltaLockedQC; let newMarginCashCC = currentMarginCashCC + deltaCashCC + traderDepositCC; let newEntryPrice = newPositionBC == 0 ? 0 : Math.abs(newLockedInValueQC / newPositionBC); let newMarginBalanceCC = newMarginCashCC + (newPositionBC * Sm - newLockedInValueQC) / S3; let newLeverage; if (newPositionBC === 0) { newLeverage = 0; } else if (newMarginBalanceCC <= 0) { newLeverage = Infinity; } else { let p = Sm; if (isPredMkt) { p -= 1; p = newPositionBC > 0 ? p : 1 - p; } newLeverage = Math.abs(newPositionBC * p) / S3 / newMarginBalanceCC; } // Liquidation params let [S2Liq, S3Liq, tau] = MarketData._getLiquidationParams(account.symbol, newLockedInValueQC, newPositionBC, newMarginCashCC, Sm, S3, S2, this.symbolToPerpStaticInfo); // New position risk let newPositionRisk = { symbol: account.symbol, positionNotionalBaseCCY: Math.abs(newPositionBC), side: newSide, entryPrice: newEntryPrice, leverage: newLeverage, markPrice: Sm, unrealizedPnlQuoteCCY: newPositionBC * Sm - newLockedInValueQC, unrealizedFundingCollateralCCY: 0, collateralCC: newMarginCashCC, collToQuoteConversion: S3, liquidationPrice: [S2Liq, S3Liq], liquidationLvg: 1 / tau, }; return { ammPrice: obj.ammPrice, newPositionRisk: newPositionRisk, orderCost: traderDepositCC, maxLongTrade: maxLongTrade, maxShortTrade: maxShortTrade, }; } /** * Fee is relative to base-currency amount (=trade amount) * @param {number} Sm Mark price * @param {number} tradeAmtBC Signed trade amount in base currency (positive for buy, negative for sell) * @param {number} traderPosBC Current trader position in base currency (signed, positive for long, negative for short) * @param {number} traderLockedInQC Locked-in value in quote currency for existing position (entry price * position size) * @param {number} collateralCC Collateral in CC for existing position (fCashCC from margin account) * @param {number} traderLeverage Leverage for the new trade * @param {number} mu_m maintenance margin rate * @param {number} mu_i Initial margin rate, for pred markets this is the min cash per contract required to open * @param {bigint} conf Configuration bigint encoding jump and sigt (volatility * sqrt(time)) * @returns {number} Exchange fee per unit of base currency (e.g., 0.05 = 5 cents per contract) */ static exchangeFeePrdMkts(Sm, tradeAmtBC, traderPosBC, traderLockedInQC, collateralCC, traderLeverage, mu_m, mu_i, conf) { const isClose = Math.sign(traderPosBC) != Math.sign(tradeAmtBC); const isFlip = Math.abs(traderPosBC + tradeAmtBC) > 0.01 && Math.sign(traderPosBC + tradeAmtBC) != Math.sign(traderPosBC); // varphi_0 is the entry mark price labeled varphiBar_0 on page 2 of the leveraged prediction markets paper used in equation (1) const varphi_0 = tradeAmtBC > 0 ? Sm - 1 : 2 - Sm; // varphi is the entry price of an existing position, we need this for the exit fee to compute liq price and the check if // the original position is overcollateralized at entry const varphi = traderPosBC == 0 ? varphi_0 : traderPosBC > 0 ? traderPosBC / traderLockedInQC - 1 : 2 - traderPosBC / traderLockedInQC; const c_min = Math.max(varphi_0, mu_i); const m_0 = Math.min(varphi_0 / traderLeverage, c_min); // for the full exit fee m0 is taken from the existing position and divided by the leverage of the new trade const m_0Exit = traderPosBC == 0 ? 0 : collateralCC / Math.abs(traderPosBC) / traderLeverage; let fee; const { jump, sigt } = (0, d8XMath_1.extractLvgFeeParams)(conf); if (isClose && !isFlip) { // exit fee //console.log("[exchangeFeePrdMkts] exit fee"); fee = (0, d8XMath_1.pmExitFee)(varphi, varphi_0, m_0Exit, mu_m, mu_i, sigt, jump); // if we are overcollateralized at entry (entry mark <= cash/contract), exit fee is 0.001 in any case if (varphi <= m_0Exit) { fee = 0.001; } } else { // entry fee fee = (0, d8XMath_1.pmOpenFee)(varphi_0, m_0, mu_m, sigt, jump); } if (fee < 0.001) { fee = 0.001; } else if (fee > 0.65535) { fee = 0.65535; } return fee; } /** * Estimates what the position risk will be if given amount of collateral is added/removed from the account. * @param {number} deltaCollateral Amount of collateral to add or remove (signed) * @param {MarginAccount} account Position risk before collateral is added or removed * @returns {MarginAccount} Position risk after collateral has been added/removed * @example * import { MarketData, PerpetualDataHandler } from '@d8x/perpetuals-sdk'; * async function main() { * console.log(MarketData); * // setup * const config = PerpetualDataHandler.readSDKConfig("cardona"); * const mktData = new MarketData(config); * await mktData.createProxyInstance(); * // Get position risk conditional on removing 3.14 MATIC * const traderAddr = "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B"; * const curPos = await mktData.positionRisk("traderAddr", "BTC-USD-MATIC"); * const posRisk = await mktData.positionRiskOnCollateralAction(-3.14, curPos); * console.log(posRisk); * } * main(); */ async positionRiskOnCollateralAction(deltaCollateral, account, indexPriceInfo, overrides) { if (this.proxyContract == null) { throw new Error("no proxy contract initialized. Use createProxyInstance()."); } if (deltaCollateral + account.collateralCC + account.unrealizedFundingCollateralCCY < 0) { throw new Error("not enough margin to remove"); } await this.refreshSymbols(); if (indexPriceInfo == undefined) { indexPriceInfo = await this.priceFeedGetter.fetchPricesForPerpetual(account.symbol); } let perpetualState = await this.getPerpetualState(account.symbol, indexPriceInfo, overrides); let Sm; //mark price if (this.isPredictionMarket(account.symbol)) { Sm = indexPriceInfo.ema + perpetualState.markPremium; } else { Sm = perpetualState.indexPrice * (1 + perpetualState.markPremium); } let [S2, S3] = [perpetualState.indexPrice, perpetualState.collToQuoteIndexPrice]; // no position: just increase collateral and kill liquidation vars if (account.positionNotionalBaseCCY == 0) { return { symbol: account.symbol, positionNotionalBaseCCY: account.positionNotionalBaseCCY, side: account.side, entryPrice: account.entryPrice, leverage: account.leverage, markPrice: Sm, unrealizedPnlQuoteCCY: account.unrealizedPnlQuoteCCY, unrealizedFundingCollateralCCY: account.unrealizedFundingCollateralCCY, collateralCC: account.collateralCC + deltaCollateral, collToQuoteConversion: S3, liquidationPrice: [0, undefined], liquidationLvg: Infinity, }; } let positionBC = account.positionNotionalBaseCCY * (account.side == constants_1.BUY_SIDE ? 1 : -1); let lockedInQC = account.entryPrice * positionBC; let newMarginCashCC = account.collateralCC + deltaCollateral; let newMarginBalanceCC = newMarginCashCC + account.unrealizedFundingCollateralCCY + (positionBC * Sm - lockedInQC) / S3; if (newMarginBalanceCC <= 0) { return { symbol: account.symbol, positionNotionalBaseCCY: account.positionNotionalBaseCCY, side: account.side, entryPrice: account.entryPrice, leverage: Infinity, markPrice: Sm, unrealizedPnlQuoteCCY: account.unrealizedPnlQuoteCCY, unrealizedFundingCollateralCCY: account.unrealizedFundingCollateralCCY, collateralCC: newMarginCashCC, collToQuoteConversion: S3, liquidationPrice: [S2, S3], liquidationLvg: 0, }; } let newLeverage = (Math.abs(positionBC) * Sm) / S3 / newMarginBalanceCC; // Liquidation params let [S2Liq, S3Liq, tau] = MarketData._getLiquidationParams(account.symbol, lockedInQC, positionBC, newMarginCashCC, Sm, S3, S2, this.symbolToPerpStaticInfo); // New position risk let newPositionRisk = { symbol: account.symbol, positionNotionalBaseCCY: account.positionNotionalBaseCCY, side: account.side, entryPrice: account.entryPrice, leverage: newLeverage, markPrice: Sm, unrealizedPnlQuoteCCY: account.unrealizedPnlQuoteCCY, unrealizedFundingCollateralCCY: account.unrealizedFundingCollateralCCY, collateralCC: newMarginCashCC, collToQuoteConversion: S3, liquidationPrice: [S2Liq, S3Liq], liquidationLvg: 1 / tau, }; return newPositionRisk; } /** * Calculates liquidation prices for a position * constructed in positionRiskOnTrade/positionRiskOnCollateralAction * @param symbol Perpetual symbol * @param lockedInQC Locked in value * @param signedPositionBC Signed position size * @param marginCashCC Available cash in margin account (includes unpaid funding) * @param markPrice Mark price * @param collToQuoteConversion Collateral index price (S3) * @param S2 index price * @param symbolToPerpStaticInfo Symbol-to-perp static info mapping * @returns [Base index price, Collateral index price, Maintenance margin rate] * @ignore */ static _getLiquidationParams(symbol, lockedInQC, signedPositionBC, marginCashCC, markPrice, collToQuoteConversion, S2, symbolToPerpStaticInfo) { let S2Liq, S3Liq; const staticInfo = symbolToPerpStaticInfo.get(symbol); let ccyType = staticInfo.collateralCurrencyType; const isPred = MarketData.isPredictionMarketStatic(staticInfo); const idx_availableCashCC = 2; const idx_cash = 3; const idx_notional = 4; const idx_locked_in = 5; const idx_mark_price = 8; const idx_s3 = 9; const idx_maint_mgn_rate = 10; let traderState = new Array(11); traderState[idx_availableCashCC] = (0, d8XMath_1.floatToABK64x64)(marginCashCC); traderState[idx_cash] = traderState[idx_availableCashCC]; traderState[idx_notional] = (0, d8XMath_1.floatToABK64x64)(signedPositionBC); traderState[idx_locked_in] = (0, d8XMath_1.floatToABK64x64)(lockedInQC); traderState[idx_mark_price] = (0, d8XMath_1.floatToABK64x64)(markPrice); traderState[idx_s3] = (0, d8XMath_1.floatToABK64x64)(collToQuoteConversion); traderState[idx_maint_mgn_rate] = (0, d8XMath_1.floatToABK64x64)(perpetualDataHandler_1.default.getMaintenanceMarginRate(staticInfo)); let tau; [S2Liq, S3Liq, tau, ,] = MarketData._calculateLiquidationPrice(symbol, traderState, S2, symbolToPerpStaticInfo, isPred); return [S2Liq, S3Liq, tau]; } /** * Gets the wallet balance in the settlement currency corresponding to a given perpetual symbol. * The settlement currency is usually the same as the collateral currency. * @param address Address to check * @param symbol Symbol of the form ETH-USD-MATIC. * @returns Perpetual's collateral token balance of the given address. * @example * import { MarketData, PerpetualDataHandler } from '@d8x/perpetuals-sdk'; * async function main() { * console.log(MarketData); * // setup (authentication required, PK is an environment variable with a private key) * const config = PerpetualDataHandler.readSDKConfig("cardona"); * let md = new MarketData(config); * await md.createProxyInstance(); * // get MATIC balance of address * let marginTokenBalance = await md.getWalletBalance(myaddress, "BTC-USD-MATIC"); * console.log(marginTokenBalance); * } * main(); */ async getWalletBalance(address, symbol, overrides) { await this.refreshSymbols(); let poolIdx = this.getPoolStaticInfoIndexFromSymbol(symbol); let settleTokenAddr = this.poolStaticInfos[poolIdx].poolSettleTokenAddr; let token = contracts_1.ERC20__factory.connect(settleTokenAddr, this.provider); let walletBalance = await token.balanceOf(address, overrides || {}); let decimals = this.poolStaticInfos[poolIdx].poolSettleTokenDecimals; return Number((0, ethers_1.formatUnits)(walletBalance, decimals)); } /** * Get the address' balance of the pool share token * @param {string} address address of the liquidity provider * @param {string | number} symbolOrId Symbol of the form ETH-USD-MATIC, or MATIC (collateral only), or Pool-Id * @returns {number} Pool share token balance of the given address (e.g. dMATIC balance) * @example * import { MarketData, PerpetualDataHandler } from '@d8x/perpetuals-sdk'; * async function main() { * console.log(MarketData); * // setup (authentication required, PK is an environment variable with a private key) * const config = PerpetualDataHandler.readSDKConfig("cardona"); * let md = new MarketData(config); * await md.createProxyInstance(); * // get dMATIC balance of address * let shareTokenBalance = await md.getPoolShareTokenBalance(myaddress, "MATIC"); * console.log(shareTokenBalance); * } * main(); */ async getPoolShareTokenBalance(address, symbolOrId, overrides) { let poolId = this._poolSymbolOrIdToPoolId(symbolOrId); return this._getPoolShareTokenBalanceFromId(address, poolId, overrides); } /** * Query the pool share token holdings of address * @param address address of token holder * @param poolId pool id * @returns pool share token balance of address * @ignore */ async _getPoolShareTokenBalanceFromId(address, poolId, overrides) { let shareTokenAddr = this.poolStaticInfos[poolId - 1].shareTokenAddr; let shareToken = contracts_1.ERC20__factory.connect(shareTokenAddr, this.provider); let d18ShareTokenBalanceOfAddr = await shareToken.balanceOf(address, overrides || {}); return (0, d8XMath_1.dec18ToFloat)(d18ShareTokenBalanceOfAddr); } /** * Value of pool token in collateral currency * @param {string | number} symbolOrId symbol of the form ETH-USD-MATIC, MATIC (collateral), or poolId * @returns {number} current pool share token price in collateral currency * @example * import { MarketData, PerpetualDataHandler } from '@d8x/perpetuals-sdk'; * async function main() { * console.log(MarketData); * // setup (authentication required, PK is an environment variable with a private key) * const config = PerpetualDataHandler.readSDKConfig("cardona"); * let md = new MarketData(config); * await md.createProxyInstance(); * // get price of 1 dMATIC in MATIC * let shareTokenPrice = await md.getShareTokenPrice(myaddress, "MATIC"); * console.log(shareTokenPrice); * } * main(); */ async getShareTokenPrice(symbolOrId, overrides) { let poolId = this._poolSymbolOrIdToPoolId(symbolOrId); const priceDec18 = await this.proxyContract.getShareTokenPriceD18(poolId, overrides || {}); const price = (0, d8XMath_1.dec18ToFloat)(priceDec18); return price; } /** * Value of the pool share tokens for this liquidity provider * in poolSymbol-currency (e.g. MATIC, USDC). * @param {string} address address of liquidity provider * @param {string | number} symbolOrId symbol of the form ETH-USD-MATIC, MATIC (collateral), or poolId * @returns the value (in collateral tokens) of the pool share, #share tokens, shareTokenAddress * @example * import { MarketData, PerpetualDataHandler } from '@d8x/perpetuals-sdk'; * async function main() { * console.log(MarketData); * // setup (authentication required, PK is an environment variable with a private key) * const config = PerpetualDataHandler.readSDKConfig("cardona"); * let md = new MarketData(config); * await md.createProxyInstance(); * // get value of pool share token * let shareToken = await md.getParticipationValue(myaddress, "MATIC"); * console.log(shareToken); * } * main(); */ async getParticipationValue(address, symbolOrId, overrides) { let poolId = this._poolSymbolOrIdToPoolId(symbolOrId); const shareTokens = await this._getPoolShareTokenBalanceFromId(address, poolId, overrides); const priceDec18 = await this.proxyContract.getShareTokenPriceD18(poolId, overrides || {}); const price = (0, d8XMath_1.dec18ToFloat)(priceDec18); const value = price * shareTokens; const shareTokenAddr = this.poolStaticInfos[poolId - 1].shareTokenAddr; return { value: value, shareTokenBalance: shareTokens, poolShareToken: shareTokenAddr, }; } /** * Get pool id from symbol * @param poolSymbolOrId Pool symbol or pool Id * @returns Pool Id * @ignore */ _poolSymbolOrIdToPoolId(poolSymbolOrId) { if (this.proxyContract == null || this.poolStaticInfos.length == 0) { throw Error("no proxy contract or wallet or data initialized. Use createProxyInstance()."); } let poolId; if (isNaN(Number(poolSymbolOrId))) { poolId = perpetualDataHandler_1.default._getPoolIdFromSymbol(poolSymbolOrId, this.poolStaticInfos); } else { poolId = Number(poolSymbolOrId); } return poolId; } /** * Gets the maximal order sizes to open/close/flip positions, both long and short, * considering the existing position, state of the perpetual * Accounts for user's wallet balance only in rmMaxOrderSizeForTrader case. * @param {string} traderAddr Address of trader * @param {symbol} symbol Symbol of the form ETH-USD-MATIC * @returns Maximal buy and sell trade sizes (positive) * @example * import { MarketData, PerpetualDataHandler } from '@d8x/perpetuals-sdk'; * async function main() { * console.log(MarketData); * // setup (authentication required, PK is an environment variable with a private key) * const config = PerpetualDataHandler.readSDKConfig("cardona"); * let md = new MarketData(config); * await md.createProxyInstance(); * // max order sizes * let shareToken = await md.maxOrderSizeForTrader(myaddress, "BTC-USD-MATIC"); * console.log(shareToken); // {buy: 314, sell: 415} * } * main(); */ async maxOrderSizeForTrader(traderAddr, symbol, overrides) { if (!this.proxyContract || !this.multicall) { throw new Error("proxy contract not initialized"); } if (this.isPredictionMarket(symbol)) { await this.refreshSymbols(); // prediction markets: also works for closing positions return this.pmMaxOrderSizeForTrader(traderAddr, symbol, overrides); } // regular markets: also works for closing positions return this.rmMaxOrderSizeForTrader(traderAddr, symbol, overrides); } /** * pmMaxOrderSizeForTrader returns the max order size for the * trader that is possible from AMM perspective (agnostic about wallet * balance and leverage, also correct if trader is shrinking their position) * @param traderAddr address of trader * @param symbol perp symbol * @param overrides optional * @returns buy: number; sell: number absolute */ async pmMaxOrderSizeForTrader(traderAddr, symbol, overrides) { if (!this.proxyContract || !this.multicall) { throw new Error("proxy contract not initialized"); } const perpId = perpetualDataHandler_1.default.symbolToPerpetualId(symbol, this.symbolToPerpStaticInfo); const indexPriceInfo = await this.priceFeedGetter.fetchPricesForPerpetual(symbol); const [fS3, fEma] = [indexPriceInfo.s3 ?? 0, indexPriceInfo.ema].map((x) => (0, d8XMath_1.floatToABK64x64)(x)); const traderState = await this.proxyContract.getTraderState(perpId,