@guiie/buda-mcp 1.3.0 → 1.4.1

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/PUBLISH_CHECKLIST.md +69 -70
  3. package/README.md +4 -4
  4. package/dist/http.js +17 -0
  5. package/dist/index.js +10 -0
  6. package/dist/tools/calculate_position_size.d.ts +48 -0
  7. package/dist/tools/calculate_position_size.d.ts.map +1 -0
  8. package/dist/tools/calculate_position_size.js +111 -0
  9. package/dist/tools/dead_mans_switch.d.ts +84 -0
  10. package/dist/tools/dead_mans_switch.d.ts.map +1 -0
  11. package/dist/tools/dead_mans_switch.js +236 -0
  12. package/dist/tools/market_sentiment.d.ts +30 -0
  13. package/dist/tools/market_sentiment.d.ts.map +1 -0
  14. package/dist/tools/market_sentiment.js +104 -0
  15. package/dist/tools/price_history.d.ts.map +1 -1
  16. package/dist/tools/price_history.js +2 -40
  17. package/dist/tools/simulate_order.d.ts +45 -0
  18. package/dist/tools/simulate_order.d.ts.map +1 -0
  19. package/dist/tools/simulate_order.js +139 -0
  20. package/dist/tools/technical_indicators.d.ts +39 -0
  21. package/dist/tools/technical_indicators.d.ts.map +1 -0
  22. package/dist/tools/technical_indicators.js +223 -0
  23. package/dist/types.d.ts +9 -0
  24. package/dist/types.d.ts.map +1 -1
  25. package/dist/utils.d.ts +7 -1
  26. package/dist/utils.d.ts.map +1 -1
  27. package/dist/utils.js +47 -0
  28. package/marketplace/README.md +1 -1
  29. package/marketplace/claude-listing.md +35 -1
  30. package/marketplace/gemini-tools.json +230 -1
  31. package/marketplace/openapi.yaml +1 -1
  32. package/package.json +1 -1
  33. package/server.json +2 -2
  34. package/src/http.ts +17 -0
  35. package/src/index.ts +10 -0
  36. package/src/tools/calculate_position_size.ts +141 -0
  37. package/src/tools/dead_mans_switch.ts +314 -0
  38. package/src/tools/market_sentiment.ts +141 -0
  39. package/src/tools/price_history.ts +2 -54
  40. package/src/tools/simulate_order.ts +182 -0
  41. package/src/tools/technical_indicators.ts +282 -0
  42. package/src/types.ts +12 -0
  43. package/src/utils.ts +53 -1
  44. package/test/run-all.ts +197 -0
  45. package/test/unit.ts +505 -1
package/test/run-all.ts CHANGED
@@ -18,6 +18,12 @@ try {
18
18
  }
19
19
 
20
20
  import { BudaClient } from "../src/client.js";
21
+ import { MemoryCache } from "../src/cache.js";
22
+ import { handleSimulateOrder } from "../src/tools/simulate_order.js";
23
+ import { handleCalculatePositionSize } from "../src/tools/calculate_position_size.js";
24
+ import { handleMarketSentiment } from "../src/tools/market_sentiment.js";
25
+ import { handleTechnicalIndicators } from "../src/tools/technical_indicators.js";
26
+ import { handleScheduleCancelAll, handleDisarmCancelTimer } from "../src/tools/dead_mans_switch.js";
21
27
  import type {
22
28
  MarketsResponse,
23
29
  TickerResponse,
@@ -225,6 +231,167 @@ try {
225
231
  failures++;
226
232
  }
227
233
 
234
+ // ----------------------------------------------------------------
235
+ // 9. simulate_order
236
+ // ----------------------------------------------------------------
237
+ section(`simulate_order — ${TEST_MARKET} market buy`);
238
+ {
239
+ const cache = new MemoryCache();
240
+ try {
241
+ const result = await handleSimulateOrder(
242
+ { market_id: TEST_MARKET, side: "buy", amount: 0.001 },
243
+ client,
244
+ cache,
245
+ );
246
+ if (result.isError) throw new Error(result.content[0].text);
247
+ const parsed = JSON.parse(result.content[0].text) as {
248
+ simulation: boolean;
249
+ estimated_fill_price: number;
250
+ fee_amount: number;
251
+ fee_rate_pct: number;
252
+ total_cost: number;
253
+ slippage_vs_mid_pct: number;
254
+ order_type_assumed: string;
255
+ };
256
+ if (parsed.simulation !== true) throw new Error("simulation flag must be true");
257
+ pass("simulation: true", "✓");
258
+ pass("order_type_assumed", parsed.order_type_assumed);
259
+ pass("estimated_fill_price", `${parsed.estimated_fill_price.toLocaleString()} CLP`);
260
+ pass("fee_rate_pct", `${parsed.fee_rate_pct}%`);
261
+ pass("fee_amount", `${parsed.fee_amount.toFixed(2)} CLP`);
262
+ pass("total_cost", `${parsed.total_cost.toFixed(2)} CLP`);
263
+ pass("slippage_vs_mid_pct", `${parsed.slippage_vs_mid_pct}%`);
264
+ } catch (err) {
265
+ fail("simulate_order", err);
266
+ failures++;
267
+ }
268
+ }
269
+
270
+ // ----------------------------------------------------------------
271
+ // 10. calculate_position_size
272
+ // ----------------------------------------------------------------
273
+ section(`calculate_position_size — ${TEST_MARKET}`);
274
+ {
275
+ // Fetch live ticker to use real entry/stop prices
276
+ try {
277
+ const tickerData = await client.get<TickerResponse>(
278
+ `/markets/${TEST_MARKET.toLowerCase()}/ticker`,
279
+ );
280
+ const lastPrice = parseFloat(tickerData.ticker.last_price[0]);
281
+ const entryPrice = lastPrice;
282
+ const stopLossPrice = parseFloat((lastPrice * 0.97).toFixed(0)); // 3% below entry
283
+
284
+ const result = handleCalculatePositionSize({
285
+ market_id: TEST_MARKET,
286
+ capital: 1_000_000,
287
+ risk_pct: 2,
288
+ entry_price: entryPrice,
289
+ stop_loss_price: stopLossPrice,
290
+ });
291
+ if (result.isError) throw new Error(result.content[0].text);
292
+ const parsed = JSON.parse(result.content[0].text) as {
293
+ side: string;
294
+ units: number;
295
+ capital_at_risk: number;
296
+ position_value: number;
297
+ fee_impact: number;
298
+ fee_currency: string;
299
+ };
300
+ pass("side", parsed.side);
301
+ pass("units", `${parsed.units} BTC`);
302
+ pass("capital_at_risk", `${parsed.capital_at_risk.toLocaleString()} CLP`);
303
+ pass("position_value", `${parsed.position_value.toLocaleString()} CLP`);
304
+ pass("fee_impact", `${parsed.fee_impact.toFixed(2)} ${parsed.fee_currency}`);
305
+ } catch (err) {
306
+ fail("calculate_position_size", err);
307
+ failures++;
308
+ }
309
+ }
310
+
311
+ // ----------------------------------------------------------------
312
+ // 11. get_market_sentiment
313
+ // ----------------------------------------------------------------
314
+ section(`get_market_sentiment — ${TEST_MARKET}`);
315
+ {
316
+ const cache = new MemoryCache();
317
+ try {
318
+ const result = await handleMarketSentiment({ market_id: TEST_MARKET }, client, cache);
319
+ if (result.isError) throw new Error(result.content[0].text);
320
+ const parsed = JSON.parse(result.content[0].text) as {
321
+ score: number;
322
+ label: string;
323
+ component_breakdown: {
324
+ price_variation_24h_pct: number;
325
+ volume_ratio: number;
326
+ spread_pct: number;
327
+ };
328
+ disclaimer: string;
329
+ };
330
+ if (!["bearish", "neutral", "bullish"].includes(parsed.label)) {
331
+ throw new Error(`unexpected label: ${parsed.label}`);
332
+ }
333
+ if (typeof parsed.score !== "number" || parsed.score < -100 || parsed.score > 100) {
334
+ throw new Error(`score out of range: ${parsed.score}`);
335
+ }
336
+ pass("score", String(parsed.score));
337
+ pass("label", parsed.label);
338
+ pass("price_variation_24h_pct", `${parsed.component_breakdown.price_variation_24h_pct}%`);
339
+ pass("volume_ratio", String(parsed.component_breakdown.volume_ratio));
340
+ pass("spread_pct", `${parsed.component_breakdown.spread_pct}%`);
341
+ pass("disclaimer", parsed.disclaimer.length > 0 ? "present" : "MISSING");
342
+ } catch (err) {
343
+ fail("get_market_sentiment", err);
344
+ failures++;
345
+ }
346
+ }
347
+
348
+ // ----------------------------------------------------------------
349
+ // 12. get_technical_indicators
350
+ // ----------------------------------------------------------------
351
+ section(`get_technical_indicators — ${TEST_MARKET} (1h, limit 1000)`);
352
+ {
353
+ try {
354
+ const result = await handleTechnicalIndicators(
355
+ { market_id: TEST_MARKET, period: "1h", limit: 1000 },
356
+ client,
357
+ );
358
+ if (result.isError) throw new Error(result.content[0].text);
359
+ const parsed = JSON.parse(result.content[0].text) as {
360
+ candles_used?: number;
361
+ candles_available?: number;
362
+ warning?: string;
363
+ indicators: {
364
+ rsi: number | null;
365
+ macd: { line: number; signal: number; histogram: number } | null;
366
+ bollinger_bands: { upper: number; mid: number; lower: number } | null;
367
+ sma_20: number;
368
+ sma_50: number;
369
+ } | null;
370
+ signals: { rsi_signal: string; macd_signal: string; bb_signal: string };
371
+ disclaimer: string;
372
+ };
373
+
374
+ if (parsed.warning === "insufficient_data") {
375
+ pass("warning", `insufficient_data (${parsed.candles_available} candles available, need 50)`);
376
+ } else {
377
+ pass("candles_used", String(parsed.candles_used));
378
+ if (!parsed.indicators) throw new Error("indicators is null without a warning");
379
+ pass("rsi", String(parsed.indicators.rsi));
380
+ pass("rsi_signal", parsed.signals.rsi_signal);
381
+ pass("macd_histogram", String(parsed.indicators.macd?.histogram));
382
+ pass("macd_signal", parsed.signals.macd_signal);
383
+ pass("bb_upper", String(parsed.indicators.bollinger_bands?.upper));
384
+ pass("bb_signal", parsed.signals.bb_signal);
385
+ pass("sma_20", String(parsed.indicators.sma_20));
386
+ pass("sma_50", String(parsed.indicators.sma_50));
387
+ pass("disclaimer", parsed.disclaimer.length > 0 ? "present" : "MISSING");
388
+ }
389
+ } catch (err) {
390
+ fail("get_technical_indicators", err);
391
+ failures++;
392
+ }
393
+ }
394
+
228
395
  // ----------------------------------------------------------------
229
396
  // Auth tools: get_balances, get_orders, place_order, cancel_order
230
397
  // ----------------------------------------------------------------
@@ -263,6 +430,34 @@ if (!client.hasAuth()) {
263
430
  // cancel_order — confirmation guard test (must reject without CONFIRM)
264
431
  console.log(" Skipping: cancel_order live execution (destructive — requires confirmation_token=CONFIRM)");
265
432
  pass("cancel_order guard", "confirmation_token check enforced at tool layer (code-audited)");
433
+
434
+ // schedule_cancel_all — arm then immediately disarm (non-destructive)
435
+ try {
436
+ const armResult = await handleScheduleCancelAll(
437
+ { market_id: TEST_MARKET, ttl_seconds: 300, confirmation_token: "CONFIRM" },
438
+ client,
439
+ );
440
+ if (armResult.isError) throw new Error(armResult.content[0].text);
441
+ const armed = JSON.parse(armResult.content[0].text) as {
442
+ active: boolean;
443
+ expires_at: string;
444
+ ttl_seconds: number;
445
+ warning: string;
446
+ };
447
+ if (!armed.active) throw new Error("active should be true after CONFIRM");
448
+ pass("schedule_cancel_all active", armed.active ? "true" : "false");
449
+ pass("schedule_cancel_all expires_at", armed.expires_at);
450
+ pass("schedule_cancel_all warning", armed.warning.length > 0 ? "present" : "MISSING");
451
+
452
+ // Immediately disarm so no orders are cancelled
453
+ const disarmResult = handleDisarmCancelTimer({ market_id: TEST_MARKET });
454
+ if (disarmResult.isError) throw new Error(disarmResult.content[0].text);
455
+ const disarmed = JSON.parse(disarmResult.content[0].text) as { disarmed: boolean };
456
+ pass("disarm_cancel_timer", disarmed.disarmed ? "timer cleared ✓" : "FAILED to disarm");
457
+ } catch (err) {
458
+ fail("schedule_cancel_all / disarm_cancel_timer", err);
459
+ failures++;
460
+ }
266
461
  }
267
462
 
268
463
  // ----------------------------------------------------------------
@@ -271,6 +466,8 @@ if (!client.hasAuth()) {
271
466
  section("Summary");
272
467
  if (failures === 0) {
273
468
  console.log(" All tools returned valid data from the live Buda API.");
469
+ console.log(" Coverage: simulate_order, calculate_position_size, get_market_sentiment,");
470
+ console.log(" get_technical_indicators, schedule_cancel_all/disarm (auth-gated if credentials set).");
274
471
  } else {
275
472
  console.error(` ${failures} tool(s) failed. See errors above.`);
276
473
  process.exit(1);