@katanaperps/katana-perps-sdk 2.1.0-beta.13 → 2.1.0-beta.14

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.
@@ -0,0 +1,713 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.calculateBuySellPanelEstimate = void 0;
4
+ const _pipmath_1 = require("#pipmath");
5
+ const quantities_1 = require("#orderbook/quantities");
6
+ const request_1 = require("#types/enums/request");
7
+ /**
8
+ * The taker (trade fee + gas fee) on a single fill is limited to 5% of the
9
+ * fill's quote quantity.
10
+ *
11
+ * @see {@link https://github.com/idexio/idex-contracts-ikon} (contracts)
12
+ */
13
+ const maximumTradeFeeFraction = (0, _pipmath_1.decimalToPip)('0.05');
14
+ function makeEmptyEstimate() {
15
+ return {
16
+ tradeBaseQuantity: BigInt(0),
17
+ tradeQuoteQuantity: BigInt(0),
18
+ makerBaseQuantity: BigInt(0),
19
+ cost: BigInt(0),
20
+ liquidationPrice: null,
21
+ selfTradeEncountered: false,
22
+ freeCollateralExceeded: false,
23
+ availableCollateralExceeded: false,
24
+ executionPriceLimitExceeded: false,
25
+ maximumPositionSizeExceeded: false,
26
+ postOnlyWouldCross: false,
27
+ fillOrKillWouldNotExecute: false,
28
+ reduceOnlyWouldNotReducePosition: false,
29
+ reduceOnlyNoOpenPosition: false,
30
+ reduceOnlyOpenPositionSizeExceeded: false,
31
+ };
32
+ }
33
+ /**
34
+ * @private
35
+ */
36
+ function wasPositionReduced(before, after) {
37
+ return (after === BigInt(0) ||
38
+ (before > BigInt(0) && after > BigInt(0) && after < before) ||
39
+ (before < BigInt(0) && after < BigInt(0) && after > before));
40
+ }
41
+ /**
42
+ * @private
43
+ * Margin requirement (held collateral) for a single open order quantity.
44
+ */
45
+ function calculateMarginRequirementForOpenOrderQuantity(context, openBaseQuantity, limitPrice) {
46
+ if (openBaseQuantity === BigInt(0)) {
47
+ return BigInt(0);
48
+ }
49
+ const orderOpenQuoteQuantity = (0, _pipmath_1.multiplyPips)(openBaseQuantity, limitPrice);
50
+ return (0, _pipmath_1.multiplyPips)(orderOpenQuoteQuantity, (0, quantities_1.calculateInitialMarginFractionWithOverride)({
51
+ baseQuantity: openBaseQuantity,
52
+ initialMarginFractionOverride: context.initialMarginFractionOverride,
53
+ leverageParameters: context.leverageParameters,
54
+ }));
55
+ }
56
+ /**
57
+ * @private
58
+ * Held collateral required for a set of standing orders in a single market,
59
+ * given a signed position quantity. Orders that reduce the position do not
60
+ * require margin, up to a combined quantity that equals the position size.
61
+ */
62
+ function calculateHeldCollateralForMarket(context, orders, positionQuantity) {
63
+ let marginRequirement = BigInt(0);
64
+ // The reducing side (sells for a long position, buys for a short position),
65
+ // processed best price first, offsets against the position.
66
+ const positionIsLong = positionQuantity > BigInt(0);
67
+ const reducingOrders = orders
68
+ .filter((order) => order.isBuy === !positionIsLong && order.openQuantity > BigInt(0))
69
+ .sort((a, b) =>
70
+ // Sells: lowest price first; buys: highest price first
71
+ a.isBuy ? Number(b.price - a.price) : Number(a.price - b.price));
72
+ const nonReducingOrders = orders.filter((order) => order.isBuy === positionIsLong && order.openQuantity > BigInt(0));
73
+ let remainingPositionQuantity = (0, _pipmath_1.absBigInt)(positionQuantity);
74
+ for (const order of reducingOrders) {
75
+ let openBaseQuantity = order.openQuantity;
76
+ if (remainingPositionQuantity > BigInt(0)) {
77
+ if (openBaseQuantity > remainingPositionQuantity) {
78
+ openBaseQuantity -= remainingPositionQuantity;
79
+ remainingPositionQuantity = BigInt(0);
80
+ }
81
+ else {
82
+ remainingPositionQuantity -= openBaseQuantity;
83
+ openBaseQuantity = BigInt(0);
84
+ }
85
+ }
86
+ marginRequirement += calculateMarginRequirementForOpenOrderQuantity(context, openBaseQuantity, order.price);
87
+ }
88
+ for (const order of nonReducingOrders) {
89
+ marginRequirement += calculateMarginRequirementForOpenOrderQuantity(context, order.openQuantity, order.price);
90
+ }
91
+ return marginRequirement;
92
+ }
93
+ /**
94
+ * @private
95
+ * Solves for the index price at which the account's value equals its
96
+ * maintenance margin requirement (the liquidation price), holding the index
97
+ * prices of all other markets constant (cross-margin, account-wide).
98
+ *
99
+ * Returns `null` if the resulting position is zero, and `0n` if no positive
100
+ * liquidation price exists (the account is not liquidatable by an adverse
101
+ * move).
102
+ */
103
+ function calculateLiquidationPrice(args) {
104
+ const { newPositionQuantity, maintenanceMarginFraction, quoteBalanceAfter, otherPositionsNotionalAtIndex, otherPositionsMaintenanceMarginRequirement, } = args;
105
+ if (newPositionQuantity === BigInt(0)) {
106
+ return null;
107
+ }
108
+ // accountValue(P) = MMR(P), solved for P (in pips):
109
+ // P = (otherMMR - otherNotional - quoteBalance) * oneInPips^2
110
+ // / (newQty * oneInPips - abs(newQty) * mmf)
111
+ const numerator = (otherPositionsMaintenanceMarginRequirement -
112
+ otherPositionsNotionalAtIndex -
113
+ quoteBalanceAfter) *
114
+ _pipmath_1.oneInPips *
115
+ _pipmath_1.oneInPips;
116
+ const denominator = newPositionQuantity * _pipmath_1.oneInPips -
117
+ (0, _pipmath_1.absBigInt)(newPositionQuantity) * maintenanceMarginFraction;
118
+ if (denominator === BigInt(0)) {
119
+ return BigInt(0);
120
+ }
121
+ const liquidationPrice = numerator / denominator;
122
+ return liquidationPrice > BigInt(0) ? liquidationPrice : BigInt(0);
123
+ }
124
+ /**
125
+ * @private
126
+ * Walks the maker side of the book, accumulating the crossing (trade) portion
127
+ * of the taker order, distinguishing the wallet's own resting liquidity
128
+ * (self-trades) from other liquidity, and applying execution price limits and
129
+ * taker fees.
130
+ */
131
+ function matchTakerOrder(context, quantity, reduceOnlyMaximumBaseQuantity) {
132
+ const isQuantityInQuote = 'quoteQuantity' in quantity;
133
+ // Working copy of own maker-side resting liquidity, indexed by price.
134
+ const standingOrdersCopy = context.marketStandingOrders.map((order) => ({
135
+ ...order,
136
+ }));
137
+ const ownMakerOrdersByPrice = new Map();
138
+ for (const order of standingOrdersCopy) {
139
+ if (order.isBuy !== context.isBuy && order.openQuantity > BigInt(0)) {
140
+ const list = ownMakerOrdersByPrice.get(order.price) ?? [];
141
+ list.push(order);
142
+ ownMakerOrdersByPrice.set(order.price, list);
143
+ }
144
+ }
145
+ const result = {
146
+ tradeBaseQuantity: BigInt(0),
147
+ tradeQuoteQuantity: BigInt(0),
148
+ makerOrdersMatched: BigInt(0),
149
+ takerTradeFee: BigInt(0),
150
+ takerGasFee: BigInt(0),
151
+ selfTradeBaseQuantity: BigInt(0),
152
+ selfTradeEncountered: false,
153
+ executionPriceLimitExceeded: false,
154
+ standingOrdersAfterSelfTrade: standingOrdersCopy,
155
+ remainingBaseQuantity: BigInt(0),
156
+ };
157
+ let remainingBase = isQuantityInQuote ? null : quantity.baseQuantity;
158
+ let remainingQuote = isQuantityInQuote ? quantity.quoteQuantity : null;
159
+ // Tracks reduce-only fill capacity (real, position-reducing fills only)
160
+ let remainingReduceOnly = reduceOnlyMaximumBaseQuantity;
161
+ for (const level of context.makerLevels) {
162
+ // doOrdersMatch
163
+ if (!context.isMarketOrder) {
164
+ const crosses = context.isBuy ?
165
+ context.limitPrice >= level.price
166
+ : context.limitPrice <= level.price;
167
+ if (!crosses) {
168
+ break;
169
+ }
170
+ }
171
+ // Execution price limit (market orders)
172
+ if ((context.minimumExecutionPrice !== null &&
173
+ level.price < context.minimumExecutionPrice) ||
174
+ (context.maximumExecutionPrice !== null &&
175
+ level.price > context.maximumExecutionPrice)) {
176
+ result.executionPriceLimitExceeded = true;
177
+ break;
178
+ }
179
+ const exhausted = remainingBase !== null ?
180
+ remainingBase <= BigInt(0)
181
+ : remainingQuote <= BigInt(0);
182
+ if (exhausted) {
183
+ break;
184
+ }
185
+ // Determine how much of this level the taker consumes.
186
+ let consumedBase;
187
+ if (remainingBase !== null) {
188
+ consumedBase = (0, _pipmath_1.minBigInt)(remainingBase, level.size);
189
+ }
190
+ else {
191
+ const levelQuoteIfFull = (0, _pipmath_1.multiplyPips)(level.size, level.price, context.isMarketOrder && context.isBuy);
192
+ if (levelQuoteIfFull <= remainingQuote) {
193
+ consumedBase = level.size;
194
+ }
195
+ else {
196
+ // Partial: reduce base proportionally to the quote budget
197
+ consumedBase = (level.size * remainingQuote) / levelQuoteIfFull;
198
+ }
199
+ }
200
+ // Split into self-trade (own liquidity) and real fill (other liquidity).
201
+ const ownOrders = ownMakerOrdersByPrice.get(level.price) ?? [];
202
+ let ownSizeAtLevel = BigInt(0);
203
+ for (const order of ownOrders) {
204
+ ownSizeAtLevel += order.openQuantity;
205
+ }
206
+ const selfPart = (0, _pipmath_1.minBigInt)(consumedBase, ownSizeAtLevel);
207
+ let realPart = consumedBase - selfPart;
208
+ // Reduce-only orders may only reduce the position; cap the real fill.
209
+ if (remainingReduceOnly !== null && realPart > remainingReduceOnly) {
210
+ realPart = (0, _pipmath_1.maxBigInt)(remainingReduceOnly, BigInt(0));
211
+ // The self-trade portion is unaffected by reduce-only fill capacity, but
212
+ // the order stops once its reduce-only capacity is consumed.
213
+ consumedBase = selfPart + realPart;
214
+ }
215
+ if (selfPart > BigInt(0)) {
216
+ result.selfTradeEncountered = true;
217
+ result.selfTradeBaseQuantity += selfPart;
218
+ // Reduce the wallet's own resting orders at this price.
219
+ let toReduce = selfPart;
220
+ for (const order of ownOrders) {
221
+ if (toReduce <= BigInt(0)) {
222
+ break;
223
+ }
224
+ const reduction = (0, _pipmath_1.minBigInt)(order.openQuantity, toReduce);
225
+ order.openQuantity -= reduction;
226
+ toReduce -= reduction;
227
+ }
228
+ }
229
+ if (realPart > BigInt(0)) {
230
+ const realQuote = (0, _pipmath_1.multiplyPips)(realPart, level.price, context.isMarketOrder && context.isBuy);
231
+ if (realQuote === BigInt(0)) {
232
+ // Sub-pip quote quantity is invalid; the engine rejects such trades.
233
+ break;
234
+ }
235
+ result.tradeBaseQuantity += realPart;
236
+ result.tradeQuoteQuantity += realQuote;
237
+ // Taker fees for this fill. The sum of the trade and gas fee is capped at
238
+ // 5% of the fill's quote quantity, with priority given to the trade fee:
239
+ // the trade fee is capped at 5%, and the gas fee may only consume whatever
240
+ // of the 5% budget the trade fee leaves (zero once the trade fee hits 5%).
241
+ const maxFee = (0, _pipmath_1.multiplyPips)(realQuote, maximumTradeFeeFraction);
242
+ const tradeFee = (0, _pipmath_1.minBigInt)((0, _pipmath_1.multiplyPips)(realQuote, context.takerFeeRate), maxFee);
243
+ const realSizeAtLevel = (0, _pipmath_1.maxBigInt)(level.size - ownSizeAtLevel, BigInt(0));
244
+ const ordersAtLevel = realSizeAtLevel <= BigInt(0) ?
245
+ BigInt(0)
246
+ : (0, _pipmath_1.maxBigInt)(BigInt(1), (BigInt(level.numOrders) * realPart + realSizeAtLevel - BigInt(1)) /
247
+ realSizeAtLevel);
248
+ const gasFee = (0, _pipmath_1.minBigInt)(context.gasFeePerOrder * ordersAtLevel, (0, _pipmath_1.maxBigInt)(maxFee - tradeFee, BigInt(0)));
249
+ result.makerOrdersMatched += ordersAtLevel;
250
+ result.takerTradeFee += tradeFee;
251
+ result.takerGasFee += gasFee;
252
+ if (remainingReduceOnly !== null) {
253
+ remainingReduceOnly -= realPart;
254
+ }
255
+ }
256
+ // Consume the taker order's remaining budget (self-trades consume it too).
257
+ if (remainingBase !== null) {
258
+ remainingBase -= consumedBase;
259
+ }
260
+ else {
261
+ remainingQuote -= (0, _pipmath_1.multiplyPips)(consumedBase, level.price, context.isMarketOrder && context.isBuy);
262
+ }
263
+ if (remainingReduceOnly !== null && remainingReduceOnly <= BigInt(0)) {
264
+ break;
265
+ }
266
+ }
267
+ result.remainingBaseQuantity =
268
+ remainingBase !== null ? (0, _pipmath_1.maxBigInt)(remainingBase, BigInt(0)) : BigInt(0);
269
+ return result;
270
+ }
271
+ /**
272
+ * @private
273
+ * Produces a complete estimate for a concrete base or quote quantity.
274
+ */
275
+ function runEstimate(context, quantity) {
276
+ const estimate = makeEmptyEstimate();
277
+ // Reduce-only validity and fill capacity.
278
+ let reduceOnlyMaximumBaseQuantity = null;
279
+ if (context.reduceOnly) {
280
+ const positionQuantity = context.currentPositionQuantity;
281
+ if (positionQuantity === BigInt(0)) {
282
+ estimate.reduceOnlyNoOpenPosition = true;
283
+ return estimate;
284
+ }
285
+ const reduces = (context.isBuy && positionQuantity < BigInt(0)) ||
286
+ (!context.isBuy && positionQuantity > BigInt(0));
287
+ if (!reduces) {
288
+ estimate.reduceOnlyWouldNotReducePosition = true;
289
+ return estimate;
290
+ }
291
+ reduceOnlyMaximumBaseQuantity = (0, _pipmath_1.absBigInt)(positionQuantity);
292
+ }
293
+ // Post-only (gtx) orders may not cross the spread.
294
+ const bestMakerPrice = context.makerLevels.length > 0 ? context.makerLevels[0].price : null;
295
+ const crossesSpread = bestMakerPrice !== null &&
296
+ (context.isMarketOrder ||
297
+ (context.isBuy ?
298
+ context.limitPrice >= bestMakerPrice
299
+ : context.limitPrice <= bestMakerPrice));
300
+ if (context.timeInForce === request_1.TimeInForce.gtx && crossesSpread) {
301
+ estimate.postOnlyWouldCross = true;
302
+ return estimate;
303
+ }
304
+ // Limit orders are rejected if their price is outside the allowed range.
305
+ if (context.limitOrderPriceExceedsLimit) {
306
+ estimate.executionPriceLimitExceeded = true;
307
+ return estimate;
308
+ }
309
+ const fill = matchTakerOrder(context, quantity, reduceOnlyMaximumBaseQuantity);
310
+ estimate.selfTradeEncountered = fill.selfTradeEncountered;
311
+ estimate.executionPriceLimitExceeded = fill.executionPriceLimitExceeded;
312
+ // Fill-or-kill: the order must be fully filled by crossing liquidity.
313
+ if (context.timeInForce === request_1.TimeInForce.fok) {
314
+ const requestedFilled = 'baseQuantity' in quantity ?
315
+ fill.tradeBaseQuantity + fill.selfTradeBaseQuantity >=
316
+ quantity.baseQuantity
317
+ : fill.tradeQuoteQuantity +
318
+ (0, _pipmath_1.multiplyPips)(fill.selfTradeBaseQuantity, context.indexPrice) >=
319
+ quantity.quoteQuantity;
320
+ if (!requestedFilled) {
321
+ estimate.fillOrKillWouldNotExecute = true;
322
+ return estimate;
323
+ }
324
+ }
325
+ estimate.tradeBaseQuantity = fill.tradeBaseQuantity;
326
+ estimate.tradeQuoteQuantity = fill.tradeQuoteQuantity;
327
+ // Determine the resting (maker) portion. Reduce-only limit orders may rest on
328
+ // the books (their reducing portion); whether the resting quantity is valid is
329
+ // checked below via `reduceOnlyOpenPositionSizeExceeded`.
330
+ const canRest = !context.isMarketOrder &&
331
+ (context.timeInForce === request_1.TimeInForce.gtc ||
332
+ context.timeInForce === request_1.TimeInForce.gtx);
333
+ let makerBaseQuantity = BigInt(0);
334
+ if (canRest) {
335
+ if ('baseQuantity' in quantity) {
336
+ makerBaseQuantity = fill.remainingBaseQuantity;
337
+ }
338
+ else {
339
+ // Convert the remaining quote budget to base at the limit price.
340
+ const remainingQuote = (0, _pipmath_1.maxBigInt)(quantity.quoteQuantity -
341
+ fill.tradeQuoteQuantity -
342
+ (0, _pipmath_1.multiplyPips)(fill.selfTradeBaseQuantity, context.limitPrice), BigInt(0));
343
+ makerBaseQuantity =
344
+ context.limitPrice > BigInt(0) ?
345
+ (0, _pipmath_1.dividePips)(remainingQuote, context.limitPrice)
346
+ : BigInt(0);
347
+ }
348
+ }
349
+ estimate.makerBaseQuantity = makerBaseQuantity;
350
+ // A reduce-only order's resting (maker) quantity may not exceed the open
351
+ // position size that remains after the reducing fills (it would otherwise no
352
+ // longer be fully reducing). A zero remaining position with a resting quantity
353
+ // (i.e. the position is closed) is likewise flagged.
354
+ if (context.reduceOnly && makerBaseQuantity > BigInt(0)) {
355
+ const remainingOpenPositionSize = (0, _pipmath_1.absBigInt)(context.currentPositionQuantity) - fill.tradeBaseQuantity;
356
+ if (makerBaseQuantity > remainingOpenPositionSize) {
357
+ estimate.reduceOnlyOpenPositionSizeExceeded = true;
358
+ }
359
+ }
360
+ // Resulting position quantity in the order's market.
361
+ const newPositionQuantity = context.isBuy ?
362
+ context.currentPositionQuantity + fill.tradeBaseQuantity
363
+ : context.currentPositionQuantity - fill.tradeBaseQuantity;
364
+ // Quote balance after the trade.
365
+ const totalFees = fill.takerTradeFee + fill.takerGasFee;
366
+ const quoteBalanceAfter = context.isBuy ?
367
+ context.quoteBalance - fill.tradeQuoteQuantity - totalFees
368
+ : context.quoteBalance + fill.tradeQuoteQuantity - totalFees;
369
+ // Account value (equity) after the trade, holding other markets constant.
370
+ const equityAfter = quoteBalanceAfter +
371
+ (0, _pipmath_1.multiplyPips)(newPositionQuantity, context.indexPrice) +
372
+ (context.otherPositionsNotionalAtIndex -
373
+ (0, _pipmath_1.multiplyPips)(context.currentPositionQuantity, context.indexPrice));
374
+ // Initial margin requirement after the trade.
375
+ const newPositionNotional = (0, _pipmath_1.multiplyPips)((0, _pipmath_1.absBigInt)(newPositionQuantity), context.indexPrice);
376
+ const newPositionInitialMarginRequirement = (0, _pipmath_1.multiplyPips)(newPositionNotional, (0, quantities_1.calculateInitialMarginFractionWithOverride)({
377
+ baseQuantity: newPositionQuantity,
378
+ initialMarginFractionOverride: context.initialMarginFractionOverride,
379
+ leverageParameters: context.leverageParameters,
380
+ }));
381
+ const initialMarginRequirementAfter = context.initialMarginRequirement -
382
+ context.currentInitialMarginRequirement +
383
+ newPositionInitialMarginRequirement;
384
+ // Maintenance margin requirement after the trade.
385
+ const currentMaintenanceMarginRequirement = (0, _pipmath_1.multiplyPips)((0, _pipmath_1.multiplyPips)((0, _pipmath_1.absBigInt)(context.currentPositionQuantity), context.indexPrice), context.maintenanceMarginFraction);
386
+ const newPositionMaintenanceMarginRequirement = (0, _pipmath_1.multiplyPips)(newPositionNotional, context.maintenanceMarginFraction);
387
+ const otherPositionsMaintenanceMarginRequirement = (0, _pipmath_1.maxBigInt)(context.totalMaintenanceMarginRequirement -
388
+ currentMaintenanceMarginRequirement, BigInt(0));
389
+ // Held collateral after the trade: only this market's orders are affected
390
+ // (by the position change, the new resting order, and self-trade reductions).
391
+ const heldBeforeForMarket = calculateHeldCollateralForMarket(context, context.marketStandingOrders, context.currentPositionQuantity);
392
+ const standingOrdersAfter = fill.standingOrdersAfterSelfTrade.slice();
393
+ if (makerBaseQuantity > BigInt(0)) {
394
+ standingOrdersAfter.push({
395
+ isBuy: context.isBuy,
396
+ price: context.limitPrice,
397
+ openQuantity: makerBaseQuantity,
398
+ });
399
+ }
400
+ const heldAfterForMarket = calculateHeldCollateralForMarket(context, standingOrdersAfter, newPositionQuantity);
401
+ const totalHeldCollateralAfter = context.totalHeldCollateral - heldBeforeForMarket + heldAfterForMarket;
402
+ // Collateral measures (unclamped values are used for feasibility flags).
403
+ const freeCollateralBefore = (0, _pipmath_1.maxBigInt)(context.equity - context.initialMarginRequirement, BigInt(0));
404
+ const availableCollateralBefore = (0, _pipmath_1.maxBigInt)(freeCollateralBefore - context.totalHeldCollateral, BigInt(0));
405
+ const freeCollateralAfterUnclamped = equityAfter - initialMarginRequirementAfter;
406
+ const freeCollateralAfter = (0, _pipmath_1.maxBigInt)(freeCollateralAfterUnclamped, BigInt(0));
407
+ // Available collateral after the order is intentionally NOT clamped at zero:
408
+ // `cost` is allowed to exceed the wallet's available collateral (e.g. an order
409
+ // whose margin or held-collateral requirement is greater than the wallet can
410
+ // cover), consistent with the feasibility flags, which also reflect unclamped
411
+ // values.
412
+ const availableCollateralAfter = freeCollateralAfterUnclamped - totalHeldCollateralAfter;
413
+ estimate.cost = availableCollateralBefore - availableCollateralAfter;
414
+ // Liquidation price (cross-margin, account-wide).
415
+ estimate.liquidationPrice = calculateLiquidationPrice({
416
+ newPositionQuantity,
417
+ maintenanceMarginFraction: context.maintenanceMarginFraction,
418
+ quoteBalanceAfter,
419
+ otherPositionsNotionalAtIndex: context.otherPositionsNotionalAtIndex -
420
+ (0, _pipmath_1.multiplyPips)(context.currentPositionQuantity, context.indexPrice),
421
+ otherPositionsMaintenanceMarginRequirement,
422
+ });
423
+ // Feasibility flags.
424
+ //
425
+ // `freeCollateralExceeded` covers the crossing (trade) portion: the initial
426
+ // margin requirement must be met after a trade that increases a position, and
427
+ // the maintenance margin requirement after a trade that reduces it.
428
+ if (crossesSpread && fill.tradeBaseQuantity > BigInt(0)) {
429
+ const positionReduced = wasPositionReduced(context.currentPositionQuantity, newPositionQuantity);
430
+ estimate.freeCollateralExceeded =
431
+ positionReduced ?
432
+ equityAfter -
433
+ (otherPositionsMaintenanceMarginRequirement +
434
+ newPositionMaintenanceMarginRequirement) <
435
+ BigInt(0)
436
+ : freeCollateralAfterUnclamped < BigInt(0);
437
+ }
438
+ // `availableCollateralExceeded` covers any resting (maker) portion: its held
439
+ // collateral may not exceed the wallet's available collateral. This applies
440
+ // both to non-crossing orders and to the unfilled remainder of an order that
441
+ // partially crossed the spread.
442
+ if (makerBaseQuantity > BigInt(0)) {
443
+ estimate.availableCollateralExceeded =
444
+ freeCollateralAfter - totalHeldCollateralAfter < BigInt(0);
445
+ }
446
+ // Maximum position size. Check the order's requested quantity against the
447
+ // room left by the current position (`maximumPositionSize` ∓ position).
448
+ if (!context.reduceOnly) {
449
+ const maxAdditionalLiquidity = context.isBuy ?
450
+ context.maximumPositionSize - context.currentPositionQuantity
451
+ : context.maximumPositionSize + context.currentPositionQuantity;
452
+ // For quote-denominated orders the requested base quantity is not fixed; the
453
+ // realized base (fills + resting + self-trade) is used as a proxy.
454
+ const orderBaseQuantity = 'baseQuantity' in quantity ?
455
+ quantity.baseQuantity
456
+ : fill.tradeBaseQuantity + fill.selfTradeBaseQuantity + makerBaseQuantity;
457
+ if (context.isMarketOrder || crossesSpread) {
458
+ // Market and crossing limit orders are checked against the order quantity
459
+ // alone; the wallet's other standing orders may be canceled after the
460
+ // incoming order is executed if the sum of all standing orders exceeds
461
+ // the maximum position size.
462
+ if (orderBaseQuantity > maxAdditionalLiquidity) {
463
+ estimate.maximumPositionSizeExceeded = true;
464
+ }
465
+ }
466
+ else {
467
+ // A non-crossing (resting) limit order must fit alongside the wallet's
468
+ // other same-side standing orders without collectively exceeding the
469
+ // maximum position.
470
+ let sameSideActiveQuantity = BigInt(0);
471
+ for (const order of context.marketStandingOrders) {
472
+ if (order.isBuy === context.isBuy) {
473
+ sameSideActiveQuantity += order.openQuantity;
474
+ }
475
+ }
476
+ if (sameSideActiveQuantity + orderBaseQuantity > maxAdditionalLiquidity) {
477
+ estimate.maximumPositionSizeExceeded = true;
478
+ }
479
+ }
480
+ }
481
+ return estimate;
482
+ }
483
+ /**
484
+ * @private
485
+ * Resolves the slider input (a fraction of available collateral to consume)
486
+ * into a base quantity, by searching for the largest base quantity whose
487
+ * estimated cost does not exceed the target.
488
+ */
489
+ function resolveBaseQuantityForCollateralRatio(context, ratio) {
490
+ const freeCollateralBefore = (0, _pipmath_1.maxBigInt)(context.equity - context.initialMarginRequirement, BigInt(0));
491
+ const availableCollateralBefore = (0, _pipmath_1.maxBigInt)(freeCollateralBefore - context.totalHeldCollateral, BigInt(0));
492
+ const targetCost = (0, _pipmath_1.multiplyPips)(availableCollateralBefore, ratio);
493
+ if (targetCost <= BigInt(0)) {
494
+ return BigInt(0);
495
+ }
496
+ // Upper bound: all matchable liquidity, plus resting capacity for limit
497
+ // orders that may add to the books.
498
+ let matchableBase = BigInt(0);
499
+ for (const level of context.makerLevels) {
500
+ if (!context.isMarketOrder) {
501
+ const crosses = context.isBuy ?
502
+ context.limitPrice >= level.price
503
+ : context.limitPrice <= level.price;
504
+ if (!crosses) {
505
+ break;
506
+ }
507
+ }
508
+ if ((context.minimumExecutionPrice !== null &&
509
+ level.price < context.minimumExecutionPrice) ||
510
+ (context.maximumExecutionPrice !== null &&
511
+ level.price > context.maximumExecutionPrice)) {
512
+ break;
513
+ }
514
+ matchableBase += level.size;
515
+ }
516
+ const canRest = !context.isMarketOrder &&
517
+ (context.timeInForce === request_1.TimeInForce.gtc ||
518
+ context.timeInForce === request_1.TimeInForce.gtx);
519
+ const restingCapacity = canRest ?
520
+ (0, _pipmath_1.maxBigInt)(context.maximumPositionSize +
521
+ (0, _pipmath_1.absBigInt)(context.currentPositionQuantity), BigInt(0))
522
+ : BigInt(0);
523
+ let high = matchableBase + restingCapacity;
524
+ if (high <= BigInt(0)) {
525
+ return BigInt(0);
526
+ }
527
+ // The slider seeks the largest quantity that (a) does not exceed the target
528
+ // cost and (b) remains feasible (does not breach the wallet's collateral).
529
+ //
530
+ // The feasibility gate is essential, not merely a cost comparison: `cost` is
531
+ // clamped at the available collateral (available-after cannot go below zero),
532
+ // so at a 100% slider the target equals that ceiling and a cost-only check
533
+ // (`cost <= target`) is satisfied by *every* larger quantity on the clamped
534
+ // "plateau" — which made the search walk all the way to `high` (matching all
535
+ // liquidity, or all liquidity up to the limit price). A quantity past the
536
+ // boundary sets freeCollateral/availableCollateralExceeded, which excludes the
537
+ // plateau and yields the largest quantity that drives available collateral to
538
+ // ~zero while remaining acceptable.
539
+ const isAcceptable = (baseQuantity) => {
540
+ const e = runEstimate(context, { baseQuantity });
541
+ return (e.cost <= targetCost &&
542
+ !e.freeCollateralExceeded &&
543
+ !e.availableCollateralExceeded);
544
+ };
545
+ // If even the maximum quantity is acceptable (e.g. liquidity- or
546
+ // position-size-limited so the target cost is never reached), return it.
547
+ if (isAcceptable(high)) {
548
+ return high;
549
+ }
550
+ let low = BigInt(0);
551
+ for (let iteration = 0; iteration < 80; iteration += 1) {
552
+ if (high - low <= BigInt(1)) {
553
+ break;
554
+ }
555
+ const mid = (low + high) / BigInt(2);
556
+ if (isAcceptable(mid)) {
557
+ low = mid;
558
+ }
559
+ else {
560
+ high = mid;
561
+ }
562
+ }
563
+ return low;
564
+ }
565
+ function buildContext(args) {
566
+ const { market, wallet, order } = args;
567
+ const { side } = order;
568
+ const isBuy = side === request_1.OrderSide.buy;
569
+ const isMarketOrder = typeof order.limitPrice === 'undefined';
570
+ const limitPrice = order.limitPrice ?? BigInt(0);
571
+ const timeInForce = order.timeInForce ?? request_1.TimeInForce.gtc;
572
+ const reduceOnly = order.reduceOnly ?? false;
573
+ const indexPrice = (0, _pipmath_1.decimalToPip)(market.indexPrice);
574
+ const leverageParameters = (0, quantities_1.convertToLeverageParametersBigInt)(market);
575
+ const maximumPositionSize = (0, _pipmath_1.decimalToPip)(market.maximumPositionSize);
576
+ const maintenanceMarginFraction = (0, _pipmath_1.decimalToPip)(market.maintenanceMarginFraction);
577
+ const overrideDecimal = (args.walletInitialMarginFractionOverrides ?? []).find((imfo) => imfo.market === market.market)?.initialMarginFractionOverride;
578
+ const initialMarginFractionOverride = typeof overrideDecimal === 'string' ? (0, _pipmath_1.decimalToPip)(overrideDecimal) : null;
579
+ // The wallet's effective taker fee rate already reflects the lowest of the
580
+ // exchange, market, and wallet rates.
581
+ const takerFeeRate = (0, _pipmath_1.decimalToPip)(wallet.takerFeeRate);
582
+ const gasFeePerOrder = args.takerTradeGasFee ?? BigInt(0);
583
+ // Maker-side order book levels, best price first.
584
+ const makerLevels = (isBuy ? args.orderBook.asks : args.orderBook.bids)
585
+ .filter((level) => level.size > BigInt(0))
586
+ .slice()
587
+ .sort((a, b) => isBuy ? Number(a.price - b.price) : Number(b.price - a.price));
588
+ // Execution price limits.
589
+ const bestAsk = args.orderBook.asks.length > 0 ? args.orderBook.asks[0].price : null;
590
+ const bestBid = args.orderBook.bids.length > 0 ? args.orderBook.bids[0].price : null;
591
+ const baselinePrice = bestAsk !== null && bestBid !== null ?
592
+ (bestAsk + bestBid) / BigInt(2)
593
+ : indexPrice;
594
+ const marketOrderExecutionPriceLimit = (0, _pipmath_1.decimalToPip)(market.marketOrderExecutionPriceLimit);
595
+ let minimumExecutionPrice = null;
596
+ let maximumExecutionPrice = null;
597
+ if (isMarketOrder &&
598
+ marketOrderExecutionPriceLimit > BigInt(0) &&
599
+ baselinePrice > BigInt(0)) {
600
+ minimumExecutionPrice = (0, _pipmath_1.multiplyPips)(baselinePrice, _pipmath_1.oneInPips - marketOrderExecutionPriceLimit);
601
+ maximumExecutionPrice = (0, _pipmath_1.dividePips)(baselinePrice, _pipmath_1.oneInPips - marketOrderExecutionPriceLimit);
602
+ }
603
+ const limitOrderExecutionPriceLimit = (0, _pipmath_1.decimalToPip)(market.limitOrderExecutionPriceLimit);
604
+ let limitOrderPriceExceedsLimit = false;
605
+ if (!isMarketOrder &&
606
+ limitOrderExecutionPriceLimit > BigInt(0) &&
607
+ baselinePrice > BigInt(0)) {
608
+ const minimumLimitPrice = (0, _pipmath_1.multiplyPips)(baselinePrice, _pipmath_1.oneInPips - limitOrderExecutionPriceLimit);
609
+ const maximumLimitPrice = (0, _pipmath_1.dividePips)(baselinePrice, _pipmath_1.oneInPips - limitOrderExecutionPriceLimit);
610
+ limitOrderPriceExceedsLimit =
611
+ limitPrice < minimumLimitPrice || limitPrice > maximumLimitPrice;
612
+ }
613
+ // The wallet's resting limit orders in the order's market. Untriggered stop
614
+ // orders have the `active` status; they are excluded because they cannot be
615
+ // matched or self-traded and do not require held collateral.
616
+ const restingOrderStatuses = ['open', 'partiallyFilled'];
617
+ const marketStandingOrders = (args.walletsStandingOrders ?? [])
618
+ .filter((standingOrder) => standingOrder.market === market.market &&
619
+ typeof standingOrder.price !== 'undefined' &&
620
+ restingOrderStatuses.includes(standingOrder.status))
621
+ .map((standingOrder) => ({
622
+ isBuy: standingOrder.side === request_1.OrderSide.buy,
623
+ // The filter above guarantees a defined price.
624
+ price: (0, _pipmath_1.decimalToPip)(standingOrder.price ?? '0'),
625
+ openQuantity: (0, _pipmath_1.decimalToPip)(standingOrder.originalQuantity) -
626
+ (0, _pipmath_1.decimalToPip)(standingOrder.executedQuantity),
627
+ }))
628
+ .filter((standingOrder) => standingOrder.openQuantity > BigInt(0));
629
+ // Position and account-level aggregates.
630
+ const positions = wallet.positions ?? [];
631
+ const equity = (0, _pipmath_1.decimalToPip)(wallet.equity);
632
+ const quoteBalance = (0, _pipmath_1.decimalToPip)(wallet.quoteBalance);
633
+ const totalHeldCollateral = (0, _pipmath_1.decimalToPip)(wallet.heldCollateral);
634
+ let initialMarginRequirement = BigInt(0);
635
+ let currentPositionQuantity = BigInt(0);
636
+ let currentInitialMarginRequirement = BigInt(0);
637
+ for (const position of positions) {
638
+ initialMarginRequirement += (0, _pipmath_1.decimalToPip)(position.marginRequirement);
639
+ if (position.market === market.market) {
640
+ currentPositionQuantity = (0, _pipmath_1.decimalToPip)(position.quantity);
641
+ currentInitialMarginRequirement = (0, _pipmath_1.decimalToPip)(position.marginRequirement);
642
+ }
643
+ }
644
+ const totalMaintenanceMarginRequirement = (0, _pipmath_1.multiplyPips)((0, _pipmath_1.decimalToPip)(wallet.marginRatio), equity);
645
+ // Σ (every position's notional at index price) == equity - quoteBalance
646
+ const otherPositionsNotionalAtIndex = equity - quoteBalance;
647
+ return {
648
+ side,
649
+ isBuy,
650
+ isMarketOrder,
651
+ limitPrice,
652
+ timeInForce,
653
+ reduceOnly,
654
+ indexPrice,
655
+ leverageParameters,
656
+ maximumPositionSize,
657
+ initialMarginFractionOverride,
658
+ maintenanceMarginFraction,
659
+ takerFeeRate,
660
+ gasFeePerOrder,
661
+ makerLevels,
662
+ minimumExecutionPrice,
663
+ maximumExecutionPrice,
664
+ limitOrderPriceExceedsLimit,
665
+ marketStandingOrders,
666
+ currentPositionQuantity,
667
+ equity,
668
+ quoteBalance,
669
+ initialMarginRequirement,
670
+ currentInitialMarginRequirement,
671
+ totalHeldCollateral,
672
+ totalMaintenanceMarginRequirement,
673
+ otherPositionsNotionalAtIndex,
674
+ };
675
+ }
676
+ /**
677
+ * Generates an estimate of how a taker order submitted via the buy/sell panel
678
+ * would be executed by the trading engine. The estimate takes into account
679
+ * trade and gas fees, the (incremental) initial margin fraction and its
680
+ * overrides, the maximum position size, collateral freed up by the reduction of
681
+ * positions, the effect of differences between the execution and index price on
682
+ * collateral, self-trades, held collateral (for standing orders), and changes
683
+ * in held collateral as a result of self-trades and position size changes.
684
+ *
685
+ * The minimum taker quantity is intentionally ignored; the estimate generates
686
+ * results for quantities that do not meet the minimum.
687
+ *
688
+ * The order's quantity may be expressed in base asset terms, in quote asset
689
+ * terms, or as a fraction of the wallet's available collateral to consume (the
690
+ * panel's slider). See {@link BuySellPanelEstimateQuantity}.
691
+ *
692
+ * All quantities, prices, and collateral values are expressed in pips
693
+ * (see {@link decimalToPip}).
694
+ */
695
+ function calculateBuySellPanelEstimate(args) {
696
+ const { order } = args;
697
+ const quantityInputCount = (typeof order.baseQuantity !== 'undefined' ? 1 : 0) +
698
+ (typeof order.quoteQuantity !== 'undefined' ? 1 : 0) +
699
+ (typeof order.availableCollateralRatio !== 'undefined' ? 1 : 0);
700
+ if (quantityInputCount !== 1) {
701
+ throw new Error('Provide exactly one of baseQuantity, quoteQuantity, or availableCollateralRatio');
702
+ }
703
+ const context = buildContext(args);
704
+ if (typeof order.quoteQuantity !== 'undefined') {
705
+ return runEstimate(context, { quoteQuantity: order.quoteQuantity });
706
+ }
707
+ if (typeof order.baseQuantity !== 'undefined') {
708
+ return runEstimate(context, { baseQuantity: order.baseQuantity });
709
+ }
710
+ const baseQuantity = resolveBaseQuantityForCollateralRatio(context, order.availableCollateralRatio);
711
+ return runEstimate(context, { baseQuantity });
712
+ }
713
+ exports.calculateBuySellPanelEstimate = calculateBuySellPanelEstimate;