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,600 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { executePlan } from "../../plan-executor.js";
|
|
3
|
+
vi.mock("../../execution-log.js", () => ({ logExecution: vi.fn() }));
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Mock adapter factory
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
function createMockAdapter(overrides) {
|
|
8
|
+
const positions = overrides?.positions ?? [];
|
|
9
|
+
const balance = {
|
|
10
|
+
equity: "10000",
|
|
11
|
+
available: "8000",
|
|
12
|
+
marginUsed: "2000",
|
|
13
|
+
unrealizedPnl: "0",
|
|
14
|
+
...overrides?.balance,
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
name: "mock",
|
|
18
|
+
getMarkets: vi.fn().mockResolvedValue([]),
|
|
19
|
+
getOrderbook: vi.fn().mockResolvedValue({ bids: [], asks: [] }),
|
|
20
|
+
getRecentTrades: vi.fn().mockResolvedValue([]),
|
|
21
|
+
getFundingHistory: vi.fn().mockResolvedValue([]),
|
|
22
|
+
getKlines: vi.fn().mockResolvedValue([]),
|
|
23
|
+
getBalance: vi.fn().mockResolvedValue(balance),
|
|
24
|
+
getPositions: vi.fn().mockResolvedValue(positions),
|
|
25
|
+
getOpenOrders: vi.fn().mockResolvedValue([]),
|
|
26
|
+
getOrderHistory: vi.fn().mockResolvedValue([]),
|
|
27
|
+
getTradeHistory: vi.fn().mockResolvedValue([]),
|
|
28
|
+
getFundingPayments: vi.fn().mockResolvedValue([]),
|
|
29
|
+
marketOrder: vi.fn().mockResolvedValue({ orderId: "m-1" }),
|
|
30
|
+
limitOrder: vi.fn().mockResolvedValue({ orderId: "l-1" }),
|
|
31
|
+
editOrder: vi.fn().mockResolvedValue({ orderId: "e-1" }),
|
|
32
|
+
cancelOrder: vi.fn().mockResolvedValue({ cancelled: true }),
|
|
33
|
+
cancelAllOrders: vi.fn().mockResolvedValue({ cancelled: true }),
|
|
34
|
+
setLeverage: vi.fn().mockResolvedValue({ ok: true }),
|
|
35
|
+
stopOrder: vi.fn().mockResolvedValue({ orderId: "s-1" }),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// 1. Open long BTC with stop loss
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
describe("Scenario 1: Open long BTC with stop loss", () => {
|
|
42
|
+
it("executes setLeverage -> marketOrder (buy) -> stopOrder (sell) in dependency order", async () => {
|
|
43
|
+
const adapter = createMockAdapter();
|
|
44
|
+
const plan = {
|
|
45
|
+
version: "1.0",
|
|
46
|
+
steps: [
|
|
47
|
+
{ id: "lev", action: "set_leverage", params: { symbol: "BTC", leverage: 10 } },
|
|
48
|
+
{ id: "buy", action: "market_order", params: { symbol: "BTC", side: "buy", size: "0.01" }, dependsOn: "lev" },
|
|
49
|
+
{ id: "sl", action: "stop_order", params: { symbol: "BTC", side: "sell", size: "0.01", triggerPrice: "60000" }, dependsOn: "buy" },
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
const result = await executePlan(adapter, plan);
|
|
53
|
+
expect(result.status).toBe("completed");
|
|
54
|
+
expect(result.steps).toHaveLength(3);
|
|
55
|
+
expect(result.steps.every(s => s.status === "success")).toBe(true);
|
|
56
|
+
// setLeverage: symbol, leverage, marginMode (default "cross")
|
|
57
|
+
expect(adapter.setLeverage).toHaveBeenCalledTimes(1);
|
|
58
|
+
expect(adapter.setLeverage).toHaveBeenCalledWith("BTC", 10, "cross");
|
|
59
|
+
// marketOrder: symbol, side, size
|
|
60
|
+
expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
|
|
61
|
+
expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "buy", "0.01");
|
|
62
|
+
// stopOrder: symbol, side, size, triggerPrice, opts
|
|
63
|
+
// CRITICAL: stop loss side is SELL (opposite of long)
|
|
64
|
+
expect(adapter.stopOrder).toHaveBeenCalledTimes(1);
|
|
65
|
+
expect(adapter.stopOrder).toHaveBeenCalledWith("BTC", "sell", "0.01", "60000", { limitPrice: undefined, reduceOnly: false });
|
|
66
|
+
// Verify call order
|
|
67
|
+
const setLevOrder = adapter.setLeverage.mock.invocationCallOrder[0];
|
|
68
|
+
const buyOrder = adapter.marketOrder.mock.invocationCallOrder[0];
|
|
69
|
+
const slOrder = adapter.stopOrder.mock.invocationCallOrder[0];
|
|
70
|
+
expect(setLevOrder).toBeLessThan(buyOrder);
|
|
71
|
+
expect(buyOrder).toBeLessThan(slOrder);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// 2. Delta-neutral hedge
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
describe("Scenario 2: Delta-neutral hedge", () => {
|
|
78
|
+
it("opens long BTC and short ETH with correct sides", async () => {
|
|
79
|
+
const adapter = createMockAdapter();
|
|
80
|
+
const plan = {
|
|
81
|
+
version: "1.0",
|
|
82
|
+
steps: [
|
|
83
|
+
{ id: "long", action: "market_order", params: { symbol: "BTC", side: "buy", size: "0.01" } },
|
|
84
|
+
{ id: "short", action: "market_order", params: { symbol: "ETH", side: "sell", size: "0.1" } },
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
const result = await executePlan(adapter, plan);
|
|
88
|
+
expect(result.status).toBe("completed");
|
|
89
|
+
expect(adapter.marketOrder).toHaveBeenCalledTimes(2);
|
|
90
|
+
// First call: long BTC
|
|
91
|
+
expect(adapter.marketOrder).toHaveBeenNthCalledWith(1, "BTC", "buy", "0.01");
|
|
92
|
+
// Second call: short ETH
|
|
93
|
+
expect(adapter.marketOrder).toHaveBeenNthCalledWith(2, "ETH", "sell", "0.1");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// 3. Emergency close all
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
describe("Scenario 3: Emergency close all", () => {
|
|
100
|
+
it("cancels all orders then closes BTC long by selling", async () => {
|
|
101
|
+
const adapter = createMockAdapter({
|
|
102
|
+
positions: [
|
|
103
|
+
{
|
|
104
|
+
symbol: "BTC",
|
|
105
|
+
side: "long",
|
|
106
|
+
size: "0.5",
|
|
107
|
+
entryPrice: "65000",
|
|
108
|
+
markPrice: "64000",
|
|
109
|
+
liquidationPrice: "55000",
|
|
110
|
+
unrealizedPnl: "-500",
|
|
111
|
+
leverage: 10,
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
});
|
|
115
|
+
const plan = {
|
|
116
|
+
version: "1.0",
|
|
117
|
+
steps: [
|
|
118
|
+
{ id: "cancel", action: "cancel_all", params: {} },
|
|
119
|
+
{ id: "close", action: "close_position", params: { symbol: "BTC" }, dependsOn: "cancel" },
|
|
120
|
+
],
|
|
121
|
+
};
|
|
122
|
+
const result = await executePlan(adapter, plan);
|
|
123
|
+
expect(result.status).toBe("completed");
|
|
124
|
+
expect(result.steps).toHaveLength(2);
|
|
125
|
+
// cancelAllOrders called first
|
|
126
|
+
expect(adapter.cancelAllOrders).toHaveBeenCalledTimes(1);
|
|
127
|
+
// close_position fetches positions, then sells the long
|
|
128
|
+
expect(adapter.getPositions).toHaveBeenCalledTimes(1);
|
|
129
|
+
expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
|
|
130
|
+
// CRITICAL: closing a LONG = must SELL, not buy
|
|
131
|
+
expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "sell", "0.5");
|
|
132
|
+
// Verify order: cancel before market order
|
|
133
|
+
const cancelCallOrder = adapter.cancelAllOrders.mock.invocationCallOrder[0];
|
|
134
|
+
const marketCallOrder = adapter.marketOrder.mock.invocationCallOrder[0];
|
|
135
|
+
expect(cancelCallOrder).toBeLessThan(marketCallOrder);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// 4. Scale in: multiple buys with wait
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
describe("Scenario 4: Scale in with wait", () => {
|
|
142
|
+
it("buys ETH twice with a wait pause in between, then checks position", async () => {
|
|
143
|
+
const adapter = createMockAdapter({
|
|
144
|
+
positions: [
|
|
145
|
+
{
|
|
146
|
+
symbol: "ETH",
|
|
147
|
+
side: "long",
|
|
148
|
+
size: "2",
|
|
149
|
+
entryPrice: "3000",
|
|
150
|
+
markPrice: "3100",
|
|
151
|
+
liquidationPrice: "2500",
|
|
152
|
+
unrealizedPnl: "200",
|
|
153
|
+
leverage: 5,
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
});
|
|
157
|
+
const plan = {
|
|
158
|
+
version: "1.0",
|
|
159
|
+
steps: [
|
|
160
|
+
{ id: "buy1", action: "market_order", params: { symbol: "ETH", side: "buy", size: "1" } },
|
|
161
|
+
{ id: "wait", action: "wait", params: { ms: 100 }, dependsOn: "buy1" },
|
|
162
|
+
{ id: "buy2", action: "market_order", params: { symbol: "ETH", side: "buy", size: "1" }, dependsOn: "wait" },
|
|
163
|
+
{ id: "check", action: "check_position", params: { symbol: "ETH", mustExist: true }, dependsOn: "buy2" },
|
|
164
|
+
],
|
|
165
|
+
};
|
|
166
|
+
const before = Date.now();
|
|
167
|
+
const result = await executePlan(adapter, plan);
|
|
168
|
+
const elapsed = Date.now() - before;
|
|
169
|
+
expect(result.status).toBe("completed");
|
|
170
|
+
expect(result.steps).toHaveLength(4);
|
|
171
|
+
expect(result.steps.every(s => s.status === "success")).toBe(true);
|
|
172
|
+
// marketOrder called exactly twice, both buy ETH 1
|
|
173
|
+
expect(adapter.marketOrder).toHaveBeenCalledTimes(2);
|
|
174
|
+
expect(adapter.marketOrder).toHaveBeenNthCalledWith(1, "ETH", "buy", "1");
|
|
175
|
+
expect(adapter.marketOrder).toHaveBeenNthCalledWith(2, "ETH", "buy", "1");
|
|
176
|
+
// Wait actually paused at least ~100ms
|
|
177
|
+
expect(elapsed).toBeGreaterThanOrEqual(90); // allow small timing margin
|
|
178
|
+
// check_position succeeded (position exists)
|
|
179
|
+
expect(adapter.getPositions).toHaveBeenCalled();
|
|
180
|
+
const checkStep = result.steps.find(s => s.stepId === "check");
|
|
181
|
+
expect(checkStep?.status).toBe("success");
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// 5. Rollback on failure
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
describe("Scenario 5: Rollback on failure", () => {
|
|
188
|
+
it("rolls back (cancelAllOrders) when a step with onFailure=rollback throws", async () => {
|
|
189
|
+
const adapter = createMockAdapter();
|
|
190
|
+
// limitOrder will fail
|
|
191
|
+
adapter.limitOrder.mockRejectedValueOnce(new Error("Insufficient margin"));
|
|
192
|
+
const plan = {
|
|
193
|
+
version: "1.0",
|
|
194
|
+
steps: [
|
|
195
|
+
{ id: "buy", action: "market_order", params: { symbol: "BTC", side: "buy", size: "0.01" } },
|
|
196
|
+
{ id: "fail", action: "limit_order", params: { symbol: "ETH", side: "sell", size: "1", price: "2000" }, dependsOn: "buy", onFailure: "rollback" },
|
|
197
|
+
],
|
|
198
|
+
};
|
|
199
|
+
const result = await executePlan(adapter, plan);
|
|
200
|
+
// Overall status is failed
|
|
201
|
+
expect(result.status).toBe("failed");
|
|
202
|
+
// Step 1 succeeded
|
|
203
|
+
const buyStep = result.steps.find(s => s.stepId === "buy");
|
|
204
|
+
expect(buyStep?.status).toBe("success");
|
|
205
|
+
// Step 2 rolled back
|
|
206
|
+
const failStep = result.steps.find(s => s.stepId === "fail");
|
|
207
|
+
expect(failStep?.status).toBe("rolled_back");
|
|
208
|
+
expect(failStep?.error?.message).toContain("Insufficient margin");
|
|
209
|
+
// Rollback called cancelAllOrders (the rollback function cancels all for market_order steps)
|
|
210
|
+
expect(adapter.cancelAllOrders).toHaveBeenCalled();
|
|
211
|
+
// marketOrder was called once (the buy), limitOrder was called once (the failing sell)
|
|
212
|
+
expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
|
|
213
|
+
expect(adapter.limitOrder).toHaveBeenCalledTimes(1);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// 6. Skip non-critical step
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
describe("Scenario 6: Skip non-critical step", () => {
|
|
220
|
+
it("continues execution when a step with onFailure=skip fails", async () => {
|
|
221
|
+
const adapter = createMockAdapter();
|
|
222
|
+
// stopOrder will fail
|
|
223
|
+
adapter.stopOrder.mockRejectedValueOnce(new Error("Rate limit exceeded"));
|
|
224
|
+
const plan = {
|
|
225
|
+
version: "1.0",
|
|
226
|
+
steps: [
|
|
227
|
+
{ id: "buy", action: "market_order", params: { symbol: "BTC", side: "buy", size: "0.01" } },
|
|
228
|
+
{ id: "tp", action: "stop_order", params: { symbol: "BTC", side: "sell", size: "0.01", triggerPrice: "100000" }, onFailure: "skip" },
|
|
229
|
+
{ id: "check", action: "check_balance", params: {} },
|
|
230
|
+
],
|
|
231
|
+
};
|
|
232
|
+
const result = await executePlan(adapter, plan);
|
|
233
|
+
// Overall: completed (skip doesn't cause failure)
|
|
234
|
+
expect(result.status).toBe("completed");
|
|
235
|
+
expect(result.steps).toHaveLength(3);
|
|
236
|
+
// Step 1 succeeded
|
|
237
|
+
expect(result.steps[0].status).toBe("success");
|
|
238
|
+
// Step 2 was skipped
|
|
239
|
+
expect(result.steps[1].status).toBe("skipped");
|
|
240
|
+
expect(result.steps[1].error?.message).toContain("Rate limit exceeded");
|
|
241
|
+
// Step 3 still executed successfully
|
|
242
|
+
expect(result.steps[2].status).toBe("success");
|
|
243
|
+
expect(adapter.getBalance).toHaveBeenCalledTimes(1);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// 7. Balance gate
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
describe("Scenario 7: Balance gate - don't trade if balance too low", () => {
|
|
250
|
+
it("aborts the plan when check_balance fails with onFailure=abort", async () => {
|
|
251
|
+
const adapter = createMockAdapter({
|
|
252
|
+
balance: { available: "800", equity: "800", marginUsed: "0", unrealizedPnl: "0" },
|
|
253
|
+
});
|
|
254
|
+
const plan = {
|
|
255
|
+
version: "1.0",
|
|
256
|
+
steps: [
|
|
257
|
+
{ id: "gate", action: "check_balance", params: { minAvailable: 10000 }, onFailure: "abort" },
|
|
258
|
+
{ id: "buy", action: "market_order", params: { symbol: "BTC", side: "buy", size: "1" }, dependsOn: "gate" },
|
|
259
|
+
],
|
|
260
|
+
};
|
|
261
|
+
const result = await executePlan(adapter, plan);
|
|
262
|
+
// Plan failed/aborted at the gate
|
|
263
|
+
expect(result.status).toBe("failed");
|
|
264
|
+
// Gate step failed
|
|
265
|
+
const gateStep = result.steps.find(s => s.stepId === "gate");
|
|
266
|
+
expect(gateStep?.status).toBe("failed");
|
|
267
|
+
expect(gateStep?.error?.message).toContain("$800");
|
|
268
|
+
expect(gateStep?.error?.message).toContain("$10000");
|
|
269
|
+
// CRITICAL: marketOrder was NEVER called — the safety gate worked
|
|
270
|
+
expect(adapter.marketOrder).toHaveBeenCalledTimes(0);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// 8. Close position side correctness (CRITICAL)
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
describe("Scenario 8: Close position side correctness", () => {
|
|
277
|
+
it("closes a LONG position by SELLING (not buying)", async () => {
|
|
278
|
+
const adapter = createMockAdapter({
|
|
279
|
+
positions: [
|
|
280
|
+
{
|
|
281
|
+
symbol: "BTC",
|
|
282
|
+
side: "long",
|
|
283
|
+
size: "0.5",
|
|
284
|
+
entryPrice: "65000",
|
|
285
|
+
markPrice: "66000",
|
|
286
|
+
liquidationPrice: "55000",
|
|
287
|
+
unrealizedPnl: "500",
|
|
288
|
+
leverage: 10,
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
});
|
|
292
|
+
const plan = {
|
|
293
|
+
version: "1.0",
|
|
294
|
+
steps: [
|
|
295
|
+
{ id: "close", action: "close_position", params: { symbol: "BTC" } },
|
|
296
|
+
],
|
|
297
|
+
};
|
|
298
|
+
const result = await executePlan(adapter, plan);
|
|
299
|
+
expect(result.status).toBe("completed");
|
|
300
|
+
expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
|
|
301
|
+
// MUST be sell to close a long — if this were "buy", user doubles their position!
|
|
302
|
+
expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "sell", "0.5");
|
|
303
|
+
});
|
|
304
|
+
it("closes a SHORT position by BUYING (not selling)", async () => {
|
|
305
|
+
const adapter = createMockAdapter({
|
|
306
|
+
positions: [
|
|
307
|
+
{
|
|
308
|
+
symbol: "ETH",
|
|
309
|
+
side: "short",
|
|
310
|
+
size: "2.0",
|
|
311
|
+
entryPrice: "3500",
|
|
312
|
+
markPrice: "3400",
|
|
313
|
+
liquidationPrice: "4000",
|
|
314
|
+
unrealizedPnl: "200",
|
|
315
|
+
leverage: 5,
|
|
316
|
+
},
|
|
317
|
+
],
|
|
318
|
+
});
|
|
319
|
+
const plan = {
|
|
320
|
+
version: "1.0",
|
|
321
|
+
steps: [
|
|
322
|
+
{ id: "close", action: "close_position", params: { symbol: "ETH" } },
|
|
323
|
+
],
|
|
324
|
+
};
|
|
325
|
+
const result = await executePlan(adapter, plan);
|
|
326
|
+
expect(result.status).toBe("completed");
|
|
327
|
+
expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
|
|
328
|
+
// MUST be buy to close a short — if this were "sell", user doubles their position!
|
|
329
|
+
expect(adapter.marketOrder).toHaveBeenCalledWith("ETH", "buy", "2.0");
|
|
330
|
+
});
|
|
331
|
+
it("fails gracefully when closing a non-existent position", async () => {
|
|
332
|
+
const adapter = createMockAdapter({ positions: [] });
|
|
333
|
+
const plan = {
|
|
334
|
+
version: "1.0",
|
|
335
|
+
steps: [
|
|
336
|
+
{ id: "close", action: "close_position", params: { symbol: "SOL" } },
|
|
337
|
+
],
|
|
338
|
+
};
|
|
339
|
+
const result = await executePlan(adapter, plan);
|
|
340
|
+
// Default onFailure is "abort"
|
|
341
|
+
expect(result.status).toBe("failed");
|
|
342
|
+
const closeStep = result.steps.find(s => s.stepId === "close");
|
|
343
|
+
expect(closeStep?.status).toBe("failed");
|
|
344
|
+
expect(closeStep?.error?.message).toContain("No position found for SOL");
|
|
345
|
+
// No market order placed
|
|
346
|
+
expect(adapter.marketOrder).toHaveBeenCalledTimes(0);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
// ---------------------------------------------------------------------------
|
|
350
|
+
// 9. Dry run doesn't execute
|
|
351
|
+
// ---------------------------------------------------------------------------
|
|
352
|
+
describe("Scenario 9: Dry run doesn't execute", () => {
|
|
353
|
+
it("returns dry_run status without calling any adapter trading methods", async () => {
|
|
354
|
+
const adapter = createMockAdapter();
|
|
355
|
+
const plan = {
|
|
356
|
+
version: "1.0",
|
|
357
|
+
steps: [
|
|
358
|
+
{ id: "lev", action: "set_leverage", params: { symbol: "BTC", leverage: 10 } },
|
|
359
|
+
{ id: "buy", action: "market_order", params: { symbol: "BTC", side: "buy", size: "0.01" }, dependsOn: "lev" },
|
|
360
|
+
{ id: "sl", action: "stop_order", params: { symbol: "BTC", side: "sell", size: "0.01", triggerPrice: "60000" }, dependsOn: "buy" },
|
|
361
|
+
],
|
|
362
|
+
};
|
|
363
|
+
const result = await executePlan(adapter, plan, { dryRun: true });
|
|
364
|
+
// Overall status is dry_run
|
|
365
|
+
expect(result.status).toBe("dry_run");
|
|
366
|
+
expect(result.steps).toHaveLength(3);
|
|
367
|
+
expect(result.steps.every(s => s.status === "dry_run")).toBe(true);
|
|
368
|
+
// ALL adapter methods have 0 calls
|
|
369
|
+
expect(adapter.setLeverage).toHaveBeenCalledTimes(0);
|
|
370
|
+
expect(adapter.marketOrder).toHaveBeenCalledTimes(0);
|
|
371
|
+
expect(adapter.stopOrder).toHaveBeenCalledTimes(0);
|
|
372
|
+
expect(adapter.limitOrder).toHaveBeenCalledTimes(0);
|
|
373
|
+
expect(adapter.cancelOrder).toHaveBeenCalledTimes(0);
|
|
374
|
+
expect(adapter.cancelAllOrders).toHaveBeenCalledTimes(0);
|
|
375
|
+
expect(adapter.getPositions).toHaveBeenCalledTimes(0);
|
|
376
|
+
expect(adapter.getBalance).toHaveBeenCalledTimes(0);
|
|
377
|
+
// Each step records what it would have done
|
|
378
|
+
for (const step of result.steps) {
|
|
379
|
+
expect(step.result.wouldExecute).toBe(true);
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
// 10. Dependency chain failure propagation
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
describe("Scenario 10: Dependency chain failure propagation", () => {
|
|
387
|
+
it("skips dependent steps when a step fails with default abort", async () => {
|
|
388
|
+
const adapter = createMockAdapter();
|
|
389
|
+
// Step A (market_order) will fail
|
|
390
|
+
adapter.marketOrder.mockRejectedValueOnce(new Error("Exchange down"));
|
|
391
|
+
const plan = {
|
|
392
|
+
version: "1.0",
|
|
393
|
+
steps: [
|
|
394
|
+
{ id: "A", action: "market_order", params: { symbol: "BTC", side: "buy", size: "0.01" } },
|
|
395
|
+
{ id: "B", action: "limit_order", params: { symbol: "BTC", side: "buy", size: "0.02", price: "60000" }, dependsOn: "A" },
|
|
396
|
+
{ id: "C", action: "stop_order", params: { symbol: "BTC", side: "sell", size: "0.01", triggerPrice: "55000" }, dependsOn: "B" },
|
|
397
|
+
],
|
|
398
|
+
};
|
|
399
|
+
const result = await executePlan(adapter, plan);
|
|
400
|
+
// A fails with default abort -> plan stops immediately, B and C never run
|
|
401
|
+
expect(result.status).toBe("failed");
|
|
402
|
+
// Only A is in the results (abort exits immediately)
|
|
403
|
+
expect(result.steps).toHaveLength(1);
|
|
404
|
+
expect(result.steps[0].stepId).toBe("A");
|
|
405
|
+
expect(result.steps[0].status).toBe("failed");
|
|
406
|
+
// B and C never executed
|
|
407
|
+
expect(adapter.limitOrder).toHaveBeenCalledTimes(0);
|
|
408
|
+
expect(adapter.stopOrder).toHaveBeenCalledTimes(0);
|
|
409
|
+
});
|
|
410
|
+
it("skips A but still runs B (dependsOn skipped step is not treated as failed) and C", async () => {
|
|
411
|
+
const adapter = createMockAdapter();
|
|
412
|
+
// Step A will fail but is marked skip
|
|
413
|
+
adapter.marketOrder.mockRejectedValueOnce(new Error("Exchange down"));
|
|
414
|
+
const plan = {
|
|
415
|
+
version: "1.0",
|
|
416
|
+
steps: [
|
|
417
|
+
{ id: "A", action: "market_order", params: { symbol: "BTC", side: "buy", size: "0.01" }, onFailure: "skip" },
|
|
418
|
+
{ id: "B", action: "limit_order", params: { symbol: "BTC", side: "buy", size: "0.02", price: "60000" }, dependsOn: "A" },
|
|
419
|
+
{ id: "C", action: "check_balance", params: {} },
|
|
420
|
+
],
|
|
421
|
+
};
|
|
422
|
+
const result = await executePlan(adapter, plan);
|
|
423
|
+
// The executor dependency check only blocks on dep.status === "failed", NOT "skipped".
|
|
424
|
+
// So A is skipped (not "failed"), B's dependency sees a non-failed dep and proceeds, C runs too.
|
|
425
|
+
expect(result.steps).toHaveLength(3);
|
|
426
|
+
expect(result.steps[0].stepId).toBe("A");
|
|
427
|
+
expect(result.steps[0].status).toBe("skipped");
|
|
428
|
+
// B runs because "skipped" !== "failed" in the dependency check
|
|
429
|
+
expect(result.steps[1].stepId).toBe("B");
|
|
430
|
+
expect(result.steps[1].status).toBe("success");
|
|
431
|
+
expect(result.steps[2].stepId).toBe("C");
|
|
432
|
+
expect(result.steps[2].status).toBe("success");
|
|
433
|
+
// marketOrder called once (A, which failed), limitOrder called once (B succeeded)
|
|
434
|
+
expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
|
|
435
|
+
expect(adapter.limitOrder).toHaveBeenCalledTimes(1);
|
|
436
|
+
expect(adapter.limitOrder).toHaveBeenCalledWith("BTC", "buy", "60000", "0.02");
|
|
437
|
+
expect(adapter.getBalance).toHaveBeenCalledTimes(1);
|
|
438
|
+
});
|
|
439
|
+
it("blocks dependsOn when a step actually fails (status=failed), not skipped", async () => {
|
|
440
|
+
const adapter = createMockAdapter();
|
|
441
|
+
// Step A will fail with default onFailure=abort... but we need "failed" status recorded.
|
|
442
|
+
// With abort, the executor returns immediately so B never enters.
|
|
443
|
+
// To get a "failed" status in completedSteps while continuing, we need a special setup:
|
|
444
|
+
// A fails+abort -> only A in results, plan stops. B never runs.
|
|
445
|
+
adapter.marketOrder.mockRejectedValueOnce(new Error("Exchange down"));
|
|
446
|
+
const plan = {
|
|
447
|
+
version: "1.0",
|
|
448
|
+
steps: [
|
|
449
|
+
{ id: "A", action: "market_order", params: { symbol: "BTC", side: "buy", size: "0.01" } },
|
|
450
|
+
{ id: "B", action: "limit_order", params: { symbol: "BTC", side: "buy", size: "0.02", price: "60000" }, dependsOn: "A" },
|
|
451
|
+
],
|
|
452
|
+
};
|
|
453
|
+
const result = await executePlan(adapter, plan);
|
|
454
|
+
// Default onFailure=abort causes immediate return
|
|
455
|
+
expect(result.status).toBe("failed");
|
|
456
|
+
expect(result.steps).toHaveLength(1);
|
|
457
|
+
expect(result.steps[0].status).toBe("failed");
|
|
458
|
+
// B never ran
|
|
459
|
+
expect(adapter.limitOrder).toHaveBeenCalledTimes(0);
|
|
460
|
+
});
|
|
461
|
+
it("skipped status does NOT propagate through dependency chain: A(skip) -> B(dep A) -> C(dep B) all run", async () => {
|
|
462
|
+
const adapter = createMockAdapter();
|
|
463
|
+
adapter.marketOrder.mockRejectedValueOnce(new Error("fail"));
|
|
464
|
+
const plan = {
|
|
465
|
+
version: "1.0",
|
|
466
|
+
steps: [
|
|
467
|
+
{ id: "A", action: "market_order", params: { symbol: "BTC", side: "buy", size: "0.01" }, onFailure: "skip" },
|
|
468
|
+
{ id: "B", action: "limit_order", params: { symbol: "BTC", side: "buy", size: "0.02", price: "60000" }, dependsOn: "A" },
|
|
469
|
+
{ id: "C", action: "stop_order", params: { symbol: "BTC", side: "sell", size: "0.01", triggerPrice: "55000" }, dependsOn: "B" },
|
|
470
|
+
],
|
|
471
|
+
};
|
|
472
|
+
const result = await executePlan(adapter, plan);
|
|
473
|
+
// The executor only blocks on dep.status === "failed". "skipped" is NOT "failed",
|
|
474
|
+
// so B proceeds (dep A exists and is not failed), and C proceeds (dep B exists and is success).
|
|
475
|
+
expect(result.status).toBe("completed");
|
|
476
|
+
expect(result.steps).toHaveLength(3);
|
|
477
|
+
expect(result.steps[0].status).toBe("skipped"); // A failed but skipped
|
|
478
|
+
expect(result.steps[1].status).toBe("success"); // B runs (A's "skipped" != "failed")
|
|
479
|
+
expect(result.steps[2].status).toBe("success"); // C runs (B succeeded)
|
|
480
|
+
// A's market order attempted (failed), B's limit order ran, C's stop order ran
|
|
481
|
+
expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
|
|
482
|
+
expect(adapter.limitOrder).toHaveBeenCalledTimes(1);
|
|
483
|
+
expect(adapter.stopOrder).toHaveBeenCalledTimes(1);
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
// ---------------------------------------------------------------------------
|
|
487
|
+
// Additional edge cases
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
describe("Edge cases", () => {
|
|
490
|
+
it("cancel_all with empty symbol passes undefined", async () => {
|
|
491
|
+
const adapter = createMockAdapter();
|
|
492
|
+
const plan = {
|
|
493
|
+
version: "1.0",
|
|
494
|
+
steps: [
|
|
495
|
+
{ id: "c", action: "cancel_all", params: {} },
|
|
496
|
+
],
|
|
497
|
+
};
|
|
498
|
+
await executePlan(adapter, plan);
|
|
499
|
+
// Empty string from params -> cancelAllOrders(undefined)
|
|
500
|
+
expect(adapter.cancelAllOrders).toHaveBeenCalledWith(undefined);
|
|
501
|
+
});
|
|
502
|
+
it("set_leverage uses isolated margin mode when specified", async () => {
|
|
503
|
+
const adapter = createMockAdapter();
|
|
504
|
+
const plan = {
|
|
505
|
+
version: "1.0",
|
|
506
|
+
steps: [
|
|
507
|
+
{ id: "lev", action: "set_leverage", params: { symbol: "ETH", leverage: 20, marginMode: "isolated" } },
|
|
508
|
+
],
|
|
509
|
+
};
|
|
510
|
+
await executePlan(adapter, plan);
|
|
511
|
+
expect(adapter.setLeverage).toHaveBeenCalledWith("ETH", 20, "isolated");
|
|
512
|
+
});
|
|
513
|
+
it("check_position with mustExist=true throws when position absent", async () => {
|
|
514
|
+
const adapter = createMockAdapter({ positions: [] });
|
|
515
|
+
const plan = {
|
|
516
|
+
version: "1.0",
|
|
517
|
+
steps: [
|
|
518
|
+
{ id: "chk", action: "check_position", params: { symbol: "SOL", mustExist: true } },
|
|
519
|
+
],
|
|
520
|
+
};
|
|
521
|
+
const result = await executePlan(adapter, plan);
|
|
522
|
+
expect(result.status).toBe("failed");
|
|
523
|
+
const step = result.steps[0];
|
|
524
|
+
expect(step.status).toBe("failed");
|
|
525
|
+
expect(step.error?.message).toContain("SOL");
|
|
526
|
+
expect(step.error?.message).toContain("not found");
|
|
527
|
+
});
|
|
528
|
+
it("limit_order passes all arguments correctly", async () => {
|
|
529
|
+
const adapter = createMockAdapter();
|
|
530
|
+
const plan = {
|
|
531
|
+
version: "1.0",
|
|
532
|
+
steps: [
|
|
533
|
+
{ id: "lim", action: "limit_order", params: { symbol: "ETH", side: "buy", price: "3000", size: "2.5" } },
|
|
534
|
+
],
|
|
535
|
+
};
|
|
536
|
+
await executePlan(adapter, plan);
|
|
537
|
+
expect(adapter.limitOrder).toHaveBeenCalledWith("ETH", "buy", "3000", "2.5");
|
|
538
|
+
});
|
|
539
|
+
it("stop_order passes limitPrice and reduceOnly options", async () => {
|
|
540
|
+
const adapter = createMockAdapter();
|
|
541
|
+
const plan = {
|
|
542
|
+
version: "1.0",
|
|
543
|
+
steps: [
|
|
544
|
+
{
|
|
545
|
+
id: "stop",
|
|
546
|
+
action: "stop_order",
|
|
547
|
+
params: {
|
|
548
|
+
symbol: "BTC",
|
|
549
|
+
side: "sell",
|
|
550
|
+
size: "0.1",
|
|
551
|
+
triggerPrice: "58000",
|
|
552
|
+
limitPrice: "57500",
|
|
553
|
+
reduceOnly: true,
|
|
554
|
+
},
|
|
555
|
+
},
|
|
556
|
+
],
|
|
557
|
+
};
|
|
558
|
+
await executePlan(adapter, plan);
|
|
559
|
+
expect(adapter.stopOrder).toHaveBeenCalledWith("BTC", "sell", "0.1", "58000", { limitPrice: "57500", reduceOnly: true });
|
|
560
|
+
});
|
|
561
|
+
it("cancel_order passes symbol and orderId", async () => {
|
|
562
|
+
const adapter = createMockAdapter();
|
|
563
|
+
const plan = {
|
|
564
|
+
version: "1.0",
|
|
565
|
+
steps: [
|
|
566
|
+
{ id: "cx", action: "cancel_order", params: { symbol: "BTC", orderId: "order-abc-123" } },
|
|
567
|
+
],
|
|
568
|
+
};
|
|
569
|
+
await executePlan(adapter, plan);
|
|
570
|
+
expect(adapter.cancelOrder).toHaveBeenCalledWith("BTC", "order-abc-123");
|
|
571
|
+
});
|
|
572
|
+
it("symbols are uppercased consistently", async () => {
|
|
573
|
+
const adapter = createMockAdapter({
|
|
574
|
+
positions: [
|
|
575
|
+
{
|
|
576
|
+
symbol: "BTC",
|
|
577
|
+
side: "long",
|
|
578
|
+
size: "1",
|
|
579
|
+
entryPrice: "65000",
|
|
580
|
+
markPrice: "65000",
|
|
581
|
+
liquidationPrice: "55000",
|
|
582
|
+
unrealizedPnl: "0",
|
|
583
|
+
leverage: 10,
|
|
584
|
+
},
|
|
585
|
+
],
|
|
586
|
+
});
|
|
587
|
+
const plan = {
|
|
588
|
+
version: "1.0",
|
|
589
|
+
steps: [
|
|
590
|
+
{ id: "buy", action: "market_order", params: { symbol: "btc", side: "buy", size: "0.1" } },
|
|
591
|
+
{ id: "close", action: "close_position", params: { symbol: "btc" }, dependsOn: "buy" },
|
|
592
|
+
],
|
|
593
|
+
};
|
|
594
|
+
await executePlan(adapter, plan);
|
|
595
|
+
// market_order uppercases the symbol
|
|
596
|
+
expect(adapter.marketOrder).toHaveBeenNthCalledWith(1, "BTC", "buy", "0.1");
|
|
597
|
+
// close_position uppercases for lookup and uses position's original symbol
|
|
598
|
+
expect(adapter.marketOrder).toHaveBeenNthCalledWith(2, "BTC", "sell", "1");
|
|
599
|
+
});
|
|
600
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|