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