@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.
- package/dist/orderbook/buySellPanelEstimate.d.ts +155 -0
- package/dist/orderbook/buySellPanelEstimate.d.ts.map +1 -0
- package/dist/orderbook/buySellPanelEstimate.js +713 -0
- package/dist/orderbook/index.d.ts +1 -0
- package/dist/orderbook/index.d.ts.map +1 -1
- package/dist/orderbook/index.js +1 -0
- package/dist/tests/orderbook/buySellPanelEstimate.test.d.ts +2 -0
- package/dist/tests/orderbook/buySellPanelEstimate.test.d.ts.map +1 -0
- package/dist/tests/orderbook/buySellPanelEstimate.test.js +618 -0
- package/package.json +1 -1
|
@@ -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;
|