@d8x/perpetuals-sdk 0.0.21 → 0.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/d8XMath.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { assert } from "console";
1
2
  import { BigNumber } from "ethers";
2
3
  import { DECIMALS, ONE_64x64 } from "./nodeSDKTypes";
3
4
 
@@ -177,3 +178,87 @@ export function calculateLiquidationPriceCollateralQuote(
177
178
  let denominator = maintenance_margin_rate * Math.abs(position) - position;
178
179
  return numerator / denominator;
179
180
  }
181
+
182
+ /**
183
+ *
184
+ * @param targetLeverage Leverage of the resulting position. It must be positive unless the resulting position is closed.
185
+ * @param currentPosition Current position size, in base currency, signed.
186
+ * @param currentLockedInValue Current locked in value, average entry price times position size, in quote currency.
187
+ * @param tradeAmount Trade amount, in base currency, signed.
188
+ * @param markPrice Mark price, positive.
189
+ * @param indexPriceS2 Index price, positive.
190
+ * @param indexPriceS3 Collateral index price, positive.
191
+ * @param tradePrice Expected price to trade tradeAmount.
192
+ * @param feeRate
193
+ * @returns Total collateral amount needed for the new position to have he desired leverage.
194
+ */
195
+ export function getMarginRequiredForLeveragedTrade(
196
+ targetLeverage: number | undefined,
197
+ currentPosition: number,
198
+ currentLockedInValue: number,
199
+ tradeAmount: number,
200
+ markPrice: number,
201
+ indexPriceS2: number,
202
+ indexPriceS3: number,
203
+ tradePrice: number,
204
+ feeRate: number
205
+ ): number {
206
+ // we solve for margin in:
207
+ // |new position| * Sm / leverage + fee rate * |trade amount| * S2 = margin * S3 + current position * Sm - L + trade amount * (Sm - trade price)
208
+ // --> M S3 = |P'|Sm/L + FeeQC - PnL + (P'-P)(Price - Sm) = pos value / leverage + fees + price impact - pnl
209
+ let isClosing =
210
+ currentPosition != 0 && currentPosition * tradeAmount < 0 && currentPosition * (currentPosition + tradeAmount) >= 0;
211
+ let feesCC = (feeRate * Math.abs(tradeAmount) * indexPriceS2) / indexPriceS3;
212
+ let collRequired = feesCC;
213
+
214
+ if (!isClosing) {
215
+ if (targetLeverage == undefined || targetLeverage <= 0) {
216
+ throw Error("opening trades must have positive leverage");
217
+ }
218
+ // unrealized pnl (could be + or -) - price impact premium (+)
219
+ let pnlQC = currentPosition * markPrice - currentLockedInValue - tradeAmount * (tradePrice - markPrice);
220
+ collRequired +=
221
+ Math.max(0, (Math.abs(currentPosition + tradeAmount) * markPrice) / targetLeverage - pnlQC) / indexPriceS3;
222
+ }
223
+ return collRequired;
224
+ }
225
+
226
+ export function getMaxSignedPositionSize(
227
+ marginCollateral: number,
228
+ currentPosition: number,
229
+ currentLockedInValue: number,
230
+ direction: number,
231
+ limitPrice: number,
232
+ initialMarginRate: number,
233
+ feeRate: number,
234
+ markPrice: number,
235
+ indexPriceS2: number,
236
+ indexPriceS3: number
237
+ ): number {
238
+ // we solve for new position in:
239
+ // |new position| * Sm / leverage + fee rate * |trade amount| * S2 = margin * S3 + current position * Sm - L + trade amount * (Sm - entry price)
240
+ // |trade amount| = (new position - current position) * direction
241
+ let availableCash = marginCollateral * indexPriceS3 + currentPosition * markPrice - currentLockedInValue;
242
+ let effectiveMarginRate =
243
+ markPrice * initialMarginRate + feeRate * indexPriceS2 + direction * (limitPrice - markPrice);
244
+
245
+ return availableCash / effectiveMarginRate;
246
+ }
247
+
248
+ export function getNewPositionLeverage(
249
+ tradeAmount: number,
250
+ marginCollateral: number,
251
+ currentPosition: number,
252
+ currentLockedInValue: number,
253
+ indexPriceS2: number,
254
+ indexPriceS3: number,
255
+ markPrice: number,
256
+ limitPrice: number,
257
+ feeRate: number
258
+ ): number {
259
+ let newPosition = tradeAmount + currentPosition;
260
+ let pnlQC = currentPosition * markPrice - currentLockedInValue + tradeAmount * (markPrice - limitPrice);
261
+ return (
262
+ (Math.abs(newPosition) * indexPriceS2) / (marginCollateral * indexPriceS3 + pnlQC - feeRate * Math.abs(tradeAmount))
263
+ );
264
+ }
@@ -149,12 +149,4 @@ export default class LiquidityProviderTool extends WriteAccessHandler {
149
149
  });
150
150
  return tx;
151
151
  }
152
-
153
- /*
154
- TODO:
155
- - add liquidity
156
- addLiquidity(uint8 _poolId, int128 _fTokenAmount)
157
- - remove liquidity
158
- function removeLiquidity(uint8 _poolId, int128 _fShareAmount) external override nonReentrant
159
- */
160
152
  }
package/src/marketData.ts CHANGED
@@ -8,10 +8,24 @@ import {
8
8
  COLLATERAL_CURRENCY_QUANTO,
9
9
  PERP_STATE_STR,
10
10
  ZERO_ADDRESS,
11
+ PoolStaticInfo,
12
+ BUY_SIDE,
13
+ CLOSED_SIDE,
14
+ SELL_SIDE,
15
+ CollaterlCCY,
11
16
  } from "./nodeSDKTypes";
12
17
  import { BigNumber, BytesLike, ethers } from "ethers";
13
- import { floatToABK64x64, ABK64x64ToFloat } from "./d8XMath";
14
- import { fromBytes4HexString, toBytes4 } from "./utils";
18
+ import {
19
+ floatToABK64x64,
20
+ ABK64x64ToFloat,
21
+ getNewPositionLeverage,
22
+ getMarginRequiredForLeveragedTrade,
23
+ calculateLiquidationPriceCollateralBase,
24
+ calculateLiquidationPriceCollateralQuanto,
25
+ calculateLiquidationPriceCollateralQuote,
26
+ getMaxSignedPositionSize,
27
+ } from "./d8XMath";
28
+ import { contractSymbolToSymbol, fromBytes4HexString, toBytes4 } from "./utils";
15
29
  import PerpetualDataHandler from "./perpetualDataHandler";
16
30
  import { SmartContractOrder, Order } from "./nodeSDKTypes";
17
31
  import "./nodeSDKTypes";
@@ -60,6 +74,15 @@ export default class MarketData extends PerpetualDataHandler {
60
74
  await this.initContractsAndData(this.provider);
61
75
  }
62
76
 
77
+ /**
78
+ * Convert the smart contract output of an order into a convenient format of type "Order"
79
+ * @param smOrder SmartContractOrder, as obtained e.g., by PerpetualLimitOrderCreated event
80
+ * @returns more convenient format of order, type "Order"
81
+ */
82
+ public smartContractOrderToOrder(smOrder: SmartContractOrder): Order {
83
+ return PerpetualDataHandler.fromSmartContractOrder(smOrder, this.symbolToPerpStaticInfo);
84
+ }
85
+
63
86
  /**
64
87
  * Get contract instance. Useful for event listening.
65
88
  * @example
@@ -107,7 +130,7 @@ export default class MarketData extends PerpetualDataHandler {
107
130
  if (this.proxyContract == null) {
108
131
  throw Error("no proxy contract initialized. Use createProxyInstance().");
109
132
  }
110
- return await MarketData._exchangeInfo(this.proxyContract);
133
+ return await MarketData._exchangeInfo(this.proxyContract, this.poolStaticInfos, this.symbolList);
111
134
  }
112
135
 
113
136
  /**
@@ -142,9 +165,9 @@ export default class MarketData extends PerpetualDataHandler {
142
165
  }
143
166
 
144
167
  /**
145
- * Information about the positions open by a given trader in a given perpetual contract.
168
+ * Information about the position open by a given trader in a given perpetual contract.
146
169
  * @param {string} traderAddr Address of the trader for which we get the position risk.
147
- * @param {string} symbol Symbol of the form ETH-USD-MATIC.
170
+ * @param {string} symbol Symbol of the form ETH-USD-MATIC. Can also be the perpetual id as string
148
171
  * @example
149
172
  * import { MarketData, PerpetualDataHandler } from '@d8x/perpetuals-sdk';
150
173
  * async function main() {
@@ -175,6 +198,151 @@ export default class MarketData extends PerpetualDataHandler {
175
198
  return mgnAcct;
176
199
  }
177
200
 
201
+ public async positionRiskOnTrade(
202
+ traderAddr: string,
203
+ order: Order,
204
+ perpetualState: PerpetualState,
205
+ currentPositionRisk?: MarginAccount
206
+ ): Promise<MarginAccount> {
207
+ if (this.proxyContract == null) {
208
+ throw Error("no proxy contract initialized. Use createProxyInstance().");
209
+ }
210
+ if (currentPositionRisk == undefined) {
211
+ currentPositionRisk = await this.positionRisk(traderAddr, order.symbol);
212
+ }
213
+ let tradeAmount = order.quantity * (order.side == BUY_SIDE ? 1 : -1);
214
+ let currentPosition = currentPositionRisk.positionNotionalBaseCCY;
215
+ let newPosition = currentPositionRisk.positionNotionalBaseCCY + tradeAmount;
216
+ let side = newPosition > 0 ? BUY_SIDE : newPosition < 0 ? SELL_SIDE : CLOSED_SIDE;
217
+ let lockedInValue = currentPositionRisk.entryPrice * currentPosition;
218
+ let poolId = PerpetualDataHandler._getPoolIdFromSymbol(order.symbol, this.poolStaticInfos);
219
+
220
+ // total fee rate = exchange fee + broker fee
221
+ let feeRate =
222
+ (await this.proxyContract.queryExchangeFee(poolId, traderAddr, order.brokerAddr ?? ZERO_ADDRESS)) +
223
+ (order.brokerFeeTbps ?? 0) / 100_000;
224
+
225
+ // price for this order = limit price (conservative) if given, else the current perp price
226
+ let tradePrice = order.limitPrice ?? (await this.getPerpetualPrice(order.symbol, tradeAmount));
227
+
228
+ // need these for leverage/margin calculations
229
+ let [markPrice, indexPriceS2, indexPriceS3] = [
230
+ perpetualState.markPrice,
231
+ perpetualState.indexPrice,
232
+ perpetualState.collToQuoteIndexPrice,
233
+ ];
234
+ let newCollateral: number;
235
+ let newLeverage: number;
236
+ if (order.keepPositionLvg) {
237
+ // we have a target leverage for the resulting position
238
+ // this gives us the total margin needed in the account so that it satisfies the leverage condition
239
+ newCollateral = getMarginRequiredForLeveragedTrade(
240
+ currentPositionRisk.leverage,
241
+ currentPosition,
242
+ lockedInValue,
243
+ tradeAmount,
244
+ markPrice,
245
+ indexPriceS2,
246
+ indexPriceS3,
247
+ tradePrice,
248
+ feeRate
249
+ );
250
+ // the new leverage follows from the updated margin and position
251
+ newLeverage = getNewPositionLeverage(
252
+ tradeAmount,
253
+ newCollateral,
254
+ currentPosition,
255
+ lockedInValue,
256
+ indexPriceS2,
257
+ indexPriceS3,
258
+ markPrice,
259
+ tradePrice,
260
+ feeRate
261
+ );
262
+ } else {
263
+ // the order has its own leverage and margin requirements
264
+ let tradeCollateral = getMarginRequiredForLeveragedTrade(
265
+ order.leverage,
266
+ 0,
267
+ 0,
268
+ tradeAmount,
269
+ markPrice,
270
+ indexPriceS2,
271
+ indexPriceS3,
272
+ tradePrice,
273
+ feeRate
274
+ );
275
+ newCollateral = currentPositionRisk.collateralCC + tradeCollateral;
276
+ // the new leverage corresponds to increasing the position and collateral according to the order
277
+ newLeverage = getNewPositionLeverage(
278
+ tradeAmount,
279
+ newCollateral,
280
+ currentPosition,
281
+ lockedInValue,
282
+ indexPriceS2,
283
+ indexPriceS3,
284
+ markPrice,
285
+ tradePrice,
286
+ feeRate
287
+ );
288
+ }
289
+ let newLockedInValue = lockedInValue + tradeAmount * tradePrice;
290
+
291
+ // liquidation vars
292
+ let S2Liq: number, S3Liq: number | undefined;
293
+ let tau = this.symbolToPerpStaticInfo.get(order.symbol)!.maintenanceMarginRate;
294
+ let ccyType = this.symbolToPerpStaticInfo.get(order.symbol)!.collateralCurrencyType;
295
+ if (ccyType == CollaterlCCY.BASE) {
296
+ S2Liq = calculateLiquidationPriceCollateralBase(newLockedInValue, newPosition, newCollateral, tau);
297
+ S3Liq = S2Liq;
298
+ } else if (ccyType == CollaterlCCY.QUANTO) {
299
+ S3Liq = indexPriceS3;
300
+ S2Liq = calculateLiquidationPriceCollateralQuanto(
301
+ newLockedInValue,
302
+ newPosition,
303
+ newCollateral,
304
+ tau,
305
+ indexPriceS3,
306
+ markPrice
307
+ );
308
+ } else {
309
+ S2Liq = calculateLiquidationPriceCollateralQuote(newLockedInValue, newPosition, newCollateral, tau);
310
+ }
311
+ let newPositionRisk: MarginAccount = {
312
+ symbol: currentPositionRisk.symbol,
313
+ positionNotionalBaseCCY: newPosition,
314
+ side: side,
315
+ entryPrice: Math.abs(newLockedInValue / newPosition),
316
+ leverage: newLeverage,
317
+ markPrice: markPrice,
318
+ unrealizedPnlQuoteCCY: tradeAmount * (markPrice - tradePrice),
319
+ unrealizedFundingCollateralCCY: currentPositionRisk.unrealizedFundingCollateralCCY,
320
+ collateralCC: newCollateral,
321
+ collToQuoteConversion: indexPriceS3,
322
+ liquidationPrice: [S2Liq, S3Liq],
323
+ liquidationLvg: 1 / tau,
324
+ };
325
+ return newPositionRisk;
326
+ }
327
+
328
+ public maxOrderSizeForTrader(side: string, positionRisk: MarginAccount, perpetualState: PerpetualState): number {
329
+ let initialMarginRate = this.symbolToPerpStaticInfo.get(positionRisk.symbol)!.initialMarginRate;
330
+ // fees not considered here
331
+ let maxPosition = getMaxSignedPositionSize(
332
+ positionRisk.collateralCC,
333
+ positionRisk.positionNotionalBaseCCY,
334
+ positionRisk.entryPrice * positionRisk.positionNotionalBaseCCY,
335
+ side == BUY_SIDE ? 1 : -1,
336
+ perpetualState.markPrice,
337
+ initialMarginRate,
338
+ 0,
339
+ perpetualState.markPrice,
340
+ perpetualState.indexPrice,
341
+ perpetualState.collToQuoteIndexPrice
342
+ );
343
+ return maxPosition - positionRisk.positionNotionalBaseCCY;
344
+ }
345
+
178
346
  /**
179
347
  * Uses the Oracle(s) in the exchange to get the latest price of a given index in a given currency, if a route exists.
180
348
  * @param {string} base Index name, e.g. ETH.
@@ -261,6 +429,49 @@ export default class MarketData extends PerpetualDataHandler {
261
429
  );
262
430
  }
263
431
 
432
+ /**
433
+ * Query recent perpetual state from blockchain
434
+ * @param symbol symbol of the form ETH-USD-MATIC
435
+ * @returns PerpetualState reference
436
+ */
437
+ public async getPerpetualState(symbol: string): Promise<PerpetualState> {
438
+ if (this.proxyContract == null) {
439
+ throw Error("no proxy contract initialized. Use createProxyInstance().");
440
+ }
441
+ let state: PerpetualState = await PerpetualDataHandler._queryPerpetualState(
442
+ symbol,
443
+ this.symbolToPerpStaticInfo,
444
+ this.proxyContract
445
+ );
446
+ return state;
447
+ }
448
+
449
+ /**
450
+ * get the current mid-price for a perpetual
451
+ * @param symbol symbol of the form ETH-USD-MATIC
452
+ * @example
453
+ * import { MarketData, PerpetualDataHandler } from '@d8x/perpetuals-sdk';
454
+ * async function main() {
455
+ * console.log(MarketData);
456
+ * // setup
457
+ * const config = PerpetualDataHandler.readSDKConfig("testnet");
458
+ * let mktData = new MarketData(config);
459
+ * await mktData.createProxyInstance();
460
+ * // get perpetual mid price
461
+ * let midPrice = await mktData.getPerpetualMidPrice("ETH-USD-MATIC");
462
+ * console.log(midPrice);
463
+ * }
464
+ * main();
465
+ *
466
+ * @returns {number} price
467
+ */
468
+ public async getPerpetualMidPrice(symbol: string): Promise<number> {
469
+ if (this.proxyContract == null) {
470
+ throw Error("no proxy contract initialized. Use createProxyInstance().");
471
+ }
472
+ return await this.getPerpetualPrice(symbol, 0);
473
+ }
474
+
264
475
  /**
265
476
  * Query smart contract to get user orders and convert to user friendly order format.
266
477
  * @param {string} traderAddr Address of trader.
@@ -298,7 +509,11 @@ export default class MarketData extends PerpetualDataHandler {
298
509
  return digests;
299
510
  }
300
511
 
301
- public static async _exchangeInfo(_proxyContract: ethers.Contract): Promise<ExchangeInfo> {
512
+ public static async _exchangeInfo(
513
+ _proxyContract: ethers.Contract,
514
+ _poolStaticInfos: Array<PoolStaticInfo>,
515
+ _symbolList: Array<{ [key: string]: string }>
516
+ ): Promise<ExchangeInfo> {
302
517
  let nestedPerpetualIDs = await PerpetualDataHandler.getNestedPerpetualIds(_proxyContract);
303
518
  let factory = await _proxyContract.getOracleFactory();
304
519
  let info: ExchangeInfo = { pools: [], oracleFactoryAddr: factory };
@@ -308,6 +523,7 @@ export default class MarketData extends PerpetualDataHandler {
308
523
  let pool = await _proxyContract.getLiquidityPool(j + 1);
309
524
  let PoolState: PoolState = {
310
525
  isRunning: pool.isRunning,
526
+ poolSymbol: _poolStaticInfos[j].poolMarginSymbol,
311
527
  marginTokenAddr: pool.marginTokenAddress,
312
528
  poolShareTokenAddr: pool.shareTokenAddress,
313
529
  defaultFundCashCC: ABK64x64ToFloat(pool.fDefaultFundCashCC),
@@ -320,6 +536,7 @@ export default class MarketData extends PerpetualDataHandler {
320
536
  for (var k = 0; k < perpetualIDs.length; k++) {
321
537
  let perp = await _proxyContract.getPerpetual(perpetualIDs[k]);
322
538
  let fIndexS2 = await _proxyContract.getOraclePrice([perp.S2BaseCCY, perp.S2QuoteCCY]);
539
+ let fMidPrice = await _proxyContract.queryPerpetualPrice(perpetualIDs[k], BigNumber.from(0));
323
540
  let indexS2 = ABK64x64ToFloat(fIndexS2);
324
541
  let indexS3 = 1;
325
542
  if (perp.eCollateralCurrency == COLLATERAL_CURRENCY_BASE) {
@@ -333,11 +550,12 @@ export default class MarketData extends PerpetualDataHandler {
333
550
  let PerpetualState: PerpetualState = {
334
551
  id: perp.id,
335
552
  state: state,
336
- baseCurrency: fromBytes4HexString(perp.S2BaseCCY),
337
- quoteCurrency: fromBytes4HexString(perp.S2QuoteCCY),
553
+ baseCurrency: contractSymbolToSymbol(perp.S2BaseCCY, _symbolList)!,
554
+ quoteCurrency: contractSymbolToSymbol(perp.S2QuoteCCY, _symbolList)!,
338
555
  indexPrice: indexS2,
339
556
  collToQuoteIndexPrice: indexS3,
340
557
  markPrice: indexS2 * (1 + markPremiumRate),
558
+ midPrice: ABK64x64ToFloat(fMidPrice),
341
559
  currentFundingRateBps: currentFundingRateBps,
342
560
  openInterestBC: ABK64x64ToFloat(perp.fOpenInterest),
343
561
  maxPositionBC: ABK64x64ToFloat(perp.fMaxPositionBC),
@@ -39,6 +39,7 @@ export interface NodeSDKConfig {
39
39
  limitOrderBookFactoryAddr: string;
40
40
  limitOrderBookABILocation: string;
41
41
  limitOrderBookFactoryABILocation: string;
42
+ symbolListLocation: string;
42
43
  gasLimit?: number | undefined;
43
44
  }
44
45
 
@@ -110,6 +111,7 @@ export interface ExchangeInfo {
110
111
  */
111
112
  export interface PoolState {
112
113
  isRunning: boolean;
114
+ poolSymbol: string;
113
115
  marginTokenAddr: string;
114
116
  poolShareTokenAddr: string;
115
117
  defaultFundCashCC: number;
@@ -128,6 +130,7 @@ export interface PerpetualState {
128
130
  indexPrice: number;
129
131
  collToQuoteIndexPrice: number;
130
132
  markPrice: number;
133
+ midPrice: number;
131
134
  currentFundingRateBps: number;
132
135
  openInterestBC: number;
133
136
  maxPositionBC: number;
@@ -138,6 +141,11 @@ export interface OrderResponse {
138
141
  orderId: string;
139
142
  }
140
143
 
144
+ export interface OrderStruct {
145
+ orders: Order[];
146
+ orderIds: string[];
147
+ }
148
+
141
149
  export interface Order {
142
150
  symbol: string;
143
151
  side: string;
@@ -156,6 +164,14 @@ export interface Order {
156
164
  submittedBlock?: number;
157
165
  }
158
166
 
167
+ export interface TradeEvent {
168
+ perpetualId: number;
169
+ positionId: string;
170
+ orderId: string;
171
+ newPositionSizeBC: number;
172
+ executionPrice: number;
173
+ }
174
+
159
175
  export interface SmartContractOrder {
160
176
  flags: BigNumberish;
161
177
  iPerpetualId: BigNumberish;
@@ -234,9 +234,6 @@ export default class OrderReferrerTool extends WriteAccessHandler {
234
234
  if (this.proxyContract == null) {
235
235
  throw Error("no proxy contract initialized. Use createProxyInstance().");
236
236
  }
237
- if (order.limitPrice == undefined) {
238
- throw Error("order does not have a limit price");
239
- }
240
237
  // check expiration date
241
238
  if (order.deadline != undefined && order.deadline < Date.now() / 1000) {
242
239
  return false;
@@ -245,7 +242,10 @@ export default class OrderReferrerTool extends WriteAccessHandler {
245
242
  if (order.quantity < PerpetualDataHandler._getLotSize(order.symbol, this.symbolToPerpStaticInfo)) {
246
243
  return false;
247
244
  }
248
- // check limit price
245
+ // check limit price, which may be undefined if it's an unrestricted market order
246
+ if (order.limitPrice == undefined) {
247
+ order.limitPrice = order.side == BUY_SIDE ? Infinity : 0;
248
+ }
249
249
  let orderPrice = await PerpetualDataHandler._queryPerpetualPrice(
250
250
  order.symbol,
251
251
  order.quantity,