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,76 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { formatUsd, formatPnl, formatPercent, printJson } from "../utils.js";
|
|
3
|
+
describe("formatUsd", () => {
|
|
4
|
+
it("formats numeric strings", () => {
|
|
5
|
+
expect(formatUsd("1234.5")).toBe("1,234.50");
|
|
6
|
+
});
|
|
7
|
+
it("formats numbers", () => {
|
|
8
|
+
expect(formatUsd(99999.999)).toBe("100,000.00");
|
|
9
|
+
});
|
|
10
|
+
it("returns original string for NaN", () => {
|
|
11
|
+
expect(formatUsd("abc")).toBe("abc");
|
|
12
|
+
});
|
|
13
|
+
it("formats zero", () => {
|
|
14
|
+
expect(formatUsd(0)).toBe("0.00");
|
|
15
|
+
});
|
|
16
|
+
it("formats negative numbers", () => {
|
|
17
|
+
expect(formatUsd(-42.1)).toBe("-42.10");
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
describe("formatPnl", () => {
|
|
21
|
+
it("positive PnL has + prefix", () => {
|
|
22
|
+
const result = formatPnl(100);
|
|
23
|
+
expect(result).toContain("+$100.00");
|
|
24
|
+
});
|
|
25
|
+
it("negative PnL has - prefix", () => {
|
|
26
|
+
const result = formatPnl(-50);
|
|
27
|
+
expect(result).toContain("-$50.00");
|
|
28
|
+
});
|
|
29
|
+
it("zero PnL", () => {
|
|
30
|
+
const result = formatPnl(0);
|
|
31
|
+
expect(result).toContain("$0.00");
|
|
32
|
+
});
|
|
33
|
+
it("handles string input", () => {
|
|
34
|
+
const result = formatPnl("123.456");
|
|
35
|
+
expect(result).toContain("$123.46");
|
|
36
|
+
});
|
|
37
|
+
it("handles NaN", () => {
|
|
38
|
+
expect(formatPnl("abc")).toBe("abc");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe("formatPercent", () => {
|
|
42
|
+
it("positive percent", () => {
|
|
43
|
+
const result = formatPercent(0.0015);
|
|
44
|
+
expect(result).toContain("+0.1500%");
|
|
45
|
+
});
|
|
46
|
+
it("negative percent", () => {
|
|
47
|
+
const result = formatPercent(-0.0025);
|
|
48
|
+
expect(result).toContain("-0.2500%");
|
|
49
|
+
});
|
|
50
|
+
it("zero percent", () => {
|
|
51
|
+
const result = formatPercent(0);
|
|
52
|
+
expect(result).toContain("0.0000%");
|
|
53
|
+
});
|
|
54
|
+
it("handles NaN", () => {
|
|
55
|
+
expect(formatPercent("abc")).toBe("abc");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe("printJson", () => {
|
|
59
|
+
it("outputs valid JSON to console", () => {
|
|
60
|
+
const logs = [];
|
|
61
|
+
const origLog = console.log;
|
|
62
|
+
console.log = (...args) => logs.push(args.join(" "));
|
|
63
|
+
printJson({ a: 1, b: "hello" });
|
|
64
|
+
console.log = origLog;
|
|
65
|
+
const parsed = JSON.parse(logs[0]);
|
|
66
|
+
expect(parsed).toEqual({ a: 1, b: "hello" });
|
|
67
|
+
});
|
|
68
|
+
it("handles arrays", () => {
|
|
69
|
+
const logs = [];
|
|
70
|
+
const origLog = console.log;
|
|
71
|
+
console.log = (...args) => logs.push(args.join(" "));
|
|
72
|
+
printJson([1, 2, 3]);
|
|
73
|
+
console.log = origLog;
|
|
74
|
+
expect(JSON.parse(logs[0])).toEqual([1, 2, 3]);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface HyperliquidAsset {
|
|
2
|
+
symbol: string;
|
|
3
|
+
funding: number;
|
|
4
|
+
markPx: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function fetchHyperliquidMeta(): Promise<HyperliquidAsset[]>;
|
|
7
|
+
export declare function fetchHyperliquidMetaRaw(): Promise<unknown>;
|
|
8
|
+
export declare function parseHyperliquidMetaRaw(raw: unknown): {
|
|
9
|
+
rates: Map<string, number>;
|
|
10
|
+
prices: Map<string, number>;
|
|
11
|
+
};
|
|
12
|
+
export declare function fetchHyperliquidAllMids(): Promise<Record<string, string>>;
|
|
13
|
+
export declare function fetchHyperliquidAllMidsRaw(): Promise<unknown>;
|
|
14
|
+
export declare function pingHyperliquid(): Promise<{
|
|
15
|
+
ok: boolean;
|
|
16
|
+
latencyMs: number;
|
|
17
|
+
status: number;
|
|
18
|
+
}>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { HYPERLIQUID_API_URL } from "./urls.js";
|
|
2
|
+
import { withCache, TTL_MARKET } from "../../cache.js";
|
|
3
|
+
// ── Internal ──
|
|
4
|
+
function hlPost(type) {
|
|
5
|
+
return fetch(HYPERLIQUID_API_URL, {
|
|
6
|
+
method: "POST",
|
|
7
|
+
headers: { "Content-Type": "application/json" },
|
|
8
|
+
body: JSON.stringify({ type }),
|
|
9
|
+
}).then(r => r.json());
|
|
10
|
+
}
|
|
11
|
+
// ── Fetchers ──
|
|
12
|
+
export function fetchHyperliquidMeta() {
|
|
13
|
+
return withCache("pub:hl:metaAndAssetCtxs", TTL_MARKET, async () => {
|
|
14
|
+
try {
|
|
15
|
+
const json = await hlPost("metaAndAssetCtxs");
|
|
16
|
+
const universe = (json[0] ?? {}).universe ?? [];
|
|
17
|
+
const ctxs = (json[1] ?? []);
|
|
18
|
+
return universe.map((asset, i) => {
|
|
19
|
+
const ctx = (ctxs[i] ?? {});
|
|
20
|
+
return {
|
|
21
|
+
symbol: String(asset.name ?? ""),
|
|
22
|
+
funding: Number(ctx.funding ?? 0),
|
|
23
|
+
markPx: Number(ctx.markPx ?? 0),
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export function fetchHyperliquidMetaRaw() {
|
|
33
|
+
return withCache("pub:hl:metaAndAssetCtxs:raw", TTL_MARKET, () => hlPost("metaAndAssetCtxs").catch(() => null));
|
|
34
|
+
}
|
|
35
|
+
export function parseHyperliquidMetaRaw(raw) {
|
|
36
|
+
const rates = new Map();
|
|
37
|
+
const prices = new Map();
|
|
38
|
+
if (!raw || !Array.isArray(raw))
|
|
39
|
+
return { rates, prices };
|
|
40
|
+
const universe = raw[0];
|
|
41
|
+
const ctxs = (raw[1] ?? []);
|
|
42
|
+
const assets = (universe?.universe ?? []);
|
|
43
|
+
assets.forEach((a, i) => {
|
|
44
|
+
const ctx = (ctxs[i] ?? {});
|
|
45
|
+
const sym = String(a.name ?? "");
|
|
46
|
+
if (!sym)
|
|
47
|
+
return;
|
|
48
|
+
rates.set(sym, Number(ctx.funding ?? 0));
|
|
49
|
+
const mp = Number(ctx.markPx ?? 0);
|
|
50
|
+
if (mp > 0)
|
|
51
|
+
prices.set(sym, mp);
|
|
52
|
+
});
|
|
53
|
+
return { rates, prices };
|
|
54
|
+
}
|
|
55
|
+
export function fetchHyperliquidAllMids() {
|
|
56
|
+
return withCache("pub:hl:allMids", TTL_MARKET, async () => {
|
|
57
|
+
try {
|
|
58
|
+
return await hlPost("allMids");
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
export function fetchHyperliquidAllMidsRaw() {
|
|
66
|
+
return withCache("pub:hl:allMids:raw", TTL_MARKET, () => hlPost("allMids").catch(() => null));
|
|
67
|
+
}
|
|
68
|
+
// ── Health check ──
|
|
69
|
+
export async function pingHyperliquid() {
|
|
70
|
+
const start = Date.now();
|
|
71
|
+
try {
|
|
72
|
+
const res = await fetch(HYPERLIQUID_API_URL, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: { "Content-Type": "application/json" },
|
|
75
|
+
body: JSON.stringify({ type: "allMids" }),
|
|
76
|
+
});
|
|
77
|
+
return { ok: res.ok, latencyMs: Date.now() - start, status: res.status };
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return { ok: false, latencyMs: Date.now() - start, status: 0 };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Barrel export for public API fetchers.
|
|
3
|
+
* All exchange-specific fetchers, parsers, and ping functions are re-exported here.
|
|
4
|
+
*/
|
|
5
|
+
export * from "./urls.js";
|
|
6
|
+
export * from "./pacifica.js";
|
|
7
|
+
export * from "./hyperliquid.js";
|
|
8
|
+
export * from "./lighter.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Barrel export for public API fetchers.
|
|
3
|
+
* All exchange-specific fetchers, parsers, and ping functions are re-exported here.
|
|
4
|
+
*/
|
|
5
|
+
export * from "./urls.js";
|
|
6
|
+
export * from "./pacifica.js";
|
|
7
|
+
export * from "./hyperliquid.js";
|
|
8
|
+
export * from "./lighter.js";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface LighterMarketDetail {
|
|
2
|
+
marketId: number;
|
|
3
|
+
symbol: string;
|
|
4
|
+
lastTradePrice: number;
|
|
5
|
+
}
|
|
6
|
+
export interface LighterFundingEntry {
|
|
7
|
+
marketId: number;
|
|
8
|
+
symbol: string;
|
|
9
|
+
rate: number;
|
|
10
|
+
markPrice: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function fetchLighterOrderBookDetails(): Promise<LighterMarketDetail[]>;
|
|
13
|
+
export declare function fetchLighterOrderBookDetailsRaw(): Promise<unknown>;
|
|
14
|
+
export declare function fetchLighterFundingRates(): Promise<LighterFundingEntry[]>;
|
|
15
|
+
export declare function fetchLighterFundingRatesRaw(): Promise<unknown>;
|
|
16
|
+
export declare function parseLighterRaw(detailsRaw: unknown, fundingRaw: unknown): {
|
|
17
|
+
rates: Map<string, number>;
|
|
18
|
+
prices: Map<string, number>;
|
|
19
|
+
};
|
|
20
|
+
export declare function pingLighter(): Promise<{
|
|
21
|
+
ok: boolean;
|
|
22
|
+
latencyMs: number;
|
|
23
|
+
status: number;
|
|
24
|
+
}>;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { LIGHTER_API_URL } from "./urls.js";
|
|
2
|
+
import { withCache, TTL_MARKET } from "../../cache.js";
|
|
3
|
+
// ── Fetchers ──
|
|
4
|
+
export function fetchLighterOrderBookDetails() {
|
|
5
|
+
return withCache("pub:lt:orderBookDetails", TTL_MARKET, async () => {
|
|
6
|
+
try {
|
|
7
|
+
const res = await fetch(`${LIGHTER_API_URL}/api/v1/orderBookDetails`);
|
|
8
|
+
const json = await res.json();
|
|
9
|
+
const details = (json.order_book_details ?? []);
|
|
10
|
+
return details.map(m => ({
|
|
11
|
+
marketId: Number(m.market_id),
|
|
12
|
+
symbol: String(m.symbol ?? ""),
|
|
13
|
+
lastTradePrice: Number(m.last_trade_price ?? 0),
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export function fetchLighterOrderBookDetailsRaw() {
|
|
22
|
+
return withCache("pub:lt:orderBookDetails:raw", TTL_MARKET, () => fetch(`${LIGHTER_API_URL}/api/v1/orderBookDetails`).then(r => r.json()).catch(() => null));
|
|
23
|
+
}
|
|
24
|
+
export function fetchLighterFundingRates() {
|
|
25
|
+
return withCache("pub:lt:fundingRates", TTL_MARKET, async () => {
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch(`${LIGHTER_API_URL}/api/v1/funding-rates`);
|
|
28
|
+
const json = await res.json();
|
|
29
|
+
const list = (json.funding_rates ?? []);
|
|
30
|
+
const entries = [];
|
|
31
|
+
for (const fr of list) {
|
|
32
|
+
if (String(fr.exchange ?? "").toLowerCase() !== "lighter")
|
|
33
|
+
continue;
|
|
34
|
+
entries.push({
|
|
35
|
+
marketId: Number(fr.market_id),
|
|
36
|
+
symbol: String(fr.symbol ?? ""),
|
|
37
|
+
rate: Number(fr.rate ?? fr.funding_rate ?? 0),
|
|
38
|
+
markPrice: Number(fr.mark_price ?? 0),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return entries;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export function fetchLighterFundingRatesRaw() {
|
|
49
|
+
return withCache("pub:lt:fundingRates:raw", TTL_MARKET, () => fetch(`${LIGHTER_API_URL}/api/v1/funding-rates`).then(r => r.json()).catch(() => null));
|
|
50
|
+
}
|
|
51
|
+
export function parseLighterRaw(detailsRaw, fundingRaw) {
|
|
52
|
+
const rates = new Map();
|
|
53
|
+
const prices = new Map();
|
|
54
|
+
const idToSym = new Map();
|
|
55
|
+
const idToPrice = new Map();
|
|
56
|
+
if (detailsRaw) {
|
|
57
|
+
const details = (detailsRaw.order_book_details ?? []);
|
|
58
|
+
for (const m of details) {
|
|
59
|
+
const mid = Number(m.market_id);
|
|
60
|
+
idToSym.set(mid, String(m.symbol ?? ""));
|
|
61
|
+
const p = Number(m.last_trade_price ?? 0);
|
|
62
|
+
if (p > 0)
|
|
63
|
+
idToPrice.set(mid, p);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (fundingRaw) {
|
|
67
|
+
const fundingList = (fundingRaw.funding_rates ?? []);
|
|
68
|
+
for (const fr of fundingList) {
|
|
69
|
+
// API returns rates from multiple exchanges — only use Lighter's own rates
|
|
70
|
+
if (String(fr.exchange ?? "").toLowerCase() !== "lighter")
|
|
71
|
+
continue;
|
|
72
|
+
const sym = String(fr.symbol ?? "") || idToSym.get(Number(fr.market_id)) || "";
|
|
73
|
+
if (!sym || rates.has(sym))
|
|
74
|
+
continue;
|
|
75
|
+
rates.set(sym, Number(fr.rate ?? fr.funding_rate ?? 0));
|
|
76
|
+
const mp = Number(fr.mark_price ?? 0) || idToPrice.get(Number(fr.market_id)) || 0;
|
|
77
|
+
if (mp > 0)
|
|
78
|
+
prices.set(sym, mp);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const [mid, sym] of idToSym) {
|
|
82
|
+
if (!prices.has(sym)) {
|
|
83
|
+
const p = idToPrice.get(mid);
|
|
84
|
+
if (p && p > 0)
|
|
85
|
+
prices.set(sym, p);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { rates, prices };
|
|
89
|
+
}
|
|
90
|
+
// ── Health check ──
|
|
91
|
+
export async function pingLighter() {
|
|
92
|
+
const start = Date.now();
|
|
93
|
+
try {
|
|
94
|
+
const res = await fetch(`${LIGHTER_API_URL}/api/v1/orderBookDetails`);
|
|
95
|
+
return { ok: res.ok, latencyMs: Date.now() - start, status: res.status };
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return { ok: false, latencyMs: Date.now() - start, status: 0 };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface PacificaAsset {
|
|
2
|
+
symbol: string;
|
|
3
|
+
funding: number;
|
|
4
|
+
mark: number;
|
|
5
|
+
nextFunding?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function fetchPacificaPrices(): Promise<PacificaAsset[]>;
|
|
8
|
+
export declare function fetchPacificaPricesRaw(): Promise<unknown>;
|
|
9
|
+
export declare function parsePacificaRaw(raw: unknown): {
|
|
10
|
+
rates: Map<string, number>;
|
|
11
|
+
prices: Map<string, number>;
|
|
12
|
+
};
|
|
13
|
+
export declare function pingPacifica(): Promise<{
|
|
14
|
+
ok: boolean;
|
|
15
|
+
latencyMs: number;
|
|
16
|
+
status: number;
|
|
17
|
+
}>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { PACIFICA_API_URL } from "./urls.js";
|
|
2
|
+
import { withCache, TTL_MARKET } from "../../cache.js";
|
|
3
|
+
// ── Fetchers ──
|
|
4
|
+
export function fetchPacificaPrices() {
|
|
5
|
+
return withCache("pub:pac:prices", TTL_MARKET, async () => {
|
|
6
|
+
try {
|
|
7
|
+
const res = await fetch(PACIFICA_API_URL);
|
|
8
|
+
const json = await res.json();
|
|
9
|
+
const data = json.data ?? json;
|
|
10
|
+
if (!Array.isArray(data))
|
|
11
|
+
return [];
|
|
12
|
+
return data.map((p) => ({
|
|
13
|
+
symbol: String(p.symbol ?? ""),
|
|
14
|
+
funding: Number(p.next_funding ?? p.funding ?? 0),
|
|
15
|
+
mark: Number(p.mark ?? 0),
|
|
16
|
+
nextFunding: p.next_funding ? Number(p.next_funding) : undefined,
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export function fetchPacificaPricesRaw() {
|
|
25
|
+
return withCache("pub:pac:prices:raw", TTL_MARKET, () => fetch(PACIFICA_API_URL).then(r => r.json()).catch(() => null));
|
|
26
|
+
}
|
|
27
|
+
export function parsePacificaRaw(raw) {
|
|
28
|
+
const rates = new Map();
|
|
29
|
+
const prices = new Map();
|
|
30
|
+
const data = raw?.data ?? raw;
|
|
31
|
+
if (!Array.isArray(data))
|
|
32
|
+
return { rates, prices };
|
|
33
|
+
for (const p of data) {
|
|
34
|
+
const sym = String(p.symbol ?? "");
|
|
35
|
+
if (!sym)
|
|
36
|
+
continue;
|
|
37
|
+
rates.set(sym, Number(p.next_funding ?? p.funding ?? 0));
|
|
38
|
+
const mark = Number(p.mark ?? p.price ?? 0);
|
|
39
|
+
if (mark > 0)
|
|
40
|
+
prices.set(sym, mark);
|
|
41
|
+
}
|
|
42
|
+
return { rates, prices };
|
|
43
|
+
}
|
|
44
|
+
// ── Health check ──
|
|
45
|
+
export async function pingPacifica() {
|
|
46
|
+
const start = Date.now();
|
|
47
|
+
try {
|
|
48
|
+
const res = await fetch(PACIFICA_API_URL);
|
|
49
|
+
return { ok: res.ok, latencyMs: Date.now() - start, status: res.status };
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return { ok: false, latencyMs: Date.now() - start, status: 0 };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared API URL management for all exchanges.
|
|
3
|
+
* URLs are mutable to support --network testnet switching.
|
|
4
|
+
*/
|
|
5
|
+
export declare let PACIFICA_API_URL: string;
|
|
6
|
+
export declare let HYPERLIQUID_API_URL: string;
|
|
7
|
+
export declare let LIGHTER_API_URL: string;
|
|
8
|
+
/**
|
|
9
|
+
* Switch shared API URLs between mainnet and testnet.
|
|
10
|
+
* Call from CLI entry point after parsing --network flag.
|
|
11
|
+
*/
|
|
12
|
+
export declare function setSharedApiNetwork(network: "mainnet" | "testnet"): void;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared API URL management for all exchanges.
|
|
3
|
+
* URLs are mutable to support --network testnet switching.
|
|
4
|
+
*/
|
|
5
|
+
const MAINNET_URLS = {
|
|
6
|
+
pacifica: "https://api.pacifica.fi/api/v1/info/prices",
|
|
7
|
+
hyperliquid: "https://api.hyperliquid.xyz/info",
|
|
8
|
+
lighter: "https://mainnet.zklighter.elliot.ai",
|
|
9
|
+
};
|
|
10
|
+
const TESTNET_URLS = {
|
|
11
|
+
pacifica: null, // no public testnet API
|
|
12
|
+
hyperliquid: "https://api.hyperliquid-testnet.xyz/info",
|
|
13
|
+
lighter: "https://testnet.zklighter.elliot.ai",
|
|
14
|
+
};
|
|
15
|
+
export let PACIFICA_API_URL = MAINNET_URLS.pacifica;
|
|
16
|
+
export let HYPERLIQUID_API_URL = MAINNET_URLS.hyperliquid;
|
|
17
|
+
export let LIGHTER_API_URL = MAINNET_URLS.lighter;
|
|
18
|
+
/**
|
|
19
|
+
* Switch shared API URLs between mainnet and testnet.
|
|
20
|
+
* Call from CLI entry point after parsing --network flag.
|
|
21
|
+
*/
|
|
22
|
+
export function setSharedApiNetwork(network) {
|
|
23
|
+
if (network === "testnet") {
|
|
24
|
+
PACIFICA_API_URL = TESTNET_URLS.pacifica ?? MAINNET_URLS.pacifica;
|
|
25
|
+
HYPERLIQUID_API_URL = TESTNET_URLS.hyperliquid ?? MAINNET_URLS.hyperliquid;
|
|
26
|
+
LIGHTER_API_URL = TESTNET_URLS.lighter ?? MAINNET_URLS.lighter;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
PACIFICA_API_URL = MAINNET_URLS.pacifica;
|
|
30
|
+
HYPERLIQUID_API_URL = MAINNET_URLS.hyperliquid;
|
|
31
|
+
LIGHTER_API_URL = MAINNET_URLS.lighter;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced arb history statistics: exchange pair performance,
|
|
3
|
+
* time-of-day analysis, and optimal hold time calculation.
|
|
4
|
+
*/
|
|
5
|
+
export interface ArbTradeForStats {
|
|
6
|
+
symbol: string;
|
|
7
|
+
exchanges: string;
|
|
8
|
+
entryDate: string;
|
|
9
|
+
exitDate: string | null;
|
|
10
|
+
holdDurationMs: number;
|
|
11
|
+
entrySpread: number | null;
|
|
12
|
+
exitSpread: number | null;
|
|
13
|
+
netReturn: number;
|
|
14
|
+
status: "completed" | "open" | "failed";
|
|
15
|
+
}
|
|
16
|
+
export interface ExchangePairPerf {
|
|
17
|
+
pair: string;
|
|
18
|
+
trades: number;
|
|
19
|
+
winRate: number;
|
|
20
|
+
avgNetPnl: number;
|
|
21
|
+
avgHoldTime: string;
|
|
22
|
+
avgHoldTimeMs: number;
|
|
23
|
+
}
|
|
24
|
+
export interface TimeOfDayPerf {
|
|
25
|
+
bucket: string;
|
|
26
|
+
trades: number;
|
|
27
|
+
winRate: number;
|
|
28
|
+
avgNetPnl: number;
|
|
29
|
+
}
|
|
30
|
+
export interface EnhancedHistoryStats {
|
|
31
|
+
avgEntrySpread: number;
|
|
32
|
+
avgExitSpread: number;
|
|
33
|
+
avgSpreadDecay: number;
|
|
34
|
+
byExchangePair: ExchangePairPerf[];
|
|
35
|
+
byTimeOfDay: TimeOfDayPerf[];
|
|
36
|
+
optimalHoldTime: string | null;
|
|
37
|
+
optimalHoldTimeMs: number | null;
|
|
38
|
+
}
|
|
39
|
+
/** Normalize exchange pair string to a short abbreviation like "HL/PAC" */
|
|
40
|
+
export declare function normalizeExchangePair(exchanges: string): string;
|
|
41
|
+
/** Get 4-hour UTC time bucket for a timestamp */
|
|
42
|
+
export declare function getTimeBucket(isoTimestamp: string): string;
|
|
43
|
+
/** Compute enhanced statistics from a list of trades */
|
|
44
|
+
export declare function computeEnhancedStats(trades: ArbTradeForStats[]): EnhancedHistoryStats;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced arb history statistics: exchange pair performance,
|
|
3
|
+
* time-of-day analysis, and optimal hold time calculation.
|
|
4
|
+
*/
|
|
5
|
+
const EXCHANGE_ABBREVS = {
|
|
6
|
+
hyperliquid: "HL",
|
|
7
|
+
pacifica: "PAC",
|
|
8
|
+
lighter: "LT",
|
|
9
|
+
};
|
|
10
|
+
function abbrevExchange(name) {
|
|
11
|
+
return EXCHANGE_ABBREVS[name.toLowerCase()] ?? name.toUpperCase().slice(0, 3);
|
|
12
|
+
}
|
|
13
|
+
/** Normalize exchange pair string to a short abbreviation like "HL/PAC" */
|
|
14
|
+
export function normalizeExchangePair(exchanges) {
|
|
15
|
+
// exchanges format: "hyperliquid+pacifica" or "lighter+hyperliquid"
|
|
16
|
+
const parts = exchanges.split("+").map(e => abbrevExchange(e.trim()));
|
|
17
|
+
parts.sort(); // alphabetical for consistency
|
|
18
|
+
return parts.join("/");
|
|
19
|
+
}
|
|
20
|
+
/** Get 4-hour UTC time bucket for a timestamp */
|
|
21
|
+
export function getTimeBucket(isoTimestamp) {
|
|
22
|
+
const date = new Date(isoTimestamp);
|
|
23
|
+
const hour = date.getUTCHours();
|
|
24
|
+
const bucketStart = Math.floor(hour / 4) * 4;
|
|
25
|
+
const bucketEnd = bucketStart + 4;
|
|
26
|
+
return `${String(bucketStart).padStart(2, "0")}-${String(bucketEnd).padStart(2, "0")} UTC`;
|
|
27
|
+
}
|
|
28
|
+
function formatDuration(ms) {
|
|
29
|
+
const hours = Math.floor(ms / (1000 * 60 * 60));
|
|
30
|
+
const days = Math.floor(hours / 24);
|
|
31
|
+
const remainingHours = hours % 24;
|
|
32
|
+
if (days > 0)
|
|
33
|
+
return `${days}d ${remainingHours}h`;
|
|
34
|
+
if (hours > 0)
|
|
35
|
+
return `${hours}h`;
|
|
36
|
+
const minutes = Math.floor(ms / (1000 * 60));
|
|
37
|
+
return `${minutes}m`;
|
|
38
|
+
}
|
|
39
|
+
/** Compute enhanced statistics from a list of trades */
|
|
40
|
+
export function computeEnhancedStats(trades) {
|
|
41
|
+
const completed = trades.filter(t => t.status === "completed");
|
|
42
|
+
// Average entry/exit spreads
|
|
43
|
+
const entrySpreads = completed
|
|
44
|
+
.map(t => t.entrySpread)
|
|
45
|
+
.filter((s) => s !== null);
|
|
46
|
+
const exitSpreads = completed
|
|
47
|
+
.map(t => t.exitSpread)
|
|
48
|
+
.filter((s) => s !== null);
|
|
49
|
+
const avgEntrySpread = entrySpreads.length > 0
|
|
50
|
+
? entrySpreads.reduce((s, v) => s + v, 0) / entrySpreads.length
|
|
51
|
+
: 0;
|
|
52
|
+
const avgExitSpread = exitSpreads.length > 0
|
|
53
|
+
? exitSpreads.reduce((s, v) => s + v, 0) / exitSpreads.length
|
|
54
|
+
: 0;
|
|
55
|
+
// Average spread decay: how much spread typically decays from entry to exit
|
|
56
|
+
const spreadDecays = [];
|
|
57
|
+
for (const t of completed) {
|
|
58
|
+
if (t.entrySpread !== null && t.exitSpread !== null) {
|
|
59
|
+
spreadDecays.push(t.entrySpread - t.exitSpread);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const avgSpreadDecay = spreadDecays.length > 0
|
|
63
|
+
? spreadDecays.reduce((s, v) => s + v, 0) / spreadDecays.length
|
|
64
|
+
: 0;
|
|
65
|
+
// ── By Exchange Pair ──
|
|
66
|
+
const pairMap = new Map();
|
|
67
|
+
for (const t of completed) {
|
|
68
|
+
const pair = normalizeExchangePair(t.exchanges);
|
|
69
|
+
if (!pairMap.has(pair))
|
|
70
|
+
pairMap.set(pair, []);
|
|
71
|
+
pairMap.get(pair).push(t);
|
|
72
|
+
}
|
|
73
|
+
const byExchangePair = [];
|
|
74
|
+
for (const [pair, pairTrades] of pairMap) {
|
|
75
|
+
const wins = pairTrades.filter(t => t.netReturn > 0).length;
|
|
76
|
+
const avgPnl = pairTrades.reduce((s, t) => s + t.netReturn, 0) / pairTrades.length;
|
|
77
|
+
const avgHoldMs = pairTrades.reduce((s, t) => s + t.holdDurationMs, 0) / pairTrades.length;
|
|
78
|
+
byExchangePair.push({
|
|
79
|
+
pair,
|
|
80
|
+
trades: pairTrades.length,
|
|
81
|
+
winRate: (wins / pairTrades.length) * 100,
|
|
82
|
+
avgNetPnl: avgPnl,
|
|
83
|
+
avgHoldTime: formatDuration(avgHoldMs),
|
|
84
|
+
avgHoldTimeMs: avgHoldMs,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// Sort by trade count descending
|
|
88
|
+
byExchangePair.sort((a, b) => b.trades - a.trades);
|
|
89
|
+
// ── By Time of Day ──
|
|
90
|
+
const bucketMap = new Map();
|
|
91
|
+
for (const t of completed) {
|
|
92
|
+
const bucket = getTimeBucket(t.entryDate);
|
|
93
|
+
if (!bucketMap.has(bucket))
|
|
94
|
+
bucketMap.set(bucket, []);
|
|
95
|
+
bucketMap.get(bucket).push(t);
|
|
96
|
+
}
|
|
97
|
+
const byTimeOfDay = [];
|
|
98
|
+
// Ensure all 6 buckets are represented (even if empty) for completeness
|
|
99
|
+
const allBuckets = ["00-04 UTC", "04-08 UTC", "08-12 UTC", "12-16 UTC", "16-20 UTC", "20-24 UTC"];
|
|
100
|
+
for (const bucket of allBuckets) {
|
|
101
|
+
const bucketTrades = bucketMap.get(bucket) ?? [];
|
|
102
|
+
if (bucketTrades.length === 0)
|
|
103
|
+
continue; // skip empty buckets in output
|
|
104
|
+
const wins = bucketTrades.filter(t => t.netReturn > 0).length;
|
|
105
|
+
const avgPnl = bucketTrades.reduce((s, t) => s + t.netReturn, 0) / bucketTrades.length;
|
|
106
|
+
byTimeOfDay.push({
|
|
107
|
+
bucket,
|
|
108
|
+
trades: bucketTrades.length,
|
|
109
|
+
winRate: (wins / bucketTrades.length) * 100,
|
|
110
|
+
avgNetPnl: avgPnl,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
// ── Optimal Hold Time ──
|
|
114
|
+
// Median hold time of profitable completed trades
|
|
115
|
+
const profitableHoldTimes = completed
|
|
116
|
+
.filter(t => t.netReturn > 0)
|
|
117
|
+
.map(t => t.holdDurationMs)
|
|
118
|
+
.sort((a, b) => a - b);
|
|
119
|
+
let optimalHoldTimeMs = null;
|
|
120
|
+
if (profitableHoldTimes.length > 0) {
|
|
121
|
+
const mid = Math.floor(profitableHoldTimes.length / 2);
|
|
122
|
+
optimalHoldTimeMs = profitableHoldTimes.length % 2 === 0
|
|
123
|
+
? (profitableHoldTimes[mid - 1] + profitableHoldTimes[mid]) / 2
|
|
124
|
+
: profitableHoldTimes[mid];
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
avgEntrySpread,
|
|
128
|
+
avgExitSpread,
|
|
129
|
+
avgSpreadDecay,
|
|
130
|
+
byExchangePair,
|
|
131
|
+
byTimeOfDay,
|
|
132
|
+
optimalHoldTime: optimalHoldTimeMs !== null ? formatDuration(optimalHoldTimeMs) : null,
|
|
133
|
+
optimalHoldTimeMs,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ExchangeAdapter } from "../exchanges/interface.js";
|
|
2
|
+
/**
|
|
3
|
+
* Calculate the exact same size for both legs of an arb position,
|
|
4
|
+
* respecting both exchanges' minimum order and size precision constraints.
|
|
5
|
+
*
|
|
6
|
+
* @returns size string that works for both exchanges, or null if not viable
|
|
7
|
+
*/
|
|
8
|
+
export declare function computeMatchedSize(sizeUsd: number, price: number, longExchange: string, shortExchange: string): {
|
|
9
|
+
size: string;
|
|
10
|
+
notional: number;
|
|
11
|
+
} | null;
|
|
12
|
+
/**
|
|
13
|
+
* After both legs are submitted, verify actual fills match.
|
|
14
|
+
* If there's a size mismatch, place a correction order on the larger side.
|
|
15
|
+
*
|
|
16
|
+
* @returns the corrected size, or null if already matched
|
|
17
|
+
*/
|
|
18
|
+
export declare function reconcileArbFills(longAdapter: ExchangeAdapter, shortAdapter: ExchangeAdapter, symbol: string, log?: (msg: string) => void): Promise<{
|
|
19
|
+
matched: boolean;
|
|
20
|
+
longSize: number;
|
|
21
|
+
shortSize: number;
|
|
22
|
+
correction?: string;
|
|
23
|
+
}>;
|