perp-cli 0.9.8 → 0.12.0
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/README.md +30 -25
- package/dist/_stderr-filter.d.ts +0 -0
- package/dist/_stderr-filter.js +30 -0
- package/dist/agent-wallet/expiry.d.ts +15 -0
- package/dist/agent-wallet/expiry.js +26 -0
- package/dist/agent-wallet/lighter-keystore.d.ts +34 -0
- package/dist/agent-wallet/lighter-keystore.js +232 -0
- package/dist/agent-wallet/passphrase.d.ts +13 -0
- package/dist/agent-wallet/passphrase.js +54 -0
- package/dist/agent-wallet/signing-strategy.d.ts +67 -0
- package/dist/agent-wallet/signing-strategy.js +51 -0
- package/dist/agent-wallet/store.d.ts +37 -0
- package/dist/agent-wallet/store.js +172 -0
- package/dist/api/public/_http.d.ts +15 -0
- package/dist/api/public/_http.js +24 -0
- package/dist/api/public/hyperliquid.js +36 -24
- package/dist/api/public/lighter.d.ts +7 -1
- package/dist/api/public/lighter.js +85 -37
- package/dist/api/public/pacifica.js +47 -18
- package/dist/bot/engine.js +23 -4
- package/dist/bot/presets.js +3 -3
- package/dist/bot/strategies/funding-arb-strategy.d.ts +0 -23
- package/dist/bot/strategies/funding-arb-strategy.js +41 -176
- package/dist/bot/strategies/funding-auto.js +2 -2
- package/dist/bridge-engine.js +102 -49
- package/dist/cli-spec.js +1 -2
- package/dist/commands/account.js +2 -500
- package/dist/commands/agent.d.ts +8 -2
- package/dist/commands/agent.js +1903 -366
- package/dist/commands/alerts.js +3 -1
- package/dist/commands/arb/index.js +37 -7
- package/dist/commands/arb-auto.js +44 -11
- package/dist/commands/arb.js +83 -34
- package/dist/commands/bot.d.ts +1 -1
- package/dist/commands/bot.js +20 -16
- package/dist/commands/bridge.d.ts +1 -1
- package/dist/commands/bridge.js +4 -4
- package/dist/commands/funds.d.ts +1 -1
- package/dist/commands/funds.js +18 -68
- package/dist/commands/jobs.d.ts +1 -1
- package/dist/commands/jobs.js +2 -2
- package/dist/commands/manage.d.ts +1 -1
- package/dist/commands/manage.js +12 -84
- package/dist/commands/plan.d.ts +1 -1
- package/dist/commands/plan.js +2 -2
- package/dist/commands/portfolio.d.ts +19 -3
- package/dist/commands/portfolio.js +747 -163
- package/dist/commands/rebalance.d.ts +1 -1
- package/dist/commands/rebalance.js +18 -12
- package/dist/commands/risk.js +2 -6
- package/dist/commands/settings.js +30 -53
- package/dist/commands/trade.js +16 -15
- package/dist/commands/wallet.d.ts +2 -1
- package/dist/commands/wallet.js +1122 -176
- package/dist/config.d.ts +9 -0
- package/dist/config.js +55 -9
- package/dist/errors.d.ts +64 -0
- package/dist/errors.js +82 -2
- package/dist/event-stream.js +12 -1
- package/dist/exchanges/aster-typed-data.d.ts +156 -0
- package/dist/exchanges/aster-typed-data.js +189 -0
- package/dist/exchanges/aster.d.ts +60 -11
- package/dist/exchanges/aster.js +278 -136
- package/dist/exchanges/hyperliquid.d.ts +14 -0
- package/dist/exchanges/hyperliquid.js +88 -34
- package/dist/exchanges/lighter-spot.js +19 -5
- package/dist/exchanges/lighter.d.ts +66 -0
- package/dist/exchanges/lighter.js +213 -31
- package/dist/exchanges/pacifica-typed-data.d.ts +75 -0
- package/dist/exchanges/pacifica-typed-data.js +74 -0
- package/dist/exchanges/pacifica.d.ts +29 -1
- package/dist/exchanges/pacifica.js +94 -6
- package/dist/funding/rates.js +59 -21
- package/dist/guardrail/contracts.d.ts +14 -0
- package/dist/guardrail/contracts.js +72 -0
- package/dist/guardrail/perp-guardrail.d.ts +21 -0
- package/dist/guardrail/perp-guardrail.js +145 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +307 -362
- package/dist/jobs.js +3 -1
- package/dist/mcp-server.js +92 -88
- package/dist/position-history.js +47 -4
- package/dist/settings.d.ts +69 -3
- package/dist/settings.js +3 -2
- package/dist/signer/index.d.ts +2 -0
- package/dist/signer/index.js +2 -0
- package/dist/signer/ows-evm.d.ts +32 -0
- package/dist/signer/ows-evm.js +86 -0
- package/dist/signer/ows-loader.d.ts +2 -0
- package/dist/signer/ows-loader.js +6 -0
- package/dist/signer/ows-solana.d.ts +25 -0
- package/dist/signer/ows-solana.js +56 -0
- package/dist/smart-order.d.ts +8 -2
- package/dist/smart-order.js +46 -11
- package/dist/strategies/funding-arb.js +29 -8
- package/dist/trade-validator.js +30 -5
- package/dist/utils.d.ts +10 -0
- package/dist/utils.js +19 -0
- package/package.json +4 -2
- package/skills/perp-cli/SKILL.md +23 -14
- package/skills/perp-cli/references/agent-operations.md +6 -4
- package/skills/perp-cli/references/commands.md +55 -34
- package/skills/perp-cli/scripts/preflight.sh +4 -4
- package/skills/perp-cli/scripts/spot-perp-scan.sh +2 -2
- package/dist/__tests__/alert-logic.test.d.ts +0 -1
- package/dist/__tests__/alert-logic.test.js +0 -107
- package/dist/__tests__/arb-auto-3dex.test.d.ts +0 -1
- package/dist/__tests__/arb-auto-3dex.test.js +0 -397
- package/dist/__tests__/arb-history-stats.test.d.ts +0 -1
- package/dist/__tests__/arb-history-stats.test.js +0 -176
- package/dist/__tests__/arb-logic.test.d.ts +0 -1
- package/dist/__tests__/arb-logic.test.js +0 -84
- package/dist/__tests__/arb-manage.test.d.ts +0 -1
- package/dist/__tests__/arb-manage.test.js +0 -253
- package/dist/__tests__/arb-new-features.test.d.ts +0 -1
- package/dist/__tests__/arb-new-features.test.js +0 -457
- package/dist/__tests__/arb-sizing.test.d.ts +0 -1
- package/dist/__tests__/arb-sizing.test.js +0 -48
- package/dist/__tests__/arb-state.test.d.ts +0 -1
- package/dist/__tests__/arb-state.test.js +0 -284
- package/dist/__tests__/arb-userflow.test.d.ts +0 -1
- package/dist/__tests__/arb-userflow.test.js +0 -945
- package/dist/__tests__/arb-utils.test.d.ts +0 -1
- package/dist/__tests__/arb-utils.test.js +0 -264
- package/dist/__tests__/bot-conditions.test.d.ts +0 -1
- package/dist/__tests__/bot-conditions.test.js +0 -341
- package/dist/__tests__/client-id-tracker.test.d.ts +0 -1
- package/dist/__tests__/client-id-tracker.test.js +0 -137
- package/dist/__tests__/commands/new-atomic-commands.test.d.ts +0 -1
- package/dist/__tests__/commands/new-atomic-commands.test.js +0 -502
- package/dist/__tests__/commands/order-intent.test.d.ts +0 -1
- package/dist/__tests__/commands/order-intent.test.js +0 -600
- package/dist/__tests__/commands/trade-commands.test.d.ts +0 -1
- package/dist/__tests__/commands/trade-commands.test.js +0 -821
- package/dist/__tests__/config.test.d.ts +0 -1
- package/dist/__tests__/config.test.js +0 -86
- package/dist/__tests__/cross-chain-margin.test.d.ts +0 -1
- package/dist/__tests__/cross-chain-margin.test.js +0 -287
- package/dist/__tests__/dex-asset-map.test.d.ts +0 -1
- package/dist/__tests__/dex-asset-map.test.js +0 -191
- package/dist/__tests__/errors.test.d.ts +0 -1
- package/dist/__tests__/errors.test.js +0 -110
- package/dist/__tests__/event-stream.test.d.ts +0 -1
- package/dist/__tests__/event-stream.test.js +0 -276
- package/dist/__tests__/exchanges/interface.test.d.ts +0 -1
- package/dist/__tests__/exchanges/interface.test.js +0 -132
- package/dist/__tests__/exchanges/mock-adapter.d.ts +0 -69
- package/dist/__tests__/exchanges/mock-adapter.js +0 -137
- package/dist/__tests__/execution-log.test.d.ts +0 -1
- package/dist/__tests__/execution-log.test.js +0 -106
- package/dist/__tests__/funding-calc.test.d.ts +0 -1
- package/dist/__tests__/funding-calc.test.js +0 -71
- package/dist/__tests__/funding-history.test.d.ts +0 -1
- package/dist/__tests__/funding-history.test.js +0 -343
- package/dist/__tests__/funding-rates.test.d.ts +0 -1
- package/dist/__tests__/funding-rates.test.js +0 -342
- package/dist/__tests__/funding.test.d.ts +0 -1
- package/dist/__tests__/funding.test.js +0 -173
- package/dist/__tests__/gap-logic.test.d.ts +0 -1
- package/dist/__tests__/gap-logic.test.js +0 -43
- package/dist/__tests__/hip3-dex.test.d.ts +0 -1
- package/dist/__tests__/hip3-dex.test.js +0 -234
- package/dist/__tests__/integration/agent-features.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/agent-features.integration.test.js +0 -553
- package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +0 -13
- package/dist/__tests__/integration/atomic-commands.integration.test.js +0 -246
- package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/bridge-simulation.integration.test.js +0 -453
- package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/bridge-strict.integration.test.js +0 -812
- package/dist/__tests__/integration/bridge.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/bridge.integration.test.js +0 -309
- package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/cli-e2e.integration.test.js +0 -202
- package/dist/__tests__/integration/dex-arb.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/dex-arb.integration.test.js +0 -116
- package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +0 -13
- package/dist/__tests__/integration/envelope-consistency.integration.test.js +0 -205
- package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/hip3-dex.integration.test.js +0 -147
- package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/hyperliquid.integration.test.js +0 -79
- package/dist/__tests__/integration/lighter.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/lighter.integration.test.js +0 -53
- package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +0 -9
- package/dist/__tests__/integration/new-commands-e2e.integration.test.js +0 -236
- package/dist/__tests__/integration/order-verification.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/order-verification.integration.test.js +0 -321
- package/dist/__tests__/integration/pacifica.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/pacifica.integration.test.js +0 -75
- package/dist/__tests__/integration/response-shapes.integration.test.d.ts +0 -1
- package/dist/__tests__/integration/response-shapes.integration.test.js +0 -278
- package/dist/__tests__/liquidity.test.d.ts +0 -1
- package/dist/__tests__/liquidity.test.js +0 -225
- package/dist/__tests__/plan-executor.test.d.ts +0 -1
- package/dist/__tests__/plan-executor.test.js +0 -314
- package/dist/__tests__/position-history.test.d.ts +0 -1
- package/dist/__tests__/position-history.test.js +0 -367
- package/dist/__tests__/retry.test.d.ts +0 -1
- package/dist/__tests__/retry.test.js +0 -310
- package/dist/__tests__/risk-assessment.test.d.ts +0 -1
- package/dist/__tests__/risk-assessment.test.js +0 -288
- package/dist/__tests__/security-adversarial.test.d.ts +0 -1
- package/dist/__tests__/security-adversarial.test.js +0 -574
- package/dist/__tests__/strategies.test.d.ts +0 -1
- package/dist/__tests__/strategies.test.js +0 -539
- package/dist/__tests__/trade-execution.test.d.ts +0 -1
- package/dist/__tests__/trade-execution.test.js +0 -129
- package/dist/__tests__/trade-validator.test.d.ts +0 -1
- package/dist/__tests__/trade-validator.test.js +0 -655
- package/dist/__tests__/utils.test.d.ts +0 -1
- package/dist/__tests__/utils.test.js +0 -76
- package/dist/arb/reconcile.d.ts +0 -28
- package/dist/arb/reconcile.js +0 -162
- package/dist/commands/alert.d.ts +0 -2
- package/dist/commands/alert.js +0 -421
- package/dist/commands/analytics.d.ts +0 -3
- package/dist/commands/analytics.js +0 -311
- package/dist/commands/arb-manage.d.ts +0 -5
- package/dist/commands/arb-manage.js +0 -5
- package/dist/commands/dashboard.d.ts +0 -3
- package/dist/commands/dashboard.js +0 -176
- package/dist/commands/deposit.d.ts +0 -4
- package/dist/commands/deposit.js +0 -573
- package/dist/commands/dex.d.ts +0 -3
- package/dist/commands/dex.js +0 -120
- package/dist/commands/env.d.ts +0 -2
- package/dist/commands/env.js +0 -136
- package/dist/commands/funding.d.ts +0 -3
- package/dist/commands/funding.js +0 -554
- package/dist/commands/gap.d.ts +0 -2
- package/dist/commands/gap.js +0 -305
- package/dist/commands/health.d.ts +0 -2
- package/dist/commands/health.js +0 -67
- package/dist/commands/multileg.d.ts +0 -16
- package/dist/commands/multileg.js +0 -174
- package/dist/commands/pnl.d.ts +0 -12
- package/dist/commands/pnl.js +0 -243
- package/dist/commands/run.d.ts +0 -3
- package/dist/commands/run.js +0 -202
- package/dist/commands/stream.d.ts +0 -5
- package/dist/commands/stream.js +0 -200
- package/dist/commands/withdraw.d.ts +0 -3
- package/dist/commands/withdraw.js +0 -187
- package/dist/exchanges/edgex.d.ts +0 -62
- package/dist/exchanges/edgex.js +0 -552
- package/dist/exchanges/nado.d.ts +0 -52
- package/dist/exchanges/nado.js +0 -223
- package/dist/funding/index.d.ts +0 -3
- package/dist/funding/index.js +0 -3
- package/dist/retry.d.ts +0 -74
- package/dist/retry.js +0 -129
- package/dist/ws/market-feed.d.ts +0 -74
- package/dist/ws/market-feed.js +0 -227
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { computeEnhancedStats, normalizeExchangePair, getTimeBucket, } from "../arb-history-stats.js";
|
|
3
|
-
function makeTrade(overrides = {}) {
|
|
4
|
-
return {
|
|
5
|
-
symbol: "ETH",
|
|
6
|
-
exchanges: "hyperliquid+pacifica",
|
|
7
|
-
entryDate: "2025-01-15T10:00:00.000Z",
|
|
8
|
-
exitDate: "2025-01-19T14:00:00.000Z",
|
|
9
|
-
holdDurationMs: 4 * 24 * 60 * 60 * 1000 + 4 * 60 * 60 * 1000, // 4d 4h
|
|
10
|
-
entrySpread: 35.0,
|
|
11
|
-
exitSpread: 8.0,
|
|
12
|
-
netReturn: 5.20,
|
|
13
|
-
status: "completed",
|
|
14
|
-
...overrides,
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
describe("normalizeExchangePair", () => {
|
|
18
|
-
it("converts exchange names to abbreviations and sorts", () => {
|
|
19
|
-
expect(normalizeExchangePair("hyperliquid+pacifica")).toBe("HL/PAC");
|
|
20
|
-
expect(normalizeExchangePair("pacifica+hyperliquid")).toBe("HL/PAC");
|
|
21
|
-
expect(normalizeExchangePair("lighter+hyperliquid")).toBe("HL/LT");
|
|
22
|
-
expect(normalizeExchangePair("lighter+pacifica")).toBe("LT/PAC");
|
|
23
|
-
});
|
|
24
|
-
it("handles unknown exchanges with truncation", () => {
|
|
25
|
-
expect(normalizeExchangePair("binance+coinbase")).toBe("BIN/COI");
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
describe("getTimeBucket", () => {
|
|
29
|
-
it("returns correct 4-hour UTC buckets", () => {
|
|
30
|
-
expect(getTimeBucket("2025-01-15T00:00:00.000Z")).toBe("00-04 UTC");
|
|
31
|
-
expect(getTimeBucket("2025-01-15T03:59:59.000Z")).toBe("00-04 UTC");
|
|
32
|
-
expect(getTimeBucket("2025-01-15T04:00:00.000Z")).toBe("04-08 UTC");
|
|
33
|
-
expect(getTimeBucket("2025-01-15T08:30:00.000Z")).toBe("08-12 UTC");
|
|
34
|
-
expect(getTimeBucket("2025-01-15T12:00:00.000Z")).toBe("12-16 UTC");
|
|
35
|
-
expect(getTimeBucket("2025-01-15T16:45:00.000Z")).toBe("16-20 UTC");
|
|
36
|
-
expect(getTimeBucket("2025-01-15T20:00:00.000Z")).toBe("20-24 UTC");
|
|
37
|
-
expect(getTimeBucket("2025-01-15T23:59:59.000Z")).toBe("20-24 UTC");
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
describe("computeEnhancedStats", () => {
|
|
41
|
-
it("handles empty history gracefully", () => {
|
|
42
|
-
const stats = computeEnhancedStats([]);
|
|
43
|
-
expect(stats.avgEntrySpread).toBe(0);
|
|
44
|
-
expect(stats.avgExitSpread).toBe(0);
|
|
45
|
-
expect(stats.avgSpreadDecay).toBe(0);
|
|
46
|
-
expect(stats.byExchangePair).toEqual([]);
|
|
47
|
-
expect(stats.byTimeOfDay).toEqual([]);
|
|
48
|
-
expect(stats.optimalHoldTime).toBeNull();
|
|
49
|
-
expect(stats.optimalHoldTimeMs).toBeNull();
|
|
50
|
-
});
|
|
51
|
-
it("ignores open and failed trades in completed stats", () => {
|
|
52
|
-
const trades = [
|
|
53
|
-
makeTrade({ status: "open", netReturn: 0 }),
|
|
54
|
-
makeTrade({ status: "failed", netReturn: -1 }),
|
|
55
|
-
];
|
|
56
|
-
const stats = computeEnhancedStats(trades);
|
|
57
|
-
expect(stats.byExchangePair).toEqual([]);
|
|
58
|
-
expect(stats.byTimeOfDay).toEqual([]);
|
|
59
|
-
});
|
|
60
|
-
it("computes average entry/exit spreads correctly", () => {
|
|
61
|
-
const trades = [
|
|
62
|
-
makeTrade({ entrySpread: 30, exitSpread: 10 }),
|
|
63
|
-
makeTrade({ entrySpread: 40, exitSpread: 6 }),
|
|
64
|
-
];
|
|
65
|
-
const stats = computeEnhancedStats(trades);
|
|
66
|
-
expect(stats.avgEntrySpread).toBe(35);
|
|
67
|
-
expect(stats.avgExitSpread).toBe(8);
|
|
68
|
-
expect(stats.avgSpreadDecay).toBe(27); // (20 + 34) / 2
|
|
69
|
-
});
|
|
70
|
-
it("handles null spreads in averages", () => {
|
|
71
|
-
const trades = [
|
|
72
|
-
makeTrade({ entrySpread: 30, exitSpread: null }),
|
|
73
|
-
makeTrade({ entrySpread: null, exitSpread: 10 }),
|
|
74
|
-
];
|
|
75
|
-
const stats = computeEnhancedStats(trades);
|
|
76
|
-
expect(stats.avgEntrySpread).toBe(30);
|
|
77
|
-
expect(stats.avgExitSpread).toBe(10);
|
|
78
|
-
expect(stats.avgSpreadDecay).toBe(0); // neither has both
|
|
79
|
-
});
|
|
80
|
-
it("groups by exchange pair correctly", () => {
|
|
81
|
-
const trades = [
|
|
82
|
-
makeTrade({ exchanges: "hyperliquid+pacifica", netReturn: 10 }),
|
|
83
|
-
makeTrade({ exchanges: "hyperliquid+pacifica", netReturn: 5 }),
|
|
84
|
-
makeTrade({ exchanges: "hyperliquid+pacifica", netReturn: -2 }),
|
|
85
|
-
makeTrade({ exchanges: "lighter+pacifica", netReturn: 8 }),
|
|
86
|
-
makeTrade({ exchanges: "lighter+pacifica", netReturn: 3 }),
|
|
87
|
-
makeTrade({ exchanges: "hyperliquid+lighter", netReturn: 1 }),
|
|
88
|
-
];
|
|
89
|
-
const stats = computeEnhancedStats(trades);
|
|
90
|
-
expect(stats.byExchangePair).toHaveLength(3);
|
|
91
|
-
// Sorted by trade count descending
|
|
92
|
-
const hlPac = stats.byExchangePair.find(p => p.pair === "HL/PAC");
|
|
93
|
-
expect(hlPac).toBeDefined();
|
|
94
|
-
expect(hlPac.trades).toBe(3);
|
|
95
|
-
expect(hlPac.winRate).toBeCloseTo(66.67, 0);
|
|
96
|
-
expect(hlPac.avgNetPnl).toBeCloseTo(4.33, 1);
|
|
97
|
-
const ltPac = stats.byExchangePair.find(p => p.pair === "LT/PAC");
|
|
98
|
-
expect(ltPac).toBeDefined();
|
|
99
|
-
expect(ltPac.trades).toBe(2);
|
|
100
|
-
expect(ltPac.winRate).toBe(100);
|
|
101
|
-
expect(ltPac.avgNetPnl).toBe(5.5);
|
|
102
|
-
const hlLt = stats.byExchangePair.find(p => p.pair === "HL/LT");
|
|
103
|
-
expect(hlLt).toBeDefined();
|
|
104
|
-
expect(hlLt.trades).toBe(1);
|
|
105
|
-
});
|
|
106
|
-
it("buckets by time of day correctly", () => {
|
|
107
|
-
const trades = [
|
|
108
|
-
makeTrade({ entryDate: "2025-01-15T01:00:00.000Z", netReturn: 12 }),
|
|
109
|
-
makeTrade({ entryDate: "2025-01-16T02:30:00.000Z", netReturn: 8 }),
|
|
110
|
-
makeTrade({ entryDate: "2025-01-17T03:00:00.000Z", netReturn: 16 }),
|
|
111
|
-
makeTrade({ entryDate: "2025-01-18T08:15:00.000Z", netReturn: 5 }),
|
|
112
|
-
makeTrade({ entryDate: "2025-01-19T09:00:00.000Z", netReturn: -3 }),
|
|
113
|
-
makeTrade({ entryDate: "2025-01-20T16:00:00.000Z", netReturn: -2 }),
|
|
114
|
-
makeTrade({ entryDate: "2025-01-20T18:30:00.000Z", netReturn: 1 }),
|
|
115
|
-
];
|
|
116
|
-
const stats = computeEnhancedStats(trades);
|
|
117
|
-
const bucket00 = stats.byTimeOfDay.find(b => b.bucket === "00-04 UTC");
|
|
118
|
-
expect(bucket00).toBeDefined();
|
|
119
|
-
expect(bucket00.trades).toBe(3);
|
|
120
|
-
expect(bucket00.winRate).toBe(100);
|
|
121
|
-
expect(bucket00.avgNetPnl).toBe(12);
|
|
122
|
-
const bucket08 = stats.byTimeOfDay.find(b => b.bucket === "08-12 UTC");
|
|
123
|
-
expect(bucket08).toBeDefined();
|
|
124
|
-
expect(bucket08.trades).toBe(2);
|
|
125
|
-
expect(bucket08.winRate).toBe(50);
|
|
126
|
-
const bucket16 = stats.byTimeOfDay.find(b => b.bucket === "16-20 UTC");
|
|
127
|
-
expect(bucket16).toBeDefined();
|
|
128
|
-
expect(bucket16.trades).toBe(2);
|
|
129
|
-
expect(bucket16.avgNetPnl).toBe(-0.5);
|
|
130
|
-
});
|
|
131
|
-
it("computes optimal hold time as median of winning trades", () => {
|
|
132
|
-
const dayMs = 24 * 60 * 60 * 1000;
|
|
133
|
-
const trades = [
|
|
134
|
-
makeTrade({ holdDurationMs: 2 * dayMs, netReturn: 5 }), // win
|
|
135
|
-
makeTrade({ holdDurationMs: 4 * dayMs, netReturn: 10 }), // win
|
|
136
|
-
makeTrade({ holdDurationMs: 6 * dayMs, netReturn: 8 }), // win
|
|
137
|
-
makeTrade({ holdDurationMs: 8 * dayMs, netReturn: -3 }), // loss
|
|
138
|
-
makeTrade({ holdDurationMs: 10 * dayMs, netReturn: 12 }), // win
|
|
139
|
-
];
|
|
140
|
-
const stats = computeEnhancedStats(trades);
|
|
141
|
-
// Profitable hold times sorted: 2d, 4d, 6d, 10d
|
|
142
|
-
// Median of 4 items: (4d + 6d) / 2 = 5d
|
|
143
|
-
expect(stats.optimalHoldTimeMs).toBe(5 * dayMs);
|
|
144
|
-
expect(stats.optimalHoldTime).toBe("5d 0h");
|
|
145
|
-
});
|
|
146
|
-
it("optimal hold time with odd number of winning trades", () => {
|
|
147
|
-
const dayMs = 24 * 60 * 60 * 1000;
|
|
148
|
-
const trades = [
|
|
149
|
-
makeTrade({ holdDurationMs: 3 * dayMs, netReturn: 2 }),
|
|
150
|
-
makeTrade({ holdDurationMs: 5 * dayMs, netReturn: 7 }),
|
|
151
|
-
makeTrade({ holdDurationMs: 7 * dayMs, netReturn: 1 }),
|
|
152
|
-
];
|
|
153
|
-
const stats = computeEnhancedStats(trades);
|
|
154
|
-
// Median of [3d, 5d, 7d] = 5d
|
|
155
|
-
expect(stats.optimalHoldTimeMs).toBe(5 * dayMs);
|
|
156
|
-
expect(stats.optimalHoldTime).toBe("5d 0h");
|
|
157
|
-
});
|
|
158
|
-
it("optimal hold time is null when no profitable trades", () => {
|
|
159
|
-
const trades = [
|
|
160
|
-
makeTrade({ netReturn: -5 }),
|
|
161
|
-
makeTrade({ netReturn: -2 }),
|
|
162
|
-
];
|
|
163
|
-
const stats = computeEnhancedStats(trades);
|
|
164
|
-
expect(stats.optimalHoldTime).toBeNull();
|
|
165
|
-
expect(stats.optimalHoldTimeMs).toBeNull();
|
|
166
|
-
});
|
|
167
|
-
it("skips empty time-of-day buckets", () => {
|
|
168
|
-
const trades = [
|
|
169
|
-
makeTrade({ entryDate: "2025-01-15T02:00:00.000Z" }),
|
|
170
|
-
];
|
|
171
|
-
const stats = computeEnhancedStats(trades);
|
|
172
|
-
// Only the 00-04 bucket should appear
|
|
173
|
-
expect(stats.byTimeOfDay).toHaveLength(1);
|
|
174
|
-
expect(stats.byTimeOfDay[0].bucket).toBe("00-04 UTC");
|
|
175
|
-
});
|
|
176
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
function determinArbDirection(snap) {
|
|
3
|
-
// Short the high-funding exchange (get paid), long the low-funding one
|
|
4
|
-
const shortExchange = snap.pacRate > snap.hlRate ? "pacifica" : "hyperliquid";
|
|
5
|
-
const longExchange = snap.pacRate > snap.hlRate ? "hyperliquid" : "pacifica";
|
|
6
|
-
return { longExchange, shortExchange };
|
|
7
|
-
}
|
|
8
|
-
function computeAnnualizedSpread(pacRate, hlRate) {
|
|
9
|
-
return (pacRate - hlRate) * 24 * 365 * 100;
|
|
10
|
-
}
|
|
11
|
-
describe("Arb direction logic", () => {
|
|
12
|
-
it("shorts high-funding exchange (Pacifica higher)", () => {
|
|
13
|
-
const snap = { symbol: "BTC", pacRate: 0.001, hlRate: 0.0002, spread: 87.6 };
|
|
14
|
-
const { longExchange, shortExchange } = determinArbDirection(snap);
|
|
15
|
-
// Pacifica rate is higher → short Pacifica (get paid funding), long HL
|
|
16
|
-
expect(shortExchange).toBe("pacifica");
|
|
17
|
-
expect(longExchange).toBe("hyperliquid");
|
|
18
|
-
});
|
|
19
|
-
it("shorts high-funding exchange (Hyperliquid higher)", () => {
|
|
20
|
-
const snap = { symbol: "ETH", pacRate: 0.0001, hlRate: 0.0008, spread: -76.65 };
|
|
21
|
-
const { longExchange, shortExchange } = determinArbDirection(snap);
|
|
22
|
-
// HL rate is higher → short HL, long Pacifica
|
|
23
|
-
expect(shortExchange).toBe("hyperliquid");
|
|
24
|
-
expect(longExchange).toBe("pacifica");
|
|
25
|
-
});
|
|
26
|
-
it("handles equal rates", () => {
|
|
27
|
-
const snap = { symbol: "SOL", pacRate: 0.0003, hlRate: 0.0003, spread: 0 };
|
|
28
|
-
const { longExchange, shortExchange } = determinArbDirection(snap);
|
|
29
|
-
// Equal rates → default to long pacifica (pacRate > hlRate is false)
|
|
30
|
-
expect(longExchange).toBe("pacifica");
|
|
31
|
-
expect(shortExchange).toBe("hyperliquid");
|
|
32
|
-
});
|
|
33
|
-
it("handles negative funding rates", () => {
|
|
34
|
-
const snap = { symbol: "DOGE", pacRate: -0.001, hlRate: -0.0002, spread: -87.6 };
|
|
35
|
-
const { longExchange, shortExchange } = determinArbDirection(snap);
|
|
36
|
-
// Pacifica rate is MORE negative → hlRate > pacRate → short HL, long Pacifica
|
|
37
|
-
expect(shortExchange).toBe("hyperliquid");
|
|
38
|
-
expect(longExchange).toBe("pacifica");
|
|
39
|
-
});
|
|
40
|
-
it("handles mixed sign funding rates", () => {
|
|
41
|
-
const snap = { symbol: "ARB", pacRate: 0.001, hlRate: -0.0005, spread: 164.25 };
|
|
42
|
-
const { longExchange, shortExchange } = determinArbDirection(snap);
|
|
43
|
-
// Pac is positive (longs pay), HL is negative (shorts pay)
|
|
44
|
-
// Short Pac (get paid) + Long HL (get paid) = double collect!
|
|
45
|
-
expect(shortExchange).toBe("pacifica");
|
|
46
|
-
expect(longExchange).toBe("hyperliquid");
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
describe("Spread calculation", () => {
|
|
50
|
-
it("computes annualized spread correctly", () => {
|
|
51
|
-
// 0.01% per hour difference → 0.24% per day → 87.6% per year
|
|
52
|
-
const spread = computeAnnualizedSpread(0.0002, 0.0001);
|
|
53
|
-
expect(spread).toBeCloseTo(87.6, 1);
|
|
54
|
-
});
|
|
55
|
-
it("negative spread when HL rate is higher", () => {
|
|
56
|
-
const spread = computeAnnualizedSpread(0.0001, 0.0003);
|
|
57
|
-
expect(spread).toBeLessThan(0);
|
|
58
|
-
expect(Math.abs(spread)).toBeCloseTo(175.2, 1);
|
|
59
|
-
});
|
|
60
|
-
it("zero spread when rates are equal", () => {
|
|
61
|
-
const spread = computeAnnualizedSpread(0.0001, 0.0001);
|
|
62
|
-
expect(spread).toBe(0);
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
describe("Entry/exit conditions", () => {
|
|
66
|
-
const minSpread = 30; // 30% annual
|
|
67
|
-
const closeSpread = 5; // 5% annual
|
|
68
|
-
it("should enter when spread exceeds threshold", () => {
|
|
69
|
-
const absSpread = 45; // 45% > 30%
|
|
70
|
-
expect(absSpread >= minSpread).toBe(true);
|
|
71
|
-
});
|
|
72
|
-
it("should NOT enter when spread below threshold", () => {
|
|
73
|
-
const absSpread = 20; // 20% < 30%
|
|
74
|
-
expect(absSpread >= minSpread).toBe(false);
|
|
75
|
-
});
|
|
76
|
-
it("should close when spread drops below close threshold", () => {
|
|
77
|
-
const currentSpread = 3; // 3% < 5%
|
|
78
|
-
expect(currentSpread <= closeSpread).toBe(true);
|
|
79
|
-
});
|
|
80
|
-
it("should NOT close when spread is still profitable", () => {
|
|
81
|
-
const currentSpread = 15; // 15% > 5%
|
|
82
|
-
expect(currentSpread <= closeSpread).toBe(false);
|
|
83
|
-
});
|
|
84
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { existsSync, unlinkSync, readFileSync, writeFileSync, renameSync } from "fs";
|
|
3
|
-
import { resolve } from "path";
|
|
4
|
-
import { logExecution, readExecutionLog } from "../execution-log.js";
|
|
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.arb-manage-backup");
|
|
8
|
-
function detectArbPairs(positions) {
|
|
9
|
-
const bySymbol = new Map();
|
|
10
|
-
for (const p of positions) {
|
|
11
|
-
const key = p.symbol.toUpperCase();
|
|
12
|
-
if (!bySymbol.has(key))
|
|
13
|
-
bySymbol.set(key, []);
|
|
14
|
-
bySymbol.get(key).push(p);
|
|
15
|
-
}
|
|
16
|
-
const pairs = [];
|
|
17
|
-
for (const [symbol, pos] of bySymbol) {
|
|
18
|
-
const longs = pos.filter(p => p.side === "long");
|
|
19
|
-
const shorts = pos.filter(p => p.side === "short");
|
|
20
|
-
for (const l of longs) {
|
|
21
|
-
for (const s of shorts) {
|
|
22
|
-
if (l.exchange !== s.exchange) {
|
|
23
|
-
pairs.push({ symbol, longExchange: l.exchange, shortExchange: s.exchange, longPos: l, shortPos: s });
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return pairs;
|
|
29
|
-
}
|
|
30
|
-
// ── Tests ──
|
|
31
|
-
describe("Arb pair detection", () => {
|
|
32
|
-
it("matches positions on different exchanges with opposite sides", () => {
|
|
33
|
-
const positions = [
|
|
34
|
-
{ exchange: "hyperliquid", symbol: "BTC", side: "long", size: 0.1, entryPrice: 100000, markPrice: 101000, unrealizedPnl: 100 },
|
|
35
|
-
{ exchange: "lighter", symbol: "BTC", side: "short", size: 0.1, entryPrice: 100500, markPrice: 101000, unrealizedPnl: -50 },
|
|
36
|
-
];
|
|
37
|
-
const pairs = detectArbPairs(positions);
|
|
38
|
-
expect(pairs).toHaveLength(1);
|
|
39
|
-
expect(pairs[0].symbol).toBe("BTC");
|
|
40
|
-
expect(pairs[0].longExchange).toBe("hyperliquid");
|
|
41
|
-
expect(pairs[0].shortExchange).toBe("lighter");
|
|
42
|
-
});
|
|
43
|
-
it("does not match positions on the same exchange", () => {
|
|
44
|
-
const positions = [
|
|
45
|
-
{ exchange: "hyperliquid", symbol: "ETH", side: "long", size: 1, entryPrice: 3000, markPrice: 3100, unrealizedPnl: 100 },
|
|
46
|
-
{ exchange: "hyperliquid", symbol: "ETH", side: "short", size: 1, entryPrice: 3050, markPrice: 3100, unrealizedPnl: -50 },
|
|
47
|
-
];
|
|
48
|
-
const pairs = detectArbPairs(positions);
|
|
49
|
-
expect(pairs).toHaveLength(0);
|
|
50
|
-
});
|
|
51
|
-
it("does not match positions with same side", () => {
|
|
52
|
-
const positions = [
|
|
53
|
-
{ exchange: "hyperliquid", symbol: "SOL", side: "long", size: 10, entryPrice: 150, markPrice: 155, unrealizedPnl: 50 },
|
|
54
|
-
{ exchange: "lighter", symbol: "SOL", side: "long", size: 10, entryPrice: 151, markPrice: 155, unrealizedPnl: 40 },
|
|
55
|
-
];
|
|
56
|
-
const pairs = detectArbPairs(positions);
|
|
57
|
-
expect(pairs).toHaveLength(0);
|
|
58
|
-
});
|
|
59
|
-
it("returns empty for no positions", () => {
|
|
60
|
-
const pairs = detectArbPairs([]);
|
|
61
|
-
expect(pairs).toHaveLength(0);
|
|
62
|
-
});
|
|
63
|
-
it("matches multiple arb pairs for different symbols", () => {
|
|
64
|
-
const positions = [
|
|
65
|
-
{ exchange: "hyperliquid", symbol: "BTC", side: "long", size: 0.1, entryPrice: 100000, markPrice: 101000, unrealizedPnl: 100 },
|
|
66
|
-
{ exchange: "lighter", symbol: "BTC", side: "short", size: 0.1, entryPrice: 100500, markPrice: 101000, unrealizedPnl: -50 },
|
|
67
|
-
{ exchange: "pacifica", symbol: "ETH", side: "long", size: 1, entryPrice: 3000, markPrice: 3100, unrealizedPnl: 100 },
|
|
68
|
-
{ exchange: "hyperliquid", symbol: "ETH", side: "short", size: 1, entryPrice: 3050, markPrice: 3100, unrealizedPnl: -50 },
|
|
69
|
-
];
|
|
70
|
-
const pairs = detectArbPairs(positions);
|
|
71
|
-
expect(pairs).toHaveLength(2);
|
|
72
|
-
const symbols = pairs.map(p => p.symbol).sort();
|
|
73
|
-
expect(symbols).toEqual(["BTC", "ETH"]);
|
|
74
|
-
});
|
|
75
|
-
it("handles case-insensitive symbol matching", () => {
|
|
76
|
-
const positions = [
|
|
77
|
-
{ exchange: "hyperliquid", symbol: "btc", side: "long", size: 0.1, entryPrice: 100000, markPrice: 101000, unrealizedPnl: 100 },
|
|
78
|
-
{ exchange: "lighter", symbol: "BTC", side: "short", size: 0.1, entryPrice: 100500, markPrice: 101000, unrealizedPnl: -50 },
|
|
79
|
-
];
|
|
80
|
-
const pairs = detectArbPairs(positions);
|
|
81
|
-
expect(pairs).toHaveLength(1);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
describe("Arb close dry-run", () => {
|
|
85
|
-
it("dry-run flag prevents execution", () => {
|
|
86
|
-
// Simulating dry-run behavior: the flag should be passed through
|
|
87
|
-
const dryRun = true;
|
|
88
|
-
const executed = [];
|
|
89
|
-
// In dry-run mode, no orders should be placed
|
|
90
|
-
if (!dryRun) {
|
|
91
|
-
executed.push("sell");
|
|
92
|
-
executed.push("buy");
|
|
93
|
-
}
|
|
94
|
-
expect(executed).toHaveLength(0);
|
|
95
|
-
});
|
|
96
|
-
it("produces correct actions for close", () => {
|
|
97
|
-
const longPos = { exchange: "hyperliquid", symbol: "BTC", rawSymbol: "BTC", side: "long", size: 0.1 };
|
|
98
|
-
const shortPos = { exchange: "lighter", symbol: "BTC", rawSymbol: "BTC-PERP", side: "short", size: 0.1 };
|
|
99
|
-
const actions = [
|
|
100
|
-
{ exchange: longPos.exchange, action: "sell", symbol: longPos.rawSymbol, size: String(longPos.size) },
|
|
101
|
-
{ exchange: shortPos.exchange, action: "buy", symbol: shortPos.rawSymbol, size: String(shortPos.size) },
|
|
102
|
-
];
|
|
103
|
-
expect(actions).toHaveLength(2);
|
|
104
|
-
expect(actions[0].action).toBe("sell"); // close long = sell
|
|
105
|
-
expect(actions[1].action).toBe("buy"); // close short = buy
|
|
106
|
-
expect(actions[0].exchange).toBe("hyperliquid");
|
|
107
|
-
expect(actions[1].exchange).toBe("lighter");
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
describe("Arb history grouping and stats", () => {
|
|
111
|
-
beforeEach(() => {
|
|
112
|
-
if (existsSync(LOG_FILE)) {
|
|
113
|
-
const content = readFileSync(LOG_FILE, "utf-8");
|
|
114
|
-
writeFileSync(BACKUP_FILE, content);
|
|
115
|
-
}
|
|
116
|
-
if (existsSync(LOG_FILE))
|
|
117
|
-
unlinkSync(LOG_FILE);
|
|
118
|
-
});
|
|
119
|
-
afterEach(() => {
|
|
120
|
-
if (existsSync(LOG_FILE))
|
|
121
|
-
unlinkSync(LOG_FILE);
|
|
122
|
-
if (existsSync(BACKUP_FILE)) {
|
|
123
|
-
renameSync(BACKUP_FILE, LOG_FILE);
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
it("groups arb entries and closes by symbol", () => {
|
|
127
|
-
logExecution({
|
|
128
|
-
type: "arb_entry", exchange: "hyperliquid+lighter",
|
|
129
|
-
symbol: "BTC", side: "entry", size: "0.1",
|
|
130
|
-
status: "success", dryRun: false,
|
|
131
|
-
meta: { longExchange: "hyperliquid", shortExchange: "lighter", spread: 35, markPrice: 100000 },
|
|
132
|
-
});
|
|
133
|
-
logExecution({
|
|
134
|
-
type: "arb_close", exchange: "hyperliquid+lighter",
|
|
135
|
-
symbol: "BTC", side: "close", size: "0.1",
|
|
136
|
-
status: "success", dryRun: false,
|
|
137
|
-
meta: { longExchange: "hyperliquid", shortExchange: "lighter", currentSpread: 3, unrealizedPnl: 5.5, netPnl: 4.2 },
|
|
138
|
-
});
|
|
139
|
-
const entries = readExecutionLog({ type: "arb_entry" });
|
|
140
|
-
const closes = readExecutionLog({ type: "arb_close" });
|
|
141
|
-
expect(entries).toHaveLength(1);
|
|
142
|
-
expect(closes).toHaveLength(1);
|
|
143
|
-
expect(entries[0].symbol).toBe("BTC");
|
|
144
|
-
expect(closes[0].symbol).toBe("BTC");
|
|
145
|
-
expect(entries[0].meta?.spread).toBe(35);
|
|
146
|
-
expect(closes[0].meta?.netPnl).toBe(4.2);
|
|
147
|
-
});
|
|
148
|
-
it("calculates trade stats from multiple entries", () => {
|
|
149
|
-
// Trade 1: winner
|
|
150
|
-
logExecution({
|
|
151
|
-
type: "arb_entry", exchange: "hyperliquid+lighter",
|
|
152
|
-
symbol: "BTC", side: "entry", size: "0.1",
|
|
153
|
-
status: "success", dryRun: false,
|
|
154
|
-
meta: { spread: 40, markPrice: 100000 },
|
|
155
|
-
});
|
|
156
|
-
logExecution({
|
|
157
|
-
type: "arb_close", exchange: "hyperliquid+lighter",
|
|
158
|
-
symbol: "BTC", side: "close", size: "0.1",
|
|
159
|
-
status: "success", dryRun: false,
|
|
160
|
-
meta: { unrealizedPnl: 10, netPnl: 8 },
|
|
161
|
-
});
|
|
162
|
-
// Trade 2: loser
|
|
163
|
-
logExecution({
|
|
164
|
-
type: "arb_entry", exchange: "pacifica+hyperliquid",
|
|
165
|
-
symbol: "ETH", side: "entry", size: "1.0",
|
|
166
|
-
status: "success", dryRun: false,
|
|
167
|
-
meta: { spread: 25, markPrice: 3000 },
|
|
168
|
-
});
|
|
169
|
-
logExecution({
|
|
170
|
-
type: "arb_close", exchange: "pacifica+hyperliquid",
|
|
171
|
-
symbol: "ETH", side: "close", size: "1.0",
|
|
172
|
-
status: "success", dryRun: false,
|
|
173
|
-
meta: { unrealizedPnl: -5, netPnl: -7 },
|
|
174
|
-
});
|
|
175
|
-
const allEntries = readExecutionLog()
|
|
176
|
-
.filter(r => r.type === "arb_entry" || r.type === "arb_close");
|
|
177
|
-
const entries = allEntries.filter(r => r.type === "arb_entry");
|
|
178
|
-
const closes = allEntries.filter(r => r.type === "arb_close");
|
|
179
|
-
expect(entries).toHaveLength(2);
|
|
180
|
-
expect(closes).toHaveLength(2);
|
|
181
|
-
// Verify we can compute stats
|
|
182
|
-
const completedNetPnls = closes
|
|
183
|
-
.filter(c => c.status === "success" && c.meta?.netPnl !== undefined)
|
|
184
|
-
.map(c => Number(c.meta.netPnl));
|
|
185
|
-
expect(completedNetPnls).toHaveLength(2);
|
|
186
|
-
const totalNetPnl = completedNetPnls.reduce((s, n) => s + n, 0);
|
|
187
|
-
expect(totalNetPnl).toBe(1); // 8 + (-7) = 1
|
|
188
|
-
const winners = completedNetPnls.filter(n => n > 0);
|
|
189
|
-
expect(winners).toHaveLength(1);
|
|
190
|
-
const winRate = (winners.length / completedNetPnls.length) * 100;
|
|
191
|
-
expect(winRate).toBe(50);
|
|
192
|
-
});
|
|
193
|
-
it("handles entries without matching closes (open trades)", () => {
|
|
194
|
-
logExecution({
|
|
195
|
-
type: "arb_entry", exchange: "hyperliquid+lighter",
|
|
196
|
-
symbol: "SOL", side: "entry", size: "10",
|
|
197
|
-
status: "success", dryRun: false,
|
|
198
|
-
meta: { spread: 30, markPrice: 150 },
|
|
199
|
-
});
|
|
200
|
-
const entries = readExecutionLog({ type: "arb_entry" });
|
|
201
|
-
const closes = readExecutionLog({ type: "arb_close" });
|
|
202
|
-
expect(entries).toHaveLength(1);
|
|
203
|
-
expect(closes).toHaveLength(0);
|
|
204
|
-
// Should be treated as an open trade
|
|
205
|
-
const symbol = entries[0].symbol;
|
|
206
|
-
const matchingClose = closes.find(c => c.symbol === symbol);
|
|
207
|
-
expect(matchingClose).toBeUndefined();
|
|
208
|
-
});
|
|
209
|
-
it("filters by period correctly", () => {
|
|
210
|
-
// Log an entry with a very old timestamp manually
|
|
211
|
-
const oldDate = new Date(Date.now() - 60 * 24 * 60 * 60 * 1000).toISOString();
|
|
212
|
-
const recentDate = new Date().toISOString();
|
|
213
|
-
logExecution({
|
|
214
|
-
type: "arb_entry", exchange: "hyperliquid+lighter",
|
|
215
|
-
symbol: "OLD_TRADE", side: "entry", size: "0.1",
|
|
216
|
-
status: "success", dryRun: false,
|
|
217
|
-
});
|
|
218
|
-
logExecution({
|
|
219
|
-
type: "arb_entry", exchange: "hyperliquid+lighter",
|
|
220
|
-
symbol: "NEW_TRADE", side: "entry", size: "0.1",
|
|
221
|
-
status: "success", dryRun: false,
|
|
222
|
-
});
|
|
223
|
-
// Read with 30-day filter: both are recent (logged just now), so both should appear
|
|
224
|
-
const since30d = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
225
|
-
const recent = readExecutionLog({ type: "arb_entry", since: since30d });
|
|
226
|
-
expect(recent).toHaveLength(2);
|
|
227
|
-
});
|
|
228
|
-
});
|
|
229
|
-
describe("Duration formatting", () => {
|
|
230
|
-
function formatDuration(ms) {
|
|
231
|
-
const hours = Math.floor(ms / (1000 * 60 * 60));
|
|
232
|
-
const days = Math.floor(hours / 24);
|
|
233
|
-
const remainingHours = hours % 24;
|
|
234
|
-
if (days > 0)
|
|
235
|
-
return `${days}d ${remainingHours}h`;
|
|
236
|
-
if (hours > 0)
|
|
237
|
-
return `${hours}h`;
|
|
238
|
-
const minutes = Math.floor(ms / (1000 * 60));
|
|
239
|
-
return `${minutes}m`;
|
|
240
|
-
}
|
|
241
|
-
it("formats minutes", () => {
|
|
242
|
-
expect(formatDuration(30 * 60 * 1000)).toBe("30m");
|
|
243
|
-
});
|
|
244
|
-
it("formats hours", () => {
|
|
245
|
-
expect(formatDuration(5 * 60 * 60 * 1000)).toBe("5h");
|
|
246
|
-
});
|
|
247
|
-
it("formats days and hours", () => {
|
|
248
|
-
expect(formatDuration(26 * 60 * 60 * 1000)).toBe("1d 2h");
|
|
249
|
-
});
|
|
250
|
-
it("formats zero", () => {
|
|
251
|
-
expect(formatDuration(0)).toBe("0m");
|
|
252
|
-
});
|
|
253
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|