@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.
- package/.cursor/rules/release-workflow.mdc +54 -0
- package/CHANGELOG.md +37 -0
- package/PUBLISH_CHECKLIST.md +62 -53
- package/dist/http.js +22 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +20 -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/compare_markets.d.ts.map +1 -1
- package/dist/tools/compare_markets.js +11 -10
- 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.js +14 -14
- package/dist/tools/spread.d.ts.map +1 -1
- package/dist/tools/spread.js +10 -8
- 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/utils.d.ts +17 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +21 -0
- package/marketplace/README.md +1 -1
- package/marketplace/claude-listing.md +26 -14
- package/marketplace/gemini-tools.json +41 -9
- package/marketplace/openapi.yaml +335 -119
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/http.ts +27 -0
- package/src/index.ts +25 -0
- package/src/tools/arbitrage.ts +202 -0
- package/src/tools/balances.ts +27 -4
- package/src/tools/compare_markets.ts +11 -10
- 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 +17 -17
- package/src/tools/spread.ts +10 -8
- package/src/tools/ticker.ts +27 -3
- package/src/tools/trades.ts +18 -3
- package/src/tools/volume.ts +24 -3
- package/src/utils.ts +21 -0
- 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:
|
|
17
|
-
high:
|
|
18
|
-
low:
|
|
19
|
-
close:
|
|
20
|
-
volume:
|
|
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
|
|
30
|
-
"Candle timestamps are UTC bucket boundaries
|
|
31
|
-
"
|
|
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:
|
|
126
|
-
high:
|
|
127
|
-
low:
|
|
128
|
-
close:
|
|
129
|
-
volume:
|
|
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 >
|
|
135
|
-
if (p <
|
|
136
|
-
candle.close =
|
|
137
|
-
candle.volume =
|
|
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
|
}
|
package/src/tools/spread.ts
CHANGED
|
@@ -8,8 +8,10 @@ import type { TickerResponse } from "../types.js";
|
|
|
8
8
|
export const toolSchema = {
|
|
9
9
|
name: "get_spread",
|
|
10
10
|
description:
|
|
11
|
-
"
|
|
12
|
-
"
|
|
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
|
|
75
|
-
best_ask: ask
|
|
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 {
|
package/src/tools/ticker.ts
CHANGED
|
@@ -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
|
-
"
|
|
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(
|
|
75
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
52
76
|
};
|
|
53
77
|
} catch (err) {
|
|
54
78
|
const msg =
|
package/src/tools/trades.ts
CHANGED
|
@@ -8,8 +8,10 @@ import type { TradesResponse } from "../types.js";
|
|
|
8
8
|
export const toolSchema = {
|
|
9
9
|
name: "get_trades",
|
|
10
10
|
description:
|
|
11
|
-
"
|
|
12
|
-
"
|
|
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(
|
|
92
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
78
93
|
};
|
|
79
94
|
} catch (err) {
|
|
80
95
|
const msg =
|
package/src/tools/volume.ts
CHANGED
|
@@ -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
|
-
"
|
|
12
|
-
"
|
|
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(
|
|
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
|
// ----------------------------------------------------------------
|