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 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { withRetry, withRetrySimple, wrapAdapterWithRetry, applyJitter, computeDelay, RetriesExhaustedError, } from "../retry.js";
|
|
3
|
+
// ── Helpers ──
|
|
4
|
+
/** Create an error whose message triggers classification as the given code */
|
|
5
|
+
function rateLimitError() {
|
|
6
|
+
return new Error("429 Too Many Requests");
|
|
7
|
+
}
|
|
8
|
+
function networkError() {
|
|
9
|
+
return new Error("fetch failed");
|
|
10
|
+
}
|
|
11
|
+
function timeoutError() {
|
|
12
|
+
return new Error("Request timed out");
|
|
13
|
+
}
|
|
14
|
+
function insufficientBalanceError() {
|
|
15
|
+
return new Error("Insufficient balance for order");
|
|
16
|
+
}
|
|
17
|
+
// Speed up tests by using tiny delays
|
|
18
|
+
const FAST_OPTS = {
|
|
19
|
+
maxRetries: 3,
|
|
20
|
+
baseDelayMs: 1,
|
|
21
|
+
maxDelayMs: 10,
|
|
22
|
+
backoffMultiplier: 2,
|
|
23
|
+
};
|
|
24
|
+
describe("withRetry", () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.spyOn(Math, "random").mockReturnValue(0.5); // jitter factor = 0.8 + 0.5*0.4 = 1.0
|
|
27
|
+
});
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
vi.restoreAllMocks();
|
|
30
|
+
});
|
|
31
|
+
it("returns result on first successful attempt (no retry)", async () => {
|
|
32
|
+
const fn = vi.fn().mockResolvedValue("ok");
|
|
33
|
+
const result = await withRetry(fn, FAST_OPTS);
|
|
34
|
+
expect(result.data).toBe("ok");
|
|
35
|
+
expect(result.attempts).toBe(1);
|
|
36
|
+
expect(result.totalDelayMs).toBe(0);
|
|
37
|
+
expect(result.retries).toHaveLength(0);
|
|
38
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
39
|
+
});
|
|
40
|
+
it("retries on retryable error (rate limit) and succeeds", async () => {
|
|
41
|
+
const fn = vi.fn()
|
|
42
|
+
.mockRejectedValueOnce(rateLimitError())
|
|
43
|
+
.mockRejectedValueOnce(networkError())
|
|
44
|
+
.mockResolvedValue("recovered");
|
|
45
|
+
const result = await withRetry(fn, FAST_OPTS);
|
|
46
|
+
expect(result.data).toBe("recovered");
|
|
47
|
+
expect(result.attempts).toBe(3);
|
|
48
|
+
expect(result.retries).toHaveLength(2);
|
|
49
|
+
expect(result.retries[0].error.code).toBe("RATE_LIMITED");
|
|
50
|
+
expect(result.retries[1].error.code).toBe("EXCHANGE_UNREACHABLE");
|
|
51
|
+
expect(fn).toHaveBeenCalledTimes(3);
|
|
52
|
+
});
|
|
53
|
+
it("throws immediately on non-retryable error (insufficient balance)", async () => {
|
|
54
|
+
const fn = vi.fn().mockRejectedValue(insufficientBalanceError());
|
|
55
|
+
await expect(withRetry(fn, FAST_OPTS)).rejects.toThrow("Insufficient balance");
|
|
56
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
57
|
+
});
|
|
58
|
+
it("throws RetriesExhaustedError when max retries exceeded", async () => {
|
|
59
|
+
const fn = vi.fn().mockRejectedValue(rateLimitError());
|
|
60
|
+
try {
|
|
61
|
+
await withRetry(fn, { ...FAST_OPTS, maxRetries: 2 });
|
|
62
|
+
expect.fail("Should have thrown");
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
expect(err).toBeInstanceOf(RetriesExhaustedError);
|
|
66
|
+
const rex = err;
|
|
67
|
+
expect(rex.attempts).toBe(3); // 1 initial + 2 retries
|
|
68
|
+
expect(rex.lastError.code).toBe("RATE_LIMITED");
|
|
69
|
+
expect(rex.retries).toHaveLength(2);
|
|
70
|
+
}
|
|
71
|
+
// initial attempt + 2 retries + 1 final attempt = 3 calls
|
|
72
|
+
expect(fn).toHaveBeenCalledTimes(3);
|
|
73
|
+
});
|
|
74
|
+
it("uses exponential backoff timing", async () => {
|
|
75
|
+
// With Math.random() = 0.5, jitter factor = 1.0 (no change)
|
|
76
|
+
const fn = vi.fn()
|
|
77
|
+
.mockRejectedValueOnce(networkError())
|
|
78
|
+
.mockRejectedValueOnce(networkError())
|
|
79
|
+
.mockRejectedValueOnce(networkError())
|
|
80
|
+
.mockResolvedValue("ok");
|
|
81
|
+
const opts = {
|
|
82
|
+
maxRetries: 3,
|
|
83
|
+
baseDelayMs: 100,
|
|
84
|
+
maxDelayMs: 10000,
|
|
85
|
+
backoffMultiplier: 2,
|
|
86
|
+
};
|
|
87
|
+
const result = await withRetry(fn, opts);
|
|
88
|
+
// Attempt 1 fail: delay = 100 * 2^0 = 100
|
|
89
|
+
// Attempt 2 fail: delay = 100 * 2^1 = 200
|
|
90
|
+
// Attempt 3 fail: delay = 100 * 2^2 = 400
|
|
91
|
+
expect(result.retries[0].delayMs).toBe(100);
|
|
92
|
+
expect(result.retries[1].delayMs).toBe(200);
|
|
93
|
+
expect(result.retries[2].delayMs).toBe(400);
|
|
94
|
+
expect(result.totalDelayMs).toBe(700);
|
|
95
|
+
});
|
|
96
|
+
it("respects retryAfterMs from error code (rate limit = 1000ms)", async () => {
|
|
97
|
+
// Rate limit has retryAfterMs: 1000
|
|
98
|
+
// With baseDelayMs=1 and multiplier=2, backoff would be tiny,
|
|
99
|
+
// but retryAfterMs: 1000 should override
|
|
100
|
+
const fn = vi.fn()
|
|
101
|
+
.mockRejectedValueOnce(rateLimitError())
|
|
102
|
+
.mockResolvedValue("ok");
|
|
103
|
+
const opts = {
|
|
104
|
+
maxRetries: 3,
|
|
105
|
+
baseDelayMs: 1,
|
|
106
|
+
maxDelayMs: 30000,
|
|
107
|
+
backoffMultiplier: 2,
|
|
108
|
+
};
|
|
109
|
+
const result = await withRetry(fn, opts);
|
|
110
|
+
// retryAfterMs=1000 is larger than baseDelay*multiplier^0=1, so 1000 is used
|
|
111
|
+
expect(result.retries[0].delayMs).toBe(1000);
|
|
112
|
+
});
|
|
113
|
+
it("caps delay at maxDelayMs", async () => {
|
|
114
|
+
const fn = vi.fn()
|
|
115
|
+
.mockRejectedValueOnce(networkError())
|
|
116
|
+
.mockResolvedValue("ok");
|
|
117
|
+
const opts = {
|
|
118
|
+
maxRetries: 3,
|
|
119
|
+
baseDelayMs: 50000, // Very large base
|
|
120
|
+
maxDelayMs: 100,
|
|
121
|
+
backoffMultiplier: 2,
|
|
122
|
+
};
|
|
123
|
+
const result = await withRetry(fn, opts);
|
|
124
|
+
// 50000 would be the computed delay, but capped to 100
|
|
125
|
+
expect(result.retries[0].delayMs).toBe(100);
|
|
126
|
+
});
|
|
127
|
+
it("calls onRetry callback on each retry", async () => {
|
|
128
|
+
const onRetry = vi.fn();
|
|
129
|
+
const fn = vi.fn()
|
|
130
|
+
.mockRejectedValueOnce(rateLimitError())
|
|
131
|
+
.mockRejectedValueOnce(timeoutError())
|
|
132
|
+
.mockResolvedValue("ok");
|
|
133
|
+
await withRetry(fn, { ...FAST_OPTS, onRetry });
|
|
134
|
+
expect(onRetry).toHaveBeenCalledTimes(2);
|
|
135
|
+
// First call: attempt 1, rate limit error
|
|
136
|
+
expect(onRetry.mock.calls[0][0]).toBe(1); // attempt
|
|
137
|
+
expect(onRetry.mock.calls[0][1].code).toBe("RATE_LIMITED");
|
|
138
|
+
expect(typeof onRetry.mock.calls[0][2]).toBe("number"); // delayMs
|
|
139
|
+
// Second call: attempt 2, timeout error
|
|
140
|
+
expect(onRetry.mock.calls[1][0]).toBe(2);
|
|
141
|
+
expect(onRetry.mock.calls[1][1].code).toBe("TIMEOUT");
|
|
142
|
+
});
|
|
143
|
+
it("uses default options when none provided", async () => {
|
|
144
|
+
// Just verify it runs with defaults (maxRetries=3, baseDelay=1000, etc.)
|
|
145
|
+
const fn = vi.fn().mockResolvedValue(42);
|
|
146
|
+
const result = await withRetry(fn);
|
|
147
|
+
expect(result.data).toBe(42);
|
|
148
|
+
expect(result.attempts).toBe(1);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
describe("applyJitter", () => {
|
|
152
|
+
afterEach(() => {
|
|
153
|
+
vi.restoreAllMocks();
|
|
154
|
+
});
|
|
155
|
+
it("applies jitter within +/-20% range", () => {
|
|
156
|
+
// Test minimum jitter (random=0 -> factor=0.8)
|
|
157
|
+
vi.spyOn(Math, "random").mockReturnValue(0);
|
|
158
|
+
expect(applyJitter(1000)).toBe(800);
|
|
159
|
+
// Test maximum jitter (random=1 -> factor=1.2)
|
|
160
|
+
vi.mocked(Math.random).mockReturnValue(1);
|
|
161
|
+
expect(applyJitter(1000)).toBe(1200);
|
|
162
|
+
// Test midpoint (random=0.5 -> factor=1.0)
|
|
163
|
+
vi.mocked(Math.random).mockReturnValue(0.5);
|
|
164
|
+
expect(applyJitter(1000)).toBe(1000);
|
|
165
|
+
});
|
|
166
|
+
it("rounds to integer", () => {
|
|
167
|
+
vi.spyOn(Math, "random").mockReturnValue(0.3); // factor = 0.92
|
|
168
|
+
expect(applyJitter(100)).toBe(92);
|
|
169
|
+
expect(Number.isInteger(applyJitter(100))).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
describe("computeDelay", () => {
|
|
173
|
+
beforeEach(() => {
|
|
174
|
+
vi.spyOn(Math, "random").mockReturnValue(0.5); // jitter factor = 1.0
|
|
175
|
+
});
|
|
176
|
+
afterEach(() => {
|
|
177
|
+
vi.restoreAllMocks();
|
|
178
|
+
});
|
|
179
|
+
const defaultOpts = {
|
|
180
|
+
maxRetries: 3,
|
|
181
|
+
baseDelayMs: 1000,
|
|
182
|
+
maxDelayMs: 30000,
|
|
183
|
+
backoffMultiplier: 2,
|
|
184
|
+
};
|
|
185
|
+
it("computes exponential backoff", () => {
|
|
186
|
+
const error = { code: "TIMEOUT", message: "timed out", status: 504, retryable: true };
|
|
187
|
+
expect(computeDelay(1, error, defaultOpts)).toBe(1000); // 1000 * 2^0
|
|
188
|
+
expect(computeDelay(2, error, defaultOpts)).toBe(2000); // 1000 * 2^1
|
|
189
|
+
expect(computeDelay(3, error, defaultOpts)).toBe(4000); // 1000 * 2^2
|
|
190
|
+
});
|
|
191
|
+
it("uses retryAfterMs when larger than backoff", () => {
|
|
192
|
+
const error = {
|
|
193
|
+
code: "RATE_LIMITED", message: "429", status: 429, retryable: true, retryAfterMs: 5000,
|
|
194
|
+
};
|
|
195
|
+
// Attempt 1: backoff = 1000, retryAfterMs = 5000 -> 5000
|
|
196
|
+
expect(computeDelay(1, error, defaultOpts)).toBe(5000);
|
|
197
|
+
// Attempt 3: backoff = 4000, retryAfterMs = 5000 -> 5000
|
|
198
|
+
expect(computeDelay(3, error, defaultOpts)).toBe(5000);
|
|
199
|
+
// Attempt 4: backoff = 8000, retryAfterMs = 5000 -> 8000
|
|
200
|
+
expect(computeDelay(4, error, defaultOpts)).toBe(8000);
|
|
201
|
+
});
|
|
202
|
+
it("caps at maxDelayMs", () => {
|
|
203
|
+
const error = { code: "TIMEOUT", message: "timed out", status: 504, retryable: true };
|
|
204
|
+
const opts = { ...defaultOpts, maxDelayMs: 3000 };
|
|
205
|
+
expect(computeDelay(3, error, opts)).toBe(3000); // 4000 capped to 3000
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
describe("withRetrySimple", () => {
|
|
209
|
+
beforeEach(() => {
|
|
210
|
+
vi.spyOn(Math, "random").mockReturnValue(0.5);
|
|
211
|
+
});
|
|
212
|
+
afterEach(() => {
|
|
213
|
+
vi.restoreAllMocks();
|
|
214
|
+
});
|
|
215
|
+
it("returns data directly without metadata", async () => {
|
|
216
|
+
const fn = vi.fn()
|
|
217
|
+
.mockRejectedValueOnce(rateLimitError())
|
|
218
|
+
.mockResolvedValue({ price: "100.00" });
|
|
219
|
+
const data = await withRetrySimple(fn, 3);
|
|
220
|
+
expect(data).toEqual({ price: "100.00" });
|
|
221
|
+
});
|
|
222
|
+
it("throws on non-retryable error", async () => {
|
|
223
|
+
const fn = vi.fn().mockRejectedValue(insufficientBalanceError());
|
|
224
|
+
await expect(withRetrySimple(fn)).rejects.toThrow("Insufficient balance");
|
|
225
|
+
});
|
|
226
|
+
it("uses default maxRetries when not specified", async () => {
|
|
227
|
+
const fn = vi.fn().mockResolvedValue("ok");
|
|
228
|
+
const result = await withRetrySimple(fn);
|
|
229
|
+
expect(result).toBe("ok");
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
describe("wrapAdapterWithRetry", () => {
|
|
233
|
+
beforeEach(() => {
|
|
234
|
+
vi.spyOn(Math, "random").mockReturnValue(0.5);
|
|
235
|
+
});
|
|
236
|
+
afterEach(() => {
|
|
237
|
+
vi.restoreAllMocks();
|
|
238
|
+
});
|
|
239
|
+
function createMockAdapter(overrides) {
|
|
240
|
+
return {
|
|
241
|
+
name: "mock-exchange",
|
|
242
|
+
getMarkets: vi.fn().mockResolvedValue([]),
|
|
243
|
+
getOrderbook: vi.fn().mockResolvedValue({ bids: [], asks: [] }),
|
|
244
|
+
getRecentTrades: vi.fn().mockResolvedValue([]),
|
|
245
|
+
getFundingHistory: vi.fn().mockResolvedValue([]),
|
|
246
|
+
getKlines: vi.fn().mockResolvedValue([]),
|
|
247
|
+
getBalance: vi.fn().mockResolvedValue({ equity: "0", available: "0", marginUsed: "0", unrealizedPnl: "0" }),
|
|
248
|
+
getPositions: vi.fn().mockResolvedValue([]),
|
|
249
|
+
getOpenOrders: vi.fn().mockResolvedValue([]),
|
|
250
|
+
getOrderHistory: vi.fn().mockResolvedValue([]),
|
|
251
|
+
getTradeHistory: vi.fn().mockResolvedValue([]),
|
|
252
|
+
getFundingPayments: vi.fn().mockResolvedValue([]),
|
|
253
|
+
marketOrder: vi.fn().mockResolvedValue({ orderId: "123" }),
|
|
254
|
+
limitOrder: vi.fn().mockResolvedValue({ orderId: "456" }),
|
|
255
|
+
editOrder: vi.fn().mockResolvedValue({}),
|
|
256
|
+
cancelOrder: vi.fn().mockResolvedValue({}),
|
|
257
|
+
cancelAllOrders: vi.fn().mockResolvedValue({}),
|
|
258
|
+
setLeverage: vi.fn().mockResolvedValue({}),
|
|
259
|
+
stopOrder: vi.fn().mockResolvedValue({}),
|
|
260
|
+
...overrides,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
it("passes through the name property without wrapping", () => {
|
|
264
|
+
const adapter = createMockAdapter();
|
|
265
|
+
const wrapped = wrapAdapterWithRetry(adapter, FAST_OPTS);
|
|
266
|
+
expect(wrapped.name).toBe("mock-exchange");
|
|
267
|
+
});
|
|
268
|
+
it("proxies async method calls through to the original adapter", async () => {
|
|
269
|
+
const mockMarkets = [{ symbol: "BTC-PERP", markPrice: "50000" }];
|
|
270
|
+
const adapter = createMockAdapter({
|
|
271
|
+
getMarkets: vi.fn().mockResolvedValue(mockMarkets),
|
|
272
|
+
});
|
|
273
|
+
const wrapped = wrapAdapterWithRetry(adapter, FAST_OPTS);
|
|
274
|
+
const result = await wrapped.getMarkets();
|
|
275
|
+
expect(result).toEqual(mockMarkets);
|
|
276
|
+
expect(adapter.getMarkets).toHaveBeenCalledTimes(1);
|
|
277
|
+
});
|
|
278
|
+
it("retries async methods on retryable errors", async () => {
|
|
279
|
+
const getBalance = vi.fn()
|
|
280
|
+
.mockRejectedValueOnce(rateLimitError())
|
|
281
|
+
.mockResolvedValue({ equity: "1000", available: "500", marginUsed: "500", unrealizedPnl: "50" });
|
|
282
|
+
const adapter = createMockAdapter({ getBalance });
|
|
283
|
+
const wrapped = wrapAdapterWithRetry(adapter, FAST_OPTS);
|
|
284
|
+
const result = await wrapped.getBalance();
|
|
285
|
+
expect(result).toEqual({ equity: "1000", available: "500", marginUsed: "500", unrealizedPnl: "50" });
|
|
286
|
+
expect(getBalance).toHaveBeenCalledTimes(2);
|
|
287
|
+
});
|
|
288
|
+
it("passes arguments through to the wrapped method", async () => {
|
|
289
|
+
const marketOrder = vi.fn().mockResolvedValue({ orderId: "abc" });
|
|
290
|
+
const adapter = createMockAdapter({ marketOrder });
|
|
291
|
+
const wrapped = wrapAdapterWithRetry(adapter, FAST_OPTS);
|
|
292
|
+
await wrapped.marketOrder("BTC-PERP", "buy", "0.1");
|
|
293
|
+
expect(marketOrder).toHaveBeenCalledWith("BTC-PERP", "buy", "0.1");
|
|
294
|
+
});
|
|
295
|
+
it("does not retry non-retryable errors from wrapped methods", async () => {
|
|
296
|
+
const marketOrder = vi.fn().mockRejectedValue(insufficientBalanceError());
|
|
297
|
+
const adapter = createMockAdapter({ marketOrder });
|
|
298
|
+
const wrapped = wrapAdapterWithRetry(adapter, FAST_OPTS);
|
|
299
|
+
await expect(wrapped.marketOrder("BTC-PERP", "buy", "999")).rejects.toThrow("Insufficient balance");
|
|
300
|
+
expect(marketOrder).toHaveBeenCalledTimes(1);
|
|
301
|
+
});
|
|
302
|
+
it("respects retry options passed to wrapAdapterWithRetry", async () => {
|
|
303
|
+
const getPositions = vi.fn().mockRejectedValue(timeoutError());
|
|
304
|
+
const adapter = createMockAdapter({ getPositions });
|
|
305
|
+
const wrapped = wrapAdapterWithRetry(adapter, { ...FAST_OPTS, maxRetries: 1 });
|
|
306
|
+
await expect(wrapped.getPositions()).rejects.toBeInstanceOf(RetriesExhaustedError);
|
|
307
|
+
// 1 initial + 1 retry = 2 calls
|
|
308
|
+
expect(getPositions).toHaveBeenCalledTimes(2);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { assessRisk, preTradeCheck } from "../risk.js";
|
|
3
|
+
const defaultLimits = {
|
|
4
|
+
maxDrawdownUsd: 500,
|
|
5
|
+
maxPositionUsd: 5000,
|
|
6
|
+
maxTotalExposureUsd: 20000,
|
|
7
|
+
dailyLossLimitUsd: 200,
|
|
8
|
+
maxPositions: 10,
|
|
9
|
+
maxLeverage: 20,
|
|
10
|
+
maxMarginUtilization: 80,
|
|
11
|
+
};
|
|
12
|
+
function makeBalance(equity, available, marginUsed, pnl) {
|
|
13
|
+
return { equity: String(equity), available: String(available), marginUsed: String(marginUsed), unrealizedPnl: String(pnl) };
|
|
14
|
+
}
|
|
15
|
+
function makePosition(symbol, side, size, markPrice, leverage, pnl) {
|
|
16
|
+
return { symbol, side, size: String(size), entryPrice: String(markPrice), markPrice: String(markPrice), liquidationPrice: "0", unrealizedPnl: String(pnl), leverage };
|
|
17
|
+
}
|
|
18
|
+
describe("Risk Assessment", () => {
|
|
19
|
+
it("should return low risk when everything is within limits", () => {
|
|
20
|
+
const balances = [{ exchange: "test", balance: makeBalance(10000, 8000, 2000, 100) }];
|
|
21
|
+
const positions = [{ exchange: "test", position: makePosition("BTC", "long", 0.01, 100000, 5, 50) }];
|
|
22
|
+
const result = assessRisk(balances, positions, defaultLimits);
|
|
23
|
+
expect(result.level).toBe("low");
|
|
24
|
+
expect(result.violations).toHaveLength(0);
|
|
25
|
+
expect(result.canTrade).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
it("should detect max drawdown violation (critical)", () => {
|
|
28
|
+
const balances = [{ exchange: "test", balance: makeBalance(10000, 8000, 2000, -600) }];
|
|
29
|
+
const positions = [{ exchange: "test", position: makePosition("BTC", "long", 0.01, 100000, 5, -600) }];
|
|
30
|
+
const result = assessRisk(balances, positions, defaultLimits);
|
|
31
|
+
expect(result.level).toBe("critical");
|
|
32
|
+
expect(result.violations.some(v => v.rule === "max_drawdown")).toBe(true);
|
|
33
|
+
expect(result.canTrade).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
it("should detect max position size violation", () => {
|
|
36
|
+
const balances = [{ exchange: "test", balance: makeBalance(10000, 5000, 5000, 0) }];
|
|
37
|
+
const positions = [{ exchange: "test", position: makePosition("BTC", "long", 0.1, 100000, 10, 0) }]; // 0.1 * 100000 = $10000 > $5000 limit
|
|
38
|
+
const result = assessRisk(balances, positions, defaultLimits);
|
|
39
|
+
expect(result.violations.some(v => v.rule === "max_position_size")).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
it("should detect max total exposure violation", () => {
|
|
42
|
+
const balances = [{ exchange: "test", balance: makeBalance(50000, 30000, 20000, 0) }];
|
|
43
|
+
const positions = [
|
|
44
|
+
{ exchange: "test", position: makePosition("BTC", "long", 0.1, 100000, 5, 0) }, // $10000
|
|
45
|
+
{ exchange: "test", position: makePosition("ETH", "short", 5, 3500, 5, 0) }, // $17500
|
|
46
|
+
]; // total = $27500 > $20000
|
|
47
|
+
const result = assessRisk(balances, positions, defaultLimits);
|
|
48
|
+
expect(result.violations.some(v => v.rule === "max_total_exposure")).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
it("should detect max positions violation", () => {
|
|
51
|
+
const limits = { ...defaultLimits, maxPositions: 2 };
|
|
52
|
+
const balances = [{ exchange: "test", balance: makeBalance(10000, 8000, 2000, 0) }];
|
|
53
|
+
const positions = [
|
|
54
|
+
{ exchange: "test", position: makePosition("BTC", "long", 0.001, 100000, 2, 0) },
|
|
55
|
+
{ exchange: "test", position: makePosition("ETH", "short", 0.01, 3500, 2, 0) },
|
|
56
|
+
{ exchange: "test", position: makePosition("SOL", "long", 0.1, 150, 2, 0) },
|
|
57
|
+
];
|
|
58
|
+
const result = assessRisk(balances, positions, limits);
|
|
59
|
+
expect(result.violations.some(v => v.rule === "max_positions")).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
it("should detect max leverage violation", () => {
|
|
62
|
+
const balances = [{ exchange: "test", balance: makeBalance(10000, 8000, 2000, 0) }];
|
|
63
|
+
const positions = [{ exchange: "test", position: makePosition("BTC", "long", 0.01, 100000, 50, 0) }]; // 50x > 20x
|
|
64
|
+
const result = assessRisk(balances, positions, defaultLimits);
|
|
65
|
+
expect(result.violations.some(v => v.rule === "max_leverage")).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
it("should detect margin utilization violation", () => {
|
|
68
|
+
const balances = [{ exchange: "test", balance: makeBalance(1000, 100, 900, 0) }]; // 90% margin usage
|
|
69
|
+
const positions = [];
|
|
70
|
+
const result = assessRisk(balances, positions, defaultLimits);
|
|
71
|
+
expect(result.violations.some(v => v.rule === "max_margin_utilization")).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
it("should calculate correct metrics", () => {
|
|
74
|
+
const balances = [
|
|
75
|
+
{ exchange: "ex1", balance: makeBalance(5000, 3000, 2000, 100) },
|
|
76
|
+
{ exchange: "ex2", balance: makeBalance(3000, 2000, 1000, -50) },
|
|
77
|
+
];
|
|
78
|
+
const positions = [
|
|
79
|
+
{ exchange: "ex1", position: makePosition("BTC", "long", 0.02, 100000, 10, 100) },
|
|
80
|
+
{ exchange: "ex2", position: makePosition("ETH", "short", 1.0, 3500, 5, -50) },
|
|
81
|
+
];
|
|
82
|
+
const result = assessRisk(balances, positions, defaultLimits);
|
|
83
|
+
expect(result.metrics.totalEquity).toBe(8000);
|
|
84
|
+
expect(result.metrics.totalUnrealizedPnl).toBe(50);
|
|
85
|
+
expect(result.metrics.totalMarginUsed).toBe(3000);
|
|
86
|
+
expect(result.metrics.positionCount).toBe(2);
|
|
87
|
+
expect(result.metrics.totalExposure).toBe(2000 + 3500); // 0.02*100000 + 1.0*3500
|
|
88
|
+
expect(result.metrics.largestPositionUsd).toBe(3500);
|
|
89
|
+
expect(result.metrics.maxLeverageUsed).toBe(10);
|
|
90
|
+
expect(result.metrics.marginUtilization).toBeCloseTo(37.5, 1);
|
|
91
|
+
});
|
|
92
|
+
it("should handle empty portfolio", () => {
|
|
93
|
+
const result = assessRisk([], [], defaultLimits);
|
|
94
|
+
expect(result.level).toBe("low");
|
|
95
|
+
expect(result.canTrade).toBe(true);
|
|
96
|
+
expect(result.metrics.totalEquity).toBe(0);
|
|
97
|
+
});
|
|
98
|
+
it("should handle multiple violations and pick highest severity", () => {
|
|
99
|
+
const balances = [{ exchange: "test", balance: makeBalance(1000, 50, 950, -600) }]; // drawdown + margin
|
|
100
|
+
const positions = [{ exchange: "test", position: makePosition("BTC", "long", 0.1, 100000, 50, -600) }]; // position + leverage
|
|
101
|
+
const result = assessRisk(balances, positions, defaultLimits);
|
|
102
|
+
expect(result.level).toBe("critical"); // drawdown is critical
|
|
103
|
+
expect(result.violations.length).toBeGreaterThanOrEqual(3);
|
|
104
|
+
expect(result.canTrade).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
describe("Pre-Trade Check", () => {
|
|
108
|
+
it("should allow trade within limits", () => {
|
|
109
|
+
const assessment = assessRisk([{ exchange: "test", balance: makeBalance(10000, 8000, 2000, 0) }], [{ exchange: "test", position: makePosition("BTC", "long", 0.01, 100000, 5, 0) }], defaultLimits);
|
|
110
|
+
const result = preTradeCheck(assessment, 1000, 5);
|
|
111
|
+
expect(result.allowed).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
it("should block trade when trading is suspended (critical violation)", () => {
|
|
114
|
+
const assessment = assessRisk([{ exchange: "test", balance: makeBalance(10000, 8000, 2000, -600) }], [], defaultLimits);
|
|
115
|
+
const result = preTradeCheck(assessment, 100, 2);
|
|
116
|
+
expect(result.allowed).toBe(false);
|
|
117
|
+
expect(result.reason).toContain("suspended");
|
|
118
|
+
});
|
|
119
|
+
it("should block trade exceeding max position size", () => {
|
|
120
|
+
const assessment = assessRisk([{ exchange: "test", balance: makeBalance(10000, 8000, 2000, 0) }], [], defaultLimits);
|
|
121
|
+
const result = preTradeCheck(assessment, 6000, 5); // > $5000 limit
|
|
122
|
+
expect(result.allowed).toBe(false);
|
|
123
|
+
expect(result.reason).toContain("max position size");
|
|
124
|
+
});
|
|
125
|
+
it("should block trade exceeding total exposure", () => {
|
|
126
|
+
const assessment = assessRisk([{ exchange: "test", balance: makeBalance(50000, 30000, 20000, 0) }], [{ exchange: "test", position: makePosition("BTC", "long", 0.19, 100000, 5, 0) }], // $19000
|
|
127
|
+
defaultLimits);
|
|
128
|
+
const result = preTradeCheck(assessment, 2000, 5); // 19000 + 2000 = 21000 > 20000
|
|
129
|
+
expect(result.allowed).toBe(false);
|
|
130
|
+
expect(result.reason).toContain("exposure");
|
|
131
|
+
});
|
|
132
|
+
it("should block trade exceeding max positions", () => {
|
|
133
|
+
const limits = { ...defaultLimits, maxPositions: 1 };
|
|
134
|
+
const assessment = assessRisk([{ exchange: "test", balance: makeBalance(10000, 8000, 2000, 0) }], [{ exchange: "test", position: makePosition("BTC", "long", 0.01, 100000, 5, 0) }], limits);
|
|
135
|
+
const result = preTradeCheck(assessment, 500, 5); // would be 2nd position
|
|
136
|
+
expect(result.allowed).toBe(false);
|
|
137
|
+
expect(result.reason).toContain("max positions");
|
|
138
|
+
});
|
|
139
|
+
it("should block trade exceeding max leverage", () => {
|
|
140
|
+
const assessment = assessRisk([{ exchange: "test", balance: makeBalance(10000, 8000, 2000, 0) }], [], defaultLimits);
|
|
141
|
+
const result = preTradeCheck(assessment, 500, 25); // 25x > 20x limit
|
|
142
|
+
expect(result.allowed).toBe(false);
|
|
143
|
+
expect(result.reason).toContain("Leverage");
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|