@d8x/perpetuals-sdk
Version:
Node TypeScript SDK for D8X Perpetual Futures
995 lines • 102 kB
JavaScript
import { Contract, formatUnits, hexlify, Interface, JsonRpcProvider } from "ethers";
import { BUY_SIDE, CLOSED_SIDE, COLLATERAL_CURRENCY_BASE, COLLATERAL_CURRENCY_QUANTO, ERC20_ABI, MULTICALL_ADDRESS, ORDER_TYPE_MARKET, PERP_STATE_STR, SELL_SIDE, ZERO_ADDRESS, ZERO_ORDER_ID, } from "./constants";
import { CompositeToken__factory, ERC20__factory, IPerpetualManager__factory, Multicall3__factory, } from "./contracts";
import { ABK64x64ToFloat, dec18ToFloat, decNToFloat, extractLvgFeeParams, floatToABK64x64, getDepositAmountForLvgTrade, getMaxSignedPositionSize, pmExitFee, pmMaxSignedOpenTradeSize, pmOpenFee, } from "./d8XMath";
import PerpetualDataHandler from "./perpetualDataHandler";
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();
*
*/
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 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
*/
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.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 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);
}
/**
* 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 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 !== 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.
*/
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 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.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
*/
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.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;
}
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.symbolToPerpetualId(symbol, this.symbolToPerpStaticInfo);
const [fS2, fS3, fEma] = [indexPriceInfo.s2, indexPriceInfo.s3 ?? 0, indexPriceInfo.ema].map((x) => 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,
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 || {}));
// 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, 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];
const maxLongTrade = ABK64x64ToFloat(fMaxLong);
// max sell
const fMaxShort = this.proxyContract.interface.decodeFunctionResult("getMaxSignedOpenTradeSizeForPos", encodedResults[3].returnData)[0];
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();
*/
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 == 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 == 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;
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 = 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 } = 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();
*/
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 == 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] = 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();
*/
async getWalletBalance(address, symbol, overrides) {
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();
*/
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 = 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();
*/
async getShareTokenPrice(symbolOrId, overrides) {
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();
*/
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 = 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._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.symbolToPerpetualId(symbol, this.symbolToPerpStaticInfo);
const indexPriceInfo = await this.priceFeedGetter.fetchPricesForPerpetual(symbol);
const [fS3, fEma] = [indexPriceInfo.s3 ?? 0, indexPriceInfo.ema].map((x) => floatToABK64x64(x));
const traderState = await this.proxyContract.getTraderState(perpId, traderAddr, [fEma, fS3], overrides || {});
const ammState = await this.proxyContract.getAMMState(perpId, [fEma, fS3], overrides || {});
const idxNotional = 4;
const idxMaintenanceMarginRate = 10;
const traderPos = ABK64x64ToFloat(traderState[idxNotional]);
const mr = ABK64x64ToFloat(traderState[idxMaintenanceMarginRate]);
// const idxAMMFunds = [2,3,4]; // M1, M2, M3, but pred can't be base, M2 = 0
const idxK2 = 1; // ammpos = -K2
const idxOI = 11;
const ne