@guiie/buda-mcp 1.2.1 → 1.3.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.
Files changed (56) hide show
  1. package/.cursor/rules/release-workflow.mdc +54 -0
  2. package/CHANGELOG.md +37 -0
  3. package/PUBLISH_CHECKLIST.md +62 -53
  4. package/dist/http.js +22 -0
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.js +20 -0
  7. package/dist/tools/arbitrage.d.ts +35 -0
  8. package/dist/tools/arbitrage.d.ts.map +1 -0
  9. package/dist/tools/arbitrage.js +142 -0
  10. package/dist/tools/balances.d.ts.map +1 -1
  11. package/dist/tools/balances.js +24 -4
  12. package/dist/tools/compare_markets.d.ts.map +1 -1
  13. package/dist/tools/compare_markets.js +11 -10
  14. package/dist/tools/market_summary.d.ts +43 -0
  15. package/dist/tools/market_summary.d.ts.map +1 -0
  16. package/dist/tools/market_summary.js +81 -0
  17. package/dist/tools/markets.d.ts.map +1 -1
  18. package/dist/tools/markets.js +4 -2
  19. package/dist/tools/orderbook.d.ts.map +1 -1
  20. package/dist/tools/orderbook.js +14 -4
  21. package/dist/tools/orders.d.ts.map +1 -1
  22. package/dist/tools/orders.js +41 -3
  23. package/dist/tools/price_history.js +14 -14
  24. package/dist/tools/spread.d.ts.map +1 -1
  25. package/dist/tools/spread.js +10 -8
  26. package/dist/tools/ticker.d.ts.map +1 -1
  27. package/dist/tools/ticker.js +24 -3
  28. package/dist/tools/trades.d.ts.map +1 -1
  29. package/dist/tools/trades.js +17 -3
  30. package/dist/tools/volume.d.ts.map +1 -1
  31. package/dist/tools/volume.js +21 -3
  32. package/dist/utils.d.ts +17 -0
  33. package/dist/utils.d.ts.map +1 -0
  34. package/dist/utils.js +21 -0
  35. package/marketplace/README.md +1 -1
  36. package/marketplace/claude-listing.md +26 -14
  37. package/marketplace/gemini-tools.json +41 -9
  38. package/marketplace/openapi.yaml +335 -119
  39. package/package.json +1 -1
  40. package/server.json +2 -2
  41. package/src/http.ts +27 -0
  42. package/src/index.ts +25 -0
  43. package/src/tools/arbitrage.ts +202 -0
  44. package/src/tools/balances.ts +27 -4
  45. package/src/tools/compare_markets.ts +11 -10
  46. package/src/tools/market_summary.ts +124 -0
  47. package/src/tools/markets.ts +4 -2
  48. package/src/tools/orderbook.ts +15 -4
  49. package/src/tools/orders.ts +45 -4
  50. package/src/tools/price_history.ts +17 -17
  51. package/src/tools/spread.ts +10 -8
  52. package/src/tools/ticker.ts +27 -3
  53. package/src/tools/trades.ts +18 -3
  54. package/src/tools/volume.ts +24 -3
  55. package/src/utils.ts +21 -0
  56. package/test/unit.ts +254 -0
@@ -13,11 +13,11 @@ const PERIOD_MS: Record<string, number> = {
13
13
 
14
14
  interface OhlcvCandle {
15
15
  time: string;
16
- open: string;
17
- high: string;
18
- low: string;
19
- close: string;
20
- volume: string;
16
+ open: number;
17
+ high: number;
18
+ low: number;
19
+ close: number;
20
+ volume: number;
21
21
  trade_count: number;
22
22
  }
23
23
 
@@ -26,9 +26,9 @@ export const toolSchema = {
26
26
  description:
27
27
  "IMPORTANT: Candles are aggregated client-side from raw trades (Buda has no native candlestick " +
28
28
  "endpoint) — fetching more trades via the 'limit' parameter gives deeper history but slower " +
29
- "responses. Returns OHLCV (open/high/low/close/volume) price history for a Buda.com market. " +
30
- "Candle timestamps are UTC bucket boundaries (e.g. '2026-04-10T12:00:00.000Z' for 1h). " +
31
- "Supports 1h, 4h, and 1d candle periods.",
29
+ "responses. Returns OHLCV candles (open/high/low/close as floats in quote currency; volume as float " +
30
+ "in base currency) for periods 1h, 4h, or 1d. Candle timestamps are UTC bucket boundaries. " +
31
+ "Example: 'Show me the hourly BTC-CLP price chart for the past 24 hours.'",
32
32
  inputSchema: {
33
33
  type: "object" as const,
34
34
  properties: {
@@ -122,19 +122,19 @@ export function register(server: McpServer, client: BudaClient, _cache: MemoryCa
122
122
  if (!buckets.has(bucketStart)) {
123
123
  buckets.set(bucketStart, {
124
124
  time: new Date(bucketStart).toISOString(),
125
- open: price,
126
- high: price,
127
- low: price,
128
- close: price,
129
- volume: amount,
125
+ open: p,
126
+ high: p,
127
+ low: p,
128
+ close: p,
129
+ volume: v,
130
130
  trade_count: 1,
131
131
  });
132
132
  } else {
133
133
  const candle = buckets.get(bucketStart)!;
134
- if (p > parseFloat(candle.high)) candle.high = price;
135
- if (p < parseFloat(candle.low)) candle.low = price;
136
- candle.close = price;
137
- candle.volume = (parseFloat(candle.volume) + v).toFixed(8);
134
+ if (p > candle.high) candle.high = p;
135
+ if (p < candle.low) candle.low = p;
136
+ candle.close = p;
137
+ candle.volume = parseFloat((candle.volume + v).toFixed(8));
138
138
  candle.trade_count++;
139
139
  }
140
140
  }
@@ -8,8 +8,10 @@ import type { TickerResponse } from "../types.js";
8
8
  export const toolSchema = {
9
9
  name: "get_spread",
10
10
  description:
11
- "Calculate the bid/ask spread for a Buda.com market. " +
12
- "Returns the best bid, best ask, absolute spread, and spread as a percentage of the ask price.",
11
+ "Returns the best bid, best ask, absolute spread, and spread percentage for a Buda.com market. " +
12
+ "All prices are floats in the quote currency (e.g. CLP). spread_percentage is a float in percent " +
13
+ "(e.g. 0.15 means 0.15%). Use this to evaluate liquidity before placing a large order. " +
14
+ "Example: 'Is BTC-CLP liquid enough to buy 10M CLP without significant slippage?'",
13
15
  inputSchema: {
14
16
  type: "object" as const,
15
17
  properties: {
@@ -70,12 +72,12 @@ export function register(server: McpServer, client: BudaClient, cache: MemoryCac
70
72
 
71
73
  const result = {
72
74
  market_id: ticker.market_id,
73
- currency,
74
- best_bid: bid.toString(),
75
- best_ask: ask.toString(),
76
- spread_absolute: spreadAbs.toFixed(2),
77
- spread_percentage: spreadPct.toFixed(4) + "%",
78
- last_price: ticker.last_price[0],
75
+ price_currency: currency,
76
+ best_bid: bid,
77
+ best_ask: ask,
78
+ spread_absolute: parseFloat(spreadAbs.toFixed(2)),
79
+ spread_percentage: parseFloat(spreadPct.toFixed(4)),
80
+ last_price: parseFloat(ticker.last_price[0]),
79
81
  };
80
82
 
81
83
  return {
@@ -3,13 +3,16 @@ import { z } from "zod";
3
3
  import { BudaClient, BudaApiError } from "../client.js";
4
4
  import { MemoryCache, CACHE_TTL } from "../cache.js";
5
5
  import { validateMarketId } from "../validation.js";
6
+ import { flattenAmount } from "../utils.js";
6
7
  import type { TickerResponse } from "../types.js";
7
8
 
8
9
  export const toolSchema = {
9
10
  name: "get_ticker",
10
11
  description:
11
- "Get the current ticker for a Buda.com market: last traded price, best bid/ask, " +
12
- "24h volume, and price change over 24h and 7d.",
12
+ "Returns the current market snapshot for a Buda.com market: last traded price, best bid, " +
13
+ "best ask, 24h volume, and price change over 24h and 7d. All prices are floats in the quote " +
14
+ "currency (e.g. CLP for BTC-CLP). price_variation_24h is a decimal fraction (0.012 = +1.2%). " +
15
+ "Example: 'What is the current Bitcoin price in Chilean pesos?'",
13
16
  inputSchema: {
14
17
  type: "object" as const,
15
18
  properties: {
@@ -47,8 +50,29 @@ export function register(server: McpServer, client: BudaClient, cache: MemoryCac
47
50
  CACHE_TTL.TICKER,
48
51
  () => client.get<TickerResponse>(`/markets/${id}/ticker`),
49
52
  );
53
+
54
+ const t = data.ticker;
55
+ const lastPrice = flattenAmount(t.last_price);
56
+ const minAsk = flattenAmount(t.min_ask);
57
+ const maxBid = flattenAmount(t.max_bid);
58
+ const volume = flattenAmount(t.volume);
59
+
60
+ const result = {
61
+ market_id: t.market_id,
62
+ last_price: lastPrice.value,
63
+ last_price_currency: lastPrice.currency,
64
+ min_ask: minAsk.value,
65
+ min_ask_currency: minAsk.currency,
66
+ max_bid: maxBid.value,
67
+ max_bid_currency: maxBid.currency,
68
+ volume: volume.value,
69
+ volume_currency: volume.currency,
70
+ price_variation_24h: parseFloat(t.price_variation_24h),
71
+ price_variation_7d: parseFloat(t.price_variation_7d),
72
+ };
73
+
50
74
  return {
51
- content: [{ type: "text", text: JSON.stringify(data.ticker, null, 2) }],
75
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
52
76
  };
53
77
  } catch (err) {
54
78
  const msg =
@@ -8,8 +8,10 @@ import type { TradesResponse } from "../types.js";
8
8
  export const toolSchema = {
9
9
  name: "get_trades",
10
10
  description:
11
- "Get recent trade history for a Buda.com market. Each entry contains " +
12
- "[timestamp_ms, amount, price, direction]. Direction is 'buy' or 'sell'.",
11
+ "Returns recent trade history for a Buda.com market as typed objects. Each entry has " +
12
+ "timestamp_ms (integer), amount (float, base currency), price (float, quote currency), " +
13
+ "and direction ('buy' or 'sell'). " +
14
+ "Example: 'What was the last executed price for BTC-CLP and was it a buy or sell?'",
13
15
  inputSchema: {
14
16
  type: "object" as const,
15
17
  properties: {
@@ -73,8 +75,21 @@ export function register(server: McpServer, client: BudaClient, _cache: MemoryCa
73
75
  Object.keys(params).length > 0 ? params : undefined,
74
76
  );
75
77
 
78
+ const t = data.trades;
79
+ const result = {
80
+ timestamp: t.timestamp,
81
+ last_timestamp: t.last_timestamp,
82
+ market_id: t.market_id,
83
+ entries: t.entries.map(([tsMs, amount, price, direction]) => ({
84
+ timestamp_ms: parseInt(tsMs, 10),
85
+ amount: parseFloat(amount),
86
+ price: parseFloat(price),
87
+ direction,
88
+ })),
89
+ };
90
+
76
91
  return {
77
- content: [{ type: "text", text: JSON.stringify(data.trades, null, 2) }],
92
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
78
93
  };
79
94
  } catch (err) {
80
95
  const msg =
@@ -3,13 +3,15 @@ import { z } from "zod";
3
3
  import { BudaClient, BudaApiError } from "../client.js";
4
4
  import { MemoryCache } from "../cache.js";
5
5
  import { validateMarketId } from "../validation.js";
6
+ import { flattenAmount } from "../utils.js";
6
7
  import type { VolumeResponse } from "../types.js";
7
8
 
8
9
  export const toolSchema = {
9
10
  name: "get_market_volume",
10
11
  description:
11
- "Get 24h and 7-day transacted volume for a Buda.com market. " +
12
- "Returns ask (sell) and bid (buy) volumes in the market's base currency.",
12
+ "Returns 24h and 7-day transacted volume for a Buda.com market, split by buy (bid) and sell (ask) side. " +
13
+ "All volume values are floats in the base currency (e.g. BTC for BTC-CLP). " +
14
+ "Example: 'How much Bitcoin was sold on BTC-CLP in the last 24 hours?'",
13
15
  inputSchema: {
14
16
  type: "object" as const,
15
17
  properties: {
@@ -44,8 +46,27 @@ export function register(server: McpServer, client: BudaClient, _cache: MemoryCa
44
46
  const data = await client.get<VolumeResponse>(
45
47
  `/markets/${market_id.toLowerCase()}/volume`,
46
48
  );
49
+
50
+ const v = data.volume;
51
+ const ask24 = flattenAmount(v.ask_volume_24h);
52
+ const ask7d = flattenAmount(v.ask_volume_7d);
53
+ const bid24 = flattenAmount(v.bid_volume_24h);
54
+ const bid7d = flattenAmount(v.bid_volume_7d);
55
+
56
+ const result = {
57
+ market_id: v.market_id,
58
+ ask_volume_24h: ask24.value,
59
+ ask_volume_24h_currency: ask24.currency,
60
+ ask_volume_7d: ask7d.value,
61
+ ask_volume_7d_currency: ask7d.currency,
62
+ bid_volume_24h: bid24.value,
63
+ bid_volume_24h_currency: bid24.currency,
64
+ bid_volume_7d: bid7d.value,
65
+ bid_volume_7d_currency: bid7d.currency,
66
+ };
67
+
47
68
  return {
48
- content: [{ type: "text", text: JSON.stringify(data.volume, null, 2) }],
69
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
49
70
  };
50
71
  } catch (err) {
51
72
  const msg =
package/src/utils.ts ADDED
@@ -0,0 +1,21 @@
1
+ import type { Amount } from "./types.js";
2
+
3
+ /**
4
+ * Flattens a Buda API Amount tuple [value_string, currency] into a typed object.
5
+ * All numeric strings are cast to float via parseFloat.
6
+ */
7
+ export function flattenAmount(amount: Amount): { value: number; currency: string } {
8
+ return { value: parseFloat(amount[0]), currency: amount[1] };
9
+ }
10
+
11
+ /**
12
+ * Returns a liquidity rating based on the bid/ask spread percentage.
13
+ * < 0.3% → "high"
14
+ * 0.3–1% → "medium"
15
+ * > 1% → "low"
16
+ */
17
+ export function getLiquidityRating(spreadPct: number): "high" | "medium" | "low" {
18
+ if (spreadPct < 0.3) return "high";
19
+ if (spreadPct <= 1.0) return "medium";
20
+ return "low";
21
+ }
package/test/unit.ts CHANGED
@@ -9,6 +9,9 @@ import { MemoryCache } from "../src/cache.js";
9
9
  import { validateMarketId } from "../src/validation.js";
10
10
  import { handlePlaceOrder } from "../src/tools/place_order.js";
11
11
  import { handleCancelOrder } from "../src/tools/cancel_order.js";
12
+ import { flattenAmount, getLiquidityRating } from "../src/utils.js";
13
+ import { handleArbitrageOpportunities } from "../src/tools/arbitrage.js";
14
+ import { handleMarketSummary } from "../src/tools/market_summary.js";
12
15
 
13
16
  // ----------------------------------------------------------------
14
17
  // Minimal test harness
@@ -401,6 +404,257 @@ await test("defaults to 1000ms when Retry-After header is absent", async () => {
401
404
  }
402
405
  });
403
406
 
407
+ // ----------------------------------------------------------------
408
+ // f. Numeric flattening — flattenAmount returns typed float, not string
409
+ // ----------------------------------------------------------------
410
+
411
+ section("f. Numeric flattening — flattenAmount");
412
+
413
+ await test("flattenAmount returns a number value, not a string", () => {
414
+ const result = flattenAmount(["65000000", "CLP"]);
415
+ assert(typeof result.value === "number", "value should be a number");
416
+ assertEqual(result.value, 65000000, "value should equal 65000000");
417
+ assertEqual(result.currency, "CLP", "currency should equal CLP");
418
+ });
419
+
420
+ await test("flattenAmount handles decimal strings correctly", () => {
421
+ const result = flattenAmount(["4.99123456", "BTC"]);
422
+ assert(typeof result.value === "number", "value should be a number");
423
+ assertEqual(result.value, 4.99123456, "value should equal 4.99123456");
424
+ assertEqual(result.currency, "BTC", "currency should equal BTC");
425
+ });
426
+
427
+ await test("flattenAmount on zero amount", () => {
428
+ const result = flattenAmount(["0.0", "CLP"]);
429
+ assertEqual(result.value, 0, "zero should parse to 0");
430
+ });
431
+
432
+ await test("flattenAmount value is not a string array", () => {
433
+ const result = flattenAmount(["65000000", "CLP"]);
434
+ assert(!Array.isArray(result), "result should not be an array");
435
+ assert(typeof result.value !== "string", "value should not be a string");
436
+ });
437
+
438
+ // ----------------------------------------------------------------
439
+ // g. get_arbitrage_opportunities — discrepancy calculation
440
+ // ----------------------------------------------------------------
441
+
442
+ section("g. get_arbitrage_opportunities — discrepancy calculation");
443
+
444
+ await test("correctly computes USDC-normalized price discrepancy between CLP and PEN markets", async () => {
445
+ const savedFetch = globalThis.fetch;
446
+
447
+ // BTC-CLP: 65000000 CLP, USDC-CLP: 1000 CLP → BTC in USDC = 65000
448
+ // BTC-PEN: 250000000 PEN, USDC-PEN: 3700 PEN → BTC in USDC ≈ 67567.567...
449
+ // Discrepancy: (67567.567 - 65000) / 65000 * 100 ≈ 3.95%
450
+ const mockTickers = {
451
+ tickers: [
452
+ { market_id: "BTC-CLP", last_price: ["65000000", "CLP"], max_bid: ["64900000", "CLP"], min_ask: ["65100000", "CLP"], volume: ["4.99", "BTC"], price_variation_24h: "0.01", price_variation_7d: "0.05" },
453
+ { market_id: "BTC-PEN", last_price: ["250000000", "PEN"], max_bid: ["249500000", "PEN"], min_ask: ["250500000", "PEN"], volume: ["1.5", "BTC"], price_variation_24h: "0.012", price_variation_7d: "0.04" },
454
+ { market_id: "USDC-CLP", last_price: ["1000", "CLP"], max_bid: ["999", "CLP"], min_ask: ["1001", "CLP"], volume: ["100", "USDC"], price_variation_24h: "0.001", price_variation_7d: "0.002" },
455
+ { market_id: "USDC-PEN", last_price: ["3700", "PEN"], max_bid: ["3695", "PEN"], min_ask: ["3705", "PEN"], volume: ["50", "USDC"], price_variation_24h: "0.001", price_variation_7d: "0.002" },
456
+ ],
457
+ };
458
+
459
+ globalThis.fetch = async (): Promise<Response> => {
460
+ return new Response(JSON.stringify(mockTickers), {
461
+ status: 200,
462
+ headers: { "Content-Type": "application/json" },
463
+ });
464
+ };
465
+
466
+ try {
467
+ const client = new BudaClient("https://www.buda.com/api/v2");
468
+ const cache = new MemoryCache();
469
+ const result = await handleArbitrageOpportunities(
470
+ { base_currency: "BTC", threshold_pct: 0.5 },
471
+ client,
472
+ cache,
473
+ );
474
+
475
+ assert(!result.isError, "should not return an error");
476
+ const parsed = JSON.parse(result.content[0].text) as {
477
+ opportunities: Array<{ market_a: string; market_b: string; discrepancy_pct: number }>;
478
+ markets_analyzed: Array<{ market_id: string; price_usdc: number }>;
479
+ };
480
+
481
+ assertEqual(parsed.markets_analyzed.length, 2, "should have 2 markets analyzed");
482
+ assertEqual(parsed.opportunities.length, 1, "should have exactly 1 opportunity");
483
+
484
+ const opp = parsed.opportunities[0];
485
+ const expectedDiscrepancy = ((67567.5676 - 65000) / 65000) * 100;
486
+ assert(
487
+ Math.abs(opp.discrepancy_pct - expectedDiscrepancy) < 0.01,
488
+ `discrepancy_pct should be ≈${expectedDiscrepancy.toFixed(2)}%, got ${opp.discrepancy_pct}`,
489
+ );
490
+ } finally {
491
+ globalThis.fetch = savedFetch;
492
+ }
493
+ });
494
+
495
+ await test("threshold filtering excludes opportunities below threshold", async () => {
496
+ const savedFetch = globalThis.fetch;
497
+
498
+ // ~3.95% discrepancy between CLP and PEN — threshold 5% should exclude it
499
+ const mockTickers = {
500
+ tickers: [
501
+ { market_id: "BTC-CLP", last_price: ["65000000", "CLP"], max_bid: ["64900000", "CLP"], min_ask: ["65100000", "CLP"], volume: ["4.99", "BTC"], price_variation_24h: "0.01", price_variation_7d: "0.05" },
502
+ { market_id: "BTC-PEN", last_price: ["250000000", "PEN"], max_bid: ["249500000", "PEN"], min_ask: ["250500000", "PEN"], volume: ["1.5", "BTC"], price_variation_24h: "0.012", price_variation_7d: "0.04" },
503
+ { market_id: "USDC-CLP", last_price: ["1000", "CLP"], max_bid: ["999", "CLP"], min_ask: ["1001", "CLP"], volume: ["100", "USDC"], price_variation_24h: "0.001", price_variation_7d: "0.002" },
504
+ { market_id: "USDC-PEN", last_price: ["3700", "PEN"], max_bid: ["3695", "PEN"], min_ask: ["3705", "PEN"], volume: ["50", "USDC"], price_variation_24h: "0.001", price_variation_7d: "0.002" },
505
+ ],
506
+ };
507
+
508
+ globalThis.fetch = async (): Promise<Response> => {
509
+ return new Response(JSON.stringify(mockTickers), {
510
+ status: 200,
511
+ headers: { "Content-Type": "application/json" },
512
+ });
513
+ };
514
+
515
+ try {
516
+ const client = new BudaClient("https://www.buda.com/api/v2");
517
+ const cache = new MemoryCache();
518
+ const result = await handleArbitrageOpportunities(
519
+ { base_currency: "BTC", threshold_pct: 5.0 },
520
+ client,
521
+ cache,
522
+ );
523
+
524
+ assert(!result.isError, "should not return an error");
525
+ const parsed = JSON.parse(result.content[0].text) as {
526
+ opportunities: Array<unknown>;
527
+ };
528
+ assertEqual(parsed.opportunities.length, 0, "threshold 5% should exclude the ~3.95% discrepancy");
529
+ } finally {
530
+ globalThis.fetch = savedFetch;
531
+ }
532
+ });
533
+
534
+ await test("returns error when fewer than 2 markets are found", async () => {
535
+ const savedFetch = globalThis.fetch;
536
+
537
+ // Only CLP market available, no PEN or COP
538
+ const mockTickers = {
539
+ tickers: [
540
+ { market_id: "BTC-CLP", last_price: ["65000000", "CLP"], max_bid: ["64900000", "CLP"], min_ask: ["65100000", "CLP"], volume: ["4.99", "BTC"], price_variation_24h: "0.01", price_variation_7d: "0.05" },
541
+ { market_id: "USDC-CLP", last_price: ["1000", "CLP"], max_bid: ["999", "CLP"], min_ask: ["1001", "CLP"], volume: ["100", "USDC"], price_variation_24h: "0.001", price_variation_7d: "0.002" },
542
+ ],
543
+ };
544
+
545
+ globalThis.fetch = async (): Promise<Response> => {
546
+ return new Response(JSON.stringify(mockTickers), {
547
+ status: 200,
548
+ headers: { "Content-Type": "application/json" },
549
+ });
550
+ };
551
+
552
+ try {
553
+ const client = new BudaClient("https://www.buda.com/api/v2");
554
+ const cache = new MemoryCache();
555
+ const result = await handleArbitrageOpportunities(
556
+ { base_currency: "BTC", threshold_pct: 0.5 },
557
+ client,
558
+ cache,
559
+ );
560
+ assert(result.isError === true, "should return isError when not enough markets");
561
+ } finally {
562
+ globalThis.fetch = savedFetch;
563
+ }
564
+ });
565
+
566
+ // ----------------------------------------------------------------
567
+ // h. get_market_summary — liquidity_rating thresholds
568
+ // ----------------------------------------------------------------
569
+
570
+ section("h. get_market_summary — liquidity_rating thresholds");
571
+
572
+ await test("getLiquidityRating: spread < 0.3% → 'high'", () => {
573
+ assertEqual(getLiquidityRating(0), "high", "0% spread should be high");
574
+ assertEqual(getLiquidityRating(0.1), "high", "0.1% spread should be high");
575
+ assertEqual(getLiquidityRating(0.29), "high", "0.29% spread should be high");
576
+ });
577
+
578
+ await test("getLiquidityRating: spread at 0.3% boundary → 'medium'", () => {
579
+ assertEqual(getLiquidityRating(0.3), "medium", "exactly 0.3% spread should be medium");
580
+ });
581
+
582
+ await test("getLiquidityRating: spread 0.3–1% → 'medium'", () => {
583
+ assertEqual(getLiquidityRating(0.5), "medium", "0.5% spread should be medium");
584
+ assertEqual(getLiquidityRating(1.0), "medium", "exactly 1.0% spread should be medium");
585
+ });
586
+
587
+ await test("getLiquidityRating: spread > 1% → 'low'", () => {
588
+ assertEqual(getLiquidityRating(1.01), "low", "1.01% spread should be low");
589
+ assertEqual(getLiquidityRating(5.0), "low", "5% spread should be low");
590
+ });
591
+
592
+ await test("handleMarketSummary returns correct liquidity_rating from mocked API", async () => {
593
+ const savedFetch = globalThis.fetch;
594
+ let callCount = 0;
595
+
596
+ // Ticker: bid 64870, ask 65000 → spread = 130 / 65000 * 100 = 0.2% → "high"
597
+ const mockTicker = {
598
+ ticker: {
599
+ market_id: "BTC-CLP",
600
+ last_price: ["65000", "CLP"],
601
+ max_bid: ["64870", "CLP"],
602
+ min_ask: ["65000", "CLP"],
603
+ volume: ["4.99", "BTC"],
604
+ price_variation_24h: "0.012",
605
+ price_variation_7d: "0.05",
606
+ },
607
+ };
608
+ const mockVolume = {
609
+ volume: {
610
+ market_id: "BTC-CLP",
611
+ ask_volume_24h: ["10.5", "BTC"],
612
+ ask_volume_7d: ["72.1", "BTC"],
613
+ bid_volume_24h: ["9.8", "BTC"],
614
+ bid_volume_7d: ["68.3", "BTC"],
615
+ },
616
+ };
617
+
618
+ globalThis.fetch = async (url: string | URL): Promise<Response> => {
619
+ callCount++;
620
+ const urlStr = url.toString();
621
+ if (urlStr.includes("/volume")) {
622
+ return new Response(JSON.stringify(mockVolume), { status: 200, headers: { "Content-Type": "application/json" } });
623
+ }
624
+ return new Response(JSON.stringify(mockTicker), { status: 200, headers: { "Content-Type": "application/json" } });
625
+ };
626
+
627
+ try {
628
+ const client = new BudaClient("https://www.buda.com/api/v2");
629
+ const cache = new MemoryCache();
630
+ const result = await handleMarketSummary({ market_id: "BTC-CLP" }, client, cache);
631
+
632
+ assert(!result.isError, "should not return an error");
633
+ const parsed = JSON.parse(result.content[0].text) as {
634
+ market_id: string;
635
+ last_price: number;
636
+ last_price_currency: string;
637
+ bid: number;
638
+ ask: number;
639
+ spread_pct: number;
640
+ volume_24h: number;
641
+ liquidity_rating: string;
642
+ };
643
+
644
+ assertEqual(parsed.market_id, "BTC-CLP", "market_id should match");
645
+ assertEqual(parsed.last_price, 65000, "last_price should be a number");
646
+ assert(typeof parsed.last_price === "number", "last_price should be a number type");
647
+ assertEqual(parsed.last_price_currency, "CLP", "currency should be CLP");
648
+ assertEqual(parsed.bid, 64870, "bid should be a float");
649
+ assertEqual(parsed.ask, 65000, "ask should be a float");
650
+ // spread = (65000 - 64870) / 65000 * 100 = 130/65000*100 = 0.2%
651
+ assertEqual(parsed.liquidity_rating, "high", "spread 0.2% should yield 'high' liquidity");
652
+ assertEqual(parsed.volume_24h, 10.5, "volume_24h should be a float");
653
+ } finally {
654
+ globalThis.fetch = savedFetch;
655
+ }
656
+ });
657
+
404
658
  // ----------------------------------------------------------------
405
659
  // Summary
406
660
  // ----------------------------------------------------------------