| | @@ -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 }; |