@guiie/buda-mcp 1.4.0 → 1.4.2
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 +20 -0
- package/dist/tools/price_history.js +4 -4
- package/dist/tools/simulate_order.js +1 -1
- package/dist/tools/technical_indicators.d.ts +1 -1
- package/dist/tools/technical_indicators.d.ts.map +1 -1
- package/dist/tools/technical_indicators.js +6 -5
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +3 -0
- package/marketplace/gemini-tools.json +87 -0
- package/marketplace/openapi.yaml +375 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/tools/price_history.ts +4 -4
- package/src/tools/simulate_order.ts +1 -1
- package/src/tools/technical_indicators.ts +7 -6
- package/src/utils.ts +6 -3
- package/test/run-all.ts +225 -0
- package/test/unit.ts +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ This project uses [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## [1.4.2] – 2026-04-11
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Shorter candle periods** (`5m`, `15m`, `30m`) now supported in both `get_price_history` and `get_technical_indicators`. Previously only `1h`, `4h`, `1d` were available.
|
|
15
|
+
- **Lowered `MIN_CANDLES`** in `get_technical_indicators` from 50 to 20, matching the actual minimum required by the algorithms (RSI-14, MACD-26, BB-20). Individual indicators that still lack enough data return `null`.
|
|
16
|
+
- **Integration tests** now cover the full `get_technical_indicators` indicators branch using `5m` period (42 live candles from BTC-CLP). Previously only the `insufficient_data` branch was tested live.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## [1.4.1] – 2026-04-11
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **`simulate_order`**: `taker_fee` returned by Buda API is already expressed as a percentage (`0.8` = 0.8%), not a decimal. Dividing by 100 before use gives correct fee calculations. Previously this caused fee_amount and total_cost to be ~100× too large.
|
|
25
|
+
- Integration test (`test/run-all.ts`): added live checks for all 5 v1.4.0 tools; fixed field name `candles_available` (was `candles_used`).
|
|
26
|
+
- Unit test mocks: updated `taker_fee` mock values from `"0.008"`/`"0.005"` to `"0.8"`/`"0.5"` to match the real Buda API format.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
10
30
|
## [1.4.0] – 2026-04-11
|
|
11
31
|
|
|
12
32
|
### Added
|
|
@@ -7,7 +7,7 @@ export const toolSchema = {
|
|
|
7
7
|
description: "IMPORTANT: Candles are aggregated client-side from raw trades (Buda has no native candlestick " +
|
|
8
8
|
"endpoint) — fetching more trades via the 'limit' parameter gives deeper history but slower " +
|
|
9
9
|
"responses. Returns OHLCV candles (open/high/low/close as floats in quote currency; volume as float " +
|
|
10
|
-
"in base currency) for periods 1h, 4h, or 1d. Candle timestamps are UTC bucket boundaries. " +
|
|
10
|
+
"in base currency) for periods 5m, 15m, 30m, 1h, 4h, or 1d. Candle timestamps are UTC bucket boundaries. " +
|
|
11
11
|
"Example: 'Show me the hourly BTC-CLP price chart for the past 24 hours.'",
|
|
12
12
|
inputSchema: {
|
|
13
13
|
type: "object",
|
|
@@ -18,7 +18,7 @@ export const toolSchema = {
|
|
|
18
18
|
},
|
|
19
19
|
period: {
|
|
20
20
|
type: "string",
|
|
21
|
-
description: "Candle period: '
|
|
21
|
+
description: "Candle period: '5m', '15m', '30m', '1h', '4h', or '1d'. Default: '1h'.",
|
|
22
22
|
},
|
|
23
23
|
limit: {
|
|
24
24
|
type: "number",
|
|
@@ -35,9 +35,9 @@ export function register(server, client, _cache) {
|
|
|
35
35
|
.string()
|
|
36
36
|
.describe("Market ID (e.g. 'BTC-CLP', 'ETH-BTC')."),
|
|
37
37
|
period: z
|
|
38
|
-
.enum(["1h", "4h", "1d"])
|
|
38
|
+
.enum(["5m", "15m", "30m", "1h", "4h", "1d"])
|
|
39
39
|
.default("1h")
|
|
40
|
-
.describe("Candle period: '
|
|
40
|
+
.describe("Candle period: '5m', '15m', '30m', '1h', '4h', or '1d'. Default: '1h'."),
|
|
41
41
|
limit: z
|
|
42
42
|
.number()
|
|
43
43
|
.int()
|
|
@@ -67,7 +67,7 @@ export async function handleSimulateOrder(args, client, cache) {
|
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
69
|
const mid = (minAsk + maxBid) / 2;
|
|
70
|
-
const takerFeeRate = parseFloat(market.taker_fee);
|
|
70
|
+
const takerFeeRate = parseFloat(market.taker_fee) / 100;
|
|
71
71
|
const orderTypeAssumed = price !== undefined ? "limit" : "market";
|
|
72
72
|
let estimatedFillPrice;
|
|
73
73
|
if (orderTypeAssumed === "market") {
|
|
@@ -24,7 +24,7 @@ export declare const toolSchema: {
|
|
|
24
24
|
};
|
|
25
25
|
type TechnicalIndicatorsArgs = {
|
|
26
26
|
market_id: string;
|
|
27
|
-
period: "1h" | "4h" | "1d";
|
|
27
|
+
period: "5m" | "15m" | "30m" | "1h" | "4h" | "1d";
|
|
28
28
|
limit?: number;
|
|
29
29
|
};
|
|
30
30
|
export declare function handleTechnicalIndicators(args: TechnicalIndicatorsArgs, client: BudaClient): Promise<{
|
|
@@ -1 +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;;;;;;;;;;;;;;;;;;;;;
|
|
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;;;;;;;;;;;;;;;;;;;;;CA8BtB,CAAC;AAsGF,KAAK,uBAAuB,GAAG;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,IAAI,GAAG,KAAK,GAAG,KAAK,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAClD,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"}
|
|
@@ -6,9 +6,10 @@ export const toolSchema = {
|
|
|
6
6
|
name: "get_technical_indicators",
|
|
7
7
|
description: "Computes RSI (14), MACD (12/26/9), Bollinger Bands (20, 2σ), SMA 20, and SMA 50 " +
|
|
8
8
|
"from Buda trade history — no external data or libraries. " +
|
|
9
|
+
"Supports periods: 5m, 15m, 30m, 1h, 4h, 1d. Use shorter periods (5m/15m) for intraday analysis. " +
|
|
9
10
|
"Uses at least 500 trades for reliable results (set limit=1000 for maximum depth). " +
|
|
10
11
|
"Returns latest indicator values and signal interpretations (overbought/oversold, crossover, band position). " +
|
|
11
|
-
"If fewer than
|
|
12
|
+
"If fewer than 20 candles are available after aggregation, returns a structured warning instead. " +
|
|
12
13
|
"Example: 'Is BTC-CLP RSI overbought on the 4-hour chart?'",
|
|
13
14
|
inputSchema: {
|
|
14
15
|
type: "object",
|
|
@@ -19,7 +20,7 @@ export const toolSchema = {
|
|
|
19
20
|
},
|
|
20
21
|
period: {
|
|
21
22
|
type: "string",
|
|
22
|
-
description: "Candle period: '1h', '4h', or '1d'. Default: '1h'.",
|
|
23
|
+
description: "Candle period: '5m', '15m', '30m', '1h', '4h', or '1d'. Default: '1h'.",
|
|
23
24
|
},
|
|
24
25
|
limit: {
|
|
25
26
|
type: "number",
|
|
@@ -108,7 +109,7 @@ function bollingerBands(closes, period = 20, stdMult = 2) {
|
|
|
108
109
|
};
|
|
109
110
|
}
|
|
110
111
|
// ---- Tool handler ----
|
|
111
|
-
const MIN_CANDLES =
|
|
112
|
+
const MIN_CANDLES = 20;
|
|
112
113
|
export async function handleTechnicalIndicators(args, client) {
|
|
113
114
|
const { market_id, period, limit } = args;
|
|
114
115
|
const validationError = validateMarketId(market_id);
|
|
@@ -207,9 +208,9 @@ export function register(server, client) {
|
|
|
207
208
|
.string()
|
|
208
209
|
.describe("Market ID (e.g. 'BTC-CLP', 'ETH-BTC')."),
|
|
209
210
|
period: z
|
|
210
|
-
.enum(["1h", "4h", "1d"])
|
|
211
|
+
.enum(["5m", "15m", "30m", "1h", "4h", "1d"])
|
|
211
212
|
.default("1h")
|
|
212
|
-
.describe("Candle period: '1h', '4h', or '1d'. Default: '1h'."),
|
|
213
|
+
.describe("Candle period: '5m', '15m', '30m', '1h', '4h', or '1d'. Default: '1h'."),
|
|
213
214
|
limit: z
|
|
214
215
|
.number()
|
|
215
216
|
.int()
|
package/dist/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEtD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAEjF;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAI/E;
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEtD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAEjF;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAI/E;AAWD;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,EAC3C,MAAM,EAAE,MAAM,GACb,WAAW,EAAE,CAoCf"}
|
package/dist/utils.js
CHANGED
|
@@ -306,6 +306,93 @@
|
|
|
306
306
|
},
|
|
307
307
|
"required": ["market_id"]
|
|
308
308
|
}
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
"name": "get_balances",
|
|
312
|
+
"description": "Returns all currency balances for the authenticated Buda.com account as flat typed objects. Each currency entry includes total amount, available amount (not frozen), frozen amount, and pending withdrawal amount — all as floats with separate _currency fields. Requires BUDA_API_KEY and BUDA_API_SECRET. Auth-gated. Example: 'How much BTC do I have available to trade right now?'",
|
|
313
|
+
"parameters": {
|
|
314
|
+
"type": "OBJECT",
|
|
315
|
+
"properties": {},
|
|
316
|
+
"required": []
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
"name": "get_orders",
|
|
321
|
+
"description": "Returns orders for a given Buda.com market as flat typed objects. All monetary amounts are floats with separate _currency fields (e.g. amount + amount_currency). Filterable by state: pending, active, traded, canceled, canceled_and_traded. Supports pagination via per and page. Requires BUDA_API_KEY and BUDA_API_SECRET. Auth-gated. Example: 'Show my open limit orders on BTC-CLP.'",
|
|
322
|
+
"parameters": {
|
|
323
|
+
"type": "OBJECT",
|
|
324
|
+
"properties": {
|
|
325
|
+
"market_id": {
|
|
326
|
+
"type": "STRING",
|
|
327
|
+
"description": "Market identifier (e.g. 'BTC-CLP', 'ETH-BTC'). Case-insensitive."
|
|
328
|
+
},
|
|
329
|
+
"state": {
|
|
330
|
+
"type": "STRING",
|
|
331
|
+
"description": "Filter by order state: 'pending', 'active', 'traded', 'canceled', 'canceled_and_traded'. Omit to return all orders."
|
|
332
|
+
},
|
|
333
|
+
"per": {
|
|
334
|
+
"type": "INTEGER",
|
|
335
|
+
"description": "Results per page (default: 20, max: 300)."
|
|
336
|
+
},
|
|
337
|
+
"page": {
|
|
338
|
+
"type": "INTEGER",
|
|
339
|
+
"description": "Page number (default: 1)."
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
"required": ["market_id"]
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
"name": "place_order",
|
|
347
|
+
"description": "Places a limit or market order on Buda.com. IMPORTANT: Requires confirmation_token='CONFIRM' to execute — any other value rejects without placing an order, preventing accidental execution. Requires BUDA_API_KEY and BUDA_API_SECRET. Auth-gated. WARNING: Only use on locally-run instances — never on a publicly exposed server.",
|
|
348
|
+
"parameters": {
|
|
349
|
+
"type": "OBJECT",
|
|
350
|
+
"properties": {
|
|
351
|
+
"market_id": {
|
|
352
|
+
"type": "STRING",
|
|
353
|
+
"description": "Market identifier (e.g. 'BTC-CLP', 'ETH-BTC'). Case-insensitive."
|
|
354
|
+
},
|
|
355
|
+
"type": {
|
|
356
|
+
"type": "STRING",
|
|
357
|
+
"description": "Order side: 'Bid' to buy, 'Ask' to sell."
|
|
358
|
+
},
|
|
359
|
+
"price_type": {
|
|
360
|
+
"type": "STRING",
|
|
361
|
+
"description": "Order type: 'limit' places at a specific price, 'market' executes immediately at best available price."
|
|
362
|
+
},
|
|
363
|
+
"amount": {
|
|
364
|
+
"type": "NUMBER",
|
|
365
|
+
"description": "Order size in the market's base currency (e.g. BTC amount for BTC-CLP)."
|
|
366
|
+
},
|
|
367
|
+
"limit_price": {
|
|
368
|
+
"type": "NUMBER",
|
|
369
|
+
"description": "Limit price in quote currency. Required when price_type is 'limit'. For Bid orders: highest price you will pay. For Ask orders: lowest price you will accept."
|
|
370
|
+
},
|
|
371
|
+
"confirmation_token": {
|
|
372
|
+
"type": "STRING",
|
|
373
|
+
"description": "Safety confirmation. Must equal exactly 'CONFIRM' (case-sensitive) to place the order. Any other value rejects the request."
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
"required": ["market_id", "type", "price_type", "amount", "confirmation_token"]
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
"name": "cancel_order",
|
|
381
|
+
"description": "Cancels an open order by numeric ID on Buda.com. IMPORTANT: Requires confirmation_token='CONFIRM' to execute — any other value rejects without cancelling. Requires BUDA_API_KEY and BUDA_API_SECRET. Auth-gated.",
|
|
382
|
+
"parameters": {
|
|
383
|
+
"type": "OBJECT",
|
|
384
|
+
"properties": {
|
|
385
|
+
"order_id": {
|
|
386
|
+
"type": "INTEGER",
|
|
387
|
+
"description": "The numeric ID of the order to cancel."
|
|
388
|
+
},
|
|
389
|
+
"confirmation_token": {
|
|
390
|
+
"type": "STRING",
|
|
391
|
+
"description": "Safety confirmation. Must equal exactly 'CONFIRM' (case-sensitive) to cancel the order. Any other value rejects the request."
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
"required": ["order_id", "confirmation_token"]
|
|
395
|
+
}
|
|
309
396
|
}
|
|
310
397
|
]
|
|
311
398
|
}
|
package/marketplace/openapi.yaml
CHANGED
|
@@ -14,7 +14,7 @@ info:
|
|
|
14
14
|
stdio server. Deploy locally with mcp-proxy:
|
|
15
15
|
mcp-proxy --port 8000 -- npx -y @guiie/buda-mcp
|
|
16
16
|
Or point `servers[0].url` at your hosted instance.
|
|
17
|
-
version: 1.
|
|
17
|
+
version: 1.4.1
|
|
18
18
|
contact:
|
|
19
19
|
url: https://github.com/gtorreal/buda-mcp
|
|
20
20
|
|
|
@@ -350,6 +350,189 @@ paths:
|
|
|
350
350
|
"404":
|
|
351
351
|
$ref: "#/components/responses/NotFound"
|
|
352
352
|
|
|
353
|
+
/simulate_order:
|
|
354
|
+
get:
|
|
355
|
+
operationId: simulateOrder
|
|
356
|
+
summary: Simulate a buy or sell order without placing it
|
|
357
|
+
description: |
|
|
358
|
+
Simulates a buy or sell order using live ticker data — no order is ever placed.
|
|
359
|
+
Returns estimated fill price, fee amount, total cost, and slippage vs mid-price.
|
|
360
|
+
Omit 'price' for a market order simulation; supply 'price' for a limit order simulation.
|
|
361
|
+
All outputs include simulation: true.
|
|
362
|
+
Example: 'How much would it cost to buy 0.01 BTC on BTC-CLP right now?'
|
|
363
|
+
parameters:
|
|
364
|
+
- name: market_id
|
|
365
|
+
in: query
|
|
366
|
+
required: true
|
|
367
|
+
description: Market identifier (e.g. "BTC-CLP", "ETH-BTC"). Case-insensitive.
|
|
368
|
+
schema:
|
|
369
|
+
type: string
|
|
370
|
+
example: BTC-CLP
|
|
371
|
+
- name: side
|
|
372
|
+
in: query
|
|
373
|
+
required: true
|
|
374
|
+
description: Order side — "buy" or "sell".
|
|
375
|
+
schema:
|
|
376
|
+
type: string
|
|
377
|
+
enum: [buy, sell]
|
|
378
|
+
- name: amount
|
|
379
|
+
in: query
|
|
380
|
+
required: true
|
|
381
|
+
description: Order size in base currency (e.g. BTC for BTC-CLP).
|
|
382
|
+
schema:
|
|
383
|
+
type: number
|
|
384
|
+
minimum: 0
|
|
385
|
+
example: 0.01
|
|
386
|
+
- name: price
|
|
387
|
+
in: query
|
|
388
|
+
required: false
|
|
389
|
+
description: Limit price in quote currency. Omit for a market order simulation.
|
|
390
|
+
schema:
|
|
391
|
+
type: number
|
|
392
|
+
minimum: 0
|
|
393
|
+
example: 80000000
|
|
394
|
+
responses:
|
|
395
|
+
"200":
|
|
396
|
+
description: Order simulation result
|
|
397
|
+
content:
|
|
398
|
+
application/json:
|
|
399
|
+
schema:
|
|
400
|
+
$ref: "#/components/schemas/SimulateOrderResponse"
|
|
401
|
+
"404":
|
|
402
|
+
$ref: "#/components/responses/NotFound"
|
|
403
|
+
|
|
404
|
+
/calculate_position_size:
|
|
405
|
+
get:
|
|
406
|
+
operationId: calculatePositionSize
|
|
407
|
+
summary: Calculate position size from capital and risk parameters
|
|
408
|
+
description: |
|
|
409
|
+
Calculates position size based on capital, risk tolerance, entry price, and stop-loss.
|
|
410
|
+
Determines how many units to buy or sell so that a stop-loss hit costs exactly risk_pct% of capital.
|
|
411
|
+
Fully client-side — no API call is made.
|
|
412
|
+
Example: 'How many BTC can I buy with 1,000,000 CLP, risking 2%, entry 80,000,000 CLP, stop at 78,000,000 CLP?'
|
|
413
|
+
parameters:
|
|
414
|
+
- name: market_id
|
|
415
|
+
in: query
|
|
416
|
+
required: true
|
|
417
|
+
description: Market identifier (e.g. "BTC-CLP"). Used to derive base and quote currencies.
|
|
418
|
+
schema:
|
|
419
|
+
type: string
|
|
420
|
+
example: BTC-CLP
|
|
421
|
+
- name: capital
|
|
422
|
+
in: query
|
|
423
|
+
required: true
|
|
424
|
+
description: Total available capital in the quote currency (e.g. CLP for BTC-CLP).
|
|
425
|
+
schema:
|
|
426
|
+
type: number
|
|
427
|
+
minimum: 0
|
|
428
|
+
example: 1000000
|
|
429
|
+
- name: risk_pct
|
|
430
|
+
in: query
|
|
431
|
+
required: true
|
|
432
|
+
description: Percentage of capital to risk on this trade (0.1–10, e.g. 2 means 2%).
|
|
433
|
+
schema:
|
|
434
|
+
type: number
|
|
435
|
+
minimum: 0.1
|
|
436
|
+
maximum: 10
|
|
437
|
+
example: 2
|
|
438
|
+
- name: entry_price
|
|
439
|
+
in: query
|
|
440
|
+
required: true
|
|
441
|
+
description: Planned entry price in quote currency.
|
|
442
|
+
schema:
|
|
443
|
+
type: number
|
|
444
|
+
minimum: 0
|
|
445
|
+
example: 80000000
|
|
446
|
+
- name: stop_loss_price
|
|
447
|
+
in: query
|
|
448
|
+
required: true
|
|
449
|
+
description: Stop-loss price in quote currency. Must differ from entry_price.
|
|
450
|
+
schema:
|
|
451
|
+
type: number
|
|
452
|
+
minimum: 0
|
|
453
|
+
example: 78000000
|
|
454
|
+
responses:
|
|
455
|
+
"200":
|
|
456
|
+
description: Position sizing result
|
|
457
|
+
content:
|
|
458
|
+
application/json:
|
|
459
|
+
schema:
|
|
460
|
+
$ref: "#/components/schemas/PositionSizeResponse"
|
|
461
|
+
"404":
|
|
462
|
+
$ref: "#/components/responses/NotFound"
|
|
463
|
+
|
|
464
|
+
/get_market_sentiment:
|
|
465
|
+
get:
|
|
466
|
+
operationId: getMarketSentiment
|
|
467
|
+
summary: Composite sentiment score for a market
|
|
468
|
+
description: |
|
|
469
|
+
Computes a composite sentiment score (−100 to +100) based on 24h price variation (40%),
|
|
470
|
+
volume vs 7-day average (35%), and bid/ask spread vs baseline (25%).
|
|
471
|
+
Returns a score, a label (bearish/neutral/bullish), and a full component breakdown.
|
|
472
|
+
Example: 'Is the BTC-CLP market currently bullish or bearish?'
|
|
473
|
+
parameters:
|
|
474
|
+
- name: market_id
|
|
475
|
+
in: query
|
|
476
|
+
required: true
|
|
477
|
+
description: Market identifier (e.g. "BTC-CLP", "ETH-BTC"). Case-insensitive.
|
|
478
|
+
schema:
|
|
479
|
+
type: string
|
|
480
|
+
example: BTC-CLP
|
|
481
|
+
responses:
|
|
482
|
+
"200":
|
|
483
|
+
description: Market sentiment result
|
|
484
|
+
content:
|
|
485
|
+
application/json:
|
|
486
|
+
schema:
|
|
487
|
+
$ref: "#/components/schemas/MarketSentimentResponse"
|
|
488
|
+
"404":
|
|
489
|
+
$ref: "#/components/responses/NotFound"
|
|
490
|
+
|
|
491
|
+
/get_technical_indicators:
|
|
492
|
+
get:
|
|
493
|
+
operationId: getTechnicalIndicators
|
|
494
|
+
summary: RSI, MACD, Bollinger Bands, SMA20, SMA50 from trade history
|
|
495
|
+
description: |
|
|
496
|
+
Computes RSI (14), MACD (12/26/9), Bollinger Bands (20, 2σ), SMA 20, and SMA 50
|
|
497
|
+
from Buda trade history — no external data or libraries required.
|
|
498
|
+
Uses at least 500 trades for reliable results (set limit=1000 for maximum depth).
|
|
499
|
+
Returns latest indicator values and signal interpretations (overbought/oversold, crossover, band position).
|
|
500
|
+
If fewer than 50 candles are available after aggregation, returns a structured warning instead.
|
|
501
|
+
Example: 'Is BTC-CLP RSI overbought on the 4-hour chart?'
|
|
502
|
+
parameters:
|
|
503
|
+
- name: market_id
|
|
504
|
+
in: query
|
|
505
|
+
required: true
|
|
506
|
+
description: Market identifier (e.g. "BTC-CLP", "ETH-BTC"). Case-insensitive.
|
|
507
|
+
schema:
|
|
508
|
+
type: string
|
|
509
|
+
example: BTC-CLP
|
|
510
|
+
- name: period
|
|
511
|
+
in: query
|
|
512
|
+
required: false
|
|
513
|
+
description: Candle period. One of "1h", "4h", "1d". Default is "1h".
|
|
514
|
+
schema:
|
|
515
|
+
type: string
|
|
516
|
+
enum: ["1h", "4h", "1d"]
|
|
517
|
+
default: "1h"
|
|
518
|
+
- name: limit
|
|
519
|
+
in: query
|
|
520
|
+
required: false
|
|
521
|
+
description: Number of raw trades to fetch (default 500, max 1000). More trades = more candles = more reliable indicators.
|
|
522
|
+
schema:
|
|
523
|
+
type: integer
|
|
524
|
+
minimum: 500
|
|
525
|
+
maximum: 1000
|
|
526
|
+
responses:
|
|
527
|
+
"200":
|
|
528
|
+
description: Technical indicators result
|
|
529
|
+
content:
|
|
530
|
+
application/json:
|
|
531
|
+
schema:
|
|
532
|
+
$ref: "#/components/schemas/TechnicalIndicatorsResponse"
|
|
533
|
+
"404":
|
|
534
|
+
$ref: "#/components/responses/NotFound"
|
|
535
|
+
|
|
353
536
|
components:
|
|
354
537
|
schemas:
|
|
355
538
|
Market:
|
|
@@ -747,6 +930,197 @@ components:
|
|
|
747
930
|
type: string
|
|
748
931
|
example: "Buda taker fee is 0.8% per leg. A round-trip arbitrage costs approximately 1.6% in fees."
|
|
749
932
|
|
|
933
|
+
SimulateOrderResponse:
|
|
934
|
+
type: object
|
|
935
|
+
properties:
|
|
936
|
+
simulation:
|
|
937
|
+
type: boolean
|
|
938
|
+
example: true
|
|
939
|
+
market_id:
|
|
940
|
+
type: string
|
|
941
|
+
example: BTC-CLP
|
|
942
|
+
side:
|
|
943
|
+
type: string
|
|
944
|
+
enum: [buy, sell]
|
|
945
|
+
amount:
|
|
946
|
+
type: number
|
|
947
|
+
example: 0.01
|
|
948
|
+
order_type_assumed:
|
|
949
|
+
type: string
|
|
950
|
+
enum: [market, limit]
|
|
951
|
+
estimated_fill_price:
|
|
952
|
+
type: number
|
|
953
|
+
example: 80000000
|
|
954
|
+
price_currency:
|
|
955
|
+
type: string
|
|
956
|
+
example: CLP
|
|
957
|
+
fee_amount:
|
|
958
|
+
type: number
|
|
959
|
+
example: 6400
|
|
960
|
+
fee_currency:
|
|
961
|
+
type: string
|
|
962
|
+
example: CLP
|
|
963
|
+
fee_rate_pct:
|
|
964
|
+
type: number
|
|
965
|
+
example: 0.8
|
|
966
|
+
total_cost:
|
|
967
|
+
type: number
|
|
968
|
+
description: Total outlay for buys (gross + fee) or net proceeds for sells (gross − fee).
|
|
969
|
+
example: 806400
|
|
970
|
+
slippage_vs_mid_pct:
|
|
971
|
+
type: number
|
|
972
|
+
description: Percentage difference between estimated fill price and mid-price.
|
|
973
|
+
example: 0.0027
|
|
974
|
+
mid_price:
|
|
975
|
+
type: number
|
|
976
|
+
example: 79997840
|
|
977
|
+
|
|
978
|
+
PositionSizeResponse:
|
|
979
|
+
type: object
|
|
980
|
+
properties:
|
|
981
|
+
market_id:
|
|
982
|
+
type: string
|
|
983
|
+
example: BTC-CLP
|
|
984
|
+
side:
|
|
985
|
+
type: string
|
|
986
|
+
enum: [buy, sell]
|
|
987
|
+
units:
|
|
988
|
+
type: number
|
|
989
|
+
description: Number of base currency units to trade.
|
|
990
|
+
example: 0.01
|
|
991
|
+
base_currency:
|
|
992
|
+
type: string
|
|
993
|
+
example: BTC
|
|
994
|
+
capital_at_risk:
|
|
995
|
+
type: number
|
|
996
|
+
description: Amount of capital at risk (capital × risk_pct / 100) in quote currency.
|
|
997
|
+
example: 20000
|
|
998
|
+
position_value:
|
|
999
|
+
type: number
|
|
1000
|
+
description: Total position value in quote currency (units × entry_price).
|
|
1001
|
+
example: 800000
|
|
1002
|
+
fee_impact:
|
|
1003
|
+
type: number
|
|
1004
|
+
description: Estimated entry fee at 0.8% taker rate in quote currency.
|
|
1005
|
+
example: 6400
|
|
1006
|
+
fee_currency:
|
|
1007
|
+
type: string
|
|
1008
|
+
example: CLP
|
|
1009
|
+
risk_reward_note:
|
|
1010
|
+
type: string
|
|
1011
|
+
description: Human-readable summary of the trade setup.
|
|
1012
|
+
|
|
1013
|
+
MarketSentimentResponse:
|
|
1014
|
+
type: object
|
|
1015
|
+
properties:
|
|
1016
|
+
market_id:
|
|
1017
|
+
type: string
|
|
1018
|
+
example: BTC-CLP
|
|
1019
|
+
score:
|
|
1020
|
+
type: number
|
|
1021
|
+
description: Composite sentiment score from −100 (very bearish) to +100 (very bullish).
|
|
1022
|
+
example: 23.5
|
|
1023
|
+
label:
|
|
1024
|
+
type: string
|
|
1025
|
+
enum: [bearish, neutral, bullish]
|
|
1026
|
+
example: bullish
|
|
1027
|
+
component_breakdown:
|
|
1028
|
+
type: object
|
|
1029
|
+
properties:
|
|
1030
|
+
price_variation_24h_pct:
|
|
1031
|
+
type: number
|
|
1032
|
+
example: 1.9
|
|
1033
|
+
volume_ratio:
|
|
1034
|
+
type: number
|
|
1035
|
+
description: Today's volume vs 7-day daily average (1.0 = average).
|
|
1036
|
+
example: 1.25
|
|
1037
|
+
spread_pct:
|
|
1038
|
+
type: number
|
|
1039
|
+
example: 0.005
|
|
1040
|
+
spread_baseline_pct:
|
|
1041
|
+
type: number
|
|
1042
|
+
example: 1.0
|
|
1043
|
+
price_score:
|
|
1044
|
+
type: number
|
|
1045
|
+
volume_score:
|
|
1046
|
+
type: number
|
|
1047
|
+
spread_score:
|
|
1048
|
+
type: number
|
|
1049
|
+
data_timestamp:
|
|
1050
|
+
type: string
|
|
1051
|
+
format: date-time
|
|
1052
|
+
disclaimer:
|
|
1053
|
+
type: string
|
|
1054
|
+
|
|
1055
|
+
TechnicalIndicatorsResponse:
|
|
1056
|
+
type: object
|
|
1057
|
+
properties:
|
|
1058
|
+
market_id:
|
|
1059
|
+
type: string
|
|
1060
|
+
example: BTC-CLP
|
|
1061
|
+
period:
|
|
1062
|
+
type: string
|
|
1063
|
+
enum: ["1h", "4h", "1d"]
|
|
1064
|
+
candles_used:
|
|
1065
|
+
type: integer
|
|
1066
|
+
example: 85
|
|
1067
|
+
indicators:
|
|
1068
|
+
type: object
|
|
1069
|
+
nullable: true
|
|
1070
|
+
properties:
|
|
1071
|
+
rsi:
|
|
1072
|
+
type: number
|
|
1073
|
+
nullable: true
|
|
1074
|
+
description: RSI(14). null if insufficient data.
|
|
1075
|
+
example: 62.4
|
|
1076
|
+
macd:
|
|
1077
|
+
type: object
|
|
1078
|
+
nullable: true
|
|
1079
|
+
properties:
|
|
1080
|
+
line:
|
|
1081
|
+
type: number
|
|
1082
|
+
signal:
|
|
1083
|
+
type: number
|
|
1084
|
+
histogram:
|
|
1085
|
+
type: number
|
|
1086
|
+
bollinger_bands:
|
|
1087
|
+
type: object
|
|
1088
|
+
nullable: true
|
|
1089
|
+
properties:
|
|
1090
|
+
upper:
|
|
1091
|
+
type: number
|
|
1092
|
+
mid:
|
|
1093
|
+
type: number
|
|
1094
|
+
lower:
|
|
1095
|
+
type: number
|
|
1096
|
+
sma_20:
|
|
1097
|
+
type: number
|
|
1098
|
+
sma_50:
|
|
1099
|
+
type: number
|
|
1100
|
+
signals:
|
|
1101
|
+
type: object
|
|
1102
|
+
properties:
|
|
1103
|
+
rsi_signal:
|
|
1104
|
+
type: string
|
|
1105
|
+
enum: [overbought, oversold, neutral]
|
|
1106
|
+
macd_signal:
|
|
1107
|
+
type: string
|
|
1108
|
+
enum: [bullish_crossover, bearish_crossover, neutral]
|
|
1109
|
+
bb_signal:
|
|
1110
|
+
type: string
|
|
1111
|
+
enum: [above_upper, below_lower, within_bands]
|
|
1112
|
+
warning:
|
|
1113
|
+
type: string
|
|
1114
|
+
description: Present only when candles_available < 50. Value is "insufficient_data".
|
|
1115
|
+
candles_available:
|
|
1116
|
+
type: integer
|
|
1117
|
+
description: Present only when warning is set.
|
|
1118
|
+
minimum_required:
|
|
1119
|
+
type: integer
|
|
1120
|
+
description: Present only when warning is set. Always 50.
|
|
1121
|
+
disclaimer:
|
|
1122
|
+
type: string
|
|
1123
|
+
|
|
750
1124
|
Error:
|
|
751
1125
|
type: object
|
|
752
1126
|
properties:
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/gtorreal/buda-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.4.
|
|
9
|
+
"version": "1.4.2",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@guiie/buda-mcp",
|
|
14
|
-
"version": "1.4.
|
|
14
|
+
"version": "1.4.2",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
}
|
|
@@ -12,7 +12,7 @@ export const toolSchema = {
|
|
|
12
12
|
"IMPORTANT: Candles are aggregated client-side from raw trades (Buda has no native candlestick " +
|
|
13
13
|
"endpoint) — fetching more trades via the 'limit' parameter gives deeper history but slower " +
|
|
14
14
|
"responses. Returns OHLCV candles (open/high/low/close as floats in quote currency; volume as float " +
|
|
15
|
-
"in base currency) for periods 1h, 4h, or 1d. Candle timestamps are UTC bucket boundaries. " +
|
|
15
|
+
"in base currency) for periods 5m, 15m, 30m, 1h, 4h, or 1d. Candle timestamps are UTC bucket boundaries. " +
|
|
16
16
|
"Example: 'Show me the hourly BTC-CLP price chart for the past 24 hours.'",
|
|
17
17
|
inputSchema: {
|
|
18
18
|
type: "object" as const,
|
|
@@ -23,7 +23,7 @@ export const toolSchema = {
|
|
|
23
23
|
},
|
|
24
24
|
period: {
|
|
25
25
|
type: "string",
|
|
26
|
-
description: "Candle period: '
|
|
26
|
+
description: "Candle period: '5m', '15m', '30m', '1h', '4h', or '1d'. Default: '1h'.",
|
|
27
27
|
},
|
|
28
28
|
limit: {
|
|
29
29
|
type: "number",
|
|
@@ -45,9 +45,9 @@ export function register(server: McpServer, client: BudaClient, _cache: MemoryCa
|
|
|
45
45
|
.string()
|
|
46
46
|
.describe("Market ID (e.g. 'BTC-CLP', 'ETH-BTC')."),
|
|
47
47
|
period: z
|
|
48
|
-
.enum(["1h", "4h", "1d"])
|
|
48
|
+
.enum(["5m", "15m", "30m", "1h", "4h", "1d"])
|
|
49
49
|
.default("1h")
|
|
50
|
-
.describe("Candle period: '
|
|
50
|
+
.describe("Candle period: '5m', '15m', '30m', '1h', '4h', or '1d'. Default: '1h'."),
|
|
51
51
|
limit: z
|
|
52
52
|
.number()
|
|
53
53
|
.int()
|
|
@@ -99,7 +99,7 @@ export async function handleSimulateOrder(
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
const mid = (minAsk + maxBid) / 2;
|
|
102
|
-
const takerFeeRate = parseFloat(market.taker_fee);
|
|
102
|
+
const takerFeeRate = parseFloat(market.taker_fee) / 100;
|
|
103
103
|
const orderTypeAssumed = price !== undefined ? "limit" : "market";
|
|
104
104
|
|
|
105
105
|
let estimatedFillPrice: number;
|
|
@@ -10,9 +10,10 @@ export const toolSchema = {
|
|
|
10
10
|
description:
|
|
11
11
|
"Computes RSI (14), MACD (12/26/9), Bollinger Bands (20, 2σ), SMA 20, and SMA 50 " +
|
|
12
12
|
"from Buda trade history — no external data or libraries. " +
|
|
13
|
+
"Supports periods: 5m, 15m, 30m, 1h, 4h, 1d. Use shorter periods (5m/15m) for intraday analysis. " +
|
|
13
14
|
"Uses at least 500 trades for reliable results (set limit=1000 for maximum depth). " +
|
|
14
15
|
"Returns latest indicator values and signal interpretations (overbought/oversold, crossover, band position). " +
|
|
15
|
-
"If fewer than
|
|
16
|
+
"If fewer than 20 candles are available after aggregation, returns a structured warning instead. " +
|
|
16
17
|
"Example: 'Is BTC-CLP RSI overbought on the 4-hour chart?'",
|
|
17
18
|
inputSchema: {
|
|
18
19
|
type: "object" as const,
|
|
@@ -23,7 +24,7 @@ export const toolSchema = {
|
|
|
23
24
|
},
|
|
24
25
|
period: {
|
|
25
26
|
type: "string",
|
|
26
|
-
description: "Candle period: '1h', '4h', or '1d'. Default: '1h'.",
|
|
27
|
+
description: "Candle period: '5m', '15m', '30m', '1h', '4h', or '1d'. Default: '1h'.",
|
|
27
28
|
},
|
|
28
29
|
limit: {
|
|
29
30
|
type: "number",
|
|
@@ -134,11 +135,11 @@ function bollingerBands(closes: number[], period: number = 20, stdMult: number =
|
|
|
134
135
|
|
|
135
136
|
// ---- Tool handler ----
|
|
136
137
|
|
|
137
|
-
const MIN_CANDLES =
|
|
138
|
+
const MIN_CANDLES = 20;
|
|
138
139
|
|
|
139
140
|
type TechnicalIndicatorsArgs = {
|
|
140
141
|
market_id: string;
|
|
141
|
-
period: "1h" | "4h" | "1d";
|
|
142
|
+
period: "5m" | "15m" | "30m" | "1h" | "4h" | "1d";
|
|
142
143
|
limit?: number;
|
|
143
144
|
};
|
|
144
145
|
|
|
@@ -263,9 +264,9 @@ export function register(server: McpServer, client: BudaClient): void {
|
|
|
263
264
|
.string()
|
|
264
265
|
.describe("Market ID (e.g. 'BTC-CLP', 'ETH-BTC')."),
|
|
265
266
|
period: z
|
|
266
|
-
.enum(["1h", "4h", "1d"])
|
|
267
|
+
.enum(["5m", "15m", "30m", "1h", "4h", "1d"])
|
|
267
268
|
.default("1h")
|
|
268
|
-
.describe("Candle period: '1h', '4h', or '1d'. Default: '1h'."),
|
|
269
|
+
.describe("Candle period: '5m', '15m', '30m', '1h', '4h', or '1d'. Default: '1h'."),
|
|
269
270
|
limit: z
|
|
270
271
|
.number()
|
|
271
272
|
.int()
|
package/src/utils.ts
CHANGED
|
@@ -21,9 +21,12 @@ export function getLiquidityRating(spreadPct: number): "high" | "medium" | "low"
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
const PERIOD_MS: Record<string, number> = {
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
24
|
+
"5m": 5 * 60 * 1000,
|
|
25
|
+
"15m": 15 * 60 * 1000,
|
|
26
|
+
"30m": 30 * 60 * 1000,
|
|
27
|
+
"1h": 60 * 60 * 1000,
|
|
28
|
+
"4h": 4 * 60 * 60 * 1000,
|
|
29
|
+
"1d": 24 * 60 * 60 * 1000,
|
|
27
30
|
};
|
|
28
31
|
|
|
29
32
|
/**
|
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,195 @@ 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
|
+
|
|
352
|
+
type TechIndicatorsResponse = {
|
|
353
|
+
candles_used?: number;
|
|
354
|
+
candles_available?: number;
|
|
355
|
+
warning?: string;
|
|
356
|
+
indicators: {
|
|
357
|
+
rsi: number | null;
|
|
358
|
+
macd: { line: number; signal: number; histogram: number } | null;
|
|
359
|
+
bollinger_bands: { upper: number; mid: number; lower: number } | null;
|
|
360
|
+
sma_20: number;
|
|
361
|
+
sma_50: number;
|
|
362
|
+
} | null;
|
|
363
|
+
signals: { rsi_signal: string; macd_signal: string; bb_signal: string };
|
|
364
|
+
disclaimer: string;
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
// 12a. 1h period — expected to hit insufficient_data (BTC-CLP has ~8 candles/1h)
|
|
368
|
+
section(`get_technical_indicators — ${TEST_MARKET} (1h, insufficient_data branch)`);
|
|
369
|
+
{
|
|
370
|
+
try {
|
|
371
|
+
const result = await handleTechnicalIndicators(
|
|
372
|
+
{ market_id: TEST_MARKET, period: "1h", limit: 1000 },
|
|
373
|
+
client,
|
|
374
|
+
);
|
|
375
|
+
if (result.isError) throw new Error(result.content[0].text);
|
|
376
|
+
const parsed = JSON.parse(result.content[0].text) as TechIndicatorsResponse;
|
|
377
|
+
if (parsed.warning !== "insufficient_data") {
|
|
378
|
+
pass("note", `got ${parsed.candles_used} candles — unexpectedly enough data, indicators returned`);
|
|
379
|
+
} else {
|
|
380
|
+
pass("warning", `insufficient_data — ${parsed.candles_available} candles available (need 50) ✓`);
|
|
381
|
+
pass("indicators", parsed.indicators === null ? "null ✓" : "SHOULD BE NULL");
|
|
382
|
+
}
|
|
383
|
+
} catch (err) {
|
|
384
|
+
fail("get_technical_indicators (1h)", err);
|
|
385
|
+
failures++;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// 12b. 5m period — enough candles to compute real indicators (~42 from last 100 trades)
|
|
390
|
+
section(`get_technical_indicators — ${TEST_MARKET} (5m, indicators branch)`);
|
|
391
|
+
{
|
|
392
|
+
try {
|
|
393
|
+
const result = await handleTechnicalIndicators(
|
|
394
|
+
{ market_id: TEST_MARKET, period: "5m", limit: 1000 },
|
|
395
|
+
client,
|
|
396
|
+
);
|
|
397
|
+
if (result.isError) throw new Error(result.content[0].text);
|
|
398
|
+
const parsed = JSON.parse(result.content[0].text) as TechIndicatorsResponse;
|
|
399
|
+
|
|
400
|
+
if (parsed.warning === "insufficient_data") {
|
|
401
|
+
// Market too quiet right now — report but don't fail
|
|
402
|
+
pass("note", `insufficient_data with 1m period (${parsed.candles_available} candles) — market unusually quiet`);
|
|
403
|
+
} else {
|
|
404
|
+
if (!parsed.indicators) throw new Error("indicators is null without a warning");
|
|
405
|
+
pass("candles_used", String(parsed.candles_used));
|
|
406
|
+
pass("rsi", parsed.indicators.rsi !== null ? `${parsed.indicators.rsi} (${parsed.signals.rsi_signal})` : "null (insufficient RSI data)");
|
|
407
|
+
pass("macd_histogram", parsed.indicators.macd !== null
|
|
408
|
+
? `${parsed.indicators.macd.histogram.toFixed(2)} (${parsed.signals.macd_signal})`
|
|
409
|
+
: "null (insufficient MACD data)");
|
|
410
|
+
pass("bb_upper", parsed.indicators.bollinger_bands !== null
|
|
411
|
+
? `${parsed.indicators.bollinger_bands.upper.toLocaleString()} (${parsed.signals.bb_signal})`
|
|
412
|
+
: "null (insufficient BB data)");
|
|
413
|
+
pass("sma_20", String(parsed.indicators.sma_20?.toLocaleString()));
|
|
414
|
+
pass("sma_50", parsed.indicators.sma_50 !== null ? String(parsed.indicators.sma_50?.toLocaleString()) : "null (need 50 candles)");
|
|
415
|
+
pass("disclaimer", parsed.disclaimer?.length > 0 ? "present ✓" : "MISSING");
|
|
416
|
+
}
|
|
417
|
+
} catch (err) {
|
|
418
|
+
fail("get_technical_indicators (1m)", err);
|
|
419
|
+
failures++;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
228
423
|
// ----------------------------------------------------------------
|
|
229
424
|
// Auth tools: get_balances, get_orders, place_order, cancel_order
|
|
230
425
|
// ----------------------------------------------------------------
|
|
@@ -263,6 +458,34 @@ if (!client.hasAuth()) {
|
|
|
263
458
|
// cancel_order — confirmation guard test (must reject without CONFIRM)
|
|
264
459
|
console.log(" Skipping: cancel_order live execution (destructive — requires confirmation_token=CONFIRM)");
|
|
265
460
|
pass("cancel_order guard", "confirmation_token check enforced at tool layer (code-audited)");
|
|
461
|
+
|
|
462
|
+
// schedule_cancel_all — arm then immediately disarm (non-destructive)
|
|
463
|
+
try {
|
|
464
|
+
const armResult = await handleScheduleCancelAll(
|
|
465
|
+
{ market_id: TEST_MARKET, ttl_seconds: 300, confirmation_token: "CONFIRM" },
|
|
466
|
+
client,
|
|
467
|
+
);
|
|
468
|
+
if (armResult.isError) throw new Error(armResult.content[0].text);
|
|
469
|
+
const armed = JSON.parse(armResult.content[0].text) as {
|
|
470
|
+
active: boolean;
|
|
471
|
+
expires_at: string;
|
|
472
|
+
ttl_seconds: number;
|
|
473
|
+
warning: string;
|
|
474
|
+
};
|
|
475
|
+
if (!armed.active) throw new Error("active should be true after CONFIRM");
|
|
476
|
+
pass("schedule_cancel_all active", armed.active ? "true" : "false");
|
|
477
|
+
pass("schedule_cancel_all expires_at", armed.expires_at);
|
|
478
|
+
pass("schedule_cancel_all warning", armed.warning.length > 0 ? "present" : "MISSING");
|
|
479
|
+
|
|
480
|
+
// Immediately disarm so no orders are cancelled
|
|
481
|
+
const disarmResult = handleDisarmCancelTimer({ market_id: TEST_MARKET });
|
|
482
|
+
if (disarmResult.isError) throw new Error(disarmResult.content[0].text);
|
|
483
|
+
const disarmed = JSON.parse(disarmResult.content[0].text) as { disarmed: boolean };
|
|
484
|
+
pass("disarm_cancel_timer", disarmed.disarmed ? "timer cleared ✓" : "FAILED to disarm");
|
|
485
|
+
} catch (err) {
|
|
486
|
+
fail("schedule_cancel_all / disarm_cancel_timer", err);
|
|
487
|
+
failures++;
|
|
488
|
+
}
|
|
266
489
|
}
|
|
267
490
|
|
|
268
491
|
// ----------------------------------------------------------------
|
|
@@ -271,6 +494,8 @@ if (!client.hasAuth()) {
|
|
|
271
494
|
section("Summary");
|
|
272
495
|
if (failures === 0) {
|
|
273
496
|
console.log(" All tools returned valid data from the live Buda API.");
|
|
497
|
+
console.log(" Coverage: simulate_order, calculate_position_size, get_market_sentiment,");
|
|
498
|
+
console.log(" get_technical_indicators, schedule_cancel_all/disarm (auth-gated if credentials set).");
|
|
274
499
|
} else {
|
|
275
500
|
console.error(` ${failures} tool(s) failed. See errors above.`);
|
|
276
501
|
process.exit(1);
|
package/test/unit.ts
CHANGED
|
@@ -666,7 +666,7 @@ await test("handleMarketSummary returns correct liquidity_rating from mocked API
|
|
|
666
666
|
|
|
667
667
|
section("i. simulate_order");
|
|
668
668
|
|
|
669
|
-
function makeMockFetchForSimulate(takerFee = "0.
|
|
669
|
+
function makeMockFetchForSimulate(takerFee = "0.8"): typeof fetch {
|
|
670
670
|
const mockTicker = {
|
|
671
671
|
ticker: {
|
|
672
672
|
market_id: "BTC-CLP",
|
|
@@ -720,7 +720,7 @@ await test("market buy: estimated_fill_price = min_ask, simulation: true", async
|
|
|
720
720
|
assertEqual(parsed.simulation, true, "simulation flag must be true");
|
|
721
721
|
assertEqual(parsed.estimated_fill_price, 65100000, "market buy fills at min_ask");
|
|
722
722
|
assertEqual(parsed.order_type_assumed, "market", "order_type_assumed should be market");
|
|
723
|
-
assertEqual(parsed.fee_rate_pct, 0.8, "
|
|
723
|
+
assertEqual(parsed.fee_rate_pct, 0.8, "fee_rate_pct should be 0.8 for crypto (0.8% taker fee)");
|
|
724
724
|
} finally {
|
|
725
725
|
globalThis.fetch = savedFetch;
|
|
726
726
|
}
|
|
@@ -758,14 +758,14 @@ await test("limit order: order_type_assumed = 'limit'", async () => {
|
|
|
758
758
|
|
|
759
759
|
await test("stablecoin market uses 0.5% fee", async () => {
|
|
760
760
|
const savedFetch = globalThis.fetch;
|
|
761
|
-
globalThis.fetch = makeMockFetchForSimulate("0.
|
|
761
|
+
globalThis.fetch = makeMockFetchForSimulate("0.5");
|
|
762
762
|
try {
|
|
763
763
|
const client = new BudaClient("https://www.buda.com/api/v2");
|
|
764
764
|
const cache = new MemoryCache();
|
|
765
765
|
const result = await handleSimulateOrder({ market_id: "BTC-CLP", side: "buy", amount: 1 }, client, cache);
|
|
766
766
|
assert(!result.isError, "should not be an error");
|
|
767
767
|
const parsed = JSON.parse(result.content[0].text) as { fee_rate_pct: number };
|
|
768
|
-
assertEqual(parsed.fee_rate_pct, 0.5, "fee_rate_pct should be 0.5 for stablecoin");
|
|
768
|
+
assertEqual(parsed.fee_rate_pct, 0.5, "fee_rate_pct should be 0.5 for stablecoin (0.5% taker fee)");
|
|
769
769
|
} finally {
|
|
770
770
|
globalThis.fetch = savedFetch;
|
|
771
771
|
}
|
|
@@ -1024,7 +1024,7 @@ await test("technical indicators: insufficient candles returns warning", async (
|
|
|
1024
1024
|
};
|
|
1025
1025
|
assertEqual(parsed.warning, "insufficient_data", "should return insufficient_data warning");
|
|
1026
1026
|
assertEqual(parsed.indicators, null, "indicators should be null");
|
|
1027
|
-
assertEqual(parsed.minimum_required,
|
|
1027
|
+
assertEqual(parsed.minimum_required, 20, "minimum_required should be 20");
|
|
1028
1028
|
} finally {
|
|
1029
1029
|
globalThis.fetch = savedFetch;
|
|
1030
1030
|
}
|