@pear-protocol/utils 0.4.1 → 0.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.
@@ -7,17 +7,41 @@ export type AssetUnrealizedPnL = {
7
7
  currentPrice: BigNumber;
8
8
  upnl: BigNumber;
9
9
  };
10
- export type AssetRealization = {
10
+ /**
11
+ * One reduction of the position, attributed to the fill that made it.
12
+ *
13
+ * A discriminated union rather than an optional gross, because whether a close could
14
+ * be valued is a property of the replay, not of the fill: a caller cannot tell a
15
+ * priced close from an unpriceable one by looking at the fill alone. `reduceOnly` is
16
+ * an order intent and says nothing about what actually closed, and a 0-price synthetic
17
+ * can equally well OPEN a position — so the walk is the only thing that knows, and it
18
+ * says so here instead of leaving the answer to be guessed downstream.
19
+ *
20
+ * The walk is time-agnostic (a Fill carries no timestamp), so callers that need to
21
+ * filter by time resolve fillId against their own fill records.
22
+ */
23
+ export type AssetClosure = {
24
+ kind: 'PRICED';
11
25
  fillId: string;
12
26
  gross: BigNumber;
27
+ entryPrice: BigNumber;
28
+ exitPrice: BigNumber;
29
+ closedQuantity: BigNumber;
30
+ } | {
31
+ kind: 'UNPRICED';
32
+ fillId: string;
33
+ closedQuantity: BigNumber;
13
34
  };
14
35
  export type AssetRealizedPnL = {
15
36
  id: InstrumentId;
16
37
  grossPnl: BigNumber;
17
38
  netPnl: BigNumber;
18
39
  undeterminedSize: BigNumber;
40
+ exitQuantity: BigNumber;
41
+ exitNotional: BigNumber;
42
+ exitVwap: BigNumber | null;
19
43
  totalFees: BigNumber;
20
44
  tradeFees: BigNumber;
21
45
  pearFees: BigNumber;
22
- realizations: AssetRealization[];
46
+ closures: AssetClosure[];
23
47
  };
@@ -1,2 +1,2 @@
1
- export type { AssetRealization, AssetRealizedPnL, AssetUnrealizedPnL } from './asset.types';
1
+ export type { AssetClosure, AssetRealizedPnL, AssetUnrealizedPnL } from './asset.types';
2
2
  export * from './compute';
@@ -0,0 +1,11 @@
1
+ import type { InstrumentId } from '@pear-protocol/types';
2
+ /**
3
+ * The canonical basket key: sha256 hex over the instrument ids a basket holds long
4
+ * and short, each side sorted, spelled as {"long":[...],"short":[...]}.
5
+ *
6
+ * This is the `basketKey` the backend reports on /markets/baskets rows and accepts
7
+ * in /markets/baskets/positions, derived byte-for-byte the way the backend derives
8
+ * it (deriveBasketKeyFromParts). The format is FROZEN: persisted rows are stored
9
+ * under it, so neither the sort, the payload spelling, nor the hash may change.
10
+ */
11
+ export declare function deriveBasketKey(longInstrumentIds: InstrumentId[], shortInstrumentIds: InstrumentId[]): string;
@@ -0,0 +1 @@
1
+ export {};
@@ -1 +1,2 @@
1
1
  export * from './compute';
2
+ export * from './derive-basket-key';
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import BigNumber9 from 'bignumber.js';
2
2
  import { sha256 } from '@noble/hashes/sha256';
3
3
  import { bytesToHex } from '@noble/hashes/utils';
4
+ import { WATERMARK_FUTURE_SKEW_MS } from '@pear-protocol/types';
4
5
 
5
6
  // src/asset/compute.ts
6
7
  function parse(value) {
@@ -185,17 +186,21 @@ function computeAssetRealizedPnLs(fills) {
185
186
  const tradeFees = assetFills.reduce((acc, fill) => acc.plus(new BigNumber9(fill.tradeFee ?? "0")), ZERO);
186
187
  const pearFees = assetFills.reduce((acc, fill) => acc.plus(new BigNumber9(fill.pearFee ?? "0")), ZERO);
187
188
  const totalFees = tradeFees.plus(pearFees);
188
- const { gross: grossPnl, undeterminedSize, realizations } = walkAssetFills(assetFills);
189
+ const { gross: grossPnl, undeterminedSize, closures, exitQuantity, exitNotional } = walkAssetFills(assetFills);
189
190
  const netPnl = grossPnl.minus(totalFees);
191
+ const exitVwap = isZero(exitQuantity) ? null : divide(exitNotional, exitQuantity);
190
192
  result.push({
191
193
  id,
192
194
  grossPnl,
193
195
  netPnl,
194
196
  undeterminedSize,
197
+ exitQuantity,
198
+ exitNotional,
199
+ exitVwap,
195
200
  totalFees,
196
201
  tradeFees,
197
202
  pearFees,
198
- realizations
203
+ closures
199
204
  });
200
205
  }
201
206
  return result;
@@ -206,7 +211,9 @@ function walkAssetFills(fills) {
206
211
  let basisUnpriced = false;
207
212
  let gross = ZERO;
208
213
  let undeterminedSize = ZERO;
209
- const realizations = [];
214
+ const closures = [];
215
+ let exitQuantity = ZERO;
216
+ let exitNotional = ZERO;
210
217
  for (const fill of fills) {
211
218
  const price = parse(fill.price);
212
219
  const quantity = parse(fill.quantity);
@@ -216,10 +223,24 @@ function walkAssetFills(fills) {
216
223
  const closedQty = min(positionSize.abs(), signedQuantity.abs());
217
224
  if (basisUnpriced || isZero(price)) {
218
225
  undeterminedSize = add(undeterminedSize, closedQty);
226
+ closures.push({
227
+ kind: "UNPRICED",
228
+ fillId: fill.id,
229
+ closedQuantity: closedQty
230
+ });
219
231
  } else {
220
232
  const realized = isPositive(positionSize) ? multiply(closedQty, subtract(price, entryPrice)) : multiply(closedQty, subtract(entryPrice, price));
221
233
  gross = add(gross, realized);
222
- realizations.push({ fillId: fill.id, gross: realized });
234
+ closures.push({
235
+ kind: "PRICED",
236
+ fillId: fill.id,
237
+ gross: realized,
238
+ entryPrice,
239
+ exitPrice: price,
240
+ closedQuantity: closedQty
241
+ });
242
+ exitQuantity = add(exitQuantity, closedQty);
243
+ exitNotional = add(exitNotional, multiply(closedQty, price));
223
244
  }
224
245
  }
225
246
  const isSameDirection = !isZero(positionSize) && isPositive(positionSize) === isPositive(signedQuantity);
@@ -233,7 +254,13 @@ function walkAssetFills(fills) {
233
254
  }
234
255
  positionSize = newPositionSize;
235
256
  }
236
- return { gross, undeterminedSize, realizations };
257
+ return {
258
+ gross,
259
+ undeterminedSize,
260
+ closures,
261
+ exitQuantity,
262
+ exitNotional
263
+ };
237
264
  }
238
265
  function computeBasketWeightedRatioV1(basket, priceMap) {
239
266
  let logSum = 0;
@@ -251,6 +278,13 @@ function computeBasketWeightedRatioV1(basket, priceMap) {
251
278
  }
252
279
  return exponentiate(logSum);
253
280
  }
281
+ function deriveBasketKey(longInstrumentIds, shortInstrumentIds) {
282
+ const payload = JSON.stringify({
283
+ long: [...longInstrumentIds].sort((a, b) => a.localeCompare(b)),
284
+ short: [...shortInstrumentIds].sort((a, b) => a.localeCompare(b))
285
+ });
286
+ return bytesToHex(sha256(new TextEncoder().encode(payload)));
287
+ }
254
288
  function contractsToBase(contracts, contractSize) {
255
289
  return new BigNumber9(contracts).times(contractSize);
256
290
  }
@@ -330,7 +364,9 @@ function computeUnrealizedPositionState(exposureMap, priceMap, fills) {
330
364
  };
331
365
  }
332
366
  function computeRealizedPositionState(fills) {
333
- const assetPnLs = computeAssetRealizedPnLs(fills);
367
+ return aggregateRealizedPositionState(computeAssetRealizedPnLs(fills));
368
+ }
369
+ function aggregateRealizedPositionState(assetPnLs) {
334
370
  const grossPnl = computePositionGrossRealizedPnL(assetPnLs);
335
371
  const netPnl = computePositionNetRealizedPnL(assetPnLs);
336
372
  const tradeFees = assetPnLs.reduce((acc, asset) => acc.plus(asset.tradeFees), ZERO);
@@ -1663,7 +1699,10 @@ function _isValidExternalQuantity(quantity) {
1663
1699
  return isGreaterThan(quantity, ZERO);
1664
1700
  }
1665
1701
  function _isValidExternalTimestamp(timestamp) {
1666
- return Number.isFinite(timestamp) && timestamp >= 0;
1702
+ if (!Number.isFinite(timestamp) || timestamp < 0) {
1703
+ return false;
1704
+ }
1705
+ return timestamp <= Date.now() + WATERMARK_FUTURE_SKEW_MS;
1667
1706
  }
1668
1707
  function groupConfirmablesByPosition(confirmableFills) {
1669
1708
  return confirmableFills.reduce((acc, fill) => {
@@ -1752,4 +1791,4 @@ function validateQuantity(quantity, price, precision) {
1752
1791
  return true;
1753
1792
  }
1754
1793
 
1755
- export { BIPS, ZERO, abs, add, baseToContracts, buildApplicables, compareValue, computeAssetEntryNotional, computeAssetGrossRealizedPnL, computeAssetRealizedPnLs, computeAssetUndeterminedSizes, computeAssetUnrealizedPnL, computeAssetUnrealizedPnLs, computeBasketWeightedRatioV1, computeEntryPriceForAsset, computePositionEntryPrices, computeRealizedPnlBySymbol, computeRealizedPnlFromFills, computeRealizedPositionState, computeSyncPayload, computeUnrealizedPositionState, computeUnrealizedPositionStateFromEntryPrices, configureProxy, contractsToBase, countDecimals, divide, exponentiate, generateDeterministicPositionKey, isEqualTo, isFiniteDecimal, isGreaterThan, isGreaterThanOrEqual, isLessThan, isLessThanOrEqual, isNegative, isPositive, isZero, min, multiply, negate, parse, precisePrice, preciseQuantity, proxyFetch, sideSign, sign, signed, signedBySide, subtract, toDecimalString, validateLeverage, validateQuantity };
1794
+ export { BIPS, ZERO, abs, add, aggregateRealizedPositionState, baseToContracts, buildApplicables, compareValue, computeAssetEntryNotional, computeAssetGrossRealizedPnL, computeAssetRealizedPnLs, computeAssetUndeterminedSizes, computeAssetUnrealizedPnL, computeAssetUnrealizedPnLs, computeBasketWeightedRatioV1, computeEntryPriceForAsset, computePositionEntryPrices, computeRealizedPnlBySymbol, computeRealizedPnlFromFills, computeRealizedPositionState, computeSyncPayload, computeUnrealizedPositionState, computeUnrealizedPositionStateFromEntryPrices, configureProxy, contractsToBase, countDecimals, deriveBasketKey, divide, exponentiate, generateDeterministicPositionKey, isEqualTo, isFiniteDecimal, isGreaterThan, isGreaterThanOrEqual, isLessThan, isLessThanOrEqual, isNegative, isPositive, isZero, min, multiply, negate, parse, precisePrice, preciseQuantity, proxyFetch, sideSign, sign, signed, signedBySide, subtract, toDecimalString, validateLeverage, validateQuantity };
@@ -1,4 +1,5 @@
1
1
  import type { InstrumentId } from '@pear-protocol/types';
2
+ import type { AssetRealizedPnL } from '../asset/asset.types';
2
3
  import type { Fill } from '../fills/fill.types';
3
4
  import type { RealizedPositionState, UnrealizedPositionState } from './position.types';
4
5
  type ExposureMap = Record<InstrumentId, string>;
@@ -19,6 +20,18 @@ export declare function computeUnrealizedPositionState(exposureMap: ExposureMap,
19
20
  * @returns The realized position state.
20
21
  */
21
22
  export declare function computeRealizedPositionState(fills: Fill[]): RealizedPositionState;
23
+ /**
24
+ * Roll already-walked per-asset rows up into the position total.
25
+ *
26
+ * Separated from computeRealizedPositionState so a caller that already holds the
27
+ * per-asset rows can total them without walking the ledger a second time. The
28
+ * position total and the per-asset detail then come from one execution of the walk,
29
+ * which is the guarantee that makes them impossible to disagree.
30
+ *
31
+ * @param assetPnLs - Per-asset realized rows from computeAssetRealizedPnLs.
32
+ * @returns The realized position state those rows add up to.
33
+ */
34
+ export declare function aggregateRealizedPositionState(assetPnLs: AssetRealizedPnL[]): RealizedPositionState;
22
35
  /**
23
36
  * Weighted-average entry (cost-basis) price per instrument in a position, derived purely from
24
37
  * fills — no mark price needed. This is the authoritative source for a position's entry price;
@@ -1,4 +1,4 @@
1
- import type { HttpTypes } from '@pear-protocol/types';
1
+ import { type HttpTypes } from '@pear-protocol/types';
2
2
  import type { ComputeSyncPayloadArgs, ComputeSyncPayloadResult } from './types';
3
3
  /**
4
4
  * Derive the full sync payload from raw exchange data and local state.
@@ -29,5 +29,5 @@ export declare function _normalizeCloid(cloid?: string | null): string | undefin
29
29
  export declare function _normalizeExchangeFillId(exchangeFillId?: string | null): string | undefined;
30
30
  /** Rejects scientific-notation quantities (e.g. 1e-8) that cannot be safely parsed as fills. */
31
31
  export declare function _isValidExternalQuantity(quantity: string): boolean;
32
- /** Returns true only when the timestamp is a non-negative finite number. */
32
+ /** Returns true only when the timestamp is a non-negative finite number no further than clock skew ahead of now. */
33
33
  export declare function _isValidExternalTimestamp(timestamp: number): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pear-protocol/utils",
3
- "version": "0.4.1",
3
+ "version": "0.5.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": "^1.15.1",
30
+ "@pear-protocol/types": "^1.17.0",
31
31
  "bignumber.js": "9.3.1"
32
32
  },
33
33
  "devDependencies": {