Repository Public
1// GMX v2 (GMX Synthetics) — READ side of the venue adapter (see docs/v4/venues/gmx.md).2// Plain HTTP on the documented public surfaces: the GMX API (`https://{chain}.gmxapi.io/v1`), the Oracle API3// (`https://{chain}-api.gmxinfra.io`) and the Subsquid GraphQL endpoint for the public trade feed. No SDK, no transaction.4//5// GMX is not an order-book venue: traders open against a liquidity pool at the signed oracle price, adjusted by price impact.6// `book()` therefore throws `unsupported`; what the venue really has is exposed by `liquidity()`.7import { D, type DecStr } from '../../decimal.ts';8import type { AccountState, Book, Candle, Fill, FundingPayment, Instrument, Order, Position, PublicTrade, Ticker } from '../../models.ts';9import type { Capabilities, VenueAdapter } from '../../venue.ts';10import { ds, jsonNumberToDecStr, venueError, venueHttp, type HttpAnswer } from '../http.ts';11import * as W from './wire.ts';12import { z } from 'zod';1314const GMX = 'gmx' as const;15export type GmxChain = 'arbitrum' | 'avalanche';16/** Pinned public endpoints — https://docs.gmx.io/docs/api/overview/ , /rest-api/fallback-urls/ , /graphql/ */17export const GMX_ENDPOINTS: Record<GmxChain, { api: string; apiPeer: string; oracle: string; oracleFallbacks: string[]; graphql: string; chainId: number }> = {18 arbitrum: { api: 'https://arbitrum.gmxapi.io/v1', apiPeer: 'https://arbitrum.gmxapi.ai/v1', oracle: 'https://arbitrum-api.gmxinfra.io',19 oracleFallbacks: ['https://arbitrum-api-fallback.gmxinfra.io', 'https://arbitrum-api-fallback.gmxinfra2.io'],20 graphql: 'https://gmx.squids.live/gmx-synthetics-arbitrum:prod/api/graphql', chainId: 42161 },21 avalanche: { api: 'https://avalanche.gmxapi.io/v1', apiPeer: 'https://avalanche.gmxapi.ai/v1', oracle: 'https://avalanche-api.gmxinfra.io',22 oracleFallbacks: ['https://avalanche-api-fallback.gmxinfra.io', 'https://avalanche-api-fallback.gmxinfra2.io'],23 graphql: 'https://gmx.squids.live/gmx-synthetics-avalanche:prod/api/graphql', chainId: 43114 },24};25/** Funding accrues continuously (per second); the venue quotes it per hour, which is the interval used for `Ticker.funding`. */26export const GMX_FUNDING_INTERVAL_SEC = 3600;27export const GMX_SETTLEMENT = 'isolated position against a GM liquidity pool; PnL is computed in USD and paid in the pool token of the side (long token for longs, short token for shorts) on top of the returned collateral; settlement happens on chain when a keeper executes the order';28export const GMX_MARK_METHOD = 'no order book and no separate mark price: orders execute at the Chainlink Data Streams signed min/max oracle price (longs open at max, close at min) adjusted by price impact; the displayed mark is (min + max) / 2';2930export const GMX_CAPABILITIES: Capabilities = {31 marketData: true, account: true, orders: false, orderTypes: [], tif: [], modify: false, leverage: false,32 marginModes: ['isolated'], subaccounts: false, testnet: false, stream: false,33 notes: [34 'Read-only delivery: no order, approval or transaction is built or sent.',35 'Trading would require two on-chain steps: ExchangeRouter.multicall([sendWnt, sendTokens, createOrder]) signed by the account (or by a registered GMX subaccount, or relayed through GMX Express), then execution by a keeper at a signed oracle price; an order can be cancelled or frozen instead of filled — see docs/v4/venues/gmx.md.',36 'NO ORDER BOOK: book() throws "unsupported". Use liquidity(symbol): available liquidity and open interest per side, price-impact parameters, funding and borrowing rates.',37 'One instrument per GM market: the same index (e.g. BTC/USD) is listed against several pools with different collateral, liquidity and rates; they are distinct instruments.',38 'Instrument.tickSize is the price precision of the contracts (10^-(30 − token decimals)), lotSize the token precision; orders are sized in USD (30 decimals), minNotional is the venue minimum position size. Instrument.maxLeverage is the contract bound published by the API; the effective maximum is lower and falls as open interest grows.',39 'Order.size is in USD on this venue (GMX orders carry sizeDeltaUsd), Position.size and Fill.size are in index tokens. A decrease order for the whole position has gmx.closesWholePosition = true.',40 'Ticker: mark = index = oracle mid; mid is null; funding is the rate longs pay per hour (negative = longs receive); openInterest is long + short in index tokens (the two sides are not matched against each other); volume24h comes from the /pairs listing feed.',41 'Candles carry no volume (GMX publishes none): Candle.volume is "0", not a measurement.',42 'account(): GMX has no account-level margin. equity = sum of position net values, collateral = marginUsed = sum of position collateral, freeCollateral = 0; wallet balances are not venue state.',43 'funding(account) throws "unsupported": funding is settled into collateral at each position change (Fill.gmx.fundingFeeAmount) and pending amounts are on Position.gmx. Swap orders are not derivatives and are left out of order lists.',44 'No documented public websocket; no testnet API host. Avalanche is served by the same code (chain option); MegaETH is not wired.',45 ],46};4748export interface GmxLiquidity {49 instrument: string; symbol: string; market: string;50 /** USD */ openInterestLongUsd: DecStr; openInterestShortUsd: DecStr; maxOpenInterestLongUsd: DecStr; maxOpenInterestShortUsd: DecStr;51 /** open interest in index tokens per side, and whether the contracts use it (× price) to measure the imbalance — `null` when the venue does not say.52 * Recorded proof (test/gmx): the venue's own `closePriceImpactDeltaUsd` is reproduced with tokens × mark, and is 3× off with the USD figures above. */53 openInterestLongTokens: DecStr; openInterestShortTokens: DecStr; useOpenInterestInTokensForBalance: boolean | null;54 /** virtual inventory of the index token for positions, shared by every market on that index. `applies`: the market has one (null: not published).55 * `tokens` / `usd`: its signed value in the contracts' convention — negative = traders are net LONG across those markets — or null when56 * GET /markets/values could not be read. A NEGATIVE price impact is the worse of the market's own and the one computed on this inventory. */57 virtualInventory: { applies: boolean | null; tokens: DecStr | null; usd: DecStr | null };58 /** USD that can still be opened on each side: pool view, then the JIT-aware capacity the venue recommends for sizing */59 availableLiquidityLongUsd: DecStr; availableLiquidityShortUsd: DecStr; capacityLongUsd: DecStr; capacityShortUsd: DecStr; capacityLimitedBy: { long: string; short: string };60 /** per hour, as a fraction; sign as published by the venue: positive = the side RECEIVES, negative = the side PAYS */61 fundingRateLongPerHour: DecStr; fundingRateShortPerHour: DecStr; borrowingRateLongPerHour: DecStr; borrowingRateShortPerHour: DecStr; netRateLongPerHour: DecStr; netRateShortPerHour: DecStr;62 /** price impact = f(change in |long OI − short OI|): factor × imbalance^exponent, capped at the max factors (fractions of size).63 * The imbalance is measured on tokens × price when `useOpenInterestInTokensForBalance` is true (see packages/derivatives/src/router.ts). */64 priceImpact: { factorPositive: DecStr; factorNegative: DecStr; exponentPositive: DecStr; exponentNegative: DecStr; maxFactorPositive: DecStr; maxFactorNegative: DecStr; maxFactorForLiquidations: DecStr };65 positionFeeFactor: { balanceImproved: DecStr; balanceNotImproved: DecStr };66 minCollateralFactor: DecStr; minCollateralFactorForLiquidation: DecStr; minCollateralFactorPerUsdOfOpenInterest: { long: DecStr; short: DecStr };67 oracleMin: DecStr; oracleMax: DecStr; ts: number;68}69export interface GmxPositionDetail {70 key: string; contractKey: string; market: string; collateralToken: string; collateralSymbol: string;71 sizeUsd: DecStr; collateralAmount: DecStr; collateralUsd: DecStr; remainingCollateralUsd: DecStr; netValueUsd: DecStr; netValueAfterAllFeesUsd: DecStr;72 pnlAfterFeesUsd: DecStr; pnlAfterAllFeesUsd: DecStr; hasLowCollateral: boolean;73 pendingBorrowingFeesUsd: DecStr; pendingFundingFeesUsd: DecStr; pendingClaimableFundingFeesUsd: DecStr; closingFeeUsd: DecStr; uiFeeUsd: DecStr;74 pendingImpactUsd: DecStr; closePriceImpactDeltaUsd: DecStr; netPriceImpactDeltaUsd: DecStr; priceImpactDiffUsd: DecStr;75 increasedAt: number; decreasedAt: number;76}77export type GmxPosition = Position & { gmx: GmxPositionDetail };78export interface GmxOrderDetail {79 key: string; orderType: string; isLong: boolean; market: string; sizeUnit: 'usd'; closesWholePosition: boolean;80 collateralToken: string; collateralSymbol: string; collateralDelta: DecStr; swapPath: string[];81 /** native token (wei → 18 decimals) */ executionFee: DecStr | null; isFrozen: boolean; autoCancel: boolean; receiver: string | null; validFrom: number | null; cancelReason: string | null;82}83export type GmxOrder = Order & { gmx: GmxOrderDetail };84export interface GmxFillDetail {85 orderKey: string; orderType: string; txHash: string; isLong: boolean; sizeDeltaUsd: DecStr; feeToken: string;86 positionFeeAmount: DecStr; borrowingFeeAmount: DecStr; fundingFeeAmount: DecStr; liquidationFeeAmount: DecStr; traderDiscountAmount: DecStr;87 priceImpactUsd: DecStr | null; totalImpactUsd: DecStr | null; proportionalPendingImpactUsd: DecStr | null; priceImpactDiffUsd: DecStr | null;88 swapImpactUsd: DecStr | null; swapFeeUsd: DecStr | null; basePnlUsd: DecStr | null; indexPriceMin: DecStr | null; indexPriceMax: DecStr | null;89}90export type GmxFill = Fill & { gmx: GmxFillDetail };9192export interface GmxAdapterOptions { chain?: GmxChain; fetch?: typeof fetch; now?: () => number; apiUrl?: string; oracleUrl?: string; graphqlUrl?: string; cacheMs?: number }93export interface GmxAdapter extends VenueAdapter {94 readonly chain: GmxChain;95 positions(account: string): Promise<GmxPosition[]>;96 openOrders(account: string): Promise<GmxOrder[]>;97 orderHistory(account: string, o?: { limit?: number }): Promise<GmxOrder[]>;98 fills(account: string, o?: { start?: number; limit?: number }): Promise<GmxFill[]>;99 /** what GMX has instead of a book: liquidity, open interest, rates and price-impact parameters of one market (or all) */100 liquidity(symbol?: string): Promise<GmxLiquidity[]>;101 /** raw signed-oracle price range per token from the Oracle API (contract precision converted exactly) */102 oraclePrices(): Promise<{ token: string; symbol: string; min: DecStr; max: DecStr; ts: number }[]>;103}104105interface Ctx { markets: W.Market[]; byAddress: Map<string, W.Market>; bySymbol: Map<string, W.Market>; tokens: Map<string, { symbol: string; decimals: number }> }106const lc = (a: string) => a.toLowerCase();107const ZERO = '0x0000000000000000000000000000000000000000';108109export function createGmxAdapter(o: GmxAdapterOptions = {}): GmxAdapter {110 const chain = o.chain ?? 'arbitrum', now = o.now ?? Date.now, ep = GMX_ENDPOINTS[chain];111 const api = o.apiUrl ?? ep.api, oracle = o.oracleUrl ?? ep.oracle, graphql = o.graphqlUrl ?? ep.graphql, cacheMs = o.cacheMs ?? 60_000;112 const http = venueHttp(GMX, o.fetch);113 const bad = (m: string) => venueError(GMX, 'bad_answer', m);114 const on4xx = (a: HttpAnswer) => {115 let msg = a.text.slice(0, 200);116 try { const p = z.object({ message: z.string() }).safeParse(JSON.parse(a.text)); if (p.success) msg = p.data.message; } catch { /* keep the raw text */ }117 return venueError(GMX, 'rejected', `HTTP ${a.status}: ${msg.slice(0, 200)}`);118 };119 const get = <T>(url: string, schema: z.ZodType<T>, exact = false) => http.get(url, schema, { on4xx, exact });120121 let cache: { at: number; ctx: Promise<Ctx> } | null = null;122 const context = (): Promise<Ctx> => {123 if (cache && now() - cache.at < cacheMs) return cache.ctx;124 const ctx = (async (): Promise<Ctx> => {125 const [ms, ts] = await Promise.all([get(`${api}/markets`, W.markets), get(`${api}/tokens/info`, W.tokensInfo)]);126 return { markets: ms, byAddress: new Map(ms.map((m) => [lc(m.marketTokenAddress), m])), bySymbol: new Map(ms.map((m) => [m.symbol, m])),127 tokens: new Map(ts.map((t) => [lc(t.address), { symbol: t.symbol, decimals: t.decimals }])) };128 })();129 cache = { at: now(), ctx };130 ctx.catch(() => { if (cache?.ctx === ctx) cache = null; });131 return ctx;132 };133 const token = (c: Ctx, addr: string) => { const t = c.tokens.get(lc(addr)); if (!t) throw bad(`unknown token ${addr}`); return t; };134 const perp = (c: Ctx, symbol: string) => {135 const m = c.bySymbol.get(symbol);136 if (!m || m.isSpotOnly) throw venueError(GMX, 'rejected', `unknown perpetual market "${symbol.slice(0, 60)}"`);137 return m;138 };139 const marketAt = (c: Ctx, addr: string) => { const m = c.byAddress.get(lc(addr)); if (!m) throw bad(`unknown market ${addr}`); return m; };140 const instrumentId = (symbol: string) => `${GMX}:${symbol}`;141 const baseOf = (symbol: string) => { const i = symbol.indexOf('/'); if (i <= 0) throw bad(`market symbol ${symbol}`); return symbol.slice(0, i); };142 const indexDecimals = (c: Ctx, m: W.Market) => token(c, m.indexTokenAddress).decimals;143 const evm = (account: string) => { if (!W.address.safeParse(account).success) throw venueError(GMX, 'rejected', 'account must be an EVM address'); return account; };144 /** token a swap path ends in (each hop swaps the market's long token for its short token or the reverse) */145 const afterSwaps = (c: Ctx, tokenIn: string, path: string[]): string => path.reduce((t, mk) => {146 const m = marketAt(c, mk);147 return lc(t) === lc(m.longTokenAddress) ? m.shortTokenAddress : m.longTokenAddress;148 }, tokenIn);149150 const instrumentOf = (c: Ctx, m: W.Market): Instrument => {151 const dec = indexDecimals(c, m), long = token(c, m.longTokenAddress).symbol, short = token(c, m.shortTokenAddress).symbol;152 return {153 venue: GMX, symbol: m.symbol, id: instrumentId(m.symbol), base: baseOf(m.symbol), quote: 'USD', kind: 'perpetual',154 collateral: long === short ? long : `${long} or ${short}`, settlement: GMX_SETTLEMENT,155 tickSize: W.priceStep(dec), lotSize: W.tokenAmount(1n, dec), minOrderSize: null, minNotional: W.usd(m.minPositionSizeUsd),156 maxLeverage: W.bps(m.leverageTiers[0]!.maxLeverage), marginModes: ['isolated'],157 fundingIntervalSec: GMX_FUNDING_INTERVAL_SEC, markPriceMethod: GMX_MARK_METHOD, status: m.isListed ? 'active' : 'halted',158 };159 };160161 const ORDER_KIND: Record<number, Order['type']> = { 2: 'market', 3: 'limit', 4: 'market', 5: 'take_profit_market', 6: 'stop_market', 7: 'market', 8: 'stop_market' };162 const orderShape = (c: Ctx, w: { orderType: number; isLong: boolean; marketAddress: string; sizeDeltaUsd: string; triggerPrice: string; acceptablePrice: string;163 initialCollateralTokenAddress: string; initialCollateralDeltaAmount: string; swapPath: string[] }, key: string) => {164 const type = ORDER_KIND[w.orderType];165 if (!type) throw venueError(GMX, 'unsupported', `order ${key}: type ${W.ORDER_TYPE[w.orderType] ?? w.orderType} is not a position order`);166 const m = marketAt(c, w.marketAddress), dec = indexDecimals(c, m), increase = W.isIncrease(w.orderType);167 const whole = BigInt(w.sizeDeltaUsd) === W.MAX_UINT256, acceptable = BigInt(w.acceptablePrice), trigger = BigInt(w.triggerPrice);168 const coll = token(c, w.initialCollateralTokenAddress);169 return {170 m, whole,171 base: {172 instrument: instrumentId(m.symbol), side: (increase === w.isLong ? 'buy' : 'sell') as Order['side'], type,173 size: whole ? '0' : W.usd(w.sizeDeltaUsd),174 // acceptable price: the worst execution price the order tolerates; 0 or a value near max uint256 mean "no bound"175 price: acceptable === 0n || acceptable >= 1n << 200n ? null : W.contractPrice(acceptable, dec),176 triggerPrice: trigger === 0n ? null : W.contractPrice(trigger, dec),177 reduceOnly: !increase, tif: 'gtc' as const, clientId: key, venueOrderId: key,178 },179 detail: { key, orderType: W.ORDER_TYPE[w.orderType]!, isLong: w.isLong, market: m.marketTokenAddress, sizeUnit: 'usd' as const, closesWholePosition: whole,180 collateralToken: w.initialCollateralTokenAddress, collateralSymbol: coll.symbol, collateralDelta: W.tokenAmount(w.initialCollateralDeltaAmount, coll.decimals), swapPath: w.swapPath },181 };182 };183184 const readPositions = async (account: string) => Promise.all([context(), get(`${api}/positions?address=${evm(account)}`, W.positions)]);185 const positionOf = (c: Ctx, p: W.WirePosition): GmxPosition => {186 const m = marketAt(c, p.marketAddress), dec = indexDecimals(c, m), coll = token(c, p.collateralTokenAddress);187 return {188 instrument: instrumentId(m.symbol), side: p.isLong ? 'long' : 'short', size: W.tokenAmount(p.sizeInTokens, dec),189 entryPrice: p.entryPrice === undefined ? '0' : W.apiPrice(p.entryPrice), markPrice: W.apiPrice(p.markPrice),190 liquidationPrice: p.liquidationPrice === undefined ? null : W.apiPrice(p.liquidationPrice),191 leverage: p.leverage === undefined ? '0' : W.bps(p.leverage), marginMode: 'isolated', margin: W.usd(p.collateralUsd),192 unrealizedPnl: W.usd(p.pnl), // before fees and price impact; see gmx.pnlAfterAllFeesUsd193 realizedPnl: null, fundingPaid: null, source: 'venue',194 gmx: {195 key: p.key, contractKey: p.contractKey, market: m.marketTokenAddress, collateralToken: p.collateralTokenAddress, collateralSymbol: coll.symbol,196 sizeUsd: W.usd(p.sizeInUsd), collateralAmount: W.tokenAmount(p.collateralAmount, coll.decimals), collateralUsd: W.usd(p.collateralUsd),197 remainingCollateralUsd: W.usd(p.remainingCollateralUsd), netValueUsd: W.usd(p.netValue), netValueAfterAllFeesUsd: W.usd(p.netValueAfterAllFees),198 pnlAfterFeesUsd: W.usd(p.pnlAfterFees), pnlAfterAllFeesUsd: W.usd(p.pnlAfterAllFees), hasLowCollateral: p.hasLowCollateral,199 pendingBorrowingFeesUsd: W.usd(p.pendingBorrowingFeesUsd), pendingFundingFeesUsd: W.usd(p.pendingFundingFeesUsd),200 pendingClaimableFundingFeesUsd: W.usd(p.pendingClaimableFundingFeesUsd), closingFeeUsd: W.usd(p.closingFeeUsd), uiFeeUsd: W.usd(p.uiFeeUsd),201 pendingImpactUsd: W.usd(p.pendingImpactUsd), closePriceImpactDeltaUsd: W.usd(p.closePriceImpactDeltaUsd),202 netPriceImpactDeltaUsd: W.usd(p.netPriceImapctDeltaUsd), priceImpactDiffUsd: W.usd(p.priceImpactDiffUsd),203 increasedAt: Number(p.increasedAtTime) * 1000, decreasedAt: Number(p.decreasedAtTime) * 1000,204 },205 };206 };207 const tradeActions = async (account: string, limit: number) =>208 Promise.all([context(), get(`${api}/trades?address=${evm(account)}&limit=${Math.min(Math.max(1, limit), 1000)}`, W.tradesList)]);209 const sum = (xs: string[]) => ds(xs.reduce((a, x) => D.add(a, D.from(x)), D.from('0')));210211 return {212 id: GMX, network: 'mainnet', chain, capabilities: GMX_CAPABILITIES,213214 async instruments(): Promise<Instrument[]> { const c = await context(); return c.markets.filter((m) => !m.isSpotOnly).map((m) => instrumentOf(c, m)); },215216 async tickers(): Promise<Ticker[]> {217 const ts = now();218 const [c, tk, vol] = await Promise.all([219 context(), get(`${api}/markets/tickers`, W.marketTickers),220 // the listing feed is the only place the venue publishes 24 h volume; if it is down the tickers are still valid without it221 get(`${api}/pairs`, W.pairs, true).then((p) => new Map(p.filter((x) => x.product_type === 'Perpetual').map((x) => [lc(x.pool_id), jsonNumberToDecStr(x.target_volume)])), () => null),222 ]);223 const out: Ticker[] = [];224 for (const t of tk) {225 const m = c.byAddress.get(lc(t.marketTokenAddress));226 if (!m || m.isSpotOnly) continue;227 const dec = indexDecimals(c, m), mark = W.apiPrice(t.markPrice);228 out.push({229 instrument: instrumentId(m.symbol), mark, index: mark, mid: null,230 funding: W.factor(-BigInt(t.fundingRateLong)), // venue sign is "what the side receives"; normalized to "what longs pay", per hour231 openInterest: W.tokenAmount(BigInt(t.longInterestInTokens) + BigInt(t.shortInterestInTokens), dec),232 volume24h: vol?.get(lc(t.marketTokenAddress)) ?? null,233 change24h: W.bps(t.priceChangePercent24hBps), ts,234 });235 }236 return out;237 },238239 async book(): Promise<Book> { throw venueError(GMX, 'unsupported', 'GMX has no order book; use liquidity(symbol)'); },240241 async liquidity(symbol?: string): Promise<GmxLiquidity[]> {242 const ts = now();243 const [c, tk, cf, vals] = await Promise.all([context(), get(`${api}/markets/tickers`, W.marketTickers), get(`${api}/markets/config`, W.marketConfigs),244 // only the virtual inventory comes from here: if it cannot be read the liquidity view stands, with that one input unknown (null)245 get(`${api}/markets/values`, W.marketValues).then((v) => new Map(v.map((x) => [lc(x.marketTokenAddress), x])), () => null)]);246 if (symbol !== undefined) perp(c, symbol);247 const config = new Map(cf.map((x) => [lc(x.marketTokenAddress), x]));248 const out: GmxLiquidity[] = [];249 for (const t of tk) {250 const m = c.byAddress.get(lc(t.marketTokenAddress)), g = config.get(lc(t.marketTokenAddress));251 if (!m || m.isSpotOnly || (symbol !== undefined && m.symbol !== symbol)) continue;252 if (!g) throw bad(`no configuration for market ${m.symbol}`);253 out.push({254 instrument: instrumentId(m.symbol), symbol: m.symbol, market: m.marketTokenAddress,255 openInterestLongUsd: W.usd(t.longInterestUsd), openInterestShortUsd: W.usd(t.shortInterestUsd),256 openInterestLongTokens: W.tokenAmount(t.longInterestInTokens, indexDecimals(c, m)), openInterestShortTokens: W.tokenAmount(t.shortInterestInTokens, indexDecimals(c, m)),257 useOpenInterestInTokensForBalance: g.useOpenInterestInTokensForBalance ?? null,258 virtualInventory: { applies: g.virtualIndexTokenId === undefined ? null : BigInt(g.virtualIndexTokenId) !== 0n,259 tokens: vals?.get(lc(t.marketTokenAddress)) ? W.tokenAmount(vals.get(lc(t.marketTokenAddress))!.virtualInventoryForPositionsInTokens, indexDecimals(c, m)) : null,260 usd: vals?.get(lc(t.marketTokenAddress)) ? W.usd(vals.get(lc(t.marketTokenAddress))!.virtualInventoryForPositions) : null },261 maxOpenInterestLongUsd: W.usd(g.maxOpenInterestLong), maxOpenInterestShortUsd: W.usd(g.maxOpenInterestShort),262 availableLiquidityLongUsd: W.usd(t.availableLiquidityLong), availableLiquidityShortUsd: W.usd(t.availableLiquidityShort),263 capacityLongUsd: W.usd(t.capacityLong.availableLiquidity), capacityShortUsd: W.usd(t.capacityShort.availableLiquidity),264 capacityLimitedBy: { long: t.capacityLong.limitingFactor, short: t.capacityShort.limitingFactor },265 fundingRateLongPerHour: W.factor(t.fundingRateLong), fundingRateShortPerHour: W.factor(t.fundingRateShort),266 borrowingRateLongPerHour: W.factor(t.borrowingRateLong), borrowingRateShortPerHour: W.factor(t.borrowingRateShort),267 netRateLongPerHour: W.factor(t.netRateLong), netRateShortPerHour: W.factor(t.netRateShort),268 priceImpact: { factorPositive: W.factor(g.positionImpactFactorPositive), factorNegative: W.factor(g.positionImpactFactorNegative),269 exponentPositive: W.factor(g.positionImpactExponentFactorPositive), exponentNegative: W.factor(g.positionImpactExponentFactorNegative),270 maxFactorPositive: W.factor(g.maxPositionImpactFactorPositive), maxFactorNegative: W.factor(g.maxPositionImpactFactorNegative),271 maxFactorForLiquidations: W.factor(g.maxPositionImpactFactorForLiquidations) },272 positionFeeFactor: { balanceImproved: W.factor(g.positionFeeFactorForBalanceWasImproved), balanceNotImproved: W.factor(g.positionFeeFactorForBalanceWasNotImproved) },273 minCollateralFactor: W.factor(g.minCollateralFactor), minCollateralFactorForLiquidation: W.factor(g.minCollateralFactorForLiquidation),274 minCollateralFactorPerUsdOfOpenInterest: { long: W.factor(g.minCollateralFactorForOpenInterestLong), short: W.factor(g.minCollateralFactorForOpenInterestShort) },275 oracleMin: W.apiPrice(t.minPrice), oracleMax: W.apiPrice(t.maxPrice), ts,276 });277 }278 return out;279 },280281 async oraclePrices() {282 const [tk, tokens] = await Promise.all([get(`${oracle}/prices/tickers`, W.oracleTickers), get(`${oracle}/tokens`, W.oracleTokens)]);283 const dec = new Map(tokens.tokens.map((t) => [lc(t.address), t.decimals]));284 return tk.flatMap((t) => {285 const d = dec.get(lc(t.tokenAddress));286 // GM/GLV pseudo-tokens of the ticker list are not in /tokens: no decimals, no conversion287 return d === undefined ? [] : [{ token: t.tokenAddress, symbol: t.tokenSymbol, min: W.contractPrice(t.minPrice, d), max: W.contractPrice(t.maxPrice, d), ts: t.updatedAt }];288 });289 },290291 /** Executed position orders of one market from the public Subsquid feed, newest first. */292 async trades(symbol: string, limit = 100): Promise<PublicTrade[]> {293 const c = await context(), m = perp(c, symbol), dec = indexDecimals(c, m), n = Math.min(Math.max(1, Math.trunc(limit)), 1000);294 const query = `{ tradeActions(limit: ${n}, orderBy: timestamp_DESC, where: { eventName_eq: "OrderExecuted", marketAddress_eq: "${m.marketTokenAddress}", orderType_in: [2,3,4,5,6,7,8] }) { id orderType isLong sizeDeltaInTokens executionPrice timestamp } }`;295 const r = await http.get(graphql, W.graphqlTrades, { body: { query }, on4xx });296 // an executed order with no size change is a collateral deposit/withdrawal, not a trade297 return r.data.tradeActions.filter((t) => BigInt(t.sizeDeltaInTokens) > 0n).map((t): PublicTrade => ({298 instrument: instrumentId(m.symbol), price: W.contractPrice(t.executionPrice, dec), size: W.tokenAmount(t.sizeDeltaInTokens, dec),299 side: W.isIncrease(t.orderType) === t.isLong ? 'buy' : 'sell', ts: t.timestamp * 1000, id: t.id,300 }));301 },302303 async candles(symbol, interval, q): Promise<Candle[]> {304 const c = await context(), m = perp(c, symbol);305 const qs = `symbol=${encodeURIComponent(baseOf(m.symbol))}&timeframe=${interval}&limit=${Math.min(Math.max(1, q.limit ?? 500), 10_000)}${q.start === undefined ? '' : `&since=${Math.trunc(q.start)}`}`;306 const r = await get(`${api}/prices/ohlcv?${qs}`, W.ohlcv);307 const price = (s: string) => { try { return ds(D.from(s)); } catch { throw bad(`candle price ${s.slice(0, 30)}`); } };308 return r.filter((x) => (q.start === undefined || x.timestamp >= q.start) && (q.end === undefined || x.timestamp <= q.end))309 .map((x): Candle => ({ ts: x.timestamp, open: price(x.open), high: price(x.high), low: price(x.low), close: price(x.close), volume: '0' }))310 .sort((a, b) => a.ts - b.ts);311 },312313 /** Hourly snapshots of the funding rate, normalized like Ticker.funding (what longs pay per hour). */314 async fundingHistory(symbol, f) {315 const c = await context(), m = perp(c, symbol), age = now() - f.start, DAY = 86_400_000;316 const period = age <= DAY ? '1d' : age <= 7 * DAY ? '7d' : age <= 30 * DAY ? '30d' : age <= 90 * DAY ? '90d' : age <= 180 * DAY ? '180d' : age <= 365 * DAY ? '1y' : 'total';317 const r = await get(`${api}/rates?period=${period}&address=${m.marketTokenAddress}`, W.rates);318 const mine = r.find((x) => lc(x.marketAddress) === lc(m.marketTokenAddress));319 return (mine?.ratesSnapshots ?? [])320 .map((s) => ({ ts: s.timestamp * 1000, rate: W.perSecondToHourly(-BigInt(s.fundingRateLong)), premium: null }))321 .filter((x) => x.ts >= f.start && (f.end === undefined || x.ts <= f.end)).sort((a, b) => a.ts - b.ts);322 },323324 async account(account: string): Promise<AccountState> {325 const [, ps] = await readPositions(account);326 const collateral = sum(ps.map((p) => W.usd(p.collateralUsd)));327 return { venue: GMX, account, equity: sum(ps.map((p) => W.usd(p.netValue))), collateral, freeCollateral: '0', marginUsed: collateral,328 maintenanceMargin: null, withdrawable: null, ts: now(), source: 'venue' };329 },330 async positions(account: string): Promise<GmxPosition[]> { const [c, ps] = await readPositions(account); return ps.map((p) => positionOf(c, p)); },331332 async openOrders(account: string): Promise<GmxOrder[]> {333 const [c, os, ps] = await Promise.all([context(), get(`${api}/orders?address=${evm(account)}`, W.orders), get(`${api}/positions?address=${evm(account)}`, W.positions)]);334 return os.filter((w) => w.orderType >= 2 && w.orderType <= 8 && lc(w.marketAddress) !== ZERO).map((w): GmxOrder => {335 const s = orderShape(c, w, w.key), at = Number(w.updatedAtTime) * 1000;336 // a decrease order sized max uint256 closes the whole position: report that position's current USD size337 const target = s.whole ? ps.find((p) => lc(p.marketAddress) === lc(w.marketAddress) && p.isLong === w.isLong && lc(p.collateralTokenAddress) === lc(afterSwaps(c, w.initialCollateralTokenAddress, W.isIncrease(w.orderType) ? w.swapPath : []))) : undefined;338 return {339 // a market order that is still listed is waiting for its keeper; limit and trigger orders rest until their price is reached340 ...s.base, size: target ? W.usd(target.sizeInUsd) : s.base.size, status: s.base.type === 'market' ? 'pending' : 'open', filled: '0', avgFillPrice: null, createdAt: at, updatedAt: at, rejectReason: null,341 gmx: { ...s.detail, executionFee: W.tokenAmount(w.executionFee, 18), isFrozen: w.isFrozen, autoCancel: w.autoCancel, receiver: w.receiver,342 validFrom: w.validFromTime === '0' ? null : Number(w.validFromTime) * 1000, cancelReason: null },343 };344 });345 },346347 /** Order life cycles rebuilt from the account's trade actions (created → executed / cancelled / frozen), newest first. */348 async orderHistory(account: string, h?: { limit?: number }): Promise<GmxOrder[]> {349 const limit = Math.max(1, h?.limit ?? 100);350 const [c, r] = await tradeActions(account, limit * 4);351 const byKey = new Map<string, W.TradeAction[]>();352 for (const t of r.trades) { if (t.orderType < 2 || t.orderType > 8 || !t.marketAddress) continue; const l = byKey.get(t.orderKey) ?? []; l.push(t); byKey.set(t.orderKey, l); }353 const out: GmxOrder[] = [];354 for (const [key, evs] of byKey) {355 evs.sort((a, b) => a.timestamp - b.timestamp);356 const last = evs[evs.length - 1]!, first = evs[0]!;357 if (last.isLong === undefined || last.sizeDeltaUsd === undefined) throw bad(`trade action ${last.id}: missing order fields`);358 const s = orderShape(c, { orderType: last.orderType, isLong: last.isLong, marketAddress: last.marketAddress!, sizeDeltaUsd: last.sizeDeltaUsd, triggerPrice: last.triggerPrice ?? '0',359 acceptablePrice: last.acceptablePrice ?? '0', initialCollateralTokenAddress: last.initialCollateralTokenAddress, initialCollateralDeltaAmount: last.initialCollateralDeltaAmount, swapPath: last.swapPath }, key);360 const executed = last.eventName === 'OrderExecuted', dec = indexDecimals(c, s.m);361 const status: Order['status'] = executed ? 'filled' : last.eventName === 'OrderCancelled' ? 'cancelled' : last.eventName === 'OrderFrozen' || s.base.type === 'market' ? 'pending' : 'open';362 out.push({363 ...s.base, status, filled: executed ? s.base.size : '0',364 avgFillPrice: executed && last.executionPrice !== undefined ? W.contractPrice(last.executionPrice, dec) : null,365 createdAt: first.timestamp * 1000, updatedAt: last.timestamp * 1000, rejectReason: last.reason ?? null,366 gmx: { ...s.detail, executionFee: null, isFrozen: last.eventName === 'OrderFrozen', autoCancel: false, receiver: null, validFrom: null, cancelReason: last.reason ?? null },367 });368 }369 return out.sort((a, b) => b.updatedAt - a.updatedAt).slice(0, limit);370 },371372 async fills(account: string, f?: { start?: number; limit?: number }): Promise<GmxFill[]> {373 const [c, r] = await tradeActions(account, (f?.limit ?? 100) * 3);374 const out: GmxFill[] = [];375 for (const t of r.trades) {376 if (t.eventName !== 'OrderExecuted' || t.orderType < 2 || t.orderType > 8 || !t.marketAddress) continue;377 if (f?.start !== undefined && t.timestamp * 1000 < f.start) continue;378 if (t.isLong === undefined || t.sizeDeltaInTokens === undefined || t.executionPrice === undefined || t.sizeDeltaUsd === undefined) throw bad(`trade action ${t.id}: missing execution fields`);379 if (BigInt(t.sizeDeltaInTokens) === 0n) continue; // collateral-only change: no size traded380 const m = marketAt(c, t.marketAddress), dec = indexDecimals(c, m), increase = W.isIncrease(t.orderType);381 // fee amounts are in the position's collateral token: for an increase that is what the swap path ends in382 const feeToken = increase ? afterSwaps(c, t.initialCollateralTokenAddress, t.swapPath) : t.initialCollateralTokenAddress, ft = token(c, feeToken);383 const amt = (x?: string) => W.tokenAmount(x ?? '0', ft.decimals), u = (x?: string) => (x === undefined ? null : W.usd(x));384 out.push({385 instrument: instrumentId(m.symbol), orderId: t.orderKey, clientId: null, side: increase === t.isLong ? 'buy' : 'sell',386 price: W.contractPrice(t.executionPrice, dec), size: W.tokenAmount(t.sizeDeltaInTokens, dec),387 fee: amt(t.positionFeeAmount), // position (open/close) fee only; borrowing, funding, liquidation fees are in `gmx`388 feeAsset: ft.symbol, closedPnl: increase ? null : u(t.pnlUsd), ts: t.timestamp * 1000, id: t.id, liquidation: t.orderType === 7,389 gmx: { orderKey: t.orderKey, orderType: W.ORDER_TYPE[t.orderType]!, txHash: t.transactionHash, isLong: t.isLong, sizeDeltaUsd: W.usd(t.sizeDeltaUsd), feeToken,390 positionFeeAmount: amt(t.positionFeeAmount), borrowingFeeAmount: amt(t.borrowingFeeAmount), fundingFeeAmount: amt(t.fundingFeeAmount),391 liquidationFeeAmount: amt(t.liquidationFeeAmount), traderDiscountAmount: amt(t.traderDiscountAmount),392 priceImpactUsd: u(t.priceImpactUsd), totalImpactUsd: u(t.totalImpactUsd), proportionalPendingImpactUsd: u(t.proportionalPendingImpactUsd),393 priceImpactDiffUsd: u(t.priceImpactDiffUsd), swapImpactUsd: u(t.swapImpactUsd), swapFeeUsd: u(t.swapFeeUsd), basePnlUsd: u(t.basePnlUsd),394 indexPriceMin: t.indexTokenPriceMin === undefined ? null : W.contractPrice(t.indexTokenPriceMin, dec),395 indexPriceMax: t.indexTokenPriceMax === undefined ? null : W.contractPrice(t.indexTokenPriceMax, dec) },396 });397 }398 return out.slice(0, f?.limit ?? 100);399 },400401 async funding(): Promise<FundingPayment[]> {402 throw venueError(GMX, 'unsupported', 'GMX settles funding into collateral at each position change: see fills()[].gmx.fundingFeeAmount and positions()[].gmx.pendingFundingFeesUsd');403 },404 };405}406