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.
- package/README.md +364 -0
- package/dist/100x-sdk.cjs.js +57601 -0
- package/dist/100x-sdk.cjs.js.map +1 -0
- package/dist/100x-sdk.esm.js +46234 -0
- package/dist/100x-sdk.esm.js.map +1 -0
- package/dist/100x-sdk.js +46245 -0
- package/dist/100x-sdk.js.map +1 -0
- package/dist/index.d.ts +386 -0
- package/package.json +69 -0
- package/src/idl/fun100x_localnet.json +3735 -0
- package/src/idl/fun100x_main.json +3735 -0
- package/src/index.js +50 -0
- package/src/modules/chain.js +1310 -0
- package/src/modules/fast.js +734 -0
- package/src/modules/param.js +171 -0
- package/src/modules/simulator/buy_sell_token.js +326 -0
- package/src/modules/simulator/calcLiq.js +778 -0
- package/src/modules/simulator/calc_sol_liq.js +268 -0
- package/src/modules/simulator/close_indices.js +217 -0
- package/src/modules/simulator/long_shrot_stop.js +1028 -0
- package/src/modules/simulator/stop_loss_utils.js +378 -0
- package/src/modules/simulator/utils.js +63 -0
- package/src/modules/simulator.js +386 -0
- package/src/modules/token.js +455 -0
- package/src/modules/tools.js +370 -0
- package/src/modules/trading.js +848 -0
- package/src/sdk.js +206 -0
- package/src/types/index.d.ts +386 -0
- package/src/utils/constants.js +57 -0
- package/src/utils/curve_amm.js +1215 -0
- package/src/utils/orderUtils.js +17 -0
|
@@ -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
|
+
};
|