@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 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: '1h' (1 hour), '4h' (4 hours), or '1d' (1 day). Default: '1h'.",
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: '1h' (1 hour), '4h' (4 hours), or '1d' (1 day). Default: '1h'."),
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;;;;;;;;;;;;;;;;;;;;;CA6BtB,CAAC;AAsGF,KAAK,uBAAuB,GAAG;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,uBAAuB,EAC7B,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CA2GhF;AAED,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,GAAG,IAAI,CAyBpE"}
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 50 candles are available after aggregation, returns a structured warning instead. " +
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 = 50;
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()
@@ -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;AAQD;;;;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"}
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
@@ -19,6 +19,9 @@ export function getLiquidityRating(spreadPct) {
19
19
  return "low";
20
20
  }
21
21
  const PERIOD_MS = {
22
+ "5m": 5 * 60 * 1000,
23
+ "15m": 15 * 60 * 1000,
24
+ "30m": 30 * 60 * 1000,
22
25
  "1h": 60 * 60 * 1000,
23
26
  "4h": 4 * 60 * 60 * 1000,
24
27
  "1d": 24 * 60 * 60 * 1000,
@@ -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
  }
@@ -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.3.0
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guiie/buda-mcp",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "mcpName": "io.github.gtorreal/buda-mcp",
5
5
  "description": "MCP server for Buda.com's public cryptocurrency exchange API (Chile, Colombia, Peru)",
6
6
  "type": "module",
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.0",
9
+ "version": "1.4.2",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@guiie/buda-mcp",
14
- "version": "1.4.0",
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: '1h' (1 hour), '4h' (4 hours), or '1d' (1 day). Default: '1h'.",
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: '1h' (1 hour), '4h' (4 hours), or '1d' (1 day). Default: '1h'."),
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 50 candles are available after aggregation, returns a structured warning instead. " +
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 = 50;
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
- "1h": 60 * 60 * 1000,
25
- "4h": 4 * 60 * 60 * 1000,
26
- "1d": 24 * 60 * 60 * 1000,
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.008"): typeof fetch {
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, "fee rate should be 0.8% for crypto");
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.005");
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, 50, "minimum_required should be 50");
1027
+ assertEqual(parsed.minimum_required, 20, "minimum_required should be 20");
1028
1028
  } finally {
1029
1029
  globalThis.fetch = savedFetch;
1030
1030
  }