@pear-protocol/utils 1.3.0 → 1.5.0

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/index.d.ts CHANGED
@@ -8,6 +8,7 @@ export * from './position/index';
8
8
  export * from './precise-price';
9
9
  export * from './precise-quantity';
10
10
  export * from './proxy';
11
+ export * from './reduce-only-minimum';
11
12
  export * from './sync/index';
12
13
  export * from './validate-leverage';
13
14
  export * from './validate-quantity';
package/dist/index.js CHANGED
@@ -1188,21 +1188,56 @@ BigNumber7.config({
1188
1188
  DECIMAL_PLACES: 30,
1189
1189
  ROUNDING_MODE: BigNumber7.ROUND_DOWN
1190
1190
  });
1191
- function precisePrice(connector, raw, precision, side = "BUY", slippageToleranceBps = 0) {
1191
+ var InvalidPriceError = class extends Error {
1192
+ static {
1193
+ __name(this, "InvalidPriceError");
1194
+ }
1195
+ };
1196
+ function precisePrice(connector, raw, precision, side = "BUY", slippageToleranceBps = 0, rounding = "marketable") {
1192
1197
  const normalized = normalizePrice(raw);
1198
+ if (!normalized.isFinite() || !normalized.isGreaterThan(0)) {
1199
+ throw new InvalidPriceError(`Price ${String(raw)} is not a positive, finite price`);
1200
+ }
1193
1201
  const effectiveRaw = withSlippageTolerance(normalized, slippageToleranceBps, side);
1202
+ const roundingMode = resolveRoundingMode(side, rounding);
1203
+ const precise = preciseOnGrid(connector, effectiveRaw, precision, roundingMode);
1204
+ const clampsToOneStep = rounding === "marketable" && side === "SELL" && !new BigNumber7(precise).isGreaterThan(0);
1205
+ const result = clampsToOneStep ? smallestGridPrice(connector, precision) : precise;
1206
+ const resultBN = new BigNumber7(result);
1207
+ if (!resultBN.isFinite() || !resultBN.isGreaterThan(0)) {
1208
+ throw new InvalidPriceError(`Price ${effectiveRaw.toFixed()} has no valid ${side} price on the grid`);
1209
+ }
1210
+ return result;
1211
+ }
1212
+ __name(precisePrice, "precisePrice");
1213
+ function preciseOnGrid(connector, price, precision, rounding) {
1194
1214
  switch (connector) {
1195
1215
  case "hyperliquid":
1196
- return preciseHyperliquidPrice(effectiveRaw, precision, side);
1216
+ return preciseHyperliquidPrice(price, precision, rounding);
1197
1217
  case "binance":
1198
1218
  case "bybit":
1199
1219
  case "okx":
1200
1220
  case "lighter":
1201
1221
  case "lighter_rhc":
1202
- return preciseTickGridPrice(effectiveRaw, precision, side);
1222
+ return preciseTickGridPrice(price, precision, rounding);
1203
1223
  }
1204
1224
  }
1205
- __name(precisePrice, "precisePrice");
1225
+ __name(preciseOnGrid, "preciseOnGrid");
1226
+ function smallestGridPrice(connector, precision) {
1227
+ return preciseOnGrid(connector, new BigNumber7(1).shiftedBy(-precision.price.decimals), precision, BigNumber7.ROUND_CEIL);
1228
+ }
1229
+ __name(smallestGridPrice, "smallestGridPrice");
1230
+ function resolveRoundingMode(side, rounding) {
1231
+ switch (rounding) {
1232
+ // Towards crossing: the buffer survives rounding.
1233
+ case "marketable":
1234
+ return side === "BUY" ? BigNumber7.ROUND_CEIL : BigNumber7.ROUND_FLOOR;
1235
+ // Away from crossing: the limit holds.
1236
+ case "within_limit":
1237
+ return side === "BUY" ? BigNumber7.ROUND_FLOOR : BigNumber7.ROUND_CEIL;
1238
+ }
1239
+ }
1240
+ __name(resolveRoundingMode, "resolveRoundingMode");
1206
1241
  function withSlippageTolerance(normalized, slippageToleranceBps, side) {
1207
1242
  if (slippageToleranceBps === 0) {
1208
1243
  return normalized;
@@ -1212,18 +1247,16 @@ function withSlippageTolerance(normalized, slippageToleranceBps, side) {
1212
1247
  return side === "BUY" ? normalized.times(new BigNumber7(1).plus(factor)) : normalized.times(new BigNumber7(1).minus(factor));
1213
1248
  }
1214
1249
  __name(withSlippageTolerance, "withSlippageTolerance");
1215
- function preciseHyperliquidPrice(price, precision, side) {
1250
+ function preciseHyperliquidPrice(price, precision, rounding) {
1216
1251
  const maxDecimals = precision.price.decimals;
1217
1252
  const maxSigFigs = precision.price.sigFigs ?? 5;
1218
- const rounding = side === "BUY" ? BigNumber7.ROUND_FLOOR : BigNumber7.ROUND_CEIL;
1219
1253
  price = price.decimalPlaces(maxDecimals, rounding);
1220
1254
  price = clampSigFigs(price, maxSigFigs, maxDecimals, rounding);
1221
1255
  return price.toFixed();
1222
1256
  }
1223
1257
  __name(preciseHyperliquidPrice, "preciseHyperliquidPrice");
1224
- function preciseTickGridPrice(price, precision, side) {
1258
+ function preciseTickGridPrice(price, precision, rounding) {
1225
1259
  const tick = precision.price.tick ?? 1e-4;
1226
- const rounding = side === "BUY" ? BigNumber7.ROUND_FLOOR : BigNumber7.ROUND_CEIL;
1227
1260
  const tickBN = new BigNumber7(tick);
1228
1261
  const scaled = price.div(tickBN);
1229
1262
  const snapped = scaled.integerValue(rounding).times(tickBN);
@@ -1237,13 +1270,13 @@ function clampSigFigs(price, maxSigFigs, maxDecimals, rounding) {
1237
1270
  if (countSigFigs(price) <= maxSigFigs) {
1238
1271
  return price;
1239
1272
  }
1240
- for (let decimals = maxDecimals; decimals >= 0; decimals--) {
1273
+ for (let decimals = maxDecimals; decimals >= 1; decimals--) {
1241
1274
  const candidate = price.decimalPlaces(decimals, rounding);
1242
1275
  if (countSigFigs(candidate) <= maxSigFigs) {
1243
1276
  return candidate;
1244
1277
  }
1245
1278
  }
1246
- return price;
1279
+ return price.decimalPlaces(0, rounding);
1247
1280
  }
1248
1281
  __name(clampSigFigs, "clampSigFigs");
1249
1282
  function normalizePrice(value) {
@@ -1330,6 +1363,85 @@ function proxyFetch(input, init) {
1330
1363
  }), () => fetch(input, init));
1331
1364
  }
1332
1365
  __name(proxyFetch, "proxyFetch");
1366
+ function validateQuantity(quantity, price, precision) {
1367
+ const parsedQuantity = new BigNumber7(quantity);
1368
+ if (!parsedQuantity.isFinite() || parsedQuantity.lte(0)) {
1369
+ return false;
1370
+ }
1371
+ const parsedPrice = new BigNumber7(price);
1372
+ if (!parsedPrice.isFinite() || parsedPrice.lte(0)) {
1373
+ return false;
1374
+ }
1375
+ const step = new BigNumber7(precision.quantity.step);
1376
+ const scaled = parsedQuantity.div(step);
1377
+ if (!scaled.isInteger()) {
1378
+ return false;
1379
+ }
1380
+ const minQuantity = precision.quantity.min;
1381
+ if (minQuantity !== void 0 && minQuantity !== null && parsedQuantity.lt(minQuantity)) {
1382
+ return false;
1383
+ }
1384
+ const notional = contractsToBase(parsedQuantity, precision.quantity.contractSize).times(parsedPrice);
1385
+ if (precision.notional?.min !== void 0 && precision.notional.min !== null && precision.notional.min > 0) {
1386
+ if (notional.lt(new BigNumber7(precision.notional.min))) {
1387
+ return false;
1388
+ }
1389
+ }
1390
+ if (precision.notional?.max !== void 0 && precision.notional.max !== null && precision.notional.max > 0) {
1391
+ if (notional.gt(new BigNumber7(precision.notional.max))) {
1392
+ return false;
1393
+ }
1394
+ }
1395
+ return true;
1396
+ }
1397
+ __name(validateQuantity, "validateQuantity");
1398
+
1399
+ // src/reduce-only-minimum.ts
1400
+ function findWholeCloseQuantity(input) {
1401
+ if (input.heldByAnotherPosition) {
1402
+ return void 0;
1403
+ }
1404
+ if (input.exposure === void 0 || isZero(input.exposure)) {
1405
+ return void 0;
1406
+ }
1407
+ if (input.side === "BUY" !== isNegative(input.exposure)) {
1408
+ return void 0;
1409
+ }
1410
+ const { step, contractSize } = input.precision.quantity;
1411
+ const exposureQuantity = divide(abs(input.exposure), String(contractSize));
1412
+ const coversExposure = isGreaterThanOrEqual(input.quantity, exposureQuantity);
1413
+ const requestsExposure = isGreaterThanOrEqual(input.requestedQuantity, abs(input.exposure));
1414
+ const roundedShort = isGreaterThan(input.requestedQuantity, subtract(abs(input.exposure), multiply(String(step), String(contractSize))));
1415
+ const refusedShort = roundedShort && !validateQuantity(input.quantity, input.price, input.precision);
1416
+ if (!coversExposure && !requestsExposure && !refusedShort) {
1417
+ return void 0;
1418
+ }
1419
+ const steps = divide(exposureQuantity, String(step)).integerValue(BigNumber7.ROUND_CEIL);
1420
+ return multiply(steps, String(step)).toFixed();
1421
+ }
1422
+ __name(findWholeCloseQuantity, "findWholeCloseQuantity");
1423
+ function requestedBaseQuantity(leg, referencePrice) {
1424
+ switch (leg.mode) {
1425
+ case "QUANTITY":
1426
+ return leg.amount;
1427
+ case "USD":
1428
+ return divide(leg.amount, referencePrice).toFixed();
1429
+ }
1430
+ }
1431
+ __name(requestedBaseQuantity, "requestedBaseQuantity");
1432
+ function isRefusedBelowMinimum(quantity, price, precision, heldByAnotherPosition) {
1433
+ if (!heldByAnotherPosition) {
1434
+ return false;
1435
+ }
1436
+ const minQuantity = precision.quantity.min;
1437
+ if (minQuantity !== void 0 && minQuantity !== null && isLessThan(quantity, String(minQuantity))) {
1438
+ return true;
1439
+ }
1440
+ const notional = multiply(contractsToBase(quantity, precision.quantity.contractSize), price);
1441
+ const minNotional = precision.notional?.min;
1442
+ return minNotional !== void 0 && minNotional !== null && isLessThan(notional, String(minNotional));
1443
+ }
1444
+ __name(isRefusedBelowMinimum, "isRefusedBelowMinimum");
1333
1445
 
1334
1446
  // src/sync/applicables/constants.ts
1335
1447
  var DUST = parse("0.00000001");
@@ -1674,7 +1786,7 @@ function buildMissedHistoryFills(args) {
1674
1786
  });
1675
1787
  result.push({
1676
1788
  connector,
1677
- exchangeFillId: `${MISSED_HISTORY_FILL_PREFIX}${symbol}-${gap.side.toLowerCase()}`,
1789
+ exchangeFillId: `${MISSED_HISTORY_FILL_PREFIX}${symbol}-${gap.side.toLowerCase()}-${watermark}`,
1678
1790
  cloid: null,
1679
1791
  symbol,
1680
1792
  price,
@@ -2658,34 +2770,6 @@ function validateLeverage(leverage, instrument) {
2658
2770
  return true;
2659
2771
  }
2660
2772
  __name(validateLeverage, "validateLeverage");
2661
- function validateQuantity(quantity, price, precision) {
2662
- const parsedQuantity = new BigNumber7(quantity);
2663
- if (!parsedQuantity.isFinite() || parsedQuantity.lte(0)) {
2664
- return false;
2665
- }
2666
- const parsedPrice = new BigNumber7(price);
2667
- if (!parsedPrice.isFinite() || parsedPrice.lte(0)) {
2668
- return false;
2669
- }
2670
- const step = new BigNumber7(precision.quantity.step);
2671
- const scaled = parsedQuantity.div(step);
2672
- if (!scaled.isInteger()) {
2673
- return false;
2674
- }
2675
- const notional = contractsToBase(parsedQuantity, precision.quantity.contractSize).times(parsedPrice);
2676
- if (precision.notional?.min !== void 0 && precision.notional.min !== null && precision.notional.min > 0) {
2677
- if (notional.lt(new BigNumber7(precision.notional.min))) {
2678
- return false;
2679
- }
2680
- }
2681
- if (precision.notional?.max !== void 0 && precision.notional.max !== null && precision.notional.max > 0) {
2682
- if (notional.gt(new BigNumber7(precision.notional.max))) {
2683
- return false;
2684
- }
2685
- }
2686
- return true;
2687
- }
2688
- __name(validateQuantity, "validateQuantity");
2689
2773
  var VAULT_ACTION_DOMAIN = {
2690
2774
  name: "PearVaultService",
2691
2775
  version: "1",
@@ -2997,4 +3081,4 @@ function takeVerifier(handoffId, storage) {
2997
3081
  }
2998
3082
  __name(takeVerifier, "takeVerifier");
2999
3083
 
3000
- export { BIPS, CREATE_VAULT_TYPES, VAULT_ACTION_DOMAIN, VAULT_ACTION_TYPES, ZERO, abs, add, aggregateRealizedPositionState, baseToContracts, buildApplicables, buildCreateVaultMessage, buildLeaderActionTypedData, compareValue, computeAssetEntryNotional, computeAssetGrossRealizedPnL, computeAssetRealizedPnLs, computeAssetUndeterminedSizes, computeAssetUnrealizedPnL, computeAssetUnrealizedPnLs, computeBasketWeightedRatioV1, computeDeployedCapital, computeDeployedCapitalState, computeEntryEquityFraction, computeEntryPriceForAsset, computeEquityFraction, computeFillTransitions, computeFilledEntrySizing, computeMaxBasketSize, computeMultiQuoteBasketSize, computeNotionalEquityFraction, computePositionEntryPrices, computeRealizedPnlBySymbol, computeRealizedPnlFromFills, computeRealizedPositionState, computeSyncPayload, computeUnrealizedPositionState, computeUnrealizedPositionStateFromEntryPrices, configureProxy, contractsToBase, countDecimals, deriveBasketKey, divide, exponentiate, finishXHandoff, generateDeterministicPositionKey, groupFillsByAsset, groupFillsByPosition, isEqualTo, isFiniteDecimal, isGreaterThan, isGreaterThanOrEqual, isLessThan, isLessThanOrEqual, isNegative, isPositive, isZero, min, multiply, negate, parse, precisePrice, preciseQuantity, proxyFetch, sideSign, sign, signed, signedBySide, sizeBasket, sizeBasketByLeverageMode, sizeBasketPerAssetLeverage, startXHandoff, stripXReturn, subtract, sumAttributedFunding, toDecimalString, validateLeverage, validateQuantity, walkPositionLedger, walkPositionTransitions };
3084
+ export { BIPS, CREATE_VAULT_TYPES, InvalidPriceError, VAULT_ACTION_DOMAIN, VAULT_ACTION_TYPES, ZERO, abs, add, aggregateRealizedPositionState, baseToContracts, buildApplicables, buildCreateVaultMessage, buildLeaderActionTypedData, compareValue, computeAssetEntryNotional, computeAssetGrossRealizedPnL, computeAssetRealizedPnLs, computeAssetUndeterminedSizes, computeAssetUnrealizedPnL, computeAssetUnrealizedPnLs, computeBasketWeightedRatioV1, computeDeployedCapital, computeDeployedCapitalState, computeEntryEquityFraction, computeEntryPriceForAsset, computeEquityFraction, computeFillTransitions, computeFilledEntrySizing, computeMaxBasketSize, computeMultiQuoteBasketSize, computeNotionalEquityFraction, computePositionEntryPrices, computeRealizedPnlBySymbol, computeRealizedPnlFromFills, computeRealizedPositionState, computeSyncPayload, computeUnrealizedPositionState, computeUnrealizedPositionStateFromEntryPrices, configureProxy, contractsToBase, countDecimals, deriveBasketKey, divide, exponentiate, findWholeCloseQuantity, finishXHandoff, generateDeterministicPositionKey, groupFillsByAsset, groupFillsByPosition, isEqualTo, isFiniteDecimal, isGreaterThan, isGreaterThanOrEqual, isLessThan, isLessThanOrEqual, isNegative, isPositive, isRefusedBelowMinimum, isZero, min, multiply, negate, parse, precisePrice, preciseQuantity, proxyFetch, requestedBaseQuantity, sideSign, sign, signed, signedBySide, sizeBasket, sizeBasketByLeverageMode, sizeBasketPerAssetLeverage, startXHandoff, stripXReturn, subtract, sumAttributedFunding, toDecimalString, validateLeverage, validateQuantity, walkPositionLedger, walkPositionTransitions };
@@ -1,8 +1,30 @@
1
1
  import type { Connector, InstrumentPrecision, Side } from '@pear-protocol/types';
2
2
  import BigNumber from 'bignumber.js';
3
+ /**
4
+ * Which way a price is rounded onto the exchange grid.
5
+ * - `marketable`: towards crossing (BUY up, SELL down), so a market order keeps its slippage buffer
6
+ * and still reaches the opposite touch; it may cost up to one grid step beyond the buffer (#785).
7
+ * - `within_limit`: away from crossing (BUY down, SELL up), so a user-set limit is never exceeded.
8
+ */
9
+ export type PriceRounding = 'marketable' | 'within_limit';
10
+ /**
11
+ * A price `precisePrice` produced: on the venue's grid and greater than zero (#1157), so it can be an
12
+ * order's limit. USD sizing deliberately does not require it: an unconstrained leg is sized off the
13
+ * unpadded mid, a raw reference price (#1145). It is still a string, so it passes anywhere a string is
14
+ * accepted, `validateQuantity` included.
15
+ */
16
+ export type GridPrice = string & {
17
+ readonly __brand: 'GridPrice';
18
+ };
19
+ /**
20
+ * Thrown by `precisePrice` when no placeable price exists: a raw price that is not positive and
21
+ * finite, or a limit that rounds to zero on the grid (a BUY cap below one grid step).
22
+ */
23
+ export declare class InvalidPriceError extends Error {
24
+ }
3
25
  /**
4
26
  * Round a raw price to an exchange-valid string for the given connector,
5
- * biased so the order stays marketable (BUY rounds down, SELL rounds up),
6
- * optionally padded with slippage tolerance first.
27
+ * optionally padded with slippage tolerance first, biased per `rounding`.
28
+ * The result is always on the grid and above zero; otherwise this throws `InvalidPriceError`.
7
29
  */
8
- export declare function precisePrice(connector: Connector, raw: BigNumber.Value, precision: InstrumentPrecision, side?: Side, slippageToleranceBps?: number): string;
30
+ export declare function precisePrice(connector: Connector, raw: BigNumber.Value, precision: InstrumentPrecision, side?: Side, slippageToleranceBps?: number, rounding?: PriceRounding): GridPrice;
@@ -0,0 +1,42 @@
1
+ /** When a reduce-only order is held to the venue minimum (#1184). Pure, so the backend and the frontend share one rule. */
2
+ import type { InstrumentPrecision, Side } from '@pear-protocol/types';
3
+ /**
4
+ * The quantity that closes the whole venue position a reduce-only leg reduces, or undefined when the leg
5
+ * is only a partial reduce. Under #1184's rule a whole close is the only reduce-only order exempt from the
6
+ * venue minimum; this is Pear's product rule, not a venue constraint (Binance, for one, exempts every
7
+ * reduce-only order from its minimum order value, -4164, though not from its minimum quantity, -4004).
8
+ *
9
+ * The venue nets every position of the account holding the asset, so the leg is a whole close only when no
10
+ * other position holds the asset, its side opposes the exposure (a BUY closes a short, a SELL a long), and
11
+ * it covers the exposure: its rounded quantity reaches it, the quantity requested before rounding reaches it
12
+ * (so a request for the whole position always closes it), or that request falls short of it by less than
13
+ * one step and the rounded partial would be refused at the order price, so a USD amount sized at a slightly
14
+ * different price that rounds a step short below the minimum still closes it, while a partial the venue
15
+ * accepts, or an exact reduce of one step less, stays partial. A whole close is
16
+ * the exposure rounded up onto the venue's step grid; the venue caps reduce-only at the position. The
17
+ * quantity and the returned quantity are in the venue's contract units (base units where a contract is one
18
+ * unit); the requested quantity and the exposure are in base units.
19
+ */
20
+ export declare function findWholeCloseQuantity(input: {
21
+ quantity: string;
22
+ requestedQuantity: string;
23
+ side: Side;
24
+ exposure: string | undefined;
25
+ price: string;
26
+ precision: InstrumentPrecision;
27
+ heldByAnotherPosition: boolean;
28
+ }): string | undefined;
29
+ /**
30
+ * The base quantity a leg asked for before venue rounding: a quantity leg's own amount, a USD leg's amount
31
+ * at the price it is sized against.
32
+ */
33
+ export declare function requestedBaseQuantity(leg: {
34
+ mode: 'QUANTITY' | 'USD';
35
+ amount: string;
36
+ }, referencePrice: string): string;
37
+ /**
38
+ * Whether a close leg is refused for its size. A close places the position's whole exposure, so it closes
39
+ * the venue position, valid at any size, unless another position holds the asset: then the venue reads it
40
+ * as a partial reduce, held to its minimum order quantity and minimum order value at the order price (#1184).
41
+ */
42
+ export declare function isRefusedBelowMinimum(quantity: string, price: string, precision: InstrumentPrecision, heldByAnotherPosition: boolean): boolean;
@@ -1,6 +1,7 @@
1
1
  import type { InstrumentPrecision } from '@pear-protocol/types';
2
2
  /**
3
- * True when an order quantity is positive, step-aligned, and within the
4
- * instrument's optional notional bounds at the given price.
3
+ * True when an order quantity is positive, step-aligned, at or above the venue's
4
+ * optional minimum order quantity, and within the instrument's optional notional
5
+ * bounds at the given price.
5
6
  */
6
7
  export declare function validateQuantity(quantity: string, price: string, precision: InstrumentPrecision): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pear-protocol/utils",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Pear Protocol Utility functions",
5
5
  "private": false,
6
6
  "type": "module",
@@ -23,11 +23,11 @@
23
23
  "test": "vitest run",
24
24
  "build": "tsup && tsc -p tsconfig.build.json",
25
25
  "clean": "rm -rf dist .turbo",
26
- "typecheck": "tsc --noEmit"
26
+ "typecheck": "bunx --package bun@1.4.3 bun --check --threads=2"
27
27
  },
28
28
  "dependencies": {
29
29
  "@noble/hashes": "^1.3.2",
30
- "@pear-protocol/types": "^2.12.0",
30
+ "@pear-protocol/types": "^2.21.0",
31
31
  "@scure/base": "^2.3.0",
32
32
  "bignumber.js": "9.3.1"
33
33
  },