@guiie/buda-mcp 1.2.2 → 1.4.0
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 +68 -0
- package/PUBLISH_CHECKLIST.md +71 -63
- package/README.md +4 -4
- package/dist/http.js +39 -0
- package/dist/index.js +29 -0
- package/dist/tools/arbitrage.d.ts +35 -0
- package/dist/tools/arbitrage.d.ts.map +1 -0
- package/dist/tools/arbitrage.js +142 -0
- package/dist/tools/balances.d.ts.map +1 -1
- package/dist/tools/balances.js +24 -4
- package/dist/tools/calculate_position_size.d.ts +48 -0
- package/dist/tools/calculate_position_size.d.ts.map +1 -0
- package/dist/tools/calculate_position_size.js +111 -0
- package/dist/tools/compare_markets.d.ts.map +1 -1
- package/dist/tools/compare_markets.js +11 -10
- package/dist/tools/dead_mans_switch.d.ts +84 -0
- package/dist/tools/dead_mans_switch.d.ts.map +1 -0
- package/dist/tools/dead_mans_switch.js +236 -0
- package/dist/tools/market_sentiment.d.ts +30 -0
- package/dist/tools/market_sentiment.d.ts.map +1 -0
- package/dist/tools/market_sentiment.js +104 -0
- package/dist/tools/market_summary.d.ts +43 -0
- package/dist/tools/market_summary.d.ts.map +1 -0
- package/dist/tools/market_summary.js +81 -0
- package/dist/tools/markets.d.ts.map +1 -1
- package/dist/tools/markets.js +4 -2
- package/dist/tools/orderbook.d.ts.map +1 -1
- package/dist/tools/orderbook.js +14 -4
- package/dist/tools/orders.d.ts.map +1 -1
- package/dist/tools/orders.js +41 -3
- package/dist/tools/price_history.d.ts.map +1 -1
- package/dist/tools/price_history.js +5 -43
- package/dist/tools/simulate_order.d.ts +45 -0
- package/dist/tools/simulate_order.d.ts.map +1 -0
- package/dist/tools/simulate_order.js +139 -0
- package/dist/tools/spread.d.ts.map +1 -1
- package/dist/tools/spread.js +10 -8
- package/dist/tools/technical_indicators.d.ts +39 -0
- package/dist/tools/technical_indicators.d.ts.map +1 -0
- package/dist/tools/technical_indicators.js +223 -0
- package/dist/tools/ticker.d.ts.map +1 -1
- package/dist/tools/ticker.js +24 -3
- package/dist/tools/trades.d.ts.map +1 -1
- package/dist/tools/trades.js +17 -3
- package/dist/tools/volume.d.ts.map +1 -1
- package/dist/tools/volume.js +21 -3
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +23 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +68 -0
- package/marketplace/README.md +1 -1
- package/marketplace/claude-listing.md +60 -14
- package/marketplace/gemini-tools.json +183 -9
- package/marketplace/openapi.yaml +335 -119
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/http.ts +44 -0
- package/src/index.ts +34 -0
- package/src/tools/arbitrage.ts +202 -0
- package/src/tools/balances.ts +27 -4
- package/src/tools/calculate_position_size.ts +141 -0
- package/src/tools/compare_markets.ts +11 -10
- package/src/tools/dead_mans_switch.ts +314 -0
- package/src/tools/market_sentiment.ts +141 -0
- package/src/tools/market_summary.ts +124 -0
- package/src/tools/markets.ts +4 -2
- package/src/tools/orderbook.ts +15 -4
- package/src/tools/orders.ts +45 -4
- package/src/tools/price_history.ts +5 -57
- package/src/tools/simulate_order.ts +182 -0
- package/src/tools/spread.ts +10 -8
- package/src/tools/technical_indicators.ts +282 -0
- package/src/tools/ticker.ts +27 -3
- package/src/tools/trades.ts +18 -3
- package/src/tools/volume.ts +24 -3
- package/src/types.ts +12 -0
- package/src/utils.ts +73 -0
- package/test/unit.ts +758 -0
package/src/http.ts
CHANGED
|
@@ -13,10 +13,18 @@ import * as volume from "./tools/volume.js";
|
|
|
13
13
|
import * as spread from "./tools/spread.js";
|
|
14
14
|
import * as compareMarkets from "./tools/compare_markets.js";
|
|
15
15
|
import * as priceHistory from "./tools/price_history.js";
|
|
16
|
+
import * as arbitrage from "./tools/arbitrage.js";
|
|
17
|
+
import * as marketSummary from "./tools/market_summary.js";
|
|
16
18
|
import * as balances from "./tools/balances.js";
|
|
17
19
|
import * as orders from "./tools/orders.js";
|
|
18
20
|
import * as placeOrder from "./tools/place_order.js";
|
|
19
21
|
import * as cancelOrder from "./tools/cancel_order.js";
|
|
22
|
+
import * as simulateOrder from "./tools/simulate_order.js";
|
|
23
|
+
import * as positionSize from "./tools/calculate_position_size.js";
|
|
24
|
+
import * as marketSentiment from "./tools/market_sentiment.js";
|
|
25
|
+
import * as technicalIndicators from "./tools/technical_indicators.js";
|
|
26
|
+
import * as deadMansSwitch from "./tools/dead_mans_switch.js";
|
|
27
|
+
import { handleMarketSummary } from "./tools/market_summary.js";
|
|
20
28
|
|
|
21
29
|
const PORT = parseInt(process.env.PORT ?? "3000", 10);
|
|
22
30
|
|
|
@@ -39,6 +47,12 @@ const PUBLIC_TOOL_SCHEMAS = [
|
|
|
39
47
|
spread.toolSchema,
|
|
40
48
|
compareMarkets.toolSchema,
|
|
41
49
|
priceHistory.toolSchema,
|
|
50
|
+
arbitrage.toolSchema,
|
|
51
|
+
marketSummary.toolSchema,
|
|
52
|
+
simulateOrder.toolSchema,
|
|
53
|
+
positionSize.toolSchema,
|
|
54
|
+
marketSentiment.toolSchema,
|
|
55
|
+
technicalIndicators.toolSchema,
|
|
42
56
|
];
|
|
43
57
|
|
|
44
58
|
const AUTH_TOOL_SCHEMAS = [
|
|
@@ -46,6 +60,9 @@ const AUTH_TOOL_SCHEMAS = [
|
|
|
46
60
|
orders.toolSchema,
|
|
47
61
|
placeOrder.toolSchema,
|
|
48
62
|
cancelOrder.toolSchema,
|
|
63
|
+
deadMansSwitch.toolSchema,
|
|
64
|
+
deadMansSwitch.renewToolSchema,
|
|
65
|
+
deadMansSwitch.disarmToolSchema,
|
|
49
66
|
];
|
|
50
67
|
|
|
51
68
|
function createServer(): McpServer {
|
|
@@ -62,12 +79,19 @@ function createServer(): McpServer {
|
|
|
62
79
|
spread.register(server, client, reqCache);
|
|
63
80
|
compareMarkets.register(server, client, reqCache);
|
|
64
81
|
priceHistory.register(server, client, reqCache);
|
|
82
|
+
arbitrage.register(server, client, reqCache);
|
|
83
|
+
marketSummary.register(server, client, reqCache);
|
|
84
|
+
simulateOrder.register(server, client, reqCache);
|
|
85
|
+
positionSize.register(server);
|
|
86
|
+
marketSentiment.register(server, client, reqCache);
|
|
87
|
+
technicalIndicators.register(server, client);
|
|
65
88
|
|
|
66
89
|
if (authEnabled) {
|
|
67
90
|
balances.register(server, client);
|
|
68
91
|
orders.register(server, client);
|
|
69
92
|
placeOrder.register(server, client);
|
|
70
93
|
cancelOrder.register(server, client);
|
|
94
|
+
deadMansSwitch.register(server, client);
|
|
71
95
|
}
|
|
72
96
|
|
|
73
97
|
// MCP Resources
|
|
@@ -114,6 +138,25 @@ function createServer(): McpServer {
|
|
|
114
138
|
},
|
|
115
139
|
);
|
|
116
140
|
|
|
141
|
+
server.resource(
|
|
142
|
+
"buda-summary",
|
|
143
|
+
new ResourceTemplate("buda://summary/{market}", { list: undefined }),
|
|
144
|
+
async (uri, params) => {
|
|
145
|
+
const marketId = (params.market as string).toUpperCase();
|
|
146
|
+
const result = await handleMarketSummary({ market_id: marketId }, client, reqCache);
|
|
147
|
+
const text = result.content[0].text;
|
|
148
|
+
return {
|
|
149
|
+
contents: [
|
|
150
|
+
{
|
|
151
|
+
uri: uri.href,
|
|
152
|
+
mimeType: "application/json",
|
|
153
|
+
text,
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
|
|
117
160
|
return server;
|
|
118
161
|
}
|
|
119
162
|
|
|
@@ -140,6 +183,7 @@ app.get("/.well-known/mcp/server-card.json", (_req, res) => {
|
|
|
140
183
|
resources: [
|
|
141
184
|
{ uri: "buda://markets", name: "All Buda.com markets", mimeType: "application/json" },
|
|
142
185
|
{ uri: "buda://ticker/{market}", name: "Ticker for a specific market", mimeType: "application/json" },
|
|
186
|
+
{ uri: "buda://summary/{market}", name: "Full market summary with liquidity rating", mimeType: "application/json" },
|
|
143
187
|
],
|
|
144
188
|
prompts: [],
|
|
145
189
|
});
|
package/src/index.ts
CHANGED
|
@@ -14,10 +14,18 @@ import * as volume from "./tools/volume.js";
|
|
|
14
14
|
import * as spread from "./tools/spread.js";
|
|
15
15
|
import * as compareMarkets from "./tools/compare_markets.js";
|
|
16
16
|
import * as priceHistory from "./tools/price_history.js";
|
|
17
|
+
import * as arbitrage from "./tools/arbitrage.js";
|
|
18
|
+
import * as marketSummary from "./tools/market_summary.js";
|
|
17
19
|
import * as balances from "./tools/balances.js";
|
|
18
20
|
import * as orders from "./tools/orders.js";
|
|
19
21
|
import * as placeOrder from "./tools/place_order.js";
|
|
20
22
|
import * as cancelOrder from "./tools/cancel_order.js";
|
|
23
|
+
import * as simulateOrder from "./tools/simulate_order.js";
|
|
24
|
+
import * as positionSize from "./tools/calculate_position_size.js";
|
|
25
|
+
import * as marketSentiment from "./tools/market_sentiment.js";
|
|
26
|
+
import * as technicalIndicators from "./tools/technical_indicators.js";
|
|
27
|
+
import * as deadMansSwitch from "./tools/dead_mans_switch.js";
|
|
28
|
+
import { handleMarketSummary } from "./tools/market_summary.js";
|
|
21
29
|
|
|
22
30
|
const client = new BudaClient(
|
|
23
31
|
undefined,
|
|
@@ -39,6 +47,12 @@ volume.register(server, client, cache);
|
|
|
39
47
|
spread.register(server, client, cache);
|
|
40
48
|
compareMarkets.register(server, client, cache);
|
|
41
49
|
priceHistory.register(server, client, cache);
|
|
50
|
+
arbitrage.register(server, client, cache);
|
|
51
|
+
marketSummary.register(server, client, cache);
|
|
52
|
+
simulateOrder.register(server, client, cache);
|
|
53
|
+
positionSize.register(server);
|
|
54
|
+
marketSentiment.register(server, client, cache);
|
|
55
|
+
technicalIndicators.register(server, client);
|
|
42
56
|
|
|
43
57
|
// Auth-gated tools — only registered when API credentials are present
|
|
44
58
|
if (client.hasAuth()) {
|
|
@@ -46,6 +60,7 @@ if (client.hasAuth()) {
|
|
|
46
60
|
orders.register(server, client);
|
|
47
61
|
placeOrder.register(server, client);
|
|
48
62
|
cancelOrder.register(server, client);
|
|
63
|
+
deadMansSwitch.register(server, client);
|
|
49
64
|
}
|
|
50
65
|
|
|
51
66
|
// MCP Resources
|
|
@@ -92,5 +107,24 @@ server.resource(
|
|
|
92
107
|
},
|
|
93
108
|
);
|
|
94
109
|
|
|
110
|
+
server.resource(
|
|
111
|
+
"buda-summary",
|
|
112
|
+
new ResourceTemplate("buda://summary/{market}", { list: undefined }),
|
|
113
|
+
async (uri, params) => {
|
|
114
|
+
const marketId = (params.market as string).toUpperCase();
|
|
115
|
+
const result = await handleMarketSummary({ market_id: marketId }, client, cache);
|
|
116
|
+
const text = result.content[0].text;
|
|
117
|
+
return {
|
|
118
|
+
contents: [
|
|
119
|
+
{
|
|
120
|
+
uri: uri.href,
|
|
121
|
+
mimeType: "application/json",
|
|
122
|
+
text,
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
|
|
95
129
|
const transport = new StdioServerTransport();
|
|
96
130
|
await server.connect(transport);
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { BudaClient, BudaApiError } from "../client.js";
|
|
4
|
+
import { MemoryCache, CACHE_TTL } from "../cache.js";
|
|
5
|
+
import type { AllTickersResponse, Ticker } from "../types.js";
|
|
6
|
+
|
|
7
|
+
export const toolSchema = {
|
|
8
|
+
name: "get_arbitrage_opportunities",
|
|
9
|
+
description:
|
|
10
|
+
"Detects cross-country price discrepancies for a given asset across Buda's CLP, COP, and PEN markets, " +
|
|
11
|
+
"normalized to USDC. Fetches all relevant tickers, converts each local price to USDC using the " +
|
|
12
|
+
"current USDC-CLP / USDC-COP / USDC-PEN rates, then computes pairwise discrepancy percentages. " +
|
|
13
|
+
"Results above threshold_pct are returned sorted by opportunity size. Note: Buda taker fee is 0.8% " +
|
|
14
|
+
"per leg (~1.6% round-trip) — always deduct fees before acting on any discrepancy. " +
|
|
15
|
+
"Example: 'Is there an arbitrage opportunity for BTC between Chile and Peru right now?'",
|
|
16
|
+
inputSchema: {
|
|
17
|
+
type: "object" as const,
|
|
18
|
+
properties: {
|
|
19
|
+
base_currency: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "Base asset to scan (e.g. 'BTC', 'ETH', 'XRP').",
|
|
22
|
+
},
|
|
23
|
+
threshold_pct: {
|
|
24
|
+
type: "number",
|
|
25
|
+
description:
|
|
26
|
+
"Minimum price discrepancy percentage to include in results (default: 0.5). " +
|
|
27
|
+
"Buda taker fee is 0.8% per leg, so a round-trip requires > 1.6% to be profitable.",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
required: ["base_currency"],
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
interface ArbitrageOpportunity {
|
|
35
|
+
market_a: string;
|
|
36
|
+
market_b: string;
|
|
37
|
+
price_a_usdc: number;
|
|
38
|
+
price_b_usdc: number;
|
|
39
|
+
discrepancy_pct: number;
|
|
40
|
+
higher_market: string;
|
|
41
|
+
lower_market: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface ArbitrageInput {
|
|
45
|
+
base_currency: string;
|
|
46
|
+
threshold_pct?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function handleArbitrageOpportunities(
|
|
50
|
+
{ base_currency, threshold_pct = 0.5 }: ArbitrageInput,
|
|
51
|
+
client: BudaClient,
|
|
52
|
+
cache: MemoryCache,
|
|
53
|
+
): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
|
|
54
|
+
try {
|
|
55
|
+
const base = base_currency.toUpperCase();
|
|
56
|
+
const data = await cache.getOrFetch<AllTickersResponse>(
|
|
57
|
+
"tickers:all",
|
|
58
|
+
CACHE_TTL.TICKER,
|
|
59
|
+
() => client.get<AllTickersResponse>("/tickers"),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const tickerMap = new Map<string, Ticker>();
|
|
63
|
+
for (const t of data.tickers) {
|
|
64
|
+
tickerMap.set(t.market_id, t);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Find USDC conversion rates for each fiat currency
|
|
68
|
+
const usdcClpTicker = tickerMap.get("USDC-CLP");
|
|
69
|
+
const usdcCopTicker = tickerMap.get("USDC-COP");
|
|
70
|
+
const usdcPenTicker = tickerMap.get("USDC-PEN");
|
|
71
|
+
|
|
72
|
+
// Build list of markets for the requested base currency with USDC-normalized prices
|
|
73
|
+
interface MarketPrice {
|
|
74
|
+
market_id: string;
|
|
75
|
+
local_price: number;
|
|
76
|
+
usdc_rate: number;
|
|
77
|
+
price_usdc: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const marketPrices: MarketPrice[] = [];
|
|
81
|
+
|
|
82
|
+
const candidates: Array<{ suffix: string; usdcTicker: Ticker | undefined }> = [
|
|
83
|
+
{ suffix: "CLP", usdcTicker: usdcClpTicker },
|
|
84
|
+
{ suffix: "COP", usdcTicker: usdcCopTicker },
|
|
85
|
+
{ suffix: "PEN", usdcTicker: usdcPenTicker },
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
for (const { suffix, usdcTicker } of candidates) {
|
|
89
|
+
const marketId = `${base}-${suffix}`;
|
|
90
|
+
const baseTicker = tickerMap.get(marketId);
|
|
91
|
+
|
|
92
|
+
if (!baseTicker || !usdcTicker) continue;
|
|
93
|
+
|
|
94
|
+
const localPrice = parseFloat(baseTicker.last_price[0]);
|
|
95
|
+
const usdcRate = parseFloat(usdcTicker.last_price[0]);
|
|
96
|
+
|
|
97
|
+
if (isNaN(localPrice) || isNaN(usdcRate) || usdcRate === 0) continue;
|
|
98
|
+
|
|
99
|
+
marketPrices.push({
|
|
100
|
+
market_id: marketId,
|
|
101
|
+
local_price: localPrice,
|
|
102
|
+
usdc_rate: usdcRate,
|
|
103
|
+
price_usdc: localPrice / usdcRate,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (marketPrices.length < 2) {
|
|
108
|
+
return {
|
|
109
|
+
content: [
|
|
110
|
+
{
|
|
111
|
+
type: "text",
|
|
112
|
+
text: JSON.stringify({
|
|
113
|
+
error: `Not enough markets found for base currency '${base}' to compute arbitrage. ` +
|
|
114
|
+
`Need at least 2 of: ${base}-CLP, ${base}-COP, ${base}-PEN with USDC rates available.`,
|
|
115
|
+
code: "INSUFFICIENT_MARKETS",
|
|
116
|
+
}),
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
isError: true,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Compute all pairwise discrepancies
|
|
124
|
+
const opportunities: ArbitrageOpportunity[] = [];
|
|
125
|
+
|
|
126
|
+
for (let i = 0; i < marketPrices.length; i++) {
|
|
127
|
+
for (let j = i + 1; j < marketPrices.length; j++) {
|
|
128
|
+
const a = marketPrices[i];
|
|
129
|
+
const b = marketPrices[j];
|
|
130
|
+
const minPrice = Math.min(a.price_usdc, b.price_usdc);
|
|
131
|
+
const discrepancyPct = (Math.abs(a.price_usdc - b.price_usdc) / minPrice) * 100;
|
|
132
|
+
|
|
133
|
+
if (discrepancyPct < threshold_pct) continue;
|
|
134
|
+
|
|
135
|
+
const higherMarket = a.price_usdc > b.price_usdc ? a.market_id : b.market_id;
|
|
136
|
+
const lowerMarket = a.price_usdc < b.price_usdc ? a.market_id : b.market_id;
|
|
137
|
+
|
|
138
|
+
opportunities.push({
|
|
139
|
+
market_a: a.market_id,
|
|
140
|
+
market_b: b.market_id,
|
|
141
|
+
price_a_usdc: parseFloat(a.price_usdc.toFixed(4)),
|
|
142
|
+
price_b_usdc: parseFloat(b.price_usdc.toFixed(4)),
|
|
143
|
+
discrepancy_pct: parseFloat(discrepancyPct.toFixed(4)),
|
|
144
|
+
higher_market: higherMarket,
|
|
145
|
+
lower_market: lowerMarket,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
opportunities.sort((a, b) => b.discrepancy_pct - a.discrepancy_pct);
|
|
151
|
+
|
|
152
|
+
const result = {
|
|
153
|
+
base_currency: base,
|
|
154
|
+
threshold_pct,
|
|
155
|
+
markets_analyzed: marketPrices.map((m) => ({
|
|
156
|
+
market_id: m.market_id,
|
|
157
|
+
price_usdc: parseFloat(m.price_usdc.toFixed(4)),
|
|
158
|
+
local_price: m.local_price,
|
|
159
|
+
usdc_rate: m.usdc_rate,
|
|
160
|
+
})),
|
|
161
|
+
opportunities_found: opportunities.length,
|
|
162
|
+
opportunities,
|
|
163
|
+
fees_note:
|
|
164
|
+
"Buda taker fee is 0.8% per leg. A round-trip arbitrage (buy on one market, sell on another) " +
|
|
165
|
+
"costs approximately 1.6% in fees. Only discrepancies well above 1.6% are likely profitable.",
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
170
|
+
};
|
|
171
|
+
} catch (err) {
|
|
172
|
+
const msg =
|
|
173
|
+
err instanceof BudaApiError
|
|
174
|
+
? { error: err.message, code: err.status, path: err.path }
|
|
175
|
+
: { error: String(err), code: "UNKNOWN" };
|
|
176
|
+
return {
|
|
177
|
+
content: [{ type: "text", text: JSON.stringify(msg) }],
|
|
178
|
+
isError: true,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function register(server: McpServer, client: BudaClient, cache: MemoryCache): void {
|
|
184
|
+
server.tool(
|
|
185
|
+
toolSchema.name,
|
|
186
|
+
toolSchema.description,
|
|
187
|
+
{
|
|
188
|
+
base_currency: z
|
|
189
|
+
.string()
|
|
190
|
+
.describe("Base asset to scan (e.g. 'BTC', 'ETH', 'XRP')."),
|
|
191
|
+
threshold_pct: z
|
|
192
|
+
.number()
|
|
193
|
+
.min(0)
|
|
194
|
+
.default(0.5)
|
|
195
|
+
.describe(
|
|
196
|
+
"Minimum price discrepancy percentage to include in results (default: 0.5). " +
|
|
197
|
+
"Buda taker fee is 0.8% per leg, so a round-trip requires > 1.6% to be profitable.",
|
|
198
|
+
),
|
|
199
|
+
},
|
|
200
|
+
(args) => handleArbitrageOpportunities(args, client, cache),
|
|
201
|
+
);
|
|
202
|
+
}
|
package/src/tools/balances.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { BudaClient, BudaApiError } from "../client.js";
|
|
3
|
+
import { flattenAmount } from "../utils.js";
|
|
3
4
|
import type { BalancesResponse } from "../types.js";
|
|
4
5
|
|
|
5
6
|
export const toolSchema = {
|
|
6
7
|
name: "get_balances",
|
|
7
8
|
description:
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
9
|
+
"Returns all currency balances for the authenticated Buda.com account as flat typed objects. " +
|
|
10
|
+
"Each currency entry includes total amount, available amount (not frozen), frozen amount, and " +
|
|
11
|
+
"pending withdrawal amount — all as floats with separate _currency fields. " +
|
|
12
|
+
"Requires BUDA_API_KEY and BUDA_API_SECRET. " +
|
|
13
|
+
"Example: 'How much BTC do I have available to trade right now?'",
|
|
11
14
|
inputSchema: {
|
|
12
15
|
type: "object" as const,
|
|
13
16
|
properties: {},
|
|
@@ -22,8 +25,28 @@ export function register(server: McpServer, client: BudaClient): void {
|
|
|
22
25
|
async () => {
|
|
23
26
|
try {
|
|
24
27
|
const data = await client.get<BalancesResponse>("/balances");
|
|
28
|
+
|
|
29
|
+
const result = data.balances.map((b) => {
|
|
30
|
+
const amount = flattenAmount(b.amount);
|
|
31
|
+
const available = flattenAmount(b.available_amount);
|
|
32
|
+
const frozen = flattenAmount(b.frozen_amount);
|
|
33
|
+
const pending = flattenAmount(b.pending_withdraw_amount);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
id: b.id,
|
|
37
|
+
amount: amount.value,
|
|
38
|
+
amount_currency: amount.currency,
|
|
39
|
+
available_amount: available.value,
|
|
40
|
+
available_amount_currency: available.currency,
|
|
41
|
+
frozen_amount: frozen.value,
|
|
42
|
+
frozen_amount_currency: frozen.currency,
|
|
43
|
+
pending_withdraw_amount: pending.value,
|
|
44
|
+
pending_withdraw_amount_currency: pending.currency,
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
|
|
25
48
|
return {
|
|
26
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
49
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
27
50
|
};
|
|
28
51
|
} catch (err) {
|
|
29
52
|
const msg =
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { validateMarketId } from "../validation.js";
|
|
4
|
+
|
|
5
|
+
export const toolSchema = {
|
|
6
|
+
name: "calculate_position_size",
|
|
7
|
+
description:
|
|
8
|
+
"Calculates position size based on your capital, risk tolerance, entry price, and stop-loss. " +
|
|
9
|
+
"Determines how many units to buy or sell so that a stop-loss hit costs exactly risk_pct% of capital. " +
|
|
10
|
+
"Fully client-side — no API call is made. " +
|
|
11
|
+
"Example: 'How many BTC can I buy on BTC-CLP if I have 1,000,000 CLP, risk 2%, entry 80,000,000 CLP, stop at 78,000,000 CLP?'",
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: "object" as const,
|
|
14
|
+
properties: {
|
|
15
|
+
market_id: {
|
|
16
|
+
type: "string",
|
|
17
|
+
description: "Market ID (e.g. 'BTC-CLP', 'ETH-COP'). Used to derive the quote currency.",
|
|
18
|
+
},
|
|
19
|
+
capital: {
|
|
20
|
+
type: "number",
|
|
21
|
+
description: "Total available capital in the quote currency (e.g. CLP for BTC-CLP).",
|
|
22
|
+
},
|
|
23
|
+
risk_pct: {
|
|
24
|
+
type: "number",
|
|
25
|
+
description: "Percentage of capital to risk on this trade (0.1–10, e.g. 2 = 2%).",
|
|
26
|
+
},
|
|
27
|
+
entry_price: {
|
|
28
|
+
type: "number",
|
|
29
|
+
description: "Planned entry price in quote currency.",
|
|
30
|
+
},
|
|
31
|
+
stop_loss_price: {
|
|
32
|
+
type: "number",
|
|
33
|
+
description:
|
|
34
|
+
"Stop-loss price in quote currency. Must be below entry for buys, above entry for sells.",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
required: ["market_id", "capital", "risk_pct", "entry_price", "stop_loss_price"],
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type CalculatePositionSizeArgs = {
|
|
42
|
+
market_id: string;
|
|
43
|
+
capital: number;
|
|
44
|
+
risk_pct: number;
|
|
45
|
+
entry_price: number;
|
|
46
|
+
stop_loss_price: number;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export function handleCalculatePositionSize(
|
|
50
|
+
args: CalculatePositionSizeArgs,
|
|
51
|
+
): { content: Array<{ type: "text"; text: string }>; isError?: boolean } {
|
|
52
|
+
const { market_id, capital, risk_pct, entry_price, stop_loss_price } = args;
|
|
53
|
+
|
|
54
|
+
const validationError = validateMarketId(market_id);
|
|
55
|
+
if (validationError) {
|
|
56
|
+
return {
|
|
57
|
+
content: [{ type: "text", text: JSON.stringify({ error: validationError, code: "INVALID_MARKET_ID" }) }],
|
|
58
|
+
isError: true,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (stop_loss_price === entry_price) {
|
|
63
|
+
return {
|
|
64
|
+
content: [
|
|
65
|
+
{
|
|
66
|
+
type: "text",
|
|
67
|
+
text: JSON.stringify({
|
|
68
|
+
error: "stop_loss_price must differ from entry_price.",
|
|
69
|
+
code: "INVALID_STOP_LOSS",
|
|
70
|
+
}),
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
isError: true,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const quoteCurrency = market_id.split("-")[1].toUpperCase();
|
|
78
|
+
const baseCurrency = market_id.split("-")[0].toUpperCase();
|
|
79
|
+
const side: "buy" | "sell" = stop_loss_price < entry_price ? "buy" : "sell";
|
|
80
|
+
|
|
81
|
+
const capitalAtRisk = capital * (risk_pct / 100);
|
|
82
|
+
const riskPerUnit = Math.abs(entry_price - stop_loss_price);
|
|
83
|
+
const units = capitalAtRisk / riskPerUnit;
|
|
84
|
+
const positionValue = units * entry_price;
|
|
85
|
+
const feeImpact = parseFloat((positionValue * 0.008).toFixed(8));
|
|
86
|
+
|
|
87
|
+
const riskRewardNote =
|
|
88
|
+
`${side === "buy" ? "Buy" : "Sell"} ${units.toFixed(8)} ${baseCurrency} at ${entry_price} ${quoteCurrency} ` +
|
|
89
|
+
`with stop at ${stop_loss_price} ${quoteCurrency}. ` +
|
|
90
|
+
`Risking ${risk_pct}% of capital (${capitalAtRisk.toFixed(2)} ${quoteCurrency}) ` +
|
|
91
|
+
`on a ${riskPerUnit.toFixed(2)} ${quoteCurrency}/unit move. ` +
|
|
92
|
+
`Estimated entry fee: ${feeImpact.toFixed(2)} ${quoteCurrency} (0.8% taker, conservative estimate).`;
|
|
93
|
+
|
|
94
|
+
const result = {
|
|
95
|
+
market_id: market_id.toUpperCase(),
|
|
96
|
+
side,
|
|
97
|
+
units: parseFloat(units.toFixed(8)),
|
|
98
|
+
base_currency: baseCurrency,
|
|
99
|
+
capital_at_risk: parseFloat(capitalAtRisk.toFixed(2)),
|
|
100
|
+
position_value: parseFloat(positionValue.toFixed(2)),
|
|
101
|
+
fee_impact: feeImpact,
|
|
102
|
+
fee_currency: quoteCurrency,
|
|
103
|
+
risk_reward_note: riskRewardNote,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function register(server: McpServer): void {
|
|
112
|
+
server.tool(
|
|
113
|
+
toolSchema.name,
|
|
114
|
+
toolSchema.description,
|
|
115
|
+
{
|
|
116
|
+
market_id: z
|
|
117
|
+
.string()
|
|
118
|
+
.describe("Market ID (e.g. 'BTC-CLP', 'ETH-COP'). Used to derive the quote currency."),
|
|
119
|
+
capital: z
|
|
120
|
+
.number()
|
|
121
|
+
.positive()
|
|
122
|
+
.describe("Total available capital in the quote currency (e.g. CLP for BTC-CLP)."),
|
|
123
|
+
risk_pct: z
|
|
124
|
+
.number()
|
|
125
|
+
.min(0.1)
|
|
126
|
+
.max(10)
|
|
127
|
+
.describe("Percentage of capital to risk on this trade (0.1–10, e.g. 2 = 2%)."),
|
|
128
|
+
entry_price: z
|
|
129
|
+
.number()
|
|
130
|
+
.positive()
|
|
131
|
+
.describe("Planned entry price in quote currency."),
|
|
132
|
+
stop_loss_price: z
|
|
133
|
+
.number()
|
|
134
|
+
.positive()
|
|
135
|
+
.describe(
|
|
136
|
+
"Stop-loss price in quote currency. Must be below entry for buys, above entry for sells.",
|
|
137
|
+
),
|
|
138
|
+
},
|
|
139
|
+
(args) => handleCalculatePositionSize(args),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
@@ -7,9 +7,10 @@ import type { AllTickersResponse } from "../types.js";
|
|
|
7
7
|
export const toolSchema = {
|
|
8
8
|
name: "compare_markets",
|
|
9
9
|
description:
|
|
10
|
-
"
|
|
11
|
-
"supported quote currencies (CLP, COP, PEN, BTC, USDC, ETH). " +
|
|
12
|
-
"
|
|
10
|
+
"Returns side-by-side ticker data for all trading pairs of a given base currency across Buda.com's " +
|
|
11
|
+
"supported quote currencies (CLP, COP, PEN, BTC, USDC, ETH). All prices are floats; " +
|
|
12
|
+
"price_change_24h and price_change_7d are floats in percent (e.g. 1.23 means +1.23%). " +
|
|
13
|
+
"Example: 'In which country is Bitcoin currently most expensive on Buda?'",
|
|
13
14
|
inputSchema: {
|
|
14
15
|
type: "object" as const,
|
|
15
16
|
properties: {
|
|
@@ -67,16 +68,16 @@ export function register(server: McpServer, client: BudaClient, cache: MemoryCac
|
|
|
67
68
|
base_currency: base,
|
|
68
69
|
markets: matching.map((t) => ({
|
|
69
70
|
market_id: t.market_id,
|
|
70
|
-
last_price: t.last_price[0],
|
|
71
|
-
|
|
72
|
-
best_bid: t.max_bid ? t.max_bid[0] : null,
|
|
73
|
-
best_ask: t.min_ask ? t.min_ask[0] : null,
|
|
74
|
-
volume_24h: t.volume ? t.volume[0] : null,
|
|
71
|
+
last_price: parseFloat(t.last_price[0]),
|
|
72
|
+
last_price_currency: t.last_price[1],
|
|
73
|
+
best_bid: t.max_bid ? parseFloat(t.max_bid[0]) : null,
|
|
74
|
+
best_ask: t.min_ask ? parseFloat(t.min_ask[0]) : null,
|
|
75
|
+
volume_24h: t.volume ? parseFloat(t.volume[0]) : null,
|
|
75
76
|
price_change_24h: t.price_variation_24h
|
|
76
|
-
? (parseFloat(t.price_variation_24h) * 100).toFixed(
|
|
77
|
+
? parseFloat((parseFloat(t.price_variation_24h) * 100).toFixed(4))
|
|
77
78
|
: null,
|
|
78
79
|
price_change_7d: t.price_variation_7d
|
|
79
|
-
? (parseFloat(t.price_variation_7d) * 100).toFixed(
|
|
80
|
+
? parseFloat((parseFloat(t.price_variation_7d) * 100).toFixed(4))
|
|
80
81
|
: null,
|
|
81
82
|
})),
|
|
82
83
|
};
|