@katanaperps/katana-perps-sdk 2.1.0-beta.14 → 2.1.0-beta.16
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) => ({
|
|
@@ -149,6 +154,7 @@ function matchTakerOrder(context, quantity, reduceOnlyMaximumBaseQuantity) {
|
|
|
149
154
|
takerTradeFee: BigInt(0),
|
|
150
155
|
takerGasFee: BigInt(0),
|
|
151
156
|
selfTradeBaseQuantity: BigInt(0),
|
|
157
|
+
selfTradeQuoteQuantity: BigInt(0),
|
|
152
158
|
selfTradeEncountered: false,
|
|
153
159
|
executionPriceLimitExceeded: false,
|
|
154
160
|
standingOrdersAfterSelfTrade: standingOrdersCopy,
|
|
@@ -159,6 +165,13 @@ function matchTakerOrder(context, quantity, reduceOnlyMaximumBaseQuantity) {
|
|
|
159
165
|
// Tracks reduce-only fill capacity (real, position-reducing fills only)
|
|
160
166
|
let remainingReduceOnly = reduceOnlyMaximumBaseQuantity;
|
|
161
167
|
for (const level of context.makerLevels) {
|
|
168
|
+
// Stop once the taker order's quantity is fully consumed.
|
|
169
|
+
const exhausted = remainingBase !== null ?
|
|
170
|
+
remainingBase <= BigInt(0)
|
|
171
|
+
: remainingQuote <= BigInt(0);
|
|
172
|
+
if (exhausted) {
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
162
175
|
// doOrdersMatch
|
|
163
176
|
if (!context.isMarketOrder) {
|
|
164
177
|
const crosses = context.isBuy ?
|
|
@@ -168,20 +181,6 @@ function matchTakerOrder(context, quantity, reduceOnlyMaximumBaseQuantity) {
|
|
|
168
181
|
break;
|
|
169
182
|
}
|
|
170
183
|
}
|
|
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
184
|
// Determine how much of this level the taker consumes.
|
|
186
185
|
let consumedBase;
|
|
187
186
|
if (remainingBase !== null) {
|
|
@@ -197,6 +196,24 @@ function matchTakerOrder(context, quantity, reduceOnlyMaximumBaseQuantity) {
|
|
|
197
196
|
consumedBase = (level.size * remainingQuote) / levelQuoteIfFull;
|
|
198
197
|
}
|
|
199
198
|
}
|
|
199
|
+
// Nothing more can be filled at this (best remaining) price — e.g. a
|
|
200
|
+
// sub-pip quote remainder left by flooring. The order is complete, so worse
|
|
201
|
+
// levels it never reaches must not trip the execution price limit.
|
|
202
|
+
if (consumedBase <= BigInt(0)) {
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
// A trade occurs at this level's price, so apply the execution price limit.
|
|
206
|
+
// When not enforcing (slider sizing), matching continues but the breach is
|
|
207
|
+
// still recorded on the result.
|
|
208
|
+
if ((context.minimumExecutionPrice !== null &&
|
|
209
|
+
level.price < context.minimumExecutionPrice) ||
|
|
210
|
+
(context.maximumExecutionPrice !== null &&
|
|
211
|
+
level.price > context.maximumExecutionPrice)) {
|
|
212
|
+
result.executionPriceLimitExceeded = true;
|
|
213
|
+
if (enforceExecutionPriceLimit) {
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
200
217
|
// Split into self-trade (own liquidity) and real fill (other liquidity).
|
|
201
218
|
const ownOrders = ownMakerOrdersByPrice.get(level.price) ?? [];
|
|
202
219
|
let ownSizeAtLevel = BigInt(0);
|
|
@@ -215,6 +232,7 @@ function matchTakerOrder(context, quantity, reduceOnlyMaximumBaseQuantity) {
|
|
|
215
232
|
if (selfPart > BigInt(0)) {
|
|
216
233
|
result.selfTradeEncountered = true;
|
|
217
234
|
result.selfTradeBaseQuantity += selfPart;
|
|
235
|
+
result.selfTradeQuoteQuantity += (0, _pipmath_1.multiplyPips)(selfPart, level.price, context.isMarketOrder && context.isBuy);
|
|
218
236
|
// Reduce the wallet's own resting orders at this price.
|
|
219
237
|
let toReduce = selfPart;
|
|
220
238
|
for (const order of ownOrders) {
|
|
@@ -272,7 +290,19 @@ function matchTakerOrder(context, quantity, reduceOnlyMaximumBaseQuantity) {
|
|
|
272
290
|
* @private
|
|
273
291
|
* Produces a complete estimate for a concrete base or quote quantity.
|
|
274
292
|
*/
|
|
275
|
-
function runEstimate(context, quantity
|
|
293
|
+
function runEstimate(context, quantity,
|
|
294
|
+
// Whether the *input* quantity is zero. A zero-quantity order does nothing, so
|
|
295
|
+
// none of the time-in-force feasibility flags apply to it. This is based on
|
|
296
|
+
// the input (e.g. the slider ratio) rather than the resolved base quantity: a
|
|
297
|
+
// non-zero slider on an order that cannot execute or rest resolves to a base
|
|
298
|
+
// quantity of zero, yet its time-in-force flags should still be evaluated.
|
|
299
|
+
isZeroQuantity = ('baseQuantity' in quantity ?
|
|
300
|
+
quantity.baseQuantity
|
|
301
|
+
: quantity.quoteQuantity) <= BigInt(0),
|
|
302
|
+
// When `false`, matching continues past the execution price limit (still
|
|
303
|
+
// flagged) instead of stopping at it. Used for slider sizing so the order can
|
|
304
|
+
// be sized to consume the target collateral.
|
|
305
|
+
enforceExecutionPriceLimit = true) {
|
|
276
306
|
const estimate = makeEmptyEstimate();
|
|
277
307
|
// Reduce-only validity and fill capacity.
|
|
278
308
|
let reduceOnlyMaximumBaseQuantity = null;
|
|
@@ -297,7 +327,9 @@ function runEstimate(context, quantity) {
|
|
|
297
327
|
(context.isBuy ?
|
|
298
328
|
context.limitPrice >= bestMakerPrice
|
|
299
329
|
: context.limitPrice <= bestMakerPrice));
|
|
300
|
-
if (context.timeInForce === request_1.TimeInForce.gtx &&
|
|
330
|
+
if (context.timeInForce === request_1.TimeInForce.gtx &&
|
|
331
|
+
crossesSpread &&
|
|
332
|
+
!isZeroQuantity) {
|
|
301
333
|
estimate.postOnlyWouldCross = true;
|
|
302
334
|
return estimate;
|
|
303
335
|
}
|
|
@@ -306,24 +338,51 @@ function runEstimate(context, quantity) {
|
|
|
306
338
|
estimate.executionPriceLimitExceeded = true;
|
|
307
339
|
return estimate;
|
|
308
340
|
}
|
|
309
|
-
const fill = matchTakerOrder(context, quantity, reduceOnlyMaximumBaseQuantity);
|
|
341
|
+
const fill = matchTakerOrder(context, quantity, reduceOnlyMaximumBaseQuantity, enforceExecutionPriceLimit);
|
|
310
342
|
estimate.selfTradeEncountered = fill.selfTradeEncountered;
|
|
311
343
|
estimate.executionPriceLimitExceeded = fill.executionPriceLimitExceeded;
|
|
344
|
+
// Immediate-or-cancel: the unfilled remainder is canceled rather than rested,
|
|
345
|
+
// so an order that matched no liquidity would not execute at all.
|
|
346
|
+
estimate.immediateOrCancelWouldNotExecute =
|
|
347
|
+
context.timeInForce === request_1.TimeInForce.ioc &&
|
|
348
|
+
!isZeroQuantity &&
|
|
349
|
+
fill.tradeBaseQuantity === BigInt(0);
|
|
312
350
|
// 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
|
-
|
|
351
|
+
if (context.timeInForce === request_1.TimeInForce.fok && !isZeroQuantity) {
|
|
352
|
+
let fullyFillable;
|
|
353
|
+
if ('baseQuantity' in quantity) {
|
|
354
|
+
// Base quantities are matched exactly (no rounding), so the bounded fill
|
|
355
|
+
// determines fillability directly.
|
|
356
|
+
fullyFillable =
|
|
357
|
+
fill.tradeBaseQuantity + fill.selfTradeBaseQuantity >=
|
|
358
|
+
quantity.baseQuantity;
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
// Quote fills are floored to whole base pips, so the achievable quote is
|
|
362
|
+
// the largest value not exceeding the requested quote and rarely equals it
|
|
363
|
+
// exactly. Decide fillability from the *total* crossable quote liquidity
|
|
364
|
+
// instead: re-match with an unbounded budget (every crossable level fills
|
|
365
|
+
// in full, so no flooring occurs).
|
|
366
|
+
const unboundedQuote = context.makerLevels.reduce((sum, level) => sum + (0, _pipmath_1.multiplyPips)(level.size, level.price), BigInt(1));
|
|
367
|
+
const maxFill = matchTakerOrder(context, { quoteQuantity: unboundedQuote }, reduceOnlyMaximumBaseQuantity, enforceExecutionPriceLimit);
|
|
368
|
+
fullyFillable =
|
|
369
|
+
maxFill.tradeQuoteQuantity + maxFill.selfTradeQuoteQuantity >=
|
|
370
|
+
quantity.quoteQuantity;
|
|
371
|
+
}
|
|
372
|
+
if (!fullyFillable) {
|
|
321
373
|
estimate.fillOrKillWouldNotExecute = true;
|
|
322
374
|
return estimate;
|
|
323
375
|
}
|
|
324
376
|
}
|
|
325
|
-
|
|
326
|
-
|
|
377
|
+
// The crossing (trade) quantity includes any self-traded quantity: the taker
|
|
378
|
+
// order must be submitted with enough quantity to traverse the book and reach
|
|
379
|
+
// the wallet's own resting order, otherwise that self-trade would not occur.
|
|
380
|
+
// (Position, collateral and liquidation effects below use only the real
|
|
381
|
+
// fills, since a self-trade does not change the position or quote balance.)
|
|
382
|
+
estimate.tradeBaseQuantity =
|
|
383
|
+
fill.tradeBaseQuantity + fill.selfTradeBaseQuantity;
|
|
384
|
+
estimate.tradeQuoteQuantity =
|
|
385
|
+
fill.tradeQuoteQuantity + fill.selfTradeQuoteQuantity;
|
|
327
386
|
// Determine the resting (maker) portion. Reduce-only limit orders may rest on
|
|
328
387
|
// the books (their reducing portion); whether the resting quantity is valid is
|
|
329
388
|
// checked below via `reduceOnlyOpenPositionSizeExceeded`.
|
|
@@ -336,10 +395,11 @@ function runEstimate(context, quantity) {
|
|
|
336
395
|
makerBaseQuantity = fill.remainingBaseQuantity;
|
|
337
396
|
}
|
|
338
397
|
else {
|
|
339
|
-
// Convert the remaining quote budget to base at the limit price.
|
|
398
|
+
// Convert the remaining quote budget to base at the limit price. The
|
|
399
|
+
// crossing portion (real fills + self-trades) has consumed the rest.
|
|
340
400
|
const remainingQuote = (0, _pipmath_1.maxBigInt)(quantity.quoteQuantity -
|
|
341
401
|
fill.tradeQuoteQuantity -
|
|
342
|
-
|
|
402
|
+
fill.selfTradeQuoteQuantity, BigInt(0));
|
|
343
403
|
makerBaseQuantity =
|
|
344
404
|
context.limitPrice > BigInt(0) ?
|
|
345
405
|
(0, _pipmath_1.dividePips)(remainingQuote, context.limitPrice)
|
|
@@ -494,7 +554,9 @@ function resolveBaseQuantityForCollateralRatio(context, ratio) {
|
|
|
494
554
|
return BigInt(0);
|
|
495
555
|
}
|
|
496
556
|
// Upper bound: all matchable liquidity, plus resting capacity for limit
|
|
497
|
-
// orders that may add to the books.
|
|
557
|
+
// orders that may add to the books. Liquidity beyond the execution price limit
|
|
558
|
+
// is included: the slider matches through the book (the breach is flagged on
|
|
559
|
+
// the result rather than capping the size).
|
|
498
560
|
let matchableBase = BigInt(0);
|
|
499
561
|
for (const level of context.makerLevels) {
|
|
500
562
|
if (!context.isMarketOrder) {
|
|
@@ -505,12 +567,6 @@ function resolveBaseQuantityForCollateralRatio(context, ratio) {
|
|
|
505
567
|
break;
|
|
506
568
|
}
|
|
507
569
|
}
|
|
508
|
-
if ((context.minimumExecutionPrice !== null &&
|
|
509
|
-
level.price < context.minimumExecutionPrice) ||
|
|
510
|
-
(context.maximumExecutionPrice !== null &&
|
|
511
|
-
level.price > context.maximumExecutionPrice)) {
|
|
512
|
-
break;
|
|
513
|
-
}
|
|
514
570
|
matchableBase += level.size;
|
|
515
571
|
}
|
|
516
572
|
const canRest = !context.isMarketOrder &&
|
|
@@ -537,7 +593,9 @@ function resolveBaseQuantityForCollateralRatio(context, ratio) {
|
|
|
537
593
|
// plateau and yields the largest quantity that drives available collateral to
|
|
538
594
|
// ~zero while remaining acceptable.
|
|
539
595
|
const isAcceptable = (baseQuantity) => {
|
|
540
|
-
|
|
596
|
+
// Size against the full book (do not cap at the execution price limit), so a
|
|
597
|
+
// breach does not prevent the slider from consuming the target collateral.
|
|
598
|
+
const e = runEstimate(context, { baseQuantity }, undefined, false);
|
|
541
599
|
return (e.cost <= targetCost &&
|
|
542
600
|
!e.freeCollateralExceeded &&
|
|
543
601
|
!e.availableCollateralExceeded);
|
|
@@ -585,9 +643,16 @@ function buildContext(args) {
|
|
|
585
643
|
.filter((level) => level.size > BigInt(0))
|
|
586
644
|
.slice()
|
|
587
645
|
.sort((a, b) => isBuy ? Number(a.price - b.price) : Number(b.price - a.price));
|
|
588
|
-
// Execution price limits.
|
|
589
|
-
|
|
590
|
-
const
|
|
646
|
+
// Execution price limits. The best bid/ask are taken defensively (the input
|
|
647
|
+
// book is not assumed to be sorted), ignoring empty levels.
|
|
648
|
+
const positiveAsks = args.orderBook.asks.filter((l) => l.size > BigInt(0));
|
|
649
|
+
const positiveBids = args.orderBook.bids.filter((l) => l.size > BigInt(0));
|
|
650
|
+
const bestAsk = positiveAsks.length > 0 ?
|
|
651
|
+
positiveAsks.reduce((best, l) => (l.price < best ? l.price : best), positiveAsks[0].price)
|
|
652
|
+
: null;
|
|
653
|
+
const bestBid = positiveBids.length > 0 ?
|
|
654
|
+
positiveBids.reduce((best, l) => (l.price > best ? l.price : best), positiveBids[0].price)
|
|
655
|
+
: null;
|
|
591
656
|
const baselinePrice = bestAsk !== null && bestBid !== null ?
|
|
592
657
|
(bestAsk + bestBid) / BigInt(2)
|
|
593
658
|
: indexPrice;
|
|
@@ -707,7 +772,12 @@ function calculateBuySellPanelEstimate(args) {
|
|
|
707
772
|
if (typeof order.baseQuantity !== 'undefined') {
|
|
708
773
|
return runEstimate(context, { baseQuantity: order.baseQuantity });
|
|
709
774
|
}
|
|
710
|
-
const
|
|
711
|
-
|
|
775
|
+
const ratio = order.availableCollateralRatio;
|
|
776
|
+
const baseQuantity = resolveBaseQuantityForCollateralRatio(context, ratio);
|
|
777
|
+
// Base the zero-quantity determination on the slider input, not the resolved
|
|
778
|
+
// base quantity (which is zero when the order cannot execute or rest). The
|
|
779
|
+
// slider also matches through the book rather than capping at the execution
|
|
780
|
+
// price limit (the breach is still flagged).
|
|
781
|
+
return runEstimate(context, { baseQuantity }, ratio <= BigInt(0), false);
|
|
712
782
|
}
|
|
713
783
|
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({
|
|
@@ -233,11 +328,13 @@ describe('orderbook/buySellPanelEstimate', () => {
|
|
|
233
328
|
order: { side: request_1.OrderSide.buy, baseQuantity: (0, _pipmath_1.decimalToPip)('5') },
|
|
234
329
|
});
|
|
235
330
|
expect(estimate.selfTradeEncountered).to.equal(true);
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
testHelpers.assertBigintsEqual(estimate.
|
|
239
|
-
|
|
240
|
-
//
|
|
331
|
+
// The order must cross all 5 (the 2 others + the 3 own units) to reach the
|
|
332
|
+
// wallet's own order, so the self-traded 3 count toward the trade quantity.
|
|
333
|
+
testHelpers.assertBigintsEqual(estimate.tradeBaseQuantity, (0, _pipmath_1.decimalToPip)('5'));
|
|
334
|
+
testHelpers.assertBigintsEqual(estimate.tradeQuoteQuantity, (0, _pipmath_1.decimalToPip)('500'));
|
|
335
|
+
// Cost reflects only the real 2-unit fill: freeing the 30 held by the
|
|
336
|
+
// canceled own order more than offsets the new position’s margin (20) and
|
|
337
|
+
// fee (0.2), so collateral increases.
|
|
241
338
|
testHelpers.assertBigintsEqual(estimate.cost, (0, _pipmath_1.decimalToPip)('-9.8'));
|
|
242
339
|
});
|
|
243
340
|
it('flags an order that exceeds free collateral', () => {
|
|
@@ -323,6 +420,66 @@ describe('orderbook/buySellPanelEstimate', () => {
|
|
|
323
420
|
// Only the 10 available at 100 (within the limit) are filled
|
|
324
421
|
testHelpers.assertBigintsEqual(estimate.tradeBaseQuantity, (0, _pipmath_1.decimalToPip)('10'));
|
|
325
422
|
});
|
|
423
|
+
it('does not flag the execution price limit when the order fills entirely within it', () => {
|
|
424
|
+
const estimate = runEstimate({
|
|
425
|
+
market: {
|
|
426
|
+
...defaultMarket,
|
|
427
|
+
marketOrderExecutionPriceLimit: '0.01000000', // 1%
|
|
428
|
+
},
|
|
429
|
+
orderBook: {
|
|
430
|
+
// best ask 100.1, plus worse levels beyond the 1% limit (max ~101.01)
|
|
431
|
+
asks: [level('100.1', '10'), level('102', '10'), level('103', '10')],
|
|
432
|
+
bids: [level('99.9', '10'), level('98', '10')],
|
|
433
|
+
},
|
|
434
|
+
// baseline 100; fully fills 5 @ 100.1 and never reaches the 102 level
|
|
435
|
+
order: { side: request_1.OrderSide.buy, baseQuantity: (0, _pipmath_1.decimalToPip)('5') },
|
|
436
|
+
});
|
|
437
|
+
expect(estimate.executionPriceLimitExceeded).to.equal(false);
|
|
438
|
+
testHelpers.assertBigintsEqual(estimate.tradeBaseQuantity, (0, _pipmath_1.decimalToPip)('5'));
|
|
439
|
+
});
|
|
440
|
+
it('does not flag the execution price limit for a quote order that fills within it', () => {
|
|
441
|
+
const estimate = runEstimate({
|
|
442
|
+
market: {
|
|
443
|
+
...defaultMarket,
|
|
444
|
+
marketOrderExecutionPriceLimit: '0.01000000', // 1%
|
|
445
|
+
},
|
|
446
|
+
orderBook: {
|
|
447
|
+
asks: [level('100.1', '10'), level('102', '10'), level('103', '10')],
|
|
448
|
+
bids: [level('99.9', '10')],
|
|
449
|
+
},
|
|
450
|
+
// 50 quote fills within the best ask (well under its 1,001 quote of
|
|
451
|
+
// depth); the worse 102 level is never reached.
|
|
452
|
+
order: { side: request_1.OrderSide.buy, quoteQuantity: (0, _pipmath_1.decimalToPip)('50') },
|
|
453
|
+
});
|
|
454
|
+
expect(estimate.executionPriceLimitExceeded).to.equal(false);
|
|
455
|
+
expect(estimate.tradeBaseQuantity > (0, _pipmath_1.decimalToPip)('0')).to.equal(true);
|
|
456
|
+
});
|
|
457
|
+
it('matches a slider order through the book past the execution price limit and flags it', () => {
|
|
458
|
+
const estimate = runEstimate({
|
|
459
|
+
market: {
|
|
460
|
+
...defaultMarket,
|
|
461
|
+
marketOrderExecutionPriceLimit: '0.01000000', // 1%, max ~101.01
|
|
462
|
+
},
|
|
463
|
+
wallet: {
|
|
464
|
+
...defaultWallet,
|
|
465
|
+
equity: '100.00000000',
|
|
466
|
+
quoteBalance: '100.00000000',
|
|
467
|
+
},
|
|
468
|
+
orderBook: {
|
|
469
|
+
// Only 2 of liquidity within the limit; the rest is beyond it.
|
|
470
|
+
asks: [level('100.1', '2'), level('102', '10'), level('103', '10')],
|
|
471
|
+
bids: [level('99.9', '10')],
|
|
472
|
+
},
|
|
473
|
+
order: { side: request_1.OrderSide.buy, availableCollateralRatio: _pipmath_1.oneInPips }, // 100%
|
|
474
|
+
});
|
|
475
|
+
// The slider sizes the order to consume the available collateral by
|
|
476
|
+
// matching beyond the limit, and flags the breach rather than capping.
|
|
477
|
+
expect(estimate.executionPriceLimitExceeded).to.equal(true);
|
|
478
|
+
expect(estimate.tradeBaseQuantity > (0, _pipmath_1.decimalToPip)('2')).to.equal(true);
|
|
479
|
+
const target = (0, _pipmath_1.decimalToPip)('100');
|
|
480
|
+
expect(estimate.cost <= target).to.equal(true);
|
|
481
|
+
expect(target - estimate.cost <= (0, _pipmath_1.decimalToPip)('1')).to.equal(true);
|
|
482
|
+
});
|
|
326
483
|
it('flags a limit order whose price is outside the allowed range', () => {
|
|
327
484
|
const estimate = runEstimate({
|
|
328
485
|
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.16",
|
|
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",
|