@michaleffffff/mcp-trading-server 3.0.16 → 3.0.21
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 +38 -0
- package/README.md +25 -4
- package/TOOL_EXAMPLES.md +25 -1
- package/dist/prompts/tradingGuide.js +13 -7
- package/dist/server.js +2 -2
- package/dist/services/marketService.js +41 -0
- package/dist/services/poolService.js +15 -66
- 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 +14 -0
- package/dist/tools/executeTrade.js +26 -1
- package/dist/tools/getPoolMetadata.js +181 -0
- package/dist/tools/manageLiquidity.js +36 -4
- package/dist/tools/manageTpSl.js +81 -7
- package/dist/tools/openPositionSimple.js +37 -18
- 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 +1 -1
|
@@ -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
|
});
|
|
@@ -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.",
|
|
@@ -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 = {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { formatUnits } from "ethers";
|
|
2
3
|
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
3
4
|
import { getMarketDetail, resolvePool } from "../services/marketService.js";
|
|
4
5
|
import { getPoolInfo, getLiquidityInfo } from "../services/poolService.js";
|
|
5
6
|
import { extractErrorMessage } from "../utils/errorMessage.js";
|
|
6
7
|
import { parseUserPrice30 } from "../utils/units.js";
|
|
7
8
|
const INTEGER_RE = /^\d+$/;
|
|
9
|
+
const SIGNED_INTEGER_RE = /^-?\d+$/;
|
|
8
10
|
const DECIMAL_RE = /^\d+(\.\d+)?$/;
|
|
9
11
|
function normalizeMarketPrice30Input(value) {
|
|
10
12
|
const text = String(value ?? "").trim();
|
|
@@ -29,6 +31,180 @@ function compactWarning(scope, err) {
|
|
|
29
31
|
}
|
|
30
32
|
return `${scope}: ${flat}`;
|
|
31
33
|
}
|
|
34
|
+
function formatRawValue(value, decimals) {
|
|
35
|
+
const text = String(value ?? "").trim();
|
|
36
|
+
if (!INTEGER_RE.test(text))
|
|
37
|
+
return null;
|
|
38
|
+
try {
|
|
39
|
+
return formatUnits(text, decimals);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function buildFormattedAmount(value, decimals, symbol) {
|
|
46
|
+
const raw = String(value ?? "");
|
|
47
|
+
const formatted = formatRawValue(value, decimals);
|
|
48
|
+
return {
|
|
49
|
+
raw,
|
|
50
|
+
decimals,
|
|
51
|
+
formatted,
|
|
52
|
+
display: formatted && symbol ? `${formatted} ${symbol}` : formatted,
|
|
53
|
+
symbol: symbol ?? null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function buildFormattedPrice30(value, quoteSymbol) {
|
|
57
|
+
const raw = String(value ?? "");
|
|
58
|
+
const formatted = formatRawValue(value, 30);
|
|
59
|
+
return {
|
|
60
|
+
raw,
|
|
61
|
+
decimals: 30,
|
|
62
|
+
formatted,
|
|
63
|
+
display: formatted && quoteSymbol ? `${formatted} ${quoteSymbol}` : formatted,
|
|
64
|
+
symbol: quoteSymbol ?? null,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function buildFormattedRatio18(value) {
|
|
68
|
+
const raw = String(value ?? "");
|
|
69
|
+
const formatted = formatRawValue(value, 18);
|
|
70
|
+
return {
|
|
71
|
+
raw,
|
|
72
|
+
decimals: 18,
|
|
73
|
+
formatted,
|
|
74
|
+
display: formatted,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function buildRawScalar(value) {
|
|
78
|
+
const raw = String(value ?? "").trim();
|
|
79
|
+
if (!SIGNED_INTEGER_RE.test(raw)) {
|
|
80
|
+
return { raw, withCommas: raw || null };
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
return {
|
|
84
|
+
raw,
|
|
85
|
+
withCommas: BigInt(raw).toLocaleString("en-US"),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return { raw, withCommas: raw };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function formatScaledIntegerString(raw, decimals) {
|
|
93
|
+
if (!SIGNED_INTEGER_RE.test(raw))
|
|
94
|
+
return null;
|
|
95
|
+
const negative = raw.startsWith("-");
|
|
96
|
+
const digits = negative ? raw.slice(1) : raw;
|
|
97
|
+
const padded = digits.padStart(decimals + 1, "0");
|
|
98
|
+
const intPart = padded.slice(0, -decimals) || "0";
|
|
99
|
+
const fracPart = padded.slice(-decimals).replace(/0+$/, "");
|
|
100
|
+
const normalized = fracPart ? `${intPart}.${fracPart}` : intPart;
|
|
101
|
+
return negative ? `-${normalized}` : normalized;
|
|
102
|
+
}
|
|
103
|
+
function buildFundingRateInfo(value) {
|
|
104
|
+
const base = buildRawScalar(value);
|
|
105
|
+
const raw = String(value ?? "").trim();
|
|
106
|
+
if (!SIGNED_INTEGER_RE.test(raw))
|
|
107
|
+
return base;
|
|
108
|
+
try {
|
|
109
|
+
const rawBig = BigInt(raw);
|
|
110
|
+
const perDayRaw = rawBig * 86400n;
|
|
111
|
+
const percentPerSecond = formatScaledIntegerString(raw, 12);
|
|
112
|
+
const percentPerDay = formatScaledIntegerString(perDayRaw.toString(), 12);
|
|
113
|
+
return {
|
|
114
|
+
...base,
|
|
115
|
+
scale: "1e12 => percent",
|
|
116
|
+
percentPerSecond,
|
|
117
|
+
displayPerSecond: percentPerSecond ? `${percentPerSecond}%/秒` : null,
|
|
118
|
+
percentPerDay,
|
|
119
|
+
displayPerDay: percentPerDay ? `${percentPerDay}%/天` : null,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return base;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function buildTimestampInfo(value) {
|
|
127
|
+
const raw = String(value ?? "").trim();
|
|
128
|
+
const base = buildRawScalar(value);
|
|
129
|
+
if (!INTEGER_RE.test(raw)) {
|
|
130
|
+
return base;
|
|
131
|
+
}
|
|
132
|
+
const seconds = Number(raw);
|
|
133
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
134
|
+
return base;
|
|
135
|
+
}
|
|
136
|
+
const isoUtc = new Date(seconds * 1000).toISOString();
|
|
137
|
+
const secondsUntil = seconds - Math.floor(Date.now() / 1000);
|
|
138
|
+
return {
|
|
139
|
+
...base,
|
|
140
|
+
isoUtc,
|
|
141
|
+
secondsUntil,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function computeQuoteNotionalDisplay(baseRaw, price30Raw, baseDecimals, quoteDecimals, quoteSymbol) {
|
|
145
|
+
const baseText = String(baseRaw ?? "").trim();
|
|
146
|
+
const priceText = String(price30Raw ?? "").trim();
|
|
147
|
+
if (!INTEGER_RE.test(baseText) || !INTEGER_RE.test(priceText))
|
|
148
|
+
return null;
|
|
149
|
+
try {
|
|
150
|
+
const notionalRaw = (BigInt(baseText) * BigInt(priceText) * (10n ** BigInt(quoteDecimals))) /
|
|
151
|
+
(10n ** BigInt(baseDecimals + 30));
|
|
152
|
+
return buildFormattedAmount(notionalRaw.toString(), quoteDecimals, quoteSymbol);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function formatPoolInfoSnapshot(poolInfo, marketDetail) {
|
|
159
|
+
if (!poolInfo || typeof poolInfo !== "object")
|
|
160
|
+
return null;
|
|
161
|
+
const baseSymbol = String(marketDetail?.baseSymbol ?? "BASE");
|
|
162
|
+
const quoteSymbol = String(marketDetail?.quoteSymbol ?? "QUOTE");
|
|
163
|
+
const baseDecimals = Number(marketDetail?.baseDecimals ?? 18);
|
|
164
|
+
const quoteDecimals = Number(marketDetail?.quoteDecimals ?? 6);
|
|
165
|
+
return {
|
|
166
|
+
quotePool: poolInfo.quotePool ? {
|
|
167
|
+
poolToken: poolInfo.quotePool.poolToken ?? null,
|
|
168
|
+
exchangeRate: buildFormattedRatio18(poolInfo.quotePool.exchangeRate),
|
|
169
|
+
poolTokenPrice: buildFormattedPrice30(poolInfo.quotePool.poolTokenPrice, quoteSymbol),
|
|
170
|
+
poolTokenSupply: buildFormattedAmount(poolInfo.quotePool.poolTokenSupply, 18, `m${quoteSymbol}.${baseSymbol}`),
|
|
171
|
+
totalDebt: buildFormattedAmount(poolInfo.quotePool.totalDebt, quoteDecimals, quoteSymbol),
|
|
172
|
+
baseCollateral: buildFormattedAmount(poolInfo.quotePool.baseCollateral, baseDecimals, baseSymbol),
|
|
173
|
+
} : null,
|
|
174
|
+
basePool: poolInfo.basePool ? {
|
|
175
|
+
poolToken: poolInfo.basePool.poolToken ?? null,
|
|
176
|
+
exchangeRate: buildFormattedRatio18(poolInfo.basePool.exchangeRate),
|
|
177
|
+
poolTokenPrice: buildFormattedPrice30(poolInfo.basePool.poolTokenPrice, quoteSymbol),
|
|
178
|
+
poolTokenSupply: buildFormattedAmount(poolInfo.basePool.poolTokenSupply, 18, `m${baseSymbol}.${quoteSymbol}`),
|
|
179
|
+
totalDebt: buildFormattedAmount(poolInfo.basePool.totalDebt, quoteDecimals, quoteSymbol),
|
|
180
|
+
baseCollateral: buildFormattedAmount(poolInfo.basePool.baseCollateral, baseDecimals, baseSymbol),
|
|
181
|
+
} : null,
|
|
182
|
+
reserveInfo: poolInfo.reserveInfo ? {
|
|
183
|
+
baseTotalAmount: buildFormattedAmount(poolInfo.reserveInfo.baseTotalAmount, baseDecimals, baseSymbol),
|
|
184
|
+
baseReservedAmount: buildFormattedAmount(poolInfo.reserveInfo.baseReservedAmount, baseDecimals, baseSymbol),
|
|
185
|
+
quoteTotalAmount: buildFormattedAmount(poolInfo.reserveInfo.quoteTotalAmount, quoteDecimals, quoteSymbol),
|
|
186
|
+
quoteReservedAmount: buildFormattedAmount(poolInfo.reserveInfo.quoteReservedAmount, quoteDecimals, quoteSymbol),
|
|
187
|
+
} : null,
|
|
188
|
+
fundingInfo: poolInfo.fundingInfo ? {
|
|
189
|
+
nextFundingRate: buildFundingRateInfo(poolInfo.fundingInfo.nextFundingRate),
|
|
190
|
+
lastFundingFeeTracker: buildRawScalar(poolInfo.fundingInfo.lastFundingFeeTracker),
|
|
191
|
+
nextEpochTime: buildTimestampInfo(poolInfo.fundingInfo.nextEpochTime),
|
|
192
|
+
} : null,
|
|
193
|
+
ioTracker: poolInfo.ioTracker ? {
|
|
194
|
+
tracker: buildFormattedAmount(poolInfo.ioTracker.tracker, baseDecimals, baseSymbol),
|
|
195
|
+
longSize: buildFormattedAmount(poolInfo.ioTracker.longSize, baseDecimals, baseSymbol),
|
|
196
|
+
shortSize: buildFormattedAmount(poolInfo.ioTracker.shortSize, baseDecimals, baseSymbol),
|
|
197
|
+
poolEntryPrice: buildFormattedPrice30(poolInfo.ioTracker.poolEntryPrice, quoteSymbol),
|
|
198
|
+
trackerNotionalAtEntry: computeQuoteNotionalDisplay(poolInfo.ioTracker.tracker, poolInfo.ioTracker.poolEntryPrice, baseDecimals, quoteDecimals, quoteSymbol),
|
|
199
|
+
longNotionalAtEntry: computeQuoteNotionalDisplay(poolInfo.ioTracker.longSize, poolInfo.ioTracker.poolEntryPrice, baseDecimals, quoteDecimals, quoteSymbol),
|
|
200
|
+
shortNotionalAtEntry: computeQuoteNotionalDisplay(poolInfo.ioTracker.shortSize, poolInfo.ioTracker.poolEntryPrice, baseDecimals, quoteDecimals, quoteSymbol),
|
|
201
|
+
} : null,
|
|
202
|
+
liquidityInfo: poolInfo.liquidityInfo ? {
|
|
203
|
+
windowCaps: buildFormattedAmount(poolInfo.liquidityInfo.windowCaps, quoteDecimals, quoteSymbol),
|
|
204
|
+
openInterest: buildFormattedAmount(poolInfo.liquidityInfo.openInterest, 18, quoteSymbol),
|
|
205
|
+
} : null,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
32
208
|
export const getPoolMetadataTool = {
|
|
33
209
|
name: "get_pool_metadata",
|
|
34
210
|
description: "[MARKET] Get comprehensive metadata for a pool (market detail, on-chain info, liquidity, and limits).",
|
|
@@ -57,6 +233,11 @@ export const getPoolMetadataTool = {
|
|
|
57
233
|
// 2. Pool Info (Reserves, Utilization)
|
|
58
234
|
try {
|
|
59
235
|
results.poolInfo = await getPoolInfo(poolId, chainId, client);
|
|
236
|
+
const rawMarketDetail = results.marketDetail?.data ?? results.marketDetail;
|
|
237
|
+
const formatted = formatPoolInfoSnapshot(results.poolInfo, rawMarketDetail);
|
|
238
|
+
if (formatted) {
|
|
239
|
+
results.poolInfoFormatted = formatted;
|
|
240
|
+
}
|
|
60
241
|
}
|
|
61
242
|
catch (err) {
|
|
62
243
|
errors.push(compactWarning("poolInfo", err));
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { formatUnits } from "ethers";
|
|
2
3
|
import { quoteDeposit, quoteWithdraw, baseDeposit, baseWithdraw, getLpPrice, } from "../services/poolService.js";
|
|
3
4
|
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
4
5
|
import { resolvePool } from "../services/marketService.js";
|
|
5
6
|
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
6
7
|
import { extractErrorMessage } from "../utils/errorMessage.js";
|
|
8
|
+
import { normalizeLpSlippageRatio } from "../utils/slippage.js";
|
|
7
9
|
function normalizeAssetSymbol(value) {
|
|
8
10
|
const text = String(value ?? "").trim();
|
|
9
11
|
if (!text)
|
|
@@ -45,6 +47,20 @@ function resolveLpAssetNames(detail) {
|
|
|
45
47
|
quoteLpAssetName,
|
|
46
48
|
};
|
|
47
49
|
}
|
|
50
|
+
function formatLpPricePayload(value, quoteSymbol) {
|
|
51
|
+
const raw = String(value ?? "").trim();
|
|
52
|
+
if (!/^\d+$/.test(raw)) {
|
|
53
|
+
return { raw, formatted: null, decimals: 30, symbol: quoteSymbol ?? null };
|
|
54
|
+
}
|
|
55
|
+
const formatted = formatUnits(raw, 30);
|
|
56
|
+
return {
|
|
57
|
+
raw,
|
|
58
|
+
formatted,
|
|
59
|
+
decimals: 30,
|
|
60
|
+
symbol: quoteSymbol ?? null,
|
|
61
|
+
display: quoteSymbol ? `${formatted} ${quoteSymbol}` : formatted,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
48
64
|
export const manageLiquidityTool = {
|
|
49
65
|
name: "manage_liquidity",
|
|
50
66
|
description: "[LIQUIDITY] Add or withdraw liquidity from a BASE or QUOTE pool. Success response includes LP naming metadata: base `mBASE.QUOTE`, quote `mQUOTE.BASE`, plus `operatedLpAssetName` based on poolType.",
|
|
@@ -52,7 +68,7 @@ export const manageLiquidityTool = {
|
|
|
52
68
|
action: z.coerce.string().describe("'deposit' or 'withdraw' (aliases: add/remove/increase/decrease; case-insensitive)"),
|
|
53
69
|
poolType: z.enum(["BASE", "QUOTE"]).describe("'BASE' or 'QUOTE'"),
|
|
54
70
|
poolId: z.string().describe("Pool ID or Base Token Address"),
|
|
55
|
-
amount: z.coerce.
|
|
71
|
+
amount: z.coerce.string().describe("Amount in human-readable units string"),
|
|
56
72
|
slippage: z.coerce.number().min(0).describe("LP slippage ratio (e.g. 0.01 = 1%)"),
|
|
57
73
|
chainId: z.coerce.number().int().positive().optional().describe("Optional chainId override"),
|
|
58
74
|
},
|
|
@@ -60,9 +76,13 @@ export const manageLiquidityTool = {
|
|
|
60
76
|
try {
|
|
61
77
|
const { client, signer } = await resolveClient();
|
|
62
78
|
let { action, poolType, poolId } = args;
|
|
63
|
-
const
|
|
79
|
+
const amount = String(args.amount ?? "").trim();
|
|
80
|
+
const slippage = normalizeLpSlippageRatio(args.slippage);
|
|
64
81
|
const chainId = args.chainId ?? getChainId();
|
|
65
82
|
action = String(action ?? "").trim().toLowerCase();
|
|
83
|
+
if (!amount) {
|
|
84
|
+
throw new Error("amount is required.");
|
|
85
|
+
}
|
|
66
86
|
const validActions = new Set(["deposit", "withdraw", "add", "remove", "increase", "decrease"]);
|
|
67
87
|
if (!validActions.has(action)) {
|
|
68
88
|
throw new Error(`Invalid action: ${args.action}. Use deposit/withdraw or aliases add/remove/increase/decrease.`);
|
|
@@ -135,8 +155,20 @@ export const getLpPriceTool = {
|
|
|
135
155
|
},
|
|
136
156
|
handler: async (args) => {
|
|
137
157
|
try {
|
|
138
|
-
const
|
|
139
|
-
|
|
158
|
+
const { client } = await resolveClient();
|
|
159
|
+
const chainId = args.chainId ?? getChainId();
|
|
160
|
+
const poolId = await resolvePool(client, args.poolId, undefined, chainId);
|
|
161
|
+
const detailRes = await client.markets.getMarketDetail({ chainId, poolId }).catch(() => null);
|
|
162
|
+
const detail = detailRes?.data || (detailRes?.marketId ? detailRes : null);
|
|
163
|
+
const quoteSymbol = String(detail?.quoteSymbol ?? "").trim() || null;
|
|
164
|
+
const rawPrice = await getLpPrice(args.poolType, poolId, chainId);
|
|
165
|
+
const payload = {
|
|
166
|
+
raw: String(rawPrice ?? ""),
|
|
167
|
+
formatted: formatLpPricePayload(rawPrice, quoteSymbol ?? undefined),
|
|
168
|
+
poolType: args.poolType,
|
|
169
|
+
poolId,
|
|
170
|
+
};
|
|
171
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: payload }, (k, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
140
172
|
}
|
|
141
173
|
catch (error) {
|
|
142
174
|
return { content: [{ type: "text", text: `Error: ${extractErrorMessage(error)}` }], isError: true };
|
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;
|
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { OrderType } from "@myx-trade/sdk";
|
|
3
3
|
import { formatUnits } from "ethers";
|
|
4
4
|
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
5
|
-
import { resolvePool } from "../services/marketService.js";
|
|
5
|
+
import { resolvePool, getFreshOraclePrice } from "../services/marketService.js";
|
|
6
6
|
import { openPosition } from "../services/tradeService.js";
|
|
7
7
|
import { isZeroAddress, normalizeAddress } from "../utils/address.js";
|
|
8
8
|
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
@@ -25,6 +25,29 @@ function asBigint(raw, label) {
|
|
|
25
25
|
throw new Error(`${label} must be an integer string.`);
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
+
function computeQuoteNotionalRaw(sizeRaw, priceRaw30, baseDecimals, quoteDecimals) {
|
|
29
|
+
const numerator = sizeRaw * priceRaw30 * pow10(quoteDecimals);
|
|
30
|
+
const denominator = pow10(baseDecimals + 30);
|
|
31
|
+
return numerator / denominator;
|
|
32
|
+
}
|
|
33
|
+
async function getRequiredApprovalSpendRaw(client, marketId, args, chainId) {
|
|
34
|
+
const networkFeeText = String(await client.utils.getNetworkFee(marketId, chainId) ?? "").trim();
|
|
35
|
+
if (!/^\d+$/.test(networkFeeText)) {
|
|
36
|
+
throw new Error(`Failed to resolve networkFee for marketId=${marketId}.`);
|
|
37
|
+
}
|
|
38
|
+
const baseNetworkFeeRaw = BigInt(networkFeeText);
|
|
39
|
+
if (baseNetworkFeeRaw <= 0n) {
|
|
40
|
+
throw new Error(`networkFee must be > 0 for marketId=${marketId}.`);
|
|
41
|
+
}
|
|
42
|
+
let executionOrderCount = 1n;
|
|
43
|
+
if (args.tpPrice && BigInt(String(args.tpSize ?? "0")) > 0n)
|
|
44
|
+
executionOrderCount += 1n;
|
|
45
|
+
if (args.slPrice && BigInt(String(args.slSize ?? "0")) > 0n)
|
|
46
|
+
executionOrderCount += 1n;
|
|
47
|
+
return (args.collateralRaw +
|
|
48
|
+
BigInt(args.tradingFeeRaw) +
|
|
49
|
+
(baseNetworkFeeRaw * executionOrderCount)).toString();
|
|
50
|
+
}
|
|
28
51
|
function pickMarketDetail(res) {
|
|
29
52
|
if (!res)
|
|
30
53
|
return null;
|
|
@@ -128,19 +151,9 @@ export const openPositionSimpleTool = {
|
|
|
128
151
|
let price30;
|
|
129
152
|
let priceMeta = { source: "user", publishTime: null, oracleType: null, human: null };
|
|
130
153
|
if (orderType === OrderType.MARKET) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
priceMeta = { source: "oracle", publishTime: oracle.publishTime, oracleType: oracle.oracleType, human: oracle.price };
|
|
135
|
-
}
|
|
136
|
-
catch (e) {
|
|
137
|
-
const tickers = await client.markets.getTickerList({ chainId, poolIds: [poolId] });
|
|
138
|
-
const row = Array.isArray(tickers) ? tickers[0] : tickers?.data?.[0];
|
|
139
|
-
if (!row?.price)
|
|
140
|
-
throw new Error(`Failed to fetch oracle and ticker price for poolId=${poolId}: ${e?.message || e}`);
|
|
141
|
-
price30 = parseUserPrice30(row.price, "marketPrice");
|
|
142
|
-
priceMeta = { source: "ticker", publishTime: null, oracleType: null, human: row.price };
|
|
143
|
-
}
|
|
154
|
+
const oracle = await getFreshOraclePrice(client, poolId, chainId);
|
|
155
|
+
price30 = parseUserPrice30(oracle.price, "oraclePrice");
|
|
156
|
+
priceMeta = { source: "oracle", publishTime: oracle.publishTime, oracleType: oracle.oracleType, human: oracle.price };
|
|
144
157
|
}
|
|
145
158
|
else {
|
|
146
159
|
const userPrice = String(args.price ?? "").trim();
|
|
@@ -219,7 +232,8 @@ export const openPositionSimpleTool = {
|
|
|
219
232
|
const rateRaw = postOnly ? feeData.makerFeeRate : feeData.takerFeeRate;
|
|
220
233
|
tradingFeeMeta.feeRate = rateRaw;
|
|
221
234
|
const rate = asBigint(String(rateRaw), "feeRate");
|
|
222
|
-
const
|
|
235
|
+
const notionalQuoteRaw = computeQuoteNotionalRaw(sizeRawBig, price30Big, baseDecimals, quoteDecimals);
|
|
236
|
+
const fee = (notionalQuoteRaw * rate) / 1000000n;
|
|
223
237
|
tradingFeeRaw = fee.toString();
|
|
224
238
|
}
|
|
225
239
|
else {
|
|
@@ -274,9 +288,14 @@ export const openPositionSimpleTool = {
|
|
|
274
288
|
// 7) Optional approval
|
|
275
289
|
let approval = null;
|
|
276
290
|
if (args.autoApprove) {
|
|
277
|
-
const requiredApprovalRaw =
|
|
278
|
-
|
|
279
|
-
:
|
|
291
|
+
const requiredApprovalRaw = await getRequiredApprovalSpendRaw(client, marketId, {
|
|
292
|
+
collateralRaw: collateralRawBig,
|
|
293
|
+
tradingFeeRaw: String(tradingFeeRaw),
|
|
294
|
+
tpPrice: prep.tpPrice,
|
|
295
|
+
tpSize: prep.tpSize,
|
|
296
|
+
slPrice: prep.slPrice,
|
|
297
|
+
slSize: prep.slSize,
|
|
298
|
+
}, chainId);
|
|
280
299
|
const needApproval = await client.utils.needsApproval(address, chainId, quoteToken, requiredApprovalRaw);
|
|
281
300
|
if (needApproval) {
|
|
282
301
|
const approveAmount = args.approveMax ? MAX_UINT256 : requiredApprovalRaw;
|