@michaleffffff/mcp-trading-server 3.0.4 → 3.0.7
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 +42 -6
- package/README.md +49 -414
- package/TOOL_EXAMPLES.md +63 -559
- package/dist/auth/resolveClient.js +19 -2
- package/dist/prompts/tradingGuide.js +16 -23
- package/dist/server.js +20 -2
- package/dist/services/balanceService.js +4 -4
- package/dist/services/marketService.js +48 -12
- package/dist/services/poolService.js +45 -44
- package/dist/services/tradeService.js +51 -23
- package/dist/tools/accountInfo.js +5 -134
- package/dist/tools/accountTransfer.js +7 -12
- package/dist/tools/adjustMargin.js +1 -1
- package/dist/tools/cancelOrders.js +71 -0
- package/dist/tools/checkAccountReady.js +75 -0
- package/dist/tools/checkApproval.js +1 -1
- package/dist/tools/closeAllPositions.js +9 -7
- package/dist/tools/closePosition.js +18 -11
- package/dist/tools/createPerpMarket.js +1 -1
- package/dist/tools/executeTrade.js +6 -6
- package/dist/tools/findPool.js +26 -0
- package/dist/tools/getAccountSnapshot.js +47 -0
- package/dist/tools/getAllTickers.js +5 -4
- package/dist/tools/getBaseDetail.js +1 -1
- package/dist/tools/getKline.js +7 -4
- package/dist/tools/getMyLpHoldings.js +3 -2
- package/dist/tools/getNetworkFee.js +1 -1
- package/dist/tools/getOrders.js +46 -0
- package/dist/tools/getPoolMetadata.js +95 -0
- package/dist/tools/getPositionsAll.js +80 -0
- package/dist/tools/{getMarketPrice.js → getPrice.js} +8 -5
- package/dist/tools/getUserTradingFeeRate.js +66 -3
- package/dist/tools/index.js +15 -19
- package/dist/tools/listPools.js +53 -0
- package/dist/tools/manageLiquidity.js +4 -4
- package/dist/tools/manageTpSl.js +234 -0
- package/dist/tools/openPositionSimple.js +27 -8
- package/dist/tools/searchTools.js +35 -0
- package/dist/utils/mappings.js +10 -7
- package/package.json +2 -2
- package/dist/tools/cancelAllOrders.js +0 -57
- package/dist/tools/cancelOrder.js +0 -22
- package/dist/tools/getAccountVipInfo.js +0 -20
- package/dist/tools/getKlineLatestBar.js +0 -28
- package/dist/tools/getOraclePrice.js +0 -22
- package/dist/tools/getPoolList.js +0 -17
- package/dist/tools/getPoolSymbolAll.js +0 -16
- package/dist/tools/getPositions.js +0 -77
- package/dist/tools/marketInfo.js +0 -88
- package/dist/tools/orderQueries.js +0 -51
- package/dist/tools/poolConfig.js +0 -22
- package/dist/tools/positionHistory.js +0 -28
- package/dist/tools/searchMarket.js +0 -21
- package/dist/tools/setTpSl.js +0 -50
- package/dist/tools/updateOrderTpSl.js +0 -34
|
@@ -1,141 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
3
|
-
import { getBalances, getMarginBalance } from "../services/balanceService.js";
|
|
4
3
|
import { getTradeFlowTypeDesc } from "../utils/mappings.js";
|
|
5
|
-
function readErrorMessage(error) {
|
|
6
|
-
if (error instanceof Error)
|
|
7
|
-
return error.message;
|
|
8
|
-
if (typeof error === "string")
|
|
9
|
-
return error;
|
|
10
|
-
if (error && typeof error === "object" && "message" in error) {
|
|
11
|
-
return String(error.message);
|
|
12
|
-
}
|
|
13
|
-
return String(error ?? "unknown error");
|
|
14
|
-
}
|
|
15
|
-
function assertSdkReadSuccess(result, actionName) {
|
|
16
|
-
if (!result || typeof result !== "object" || Array.isArray(result))
|
|
17
|
-
return;
|
|
18
|
-
if (!Object.prototype.hasOwnProperty.call(result, "code"))
|
|
19
|
-
return;
|
|
20
|
-
const code = Number(result.code);
|
|
21
|
-
if (!Number.isFinite(code) || code === 0)
|
|
22
|
-
return;
|
|
23
|
-
const payload = result.data;
|
|
24
|
-
const hasPayload = payload !== null &&
|
|
25
|
-
payload !== undefined &&
|
|
26
|
-
(!Array.isArray(payload) || payload.length > 0);
|
|
27
|
-
if (code > 0 && hasPayload) {
|
|
28
|
-
// Keep aligned with server-side read normalization:
|
|
29
|
-
// positive non-zero code with usable payload is warning-like.
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
const message = String(result.msg ?? result.message ?? "unknown error");
|
|
33
|
-
throw new Error(`${actionName} failed: code=${code}, msg=${message}`);
|
|
34
|
-
}
|
|
35
|
-
function normalizeAccountInfo(result) {
|
|
36
|
-
if (!(result && result.code === 0 && Array.isArray(result.data))) {
|
|
37
|
-
return result;
|
|
38
|
-
}
|
|
39
|
-
const values = result.data;
|
|
40
|
-
return {
|
|
41
|
-
...result,
|
|
42
|
-
data: {
|
|
43
|
-
freeMargin: values[0],
|
|
44
|
-
walletBalance: values[1],
|
|
45
|
-
freeBaseAmount: values[2],
|
|
46
|
-
baseProfit: values[3],
|
|
47
|
-
quoteProfit: values[4],
|
|
48
|
-
reservedAmount: values[5],
|
|
49
|
-
releaseTime: values[6],
|
|
50
|
-
tradeableMargin: (BigInt(values[0]) + BigInt(values[1]) + (BigInt(values[6]) === 0n ? BigInt(values[4]) : 0n)).toString(),
|
|
51
|
-
baseProfitStatus: "base token to be unlocked",
|
|
52
|
-
quoteProfitStatus: BigInt(values[6]) > 0n ? "quote token to be unlocked" : "quote token unlocked/available"
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
export const getAccountTool = {
|
|
57
|
-
name: "get_account",
|
|
58
|
-
description: "Unified account snapshot. Distinguishes wallet balance vs trading-account metrics. Provide poolId for full trading-account details.",
|
|
59
|
-
schema: {
|
|
60
|
-
poolId: z.string().optional().describe("Optional Pool ID. Required to include trading-account metrics (account info + margin)."),
|
|
61
|
-
},
|
|
62
|
-
handler: async (args) => {
|
|
63
|
-
try {
|
|
64
|
-
const { client, address } = await resolveClient();
|
|
65
|
-
const chainId = getChainId();
|
|
66
|
-
const poolId = args.poolId?.trim();
|
|
67
|
-
const overview = {
|
|
68
|
-
meta: {
|
|
69
|
-
address,
|
|
70
|
-
chainId,
|
|
71
|
-
poolId: poolId ?? null,
|
|
72
|
-
partial: false,
|
|
73
|
-
},
|
|
74
|
-
wallet: {
|
|
75
|
-
scope: "wallet_balance",
|
|
76
|
-
description: "On-chain wallet quote-token balance (funds not yet deposited into trading account).",
|
|
77
|
-
quoteTokenBalance: null,
|
|
78
|
-
raw: null,
|
|
79
|
-
error: null,
|
|
80
|
-
},
|
|
81
|
-
tradingAccount: {
|
|
82
|
-
scope: "trading_account_balance",
|
|
83
|
-
description: poolId
|
|
84
|
-
? "Trading-account metrics for the selected pool."
|
|
85
|
-
: "Provide poolId to include trading-account metrics (account info + margin).",
|
|
86
|
-
poolId: poolId ?? null,
|
|
87
|
-
accountInfo: null,
|
|
88
|
-
marginBalance: null,
|
|
89
|
-
error: null,
|
|
90
|
-
},
|
|
91
|
-
warnings: [],
|
|
92
|
-
};
|
|
93
|
-
try {
|
|
94
|
-
const walletBalances = await getBalances(client, address);
|
|
95
|
-
assertSdkReadSuccess(walletBalances, "getWalletQuoteTokenBalance");
|
|
96
|
-
overview.wallet.quoteTokenBalance = walletBalances?.data ?? walletBalances;
|
|
97
|
-
overview.wallet.raw = walletBalances;
|
|
98
|
-
}
|
|
99
|
-
catch (walletError) {
|
|
100
|
-
overview.meta.partial = true;
|
|
101
|
-
overview.wallet.error = readErrorMessage(walletError);
|
|
102
|
-
overview.warnings.push(`wallet section failed: ${overview.wallet.error}`);
|
|
103
|
-
}
|
|
104
|
-
if (poolId) {
|
|
105
|
-
try {
|
|
106
|
-
const [accountInfoRaw, marginBalanceRaw] = await Promise.all([
|
|
107
|
-
client.account.getAccountInfo(chainId, address, poolId),
|
|
108
|
-
getMarginBalance(client, address, poolId),
|
|
109
|
-
]);
|
|
110
|
-
assertSdkReadSuccess(accountInfoRaw, "getAccountInfo");
|
|
111
|
-
assertSdkReadSuccess(marginBalanceRaw, "getAvailableMarginBalance");
|
|
112
|
-
overview.tradingAccount.accountInfo = normalizeAccountInfo(accountInfoRaw);
|
|
113
|
-
overview.tradingAccount.marginBalance = marginBalanceRaw;
|
|
114
|
-
}
|
|
115
|
-
catch (tradingError) {
|
|
116
|
-
overview.meta.partial = true;
|
|
117
|
-
overview.tradingAccount.error = readErrorMessage(tradingError);
|
|
118
|
-
overview.warnings.push(`tradingAccount section failed: ${overview.tradingAccount.error}`);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if (overview.warnings.length === 0) {
|
|
122
|
-
delete overview.warnings;
|
|
123
|
-
}
|
|
124
|
-
const hasWalletData = overview.wallet.quoteTokenBalance !== null;
|
|
125
|
-
const hasTradingData = overview.tradingAccount.accountInfo !== null || overview.tradingAccount.marginBalance !== null;
|
|
126
|
-
if (!hasWalletData && !hasTradingData) {
|
|
127
|
-
throw new Error("Failed to build account snapshot: wallet and trading-account sections both unavailable.");
|
|
128
|
-
}
|
|
129
|
-
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: overview }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
130
|
-
}
|
|
131
|
-
catch (error) {
|
|
132
|
-
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
133
|
-
}
|
|
134
|
-
},
|
|
135
|
-
};
|
|
136
4
|
export const getTradeFlowTool = {
|
|
137
5
|
name: "get_trade_flow",
|
|
138
|
-
description: "Get account trade flow / transaction history.",
|
|
6
|
+
description: "[ACCOUNT] Get account trade flow / transaction history.",
|
|
139
7
|
schema: {
|
|
140
8
|
poolId: z.string().optional().describe("Optional pool ID filter."),
|
|
141
9
|
limit: z.number().int().positive().optional().describe("Results per page (default 20)"),
|
|
@@ -145,7 +13,10 @@ export const getTradeFlowTool = {
|
|
|
145
13
|
const { client, address } = await resolveClient();
|
|
146
14
|
const chainId = getChainId();
|
|
147
15
|
const query = { chainId, poolId: args.poolId, limit: args.limit ?? 20 };
|
|
148
|
-
const
|
|
16
|
+
const accessToken = typeof client?.getAccessToken === "function"
|
|
17
|
+
? (await client.getAccessToken()) || ""
|
|
18
|
+
: "";
|
|
19
|
+
const result = await client.api.getTradeFlow({ ...query, address, accessToken });
|
|
149
20
|
const enhancedData = (result?.data || []).map((flow) => ({
|
|
150
21
|
...flow,
|
|
151
22
|
typeDesc: getTradeFlowTypeDesc(flow.type)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
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
5
|
const MAX_UINT256 = "115792089237316195423570985008687907853269984665640564039457584007913129639935";
|
|
@@ -39,10 +39,10 @@ function formatUnixTimestamp(timestamp) {
|
|
|
39
39
|
}
|
|
40
40
|
export const accountDepositTool = {
|
|
41
41
|
name: "account_deposit",
|
|
42
|
-
description: "Deposit funds from wallet into the MYX trading account.",
|
|
42
|
+
description: "[ACCOUNT] Deposit funds from wallet into the MYX trading account.",
|
|
43
43
|
schema: {
|
|
44
44
|
amount: z.union([z.string(), z.number()]).describe("Amount to deposit (human-readable or raw units)"),
|
|
45
|
-
tokenAddress: z.string().describe("Token address"),
|
|
45
|
+
tokenAddress: z.string().optional().describe("Token address (optional, default: QUOTE_TOKEN_ADDRESS)"),
|
|
46
46
|
autoApprove: z.coerce.boolean().optional().describe("If true, auto-approve token allowance when needed (default false)."),
|
|
47
47
|
approveMax: z.coerce.boolean().optional().describe("If autoApprove=true, approve MaxUint256 instead of exact amount."),
|
|
48
48
|
},
|
|
@@ -50,7 +50,8 @@ export const accountDepositTool = {
|
|
|
50
50
|
try {
|
|
51
51
|
const { client, signer, address } = await resolveClient();
|
|
52
52
|
const chainId = getChainId();
|
|
53
|
-
const
|
|
53
|
+
const tokenAddressInput = String(args.tokenAddress ?? "").trim() || getQuoteToken();
|
|
54
|
+
const tokenAddress = normalizeAddress(tokenAddressInput, "tokenAddress");
|
|
54
55
|
// For deposit, we default to quote decimals (6) as it's the most common use case.
|
|
55
56
|
// ensureUnits handles 'raw:' prefix if absolute precision is needed.
|
|
56
57
|
const { ensureUnits } = await import("../utils/units.js");
|
|
@@ -90,7 +91,7 @@ export const accountDepositTool = {
|
|
|
90
91
|
};
|
|
91
92
|
export const accountWithdrawTool = {
|
|
92
93
|
name: "account_withdraw",
|
|
93
|
-
description: "Withdraw funds from MYX trading account back to wallet.",
|
|
94
|
+
description: "[ACCOUNT] Withdraw funds from MYX trading account back to wallet.",
|
|
94
95
|
schema: {
|
|
95
96
|
poolId: z.string().describe("Pool ID to withdraw from"),
|
|
96
97
|
amount: z.union([z.string(), z.number()]).describe("Amount to withdraw (human-readable or raw units)"),
|
|
@@ -128,13 +129,7 @@ export const accountWithdrawTool = {
|
|
|
128
129
|
throw new Error(`Account has locked funds until releaseTime=${formatUnixTimestamp(releaseTime)}. ` +
|
|
129
130
|
`Retry after unlock or reduce withdraw amount.`);
|
|
130
131
|
}
|
|
131
|
-
const raw = await client.account.
|
|
132
|
-
chainId,
|
|
133
|
-
receiver: address,
|
|
134
|
-
amount,
|
|
135
|
-
poolId: args.poolId,
|
|
136
|
-
isQuoteToken: args.isQuoteToken,
|
|
137
|
-
});
|
|
132
|
+
const raw = await client.account.updateAndWithdraw(address, args.poolId, Boolean(args.isQuoteToken), amount, chainId);
|
|
138
133
|
const data = await finalizeMutationResult(raw, signer, "account_withdraw");
|
|
139
134
|
const preflight = {
|
|
140
135
|
requestedAmountRaw: amountRaw.toString(),
|
|
@@ -5,7 +5,7 @@ import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
|
5
5
|
import { extractErrorMessage } from "../utils/errorMessage.js";
|
|
6
6
|
export const adjustMarginTool = {
|
|
7
7
|
name: "adjust_margin",
|
|
8
|
-
description: "Adjust the margin (collateral) of an open position.",
|
|
8
|
+
description: "[TRADE] Adjust the margin (collateral) of an open position.",
|
|
9
9
|
schema: {
|
|
10
10
|
poolId: z.string().describe("Pool ID"),
|
|
11
11
|
positionId: z.string().describe("Position ID"),
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
3
|
+
import { resolvePool } from "../services/marketService.js";
|
|
4
|
+
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
5
|
+
function normalizeOrderIds(input) {
|
|
6
|
+
if (Array.isArray(input))
|
|
7
|
+
return input.map((id) => String(id).trim()).filter(Boolean);
|
|
8
|
+
if (typeof input !== "string")
|
|
9
|
+
return [];
|
|
10
|
+
const text = input.trim();
|
|
11
|
+
if (!text)
|
|
12
|
+
return [];
|
|
13
|
+
if (text.startsWith("[") && text.endsWith("]")) {
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(text);
|
|
16
|
+
if (Array.isArray(parsed))
|
|
17
|
+
return parsed.map((id) => String(id).trim()).filter(Boolean);
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
}
|
|
21
|
+
if (text.includes(","))
|
|
22
|
+
return text.split(",").map((id) => id.trim()).filter(Boolean);
|
|
23
|
+
return [text];
|
|
24
|
+
}
|
|
25
|
+
export const cancelOrdersTool = {
|
|
26
|
+
name: "cancel_orders",
|
|
27
|
+
description: "[TRADE] Cancel open orders. Supports specific IDs, all orders in a pool, or ALL open orders.",
|
|
28
|
+
schema: {
|
|
29
|
+
orderIds: z.union([z.array(z.string()), z.string()]).optional().describe("Specific order ID(s) to cancel."),
|
|
30
|
+
poolId: z.string().optional().describe("Pool ID or keyword to cancel all orders within."),
|
|
31
|
+
keyword: z.string().optional().describe("Market keyword (e.g. 'BTC') for pool-based cancellation."),
|
|
32
|
+
cancelAll: z.boolean().default(false).describe("If true, cancel ALL open orders for the account."),
|
|
33
|
+
},
|
|
34
|
+
handler: async (args) => {
|
|
35
|
+
try {
|
|
36
|
+
const { client, address, signer } = await resolveClient();
|
|
37
|
+
const chainId = getChainId();
|
|
38
|
+
let targetOrderIds = normalizeOrderIds(args.orderIds);
|
|
39
|
+
if (args.cancelAll || args.poolId || args.keyword) {
|
|
40
|
+
const ordersRes = await client.order.getOrders(address);
|
|
41
|
+
const allOrders = ordersRes?.data || [];
|
|
42
|
+
if (args.cancelAll) {
|
|
43
|
+
targetOrderIds = allOrders.map((o) => o.orderId);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const poolId = await resolvePool(client, args.poolId, args.keyword);
|
|
47
|
+
targetOrderIds = allOrders
|
|
48
|
+
.filter((o) => String(o.poolId).toLowerCase() === poolId.toLowerCase())
|
|
49
|
+
.map((o) => o.orderId);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (targetOrderIds.length === 0) {
|
|
53
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "success", message: "No matching open orders found to cancel." }) }] };
|
|
54
|
+
}
|
|
55
|
+
const raw = await client.order.cancelAllOrders(targetOrderIds, chainId);
|
|
56
|
+
const result = await finalizeMutationResult(raw, signer, "cancel_orders");
|
|
57
|
+
return {
|
|
58
|
+
content: [{
|
|
59
|
+
type: "text",
|
|
60
|
+
text: JSON.stringify({
|
|
61
|
+
status: "success",
|
|
62
|
+
data: { cancelledCount: targetOrderIds.length, orderIds: targetOrderIds, result }
|
|
63
|
+
}, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2)
|
|
64
|
+
}]
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
3
|
+
import { resolvePool } from "../services/marketService.js";
|
|
4
|
+
import { parseUserUnits } from "../utils/units.js";
|
|
5
|
+
import { ethers } from "ethers";
|
|
6
|
+
function toBigIntOrZero(value) {
|
|
7
|
+
try {
|
|
8
|
+
const text = String(value ?? "").trim();
|
|
9
|
+
if (!text)
|
|
10
|
+
return 0n;
|
|
11
|
+
return BigInt(text);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return 0n;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export const checkAccountReadyTool = {
|
|
18
|
+
name: "check_account_ready",
|
|
19
|
+
description: "[TRADE] Check if the account has sufficient funds (margin + wallet) for a planned trade.",
|
|
20
|
+
schema: {
|
|
21
|
+
poolId: z.string().optional().describe("Pool ID or keyword."),
|
|
22
|
+
keyword: z.string().optional().describe("Market keyword, e.g. 'BTC'."),
|
|
23
|
+
collateralAmount: z.string().describe("Planned collateral, e.g. '100'."),
|
|
24
|
+
},
|
|
25
|
+
handler: async (args) => {
|
|
26
|
+
try {
|
|
27
|
+
const { client, address } = await resolveClient();
|
|
28
|
+
const chainId = getChainId();
|
|
29
|
+
const poolId = await resolvePool(client, args.poolId, args.keyword);
|
|
30
|
+
const detailRes = await client.markets.getMarketDetail({ chainId, poolId });
|
|
31
|
+
const detail = detailRes?.data || detailRes;
|
|
32
|
+
const quoteDecimals = Number(detail?.quoteDecimals ?? 6);
|
|
33
|
+
const quoteSymbol = detail?.quoteSymbol || "USDC";
|
|
34
|
+
const marginInfo = await client.account.getAccountInfo(chainId, address, poolId);
|
|
35
|
+
let marginBalanceRaw = 0n;
|
|
36
|
+
let accountWalletBalanceRaw = 0n;
|
|
37
|
+
if (marginInfo?.code === 0 && marginInfo?.data) {
|
|
38
|
+
marginBalanceRaw = toBigIntOrZero(marginInfo.data.freeMargin);
|
|
39
|
+
accountWalletBalanceRaw = toBigIntOrZero(marginInfo.data.walletBalance);
|
|
40
|
+
}
|
|
41
|
+
const walletRes = await client.account.getWalletQuoteTokenBalance(chainId, address);
|
|
42
|
+
const walletBalanceRaw = BigInt(walletRes?.data || "0");
|
|
43
|
+
const requiredRaw = BigInt(parseUserUnits(args.collateralAmount, quoteDecimals, "required"));
|
|
44
|
+
const isReady = (marginBalanceRaw >= requiredRaw) || (marginBalanceRaw + walletBalanceRaw >= requiredRaw);
|
|
45
|
+
const deficitRaw = requiredRaw > marginBalanceRaw ? requiredRaw - marginBalanceRaw : 0n;
|
|
46
|
+
const needDepositFromWallet = deficitRaw > 0n;
|
|
47
|
+
const walletSufficient = walletBalanceRaw >= deficitRaw;
|
|
48
|
+
const format = (v) => ethers.formatUnits(v, quoteDecimals);
|
|
49
|
+
return {
|
|
50
|
+
content: [{
|
|
51
|
+
type: "text",
|
|
52
|
+
text: JSON.stringify({
|
|
53
|
+
status: "success",
|
|
54
|
+
data: {
|
|
55
|
+
isReady,
|
|
56
|
+
neededTotal: format(requiredRaw),
|
|
57
|
+
currentMarginBalance: format(marginBalanceRaw),
|
|
58
|
+
currentWalletBalance: format(walletBalanceRaw),
|
|
59
|
+
summary: {
|
|
60
|
+
hasEnoughInMargin: marginBalanceRaw >= requiredRaw,
|
|
61
|
+
needDepositFromWallet,
|
|
62
|
+
walletSufficientForDeposit: walletSufficient,
|
|
63
|
+
accountInfoWalletBalance: format(accountWalletBalanceRaw),
|
|
64
|
+
quoteSymbol
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}, null, 2)
|
|
68
|
+
}]
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
@@ -5,7 +5,7 @@ import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
|
5
5
|
const MAX_UINT256 = "115792089237316195423570985008687907853269984665640564039457584007913129639935";
|
|
6
6
|
export const checkApprovalTool = {
|
|
7
7
|
name: "check_approval",
|
|
8
|
-
description: "Check if token spending approval is needed. Supports auto-approve exact amount (default) or optional unlimited approval.",
|
|
8
|
+
description: "[TRADE] Check if token spending approval is needed. Supports auto-approve exact amount (default) or optional unlimited approval.",
|
|
9
9
|
schema: {
|
|
10
10
|
amount: z.string().regex(/^\d+$/).describe("Amount to check approval for (token raw units)"),
|
|
11
11
|
quoteToken: z.string().optional().describe("Token address to check. Uses default if omitted."),
|
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { resolveClient, getChainId, getQuoteToken } from "../auth/resolveClient.js";
|
|
3
|
-
import { getOraclePrice } from "../services/marketService.js";
|
|
3
|
+
import { getOraclePrice, 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
7
|
export const closeAllPositionsTool = {
|
|
8
8
|
name: "close_all_positions",
|
|
9
|
-
description: "Emergency: close ALL open positions in a pool at once. Use for risk management.",
|
|
9
|
+
description: "[TRADE] Emergency: close ALL open positions in a pool at once. Use for risk management.",
|
|
10
10
|
schema: {
|
|
11
|
-
poolId: z.string().describe("Pool ID
|
|
11
|
+
poolId: z.string().optional().describe("Pool ID or keyword."),
|
|
12
|
+
keyword: z.string().optional().describe("Market keyword, e.g. 'BTC'."),
|
|
12
13
|
slippagePct: z.union([z.string(), z.number()]).optional().describe(`${SLIPPAGE_PCT_4DP_DESC}. Also supports human percent format like "1.0" or "1%".`),
|
|
13
14
|
},
|
|
14
15
|
handler: async (args) => {
|
|
15
16
|
try {
|
|
16
17
|
const { client, address, signer } = await resolveClient();
|
|
17
18
|
const chainId = getChainId();
|
|
19
|
+
const poolId = await resolvePool(client, args.poolId, args.keyword);
|
|
18
20
|
// 1) 先获取该池的所有持仓
|
|
19
21
|
const posResult = await client.position.listPositions(address);
|
|
20
22
|
const positions = posResult?.data || posResult || [];
|
|
21
23
|
// 过滤出指定 pool 的仓位
|
|
22
24
|
const poolPositions = Array.isArray(positions)
|
|
23
|
-
? positions.filter((p) => p.poolId ===
|
|
25
|
+
? positions.filter((p) => p.poolId === poolId || p.pool_id === poolId)
|
|
24
26
|
: [];
|
|
25
27
|
if (poolPositions.length === 0) {
|
|
26
28
|
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) }] };
|
|
@@ -28,14 +30,14 @@ export const closeAllPositionsTool = {
|
|
|
28
30
|
// 2) 为每个仓位构建平仓参数
|
|
29
31
|
const slippagePct = normalizeSlippagePct4dpFlexible(args.slippagePct ?? "200");
|
|
30
32
|
// 3) We need actual oracle prices to avoid Revert 0x613970e0 (InvalidParameter)
|
|
31
|
-
const oraclePriceReq = await getOraclePrice(client,
|
|
33
|
+
const oraclePriceReq = await getOraclePrice(client, poolId).catch(() => null);
|
|
32
34
|
let fallbackPrice = "0";
|
|
33
35
|
if (oraclePriceReq && oraclePriceReq.price) {
|
|
34
36
|
fallbackPrice = oraclePriceReq.price.toString();
|
|
35
37
|
}
|
|
36
38
|
else {
|
|
37
39
|
const { getMarketPrice } = await import("../services/marketService.js");
|
|
38
|
-
const marketData = await getMarketPrice(client,
|
|
40
|
+
const marketData = await getMarketPrice(client, poolId).catch(() => null);
|
|
39
41
|
if (marketData && marketData.price) {
|
|
40
42
|
fallbackPrice = marketData.price.toString();
|
|
41
43
|
}
|
|
@@ -50,7 +52,7 @@ export const closeAllPositionsTool = {
|
|
|
50
52
|
return {
|
|
51
53
|
chainId,
|
|
52
54
|
address,
|
|
53
|
-
poolId:
|
|
55
|
+
poolId: poolId,
|
|
54
56
|
positionId: pos.positionId || pos.position_id || pos.id,
|
|
55
57
|
orderType: 0, // MARKET
|
|
56
58
|
triggerType: 0, // NONE
|
|
@@ -5,26 +5,33 @@ import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
|
5
5
|
import { SLIPPAGE_PCT_4DP_DESC } from "../utils/slippage.js";
|
|
6
6
|
import { verifyTradeOutcome } from "../utils/verification.js";
|
|
7
7
|
import { mapDirection, mapOrderType, mapTriggerType } from "../utils/mappings.js";
|
|
8
|
+
import { parseUserUnits } from "../utils/units.js";
|
|
8
9
|
const FULL_CLOSE_MARKERS = new Set(["ALL", "FULL", "MAX"]);
|
|
9
10
|
const INTEGER_RE = /^\d+$/;
|
|
10
11
|
function wantsFullCloseMarker(input) {
|
|
11
12
|
const value = String(input ?? "").trim().toUpperCase();
|
|
12
13
|
return FULL_CLOSE_MARKERS.has(value);
|
|
13
14
|
}
|
|
14
|
-
function
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const second = String(position?.[fallback] ?? "").trim();
|
|
20
|
-
if (INTEGER_RE.test(second))
|
|
21
|
-
return second;
|
|
15
|
+
function readFirstPositionField(position, fields) {
|
|
16
|
+
for (const field of fields) {
|
|
17
|
+
const value = String(position?.[field] ?? "").trim();
|
|
18
|
+
if (value)
|
|
19
|
+
return value;
|
|
22
20
|
}
|
|
23
21
|
return "";
|
|
24
22
|
}
|
|
23
|
+
function resolvePositionRaw(position, rawFields, humanFields, decimals, label) {
|
|
24
|
+
const raw = readFirstPositionField(position, rawFields);
|
|
25
|
+
if (INTEGER_RE.test(raw))
|
|
26
|
+
return raw;
|
|
27
|
+
const human = readFirstPositionField(position, humanFields);
|
|
28
|
+
if (!human)
|
|
29
|
+
return "";
|
|
30
|
+
return parseUserUnits(human, decimals, label);
|
|
31
|
+
}
|
|
25
32
|
export const closePositionTool = {
|
|
26
33
|
name: "close_position",
|
|
27
|
-
description: "Create a decrease order using SDK-native parameters.",
|
|
34
|
+
description: "[TRADE] Create a decrease order (close or reduce position) using SDK-native parameters.",
|
|
28
35
|
schema: {
|
|
29
36
|
poolId: z.string().describe("Pool ID"),
|
|
30
37
|
positionId: z.string().describe("Position ID to close"),
|
|
@@ -67,14 +74,14 @@ export const closePositionTool = {
|
|
|
67
74
|
throw new Error(`Could not find live position snapshot for positionId=${preparedArgs.positionId} in poolId=${poolId}.`);
|
|
68
75
|
}
|
|
69
76
|
if (needAutoSize) {
|
|
70
|
-
const rawSize =
|
|
77
|
+
const rawSize = resolvePositionRaw(target, ["sizeRaw", "positionSizeRaw"], ["size", "positionSize"], Number(poolData.baseDecimals ?? 18), "position.size");
|
|
71
78
|
if (!rawSize || rawSize === "0") {
|
|
72
79
|
throw new Error(`Resolved position size is empty/zero for positionId=${preparedArgs.positionId}.`);
|
|
73
80
|
}
|
|
74
81
|
preparedArgs.size = `raw:${rawSize}`;
|
|
75
82
|
}
|
|
76
83
|
if (needAutoCollateral) {
|
|
77
|
-
const rawCollateral =
|
|
84
|
+
const rawCollateral = resolvePositionRaw(target, ["collateralRaw", "collateralAmountRaw"], ["collateral", "collateralAmount"], Number(poolData.quoteDecimals ?? 6), "position.collateralAmount");
|
|
78
85
|
if (!rawCollateral) {
|
|
79
86
|
throw new Error(`Resolved position collateral is empty for positionId=${preparedArgs.positionId}.`);
|
|
80
87
|
}
|
|
@@ -4,7 +4,7 @@ import { resolveClient } from "../auth/resolveClient.js";
|
|
|
4
4
|
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
5
5
|
export const createPerpMarketTool = {
|
|
6
6
|
name: "create_perp_market",
|
|
7
|
-
description: "Create a new perpetual contract pool on MYX. IMPORTANT: marketId cannot be randomly generated. It must be a valid 66-character config hash (0x...) tied to a supported quote token (like USDC). Use
|
|
7
|
+
description: "[LIQUIDITY] Create a new perpetual contract pool on MYX. IMPORTANT: marketId cannot be randomly generated. It must be a valid 66-character config hash (0x...) tied to a supported quote token (like USDC). Use get_pool_metadata (after find_pool/list_pools) to fetch an existing marketId if you don't have a specific newly allocated one.",
|
|
8
8
|
schema: {
|
|
9
9
|
baseToken: z.string().describe("Base token contract address (e.g., 0xb40aaadc43...)"),
|
|
10
10
|
marketId: z.string().describe("MUST be a valid 66-char config hash (e.g., existing USDC marketId: 0x7f6727d8026fd2c87ccc745846c83cd0b68e886c73e1e05a54a675bcadd8adb6). Do NOT generate randomly."),
|
|
@@ -11,12 +11,12 @@ const POSITION_ID_RE = /^$|^0x[0-9a-fA-F]{64}$/;
|
|
|
11
11
|
const ZERO_POSITION_ID_RE = /^0x0{64}$/i;
|
|
12
12
|
export const executeTradeTool = {
|
|
13
13
|
name: "execute_trade",
|
|
14
|
-
description: "Create an increase order using SDK-native parameters.",
|
|
14
|
+
description: "[TRADE] Create an increase order (open or add to position) using SDK-native parameters.",
|
|
15
15
|
schema: {
|
|
16
|
-
poolId: z.string().describe("Hex Pool ID, e.g. '0x14a19...'. Get via
|
|
17
|
-
positionId: z.string().refine((value) => POSITION_ID_RE.test(value), {
|
|
16
|
+
poolId: z.string().describe("Hex Pool ID, e.g. '0x14a19...'. Get via list_pools."),
|
|
17
|
+
positionId: z.string().optional().refine((value) => !value || POSITION_ID_RE.test(value), {
|
|
18
18
|
message: "positionId must be empty string for new position, or a bytes32 hex string.",
|
|
19
|
-
}).describe("Position ID: Use empty string '' for NEW positions, or valid hex for INCREASING existing ones. 0x000..00 is auto-treated as NEW."),
|
|
19
|
+
}).describe("Position ID: Use empty string '' for NEW positions, or valid hex for INCREASING existing ones. 0x000..00 is auto-treated as NEW. Default is empty string."),
|
|
20
20
|
orderType: z.union([z.number(), z.string()]).describe("Market/Limit/Stop. e.g. 0 or 'MARKET'."),
|
|
21
21
|
triggerType: z.union([z.number(), z.string()]).optional().describe("0=None (Market), 1=GTE, 2=LTE. e.g. 'GTE'."),
|
|
22
22
|
direction: z.union([z.number(), z.string()]).describe("0/LONG/BUY or 1/SHORT/SELL."),
|
|
@@ -26,7 +26,7 @@ export const executeTradeTool = {
|
|
|
26
26
|
timeInForce: z.coerce.number().int().describe("0=GTC, 1=IOC, 2=FOK"),
|
|
27
27
|
postOnly: z.coerce.boolean().describe("If true, order only executes as Maker."),
|
|
28
28
|
slippagePct: z.coerce.string().default("50").describe(`${SLIPPAGE_PCT_4DP_DESC}. Default is 50 (0.5%).`),
|
|
29
|
-
executionFeeToken: z.string().describe("Address of token to pay gas/execution fees (typically USDC)."),
|
|
29
|
+
executionFeeToken: z.string().optional().describe("Address of token to pay gas/execution fees (typically USDC). Default is pool quoteToken."),
|
|
30
30
|
leverage: z.coerce.number().positive().describe("Leverage multiplier, e.g., 10 for 10x."),
|
|
31
31
|
tpSize: z.union([z.string(), z.number()]).optional().describe("Take Profit size. Use '0' to disable."),
|
|
32
32
|
tpPrice: z.union([z.string(), z.number()]).optional().describe("Take Profit trigger price."),
|
|
@@ -35,7 +35,7 @@ export const executeTradeTool = {
|
|
|
35
35
|
tradingFee: z.union([z.string(), z.number()]).optional().describe("Trading fee in quote token units. Supports human/raw prefix. Optional: auto-computed via get_user_trading_fee_rate."),
|
|
36
36
|
assetClass: z.coerce.number().int().nonnegative().optional().describe("Optional fee lookup assetClass (default from pool config or 1)."),
|
|
37
37
|
riskTier: z.coerce.number().int().nonnegative().optional().describe("Optional fee lookup riskTier (default from pool config or 1)."),
|
|
38
|
-
marketId: z.string().describe("Specific Market Config Hash. Fetch via
|
|
38
|
+
marketId: z.string().describe("Specific Market Config Hash. Fetch via get_pool_metadata (resolve poolId first with find_pool/list_pools)."),
|
|
39
39
|
},
|
|
40
40
|
handler: async (args) => {
|
|
41
41
|
try {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { resolveClient } from "../auth/resolveClient.js";
|
|
3
|
+
import { searchMarket } from "../services/marketService.js";
|
|
4
|
+
export const findPoolTool = {
|
|
5
|
+
name: "find_pool",
|
|
6
|
+
description: "[MARKET] Search for pools by keyword or symbol. Returns matched market metadata.",
|
|
7
|
+
schema: {
|
|
8
|
+
keyword: z.string().describe("Search keyword or symbol, e.g. 'BTC', 'ETH/USDC'"),
|
|
9
|
+
limit: z.number().int().positive().optional().describe("Max search results (default 10)"),
|
|
10
|
+
},
|
|
11
|
+
handler: async (args) => {
|
|
12
|
+
try {
|
|
13
|
+
const { client } = await resolveClient();
|
|
14
|
+
const results = await searchMarket(client, args.keyword, args.limit ?? 10);
|
|
15
|
+
return {
|
|
16
|
+
content: [{
|
|
17
|
+
type: "text",
|
|
18
|
+
text: JSON.stringify({ status: "success", data: results }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2)
|
|
19
|
+
}]
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
3
|
+
function unwrapData(result) {
|
|
4
|
+
if (result && typeof result === "object" && "data" in result) {
|
|
5
|
+
return result.data;
|
|
6
|
+
}
|
|
7
|
+
return result;
|
|
8
|
+
}
|
|
9
|
+
export const getAccountSnapshotTool = {
|
|
10
|
+
name: "get_account_snapshot",
|
|
11
|
+
description: "[ACCOUNT] Get unified account snapshot (balances, trading metrics, and VIP tier).",
|
|
12
|
+
schema: {
|
|
13
|
+
poolId: z.string().optional().describe("Pool ID for detailed trading-account metrics."),
|
|
14
|
+
chainId: z.number().int().positive().optional().describe("Optional chainId override"),
|
|
15
|
+
},
|
|
16
|
+
handler: async (args) => {
|
|
17
|
+
try {
|
|
18
|
+
const { client, address } = await resolveClient();
|
|
19
|
+
const chainId = args.chainId ?? getChainId();
|
|
20
|
+
const { getBalances, getMarginBalance } = await import("../services/balanceService.js");
|
|
21
|
+
const [vipRes, walletRes] = await Promise.all([
|
|
22
|
+
client.account.getAccountVipInfo(chainId, address).catch(() => null),
|
|
23
|
+
getBalances(client, address, chainId).catch(() => null)
|
|
24
|
+
]);
|
|
25
|
+
let tradingAccount = null;
|
|
26
|
+
if (args.poolId) {
|
|
27
|
+
const [info, margin] = await Promise.all([
|
|
28
|
+
client.account.getAccountInfo(chainId, address, args.poolId).catch(() => null),
|
|
29
|
+
getMarginBalance(client, address, args.poolId, chainId).catch(() => null)
|
|
30
|
+
]);
|
|
31
|
+
tradingAccount = {
|
|
32
|
+
info: unwrapData(info),
|
|
33
|
+
margin: unwrapData(margin)
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const results = {
|
|
37
|
+
wallet: unwrapData(walletRes),
|
|
38
|
+
tradingAccount,
|
|
39
|
+
vip: unwrapData(vipRes),
|
|
40
|
+
};
|
|
41
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: results }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
2
|
+
import { getPoolList } from "../services/marketService.js";
|
|
2
3
|
export const getAllTickersTool = {
|
|
3
4
|
name: "get_all_tickers",
|
|
4
|
-
description: "Get ticker snapshots for all markets.",
|
|
5
|
+
description: "[MARKET] Get ticker snapshots for all markets.",
|
|
5
6
|
schema: {},
|
|
6
7
|
handler: async () => {
|
|
7
8
|
try {
|
|
@@ -13,9 +14,9 @@ export const getAllTickersTool = {
|
|
|
13
14
|
catch {
|
|
14
15
|
// Fallback for networks/environments where getAllTickers endpoint is unavailable.
|
|
15
16
|
const chainId = getChainId();
|
|
16
|
-
const poolList = await
|
|
17
|
-
const pools = Array.isArray(poolList?.data) ? poolList.data : [];
|
|
18
|
-
const poolIds = pools.map((p) => p
|
|
17
|
+
const poolList = await getPoolList(client, chainId);
|
|
18
|
+
const pools = Array.isArray(poolList?.data) ? poolList.data : (Array.isArray(poolList) ? poolList : []);
|
|
19
|
+
const poolIds = pools.map((p) => p?.poolId ?? p?.pool_id).filter((id) => !!id);
|
|
19
20
|
if (poolIds.length === 0) {
|
|
20
21
|
throw new Error("Failed to fetch all tickers and no pools were available for fallback query.");
|
|
21
22
|
}
|