🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@pear-protocol/utils

Package Overview
Dependencies
Maintainers
3
Versions
21
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@pear-protocol/utils - npm Package Compare versions

Comparing version
0.0.16
to
0.0.17
+17
dist/fills/realized.d.ts
import type { InstrumentId, Side } from '@pear-protocol/types';
import BigNumber from 'bignumber.js';
export type RealizedPnlFillInput = {
symbol: InstrumentId;
side: Side;
price: string;
quantity: string;
timestamp: string;
};
/**
* Realized trading PnL from fill prices per instrument (FIFO-style average entry), excluding fees and funding.
*/
export declare function computeRealizedPnlBySymbol(fills: RealizedPnlFillInput[]): Map<string, BigNumber>;
/**
* Realized trading PnL from fill prices (per instrument, FIFO-style average entry), excluding fees and funding.
*/
export declare function computeRealizedPnlFromFills(fills: RealizedPnlFillInput[]): BigNumber;
import type { HttpTypes } from '@pear-protocol/types';
import type { ComputeSyncPayloadArgs, ComputeSyncPayloadResult, GroupedCreatables, GroupedCreatablesMap, OrphanCreatableExternalFill } from './types';
export declare function computeSyncPayload(args: ComputeSyncPayloadArgs): Promise<ComputeSyncPayloadResult | null>;
export declare function _prepareExternalFills(externalFills: HttpTypes.UnknownExternalFill[], confirmedExchangeFillIdsInput: string[]): HttpTypes.UnknownExternalFill[];
export declare function _groupCreatablesBySymbol(orphanCreatables: OrphanCreatableExternalFill[], openPositions: HttpTypes.OpenPositionLite[]): GroupedCreatablesMap;
export declare function _splitMaybeCompositeFillAcrossPositions(fill: OrphanCreatableExternalFill, positionCandidates: [HttpTypes.OpenPositionLite, ...HttpTypes.OpenPositionLite[]]): Array<{
position: HttpTypes.OpenPositionLite;
maybeModifiedFill: OrphanCreatableExternalFill;
}>;
export declare function _walkBackwardsToFindLastZeroCrossing(group: GroupedCreatables): HttpTypes.CreatableExternalFill[];
export declare function _seggregateExternalFills(provisionalFills: HttpTypes.ProvisionalFill[], externalFills: HttpTypes.UnknownExternalFill[]): {
reconcilables: HttpTypes.ReconcileFillsRequest;
confirmables: HttpTypes.ConfirmableExternalFill[];
orphanCreatables: OrphanCreatableExternalFill[];
};
export declare function _normalizeCloid(cloid?: string | null): string | undefined;
export declare function _normalizeExchangeFillId(exchangeFillId?: string | null): string | undefined;
export declare function _isValidExternalQuantity(quantity: string): boolean;
export declare function _isValidExternalTimestamp(timestamp: number): boolean;
export { computeSyncPayload } from './compute';
export type { ComputeSyncPayloadArgs, ComputeSyncPayloadResult, } from './types';
import type { HttpTypes } from '@pear-protocol/types';
import type BigNumber from 'bignumber.js';
export type OrphanCreatableExternalFill = Omit<HttpTypes.CreatableExternalFill, 'positionId' | 'type'>;
export type CompositeGroupKey = `${string}-${string}`;
export type GroupedCreatables = {
positionId: string;
symbol: string;
exposure: BigNumber;
fills: [HttpTypes.CreatableExternalFill, ...HttpTypes.CreatableExternalFill[]];
};
export type GroupedCreatablesMap = Map<CompositeGroupKey, GroupedCreatables>;
export type ComputeSyncPayloadArgs = {
openPositions: HttpTypes.OpenPositionLite[];
provisionalFills: HttpTypes.ProvisionalFill[];
confirmedExchangeFillIds: string[];
externalFills: HttpTypes.UnknownExternalFill[];
};
export type ComputeSyncPayloadResult = {
reconcilables: HttpTypes.ReconcileFillsRequest | null;
confirmables: HttpTypes.ConfirmFillsRequest | null;
creatables: HttpTypes.CreateFillsRequest | null;
meta: {
externalMinTs: number | null;
externalMaxTs: number | null;
processedCount: number;
};
};
+10
-2
import type { InstrumentId } from '@pear-protocol/types';
import BigNumber from 'bignumber.js';
export type AssetPnL = {
export type AssetUnrealizedPnL = {
id: InstrumentId;

@@ -8,3 +8,11 @@ signedQuantity: BigNumber;

currentPrice: BigNumber;
pnl: BigNumber;
upnl: BigNumber;
};
export type AssetRealizedPnL = {
id: InstrumentId;
grossPnl: BigNumber;
netPnl: BigNumber;
totalFees: BigNumber;
tradeFees: BigNumber;
pearFees: BigNumber;
};
import type { InstrumentId } from '@pear-protocol/types';
import BigNumber from 'bignumber.js';
import type { Fill } from '../fills/fill.types';
import type { AssetPnL } from './asset.types';
import type { AssetRealizedPnL, AssetUnrealizedPnL } from './asset.types';
type ExposureMap = Record<InstrumentId, string>;

@@ -18,4 +18,11 @@ type PriceMap = Record<InstrumentId, string>;

*/
export declare function computeAssetUPnL(signedQuantity: BigNumber, currentPrice: BigNumber, fills: Fill[]): BigNumber;
export declare function computeAssetUnrealizedPnL(signedQuantity: BigNumber, currentPrice: BigNumber, fills: Fill[]): BigNumber;
/**
* Compute realized gross PnL for a SINGLE asset.
*
* Realized trading PnL is accumulated when a fill reduces an existing
* position (using FIFO-style average entry), excluding fees.
*/
export declare function computeAssetGrossRealizedPnL(fills: Fill[]): BigNumber;
/**
* Compute unrealized PnL for an entire position.

@@ -25,3 +32,9 @@ *

*/
export declare function computeAssetPnLs(exposureMap: ExposureMap, priceMap: PriceMap, fills: Fill[]): AssetPnL[];
export declare function computeAssetUnrealizedPnLs(exposureMap: ExposureMap, priceMap: PriceMap, fills: Fill[]): AssetUnrealizedPnL[];
/**
* Compute realized PnL for an entire position.
*
* Formula: Σ(realizedPnL)
*/
export declare function computeAssetRealizedPnLs(fills: Fill[]): AssetRealizedPnL[];
export {};
+1
-1

@@ -1,2 +0,2 @@

export type { AssetPnL } from './asset.types';
export type { AssetRealizedPnL, AssetUnrealizedPnL } from './asset.types';
export * from './compute';

@@ -10,2 +10,4 @@ import type { InstrumentId, Side } from '@pear-protocol/types';

reduceOnly: boolean;
tradeFee?: string | null;
pearFee?: string | null;
};

@@ -1,10 +0,11 @@

export * from './asset';
export * from './basket';
export * from './asset/index';
export * from './basket/index';
export * from './contract-size';
export * from './fills';
export { countDecimals, exponentiate } from './math';
export * from './position';
export * from './fills/index';
export { countDecimals, exponentiate } from './math/index';
export * from './position/index';
export * from './precise-price';
export * from './precise-quantity';
export * from './sync/index';
export * from './validate-leverage';
export * from './validate-quantity';

@@ -1,10 +0,617 @@

export * from './asset';
export * from './basket';
export * from './contract-size';
export * from './fills';
export { countDecimals, exponentiate } from './math';
export * from './position';
export * from './precise-price';
export * from './precise-quantity';
export * from './validate-leverage';
export * from './validate-quantity';
import BigNumber7, { BigNumber } from 'bignumber.js';
// src/asset/compute.ts
var ZERO = new BigNumber7(0);
var BIPS = new BigNumber7(1e4);
function exponentiate(value) {
const result = Math.exp(value);
return new BigNumber7(result);
}
function countDecimals(value) {
const parts = value.split(".");
return parts[1]?.length ?? 0;
}
// src/fills/compute.ts
function computeEntryPriceForAsset(fills) {
let entryPrice = ZERO;
let positionSize = ZERO;
for (const fill of fills) {
const price = new BigNumber7(fill.price);
const quantity = new BigNumber7(fill.quantity);
const signed = fill.side === "BUY" ? quantity : quantity.negated();
const newPositionSize = positionSize.plus(signed);
const isSameDirection = !positionSize.isZero() && positionSize.isPositive() === signed.isPositive();
const isFlip = !positionSize.isZero() && !newPositionSize.isZero() && newPositionSize.isPositive() !== positionSize.isPositive();
if (positionSize.isZero() || isFlip) {
entryPrice = price;
} else if (isSameDirection) {
entryPrice = entryPrice.times(positionSize.abs()).plus(price.times(quantity)).div(newPositionSize.abs());
}
positionSize = newPositionSize;
}
return entryPrice;
}
// src/asset/compute.ts
function computeAssetEntryNotional(signedQuantity, fills) {
if (signedQuantity.isZero()) return ZERO;
const entryPrice = computeEntryPriceForAsset(fills);
if (entryPrice.isZero()) return ZERO;
const unsignedQuantity = signedQuantity.abs();
return unsignedQuantity.multipliedBy(entryPrice);
}
function computeAssetUnrealizedPnL(signedQuantity, currentPrice, fills) {
if (signedQuantity.isZero()) return ZERO;
const entryPrice = computeEntryPriceForAsset(fills);
if (entryPrice.isZero()) return ZERO;
const deltaPrice = currentPrice.minus(entryPrice);
return signedQuantity.multipliedBy(deltaPrice);
}
function computeAssetGrossRealizedPnL(fills) {
let entryPrice = ZERO;
let positionSize = ZERO;
let gross = ZERO;
for (const fill of fills) {
const price = new BigNumber7(fill.price);
const quantity = new BigNumber7(fill.quantity);
const signed = fill.side === "BUY" ? quantity : quantity.negated();
const newPositionSize = positionSize.plus(signed);
if (!positionSize.isZero() && positionSize.isPositive() !== signed.isPositive()) {
const closedQty = BigNumber7.min(positionSize.abs(), signed.abs());
if (positionSize.isPositive()) {
gross = gross.plus(closedQty.multipliedBy(price.minus(entryPrice)));
} else {
gross = gross.plus(closedQty.multipliedBy(entryPrice.minus(price)));
}
}
const isSameDirection = !positionSize.isZero() && positionSize.isPositive() === signed.isPositive();
const isFlip = !positionSize.isZero() && !newPositionSize.isZero() && newPositionSize.isPositive() !== positionSize.isPositive();
if (positionSize.isZero() || isFlip) {
entryPrice = price;
} else if (isSameDirection) {
entryPrice = entryPrice.times(positionSize.abs()).plus(price.times(quantity)).div(newPositionSize.abs());
}
positionSize = newPositionSize;
}
return gross;
}
function computeAssetUnrealizedPnLs(exposureMap, priceMap, fills) {
const result = [];
for (const [id, signedQtyString] of Object.entries(exposureMap)) {
const priceString = priceMap[id];
if (!priceString) continue;
const signedQuantity = new BigNumber7(signedQtyString);
if (signedQuantity.isZero()) continue;
const currentPrice = new BigNumber7(priceString);
const assetFills = fills.filter((f) => f.symbol === id);
const entryPrice = computeEntryPriceForAsset(assetFills);
const upnl = signedQuantity.multipliedBy(currentPrice.minus(entryPrice));
result.push({
id,
signedQuantity,
entryPrice,
currentPrice,
upnl
});
}
return result;
}
function computeAssetRealizedPnLs(fills) {
const bySymbol = /* @__PURE__ */ new Map();
for (const f of fills) {
const list = bySymbol.get(f.symbol) ?? [];
list.push(f);
bySymbol.set(f.symbol, list);
}
const result = [];
for (const [id, assetFills] of bySymbol) {
const tradeFees = assetFills.reduce((acc, f) => acc.plus(new BigNumber7(f.tradeFee ?? "0")), ZERO);
const pearFees = assetFills.reduce((acc, f) => acc.plus(new BigNumber7(f.pearFee ?? "0")), ZERO);
const totalFees = tradeFees.plus(pearFees);
const grossPnl = computeAssetGrossRealizedPnL(assetFills);
const netPnl = grossPnl.minus(totalFees);
result.push({
id,
grossPnl,
netPnl,
totalFees,
tradeFees,
pearFees
});
}
return result;
}
function computeBasketWeightedRatioV1(basket, priceMap) {
let logSum = 0;
for (const leg of basket) {
const price = priceMap[leg.symbol];
if (!price) return null;
const priceNum = Number(price);
if (priceNum <= 0) return null;
const sign = leg.side === "BUY" ? 1 : -1;
logSum += sign * leg.weight * Math.log(priceNum);
}
return exponentiate(logSum);
}
function computeBasketIndexV1(basket, priceMap) {
let logSum = 0;
for (const leg of basket) {
const price = priceMap[leg.symbol];
if (!price) return null;
const priceNum = Number(price);
if (priceNum <= 0) return null;
logSum += leg.weight * Math.log(priceNum);
}
return exponentiate(logSum);
}
function contractsToBase(contracts, contractSize) {
return new BigNumber7(contracts).times(contractSize);
}
function baseToContracts(base, contractSize) {
return new BigNumber7(base).div(contractSize);
}
function computePositionMarkNotional(assetUPnLs) {
return assetUPnLs.reduce((acc, asset) => {
if (asset.signedQuantity.isZero()) return acc;
return acc.plus(asset.signedQuantity.abs().multipliedBy(asset.currentPrice));
}, ZERO);
}
function computePositionEntryNotional(assetUPnLs) {
return assetUPnLs.reduce((acc, asset) => {
if (asset.signedQuantity.isZero()) return acc;
return acc.plus(asset.signedQuantity.abs().multipliedBy(asset.entryPrice));
}, ZERO);
}
function computePositionUnrealizedPnL(assetUPnLs) {
return assetUPnLs.reduce((acc, asset) => acc.plus(asset.upnl), ZERO);
}
function computePositionGrossRealizedPnL(assetPnLs) {
return assetPnLs.reduce((acc, asset) => acc.plus(asset.grossPnl), ZERO);
}
function computePositionNetRealizedPnL(assetPnLs) {
return assetPnLs.reduce((acc, asset) => acc.plus(asset.netPnl), ZERO);
}
function computePositionUnrealizedPnLBips(upnl, entryNotional) {
if (upnl.isZero() || entryNotional.isZero()) return ZERO;
return upnl.div(entryNotional).multipliedBy(BIPS);
}
function computePositionEntryPrice(assetUPnLs) {
const { totalWeightedPrice, totalWeight } = assetUPnLs.reduce(
(acc, { signedQuantity, entryPrice }) => {
const absQuantity = signedQuantity.abs();
return {
totalWeightedPrice: acc.totalWeightedPrice.plus(absQuantity.multipliedBy(entryPrice)),
totalWeight: acc.totalWeight.plus(absQuantity)
};
},
{
totalWeightedPrice: ZERO,
totalWeight: ZERO
}
);
if (totalWeight.isZero()) {
return null;
}
return totalWeightedPrice.dividedBy(totalWeight);
}
function computePositionCurrentPrice(assetUPnLs) {
const { totalWeightedPrice, totalWeight } = assetUPnLs.reduce(
(acc, { signedQuantity, currentPrice }) => {
const absQuantity = signedQuantity.abs();
return {
totalWeightedPrice: acc.totalWeightedPrice.plus(absQuantity.multipliedBy(currentPrice)),
totalWeight: acc.totalWeight.plus(absQuantity)
};
},
{
totalWeightedPrice: ZERO,
totalWeight: ZERO
}
);
if (totalWeight.isZero()) {
return null;
}
return totalWeightedPrice.dividedBy(totalWeight);
}
function computeUnrealizedPositionState(exposureMap, priceMap, fills) {
const assetUPnLs = computeAssetUnrealizedPnLs(exposureMap, priceMap, fills);
const entryPrice = computePositionEntryPrice(assetUPnLs);
const entryNotional = computePositionEntryNotional(assetUPnLs);
const markPrice = computePositionCurrentPrice(assetUPnLs);
const markNotional = computePositionMarkNotional(assetUPnLs);
const upnl = computePositionUnrealizedPnL(assetUPnLs);
const upnlBips = computePositionUnrealizedPnLBips(upnl, entryNotional);
return {
assetUPnLs,
entryPrice,
entryNotional,
markPrice,
markNotional,
upnl,
upnlBips
};
}
function computeRealizedPositionState(fills) {
const assetPnLs = computeAssetRealizedPnLs(fills);
const grossPnl = computePositionGrossRealizedPnL(assetPnLs);
const netPnl = computePositionNetRealizedPnL(assetPnLs);
const tradeFees = assetPnLs.reduce((acc, asset) => acc.plus(asset.tradeFees), ZERO);
const pearFees = assetPnLs.reduce((acc, asset) => acc.plus(asset.pearFees), ZERO);
const totalFees = tradeFees.plus(pearFees);
return {
grossPnl,
netPnl,
totalFees,
tradeFees,
pearFees
};
}
BigNumber7.config({
DECIMAL_PLACES: 30,
ROUNDING_MODE: BigNumber7.ROUND_DOWN
});
function precisePrice(connector, raw, precision, side = "BUY", slippageToleranceBps = 0) {
const normalized = normalizePrice(raw);
const effectiveRaw = withSlippageTolerance(normalized, slippageToleranceBps, side);
switch (connector) {
case "hyperliquid":
return preciseHyperliquidPrice(effectiveRaw, precision, side);
case "binance":
case "bybit":
case "okx":
case "lighter":
return preciseTickGridPrice(effectiveRaw, precision, side);
}
}
function withSlippageTolerance(normalized, slippageToleranceBps, side) {
if (slippageToleranceBps === 0) {
return normalized;
}
const bps = new BigNumber7(slippageToleranceBps);
const factor = bps.div(1e4);
return side === "BUY" ? normalized.times(new BigNumber7(1).plus(factor)) : normalized.times(new BigNumber7(1).minus(factor));
}
function preciseHyperliquidPrice(price, precision, side) {
const maxDecimals = precision.price.decimals;
const maxSigFigs = precision.price.sigFigs ?? 5;
const rounding = side === "BUY" ? BigNumber7.ROUND_FLOOR : BigNumber7.ROUND_CEIL;
price = price.decimalPlaces(maxDecimals, rounding);
price = clampSigFigs(price, maxSigFigs, maxDecimals, rounding);
return price.toFixed();
}
function preciseTickGridPrice(price, precision, side) {
const tick = precision.price.tick ?? 1e-4;
const rounding = side === "BUY" ? BigNumber7.ROUND_FLOOR : BigNumber7.ROUND_CEIL;
const tickBN = new BigNumber7(tick);
const scaled = price.div(tickBN);
const snapped = scaled.integerValue(rounding).times(tickBN);
const maxDecimals = precision.price.decimals;
const final = snapped.decimalPlaces(maxDecimals, rounding);
return final.toFixed();
}
function clampSigFigs(price, maxSigFigs, maxDecimals, rounding) {
const countSigFigs = (x) => x.toFixed().replace(".", "").replace(/^0+/, "").length;
if (countSigFigs(price) <= maxSigFigs) {
return price;
}
for (let d = maxDecimals; d >= 0; d--) {
const candidate = price.decimalPlaces(d, rounding);
if (countSigFigs(candidate) <= maxSigFigs) {
return candidate;
}
}
return price;
}
function normalizePrice(value) {
const bn = new BigNumber7(value);
return new BigNumber7(bn.toFixed());
}
BigNumber7.config({
DECIMAL_PLACES: 30,
ROUNDING_MODE: BigNumber7.ROUND_DOWN
});
function preciseQuantity(connector, raw, precision, side) {
switch (connector) {
case "hyperliquid":
return preciseHyperliquidQuantity(raw, precision, side);
case "binance":
case "bybit":
case "okx":
case "lighter":
return preciseStepGridQuantity(raw, precision, side);
}
}
function preciseHyperliquidQuantity(raw, precision, side) {
const decimals = precision.quantity.decimals;
const qty = new BigNumber7(raw);
const rounded = qty.decimalPlaces(decimals, side === "BUY" ? BigNumber7.ROUND_FLOOR : BigNumber7.ROUND_CEIL);
return rounded.toFixed();
}
function preciseStepGridQuantity(raw, precision, side) {
const step = precision.quantity.step;
const qty = baseToContracts(raw, precision.quantity.contractSize);
const stepBN = new BigNumber7(step);
const scaled = qty.div(stepBN);
const rounded = side === "BUY" ? scaled.integerValue(BigNumber7.ROUND_FLOOR) : scaled.integerValue(BigNumber7.ROUND_CEIL);
const result = rounded.times(stepBN);
const maxDecimals = precision.quantity.decimals;
const final = result.decimalPlaces(maxDecimals, side === "BUY" ? BigNumber7.ROUND_FLOOR : BigNumber7.ROUND_CEIL);
return final.toFixed();
}
async function computeSyncPayload(args) {
const { openPositions, provisionalFills, confirmedExchangeFillIds, externalFills } = args;
const fillsToProcess = _prepareExternalFills(externalFills, confirmedExchangeFillIds);
if (fillsToProcess.length === 0) {
return null;
}
const { reconcilables, confirmables, orphanCreatables } = _seggregateExternalFills(provisionalFills, fillsToProcess);
const groupedConfirmables = confirmables.reduce((acc, fill) => {
acc[fill.positionId] = [...acc[fill.positionId] || [], fill];
return acc;
}, {});
const groupedCreatablesBySymbol = _groupCreatablesBySymbol(orphanCreatables, openPositions);
const groupedCreatables = {};
for (const group of groupedCreatablesBySymbol.values()) {
const zeroCrossing = _walkBackwardsToFindLastZeroCrossing(group);
for (const fill of zeroCrossing) {
const existing = groupedCreatables[fill.positionId];
if (existing) {
groupedCreatables[fill.positionId] = [...existing, fill];
} else {
groupedCreatables[fill.positionId] = [fill];
}
}
}
for (const fills of Object.values(groupedCreatables)) {
fills.sort((a, b) => a.timestamp - b.timestamp);
}
return {
reconcilables: Object.keys(reconcilables).length > 0 ? reconcilables : null,
confirmables: Object.keys(groupedConfirmables).length > 0 ? groupedConfirmables : null,
creatables: Object.keys(groupedCreatables).length > 0 ? groupedCreatables : null,
meta: {
externalMinTs: fillsToProcess[0]?.timestamp ?? null,
externalMaxTs: fillsToProcess[fillsToProcess.length - 1]?.timestamp ?? null,
processedCount: fillsToProcess.length
}
};
}
function _prepareExternalFills(externalFills, confirmedExchangeFillIdsInput) {
const dedupedByExchangeFillId = /* @__PURE__ */ new Map();
const confirmedExchangeFillIds = new Set(
Array.from(confirmedExchangeFillIdsInput).map((exchangeFillId) => _normalizeExchangeFillId(exchangeFillId)).filter((exchangeFillId) => exchangeFillId !== void 0)
);
const candidates = externalFills.reduce((acc, fill) => {
const normalizedExchangeFillId = _normalizeExchangeFillId(fill.exchangeFillId);
if (!normalizedExchangeFillId) return acc;
if (!_isValidExternalQuantity(fill.quantity)) return acc;
if (!_isValidExternalTimestamp(fill.timestamp)) return acc;
if (confirmedExchangeFillIds.has(normalizedExchangeFillId)) return acc;
acc.push({ ...fill, exchangeFillId: normalizedExchangeFillId });
return acc;
}, []);
for (const fill of candidates) {
const existing = dedupedByExchangeFillId.get(fill.exchangeFillId);
if (!existing || fill.timestamp < existing.timestamp) {
dedupedByExchangeFillId.set(fill.exchangeFillId, fill);
}
}
return Array.from(dedupedByExchangeFillId.values()).sort((a, b) => a.timestamp - b.timestamp);
}
function _groupCreatablesBySymbol(orphanCreatables, openPositions) {
const groupedCreatables = /* @__PURE__ */ new Map();
for (const fill of orphanCreatables) {
const positionCandidates = openPositions.reduce((acc, p) => {
const exposure = p.exposure[fill.symbol];
if (!exposure) return acc;
const quantity = new BigNumber(exposure);
if (quantity.isZero() || quantity.isEqualTo("-0")) return acc;
if (fill.timestamp < Date.parse(p.createdAt)) return acc;
acc.push(p);
return acc;
}, []);
const first = positionCandidates[0];
if (!first) continue;
const splits = _splitMaybeCompositeFillAcrossPositions(fill, [first, ...positionCandidates.slice(1)]);
for (const { position, maybeModifiedFill } of splits) {
const key = `${position.id}-${maybeModifiedFill.symbol}`;
const existing = groupedCreatables.get(key);
if (existing) {
existing.fills.push({ ...maybeModifiedFill, type: "create", positionId: position.id });
groupedCreatables.set(key, existing);
} else {
groupedCreatables.set(key, {
positionId: position.id,
symbol: maybeModifiedFill.symbol,
exposure: new BigNumber(position.exposure[maybeModifiedFill.symbol] ?? 0),
fills: [{ ...maybeModifiedFill, type: "create", positionId: position.id }]
});
}
}
}
return groupedCreatables;
}
function _splitMaybeCompositeFillAcrossPositions(fill, positionCandidates) {
if (positionCandidates.length === 1) {
return [{ position: positionCandidates[0], maybeModifiedFill: fill }];
}
const firstPosition = positionCandidates[0];
const firstExposure = new BigNumber(firstPosition.exposure[fill.symbol] ?? 0);
if (firstExposure.isZero() || firstExposure.isEqualTo("-0")) {
throw new Error("Corrupted state: first position has no exposure");
}
const signedFillQuantity = new BigNumber(fill.quantity).multipliedBy(fill.side === "BUY" ? 1 : -1);
const firstSign = firstExposure.isGreaterThan(0) ? 1 : -1;
const fillSign = signedFillQuantity.isGreaterThan(0) ? 1 : -1;
if (firstSign === fillSign) {
return [{ position: firstPosition, maybeModifiedFill: fill }];
}
const totalExposure = positionCandidates.reduce((acc, p) => {
acc = acc.plus(new BigNumber(p.exposure[fill.symbol] ?? 0));
return acc;
}, new BigNumber(0));
if (totalExposure.isZero() || totalExposure.isEqualTo("-0")) {
throw new Error("Corrupted state: total exposure is zero");
}
const willBalanceToZero = totalExposure.negated().isEqualTo(signedFillQuantity);
if (!willBalanceToZero) {
return [{ position: firstPosition, maybeModifiedFill: fill }];
}
return positionCandidates.map((position) => {
const signedQuantity = position.exposure[fill.symbol] ?? "0";
const quantity = new BigNumber(signedQuantity).abs();
return {
position,
maybeModifiedFill: {
...fill,
exchangeFillId: `${fill.exchangeFillId}-${position.id}`,
quantity: quantity.toFixed(),
original: {
exchangeFillId: fill.exchangeFillId,
quantity: fill.quantity
}
}
};
});
}
function _walkBackwardsToFindLastZeroCrossing(group) {
let netDelta = new BigNumber(0);
for (const fill of group.fills) {
if (fill.symbol !== group.symbol) {
throw new Error("Corrupted state: fill symbol mismatch in grouped creatables");
}
const quantity = new BigNumber(fill.quantity);
const sign = fill.side === "BUY" ? 1 : -1;
const signedQuantity = quantity.multipliedBy(sign);
netDelta = netDelta.plus(signedQuantity);
}
if (netDelta.isZero()) {
return group.fills;
}
let walkDelta = netDelta;
const indexedNewFills = group.fills.map((fill, index) => ({ fill, index }));
let cutoff;
for (let i = indexedNewFills.length - 1; i >= 0; i--) {
const indexedFill = indexedNewFills[i];
if (!indexedFill) continue;
const { fill, index } = indexedFill;
const previousExposure = group.exposure;
if (previousExposure.plus(walkDelta).isZero() && cutoff === void 0) {
cutoff = index;
}
const sign = fill.side === "BUY" ? 1 : -1;
const quantity = new BigNumber(fill.quantity).multipliedBy(sign);
walkDelta = walkDelta.minus(quantity);
}
const allowed = new Set(
indexedNewFills.filter(({ index }) => cutoff === void 0 || index <= cutoff).map(({ fill }) => fill)
);
if (allowed.size !== group.fills.length) {
return Array.from(allowed.values());
}
if (group.exposure.isNegative() && netDelta.isPositive()) {
const newExposure = netDelta.plus(group.exposure);
if (newExposure.isGreaterThan(0)) return [];
}
if (group.exposure.isPositive() && netDelta.isNegative()) {
const newExposure = netDelta.plus(group.exposure);
if (newExposure.isLessThan(0)) return [];
}
return Array.from(allowed.values());
}
function _seggregateExternalFills(provisionalFills, externalFills) {
const provisionalFillsMap = /* @__PURE__ */ new Map();
for (const fill of provisionalFills) {
const cloid = _normalizeCloid(fill.cloid);
if (!cloid) continue;
provisionalFillsMap.set(cloid, fill);
}
const orphanCreatables = [];
const cloidBuffer = /* @__PURE__ */ new Map();
for (const fill of externalFills) {
const cloid = _normalizeCloid(fill.cloid);
if (!cloid) {
orphanCreatables.push(fill);
} else {
cloidBuffer.set(cloid, [...cloidBuffer.get(cloid) || [], fill]);
}
}
const reconcilables = {};
const confirmables = [];
for (const [cloid, fills] of cloidBuffer.entries()) {
if (fills.length > 1) {
const provisional = provisionalFillsMap.get(cloid);
if (provisional) {
const [first, second, ...rest] = fills;
if (!first || !second) {
throw new Error("Corrupted state: sibling group must contain at least 2 fills for reconcile");
}
reconcilables[cloid] = [
{ ...first, cloid },
{ ...second, cloid },
...rest.map((fill) => ({ ...fill, cloid }))
];
} else {
orphanCreatables.push(...fills);
}
} else {
const fill = fills[0];
const provisional = provisionalFillsMap.get(cloid);
if (provisional) {
confirmables.push({
...fill,
type: "confirm",
cloid,
positionId: provisional.positionId
});
} else {
orphanCreatables.push(fill);
}
}
}
return { reconcilables, confirmables, orphanCreatables };
}
function _normalizeCloid(cloid) {
const value = cloid?.trim();
return value ? value : void 0;
}
function _normalizeExchangeFillId(exchangeFillId) {
const value = exchangeFillId?.trim().toLowerCase();
return value ? value : void 0;
}
function _isValidExternalQuantity(quantity) {
if (/e/i.test(quantity)) return false;
const parsed = new BigNumber(quantity);
if (parsed.isNaN() || !parsed.isFinite()) return false;
return parsed.isGreaterThan(0);
}
function _isValidExternalTimestamp(timestamp) {
return Number.isFinite(timestamp) && timestamp >= 0;
}
// src/validate-leverage.ts
function validateLeverage(leverage, instrument) {
if (leverage < 1 || leverage > 100) return false;
if (leverage > instrument.leverage) return false;
return true;
}
function validateQuantity(quantity, price, precision) {
const q = new BigNumber7(quantity);
if (!q.isFinite() || q.lte(0)) return false;
const p = new BigNumber7(price);
if (!p.isFinite() || p.lte(0)) return false;
const step = new BigNumber7(precision.quantity.step);
const scaled = q.div(step);
if (!scaled.isInteger()) return false;
const notional = contractsToBase(q, precision.quantity.contractSize).times(p);
if (precision.notional?.min !== void 0 && precision.notional.min !== null && precision.notional.min > 0) {
if (notional.lt(new BigNumber7(precision.notional.min))) return false;
}
if (precision.notional?.max !== void 0 && precision.notional.max !== null && precision.notional.max > 0) {
if (notional.gt(new BigNumber7(precision.notional.max))) return false;
}
return true;
}
export { baseToContracts, computeAssetEntryNotional, computeAssetGrossRealizedPnL, computeAssetRealizedPnLs, computeAssetUnrealizedPnL, computeAssetUnrealizedPnLs, computeBasketIndexV1, computeBasketWeightedRatioV1, computeEntryPriceForAsset, computeRealizedPositionState, computeSyncPayload, computeUnrealizedPositionState, contractsToBase, countDecimals, exponentiate, precisePrice, preciseQuantity, validateLeverage, validateQuantity };
import type { InstrumentId } from '@pear-protocol/types';
import BigNumber from 'bignumber.js';
import type { AssetPnL } from '../asset/asset.types';
import type { Fill } from '../fills/fill.types';
import type { PositionPnL } from './position.types';
import type { RealizedPositionState, UnrealizedPositionState } from './position.types';
type ExposureMap = Record<InstrumentId, string>;
type PriceMap = Record<InstrumentId, string>;
/**
* Current position notional (mark value).
* Compute the unrealized position state for OPEN positions.
*
* Formula: Σ(|quantity| × currentPrice)
* @param exposureMap - The exposure map of the position.
* @param priceMap - The price map of the position.
* @param fills - The fills of the position.
* @returns The unrealized position state.
*/
export declare function computePositionMarkNotional(assetPnLs: AssetPnL[]): BigNumber;
export declare function computeUnrealizedPositionState(exposureMap: ExposureMap, priceMap: PriceMap, fills: Fill[]): UnrealizedPositionState;
/**
* Compute the position entry notional.
* Compute the realized position state for CLOSED positions.
*
* Formula: Σ(|quantity| × entryPrice)
*/
export declare function computePositionEntryNotional(assetPnLs: AssetPnL[]): BigNumber;
/**
* Compute the position unrealized PnL.
*
* Formula: Σ(pnl)
*/
export declare function computePositionUPnL(assetPnLs: AssetPnL[]): BigNumber;
/**
* Compute the position PnL in basis points.
*
* Formula: UPnL bips = upnl / entryNotional × 10_000
*/
export declare function computePositionUPnLBips(upnl: BigNumber, entryNotional: BigNumber): BigNumber | null;
/**
* Compute the average entry price of the current net position.
*
* This is equivalent to the weighted average price of the current basket,
* where the weights are the absolute quantities per asset.
*
* Formula: Σ(|quantity| × entryPrice) / Σ(|quantity|)
*/
export declare function computePositionEntryPrice(assetPnLs: AssetPnL[]): BigNumber | null;
/**
* Compute the current (mark) price of the current net position.
*
* This is the weighted average mark price of the current basket,
* where the weights are the absolute quantities per asset.
*
* Formula: Σ(|quantity| × currentPrice) / Σ(|quantity|)
*/
export declare function computePositionCurrentPrice(assetPnLs: AssetPnL[]): BigNumber | null;
/**
* Compute the position PnL from fills.
*
* @param exposureMap - The exposure map of the position.
* @param priceMap - The price map of the position.
* @param fills - The fills of the position.
* @returns The position PnL.
* @returns The realized position state.
*/
export declare function computePositionFromFills(exposureMap: ExposureMap, priceMap: PriceMap, fills: Fill[]): PositionPnL;
export declare function computeRealizedPositionState(fills: Fill[]): RealizedPositionState;
export {};
export * from './compute';
export type { RealizedPositionState, UnrealizedPositionState } from './position.types';
import BigNumber from 'bignumber.js';
import type { AssetPnL } from '../asset/asset.types';
export type PositionPnL = {
assets: AssetPnL[];
import type { AssetUnrealizedPnL } from '../asset/asset.types';
export type UnrealizedPositionState = {
assetUPnLs: AssetUnrealizedPnL[];
entryPrice: BigNumber | null;

@@ -10,3 +10,10 @@ entryNotional: BigNumber;

upnl: BigNumber;
upnlBips: BigNumber | null;
upnlBips: BigNumber;
};
export type RealizedPositionState = {
grossPnl: BigNumber;
netPnl: BigNumber;
totalFees: BigNumber;
tradeFees: BigNumber;
pearFees: BigNumber;
};
{
"name": "@pear-protocol/utils",
"version": "0.0.16",
"version": "0.0.17",
"description": "Pear Protocol Utility functions",

@@ -23,2 +23,3 @@ "private": false,

"scripts": {
"test": "vitest run",
"build": "tsup && tsc -p tsconfig.build.json",

@@ -29,3 +30,3 @@ "clean": "rm -rf dist .turbo",

"dependencies": {
"@pear-protocol/types": "0.0.16",
"@pear-protocol/types": "^0.0.17",
"bignumber.js": "9.3.1"

@@ -36,4 +37,5 @@ },

"tsup": "8.5.1",
"typescript": "5.9.3"
"typescript": "5.9.3",
"vitest": "4.1.2"
}
}
import 'bignumber.js';
import BigNumber from 'bignumber.js';
import { computeEntryPriceForAsset } from '../fills/compute';
import { ZERO } from '../math';
function computeAssetEntryNotional(signedQuantity, fills) {
if (signedQuantity.isZero()) return ZERO;
const entryPrice = computeEntryPriceForAsset(fills);
if (entryPrice.isZero()) return ZERO;
const unsignedQuantity = signedQuantity.abs();
return unsignedQuantity.multipliedBy(entryPrice);
}
function computeAssetUPnL(signedQuantity, currentPrice, fills) {
if (signedQuantity.isZero()) return ZERO;
const entryPrice = computeEntryPriceForAsset(fills);
if (entryPrice.isZero()) return ZERO;
const deltaPrice = currentPrice.minus(entryPrice);
return signedQuantity.multipliedBy(deltaPrice);
}
function computeAssetPnLs(exposureMap, priceMap, fills) {
const result = [];
for (const [id, signedQtyString] of Object.entries(exposureMap)) {
const priceString = priceMap[id];
if (!priceString) continue;
const signedQuantity = new BigNumber(signedQtyString);
if (signedQuantity.isZero()) continue;
const currentPrice = new BigNumber(priceString);
const assetFills = fills.filter((f) => f.symbol === id);
const entryPrice = computeEntryPriceForAsset(assetFills);
const pnl = signedQuantity.multipliedBy(currentPrice.minus(entryPrice));
result.push({
id,
signedQuantity,
entryPrice,
currentPrice,
pnl
});
}
return result;
}
export { computeAssetEntryNotional, computeAssetPnLs, computeAssetUPnL };
export * from './compute';
import 'bignumber.js';
import { exponentiate } from '../math';
function computeBasketWeightedRatioV1(basket, priceMap) {
let logSum = 0;
for (const leg of basket) {
const price = priceMap[leg.symbol];
if (!price) return null;
const priceNum = Number(price);
if (priceNum <= 0) return null;
const sign = leg.side === "BUY" ? 1 : -1;
logSum += sign * leg.weight * Math.log(priceNum);
}
return exponentiate(logSum);
}
function computeBasketIndexV1(basket, priceMap) {
let logSum = 0;
for (const leg of basket) {
const price = priceMap[leg.symbol];
if (!price) return null;
const priceNum = Number(price);
if (priceNum <= 0) return null;
logSum += leg.weight * Math.log(priceNum);
}
return exponentiate(logSum);
}
export { computeBasketIndexV1, computeBasketWeightedRatioV1 };
export * from './compute';
import BigNumber from 'bignumber.js';
function contractsToBase(contracts, contractSize) {
return new BigNumber(contracts).times(contractSize);
}
function baseToContracts(base, contractSize) {
return new BigNumber(base).div(contractSize);
}
export { baseToContracts, contractsToBase };
import BigNumber from 'bignumber.js';
import { ZERO } from '../math';
function computeEntryPriceForAsset(fills) {
let entryPrice = ZERO;
let positionSize = ZERO;
for (const fill of fills) {
const price = new BigNumber(fill.price);
const quantity = new BigNumber(fill.quantity);
const signed = fill.side === "BUY" ? quantity : quantity.negated();
const newPositionSize = positionSize.plus(signed);
const isSameDirection = !positionSize.isZero() && positionSize.isPositive() === signed.isPositive();
const isFlip = !positionSize.isZero() && !newPositionSize.isZero() && newPositionSize.isPositive() !== positionSize.isPositive();
if (positionSize.isZero() || isFlip) {
entryPrice = price;
} else if (isSameDirection) {
entryPrice = entryPrice.times(positionSize.abs()).plus(price.times(quantity)).div(newPositionSize.abs());
}
positionSize = newPositionSize;
}
return entryPrice;
}
export { computeEntryPriceForAsset };
export * from './compute';
import BigNumber from 'bignumber.js';
const ZERO = new BigNumber(0);
const BIPS = new BigNumber(1e4);
function exponentiate(value) {
const result = Math.exp(value);
return new BigNumber(result);
}
function sign(value) {
if (value.isZero()) return 0;
return value.isPositive() ? 1 : -1;
}
function countDecimals(value) {
const parts = value.split(".");
return parts[1]?.length ?? 0;
}
export { BIPS, ZERO, countDecimals, exponentiate, sign };
import 'bignumber.js';
import { computeAssetPnLs } from '../asset';
import { ZERO, BIPS } from '../math';
function computePositionMarkNotional(assetPnLs) {
return assetPnLs.reduce((acc, asset) => {
if (asset.signedQuantity.isZero()) return acc;
return acc.plus(asset.signedQuantity.abs().multipliedBy(asset.currentPrice));
}, ZERO);
}
function computePositionEntryNotional(assetPnLs) {
return assetPnLs.reduce((acc, asset) => {
if (asset.signedQuantity.isZero()) return acc;
return acc.plus(asset.signedQuantity.abs().multipliedBy(asset.entryPrice));
}, ZERO);
}
function computePositionUPnL(assetPnLs) {
return assetPnLs.reduce((acc, asset) => acc.plus(asset.pnl), ZERO);
}
function computePositionUPnLBips(upnl, entryNotional) {
if (entryNotional.isZero()) return null;
return upnl.div(entryNotional).multipliedBy(BIPS);
}
function computePositionEntryPrice(assetPnLs) {
const { totalWeightedPrice, totalWeight } = assetPnLs.reduce(
(acc, { signedQuantity, entryPrice }) => {
const absQuantity = signedQuantity.abs();
return {
totalWeightedPrice: acc.totalWeightedPrice.plus(absQuantity.multipliedBy(entryPrice)),
totalWeight: acc.totalWeight.plus(absQuantity)
};
},
{
totalWeightedPrice: ZERO,
totalWeight: ZERO
}
);
if (totalWeight.isZero()) {
return null;
}
return totalWeightedPrice.dividedBy(totalWeight);
}
function computePositionCurrentPrice(assetPnLs) {
const { totalWeightedPrice, totalWeight } = assetPnLs.reduce(
(acc, { signedQuantity, currentPrice }) => {
const absQuantity = signedQuantity.abs();
return {
totalWeightedPrice: acc.totalWeightedPrice.plus(absQuantity.multipliedBy(currentPrice)),
totalWeight: acc.totalWeight.plus(absQuantity)
};
},
{
totalWeightedPrice: ZERO,
totalWeight: ZERO
}
);
if (totalWeight.isZero()) {
return null;
}
return totalWeightedPrice.dividedBy(totalWeight);
}
function computePositionFromFills(exposureMap, priceMap, fills) {
const assets = computeAssetPnLs(exposureMap, priceMap, fills);
const entryPrice = computePositionEntryPrice(assets);
const entryNotional = computePositionEntryNotional(assets);
const markPrice = computePositionCurrentPrice(assets);
const markNotional = computePositionMarkNotional(assets);
const upnl = computePositionUPnL(assets);
const upnlBips = computePositionUPnLBips(upnl, entryNotional);
return {
assets,
entryPrice,
entryNotional,
markPrice,
markNotional,
upnl,
upnlBips
};
}
export { computePositionCurrentPrice, computePositionEntryNotional, computePositionEntryPrice, computePositionFromFills, computePositionMarkNotional, computePositionUPnL, computePositionUPnLBips };
export * from './compute';
import 'bignumber.js';
import BigNumber from 'bignumber.js';
BigNumber.config({
DECIMAL_PLACES: 30,
ROUNDING_MODE: BigNumber.ROUND_DOWN
});
function precisePrice(connector, raw, precision, side = "BUY", slippageToleranceBps = 0) {
const normalized = normalizePrice(raw);
const effectiveRaw = withSlippageTolerance(normalized, slippageToleranceBps, side);
switch (connector) {
case "hyperliquid":
return preciseHyperliquidPrice(effectiveRaw, precision, side);
case "binance":
case "bybit":
case "okx":
case "lighter":
return preciseTickGridPrice(effectiveRaw, precision, side);
}
}
function withSlippageTolerance(normalized, slippageToleranceBps, side) {
if (slippageToleranceBps === 0) {
return normalized;
}
const bps = new BigNumber(slippageToleranceBps);
const factor = bps.div(1e4);
return side === "BUY" ? normalized.times(new BigNumber(1).plus(factor)) : normalized.times(new BigNumber(1).minus(factor));
}
function preciseHyperliquidPrice(price, precision, side) {
const maxDecimals = precision.price.decimals;
const maxSigFigs = precision.price.sigFigs ?? 5;
const rounding = side === "BUY" ? BigNumber.ROUND_FLOOR : BigNumber.ROUND_CEIL;
price = price.decimalPlaces(maxDecimals, rounding);
price = clampSigFigs(price, maxSigFigs, maxDecimals, rounding);
return price.toFixed();
}
function preciseTickGridPrice(price, precision, side) {
const tick = precision.price.tick ?? 1e-4;
const rounding = side === "BUY" ? BigNumber.ROUND_FLOOR : BigNumber.ROUND_CEIL;
const tickBN = new BigNumber(tick);
const scaled = price.div(tickBN);
const snapped = scaled.integerValue(rounding).times(tickBN);
const maxDecimals = precision.price.decimals;
const final = snapped.decimalPlaces(maxDecimals, rounding);
return final.toFixed();
}
function clampSigFigs(price, maxSigFigs, maxDecimals, rounding) {
const countSigFigs = (x) => x.toFixed().replace(".", "").replace(/^0+/, "").length;
if (countSigFigs(price) <= maxSigFigs) {
return price;
}
for (let d = maxDecimals; d >= 0; d--) {
const candidate = price.decimalPlaces(d, rounding);
if (countSigFigs(candidate) <= maxSigFigs) {
return candidate;
}
}
return price;
}
function normalizePrice(value) {
const bn = new BigNumber(value);
return new BigNumber(bn.toFixed());
}
export { precisePrice };
import BigNumber from 'bignumber.js';
import { baseToContracts } from './contract-size';
BigNumber.config({
DECIMAL_PLACES: 30,
ROUNDING_MODE: BigNumber.ROUND_DOWN
});
function preciseQuantity(connector, raw, precision, side) {
switch (connector) {
case "hyperliquid":
return preciseHyperliquidQuantity(raw, precision, side);
case "binance":
case "bybit":
case "okx":
case "lighter":
return preciseStepGridQuantity(raw, precision, side);
}
}
function preciseHyperliquidQuantity(raw, precision, side) {
const decimals = precision.quantity.decimals;
const qty = new BigNumber(raw);
const rounded = qty.decimalPlaces(decimals, side === "BUY" ? BigNumber.ROUND_FLOOR : BigNumber.ROUND_CEIL);
return rounded.toFixed();
}
function preciseStepGridQuantity(raw, precision, side) {
const step = precision.quantity.step;
const qty = baseToContracts(raw, precision.quantity.contractSize);
const stepBN = new BigNumber(step);
const scaled = qty.div(stepBN);
const rounded = side === "BUY" ? scaled.integerValue(BigNumber.ROUND_FLOOR) : scaled.integerValue(BigNumber.ROUND_CEIL);
const result = rounded.times(stepBN);
const maxDecimals = precision.quantity.decimals;
const final = result.decimalPlaces(maxDecimals, side === "BUY" ? BigNumber.ROUND_FLOOR : BigNumber.ROUND_CEIL);
return final.toFixed();
}
export { preciseQuantity };
function validateLeverage(leverage, instrument) {
if (leverage < 1 || leverage > 100) return false;
if (leverage > instrument.leverage) return false;
return true;
}
export { validateLeverage };
import BigNumber from 'bignumber.js';
import { contractsToBase } from './contract-size';
function validateQuantity(quantity, price, precision) {
const q = new BigNumber(quantity);
if (!q.isFinite() || q.lte(0)) return false;
const p = new BigNumber(price);
if (!p.isFinite() || p.lte(0)) return false;
const step = new BigNumber(precision.quantity.step);
const scaled = q.div(step);
if (!scaled.isInteger()) return false;
const notional = contractsToBase(q, precision.quantity.contractSize).times(p);
if (precision.notional?.min !== void 0 && precision.notional.min !== null && precision.notional.min > 0) {
if (notional.lt(new BigNumber(precision.notional.min))) return false;
}
if (precision.notional?.max !== void 0 && precision.notional.max !== null && precision.notional.max > 0) {
if (notional.gt(new BigNumber(precision.notional.max))) return false;
}
return true;
}
export { validateQuantity };