@guiie/buda-mcp 1.2.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -0
- package/PUBLISH_CHECKLIST.md +71 -63
- package/README.md +4 -4
- package/dist/http.js +39 -0
- package/dist/index.js +29 -0
- package/dist/tools/arbitrage.d.ts +35 -0
- package/dist/tools/arbitrage.d.ts.map +1 -0
- package/dist/tools/arbitrage.js +142 -0
- package/dist/tools/balances.d.ts.map +1 -1
- package/dist/tools/balances.js +24 -4
- package/dist/tools/calculate_position_size.d.ts +48 -0
- package/dist/tools/calculate_position_size.d.ts.map +1 -0
- package/dist/tools/calculate_position_size.js +111 -0
- package/dist/tools/compare_markets.d.ts.map +1 -1
- package/dist/tools/compare_markets.js +11 -10
- package/dist/tools/dead_mans_switch.d.ts +84 -0
- package/dist/tools/dead_mans_switch.d.ts.map +1 -0
- package/dist/tools/dead_mans_switch.js +236 -0
- package/dist/tools/market_sentiment.d.ts +30 -0
- package/dist/tools/market_sentiment.d.ts.map +1 -0
- package/dist/tools/market_sentiment.js +104 -0
- package/dist/tools/market_summary.d.ts +43 -0
- package/dist/tools/market_summary.d.ts.map +1 -0
- package/dist/tools/market_summary.js +81 -0
- package/dist/tools/markets.d.ts.map +1 -1
- package/dist/tools/markets.js +4 -2
- package/dist/tools/orderbook.d.ts.map +1 -1
- package/dist/tools/orderbook.js +14 -4
- package/dist/tools/orders.d.ts.map +1 -1
- package/dist/tools/orders.js +41 -3
- package/dist/tools/price_history.d.ts.map +1 -1
- package/dist/tools/price_history.js +5 -43
- package/dist/tools/simulate_order.d.ts +45 -0
- package/dist/tools/simulate_order.d.ts.map +1 -0
- package/dist/tools/simulate_order.js +139 -0
- package/dist/tools/spread.d.ts.map +1 -1
- package/dist/tools/spread.js +10 -8
- package/dist/tools/technical_indicators.d.ts +39 -0
- package/dist/tools/technical_indicators.d.ts.map +1 -0
- package/dist/tools/technical_indicators.js +223 -0
- package/dist/tools/ticker.d.ts.map +1 -1
- package/dist/tools/ticker.js +24 -3
- package/dist/tools/trades.d.ts.map +1 -1
- package/dist/tools/trades.js +17 -3
- package/dist/tools/volume.d.ts.map +1 -1
- package/dist/tools/volume.js +21 -3
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +23 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +68 -0
- package/marketplace/README.md +1 -1
- package/marketplace/claude-listing.md +60 -14
- package/marketplace/gemini-tools.json +183 -9
- package/marketplace/openapi.yaml +335 -119
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/http.ts +44 -0
- package/src/index.ts +34 -0
- package/src/tools/arbitrage.ts +202 -0
- package/src/tools/balances.ts +27 -4
- package/src/tools/calculate_position_size.ts +141 -0
- package/src/tools/compare_markets.ts +11 -10
- package/src/tools/dead_mans_switch.ts +314 -0
- package/src/tools/market_sentiment.ts +141 -0
- package/src/tools/market_summary.ts +124 -0
- package/src/tools/markets.ts +4 -2
- package/src/tools/orderbook.ts +15 -4
- package/src/tools/orders.ts +45 -4
- package/src/tools/price_history.ts +5 -57
- package/src/tools/simulate_order.ts +182 -0
- package/src/tools/spread.ts +10 -8
- package/src/tools/technical_indicators.ts +282 -0
- package/src/tools/ticker.ts +27 -3
- package/src/tools/trades.ts +18 -3
- package/src/tools/volume.ts +24 -3
- package/src/types.ts +12 -0
- package/src/utils.ts +73 -0
- package/test/unit.ts +758 -0
package/test/unit.ts
CHANGED
|
@@ -9,6 +9,14 @@ import { MemoryCache } from "../src/cache.js";
|
|
|
9
9
|
import { validateMarketId } from "../src/validation.js";
|
|
10
10
|
import { handlePlaceOrder } from "../src/tools/place_order.js";
|
|
11
11
|
import { handleCancelOrder } from "../src/tools/cancel_order.js";
|
|
12
|
+
import { flattenAmount, getLiquidityRating, aggregateTradesToCandles } from "../src/utils.js";
|
|
13
|
+
import { handleArbitrageOpportunities } from "../src/tools/arbitrage.js";
|
|
14
|
+
import { handleMarketSummary } from "../src/tools/market_summary.js";
|
|
15
|
+
import { handleSimulateOrder } from "../src/tools/simulate_order.js";
|
|
16
|
+
import { handleCalculatePositionSize } from "../src/tools/calculate_position_size.js";
|
|
17
|
+
import { handleMarketSentiment } from "../src/tools/market_sentiment.js";
|
|
18
|
+
import { handleTechnicalIndicators } from "../src/tools/technical_indicators.js";
|
|
19
|
+
import { handleScheduleCancelAll, handleRenewCancelTimer, handleDisarmCancelTimer } from "../src/tools/dead_mans_switch.js";
|
|
12
20
|
|
|
13
21
|
// ----------------------------------------------------------------
|
|
14
22
|
// Minimal test harness
|
|
@@ -401,6 +409,756 @@ await test("defaults to 1000ms when Retry-After header is absent", async () => {
|
|
|
401
409
|
}
|
|
402
410
|
});
|
|
403
411
|
|
|
412
|
+
// ----------------------------------------------------------------
|
|
413
|
+
// f. Numeric flattening — flattenAmount returns typed float, not string
|
|
414
|
+
// ----------------------------------------------------------------
|
|
415
|
+
|
|
416
|
+
section("f. Numeric flattening — flattenAmount");
|
|
417
|
+
|
|
418
|
+
await test("flattenAmount returns a number value, not a string", () => {
|
|
419
|
+
const result = flattenAmount(["65000000", "CLP"]);
|
|
420
|
+
assert(typeof result.value === "number", "value should be a number");
|
|
421
|
+
assertEqual(result.value, 65000000, "value should equal 65000000");
|
|
422
|
+
assertEqual(result.currency, "CLP", "currency should equal CLP");
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
await test("flattenAmount handles decimal strings correctly", () => {
|
|
426
|
+
const result = flattenAmount(["4.99123456", "BTC"]);
|
|
427
|
+
assert(typeof result.value === "number", "value should be a number");
|
|
428
|
+
assertEqual(result.value, 4.99123456, "value should equal 4.99123456");
|
|
429
|
+
assertEqual(result.currency, "BTC", "currency should equal BTC");
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
await test("flattenAmount on zero amount", () => {
|
|
433
|
+
const result = flattenAmount(["0.0", "CLP"]);
|
|
434
|
+
assertEqual(result.value, 0, "zero should parse to 0");
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
await test("flattenAmount value is not a string array", () => {
|
|
438
|
+
const result = flattenAmount(["65000000", "CLP"]);
|
|
439
|
+
assert(!Array.isArray(result), "result should not be an array");
|
|
440
|
+
assert(typeof result.value !== "string", "value should not be a string");
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// ----------------------------------------------------------------
|
|
444
|
+
// g. get_arbitrage_opportunities — discrepancy calculation
|
|
445
|
+
// ----------------------------------------------------------------
|
|
446
|
+
|
|
447
|
+
section("g. get_arbitrage_opportunities — discrepancy calculation");
|
|
448
|
+
|
|
449
|
+
await test("correctly computes USDC-normalized price discrepancy between CLP and PEN markets", async () => {
|
|
450
|
+
const savedFetch = globalThis.fetch;
|
|
451
|
+
|
|
452
|
+
// BTC-CLP: 65000000 CLP, USDC-CLP: 1000 CLP → BTC in USDC = 65000
|
|
453
|
+
// BTC-PEN: 250000000 PEN, USDC-PEN: 3700 PEN → BTC in USDC ≈ 67567.567...
|
|
454
|
+
// Discrepancy: (67567.567 - 65000) / 65000 * 100 ≈ 3.95%
|
|
455
|
+
const mockTickers = {
|
|
456
|
+
tickers: [
|
|
457
|
+
{ market_id: "BTC-CLP", last_price: ["65000000", "CLP"], max_bid: ["64900000", "CLP"], min_ask: ["65100000", "CLP"], volume: ["4.99", "BTC"], price_variation_24h: "0.01", price_variation_7d: "0.05" },
|
|
458
|
+
{ market_id: "BTC-PEN", last_price: ["250000000", "PEN"], max_bid: ["249500000", "PEN"], min_ask: ["250500000", "PEN"], volume: ["1.5", "BTC"], price_variation_24h: "0.012", price_variation_7d: "0.04" },
|
|
459
|
+
{ market_id: "USDC-CLP", last_price: ["1000", "CLP"], max_bid: ["999", "CLP"], min_ask: ["1001", "CLP"], volume: ["100", "USDC"], price_variation_24h: "0.001", price_variation_7d: "0.002" },
|
|
460
|
+
{ market_id: "USDC-PEN", last_price: ["3700", "PEN"], max_bid: ["3695", "PEN"], min_ask: ["3705", "PEN"], volume: ["50", "USDC"], price_variation_24h: "0.001", price_variation_7d: "0.002" },
|
|
461
|
+
],
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
globalThis.fetch = async (): Promise<Response> => {
|
|
465
|
+
return new Response(JSON.stringify(mockTickers), {
|
|
466
|
+
status: 200,
|
|
467
|
+
headers: { "Content-Type": "application/json" },
|
|
468
|
+
});
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
try {
|
|
472
|
+
const client = new BudaClient("https://www.buda.com/api/v2");
|
|
473
|
+
const cache = new MemoryCache();
|
|
474
|
+
const result = await handleArbitrageOpportunities(
|
|
475
|
+
{ base_currency: "BTC", threshold_pct: 0.5 },
|
|
476
|
+
client,
|
|
477
|
+
cache,
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
assert(!result.isError, "should not return an error");
|
|
481
|
+
const parsed = JSON.parse(result.content[0].text) as {
|
|
482
|
+
opportunities: Array<{ market_a: string; market_b: string; discrepancy_pct: number }>;
|
|
483
|
+
markets_analyzed: Array<{ market_id: string; price_usdc: number }>;
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
assertEqual(parsed.markets_analyzed.length, 2, "should have 2 markets analyzed");
|
|
487
|
+
assertEqual(parsed.opportunities.length, 1, "should have exactly 1 opportunity");
|
|
488
|
+
|
|
489
|
+
const opp = parsed.opportunities[0];
|
|
490
|
+
const expectedDiscrepancy = ((67567.5676 - 65000) / 65000) * 100;
|
|
491
|
+
assert(
|
|
492
|
+
Math.abs(opp.discrepancy_pct - expectedDiscrepancy) < 0.01,
|
|
493
|
+
`discrepancy_pct should be ≈${expectedDiscrepancy.toFixed(2)}%, got ${opp.discrepancy_pct}`,
|
|
494
|
+
);
|
|
495
|
+
} finally {
|
|
496
|
+
globalThis.fetch = savedFetch;
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
await test("threshold filtering excludes opportunities below threshold", async () => {
|
|
501
|
+
const savedFetch = globalThis.fetch;
|
|
502
|
+
|
|
503
|
+
// ~3.95% discrepancy between CLP and PEN — threshold 5% should exclude it
|
|
504
|
+
const mockTickers = {
|
|
505
|
+
tickers: [
|
|
506
|
+
{ market_id: "BTC-CLP", last_price: ["65000000", "CLP"], max_bid: ["64900000", "CLP"], min_ask: ["65100000", "CLP"], volume: ["4.99", "BTC"], price_variation_24h: "0.01", price_variation_7d: "0.05" },
|
|
507
|
+
{ market_id: "BTC-PEN", last_price: ["250000000", "PEN"], max_bid: ["249500000", "PEN"], min_ask: ["250500000", "PEN"], volume: ["1.5", "BTC"], price_variation_24h: "0.012", price_variation_7d: "0.04" },
|
|
508
|
+
{ market_id: "USDC-CLP", last_price: ["1000", "CLP"], max_bid: ["999", "CLP"], min_ask: ["1001", "CLP"], volume: ["100", "USDC"], price_variation_24h: "0.001", price_variation_7d: "0.002" },
|
|
509
|
+
{ market_id: "USDC-PEN", last_price: ["3700", "PEN"], max_bid: ["3695", "PEN"], min_ask: ["3705", "PEN"], volume: ["50", "USDC"], price_variation_24h: "0.001", price_variation_7d: "0.002" },
|
|
510
|
+
],
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
globalThis.fetch = async (): Promise<Response> => {
|
|
514
|
+
return new Response(JSON.stringify(mockTickers), {
|
|
515
|
+
status: 200,
|
|
516
|
+
headers: { "Content-Type": "application/json" },
|
|
517
|
+
});
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
try {
|
|
521
|
+
const client = new BudaClient("https://www.buda.com/api/v2");
|
|
522
|
+
const cache = new MemoryCache();
|
|
523
|
+
const result = await handleArbitrageOpportunities(
|
|
524
|
+
{ base_currency: "BTC", threshold_pct: 5.0 },
|
|
525
|
+
client,
|
|
526
|
+
cache,
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
assert(!result.isError, "should not return an error");
|
|
530
|
+
const parsed = JSON.parse(result.content[0].text) as {
|
|
531
|
+
opportunities: Array<unknown>;
|
|
532
|
+
};
|
|
533
|
+
assertEqual(parsed.opportunities.length, 0, "threshold 5% should exclude the ~3.95% discrepancy");
|
|
534
|
+
} finally {
|
|
535
|
+
globalThis.fetch = savedFetch;
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
await test("returns error when fewer than 2 markets are found", async () => {
|
|
540
|
+
const savedFetch = globalThis.fetch;
|
|
541
|
+
|
|
542
|
+
// Only CLP market available, no PEN or COP
|
|
543
|
+
const mockTickers = {
|
|
544
|
+
tickers: [
|
|
545
|
+
{ market_id: "BTC-CLP", last_price: ["65000000", "CLP"], max_bid: ["64900000", "CLP"], min_ask: ["65100000", "CLP"], volume: ["4.99", "BTC"], price_variation_24h: "0.01", price_variation_7d: "0.05" },
|
|
546
|
+
{ market_id: "USDC-CLP", last_price: ["1000", "CLP"], max_bid: ["999", "CLP"], min_ask: ["1001", "CLP"], volume: ["100", "USDC"], price_variation_24h: "0.001", price_variation_7d: "0.002" },
|
|
547
|
+
],
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
globalThis.fetch = async (): Promise<Response> => {
|
|
551
|
+
return new Response(JSON.stringify(mockTickers), {
|
|
552
|
+
status: 200,
|
|
553
|
+
headers: { "Content-Type": "application/json" },
|
|
554
|
+
});
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
try {
|
|
558
|
+
const client = new BudaClient("https://www.buda.com/api/v2");
|
|
559
|
+
const cache = new MemoryCache();
|
|
560
|
+
const result = await handleArbitrageOpportunities(
|
|
561
|
+
{ base_currency: "BTC", threshold_pct: 0.5 },
|
|
562
|
+
client,
|
|
563
|
+
cache,
|
|
564
|
+
);
|
|
565
|
+
assert(result.isError === true, "should return isError when not enough markets");
|
|
566
|
+
} finally {
|
|
567
|
+
globalThis.fetch = savedFetch;
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
// ----------------------------------------------------------------
|
|
572
|
+
// h. get_market_summary — liquidity_rating thresholds
|
|
573
|
+
// ----------------------------------------------------------------
|
|
574
|
+
|
|
575
|
+
section("h. get_market_summary — liquidity_rating thresholds");
|
|
576
|
+
|
|
577
|
+
await test("getLiquidityRating: spread < 0.3% → 'high'", () => {
|
|
578
|
+
assertEqual(getLiquidityRating(0), "high", "0% spread should be high");
|
|
579
|
+
assertEqual(getLiquidityRating(0.1), "high", "0.1% spread should be high");
|
|
580
|
+
assertEqual(getLiquidityRating(0.29), "high", "0.29% spread should be high");
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
await test("getLiquidityRating: spread at 0.3% boundary → 'medium'", () => {
|
|
584
|
+
assertEqual(getLiquidityRating(0.3), "medium", "exactly 0.3% spread should be medium");
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
await test("getLiquidityRating: spread 0.3–1% → 'medium'", () => {
|
|
588
|
+
assertEqual(getLiquidityRating(0.5), "medium", "0.5% spread should be medium");
|
|
589
|
+
assertEqual(getLiquidityRating(1.0), "medium", "exactly 1.0% spread should be medium");
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
await test("getLiquidityRating: spread > 1% → 'low'", () => {
|
|
593
|
+
assertEqual(getLiquidityRating(1.01), "low", "1.01% spread should be low");
|
|
594
|
+
assertEqual(getLiquidityRating(5.0), "low", "5% spread should be low");
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
await test("handleMarketSummary returns correct liquidity_rating from mocked API", async () => {
|
|
598
|
+
const savedFetch = globalThis.fetch;
|
|
599
|
+
let callCount = 0;
|
|
600
|
+
|
|
601
|
+
// Ticker: bid 64870, ask 65000 → spread = 130 / 65000 * 100 = 0.2% → "high"
|
|
602
|
+
const mockTicker = {
|
|
603
|
+
ticker: {
|
|
604
|
+
market_id: "BTC-CLP",
|
|
605
|
+
last_price: ["65000", "CLP"],
|
|
606
|
+
max_bid: ["64870", "CLP"],
|
|
607
|
+
min_ask: ["65000", "CLP"],
|
|
608
|
+
volume: ["4.99", "BTC"],
|
|
609
|
+
price_variation_24h: "0.012",
|
|
610
|
+
price_variation_7d: "0.05",
|
|
611
|
+
},
|
|
612
|
+
};
|
|
613
|
+
const mockVolume = {
|
|
614
|
+
volume: {
|
|
615
|
+
market_id: "BTC-CLP",
|
|
616
|
+
ask_volume_24h: ["10.5", "BTC"],
|
|
617
|
+
ask_volume_7d: ["72.1", "BTC"],
|
|
618
|
+
bid_volume_24h: ["9.8", "BTC"],
|
|
619
|
+
bid_volume_7d: ["68.3", "BTC"],
|
|
620
|
+
},
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
globalThis.fetch = async (url: string | URL): Promise<Response> => {
|
|
624
|
+
callCount++;
|
|
625
|
+
const urlStr = url.toString();
|
|
626
|
+
if (urlStr.includes("/volume")) {
|
|
627
|
+
return new Response(JSON.stringify(mockVolume), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
628
|
+
}
|
|
629
|
+
return new Response(JSON.stringify(mockTicker), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
try {
|
|
633
|
+
const client = new BudaClient("https://www.buda.com/api/v2");
|
|
634
|
+
const cache = new MemoryCache();
|
|
635
|
+
const result = await handleMarketSummary({ market_id: "BTC-CLP" }, client, cache);
|
|
636
|
+
|
|
637
|
+
assert(!result.isError, "should not return an error");
|
|
638
|
+
const parsed = JSON.parse(result.content[0].text) as {
|
|
639
|
+
market_id: string;
|
|
640
|
+
last_price: number;
|
|
641
|
+
last_price_currency: string;
|
|
642
|
+
bid: number;
|
|
643
|
+
ask: number;
|
|
644
|
+
spread_pct: number;
|
|
645
|
+
volume_24h: number;
|
|
646
|
+
liquidity_rating: string;
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
assertEqual(parsed.market_id, "BTC-CLP", "market_id should match");
|
|
650
|
+
assertEqual(parsed.last_price, 65000, "last_price should be a number");
|
|
651
|
+
assert(typeof parsed.last_price === "number", "last_price should be a number type");
|
|
652
|
+
assertEqual(parsed.last_price_currency, "CLP", "currency should be CLP");
|
|
653
|
+
assertEqual(parsed.bid, 64870, "bid should be a float");
|
|
654
|
+
assertEqual(parsed.ask, 65000, "ask should be a float");
|
|
655
|
+
// spread = (65000 - 64870) / 65000 * 100 = 130/65000*100 = 0.2%
|
|
656
|
+
assertEqual(parsed.liquidity_rating, "high", "spread 0.2% should yield 'high' liquidity");
|
|
657
|
+
assertEqual(parsed.volume_24h, 10.5, "volume_24h should be a float");
|
|
658
|
+
} finally {
|
|
659
|
+
globalThis.fetch = savedFetch;
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
// ----------------------------------------------------------------
|
|
664
|
+
// i. simulate_order — simulation outputs
|
|
665
|
+
// ----------------------------------------------------------------
|
|
666
|
+
|
|
667
|
+
section("i. simulate_order");
|
|
668
|
+
|
|
669
|
+
function makeMockFetchForSimulate(takerFee = "0.008"): typeof fetch {
|
|
670
|
+
const mockTicker = {
|
|
671
|
+
ticker: {
|
|
672
|
+
market_id: "BTC-CLP",
|
|
673
|
+
last_price: ["65000000", "CLP"],
|
|
674
|
+
max_bid: ["64900000", "CLP"],
|
|
675
|
+
min_ask: ["65100000", "CLP"],
|
|
676
|
+
volume: ["4.99", "BTC"],
|
|
677
|
+
price_variation_24h: "0.01",
|
|
678
|
+
price_variation_7d: "0.05",
|
|
679
|
+
},
|
|
680
|
+
};
|
|
681
|
+
const mockMarket = {
|
|
682
|
+
market: {
|
|
683
|
+
id: "btc-clp",
|
|
684
|
+
name: "BTC-CLP",
|
|
685
|
+
base_currency: "BTC",
|
|
686
|
+
quote_currency: "CLP",
|
|
687
|
+
taker_fee: takerFee,
|
|
688
|
+
maker_fee: "0.004",
|
|
689
|
+
minimum_order_amount: ["0.0001", "BTC"],
|
|
690
|
+
max_orders_per_minute: 50,
|
|
691
|
+
maker_discount_percentage: "0",
|
|
692
|
+
taker_discount_percentage: "0",
|
|
693
|
+
maker_discount_tiers: {},
|
|
694
|
+
taker_discount_tiers: {},
|
|
695
|
+
},
|
|
696
|
+
};
|
|
697
|
+
return async (url: string | URL): Promise<Response> => {
|
|
698
|
+
const urlStr = url.toString();
|
|
699
|
+
if (urlStr.includes("/ticker")) {
|
|
700
|
+
return new Response(JSON.stringify(mockTicker), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
701
|
+
}
|
|
702
|
+
return new Response(JSON.stringify(mockMarket), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
await test("market buy: estimated_fill_price = min_ask, simulation: true", async () => {
|
|
707
|
+
const savedFetch = globalThis.fetch;
|
|
708
|
+
globalThis.fetch = makeMockFetchForSimulate();
|
|
709
|
+
try {
|
|
710
|
+
const client = new BudaClient("https://www.buda.com/api/v2");
|
|
711
|
+
const cache = new MemoryCache();
|
|
712
|
+
const result = await handleSimulateOrder({ market_id: "BTC-CLP", side: "buy", amount: 0.01 }, client, cache);
|
|
713
|
+
assert(!result.isError, "should not be an error");
|
|
714
|
+
const parsed = JSON.parse(result.content[0].text) as {
|
|
715
|
+
simulation: boolean;
|
|
716
|
+
estimated_fill_price: number;
|
|
717
|
+
order_type_assumed: string;
|
|
718
|
+
fee_rate_pct: number;
|
|
719
|
+
};
|
|
720
|
+
assertEqual(parsed.simulation, true, "simulation flag must be true");
|
|
721
|
+
assertEqual(parsed.estimated_fill_price, 65100000, "market buy fills at min_ask");
|
|
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");
|
|
724
|
+
} finally {
|
|
725
|
+
globalThis.fetch = savedFetch;
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
await test("market sell: estimated_fill_price = max_bid", async () => {
|
|
730
|
+
const savedFetch = globalThis.fetch;
|
|
731
|
+
globalThis.fetch = makeMockFetchForSimulate();
|
|
732
|
+
try {
|
|
733
|
+
const client = new BudaClient("https://www.buda.com/api/v2");
|
|
734
|
+
const cache = new MemoryCache();
|
|
735
|
+
const result = await handleSimulateOrder({ market_id: "BTC-CLP", side: "sell", amount: 0.01 }, client, cache);
|
|
736
|
+
assert(!result.isError, "should not be an error");
|
|
737
|
+
const parsed = JSON.parse(result.content[0].text) as { estimated_fill_price: number };
|
|
738
|
+
assertEqual(parsed.estimated_fill_price, 64900000, "market sell fills at max_bid");
|
|
739
|
+
} finally {
|
|
740
|
+
globalThis.fetch = savedFetch;
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
await test("limit order: order_type_assumed = 'limit'", async () => {
|
|
745
|
+
const savedFetch = globalThis.fetch;
|
|
746
|
+
globalThis.fetch = makeMockFetchForSimulate();
|
|
747
|
+
try {
|
|
748
|
+
const client = new BudaClient("https://www.buda.com/api/v2");
|
|
749
|
+
const cache = new MemoryCache();
|
|
750
|
+
const result = await handleSimulateOrder({ market_id: "BTC-CLP", side: "buy", amount: 0.01, price: 64000000 }, client, cache);
|
|
751
|
+
assert(!result.isError, "should not be an error");
|
|
752
|
+
const parsed = JSON.parse(result.content[0].text) as { order_type_assumed: string };
|
|
753
|
+
assertEqual(parsed.order_type_assumed, "limit", "order_type_assumed should be limit");
|
|
754
|
+
} finally {
|
|
755
|
+
globalThis.fetch = savedFetch;
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
await test("stablecoin market uses 0.5% fee", async () => {
|
|
760
|
+
const savedFetch = globalThis.fetch;
|
|
761
|
+
globalThis.fetch = makeMockFetchForSimulate("0.005");
|
|
762
|
+
try {
|
|
763
|
+
const client = new BudaClient("https://www.buda.com/api/v2");
|
|
764
|
+
const cache = new MemoryCache();
|
|
765
|
+
const result = await handleSimulateOrder({ market_id: "BTC-CLP", side: "buy", amount: 1 }, client, cache);
|
|
766
|
+
assert(!result.isError, "should not be an error");
|
|
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");
|
|
769
|
+
} finally {
|
|
770
|
+
globalThis.fetch = savedFetch;
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
await test("simulate_order: invalid market_id returns INVALID_MARKET_ID error", async () => {
|
|
775
|
+
const fakeClient = {} as BudaClient;
|
|
776
|
+
const cache = new MemoryCache();
|
|
777
|
+
const result = await handleSimulateOrder({ market_id: "INVALID", side: "buy", amount: 1 }, fakeClient, cache);
|
|
778
|
+
assert(result.isError === true, "should be an error");
|
|
779
|
+
const parsed = JSON.parse(result.content[0].text) as { code: string };
|
|
780
|
+
assertEqual(parsed.code, "INVALID_MARKET_ID", "error code should be INVALID_MARKET_ID");
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
// ----------------------------------------------------------------
|
|
784
|
+
// j. calculate_position_size — position math
|
|
785
|
+
// ----------------------------------------------------------------
|
|
786
|
+
|
|
787
|
+
section("j. calculate_position_size");
|
|
788
|
+
|
|
789
|
+
await test("buy scenario: stop < entry, side = buy", () => {
|
|
790
|
+
const result = handleCalculatePositionSize({
|
|
791
|
+
market_id: "BTC-CLP",
|
|
792
|
+
capital: 1_000_000,
|
|
793
|
+
risk_pct: 2,
|
|
794
|
+
entry_price: 80_000_000,
|
|
795
|
+
stop_loss_price: 78_000_000,
|
|
796
|
+
});
|
|
797
|
+
assert(!result.isError, "should not be an error");
|
|
798
|
+
const parsed = JSON.parse(result.content[0].text) as {
|
|
799
|
+
side: string;
|
|
800
|
+
units: number;
|
|
801
|
+
capital_at_risk: number;
|
|
802
|
+
position_value: number;
|
|
803
|
+
fee_currency: string;
|
|
804
|
+
};
|
|
805
|
+
assertEqual(parsed.side, "buy", "side should be buy");
|
|
806
|
+
// capital_at_risk = 1_000_000 * 0.02 = 20_000; risk_per_unit = 2_000_000; units = 0.01
|
|
807
|
+
assertEqual(parsed.capital_at_risk, 20000, "capital_at_risk should be 20000");
|
|
808
|
+
assertEqual(parsed.units, 0.01, "units should be 0.01");
|
|
809
|
+
assertEqual(parsed.position_value, 800000, "position_value = 0.01 * 80_000_000");
|
|
810
|
+
assertEqual(parsed.fee_currency, "CLP", "fee_currency should be quote currency CLP");
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
await test("sell scenario: stop > entry, side = sell", () => {
|
|
814
|
+
const result = handleCalculatePositionSize({
|
|
815
|
+
market_id: "ETH-BTC",
|
|
816
|
+
capital: 1,
|
|
817
|
+
risk_pct: 1,
|
|
818
|
+
entry_price: 0.05,
|
|
819
|
+
stop_loss_price: 0.06,
|
|
820
|
+
});
|
|
821
|
+
assert(!result.isError, "should not be an error");
|
|
822
|
+
const parsed = JSON.parse(result.content[0].text) as { side: string; fee_currency: string };
|
|
823
|
+
assertEqual(parsed.side, "sell", "side should be sell");
|
|
824
|
+
assertEqual(parsed.fee_currency, "BTC", "fee_currency should be BTC");
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
await test("stop == entry returns INVALID_STOP_LOSS error", () => {
|
|
828
|
+
const result = handleCalculatePositionSize({
|
|
829
|
+
market_id: "BTC-CLP",
|
|
830
|
+
capital: 1_000_000,
|
|
831
|
+
risk_pct: 2,
|
|
832
|
+
entry_price: 80_000_000,
|
|
833
|
+
stop_loss_price: 80_000_000,
|
|
834
|
+
});
|
|
835
|
+
assert(result.isError === true, "should be an error");
|
|
836
|
+
const parsed = JSON.parse(result.content[0].text) as { code: string };
|
|
837
|
+
assertEqual(parsed.code, "INVALID_STOP_LOSS", "error code should be INVALID_STOP_LOSS");
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
await test("calculate_position_size: invalid market_id returns error", () => {
|
|
841
|
+
const result = handleCalculatePositionSize({
|
|
842
|
+
market_id: "bad",
|
|
843
|
+
capital: 100,
|
|
844
|
+
risk_pct: 1,
|
|
845
|
+
entry_price: 10,
|
|
846
|
+
stop_loss_price: 9,
|
|
847
|
+
});
|
|
848
|
+
assert(result.isError === true, "should be an error for invalid market_id");
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
// ----------------------------------------------------------------
|
|
852
|
+
// k. get_market_sentiment — scoring and labels
|
|
853
|
+
// ----------------------------------------------------------------
|
|
854
|
+
|
|
855
|
+
section("k. get_market_sentiment");
|
|
856
|
+
|
|
857
|
+
function makeMockFetchForSentiment(
|
|
858
|
+
priceVariation24h: string,
|
|
859
|
+
ask24h: string,
|
|
860
|
+
bid24h: string,
|
|
861
|
+
ask7d: string,
|
|
862
|
+
bid7d: string,
|
|
863
|
+
bid: string,
|
|
864
|
+
ask: string,
|
|
865
|
+
): typeof fetch {
|
|
866
|
+
const mockTicker = {
|
|
867
|
+
ticker: {
|
|
868
|
+
market_id: "BTC-CLP",
|
|
869
|
+
last_price: ["65000000", "CLP"],
|
|
870
|
+
max_bid: [bid, "CLP"],
|
|
871
|
+
min_ask: [ask, "CLP"],
|
|
872
|
+
volume: ["4.99", "BTC"],
|
|
873
|
+
price_variation_24h: priceVariation24h,
|
|
874
|
+
price_variation_7d: "0.05",
|
|
875
|
+
},
|
|
876
|
+
};
|
|
877
|
+
const mockVolume = {
|
|
878
|
+
volume: {
|
|
879
|
+
market_id: "BTC-CLP",
|
|
880
|
+
ask_volume_24h: [ask24h, "BTC"],
|
|
881
|
+
ask_volume_7d: [ask7d, "BTC"],
|
|
882
|
+
bid_volume_24h: [bid24h, "BTC"],
|
|
883
|
+
bid_volume_7d: [bid7d, "BTC"],
|
|
884
|
+
},
|
|
885
|
+
};
|
|
886
|
+
return async (url: string | URL): Promise<Response> => {
|
|
887
|
+
const urlStr = url.toString();
|
|
888
|
+
if (urlStr.includes("/volume")) {
|
|
889
|
+
return new Response(JSON.stringify(mockVolume), { status: 200 });
|
|
890
|
+
}
|
|
891
|
+
return new Response(JSON.stringify(mockTicker), { status: 200 });
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
await test("disclaimer is always present in sentiment output", async () => {
|
|
896
|
+
const savedFetch = globalThis.fetch;
|
|
897
|
+
globalThis.fetch = makeMockFetchForSentiment("0.01", "5", "5", "35", "35", "64900000", "65100000");
|
|
898
|
+
try {
|
|
899
|
+
const client = new BudaClient("https://www.buda.com/api/v2");
|
|
900
|
+
const cache = new MemoryCache();
|
|
901
|
+
const result = await handleMarketSentiment({ market_id: "BTC-CLP" }, client, cache);
|
|
902
|
+
assert(!result.isError, "should not be an error");
|
|
903
|
+
const parsed = JSON.parse(result.content[0].text) as { disclaimer: string };
|
|
904
|
+
assert(parsed.disclaimer.length > 0, "disclaimer should be present and non-empty");
|
|
905
|
+
} finally {
|
|
906
|
+
globalThis.fetch = savedFetch;
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
await test("neutral market produces label 'neutral' (score between -20 and 20)", async () => {
|
|
911
|
+
const savedFetch = globalThis.fetch;
|
|
912
|
+
// 0% price variation, volume ratio ~1 (neutral), spread at baseline
|
|
913
|
+
globalThis.fetch = makeMockFetchForSentiment("0", "5", "5", "35", "35", "64350000", "65000000");
|
|
914
|
+
try {
|
|
915
|
+
const client = new BudaClient("https://www.buda.com/api/v2");
|
|
916
|
+
const cache = new MemoryCache();
|
|
917
|
+
const result = await handleMarketSentiment({ market_id: "BTC-CLP" }, client, cache);
|
|
918
|
+
assert(!result.isError, "should not be an error");
|
|
919
|
+
const parsed = JSON.parse(result.content[0].text) as { label: string; score: number };
|
|
920
|
+
assert(
|
|
921
|
+
parsed.label === "neutral" || parsed.label === "bearish" || parsed.label === "bullish",
|
|
922
|
+
`label should be a valid sentiment: got ${parsed.label}`,
|
|
923
|
+
);
|
|
924
|
+
assert(typeof parsed.score === "number", "score should be a number");
|
|
925
|
+
} finally {
|
|
926
|
+
globalThis.fetch = savedFetch;
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
await test("strongly positive price variation produces bullish label", async () => {
|
|
931
|
+
const savedFetch = globalThis.fetch;
|
|
932
|
+
// +10% price variation → large positive price component → bullish
|
|
933
|
+
globalThis.fetch = makeMockFetchForSentiment("0.10", "10", "10", "35", "35", "64900000", "65100000");
|
|
934
|
+
try {
|
|
935
|
+
const client = new BudaClient("https://www.buda.com/api/v2");
|
|
936
|
+
const cache = new MemoryCache();
|
|
937
|
+
const result = await handleMarketSentiment({ market_id: "BTC-CLP" }, client, cache);
|
|
938
|
+
assert(!result.isError, "should not be an error");
|
|
939
|
+
const parsed = JSON.parse(result.content[0].text) as { label: string };
|
|
940
|
+
assertEqual(parsed.label, "bullish", "strong positive price change should yield bullish");
|
|
941
|
+
} finally {
|
|
942
|
+
globalThis.fetch = savedFetch;
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
await test("strongly negative price variation produces bearish label", async () => {
|
|
947
|
+
const savedFetch = globalThis.fetch;
|
|
948
|
+
// -10% price variation → bearish
|
|
949
|
+
globalThis.fetch = makeMockFetchForSentiment("-0.10", "5", "5", "35", "35", "64900000", "65100000");
|
|
950
|
+
try {
|
|
951
|
+
const client = new BudaClient("https://www.buda.com/api/v2");
|
|
952
|
+
const cache = new MemoryCache();
|
|
953
|
+
const result = await handleMarketSentiment({ market_id: "BTC-CLP" }, client, cache);
|
|
954
|
+
assert(!result.isError, "should not be an error");
|
|
955
|
+
const parsed = JSON.parse(result.content[0].text) as { label: string };
|
|
956
|
+
assertEqual(parsed.label, "bearish", "strong negative price change should yield bearish");
|
|
957
|
+
} finally {
|
|
958
|
+
globalThis.fetch = savedFetch;
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
await test("sentiment: invalid market_id returns error", async () => {
|
|
963
|
+
const fakeClient = {} as BudaClient;
|
|
964
|
+
const cache = new MemoryCache();
|
|
965
|
+
const result = await handleMarketSentiment({ market_id: "NOHYPHEN" }, fakeClient, cache);
|
|
966
|
+
assert(result.isError === true, "should be an error");
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
// ----------------------------------------------------------------
|
|
970
|
+
// l. get_technical_indicators — math and edge cases
|
|
971
|
+
// ----------------------------------------------------------------
|
|
972
|
+
|
|
973
|
+
section("l. get_technical_indicators");
|
|
974
|
+
|
|
975
|
+
await test("aggregateTradesToCandles: produces correct OHLCV from sorted trades", () => {
|
|
976
|
+
const now = Date.now();
|
|
977
|
+
const hourMs = 3600000;
|
|
978
|
+
const bucket = Math.floor(now / hourMs) * hourMs;
|
|
979
|
+
// 3 trades in the same 1h bucket
|
|
980
|
+
const entries: [string, string, string, string][] = [
|
|
981
|
+
[String(bucket + 1000), "0.1", "100", "buy"],
|
|
982
|
+
[String(bucket + 2000), "0.2", "110", "sell"],
|
|
983
|
+
[String(bucket + 3000), "0.05", "95", "buy"],
|
|
984
|
+
];
|
|
985
|
+
const candles = aggregateTradesToCandles(entries, "1h");
|
|
986
|
+
assertEqual(candles.length, 1, "should produce 1 candle");
|
|
987
|
+
const c = candles[0];
|
|
988
|
+
assertEqual(c.open, 100, "open = first trade price");
|
|
989
|
+
assertEqual(c.close, 95, "close = last trade price");
|
|
990
|
+
assertEqual(c.high, 110, "high = max trade price");
|
|
991
|
+
assertEqual(c.low, 95, "low = min trade price");
|
|
992
|
+
assertEqual(c.trade_count, 3, "trade_count = 3");
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
await test("technical indicators: insufficient candles returns warning", async () => {
|
|
996
|
+
const savedFetch = globalThis.fetch;
|
|
997
|
+
// Return only 5 trades — will produce < 50 candles
|
|
998
|
+
const now = Date.now();
|
|
999
|
+
const hourMs = 3600000;
|
|
1000
|
+
const entries = Array.from({ length: 5 }, (_, i) => {
|
|
1001
|
+
const ts = now - (50 - i) * hourMs - 1000;
|
|
1002
|
+
return [String(ts), "0.1", String(65000000 + i * 10000), "buy"];
|
|
1003
|
+
});
|
|
1004
|
+
const mockTrades = {
|
|
1005
|
+
trades: {
|
|
1006
|
+
market_id: "BTC-CLP",
|
|
1007
|
+
timestamp: String(now),
|
|
1008
|
+
last_timestamp: String(now),
|
|
1009
|
+
entries,
|
|
1010
|
+
},
|
|
1011
|
+
};
|
|
1012
|
+
globalThis.fetch = async (): Promise<Response> => {
|
|
1013
|
+
return new Response(JSON.stringify(mockTrades), { status: 200 });
|
|
1014
|
+
};
|
|
1015
|
+
try {
|
|
1016
|
+
const client = new BudaClient("https://www.buda.com/api/v2");
|
|
1017
|
+
const result = await handleTechnicalIndicators({ market_id: "BTC-CLP", period: "1h" }, client);
|
|
1018
|
+
assert(!result.isError, "should not be isError");
|
|
1019
|
+
const parsed = JSON.parse(result.content[0].text) as {
|
|
1020
|
+
warning: string;
|
|
1021
|
+
indicators: null;
|
|
1022
|
+
candles_available: number;
|
|
1023
|
+
minimum_required: number;
|
|
1024
|
+
};
|
|
1025
|
+
assertEqual(parsed.warning, "insufficient_data", "should return insufficient_data warning");
|
|
1026
|
+
assertEqual(parsed.indicators, null, "indicators should be null");
|
|
1027
|
+
assertEqual(parsed.minimum_required, 50, "minimum_required should be 50");
|
|
1028
|
+
} finally {
|
|
1029
|
+
globalThis.fetch = savedFetch;
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
await test("technical indicators: sufficient candles returns indicator values", async () => {
|
|
1034
|
+
const savedFetch = globalThis.fetch;
|
|
1035
|
+
// Generate 60 trades, each in its own 1h bucket, with steadily increasing prices
|
|
1036
|
+
const now = Date.now();
|
|
1037
|
+
const hourMs = 3600000;
|
|
1038
|
+
const entries = Array.from({ length: 60 }, (_, i) => {
|
|
1039
|
+
const ts = now - (60 - i) * hourMs - 1000;
|
|
1040
|
+
return [String(ts), "0.1", String(60_000_000 + i * 100_000), "buy"];
|
|
1041
|
+
});
|
|
1042
|
+
const mockTrades = {
|
|
1043
|
+
trades: {
|
|
1044
|
+
market_id: "BTC-CLP",
|
|
1045
|
+
timestamp: String(now),
|
|
1046
|
+
last_timestamp: String(now),
|
|
1047
|
+
entries,
|
|
1048
|
+
},
|
|
1049
|
+
};
|
|
1050
|
+
globalThis.fetch = async (): Promise<Response> => {
|
|
1051
|
+
return new Response(JSON.stringify(mockTrades), { status: 200 });
|
|
1052
|
+
};
|
|
1053
|
+
try {
|
|
1054
|
+
const client = new BudaClient("https://www.buda.com/api/v2");
|
|
1055
|
+
const result = await handleTechnicalIndicators({ market_id: "BTC-CLP", period: "1h" }, client);
|
|
1056
|
+
assert(!result.isError, "should not be isError");
|
|
1057
|
+
const parsed = JSON.parse(result.content[0].text) as {
|
|
1058
|
+
indicators: { rsi: number; sma_20: number; sma_50: number };
|
|
1059
|
+
signals: { rsi_signal: string };
|
|
1060
|
+
disclaimer: string;
|
|
1061
|
+
};
|
|
1062
|
+
assert(parsed.indicators !== null, "indicators should not be null");
|
|
1063
|
+
assert(typeof parsed.indicators.rsi === "number", "rsi should be a number");
|
|
1064
|
+
assert(typeof parsed.indicators.sma_20 === "number", "sma_20 should be a number");
|
|
1065
|
+
assert(typeof parsed.indicators.sma_50 === "number", "sma_50 should be a number");
|
|
1066
|
+
// Steadily increasing prices → RSI should be high (overbought)
|
|
1067
|
+
assertEqual(parsed.signals.rsi_signal, "overbought", "rising prices should yield overbought RSI");
|
|
1068
|
+
assert(parsed.disclaimer.length > 0, "disclaimer should be present");
|
|
1069
|
+
} finally {
|
|
1070
|
+
globalThis.fetch = savedFetch;
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
await test("technical indicators: invalid market_id returns error", async () => {
|
|
1075
|
+
const fakeClient = {} as BudaClient;
|
|
1076
|
+
const result = await handleTechnicalIndicators({ market_id: "BAD", period: "1h" }, fakeClient);
|
|
1077
|
+
assert(result.isError === true, "should be an error for invalid market_id");
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
// ----------------------------------------------------------------
|
|
1081
|
+
// m. schedule_cancel_all (dead man's switch)
|
|
1082
|
+
// ----------------------------------------------------------------
|
|
1083
|
+
|
|
1084
|
+
section("m. schedule_cancel_all (dead man's switch)");
|
|
1085
|
+
|
|
1086
|
+
await test("schedule_cancel_all: requires CONFIRM token", async () => {
|
|
1087
|
+
const fakeClient = {} as BudaClient;
|
|
1088
|
+
const result = await handleScheduleCancelAll(
|
|
1089
|
+
{ market_id: "BTC-CLP", ttl_seconds: 30, confirmation_token: "yes" },
|
|
1090
|
+
fakeClient,
|
|
1091
|
+
);
|
|
1092
|
+
assert(result.isError === true, "should return error without CONFIRM");
|
|
1093
|
+
const parsed = JSON.parse(result.content[0].text) as { code: string };
|
|
1094
|
+
assertEqual(parsed.code, "CONFIRMATION_REQUIRED", "error code should be CONFIRMATION_REQUIRED");
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
await test("schedule_cancel_all: invalid market_id returns error", async () => {
|
|
1098
|
+
const fakeClient = {} as BudaClient;
|
|
1099
|
+
const result = await handleScheduleCancelAll(
|
|
1100
|
+
{ market_id: "BAD", ttl_seconds: 30, confirmation_token: "CONFIRM" },
|
|
1101
|
+
fakeClient,
|
|
1102
|
+
);
|
|
1103
|
+
assert(result.isError === true, "should return error for invalid market_id");
|
|
1104
|
+
const parsed = JSON.parse(result.content[0].text) as { code: string };
|
|
1105
|
+
assertEqual(parsed.code, "INVALID_MARKET_ID", "error code should be INVALID_MARKET_ID");
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
await test("schedule_cancel_all: CONFIRM activates timer and returns expires_at", async () => {
|
|
1109
|
+
const fakeClient = {} as BudaClient;
|
|
1110
|
+
const before = Date.now();
|
|
1111
|
+
const result = await handleScheduleCancelAll(
|
|
1112
|
+
{ market_id: "BTC-USDT", ttl_seconds: 60, confirmation_token: "CONFIRM" },
|
|
1113
|
+
fakeClient,
|
|
1114
|
+
);
|
|
1115
|
+
const after = Date.now();
|
|
1116
|
+
assert(!result.isError, "should not be an error");
|
|
1117
|
+
const parsed = JSON.parse(result.content[0].text) as {
|
|
1118
|
+
active: boolean;
|
|
1119
|
+
expires_at: string;
|
|
1120
|
+
ttl_seconds: number;
|
|
1121
|
+
warning: string;
|
|
1122
|
+
};
|
|
1123
|
+
assertEqual(parsed.active, true, "active should be true");
|
|
1124
|
+
assertEqual(parsed.ttl_seconds, 60, "ttl_seconds should match");
|
|
1125
|
+
assert(parsed.warning.length > 0, "warning should be present");
|
|
1126
|
+
const expiresAt = new Date(parsed.expires_at).getTime();
|
|
1127
|
+
assert(expiresAt >= before + 60000 && expiresAt <= after + 60000, "expires_at should be ~60s from now");
|
|
1128
|
+
// Clean up timer
|
|
1129
|
+
handleDisarmCancelTimer({ market_id: "BTC-USDT" });
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
await test("renew_cancel_timer: returns NO_ACTIVE_TIMER when no timer exists", () => {
|
|
1133
|
+
const fakeClient = {} as BudaClient;
|
|
1134
|
+
const result = handleRenewCancelTimer({ market_id: "ETH-CLP" }, fakeClient);
|
|
1135
|
+
assert(result.isError === true, "should return error if no timer active");
|
|
1136
|
+
const parsed = JSON.parse(result.content[0].text) as { code: string };
|
|
1137
|
+
assertEqual(parsed.code, "NO_ACTIVE_TIMER", "error code should be NO_ACTIVE_TIMER");
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
await test("disarm_cancel_timer: no-op returns disarmed:false when no timer exists", () => {
|
|
1141
|
+
const result = handleDisarmCancelTimer({ market_id: "LTC-CLP" });
|
|
1142
|
+
assert(!result.isError, "disarm should not error even if no timer");
|
|
1143
|
+
const parsed = JSON.parse(result.content[0].text) as { disarmed: boolean };
|
|
1144
|
+
assertEqual(parsed.disarmed, false, "disarmed should be false when no timer existed");
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
await test("disarm after arm: timer is cleared", async () => {
|
|
1148
|
+
const fakeClient = {} as BudaClient;
|
|
1149
|
+
await handleScheduleCancelAll(
|
|
1150
|
+
{ market_id: "BCH-CLP", ttl_seconds: 300, confirmation_token: "CONFIRM" },
|
|
1151
|
+
fakeClient,
|
|
1152
|
+
);
|
|
1153
|
+
const result = handleDisarmCancelTimer({ market_id: "BCH-CLP" });
|
|
1154
|
+
assert(!result.isError, "disarm should not error");
|
|
1155
|
+
const parsed = JSON.parse(result.content[0].text) as { disarmed: boolean };
|
|
1156
|
+
assertEqual(parsed.disarmed, true, "disarmed should be true after an active timer was cleared");
|
|
1157
|
+
// Confirm no timer remains
|
|
1158
|
+
const renewResult = handleRenewCancelTimer({ market_id: "BCH-CLP" }, fakeClient);
|
|
1159
|
+
assert(renewResult.isError === true, "should have no timer left after disarm");
|
|
1160
|
+
});
|
|
1161
|
+
|
|
404
1162
|
// ----------------------------------------------------------------
|
|
405
1163
|
// Summary
|
|
406
1164
|
// ----------------------------------------------------------------
|