UNPKG

@d8x/perpetuals-sdk

Version:

Node TypeScript SDK for D8X Perpetual Futures

1,288 lines (1,229 loc) 103 kB
import { Contract, formatUnits, hexlify, Interface, JsonRpcProvider, Overrides, Provider } from "ethers"; import { BUY_SIDE, CLOSED_SIDE, COLLATERAL_CURRENCY_BASE, COLLATERAL_CURRENCY_QUANTO, ERC20_ABI, MULTICALL_ADDRESS, OrderStatus, ORDER_TYPE_MARKET, PERP_STATE_STR, SELL_SIDE, ZERO_ADDRESS, ZERO_ORDER_ID, } from "./constants"; import { CompositeToken__factory, ERC20__factory, IPerpetualManager__factory, Multicall3__factory, type LimitOrderBook, type Multicall3, } from "./contracts"; import { type ERC20Interface } from "./contracts/ERC20"; import { IPerpetualOrder, type PerpStorage } from "./contracts/IPerpetualManager"; import { type IClientOrder } from "./contracts/LimitOrderBook"; import { ABK64x64ToFloat, dec18ToFloat, decNToFloat, extractLvgFeeParams, floatToABK64x64, getDepositAmountForLvgTrade, getMaxSignedPositionSize, pmExitFee, pmMaxSignedOpenTradeSize, pmOpenFee, } from "./d8XMath"; import { PythMetadata, type ExchangeInfo, type IdxPriceInfo, type MarginAccount, type NodeSDKConfig, type Order, type PerpetualState, type PerpetualStaticInfo, type PoolState, type PoolStaticInfo, type SmartContractOrder, } from "./nodeSDKTypes"; import PerpetualDataHandler from "./perpetualDataHandler"; import PriceFeeds from "./priceFeeds"; import { contractSymbolToSymbol, toBytes4 } from "./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 */ export default class MarketData extends PerpetualDataHandler { /** * 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(); * */ public constructor(config: NodeSDKConfig) { super(config); } /** * Initialize the marketData-Class with this function * to create instance of D8X perpetual contract and gather information * about perpetual currencies * @param provider optional provider to perform blockchain calls */ public async createProxyInstance(provider?: Provider, overrides?: Overrides): Promise<void>; /** * Initialize the marketData-Class with this function * to create instance of D8X perpetual contract and gather information * about perpetual currencies * @param marketData Initialized market data object to save on blokchain calls */ public async createProxyInstance(marketData: MarketData): Promise<void>; /** * 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 */ public async createProxyInstance(providerOrMarketData?: Provider | MarketData, overrides?: Overrides): Promise<void> { await this.priceFeedGetter.init(); if (providerOrMarketData == undefined || !("createProxyInstance" in providerOrMarketData)) { this.provider = providerOrMarketData ?? new JsonRpcProvider(this.nodeURL); await this.initContractsAndData(this.provider, overrides); } else { const mktData = providerOrMarketData; this.nodeURL = mktData.config.nodeURL; this.provider = new JsonRpcProvider(mktData.config.nodeURL, mktData.network, { staticNetwork: true }); this.proxyContract = IPerpetualManager__factory.connect(mktData.getProxyAddress(), this.provider); this.multicall = Multicall3__factory.connect(this.config.multicall ?? 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 */ public getProxyAddress(): string { if (this.proxyContract == null) { throw Error("no proxy contract initialized. Use createProxyInstance()."); } return this.proxyContract.target.toString(); } /** * Get the pre-computed triangulations * @returns Triangulations */ public 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" */ public smartContractOrderToOrder( smOrder: | SmartContractOrder | IPerpetualOrder.OrderStruct | IPerpetualOrder.OrderStructOutput | IClientOrder.ClientOrderStruct | IClientOrder.ClientOrderStructOutput ): Order { return PerpetualDataHandler.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 */ public 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. */ public async exchangeInfo(overrides?: Overrides & { rpcURL?: string }): Promise<ExchangeInfo> { if (this.proxyContract == null || this.multicall == null) { throw Error("no proxy contract initialized. Use createProxyInstance()."); } let rpcURL: string | undefined; if (overrides) { ({ rpcURL, ...overrides } = overrides); } await this.refreshSymbols(); const provider = new JsonRpcProvider(rpcURL ?? this.nodeURL, this.network, { staticNetwork: true }); return await MarketData._exchangeInfo( new Contract(this.proxyAddr, this.config.proxyABI!, provider), Multicall3__factory.connect(this.config.multicall ?? 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 as 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. */ public async openOrders( traderAddr: string, symbol?: string, overrides?: Overrides & { rpcURL?: string } ): Promise<{ orders: Order[]; orderIds: string[] }[]> { // open orders requested only for given symbol let resArray: Array<{ orders: Order[]; orderIds: string[] }> = []; let symbols: Array<string>; 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<string>() ); } let rpcURL: string | undefined; if (overrides) { ({ rpcURL, ...overrides } = overrides); } const provider = new 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 */ private async _openOrdersOfPerpetual( traderAddr: string, symbol: string, provider: Provider, overrides?: Overrides ): Promise<{ orders: Order[]; orderIds: string[] }> { // 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 */ private async _openOrdersOfPerpetuals( traderAddr: string, symbols: string[], provider: Provider, overrides?: Overrides ): Promise<{ orders: Order[]; orderIds: string[] }[]> { // filter by perpetuals with valid order book symbols = symbols.filter((symbol) => this.symbolToPerpStaticInfo.get(symbol)?.limitOrderBookAddr !== ZERO_ADDRESS); // open orders requested only for given symbol const orderBookContracts = symbols.map((symbol) => this.getOrderBookContract(symbol, provider), this); const multicall = Multicall3__factory.connect(this.config.multicall ?? 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. */ public async positionRisk( traderAddr: string, symbol?: string, overrides?: Overrides & { rpcURL?: string } ): Promise<MarginAccount[]> { if (this.proxyContract == null) { throw Error("no proxy contract initialized. Use createProxyInstance()."); } await this.refreshSymbols(); let resArray: Array<MarginAccount> = []; let symbols: Array<string>; 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<string>() ); } let rpcURL: string | undefined; if (overrides) { ({ rpcURL, ...overrides } = overrides); } const provider = new 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 */ protected async _positionRiskForTraderInPerpetual( traderAddr: string, symbol: string, provider: Provider, overrides?: Overrides ): Promise<MarginAccount> { let obj = await this.priceFeedGetter.fetchPricesForPerpetual(symbol); const isPred = this.isPredictionMarket(symbol); let mgnAcct = await PerpetualDataHandler.getMarginAccount( traderAddr, symbol, this.symbolToPerpStaticInfo, new 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 */ protected async _positionRiskForTraderInPerpetuals( traderAddr: string, symbols: string[], provider: Provider, overrides?: Overrides ): Promise<MarginAccount[]> { const MAX_SYMBOLS_PER_CALL = 10; const pxInfo = new Array<IdxPriceInfo>(); const symbolsToFetch: string[] = []; 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: MarginAccount[] = []; 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.getMarginAccounts( Array(callSymbols.length).fill(traderAddr), callSymbols, this.symbolToPerpStaticInfo, Multicall3__factory.connect(this.config.multicall ?? MULTICALL_ADDRESS, provider), new 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; } private async dataForPositionRiskOnTrade( symbol: string, traderAddr: string, tradeAmountBC: number, indexPriceInfo: IdxPriceInfo, signedPositionNotionalBaseCCY: number, overrides?: Overrides ): Promise<{ account: MarginAccount; ammPrice: number; maxShortTrade: number; maxLongTrade: number }> { 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.symbolToPerpetualId(symbol, this.symbolToPerpStaticInfo); const [fS2, fS3, fEma] = [indexPriceInfo.s2, indexPriceInfo.s3 ?? 0, indexPriceInfo.ema].map((x) => floatToABK64x64(x) ) as [bigint, bigint, bigint]; const proxyCalls: Multicall3.Call3Struct[] = [ // 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, 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, floatToABK64x64(signedPositionNotionalBaseCCY), true, ]), }, // 3: max short pos { target: this.proxyContract.target, allowFailure: false, callData: this.proxyContract.interface.encodeFunctionData("getMaxSignedOpenTradeSizeForPos", [ perpId, floatToABK64x64(signedPositionNotionalBaseCCY), false, ]), }, ]; // multicall const encodedResults = await this.multicall.aggregate3.staticCall(proxyCalls, (overrides || {}) as Overrides); // positionRisk to apply this trade on: if not given, defaults to the current trader's position let traderState: bigint[]; 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: number; { let fPrice: bigint; if (encodedResults[1].success) { fPrice = this.proxyContract.interface.decodeFunctionResult( "queryPerpetualPrice", encodedResults[1].returnData )[0]; } else { fPrice = await this.proxyContract.queryPerpetualPrice( perpId, floatToABK64x64(tradeAmountBC), [floatToABK64x64(indexPriceInfo.s2), floatToABK64x64(indexPriceInfo.s3 ?? 0)], indexPriceInfo.conf, indexPriceInfo.predMktCLOBParams ); } ammPrice = ABK64x64ToFloat(fPrice); } // max buy const fMaxLong = this.proxyContract.interface.decodeFunctionResult( "getMaxSignedOpenTradeSizeForPos", encodedResults[2].returnData )[0] as bigint; const maxLongTrade = ABK64x64ToFloat(fMaxLong); // max sell const fMaxShort = this.proxyContract.interface.decodeFunctionResult( "getMaxSignedOpenTradeSizeForPos", encodedResults[3].returnData )[0] as bigint; const maxShortTrade = 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(); */ public async positionRiskOnTrade( traderAddr: string, order: Order, signedPositionNotionalBaseCCY: number, tradingFeeTbps: number, indexPriceInfo?: IdxPriceInfo, overrides?: Overrides ): Promise<{ newPositionRisk: MarginAccount; orderCost: number; maxLongTrade: number; maxShortTrade: number; ammPrice: number; }> { 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 == 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: number; if (order.limitPrice == undefined) { tradePrice = ammPrice; } else { if (order.type == ORDER_TYPE_MARKET) { if (order.side == 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 == BUY_SIDE && order.limitPrice > ammPrice) || (order.side == 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 == 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 == 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 ? BUY_SIDE : newPositionBC < 0 ? SELL_SIDE : 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: number; let targetLvg: number; 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 = 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: number; 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: MarginAccount = { 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) */ public static exchangeFeePrdMkts( Sm: number, tradeAmtBC: number, traderPosBC: number, traderLockedInQC: number, collateralCC: number, traderLeverage: number, mu_m: number, mu_i: number, conf: bigint ): number { 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: number; const { jump, sigt } = extractLvgFeeParams(conf); if (isClose && !isFlip) { // exit fee //console.log("[exchangeFeePrdMkts] exit fee"); fee = 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 = 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(); */ public async positionRiskOnCollateralAction( deltaCollateral: number, account: MarginAccount, indexPriceInfo?: IdxPriceInfo, overrides?: Overrides ): Promise<MarginAccount> { 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 == 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: MarginAccount = { 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 */ protected static _getLiquidationParams( symbol: string, lockedInQC: number, signedPositionBC: number, marginCashCC: number, markPrice: number, collToQuoteConversion: number, S2: number, symbolToPerpStaticInfo: Map<string, PerpetualStaticInfo> ): [number, number | undefined, number] { let S2Liq: number, S3Liq: number | undefined; 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<bigint>(11); traderState[idx_availableCashCC] = floatToABK64x64(marginCashCC); traderState[idx_cash] = traderState[idx_availableCashCC]; traderState[idx_notional] = floatToABK64x64(signedPositionBC); traderState[idx_locked_in] = floatToABK64x64(lockedInQC); traderState[idx_mark_price] = floatToABK64x64(markPrice); traderState[idx_s3] = floatToABK64x64(collToQuoteConversion); traderState[idx_maint_mgn_rate] = floatToABK64x64(PerpetualDataHandler.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(); */ public async getWalletBalance(address: string, symbol: string, overrides?: Overrides): Promise<number> { await this.refreshSymbols(); let poolIdx = this.getPoolStaticInfoIndexFromSymbol(symbol); let settleTokenAddr = this.poolStaticInfos[poolIdx].poolSettleTokenAddr; let token = ERC20__factory.connect(settleTokenAddr, this.provider!); let walletBalance = await token.balanceOf(address, overrides || {}); let decimals = this.poolStaticInfos[poolIdx].poolSettleTokenDecimals; return Number(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(); */ public async getPoolShareTokenBalance( address: string, symbolOrId: string | number, overrides?: Overrides ): Promise<number> { 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 */ private async _getPoolShareTokenBalanceFromId( address: string, poolId: number, overrides?: Overrides ): Promise<number> { let shareTokenAddr = this.poolStaticInfos[poolId - 1].shareTokenAddr; let shareToken = ERC20__factory.connect(shareTokenAddr, this.provider!); let d18ShareTokenBalanceOfAddr = await shareToken.balanceOf(address, overrides || {}); return 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(); */ public async getShareTokenPrice(symbolOrId: string | number, overrides?: Overrides): Promise<number> { let poolId = this._poolSymbolOrIdToPoolId(symbolOrId); const priceDec18 = await this.proxyContract!.getShareTokenPriceD18(poolId, overrides || {}); const price = 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(); */ public async getParticipationValue( address: string, symbolOrId: string | number, overrides?: Overrides ): Promise<{ value: number; shareTokenBalance: number; poolShareToken: string }> { let poolId = this._poolSymbolOrIdToPoolId(symbolOrId); const shareTokens = await this._getPoolShareTokenBalanceFromId(address, poolId, overrides); const priceDec18 = await this.proxyContract!.getShareTokenPriceD18(poolId, overrides || {}); const price = 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 */ private _poolSymbolOrIdToPoolId(poolSymbolOrId: string | number): number { if (this.proxyContract == null || this.poolStaticInfos.length == 0) { throw Error("no proxy contract or wallet or data initialized. Use createProxyInstance()."); } let poolId: number; if (isNaN(Number(poolSymbolOrId))) { poolId = PerpetualDataHandler._getPoolIdFromSymbol(poolSymbolOrId as string, 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(); */ public async maxOrderSizeForTrader( traderAddr: string, symbol: string, overrides?: Overrides ): Promise<{ buy: number; sell: number }> { 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 */