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

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.
@@ -120,6 +120,8 @@ export interface BuySellPanelEstimate {
120
120
  maximumPositionSizeExceeded: boolean;
121
121
  /** `true` if a post-only ({@link TimeInForce.gtx gtx}) order would cross the spread (rejected) */
122
122
  postOnlyWouldCross: boolean;
123
+ /** `true` if an immediate-or-cancel ({@link TimeInForce.ioc ioc}) order matched no liquidity (nothing would execute) */
124
+ immediateOrCancelWouldNotExecute: boolean;
123
125
  /** `true` if a fill-or-kill ({@link TimeInForce.fok fok}) order could not be fully filled (rejected) */
124
126
  fillOrKillWouldNotExecute: boolean;
125
127
  /** `true` if a reduce-only order is on the same side as the open position (rejected) */
@@ -1 +1 @@
1
- {"version":3,"file":"buySellPanelEstimate.d.ts","sourceRoot":"","sources":["../../src/orderbook/buySellPanelEstimate.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"buySellPanelEstimate.d.ts","sourceRoot":"","sources":["../../src/orderbook/buySellPanelEstimate.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAE9D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,KAAK,EACV,wCAAwC,EACxC,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EAClB,MAAM,6BAA6B,CAAC;AAUrC;;;;;;;GAOG;AACH,MAAM,MAAM,4BAA4B,GACpC;IACE,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B,wBAAwB,CAAC,EAAE,SAAS,CAAC;CACtC,GACD;IACE,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,wBAAwB,CAAC,EAAE,SAAS,CAAC;CACtC,GACD;IACE,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB,aAAa,CAAC,EAAE,SAAS,CAAC;IAC1B,sFAAsF;IACtF,wBAAwB,EAAE,MAAM,CAAC;CAClC,CAAC;AAEN;;;;;;;GAOG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,IAAI,EAAE,SAAS,CAAC;IAChB,6CAA6C;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0CAA0C;IAC1C,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,0BAA0B;IAC1B,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,GAAG,4BAA4B,CAAC;AAEjC,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,IAAI,CACV,iBAAiB,EACf,QAAQ,GACR,YAAY,GACZ,MAAM,kBAAkB,GACxB,2BAA2B,GAC3B,gCAAgC,GAChC,+BAA+B,CAClC,CAAC;IACF;;;OAGG;IACH,MAAM,EAAE,IAAI,CACV,iBAAiB,EACf,QAAQ,GACR,gBAAgB,GAChB,cAAc,GACd,aAAa,GACb,cAAc,GACd,cAAc,GACd,WAAW,CACd,CAAC;IACF;;;;;OAKG;IACH,SAAS,EAAE;QAAE,IAAI,EAAE,gBAAgB,EAAE,CAAC;QAAC,IAAI,EAAE,gBAAgB,EAAE,CAAA;KAAE,CAAC;IAClE;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,IAAI,CAC1B,gBAAgB,EACd,QAAQ,GACR,MAAM,GACN,OAAO,GACP,kBAAkB,GAClB,kBAAkB,GAClB,QAAQ,CACX,EAAE,CAAC;IACJ;;;;OAIG;IACH,oCAAoC,CAAC,EAAE,wCAAwC,EAAE,CAAC;IAClF;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,EAAE,iBAAiB,CAAC;CAC1B;AAED,MAAM,WAAW,oBAAoB;IACnC,kFAAkF;IAClF,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mFAAmF;IACnF,kBAAkB,EAAE,MAAM,CAAC;IAC3B,6EAA6E;IAC7E,iBAAiB,EAAE,MAAM,CAAC;IAC1B;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,6EAA6E;IAC7E,oBAAoB,EAAE,OAAO,CAAC;IAC9B;;;OAGG;IACH,sBAAsB,EAAE,OAAO,CAAC;IAChC;;;OAGG;IACH,2BAA2B,EAAE,OAAO,CAAC;IACrC;;;OAGG;IACH,2BAA2B,EAAE,OAAO,CAAC;IACrC,8EAA8E;IAC9E,2BAA2B,EAAE,OAAO,CAAC;IACrC,kGAAkG;IAClG,kBAAkB,EAAE,OAAO,CAAC;IAC5B,wHAAwH;IACxH,gCAAgC,EAAE,OAAO,CAAC;IAC1C,wGAAwG;IACxG,yBAAyB,EAAE,OAAO,CAAC;IACnC,wFAAwF;IACxF,gCAAgC,EAAE,OAAO,CAAC;IAC1C,+EAA+E;IAC/E,wBAAwB,EAAE,OAAO,CAAC;IAClC;;;OAGG;IACH,kCAAkC,EAAE,OAAO,CAAC;CAC7C;AAqiCD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,6BAA6B,CAC3C,IAAI,EAAE,wBAAwB,GAC7B,oBAAoB,CA4BtB"}
@@ -24,6 +24,7 @@ function makeEmptyEstimate() {
24
24
  executionPriceLimitExceeded: false,
25
25
  maximumPositionSizeExceeded: false,
26
26
  postOnlyWouldCross: false,
27
+ immediateOrCancelWouldNotExecute: false,
27
28
  fillOrKillWouldNotExecute: false,
28
29
  reduceOnlyWouldNotReducePosition: false,
29
30
  reduceOnlyNoOpenPosition: false,
@@ -128,7 +129,11 @@ function calculateLiquidationPrice(args) {
128
129
  * (self-trades) from other liquidity, and applying execution price limits and
129
130
  * taker fees.
130
131
  */
131
- function matchTakerOrder(context, quantity, reduceOnlyMaximumBaseQuantity) {
132
+ function matchTakerOrder(context, quantity, reduceOnlyMaximumBaseQuantity,
133
+ // When `false`, the order keeps matching through the book past the execution
134
+ // price limit (the breach is still flagged on the result) rather than stopping
135
+ // at it. Used when sizing the slider so it can consume the target collateral.
136
+ enforceExecutionPriceLimit) {
132
137
  const isQuantityInQuote = 'quoteQuantity' in quantity;
133
138
  // Working copy of own maker-side resting liquidity, indexed by price.
134
139
  const standingOrdersCopy = context.marketStandingOrders.map((order) => ({
@@ -159,6 +164,13 @@ function matchTakerOrder(context, quantity, reduceOnlyMaximumBaseQuantity) {
159
164
  // Tracks reduce-only fill capacity (real, position-reducing fills only)
160
165
  let remainingReduceOnly = reduceOnlyMaximumBaseQuantity;
161
166
  for (const level of context.makerLevels) {
167
+ // Stop once the taker order's quantity is fully consumed.
168
+ const exhausted = remainingBase !== null ?
169
+ remainingBase <= BigInt(0)
170
+ : remainingQuote <= BigInt(0);
171
+ if (exhausted) {
172
+ break;
173
+ }
162
174
  // doOrdersMatch
163
175
  if (!context.isMarketOrder) {
164
176
  const crosses = context.isBuy ?
@@ -168,20 +180,6 @@ function matchTakerOrder(context, quantity, reduceOnlyMaximumBaseQuantity) {
168
180
  break;
169
181
  }
170
182
  }
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
183
  // Determine how much of this level the taker consumes.
186
184
  let consumedBase;
187
185
  if (remainingBase !== null) {
@@ -197,6 +195,24 @@ function matchTakerOrder(context, quantity, reduceOnlyMaximumBaseQuantity) {
197
195
  consumedBase = (level.size * remainingQuote) / levelQuoteIfFull;
198
196
  }
199
197
  }
198
+ // Nothing more can be filled at this (best remaining) price — e.g. a
199
+ // sub-pip quote remainder left by flooring. The order is complete, so worse
200
+ // levels it never reaches must not trip the execution price limit.
201
+ if (consumedBase <= BigInt(0)) {
202
+ break;
203
+ }
204
+ // A trade occurs at this level's price, so apply the execution price limit.
205
+ // When not enforcing (slider sizing), matching continues but the breach is
206
+ // still recorded on the result.
207
+ if ((context.minimumExecutionPrice !== null &&
208
+ level.price < context.minimumExecutionPrice) ||
209
+ (context.maximumExecutionPrice !== null &&
210
+ level.price > context.maximumExecutionPrice)) {
211
+ result.executionPriceLimitExceeded = true;
212
+ if (enforceExecutionPriceLimit) {
213
+ break;
214
+ }
215
+ }
200
216
  // Split into self-trade (own liquidity) and real fill (other liquidity).
201
217
  const ownOrders = ownMakerOrdersByPrice.get(level.price) ?? [];
202
218
  let ownSizeAtLevel = BigInt(0);
@@ -272,7 +288,19 @@ function matchTakerOrder(context, quantity, reduceOnlyMaximumBaseQuantity) {
272
288
  * @private
273
289
  * Produces a complete estimate for a concrete base or quote quantity.
274
290
  */
275
- function runEstimate(context, quantity) {
291
+ function runEstimate(context, quantity,
292
+ // Whether the *input* quantity is zero. A zero-quantity order does nothing, so
293
+ // none of the time-in-force feasibility flags apply to it. This is based on
294
+ // the input (e.g. the slider ratio) rather than the resolved base quantity: a
295
+ // non-zero slider on an order that cannot execute or rest resolves to a base
296
+ // quantity of zero, yet its time-in-force flags should still be evaluated.
297
+ isZeroQuantity = ('baseQuantity' in quantity ?
298
+ quantity.baseQuantity
299
+ : quantity.quoteQuantity) <= BigInt(0),
300
+ // When `false`, matching continues past the execution price limit (still
301
+ // flagged) instead of stopping at it. Used for slider sizing so the order can
302
+ // be sized to consume the target collateral.
303
+ enforceExecutionPriceLimit = true) {
276
304
  const estimate = makeEmptyEstimate();
277
305
  // Reduce-only validity and fill capacity.
278
306
  let reduceOnlyMaximumBaseQuantity = null;
@@ -297,7 +325,9 @@ function runEstimate(context, quantity) {
297
325
  (context.isBuy ?
298
326
  context.limitPrice >= bestMakerPrice
299
327
  : context.limitPrice <= bestMakerPrice));
300
- if (context.timeInForce === request_1.TimeInForce.gtx && crossesSpread) {
328
+ if (context.timeInForce === request_1.TimeInForce.gtx &&
329
+ crossesSpread &&
330
+ !isZeroQuantity) {
301
331
  estimate.postOnlyWouldCross = true;
302
332
  return estimate;
303
333
  }
@@ -306,18 +336,39 @@ function runEstimate(context, quantity) {
306
336
  estimate.executionPriceLimitExceeded = true;
307
337
  return estimate;
308
338
  }
309
- const fill = matchTakerOrder(context, quantity, reduceOnlyMaximumBaseQuantity);
339
+ const fill = matchTakerOrder(context, quantity, reduceOnlyMaximumBaseQuantity, enforceExecutionPriceLimit);
310
340
  estimate.selfTradeEncountered = fill.selfTradeEncountered;
311
341
  estimate.executionPriceLimitExceeded = fill.executionPriceLimitExceeded;
342
+ // Immediate-or-cancel: the unfilled remainder is canceled rather than rested,
343
+ // so an order that matched no liquidity would not execute at all.
344
+ estimate.immediateOrCancelWouldNotExecute =
345
+ context.timeInForce === request_1.TimeInForce.ioc &&
346
+ !isZeroQuantity &&
347
+ fill.tradeBaseQuantity === BigInt(0);
312
348
  // 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) {
349
+ if (context.timeInForce === request_1.TimeInForce.fok && !isZeroQuantity) {
350
+ let fullyFillable;
351
+ if ('baseQuantity' in quantity) {
352
+ // Base quantities are matched exactly (no rounding), so the bounded fill
353
+ // determines fillability directly.
354
+ fullyFillable =
355
+ fill.tradeBaseQuantity + fill.selfTradeBaseQuantity >=
356
+ quantity.baseQuantity;
357
+ }
358
+ else {
359
+ // Quote fills are floored to whole base pips, so the achievable quote is
360
+ // the largest value not exceeding the requested quote and rarely equals it
361
+ // exactly. Decide fillability from the *total* crossable quote liquidity
362
+ // instead: re-match with an unbounded budget (every crossable level fills
363
+ // in full, so no flooring occurs).
364
+ const unboundedQuote = context.makerLevels.reduce((sum, level) => sum + (0, _pipmath_1.multiplyPips)(level.size, level.price), BigInt(1));
365
+ const maxFill = matchTakerOrder(context, { quoteQuantity: unboundedQuote }, reduceOnlyMaximumBaseQuantity, enforceExecutionPriceLimit);
366
+ fullyFillable =
367
+ maxFill.tradeQuoteQuantity +
368
+ (0, _pipmath_1.multiplyPips)(maxFill.selfTradeBaseQuantity, context.indexPrice) >=
369
+ quantity.quoteQuantity;
370
+ }
371
+ if (!fullyFillable) {
321
372
  estimate.fillOrKillWouldNotExecute = true;
322
373
  return estimate;
323
374
  }
@@ -494,7 +545,9 @@ function resolveBaseQuantityForCollateralRatio(context, ratio) {
494
545
  return BigInt(0);
495
546
  }
496
547
  // Upper bound: all matchable liquidity, plus resting capacity for limit
497
- // orders that may add to the books.
548
+ // orders that may add to the books. Liquidity beyond the execution price limit
549
+ // is included: the slider matches through the book (the breach is flagged on
550
+ // the result rather than capping the size).
498
551
  let matchableBase = BigInt(0);
499
552
  for (const level of context.makerLevels) {
500
553
  if (!context.isMarketOrder) {
@@ -505,12 +558,6 @@ function resolveBaseQuantityForCollateralRatio(context, ratio) {
505
558
  break;
506
559
  }
507
560
  }
508
- if ((context.minimumExecutionPrice !== null &&
509
- level.price < context.minimumExecutionPrice) ||
510
- (context.maximumExecutionPrice !== null &&
511
- level.price > context.maximumExecutionPrice)) {
512
- break;
513
- }
514
561
  matchableBase += level.size;
515
562
  }
516
563
  const canRest = !context.isMarketOrder &&
@@ -537,7 +584,9 @@ function resolveBaseQuantityForCollateralRatio(context, ratio) {
537
584
  // plateau and yields the largest quantity that drives available collateral to
538
585
  // ~zero while remaining acceptable.
539
586
  const isAcceptable = (baseQuantity) => {
540
- const e = runEstimate(context, { baseQuantity });
587
+ // Size against the full book (do not cap at the execution price limit), so a
588
+ // breach does not prevent the slider from consuming the target collateral.
589
+ const e = runEstimate(context, { baseQuantity }, undefined, false);
541
590
  return (e.cost <= targetCost &&
542
591
  !e.freeCollateralExceeded &&
543
592
  !e.availableCollateralExceeded);
@@ -585,9 +634,16 @@ function buildContext(args) {
585
634
  .filter((level) => level.size > BigInt(0))
586
635
  .slice()
587
636
  .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;
637
+ // Execution price limits. The best bid/ask are taken defensively (the input
638
+ // book is not assumed to be sorted), ignoring empty levels.
639
+ const positiveAsks = args.orderBook.asks.filter((l) => l.size > BigInt(0));
640
+ const positiveBids = args.orderBook.bids.filter((l) => l.size > BigInt(0));
641
+ const bestAsk = positiveAsks.length > 0 ?
642
+ positiveAsks.reduce((best, l) => (l.price < best ? l.price : best), positiveAsks[0].price)
643
+ : null;
644
+ const bestBid = positiveBids.length > 0 ?
645
+ positiveBids.reduce((best, l) => (l.price > best ? l.price : best), positiveBids[0].price)
646
+ : null;
591
647
  const baselinePrice = bestAsk !== null && bestBid !== null ?
592
648
  (bestAsk + bestBid) / BigInt(2)
593
649
  : indexPrice;
@@ -707,7 +763,12 @@ function calculateBuySellPanelEstimate(args) {
707
763
  if (typeof order.baseQuantity !== 'undefined') {
708
764
  return runEstimate(context, { baseQuantity: order.baseQuantity });
709
765
  }
710
- const baseQuantity = resolveBaseQuantityForCollateralRatio(context, order.availableCollateralRatio);
711
- return runEstimate(context, { baseQuantity });
766
+ const ratio = order.availableCollateralRatio;
767
+ const baseQuantity = resolveBaseQuantityForCollateralRatio(context, ratio);
768
+ // Base the zero-quantity determination on the slider input, not the resolved
769
+ // base quantity (which is zero when the order cannot execute or rest). The
770
+ // slider also matches through the book rather than capping at the execution
771
+ // price limit (the breach is still flagged).
772
+ return runEstimate(context, { baseQuantity }, ratio <= BigInt(0), false);
712
773
  }
713
774
  exports.calculateBuySellPanelEstimate = calculateBuySellPanelEstimate;
@@ -174,6 +174,70 @@ describe('orderbook/buySellPanelEstimate', () => {
174
174
  });
175
175
  testHelpers.assertBigintsEqual(estimate.tradeBaseQuantity, (0, _pipmath_1.decimalToPip)('4'));
176
176
  testHelpers.assertBigintsEqual(estimate.makerBaseQuantity, (0, _pipmath_1.decimalToPip)('0'));
177
+ // Some liquidity matched, so the order executes.
178
+ expect(estimate.immediateOrCancelWouldNotExecute).to.equal(false);
179
+ });
180
+ it('flags an ioc order that matches no liquidity', () => {
181
+ const estimate = runEstimate({
182
+ orderBook: { asks: [level('100', '10')], bids: [] },
183
+ order: {
184
+ side: request_1.OrderSide.buy,
185
+ baseQuantity: (0, _pipmath_1.decimalToPip)('5'),
186
+ limitPrice: (0, _pipmath_1.decimalToPip)('99'), // below the best ask: does not cross
187
+ timeInForce: request_1.TimeInForce.ioc,
188
+ },
189
+ });
190
+ expect(estimate.immediateOrCancelWouldNotExecute).to.equal(true);
191
+ testHelpers.assertBigintsEqual(estimate.tradeBaseQuantity, (0, _pipmath_1.decimalToPip)('0'));
192
+ testHelpers.assertBigintsEqual(estimate.makerBaseQuantity, (0, _pipmath_1.decimalToPip)('0'));
193
+ });
194
+ it('flags a non-zero-slider ioc order that matches no liquidity', () => {
195
+ const estimate = runEstimate({
196
+ orderBook: { asks: [level('100', '10')], bids: [] },
197
+ order: {
198
+ side: request_1.OrderSide.buy,
199
+ // Non-zero slider, but the limit price does not cross and ioc can't
200
+ // rest, so it resolves to a zero base quantity.
201
+ availableCollateralRatio: (0, _pipmath_1.decimalToPip)('0.5'),
202
+ limitPrice: (0, _pipmath_1.decimalToPip)('99'),
203
+ timeInForce: request_1.TimeInForce.ioc,
204
+ },
205
+ });
206
+ expect(estimate.immediateOrCancelWouldNotExecute).to.equal(true);
207
+ testHelpers.assertBigintsEqual(estimate.tradeBaseQuantity, (0, _pipmath_1.decimalToPip)('0'));
208
+ });
209
+ it('sets no time-in-force flags for a zero-quantity order', () => {
210
+ for (const timeInForce of [
211
+ request_1.TimeInForce.gtc,
212
+ request_1.TimeInForce.gtx,
213
+ request_1.TimeInForce.ioc,
214
+ request_1.TimeInForce.fok,
215
+ ]) {
216
+ // Crosses the spread (gtx would otherwise flag) with a zero quantity.
217
+ const estimate = runEstimate({
218
+ orderBook: { asks: [level('100', '10')], bids: [] },
219
+ order: {
220
+ side: request_1.OrderSide.buy,
221
+ baseQuantity: (0, _pipmath_1.decimalToPip)('0'),
222
+ limitPrice: (0, _pipmath_1.decimalToPip)('100'),
223
+ timeInForce,
224
+ },
225
+ });
226
+ expect(estimate.postOnlyWouldCross, timeInForce).to.equal(false);
227
+ expect(estimate.immediateOrCancelWouldNotExecute, timeInForce).to.equal(false);
228
+ expect(estimate.fillOrKillWouldNotExecute, timeInForce).to.equal(false);
229
+ }
230
+ // A 0% slider resolves to a zero quantity as well.
231
+ const sliderEstimate = runEstimate({
232
+ orderBook: { asks: [level('100', '10')], bids: [] },
233
+ order: {
234
+ side: request_1.OrderSide.buy,
235
+ limitPrice: (0, _pipmath_1.decimalToPip)('100'),
236
+ timeInForce: request_1.TimeInForce.ioc,
237
+ availableCollateralRatio: (0, _pipmath_1.decimalToPip)('0'),
238
+ },
239
+ });
240
+ expect(sliderEstimate.immediateOrCancelWouldNotExecute).to.equal(false);
177
241
  });
178
242
  it('flags a post-only (gtx) order that would cross the spread', () => {
179
243
  const estimate = runEstimate({
@@ -215,6 +279,37 @@ describe('orderbook/buySellPanelEstimate', () => {
215
279
  expect(estimate.fillOrKillWouldNotExecute).to.equal(true);
216
280
  testHelpers.assertBigintsEqual(estimate.tradeBaseQuantity, (0, _pipmath_1.decimalToPip)('0'));
217
281
  });
282
+ it('fills a quote-denominated fill-or-kill order to the largest quote not exceeding the request', () => {
283
+ const estimate = runEstimate({
284
+ // Price 7 does not divide 100 evenly; ample liquidity (700 quote).
285
+ orderBook: { asks: [level('7', '100')], bids: [] },
286
+ order: {
287
+ side: request_1.OrderSide.buy,
288
+ quoteQuantity: (0, _pipmath_1.decimalToPip)('100'),
289
+ limitPrice: (0, _pipmath_1.decimalToPip)('10'),
290
+ timeInForce: request_1.TimeInForce.fok,
291
+ },
292
+ });
293
+ // 14.28571428 @ 7 = 99.99999996 (largest quote <= 100); not killed.
294
+ expect(estimate.fillOrKillWouldNotExecute).to.equal(false);
295
+ testHelpers.assertBigintsEqual(estimate.tradeBaseQuantity, (0, _pipmath_1.decimalToPip)('14.28571428'));
296
+ testHelpers.assertBigintsEqual(estimate.tradeQuoteQuantity, (0, _pipmath_1.decimalToPip)('99.99999996'));
297
+ expect(estimate.tradeQuoteQuantity <= (0, _pipmath_1.decimalToPip)('100')).to.equal(true);
298
+ });
299
+ it('kills a quote-denominated fill-or-kill order without enough liquidity', () => {
300
+ const estimate = runEstimate({
301
+ // Only 7 of crossable quote (1 @ 7) vs a 100 quote request.
302
+ orderBook: { asks: [level('7', '1')], bids: [] },
303
+ order: {
304
+ side: request_1.OrderSide.buy,
305
+ quoteQuantity: (0, _pipmath_1.decimalToPip)('100'),
306
+ limitPrice: (0, _pipmath_1.decimalToPip)('10'),
307
+ timeInForce: request_1.TimeInForce.fok,
308
+ },
309
+ });
310
+ expect(estimate.fillOrKillWouldNotExecute).to.equal(true);
311
+ testHelpers.assertBigintsEqual(estimate.tradeBaseQuantity, (0, _pipmath_1.decimalToPip)('0'));
312
+ });
218
313
  it('detects self-trades and frees the matched order’s held collateral', () => {
219
314
  // The book’s ask at 100 (size 10) includes the wallet’s own 3-unit sell.
220
315
  const estimate = runEstimate({
@@ -323,6 +418,66 @@ describe('orderbook/buySellPanelEstimate', () => {
323
418
  // Only the 10 available at 100 (within the limit) are filled
324
419
  testHelpers.assertBigintsEqual(estimate.tradeBaseQuantity, (0, _pipmath_1.decimalToPip)('10'));
325
420
  });
421
+ it('does not flag the execution price limit when the order fills entirely within it', () => {
422
+ const estimate = runEstimate({
423
+ market: {
424
+ ...defaultMarket,
425
+ marketOrderExecutionPriceLimit: '0.01000000', // 1%
426
+ },
427
+ orderBook: {
428
+ // best ask 100.1, plus worse levels beyond the 1% limit (max ~101.01)
429
+ asks: [level('100.1', '10'), level('102', '10'), level('103', '10')],
430
+ bids: [level('99.9', '10'), level('98', '10')],
431
+ },
432
+ // baseline 100; fully fills 5 @ 100.1 and never reaches the 102 level
433
+ order: { side: request_1.OrderSide.buy, baseQuantity: (0, _pipmath_1.decimalToPip)('5') },
434
+ });
435
+ expect(estimate.executionPriceLimitExceeded).to.equal(false);
436
+ testHelpers.assertBigintsEqual(estimate.tradeBaseQuantity, (0, _pipmath_1.decimalToPip)('5'));
437
+ });
438
+ it('does not flag the execution price limit for a quote order that fills within it', () => {
439
+ const estimate = runEstimate({
440
+ market: {
441
+ ...defaultMarket,
442
+ marketOrderExecutionPriceLimit: '0.01000000', // 1%
443
+ },
444
+ orderBook: {
445
+ asks: [level('100.1', '10'), level('102', '10'), level('103', '10')],
446
+ bids: [level('99.9', '10')],
447
+ },
448
+ // 50 quote fills within the best ask (well under its 1,001 quote of
449
+ // depth); the worse 102 level is never reached.
450
+ order: { side: request_1.OrderSide.buy, quoteQuantity: (0, _pipmath_1.decimalToPip)('50') },
451
+ });
452
+ expect(estimate.executionPriceLimitExceeded).to.equal(false);
453
+ expect(estimate.tradeBaseQuantity > (0, _pipmath_1.decimalToPip)('0')).to.equal(true);
454
+ });
455
+ it('matches a slider order through the book past the execution price limit and flags it', () => {
456
+ const estimate = runEstimate({
457
+ market: {
458
+ ...defaultMarket,
459
+ marketOrderExecutionPriceLimit: '0.01000000', // 1%, max ~101.01
460
+ },
461
+ wallet: {
462
+ ...defaultWallet,
463
+ equity: '100.00000000',
464
+ quoteBalance: '100.00000000',
465
+ },
466
+ orderBook: {
467
+ // Only 2 of liquidity within the limit; the rest is beyond it.
468
+ asks: [level('100.1', '2'), level('102', '10'), level('103', '10')],
469
+ bids: [level('99.9', '10')],
470
+ },
471
+ order: { side: request_1.OrderSide.buy, availableCollateralRatio: _pipmath_1.oneInPips }, // 100%
472
+ });
473
+ // The slider sizes the order to consume the available collateral by
474
+ // matching beyond the limit, and flags the breach rather than capping.
475
+ expect(estimate.executionPriceLimitExceeded).to.equal(true);
476
+ expect(estimate.tradeBaseQuantity > (0, _pipmath_1.decimalToPip)('2')).to.equal(true);
477
+ const target = (0, _pipmath_1.decimalToPip)('100');
478
+ expect(estimate.cost <= target).to.equal(true);
479
+ expect(target - estimate.cost <= (0, _pipmath_1.decimalToPip)('1')).to.equal(true);
480
+ });
326
481
  it('flags a limit order whose price is outside the allowed range', () => {
327
482
  const estimate = runEstimate({
328
483
  market: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@katanaperps/katana-perps-sdk",
3
- "version": "2.1.0-beta.14",
3
+ "version": "2.1.0-beta.15",
4
4
  "description": "Katana Perps SDK for Javascript in the browser and Node.js",
5
5
  "repository": "git@github.com:katanaperps/katana-perps-sdk-js.git",
6
6
  "license": "MIT",