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,821 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { registerTradeCommands } from "../../commands/trade.js";
|
|
4
|
+
// ── Mock dependencies to prevent file I/O ──
|
|
5
|
+
vi.mock("../../execution-log.js", () => ({
|
|
6
|
+
logExecution: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
vi.mock("../../client-id-tracker.js", () => ({
|
|
9
|
+
generateClientId: vi.fn(() => "test-id-123"),
|
|
10
|
+
logClientId: vi.fn(),
|
|
11
|
+
isOrderDuplicate: vi.fn(() => false),
|
|
12
|
+
}));
|
|
13
|
+
vi.mock("../../trade-validator.js", () => ({
|
|
14
|
+
validateTrade: vi.fn().mockResolvedValue({
|
|
15
|
+
valid: true,
|
|
16
|
+
checks: [],
|
|
17
|
+
warnings: [],
|
|
18
|
+
timestamp: new Date().toISOString(),
|
|
19
|
+
}),
|
|
20
|
+
}));
|
|
21
|
+
// Import mocked modules so we can control their behavior per-test
|
|
22
|
+
import { generateClientId, logClientId, isOrderDuplicate } from "../../client-id-tracker.js";
|
|
23
|
+
import { logExecution } from "../../execution-log.js";
|
|
24
|
+
// ── Mock adapter factory ──
|
|
25
|
+
function mockAdapter(overrides) {
|
|
26
|
+
return {
|
|
27
|
+
name: "test-exchange",
|
|
28
|
+
marketOrder: vi.fn().mockResolvedValue({ orderId: "m1", status: "filled" }),
|
|
29
|
+
limitOrder: vi.fn().mockResolvedValue({ orderId: "l1", status: "open" }),
|
|
30
|
+
stopOrder: vi.fn().mockResolvedValue({ orderId: "s1" }),
|
|
31
|
+
cancelOrder: vi.fn().mockResolvedValue({ success: true }),
|
|
32
|
+
cancelAllOrders: vi.fn().mockResolvedValue({ cancelled: 3 }),
|
|
33
|
+
editOrder: vi.fn().mockResolvedValue({ success: true }),
|
|
34
|
+
setLeverage: vi.fn().mockResolvedValue({ leverage: 10 }),
|
|
35
|
+
getPositions: vi.fn().mockResolvedValue([]),
|
|
36
|
+
getOpenOrders: vi.fn().mockResolvedValue([]),
|
|
37
|
+
getBalance: vi.fn().mockResolvedValue({
|
|
38
|
+
equity: "1000",
|
|
39
|
+
available: "800",
|
|
40
|
+
marginUsed: "200",
|
|
41
|
+
unrealizedPnl: "0",
|
|
42
|
+
}),
|
|
43
|
+
getMarkets: vi.fn().mockResolvedValue([]),
|
|
44
|
+
getOrderbook: vi.fn().mockResolvedValue({ bids: [], asks: [] }),
|
|
45
|
+
getRecentTrades: vi.fn().mockResolvedValue([]),
|
|
46
|
+
getFundingHistory: vi.fn().mockResolvedValue([]),
|
|
47
|
+
getKlines: vi.fn().mockResolvedValue([]),
|
|
48
|
+
getOrderHistory: vi.fn().mockResolvedValue([]),
|
|
49
|
+
getTradeHistory: vi.fn().mockResolvedValue([]),
|
|
50
|
+
getFundingPayments: vi.fn().mockResolvedValue([]),
|
|
51
|
+
...overrides,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// ── Helper: create a program, register commands, and parse ──
|
|
55
|
+
function createProgram(adapter) {
|
|
56
|
+
const program = new Command();
|
|
57
|
+
program.exitOverride(); // Prevent process.exit on parse errors
|
|
58
|
+
program.configureOutput({
|
|
59
|
+
writeOut: () => { },
|
|
60
|
+
writeErr: () => { },
|
|
61
|
+
});
|
|
62
|
+
registerTradeCommands(program, async () => adapter, () => false);
|
|
63
|
+
return program;
|
|
64
|
+
}
|
|
65
|
+
async function run(adapter, args) {
|
|
66
|
+
const program = createProgram(adapter);
|
|
67
|
+
// Suppress console output during tests
|
|
68
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => { });
|
|
69
|
+
const err = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
70
|
+
try {
|
|
71
|
+
await program.parseAsync(["node", "perp", ...args]);
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
log.mockRestore();
|
|
75
|
+
err.mockRestore();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// ── Shared setup ──
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
vi.clearAllMocks();
|
|
81
|
+
// Reset isOrderDuplicate to default: not a duplicate
|
|
82
|
+
vi.mocked(isOrderDuplicate).mockReturnValue(false);
|
|
83
|
+
});
|
|
84
|
+
// ══════════════════════════════════════════════════════════════
|
|
85
|
+
// 1. Market Order -- parameter correctness
|
|
86
|
+
// ══════════════════════════════════════════════════════════════
|
|
87
|
+
describe("trade market -- parameter correctness", () => {
|
|
88
|
+
it("calls adapter.marketOrder with uppercased symbol, lowercased side, and string size", async () => {
|
|
89
|
+
const adapter = mockAdapter();
|
|
90
|
+
await run(adapter, ["trade", "market", "btc", "buy", "0.1"]);
|
|
91
|
+
expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
|
|
92
|
+
expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "buy", "0.1");
|
|
93
|
+
});
|
|
94
|
+
it("uppercases mixed-case symbol", async () => {
|
|
95
|
+
const adapter = mockAdapter();
|
|
96
|
+
await run(adapter, ["trade", "market", "Eth", "sell", "2.5"]);
|
|
97
|
+
expect(adapter.marketOrder).toHaveBeenCalledWith("ETH", "sell", "2.5");
|
|
98
|
+
});
|
|
99
|
+
it("lowercases the side argument", async () => {
|
|
100
|
+
const adapter = mockAdapter();
|
|
101
|
+
await run(adapter, ["trade", "market", "sol", "BUY", "5"]);
|
|
102
|
+
expect(adapter.marketOrder).toHaveBeenCalledWith("SOL", "buy", "5");
|
|
103
|
+
});
|
|
104
|
+
it("passes size as a string, not a number", async () => {
|
|
105
|
+
const adapter = mockAdapter();
|
|
106
|
+
await run(adapter, ["trade", "market", "btc", "buy", "0.001"]);
|
|
107
|
+
const args = vi.mocked(adapter.marketOrder).mock.calls[0];
|
|
108
|
+
expect(typeof args[2]).toBe("string");
|
|
109
|
+
expect(args[2]).toBe("0.001");
|
|
110
|
+
});
|
|
111
|
+
it("rejects invalid side (exits with error)", async () => {
|
|
112
|
+
const adapter = mockAdapter();
|
|
113
|
+
// errorAndExit calls process.exit(1), which we can catch via exitOverride or mock
|
|
114
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => {
|
|
115
|
+
throw new Error("process.exit");
|
|
116
|
+
});
|
|
117
|
+
try {
|
|
118
|
+
await run(adapter, ["trade", "market", "btc", "long", "1"]);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Expected
|
|
122
|
+
}
|
|
123
|
+
expect(adapter.marketOrder).not.toHaveBeenCalled();
|
|
124
|
+
exitSpy.mockRestore();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
// ══════════════════════════════════════════════════════════════
|
|
128
|
+
// 2. Market Order -- client ID flow
|
|
129
|
+
// ══════════════════════════════════════════════════════════════
|
|
130
|
+
describe("trade market -- client ID flow", () => {
|
|
131
|
+
it("with --auto-id: generates client ID and logs pending then submitted", async () => {
|
|
132
|
+
const adapter = mockAdapter();
|
|
133
|
+
await run(adapter, ["trade", "market", "btc", "buy", "0.1", "--auto-id"]);
|
|
134
|
+
// generateClientId was called
|
|
135
|
+
expect(generateClientId).toHaveBeenCalled();
|
|
136
|
+
// logClientId called twice: once pending, once submitted
|
|
137
|
+
expect(logClientId).toHaveBeenCalledTimes(2);
|
|
138
|
+
const firstCall = vi.mocked(logClientId).mock.calls[0][0];
|
|
139
|
+
const secondCall = vi.mocked(logClientId).mock.calls[1][0];
|
|
140
|
+
expect(firstCall.clientOrderId).toBe("test-id-123");
|
|
141
|
+
expect(firstCall.status).toBe("pending");
|
|
142
|
+
expect(firstCall.symbol).toBe("BTC");
|
|
143
|
+
expect(firstCall.side).toBe("buy");
|
|
144
|
+
expect(firstCall.type).toBe("market");
|
|
145
|
+
expect(secondCall.clientOrderId).toBe("test-id-123");
|
|
146
|
+
expect(secondCall.status).toBe("submitted");
|
|
147
|
+
// Adapter still called
|
|
148
|
+
expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
|
|
149
|
+
});
|
|
150
|
+
it("with --client-id my-id: uses the provided ID", async () => {
|
|
151
|
+
const adapter = mockAdapter();
|
|
152
|
+
await run(adapter, ["trade", "market", "btc", "buy", "0.1", "--client-id", "my-id"]);
|
|
153
|
+
// generateClientId was NOT called
|
|
154
|
+
expect(generateClientId).not.toHaveBeenCalled();
|
|
155
|
+
// logClientId called with user's ID
|
|
156
|
+
expect(logClientId).toHaveBeenCalledTimes(2);
|
|
157
|
+
expect(vi.mocked(logClientId).mock.calls[0][0].clientOrderId).toBe("my-id");
|
|
158
|
+
expect(vi.mocked(logClientId).mock.calls[1][0].clientOrderId).toBe("my-id");
|
|
159
|
+
expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
|
|
160
|
+
});
|
|
161
|
+
it("duplicate detection: when isOrderDuplicate returns true, adapter is NOT called", async () => {
|
|
162
|
+
vi.mocked(isOrderDuplicate).mockReturnValue(true);
|
|
163
|
+
const adapter = mockAdapter();
|
|
164
|
+
await run(adapter, ["trade", "market", "btc", "buy", "0.1", "--client-id", "dup-id"]);
|
|
165
|
+
expect(adapter.marketOrder).not.toHaveBeenCalled();
|
|
166
|
+
// logClientId should NOT be called for duplicates
|
|
167
|
+
expect(logClientId).not.toHaveBeenCalled();
|
|
168
|
+
});
|
|
169
|
+
it("without --auto-id or --client-id: no client ID tracking", async () => {
|
|
170
|
+
const adapter = mockAdapter();
|
|
171
|
+
await run(adapter, ["trade", "market", "btc", "buy", "0.1"]);
|
|
172
|
+
expect(generateClientId).not.toHaveBeenCalled();
|
|
173
|
+
expect(logClientId).not.toHaveBeenCalled();
|
|
174
|
+
// Adapter still called
|
|
175
|
+
expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
// ══════════════════════════════════════════════════════════════
|
|
179
|
+
// 3. Limit Order -- parameter correctness
|
|
180
|
+
// ══════════════════════════════════════════════════════════════
|
|
181
|
+
describe("trade limit -- parameter correctness", () => {
|
|
182
|
+
it("calls adapter.limitOrder with uppercased symbol, lowercased side, price, size", async () => {
|
|
183
|
+
const adapter = mockAdapter();
|
|
184
|
+
await run(adapter, ["trade", "limit", "eth", "sell", "2000", "1.5"]);
|
|
185
|
+
expect(adapter.limitOrder).toHaveBeenCalledTimes(1);
|
|
186
|
+
expect(adapter.limitOrder).toHaveBeenCalledWith("ETH", "sell", "2000", "1.5");
|
|
187
|
+
});
|
|
188
|
+
it("all four args are strings", async () => {
|
|
189
|
+
const adapter = mockAdapter();
|
|
190
|
+
await run(adapter, ["trade", "limit", "btc", "buy", "65000", "0.05"]);
|
|
191
|
+
const args = vi.mocked(adapter.limitOrder).mock.calls[0];
|
|
192
|
+
expect(typeof args[0]).toBe("string"); // symbol
|
|
193
|
+
expect(typeof args[1]).toBe("string"); // side
|
|
194
|
+
expect(typeof args[2]).toBe("string"); // price
|
|
195
|
+
expect(typeof args[3]).toBe("string"); // size
|
|
196
|
+
});
|
|
197
|
+
it("client ID works the same as market order", async () => {
|
|
198
|
+
const adapter = mockAdapter();
|
|
199
|
+
await run(adapter, ["trade", "limit", "btc", "buy", "60000", "0.1", "--auto-id"]);
|
|
200
|
+
expect(generateClientId).toHaveBeenCalled();
|
|
201
|
+
expect(logClientId).toHaveBeenCalledTimes(2);
|
|
202
|
+
expect(vi.mocked(logClientId).mock.calls[0][0].type).toBe("limit");
|
|
203
|
+
expect(vi.mocked(logClientId).mock.calls[0][0].status).toBe("pending");
|
|
204
|
+
expect(vi.mocked(logClientId).mock.calls[1][0].status).toBe("submitted");
|
|
205
|
+
expect(adapter.limitOrder).toHaveBeenCalledTimes(1);
|
|
206
|
+
});
|
|
207
|
+
it("duplicate detection prevents execution for limit orders too", async () => {
|
|
208
|
+
vi.mocked(isOrderDuplicate).mockReturnValue(true);
|
|
209
|
+
const adapter = mockAdapter();
|
|
210
|
+
await run(adapter, ["trade", "limit", "btc", "buy", "60000", "0.1", "--client-id", "dup"]);
|
|
211
|
+
expect(adapter.limitOrder).not.toHaveBeenCalled();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
// ══════════════════════════════════════════════════════════════
|
|
215
|
+
// 4. Stop Order -- parameter correctness
|
|
216
|
+
// ══════════════════════════════════════════════════════════════
|
|
217
|
+
describe("trade stop -- parameter correctness", () => {
|
|
218
|
+
it("calls adapter.stopOrder with correct args and options", async () => {
|
|
219
|
+
const adapter = mockAdapter();
|
|
220
|
+
await run(adapter, [
|
|
221
|
+
"trade", "stop", "btc", "sell", "60000", "0.1",
|
|
222
|
+
"--limit-price", "59500", "--reduce-only",
|
|
223
|
+
]);
|
|
224
|
+
expect(adapter.stopOrder).toHaveBeenCalledTimes(1);
|
|
225
|
+
expect(adapter.stopOrder).toHaveBeenCalledWith("BTC", "sell", "0.1", "60000", { limitPrice: "59500", reduceOnly: true });
|
|
226
|
+
});
|
|
227
|
+
it("without --limit-price and --reduce-only: options have undefined values", async () => {
|
|
228
|
+
const adapter = mockAdapter();
|
|
229
|
+
await run(adapter, ["trade", "stop", "btc", "sell", "60000", "0.1"]);
|
|
230
|
+
expect(adapter.stopOrder).toHaveBeenCalledTimes(1);
|
|
231
|
+
expect(adapter.stopOrder).toHaveBeenCalledWith("BTC", "sell", "0.1", "60000", { limitPrice: undefined, reduceOnly: undefined });
|
|
232
|
+
});
|
|
233
|
+
it("uppercases symbol for stop orders", async () => {
|
|
234
|
+
const adapter = mockAdapter();
|
|
235
|
+
await run(adapter, ["trade", "stop", "eth", "buy", "3500", "1"]);
|
|
236
|
+
expect(adapter.stopOrder).toHaveBeenCalledWith("ETH", "buy", "1", "3500", { limitPrice: undefined, reduceOnly: undefined });
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
// ══════════════════════════════════════════════════════════════
|
|
240
|
+
// 5. Cancel Order
|
|
241
|
+
// ══════════════════════════════════════════════════════════════
|
|
242
|
+
describe("trade cancel -- parameter correctness", () => {
|
|
243
|
+
it("calls adapter.cancelOrder with uppercased symbol and orderId", async () => {
|
|
244
|
+
const adapter = mockAdapter();
|
|
245
|
+
await run(adapter, ["trade", "cancel", "btc", "abc123"]);
|
|
246
|
+
expect(adapter.cancelOrder).toHaveBeenCalledTimes(1);
|
|
247
|
+
expect(adapter.cancelOrder).toHaveBeenCalledWith("BTC", "abc123");
|
|
248
|
+
});
|
|
249
|
+
it("uppercases mixed-case symbol", async () => {
|
|
250
|
+
const adapter = mockAdapter();
|
|
251
|
+
await run(adapter, ["trade", "cancel", "Sol", "order-456"]);
|
|
252
|
+
expect(adapter.cancelOrder).toHaveBeenCalledWith("SOL", "order-456");
|
|
253
|
+
});
|
|
254
|
+
it("no other adapter methods are called", async () => {
|
|
255
|
+
const adapter = mockAdapter();
|
|
256
|
+
await run(adapter, ["trade", "cancel", "btc", "abc123"]);
|
|
257
|
+
expect(adapter.marketOrder).not.toHaveBeenCalled();
|
|
258
|
+
expect(adapter.limitOrder).not.toHaveBeenCalled();
|
|
259
|
+
expect(adapter.cancelAllOrders).not.toHaveBeenCalled();
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
// ══════════════════════════════════════════════════════════════
|
|
263
|
+
// 6. Cancel All Orders
|
|
264
|
+
// ══════════════════════════════════════════════════════════════
|
|
265
|
+
describe("trade cancel-all", () => {
|
|
266
|
+
it("calls adapter.cancelAllOrders with no arguments", async () => {
|
|
267
|
+
const adapter = mockAdapter();
|
|
268
|
+
await run(adapter, ["trade", "cancel-all"]);
|
|
269
|
+
expect(adapter.cancelAllOrders).toHaveBeenCalledTimes(1);
|
|
270
|
+
expect(adapter.cancelAllOrders).toHaveBeenCalledWith();
|
|
271
|
+
});
|
|
272
|
+
it("no other adapter methods are called", async () => {
|
|
273
|
+
const adapter = mockAdapter();
|
|
274
|
+
await run(adapter, ["trade", "cancel-all"]);
|
|
275
|
+
expect(adapter.marketOrder).not.toHaveBeenCalled();
|
|
276
|
+
expect(adapter.cancelOrder).not.toHaveBeenCalled();
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
// ══════════════════════════════════════════════════════════════
|
|
280
|
+
// 7. Set Leverage
|
|
281
|
+
// ══════════════════════════════════════════════════════════════
|
|
282
|
+
describe("trade leverage", () => {
|
|
283
|
+
it("calls adapter.setLeverage with uppercased symbol, parsed int, and 'cross' by default", async () => {
|
|
284
|
+
const adapter = mockAdapter();
|
|
285
|
+
await run(adapter, ["trade", "leverage", "btc", "10"]);
|
|
286
|
+
expect(adapter.setLeverage).toHaveBeenCalledTimes(1);
|
|
287
|
+
expect(adapter.setLeverage).toHaveBeenCalledWith("BTC", 10, "cross");
|
|
288
|
+
});
|
|
289
|
+
it("with --isolated: passes 'isolated' as margin mode", async () => {
|
|
290
|
+
const adapter = mockAdapter();
|
|
291
|
+
await run(adapter, ["trade", "leverage", "btc", "10", "--isolated"]);
|
|
292
|
+
expect(adapter.setLeverage).toHaveBeenCalledWith("BTC", 10, "isolated");
|
|
293
|
+
});
|
|
294
|
+
it("leverage is parsed as integer", async () => {
|
|
295
|
+
const adapter = mockAdapter();
|
|
296
|
+
await run(adapter, ["trade", "leverage", "eth", "25"]);
|
|
297
|
+
const args = vi.mocked(adapter.setLeverage).mock.calls[0];
|
|
298
|
+
expect(args[1]).toBe(25);
|
|
299
|
+
expect(typeof args[1]).toBe("number");
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
// ══════════════════════════════════════════════════════════════
|
|
303
|
+
// 8. Close Position -- side mapping
|
|
304
|
+
// ══════════════════════════════════════════════════════════════
|
|
305
|
+
describe("trade close -- side mapping", () => {
|
|
306
|
+
it("LONG position -> calls marketOrder with 'sell' (opposite side)", async () => {
|
|
307
|
+
const adapter = mockAdapter({
|
|
308
|
+
getPositions: vi.fn().mockResolvedValue([
|
|
309
|
+
{
|
|
310
|
+
symbol: "BTC",
|
|
311
|
+
side: "long",
|
|
312
|
+
size: "0.5",
|
|
313
|
+
entryPrice: "60000",
|
|
314
|
+
markPrice: "61000",
|
|
315
|
+
liquidationPrice: "50000",
|
|
316
|
+
unrealizedPnl: "500",
|
|
317
|
+
leverage: 10,
|
|
318
|
+
},
|
|
319
|
+
]),
|
|
320
|
+
});
|
|
321
|
+
await run(adapter, ["trade", "close", "btc"]);
|
|
322
|
+
expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
|
|
323
|
+
expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "sell", "0.5");
|
|
324
|
+
});
|
|
325
|
+
it("SHORT position -> calls marketOrder with 'buy' (opposite side)", async () => {
|
|
326
|
+
const adapter = mockAdapter({
|
|
327
|
+
getPositions: vi.fn().mockResolvedValue([
|
|
328
|
+
{
|
|
329
|
+
symbol: "ETH",
|
|
330
|
+
side: "short",
|
|
331
|
+
size: "3.0",
|
|
332
|
+
entryPrice: "3000",
|
|
333
|
+
markPrice: "2900",
|
|
334
|
+
liquidationPrice: "4000",
|
|
335
|
+
unrealizedPnl: "300",
|
|
336
|
+
leverage: 5,
|
|
337
|
+
},
|
|
338
|
+
]),
|
|
339
|
+
});
|
|
340
|
+
await run(adapter, ["trade", "close", "eth"]);
|
|
341
|
+
expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
|
|
342
|
+
expect(adapter.marketOrder).toHaveBeenCalledWith("ETH", "buy", "3.0");
|
|
343
|
+
});
|
|
344
|
+
it("no position -> error, adapter.marketOrder NOT called", async () => {
|
|
345
|
+
const adapter = mockAdapter({
|
|
346
|
+
getPositions: vi.fn().mockResolvedValue([]),
|
|
347
|
+
});
|
|
348
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => {
|
|
349
|
+
throw new Error("process.exit");
|
|
350
|
+
});
|
|
351
|
+
try {
|
|
352
|
+
await run(adapter, ["trade", "close", "btc"]);
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
// Expected
|
|
356
|
+
}
|
|
357
|
+
expect(adapter.marketOrder).not.toHaveBeenCalled();
|
|
358
|
+
exitSpy.mockRestore();
|
|
359
|
+
});
|
|
360
|
+
it("uses the full position size to close", async () => {
|
|
361
|
+
const adapter = mockAdapter({
|
|
362
|
+
getPositions: vi.fn().mockResolvedValue([
|
|
363
|
+
{
|
|
364
|
+
symbol: "SOL",
|
|
365
|
+
side: "long",
|
|
366
|
+
size: "100",
|
|
367
|
+
entryPrice: "150",
|
|
368
|
+
markPrice: "155",
|
|
369
|
+
liquidationPrice: "120",
|
|
370
|
+
unrealizedPnl: "500",
|
|
371
|
+
leverage: 3,
|
|
372
|
+
},
|
|
373
|
+
]),
|
|
374
|
+
});
|
|
375
|
+
await run(adapter, ["trade", "close", "sol"]);
|
|
376
|
+
expect(adapter.marketOrder).toHaveBeenCalledWith("SOL", "sell", "100");
|
|
377
|
+
});
|
|
378
|
+
it("logs execution after closing", async () => {
|
|
379
|
+
const adapter = mockAdapter({
|
|
380
|
+
getPositions: vi.fn().mockResolvedValue([
|
|
381
|
+
{
|
|
382
|
+
symbol: "BTC",
|
|
383
|
+
side: "long",
|
|
384
|
+
size: "0.1",
|
|
385
|
+
entryPrice: "60000",
|
|
386
|
+
markPrice: "61000",
|
|
387
|
+
liquidationPrice: "50000",
|
|
388
|
+
unrealizedPnl: "100",
|
|
389
|
+
leverage: 10,
|
|
390
|
+
},
|
|
391
|
+
]),
|
|
392
|
+
});
|
|
393
|
+
await run(adapter, ["trade", "close", "btc"]);
|
|
394
|
+
expect(logExecution).toHaveBeenCalledTimes(1);
|
|
395
|
+
expect(logExecution).toHaveBeenCalledWith(expect.objectContaining({
|
|
396
|
+
type: "market_order",
|
|
397
|
+
symbol: "BTC",
|
|
398
|
+
side: "sell",
|
|
399
|
+
size: "0.1",
|
|
400
|
+
meta: expect.objectContaining({ action: "close", originalSide: "long" }),
|
|
401
|
+
}));
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
// ══════════════════════════════════════════════════════════════
|
|
405
|
+
// 9. Close All -- iterates all positions
|
|
406
|
+
// ══════════════════════════════════════════════════════════════
|
|
407
|
+
describe("trade close-all -- iterates all positions", () => {
|
|
408
|
+
it("3 positions (BTC long, ETH short, SOL long) -> 3 marketOrder calls with correct opposite sides", async () => {
|
|
409
|
+
const adapter = mockAdapter({
|
|
410
|
+
getPositions: vi.fn().mockResolvedValue([
|
|
411
|
+
{ symbol: "BTC", side: "long", size: "0.5", entryPrice: "60000", markPrice: "61000", liquidationPrice: "50000", unrealizedPnl: "500", leverage: 10 },
|
|
412
|
+
{ symbol: "ETH", side: "short", size: "5.0", entryPrice: "3000", markPrice: "2900", liquidationPrice: "4000", unrealizedPnl: "500", leverage: 5 },
|
|
413
|
+
{ symbol: "SOL", side: "long", size: "100", entryPrice: "150", markPrice: "155", liquidationPrice: "120", unrealizedPnl: "500", leverage: 3 },
|
|
414
|
+
]),
|
|
415
|
+
});
|
|
416
|
+
await run(adapter, ["trade", "close-all"]);
|
|
417
|
+
expect(adapter.marketOrder).toHaveBeenCalledTimes(3);
|
|
418
|
+
expect(adapter.marketOrder).toHaveBeenNthCalledWith(1, "BTC", "sell", "0.5");
|
|
419
|
+
expect(adapter.marketOrder).toHaveBeenNthCalledWith(2, "ETH", "buy", "5.0");
|
|
420
|
+
expect(adapter.marketOrder).toHaveBeenNthCalledWith(3, "SOL", "sell", "100");
|
|
421
|
+
});
|
|
422
|
+
it("0 positions -> no marketOrder calls", async () => {
|
|
423
|
+
const adapter = mockAdapter({
|
|
424
|
+
getPositions: vi.fn().mockResolvedValue([]),
|
|
425
|
+
});
|
|
426
|
+
await run(adapter, ["trade", "close-all"]);
|
|
427
|
+
expect(adapter.marketOrder).not.toHaveBeenCalled();
|
|
428
|
+
});
|
|
429
|
+
it("logs execution for each closed position", async () => {
|
|
430
|
+
const adapter = mockAdapter({
|
|
431
|
+
getPositions: vi.fn().mockResolvedValue([
|
|
432
|
+
{ symbol: "BTC", side: "long", size: "0.5", entryPrice: "60000", markPrice: "61000", liquidationPrice: "50000", unrealizedPnl: "500", leverage: 10 },
|
|
433
|
+
{ symbol: "ETH", side: "short", size: "2.0", entryPrice: "3000", markPrice: "2900", liquidationPrice: "4000", unrealizedPnl: "200", leverage: 5 },
|
|
434
|
+
]),
|
|
435
|
+
});
|
|
436
|
+
await run(adapter, ["trade", "close-all"]);
|
|
437
|
+
expect(logExecution).toHaveBeenCalledTimes(2);
|
|
438
|
+
expect(logExecution).toHaveBeenNthCalledWith(1, expect.objectContaining({
|
|
439
|
+
symbol: "BTC",
|
|
440
|
+
side: "sell",
|
|
441
|
+
meta: expect.objectContaining({ action: "close-all", originalSide: "long" }),
|
|
442
|
+
}));
|
|
443
|
+
expect(logExecution).toHaveBeenNthCalledWith(2, expect.objectContaining({
|
|
444
|
+
symbol: "ETH",
|
|
445
|
+
side: "buy",
|
|
446
|
+
meta: expect.objectContaining({ action: "close-all", originalSide: "short" }),
|
|
447
|
+
}));
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
// ══════════════════════════════════════════════════════════════
|
|
451
|
+
// 10. Flatten -- cancel + close
|
|
452
|
+
// ══════════════════════════════════════════════════════════════
|
|
453
|
+
describe("trade flatten -- cancel + close", () => {
|
|
454
|
+
it("calls cancelAllOrders first, then marketOrder for each position with opposite side", async () => {
|
|
455
|
+
const callOrder = [];
|
|
456
|
+
const adapter = mockAdapter({
|
|
457
|
+
cancelAllOrders: vi.fn().mockImplementation(async () => {
|
|
458
|
+
callOrder.push("cancelAllOrders");
|
|
459
|
+
return { cancelled: 2 };
|
|
460
|
+
}),
|
|
461
|
+
getPositions: vi.fn().mockResolvedValue([
|
|
462
|
+
{ symbol: "BTC", side: "long", size: "1.0", entryPrice: "60000", markPrice: "61000", liquidationPrice: "50000", unrealizedPnl: "1000", leverage: 10 },
|
|
463
|
+
{ symbol: "ETH", side: "short", size: "10", entryPrice: "3000", markPrice: "2900", liquidationPrice: "4000", unrealizedPnl: "1000", leverage: 5 },
|
|
464
|
+
]),
|
|
465
|
+
marketOrder: vi.fn().mockImplementation(async (symbol) => {
|
|
466
|
+
callOrder.push(`marketOrder:${symbol}`);
|
|
467
|
+
return { orderId: "flat" };
|
|
468
|
+
}),
|
|
469
|
+
});
|
|
470
|
+
await run(adapter, ["trade", "flatten"]);
|
|
471
|
+
// Verify order: cancel first, then close positions
|
|
472
|
+
expect(callOrder).toEqual(["cancelAllOrders", "marketOrder:BTC", "marketOrder:ETH"]);
|
|
473
|
+
expect(adapter.cancelAllOrders).toHaveBeenCalledTimes(1);
|
|
474
|
+
expect(adapter.marketOrder).toHaveBeenCalledTimes(2);
|
|
475
|
+
expect(adapter.marketOrder).toHaveBeenNthCalledWith(1, "BTC", "sell", "1.0");
|
|
476
|
+
expect(adapter.marketOrder).toHaveBeenNthCalledWith(2, "ETH", "buy", "10");
|
|
477
|
+
});
|
|
478
|
+
it("with no positions: still calls cancelAllOrders, but no marketOrder", async () => {
|
|
479
|
+
const adapter = mockAdapter({
|
|
480
|
+
getPositions: vi.fn().mockResolvedValue([]),
|
|
481
|
+
});
|
|
482
|
+
await run(adapter, ["trade", "flatten"]);
|
|
483
|
+
expect(adapter.cancelAllOrders).toHaveBeenCalledTimes(1);
|
|
484
|
+
expect(adapter.marketOrder).not.toHaveBeenCalled();
|
|
485
|
+
});
|
|
486
|
+
it("logs execution for each position closed during flatten", async () => {
|
|
487
|
+
const adapter = mockAdapter({
|
|
488
|
+
getPositions: vi.fn().mockResolvedValue([
|
|
489
|
+
{ symbol: "SOL", side: "long", size: "50", entryPrice: "150", markPrice: "155", liquidationPrice: "120", unrealizedPnl: "250", leverage: 3 },
|
|
490
|
+
]),
|
|
491
|
+
});
|
|
492
|
+
await run(adapter, ["trade", "flatten"]);
|
|
493
|
+
expect(logExecution).toHaveBeenCalledWith(expect.objectContaining({
|
|
494
|
+
type: "market_order",
|
|
495
|
+
symbol: "SOL",
|
|
496
|
+
side: "sell",
|
|
497
|
+
size: "50",
|
|
498
|
+
meta: expect.objectContaining({ action: "flatten", originalSide: "long" }),
|
|
499
|
+
}));
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
// ══════════════════════════════════════════════════════════════
|
|
503
|
+
// 11. Reduce -- percentage calculation
|
|
504
|
+
// ══════════════════════════════════════════════════════════════
|
|
505
|
+
describe("trade reduce -- percentage calculation", () => {
|
|
506
|
+
it("BTC long size=10, reduce 50% -> marketOrder('BTC', 'sell', '5')", async () => {
|
|
507
|
+
const adapter = mockAdapter({
|
|
508
|
+
getPositions: vi.fn().mockResolvedValue([
|
|
509
|
+
{ symbol: "BTC", side: "long", size: "10", entryPrice: "60000", markPrice: "61000", liquidationPrice: "50000", unrealizedPnl: "10000", leverage: 10 },
|
|
510
|
+
]),
|
|
511
|
+
});
|
|
512
|
+
await run(adapter, ["trade", "reduce", "btc", "50"]);
|
|
513
|
+
expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
|
|
514
|
+
expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "sell", "5");
|
|
515
|
+
});
|
|
516
|
+
it("BTC short size=4, reduce 25% -> marketOrder('BTC', 'buy', '1')", async () => {
|
|
517
|
+
const adapter = mockAdapter({
|
|
518
|
+
getPositions: vi.fn().mockResolvedValue([
|
|
519
|
+
{ symbol: "BTC", side: "short", size: "4", entryPrice: "60000", markPrice: "59000", liquidationPrice: "70000", unrealizedPnl: "4000", leverage: 10 },
|
|
520
|
+
]),
|
|
521
|
+
});
|
|
522
|
+
await run(adapter, ["trade", "reduce", "btc", "25"]);
|
|
523
|
+
expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
|
|
524
|
+
expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "buy", "1");
|
|
525
|
+
});
|
|
526
|
+
it("reduce 100% -> closes full position", async () => {
|
|
527
|
+
const adapter = mockAdapter({
|
|
528
|
+
getPositions: vi.fn().mockResolvedValue([
|
|
529
|
+
{ symbol: "ETH", side: "long", size: "5", entryPrice: "3000", markPrice: "3100", liquidationPrice: "2500", unrealizedPnl: "500", leverage: 5 },
|
|
530
|
+
]),
|
|
531
|
+
});
|
|
532
|
+
await run(adapter, ["trade", "reduce", "eth", "100"]);
|
|
533
|
+
expect(adapter.marketOrder).toHaveBeenCalledWith("ETH", "sell", "5");
|
|
534
|
+
});
|
|
535
|
+
it("no position -> error, no adapter call", async () => {
|
|
536
|
+
const adapter = mockAdapter({
|
|
537
|
+
getPositions: vi.fn().mockResolvedValue([]),
|
|
538
|
+
});
|
|
539
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => {
|
|
540
|
+
throw new Error("process.exit");
|
|
541
|
+
});
|
|
542
|
+
try {
|
|
543
|
+
await run(adapter, ["trade", "reduce", "btc", "50"]);
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
// Expected
|
|
547
|
+
}
|
|
548
|
+
expect(adapter.marketOrder).not.toHaveBeenCalled();
|
|
549
|
+
exitSpy.mockRestore();
|
|
550
|
+
});
|
|
551
|
+
it("invalid percent 0 -> error, no adapter call", async () => {
|
|
552
|
+
const adapter = mockAdapter({
|
|
553
|
+
getPositions: vi.fn().mockResolvedValue([
|
|
554
|
+
{ symbol: "BTC", side: "long", size: "10", entryPrice: "60000", markPrice: "61000", liquidationPrice: "50000", unrealizedPnl: "10000", leverage: 10 },
|
|
555
|
+
]),
|
|
556
|
+
});
|
|
557
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => {
|
|
558
|
+
throw new Error("process.exit");
|
|
559
|
+
});
|
|
560
|
+
try {
|
|
561
|
+
await run(adapter, ["trade", "reduce", "btc", "0"]);
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
// Expected
|
|
565
|
+
}
|
|
566
|
+
expect(adapter.marketOrder).not.toHaveBeenCalled();
|
|
567
|
+
exitSpy.mockRestore();
|
|
568
|
+
});
|
|
569
|
+
it("invalid percent 101 -> error, no adapter call", async () => {
|
|
570
|
+
const adapter = mockAdapter({
|
|
571
|
+
getPositions: vi.fn().mockResolvedValue([
|
|
572
|
+
{ symbol: "BTC", side: "long", size: "10", entryPrice: "60000", markPrice: "61000", liquidationPrice: "50000", unrealizedPnl: "10000", leverage: 10 },
|
|
573
|
+
]),
|
|
574
|
+
});
|
|
575
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => {
|
|
576
|
+
throw new Error("process.exit");
|
|
577
|
+
});
|
|
578
|
+
try {
|
|
579
|
+
await run(adapter, ["trade", "reduce", "btc", "101"]);
|
|
580
|
+
}
|
|
581
|
+
catch {
|
|
582
|
+
// Expected
|
|
583
|
+
}
|
|
584
|
+
expect(adapter.marketOrder).not.toHaveBeenCalled();
|
|
585
|
+
exitSpy.mockRestore();
|
|
586
|
+
});
|
|
587
|
+
it("invalid percent -1 -> error, no adapter call", async () => {
|
|
588
|
+
const adapter = mockAdapter({
|
|
589
|
+
getPositions: vi.fn().mockResolvedValue([
|
|
590
|
+
{ symbol: "BTC", side: "long", size: "10", entryPrice: "60000", markPrice: "61000", liquidationPrice: "50000", unrealizedPnl: "10000", leverage: 10 },
|
|
591
|
+
]),
|
|
592
|
+
});
|
|
593
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => {
|
|
594
|
+
throw new Error("process.exit");
|
|
595
|
+
});
|
|
596
|
+
try {
|
|
597
|
+
await run(adapter, ["trade", "reduce", "btc", "-1"]);
|
|
598
|
+
}
|
|
599
|
+
catch {
|
|
600
|
+
// Expected
|
|
601
|
+
}
|
|
602
|
+
expect(adapter.marketOrder).not.toHaveBeenCalled();
|
|
603
|
+
exitSpy.mockRestore();
|
|
604
|
+
});
|
|
605
|
+
it("logs execution with reduce metadata", async () => {
|
|
606
|
+
const adapter = mockAdapter({
|
|
607
|
+
getPositions: vi.fn().mockResolvedValue([
|
|
608
|
+
{ symbol: "BTC", side: "long", size: "10", entryPrice: "60000", markPrice: "61000", liquidationPrice: "50000", unrealizedPnl: "10000", leverage: 10 },
|
|
609
|
+
]),
|
|
610
|
+
});
|
|
611
|
+
await run(adapter, ["trade", "reduce", "btc", "50"]);
|
|
612
|
+
expect(logExecution).toHaveBeenCalledWith(expect.objectContaining({
|
|
613
|
+
type: "market_order",
|
|
614
|
+
symbol: "BTC",
|
|
615
|
+
side: "sell",
|
|
616
|
+
size: "5",
|
|
617
|
+
meta: expect.objectContaining({ action: "reduce", percent: 50, originalSize: "10", originalSide: "long" }),
|
|
618
|
+
}));
|
|
619
|
+
});
|
|
620
|
+
it("fractional percentage: reduce 33.3% of size 9 -> '2.9970000000000003' (floating point)", async () => {
|
|
621
|
+
const adapter = mockAdapter({
|
|
622
|
+
getPositions: vi.fn().mockResolvedValue([
|
|
623
|
+
{ symbol: "BTC", side: "long", size: "9", entryPrice: "60000", markPrice: "61000", liquidationPrice: "50000", unrealizedPnl: "9000", leverage: 10 },
|
|
624
|
+
]),
|
|
625
|
+
});
|
|
626
|
+
await run(adapter, ["trade", "reduce", "btc", "33.3"]);
|
|
627
|
+
expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
|
|
628
|
+
// 9 * 33.3 / 100 = 2.997
|
|
629
|
+
const callArgs = vi.mocked(adapter.marketOrder).mock.calls[0];
|
|
630
|
+
expect(callArgs[0]).toBe("BTC");
|
|
631
|
+
expect(callArgs[1]).toBe("sell");
|
|
632
|
+
expect(parseFloat(callArgs[2])).toBeCloseTo(2.997, 2);
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
// ══════════════════════════════════════════════════════════════
|
|
636
|
+
// 12. Edit Order
|
|
637
|
+
// ══════════════════════════════════════════════════════════════
|
|
638
|
+
describe("trade edit", () => {
|
|
639
|
+
it("calls adapter.editOrder with uppercased symbol, orderId, price, size", async () => {
|
|
640
|
+
const adapter = mockAdapter();
|
|
641
|
+
await run(adapter, ["trade", "edit", "btc", "order123", "65000", "0.2"]);
|
|
642
|
+
expect(adapter.editOrder).toHaveBeenCalledTimes(1);
|
|
643
|
+
expect(adapter.editOrder).toHaveBeenCalledWith("BTC", "order123", "65000", "0.2");
|
|
644
|
+
});
|
|
645
|
+
it("preserves orderId exactly as provided (case-sensitive)", async () => {
|
|
646
|
+
const adapter = mockAdapter();
|
|
647
|
+
await run(adapter, ["trade", "edit", "eth", "MyOrder-ABC", "3000", "1"]);
|
|
648
|
+
expect(adapter.editOrder).toHaveBeenCalledWith("ETH", "MyOrder-ABC", "3000", "1");
|
|
649
|
+
});
|
|
650
|
+
it("no other adapter methods are called", async () => {
|
|
651
|
+
const adapter = mockAdapter();
|
|
652
|
+
await run(adapter, ["trade", "edit", "btc", "order123", "65000", "0.2"]);
|
|
653
|
+
expect(adapter.marketOrder).not.toHaveBeenCalled();
|
|
654
|
+
expect(adapter.limitOrder).not.toHaveBeenCalled();
|
|
655
|
+
expect(adapter.cancelOrder).not.toHaveBeenCalled();
|
|
656
|
+
});
|
|
657
|
+
});
|
|
658
|
+
// ══════════════════════════════════════════════════════════════
|
|
659
|
+
// 13. TP/SL side mapping verification (via stopOrder on generic adapter)
|
|
660
|
+
// ══════════════════════════════════════════════════════════════
|
|
661
|
+
describe("trade tpsl -- side mapping for generic (Lighter-style) adapter", () => {
|
|
662
|
+
// Note: The tpsl command uses instanceof checks (PacificaAdapter, HyperliquidAdapter, LighterAdapter).
|
|
663
|
+
// Our mock adapter won't match any instanceof check, so it will hit the `else` branch
|
|
664
|
+
// and call errorAndExit. However, we CAN test the side mapping logic indirectly
|
|
665
|
+
// through the close/reduce commands, which is already tested above.
|
|
666
|
+
//
|
|
667
|
+
// For completeness, we verify the side mapping expectation documented in the spec:
|
|
668
|
+
// - BUY position (long): close side = "sell"
|
|
669
|
+
// - SELL position (short): close side = "buy"
|
|
670
|
+
it("LONG position close side is 'sell'", async () => {
|
|
671
|
+
const adapter = mockAdapter({
|
|
672
|
+
getPositions: vi.fn().mockResolvedValue([
|
|
673
|
+
{ symbol: "BTC", side: "long", size: "1.0", entryPrice: "60000", markPrice: "61000", liquidationPrice: "50000", unrealizedPnl: "1000", leverage: 10 },
|
|
674
|
+
]),
|
|
675
|
+
});
|
|
676
|
+
await run(adapter, ["trade", "close", "btc"]);
|
|
677
|
+
expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "sell", "1.0");
|
|
678
|
+
});
|
|
679
|
+
it("SHORT position close side is 'buy'", async () => {
|
|
680
|
+
const adapter = mockAdapter({
|
|
681
|
+
getPositions: vi.fn().mockResolvedValue([
|
|
682
|
+
{ symbol: "BTC", side: "short", size: "1.0", entryPrice: "60000", markPrice: "59000", liquidationPrice: "70000", unrealizedPnl: "1000", leverage: 10 },
|
|
683
|
+
]),
|
|
684
|
+
});
|
|
685
|
+
await run(adapter, ["trade", "close", "btc"]);
|
|
686
|
+
expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "buy", "1.0");
|
|
687
|
+
});
|
|
688
|
+
it("close-all maps sides correctly for mixed positions", async () => {
|
|
689
|
+
const adapter = mockAdapter({
|
|
690
|
+
getPositions: vi.fn().mockResolvedValue([
|
|
691
|
+
{ symbol: "BTC", side: "long", size: "0.5", entryPrice: "60000", markPrice: "61000", liquidationPrice: "50000", unrealizedPnl: "500", leverage: 10 },
|
|
692
|
+
{ symbol: "ETH", side: "short", size: "5.0", entryPrice: "3000", markPrice: "2900", liquidationPrice: "4000", unrealizedPnl: "500", leverage: 5 },
|
|
693
|
+
]),
|
|
694
|
+
});
|
|
695
|
+
await run(adapter, ["trade", "close-all"]);
|
|
696
|
+
expect(adapter.marketOrder).toHaveBeenNthCalledWith(1, "BTC", "sell", "0.5");
|
|
697
|
+
expect(adapter.marketOrder).toHaveBeenNthCalledWith(2, "ETH", "buy", "5.0");
|
|
698
|
+
});
|
|
699
|
+
it("reduce on LONG uses 'sell'", async () => {
|
|
700
|
+
const adapter = mockAdapter({
|
|
701
|
+
getPositions: vi.fn().mockResolvedValue([
|
|
702
|
+
{ symbol: "BTC", side: "long", size: "2", entryPrice: "60000", markPrice: "61000", liquidationPrice: "50000", unrealizedPnl: "2000", leverage: 10 },
|
|
703
|
+
]),
|
|
704
|
+
});
|
|
705
|
+
await run(adapter, ["trade", "reduce", "btc", "50"]);
|
|
706
|
+
expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "sell", "1");
|
|
707
|
+
});
|
|
708
|
+
it("reduce on SHORT uses 'buy'", async () => {
|
|
709
|
+
const adapter = mockAdapter({
|
|
710
|
+
getPositions: vi.fn().mockResolvedValue([
|
|
711
|
+
{ symbol: "BTC", side: "short", size: "2", entryPrice: "60000", markPrice: "59000", liquidationPrice: "70000", unrealizedPnl: "2000", leverage: 10 },
|
|
712
|
+
]),
|
|
713
|
+
});
|
|
714
|
+
await run(adapter, ["trade", "reduce", "btc", "50"]);
|
|
715
|
+
expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "buy", "1");
|
|
716
|
+
});
|
|
717
|
+
});
|
|
718
|
+
// ══════════════════════════════════════════════════════════════
|
|
719
|
+
// 14. Symbol uppercasing -- cross-cutting concern
|
|
720
|
+
// ══════════════════════════════════════════════════════════════
|
|
721
|
+
describe("symbol uppercasing -- cross-cutting", () => {
|
|
722
|
+
it("market order uppercases", async () => {
|
|
723
|
+
const adapter = mockAdapter();
|
|
724
|
+
await run(adapter, ["trade", "market", "doge", "buy", "100"]);
|
|
725
|
+
expect(vi.mocked(adapter.marketOrder).mock.calls[0][0]).toBe("DOGE");
|
|
726
|
+
});
|
|
727
|
+
it("limit order uppercases", async () => {
|
|
728
|
+
const adapter = mockAdapter();
|
|
729
|
+
await run(adapter, ["trade", "limit", "arb", "sell", "1.5", "50"]);
|
|
730
|
+
expect(vi.mocked(adapter.limitOrder).mock.calls[0][0]).toBe("ARB");
|
|
731
|
+
});
|
|
732
|
+
it("stop order uppercases", async () => {
|
|
733
|
+
const adapter = mockAdapter();
|
|
734
|
+
await run(adapter, ["trade", "stop", "matic", "buy", "1.0", "100"]);
|
|
735
|
+
expect(vi.mocked(adapter.stopOrder).mock.calls[0][0]).toBe("MATIC");
|
|
736
|
+
});
|
|
737
|
+
it("cancel order uppercases", async () => {
|
|
738
|
+
const adapter = mockAdapter();
|
|
739
|
+
await run(adapter, ["trade", "cancel", "avax", "id1"]);
|
|
740
|
+
expect(vi.mocked(adapter.cancelOrder).mock.calls[0][0]).toBe("AVAX");
|
|
741
|
+
});
|
|
742
|
+
it("edit order uppercases", async () => {
|
|
743
|
+
const adapter = mockAdapter();
|
|
744
|
+
await run(adapter, ["trade", "edit", "link", "o1", "20", "5"]);
|
|
745
|
+
expect(vi.mocked(adapter.editOrder).mock.calls[0][0]).toBe("LINK");
|
|
746
|
+
});
|
|
747
|
+
it("leverage uppercases", async () => {
|
|
748
|
+
const adapter = mockAdapter();
|
|
749
|
+
await run(adapter, ["trade", "leverage", "near", "5"]);
|
|
750
|
+
expect(vi.mocked(adapter.setLeverage).mock.calls[0][0]).toBe("NEAR");
|
|
751
|
+
});
|
|
752
|
+
it("close uppercases", async () => {
|
|
753
|
+
const adapter = mockAdapter({
|
|
754
|
+
getPositions: vi.fn().mockResolvedValue([
|
|
755
|
+
{ symbol: "APT", side: "long", size: "10", entryPrice: "10", markPrice: "11", liquidationPrice: "8", unrealizedPnl: "10", leverage: 5 },
|
|
756
|
+
]),
|
|
757
|
+
});
|
|
758
|
+
await run(adapter, ["trade", "close", "apt"]);
|
|
759
|
+
expect(vi.mocked(adapter.marketOrder).mock.calls[0][0]).toBe("APT");
|
|
760
|
+
});
|
|
761
|
+
it("reduce uppercases", async () => {
|
|
762
|
+
const adapter = mockAdapter({
|
|
763
|
+
getPositions: vi.fn().mockResolvedValue([
|
|
764
|
+
{ symbol: "OP", side: "short", size: "20", entryPrice: "2", markPrice: "1.9", liquidationPrice: "3", unrealizedPnl: "2", leverage: 3 },
|
|
765
|
+
]),
|
|
766
|
+
});
|
|
767
|
+
await run(adapter, ["trade", "reduce", "op", "50"]);
|
|
768
|
+
expect(vi.mocked(adapter.marketOrder).mock.calls[0][0]).toBe("OP");
|
|
769
|
+
});
|
|
770
|
+
});
|
|
771
|
+
// ══════════════════════════════════════════════════════════════
|
|
772
|
+
// 15. No unexpected extra adapter calls
|
|
773
|
+
// ══════════════════════════════════════════════════════════════
|
|
774
|
+
describe("no unexpected extra adapter calls", () => {
|
|
775
|
+
it("market order only calls marketOrder", async () => {
|
|
776
|
+
const adapter = mockAdapter();
|
|
777
|
+
await run(adapter, ["trade", "market", "btc", "buy", "0.1"]);
|
|
778
|
+
expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
|
|
779
|
+
expect(adapter.limitOrder).not.toHaveBeenCalled();
|
|
780
|
+
expect(adapter.stopOrder).not.toHaveBeenCalled();
|
|
781
|
+
expect(adapter.cancelOrder).not.toHaveBeenCalled();
|
|
782
|
+
expect(adapter.cancelAllOrders).not.toHaveBeenCalled();
|
|
783
|
+
expect(adapter.editOrder).not.toHaveBeenCalled();
|
|
784
|
+
expect(adapter.setLeverage).not.toHaveBeenCalled();
|
|
785
|
+
expect(adapter.getPositions).not.toHaveBeenCalled();
|
|
786
|
+
});
|
|
787
|
+
it("limit order only calls limitOrder", async () => {
|
|
788
|
+
const adapter = mockAdapter();
|
|
789
|
+
await run(adapter, ["trade", "limit", "btc", "buy", "60000", "0.1"]);
|
|
790
|
+
expect(adapter.limitOrder).toHaveBeenCalledTimes(1);
|
|
791
|
+
expect(adapter.marketOrder).not.toHaveBeenCalled();
|
|
792
|
+
expect(adapter.stopOrder).not.toHaveBeenCalled();
|
|
793
|
+
});
|
|
794
|
+
it("stop order only calls stopOrder", async () => {
|
|
795
|
+
const adapter = mockAdapter();
|
|
796
|
+
await run(adapter, ["trade", "stop", "btc", "sell", "60000", "0.1"]);
|
|
797
|
+
expect(adapter.stopOrder).toHaveBeenCalledTimes(1);
|
|
798
|
+
expect(adapter.marketOrder).not.toHaveBeenCalled();
|
|
799
|
+
expect(adapter.limitOrder).not.toHaveBeenCalled();
|
|
800
|
+
});
|
|
801
|
+
it("leverage only calls setLeverage", async () => {
|
|
802
|
+
const adapter = mockAdapter();
|
|
803
|
+
await run(adapter, ["trade", "leverage", "btc", "10"]);
|
|
804
|
+
expect(adapter.setLeverage).toHaveBeenCalledTimes(1);
|
|
805
|
+
expect(adapter.marketOrder).not.toHaveBeenCalled();
|
|
806
|
+
});
|
|
807
|
+
it("flatten calls cancelAllOrders + getPositions + marketOrder only", async () => {
|
|
808
|
+
const adapter = mockAdapter({
|
|
809
|
+
getPositions: vi.fn().mockResolvedValue([
|
|
810
|
+
{ symbol: "BTC", side: "long", size: "1", entryPrice: "60000", markPrice: "61000", liquidationPrice: "50000", unrealizedPnl: "1000", leverage: 10 },
|
|
811
|
+
]),
|
|
812
|
+
});
|
|
813
|
+
await run(adapter, ["trade", "flatten"]);
|
|
814
|
+
expect(adapter.cancelAllOrders).toHaveBeenCalledTimes(1);
|
|
815
|
+
expect(adapter.getPositions).toHaveBeenCalledTimes(1);
|
|
816
|
+
expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
|
|
817
|
+
expect(adapter.limitOrder).not.toHaveBeenCalled();
|
|
818
|
+
expect(adapter.stopOrder).not.toHaveBeenCalled();
|
|
819
|
+
expect(adapter.editOrder).not.toHaveBeenCalled();
|
|
820
|
+
});
|
|
821
|
+
});
|