@pear-protocol/utils 1.2.0 → 1.4.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.
@@ -0,0 +1,13 @@
1
+ /** Pure gross-notional valuation for copy sizing. */
2
+ import type { Fill, InstrumentPriceMap, PositionExposure, TradeLeg, UnsignedDecimalString } from '@pear-protocol/types';
3
+ /** Size a non-empty set of validated positive fill notionals without a market-price lookup. */
4
+ export declare function computeFilledEntrySizing(fills: ReadonlyArray<Pick<Fill, 'symbol' | 'usd'>>, equity: UnsignedDecimalString): {
5
+ equityFraction: UnsignedDecimalString;
6
+ legNotionals: Record<string, UnsignedDecimalString>;
7
+ };
8
+ /** Value signed exposure against equity; missing prices contribute no notional. */
9
+ export declare function computeEquityFraction(exposure: PositionExposure, priceMap: InstrumentPriceMap, equity: UnsignedDecimalString): UnsignedDecimalString;
10
+ /** Value USD and quantity entry legs; missing quantity prices contribute no notional. */
11
+ export declare function computeEntryEquityFraction(legs: ReadonlyArray<Pick<TradeLeg, 'symbol' | 'mode' | 'amount'>>, priceMap: InstrumentPriceMap, equity: UnsignedDecimalString): UnsignedDecimalString;
12
+ /** Divide valued notional by validated positive equity without capping leverage. */
13
+ export declare function computeNotionalEquityFraction(notional: Fill['usd'], equity: UnsignedDecimalString): UnsignedDecimalString;
@@ -0,0 +1 @@
1
+ export * from './equity-fraction';
package/dist/index.d.ts CHANGED
@@ -1,12 +1,14 @@
1
1
  export * from './asset/index';
2
2
  export * from './basket/index';
3
3
  export * from './contract-size';
4
+ export * from './copy/index';
4
5
  export * from './fills/index';
5
6
  export * from './math/index';
6
7
  export * from './position/index';
7
8
  export * from './precise-price';
8
9
  export * from './precise-quantity';
9
10
  export * from './proxy';
11
+ export * from './reduce-only-minimum';
10
12
  export * from './sync/index';
11
13
  export * from './validate-leverage';
12
14
  export * from './validate-quantity';
package/dist/index.js CHANGED
@@ -875,6 +875,61 @@ function baseToContracts(base, contractSize) {
875
875
  }
876
876
  __name(baseToContracts, "baseToContracts");
877
877
 
878
+ // src/copy/equity-fraction.ts
879
+ function computeFilledEntrySizing(fills, equity) {
880
+ const notionals = /* @__PURE__ */ new Map();
881
+ let total = parse("0");
882
+ for (const fill of fills) {
883
+ const notional = parse(fill.usd);
884
+ notionals.set(fill.symbol, (notionals.get(fill.symbol) ?? parse("0")).plus(notional));
885
+ total = total.plus(notional);
886
+ }
887
+ const legNotionals = {};
888
+ for (const [symbol, notional] of notionals) {
889
+ legNotionals[symbol] = notional.toFixed();
890
+ }
891
+ return {
892
+ equityFraction: computeNotionalEquityFraction(total.toFixed(), equity),
893
+ legNotionals
894
+ };
895
+ }
896
+ __name(computeFilledEntrySizing, "computeFilledEntrySizing");
897
+ function computeEquityFraction(exposure, priceMap, equity) {
898
+ let notional = parse("0");
899
+ for (const [symbol, quantity] of Object.entries(exposure)) {
900
+ const price = priceMap[symbol];
901
+ if (price !== void 0) {
902
+ notional = notional.plus(parse(quantity).abs().multipliedBy(price));
903
+ }
904
+ }
905
+ return computeNotionalEquityFraction(notional.toFixed(), equity);
906
+ }
907
+ __name(computeEquityFraction, "computeEquityFraction");
908
+ function computeEntryEquityFraction(legs, priceMap, equity) {
909
+ let notional = parse("0");
910
+ for (const leg of legs) {
911
+ const amount = parse(leg.amount).abs();
912
+ switch (leg.mode) {
913
+ case "USD":
914
+ notional = notional.plus(amount);
915
+ break;
916
+ case "QUANTITY": {
917
+ const price = priceMap[leg.symbol];
918
+ if (price !== void 0) {
919
+ notional = notional.plus(amount.multipliedBy(price));
920
+ }
921
+ break;
922
+ }
923
+ }
924
+ }
925
+ return computeNotionalEquityFraction(notional.toFixed(), equity);
926
+ }
927
+ __name(computeEntryEquityFraction, "computeEntryEquityFraction");
928
+ function computeNotionalEquityFraction(notional, equity) {
929
+ return parse(notional).dividedBy(equity).toFixed();
930
+ }
931
+ __name(computeNotionalEquityFraction, "computeNotionalEquityFraction");
932
+
878
933
  // src/fills/group.ts
879
934
  function groupFillsByPosition(fills) {
880
935
  const byPosition = /* @__PURE__ */ new Map();
@@ -1133,21 +1188,56 @@ BigNumber7.config({
1133
1188
  DECIMAL_PLACES: 30,
1134
1189
  ROUNDING_MODE: BigNumber7.ROUND_DOWN
1135
1190
  });
1136
- 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") {
1137
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
+ }
1138
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) {
1139
1214
  switch (connector) {
1140
1215
  case "hyperliquid":
1141
- return preciseHyperliquidPrice(effectiveRaw, precision, side);
1216
+ return preciseHyperliquidPrice(price, precision, rounding);
1142
1217
  case "binance":
1143
1218
  case "bybit":
1144
1219
  case "okx":
1145
1220
  case "lighter":
1146
1221
  case "lighter_rhc":
1147
- return preciseTickGridPrice(effectiveRaw, precision, side);
1222
+ return preciseTickGridPrice(price, precision, rounding);
1148
1223
  }
1149
1224
  }
1150
- __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");
1151
1241
  function withSlippageTolerance(normalized, slippageToleranceBps, side) {
1152
1242
  if (slippageToleranceBps === 0) {
1153
1243
  return normalized;
@@ -1157,18 +1247,16 @@ function withSlippageTolerance(normalized, slippageToleranceBps, side) {
1157
1247
  return side === "BUY" ? normalized.times(new BigNumber7(1).plus(factor)) : normalized.times(new BigNumber7(1).minus(factor));
1158
1248
  }
1159
1249
  __name(withSlippageTolerance, "withSlippageTolerance");
1160
- function preciseHyperliquidPrice(price, precision, side) {
1250
+ function preciseHyperliquidPrice(price, precision, rounding) {
1161
1251
  const maxDecimals = precision.price.decimals;
1162
1252
  const maxSigFigs = precision.price.sigFigs ?? 5;
1163
- const rounding = side === "BUY" ? BigNumber7.ROUND_FLOOR : BigNumber7.ROUND_CEIL;
1164
1253
  price = price.decimalPlaces(maxDecimals, rounding);
1165
1254
  price = clampSigFigs(price, maxSigFigs, maxDecimals, rounding);
1166
1255
  return price.toFixed();
1167
1256
  }
1168
1257
  __name(preciseHyperliquidPrice, "preciseHyperliquidPrice");
1169
- function preciseTickGridPrice(price, precision, side) {
1258
+ function preciseTickGridPrice(price, precision, rounding) {
1170
1259
  const tick = precision.price.tick ?? 1e-4;
1171
- const rounding = side === "BUY" ? BigNumber7.ROUND_FLOOR : BigNumber7.ROUND_CEIL;
1172
1260
  const tickBN = new BigNumber7(tick);
1173
1261
  const scaled = price.div(tickBN);
1174
1262
  const snapped = scaled.integerValue(rounding).times(tickBN);
@@ -1182,13 +1270,13 @@ function clampSigFigs(price, maxSigFigs, maxDecimals, rounding) {
1182
1270
  if (countSigFigs(price) <= maxSigFigs) {
1183
1271
  return price;
1184
1272
  }
1185
- for (let decimals = maxDecimals; decimals >= 0; decimals--) {
1273
+ for (let decimals = maxDecimals; decimals >= 1; decimals--) {
1186
1274
  const candidate = price.decimalPlaces(decimals, rounding);
1187
1275
  if (countSigFigs(candidate) <= maxSigFigs) {
1188
1276
  return candidate;
1189
1277
  }
1190
1278
  }
1191
- return price;
1279
+ return price.decimalPlaces(0, rounding);
1192
1280
  }
1193
1281
  __name(clampSigFigs, "clampSigFigs");
1194
1282
  function normalizePrice(value) {
@@ -1275,6 +1363,85 @@ function proxyFetch(input, init) {
1275
1363
  }), () => fetch(input, init));
1276
1364
  }
1277
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");
1278
1445
 
1279
1446
  // src/sync/applicables/constants.ts
1280
1447
  var DUST = parse("0.00000001");
@@ -2055,7 +2222,7 @@ function assignPartIds(positions) {
2055
2222
  };
2056
2223
  }
2057
2224
  __name(assignPartIds, "assignPartIds");
2058
- function partToApplicableFill(part, id, connector) {
2225
+ function partToApplicableFill(part, id, connector, liquidationOutcome) {
2059
2226
  const fill = part.source;
2060
2227
  const base = {
2061
2228
  exchangeFillId: id.exchangeFillId,
@@ -2088,7 +2255,10 @@ function partToApplicableFill(part, id, connector) {
2088
2255
  isExternal: !fromPear,
2089
2256
  tradeFee: feeShareForPart(fill.tradeFee, part.quantity, fill.quantity),
2090
2257
  builderFee: feeShareForPart(fill.builderFee, part.quantity, fill.quantity),
2091
- liquidation: fill.liquidation,
2258
+ liquidation: fill.liquidation ? {
2259
+ mark: fill.liquidation.mark,
2260
+ outcome: liquidationOutcome ?? void 0
2261
+ } : null,
2092
2262
  ...id.original ? {
2093
2263
  original: id.original
2094
2264
  } : {}
@@ -2122,7 +2292,7 @@ function assembleApplicables(args) {
2122
2292
  }
2123
2293
  __name(assembleApplicables, "assembleApplicables");
2124
2294
  function assembleOneApplicable(pos, connector, partIdFor, assignedSynthetics, now) {
2125
- const realFills = pos.parts.map((part) => partToApplicableFill(part, partIdFor(part), connector)).sort((a, b) => a.timestamp - b.timestamp);
2295
+ const { realFills, closedReason } = finalizeRealFills(pos, connector, partIdFor);
2126
2296
  const syntheticFills = assignedSynthetics.map((synthetic) => buildSyntheticFill({
2127
2297
  positionKey: pos.positionKey,
2128
2298
  connector,
@@ -2146,13 +2316,38 @@ function assembleOneApplicable(pos, connector, partIdFor, assignedSynthetics, no
2146
2316
  origin,
2147
2317
  status,
2148
2318
  ...closed ? {
2149
- closedReason: "NETTED"
2319
+ closedReason
2150
2320
  } : {},
2151
2321
  fills,
2152
2322
  metadata: buildMetadata(new Set(exposure.symbols()), realFills)
2153
2323
  };
2154
2324
  }
2155
2325
  __name(assembleOneApplicable, "assembleOneApplicable");
2326
+ function finalizeRealFills(position, connector, partIdFor) {
2327
+ const newestFirst = [
2328
+ ...position.parts
2329
+ ].sort((a, b) => a.source.timestamp - b.source.timestamp).reverse();
2330
+ const exposure = position.exposure.clone();
2331
+ const realFills = [];
2332
+ let closedReason = "NETTED";
2333
+ for (const [index, part] of newestFirst.entries()) {
2334
+ const after = exposure.get(part.source.symbol);
2335
+ const flatAfter = exposure.isFlat();
2336
+ exposure.addDelta(part.source.symbol, negate(signedBySide(part.quantity, part.source.side)));
2337
+ const reducesLeg = isGreaterThan(exposure.get(part.source.symbol).abs(), after.abs());
2338
+ const liquidationOutcome = part.source.liquidation && reducesLeg ? flatAfter ? "CLOSED" : "REDUCED" : null;
2339
+ if (index === 0 && position.closed && liquidationOutcome === "CLOSED") {
2340
+ closedReason = "LIQUIDATED";
2341
+ }
2342
+ realFills.push(partToApplicableFill(part, partIdFor(part), connector, liquidationOutcome));
2343
+ }
2344
+ realFills.reverse();
2345
+ return {
2346
+ realFills,
2347
+ closedReason
2348
+ };
2349
+ }
2350
+ __name(finalizeRealFills, "finalizeRealFills");
2156
2351
  function finalExposure(pos, synthetics) {
2157
2352
  const exposure = pos.exposure.clone();
2158
2353
  for (const synthetic of synthetics) {
@@ -2403,8 +2598,7 @@ __name(isStrictTimestampBoundary, "isStrictTimestampBoundary");
2403
2598
  async function computeSyncPayload(args) {
2404
2599
  const { tradeAccountId, connector, openPositions, entryBySymbol, provisionalFills, confirmedExchangeFillIds, externalFills, exchangePositions, neverSynced } = args;
2405
2600
  const fillsToProcess = _prepareExternalFills(externalFills, confirmedExchangeFillIds);
2406
- const { reconcilables, confirmables: confirmableFills, orphans } = _segregateExternalFills(provisionalFills, fillsToProcess);
2407
- const confirmablesByPosition = groupConfirmablesByPosition(confirmableFills);
2601
+ const orphans = _excludeClaimedFills(provisionalFills, fillsToProcess);
2408
2602
  const { keptOrphans, firstSyncTrim } = applyFirstSyncCutoff({
2409
2603
  neverSynced,
2410
2604
  fillsToProcess,
@@ -2421,13 +2615,11 @@ async function computeSyncPayload(args) {
2421
2615
  exchangePositions
2422
2616
  });
2423
2617
  const mustPostFirstSyncCheckpoint = neverSynced;
2424
- if (!mustPostFirstSyncCheckpoint && isEmptyCycle(reconcilables, confirmablesByPosition, applicables)) {
2618
+ if (!mustPostFirstSyncCheckpoint && applicables.length === 0) {
2425
2619
  return null;
2426
2620
  }
2427
2621
  const externalMaxTs = latestFillTimestamp(fillsToProcess);
2428
2622
  return {
2429
- reconcilables: Object.keys(reconcilables).length > 0 ? reconcilables : null,
2430
- confirmables: Object.keys(confirmablesByPosition).length > 0 ? confirmablesByPosition : null,
2431
2623
  applicables,
2432
2624
  exchangePositions,
2433
2625
  meta: {
@@ -2469,77 +2661,20 @@ function _prepareExternalFills(externalFills, confirmedExchangeFillIdsInput) {
2469
2661
  return Array.from(dedupedByExchangeFillId.values()).sort((a, b) => a.timestamp - b.timestamp);
2470
2662
  }
2471
2663
  __name(_prepareExternalFills, "_prepareExternalFills");
2472
- function _segregateExternalFills(provisionalFills, externalFills) {
2473
- const provisionalFillsMap = /* @__PURE__ */ new Map();
2664
+ function _excludeClaimedFills(provisionalFills, externalFills) {
2665
+ const ownedCloids = /* @__PURE__ */ new Set();
2474
2666
  for (const fill of provisionalFills) {
2475
2667
  const cloid = _normalizeCloid(fill.cloid);
2476
- if (!cloid) {
2477
- continue;
2668
+ if (cloid) {
2669
+ ownedCloids.add(cloid);
2478
2670
  }
2479
- provisionalFillsMap.set(cloid, fill);
2480
2671
  }
2481
- const orphans = [];
2482
- const fillsByCloid = /* @__PURE__ */ new Map();
2483
- for (const fill of externalFills) {
2672
+ return externalFills.filter((fill) => {
2484
2673
  const cloid = _normalizeCloid(fill.cloid);
2485
- if (!cloid) {
2486
- orphans.push(fill);
2487
- } else {
2488
- fillsByCloid.set(cloid, [
2489
- ...fillsByCloid.get(cloid) || [],
2490
- fill
2491
- ]);
2492
- }
2493
- }
2494
- const reconcilables = {};
2495
- const confirmableFills = [];
2496
- for (const [cloid, fills] of fillsByCloid.entries()) {
2497
- if (fills.length > 1) {
2498
- const provisional = provisionalFillsMap.get(cloid);
2499
- if (provisional) {
2500
- const [first, second, ...rest] = fills;
2501
- if (!first || !second) {
2502
- throw new Error("Corrupted state: sibling group must contain at least 2 fills for reconcile");
2503
- }
2504
- reconcilables[cloid] = [
2505
- {
2506
- ...first,
2507
- cloid
2508
- },
2509
- {
2510
- ...second,
2511
- cloid
2512
- },
2513
- ...rest.map((fill) => ({
2514
- ...fill,
2515
- cloid
2516
- }))
2517
- ];
2518
- } else {
2519
- orphans.push(...fills);
2520
- }
2521
- } else {
2522
- const fill = fills[0];
2523
- const provisional = provisionalFillsMap.get(cloid);
2524
- if (provisional) {
2525
- confirmableFills.push({
2526
- ...fill,
2527
- type: "confirm",
2528
- cloid,
2529
- positionId: provisional.positionId
2530
- });
2531
- } else {
2532
- orphans.push(fill);
2533
- }
2534
- }
2535
- }
2536
- return {
2537
- reconcilables,
2538
- confirmables: confirmableFills,
2539
- orphans
2540
- };
2674
+ return cloid === void 0 || !ownedCloids.has(cloid);
2675
+ });
2541
2676
  }
2542
- __name(_segregateExternalFills, "_segregateExternalFills");
2677
+ __name(_excludeClaimedFills, "_excludeClaimedFills");
2543
2678
  function _normalizeCloid(cloid) {
2544
2679
  const value = cloid?.trim();
2545
2680
  if (!value) {
@@ -2573,16 +2708,6 @@ function _isValidExternalTimestamp(timestamp) {
2573
2708
  return timestamp <= Date.now() + WATERMARK_FUTURE_SKEW_MS;
2574
2709
  }
2575
2710
  __name(_isValidExternalTimestamp, "_isValidExternalTimestamp");
2576
- function groupConfirmablesByPosition(confirmableFills) {
2577
- return confirmableFills.reduce((acc, fill) => {
2578
- acc[fill.positionId] = [
2579
- ...acc[fill.positionId] || [],
2580
- fill
2581
- ];
2582
- return acc;
2583
- }, {});
2584
- }
2585
- __name(groupConfirmablesByPosition, "groupConfirmablesByPosition");
2586
2711
  function latestFillTimestamp(fills) {
2587
2712
  const last = fills[fills.length - 1];
2588
2713
  if (!last) {
@@ -2633,10 +2758,6 @@ function applyFirstSyncCutoff(args) {
2633
2758
  };
2634
2759
  }
2635
2760
  __name(applyFirstSyncCutoff, "applyFirstSyncCutoff");
2636
- function isEmptyCycle(reconcilables, confirmablesByPosition, applicables) {
2637
- return Object.keys(reconcilables).length === 0 && Object.keys(confirmablesByPosition).length === 0 && applicables.length === 0;
2638
- }
2639
- __name(isEmptyCycle, "isEmptyCycle");
2640
2761
 
2641
2762
  // src/validate-leverage.ts
2642
2763
  function validateLeverage(leverage, instrument) {
@@ -2649,34 +2770,6 @@ function validateLeverage(leverage, instrument) {
2649
2770
  return true;
2650
2771
  }
2651
2772
  __name(validateLeverage, "validateLeverage");
2652
- function validateQuantity(quantity, price, precision) {
2653
- const parsedQuantity = new BigNumber7(quantity);
2654
- if (!parsedQuantity.isFinite() || parsedQuantity.lte(0)) {
2655
- return false;
2656
- }
2657
- const parsedPrice = new BigNumber7(price);
2658
- if (!parsedPrice.isFinite() || parsedPrice.lte(0)) {
2659
- return false;
2660
- }
2661
- const step = new BigNumber7(precision.quantity.step);
2662
- const scaled = parsedQuantity.div(step);
2663
- if (!scaled.isInteger()) {
2664
- return false;
2665
- }
2666
- const notional = contractsToBase(parsedQuantity, precision.quantity.contractSize).times(parsedPrice);
2667
- if (precision.notional?.min !== void 0 && precision.notional.min !== null && precision.notional.min > 0) {
2668
- if (notional.lt(new BigNumber7(precision.notional.min))) {
2669
- return false;
2670
- }
2671
- }
2672
- if (precision.notional?.max !== void 0 && precision.notional.max !== null && precision.notional.max > 0) {
2673
- if (notional.gt(new BigNumber7(precision.notional.max))) {
2674
- return false;
2675
- }
2676
- }
2677
- return true;
2678
- }
2679
- __name(validateQuantity, "validateQuantity");
2680
2773
  var VAULT_ACTION_DOMAIN = {
2681
2774
  name: "PearVaultService",
2682
2775
  version: "1",
@@ -2988,4 +3081,4 @@ function takeVerifier(handoffId, storage) {
2988
3081
  }
2989
3082
  __name(takeVerifier, "takeVerifier");
2990
3083
 
2991
- 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, computeEntryPriceForAsset, computeFillTransitions, computeMaxBasketSize, computeMultiQuoteBasketSize, 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 };
@@ -11,8 +11,8 @@ type PositionKeyInput = {
11
11
  * exactly what the active-position unique index relies on and what makes the
12
12
  * apply UPSERT find-or-create the single correct target.
13
13
  *
14
- * PHASE-1 LEG ID WARNING: `symbols` must be instrument ids
15
- * (`leg.id ?? leg.symbol`, or exposure keys). A phase-3 ticker here re-hashes and orphans every position.
14
+ * SYMBOL-MIGRATION id-forever: `symbols` must be instrument ids
15
+ * (`leg.id ?? leg.symbol`, or exposure keys). A display ticker here re-hashes and orphans every position.
16
16
  */
17
17
  export declare function generateDeterministicPositionKey(input: PositionKeyInput): string;
18
18
  export {};
@@ -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;
@@ -13,4 +13,4 @@ export declare function assignPartIds(positions: MatchedPosition[]): (part: Fill
13
13
  * missed-history synthetic (external, priced to reconcile with the exchange entry).
14
14
  * Fees are quantity-prorated when the source fill was split across positions.
15
15
  */
16
- export declare function partToApplicableFill(part: FillPart, id: PartId, connector: HttpTypes.Connector): HttpTypes.ApplicableFill;
16
+ export declare function partToApplicableFill(part: FillPart, id: PartId, connector: HttpTypes.Connector, liquidationOutcome: 'CLOSED' | 'REDUCED' | null): HttpTypes.ApplicableFill;
@@ -14,15 +14,10 @@ export declare function computeSyncPayload(args: ComputeSyncPayloadArgs): Promis
14
14
  */
15
15
  export declare function _prepareExternalFills(externalFills: HttpTypes.UnknownExternalFill[], confirmedExchangeFillIdsInput: string[]): HttpTypes.UnknownExternalFill[];
16
16
  /**
17
- * Partition external fills into reconcilables, confirmables, and orphans.
18
- * Fills whose cloid matches a provisional fill we own go to confirm or reconcile;
19
- * everything else (no cloid, or cloid we do not own) becomes an orphan.
17
+ * Drop external fills whose cloid is claimed by a provisional fill we own.
18
+ * The server-side syncer owns confirming those fills; the client applies orphans only.
20
19
  */
21
- export declare function _segregateExternalFills(provisionalFills: HttpTypes.ProvisionalFill[], externalFills: HttpTypes.UnknownExternalFill[]): {
22
- reconcilables: HttpTypes.ReconcileFillsRequest;
23
- confirmables: HttpTypes.ConfirmableExternalFill[];
24
- orphans: HttpTypes.UnknownExternalFill[];
25
- };
20
+ export declare function _excludeClaimedFills(provisionalFills: HttpTypes.ProvisionalFill[], externalFills: HttpTypes.UnknownExternalFill[]): HttpTypes.UnknownExternalFill[];
26
21
  /** Trim a cloid; empty or whitespace-only collapses to undefined. */
27
22
  export declare function _normalizeCloid(cloid?: string | null): string | undefined;
28
23
  /** Normalise an exchange fill id: trim + lowercase; absent or blank → undefined. */
@@ -11,8 +11,6 @@ export type ComputeSyncPayloadArgs = {
11
11
  neverSynced: boolean;
12
12
  };
13
13
  export type ComputeSyncPayloadResult = {
14
- reconcilables: HttpTypes.ReconcileFillsRequest | null;
15
- confirmables: HttpTypes.ConfirmFillsRequest | null;
16
14
  applicables: HttpTypes.ApplicablePosition[];
17
15
  exchangePositions: HttpTypes.ExternalPosition[];
18
16
  meta: {
@@ -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.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "Pear Protocol Utility functions",
5
5
  "private": false,
6
6
  "type": "module",
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@noble/hashes": "^1.3.2",
30
- "@pear-protocol/types": "^2.10.0",
30
+ "@pear-protocol/types": "^2.19.0",
31
31
  "@scure/base": "^2.3.0",
32
32
  "bignumber.js": "9.3.1"
33
33
  },