@michaleffffff/mcp-trading-server 3.0.20 → 3.0.24
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/CHANGELOG.md +51 -1
- package/README.md +31 -7
- package/TOOL_EXAMPLES.md +25 -2
- package/dist/auth/resolveClient.js +3 -3
- package/dist/prompts/tradingGuide.js +15 -7
- package/dist/server.js +2 -2
- package/dist/services/marketService.js +41 -0
- package/dist/services/poolService.js +45 -84
- package/dist/services/tradeService.js +61 -6
- package/dist/tools/accountTransfer.js +14 -7
- package/dist/tools/closeAllPositions.js +30 -23
- package/dist/tools/closePosition.js +15 -1
- package/dist/tools/executeTrade.js +27 -2
- package/dist/tools/getMyLpHoldings.js +4 -8
- package/dist/tools/getPoolMetadata.js +11 -19
- package/dist/tools/manageLiquidity.js +7 -2
- package/dist/tools/manageTpSl.js +81 -7
- package/dist/tools/openPositionSimple.js +37 -18
- package/dist/utils/mappings.js +2 -2
- package/dist/utils/slippage.js +20 -0
- package/dist/utils/token.js +15 -0
- package/dist/utils/units.js +4 -12
- package/dist/utils/verification.js +43 -12
- package/package.json +2 -2
|
@@ -7,6 +7,7 @@ import { normalizeSlippagePct4dp } from "../utils/slippage.js";
|
|
|
7
7
|
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
8
8
|
import { extractErrorMessage } from "../utils/errorMessage.js";
|
|
9
9
|
import { mapTimeInForce } from "../utils/mappings.js";
|
|
10
|
+
import { getFreshOraclePrice } from "./marketService.js";
|
|
10
11
|
function resolveDirection(direction) {
|
|
11
12
|
if (typeof direction === "string") {
|
|
12
13
|
const text = direction.trim().toUpperCase();
|
|
@@ -154,6 +155,37 @@ function validateIncreaseOrderEconomics(args) {
|
|
|
154
155
|
const priceHuman = formatUnits(priceRawBig, 30);
|
|
155
156
|
throw new Error(`Invalid size semantics: size is BASE quantity, not USD notional. collateralAmount*leverage implies ≈${targetHuman} quote, but size*price implies ≈${actualHuman} quote. At price ${priceHuman}, recommended size is ≈${recommendedSizeHuman}.`);
|
|
156
157
|
}
|
|
158
|
+
function countAdditionalExecutionOrders(args) {
|
|
159
|
+
let count = 1;
|
|
160
|
+
if (String(args.tpPrice ?? "").trim()) {
|
|
161
|
+
count += 1;
|
|
162
|
+
}
|
|
163
|
+
if (String(args.slPrice ?? "").trim()) {
|
|
164
|
+
count += 1;
|
|
165
|
+
}
|
|
166
|
+
return count;
|
|
167
|
+
}
|
|
168
|
+
async function getRequiredIncreaseSpendRaw(client, marketId, args, chainId) {
|
|
169
|
+
const collateralRaw = BigInt(args.collateralRaw);
|
|
170
|
+
const tradingFeeRaw = BigInt(args.tradingFeeRaw);
|
|
171
|
+
const executionOrderCount = countAdditionalExecutionOrders(args);
|
|
172
|
+
const networkFeeText = String(await client.utils.getNetworkFee(marketId, chainId) ?? "").trim();
|
|
173
|
+
if (!/^\d+$/.test(networkFeeText)) {
|
|
174
|
+
throw new Error(`Failed to resolve networkFee for marketId=${marketId}.`);
|
|
175
|
+
}
|
|
176
|
+
const baseNetworkFeeRaw = BigInt(networkFeeText);
|
|
177
|
+
if (baseNetworkFeeRaw <= 0n) {
|
|
178
|
+
throw new Error(`networkFee must be > 0 for marketId=${marketId}.`);
|
|
179
|
+
}
|
|
180
|
+
const networkFeeRaw = baseNetworkFeeRaw * BigInt(executionOrderCount);
|
|
181
|
+
return {
|
|
182
|
+
collateralRaw,
|
|
183
|
+
tradingFeeRaw,
|
|
184
|
+
networkFeeRaw,
|
|
185
|
+
executionOrderCount,
|
|
186
|
+
totalSpendRaw: collateralRaw + tradingFeeRaw + networkFeeRaw,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
157
189
|
async function resolveDecimalsForUpdateOrder(client, chainId, marketId, poolIdHint) {
|
|
158
190
|
let baseDecimals = 18;
|
|
159
191
|
let quoteDecimals = getQuoteDecimals();
|
|
@@ -249,6 +281,18 @@ export async function openPosition(client, address, args) {
|
|
|
249
281
|
const sizeRaw = ensureUnits(args.size, baseDecimals, "size", { allowImplicitRaw: false });
|
|
250
282
|
const priceRaw = ensureUnits(args.price, 30, "price", { allowImplicitRaw: false });
|
|
251
283
|
const tradingFeeRaw = ensureUnits(args.tradingFee, quoteDecimals, "tradingFee", { allowImplicitRaw: false });
|
|
284
|
+
const resolvedMarketId = String(args.marketId ?? poolData.marketId ?? "").trim();
|
|
285
|
+
if (!resolvedMarketId) {
|
|
286
|
+
throw new Error(`marketId is required to compute networkFee for poolId=${args.poolId}.`);
|
|
287
|
+
}
|
|
288
|
+
const spend = await getRequiredIncreaseSpendRaw(client, resolvedMarketId, {
|
|
289
|
+
collateralRaw,
|
|
290
|
+
tradingFeeRaw,
|
|
291
|
+
tpPrice: args.tpPrice,
|
|
292
|
+
tpSize: args.tpSize,
|
|
293
|
+
slPrice: args.slPrice,
|
|
294
|
+
slSize: args.slSize,
|
|
295
|
+
}, chainId);
|
|
252
296
|
validateIncreaseOrderEconomics({
|
|
253
297
|
collateralRaw,
|
|
254
298
|
sizeRaw,
|
|
@@ -268,7 +312,7 @@ export async function openPosition(client, address, args) {
|
|
|
268
312
|
marginBalanceRaw = toBigIntOrZero(marginInfo.data.freeMargin);
|
|
269
313
|
walletBalanceRaw = toBigIntOrZero(marginInfo.data.walletBalance);
|
|
270
314
|
}
|
|
271
|
-
const requiredRaw =
|
|
315
|
+
const requiredRaw = spend.totalSpendRaw;
|
|
272
316
|
if (marginBalanceRaw < requiredRaw) {
|
|
273
317
|
if (!allowAutoDeposit) {
|
|
274
318
|
throw new Error(`Insufficient marginBalance (${marginBalanceRaw.toString()}) for required collateral (${requiredRaw.toString()}). ` +
|
|
@@ -459,9 +503,14 @@ export async function closeAllPositions(client, address) {
|
|
|
459
503
|
const results = [];
|
|
460
504
|
for (const pos of positions) {
|
|
461
505
|
const dir = pos.direction === 0 ? Direction.LONG : Direction.SHORT;
|
|
462
|
-
|
|
463
|
-
const
|
|
464
|
-
|
|
506
|
+
const marketDetailRes = await client.markets.getMarketDetail({ chainId, poolId: pos.poolId });
|
|
507
|
+
const marketDetail = marketDetailRes?.data || (marketDetailRes?.marketId ? marketDetailRes : null);
|
|
508
|
+
if (!marketDetail?.marketId) {
|
|
509
|
+
throw new Error(`Could not resolve market metadata for poolId=${pos.poolId}.`);
|
|
510
|
+
}
|
|
511
|
+
const baseDecimals = Number(marketDetail.baseDecimals ?? 18);
|
|
512
|
+
const oracleData = await getFreshOraclePrice(client, pos.poolId, chainId);
|
|
513
|
+
const currentPrice30 = ensureUnits(oracleData.price, 30, "oracle price", { allowImplicitRaw: false });
|
|
465
514
|
// For LONG close (Decrease LONG): Price should be lower (e.g. 90% of current)
|
|
466
515
|
// For SHORT close (Decrease SHORT): Price should be higher (e.g. 110% of current)
|
|
467
516
|
// Here we use a safe 10% slippage price
|
|
@@ -472,7 +521,13 @@ export async function closeAllPositions(client, address) {
|
|
|
472
521
|
else {
|
|
473
522
|
slippagePrice30 = (BigInt(currentPrice30) * 110n) / 100n;
|
|
474
523
|
}
|
|
475
|
-
const
|
|
524
|
+
const sizeInput = /^\d+$/.test(String(pos.sizeRaw ?? pos.positionSizeRaw ?? "").trim())
|
|
525
|
+
? `raw:${String(pos.sizeRaw ?? pos.positionSizeRaw).trim()}`
|
|
526
|
+
: String(pos.size ?? pos.positionSize ?? "").trim();
|
|
527
|
+
if (!sizeInput) {
|
|
528
|
+
throw new Error(`Position size missing for positionId=${String(pos.positionId ?? "").trim()}.`);
|
|
529
|
+
}
|
|
530
|
+
const sizeWei = ensureUnits(sizeInput, baseDecimals, "size", { allowImplicitRaw: false });
|
|
476
531
|
const res = await client.order.createDecreaseOrder({
|
|
477
532
|
chainId,
|
|
478
533
|
address,
|
|
@@ -485,7 +540,7 @@ export async function closeAllPositions(client, address) {
|
|
|
485
540
|
size: sizeWei,
|
|
486
541
|
price: slippagePrice30.toString(),
|
|
487
542
|
postOnly: false,
|
|
488
|
-
slippagePct: "
|
|
543
|
+
slippagePct: "100", // 1%
|
|
489
544
|
executionFeeToken: getQuoteToken(),
|
|
490
545
|
leverage: pos.userLeverage,
|
|
491
546
|
});
|
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { resolveClient, getChainId, getQuoteToken } from "../auth/resolveClient.js";
|
|
3
3
|
import { normalizeAddress } from "../utils/address.js";
|
|
4
4
|
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
5
|
+
import { fetchErc20Decimals } from "../utils/token.js";
|
|
5
6
|
const MAX_UINT256 = "115792089237316195423570985008687907853269984665640564039457584007913129639935";
|
|
6
7
|
function asBigintOrNull(value) {
|
|
7
8
|
try {
|
|
@@ -52,11 +53,9 @@ export const accountDepositTool = {
|
|
|
52
53
|
const chainId = getChainId();
|
|
53
54
|
const tokenAddressInput = String(args.tokenAddress ?? "").trim() || getQuoteToken();
|
|
54
55
|
const tokenAddress = normalizeAddress(tokenAddressInput, "tokenAddress");
|
|
55
|
-
// For deposit, we default to quote decimals (6) as it's the most common use case.
|
|
56
|
-
// ensureUnits handles 'raw:' prefix if absolute precision is needed.
|
|
57
56
|
const { ensureUnits } = await import("../utils/units.js");
|
|
58
|
-
const
|
|
59
|
-
const amount = ensureUnits(args.amount,
|
|
57
|
+
const tokenDecimals = await fetchErc20Decimals(signer.provider ?? signer, tokenAddress, "deposit token");
|
|
58
|
+
const amount = ensureUnits(args.amount, tokenDecimals, "amount", { allowImplicitRaw: false });
|
|
60
59
|
let approval = null;
|
|
61
60
|
const needApproval = await client.utils.needsApproval(address, chainId, tokenAddress, amount);
|
|
62
61
|
if (needApproval) {
|
|
@@ -102,9 +101,17 @@ export const accountWithdrawTool = {
|
|
|
102
101
|
const { client, address, signer } = await resolveClient();
|
|
103
102
|
const chainId = getChainId();
|
|
104
103
|
const { ensureUnits } = await import("../utils/units.js");
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
const marketDetailRes = await client.markets.getMarketDetail({ chainId, poolId: args.poolId });
|
|
105
|
+
const marketDetail = marketDetailRes?.data || (marketDetailRes?.marketId ? marketDetailRes : null);
|
|
106
|
+
if (!marketDetail?.marketId) {
|
|
107
|
+
throw new Error(`Could not resolve market metadata for poolId=${args.poolId}.`);
|
|
108
|
+
}
|
|
109
|
+
const decimals = Number(Boolean(args.isQuoteToken)
|
|
110
|
+
? marketDetail.quoteDecimals
|
|
111
|
+
: marketDetail.baseDecimals);
|
|
112
|
+
if (!Number.isFinite(decimals) || decimals < 0) {
|
|
113
|
+
throw new Error(`Invalid token decimals for withdraw on poolId=${args.poolId}.`);
|
|
114
|
+
}
|
|
108
115
|
const amount = ensureUnits(args.amount, decimals, "amount", { allowImplicitRaw: false });
|
|
109
116
|
const amountRaw = asBigintOrNull(amount);
|
|
110
117
|
if (amountRaw === null || amountRaw <= 0n) {
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { resolveClient, getChainId, getQuoteToken } from "../auth/resolveClient.js";
|
|
3
|
-
import {
|
|
3
|
+
import { getFreshOraclePrice, resolvePool } from "../services/marketService.js";
|
|
4
4
|
import { ensureUnits } from "../utils/units.js";
|
|
5
5
|
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
6
6
|
import { normalizeSlippagePct4dpFlexible, SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
|
|
7
|
+
const INTEGER_RE = /^\d+$/;
|
|
8
|
+
function resolvePositionSizeInput(position) {
|
|
9
|
+
const rawCandidates = [position?.sizeRaw, position?.positionSizeRaw];
|
|
10
|
+
for (const candidate of rawCandidates) {
|
|
11
|
+
const text = String(candidate ?? "").trim();
|
|
12
|
+
if (INTEGER_RE.test(text))
|
|
13
|
+
return `raw:${text}`;
|
|
14
|
+
}
|
|
15
|
+
const humanCandidates = [position?.size, position?.positionSize];
|
|
16
|
+
for (const candidate of humanCandidates) {
|
|
17
|
+
const text = String(candidate ?? "").trim();
|
|
18
|
+
if (text)
|
|
19
|
+
return text;
|
|
20
|
+
}
|
|
21
|
+
throw new Error(`Position size missing for positionId=${String(position?.positionId ?? position?.position_id ?? "").trim()}.`);
|
|
22
|
+
}
|
|
7
23
|
export const closeAllPositionsTool = {
|
|
8
24
|
name: "close_all_positions",
|
|
9
25
|
description: "[TRADE] Emergency: close ALL open positions in a pool at once. Use for risk management.",
|
|
@@ -28,27 +44,18 @@ export const closeAllPositionsTool = {
|
|
|
28
44
|
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: { message: "No open positions in this pool.", closed: 0 } }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
29
45
|
}
|
|
30
46
|
// 2) 为每个仓位构建平仓参数
|
|
31
|
-
const slippagePct = normalizeSlippagePct4dpFlexible(args.slippagePct ?? "
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
fallbackPrice = oraclePriceReq.price.toString();
|
|
37
|
-
}
|
|
38
|
-
else {
|
|
39
|
-
const { getMarketPrice } = await import("../services/marketService.js");
|
|
40
|
-
const marketData = await getMarketPrice(client, poolId).catch(() => null);
|
|
41
|
-
if (marketData && marketData.price) {
|
|
42
|
-
fallbackPrice = marketData.price.toString();
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
// Extreme fallback - but this may still revert
|
|
46
|
-
fallbackPrice = poolPositions[0]?.entryPrice || "0";
|
|
47
|
-
}
|
|
47
|
+
const slippagePct = normalizeSlippagePct4dpFlexible(args.slippagePct ?? "100");
|
|
48
|
+
const marketDetailRes = await client.markets.getMarketDetail({ chainId, poolId });
|
|
49
|
+
const marketDetail = marketDetailRes?.data || (marketDetailRes?.marketId ? marketDetailRes : null);
|
|
50
|
+
if (!marketDetail?.marketId) {
|
|
51
|
+
throw new Error(`Could not resolve market metadata for poolId=${poolId}.`);
|
|
48
52
|
}
|
|
53
|
+
const baseDecimals = Number(marketDetail.baseDecimals ?? 18);
|
|
54
|
+
const quoteToken = String(marketDetail.quoteToken ?? "").trim() || getQuoteToken();
|
|
55
|
+
const oracle = await getFreshOraclePrice(client, poolId, chainId);
|
|
56
|
+
const freshOraclePrice = oracle.price.toString();
|
|
49
57
|
const closeParams = poolPositions.map((pos) => {
|
|
50
|
-
const
|
|
51
|
-
const rawPrice = pos.markPrice || pos.mark_price || fallbackPrice;
|
|
58
|
+
const sizeInput = resolvePositionSizeInput(pos);
|
|
52
59
|
return {
|
|
53
60
|
chainId,
|
|
54
61
|
address,
|
|
@@ -59,11 +66,11 @@ export const closeAllPositionsTool = {
|
|
|
59
66
|
timeInForce: 0, // IOC
|
|
60
67
|
direction: pos.direction ?? 0,
|
|
61
68
|
collateralAmount: "0",
|
|
62
|
-
size: ensureUnits(
|
|
63
|
-
price: ensureUnits(
|
|
69
|
+
size: ensureUnits(sizeInput, baseDecimals, "size", { allowImplicitRaw: false }),
|
|
70
|
+
price: ensureUnits(freshOraclePrice, 30, "price", { allowImplicitRaw: false }),
|
|
64
71
|
postOnly: false,
|
|
65
72
|
slippagePct,
|
|
66
|
-
executionFeeToken:
|
|
73
|
+
executionFeeToken: quoteToken,
|
|
67
74
|
leverage: pos.userLeverage ?? pos.leverage ?? 1,
|
|
68
75
|
};
|
|
69
76
|
});
|
|
@@ -41,7 +41,7 @@ export const closePositionTool = {
|
|
|
41
41
|
collateralAmount: z.union([z.string(), z.number()]).describe("Collateral amount (human/raw). Also supports ALL/FULL/MAX to use live position collateral raw."),
|
|
42
42
|
size: z.union([z.string(), z.number()]).describe("Position size (human/raw). Also supports ALL/FULL/MAX for exact full-close raw size."),
|
|
43
43
|
price: z.union([z.string(), z.number()]).describe("Price (human or 30-dec raw units)"),
|
|
44
|
-
timeInForce: z.union([z.number(), z.string()]).describe("SDK v1.0.
|
|
44
|
+
timeInForce: z.union([z.number(), z.string()]).describe("SDK v1.0.4-beta.4 supports IOC only. Use 0 or 'IOC'."),
|
|
45
45
|
postOnly: z.coerce.boolean().describe("Post-only flag"),
|
|
46
46
|
slippagePct: z.coerce.string().default("50").describe(SLIPPAGE_PCT_4DP_DESC),
|
|
47
47
|
executionFeeToken: z.string().describe("Execution fee token address"),
|
|
@@ -95,6 +95,20 @@ export const closePositionTool = {
|
|
|
95
95
|
triggerType: preparedArgs.triggerType !== undefined ? mapTriggerType(preparedArgs.triggerType) : undefined,
|
|
96
96
|
executionFeeToken: poolData.quoteToken || preparedArgs.executionFeeToken
|
|
97
97
|
};
|
|
98
|
+
const positionsRes = await client.position.listPositions(address);
|
|
99
|
+
const positions = Array.isArray(positionsRes?.data) ? positionsRes.data : [];
|
|
100
|
+
const target = positions.find((position) => {
|
|
101
|
+
const pid = String(position?.positionId ?? position?.position_id ?? "").trim().toLowerCase();
|
|
102
|
+
const pool = String(position?.poolId ?? position?.pool_id ?? "").trim().toLowerCase();
|
|
103
|
+
return pid === String(preparedArgs.positionId ?? "").trim().toLowerCase() && pool === String(poolId).toLowerCase();
|
|
104
|
+
});
|
|
105
|
+
if (!target) {
|
|
106
|
+
throw new Error(`Could not find live position snapshot for positionId=${preparedArgs.positionId} in poolId=${poolId}.`);
|
|
107
|
+
}
|
|
108
|
+
const liveDirection = Number(target?.direction);
|
|
109
|
+
if (!Number.isFinite(liveDirection) || liveDirection !== mappedArgs.direction) {
|
|
110
|
+
throw new Error(`direction mismatch for positionId=${preparedArgs.positionId}: input=${mappedArgs.direction}, live=${String(target?.direction ?? "unknown")}.`);
|
|
111
|
+
}
|
|
98
112
|
const raw = await closePos(client, address, mappedArgs);
|
|
99
113
|
const data = await finalizeMutationResult(raw, signer, "close_position");
|
|
100
114
|
const txHash = data.confirmation?.txHash;
|
|
@@ -10,6 +10,14 @@ import { parseUserUnits } from "../utils/units.js";
|
|
|
10
10
|
import { isZeroAddress } from "../utils/address.js";
|
|
11
11
|
const POSITION_ID_RE = /^$|^0x[0-9a-fA-F]{64}$/;
|
|
12
12
|
const ZERO_POSITION_ID_RE = /^0x0{64}$/i;
|
|
13
|
+
function pow10(decimals) {
|
|
14
|
+
return 10n ** BigInt(decimals);
|
|
15
|
+
}
|
|
16
|
+
function computeQuoteNotionalRaw(sizeRaw, priceRaw30, baseDecimals, quoteDecimals) {
|
|
17
|
+
const numerator = sizeRaw * priceRaw30 * pow10(quoteDecimals);
|
|
18
|
+
const denominator = pow10(baseDecimals + 30);
|
|
19
|
+
return numerator / denominator;
|
|
20
|
+
}
|
|
13
21
|
export const executeTradeTool = {
|
|
14
22
|
name: "execute_trade",
|
|
15
23
|
description: "[TRADE] Create an increase order (open or add to position) using SDK-native parameters.",
|
|
@@ -24,7 +32,7 @@ export const executeTradeTool = {
|
|
|
24
32
|
collateralAmount: z.union([z.string(), z.number()]).describe("Collateral. e.g. '100' or 'raw:100000000' (6 decimals for USDC)."),
|
|
25
33
|
size: z.union([z.string(), z.number()]).describe("Notional size in base tokens. e.g. '0.5' BTC or 'raw:50000000'."),
|
|
26
34
|
price: z.union([z.string(), z.number()]).describe("Execution or Limit price. e.g. '65000' or 'raw:...'"),
|
|
27
|
-
timeInForce: z.union([z.number(), z.string()]).describe("SDK v1.0.
|
|
35
|
+
timeInForce: z.union([z.number(), z.string()]).describe("SDK v1.0.4-beta.4 supports IOC only. Use 0 or 'IOC'."),
|
|
28
36
|
postOnly: z.coerce.boolean().describe("If true, order only executes as Maker."),
|
|
29
37
|
slippagePct: z.coerce.string().default("50").describe(`${SLIPPAGE_PCT_4DP_DESC}. Default is 50 (0.5%).`),
|
|
30
38
|
executionFeeToken: z.string().optional().describe("Address of token to pay gas/execution fees (typically USDC). Default is pool quoteToken."),
|
|
@@ -76,6 +84,22 @@ export const executeTradeTool = {
|
|
|
76
84
|
throw new Error("size must be > 0.");
|
|
77
85
|
if (BigInt(priceRaw) <= 0n)
|
|
78
86
|
throw new Error("price must be > 0.");
|
|
87
|
+
if (normalizedPositionId) {
|
|
88
|
+
const positionsRes = await client.position.listPositions(address);
|
|
89
|
+
const positions = Array.isArray(positionsRes?.data) ? positionsRes.data : [];
|
|
90
|
+
const target = positions.find((position) => {
|
|
91
|
+
const positionId = String(position?.positionId ?? position?.position_id ?? "").trim().toLowerCase();
|
|
92
|
+
const positionPoolId = String(position?.poolId ?? position?.pool_id ?? "").trim().toLowerCase();
|
|
93
|
+
return positionId === normalizedPositionId.toLowerCase() && positionPoolId === String(poolId).toLowerCase();
|
|
94
|
+
});
|
|
95
|
+
if (!target) {
|
|
96
|
+
throw new Error(`Could not find live position for positionId=${normalizedPositionId} in poolId=${poolId}.`);
|
|
97
|
+
}
|
|
98
|
+
const liveDirection = Number(target?.direction);
|
|
99
|
+
if (!Number.isFinite(liveDirection) || liveDirection !== mappedDirection) {
|
|
100
|
+
throw new Error(`direction mismatch for positionId=${normalizedPositionId}: input=${mappedDirection}, live=${String(target?.direction ?? "unknown")}.`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
79
103
|
let tradingFeeRaw = "";
|
|
80
104
|
let tradingFeeMeta = { source: "user" };
|
|
81
105
|
const tradingFeeInput = String(args.tradingFee ?? "").trim();
|
|
@@ -107,7 +131,8 @@ export const executeTradeTool = {
|
|
|
107
131
|
}
|
|
108
132
|
const rateRaw = args.postOnly ? feeRes.data.makerFeeRate : feeRes.data.takerFeeRate;
|
|
109
133
|
const rateBig = BigInt(String(rateRaw ?? "0"));
|
|
110
|
-
|
|
134
|
+
const notionalQuoteRaw = computeQuoteNotionalRaw(BigInt(sizeRaw), BigInt(priceRaw), baseDecimals, quoteDecimals);
|
|
135
|
+
tradingFeeRaw = ((notionalQuoteRaw * rateBig) / 1000000n).toString();
|
|
111
136
|
tradingFeeMeta = { source: "computed", assetClass, riskTier, feeRate: String(rateRaw ?? "0") };
|
|
112
137
|
}
|
|
113
138
|
const mappedArgs = {
|
|
@@ -121,9 +121,6 @@ function toSymbol(row) {
|
|
|
121
121
|
return base;
|
|
122
122
|
return normalizePoolId(row?.poolId ?? row?.pool_id);
|
|
123
123
|
}
|
|
124
|
-
function sumRaw(baseRaw, quoteRaw) {
|
|
125
|
-
return BigInt(baseRaw || "0") + BigInt(quoteRaw || "0");
|
|
126
|
-
}
|
|
127
124
|
async function readErc20Balance(provider, tokenAddress, holder) {
|
|
128
125
|
const contract = new Contract(tokenAddress, ERC20_BALANCE_ABI, provider);
|
|
129
126
|
const balance = await contract.balanceOf(holder);
|
|
@@ -227,11 +224,10 @@ export const getMyLpHoldingsTool = {
|
|
|
227
224
|
});
|
|
228
225
|
}
|
|
229
226
|
items.sort((left, right) => {
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
return rightSum > leftSum ? 1 : -1;
|
|
227
|
+
const symbolCompare = String(left.symbol ?? "").localeCompare(String(right.symbol ?? ""));
|
|
228
|
+
if (symbolCompare !== 0)
|
|
229
|
+
return symbolCompare;
|
|
230
|
+
return String(left.poolId ?? "").localeCompare(String(right.poolId ?? ""));
|
|
235
231
|
});
|
|
236
232
|
const payload = {
|
|
237
233
|
meta: {
|
|
@@ -4,21 +4,8 @@ import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
|
4
4
|
import { getMarketDetail, resolvePool } from "../services/marketService.js";
|
|
5
5
|
import { getPoolInfo, getLiquidityInfo } from "../services/poolService.js";
|
|
6
6
|
import { extractErrorMessage } from "../utils/errorMessage.js";
|
|
7
|
-
import { parseUserPrice30 } from "../utils/units.js";
|
|
8
7
|
const INTEGER_RE = /^\d+$/;
|
|
9
8
|
const SIGNED_INTEGER_RE = /^-?\d+$/;
|
|
10
|
-
const DECIMAL_RE = /^\d+(\.\d+)?$/;
|
|
11
|
-
function normalizeMarketPrice30Input(value) {
|
|
12
|
-
const text = String(value ?? "").trim();
|
|
13
|
-
if (!text)
|
|
14
|
-
return "";
|
|
15
|
-
if (INTEGER_RE.test(text))
|
|
16
|
-
return text;
|
|
17
|
-
if (!DECIMAL_RE.test(text) && !/^raw:/i.test(text) && !/^human:/i.test(text)) {
|
|
18
|
-
throw new Error("marketPrice must be numeric, or prefixed with raw:/human:.");
|
|
19
|
-
}
|
|
20
|
-
return parseUserPrice30(text, "marketPrice");
|
|
21
|
-
}
|
|
22
9
|
function compactWarning(scope, err) {
|
|
23
10
|
const raw = extractErrorMessage(err);
|
|
24
11
|
const flat = raw.replace(/\s+/g, " ").trim();
|
|
@@ -211,8 +198,8 @@ export const getPoolMetadataTool = {
|
|
|
211
198
|
schema: {
|
|
212
199
|
poolId: z.string().optional().describe("Pool ID, Token Address, or Keyword"),
|
|
213
200
|
keyword: z.string().optional().describe("Market keyword (e.g. 'BTC')"),
|
|
214
|
-
includeLiquidity: z.boolean().default(false).describe("Whether to include liquidity depth (
|
|
215
|
-
marketPrice: z.union([z.string(), z.number()]).optional().describe("
|
|
201
|
+
includeLiquidity: z.boolean().default(false).describe("Whether to include liquidity depth (uses fresh oracle price automatically)"),
|
|
202
|
+
marketPrice: z.union([z.string(), z.number()]).optional().describe("Deprecated and ignored. MCP now uses fresh oracle price for liquidity depth."),
|
|
216
203
|
includeConfig: z.boolean().default(false).describe("Whether to include pool level configuration/limits"),
|
|
217
204
|
chainId: z.number().int().positive().optional().describe("Optional chainId override"),
|
|
218
205
|
},
|
|
@@ -245,10 +232,15 @@ export const getPoolMetadataTool = {
|
|
|
245
232
|
// 3. Optional: Liquidity Info
|
|
246
233
|
if (args.includeLiquidity) {
|
|
247
234
|
try {
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
235
|
+
const liquidityResult = await getLiquidityInfo(client, poolId, chainId);
|
|
236
|
+
results.liquidityInfo = liquidityResult.liquidityInfo;
|
|
237
|
+
results.liquidityInfoMeta = {
|
|
238
|
+
marketPriceSource: liquidityResult.marketPriceSource,
|
|
239
|
+
marketPrice: liquidityResult.marketPrice,
|
|
240
|
+
oraclePublishTime: liquidityResult.oraclePublishTime,
|
|
241
|
+
oracleType: liquidityResult.oracleType,
|
|
242
|
+
ignoredUserMarketPrice: args.marketPrice !== undefined ? String(args.marketPrice) : null,
|
|
243
|
+
};
|
|
252
244
|
}
|
|
253
245
|
catch (err) {
|
|
254
246
|
errors.push(compactWarning("liquidityInfo", err));
|
|
@@ -5,6 +5,7 @@ import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
|
5
5
|
import { resolvePool } from "../services/marketService.js";
|
|
6
6
|
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
7
7
|
import { extractErrorMessage } from "../utils/errorMessage.js";
|
|
8
|
+
import { normalizeLpSlippageRatio } from "../utils/slippage.js";
|
|
8
9
|
function normalizeAssetSymbol(value) {
|
|
9
10
|
const text = String(value ?? "").trim();
|
|
10
11
|
if (!text)
|
|
@@ -67,7 +68,7 @@ export const manageLiquidityTool = {
|
|
|
67
68
|
action: z.coerce.string().describe("'deposit' or 'withdraw' (aliases: add/remove/increase/decrease; case-insensitive)"),
|
|
68
69
|
poolType: z.enum(["BASE", "QUOTE"]).describe("'BASE' or 'QUOTE'"),
|
|
69
70
|
poolId: z.string().describe("Pool ID or Base Token Address"),
|
|
70
|
-
amount: z.coerce.
|
|
71
|
+
amount: z.coerce.string().describe("Amount in human-readable units string"),
|
|
71
72
|
slippage: z.coerce.number().min(0).describe("LP slippage ratio (e.g. 0.01 = 1%)"),
|
|
72
73
|
chainId: z.coerce.number().int().positive().optional().describe("Optional chainId override"),
|
|
73
74
|
},
|
|
@@ -75,9 +76,13 @@ export const manageLiquidityTool = {
|
|
|
75
76
|
try {
|
|
76
77
|
const { client, signer } = await resolveClient();
|
|
77
78
|
let { action, poolType, poolId } = args;
|
|
78
|
-
const
|
|
79
|
+
const amount = String(args.amount ?? "").trim();
|
|
80
|
+
const slippage = normalizeLpSlippageRatio(args.slippage);
|
|
79
81
|
const chainId = args.chainId ?? getChainId();
|
|
80
82
|
action = String(action ?? "").trim().toLowerCase();
|
|
83
|
+
if (!amount) {
|
|
84
|
+
throw new Error("amount is required.");
|
|
85
|
+
}
|
|
81
86
|
const validActions = new Set(["deposit", "withdraw", "add", "remove", "increase", "decrease"]);
|
|
82
87
|
if (!validActions.has(action)) {
|
|
83
88
|
throw new Error(`Invalid action: ${args.action}. Use deposit/withdraw or aliases add/remove/increase/decrease.`);
|
package/dist/tools/manageTpSl.js
CHANGED
|
@@ -29,7 +29,7 @@ function readSnapshotText(snapshot, keys) {
|
|
|
29
29
|
}
|
|
30
30
|
return "";
|
|
31
31
|
}
|
|
32
|
-
function
|
|
32
|
+
function normalizeSizeInput(text) {
|
|
33
33
|
const raw = String(text ?? "").trim();
|
|
34
34
|
if (!raw)
|
|
35
35
|
return "";
|
|
@@ -39,6 +39,17 @@ function normalizeUnitsInput(text) {
|
|
|
39
39
|
return `raw:${raw}`;
|
|
40
40
|
return raw;
|
|
41
41
|
}
|
|
42
|
+
function normalizePriceInput(text, source = "user") {
|
|
43
|
+
const raw = String(text ?? "").trim();
|
|
44
|
+
if (!raw)
|
|
45
|
+
return "";
|
|
46
|
+
if (/^(raw|human):/i.test(raw))
|
|
47
|
+
return raw;
|
|
48
|
+
if (source === "snapshot" && /^\d+$/.test(raw) && raw.length > 12) {
|
|
49
|
+
return `raw:${raw}`;
|
|
50
|
+
}
|
|
51
|
+
return raw;
|
|
52
|
+
}
|
|
42
53
|
function isExplicitZeroValue(value) {
|
|
43
54
|
if (value === undefined || value === null)
|
|
44
55
|
return false;
|
|
@@ -160,6 +171,58 @@ function resolvePositionSizeRaw(positionSnapshot, baseDecimals) {
|
|
|
160
171
|
}
|
|
161
172
|
return "";
|
|
162
173
|
}
|
|
174
|
+
function resolveEntryPriceInput(positionSnapshot) {
|
|
175
|
+
const rawCandidates = [
|
|
176
|
+
positionSnapshot?.entryPriceRaw,
|
|
177
|
+
positionSnapshot?.openPriceRaw,
|
|
178
|
+
positionSnapshot?.avgPriceRaw,
|
|
179
|
+
positionSnapshot?.averageOpenPriceRaw,
|
|
180
|
+
];
|
|
181
|
+
for (const value of rawCandidates) {
|
|
182
|
+
const text = String(value ?? "").trim();
|
|
183
|
+
if (INTEGER_RE.test(text))
|
|
184
|
+
return `raw:${text}`;
|
|
185
|
+
}
|
|
186
|
+
const humanCandidates = [
|
|
187
|
+
positionSnapshot?.entryPrice,
|
|
188
|
+
positionSnapshot?.openPrice,
|
|
189
|
+
positionSnapshot?.avgPrice,
|
|
190
|
+
positionSnapshot?.averageOpenPrice,
|
|
191
|
+
];
|
|
192
|
+
for (const value of humanCandidates) {
|
|
193
|
+
const text = String(value ?? "").trim();
|
|
194
|
+
if (text)
|
|
195
|
+
return text;
|
|
196
|
+
}
|
|
197
|
+
return "";
|
|
198
|
+
}
|
|
199
|
+
function validateTpSlPriceSemantics(direction, entryPriceInput, tpPriceInput, slPriceInput) {
|
|
200
|
+
if (!entryPriceInput) {
|
|
201
|
+
throw new Error("Unable to resolve entryPrice for TP/SL validation.");
|
|
202
|
+
}
|
|
203
|
+
const entryPriceRaw = BigInt(parseUserUnits(entryPriceInput, 30, "entryPrice"));
|
|
204
|
+
if (entryPriceRaw <= 0n) {
|
|
205
|
+
throw new Error("entryPrice must be > 0 for TP/SL validation.");
|
|
206
|
+
}
|
|
207
|
+
if (tpPriceInput) {
|
|
208
|
+
const tpPriceRaw = BigInt(parseUserUnits(tpPriceInput, 30, "tpPrice"));
|
|
209
|
+
if (direction === 0 && tpPriceRaw <= entryPriceRaw) {
|
|
210
|
+
throw new Error("LONG TP must be greater than entryPrice.");
|
|
211
|
+
}
|
|
212
|
+
if (direction === 1 && tpPriceRaw >= entryPriceRaw) {
|
|
213
|
+
throw new Error("SHORT TP must be less than entryPrice.");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (slPriceInput) {
|
|
217
|
+
const slPriceRaw = BigInt(parseUserUnits(slPriceInput, 30, "slPrice"));
|
|
218
|
+
if (direction === 0 && slPriceRaw >= entryPriceRaw) {
|
|
219
|
+
throw new Error("LONG SL must be less than entryPrice.");
|
|
220
|
+
}
|
|
221
|
+
if (direction === 1 && slPriceRaw <= entryPriceRaw) {
|
|
222
|
+
throw new Error("SHORT SL must be greater than entryPrice.");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
163
226
|
async function cancelTpSlByIntent(client, address, signer, chainId, args) {
|
|
164
227
|
if (args.orderId) {
|
|
165
228
|
const raw = await client.order.cancelAllOrders([String(args.orderId)], chainId);
|
|
@@ -265,12 +328,21 @@ export const manageTpSlTool = {
|
|
|
265
328
|
const snapshotTpSize = readSnapshotText(orderSnapshot, ["tpSize", "takeProfitSize"]);
|
|
266
329
|
const snapshotSlPrice = readSnapshotText(orderSnapshot, ["slPrice", "stopLossPrice", "slTriggerPrice"]);
|
|
267
330
|
const snapshotSlSize = readSnapshotText(orderSnapshot, ["slSize", "stopLossSize"]);
|
|
331
|
+
const orderPositionId = readSnapshotText(orderSnapshot, ["positionId", "position_id"]);
|
|
268
332
|
const size = isNonEmpty(args.size) ? String(args.size) : snapshotSize;
|
|
269
333
|
const price = isNonEmpty(args.price) ? String(args.price) : snapshotPrice;
|
|
270
334
|
const tpPrice = isNonEmpty(args.tpPrice) ? String(args.tpPrice) : snapshotTpPrice;
|
|
271
335
|
const tpSize = isNonEmpty(args.tpSize) ? String(args.tpSize) : snapshotTpSize;
|
|
272
336
|
const slPrice = isNonEmpty(args.slPrice) ? String(args.slPrice) : snapshotSlPrice;
|
|
273
337
|
const slSize = isNonEmpty(args.slSize) ? String(args.slSize) : snapshotSlSize;
|
|
338
|
+
const updateDirection = normalizeDirectionInput(args.direction ?? orderSnapshot?.direction);
|
|
339
|
+
const positionSnapshot = orderPositionId
|
|
340
|
+
? await findPositionSnapshot(client, address, args.poolId, orderPositionId)
|
|
341
|
+
: null;
|
|
342
|
+
if (updateDirection !== undefined) {
|
|
343
|
+
const entryPriceInput = resolveEntryPriceInput(positionSnapshot);
|
|
344
|
+
validateTpSlPriceSemantics(updateDirection, entryPriceInput, tpPrice || undefined, slPrice || undefined);
|
|
345
|
+
}
|
|
274
346
|
if (!size || !price) {
|
|
275
347
|
throw new Error("size and price are required for update. Provide them explicitly, or ensure orderId can be found via get_orders so they can be auto-resolved.");
|
|
276
348
|
}
|
|
@@ -288,12 +360,12 @@ export const manageTpSlTool = {
|
|
|
288
360
|
orderId: args.orderId,
|
|
289
361
|
marketId: market.marketId,
|
|
290
362
|
poolId: args.poolId,
|
|
291
|
-
size:
|
|
292
|
-
price:
|
|
293
|
-
tpPrice: tpPrice ?
|
|
294
|
-
tpSize: tpSize ?
|
|
295
|
-
slPrice: slPrice ?
|
|
296
|
-
slSize: slSize ?
|
|
363
|
+
size: normalizeSizeInput(size),
|
|
364
|
+
price: normalizePriceInput(price, isNonEmpty(args.price) ? "user" : "snapshot"),
|
|
365
|
+
tpPrice: tpPrice ? normalizePriceInput(tpPrice, isNonEmpty(args.tpPrice) ? "user" : "snapshot") : "0",
|
|
366
|
+
tpSize: tpSize ? normalizeSizeInput(tpSize) : "0",
|
|
367
|
+
slPrice: slPrice ? normalizePriceInput(slPrice, isNonEmpty(args.slPrice) ? "user" : "snapshot") : "0",
|
|
368
|
+
slSize: slSize ? normalizeSizeInput(slSize) : "0",
|
|
297
369
|
quoteToken: market.quoteToken,
|
|
298
370
|
useOrderCollateral: args.useOrderCollateral ?? true
|
|
299
371
|
}, chainId);
|
|
@@ -326,6 +398,8 @@ export const manageTpSlTool = {
|
|
|
326
398
|
if (resolvedDirection === undefined || !resolvedLeverage || Number(resolvedLeverage) <= 0) {
|
|
327
399
|
throw new Error("direction and leverage are required when creating new TP/SL.");
|
|
328
400
|
}
|
|
401
|
+
const entryPriceInput = resolveEntryPriceInput(positionSnapshot);
|
|
402
|
+
validateTpSlPriceSemantics(resolvedDirection, entryPriceInput, isNonEmpty(args.tpPrice) ? String(args.tpPrice) : undefined, isNonEmpty(args.slPrice) ? String(args.slPrice) : undefined);
|
|
329
403
|
let tpSizeInput = isNonEmpty(args.tpSize) ? String(args.tpSize) : "";
|
|
330
404
|
let slSizeInput = isNonEmpty(args.slSize) ? String(args.slSize) : "";
|
|
331
405
|
const needsTpSize = isNonEmpty(args.tpPrice) && !tpSizeInput;
|