@pear-protocol/utils 0.0.22 → 0.1.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.
Files changed (41) hide show
  1. package/dist/asset/asset.types.d.ts +1 -0
  2. package/dist/asset/compute.d.ts +9 -1
  3. package/dist/asset/compute.test.d.ts +1 -0
  4. package/dist/fills/compute.test.d.ts +1 -0
  5. package/dist/fills/fill.types.d.ts +1 -0
  6. package/dist/fills/index.d.ts +1 -0
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.js +1382 -308
  9. package/dist/math/decimal.d.ts +57 -0
  10. package/dist/math/decimal.test.d.ts +1 -0
  11. package/dist/math/index.d.ts +2 -0
  12. package/dist/position/compute.test.d.ts +1 -0
  13. package/dist/position/index.d.ts +1 -0
  14. package/dist/position/key.d.ts +15 -0
  15. package/dist/precise-price.d.ts +5 -0
  16. package/dist/precise-quantity.d.ts +4 -0
  17. package/dist/sync/applicables/basket.d.ts +42 -0
  18. package/dist/sync/applicables/basket.test.d.ts +1 -0
  19. package/dist/sync/applicables/constants.d.ts +19 -0
  20. package/dist/sync/applicables/exposure.d.ts +47 -0
  21. package/dist/sync/applicables/exposure.test.d.ts +1 -0
  22. package/dist/sync/applicables/fills.d.ts +16 -0
  23. package/dist/sync/applicables/fills.test.d.ts +1 -0
  24. package/dist/sync/applicables/index.d.ts +34 -0
  25. package/dist/sync/applicables/index.test.d.ts +1 -0
  26. package/dist/sync/applicables/matching.d.ts +17 -0
  27. package/dist/sync/applicables/matching.test.d.ts +1 -0
  28. package/dist/sync/applicables/position.d.ts +17 -0
  29. package/dist/sync/applicables/position.test.d.ts +1 -0
  30. package/dist/sync/applicables/snapshot.d.ts +41 -0
  31. package/dist/sync/applicables/synthetics.d.ts +52 -0
  32. package/dist/sync/applicables/synthetics.test.d.ts +1 -0
  33. package/dist/sync/applicables/types.d.ts +77 -0
  34. package/dist/sync/compute.d.ts +23 -9
  35. package/dist/sync/cutoff.d.ts +35 -0
  36. package/dist/sync/cutoff.test.d.ts +1 -0
  37. package/dist/sync/index.d.ts +1 -1
  38. package/dist/sync/types.d.ts +12 -13
  39. package/dist/validate-leverage.d.ts +3 -0
  40. package/dist/validate-quantity.d.ts +4 -0
  41. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -1,27 +1,107 @@
1
- import BigNumber7, { BigNumber } from 'bignumber.js';
1
+ import BigNumber9 from 'bignumber.js';
2
+ import { sha256 } from '@noble/hashes/sha256';
3
+ import { bytesToHex } from '@noble/hashes/utils';
2
4
 
3
5
  // src/asset/compute.ts
4
- var ZERO = new BigNumber7(0);
5
- var BIPS = new BigNumber7(1e4);
6
+ function parse(value) {
7
+ return value instanceof BigNumber9 ? value : new BigNumber9(value);
8
+ }
9
+ function toDecimalString(value) {
10
+ return parse(value).toFixed();
11
+ }
12
+ function signed(value) {
13
+ const bn = parse(value);
14
+ return bn.isNegative() && !bn.isZero() ? bn.toFixed() : `+${bn.toFixed()}`;
15
+ }
16
+ function add(a, b) {
17
+ return parse(a).plus(parse(b));
18
+ }
19
+ function subtract(a, b) {
20
+ return parse(a).minus(parse(b));
21
+ }
22
+ function multiply(a, b) {
23
+ return parse(a).times(parse(b));
24
+ }
25
+ function divide(a, b) {
26
+ return parse(a).div(parse(b));
27
+ }
28
+ function negate(value) {
29
+ return parse(value).negated();
30
+ }
31
+ function abs(value) {
32
+ return parse(value).abs();
33
+ }
34
+ function min(a, b) {
35
+ return BigNumber9.min(parse(a), parse(b));
36
+ }
37
+ function isZero(value) {
38
+ return parse(value).isZero();
39
+ }
40
+ function isPositive(value) {
41
+ return parse(value).isPositive();
42
+ }
43
+ function isNegative(value) {
44
+ return parse(value).isNegative();
45
+ }
46
+ function isFiniteDecimal(value) {
47
+ return parse(value).isFinite();
48
+ }
49
+ function isEqualTo(a, b) {
50
+ return parse(a).isEqualTo(parse(b));
51
+ }
52
+ function isGreaterThan(a, b) {
53
+ return parse(a).isGreaterThan(parse(b));
54
+ }
55
+ function isGreaterThanOrEqual(a, b) {
56
+ return parse(a).isGreaterThanOrEqualTo(parse(b));
57
+ }
58
+ function isLessThan(a, b) {
59
+ return parse(a).isLessThan(parse(b));
60
+ }
61
+ function isLessThanOrEqual(a, b) {
62
+ return parse(a).isLessThanOrEqualTo(parse(b));
63
+ }
64
+ function sideSign(side) {
65
+ switch (side) {
66
+ case "BUY":
67
+ return 1;
68
+ case "SELL":
69
+ return -1;
70
+ }
71
+ }
72
+ function signedBySide(value, side) {
73
+ return parse(value).times(sideSign(side));
74
+ }
75
+
76
+ // src/math/index.ts
77
+ var ZERO = new BigNumber9(0);
78
+ var BIPS = new BigNumber9(1e4);
6
79
  function exponentiate(value) {
7
80
  const result = Math.exp(value);
8
- return new BigNumber7(result);
81
+ return new BigNumber9(result);
82
+ }
83
+ function sign(value) {
84
+ if (value.isZero()) return 0;
85
+ return value.isPositive() ? 1 : -1;
9
86
  }
10
87
  function countDecimals(value) {
11
88
  const parts = value.split(".");
12
89
  return parts[1]?.length ?? 0;
13
90
  }
91
+ function compareValue(track, value, threshold) {
92
+ return track === "PEAK" ? value.isGreaterThanOrEqualTo(threshold) : value.isLessThan(threshold);
93
+ }
14
94
 
15
95
  // src/fills/compute.ts
16
96
  function computeEntryPriceForAsset(fills) {
17
97
  let entryPrice = ZERO;
18
98
  let positionSize = ZERO;
19
99
  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();
100
+ const price = new BigNumber9(fill.price);
101
+ const quantity = new BigNumber9(fill.quantity);
102
+ const signed2 = fill.side === "BUY" ? quantity : quantity.negated();
103
+ const newPositionSize = positionSize.plus(signed2);
104
+ const isSameDirection = !positionSize.isZero() && positionSize.isPositive() === signed2.isPositive();
25
105
  const isFlip = !positionSize.isZero() && !newPositionSize.isZero() && newPositionSize.isPositive() !== positionSize.isPositive();
26
106
  if (positionSize.isZero() || isFlip) {
27
107
  entryPrice = price;
@@ -35,56 +115,52 @@ function computeEntryPriceForAsset(fills) {
35
115
 
36
116
  // src/asset/compute.ts
37
117
  function computeAssetEntryNotional(signedQuantity, fills) {
38
- if (signedQuantity.isZero()) return ZERO;
118
+ if (signedQuantity.isZero()) {
119
+ return ZERO;
120
+ }
39
121
  const entryPrice = computeEntryPriceForAsset(fills);
40
- if (entryPrice.isZero()) return ZERO;
122
+ if (entryPrice.isZero()) {
123
+ return ZERO;
124
+ }
41
125
  const unsignedQuantity = signedQuantity.abs();
42
126
  return unsignedQuantity.multipliedBy(entryPrice);
43
127
  }
44
128
  function computeAssetUnrealizedPnL(signedQuantity, currentPrice, fills) {
45
- if (signedQuantity.isZero()) return ZERO;
129
+ if (signedQuantity.isZero()) {
130
+ return ZERO;
131
+ }
46
132
  const entryPrice = computeEntryPriceForAsset(fills);
47
- if (entryPrice.isZero()) return ZERO;
133
+ if (entryPrice.isZero()) {
134
+ return ZERO;
135
+ }
48
136
  const deltaPrice = currentPrice.minus(entryPrice);
49
137
  return signedQuantity.multipliedBy(deltaPrice);
50
138
  }
51
139
  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());
140
+ return walkAssetFills(fills).gross;
141
+ }
142
+ function computeAssetUndeterminedSizes(fills) {
143
+ const result = {};
144
+ for (const asset of computeAssetRealizedPnLs(fills)) {
145
+ if (!asset.undeterminedSize.isZero()) {
146
+ result[asset.id] = asset.undeterminedSize;
74
147
  }
75
- positionSize = newPositionSize;
76
148
  }
77
- return gross;
149
+ return result;
78
150
  }
79
151
  function computeAssetUnrealizedPnLs(exposureMap, priceMap, fills) {
80
152
  const result = [];
81
153
  for (const [id, signedQtyString] of Object.entries(exposureMap)) {
82
154
  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);
155
+ if (!priceString) {
156
+ continue;
157
+ }
158
+ const signedQuantity = new BigNumber9(signedQtyString);
159
+ if (signedQuantity.isZero()) {
160
+ continue;
161
+ }
162
+ const currentPrice = new BigNumber9(priceString);
163
+ const assetFills = fills.filter((fill) => fill.symbol === id);
88
164
  const entryPrice = computeEntryPriceForAsset(assetFills);
89
165
  const upnl = signedQuantity.multipliedBy(currentPrice.minus(entryPrice));
90
166
  result.push({
@@ -99,22 +175,23 @@ function computeAssetUnrealizedPnLs(exposureMap, priceMap, fills) {
99
175
  }
100
176
  function computeAssetRealizedPnLs(fills) {
101
177
  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);
178
+ for (const fill of fills) {
179
+ const list = bySymbol.get(fill.symbol) ?? [];
180
+ list.push(fill);
181
+ bySymbol.set(fill.symbol, list);
106
182
  }
107
183
  const result = [];
108
184
  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);
185
+ const tradeFees = assetFills.reduce((acc, fill) => acc.plus(new BigNumber9(fill.tradeFee ?? "0")), ZERO);
186
+ const pearFees = assetFills.reduce((acc, fill) => acc.plus(new BigNumber9(fill.pearFee ?? "0")), ZERO);
111
187
  const totalFees = tradeFees.plus(pearFees);
112
- const grossPnl = computeAssetGrossRealizedPnL(assetFills);
188
+ const { gross: grossPnl, undeterminedSize } = walkAssetFills(assetFills);
113
189
  const netPnl = grossPnl.minus(totalFees);
114
190
  result.push({
115
191
  id,
116
192
  grossPnl,
117
193
  netPnl,
194
+ undeterminedSize,
118
195
  totalFees,
119
196
  tradeFees,
120
197
  pearFees
@@ -122,48 +199,148 @@ function computeAssetRealizedPnLs(fills) {
122
199
  }
123
200
  return result;
124
201
  }
202
+ function walkAssetFills(fills) {
203
+ let entryPrice = ZERO;
204
+ let positionSize = ZERO;
205
+ let basisUnpriced = false;
206
+ let gross = ZERO;
207
+ let undeterminedSize = ZERO;
208
+ for (const fill of fills) {
209
+ const price = parse(fill.price);
210
+ const quantity = parse(fill.quantity);
211
+ const signedQuantity = signedBySide(quantity, fill.side);
212
+ const newPositionSize = add(positionSize, signedQuantity);
213
+ if (!isZero(positionSize) && isPositive(positionSize) !== isPositive(signedQuantity)) {
214
+ const closedQty = min(positionSize.abs(), signedQuantity.abs());
215
+ if (basisUnpriced || isZero(price)) {
216
+ undeterminedSize = add(undeterminedSize, closedQty);
217
+ } else if (isPositive(positionSize)) {
218
+ gross = add(gross, multiply(closedQty, subtract(price, entryPrice)));
219
+ } else {
220
+ gross = add(gross, multiply(closedQty, subtract(entryPrice, price)));
221
+ }
222
+ }
223
+ const isSameDirection = !isZero(positionSize) && isPositive(positionSize) === isPositive(signedQuantity);
224
+ const isFlip = !isZero(positionSize) && !isZero(newPositionSize) && isPositive(newPositionSize) !== isPositive(positionSize);
225
+ if (isZero(positionSize) || isFlip) {
226
+ entryPrice = price;
227
+ basisUnpriced = isZero(price);
228
+ } else if (isSameDirection) {
229
+ entryPrice = entryPrice.times(positionSize.abs()).plus(price.times(quantity)).div(newPositionSize.abs());
230
+ basisUnpriced = basisUnpriced || isZero(price);
231
+ }
232
+ positionSize = newPositionSize;
233
+ }
234
+ return { gross, undeterminedSize };
235
+ }
125
236
  function computeBasketWeightedRatioV1(basket, priceMap) {
126
237
  let logSum = 0;
127
238
  for (const leg of basket) {
128
239
  const price = priceMap[leg.symbol];
129
- if (!price) return null;
240
+ if (!price) {
241
+ return null;
242
+ }
130
243
  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);
244
+ if (priceNum <= 0) {
245
+ return null;
246
+ }
247
+ const sign2 = leg.side === "BUY" ? 1 : -1;
248
+ logSum += sign2 * leg.weight * Math.log(priceNum);
134
249
  }
135
250
  return exponentiate(logSum);
136
251
  }
137
252
  function contractsToBase(contracts, contractSize) {
138
- return new BigNumber7(contracts).times(contractSize);
253
+ return new BigNumber9(contracts).times(contractSize);
139
254
  }
140
255
  function baseToContracts(base, contractSize) {
141
- return new BigNumber7(base).div(contractSize);
256
+ return new BigNumber9(base).div(contractSize);
142
257
  }
143
- function computePositionMarkNotional(assetUPnLs) {
144
- return assetUPnLs.reduce((acc, asset) => {
145
- if (asset.signedQuantity.isZero()) return acc;
146
- return acc.plus(asset.signedQuantity.abs().multipliedBy(asset.currentPrice));
147
- }, ZERO);
258
+ function computeRealizedPnlBySymbol(fills) {
259
+ const bySymbol = groupFillsBySymbol(fills);
260
+ const result = /* @__PURE__ */ new Map();
261
+ for (const [symbol, symbolFills] of bySymbol) {
262
+ const sorted = [...symbolFills].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
263
+ result.set(symbol, computeRealizedForSymbol(sorted));
264
+ }
265
+ return result;
148
266
  }
149
- function computePositionEntryNotional(assetUPnLs) {
150
- return assetUPnLs.reduce((acc, asset) => {
151
- if (asset.signedQuantity.isZero()) return acc;
152
- return acc.plus(asset.signedQuantity.abs().multipliedBy(asset.entryPrice));
153
- }, ZERO);
267
+ function computeRealizedPnlFromFills(fills) {
268
+ const byPnl = computeRealizedPnlBySymbol(fills);
269
+ let total = ZERO;
270
+ for (const pnl of byPnl.values()) {
271
+ total = total.plus(pnl);
272
+ }
273
+ return total;
154
274
  }
155
- function computePositionUnrealizedPnL(assetUPnLs) {
156
- return assetUPnLs.reduce((acc, asset) => acc.plus(asset.upnl), ZERO);
275
+ function groupFillsBySymbol(fills) {
276
+ const bySymbol = /* @__PURE__ */ new Map();
277
+ for (const fill of fills) {
278
+ const list = bySymbol.get(fill.symbol) ?? [];
279
+ list.push(fill);
280
+ bySymbol.set(fill.symbol, list);
281
+ }
282
+ return bySymbol;
157
283
  }
158
- function computePositionGrossRealizedPnL(assetPnLs) {
159
- return assetPnLs.reduce((acc, asset) => acc.plus(asset.grossPnl), ZERO);
284
+ function computeRealizedForSymbol(fills) {
285
+ let entryPrice = ZERO;
286
+ let positionSize = ZERO;
287
+ let realized = ZERO;
288
+ for (const fill of fills) {
289
+ const price = new BigNumber9(fill.price);
290
+ const quantity = new BigNumber9(fill.quantity);
291
+ const signed2 = fill.side === "BUY" ? quantity : quantity.negated();
292
+ const newPositionSize = positionSize.plus(signed2);
293
+ if (!positionSize.isZero() && positionSize.isPositive() !== signed2.isPositive()) {
294
+ const closedQty = BigNumber9.min(positionSize.abs(), signed2.abs());
295
+ if (positionSize.isPositive()) {
296
+ realized = realized.plus(closedQty.multipliedBy(price.minus(entryPrice)));
297
+ } else {
298
+ realized = realized.plus(closedQty.multipliedBy(entryPrice.minus(price)));
299
+ }
300
+ }
301
+ const isSameDirection = !positionSize.isZero() && positionSize.isPositive() === signed2.isPositive();
302
+ const isFlip = !positionSize.isZero() && !newPositionSize.isZero() && newPositionSize.isPositive() !== positionSize.isPositive();
303
+ if (positionSize.isZero() || isFlip) {
304
+ entryPrice = price;
305
+ } else if (isSameDirection) {
306
+ entryPrice = entryPrice.times(positionSize.abs()).plus(price.times(quantity)).div(newPositionSize.abs());
307
+ }
308
+ positionSize = newPositionSize;
309
+ }
310
+ return realized;
160
311
  }
161
- function computePositionNetRealizedPnL(assetPnLs) {
162
- return assetPnLs.reduce((acc, asset) => acc.plus(asset.netPnl), ZERO);
312
+ function computeUnrealizedPositionState(exposureMap, priceMap, fills) {
313
+ const assetUPnLs = computeAssetUnrealizedPnLs(exposureMap, priceMap, fills);
314
+ const entryPrice = computePositionEntryPrice(assetUPnLs);
315
+ const entryNotional = computePositionEntryNotional(assetUPnLs);
316
+ const markPrice = computePositionCurrentPrice(assetUPnLs);
317
+ const markNotional = computePositionMarkNotional(assetUPnLs);
318
+ const upnl = computePositionUnrealizedPnL(assetUPnLs);
319
+ const upnlBips = computePositionUnrealizedPnLBips(upnl, entryNotional);
320
+ return {
321
+ assetUPnLs,
322
+ entryPrice,
323
+ entryNotional,
324
+ markPrice,
325
+ markNotional,
326
+ upnl,
327
+ upnlBips
328
+ };
163
329
  }
164
- function computePositionUnrealizedPnLBips(upnl, entryNotional) {
165
- if (upnl.isZero() || entryNotional.isZero()) return ZERO;
166
- return upnl.div(entryNotional).multipliedBy(BIPS);
330
+ function computeRealizedPositionState(fills) {
331
+ const assetPnLs = computeAssetRealizedPnLs(fills);
332
+ const grossPnl = computePositionGrossRealizedPnL(assetPnLs);
333
+ const netPnl = computePositionNetRealizedPnL(assetPnLs);
334
+ const tradeFees = assetPnLs.reduce((acc, asset) => acc.plus(asset.tradeFees), ZERO);
335
+ const pearFees = assetPnLs.reduce((acc, asset) => acc.plus(asset.pearFees), ZERO);
336
+ const totalFees = tradeFees.plus(pearFees);
337
+ return {
338
+ grossPnl,
339
+ netPnl,
340
+ totalFees,
341
+ tradeFees,
342
+ pearFees
343
+ };
167
344
  }
168
345
  function computePositionEntryPrice(assetUPnLs) {
169
346
  const { totalWeightedPrice, totalWeight } = assetUPnLs.reduce(
@@ -184,6 +361,14 @@ function computePositionEntryPrice(assetUPnLs) {
184
361
  }
185
362
  return totalWeightedPrice.dividedBy(totalWeight);
186
363
  }
364
+ function computePositionEntryNotional(assetUPnLs) {
365
+ return assetUPnLs.reduce((acc, asset) => {
366
+ if (asset.signedQuantity.isZero()) {
367
+ return acc;
368
+ }
369
+ return acc.plus(asset.signedQuantity.abs().multipliedBy(asset.entryPrice));
370
+ }, ZERO);
371
+ }
187
372
  function computePositionCurrentPrice(assetUPnLs) {
188
373
  const { totalWeightedPrice, totalWeight } = assetUPnLs.reduce(
189
374
  (acc, { signedQuantity, currentPrice }) => {
@@ -203,42 +388,40 @@ function computePositionCurrentPrice(assetUPnLs) {
203
388
  }
204
389
  return totalWeightedPrice.dividedBy(totalWeight);
205
390
  }
206
- function computeUnrealizedPositionState(exposureMap, priceMap, fills) {
207
- const assetUPnLs = computeAssetUnrealizedPnLs(exposureMap, priceMap, fills);
208
- const entryPrice = computePositionEntryPrice(assetUPnLs);
209
- const entryNotional = computePositionEntryNotional(assetUPnLs);
210
- const markPrice = computePositionCurrentPrice(assetUPnLs);
211
- const markNotional = computePositionMarkNotional(assetUPnLs);
212
- const upnl = computePositionUnrealizedPnL(assetUPnLs);
213
- const upnlBips = computePositionUnrealizedPnLBips(upnl, entryNotional);
214
- return {
215
- assetUPnLs,
216
- entryPrice,
217
- entryNotional,
218
- markPrice,
219
- markNotional,
220
- upnl,
221
- upnlBips
222
- };
391
+ function computePositionMarkNotional(assetUPnLs) {
392
+ return assetUPnLs.reduce((acc, asset) => {
393
+ if (asset.signedQuantity.isZero()) {
394
+ return acc;
395
+ }
396
+ return acc.plus(asset.signedQuantity.abs().multipliedBy(asset.currentPrice));
397
+ }, ZERO);
223
398
  }
224
- function computeRealizedPositionState(fills) {
225
- const assetPnLs = computeAssetRealizedPnLs(fills);
226
- const grossPnl = computePositionGrossRealizedPnL(assetPnLs);
227
- const netPnl = computePositionNetRealizedPnL(assetPnLs);
228
- const tradeFees = assetPnLs.reduce((acc, asset) => acc.plus(asset.tradeFees), ZERO);
229
- const pearFees = assetPnLs.reduce((acc, asset) => acc.plus(asset.pearFees), ZERO);
230
- const totalFees = tradeFees.plus(pearFees);
231
- return {
232
- grossPnl,
233
- netPnl,
234
- totalFees,
235
- tradeFees,
236
- pearFees
237
- };
399
+ function computePositionUnrealizedPnL(assetUPnLs) {
400
+ return assetUPnLs.reduce((acc, asset) => acc.plus(asset.upnl), ZERO);
401
+ }
402
+ function computePositionUnrealizedPnLBips(upnl, entryNotional) {
403
+ if (upnl.isZero() || entryNotional.isZero()) {
404
+ return ZERO;
405
+ }
406
+ return upnl.div(entryNotional).multipliedBy(BIPS);
407
+ }
408
+ function computePositionGrossRealizedPnL(assetPnLs) {
409
+ return assetPnLs.reduce((acc, asset) => acc.plus(asset.grossPnl), ZERO);
238
410
  }
239
- BigNumber7.config({
411
+ function computePositionNetRealizedPnL(assetPnLs) {
412
+ return assetPnLs.reduce((acc, asset) => acc.plus(asset.netPnl), ZERO);
413
+ }
414
+ function generateDeterministicPositionKey(input) {
415
+ const symbols = [...input.symbols].sort((a, b) => a.localeCompare(b));
416
+ const payload = JSON.stringify({
417
+ tradeAccountId: input.tradeAccountId.toLowerCase(),
418
+ symbols
419
+ });
420
+ return bytesToHex(sha256(new TextEncoder().encode(payload)));
421
+ }
422
+ BigNumber9.config({
240
423
  DECIMAL_PLACES: 30,
241
- ROUNDING_MODE: BigNumber7.ROUND_DOWN
424
+ ROUNDING_MODE: BigNumber9.ROUND_DOWN
242
425
  });
243
426
  function precisePrice(connector, raw, precision, side = "BUY", slippageToleranceBps = 0) {
244
427
  const normalized = normalizePrice(raw);
@@ -257,22 +440,22 @@ function withSlippageTolerance(normalized, slippageToleranceBps, side) {
257
440
  if (slippageToleranceBps === 0) {
258
441
  return normalized;
259
442
  }
260
- const bps = new BigNumber7(slippageToleranceBps);
443
+ const bps = new BigNumber9(slippageToleranceBps);
261
444
  const factor = bps.div(1e4);
262
- return side === "BUY" ? normalized.times(new BigNumber7(1).plus(factor)) : normalized.times(new BigNumber7(1).minus(factor));
445
+ return side === "BUY" ? normalized.times(new BigNumber9(1).plus(factor)) : normalized.times(new BigNumber9(1).minus(factor));
263
446
  }
264
447
  function preciseHyperliquidPrice(price, precision, side) {
265
448
  const maxDecimals = precision.price.decimals;
266
449
  const maxSigFigs = precision.price.sigFigs ?? 5;
267
- const rounding = side === "BUY" ? BigNumber7.ROUND_FLOOR : BigNumber7.ROUND_CEIL;
450
+ const rounding = side === "BUY" ? BigNumber9.ROUND_FLOOR : BigNumber9.ROUND_CEIL;
268
451
  price = price.decimalPlaces(maxDecimals, rounding);
269
452
  price = clampSigFigs(price, maxSigFigs, maxDecimals, rounding);
270
453
  return price.toFixed();
271
454
  }
272
455
  function preciseTickGridPrice(price, precision, side) {
273
456
  const tick = precision.price.tick ?? 1e-4;
274
- const rounding = side === "BUY" ? BigNumber7.ROUND_FLOOR : BigNumber7.ROUND_CEIL;
275
- const tickBN = new BigNumber7(tick);
457
+ const rounding = side === "BUY" ? BigNumber9.ROUND_FLOOR : BigNumber9.ROUND_CEIL;
458
+ const tickBN = new BigNumber9(tick);
276
459
  const scaled = price.div(tickBN);
277
460
  const snapped = scaled.integerValue(rounding).times(tickBN);
278
461
  const maxDecimals = precision.price.decimals;
@@ -280,12 +463,12 @@ function preciseTickGridPrice(price, precision, side) {
280
463
  return final.toFixed();
281
464
  }
282
465
  function clampSigFigs(price, maxSigFigs, maxDecimals, rounding) {
283
- const countSigFigs = (x) => x.toFixed().replace(".", "").replace(/^0+/, "").length;
466
+ const countSigFigs = (value) => value.toFixed().replace(".", "").replace(/^0+/, "").length;
284
467
  if (countSigFigs(price) <= maxSigFigs) {
285
468
  return price;
286
469
  }
287
- for (let d = maxDecimals; d >= 0; d--) {
288
- const candidate = price.decimalPlaces(d, rounding);
470
+ for (let decimals = maxDecimals; decimals >= 0; decimals--) {
471
+ const candidate = price.decimalPlaces(decimals, rounding);
289
472
  if (countSigFigs(candidate) <= maxSigFigs) {
290
473
  return candidate;
291
474
  }
@@ -293,12 +476,12 @@ function clampSigFigs(price, maxSigFigs, maxDecimals, rounding) {
293
476
  return price;
294
477
  }
295
478
  function normalizePrice(value) {
296
- const bn = new BigNumber7(value);
297
- return new BigNumber7(bn.toFixed());
479
+ const parsed = new BigNumber9(value);
480
+ return new BigNumber9(parsed.toFixed());
298
481
  }
299
- BigNumber7.config({
482
+ BigNumber9.config({
300
483
  DECIMAL_PLACES: 30,
301
- ROUNDING_MODE: BigNumber7.ROUND_DOWN
484
+ ROUNDING_MODE: BigNumber9.ROUND_DOWN
302
485
  });
303
486
  function preciseQuantity(connector, raw, precision, side) {
304
487
  switch (connector) {
@@ -313,222 +496,1042 @@ function preciseQuantity(connector, raw, precision, side) {
313
496
  }
314
497
  function preciseHyperliquidQuantity(raw, precision, side) {
315
498
  const decimals = precision.quantity.decimals;
316
- const qty = new BigNumber7(raw);
317
- const rounded = qty.decimalPlaces(decimals, side === "BUY" ? BigNumber7.ROUND_FLOOR : BigNumber7.ROUND_CEIL);
499
+ const quantity = new BigNumber9(raw);
500
+ const rounded = quantity.decimalPlaces(decimals, side === "BUY" ? BigNumber9.ROUND_FLOOR : BigNumber9.ROUND_CEIL);
318
501
  return rounded.toFixed();
319
502
  }
320
503
  function preciseStepGridQuantity(raw, precision, side) {
321
504
  const step = precision.quantity.step;
322
- const qty = baseToContracts(raw, precision.quantity.contractSize);
323
- const stepBN = new BigNumber7(step);
324
- const scaled = qty.div(stepBN);
325
- const rounded = side === "BUY" ? scaled.integerValue(BigNumber7.ROUND_FLOOR) : scaled.integerValue(BigNumber7.ROUND_CEIL);
505
+ const contractQuantity = baseToContracts(raw, precision.quantity.contractSize);
506
+ const stepBN = new BigNumber9(step);
507
+ const scaled = contractQuantity.div(stepBN);
508
+ const rounded = side === "BUY" ? scaled.integerValue(BigNumber9.ROUND_FLOOR) : scaled.integerValue(BigNumber9.ROUND_CEIL);
326
509
  const result = rounded.times(stepBN);
327
510
  const maxDecimals = precision.quantity.decimals;
328
- const final = result.decimalPlaces(maxDecimals, side === "BUY" ? BigNumber7.ROUND_FLOOR : BigNumber7.ROUND_CEIL);
511
+ const final = result.decimalPlaces(maxDecimals, side === "BUY" ? BigNumber9.ROUND_FLOOR : BigNumber9.ROUND_CEIL);
329
512
  return final.toFixed();
330
513
  }
331
- async function computeSyncPayload(args) {
332
- const { openPositions, provisionalFills, confirmedExchangeFillIds, externalFills } = args;
333
- const fillsToProcess = _prepareExternalFills(externalFills, confirmedExchangeFillIds);
334
- if (fillsToProcess.length === 0) {
335
- return null;
514
+
515
+ // src/sync/applicables/constants.ts
516
+ var DUST = parse("0.00000001");
517
+ var MISSED_HISTORY_FILL_PREFIX = "opening-synthetic-";
518
+ var BASKET_RULES = [
519
+ // V2 Basket Hyperliquid — legs of one Pear basket order share the order's cloid.
520
+ {
521
+ matches: (fill) => fill.connector === "hyperliquid" && isPearCloid(fill.connector, fill.cloid),
522
+ key: (fill) => `v2-basket-hl ${fill.cloid}`
336
523
  }
337
- const { reconcilables, confirmables, orphanCreatables } = _seggregateExternalFills(provisionalFills, fillsToProcess);
338
- const groupedConfirmables = confirmables.reduce((acc, fill) => {
339
- acc[fill.positionId] = [...acc[fill.positionId] || [], fill];
340
- return acc;
341
- }, {});
342
- const groupedCreatablesBySymbol = _groupCreatablesBySymbol(orphanCreatables, openPositions);
343
- const groupedCreatables = {};
344
- for (const group of groupedCreatablesBySymbol.values()) {
345
- const zeroCrossing = _walkBackwardsToFindLastZeroCrossing(group);
346
- for (const fill of zeroCrossing) {
347
- const existing = groupedCreatables[fill.positionId];
348
- if (existing) {
349
- groupedCreatables[fill.positionId] = [...existing, fill];
350
- } else {
351
- groupedCreatables[fill.positionId] = [fill];
524
+ ];
525
+ function isPearCloid(connector, cloid) {
526
+ if (!cloid) {
527
+ return false;
528
+ }
529
+ return PEAR_CLOID_CHECK[connector]?.(cloid) ?? false;
530
+ }
531
+ var PEAR_CLOID_CHECK = {
532
+ hyperliquid: (cloid) => cloid.startsWith("0x50454152"),
533
+ lighter: (cloid) => cloid.length === 15 && cloid.startsWith("10")
534
+ };
535
+
536
+ // src/sync/applicables/basket.ts
537
+ var Basket = class _Basket {
538
+ constructor(legs) {
539
+ this.legs = legs;
540
+ }
541
+ legs;
542
+ /** Detect baskets from a fill stream, returned in ascending time order. */
543
+ static detect(fills) {
544
+ return groupFillsIntoBaskets(fills).map((group) => new _Basket(combineFillsBySymbolAndSide(group)));
545
+ }
546
+ /** Active long symbol set the matcher compares against open positions. */
547
+ get longs() {
548
+ return new Set(this.legs.filter((leg) => leg.side === "BUY").map((leg) => leg.symbol));
549
+ }
550
+ /** Active short symbol set the matcher compares against open positions. */
551
+ get shorts() {
552
+ return new Set(this.legs.filter((leg) => leg.side === "SELL").map((leg) => leg.symbol));
553
+ }
554
+ };
555
+ var CombinedLeg = class {
556
+ constructor(symbol, side) {
557
+ this.symbol = symbol;
558
+ this.side = side;
559
+ }
560
+ symbol;
561
+ side;
562
+ _totalSize = ZERO;
563
+ queue = [];
564
+ get totalSize() {
565
+ return this._totalSize;
566
+ }
567
+ /** Append a fill to this leg's queue and grow the total size. */
568
+ addFill(fill) {
569
+ this._totalSize = this._totalSize.plus(fill.quantity);
570
+ this.queue.push({ fill, remaining: parse(fill.quantity) });
571
+ }
572
+ /**
573
+ * Take `qty` of fill slices from this leg (FIFO across source fills, consumes the queue).
574
+ * Returns the parts taken, which are consumed from the queue.
575
+ */
576
+ take(qty) {
577
+ const parts = [];
578
+ let need = qty;
579
+ for (const entry of this.queue) {
580
+ if (need.isLessThanOrEqualTo(0)) {
581
+ break;
352
582
  }
583
+ if (entry.remaining.isZero()) {
584
+ continue;
585
+ }
586
+ const taken = min(entry.remaining, need);
587
+ parts.push({ source: entry.fill, quantity: taken.toFixed() });
588
+ entry.remaining = entry.remaining.minus(taken);
589
+ need = need.minus(taken);
353
590
  }
591
+ return parts;
354
592
  }
355
- for (const fills of Object.values(groupedCreatables)) {
356
- fills.sort((a, b) => a.timestamp - b.timestamp);
593
+ };
594
+ function groupFillsIntoBaskets(fills) {
595
+ const buckets = /* @__PURE__ */ new Map();
596
+ for (const fill of fills) {
597
+ const rule = BASKET_RULES.find((r) => r.matches(fill));
598
+ const key = rule ? rule.key(fill) : `id ${fill.exchangeFillId}`;
599
+ const bucket = buckets.get(key) ?? { timestamp: fill.timestamp, fills: [] };
600
+ bucket.timestamp = Math.min(bucket.timestamp, fill.timestamp);
601
+ bucket.fills.push(fill);
602
+ buckets.set(key, bucket);
357
603
  }
358
- return {
359
- reconcilables: Object.keys(reconcilables).length > 0 ? reconcilables : null,
360
- confirmables: Object.keys(groupedConfirmables).length > 0 ? groupedConfirmables : null,
361
- creatables: Object.keys(groupedCreatables).length > 0 ? groupedCreatables : null,
362
- meta: {
363
- externalMinTs: fillsToProcess[0]?.timestamp ?? null,
364
- externalMaxTs: fillsToProcess[fillsToProcess.length - 1]?.timestamp ?? null,
365
- processedCount: fillsToProcess.length
604
+ const sorted = [...buckets.values()].sort((a, b) => a.timestamp - b.timestamp);
605
+ return sorted.map((bucket) => bucket.fills);
606
+ }
607
+ function combineFillsBySymbolAndSide(group) {
608
+ const map = /* @__PURE__ */ new Map();
609
+ for (const fill of group) {
610
+ const key = `${fill.symbol}-${fill.side}`;
611
+ let combined = map.get(key);
612
+ if (!combined) {
613
+ combined = new CombinedLeg(fill.symbol, fill.side);
614
+ map.set(key, combined);
366
615
  }
616
+ combined.addFill(fill);
617
+ }
618
+ return [...map.values()];
619
+ }
620
+
621
+ // src/sync/applicables/exposure.ts
622
+ var Exposure = class _Exposure {
623
+ constructor(legs) {
624
+ this.legs = legs;
625
+ }
626
+ legs;
627
+ /** Create an exposure with no legs — the starting state for a new position. */
628
+ static empty() {
629
+ return new _Exposure(/* @__PURE__ */ new Map());
630
+ }
631
+ /** Build from a wire `Record<symbol, quantity>` (existing DB position exposure). */
632
+ static fromRecord(record) {
633
+ const legs = /* @__PURE__ */ new Map();
634
+ for (const [symbol, qty] of Object.entries(record)) {
635
+ legs.set(symbol, parse(qty));
636
+ }
637
+ return new _Exposure(legs);
638
+ }
639
+ /** Net signed exposure a set of fill parts contributes for `symbol` (BUY +, SELL −). */
640
+ static netFromParts(parts, symbol) {
641
+ let total = ZERO;
642
+ for (const part of parts) {
643
+ if (part.source.symbol === symbol) {
644
+ total = total.plus(signedBySide(part.quantity, part.source.side));
645
+ }
646
+ }
647
+ return total;
648
+ }
649
+ /** Return an independent copy of this exposure so mutations do not alias. */
650
+ clone() {
651
+ return new _Exposure(new Map(this.legs));
652
+ }
653
+ /** Signed exposure for `symbol` (0 when absent). */
654
+ get(symbol) {
655
+ return this.legs.get(symbol) ?? ZERO;
656
+ }
657
+ /** Apply a signed `delta` to `symbol`, creating the leg if it does not exist. */
658
+ addDelta(symbol, delta) {
659
+ this.legs.set(symbol, this.get(symbol).plus(delta));
660
+ }
661
+ /** Convert a side+quantity pair to a signed delta and forward to `addDelta`. */
662
+ addSigned(symbol, quantity, side) {
663
+ this.addDelta(symbol, signedBySide(quantity, side));
664
+ }
665
+ /** Active (non-zero) long and short leg sets. */
666
+ activeLegs() {
667
+ const longs = /* @__PURE__ */ new Set();
668
+ const shorts = /* @__PURE__ */ new Set();
669
+ for (const [symbol, qty] of this.legs) {
670
+ switch (sign(qty)) {
671
+ case 1:
672
+ longs.add(symbol);
673
+ break;
674
+ case -1:
675
+ shorts.add(symbol);
676
+ break;
677
+ }
678
+ }
679
+ return { longs, shorts };
680
+ }
681
+ /** True once every leg sits within DUST of zero (effectively closed). */
682
+ isFlat() {
683
+ for (const qty of this.legs.values()) {
684
+ if (isGreaterThanOrEqual(qty.abs(), DUST)) {
685
+ return false;
686
+ }
687
+ }
688
+ return true;
689
+ }
690
+ /** True when every leg is exactly zero (a fully CLOSED position). */
691
+ isAllZero() {
692
+ for (const qty of this.legs.values()) {
693
+ if (!qty.isZero()) {
694
+ return false;
695
+ }
696
+ }
697
+ return true;
698
+ }
699
+ /**
700
+ * Snap every non-zero leg to exactly zero. Called once a position is flat so it
701
+ * reads CLOSED (all-zero exposure) and never lingers in the open list with stray
702
+ * dust that a later reopen of the same pair could merge into.
703
+ */
704
+ snapToZero() {
705
+ for (const [symbol, qty] of this.legs) {
706
+ if (!qty.isZero()) {
707
+ this.legs.set(symbol, ZERO);
708
+ }
709
+ }
710
+ }
711
+ /** All symbols with a leg, including zeroed ones (a closed leg still carries cost basis). */
712
+ symbols() {
713
+ return [...this.legs.keys()];
714
+ }
715
+ /** Canonical signed-string view for the wire (`+0` for zero). */
716
+ toSigned() {
717
+ const out = {};
718
+ for (const [symbol, qty] of this.legs) {
719
+ out[symbol] = signed(qty);
720
+ }
721
+ return out;
722
+ }
723
+ };
724
+
725
+ // src/sync/applicables/matching.ts
726
+ function matchPositions(args) {
727
+ const { tradeAccountId, orphanFills, pear } = args;
728
+ const book = WorkingBook.load({ tradeAccountId, openPositions: pear.positions() });
729
+ for (const basket of Basket.detect(orphanFills)) {
730
+ const exactCloseTarget = findExactCloseTarget(book.open(), basket.longs, basket.shorts);
731
+ const leftover = applyCloses({ legs: basket.legs, book, exactCloseTarget });
732
+ if (leftover.length === 0) {
733
+ continue;
734
+ }
735
+ const target = resolveLeftoverTarget({ book, leftover });
736
+ applyLeftover(target, leftover);
737
+ }
738
+ return book.toMatched();
739
+ }
740
+ var WorkingBook = class _WorkingBook {
741
+ constructor(tradeAccountId) {
742
+ this.tradeAccountId = tradeAccountId;
743
+ }
744
+ tradeAccountId;
745
+ all = [];
746
+ openByKey = /* @__PURE__ */ new Map();
747
+ /** Seed the book from existing open DB positions (they arrive pre-filtered by connector). */
748
+ static load(args) {
749
+ const { tradeAccountId, openPositions } = args;
750
+ const book = new _WorkingBook(tradeAccountId);
751
+ for (const p of openPositions) {
752
+ const position = {
753
+ positionKey: book.keyFor(Object.keys(p.exposure)),
754
+ exposure: Exposure.fromRecord(p.exposure),
755
+ parts: [],
756
+ closed: false,
757
+ existingOrigin: p.origin
758
+ };
759
+ book.all.push(position);
760
+ book.openByKey.set(position.positionKey, position);
761
+ }
762
+ return book;
763
+ }
764
+ /** Deterministic position key for a symbol set under this book's trade account. */
765
+ keyFor(symbols) {
766
+ return generateDeterministicPositionKey({ tradeAccountId: this.tradeAccountId, symbols });
767
+ }
768
+ /** Currently open working positions, in insertion order. */
769
+ open() {
770
+ return this.all.filter((position) => !position.closed);
771
+ }
772
+ /** Look up an open working position by its positionKey (for dedup on merge). */
773
+ findOpenByKey(key) {
774
+ return this.openByKey.get(key);
775
+ }
776
+ /** Register a fresh empty working position under `key` and return it. */
777
+ create(key) {
778
+ const position = {
779
+ positionKey: key,
780
+ exposure: Exposure.empty(),
781
+ parts: [],
782
+ closed: false
783
+ };
784
+ this.all.push(position);
785
+ this.openByKey.set(key, position);
786
+ return position;
787
+ }
788
+ /** Flip a position to closed once every leg is within DUST of zero. */
789
+ markClosedIfFlat(position) {
790
+ if (position.closed || !position.exposure.isFlat()) {
791
+ return;
792
+ }
793
+ position.exposure.snapToZero();
794
+ position.closed = true;
795
+ this.openByKey.delete(position.positionKey);
796
+ }
797
+ /** Working positions that received fills become MatchedPosition records; untouched ones are dropped. */
798
+ toMatched() {
799
+ return this.all.filter((position) => position.parts.length > 0).map((position) => ({
800
+ positionKey: position.positionKey,
801
+ parts: position.parts,
802
+ closed: position.closed,
803
+ exposure: position.exposure,
804
+ existingOrigin: position.existingOrigin
805
+ }));
806
+ }
807
+ };
808
+ function findExactCloseTarget(openList, basketLongs, basketShorts) {
809
+ return openList.find((position) => {
810
+ const { longs, shorts } = position.exposure.activeLegs();
811
+ return setEq(longs, basketShorts) && setEq(shorts, basketLongs);
812
+ });
813
+ }
814
+ var setEq = (a, b) => a.size === b.size && [...a].every((x) => b.has(x));
815
+ function applyCloses(args) {
816
+ const { legs, book, exactCloseTarget } = args;
817
+ const openList = book.open();
818
+ const leftover = [];
819
+ for (const leg of legs) {
820
+ let remaining = leg.totalSize;
821
+ const closeTargetIsLong = sideSign(leg.side) === -1;
822
+ const holders = openList.filter((position) => {
823
+ const qty = position.exposure.get(leg.symbol);
824
+ if (qty.isZero()) {
825
+ return false;
826
+ }
827
+ return closeTargetIsLong ? qty.isPositive() : qty.isNegative();
828
+ });
829
+ if (exactCloseTarget) {
830
+ holders.sort((a, b) => a === exactCloseTarget ? -1 : b === exactCloseTarget ? 1 : 0);
831
+ }
832
+ for (const position of holders) {
833
+ if (isLessThanOrEqual(remaining, ZERO)) {
834
+ break;
835
+ }
836
+ const have = position.exposure.get(leg.symbol).abs();
837
+ const reduce = min(have, remaining);
838
+ position.parts.push(...leg.take(reduce));
839
+ position.exposure.addDelta(leg.symbol, signedBySide(reduce, leg.side));
840
+ remaining = remaining.minus(reduce);
841
+ book.markClosedIfFlat(position);
842
+ }
843
+ if (isGreaterThanOrEqual(remaining, DUST)) {
844
+ leftover.push({ side: leg.side, qty: remaining, leg });
845
+ }
846
+ }
847
+ return leftover;
848
+ }
849
+ function resolveLeftoverTarget(args) {
850
+ const { book, leftover } = args;
851
+ const leftoverLongs = new Set(leftover.filter((entry) => entry.side === "BUY").map((entry) => entry.leg.symbol));
852
+ const leftoverShorts = new Set(leftover.filter((entry) => entry.side === "SELL").map((entry) => entry.leg.symbol));
853
+ const target = book.open().find((position) => {
854
+ const { longs, shorts } = position.exposure.activeLegs();
855
+ const longOk = [...leftoverLongs].every((a) => longs.has(a));
856
+ const shortOk = [...leftoverShorts].every((a) => shorts.has(a));
857
+ return longOk && shortOk;
858
+ });
859
+ if (target) {
860
+ return target;
861
+ }
862
+ const symbols = [.../* @__PURE__ */ new Set([...leftoverLongs, ...leftoverShorts])];
863
+ const positionKey = book.keyFor(symbols);
864
+ return book.findOpenByKey(positionKey) ?? book.create(positionKey);
865
+ }
866
+ function applyLeftover(target, leftover) {
867
+ for (const entry of leftover) {
868
+ target.parts.push(...entry.leg.take(entry.qty));
869
+ target.exposure.addSigned(entry.leg.symbol, entry.qty.toFixed(), entry.side);
870
+ }
871
+ }
872
+
873
+ // src/sync/applicables/synthetics.ts
874
+ function isMissedHistoryFill(fill) {
875
+ return fill.exchangeFillId.startsWith(MISSED_HISTORY_FILL_PREFIX);
876
+ }
877
+ function addFillsForMissedHistory(args) {
878
+ const missedHistoryFills = buildMissedHistoryFills(args);
879
+ return [...missedHistoryFills, ...args.orphanFills].sort((a, b) => a.timestamp - b.timestamp);
880
+ }
881
+ function alignPositionsWithExchange(args) {
882
+ const { positions, pear, snapshot, tradeAccountId } = args;
883
+ const neededSynthetics = findNeededSynthetics({ positions, pear, snapshot });
884
+ return assignSyntheticsToPositions({ neededSynthetics, positions, pear, tradeAccountId });
885
+ }
886
+ function buildSyntheticFill(args) {
887
+ return {
888
+ // Content-addressed id — no wall-clock, no positionId, so re-syncs collide
889
+ // on the dedup index and synthetic generation stays idempotent.
890
+ exchangeFillId: `synthetic-${args.positionKey}-${args.symbol}-${args.side}`,
891
+ cloid: null,
892
+ connector: args.connector,
893
+ symbol: args.symbol,
894
+ side: args.side,
895
+ quantity: args.quantity,
896
+ // Gone-close: exit price is unknown. "0" is the unpriceable sentinel — real
897
+ // fills are never 0 and missed-history fills derive a positive price, so
898
+ // consumers treat a 0-price fill as unpriceable (excluded from PnL/avg-entry,
899
+ // its closed size routed to undeterminedSize).
900
+ price: "0",
901
+ usd: "0",
902
+ timestamp: args.timestamp,
903
+ reduceOnly: false,
904
+ synthetic: true,
905
+ isExternal: true,
906
+ tradeFee: "0",
907
+ builderFee: "0",
908
+ closedReason: args.closedReason,
909
+ liquidation: null
367
910
  };
368
911
  }
369
- function _prepareExternalFills(externalFills, confirmedExchangeFillIdsInput) {
370
- const dedupedByExchangeFillId = /* @__PURE__ */ new Map();
371
- const confirmedExchangeFillIds = new Set(
372
- Array.from(confirmedExchangeFillIdsInput).map((exchangeFillId) => _normalizeExchangeFillId(exchangeFillId)).filter((exchangeFillId) => exchangeFillId !== void 0)
373
- );
374
- const candidates = externalFills.reduce((acc, fill) => {
375
- const normalizedExchangeFillId = _normalizeExchangeFillId(fill.exchangeFillId);
376
- if (!normalizedExchangeFillId) return acc;
377
- if (!_isValidExternalQuantity(fill.quantity)) return acc;
378
- if (!_isValidExternalTimestamp(fill.timestamp)) return acc;
379
- if (confirmedExchangeFillIds.has(normalizedExchangeFillId)) return acc;
380
- acc.push({ ...fill, exchangeFillId: normalizedExchangeFillId });
381
- return acc;
382
- }, []);
383
- for (const fill of candidates) {
384
- const existing = dedupedByExchangeFillId.get(fill.exchangeFillId);
385
- if (!existing || fill.timestamp < existing.timestamp) {
386
- dedupedByExchangeFillId.set(fill.exchangeFillId, fill);
912
+ function buildMissedHistoryFills(args) {
913
+ const { orphanFills, pear, connector, entryBySymbol, snapshot } = args;
914
+ const fillTotals = /* @__PURE__ */ new Map();
915
+ const orphanNotional = /* @__PURE__ */ new Map();
916
+ for (const fill of orphanFills) {
917
+ fillTotals.set(fill.symbol, add(fillTotals.get(fill.symbol) ?? ZERO, signedBySide(fill.quantity, fill.side)));
918
+ const notional = multiply(fill.quantity, fill.price);
919
+ orphanNotional.set(fill.symbol, add(orphanNotional.get(fill.symbol) ?? ZERO, notional));
920
+ }
921
+ const pearTotals = pear.totalBySymbol();
922
+ const assets = /* @__PURE__ */ new Set([...orphanFills.map((fill) => fill.symbol), ...snapshot.symbols()]);
923
+ const earliestTs = orphanFills.reduce((min2, fill) => Math.min(min2, fill.timestamp), Infinity);
924
+ if (!Number.isFinite(earliestTs)) {
925
+ return [];
926
+ }
927
+ const watermark = Math.max(1, earliestTs - 1);
928
+ const haveBySymbol = (symbol) => add(pearTotals.get(symbol) ?? ZERO, fillTotals.get(symbol) ?? ZERO);
929
+ const result = [];
930
+ for (const gap of gapsToTarget({
931
+ assets,
932
+ haveBySymbol,
933
+ snapshot
934
+ })) {
935
+ const { symbol } = gap;
936
+ const price = priceForMissedHistoryFill({
937
+ exchangePosition: snapshot.get(symbol),
938
+ syntheticQty: gap.quantity,
939
+ pearQty: pearTotals.get(symbol) ?? ZERO,
940
+ pearEntry: entryBySymbol[symbol],
941
+ orphanNotional: orphanNotional.get(symbol) ?? ZERO
942
+ });
943
+ result.push({
944
+ connector,
945
+ exchangeFillId: `${MISSED_HISTORY_FILL_PREFIX}${symbol}-${gap.side.toLowerCase()}`,
946
+ cloid: null,
947
+ symbol,
948
+ price,
949
+ quantity: gap.quantity,
950
+ side: gap.side,
951
+ reduceOnly: false,
952
+ timestamp: watermark,
953
+ tradeFee: "0",
954
+ builderFee: "0",
955
+ liquidation: null
956
+ });
957
+ }
958
+ return result;
959
+ }
960
+ function priceForMissedHistoryFill(args) {
961
+ const { exchangePosition, syntheticQty, pearQty, pearEntry, orphanNotional } = args;
962
+ const fallback = exchangePosition?.entryPrice ?? "0";
963
+ if (!exchangePosition) {
964
+ return fallback;
965
+ }
966
+ let realNotional = orphanNotional;
967
+ if (!isZero(pearQty)) {
968
+ if (pearEntry === void 0) {
969
+ return fallback;
387
970
  }
971
+ realNotional = add(realNotional, multiply(abs(pearQty), pearEntry));
388
972
  }
389
- return Array.from(dedupedByExchangeFillId.values()).sort((a, b) => a.timestamp - b.timestamp);
973
+ const synthQty = parse(syntheticQty);
974
+ if (isZero(synthQty)) {
975
+ return fallback;
976
+ }
977
+ const targetNotional = multiply(exchangePosition.size, exchangePosition.entryPrice);
978
+ const derived = divide(subtract(targetNotional, realNotional), synthQty);
979
+ return derived.isFinite() && derived.isPositive() ? derived.toFixed() : fallback;
390
980
  }
391
- function _groupCreatablesBySymbol(orphanCreatables, openPositions) {
392
- const groupedCreatables = /* @__PURE__ */ new Map();
393
- for (const fill of orphanCreatables) {
394
- const positionCandidates = openPositions.reduce((acc, p) => {
395
- const exposure = p.exposure[fill.symbol];
396
- if (!exposure) return acc;
397
- const quantity = new BigNumber(exposure);
398
- if (quantity.isZero() || quantity.isEqualTo("-0")) return acc;
399
- if (fill.timestamp < Date.parse(p.createdAt)) return acc;
400
- acc.push(p);
401
- return acc;
402
- }, []);
403
- const first = positionCandidates[0];
404
- if (!first) continue;
405
- const splits = _splitMaybeCompositeFillAcrossPositions(fill, [first, ...positionCandidates.slice(1)]);
406
- for (const { position, maybeModifiedFill } of splits) {
407
- const key = `${position.id}-${maybeModifiedFill.symbol}`;
408
- const existing = groupedCreatables.get(key);
409
- if (existing) {
410
- existing.fills.push({ ...maybeModifiedFill, type: "create", positionId: position.id });
411
- groupedCreatables.set(key, existing);
412
- } else {
413
- groupedCreatables.set(key, {
414
- positionId: position.id,
415
- symbol: maybeModifiedFill.symbol,
416
- exposure: new BigNumber(position.exposure[maybeModifiedFill.symbol] ?? 0),
417
- fills: [{ ...maybeModifiedFill, type: "create", positionId: position.id }]
981
+ function findNeededSynthetics(args) {
982
+ const { positions, pear, snapshot } = args;
983
+ const pearTotals = pear.totalBySymbol();
984
+ const assets = /* @__PURE__ */ new Set();
985
+ for (const pos of positions) {
986
+ for (const part of pos.parts) {
987
+ assets.add(part.source.symbol);
988
+ }
989
+ }
990
+ for (const symbol of snapshot.symbols()) {
991
+ assets.add(symbol);
992
+ }
993
+ for (const symbol of pearTotals.keys()) {
994
+ assets.add(symbol);
995
+ }
996
+ const haveBySymbol = (symbol) => {
997
+ let totalFills = ZERO;
998
+ for (const pos of positions) {
999
+ totalFills = add(totalFills, Exposure.netFromParts(pos.parts, symbol));
1000
+ }
1001
+ return add(pearTotals.get(symbol) ?? ZERO, totalFills);
1002
+ };
1003
+ const result = /* @__PURE__ */ new Map();
1004
+ for (const gap of gapsToTarget({
1005
+ assets,
1006
+ haveBySymbol,
1007
+ snapshot
1008
+ })) {
1009
+ switch (gap.kind) {
1010
+ case "gone":
1011
+ result.set(gap.symbol, {
1012
+ symbol: gap.symbol,
1013
+ side: gap.side,
1014
+ quantity: gap.quantity,
1015
+ closedReason: "NETTED"
418
1016
  });
1017
+ break;
1018
+ case "gap":
1019
+ result.set(gap.symbol, {
1020
+ symbol: gap.symbol,
1021
+ side: gap.side,
1022
+ quantity: gap.quantity
1023
+ });
1024
+ break;
1025
+ }
1026
+ }
1027
+ return result;
1028
+ }
1029
+ function assignSyntheticsToPositions(args) {
1030
+ const { neededSynthetics, positions, pear, tradeAccountId } = args;
1031
+ const keyOf = (symbols) => generateDeterministicPositionKey({ tradeAccountId, symbols });
1032
+ const existingBySymbol = pear.positionBySymbol();
1033
+ const created = [];
1034
+ const byPosition = /* @__PURE__ */ new Map();
1035
+ for (const [symbol, spec] of neededSynthetics) {
1036
+ let target = positions.find((pos) => !pos.closed && pos.parts.some((part) => part.source.symbol === symbol));
1037
+ if (!target) {
1038
+ const existing = existingBySymbol.get(symbol);
1039
+ const positionKey = existing ? keyOf(Object.keys(existing.exposure)) : keyOf([symbol]);
1040
+ target = positions.find((pos) => pos.positionKey === positionKey) ?? created.find((pos) => pos.positionKey === positionKey);
1041
+ if (!target) {
1042
+ target = existing ? (
1043
+ // Stand-in for an existing DB position untouched by this batch's fills:
1044
+ // seed its exposure and origin so folding the synthetic yields the true
1045
+ // final state (a gone-close nets to zero -> CLOSED/NETTED, a gap writes
1046
+ // the full total, not just the delta).
1047
+ {
1048
+ positionKey,
1049
+ parts: [],
1050
+ closed: false,
1051
+ exposure: Exposure.fromRecord(existing.exposure),
1052
+ existingOrigin: existing.origin
1053
+ }
1054
+ ) : {
1055
+ positionKey,
1056
+ parts: [],
1057
+ closed: false,
1058
+ exposure: Exposure.empty()
1059
+ };
1060
+ created.push(target);
419
1061
  }
420
1062
  }
1063
+ const list = byPosition.get(target) ?? [];
1064
+ list.push(spec);
1065
+ byPosition.set(target, list);
421
1066
  }
422
- return groupedCreatables;
1067
+ return { positions: [...positions, ...created], synthByPosition: byPosition };
423
1068
  }
424
- function _splitMaybeCompositeFillAcrossPositions(fill, positionCandidates) {
425
- if (positionCandidates.length === 1) {
426
- return [{ position: positionCandidates[0], maybeModifiedFill: fill }];
1069
+ function gapsToTarget(args) {
1070
+ const { assets, haveBySymbol, snapshot } = args;
1071
+ const gaps = [];
1072
+ for (const symbol of assets) {
1073
+ const gap = findGapToTarget({
1074
+ symbol,
1075
+ have: haveBySymbol(symbol),
1076
+ target: snapshot.signedSize(symbol)
1077
+ });
1078
+ if (gap) {
1079
+ gaps.push(gap);
1080
+ }
427
1081
  }
428
- const firstPosition = positionCandidates[0];
429
- const firstExposure = new BigNumber(firstPosition.exposure[fill.symbol] ?? 0);
430
- if (firstExposure.isZero() || firstExposure.isEqualTo("-0")) {
431
- throw new Error("Corrupted state: first position has no exposure");
1082
+ return gaps;
1083
+ }
1084
+ function findGapToTarget(args) {
1085
+ const { symbol, have, target } = args;
1086
+ if (isZero(target)) {
1087
+ if (isGreaterThan(abs(have), DUST)) {
1088
+ return {
1089
+ kind: "gone",
1090
+ symbol,
1091
+ side: have.isPositive() ? "SELL" : "BUY",
1092
+ quantity: have.abs().toFixed()
1093
+ };
1094
+ }
1095
+ return null;
432
1096
  }
433
- const signedFillQuantity = new BigNumber(fill.quantity).multipliedBy(fill.side === "BUY" ? 1 : -1);
434
- const firstSign = firstExposure.isGreaterThan(0) ? 1 : -1;
435
- const fillSign = signedFillQuantity.isGreaterThan(0) ? 1 : -1;
436
- if (firstSign === fillSign) {
437
- return [{ position: firstPosition, maybeModifiedFill: fill }];
1097
+ const residual = subtract(target, have);
1098
+ if (isGreaterThan(abs(residual), DUST)) {
1099
+ return {
1100
+ kind: "gap",
1101
+ symbol,
1102
+ side: residual.isPositive() ? "BUY" : "SELL",
1103
+ quantity: residual.abs().toFixed()
1104
+ };
438
1105
  }
439
- const totalExposure = positionCandidates.reduce((acc, p) => {
440
- acc = acc.plus(new BigNumber(p.exposure[fill.symbol] ?? 0));
441
- return acc;
442
- }, new BigNumber(0));
443
- if (totalExposure.isZero() || totalExposure.isEqualTo("-0")) {
444
- throw new Error("Corrupted state: total exposure is zero");
1106
+ return null;
1107
+ }
1108
+
1109
+ // src/sync/applicables/fills.ts
1110
+ function assignPartIds(positions) {
1111
+ const bySource = /* @__PURE__ */ new Map();
1112
+ for (const pos of positions) {
1113
+ for (const part of pos.parts) {
1114
+ const list = bySource.get(part.source.exchangeFillId) ?? [];
1115
+ list.push(part);
1116
+ bySource.set(part.source.exchangeFillId, list);
1117
+ }
445
1118
  }
446
- const willBalanceToZero = totalExposure.negated().isEqualTo(signedFillQuantity);
447
- if (!willBalanceToZero) {
448
- return [{ position: firstPosition, maybeModifiedFill: fill }];
1119
+ const resolution = /* @__PURE__ */ new Map();
1120
+ for (const [sourceId, parts] of bySource) {
1121
+ const single = parts.length === 1 ? parts[0] : void 0;
1122
+ if (single && isEqualTo(single.quantity, single.source.quantity)) {
1123
+ resolution.set(single, { exchangeFillId: sourceId });
1124
+ continue;
1125
+ }
1126
+ parts.forEach((part, i) => {
1127
+ resolution.set(part, {
1128
+ exchangeFillId: `${sourceId}-p${i}`,
1129
+ original: { exchangeFillId: sourceId, quantity: part.source.quantity }
1130
+ });
1131
+ });
449
1132
  }
450
- return positionCandidates.map((position) => {
451
- const signedQuantity = position.exposure[fill.symbol] ?? "0";
452
- const quantity = new BigNumber(signedQuantity).abs();
1133
+ return (part) => {
1134
+ const id = resolution.get(part);
1135
+ if (!id) {
1136
+ throw new Error("applicables: fill part was never assigned an id");
1137
+ }
1138
+ return id;
1139
+ };
1140
+ }
1141
+ function partToApplicableFill(part, id, connector) {
1142
+ const fill = part.source;
1143
+ const base = {
1144
+ exchangeFillId: id.exchangeFillId,
1145
+ connector,
1146
+ symbol: fill.symbol,
1147
+ side: fill.side,
1148
+ quantity: part.quantity,
1149
+ price: fill.price,
1150
+ usd: multiply(part.quantity, fill.price).toFixed(),
1151
+ timestamp: fill.timestamp
1152
+ };
1153
+ if (isMissedHistoryFill(fill)) {
453
1154
  return {
454
- position,
455
- maybeModifiedFill: {
456
- ...fill,
457
- exchangeFillId: `${fill.exchangeFillId}-${position.id}`,
458
- quantity: quantity.toFixed(),
459
- original: {
460
- exchangeFillId: fill.exchangeFillId,
461
- quantity: fill.quantity
1155
+ ...base,
1156
+ cloid: null,
1157
+ reduceOnly: false,
1158
+ synthetic: true,
1159
+ isExternal: true,
1160
+ tradeFee: "0",
1161
+ builderFee: "0",
1162
+ liquidation: null
1163
+ };
1164
+ }
1165
+ const fromPear = isPearCloid(connector, fill.cloid);
1166
+ return {
1167
+ ...base,
1168
+ cloid: fill.cloid ?? null,
1169
+ reduceOnly: fill.reduceOnly,
1170
+ synthetic: false,
1171
+ isExternal: !fromPear,
1172
+ tradeFee: feeShareForPart(fill.tradeFee, part.quantity, fill.quantity),
1173
+ builderFee: feeShareForPart(fill.builderFee, part.quantity, fill.quantity),
1174
+ liquidation: fill.liquidation,
1175
+ ...id.original ? { original: id.original } : {}
1176
+ };
1177
+ }
1178
+ function feeShareForPart(totalFee, partQty, sourceQty) {
1179
+ if (isEqualTo(partQty, sourceQty)) {
1180
+ return totalFee;
1181
+ }
1182
+ if (isZero(sourceQty)) {
1183
+ return "0";
1184
+ }
1185
+ const feeShare = divide(multiply(totalFee, partQty), sourceQty);
1186
+ return feeShare.toFixed();
1187
+ }
1188
+
1189
+ // src/sync/applicables/position.ts
1190
+ function assembleApplicables(args) {
1191
+ const { positions, connector, synthByPosition, now } = args;
1192
+ const partIdFor = assignPartIds(positions);
1193
+ const applicables = [];
1194
+ for (const pos of positions) {
1195
+ const applicable = assembleOneApplicable(pos, connector, partIdFor, synthByPosition.get(pos) ?? [], now);
1196
+ if (applicable) {
1197
+ applicables.push(applicable);
1198
+ }
1199
+ }
1200
+ return applicables;
1201
+ }
1202
+ function assembleOneApplicable(pos, connector, partIdFor, assignedSynthetics, now) {
1203
+ const realFills = pos.parts.map((part) => partToApplicableFill(part, partIdFor(part), connector)).sort((a, b) => a.timestamp - b.timestamp);
1204
+ const syntheticFills = assignedSynthetics.map(
1205
+ (synthetic) => buildSyntheticFill({
1206
+ positionKey: pos.positionKey,
1207
+ connector,
1208
+ timestamp: anchorSyntheticTimestamp(realFills, synthetic.symbol, now),
1209
+ ...synthetic
1210
+ })
1211
+ );
1212
+ const fills = [...realFills, ...syntheticFills].sort((a, b) => a.timestamp - b.timestamp);
1213
+ if (fills.length === 0) {
1214
+ return null;
1215
+ }
1216
+ const exposure = finalExposure(pos, assignedSynthetics);
1217
+ const closed = exposure.isAllZero();
1218
+ const status = closed ? "CLOSED" : "OPEN";
1219
+ const origin = mergeOrigin(pos.existingOrigin, fills);
1220
+ return {
1221
+ positionKey: pos.positionKey,
1222
+ exposure: exposure.toSigned(),
1223
+ origin,
1224
+ status,
1225
+ ...closed ? { closedReason: "NETTED" } : {},
1226
+ fills,
1227
+ metadata: buildMetadata(new Set(exposure.symbols()), realFills)
1228
+ };
1229
+ }
1230
+ function finalExposure(pos, synthetics) {
1231
+ const exposure = pos.exposure.clone();
1232
+ for (const synthetic of synthetics) {
1233
+ exposure.addSigned(synthetic.symbol, synthetic.quantity, synthetic.side);
1234
+ }
1235
+ return exposure;
1236
+ }
1237
+ function anchorSyntheticTimestamp(realFills, symbol, now) {
1238
+ const oldestForSymbol = realFills.find((fill) => fill.symbol === symbol)?.timestamp;
1239
+ if (oldestForSymbol !== void 0) {
1240
+ return Math.max(1, oldestForSymbol - 1);
1241
+ }
1242
+ const oldestRealTs = realFills[0]?.timestamp;
1243
+ return oldestRealTs !== void 0 ? Math.max(1, oldestRealTs - 1) : now;
1244
+ }
1245
+ function buildMetadata(symbols, realFills) {
1246
+ const initialExposureUSD = {};
1247
+ for (const symbol of symbols) {
1248
+ const legFills = realFills.flatMap(
1249
+ (fill) => fill.symbol === symbol && fill.usd != null ? [{ side: fill.side, usd: fill.usd }] : []
1250
+ );
1251
+ const opener = legFills[0];
1252
+ if (!opener) {
1253
+ continue;
1254
+ }
1255
+ const notional = legFills.filter((fill) => fill.side === opener.side).reduce((sum, fill) => add(sum, fill.usd), ZERO);
1256
+ const signedNotional = signed(opener.side === "BUY" ? notional : notional.negated());
1257
+ initialExposureUSD[symbol] = signedNotional;
1258
+ }
1259
+ return { initialExposureUSD };
1260
+ }
1261
+ function mergeOrigin(existing, fills) {
1262
+ let anyExternal = false;
1263
+ let anyPear = false;
1264
+ switch (existing) {
1265
+ case "EXTERNAL":
1266
+ anyExternal = true;
1267
+ break;
1268
+ case "PEAR":
1269
+ anyPear = true;
1270
+ break;
1271
+ case "PARTIAL":
1272
+ anyExternal = true;
1273
+ anyPear = true;
1274
+ break;
1275
+ }
1276
+ for (const fill of fills) {
1277
+ if (fill.isExternal) {
1278
+ anyExternal = true;
1279
+ } else {
1280
+ anyPear = true;
1281
+ }
1282
+ }
1283
+ if (anyExternal && anyPear) {
1284
+ return "PARTIAL";
1285
+ }
1286
+ return anyPear ? "PEAR" : "EXTERNAL";
1287
+ }
1288
+
1289
+ // src/sync/applicables/snapshot.ts
1290
+ var ExchangeSnapshot = class _ExchangeSnapshot {
1291
+ constructor(bySymbol) {
1292
+ this.bySymbol = bySymbol;
1293
+ }
1294
+ bySymbol;
1295
+ /** Index `positions` by symbol for O(1) lookup. */
1296
+ static from(positions) {
1297
+ return new _ExchangeSnapshot(new Map(positions.map((position) => [position.symbol, position])));
1298
+ }
1299
+ /** Raw exchange position for `symbol`, or `undefined` when the exchange holds nothing. */
1300
+ get(symbol) {
1301
+ return this.bySymbol.get(symbol);
1302
+ }
1303
+ /** Signed exchange exposure for `symbol` (0 when the exchange has no position). */
1304
+ signedSize(symbol) {
1305
+ const position = this.bySymbol.get(symbol);
1306
+ if (!position) {
1307
+ return ZERO;
1308
+ }
1309
+ const size = parse(position.size);
1310
+ switch (position.side) {
1311
+ case "BUY":
1312
+ return size;
1313
+ case "SELL":
1314
+ return size.negated();
1315
+ }
1316
+ }
1317
+ /** All symbols the exchange currently reports a position for. */
1318
+ symbols() {
1319
+ return [...this.bySymbol.keys()];
1320
+ }
1321
+ };
1322
+ var PearHoldings = class _PearHoldings {
1323
+ constructor(filtered) {
1324
+ this.filtered = filtered;
1325
+ }
1326
+ filtered;
1327
+ /** Filter `openPositions` to the given connector and wrap. */
1328
+ static from(args) {
1329
+ const { openPositions, connector } = args;
1330
+ return new _PearHoldings(openPositions.filter((position) => position.connector === connector));
1331
+ }
1332
+ /** The open Pear positions for this connector. */
1333
+ positions() {
1334
+ return this.filtered;
1335
+ }
1336
+ /** Sum existing open Pear exposure per symbol. */
1337
+ totalBySymbol() {
1338
+ const totals = /* @__PURE__ */ new Map();
1339
+ for (const position of this.filtered) {
1340
+ for (const [symbol, signedExposure] of Object.entries(position.exposure)) {
1341
+ if (signedExposure) {
1342
+ const newExposure = add(totals.get(symbol) ?? ZERO, signedExposure);
1343
+ totals.set(symbol, newExposure);
462
1344
  }
463
1345
  }
464
- };
1346
+ }
1347
+ return totals;
1348
+ }
1349
+ /**
1350
+ * Which open DB position holds each active asset — the fallback target for a
1351
+ * synthetic when no matched position already carries that asset. First open
1352
+ * position with a non-zero leg wins per symbol.
1353
+ */
1354
+ positionBySymbol() {
1355
+ const existingBySymbol = /* @__PURE__ */ new Map();
1356
+ for (const position of this.filtered) {
1357
+ for (const [symbol, signedQty] of Object.entries(position.exposure)) {
1358
+ const legIsZero = isZero(signedQty);
1359
+ const exists = existingBySymbol.has(symbol);
1360
+ if (!legIsZero && !exists) {
1361
+ existingBySymbol.set(symbol, position);
1362
+ }
1363
+ }
1364
+ }
1365
+ return existingBySymbol;
1366
+ }
1367
+ };
1368
+
1369
+ // src/sync/applicables/index.ts
1370
+ function buildApplicables(args) {
1371
+ const { tradeAccountId, connector, orphanFills, openPositions, entryBySymbol, exchangePositions } = args;
1372
+ const snapshot = ExchangeSnapshot.from(exchangePositions);
1373
+ const pear = PearHoldings.from({ openPositions, connector });
1374
+ const now = Date.now();
1375
+ const fills = addFillsForMissedHistory({ orphanFills, pear, connector, entryBySymbol, snapshot });
1376
+ const matched = matchPositions({ tradeAccountId, orphanFills: fills, pear });
1377
+ const { positions, synthByPosition } = alignPositionsWithExchange({
1378
+ positions: matched,
1379
+ pear,
1380
+ snapshot,
1381
+ tradeAccountId
465
1382
  });
1383
+ return assembleApplicables({ positions, connector, synthByPosition, now });
466
1384
  }
467
- function _walkBackwardsToFindLastZeroCrossing(group) {
468
- let netDelta = new BigNumber(0);
469
- for (const fill of group.fills) {
470
- if (fill.symbol !== group.symbol) {
471
- throw new Error("Corrupted state: fill symbol mismatch in grouped creatables");
472
- }
473
- const quantity = new BigNumber(fill.quantity);
474
- const sign = fill.side === "BUY" ? 1 : -1;
475
- const signedQuantity = quantity.multipliedBy(sign);
476
- netDelta = netDelta.plus(signedQuantity);
477
- }
478
- if (netDelta.isZero()) {
479
- return group.fills;
480
- }
481
- let walkDelta = netDelta;
482
- const indexedNewFills = group.fills.map((fill, index) => ({ fill, index }));
483
- let cutoff;
484
- for (let i = indexedNewFills.length - 1; i >= 0; i--) {
485
- const indexedFill = indexedNewFills[i];
486
- if (!indexedFill) continue;
487
- const { fill, index } = indexedFill;
488
- const previousExposure = group.exposure;
489
- if (previousExposure.plus(walkDelta).isZero() && cutoff === void 0) {
490
- cutoff = index;
491
- }
492
- const sign = fill.side === "BUY" ? 1 : -1;
493
- const quantity = new BigNumber(fill.quantity).multipliedBy(sign);
494
- walkDelta = walkDelta.minus(quantity);
495
- }
496
- const allowed = new Set(
497
- indexedNewFills.filter(({ index }) => cutoff === void 0 || index <= cutoff).map(({ fill }) => fill)
498
- );
499
- if (allowed.size !== group.fills.length) {
500
- return Array.from(allowed.values());
1385
+
1386
+ // src/sync/cutoff.ts
1387
+ function selectFirstSyncCutoff(args) {
1388
+ const { fills, exchangePositions, connector } = args;
1389
+ if (fills.length === 0) {
1390
+ return null;
501
1391
  }
502
- if (group.exposure.isNegative() && netDelta.isPositive()) {
503
- const newExposure = netDelta.plus(group.exposure);
504
- if (newExposure.isGreaterThan(0)) return [];
1392
+ const snapshot = ExchangeSnapshot.from(exchangePositions);
1393
+ const book = Exposure.empty();
1394
+ for (const symbol of snapshot.symbols()) {
1395
+ book.addDelta(symbol, snapshot.signedSize(symbol));
505
1396
  }
506
- if (group.exposure.isPositive() && netDelta.isNegative()) {
507
- const newExposure = netDelta.plus(group.exposure);
508
- if (newExposure.isLessThan(0)) return [];
1397
+ const fillSymbols = new Set(fills.map((fill) => fill.symbol));
1398
+ for (const symbol of snapshot.symbols()) {
1399
+ if (!snapshot.signedSize(symbol).isZero() && !fillSymbols.has(symbol)) {
1400
+ return null;
1401
+ }
509
1402
  }
510
- return Array.from(allowed.values());
1403
+ const pearIndex = connector === "hyperliquid" ? findFirstPearFillIndex(connector, fills) : null;
1404
+ for (let boundary = fills.length; boundary >= 0; boundary--) {
1405
+ const withinAnchor = pearIndex === null || boundary <= pearIndex;
1406
+ if (withinAnchor && book.isAllZero() && isStrictTimestampBoundary(fills, boundary)) {
1407
+ return boundary === 0 ? null : { keepFromIndex: boundary };
1408
+ }
1409
+ const preceding = fills[boundary - 1];
1410
+ if (preceding) {
1411
+ book.addDelta(preceding.symbol, signedBySide(preceding.quantity, preceding.side).negated());
1412
+ }
1413
+ }
1414
+ return null;
511
1415
  }
512
- function _seggregateExternalFills(provisionalFills, externalFills) {
1416
+ function findFirstPearFillIndex(connector, fills) {
1417
+ const index = fills.findIndex((fill) => isPearCloid(connector, fill.cloid));
1418
+ if (index === -1) {
1419
+ return null;
1420
+ }
1421
+ return index;
1422
+ }
1423
+ function isStrictTimestampBoundary(fills, boundary) {
1424
+ const before = fills[boundary - 1];
1425
+ const after = fills[boundary];
1426
+ if (!before || !after) {
1427
+ return true;
1428
+ }
1429
+ return before.timestamp < after.timestamp;
1430
+ }
1431
+
1432
+ // src/sync/compute.ts
1433
+ async function computeSyncPayload(args) {
1434
+ const {
1435
+ tradeAccountId,
1436
+ connector,
1437
+ openPositions,
1438
+ entryBySymbol,
1439
+ provisionalFills,
1440
+ confirmedExchangeFillIds,
1441
+ externalFills,
1442
+ exchangePositions,
1443
+ neverSynced
1444
+ } = args;
1445
+ const fillsToProcess = _prepareExternalFills(externalFills, confirmedExchangeFillIds);
1446
+ const {
1447
+ reconcilables,
1448
+ confirmables: confirmableFills,
1449
+ orphans
1450
+ } = _segregateExternalFills(provisionalFills, fillsToProcess);
1451
+ const confirmablesByPosition = groupConfirmablesByPosition(confirmableFills);
1452
+ const { keptOrphans, firstSyncTrim } = applyFirstSyncCutoff({
1453
+ neverSynced,
1454
+ fillsToProcess,
1455
+ orphans,
1456
+ exchangePositions,
1457
+ connector
1458
+ });
1459
+ const applicables = buildApplicables({
1460
+ tradeAccountId,
1461
+ connector,
1462
+ orphanFills: keptOrphans,
1463
+ openPositions,
1464
+ entryBySymbol,
1465
+ exchangePositions
1466
+ });
1467
+ const mustPostFirstSyncCheckpoint = neverSynced && fillsToProcess.length > 0;
1468
+ if (!mustPostFirstSyncCheckpoint && isEmptyCycle(reconcilables, confirmablesByPosition, applicables)) {
1469
+ return null;
1470
+ }
1471
+ const externalMaxTs = latestFillTimestamp(fillsToProcess);
1472
+ return {
1473
+ reconcilables: Object.keys(reconcilables).length > 0 ? reconcilables : null,
1474
+ confirmables: Object.keys(confirmablesByPosition).length > 0 ? confirmablesByPosition : null,
1475
+ applicables,
1476
+ exchangePositions,
1477
+ meta: {
1478
+ externalMaxTs
1479
+ },
1480
+ firstSyncTrim
1481
+ };
1482
+ }
1483
+ function _prepareExternalFills(externalFills, confirmedExchangeFillIdsInput) {
1484
+ const dedupedByExchangeFillId = /* @__PURE__ */ new Map();
1485
+ const confirmedExchangeFillIds = new Set(
1486
+ Array.from(confirmedExchangeFillIdsInput).map((exchangeFillId) => _normalizeExchangeFillId(exchangeFillId)).filter((exchangeFillId) => exchangeFillId !== void 0)
1487
+ );
1488
+ const candidates = externalFills.reduce((acc, fill) => {
1489
+ const normalizedExchangeFillId = _normalizeExchangeFillId(fill.exchangeFillId);
1490
+ if (!normalizedExchangeFillId) {
1491
+ return acc;
1492
+ }
1493
+ if (!_isValidExternalQuantity(fill.quantity)) {
1494
+ return acc;
1495
+ }
1496
+ if (!_isValidExternalTimestamp(fill.timestamp)) {
1497
+ return acc;
1498
+ }
1499
+ if (confirmedExchangeFillIds.has(normalizedExchangeFillId)) {
1500
+ return acc;
1501
+ }
1502
+ acc.push({ ...fill, exchangeFillId: normalizedExchangeFillId });
1503
+ return acc;
1504
+ }, []);
1505
+ for (const fill of candidates) {
1506
+ const existing = dedupedByExchangeFillId.get(fill.exchangeFillId);
1507
+ if (!existing || fill.timestamp < existing.timestamp) {
1508
+ dedupedByExchangeFillId.set(fill.exchangeFillId, fill);
1509
+ }
1510
+ }
1511
+ return Array.from(dedupedByExchangeFillId.values()).sort((a, b) => a.timestamp - b.timestamp);
1512
+ }
1513
+ function _segregateExternalFills(provisionalFills, externalFills) {
513
1514
  const provisionalFillsMap = /* @__PURE__ */ new Map();
514
1515
  for (const fill of provisionalFills) {
515
1516
  const cloid = _normalizeCloid(fill.cloid);
516
- if (!cloid) continue;
1517
+ if (!cloid) {
1518
+ continue;
1519
+ }
517
1520
  provisionalFillsMap.set(cloid, fill);
518
1521
  }
519
- const orphanCreatables = [];
520
- const cloidBuffer = /* @__PURE__ */ new Map();
1522
+ const orphans = [];
1523
+ const fillsByCloid = /* @__PURE__ */ new Map();
521
1524
  for (const fill of externalFills) {
522
1525
  const cloid = _normalizeCloid(fill.cloid);
523
1526
  if (!cloid) {
524
- orphanCreatables.push(fill);
1527
+ orphans.push(fill);
525
1528
  } else {
526
- cloidBuffer.set(cloid, [...cloidBuffer.get(cloid) || [], fill]);
1529
+ fillsByCloid.set(cloid, [...fillsByCloid.get(cloid) || [], fill]);
527
1530
  }
528
1531
  }
529
1532
  const reconcilables = {};
530
- const confirmables = [];
531
- for (const [cloid, fills] of cloidBuffer.entries()) {
1533
+ const confirmableFills = [];
1534
+ for (const [cloid, fills] of fillsByCloid.entries()) {
532
1535
  if (fills.length > 1) {
533
1536
  const provisional = provisionalFillsMap.get(cloid);
534
1537
  if (provisional) {
@@ -542,65 +1545,136 @@ function _seggregateExternalFills(provisionalFills, externalFills) {
542
1545
  ...rest.map((fill) => ({ ...fill, cloid }))
543
1546
  ];
544
1547
  } else {
545
- orphanCreatables.push(...fills);
1548
+ orphans.push(...fills);
546
1549
  }
547
1550
  } else {
548
1551
  const fill = fills[0];
549
1552
  const provisional = provisionalFillsMap.get(cloid);
550
1553
  if (provisional) {
551
- confirmables.push({
1554
+ confirmableFills.push({
552
1555
  ...fill,
553
1556
  type: "confirm",
554
1557
  cloid,
555
1558
  positionId: provisional.positionId
556
1559
  });
557
1560
  } else {
558
- orphanCreatables.push(fill);
1561
+ orphans.push(fill);
559
1562
  }
560
1563
  }
561
1564
  }
562
- return { reconcilables, confirmables, orphanCreatables };
1565
+ return { reconcilables, confirmables: confirmableFills, orphans };
563
1566
  }
564
1567
  function _normalizeCloid(cloid) {
565
1568
  const value = cloid?.trim();
566
- return value ? value : void 0;
1569
+ if (!value) {
1570
+ return void 0;
1571
+ }
1572
+ return value;
567
1573
  }
568
1574
  function _normalizeExchangeFillId(exchangeFillId) {
569
1575
  const value = exchangeFillId?.trim().toLowerCase();
570
- return value ? value : void 0;
1576
+ if (!value) {
1577
+ return void 0;
1578
+ }
1579
+ return value;
571
1580
  }
572
1581
  function _isValidExternalQuantity(quantity) {
573
- if (/e/i.test(quantity)) return false;
574
- const parsed = new BigNumber(quantity);
575
- if (parsed.isNaN() || !parsed.isFinite()) return false;
576
- return parsed.isGreaterThan(0);
1582
+ if (/e/i.test(quantity)) {
1583
+ return false;
1584
+ }
1585
+ if (!isFiniteDecimal(quantity)) {
1586
+ return false;
1587
+ }
1588
+ return isGreaterThan(quantity, ZERO);
577
1589
  }
578
1590
  function _isValidExternalTimestamp(timestamp) {
579
1591
  return Number.isFinite(timestamp) && timestamp >= 0;
580
1592
  }
1593
+ function groupConfirmablesByPosition(confirmableFills) {
1594
+ return confirmableFills.reduce((acc, fill) => {
1595
+ acc[fill.positionId] = [...acc[fill.positionId] || [], fill];
1596
+ return acc;
1597
+ }, {});
1598
+ }
1599
+ function latestFillTimestamp(fills) {
1600
+ const last = fills[fills.length - 1];
1601
+ if (!last) {
1602
+ return null;
1603
+ }
1604
+ return last.timestamp;
1605
+ }
1606
+ function applyFirstSyncCutoff(args) {
1607
+ const { neverSynced, fillsToProcess, orphans, exchangePositions, connector } = args;
1608
+ if (!neverSynced || fillsToProcess.length === 0) {
1609
+ return { keptOrphans: orphans, firstSyncTrim: null };
1610
+ }
1611
+ const cutoff = selectFirstSyncCutoff({
1612
+ fills: fillsToProcess,
1613
+ exchangePositions,
1614
+ connector
1615
+ });
1616
+ if (!cutoff) {
1617
+ return { keptOrphans: orphans, firstSyncTrim: null };
1618
+ }
1619
+ const keptIds = new Set(fillsToProcess.slice(cutoff.keepFromIndex).map((fill) => fill.exchangeFillId));
1620
+ const keptOrphans = [];
1621
+ const droppedOrphans = [];
1622
+ for (const orphan of orphans) {
1623
+ (keptIds.has(orphan.exchangeFillId) ? keptOrphans : droppedOrphans).push(orphan);
1624
+ }
1625
+ if (droppedOrphans.length === 0) {
1626
+ return { keptOrphans, firstSyncTrim: null };
1627
+ }
1628
+ const firstKeptFill = fillsToProcess[cutoff.keepFromIndex];
1629
+ return {
1630
+ keptOrphans,
1631
+ firstSyncTrim: {
1632
+ cutoffTs: firstKeptFill ? firstKeptFill.timestamp : null,
1633
+ trimmedCount: droppedOrphans.length,
1634
+ trimmedMaxTs: droppedOrphans.reduce((max, fill) => fill.timestamp > max ? fill.timestamp : max, 0)
1635
+ }
1636
+ };
1637
+ }
1638
+ function isEmptyCycle(reconcilables, confirmablesByPosition, applicables) {
1639
+ return Object.keys(reconcilables).length === 0 && Object.keys(confirmablesByPosition).length === 0 && applicables.length === 0;
1640
+ }
581
1641
 
582
1642
  // src/validate-leverage.ts
583
1643
  function validateLeverage(leverage, instrument) {
584
- if (leverage < 1 || leverage > 100) return false;
585
- if (leverage > instrument.leverage) return false;
1644
+ if (leverage < 1 || leverage > 100) {
1645
+ return false;
1646
+ }
1647
+ if (leverage > instrument.leverage) {
1648
+ return false;
1649
+ }
586
1650
  return true;
587
1651
  }
588
1652
  function validateQuantity(quantity, price, precision) {
589
- const q = new BigNumber7(quantity);
590
- if (!q.isFinite() || q.lte(0)) return false;
591
- const p = new BigNumber7(price);
592
- if (!p.isFinite() || p.lte(0)) return false;
593
- const step = new BigNumber7(precision.quantity.step);
594
- const scaled = q.div(step);
595
- if (!scaled.isInteger()) return false;
596
- const notional = contractsToBase(q, precision.quantity.contractSize).times(p);
1653
+ const parsedQuantity = new BigNumber9(quantity);
1654
+ if (!parsedQuantity.isFinite() || parsedQuantity.lte(0)) {
1655
+ return false;
1656
+ }
1657
+ const parsedPrice = new BigNumber9(price);
1658
+ if (!parsedPrice.isFinite() || parsedPrice.lte(0)) {
1659
+ return false;
1660
+ }
1661
+ const step = new BigNumber9(precision.quantity.step);
1662
+ const scaled = parsedQuantity.div(step);
1663
+ if (!scaled.isInteger()) {
1664
+ return false;
1665
+ }
1666
+ const notional = contractsToBase(parsedQuantity, precision.quantity.contractSize).times(parsedPrice);
597
1667
  if (precision.notional?.min !== void 0 && precision.notional.min !== null && precision.notional.min > 0) {
598
- if (notional.lt(new BigNumber7(precision.notional.min))) return false;
1668
+ if (notional.lt(new BigNumber9(precision.notional.min))) {
1669
+ return false;
1670
+ }
599
1671
  }
600
1672
  if (precision.notional?.max !== void 0 && precision.notional.max !== null && precision.notional.max > 0) {
601
- if (notional.gt(new BigNumber7(precision.notional.max))) return false;
1673
+ if (notional.gt(new BigNumber9(precision.notional.max))) {
1674
+ return false;
1675
+ }
602
1676
  }
603
1677
  return true;
604
1678
  }
605
1679
 
606
- export { baseToContracts, computeAssetEntryNotional, computeAssetGrossRealizedPnL, computeAssetRealizedPnLs, computeAssetUnrealizedPnL, computeAssetUnrealizedPnLs, computeBasketWeightedRatioV1, computeEntryPriceForAsset, computeRealizedPositionState, computeSyncPayload, computeUnrealizedPositionState, contractsToBase, countDecimals, exponentiate, precisePrice, preciseQuantity, validateLeverage, validateQuantity };
1680
+ export { BIPS, ZERO, abs, add, baseToContracts, compareValue, computeAssetEntryNotional, computeAssetGrossRealizedPnL, computeAssetRealizedPnLs, computeAssetUndeterminedSizes, computeAssetUnrealizedPnL, computeAssetUnrealizedPnLs, computeBasketWeightedRatioV1, computeEntryPriceForAsset, computeRealizedPnlBySymbol, computeRealizedPnlFromFills, computeRealizedPositionState, computeSyncPayload, computeUnrealizedPositionState, contractsToBase, countDecimals, divide, exponentiate, generateDeterministicPositionKey, isEqualTo, isFiniteDecimal, isGreaterThan, isGreaterThanOrEqual, isLessThan, isLessThanOrEqual, isNegative, isPositive, isZero, min, multiply, negate, parse, precisePrice, preciseQuantity, sideSign, sign, signed, signedBySide, subtract, toDecimalString, validateLeverage, validateQuantity };