@pear-protocol/utils 1.2.0 → 1.3.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,6 +1,7 @@
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';
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();
@@ -2055,7 +2110,7 @@ function assignPartIds(positions) {
2055
2110
  };
2056
2111
  }
2057
2112
  __name(assignPartIds, "assignPartIds");
2058
- function partToApplicableFill(part, id, connector) {
2113
+ function partToApplicableFill(part, id, connector, liquidationOutcome) {
2059
2114
  const fill = part.source;
2060
2115
  const base = {
2061
2116
  exchangeFillId: id.exchangeFillId,
@@ -2088,7 +2143,10 @@ function partToApplicableFill(part, id, connector) {
2088
2143
  isExternal: !fromPear,
2089
2144
  tradeFee: feeShareForPart(fill.tradeFee, part.quantity, fill.quantity),
2090
2145
  builderFee: feeShareForPart(fill.builderFee, part.quantity, fill.quantity),
2091
- liquidation: fill.liquidation,
2146
+ liquidation: fill.liquidation ? {
2147
+ mark: fill.liquidation.mark,
2148
+ outcome: liquidationOutcome ?? void 0
2149
+ } : null,
2092
2150
  ...id.original ? {
2093
2151
  original: id.original
2094
2152
  } : {}
@@ -2122,7 +2180,7 @@ function assembleApplicables(args) {
2122
2180
  }
2123
2181
  __name(assembleApplicables, "assembleApplicables");
2124
2182
  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);
2183
+ const { realFills, closedReason } = finalizeRealFills(pos, connector, partIdFor);
2126
2184
  const syntheticFills = assignedSynthetics.map((synthetic) => buildSyntheticFill({
2127
2185
  positionKey: pos.positionKey,
2128
2186
  connector,
@@ -2146,13 +2204,38 @@ function assembleOneApplicable(pos, connector, partIdFor, assignedSynthetics, no
2146
2204
  origin,
2147
2205
  status,
2148
2206
  ...closed ? {
2149
- closedReason: "NETTED"
2207
+ closedReason
2150
2208
  } : {},
2151
2209
  fills,
2152
2210
  metadata: buildMetadata(new Set(exposure.symbols()), realFills)
2153
2211
  };
2154
2212
  }
2155
2213
  __name(assembleOneApplicable, "assembleOneApplicable");
2214
+ function finalizeRealFills(position, connector, partIdFor) {
2215
+ const newestFirst = [
2216
+ ...position.parts
2217
+ ].sort((a, b) => a.source.timestamp - b.source.timestamp).reverse();
2218
+ const exposure = position.exposure.clone();
2219
+ const realFills = [];
2220
+ let closedReason = "NETTED";
2221
+ for (const [index, part] of newestFirst.entries()) {
2222
+ const after = exposure.get(part.source.symbol);
2223
+ const flatAfter = exposure.isFlat();
2224
+ exposure.addDelta(part.source.symbol, negate(signedBySide(part.quantity, part.source.side)));
2225
+ const reducesLeg = isGreaterThan(exposure.get(part.source.symbol).abs(), after.abs());
2226
+ const liquidationOutcome = part.source.liquidation && reducesLeg ? flatAfter ? "CLOSED" : "REDUCED" : null;
2227
+ if (index === 0 && position.closed && liquidationOutcome === "CLOSED") {
2228
+ closedReason = "LIQUIDATED";
2229
+ }
2230
+ realFills.push(partToApplicableFill(part, partIdFor(part), connector, liquidationOutcome));
2231
+ }
2232
+ realFills.reverse();
2233
+ return {
2234
+ realFills,
2235
+ closedReason
2236
+ };
2237
+ }
2238
+ __name(finalizeRealFills, "finalizeRealFills");
2156
2239
  function finalExposure(pos, synthetics) {
2157
2240
  const exposure = pos.exposure.clone();
2158
2241
  for (const synthetic of synthetics) {
@@ -2403,8 +2486,7 @@ __name(isStrictTimestampBoundary, "isStrictTimestampBoundary");
2403
2486
  async function computeSyncPayload(args) {
2404
2487
  const { tradeAccountId, connector, openPositions, entryBySymbol, provisionalFills, confirmedExchangeFillIds, externalFills, exchangePositions, neverSynced } = args;
2405
2488
  const fillsToProcess = _prepareExternalFills(externalFills, confirmedExchangeFillIds);
2406
- const { reconcilables, confirmables: confirmableFills, orphans } = _segregateExternalFills(provisionalFills, fillsToProcess);
2407
- const confirmablesByPosition = groupConfirmablesByPosition(confirmableFills);
2489
+ const orphans = _excludeClaimedFills(provisionalFills, fillsToProcess);
2408
2490
  const { keptOrphans, firstSyncTrim } = applyFirstSyncCutoff({
2409
2491
  neverSynced,
2410
2492
  fillsToProcess,
@@ -2421,13 +2503,11 @@ async function computeSyncPayload(args) {
2421
2503
  exchangePositions
2422
2504
  });
2423
2505
  const mustPostFirstSyncCheckpoint = neverSynced;
2424
- if (!mustPostFirstSyncCheckpoint && isEmptyCycle(reconcilables, confirmablesByPosition, applicables)) {
2506
+ if (!mustPostFirstSyncCheckpoint && applicables.length === 0) {
2425
2507
  return null;
2426
2508
  }
2427
2509
  const externalMaxTs = latestFillTimestamp(fillsToProcess);
2428
2510
  return {
2429
- reconcilables: Object.keys(reconcilables).length > 0 ? reconcilables : null,
2430
- confirmables: Object.keys(confirmablesByPosition).length > 0 ? confirmablesByPosition : null,
2431
2511
  applicables,
2432
2512
  exchangePositions,
2433
2513
  meta: {
@@ -2469,77 +2549,20 @@ function _prepareExternalFills(externalFills, confirmedExchangeFillIdsInput) {
2469
2549
  return Array.from(dedupedByExchangeFillId.values()).sort((a, b) => a.timestamp - b.timestamp);
2470
2550
  }
2471
2551
  __name(_prepareExternalFills, "_prepareExternalFills");
2472
- function _segregateExternalFills(provisionalFills, externalFills) {
2473
- const provisionalFillsMap = /* @__PURE__ */ new Map();
2552
+ function _excludeClaimedFills(provisionalFills, externalFills) {
2553
+ const ownedCloids = /* @__PURE__ */ new Set();
2474
2554
  for (const fill of provisionalFills) {
2475
2555
  const cloid = _normalizeCloid(fill.cloid);
2476
- if (!cloid) {
2477
- continue;
2556
+ if (cloid) {
2557
+ ownedCloids.add(cloid);
2478
2558
  }
2479
- provisionalFillsMap.set(cloid, fill);
2480
2559
  }
2481
- const orphans = [];
2482
- const fillsByCloid = /* @__PURE__ */ new Map();
2483
- for (const fill of externalFills) {
2560
+ return externalFills.filter((fill) => {
2484
2561
  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
- };
2562
+ return cloid === void 0 || !ownedCloids.has(cloid);
2563
+ });
2541
2564
  }
2542
- __name(_segregateExternalFills, "_segregateExternalFills");
2565
+ __name(_excludeClaimedFills, "_excludeClaimedFills");
2543
2566
  function _normalizeCloid(cloid) {
2544
2567
  const value = cloid?.trim();
2545
2568
  if (!value) {
@@ -2573,16 +2596,6 @@ function _isValidExternalTimestamp(timestamp) {
2573
2596
  return timestamp <= Date.now() + WATERMARK_FUTURE_SKEW_MS;
2574
2597
  }
2575
2598
  __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
2599
  function latestFillTimestamp(fills) {
2587
2600
  const last = fills[fills.length - 1];
2588
2601
  if (!last) {
@@ -2633,10 +2646,6 @@ function applyFirstSyncCutoff(args) {
2633
2646
  };
2634
2647
  }
2635
2648
  __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
2649
 
2641
2650
  // src/validate-leverage.ts
2642
2651
  function validateLeverage(leverage, instrument) {
@@ -2988,4 +2997,4 @@ function takeVerifier(handoffId, storage) {
2988
2997
  }
2989
2998
  __name(takeVerifier, "takeVerifier");
2990
2999
 
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 };
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 };
@@ -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 {};
@@ -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: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pear-protocol/utils",
3
- "version": "1.2.0",
3
+ "version": "1.3.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.12.0",
31
31
  "@scure/base": "^2.3.0",
32
32
  "bignumber.js": "9.3.1"
33
33
  },