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,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HIP-3 Deployed Dex Asset Mapping.
|
|
3
|
+
*
|
|
4
|
+
* Maps assets across Hyperliquid deployed dexes to their common underlying.
|
|
5
|
+
* Assets with the same base name (e.g., xyz:TSLA and flx:TSLA) are auto-matched.
|
|
6
|
+
* Known aliases (e.g., CL = OIL = crude oil) are explicitly mapped.
|
|
7
|
+
*
|
|
8
|
+
* Verified mismatches (NOT the same asset despite similar names):
|
|
9
|
+
* USAR ($17) ≠ US500 ($666) ≠ USA500 ($6646)
|
|
10
|
+
* SEMI ($319) ≠ SEMIS ($381)
|
|
11
|
+
* USOIL ($113) ≠ CL ($93)
|
|
12
|
+
* GOLDJM ≠ GOLD (different products)
|
|
13
|
+
* SILVERJM ≠ SILVER
|
|
14
|
+
* URNM ≠ URANIUM
|
|
15
|
+
*/
|
|
16
|
+
export interface DexAsset {
|
|
17
|
+
/** Full symbol as returned by API (e.g., "xyz:TSLA") */
|
|
18
|
+
raw: string;
|
|
19
|
+
/** Base name without prefix (e.g., "TSLA") */
|
|
20
|
+
base: string;
|
|
21
|
+
/** Dex name (e.g., "xyz") or "hl" for native */
|
|
22
|
+
dex: string;
|
|
23
|
+
/** Mark price */
|
|
24
|
+
markPrice: number;
|
|
25
|
+
/** Raw funding rate */
|
|
26
|
+
fundingRate: number;
|
|
27
|
+
/** Max leverage */
|
|
28
|
+
maxLeverage: number;
|
|
29
|
+
/** Open interest (USD notional) */
|
|
30
|
+
openInterest: number;
|
|
31
|
+
/** 24h volume (USD) */
|
|
32
|
+
volume24h: number;
|
|
33
|
+
/** Size decimals for order precision */
|
|
34
|
+
szDecimals: number;
|
|
35
|
+
}
|
|
36
|
+
export interface DexArbPair {
|
|
37
|
+
/** Canonical underlying name */
|
|
38
|
+
underlying: string;
|
|
39
|
+
/** Long side (lower funding rate) */
|
|
40
|
+
long: DexAsset;
|
|
41
|
+
/** Short side (higher funding rate) */
|
|
42
|
+
short: DexAsset;
|
|
43
|
+
/** Annualized funding spread % */
|
|
44
|
+
annualSpread: number;
|
|
45
|
+
/** Mark price difference % between the two sides */
|
|
46
|
+
priceGapPct: number;
|
|
47
|
+
/** Min OI (USD) across both legs — practical cap on position size */
|
|
48
|
+
minOiUsd: number;
|
|
49
|
+
/** Min 24h volume (USD) across both legs — liquidity indicator */
|
|
50
|
+
minVolume24hUsd: number;
|
|
51
|
+
/** Viability grade: "A" (>$1M OI), "B" (>$100K), "C" (>$10K), "D" (<$10K) */
|
|
52
|
+
viability: "A" | "B" | "C" | "D";
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Fetch all assets from all Hyperliquid dexes (native + deployed).
|
|
56
|
+
*/
|
|
57
|
+
export declare function fetchAllDexAssets(): Promise<DexAsset[]>;
|
|
58
|
+
/**
|
|
59
|
+
* Find all cross-dex arb pairs.
|
|
60
|
+
*
|
|
61
|
+
* Groups assets by canonical underlying, then finds pairs across different dexes
|
|
62
|
+
* with a funding rate spread. Validates by checking mark price similarity (< maxGapPct).
|
|
63
|
+
*/
|
|
64
|
+
export declare function findDexArbPairs(assets: DexAsset[], opts?: {
|
|
65
|
+
/** Max price gap % to consider same underlying (default: 5%) */
|
|
66
|
+
maxPriceGapPct?: number;
|
|
67
|
+
/** Min annual spread % to include (default: 0 = all) */
|
|
68
|
+
minAnnualSpread?: number;
|
|
69
|
+
/** Include native HL perps in comparison? (default: true) */
|
|
70
|
+
includeNative?: boolean;
|
|
71
|
+
}): DexArbPair[];
|
|
72
|
+
/**
|
|
73
|
+
* Fetch and return arb opportunities across all HIP-3 dexes.
|
|
74
|
+
* Single convenience function for CLI commands.
|
|
75
|
+
*/
|
|
76
|
+
export declare function scanDexArb(opts?: {
|
|
77
|
+
minAnnualSpread?: number;
|
|
78
|
+
maxPriceGapPct?: number;
|
|
79
|
+
includeNative?: boolean;
|
|
80
|
+
}): Promise<DexArbPair[]>;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HIP-3 Deployed Dex Asset Mapping.
|
|
3
|
+
*
|
|
4
|
+
* Maps assets across Hyperliquid deployed dexes to their common underlying.
|
|
5
|
+
* Assets with the same base name (e.g., xyz:TSLA and flx:TSLA) are auto-matched.
|
|
6
|
+
* Known aliases (e.g., CL = OIL = crude oil) are explicitly mapped.
|
|
7
|
+
*
|
|
8
|
+
* Verified mismatches (NOT the same asset despite similar names):
|
|
9
|
+
* USAR ($17) ≠ US500 ($666) ≠ USA500 ($6646)
|
|
10
|
+
* SEMI ($319) ≠ SEMIS ($381)
|
|
11
|
+
* USOIL ($113) ≠ CL ($93)
|
|
12
|
+
* GOLDJM ≠ GOLD (different products)
|
|
13
|
+
* SILVERJM ≠ SILVER
|
|
14
|
+
* URNM ≠ URANIUM
|
|
15
|
+
*/
|
|
16
|
+
const HL_INFO_URL = "https://api.hyperliquid.xyz/info";
|
|
17
|
+
/** Known aliases: different symbol → same underlying */
|
|
18
|
+
const ALIASES = {
|
|
19
|
+
// Crude oil (WTI benchmark)
|
|
20
|
+
"CL": "CRUDE_OIL_WTI",
|
|
21
|
+
"OIL": "CRUDE_OIL_WTI",
|
|
22
|
+
// Meme token denominations
|
|
23
|
+
"kPEPE": "1000PEPE",
|
|
24
|
+
"1000PEPE": "1000PEPE",
|
|
25
|
+
"kSHIB": "1000SHIB",
|
|
26
|
+
"kBONK": "1000BONK",
|
|
27
|
+
"kFLOKI": "1000FLOKI",
|
|
28
|
+
"kLUNC": "1000LUNC",
|
|
29
|
+
"kNEIRO": "1000NEIRO",
|
|
30
|
+
"kDOGS": "1000DOGS",
|
|
31
|
+
};
|
|
32
|
+
/** Verified price-mismatch pairs — NEVER match these */
|
|
33
|
+
const BLACKLIST_PAIRS = new Set([
|
|
34
|
+
"USAR:US500", "USAR:USA500", "US500:USA500",
|
|
35
|
+
"SEMI:SEMIS",
|
|
36
|
+
"USOIL:CL", "USOIL:OIL",
|
|
37
|
+
"GOLDJM:GOLD", "GLDMINE:GOLD",
|
|
38
|
+
"SILVERJM:SILVER",
|
|
39
|
+
"URNM:URANIUM",
|
|
40
|
+
"NUCLEAR:ENERGY", "NUCLEAR:USENERGY",
|
|
41
|
+
]);
|
|
42
|
+
function isBlacklisted(a, b) {
|
|
43
|
+
const key1 = `${a}:${b}`;
|
|
44
|
+
const key2 = `${b}:${a}`;
|
|
45
|
+
return BLACKLIST_PAIRS.has(key1) || BLACKLIST_PAIRS.has(key2);
|
|
46
|
+
}
|
|
47
|
+
async function hlInfoPost(body) {
|
|
48
|
+
const res = await fetch(HL_INFO_URL, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: { "Content-Type": "application/json" },
|
|
51
|
+
body: JSON.stringify(body),
|
|
52
|
+
});
|
|
53
|
+
if (!res.ok)
|
|
54
|
+
throw new Error(`HL API error: ${res.status}`);
|
|
55
|
+
return res.json();
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Fetch all assets from all Hyperliquid dexes (native + deployed).
|
|
59
|
+
*/
|
|
60
|
+
export async function fetchAllDexAssets() {
|
|
61
|
+
const { withCache, TTL_MARKET } = await import("./cache.js");
|
|
62
|
+
return withCache("pub:hl:allDexAssets", TTL_MARKET, () => _fetchAllDexAssetsLive());
|
|
63
|
+
}
|
|
64
|
+
async function _fetchAllDexAssetsLive() {
|
|
65
|
+
const allMetas = await hlInfoPost({ type: "allPerpMetas" });
|
|
66
|
+
if (!Array.isArray(allMetas))
|
|
67
|
+
return [];
|
|
68
|
+
// Collect dex names (index 0 = native "hl", rest = deployed dexes)
|
|
69
|
+
const dexNames = [];
|
|
70
|
+
for (let i = 0; i < allMetas.length; i++) {
|
|
71
|
+
const universe = (allMetas[i].universe ?? []);
|
|
72
|
+
if (universe.length === 0) {
|
|
73
|
+
dexNames.push("");
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (i === 0) {
|
|
77
|
+
dexNames.push("hl");
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const first = universe[0].name;
|
|
81
|
+
const colon = first.indexOf(":");
|
|
82
|
+
dexNames.push(colon > 0 ? first.slice(0, colon) : `dex-${i}`);
|
|
83
|
+
}
|
|
84
|
+
// Fetch metaAndAssetCtxs for each dex in batches of 3 to avoid 429 rate limits
|
|
85
|
+
const BATCH_SIZE = 3;
|
|
86
|
+
const allAssets = [];
|
|
87
|
+
const tasks = dexNames.map((dex, i) => ({ dex, i })).filter(t => t.dex !== "");
|
|
88
|
+
for (let b = 0; b < tasks.length; b += BATCH_SIZE) {
|
|
89
|
+
const batch = tasks.slice(b, b + BATCH_SIZE);
|
|
90
|
+
const results = await Promise.all(batch.map(async ({ dex, i }) => {
|
|
91
|
+
try {
|
|
92
|
+
const body = i === 0
|
|
93
|
+
? { type: "metaAndAssetCtxs" }
|
|
94
|
+
: { type: "metaAndAssetCtxs", dex };
|
|
95
|
+
const data = await hlInfoPost(body);
|
|
96
|
+
const universe = data[0]?.universe ?? [];
|
|
97
|
+
const ctxs = data[1] ?? [];
|
|
98
|
+
return universe
|
|
99
|
+
.map((asset, j) => {
|
|
100
|
+
if (asset.isDelisted)
|
|
101
|
+
return null;
|
|
102
|
+
const ctx = ctxs[j];
|
|
103
|
+
const raw = asset.name;
|
|
104
|
+
const colon = raw.indexOf(":");
|
|
105
|
+
return {
|
|
106
|
+
raw,
|
|
107
|
+
base: colon > 0 ? raw.slice(colon + 1) : raw,
|
|
108
|
+
dex,
|
|
109
|
+
markPrice: Number(ctx?.markPx ?? 0),
|
|
110
|
+
fundingRate: Number(ctx?.funding ?? 0),
|
|
111
|
+
maxLeverage: asset.maxLeverage,
|
|
112
|
+
openInterest: Number(ctx?.openInterest ?? 0),
|
|
113
|
+
volume24h: Number(ctx?.dayNtlVlm ?? 0),
|
|
114
|
+
szDecimals: asset.szDecimals,
|
|
115
|
+
};
|
|
116
|
+
})
|
|
117
|
+
.filter((a) => a !== null && a.markPrice > 0);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
}));
|
|
123
|
+
allAssets.push(...results.flat());
|
|
124
|
+
}
|
|
125
|
+
return allAssets;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Normalize a base name to its canonical underlying.
|
|
129
|
+
*/
|
|
130
|
+
function toCanonical(base) {
|
|
131
|
+
return ALIASES[base] ?? base;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Find all cross-dex arb pairs.
|
|
135
|
+
*
|
|
136
|
+
* Groups assets by canonical underlying, then finds pairs across different dexes
|
|
137
|
+
* with a funding rate spread. Validates by checking mark price similarity (< maxGapPct).
|
|
138
|
+
*/
|
|
139
|
+
export function findDexArbPairs(assets, opts = {}) {
|
|
140
|
+
const maxGap = opts.maxPriceGapPct ?? 5;
|
|
141
|
+
const minSpread = opts.minAnnualSpread ?? 0;
|
|
142
|
+
const includeNative = opts.includeNative ?? true;
|
|
143
|
+
// Group by canonical underlying
|
|
144
|
+
const groups = new Map();
|
|
145
|
+
for (const asset of assets) {
|
|
146
|
+
if (!includeNative && asset.dex === "hl")
|
|
147
|
+
continue;
|
|
148
|
+
const canonical = toCanonical(asset.base);
|
|
149
|
+
if (!groups.has(canonical))
|
|
150
|
+
groups.set(canonical, []);
|
|
151
|
+
groups.get(canonical).push(asset);
|
|
152
|
+
}
|
|
153
|
+
const pairs = [];
|
|
154
|
+
for (const [underlying, group] of groups) {
|
|
155
|
+
if (group.length < 2)
|
|
156
|
+
continue;
|
|
157
|
+
// Compare all combinations of different dexes
|
|
158
|
+
for (let i = 0; i < group.length; i++) {
|
|
159
|
+
for (let j = i + 1; j < group.length; j++) {
|
|
160
|
+
const a = group[i];
|
|
161
|
+
const b = group[j];
|
|
162
|
+
if (a.dex === b.dex)
|
|
163
|
+
continue; // same dex, skip
|
|
164
|
+
// Blacklist check
|
|
165
|
+
if (isBlacklisted(a.base, b.base))
|
|
166
|
+
continue;
|
|
167
|
+
// Price gap validation
|
|
168
|
+
const avgPrice = (a.markPrice + b.markPrice) / 2;
|
|
169
|
+
const gapPct = Math.abs(a.markPrice - b.markPrice) / avgPrice * 100;
|
|
170
|
+
if (gapPct > maxGap)
|
|
171
|
+
continue;
|
|
172
|
+
// All dexes (including HIP-3 deployed) settle funding every 1h
|
|
173
|
+
const hourlyA = a.fundingRate;
|
|
174
|
+
const hourlyB = b.fundingRate;
|
|
175
|
+
const annualSpread = Math.abs(hourlyA - hourlyB) * 8760 * 100;
|
|
176
|
+
if (annualSpread < minSpread)
|
|
177
|
+
continue;
|
|
178
|
+
// Long the lower-funding side, short the higher-funding side
|
|
179
|
+
const [long, short] = hourlyA < hourlyB ? [a, b] : [b, a];
|
|
180
|
+
// Liquidity metrics: OI in USD, min across both legs
|
|
181
|
+
const longOiUsd = long.openInterest * long.markPrice;
|
|
182
|
+
const shortOiUsd = short.openInterest * short.markPrice;
|
|
183
|
+
const minOiUsd = Math.min(longOiUsd, shortOiUsd);
|
|
184
|
+
const minVolume24hUsd = Math.min(long.volume24h, short.volume24h);
|
|
185
|
+
const viability = minOiUsd >= 1_000_000 ? "A" :
|
|
186
|
+
minOiUsd >= 100_000 ? "B" :
|
|
187
|
+
minOiUsd >= 10_000 ? "C" : "D";
|
|
188
|
+
pairs.push({ underlying, long, short, annualSpread, priceGapPct: gapPct, minOiUsd, minVolume24hUsd, viability });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return pairs.sort((a, b) => b.annualSpread - a.annualSpread);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Fetch and return arb opportunities across all HIP-3 dexes.
|
|
196
|
+
* Single convenience function for CLI commands.
|
|
197
|
+
*/
|
|
198
|
+
export async function scanDexArb(opts) {
|
|
199
|
+
const assets = await fetchAllDexAssets();
|
|
200
|
+
return findDexArbPairs(assets, opts);
|
|
201
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/** All structured error codes for the CLI */
|
|
2
|
+
export declare const ERROR_CODES: {
|
|
3
|
+
readonly INVALID_PARAMS: {
|
|
4
|
+
readonly code: "INVALID_PARAMS";
|
|
5
|
+
readonly status: 400;
|
|
6
|
+
readonly retryable: false;
|
|
7
|
+
};
|
|
8
|
+
readonly SYMBOL_NOT_FOUND: {
|
|
9
|
+
readonly code: "SYMBOL_NOT_FOUND";
|
|
10
|
+
readonly status: 404;
|
|
11
|
+
readonly retryable: false;
|
|
12
|
+
};
|
|
13
|
+
readonly ORDER_NOT_FOUND: {
|
|
14
|
+
readonly code: "ORDER_NOT_FOUND";
|
|
15
|
+
readonly status: 404;
|
|
16
|
+
readonly retryable: false;
|
|
17
|
+
};
|
|
18
|
+
readonly POSITION_NOT_FOUND: {
|
|
19
|
+
readonly code: "POSITION_NOT_FOUND";
|
|
20
|
+
readonly status: 404;
|
|
21
|
+
readonly retryable: false;
|
|
22
|
+
};
|
|
23
|
+
readonly INSUFFICIENT_BALANCE: {
|
|
24
|
+
readonly code: "INSUFFICIENT_BALANCE";
|
|
25
|
+
readonly status: 400;
|
|
26
|
+
readonly retryable: false;
|
|
27
|
+
};
|
|
28
|
+
readonly MARGIN_INSUFFICIENT: {
|
|
29
|
+
readonly code: "MARGIN_INSUFFICIENT";
|
|
30
|
+
readonly status: 400;
|
|
31
|
+
readonly retryable: false;
|
|
32
|
+
};
|
|
33
|
+
readonly SIZE_TOO_SMALL: {
|
|
34
|
+
readonly code: "SIZE_TOO_SMALL";
|
|
35
|
+
readonly status: 400;
|
|
36
|
+
readonly retryable: false;
|
|
37
|
+
};
|
|
38
|
+
readonly SIZE_TOO_LARGE: {
|
|
39
|
+
readonly code: "SIZE_TOO_LARGE";
|
|
40
|
+
readonly status: 400;
|
|
41
|
+
readonly retryable: false;
|
|
42
|
+
};
|
|
43
|
+
readonly RISK_VIOLATION: {
|
|
44
|
+
readonly code: "RISK_VIOLATION";
|
|
45
|
+
readonly status: 403;
|
|
46
|
+
readonly retryable: false;
|
|
47
|
+
};
|
|
48
|
+
readonly DUPLICATE_ORDER: {
|
|
49
|
+
readonly code: "DUPLICATE_ORDER";
|
|
50
|
+
readonly status: 409;
|
|
51
|
+
readonly retryable: false;
|
|
52
|
+
};
|
|
53
|
+
readonly EXCHANGE_UNREACHABLE: {
|
|
54
|
+
readonly code: "EXCHANGE_UNREACHABLE";
|
|
55
|
+
readonly status: 503;
|
|
56
|
+
readonly retryable: true;
|
|
57
|
+
};
|
|
58
|
+
readonly RATE_LIMITED: {
|
|
59
|
+
readonly code: "RATE_LIMITED";
|
|
60
|
+
readonly status: 429;
|
|
61
|
+
readonly retryable: true;
|
|
62
|
+
readonly retryAfterMs: 1000;
|
|
63
|
+
};
|
|
64
|
+
readonly PRICE_STALE: {
|
|
65
|
+
readonly code: "PRICE_STALE";
|
|
66
|
+
readonly status: 503;
|
|
67
|
+
readonly retryable: true;
|
|
68
|
+
};
|
|
69
|
+
readonly SIGNATURE_FAILED: {
|
|
70
|
+
readonly code: "SIGNATURE_FAILED";
|
|
71
|
+
readonly status: 500;
|
|
72
|
+
readonly retryable: false;
|
|
73
|
+
};
|
|
74
|
+
readonly EXCHANGE_ERROR: {
|
|
75
|
+
readonly code: "EXCHANGE_ERROR";
|
|
76
|
+
readonly status: 502;
|
|
77
|
+
readonly retryable: true;
|
|
78
|
+
};
|
|
79
|
+
readonly TIMEOUT: {
|
|
80
|
+
readonly code: "TIMEOUT";
|
|
81
|
+
readonly status: 504;
|
|
82
|
+
readonly retryable: true;
|
|
83
|
+
};
|
|
84
|
+
readonly UNKNOWN: {
|
|
85
|
+
readonly code: "UNKNOWN";
|
|
86
|
+
readonly status: 500;
|
|
87
|
+
readonly retryable: false;
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
export type ErrorCode = keyof typeof ERROR_CODES;
|
|
91
|
+
export interface StructuredError {
|
|
92
|
+
code: ErrorCode;
|
|
93
|
+
message: string;
|
|
94
|
+
status: number;
|
|
95
|
+
retryable: boolean;
|
|
96
|
+
retryAfterMs?: number;
|
|
97
|
+
exchange?: string;
|
|
98
|
+
details?: Record<string, unknown>;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Classify an error from any exchange into a structured error code.
|
|
102
|
+
* Pattern-matches on error messages to detect known error types.
|
|
103
|
+
*/
|
|
104
|
+
export declare function classifyError(err: unknown, exchange?: string): StructuredError;
|
|
105
|
+
/** Custom error class that carries a structured error code */
|
|
106
|
+
export declare class PerpError extends Error {
|
|
107
|
+
readonly structured: StructuredError;
|
|
108
|
+
constructor(code: ErrorCode, message: string, details?: Record<string, unknown>);
|
|
109
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** All structured error codes for the CLI */
|
|
2
|
+
export const ERROR_CODES = {
|
|
3
|
+
// 4xx - Client / user errors
|
|
4
|
+
INVALID_PARAMS: { code: "INVALID_PARAMS", status: 400, retryable: false },
|
|
5
|
+
SYMBOL_NOT_FOUND: { code: "SYMBOL_NOT_FOUND", status: 404, retryable: false },
|
|
6
|
+
ORDER_NOT_FOUND: { code: "ORDER_NOT_FOUND", status: 404, retryable: false },
|
|
7
|
+
POSITION_NOT_FOUND: { code: "POSITION_NOT_FOUND", status: 404, retryable: false },
|
|
8
|
+
INSUFFICIENT_BALANCE: { code: "INSUFFICIENT_BALANCE", status: 400, retryable: false },
|
|
9
|
+
MARGIN_INSUFFICIENT: { code: "MARGIN_INSUFFICIENT", status: 400, retryable: false },
|
|
10
|
+
SIZE_TOO_SMALL: { code: "SIZE_TOO_SMALL", status: 400, retryable: false },
|
|
11
|
+
SIZE_TOO_LARGE: { code: "SIZE_TOO_LARGE", status: 400, retryable: false },
|
|
12
|
+
RISK_VIOLATION: { code: "RISK_VIOLATION", status: 403, retryable: false },
|
|
13
|
+
DUPLICATE_ORDER: { code: "DUPLICATE_ORDER", status: 409, retryable: false },
|
|
14
|
+
// 5xx - System / transient errors
|
|
15
|
+
EXCHANGE_UNREACHABLE: { code: "EXCHANGE_UNREACHABLE", status: 503, retryable: true },
|
|
16
|
+
RATE_LIMITED: { code: "RATE_LIMITED", status: 429, retryable: true, retryAfterMs: 1000 },
|
|
17
|
+
PRICE_STALE: { code: "PRICE_STALE", status: 503, retryable: true },
|
|
18
|
+
SIGNATURE_FAILED: { code: "SIGNATURE_FAILED", status: 500, retryable: false },
|
|
19
|
+
EXCHANGE_ERROR: { code: "EXCHANGE_ERROR", status: 502, retryable: true },
|
|
20
|
+
TIMEOUT: { code: "TIMEOUT", status: 504, retryable: true },
|
|
21
|
+
UNKNOWN: { code: "UNKNOWN", status: 500, retryable: false },
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Classify an error from any exchange into a structured error code.
|
|
25
|
+
* Pattern-matches on error messages to detect known error types.
|
|
26
|
+
*/
|
|
27
|
+
export function classifyError(err, exchange) {
|
|
28
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
29
|
+
const lower = message.toLowerCase();
|
|
30
|
+
// Margin-specific checks first (before generic "insufficient" catch)
|
|
31
|
+
if (lower.includes("margin") && (lower.includes("insufficient") || lower.includes("not enough"))) {
|
|
32
|
+
return { ...ERROR_CODES.MARGIN_INSUFFICIENT, message, exchange };
|
|
33
|
+
}
|
|
34
|
+
if (lower.includes("insufficient") || lower.includes("not enough") || lower.includes("balance")) {
|
|
35
|
+
return { ...ERROR_CODES.INSUFFICIENT_BALANCE, message, exchange };
|
|
36
|
+
}
|
|
37
|
+
if (lower.includes("rate limit") || lower.includes("429") || lower.includes("too many request")) {
|
|
38
|
+
return { ...ERROR_CODES.RATE_LIMITED, message, exchange };
|
|
39
|
+
}
|
|
40
|
+
if (lower.includes("econnrefused") || lower.includes("enotfound") || lower.includes("fetch failed") || lower.includes("network")) {
|
|
41
|
+
return { ...ERROR_CODES.EXCHANGE_UNREACHABLE, message, exchange };
|
|
42
|
+
}
|
|
43
|
+
if (lower.includes("timeout") || lower.includes("timed out") || lower.includes("etimedout")) {
|
|
44
|
+
return { ...ERROR_CODES.TIMEOUT, message, exchange };
|
|
45
|
+
}
|
|
46
|
+
if (lower.includes("not found") && (lower.includes("symbol") || lower.includes("market") || lower.includes("asset"))) {
|
|
47
|
+
return { ...ERROR_CODES.SYMBOL_NOT_FOUND, message, exchange };
|
|
48
|
+
}
|
|
49
|
+
if (lower.includes("order") && lower.includes("not found")) {
|
|
50
|
+
return { ...ERROR_CODES.ORDER_NOT_FOUND, message, exchange };
|
|
51
|
+
}
|
|
52
|
+
if (lower.includes("position") && lower.includes("not found")) {
|
|
53
|
+
return { ...ERROR_CODES.POSITION_NOT_FOUND, message, exchange };
|
|
54
|
+
}
|
|
55
|
+
if (lower.includes("too small") || lower.includes("minimum") || lower.includes("below min")) {
|
|
56
|
+
return { ...ERROR_CODES.SIZE_TOO_SMALL, message, exchange };
|
|
57
|
+
}
|
|
58
|
+
if (lower.includes("too large") || lower.includes("maximum") || lower.includes("exceeds max")) {
|
|
59
|
+
return { ...ERROR_CODES.SIZE_TOO_LARGE, message, exchange };
|
|
60
|
+
}
|
|
61
|
+
if (lower.includes("signature") || lower.includes("signing") || lower.includes("sign")) {
|
|
62
|
+
return { ...ERROR_CODES.SIGNATURE_FAILED, message, exchange };
|
|
63
|
+
}
|
|
64
|
+
if (lower.includes("duplicate") || lower.includes("already exists")) {
|
|
65
|
+
return { ...ERROR_CODES.DUPLICATE_ORDER, message, exchange };
|
|
66
|
+
}
|
|
67
|
+
if (lower.includes("risk") || lower.includes("violation")) {
|
|
68
|
+
return { ...ERROR_CODES.RISK_VIOLATION, message, exchange };
|
|
69
|
+
}
|
|
70
|
+
// Default: exchange error if we know the exchange, unknown otherwise
|
|
71
|
+
if (exchange) {
|
|
72
|
+
return { ...ERROR_CODES.EXCHANGE_ERROR, message, exchange };
|
|
73
|
+
}
|
|
74
|
+
return { ...ERROR_CODES.UNKNOWN, message };
|
|
75
|
+
}
|
|
76
|
+
/** Custom error class that carries a structured error code */
|
|
77
|
+
export class PerpError extends Error {
|
|
78
|
+
structured;
|
|
79
|
+
constructor(code, message, details) {
|
|
80
|
+
super(message);
|
|
81
|
+
this.name = "PerpError";
|
|
82
|
+
this.structured = { ...ERROR_CODES[code], message, details };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified event streaming for agent consumption.
|
|
3
|
+
*
|
|
4
|
+
* Streams normalized account events as NDJSON (one JSON per line).
|
|
5
|
+
* Uses poll-based diffing for cross-exchange compatibility.
|
|
6
|
+
*/
|
|
7
|
+
import type { ExchangeAdapter } from "./exchanges/interface.js";
|
|
8
|
+
export type EventType = "position_opened" | "position_closed" | "position_updated" | "order_placed" | "order_filled" | "order_cancelled" | "balance_update" | "liquidation_warning" | "margin_call" | "heartbeat" | "error";
|
|
9
|
+
export interface StreamEvent {
|
|
10
|
+
type: EventType;
|
|
11
|
+
exchange: string;
|
|
12
|
+
timestamp: string;
|
|
13
|
+
data: Record<string, unknown>;
|
|
14
|
+
riskLevel?: "normal" | "warning" | "critical";
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Start polling-based event stream.
|
|
18
|
+
* Emits NDJSON events to the provided callback.
|
|
19
|
+
*/
|
|
20
|
+
export declare function startEventStream(adapter: ExchangeAdapter, opts: {
|
|
21
|
+
intervalMs?: number;
|
|
22
|
+
liquidationWarningPct?: number;
|
|
23
|
+
onEvent: (event: StreamEvent) => void;
|
|
24
|
+
signal?: AbortSignal;
|
|
25
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified event streaming for agent consumption.
|
|
3
|
+
*
|
|
4
|
+
* Streams normalized account events as NDJSON (one JSON per line).
|
|
5
|
+
* Uses poll-based diffing for cross-exchange compatibility.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Start polling-based event stream.
|
|
9
|
+
* Emits NDJSON events to the provided callback.
|
|
10
|
+
*/
|
|
11
|
+
export async function startEventStream(adapter, opts) {
|
|
12
|
+
const intervalMs = opts.intervalMs ?? 5000;
|
|
13
|
+
const liqWarnPct = opts.liquidationWarningPct ?? 10; // warn when < 10% from liq price
|
|
14
|
+
const emit = opts.onEvent;
|
|
15
|
+
let prevPositions = new Map();
|
|
16
|
+
let prevOrders = new Map();
|
|
17
|
+
let prevBalance = null;
|
|
18
|
+
let cycle = 0;
|
|
19
|
+
const poll = async () => {
|
|
20
|
+
cycle++;
|
|
21
|
+
const ts = new Date().toISOString();
|
|
22
|
+
try {
|
|
23
|
+
const [positions, orders, balance] = await Promise.all([
|
|
24
|
+
adapter.getPositions(),
|
|
25
|
+
adapter.getOpenOrders(),
|
|
26
|
+
adapter.getBalance(),
|
|
27
|
+
]);
|
|
28
|
+
// ── Position diffs ──
|
|
29
|
+
const currentPositions = new Map();
|
|
30
|
+
for (const p of positions) {
|
|
31
|
+
currentPositions.set(p.symbol, {
|
|
32
|
+
symbol: p.symbol,
|
|
33
|
+
side: p.side,
|
|
34
|
+
size: p.size,
|
|
35
|
+
entryPrice: p.entryPrice,
|
|
36
|
+
unrealizedPnl: p.unrealizedPnl,
|
|
37
|
+
liquidationPrice: p.liquidationPrice,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
// New positions
|
|
41
|
+
for (const [sym, pos] of currentPositions) {
|
|
42
|
+
const prev = prevPositions.get(sym);
|
|
43
|
+
if (!prev) {
|
|
44
|
+
emit({ type: "position_opened", exchange: adapter.name, timestamp: ts, data: { ...pos } });
|
|
45
|
+
}
|
|
46
|
+
else if (prev.size !== pos.size || prev.side !== pos.side) {
|
|
47
|
+
emit({ type: "position_updated", exchange: adapter.name, timestamp: ts, data: { ...pos, prevSize: prev.size, prevSide: prev.side } });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Closed positions
|
|
51
|
+
for (const [sym, prev] of prevPositions) {
|
|
52
|
+
if (!currentPositions.has(sym)) {
|
|
53
|
+
emit({ type: "position_closed", exchange: adapter.name, timestamp: ts, data: { ...prev } });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// ── Liquidation warnings ──
|
|
57
|
+
for (const p of positions) {
|
|
58
|
+
const mark = Number(p.markPrice);
|
|
59
|
+
const liq = Number(p.liquidationPrice);
|
|
60
|
+
if (mark > 0 && liq > 0 && p.liquidationPrice !== "N/A") {
|
|
61
|
+
const distancePct = Math.abs(mark - liq) / mark * 100;
|
|
62
|
+
if (distancePct < 3) {
|
|
63
|
+
emit({
|
|
64
|
+
type: "margin_call",
|
|
65
|
+
exchange: adapter.name,
|
|
66
|
+
timestamp: ts,
|
|
67
|
+
riskLevel: "critical",
|
|
68
|
+
data: { symbol: p.symbol, side: p.side, markPrice: mark, liquidationPrice: liq, distancePct: +distancePct.toFixed(2) },
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
else if (distancePct < liqWarnPct) {
|
|
72
|
+
emit({
|
|
73
|
+
type: "liquidation_warning",
|
|
74
|
+
exchange: adapter.name,
|
|
75
|
+
timestamp: ts,
|
|
76
|
+
riskLevel: "warning",
|
|
77
|
+
data: { symbol: p.symbol, side: p.side, markPrice: mark, liquidationPrice: liq, distancePct: +distancePct.toFixed(2) },
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
prevPositions = currentPositions;
|
|
83
|
+
// ── Order diffs ──
|
|
84
|
+
const currentOrders = new Map();
|
|
85
|
+
for (const o of orders) {
|
|
86
|
+
currentOrders.set(o.orderId, {
|
|
87
|
+
orderId: o.orderId,
|
|
88
|
+
symbol: o.symbol,
|
|
89
|
+
side: o.side,
|
|
90
|
+
price: o.price,
|
|
91
|
+
size: o.size,
|
|
92
|
+
status: o.status,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// New orders
|
|
96
|
+
for (const [id, order] of currentOrders) {
|
|
97
|
+
if (!prevOrders.has(id)) {
|
|
98
|
+
emit({ type: "order_placed", exchange: adapter.name, timestamp: ts, data: { ...order } });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Removed orders (filled or cancelled)
|
|
102
|
+
for (const [id, prev] of prevOrders) {
|
|
103
|
+
if (!currentOrders.has(id)) {
|
|
104
|
+
// Can't distinguish filled vs cancelled from diff alone; mark as filled if position changed
|
|
105
|
+
const posChanged = currentPositions.get(prev.symbol)?.size !== prevPositions.get(prev.symbol)?.size;
|
|
106
|
+
const type = posChanged ? "order_filled" : "order_cancelled";
|
|
107
|
+
emit({ type, exchange: adapter.name, timestamp: ts, data: { ...prev } });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
prevOrders = currentOrders;
|
|
111
|
+
// ── Balance updates ──
|
|
112
|
+
if (prevBalance) {
|
|
113
|
+
const equityDelta = Math.abs(Number(balance.equity) - Number(prevBalance.equity));
|
|
114
|
+
const availDelta = Math.abs(Number(balance.available) - Number(prevBalance.available));
|
|
115
|
+
if (equityDelta > 0.01 || availDelta > 0.01) {
|
|
116
|
+
emit({
|
|
117
|
+
type: "balance_update",
|
|
118
|
+
exchange: adapter.name,
|
|
119
|
+
timestamp: ts,
|
|
120
|
+
data: {
|
|
121
|
+
equity: balance.equity,
|
|
122
|
+
available: balance.available,
|
|
123
|
+
marginUsed: balance.marginUsed,
|
|
124
|
+
unrealizedPnl: balance.unrealizedPnl,
|
|
125
|
+
prevEquity: prevBalance.equity,
|
|
126
|
+
prevAvailable: prevBalance.available,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
prevBalance = balance;
|
|
132
|
+
// Heartbeat every 12 cycles (~60s at 5s interval)
|
|
133
|
+
if (cycle % 12 === 0) {
|
|
134
|
+
emit({
|
|
135
|
+
type: "heartbeat",
|
|
136
|
+
exchange: adapter.name,
|
|
137
|
+
timestamp: ts,
|
|
138
|
+
data: {
|
|
139
|
+
cycle,
|
|
140
|
+
positions: positions.length,
|
|
141
|
+
openOrders: orders.length,
|
|
142
|
+
equity: balance.equity,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch (err) {
|
|
148
|
+
emit({
|
|
149
|
+
type: "error",
|
|
150
|
+
exchange: adapter.name,
|
|
151
|
+
timestamp: ts,
|
|
152
|
+
data: { message: err instanceof Error ? err.message : String(err), cycle },
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
// Initial poll
|
|
157
|
+
await poll();
|
|
158
|
+
// Polling loop
|
|
159
|
+
while (!opts.signal?.aborted) {
|
|
160
|
+
await new Promise((resolve) => {
|
|
161
|
+
const timer = setTimeout(resolve, intervalMs);
|
|
162
|
+
opts.signal?.addEventListener("abort", () => { clearTimeout(timer); resolve(); }, { once: true });
|
|
163
|
+
});
|
|
164
|
+
if (opts.signal?.aborted)
|
|
165
|
+
break;
|
|
166
|
+
await poll();
|
|
167
|
+
}
|
|
168
|
+
}
|