@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":"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"}
|
|
@@ -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 &&
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
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
|
-
|
|
590
|
-
const
|
|
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
|
|
711
|
-
|
|
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.
|
|
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",
|