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,1028 @@
1
+
2
+ const Decimal = require('decimal.js');
3
+ const CurveAMM = require('../../utils/curve_amm');
4
+ const {transformOrdersData , checkPriceRangeOverlap} = require('./stop_loss_utils')
5
+ const { PRICE_ADJUSTMENT_PERCENTAGE, MIN_STOP_LOSS_PERCENT } = require('./utils');
6
+ const JSONbig = require('json-bigint')({ storeAsString: false });
7
+
8
+ /**
9
+ * Simulate long position stop loss calculation
10
+ *
11
+ * 模拟做多仓位的止损计算,返回可执行的止损价格和相关参数。
12
+ * 该函数会自动调整止损价格以避免与现有订单的价格区间重叠,
13
+ * 并返回合约执行时需要的插入位置索引数组。
14
+ *
15
+ * @param {string} mint - Token address / 代币地址
16
+ * @param {bigint|string|number} buyTokenAmount - Token amount to buy for long position (u64 format, precision 10^9) / 做多买入的代币数量 (u64格式, 精度 10^9)
17
+ * @param {bigint|string|number} stopLossPrice - User desired stop loss price (u128 format) / 用户期望的止损价格 (u128格式)
18
+ * @param {Object|null} lastPrice - Token info, default null / 代币当前价格信息,默认null会自动获取
19
+ * @param {Object|null} ordersData - Orders data, default null / 订单数据,默认null会自动获取
20
+ * @param {number} borrowFee - Borrow fee rate, default 2000 (2000/100000 = 0.02%) / 借贷手续费率,默认2000 (2000/100000 = 0.02%)
21
+ *
22
+ * @returns {Promise<Object>} Stop loss analysis result / 止损分析结果对象
23
+ * @returns {bigint} returns.executableStopLossPrice - 计算出的可执行止损价格 (u128格式)
24
+ * - 这是经过调整后不与现有订单重叠的止损价格
25
+ * - 可能低于用户输入的 stopLossPrice (因为需要避免重叠)
26
+ * - 可以直接用于调用 sdk.trading.long() 的 closePrice 参数
27
+ *
28
+ * @returns {bigint} returns.tradeAmount - 止损时预计卖出获得的SOL数量 (lamports)
29
+ * - 这是在 executableStopLossPrice 价格卖出 buyTokenAmount 代币能获得的SOL
30
+ * - 不包含手续费扣除
31
+ * - 用于估算止损时的收益
32
+ *
33
+ * @returns {number} returns.stopLossPercentage - 止损百分比 (相对于当前价格)
34
+ * - 计算公式: ((currentPrice - executableStopLossPrice) / currentPrice) * 100
35
+ * - 例如: 3.5 表示止损价格比当前价格低3.5%
36
+ * - 做多时这个值应该是正数 (止损价低于当前价)
37
+ *
38
+ * @returns {number} returns.leverage - 杠杆倍数
39
+ * - 计算公式: currentPrice / (currentPrice - executableStopLossPrice)
40
+ * - 例如: 28.57 表示约28.57倍杠杆
41
+ * - 杠杆越高,风险越大,但潜在收益也越大
42
+ *
43
+ * @returns {bigint} returns.currentPrice - 当前价格 (u128格式)
44
+ * - 计算时使用的代币当前价格
45
+ * - 用于参考和验证
46
+ *
47
+ * @returns {number} returns.iterations - 价格调整迭代次数
48
+ * - 为了避免价格区间重叠,函数自动调整止损价格的次数
49
+ * - 每次调整会将价格降低 PRICE_ADJUSTMENT_PERCENTAGE (默认0.5%)
50
+ * - 如果迭代次数过高,可能需要重新选择止损价格
51
+ *
52
+ * @returns {bigint} returns.originalStopLossPrice - 用户输入的原始止损价格 (u128格式)
53
+ * - 用于对比调整前后的价格差异
54
+ * - 如果 executableStopLossPrice 与此差异较大,说明现有订单较密集
55
+ *
56
+ * @returns {number[]} returns.close_insert_indices - 平仓订单插入位置的候选索引数组 ⭐ 新增
57
+ * - 数组包含多个候选插入位置的 OrderBook 索引值
58
+ * - 结构: [主位置index, 前1个index, 后1个index, 前2个index, 后2个index, 前3个index, 后3个index]
59
+ * - 例如: [25, 10, 33, 5, 40, 2, 50] 表示主位置是索引25,备选位置包括索引10、33等
60
+ * - 最多包含7个索引值 (1个主位置 + 前3个 + 后3个)
61
+ * - 如果订单簿为空,返回 [65535] (u16::MAX,表示插入到头部)
62
+ * - 用途: 传递给 sdk.trading.long() 的 closeInsertIndices 参数
63
+ * - 提高成功率: 即使主位置的订单被删除,合约也能尝试其他候选位置
64
+ *
65
+ * @returns {bigint} returns.estimatedMargin - 预估所需保证金 (SOL lamports)
66
+ * - 计算公式: 买入成本 - 平仓收益(扣除手续费后)
67
+ * - 这是执行此止损策略需要的最少保证金
68
+ * - 可以用于 sdk.trading.long() 的 marginSolMax 参数
69
+ * - 实际调用时建议增加10-20%余量以应对价格波动
70
+ *
71
+ * @throws {Error} 当缺少必需参数时
72
+ * @throws {Error} 当无法获取价格或订单数据时
73
+ * @throws {Error} 当达到最大迭代次数仍无法找到合适的止损价格时
74
+ * @throws {Error} 当价格调整后变为负数时
75
+ *
76
+ * @example
77
+ * // 基础用法: 做多1个代币,止损价格为当前价格的97%
78
+ * const result = await sdk.simulator.simulateLongStopLoss(
79
+ * '4Kq51Kt48FCwdo5CeKjRVPodH1ticHa7mZ5n5gqMEy1X', // mint
80
+ * 1000000000n, // 1 token (精度10^9)
81
+ * BigInt('97000000000000000000') // 止损价格
82
+ * );
83
+ *
84
+ * console.log(`可执行止损价格: ${result.executableStopLossPrice}`);
85
+ * console.log(`止损百分比: ${result.stopLossPercentage}%`);
86
+ * console.log(`杠杆倍数: ${result.leverage}x`);
87
+ * console.log(`预估保证金: ${result.estimatedMargin} lamports`);
88
+ * console.log(`插入位置索引: ${result.close_insert_indices}`);
89
+ *
90
+ * @example
91
+ * // 完整使用流程: 模拟后执行做多交易
92
+ * async function openLongPosition(sdk, mint, buyTokenAmount, stopLossPrice) {
93
+ * // 1. 模拟止损计算
94
+ * const simulation = await sdk.simulator.simulateLongStopLoss(
95
+ * mint,
96
+ * buyTokenAmount,
97
+ * stopLossPrice
98
+ * );
99
+ *
100
+ * // 2. 检查止损价格是否被大幅调整
101
+ * const priceDiff = Number((simulation.originalStopLossPrice - simulation.executableStopLossPrice) * 10000n / simulation.originalStopLossPrice) / 100;
102
+ * if (priceDiff > 1.0) {
103
+ * console.warn(`止损价格被调整了 ${priceDiff}%, 当前订单较密集`);
104
+ * }
105
+ *
106
+ * // 3. 准备交易参数
107
+ * const maxSolAmount = simulation.estimatedMargin * 120n / 100n; // 增加20%余量
108
+ * const marginSolMax = simulation.estimatedMargin * 115n / 100n; // 增加15%余量
109
+ *
110
+ * // 4. 执行做多交易
111
+ * const tx = await sdk.trading.long({
112
+ * mint: mint,
113
+ * buyTokenAmount: buyTokenAmount,
114
+ * maxSolAmount: maxSolAmount,
115
+ * marginSolMax: marginSolMax,
116
+ * closePrice: simulation.executableStopLossPrice,
117
+ * closeInsertIndices: simulation.close_insert_indices // ⭐ 使用新的索引数组
118
+ * });
119
+ *
120
+ * return tx;
121
+ * }
122
+ *
123
+ * @see {@link simulateShortStopLoss} 做空仓位的止损计算
124
+ * @see {@link simulateLongSolStopLoss} 基于SOL金额的做多止损计算
125
+ * @since 2.0.0
126
+ * @version 2.0.0 - 从返回 prev_order_pda/next_order_pda 改为返回 close_insert_indices
127
+ */
128
+ async function simulateLongStopLoss(mint, buyTokenAmount, stopLossPrice, lastPrice = null, ordersData = null, borrowFee = null, initialVirtualSol = null, initialVirtualToken = null) {
129
+ try {
130
+ // Parameter validation
131
+ if (!mint || !buyTokenAmount || !stopLossPrice) {
132
+ throw new Error('Missing required parameters');
133
+ }
134
+
135
+ // 如果没有传入 borrowFee 或池子参数,从链上一次性获取
136
+ if (borrowFee === null || initialVirtualSol === null || initialVirtualToken === null) {
137
+ const curveAccount = await this.sdk.chain.getCurveAccount(mint, { skipBalances: true });
138
+ if (borrowFee === null) borrowFee = curveAccount.borrowFee;
139
+ // 链上返回的是 u64 原始单位(lamports/最小单位),需要除以 10^9 转为人类可读单位
140
+ // 与 calcLiq.js 中的转换方式一致
141
+ if (initialVirtualSol === null) initialVirtualSol = new Decimal(curveAccount.initialVirtualSol.toString()).div(CurveAMM.SOL_PRECISION_FACTOR_DECIMAL).toString();
142
+ if (initialVirtualToken === null) initialVirtualToken = new Decimal(curveAccount.initialVirtualToken.toString()).div(CurveAMM.TOKEN_PRECISION_FACTOR_DECIMAL).toString();
143
+ }
144
+
145
+ // Get current price
146
+ if (!lastPrice) {
147
+ //console.log('Getting current price...');
148
+ lastPrice = await this.sdk.data.price(mint);
149
+ if (!lastPrice) {
150
+ throw new Error('Failed to get current price');
151
+ }
152
+ }
153
+ //console.log("simulateLongStopLoss lastPrice=",lastPrice)
154
+
155
+ // Get ordersData
156
+ if (!ordersData) {
157
+ //console.log('Getting orders data...');
158
+ ordersData = await this.sdk.data.orders(mint, { type: 'down_orders' });
159
+ if (!ordersData || !ordersData.success) {
160
+ throw new Error('Failed to get orders data');
161
+ }
162
+ }
163
+
164
+ //console.log("ordersData=", JSONbig.stringify(ordersData, null, 2))
165
+ //console.log("ordersData len=", ordersData.data.orders.length)
166
+
167
+ // Calculate current price
168
+ let currentPrice;
169
+ if (lastPrice === null || lastPrice === undefined || lastPrice === '0') {
170
+ //console.log('Current price is empty, using initial price');
171
+ currentPrice = CurveAMM.getInitialPrice();
172
+ } else {
173
+ currentPrice = BigInt(lastPrice);
174
+ if (!currentPrice || currentPrice === 0n) {
175
+ //console.log('Current price is 0, using initial price');
176
+ currentPrice = CurveAMM.getInitialPrice();
177
+ }
178
+ }
179
+
180
+
181
+ // Transform orders data
182
+ const downOrders = transformOrdersData(ordersData);
183
+ //console.log(`downOrders Found ${downOrders.length} existing long orders`);
184
+ //console.log("downOrders downOrders=",downOrders)
185
+
186
+ // Initialize stop loss prices
187
+ let stopLossStartPrice = BigInt(stopLossPrice);
188
+ let stopLossEndPrice;
189
+ let maxIterations = 1000; // Prevent infinite loop
190
+ let iteration = 0;
191
+ let finalOverlapResult = null; // Record final overlap result
192
+ let finalTradeAmount = 0n; // Record final trade amount
193
+
194
+ // 检查并调整止损价格以满足最小距离要求 (做多: 止损价必须低于当前价至少 MIN_STOP_LOSS_PERCENT)
195
+ // Check and adjust stop loss price to meet minimum distance requirement (long: stop loss must be below current price by at least MIN_STOP_LOSS_PERCENT)
196
+ const minAllowedStopLoss = currentPrice - (currentPrice * BigInt(MIN_STOP_LOSS_PERCENT)) / 1000n;
197
+ if (stopLossStartPrice > minAllowedStopLoss) {
198
+ const originalStopLoss = stopLossStartPrice;
199
+ stopLossStartPrice = minAllowedStopLoss;
200
+ const originalPercent = Number((currentPrice - originalStopLoss) * 1000n / currentPrice) / 10;
201
+ const adjustedPercent = Number((currentPrice - stopLossStartPrice) * 1000n / currentPrice) / 10;
202
+ console.log(`止损价格自动调整以满足最小距离要求:`);
203
+ console.log(` 原始止损距离: ${originalPercent.toFixed(2)}%`);
204
+ console.log(` 调整后距离: ${adjustedPercent.toFixed(2)}% (最小要求: ${Number(MIN_STOP_LOSS_PERCENT) / 10}%)`);
205
+ console.log(` 原始止损价: ${originalStopLoss}`);
206
+ console.log(` 调整后止损价: ${stopLossStartPrice}`);
207
+ }
208
+
209
+ //console.log(`Start price: ${stopLossStartPrice}, Target token amount: ${buyTokenAmount}`);
210
+
211
+ // Loop to adjust stop loss price until no overlap
212
+ while (iteration < maxIterations) {
213
+ iteration++;
214
+
215
+ // // Calculate stop loss end price
216
+ // console.log(`[Long Stop Loss Debug] Iteration ${iteration}:`);
217
+ // console.log(` - stopLossStartPrice: ${stopLossStartPrice.toString()}`);
218
+ // console.log(` - buyTokenAmount: ${buyTokenAmount.toString()}`);
219
+ // console.log(` - Calling CurveAMM.sellFromPriceWithTokenInput...`);
220
+
221
+ const tradeResult = CurveAMM.sellFromPriceWithTokenInputWithParams(stopLossStartPrice, buyTokenAmount, initialVirtualSol, initialVirtualToken);
222
+
223
+ //console.log(` - tradeResult:`, tradeResult);
224
+
225
+ if (!tradeResult) {
226
+ console.error(`[Long Stop Loss Error] Failed at iteration ${iteration}`);
227
+ console.error(` - stopLossStartPrice: ${stopLossStartPrice.toString()}`);
228
+ console.error(` - buyTokenAmount: ${buyTokenAmount.toString()}`);
229
+ throw new Error('Failed to calculate stop loss end price');
230
+ }
231
+
232
+ stopLossEndPrice = tradeResult[0]; // Price after trade completion
233
+ const tradeAmount = tradeResult[1]; // SOL输出量 / SOL output amount
234
+
235
+ // console.log(` - stopLossEndPrice: ${stopLossEndPrice.toString()}`);
236
+ // console.log(` - tradeAmount: ${tradeAmount.toString()}`);
237
+
238
+ //console.log(`迭代 ${iteration}: 起始价格=${stopLossStartPrice}, 结束价格=${stopLossEndPrice}, SOL输出量=${tradeAmount} / Iteration ${iteration}: Start=${stopLossStartPrice}, End=${stopLossEndPrice}, SOL output=${tradeAmount}`);
239
+
240
+ // 检查价格区间重叠 / Check price range overlap
241
+ const overlapResult = checkPriceRangeOverlap('down_orders', downOrders, stopLossStartPrice, stopLossEndPrice);
242
+
243
+ if (overlapResult.no_overlap) {
244
+ //console.log('价格区间无重叠,可以执行 / No price range overlap, can execute');
245
+ finalOverlapResult = overlapResult; // 记录最终的overlap结果 / Record final overlap result
246
+ finalTradeAmount = tradeAmount; // 记录最终的交易金额 / Record final trade amount
247
+ break;
248
+ }
249
+
250
+ //console.log(`发现重叠: ${overlapResult.overlap_reason} / Found overlap: ${overlapResult.overlap_reason}`);
251
+
252
+ // 调整起始价格(减少0.5%)/ Adjust start price (decrease by 0.5%)
253
+ // 使用方案2:直接计算 0.5% = 5/1000
254
+ const adjustmentAmount = (stopLossStartPrice * BigInt(PRICE_ADJUSTMENT_PERCENTAGE)) / 1000n;
255
+ stopLossStartPrice = stopLossStartPrice - adjustmentAmount;
256
+
257
+ //console.log(`调整后起始价格: ${stopLossStartPrice} / Adjusted start price: ${stopLossStartPrice}`);
258
+
259
+ // 安全检查:确保价格不会变成负数 / Safety check: ensure price doesn't become negative
260
+ if (stopLossStartPrice <= 0n) {
261
+ throw new Error('止损价格调整后变为负数,无法继续 / Stop loss price became negative after adjustment');
262
+ }
263
+ }
264
+
265
+ if (iteration >= maxIterations) {
266
+ throw new Error('达到最大迭代次数,无法找到合适的止损价格 / Reached maximum iterations, cannot find suitable stop loss price');
267
+ }
268
+
269
+ // 计算最终返回值 / Calculate final return values
270
+ const executableStopLossPrice = stopLossStartPrice;
271
+
272
+ // 计算止损百分比 / Calculate stop loss percentage
273
+ let stopLossPercentage = 0;
274
+ let leverage = 1;
275
+
276
+ if (currentPrice !== executableStopLossPrice) {
277
+ stopLossPercentage = Number((BigInt(10000) * (currentPrice - executableStopLossPrice)) / currentPrice) / 100;
278
+ leverage = Number((BigInt(10000) * currentPrice) / (currentPrice - executableStopLossPrice)) / 10000;
279
+ }
280
+
281
+ // 计算保证金 / Calculate margin requirement
282
+ let estimatedMargin = 0n;
283
+ try {
284
+ // 1. 计算从当前价格买入所需的SOL
285
+ const buyResult = CurveAMM.buyFromPriceWithTokenOutputWithParams(currentPrice, buyTokenAmount, initialVirtualSol, initialVirtualToken);
286
+ if (buyResult) {
287
+ const requiredSol = buyResult[1]; // SOL input amount
288
+
289
+ // 2. 计算平仓时扣除手续费后的收益
290
+ const closeOutputSolAfterFee = CurveAMM.calculateAmountAfterFee(finalTradeAmount, borrowFee);
291
+
292
+ // 3. 计算保证金 = 买入成本 - 平仓收益(扣费后)
293
+ if (closeOutputSolAfterFee !== null && requiredSol > closeOutputSolAfterFee) {
294
+ estimatedMargin = requiredSol - closeOutputSolAfterFee;
295
+ }
296
+ }
297
+ } catch (marginError) {
298
+ console.warn('Failed to calculate estimated margin:', marginError.message);
299
+ // Keep estimatedMargin as 0n
300
+ }
301
+
302
+ // console.log(`Calculation completed:`);
303
+ // console.log(` Executable stop loss price: ${executableStopLossPrice}`);
304
+ // console.log(` SOL output amount: ${finalTradeAmount}`);
305
+ // console.log(` Stop loss percentage: ${stopLossPercentage}%`);
306
+ // console.log(` Leverage: ${leverage}x`);
307
+ // console.log(` Close insert indices: ${finalOverlapResult.close_insert_indices}`);
308
+
309
+ return {
310
+ executableStopLossPrice: executableStopLossPrice, // Calculated reasonable stop loss value
311
+ tradeAmount: finalTradeAmount, // SOL output amount
312
+ stopLossPercentage: stopLossPercentage, // Stop loss percentage relative to current price
313
+ leverage: leverage, // Leverage ratio
314
+ currentPrice: currentPrice, // Current price
315
+ iterations: iteration, // Number of adjustments
316
+ originalStopLossPrice: BigInt(stopLossPrice), // Original stop loss price
317
+ close_insert_indices: finalOverlapResult.close_insert_indices, // Candidate insertion indices for closing order
318
+ estimatedMargin: estimatedMargin // Estimated margin requirement in SOL (lamports)
319
+ };
320
+
321
+ } catch (error) {
322
+ console.error('Failed to simulate stop loss calculation:', error.message);
323
+ throw error;
324
+ }
325
+ }
326
+
327
+
328
+ /**
329
+ * Simulate short position stop loss calculation
330
+ *
331
+ * 模拟做空仓位的止损计算,返回可执行的止损价格和相关参数。
332
+ * 该函数会自动调整止损价格以避免与现有订单的价格区间重叠,
333
+ * 并返回合约执行时需要的插入位置索引数组。
334
+ *
335
+ * @param {string} mint - Token address / 代币地址
336
+ * @param {bigint|string|number} sellTokenAmount - Token amount to sell for short position (u64 format, precision 10^9) / 做空卖出的代币数量 (u64格式, 精度 10^9)
337
+ * @param {bigint|string|number} stopLossPrice - User desired stop loss price (u128 format) / 用户期望的止损价格 (u128格式)
338
+ * @param {Object|null} lastPrice - Token info, default null / 代币当前价格信息,默认null会自动获取
339
+ * @param {Object|null} ordersData - Orders data, default null / 订单数据,默认null会自动获取
340
+ * @param {number} borrowFee - Borrow fee rate, default 2000 (2000/100000 = 0.02%) / 借贷手续费率,默认2000 (2000/100000 = 0.02%)
341
+ *
342
+ * @returns {Promise<Object>} Stop loss analysis result / 止损分析结果对象
343
+ * @returns {bigint} returns.executableStopLossPrice - 计算出的可执行止损价格 (u128格式)
344
+ * - 这是经过调整后不与现有订单重叠的止损价格
345
+ * - 可能高于用户输入的 stopLossPrice (因为需要避免重叠)
346
+ * - 可以直接用于调用 sdk.trading.short() 的 closePrice 参数
347
+ *
348
+ * @returns {bigint} returns.tradeAmount - 止损时预计买入需要的SOL数量 (lamports)
349
+ * - 这是在 executableStopLossPrice 价格买回 sellTokenAmount 代币需要的SOL
350
+ * - 不包含手续费
351
+ * - 用于估算止损时的成本
352
+ *
353
+ * @returns {number} returns.stopLossPercentage - 止损百分比 (相对于当前价格)
354
+ * - 计算公式: ((executableStopLossPrice - currentPrice) / currentPrice) * 100
355
+ * - 例如: 3.5 表示止损价格比当前价格高3.5%
356
+ * - 做空时这个值应该是正数 (止损价高于当前价)
357
+ *
358
+ * @returns {number} returns.leverage - 杠杆倍数
359
+ * - 计算公式: currentPrice / (executableStopLossPrice - currentPrice)
360
+ * - 例如: 28.57 表示约28.57倍杠杆
361
+ * - 杠杆越高,风险越大,但潜在收益也越大
362
+ *
363
+ * @returns {bigint} returns.currentPrice - 当前价格 (u128格式)
364
+ * - 计算时使用的代币当前价格
365
+ * - 用于参考和验证
366
+ *
367
+ * @returns {number} returns.iterations - 价格调整迭代次数
368
+ * - 为了避免价格区间重叠,函数自动调整止损价格的次数
369
+ * - 每次调整会将价格提高 PRICE_ADJUSTMENT_PERCENTAGE (默认0.5%)
370
+ * - 如果迭代次数过高,可能需要重新选择止损价格
371
+ *
372
+ * @returns {bigint} returns.originalStopLossPrice - 用户输入的原始止损价格 (u128格式)
373
+ * - 用于对比调整前后的价格差异
374
+ * - 如果 executableStopLossPrice 与此差异较大,说明现有订单较密集
375
+ *
376
+ * @returns {number[]} returns.close_insert_indices - 平仓订单插入位置的候选索引数组 ⭐ 新增
377
+ * - 数组包含多个候选插入位置的 OrderBook 索引值
378
+ * - 结构: [主位置index, 前1个index, 后1个index, 前2个index, 后2个index, 前3个index, 后3个index]
379
+ * - 例如: [25, 10, 33, 5, 40, 2, 50] 表示主位置是索引25,备选位置包括索引10、33等
380
+ * - 最多包含7个索引值 (1个主位置 + 前3个 + 后3个)
381
+ * - 如果订单簿为空,返回 [65535] (u16::MAX,表示插入到头部)
382
+ * - 用途: 传递给 sdk.trading.short() 的 closeInsertIndices 参数
383
+ * - 提高成功率: 即使主位置的订单被删除,合约也能尝试其他候选位置
384
+ *
385
+ * @returns {bigint} returns.estimatedMargin - 预估所需保证金 (SOL lamports)
386
+ * - 计算公式: 平仓成本(含手续费) - 开仓收益 - 开仓手续费
387
+ * - 这是执行此止损策略需要的最少保证金
388
+ * - 可以用于 sdk.trading.short() 的 marginSolMax 参数
389
+ * - 实际调用时建议增加10-20%余量以应对价格波动
390
+ *
391
+ * @throws {Error} 当缺少必需参数时
392
+ * @throws {Error} 当无法获取价格或订单数据时
393
+ * @throws {Error} 当达到最大迭代次数仍无法找到合适的止损价格时
394
+ * @throws {Error} 当价格调整后超过最大值时
395
+ *
396
+ * @example
397
+ * // 基础用法: 做空1个代币,止损价格为当前价格的103%
398
+ * const result = await sdk.simulator.simulateShortStopLoss(
399
+ * '4Kq51Kt48FCwdo5CeKjRVPodH1ticHa7mZ5n5gqMEy1X', // mint
400
+ * 1000000000n, // 1 token (精度10^9)
401
+ * BigInt('103000000000000000000') // 止损价格
402
+ * );
403
+ *
404
+ * console.log(`可执行止损价格: ${result.executableStopLossPrice}`);
405
+ * console.log(`止损百分比: ${result.stopLossPercentage}%`);
406
+ * console.log(`杠杆倍数: ${result.leverage}x`);
407
+ * console.log(`预估保证金: ${result.estimatedMargin} lamports`);
408
+ * console.log(`插入位置索引: ${result.close_insert_indices}`);
409
+ *
410
+ * @example
411
+ * // 完整使用流程: 模拟后执行做空交易
412
+ * async function openShortPosition(sdk, mint, sellTokenAmount, stopLossPrice) {
413
+ * // 1. 模拟止损计算
414
+ * const simulation = await sdk.simulator.simulateShortStopLoss(
415
+ * mint,
416
+ * sellTokenAmount,
417
+ * stopLossPrice
418
+ * );
419
+ *
420
+ * // 2. 检查止损价格是否被大幅调整
421
+ * const priceDiff = Number((simulation.executableStopLossPrice - simulation.originalStopLossPrice) * 10000n / simulation.originalStopLossPrice) / 100;
422
+ * if (priceDiff > 1.0) {
423
+ * console.warn(`止损价格被调整了 ${priceDiff}%, 当前订单较密集`);
424
+ * }
425
+ *
426
+ * // 3. 准备交易参数
427
+ * const minSolOutput = simulation.tradeAmount * 80n / 100n; // 至少获得80%
428
+ * const marginSolMax = simulation.estimatedMargin * 115n / 100n; // 增加15%余量
429
+ *
430
+ * // 4. 执行做空交易
431
+ * const tx = await sdk.trading.short({
432
+ * mint: mint,
433
+ * borrowSellTokenAmount: sellTokenAmount,
434
+ * minSolOutput: minSolOutput,
435
+ * marginSolMax: marginSolMax,
436
+ * closePrice: simulation.executableStopLossPrice,
437
+ * closeInsertIndices: simulation.close_insert_indices // ⭐ 使用新的索引数组
438
+ * });
439
+ *
440
+ * return tx;
441
+ * }
442
+ *
443
+ * @see {@link simulateLongStopLoss} 做多仓位的止损计算
444
+ * @see {@link simulateShortSolStopLoss} 基于SOL金额的做空止损计算
445
+ * @since 2.0.0
446
+ * @version 2.0.0 - 从返回 prev_order_pda/next_order_pda 改为返回 close_insert_indices
447
+ */
448
+ async function simulateShortStopLoss(mint, sellTokenAmount, stopLossPrice, lastPrice = null, ordersData = null, borrowFee = null, initialVirtualSol = null, initialVirtualToken = null) {
449
+ try {
450
+ // Parameter validation
451
+ if (!mint || !sellTokenAmount || !stopLossPrice) {
452
+ throw new Error('Missing required parameters');
453
+ }
454
+
455
+ // 如果没有传入 borrowFee 或池子参数,从链上一次性获取
456
+ if (borrowFee === null || initialVirtualSol === null || initialVirtualToken === null) {
457
+ const curveAccount = await this.sdk.chain.getCurveAccount(mint, { skipBalances: true });
458
+ if (borrowFee === null) borrowFee = curveAccount.borrowFee;
459
+ // 链上返回的是 u64 原始单位(lamports/最小单位),需要除以 10^9 转为人类可读单位
460
+ // 与 calcLiq.js 中的转换方式一致
461
+ if (initialVirtualSol === null) initialVirtualSol = new Decimal(curveAccount.initialVirtualSol.toString()).div(CurveAMM.SOL_PRECISION_FACTOR_DECIMAL).toString();
462
+ if (initialVirtualToken === null) initialVirtualToken = new Decimal(curveAccount.initialVirtualToken.toString()).div(CurveAMM.TOKEN_PRECISION_FACTOR_DECIMAL).toString();
463
+ }
464
+
465
+ // Get current price
466
+ if (!lastPrice) {
467
+ //console.log('Getting current price...');
468
+ lastPrice = await this.sdk.data.price(mint);
469
+ if (!lastPrice) {
470
+ throw new Error('Failed to get current price');
471
+ }
472
+ }
473
+
474
+ // Get ordersData
475
+ if (!ordersData) {
476
+ //console.log('Getting orders data...');
477
+ ordersData = await this.sdk.data.orders(mint, { type: 'up_orders' });
478
+ if (!ordersData || !ordersData.success) {
479
+ throw new Error('Failed to get orders data');
480
+ }
481
+ }
482
+
483
+ //console.log("ordersData=", JSONbig.stringify(ordersData, null, 2))
484
+ //console.log("ordersData len=", ordersData.data.orders.length)
485
+
486
+ // Calculate current price
487
+ let currentPrice;
488
+ if (lastPrice === null || lastPrice === undefined || lastPrice === '0') {
489
+ //console.log('Current price is empty, using initial price');
490
+ currentPrice = CurveAMM.getInitialPrice();
491
+ } else {
492
+ currentPrice = BigInt(lastPrice);
493
+ if (!currentPrice || currentPrice === 0n) {
494
+ //console.log('Current price is 0, using initial price');
495
+ currentPrice = CurveAMM.getInitialPrice();
496
+ }
497
+ }
498
+
499
+ // Transform orders data
500
+ const upOrders = transformOrdersData(ordersData);
501
+ //console.log(`upOrders Found ${upOrders.length} existing short orders`);
502
+
503
+ // Initialize stop loss prices
504
+ let stopLossStartPrice = BigInt(stopLossPrice);
505
+ let stopLossEndPrice;
506
+ let maxIterations = 1000; // Prevent infinite loop
507
+ let iteration = 0;
508
+ let finalOverlapResult = null; // Record final overlap result
509
+ let finalTradeAmount = 0n; // Record final trade amount
510
+
511
+ // 检查并调整止损价格以满足最小距离要求 (做空: 止损价必须高于当前价至少 MIN_STOP_LOSS_PERCENT)
512
+ // Check and adjust stop loss price to meet minimum distance requirement (short: stop loss must be above current price by at least MIN_STOP_LOSS_PERCENT)
513
+ const minAllowedStopLoss = currentPrice + (currentPrice * BigInt(MIN_STOP_LOSS_PERCENT)) / 1000n;
514
+ if (stopLossStartPrice < minAllowedStopLoss) {
515
+ const originalStopLoss = stopLossStartPrice;
516
+ stopLossStartPrice = minAllowedStopLoss;
517
+ const originalPercent = Number((originalStopLoss - currentPrice) * 1000n / currentPrice) / 10;
518
+ const adjustedPercent = Number((stopLossStartPrice - currentPrice) * 1000n / currentPrice) / 10;
519
+
520
+ }
521
+
522
+ //console.log(`Start price: ${stopLossStartPrice}, Target token amount: ${sellTokenAmount}`);
523
+
524
+ // Loop to adjust stop loss price until no overlap
525
+ while (iteration < maxIterations) {
526
+ iteration++;
527
+
528
+ // // Calculate stop loss end price
529
+ // console.log(`[Sell Stop Loss Debug] Iteration ${iteration}:`);
530
+ // console.log(` - stopLossStartPrice: ${stopLossStartPrice.toString()}`);
531
+ // console.log(` - sellTokenAmount: ${sellTokenAmount.toString()}`);
532
+ // console.log(` - Calling CurveAMM.buyFromPriceWithTokenOutput...`);
533
+
534
+ const tradeResult = CurveAMM.buyFromPriceWithTokenOutputWithParams(stopLossStartPrice, sellTokenAmount, initialVirtualSol, initialVirtualToken);
535
+
536
+ //console.log(` - tradeResult:`, tradeResult);
537
+
538
+ if (!tradeResult) {
539
+ console.error(`[Sell Stop Loss Error] Failed at iteration ${iteration}`);
540
+ console.error(` - stopLossStartPrice: ${stopLossStartPrice.toString()}`);
541
+ console.error(` - sellTokenAmount: ${sellTokenAmount.toString()}`);
542
+ throw new Error('Failed to calculate stop loss end price');
543
+ }
544
+
545
+ stopLossEndPrice = tradeResult[0]; // Price after trade completion
546
+ const tradeAmount = tradeResult[1]; // SOL输入量 / SOL input amount
547
+
548
+ // console.log(` - stopLossEndPrice: ${stopLossEndPrice.toString()}`);
549
+ // console.log(` - tradeAmount: ${tradeAmount.toString()}`);
550
+
551
+ //console.log(`迭代 ${iteration}: 起始价格=${stopLossStartPrice}, 结束价格=${stopLossEndPrice}, SOL输入量=${tradeAmount} / Iteration ${iteration}: Start=${stopLossStartPrice}, End=${stopLossEndPrice}, SOL input=${tradeAmount}`);
552
+
553
+ // 检查价格区间重叠 / Check price range overlap
554
+ const overlapResult = checkPriceRangeOverlap('up_orders', upOrders, stopLossStartPrice, stopLossEndPrice);
555
+
556
+ if (overlapResult.no_overlap) {
557
+ //console.log(' / No price range overlap, can execute');
558
+ finalOverlapResult = overlapResult; // 记录最终的overlap结果 / Record final overlap result
559
+ finalTradeAmount = tradeAmount; // 记录最终的交易金额 / Record final trade amount
560
+ break;
561
+ }
562
+
563
+ //console.log(`发现重叠: ${overlapResult.overlap_reason} / Found overlap: ${overlapResult.overlap_reason}`);
564
+
565
+ // 调整起始价格(增加0.5%)/ Adjust start price (increase by 0.5%)
566
+ // 使用方案2:直接计算 0.5% = 5/1000
567
+ const adjustmentAmount = (stopLossStartPrice * BigInt(PRICE_ADJUSTMENT_PERCENTAGE)) / 1000n;
568
+ stopLossStartPrice = stopLossStartPrice + adjustmentAmount;
569
+
570
+ //console.log(`调整后起始价格: ${stopLossStartPrice} / Adjusted start price: ${stopLossStartPrice}`);
571
+
572
+ // 安全检查:确保价格不会超过最大值 / Safety check: ensure price doesn't exceed maximum
573
+ if (stopLossStartPrice >= CurveAMM.MAX_U128_PRICE) {
574
+ throw new Error(`Stop loss price exceeded maximum after adjustment: ${stopLossStartPrice} >= ${CurveAMM.MAX_U128_PRICE}`);
575
+ }
576
+ }
577
+
578
+ if (iteration >= maxIterations) {
579
+ throw new Error('达到最大迭代次数,无法找到合适的止损价格 / Reached maximum iterations, cannot find suitable stop loss price');
580
+ }
581
+
582
+ // 计算最终返回值 / Calculate final return values
583
+ const executableStopLossPrice = stopLossStartPrice;
584
+
585
+ // 计算止损百分比 / Calculate stop loss percentage
586
+ // For short position, stop loss price is higher than current price, so it's a positive percentage
587
+ const stopLossPercentage = Number((BigInt(10000) * (executableStopLossPrice - currentPrice)) / currentPrice) / 100;
588
+
589
+ // 计算杠杆比例 / Calculate leverage ratio
590
+ // For short position, leverage = current price / (stop loss price - current price)
591
+ const leverage = Number((BigInt(10000) * currentPrice) / (executableStopLossPrice - currentPrice)) / 10000;
592
+
593
+ // 计算保证金 / Calculate margin requirement
594
+ // 与合约公式一致 (long_short.rs 第890-894行):
595
+ // real_margin_sol = close_buy_sol_with_fee - output_sol - fee_sol
596
+ // 其中 output_sol 是扣费后净SOL, fee_sol 是开仓手续费
597
+ // 展开: real_margin_sol = close_buy_sol_with_fee - raw_sell_sol
598
+ let estimatedMargin = 0n;
599
+ let rawSellSol = 0n; // 卖出 token 获得的原始 SOL(未扣费),供调用方计算 minSolOutput
600
+ try {
601
+ // 1. 计算从当前价格卖出代币获得的原始SOL(未扣费)
602
+ const sellResult = CurveAMM.sellFromPriceWithTokenInputWithParams(currentPrice, sellTokenAmount, initialVirtualSol, initialVirtualToken);
603
+ if (sellResult) {
604
+ rawSellSol = sellResult[1]; // 卖出获得的原始SOL(未扣费)
605
+
606
+ // 2. 计算平仓成本(含手续费,使用 ceiling 除法与合约一致)
607
+ const closeCostWithFee = CurveAMM.calculateTotalAmountWithFee(finalTradeAmount, borrowFee);
608
+
609
+ // 3. 保证金 = 平仓成本(含费) - 原始卖出SOL
610
+ if (closeCostWithFee !== null && closeCostWithFee > rawSellSol) {
611
+ estimatedMargin = closeCostWithFee - rawSellSol;
612
+ }
613
+ }
614
+ } catch (marginError) {
615
+ console.warn('Failed to calculate estimated margin for short position:', marginError.message);
616
+ // Keep estimatedMargin as 0n
617
+ }
618
+
619
+ // console.log(`Calculation completed:`);
620
+ // console.log(` Executable stop loss price: ${executableStopLossPrice}`);
621
+ // console.log(` SOL input amount: ${finalTradeAmount}`);
622
+ // console.log(` Stop loss percentage: ${stopLossPercentage}%`);
623
+ // console.log(` Leverage: ${leverage}x`);
624
+ // console.log(` Close insert indices: ${finalOverlapResult.close_insert_indices}`);
625
+
626
+ return {
627
+ executableStopLossPrice: executableStopLossPrice, // Calculated reasonable stop loss value
628
+ tradeAmount: finalTradeAmount, // SOL input amount (平仓时买回 token 需要的 SOL)
629
+ stopLossPercentage: stopLossPercentage, // Stop loss percentage relative to current price
630
+ leverage: leverage, // Leverage ratio
631
+ currentPrice: currentPrice, // Current price
632
+ iterations: iteration, // Number of adjustments
633
+ originalStopLossPrice: BigInt(stopLossPrice), // Original stop loss price
634
+ close_insert_indices: finalOverlapResult.close_insert_indices, // Candidate insertion indices for closing order
635
+ estimatedMargin: estimatedMargin, // Estimated margin requirement in SOL (lamports)
636
+ rawSellSol: rawSellSol // 卖出 token 获得的原始 SOL(未扣费),用于调用方计算 minSolOutput
637
+ };
638
+
639
+ } catch (error) {
640
+ console.error('Failed to simulate short position stop loss calculation:', error.message);
641
+ throw error;
642
+ }
643
+ }
644
+
645
+
646
+
647
+
648
+
649
+
650
+
651
+
652
+
653
+ /**
654
+ * Simulate long position stop loss calculation with SOL amount input
655
+ *
656
+ * 基于 SOL 金额的做多止损计算。该函数会自动计算出对应的代币数量,
657
+ * 使得保证金需求接近用户输入的 SOL 金额。
658
+ *
659
+ * @param {string} mint - Token address / 代币地址
660
+ * @param {bigint|string|number} buySolAmount - SOL amount to spend for long position (u64 format, lamports) / 做多投入的SOL金额 (u64格式, lamports)
661
+ * @param {bigint|string|number} stopLossPrice - User desired stop loss price (u128 format) / 用户期望的止损价格 (u128格式)
662
+ * @param {Object|null} lastPrice - Token info, default null / 代币当前价格信息,默认null会自动获取
663
+ * @param {Object|null} ordersData - Orders data, default null / 订单数据,默认null会自动获取
664
+ * @param {number} borrowFee - Borrow fee rate, default 2000 (2000/100000 = 0.02%) / 借贷手续费率,默认2000 (2000/100000 = 0.02%)
665
+ *
666
+ * @returns {Promise<Object>} Stop loss analysis result / 止损分析结果对象
667
+ * @returns {bigint} returns.executableStopLossPrice - 可执行止损价格 (u128格式) - 同 {@link simulateLongStopLoss}
668
+ * @returns {bigint} returns.tradeAmount - 止损时预计卖出获得的SOL数量 (lamports) - 同 {@link simulateLongStopLoss}
669
+ * @returns {number} returns.stopLossPercentage - 止损百分比 - 同 {@link simulateLongStopLoss}
670
+ * @returns {number} returns.leverage - 杠杆倍数 - 同 {@link simulateLongStopLoss}
671
+ * @returns {bigint} returns.currentPrice - 当前价格 (u128格式) - 同 {@link simulateLongStopLoss}
672
+ * @returns {number} returns.iterations - 价格调整迭代次数 - 同 {@link simulateLongStopLoss}
673
+ * @returns {bigint} returns.originalStopLossPrice - 原始止损价格 (u128格式) - 同 {@link simulateLongStopLoss}
674
+ * @returns {number[]} returns.close_insert_indices - 平仓订单插入位置的候选索引数组 ⭐ - 同 {@link simulateLongStopLoss}
675
+ * @returns {bigint} returns.estimatedMargin - 预估所需保证金 (SOL lamports) - 同 {@link simulateLongStopLoss}
676
+ * @returns {bigint} returns.buyTokenAmount - 计算出的买入代币数量 ⭐ 额外字段
677
+ * - 这是根据 buySolAmount 反向计算出的代币数量
678
+ * - 使得 estimatedMargin 接近 buySolAmount
679
+ * - 可以直接用于 sdk.trading.long() 的 buyTokenAmount 参数
680
+ * @returns {number} returns.adjustmentIterations - 代币数量调整迭代次数 ⭐ 额外字段
681
+ * - 二分查找算法调整代币数量的迭代次数
682
+ * - 用于评估计算精度
683
+ *
684
+ * @throws {Error} 当缺少必需参数时
685
+ * @throws {Error} 当无法获取价格或订单数据时
686
+ * @throws {Error} 当无法计算代币数量时
687
+ *
688
+ * @example
689
+ * // 基础用法: 投入 0.1 SOL 做多,止损价格为当前价格的97%
690
+ * const result = await sdk.simulator.simulateLongSolStopLoss(
691
+ * '4Kq51Kt48FCwdo5CeKjRVPodH1ticHa7mZ5n5gqMEy1X', // mint
692
+ * 100000000n, // 0.1 SOL (精度10^9)
693
+ * BigInt('97000000000000000000') // 止损价格
694
+ * );
695
+ *
696
+ * console.log(`买入代币数量: ${result.buyTokenAmount}`);
697
+ * console.log(`预估保证金: ${result.estimatedMargin} lamports`);
698
+ * console.log(`插入位置索引: ${result.close_insert_indices}`);
699
+ *
700
+ * @see {@link simulateLongStopLoss} 基于代币数量的做多止损计算
701
+ * @see {@link simulateShortSolStopLoss} 基于SOL金额的做空止损计算
702
+ * @since 2.0.0
703
+ * @version 2.0.0 - 从返回 prev_order_pda/next_order_pda 改为返回 close_insert_indices
704
+ */
705
+ async function simulateLongSolStopLoss(mint, buySolAmount, stopLossPrice, lastPrice = null, ordersData = null, borrowFee = null, initialVirtualSol = null, initialVirtualToken = null, curveAccount = null) {
706
+ try {
707
+ // Parameter validation
708
+ if (!mint || !buySolAmount || !stopLossPrice) {
709
+ throw new Error('Missing required parameters');
710
+ }
711
+
712
+ // 如果没有传入 borrowFee 或池子参数,从链上一次性获取(支持外部传入 curveAccount 避免重复 RPC)
713
+ if (borrowFee === null || initialVirtualSol === null || initialVirtualToken === null) {
714
+ if (!curveAccount) {
715
+ curveAccount = await this.sdk.chain.getCurveAccount(mint, { skipBalances: true });
716
+ }
717
+ if (borrowFee === null) borrowFee = curveAccount.borrowFee;
718
+ if (initialVirtualSol === null) initialVirtualSol = new Decimal(curveAccount.initialVirtualSol.toString()).div(CurveAMM.SOL_PRECISION_FACTOR_DECIMAL).toString();
719
+ if (initialVirtualToken === null) initialVirtualToken = new Decimal(curveAccount.initialVirtualToken.toString()).div(CurveAMM.TOKEN_PRECISION_FACTOR_DECIMAL).toString();
720
+ }
721
+
722
+ // Get current price if not provided
723
+ let currentPrice;
724
+ if (!lastPrice) {
725
+ lastPrice = await this.sdk.data.price(mint);
726
+ if (!lastPrice) {
727
+ throw new Error('Failed to get current price');
728
+ }
729
+ }
730
+
731
+ // Calculate current price
732
+ if (lastPrice === null || lastPrice === undefined || lastPrice === '0') {
733
+ currentPrice = CurveAMM.getInitialPrice();
734
+ } else {
735
+ currentPrice = BigInt(lastPrice);
736
+ if (!currentPrice || currentPrice === 0n) {
737
+ currentPrice = CurveAMM.getInitialPrice();
738
+ }
739
+ }
740
+
741
+ // Calculate initial token amount from SOL amount using sellFromPriceWithSolOutput
742
+ // This gives us how many tokens we can get when we later sell for buySolAmount SOL
743
+ const initialResult = CurveAMM.sellFromPriceWithSolOutputWithParams(currentPrice, buySolAmount, initialVirtualSol, initialVirtualToken);
744
+ if (!initialResult) {
745
+ throw new Error('Failed to calculate token amount from SOL amount');
746
+ }
747
+
748
+ let buyTokenAmount = initialResult[1]; // Token amount
749
+ let stopLossResult;
750
+ let iterations = 0;
751
+ const maxIterations = 50;
752
+
753
+ // 根据杠杆倍数动态计算二分查找上界
754
+ // 高杠杆(如20x)时止损距离小,每个代币的保证金贡献小,需要更多代币才能消耗完保证金
755
+ // Calculate dynamic binary search upper bound based on leverage
756
+ const stopLossPriceBigInt = BigInt(stopLossPrice);
757
+ const priceDiff = currentPrice - stopLossPriceBigInt;
758
+ const estimatedLeverage = priceDiff > 0n ? Number(currentPrice * 10000n / priceDiff) / 10000 : 10;
759
+ const safeMultiplier = BigInt(Math.ceil(estimatedLeverage * 3)); // 3倍安全系数
760
+ const multiplier = safeMultiplier > 10n ? safeMultiplier : 10n; // 最小10倍
761
+
762
+ // 使用二分查找算法找到 estimatedMargin < buySolAmount 的最大值
763
+ // Use binary search algorithm to find maximum estimatedMargin that is less than buySolAmount
764
+ let left = 1n; // 最小值,确保有一个有效的下界
765
+ let right = buyTokenAmount * multiplier; // 上界:根据杠杆动态计算
766
+ let bestResult = null;
767
+ let bestMargin = 0n; // 记录最大的合法 estimatedMargin
768
+ let bestTokenAmount = buyTokenAmount;
769
+
770
+ // 二分查找主循环:寻找 estimatedMargin < buySolAmount 的最大值
771
+ while (iterations < maxIterations && left <= right) {
772
+ const mid = (left + right) / 2n;
773
+
774
+ // 计算当前 token 数量的结果
775
+ const currentResult = await simulateLongStopLoss.call(this, mint, mid, stopLossPrice, lastPrice, ordersData, borrowFee, initialVirtualSol, initialVirtualToken);
776
+ const currentMargin = currentResult.estimatedMargin;
777
+
778
+ //console.log(`Binary search iteration ${iterations}: tokenAmount=${mid}, estimatedMargin=${currentMargin}, target=${buySolAmount}`);
779
+
780
+ // 只考虑 estimatedMargin < buySolAmount 的情况
781
+ if (currentMargin < BigInt(buySolAmount)) {
782
+ // 这是一个合法的解,检查是否比当前最佳解更好
783
+ if (currentMargin > bestMargin) {
784
+ bestMargin = currentMargin;
785
+ bestResult = currentResult;
786
+ bestTokenAmount = mid;
787
+ //console.log(`Found better solution: estimatedMargin=${currentMargin}, tokenAmount=${mid}`);
788
+ }
789
+
790
+ // 如果差距已经很小(距离目标值小于10000000 lamports),可以提前退出
791
+ if (BigInt(buySolAmount) - currentMargin <= 10000000n) {
792
+ //console.log(`Found optimal solution: estimatedMargin=${currentMargin}, diff=${BigInt(buySolAmount) - currentMargin} (< 10000000 lamports tolerance)`);
793
+ break;
794
+ }
795
+
796
+ // 继续向右搜索,寻找更大的合法值
797
+ left = mid + 1n;
798
+ } else {
799
+ // estimatedMargin >= buySolAmount,需要减少 tokenAmount
800
+ //console.log(`estimatedMargin too large (${currentMargin} >= ${buySolAmount}), searching left`);
801
+ right = mid - 1n;
802
+ }
803
+
804
+ iterations++;
805
+ }
806
+
807
+ // 确保找到的结果满足要求
808
+ if (bestResult && bestMargin < BigInt(buySolAmount)) {
809
+ stopLossResult = bestResult;
810
+ buyTokenAmount = bestTokenAmount;
811
+ //console.log(`Binary search completed: best tokenAmount=${bestTokenAmount}, estimatedMargin=${bestMargin}, target=${buySolAmount}`);
812
+ } else {
813
+ // 如果没有找到合法解,使用一个很小的 tokenAmount 作为安全回退
814
+ //console.log(`No valid solution found (estimatedMargin < buySolAmount), using minimal tokenAmount`);
815
+ buyTokenAmount = buyTokenAmount / 10n; // 使用更小的值
816
+ if (buyTokenAmount <= 0n) buyTokenAmount = 1000000000n; // 最小值保护 (0.001 token with 9 decimals)
817
+ stopLossResult = await simulateLongStopLoss.call(this, mint, buyTokenAmount, stopLossPrice, lastPrice, ordersData, borrowFee, initialVirtualSol, initialVirtualToken);
818
+ }
819
+
820
+ // if (iterations >= maxIterations) {
821
+ // console.warn(`simulateLongSolStopLoss: Reached maximum iterations (${maxIterations}), tradeAmount=${stopLossResult.tradeAmount}, target=${buySolAmount}`);
822
+ // }
823
+
824
+ // Add buyTokenAmount and iteration info to the result
825
+ return {
826
+ ...stopLossResult,
827
+ buyTokenAmount: buyTokenAmount,
828
+ adjustmentIterations: iterations
829
+ };
830
+
831
+ } catch (error) {
832
+ console.error('Failed to simulate long stop loss with SOL amount:', error.message);
833
+ throw error;
834
+ }
835
+ }
836
+
837
+ /**
838
+ * Simulate short position stop loss calculation with SOL amount input
839
+ *
840
+ * 基于 SOL 金额的做空止损计算。该函数会自动计算出对应的代币数量,
841
+ * 使得保证金需求接近用户输入的 SOL 金额。
842
+ *
843
+ * @param {string} mint - Token address / 代币地址
844
+ * @param {bigint|string|number} sellSolAmount - SOL amount needed for short position stop loss (u64 format, lamports) / 做空投入的SOL金额 (u64格式, lamports)
845
+ * @param {bigint|string|number} stopLossPrice - User desired stop loss price (u128 format) / 用户期望的止损价格 (u128格式)
846
+ * @param {Object|null} lastPrice - Token info, default null / 代币当前价格信息,默认null会自动获取
847
+ * @param {Object|null} ordersData - Orders data, default null / 订单数据,默认null会自动获取
848
+ * @param {number} borrowFee - Borrow fee rate, default 2000 (2000/100000 = 0.02%) / 借贷手续费率,默认2000 (2000/100000 = 0.02%)
849
+ *
850
+ * @returns {Promise<Object>} Stop loss analysis result / 止损分析结果对象
851
+ * @returns {bigint} returns.executableStopLossPrice - 可执行止损价格 (u128格式) - 同 {@link simulateShortStopLoss}
852
+ * @returns {bigint} returns.tradeAmount - 止损时预计买入需要的SOL数量 (lamports) - 同 {@link simulateShortStopLoss}
853
+ * @returns {number} returns.stopLossPercentage - 止损百分比 - 同 {@link simulateShortStopLoss}
854
+ * @returns {number} returns.leverage - 杠杆倍数 - 同 {@link simulateShortStopLoss}
855
+ * @returns {bigint} returns.currentPrice - 当前价格 (u128格式) - 同 {@link simulateShortStopLoss}
856
+ * @returns {number} returns.iterations - 价格调整迭代次数 - 同 {@link simulateShortStopLoss}
857
+ * @returns {bigint} returns.originalStopLossPrice - 原始止损价格 (u128格式) - 同 {@link simulateShortStopLoss}
858
+ * @returns {number[]} returns.close_insert_indices - 平仓订单插入位置的候选索引数组 ⭐ - 同 {@link simulateShortStopLoss}
859
+ * @returns {bigint} returns.estimatedMargin - 预估所需保证金 (SOL lamports) - 同 {@link simulateShortStopLoss}
860
+ * @returns {bigint} returns.sellTokenAmount - 计算出的卖出代币数量 ⭐ 额外字段
861
+ * - 这是根据 sellSolAmount 反向计算出的代币数量
862
+ * - 使得 estimatedMargin 接近 sellSolAmount
863
+ * - 可以直接用于 sdk.trading.short() 的 borrowSellTokenAmount 参数
864
+ * @returns {number} returns.adjustmentIterations - 代币数量调整迭代次数 ⭐ 额外字段
865
+ * - 二分查找算法调整代币数量的迭代次数
866
+ * - 用于评估计算精度
867
+ *
868
+ * @throws {Error} 当缺少必需参数时
869
+ * @throws {Error} 当无法获取价格或订单数据时
870
+ * @throws {Error} 当无法计算代币数量时
871
+ *
872
+ * @example
873
+ * // 基础用法: 投入 0.1 SOL 做空,止损价格为当前价格的103%
874
+ * const result = await sdk.simulator.simulateShortSolStopLoss(
875
+ * '4Kq51Kt48FCwdo5CeKjRVPodH1ticHa7mZ5n5gqMEy1X', // mint
876
+ * 100000000n, // 0.1 SOL (精度10^9)
877
+ * BigInt('103000000000000000000') // 止损价格
878
+ * );
879
+ *
880
+ * console.log(`卖出代币数量: ${result.sellTokenAmount}`);
881
+ * console.log(`预估保证金: ${result.estimatedMargin} lamports`);
882
+ * console.log(`插入位置索引: ${result.close_insert_indices}`);
883
+ *
884
+ * @see {@link simulateShortStopLoss} 基于代币数量的做空止损计算
885
+ * @see {@link simulateLongSolStopLoss} 基于SOL金额的做多止损计算
886
+ * @since 2.0.0
887
+ * @version 2.0.0 - 从返回 prev_order_pda/next_order_pda 改为返回 close_insert_indices
888
+ */
889
+ async function simulateShortSolStopLoss(mint, sellSolAmount, stopLossPrice, lastPrice = null, ordersData = null, borrowFee = null, initialVirtualSol = null, initialVirtualToken = null, curveAccount = null) {
890
+ try {
891
+ // Parameter validation
892
+ if (!mint || !sellSolAmount || !stopLossPrice) {
893
+ throw new Error('Missing required parameters');
894
+ }
895
+
896
+ // 如果没有传入 borrowFee 或池子参数,从链上一次性获取(支持外部传入 curveAccount 避免重复 RPC)
897
+ if (borrowFee === null || initialVirtualSol === null || initialVirtualToken === null) {
898
+ if (!curveAccount) {
899
+ curveAccount = await this.sdk.chain.getCurveAccount(mint, { skipBalances: true });
900
+ }
901
+ if (borrowFee === null) borrowFee = curveAccount.borrowFee;
902
+ if (initialVirtualSol === null) initialVirtualSol = new Decimal(curveAccount.initialVirtualSol.toString()).div(CurveAMM.SOL_PRECISION_FACTOR_DECIMAL).toString();
903
+ if (initialVirtualToken === null) initialVirtualToken = new Decimal(curveAccount.initialVirtualToken.toString()).div(CurveAMM.TOKEN_PRECISION_FACTOR_DECIMAL).toString();
904
+ }
905
+
906
+ // Get current price if not provided
907
+ let currentPrice;
908
+ if (!lastPrice) {
909
+ lastPrice = await this.sdk.data.price(mint);
910
+ if (!lastPrice) {
911
+ throw new Error('Failed to get current price');
912
+ }
913
+ }
914
+
915
+ //console.log("simulateShortSolStopLoss lastPrice=",lastPrice)
916
+
917
+ // Calculate current price
918
+ if (lastPrice === null || lastPrice === undefined || lastPrice === '0') {
919
+ currentPrice = CurveAMM.getInitialPrice();
920
+ } else {
921
+ currentPrice = BigInt(lastPrice);
922
+ if (!currentPrice || currentPrice === 0n) {
923
+ currentPrice = CurveAMM.getInitialPrice();
924
+ }
925
+ }
926
+
927
+ // Calculate initial token amount from SOL amount using buyFromPriceWithSolInput
928
+ // This gives us how many tokens we need to buy later using sellSolAmount SOL
929
+ const initialResult = CurveAMM.buyFromPriceWithSolInputWithParams(currentPrice, sellSolAmount, initialVirtualSol, initialVirtualToken);
930
+ if (!initialResult) {
931
+ throw new Error('Failed to calculate token amount from SOL amount');
932
+ }
933
+
934
+ let sellTokenAmount = initialResult[1]; // Token amount
935
+ let stopLossResult;
936
+ let iterations = 0;
937
+ const maxIterations = 50;
938
+
939
+ // 根据杠杆倍数动态计算二分查找上界
940
+ // 高杠杆(如20x)时止损距离小,每个代币的保证金贡献小,需要更多代币才能消耗完保证金
941
+ // Calculate dynamic binary search upper bound based on leverage
942
+ const stopLossPriceBigInt = BigInt(stopLossPrice);
943
+ const priceDiff = stopLossPriceBigInt - currentPrice;
944
+ const estimatedLeverage = priceDiff > 0n ? Number(currentPrice * 10000n / priceDiff) / 10000 : 10;
945
+ const safeMultiplier = BigInt(Math.ceil(estimatedLeverage * 3)); // 3倍安全系数
946
+ const multiplier = safeMultiplier > 10n ? safeMultiplier : 10n; // 最小10倍
947
+
948
+ // 使用二分查找算法找到 estimatedMargin < sellSolAmount 的最大值
949
+ // Use binary search algorithm to find maximum estimatedMargin that is less than sellSolAmount
950
+ let left = 1n; // 最小值,确保有一个有效的下界
951
+ let right = sellTokenAmount * multiplier; // 上界:根据杠杆动态计算
952
+ let bestResult = null;
953
+ let bestMargin = 0n; // 记录最大的合法 estimatedMargin
954
+ let bestTokenAmount = sellTokenAmount;
955
+
956
+ // 二分查找主循环:寻找 estimatedMargin < sellSolAmount 的最大值
957
+ while (iterations < maxIterations && left <= right) {
958
+ const mid = (left + right) / 2n;
959
+
960
+ // 计算当前 token 数量的结果
961
+ const currentResult = await simulateShortStopLoss.call(this, mint, mid, stopLossPrice, lastPrice, ordersData, borrowFee, initialVirtualSol, initialVirtualToken);
962
+ const currentMargin = currentResult.estimatedMargin;
963
+
964
+ //console.log(`Binary search iteration ${iterations}: tokenAmount=${mid}, estimatedMargin=${currentMargin}, target=${sellSolAmount}`);
965
+
966
+ // 只考虑 estimatedMargin < sellSolAmount 的情况
967
+ if (currentMargin < BigInt(sellSolAmount)) {
968
+ // 这是一个合法的解,检查是否比当前最佳解更好
969
+ if (currentMargin > bestMargin) {
970
+ bestMargin = currentMargin;
971
+ bestResult = currentResult;
972
+ bestTokenAmount = mid;
973
+ //console.log(`Found better solution: estimatedMargin=${currentMargin}, tokenAmount=${mid}`);
974
+ }
975
+
976
+ // 如果差距已经很小(距离目标值小于10000000 lamports),可以提前退出
977
+ if (BigInt(sellSolAmount) - currentMargin <= 10000000n) {
978
+ //console.log(`Found optimal solution: estimatedMargin=${currentMargin}, diff=${BigInt(sellSolAmount) - currentMargin} (< 10000000 lamports tolerance)`);
979
+ break;
980
+ }
981
+
982
+ // 继续向右搜索,寻找更大的合法值
983
+ left = mid + 1n;
984
+ } else {
985
+ // estimatedMargin >= sellSolAmount,需要减少 tokenAmount
986
+ //console.log(`estimatedMargin too large (${currentMargin} >= ${sellSolAmount}), searching left`);
987
+ right = mid - 1n;
988
+ }
989
+
990
+ iterations++;
991
+ }
992
+
993
+ // 确保找到的结果满足要求
994
+ if (bestResult && bestMargin < BigInt(sellSolAmount)) {
995
+ stopLossResult = bestResult;
996
+ sellTokenAmount = bestTokenAmount;
997
+ //console.log(`Binary search completed: best tokenAmount=${bestTokenAmount}, estimatedMargin=${bestMargin}, target=${sellSolAmount}`);
998
+ } else {
999
+ // 如果没有找到合法解,使用一个很小的 tokenAmount 作为安全回退
1000
+ //console.log(`No valid solution found (estimatedMargin < sellSolAmount), using minimal tokenAmount`);
1001
+ sellTokenAmount = sellTokenAmount / 10n; // 使用更小的值
1002
+ if (sellTokenAmount <= 0n) sellTokenAmount = 1000000000n; // 最小值保护 (0.001 token with 9 decimals)
1003
+ stopLossResult = await simulateShortStopLoss.call(this, mint, sellTokenAmount, stopLossPrice, lastPrice, ordersData, borrowFee, initialVirtualSol, initialVirtualToken);
1004
+ }
1005
+
1006
+ // if (iterations >= maxIterations) {
1007
+ // //console.warn(`simulateShortSolStopLoss: Reached maximum iterations (${maxIterations}), tradeAmount=${stopLossResult.tradeAmount}, target=${sellSolAmount}`);
1008
+ // }
1009
+
1010
+ // Add sellTokenAmount and iteration info to the result
1011
+ return {
1012
+ ...stopLossResult,
1013
+ sellTokenAmount: sellTokenAmount,
1014
+ adjustmentIterations: iterations
1015
+ };
1016
+
1017
+ } catch (error) {
1018
+ console.error('Failed to simulate short stop loss with SOL amount:', error.message);
1019
+ throw error;
1020
+ }
1021
+ }
1022
+
1023
+ module.exports = {
1024
+ simulateLongStopLoss,
1025
+ simulateShortStopLoss,
1026
+ simulateLongSolStopLoss,
1027
+ simulateShortSolStopLoss
1028
+ };