100x-sdk 1.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.
@@ -0,0 +1,378 @@
1
+
2
+ const { MAX_CANDIDATE_INDICES } = require('./utils');
3
+
4
+ // Liquidity reservation ratio - how much liquidity to reserve relative to the last locked liquidity
5
+ const LIQUIDITY_RESERVATION = 100; // 100%
6
+
7
+ // Calculate number of nodes to include before and after the main position
8
+ const CANDIDATE_NODES_EACH_SIDE = Math.floor((MAX_CANDIDATE_INDICES - 1) / 2);
9
+
10
+ /**
11
+ * Transform orders data format
12
+ * @param {Object} ordersData - Raw orders data
13
+ * @returns {Array} Transformed orders array
14
+ */
15
+ function transformOrdersData(ordersData) {
16
+ if (!ordersData || !ordersData.success || !ordersData.data || !ordersData.data.orders) {
17
+ throw new Error('Invalid orders data format');
18
+ }
19
+
20
+ return ordersData.data.orders.map(order => ({
21
+ order_type: order.order_type,
22
+ lock_lp_start_price: BigInt(order.lock_lp_start_price),
23
+ lock_lp_end_price: BigInt(order.lock_lp_end_price),
24
+ lock_lp_sol_amount: order.lock_lp_sol_amount,
25
+ lock_lp_token_amount: order.lock_lp_token_amount,
26
+ index: order.index, // 保留 OrderBook 中的索引
27
+ order_id: order.order_id // 保留订单ID
28
+ }));
29
+ }
30
+
31
+ /**
32
+ * @typedef {Object} Order
33
+ * @property {number} order_type - Order type (e.g., 1 for down_orders, 2 for up_orders).
34
+ * @property {bigint} lock_lp_start_price - Locked liquidity start price.
35
+ * @property {bigint} lock_lp_end_price - Locked liquidity end price.
36
+ * @property {number} lock_lp_sol_amount - 锁定的SOL数量。
37
+ * @property {number} lock_lp_token_amount - 锁定的代币数量。
38
+ */
39
+
40
+ /**
41
+ * @typedef {Object} OverlapResult
42
+ * @property {boolean} no_overlap - 是否没有重叠。`true` 表示没有重叠(可以安全插入),`false` 表示有重叠。
43
+ * @property {number[]} close_insert_indices - 平仓时插入订单簿的位置索引数组。包含主位置索引及其前后3个节点的索引。
44
+ * @property {string} overlap_reason - 重叠原因说明。当没有重叠时为空字符串,有重叠时说明具体原因。
45
+ */
46
+
47
+ /**
48
+ * 检查给定价格区间是否与已排序的订单列表中的任何区间发生重叠,并返回合适的插入位置索引。
49
+ *
50
+ * ## 功能说明
51
+ * 此函数用于保证金交易(long/short)场景,在开仓时需要确定平仓订单应该插入到订单簿(OrderBook)的哪个位置。
52
+ * 函数会检查新订单的价格区间是否与现有订单重叠,并返回多个候选插入位置索引,以提高合约执行成功率。
53
+ *
54
+ * ## 核心逻辑
55
+ * 1. **价格区间检查**:使用二分查找算法在已排序的订单列表中查找合适的插入位置
56
+ * 2. **重叠检测**:
57
+ * - 基础重叠:新区间与现有订单的价格区间直接重叠
58
+ * - 流动性预留重叠:考虑到流动性预留区域(默认100%),防止价格区间过于接近
59
+ * 3. **候选索引生成**:
60
+ * - 主插入位置:逻辑上最合适的插入位置索引
61
+ * - 备选位置:该位置前后各若干个节点的索引(数量由 MAX_CANDIDATE_INDICES 常量决定)
62
+ * - 目的:即使主位置的订单被删除或移动,合约也能找到其他合适位置
63
+ *
64
+ * ## 返回值说明
65
+ * - **无重叠时**:返回 `close_insert_indices` 数组,包含候选插入位置的 OrderBook 索引
66
+ * - 优先级:主位置 → 前1个 → 后1个 → 前2个 → 后2个 → ... → 前N个 → 后N个
67
+ * - 索引数量由 MAX_CANDIDATE_INDICES 常量决定(默认21个,即主位置+前10个+后10个)
68
+ * - **有重叠时**:返回空数组 `[]`,表示无法插入
69
+ * - **空订单簿**:返回 `[65535]` (u16::MAX),表示插入到头部
70
+ *
71
+ * ## 订单类型规则
72
+ * - **down_orders(做多订单)**:价格从高到低排序
73
+ * - lock_lp_start_price > lock_lp_end_price(价格下跌)
74
+ * - 新订单的 end_price 必须 >= 下一个订单的 start_price
75
+ * - **up_orders(做空订单)**:价格从低到高排序
76
+ * - lock_lp_start_price < lock_lp_end_price(价格上涨)
77
+ * - 新订单的 end_price 必须 <= 下一个订单的 start_price
78
+ *
79
+ * @param {'down_orders' | 'up_orders'} order_type - 订单类型
80
+ * - 'down_orders': 做多订单,价格从高到低排序
81
+ * - 'up_orders': 做空订单,价格从低到高排序
82
+ *
83
+ * @param {Order[]} order_list - 已排序的订单对象数组
84
+ * - 每个订单必须包含以下字段:
85
+ * - `index` {number}: 订单在 OrderBook 中的原始索引值(这是合约需要的关键字段)
86
+ * - `lock_lp_start_price` {bigint|string}: 锁定流动池区间起始价格
87
+ * - `lock_lp_end_price` {bigint|string}: 锁定流动池区间结束价格
88
+ * - 数组必须已按价格排序(down_orders 从高到低,up_orders 从低到高)
89
+ * - 通常来自 `sdk.chain.orders()` 或 `sdk.fast.orders()` 的返回数据
90
+ *
91
+ * @param {bigint | number | string} lp_start_price - 新订单的起始价格
92
+ * - 对于 down_orders:这是较高的价格(开仓价附近)
93
+ * - 对于 up_orders:这是较低的价格(止损价附近)
94
+ *
95
+ * @param {bigint | number | string} lp_end_price - 新订单的结束价格
96
+ * - 对于 down_orders:这是较低的价格(止损价附近)
97
+ * - 对于 up_orders:这是较高的价格(开仓价附近)
98
+ *
99
+ * @returns {OverlapResult} 返回包含重叠检查结果和候选插入索引的对象
100
+ * @returns {boolean} returns.no_overlap - 是否没有重叠
101
+ * - `true`: 可以安全插入,使用 `close_insert_indices` 中的索引
102
+ * - `false`: 存在重叠,无法插入
103
+ * @returns {number[]} returns.close_insert_indices - 候选插入位置的 OrderBook 索引数组
104
+ * - 无重叠时:包含主位置及前后3个节点的索引(最多7个)
105
+ * - 有重叠时:空数组 `[]`
106
+ * - 空订单簿时:`[65535]` 表示插入到头部
107
+ * @returns {string} returns.overlap_reason - 重叠原因说明
108
+ * - 无重叠时:空字符串 `""`
109
+ * - 有重叠时:描述具体原因(如 "Overlaps with existing order range")
110
+ *
111
+ * @example
112
+ * // 示例 1: down_orders(做多订单)- 插入到中间位置
113
+ * const downOrders = [
114
+ * { index: 10, lock_lp_start_price: 100n, lock_lp_end_price: 90n }, // 订单1
115
+ * { index: 25, lock_lp_start_price: 80n, lock_lp_end_price: 70n }, // 订单2
116
+ * { index: 33, lock_lp_start_price: 60n, lock_lp_end_price: 50n } // 订单3
117
+ * ];
118
+ *
119
+ * // 检查新订单 [75, 72] 是否可以插入
120
+ * const result = checkPriceRangeOverlap('down_orders', downOrders, 75n, 72n);
121
+ * console.log(result);
122
+ * // 返回: {
123
+ * // no_overlap: true,
124
+ * // close_insert_indices: [25, 10, 33],
125
+ * // // 主位置是 25(订单2),因为新订单应该插入到订单2和订单3之间
126
+ * // // 备选位置:10(订单1在前),33(订单3在后)
127
+ * // overlap_reason: ""
128
+ * // }
129
+ *
130
+ * @example
131
+ * // 示例 2: down_orders - 价格重叠的情况
132
+ * const downOrders = [
133
+ * { index: 10, lock_lp_start_price: 100n, lock_lp_end_price: 90n },
134
+ * { index: 25, lock_lp_start_price: 80n, lock_lp_end_price: 70n }
135
+ * ];
136
+ *
137
+ * // 新订单 [95, 85] 与订单1 [100, 90] 重叠
138
+ * const result = checkPriceRangeOverlap('down_orders', downOrders, 95n, 85n);
139
+ * console.log(result);
140
+ * // 返回: {
141
+ * // no_overlap: false,
142
+ * // close_insert_indices: [],
143
+ * // overlap_reason: "Overlaps with existing order range"
144
+ * // }
145
+ *
146
+ * @example
147
+ * // 示例 3: up_orders(做空订单)- 插入到末尾
148
+ * const upOrders = [
149
+ * { index: 5, lock_lp_start_price: 70n, lock_lp_end_price: 80n },
150
+ * { index: 12, lock_lp_start_price: 90n, lock_lp_end_price: 100n }
151
+ * ];
152
+ *
153
+ * // 新订单 [110, 120] 应该插入到末尾
154
+ * const result = checkPriceRangeOverlap('up_orders', upOrders, 110n, 120n);
155
+ * console.log(result);
156
+ * // 返回: {
157
+ * // no_overlap: true,
158
+ * // close_insert_indices: [12, 5],
159
+ * // // 主位置是 12(订单2),因为新订单应该插入到订单2之后
160
+ * // // 备选位置:5(订单1在前)
161
+ * // overlap_reason: ""
162
+ * // }
163
+ *
164
+ * @example
165
+ * // 示例 4: 空订单簿 - 第一个订单
166
+ * const emptyOrders = [];
167
+ * const result = checkPriceRangeOverlap('down_orders', emptyOrders, 100n, 90n);
168
+ * console.log(result);
169
+ * // 返回: {
170
+ * // no_overlap: true,
171
+ * // close_insert_indices: [65535],
172
+ * // // 65535 是 u16::MAX,表示插入到头部(空订单簿时的特殊值)
173
+ * // overlap_reason: ""
174
+ * // }
175
+ *
176
+ * @example
177
+ * // 示例 5: 实际使用场景 - 做多交易
178
+ * async function openLongPosition(sdk, mint, buyTokenAmount, stopLossPrice) {
179
+ * // 1. 获取 down_orders 数据
180
+ * const ordersData = await sdk.data.orders(mint, { type: 'down_orders' });
181
+ * const orders = ordersData.data.orders;
182
+ *
183
+ * // 2. 获取当前价格
184
+ * const currentPrice = BigInt(await sdk.data.price(mint));
185
+ *
186
+ * // 3. 计算平仓价格区间(模拟)
187
+ * const simulateResult = await sdk.simulator.simulateLongStopLoss(
188
+ * mint,
189
+ * buyTokenAmount,
190
+ * stopLossPrice
191
+ * );
192
+ *
193
+ * // 4. 检查价格区间是否可以插入
194
+ * const overlapCheck = checkPriceRangeOverlap(
195
+ * 'down_orders',
196
+ * orders,
197
+ * simulateResult.close_lp_start_price,
198
+ * simulateResult.close_lp_end_price
199
+ * );
200
+ *
201
+ * if (!overlapCheck.no_overlap) {
202
+ * throw new Error(`无法开仓: ${overlapCheck.overlap_reason}`);
203
+ * }
204
+ *
205
+ * // 5. 使用 close_insert_indices 调用合约
206
+ * const tx = await sdk.trading.long({
207
+ * mint,
208
+ * buyTokenAmount,
209
+ * maxSolAmount,
210
+ * marginSolMax,
211
+ * closePrice: stopLossPrice,
212
+ * closeInsertIndices: overlapCheck.close_insert_indices // 传递给合约
213
+ * });
214
+ *
215
+ * return tx;
216
+ * }
217
+ *
218
+ * @throws {Error} 当输入的起始和结束价格与订单类型规则不匹配时抛出错误
219
+ *
220
+ * @see {@link https://github.com/your-repo/docs/orderbook.md|OrderBook 文档}
221
+ * @see {@link transformOrdersData} 数据格式转换函数
222
+ *
223
+ * @since 2.0.0
224
+ * @version 2.0.0 - 从返回 prev_order_pda/next_order_pda 改为返回 close_insert_indices
225
+ */
226
+ function checkPriceRangeOverlap(order_type, order_list, lp_start_price, lp_end_price) {
227
+ // console.log("checkPriceRangeOverlap=",order_type,lp_start_price, lp_end_price)
228
+
229
+ const startPrice = BigInt(lp_start_price);
230
+ const endPrice = BigInt(lp_end_price);
231
+
232
+ // If order list is empty, return u16::MAX to indicate insertion at head
233
+ if (order_list.length === 0) {
234
+ return { no_overlap: true, close_insert_indices: [65535], overlap_reason: "" };
235
+ }
236
+
237
+ const isDown = order_type === 'down_orders';
238
+
239
+ // 验证并规范化输入价格区间,确保 minPrice <= maxPrice
240
+ if ((isDown && startPrice < endPrice) || (!isDown && startPrice > endPrice)) {
241
+ throw new Error('输入的起始和结束价格与订单类型规则不匹配。');
242
+ }
243
+ const minPrice = isDown ? endPrice : startPrice;
244
+ const maxPrice = isDown ? startPrice : endPrice;
245
+
246
+ let low = 0;
247
+ let high = order_list.length - 1;
248
+ let insertionIndex = order_list.length; // 默认插入到最后
249
+
250
+ while (low <= high) {
251
+ const mid = Math.floor((low + high) / 2);
252
+ const order = order_list[mid];
253
+ const orderStart = BigInt(order.lock_lp_start_price);
254
+ const orderEnd = BigInt(order.lock_lp_end_price);
255
+
256
+ const orderMin = isDown ? orderEnd : orderStart;
257
+ const orderMax = isDown ? orderStart : orderEnd;
258
+
259
+ // 核心重叠判断: (StartA < EndB) and (EndA > StartB)
260
+ if (minPrice < orderMax && maxPrice > orderMin) {
261
+ // 发生基础重叠
262
+ return {
263
+ no_overlap: false,
264
+ close_insert_indices: [],
265
+ overlap_reason: "Overlaps with existing order range"
266
+ };
267
+ }
268
+
269
+ if (isDown) {
270
+ // down_orders: 价格从大到小 (orderMax 递减)
271
+ if (maxPrice > orderMax) { // 新区间在当前区间的“左边”(价格更高)
272
+ insertionIndex = mid;
273
+ high = mid - 1;
274
+ } else {
275
+ low = mid + 1;
276
+ }
277
+ } else {
278
+ // up_orders: 价格从小到大 (orderMin 递增)
279
+ if (minPrice < orderMin) { // 新区间在当前区间的“左边”(价格更低)
280
+ insertionIndex = mid;
281
+ high = mid - 1;
282
+ } else {
283
+ low = mid + 1;
284
+ }
285
+ }
286
+ }
287
+
288
+ // 根据找到的插入点,确定逻辑上的前后订单
289
+ // insertionIndex 是新区间应该插入的位置,使得列表依然有序
290
+ const nextOrder = order_list[insertionIndex] || null;
291
+ const prevOrder = order_list[insertionIndex - 1] || null;
292
+
293
+ // 检查流动性预留重叠
294
+ function checkLiquidityReservationOverlap(checkOrder) {
295
+ if (!checkOrder) return false;
296
+
297
+ const orderStart = BigInt(checkOrder.lock_lp_start_price);
298
+ const orderEnd = BigInt(checkOrder.lock_lp_end_price);
299
+ const orderMin = isDown ? orderEnd : orderStart;
300
+ const orderMax = isDown ? orderStart : orderEnd;
301
+
302
+ // 计算扩大区间值
303
+ const expansionAmount = (orderMax - orderMin) * BigInt(Math.floor(LIQUIDITY_RESERVATION)) / 100n;
304
+
305
+ let hasOverlap;
306
+ if (isDown) {
307
+ // down_orders: start不变,end向下扩大
308
+ const expandedEnd = orderMin - expansionAmount;
309
+ hasOverlap = startPrice >= expandedEnd;
310
+ } else {
311
+ // up_orders: start不变,end向上扩大
312
+ const expandedEnd = orderMax + expansionAmount;
313
+ hasOverlap = startPrice <= expandedEnd;
314
+ }
315
+
316
+ return hasOverlap;
317
+ }
318
+
319
+ // 检查与前一个订单的流动性预留重叠
320
+ if (prevOrder && checkLiquidityReservationOverlap(prevOrder)) {
321
+ return {
322
+ no_overlap: false,
323
+ close_insert_indices: [],
324
+ overlap_reason: "Overlaps with previous order's liquidity reservation range"
325
+ };
326
+ }
327
+
328
+ // 无重叠,构建 close_insert_indices 数组
329
+ // 优先级:主位置 → 前1个 → 后1个 → 前2个 → 后2个 → 前3个 → 后3个
330
+ const indices = [];
331
+
332
+ // 主插入位置逻辑:
333
+ // - down_orders (价格从高到低): 插入到 prevOrder 之后
334
+ // - 如果没有 prevOrder (insertionIndex=0),说明价格最高,使用 u16::MAX 插入头部
335
+ // - 如果有 prevOrder,使用 prevOrder.index,插入到它后面
336
+ // - up_orders (价格从低到高): 插入到 prevOrder 之后
337
+ // - 如果没有 prevOrder (insertionIndex=0),说明价格最低,使用 u16::MAX 插入头部
338
+ // - 如果有 prevOrder,使用 prevOrder.index,插入到它后面
339
+
340
+ if (prevOrder && prevOrder.index !== undefined) {
341
+ // 有前置订单,插入到它后面
342
+ indices.push(prevOrder.index);
343
+ } else {
344
+ // 没有前置订单 (insertionIndex=0)
345
+ // down_orders: 价格最高,插入头部 (65535)
346
+ // up_orders: 价格最低,插入头部 (65535)
347
+ indices.push(65535); // u16::MAX - 插入到头部
348
+ }
349
+
350
+ // 添加前后节点的索引
351
+ // 根据 MAX_CANDIDATE_INDICES 常量计算需要添加多少个前后节点
352
+ for (let offset = 1; offset <= CANDIDATE_NODES_EACH_SIDE; offset++) {
353
+ // 添加前面第 offset 个节点
354
+ const beforeIndex = insertionIndex - 1 - offset;
355
+ if (beforeIndex >= 0 && order_list[beforeIndex] && order_list[beforeIndex].index !== undefined) {
356
+ indices.push(order_list[beforeIndex].index);
357
+ }
358
+
359
+ // 添加后面第 offset 个节点
360
+ // offset=1 应该是 nextOrder (insertionIndex),offset=2 是 insertionIndex+1,以此类推
361
+ const afterIndex = insertionIndex + offset - 1;
362
+ if (afterIndex < order_list.length && order_list[afterIndex] && order_list[afterIndex].index !== undefined) {
363
+ indices.push(order_list[afterIndex].index);
364
+ }
365
+ }
366
+
367
+ return {
368
+ no_overlap: true,
369
+ close_insert_indices: indices,
370
+ overlap_reason: ""
371
+ };
372
+ }
373
+
374
+
375
+ module.exports = {
376
+ transformOrdersData,
377
+ checkPriceRangeOverlap
378
+ };
@@ -0,0 +1,63 @@
1
+
2
+
3
+ // Liquidity reservation ratio - how much liquidity to reserve relative to the last locked liquidity
4
+ const LIQUIDITY_RESERVATION = 100; // 100%
5
+
6
+ // Price adjustment percentage
7
+ const PRICE_ADJUSTMENT_PERCENTAGE = 15; // 5就是 0.5%
8
+
9
+ // Minimum stop loss percentage - stop loss price must be at least this far from current price
10
+ // 最小止损百分比 - 止损价格必须与当前价格至少相差此百分比
11
+ // Example: 40 means 4.0% (calculation: 40/1000 = 0.04 = 4%)
12
+ const MIN_STOP_LOSS_PERCENT = 40; // 4.0%
13
+
14
+ // Maximum number of candidate indices to include in close_insert_indices
15
+ // This represents: 1 main position + N nodes before + N nodes after
16
+ // Must be an odd number >= 1 (e.g., 41 = 1 main + 20 before + 20 after)
17
+ // The contract accepts up to 41
18
+ const MAX_CANDIDATE_INDICES = 19;
19
+
20
+
21
+ // Validate MAX_CANDIDATE_INDICES constant
22
+ if (MAX_CANDIDATE_INDICES < 1 || MAX_CANDIDATE_INDICES % 2 === 0) {
23
+ throw new Error(`MAX_CANDIDATE_INDICES must be an odd number >= 1, got ${MAX_CANDIDATE_INDICES}`);
24
+ }
25
+
26
+
27
+ /**
28
+ * Convert API order format to expected format
29
+ * @param {Array} apiOrders - Orders returned from API
30
+ * @returns {Array} Converted order list
31
+ */
32
+ function convertApiOrdersFormat(apiOrders) {
33
+ if (!apiOrders || !Array.isArray(apiOrders)) {
34
+ return [];
35
+ }
36
+
37
+ return apiOrders.map(order => ({
38
+ ...order,
39
+ lockLpStartPrice: order.lock_lp_start_price,
40
+ lockLpEndPrice: order.lock_lp_end_price
41
+ }));
42
+ }
43
+
44
+
45
+ /**
46
+ * Handle BigInt absolute value
47
+ * @param {BigInt} value - BigInt value to calculate absolute value
48
+ * @returns {BigInt} Absolute value result
49
+ */
50
+ function absoluteValue(value) {
51
+ return value < 0n ? -value : value;
52
+ }
53
+
54
+
55
+
56
+ module.exports = {
57
+ convertApiOrdersFormat,
58
+ absoluteValue,
59
+ LIQUIDITY_RESERVATION,
60
+ PRICE_ADJUSTMENT_PERCENTAGE,
61
+ MIN_STOP_LOSS_PERCENT,
62
+ MAX_CANDIDATE_INDICES
63
+ };