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