Repository Public
1// GMX v2 wire formats (zod) and exact unit conversions.2// Sources: GMX API OpenAPI spec https://arbitrum.gmxapi.io/swagger.json, Oracle API https://docs.gmx.io/docs/api/rest-api/oracle-prices/3// Shapes checked against the live endpoints on 2026-11-10 (fixtures in packages/derivatives/test/fixtures/gmx/).4//5// Units (all integers in decimal strings, converted with bigint only):6// USD value × 10^30 sizeInUsd, collateralUsd, open interest, liquidity, pnl, fees in USD7// GMX API price × 10^30 per WHOLE token /markets/tickers, /prices/tickers, positions (mark, entry, liquidation)8// contract price × 10^(30 − tokenDecimals) Oracle API /prices/tickers, order trigger/acceptable price, trade execution price9// token amount × 10^tokenDecimals sizeInTokens, collateralAmount, fee amounts10// factor / rate × 10^30 funding, borrowing, impact and fee factors11// leverage, percentage basis points (10 000 = 1×) leverage, leverageTiers[].maxLeverage, pnlPercentage12import { z } from 'zod';13import { fixed, JsonNumber } from '../http.ts';1415export const USD_DECIMALS = 30;16export const MAX_UINT256 = (1n << 256n) - 1n;17const intStr = z.string().regex(/^-?\d+$/);18const uintStr = z.string().regex(/^\d+$/);19export const address = z.string().regex(/^0x[0-9a-fA-F]{40}$/);20const bytes32 = z.string().regex(/^0x[0-9a-fA-F]{64}$/);21const jsonNumber = z.instanceof(JsonNumber);2223export const market = z.object({24 symbol: z.string().min(1), marketTokenAddress: address, indexTokenAddress: address, longTokenAddress: address, shortTokenAddress: address,25 isListed: z.boolean(), isSpotOnly: z.boolean(),26 leverageTiers: z.array(z.object({ maxLeverage: uintStr, minCollateralFactor: uintStr, maxPositionSize: uintStr.optional() })).min(1),27 minPositionSizeUsd: uintStr, minCollateralUsd: uintStr,28});29export type Market = z.infer<typeof market>;30export const markets = z.array(market);3132const capacity = z.object({ availableLiquidity: intStr, baseAvailableLiquidity: intStr, jitAvailableLiquidity: intStr, limitingFactor: z.string(), jitDataStatus: z.string(), marketDataStatus: z.string() });33export const marketTicker = z.object({34 symbol: z.string().min(1), marketTokenAddress: address, minPrice: uintStr, maxPrice: uintStr, markPrice: uintStr,35 priceChangePercent24hBps: intStr, longInterestInTokens: uintStr, shortInterestInTokens: uintStr, longInterestUsd: uintStr, shortInterestUsd: uintStr,36 availableLiquidityLong: intStr, availableLiquidityShort: intStr, // negative when a side is over its reserve (seen live)37 fundingRateLong: intStr, fundingRateShort: intStr, borrowingRateLong: intStr, borrowingRateShort: intStr, netRateLong: intStr, netRateShort: intStr,38 capacityLong: capacity, capacityShort: capacity,39});40export type MarketTicker = z.infer<typeof marketTicker>;41export const marketTickers = z.array(marketTicker);4243export const marketConfig = z.object({44 name: z.string(), marketTokenAddress: address, isDisabled: z.boolean(),45 maxOpenInterestLong: uintStr, maxOpenInterestShort: uintStr,46 minCollateralFactor: uintStr, minCollateralFactorForLiquidation: uintStr, minCollateralFactorForOpenInterestLong: uintStr, minCollateralFactorForOpenInterestShort: uintStr,47 positionFeeFactorForBalanceWasImproved: uintStr, positionFeeFactorForBalanceWasNotImproved: uintStr,48 positionImpactFactorPositive: uintStr, positionImpactFactorNegative: uintStr,49 positionImpactExponentFactorPositive: uintStr, positionImpactExponentFactorNegative: uintStr,50 maxPositionImpactFactorPositive: uintStr, maxPositionImpactFactorNegative: uintStr, maxPositionImpactFactorForLiquidations: uintStr,51 /** when true the contracts measure the long/short imbalance (price impact, fee tier) on open interest IN TOKENS × price, not on the USD cost basis */52 useOpenInterestInTokensForBalance: z.boolean().optional(),53 /** non-zero: the index token has a virtual inventory shared by every market on it (a negative price impact is the worse of the market's own and the virtual one) */54 virtualIndexTokenId: z.string().regex(/^0x[0-9a-fA-F]{64}$/).optional(),55});56export type MarketConfig = z.infer<typeof marketConfig>;57export const marketConfigs = z.array(marketConfig);58/** GET /markets/values — only what the price-impact formula needs. Signed: negative = traders are net LONG across the index's markets (contract convention). */59export const marketValues = z.array(z.object({ marketTokenAddress: address, virtualInventoryForPositionsInTokens: intStr, virtualInventoryForPositions: intStr }));6061export const tokenInfo = z.object({ symbol: z.string().min(1), address, decimals: z.number().int().min(0).max(30), isSynthetic: z.boolean().optional(), isNative: z.boolean().optional() });62export const tokensInfo = z.array(tokenInfo);6364/** /pairs is a listing feed with plain JSON numbers: parsed with `parseJsonExact`, so numbers arrive as their source text */65export const pairs = z.array(z.object({ ticker_id: z.string(), pool_id: z.string(), product_type: z.string(), target_volume: jsonNumber }));6667export const ohlcv = z.array(z.object({ timestamp: z.number().int().positive(), open: z.string(), high: z.string(), low: z.string(), close: z.string() }));68/** Oracle API candles: `[timestamp s, open, high, low, close]` with JSON numbers (parsed exact) */69export const oracleCandles = z.object({ period: z.string(), candles: z.array(z.tuple([jsonNumber, jsonNumber, jsonNumber, jsonNumber, jsonNumber])) });70export const oracleTickers = z.array(z.object({ tokenAddress: address, tokenSymbol: z.string().min(1), minPrice: uintStr, maxPrice: uintStr, updatedAt: z.number(), timestamp: z.number().optional() }));71export const oracleTokens = z.object({ tokens: z.array(z.object({ symbol: z.string().min(1), address, decimals: z.number().int().min(0).max(30), synthetic: z.boolean().optional() })) });7273export const rates = z.array(z.object({ marketAddress: address, ratesSnapshots: z.array(z.object({74 timestamp: z.number().int().positive(), fundingRateLong: intStr, fundingRateShort: intStr, borrowingRateLong: intStr, borrowingRateShort: intStr, netRateLong: intStr, netRateShort: intStr,75})) }));7677export const position = z.object({78 key: z.string().min(1), contractKey: bytes32, account: address, marketAddress: address, collateralTokenAddress: address, isLong: z.boolean(),79 sizeInUsd: uintStr, sizeInTokens: uintStr, collateralAmount: uintStr, increasedAtTime: uintStr, decreasedAtTime: uintStr,80 markPrice: uintStr, entryPrice: uintStr.optional(), liquidationPrice: uintStr.optional(), leverage: uintStr.optional(), leverageWithPnl: uintStr.optional(), leverageWithoutPnl: uintStr.optional(),81 collateralUsd: uintStr, remainingCollateralUsd: intStr, remainingCollateralAmount: intStr, hasLowCollateral: z.boolean(),82 pnl: intStr, pnlAfterFees: intStr, pnlAfterAllFees: intStr, netValue: intStr, netValueAfterAllFees: intStr, positionValueInUsd: intStr,83 pendingBorrowingFeesUsd: intStr, pendingFundingFeesUsd: intStr, pendingClaimableFundingFeesUsd: intStr, fundingFeeAmount: intStr,84 claimableLongTokenAmount: intStr, claimableShortTokenAmount: intStr,85 positionFeeAmount: intStr, traderDiscountAmount: intStr, uiFeeAmount: intStr, closingFeeUsd: intStr, uiFeeUsd: intStr,86 pendingImpactAmount: intStr, pendingImpactUsd: intStr, closePriceImpactDeltaUsd: intStr, priceImpactDiffUsd: intStr, netPriceImapctDeltaUsd: intStr,87});88export type WirePosition = z.infer<typeof position>;89export const positions = z.array(position);9091export const order = z.object({92 key: bytes32, account: address, receiver: address, cancellationReceiver: address, callbackContract: address, uiFeeReceiver: address,93 marketAddress: address, initialCollateralTokenAddress: address, swapPath: z.array(address), orderType: z.number().int().min(0).max(9), decreasePositionSwapType: z.number().int(),94 sizeDeltaUsd: uintStr, initialCollateralDeltaAmount: uintStr, triggerPrice: uintStr, acceptablePrice: uintStr, executionFee: uintStr, callbackGasLimit: uintStr,95 minOutputAmount: uintStr, updatedAtTime: uintStr, validFromTime: uintStr, isLong: z.boolean(), shouldUnwrapNativeToken: z.boolean(), isFrozen: z.boolean(), autoCancel: z.boolean(),96});97export type WireOrder = z.infer<typeof order>;98export const orders = z.array(order);99100export const tradeAction = z.object({101 id: z.string().min(1), eventName: z.string().min(1), account: address, marketAddress: address.optional(), orderType: z.number().int().min(0).max(9), orderKey: bytes32,102 timestamp: z.number().int().positive(), transactionHash: z.string(), swapPath: z.array(address), initialCollateralTokenAddress: address, initialCollateralDeltaAmount: uintStr,103 isLong: z.boolean().optional(), sizeDeltaUsd: uintStr.optional(), sizeDeltaInTokens: uintStr.optional(), triggerPrice: uintStr.optional(), acceptablePrice: uintStr.optional(),104 executionPrice: uintStr.optional(), indexTokenPriceMin: uintStr.optional(), indexTokenPriceMax: uintStr.optional(),105 positionFeeAmount: intStr.optional(), borrowingFeeAmount: intStr.optional(), fundingFeeAmount: intStr.optional(), liquidationFeeAmount: intStr.optional(), traderDiscountAmount: intStr.optional(),106 priceImpactUsd: intStr.optional(), totalImpactUsd: intStr.optional(), proportionalPendingImpactUsd: intStr.optional(), priceImpactDiffUsd: intStr.optional(),107 swapImpactUsd: intStr.optional(), swapFeeUsd: intStr.optional(), basePnlUsd: intStr.optional(), pnlUsd: intStr.optional(), reason: z.string().optional(),108});109export type TradeAction = z.infer<typeof tradeAction>;110export const tradesList = z.object({ trades: z.array(tradeAction), hasMore: z.boolean(), nextCursor: z.string().nullable() });111112export const graphqlTrades = z.object({ data: z.object({ tradeActions: z.array(z.object({113 id: z.string().min(1), orderType: z.number().int(), isLong: z.boolean(), sizeDeltaInTokens: uintStr, executionPrice: uintStr, timestamp: z.number().int().positive(),114})) }) });115116/** GMX order types (contracts/order/Order.sol, enum order) */117export const ORDER_TYPE = ['MarketSwap', 'LimitSwap', 'MarketIncrease', 'LimitIncrease', 'MarketDecrease', 'LimitDecrease', 'StopLossDecrease', 'Liquidation', 'StopIncrease', 'Deposit'] as const;118export const isIncrease = (t: number): boolean => t === 2 || t === 3 || t === 8;119export const isDecrease = (t: number): boolean => t === 4 || t === 5 || t === 6 || t === 7;120121// ---- exact conversions ----122export const usd = (raw: string | bigint): string => fixed(BigInt(raw), USD_DECIMALS);123/** GMX API price (30 decimals per whole token) → USD per token */124export const apiPrice = usd;125/** contract price (× 10^(30 − tokenDecimals)) → USD per whole token */126export function contractPrice(raw: string | bigint, tokenDecimals: number): string {127 if (!Number.isInteger(tokenDecimals) || tokenDecimals < 0 || tokenDecimals > USD_DECIMALS) throw new Error('token decimals out of range');128 return fixed(BigInt(raw), USD_DECIMALS - tokenDecimals);129}130/** USD per whole token → contract price; throws if the price has more precision than the contract can hold */131export function toContractPrice(price: string, tokenDecimals: number): bigint {132 const m = /^(\d+)(?:\.(\d+))?$/.exec(price);133 if (!m) throw new Error(`not a price: ${price.slice(0, 40)}`);134 const d = USD_DECIMALS - tokenDecimals, frac = (m[2] ?? '').replace(/0+$/, '');135 if (frac.length > d) throw new Error(`${price} has more than ${d} decimals`);136 return BigInt(m[1]! + frac.padEnd(d, '0'));137}138export const tokenAmount = (raw: string | bigint, decimals: number): string => fixed(BigInt(raw), decimals);139/** 30-decimal factor or rate → decimal fraction */140export const factor = usd;141/** basis points → multiple (384930 → "38.493") */142export const bps = (raw: string | bigint): string => fixed(BigInt(raw), 4);143/** per-second 30-decimal rate → per-hour fraction (exact: × 3600) */144export const perSecondToHourly = (raw: string | bigint): string => fixed(BigInt(raw) * 3600n, USD_DECIMALS);145/** smallest price step the contracts can represent for a token, in USD per whole token */146export const priceStep = (tokenDecimals: number): string => fixed(1n, USD_DECIMALS - tokenDecimals);147