@michaleffffff/mcp-trading-server 3.0.3 → 3.0.5
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 +23 -0
- package/README.md +48 -408
- package/TOOL_EXAMPLES.md +63 -555
- package/dist/prompts/tradingGuide.js +16 -19
- package/dist/server.js +20 -2
- package/dist/services/balanceService.js +4 -4
- package/dist/services/marketService.js +48 -12
- package/dist/services/tradeService.js +33 -10
- package/dist/tools/accountInfo.js +1 -82
- package/dist/tools/accountTransfer.js +91 -5
- package/dist/tools/adjustMargin.js +1 -1
- package/dist/tools/cancelOrders.js +71 -0
- package/dist/tools/checkAccountReady.js +69 -0
- package/dist/tools/checkApproval.js +1 -1
- package/dist/tools/closeAllPositions.js +12 -12
- package/dist/tools/closePosition.js +60 -12
- 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 +41 -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 +83 -0
- package/dist/tools/getPositionsAll.js +80 -0
- package/dist/tools/{getMarketPrice.js → getPrice.js} +8 -5
- package/dist/tools/getUserTradingFeeRate.js +1 -1
- package/dist/tools/index.js +15 -19
- package/dist/tools/listPools.js +53 -0
- package/dist/tools/manageLiquidity.js +10 -5
- package/dist/tools/manageTpSl.js +163 -0
- package/dist/tools/openPositionSimple.js +22 -5
- package/dist/tools/searchTools.js +35 -0
- package/dist/utils/mappings.js +10 -7
- package/dist/utils/slippage.js +23 -0
- package/package.json +1 -1
- 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 -72
- 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
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
3
|
+
import { getPositions } from "../services/positionService.js";
|
|
4
|
+
import { getDirectionDesc } from "../utils/mappings.js";
|
|
5
|
+
export const getPositionsAllTool = {
|
|
6
|
+
name: "get_positions_all",
|
|
7
|
+
description: "[ACCOUNT] Get positions (open or history) with optional filters and ROI/PnL metrics.",
|
|
8
|
+
schema: {
|
|
9
|
+
status: z.enum(["OPEN", "HISTORY", "ALL"]).default("OPEN").describe("Filter by status: 'OPEN' (default), 'HISTORY', or 'ALL'"),
|
|
10
|
+
poolId: z.string().optional().describe("Filter by pool ID"),
|
|
11
|
+
limit: z.number().int().positive().optional().describe("Results per page (default 20, for history)"),
|
|
12
|
+
},
|
|
13
|
+
handler: async (args) => {
|
|
14
|
+
try {
|
|
15
|
+
const { client, address } = await resolveClient();
|
|
16
|
+
const chainId = getChainId();
|
|
17
|
+
const results = {};
|
|
18
|
+
if (args.status === "OPEN" || args.status === "ALL") {
|
|
19
|
+
const data = await getPositions(client, address);
|
|
20
|
+
const positions = Array.isArray(data) ? data : (Array.isArray(data?.data) ? data.data : []);
|
|
21
|
+
// Enhance open positions with metrics
|
|
22
|
+
const filtered = args.poolId ? positions.filter((p) => String(p.poolId).toLowerCase() === args.poolId.toLowerCase()) : positions;
|
|
23
|
+
if (filtered.length > 0) {
|
|
24
|
+
const poolIds = [...new Set(filtered.map((p) => p.poolId))];
|
|
25
|
+
const [tickersRes, configs] = await Promise.all([
|
|
26
|
+
client.markets.getTickerList({ chainId, poolIds }),
|
|
27
|
+
Promise.all(poolIds.map(async (pid) => {
|
|
28
|
+
try {
|
|
29
|
+
const { getPoolLevelConfig } = await import("../services/marketService.js");
|
|
30
|
+
const res = await getPoolLevelConfig(client, pid, chainId);
|
|
31
|
+
return { poolId: pid, config: res?.levelConfig || res?.data?.levelConfig || res };
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return { poolId: pid, config: null };
|
|
35
|
+
}
|
|
36
|
+
}))
|
|
37
|
+
]);
|
|
38
|
+
const tickers = Array.isArray(tickersRes) ? tickersRes : (tickersRes?.data ?? []);
|
|
39
|
+
results.open = filtered.map((pos) => {
|
|
40
|
+
const ticker = tickers.find((t) => t.poolId === pos.poolId);
|
|
41
|
+
const currentPrice = Number(ticker?.price || 0);
|
|
42
|
+
const entryPrice = Number(pos.entryPrice || 0);
|
|
43
|
+
const size = Number(pos.size || 0);
|
|
44
|
+
const collateral = Number(pos.collateralAmount || 0);
|
|
45
|
+
const direction = pos.direction;
|
|
46
|
+
const mm = configs.find(c => c.poolId === pos.poolId)?.config?.maintainCollateralRate || 0.02;
|
|
47
|
+
let estimatedPnl = direction === 0 ? (currentPrice - entryPrice) * size : (entryPrice - currentPrice) * size;
|
|
48
|
+
const roi = collateral > 0 ? (estimatedPnl / collateral) * 100 : 0;
|
|
49
|
+
let liqPrice = size > 0 ? (direction === 0 ? (entryPrice * size - collateral) / (size * (1 - mm)) : (entryPrice * size + collateral) / (size * (1 + mm))) : 0;
|
|
50
|
+
if (liqPrice < 0)
|
|
51
|
+
liqPrice = 0;
|
|
52
|
+
return {
|
|
53
|
+
...pos,
|
|
54
|
+
directionDesc: getDirectionDesc(pos.direction),
|
|
55
|
+
currentPrice: currentPrice.toString(),
|
|
56
|
+
estimatedPnl: estimatedPnl.toFixed(4),
|
|
57
|
+
roi: roi.toFixed(2) + "%",
|
|
58
|
+
liquidationPrice: liqPrice.toFixed(4)
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
results.open = [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (args.status === "HISTORY" || args.status === "ALL") {
|
|
67
|
+
const query = { chainId, poolId: args.poolId, limit: args.limit ?? 20 };
|
|
68
|
+
const historyRes = await client.position.getPositionHistory(query, address);
|
|
69
|
+
results.history = (historyRes?.data || []).map((pos) => ({
|
|
70
|
+
...pos,
|
|
71
|
+
directionDesc: getDirectionDesc(pos.direction)
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: results }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
};
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
3
|
-
import { getMarketPrice } from "../services/marketService.js";
|
|
4
|
-
export const
|
|
5
|
-
name: "
|
|
6
|
-
description: "Get the current
|
|
3
|
+
import { getMarketPrice, getOraclePrice } from "../services/marketService.js";
|
|
4
|
+
export const getPriceTool = {
|
|
5
|
+
name: "get_price",
|
|
6
|
+
description: "[MARKET] Get the current price for a specific pool. Support both market (impact) and oracle prices.",
|
|
7
7
|
schema: {
|
|
8
8
|
poolId: z.string().describe("Pool ID to get price for"),
|
|
9
|
+
priceType: z.enum(["market", "oracle"]).default("market").describe("Type of price to fetch: 'market' (default) or 'oracle'"),
|
|
9
10
|
chainId: z.number().int().positive().optional().describe("Optional chainId override"),
|
|
10
11
|
},
|
|
11
12
|
handler: async (args) => {
|
|
12
13
|
try {
|
|
13
14
|
const { client } = await resolveClient();
|
|
14
15
|
const chainId = args.chainId ?? getChainId();
|
|
15
|
-
const data =
|
|
16
|
+
const data = args.priceType === "oracle"
|
|
17
|
+
? await getOraclePrice(client, args.poolId, chainId)
|
|
18
|
+
: await getMarketPrice(client, args.poolId, chainId);
|
|
16
19
|
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (k, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
17
20
|
}
|
|
18
21
|
catch (error) {
|
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
3
3
|
export const getUserTradingFeeRateTool = {
|
|
4
4
|
name: "get_user_trading_fee_rate",
|
|
5
|
-
description: "Get maker/taker fee rates for a given assetClass and riskTier.",
|
|
5
|
+
description: "[TRADE] Get maker/taker fee rates for a given assetClass and riskTier.",
|
|
6
6
|
schema: {
|
|
7
7
|
assetClass: z.coerce.number().int().nonnegative().describe("Asset class ID"),
|
|
8
8
|
riskTier: z.coerce.number().int().nonnegative().describe("Risk tier"),
|
package/dist/tools/index.js
CHANGED
|
@@ -1,36 +1,32 @@
|
|
|
1
1
|
// Tools — 交易
|
|
2
2
|
export { openPositionSimpleTool } from "./openPositionSimple.js";
|
|
3
3
|
export { executeTradeTool } from "./executeTrade.js";
|
|
4
|
-
export {
|
|
5
|
-
export { cancelAllOrdersTool } from "./cancelAllOrders.js";
|
|
4
|
+
export { cancelOrdersTool } from "./cancelOrders.js"; // Unified
|
|
6
5
|
export { closePositionTool } from "./closePosition.js";
|
|
7
|
-
export {
|
|
8
|
-
export { updateOrderTpSlTool } from "./updateOrderTpSl.js";
|
|
6
|
+
export { manageTpSlTool } from "./manageTpSl.js"; // Unified
|
|
9
7
|
export { adjustMarginTool } from "./adjustMargin.js";
|
|
10
8
|
export { closeAllPositionsTool } from "./closeAllPositions.js";
|
|
11
9
|
export { checkApprovalTool } from "./checkApproval.js";
|
|
12
10
|
export { getUserTradingFeeRateTool } from "./getUserTradingFeeRate.js";
|
|
13
11
|
export { getNetworkFeeTool } from "./getNetworkFee.js";
|
|
12
|
+
export { checkAccountReadyTool } from "./checkAccountReady.js";
|
|
14
13
|
// Tools — 市场数据
|
|
15
|
-
export {
|
|
16
|
-
export {
|
|
17
|
-
export {
|
|
18
|
-
export {
|
|
19
|
-
export {
|
|
20
|
-
export { getAllTickersTool } from "./getAllTickers.js";
|
|
21
|
-
export { getMarketDetailTool, getPoolInfoTool, getLiquidityInfoTool } from "./marketInfo.js";
|
|
22
|
-
export { getPoolListTool } from "./getPoolList.js";
|
|
23
|
-
export { getPoolSymbolAllTool } from "./getPoolSymbolAll.js";
|
|
24
|
-
export { getPoolLevelConfigTool } from "./poolConfig.js";
|
|
14
|
+
export { getPriceTool } from "./getPrice.js"; // Unified
|
|
15
|
+
export { getKlineTool } from "./getKline.js"; // Enhanced
|
|
16
|
+
export { listPoolsTool } from "./listPools.js"; // Unified
|
|
17
|
+
export { findPoolTool } from "./findPool.js"; // Unified
|
|
18
|
+
export { getPoolMetadataTool } from "./getPoolMetadata.js"; // Unified
|
|
25
19
|
export { getBaseDetailTool } from "./getBaseDetail.js";
|
|
20
|
+
export { getAllTickersTool } from "./getAllTickers.js";
|
|
26
21
|
// Tools — 池子 & 流动性
|
|
27
22
|
export { createPerpMarketTool } from "./createPerpMarket.js";
|
|
28
23
|
export { manageLiquidityTool, getLpPriceTool } from "./manageLiquidity.js";
|
|
29
24
|
// Tools — 账户 & 查询
|
|
30
|
-
export {
|
|
31
|
-
export {
|
|
32
|
-
export {
|
|
33
|
-
export {
|
|
25
|
+
export { getPositionsAllTool } from "./getPositionsAll.js"; // Unified
|
|
26
|
+
export { getOrdersTool } from "./getOrders.js"; // Unified
|
|
27
|
+
export { getAccountSnapshotTool } from "./getAccountSnapshot.js"; // Unified
|
|
28
|
+
export { getTradeFlowTool } from "./accountInfo.js"; // Kept for trade flow detail
|
|
34
29
|
export { getMyLpHoldingsTool } from "./getMyLpHoldings.js";
|
|
35
|
-
export { getAccountVipInfoTool } from "./getAccountVipInfo.js";
|
|
36
30
|
export { accountDepositTool, accountWithdrawTool } from "./accountTransfer.js";
|
|
31
|
+
// Tools — 系统
|
|
32
|
+
export { searchToolsTool } from "./searchTools.js";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { resolveClient } from "../auth/resolveClient.js";
|
|
2
|
+
import { getPoolList } from "../services/marketService.js";
|
|
3
|
+
function collectRows(input) {
|
|
4
|
+
if (Array.isArray(input))
|
|
5
|
+
return input.flatMap(collectRows);
|
|
6
|
+
if (!input || typeof input !== "object")
|
|
7
|
+
return [];
|
|
8
|
+
if (input.poolId || input.pool_id)
|
|
9
|
+
return [input];
|
|
10
|
+
return Object.values(input).flatMap(collectRows);
|
|
11
|
+
}
|
|
12
|
+
export const listPoolsTool = {
|
|
13
|
+
name: "list_pools",
|
|
14
|
+
description: "[MARKET] Get the complete list of all available tradable pools, including symbol and icon metadata.",
|
|
15
|
+
schema: {},
|
|
16
|
+
handler: async () => {
|
|
17
|
+
try {
|
|
18
|
+
const { client } = await resolveClient();
|
|
19
|
+
// Fetch both list and symbols
|
|
20
|
+
const [poolListRes, symbolsRes] = await Promise.all([
|
|
21
|
+
getPoolList(client),
|
|
22
|
+
client.markets.getPoolSymbolAll().catch(() => ({ data: [] }))
|
|
23
|
+
]);
|
|
24
|
+
const poolsRaw = collectRows(poolListRes?.data ?? poolListRes);
|
|
25
|
+
const symbolsRaw = collectRows(symbolsRes?.data ?? symbolsRes);
|
|
26
|
+
const symbolMap = new Map(symbolsRaw
|
|
27
|
+
.filter((row) => row?.poolId || row?.pool_id)
|
|
28
|
+
.map((s) => [String(s.poolId ?? s.pool_id).toLowerCase(), s]));
|
|
29
|
+
const deduped = new Map();
|
|
30
|
+
for (const row of poolsRaw) {
|
|
31
|
+
const poolId = String(row?.poolId ?? row?.pool_id ?? "").trim().toLowerCase();
|
|
32
|
+
if (!poolId)
|
|
33
|
+
continue;
|
|
34
|
+
if (!deduped.has(poolId)) {
|
|
35
|
+
deduped.set(poolId, row);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const enriched = Array.from(deduped.values()).map(pool => {
|
|
39
|
+
const poolId = String(pool.poolId ?? pool.pool_id ?? "").toLowerCase();
|
|
40
|
+
const symbolData = symbolMap.get(poolId);
|
|
41
|
+
return {
|
|
42
|
+
...pool,
|
|
43
|
+
icon: symbolData?.icon || null,
|
|
44
|
+
symbolName: symbolData?.symbolName || pool.symbolName || pool.baseQuoteSymbol || pool.symbol || null,
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data: enriched }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -47,9 +47,9 @@ function resolveLpAssetNames(detail) {
|
|
|
47
47
|
}
|
|
48
48
|
export const manageLiquidityTool = {
|
|
49
49
|
name: "manage_liquidity",
|
|
50
|
-
description: "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.",
|
|
50
|
+
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.",
|
|
51
51
|
schema: {
|
|
52
|
-
action: z.
|
|
52
|
+
action: z.coerce.string().describe("'deposit' or 'withdraw' (aliases: add/remove/increase/decrease; case-insensitive)"),
|
|
53
53
|
poolType: z.enum(["BASE", "QUOTE"]).describe("'BASE' or 'QUOTE'"),
|
|
54
54
|
poolId: z.string().describe("Pool ID or Base Token Address"),
|
|
55
55
|
amount: z.coerce.number().positive().describe("Amount in human-readable units"),
|
|
@@ -62,6 +62,11 @@ export const manageLiquidityTool = {
|
|
|
62
62
|
let { action, poolType, poolId } = args;
|
|
63
63
|
const { amount, slippage } = args;
|
|
64
64
|
const chainId = args.chainId ?? getChainId();
|
|
65
|
+
action = String(action ?? "").trim().toLowerCase();
|
|
66
|
+
const validActions = new Set(["deposit", "withdraw", "add", "remove", "increase", "decrease"]);
|
|
67
|
+
if (!validActions.has(action)) {
|
|
68
|
+
throw new Error(`Invalid action: ${args.action}. Use deposit/withdraw or aliases add/remove/increase/decrease.`);
|
|
69
|
+
}
|
|
65
70
|
// 1. Action Alias Mapping
|
|
66
71
|
if (action === "add" || action === "increase")
|
|
67
72
|
action = "deposit";
|
|
@@ -74,7 +79,7 @@ export const manageLiquidityTool = {
|
|
|
74
79
|
const detail = detailRes?.data || (detailRes?.marketId ? detailRes : null);
|
|
75
80
|
if (!detail?.marketId) {
|
|
76
81
|
throw new Error(`Pool ${poolId} not found on chainId ${chainId}. ` +
|
|
77
|
-
`Please query a valid active pool via
|
|
82
|
+
`Please query a valid active pool via find_pool/list_pools first.`);
|
|
78
83
|
}
|
|
79
84
|
let raw;
|
|
80
85
|
if (poolType === "QUOTE") {
|
|
@@ -88,7 +93,7 @@ export const manageLiquidityTool = {
|
|
|
88
93
|
: await baseWithdraw(poolId, amount, slippage, chainId);
|
|
89
94
|
}
|
|
90
95
|
if (!raw) {
|
|
91
|
-
throw new Error(`SDK returned an empty result for liquidity ${action}. This usually occurs if the pool is not in an Active state (state: 2) or if there is a contract-level restriction. Please check
|
|
96
|
+
throw new Error(`SDK returned an empty result for liquidity ${action}. This usually occurs if the pool is not in an Active state (state: 2) or if there is a contract-level restriction. Please check get_pool_metadata.`);
|
|
92
97
|
}
|
|
93
98
|
if (raw && typeof raw === "object" && "code" in raw && Number(raw.code) !== 0) {
|
|
94
99
|
throw new Error(`Liquidity ${action} failed: ${extractErrorMessage(raw)}`);
|
|
@@ -122,7 +127,7 @@ export const manageLiquidityTool = {
|
|
|
122
127
|
};
|
|
123
128
|
export const getLpPriceTool = {
|
|
124
129
|
name: "get_lp_price",
|
|
125
|
-
description: "Get the current internal net asset value (NAV) price of an LP token for a BASE or QUOTE pool. Note: This is NOT the underlying token's external Oracle market price (e.g. WETH's price), but rather the internal exchange rate / net worth of the LP token itself which fluctuates based on pool PnL and fees.",
|
|
130
|
+
description: "[LIQUIDITY] Get the current internal net asset value (NAV) price of an LP token for a BASE or QUOTE pool. Note: This is NOT the underlying token's external Oracle market price (e.g. WETH's price), but rather the internal exchange rate / net worth of the LP token itself which fluctuates based on pool PnL and fees.",
|
|
126
131
|
schema: {
|
|
127
132
|
poolType: z.enum(["BASE", "QUOTE"]).describe("'BASE' or 'QUOTE'"),
|
|
128
133
|
poolId: z.string().describe("Pool ID"),
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { resolveClient, getChainId } from "../auth/resolveClient.js";
|
|
3
|
+
import { finalizeMutationResult } from "../utils/mutationResult.js";
|
|
4
|
+
function normalizeDirectionInput(value) {
|
|
5
|
+
if (value === undefined || value === null || value === "")
|
|
6
|
+
return undefined;
|
|
7
|
+
if (typeof value === "number") {
|
|
8
|
+
if (value === 0 || value === 1)
|
|
9
|
+
return value;
|
|
10
|
+
throw new Error("direction must be LONG/SHORT or 0/1.");
|
|
11
|
+
}
|
|
12
|
+
const text = String(value).trim().toUpperCase();
|
|
13
|
+
if (text === "0" || text === "LONG" || text === "BUY")
|
|
14
|
+
return 0;
|
|
15
|
+
if (text === "1" || text === "SHORT" || text === "SELL")
|
|
16
|
+
return 1;
|
|
17
|
+
throw new Error("direction must be LONG/SHORT or 0/1.");
|
|
18
|
+
}
|
|
19
|
+
function isNonEmpty(value) {
|
|
20
|
+
return value !== undefined && value !== null && String(value).trim().length > 0;
|
|
21
|
+
}
|
|
22
|
+
function readSnapshotText(snapshot, keys) {
|
|
23
|
+
for (const key of keys) {
|
|
24
|
+
const value = snapshot?.[key];
|
|
25
|
+
if (isNonEmpty(value))
|
|
26
|
+
return String(value).trim();
|
|
27
|
+
}
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
function normalizeUnitsInput(text) {
|
|
31
|
+
const raw = String(text ?? "").trim();
|
|
32
|
+
if (!raw)
|
|
33
|
+
return "";
|
|
34
|
+
if (/^(raw|human):/i.test(raw))
|
|
35
|
+
return raw;
|
|
36
|
+
if (/^\d+$/.test(raw))
|
|
37
|
+
return `raw:${raw}`;
|
|
38
|
+
return raw;
|
|
39
|
+
}
|
|
40
|
+
async function findOrderSnapshot(client, address, chainId, orderId, poolId) {
|
|
41
|
+
const target = String(orderId).toLowerCase();
|
|
42
|
+
try {
|
|
43
|
+
const openRes = await client.order.getOrders(address);
|
|
44
|
+
const openOrders = Array.isArray(openRes?.data) ? openRes.data : [];
|
|
45
|
+
const found = openOrders.find((order) => String(order?.orderId ?? order?.id ?? "").toLowerCase() === target);
|
|
46
|
+
if (found)
|
|
47
|
+
return found;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
const historyQuery = { chainId, limit: 50, page: 1 };
|
|
53
|
+
if (poolId)
|
|
54
|
+
historyQuery.poolId = poolId;
|
|
55
|
+
const historyRes = await client.order.getOrderHistory(historyQuery, address);
|
|
56
|
+
const historyOrders = Array.isArray(historyRes?.data) ? historyRes.data : [];
|
|
57
|
+
return historyOrders.find((order) => String(order?.orderId ?? order?.id ?? "").toLowerCase() === target) ?? null;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export const manageTpSlTool = {
|
|
64
|
+
name: "manage_tp_sl",
|
|
65
|
+
description: "[TRADE] Set or update Take Profit (TP) and Stop Loss (SL) for a position.",
|
|
66
|
+
schema: {
|
|
67
|
+
poolId: z.string().describe("Pool ID"),
|
|
68
|
+
positionId: z.string().optional().describe("Position ID (required for creating TP/SL on a position)"),
|
|
69
|
+
orderId: z.string().optional().describe("Existing TP/SL Order ID to update (optional)"),
|
|
70
|
+
size: z.union([z.string(), z.number()]).optional().describe("Order size (required for update if cannot be auto-resolved from order snapshot)"),
|
|
71
|
+
price: z.union([z.string(), z.number()]).optional().describe("Order price (required for update if cannot be auto-resolved from order snapshot)"),
|
|
72
|
+
tpPrice: z.union([z.string(), z.number()]).optional().describe("New TP price"),
|
|
73
|
+
tpSize: z.union([z.string(), z.number()]).optional().describe("New TP size"),
|
|
74
|
+
slPrice: z.union([z.string(), z.number()]).optional().describe("New SL price"),
|
|
75
|
+
slSize: z.union([z.string(), z.number()]).optional().describe("New SL size"),
|
|
76
|
+
direction: z.union([z.enum(["LONG", "SHORT"]), z.number().int()]).optional().describe("Position direction (LONG/SHORT or 0/1; required for new TP/SL)"),
|
|
77
|
+
leverage: z.number().optional().describe("Position leverage"),
|
|
78
|
+
executionFeeToken: z.string().optional().describe("Fee token address"),
|
|
79
|
+
slippagePct: z.union([z.string(), z.number()]).optional().describe("Slippage in 4dp raw units. Default 100 (1%)."),
|
|
80
|
+
useOrderCollateral: z.boolean().optional().describe("Whether updateOrderTpSl should use order collateral (default true)."),
|
|
81
|
+
chainId: z.number().int().positive().optional().describe("Optional chainId override"),
|
|
82
|
+
},
|
|
83
|
+
handler: async (args) => {
|
|
84
|
+
try {
|
|
85
|
+
const { client, address, signer } = await resolveClient();
|
|
86
|
+
const chainId = args.chainId ?? getChainId();
|
|
87
|
+
const direction = normalizeDirectionInput(args.direction);
|
|
88
|
+
const slippagePct = args.slippagePct ?? "100";
|
|
89
|
+
const { setPositionTpSl, updateOrderTpSl } = await import("../services/tradeService.js");
|
|
90
|
+
let raw;
|
|
91
|
+
if (args.orderId) {
|
|
92
|
+
// Update existing
|
|
93
|
+
const { getMarketDetail } = await import("../services/marketService.js");
|
|
94
|
+
const marketRes = await getMarketDetail(client, args.poolId, chainId);
|
|
95
|
+
const market = marketRes.data ?? marketRes;
|
|
96
|
+
const orderSnapshot = await findOrderSnapshot(client, address, chainId, args.orderId, args.poolId);
|
|
97
|
+
const snapshotSize = readSnapshotText(orderSnapshot, ["size", "orderSize", "positionSize"]);
|
|
98
|
+
const snapshotPrice = readSnapshotText(orderSnapshot, ["price", "orderPrice", "triggerPrice"]);
|
|
99
|
+
const snapshotTpPrice = readSnapshotText(orderSnapshot, ["tpPrice", "takeProfitPrice", "tpTriggerPrice"]);
|
|
100
|
+
const snapshotTpSize = readSnapshotText(orderSnapshot, ["tpSize", "takeProfitSize"]);
|
|
101
|
+
const snapshotSlPrice = readSnapshotText(orderSnapshot, ["slPrice", "stopLossPrice", "slTriggerPrice"]);
|
|
102
|
+
const snapshotSlSize = readSnapshotText(orderSnapshot, ["slSize", "stopLossSize"]);
|
|
103
|
+
const size = isNonEmpty(args.size) ? String(args.size) : snapshotSize;
|
|
104
|
+
const price = isNonEmpty(args.price) ? String(args.price) : snapshotPrice;
|
|
105
|
+
const tpPrice = isNonEmpty(args.tpPrice) ? String(args.tpPrice) : snapshotTpPrice;
|
|
106
|
+
const tpSize = isNonEmpty(args.tpSize) ? String(args.tpSize) : snapshotTpSize;
|
|
107
|
+
const slPrice = isNonEmpty(args.slPrice) ? String(args.slPrice) : snapshotSlPrice;
|
|
108
|
+
const slSize = isNonEmpty(args.slSize) ? String(args.slSize) : snapshotSlSize;
|
|
109
|
+
if (!size || !price) {
|
|
110
|
+
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.");
|
|
111
|
+
}
|
|
112
|
+
if (!tpPrice && !slPrice) {
|
|
113
|
+
throw new Error("At least one of tpPrice or slPrice is required when updating TP/SL.");
|
|
114
|
+
}
|
|
115
|
+
if ((tpPrice && !tpSize) || (!tpPrice && tpSize)) {
|
|
116
|
+
throw new Error("TP update requires both tpPrice and tpSize (or resolvable existing TP fields).");
|
|
117
|
+
}
|
|
118
|
+
if ((slPrice && !slSize) || (!slPrice && slSize)) {
|
|
119
|
+
throw new Error("SL update requires both slPrice and slSize (or resolvable existing SL fields).");
|
|
120
|
+
}
|
|
121
|
+
raw = await updateOrderTpSl(client, address, {
|
|
122
|
+
orderId: args.orderId,
|
|
123
|
+
marketId: market.marketId,
|
|
124
|
+
poolId: args.poolId,
|
|
125
|
+
size: normalizeUnitsInput(size),
|
|
126
|
+
price: normalizeUnitsInput(price),
|
|
127
|
+
tpPrice: tpPrice ? normalizeUnitsInput(tpPrice) : "0",
|
|
128
|
+
tpSize: tpSize ? normalizeUnitsInput(tpSize) : "0",
|
|
129
|
+
slPrice: slPrice ? normalizeUnitsInput(slPrice) : "0",
|
|
130
|
+
slSize: slSize ? normalizeUnitsInput(slSize) : "0",
|
|
131
|
+
quoteToken: market.quoteToken,
|
|
132
|
+
useOrderCollateral: args.useOrderCollateral ?? true
|
|
133
|
+
}, chainId);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
// Create new
|
|
137
|
+
if (direction === undefined || !args.leverage) {
|
|
138
|
+
throw new Error("direction and leverage are required when creating new TP/SL.");
|
|
139
|
+
}
|
|
140
|
+
if (!args.positionId) {
|
|
141
|
+
throw new Error("positionId is required when creating new TP/SL.");
|
|
142
|
+
}
|
|
143
|
+
raw = await setPositionTpSl(client, address, {
|
|
144
|
+
poolId: args.poolId,
|
|
145
|
+
positionId: args.positionId,
|
|
146
|
+
direction,
|
|
147
|
+
leverage: args.leverage,
|
|
148
|
+
executionFeeToken: args.executionFeeToken,
|
|
149
|
+
slippagePct,
|
|
150
|
+
tpPrice: args.tpPrice,
|
|
151
|
+
tpSize: args.tpSize,
|
|
152
|
+
slPrice: args.slPrice,
|
|
153
|
+
slSize: args.slSize
|
|
154
|
+
}, chainId);
|
|
155
|
+
}
|
|
156
|
+
const data = await finalizeMutationResult(raw, signer, "manage_tp_sl");
|
|
157
|
+
return { content: [{ type: "text", text: JSON.stringify({ status: "success", data }, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2) }] };
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
};
|
|
@@ -35,7 +35,7 @@ function pickMarketDetail(res) {
|
|
|
35
35
|
}
|
|
36
36
|
export const openPositionSimpleTool = {
|
|
37
37
|
name: "open_position_simple",
|
|
38
|
-
description: "High-level open position helper. Computes size/price/tradingFee and submits an increase order. Human units by default; use 'raw:' prefix for raw units.",
|
|
38
|
+
description: "[TRADE] High-level open position helper. Computes size/price/tradingFee and submits an increase order. Human units by default; use 'raw:' prefix for raw units.",
|
|
39
39
|
schema: {
|
|
40
40
|
poolId: z.string().optional().describe("Hex Pool ID. Provide either poolId or keyword."),
|
|
41
41
|
keyword: z.string().optional().describe('Recommended: Market keyword, e.g. "BTC", "ETH", "XRP".'),
|
|
@@ -56,7 +56,7 @@ export const openPositionSimpleTool = {
|
|
|
56
56
|
slippagePct: z.coerce
|
|
57
57
|
.string()
|
|
58
58
|
.optional()
|
|
59
|
-
.describe(`${SLIPPAGE_PCT_4DP_DESC} Default
|
|
59
|
+
.describe(`${SLIPPAGE_PCT_4DP_DESC} Default 50 (=0.5%).`),
|
|
60
60
|
postOnly: z.coerce.boolean().optional().describe("Post-only (default false)."),
|
|
61
61
|
executionFeeToken: z.string().optional().describe("Execution fee token address (default quoteToken)."),
|
|
62
62
|
assetClass: z.coerce.number().int().nonnegative().optional().describe("Fee query assetClass (default 1)."),
|
|
@@ -65,6 +65,10 @@ export const openPositionSimpleTool = {
|
|
|
65
65
|
.string()
|
|
66
66
|
.optional()
|
|
67
67
|
.describe("Trading fee. e.g. '0.2' USDC or 'raw:...'. Default: computed via getUserTradingFeeRate."),
|
|
68
|
+
tpPrice: z.coerce.string().optional().describe("Take Profit trigger price."),
|
|
69
|
+
tpSize: z.coerce.string().optional().describe("Take Profit size (base units). If omitted but tpPrice set, uses full position size."),
|
|
70
|
+
slPrice: z.coerce.string().optional().describe("Stop Loss trigger price."),
|
|
71
|
+
slSize: z.coerce.string().optional().describe("Stop Loss size (base units). If omitted but slPrice set, uses full position size."),
|
|
68
72
|
autoApprove: z.coerce.boolean().optional().describe("If true, auto-approve token spend (default false)."),
|
|
69
73
|
approveMax: z.coerce.boolean().optional().describe("If autoApprove, approve MaxUint256 (default false)."),
|
|
70
74
|
autoDeposit: z.coerce
|
|
@@ -118,7 +122,7 @@ export const openPositionSimpleTool = {
|
|
|
118
122
|
}
|
|
119
123
|
const orderType = mapOrderType(args.orderType ?? 0);
|
|
120
124
|
const postOnly = Boolean(args.postOnly ?? false);
|
|
121
|
-
const slippagePct = normalizeSlippagePct4dp(args.slippagePct ?? "
|
|
125
|
+
const slippagePct = normalizeSlippagePct4dp(args.slippagePct ?? "50");
|
|
122
126
|
const executionFeeToken = normalizeAddress(args.executionFeeToken || quoteToken, "executionFeeToken");
|
|
123
127
|
// 4) Determine reference price (30 decimals)
|
|
124
128
|
let price30;
|
|
@@ -222,6 +226,10 @@ export const openPositionSimpleTool = {
|
|
|
222
226
|
autoDeposit: Boolean(args.autoDeposit ?? false),
|
|
223
227
|
priceMeta,
|
|
224
228
|
sizeMeta,
|
|
229
|
+
tpPrice: args.tpPrice ? parseUserPrice30(args.tpPrice, "tpPrice") : null,
|
|
230
|
+
tpSize: args.tpSize ? parseUserUnits(args.tpSize, baseDecimals, "tpSize") : (args.tpPrice ? sizeRaw : null),
|
|
231
|
+
slPrice: args.slPrice ? parseUserPrice30(args.slPrice, "slPrice") : null,
|
|
232
|
+
slSize: args.slSize ? parseUserUnits(args.slSize, baseDecimals, "slSize") : (args.slPrice ? sizeRaw : null),
|
|
225
233
|
};
|
|
226
234
|
if (args.dryRun) {
|
|
227
235
|
return {
|
|
@@ -254,7 +262,7 @@ export const openPositionSimpleTool = {
|
|
|
254
262
|
}
|
|
255
263
|
}
|
|
256
264
|
// 8) Submit increase order using existing trade service
|
|
257
|
-
const
|
|
265
|
+
const openArgs = {
|
|
258
266
|
poolId,
|
|
259
267
|
positionId: "",
|
|
260
268
|
orderType,
|
|
@@ -271,7 +279,16 @@ export const openPositionSimpleTool = {
|
|
|
271
279
|
tradingFee: `raw:${String(tradingFeeRaw)}`,
|
|
272
280
|
marketId,
|
|
273
281
|
autoDeposit: Boolean(args.autoDeposit ?? false),
|
|
274
|
-
}
|
|
282
|
+
};
|
|
283
|
+
if (prep.tpPrice) {
|
|
284
|
+
openArgs.tpPrice = `raw:${prep.tpPrice}`;
|
|
285
|
+
openArgs.tpSize = `raw:${prep.tpSize}`;
|
|
286
|
+
}
|
|
287
|
+
if (prep.slPrice) {
|
|
288
|
+
openArgs.slPrice = `raw:${prep.slPrice}`;
|
|
289
|
+
openArgs.slSize = `raw:${prep.slSize}`;
|
|
290
|
+
}
|
|
291
|
+
const raw = await openPosition(client, address, openArgs);
|
|
275
292
|
const data = await finalizeMutationResult(raw, signer, "open_position_simple");
|
|
276
293
|
const txHash = data.confirmation?.txHash;
|
|
277
294
|
const verification = txHash ? await verifyTradeOutcome(client, address, poolId, txHash) : null;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import * as allTools from "./index.js";
|
|
3
|
+
export const searchToolsTool = {
|
|
4
|
+
name: "search_tools",
|
|
5
|
+
description: "[UTILS] Search for available tools by keyword in their name or description.",
|
|
6
|
+
schema: {
|
|
7
|
+
keyword: z.string().describe("Keyword to search for in tool names or descriptions."),
|
|
8
|
+
},
|
|
9
|
+
handler: async (args) => {
|
|
10
|
+
try {
|
|
11
|
+
const keyword = args.keyword.toLowerCase();
|
|
12
|
+
const tools = Object.values(allTools);
|
|
13
|
+
const matches = tools
|
|
14
|
+
.filter(t => (t.name && t.name.toLowerCase().includes(keyword)) ||
|
|
15
|
+
(t.description && t.description.toLowerCase().includes(keyword)))
|
|
16
|
+
.map(t => ({
|
|
17
|
+
name: t.name,
|
|
18
|
+
description: t.description,
|
|
19
|
+
// We only return name and description to keep it concise
|
|
20
|
+
}));
|
|
21
|
+
if (matches.length === 0) {
|
|
22
|
+
return { content: [{ type: "text", text: `No tools found matching: ${args.keyword}` }] };
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
content: [{
|
|
26
|
+
type: "text",
|
|
27
|
+
text: JSON.stringify({ status: "success", count: matches.length, tools: matches }, null, 2)
|
|
28
|
+
}]
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
};
|
package/dist/utils/mappings.js
CHANGED
|
@@ -50,13 +50,16 @@ export const getHistoryOrderStatusDesc = (status) => {
|
|
|
50
50
|
* MarketPoolState
|
|
51
51
|
*/
|
|
52
52
|
export const getMarketStateDesc = (state) => {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
case
|
|
58
|
-
case
|
|
59
|
-
|
|
53
|
+
if (state === undefined || state === null || state === "")
|
|
54
|
+
return "Unknown";
|
|
55
|
+
const s = Number(state);
|
|
56
|
+
switch (s) {
|
|
57
|
+
case 0: return "Created (Pending Setup)";
|
|
58
|
+
case 1: return "WaitOracle (Waiting for Price)";
|
|
59
|
+
case 2: return "Active (Tradable)";
|
|
60
|
+
case 3: return "PreDelisting (Closing Only)";
|
|
61
|
+
case 4: return "Delisted (Closed)";
|
|
62
|
+
default: return `Other(${s})`;
|
|
60
63
|
}
|
|
61
64
|
};
|
|
62
65
|
/**
|
package/dist/utils/slippage.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const SLIPPAGE_PCT_4DP_RE = /^\d+$/;
|
|
2
2
|
export const SLIPPAGE_PCT_4DP_MAX = 10000n;
|
|
3
3
|
export const SLIPPAGE_PCT_4DP_DESC = "Slippage in 4-decimal precision raw units (1 = 0.01%, 10000 = 100%)";
|
|
4
|
+
const SLIPPAGE_PERCENT_HUMAN_RE = /^\d+(\.\d{1,2})?$/;
|
|
4
5
|
export function isValidSlippagePct4dp(value) {
|
|
5
6
|
if (!SLIPPAGE_PCT_4DP_RE.test(value))
|
|
6
7
|
return false;
|
|
@@ -13,3 +14,25 @@ export function normalizeSlippagePct4dp(value, label = "slippagePct") {
|
|
|
13
14
|
}
|
|
14
15
|
return raw;
|
|
15
16
|
}
|
|
17
|
+
export function normalizeSlippagePct4dpFlexible(value, label = "slippagePct") {
|
|
18
|
+
const raw = String(value ?? "").trim();
|
|
19
|
+
if (!raw) {
|
|
20
|
+
throw new Error(`${label} is required.`);
|
|
21
|
+
}
|
|
22
|
+
// Keep backward-compatible behavior: integer values remain raw 4dp units.
|
|
23
|
+
if (isValidSlippagePct4dp(raw)) {
|
|
24
|
+
return raw;
|
|
25
|
+
}
|
|
26
|
+
// Human percent helper: "1.0" / "1.25" / "1%"
|
|
27
|
+
const percentText = raw.endsWith("%") ? raw.slice(0, -1).trim() : raw;
|
|
28
|
+
if (!SLIPPAGE_PERCENT_HUMAN_RE.test(percentText)) {
|
|
29
|
+
throw new Error(`${label} must be raw 4dp integer (e.g. 100=1%) or human percent like "1.0" / "1%".`);
|
|
30
|
+
}
|
|
31
|
+
const [intPart, fracPart = ""] = percentText.split(".");
|
|
32
|
+
const frac2 = (fracPart + "00").slice(0, 2);
|
|
33
|
+
const converted = BigInt(intPart) * 100n + BigInt(frac2);
|
|
34
|
+
if (converted > SLIPPAGE_PCT_4DP_MAX) {
|
|
35
|
+
throw new Error(`${label} must be <= 100% (raw <= 10000).`);
|
|
36
|
+
}
|
|
37
|
+
return converted.toString();
|
|
38
|
+
}
|