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,264 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { getLastSettlement, getMinutesSinceSettlement, aggressiveSettleBoost, estimateFundingUntilSettlement, computeBasisRisk, formatNotifyMessage, sendNotification, } from "../arb-utils.js";
|
|
3
|
+
// ── Settlement Timing Tests ──
|
|
4
|
+
describe("getMinutesSinceSettlement", () => {
|
|
5
|
+
it("returns correct minutes after HL settlement (hourly)", () => {
|
|
6
|
+
// 14:30 UTC — last HL settlement was at 14:00, so 30 minutes ago
|
|
7
|
+
const now = new Date("2024-06-15T14:30:00Z");
|
|
8
|
+
const mins = getMinutesSinceSettlement("hyperliquid", now);
|
|
9
|
+
expect(mins).toBeCloseTo(30, 0);
|
|
10
|
+
});
|
|
11
|
+
it("returns correct minutes after PAC settlement (every 1h, same as HL)", () => {
|
|
12
|
+
// 10:15 UTC — last PAC settlement was at 10:00, so 15 minutes ago
|
|
13
|
+
const now = new Date("2024-06-15T10:15:00Z");
|
|
14
|
+
const mins = getMinutesSinceSettlement("pacifica", now);
|
|
15
|
+
expect(mins).toBeCloseTo(15, 0);
|
|
16
|
+
});
|
|
17
|
+
it("returns small value right after settlement", () => {
|
|
18
|
+
// 16:02 UTC — 2 minutes after PAC settlement at 16:00
|
|
19
|
+
const now = new Date("2024-06-15T16:02:00Z");
|
|
20
|
+
const mins = getMinutesSinceSettlement("pacifica", now);
|
|
21
|
+
expect(mins).toBeCloseTo(2, 0);
|
|
22
|
+
});
|
|
23
|
+
it("returns correct value right before next settlement", () => {
|
|
24
|
+
// 07:55 UTC — 55 minutes since last PAC settlement at 07:00
|
|
25
|
+
const now = new Date("2024-06-15T07:55:00Z");
|
|
26
|
+
const mins = getMinutesSinceSettlement("pacifica", now);
|
|
27
|
+
expect(mins).toBeCloseTo(55, 0);
|
|
28
|
+
});
|
|
29
|
+
it("handles midnight correctly for PAC", () => {
|
|
30
|
+
// 00:05 UTC — 5 minutes after 00:00 PAC settlement
|
|
31
|
+
const now = new Date("2024-06-15T00:05:00Z");
|
|
32
|
+
const mins = getMinutesSinceSettlement("pacifica", now);
|
|
33
|
+
expect(mins).toBeCloseTo(5, 0);
|
|
34
|
+
});
|
|
35
|
+
it("handles Lighter same as Pacifica (both hourly)", () => {
|
|
36
|
+
const now = new Date("2024-06-15T10:00:00Z");
|
|
37
|
+
const pacMins = getMinutesSinceSettlement("pacifica", now);
|
|
38
|
+
const ltMins = getMinutesSinceSettlement("lighter", now);
|
|
39
|
+
expect(ltMins).toBe(pacMins);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe("getLastSettlement", () => {
|
|
43
|
+
it("returns the exact settlement time for HL", () => {
|
|
44
|
+
const now = new Date("2024-06-15T14:30:00Z");
|
|
45
|
+
const last = getLastSettlement("hyperliquid", now);
|
|
46
|
+
expect(last.getUTCHours()).toBe(14);
|
|
47
|
+
expect(last.getUTCMinutes()).toBe(0);
|
|
48
|
+
});
|
|
49
|
+
it("returns same hour for PAC (hourly settlement, same as HL)", () => {
|
|
50
|
+
// At 00:05 the last PAC settlement is 00:00 same day
|
|
51
|
+
const now = new Date("2024-06-15T00:05:00Z");
|
|
52
|
+
const last = getLastSettlement("pacifica", now);
|
|
53
|
+
expect(last.getUTCHours()).toBe(0);
|
|
54
|
+
expect(last.getUTCDate()).toBe(15); // same day since 00:00 is a settlement
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe("aggressiveSettleBoost", () => {
|
|
58
|
+
it("returns > 1.0 immediately after both exchanges settle", () => {
|
|
59
|
+
// Right after both HL and PAC settle (e.g., 16:01)
|
|
60
|
+
const now = new Date("2024-06-15T16:01:00Z");
|
|
61
|
+
const boost = aggressiveSettleBoost("hyperliquid", "pacifica", 10, now);
|
|
62
|
+
expect(boost).toBeGreaterThan(1.0);
|
|
63
|
+
expect(boost).toBeLessThanOrEqual(1.5);
|
|
64
|
+
});
|
|
65
|
+
it("returns 1.5 at exactly settlement time", () => {
|
|
66
|
+
// At exactly 16:00:01 — both just settled
|
|
67
|
+
const now = new Date("2024-06-15T16:00:01Z");
|
|
68
|
+
const boost = aggressiveSettleBoost("hyperliquid", "pacifica", 10, now);
|
|
69
|
+
// HL settled 0.01min ago, PAC settled 0.01min ago
|
|
70
|
+
// min = ~0.01, factor = 1 + 0.5 * (1 - 0.01/10) = ~1.499
|
|
71
|
+
expect(boost).toBeGreaterThan(1.4);
|
|
72
|
+
expect(boost).toBeLessThanOrEqual(1.5);
|
|
73
|
+
});
|
|
74
|
+
it("returns 1.0 when far from settlement", () => {
|
|
75
|
+
// 14:30 — both HL and PAC settled at 14:00, 30 minutes ago
|
|
76
|
+
const now = new Date("2024-06-15T14:30:00Z");
|
|
77
|
+
const boost = aggressiveSettleBoost("hyperliquid", "pacifica", 10, now);
|
|
78
|
+
// HL settled 30min ago > 10 window, PAC settled 30min ago > 10 window
|
|
79
|
+
// min(30, 30) = 30 > 10, so boost = 1.0
|
|
80
|
+
expect(boost).toBe(1.0);
|
|
81
|
+
});
|
|
82
|
+
it("returns 1.0 for both exchanges when settled > window ago", () => {
|
|
83
|
+
// HL settled 15 minutes ago, PAC settled 15 minutes ago
|
|
84
|
+
// With window of 10, both are > 10 so boost = 1.0
|
|
85
|
+
const now = new Date("2024-06-15T08:15:00Z");
|
|
86
|
+
const boost = aggressiveSettleBoost("hyperliquid", "pacifica", 10, now);
|
|
87
|
+
// HL settled at 08:00 (15 min ago), PAC settled at 08:00 (15 min ago)
|
|
88
|
+
// min(15, 15) = 15 > 10 => 1.0
|
|
89
|
+
expect(boost).toBe(1.0);
|
|
90
|
+
});
|
|
91
|
+
it("decays linearly within window", () => {
|
|
92
|
+
// HL settled 5 minutes ago, PAC settled 5 minutes ago (at 08:05)
|
|
93
|
+
const now = new Date("2024-06-15T08:05:00Z");
|
|
94
|
+
const boost = aggressiveSettleBoost("hyperliquid", "pacifica", 10, now);
|
|
95
|
+
// min(5, 5) = 5, factor = 1 + 0.5 * (1 - 5/10) = 1.25
|
|
96
|
+
expect(boost).toBeCloseTo(1.25, 1);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
// ── Funding Estimation Tests ──
|
|
100
|
+
describe("estimateFundingUntilSettlement", () => {
|
|
101
|
+
it("calculates correct cumulative HL funding", () => {
|
|
102
|
+
// 0.01% hourly rate, $1000 position, 4 hours until settlement
|
|
103
|
+
const result = estimateFundingUntilSettlement(0.0001, 0.00005, 1000, 4);
|
|
104
|
+
// hlCumulative = 0.0001 * 1000 * 4 = 0.4
|
|
105
|
+
expect(result.hlCumulative).toBeCloseTo(0.4, 4);
|
|
106
|
+
});
|
|
107
|
+
it("calculates correct PAC payment (also hourly)", () => {
|
|
108
|
+
// PAC hourly rate = 0.00005, $1000 position, 4 hours
|
|
109
|
+
const result = estimateFundingUntilSettlement(0.0001, 0.00005, 1000, 4);
|
|
110
|
+
// pacPayment = 0.00005 * 1000 * 4 = 0.2
|
|
111
|
+
expect(result.pacPayment).toBeCloseTo(0.2, 4);
|
|
112
|
+
});
|
|
113
|
+
it("calculates net funding correctly", () => {
|
|
114
|
+
const result = estimateFundingUntilSettlement(0.0001, 0.00005, 1000, 4);
|
|
115
|
+
// net = 0.4 - 0.2 = 0.2
|
|
116
|
+
expect(result.netFunding).toBeCloseTo(0.2, 4);
|
|
117
|
+
});
|
|
118
|
+
it("handles zero rates", () => {
|
|
119
|
+
const result = estimateFundingUntilSettlement(0, 0, 1000, 8);
|
|
120
|
+
expect(result.hlCumulative).toBe(0);
|
|
121
|
+
expect(result.pacPayment).toBe(0);
|
|
122
|
+
expect(result.netFunding).toBe(0);
|
|
123
|
+
});
|
|
124
|
+
it("scales linearly with position size", () => {
|
|
125
|
+
const small = estimateFundingUntilSettlement(0.0001, 0.00005, 100, 4);
|
|
126
|
+
const big = estimateFundingUntilSettlement(0.0001, 0.00005, 1000, 4);
|
|
127
|
+
expect(big.hlCumulative).toBeCloseTo(small.hlCumulative * 10, 4);
|
|
128
|
+
expect(big.pacPayment).toBeCloseTo(small.pacPayment * 10, 4);
|
|
129
|
+
});
|
|
130
|
+
it("both sides scale with time (both hourly)", () => {
|
|
131
|
+
const short = estimateFundingUntilSettlement(0.0001, 0.00005, 1000, 2);
|
|
132
|
+
const long = estimateFundingUntilSettlement(0.0001, 0.00005, 1000, 8);
|
|
133
|
+
expect(long.hlCumulative).toBeCloseTo(short.hlCumulative * 4, 4);
|
|
134
|
+
expect(long.pacPayment).toBeCloseTo(short.pacPayment * 4, 4);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
// ── Basis Risk Tests ──
|
|
138
|
+
describe("computeBasisRisk", () => {
|
|
139
|
+
it("detects divergence correctly", () => {
|
|
140
|
+
const result = computeBasisRisk(100, 104, 3);
|
|
141
|
+
// |100 - 104| / 102 * 100 = ~3.92%
|
|
142
|
+
expect(result.divergencePct).toBeCloseTo(3.92, 1);
|
|
143
|
+
expect(result.warning).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
it("no warning when divergence is low", () => {
|
|
146
|
+
const result = computeBasisRisk(100, 101, 3);
|
|
147
|
+
// |100 - 101| / 100.5 * 100 = ~0.995%
|
|
148
|
+
expect(result.divergencePct).toBeCloseTo(1.0, 0);
|
|
149
|
+
expect(result.warning).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
it("handles equal prices", () => {
|
|
152
|
+
const result = computeBasisRisk(50, 50, 3);
|
|
153
|
+
expect(result.divergencePct).toBe(0);
|
|
154
|
+
expect(result.warning).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
it("handles zero prices", () => {
|
|
157
|
+
const result = computeBasisRisk(0, 100, 3);
|
|
158
|
+
expect(result.divergencePct).toBe(0);
|
|
159
|
+
expect(result.warning).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
it("uses custom threshold", () => {
|
|
162
|
+
const result = computeBasisRisk(100, 101, 0.5);
|
|
163
|
+
// ~1% divergence > 0.5% threshold
|
|
164
|
+
expect(result.warning).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
it("symmetric for long/short swap", () => {
|
|
167
|
+
const a = computeBasisRisk(100, 105, 3);
|
|
168
|
+
const b = computeBasisRisk(105, 100, 3);
|
|
169
|
+
expect(a.divergencePct).toBeCloseTo(b.divergencePct, 4);
|
|
170
|
+
expect(a.warning).toBe(b.warning);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
// ── Notification Tests ──
|
|
174
|
+
describe("formatNotifyMessage", () => {
|
|
175
|
+
it("formats entry message", () => {
|
|
176
|
+
const msg = formatNotifyMessage("entry", {
|
|
177
|
+
symbol: "WIF", longExchange: "lighter", shortExchange: "pacifica",
|
|
178
|
+
size: 500, netSpread: 28.5,
|
|
179
|
+
});
|
|
180
|
+
expect(msg).toContain("WIF");
|
|
181
|
+
expect(msg).toContain("Long lighter");
|
|
182
|
+
expect(msg).toContain("Short pacifica");
|
|
183
|
+
expect(msg).toContain("$500");
|
|
184
|
+
expect(msg).toContain("28.5%");
|
|
185
|
+
});
|
|
186
|
+
it("formats exit message", () => {
|
|
187
|
+
const msg = formatNotifyMessage("exit", {
|
|
188
|
+
symbol: "ETH", pnl: 10.5, duration: "7d 3h",
|
|
189
|
+
});
|
|
190
|
+
expect(msg).toContain("ETH");
|
|
191
|
+
expect(msg).toContain("+$10.50");
|
|
192
|
+
expect(msg).toContain("7d 3h");
|
|
193
|
+
});
|
|
194
|
+
it("formats exit message with negative PnL", () => {
|
|
195
|
+
const msg = formatNotifyMessage("exit", {
|
|
196
|
+
symbol: "SOL", pnl: -5.25, duration: "2d",
|
|
197
|
+
});
|
|
198
|
+
expect(msg).toContain("SOL");
|
|
199
|
+
expect(msg).toContain("-$5.25");
|
|
200
|
+
});
|
|
201
|
+
it("formats reversal message", () => {
|
|
202
|
+
const msg = formatNotifyMessage("reversal", { symbol: "WIF" });
|
|
203
|
+
expect(msg).toContain("REVERSAL");
|
|
204
|
+
expect(msg).toContain("WIF");
|
|
205
|
+
});
|
|
206
|
+
it("formats margin message", () => {
|
|
207
|
+
const msg = formatNotifyMessage("margin", {
|
|
208
|
+
exchange: "Lighter", marginPct: 25.3, threshold: 30,
|
|
209
|
+
});
|
|
210
|
+
expect(msg).toContain("LOW MARGIN");
|
|
211
|
+
expect(msg).toContain("Lighter");
|
|
212
|
+
expect(msg).toContain("25.3%");
|
|
213
|
+
expect(msg).toContain("30.0%");
|
|
214
|
+
});
|
|
215
|
+
it("formats basis risk message", () => {
|
|
216
|
+
const msg = formatNotifyMessage("basis", {
|
|
217
|
+
symbol: "WIF", divergencePct: 4.2, longExchange: "LT", shortExchange: "PAC",
|
|
218
|
+
});
|
|
219
|
+
expect(msg).toContain("BASIS RISK");
|
|
220
|
+
expect(msg).toContain("WIF");
|
|
221
|
+
expect(msg).toContain("4.2%");
|
|
222
|
+
expect(msg).toContain("LT/PAC");
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
describe("sendNotification", () => {
|
|
226
|
+
it("sends Discord webhook with content field", async () => {
|
|
227
|
+
const mockFetch = vi.fn().mockResolvedValue({ ok: true });
|
|
228
|
+
await sendNotification("https://discord.com/api/webhooks/123/abc", "entry", { symbol: "BTC", longExchange: "HL", shortExchange: "PAC", size: 100, netSpread: 30 }, mockFetch);
|
|
229
|
+
expect(mockFetch).toHaveBeenCalledOnce();
|
|
230
|
+
const [url, opts] = mockFetch.mock.calls[0];
|
|
231
|
+
expect(url).toContain("discord.com/api/webhooks");
|
|
232
|
+
const body = JSON.parse(opts.body);
|
|
233
|
+
expect(body).toHaveProperty("content");
|
|
234
|
+
expect(body.content).toContain("BTC");
|
|
235
|
+
});
|
|
236
|
+
it("sends Telegram webhook with chat_id and text", async () => {
|
|
237
|
+
const mockFetch = vi.fn().mockResolvedValue({ ok: true });
|
|
238
|
+
await sendNotification("https://api.telegram.org/bot123:TOKEN/sendMessage?chat_id=456", "exit", { symbol: "ETH", pnl: 5, duration: "3h" }, mockFetch);
|
|
239
|
+
expect(mockFetch).toHaveBeenCalledOnce();
|
|
240
|
+
const [url, opts] = mockFetch.mock.calls[0];
|
|
241
|
+
expect(url).toContain("api.telegram.org");
|
|
242
|
+
const body = JSON.parse(opts.body);
|
|
243
|
+
expect(body).toHaveProperty("chat_id", "456");
|
|
244
|
+
expect(body).toHaveProperty("text");
|
|
245
|
+
expect(body.text).toContain("ETH");
|
|
246
|
+
});
|
|
247
|
+
it("sends generic webhook with JSON body", async () => {
|
|
248
|
+
const mockFetch = vi.fn().mockResolvedValue({ ok: true });
|
|
249
|
+
await sendNotification("https://my-api.example.com/webhook", "basis", { symbol: "SOL", divergencePct: 5 }, mockFetch);
|
|
250
|
+
expect(mockFetch).toHaveBeenCalledOnce();
|
|
251
|
+
const [url, opts] = mockFetch.mock.calls[0];
|
|
252
|
+
expect(url).toContain("my-api.example.com");
|
|
253
|
+
const body = JSON.parse(opts.body);
|
|
254
|
+
expect(body).toHaveProperty("event", "basis");
|
|
255
|
+
expect(body).toHaveProperty("message");
|
|
256
|
+
expect(body).toHaveProperty("data");
|
|
257
|
+
});
|
|
258
|
+
it("does not throw on fetch failure", async () => {
|
|
259
|
+
const mockFetch = vi.fn().mockRejectedValue(new Error("network error"));
|
|
260
|
+
// Should not throw
|
|
261
|
+
await sendNotification("https://discord.com/api/webhooks/123/abc", "entry", { symbol: "BTC" }, mockFetch);
|
|
262
|
+
expect(mockFetch).toHaveBeenCalledOnce();
|
|
263
|
+
});
|
|
264
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { calculateRSI, evaluateCondition, evaluateAllConditions, } from "../bot/conditions.js";
|
|
3
|
+
// ── Helper: build a snapshot with defaults ──
|
|
4
|
+
function makeSnapshot(overrides = {}) {
|
|
5
|
+
return {
|
|
6
|
+
price: 100,
|
|
7
|
+
high24h: 105,
|
|
8
|
+
low24h: 95,
|
|
9
|
+
volume24h: 1_000_000,
|
|
10
|
+
fundingRate: 0.0001,
|
|
11
|
+
volatility24h: 10,
|
|
12
|
+
rsi: 50,
|
|
13
|
+
spreadPct: 0.05,
|
|
14
|
+
...overrides,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const defaultContext = {
|
|
18
|
+
equity: 10_000,
|
|
19
|
+
startTime: Date.now() - 60_000,
|
|
20
|
+
peakEquity: 10_500,
|
|
21
|
+
dailyPnl: -200,
|
|
22
|
+
};
|
|
23
|
+
// ═══════════════════════════════════════════════════
|
|
24
|
+
// RSI Calculation Tests
|
|
25
|
+
// ═══════════════════════════════════════════════════
|
|
26
|
+
describe("calculateRSI", () => {
|
|
27
|
+
it("returns NaN when insufficient data (fewer than period+1 prices)", () => {
|
|
28
|
+
// Need at least 15 prices for period=14 (to get 14 deltas)
|
|
29
|
+
const closes = [100, 101, 102, 103, 104]; // only 5 prices
|
|
30
|
+
expect(calculateRSI(closes, 14)).toBeNaN();
|
|
31
|
+
});
|
|
32
|
+
it("returns NaN for empty array", () => {
|
|
33
|
+
expect(calculateRSI([])).toBeNaN();
|
|
34
|
+
});
|
|
35
|
+
it("returns NaN for single price", () => {
|
|
36
|
+
expect(calculateRSI([100])).toBeNaN();
|
|
37
|
+
});
|
|
38
|
+
it("returns 100 when all changes are positive (14 consecutive ups)", () => {
|
|
39
|
+
// 15 prices: 100, 101, ..., 114 — all gains, no losses
|
|
40
|
+
const closes = Array.from({ length: 15 }, (_, i) => 100 + i);
|
|
41
|
+
const rsi = calculateRSI(closes, 14);
|
|
42
|
+
expect(rsi).toBe(100);
|
|
43
|
+
});
|
|
44
|
+
it("returns ~0 when all changes are negative (14 consecutive downs)", () => {
|
|
45
|
+
// 15 prices: 114, 113, ..., 100 — all losses, no gains
|
|
46
|
+
const closes = Array.from({ length: 15 }, (_, i) => 114 - i);
|
|
47
|
+
const rsi = calculateRSI(closes, 14);
|
|
48
|
+
expect(rsi).toBeCloseTo(0, 5);
|
|
49
|
+
});
|
|
50
|
+
it("returns 50 when all prices are the same (no movement)", () => {
|
|
51
|
+
const closes = Array.from({ length: 20 }, () => 100);
|
|
52
|
+
const rsi = calculateRSI(closes, 14);
|
|
53
|
+
// avgGain=0, avgLoss=0 => RSI=50 by convention
|
|
54
|
+
expect(rsi).toBe(50);
|
|
55
|
+
});
|
|
56
|
+
it("returns ~50 when gains and losses are perfectly balanced", () => {
|
|
57
|
+
// Alternating +1, -1 with enough data for Wilder smoothing to converge
|
|
58
|
+
const closes = [100];
|
|
59
|
+
for (let i = 1; i <= 100; i++) {
|
|
60
|
+
closes.push(closes[i - 1] + (i % 2 === 1 ? 1 : -1));
|
|
61
|
+
}
|
|
62
|
+
// With Wilder smoothing, alternating equal gains/losses converges toward 50
|
|
63
|
+
const rsi = calculateRSI(closes, 14);
|
|
64
|
+
expect(rsi).toBeGreaterThan(45);
|
|
65
|
+
expect(rsi).toBeLessThan(55);
|
|
66
|
+
});
|
|
67
|
+
it("computes correct RSI for a known sequence (textbook example)", () => {
|
|
68
|
+
// Classic textbook: 14-period RSI example
|
|
69
|
+
// Prices chosen so that first 14 deltas have known gains/losses
|
|
70
|
+
const closes = [
|
|
71
|
+
44.34, 44.09, 44.15, 43.61, 44.33,
|
|
72
|
+
44.83, 45.10, 45.42, 45.84, 46.08,
|
|
73
|
+
45.89, 46.03, 45.61, 46.28, 46.28,
|
|
74
|
+
46.00, 46.03, 46.41, 46.22, 46.21,
|
|
75
|
+
];
|
|
76
|
+
// This is a well-known RSI example; 14 deltas from 20 prices
|
|
77
|
+
// First 14 deltas: -0.25, 0.06, -0.54, 0.72, 0.50, 0.27, 0.32, 0.42, 0.24, -0.19, 0.14, -0.42, 0.67, 0.00
|
|
78
|
+
// Gains: 0, 0.06, 0, 0.72, 0.50, 0.27, 0.32, 0.42, 0.24, 0, 0.14, 0, 0.67, 0 = 3.34, avg = 0.2386
|
|
79
|
+
// Losses: 0.25, 0, 0.54, 0, 0, 0, 0, 0, 0, 0.19, 0, 0.42, 0, 0 = 1.40, avg = 0.1000
|
|
80
|
+
// After Wilder smoothing for remaining 5 deltas...
|
|
81
|
+
// We just verify it's in a reasonable range (60-80 for this bullish data)
|
|
82
|
+
const rsi = calculateRSI(closes, 14);
|
|
83
|
+
expect(rsi).toBeGreaterThan(55);
|
|
84
|
+
expect(rsi).toBeLessThan(80);
|
|
85
|
+
});
|
|
86
|
+
it("works with period=7 (shorter period)", () => {
|
|
87
|
+
// 8 prices needed minimum for period=7
|
|
88
|
+
const closes = [100, 102, 101, 103, 105, 104, 106, 108];
|
|
89
|
+
const rsi = calculateRSI(closes, 7);
|
|
90
|
+
// Mostly up, should be above 50
|
|
91
|
+
expect(rsi).toBeGreaterThan(50);
|
|
92
|
+
expect(rsi).toBeLessThanOrEqual(100);
|
|
93
|
+
});
|
|
94
|
+
it("handles exactly period+1 prices (minimum required)", () => {
|
|
95
|
+
// Exactly 15 prices for period=14
|
|
96
|
+
const closes = Array.from({ length: 15 }, (_, i) => 100 + i * 0.5);
|
|
97
|
+
const rsi = calculateRSI(closes, 14);
|
|
98
|
+
// All ups => RSI = 100
|
|
99
|
+
expect(rsi).toBe(100);
|
|
100
|
+
});
|
|
101
|
+
it("handles large datasets correctly", () => {
|
|
102
|
+
// 200 prices with upward trend + noise
|
|
103
|
+
const closes = [1000];
|
|
104
|
+
for (let i = 1; i < 200; i++) {
|
|
105
|
+
// Trend up ~0.5 with noise +-2
|
|
106
|
+
closes.push(closes[i - 1] + 0.5 + (Math.sin(i) * 2));
|
|
107
|
+
}
|
|
108
|
+
const rsi = calculateRSI(closes, 14);
|
|
109
|
+
expect(rsi).toBeGreaterThan(0);
|
|
110
|
+
expect(rsi).toBeLessThan(100);
|
|
111
|
+
expect(Number.isFinite(rsi)).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
it("RSI increases when more gains are added to the series", () => {
|
|
114
|
+
// Base: mixed data
|
|
115
|
+
const base = [100, 99, 101, 98, 102, 97, 103, 96, 104, 95, 105, 94, 106, 93, 107];
|
|
116
|
+
const rsiBase = calculateRSI(base, 14);
|
|
117
|
+
// Extend with strong gains
|
|
118
|
+
const bullish = [...base, 110, 115, 120, 125, 130];
|
|
119
|
+
const rsiBullish = calculateRSI(bullish, 14);
|
|
120
|
+
expect(rsiBullish).toBeGreaterThan(rsiBase);
|
|
121
|
+
});
|
|
122
|
+
it("RSI decreases when more losses are added to the series", () => {
|
|
123
|
+
const base = [100, 99, 101, 98, 102, 97, 103, 96, 104, 95, 105, 94, 106, 93, 107];
|
|
124
|
+
const rsiBase = calculateRSI(base, 14);
|
|
125
|
+
// Extend with strong losses
|
|
126
|
+
const bearish = [...base, 102, 97, 92, 87, 82];
|
|
127
|
+
const rsiBearish = calculateRSI(bearish, 14);
|
|
128
|
+
expect(rsiBearish).toBeLessThan(rsiBase);
|
|
129
|
+
});
|
|
130
|
+
it("returns value in [0, 100] range for random data", () => {
|
|
131
|
+
// Generate random-walk prices
|
|
132
|
+
const closes = [100];
|
|
133
|
+
for (let i = 1; i < 50; i++) {
|
|
134
|
+
closes.push(closes[i - 1] + (Math.random() - 0.5) * 4);
|
|
135
|
+
}
|
|
136
|
+
const rsi = calculateRSI(closes, 14);
|
|
137
|
+
expect(rsi).toBeGreaterThanOrEqual(0);
|
|
138
|
+
expect(rsi).toBeLessThanOrEqual(100);
|
|
139
|
+
});
|
|
140
|
+
it("default period is 14", () => {
|
|
141
|
+
const closes = Array.from({ length: 20 }, (_, i) => 100 + i);
|
|
142
|
+
const rsiDefault = calculateRSI(closes);
|
|
143
|
+
const rsi14 = calculateRSI(closes, 14);
|
|
144
|
+
expect(rsiDefault).toBe(rsi14);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
// ═══════════════════════════════════════════════════
|
|
148
|
+
// RSI with real-world-like price data
|
|
149
|
+
// ═══════════════════════════════════════════════════
|
|
150
|
+
describe("calculateRSI with realistic price data", () => {
|
|
151
|
+
it("produces overbought signal (>70) in strong uptrend", () => {
|
|
152
|
+
// Simulate strong uptrend: ETH going from 2000 to 2400 over 30 candles
|
|
153
|
+
const closes = [];
|
|
154
|
+
for (let i = 0; i < 30; i++) {
|
|
155
|
+
// Strong consistent uptrend with small pullbacks
|
|
156
|
+
closes.push(2000 + i * 14 - (i % 3 === 0 ? 5 : 0));
|
|
157
|
+
}
|
|
158
|
+
const rsi = calculateRSI(closes, 14);
|
|
159
|
+
expect(rsi).toBeGreaterThan(70);
|
|
160
|
+
});
|
|
161
|
+
it("produces oversold signal (<30) in strong downtrend", () => {
|
|
162
|
+
// Simulate strong downtrend: ETH going from 2400 to 2000 over 30 candles
|
|
163
|
+
const closes = [];
|
|
164
|
+
for (let i = 0; i < 30; i++) {
|
|
165
|
+
closes.push(2400 - i * 14 + (i % 3 === 0 ? 5 : 0));
|
|
166
|
+
}
|
|
167
|
+
const rsi = calculateRSI(closes, 14);
|
|
168
|
+
expect(rsi).toBeLessThan(30);
|
|
169
|
+
});
|
|
170
|
+
it("hovers around 50 in choppy/sideways market", () => {
|
|
171
|
+
// Simulate choppy market: oscillating around 2000
|
|
172
|
+
const closes = [];
|
|
173
|
+
for (let i = 0; i < 40; i++) {
|
|
174
|
+
closes.push(2000 + Math.sin(i * 0.8) * 20);
|
|
175
|
+
}
|
|
176
|
+
const rsi = calculateRSI(closes, 14);
|
|
177
|
+
expect(rsi).toBeGreaterThan(30);
|
|
178
|
+
expect(rsi).toBeLessThan(70);
|
|
179
|
+
});
|
|
180
|
+
it("responds to trend reversal", () => {
|
|
181
|
+
// Downtrend then reversal
|
|
182
|
+
const closes = [];
|
|
183
|
+
// 20 candles of downtrend
|
|
184
|
+
for (let i = 0; i < 20; i++) {
|
|
185
|
+
closes.push(2400 - i * 10);
|
|
186
|
+
}
|
|
187
|
+
const rsiBeforeReversal = calculateRSI(closes, 14);
|
|
188
|
+
// 10 candles of uptrend (reversal)
|
|
189
|
+
for (let i = 0; i < 10; i++) {
|
|
190
|
+
closes.push(2200 + i * 15);
|
|
191
|
+
}
|
|
192
|
+
const rsiAfterReversal = calculateRSI(closes, 14);
|
|
193
|
+
expect(rsiAfterReversal).toBeGreaterThan(rsiBeforeReversal);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
// ═══════════════════════════════════════════════════
|
|
197
|
+
// evaluateCondition tests for RSI
|
|
198
|
+
// ═══════════════════════════════════════════════════
|
|
199
|
+
describe("evaluateCondition — rsi_above", () => {
|
|
200
|
+
it("returns true when RSI exceeds the threshold", () => {
|
|
201
|
+
const snapshot = makeSnapshot({ rsi: 75 });
|
|
202
|
+
const cond = { type: "rsi_above", value: 70 };
|
|
203
|
+
expect(evaluateCondition(cond, snapshot, defaultContext)).toBe(true);
|
|
204
|
+
});
|
|
205
|
+
it("returns false when RSI is below the threshold", () => {
|
|
206
|
+
const snapshot = makeSnapshot({ rsi: 65 });
|
|
207
|
+
const cond = { type: "rsi_above", value: 70 };
|
|
208
|
+
expect(evaluateCondition(cond, snapshot, defaultContext)).toBe(false);
|
|
209
|
+
});
|
|
210
|
+
it("returns false when RSI equals the threshold (not strictly above)", () => {
|
|
211
|
+
const snapshot = makeSnapshot({ rsi: 70 });
|
|
212
|
+
const cond = { type: "rsi_above", value: 70 };
|
|
213
|
+
expect(evaluateCondition(cond, snapshot, defaultContext)).toBe(false);
|
|
214
|
+
});
|
|
215
|
+
it("returns false when RSI is NaN (insufficient data)", () => {
|
|
216
|
+
const snapshot = makeSnapshot({ rsi: NaN });
|
|
217
|
+
const cond = { type: "rsi_above", value: 30 };
|
|
218
|
+
expect(evaluateCondition(cond, snapshot, defaultContext)).toBe(false);
|
|
219
|
+
});
|
|
220
|
+
it("accepts string value and parses it", () => {
|
|
221
|
+
const snapshot = makeSnapshot({ rsi: 80 });
|
|
222
|
+
const cond = { type: "rsi_above", value: "70" };
|
|
223
|
+
expect(evaluateCondition(cond, snapshot, defaultContext)).toBe(true);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
describe("evaluateCondition — rsi_below", () => {
|
|
227
|
+
it("returns true when RSI is below the threshold", () => {
|
|
228
|
+
const snapshot = makeSnapshot({ rsi: 25 });
|
|
229
|
+
const cond = { type: "rsi_below", value: 30 };
|
|
230
|
+
expect(evaluateCondition(cond, snapshot, defaultContext)).toBe(true);
|
|
231
|
+
});
|
|
232
|
+
it("returns false when RSI is above the threshold", () => {
|
|
233
|
+
const snapshot = makeSnapshot({ rsi: 45 });
|
|
234
|
+
const cond = { type: "rsi_below", value: 30 };
|
|
235
|
+
expect(evaluateCondition(cond, snapshot, defaultContext)).toBe(false);
|
|
236
|
+
});
|
|
237
|
+
it("returns false when RSI equals the threshold", () => {
|
|
238
|
+
const snapshot = makeSnapshot({ rsi: 30 });
|
|
239
|
+
const cond = { type: "rsi_below", value: 30 };
|
|
240
|
+
expect(evaluateCondition(cond, snapshot, defaultContext)).toBe(false);
|
|
241
|
+
});
|
|
242
|
+
it("returns false when RSI is NaN", () => {
|
|
243
|
+
const snapshot = makeSnapshot({ rsi: NaN });
|
|
244
|
+
const cond = { type: "rsi_below", value: 70 };
|
|
245
|
+
expect(evaluateCondition(cond, snapshot, defaultContext)).toBe(false);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
// ═══════════════════════════════════════════════════
|
|
249
|
+
// evaluateCondition tests for spread_above
|
|
250
|
+
// ═══════════════════════════════════════════════════
|
|
251
|
+
describe("evaluateCondition — spread_above", () => {
|
|
252
|
+
it("returns true when spread exceeds threshold", () => {
|
|
253
|
+
const snapshot = makeSnapshot({ spreadPct: 0.15 });
|
|
254
|
+
const cond = { type: "spread_above", value: 0.1 };
|
|
255
|
+
expect(evaluateCondition(cond, snapshot, defaultContext)).toBe(true);
|
|
256
|
+
});
|
|
257
|
+
it("returns false when spread is below threshold", () => {
|
|
258
|
+
const snapshot = makeSnapshot({ spreadPct: 0.03 });
|
|
259
|
+
const cond = { type: "spread_above", value: 0.1 };
|
|
260
|
+
expect(evaluateCondition(cond, snapshot, defaultContext)).toBe(false);
|
|
261
|
+
});
|
|
262
|
+
it("returns false when spread equals threshold", () => {
|
|
263
|
+
const snapshot = makeSnapshot({ spreadPct: 0.1 });
|
|
264
|
+
const cond = { type: "spread_above", value: 0.1 };
|
|
265
|
+
expect(evaluateCondition(cond, snapshot, defaultContext)).toBe(false);
|
|
266
|
+
});
|
|
267
|
+
it("returns false when spread is zero (tight book)", () => {
|
|
268
|
+
const snapshot = makeSnapshot({ spreadPct: 0 });
|
|
269
|
+
const cond = { type: "spread_above", value: 0.01 };
|
|
270
|
+
expect(evaluateCondition(cond, snapshot, defaultContext)).toBe(false);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
// ═══════════════════════════════════════════════════
|
|
274
|
+
// evaluateAllConditions with RSI
|
|
275
|
+
// ═══════════════════════════════════════════════════
|
|
276
|
+
describe("evaluateAllConditions with RSI conditions", () => {
|
|
277
|
+
it("combines RSI with price condition in 'all' mode", () => {
|
|
278
|
+
const snapshot = makeSnapshot({ price: 110, rsi: 75 });
|
|
279
|
+
const conditions = [
|
|
280
|
+
{ type: "price_above", value: 100 },
|
|
281
|
+
{ type: "rsi_above", value: 70 },
|
|
282
|
+
];
|
|
283
|
+
expect(evaluateAllConditions(conditions, snapshot, defaultContext, "all")).toBe(true);
|
|
284
|
+
});
|
|
285
|
+
it("fails 'all' mode when RSI condition not met", () => {
|
|
286
|
+
const snapshot = makeSnapshot({ price: 110, rsi: 65 });
|
|
287
|
+
const conditions = [
|
|
288
|
+
{ type: "price_above", value: 100 },
|
|
289
|
+
{ type: "rsi_above", value: 70 },
|
|
290
|
+
];
|
|
291
|
+
expect(evaluateAllConditions(conditions, snapshot, defaultContext, "all")).toBe(false);
|
|
292
|
+
});
|
|
293
|
+
it("passes 'any' mode when only RSI condition is met", () => {
|
|
294
|
+
const snapshot = makeSnapshot({ price: 90, rsi: 25 });
|
|
295
|
+
const conditions = [
|
|
296
|
+
{ type: "price_above", value: 100 },
|
|
297
|
+
{ type: "rsi_below", value: 30 },
|
|
298
|
+
];
|
|
299
|
+
expect(evaluateAllConditions(conditions, snapshot, defaultContext, "any")).toBe(true);
|
|
300
|
+
});
|
|
301
|
+
it("combines spread_above with rsi_below for entry signal", () => {
|
|
302
|
+
// Wide spread + oversold RSI = potential entry
|
|
303
|
+
const snapshot = makeSnapshot({ spreadPct: 0.5, rsi: 22 });
|
|
304
|
+
const conditions = [
|
|
305
|
+
{ type: "spread_above", value: 0.3 },
|
|
306
|
+
{ type: "rsi_below", value: 30 },
|
|
307
|
+
];
|
|
308
|
+
expect(evaluateAllConditions(conditions, snapshot, defaultContext, "all")).toBe(true);
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
// ═══════════════════════════════════════════════════
|
|
312
|
+
// Existing conditions still work (regression)
|
|
313
|
+
// ═══════════════════════════════════════════════════
|
|
314
|
+
describe("evaluateCondition — regression for existing conditions", () => {
|
|
315
|
+
it("always returns true", () => {
|
|
316
|
+
const cond = { type: "always", value: 0 };
|
|
317
|
+
expect(evaluateCondition(cond, makeSnapshot(), defaultContext)).toBe(true);
|
|
318
|
+
});
|
|
319
|
+
it("price_above works", () => {
|
|
320
|
+
const snapshot = makeSnapshot({ price: 110 });
|
|
321
|
+
expect(evaluateCondition({ type: "price_above", value: 100 }, snapshot, defaultContext)).toBe(true);
|
|
322
|
+
expect(evaluateCondition({ type: "price_above", value: 120 }, snapshot, defaultContext)).toBe(false);
|
|
323
|
+
});
|
|
324
|
+
it("price_below works", () => {
|
|
325
|
+
const snapshot = makeSnapshot({ price: 90 });
|
|
326
|
+
expect(evaluateCondition({ type: "price_below", value: 100 }, snapshot, defaultContext)).toBe(true);
|
|
327
|
+
expect(evaluateCondition({ type: "price_below", value: 80 }, snapshot, defaultContext)).toBe(false);
|
|
328
|
+
});
|
|
329
|
+
it("volatility_above works", () => {
|
|
330
|
+
const snapshot = makeSnapshot({ volatility24h: 15 });
|
|
331
|
+
expect(evaluateCondition({ type: "volatility_above", value: 10 }, snapshot, defaultContext)).toBe(true);
|
|
332
|
+
});
|
|
333
|
+
it("funding_rate_above works", () => {
|
|
334
|
+
const snapshot = makeSnapshot({ fundingRate: 0.001 });
|
|
335
|
+
expect(evaluateCondition({ type: "funding_rate_above", value: 0.0005 }, snapshot, defaultContext)).toBe(true);
|
|
336
|
+
});
|
|
337
|
+
it("balance_above works", () => {
|
|
338
|
+
const ctx = { ...defaultContext, equity: 15000 };
|
|
339
|
+
expect(evaluateCondition({ type: "balance_above", value: 10000 }, makeSnapshot(), ctx)).toBe(true);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|