@lifi/perps-sdk 24.0.0 → 25.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/README.md +34 -28
  2. package/dist/cjs/decimal/arithmetic.d.ts +2 -0
  3. package/dist/cjs/decimal/arithmetic.d.ts.map +1 -1
  4. package/dist/cjs/decimal/arithmetic.js +6 -1
  5. package/dist/cjs/decimal/arithmetic.js.map +1 -1
  6. package/dist/cjs/decimal/convert.d.ts +0 -2
  7. package/dist/cjs/decimal/convert.d.ts.map +1 -1
  8. package/dist/cjs/decimal/convert.js +2 -11
  9. package/dist/cjs/decimal/convert.js.map +1 -1
  10. package/dist/cjs/decimal/decimalStringToBig.d.ts.map +1 -1
  11. package/dist/cjs/decimal/decimalStringToBig.js +3 -5
  12. package/dist/cjs/decimal/decimalStringToBig.js.map +1 -1
  13. package/dist/cjs/decimal/parse.d.ts +1 -0
  14. package/dist/cjs/decimal/parse.d.ts.map +1 -1
  15. package/dist/cjs/decimal/parse.js +2 -1
  16. package/dist/cjs/decimal/parse.js.map +1 -1
  17. package/dist/cjs/errors/invalidInput.d.ts +3 -0
  18. package/dist/cjs/errors/invalidInput.d.ts.map +1 -0
  19. package/dist/cjs/errors/invalidInput.js +9 -0
  20. package/dist/cjs/errors/invalidInput.js.map +1 -0
  21. package/dist/cjs/index.d.ts +5 -4
  22. package/dist/cjs/index.d.ts.map +1 -1
  23. package/dist/cjs/index.js +10 -6
  24. package/dist/cjs/index.js.map +1 -1
  25. package/dist/cjs/math/order.d.ts +4 -4
  26. package/dist/cjs/math/order.d.ts.map +1 -1
  27. package/dist/cjs/math/order.js +31 -18
  28. package/dist/cjs/math/order.js.map +1 -1
  29. package/dist/cjs/math/position.d.ts +2 -2
  30. package/dist/cjs/math/position.d.ts.map +1 -1
  31. package/dist/cjs/math/position.js +17 -14
  32. package/dist/cjs/math/position.js.map +1 -1
  33. package/dist/cjs/types/provider.d.ts.map +1 -1
  34. package/dist/cjs/utils/warnOnce.d.ts +6 -2
  35. package/dist/cjs/utils/warnOnce.d.ts.map +1 -1
  36. package/dist/cjs/utils/warnOnce.js +12 -6
  37. package/dist/cjs/utils/warnOnce.js.map +1 -1
  38. package/dist/cjs/version.d.ts +1 -1
  39. package/dist/cjs/version.js +1 -1
  40. package/dist/cjs/websocket/wsLog.d.ts +1 -1
  41. package/dist/cjs/websocket/wsLog.d.ts.map +1 -1
  42. package/dist/cjs/websocket/wsLog.js +4 -3
  43. package/dist/cjs/websocket/wsLog.js.map +1 -1
  44. package/dist/cjs/wire/orderAmounts.d.ts +3 -2
  45. package/dist/cjs/wire/orderAmounts.d.ts.map +1 -1
  46. package/dist/cjs/wire/orderAmounts.js +17 -13
  47. package/dist/cjs/wire/orderAmounts.js.map +1 -1
  48. package/dist/cjs/wire/refuel.js +2 -2
  49. package/dist/cjs/wire/refuel.js.map +1 -1
  50. package/dist/esm/decimal/arithmetic.d.ts +24 -10
  51. package/dist/esm/decimal/arithmetic.d.ts.map +1 -1
  52. package/dist/esm/decimal/arithmetic.js +26 -10
  53. package/dist/esm/decimal/arithmetic.js.map +1 -1
  54. package/dist/esm/decimal/compare.d.ts +3 -3
  55. package/dist/esm/decimal/compare.js +3 -3
  56. package/dist/esm/decimal/convert.d.ts +2 -24
  57. package/dist/esm/decimal/convert.d.ts.map +1 -1
  58. package/dist/esm/decimal/convert.js +3 -32
  59. package/dist/esm/decimal/convert.js.map +1 -1
  60. package/dist/esm/decimal/decimalStringToBig.d.ts +4 -5
  61. package/dist/esm/decimal/decimalStringToBig.d.ts.map +1 -1
  62. package/dist/esm/decimal/decimalStringToBig.js +7 -10
  63. package/dist/esm/decimal/decimalStringToBig.js.map +1 -1
  64. package/dist/esm/decimal/format.js +3 -3
  65. package/dist/esm/decimal/parse.d.ts +8 -1
  66. package/dist/esm/decimal/parse.d.ts.map +1 -1
  67. package/dist/esm/decimal/parse.js +8 -1
  68. package/dist/esm/decimal/parse.js.map +1 -1
  69. package/dist/esm/errors/invalidInput.d.ts +4 -0
  70. package/dist/esm/errors/invalidInput.d.ts.map +1 -0
  71. package/dist/esm/errors/invalidInput.js +7 -0
  72. package/dist/esm/errors/invalidInput.js.map +1 -0
  73. package/dist/esm/index.d.ts +5 -4
  74. package/dist/esm/index.d.ts.map +1 -1
  75. package/dist/esm/index.js +5 -4
  76. package/dist/esm/index.js.map +1 -1
  77. package/dist/esm/math/fills.d.ts +4 -4
  78. package/dist/esm/math/fills.js +4 -4
  79. package/dist/esm/math/order.d.ts +24 -26
  80. package/dist/esm/math/order.d.ts.map +1 -1
  81. package/dist/esm/math/order.js +52 -41
  82. package/dist/esm/math/order.js.map +1 -1
  83. package/dist/esm/math/position.d.ts +30 -26
  84. package/dist/esm/math/position.d.ts.map +1 -1
  85. package/dist/esm/math/position.js +46 -39
  86. package/dist/esm/math/position.js.map +1 -1
  87. package/dist/esm/services/resolveSubscribeQuote.d.ts +1 -1
  88. package/dist/esm/services/resolveSubscribeQuote.js +1 -1
  89. package/dist/esm/types/provider.d.ts +5 -3
  90. package/dist/esm/types/provider.d.ts.map +1 -1
  91. package/dist/esm/utils/createSafeFunction.d.ts +1 -1
  92. package/dist/esm/utils/createSafeFunction.js +1 -1
  93. package/dist/esm/utils/warnOnce.d.ts +17 -6
  94. package/dist/esm/utils/warnOnce.d.ts.map +1 -1
  95. package/dist/esm/utils/warnOnce.js +22 -10
  96. package/dist/esm/utils/warnOnce.js.map +1 -1
  97. package/dist/esm/version.d.ts +1 -1
  98. package/dist/esm/version.js +1 -1
  99. package/dist/esm/websocket/wsLog.d.ts +4 -4
  100. package/dist/esm/websocket/wsLog.d.ts.map +1 -1
  101. package/dist/esm/websocket/wsLog.js +8 -7
  102. package/dist/esm/websocket/wsLog.js.map +1 -1
  103. package/dist/esm/wire/orderAmounts.d.ts +9 -7
  104. package/dist/esm/wire/orderAmounts.d.ts.map +1 -1
  105. package/dist/esm/wire/orderAmounts.js +22 -18
  106. package/dist/esm/wire/orderAmounts.js.map +1 -1
  107. package/dist/esm/wire/refuel.d.ts +1 -1
  108. package/dist/esm/wire/refuel.js +4 -4
  109. package/dist/esm/wire/refuel.js.map +1 -1
  110. package/dist/esm/wire/transferable.d.ts +2 -2
  111. package/dist/esm/wire/transferable.js +2 -2
  112. package/dist/types/decimal/arithmetic.d.ts +24 -10
  113. package/dist/types/decimal/arithmetic.d.ts.map +1 -1
  114. package/dist/types/decimal/compare.d.ts +3 -3
  115. package/dist/types/decimal/convert.d.ts +2 -24
  116. package/dist/types/decimal/convert.d.ts.map +1 -1
  117. package/dist/types/decimal/decimalStringToBig.d.ts +4 -5
  118. package/dist/types/decimal/decimalStringToBig.d.ts.map +1 -1
  119. package/dist/types/decimal/parse.d.ts +8 -1
  120. package/dist/types/decimal/parse.d.ts.map +1 -1
  121. package/dist/types/errors/invalidInput.d.ts +4 -0
  122. package/dist/types/errors/invalidInput.d.ts.map +1 -0
  123. package/dist/types/index.d.ts +5 -4
  124. package/dist/types/index.d.ts.map +1 -1
  125. package/dist/types/math/fills.d.ts +4 -4
  126. package/dist/types/math/order.d.ts +24 -26
  127. package/dist/types/math/order.d.ts.map +1 -1
  128. package/dist/types/math/position.d.ts +30 -26
  129. package/dist/types/math/position.d.ts.map +1 -1
  130. package/dist/types/services/resolveSubscribeQuote.d.ts +1 -1
  131. package/dist/types/types/provider.d.ts +5 -3
  132. package/dist/types/types/provider.d.ts.map +1 -1
  133. package/dist/types/utils/createSafeFunction.d.ts +1 -1
  134. package/dist/types/utils/warnOnce.d.ts +17 -6
  135. package/dist/types/utils/warnOnce.d.ts.map +1 -1
  136. package/dist/types/version.d.ts +1 -1
  137. package/dist/types/websocket/wsLog.d.ts +4 -4
  138. package/dist/types/websocket/wsLog.d.ts.map +1 -1
  139. package/dist/types/wire/orderAmounts.d.ts +9 -7
  140. package/dist/types/wire/orderAmounts.d.ts.map +1 -1
  141. package/dist/types/wire/refuel.d.ts +1 -1
  142. package/dist/types/wire/transferable.d.ts +2 -2
  143. package/package.json +2 -2
  144. package/src/decimal/arithmetic.ts +36 -10
  145. package/src/decimal/compare.ts +3 -3
  146. package/src/decimal/convert.ts +3 -40
  147. package/src/decimal/decimalStringToBig.ts +7 -12
  148. package/src/decimal/format.ts +3 -3
  149. package/src/decimal/parse.ts +12 -1
  150. package/src/errors/invalidInput.ts +10 -0
  151. package/src/index.ts +8 -3
  152. package/src/math/fills.ts +4 -4
  153. package/src/math/order.ts +56 -48
  154. package/src/math/position.ts +50 -44
  155. package/src/services/resolveSubscribeQuote.ts +1 -1
  156. package/src/types/provider.ts +5 -3
  157. package/src/utils/createSafeFunction.ts +1 -1
  158. package/src/utils/warnOnce.ts +28 -12
  159. package/src/version.ts +1 -1
  160. package/src/websocket/wsLog.ts +22 -9
  161. package/src/wire/orderAmounts.ts +42 -24
  162. package/src/wire/refuel.ts +4 -4
  163. package/src/wire/transferable.ts +2 -2
@@ -2,7 +2,7 @@
2
2
  * Display-tier position formulas and margin-adjustment predicates. Every
3
3
  * formula takes and gives decimal strings; arithmetic is exact Big.js, with
4
4
  * division to 40 decimal places. Each formula throws `ValidationError` on an
5
- * input that is not a decimal string and has a `safe*` pair that gives
5
+ * input that does not match the decimal pattern and has a `safe*` pair that gives
6
6
  * `undefined` in place of the throw.
7
7
  *
8
8
  * Sign convention: long = +1, short = -1. Sizes passed to the `estimate*`
@@ -12,26 +12,24 @@
12
12
 
13
13
  import {
14
14
  MarginMode,
15
- PerpsErrorCode,
16
15
  type PerpsMarket,
17
16
  type Position,
18
17
  PositionMarginAdjustment,
19
18
  } from '@lifi/perps-types'
20
19
  import { DivBig } from '../decimal/big.js'
21
- import { numberToDecimalString } from '../decimal/convert.js'
22
20
  import {
23
21
  bigToDecimalString,
24
22
  decimalStringToBig,
25
23
  decimalStringToDivBig,
26
24
  } from '../decimal/decimalStringToBig.js'
27
- import { PerpsError } from '../errors/PerpsError.js'
25
+ import { invalidInput } from '../errors/invalidInput.js'
28
26
  import type { LiquidationEstimateParams } from '../types/provider.js'
29
27
  import { createSafeFunction } from '../utils/createSafeFunction.js'
30
28
 
31
29
  /**
32
30
  * Notional value of a position: `|size| × price`.
33
31
  *
34
- * @throws {PerpsError} `ValidationError` when an input is not a decimal string.
32
+ * @throws {PerpsError} `ValidationError` when an input does not match the decimal pattern.
35
33
  * @public
36
34
  */
37
35
  export function calculateNotionalValue(size: string, price: string): string {
@@ -50,7 +48,7 @@ export const safeCalculateNotionalValue = createSafeFunction(
50
48
  * Unrealized PnL: `(currentPrice - entryPrice) × size`, where `size` is
51
49
  * positive for a long and negative for a short.
52
50
  *
53
- * @throws {PerpsError} `ValidationError` when an input is not a decimal string.
51
+ * @throws {PerpsError} `ValidationError` when an input does not match the decimal pattern.
54
52
  * @public
55
53
  */
56
54
  export function calculateUnrealizedPnl(
@@ -73,16 +71,16 @@ export const safeCalculateUnrealizedPnl = createSafeFunction(
73
71
 
74
72
  /**
75
73
  * Return on equity as a percentage (`'10'` for 10%): `pnl ÷ margin × 100`.
76
- * A zero margin gives `'0'`.
77
74
  *
78
- * @throws {PerpsError} `ValidationError` when an input is not a decimal string.
75
+ * @throws {PerpsError} `ValidationError` when an input does not match the
76
+ * decimal pattern or `margin` is zero.
79
77
  * @public
80
78
  */
81
79
  export function calculateRoe(pnl: string, margin: string): string {
82
80
  const marginBig = decimalStringToBig(margin)
83
81
  const pnlBig = decimalStringToDivBig(pnl)
84
82
  if (marginBig.eq(0)) {
85
- return '0'
83
+ throw invalidInput('margin', 'must not be zero')
86
84
  }
87
85
  return bigToDecimalString(pnlBig.div(marginBig).times(100))
88
86
  }
@@ -93,7 +91,7 @@ export const safeCalculateRoe = createSafeFunction('calculateRoe', calculateRoe)
93
91
  /**
94
92
  * Required margin: `notionalValue ÷ leverage`.
95
93
  *
96
- * @throws {PerpsError} `ValidationError` when an input is not a decimal string
94
+ * @throws {PerpsError} `ValidationError` when an input does not match the decimal pattern
97
95
  * or `leverage` is zero.
98
96
  * @public
99
97
  */
@@ -104,10 +102,7 @@ export function calculateRequiredMargin(
104
102
  const notionalBig = decimalStringToDivBig(notionalValue)
105
103
  const leverageBig = decimalStringToBig(leverage)
106
104
  if (leverageBig.eq(0)) {
107
- throw new PerpsError(
108
- PerpsErrorCode.ValidationError,
109
- 'Leverage must not be zero.'
110
- )
105
+ throw invalidInput('leverage', 'must not be zero')
111
106
  }
112
107
  return bigToDecimalString(notionalBig.div(leverageBig))
113
108
  }
@@ -120,9 +115,10 @@ export const safeCalculateRequiredMargin = createSafeFunction(
120
115
 
121
116
  /**
122
117
  * Distance from the current price to the liquidation price, as an absolute
123
- * percentage of the current price. A zero current price gives `'0'`.
118
+ * percentage of the current price.
124
119
  *
125
- * @throws {PerpsError} `ValidationError` when an input is not a decimal string.
120
+ * @throws {PerpsError} `ValidationError` when an input does not match the
121
+ * decimal pattern or `currentPrice` is zero.
126
122
  * @public
127
123
  */
128
124
  export function calculateLiquidationDistance(params: {
@@ -132,7 +128,7 @@ export function calculateLiquidationDistance(params: {
132
128
  const liquidationPrice = decimalStringToDivBig(params.liquidationPrice)
133
129
  const currentPrice = decimalStringToBig(params.currentPrice)
134
130
  if (currentPrice.eq(0)) {
135
- return '0'
131
+ throw invalidInput('currentPrice', 'must not be zero')
136
132
  }
137
133
  return bigToDecimalString(
138
134
  liquidationPrice.minus(currentPrice).div(currentPrice).abs().times(100)
@@ -147,9 +143,9 @@ export const safeCalculateLiquidationDistance = createSafeFunction(
147
143
 
148
144
  /**
149
145
  * Effective leverage of an open position: `positionValueUsd ÷ marginUsd`.
150
- * A zero margin gives `'0'`.
151
146
  *
152
- * @throws {PerpsError} `ValidationError` when an input is not a decimal string.
147
+ * @throws {PerpsError} `ValidationError` when an input does not match the
148
+ * decimal pattern or `marginUsd` is zero.
153
149
  * @public
154
150
  */
155
151
  export function calculateEffectiveLeverage(params: {
@@ -159,7 +155,7 @@ export function calculateEffectiveLeverage(params: {
159
155
  const positionValue = decimalStringToDivBig(params.positionValueUsd)
160
156
  const margin = decimalStringToBig(params.marginUsd)
161
157
  if (margin.eq(0)) {
162
- return '0'
158
+ throw invalidInput('marginUsd', 'must not be zero')
163
159
  }
164
160
  return bigToDecimalString(positionValue.div(margin))
165
161
  }
@@ -182,9 +178,12 @@ export const safeCalculateEffectiveLeverage = createSafeFunction(
182
178
  *
183
179
  * @param maintenanceMarginRate - Venue maintenance margin rate as a fraction
184
180
  * (`'0.01'` = 1%).
185
- * @returns The estimate, or `undefined` for a zero leverage or a zero
186
- * denominator.
187
- * @throws {PerpsError} `ValidationError` when an input is not a decimal string.
181
+ * @returns The estimate, or `undefined` when `1 / leverage` is at or below
182
+ * `maintenanceMarginRate`: the position opens at or past liquidation, so
183
+ * the model has no price on the correct side of entry.
184
+ * @throws {PerpsError} `ValidationError` when an input does not match the
185
+ * decimal pattern, `leverage` is zero, or `maintenanceMarginRate` gives a
186
+ * zero denominator (`1` for a long, `-1` for a short).
188
187
  * @public
189
188
  */
190
189
  export function estimateLiquidationPrice(params: {
@@ -198,13 +197,20 @@ export function estimateLiquidationPrice(params: {
198
197
  const mmr = decimalStringToBig(params.maintenanceMarginRate)
199
198
  const side = params.isLong ? 1 : -1
200
199
  const denominator = mmr.times(-side).plus(1)
201
- if (leverage.eq(0) || denominator.eq(0)) {
200
+ if (leverage.eq(0)) {
201
+ throw invalidInput('leverage', 'must not be zero')
202
+ }
203
+ if (denominator.eq(0)) {
204
+ throw invalidInput(
205
+ 'maintenanceMarginRate',
206
+ `must not be ${params.isLong ? '1' : '-1'} for this side`
207
+ )
208
+ }
209
+ const marginFractionAvailable = new DivBig(1).div(leverage).minus(mmr)
210
+ if (marginFractionAvailable.lte(0)) {
202
211
  return undefined
203
212
  }
204
- const marginAvailable = new DivBig(1)
205
- .div(leverage)
206
- .minus(mmr)
207
- .times(entryPrice)
213
+ const marginAvailable = marginFractionAvailable.times(entryPrice)
208
214
  return bigToDecimalString(
209
215
  entryPrice.minus(marginAvailable.times(side).div(denominator))
210
216
  )
@@ -221,8 +227,10 @@ export const safeEstimateLiquidationPrice = createSafeFunction(
221
227
  * `market.maintenanceMarginRate`.
222
228
  *
223
229
  * @returns The estimate, or `undefined` when the market carries no
224
- * `maintenanceMarginRate` or `estimateLiquidationPrice` gives `undefined`.
225
- * @throws {PerpsError} `ValidationError` when an input is not a decimal string.
230
+ * `maintenanceMarginRate` or {@link estimateLiquidationPrice} gives
231
+ * `undefined`.
232
+ * @throws {PerpsError} `ValidationError` on an input that
233
+ * {@link estimateLiquidationPrice} rejects.
226
234
  * @public
227
235
  */
228
236
  export function estimateLiquidationPriceAtMarketRate(
@@ -236,7 +244,7 @@ export function estimateLiquidationPriceAtMarketRate(
236
244
  entryPrice: params.entryPrice,
237
245
  leverage: params.leverage,
238
246
  isLong: params.isLong,
239
- maintenanceMarginRate: numberToDecimalString(market.maintenanceMarginRate),
247
+ maintenanceMarginRate: market.maintenanceMarginRate,
240
248
  })
241
249
  }
242
250
 
@@ -252,7 +260,7 @@ export const safeEstimateLiquidationPriceAtMarketRate = createSafeFunction(
252
260
  * liquidation level, a short when it rises to it. A non-positive price on
253
261
  * either side counts as unknown, so the result is `false`.
254
262
  *
255
- * @throws {PerpsError} `ValidationError` when a price is not a decimal string.
263
+ * @throws {PerpsError} `ValidationError` when a price does not match the decimal pattern.
256
264
  * @public
257
265
  */
258
266
  export function wouldImmediatelyLiquidate(params: {
@@ -283,9 +291,8 @@ export const safeWouldImmediatelyLiquidate = createSafeFunction(
283
291
  *
284
292
  * @param fillPrice - Expected fill price (mid for market, limit price for
285
293
  * limit orders).
286
- * @returns The new average entry price, or `undefined` when the combined size
287
- * is not positive.
288
- * @throws {PerpsError} `ValidationError` when an input is not a decimal string.
294
+ * @throws {PerpsError} `ValidationError` when an input does not match the
295
+ * decimal pattern or `currentSize + addSize` is not positive.
289
296
  * @public
290
297
  */
291
298
  export function estimateAverageEntryPrice(params: {
@@ -293,14 +300,14 @@ export function estimateAverageEntryPrice(params: {
293
300
  currentEntry: string
294
301
  addSize: string
295
302
  fillPrice: string
296
- }): string | undefined {
303
+ }): string {
297
304
  const currentSize = decimalStringToDivBig(params.currentSize)
298
305
  const addSize = decimalStringToBig(params.addSize)
299
306
  const currentEntry = decimalStringToBig(params.currentEntry)
300
307
  const fillPrice = decimalStringToBig(params.fillPrice)
301
308
  const totalSize = currentSize.plus(addSize)
302
309
  if (totalSize.lte(0)) {
303
- return undefined
310
+ throw invalidInput('currentSize + addSize', 'must be positive')
304
311
  }
305
312
  return bigToDecimalString(
306
313
  currentSize
@@ -320,9 +327,8 @@ export const safeEstimateAverageEntryPrice = createSafeFunction(
320
327
  * Estimated effective leverage after adding margin and notional:
321
328
  * `(currentNotional + addNotional) ÷ (currentMargin + addMargin)`.
322
329
  *
323
- * @returns The new leverage, or `undefined` when the total margin is not
324
- * positive.
325
- * @throws {PerpsError} `ValidationError` when an input is not a decimal string.
330
+ * @throws {PerpsError} `ValidationError` when an input does not match the
331
+ * decimal pattern or `currentMargin + addMargin` is not positive.
326
332
  * @public
327
333
  */
328
334
  export function estimateNewLeverage(params: {
@@ -330,7 +336,7 @@ export function estimateNewLeverage(params: {
330
336
  currentMargin: string
331
337
  addNotional: string
332
338
  addMargin: string
333
- }): string | undefined {
339
+ }): string {
334
340
  const totalNotional = decimalStringToDivBig(params.currentNotional).plus(
335
341
  decimalStringToBig(params.addNotional)
336
342
  )
@@ -338,7 +344,7 @@ export function estimateNewLeverage(params: {
338
344
  decimalStringToBig(params.addMargin)
339
345
  )
340
346
  if (totalMargin.lte(0)) {
341
- return undefined
347
+ throw invalidInput('currentMargin + addMargin', 'must be positive')
342
348
  }
343
349
  return bigToDecimalString(totalNotional.div(totalMargin))
344
350
  }
@@ -354,7 +360,7 @@ export const safeEstimateNewLeverage = createSafeFunction(
354
360
  * `(markPrice - entryPrice) × size × (isLong ? 1 : -1)`.
355
361
  *
356
362
  * @param size - Position size as a non-negative magnitude.
357
- * @throws {PerpsError} `ValidationError` when an input is not a decimal string.
363
+ * @throws {PerpsError} `ValidationError` when an input does not match the decimal pattern.
358
364
  * @public
359
365
  */
360
366
  export function estimateUnrealizedPnl(params: {
@@ -382,7 +388,7 @@ export const safeEstimateUnrealizedPnl = createSafeFunction(
382
388
  * `(closePrice - entryPrice) × closeSize × (isLong ? 1 : -1)`.
383
389
  *
384
390
  * @param closeSize - Size being closed as a non-negative magnitude.
385
- * @throws {PerpsError} `ValidationError` when an input is not a decimal string.
391
+ * @throws {PerpsError} `ValidationError` when an input does not match the decimal pattern.
386
392
  * @public
387
393
  */
388
394
  export function calculateRealizedPnl(params: {
@@ -45,7 +45,7 @@ export const QUOTE_THROTTLE_MS = 100
45
45
  * price, a throwing `onQuote`) is logged via `wsLog.listenerFailure` and the
46
46
  * emission is skipped, on both the immediate and the trailing-timer path.
47
47
  *
48
- * @throws {PerpsError} `ValidationError` when `params.size` is not a decimal string.
48
+ * @throws {PerpsError} `ValidationError` when `params.size` does not match the decimal pattern.
49
49
  * @throws {PerpsError} `MarketNotFound` when no market matches symbol+type.
50
50
  * @internal
51
51
  */
@@ -599,9 +599,11 @@ export interface PerpsProviderPlugin {
599
599
  * positions, prefer `Position.liquidationPrice` from the venue. Pure —
600
600
  * does no I/O.
601
601
  *
602
- * @returns The estimated liquidation price, or `undefined` when the venue's
603
- * model cannot be evaluated client-side (degenerate inputs, or `market`
604
- * lacks the margin metadata the model needs).
602
+ * @returns The estimated liquidation price, or `undefined` when `market`
603
+ * lacks the margin metadata the model needs or `leverage` leaves no
604
+ * margin above the maintenance requirement.
605
+ * @throws {PerpsError} `ValidationError` when an input does not match the
606
+ * decimal pattern or is degenerate, such as a zero leverage.
605
607
  */
606
608
  estimateLiquidationPrice(
607
609
  market: PerpsMarket,
@@ -3,7 +3,7 @@
3
3
  * The wrapper logs a warning with `name` and the error, which carries the
4
4
  * stack trace.
5
5
  *
6
- * @internal
6
+ * @public
7
7
  */
8
8
  export function createSafeFunction<Args extends unknown[], Result>(
9
9
  name: string,
@@ -30,6 +30,19 @@ export const createWarnOnce = (): ((key: string, message: string) => void) => {
30
30
 
31
31
  const MAX_VALUE_CHARS = 64
32
32
 
33
+ /**
34
+ * Cut venue text to a fixed length, so a warner key or message built from it
35
+ * stays small.
36
+ *
37
+ * @internal
38
+ */
39
+ export const cutVenueText = (value: unknown): string => {
40
+ const text = String(value)
41
+ return text.length > MAX_VALUE_CHARS
42
+ ? `${text.slice(0, MAX_VALUE_CHARS)}…`
43
+ : text
44
+ }
45
+
33
46
  /**
34
47
  * Describe a skipped venue row. The venue value is cut to a fixed length, so
35
48
  * a warner key built from the text stays small.
@@ -41,22 +54,22 @@ export const skippedRowMessage = (
41
54
  row: string,
42
55
  field: string,
43
56
  value: unknown,
44
- expected: string
57
+ expected: string,
58
+ marketId?: string
45
59
  ): string => {
46
- const text = String(value)
47
- const shown =
48
- text.length > MAX_VALUE_CHARS ? `${text.slice(0, MAX_VALUE_CHARS)}…` : text
49
- return `[${source}] skipping ${row} row: \`${field}\` is not a valid ${expected}: '${shown}'`
60
+ const market = marketId === undefined ? '' : ` on market '${marketId}'`
61
+ return `[${source}] skipping ${row} row${market}: \`${field}\` is not a valid ${expected}: '${cutVenueText(value)}'`
50
62
  }
51
63
 
52
64
  const warnSkippedRowOnce = createWarnOnce()
53
65
 
54
66
  /**
55
- * Warn once for each provider, row, field and expected kind that a provider
56
- * mapper skipped a venue row because a required field is not valid. The key
57
- * holds no venue value, so a second bad value for the same field stays silent.
67
+ * Warn once for each provider, row, field, expected kind and market that a
68
+ * provider mapper skipped a venue row because a required field is not valid.
69
+ * The key holds no venue value, so a second bad value for the same field on
70
+ * the same market stays silent.
58
71
  *
59
- * @param expected - What the field must be. Defaults to `'decimal'`.
72
+ * @param options.expected - What the field must be. Defaults to `'decimal'`.
60
73
  * @internal
61
74
  */
62
75
  export const warnSkippedVenueRow = (
@@ -64,10 +77,13 @@ export const warnSkippedVenueRow = (
64
77
  row: string,
65
78
  field: string,
66
79
  value: unknown,
67
- expected = 'decimal'
80
+ {
81
+ marketId,
82
+ expected = 'decimal',
83
+ }: { marketId?: string; expected?: string } = {}
68
84
  ): void => {
69
85
  warnSkippedRowOnce(
70
- `${provider}|${row}|${field}|${expected}`,
71
- skippedRowMessage(provider, row, field, value, expected)
86
+ `${provider}|${row}|${field}|${expected}|${marketId ?? ''}`,
87
+ skippedRowMessage(provider, row, field, value, expected, marketId)
72
88
  )
73
89
  }
package/src/version.ts CHANGED
@@ -10,4 +10,4 @@ export const name = '@lifi/perps-sdk'
10
10
  *
11
11
  * @public
12
12
  */
13
- export const version = '24.0.0'
13
+ export const version = '25.0.1'
@@ -1,4 +1,8 @@
1
- import { createWarnOnce, skippedRowMessage } from '../utils/warnOnce.js'
1
+ import {
2
+ createWarnOnce,
3
+ cutVenueText,
4
+ skippedRowMessage,
5
+ } from '../utils/warnOnce.js'
2
6
 
3
7
  const MAX_PAYLOAD_CHARS = 512
4
8
 
@@ -64,29 +68,38 @@ export const wsLog = {
64
68
  },
65
69
  /**
66
70
  * A frame row or order-book level with a required field that is not valid.
67
- * Only that row is skipped. Logged once at `warn` for each provider, row and
68
- * field; the key holds no venue value.
71
+ * Only that row is skipped. Logged once at `warn` for each provider, row,
72
+ * field and market; the key holds no venue value.
69
73
  */
70
74
  skippedRow(
71
75
  provider: string,
72
76
  row: string,
73
77
  field: string,
74
- value: unknown
78
+ value: unknown,
79
+ marketId?: string
75
80
  ): void {
76
81
  warnSkippedRowOnce(
77
- `${provider}|${row}|${field}`,
78
- skippedRowMessage(`${provider}:ws`, row, field, value, 'decimal')
82
+ `${provider}|${row}|${field}|${marketId ?? ''}`,
83
+ skippedRowMessage(
84
+ `${provider}:ws`,
85
+ row,
86
+ field,
87
+ value,
88
+ 'decimal',
89
+ marketId
90
+ )
79
91
  )
80
92
  },
81
93
  /**
82
94
  * A frame row the mapper rejected for a reason other than one field, such as
83
95
  * a lifecycle state the SDK does not carry. Logged once at `warn` for each
84
- * provider and row; the key holds no venue value.
96
+ * provider, row and reason; the reason is cut to a fixed length.
85
97
  */
86
98
  droppedRow(provider: string, row: string, reason: string): void {
99
+ const shown = cutVenueText(reason)
87
100
  warnSkippedRowOnce(
88
- `${provider}|${row}|dropped`,
89
- `[${provider}:ws] skipping ${row} row: ${reason}`
101
+ `${provider}|${row}|dropped|${shown}`,
102
+ `[${provider}:ws] skipping ${row} row: ${shown}`
90
103
  )
91
104
  },
92
105
  /**
@@ -1,9 +1,16 @@
1
- import type { DecimalString, Market } from '@lifi/perps-types'
1
+ import {
2
+ type DecimalString,
3
+ type Market,
4
+ PerpsErrorCode,
5
+ } from '@lifi/perps-types'
2
6
  import type Big from 'big.js'
3
7
  import { DivBig, TruncBig } from '../decimal/big.js'
4
8
  import { isDecimalStringZero } from '../decimal/compare.js'
5
9
  import { isDecimalString } from '../decimal/parse.js'
10
+ import { invalidInput } from '../errors/invalidInput.js'
11
+ import { PerpsError } from '../errors/PerpsError.js'
6
12
  import type { PerpsSDKClient } from '../types/provider.js'
13
+ import { createSafeFunction } from '../utils/createSafeFunction.js'
7
14
  import { snapOrderSize } from './snap.js'
8
15
 
9
16
  /** @public */
@@ -31,14 +38,20 @@ export interface OrderAmounts {
31
38
  notional: DecimalString
32
39
  }
33
40
 
34
- function positiveDecimal(value: DecimalString): Big | null {
35
- if (!isDecimalString(value)) {
36
- return null
41
+ function requirePositiveDecimal(value: DecimalString, parameter: string): Big {
42
+ const parsed = isDecimalString(value) ? new TruncBig(value) : undefined
43
+ if (parsed === undefined || !parsed.gt(0)) {
44
+ throw invalidInput(
45
+ parameter,
46
+ `must be a positive decimal, got '${String(value)}'`
47
+ )
37
48
  }
38
- const parsed = new TruncBig(value)
39
- return parsed.gt(0) ? parsed : null
49
+ return parsed
40
50
  }
41
51
 
52
+ const lotSize = (market: Market): DecimalString =>
53
+ market.sizeIncrement ?? new TruncBig(`1e-${market.szDecimals}`).toFixed()
54
+
42
55
  /**
43
56
  * The three order-entry amounts, derived from whichever one the user typed.
44
57
  * Only `size` snaps, onto the market's venue lot grid, because an order sends
@@ -46,26 +59,22 @@ function positiveDecimal(value: DecimalString): Big | null {
46
59
  * `notional` is the snapped size × price, and a derived margin is
47
60
  * `notional ÷ leverage` in `DivBig` (40 places, half-up).
48
61
  *
49
- * @returns `null` when `amount` or `price` is not a positive
50
- * {@link DecimalString}, when `leverage` is not a positive finite number, or
51
- * when the snapped size is zero (below one lot).
52
- * @throws {PerpsError} `SDKError` when `market.providerId` names no registered
53
- * provider plugin.
62
+ * @throws {PerpsError} `ValidationError` that names `amount` or `price` when
63
+ * it is not a positive decimal, or `leverage` when it is not a finite number
64
+ * above zero. `ValidationError` with the lot size when the snapped size is
65
+ * zero (below one lot). `SDKError` when `market.providerId` names no
66
+ * registered provider plugin.
54
67
  * @public
55
68
  */
56
- export function calculateOrderAmounts(
57
- input: OrderAmountsInput
58
- ): OrderAmounts | null {
69
+ export function calculateOrderAmounts(input: OrderAmountsInput): OrderAmounts {
59
70
  const { sdk, market, held, leverage } = input
60
- const amount = positiveDecimal(input.amount)
61
- const price = positiveDecimal(input.price)
62
- if (
63
- amount === null ||
64
- price === null ||
65
- !Number.isFinite(leverage) ||
66
- leverage <= 0
67
- ) {
68
- return null
71
+ const amount = requirePositiveDecimal(input.amount, 'amount')
72
+ const price = requirePositiveDecimal(input.price, 'price')
73
+ if (!Number.isFinite(leverage) || leverage <= 0) {
74
+ throw invalidInput(
75
+ 'leverage',
76
+ `must be a finite number above zero, got ${leverage}`
77
+ )
69
78
  }
70
79
 
71
80
  const requested =
@@ -76,7 +85,10 @@ export function calculateOrderAmounts(
76
85
  : amount.times(leverage).div(price).toFixed()
77
86
  const size = snapOrderSize(sdk, market, requested)
78
87
  if (isDecimalStringZero(size)) {
79
- return null
88
+ throw new PerpsError(
89
+ PerpsErrorCode.ValidationError,
90
+ `The order size ${requested} is below the lot size ${lotSize(market)}.`
91
+ )
80
92
  }
81
93
 
82
94
  const notional = new TruncBig(size).times(price)
@@ -89,3 +101,9 @@ export function calculateOrderAmounts(
89
101
  notional: notional.toFixed(),
90
102
  }
91
103
  }
104
+
105
+ /** @public */
106
+ export const safeCalculateOrderAmounts = createSafeFunction(
107
+ 'calculateOrderAmounts',
108
+ calculateOrderAmounts
109
+ )
@@ -2,7 +2,7 @@ import { type DecimalString, PerpsErrorCode } from '@lifi/perps-types'
2
2
  import type Big from 'big.js'
3
3
  import { DivBig } from '../decimal/big.js'
4
4
  import { BASE_UNITS_PATTERN } from '../decimal/convert.js'
5
- import { decimalStringToBig } from '../decimal/decimalStringToBig.js'
5
+ import { decimalStringToDivBig } from '../decimal/decimalStringToBig.js'
6
6
  import { PerpsError } from '../errors/PerpsError.js'
7
7
 
8
8
  /**
@@ -48,7 +48,7 @@ function requireBaseUnits(value: DecimalString, field: string): Big {
48
48
  * when `recommendedAmount`, `recommendedUsd` or `priceUsd` is not greater
49
49
  * than zero: a route with nothing to refuel, not a failure.
50
50
  * @throws {PerpsError} `ValidationError` when a base-unit field is not an
51
- * integer string, a USD field is not a {@link DecimalString}, or `decimals`
51
+ * integer string, a USD field does not match the decimal pattern, or `decimals`
52
52
  * is not a non-negative integer.
53
53
  * @public
54
54
  */
@@ -67,8 +67,8 @@ export function calculateRefuelAmount(
67
67
  'recommendedAmount'
68
68
  )
69
69
  const balance = requireBaseUnits(input.nativeBalance, 'nativeBalance')
70
- const recommendedUsd = decimalStringToBig(input.recommendedUsd)
71
- const priceUsd = decimalStringToBig(input.priceUsd)
70
+ const recommendedUsd = decimalStringToDivBig(input.recommendedUsd)
71
+ const priceUsd = decimalStringToDivBig(input.priceUsd)
72
72
  const deficit = recommended.minus(balance)
73
73
  if (
74
74
  !recommended.gt(0) ||
@@ -9,8 +9,8 @@ import { decimalStringToBig } from '../decimal/decimalStringToBig.js'
9
9
  * @param venueFigure - The venue's free figure for the category, which can
10
10
  * exceed `units` or fall below zero.
11
11
  * @param units - Units held in the category, the upper bound of the clamp.
12
- * @throws {PerpsError} `ValidationError` when either input is not a
13
- * {@link DecimalString}, naming the field.
12
+ * @throws {PerpsError} `ValidationError` when either input does not match the
13
+ * decimal pattern, naming the field.
14
14
  * @public
15
15
  */
16
16
  export function calculateTransferable(