@guiie/buda-mcp 1.3.0 → 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 +31 -0
- package/PUBLISH_CHECKLIST.md +69 -70
- package/README.md +4 -4
- package/dist/http.js +17 -0
- package/dist/index.js +10 -0
- 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/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/price_history.d.ts.map +1 -1
- package/dist/tools/price_history.js +2 -40
- 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/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/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +7 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +47 -0
- package/marketplace/README.md +1 -1
- package/marketplace/claude-listing.md +35 -1
- package/marketplace/gemini-tools.json +143 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/http.ts +17 -0
- package/src/index.ts +10 -0
- package/src/tools/calculate_position_size.ts +141 -0
- package/src/tools/dead_mans_switch.ts +314 -0
- package/src/tools/market_sentiment.ts +141 -0
- package/src/tools/price_history.ts +2 -54
- package/src/tools/simulate_order.ts +182 -0
- package/src/tools/technical_indicators.ts +282 -0
- package/src/types.ts +12 -0
- package/src/utils.ts +53 -1
- package/test/unit.ts +505 -1
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { validateMarketId } from "../validation.js";
|
|
3
|
+
const timers = new Map();
|
|
4
|
+
async function cancelAllOrdersForMarket(marketId, client) {
|
|
5
|
+
try {
|
|
6
|
+
const data = await client.get(`/markets/${marketId}/orders`, { state: "pending", per: 300 });
|
|
7
|
+
const orders = data.orders ?? [];
|
|
8
|
+
await Promise.allSettled(orders.map((order) => client.put(`/orders/${order.id}`, { state: "canceling" })));
|
|
9
|
+
timers.delete(marketId);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
// Swallow errors — the timer has fired; we cannot surface them to the caller
|
|
13
|
+
timers.delete(marketId);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function armTimer(marketId, ttlSeconds, client) {
|
|
17
|
+
const existing = timers.get(marketId);
|
|
18
|
+
if (existing)
|
|
19
|
+
clearTimeout(existing.timeout);
|
|
20
|
+
const expiresAt = Date.now() + ttlSeconds * 1000;
|
|
21
|
+
const timeout = setTimeout(() => {
|
|
22
|
+
void cancelAllOrdersForMarket(marketId, client);
|
|
23
|
+
}, ttlSeconds * 1000);
|
|
24
|
+
const entry = { timeout, expiresAt, ttlSeconds };
|
|
25
|
+
timers.set(marketId, entry);
|
|
26
|
+
return entry;
|
|
27
|
+
}
|
|
28
|
+
// ---- Tool schemas ----
|
|
29
|
+
export const toolSchema = {
|
|
30
|
+
name: "schedule_cancel_all",
|
|
31
|
+
description: "WARNING: timer state is lost on server restart. Not suitable as a production dead man's switch " +
|
|
32
|
+
"on hosted deployments (e.g. Railway). Use only on locally-run instances.\n\n" +
|
|
33
|
+
"Arms an in-memory dead man's switch: if not renewed within ttl_seconds, all open orders for the " +
|
|
34
|
+
"market are automatically cancelled. Requires confirmation_token='CONFIRM' to activate. " +
|
|
35
|
+
"Use renew_cancel_timer to reset the countdown, or disarm_cancel_timer to cancel without touching orders. " +
|
|
36
|
+
"Requires BUDA_API_KEY and BUDA_API_SECRET environment variables.",
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: "object",
|
|
39
|
+
properties: {
|
|
40
|
+
market_id: {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: "Market ID to protect (e.g. 'BTC-CLP').",
|
|
43
|
+
},
|
|
44
|
+
ttl_seconds: {
|
|
45
|
+
type: "number",
|
|
46
|
+
description: "Seconds before all orders are cancelled if not renewed (10–300).",
|
|
47
|
+
},
|
|
48
|
+
confirmation_token: {
|
|
49
|
+
type: "string",
|
|
50
|
+
description: "Must equal exactly 'CONFIRM' (case-sensitive) to arm the switch.",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
required: ["market_id", "ttl_seconds", "confirmation_token"],
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
export const renewToolSchema = {
|
|
57
|
+
name: "renew_cancel_timer",
|
|
58
|
+
description: "Resets the dead man's switch TTL for a market, preventing automatic order cancellation. " +
|
|
59
|
+
"No confirmation required. Requires an active timer set by schedule_cancel_all. " +
|
|
60
|
+
"Requires BUDA_API_KEY and BUDA_API_SECRET environment variables.",
|
|
61
|
+
inputSchema: {
|
|
62
|
+
type: "object",
|
|
63
|
+
properties: {
|
|
64
|
+
market_id: {
|
|
65
|
+
type: "string",
|
|
66
|
+
description: "Market ID whose timer should be renewed (e.g. 'BTC-CLP').",
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
required: ["market_id"],
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
export const disarmToolSchema = {
|
|
73
|
+
name: "disarm_cancel_timer",
|
|
74
|
+
description: "Disarms the dead man's switch for a market without cancelling any orders. " +
|
|
75
|
+
"No confirmation required. Safe to call even if no timer is active. " +
|
|
76
|
+
"Requires BUDA_API_KEY and BUDA_API_SECRET environment variables.",
|
|
77
|
+
inputSchema: {
|
|
78
|
+
type: "object",
|
|
79
|
+
properties: {
|
|
80
|
+
market_id: {
|
|
81
|
+
type: "string",
|
|
82
|
+
description: "Market ID whose timer should be disarmed (e.g. 'BTC-CLP').",
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
required: ["market_id"],
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
export async function handleScheduleCancelAll(args, client) {
|
|
89
|
+
const { market_id, ttl_seconds, confirmation_token } = args;
|
|
90
|
+
if (confirmation_token !== "CONFIRM") {
|
|
91
|
+
return {
|
|
92
|
+
content: [
|
|
93
|
+
{
|
|
94
|
+
type: "text",
|
|
95
|
+
text: JSON.stringify({
|
|
96
|
+
error: "Dead man's switch not armed. confirmation_token must equal 'CONFIRM' to activate. " +
|
|
97
|
+
"Review the parameters and set confirmation_token='CONFIRM' to proceed.",
|
|
98
|
+
code: "CONFIRMATION_REQUIRED",
|
|
99
|
+
market_id,
|
|
100
|
+
ttl_seconds,
|
|
101
|
+
}),
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
isError: true,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const validationError = validateMarketId(market_id);
|
|
108
|
+
if (validationError) {
|
|
109
|
+
return {
|
|
110
|
+
content: [{ type: "text", text: JSON.stringify({ error: validationError, code: "INVALID_MARKET_ID" }) }],
|
|
111
|
+
isError: true,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const id = market_id.toLowerCase();
|
|
115
|
+
const entry = armTimer(id, ttl_seconds, client);
|
|
116
|
+
return {
|
|
117
|
+
content: [
|
|
118
|
+
{
|
|
119
|
+
type: "text",
|
|
120
|
+
text: JSON.stringify({
|
|
121
|
+
active: true,
|
|
122
|
+
market_id: market_id.toUpperCase(),
|
|
123
|
+
expires_at: new Date(entry.expiresAt).toISOString(),
|
|
124
|
+
ttl_seconds,
|
|
125
|
+
warning: "in-memory only — timer is lost on server restart. Not suitable for hosted deployments.",
|
|
126
|
+
}),
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
export function handleRenewCancelTimer({ market_id }, client) {
|
|
132
|
+
const validationError = validateMarketId(market_id);
|
|
133
|
+
if (validationError) {
|
|
134
|
+
return {
|
|
135
|
+
content: [{ type: "text", text: JSON.stringify({ error: validationError, code: "INVALID_MARKET_ID" }) }],
|
|
136
|
+
isError: true,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const id = market_id.toLowerCase();
|
|
140
|
+
const existing = timers.get(id);
|
|
141
|
+
if (!existing) {
|
|
142
|
+
return {
|
|
143
|
+
content: [
|
|
144
|
+
{
|
|
145
|
+
type: "text",
|
|
146
|
+
text: JSON.stringify({
|
|
147
|
+
error: `No active dead man's switch for market ${market_id.toUpperCase()}. Arm one first with schedule_cancel_all.`,
|
|
148
|
+
code: "NO_ACTIVE_TIMER",
|
|
149
|
+
market_id: market_id.toUpperCase(),
|
|
150
|
+
}),
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
isError: true,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const entry = armTimer(id, existing.ttlSeconds, client);
|
|
157
|
+
return {
|
|
158
|
+
content: [
|
|
159
|
+
{
|
|
160
|
+
type: "text",
|
|
161
|
+
text: JSON.stringify({
|
|
162
|
+
active: true,
|
|
163
|
+
market_id: market_id.toUpperCase(),
|
|
164
|
+
expires_at: new Date(entry.expiresAt).toISOString(),
|
|
165
|
+
ttl_seconds: entry.ttlSeconds,
|
|
166
|
+
}),
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
export function handleDisarmCancelTimer({ market_id }) {
|
|
172
|
+
const validationError = validateMarketId(market_id);
|
|
173
|
+
if (validationError) {
|
|
174
|
+
return {
|
|
175
|
+
content: [{ type: "text", text: JSON.stringify({ error: validationError, code: "INVALID_MARKET_ID" }) }],
|
|
176
|
+
isError: true,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const id = market_id.toLowerCase();
|
|
180
|
+
const existing = timers.get(id);
|
|
181
|
+
if (!existing) {
|
|
182
|
+
return {
|
|
183
|
+
content: [
|
|
184
|
+
{
|
|
185
|
+
type: "text",
|
|
186
|
+
text: JSON.stringify({
|
|
187
|
+
disarmed: false,
|
|
188
|
+
market_id: market_id.toUpperCase(),
|
|
189
|
+
note: "No active timer for this market.",
|
|
190
|
+
}),
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
clearTimeout(existing.timeout);
|
|
196
|
+
timers.delete(id);
|
|
197
|
+
return {
|
|
198
|
+
content: [
|
|
199
|
+
{
|
|
200
|
+
type: "text",
|
|
201
|
+
text: JSON.stringify({
|
|
202
|
+
disarmed: true,
|
|
203
|
+
market_id: market_id.toUpperCase(),
|
|
204
|
+
}),
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
// ---- Registration ----
|
|
210
|
+
export function register(server, client) {
|
|
211
|
+
server.tool(toolSchema.name, toolSchema.description, {
|
|
212
|
+
market_id: z
|
|
213
|
+
.string()
|
|
214
|
+
.describe("Market ID to protect (e.g. 'BTC-CLP')."),
|
|
215
|
+
ttl_seconds: z
|
|
216
|
+
.number()
|
|
217
|
+
.int()
|
|
218
|
+
.min(10)
|
|
219
|
+
.max(300)
|
|
220
|
+
.describe("Seconds before all orders are cancelled if not renewed (10–300)."),
|
|
221
|
+
confirmation_token: z
|
|
222
|
+
.string()
|
|
223
|
+
.describe("Must equal exactly 'CONFIRM' (case-sensitive) to arm the switch."),
|
|
224
|
+
}, (args) => handleScheduleCancelAll(args, client));
|
|
225
|
+
server.tool(renewToolSchema.name, renewToolSchema.description, {
|
|
226
|
+
market_id: z
|
|
227
|
+
.string()
|
|
228
|
+
.describe("Market ID whose timer should be renewed (e.g. 'BTC-CLP')."),
|
|
229
|
+
}, (args) => handleRenewCancelTimer(args, client));
|
|
230
|
+
server.tool(disarmToolSchema.name, disarmToolSchema.description, {
|
|
231
|
+
market_id: z
|
|
232
|
+
.string()
|
|
233
|
+
.describe("Market ID whose timer should be disarmed (e.g. 'BTC-CLP')."),
|
|
234
|
+
}, (args) => handleDisarmCancelTimer(args));
|
|
235
|
+
}
|
|
236
|
+
//# sourceMappingURL=dead_mans_switch.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { BudaClient } from "../client.js";
|
|
3
|
+
import { MemoryCache } from "../cache.js";
|
|
4
|
+
export declare const toolSchema: {
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: "object";
|
|
9
|
+
properties: {
|
|
10
|
+
market_id: {
|
|
11
|
+
type: string;
|
|
12
|
+
description: string;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
required: string[];
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
type MarketSentimentArgs = {
|
|
19
|
+
market_id: string;
|
|
20
|
+
};
|
|
21
|
+
export declare function handleMarketSentiment({ market_id }: MarketSentimentArgs, client: BudaClient, cache: MemoryCache): Promise<{
|
|
22
|
+
content: Array<{
|
|
23
|
+
type: "text";
|
|
24
|
+
text: string;
|
|
25
|
+
}>;
|
|
26
|
+
isError?: boolean;
|
|
27
|
+
}>;
|
|
28
|
+
export declare function register(server: McpServer, client: BudaClient, cache: MemoryCache): void;
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=market_sentiment.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"market_sentiment.d.ts","sourceRoot":"","sources":["../../src/tools/market_sentiment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,UAAU,EAAgB,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,WAAW,EAAa,MAAM,aAAa,CAAC;AAIrD,eAAO,MAAM,UAAU;;;;;;;;;;;;;CAiBtB,CAAC;AAUF,KAAK,mBAAmB,GAAG;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjD,wBAAsB,qBAAqB,CACzC,EAAE,SAAS,EAAE,EAAE,mBAAmB,EAClC,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,WAAW,GACjB,OAAO,CAAC;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAuFhF;AAED,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAWxF"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { BudaApiError } from "../client.js";
|
|
3
|
+
import { CACHE_TTL } from "../cache.js";
|
|
4
|
+
import { validateMarketId } from "../validation.js";
|
|
5
|
+
export const toolSchema = {
|
|
6
|
+
name: "get_market_sentiment",
|
|
7
|
+
description: "Computes a composite sentiment score (−100 to +100) for a Buda.com market based on " +
|
|
8
|
+
"24h price variation (40%), volume vs 7-day average (35%), and bid/ask spread vs baseline (25%). " +
|
|
9
|
+
"Returns a score, a label (bearish/neutral/bullish), and a full component breakdown. " +
|
|
10
|
+
"Example: 'Is the BTC-CLP market currently bullish or bearish?'",
|
|
11
|
+
inputSchema: {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
market_id: {
|
|
15
|
+
type: "string",
|
|
16
|
+
description: "Market ID (e.g. 'BTC-CLP', 'ETH-BTC', 'BTC-USDT').",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
required: ["market_id"],
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
function clamp(value, min, max) {
|
|
23
|
+
return Math.min(max, Math.max(min, value));
|
|
24
|
+
}
|
|
25
|
+
function isStablecoinPair(marketId) {
|
|
26
|
+
return /-(USDT|USDC|DAI|TUSD)$/i.test(marketId);
|
|
27
|
+
}
|
|
28
|
+
export async function handleMarketSentiment({ market_id }, client, cache) {
|
|
29
|
+
const validationError = validateMarketId(market_id);
|
|
30
|
+
if (validationError) {
|
|
31
|
+
return {
|
|
32
|
+
content: [{ type: "text", text: JSON.stringify({ error: validationError, code: "INVALID_MARKET_ID" }) }],
|
|
33
|
+
isError: true,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const id = market_id.toLowerCase();
|
|
38
|
+
const [tickerData, volumeData] = await Promise.all([
|
|
39
|
+
cache.getOrFetch(`ticker:${id}`, CACHE_TTL.TICKER, () => client.get(`/markets/${id}/ticker`)),
|
|
40
|
+
client.get(`/markets/${id}/volume`),
|
|
41
|
+
]);
|
|
42
|
+
const ticker = tickerData.ticker;
|
|
43
|
+
const vol = volumeData.volume;
|
|
44
|
+
const bid = parseFloat(ticker.max_bid[0]);
|
|
45
|
+
const ask = parseFloat(ticker.min_ask[0]);
|
|
46
|
+
const priceVariation24h = parseFloat(ticker.price_variation_24h);
|
|
47
|
+
const ask24h = parseFloat(vol.ask_volume_24h[0]);
|
|
48
|
+
const bid24h = parseFloat(vol.bid_volume_24h[0]);
|
|
49
|
+
const ask7d = parseFloat(vol.ask_volume_7d[0]);
|
|
50
|
+
const bid7d = parseFloat(vol.bid_volume_7d[0]);
|
|
51
|
+
const spreadPct = ask > 0 ? ((ask - bid) / ask) * 100 : 0;
|
|
52
|
+
const spreadBaseline = isStablecoinPair(market_id) ? 0.3 : 1.0;
|
|
53
|
+
const volume24h = ask24h + bid24h;
|
|
54
|
+
const volume7d = ask7d + bid7d;
|
|
55
|
+
const volumeRatio = volume7d > 0 ? (volume24h * 7) / volume7d : 1;
|
|
56
|
+
// Price component: ±5% daily change → ±100 on this sub-score
|
|
57
|
+
const priceRaw = clamp(priceVariation24h * 2000, -100, 100);
|
|
58
|
+
const priceScore = parseFloat((priceRaw * 0.4).toFixed(4));
|
|
59
|
+
// Volume component: ratio vs 7d daily average
|
|
60
|
+
const volumeRaw = clamp((volumeRatio - 1) * 100, -100, 100);
|
|
61
|
+
const volumeScore = parseFloat((volumeRaw * 0.35).toFixed(4));
|
|
62
|
+
// Spread component: tighter spread is bullish
|
|
63
|
+
const spreadRaw = clamp((1 - spreadPct / spreadBaseline) * 100, -100, 100);
|
|
64
|
+
const spreadScore = parseFloat((spreadRaw * 0.25).toFixed(4));
|
|
65
|
+
const score = parseFloat((priceScore + volumeScore + spreadScore).toFixed(1));
|
|
66
|
+
const label = score < -20 ? "bearish" : score > 20 ? "bullish" : "neutral";
|
|
67
|
+
const result = {
|
|
68
|
+
market_id: ticker.market_id,
|
|
69
|
+
score,
|
|
70
|
+
label,
|
|
71
|
+
component_breakdown: {
|
|
72
|
+
price_variation_24h_pct: parseFloat((priceVariation24h * 100).toFixed(4)),
|
|
73
|
+
volume_ratio: parseFloat(volumeRatio.toFixed(4)),
|
|
74
|
+
spread_pct: parseFloat(spreadPct.toFixed(4)),
|
|
75
|
+
spread_baseline_pct: spreadBaseline,
|
|
76
|
+
price_score: priceScore,
|
|
77
|
+
volume_score: volumeScore,
|
|
78
|
+
spread_score: spreadScore,
|
|
79
|
+
},
|
|
80
|
+
data_timestamp: new Date().toISOString(),
|
|
81
|
+
disclaimer: "Sentiment is derived from market microstructure data only. Not investment advice.",
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
const msg = err instanceof BudaApiError
|
|
89
|
+
? { error: err.message, code: err.status, path: err.path }
|
|
90
|
+
: { error: String(err), code: "UNKNOWN" };
|
|
91
|
+
return {
|
|
92
|
+
content: [{ type: "text", text: JSON.stringify(msg) }],
|
|
93
|
+
isError: true,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
export function register(server, client, cache) {
|
|
98
|
+
server.tool(toolSchema.name, toolSchema.description, {
|
|
99
|
+
market_id: z
|
|
100
|
+
.string()
|
|
101
|
+
.describe("Market ID (e.g. 'BTC-CLP', 'ETH-BTC', 'BTC-USDT')."),
|
|
102
|
+
}, (args) => handleMarketSentiment(args, client, cache));
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=market_sentiment.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"price_history.d.ts","sourceRoot":"","sources":["../../src/tools/price_history.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,UAAU,EAAgB,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"price_history.d.ts","sourceRoot":"","sources":["../../src/tools/price_history.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,UAAU,EAAgB,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAK1C,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;CA4BtB,CAAC;AAEF,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI,CAiFzF"}
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { BudaApiError } from "../client.js";
|
|
3
3
|
import { validateMarketId } from "../validation.js";
|
|
4
|
-
|
|
5
|
-
"1h": 60 * 60 * 1000,
|
|
6
|
-
"4h": 4 * 60 * 60 * 1000,
|
|
7
|
-
"1d": 24 * 60 * 60 * 1000,
|
|
8
|
-
};
|
|
4
|
+
import { aggregateTradesToCandles } from "../utils.js";
|
|
9
5
|
export const toolSchema = {
|
|
10
6
|
name: "get_price_history",
|
|
11
7
|
description: "IMPORTANT: Candles are aggregated client-side from raw trades (Buda has no native candlestick " +
|
|
@@ -73,41 +69,7 @@ export function register(server, client, _cache) {
|
|
|
73
69
|
],
|
|
74
70
|
};
|
|
75
71
|
}
|
|
76
|
-
|
|
77
|
-
// and close = last chronological price within each candle bucket.
|
|
78
|
-
const sortedEntries = [...entries].sort(([a], [b]) => parseInt(a, 10) - parseInt(b, 10));
|
|
79
|
-
const periodMs = PERIOD_MS[period];
|
|
80
|
-
const buckets = new Map();
|
|
81
|
-
for (const [tsMs, amount, price, _direction] of sortedEntries) {
|
|
82
|
-
const ts = parseInt(tsMs, 10);
|
|
83
|
-
const bucketStart = Math.floor(ts / periodMs) * periodMs;
|
|
84
|
-
const p = parseFloat(price);
|
|
85
|
-
const v = parseFloat(amount);
|
|
86
|
-
if (!buckets.has(bucketStart)) {
|
|
87
|
-
buckets.set(bucketStart, {
|
|
88
|
-
time: new Date(bucketStart).toISOString(),
|
|
89
|
-
open: p,
|
|
90
|
-
high: p,
|
|
91
|
-
low: p,
|
|
92
|
-
close: p,
|
|
93
|
-
volume: v,
|
|
94
|
-
trade_count: 1,
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
const candle = buckets.get(bucketStart);
|
|
99
|
-
if (p > candle.high)
|
|
100
|
-
candle.high = p;
|
|
101
|
-
if (p < candle.low)
|
|
102
|
-
candle.low = p;
|
|
103
|
-
candle.close = p;
|
|
104
|
-
candle.volume = parseFloat((candle.volume + v).toFixed(8));
|
|
105
|
-
candle.trade_count++;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
const candles = Array.from(buckets.entries())
|
|
109
|
-
.sort(([a], [b]) => a - b)
|
|
110
|
-
.map(([, candle]) => candle);
|
|
72
|
+
const candles = aggregateTradesToCandles(entries, period);
|
|
111
73
|
const result = {
|
|
112
74
|
market_id: market_id.toUpperCase(),
|
|
113
75
|
period,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { BudaClient } from "../client.js";
|
|
3
|
+
import { MemoryCache } from "../cache.js";
|
|
4
|
+
export declare const toolSchema: {
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: "object";
|
|
9
|
+
properties: {
|
|
10
|
+
market_id: {
|
|
11
|
+
type: string;
|
|
12
|
+
description: string;
|
|
13
|
+
};
|
|
14
|
+
side: {
|
|
15
|
+
type: string;
|
|
16
|
+
description: string;
|
|
17
|
+
};
|
|
18
|
+
amount: {
|
|
19
|
+
type: string;
|
|
20
|
+
description: string;
|
|
21
|
+
};
|
|
22
|
+
price: {
|
|
23
|
+
type: string;
|
|
24
|
+
description: string;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
required: string[];
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
type SimulateOrderArgs = {
|
|
31
|
+
market_id: string;
|
|
32
|
+
side: "buy" | "sell";
|
|
33
|
+
amount: number;
|
|
34
|
+
price?: number;
|
|
35
|
+
};
|
|
36
|
+
export declare function handleSimulateOrder(args: SimulateOrderArgs, client: BudaClient, cache: MemoryCache): Promise<{
|
|
37
|
+
content: Array<{
|
|
38
|
+
type: "text";
|
|
39
|
+
text: string;
|
|
40
|
+
}>;
|
|
41
|
+
isError?: boolean;
|
|
42
|
+
}>;
|
|
43
|
+
export declare function register(server: McpServer, client: BudaClient, cache: MemoryCache): void;
|
|
44
|
+
export {};
|
|
45
|
+
//# sourceMappingURL=simulate_order.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"simulate_order.d.ts","sourceRoot":"","sources":["../../src/tools/simulate_order.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,UAAU,EAAgB,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,WAAW,EAAa,MAAM,aAAa,CAAC;AAIrD,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;CA+BtB,CAAC;AAEF,KAAK,iBAAiB,GAAG;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,KAAK,GAAG,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,iBAAiB,EACvB,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,WAAW,GACjB,OAAO,CAAC;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CAyGhF;AAED,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAuBxF"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { BudaApiError } from "../client.js";
|
|
3
|
+
import { CACHE_TTL } from "../cache.js";
|
|
4
|
+
import { validateMarketId } from "../validation.js";
|
|
5
|
+
export const toolSchema = {
|
|
6
|
+
name: "simulate_order",
|
|
7
|
+
description: "Simulates a buy or sell order on Buda.com using live ticker data — no order is placed. " +
|
|
8
|
+
"Returns estimated fill price, fee, total cost, and slippage vs mid-price. " +
|
|
9
|
+
"Omit 'price' for a market order simulation; supply 'price' for a limit order simulation. " +
|
|
10
|
+
"All outputs are labelled simulation: true — this tool never places a real order. " +
|
|
11
|
+
"Example: 'How much would it cost to buy 0.01 BTC on BTC-CLP right now?'",
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
market_id: {
|
|
16
|
+
type: "string",
|
|
17
|
+
description: "Market ID (e.g. 'BTC-CLP', 'ETH-BTC').",
|
|
18
|
+
},
|
|
19
|
+
side: {
|
|
20
|
+
type: "string",
|
|
21
|
+
description: "'buy' or 'sell'.",
|
|
22
|
+
},
|
|
23
|
+
amount: {
|
|
24
|
+
type: "number",
|
|
25
|
+
description: "Order size in base currency (e.g. BTC for BTC-CLP).",
|
|
26
|
+
},
|
|
27
|
+
price: {
|
|
28
|
+
type: "number",
|
|
29
|
+
description: "Limit price in quote currency. Omit for a market order simulation.",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
required: ["market_id", "side", "amount"],
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
export async function handleSimulateOrder(args, client, cache) {
|
|
36
|
+
const { market_id, side, amount, price } = args;
|
|
37
|
+
const validationError = validateMarketId(market_id);
|
|
38
|
+
if (validationError) {
|
|
39
|
+
return {
|
|
40
|
+
content: [{ type: "text", text: JSON.stringify({ error: validationError, code: "INVALID_MARKET_ID" }) }],
|
|
41
|
+
isError: true,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const id = market_id.toLowerCase();
|
|
46
|
+
const [tickerData, marketData] = await Promise.all([
|
|
47
|
+
cache.getOrFetch(`ticker:${id}`, CACHE_TTL.TICKER, () => client.get(`/markets/${id}/ticker`)),
|
|
48
|
+
cache.getOrFetch(`market:${id}`, CACHE_TTL.MARKETS, () => client.get(`/markets/${id}`)),
|
|
49
|
+
]);
|
|
50
|
+
const ticker = tickerData.ticker;
|
|
51
|
+
const market = marketData.market;
|
|
52
|
+
const minAsk = parseFloat(ticker.min_ask[0]);
|
|
53
|
+
const maxBid = parseFloat(ticker.max_bid[0]);
|
|
54
|
+
const quoteCurrency = ticker.min_ask[1];
|
|
55
|
+
if (isNaN(minAsk) || isNaN(maxBid) || minAsk <= 0 || maxBid <= 0) {
|
|
56
|
+
return {
|
|
57
|
+
content: [
|
|
58
|
+
{
|
|
59
|
+
type: "text",
|
|
60
|
+
text: JSON.stringify({
|
|
61
|
+
error: "Unable to simulate: invalid or zero bid/ask values in ticker.",
|
|
62
|
+
code: "INVALID_TICKER",
|
|
63
|
+
}),
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
isError: true,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const mid = (minAsk + maxBid) / 2;
|
|
70
|
+
const takerFeeRate = parseFloat(market.taker_fee);
|
|
71
|
+
const orderTypeAssumed = price !== undefined ? "limit" : "market";
|
|
72
|
+
let estimatedFillPrice;
|
|
73
|
+
if (orderTypeAssumed === "market") {
|
|
74
|
+
estimatedFillPrice = side === "buy" ? minAsk : maxBid;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
// Limit order: fill at provided price if it crosses the spread, otherwise at limit price
|
|
78
|
+
if (side === "buy") {
|
|
79
|
+
estimatedFillPrice = price >= minAsk ? minAsk : price;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
estimatedFillPrice = price <= maxBid ? maxBid : price;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const grossValue = amount * estimatedFillPrice;
|
|
86
|
+
const feeAmount = parseFloat((grossValue * takerFeeRate).toFixed(8));
|
|
87
|
+
const totalCost = side === "buy"
|
|
88
|
+
? parseFloat((grossValue + feeAmount).toFixed(8))
|
|
89
|
+
: parseFloat((grossValue - feeAmount).toFixed(8));
|
|
90
|
+
const slippageVsMidPct = parseFloat((((estimatedFillPrice - mid) / mid) * 100).toFixed(4));
|
|
91
|
+
const result = {
|
|
92
|
+
simulation: true,
|
|
93
|
+
market_id: ticker.market_id,
|
|
94
|
+
side,
|
|
95
|
+
amount,
|
|
96
|
+
order_type_assumed: orderTypeAssumed,
|
|
97
|
+
estimated_fill_price: parseFloat(estimatedFillPrice.toFixed(2)),
|
|
98
|
+
price_currency: quoteCurrency,
|
|
99
|
+
fee_amount: feeAmount,
|
|
100
|
+
fee_currency: quoteCurrency,
|
|
101
|
+
fee_rate_pct: parseFloat((takerFeeRate * 100).toFixed(3)),
|
|
102
|
+
total_cost: totalCost,
|
|
103
|
+
slippage_vs_mid_pct: slippageVsMidPct,
|
|
104
|
+
mid_price: parseFloat(mid.toFixed(2)),
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
const msg = err instanceof BudaApiError
|
|
112
|
+
? { error: err.message, code: err.status, path: err.path }
|
|
113
|
+
: { error: String(err), code: "UNKNOWN" };
|
|
114
|
+
return {
|
|
115
|
+
content: [{ type: "text", text: JSON.stringify(msg) }],
|
|
116
|
+
isError: true,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export function register(server, client, cache) {
|
|
121
|
+
server.tool(toolSchema.name, toolSchema.description, {
|
|
122
|
+
market_id: z
|
|
123
|
+
.string()
|
|
124
|
+
.describe("Market ID (e.g. 'BTC-CLP', 'ETH-BTC')."),
|
|
125
|
+
side: z
|
|
126
|
+
.enum(["buy", "sell"])
|
|
127
|
+
.describe("'buy' or 'sell'."),
|
|
128
|
+
amount: z
|
|
129
|
+
.number()
|
|
130
|
+
.positive()
|
|
131
|
+
.describe("Order size in base currency (e.g. BTC for BTC-CLP)."),
|
|
132
|
+
price: z
|
|
133
|
+
.number()
|
|
134
|
+
.positive()
|
|
135
|
+
.optional()
|
|
136
|
+
.describe("Limit price in quote currency. Omit for a market order simulation."),
|
|
137
|
+
}, (args) => handleSimulateOrder(args, client, cache));
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=simulate_order.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { BudaClient } from "../client.js";
|
|
3
|
+
export declare const toolSchema: {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: "object";
|
|
8
|
+
properties: {
|
|
9
|
+
market_id: {
|
|
10
|
+
type: string;
|
|
11
|
+
description: string;
|
|
12
|
+
};
|
|
13
|
+
period: {
|
|
14
|
+
type: string;
|
|
15
|
+
description: string;
|
|
16
|
+
};
|
|
17
|
+
limit: {
|
|
18
|
+
type: string;
|
|
19
|
+
description: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
required: string[];
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
type TechnicalIndicatorsArgs = {
|
|
26
|
+
market_id: string;
|
|
27
|
+
period: "1h" | "4h" | "1d";
|
|
28
|
+
limit?: number;
|
|
29
|
+
};
|
|
30
|
+
export declare function handleTechnicalIndicators(args: TechnicalIndicatorsArgs, client: BudaClient): Promise<{
|
|
31
|
+
content: Array<{
|
|
32
|
+
type: "text";
|
|
33
|
+
text: string;
|
|
34
|
+
}>;
|
|
35
|
+
isError?: boolean;
|
|
36
|
+
}>;
|
|
37
|
+
export declare function register(server: McpServer, client: BudaClient): void;
|
|
38
|
+
export {};
|
|
39
|
+
//# sourceMappingURL=technical_indicators.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"technical_indicators.d.ts","sourceRoot":"","sources":["../../src/tools/technical_indicators.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEpE,OAAO,EAAE,UAAU,EAAgB,MAAM,cAAc,CAAC;AAKxD,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;CA6BtB,CAAC;AAsGF,KAAK,uBAAuB,GAAG;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,uBAAuB,EAC7B,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CA2GhF;AAED,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI,CAyBpE"}
|