@pear-protocol/utils 0.0.15 → 0.0.17

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.
@@ -1,9 +1,17 @@
1
1
  import type { InstrumentId } from '@pear-protocol/types';
2
2
  import BigNumber from 'bignumber.js';
3
- export type AssetPnL = {
3
+ export type AssetUnrealizedPnL = {
4
4
  id: InstrumentId;
5
5
  signedQuantity: BigNumber;
6
6
  entryPrice: BigNumber;
7
7
  currentPrice: BigNumber;
8
- pnl: BigNumber;
8
+ upnl: BigNumber;
9
+ };
10
+ export type AssetRealizedPnL = {
11
+ id: InstrumentId;
12
+ grossPnl: BigNumber;
13
+ netPnl: BigNumber;
14
+ totalFees: BigNumber;
15
+ tradeFees: BigNumber;
16
+ pearFees: BigNumber;
9
17
  };
@@ -1,7 +1,7 @@
1
1
  import type { InstrumentId } from '@pear-protocol/types';
2
2
  import BigNumber from 'bignumber.js';
3
3
  import type { Fill } from '../fills/fill.types';
4
- import type { AssetPnL } from './asset.types';
4
+ import type { AssetRealizedPnL, AssetUnrealizedPnL } from './asset.types';
5
5
  type ExposureMap = Record<InstrumentId, string>;
6
6
  type PriceMap = Record<InstrumentId, string>;
7
7
  /**
@@ -15,11 +15,24 @@ export declare function computeAssetEntryNotional(signedQuantity: BigNumber, fil
15
15
  *
16
16
  * Formula: signedQty × (currentPrice − entryPrice)
17
17
  */
18
- export declare function computeAssetUPnL(signedQuantity: BigNumber, currentPrice: BigNumber, fills: Fill[]): BigNumber;
18
+ export declare function computeAssetUnrealizedPnL(signedQuantity: BigNumber, currentPrice: BigNumber, fills: Fill[]): BigNumber;
19
+ /**
20
+ * Compute realized gross PnL for a SINGLE asset.
21
+ *
22
+ * Realized trading PnL is accumulated when a fill reduces an existing
23
+ * position (using FIFO-style average entry), excluding fees.
24
+ */
25
+ export declare function computeAssetGrossRealizedPnL(fills: Fill[]): BigNumber;
19
26
  /**
20
27
  * Compute unrealized PnL for an entire position.
21
28
  *
22
29
  * Formula: Σ(signedQty × (currentPrice − entryPrice))
23
30
  */
24
- export declare function computeAssetPnLs(exposureMap: ExposureMap, priceMap: PriceMap, fills: Fill[]): AssetPnL[];
31
+ export declare function computeAssetUnrealizedPnLs(exposureMap: ExposureMap, priceMap: PriceMap, fills: Fill[]): AssetUnrealizedPnL[];
32
+ /**
33
+ * Compute realized PnL for an entire position.
34
+ *
35
+ * Formula: Σ(realizedPnL)
36
+ */
37
+ export declare function computeAssetRealizedPnLs(fills: Fill[]): AssetRealizedPnL[];
25
38
  export {};
@@ -1,2 +1,2 @@
1
- export type { AssetPnL } from './asset.types';
1
+ export type { AssetRealizedPnL, AssetUnrealizedPnL } from './asset.types';
2
2
  export * from './compute';
@@ -7,4 +7,6 @@ export type Fill = {
7
7
  quantity: string;
8
8
  side: Side;
9
9
  reduceOnly: boolean;
10
+ tradeFee?: string | null;
11
+ pearFee?: string | null;
10
12
  };
@@ -0,0 +1,17 @@
1
+ import type { InstrumentId, Side } from '@pear-protocol/types';
2
+ import BigNumber from 'bignumber.js';
3
+ export type RealizedPnlFillInput = {
4
+ symbol: InstrumentId;
5
+ side: Side;
6
+ price: string;
7
+ quantity: string;
8
+ timestamp: string;
9
+ };
10
+ /**
11
+ * Realized trading PnL from fill prices per instrument (FIFO-style average entry), excluding fees and funding.
12
+ */
13
+ export declare function computeRealizedPnlBySymbol(fills: RealizedPnlFillInput[]): Map<string, BigNumber>;
14
+ /**
15
+ * Realized trading PnL from fill prices (per instrument, FIFO-style average entry), excluding fees and funding.
16
+ */
17
+ export declare function computeRealizedPnlFromFills(fills: RealizedPnlFillInput[]): BigNumber;
package/dist/index.d.ts CHANGED
@@ -1,10 +1,11 @@
1
- export * from './asset';
2
- export * from './basket';
1
+ export * from './asset/index';
2
+ export * from './basket/index';
3
3
  export * from './contract-size';
4
- export * from './fills';
5
- export { countDecimals, exponentiate } from './math';
6
- export * from './position';
4
+ export * from './fills/index';
5
+ export { countDecimals, exponentiate } from './math/index';
6
+ export * from './position/index';
7
7
  export * from './precise-price';
8
8
  export * from './precise-quantity';
9
+ export * from './sync/index';
9
10
  export * from './validate-leverage';
10
11
  export * from './validate-quantity';
package/dist/index.js CHANGED
@@ -1,10 +1,617 @@
1
- export * from './asset';
2
- export * from './basket';
3
- export * from './contract-size';
4
- export * from './fills';
5
- export { countDecimals, exponentiate } from './math';
6
- export * from './position';
7
- export * from './precise-price';
8
- export * from './precise-quantity';
9
- export * from './validate-leverage';
10
- export * from './validate-quantity';
1
+ import BigNumber7, { BigNumber } from 'bignumber.js';
2
+
3
+ // src/asset/compute.ts
4
+ var ZERO = new BigNumber7(0);
5
+ var BIPS = new BigNumber7(1e4);
6
+ function exponentiate(value) {
7
+ const result = Math.exp(value);
8
+ return new BigNumber7(result);
9
+ }
10
+ function countDecimals(value) {
11
+ const parts = value.split(".");
12
+ return parts[1]?.length ?? 0;
13
+ }
14
+
15
+ // src/fills/compute.ts
16
+ function computeEntryPriceForAsset(fills) {
17
+ let entryPrice = ZERO;
18
+ let positionSize = ZERO;
19
+ for (const fill of fills) {
20
+ const price = new BigNumber7(fill.price);
21
+ const quantity = new BigNumber7(fill.quantity);
22
+ const signed = fill.side === "BUY" ? quantity : quantity.negated();
23
+ const newPositionSize = positionSize.plus(signed);
24
+ const isSameDirection = !positionSize.isZero() && positionSize.isPositive() === signed.isPositive();
25
+ const isFlip = !positionSize.isZero() && !newPositionSize.isZero() && newPositionSize.isPositive() !== positionSize.isPositive();
26
+ if (positionSize.isZero() || isFlip) {
27
+ entryPrice = price;
28
+ } else if (isSameDirection) {
29
+ entryPrice = entryPrice.times(positionSize.abs()).plus(price.times(quantity)).div(newPositionSize.abs());
30
+ }
31
+ positionSize = newPositionSize;
32
+ }
33
+ return entryPrice;
34
+ }
35
+
36
+ // src/asset/compute.ts
37
+ function computeAssetEntryNotional(signedQuantity, fills) {
38
+ if (signedQuantity.isZero()) return ZERO;
39
+ const entryPrice = computeEntryPriceForAsset(fills);
40
+ if (entryPrice.isZero()) return ZERO;
41
+ const unsignedQuantity = signedQuantity.abs();
42
+ return unsignedQuantity.multipliedBy(entryPrice);
43
+ }
44
+ function computeAssetUnrealizedPnL(signedQuantity, currentPrice, fills) {
45
+ if (signedQuantity.isZero()) return ZERO;
46
+ const entryPrice = computeEntryPriceForAsset(fills);
47
+ if (entryPrice.isZero()) return ZERO;
48
+ const deltaPrice = currentPrice.minus(entryPrice);
49
+ return signedQuantity.multipliedBy(deltaPrice);
50
+ }
51
+ function computeAssetGrossRealizedPnL(fills) {
52
+ let entryPrice = ZERO;
53
+ let positionSize = ZERO;
54
+ let gross = ZERO;
55
+ for (const fill of fills) {
56
+ const price = new BigNumber7(fill.price);
57
+ const quantity = new BigNumber7(fill.quantity);
58
+ const signed = fill.side === "BUY" ? quantity : quantity.negated();
59
+ const newPositionSize = positionSize.plus(signed);
60
+ if (!positionSize.isZero() && positionSize.isPositive() !== signed.isPositive()) {
61
+ const closedQty = BigNumber7.min(positionSize.abs(), signed.abs());
62
+ if (positionSize.isPositive()) {
63
+ gross = gross.plus(closedQty.multipliedBy(price.minus(entryPrice)));
64
+ } else {
65
+ gross = gross.plus(closedQty.multipliedBy(entryPrice.minus(price)));
66
+ }
67
+ }
68
+ const isSameDirection = !positionSize.isZero() && positionSize.isPositive() === signed.isPositive();
69
+ const isFlip = !positionSize.isZero() && !newPositionSize.isZero() && newPositionSize.isPositive() !== positionSize.isPositive();
70
+ if (positionSize.isZero() || isFlip) {
71
+ entryPrice = price;
72
+ } else if (isSameDirection) {
73
+ entryPrice = entryPrice.times(positionSize.abs()).plus(price.times(quantity)).div(newPositionSize.abs());
74
+ }
75
+ positionSize = newPositionSize;
76
+ }
77
+ return gross;
78
+ }
79
+ function computeAssetUnrealizedPnLs(exposureMap, priceMap, fills) {
80
+ const result = [];
81
+ for (const [id, signedQtyString] of Object.entries(exposureMap)) {
82
+ const priceString = priceMap[id];
83
+ if (!priceString) continue;
84
+ const signedQuantity = new BigNumber7(signedQtyString);
85
+ if (signedQuantity.isZero()) continue;
86
+ const currentPrice = new BigNumber7(priceString);
87
+ const assetFills = fills.filter((f) => f.symbol === id);
88
+ const entryPrice = computeEntryPriceForAsset(assetFills);
89
+ const upnl = signedQuantity.multipliedBy(currentPrice.minus(entryPrice));
90
+ result.push({
91
+ id,
92
+ signedQuantity,
93
+ entryPrice,
94
+ currentPrice,
95
+ upnl
96
+ });
97
+ }
98
+ return result;
99
+ }
100
+ function computeAssetRealizedPnLs(fills) {
101
+ const bySymbol = /* @__PURE__ */ new Map();
102
+ for (const f of fills) {
103
+ const list = bySymbol.get(f.symbol) ?? [];
104
+ list.push(f);
105
+ bySymbol.set(f.symbol, list);
106
+ }
107
+ const result = [];
108
+ for (const [id, assetFills] of bySymbol) {
109
+ const tradeFees = assetFills.reduce((acc, f) => acc.plus(new BigNumber7(f.tradeFee ?? "0")), ZERO);
110
+ const pearFees = assetFills.reduce((acc, f) => acc.plus(new BigNumber7(f.pearFee ?? "0")), ZERO);
111
+ const totalFees = tradeFees.plus(pearFees);
112
+ const grossPnl = computeAssetGrossRealizedPnL(assetFills);
113
+ const netPnl = grossPnl.minus(totalFees);
114
+ result.push({
115
+ id,
116
+ grossPnl,
117
+ netPnl,
118
+ totalFees,
119
+ tradeFees,
120
+ pearFees
121
+ });
122
+ }
123
+ return result;
124
+ }
125
+ function computeBasketWeightedRatioV1(basket, priceMap) {
126
+ let logSum = 0;
127
+ for (const leg of basket) {
128
+ const price = priceMap[leg.symbol];
129
+ if (!price) return null;
130
+ const priceNum = Number(price);
131
+ if (priceNum <= 0) return null;
132
+ const sign = leg.side === "BUY" ? 1 : -1;
133
+ logSum += sign * leg.weight * Math.log(priceNum);
134
+ }
135
+ return exponentiate(logSum);
136
+ }
137
+ function computeBasketIndexV1(basket, priceMap) {
138
+ let logSum = 0;
139
+ for (const leg of basket) {
140
+ const price = priceMap[leg.symbol];
141
+ if (!price) return null;
142
+ const priceNum = Number(price);
143
+ if (priceNum <= 0) return null;
144
+ logSum += leg.weight * Math.log(priceNum);
145
+ }
146
+ return exponentiate(logSum);
147
+ }
148
+ function contractsToBase(contracts, contractSize) {
149
+ return new BigNumber7(contracts).times(contractSize);
150
+ }
151
+ function baseToContracts(base, contractSize) {
152
+ return new BigNumber7(base).div(contractSize);
153
+ }
154
+ function computePositionMarkNotional(assetUPnLs) {
155
+ return assetUPnLs.reduce((acc, asset) => {
156
+ if (asset.signedQuantity.isZero()) return acc;
157
+ return acc.plus(asset.signedQuantity.abs().multipliedBy(asset.currentPrice));
158
+ }, ZERO);
159
+ }
160
+ function computePositionEntryNotional(assetUPnLs) {
161
+ return assetUPnLs.reduce((acc, asset) => {
162
+ if (asset.signedQuantity.isZero()) return acc;
163
+ return acc.plus(asset.signedQuantity.abs().multipliedBy(asset.entryPrice));
164
+ }, ZERO);
165
+ }
166
+ function computePositionUnrealizedPnL(assetUPnLs) {
167
+ return assetUPnLs.reduce((acc, asset) => acc.plus(asset.upnl), ZERO);
168
+ }
169
+ function computePositionGrossRealizedPnL(assetPnLs) {
170
+ return assetPnLs.reduce((acc, asset) => acc.plus(asset.grossPnl), ZERO);
171
+ }
172
+ function computePositionNetRealizedPnL(assetPnLs) {
173
+ return assetPnLs.reduce((acc, asset) => acc.plus(asset.netPnl), ZERO);
174
+ }
175
+ function computePositionUnrealizedPnLBips(upnl, entryNotional) {
176
+ if (upnl.isZero() || entryNotional.isZero()) return ZERO;
177
+ return upnl.div(entryNotional).multipliedBy(BIPS);
178
+ }
179
+ function computePositionEntryPrice(assetUPnLs) {
180
+ const { totalWeightedPrice, totalWeight } = assetUPnLs.reduce(
181
+ (acc, { signedQuantity, entryPrice }) => {
182
+ const absQuantity = signedQuantity.abs();
183
+ return {
184
+ totalWeightedPrice: acc.totalWeightedPrice.plus(absQuantity.multipliedBy(entryPrice)),
185
+ totalWeight: acc.totalWeight.plus(absQuantity)
186
+ };
187
+ },
188
+ {
189
+ totalWeightedPrice: ZERO,
190
+ totalWeight: ZERO
191
+ }
192
+ );
193
+ if (totalWeight.isZero()) {
194
+ return null;
195
+ }
196
+ return totalWeightedPrice.dividedBy(totalWeight);
197
+ }
198
+ function computePositionCurrentPrice(assetUPnLs) {
199
+ const { totalWeightedPrice, totalWeight } = assetUPnLs.reduce(
200
+ (acc, { signedQuantity, currentPrice }) => {
201
+ const absQuantity = signedQuantity.abs();
202
+ return {
203
+ totalWeightedPrice: acc.totalWeightedPrice.plus(absQuantity.multipliedBy(currentPrice)),
204
+ totalWeight: acc.totalWeight.plus(absQuantity)
205
+ };
206
+ },
207
+ {
208
+ totalWeightedPrice: ZERO,
209
+ totalWeight: ZERO
210
+ }
211
+ );
212
+ if (totalWeight.isZero()) {
213
+ return null;
214
+ }
215
+ return totalWeightedPrice.dividedBy(totalWeight);
216
+ }
217
+ function computeUnrealizedPositionState(exposureMap, priceMap, fills) {
218
+ const assetUPnLs = computeAssetUnrealizedPnLs(exposureMap, priceMap, fills);
219
+ const entryPrice = computePositionEntryPrice(assetUPnLs);
220
+ const entryNotional = computePositionEntryNotional(assetUPnLs);
221
+ const markPrice = computePositionCurrentPrice(assetUPnLs);
222
+ const markNotional = computePositionMarkNotional(assetUPnLs);
223
+ const upnl = computePositionUnrealizedPnL(assetUPnLs);
224
+ const upnlBips = computePositionUnrealizedPnLBips(upnl, entryNotional);
225
+ return {
226
+ assetUPnLs,
227
+ entryPrice,
228
+ entryNotional,
229
+ markPrice,
230
+ markNotional,
231
+ upnl,
232
+ upnlBips
233
+ };
234
+ }
235
+ function computeRealizedPositionState(fills) {
236
+ const assetPnLs = computeAssetRealizedPnLs(fills);
237
+ const grossPnl = computePositionGrossRealizedPnL(assetPnLs);
238
+ const netPnl = computePositionNetRealizedPnL(assetPnLs);
239
+ const tradeFees = assetPnLs.reduce((acc, asset) => acc.plus(asset.tradeFees), ZERO);
240
+ const pearFees = assetPnLs.reduce((acc, asset) => acc.plus(asset.pearFees), ZERO);
241
+ const totalFees = tradeFees.plus(pearFees);
242
+ return {
243
+ grossPnl,
244
+ netPnl,
245
+ totalFees,
246
+ tradeFees,
247
+ pearFees
248
+ };
249
+ }
250
+ BigNumber7.config({
251
+ DECIMAL_PLACES: 30,
252
+ ROUNDING_MODE: BigNumber7.ROUND_DOWN
253
+ });
254
+ function precisePrice(connector, raw, precision, side = "BUY", slippageToleranceBps = 0) {
255
+ const normalized = normalizePrice(raw);
256
+ const effectiveRaw = withSlippageTolerance(normalized, slippageToleranceBps, side);
257
+ switch (connector) {
258
+ case "hyperliquid":
259
+ return preciseHyperliquidPrice(effectiveRaw, precision, side);
260
+ case "binance":
261
+ case "bybit":
262
+ case "okx":
263
+ case "lighter":
264
+ return preciseTickGridPrice(effectiveRaw, precision, side);
265
+ }
266
+ }
267
+ function withSlippageTolerance(normalized, slippageToleranceBps, side) {
268
+ if (slippageToleranceBps === 0) {
269
+ return normalized;
270
+ }
271
+ const bps = new BigNumber7(slippageToleranceBps);
272
+ const factor = bps.div(1e4);
273
+ return side === "BUY" ? normalized.times(new BigNumber7(1).plus(factor)) : normalized.times(new BigNumber7(1).minus(factor));
274
+ }
275
+ function preciseHyperliquidPrice(price, precision, side) {
276
+ const maxDecimals = precision.price.decimals;
277
+ const maxSigFigs = precision.price.sigFigs ?? 5;
278
+ const rounding = side === "BUY" ? BigNumber7.ROUND_FLOOR : BigNumber7.ROUND_CEIL;
279
+ price = price.decimalPlaces(maxDecimals, rounding);
280
+ price = clampSigFigs(price, maxSigFigs, maxDecimals, rounding);
281
+ return price.toFixed();
282
+ }
283
+ function preciseTickGridPrice(price, precision, side) {
284
+ const tick = precision.price.tick ?? 1e-4;
285
+ const rounding = side === "BUY" ? BigNumber7.ROUND_FLOOR : BigNumber7.ROUND_CEIL;
286
+ const tickBN = new BigNumber7(tick);
287
+ const scaled = price.div(tickBN);
288
+ const snapped = scaled.integerValue(rounding).times(tickBN);
289
+ const maxDecimals = precision.price.decimals;
290
+ const final = snapped.decimalPlaces(maxDecimals, rounding);
291
+ return final.toFixed();
292
+ }
293
+ function clampSigFigs(price, maxSigFigs, maxDecimals, rounding) {
294
+ const countSigFigs = (x) => x.toFixed().replace(".", "").replace(/^0+/, "").length;
295
+ if (countSigFigs(price) <= maxSigFigs) {
296
+ return price;
297
+ }
298
+ for (let d = maxDecimals; d >= 0; d--) {
299
+ const candidate = price.decimalPlaces(d, rounding);
300
+ if (countSigFigs(candidate) <= maxSigFigs) {
301
+ return candidate;
302
+ }
303
+ }
304
+ return price;
305
+ }
306
+ function normalizePrice(value) {
307
+ const bn = new BigNumber7(value);
308
+ return new BigNumber7(bn.toFixed());
309
+ }
310
+ BigNumber7.config({
311
+ DECIMAL_PLACES: 30,
312
+ ROUNDING_MODE: BigNumber7.ROUND_DOWN
313
+ });
314
+ function preciseQuantity(connector, raw, precision, side) {
315
+ switch (connector) {
316
+ case "hyperliquid":
317
+ return preciseHyperliquidQuantity(raw, precision, side);
318
+ case "binance":
319
+ case "bybit":
320
+ case "okx":
321
+ case "lighter":
322
+ return preciseStepGridQuantity(raw, precision, side);
323
+ }
324
+ }
325
+ function preciseHyperliquidQuantity(raw, precision, side) {
326
+ const decimals = precision.quantity.decimals;
327
+ const qty = new BigNumber7(raw);
328
+ const rounded = qty.decimalPlaces(decimals, side === "BUY" ? BigNumber7.ROUND_FLOOR : BigNumber7.ROUND_CEIL);
329
+ return rounded.toFixed();
330
+ }
331
+ function preciseStepGridQuantity(raw, precision, side) {
332
+ const step = precision.quantity.step;
333
+ const qty = baseToContracts(raw, precision.quantity.contractSize);
334
+ const stepBN = new BigNumber7(step);
335
+ const scaled = qty.div(stepBN);
336
+ const rounded = side === "BUY" ? scaled.integerValue(BigNumber7.ROUND_FLOOR) : scaled.integerValue(BigNumber7.ROUND_CEIL);
337
+ const result = rounded.times(stepBN);
338
+ const maxDecimals = precision.quantity.decimals;
339
+ const final = result.decimalPlaces(maxDecimals, side === "BUY" ? BigNumber7.ROUND_FLOOR : BigNumber7.ROUND_CEIL);
340
+ return final.toFixed();
341
+ }
342
+ async function computeSyncPayload(args) {
343
+ const { openPositions, provisionalFills, confirmedExchangeFillIds, externalFills } = args;
344
+ const fillsToProcess = _prepareExternalFills(externalFills, confirmedExchangeFillIds);
345
+ if (fillsToProcess.length === 0) {
346
+ return null;
347
+ }
348
+ const { reconcilables, confirmables, orphanCreatables } = _seggregateExternalFills(provisionalFills, fillsToProcess);
349
+ const groupedConfirmables = confirmables.reduce((acc, fill) => {
350
+ acc[fill.positionId] = [...acc[fill.positionId] || [], fill];
351
+ return acc;
352
+ }, {});
353
+ const groupedCreatablesBySymbol = _groupCreatablesBySymbol(orphanCreatables, openPositions);
354
+ const groupedCreatables = {};
355
+ for (const group of groupedCreatablesBySymbol.values()) {
356
+ const zeroCrossing = _walkBackwardsToFindLastZeroCrossing(group);
357
+ for (const fill of zeroCrossing) {
358
+ const existing = groupedCreatables[fill.positionId];
359
+ if (existing) {
360
+ groupedCreatables[fill.positionId] = [...existing, fill];
361
+ } else {
362
+ groupedCreatables[fill.positionId] = [fill];
363
+ }
364
+ }
365
+ }
366
+ for (const fills of Object.values(groupedCreatables)) {
367
+ fills.sort((a, b) => a.timestamp - b.timestamp);
368
+ }
369
+ return {
370
+ reconcilables: Object.keys(reconcilables).length > 0 ? reconcilables : null,
371
+ confirmables: Object.keys(groupedConfirmables).length > 0 ? groupedConfirmables : null,
372
+ creatables: Object.keys(groupedCreatables).length > 0 ? groupedCreatables : null,
373
+ meta: {
374
+ externalMinTs: fillsToProcess[0]?.timestamp ?? null,
375
+ externalMaxTs: fillsToProcess[fillsToProcess.length - 1]?.timestamp ?? null,
376
+ processedCount: fillsToProcess.length
377
+ }
378
+ };
379
+ }
380
+ function _prepareExternalFills(externalFills, confirmedExchangeFillIdsInput) {
381
+ const dedupedByExchangeFillId = /* @__PURE__ */ new Map();
382
+ const confirmedExchangeFillIds = new Set(
383
+ Array.from(confirmedExchangeFillIdsInput).map((exchangeFillId) => _normalizeExchangeFillId(exchangeFillId)).filter((exchangeFillId) => exchangeFillId !== void 0)
384
+ );
385
+ const candidates = externalFills.reduce((acc, fill) => {
386
+ const normalizedExchangeFillId = _normalizeExchangeFillId(fill.exchangeFillId);
387
+ if (!normalizedExchangeFillId) return acc;
388
+ if (!_isValidExternalQuantity(fill.quantity)) return acc;
389
+ if (!_isValidExternalTimestamp(fill.timestamp)) return acc;
390
+ if (confirmedExchangeFillIds.has(normalizedExchangeFillId)) return acc;
391
+ acc.push({ ...fill, exchangeFillId: normalizedExchangeFillId });
392
+ return acc;
393
+ }, []);
394
+ for (const fill of candidates) {
395
+ const existing = dedupedByExchangeFillId.get(fill.exchangeFillId);
396
+ if (!existing || fill.timestamp < existing.timestamp) {
397
+ dedupedByExchangeFillId.set(fill.exchangeFillId, fill);
398
+ }
399
+ }
400
+ return Array.from(dedupedByExchangeFillId.values()).sort((a, b) => a.timestamp - b.timestamp);
401
+ }
402
+ function _groupCreatablesBySymbol(orphanCreatables, openPositions) {
403
+ const groupedCreatables = /* @__PURE__ */ new Map();
404
+ for (const fill of orphanCreatables) {
405
+ const positionCandidates = openPositions.reduce((acc, p) => {
406
+ const exposure = p.exposure[fill.symbol];
407
+ if (!exposure) return acc;
408
+ const quantity = new BigNumber(exposure);
409
+ if (quantity.isZero() || quantity.isEqualTo("-0")) return acc;
410
+ if (fill.timestamp < Date.parse(p.createdAt)) return acc;
411
+ acc.push(p);
412
+ return acc;
413
+ }, []);
414
+ const first = positionCandidates[0];
415
+ if (!first) continue;
416
+ const splits = _splitMaybeCompositeFillAcrossPositions(fill, [first, ...positionCandidates.slice(1)]);
417
+ for (const { position, maybeModifiedFill } of splits) {
418
+ const key = `${position.id}-${maybeModifiedFill.symbol}`;
419
+ const existing = groupedCreatables.get(key);
420
+ if (existing) {
421
+ existing.fills.push({ ...maybeModifiedFill, type: "create", positionId: position.id });
422
+ groupedCreatables.set(key, existing);
423
+ } else {
424
+ groupedCreatables.set(key, {
425
+ positionId: position.id,
426
+ symbol: maybeModifiedFill.symbol,
427
+ exposure: new BigNumber(position.exposure[maybeModifiedFill.symbol] ?? 0),
428
+ fills: [{ ...maybeModifiedFill, type: "create", positionId: position.id }]
429
+ });
430
+ }
431
+ }
432
+ }
433
+ return groupedCreatables;
434
+ }
435
+ function _splitMaybeCompositeFillAcrossPositions(fill, positionCandidates) {
436
+ if (positionCandidates.length === 1) {
437
+ return [{ position: positionCandidates[0], maybeModifiedFill: fill }];
438
+ }
439
+ const firstPosition = positionCandidates[0];
440
+ const firstExposure = new BigNumber(firstPosition.exposure[fill.symbol] ?? 0);
441
+ if (firstExposure.isZero() || firstExposure.isEqualTo("-0")) {
442
+ throw new Error("Corrupted state: first position has no exposure");
443
+ }
444
+ const signedFillQuantity = new BigNumber(fill.quantity).multipliedBy(fill.side === "BUY" ? 1 : -1);
445
+ const firstSign = firstExposure.isGreaterThan(0) ? 1 : -1;
446
+ const fillSign = signedFillQuantity.isGreaterThan(0) ? 1 : -1;
447
+ if (firstSign === fillSign) {
448
+ return [{ position: firstPosition, maybeModifiedFill: fill }];
449
+ }
450
+ const totalExposure = positionCandidates.reduce((acc, p) => {
451
+ acc = acc.plus(new BigNumber(p.exposure[fill.symbol] ?? 0));
452
+ return acc;
453
+ }, new BigNumber(0));
454
+ if (totalExposure.isZero() || totalExposure.isEqualTo("-0")) {
455
+ throw new Error("Corrupted state: total exposure is zero");
456
+ }
457
+ const willBalanceToZero = totalExposure.negated().isEqualTo(signedFillQuantity);
458
+ if (!willBalanceToZero) {
459
+ return [{ position: firstPosition, maybeModifiedFill: fill }];
460
+ }
461
+ return positionCandidates.map((position) => {
462
+ const signedQuantity = position.exposure[fill.symbol] ?? "0";
463
+ const quantity = new BigNumber(signedQuantity).abs();
464
+ return {
465
+ position,
466
+ maybeModifiedFill: {
467
+ ...fill,
468
+ exchangeFillId: `${fill.exchangeFillId}-${position.id}`,
469
+ quantity: quantity.toFixed(),
470
+ original: {
471
+ exchangeFillId: fill.exchangeFillId,
472
+ quantity: fill.quantity
473
+ }
474
+ }
475
+ };
476
+ });
477
+ }
478
+ function _walkBackwardsToFindLastZeroCrossing(group) {
479
+ let netDelta = new BigNumber(0);
480
+ for (const fill of group.fills) {
481
+ if (fill.symbol !== group.symbol) {
482
+ throw new Error("Corrupted state: fill symbol mismatch in grouped creatables");
483
+ }
484
+ const quantity = new BigNumber(fill.quantity);
485
+ const sign = fill.side === "BUY" ? 1 : -1;
486
+ const signedQuantity = quantity.multipliedBy(sign);
487
+ netDelta = netDelta.plus(signedQuantity);
488
+ }
489
+ if (netDelta.isZero()) {
490
+ return group.fills;
491
+ }
492
+ let walkDelta = netDelta;
493
+ const indexedNewFills = group.fills.map((fill, index) => ({ fill, index }));
494
+ let cutoff;
495
+ for (let i = indexedNewFills.length - 1; i >= 0; i--) {
496
+ const indexedFill = indexedNewFills[i];
497
+ if (!indexedFill) continue;
498
+ const { fill, index } = indexedFill;
499
+ const previousExposure = group.exposure;
500
+ if (previousExposure.plus(walkDelta).isZero() && cutoff === void 0) {
501
+ cutoff = index;
502
+ }
503
+ const sign = fill.side === "BUY" ? 1 : -1;
504
+ const quantity = new BigNumber(fill.quantity).multipliedBy(sign);
505
+ walkDelta = walkDelta.minus(quantity);
506
+ }
507
+ const allowed = new Set(
508
+ indexedNewFills.filter(({ index }) => cutoff === void 0 || index <= cutoff).map(({ fill }) => fill)
509
+ );
510
+ if (allowed.size !== group.fills.length) {
511
+ return Array.from(allowed.values());
512
+ }
513
+ if (group.exposure.isNegative() && netDelta.isPositive()) {
514
+ const newExposure = netDelta.plus(group.exposure);
515
+ if (newExposure.isGreaterThan(0)) return [];
516
+ }
517
+ if (group.exposure.isPositive() && netDelta.isNegative()) {
518
+ const newExposure = netDelta.plus(group.exposure);
519
+ if (newExposure.isLessThan(0)) return [];
520
+ }
521
+ return Array.from(allowed.values());
522
+ }
523
+ function _seggregateExternalFills(provisionalFills, externalFills) {
524
+ const provisionalFillsMap = /* @__PURE__ */ new Map();
525
+ for (const fill of provisionalFills) {
526
+ const cloid = _normalizeCloid(fill.cloid);
527
+ if (!cloid) continue;
528
+ provisionalFillsMap.set(cloid, fill);
529
+ }
530
+ const orphanCreatables = [];
531
+ const cloidBuffer = /* @__PURE__ */ new Map();
532
+ for (const fill of externalFills) {
533
+ const cloid = _normalizeCloid(fill.cloid);
534
+ if (!cloid) {
535
+ orphanCreatables.push(fill);
536
+ } else {
537
+ cloidBuffer.set(cloid, [...cloidBuffer.get(cloid) || [], fill]);
538
+ }
539
+ }
540
+ const reconcilables = {};
541
+ const confirmables = [];
542
+ for (const [cloid, fills] of cloidBuffer.entries()) {
543
+ if (fills.length > 1) {
544
+ const provisional = provisionalFillsMap.get(cloid);
545
+ if (provisional) {
546
+ const [first, second, ...rest] = fills;
547
+ if (!first || !second) {
548
+ throw new Error("Corrupted state: sibling group must contain at least 2 fills for reconcile");
549
+ }
550
+ reconcilables[cloid] = [
551
+ { ...first, cloid },
552
+ { ...second, cloid },
553
+ ...rest.map((fill) => ({ ...fill, cloid }))
554
+ ];
555
+ } else {
556
+ orphanCreatables.push(...fills);
557
+ }
558
+ } else {
559
+ const fill = fills[0];
560
+ const provisional = provisionalFillsMap.get(cloid);
561
+ if (provisional) {
562
+ confirmables.push({
563
+ ...fill,
564
+ type: "confirm",
565
+ cloid,
566
+ positionId: provisional.positionId
567
+ });
568
+ } else {
569
+ orphanCreatables.push(fill);
570
+ }
571
+ }
572
+ }
573
+ return { reconcilables, confirmables, orphanCreatables };
574
+ }
575
+ function _normalizeCloid(cloid) {
576
+ const value = cloid?.trim();
577
+ return value ? value : void 0;
578
+ }
579
+ function _normalizeExchangeFillId(exchangeFillId) {
580
+ const value = exchangeFillId?.trim().toLowerCase();
581
+ return value ? value : void 0;
582
+ }
583
+ function _isValidExternalQuantity(quantity) {
584
+ if (/e/i.test(quantity)) return false;
585
+ const parsed = new BigNumber(quantity);
586
+ if (parsed.isNaN() || !parsed.isFinite()) return false;
587
+ return parsed.isGreaterThan(0);
588
+ }
589
+ function _isValidExternalTimestamp(timestamp) {
590
+ return Number.isFinite(timestamp) && timestamp >= 0;
591
+ }
592
+
593
+ // src/validate-leverage.ts
594
+ function validateLeverage(leverage, instrument) {
595
+ if (leverage < 1 || leverage > 100) return false;
596
+ if (leverage > instrument.leverage) return false;
597
+ return true;
598
+ }
599
+ function validateQuantity(quantity, price, precision) {
600
+ const q = new BigNumber7(quantity);
601
+ if (!q.isFinite() || q.lte(0)) return false;
602
+ const p = new BigNumber7(price);
603
+ if (!p.isFinite() || p.lte(0)) return false;
604
+ const step = new BigNumber7(precision.quantity.step);
605
+ const scaled = q.div(step);
606
+ if (!scaled.isInteger()) return false;
607
+ const notional = contractsToBase(q, precision.quantity.contractSize).times(p);
608
+ if (precision.notional?.min !== void 0 && precision.notional.min !== null && precision.notional.min > 0) {
609
+ if (notional.lt(new BigNumber7(precision.notional.min))) return false;
610
+ }
611
+ if (precision.notional?.max !== void 0 && precision.notional.max !== null && precision.notional.max > 0) {
612
+ if (notional.gt(new BigNumber7(precision.notional.max))) return false;
613
+ }
614
+ return true;
615
+ }
616
+
617
+ export { baseToContracts, computeAssetEntryNotional, computeAssetGrossRealizedPnL, computeAssetRealizedPnLs, computeAssetUnrealizedPnL, computeAssetUnrealizedPnLs, computeBasketIndexV1, computeBasketWeightedRatioV1, computeEntryPriceForAsset, computeRealizedPositionState, computeSyncPayload, computeUnrealizedPositionState, contractsToBase, countDecimals, exponentiate, precisePrice, preciseQuantity, validateLeverage, validateQuantity };
@@ -1,59 +1,22 @@
1
1
  import type { InstrumentId } from '@pear-protocol/types';
2
- import BigNumber from 'bignumber.js';
3
- import type { AssetPnL } from '../asset/asset.types';
4
2
  import type { Fill } from '../fills/fill.types';
5
- import type { PositionPnL } from './position.types';
3
+ import type { RealizedPositionState, UnrealizedPositionState } from './position.types';
6
4
  type ExposureMap = Record<InstrumentId, string>;
7
5
  type PriceMap = Record<InstrumentId, string>;
8
6
  /**
9
- * Current position notional (mark value).
7
+ * Compute the unrealized position state for OPEN positions.
10
8
  *
11
- * Formula: Σ(|quantity| × currentPrice)
12
- */
13
- export declare function computePositionMarkNotional(assetPnLs: AssetPnL[]): BigNumber;
14
- /**
15
- * Compute the position entry notional.
16
- *
17
- * Formula: Σ(|quantity| × entryPrice)
18
- */
19
- export declare function computePositionEntryNotional(assetPnLs: AssetPnL[]): BigNumber;
20
- /**
21
- * Compute the position unrealized PnL.
22
- *
23
- * Formula: Σ(pnl)
24
- */
25
- export declare function computePositionUPnL(assetPnLs: AssetPnL[]): BigNumber;
26
- /**
27
- * Compute the position PnL in basis points.
28
- *
29
- * Formula: UPnL bips = upnl / entryNotional × 10_000
30
- */
31
- export declare function computePositionUPnLBips(upnl: BigNumber, entryNotional: BigNumber): BigNumber | null;
32
- /**
33
- * Compute the average entry price of the current net position.
34
- *
35
- * This is equivalent to the weighted average price of the current basket,
36
- * where the weights are the absolute quantities per asset.
37
- *
38
- * Formula: Σ(|quantity| × entryPrice) / Σ(|quantity|)
39
- */
40
- export declare function computePositionEntryPrice(assetPnLs: AssetPnL[]): BigNumber | null;
41
- /**
42
- * Compute the current (mark) price of the current net position.
43
- *
44
- * This is the weighted average mark price of the current basket,
45
- * where the weights are the absolute quantities per asset.
46
- *
47
- * Formula: Σ(|quantity| × currentPrice) / Σ(|quantity|)
9
+ * @param exposureMap - The exposure map of the position.
10
+ * @param priceMap - The price map of the position.
11
+ * @param fills - The fills of the position.
12
+ * @returns The unrealized position state.
48
13
  */
49
- export declare function computePositionCurrentPrice(assetPnLs: AssetPnL[]): BigNumber | null;
14
+ export declare function computeUnrealizedPositionState(exposureMap: ExposureMap, priceMap: PriceMap, fills: Fill[]): UnrealizedPositionState;
50
15
  /**
51
- * Compute the position PnL from fills.
16
+ * Compute the realized position state for CLOSED positions.
52
17
  *
53
- * @param exposureMap - The exposure map of the position.
54
- * @param priceMap - The price map of the position.
55
18
  * @param fills - The fills of the position.
56
- * @returns The position PnL.
19
+ * @returns The realized position state.
57
20
  */
58
- export declare function computePositionFromFills(exposureMap: ExposureMap, priceMap: PriceMap, fills: Fill[]): PositionPnL;
21
+ export declare function computeRealizedPositionState(fills: Fill[]): RealizedPositionState;
59
22
  export {};
@@ -1 +1,2 @@
1
1
  export * from './compute';
2
+ export type { RealizedPositionState, UnrealizedPositionState } from './position.types';
@@ -1,11 +1,18 @@
1
1
  import BigNumber from 'bignumber.js';
2
- import type { AssetPnL } from '../asset/asset.types';
3
- export type PositionPnL = {
4
- assets: AssetPnL[];
2
+ import type { AssetUnrealizedPnL } from '../asset/asset.types';
3
+ export type UnrealizedPositionState = {
4
+ assetUPnLs: AssetUnrealizedPnL[];
5
5
  entryPrice: BigNumber | null;
6
6
  entryNotional: BigNumber;
7
7
  markPrice: BigNumber | null;
8
8
  markNotional: BigNumber;
9
9
  upnl: BigNumber;
10
- upnlBips: BigNumber | null;
10
+ upnlBips: BigNumber;
11
+ };
12
+ export type RealizedPositionState = {
13
+ grossPnl: BigNumber;
14
+ netPnl: BigNumber;
15
+ totalFees: BigNumber;
16
+ tradeFees: BigNumber;
17
+ pearFees: BigNumber;
11
18
  };
@@ -0,0 +1,19 @@
1
+ import type { HttpTypes } from '@pear-protocol/types';
2
+ import type { ComputeSyncPayloadArgs, ComputeSyncPayloadResult, GroupedCreatables, GroupedCreatablesMap, OrphanCreatableExternalFill } from './types';
3
+ export declare function computeSyncPayload(args: ComputeSyncPayloadArgs): Promise<ComputeSyncPayloadResult | null>;
4
+ export declare function _prepareExternalFills(externalFills: HttpTypes.UnknownExternalFill[], confirmedExchangeFillIdsInput: string[]): HttpTypes.UnknownExternalFill[];
5
+ export declare function _groupCreatablesBySymbol(orphanCreatables: OrphanCreatableExternalFill[], openPositions: HttpTypes.OpenPositionLite[]): GroupedCreatablesMap;
6
+ export declare function _splitMaybeCompositeFillAcrossPositions(fill: OrphanCreatableExternalFill, positionCandidates: [HttpTypes.OpenPositionLite, ...HttpTypes.OpenPositionLite[]]): Array<{
7
+ position: HttpTypes.OpenPositionLite;
8
+ maybeModifiedFill: OrphanCreatableExternalFill;
9
+ }>;
10
+ export declare function _walkBackwardsToFindLastZeroCrossing(group: GroupedCreatables): HttpTypes.CreatableExternalFill[];
11
+ export declare function _seggregateExternalFills(provisionalFills: HttpTypes.ProvisionalFill[], externalFills: HttpTypes.UnknownExternalFill[]): {
12
+ reconcilables: HttpTypes.ReconcileFillsRequest;
13
+ confirmables: HttpTypes.ConfirmableExternalFill[];
14
+ orphanCreatables: OrphanCreatableExternalFill[];
15
+ };
16
+ export declare function _normalizeCloid(cloid?: string | null): string | undefined;
17
+ export declare function _normalizeExchangeFillId(exchangeFillId?: string | null): string | undefined;
18
+ export declare function _isValidExternalQuantity(quantity: string): boolean;
19
+ export declare function _isValidExternalTimestamp(timestamp: number): boolean;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export { computeSyncPayload } from './compute';
2
+ export type { ComputeSyncPayloadArgs, ComputeSyncPayloadResult, } from './types';
@@ -0,0 +1,27 @@
1
+ import type { HttpTypes } from '@pear-protocol/types';
2
+ import type BigNumber from 'bignumber.js';
3
+ export type OrphanCreatableExternalFill = Omit<HttpTypes.CreatableExternalFill, 'positionId' | 'type'>;
4
+ export type CompositeGroupKey = `${string}-${string}`;
5
+ export type GroupedCreatables = {
6
+ positionId: string;
7
+ symbol: string;
8
+ exposure: BigNumber;
9
+ fills: [HttpTypes.CreatableExternalFill, ...HttpTypes.CreatableExternalFill[]];
10
+ };
11
+ export type GroupedCreatablesMap = Map<CompositeGroupKey, GroupedCreatables>;
12
+ export type ComputeSyncPayloadArgs = {
13
+ openPositions: HttpTypes.OpenPositionLite[];
14
+ provisionalFills: HttpTypes.ProvisionalFill[];
15
+ confirmedExchangeFillIds: string[];
16
+ externalFills: HttpTypes.UnknownExternalFill[];
17
+ };
18
+ export type ComputeSyncPayloadResult = {
19
+ reconcilables: HttpTypes.ReconcileFillsRequest | null;
20
+ confirmables: HttpTypes.ConfirmFillsRequest | null;
21
+ creatables: HttpTypes.CreateFillsRequest | null;
22
+ meta: {
23
+ externalMinTs: number | null;
24
+ externalMaxTs: number | null;
25
+ processedCount: number;
26
+ };
27
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pear-protocol/utils",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "description": "Pear Protocol Utility functions",
5
5
  "private": false,
6
6
  "type": "module",
@@ -20,17 +20,19 @@
20
20
  },
21
21
  "sideEffects": false,
22
22
  "scripts": {
23
+ "test": "vitest run",
23
24
  "build": "tsup && tsc -p tsconfig.build.json",
24
25
  "clean": "rm -rf dist .turbo",
25
26
  "typecheck": "tsc --noEmit"
26
27
  },
27
28
  "dependencies": {
28
- "@pear-protocol/types": "0.0.15",
29
+ "@pear-protocol/types": "^0.0.17",
29
30
  "bignumber.js": "9.3.1"
30
31
  },
31
32
  "devDependencies": {
32
33
  "@backend/typescript-config": "workspace:*",
33
34
  "tsup": "8.5.1",
34
- "typescript": "5.9.3"
35
+ "typescript": "5.9.3",
36
+ "vitest": "4.1.2"
35
37
  }
36
38
  }
@@ -1 +0,0 @@
1
- import 'bignumber.js';
@@ -1,41 +0,0 @@
1
- import BigNumber from 'bignumber.js';
2
- import { computeEntryPriceForAsset } from '../fills/compute';
3
- import { ZERO } from '../math';
4
-
5
- function computeAssetEntryNotional(signedQuantity, fills) {
6
- if (signedQuantity.isZero()) return ZERO;
7
- const entryPrice = computeEntryPriceForAsset(fills);
8
- if (entryPrice.isZero()) return ZERO;
9
- const unsignedQuantity = signedQuantity.abs();
10
- return unsignedQuantity.multipliedBy(entryPrice);
11
- }
12
- function computeAssetUPnL(signedQuantity, currentPrice, fills) {
13
- if (signedQuantity.isZero()) return ZERO;
14
- const entryPrice = computeEntryPriceForAsset(fills);
15
- if (entryPrice.isZero()) return ZERO;
16
- const deltaPrice = currentPrice.minus(entryPrice);
17
- return signedQuantity.multipliedBy(deltaPrice);
18
- }
19
- function computeAssetPnLs(exposureMap, priceMap, fills) {
20
- const result = [];
21
- for (const [id, signedQtyString] of Object.entries(exposureMap)) {
22
- const priceString = priceMap[id];
23
- if (!priceString) continue;
24
- const signedQuantity = new BigNumber(signedQtyString);
25
- if (signedQuantity.isZero()) continue;
26
- const currentPrice = new BigNumber(priceString);
27
- const assetFills = fills.filter((f) => f.symbol === id);
28
- const entryPrice = computeEntryPriceForAsset(assetFills);
29
- const pnl = signedQuantity.multipliedBy(currentPrice.minus(entryPrice));
30
- result.push({
31
- id,
32
- signedQuantity,
33
- entryPrice,
34
- currentPrice,
35
- pnl
36
- });
37
- }
38
- return result;
39
- }
40
-
41
- export { computeAssetEntryNotional, computeAssetPnLs, computeAssetUPnL };
@@ -1 +0,0 @@
1
- export * from './compute';
@@ -1 +0,0 @@
1
-
@@ -1,28 +0,0 @@
1
- import 'bignumber.js';
2
- import { exponentiate } from '../math';
3
-
4
- function computeBasketWeightedRatioV1(basket, priceMap) {
5
- let logSum = 0;
6
- for (const leg of basket) {
7
- const price = priceMap[leg.symbol];
8
- if (!price) return null;
9
- const priceNum = Number(price);
10
- if (priceNum <= 0) return null;
11
- const sign = leg.side === "BUY" ? 1 : -1;
12
- logSum += sign * leg.weight * Math.log(priceNum);
13
- }
14
- return exponentiate(logSum);
15
- }
16
- function computeBasketIndexV1(basket, priceMap) {
17
- let logSum = 0;
18
- for (const leg of basket) {
19
- const price = priceMap[leg.symbol];
20
- if (!price) return null;
21
- const priceNum = Number(price);
22
- if (priceNum <= 0) return null;
23
- logSum += leg.weight * Math.log(priceNum);
24
- }
25
- return exponentiate(logSum);
26
- }
27
-
28
- export { computeBasketIndexV1, computeBasketWeightedRatioV1 };
@@ -1 +0,0 @@
1
- export * from './compute';
@@ -1,10 +0,0 @@
1
- import BigNumber from 'bignumber.js';
2
-
3
- function contractsToBase(contracts, contractSize) {
4
- return new BigNumber(contracts).times(contractSize);
5
- }
6
- function baseToContracts(base, contractSize) {
7
- return new BigNumber(base).div(contractSize);
8
- }
9
-
10
- export { baseToContracts, contractsToBase };
@@ -1,24 +0,0 @@
1
- import BigNumber from 'bignumber.js';
2
- import { ZERO } from '../math';
3
-
4
- function computeEntryPriceForAsset(fills) {
5
- let entryPrice = ZERO;
6
- let positionSize = ZERO;
7
- for (const fill of fills) {
8
- const price = new BigNumber(fill.price);
9
- const quantity = new BigNumber(fill.quantity);
10
- const signed = fill.side === "BUY" ? quantity : quantity.negated();
11
- const newPositionSize = positionSize.plus(signed);
12
- const isSameDirection = !positionSize.isZero() && positionSize.isPositive() === signed.isPositive();
13
- const isFlip = !positionSize.isZero() && !newPositionSize.isZero() && newPositionSize.isPositive() !== positionSize.isPositive();
14
- if (positionSize.isZero() || isFlip) {
15
- entryPrice = price;
16
- } else if (isSameDirection) {
17
- entryPrice = entryPrice.times(positionSize.abs()).plus(price.times(quantity)).div(newPositionSize.abs());
18
- }
19
- positionSize = newPositionSize;
20
- }
21
- return entryPrice;
22
- }
23
-
24
- export { computeEntryPriceForAsset };
@@ -1 +0,0 @@
1
-
@@ -1 +0,0 @@
1
- export * from './compute';
@@ -1,18 +0,0 @@
1
- import BigNumber from 'bignumber.js';
2
-
3
- const ZERO = new BigNumber(0);
4
- const BIPS = new BigNumber(1e4);
5
- function exponentiate(value) {
6
- const result = Math.exp(value);
7
- return new BigNumber(result);
8
- }
9
- function sign(value) {
10
- if (value.isZero()) return 0;
11
- return value.isPositive() ? 1 : -1;
12
- }
13
- function countDecimals(value) {
14
- const parts = value.split(".");
15
- return parts[1]?.length ?? 0;
16
- }
17
-
18
- export { BIPS, ZERO, countDecimals, exponentiate, sign };
@@ -1,81 +0,0 @@
1
- import 'bignumber.js';
2
- import { computeAssetPnLs } from '../asset';
3
- import { ZERO, BIPS } from '../math';
4
-
5
- function computePositionMarkNotional(assetPnLs) {
6
- return assetPnLs.reduce((acc, asset) => {
7
- if (asset.signedQuantity.isZero()) return acc;
8
- return acc.plus(asset.signedQuantity.abs().multipliedBy(asset.currentPrice));
9
- }, ZERO);
10
- }
11
- function computePositionEntryNotional(assetPnLs) {
12
- return assetPnLs.reduce((acc, asset) => {
13
- if (asset.signedQuantity.isZero()) return acc;
14
- return acc.plus(asset.signedQuantity.abs().multipliedBy(asset.entryPrice));
15
- }, ZERO);
16
- }
17
- function computePositionUPnL(assetPnLs) {
18
- return assetPnLs.reduce((acc, asset) => acc.plus(asset.pnl), ZERO);
19
- }
20
- function computePositionUPnLBips(upnl, entryNotional) {
21
- if (entryNotional.isZero()) return null;
22
- return upnl.div(entryNotional).multipliedBy(BIPS);
23
- }
24
- function computePositionEntryPrice(assetPnLs) {
25
- const { totalWeightedPrice, totalWeight } = assetPnLs.reduce(
26
- (acc, { signedQuantity, entryPrice }) => {
27
- const absQuantity = signedQuantity.abs();
28
- return {
29
- totalWeightedPrice: acc.totalWeightedPrice.plus(absQuantity.multipliedBy(entryPrice)),
30
- totalWeight: acc.totalWeight.plus(absQuantity)
31
- };
32
- },
33
- {
34
- totalWeightedPrice: ZERO,
35
- totalWeight: ZERO
36
- }
37
- );
38
- if (totalWeight.isZero()) {
39
- return null;
40
- }
41
- return totalWeightedPrice.dividedBy(totalWeight);
42
- }
43
- function computePositionCurrentPrice(assetPnLs) {
44
- const { totalWeightedPrice, totalWeight } = assetPnLs.reduce(
45
- (acc, { signedQuantity, currentPrice }) => {
46
- const absQuantity = signedQuantity.abs();
47
- return {
48
- totalWeightedPrice: acc.totalWeightedPrice.plus(absQuantity.multipliedBy(currentPrice)),
49
- totalWeight: acc.totalWeight.plus(absQuantity)
50
- };
51
- },
52
- {
53
- totalWeightedPrice: ZERO,
54
- totalWeight: ZERO
55
- }
56
- );
57
- if (totalWeight.isZero()) {
58
- return null;
59
- }
60
- return totalWeightedPrice.dividedBy(totalWeight);
61
- }
62
- function computePositionFromFills(exposureMap, priceMap, fills) {
63
- const assets = computeAssetPnLs(exposureMap, priceMap, fills);
64
- const entryPrice = computePositionEntryPrice(assets);
65
- const entryNotional = computePositionEntryNotional(assets);
66
- const markPrice = computePositionCurrentPrice(assets);
67
- const markNotional = computePositionMarkNotional(assets);
68
- const upnl = computePositionUPnL(assets);
69
- const upnlBips = computePositionUPnLBips(upnl, entryNotional);
70
- return {
71
- assets,
72
- entryPrice,
73
- entryNotional,
74
- markPrice,
75
- markNotional,
76
- upnl,
77
- upnlBips
78
- };
79
- }
80
-
81
- export { computePositionCurrentPrice, computePositionEntryNotional, computePositionEntryPrice, computePositionFromFills, computePositionMarkNotional, computePositionUPnL, computePositionUPnLBips };
@@ -1 +0,0 @@
1
- export * from './compute';
@@ -1 +0,0 @@
1
- import 'bignumber.js';
@@ -1,64 +0,0 @@
1
- import BigNumber from 'bignumber.js';
2
-
3
- BigNumber.config({
4
- DECIMAL_PLACES: 30,
5
- ROUNDING_MODE: BigNumber.ROUND_DOWN
6
- });
7
- function precisePrice(connector, raw, precision, side = "BUY", slippageToleranceBps = 0) {
8
- const normalized = normalizePrice(raw);
9
- const effectiveRaw = withSlippageTolerance(normalized, slippageToleranceBps, side);
10
- switch (connector) {
11
- case "hyperliquid":
12
- return preciseHyperliquidPrice(effectiveRaw, precision, side);
13
- case "binance":
14
- case "bybit":
15
- case "okx":
16
- case "lighter":
17
- return preciseTickGridPrice(effectiveRaw, precision, side);
18
- }
19
- }
20
- function withSlippageTolerance(normalized, slippageToleranceBps, side) {
21
- if (slippageToleranceBps === 0) {
22
- return normalized;
23
- }
24
- const bps = new BigNumber(slippageToleranceBps);
25
- const factor = bps.div(1e4);
26
- return side === "BUY" ? normalized.times(new BigNumber(1).plus(factor)) : normalized.times(new BigNumber(1).minus(factor));
27
- }
28
- function preciseHyperliquidPrice(price, precision, side) {
29
- const maxDecimals = precision.price.decimals;
30
- const maxSigFigs = precision.price.sigFigs ?? 5;
31
- const rounding = side === "BUY" ? BigNumber.ROUND_FLOOR : BigNumber.ROUND_CEIL;
32
- price = price.decimalPlaces(maxDecimals, rounding);
33
- price = clampSigFigs(price, maxSigFigs, maxDecimals, rounding);
34
- return price.toFixed();
35
- }
36
- function preciseTickGridPrice(price, precision, side) {
37
- const tick = precision.price.tick ?? 1e-4;
38
- const rounding = side === "BUY" ? BigNumber.ROUND_FLOOR : BigNumber.ROUND_CEIL;
39
- const tickBN = new BigNumber(tick);
40
- const scaled = price.div(tickBN);
41
- const snapped = scaled.integerValue(rounding).times(tickBN);
42
- const maxDecimals = precision.price.decimals;
43
- const final = snapped.decimalPlaces(maxDecimals, rounding);
44
- return final.toFixed();
45
- }
46
- function clampSigFigs(price, maxSigFigs, maxDecimals, rounding) {
47
- const countSigFigs = (x) => x.toFixed().replace(".", "").replace(/^0+/, "").length;
48
- if (countSigFigs(price) <= maxSigFigs) {
49
- return price;
50
- }
51
- for (let d = maxDecimals; d >= 0; d--) {
52
- const candidate = price.decimalPlaces(d, rounding);
53
- if (countSigFigs(candidate) <= maxSigFigs) {
54
- return candidate;
55
- }
56
- }
57
- return price;
58
- }
59
- function normalizePrice(value) {
60
- const bn = new BigNumber(value);
61
- return new BigNumber(bn.toFixed());
62
- }
63
-
64
- export { precisePrice };
@@ -1,37 +0,0 @@
1
- import BigNumber from 'bignumber.js';
2
- import { baseToContracts } from './contract-size';
3
-
4
- BigNumber.config({
5
- DECIMAL_PLACES: 30,
6
- ROUNDING_MODE: BigNumber.ROUND_DOWN
7
- });
8
- function preciseQuantity(connector, raw, precision, side) {
9
- switch (connector) {
10
- case "hyperliquid":
11
- return preciseHyperliquidQuantity(raw, precision, side);
12
- case "binance":
13
- case "bybit":
14
- case "okx":
15
- case "lighter":
16
- return preciseStepGridQuantity(raw, precision, side);
17
- }
18
- }
19
- function preciseHyperliquidQuantity(raw, precision, side) {
20
- const decimals = precision.quantity.decimals;
21
- const qty = new BigNumber(raw);
22
- const rounded = qty.decimalPlaces(decimals, side === "BUY" ? BigNumber.ROUND_FLOOR : BigNumber.ROUND_CEIL);
23
- return rounded.toFixed();
24
- }
25
- function preciseStepGridQuantity(raw, precision, side) {
26
- const step = precision.quantity.step;
27
- const qty = baseToContracts(raw, precision.quantity.contractSize);
28
- const stepBN = new BigNumber(step);
29
- const scaled = qty.div(stepBN);
30
- const rounded = side === "BUY" ? scaled.integerValue(BigNumber.ROUND_FLOOR) : scaled.integerValue(BigNumber.ROUND_CEIL);
31
- const result = rounded.times(stepBN);
32
- const maxDecimals = precision.quantity.decimals;
33
- const final = result.decimalPlaces(maxDecimals, side === "BUY" ? BigNumber.ROUND_FLOOR : BigNumber.ROUND_CEIL);
34
- return final.toFixed();
35
- }
36
-
37
- export { preciseQuantity };
@@ -1,7 +0,0 @@
1
- function validateLeverage(leverage, instrument) {
2
- if (leverage < 1 || leverage > 100) return false;
3
- if (leverage > instrument.leverage) return false;
4
- return true;
5
- }
6
-
7
- export { validateLeverage };
@@ -1,22 +0,0 @@
1
- import BigNumber from 'bignumber.js';
2
- import { contractsToBase } from './contract-size';
3
-
4
- function validateQuantity(quantity, price, precision) {
5
- const q = new BigNumber(quantity);
6
- if (!q.isFinite() || q.lte(0)) return false;
7
- const p = new BigNumber(price);
8
- if (!p.isFinite() || p.lte(0)) return false;
9
- const step = new BigNumber(precision.quantity.step);
10
- const scaled = q.div(step);
11
- if (!scaled.isInteger()) return false;
12
- const notional = contractsToBase(q, precision.quantity.contractSize).times(p);
13
- if (precision.notional?.min !== void 0 && precision.notional.min !== null && precision.notional.min > 0) {
14
- if (notional.lt(new BigNumber(precision.notional.min))) return false;
15
- }
16
- if (precision.notional?.max !== void 0 && precision.notional.max !== null && precision.notional.max > 0) {
17
- if (notional.gt(new BigNumber(precision.notional.max))) return false;
18
- }
19
- return true;
20
- }
21
-
22
- export { validateQuantity };