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,457 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { existsSync, unlinkSync, mkdirSync } from "fs";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
// ── Imports from actual source files ──
|
|
5
|
+
import { formatNotifyMessage, notifyIfEnabled, } from "../arb-utils.js";
|
|
6
|
+
import { loadArbState, saveArbState, createInitialState, setStateFilePath, resetStateFilePath, } from "../arb-state.js";
|
|
7
|
+
import { computeNetSpread, computeRoundTripCostPct, isNearSettlement, isSpreadReversed, getNextSettlement, } from "../commands/arb-auto.js";
|
|
8
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
9
|
+
// 1. Promise.allSettled rollback logic (mock-based)
|
|
10
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
11
|
+
describe("Promise.allSettled rollback logic", () => {
|
|
12
|
+
/**
|
|
13
|
+
* Re-implements the decision logic from arb-auto.ts daemon cycle:
|
|
14
|
+
* After calling Promise.allSettled on [longOrder, shortOrder], determine
|
|
15
|
+
* what action to take based on the settlement results.
|
|
16
|
+
*/
|
|
17
|
+
function evaluateDualLegResult(longResult, shortResult) {
|
|
18
|
+
const longOk = longResult.status === "fulfilled";
|
|
19
|
+
const shortOk = shortResult.status === "fulfilled";
|
|
20
|
+
if (longOk && shortOk) {
|
|
21
|
+
return { action: "success" };
|
|
22
|
+
}
|
|
23
|
+
else if (longOk !== shortOk) {
|
|
24
|
+
const filledSide = longOk ? "long" : "short";
|
|
25
|
+
const failedSide = longOk ? "short" : "long";
|
|
26
|
+
return { action: "rollback", filledSide, failedSide };
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
return { action: "both_failed" };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
it("both fulfilled -> indicates success", () => {
|
|
33
|
+
const longResult = { status: "fulfilled", value: "order-123" };
|
|
34
|
+
const shortResult = { status: "fulfilled", value: "order-456" };
|
|
35
|
+
const result = evaluateDualLegResult(longResult, shortResult);
|
|
36
|
+
expect(result.action).toBe("success");
|
|
37
|
+
expect(result.filledSide).toBeUndefined();
|
|
38
|
+
expect(result.failedSide).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
it("long fulfilled, short rejected -> should attempt rollback of long", () => {
|
|
41
|
+
const longResult = { status: "fulfilled", value: "order-123" };
|
|
42
|
+
const shortResult = { status: "rejected", reason: new Error("insufficient margin") };
|
|
43
|
+
const result = evaluateDualLegResult(longResult, shortResult);
|
|
44
|
+
expect(result.action).toBe("rollback");
|
|
45
|
+
expect(result.filledSide).toBe("long");
|
|
46
|
+
expect(result.failedSide).toBe("short");
|
|
47
|
+
});
|
|
48
|
+
it("short fulfilled, long rejected -> should attempt rollback of short", () => {
|
|
49
|
+
const longResult = { status: "rejected", reason: new Error("timeout") };
|
|
50
|
+
const shortResult = { status: "fulfilled", value: "order-789" };
|
|
51
|
+
const result = evaluateDualLegResult(longResult, shortResult);
|
|
52
|
+
expect(result.action).toBe("rollback");
|
|
53
|
+
expect(result.filledSide).toBe("short");
|
|
54
|
+
expect(result.failedSide).toBe("long");
|
|
55
|
+
});
|
|
56
|
+
it("both rejected -> indicates both failed, no rollback needed", () => {
|
|
57
|
+
const longResult = { status: "rejected", reason: new Error("exchange down") };
|
|
58
|
+
const shortResult = { status: "rejected", reason: new Error("rate limited") };
|
|
59
|
+
const result = evaluateDualLegResult(longResult, shortResult);
|
|
60
|
+
expect(result.action).toBe("both_failed");
|
|
61
|
+
expect(result.filledSide).toBeUndefined();
|
|
62
|
+
expect(result.failedSide).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
it("rollback action determines correct reverse order direction", () => {
|
|
65
|
+
// In arb-auto.ts: rollbackAction = longOk ? "sell" : "buy"
|
|
66
|
+
const longOk = true;
|
|
67
|
+
const shortOk = false;
|
|
68
|
+
const rollbackAction = longOk ? "sell" : "buy";
|
|
69
|
+
expect(rollbackAction).toBe("sell"); // reverse the long (buy) with a sell
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
73
|
+
// 2. --min-size threshold
|
|
74
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
75
|
+
describe("--min-size threshold", () => {
|
|
76
|
+
/**
|
|
77
|
+
* Re-implements the min-size gate from arb-auto.ts:
|
|
78
|
+
* if (sizeIsAuto && actualSizeUsd < minSizeUsd) { continue; }
|
|
79
|
+
*/
|
|
80
|
+
function shouldSkipForMinSize(sizeIsAuto, actualSizeUsd, minSizeUsd) {
|
|
81
|
+
if (sizeIsAuto && actualSizeUsd < minSizeUsd)
|
|
82
|
+
return true;
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
it("auto-size $25 with min-size $30 -> should skip (below floor)", () => {
|
|
86
|
+
expect(shouldSkipForMinSize(true, 25, 30)).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
it("auto-size $50 with min-size $30 -> should proceed", () => {
|
|
89
|
+
expect(shouldSkipForMinSize(true, 50, 30)).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
it("auto-size $30 with min-size $30 -> should proceed (exact boundary)", () => {
|
|
92
|
+
expect(shouldSkipForMinSize(true, 30, 30)).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
it("non-auto mode ignores min-size even when size is below threshold", () => {
|
|
95
|
+
// When sizeIsAuto is false (fixed size mode), min-size check is skipped
|
|
96
|
+
expect(shouldSkipForMinSize(false, 25, 30)).toBe(false);
|
|
97
|
+
expect(shouldSkipForMinSize(false, 10, 30)).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
it("auto-size $0 with any min-size -> should skip", () => {
|
|
100
|
+
expect(shouldSkipForMinSize(true, 0, 30)).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
104
|
+
// 3. Heartbeat notification
|
|
105
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
106
|
+
describe("Heartbeat notification", () => {
|
|
107
|
+
it("formatNotifyMessage('heartbeat') returns proper message with minutes and last scan time", () => {
|
|
108
|
+
const msg = formatNotifyMessage("heartbeat", {
|
|
109
|
+
lastScanTime: "2025-01-15T10:30:00.000Z",
|
|
110
|
+
minutesAgo: 12,
|
|
111
|
+
});
|
|
112
|
+
expect(msg).toContain("HEARTBEAT");
|
|
113
|
+
expect(msg).toContain("12");
|
|
114
|
+
expect(msg).toContain("2025-01-15T10:30:00.000Z");
|
|
115
|
+
});
|
|
116
|
+
it("heartbeat message includes 'minutes' wording", () => {
|
|
117
|
+
const msg = formatNotifyMessage("heartbeat", {
|
|
118
|
+
lastScanTime: "2025-01-15T08:00:00.000Z",
|
|
119
|
+
minutesAgo: 7,
|
|
120
|
+
});
|
|
121
|
+
expect(msg).toContain("minutes");
|
|
122
|
+
expect(msg).toContain("7");
|
|
123
|
+
});
|
|
124
|
+
it("notifyIfEnabled with 'heartbeat' in events -> sends notification", async () => {
|
|
125
|
+
const mockFetch = vi.fn().mockResolvedValue({ ok: true });
|
|
126
|
+
const events = ["entry", "exit", "heartbeat"];
|
|
127
|
+
await notifyIfEnabled("https://discord.com/api/webhooks/123/abc", events, "heartbeat", { lastScanTime: "2025-01-15T10:00:00Z", minutesAgo: 10 }, mockFetch);
|
|
128
|
+
expect(mockFetch).toHaveBeenCalledOnce();
|
|
129
|
+
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
130
|
+
expect(body.content).toContain("HEARTBEAT");
|
|
131
|
+
});
|
|
132
|
+
it("notifyIfEnabled with 'heartbeat' NOT in events -> does not send", async () => {
|
|
133
|
+
const mockFetch = vi.fn().mockResolvedValue({ ok: true });
|
|
134
|
+
const events = ["entry", "exit", "reversal"];
|
|
135
|
+
await notifyIfEnabled("https://discord.com/api/webhooks/123/abc", events, "heartbeat", { lastScanTime: "2025-01-15T10:00:00Z", minutesAgo: 10 }, mockFetch);
|
|
136
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
137
|
+
});
|
|
138
|
+
it("notifyIfEnabled with empty events list -> sends (empty = all events)", async () => {
|
|
139
|
+
const mockFetch = vi.fn().mockResolvedValue({ ok: true });
|
|
140
|
+
// Empty events list means "all events enabled"
|
|
141
|
+
await notifyIfEnabled("https://discord.com/api/webhooks/123/abc", [], "heartbeat", { lastScanTime: "2025-01-15T10:00:00Z", minutesAgo: 5 }, mockFetch);
|
|
142
|
+
expect(mockFetch).toHaveBeenCalledOnce();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
146
|
+
// 4. Exit reason tagging
|
|
147
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
148
|
+
describe("Exit reason tagging", () => {
|
|
149
|
+
/**
|
|
150
|
+
* Re-implements the exit reason derivation from arb-auto.ts:
|
|
151
|
+
* const exitReason = closeReason.includes("REVERSAL") ? "reversal"
|
|
152
|
+
* : closeReason.includes("spread") ? "spread"
|
|
153
|
+
* : "manual";
|
|
154
|
+
*/
|
|
155
|
+
function deriveExitReason(closeReason) {
|
|
156
|
+
if (closeReason.includes("REVERSAL"))
|
|
157
|
+
return "reversal";
|
|
158
|
+
if (closeReason.includes("spread"))
|
|
159
|
+
return "spread";
|
|
160
|
+
return "manual";
|
|
161
|
+
}
|
|
162
|
+
it("closeReason containing 'REVERSAL' -> exitReason = 'reversal'", () => {
|
|
163
|
+
const reason = "REVERSAL DETECTED — long exchange now has higher rate than short";
|
|
164
|
+
expect(deriveExitReason(reason)).toBe("reversal");
|
|
165
|
+
});
|
|
166
|
+
it("closeReason containing 'spread' -> exitReason = 'spread'", () => {
|
|
167
|
+
const reason = "spread 3.2% <= 5%";
|
|
168
|
+
expect(deriveExitReason(reason)).toBe("spread");
|
|
169
|
+
});
|
|
170
|
+
it("manual close -> exitReason = 'manual'", () => {
|
|
171
|
+
const reason = "user requested close";
|
|
172
|
+
expect(deriveExitReason(reason)).toBe("manual");
|
|
173
|
+
});
|
|
174
|
+
it("empty closeReason -> exitReason = 'manual'", () => {
|
|
175
|
+
expect(deriveExitReason("")).toBe("manual");
|
|
176
|
+
});
|
|
177
|
+
it("exitReason appears in execution log meta structure", () => {
|
|
178
|
+
// Verify the expected shape of an arb_close execution record with exitReason
|
|
179
|
+
const record = {
|
|
180
|
+
type: "arb_close",
|
|
181
|
+
exchange: "hyperliquid+pacifica",
|
|
182
|
+
symbol: "ETH",
|
|
183
|
+
side: "close",
|
|
184
|
+
size: "0.5000",
|
|
185
|
+
status: "success",
|
|
186
|
+
dryRun: false,
|
|
187
|
+
meta: {
|
|
188
|
+
longExchange: "hyperliquid",
|
|
189
|
+
shortExchange: "pacifica",
|
|
190
|
+
currentSpread: 3.2,
|
|
191
|
+
reason: "spread 3.2% <= 5%",
|
|
192
|
+
exitReason: "spread",
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
expect(record.meta).toBeDefined();
|
|
196
|
+
expect(record.meta.exitReason).toBe("spread");
|
|
197
|
+
});
|
|
198
|
+
it("REVERSAL in closeReason takes priority over 'spread' substring", () => {
|
|
199
|
+
// If somehow both words appear, REVERSAL check is first
|
|
200
|
+
const reason = "REVERSAL — spread has flipped";
|
|
201
|
+
expect(deriveExitReason(reason)).toBe("reversal");
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
205
|
+
// 5. Funding reconciliation
|
|
206
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
207
|
+
describe("Funding reconciliation", () => {
|
|
208
|
+
/**
|
|
209
|
+
* Re-implements the diff logic from arb-auto.ts arb status command:
|
|
210
|
+
* const fundingDiff = totalActualFunding - totalEstFunding;
|
|
211
|
+
* const diffPct = totalEstFunding !== 0
|
|
212
|
+
* ? ((fundingDiff / Math.abs(totalEstFunding)) * 100).toFixed(1)
|
|
213
|
+
* : "N/A";
|
|
214
|
+
*/
|
|
215
|
+
function computeFundingReconciliation(estimated, actual) {
|
|
216
|
+
const diff = actual - estimated;
|
|
217
|
+
const diffPct = estimated !== 0
|
|
218
|
+
? ((diff / Math.abs(estimated)) * 100).toFixed(1)
|
|
219
|
+
: "N/A";
|
|
220
|
+
return { diff, diffPct };
|
|
221
|
+
}
|
|
222
|
+
it("computes positive diff when actual > estimated", () => {
|
|
223
|
+
const result = computeFundingReconciliation(10, 12);
|
|
224
|
+
expect(result.diff).toBe(2);
|
|
225
|
+
expect(result.diffPct).toBe("20.0");
|
|
226
|
+
});
|
|
227
|
+
it("computes negative diff when actual < estimated", () => {
|
|
228
|
+
const result = computeFundingReconciliation(10, 8);
|
|
229
|
+
expect(result.diff).toBe(-2);
|
|
230
|
+
expect(result.diffPct).toBe("-20.0");
|
|
231
|
+
});
|
|
232
|
+
it("zero diff when actual = estimated", () => {
|
|
233
|
+
const result = computeFundingReconciliation(5, 5);
|
|
234
|
+
expect(result.diff).toBe(0);
|
|
235
|
+
expect(result.diffPct).toBe("0.0");
|
|
236
|
+
});
|
|
237
|
+
it("handles when estimated = 0 (returns N/A for percentage)", () => {
|
|
238
|
+
const result = computeFundingReconciliation(0, 3.5);
|
|
239
|
+
expect(result.diff).toBe(3.5);
|
|
240
|
+
expect(result.diffPct).toBe("N/A");
|
|
241
|
+
});
|
|
242
|
+
it("handles when actual = 0 (no settled funding yet)", () => {
|
|
243
|
+
const result = computeFundingReconciliation(5, 0);
|
|
244
|
+
expect(result.diff).toBe(-5);
|
|
245
|
+
expect(result.diffPct).toBe("-100.0");
|
|
246
|
+
});
|
|
247
|
+
it("handles negative estimated funding", () => {
|
|
248
|
+
const result = computeFundingReconciliation(-10, -12);
|
|
249
|
+
expect(result.diff).toBe(-2);
|
|
250
|
+
// diff / |estimated| = -2/10 = -20%
|
|
251
|
+
expect(result.diffPct).toBe("-20.0");
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
255
|
+
// 6. ArbDaemonState with lastSuccessfulScanTime
|
|
256
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
257
|
+
describe("ArbDaemonState with lastSuccessfulScanTime", () => {
|
|
258
|
+
const TEST_DIR = resolve(process.env.HOME || "~", ".perp", "test-arb-new-features");
|
|
259
|
+
const TEST_FILE = resolve(TEST_DIR, "arb-state.json");
|
|
260
|
+
const TMP_FILE = TEST_FILE + ".tmp";
|
|
261
|
+
beforeEach(() => {
|
|
262
|
+
if (!existsSync(TEST_DIR))
|
|
263
|
+
mkdirSync(TEST_DIR, { recursive: true });
|
|
264
|
+
if (existsSync(TEST_FILE))
|
|
265
|
+
unlinkSync(TEST_FILE);
|
|
266
|
+
if (existsSync(TMP_FILE))
|
|
267
|
+
unlinkSync(TMP_FILE);
|
|
268
|
+
setStateFilePath(TEST_FILE);
|
|
269
|
+
});
|
|
270
|
+
afterEach(() => {
|
|
271
|
+
if (existsSync(TEST_FILE))
|
|
272
|
+
unlinkSync(TEST_FILE);
|
|
273
|
+
if (existsSync(TMP_FILE))
|
|
274
|
+
unlinkSync(TMP_FILE);
|
|
275
|
+
resetStateFilePath();
|
|
276
|
+
});
|
|
277
|
+
it("createInitialState includes lastSuccessfulScanTime", () => {
|
|
278
|
+
const state = createInitialState({
|
|
279
|
+
minSpread: 30, closeSpread: 5, size: 100,
|
|
280
|
+
holdDays: 7, bridgeCost: 0.5, maxPositions: 5, settleStrategy: "block",
|
|
281
|
+
});
|
|
282
|
+
expect(state).toHaveProperty("lastSuccessfulScanTime");
|
|
283
|
+
expect(state.lastSuccessfulScanTime).toBeTruthy();
|
|
284
|
+
});
|
|
285
|
+
it("lastSuccessfulScanTime is an ISO timestamp string", () => {
|
|
286
|
+
const state = createInitialState({
|
|
287
|
+
minSpread: 30, closeSpread: 5, size: 100,
|
|
288
|
+
holdDays: 7, bridgeCost: 0.5, maxPositions: 5, settleStrategy: "block",
|
|
289
|
+
});
|
|
290
|
+
// Validate ISO format by parsing it
|
|
291
|
+
const parsed = new Date(state.lastSuccessfulScanTime);
|
|
292
|
+
expect(parsed.getTime()).not.toBeNaN();
|
|
293
|
+
expect(state.lastSuccessfulScanTime).toMatch(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
|
294
|
+
});
|
|
295
|
+
it("saveArbState + loadArbState preserves lastSuccessfulScanTime", () => {
|
|
296
|
+
const state = createInitialState({
|
|
297
|
+
minSpread: 30, closeSpread: 5, size: 100,
|
|
298
|
+
holdDays: 7, bridgeCost: 0.5, maxPositions: 5, settleStrategy: "block",
|
|
299
|
+
});
|
|
300
|
+
const testTimestamp = "2025-06-15T14:30:00.000Z";
|
|
301
|
+
state.lastSuccessfulScanTime = testTimestamp;
|
|
302
|
+
saveArbState(state);
|
|
303
|
+
const loaded = loadArbState();
|
|
304
|
+
expect(loaded).not.toBeNull();
|
|
305
|
+
expect(loaded.lastSuccessfulScanTime).toBe(testTimestamp);
|
|
306
|
+
});
|
|
307
|
+
it("lastSuccessfulScanTime survives state update cycle", () => {
|
|
308
|
+
const state = createInitialState({
|
|
309
|
+
minSpread: 30, closeSpread: 5, size: 100,
|
|
310
|
+
holdDays: 7, bridgeCost: 0.5, maxPositions: 5, settleStrategy: "block",
|
|
311
|
+
});
|
|
312
|
+
const ts1 = "2025-06-15T10:00:00.000Z";
|
|
313
|
+
state.lastSuccessfulScanTime = ts1;
|
|
314
|
+
saveArbState(state);
|
|
315
|
+
// Simulate daemon updating lastSuccessfulScanTime
|
|
316
|
+
const loaded = loadArbState();
|
|
317
|
+
const ts2 = "2025-06-15T11:00:00.000Z";
|
|
318
|
+
loaded.lastSuccessfulScanTime = ts2;
|
|
319
|
+
loaded.lastScanTime = ts2;
|
|
320
|
+
saveArbState(loaded);
|
|
321
|
+
const reloaded = loadArbState();
|
|
322
|
+
expect(reloaded.lastSuccessfulScanTime).toBe(ts2);
|
|
323
|
+
expect(reloaded.lastScanTime).toBe(ts2);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
327
|
+
// 7. Exchange downtime detection logic
|
|
328
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
329
|
+
describe("Exchange downtime detection logic", () => {
|
|
330
|
+
/**
|
|
331
|
+
* Re-implements the exchange health check pattern from arb-auto.ts:
|
|
332
|
+
* - Try getMarkets() for each exchange
|
|
333
|
+
* - If it throws, add to downExchanges and blockedExchanges
|
|
334
|
+
* - If a position has an exchange in downExchanges, mark as degraded
|
|
335
|
+
*/
|
|
336
|
+
async function checkExchangeHealth(exchangeNames, getMarketsFn) {
|
|
337
|
+
const downExchanges = new Set();
|
|
338
|
+
const blockedExchanges = new Set();
|
|
339
|
+
for (const name of exchangeNames) {
|
|
340
|
+
try {
|
|
341
|
+
await getMarketsFn(name);
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
downExchanges.add(name);
|
|
345
|
+
blockedExchanges.add(name);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return { downExchanges, blockedExchanges };
|
|
349
|
+
}
|
|
350
|
+
function isPositionDegraded(position, downExchanges) {
|
|
351
|
+
return downExchanges.has(position.longExchange) || downExchanges.has(position.shortExchange);
|
|
352
|
+
}
|
|
353
|
+
it("getMarkets() success -> not in downExchanges", async () => {
|
|
354
|
+
const getMarkets = vi.fn().mockResolvedValue([{ symbol: "BTC", markPrice: "50000" }]);
|
|
355
|
+
const result = await checkExchangeHealth(["hyperliquid"], getMarkets);
|
|
356
|
+
expect(result.downExchanges.has("hyperliquid")).toBe(false);
|
|
357
|
+
expect(result.blockedExchanges.has("hyperliquid")).toBe(false);
|
|
358
|
+
});
|
|
359
|
+
it("getMarkets() throws -> in downExchanges and blockedExchanges", async () => {
|
|
360
|
+
const getMarkets = vi.fn().mockRejectedValue(new Error("connection refused"));
|
|
361
|
+
const result = await checkExchangeHealth(["lighter"], getMarkets);
|
|
362
|
+
expect(result.downExchanges.has("lighter")).toBe(true);
|
|
363
|
+
expect(result.blockedExchanges.has("lighter")).toBe(true);
|
|
364
|
+
});
|
|
365
|
+
it("mixed exchange health: some up, some down", async () => {
|
|
366
|
+
const getMarkets = vi.fn()
|
|
367
|
+
.mockImplementation(async (name) => {
|
|
368
|
+
if (name === "pacifica")
|
|
369
|
+
throw new Error("503 service unavailable");
|
|
370
|
+
return [{ symbol: "ETH" }];
|
|
371
|
+
});
|
|
372
|
+
const result = await checkExchangeHealth(["hyperliquid", "pacifica", "lighter"], getMarkets);
|
|
373
|
+
expect(result.downExchanges.has("pacifica")).toBe(true);
|
|
374
|
+
expect(result.blockedExchanges.has("pacifica")).toBe(true);
|
|
375
|
+
expect(result.downExchanges.has("hyperliquid")).toBe(false);
|
|
376
|
+
expect(result.downExchanges.has("lighter")).toBe(false);
|
|
377
|
+
});
|
|
378
|
+
it("position on down exchange -> marked as degraded", async () => {
|
|
379
|
+
const downExchanges = new Set(["pacifica"]);
|
|
380
|
+
const position = { longExchange: "hyperliquid", shortExchange: "pacifica" };
|
|
381
|
+
expect(isPositionDegraded(position, downExchanges)).toBe(true);
|
|
382
|
+
});
|
|
383
|
+
it("position on healthy exchanges -> not degraded", () => {
|
|
384
|
+
const downExchanges = new Set();
|
|
385
|
+
const position = { longExchange: "hyperliquid", shortExchange: "lighter" };
|
|
386
|
+
expect(isPositionDegraded(position, downExchanges)).toBe(false);
|
|
387
|
+
});
|
|
388
|
+
it("position degraded when long exchange is down", () => {
|
|
389
|
+
const downExchanges = new Set(["hyperliquid"]);
|
|
390
|
+
const position = { longExchange: "hyperliquid", shortExchange: "pacifica" };
|
|
391
|
+
expect(isPositionDegraded(position, downExchanges)).toBe(true);
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
395
|
+
// Additional: computeNetSpread, computeRoundTripCostPct, isNearSettlement
|
|
396
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
397
|
+
describe("computeNetSpread", () => {
|
|
398
|
+
it("deducts round-trip cost annualized over hold period", () => {
|
|
399
|
+
// Gross 50% annual, 0.38% round trip cost, 7 day hold, no bridge
|
|
400
|
+
const net = computeNetSpread(50, 7, 0.38, 0, 0);
|
|
401
|
+
// annualized cost = (0.38 / 7) * 365 = ~19.81%
|
|
402
|
+
// net = 50 - 19.81 = ~30.19%
|
|
403
|
+
expect(net).toBeCloseTo(30.19, 0);
|
|
404
|
+
});
|
|
405
|
+
it("includes bridge cost when provided", () => {
|
|
406
|
+
// Gross 50%, 0.38% RT, 7d hold, $1 bridge, $100 position
|
|
407
|
+
const net = computeNetSpread(50, 7, 0.38, 1, 100);
|
|
408
|
+
// bridge round-trip pct = (1*2/100)*100 = 2%
|
|
409
|
+
// bridge annualized = (2/7)*365 = ~104.3%
|
|
410
|
+
// net = 50 - 19.81 - 104.3 -> very negative
|
|
411
|
+
expect(net).toBeLessThan(0);
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
describe("computeRoundTripCostPct", () => {
|
|
415
|
+
it("computes standard costs for default exchanges", () => {
|
|
416
|
+
const cost = computeRoundTripCostPct("hyperliquid", "pacifica", 0.05);
|
|
417
|
+
// 2 * (0.035 + 0.035) + 2 * 0.05 = 0.14 + 0.10 = 0.24%
|
|
418
|
+
expect(cost).toBeCloseTo(0.24, 2);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
describe("isNearSettlement", () => {
|
|
422
|
+
it("blocks when within buffer of next settlement", () => {
|
|
423
|
+
// 15:57 UTC, next settlement at 16:00 -> 3 minutes away, within 5 min buffer
|
|
424
|
+
const now = new Date("2024-06-15T15:57:00Z");
|
|
425
|
+
const result = isNearSettlement("hyperliquid", "pacifica", 5, now);
|
|
426
|
+
expect(result.blocked).toBe(true);
|
|
427
|
+
expect(result.minutesUntil).toBeLessThanOrEqual(5);
|
|
428
|
+
});
|
|
429
|
+
it("does not block when far from settlement", () => {
|
|
430
|
+
// 15:30 UTC, next settlement at 16:00 -> 30 minutes away
|
|
431
|
+
const now = new Date("2024-06-15T15:30:00Z");
|
|
432
|
+
const result = isNearSettlement("hyperliquid", "pacifica", 5, now);
|
|
433
|
+
expect(result.blocked).toBe(false);
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
describe("getNextSettlement", () => {
|
|
437
|
+
it("returns next hour for hourly exchanges", () => {
|
|
438
|
+
const now = new Date("2024-06-15T14:30:00Z");
|
|
439
|
+
const next = getNextSettlement("hyperliquid", now);
|
|
440
|
+
expect(next.getUTCHours()).toBe(15);
|
|
441
|
+
expect(next.getUTCMinutes()).toBe(0);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
describe("isSpreadReversed", () => {
|
|
445
|
+
it("detects reversal when long exchange has higher rate", () => {
|
|
446
|
+
// Long HL, Short PAC. If HL rate > PAC rate hourly -> reversed
|
|
447
|
+
const snapshot = { symbol: "ETH", pacRate: 0.0001, hlRate: 0.0005, ltRate: 0, spread: 0, longExch: "hyperliquid", shortExch: "pacifica", markPrice: 3000, pacMarkPrice: 0, hlMarkPrice: 0, ltMarkPrice: 0 };
|
|
448
|
+
const reversed = isSpreadReversed("hyperliquid", "pacifica", snapshot);
|
|
449
|
+
expect(reversed).toBe(true);
|
|
450
|
+
});
|
|
451
|
+
it("no reversal when short exchange has higher rate", () => {
|
|
452
|
+
// Long HL, Short PAC. PAC rate > HL rate -> NOT reversed
|
|
453
|
+
const snapshot = { symbol: "ETH", pacRate: 0.0005, hlRate: 0.0001, ltRate: 0, spread: 0, longExch: "hyperliquid", shortExch: "pacifica", markPrice: 3000, pacMarkPrice: 0, hlMarkPrice: 0, ltMarkPrice: 0 };
|
|
454
|
+
const reversed = isSpreadReversed("hyperliquid", "pacifica", snapshot);
|
|
455
|
+
expect(reversed).toBe(false);
|
|
456
|
+
});
|
|
457
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { computeMatchedSize } from "../arb-sizing.js";
|
|
3
|
+
describe("computeMatchedSize", () => {
|
|
4
|
+
it("should compute size respecting least precise exchange", () => {
|
|
5
|
+
// HL (1 decimal) + Lighter (2 decimals) -> use 1 decimal
|
|
6
|
+
const result = computeMatchedSize(100, 50, "hyperliquid", "lighter");
|
|
7
|
+
expect(result).not.toBeNull();
|
|
8
|
+
expect(result.size).toBe("2.0"); // 100/50 = 2.0
|
|
9
|
+
expect(result.notional).toBe(100);
|
|
10
|
+
});
|
|
11
|
+
it("should round down to avoid exceeding requested size", () => {
|
|
12
|
+
const result = computeMatchedSize(100, 33, "hyperliquid", "lighter");
|
|
13
|
+
expect(result).not.toBeNull();
|
|
14
|
+
// 100/33 = 3.0303... -> floor to 3.0 (HL=1 decimal)
|
|
15
|
+
expect(result.size).toBe("3.0");
|
|
16
|
+
expect(result.notional).toBeLessThanOrEqual(100);
|
|
17
|
+
});
|
|
18
|
+
it("should return null when price is 0", () => {
|
|
19
|
+
expect(computeMatchedSize(100, 0, "hyperliquid", "lighter")).toBeNull();
|
|
20
|
+
});
|
|
21
|
+
it("should return null when size is too small for min notional", () => {
|
|
22
|
+
// $5 / $100000 = 0.00005 -> rounds to 0.0 for HL (1 decimal)
|
|
23
|
+
expect(computeMatchedSize(5, 100000, "hyperliquid", "lighter")).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
it("should use more precision for pacifica", () => {
|
|
26
|
+
// Pacifica (4 decimals) + Lighter (2 decimals) -> use 2 decimals
|
|
27
|
+
const result = computeMatchedSize(100, 3500, "pacifica", "lighter");
|
|
28
|
+
expect(result).not.toBeNull();
|
|
29
|
+
// 100/3500 = 0.02857... -> floor to 0.02
|
|
30
|
+
expect(result.size).toBe("0.02");
|
|
31
|
+
});
|
|
32
|
+
it("should handle same exchange pair", () => {
|
|
33
|
+
const result = computeMatchedSize(1000, 100, "hyperliquid", "hyperliquid");
|
|
34
|
+
expect(result).not.toBeNull();
|
|
35
|
+
expect(result.size).toBe("10.0");
|
|
36
|
+
});
|
|
37
|
+
it("should try rounding up if floor is below min notional", () => {
|
|
38
|
+
// $12 / $3500 = 0.00342... -> floor to 0.0 (HL 1 decimal) -> try round up to 0.1
|
|
39
|
+
// 0.1 * 3500 = 350 which is way more than 12*1.2=14.4, so should return null
|
|
40
|
+
expect(computeMatchedSize(12, 3500, "hyperliquid", "lighter")).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
it("should meet min notional of both exchanges", () => {
|
|
43
|
+
// Both HL and LT need $10 min
|
|
44
|
+
const result = computeMatchedSize(15, 150, "hyperliquid", "lighter");
|
|
45
|
+
expect(result).not.toBeNull();
|
|
46
|
+
expect(result.notional).toBeGreaterThanOrEqual(10);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|