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,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock adapter for unit testing. Records all calls and returns configurable responses.
|
|
3
|
+
*/
|
|
4
|
+
export class MockAdapter {
|
|
5
|
+
name;
|
|
6
|
+
calls = [];
|
|
7
|
+
// Configurable return values
|
|
8
|
+
marketsResponse = [];
|
|
9
|
+
orderbookResponse = { bids: [], asks: [] };
|
|
10
|
+
balanceResponse = { equity: "1000", available: "800", marginUsed: "200", unrealizedPnl: "50" };
|
|
11
|
+
positionsResponse = [];
|
|
12
|
+
ordersResponse = [];
|
|
13
|
+
orderResult = { status: "ok", orderId: "12345" };
|
|
14
|
+
constructor(name = "mock") {
|
|
15
|
+
this.name = name;
|
|
16
|
+
}
|
|
17
|
+
record(method, args) {
|
|
18
|
+
this.calls.push({ method, args });
|
|
19
|
+
}
|
|
20
|
+
getCallsFor(method) {
|
|
21
|
+
return this.calls.filter((c) => c.method === method);
|
|
22
|
+
}
|
|
23
|
+
reset() {
|
|
24
|
+
this.calls = [];
|
|
25
|
+
}
|
|
26
|
+
async getMarkets() {
|
|
27
|
+
this.record("getMarkets", []);
|
|
28
|
+
return this.marketsResponse;
|
|
29
|
+
}
|
|
30
|
+
async getOrderbook(symbol) {
|
|
31
|
+
this.record("getOrderbook", [symbol]);
|
|
32
|
+
return this.orderbookResponse;
|
|
33
|
+
}
|
|
34
|
+
async getBalance() {
|
|
35
|
+
this.record("getBalance", []);
|
|
36
|
+
return this.balanceResponse;
|
|
37
|
+
}
|
|
38
|
+
async getPositions() {
|
|
39
|
+
this.record("getPositions", []);
|
|
40
|
+
return this.positionsResponse;
|
|
41
|
+
}
|
|
42
|
+
async getOpenOrders() {
|
|
43
|
+
this.record("getOpenOrders", []);
|
|
44
|
+
return this.ordersResponse;
|
|
45
|
+
}
|
|
46
|
+
async marketOrder(symbol, side, size) {
|
|
47
|
+
this.record("marketOrder", [symbol, side, size]);
|
|
48
|
+
return this.orderResult;
|
|
49
|
+
}
|
|
50
|
+
async limitOrder(symbol, side, price, size) {
|
|
51
|
+
this.record("limitOrder", [symbol, side, price, size]);
|
|
52
|
+
return this.orderResult;
|
|
53
|
+
}
|
|
54
|
+
async editOrder(symbol, orderId, price, size) {
|
|
55
|
+
this.record("editOrder", [symbol, orderId, price, size]);
|
|
56
|
+
return this.orderResult;
|
|
57
|
+
}
|
|
58
|
+
async cancelOrder(symbol, orderId) {
|
|
59
|
+
this.record("cancelOrder", [symbol, orderId]);
|
|
60
|
+
return { status: "cancelled" };
|
|
61
|
+
}
|
|
62
|
+
async cancelAllOrders(symbol) {
|
|
63
|
+
this.record("cancelAllOrders", [symbol]);
|
|
64
|
+
return { status: "all_cancelled" };
|
|
65
|
+
}
|
|
66
|
+
async setLeverage(symbol, leverage, marginMode) {
|
|
67
|
+
this.record("setLeverage", [symbol, leverage, marginMode]);
|
|
68
|
+
return { symbol, leverage, marginMode };
|
|
69
|
+
}
|
|
70
|
+
async stopOrder(symbol, side, size, triggerPrice, opts) {
|
|
71
|
+
this.record("stopOrder", [symbol, side, size, triggerPrice, opts]);
|
|
72
|
+
return this.orderResult;
|
|
73
|
+
}
|
|
74
|
+
async getRecentTrades(symbol, limit) {
|
|
75
|
+
this.record("getRecentTrades", [symbol, limit]);
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
async getFundingHistory(symbol, limit) {
|
|
79
|
+
this.record("getFundingHistory", [symbol, limit]);
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
async getKlines(symbol, interval, startTime, endTime) {
|
|
83
|
+
this.record("getKlines", [symbol, interval, startTime, endTime]);
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
async getOrderHistory(limit) {
|
|
87
|
+
this.record("getOrderHistory", [limit]);
|
|
88
|
+
return this.ordersResponse;
|
|
89
|
+
}
|
|
90
|
+
async getTradeHistory(limit) {
|
|
91
|
+
this.record("getTradeHistory", [limit]);
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
async getFundingPayments(limit) {
|
|
95
|
+
this.record("getFundingPayments", [limit]);
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Factory to create mock markets data */
|
|
100
|
+
export function createMockMarkets(count = 3) {
|
|
101
|
+
const symbols = ["BTC", "ETH", "SOL", "DOGE", "ARB"];
|
|
102
|
+
return symbols.slice(0, count).map((symbol) => ({
|
|
103
|
+
symbol,
|
|
104
|
+
markPrice: String(symbol === "BTC" ? 100000 : symbol === "ETH" ? 3500 : 150),
|
|
105
|
+
indexPrice: String(symbol === "BTC" ? 99990 : symbol === "ETH" ? 3498 : 149.5),
|
|
106
|
+
fundingRate: "0.0001",
|
|
107
|
+
volume24h: "1000000",
|
|
108
|
+
openInterest: "500000",
|
|
109
|
+
maxLeverage: symbol === "BTC" ? 100 : 50,
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
/** Factory to create mock positions */
|
|
113
|
+
export function createMockPositions(count = 1) {
|
|
114
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
115
|
+
symbol: i === 0 ? "BTC" : "ETH",
|
|
116
|
+
side: (i % 2 === 0 ? "long" : "short"),
|
|
117
|
+
size: "0.1",
|
|
118
|
+
entryPrice: "100000",
|
|
119
|
+
markPrice: "101000",
|
|
120
|
+
liquidationPrice: "90000",
|
|
121
|
+
unrealizedPnl: "100",
|
|
122
|
+
leverage: 10,
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
/** Factory to create mock orders */
|
|
126
|
+
export function createMockOrders(count = 2) {
|
|
127
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
128
|
+
orderId: String(1000 + i),
|
|
129
|
+
symbol: "BTC",
|
|
130
|
+
side: (i % 2 === 0 ? "buy" : "sell"),
|
|
131
|
+
price: String(99000 + i * 1000),
|
|
132
|
+
size: "0.05",
|
|
133
|
+
filled: "0",
|
|
134
|
+
status: "open",
|
|
135
|
+
type: "limit",
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { logExecution, readExecutionLog, getExecutionStats } from "../execution-log.js";
|
|
3
|
+
import { existsSync, unlinkSync, readFileSync, writeFileSync, renameSync } from "fs";
|
|
4
|
+
import { resolve } from "path";
|
|
5
|
+
const PERP_DIR = resolve(process.env.HOME || "~", ".perp");
|
|
6
|
+
const LOG_FILE = resolve(PERP_DIR, "executions.jsonl");
|
|
7
|
+
const BACKUP_FILE = resolve(PERP_DIR, "executions.jsonl.test-backup");
|
|
8
|
+
describe("Execution Log", () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
// Backup existing log if present
|
|
11
|
+
if (existsSync(LOG_FILE)) {
|
|
12
|
+
const content = readFileSync(LOG_FILE, "utf-8");
|
|
13
|
+
writeFileSync(BACKUP_FILE, content);
|
|
14
|
+
}
|
|
15
|
+
// Clear log for test
|
|
16
|
+
if (existsSync(LOG_FILE))
|
|
17
|
+
unlinkSync(LOG_FILE);
|
|
18
|
+
});
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
// Restore original log
|
|
21
|
+
if (existsSync(LOG_FILE))
|
|
22
|
+
unlinkSync(LOG_FILE);
|
|
23
|
+
if (existsSync(BACKUP_FILE)) {
|
|
24
|
+
renameSync(BACKUP_FILE, LOG_FILE);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
it("should log an execution record", () => {
|
|
28
|
+
const record = logExecution({
|
|
29
|
+
type: "market_order",
|
|
30
|
+
exchange: "hyperliquid",
|
|
31
|
+
symbol: "BTC",
|
|
32
|
+
side: "buy",
|
|
33
|
+
size: "0.1",
|
|
34
|
+
price: "100000",
|
|
35
|
+
notional: 10000,
|
|
36
|
+
status: "success",
|
|
37
|
+
dryRun: false,
|
|
38
|
+
});
|
|
39
|
+
expect(record.id).toBeTruthy();
|
|
40
|
+
expect(record.timestamp).toBeTruthy();
|
|
41
|
+
expect(record.exchange).toBe("hyperliquid");
|
|
42
|
+
expect(record.symbol).toBe("BTC");
|
|
43
|
+
});
|
|
44
|
+
it("should read back logged records", () => {
|
|
45
|
+
logExecution({ type: "market_order", exchange: "test", symbol: "BTC", side: "buy", size: "0.1", status: "success", dryRun: false });
|
|
46
|
+
logExecution({ type: "limit_order", exchange: "test", symbol: "ETH", side: "sell", size: "1.0", status: "success", dryRun: false });
|
|
47
|
+
const records = readExecutionLog();
|
|
48
|
+
expect(records).toHaveLength(2);
|
|
49
|
+
});
|
|
50
|
+
it("should filter by exchange", () => {
|
|
51
|
+
logExecution({ type: "market_order", exchange: "hl", symbol: "BTC", side: "buy", size: "0.1", status: "success", dryRun: false });
|
|
52
|
+
logExecution({ type: "market_order", exchange: "pac", symbol: "BTC", side: "buy", size: "0.1", status: "success", dryRun: false });
|
|
53
|
+
const records = readExecutionLog({ exchange: "hl" });
|
|
54
|
+
expect(records).toHaveLength(1);
|
|
55
|
+
expect(records[0].exchange).toBe("hl");
|
|
56
|
+
});
|
|
57
|
+
it("should filter by symbol", () => {
|
|
58
|
+
logExecution({ type: "market_order", exchange: "test", symbol: "BTC", side: "buy", size: "0.1", status: "success", dryRun: false });
|
|
59
|
+
logExecution({ type: "market_order", exchange: "test", symbol: "ETH", side: "sell", size: "1.0", status: "success", dryRun: false });
|
|
60
|
+
const records = readExecutionLog({ symbol: "ETH" });
|
|
61
|
+
expect(records).toHaveLength(1);
|
|
62
|
+
expect(records[0].symbol).toBe("ETH");
|
|
63
|
+
});
|
|
64
|
+
it("should mark dry-run executions", () => {
|
|
65
|
+
logExecution({ type: "market_order", exchange: "test", symbol: "BTC", side: "buy", size: "0.1", status: "simulated", dryRun: true });
|
|
66
|
+
logExecution({ type: "market_order", exchange: "test", symbol: "ETH", side: "sell", size: "1.0", status: "success", dryRun: false });
|
|
67
|
+
const dryRuns = readExecutionLog({ dryRunOnly: true });
|
|
68
|
+
expect(dryRuns).toHaveLength(1);
|
|
69
|
+
expect(dryRuns[0].dryRun).toBe(true);
|
|
70
|
+
expect(dryRuns[0].status).toBe("simulated");
|
|
71
|
+
});
|
|
72
|
+
it("should compute execution stats", () => {
|
|
73
|
+
logExecution({ type: "market_order", exchange: "hl", symbol: "BTC", side: "buy", size: "0.1", status: "success", dryRun: false });
|
|
74
|
+
logExecution({ type: "limit_order", exchange: "pac", symbol: "ETH", side: "sell", size: "1.0", status: "success", dryRun: false });
|
|
75
|
+
logExecution({ type: "market_order", exchange: "hl", symbol: "SOL", side: "buy", size: "10", status: "failed", error: "insufficient balance", dryRun: false });
|
|
76
|
+
const stats = getExecutionStats();
|
|
77
|
+
expect(stats.totalTrades).toBe(3);
|
|
78
|
+
expect(stats.successRate).toBeCloseTo(66.67, 0);
|
|
79
|
+
expect(stats.byExchange.hl).toBe(2);
|
|
80
|
+
expect(stats.byExchange.pac).toBe(1);
|
|
81
|
+
expect(stats.byType.market_order).toBe(2);
|
|
82
|
+
expect(stats.byType.limit_order).toBe(1);
|
|
83
|
+
expect(stats.recentErrors).toHaveLength(1);
|
|
84
|
+
});
|
|
85
|
+
it("should return empty stats when no log file", () => {
|
|
86
|
+
const stats = getExecutionStats();
|
|
87
|
+
expect(stats.totalTrades).toBe(0);
|
|
88
|
+
expect(stats.successRate).toBe(0);
|
|
89
|
+
});
|
|
90
|
+
it("should limit results", () => {
|
|
91
|
+
for (let i = 0; i < 10; i++) {
|
|
92
|
+
logExecution({ type: "market_order", exchange: "test", symbol: "BTC", side: "buy", size: "0.1", status: "success", dryRun: false });
|
|
93
|
+
}
|
|
94
|
+
const records = readExecutionLog({ limit: 3 });
|
|
95
|
+
expect(records).toHaveLength(3);
|
|
96
|
+
});
|
|
97
|
+
it("should sort newest first", async () => {
|
|
98
|
+
logExecution({ type: "market_order", exchange: "test", symbol: "FIRST", side: "buy", size: "0.1", status: "success", dryRun: false });
|
|
99
|
+
// Ensure distinct timestamps (Date.now resolution is 1ms)
|
|
100
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
101
|
+
logExecution({ type: "market_order", exchange: "test", symbol: "SECOND", side: "buy", size: "0.1", status: "success", dryRun: false });
|
|
102
|
+
const records = readExecutionLog();
|
|
103
|
+
expect(records[0].symbol).toBe("SECOND");
|
|
104
|
+
expect(records[1].symbol).toBe("FIRST");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { annualizeRate, computeAnnualSpread, toHourlyRate, estimateHourlyFunding } from "../funding.js";
|
|
3
|
+
describe("Funding Rate Normalization", () => {
|
|
4
|
+
describe("toHourlyRate", () => {
|
|
5
|
+
it("should return HL rate as-is (already hourly)", () => {
|
|
6
|
+
expect(toHourlyRate(0.001, "hyperliquid")).toBeCloseTo(0.001);
|
|
7
|
+
});
|
|
8
|
+
it("should return Pacifica rate as-is (already hourly)", () => {
|
|
9
|
+
expect(toHourlyRate(0.001, "pacifica")).toBeCloseTo(0.001);
|
|
10
|
+
});
|
|
11
|
+
it("should return Lighter rate as-is (already hourly)", () => {
|
|
12
|
+
expect(toHourlyRate(0.001, "lighter")).toBeCloseTo(0.001);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
describe("annualizeRate", () => {
|
|
16
|
+
it("should annualize HL hourly rate correctly", () => {
|
|
17
|
+
// 0.01% per hour * 8760 hours = 87.6%
|
|
18
|
+
const result = annualizeRate(0.0001, "hyperliquid");
|
|
19
|
+
expect(result).toBeCloseTo(87.6, 0);
|
|
20
|
+
});
|
|
21
|
+
it("should annualize Pacifica hourly rate correctly", () => {
|
|
22
|
+
// 0.01% per hour * 8760 = 87.6%
|
|
23
|
+
const result = annualizeRate(0.0001, "pacifica");
|
|
24
|
+
expect(result).toBeCloseTo(87.6, 0);
|
|
25
|
+
});
|
|
26
|
+
it("should handle zero rate", () => {
|
|
27
|
+
expect(annualizeRate(0, "hyperliquid")).toBe(0);
|
|
28
|
+
});
|
|
29
|
+
it("should handle negative rates", () => {
|
|
30
|
+
expect(annualizeRate(-0.0001, "hyperliquid")).toBeLessThan(0);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe("computeAnnualSpread", () => {
|
|
34
|
+
it("should compute spread between different exchanges", () => {
|
|
35
|
+
// HL: 0.01% hourly, PAC: 0.001% hourly
|
|
36
|
+
// Spread = |0.0001 - 0.00001| * 8760 * 100 = ~78.84%
|
|
37
|
+
const spread = computeAnnualSpread(0.0001, "hyperliquid", 0.00001, "pacifica");
|
|
38
|
+
expect(spread).toBeGreaterThan(0);
|
|
39
|
+
});
|
|
40
|
+
it("should return positive spread when rates are ordered correctly", () => {
|
|
41
|
+
const spread = computeAnnualSpread(0.001, "hyperliquid", 0.0001, "hyperliquid");
|
|
42
|
+
expect(spread).toBeGreaterThan(0);
|
|
43
|
+
});
|
|
44
|
+
it("should handle same exchange same rate (zero spread)", () => {
|
|
45
|
+
const spread = computeAnnualSpread(0.0001, "hyperliquid", 0.0001, "hyperliquid");
|
|
46
|
+
expect(spread).toBeCloseTo(0, 1);
|
|
47
|
+
});
|
|
48
|
+
it("should compute zero spread for identical hourly rates", () => {
|
|
49
|
+
// Same hourly rate on both exchanges
|
|
50
|
+
const spread = computeAnnualSpread(0.001, "hyperliquid", 0.001, "pacifica");
|
|
51
|
+
expect(Math.abs(spread)).toBeLessThan(1); // should be ~0
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe("estimateHourlyFunding", () => {
|
|
55
|
+
it("should estimate funding cost for long position (positive rate = longs pay)", () => {
|
|
56
|
+
// Positive rate, long position = pay funding (positive return means you pay)
|
|
57
|
+
const result = estimateHourlyFunding(0.0001, "hyperliquid", 10000, "long");
|
|
58
|
+
expect(result).toBeGreaterThan(0); // longs pay when rate is positive
|
|
59
|
+
});
|
|
60
|
+
it("should estimate funding income for short position (positive rate = shorts receive)", () => {
|
|
61
|
+
// Positive rate, short position = receive funding (negative return means you receive)
|
|
62
|
+
const result = estimateHourlyFunding(0.0001, "hyperliquid", 10000, "short");
|
|
63
|
+
expect(result).toBeLessThan(0); // shorts receive when rate is positive
|
|
64
|
+
});
|
|
65
|
+
it("should scale with position size", () => {
|
|
66
|
+
const small = estimateHourlyFunding(0.0001, "hyperliquid", 1000, "long");
|
|
67
|
+
const big = estimateHourlyFunding(0.0001, "hyperliquid", 10000, "long");
|
|
68
|
+
expect(Math.abs(big)).toBeCloseTo(Math.abs(small) * 10, 2);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
// Use vi.hoisted so TEST_DIR is available when vi.mock runs (hoisted)
|
|
5
|
+
const { TEST_DIR } = vi.hoisted(() => {
|
|
6
|
+
const { tmpdir } = require("node:os");
|
|
7
|
+
const { join } = require("node:path");
|
|
8
|
+
return {
|
|
9
|
+
TEST_DIR: join(tmpdir(), `perp-funding-test-${Date.now()}-${Math.random().toString(36).slice(2)}`),
|
|
10
|
+
};
|
|
11
|
+
});
|
|
12
|
+
// Mock the homedir to redirect storage to our test directory
|
|
13
|
+
vi.mock("node:os", async () => {
|
|
14
|
+
const actual = await vi.importActual("node:os");
|
|
15
|
+
return {
|
|
16
|
+
...actual,
|
|
17
|
+
homedir: () => join(TEST_DIR, "home"),
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
import { saveFundingSnapshot, getAvgFundingRate, getHistoricalRates, getHistoricalAverages, getCompoundedAnnualReturn, getExchangeCompoundingHours, cleanupOldFiles, _resetCleanupFlag, } from "../funding-history.js";
|
|
21
|
+
const DATA_DIR = join(TEST_DIR, "home", ".perp", "funding-rates");
|
|
22
|
+
function makeRate(overrides = {}) {
|
|
23
|
+
return {
|
|
24
|
+
exchange: "hyperliquid",
|
|
25
|
+
symbol: "BTC",
|
|
26
|
+
fundingRate: 0.0001,
|
|
27
|
+
hourlyRate: 0.0001,
|
|
28
|
+
annualizedPct: 87.6,
|
|
29
|
+
markPrice: 50000,
|
|
30
|
+
...overrides,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function readJsonl(filePath) {
|
|
34
|
+
if (!existsSync(filePath))
|
|
35
|
+
return [];
|
|
36
|
+
const content = readFileSync(filePath, "utf-8");
|
|
37
|
+
return content
|
|
38
|
+
.split("\n")
|
|
39
|
+
.filter(l => l.trim())
|
|
40
|
+
.map(l => JSON.parse(l));
|
|
41
|
+
}
|
|
42
|
+
function getMonthKey(date) {
|
|
43
|
+
const y = date.getFullYear();
|
|
44
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
45
|
+
return `${y}-${m}`;
|
|
46
|
+
}
|
|
47
|
+
describe("funding-history", () => {
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
_resetCleanupFlag();
|
|
50
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
51
|
+
});
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
try {
|
|
54
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// ignore cleanup errors
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
// ──────────────────────────────────────────────
|
|
61
|
+
// saveFundingSnapshot
|
|
62
|
+
// ──────────────────────────────────────────────
|
|
63
|
+
describe("saveFundingSnapshot", () => {
|
|
64
|
+
it("writes JSONL correctly", () => {
|
|
65
|
+
const rates = [
|
|
66
|
+
makeRate({ exchange: "hyperliquid", symbol: "BTC", fundingRate: 0.0001, hourlyRate: 0.0001 }),
|
|
67
|
+
makeRate({ exchange: "pacifica", symbol: "ETH", fundingRate: 0.0008, hourlyRate: 0.0001 }),
|
|
68
|
+
];
|
|
69
|
+
saveFundingSnapshot(rates);
|
|
70
|
+
const monthKey = getMonthKey(new Date());
|
|
71
|
+
const filePath = join(DATA_DIR, `${monthKey}.jsonl`);
|
|
72
|
+
expect(existsSync(filePath)).toBe(true);
|
|
73
|
+
const entries = readJsonl(filePath);
|
|
74
|
+
expect(entries).toHaveLength(2);
|
|
75
|
+
expect(entries[0].symbol).toBe("BTC");
|
|
76
|
+
expect(entries[0].exchange).toBe("hyperliquid");
|
|
77
|
+
expect(entries[0].rate).toBe(0.0001);
|
|
78
|
+
expect(entries[0].hourlyRate).toBe(0.0001);
|
|
79
|
+
expect(entries[0].ts).toBeDefined();
|
|
80
|
+
expect(entries[1].symbol).toBe("ETH");
|
|
81
|
+
expect(entries[1].exchange).toBe("pacifica");
|
|
82
|
+
});
|
|
83
|
+
it("deduplicates entries within 5 minutes", () => {
|
|
84
|
+
const rates = [makeRate({ symbol: "BTC", exchange: "hyperliquid" })];
|
|
85
|
+
// Save twice in quick succession
|
|
86
|
+
saveFundingSnapshot(rates);
|
|
87
|
+
saveFundingSnapshot(rates);
|
|
88
|
+
const monthKey = getMonthKey(new Date());
|
|
89
|
+
const filePath = join(DATA_DIR, `${monthKey}.jsonl`);
|
|
90
|
+
const entries = readJsonl(filePath);
|
|
91
|
+
// Should only have 1 entry due to dedup
|
|
92
|
+
expect(entries).toHaveLength(1);
|
|
93
|
+
});
|
|
94
|
+
it("allows entries after 5 minute gap", () => {
|
|
95
|
+
const rates = [makeRate({ symbol: "BTC", exchange: "hyperliquid" })];
|
|
96
|
+
// Write an entry with a timestamp 6 minutes ago
|
|
97
|
+
const monthKey = getMonthKey(new Date());
|
|
98
|
+
const filePath = join(DATA_DIR, `${monthKey}.jsonl`);
|
|
99
|
+
const oldTs = new Date(Date.now() - 6 * 60 * 1000).toISOString();
|
|
100
|
+
const oldEntry = {
|
|
101
|
+
ts: oldTs,
|
|
102
|
+
symbol: "BTC",
|
|
103
|
+
exchange: "hyperliquid",
|
|
104
|
+
rate: 0.0001,
|
|
105
|
+
hourlyRate: 0.0001,
|
|
106
|
+
};
|
|
107
|
+
writeFileSync(filePath, JSON.stringify(oldEntry) + "\n");
|
|
108
|
+
// Now save new snapshot
|
|
109
|
+
saveFundingSnapshot(rates);
|
|
110
|
+
const entries = readJsonl(filePath);
|
|
111
|
+
expect(entries).toHaveLength(2);
|
|
112
|
+
});
|
|
113
|
+
it("uppercases symbol names", () => {
|
|
114
|
+
const rates = [makeRate({ symbol: "btc" })];
|
|
115
|
+
saveFundingSnapshot(rates);
|
|
116
|
+
const monthKey = getMonthKey(new Date());
|
|
117
|
+
const filePath = join(DATA_DIR, `${monthKey}.jsonl`);
|
|
118
|
+
const entries = readJsonl(filePath);
|
|
119
|
+
expect(entries[0].symbol).toBe("BTC");
|
|
120
|
+
});
|
|
121
|
+
it("skips entries with empty symbol", () => {
|
|
122
|
+
const rates = [makeRate({ symbol: "" })];
|
|
123
|
+
saveFundingSnapshot(rates);
|
|
124
|
+
const monthKey = getMonthKey(new Date());
|
|
125
|
+
const filePath = join(DATA_DIR, `${monthKey}.jsonl`);
|
|
126
|
+
// File may or may not exist; if it does, it should be empty
|
|
127
|
+
if (existsSync(filePath)) {
|
|
128
|
+
const entries = readJsonl(filePath);
|
|
129
|
+
expect(entries).toHaveLength(0);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
// ──────────────────────────────────────────────
|
|
134
|
+
// getAvgFundingRate
|
|
135
|
+
// ──────────────────────────────────────────────
|
|
136
|
+
describe("getAvgFundingRate", () => {
|
|
137
|
+
it("returns correct average", () => {
|
|
138
|
+
const monthKey = getMonthKey(new Date());
|
|
139
|
+
const filePath = join(DATA_DIR, `${monthKey}.jsonl`);
|
|
140
|
+
// Write 3 entries with different hourly rates at recent times
|
|
141
|
+
const entries = [
|
|
142
|
+
{ ts: new Date(Date.now() - 30 * 60 * 1000).toISOString(), symbol: "BTC", exchange: "hyperliquid", rate: 0.0001, hourlyRate: 0.0001 },
|
|
143
|
+
{ ts: new Date(Date.now() - 20 * 60 * 1000).toISOString(), symbol: "BTC", exchange: "hyperliquid", rate: 0.0002, hourlyRate: 0.0002 },
|
|
144
|
+
{ ts: new Date(Date.now() - 10 * 60 * 1000).toISOString(), symbol: "BTC", exchange: "hyperliquid", rate: 0.0003, hourlyRate: 0.0003 },
|
|
145
|
+
];
|
|
146
|
+
writeFileSync(filePath, entries.map(e => JSON.stringify(e)).join("\n") + "\n");
|
|
147
|
+
const avg = getAvgFundingRate("BTC", "hyperliquid", 1);
|
|
148
|
+
expect(avg).toBeCloseTo(0.0002); // (0.0001 + 0.0002 + 0.0003) / 3
|
|
149
|
+
});
|
|
150
|
+
it("returns null when no data available", () => {
|
|
151
|
+
const avg = getAvgFundingRate("NOEXIST", "hyperliquid", 24);
|
|
152
|
+
expect(avg).toBeNull();
|
|
153
|
+
});
|
|
154
|
+
it("filters by symbol and exchange correctly", () => {
|
|
155
|
+
const monthKey = getMonthKey(new Date());
|
|
156
|
+
const filePath = join(DATA_DIR, `${monthKey}.jsonl`);
|
|
157
|
+
const entries = [
|
|
158
|
+
{ ts: new Date(Date.now() - 10 * 60 * 1000).toISOString(), symbol: "BTC", exchange: "hyperliquid", rate: 0.0001, hourlyRate: 0.0001 },
|
|
159
|
+
{ ts: new Date(Date.now() - 10 * 60 * 1000).toISOString(), symbol: "ETH", exchange: "hyperliquid", rate: 0.0005, hourlyRate: 0.0005 },
|
|
160
|
+
{ ts: new Date(Date.now() - 10 * 60 * 1000).toISOString(), symbol: "BTC", exchange: "pacifica", rate: 0.0008, hourlyRate: 0.0001 },
|
|
161
|
+
];
|
|
162
|
+
writeFileSync(filePath, entries.map(e => JSON.stringify(e)).join("\n") + "\n");
|
|
163
|
+
const avgBtcHL = getAvgFundingRate("BTC", "hyperliquid", 1);
|
|
164
|
+
expect(avgBtcHL).toBeCloseTo(0.0001);
|
|
165
|
+
const avgEthHL = getAvgFundingRate("ETH", "hyperliquid", 1);
|
|
166
|
+
expect(avgEthHL).toBeCloseTo(0.0005);
|
|
167
|
+
const avgBtcPac = getAvgFundingRate("BTC", "pacifica", 1);
|
|
168
|
+
expect(avgBtcPac).toBeCloseTo(0.0001);
|
|
169
|
+
});
|
|
170
|
+
it("only includes entries within the time window", () => {
|
|
171
|
+
const monthKey = getMonthKey(new Date());
|
|
172
|
+
const filePath = join(DATA_DIR, `${monthKey}.jsonl`);
|
|
173
|
+
const entries = [
|
|
174
|
+
{ ts: new Date(Date.now() - 3 * 60 * 60 * 1000).toISOString(), symbol: "BTC", exchange: "hyperliquid", rate: 0.001, hourlyRate: 0.001 },
|
|
175
|
+
{ ts: new Date(Date.now() - 10 * 60 * 1000).toISOString(), symbol: "BTC", exchange: "hyperliquid", rate: 0.0001, hourlyRate: 0.0001 },
|
|
176
|
+
];
|
|
177
|
+
writeFileSync(filePath, entries.map(e => JSON.stringify(e)).join("\n") + "\n");
|
|
178
|
+
// Only 1h window should exclude the 3h-old entry
|
|
179
|
+
const avg1h = getAvgFundingRate("BTC", "hyperliquid", 1);
|
|
180
|
+
expect(avg1h).toBeCloseTo(0.0001);
|
|
181
|
+
// 4h window should include both
|
|
182
|
+
const avg4h = getAvgFundingRate("BTC", "hyperliquid", 4);
|
|
183
|
+
expect(avg4h).toBeCloseTo(0.00055); // (0.001 + 0.0001) / 2
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
// ──────────────────────────────────────────────
|
|
187
|
+
// getHistoricalRates
|
|
188
|
+
// ──────────────────────────────────────────────
|
|
189
|
+
describe("getHistoricalRates", () => {
|
|
190
|
+
it("returns sorted entries in time range", () => {
|
|
191
|
+
const monthKey = getMonthKey(new Date());
|
|
192
|
+
const filePath = join(DATA_DIR, `${monthKey}.jsonl`);
|
|
193
|
+
const t1 = new Date(Date.now() - 30 * 60 * 1000);
|
|
194
|
+
const t2 = new Date(Date.now() - 20 * 60 * 1000);
|
|
195
|
+
const t3 = new Date(Date.now() - 10 * 60 * 1000);
|
|
196
|
+
const entries = [
|
|
197
|
+
{ ts: t3.toISOString(), symbol: "BTC", exchange: "hyperliquid", rate: 0.0003, hourlyRate: 0.0003 },
|
|
198
|
+
{ ts: t1.toISOString(), symbol: "BTC", exchange: "hyperliquid", rate: 0.0001, hourlyRate: 0.0001 },
|
|
199
|
+
{ ts: t2.toISOString(), symbol: "BTC", exchange: "hyperliquid", rate: 0.0002, hourlyRate: 0.0002 },
|
|
200
|
+
];
|
|
201
|
+
writeFileSync(filePath, entries.map(e => JSON.stringify(e)).join("\n") + "\n");
|
|
202
|
+
const startTime = new Date(Date.now() - 60 * 60 * 1000);
|
|
203
|
+
const endTime = new Date();
|
|
204
|
+
const result = getHistoricalRates("BTC", "hyperliquid", startTime, endTime);
|
|
205
|
+
expect(result).toHaveLength(3);
|
|
206
|
+
// Should be sorted by time ascending
|
|
207
|
+
expect(result[0].rate).toBe(0.0001);
|
|
208
|
+
expect(result[1].rate).toBe(0.0002);
|
|
209
|
+
expect(result[2].rate).toBe(0.0003);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
// ──────────────────────────────────────────────
|
|
213
|
+
// getHistoricalAverages
|
|
214
|
+
// ──────────────────────────────────────────────
|
|
215
|
+
describe("getHistoricalAverages", () => {
|
|
216
|
+
it("handles missing data (returns null)", () => {
|
|
217
|
+
const result = getHistoricalAverages(["BTC"], ["hyperliquid"]);
|
|
218
|
+
const avgs = result.get("BTC:hyperliquid");
|
|
219
|
+
expect(avgs).toBeDefined();
|
|
220
|
+
expect(avgs.avg1h).toBeNull();
|
|
221
|
+
expect(avgs.avg8h).toBeNull();
|
|
222
|
+
expect(avgs.avg24h).toBeNull();
|
|
223
|
+
expect(avgs.avg7d).toBeNull();
|
|
224
|
+
});
|
|
225
|
+
it("computes averages for different time windows", () => {
|
|
226
|
+
const monthKey = getMonthKey(new Date());
|
|
227
|
+
const filePath = join(DATA_DIR, `${monthKey}.jsonl`);
|
|
228
|
+
const entries = [
|
|
229
|
+
// Within 1h
|
|
230
|
+
{ ts: new Date(Date.now() - 30 * 60 * 1000).toISOString(), symbol: "BTC", exchange: "hyperliquid", rate: 0.0002, hourlyRate: 0.0002 },
|
|
231
|
+
// Within 8h but not 1h
|
|
232
|
+
{ ts: new Date(Date.now() - 4 * 60 * 60 * 1000).toISOString(), symbol: "BTC", exchange: "hyperliquid", rate: 0.0004, hourlyRate: 0.0004 },
|
|
233
|
+
];
|
|
234
|
+
writeFileSync(filePath, entries.map(e => JSON.stringify(e)).join("\n") + "\n");
|
|
235
|
+
const result = getHistoricalAverages(["BTC"], ["hyperliquid"]);
|
|
236
|
+
const avgs = result.get("BTC:hyperliquid");
|
|
237
|
+
expect(avgs).toBeDefined();
|
|
238
|
+
expect(avgs.avg1h).toBeCloseTo(0.0002); // only the 30-min-old entry
|
|
239
|
+
expect(avgs.avg8h).toBeCloseTo(0.0003); // both entries: (0.0002 + 0.0004) / 2
|
|
240
|
+
expect(avgs.avg24h).toBeCloseTo(0.0003); // both entries
|
|
241
|
+
expect(avgs.avg7d).toBeCloseTo(0.0003); // both entries
|
|
242
|
+
});
|
|
243
|
+
it("generates correct keys for multiple symbols and exchanges", () => {
|
|
244
|
+
const result = getHistoricalAverages(["BTC", "ETH"], ["hyperliquid", "pacifica"]);
|
|
245
|
+
expect(result.has("BTC:hyperliquid")).toBe(true);
|
|
246
|
+
expect(result.has("BTC:pacifica")).toBe(true);
|
|
247
|
+
expect(result.has("ETH:hyperliquid")).toBe(true);
|
|
248
|
+
expect(result.has("ETH:pacifica")).toBe(true);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
// ──────────────────────────────────────────────
|
|
252
|
+
// cleanupOldFiles
|
|
253
|
+
// ──────────────────────────────────────────────
|
|
254
|
+
describe("cleanupOldFiles", () => {
|
|
255
|
+
it("removes files older than 30 days", () => {
|
|
256
|
+
// Create a file from 3 months ago
|
|
257
|
+
const oldDate = new Date();
|
|
258
|
+
oldDate.setMonth(oldDate.getMonth() - 3);
|
|
259
|
+
const oldKey = getMonthKey(oldDate);
|
|
260
|
+
const oldFilePath = join(DATA_DIR, `${oldKey}.jsonl`);
|
|
261
|
+
writeFileSync(oldFilePath, '{"ts":"old","symbol":"BTC","exchange":"hl","rate":0.01,"hourlyRate":0.01}\n');
|
|
262
|
+
// Create a current month file
|
|
263
|
+
const curKey = getMonthKey(new Date());
|
|
264
|
+
const curFilePath = join(DATA_DIR, `${curKey}.jsonl`);
|
|
265
|
+
writeFileSync(curFilePath, '{"ts":"now","symbol":"BTC","exchange":"hl","rate":0.01,"hourlyRate":0.01}\n');
|
|
266
|
+
_resetCleanupFlag();
|
|
267
|
+
cleanupOldFiles();
|
|
268
|
+
expect(existsSync(oldFilePath)).toBe(false);
|
|
269
|
+
expect(existsSync(curFilePath)).toBe(true);
|
|
270
|
+
});
|
|
271
|
+
it("keeps recent files", () => {
|
|
272
|
+
const curKey = getMonthKey(new Date());
|
|
273
|
+
const curFilePath = join(DATA_DIR, `${curKey}.jsonl`);
|
|
274
|
+
writeFileSync(curFilePath, '{"ts":"now","symbol":"BTC","exchange":"hl","rate":0.01,"hourlyRate":0.01}\n');
|
|
275
|
+
_resetCleanupFlag();
|
|
276
|
+
cleanupOldFiles();
|
|
277
|
+
expect(existsSync(curFilePath)).toBe(true);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
// ──────────────────────────────────────────────
|
|
281
|
+
// getCompoundedAnnualReturn
|
|
282
|
+
// ──────────────────────────────────────────────
|
|
283
|
+
describe("getCompoundedAnnualReturn", () => {
|
|
284
|
+
it("calculates correct compounded return for HL (1h compounding)", () => {
|
|
285
|
+
// hourlyRate = 0.01% = 0.0001
|
|
286
|
+
// periodRate = 0.0001 * 1 = 0.0001
|
|
287
|
+
// periodsPerYear = 8760/1 = 8760
|
|
288
|
+
// (1 + 0.0001)^8760 - 1
|
|
289
|
+
const result = getCompoundedAnnualReturn(0.0001, 1);
|
|
290
|
+
// Expected: (1.0001)^8760 - 1 = ~1.3964 (139.64%)
|
|
291
|
+
expect(result).toBeCloseTo(Math.pow(1.0001, 8760) - 1, 2);
|
|
292
|
+
expect(result).toBeGreaterThan(0.876); // Should be > simple rate of 87.6%
|
|
293
|
+
});
|
|
294
|
+
it("calculates correct compounded return for PAC/LT (1h compounding, same as HL)", () => {
|
|
295
|
+
// hourlyRate = 0.0001
|
|
296
|
+
// periodRate = 0.0001 * 1 = 0.0001
|
|
297
|
+
// periodsPerYear = 8760/1 = 8760
|
|
298
|
+
// (1 + 0.0001)^8760 - 1
|
|
299
|
+
const result = getCompoundedAnnualReturn(0.0001, 1);
|
|
300
|
+
expect(result).toBeCloseTo(Math.pow(1.0001, 8760) - 1, 2);
|
|
301
|
+
});
|
|
302
|
+
it("returns 0 for zero rate", () => {
|
|
303
|
+
expect(getCompoundedAnnualReturn(0, 1)).toBe(0);
|
|
304
|
+
});
|
|
305
|
+
it("handles negative rates", () => {
|
|
306
|
+
const result = getCompoundedAnnualReturn(-0.0001, 1);
|
|
307
|
+
// (1 - 0.0001)^8760 - 1 should be negative
|
|
308
|
+
expect(result).toBeLessThan(0);
|
|
309
|
+
expect(result).toBeCloseTo(Math.pow(1 - 0.0001, 8760) - 1, 2);
|
|
310
|
+
});
|
|
311
|
+
it("all exchanges compound at the same frequency (1h)", () => {
|
|
312
|
+
const hourlyRate = 0.0001;
|
|
313
|
+
const hl = getCompoundedAnnualReturn(hourlyRate, 1); // compound every 1h
|
|
314
|
+
const pac = getCompoundedAnnualReturn(hourlyRate, 1); // compound every 1h (same as HL)
|
|
315
|
+
// Same compounding frequency, same effective annual return
|
|
316
|
+
expect(hl).toBeCloseTo(pac, 10);
|
|
317
|
+
});
|
|
318
|
+
it("simple rate sanity check: small rate, compounded vs simple", () => {
|
|
319
|
+
const hourlyRate = 0.0001;
|
|
320
|
+
const simpleAnnual = hourlyRate * 8760; // 0.876
|
|
321
|
+
const compoundedAnnual = getCompoundedAnnualReturn(hourlyRate, 1);
|
|
322
|
+
// Compounded should always be greater than simple for positive rates
|
|
323
|
+
expect(compoundedAnnual).toBeGreaterThan(simpleAnnual);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
// ──────────────────────────────────────────────
|
|
327
|
+
// getExchangeCompoundingHours
|
|
328
|
+
// ──────────────────────────────────────────────
|
|
329
|
+
describe("getExchangeCompoundingHours", () => {
|
|
330
|
+
it("returns 1 for hyperliquid", () => {
|
|
331
|
+
expect(getExchangeCompoundingHours("hyperliquid")).toBe(1);
|
|
332
|
+
});
|
|
333
|
+
it("returns 1 for pacifica", () => {
|
|
334
|
+
expect(getExchangeCompoundingHours("pacifica")).toBe(1);
|
|
335
|
+
});
|
|
336
|
+
it("returns 1 for lighter", () => {
|
|
337
|
+
expect(getExchangeCompoundingHours("lighter")).toBe(1);
|
|
338
|
+
});
|
|
339
|
+
it("returns 1 for unknown exchanges", () => {
|
|
340
|
+
expect(getExchangeCompoundingHours("binance")).toBe(1);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
});
|