perp-cli 0.3.3
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/LICENSE +21 -0
- package/README.md +293 -0
- package/dist/__tests__/alert-logic.test.d.ts +1 -0
- package/dist/__tests__/alert-logic.test.js +107 -0
- package/dist/__tests__/arb-auto-3dex.test.d.ts +1 -0
- package/dist/__tests__/arb-auto-3dex.test.js +397 -0
- package/dist/__tests__/arb-history-stats.test.d.ts +1 -0
- package/dist/__tests__/arb-history-stats.test.js +176 -0
- package/dist/__tests__/arb-logic.test.d.ts +1 -0
- package/dist/__tests__/arb-logic.test.js +84 -0
- package/dist/__tests__/arb-manage.test.d.ts +1 -0
- package/dist/__tests__/arb-manage.test.js +253 -0
- package/dist/__tests__/arb-new-features.test.d.ts +1 -0
- package/dist/__tests__/arb-new-features.test.js +457 -0
- package/dist/__tests__/arb-sizing.test.d.ts +1 -0
- package/dist/__tests__/arb-sizing.test.js +48 -0
- package/dist/__tests__/arb-state.test.d.ts +1 -0
- package/dist/__tests__/arb-state.test.js +284 -0
- package/dist/__tests__/arb-userflow.test.d.ts +1 -0
- package/dist/__tests__/arb-userflow.test.js +945 -0
- package/dist/__tests__/arb-utils.test.d.ts +1 -0
- package/dist/__tests__/arb-utils.test.js +264 -0
- package/dist/__tests__/bot-conditions.test.d.ts +1 -0
- package/dist/__tests__/bot-conditions.test.js +341 -0
- package/dist/__tests__/client-id-tracker.test.d.ts +1 -0
- package/dist/__tests__/client-id-tracker.test.js +137 -0
- package/dist/__tests__/commands/new-atomic-commands.test.d.ts +1 -0
- package/dist/__tests__/commands/new-atomic-commands.test.js +502 -0
- package/dist/__tests__/commands/order-intent.test.d.ts +1 -0
- package/dist/__tests__/commands/order-intent.test.js +600 -0
- package/dist/__tests__/commands/trade-commands.test.d.ts +1 -0
- package/dist/__tests__/commands/trade-commands.test.js +821 -0
- package/dist/__tests__/config.test.d.ts +1 -0
- package/dist/__tests__/config.test.js +86 -0
- package/dist/__tests__/cross-chain-margin.test.d.ts +1 -0
- package/dist/__tests__/cross-chain-margin.test.js +287 -0
- package/dist/__tests__/dex-asset-map.test.d.ts +1 -0
- package/dist/__tests__/dex-asset-map.test.js +191 -0
- package/dist/__tests__/errors.test.d.ts +1 -0
- package/dist/__tests__/errors.test.js +110 -0
- package/dist/__tests__/event-stream.test.d.ts +1 -0
- package/dist/__tests__/event-stream.test.js +276 -0
- package/dist/__tests__/exchanges/interface.test.d.ts +1 -0
- package/dist/__tests__/exchanges/interface.test.js +132 -0
- package/dist/__tests__/exchanges/mock-adapter.d.ts +69 -0
- package/dist/__tests__/exchanges/mock-adapter.js +137 -0
- package/dist/__tests__/execution-log.test.d.ts +1 -0
- package/dist/__tests__/execution-log.test.js +106 -0
- package/dist/__tests__/funding-calc.test.d.ts +1 -0
- package/dist/__tests__/funding-calc.test.js +71 -0
- package/dist/__tests__/funding-history.test.d.ts +1 -0
- package/dist/__tests__/funding-history.test.js +343 -0
- package/dist/__tests__/funding-rates.test.d.ts +1 -0
- package/dist/__tests__/funding-rates.test.js +342 -0
- package/dist/__tests__/funding.test.d.ts +1 -0
- package/dist/__tests__/funding.test.js +173 -0
- package/dist/__tests__/gap-logic.test.d.ts +1 -0
- package/dist/__tests__/gap-logic.test.js +43 -0
- package/dist/__tests__/hip3-dex.test.d.ts +1 -0
- package/dist/__tests__/hip3-dex.test.js +234 -0
- package/dist/__tests__/integration/agent-features.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/agent-features.integration.test.js +553 -0
- package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +13 -0
- package/dist/__tests__/integration/atomic-commands.integration.test.js +246 -0
- package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/bridge-simulation.integration.test.js +453 -0
- package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/bridge-strict.integration.test.js +812 -0
- package/dist/__tests__/integration/bridge.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/bridge.integration.test.js +309 -0
- package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/cli-e2e.integration.test.js +202 -0
- package/dist/__tests__/integration/dex-arb.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/dex-arb.integration.test.js +116 -0
- package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +13 -0
- package/dist/__tests__/integration/envelope-consistency.integration.test.js +205 -0
- package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/hip3-dex.integration.test.js +147 -0
- package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/hyperliquid.integration.test.js +79 -0
- package/dist/__tests__/integration/lighter.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/lighter.integration.test.js +53 -0
- package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +9 -0
- package/dist/__tests__/integration/new-commands-e2e.integration.test.js +236 -0
- package/dist/__tests__/integration/order-verification.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/order-verification.integration.test.js +321 -0
- package/dist/__tests__/integration/pacifica.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/pacifica.integration.test.js +75 -0
- package/dist/__tests__/integration/response-shapes.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/response-shapes.integration.test.js +278 -0
- package/dist/__tests__/liquidity.test.d.ts +1 -0
- package/dist/__tests__/liquidity.test.js +225 -0
- package/dist/__tests__/plan-executor.test.d.ts +1 -0
- package/dist/__tests__/plan-executor.test.js +314 -0
- package/dist/__tests__/position-history.test.d.ts +1 -0
- package/dist/__tests__/position-history.test.js +367 -0
- package/dist/__tests__/retry.test.d.ts +1 -0
- package/dist/__tests__/retry.test.js +310 -0
- package/dist/__tests__/risk-assessment.test.d.ts +1 -0
- package/dist/__tests__/risk-assessment.test.js +145 -0
- package/dist/__tests__/security-adversarial.test.d.ts +1 -0
- package/dist/__tests__/security-adversarial.test.js +574 -0
- package/dist/__tests__/strategies.test.d.ts +1 -0
- package/dist/__tests__/strategies.test.js +539 -0
- package/dist/__tests__/trade-execution.test.d.ts +1 -0
- package/dist/__tests__/trade-execution.test.js +129 -0
- package/dist/__tests__/trade-validator.test.d.ts +1 -0
- package/dist/__tests__/trade-validator.test.js +655 -0
- package/dist/__tests__/utils.test.d.ts +1 -0
- package/dist/__tests__/utils.test.js +76 -0
- package/dist/api/public/hyperliquid.d.ts +18 -0
- package/dist/api/public/hyperliquid.js +82 -0
- package/dist/api/public/index.d.ts +8 -0
- package/dist/api/public/index.js +8 -0
- package/dist/api/public/lighter.d.ts +24 -0
- package/dist/api/public/lighter.js +100 -0
- package/dist/api/public/pacifica.d.ts +17 -0
- package/dist/api/public/pacifica.js +54 -0
- package/dist/api/public/urls.d.ts +12 -0
- package/dist/api/public/urls.js +33 -0
- package/dist/arb/history-stats.d.ts +44 -0
- package/dist/arb/history-stats.js +135 -0
- package/dist/arb/index.d.ts +4 -0
- package/dist/arb/index.js +4 -0
- package/dist/arb/sizing.d.ts +23 -0
- package/dist/arb/sizing.js +96 -0
- package/dist/arb/state.d.ts +51 -0
- package/dist/arb/state.js +112 -0
- package/dist/arb/utils.d.ts +81 -0
- package/dist/arb/utils.js +267 -0
- package/dist/arb-history-stats.d.ts +5 -0
- package/dist/arb-history-stats.js +5 -0
- package/dist/arb-sizing.d.ts +5 -0
- package/dist/arb-sizing.js +5 -0
- package/dist/arb-state.d.ts +5 -0
- package/dist/arb-state.js +5 -0
- package/dist/arb-utils.d.ts +5 -0
- package/dist/arb-utils.js +5 -0
- package/dist/bot/conditions.d.ts +32 -0
- package/dist/bot/conditions.js +141 -0
- package/dist/bot/config.d.ts +76 -0
- package/dist/bot/config.js +160 -0
- package/dist/bot/engine.d.ts +8 -0
- package/dist/bot/engine.js +519 -0
- package/dist/bot/presets.d.ts +11 -0
- package/dist/bot/presets.js +296 -0
- package/dist/bridge-engine.d.ts +133 -0
- package/dist/bridge-engine.js +1487 -0
- package/dist/cache.d.ts +25 -0
- package/dist/cache.js +99 -0
- package/dist/cli-spec.d.ts +50 -0
- package/dist/cli-spec.js +75 -0
- package/dist/client-id-tracker.d.ts +25 -0
- package/dist/client-id-tracker.js +76 -0
- package/dist/commands/account.d.ts +3 -0
- package/dist/commands/account.js +425 -0
- package/dist/commands/agent.d.ts +3 -0
- package/dist/commands/agent.js +386 -0
- package/dist/commands/alert.d.ts +2 -0
- package/dist/commands/alert.js +421 -0
- package/dist/commands/analytics.d.ts +3 -0
- package/dist/commands/analytics.js +311 -0
- package/dist/commands/arb/index.d.ts +3 -0
- package/dist/commands/arb/index.js +921 -0
- package/dist/commands/arb-auto.d.ts +54 -0
- package/dist/commands/arb-auto.js +1328 -0
- package/dist/commands/arb-manage.d.ts +5 -0
- package/dist/commands/arb-manage.js +5 -0
- package/dist/commands/arb.d.ts +2 -0
- package/dist/commands/arb.js +347 -0
- package/dist/commands/backtest.d.ts +2 -0
- package/dist/commands/backtest.js +327 -0
- package/dist/commands/bot.d.ts +3 -0
- package/dist/commands/bot.js +412 -0
- package/dist/commands/bridge.d.ts +2 -0
- package/dist/commands/bridge.js +396 -0
- package/dist/commands/dashboard.d.ts +3 -0
- package/dist/commands/dashboard.js +176 -0
- package/dist/commands/deposit.d.ts +4 -0
- package/dist/commands/deposit.js +573 -0
- package/dist/commands/dex.d.ts +3 -0
- package/dist/commands/dex.js +114 -0
- package/dist/commands/env.d.ts +2 -0
- package/dist/commands/env.js +136 -0
- package/dist/commands/funding.d.ts +2 -0
- package/dist/commands/funding.js +347 -0
- package/dist/commands/gap.d.ts +2 -0
- package/dist/commands/gap.js +305 -0
- package/dist/commands/health.d.ts +2 -0
- package/dist/commands/health.js +67 -0
- package/dist/commands/history.d.ts +2 -0
- package/dist/commands/history.js +235 -0
- package/dist/commands/init.d.ts +15 -0
- package/dist/commands/init.js +266 -0
- package/dist/commands/jobs.d.ts +2 -0
- package/dist/commands/jobs.js +133 -0
- package/dist/commands/manage.d.ts +4 -0
- package/dist/commands/manage.js +309 -0
- package/dist/commands/market.d.ts +3 -0
- package/dist/commands/market.js +225 -0
- package/dist/commands/plan.d.ts +3 -0
- package/dist/commands/plan.js +95 -0
- package/dist/commands/portfolio.d.ts +3 -0
- package/dist/commands/portfolio.js +169 -0
- package/dist/commands/rebalance.d.ts +3 -0
- package/dist/commands/rebalance.js +293 -0
- package/dist/commands/risk.d.ts +3 -0
- package/dist/commands/risk.js +169 -0
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.js +202 -0
- package/dist/commands/settings.d.ts +2 -0
- package/dist/commands/settings.js +102 -0
- package/dist/commands/stream.d.ts +5 -0
- package/dist/commands/stream.js +123 -0
- package/dist/commands/trade.d.ts +3 -0
- package/dist/commands/trade.js +1273 -0
- package/dist/commands/wallet.d.ts +14 -0
- package/dist/commands/wallet.js +602 -0
- package/dist/commands/withdraw.d.ts +3 -0
- package/dist/commands/withdraw.js +187 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +68 -0
- package/dist/cross-chain-margin.d.ts +46 -0
- package/dist/cross-chain-margin.js +107 -0
- package/dist/dashboard/server.d.ts +80 -0
- package/dist/dashboard/server.js +340 -0
- package/dist/dashboard/ui.d.ts +4 -0
- package/dist/dashboard/ui.js +538 -0
- package/dist/dashboard/ws-feeds.d.ts +29 -0
- package/dist/dashboard/ws-feeds.js +660 -0
- package/dist/dex-asset-map.d.ts +80 -0
- package/dist/dex-asset-map.js +201 -0
- package/dist/errors.d.ts +109 -0
- package/dist/errors.js +84 -0
- package/dist/event-stream.d.ts +25 -0
- package/dist/event-stream.js +168 -0
- package/dist/exchanges/hyperliquid.d.ts +212 -0
- package/dist/exchanges/hyperliquid.js +931 -0
- package/dist/exchanges/interface.d.ts +95 -0
- package/dist/exchanges/interface.js +5 -0
- package/dist/exchanges/lighter.d.ts +159 -0
- package/dist/exchanges/lighter.js +793 -0
- package/dist/exchanges/pacifica.d.ts +51 -0
- package/dist/exchanges/pacifica.js +248 -0
- package/dist/execution-log.d.ts +36 -0
- package/dist/execution-log.js +102 -0
- package/dist/funding/history.d.ts +63 -0
- package/dist/funding/history.js +266 -0
- package/dist/funding/index.d.ts +3 -0
- package/dist/funding/index.js +3 -0
- package/dist/funding/normalize.d.ts +39 -0
- package/dist/funding/normalize.js +66 -0
- package/dist/funding/rates.d.ts +45 -0
- package/dist/funding/rates.js +172 -0
- package/dist/funding-history.d.ts +5 -0
- package/dist/funding-history.js +5 -0
- package/dist/funding-rates.d.ts +5 -0
- package/dist/funding-rates.js +5 -0
- package/dist/funding.d.ts +5 -0
- package/dist/funding.js +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +458 -0
- package/dist/jobs.d.ts +37 -0
- package/dist/jobs.js +152 -0
- package/dist/liquidity.d.ts +34 -0
- package/dist/liquidity.js +100 -0
- package/dist/mcp-server.d.ts +9 -0
- package/dist/mcp-server.js +1206 -0
- package/dist/pacifica/client.d.ts +111 -0
- package/dist/pacifica/client.js +310 -0
- package/dist/pacifica/constants.d.ts +27 -0
- package/dist/pacifica/constants.js +47 -0
- package/dist/pacifica/deposit.d.ts +14 -0
- package/dist/pacifica/deposit.js +78 -0
- package/dist/pacifica/index.d.ts +6 -0
- package/dist/pacifica/index.js +11 -0
- package/dist/pacifica/signing.d.ts +49 -0
- package/dist/pacifica/signing.js +97 -0
- package/dist/pacifica/types/account.d.ts +42 -0
- package/dist/pacifica/types/account.js +1 -0
- package/dist/pacifica/types/index.d.ts +6 -0
- package/dist/pacifica/types/index.js +6 -0
- package/dist/pacifica/types/lake.d.ts +18 -0
- package/dist/pacifica/types/lake.js +1 -0
- package/dist/pacifica/types/market.d.ts +64 -0
- package/dist/pacifica/types/market.js +1 -0
- package/dist/pacifica/types/order.d.ts +92 -0
- package/dist/pacifica/types/order.js +1 -0
- package/dist/pacifica/types/position.d.ts +25 -0
- package/dist/pacifica/types/position.js +1 -0
- package/dist/pacifica/types/ws.d.ts +34 -0
- package/dist/pacifica/types/ws.js +41 -0
- package/dist/pacifica/ws-client.d.ts +42 -0
- package/dist/pacifica/ws-client.js +180 -0
- package/dist/plan-executor.d.ts +48 -0
- package/dist/plan-executor.js +280 -0
- package/dist/position-history.d.ts +68 -0
- package/dist/position-history.js +222 -0
- package/dist/rebalance.d.ts +64 -0
- package/dist/rebalance.js +142 -0
- package/dist/retry.d.ts +74 -0
- package/dist/retry.js +129 -0
- package/dist/risk.d.ts +48 -0
- package/dist/risk.js +156 -0
- package/dist/settings.d.ts +19 -0
- package/dist/settings.js +45 -0
- package/dist/shared-api.d.ts +5 -0
- package/dist/shared-api.js +5 -0
- package/dist/strategies/dca.d.ts +25 -0
- package/dist/strategies/dca.js +114 -0
- package/dist/strategies/funding-arb.d.ts +15 -0
- package/dist/strategies/funding-arb.js +281 -0
- package/dist/strategies/grid.d.ts +34 -0
- package/dist/strategies/grid.js +185 -0
- package/dist/strategies/trailing-stop.d.ts +17 -0
- package/dist/strategies/trailing-stop.js +121 -0
- package/dist/strategies/twap.d.ts +20 -0
- package/dist/strategies/twap.js +78 -0
- package/dist/trade-validator.d.ts +39 -0
- package/dist/trade-validator.js +154 -0
- package/dist/utils.d.ts +38 -0
- package/dist/utils.js +110 -0
- package/package.json +63 -0
- package/skills/perp-cli/SKILL.md +149 -0
- package/skills/perp-cli/references/commands.md +143 -0
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
// ══════════════════════════════════════════════
|
|
3
|
+
// Strategy Calculators — Pure Logic Tests
|
|
4
|
+
//
|
|
5
|
+
// These tests focus on the calculation logic in the strategy modules,
|
|
6
|
+
// not the I/O-heavy run loops. We test:
|
|
7
|
+
// - TWAP: slice sizing, interval computation, last-slice remainder
|
|
8
|
+
// - Grid: validation, grid line generation, step calculation
|
|
9
|
+
// - DCA: parameter handling, state initialization
|
|
10
|
+
// ══════════════════════════════════════════════
|
|
11
|
+
// We import types and test the computational parts by exercising
|
|
12
|
+
// the functions with mocked adapters, using short timeouts.
|
|
13
|
+
// Mock jobs module to prevent file I/O
|
|
14
|
+
vi.mock("../jobs.js", () => ({
|
|
15
|
+
updateJobState: vi.fn(),
|
|
16
|
+
}));
|
|
17
|
+
// ── Mock adapter factory ──
|
|
18
|
+
function mockAdapter(overrides) {
|
|
19
|
+
return {
|
|
20
|
+
name: "test",
|
|
21
|
+
getMarkets: vi.fn().mockResolvedValue([
|
|
22
|
+
{ symbol: "BTC", markPrice: "60000", indexPrice: "60000", fundingRate: "0.0001", volume24h: "1000000", openInterest: "500000", maxLeverage: 20 },
|
|
23
|
+
]),
|
|
24
|
+
getBalance: vi.fn().mockResolvedValue({ equity: "10000", available: "8000", marginUsed: "2000", unrealizedPnl: "0" }),
|
|
25
|
+
getPositions: vi.fn().mockResolvedValue([]),
|
|
26
|
+
getOpenOrders: vi.fn().mockResolvedValue([]),
|
|
27
|
+
getOrderbook: vi.fn().mockResolvedValue({ bids: [["59990", "1"]], asks: [["60010", "1"]] }),
|
|
28
|
+
getOrderHistory: vi.fn().mockResolvedValue([]),
|
|
29
|
+
getTradeHistory: vi.fn().mockResolvedValue([]),
|
|
30
|
+
getRecentTrades: vi.fn().mockResolvedValue([]),
|
|
31
|
+
getFundingHistory: vi.fn().mockResolvedValue([]),
|
|
32
|
+
getFundingPayments: vi.fn().mockResolvedValue([]),
|
|
33
|
+
getKlines: vi.fn().mockResolvedValue([]),
|
|
34
|
+
marketOrder: vi.fn().mockResolvedValue({ orderId: "m1", price: "60000" }),
|
|
35
|
+
limitOrder: vi.fn().mockResolvedValue({ orderId: "l1" }),
|
|
36
|
+
editOrder: vi.fn().mockResolvedValue({}),
|
|
37
|
+
cancelOrder: vi.fn().mockResolvedValue({}),
|
|
38
|
+
cancelAllOrders: vi.fn().mockResolvedValue({}),
|
|
39
|
+
setLeverage: vi.fn().mockResolvedValue({}),
|
|
40
|
+
stopOrder: vi.fn().mockResolvedValue({}),
|
|
41
|
+
...overrides,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// ══════════════════════════════════════════════
|
|
45
|
+
// TWAP Strategy Tests
|
|
46
|
+
// ══════════════════════════════════════════════
|
|
47
|
+
describe("TWAP — runTWAP", () => {
|
|
48
|
+
it("computes correct totalSlices from duration (default: 1 slice per 30s)", async () => {
|
|
49
|
+
const { runTWAP } = await import("../strategies/twap.js");
|
|
50
|
+
const adapter = mockAdapter();
|
|
51
|
+
const log = vi.fn();
|
|
52
|
+
const result = await runTWAP(adapter, {
|
|
53
|
+
symbol: "BTC",
|
|
54
|
+
side: "buy",
|
|
55
|
+
totalSize: 1.0,
|
|
56
|
+
durationSec: 3, // 3 seconds / 30 = 0.1 → Math.max(floor, 2) = 2 slices
|
|
57
|
+
}, undefined, log);
|
|
58
|
+
expect(result.totalSlices).toBe(2);
|
|
59
|
+
expect(adapter.marketOrder).toHaveBeenCalledTimes(2);
|
|
60
|
+
});
|
|
61
|
+
it("uses custom slice count when provided", async () => {
|
|
62
|
+
const { runTWAP } = await import("../strategies/twap.js");
|
|
63
|
+
const adapter = mockAdapter();
|
|
64
|
+
const log = vi.fn();
|
|
65
|
+
const result = await runTWAP(adapter, {
|
|
66
|
+
symbol: "BTC",
|
|
67
|
+
side: "sell",
|
|
68
|
+
totalSize: 0.5,
|
|
69
|
+
durationSec: 5,
|
|
70
|
+
slices: 5,
|
|
71
|
+
}, undefined, log);
|
|
72
|
+
expect(result.totalSlices).toBe(5);
|
|
73
|
+
expect(adapter.marketOrder).toHaveBeenCalledTimes(5);
|
|
74
|
+
});
|
|
75
|
+
it("fills the exact total size across all slices (no dust)", async () => {
|
|
76
|
+
const { runTWAP } = await import("../strategies/twap.js");
|
|
77
|
+
const adapter = mockAdapter();
|
|
78
|
+
const log = vi.fn();
|
|
79
|
+
const result = await runTWAP(adapter, {
|
|
80
|
+
symbol: "BTC",
|
|
81
|
+
side: "buy",
|
|
82
|
+
totalSize: 1.0,
|
|
83
|
+
durationSec: 1,
|
|
84
|
+
slices: 3,
|
|
85
|
+
}, undefined, log);
|
|
86
|
+
expect(result.filled).toBeCloseTo(1.0);
|
|
87
|
+
});
|
|
88
|
+
it("last slice handles remainder correctly", async () => {
|
|
89
|
+
const { runTWAP } = await import("../strategies/twap.js");
|
|
90
|
+
const adapter = mockAdapter();
|
|
91
|
+
const log = vi.fn();
|
|
92
|
+
// 1.0 / 3 = 0.3333... per slice. Last slice should do remaining.
|
|
93
|
+
await runTWAP(adapter, {
|
|
94
|
+
symbol: "BTC",
|
|
95
|
+
side: "buy",
|
|
96
|
+
totalSize: 1.0,
|
|
97
|
+
durationSec: 1,
|
|
98
|
+
slices: 3,
|
|
99
|
+
}, undefined, log);
|
|
100
|
+
const calls = adapter.marketOrder.mock.calls;
|
|
101
|
+
expect(calls).toHaveLength(3);
|
|
102
|
+
// First two slices: 1/3 = 0.3333...
|
|
103
|
+
const slice1 = parseFloat(calls[0][2]);
|
|
104
|
+
const slice2 = parseFloat(calls[1][2]);
|
|
105
|
+
const slice3 = parseFloat(calls[2][2]);
|
|
106
|
+
expect(slice1).toBeCloseTo(1 / 3, 4);
|
|
107
|
+
expect(slice2).toBeCloseTo(1 / 3, 4);
|
|
108
|
+
// Last slice = remainder (handles dust)
|
|
109
|
+
expect(slice1 + slice2 + slice3).toBeCloseTo(1.0, 6);
|
|
110
|
+
});
|
|
111
|
+
it("handles order errors without aborting (continues other slices)", async () => {
|
|
112
|
+
const { runTWAP } = await import("../strategies/twap.js");
|
|
113
|
+
let callCount = 0;
|
|
114
|
+
const adapter = mockAdapter({
|
|
115
|
+
marketOrder: vi.fn().mockImplementation(() => {
|
|
116
|
+
callCount++;
|
|
117
|
+
if (callCount === 2)
|
|
118
|
+
return Promise.reject(new Error("temporary error"));
|
|
119
|
+
return Promise.resolve({ orderId: "ok", price: "60000" });
|
|
120
|
+
}),
|
|
121
|
+
});
|
|
122
|
+
const log = vi.fn();
|
|
123
|
+
const result = await runTWAP(adapter, {
|
|
124
|
+
symbol: "BTC",
|
|
125
|
+
side: "buy",
|
|
126
|
+
totalSize: 1.0,
|
|
127
|
+
durationSec: 1,
|
|
128
|
+
slices: 4,
|
|
129
|
+
}, undefined, log);
|
|
130
|
+
expect(result.errors).toBe(1);
|
|
131
|
+
expect(result.slicesDone).toBe(3); // 4 attempts, 1 failed, 3 succeeded
|
|
132
|
+
});
|
|
133
|
+
it("aborts when too many errors (>50% of slices)", async () => {
|
|
134
|
+
const { runTWAP } = await import("../strategies/twap.js");
|
|
135
|
+
const adapter = mockAdapter({
|
|
136
|
+
marketOrder: vi.fn().mockRejectedValue(new Error("always fails")),
|
|
137
|
+
});
|
|
138
|
+
const log = vi.fn();
|
|
139
|
+
const result = await runTWAP(adapter, {
|
|
140
|
+
symbol: "BTC",
|
|
141
|
+
side: "buy",
|
|
142
|
+
totalSize: 1.0,
|
|
143
|
+
durationSec: 1,
|
|
144
|
+
slices: 4,
|
|
145
|
+
}, undefined, log);
|
|
146
|
+
// Should abort after 3rd error (3 > 4*0.5)
|
|
147
|
+
expect(result.errors).toBeGreaterThan(0);
|
|
148
|
+
expect(result.slicesDone).toBe(0);
|
|
149
|
+
expect(log).toHaveBeenCalledWith(expect.stringContaining("Too many errors"));
|
|
150
|
+
});
|
|
151
|
+
it("passes correct side to market orders", async () => {
|
|
152
|
+
const { runTWAP } = await import("../strategies/twap.js");
|
|
153
|
+
const adapter = mockAdapter();
|
|
154
|
+
const log = vi.fn();
|
|
155
|
+
await runTWAP(adapter, {
|
|
156
|
+
symbol: "ETH",
|
|
157
|
+
side: "sell",
|
|
158
|
+
totalSize: 2.0,
|
|
159
|
+
durationSec: 1,
|
|
160
|
+
slices: 2,
|
|
161
|
+
}, undefined, log);
|
|
162
|
+
for (const call of adapter.marketOrder.mock.calls) {
|
|
163
|
+
expect(call[0]).toBe("ETH");
|
|
164
|
+
expect(call[1]).toBe("sell");
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
// ══════════════════════════════════════════════
|
|
169
|
+
// Grid Strategy Tests
|
|
170
|
+
// ══════════════════════════════════════════════
|
|
171
|
+
describe("Grid — runGrid validation", () => {
|
|
172
|
+
it("rejects upperPrice <= lowerPrice", async () => {
|
|
173
|
+
const { runGrid } = await import("../strategies/grid.js");
|
|
174
|
+
const adapter = mockAdapter();
|
|
175
|
+
await expect(runGrid(adapter, {
|
|
176
|
+
symbol: "BTC",
|
|
177
|
+
side: "neutral",
|
|
178
|
+
upperPrice: 100,
|
|
179
|
+
lowerPrice: 100,
|
|
180
|
+
grids: 5,
|
|
181
|
+
totalSize: 1,
|
|
182
|
+
})).rejects.toThrow("upperPrice must be > lowerPrice");
|
|
183
|
+
});
|
|
184
|
+
it("rejects fewer than 2 grid lines", async () => {
|
|
185
|
+
const { runGrid } = await import("../strategies/grid.js");
|
|
186
|
+
const adapter = mockAdapter();
|
|
187
|
+
await expect(runGrid(adapter, {
|
|
188
|
+
symbol: "BTC",
|
|
189
|
+
side: "neutral",
|
|
190
|
+
upperPrice: 200,
|
|
191
|
+
lowerPrice: 100,
|
|
192
|
+
grids: 1,
|
|
193
|
+
totalSize: 1,
|
|
194
|
+
})).rejects.toThrow("at least 2 grid lines");
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
describe("Grid — order placement", () => {
|
|
198
|
+
it("places correct number of grid orders", async () => {
|
|
199
|
+
const { runGrid } = await import("../strategies/grid.js");
|
|
200
|
+
const adapter = mockAdapter({
|
|
201
|
+
// Return no open orders to prevent fill-monitoring from running
|
|
202
|
+
getOpenOrders: vi.fn().mockResolvedValue(
|
|
203
|
+
// Return all grid order IDs as still open to prevent fill loop churn
|
|
204
|
+
Array.from({ length: 5 }, (_, i) => ({
|
|
205
|
+
orderId: "l1",
|
|
206
|
+
symbol: "BTC",
|
|
207
|
+
side: "buy",
|
|
208
|
+
price: String(59000 + i * 500),
|
|
209
|
+
size: "0.2",
|
|
210
|
+
filled: "0",
|
|
211
|
+
status: "open",
|
|
212
|
+
type: "limit",
|
|
213
|
+
}))),
|
|
214
|
+
});
|
|
215
|
+
const log = vi.fn();
|
|
216
|
+
// Use maxRuntime to stop quickly
|
|
217
|
+
const resultPromise = runGrid(adapter, {
|
|
218
|
+
symbol: "BTC",
|
|
219
|
+
side: "neutral",
|
|
220
|
+
upperPrice: 61000,
|
|
221
|
+
lowerPrice: 59000,
|
|
222
|
+
grids: 5,
|
|
223
|
+
totalSize: 1,
|
|
224
|
+
intervalSec: 0.01,
|
|
225
|
+
maxRuntime: 0.05, // stop after 50ms
|
|
226
|
+
}, undefined, log);
|
|
227
|
+
const result = await resultPromise;
|
|
228
|
+
// Should have placed 5 initial grid orders
|
|
229
|
+
expect(adapter.limitOrder).toHaveBeenCalledTimes(5);
|
|
230
|
+
});
|
|
231
|
+
it("computes correct step size", async () => {
|
|
232
|
+
const { runGrid } = await import("../strategies/grid.js");
|
|
233
|
+
const adapter = mockAdapter({
|
|
234
|
+
getOpenOrders: vi.fn().mockResolvedValue(Array.from({ length: 3 }, () => ({
|
|
235
|
+
orderId: "l1", symbol: "BTC", side: "buy", price: "60000", size: "0.1",
|
|
236
|
+
filled: "0", status: "open", type: "limit",
|
|
237
|
+
}))),
|
|
238
|
+
});
|
|
239
|
+
const log = vi.fn();
|
|
240
|
+
await runGrid(adapter, {
|
|
241
|
+
symbol: "BTC",
|
|
242
|
+
side: "neutral",
|
|
243
|
+
upperPrice: 62000,
|
|
244
|
+
lowerPrice: 60000,
|
|
245
|
+
grids: 3,
|
|
246
|
+
totalSize: 0.3,
|
|
247
|
+
intervalSec: 0.01,
|
|
248
|
+
maxRuntime: 0.05,
|
|
249
|
+
}, undefined, log);
|
|
250
|
+
// step = (62000 - 60000) / (3-1) = 1000
|
|
251
|
+
// Grid lines at: 60000, 61000, 62000
|
|
252
|
+
const calls = adapter.limitOrder.mock.calls;
|
|
253
|
+
expect(calls).toHaveLength(3);
|
|
254
|
+
const prices = calls.map((c) => parseFloat(c[2]));
|
|
255
|
+
prices.sort((a, b) => a - b);
|
|
256
|
+
expect(prices[0]).toBeCloseTo(60000);
|
|
257
|
+
expect(prices[1]).toBeCloseTo(61000);
|
|
258
|
+
expect(prices[2]).toBeCloseTo(62000);
|
|
259
|
+
});
|
|
260
|
+
it("distributes size equally across grid lines", async () => {
|
|
261
|
+
const { runGrid } = await import("../strategies/grid.js");
|
|
262
|
+
const adapter = mockAdapter({
|
|
263
|
+
getOpenOrders: vi.fn().mockResolvedValue(Array.from({ length: 4 }, () => ({
|
|
264
|
+
orderId: "l1", symbol: "BTC", side: "buy", price: "60000", size: "0.25",
|
|
265
|
+
filled: "0", status: "open", type: "limit",
|
|
266
|
+
}))),
|
|
267
|
+
});
|
|
268
|
+
const log = vi.fn();
|
|
269
|
+
await runGrid(adapter, {
|
|
270
|
+
symbol: "BTC",
|
|
271
|
+
side: "neutral",
|
|
272
|
+
upperPrice: 62000,
|
|
273
|
+
lowerPrice: 60000,
|
|
274
|
+
grids: 4,
|
|
275
|
+
totalSize: 2.0,
|
|
276
|
+
intervalSec: 0.01,
|
|
277
|
+
maxRuntime: 0.05,
|
|
278
|
+
}, undefined, log);
|
|
279
|
+
// Each grid line gets 2.0/4 = 0.5
|
|
280
|
+
for (const call of adapter.limitOrder.mock.calls) {
|
|
281
|
+
expect(parseFloat(call[3])).toBeCloseTo(0.5);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
it("assigns buy below current price and sell above for neutral side", async () => {
|
|
285
|
+
const { runGrid } = await import("../strategies/grid.js");
|
|
286
|
+
const adapter = mockAdapter({
|
|
287
|
+
getMarkets: vi.fn().mockResolvedValue([
|
|
288
|
+
{ symbol: "BTC", markPrice: "61000", indexPrice: "61000", fundingRate: "0", volume24h: "0", openInterest: "0", maxLeverage: 20 },
|
|
289
|
+
]),
|
|
290
|
+
getOpenOrders: vi.fn().mockResolvedValue(Array.from({ length: 5 }, () => ({
|
|
291
|
+
orderId: "l1", symbol: "BTC", side: "buy", price: "60000", size: "0.1",
|
|
292
|
+
filled: "0", status: "open", type: "limit",
|
|
293
|
+
}))),
|
|
294
|
+
});
|
|
295
|
+
const log = vi.fn();
|
|
296
|
+
await runGrid(adapter, {
|
|
297
|
+
symbol: "BTC",
|
|
298
|
+
side: "neutral",
|
|
299
|
+
upperPrice: 63000,
|
|
300
|
+
lowerPrice: 59000,
|
|
301
|
+
grids: 5, // step = 1000. Lines: 59000, 60000, 61000, 62000, 63000
|
|
302
|
+
totalSize: 0.5,
|
|
303
|
+
intervalSec: 0.01,
|
|
304
|
+
maxRuntime: 0.05,
|
|
305
|
+
}, undefined, log);
|
|
306
|
+
// Current price = 61000
|
|
307
|
+
// 59000 → buy, 60000 → buy, 61000 → sell (>=61000), 62000 → sell, 63000 → sell
|
|
308
|
+
const calls = adapter.limitOrder.mock.calls;
|
|
309
|
+
const orders = calls.map((c) => ({ side: c[1], price: parseFloat(c[2]) }));
|
|
310
|
+
orders.sort((a, b) => a.price - b.price);
|
|
311
|
+
// Lines below current (59000, 60000) should be buy
|
|
312
|
+
expect(orders[0].side).toBe("buy");
|
|
313
|
+
expect(orders[1].side).toBe("buy");
|
|
314
|
+
// Lines at or above current (61000, 62000, 63000) should be sell
|
|
315
|
+
expect(orders[2].side).toBe("sell");
|
|
316
|
+
expect(orders[3].side).toBe("sell");
|
|
317
|
+
expect(orders[4].side).toBe("sell");
|
|
318
|
+
});
|
|
319
|
+
it("sets leverage if provided", async () => {
|
|
320
|
+
const { runGrid } = await import("../strategies/grid.js");
|
|
321
|
+
const adapter = mockAdapter({
|
|
322
|
+
getOpenOrders: vi.fn().mockResolvedValue([
|
|
323
|
+
{ orderId: "l1", symbol: "BTC", side: "buy", price: "60000", size: "0.5", filled: "0", status: "open", type: "limit" },
|
|
324
|
+
{ orderId: "l1", symbol: "BTC", side: "sell", price: "62000", size: "0.5", filled: "0", status: "open", type: "limit" },
|
|
325
|
+
]),
|
|
326
|
+
});
|
|
327
|
+
const log = vi.fn();
|
|
328
|
+
await runGrid(adapter, {
|
|
329
|
+
symbol: "BTC",
|
|
330
|
+
side: "neutral",
|
|
331
|
+
upperPrice: 62000,
|
|
332
|
+
lowerPrice: 60000,
|
|
333
|
+
grids: 2,
|
|
334
|
+
totalSize: 1,
|
|
335
|
+
leverage: 5,
|
|
336
|
+
intervalSec: 0.01,
|
|
337
|
+
maxRuntime: 0.05,
|
|
338
|
+
}, undefined, log);
|
|
339
|
+
expect(adapter.setLeverage).toHaveBeenCalledWith("BTC", 5);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
// ══════════════════════════════════════════════
|
|
343
|
+
// DCA Strategy Tests
|
|
344
|
+
// ══════════════════════════════════════════════
|
|
345
|
+
describe("DCA — runDCA", () => {
|
|
346
|
+
it("places the specified number of orders then stops", async () => {
|
|
347
|
+
const { runDCA } = await import("../strategies/dca.js");
|
|
348
|
+
const adapter = mockAdapter();
|
|
349
|
+
const log = vi.fn();
|
|
350
|
+
const result = await runDCA(adapter, {
|
|
351
|
+
symbol: "BTC",
|
|
352
|
+
side: "buy",
|
|
353
|
+
amountPerOrder: 0.01,
|
|
354
|
+
intervalSec: 0.001, // 1ms intervals for fast test
|
|
355
|
+
totalOrders: 3,
|
|
356
|
+
}, undefined, log);
|
|
357
|
+
expect(result.ordersPlaced).toBe(3);
|
|
358
|
+
expect(adapter.marketOrder).toHaveBeenCalledTimes(3);
|
|
359
|
+
});
|
|
360
|
+
it("passes correct symbol, side, and size to each order", async () => {
|
|
361
|
+
const { runDCA } = await import("../strategies/dca.js");
|
|
362
|
+
const adapter = mockAdapter();
|
|
363
|
+
const log = vi.fn();
|
|
364
|
+
await runDCA(adapter, {
|
|
365
|
+
symbol: "ETH",
|
|
366
|
+
side: "sell",
|
|
367
|
+
amountPerOrder: 0.5,
|
|
368
|
+
intervalSec: 0.001,
|
|
369
|
+
totalOrders: 2,
|
|
370
|
+
}, undefined, log);
|
|
371
|
+
for (const call of adapter.marketOrder.mock.calls) {
|
|
372
|
+
expect(call[0]).toBe("ETH");
|
|
373
|
+
expect(call[1]).toBe("sell");
|
|
374
|
+
expect(call[2]).toBe("0.5");
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
it("tracks total filled amount", async () => {
|
|
378
|
+
const { runDCA } = await import("../strategies/dca.js");
|
|
379
|
+
const adapter = mockAdapter();
|
|
380
|
+
const log = vi.fn();
|
|
381
|
+
const result = await runDCA(adapter, {
|
|
382
|
+
symbol: "BTC",
|
|
383
|
+
side: "buy",
|
|
384
|
+
amountPerOrder: 0.1,
|
|
385
|
+
intervalSec: 0.001,
|
|
386
|
+
totalOrders: 5,
|
|
387
|
+
}, undefined, log);
|
|
388
|
+
expect(result.totalFilled).toBeCloseTo(0.5);
|
|
389
|
+
});
|
|
390
|
+
it("computes average price from fill prices", async () => {
|
|
391
|
+
const { runDCA } = await import("../strategies/dca.js");
|
|
392
|
+
let callNum = 0;
|
|
393
|
+
const prices = [59000, 60000, 61000];
|
|
394
|
+
const adapter = mockAdapter({
|
|
395
|
+
marketOrder: vi.fn().mockImplementation(() => {
|
|
396
|
+
const price = prices[callNum++];
|
|
397
|
+
return Promise.resolve({ orderId: `m${callNum}`, price: String(price) });
|
|
398
|
+
}),
|
|
399
|
+
});
|
|
400
|
+
const log = vi.fn();
|
|
401
|
+
const result = await runDCA(adapter, {
|
|
402
|
+
symbol: "BTC",
|
|
403
|
+
side: "buy",
|
|
404
|
+
amountPerOrder: 1,
|
|
405
|
+
intervalSec: 0.001,
|
|
406
|
+
totalOrders: 3,
|
|
407
|
+
}, undefined, log);
|
|
408
|
+
// avg = (1*59000 + 1*60000 + 1*61000) / 3 = 60000
|
|
409
|
+
expect(result.avgPrice).toBeCloseTo(60000);
|
|
410
|
+
});
|
|
411
|
+
it("continues on individual order errors", async () => {
|
|
412
|
+
const { runDCA } = await import("../strategies/dca.js");
|
|
413
|
+
// DCA keeps going until ordersPlaced reaches totalOrders.
|
|
414
|
+
// If one call fails, it retries on the next loop iteration.
|
|
415
|
+
// So with totalOrders=3, it needs 4 calls: succeed, fail, succeed, succeed → 3 placed.
|
|
416
|
+
const marketOrderMock = vi.fn()
|
|
417
|
+
.mockResolvedValueOnce({ orderId: "m1", price: "60000" })
|
|
418
|
+
.mockRejectedValueOnce(new Error("temporary"))
|
|
419
|
+
.mockResolvedValueOnce({ orderId: "m3", price: "60000" })
|
|
420
|
+
.mockResolvedValueOnce({ orderId: "m4", price: "60000" });
|
|
421
|
+
const adapter = mockAdapter({ marketOrder: marketOrderMock });
|
|
422
|
+
const log = vi.fn();
|
|
423
|
+
const result = await runDCA(adapter, {
|
|
424
|
+
symbol: "BTC",
|
|
425
|
+
side: "buy",
|
|
426
|
+
amountPerOrder: 0.1,
|
|
427
|
+
intervalSec: 0.001,
|
|
428
|
+
totalOrders: 3,
|
|
429
|
+
}, undefined, log);
|
|
430
|
+
// 4 marketOrder calls: 3 succeeded, 1 failed
|
|
431
|
+
expect(marketOrderMock).toHaveBeenCalledTimes(4);
|
|
432
|
+
expect(result.ordersPlaced).toBe(3);
|
|
433
|
+
// Verify an error was logged
|
|
434
|
+
expect(log).toHaveBeenCalledWith(expect.stringContaining("Order error"));
|
|
435
|
+
});
|
|
436
|
+
it("stops when too many errors (errors > ordersPlaced and errors > 10)", async () => {
|
|
437
|
+
const { runDCA } = await import("../strategies/dca.js");
|
|
438
|
+
const adapter = mockAdapter({
|
|
439
|
+
marketOrder: vi.fn().mockRejectedValue(new Error("always fails")),
|
|
440
|
+
});
|
|
441
|
+
const log = vi.fn();
|
|
442
|
+
const result = await runDCA(adapter, {
|
|
443
|
+
symbol: "BTC",
|
|
444
|
+
side: "buy",
|
|
445
|
+
amountPerOrder: 0.1,
|
|
446
|
+
intervalSec: 0.001,
|
|
447
|
+
totalOrders: 100, // high count but errors will stop it
|
|
448
|
+
}, undefined, log);
|
|
449
|
+
// Stops at 11 errors (>10 and >0 placed)
|
|
450
|
+
expect(result.ordersPlaced).toBe(0);
|
|
451
|
+
expect(log).toHaveBeenCalledWith(expect.stringContaining("Too many errors"));
|
|
452
|
+
});
|
|
453
|
+
it("skips order when buy price exceeds price limit", async () => {
|
|
454
|
+
const { runDCA } = await import("../strategies/dca.js");
|
|
455
|
+
const adapter = mockAdapter({
|
|
456
|
+
getMarkets: vi.fn().mockResolvedValue([
|
|
457
|
+
{ symbol: "BTC", markPrice: "65000", indexPrice: "65000", fundingRate: "0", volume24h: "0", openInterest: "0", maxLeverage: 20 },
|
|
458
|
+
]),
|
|
459
|
+
});
|
|
460
|
+
const log = vi.fn();
|
|
461
|
+
// Price limit of $60000, but market is at $65000 — should skip
|
|
462
|
+
// With totalOrders=0 this would loop forever, so we use maxRuntime
|
|
463
|
+
const result = await runDCA(adapter, {
|
|
464
|
+
symbol: "BTC",
|
|
465
|
+
side: "buy",
|
|
466
|
+
amountPerOrder: 0.1,
|
|
467
|
+
intervalSec: 0.001,
|
|
468
|
+
totalOrders: 2,
|
|
469
|
+
priceLimit: 60000,
|
|
470
|
+
maxRuntime: 0.1, // 100ms timeout
|
|
471
|
+
}, undefined, log);
|
|
472
|
+
expect(result.ordersPlaced).toBe(0);
|
|
473
|
+
expect(adapter.marketOrder).not.toHaveBeenCalled();
|
|
474
|
+
});
|
|
475
|
+
it("respects maxRuntime", async () => {
|
|
476
|
+
const { runDCA } = await import("../strategies/dca.js");
|
|
477
|
+
const adapter = mockAdapter();
|
|
478
|
+
const log = vi.fn();
|
|
479
|
+
const start = Date.now();
|
|
480
|
+
const result = await runDCA(adapter, {
|
|
481
|
+
symbol: "BTC",
|
|
482
|
+
side: "buy",
|
|
483
|
+
amountPerOrder: 0.01,
|
|
484
|
+
intervalSec: 0.01, // 10ms intervals (short enough to not block)
|
|
485
|
+
totalOrders: 0, // unlimited
|
|
486
|
+
maxRuntime: 0.1, // 100ms max
|
|
487
|
+
}, undefined, log);
|
|
488
|
+
const elapsed = Date.now() - start;
|
|
489
|
+
// Should stop within a reasonable time (definitely under 5s)
|
|
490
|
+
expect(elapsed).toBeLessThan(5000);
|
|
491
|
+
// With 10ms intervals and 100ms runtime, should place a handful of orders but not many
|
|
492
|
+
expect(result.ordersPlaced).toBeGreaterThanOrEqual(1);
|
|
493
|
+
expect(result.ordersPlaced).toBeLessThan(50); // sanity check
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
// ══════════════════════════════════════════════
|
|
497
|
+
// TWAP Slice Calculation (unit-level)
|
|
498
|
+
// ══════════════════════════════════════════════
|
|
499
|
+
describe("TWAP — slice calculation edge cases", () => {
|
|
500
|
+
it("minimum 2 slices even for very short duration", async () => {
|
|
501
|
+
const { runTWAP } = await import("../strategies/twap.js");
|
|
502
|
+
const adapter = mockAdapter();
|
|
503
|
+
const log = vi.fn();
|
|
504
|
+
const result = await runTWAP(adapter, {
|
|
505
|
+
symbol: "BTC",
|
|
506
|
+
side: "buy",
|
|
507
|
+
totalSize: 0.1,
|
|
508
|
+
durationSec: 1, // 1s / 30 = 0.03 → floor = 0 → max(0, 2) = 2
|
|
509
|
+
}, undefined, log);
|
|
510
|
+
expect(result.totalSlices).toBe(2);
|
|
511
|
+
});
|
|
512
|
+
it("reports correct remaining after partial completion", async () => {
|
|
513
|
+
const { runTWAP } = await import("../strategies/twap.js");
|
|
514
|
+
const marketOrderMock = vi.fn()
|
|
515
|
+
.mockResolvedValueOnce({ orderId: "ok" })
|
|
516
|
+
.mockRejectedValueOnce(new Error("fail"))
|
|
517
|
+
.mockResolvedValueOnce({ orderId: "ok" });
|
|
518
|
+
const adapter = mockAdapter({ marketOrder: marketOrderMock });
|
|
519
|
+
const log = vi.fn();
|
|
520
|
+
const result = await runTWAP(adapter, {
|
|
521
|
+
symbol: "BTC",
|
|
522
|
+
side: "buy",
|
|
523
|
+
totalSize: 1.0,
|
|
524
|
+
durationSec: 1,
|
|
525
|
+
slices: 3,
|
|
526
|
+
}, undefined, log);
|
|
527
|
+
// 3 slices, each nominally 1/3. Slice 2 fails.
|
|
528
|
+
// Slice 1: filled += 1/3, remaining = 2/3
|
|
529
|
+
// Slice 2: fails, no change to filled/remaining
|
|
530
|
+
// Slice 3 is last slice: uses state.remaining = 2/3 as the size
|
|
531
|
+
// Slice 3: filled += 2/3, remaining = 0
|
|
532
|
+
// So final remaining = 0, filled = 1.0
|
|
533
|
+
expect(result.slicesDone).toBe(2);
|
|
534
|
+
expect(result.errors).toBe(1);
|
|
535
|
+
// Last slice picks up all remaining, so total filled = 1/3 + 2/3 = 1.0
|
|
536
|
+
expect(result.remaining).toBeCloseTo(0, 2);
|
|
537
|
+
expect(result.filled).toBeCloseTo(1.0, 2);
|
|
538
|
+
});
|
|
539
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { MockAdapter, createMockPositions } from "./exchanges/mock-adapter.js";
|
|
3
|
+
describe("Trade Execution", () => {
|
|
4
|
+
let adapter;
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
adapter = new MockAdapter("test-exchange");
|
|
7
|
+
adapter.orderResult = { status: "ok", orderId: "test-123" };
|
|
8
|
+
});
|
|
9
|
+
describe("Market Orders", () => {
|
|
10
|
+
it("should place a buy market order with correct params", async () => {
|
|
11
|
+
await adapter.marketOrder("BTC", "buy", "0.1");
|
|
12
|
+
const calls = adapter.getCallsFor("marketOrder");
|
|
13
|
+
expect(calls).toHaveLength(1);
|
|
14
|
+
expect(calls[0].args).toEqual(["BTC", "buy", "0.1"]);
|
|
15
|
+
});
|
|
16
|
+
it("should place a sell market order", async () => {
|
|
17
|
+
await adapter.marketOrder("ETH", "sell", "1.5");
|
|
18
|
+
const calls = adapter.getCallsFor("marketOrder");
|
|
19
|
+
expect(calls).toHaveLength(1);
|
|
20
|
+
expect(calls[0].args).toEqual(["ETH", "sell", "1.5"]);
|
|
21
|
+
});
|
|
22
|
+
it("should return order result", async () => {
|
|
23
|
+
const result = await adapter.marketOrder("BTC", "buy", "0.1");
|
|
24
|
+
expect(result).toEqual({ status: "ok", orderId: "test-123" });
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
describe("Limit Orders", () => {
|
|
28
|
+
it("should place limit order with price and size", async () => {
|
|
29
|
+
await adapter.limitOrder("BTC", "buy", "95000", "0.05");
|
|
30
|
+
const calls = adapter.getCallsFor("limitOrder");
|
|
31
|
+
expect(calls).toHaveLength(1);
|
|
32
|
+
expect(calls[0].args).toEqual(["BTC", "buy", "95000", "0.05"]);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
describe("Stop Orders", () => {
|
|
36
|
+
it("should place stop order with trigger price", async () => {
|
|
37
|
+
await adapter.stopOrder("BTC", "sell", "0.1", "90000", { reduceOnly: true });
|
|
38
|
+
const calls = adapter.getCallsFor("stopOrder");
|
|
39
|
+
expect(calls).toHaveLength(1);
|
|
40
|
+
expect(calls[0].args).toEqual(["BTC", "sell", "0.1", "90000", { reduceOnly: true }]);
|
|
41
|
+
});
|
|
42
|
+
it("should place stop-limit order", async () => {
|
|
43
|
+
await adapter.stopOrder("ETH", "buy", "1.0", "4000", { limitPrice: "4050", reduceOnly: false });
|
|
44
|
+
const calls = adapter.getCallsFor("stopOrder");
|
|
45
|
+
expect(calls[0].args[4]).toEqual({ limitPrice: "4050", reduceOnly: false });
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe("Order Management", () => {
|
|
49
|
+
it("should cancel specific order", async () => {
|
|
50
|
+
await adapter.cancelOrder("BTC", "order-456");
|
|
51
|
+
expect(adapter.getCallsFor("cancelOrder")[0].args).toEqual(["BTC", "order-456"]);
|
|
52
|
+
});
|
|
53
|
+
it("should cancel all orders", async () => {
|
|
54
|
+
await adapter.cancelAllOrders();
|
|
55
|
+
expect(adapter.getCallsFor("cancelAllOrders")).toHaveLength(1);
|
|
56
|
+
});
|
|
57
|
+
it("should cancel orders for specific symbol", async () => {
|
|
58
|
+
await adapter.cancelAllOrders("ETH");
|
|
59
|
+
expect(adapter.getCallsFor("cancelAllOrders")[0].args).toEqual(["ETH"]);
|
|
60
|
+
});
|
|
61
|
+
it("should edit existing order", async () => {
|
|
62
|
+
await adapter.editOrder("BTC", "order-789", "96000", "0.2");
|
|
63
|
+
expect(adapter.getCallsFor("editOrder")[0].args).toEqual(["BTC", "order-789", "96000", "0.2"]);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe("Position Close Logic", () => {
|
|
67
|
+
it("should close long position with sell order", async () => {
|
|
68
|
+
adapter.positionsResponse = createMockPositions(1); // long BTC 0.1
|
|
69
|
+
const positions = await adapter.getPositions();
|
|
70
|
+
expect(positions).toHaveLength(1);
|
|
71
|
+
expect(positions[0].side).toBe("long");
|
|
72
|
+
// Close: sell the same size
|
|
73
|
+
await adapter.marketOrder(positions[0].symbol, "sell", positions[0].size);
|
|
74
|
+
const calls = adapter.getCallsFor("marketOrder");
|
|
75
|
+
expect(calls).toHaveLength(1);
|
|
76
|
+
expect(calls[0].args).toEqual(["BTC", "sell", "0.1"]);
|
|
77
|
+
});
|
|
78
|
+
it("should close short position with buy order", async () => {
|
|
79
|
+
adapter.positionsResponse = [{
|
|
80
|
+
symbol: "ETH", side: "short", size: "2.0",
|
|
81
|
+
entryPrice: "3500", markPrice: "3400", liquidationPrice: "4500",
|
|
82
|
+
unrealizedPnl: "200", leverage: 5,
|
|
83
|
+
}];
|
|
84
|
+
const positions = await adapter.getPositions();
|
|
85
|
+
await adapter.marketOrder(positions[0].symbol, "buy", positions[0].size);
|
|
86
|
+
const calls = adapter.getCallsFor("marketOrder");
|
|
87
|
+
expect(calls[0].args).toEqual(["ETH", "buy", "2.0"]);
|
|
88
|
+
});
|
|
89
|
+
it("should close all positions with opposite-side orders", async () => {
|
|
90
|
+
adapter.positionsResponse = [
|
|
91
|
+
{ symbol: "BTC", side: "long", size: "0.1", entryPrice: "100000", markPrice: "101000", liquidationPrice: "90000", unrealizedPnl: "100", leverage: 10 },
|
|
92
|
+
{ symbol: "ETH", side: "short", size: "1.5", entryPrice: "3500", markPrice: "3400", liquidationPrice: "4500", unrealizedPnl: "150", leverage: 5 },
|
|
93
|
+
];
|
|
94
|
+
const positions = await adapter.getPositions();
|
|
95
|
+
for (const pos of positions) {
|
|
96
|
+
const closeSide = pos.side === "long" ? "sell" : "buy";
|
|
97
|
+
await adapter.marketOrder(pos.symbol, closeSide, pos.size);
|
|
98
|
+
}
|
|
99
|
+
const calls = adapter.getCallsFor("marketOrder");
|
|
100
|
+
expect(calls).toHaveLength(2);
|
|
101
|
+
expect(calls[0].args).toEqual(["BTC", "sell", "0.1"]);
|
|
102
|
+
expect(calls[1].args).toEqual(["ETH", "buy", "1.5"]);
|
|
103
|
+
});
|
|
104
|
+
it("should reduce position by percentage", async () => {
|
|
105
|
+
adapter.positionsResponse = [{
|
|
106
|
+
symbol: "BTC", side: "long", size: "1.0",
|
|
107
|
+
entryPrice: "100000", markPrice: "101000", liquidationPrice: "90000",
|
|
108
|
+
unrealizedPnl: "1000", leverage: 10,
|
|
109
|
+
}];
|
|
110
|
+
const positions = await adapter.getPositions();
|
|
111
|
+
const pos = positions[0];
|
|
112
|
+
const reducePct = 50;
|
|
113
|
+
const reduceSize = (Number(pos.size) * reducePct / 100).toString();
|
|
114
|
+
await adapter.marketOrder(pos.symbol, "sell", reduceSize);
|
|
115
|
+
const calls = adapter.getCallsFor("marketOrder");
|
|
116
|
+
expect(calls[0].args).toEqual(["BTC", "sell", "0.5"]);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe("Leverage Management", () => {
|
|
120
|
+
it("should set leverage with cross margin", async () => {
|
|
121
|
+
await adapter.setLeverage("BTC", 20, "cross");
|
|
122
|
+
expect(adapter.getCallsFor("setLeverage")[0].args).toEqual(["BTC", 20, "cross"]);
|
|
123
|
+
});
|
|
124
|
+
it("should set leverage with isolated margin", async () => {
|
|
125
|
+
await adapter.setLeverage("ETH", 5, "isolated");
|
|
126
|
+
expect(adapter.getCallsFor("setLeverage")[0].args).toEqual(["ETH", 5, "isolated"]);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|